summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/tools/third_party
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /testing/web-platform/tests/tools/third_party
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/tools/third_party')
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/.gitignore9
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/.travis.yml35
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/CONTRIBUTING.rst11
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/LICENSE19
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/MANIFEST.in6
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/Makefile2
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/README.rst102
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/appveyor.yml18
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/atomicwrites/__init__.py201
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/docs/Makefile177
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/docs/conf.py107
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/docs/index.rst35
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/docs/make.bat242
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/setup.cfg2
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/setup.py27
-rw-r--r--testing/web-platform/tests/tools/third_party/atomicwrites/tox.ini11
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/.github/CODE_OF_CONDUCT.md133
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/.github/CONTRIBUTING.md230
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/.github/FUNDING.yml5
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/.github/PULL_REQUEST_TEMPLATE.md34
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/.github/SECURITY.md2
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/.github/workflows/main.yml113
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/.gitignore13
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/.pre-commit-config.yaml43
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/.readthedocs.yml16
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/AUTHORS.rst11
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/CHANGELOG.rst1027
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/LICENSE21
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/MANIFEST.in24
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/README.rst135
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/changelog.d/.gitignore0
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/changelog.d/towncrier_template.rst35
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/conftest.py29
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/Makefile177
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/_static/attrs_logo.pngbin0 -> 7639 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/_static/attrs_logo.svg10
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/_static/attrs_logo_white.svg10
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/api.rst826
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/changelog.rst1
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/comparison.rst66
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/conf.py155
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/docutils.conf3
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/examples.rst709
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/extending.rst313
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/glossary.rst104
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/hashing.rst86
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/how-does-it-work.rst109
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/index.rst100
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/init.rst489
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/license.rst8
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/names.rst122
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/overview.rst58
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/python-2.rst25
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/types.rst108
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/docs/why.rst290
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/mypy.ini3
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/pyproject.toml71
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/setup.py151
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/__init__.py80
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/__init__.pyi484
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/_cmp.py154
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/_cmp.pyi13
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/_compat.py261
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/_config.py33
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/_funcs.py422
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/_make.py3173
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/_next_gen.py216
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/_version_info.py87
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/_version_info.pyi9
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/converters.py155
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/converters.pyi13
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/exceptions.py94
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/exceptions.pyi17
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/filters.py54
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/filters.pyi6
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/py.typed0
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/setters.py79
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/setters.pyi19
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/validators.py561
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attr/validators.pyi78
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attrs/__init__.py70
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attrs/__init__.pyi63
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attrs/converters.py3
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attrs/exceptions.py3
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attrs/filters.py3
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attrs/py.typed0
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attrs/setters.py3
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/src/attrs/validators.py3
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/__init__.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/attr_import_star.py10
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/dataclass_transform_example.py45
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/strategies.py198
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_3rd_party.py31
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_annotations.py671
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_cmp.py510
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_compat.py52
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_config.py45
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_converters.py163
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_dunders.py1008
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_filters.py111
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_funcs.py680
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_functional.py790
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_hooks.py209
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_import.py11
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_init_subclass.py48
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_make.py2462
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_mypy.yml1395
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_next_gen.py440
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_pattern_matching.py101
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_pyright.py71
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_setattr.py437
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_slots.py740
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_validators.py952
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/test_version_info.py62
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/typing_example.py420
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tests/utils.py86
-rw-r--r--testing/web-platform/tests/tools/third_party/attrs/tox.ini129
-rw-r--r--testing/web-platform/tests/tools/third_party/certifi/LICENSE21
-rw-r--r--testing/web-platform/tests/tools/third_party/certifi/MANIFEST.in1
-rw-r--r--testing/web-platform/tests/tools/third_party/certifi/PKG-INFO69
-rw-r--r--testing/web-platform/tests/tools/third_party/certifi/README.rst46
-rw-r--r--testing/web-platform/tests/tools/third_party/certifi/certifi/__init__.py3
-rw-r--r--testing/web-platform/tests/tools/third_party/certifi/certifi/__main__.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/certifi/certifi/cacert.pem4400
-rw-r--r--testing/web-platform/tests/tools/third_party/certifi/certifi/core.py37
-rw-r--r--testing/web-platform/tests/tools/third_party/certifi/setup.cfg11
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/certifi/setup.py67
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/.coveragerc6
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/.gitignore19
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/.travis.yml18
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/CHANGELOG24
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/LICENSE13
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/MANIFEST.in7
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/Makefile39
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/README.rst353
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/docs/Makefile153
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/docs/_templates/page.html9
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/docs/conf.py251
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/funcsigs/__init__.py829
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/funcsigs/version.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/requirements/development.txt5
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/setup.cfg2
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/setup.py52
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/tests/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/tests/test_formatannotation.py17
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/tests/test_funcsigs.py91
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/tests/test_inspect.py1002
-rw-r--r--testing/web-platform/tests/tools/third_party/funcsigs/tox.ini8
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/.coveragerc18
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/CONTRIBUTORS.rst115
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/HISTORY.rst760
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/LICENSE21
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/MANIFEST.in8
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/Makefile9
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/README.rst65
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/Makefile177
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/make.bat242
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/_static/.keep0
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/_static/h2.connection.H2ConnectionStateMachine.dot.pngbin0 -> 714520 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/_static/h2.stream.H2StreamStateMachine.dot.pngbin0 -> 1856508 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/advanced-usage.rst325
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/api.rst169
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/asyncio-example.rst17
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/basic-usage.rst746
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/conf.py270
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/contributors.rst4
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/curio-example.rst17
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/eventlet-example.rst19
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/examples.rst28
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/index.rst41
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/installation.rst18
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/low-level.rst159
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/negotiating-http2.rst103
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/release-notes.rst101
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/release-process.rst56
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/testimonials.rst9
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/tornado-example.rst16
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/twisted-example.rst18
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/twisted-head-example.rst17
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/twisted-post-example.rst18
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/docs/source/wsgi-example.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/asyncio/asyncio-server.py210
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/asyncio/cert.crt21
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/asyncio/cert.key27
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/asyncio/wsgi-server.py760
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/curio/curio-server.py206
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/curio/localhost.crt.pem21
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/curio/localhost.key27
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/eventlet/eventlet-server.py102
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/eventlet/server.crt20
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/eventlet/server.key27
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/fragments/client_https_setup_fragment.py110
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/fragments/client_upgrade_fragment.py103
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/fragments/server_https_setup_fragment.py112
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/fragments/server_upgrade_fragment.py100
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/tornado/server.crt20
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/tornado/server.key27
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/h2/examples/tornado/tornado-server.py92
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/twisted/head_request.py111
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/twisted/post_request.py249
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/twisted/server.crt20
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/twisted/server.csr17
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/twisted/server.key27
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/examples/twisted/twisted-server.py192
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/h2/__init__.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/h2/config.py170
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/h2/connection.py2048
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/h2/errors.py75
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/h2/events.py648
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/h2/exceptions.py186
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/h2/frame_buffer.py175
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/h2/settings.py339
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/h2/stream.py1369
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/h2/utilities.py660
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/h2/windows.py139
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/setup.cfg10
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/setup.py76
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/conftest.py16
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/coroutine_tests.py74
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/helpers.py176
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_basic_logic.py1877
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_closed_streams.py555
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_complex_logic.py586
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_config.py130
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_events.py367
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_exceptions.py15
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_flow_control_window.py952
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_h2_upgrade.py302
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_head_request.py55
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_header_indexing.py637
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_informational_responses.py444
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_interacting_stacks.py120
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_invalid_content_lengths.py136
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_invalid_frame_sequences.py488
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_invalid_headers.py952
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_priority.py358
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_related_events.py370
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_rfc7838.py447
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_settings.py470
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_state_machines.py163
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_stream_reset.py137
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test/test_utility_functions.py226
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/test_requirements.txt5
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/tox.ini48
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/h2/utils/backport.sh31
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/visualizer/NOTICES.visualizer24
-rw-r--r--testing/web-platform/tests/tools/third_party/h2/visualizer/visualize.py252
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/CONTRIBUTORS.rst62
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/HISTORY.rst134
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/LICENSE21
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/MANIFEST.in2
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/PKG-INFO199
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/README.rst41
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/hpack/__init__.py20
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/hpack/compat.py42
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/hpack/exceptions.py49
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/hpack/hpack.py629
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/hpack/huffman.py68
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/hpack/huffman_constants.py288
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/hpack/huffman_table.py4739
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/hpack/struct.py39
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/hpack/table.py215
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/setup.cfg12
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/setup.py57
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/test/test_encode_decode.py141
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/test/test_hpack.py828
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/test/test_hpack_integration.py75
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/test/test_huffman.py55
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/test/test_struct.py77
-rw-r--r--testing/web-platform/tests/tools/third_party/hpack/test/test_table.py158
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/.appveyor.yml31
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/.coveragerc8
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/.gitignore85
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/.prospector.yaml21
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/.pylintrc10
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/.pytest.expect1322
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/.travis.yml32
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/AUTHORS.rst66
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/CHANGES.rst359
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/CONTRIBUTING.rst60
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/LICENSE20
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/MANIFEST.in10
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/README.rst151
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/bench_html.py57
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/bench_wpt.py45
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/README.md8
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/html.html5000
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/LICENSE.md11
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/README.md52
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/001.html3
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/background-origin-007-ref.html18
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/background_shorthand_css_relative_url.html24
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/beforeunload-on-history-back-1.html5
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/euckr-encode-form.html52
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/frame-ancestors-self-allow.html16
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/grouping-dl.html30
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/heavy-styling-005.html15
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/htb-ltr-ltr.html74
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/idbindex_get8.htm27
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/idlharness.html34
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/li-type-unsupported-ref.html13
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/moz-css21-float-page-break-inside-avoid-6.html19
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/shape-outside-content-box-002.html66
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/worker-constructor.https.html86
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/2d.composite.image.destination-over.html33
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/align-content-wrap-002.html108
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/big5_chars_extra.html1
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/fetch.http.html143
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/filter-turbulence-invalid-001.html51
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/grid-auto-fill-rows-001.html184
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/image-orientation-from-image-content-images-ref.html86
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/masonry-item-placement-006.html149
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/moz-css21-table-page-break-inside-avoid-2.html29
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/position-sticky-table-th-bottom-ref.html62
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/pre-float-001.html36
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/resize-004.html20
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/test-plan.src.html1616
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/toBlob.png.html17
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/will-change-abspos-cb-001.html30
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/debug-info.py37
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/doc/Makefile177
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/doc/changes.rst3
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/doc/conf.py123
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.filters.rst58
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.rst38
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.treeadapters.rst20
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.treebuilders.rst42
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.treewalkers.rst50
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/doc/index.rst22
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/doc/license.rst4
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/doc/make.bat242
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/doc/modules.rst7
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/doc/movingparts.rst165
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/html5lib/flake8-run.sh9
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/__init__.py35
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/_ihatexml.py289
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/_inputstream.py918
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/_tokenizer.py1735
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/_trie/__init__.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/_trie/_base.py40
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/_trie/py.py67
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/_utils.py159
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/constants.py2946
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/alphabeticalattributes.py29
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/base.py12
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/inject_meta_charset.py73
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/lint.py93
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/optionaltags.py207
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/sanitizer.py916
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/whitespace.py38
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/html5parser.py2795
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/serializer.py409
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/__init__.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/conftest.py108
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/sanitizer-testdata/tests1.dat433
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/sanitizer.py51
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/core.test395
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/injectmeta.test350
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/optionaltags.test3254
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/options.test334
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/whitespace.test198
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/support.py199
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_alphabeticalattributes.py78
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_encoding.py117
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_meta.py41
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_optionaltags_filter.py7
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_parser2.py94
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_sanitizer.py133
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_serializer.py226
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_stream.py325
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_tokenizer2.py66
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_treeadapters.py40
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_treewalkers.py205
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_whitespace_filter.py125
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/tokenizer.py253
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/tokenizertotree.py69
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/tree_construction.py205
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/us-ascii.html3
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/utf-8-bom.html3
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treeadapters/__init__.py30
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treeadapters/genshi.py54
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treeadapters/sax.py50
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/__init__.py88
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/base.py417
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/dom.py239
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/etree.py343
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/etree_lxml.py392
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/__init__.py154
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/base.py252
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/dom.py43
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/etree.py131
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/etree_lxml.py215
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/genshi.py69
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/html5lib/parse.py236
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/pytest.ini17
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/html5lib/requirements-install.sh15
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/requirements-optional.txt13
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/requirements-test.txt10
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/requirements.txt2
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/setup.cfg11
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/setup.py127
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/tox.ini20
-rw-r--r--testing/web-platform/tests/tools/third_party/html5lib/utils/entities.py101
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/CONTRIBUTORS.rst56
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/HISTORY.rst179
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/LICENSE21
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/MANIFEST.in2
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/PKG-INFO242
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/README.rst39
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/__init__.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/exceptions.py41
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/flags.py50
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/frame.py822
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/setup.cfg10
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/setup.py59
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/test/test_flags.py35
-rw-r--r--testing/web-platform/tests/tools/third_party/hyperframe/test/test_frames.py791
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/.github/workflows/main.yml126
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/.gitignore13
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/.readthedocs.yml5
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/LICENSE13
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/MANIFEST.in5
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/README.rst42
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/codecov.yml2
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/coverage.ini24
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/coverplug.py21
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/docs/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/docs/changelog.rst314
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/docs/conf.py185
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/docs/index.rst52
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/docs/using.rst260
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/importlib_metadata/__init__.py627
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/importlib_metadata/_compat.py152
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/prepare/example/example/__init__.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/prepare/example/setup.py10
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/pyproject.toml2
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/setup.cfg47
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/setup.py3
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/tests/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/tests/data/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/tests/data/example-21.12-py3-none-any.whlbin0 -> 1455 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/tests/data/example-21.12-py3.6.eggbin0 -> 1497 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/tests/fixtures.py263
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/tests/py39compat.py4
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_api.py196
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_integration.py54
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_main.py285
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_zip.py80
-rw-r--r--testing/web-platform/tests/tools/third_party/importlib_metadata/tox.ini97
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/.gitignore8
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/.landscape.yml5
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/.travis.yml18
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/CHANGELOG32
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/LICENSE19
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/MANIFEST.in5
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/README.txt51
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/example.ini10
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/pyproject.toml5
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/setup.cfg2
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/setup.py46
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/src/iniconfig/__init__.py165
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/src/iniconfig/__init__.pyi31
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/src/iniconfig/py.typed0
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/testing/conftest.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/testing/test_iniconfig.py314
-rw-r--r--testing/web-platform/tests/tools/third_party/iniconfig/tox.ini14
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/.gitignore34
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/.travis.yml26
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/LICENSE19
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/MANIFEST.in8
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/README.rst59
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/docs/Makefile153
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/docs/api.rst234
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/docs/conf.py244
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/docs/index.rst16
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/docs/license.rst16
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/docs/make.bat190
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/docs/testing.rst19
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/docs/versions.rst237
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/__init__.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/more.py2068
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/recipes.py565
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/tests/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/tests/test_more.py1848
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/tests/test_recipes.py607
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/setup.cfg3
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/setup.py59
-rw-r--r--testing/web-platform/tests/tools/third_party/more-itertools/tox.ini5
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/.coveragerc9
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/.flake83
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/.github/workflows/docs.yml30
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/.github/workflows/lint.yml59
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/.github/workflows/test.yml56
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/.gitignore18
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/.pre-commit-config.yaml39
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/.readthedocs.yml15
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/CHANGELOG.rst347
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/CONTRIBUTING.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/LICENSE3
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/LICENSE.APACHE177
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/LICENSE.BSD23
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/MANIFEST.in24
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/README.rst73
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/Makefile153
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/_static/.empty0
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/changelog.rst1
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/conf.py111
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/development/getting-started.rst77
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/development/index.rst19
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/development/release-process.rst25
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/development/reviewing-patches.rst37
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/development/submitting-patches.rst74
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/index.rst38
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/markers.rst93
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/requirements.rst89
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/requirements.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/security.rst18
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/specifiers.rst222
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/tags.rst225
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/utils.rst92
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/docs/version.rst292
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/mypy.ini17
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/noxfile.py321
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/packaging/__about__.py26
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/packaging/__init__.py25
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/packaging/_manylinux.py301
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/packaging/_musllinux.py136
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/packaging/_structures.py61
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/packaging/markers.py304
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/packaging/py.typed0
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/packaging/requirements.py146
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/packaging/specifiers.py802
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/packaging/tags.py487
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/packaging/utils.py136
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/packaging/version.py504
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/pyproject.toml3
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/setup.cfg3
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/setup.py70
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tasks/__init__.py9
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tasks/check.py141
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tasks/paths.py9
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tasks/requirements.txt3
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/__init__.py3
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/hello-world.c7
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/packaging/tests/manylinux/build.sh39
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-armv7l-armelbin0 -> 52 bytes
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-armv7l-armhfbin0 -> 52 bytes
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-invalid-classbin0 -> 52 bytes
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-invalid-databin0 -> 52 bytes
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-invalid-magicbin0 -> 52 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-s390x-s390xbin0 -> 64 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-too-shortbin0 -> 40 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-x86_64-amd64bin0 -> 64 bytes
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-x86_64-i386bin0 -> 52 bytes
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-x86_64-x32bin0 -> 52 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/musllinux/build.sh61
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/test_manylinux.py253
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/test_markers.py310
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/test_musllinux.py146
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/test_requirements.py197
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/test_specifiers.py998
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/test_structures.py59
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/test_tags.py1191
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/test_utils.py124
-rw-r--r--testing/web-platform/tests/tools/third_party/packaging/tests/test_version.py904
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/.gitignore54
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/.travis.yml47
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/CHANGELOG.rst163
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/LICENSE.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/MANIFEST.in10
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/README.rst66
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/VERSION1
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/appveyor.yml30
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/appveyor/install.ps144
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/codecov.yml1
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/pathlib2/__init__.py1809
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/requirements.txt3
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/setup.cfg8
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/setup.py48
-rw-r--r--testing/web-platform/tests/tools/third_party/pathlib2/tests/test_pathlib2.py2406
-rw-r--r--testing/web-platform/tests/tools/third_party/pdf_js/LICENSE177
-rw-r--r--testing/web-platform/tests/tools/third_party/pdf_js/pdf.js24624
-rw-r--r--testing/web-platform/tests/tools/third_party/pdf_js/pdf.worker.js56199
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/.coveragerc14
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/.github/workflows/main.yml148
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/.gitignore64
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/.pre-commit-config.yaml34
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/CHANGELOG.rst409
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/LICENSE21
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/MANIFEST.in7
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/README.rst101
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/RELEASING.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/changelog/README.rst32
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/changelog/_template.rst40
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/codecov.yml7
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/_static/img/plug.pngbin0 -> 9350 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/api_reference.rst19
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/changelog.rst1
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/conf.py87
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/eggsample_spam.py22
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/setup.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/__init__.py4
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/hookspecs.py21
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/host.py57
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/lib.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/setup.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/toy-example.py41
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/index.rst957
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/pyproject.toml47
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/scripts/release.py69
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pluggy/scripts/upload-coverage.sh16
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/setup.cfg52
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/setup.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/__init__.py18
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_callers.py60
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_hooks.py325
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_manager.py373
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_result.py60
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_tracing.py62
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/benchmark.py102
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/conftest.py26
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_details.py135
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_helpers.py84
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_hookcaller.py272
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_invocations.py215
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_multicall.py147
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_pluginmanager.py544
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_tracer.py78
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/tox.ini57
-rw-r--r--testing/web-platform/tests/tools/third_party/py/.flake84
-rw-r--r--testing/web-platform/tests/tools/third_party/py/.github/workflows/main.yml66
-rw-r--r--testing/web-platform/tests/tools/third_party/py/.gitignore15
-rw-r--r--testing/web-platform/tests/tools/third_party/py/AUTHORS25
-rw-r--r--testing/web-platform/tests/tools/third_party/py/CHANGELOG.rst1236
-rw-r--r--testing/web-platform/tests/tools/third_party/py/LICENSE19
-rw-r--r--testing/web-platform/tests/tools/third_party/py/MANIFEST.in11
-rw-r--r--testing/web-platform/tests/tools/third_party/py/README.rst31
-rw-r--r--testing/web-platform/tests/tools/third_party/py/RELEASING.rst17
-rw-r--r--testing/web-platform/tests/tools/third_party/py/bench/localpath.py73
-rw-r--r--testing/web-platform/tests/tools/third_party/py/codecov.yml7
-rw-r--r--testing/web-platform/tests/tools/third_party/py/conftest.py60
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/Makefile133
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/_templates/layout.html18
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-0.9.0.txt7
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-0.9.2.txt27
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.0.0.txt63
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.0.1.txt48
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.0.2.txt5
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.1.0.txt115
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.1.1.txt48
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.2.0.txt116
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.2.1.txt66
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.0.txt580
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.1.txt104
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.2.txt720
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.3.txt26
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.4.txt22
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.4.0.txt47
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.4.1.txt47
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/announce/releases.txt16
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/changelog.txt3
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/code.txt150
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/conf.py263
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/download.html18
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/example/genhtml.py13
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/example/genhtmlcss.py23
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/example/genxml.py17
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/faq.txt170
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/img/pylib.pngbin0 -> 8276 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/index.txt39
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/install.txt91
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/io.txt59
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/links.inc15
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/log.txt208
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/misc.txt93
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/path.txt264
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/style.css1044
-rw-r--r--testing/web-platform/tests/tools/third_party/py/doc/xml.txt164
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/__init__.py156
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/__init__.pyi20
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/__metainfo.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_builtin.py149
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_code/__init__.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_code/_assertionnew.py322
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_code/_assertionold.py556
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_code/_py2traceback.py79
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_code/assertion.py90
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_code/code.py796
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_code/source.py410
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_error.py91
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_io/__init__.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_io/capture.py371
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_io/saferepr.py71
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_io/terminalwriter.py423
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_log/__init__.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_log/log.py206
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_log/warning.py79
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_path/__init__.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_path/cacheutil.py114
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_path/common.py459
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_path/local.py1030
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_path/svnurl.py380
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_path/svnwc.py1240
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_process/__init__.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_process/cmdexec.py49
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_process/forkedfunc.py120
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_process/killproc.py23
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_std.py27
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/INSTALLER1
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/LICENSE18
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/METADATA125
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/RECORD11
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/REQUESTED0
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/WHEEL6
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/top_level.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg/__init__.py217
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg/version.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/INSTALLER1
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/LICENSE19
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/METADATA78
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/RECORD11
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/REQUESTED0
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/WHEEL6
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/top_level.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.py165
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.pyi31
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig/py.typed0
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/_xmlgen.py255
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/error.pyi129
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/iniconfig.pyi31
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/io.pyi130
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/path.pyi197
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/py.typed0
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/test.py10
-rw-r--r--testing/web-platform/tests/tools/third_party/py/py/xml.pyi25
-rw-r--r--testing/web-platform/tests/tools/third_party/py/pyproject.toml6
-rw-r--r--testing/web-platform/tests/tools/third_party/py/setup.cfg8
-rw-r--r--testing/web-platform/tests/tools/third_party/py/setup.py48
-rw-r--r--testing/web-platform/tests/tools/third_party/py/tasks/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/py/tasks/vendoring.py41
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/code/test_assertion.py305
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/code/test_code.py159
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/code/test_excinfo.py956
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/code/test_source.py656
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/conftest.py3
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/io_/__init__.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/io_/test_capture.py501
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/io_/test_saferepr.py75
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/io_/test_terminalwriter.py341
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/io_/test_terminalwriter_linewidth.py56
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/log/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/log/test_log.py191
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/log/test_warning.py85
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/path/common.py492
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/path/conftest.py80
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/path/repotest.dump228
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/path/svntestbase.py31
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/path/test_cacheutil.py89
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/path/test_local.py1078
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/path/test_svnauth.py460
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/path/test_svnurl.py95
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/path/test_svnwc.py557
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/process/__init__.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/process/test_cmdexec.py41
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/process/test_forkedfunc.py173
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/process/test_killproc.py18
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/root/__init__.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/root/test_builtin.py156
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/root/test_error.py76
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/root/test_py_imports.py71
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/root/test_std.py13
-rw-r--r--testing/web-platform/tests/tools/third_party/py/testing/root/test_xmlgen.py146
-rw-r--r--testing/web-platform/tests/tools/third_party/py/tox.ini44
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/CHANGELOG.rst169
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/LICENSE201
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/MANIFEST.in5
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/Makefile39
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/PKG-INFO285
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/README.rst259
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/default/constraints.txt24
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/default/requirements.txt4
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/pytest-min/constraints.txt22
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/pytest-min/requirements.txt4
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/pyproject.toml10
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/PKG-INFO285
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/SOURCES.txt51
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/dependency_links.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/entry_points.txt2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/requires.txt11
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/top_level.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/__init__.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/_version.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/plugin.py546
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/py.typed0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/setup.cfg73
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_fixtures.py25
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_fixtures_scope.py25
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_fixtures_with_finalizer.py59
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_gen_fixtures.py38
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_nested.py26
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_parametrized_loop.py31
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/conftest.py32
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/hypothesis/test_base.py88
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/hypothesis/test_inherited_test.py20
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/loop_fixture_scope/conftest.py17
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/loop_fixture_scope/test_loop_fixture_scope.py16
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/markers/test_class_marker.py25
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/markers/test_module_marker.py39
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/modes/test_auto_mode.py139
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/modes/test_legacy_mode.py112
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/modes/test_strict_mode.py68
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/multiloop/conftest.py15
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/multiloop/test_alternative_loops.py16
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/respect_event_loop_policy/conftest.py16
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/respect_event_loop_policy/test_respects_event_loop_policy.py17
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_asyncio_fixture.py64
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_dependent_fixtures.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_event_loop_scope.py37
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_flaky_integration.py43
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_simple.py275
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_subprocess.py36
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/trio/test_fixtures.py25
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tools/get-version.py17
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest-asyncio/tox.ini56
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.coveragerc31
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.gitblameignore28
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.github/FUNDING.yml5
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.github/ISSUE_TEMPLATE/1_bug_report.md16
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.github/ISSUE_TEMPLATE/2_feature_request.md25
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.github/ISSUE_TEMPLATE/config.yml5
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.github/PULL_REQUEST_TEMPLATE.md26
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.github/config.yml2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.github/dependabot.yml11
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.github/labels.toml149
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.github/workflows/main.yml231
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.github/workflows/prepare-release-pr.yml52
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.github/workflows/update-plugin-list.yml49
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.gitignore58
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.pre-commit-config.yaml99
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/.readthedocs.yml19
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/AUTHORS356
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/CHANGELOG.rst7
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/CITATION16
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/CODE_OF_CONDUCT.md83
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/CONTRIBUTING.rst481
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/LICENSE21
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/OPENCOLLECTIVE.rst44
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/README.rst167
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/RELEASING.rst173
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/TIDELIFT.rst60
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/bench/bench.py13
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/bench/bench_argcomplete.py19
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/bench/empty.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/bench/manyparam.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/bench/skip.py9
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/bench/unit_test.py13
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/bench/xunit.py11
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/changelog/README.rst37
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/changelog/_template.rst40
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/codecov.yml6
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/Makefile43
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/globaltoc.html34
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/layout.html52
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/links.html7
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/relations.html19
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/sidebarintro.html5
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/slim_searchbox.html15
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/adopt.rst78
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/index.rst154
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.0.rst129
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.1.rst67
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.2.rst73
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.3.rst39
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.0.rst47
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.1.rst36
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.2.rst32
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.3.rst32
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.0.rst95
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.1.rst41
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.2.rst43
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.4.rst38
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.0.rst133
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.1.rst39
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.2.rst57
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.3.rst61
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.4.rst39
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.5.rst96
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.4.0.rst223
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.4.1.rst25
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.4.2.rst39
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.5.0.rst174
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.5.1.rst46
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.5.2.rst63
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.0.rst153
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.1.rst58
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.2.rst51
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.3.rst51
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.7.0.rst100
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.7.1.rst58
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.7.2.rst57
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.2.rst44
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.3.rst58
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.4.rst52
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.5.rst39
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.6.rst67
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.7.rst31
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.9.0.rst134
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.9.1.rst57
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.9.2.rst65
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.0.rst82
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.1.rst26
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.2.rst24
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.3.rst27
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.4.rst29
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.5.rst27
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.6.rst33
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.7.rst33
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.0.rst61
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.1.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.2.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.3.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.10.0.rst43
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.10.1.rst24
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.0.rst48
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.1.rst22
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.2.rst28
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.3.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.4.rst36
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.5.rst18
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.3.0.rst50
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.3.1.rst25
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.3.2.rst28
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.4.0.rst52
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.4.1.rst27
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.4.2.rst28
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.5.0.rst51
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.5.1.rst30
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.0.rst41
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.1.rst24
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.2.rst29
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.3.rst27
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.4.rst24
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.0.rst41
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.1.rst21
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.2.rst25
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.3.rst32
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.4.rst22
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.8.0.rst38
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.8.1.rst25
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.8.2.rst28
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.0.rst43
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.1.rst20
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.2.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.3.rst24
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.0.0.rst30
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.0.1.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.0.2.rst24
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.1.0.rst44
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.1.1.rst27
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.2.0.rst37
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.2.1.rst30
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.3.0.rst36
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.3.1.rst28
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.4.0.rst39
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.4.1.rst20
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.4.2.rst33
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.5.0.rst34
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.0.rst43
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.1.rst19
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.2.rst18
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.3.rst21
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.4.rst22
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.5.rst21
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.6.rst20
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.7.rst19
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.8.rst20
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.9.rst21
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.0.0.rst46
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.0.1.rst25
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.0.rst56
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.1.rst24
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.2.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.3.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.0.rst35
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.1.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.2.rst29
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.3.rst28
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.4.rst22
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.0.rst45
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.1.rst26
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.2.rst26
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.3.rst30
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.4.rst20
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.5.rst19
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.0.rst59
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.1.rst18
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.2.rst22
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.3.rst21
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.0.rst40
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.0rc1.rst67
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.1.rst21
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.2.rst19
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.1.0.rst44
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.1.1.rst18
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.1.2.rst22
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.0.rst76
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.1.rst20
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.2.rst21
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.3.rst19
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.4.rst22
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.5.rst30
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-7.0.0.rst74
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-7.0.0rc1.rst74
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-7.0.1.rst20
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/sprint2016.rst64
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/backwards-compatibility.rst79
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/builtin.rst197
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/changelog.rst9044
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/conf.py478
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/conftest.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/contact.rst54
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/contents.rst116
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/contributing.rst3
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/deprecations.rst954
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/development_guide.rst7
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/failure_demo.py281
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/global_testmodule_config/conftest.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/global_testmodule_config/test_hello_world.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/test_failures.py13
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/test_setup_flow_example.py44
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/attic.rst83
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/conftest.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/fixture_availability.svg132
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/fixture_availability_plugins.svg142
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse.py45
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse.svg64
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_flat.svg56
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.py31
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.svg76
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.py36
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.svg100
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies.py45
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies.svg60
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies_flat.svg51
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies_unclear.svg60
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_scope.py36
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_scope.svg55
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_request_different_scope.py29
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_request_different_scope.svg115
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/index.rst34
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/markers.rst734
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/multipython.py72
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython.rst102
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython/conftest.py47
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython/test_simple.yaml7
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/parametrize.rst708
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/pythoncollection.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/pythoncollection.rst321
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/reportingdemo.rst708
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/simple.rst1086
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/special.rst84
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/example/xfail_demo.py38
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/anatomy.rst46
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/fixtures.rst174
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/flaky.rst126
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/goodpractices.rst288
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/index.rst15
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/pythonpath.rst133
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/funcarg_compare.rst230
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/funcargs.rst13
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/getting-started.rst257
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/historical-notes.rst312
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/history.rst145
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/assert.rst336
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/bash-completion.rst33
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/cache.rst329
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/capture-stdout-stderr.rst170
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/capture-warnings.rst443
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/doctest.rst312
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/existingtestsuite.rst34
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/failures.rst160
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/fixtures.rst1887
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/index.rst64
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/logging.rst292
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/mark.rst93
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/monkeypatch.rst444
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/nose.rst79
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/output.rst710
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/parametrize.rst298
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/plugins.rst136
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/skipping.rst430
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/tmp_path.rst139
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/unittest.rst251
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/usage.rst214
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/writing_hook_functions.rst352
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/writing_plugins.rst458
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/xunit_setup.rst117
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/img/cramer2.pngbin0 -> 25291 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/img/favicon.pngbin0 -> 1334 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/img/freiburg2.jpgbin0 -> 104057 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/img/gaynor3.pngbin0 -> 23032 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/img/keleshev.pngbin0 -> 23246 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pullrequest.pngbin0 -> 17035 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pylib.pngbin0 -> 8276 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pytest1.pngbin0 -> 6010 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pytest_logo_curves.svg29
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/img/theuni.pngbin0 -> 31476 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/index.rst148
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/license.rst32
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/naming20.rst20
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/proposals/parametrize_with_fixtures.rst164
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/py27-py34-deprecation.rst99
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/pytest.ini2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/recwarn.rst3
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/customize.rst248
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/exit-codes.rst26
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/fixtures.rst455
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/index.rst15
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/plugin_list.rst7728
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/reference.rst2101
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/requirements.txt7
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/sponsor.rst26
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/talks.rst109
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/tidelift.rst45
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/doc/en/yieldfixture.rst18
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/extra/get_issues.py85
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/extra/setup-py.test/setup.py11
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/pyproject.toml116
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/scripts/prepare-release-pr.py174
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/scripts/publish-gh-release-notes.py102
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/scripts/release.major.rst24
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/scripts/release.minor.rst24
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/scripts/release.patch.rst17
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/scripts/release.pre.rst29
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/scripts/release.py131
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/scripts/towncrier-draft-to-file.py15
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/scripts/update-plugin-list.py140
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/setup.cfg105
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/setup.py4
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/__init__.py9
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_argcomplete.py117
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_code/__init__.py22
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_code/code.py1274
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_code/source.py217
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/__init__.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/saferepr.py153
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/terminalwriter.py233
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/wcwidth.py55
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_version.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/__init__.py181
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/rewrite.py1136
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/truncate.py94
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/util.py498
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/cacheprovider.py580
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/capture.py942
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/compat.py417
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/__init__.py1697
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/argparsing.py535
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/compat.py71
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/exceptions.py11
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/findpaths.py213
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/debugging.py388
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/deprecated.py155
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/doctest.py734
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/faulthandler.py97
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/fixtures.py1686
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/freeze_support.py44
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/helpconfig.py264
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/hookspec.py928
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/junitxml.py696
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/legacypath.py467
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/logging.py831
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/main.py896
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/mark/__init__.py282
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/mark/expression.py225
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/mark/structures.py595
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/monkeypatch.py383
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/nodes.py762
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/nose.py42
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/outcomes.py307
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pastebin.py110
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pathlib.py724
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/py.typed0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pytester.py1748
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pytester_assertions.py75
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/python.py1764
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/python_api.py961
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/python_path.py24
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/recwarn.py296
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/reports.py598
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/runner.py548
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/scope.py91
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/setuponly.py97
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/setupplan.py40
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/skipping.py296
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/stash.py112
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/stepwise.py122
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/terminal.py1394
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/threadexception.py88
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/timing.py12
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/tmpdir.py211
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/unittest.py414
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/unraisableexception.py93
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/warning_types.py145
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/_pytest/warnings.py141
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/pytest/__init__.py171
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/pytest/__main__.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/pytest/collect.py38
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/src/pytest/py.typed0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/acceptance_test.py1297
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/code/test_code.py212
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/code/test_excinfo.py1470
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/code/test_source.py656
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/conftest.py216
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/deprecated_test.py310
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/README.rst9
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/acceptance/fixture_mock_integration.py16
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/pytest.ini2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/__init__.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/test_foo.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/__init__.pyi0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/conftest.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/tests/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/test_foo.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/__init__.pyi0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/conftest.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/test_foo.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/conftest_usageerror/__init__.pyi0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/conftest_usageerror/conftest.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py44
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py19
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/__main__.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/test_normal_module.py6
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/__init__.pyi0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/conftest.py15
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/foo/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/foo/test_foo.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py7
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/test_in_sub1.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py6
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/test_in_sub2.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_detect_recursive_dependency_error.py15
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/__init__.pyi0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/conftest.py6
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/conftest.py6
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/test_spam.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/__init__.pyi0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/conftest.py6
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/test_extend_fixture_conftest_module.py10
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_module_class.py15
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_basic.py15
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_classlevel.py10
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_modulelevel.py15
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookupfails.py10
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_fixture_named_request.py10
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py20
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/__init__.pyi0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/conftest.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/test_hello.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue_519.py53
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/junit-10.xsd147
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/__init__.pyi0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/conftest.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/test_marks_as_keywords.py6
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/.gitignore1
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/generate_folders.py27
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/template_test.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/pytest.ini2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/tmpdir/tmp_path_fixture.py7
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_parametrized_fixture_error_message.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip.py13
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_class.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_module.py12
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asyncio.py25
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asynctest.py23
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_plain_async.py6
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message.py21
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py21
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/examples/test_issue519.py7
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/freeze/.gitignore3
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/freeze/create_executable.py11
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/freeze/runtests_script.py10
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/freeze/tests/test_doctest.txt6
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/freeze/tests/test_trivial.py6
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/freeze/tox_run.py12
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/io/test_saferepr.py181
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/io/test_terminalwriter.py293
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/io/test_wcwidth.py38
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/logging/test_fixture.py310
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/logging/test_formatter.py173
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/logging/test_reporting.py1167
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/.gitignore2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/README.rst13
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/bdd_wallet.feature9
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/bdd_wallet.py39
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/django_settings.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest.ini5
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_anyio_integration.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_asyncio_integration.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_mock_integration.py2
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_trio_integration.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_twisted_integration.py18
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/requirements.txt15
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/simple_integration.py10
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/python/approx.py872
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/python/collect.py1493
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/python/fixtures.py4474
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/python/integration.py503
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/python/metafunc.py1907
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/python/raises.py298
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/python/show_fixtures_per_test.py254
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_argcomplete.py95
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_assertion.py1685
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_assertrewrite.py1841
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_cacheprovider.py1251
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_capture.py1666
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_collection.py1506
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_compat.py265
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_config.py2115
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_conftest.py696
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_debugging.py1327
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_doctest.py1572
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_entry_points.py7
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_error_diffs.py283
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_faulthandler.py172
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_findpaths.py135
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_helpconfig.py124
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_junitxml.py1703
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_legacypath.py180
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_link_resolve.py80
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_main.py264
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_mark.py1130
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_mark_expression.py195
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_meta.py32
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_monkeypatch.py455
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_nodes.py167
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_nose.py498
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_parseopt.py344
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_pastebin.py184
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_pathlib.py574
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_pluginmanager.py427
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_pytester.py855
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_python_path.py110
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_recwarn.py410
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_reports.py488
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_runner.py1061
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_runner_xunit.py297
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_scope.py39
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_session.py369
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_setuponly.py318
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_setupplan.py120
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_skipping.py1533
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_stash.py67
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_stepwise.py280
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_terminal.py2486
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_threadexception.py137
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_tmpdir.py480
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_unittest.py1500
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_unraisableexception.py133
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_warning_types.py38
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/test_warnings.py775
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/testing/typing_checks.py24
-rw-r--r--testing/web-platform/tests/tools/third_party/pytest/tox.ini184
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore4
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml17
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING30
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/LICENSE28
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in6
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/README.md36
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py43
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py43
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/arraybuffer_benchmark.html134
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py59
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html175
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.js238
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py84
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/example/cgi-bin/hi.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py70
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/console.html317
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py54
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py699
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_noext_wsh.py62
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_wsh.py55
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/handler_map.txt11
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/hsts_wsh.py40
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py42
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/origin_check_wsh.py44
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.html37
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js86
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi26
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/util.js323
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js89
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js44
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/__init__.py172
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/_stream_exceptions.py82
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/common.py273
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/dispatch.py385
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/extensions.py474
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/fast_masking.i98
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/__init__.py101
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/base.py396
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/hybi.py223
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/http_header_util.py254
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/memorizingfile.py99
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/msgutil.py214
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/request_handler.py319
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/server_util.py87
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/standalone.py481
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/stream.py950
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/util.py386
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/websocket_server.py285
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/setup.py73
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cacert.pem17
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cert.pem61
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/client_cert.p12bin0 -> 2582 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/key.pem15
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py726
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py227
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py88
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py41
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py298
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py738
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py192
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py172
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py422
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py93
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py100
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py137
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py912
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py70
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py191
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README1
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py41
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/blank_wsh.py30
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/origin_check_wsh.py43
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/exception_in_transfer_wsh.py42
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/no_wsh_at_the_end.py43
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/non_callable_wsh.py35
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/plain_wsh.py41
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py43
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py43
-rw-r--r--testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/hello.pl32
-rw-r--r--testing/web-platform/tests/tools/third_party/six/CHANGES315
-rw-r--r--testing/web-platform/tests/tools/third_party/six/LICENSE18
-rw-r--r--testing/web-platform/tests/tools/third_party/six/MANIFEST.in6
-rw-r--r--testing/web-platform/tests/tools/third_party/six/README.rst32
-rw-r--r--testing/web-platform/tests/tools/third_party/six/documentation/Makefile130
-rw-r--r--testing/web-platform/tests/tools/third_party/six/documentation/conf.py217
-rw-r--r--testing/web-platform/tests/tools/third_party/six/documentation/index.rst875
-rw-r--r--testing/web-platform/tests/tools/third_party/six/setup.cfg20
-rw-r--r--testing/web-platform/tests/tools/third_party/six/setup.py58
-rw-r--r--testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/INSTALLER1
-rw-r--r--testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/LICENSE18
-rw-r--r--testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/METADATA49
-rw-r--r--testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/RECORD8
-rw-r--r--testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/WHEEL6
-rw-r--r--testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/top_level.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/six/six.py982
-rw-r--r--testing/web-platform/tests/tools/third_party/six/test_six.py1052
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/tooltool/tooltool.py1704
-rw-r--r--testing/web-platform/tests/tools/third_party/typing_extensions/LICENSE254
-rw-r--r--testing/web-platform/tests/tools/third_party/typing_extensions/PKG-INFO35
-rw-r--r--testing/web-platform/tests/tools/third_party/typing_extensions/pyproject.toml63
-rw-r--r--testing/web-platform/tests/tools/third_party/typing_extensions/src/typing_extensions.py2908
-rw-r--r--testing/web-platform/tests/tools/third_party/webencodings/PKG-INFO50
-rw-r--r--testing/web-platform/tests/tools/third_party/webencodings/README.rst25
-rw-r--r--testing/web-platform/tests/tools/third_party/webencodings/setup.cfg14
-rw-r--r--testing/web-platform/tests/tools/third_party/webencodings/setup.py47
-rw-r--r--testing/web-platform/tests/tools/third_party/webencodings/webencodings/__init__.py342
-rw-r--r--testing/web-platform/tests/tools/third_party/webencodings/webencodings/labels.py231
-rw-r--r--testing/web-platform/tests/tools/third_party/webencodings/webencodings/mklabels.py59
-rw-r--r--testing/web-platform/tests/tools/third_party/webencodings/webencodings/tests.py153
-rw-r--r--testing/web-platform/tests/tools/third_party/webencodings/webencodings/x_user_defined.py325
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.appveyor.yml27
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.circleci/config.yml55
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.github/FUNDING.yml1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.gitignore12
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.readthedocs.yml7
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/.travis.yml36
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md46
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/LICENSE25
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/MANIFEST.in2
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/Makefile29
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/README.rst154
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/compliance/README.rst50
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingclient.json11
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingserver.json12
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/compliance/test_client.py49
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/compliance/test_server.py27
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/Makefile160
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.pngbin0 -> 4069 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/_static/websockets.svg31
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/api.rst152
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/changelog.rst563
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/cheatsheet.rst109
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/conf.py272
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/contributing.rst61
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/deployment.rst162
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/design.rst571
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/extensions.rst87
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/faq.rst261
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/index.rst99
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/intro.rst209
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/license.rst4
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/lifecycle.grafflebin0 -> 3134 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/lifecycle.svg3
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/limitations.rst10
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/protocol.grafflebin0 -> 4740 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/protocol.svg3
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt4
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/security.rst39
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt39
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/tidelift.rst112
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/basic_auth_client.py14
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/basic_auth_server.py20
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/client.py19
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/counter.html80
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/counter.py69
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/echo.py13
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/health_check_server.py22
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/hello.py12
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/localhost.pem48
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/secure_client.py27
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/secure_server.py28
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/server.py20
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/show_time.html20
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/show_time.py19
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/shutdown.py22
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/unix_client.py19
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/websockets/example/unix_server.py22
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/logo/horizontal.svg31
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/logo/icon.svg15
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/logo/old.svg14
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/logo/tidelift.pngbin0 -> 4069 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/logo/vertical.svg31
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/performance/mem_client.py54
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/performance/mem_server.py63
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/setup.cfg30
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/setup.py66
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py55
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py206
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py160
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py584
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py366
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py119
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py588
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/framing.py342
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/handshake.py185
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/headers.py515
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py360
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py1429
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/py.typed0
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py996
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.c206
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py49
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py81
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/utils.py18
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/__init__.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/extensions/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_base.py4
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py792
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_auth.py139
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_client_server.py1546
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_exceptions.py145
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_exports.py22
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_framing.py242
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_handshake.py190
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_headers.py185
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_http.py249
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.cnf26
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.pem48
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_protocol.py1475
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_uri.py33
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/test_utils.py92
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tests/utils.py93
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/tox.ini28
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/.flake89
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/.github/workflows/main.yml42
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/.pre-commit-config.yaml5
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/.readthedocs.yml5
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/.travis.yml28
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/CHANGES.rst100
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/LICENSE7
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/PKG-INFO39
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/README.rst21
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/appveyor.yml24
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/conftest.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/docs/conf.py26
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/docs/history.rst8
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/docs/index.rst22
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/mypy.ini2
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/pyproject.toml6
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/pytest.ini9
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/setup.cfg45
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/setup.py6
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/skeleton.md137
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/test_zipp.py245
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/tox.ini36
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/PKG-INFO39
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/SOURCES.txt24
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/dependency_links.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/requires.txt14
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/top_level.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/zipp/zipp.py286
1622 files changed, 400062 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/.gitignore b/testing/web-platform/tests/tools/third_party/atomicwrites/.gitignore
new file mode 100644
index 0000000000..2b2d312875
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/.gitignore
@@ -0,0 +1,9 @@
+.tox
+*.pyc
+*.pyo
+__pycache__
+*.egg-info
+docs/_build
+build
+dist
+.cache
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/.travis.yml b/testing/web-platform/tests/tools/third_party/atomicwrites/.travis.yml
new file mode 100644
index 0000000000..e9779018ad
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/.travis.yml
@@ -0,0 +1,35 @@
+sudo: false
+os: linux
+language: python
+
+matrix:
+ include:
+ - os: osx
+ language: generic
+ env: TOXENV_SUFFIX=test
+
+python:
+ - "2.6"
+ - "2.7"
+ - "pypy"
+ - "3.3"
+ - "3.4"
+
+install:
+ - # The OS X VM doesn't have any Python support at all
+ # See https://github.com/travis-ci/travis-ci/issues/2312
+ if [ "$TRAVIS_OS_NAME" = "osx" ]; then
+ brew update;
+ brew install python3;
+ virtualenv -p python3 $HOME/osx-py3;
+ . $HOME/osx-py3/bin/activate;
+ export TRAVIS_PYTHON_VERSION="$(python --version | cut -d ' ' -f 2 | cut -d . -f -2)";
+ fi
+ - pip install tox
+
+script:
+ - export TOX_PY="$(echo py$TRAVIS_PYTHON_VERSION | tr -d . | sed -e 's/pypypy/pypy/')"
+ - tox -e $TOX_PY-test
+ - if [ "$TRAVIS_PYTHON_VERSION" = "2.7" ] || [ "$TRAVIS_PYTHON_VERSION" = ".3.5" ]; then
+ tox -e $TOX_PY-stylecheck;
+ fi
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/CONTRIBUTING.rst b/testing/web-platform/tests/tools/third_party/atomicwrites/CONTRIBUTING.rst
new file mode 100644
index 0000000000..86d3e4a65e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/CONTRIBUTING.rst
@@ -0,0 +1,11 @@
+Thanks for contributing to python-atomicwrites! This document is a
+work-in-progress. Below are a few notes that are useful for writing patches.
+
+Running the tests
+=================
+
+::
+
+ pip install tox
+ tox -e py-test
+ tox -e py-stylecheck
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/LICENSE b/testing/web-platform/tests/tools/third_party/atomicwrites/LICENSE
new file mode 100644
index 0000000000..3bbadc3af2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2015-2016 Markus Unterwaditzer
+
+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/testing/web-platform/tests/tools/third_party/atomicwrites/MANIFEST.in b/testing/web-platform/tests/tools/third_party/atomicwrites/MANIFEST.in
new file mode 100644
index 0000000000..1b28469174
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/MANIFEST.in
@@ -0,0 +1,6 @@
+include LICENSE
+include README.rst
+
+recursive-include docs *
+recursive-include tests *
+prune docs/_build
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/Makefile b/testing/web-platform/tests/tools/third_party/atomicwrites/Makefile
new file mode 100644
index 0000000000..d257e7b673
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/Makefile
@@ -0,0 +1,2 @@
+release:
+ python setup.py sdist bdist_wheel upload
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/README.rst b/testing/web-platform/tests/tools/third_party/atomicwrites/README.rst
new file mode 100644
index 0000000000..3a5658cbd8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/README.rst
@@ -0,0 +1,102 @@
+===================
+python-atomicwrites
+===================
+
+.. image:: https://travis-ci.org/untitaker/python-atomicwrites.svg?branch=master
+ :target: https://travis-ci.org/untitaker/python-atomicwrites
+
+.. image:: https://ci.appveyor.com/api/projects/status/vadc4le3c27to59x/branch/master?svg=true
+ :target: https://ci.appveyor.com/project/untitaker/python-atomicwrites/branch/master
+
+Atomic file writes.
+
+.. code-block:: python
+
+ from atomicwrites import atomic_write
+
+ with atomic_write('foo.txt', overwrite=True) as f:
+ f.write('Hello world.')
+ # "foo.txt" doesn't exist yet.
+
+ # Now it does.
+
+
+Features that distinguish it from other similar libraries (see `Alternatives and Credit`_):
+
+- Race-free assertion that the target file doesn't yet exist. This can be
+ controlled with the ``overwrite`` parameter.
+
+- Windows support, although not well-tested. The MSDN resources are not very
+ explicit about which operations are atomic.
+
+- Simple high-level API that wraps a very flexible class-based API.
+
+- Consistent error handling across platforms.
+
+
+How it works
+============
+
+It uses a temporary file in the same directory as the given path. This ensures
+that the temporary file resides on the same filesystem.
+
+The temporary file will then be atomically moved to the target location: On
+POSIX, it will use ``rename`` if files should be overwritten, otherwise a
+combination of ``link`` and ``unlink``. On Windows, it uses MoveFileEx_ through
+stdlib's ``ctypes`` with the appropriate flags.
+
+Note that with ``link`` and ``unlink``, there's a timewindow where the file
+might be available under two entries in the filesystem: The name of the
+temporary file, and the name of the target file.
+
+Also note that the permissions of the target file may change this way. In some
+situations a ``chmod`` can be issued without any concurrency problems, but
+since that is not always the case, this library doesn't do it by itself.
+
+.. _MoveFileEx: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365240%28v=vs.85%29.aspx
+
+fsync
+-----
+
+On POSIX, ``fsync`` is invoked on the temporary file after it is written (to
+flush file content and metadata), and on the parent directory after the file is
+moved (to flush filename).
+
+``fsync`` does not take care of disks' internal buffers, but there don't seem
+to be any standard POSIX APIs for that. On OS X, ``fcntl`` is used with
+``F_FULLFSYNC`` instead of ``fsync`` for that reason.
+
+On Windows, `_commit <https://msdn.microsoft.com/en-us/library/17618685.aspx>`_
+is used, but there are no guarantees about disk internal buffers.
+
+Alternatives and Credit
+=======================
+
+Atomicwrites is directly inspired by the following libraries (and shares a
+minimal amount of code):
+
+- The Trac project's `utility functions
+ <http://www.edgewall.org/docs/tags-trac-0.11.7/epydoc/trac.util-pysrc.html>`_,
+ also used in `Werkzeug <http://werkzeug.pocoo.org/>`_ and
+ `mitsuhiko/python-atomicfile
+ <https://github.com/mitsuhiko/python-atomicfile>`_. The idea to use
+ ``ctypes`` instead of ``PyWin32`` originated there.
+
+- `abarnert/fatomic <https://github.com/abarnert/fatomic>`_. Windows support
+ (based on ``PyWin32``) was originally taken from there.
+
+Other alternatives to atomicwrites include:
+
+- `sashka/atomicfile <https://github.com/sashka/atomicfile>`_. Originally I
+ considered using that, but at the time it was lacking a lot of features I
+ needed (Windows support, overwrite-parameter, overriding behavior through
+ subclassing).
+
+- The `Boltons library collection <https://github.com/mahmoud/boltons>`_
+ features a class for atomic file writes, which seems to have a very similar
+ ``overwrite`` parameter. It is lacking Windows support though.
+
+License
+=======
+
+Licensed under the MIT, see ``LICENSE``.
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/appveyor.yml b/testing/web-platform/tests/tools/third_party/atomicwrites/appveyor.yml
new file mode 100644
index 0000000000..a5d47a0768
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/appveyor.yml
@@ -0,0 +1,18 @@
+build: false # Not a C# project, build stuff at the test step instead.
+environment:
+ matrix:
+ - PYTHON: "C:/Python27"
+ - PYTHON: "C:/Python33"
+ - PYTHON: "C:/Python34"
+
+init:
+ - "ECHO %PYTHON%"
+ - ps: "ls C:/Python*"
+
+install:
+ - ps: (new-object net.webclient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', 'C:/get-pip.py')
+ - "%PYTHON%/python.exe C:/get-pip.py"
+ - "%PYTHON%/Scripts/pip.exe install tox"
+
+test_script:
+ - "%PYTHON%/Scripts/tox.exe -e py-test"
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/atomicwrites/__init__.py b/testing/web-platform/tests/tools/third_party/atomicwrites/atomicwrites/__init__.py
new file mode 100644
index 0000000000..a182c07afd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/atomicwrites/__init__.py
@@ -0,0 +1,201 @@
+import contextlib
+import os
+import sys
+import tempfile
+
+try:
+ import fcntl
+except ImportError:
+ fcntl = None
+
+__version__ = '1.1.5'
+
+
+PY2 = sys.version_info[0] == 2
+
+text_type = unicode if PY2 else str # noqa
+
+
+def _path_to_unicode(x):
+ if not isinstance(x, text_type):
+ return x.decode(sys.getfilesystemencoding())
+ return x
+
+
+_proper_fsync = os.fsync
+
+
+if sys.platform != 'win32':
+ if hasattr(fcntl, 'F_FULLFSYNC'):
+ def _proper_fsync(fd):
+ # https://lists.apple.com/archives/darwin-dev/2005/Feb/msg00072.html
+ # https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man2/fsync.2.html
+ # https://github.com/untitaker/python-atomicwrites/issues/6
+ fcntl.fcntl(fd, fcntl.F_FULLFSYNC)
+
+ def _sync_directory(directory):
+ # Ensure that filenames are written to disk
+ fd = os.open(directory, 0)
+ try:
+ _proper_fsync(fd)
+ finally:
+ os.close(fd)
+
+ def _replace_atomic(src, dst):
+ os.rename(src, dst)
+ _sync_directory(os.path.normpath(os.path.dirname(dst)))
+
+ def _move_atomic(src, dst):
+ os.link(src, dst)
+ os.unlink(src)
+
+ src_dir = os.path.normpath(os.path.dirname(src))
+ dst_dir = os.path.normpath(os.path.dirname(dst))
+ _sync_directory(dst_dir)
+ if src_dir != dst_dir:
+ _sync_directory(src_dir)
+else:
+ from ctypes import windll, WinError
+
+ _MOVEFILE_REPLACE_EXISTING = 0x1
+ _MOVEFILE_WRITE_THROUGH = 0x8
+ _windows_default_flags = _MOVEFILE_WRITE_THROUGH
+
+ def _handle_errors(rv):
+ if not rv:
+ raise WinError()
+
+ def _replace_atomic(src, dst):
+ _handle_errors(windll.kernel32.MoveFileExW(
+ _path_to_unicode(src), _path_to_unicode(dst),
+ _windows_default_flags | _MOVEFILE_REPLACE_EXISTING
+ ))
+
+ def _move_atomic(src, dst):
+ _handle_errors(windll.kernel32.MoveFileExW(
+ _path_to_unicode(src), _path_to_unicode(dst),
+ _windows_default_flags
+ ))
+
+
+def replace_atomic(src, dst):
+ '''
+ Move ``src`` to ``dst``. If ``dst`` exists, it will be silently
+ overwritten.
+
+ Both paths must reside on the same filesystem for the operation to be
+ atomic.
+ '''
+ return _replace_atomic(src, dst)
+
+
+def move_atomic(src, dst):
+ '''
+ Move ``src`` to ``dst``. There might a timewindow where both filesystem
+ entries exist. If ``dst`` already exists, :py:exc:`FileExistsError` will be
+ raised.
+
+ Both paths must reside on the same filesystem for the operation to be
+ atomic.
+ '''
+ return _move_atomic(src, dst)
+
+
+class AtomicWriter(object):
+ '''
+ A helper class for performing atomic writes. Usage::
+
+ with AtomicWriter(path).open() as f:
+ f.write(...)
+
+ :param path: The destination filepath. May or may not exist.
+ :param mode: The filemode for the temporary file.
+ :param overwrite: If set to false, an error is raised if ``path`` exists.
+ Errors are only raised after the file has been written to. Either way,
+ the operation is atomic.
+
+ If you need further control over the exact behavior, you are encouraged to
+ subclass.
+ '''
+
+ def __init__(self, path, mode='w', overwrite=False):
+ if 'a' in mode:
+ raise ValueError(
+ 'Appending to an existing file is not supported, because that '
+ 'would involve an expensive `copy`-operation to a temporary '
+ 'file. Open the file in normal `w`-mode and copy explicitly '
+ 'if that\'s what you\'re after.'
+ )
+ if 'x' in mode:
+ raise ValueError('Use the `overwrite`-parameter instead.')
+ if 'w' not in mode:
+ raise ValueError('AtomicWriters can only be written to.')
+
+ self._path = path
+ self._mode = mode
+ self._overwrite = overwrite
+
+ def open(self):
+ '''
+ Open the temporary file.
+ '''
+ return self._open(self.get_fileobject)
+
+ @contextlib.contextmanager
+ def _open(self, get_fileobject):
+ f = None # make sure f exists even if get_fileobject() fails
+ try:
+ success = False
+ with get_fileobject() as f:
+ yield f
+ self.sync(f)
+ self.commit(f)
+ success = True
+ finally:
+ if not success:
+ try:
+ self.rollback(f)
+ except Exception:
+ pass
+
+ def get_fileobject(self, dir=None, **kwargs):
+ '''Return the temporary file to use.'''
+ if dir is None:
+ dir = os.path.normpath(os.path.dirname(self._path))
+ return tempfile.NamedTemporaryFile(mode=self._mode, dir=dir,
+ delete=False, **kwargs)
+
+ def sync(self, f):
+ '''responsible for clearing as many file caches as possible before
+ commit'''
+ f.flush()
+ _proper_fsync(f.fileno())
+
+ def commit(self, f):
+ '''Move the temporary file to the target location.'''
+ if self._overwrite:
+ replace_atomic(f.name, self._path)
+ else:
+ move_atomic(f.name, self._path)
+
+ def rollback(self, f):
+ '''Clean up all temporary resources.'''
+ os.unlink(f.name)
+
+
+def atomic_write(path, writer_cls=AtomicWriter, **cls_kwargs):
+ '''
+ Simple atomic writes. This wraps :py:class:`AtomicWriter`::
+
+ with atomic_write(path) as f:
+ f.write(...)
+
+ :param path: The target path to write to.
+ :param writer_cls: The writer class to use. This parameter is useful if you
+ subclassed :py:class:`AtomicWriter` to change some behavior and want to
+ use that new subclass.
+
+ Additional keyword arguments are passed to the writer class. See
+ :py:class:`AtomicWriter`.
+ '''
+ return writer_cls(path, **cls_kwargs).open()
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/docs/Makefile b/testing/web-platform/tests/tools/third_party/atomicwrites/docs/Makefile
new file mode 100644
index 0000000000..af5f9d9aa5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/docs/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+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 " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @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)/*
+
+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/atomicwrites.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/atomicwrites.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/atomicwrites"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/atomicwrites"
+ @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."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @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."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+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."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/docs/conf.py b/testing/web-platform/tests/tools/third_party/atomicwrites/docs/conf.py
new file mode 100644
index 0000000000..b7c7b59c4d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/docs/conf.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import pkg_resources
+
+extensions = [
+ 'sphinx.ext.autodoc',
+ '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 = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'atomicwrites'
+copyright = '2015, Markus Unterwaditzer'
+
+try:
+ # The full version, including alpha/beta/rc tags.
+ release = pkg_resources.require('atomicwrites')[0].version
+except pkg_resources.DistributionNotFound:
+ print('To build the documentation, the distribution information of '
+ 'atomicwrites has to be available. Run "setup.py develop" to do '
+ 'this.')
+ sys.exit(1)
+
+version = '.'.join(release.split('.')[:2]) # The short X.Y version.
+
+on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+
+try:
+ import sphinx_rtd_theme
+ html_theme = 'sphinx_rtd_theme'
+ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+except ImportError:
+ html_theme = 'default'
+ if not on_rtd:
+ print('-' * 74)
+ print('Warning: sphinx-rtd-theme not installed, building with default '
+ 'theme.')
+ print('-' * 74)
+
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# 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']
+
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'atomicwritesdoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ ('index', 'atomicwrites.tex', 'atomicwrites Documentation',
+ 'Markus Unterwaditzer', 'manual'),
+]
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'atomicwrites', 'atomicwrites Documentation',
+ ['Markus Unterwaditzer'], 1)
+]
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'atomicwrites', 'atomicwrites Documentation',
+ 'Markus Unterwaditzer', 'atomicwrites', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Bibliographic Dublin Core info.
+epub_title = 'atomicwrites'
+epub_author = 'Markus Unterwaditzer'
+epub_publisher = 'Markus Unterwaditzer'
+epub_copyright = '2015, Markus Unterwaditzer'
+
+# A list of files that should not be packed into the epub file.
+epub_exclude_files = ['search.html']
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'http://docs.python.org/': None}
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/docs/index.rst b/testing/web-platform/tests/tools/third_party/atomicwrites/docs/index.rst
new file mode 100644
index 0000000000..0391c04477
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/docs/index.rst
@@ -0,0 +1,35 @@
+.. include:: ../README.rst
+
+.. module:: atomicwrites
+
+API
+===
+
+.. autofunction:: atomic_write
+
+
+Errorhandling
+-------------
+
+All filesystem errors are subclasses of :py:exc:`OSError`.
+
+- On UNIX systems, errors from the Python stdlib calls are thrown.
+- On Windows systems, errors from Python's ``ctypes`` are thrown.
+
+In either case, the ``errno`` attribute on the thrown exception maps to an
+errorcode in the ``errno`` module.
+
+Low-level API
+-------------
+
+.. autofunction:: replace_atomic
+
+.. autofunction:: move_atomic
+
+.. autoclass:: AtomicWriter
+ :members:
+
+License
+=======
+
+.. include:: ../LICENSE
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/docs/make.bat b/testing/web-platform/tests/tools/third_party/atomicwrites/docs/make.bat
new file mode 100644
index 0000000000..36fd3f6baf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/docs/make.bat
@@ -0,0 +1,242 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :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. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\atomicwrites.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\atomicwrites.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+:end
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/setup.cfg b/testing/web-platform/tests/tools/third_party/atomicwrites/setup.cfg
new file mode 100644
index 0000000000..5e4090017a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/setup.cfg
@@ -0,0 +1,2 @@
+[wheel]
+universal = 1
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/setup.py b/testing/web-platform/tests/tools/third_party/atomicwrites/setup.py
new file mode 100644
index 0000000000..98488e9b98
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/setup.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+
+import ast
+import re
+
+from setuptools import find_packages, setup
+
+
+_version_re = re.compile(r'__version__\s+=\s+(.*)')
+
+
+with open('atomicwrites/__init__.py', 'rb') as f:
+ version = str(ast.literal_eval(_version_re.search(
+ f.read().decode('utf-8')).group(1)))
+
+setup(
+ name='atomicwrites',
+ version=version,
+ author='Markus Unterwaditzer',
+ author_email='markus@unterwaditzer.net',
+ url='https://github.com/untitaker/python-atomicwrites',
+ description='Atomic file writes.',
+ license='MIT',
+ long_description=open('README.rst').read(),
+ packages=find_packages(exclude=['tests.*', 'tests']),
+ include_package_data=True,
+)
diff --git a/testing/web-platform/tests/tools/third_party/atomicwrites/tox.ini b/testing/web-platform/tests/tools/third_party/atomicwrites/tox.ini
new file mode 100644
index 0000000000..dfadf03336
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/atomicwrites/tox.ini
@@ -0,0 +1,11 @@
+[tox]
+envlist = py{26,27,py,33,34,35}-{test,stylecheck}
+
+[testenv]
+deps =
+ test: pytest
+ stylecheck: flake8
+ stylecheck: flake8-import-order
+commands =
+ test: py.test []
+ stylecheck: flake8 []
diff --git a/testing/web-platform/tests/tools/third_party/attrs/.github/CODE_OF_CONDUCT.md b/testing/web-platform/tests/tools/third_party/attrs/.github/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..1d8ad1833e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/.github/CODE_OF_CONDUCT.md
@@ -0,0 +1,133 @@
+
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+ community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances of
+ any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address,
+ without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+<mailto:hs@ox.cx>.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/testing/web-platform/tests/tools/third_party/attrs/.github/CONTRIBUTING.md b/testing/web-platform/tests/tools/third_party/attrs/.github/CONTRIBUTING.md
new file mode 100644
index 0000000000..bbdc20f193
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/.github/CONTRIBUTING.md
@@ -0,0 +1,230 @@
+# How To Contribute
+
+First off, 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.
+Don't be afraid to open half-finished PRs, and ask questions if something is unclear!
+
+Please note that this project is released with a Contributor [Code of Conduct](https://github.com/python-attrs/attrs/blob/main/.github/CODE_OF_CONDUCT.md).
+By participating in this project you agree to abide by its terms.
+Please report any harm to [Hynek Schlawack] in any way you find appropriate.
+
+
+## Support
+
+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!
+
+
+## Workflow
+
+- 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.
+- *Always* add tests and docs for your code.
+ This is a hard rule; patches with missing tests or documentation can'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.
+
+
+## Code
+
+- Obey [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 257](https://www.python.org/dev/peps/pep-0257/).
+ We use the `"""`-on-separate-lines style for docstrings:
+
+ ```python
+ def func(x):
+ """
+ Do something.
+
+ :param str x: A very important parameter.
+
+ :rtype: str
+ """
+ ```
+- 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.
+ If you don't, [CI] will catch it for you – but that seems like a waste of your time!
+
+
+## Tests
+
+- Write your asserts as `expected == actual` to line them up nicely:
+
+ ```python
+ x = f()
+
+ assert 42 == x.some_attribute
+ assert "foo" == x._a_private_attribute
+ ```
+
+- 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`.
+
+ 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).
+- To ensure new features work well with the rest of the system, they should be also added to our [*Hypothesis*](https://hypothesis.readthedocs.io/) testing strategy, which can be found in `tests/strategies.py`.
+- If you've changed or added public APIs, please update our type stubs (files ending in `.pyi`).
+
+
+## Documentation
+
+- Use [semantic newlines] in [*reStructuredText*] files (files ending in `.rst`):
+
+ ```rst
+ This is a sentence.
+ This is another sentence.
+ ```
+
+- If you start a new section, add two blank lines before and one blank line after the header, except if two headers follow immediately after each other:
+
+ ```rst
+ Last line of previous section.
+
+
+ Header of New Top Section
+ -------------------------
+
+ Header of New Section
+ ^^^^^^^^^^^^^^^^^^^^^
+
+ 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)!
+
+
+### 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).
+
+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.
+- 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 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:
+ `attrs.func()` or `attrs.Class.method()`.
+ This makes the changelog a lot more readable.
+- Prefer simple past tense or constructions with "now".
+ For example:
+
+ + 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.
+
+Example entries:
+
+ ```rst
+ 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.
+ 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.
+
+
+## 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.
+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.**
+
+[Hynek Schlawack] acts reluctantly as the [BDFL](https://en.wikipedia.org/wiki/Benevolent_dictator_for_life) and has the final say over design decisions.
+
+
+[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/
+[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/testing/web-platform/tests/tools/third_party/attrs/.github/FUNDING.yml b/testing/web-platform/tests/tools/third_party/attrs/.github/FUNDING.yml
new file mode 100644
index 0000000000..ef4f212162
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/.github/FUNDING.yml
@@ -0,0 +1,5 @@
+---
+
+github: hynek
+ko_fi: the_hynek
+tidelift: "pypi/attrs"
diff --git a/testing/web-platform/tests/tools/third_party/attrs/.github/PULL_REQUEST_TEMPLATE.md b/testing/web-platform/tests/tools/third_party/attrs/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000..88f6415e96
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,34 @@
+# Summary
+
+<!-- Please tell us what your pull request is about here. -->
+
+
+# Pull Request Check List
+
+<!--
+This is just a friendly reminder about the most common mistakes.
+Please make sure that you tick all boxes.
+But please read our [contribution guide](https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md) at least once, it will save you unnecessary review cycles!
+
+If an item doesn't apply to your pull request, **check it anyway** to make it apparent that there's nothing left to do.
+If your pull request is a documentation fix or a trivial typo, feel free to delete the whole thing.
+-->
+
+- [ ] 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).
+- [ ] Changes or additions to public APIs are reflected in our type stubs (files ending in ``.pyi``).
+ - [ ] ...and used in the stub test file `tests/typing_example.py`.
+ - [ ] If they've been added to `attr/__init__.pyi`, they've *also* been re-imported in `attrs/__init__.pyi`.
+- [ ] Updated **documentation** for changed code.
+ - [ ] 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/).
+- [ ] Changes (and possible deprecations) have news fragments in [`changelog.d`](https://github.com/python-attrs/attrs/blob/main/changelog.d).
+
+<!--
+If you have *any* questions to *any* of the points above, just **submit and ask**!
+This checklist is here to *help* you, not to deter you from contributing!
+-->
diff --git a/testing/web-platform/tests/tools/third_party/attrs/.github/SECURITY.md b/testing/web-platform/tests/tools/third_party/attrs/.github/SECURITY.md
new file mode 100644
index 0000000000..5e565ec19c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/.github/SECURITY.md
@@ -0,0 +1,2 @@
+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/testing/web-platform/tests/tools/third_party/attrs/.github/workflows/main.yml b/testing/web-platform/tests/tools/third_party/attrs/.github/workflows/main.yml
new file mode 100644
index 0000000000..f38fd91509
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/.github/workflows/main.yml
@@ -0,0 +1,113 @@
+---
+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/testing/web-platform/tests/tools/third_party/attrs/.gitignore b/testing/web-platform/tests/tools/third_party/attrs/.gitignore
new file mode 100644
index 0000000000..d054dc6267
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/.gitignore
@@ -0,0 +1,13 @@
+*.egg-info
+*.pyc
+.cache
+.coverage*
+.hypothesis
+.mypy_cache
+.pytest_cache
+.tox
+build
+dist
+docs/_build/
+htmlcov
+pip-wheel-metadata
diff --git a/testing/web-platform/tests/tools/third_party/attrs/.pre-commit-config.yaml b/testing/web-platform/tests/tools/third_party/attrs/.pre-commit-config.yaml
new file mode 100644
index 0000000000..a913b068f5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/.pre-commit-config.yaml
@@ -0,0 +1,43 @@
+---
+ci:
+ autoupdate_schedule: monthly
+
+repos:
+ - repo: https://github.com/psf/black
+ rev: 21.12b0
+ hooks:
+ - id: black
+ exclude: tests/test_pattern_matching.py
+ language_version: python3.10
+
+ - repo: https://github.com/PyCQA/isort
+ rev: 5.10.1
+ 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
+
+ - 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
+ 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/testing/web-platform/tests/tools/third_party/attrs/.readthedocs.yml b/testing/web-platform/tests/tools/third_party/attrs/.readthedocs.yml
new file mode 100644
index 0000000000..d335c40d56
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/.readthedocs.yml
@@ -0,0 +1,16 @@
+---
+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/testing/web-platform/tests/tools/third_party/attrs/AUTHORS.rst b/testing/web-platform/tests/tools/third_party/attrs/AUTHORS.rst
new file mode 100644
index 0000000000..f14ef6c607
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/AUTHORS.rst
@@ -0,0 +1,11 @@
+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/testing/web-platform/tests/tools/third_party/attrs/CHANGELOG.rst b/testing/web-platform/tests/tools/third_party/attrs/CHANGELOG.rst
new file mode 100644
index 0000000000..1d194add22
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/CHANGELOG.rst
@@ -0,0 +1,1027 @@
+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/testing/web-platform/tests/tools/third_party/attrs/LICENSE b/testing/web-platform/tests/tools/third_party/attrs/LICENSE
new file mode 100644
index 0000000000..7ae3df9309
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Hynek Schlawack
+
+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/testing/web-platform/tests/tools/third_party/attrs/MANIFEST.in b/testing/web-platform/tests/tools/third_party/attrs/MANIFEST.in
new file mode 100644
index 0000000000..3d68bf9c5d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/MANIFEST.in
@@ -0,0 +1,24 @@
+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/testing/web-platform/tests/tools/third_party/attrs/README.rst b/testing/web-platform/tests/tools/third_party/attrs/README.rst
new file mode 100644
index 0000000000..709bba83d7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/README.rst
@@ -0,0 +1,135 @@
+.. 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/testing/web-platform/tests/tools/third_party/attrs/changelog.d/.gitignore b/testing/web-platform/tests/tools/third_party/attrs/changelog.d/.gitignore
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/changelog.d/.gitignore
diff --git a/testing/web-platform/tests/tools/third_party/attrs/changelog.d/towncrier_template.rst b/testing/web-platform/tests/tools/third_party/attrs/changelog.d/towncrier_template.rst
new file mode 100644
index 0000000000..29ca74c4e8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/changelog.d/towncrier_template.rst
@@ -0,0 +1,35 @@
+{% 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/testing/web-platform/tests/tools/third_party/attrs/conftest.py b/testing/web-platform/tests/tools/third_party/attrs/conftest.py
new file mode 100644
index 0000000000..0d539a115c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/conftest.py
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: MIT
+
+from __future__ import absolute_import, division, print_function
+
+from hypothesis import HealthCheck, settings
+
+from attr._compat import PY36, PY310
+
+
+def pytest_configure(config):
+ # HealthCheck.too_slow causes more trouble than good -- especially in CIs.
+ settings.register_profile(
+ "patience", settings(suppress_health_check=[HealthCheck.too_slow])
+ )
+ settings.load_profile("patience")
+
+
+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/testing/web-platform/tests/tools/third_party/attrs/docs/Makefile b/testing/web-platform/tests/tools/third_party/attrs/docs/Makefile
new file mode 100644
index 0000000000..3143891daf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+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 " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @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)/*
+
+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/attrs.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/attrs.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/attrs"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/attrs"
+ @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."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @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."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+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."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/testing/web-platform/tests/tools/third_party/attrs/docs/_static/attrs_logo.png b/testing/web-platform/tests/tools/third_party/attrs/docs/_static/attrs_logo.png
new file mode 100644
index 0000000000..11b6e6fe3f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/_static/attrs_logo.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/attrs/docs/_static/attrs_logo.svg b/testing/web-platform/tests/tools/third_party/attrs/docs/_static/attrs_logo.svg
new file mode 100644
index 0000000000..b02ae6c025
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/_static/attrs_logo.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 142 118" version="1.1" fill="#222">
+ <path d="M 88.984375 4.2460938 L 88.269531 6.40625 A 13.133 13.133 0 0 0 85.244141 6.9023438 L 83.9375 5.0625 L 83.699219 5.1523438 A 14.916 14.916 0 0 0 80.007812 7.0859375 L 79.8125 7.2265625 L 80.507812 9.40625 A 12.958 12.958 0 0 0 78.490234 11.496094 L 76.351562 10.785156 L 76.214844 10.996094 A 15.179 15.179 0 0 0 74.369141 14.8125 L 74.304688 15.035156 L 76.15625 16.398438 C 76.14425 16.418437 76.140719 16.441031 76.136719 16.457031 A 12.874 12.874 0 0 0 75.796875 19.035156 L 73.640625 19.71875 L 73.652344 19.964844 C 73.706344 21.432844 73.959109 22.867563 74.412109 24.226562 L 74.488281 24.453125 L 76.796875 24.464844 C 77.109875 25.214844 77.488594 25.930281 77.933594 26.613281 L 76.585938 28.441406 L 76.734375 28.636719 A 15.028 15.028 0 0 0 79.863281 31.710938 L 80.054688 31.851562 L 81.921875 30.515625 C 82.562875 30.917625 83.24975 31.265687 83.96875 31.554688 L 83.951172 33.835938 L 84.183594 33.910156 C 84.570594 34.031156 84.960281 34.144188 85.363281 34.242188 C 86.425281 34.488188 87.48425 34.621531 88.53125 34.644531 L 88.773438 34.648438 L 89.490234 32.484375 A 12.819 12.819 0 0 0 91.787109 32.167969 L 93.123047 34.03125 L 93.355469 33.957031 A 15.097 15.097 0 0 0 97.300781 32.070312 L 97.503906 31.933594 L 96.824219 29.773438 A 13.195 13.195 0 0 0 98.628906 28.085938 L 100.8125 28.8125 L 100.95508 28.621094 A 14.78 14.78 0 0 0 103.04688 24.859375 L 103.13672 24.621094 L 101.32031 23.285156 C 101.40631 23.008156 101.48078 22.726313 101.55078 22.445312 C 101.69178 21.832313 101.78875 21.226 101.84375 20.625 L 104.0332 19.929688 L 104.0332 19.691406 C 104.0332 19.605406 104.04297 19.518687 104.04297 19.429688 A 15.365 15.365 0 0 0 103.51953 15.5 L 103.45117 15.257812 L 101.19922 15.246094 A 13.253 13.253 0 0 0 99.941406 12.582031 L 101.29297 10.738281 L 101.15625 10.546875 A 15.367 15.367 0 0 0 98.287109 7.5429688 L 98.09375 7.3867188 L 96.253906 8.703125 A 13.082 13.082 0 0 0 93.53125 7.265625 L 93.542969 5 L 93.3125 4.9257812 A 18.186 18.186 0 0 0 92.320312 4.6523438 A 15.815 15.815 0 0 0 89.234375 4.25 L 88.984375 4.2460938 z M 88.759766 15.541016 A 3.914 3.914 0 0 1 89.740234 15.644531 A 3.913 3.913 0 0 1 92.753906 19.441406 C 92.753906 19.742406 92.722344 20.04275 92.652344 20.34375 A 3.92 3.92 0 0 1 88.847656 23.359375 A 3.72 3.72 0 0 1 87.949219 23.25 C 86.144219 22.836 84.9375 21.226125 84.9375 19.453125 C 84.9375 19.156125 84.967203 18.858688 85.033203 18.554688 A 3.914 3.914 0 0 1 88.759766 15.541016 z "/>
+ <path d="M 60.488281 22.824219 C 58.968281 22.824219 57.488594 22.98425 56.058594 23.28125 L 55.78125 23.332031 L 55.488281 26.582031 C 54.023281 26.992031 52.624219 27.5785 51.324219 28.3125 L 48.886719 26.179688 L 48.648438 26.335938 A 21.852 21.852 0 0 0 44.152344 30.230469 L 43.972656 30.4375 L 45.65625 33.257812 A 18.478 18.478 0 0 0 43.46875 36.933594 L 40.248047 36.644531 L 40.15625 36.910156 A 21.157 21.157 0 0 0 38.84375 42.828125 L 38.820312 43.09375 L 41.855469 44.390625 C 41.851469 44.437625 41.851562 44.488063 41.851562 44.539062 C 41.851562 45.828063 41.988281 47.093687 42.238281 48.304688 L 39.455078 49.960938 L 39.527344 50.234375 A 21.58 21.58 0 0 0 41.980469 55.90625 L 42.119141 56.132812 L 45.34375 55.402344 A 18.763 18.763 0 0 0 47.714844 58.105469 L 46.4375 61.097656 L 46.648438 61.277344 A 21.703 21.703 0 0 0 52.007812 64.535156 L 52.248047 64.640625 L 54.425781 62.160156 C 55.484781 62.527156 56.601281 62.80075 57.738281 62.96875 L 58.464844 66.15625 L 58.744141 66.175781 C 59.307141 66.222781 59.894281 66.253906 60.488281 66.253906 C 62.042281 66.253906 63.558437 66.086437 65.023438 65.773438 L 65.296875 65.714844 L 65.589844 62.460938 A 19.053 19.053 0 0 0 68.792969 61.21875 L 71.269531 63.382812 L 71.503906 63.246094 A 21.892 21.892 0 0 0 76.378906 59.328125 L 76.574219 59.125 L 74.908203 56.335938 A 18.426 18.426 0 0 0 76.9375 53.289062 L 80.230469 53.585938 L 80.335938 53.335938 A 21.627 21.627 0 0 0 82.007812 47.414062 L 82.042969 47.128906 L 79.066406 45.859375 C 79.097406 45.430375 79.119141 44.988062 79.119141 44.539062 C 79.119141 43.625062 79.054781 42.734375 78.925781 41.859375 L 81.757812 40.171875 L 81.699219 39.90625 A 21.733 21.733 0 0 0 79.613281 34.246094 L 79.476562 33.992188 L 76.320312 34.714844 A 18.63 18.63 0 0 0 73.617188 31.320312 L 74.902344 28.308594 L 74.701172 28.132812 A 22.087 22.087 0 0 0 69.726562 24.886719 L 69.472656 24.769531 L 67.335938 27.210938 A 18.403 18.403 0 0 0 62.949219 26.074219 L 62.222656 22.898438 L 61.945312 22.882812 A 19.927 19.927 0 0 0 60.488281 22.824219 z M 60.488281 38.824219 C 63.644281 38.836219 66.199219 41.387062 66.199219 44.539062 A 5.715 5.715 0 0 1 60.488281 50.253906 A 5.717 5.717 0 0 1 54.773438 44.539062 A 5.725 5.725 0 0 1 60.488281 38.824219 z "/>
+ <path d="m 134.226,94.281 c 0,0 0.445,2.621 -0.574,7.356 -1.024,4.796 -2.559,7.351 -2.559,7.351 a 31.76,31.76 0 0 1 -10.809,1.922 c -3.773,0 -7.16,-0.707 -9.976,-1.922 0,0 -0.383,-1.726 0.129,-4.988 1.406,0.387 6.457,1.793 10.933,1.793 2.497,0 5.375,-0.703 5.375,-0.703 0,0 0.704,-1.153 1.149,-3.453 0.512,-2.305 0.32,-3.454 0.32,-3.454 0,0 -2.558,-0.703 -5.051,-0.703 -3.902,0 -7.226,-0.64 -10.039,-1.855 0,0 -0.386,-2.879 0.383,-6.524 0.766,-3.707 2.43,-6.585 2.43,-6.585 3.324,-1.153 7.035,-1.856 10.808,-1.856 3.77,0 7.161,0.703 9.973,1.856 0,0.128 0.387,2.046 -0.062,5.179 -1.536,-0.449 -7.165,-1.918 -11,-1.918 -2.493,0 -5.372,0.703 -5.372,0.703 0,0 -0.64,1.024 -0.957,2.621 -0.32,1.598 -0.195,2.622 -0.195,2.622 0,0 2.496,0.64 5.117,0.703 3.774,0 7.164,0.64 9.977,1.855 z"/>
+ <path d="m 105.511,80.66 c 1.984,0 3.84,0.191 5.629,0.578 -0.703,1.727 -1.469,3.324 -2.367,4.86 -1.406,-0.192 -2.813,-0.321 -4.348,-0.321 -2.492,0 -5.242,0.703 -5.242,0.703 0,0 -1.856,6.075 -2.496,9.274 L 93.62,110.269 H 87.8 l 3.07,-14.515 a 84.252,84.252 0 0 1 3.836,-13.238 c 3.325,-1.153 7.035,-1.856 10.805,-1.856 z"/>
+ <path d="m 77.374,105.793 c 2.817,0 5.629,-0.512 7.867,-1.024 -0.765,1.981 -1.664,3.774 -2.621,5.5 -2.046,0.383 -4.156,0.641 -6.332,0.641 -3.773,0 -7.097,-0.707 -9.91,-1.922 0.125,-3.965 0.766,-8.441 1.789,-13.234 l 1.918,-9.082 h -6.457 c 0.703,-1.789 1.469,-3.453 2.492,-5.051 h 5.055 l 1.34,-6.332 a 31.365,31.365 0 0 1 6.074,-1.535 l -1.66,7.867 h 11.125 c -0.703,1.789 -1.535,3.516 -2.492,5.051 h -9.719 l -1.922,9.082 c -0.703,3.262 -1.469,9.336 -1.469,9.336 0,0 2.493,0.703 4.922,0.703 z"/>
+ <path d="m 49.878,105.793 c 2.813,0 5.629,-0.512 7.867,-1.024 -0.769,1.981 -1.664,3.774 -2.621,5.5 -2.047,0.383 -4.16,0.641 -6.332,0.641 -3.773,0 -7.097,-0.707 -9.914,-1.922 0.129,-3.965 0.77,-8.441 1.793,-13.234 l 1.918,-9.082 h -6.461 c 0.707,-1.789 1.473,-3.453 2.496,-5.051 h 5.051 l 1.344,-6.332 a 31.365,31.365 0 0 1 6.074,-1.535 l -1.66,7.867 h 11.125 c -0.703,1.789 -1.535,3.516 -2.492,5.051 h -9.723 l -1.918,9.082 c -0.703,3.262 -1.469,9.336 -1.469,9.336 0,0 2.493,0.703 4.922,0.703 z"/>
+ <path d="M 22.574219 80.660156 C 18.800219 80.660156 15.093625 81.362625 11.765625 82.515625 C 11.128625 84.112625 10.616109 85.715406 10.037109 87.441406 C 12.022109 86.863406 16.624281 85.777344 21.488281 85.777344 C 23.980281 85.777344 26.476562 86.480469 26.476562 86.480469 C 26.476562 86.480469 26.089531 89.101062 25.644531 91.789062 C 23.980531 91.469062 22.191938 91.277344 20.335938 91.277344 A 32.101 32.101 0 0 0 9.4648438 93.195312 C 9.4648437 93.195312 7.8003437 96.328 6.7773438 101.125 C 5.7533437 105.855 6.140625 108.98828 6.140625 108.98828 C 8.952625 110.20328 12.343281 110.91016 16.113281 110.91016 A 31.74 31.74 0 0 0 26.921875 108.98828 C 28.456875 105.02328 29.734813 100.54691 30.757812 95.753906 C 31.718813 91.018906 32.359781 86.542625 32.550781 82.515625 C 29.734781 81.362625 26.347219 80.660156 22.574219 80.660156 z M 19.248047 96.390625 A 21.116 21.116 0 0 1 24.619141 97.09375 C 23.850141 100.42175 22.511719 105.08984 22.511719 105.08984 C 22.511719 105.08984 19.951172 105.79297 17.201172 105.79297 C 14.705172 105.79297 12.152344 105.08984 12.152344 105.08984 C 12.152344 105.08984 12.085656 103.426 12.597656 101.125 C 13.109656 98.758 13.8125 97.09375 13.8125 97.09375 C 13.8125 97.09375 16.752047 96.390625 19.248047 96.390625 z "/>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/attrs/docs/_static/attrs_logo_white.svg b/testing/web-platform/tests/tools/third_party/attrs/docs/_static/attrs_logo_white.svg
new file mode 100644
index 0000000000..daad798da0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/_static/attrs_logo_white.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 142 118" version="1.1" fill="#fff">
+ <path d="M 88.984375 4.2460938 L 88.269531 6.40625 A 13.133 13.133 0 0 0 85.244141 6.9023438 L 83.9375 5.0625 L 83.699219 5.1523438 A 14.916 14.916 0 0 0 80.007812 7.0859375 L 79.8125 7.2265625 L 80.507812 9.40625 A 12.958 12.958 0 0 0 78.490234 11.496094 L 76.351562 10.785156 L 76.214844 10.996094 A 15.179 15.179 0 0 0 74.369141 14.8125 L 74.304688 15.035156 L 76.15625 16.398438 C 76.14425 16.418437 76.140719 16.441031 76.136719 16.457031 A 12.874 12.874 0 0 0 75.796875 19.035156 L 73.640625 19.71875 L 73.652344 19.964844 C 73.706344 21.432844 73.959109 22.867563 74.412109 24.226562 L 74.488281 24.453125 L 76.796875 24.464844 C 77.109875 25.214844 77.488594 25.930281 77.933594 26.613281 L 76.585938 28.441406 L 76.734375 28.636719 A 15.028 15.028 0 0 0 79.863281 31.710938 L 80.054688 31.851562 L 81.921875 30.515625 C 82.562875 30.917625 83.24975 31.265687 83.96875 31.554688 L 83.951172 33.835938 L 84.183594 33.910156 C 84.570594 34.031156 84.960281 34.144188 85.363281 34.242188 C 86.425281 34.488188 87.48425 34.621531 88.53125 34.644531 L 88.773438 34.648438 L 89.490234 32.484375 A 12.819 12.819 0 0 0 91.787109 32.167969 L 93.123047 34.03125 L 93.355469 33.957031 A 15.097 15.097 0 0 0 97.300781 32.070312 L 97.503906 31.933594 L 96.824219 29.773438 A 13.195 13.195 0 0 0 98.628906 28.085938 L 100.8125 28.8125 L 100.95508 28.621094 A 14.78 14.78 0 0 0 103.04688 24.859375 L 103.13672 24.621094 L 101.32031 23.285156 C 101.40631 23.008156 101.48078 22.726313 101.55078 22.445312 C 101.69178 21.832313 101.78875 21.226 101.84375 20.625 L 104.0332 19.929688 L 104.0332 19.691406 C 104.0332 19.605406 104.04297 19.518687 104.04297 19.429688 A 15.365 15.365 0 0 0 103.51953 15.5 L 103.45117 15.257812 L 101.19922 15.246094 A 13.253 13.253 0 0 0 99.941406 12.582031 L 101.29297 10.738281 L 101.15625 10.546875 A 15.367 15.367 0 0 0 98.287109 7.5429688 L 98.09375 7.3867188 L 96.253906 8.703125 A 13.082 13.082 0 0 0 93.53125 7.265625 L 93.542969 5 L 93.3125 4.9257812 A 18.186 18.186 0 0 0 92.320312 4.6523438 A 15.815 15.815 0 0 0 89.234375 4.25 L 88.984375 4.2460938 z M 88.759766 15.541016 A 3.914 3.914 0 0 1 89.740234 15.644531 A 3.913 3.913 0 0 1 92.753906 19.441406 C 92.753906 19.742406 92.722344 20.04275 92.652344 20.34375 A 3.92 3.92 0 0 1 88.847656 23.359375 A 3.72 3.72 0 0 1 87.949219 23.25 C 86.144219 22.836 84.9375 21.226125 84.9375 19.453125 C 84.9375 19.156125 84.967203 18.858688 85.033203 18.554688 A 3.914 3.914 0 0 1 88.759766 15.541016 z "/>
+ <path d="M 60.488281 22.824219 C 58.968281 22.824219 57.488594 22.98425 56.058594 23.28125 L 55.78125 23.332031 L 55.488281 26.582031 C 54.023281 26.992031 52.624219 27.5785 51.324219 28.3125 L 48.886719 26.179688 L 48.648438 26.335938 A 21.852 21.852 0 0 0 44.152344 30.230469 L 43.972656 30.4375 L 45.65625 33.257812 A 18.478 18.478 0 0 0 43.46875 36.933594 L 40.248047 36.644531 L 40.15625 36.910156 A 21.157 21.157 0 0 0 38.84375 42.828125 L 38.820312 43.09375 L 41.855469 44.390625 C 41.851469 44.437625 41.851562 44.488063 41.851562 44.539062 C 41.851562 45.828063 41.988281 47.093687 42.238281 48.304688 L 39.455078 49.960938 L 39.527344 50.234375 A 21.58 21.58 0 0 0 41.980469 55.90625 L 42.119141 56.132812 L 45.34375 55.402344 A 18.763 18.763 0 0 0 47.714844 58.105469 L 46.4375 61.097656 L 46.648438 61.277344 A 21.703 21.703 0 0 0 52.007812 64.535156 L 52.248047 64.640625 L 54.425781 62.160156 C 55.484781 62.527156 56.601281 62.80075 57.738281 62.96875 L 58.464844 66.15625 L 58.744141 66.175781 C 59.307141 66.222781 59.894281 66.253906 60.488281 66.253906 C 62.042281 66.253906 63.558437 66.086437 65.023438 65.773438 L 65.296875 65.714844 L 65.589844 62.460938 A 19.053 19.053 0 0 0 68.792969 61.21875 L 71.269531 63.382812 L 71.503906 63.246094 A 21.892 21.892 0 0 0 76.378906 59.328125 L 76.574219 59.125 L 74.908203 56.335938 A 18.426 18.426 0 0 0 76.9375 53.289062 L 80.230469 53.585938 L 80.335938 53.335938 A 21.627 21.627 0 0 0 82.007812 47.414062 L 82.042969 47.128906 L 79.066406 45.859375 C 79.097406 45.430375 79.119141 44.988062 79.119141 44.539062 C 79.119141 43.625062 79.054781 42.734375 78.925781 41.859375 L 81.757812 40.171875 L 81.699219 39.90625 A 21.733 21.733 0 0 0 79.613281 34.246094 L 79.476562 33.992188 L 76.320312 34.714844 A 18.63 18.63 0 0 0 73.617188 31.320312 L 74.902344 28.308594 L 74.701172 28.132812 A 22.087 22.087 0 0 0 69.726562 24.886719 L 69.472656 24.769531 L 67.335938 27.210938 A 18.403 18.403 0 0 0 62.949219 26.074219 L 62.222656 22.898438 L 61.945312 22.882812 A 19.927 19.927 0 0 0 60.488281 22.824219 z M 60.488281 38.824219 C 63.644281 38.836219 66.199219 41.387062 66.199219 44.539062 A 5.715 5.715 0 0 1 60.488281 50.253906 A 5.717 5.717 0 0 1 54.773438 44.539062 A 5.725 5.725 0 0 1 60.488281 38.824219 z "/>
+ <path d="m 134.226,94.281 c 0,0 0.445,2.621 -0.574,7.356 -1.024,4.796 -2.559,7.351 -2.559,7.351 a 31.76,31.76 0 0 1 -10.809,1.922 c -3.773,0 -7.16,-0.707 -9.976,-1.922 0,0 -0.383,-1.726 0.129,-4.988 1.406,0.387 6.457,1.793 10.933,1.793 2.497,0 5.375,-0.703 5.375,-0.703 0,0 0.704,-1.153 1.149,-3.453 0.512,-2.305 0.32,-3.454 0.32,-3.454 0,0 -2.558,-0.703 -5.051,-0.703 -3.902,0 -7.226,-0.64 -10.039,-1.855 0,0 -0.386,-2.879 0.383,-6.524 0.766,-3.707 2.43,-6.585 2.43,-6.585 3.324,-1.153 7.035,-1.856 10.808,-1.856 3.77,0 7.161,0.703 9.973,1.856 0,0.128 0.387,2.046 -0.062,5.179 -1.536,-0.449 -7.165,-1.918 -11,-1.918 -2.493,0 -5.372,0.703 -5.372,0.703 0,0 -0.64,1.024 -0.957,2.621 -0.32,1.598 -0.195,2.622 -0.195,2.622 0,0 2.496,0.64 5.117,0.703 3.774,0 7.164,0.64 9.977,1.855 z"/>
+ <path d="m 105.511,80.66 c 1.984,0 3.84,0.191 5.629,0.578 -0.703,1.727 -1.469,3.324 -2.367,4.86 -1.406,-0.192 -2.813,-0.321 -4.348,-0.321 -2.492,0 -5.242,0.703 -5.242,0.703 0,0 -1.856,6.075 -2.496,9.274 L 93.62,110.269 H 87.8 l 3.07,-14.515 a 84.252,84.252 0 0 1 3.836,-13.238 c 3.325,-1.153 7.035,-1.856 10.805,-1.856 z"/>
+ <path d="m 77.374,105.793 c 2.817,0 5.629,-0.512 7.867,-1.024 -0.765,1.981 -1.664,3.774 -2.621,5.5 -2.046,0.383 -4.156,0.641 -6.332,0.641 -3.773,0 -7.097,-0.707 -9.91,-1.922 0.125,-3.965 0.766,-8.441 1.789,-13.234 l 1.918,-9.082 h -6.457 c 0.703,-1.789 1.469,-3.453 2.492,-5.051 h 5.055 l 1.34,-6.332 a 31.365,31.365 0 0 1 6.074,-1.535 l -1.66,7.867 h 11.125 c -0.703,1.789 -1.535,3.516 -2.492,5.051 h -9.719 l -1.922,9.082 c -0.703,3.262 -1.469,9.336 -1.469,9.336 0,0 2.493,0.703 4.922,0.703 z"/>
+ <path d="m 49.878,105.793 c 2.813,0 5.629,-0.512 7.867,-1.024 -0.769,1.981 -1.664,3.774 -2.621,5.5 -2.047,0.383 -4.16,0.641 -6.332,0.641 -3.773,0 -7.097,-0.707 -9.914,-1.922 0.129,-3.965 0.77,-8.441 1.793,-13.234 l 1.918,-9.082 h -6.461 c 0.707,-1.789 1.473,-3.453 2.496,-5.051 h 5.051 l 1.344,-6.332 a 31.365,31.365 0 0 1 6.074,-1.535 l -1.66,7.867 h 11.125 c -0.703,1.789 -1.535,3.516 -2.492,5.051 h -9.723 l -1.918,9.082 c -0.703,3.262 -1.469,9.336 -1.469,9.336 0,0 2.493,0.703 4.922,0.703 z"/>
+ <path d="M 22.574219 80.660156 C 18.800219 80.660156 15.093625 81.362625 11.765625 82.515625 C 11.128625 84.112625 10.616109 85.715406 10.037109 87.441406 C 12.022109 86.863406 16.624281 85.777344 21.488281 85.777344 C 23.980281 85.777344 26.476562 86.480469 26.476562 86.480469 C 26.476562 86.480469 26.089531 89.101062 25.644531 91.789062 C 23.980531 91.469062 22.191938 91.277344 20.335938 91.277344 A 32.101 32.101 0 0 0 9.4648438 93.195312 C 9.4648437 93.195312 7.8003437 96.328 6.7773438 101.125 C 5.7533437 105.855 6.140625 108.98828 6.140625 108.98828 C 8.952625 110.20328 12.343281 110.91016 16.113281 110.91016 A 31.74 31.74 0 0 0 26.921875 108.98828 C 28.456875 105.02328 29.734813 100.54691 30.757812 95.753906 C 31.718813 91.018906 32.359781 86.542625 32.550781 82.515625 C 29.734781 81.362625 26.347219 80.660156 22.574219 80.660156 z M 19.248047 96.390625 A 21.116 21.116 0 0 1 24.619141 97.09375 C 23.850141 100.42175 22.511719 105.08984 22.511719 105.08984 C 22.511719 105.08984 19.951172 105.79297 17.201172 105.79297 C 14.705172 105.79297 12.152344 105.08984 12.152344 105.08984 C 12.152344 105.08984 12.085656 103.426 12.597656 101.125 C 13.109656 98.758 13.8125 97.09375 13.8125 97.09375 C 13.8125 97.09375 16.752047 96.390625 19.248047 96.390625 z "/>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/attrs/docs/api.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/api.rst
new file mode 100644
index 0000000000..02aed52ad5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/api.rst
@@ -0,0 +1,826 @@
+API Reference
+=============
+
+.. currentmodule:: attr
+
+``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.
+
+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 introduction, have a look at `examples`.
+
+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.
+ 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.
+
+
+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
+
+.. autofunction:: attrs.define
+
+.. function:: attrs.mutable(same_as_define)
+
+ Alias for `attrs.define`.
+
+ .. versionadded:: 20.1.0
+
+.. function:: attrs.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`.
+
+.. function:: field
+
+ Old import path for `attrs.field`.
+
+.. autoclass:: attrs.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)
+
+
+.. autofunction:: attrs.make_class
+
+ This is handy if you want to programmatically create classes.
+
+ For example:
+
+ .. doctest::
+
+ >>> C1 = attr.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()
+ C2(x=42, y=[])
+
+
+.. autoclass:: attrs.Factory
+
+ For example:
+
+ .. doctest::
+
+ >>> @attr.s
+ ... class C(object):
+ ... x = attr.ib(default=attr.Factory(list))
+ ... y = attr.ib(default=attr.Factory(
+ ... lambda self: set(self.x),
+ ... takes_self=True)
+ ... )
+ >>> C()
+ C(x=[], y=set())
+ >>> C([1, 2, 3])
+ 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
+----------
+
+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:
+
+.. doctest::
+
+ >>> import attrs, attr
+ >>> try:
+ ... raise attrs.exceptions.FrozenError()
+ ... except attr.exceptions.FrozenError:
+ ... 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
+
+ For example::
+
+ @attr.s(auto_attribs=True)
+ class C:
+ x: int
+ y = attr.ib() # <- ERROR!
+
+
+.. _helpers:
+
+Helpers
+-------
+
+``attrs`` comes with a bunch of helper methods that make working with it easier:
+
+.. autofunction:: attrs.cmp_using
+.. function:: attr.cmp_using
+
+ Same as `attrs.cmp_using`.
+
+.. autofunction:: attrs.fields
+
+ For example:
+
+ .. doctest::
+
+ >>> @attr.s
+ ... class C(object):
+ ... x = attr.ib()
+ ... y = attr.ib()
+ >>> 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))
+ >>> 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)
+ >>> attrs.fields(C).y is attrs.fields(C)[1]
+ True
+
+.. function:: attr.fields
+
+ Same as `attrs.fields`.
+
+.. autofunction:: attrs.fields_dict
+
+ For example:
+
+ .. doctest::
+
+ >>> @attr.s
+ ... class C(object):
+ ... 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)}
+ >>> 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)
+ >>> 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:
+
+ .. doctest::
+
+ >>> @attr.s
+ ... class C(object):
+ ... pass
+ >>> attr.has(C)
+ True
+ >>> attr.has(object)
+ False
+
+.. function:: attr.has
+
+ Same as `attrs.has`.
+
+.. autofunction:: attrs.resolve_types
+
+ For example:
+
+ .. doctest::
+
+ >>> import typing
+ >>> @attrs.define
+ ... class A:
+ ... a: typing.List['A']
+ ... b: 'B'
+ ...
+ >>> @attrs.define
+ ... class B:
+ ... a: A
+ ...
+ >>> attrs.fields(A).a.type
+ typing.List[ForwardRef('A')]
+ >>> attrs.fields(A).b.type
+ 'B'
+ >>> attrs.resolve_types(A, globals(), locals())
+ <class 'A'>
+ >>> attrs.fields(A).a.type
+ typing.List[A]
+ >>> attrs.fields(A).b.type
+ <class 'B'>
+
+.. function:: attr.resolve_types
+
+ Same as `attrs.resolve_types`.
+
+.. autofunction:: attrs.asdict
+
+ For example:
+
+ .. doctest::
+
+ >>> @attrs.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
+ ... class C:
+ ... x = attr.field()
+ ... y = attr.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
+
+.. function:: attr.filters.include
+
+ Same as `attrs.filters.include`.
+
+.. function:: attr.filters.exclude
+
+ Same as `attrs.filters.exclude`.
+
+See :func:`attrs.asdict` for examples.
+
+All objects from ``attrs.filters`` are also available from ``attr.filters``.
+
+----
+
+.. autofunction:: attrs.evolve
+
+ For example:
+
+ .. doctest::
+
+ >>> @attrs.define
+ ... class C:
+ ... x: int
+ ... y: int
+ >>> i1 = C(1, 2)
+ >>> i1
+ C(x=1, y=2)
+ >>> i2 = attrs.evolve(i1, y=3)
+ >>> i2
+ C(x=1, y=3)
+ >>> i1 == i2
+ False
+
+ ``evolve`` creates a new instance using ``__init__``.
+ This fact has several implications:
+
+ * private attributes should be specified without the leading underscore, just like in ``__init__``.
+ * 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)
+ ... class C:
+ ... x = attrs.field(validator=attrs.validators.instance_of(int))
+ >>> i = C(1)
+ >>> i.x = "1"
+ >>> attrs.validate(i)
+ Traceback (most recent call last):
+ ...
+ 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:
+
+Validators
+----------
+
+``attrs`` comes with some common validators in the ``attrs.validators`` module.
+All objects from ``attrs.converters`` are also available from ``attr.converters``.
+
+
+.. autofunction:: attrs.validators.lt
+
+ For example:
+
+ .. doctest::
+
+ >>> @attrs.define
+ ... class C:
+ ... x = attrs.field(validator=attrs.validators.lt(42))
+ >>> C(41)
+ C(x=41)
+ >>> C(42)
+ Traceback (most recent call last):
+ ...
+ ValueError: ("'x' must be < 42: 42")
+
+.. autofunction:: attrs.validators.le
+
+ For example:
+
+ .. doctest::
+
+ >>> @attrs.define
+ ... class C(object):
+ ... x = attrs.field(validator=attr.validators.le(42))
+ >>> C(42)
+ C(x=42)
+ >>> C(43)
+ Traceback (most recent call last):
+ ...
+ ValueError: ("'x' must be <= 42: 43")
+
+.. autofunction:: attrs.validators.ge
+
+ For example:
+
+ .. doctest::
+
+ >>> @attrs.define
+ ... class C:
+ ... x = attrs.field(validator=attrs.validators.ge(42))
+ >>> C(42)
+ C(x=42)
+ >>> C(41)
+ Traceback (most recent call last):
+ ...
+ ValueError: ("'x' must be => 42: 41")
+
+.. autofunction:: attrs.validators.gt
+
+ For example:
+
+ .. doctest::
+
+ >>> @attrs.define
+ ... class C:
+ ... x = attr.field(validator=attrs.validators.gt(42))
+ >>> C(43)
+ C(x=43)
+ >>> C(42)
+ Traceback (most recent call last):
+ ...
+ ValueError: ("'x' must be > 42: 42")
+
+.. autofunction:: attrs.validators.max_len
+
+ For example:
+
+ .. doctest::
+
+ >>> @attrs.define
+ ... class C:
+ ... x = attrs.field(validator=attrs.validators.max_len(4))
+ >>> C("spam")
+ C(x='spam')
+ >>> C("bacon")
+ Traceback (most recent call last):
+ ...
+ ValueError: ("Length of 'x' must be <= 4: 5")
+
+.. autofunction:: attrs.validators.instance_of
+
+ For example:
+
+ .. doctest::
+
+ >>> @attrs.define
+ ... class C:
+ ... x = attrs.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, validator=<instance_of validator for type <type 'int'>>, type=None, kw_only=False), <type 'int'>, '42')
+ >>> C(None)
+ Traceback (most recent call last):
+ ...
+ TypeError: ("'x' must be <type 'int'> (got None that is a <type 'NoneType'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, repr=True, cmp=True, hash=None, init=True, type=None, kw_only=False), <type 'int'>, None)
+
+.. autofunction:: attrs.validators.in_
+
+ For example:
+
+ .. 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)
+
+.. autofunction:: attrs.validators.provides
+
+.. autofunction:: attrs.validators.and_
+
+ For convenience, it's also possible to pass a list to `attrs.field`'s validator argument.
+
+ Thus the following two statements are equivalent::
+
+ x = attrs.field(validator=attrs.validators.and_(v1, v2, v3))
+ x = attrs.field(validator=[v1, v2, v3])
+
+.. autofunction:: attrs.validators.optional
+
+ For example:
+
+ .. doctest::
+
+ >>> @attrs.define
+ ... class C:
+ ... x = attrs.field(validator=attrs.validators.optional(attr.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, validator=<instance_of validator for type <type 'int'>>, type=None, kw_only=False), <type 'int'>, '42')
+ >>> C(None)
+ C(x=None)
+
+
+.. autofunction:: attrs.validators.is_callable
+
+ For example:
+
+ .. doctest::
+
+ >>> @attrs.define
+ ... class C:
+ ... x = attrs.field(validator=attrs.validators.is_callable())
+ >>> C(isinstance)
+ C(x=<built-in function isinstance>)
+ >>> C("not a callable")
+ Traceback (most recent call last):
+ ...
+ attr.exceptions.NotCallableError: 'x' must be callable (got 'not a callable' that is a <class 'str'>).
+
+
+.. autofunction:: attrs.validators.matches_re
+
+ For example:
+
+ .. doctest::
+
+ >>> @attrs.define
+ ... class User:
+ ... email = attrs.field(validator=attrs.validators.matches_re(
+ ... "(^[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")
+ Traceback (most recent call last):
+ ...
+ ValueError: ("'email' must match regex '(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\\\.[a-zA-Z0-9-.]+$)' ('user@example.com@test.com' doesn't)", Attribute(name='email', default=NOTHING, validator=<matches_re validator for pattern re.compile('(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$)')>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), re.compile('(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$)'), 'user@example.com@test.com')
+
+
+.. autofunction:: attrs.validators.deep_iterable
+
+ For example:
+
+ .. doctest::
+
+ >>> @attrs.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)
+ ... ))
+ >>> C(x=[1, 2, 3])
+ C(x=[1, 2, 3])
+ >>> C(x=set([1, 2, 3]))
+ Traceback (most recent call last):
+ ...
+ TypeError: ("'x' must be <class 'list'> (got {1, 2, 3} that is a <class 'set'>).", Attribute(name='x', default=NOTHING, validator=<deep_iterable validator for <instance_of validator for type <class 'list'>> iterables of <instance_of validator for type <class 'int'>>>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), <class 'list'>, {1, 2, 3})
+ >>> C(x=[1, 2, "3"])
+ Traceback (most recent call last):
+ ...
+ TypeError: ("'x' must be <class 'int'> (got '3' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=<deep_iterable validator for <instance_of validator for type <class 'list'>> iterables of <instance_of validator for type <class 'int'>>>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), <class 'int'>, '3')
+
+
+.. autofunction:: attrs.validators.deep_mapping
+
+ For example:
+
+ .. doctest::
+
+ >>> @attrs.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)
+ ... ))
+ >>> C(x={"a": 1, "b": 2})
+ C(x={'a': 1, 'b': 2})
+ >>> C(x=None)
+ Traceback (most recent call last):
+ ...
+ TypeError: ("'x' must be <class 'dict'> (got None that is a <class 'NoneType'>).", Attribute(name='x', default=NOTHING, validator=<deep_mapping validator for objects mapping <instance_of validator for type <class 'str'>> to <instance_of validator for type <class 'int'>>>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), <class 'dict'>, None)
+ >>> C(x={"a": 1.0, "b": 2})
+ Traceback (most recent call last):
+ ...
+ TypeError: ("'x' must be <class 'int'> (got 1.0 that is a <class 'float'>).", Attribute(name='x', default=NOTHING, validator=<deep_mapping validator for objects mapping <instance_of validator for type <class 'str'>> to <instance_of validator for type <class 'int'>>>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), <class 'int'>, 1.0)
+ >>> C(x={"a": 1, 7: 2})
+ Traceback (most recent call last):
+ ...
+ TypeError: ("'x' must be <class 'str'> (got 7 that is a <class 'int'>).", Attribute(name='x', default=NOTHING, validator=<deep_mapping validator for objects mapping <instance_of validator for type <class 'str'>> to <instance_of validator for type <class 'int'>>>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), <class 'str'>, 7)
+
+Validators can be both globally and locally disabled:
+
+.. autofunction:: attrs.validators.set_disabled
+
+.. autofunction:: attrs.validators.get_disabled
+
+.. autofunction:: attrs.validators.disabled
+
+
+Converters
+----------
+
+All objects from ``attrs.converters`` are also available from ``attr.converters``.
+
+.. autofunction:: attrs.converters.pipe
+
+ For convenience, it's also possible to pass a list to `attr.ib`'s converter argument.
+
+ Thus the following two statements are equivalent::
+
+ x = attr.ib(converter=attr.converter.pipe(c1, c2, c3))
+ x = attr.ib(converter=[c1, c2, c3])
+
+.. autofunction:: attrs.converters.optional
+
+ For example:
+
+ .. doctest::
+
+ >>> @attr.s
+ ... class C(object):
+ ... x = attr.ib(converter=attr.converters.optional(int))
+ >>> C(None)
+ C(x=None)
+ >>> C(42)
+ C(x=42)
+
+
+.. autofunction:: attrs.converters.default_if_none
+
+ For example:
+
+ .. doctest::
+
+ >>> @attr.s
+ ... class C(object):
+ ... x = attr.ib(
+ ... converter=attr.converters.default_if_none("")
+ ... )
+ >>> C(None)
+ C(x='')
+
+
+.. autofunction:: attrs.converters.to_bool
+
+ For example:
+
+ .. doctest::
+
+ >>> @attr.s
+ ... class C(object):
+ ... x = attr.ib(
+ ... converter=attr.converters.to_bool
+ ... )
+ >>> C("yes")
+ C(x=True)
+ >>> C(0)
+ C(x=False)
+ >>> C("foo")
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ ValueError: Cannot convert value to bool: foo
+
+
+
+.. _api_setters:
+
+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``.
+
+.. autofunction:: attrs.setters.frozen
+.. autofunction:: attrs.setters.validate
+.. autofunction:: attrs.setters.convert
+.. autofunction:: attrs.setters.pipe
+.. autodata:: attrs.setters.NO_OP
+
+ For example, only ``x`` is frozen here:
+
+ .. doctest::
+
+ >>> @attrs.define(on_setattr=attr.setters.frozen)
+ ... class C:
+ ... x = attr.field()
+ ... y = attr.field(on_setattr=attr.setters.NO_OP)
+ >>> c = C(1, 2)
+ >>> c.y = 3
+ >>> c.y
+ 3
+ >>> c.x = 4
+ Traceback (most recent call last):
+ ...
+ 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/testing/web-platform/tests/tools/third_party/attrs/docs/changelog.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/changelog.rst
new file mode 100644
index 0000000000..565b0521d0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/changelog.rst
@@ -0,0 +1 @@
+.. include:: ../CHANGELOG.rst
diff --git a/testing/web-platform/tests/tools/third_party/attrs/docs/comparison.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/comparison.rst
new file mode 100644
index 0000000000..760124ca3b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/comparison.rst
@@ -0,0 +1,66 @@
+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/testing/web-platform/tests/tools/third_party/attrs/docs/conf.py b/testing/web-platform/tests/tools/third_party/attrs/docs/conf.py
new file mode 100644
index 0000000000..0cc80be6a6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/conf.py
@@ -0,0 +1,155 @@
+# SPDX-License-Identifier: MIT
+
+from importlib import metadata
+
+
+# -- General configuration ------------------------------------------------
+
+doctest_global_setup = """
+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",
+]
+
+# In nitpick mode (-n), still ignore any of the following "broken" references
+# to non-types.
+nitpick_ignore = [
+ ("py:class", "Any value"),
+ ("py:class", "callable"),
+ ("py:class", "callables"),
+ ("py:class", "tuple of types"),
+]
+
+# 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.todo",
+ "notfound.extension",
+]
+
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# The suffix of source filenames.
+source_suffix = ".rst"
+
+# The master toctree document.
+master_doc = "index"
+
+# General information about the project.
+project = "attrs"
+author = "Hynek Schlawack"
+copyright = f"2015, {author}"
+
+# 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 full version, including alpha/beta/rc tags.
+release = metadata.version("attrs")
+# 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.
+exclude_patterns = ["_build"]
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+default_role = "any"
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+add_function_parentheses = True
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+
+html_theme = "furo"
+html_theme_options = {
+ "sidebar_hide_name": True,
+ "light_logo": "attrs_logo.svg",
+ "dark_logo": "attrs_logo_white.svg",
+}
+
+
+# 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 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 = False
+
+# 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 = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = "attrsdoc"
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [("index", "attrs", "attrs Documentation", ["Hynek Schlawack"], 1)]
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (
+ "index",
+ "attrs",
+ "attrs Documentation",
+ "Hynek Schlawack",
+ "attrs",
+ "Python Clases Without Boilerplate",
+ "Miscellaneous",
+ )
+]
+
+epub_description = "Python Clases Without Boilerplate"
+
+intersphinx_mapping = {
+ "https://docs.python.org/3": None,
+}
+
+# Allow non-local URIs so we can have images in CHANGELOG etc.
+suppress_warnings = ["image.nonlocal_uri"]
diff --git a/testing/web-platform/tests/tools/third_party/attrs/docs/docutils.conf b/testing/web-platform/tests/tools/third_party/attrs/docs/docutils.conf
new file mode 100644
index 0000000000..db8ca82c74
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/docutils.conf
@@ -0,0 +1,3 @@
+[parsers]
+[restructuredtext parser]
+smart_quotes=yes
diff --git a/testing/web-platform/tests/tools/third_party/attrs/docs/examples.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/examples.rst
new file mode 100644
index 0000000000..ba5343d4ad
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/examples.rst
@@ -0,0 +1,709 @@
+``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/testing/web-platform/tests/tools/third_party/attrs/docs/extending.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/extending.rst
new file mode 100644
index 0000000000..faf71afd91
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/extending.rst
@@ -0,0 +1,313 @@
+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/testing/web-platform/tests/tools/third_party/attrs/docs/glossary.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/glossary.rst
new file mode 100644
index 0000000000..5fd01f4fb1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/glossary.rst
@@ -0,0 +1,104 @@
+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/testing/web-platform/tests/tools/third_party/attrs/docs/hashing.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/hashing.rst
new file mode 100644
index 0000000000..30888f97bb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/hashing.rst
@@ -0,0 +1,86 @@
+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/testing/web-platform/tests/tools/third_party/attrs/docs/how-does-it-work.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/how-does-it-work.rst
new file mode 100644
index 0000000000..f899740542
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/how-does-it-work.rst
@@ -0,0 +1,109 @@
+.. _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/testing/web-platform/tests/tools/third_party/attrs/docs/index.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/index.rst
new file mode 100644
index 0000000000..ff65a6738c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/index.rst
@@ -0,0 +1,100 @@
+.. 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/testing/web-platform/tests/tools/third_party/attrs/docs/init.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/init.rst
new file mode 100644
index 0000000000..fb276ded8a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/init.rst
@@ -0,0 +1,489 @@
+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/testing/web-platform/tests/tools/third_party/attrs/docs/license.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/license.rst
new file mode 100644
index 0000000000..a341a31eb9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/license.rst
@@ -0,0 +1,8 @@
+===================
+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/testing/web-platform/tests/tools/third_party/attrs/docs/names.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/names.rst
new file mode 100644
index 0000000000..0fe953e6a5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/names.rst
@@ -0,0 +1,122 @@
+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/testing/web-platform/tests/tools/third_party/attrs/docs/overview.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/overview.rst
new file mode 100644
index 0000000000..b35f66f2dd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/overview.rst
@@ -0,0 +1,58 @@
+========
+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/testing/web-platform/tests/tools/third_party/attrs/docs/python-2.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/python-2.rst
new file mode 100644
index 0000000000..7ec9e5112c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/python-2.rst
@@ -0,0 +1,25 @@
+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/testing/web-platform/tests/tools/third_party/attrs/docs/types.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/types.rst
new file mode 100644
index 0000000000..fbb90a7e93
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/types.rst
@@ -0,0 +1,108 @@
+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/testing/web-platform/tests/tools/third_party/attrs/docs/why.rst b/testing/web-platform/tests/tools/third_party/attrs/docs/why.rst
new file mode 100644
index 0000000000..2c0ca4cd66
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/docs/why.rst
@@ -0,0 +1,290 @@
+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/testing/web-platform/tests/tools/third_party/attrs/mypy.ini b/testing/web-platform/tests/tools/third_party/attrs/mypy.ini
new file mode 100644
index 0000000000..685c02599f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/mypy.ini
@@ -0,0 +1,3 @@
+[mypy]
+disallow_untyped_defs = True
+check_untyped_defs = True
diff --git a/testing/web-platform/tests/tools/third_party/attrs/pyproject.toml b/testing/web-platform/tests/tools/third_party/attrs/pyproject.toml
new file mode 100644
index 0000000000..52c0e49ec2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/pyproject.toml
@@ -0,0 +1,71 @@
+[build-system]
+requires = ["setuptools>=40.6.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+
+[tool.coverage.run]
+parallel = true
+branch = true
+source = ["attr", "attrs"]
+
+[tool.coverage.paths]
+source = ["src", ".tox/*/site-packages"]
+
+[tool.coverage.report]
+show_missing = true
+exclude_lines = [
+ "pragma: no cover",
+ # PyPy is unacceptably slow under coverage.
+ "if PYPY:",
+]
+
+
+[tool.black]
+line-length = 79
+extend-exclude = '''
+# Exclude pattern matching test till black gains Python 3.10 support
+.*test_pattern_matching.*
+'''
+
+
+[tool.interrogate]
+verbose = 2
+fail-under = 100
+whitelist-regex = ["test_.*"]
+
+
+[tool.check-wheel-contents]
+toplevel = ["attr", "attrs"]
+
+
+[tool.isort]
+profile = "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
diff --git a/testing/web-platform/tests/tools/third_party/attrs/setup.py b/testing/web-platform/tests/tools/third_party/attrs/setup.py
new file mode 100644
index 0000000000..00e7b012ae
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/setup.py
@@ -0,0 +1,151 @@
+# 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/testing/web-platform/tests/tools/third_party/attrs/src/attr/__init__.py b/testing/web-platform/tests/tools/third_party/attrs/src/attr/__init__.py
new file mode 100644
index 0000000000..f95c96dd57
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/__init__.py
@@ -0,0 +1,80 @@
+# SPDX-License-Identifier: MIT
+
+from __future__ import absolute_import, division, print_function
+
+import sys
+
+from functools import partial
+
+from . import converters, exceptions, filters, setters, validators
+from ._cmp import cmp_using
+from ._config import get_run_validators, set_run_validators
+from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types
+from ._make import (
+ NOTHING,
+ Attribute,
+ Factory,
+ attrib,
+ attrs,
+ fields,
+ fields_dict,
+ make_class,
+ validate,
+)
+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 ;)
+
+__all__ = [
+ "Attribute",
+ "Factory",
+ "NOTHING",
+ "asdict",
+ "assoc",
+ "astuple",
+ "attr",
+ "attrib",
+ "attributes",
+ "attrs",
+ "cmp_using",
+ "converters",
+ "evolve",
+ "exceptions",
+ "fields",
+ "fields_dict",
+ "filters",
+ "get_run_validators",
+ "has",
+ "ib",
+ "make_class",
+ "resolve_types",
+ "s",
+ "set_run_validators",
+ "setters",
+ "validate",
+ "validators",
+]
+
+if sys.version_info[:2] >= (3, 6):
+ from ._next_gen import define, field, frozen, mutable # noqa: F401
+
+ __all__.extend(("define", "field", "frozen", "mutable"))
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/__init__.pyi b/testing/web-platform/tests/tools/third_party/attrs/src/attr/__init__.pyi
new file mode 100644
index 0000000000..c0a2126503
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/__init__.pyi
@@ -0,0 +1,484 @@
+import sys
+
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Generic,
+ List,
+ Mapping,
+ Optional,
+ Sequence,
+ Tuple,
+ Type,
+ TypeVar,
+ Union,
+ overload,
+)
+
+# `import X as X` is required to make these public
+from . import converters as converters
+from . import exceptions as exceptions
+from . import filters as filters
+from . import setters as setters
+from . import validators as validators
+from ._version_info import VersionInfo
+
+__version__: str
+__version_info__: VersionInfo
+__title__: str
+__description__: str
+__url__: str
+__uri__: str
+__author__: str
+__email__: str
+__license__: str
+__copyright__: str
+
+_T = TypeVar("_T")
+_C = TypeVar("_C", bound=type)
+
+_EqOrderType = Union[bool, Callable[[Any], Any]]
+_ValidatorType = Callable[[Any, Attribute[_T], _T], Any]
+_ConverterType = Callable[[Any], Any]
+_FilterType = Callable[[Attribute[_T], _T], bool]
+_ReprType = Callable[[Any], str]
+_ReprArgType = Union[bool, _ReprType]
+_OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any]
+_OnSetAttrArgType = Union[
+ _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType
+]
+_FieldTransformer = Callable[
+ [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 --
+
+NOTHING: object
+
+# NOTE: Factory lies about its return type to make this possible:
+# `x: List[int] # = Factory(list)`
+# Work around mypy issue #4554 in the common case by using an overload.
+if sys.version_info >= (3, 8):
+ from typing import Literal
+ @overload
+ def Factory(factory: Callable[[], _T]) -> _T: ...
+ @overload
+ def Factory(
+ factory: Callable[[Any], _T],
+ takes_self: Literal[True],
+ ) -> _T: ...
+ @overload
+ def Factory(
+ factory: Callable[[], _T],
+ takes_self: Literal[False],
+ ) -> _T: ...
+
+else:
+ @overload
+ def Factory(factory: Callable[[], _T]) -> _T: ...
+ @overload
+ def Factory(
+ factory: Union[Callable[[Any], _T], Callable[[], _T]],
+ 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]
+ validator: Optional[_ValidatorType[_T]]
+ repr: _ReprArgType
+ cmp: _EqOrderType
+ eq: _EqOrderType
+ order: _EqOrderType
+ hash: Optional[bool]
+ init: bool
+ converter: Optional[_ConverterType]
+ metadata: Dict[Any, Any]
+ type: Optional[Type[_T]]
+ kw_only: bool
+ on_setattr: _OnSetAttrType
+ def evolve(self, **changes: Any) -> "Attribute[Any]": ...
+
+# NOTE: We had several choices for the annotation to use for type arg:
+# 1) Type[_T]
+# - Pros: Handles simple cases correctly
+# - Cons: Might produce less informative errors in the case of conflicting
+# TypeVars e.g. `attr.ib(default='bad', type=int)`
+# 2) Callable[..., _T]
+# - Pros: Better error messages than #1 for conflicting TypeVars
+# - Cons: Terrible error messages for validator checks.
+# e.g. attr.ib(type=int, validator=validate_str)
+# -> error: Cannot infer function type argument
+# 3) type (and do all of the work in the mypy plugin)
+# - Pros: Simple here, and we could customize the plugin with our own errors.
+# - Cons: Would need to write mypy plugin code to handle all the cases.
+# We chose option #1.
+
+# `attr` lies about its return type to make the following possible:
+# attr() -> Any
+# attr(8) -> int
+# attr(validator=<some callable>) -> Whatever the callable expects.
+# This makes this type of assignments possible:
+# x: int = attr(8)
+#
+# This form catches explicit None or no default but with no other arguments
+# returns Any.
+@overload
+def attrib(
+ default: None = ...,
+ validator: None = ...,
+ repr: _ReprArgType = ...,
+ cmp: Optional[_EqOrderType] = ...,
+ hash: Optional[bool] = ...,
+ init: bool = ...,
+ metadata: Optional[Mapping[Any, Any]] = ...,
+ type: None = ...,
+ converter: None = ...,
+ factory: None = ...,
+ kw_only: bool = ...,
+ eq: Optional[_EqOrderType] = ...,
+ order: Optional[_EqOrderType] = ...,
+ on_setattr: Optional[_OnSetAttrArgType] = ...,
+) -> Any: ...
+
+# This form catches an explicit None or no default and infers the type from the
+# other arguments.
+@overload
+def attrib(
+ default: None = ...,
+ validator: Optional[_ValidatorArgType[_T]] = ...,
+ repr: _ReprArgType = ...,
+ cmp: Optional[_EqOrderType] = ...,
+ hash: Optional[bool] = ...,
+ init: bool = ...,
+ metadata: Optional[Mapping[Any, Any]] = ...,
+ type: Optional[Type[_T]] = ...,
+ converter: Optional[_ConverterType] = ...,
+ factory: Optional[Callable[[], _T]] = ...,
+ kw_only: bool = ...,
+ eq: Optional[_EqOrderType] = ...,
+ order: Optional[_EqOrderType] = ...,
+ on_setattr: Optional[_OnSetAttrArgType] = ...,
+) -> _T: ...
+
+# This form catches an explicit default argument.
+@overload
+def attrib(
+ default: _T,
+ validator: Optional[_ValidatorArgType[_T]] = ...,
+ repr: _ReprArgType = ...,
+ cmp: Optional[_EqOrderType] = ...,
+ hash: Optional[bool] = ...,
+ init: bool = ...,
+ metadata: Optional[Mapping[Any, Any]] = ...,
+ type: Optional[Type[_T]] = ...,
+ converter: Optional[_ConverterType] = ...,
+ factory: Optional[Callable[[], _T]] = ...,
+ kw_only: bool = ...,
+ eq: Optional[_EqOrderType] = ...,
+ order: Optional[_EqOrderType] = ...,
+ on_setattr: Optional[_OnSetAttrArgType] = ...,
+) -> _T: ...
+
+# This form covers type=non-Type: e.g. forward references (str), Any
+@overload
+def attrib(
+ default: Optional[_T] = ...,
+ validator: Optional[_ValidatorArgType[_T]] = ...,
+ repr: _ReprArgType = ...,
+ cmp: Optional[_EqOrderType] = ...,
+ hash: Optional[bool] = ...,
+ init: bool = ...,
+ metadata: Optional[Mapping[Any, Any]] = ...,
+ type: object = ...,
+ converter: Optional[_ConverterType] = ...,
+ factory: Optional[Callable[[], _T]] = ...,
+ kw_only: bool = ...,
+ eq: Optional[_EqOrderType] = ...,
+ order: Optional[_EqOrderType] = ...,
+ on_setattr: Optional[_OnSetAttrArgType] = ...,
+) -> Any: ...
+@overload
+def field(
+ *,
+ default: None = ...,
+ validator: None = ...,
+ repr: _ReprArgType = ...,
+ hash: Optional[bool] = ...,
+ init: bool = ...,
+ metadata: Optional[Mapping[Any, Any]] = ...,
+ converter: None = ...,
+ factory: None = ...,
+ kw_only: bool = ...,
+ eq: Optional[bool] = ...,
+ order: Optional[bool] = ...,
+ on_setattr: Optional[_OnSetAttrArgType] = ...,
+) -> Any: ...
+
+# This form catches an explicit None or no default and infers the type from the
+# other arguments.
+@overload
+def field(
+ *,
+ default: None = ...,
+ validator: Optional[_ValidatorArgType[_T]] = ...,
+ repr: _ReprArgType = ...,
+ hash: Optional[bool] = ...,
+ init: bool = ...,
+ metadata: Optional[Mapping[Any, Any]] = ...,
+ converter: Optional[_ConverterType] = ...,
+ factory: Optional[Callable[[], _T]] = ...,
+ kw_only: bool = ...,
+ eq: Optional[_EqOrderType] = ...,
+ order: Optional[_EqOrderType] = ...,
+ on_setattr: Optional[_OnSetAttrArgType] = ...,
+) -> _T: ...
+
+# This form catches an explicit default argument.
+@overload
+def field(
+ *,
+ default: _T,
+ validator: Optional[_ValidatorArgType[_T]] = ...,
+ repr: _ReprArgType = ...,
+ hash: Optional[bool] = ...,
+ init: bool = ...,
+ metadata: Optional[Mapping[Any, Any]] = ...,
+ converter: Optional[_ConverterType] = ...,
+ factory: Optional[Callable[[], _T]] = ...,
+ kw_only: bool = ...,
+ eq: Optional[_EqOrderType] = ...,
+ order: Optional[_EqOrderType] = ...,
+ on_setattr: Optional[_OnSetAttrArgType] = ...,
+) -> _T: ...
+
+# This form covers type=non-Type: e.g. forward references (str), Any
+@overload
+def field(
+ *,
+ default: Optional[_T] = ...,
+ validator: Optional[_ValidatorArgType[_T]] = ...,
+ repr: _ReprArgType = ...,
+ hash: Optional[bool] = ...,
+ init: bool = ...,
+ metadata: Optional[Mapping[Any, Any]] = ...,
+ converter: Optional[_ConverterType] = ...,
+ factory: Optional[Callable[[], _T]] = ...,
+ kw_only: bool = ...,
+ eq: Optional[_EqOrderType] = ...,
+ order: Optional[_EqOrderType] = ...,
+ on_setattr: Optional[_OnSetAttrArgType] = ...,
+) -> Any: ...
+@overload
+@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))
+def attrs(
+ maybe_cls: _C,
+ these: Optional[Dict[str, Any]] = ...,
+ repr_ns: Optional[str] = ...,
+ repr: bool = ...,
+ cmp: Optional[_EqOrderType] = ...,
+ 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[_EqOrderType] = ...,
+ order: Optional[_EqOrderType] = ...,
+ auto_detect: bool = ...,
+ collect_by_mro: bool = ...,
+ getstate_setstate: Optional[bool] = ...,
+ on_setattr: Optional[_OnSetAttrArgType] = ...,
+ field_transformer: Optional[_FieldTransformer] = ...,
+ match_args: bool = ...,
+) -> _C: ...
+@overload
+@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))
+def attrs(
+ maybe_cls: None = ...,
+ these: Optional[Dict[str, Any]] = ...,
+ repr_ns: Optional[str] = ...,
+ repr: bool = ...,
+ cmp: Optional[_EqOrderType] = ...,
+ 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[_EqOrderType] = ...,
+ order: Optional[_EqOrderType] = ...,
+ auto_detect: bool = ...,
+ collect_by_mro: bool = ...,
+ getstate_setstate: Optional[bool] = ...,
+ on_setattr: Optional[_OnSetAttrArgType] = ...,
+ field_transformer: Optional[_FieldTransformer] = ...,
+ match_args: bool = ...,
+) -> Callable[[_C], _C]: ...
+@overload
+@__dataclass_transform__(field_descriptors=(attrib, field))
+def define(
+ maybe_cls: _C,
+ *,
+ these: Optional[Dict[str, Any]] = ...,
+ repr: 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__(field_descriptors=(attrib, field))
+def define(
+ maybe_cls: None = ...,
+ *,
+ these: Optional[Dict[str, Any]] = ...,
+ repr: 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]: ...
+
+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: ...
+def resolve_types(
+ cls: _C,
+ globalns: Optional[Dict[str, Any]] = ...,
+ localns: Optional[Dict[str, Any]] = ...,
+ attribs: Optional[List[Attribute[Any]]] = ...,
+) -> _C: ...
+
+# 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',
+# [attr.ib()])` is valid
+def make_class(
+ name: str,
+ attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]],
+ bases: Tuple[type, ...] = ...,
+ repr_ns: Optional[str] = ...,
+ repr: bool = ...,
+ cmp: Optional[_EqOrderType] = ...,
+ 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[_EqOrderType] = ...,
+ order: Optional[_EqOrderType] = ...,
+ collect_by_mro: bool = ...,
+ on_setattr: Optional[_OnSetAttrArgType] = ...,
+ field_transformer: Optional[_FieldTransformer] = ...,
+) -> type: ...
+
+# _funcs --
+
+# TODO: add support for returning TypedDict from the mypy plugin
+# FIXME: asdict/astuple do not honor their factory args. Waiting on one of
+# these:
+# https://github.com/python/mypy/issues/4236
+# https://github.com/python/typing/issues/253
+# XXX: remember to fix attrs.asdict/astuple too!
+def asdict(
+ inst: Any,
+ recurse: bool = ...,
+ filter: Optional[_FilterType[Any]] = ...,
+ dict_factory: Type[Mapping[Any, Any]] = ...,
+ retain_collection_types: bool = ...,
+ value_serializer: Optional[
+ Callable[[type, Attribute[Any], Any], Any]
+ ] = ...,
+ tuple_keys: Optional[bool] = ...,
+) -> Dict[str, Any]: ...
+
+# TODO: add support for returning NamedTuple from the mypy plugin
+def astuple(
+ inst: Any,
+ recurse: bool = ...,
+ filter: Optional[_FilterType[Any]] = ...,
+ tuple_factory: Type[Sequence[Any]] = ...,
+ retain_collection_types: bool = ...,
+) -> Tuple[Any, ...]: ...
+def has(cls: type) -> bool: ...
+def assoc(inst: _T, **changes: Any) -> _T: ...
+def evolve(inst: _T, **changes: Any) -> _T: ...
+
+# _config --
+
+def set_run_validators(run: bool) -> None: ...
+def get_run_validators() -> bool: ...
+
+# aliases --
+
+s = attributes = attrs
+ib = attr = attrib
+dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;)
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/_cmp.py b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_cmp.py
new file mode 100644
index 0000000000..6cffa4dbab
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_cmp.py
@@ -0,0 +1,154 @@
+# SPDX-License-Identifier: MIT
+
+from __future__ import absolute_import, division, print_function
+
+import functools
+
+from ._compat import new_class
+from ._make import _make_ne
+
+
+_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="}
+
+
+def cmp_using(
+ eq=None,
+ lt=None,
+ le=None,
+ gt=None,
+ ge=None,
+ require_same_type=True,
+ 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.
+
+ :param bool require_same_type: When `True`, equality and ordering methods
+ will return `NotImplemented` if objects are not of the same type.
+
+ :param Optional[str] class_name: Name of class. Defaults to 'Comparable'.
+
+ See `comparison` for more details.
+
+ .. versionadded:: 21.1.0
+ """
+
+ body = {
+ "__slots__": ["value"],
+ "__init__": _make_init(),
+ "_requirements": [],
+ "_is_comparable_to": _is_comparable_to,
+ }
+
+ # Add operations.
+ num_order_functions = 0
+ has_eq_function = False
+
+ if eq is not None:
+ has_eq_function = True
+ body["__eq__"] = _make_operator("eq", eq)
+ body["__ne__"] = _make_ne()
+
+ if lt is not None:
+ num_order_functions += 1
+ body["__lt__"] = _make_operator("lt", lt)
+
+ if le is not None:
+ num_order_functions += 1
+ body["__le__"] = _make_operator("le", le)
+
+ if gt is not None:
+ num_order_functions += 1
+ body["__gt__"] = _make_operator("gt", gt)
+
+ if ge is not None:
+ num_order_functions += 1
+ body["__ge__"] = _make_operator("ge", ge)
+
+ type_ = new_class(class_name, (object,), {}, lambda ns: ns.update(body))
+
+ # Add same type requirement.
+ if require_same_type:
+ type_._requirements.append(_check_same_type)
+
+ # Add total ordering if at least one operation was defined.
+ if 0 < num_order_functions < 4:
+ 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."
+ )
+ type_ = functools.total_ordering(type_)
+
+ return type_
+
+
+def _make_init():
+ """
+ Create __init__ method.
+ """
+
+ def __init__(self, value):
+ """
+ Initialize object with *value*.
+ """
+ self.value = value
+
+ return __init__
+
+
+def _make_operator(name, func):
+ """
+ Create operator method.
+ """
+
+ def method(self, other):
+ if not self._is_comparable_to(other):
+ return NotImplemented
+
+ result = func(self.value, other.value)
+ if result is NotImplemented:
+ return NotImplemented
+
+ return result
+
+ method.__name__ = "__%s__" % (name,)
+ method.__doc__ = "Return a %s b. Computed by attrs." % (
+ _operation_names[name],
+ )
+
+ return method
+
+
+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
+
+
+def _check_same_type(self, other):
+ """
+ Return True if *self* and *other* are of the same type, False otherwise.
+ """
+ return other.value.__class__ is self.value.__class__
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/_cmp.pyi b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_cmp.pyi
new file mode 100644
index 0000000000..e71aaff7a1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_cmp.pyi
@@ -0,0 +1,13 @@
+from typing import Type
+
+from . import _CompareWithType
+
+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,
+) -> Type: ...
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/_compat.py b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_compat.py
new file mode 100644
index 0000000000..dc0cb02b64
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_compat.py
@@ -0,0 +1,261 @@
+# SPDX-License-Identifier: MIT
+
+from __future__ import absolute_import, division, print_function
+
+import platform
+import sys
+import threading
+import types
+import warnings
+
+
+PY2 = sys.version_info[0] == 2
+PYPY = platform.python_implementation() == "PyPy"
+PY36 = sys.version_info[:2] >= (3, 6)
+HAS_F_STRINGS = PY36
+PY310 = sys.version_info[:2] >= (3, 10)
+
+
+if PYPY or PY36:
+ ordered_dict = dict
+else:
+ from collections import OrderedDict
+
+ ordered_dict = OrderedDict
+
+
+if PY2:
+ from collections import Mapping, Sequence
+
+ 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)
+
+ # TYPE is used in exceptions, repr(int) is different on Python 2 and 3.
+ TYPE = "type"
+
+ def iteritems(d):
+ return d.iteritems()
+
+ # Python 2 is bereft of a read-only dict proxy, so we make one!
+ class ReadOnlyDict(IterableUserDict):
+ """
+ Best-effort read-only dict wrapper.
+ """
+
+ def __setitem__(self, key, val):
+ # We gently pretend we're a Python 3 mappingproxy.
+ raise TypeError(
+ "'mappingproxy' object does not support item assignment"
+ )
+
+ def update(self, _):
+ # We gently pretend we're a Python 3 mappingproxy.
+ raise AttributeError(
+ "'mappingproxy' object has no attribute 'update'"
+ )
+
+ 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):
+ """
+ We only warn on Python 3 because we are not aware of any concrete
+ consequences of not setting the cell on Python 2.
+ """
+ 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)
+
+ TYPE = "class"
+
+ 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
+# about the instances that are already being repr'd through the call stack
+# in order to ensure we don't perform infinite recursion.
+#
+# For instance, if an instance contains a dict which contains that instance,
+# we need to know that we're already repr'ing the outside instance from within
+# the dict's repr() call.
+#
+# This lives here rather than in _make.py so that the functions in _make.py
+# 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()
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/_config.py b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_config.py
new file mode 100644
index 0000000000..fc9be29d00
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_config.py
@@ -0,0 +1,33 @@
+# SPDX-License-Identifier: MIT
+
+from __future__ import absolute_import, division, print_function
+
+
+__all__ = ["set_run_validators", "get_run_validators"]
+
+_run_validators = True
+
+
+def set_run_validators(run):
+ """
+ Set whether or not validators are run. By default, they are run.
+
+ .. deprecated:: 21.3.0 It will not be removed, but it also will not be
+ moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()`
+ instead.
+ """
+ if not isinstance(run, bool):
+ raise TypeError("'run' must be bool.")
+ global _run_validators
+ _run_validators = run
+
+
+def get_run_validators():
+ """
+ Return whether or not validators are run.
+
+ .. deprecated:: 21.3.0 It will not be removed, but it also will not be
+ moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()`
+ instead.
+ """
+ return _run_validators
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/_funcs.py b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_funcs.py
new file mode 100644
index 0000000000..4c90085a40
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_funcs.py
@@ -0,0 +1,422 @@
+# SPDX-License-Identifier: MIT
+
+from __future__ import absolute_import, division, print_function
+
+import copy
+
+from ._compat import iteritems
+from ._make import NOTHING, _obj_setattr, fields
+from .exceptions import AttrsAttributeNotFoundError
+
+
+def asdict(
+ inst,
+ recurse=True,
+ filter=None,
+ dict_factory=dict,
+ retain_collection_types=False,
+ value_serializer=None,
+):
+ """
+ Return the ``attrs`` attribute values of *inst* as a dict.
+
+ Optionally recurse into other ``attrs``-decorated classes.
+
+ :param inst: Instance of an ``attrs``-decorated class.
+ :param bool recurse: Recurse into classes that are also
+ ``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
+ value as the second argument.
+ :param callable dict_factory: A callable to produce dictionaries from. For
+ example, to produce ordered dictionaries instead of normal Python
+ dictionaries, pass in ``collections.OrderedDict``.
+ :param bool retain_collection_types: Do not convert to ``list`` when
+ encountering an attribute whose type is ``tuple`` or ``set``. Only
+ meaningful if ``recurse`` is ``True``.
+ :param Optional[callable] value_serializer: A hook that is called for every
+ attribute or dict key/value. It receives the current instance, field
+ and value and must return the (updated) value. The hook is run *after*
+ the optional *filter* has been applied.
+
+ :rtype: return type of *dict_factory*
+
+ :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
+ class.
+
+ .. versionadded:: 16.0.0 *dict_factory*
+ .. versionadded:: 16.1.0 *retain_collection_types*
+ .. versionadded:: 20.3.0 *value_serializer*
+ .. versionadded:: 21.3.0 If a dict has a collection for a key, it is
+ serialized as a tuple.
+ """
+ attrs = fields(inst.__class__)
+ rv = dict_factory()
+ for a in attrs:
+ v = getattr(inst, a.name)
+ if filter is not None and not filter(a, v):
+ continue
+
+ if value_serializer is not None:
+ v = value_serializer(inst, a, v)
+
+ if recurse is True:
+ if has(v.__class__):
+ rv[a.name] = asdict(
+ v,
+ recurse=True,
+ filter=filter,
+ dict_factory=dict_factory,
+ retain_collection_types=retain_collection_types,
+ value_serializer=value_serializer,
+ )
+ 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
+ ]
+ )
+ elif isinstance(v, dict):
+ df = dict_factory
+ rv[a.name] = df(
+ (
+ _asdict_anything(
+ kk,
+ is_key=True,
+ filter=filter,
+ dict_factory=df,
+ retain_collection_types=retain_collection_types,
+ value_serializer=value_serializer,
+ ),
+ _asdict_anything(
+ vv,
+ is_key=False,
+ filter=filter,
+ dict_factory=df,
+ retain_collection_types=retain_collection_types,
+ value_serializer=value_serializer,
+ ),
+ )
+ for kk, vv in iteritems(v)
+ )
+ else:
+ rv[a.name] = v
+ else:
+ rv[a.name] = v
+ return rv
+
+
+def _asdict_anything(
+ val,
+ is_key,
+ filter,
+ dict_factory,
+ retain_collection_types,
+ value_serializer,
+):
+ """
+ ``asdict`` only works on attrs instances, this works on anything.
+ """
+ if getattr(val.__class__, "__attrs_attrs__", None) is not None:
+ # Attrs class.
+ rv = asdict(
+ val,
+ recurse=True,
+ filter=filter,
+ dict_factory=dict_factory,
+ retain_collection_types=retain_collection_types,
+ value_serializer=value_serializer,
+ )
+ elif isinstance(val, (tuple, list, set, frozenset)):
+ if retain_collection_types is True:
+ cf = val.__class__
+ elif is_key:
+ cf = tuple
+ else:
+ cf = list
+
+ rv = 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 val
+ ]
+ )
+ elif isinstance(val, dict):
+ df = dict_factory
+ rv = df(
+ (
+ _asdict_anything(
+ kk,
+ is_key=True,
+ filter=filter,
+ dict_factory=df,
+ retain_collection_types=retain_collection_types,
+ value_serializer=value_serializer,
+ ),
+ _asdict_anything(
+ vv,
+ is_key=False,
+ filter=filter,
+ dict_factory=df,
+ retain_collection_types=retain_collection_types,
+ value_serializer=value_serializer,
+ ),
+ )
+ for kk, vv in iteritems(val)
+ )
+ else:
+ rv = val
+ if value_serializer is not None:
+ rv = value_serializer(None, None, rv)
+
+ return rv
+
+
+def astuple(
+ inst,
+ recurse=True,
+ filter=None,
+ tuple_factory=tuple,
+ retain_collection_types=False,
+):
+ """
+ Return the ``attrs`` attribute values of *inst* as a tuple.
+
+ Optionally recurse into other ``attrs``-decorated classes.
+
+ :param inst: Instance of an ``attrs``-decorated class.
+ :param bool recurse: Recurse into classes that are also
+ ``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
+ value as the second argument.
+ :param callable tuple_factory: A callable to produce tuples from. For
+ example, to produce lists instead of tuples.
+ :param bool retain_collection_types: Do not convert to ``list``
+ or ``dict`` when encountering an attribute which type is
+ ``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is
+ ``True``.
+
+ :rtype: return type of *tuple_factory*
+
+ :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
+ class.
+
+ .. versionadded:: 16.2.0
+ """
+ attrs = fields(inst.__class__)
+ rv = []
+ retain = retain_collection_types # Very long. :/
+ for a in attrs:
+ v = getattr(inst, a.name)
+ if filter is not None and not filter(a, v):
+ continue
+ if recurse is True:
+ if has(v.__class__):
+ rv.append(
+ astuple(
+ v,
+ recurse=True,
+ filter=filter,
+ tuple_factory=tuple_factory,
+ retain_collection_types=retain,
+ )
+ )
+ 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
+ ]
+ )
+ )
+ elif isinstance(v, dict):
+ df = v.__class__ if retain is True else dict
+ rv.append(
+ df(
+ (
+ astuple(
+ kk,
+ tuple_factory=tuple_factory,
+ retain_collection_types=retain,
+ )
+ if has(kk.__class__)
+ else kk,
+ astuple(
+ vv,
+ tuple_factory=tuple_factory,
+ retain_collection_types=retain,
+ )
+ if has(vv.__class__)
+ else vv,
+ )
+ for kk, vv in iteritems(v)
+ )
+ )
+ else:
+ rv.append(v)
+ else:
+ rv.append(v)
+
+ return rv if tuple_factory is list else tuple_factory(rv)
+
+
+def has(cls):
+ """
+ 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
+
+
+def assoc(inst, **changes):
+ """
+ Copy *inst* and apply *changes*.
+
+ :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``
+ class.
+
+ .. deprecated:: 17.1.0
+ Use `attrs.evolve` instead if you can.
+ 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):
+ 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__
+ )
+ )
+ _obj_setattr(new, k, v)
+ return new
+
+
+def evolve(inst, **changes):
+ """
+ Create a new instance, based on *inst* with *changes* applied.
+
+ :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``
+ class.
+
+ .. versionadded:: 17.1.0
+ """
+ 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:]
+ 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):
+ """
+ Resolve any strings and forward annotations in type annotations.
+
+ This is only required if you need concrete types in `Attribute`'s *type*
+ field. In other words, you don't need to resolve your types if you only
+ use them for static type checking.
+
+ With no arguments, names will be looked up in the module in which the class
+ was created. If this is not what you want, e.g. if the name only exists
+ inside a method, you may pass *globalns* or *localns* to specify other
+ dictionaries in which to look up these names. See the docs of
+ `typing.get_type_hints` for more details.
+
+ :param type cls: Class to resolve.
+ :param Optional[dict] globalns: Dictionary containing global variables.
+ :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.
+
+ :raise TypeError: If *cls* is not a class.
+ :raise attr.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.
+
+ :returns: *cls* so you can use this function also as a class decorator.
+ Please note that you have to apply it **after** `attrs.define`. That
+ means the decorator has to come in the line **before** `attrs.define`.
+
+ .. versionadded:: 20.1.0
+ .. versionadded:: 21.1.0 *attribs*
+
+ """
+ # Since calling get_type_hints is expensive we cache whether we've
+ # done it already.
+ if getattr(cls, "__attrs_types_resolved__", None) != cls:
+ import typing
+
+ hints = typing.get_type_hints(cls, globalns=globalns, localns=localns)
+ 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.
+ _obj_setattr(field, "type", hints[field.name])
+ # We store the class we resolved so that subclasses know they haven't
+ # been resolved.
+ cls.__attrs_types_resolved__ = cls
+
+ # Return the class so you can use it as a decorator too.
+ return cls
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/_make.py b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_make.py
new file mode 100644
index 0000000000..d46f8a3e7a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_make.py
@@ -0,0 +1,3173 @@
+# SPDX-License-Identifier: MIT
+
+from __future__ import absolute_import, division, print_function
+
+import copy
+import inspect
+import linecache
+import sys
+import warnings
+
+from operator import itemgetter
+
+# We need to import _compat itself in addition to the _compat members to avoid
+# 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,
+)
+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}))"
+)
+_classvar_prefixes = (
+ "typing.ClassVar",
+ "t.ClassVar",
+ "ClassVar",
+ "typing_extensions.ClassVar",
+)
+# we don't use a double-underscore prefix because that triggers
+# name mangling when trying to create a slot for the field
+# (when slots=True)
+_hash_cache_field = "_attrs_cached_hash"
+
+_empty_metadata_singleton = metadata_proxy({})
+
+# Unique object for unequivocal getattr() defaults.
+_sentinel = object()
+
+_ng_default_on_setattr = setters.pipe(setters.convert, setters.validate)
+
+
+class _Nothing(object):
+ """
+ Sentinel class to indicate the lack of a value when ``None`` is ambiguous.
+
+ ``_Nothing`` is a singleton. There is only ever one of it.
+
+ .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False.
+ """
+
+ _singleton = None
+
+ def __new__(cls):
+ if _Nothing._singleton is None:
+ _Nothing._singleton = super(_Nothing, cls).__new__(cls)
+ return _Nothing._singleton
+
+ def __repr__(self):
+ return "NOTHING"
+
+ def __bool__(self):
+ return False
+
+ def __len__(self):
+ return 0 # __bool__ for Python 2
+
+
+NOTHING = _Nothing()
+"""
+Sentinel to indicate the lack of a value when ``None`` is ambiguous.
+"""
+
+
+class _CacheHashWrapper(int):
+ """
+ An integer subclass that pickles / copies as None
+
+ This is used for non-slots classes with ``cache_hash=True``, to avoid
+ serializing a potentially (even likely) invalid hash value. Since ``None``
+ is the default value for uncalculated hashes, whenever this is copied,
+ the copy's value for the hash should automatically reset.
+
+ 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 attrib(
+ default=NOTHING,
+ validator=None,
+ repr=True,
+ cmp=None,
+ hash=None,
+ init=True,
+ metadata=None,
+ type=None,
+ converter=None,
+ factory=None,
+ kw_only=False,
+ eq=None,
+ order=None,
+ on_setattr=None,
+):
+ """
+ Create a new attribute on a class.
+
+ .. warning::
+
+ Does *not* do anything unless the class is also decorated with
+ `attr.s`!
+
+ :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``.
+
+ If the value is an instance of `attrs.Factory`, its callable will be
+ used to construct a new value (useful for mutable data types like lists
+ 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.
+
+ The default can also be set using decorator notation as shown below.
+
+ :type default: Any value
+
+ :param callable factory: Syntactic sugar for
+ ``default=attr.Factory(factory)``.
+
+ :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.
+
+ 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.
+
+ Validators can be globally disabled and re-enabled using
+ `get_run_validators`.
+
+ The validator can also be set using decorator notation as shown below.
+
+ :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).
+ :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.
+ :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.
+ :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*.
+ :type cmp: a `bool` or a `callable`.
+
+ :param Optional[bool] 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*.
+ :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).
+ :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`
+
+ .. versionadded:: 15.2.0 *convert*
+ .. versionadded:: 16.3.0 *metadata*
+ .. versionchanged:: 17.1.0 *validator* can be a ``list`` now.
+ .. versionchanged:: 17.1.0
+ *hash* is ``None`` and therefore mirrors *eq* by default.
+ .. versionadded:: 17.3.0 *type*
+ .. deprecated:: 17.4.0 *convert*
+ .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated
+ *convert* to achieve consistency with other noun-based arguments.
+ .. versionadded:: 18.1.0
+ ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``.
+ .. versionadded:: 18.2.0 *kw_only*
+ .. versionchanged:: 19.2.0 *convert* keyword argument removed.
+ .. versionchanged:: 19.2.0 *repr* also accepts a custom callable.
+ .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
+ .. versionadded:: 19.2.0 *eq* and *order*
+ .. versionadded:: 20.1.0 *on_setattr*
+ .. versionchanged:: 20.3.0 *kw_only* backported to Python 2
+ .. versionchanged:: 21.1.0
+ *eq*, *order*, and *cmp* also accept a custom callable
+ .. versionchanged:: 21.1.0 *cmp* undeprecated
+ """
+ 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."
+ )
+
+ if factory is not None:
+ if default is not NOTHING:
+ raise ValueError(
+ "The `default` and `factory` arguments are mutually "
+ "exclusive."
+ )
+ if not callable(factory):
+ raise ValueError("The `factory` argument must be a callable.")
+ default = Factory(factory)
+
+ if metadata is None:
+ metadata = {}
+
+ # Apply syntactic sugar by auto-wrapping.
+ if isinstance(on_setattr, (list, tuple)):
+ on_setattr = setters.pipe(*on_setattr)
+
+ if validator and isinstance(validator, (list, tuple)):
+ validator = and_(*validator)
+
+ if converter and isinstance(converter, (list, tuple)):
+ converter = pipe(*converter)
+
+ return _CountingAttr(
+ default=default,
+ validator=validator,
+ repr=repr,
+ cmp=None,
+ hash=hash,
+ init=init,
+ converter=converter,
+ metadata=metadata,
+ type=type,
+ kw_only=kw_only,
+ eq=eq,
+ eq_key=eq_key,
+ order=order,
+ order_key=order_key,
+ on_setattr=on_setattr,
+ )
+
+
+def _compile_and_eval(script, globs, locs=None, filename=""):
+ """
+ "Exec" the script with the given global (globs) and local (locs) variables.
+ """
+ bytecode = compile(script, filename, "exec")
+ eval(bytecode, globs, locs)
+
+
+def _make_method(name, script, filename, globs=None):
+ """
+ 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.
+ count = 1
+ base_filename = filename
+ while True:
+ linecache_tuple = (
+ len(script),
+ None,
+ script.splitlines(True),
+ filename,
+ )
+ old_val = linecache.cache.setdefault(filename, linecache_tuple)
+ if old_val == linecache_tuple:
+ break
+ else:
+ filename = "{}-{}>".format(base_filename[:-1], count)
+ count += 1
+
+ _compile_and_eval(script, globs, locs, filename)
+
+ return locs[name]
+
+
+def _make_attr_tuple_class(cls_name, attr_names):
+ """
+ Create a tuple subclass to hold `Attribute`s for an `attrs` class.
+
+ The subclass is a bare tuple with properties for names.
+
+ class MyClassAttributes(tuple):
+ __slots__ = ()
+ x = property(itemgetter(0))
+ """
+ attr_class_name = "{}Attributes".format(cls_name)
+ attr_class_template = [
+ "class {}(tuple):".format(attr_class_name),
+ " __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)
+ )
+ else:
+ attr_class_template.append(" pass")
+ globs = {"_attrs_itemgetter": itemgetter, "_attrs_property": property}
+ _compile_and_eval("\n".join(attr_class_template), globs)
+ return globs[attr_class_name]
+
+
+# Tuple class for extracted attributes from a class definition.
+# `base_attrs` is a subset of `attrs`.
+_Attributes = _make_attr_tuple_class(
+ "_Attributes",
+ [
+ # all attributes to build dunder methods for
+ "attrs",
+ # attributes that have been inherited
+ "base_attrs",
+ # map inherited attributes to their originating classes
+ "base_attrs_map",
+ ],
+)
+
+
+def _is_class_var(annot):
+ """
+ Check whether *annot* is a typing.ClassVar.
+
+ The string comparison hack is used to avoid evaluating all string
+ annotations which would put attrs-based classes at a performance
+ disadvantage compared to plain old classes.
+ """
+ annot = str(annot)
+
+ # Annotation can be quoted.
+ if annot.startswith(("'", '"')) and annot.endswith(("'", '"')):
+ annot = annot[1:-1]
+
+ return annot.startswith(_classvar_prefixes)
+
+
+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:
+ return False
+
+ for base_cls in cls.__mro__[1:]:
+ a = getattr(base_cls, attrib_name, None)
+ if attr is a:
+ return False
+
+ return True
+
+
+def _get_annotations(cls):
+ """
+ Get annotations for *cls*.
+ """
+ if _has_own_attribute(cls, "__annotations__"):
+ return cls.__annotations__
+
+ 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*.
+ """
+ base_attrs = []
+ base_attr_map = {} # A dictionary of base attrs to their classes.
+
+ # Traverse the MRO and collect attributes.
+ for base_cls in reversed(cls.__mro__[1:-1]):
+ for a in getattr(base_cls, "__attrs_attrs__", []):
+ if a.inherited or a.name in taken_attr_names:
+ continue
+
+ a = a.evolve(inherited=True)
+ base_attrs.append(a)
+ base_attr_map[a.name] = base_cls
+
+ # For each name, only keep the freshest definition i.e. the furthest at the
+ # back. base_attr_map is fine because it gets overwritten with every new
+ # instance.
+ filtered = []
+ seen = set()
+ for a in reversed(base_attrs):
+ if a.name in seen:
+ continue
+ filtered.insert(0, a)
+ seen.add(a.name)
+
+ return filtered, base_attr_map
+
+
+def _collect_base_attrs_broken(cls, taken_attr_names):
+ """
+ Collect attr.ibs from base classes of *cls*, except *taken_attr_names*.
+
+ N.B. *taken_attr_names* will be mutated.
+
+ Adhere to the old incorrect behavior.
+
+ Notably it collects from the front and considers inherited attributes which
+ leads to the buggy behavior reported in #428.
+ """
+ base_attrs = []
+ base_attr_map = {} # A dictionary of base attrs to their classes.
+
+ # Traverse the MRO and collect attributes.
+ for base_cls in cls.__mro__[1:-1]:
+ for a in getattr(base_cls, "__attrs_attrs__", []):
+ if a.name in taken_attr_names:
+ continue
+
+ a = a.evolve(inherited=True)
+ taken_attr_names.add(a.name)
+ base_attrs.append(a)
+ base_attr_map[a.name] = base_cls
+
+ return base_attrs, base_attr_map
+
+
+def _transform_attrs(
+ cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer
+):
+ """
+ Transform all `_CountingAttr`s on a class into `Attribute`s.
+
+ If *these* is passed, use that and don't look for them on the class.
+
+ *collect_by_mro* is True, collect them in the correct MRO order, otherwise
+ use the old -- incorrect -- order. See #428.
+
+ Return an `_Attributes`.
+ """
+ cd = cls.__dict__
+ 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)
+ elif auto_attribs is True:
+ ca_names = {
+ name
+ for name, attr in cd.items()
+ if isinstance(attr, _CountingAttr)
+ }
+ ca_list = []
+ annot_names = set()
+ for attr_name, type in anns.items():
+ if _is_class_var(type):
+ continue
+ annot_names.add(attr_name)
+ a = cd.get(attr_name, NOTHING)
+
+ if not isinstance(a, _CountingAttr):
+ if a is NOTHING:
+ a = attrib()
+ else:
+ a = attrib(default=a)
+ ca_list.append((attr_name, a))
+
+ unannotated = ca_names - annot_names
+ if len(unannotated) > 0:
+ raise UnannotatedAttributeError(
+ "The following `attr.ib`s lack a type annotation: "
+ + ", ".join(
+ sorted(unannotated, key=lambda n: cd.get(n).counter)
+ )
+ + "."
+ )
+ else:
+ ca_list = sorted(
+ (
+ (name, attr)
+ for name, attr in cd.items()
+ if isinstance(attr, _CountingAttr)
+ ),
+ key=lambda e: e[1].counter,
+ )
+
+ own_attrs = [
+ Attribute.from_counting_attr(
+ name=attr_name, ca=ca, type=anns.get(attr_name)
+ )
+ for attr_name, ca in ca_list
+ ]
+
+ if collect_by_mro:
+ base_attrs, base_attr_map = _collect_base_attrs(
+ cls, {a.name for a in own_attrs}
+ )
+ else:
+ base_attrs, base_attr_map = _collect_base_attrs_broken(
+ cls, {a.name for a in own_attrs}
+ )
+
+ if kw_only:
+ own_attrs = [a.evolve(kw_only=True) for a in own_attrs]
+ base_attrs = [a.evolve(kw_only=True) for a in base_attrs]
+
+ attrs = base_attrs + own_attrs
+
+ # Mandatory vs non-mandatory attr order only matters when they are part of
+ # the __init__ signature and when they aren't kw_only (which are moved to
+ # the end and can be mandatory or non-mandatory in any order, as they will
+ # be specified as keyword args anyway). Check the order of those 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,)
+ )
+
+ if had_default is False and a.default is not NOTHING:
+ had_default = True
+
+ if field_transformer is not None:
+ attrs = field_transformer(cls, attrs)
+
+ # Create AttrsClass *after* applying the field_transformer since it may
+ # add or remove attributes!
+ attr_names = [a.name for a in attrs]
+ AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names)
+
+ return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map))
+
+
+if PYPY:
+
+ 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
+
+ raise FrozenInstanceError()
+
+else:
+
+ def _frozen_setattrs(self, name, value):
+ """
+ Attached to frozen classes as __setattr__.
+ """
+ raise FrozenInstanceError()
+
+
+def _frozen_delattrs(self, name):
+ """
+ Attached to frozen classes as __delattr__.
+ """
+ raise FrozenInstanceError()
+
+
+class _ClassBuilder(object):
+ """
+ Iteratively build *one* class.
+ """
+
+ __slots__ = (
+ "_attr_names",
+ "_attrs",
+ "_base_attr_map",
+ "_base_names",
+ "_cache_hash",
+ "_cls",
+ "_cls_dict",
+ "_delete_attribs",
+ "_frozen",
+ "_has_pre_init",
+ "_has_post_init",
+ "_is_exc",
+ "_on_setattr",
+ "_slots",
+ "_weakref_slot",
+ "_wrote_own_setattr",
+ "_has_custom_setattr",
+ )
+
+ def __init__(
+ self,
+ cls,
+ these,
+ slots,
+ frozen,
+ weakref_slot,
+ getstate_setstate,
+ auto_attribs,
+ kw_only,
+ cache_hash,
+ is_exc,
+ collect_by_mro,
+ on_setattr,
+ has_custom_setattr,
+ field_transformer,
+ ):
+ attrs, base_attrs, base_map = _transform_attrs(
+ cls,
+ these,
+ auto_attribs,
+ kw_only,
+ collect_by_mro,
+ field_transformer,
+ )
+
+ 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_attr_map = base_map
+ self._attr_names = tuple(a.name for a in attrs)
+ self._slots = slots
+ self._frozen = frozen
+ self._weakref_slot = weakref_slot
+ self._cache_hash = cache_hash
+ self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False))
+ self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))
+ self._delete_attribs = not bool(these)
+ self._is_exc = is_exc
+ self._on_setattr = on_setattr
+
+ self._has_custom_setattr = has_custom_setattr
+ self._wrote_own_setattr = False
+
+ self._cls_dict["__attrs_attrs__"] = self._attrs
+
+ if frozen:
+ self._cls_dict["__setattr__"] = _frozen_setattrs
+ self._cls_dict["__delattr__"] = _frozen_delattrs
+
+ self._wrote_own_setattr = True
+ elif on_setattr in (
+ _ng_default_on_setattr,
+ setters.validate,
+ setters.convert,
+ ):
+ has_validator = has_converter = False
+ for a in attrs:
+ if a.validator is not None:
+ has_validator = True
+ if a.converter is not None:
+ has_converter = True
+
+ if has_validator and has_converter:
+ break
+ if (
+ (
+ on_setattr == _ng_default_on_setattr
+ and not (has_validator or has_converter)
+ )
+ or (on_setattr == setters.validate and not has_validator)
+ or (on_setattr == setters.convert and not has_converter)
+ ):
+ # If class-level on_setattr is set to convert + validate, but
+ # there's no field to convert or validate, pretend like there's
+ # no on_setattr.
+ self._on_setattr = None
+
+ if getstate_setstate:
+ (
+ self._cls_dict["__getstate__"],
+ self._cls_dict["__setstate__"],
+ ) = self._make_getstate_setstate()
+
+ def __repr__(self):
+ return "<_ClassBuilder(cls={cls})>".format(cls=self._cls.__name__)
+
+ 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()
+ else:
+ return self._patch_original_class()
+
+ def _patch_original_class(self):
+ """
+ Apply accumulated methods and return the class.
+ """
+ cls = self._cls
+ base_names = self._base_names
+
+ # Clean class of attribute definitions (`attr.ib()`s).
+ if self._delete_attribs:
+ for name in self._attr_names:
+ if (
+ name not in base_names
+ and getattr(cls, name, _sentinel) is not _sentinel
+ ):
+ try:
+ 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():
+ setattr(cls, name, value)
+
+ # If we've inherited an attrs __setattr__ and don't write our own,
+ # reset it to object's.
+ if not self._wrote_own_setattr and getattr(
+ cls, "__attrs_own_setattr__", False
+ ):
+ cls.__attrs_own_setattr__ = False
+
+ if not self._has_custom_setattr:
+ cls.__setattr__ = object.__setattr__
+
+ return cls
+
+ def _create_slots_class(self):
+ """
+ Build and return a new class with a `__slots__` attribute.
+ """
+ cd = {
+ k: v
+ for k, v in iteritems(self._cls_dict)
+ if k not in tuple(self._attr_names) + ("__dict__", "__weakref__")
+ }
+
+ # If our class doesn't have its own implementation of __setattr__
+ # (either from the user or by us), check the bases, if one of them has
+ # an attrs-made __setattr__, that needs to be reset. We don't walk the
+ # MRO because we only care about our immediate base classes.
+ # XXX: This can be confused by subclassing a slotted attrs class with
+ # XXX: a non-attrs class and subclass the resulting class with an attrs
+ # XXX: class. See `test_slotted_confused` for details. For now that's
+ # XXX: OK with us.
+ if not self._wrote_own_setattr:
+ cd["__attrs_own_setattr__"] = False
+
+ 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__
+ break
+
+ # Traverse the MRO to collect existing slots
+ # and check for an existing __weakref__.
+ existing_slots = dict()
+ weakref_inherited = False
+ for base_cls in self._cls.__mro__[1:-1]:
+ if base_cls.__dict__.get("__weakref__", None) is not None:
+ weakref_inherited = True
+ existing_slots.update(
+ {
+ name: getattr(base_cls, name)
+ for name in getattr(base_cls, "__slots__", [])
+ }
+ )
+
+ base_names = set(self._base_names)
+
+ names = self._attr_names
+ if (
+ self._weakref_slot
+ and "__weakref__" not in getattr(self._cls, "__slots__", ())
+ and "__weakref__" not in names
+ and not weakref_inherited
+ ):
+ names += ("__weakref__",)
+
+ # 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,
+ # we collect them here and update the class dict
+ reused_slots = {
+ slot: slot_descriptor
+ for slot, slot_descriptor in iteritems(existing_slots)
+ 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
+
+ # 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
+ # 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():
+ if isinstance(item, (classmethod, staticmethod)):
+ # Class- and staticmethods hide their functions inside.
+ # These might need to be rewritten as well.
+ closure_cells = getattr(item.__func__, "__closure__", None)
+ elif isinstance(item, property):
+ # Workaround for property `super()` shortcut (PY3-only).
+ # There is no universal way for other descriptors.
+ closure_cells = getattr(item.fget, "__closure__", None)
+ else:
+ closure_cells = getattr(item, "__closure__", None)
+
+ if not closure_cells: # Catch None or the empty list.
+ continue
+ for cell in closure_cells:
+ try:
+ match = cell.cell_contents is self._cls
+ except ValueError: # ValueError: Cell is empty
+ pass
+ else:
+ if match:
+ set_closure_cell(cell, cls)
+
+ return cls
+
+ def add_repr(self, ns):
+ self._cls_dict["__repr__"] = self._add_method_dunders(
+ _make_repr(self._attrs, ns, self._cls)
+ )
+ return self
+
+ 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."
+ )
+
+ def __str__(self):
+ return self.__repr__()
+
+ self._cls_dict["__str__"] = self._add_method_dunders(__str__)
+ return self
+
+ def _make_getstate_setstate(self):
+ """
+ Create custom __setstate__ and __getstate__ methods.
+ """
+ # __weakref__ is not writable.
+ state_attr_names = tuple(
+ an for an in self._attr_names if an != "__weakref__"
+ )
+
+ def slots_getstate(self):
+ """
+ Automatically created by attrs.
+ """
+ return tuple(getattr(self, name) for name in state_attr_names)
+
+ hash_caching_enabled = self._cache_hash
+
+ def slots_setstate(self, state):
+ """
+ Automatically created by attrs.
+ """
+ __bound_setattr = _obj_setattr.__get__(self, Attribute)
+ for name, value in zip(state_attr_names, state):
+ __bound_setattr(name, value)
+
+ # The hash code cache is not included when the object is
+ # serialized, but it still needs to be initialized to None to
+ # indicate that the first call to __hash__ should be a cache
+ # miss.
+ if hash_caching_enabled:
+ __bound_setattr(_hash_cache_field, None)
+
+ return slots_getstate, slots_setstate
+
+ def make_unhashable(self):
+ self._cls_dict["__hash__"] = None
+ return self
+
+ def add_hash(self):
+ self._cls_dict["__hash__"] = self._add_method_dunders(
+ _make_hash(
+ self._cls,
+ self._attrs,
+ frozen=self._frozen,
+ cache_hash=self._cache_hash,
+ )
+ )
+
+ return self
+
+ def add_init(self):
+ self._cls_dict["__init__"] = self._add_method_dunders(
+ _make_init(
+ self._cls,
+ self._attrs,
+ self._has_pre_init,
+ self._has_post_init,
+ self._frozen,
+ self._slots,
+ self._cache_hash,
+ self._base_attr_map,
+ self._is_exc,
+ self._on_setattr,
+ attrs_init=False,
+ )
+ )
+
+ return self
+
+ def add_match_args(self):
+ self._cls_dict["__match_args__"] = tuple(
+ field.name
+ for field in self._attrs
+ if field.init and not field.kw_only
+ )
+
+ def add_attrs_init(self):
+ self._cls_dict["__attrs_init__"] = self._add_method_dunders(
+ _make_init(
+ self._cls,
+ self._attrs,
+ self._has_pre_init,
+ self._has_post_init,
+ self._frozen,
+ self._slots,
+ self._cache_hash,
+ self._base_attr_map,
+ self._is_exc,
+ self._on_setattr,
+ attrs_init=True,
+ )
+ )
+
+ return self
+
+ def add_eq(self):
+ cd = self._cls_dict
+
+ cd["__eq__"] = self._add_method_dunders(
+ _make_eq(self._cls, self._attrs)
+ )
+ cd["__ne__"] = self._add_method_dunders(_make_ne())
+
+ return self
+
+ def add_order(self):
+ cd = self._cls_dict
+
+ cd["__lt__"], cd["__le__"], cd["__gt__"], cd["__ge__"] = (
+ self._add_method_dunders(meth)
+ for meth in _make_order(self._cls, self._attrs)
+ )
+
+ return self
+
+ def add_setattr(self):
+ if self._frozen:
+ return self
+
+ sa_attrs = {}
+ for a in self._attrs:
+ on_setattr = a.on_setattr or self._on_setattr
+ if on_setattr and on_setattr is not setters.NO_OP:
+ sa_attrs[a.name] = a, on_setattr
+
+ if not sa_attrs:
+ return self
+
+ 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."
+ )
+
+ # docstring comes from _add_method_dunders
+ def __setattr__(self, name, val):
+ try:
+ a, hook = sa_attrs[name]
+ except KeyError:
+ nval = val
+ else:
+ nval = hook(self, a, val)
+
+ _obj_setattr(self, name, nval)
+
+ self._cls_dict["__attrs_own_setattr__"] = True
+ self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__)
+ self._wrote_own_setattr = True
+
+ return self
+
+ def _add_method_dunders(self, method):
+ """
+ Add __module__ and __qualname__ to a *method* if possible.
+ """
+ try:
+ method.__module__ = self._cls.__module__
+ except AttributeError:
+ pass
+
+ try:
+ method.__qualname__ = ".".join(
+ (self._cls.__qualname__, method.__name__)
+ )
+ except AttributeError:
+ pass
+
+ try:
+ method.__doc__ = "Method generated by attrs for class %s." % (
+ 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`.")
+
+ # cmp takes precedence due to bw-compatibility.
+ if cmp is not None:
+ return cmp, cmp
+
+ # If left None, equality is set to the specified default and ordering
+ # mirrors equality.
+ if eq is None:
+ eq = default_eq
+
+ if order is None:
+ order = eq
+
+ if eq is False and order is True:
+ raise ValueError("`order` can only be True if `eq` is True too.")
+
+ return eq, order
+
+
+def _determine_attrib_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`.")
+
+ def decide_callable_or_boolean(value):
+ """
+ Decide whether a key function is used.
+ """
+ if callable(value):
+ value, key = True, value
+ else:
+ key = None
+ return value, key
+
+ # cmp takes precedence due to bw-compatibility.
+ if cmp is not None:
+ cmp, cmp_key = decide_callable_or_boolean(cmp)
+ return cmp, cmp_key, cmp, cmp_key
+
+ # If left None, equality is set to the specified default and ordering
+ # mirrors equality.
+ if eq is None:
+ eq, eq_key = default_eq, None
+ else:
+ eq, eq_key = decide_callable_or_boolean(eq)
+
+ if order is None:
+ order, order_key = eq, eq_key
+ else:
+ 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.")
+
+ return eq, eq_key, order, order_key
+
+
+def _determine_whether_to_implement(
+ cls, flag, auto_detect, dunders, default=True
+):
+ """
+ Check whether we should implement a set of methods for *cls*.
+
+ *flag* is the argument passed into @attr.s like 'init', *auto_detect* the
+ same as passed into @attr.s and *dunders* is a tuple of attribute names
+ 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
+
+ if flag is None and auto_detect is False:
+ return default
+
+ # Logically, flag is None and auto_detect is True here.
+ for dunder in dunders:
+ if _has_own_attribute(cls, dunder):
+ return False
+
+ return default
+
+
+def attrs(
+ maybe_cls=None,
+ 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,
+):
+ r"""
+ A class decorator that adds `dunder
+ <https://wiki.python.org/moin/DunderAlias>`_\ -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
+ 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
+ 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.
+
+ :type these: `dict` of `str` to `attr.ib`
+
+ :param str repr_ns: When using nested classes, there's no way in Python 2
+ to automatically detect that. Therefore it's possible to set the
+ namespace explicitly for a more meaningful ``repr`` output.
+ :param bool auto_detect: Instead of setting the *init*, *repr*, *eq*,
+ *order*, and *hash* arguments explicitly, assume they are set to
+ ``True`` **unless any** of the involved methods for one of the
+ 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).
+
+ .. warning::
+
+ 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`.
+
+ :param bool repr: Create a ``__repr__`` method with a human readable
+ 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__``
+ and ``__ne__`` methods that check two instances for equality.
+
+ 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__``,
+ ``__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.
+ 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
+ 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>`.
+ :param bool frozen: Make instances immutable after initialization. If
+ someone attempts to modify a frozen instance,
+ `attr.exceptions.FrozenInstanceError` is raised.
+
+ .. note::
+
+ 1. This is achieved by installing a custom ``__setattr__`` method
+ on your class, so you can't implement your own.
+
+ 2. True immutability is impossible in Python.
+
+ 3. This *does* have a minor a runtime performance `impact
+ <how-frozen>` when initializing new instances. In other words:
+ ``__init__`` is slightly slower with ``frozen=True``.
+
+ 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)``.
+
+ 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.
+
+ 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
+ ``attr.ib(default=42)``. Passing an instance of `attrs.Factory` also
+ works as expected in most cases (see warning below).
+
+ Attributes annotated as `typing.ClassVar`, and attributes that are
+ neither annotated nor set to an `attr.ib` are **ignored**.
+
+ .. 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.
+
+ 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:
+
+ - the values for *eq*, *order*, and *hash* are ignored and the
+ 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``
+ 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.
+
+ :param Optional[bool] 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 *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
+ an attribute (either by assignment like ``i.x = 42`` or by using
+ `setattr` like ``setattr(i, "x", 42)``). It receives the same arguments
+ as validators: the instance, the attribute that is being modified, and
+ the new value.
+
+ If no exception is raised, the attribute is set to the return value of
+ the callable.
+
+ If a list of callables is passed, they're automatically wrapped in an
+ `attrs.setters.pipe`.
+
+ :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 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.
+
+ .. versionadded:: 16.0.0 *slots*
+ .. versionadded:: 16.1.0 *frozen*
+ .. versionadded:: 16.3.0 *str*
+ .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``.
+ .. versionchanged:: 17.1.0
+ *hash* supports ``None`` as value which is also the default now.
+ .. versionadded:: 17.3.0 *auto_attribs*
+ .. versionchanged:: 18.1.0
+ If *these* is passed, no attributes are deleted from the class body.
+ .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained.
+ .. versionadded:: 18.2.0 *weakref_slot*
+ .. deprecated:: 18.2.0
+ ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a
+ `DeprecationWarning` if the classes compared are subclasses of
+ each other. ``__eq`` and ``__ne__`` never tried to compared subclasses
+ to each other.
+ .. versionchanged:: 19.2.0
+ ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider
+ subclasses comparable anymore.
+ .. versionadded:: 18.2.0 *kw_only*
+ .. versionadded:: 18.2.0 *cache_hash*
+ .. versionadded:: 19.1.0 *auto_exc*
+ .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
+ .. versionadded:: 19.2.0 *eq* and *order*
+ .. versionadded:: 20.1.0 *auto_detect*
+ .. versionadded:: 20.1.0 *collect_by_mro*
+ .. versionadded:: 20.1.0 *getstate_setstate*
+ .. versionadded:: 20.1.0 *on_setattr*
+ .. versionadded:: 20.3.0 *field_transformer*
+ .. versionchanged:: 21.1.0
+ ``init=False`` injects ``__attrs_init__``
+ .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__``
+ .. versionchanged:: 21.1.0 *cmp* undeprecated
+ .. versionadded:: 21.3.0 *match_args*
+ """
+ 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
+
+ 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(
+ cls, "__setattr__"
+ )
+
+ if has_own_setattr and is_frozen:
+ raise ValueError("Can't freeze a class with a custom __setattr__.")
+
+ builder = _ClassBuilder(
+ cls,
+ these,
+ slots,
+ is_frozen,
+ weakref_slot,
+ _determine_whether_to_implement(
+ cls,
+ getstate_setstate,
+ auto_detect,
+ ("__getstate__", "__setstate__"),
+ default=slots,
+ ),
+ auto_attribs,
+ kw_only,
+ cache_hash,
+ is_exc,
+ collect_by_mro,
+ on_setattr,
+ has_own_setattr,
+ field_transformer,
+ )
+ if _determine_whether_to_implement(
+ cls, repr, auto_detect, ("__repr__",)
+ ):
+ builder.add_repr(repr_ns)
+ if str is True:
+ builder.add_str()
+
+ eq = _determine_whether_to_implement(
+ cls, eq_, auto_detect, ("__eq__", "__ne__")
+ )
+ if not is_exc and eq is True:
+ builder.add_eq()
+ if not is_exc and _determine_whether_to_implement(
+ cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__")
+ ):
+ builder.add_order()
+
+ builder.add_setattr()
+
+ if (
+ 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:
+ # 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."
+ )
+ elif hash is True or (
+ hash is None and eq is True and is_frozen is True
+ ):
+ # Build a __hash__ if told so, or if it's safe.
+ builder.add_hash()
+ 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."
+ )
+ builder.make_unhashable()
+
+ if _determine_whether_to_implement(
+ cls, init, auto_detect, ("__init__",)
+ ):
+ builder.add_init()
+ else:
+ builder.add_attrs_init()
+ if cache_hash:
+ raise TypeError(
+ "Invalid value for cache_hash. To use hash caching,"
+ " init must be True."
+ )
+
+ if (
+ PY310
+ and match_args
+ and not _has_own_attribute(cls, "__match_args__")
+ ):
+ builder.add_match_args()
+
+ return builder.build_class()
+
+ # maybe_cls's type depends on the usage of the decorator. It's a class
+ # if it's used as `@attrs` but ``None`` if used as `@attrs()`.
+ if maybe_cls is None:
+ return wrap
+ else:
+ return wrap(maybe_cls)
+
+
+_attrs = attrs
+"""
+Internal alias so we can use it in functions that take an argument called
+*attrs*.
+"""
+
+
+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 _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 unique_filename
+
+
+def _make_hash(cls, attrs, frozen, cache_hash):
+ attrs = tuple(
+ a for a in attrs if a.hash is True or (a.hash is None and a.eq is True)
+ )
+
+ tab = " "
+
+ unique_filename = _generate_unique_filename(cls, "hash")
+ type_hash = hash(unique_filename)
+
+ hash_def = "def __hash__(self"
+ hash_func = "hash(("
+ closing_braces = "))"
+ if not cache_hash:
+ hash_def += "):"
+ else:
+ if not PY2:
+ hash_def += ", *"
+
+ hash_def += (
+ ", _cache_wrapper="
+ + "__import__('attr._make')._make._CacheHashWrapper):"
+ )
+ hash_func = "_cache_wrapper(" + hash_func
+ closing_braces += ")"
+
+ method_lines = [hash_def]
+
+ def append_hash_computation_lines(prefix, indent):
+ """
+ Generate the code for actually computing the hash code.
+ Below this will either be returned directly or used to compute
+ a value which is then cached, depending on the value of cache_hash
+ """
+
+ method_lines.extend(
+ [
+ indent + prefix + hash_func,
+ indent + " %d," % (type_hash,),
+ ]
+ )
+
+ for a in attrs:
+ method_lines.append(indent + " self.%s," % a.name)
+
+ method_lines.append(indent + " " + closing_braces)
+
+ if cache_hash:
+ method_lines.append(tab + "if self.%s is None:" % _hash_cache_field)
+ if frozen:
+ append_hash_computation_lines(
+ "object.__setattr__(self, '%s', " % _hash_cache_field, tab * 2
+ )
+ method_lines.append(tab * 2 + ")") # close __setattr__
+ else:
+ append_hash_computation_lines(
+ "self.%s = " % _hash_cache_field, tab * 2
+ )
+ method_lines.append(tab + "return self.%s" % _hash_cache_field)
+ else:
+ append_hash_computation_lines("return ", tab)
+
+ script = "\n".join(method_lines)
+ return _make_method("__hash__", script, unique_filename)
+
+
+def _add_hash(cls, attrs):
+ """
+ Add a hash method to *cls*.
+ """
+ cls.__hash__ = _make_hash(cls, attrs, frozen=False, cache_hash=False)
+ return cls
+
+
+def _make_ne():
+ """
+ Create __ne__ method.
+ """
+
+ def __ne__(self, other):
+ """
+ Check equality and either forward a NotImplemented or
+ return the result negated.
+ """
+ result = self.__eq__(other)
+ if result is NotImplemented:
+ return NotImplemented
+
+ return not result
+
+ return __ne__
+
+
+def _make_eq(cls, attrs):
+ """
+ Create __eq__ method for *cls* with *attrs*.
+ """
+ attrs = [a for a in attrs if a.eq]
+
+ unique_filename = _generate_unique_filename(cls, "eq")
+ lines = [
+ "def __eq__(self, other):",
+ " if other.__class__ is not self.__class__:",
+ " return NotImplemented",
+ ]
+
+ # We can't just do a big self.x = other.x and... clause due to
+ # irregularities like nan == nan is false but (nan,) == (nan,) is true.
+ globs = {}
+ if attrs:
+ lines.append(" return (")
+ others = [" ) == ("]
+ for a in attrs:
+ if a.eq_key:
+ cmp_name = "_%s_key" % (a.name,)
+ # 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,
+ )
+ )
+ else:
+ lines.append(" self.%s," % (a.name,))
+ others.append(" other.%s," % (a.name,))
+
+ lines += others + [" )"]
+ else:
+ lines.append(" return True")
+
+ script = "\n".join(lines)
+
+ return _make_method("__eq__", script, unique_filename, globs)
+
+
+def _make_order(cls, attrs):
+ """
+ Create ordering methods for *cls* with *attrs*.
+ """
+ attrs = [a for a in attrs if a.order]
+
+ def attrs_to_tuple(obj):
+ """
+ Save us some typing.
+ """
+ return tuple(
+ key(value) if key else value
+ for value, key in (
+ (getattr(obj, a.name), a.order_key) for a in attrs
+ )
+ )
+
+ def __lt__(self, other):
+ """
+ Automatically created by attrs.
+ """
+ if other.__class__ is self.__class__:
+ return attrs_to_tuple(self) < attrs_to_tuple(other)
+
+ return NotImplemented
+
+ def __le__(self, other):
+ """
+ Automatically created by attrs.
+ """
+ if other.__class__ is self.__class__:
+ return attrs_to_tuple(self) <= attrs_to_tuple(other)
+
+ return NotImplemented
+
+ def __gt__(self, other):
+ """
+ Automatically created by attrs.
+ """
+ if other.__class__ is self.__class__:
+ return attrs_to_tuple(self) > attrs_to_tuple(other)
+
+ return NotImplemented
+
+ def __ge__(self, other):
+ """
+ Automatically created by attrs.
+ """
+ if other.__class__ is self.__class__:
+ return attrs_to_tuple(self) >= attrs_to_tuple(other)
+
+ return NotImplemented
+
+ return __lt__, __le__, __gt__, __ge__
+
+
+def _add_eq(cls, attrs=None):
+ """
+ Add equality methods to *cls* with *attrs*.
+ """
+ if attrs is None:
+ attrs = cls.__attrs_attrs__
+
+ cls.__eq__ = _make_eq(cls, attrs)
+ cls.__ne__ = _make_ne()
+
+ 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
+ )
+ 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
+ )
+
+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__
+
+ # 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))
+
+ return __repr__
+
+
+def _add_repr(cls, ns=None, attrs=None):
+ """
+ Add a repr method to *cls*.
+ """
+ if attrs is None:
+ attrs = cls.__attrs_attrs__
+
+ cls.__repr__ = _make_repr(attrs, ns, cls)
+ return cls
+
+
+def fields(cls):
+ """
+ Return the tuple of ``attrs`` attributes for a class.
+
+ The tuple also allows accessing the fields by their names (see below for
+ examples).
+
+ :param type cls: Class to introspect.
+
+ :raise TypeError: If *cls* is not a class.
+ :raise attr.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.
+ """
+ if not isclass(cls):
+ raise TypeError("Passed object must be a class.")
+ attrs = getattr(cls, "__attrs_attrs__", None)
+ if attrs is None:
+ raise NotAnAttrsClassError(
+ "{cls!r} is not an attrs-decorated class.".format(cls=cls)
+ )
+ return attrs
+
+
+def fields_dict(cls):
+ """
+ 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``
+ 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.
+
+ .. versionadded:: 18.1.0
+ """
+ if not isclass(cls):
+ raise TypeError("Passed object must be a class.")
+ 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))
+
+
+def validate(inst):
+ """
+ Validate all attributes on *inst* that have a validator.
+
+ Leaves all exceptions through.
+
+ :param inst: Instance of a class with ``attrs`` attributes.
+ """
+ if _config._run_validators is False:
+ return
+
+ for a in fields(inst.__class__):
+ v = a.validator
+ if v is not None:
+ v(inst, a, getattr(inst, a.name))
+
+
+def _is_slot_cls(cls):
+ return "__slots__" in cls.__dict__
+
+
+def _is_slot_attr(a_name, base_attr_map):
+ """
+ Check if the attribute name comes from a slot class.
+ """
+ return a_name in base_attr_map and _is_slot_cls(base_attr_map[a_name])
+
+
+def _make_init(
+ cls,
+ attrs,
+ pre_init,
+ post_init,
+ frozen,
+ slots,
+ cache_hash,
+ base_attr_map,
+ is_exc,
+ cls_on_setattr,
+ attrs_init,
+):
+ has_cls_on_setattr = (
+ cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP
+ )
+
+ if frozen and has_cls_on_setattr:
+ raise ValueError("Frozen classes can't use on_setattr.")
+
+ needs_cached_setattr = cache_hash or frozen
+ filtered_attrs = []
+ attr_dict = {}
+ for a in attrs:
+ if not a.init and a.default is NOTHING:
+ continue
+
+ filtered_attrs.append(a)
+ attr_dict[a.name] = a
+
+ if a.on_setattr is not None:
+ if frozen is True:
+ raise ValueError("Frozen classes can't use on_setattr.")
+
+ needs_cached_setattr = True
+ elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP:
+ needs_cached_setattr = True
+
+ unique_filename = _generate_unique_filename(cls, "init")
+
+ script, globs, annotations = _attrs_to_init_script(
+ filtered_attrs,
+ frozen,
+ slots,
+ pre_init,
+ post_init,
+ cache_hash,
+ base_attr_map,
+ is_exc,
+ needs_cached_setattr,
+ has_cls_on_setattr,
+ attrs_init,
+ )
+ if cls.__module__ in sys.modules:
+ # This makes typing.get_type_hints(CLS.__init__) resolve string types.
+ globs.update(sys.modules[cls.__module__].__dict__)
+
+ globs.update({"NOTHING": NOTHING, "attr_dict": attr_dict})
+
+ if needs_cached_setattr:
+ # Save the lookup overhead in __init__ if we need to circumvent
+ # setattr hooks.
+ globs["_cached_setattr"] = _obj_setattr
+
+ init = _make_method(
+ "__attrs_init__" if attrs_init else "__init__",
+ script,
+ unique_filename,
+ globs,
+ )
+ init.__annotations__ = annotations
+
+ return init
+
+
+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)
+
+
+def _setattr_with_converter(attr_name, value_var, has_on_setattr):
+ """
+ Use the cached object.setattr to set *attr_name* to *value_var*, but run
+ its converter first.
+ """
+ return "_setattr('%s', %s(%s))" % (
+ attr_name,
+ _init_converter_pat % (attr_name,),
+ value_var,
+ )
+
+
+def _assign(attr_name, value, has_on_setattr):
+ """
+ Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise
+ relegate to _setattr.
+ """
+ if has_on_setattr:
+ return _setattr(attr_name, value, True)
+
+ return "self.%s = %s" % (attr_name, value)
+
+
+def _assign_with_converter(attr_name, value_var, has_on_setattr):
+ """
+ Unless *attr_name* has an on_setattr hook, use normal assignment after
+ conversion. Otherwise relegate to _setattr_with_converter.
+ """
+ if has_on_setattr:
+ return _setattr_with_converter(attr_name, value_var, True)
+
+ return "self.%s = %s(%s)" % (
+ attr_name,
+ _init_converter_pat % (attr_name,),
+ value_var,
+ )
+
+
+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,
+ post_init,
+ cache_hash,
+ base_attr_map,
+ is_exc,
+ needs_cached_setattr,
+ has_cls_on_setattr,
+ attrs_init,
+):
+ """
+ Return a script of an initializer for *attrs* and a dict of globals.
+
+ The globals are expected by the generated script.
+
+ If *frozen* is True, we cannot set the attributes directly so we use
+ a cached ``object.__setattr__``.
+ """
+ lines = []
+ if pre_init:
+ lines.append("self.__attrs_pre_init__()")
+
+ if needs_cached_setattr:
+ lines.append(
+ # 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__)"
+ )
+
+ if frozen is True:
+ if slots is True:
+ fmt_setter = _setattr
+ fmt_setter_with_converter = _setattr_with_converter
+ else:
+ # Dict frozen classes assign directly to __dict__.
+ # But only if the attribute doesn't come from an ancestor slot
+ # class.
+ # Note _inst_dict will be used again below if cache_hash is True
+ lines.append("_inst_dict = self.__dict__")
+
+ def fmt_setter(attr_name, value_var, has_on_setattr):
+ 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)
+
+ def fmt_setter_with_converter(
+ attr_name, value_var, has_on_setattr
+ ):
+ if has_on_setattr or _is_slot_attr(attr_name, base_attr_map):
+ return _setattr_with_converter(
+ attr_name, value_var, has_on_setattr
+ )
+
+ return "_inst_dict['%s'] = %s(%s)" % (
+ attr_name,
+ _init_converter_pat % (attr_name,),
+ value_var,
+ )
+
+ else:
+ # Not frozen.
+ fmt_setter = _assign
+ fmt_setter_with_converter = _assign_with_converter
+
+ args = []
+ kw_only_args = []
+ attrs_to_validate = []
+
+ # This is a dictionary of names to validator and converter callables.
+ # Injecting this into __init__ globals lets us avoid lookups.
+ names_for_globals = {}
+ annotations = {"return": None}
+
+ for a in attrs:
+ if a.validator:
+ attrs_to_validate.append(a)
+
+ attr_name = a.name
+ 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("_")
+
+ has_factory = isinstance(a.default, Factory)
+ if has_factory and a.default.takes_self:
+ maybe_self = "self"
+ else:
+ maybe_self = ""
+
+ if a.init is False:
+ if has_factory:
+ init_factory_name = _init_factory_pat.format(a.name)
+ if a.converter is not None:
+ lines.append(
+ fmt_setter_with_converter(
+ attr_name,
+ init_factory_name + "(%s)" % (maybe_self,),
+ has_on_setattr,
+ )
+ )
+ conv_name = _init_converter_pat % (a.name,)
+ names_for_globals[conv_name] = a.converter
+ else:
+ lines.append(
+ fmt_setter(
+ attr_name,
+ init_factory_name + "(%s)" % (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,
+ )
+ )
+ 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,
+ )
+ )
+ elif a.default is not NOTHING and not has_factory:
+ arg = "%s=attr_dict['%s'].default" % (arg_name, attr_name)
+ if a.kw_only:
+ kw_only_args.append(arg)
+ else:
+ args.append(arg)
+
+ if a.converter is not None:
+ lines.append(
+ fmt_setter_with_converter(
+ attr_name, arg_name, has_on_setattr
+ )
+ )
+ names_for_globals[
+ _init_converter_pat % (a.name,)
+ ] = a.converter
+ else:
+ lines.append(fmt_setter(attr_name, arg_name, has_on_setattr))
+
+ elif has_factory:
+ arg = "%s=NOTHING" % (arg_name,)
+ if a.kw_only:
+ kw_only_args.append(arg)
+ else:
+ args.append(arg)
+ lines.append("if %s is not NOTHING:" % (arg_name,))
+
+ init_factory_name = _init_factory_pat.format(a.name)
+ if a.converter is not None:
+ lines.append(
+ " "
+ + fmt_setter_with_converter(
+ attr_name, arg_name, has_on_setattr
+ )
+ )
+ lines.append("else:")
+ lines.append(
+ " "
+ + fmt_setter_with_converter(
+ attr_name,
+ init_factory_name + "(" + maybe_self + ")",
+ has_on_setattr,
+ )
+ )
+ names_for_globals[
+ _init_converter_pat % (a.name,)
+ ] = a.converter
+ else:
+ lines.append(
+ " " + fmt_setter(attr_name, arg_name, has_on_setattr)
+ )
+ lines.append("else:")
+ lines.append(
+ " "
+ + fmt_setter(
+ attr_name,
+ init_factory_name + "(" + maybe_self + ")",
+ has_on_setattr,
+ )
+ )
+ names_for_globals[init_factory_name] = a.default.factory
+ else:
+ if a.kw_only:
+ kw_only_args.append(arg_name)
+ else:
+ args.append(arg_name)
+
+ if a.converter is not None:
+ lines.append(
+ fmt_setter_with_converter(
+ attr_name, arg_name, has_on_setattr
+ )
+ )
+ names_for_globals[
+ _init_converter_pat % (a.name,)
+ ] = a.converter
+ else:
+ lines.append(fmt_setter(attr_name, arg_name, has_on_setattr))
+
+ 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:
+ # 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
+
+ if attrs_to_validate: # we can skip this if there are no validators.
+ names_for_globals["_config"] = _config
+ lines.append("if _config._run_validators is True:")
+ 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)
+ )
+ 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
+ # 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 frozen and slots, then _setattr defined above
+ init_hash_cache = "_setattr('%s', %s)"
+ else:
+ # if frozen and not slots, then _inst_dict defined above
+ init_hash_cache = "_inst_dict['%s'] = %s"
+ else:
+ init_hash_cache = "self.%s = %s"
+ lines.append(init_hash_cache % (_hash_cache_field, "None"))
+
+ # 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)
+
+ lines.append("BaseException.__init__(self, %s)" % (vals,))
+
+ args = ", ".join(args)
+ if kw_only_args:
+ if PY2:
+ lines = _unpack_kw_only_lines_py2(kw_only_args) + lines
+
+ 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",
+ ),
+ names_for_globals,
+ annotations,
+ )
+
+
+class Attribute(object):
+ """
+ *Read-only* representation of an attribute.
+
+ 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.
+ - ``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
+ that are used for comparing and ordering objects by this attribute,
+ respectively. These are set by passing a callable to `attr.ib`'s ``eq``,
+ ``order``, or ``cmp`` arguments. See also :ref:`comparison customization
+ <custom-comparison>`.
+
+ Instances of this class are frequently used for introspection purposes
+ like:
+
+ - `fields` returns a tuple of them.
+ - Validators get them passed as the first argument.
+ - The :ref:`field transformer <transform-fields>` hook receives a list of
+ them.
+
+ .. 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*
+
+ For the full version history of the fields, see `attr.ib`.
+ """
+
+ __slots__ = (
+ "name",
+ "default",
+ "validator",
+ "repr",
+ "eq",
+ "eq_key",
+ "order",
+ "order_key",
+ "hash",
+ "init",
+ "metadata",
+ "type",
+ "converter",
+ "kw_only",
+ "inherited",
+ "on_setattr",
+ )
+
+ def __init__(
+ self,
+ name,
+ default,
+ validator,
+ repr,
+ cmp, # XXX: unused, remove along with other cmp code.
+ hash,
+ init,
+ inherited,
+ metadata=None,
+ type=None,
+ converter=None,
+ kw_only=False,
+ eq=None,
+ eq_key=None,
+ order=None,
+ order_key=None,
+ on_setattr=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)
+
+ # Despite the big red warning, people *do* instantiate `Attribute`
+ # themselves.
+ bound_setattr("name", name)
+ bound_setattr("default", default)
+ bound_setattr("validator", validator)
+ bound_setattr("repr", repr)
+ bound_setattr("eq", eq)
+ bound_setattr("eq_key", eq_key)
+ bound_setattr("order", order)
+ bound_setattr("order_key", order_key)
+ bound_setattr("hash", hash)
+ bound_setattr("init", init)
+ bound_setattr("converter", converter)
+ bound_setattr(
+ "metadata",
+ (
+ metadata_proxy(metadata)
+ if metadata
+ else _empty_metadata_singleton
+ ),
+ )
+ bound_setattr("type", type)
+ bound_setattr("kw_only", kw_only)
+ bound_setattr("inherited", inherited)
+ bound_setattr("on_setattr", on_setattr)
+
+ def __setattr__(self, name, value):
+ raise FrozenInstanceError()
+
+ @classmethod
+ def from_counting_attr(cls, name, ca, type=None):
+ # type holds the annotated value. deal with conflicts:
+ if type is None:
+ type = ca.type
+ elif ca.type is not None:
+ raise ValueError(
+ "Type annotation and type argument cannot both be present"
+ )
+ inst_dict = {
+ k: getattr(ca, k)
+ for k in Attribute.__slots__
+ if k
+ not in (
+ "name",
+ "validator",
+ "default",
+ "type",
+ "inherited",
+ ) # exclude methods and deprecated alias
+ }
+ return cls(
+ name=name,
+ validator=ca._validator,
+ default=ca._default,
+ type=type,
+ cmp=None,
+ inherited=False,
+ **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
+ def evolve(self, **changes):
+ """
+ Copy *self* and apply *changes*.
+
+ This works similarly to `attr.evolve` but that function does not work
+ with ``Attribute``.
+
+ It is mainly meant to be used for `transform-fields`.
+
+ .. versionadded:: 20.3.0
+ """
+ new = copy.copy(self)
+
+ new._setattrs(changes.items())
+
+ return new
+
+ # Don't use _add_pickle since fields(Attribute) doesn't work
+ def __getstate__(self):
+ """
+ Play nice with pickle.
+ """
+ return tuple(
+ getattr(self, name) if name != "metadata" else dict(self.metadata)
+ for name in self.__slots__
+ )
+
+ def __setstate__(self, state):
+ """
+ Play nice with pickle.
+ """
+ self._setattrs(zip(self.__slots__, state))
+
+ def _setattrs(self, name_values_pairs):
+ bound_setattr = _obj_setattr.__get__(self, Attribute)
+ for name, value in name_values_pairs:
+ if name != "metadata":
+ bound_setattr(name, value)
+ else:
+ bound_setattr(
+ name,
+ metadata_proxy(value)
+ if value
+ else _empty_metadata_singleton,
+ )
+
+
+_a = [
+ Attribute(
+ name=name,
+ default=NOTHING,
+ validator=None,
+ repr=True,
+ cmp=None,
+ eq=True,
+ order=False,
+ hash=(name != "metadata"),
+ init=True,
+ inherited=False,
+ )
+ for name in Attribute.__slots__
+]
+
+Attribute = _add_hash(
+ _add_eq(
+ _add_repr(Attribute, attrs=_a),
+ attrs=[a for a in _a if a.name != "inherited"],
+ ),
+ attrs=[a for a in _a if a.hash and a.name != "inherited"],
+)
+
+
+class _CountingAttr(object):
+ """
+ Intermediate representation of attributes that uses a counter to preserve
+ the order in which the attributes have been defined.
+
+ *Internal* data structure of the attrs library. Running into is most
+ likely the result of a bug like a forgotten `@attr.s` decorator.
+ """
+
+ __slots__ = (
+ "counter",
+ "_default",
+ "repr",
+ "eq",
+ "eq_key",
+ "order",
+ "order_key",
+ "hash",
+ "init",
+ "metadata",
+ "_validator",
+ "converter",
+ "type",
+ "kw_only",
+ "on_setattr",
+ )
+ __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",
+ )
+ ) + (
+ Attribute(
+ name="metadata",
+ default=None,
+ validator=None,
+ repr=True,
+ cmp=None,
+ hash=False,
+ init=True,
+ kw_only=False,
+ eq=True,
+ eq_key=None,
+ order=False,
+ order_key=None,
+ inherited=False,
+ on_setattr=None,
+ ),
+ )
+ cls_counter = 0
+
+ def __init__(
+ self,
+ default,
+ validator,
+ repr,
+ cmp,
+ hash,
+ init,
+ converter,
+ metadata,
+ type,
+ kw_only,
+ eq,
+ eq_key,
+ order,
+ order_key,
+ on_setattr,
+ ):
+ _CountingAttr.cls_counter += 1
+ self.counter = _CountingAttr.cls_counter
+ self._default = default
+ self._validator = validator
+ self.converter = converter
+ self.repr = repr
+ self.eq = eq
+ self.eq_key = eq_key
+ self.order = order
+ self.order_key = order_key
+ self.hash = hash
+ self.init = init
+ self.metadata = metadata
+ self.type = type
+ self.kw_only = kw_only
+ self.on_setattr = on_setattr
+
+ def validator(self, meth):
+ """
+ Decorator that adds *meth* to the list of validators.
+
+ Returns *meth* unchanged.
+
+ .. versionadded:: 17.1.0
+ """
+ if self._validator is None:
+ self._validator = meth
+ else:
+ self._validator = and_(self._validator, meth)
+ return meth
+
+ def default(self, meth):
+ """
+ Decorator that allows to set the default for an attribute.
+
+ Returns *meth* unchanged.
+
+ :raises DefaultAlreadySetError: If default has been set before.
+
+ .. versionadded:: 17.1.0
+ """
+ if self._default is not NOTHING:
+ raise DefaultAlreadySetError()
+
+ self._default = Factory(meth, takes_self=True)
+
+ return meth
+
+
+_CountingAttr = _add_eq(_add_repr(_CountingAttr))
+
+
+class Factory(object):
+ """
+ Stores a factory callable.
+
+ If passed as the default value to `attrs.field`, the factory is used to
+ generate a new value.
+
+ :param callable factory: A callable that takes either none or exactly one
+ mandatory positional argument depending on *takes_self*.
+ :param bool takes_self: Pass the partially initialized instance that is
+ being initialized as a positional argument.
+
+ .. versionadded:: 17.1.0 *takes_self*
+ """
+
+ __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
+
+ def __getstate__(self):
+ """
+ Play nice with pickle.
+ """
+ return tuple(getattr(self, name) for name in self.__slots__)
+
+ def __setstate__(self, state):
+ """
+ Play nice with pickle.
+ """
+ for name, value in zip(self.__slots__, state):
+ setattr(self, name, value)
+
+
+_f = [
+ Attribute(
+ name=name,
+ default=NOTHING,
+ validator=None,
+ repr=True,
+ cmp=None,
+ eq=True,
+ order=False,
+ hash=True,
+ init=True,
+ inherited=False,
+ )
+ for name in Factory.__slots__
+]
+
+Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f)
+
+
+def make_class(name, attrs, bases=(object,), **attributes_arguments):
+ """
+ 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.
+
+ 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.
+ :type attrs: `list` or `dict`
+
+ :param tuple bases: Classes that the new class will subclass.
+
+ :param attributes_arguments: Passed unmodified to `attr.s`.
+
+ :return: A new class with *attrs*.
+ :rtype: type
+
+ .. versionadded:: 17.1.0 *bases*
+ .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained.
+ """
+ if isinstance(attrs, dict):
+ cls_dict = attrs
+ elif isinstance(attrs, (list, tuple)):
+ cls_dict = dict((a, attrib()) for a in attrs)
+ else:
+ raise TypeError("attrs argument must be a dict or a list.")
+
+ 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 pre_init is not None:
+ body["__attrs_pre_init__"] = pre_init
+ if post_init is not None:
+ body["__attrs_post_init__"] = post_init
+ if user_init is not None:
+ body["__init__"] = user_init
+
+ type_ = 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:
+ 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)
+ (
+ attributes_arguments["eq"],
+ attributes_arguments["order"],
+ ) = _determine_attrs_eq_order(
+ cmp,
+ attributes_arguments.get("eq"),
+ attributes_arguments.get("order"),
+ True,
+ )
+
+ return _attrs(these=cls_dict, **attributes_arguments)(type_)
+
+
+# These are required by within this module so we define them here and merely
+# import into .validators / .converters.
+
+
+@attrs(slots=True, hash=True)
+class _AndValidator(object):
+ """
+ Compose many validators to a single one.
+ """
+
+ _validators = attrib()
+
+ def __call__(self, inst, attr, value):
+ for v in self._validators:
+ v(inst, attr, value)
+
+
+def and_(*validators):
+ """
+ A validator that composes multiple validators into one.
+
+ When called on a value, it runs all wrapped validators.
+
+ :param callables validators: Arbitrary number of validators.
+
+ .. versionadded:: 17.1.0
+ """
+ vals = []
+ for validator in validators:
+ vals.extend(
+ validator._validators
+ if isinstance(validator, _AndValidator)
+ else [validator]
+ )
+
+ return _AndValidator(tuple(vals))
+
+
+def pipe(*converters):
+ """
+ A converter that composes multiple converters into one.
+
+ When called on a value, it runs all wrapped converters, returning the
+ *last* value.
+
+ Type annotations will be inferred from the wrapped converters', if
+ they have any.
+
+ :param callables converters: Arbitrary number of converters.
+
+ .. versionadded:: 20.1.0
+ """
+
+ def pipe_converter(val):
+ for converter in converters:
+ val = converter(val)
+
+ 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
+
+ return pipe_converter
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/_next_gen.py b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_next_gen.py
new file mode 100644
index 0000000000..068253688c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_next_gen.py
@@ -0,0 +1,216 @@
+# 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.
+"""
+
+
+from functools import partial
+
+from . import setters
+from ._funcs import asdict as _asdict
+from ._funcs import astuple as _astuple
+from ._make import (
+ NOTHING,
+ _frozen_setattrs,
+ _ng_default_on_setattr,
+ attrib,
+ attrs,
+)
+from .exceptions import UnannotatedAttributeError
+
+
+def define(
+ maybe_cls=None,
+ *,
+ these=None,
+ repr=None,
+ hash=None,
+ init=None,
+ slots=True,
+ frozen=False,
+ weakref_slot=True,
+ str=False,
+ auto_attribs=None,
+ kw_only=False,
+ cache_hash=False,
+ auto_exc=True,
+ eq=None,
+ order=False,
+ auto_detect=True,
+ getstate_setstate=None,
+ on_setattr=None,
+ field_transformer=None,
+ match_args=True,
+):
+ r"""
+ 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)
+ - *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.
+
+ Please note that these are all defaults and you can change them as you
+ wish.
+
+ :param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves
+ exactly like `attr.s`. If left `None`, `attr.s` will try to guess:
+
+ 1. If any attributes are annotated and no unannotated `attrs.fields`\ s
+ are found, it assumes *auto_attribs=True*.
+ 2. Otherwise it assumes *auto_attribs=False* and tries to collect
+ `attrs.fields`\ s.
+
+ For now, please refer to `attr.s` for the rest of the parameters.
+
+ .. versionadded:: 20.1.0
+ .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``.
+ """
+
+ def do_it(cls, auto_attribs):
+ return attrs(
+ maybe_cls=cls,
+ these=these,
+ repr=repr,
+ hash=hash,
+ init=init,
+ slots=slots,
+ frozen=frozen,
+ weakref_slot=weakref_slot,
+ str=str,
+ auto_attribs=auto_attribs,
+ kw_only=kw_only,
+ cache_hash=cache_hash,
+ auto_exc=auto_exc,
+ eq=eq,
+ order=order,
+ auto_detect=auto_detect,
+ collect_by_mro=True,
+ getstate_setstate=getstate_setstate,
+ on_setattr=on_setattr,
+ field_transformer=field_transformer,
+ match_args=match_args,
+ )
+
+ def wrap(cls):
+ """
+ Making this a wrapper ensures this code runs during class creation.
+
+ We also ensure that frozen-ness of classes is inherited.
+ """
+ nonlocal frozen, on_setattr
+
+ had_on_setattr = on_setattr not in (None, setters.NO_OP)
+
+ # By default, mutable classes convert & validate on setattr.
+ if frozen is False and on_setattr is None:
+ on_setattr = _ng_default_on_setattr
+
+ # However, if we subclass a frozen class, we inherit the immutability
+ # and disable on_setattr.
+ 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)."
+ )
+
+ on_setattr = setters.NO_OP
+ break
+
+ if auto_attribs is not None:
+ return do_it(cls, auto_attribs)
+
+ try:
+ return do_it(cls, True)
+ except UnannotatedAttributeError:
+ return do_it(cls, False)
+
+ # maybe_cls's type depends on the usage of the decorator. It's a class
+ # if it's used as `@attrs` but ``None`` if used as `@attrs()`.
+ if maybe_cls is None:
+ return wrap
+ else:
+ return wrap(maybe_cls)
+
+
+mutable = define
+frozen = partial(define, frozen=True, on_setattr=None)
+
+
+def field(
+ *,
+ default=NOTHING,
+ validator=None,
+ repr=True,
+ hash=None,
+ init=True,
+ metadata=None,
+ converter=None,
+ factory=None,
+ kw_only=False,
+ eq=None,
+ order=None,
+ on_setattr=None,
+):
+ """
+ Identical to `attr.ib`, except keyword-only and with some arguments
+ removed.
+
+ .. versionadded:: 20.1.0
+ """
+ return attrib(
+ default=default,
+ validator=validator,
+ repr=repr,
+ hash=hash,
+ init=init,
+ metadata=metadata,
+ converter=converter,
+ factory=factory,
+ kw_only=kw_only,
+ eq=eq,
+ order=order,
+ on_setattr=on_setattr,
+ )
+
+
+def asdict(inst, *, recurse=True, filter=None, value_serializer=None):
+ """
+ Same as `attr.asdict`, except that collections types are always retained
+ and dict is always used as *dict_factory*.
+
+ .. versionadded:: 21.3.0
+ """
+ return _asdict(
+ inst=inst,
+ recurse=recurse,
+ filter=filter,
+ value_serializer=value_serializer,
+ retain_collection_types=True,
+ )
+
+
+def astuple(inst, *, recurse=True, filter=None):
+ """
+ Same as `attr.astuple`, except that collections types are always retained
+ and `tuple` is always used as the *tuple_factory*.
+
+ .. versionadded:: 21.3.0
+ """
+ return _astuple(
+ inst=inst, recurse=recurse, filter=filter, retain_collection_types=True
+ )
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/_version_info.py b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_version_info.py
new file mode 100644
index 0000000000..cdaeec37a1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_version_info.py
@@ -0,0 +1,87 @@
+# SPDX-License-Identifier: MIT
+
+from __future__ import absolute_import, division, print_function
+
+from functools import total_ordering
+
+from ._funcs import astuple
+from ._make import attrib, attrs
+
+
+@total_ordering
+@attrs(eq=False, order=False, slots=True, frozen=True)
+class VersionInfo(object):
+ """
+ A version object that can be compared to tuple of length 1--4:
+
+ >>> attr.VersionInfo(19, 1, 0, "final") <= (19, 2)
+ True
+ >>> attr.VersionInfo(19, 1, 0, "final") < (19, 1, 1)
+ True
+ >>> vi = attr.VersionInfo(19, 2, 0, "final")
+ >>> vi < (19, 1, 1)
+ False
+ >>> vi < (19,)
+ False
+ >>> vi == (19, 2,)
+ True
+ >>> vi == (19, 2, 1)
+ False
+
+ .. versionadded:: 19.2
+ """
+
+ year = attrib(type=int)
+ minor = attrib(type=int)
+ micro = attrib(type=int)
+ releaselevel = attrib(type=str)
+
+ @classmethod
+ def _from_version_string(cls, s):
+ """
+ Parse *s* and return a _VersionInfo.
+ """
+ v = s.split(".")
+ if len(v) == 3:
+ v.append("final")
+
+ return cls(
+ year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3]
+ )
+
+ def _ensure_tuple(self, other):
+ """
+ Ensure *other* is a tuple of a valid length.
+
+ Returns a possibly transformed *other* and ourselves as a tuple of
+ the same length as *other*.
+ """
+
+ if self.__class__ is other.__class__:
+ other = astuple(other)
+
+ if not isinstance(other, tuple):
+ raise NotImplementedError
+
+ if not (1 <= len(other) <= 4):
+ raise NotImplementedError
+
+ return astuple(self)[: len(other)], other
+
+ def __eq__(self, other):
+ try:
+ us, them = self._ensure_tuple(other)
+ except NotImplementedError:
+ return NotImplemented
+
+ return us == them
+
+ def __lt__(self, other):
+ try:
+ us, them = self._ensure_tuple(other)
+ except NotImplementedError:
+ return NotImplemented
+
+ # Since alphabetically "dev0" < "final" < "post1" < "post2", we don't
+ # have to do anything special with releaselevel for now.
+ return us < them
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/_version_info.pyi b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_version_info.pyi
new file mode 100644
index 0000000000..45ced08633
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/_version_info.pyi
@@ -0,0 +1,9 @@
+class VersionInfo:
+ @property
+ def year(self) -> int: ...
+ @property
+ def minor(self) -> int: ...
+ @property
+ def micro(self) -> int: ...
+ @property
+ def releaselevel(self) -> str: ...
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/converters.py b/testing/web-platform/tests/tools/third_party/attrs/src/attr/converters.py
new file mode 100644
index 0000000000..1fb6c05d7b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/converters.py
@@ -0,0 +1,155 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Commonly useful converters.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+from ._compat import PY2
+from ._make import NOTHING, Factory, pipe
+
+
+if not PY2:
+ import inspect
+ import typing
+
+
+__all__ = [
+ "default_if_none",
+ "optional",
+ "pipe",
+ "to_bool",
+]
+
+
+def optional(converter):
+ """
+ A converter that allows an attribute to be optional. An optional attribute
+ is one which can be set to ``None``.
+
+ Type annotations will be inferred from the wrapped converter's, if it
+ has any.
+
+ :param callable converter: the converter that is used for non-``None``
+ values.
+
+ .. versionadded:: 17.1.0
+ """
+
+ def optional_converter(val):
+ if val is None:
+ 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
+ ]
+
+ return optional_converter
+
+
+def default_if_none(default=NOTHING, factory=None):
+ """
+ A converter that allows to replace ``None`` values by *default* or the
+ result of *factory*.
+
+ :param default: Value to be used if ``None`` is passed. Passing an instance
+ of `attrs.Factory` is supported, however the ``takes_self`` option
+ is *not*.
+ :param callable factory: A callable that takes no parameters whose result
+ is used if ``None`` is passed.
+
+ :raises TypeError: If **neither** *default* or *factory* is passed.
+ :raises TypeError: If **both** *default* and *factory* are passed.
+ :raises ValueError: If an instance of `attrs.Factory` is passed with
+ ``takes_self=True``.
+
+ .. versionadded:: 18.2.0
+ """
+ if default is NOTHING and factory is None:
+ raise TypeError("Must pass either `default` or `factory`.")
+
+ if default is not NOTHING and factory is not None:
+ raise TypeError(
+ "Must pass either `default` or `factory` but not both."
+ )
+
+ 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."
+ )
+
+ def default_if_none_converter(val):
+ if val is not None:
+ return val
+
+ return default.factory()
+
+ else:
+
+ def default_if_none_converter(val):
+ if val is not None:
+ return val
+
+ return default
+
+ return default_if_none_converter
+
+
+def to_bool(val):
+ """
+ Convert "boolean" strings (e.g., from env. vars.) to real booleans.
+
+ Values mapping to :code:`True`:
+
+ - :code:`True`
+ - :code:`"true"` / :code:`"t"`
+ - :code:`"yes"` / :code:`"y"`
+ - :code:`"on"`
+ - :code:`"1"`
+ - :code:`1`
+
+ Values mapping to :code:`False`:
+
+ - :code:`False`
+ - :code:`"false"` / :code:`"f"`
+ - :code:`"no"` / :code:`"n"`
+ - :code:`"off"`
+ - :code:`"0"`
+ - :code:`0`
+
+ :raises ValueError: for any other value.
+
+ .. versionadded:: 21.3.0
+ """
+ if isinstance(val, str):
+ val = val.lower()
+ truthy = {True, "true", "t", "yes", "y", "on", "1", 1}
+ falsy = {False, "false", "f", "no", "n", "off", "0", 0}
+ try:
+ if val in truthy:
+ return True
+ if val in falsy:
+ return False
+ except TypeError:
+ # Raised when "val" is not hashable (e.g., lists)
+ pass
+ raise ValueError("Cannot convert value to bool: {}".format(val))
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/converters.pyi b/testing/web-platform/tests/tools/third_party/attrs/src/attr/converters.pyi
new file mode 100644
index 0000000000..0f58088a37
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/converters.pyi
@@ -0,0 +1,13 @@
+from typing import Callable, Optional, TypeVar, overload
+
+from . import _ConverterType
+
+_T = TypeVar("_T")
+
+def pipe(*validators: _ConverterType) -> _ConverterType: ...
+def optional(converter: _ConverterType) -> _ConverterType: ...
+@overload
+def default_if_none(default: _T) -> _ConverterType: ...
+@overload
+def default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ...
+def to_bool(val: str) -> bool: ...
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/exceptions.py b/testing/web-platform/tests/tools/third_party/attrs/src/attr/exceptions.py
new file mode 100644
index 0000000000..b2f1edc32a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/exceptions.py
@@ -0,0 +1,94 @@
+# SPDX-License-Identifier: MIT
+
+from __future__ import absolute_import, division, print_function
+
+
+class FrozenError(AttributeError):
+ """
+ A frozen/immutable instance or attribute have been attempted to be
+ modified.
+
+ It mirrors the behavior of ``namedtuples`` by using the same error message
+ and subclassing `AttributeError`.
+
+ .. versionadded:: 20.1.0
+ """
+
+ msg = "can't set attribute"
+ args = [msg]
+
+
+class FrozenInstanceError(FrozenError):
+ """
+ A frozen instance has been attempted to be modified.
+
+ .. versionadded:: 16.1.0
+ """
+
+
+class FrozenAttributeError(FrozenError):
+ """
+ A frozen attribute has been attempted to be modified.
+
+ .. versionadded:: 20.1.0
+ """
+
+
+class AttrsAttributeNotFoundError(ValueError):
+ """
+ An ``attrs`` function couldn't find an attribute that the user asked for.
+
+ .. versionadded:: 16.2.0
+ """
+
+
+class NotAnAttrsClassError(ValueError):
+ """
+ A non-``attrs`` class has been passed into an ``attrs`` function.
+
+ .. versionadded:: 16.2.0
+ """
+
+
+class DefaultAlreadySetError(RuntimeError):
+ """
+ A default has been set using ``attr.ib()`` and is attempted to be reset
+ using the decorator.
+
+ .. versionadded:: 17.1.0
+ """
+
+
+class UnannotatedAttributeError(RuntimeError):
+ """
+ A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type
+ annotation.
+
+ .. versionadded:: 17.3.0
+ """
+
+
+class PythonTooOldError(RuntimeError):
+ """
+ It was attempted to use an ``attrs`` feature that requires a newer Python
+ version.
+
+ .. versionadded:: 18.2.0
+ """
+
+
+class NotCallableError(TypeError):
+ """
+ A ``attr.ib()`` requiring a callable has been set with a value
+ that is not callable.
+
+ .. versionadded:: 19.2.0
+ """
+
+ def __init__(self, msg, value):
+ super(TypeError, self).__init__(msg, value)
+ self.msg = msg
+ self.value = value
+
+ def __str__(self):
+ return str(self.msg)
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/exceptions.pyi b/testing/web-platform/tests/tools/third_party/attrs/src/attr/exceptions.pyi
new file mode 100644
index 0000000000..f2680118b4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/exceptions.pyi
@@ -0,0 +1,17 @@
+from typing import Any
+
+class FrozenError(AttributeError):
+ msg: str = ...
+
+class FrozenInstanceError(FrozenError): ...
+class FrozenAttributeError(FrozenError): ...
+class AttrsAttributeNotFoundError(ValueError): ...
+class NotAnAttrsClassError(ValueError): ...
+class DefaultAlreadySetError(RuntimeError): ...
+class UnannotatedAttributeError(RuntimeError): ...
+class PythonTooOldError(RuntimeError): ...
+
+class NotCallableError(TypeError):
+ msg: str = ...
+ value: Any = ...
+ def __init__(self, msg: str, value: Any) -> None: ...
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/filters.py b/testing/web-platform/tests/tools/third_party/attrs/src/attr/filters.py
new file mode 100644
index 0000000000..a1978a8775
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/filters.py
@@ -0,0 +1,54 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Commonly useful filters for `attr.asdict`.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+from ._compat import isclass
+from ._make import Attribute
+
+
+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, Attribute)),
+ )
+
+
+def include(*what):
+ """
+ Include *what*.
+
+ :param what: What to include.
+ :type what: `list` of `type` or `attrs.Attribute`\\ s
+
+ :rtype: `callable`
+ """
+ cls, attrs = _split_what(what)
+
+ def include_(attribute, value):
+ return value.__class__ in cls or attribute in attrs
+
+ return include_
+
+
+def exclude(*what):
+ """
+ Exclude *what*.
+
+ :param what: What to exclude.
+ :type what: `list` of classes or `attrs.Attribute`\\ s.
+
+ :rtype: `callable`
+ """
+ cls, attrs = _split_what(what)
+
+ def exclude_(attribute, value):
+ return value.__class__ not in cls and attribute not in attrs
+
+ return exclude_
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/filters.pyi b/testing/web-platform/tests/tools/third_party/attrs/src/attr/filters.pyi
new file mode 100644
index 0000000000..993866865e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/filters.pyi
@@ -0,0 +1,6 @@
+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]: ...
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/py.typed b/testing/web-platform/tests/tools/third_party/attrs/src/attr/py.typed
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/py.typed
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/setters.py b/testing/web-platform/tests/tools/third_party/attrs/src/attr/setters.py
new file mode 100644
index 0000000000..b1cbb5d83e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/setters.py
@@ -0,0 +1,79 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Commonly used hooks for on_setattr.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+from . import _config
+from .exceptions import FrozenAttributeError
+
+
+def pipe(*setters):
+ """
+ Run all *setters* and return the return value of the last one.
+
+ .. versionadded:: 20.1.0
+ """
+
+ def wrapped_pipe(instance, attrib, new_value):
+ rv = new_value
+
+ for setter in setters:
+ rv = setter(instance, attrib, rv)
+
+ return rv
+
+ return wrapped_pipe
+
+
+def frozen(_, __, ___):
+ """
+ Prevent an attribute to be modified.
+
+ .. versionadded:: 20.1.0
+ """
+ raise FrozenAttributeError()
+
+
+def validate(instance, attrib, new_value):
+ """
+ Run *attrib*'s validator on *new_value* if it has one.
+
+ .. versionadded:: 20.1.0
+ """
+ if _config._run_validators is False:
+ return new_value
+
+ v = attrib.validator
+ if not v:
+ return new_value
+
+ v(instance, attrib, new_value)
+
+ return new_value
+
+
+def convert(instance, attrib, new_value):
+ """
+ Run *attrib*'s converter -- if it has one -- on *new_value* and return the
+ result.
+
+ .. versionadded:: 20.1.0
+ """
+ c = attrib.converter
+ if c:
+ return c(new_value)
+
+ return new_value
+
+
+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/testing/web-platform/tests/tools/third_party/attrs/src/attr/setters.pyi b/testing/web-platform/tests/tools/third_party/attrs/src/attr/setters.pyi
new file mode 100644
index 0000000000..3f5603c2b0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/setters.pyi
@@ -0,0 +1,19 @@
+from typing import Any, NewType, NoReturn, TypeVar, cast
+
+from . import Attribute, _OnSetAttrType
+
+_T = TypeVar("_T")
+
+def frozen(
+ instance: Any, attribute: Attribute[Any], new_value: Any
+) -> NoReturn: ...
+def pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ...
+def validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ...
+
+# convert is allowed to return Any, because they can be chained using pipe.
+def convert(
+ instance: Any, attribute: Attribute[Any], new_value: Any
+) -> Any: ...
+
+_NoOpType = NewType("_NoOpType", object)
+NO_OP: _NoOpType
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/validators.py b/testing/web-platform/tests/tools/third_party/attrs/src/attr/validators.py
new file mode 100644
index 0000000000..0b0c8342f2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/validators.py
@@ -0,0 +1,561 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Commonly useful validators.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+import operator
+import re
+
+from contextlib import contextmanager
+
+from ._config import get_run_validators, set_run_validators
+from ._make import _AndValidator, and_, attrib, attrs
+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",
+ "deep_mapping",
+ "disabled",
+ "ge",
+ "get_disabled",
+ "gt",
+ "in_",
+ "instance_of",
+ "is_callable",
+ "le",
+ "lt",
+ "matches_re",
+ "max_len",
+ "optional",
+ "provides",
+ "set_disabled",
+]
+
+
+def set_disabled(disabled):
+ """
+ Globally disable or enable running validators.
+
+ By default, they are run.
+
+ :param disabled: If ``True``, disable running all validators.
+ :type disabled: bool
+
+ .. warning::
+
+ This function is not thread-safe!
+
+ .. versionadded:: 21.3.0
+ """
+ set_run_validators(not disabled)
+
+
+def get_disabled():
+ """
+ Return a bool indicating whether validators are currently disabled or not.
+
+ :return: ``True`` if validators are currently disabled.
+ :rtype: bool
+
+ .. versionadded:: 21.3.0
+ """
+ return not get_run_validators()
+
+
+@contextmanager
+def disabled():
+ """
+ Context manager that disables running validators within its context.
+
+ .. warning::
+
+ This context manager is not thread-safe!
+
+ .. versionadded:: 21.3.0
+ """
+ set_run_validators(False)
+ try:
+ yield
+ finally:
+ set_run_validators(True)
+
+
+@attrs(repr=False, slots=True, hash=True)
+class _InstanceOfValidator(object):
+ type = attrib()
+
+ def __call__(self, inst, attr, value):
+ """
+ We use a callable class to be able to change the ``__repr__``.
+ """
+ if not isinstance(value, self.type):
+ 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,
+ ),
+ attr,
+ self.type,
+ value,
+ )
+
+ def __repr__(self):
+ return "<instance_of validator for type {type!r}>".format(
+ type=self.type
+ )
+
+
+def instance_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
+ `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
+
+ :raises TypeError: With a human readable error message, the attribute
+ (of type `attrs.Attribute`), the expected type, and the value it
+ got.
+ """
+ return _InstanceOfValidator(type)
+
+
+@attrs(repr=False, frozen=True, slots=True)
+class _MatchesReValidator(object):
+ pattern = attrib()
+ match_func = attrib()
+
+ def __call__(self, inst, attr, value):
+ """
+ We use a callable class to be able to change the ``__repr__``.
+ """
+ if not self.match_func(value):
+ raise ValueError(
+ "'{name}' must match regex {pattern!r}"
+ " ({value!r} doesn't)".format(
+ name=attr.name, pattern=self.pattern.pattern, value=value
+ ),
+ attr,
+ self.pattern,
+ value,
+ )
+
+ def __repr__(self):
+ return "<matches_re validator for pattern {pattern!r}>".format(
+ pattern=self.pattern
+ )
+
+
+def matches_re(regex, flags=0, func=None):
+ r"""
+ A validator that raises `ValueError` if the initializer is called
+ with a string that doesn't match *regex*.
+
+ :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.
+
+ .. 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)
+ 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)
+ )
+ )
+ )
+ )
+
+ if isinstance(regex, Pattern):
+ if flags:
+ raise TypeError(
+ "'flags' can only be used with a string pattern; "
+ "pass flags to re.compile() instead"
+ )
+ pattern = regex
+ else:
+ pattern = re.compile(regex, flags)
+
+ if func is re.match:
+ match_func = pattern.match
+ elif func is re.search:
+ match_func = pattern.search
+ elif fullmatch:
+ 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):
+ interface = attrib()
+
+ def __call__(self, inst, attr, value):
+ """
+ We use a callable class to be able to change the ``__repr__``.
+ """
+ if not self.interface.providedBy(value):
+ raise TypeError(
+ "'{name}' must provide {interface!r} which {value!r} "
+ "doesn't.".format(
+ name=attr.name, interface=self.interface, value=value
+ ),
+ attr,
+ self.interface,
+ value,
+ )
+
+ def __repr__(self):
+ return "<provides validator for interface {interface!r}>".format(
+ interface=self.interface
+ )
+
+
+def provides(interface):
+ """
+ A validator that raises a `TypeError` if the initializer is called
+ with an object that does not provide the requested *interface* (checks are
+ performed using ``interface.providedBy(value)`` (see `zope.interface
+ <https://zopeinterface.readthedocs.io/en/latest/>`_).
+
+ :param interface: The interface to check for.
+ :type interface: ``zope.interface.Interface``
+
+ :raises TypeError: With a human readable error message, the attribute
+ (of type `attrs.Attribute`), the expected interface, and the
+ value it got.
+ """
+ return _ProvidesValidator(interface)
+
+
+@attrs(repr=False, slots=True, hash=True)
+class _OptionalValidator(object):
+ validator = attrib()
+
+ def __call__(self, inst, attr, value):
+ if value is None:
+ return
+
+ self.validator(inst, attr, value)
+
+ def __repr__(self):
+ return "<optional validator for {what} or None>".format(
+ what=repr(self.validator)
+ )
+
+
+def optional(validator):
+ """
+ A validator that makes an attribute optional. An optional attribute is one
+ 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.
+
+ .. versionadded:: 15.1.0
+ .. versionchanged:: 17.1.0 *validator* can be a list of validators.
+ """
+ if isinstance(validator, list):
+ return _OptionalValidator(_AndValidator(validator))
+ return _OptionalValidator(validator)
+
+
+@attrs(repr=False, slots=True, hash=True)
+class _InValidator(object):
+ options = attrib()
+
+ def __call__(self, inst, attr, value):
+ try:
+ in_options = value in self.options
+ except TypeError: # e.g. `1 in "abc"`
+ in_options = False
+
+ if not in_options:
+ raise ValueError(
+ "'{name}' must be in {options!r} (got {value!r})".format(
+ name=attr.name, options=self.options, value=value
+ )
+ )
+
+ def __repr__(self):
+ return "<in_ validator with options {options!r}>".format(
+ options=self.options
+ )
+
+
+def in_(options):
+ """
+ A validator that raises a `ValueError` if the initializer is called
+ with a value that does not belong in the options provided. The check is
+ performed using ``value in options``.
+
+ :param options: Allowed options.
+ :type options: list, tuple, `enum.Enum`, ...
+
+ :raises ValueError: With a human readable error message, the attribute (of
+ type `attrs.Attribute`), the expected options, and the value it
+ got.
+
+ .. versionadded:: 17.1.0
+ """
+ return _InValidator(options)
+
+
+@attrs(repr=False, slots=False, hash=True)
+class _IsCallableValidator(object):
+ def __call__(self, inst, attr, value):
+ """
+ We use a callable class to be able to change the ``__repr__``.
+ """
+ if not callable(value):
+ message = (
+ "'{name}' must be callable "
+ "(got {value!r} that is a {actual!r})."
+ )
+ raise NotCallableError(
+ msg=message.format(
+ name=attr.name, value=value, actual=value.__class__
+ ),
+ value=value,
+ )
+
+ def __repr__(self):
+ return "<is_callable validator>"
+
+
+def is_callable():
+ """
+ A validator that raises a `attr.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
+ message containing the attribute (`attrs.Attribute`) name,
+ and the value it got.
+ """
+ return _IsCallableValidator()
+
+
+@attrs(repr=False, slots=True, hash=True)
+class _DeepIterable(object):
+ member_validator = attrib(validator=is_callable())
+ iterable_validator = attrib(
+ default=None, validator=optional(is_callable())
+ )
+
+ def __call__(self, inst, attr, value):
+ """
+ We use a callable class to be able to change the ``__repr__``.
+ """
+ if self.iterable_validator is not None:
+ self.iterable_validator(inst, attr, value)
+
+ for member in value:
+ self.member_validator(inst, attr, member)
+
+ def __repr__(self):
+ iterable_identifier = (
+ ""
+ if self.iterable_validator is None
+ else " {iterable!r}".format(iterable=self.iterable_validator)
+ )
+ return (
+ "<deep_iterable validator for{iterable_identifier}"
+ " iterables of {member!r}>"
+ ).format(
+ iterable_identifier=iterable_identifier,
+ member=self.member_validator,
+ )
+
+
+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 iterable_validator: Validator to apply to iterable itself
+ (optional)
+
+ .. versionadded:: 19.1.0
+
+ :raises TypeError: if any sub-validators fail
+ """
+ return _DeepIterable(member_validator, iterable_validator)
+
+
+@attrs(repr=False, slots=True, hash=True)
+class _DeepMapping(object):
+ key_validator = attrib(validator=is_callable())
+ value_validator = attrib(validator=is_callable())
+ mapping_validator = attrib(default=None, validator=optional(is_callable()))
+
+ def __call__(self, inst, attr, value):
+ """
+ We use a callable class to be able to change the ``__repr__``.
+ """
+ if self.mapping_validator is not None:
+ self.mapping_validator(inst, attr, value)
+
+ for key in value:
+ self.key_validator(inst, attr, key)
+ self.value_validator(inst, attr, value[key])
+
+ def __repr__(self):
+ return (
+ "<deep_mapping validator for objects mapping {key!r} to {value!r}>"
+ ).format(key=self.key_validator, value=self.value_validator)
+
+
+def deep_mapping(key_validator, value_validator, mapping_validator=None):
+ """
+ A validator that performs deep validation of a dictionary.
+
+ :param key_validator: Validator to apply to dictionary keys
+ :param value_validator: Validator to apply to dictionary values
+ :param mapping_validator: Validator to apply to top-level mapping
+ attribute (optional)
+
+ .. versionadded:: 19.1.0
+
+ :raises TypeError: if any sub-validators fail
+ """
+ return _DeepMapping(key_validator, value_validator, mapping_validator)
+
+
+@attrs(repr=False, frozen=True, slots=True)
+class _NumberValidator(object):
+ bound = attrib()
+ compare_op = attrib()
+ compare_func = attrib()
+
+ def __call__(self, inst, attr, value):
+ """
+ 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,
+ )
+ )
+
+ def __repr__(self):
+ return "<Validator for x {op} {bound}>".format(
+ op=self.compare_op, bound=self.bound
+ )
+
+
+def lt(val):
+ """
+ A validator that raises `ValueError` if the initializer is called
+ with a number larger or equal to *val*.
+
+ :param val: Exclusive upper bound for values
+
+ .. versionadded:: 21.3.0
+ """
+ return _NumberValidator(val, "<", operator.lt)
+
+
+def le(val):
+ """
+ A validator that raises `ValueError` if the initializer is called
+ with a number greater than *val*.
+
+ :param val: Inclusive upper bound for values
+
+ .. versionadded:: 21.3.0
+ """
+ return _NumberValidator(val, "<=", operator.le)
+
+
+def ge(val):
+ """
+ A validator that raises `ValueError` if the initializer is called
+ with a number smaller than *val*.
+
+ :param val: Inclusive lower bound for values
+
+ .. versionadded:: 21.3.0
+ """
+ return _NumberValidator(val, ">=", operator.ge)
+
+
+def gt(val):
+ """
+ A validator that raises `ValueError` if the initializer is called
+ with a number smaller or equal to *val*.
+
+ :param val: Exclusive lower bound for values
+
+ .. versionadded:: 21.3.0
+ """
+ return _NumberValidator(val, ">", operator.gt)
+
+
+@attrs(repr=False, frozen=True, slots=True)
+class _MaxLengthValidator(object):
+ max_length = attrib()
+
+ def __call__(self, inst, attr, value):
+ """
+ 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)
+ )
+ )
+
+ def __repr__(self):
+ return "<max_len validator for {max}>".format(max=self.max_length)
+
+
+def max_len(length):
+ """
+ A validator that raises `ValueError` if the initializer is called
+ with a string or iterable that is longer than *length*.
+
+ :param int length: Maximum length of the string or iterable
+
+ .. versionadded:: 21.3.0
+ """
+ return _MaxLengthValidator(length)
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attr/validators.pyi b/testing/web-platform/tests/tools/third_party/attrs/src/attr/validators.pyi
new file mode 100644
index 0000000000..5e00b85433
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attr/validators.pyi
@@ -0,0 +1,78 @@
+from typing import (
+ Any,
+ AnyStr,
+ Callable,
+ Container,
+ ContextManager,
+ Iterable,
+ List,
+ Mapping,
+ Match,
+ Optional,
+ Pattern,
+ Tuple,
+ Type,
+ TypeVar,
+ Union,
+ overload,
+)
+
+from . import _ValidatorType
+
+_T = TypeVar("_T")
+_T1 = TypeVar("_T1")
+_T2 = TypeVar("_T2")
+_T3 = TypeVar("_T3")
+_I = TypeVar("_I", bound=Iterable)
+_K = TypeVar("_K")
+_V = TypeVar("_V")
+_M = TypeVar("_M", bound=Mapping)
+
+def set_disabled(run: bool) -> None: ...
+def get_disabled() -> bool: ...
+def disabled() -> ContextManager[None]: ...
+
+# To be more precise on instance_of use some overloads.
+# If there are more than 3 items in the tuple then we fall back to Any
+@overload
+def instance_of(type: Type[_T]) -> _ValidatorType[_T]: ...
+@overload
+def instance_of(type: Tuple[Type[_T]]) -> _ValidatorType[_T]: ...
+@overload
+def instance_of(
+ type: Tuple[Type[_T1], Type[_T2]]
+) -> _ValidatorType[Union[_T1, _T2]]: ...
+@overload
+def instance_of(
+ type: Tuple[Type[_T1], Type[_T2], Type[_T3]]
+) -> _ValidatorType[Union[_T1, _T2, _T3]]: ...
+@overload
+def instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ...
+def provides(interface: Any) -> _ValidatorType[Any]: ...
+def optional(
+ validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]]
+) -> _ValidatorType[Optional[_T]]: ...
+def in_(options: Container[_T]) -> _ValidatorType[_T]: ...
+def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ...
+def matches_re(
+ regex: Union[Pattern[AnyStr], AnyStr],
+ flags: int = ...,
+ func: Optional[
+ Callable[[AnyStr, AnyStr, int], Optional[Match[AnyStr]]]
+ ] = ...,
+) -> _ValidatorType[AnyStr]: ...
+def deep_iterable(
+ member_validator: _ValidatorType[_T],
+ iterable_validator: Optional[_ValidatorType[_I]] = ...,
+) -> _ValidatorType[_I]: ...
+def deep_mapping(
+ key_validator: _ValidatorType[_K],
+ value_validator: _ValidatorType[_V],
+ mapping_validator: Optional[_ValidatorType[_M]] = ...,
+) -> _ValidatorType[_M]: ...
+def is_callable() -> _ValidatorType[_T]: ...
+def lt(val: _T) -> _ValidatorType[_T]: ...
+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]: ...
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attrs/__init__.py b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/__init__.py
new file mode 100644
index 0000000000..a704b8b56b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/__init__.py
@@ -0,0 +1,70 @@
+# SPDX-License-Identifier: MIT
+
+from attr import (
+ NOTHING,
+ Attribute,
+ Factory,
+ __author__,
+ __copyright__,
+ __description__,
+ __doc__,
+ __email__,
+ __license__,
+ __title__,
+ __url__,
+ __version__,
+ __version_info__,
+ assoc,
+ cmp_using,
+ define,
+ evolve,
+ field,
+ fields,
+ fields_dict,
+ frozen,
+ has,
+ make_class,
+ mutable,
+ resolve_types,
+ validate,
+)
+from attr._next_gen import asdict, astuple
+
+from . import converters, exceptions, filters, setters, validators
+
+
+__all__ = [
+ "__author__",
+ "__copyright__",
+ "__description__",
+ "__doc__",
+ "__email__",
+ "__license__",
+ "__title__",
+ "__url__",
+ "__version__",
+ "__version_info__",
+ "asdict",
+ "assoc",
+ "astuple",
+ "Attribute",
+ "cmp_using",
+ "converters",
+ "define",
+ "evolve",
+ "exceptions",
+ "Factory",
+ "field",
+ "fields_dict",
+ "fields",
+ "filters",
+ "frozen",
+ "has",
+ "make_class",
+ "mutable",
+ "NOTHING",
+ "resolve_types",
+ "setters",
+ "validate",
+ "validators",
+]
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attrs/__init__.pyi b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/__init__.pyi
new file mode 100644
index 0000000000..7426fa5ddb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/__init__.pyi
@@ -0,0 +1,63 @@
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Mapping,
+ Optional,
+ Sequence,
+ Tuple,
+ Type,
+)
+
+# Because we need to type our own stuff, we have to make everything from
+# attr explicitly public too.
+from attr import __author__ as __author__
+from attr import __copyright__ as __copyright__
+from attr import __description__ as __description__
+from attr import __email__ as __email__
+from attr import __license__ as __license__
+from attr import __title__ as __title__
+from attr import __url__ as __url__
+from attr import __version__ as __version__
+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 define as define
+from attr import evolve as evolve
+from attr import Factory as Factory
+from attr import exceptions as exceptions
+from attr import field as field
+from attr import fields as fields
+from attr import fields_dict as fields_dict
+from attr import frozen as frozen
+from attr import has as has
+from attr import make_class as make_class
+from attr import mutable as mutable
+from attr import NOTHING as NOTHING
+from attr import resolve_types as resolve_types
+from attr import setters as setters
+from attr import validate as validate
+from attr import validators as validators
+
+# TODO: see definition of attr.asdict/astuple
+def asdict(
+ inst: Any,
+ recurse: bool = ...,
+ filter: Optional[_FilterType[Any]] = ...,
+ dict_factory: Type[Mapping[Any, Any]] = ...,
+ retain_collection_types: bool = ...,
+ value_serializer: Optional[
+ Callable[[type, Attribute[Any], Any], Any]
+ ] = ...,
+ tuple_keys: bool = ...,
+) -> Dict[str, Any]: ...
+
+# TODO: add support for returning NamedTuple from the mypy plugin
+def astuple(
+ inst: Any,
+ recurse: bool = ...,
+ filter: Optional[_FilterType[Any]] = ...,
+ tuple_factory: Type[Sequence[Any]] = ...,
+ retain_collection_types: bool = ...,
+) -> Tuple[Any, ...]: ...
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attrs/converters.py b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/converters.py
new file mode 100644
index 0000000000..edfa8d3c16
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/converters.py
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: MIT
+
+from attr.converters import * # noqa
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attrs/exceptions.py b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/exceptions.py
new file mode 100644
index 0000000000..bd9efed202
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/exceptions.py
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: MIT
+
+from attr.exceptions import * # noqa
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attrs/filters.py b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/filters.py
new file mode 100644
index 0000000000..52959005b0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/filters.py
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: MIT
+
+from attr.filters import * # noqa
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attrs/py.typed b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/py.typed
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/py.typed
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attrs/setters.py b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/setters.py
new file mode 100644
index 0000000000..9b50770804
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/setters.py
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: MIT
+
+from attr.setters import * # noqa
diff --git a/testing/web-platform/tests/tools/third_party/attrs/src/attrs/validators.py b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/validators.py
new file mode 100644
index 0000000000..ab2c9b3024
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/src/attrs/validators.py
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: MIT
+
+from attr.validators import * # noqa
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/__init__.py b/testing/web-platform/tests/tools/third_party/attrs/tests/__init__.py
new file mode 100644
index 0000000000..548d2d447d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/__init__.py
@@ -0,0 +1 @@
+# SPDX-License-Identifier: MIT
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/attr_import_star.py b/testing/web-platform/tests/tools/third_party/attrs/tests/attr_import_star.py
new file mode 100644
index 0000000000..eaec321bac
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/attr_import_star.py
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: MIT
+
+from __future__ import absolute_import
+
+from attr import * # noqa: F401,F403
+
+
+# This is imported by test_import::test_from_attr_import_star; this must
+# be done indirectly because importing * is only allowed on module level,
+# so can't be done inside a test.
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/dataclass_transform_example.py b/testing/web-platform/tests/tools/third_party/attrs/tests/dataclass_transform_example.py
new file mode 100644
index 0000000000..49e09061a8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/dataclass_transform_example.py
@@ -0,0 +1,45 @@
+# SPDX-License-Identifier: MIT
+
+import attr
+
+
+@attr.define()
+class Define:
+ a: str
+ b: int
+
+
+reveal_type(Define.__init__) # noqa
+
+
+@attr.define()
+class DefineConverter:
+ # mypy plugin adapts the "int" method signature, pyright does not
+ with_converter: int = attr.field(converter=int)
+
+
+reveal_type(DefineConverter.__init__) # noqa
+
+
+# mypy plugin supports attr.frozen, pyright does not
+@attr.frozen()
+class Frozen:
+ a: str
+
+
+d = Frozen("a")
+d.a = "new"
+
+reveal_type(d.a) # noqa
+
+
+# but pyright supports attr.define(frozen)
+@attr.define(frozen=True)
+class FrozenDefine:
+ a: str
+
+
+d2 = FrozenDefine("a")
+d2.a = "new"
+
+reveal_type(d2.a) # noqa
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/strategies.py b/testing/web-platform/tests/tools/third_party/attrs/tests/strategies.py
new file mode 100644
index 0000000000..99f9f48536
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/strategies.py
@@ -0,0 +1,198 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Testing strategies for Hypothesis-based tests.
+"""
+
+import keyword
+import string
+
+from collections import OrderedDict
+
+from hypothesis import strategies as st
+
+import attr
+
+from .utils import make_class
+
+
+optional_bool = st.one_of(st.none(), st.booleans())
+
+
+def gen_attr_names():
+ """
+ Generate names for attributes, 'a'...'z', then 'aa'...'zz'.
+
+ ~702 different attribute names should be enough in practice.
+
+ Some short strings (such as 'as') are keywords, so we skip them.
+ """
+ lc = string.ascii_lowercase
+ for c in lc:
+ yield c
+ for outer in lc:
+ for inner in lc:
+ res = outer + inner
+ if keyword.iskeyword(res):
+ continue
+ yield outer + inner
+
+
+def maybe_underscore_prefix(source):
+ """
+ A generator to sometimes prepend an underscore.
+ """
+ to_underscore = False
+ for val in source:
+ yield val if not to_underscore else "_" + val
+ to_underscore = not to_underscore
+
+
+@st.composite
+def _create_hyp_nested_strategy(draw, simple_class_strategy):
+ """
+ Create a recursive attrs class.
+
+ Given a strategy for building (simpler) classes, create and return
+ a strategy for building classes that have as an attribute: either just
+ the simpler class, a list of simpler classes, a tuple of simpler classes,
+ an ordered dict or a dict mapping the string "cls" to a simpler class.
+ """
+ cls = draw(simple_class_strategy)
+ factories = [
+ cls,
+ lambda: [cls()],
+ lambda: (cls(),),
+ lambda: {"cls": cls()},
+ lambda: OrderedDict([("cls", cls())]),
+ ]
+ factory = draw(st.sampled_from(factories))
+ attrs = draw(list_of_attrs) + [attr.ib(default=attr.Factory(factory))]
+ return make_class("HypClass", dict(zip(gen_attr_names(), attrs)))
+
+
+bare_attrs = st.builds(attr.ib, default=st.none())
+int_attrs = st.integers().map(lambda i: attr.ib(default=i))
+str_attrs = st.text().map(lambda s: attr.ib(default=s))
+float_attrs = st.floats().map(lambda f: attr.ib(default=f))
+dict_attrs = st.dictionaries(keys=st.text(), values=st.integers()).map(
+ lambda d: attr.ib(default=d)
+)
+
+simple_attrs_without_metadata = (
+ bare_attrs | int_attrs | str_attrs | float_attrs | dict_attrs
+)
+
+
+@st.composite
+def simple_attrs_with_metadata(draw):
+ """
+ Create a simple attribute with arbitrary metadata.
+ """
+ c_attr = draw(simple_attrs)
+ keys = st.booleans() | st.binary() | st.integers() | st.text()
+ vals = st.booleans() | st.binary() | st.integers() | st.text()
+ metadata = draw(
+ st.dictionaries(keys=keys, values=vals, min_size=1, max_size=3)
+ )
+
+ return attr.ib(
+ default=c_attr._default,
+ validator=c_attr._validator,
+ repr=c_attr.repr,
+ eq=c_attr.eq,
+ order=c_attr.order,
+ hash=c_attr.hash,
+ init=c_attr.init,
+ metadata=metadata,
+ type=None,
+ converter=c_attr.converter,
+ )
+
+
+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
+):
+ """
+ A strategy that generates classes with default non-attr attributes.
+
+ For example, this strategy might generate a class such as:
+
+ @attr.s(slots=True, frozen=True, weakref_slot=True)
+ class HypClass:
+ a = attr.ib(default=1)
+ _b = attr.ib(default=None)
+ c = attr.ib(default='text')
+ _d = attr.ib(default=1.0)
+ c = attr.ib(default={'t': 1})
+
+ By default, all combinations of slots, frozen, and weakref_slot classes
+ will be generated. If `slots=True` is passed in, only slotted classes will
+ be generated, and if `slots=False` is passed in, no slotted classes will be
+ generated. The same applies to `frozen` and `weakref_slot`.
+
+ By default, some attributes will be private (i.e. prefixed with an
+ underscore). If `private_attrs=True` is passed in, all attributes will be
+ private, and if `private_attrs=False`, no attributes will be private.
+ """
+ attrs = draw(list_of_attrs)
+ frozen_flag = draw(st.booleans())
+ slots_flag = draw(st.booleans())
+ weakref_flag = draw(st.booleans())
+
+ if private_attrs is None:
+ attr_names = maybe_underscore_prefix(gen_attr_names())
+ elif private_attrs is True:
+ attr_names = ("_" + n for n in gen_attr_names())
+ elif private_attrs is False:
+ attr_names = gen_attr_names()
+
+ cls_dict = dict(zip(attr_names, attrs))
+ pre_init_flag = draw(st.booleans())
+ post_init_flag = draw(st.booleans())
+ init_flag = draw(st.booleans())
+
+ if pre_init_flag:
+
+ def pre_init(self):
+ pass
+
+ cls_dict["__attrs_pre_init__"] = pre_init
+
+ if post_init_flag:
+
+ def post_init(self):
+ pass
+
+ cls_dict["__attrs_post_init__"] = post_init
+
+ if not init_flag:
+
+ def init(self, *args, **kwargs):
+ self.__attrs_init__(*args, **kwargs)
+
+ cls_dict["__init__"] = init
+
+ return make_class(
+ "HypClass",
+ cls_dict,
+ 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,
+ init=init_flag,
+ )
+
+
+# st.recursive works by taking a base strategy (in this case, simple_classes)
+# and a special function. This function receives a strategy, and returns
+# another strategy (building on top of the base strategy).
+nested_classes = st.recursive(
+ simple_classes(), _create_hyp_nested_strategy, max_leaves=3
+)
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_3rd_party.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_3rd_party.py
new file mode 100644
index 0000000000..8866d7f6ef
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_3rd_party.py
@@ -0,0 +1,31 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Tests for compatibility against other Python modules.
+"""
+
+import pytest
+
+from hypothesis import given
+
+from .strategies import simple_classes
+
+
+cloudpickle = pytest.importorskip("cloudpickle")
+
+
+class TestCloudpickleCompat(object):
+ """
+ Tests for compatibility with ``cloudpickle``.
+ """
+
+ @given(simple_classes())
+ def test_repr(self, cls):
+ """
+ attrs instances can be pickled and un-pickled with cloudpickle.
+ """
+ inst = cls()
+ # Exact values aren't a concern so long as neither direction
+ # raises an exception.
+ pkl = cloudpickle.dumps(inst)
+ cloudpickle.loads(pkl)
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_annotations.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_annotations.py
new file mode 100644
index 0000000000..a201ebf7fa
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_annotations.py
@@ -0,0 +1,671 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Tests for PEP-526 type annotations.
+
+Python 3.6+ only.
+"""
+
+import sys
+import types
+import typing
+
+import pytest
+
+import attr
+
+from attr._make import _is_class_var
+from attr.exceptions import UnannotatedAttributeError
+
+
+def assert_init_annotations(cls, **annotations):
+ """
+ Assert cls.__init__ has the correct annotations.
+ """
+ __tracebackhide__ = True
+
+ annotations["return"] = type(None)
+
+ assert annotations == typing.get_type_hints(cls.__init__)
+
+
+class TestAnnotations:
+ """
+ Tests for types derived from variable annotations (PEP-526).
+ """
+
+ def test_basic_annotations(self):
+ """
+ Sets the `Attribute.type` attr from basic type annotations.
+ """
+
+ @attr.resolve_types
+ @attr.s
+ class C:
+ x: int = attr.ib()
+ y = attr.ib(type=str)
+ z = attr.ib()
+
+ assert int is attr.fields(C).x.type
+ assert str is attr.fields(C).y.type
+ assert None is attr.fields(C).z.type
+ assert_init_annotations(C, x=int, y=str)
+
+ def test_catches_basic_type_conflict(self):
+ """
+ Raises ValueError if type is specified both ways.
+ """
+ with pytest.raises(ValueError) as e:
+
+ @attr.s
+ class C:
+ x: int = attr.ib(type=int)
+
+ assert (
+ "Type annotation and type argument cannot both be present",
+ ) == e.value.args
+
+ def test_typing_annotations(self):
+ """
+ Sets the `Attribute.type` attr from typing annotations.
+ """
+
+ @attr.resolve_types
+ @attr.s
+ class C:
+ x: typing.List[int] = attr.ib()
+ y = attr.ib(type=typing.Optional[str])
+
+ assert typing.List[int] is attr.fields(C).x.type
+ assert typing.Optional[str] is attr.fields(C).y.type
+ assert_init_annotations(C, x=typing.List[int], y=typing.Optional[str])
+
+ def test_only_attrs_annotations_collected(self):
+ """
+ Annotations that aren't set to an attr.ib are ignored.
+ """
+
+ @attr.resolve_types
+ @attr.s
+ class C:
+ x: typing.List[int] = attr.ib()
+ y: int
+
+ assert 1 == len(attr.fields(C))
+ assert_init_annotations(C, x=typing.List[int])
+
+ @pytest.mark.parametrize("slots", [True, False])
+ def test_auto_attribs(self, slots):
+ """
+ If *auto_attribs* is True, bare annotations are collected too.
+ Defaults work and class variables are ignored.
+ """
+
+ @attr.s(auto_attribs=True, slots=slots)
+ class C:
+ cls_var: typing.ClassVar[int] = 23
+ a: int
+ x: typing.List[int] = attr.Factory(list)
+ y: int = 2
+ z: int = attr.ib(default=3)
+ foo: typing.Any = None
+
+ 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__)
+ assert "a" in attr_names # just double check that the set works
+ assert "cls_var" not in attr_names
+
+ attr.resolve_types(C)
+
+ assert int == attr.fields(C).a.type
+
+ assert attr.Factory(list) == attr.fields(C).x.default
+ assert typing.List[int] == attr.fields(C).x.type
+
+ assert int == attr.fields(C).y.type
+ assert 2 == attr.fields(C).y.default
+
+ assert int == attr.fields(C).z.type
+
+ assert typing.Any == attr.fields(C).foo.type
+
+ # Class body is clean.
+ if slots is False:
+ with pytest.raises(AttributeError):
+ C.y
+
+ assert 2 == i.y
+ else:
+ assert isinstance(C.y, types.MemberDescriptorType)
+
+ i.y = 23
+ assert 23 == i.y
+
+ assert_init_annotations(
+ C,
+ a=int,
+ x=typing.List[int],
+ y=int,
+ z=int,
+ foo=typing.Optional[typing.Any],
+ )
+
+ @pytest.mark.parametrize("slots", [True, False])
+ def test_auto_attribs_unannotated(self, slots):
+ """
+ Unannotated `attr.ib`s raise an error.
+ """
+ with pytest.raises(UnannotatedAttributeError) as e:
+
+ @attr.s(slots=slots, auto_attribs=True)
+ class C:
+ v = attr.ib()
+ x: int
+ y = attr.ib()
+ z: str
+
+ assert (
+ "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
+ subclass has annotations or not.
+
+ Ref #291
+ """
+
+ @attr.resolve_types
+ @attr.s(slots=slots, auto_attribs=True)
+ class A:
+ a: int = 1
+
+ @attr.resolve_types
+ @attr.s(slots=slots, auto_attribs=True)
+ class B(A):
+ b: int = 2
+
+ @attr.resolve_types
+ @attr.s(slots=slots, auto_attribs=True)
+ class C(A):
+ pass
+
+ assert "B(a=1, b=2)" == repr(B())
+ assert "C(a=1)" == repr(C())
+ assert_init_annotations(A, a=int)
+ assert_init_annotations(B, a=int, b=int)
+ assert_init_annotations(C, a=int)
+
+ def test_converter_annotations(self):
+ """
+ An unannotated attribute with an annotated converter gets its
+ annotation from the converter.
+ """
+
+ def int2str(x: int) -> str:
+ return str(x)
+
+ @attr.s
+ class A:
+ a = attr.ib(converter=int2str)
+
+ assert_init_annotations(A, a=int)
+
+ def int2str_(x: int, y: str = ""):
+ return str(x)
+
+ @attr.s
+ class A:
+ a = attr.ib(converter=int2str_)
+
+ assert_init_annotations(A, a=int)
+
+ def test_converter_attrib_annotations(self):
+ """
+ If a converter is provided, an explicit type annotation has no
+ effect on an attribute's type annotation.
+ """
+
+ def int2str(x: int) -> str:
+ return str(x)
+
+ @attr.s
+ class A:
+ a: str = attr.ib(converter=int2str)
+ b = attr.ib(converter=int2str, type=str)
+
+ assert_init_annotations(A, a=int, b=int)
+
+ def test_non_introspectable_converter(self):
+ """
+ A non-introspectable converter doesn't cause a crash.
+ """
+
+ @attr.s
+ class A:
+ a = attr.ib(converter=print)
+
+ def test_nullary_converter(self):
+ """
+ A coverter with no arguments doesn't cause a crash.
+ """
+
+ def noop():
+ pass
+
+ @attr.s
+ class A:
+ a = attr.ib(converter=noop)
+
+ assert A.__init__.__annotations__ == {"return": None}
+
+ def test_pipe(self):
+ """
+ pipe() uses the input annotation of its first argument and the
+ output annotation of its last argument.
+ """
+
+ def int2str(x: int) -> str:
+ return str(x)
+
+ def strlen(y: str) -> int:
+ return len(y)
+
+ def identity(z):
+ return z
+
+ assert attr.converters.pipe(int2str).__annotations__ == {
+ "val": int,
+ "return": str,
+ }
+ assert attr.converters.pipe(int2str, strlen).__annotations__ == {
+ "val": int,
+ "return": int,
+ }
+ assert attr.converters.pipe(identity, strlen).__annotations__ == {
+ "return": int
+ }
+ assert attr.converters.pipe(int2str, identity).__annotations__ == {
+ "val": int
+ }
+
+ def int2str_(x: int, y: int = 0) -> str:
+ return str(x)
+
+ assert attr.converters.pipe(int2str_).__annotations__ == {
+ "val": int,
+ "return": str,
+ }
+
+ def test_pipe_empty(self):
+ """
+ pipe() with no converters is annotated like the identity.
+ """
+
+ p = attr.converters.pipe()
+ assert "val" in p.__annotations__
+ t = p.__annotations__["val"]
+ assert isinstance(t, typing.TypeVar)
+ assert p.__annotations__ == {"val": t, "return": t}
+
+ def test_pipe_non_introspectable(self):
+ """
+ pipe() doesn't crash when passed a non-introspectable converter.
+ """
+
+ assert attr.converters.pipe(print).__annotations__ == {}
+
+ def test_pipe_nullary(self):
+ """
+ pipe() doesn't crash when passed a nullary converter.
+ """
+
+ def noop():
+ pass
+
+ assert attr.converters.pipe(noop).__annotations__ == {}
+
+ def test_optional(self):
+ """
+ optional() uses the annotations of the converter it wraps.
+ """
+
+ def int2str(x: int) -> str:
+ return str(x)
+
+ def int_identity(x: int):
+ return x
+
+ def strify(x) -> str:
+ return str(x)
+
+ def identity(x):
+ return x
+
+ assert attr.converters.optional(int2str).__annotations__ == {
+ "val": typing.Optional[int],
+ "return": typing.Optional[str],
+ }
+ assert attr.converters.optional(int_identity).__annotations__ == {
+ "val": typing.Optional[int]
+ }
+ assert attr.converters.optional(strify).__annotations__ == {
+ "return": typing.Optional[str]
+ }
+ assert attr.converters.optional(identity).__annotations__ == {}
+
+ def int2str_(x: int, y: int = 0) -> str:
+ return str(x)
+
+ assert attr.converters.optional(int2str_).__annotations__ == {
+ "val": typing.Optional[int],
+ "return": typing.Optional[str],
+ }
+
+ def test_optional_non_introspectable(self):
+ """
+ optional() doesn't crash when passed a non-introspectable
+ converter.
+ """
+
+ assert attr.converters.optional(print).__annotations__ == {}
+
+ def test_optional_nullary(self):
+ """
+ optional() doesn't crash when passed a nullary converter.
+ """
+
+ def noop():
+ pass
+
+ assert attr.converters.optional(noop).__annotations__ == {}
+
+ @pytest.mark.xfail(
+ sys.version_info[:2] == (3, 6), reason="Does not work on 3.6."
+ )
+ @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.
+ """
+ import typing as t
+
+ from typing import ClassVar
+
+ @attr.s(auto_attribs=True, slots=slots)
+ class C:
+ cls_var1: "typing.ClassVar[int]" = 23
+ cls_var2: "ClassVar[int]" = 23
+ cls_var3: "t.ClassVar[int]" = 23
+ a: "int"
+ x: "typing.List[int]" = attr.Factory(list)
+ y: "int" = 2
+ z: "int" = attr.ib(default=3)
+ foo: "typing.Any" = None
+
+ attr.resolve_types(C, locals(), globals())
+
+ assert_init_annotations(
+ C,
+ a=int,
+ x=typing.List[int],
+ y=int,
+ z=int,
+ foo=typing.Optional[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.
+ """
+
+ @attr.s(auto_attribs=True, slots=slots)
+ class C:
+ cls_var: "typing_extensions.ClassVar" = 23 # noqa
+
+ assert_init_annotations(C)
+
+ def test_keyword_only_auto_attribs(self):
+ """
+ `kw_only` propagates to attributes defined via `auto_attribs`.
+ """
+
+ @attr.s(auto_attribs=True, kw_only=True)
+ class C:
+ x: int
+ y: int
+
+ with pytest.raises(TypeError):
+ C(0, 1)
+
+ with pytest.raises(TypeError):
+ C(x=0)
+
+ c = C(x=0, y=1)
+
+ assert c.x == 0
+ assert c.y == 1
+
+ def test_base_class_variable(self):
+ """
+ Base class' class variables can be overridden with an attribute
+ without resorting to using an explicit `attr.ib()`.
+ """
+
+ class Base:
+ x: int = 42
+
+ @attr.s(auto_attribs=True)
+ class C(Base):
+ x: int
+
+ assert 1 == C(1).x
+
+ def test_removes_none_too(self):
+ """
+ Regression test for #523: make sure defaults that are set to None are
+ removed too.
+ """
+
+ @attr.s(auto_attribs=True)
+ class C:
+ x: int = 42
+ y: typing.Any = None
+
+ with pytest.raises(AttributeError):
+ C.x
+
+ with pytest.raises(AttributeError):
+ C.y
+
+ def test_non_comparable_defaults(self):
+ """
+ Regression test for #585: objects that are not directly comparable
+ (for example numpy arrays) would cause a crash when used as
+ default values of an attrs auto-attrib class.
+ """
+
+ class NonComparable:
+ def __eq__(self, other):
+ raise ValueError
+
+ @attr.s(auto_attribs=True)
+ class C:
+ x: typing.Any = NonComparable()
+
+ def test_basic_resolve(self):
+ """
+ Resolve the `Attribute.type` attr from basic type annotations.
+ Unannotated types are ignored.
+ """
+
+ @attr.s
+ class C:
+ x: "int" = attr.ib()
+ y = attr.ib(type=str)
+ z = attr.ib()
+
+ attr.resolve_types(C)
+
+ assert int is attr.fields(C).x.type
+ assert str is attr.fields(C).y.type
+ assert None is attr.fields(C).z.type
+
+ @pytest.mark.parametrize("slots", [True, False])
+ def test_resolve_types_auto_attrib(self, slots):
+ """
+ Types can be resolved even when strings are involved.
+ """
+
+ @attr.s(slots=slots, auto_attribs=True)
+ class A:
+ a: typing.List[int]
+ b: typing.List["int"]
+ c: "typing.List[int]"
+
+ # Note: I don't have to pass globals and locals here because
+ # int is a builtin and will be available in any scope.
+ attr.resolve_types(A)
+
+ assert typing.List[int] == attr.fields(A).a.type
+ 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.
+ """
+
+ @attr.resolve_types
+ @attr.s(slots=slots, auto_attribs=True)
+ class A:
+ a: typing.List[int]
+ b: typing.List["int"]
+ c: "typing.List[int]"
+
+ assert typing.List[int] == attr.fields(A).a.type
+ 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.
+ """
+
+ @attr.s(slots=slots, auto_attribs=True)
+ class A:
+ a: "A"
+ b: typing.Optional["A"] # noqa: will resolve below
+
+ 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.
+ """
+
+ @attr.s(slots=slots, auto_attribs=True)
+ class A:
+ a: typing.List["B"] # noqa: will resolve below
+
+ @attr.s(slots=slots, auto_attribs=True)
+ class B:
+ a: A
+
+ attr.resolve_types(A, globals(), locals())
+ attr.resolve_types(B, globals(), locals())
+
+ assert typing.List[B] == attr.fields(A).a.type
+ assert A == attr.fields(B).a.type
+
+ assert typing.List[B] == attr.fields(A).a.type
+ assert A == attr.fields(B).a.type
+
+ def test_init_type_hints(self):
+ """
+ Forward references in __init__ can be automatically resolved.
+ """
+
+ @attr.s
+ class C:
+ x = attr.ib(type="typing.List[int]")
+
+ assert_init_annotations(C, x=typing.List[int])
+
+ def test_init_type_hints_fake_module(self):
+ """
+ If you somehow set the __module__ to something that doesn't exist
+ you'll lose __init__ resolution.
+ """
+
+ class C:
+ x = attr.ib(type="typing.List[int]")
+
+ C.__module__ = "totally fake"
+ C = attr.s(C)
+
+ with pytest.raises(NameError):
+ typing.get_type_hints(C.__init__)
+
+ def test_inheritance(self):
+ """
+ Subclasses can be resolved after the parent is resolved.
+ """
+
+ @attr.define()
+ class A:
+ n: "int"
+
+ @attr.define()
+ class B(A):
+ pass
+
+ attr.resolve_types(A)
+ attr.resolve_types(B)
+
+ assert int == attr.fields(A).n.type
+ assert int == attr.fields(B).n.type
+
+ def test_resolve_twice(self):
+ """
+ You can call resolve_types as many times as you like.
+ This test is here mostly for coverage.
+ """
+
+ @attr.define()
+ class A:
+ n: "int"
+
+ attr.resolve_types(A)
+ assert int == attr.fields(A).n.type
+ attr.resolve_types(A)
+ assert int == attr.fields(A).n.type
+
+
+@pytest.mark.parametrize(
+ "annot",
+ [
+ typing.ClassVar,
+ "typing.ClassVar",
+ "'typing.ClassVar[dict]'",
+ "t.ClassVar[int]",
+ ],
+)
+def test_is_class_var(annot):
+ """
+ ClassVars are detected, even if they're a string or quoted.
+ """
+ assert _is_class_var(annot)
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_cmp.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_cmp.py
new file mode 100644
index 0000000000..ec2c687489
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_cmp.py
@@ -0,0 +1,510 @@
+# SPDX-License-Identifier: MIT
+
+"""
+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.
+EqCSameType = cmp_using(eq=lambda a, b: a == b, class_name="EqCSameType")
+PartialOrderCSameType = cmp_using(
+ eq=lambda a, b: a == b,
+ lt=lambda a, b: a < b,
+ class_name="PartialOrderCSameType",
+)
+FullOrderCSameType = cmp_using(
+ eq=lambda a, b: a == b,
+ lt=lambda a, b: a < b,
+ le=lambda a, b: a <= b,
+ gt=lambda a, b: a > b,
+ ge=lambda a, b: a >= b,
+ class_name="FullOrderCSameType",
+)
+
+EqCAnyType = cmp_using(
+ eq=lambda a, b: a == b, require_same_type=False, class_name="EqCAnyType"
+)
+PartialOrderCAnyType = cmp_using(
+ eq=lambda a, b: a == b,
+ lt=lambda a, b: a < b,
+ require_same_type=False,
+ class_name="PartialOrderCAnyType",
+)
+
+
+eq_data = [
+ (EqCSameType, True),
+ (EqCAnyType, False),
+]
+
+order_data = [
+ (PartialOrderCSameType, True),
+ (PartialOrderCAnyType, False),
+ (FullOrderCSameType, True),
+]
+
+eq_ids = [c[0].__name__ for c in eq_data]
+order_ids = [c[0].__name__ for c in order_data]
+
+cmp_data = eq_data + order_data
+cmp_ids = eq_ids + order_ids
+
+
+class TestEqOrder(object):
+ """
+ Tests for eq and order related methods.
+ """
+
+ #########
+ # eq
+ #########
+ @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.
+ """
+ assert cls(1) == cls(1)
+ assert not (cls(1) != cls(1))
+
+ @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.
+ """
+ assert cls(1) != cls(2)
+ assert not (cls(1) == cls(2))
+
+ @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.
+ """
+ assert (cls(1) == cls(1.0)) == (not requires_same_type)
+ assert not (cls(1) != cls(1.0)) == (not requires_same_type)
+
+ #########
+ # lt
+ #########
+ @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError")
+ @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__.
+ """
+ with pytest.raises(TypeError):
+ cls(1) < cls(2)
+
+ @pytest.mark.parametrize(
+ "cls, requires_same_type", order_data, ids=order_ids
+ )
+ def test_lt_same_type(self, cls, requires_same_type):
+ """
+ Less-than objects are detected appropriately.
+ """
+ assert cls(1) < cls(2)
+ assert not (cls(2) < cls(1))
+
+ @pytest.mark.parametrize(
+ "cls, requires_same_type", order_data, ids=order_ids
+ )
+ def test_not_lt_same_type(self, cls, requires_same_type):
+ """
+ Not less-than objects are detected appropriately.
+ """
+ assert cls(2) >= cls(1)
+ assert not (cls(1) >= cls(2))
+
+ @pytest.mark.parametrize(
+ "cls, requires_same_type", order_data, ids=order_ids
+ )
+ def test_lt_different_type(self, cls, requires_same_type):
+ """
+ Less-than values of different types are detected appropriately.
+ """
+ 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)
+ else:
+ assert cls(1) < cls(2.0)
+ assert not (cls(2) < cls(1.0))
+
+ #########
+ # le
+ #########
+ @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError")
+ @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__.
+ """
+ with pytest.raises(TypeError):
+ cls(1) <= cls(2)
+
+ @pytest.mark.parametrize(
+ "cls, requires_same_type", order_data, ids=order_ids
+ )
+ def test_le_same_type(self, cls, requires_same_type):
+ """
+ Less-than-or-equal objects are detected appropriately.
+ """
+ assert cls(1) <= cls(1)
+ assert cls(1) <= cls(2)
+ assert not (cls(2) <= cls(1))
+
+ @pytest.mark.parametrize(
+ "cls, requires_same_type", order_data, ids=order_ids
+ )
+ def test_not_le_same_type(self, cls, requires_same_type):
+ """
+ Not less-than-or-equal objects are detected appropriately.
+ """
+ assert cls(2) > cls(1)
+ assert not (cls(1) > cls(1))
+ assert not (cls(1) > cls(2))
+
+ @pytest.mark.parametrize(
+ "cls, requires_same_type", order_data, ids=order_ids
+ )
+ def test_le_different_type(self, cls, requires_same_type):
+ """
+ Less-than-or-equal values of diff. types are detected appropriately.
+ """
+ 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)
+ else:
+ assert cls(1) <= cls(2.0)
+ assert cls(1) <= cls(1.0)
+ assert not (cls(2) <= cls(1.0))
+
+ #########
+ # gt
+ #########
+ @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError")
+ @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__.
+ """
+ with pytest.raises(TypeError):
+ cls(2) > cls(1)
+
+ @pytest.mark.parametrize(
+ "cls, requires_same_type", order_data, ids=order_ids
+ )
+ def test_gt_same_type(self, cls, requires_same_type):
+ """
+ Greater-than objects are detected appropriately.
+ """
+ assert cls(2) > cls(1)
+ assert not (cls(1) > cls(2))
+
+ @pytest.mark.parametrize(
+ "cls, requires_same_type", order_data, ids=order_ids
+ )
+ def test_not_gt_same_type(self, cls, requires_same_type):
+ """
+ Not greater-than objects are detected appropriately.
+ """
+ assert cls(1) <= cls(2)
+ assert not (cls(2) <= cls(1))
+
+ @pytest.mark.parametrize(
+ "cls, requires_same_type", order_data, ids=order_ids
+ )
+ def test_gt_different_type(self, cls, requires_same_type):
+ """
+ Greater-than values of different types are detected appropriately.
+ """
+ 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)
+ else:
+ assert cls(2) > cls(1.0)
+ assert not (cls(1) > cls(2.0))
+
+ #########
+ # ge
+ #########
+ @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError")
+ @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__.
+ """
+ with pytest.raises(TypeError):
+ cls(2) >= cls(1)
+
+ @pytest.mark.parametrize(
+ "cls, requires_same_type", order_data, ids=order_ids
+ )
+ def test_ge_same_type(self, cls, requires_same_type):
+ """
+ Greater-than-or-equal objects are detected appropriately.
+ """
+ assert cls(1) >= cls(1)
+ assert cls(2) >= cls(1)
+ assert not (cls(1) >= cls(2))
+
+ @pytest.mark.parametrize(
+ "cls, requires_same_type", order_data, ids=order_ids
+ )
+ def test_not_ge_same_type(self, cls, requires_same_type):
+ """
+ Not greater-than-or-equal objects are detected appropriately.
+ """
+ assert cls(1) < cls(2)
+ assert not (cls(1) < cls(1))
+ assert not (cls(2) < cls(1))
+
+ @pytest.mark.parametrize(
+ "cls, requires_same_type", order_data, ids=order_ids
+ )
+ def test_ge_different_type(self, cls, requires_same_type):
+ """
+ Greater-than-or-equal values of diff. types are detected appropriately.
+ """
+ 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)
+ else:
+ assert cls(2) >= cls(2.0)
+ assert cls(2) >= cls(1.0)
+ assert not (cls(1) >= cls(2.0))
+
+
+class TestDundersUnnamedClass(object):
+ """
+ Tests for dunder attributes of unnamed classes.
+ """
+
+ cls = cmp_using(eq=lambda a, b: a == b)
+
+ def test_class(self):
+ """
+ Class name and qualified name should be well behaved.
+ """
+ assert self.cls.__name__ == "Comparable"
+ if not PY2:
+ assert self.cls.__qualname__ == "Comparable"
+
+ def test_eq(self):
+ """
+ __eq__ docstring and qualified name should be well behaved.
+ """
+ method = self.cls.__eq__
+ assert method.__doc__.strip() == "Return a == b. Computed by attrs."
+ assert method.__name__ == "__eq__"
+
+ def test_ne(self):
+ """
+ __ne__ docstring and qualified name should be well behaved.
+ """
+ method = self.cls.__ne__
+ assert method.__doc__.strip() == (
+ "Check equality and either forward a NotImplemented or\n"
+ " return the result negated."
+ )
+ assert method.__name__ == "__ne__"
+
+
+class TestTotalOrderingException(object):
+ """
+ Test for exceptions related to total ordering.
+ """
+
+ def test_eq_must_specified(self):
+ """
+ `total_ordering` requires `__eq__` to be specified.
+ """
+ with pytest.raises(ValueError) as ei:
+ cmp_using(lt=lambda a, b: a < b)
+
+ assert ei.value.args[0] == (
+ "eq must be define is order to complete ordering from "
+ "lt, le, gt, ge."
+ )
+
+
+class TestNotImplementedIsPropagated(object):
+ """
+ Test related to functions that return NotImplemented.
+ """
+
+ def test_not_implemented_is_propagated(self):
+ """
+ If the comparison function returns NotImplemented,
+ the dunder method should too.
+ """
+ C = cmp_using(eq=lambda a, b: NotImplemented if a == 1 else a == b)
+
+ assert C(2) == C(2)
+ assert C(1) != C(1)
+
+
+class TestDundersPartialOrdering(object):
+ """
+ Tests for dunder attributes of classes with partial ordering.
+ """
+
+ cls = PartialOrderCSameType
+
+ def test_class(self):
+ """
+ Class name and qualified name should be well behaved.
+ """
+ assert self.cls.__name__ == "PartialOrderCSameType"
+ if not PY2:
+ assert self.cls.__qualname__ == "PartialOrderCSameType"
+
+ def test_eq(self):
+ """
+ __eq__ docstring and qualified name should be well behaved.
+ """
+ method = self.cls.__eq__
+ assert method.__doc__.strip() == "Return a == b. Computed by attrs."
+ assert method.__name__ == "__eq__"
+
+ def test_ne(self):
+ """
+ __ne__ docstring and qualified name should be well behaved.
+ """
+ method = self.cls.__ne__
+ assert method.__doc__.strip() == (
+ "Check equality and either forward a NotImplemented or\n"
+ " return the result negated."
+ )
+ assert method.__name__ == "__ne__"
+
+ def test_lt(self):
+ """
+ __lt__ docstring and qualified name should be well behaved.
+ """
+ method = self.cls.__lt__
+ assert method.__doc__.strip() == "Return a < b. Computed by attrs."
+ assert method.__name__ == "__lt__"
+
+ def test_le(self):
+ """
+ __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.__name__ == "__le__"
+
+ def test_gt(self):
+ """
+ __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.__name__ == "__gt__"
+
+ def test_ge(self):
+ """
+ __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.__name__ == "__ge__"
+
+
+class TestDundersFullOrdering(object):
+ """
+ Tests for dunder attributes of classes with full ordering.
+ """
+
+ cls = FullOrderCSameType
+
+ def test_class(self):
+ """
+ Class name and qualified name should be well behaved.
+ """
+ assert self.cls.__name__ == "FullOrderCSameType"
+ if not PY2:
+ assert self.cls.__qualname__ == "FullOrderCSameType"
+
+ def test_eq(self):
+ """
+ __eq__ docstring and qualified name should be well behaved.
+ """
+ method = self.cls.__eq__
+ assert method.__doc__.strip() == "Return a == b. Computed by attrs."
+ assert method.__name__ == "__eq__"
+
+ def test_ne(self):
+ """
+ __ne__ docstring and qualified name should be well behaved.
+ """
+ method = self.cls.__ne__
+ assert method.__doc__.strip() == (
+ "Check equality and either forward a NotImplemented or\n"
+ " return the result negated."
+ )
+ assert method.__name__ == "__ne__"
+
+ def test_lt(self):
+ """
+ __lt__ docstring and qualified name should be well behaved.
+ """
+ method = self.cls.__lt__
+ assert method.__doc__.strip() == "Return a < b. Computed by attrs."
+ assert method.__name__ == "__lt__"
+
+ def test_le(self):
+ """
+ __le__ docstring and qualified name should be well behaved.
+ """
+ method = self.cls.__le__
+ assert method.__doc__.strip() == "Return a <= b. Computed by attrs."
+ assert method.__name__ == "__le__"
+
+ def test_gt(self):
+ """
+ __gt__ docstring and qualified name should be well behaved.
+ """
+ method = self.cls.__gt__
+ assert method.__doc__.strip() == "Return a > b. Computed by attrs."
+ assert method.__name__ == "__gt__"
+
+ def test_ge(self):
+ """
+ __ge__ docstring and qualified name should be well behaved.
+ """
+ method = self.cls.__ge__
+ assert method.__doc__.strip() == "Return a >= b. Computed by attrs."
+ assert method.__name__ == "__ge__"
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_compat.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_compat.py
new file mode 100644
index 0000000000..464b492f0f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_compat.py
@@ -0,0 +1,52 @@
+# SPDX-License-Identifier: MIT
+
+import pytest
+
+from attr._compat import metadata_proxy
+
+
+@pytest.fixture(name="mp")
+def _mp():
+ return metadata_proxy({"x": 42, "y": "foo"})
+
+
+class TestMetadataProxy:
+ """
+ Ensure properties of metadata_proxy independently of hypothesis strategies.
+ """
+
+ def test_repr(self, mp):
+ """
+ repr makes sense and is consistent across Python versions.
+ """
+ assert any(
+ [
+ "mappingproxy({'x': 42, 'y': 'foo'})" == repr(mp),
+ "mappingproxy({'y': 'foo', 'x': 42})" == repr(mp),
+ ]
+ )
+
+ def test_immutable(self, mp):
+ """
+ All mutating methods raise errors.
+ """
+ with pytest.raises(TypeError, match="not support item assignment"):
+ mp["z"] = 23
+
+ with pytest.raises(TypeError, match="not support item deletion"):
+ del mp["x"]
+
+ with pytest.raises(AttributeError, match="no attribute 'update'"):
+ mp.update({})
+
+ with pytest.raises(AttributeError, match="no attribute 'clear'"):
+ mp.clear()
+
+ with pytest.raises(AttributeError, match="no attribute 'pop'"):
+ mp.pop("x")
+
+ with pytest.raises(AttributeError, match="no attribute 'popitem'"):
+ mp.popitem()
+
+ with pytest.raises(AttributeError, match="no attribute 'setdefault'"):
+ mp.setdefault("x")
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_config.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_config.py
new file mode 100644
index 0000000000..bbf6756406
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_config.py
@@ -0,0 +1,45 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Tests for `attr._config`.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+import pytest
+
+from attr import _config
+
+
+class TestConfig(object):
+ def test_default(self):
+ """
+ Run validators by default.
+ """
+ assert True is _config._run_validators
+
+ def test_set_run_validators(self):
+ """
+ Sets `_run_validators`.
+ """
+ _config.set_run_validators(False)
+ assert False is _config._run_validators
+ _config.set_run_validators(True)
+ assert True is _config._run_validators
+
+ def test_get_run_validators(self):
+ """
+ Returns `_run_validators`.
+ """
+ _config._run_validators = False
+ assert _config._run_validators is _config.get_run_validators()
+ _config._run_validators = True
+ assert _config._run_validators is _config.get_run_validators()
+
+ def test_wrong_type(self):
+ """
+ Passing anything else than a boolean raises TypeError.
+ """
+ with pytest.raises(TypeError) as e:
+ _config.set_run_validators("False")
+ assert "'run' must be bool." == e.value.args[0]
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_converters.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_converters.py
new file mode 100644
index 0000000000..d0fc723eb1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_converters.py
@@ -0,0 +1,163 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Tests for `attr.converters`.
+"""
+
+from __future__ import absolute_import
+
+import pytest
+
+import attr
+
+from attr import Factory, attrib
+from attr.converters import default_if_none, optional, pipe, to_bool
+
+
+class TestOptional(object):
+ """
+ Tests for `optional`.
+ """
+
+ def test_success_with_type(self):
+ """
+ Wrapped converter is used as usual if value is not None.
+ """
+ c = optional(int)
+
+ assert c("42") == 42
+
+ def test_success_with_none(self):
+ """
+ Nothing happens if None.
+ """
+ c = optional(int)
+
+ assert c(None) is None
+
+ def test_fail(self):
+ """
+ Propagates the underlying conversion error when conversion fails.
+ """
+ c = optional(int)
+
+ with pytest.raises(ValueError):
+ c("not_an_int")
+
+
+class TestDefaultIfNone(object):
+ def test_missing_default(self):
+ """
+ Raises TypeError if neither default nor factory have been passed.
+ """
+ with pytest.raises(TypeError, match="Must pass either"):
+ default_if_none()
+
+ def test_too_many_defaults(self):
+ """
+ Raises TypeError if both default and factory are passed.
+ """
+ with pytest.raises(TypeError, match="but not both"):
+ default_if_none(True, lambda: 42)
+
+ def test_factory_takes_self(self):
+ """
+ Raises ValueError if passed Factory has takes_self=True.
+ """
+ with pytest.raises(ValueError, match="takes_self"):
+ default_if_none(Factory(list, takes_self=True))
+
+ @pytest.mark.parametrize("val", [1, 0, True, False, "foo", "", object()])
+ def test_not_none(self, val):
+ """
+ If a non-None value is passed, it's handed down.
+ """
+ c = default_if_none("nope")
+
+ assert val == c(val)
+
+ c = default_if_none(factory=list)
+
+ assert val == c(val)
+
+ def test_none_value(self):
+ """
+ Default values are returned when a None is passed.
+ """
+ c = default_if_none(42)
+
+ assert 42 == c(None)
+
+ def test_none_factory(self):
+ """
+ Factories are used if None is passed.
+ """
+ c = default_if_none(factory=list)
+
+ assert [] == c(None)
+
+ c = default_if_none(default=Factory(list))
+
+ assert [] == c(None)
+
+
+class TestPipe(object):
+ def test_success(self):
+ """
+ Succeeds if all wrapped converters succeed.
+ """
+ c = pipe(str, to_bool, bool)
+
+ assert True is c("True") is c(True)
+
+ def test_fail(self):
+ """
+ Fails if any wrapped converter fails.
+ """
+ c = pipe(str, to_bool)
+
+ # First wrapped converter fails:
+ with pytest.raises(ValueError):
+ c(33)
+
+ # Last wrapped converter fails:
+ with pytest.raises(ValueError):
+ c("33")
+
+ def test_sugar(self):
+ """
+ `pipe(c1, c2, c3)` and `[c1, c2, c3]` are equivalent.
+ """
+
+ @attr.s
+ class C(object):
+ 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
+
+
+class TestToBool(object):
+ def test_unhashable(self):
+ """
+ Fails if value is unhashable.
+ """
+ with pytest.raises(ValueError, match="Cannot convert value to bool"):
+ to_bool([])
+
+ def test_truthy(self):
+ """
+ Fails if truthy values are incorrectly converted.
+ """
+ assert to_bool("t")
+ assert to_bool("yes")
+ assert to_bool("on")
+
+ def test_falsy(self):
+ """
+ Fails if falsy values are incorrectly converted.
+ """
+ assert not to_bool("f")
+ assert not to_bool("no")
+ assert not to_bool("off")
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_dunders.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_dunders.py
new file mode 100644
index 0000000000..186762eb0d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_dunders.py
@@ -0,0 +1,1008 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Tests for dunder methods from `attrib._make`.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+import copy
+import pickle
+
+import pytest
+
+from hypothesis import given
+from hypothesis.strategies import booleans
+
+import attr
+
+from attr._make import (
+ NOTHING,
+ Factory,
+ _add_repr,
+ _is_slot_cls,
+ _make_init,
+ _Nothing,
+ fields,
+ make_class,
+)
+from attr.validators import instance_of
+
+from .utils import simple_attr, simple_class
+
+
+EqC = simple_class(eq=True)
+EqCSlots = simple_class(eq=True, slots=True)
+OrderC = simple_class(order=True)
+OrderCSlots = simple_class(order=True, slots=True)
+ReprC = simple_class(repr=True)
+ReprCSlots = simple_class(repr=True, slots=True)
+
+
+@attr.s(eq=True)
+class EqCallableC(object):
+ a = attr.ib(eq=str.lower, order=False)
+ b = attr.ib(eq=True)
+
+
+@attr.s(eq=True, slots=True)
+class EqCallableCSlots(object):
+ a = attr.ib(eq=str.lower, order=False)
+ b = attr.ib(eq=True)
+
+
+@attr.s(order=True)
+class OrderCallableC(object):
+ a = attr.ib(eq=True, order=str.lower)
+ b = attr.ib(order=True)
+
+
+@attr.s(order=True, slots=True)
+class OrderCallableCSlots(object):
+ a = attr.ib(eq=True, order=str.lower)
+ b = attr.ib(order=True)
+
+
+# HashC is hashable by explicit definition while HashCSlots is hashable
+# implicitly. The "Cached" versions are the same, except with hash code
+# caching enabled
+HashC = simple_class(hash=True)
+HashCSlots = simple_class(hash=None, eq=True, frozen=True, slots=True)
+HashCCached = simple_class(hash=True, cache_hash=True)
+HashCSlotsCached = simple_class(
+ hash=None, eq=True, frozen=True, slots=True, cache_hash=True
+)
+# the cached hash code is stored slightly differently in this case
+# so it needs to be tested separately
+HashCFrozenNotSlotsCached = simple_class(
+ frozen=True, slots=False, hash=True, cache_hash=True
+)
+
+
+def _add_init(cls, frozen):
+ """
+ Add a __init__ method to *cls*. If *frozen* is True, make it immutable.
+
+ 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.
+ """
+ cls.__init__ = _make_init(
+ cls,
+ cls.__attrs_attrs__,
+ getattr(cls, "__attrs_pre_init__", False),
+ getattr(cls, "__attrs_post_init__", False),
+ frozen,
+ _is_slot_cls(cls),
+ cache_hash=False,
+ base_attr_map={},
+ is_exc=False,
+ cls_on_setattr=None,
+ attrs_init=False,
+ )
+ return cls
+
+
+class InitC(object):
+ __attrs_attrs__ = [simple_attr("a"), simple_attr("b")]
+
+
+InitC = _add_init(InitC, False)
+
+
+class TestEqOrder(object):
+ """
+ Tests for eq and order related methods.
+ """
+
+ @given(booleans())
+ def test_eq_ignore_attrib(self, slots):
+ """
+ If `eq` is False for an attribute, ignore that attribute.
+ """
+ C = make_class(
+ "C", {"a": attr.ib(eq=False), "b": attr.ib()}, slots=slots
+ )
+
+ assert C(1, 2) == C(2, 2)
+
+ @pytest.mark.parametrize("cls", [EqC, EqCSlots])
+ def test_equal(self, cls):
+ """
+ Equal objects are detected as equal.
+ """
+ assert cls(1, 2) == cls(1, 2)
+ assert not (cls(1, 2) != cls(1, 2))
+
+ @pytest.mark.parametrize("cls", [EqCallableC, EqCallableCSlots])
+ def test_equal_callable(self, cls):
+ """
+ Equal objects are detected as equal.
+ """
+ assert cls("Test", 1) == cls("test", 1)
+ assert cls("Test", 1) != cls("test", 2)
+ assert not (cls("Test", 1) != cls("test", 1))
+ assert not (cls("Test", 1) == cls("test", 2))
+
+ @pytest.mark.parametrize("cls", [EqC, EqCSlots])
+ def test_unequal_same_class(self, cls):
+ """
+ Unequal objects of correct type are detected as unequal.
+ """
+ assert cls(1, 2) != cls(2, 1)
+ assert not (cls(1, 2) == cls(2, 1))
+
+ @pytest.mark.parametrize("cls", [EqCallableC, EqCallableCSlots])
+ def test_unequal_same_class_callable(self, cls):
+ """
+ Unequal objects of correct type are detected as unequal.
+ """
+ assert cls("Test", 1) != cls("foo", 2)
+ assert not (cls("Test", 1) == cls("foo", 2))
+
+ @pytest.mark.parametrize(
+ "cls", [EqC, EqCSlots, EqCallableC, EqCallableCSlots]
+ )
+ def test_unequal_different_class(self, cls):
+ """
+ Unequal objects of different type are detected even if their attributes
+ match.
+ """
+
+ class NotEqC(object):
+ a = 1
+ b = 2
+
+ assert cls(1, 2) != NotEqC()
+ assert not (cls(1, 2) == NotEqC())
+
+ @pytest.mark.parametrize("cls", [OrderC, OrderCSlots])
+ def test_lt(self, cls):
+ """
+ __lt__ compares objects as tuples of attribute values.
+ """
+ for a, b in [
+ ((1, 2), (2, 1)),
+ ((1, 2), (1, 3)),
+ (("a", "b"), ("b", "a")),
+ ]:
+ assert cls(*a) < cls(*b)
+
+ @pytest.mark.parametrize("cls", [OrderCallableC, OrderCallableCSlots])
+ def test_lt_callable(self, cls):
+ """
+ __lt__ compares objects as tuples of attribute values.
+ """
+ # Note: "A" < "a"
+ for a, b in [
+ (("test1", 1), ("Test1", 2)),
+ (("test0", 1), ("Test1", 1)),
+ ]:
+ assert cls(*a) < cls(*b)
+
+ @pytest.mark.parametrize(
+ "cls", [OrderC, OrderCSlots, OrderCallableC, OrderCallableCSlots]
+ )
+ def test_lt_unordable(self, cls):
+ """
+ __lt__ returns NotImplemented if classes differ.
+ """
+ assert NotImplemented == (cls(1, 2).__lt__(42))
+
+ @pytest.mark.parametrize("cls", [OrderC, OrderCSlots])
+ def test_le(self, cls):
+ """
+ __le__ compares objects as tuples of attribute values.
+ """
+ for a, b in [
+ ((1, 2), (2, 1)),
+ ((1, 2), (1, 3)),
+ ((1, 1), (1, 1)),
+ (("a", "b"), ("b", "a")),
+ (("a", "b"), ("a", "b")),
+ ]:
+ assert cls(*a) <= cls(*b)
+
+ @pytest.mark.parametrize("cls", [OrderCallableC, OrderCallableCSlots])
+ def test_le_callable(self, cls):
+ """
+ __le__ compares objects as tuples of attribute values.
+ """
+ # Note: "A" < "a"
+ for a, b in [
+ (("test1", 1), ("Test1", 1)),
+ (("test1", 1), ("Test1", 2)),
+ (("test0", 1), ("Test1", 1)),
+ (("test0", 2), ("Test1", 1)),
+ ]:
+ assert cls(*a) <= cls(*b)
+
+ @pytest.mark.parametrize(
+ "cls", [OrderC, OrderCSlots, OrderCallableC, OrderCallableCSlots]
+ )
+ def test_le_unordable(self, cls):
+ """
+ __le__ returns NotImplemented if classes differ.
+ """
+ assert NotImplemented == (cls(1, 2).__le__(42))
+
+ @pytest.mark.parametrize("cls", [OrderC, OrderCSlots])
+ def test_gt(self, cls):
+ """
+ __gt__ compares objects as tuples of attribute values.
+ """
+ for a, b in [
+ ((2, 1), (1, 2)),
+ ((1, 3), (1, 2)),
+ (("b", "a"), ("a", "b")),
+ ]:
+ assert cls(*a) > cls(*b)
+
+ @pytest.mark.parametrize("cls", [OrderCallableC, OrderCallableCSlots])
+ def test_gt_callable(self, cls):
+ """
+ __gt__ compares objects as tuples of attribute values.
+ """
+ # Note: "A" < "a"
+ for a, b in [
+ (("Test1", 2), ("test1", 1)),
+ (("Test1", 1), ("test0", 1)),
+ ]:
+ assert cls(*a) > cls(*b)
+
+ @pytest.mark.parametrize(
+ "cls", [OrderC, OrderCSlots, OrderCallableC, OrderCallableCSlots]
+ )
+ def test_gt_unordable(self, cls):
+ """
+ __gt__ returns NotImplemented if classes differ.
+ """
+ assert NotImplemented == (cls(1, 2).__gt__(42))
+
+ @pytest.mark.parametrize("cls", [OrderC, OrderCSlots])
+ def test_ge(self, cls):
+ """
+ __ge__ compares objects as tuples of attribute values.
+ """
+ for a, b in [
+ ((2, 1), (1, 2)),
+ ((1, 3), (1, 2)),
+ ((1, 1), (1, 1)),
+ (("b", "a"), ("a", "b")),
+ (("a", "b"), ("a", "b")),
+ ]:
+ assert cls(*a) >= cls(*b)
+
+ @pytest.mark.parametrize("cls", [OrderCallableC, OrderCallableCSlots])
+ def test_ge_callable(self, cls):
+ """
+ __ge__ compares objects as tuples of attribute values.
+ """
+ # Note: "A" < "a"
+ for a, b in [
+ (("Test1", 1), ("test1", 1)),
+ (("Test1", 2), ("test1", 1)),
+ (("Test1", 1), ("test0", 1)),
+ (("Test1", 1), ("test0", 2)),
+ ]:
+ assert cls(*a) >= cls(*b)
+
+ @pytest.mark.parametrize(
+ "cls", [OrderC, OrderCSlots, OrderCallableC, OrderCallableCSlots]
+ )
+ def test_ge_unordable(self, cls):
+ """
+ __ge__ returns NotImplemented if classes differ.
+ """
+ assert NotImplemented == (cls(1, 2).__ge__(42))
+
+
+class TestAddRepr(object):
+ """
+ Tests for `_add_repr`.
+ """
+
+ @pytest.mark.parametrize("slots", [True, False])
+ def test_repr(self, slots):
+ """
+ If `repr` is False, ignore that attribute.
+ """
+ C = make_class(
+ "C", {"a": attr.ib(repr=False), "b": attr.ib()}, slots=slots
+ )
+
+ assert "C(b=2)" == repr(C(1, 2))
+
+ @pytest.mark.parametrize("cls", [ReprC, ReprCSlots])
+ def test_repr_works(self, cls):
+ """
+ repr returns a sensible value.
+ """
+ assert "C(a=1, b=2)" == repr(cls(1, 2))
+
+ def test_custom_repr_works(self):
+ """
+ repr returns a sensible value for attributes with a custom repr
+ callable.
+ """
+
+ def custom_repr(value):
+ return "foo:" + str(value)
+
+ @attr.s
+ class C(object):
+ a = attr.ib(repr=custom_repr)
+
+ assert "C(a=foo:1)" == repr(C(1))
+
+ def test_infinite_recursion(self):
+ """
+ In the presence of a cyclic graph, repr will emit an ellipsis and not
+ raise an exception.
+ """
+
+ @attr.s
+ class Cycle(object):
+ value = attr.ib(default=7)
+ cycle = attr.ib(default=None)
+
+ cycle = Cycle()
+ cycle.cycle = cycle
+ assert "Cycle(value=7, cycle=...)" == repr(cycle)
+
+ def test_infinite_recursion_long_cycle(self):
+ """
+ A cyclic graph can pass through other non-attrs objects, and repr will
+ still emit an ellipsis and not raise an exception.
+ """
+
+ @attr.s
+ class LongCycle(object):
+ value = attr.ib(default=14)
+ cycle = attr.ib(default=None)
+
+ cycle = LongCycle()
+ # Ensure that the reference cycle passes through a non-attrs object.
+ # This demonstrates the need for a thread-local "global" ID tracker.
+ cycle.cycle = {"cycle": [cycle]}
+ assert "LongCycle(value=14, cycle={'cycle': [...]})" == repr(cycle)
+
+ def test_underscores(self):
+ """
+ repr does not strip underscores.
+ """
+
+ class C(object):
+ __attrs_attrs__ = [simple_attr("_x")]
+
+ C = _add_repr(C)
+ i = C()
+ i._x = 42
+
+ assert "C(_x=42)" == repr(i)
+
+ def test_repr_uninitialized_member(self):
+ """
+ repr signals unset attributes
+ """
+ C = make_class("C", {"a": attr.ib(init=False)})
+
+ assert "C(a=NOTHING)" == repr(C())
+
+ @given(add_str=booleans(), slots=booleans())
+ def test_str(self, add_str, slots):
+ """
+ If str is True, it returns the same as repr.
+
+ This only makes sense when subclassing a class with an poor __str__
+ (like Exceptions).
+ """
+
+ @attr.s(str=add_str, slots=slots)
+ class Error(Exception):
+ x = attr.ib()
+
+ e = Error(42)
+
+ assert (str(e) == repr(e)) is add_str
+
+ def test_str_no_repr(self):
+ """
+ Raises a ValueError if repr=False and str=True.
+ """
+ with pytest.raises(ValueError) as e:
+ simple_class(repr=False, str=True)
+
+ assert (
+ "__str__ can only be generated if a __repr__ exists."
+ ) == e.value.args[0]
+
+
+# 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):
+ foo_value = attr.ib()
+
+
+@attr.attrs(hash=True, cache_hash=True)
+class HashCacheSerializationTestCached(object):
+ foo_value = attr.ib()
+
+
+@attr.attrs(slots=True, hash=True, cache_hash=True)
+class HashCacheSerializationTestCachedSlots(object):
+ foo_value = attr.ib()
+
+
+class IncrementingHasher(object):
+ def __init__(self):
+ self.hash_value = 100
+
+ def __hash__(self):
+ rv = self.hash_value
+ self.hash_value += 1
+ return rv
+
+
+class TestAddHash(object):
+ """
+ Tests for `_add_hash`.
+ """
+
+ def test_enforces_type(self):
+ """
+ The `hash` argument to both attrs and attrib must be None, True, or
+ False.
+ """
+ exc_args = ("Invalid value for hash. Must be True, False, or None.",)
+
+ with pytest.raises(TypeError) as e:
+ make_class("C", {}, hash=1),
+
+ assert exc_args == e.value.args
+
+ with pytest.raises(TypeError) as e:
+ make_class("C", {"a": attr.ib(hash=1)}),
+
+ assert exc_args == e.value.args
+
+ def test_enforce_no_cache_hash_without_hash(self):
+ """
+ Ensure exception is thrown if caching the hash code is requested
+ but attrs is not requested to generate `__hash__`.
+ """
+ exc_args = (
+ "Invalid value for cache_hash. To use hash caching,"
+ " hashing must be either explicitly or implicitly "
+ "enabled.",
+ )
+ with pytest.raises(TypeError) as e:
+ make_class("C", {}, hash=False, cache_hash=True)
+ assert exc_args == e.value.args
+
+ # unhashable case
+ with pytest.raises(TypeError) as e:
+ make_class(
+ "C", {}, hash=None, eq=True, frozen=False, cache_hash=True
+ )
+ assert exc_args == e.value.args
+
+ def test_enforce_no_cached_hash_without_init(self):
+ """
+ Ensure exception is thrown if caching the hash code is requested
+ but attrs is not requested to generate `__init__`.
+ """
+ exc_args = (
+ "Invalid value for cache_hash. To use hash caching,"
+ " init must be True.",
+ )
+ with pytest.raises(TypeError) as e:
+ make_class("C", {}, init=False, hash=True, cache_hash=True)
+ assert exc_args == e.value.args
+
+ @given(booleans(), booleans())
+ def test_hash_attribute(self, slots, cache_hash):
+ """
+ If `hash` is False on an attribute, ignore that attribute.
+ """
+ C = make_class(
+ "C",
+ {"a": attr.ib(hash=False), "b": attr.ib()},
+ slots=slots,
+ hash=True,
+ cache_hash=cache_hash,
+ )
+
+ assert hash(C(1, 2)) == hash(C(2, 2))
+
+ @given(booleans())
+ def test_hash_attribute_mirrors_eq(self, eq):
+ """
+ If `hash` is None, the hash generation mirrors `eq`.
+ """
+ C = make_class("C", {"a": attr.ib(eq=eq)}, eq=True, frozen=True)
+
+ if eq:
+ assert C(1) != C(2)
+ assert hash(C(1)) != hash(C(2))
+ assert hash(C(1)) == hash(C(1))
+ else:
+ assert C(1) == C(2)
+ assert hash(C(1)) == hash(C(2))
+
+ @given(booleans())
+ def test_hash_mirrors_eq(self, eq):
+ """
+ If `hash` is None, the hash generation mirrors `eq`.
+ """
+ C = make_class("C", {"a": attr.ib()}, eq=eq, frozen=True)
+
+ i = C(1)
+
+ assert i == i
+ assert hash(i) == hash(i)
+
+ if eq:
+ assert C(1) == C(1)
+ assert hash(C(1)) == hash(C(1))
+ else:
+ assert C(1) != C(1)
+ assert hash(C(1)) != hash(C(1))
+
+ @pytest.mark.parametrize(
+ "cls",
+ [
+ HashC,
+ HashCSlots,
+ HashCCached,
+ HashCSlotsCached,
+ HashCFrozenNotSlotsCached,
+ ],
+ )
+ def test_hash_works(self, cls):
+ """
+ __hash__ returns different hashes for different values.
+ """
+ a = cls(1, 2)
+ b = cls(1, 1)
+ assert hash(a) != hash(b)
+ # perform the test again to test the pre-cached path through
+ # __hash__ for the cached-hash versions
+ assert hash(a) != hash(b)
+
+ def test_hash_default(self):
+ """
+ Classes are not hashable by default.
+ """
+ C = make_class("C", {})
+
+ with pytest.raises(TypeError) as e:
+ hash(C())
+
+ assert e.value.args[0] in (
+ "'C' objects are unhashable", # PyPy
+ "unhashable type: 'C'", # CPython
+ )
+
+ def test_cache_hashing(self):
+ """
+ Ensure that hash computation if cached if and only if requested
+ """
+
+ class HashCounter:
+ """
+ A class for testing which counts how many times its hash
+ has been requested
+ """
+
+ def __init__(self):
+ self.times_hash_called = 0
+
+ def __hash__(self):
+ self.times_hash_called += 1
+ return 12345
+
+ Uncached = make_class(
+ "Uncached",
+ {"hash_counter": attr.ib(factory=HashCounter)},
+ hash=True,
+ cache_hash=False,
+ )
+ Cached = make_class(
+ "Cached",
+ {"hash_counter": attr.ib(factory=HashCounter)},
+ hash=True,
+ cache_hash=True,
+ )
+
+ uncached_instance = Uncached()
+ cached_instance = Cached()
+
+ hash(uncached_instance)
+ hash(uncached_instance)
+ hash(cached_instance)
+ hash(cached_instance)
+
+ assert 2 == uncached_instance.hash_counter.times_hash_called
+ 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)
+
+ # 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):
+ x = attr.ib()
+
+ a = C(IncrementingHasher())
+ # Ensure that any hash cache would be calculated before copy
+ orig_hash = hash(a)
+ b = copy.deepcopy(a)
+
+ if kwargs["cache_hash"]:
+ # For cache_hash classes, this call is cached
+ assert orig_hash == hash(a)
+
+ assert orig_hash != hash(b)
+
+ @pytest.mark.parametrize(
+ "klass,cached",
+ [
+ (HashCacheSerializationTestUncached, False),
+ (HashCacheSerializationTestCached, True),
+ (HashCacheSerializationTestCachedSlots, True),
+ ],
+ )
+ def test_cache_hash_serialization_hash_cleared(self, klass, cached):
+ """
+ Tests that the hash cache is cleared on deserialization to fix
+ https://github.com/python-attrs/attrs/issues/482 .
+
+ This test is intended to guard against a stale hash code surviving
+ across serialization (which may cause problems when the hash value
+ is different in different interpreters).
+ """
+
+ obj = klass(IncrementingHasher())
+ original_hash = hash(obj)
+ obj_rt = self._roundtrip_pickle(obj)
+
+ if cached:
+ assert original_hash == hash(obj)
+
+ 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__
+ won't contain the state dictionary; this test ensures that the custom
+ __reduce__ generated when cache_hash=True works in that case.
+ """
+
+ @attr.s(frozen=frozen, cache_hash=True, hash=True)
+ class C(object):
+ x = attr.ib()
+
+ def __getstate__(self):
+ return None
+
+ # By the nature of this test it doesn't really create an object that's
+ # in a valid state - it basically does the equivalent of
+ # `object.__new__(C)`, so it doesn't make much sense to assert anything
+ # about the result of the copy. This test will just check that it
+ # doesn't raise an *error*.
+ copy.deepcopy(C(1))
+
+ def _roundtrip_pickle(self, obj):
+ pickle_str = pickle.dumps(obj)
+ return pickle.loads(pickle_str)
+
+
+class TestAddInit(object):
+ """
+ Tests for `_add_init`.
+ """
+
+ @given(booleans(), booleans())
+ def test_init(self, slots, frozen):
+ """
+ If `init` is False, ignore that attribute.
+ """
+ C = make_class(
+ "C",
+ {"a": attr.ib(init=False), "b": attr.ib()},
+ slots=slots,
+ frozen=frozen,
+ )
+ with pytest.raises(TypeError) as e:
+ C(a=1, b=2)
+
+ assert e.value.args[0].endswith(
+ "__init__() got an unexpected keyword argument 'a'"
+ )
+
+ @given(booleans(), booleans())
+ def test_no_init_default(self, slots, frozen):
+ """
+ If `init` is False but a Factory is specified, don't allow passing that
+ argument but initialize it anyway.
+ """
+ C = make_class(
+ "C",
+ {
+ "_a": attr.ib(init=False, default=42),
+ "_b": attr.ib(init=False, default=Factory(list)),
+ "c": attr.ib(),
+ },
+ slots=slots,
+ frozen=frozen,
+ )
+ with pytest.raises(TypeError):
+ C(a=1, c=2)
+ with pytest.raises(TypeError):
+ C(b=1, c=2)
+
+ i = C(23)
+ assert (42, [], 23) == (i._a, i._b, i.c)
+
+ @given(booleans(), booleans())
+ def test_no_init_order(self, slots, frozen):
+ """
+ If an attribute is `init=False`, it's legal to come after a mandatory
+ attribute.
+ """
+ make_class(
+ "C",
+ {"a": attr.ib(default=Factory(list)), "b": attr.ib(init=False)},
+ slots=slots,
+ frozen=frozen,
+ )
+
+ def test_sets_attributes(self):
+ """
+ The attributes are initialized using the passed keywords.
+ """
+ obj = InitC(a=1, b=2)
+ assert 1 == obj.a
+ assert 2 == obj.b
+
+ def test_default(self):
+ """
+ If a default value is present, it's used as fallback.
+ """
+
+ class C(object):
+ __attrs_attrs__ = [
+ simple_attr(name="a", default=2),
+ simple_attr(name="b", default="hallo"),
+ simple_attr(name="c", default=None),
+ ]
+
+ C = _add_init(C, False)
+ i = C()
+ assert 2 == i.a
+ assert "hallo" == i.b
+ assert None is i.c
+
+ def test_factory(self):
+ """
+ If a default factory is present, it's used as fallback.
+ """
+
+ class D(object):
+ pass
+
+ class C(object):
+ __attrs_attrs__ = [
+ simple_attr(name="a", default=Factory(list)),
+ simple_attr(name="b", default=Factory(D)),
+ ]
+
+ C = _add_init(C, False)
+ i = C()
+
+ assert [] == i.a
+ assert isinstance(i.b, D)
+
+ def test_validator(self):
+ """
+ If a validator is passed, call it with the preliminary instance, the
+ Attribute, and the argument.
+ """
+
+ class VException(Exception):
+ pass
+
+ def raiser(*args):
+ raise VException(*args)
+
+ C = make_class("C", {"a": attr.ib("a", validator=raiser)})
+ with pytest.raises(VException) as e:
+ C(42)
+
+ assert (fields(C).a, 42) == e.value.args[1:]
+ assert isinstance(e.value.args[0], C)
+
+ def test_validator_slots(self):
+ """
+ If a validator is passed, call it with the preliminary instance, the
+ Attribute, and the argument.
+ """
+
+ class VException(Exception):
+ pass
+
+ def raiser(*args):
+ raise VException(*args)
+
+ C = make_class("C", {"a": attr.ib("a", validator=raiser)}, slots=True)
+ with pytest.raises(VException) as e:
+ C(42)
+
+ assert (fields(C)[0], 42) == e.value.args[1:]
+ assert isinstance(e.value.args[0], C)
+
+ @given(booleans())
+ def test_validator_others(self, slots):
+ """
+ Does not interfere when setting non-attrs attributes.
+ """
+ C = make_class(
+ "C", {"a": attr.ib("a", validator=instance_of(int))}, slots=slots
+ )
+ i = C(1)
+
+ assert 1 == i.a
+
+ if not slots:
+ i.b = "foo"
+ assert "foo" == i.b
+ else:
+ with pytest.raises(AttributeError):
+ i.b = "foo"
+
+ def test_underscores(self):
+ """
+ The argument names in `__init__` are without leading and trailing
+ underscores.
+ """
+
+ class C(object):
+ __attrs_attrs__ = [simple_attr("_private")]
+
+ C = _add_init(C, False)
+ i = C(private=42)
+ assert 42 == i._private
+
+
+class TestNothing(object):
+ """
+ Tests for `_Nothing`.
+ """
+
+ def test_copy(self):
+ """
+ __copy__ returns the same object.
+ """
+ n = _Nothing()
+ assert n is copy.copy(n)
+
+ def test_deepcopy(self):
+ """
+ __deepcopy__ returns the same object.
+ """
+ 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()
+
+ def test_false(self):
+ """
+ NOTHING evaluates as falsey.
+ """
+ assert not NOTHING
+ assert False is bool(NOTHING)
+
+
+@attr.s(hash=True, order=True)
+class C(object):
+ pass
+
+
+# Store this class so that we recreate it.
+OriginalC = C
+
+
+@attr.s(hash=True, order=True)
+class C(object):
+ pass
+
+
+CopyC = C
+
+
+@attr.s(hash=True, order=True)
+class C(object):
+ """A different class, to generate different methods."""
+
+ a = attr.ib()
+
+
+class TestFilenames(object):
+ def test_filenames(self):
+ """
+ The created dunder methods have a "consistent" filename.
+ """
+ assert (
+ OriginalC.__init__.__code__.co_filename
+ == "<attrs generated init tests.test_dunders.C>"
+ )
+ assert (
+ OriginalC.__eq__.__code__.co_filename
+ == "<attrs generated eq tests.test_dunders.C>"
+ )
+ assert (
+ OriginalC.__hash__.__code__.co_filename
+ == "<attrs generated hash tests.test_dunders.C>"
+ )
+ assert (
+ CopyC.__init__.__code__.co_filename
+ == "<attrs generated init tests.test_dunders.C>"
+ )
+ assert (
+ CopyC.__eq__.__code__.co_filename
+ == "<attrs generated eq tests.test_dunders.C>"
+ )
+ assert (
+ CopyC.__hash__.__code__.co_filename
+ == "<attrs generated hash tests.test_dunders.C>"
+ )
+ assert (
+ C.__init__.__code__.co_filename
+ == "<attrs generated init tests.test_dunders.C-1>"
+ )
+ assert (
+ C.__eq__.__code__.co_filename
+ == "<attrs generated eq tests.test_dunders.C-1>"
+ )
+ assert (
+ C.__hash__.__code__.co_filename
+ == "<attrs generated hash tests.test_dunders.C-1>"
+ )
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_filters.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_filters.py
new file mode 100644
index 0000000000..d1ec24dc6c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_filters.py
@@ -0,0 +1,111 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Tests for `attr.filters`.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+import pytest
+
+import attr
+
+from attr import fields
+from attr.filters import _split_what, exclude, include
+
+
+@attr.s
+class C(object):
+ a = attr.ib()
+ b = attr.ib()
+
+
+class TestSplitWhat(object):
+ """
+ Tests for `_split_what`.
+ """
+
+ def test_splits(self):
+ """
+ Splits correctly.
+ """
+ assert (
+ frozenset((int, str)),
+ frozenset((fields(C).a,)),
+ ) == _split_what((str, fields(C).a, int))
+
+
+class TestInclude(object):
+ """
+ Tests for `include`.
+ """
+
+ @pytest.mark.parametrize(
+ "incl,value",
+ [
+ ((int,), 42),
+ ((str,), "hello"),
+ ((str, fields(C).a), 42),
+ ((str, fields(C).b), "hello"),
+ ],
+ )
+ def test_allow(self, incl, value):
+ """
+ Return True if a class or attribute is included.
+ """
+ i = include(*incl)
+ assert i(fields(C).a, value) is True
+
+ @pytest.mark.parametrize(
+ "incl,value",
+ [
+ ((str,), 42),
+ ((int,), "hello"),
+ ((str, fields(C).b), 42),
+ ((int, fields(C).b), "hello"),
+ ],
+ )
+ def test_drop_class(self, incl, value):
+ """
+ Return False on non-included classes and attributes.
+ """
+ i = include(*incl)
+ assert i(fields(C).a, value) is False
+
+
+class TestExclude(object):
+ """
+ Tests for `exclude`.
+ """
+
+ @pytest.mark.parametrize(
+ "excl,value",
+ [
+ ((str,), 42),
+ ((int,), "hello"),
+ ((str, fields(C).b), 42),
+ ((int, fields(C).b), "hello"),
+ ],
+ )
+ def test_allow(self, excl, value):
+ """
+ Return True if class or attribute is not excluded.
+ """
+ e = exclude(*excl)
+ assert e(fields(C).a, value) is True
+
+ @pytest.mark.parametrize(
+ "excl,value",
+ [
+ ((int,), 42),
+ ((str,), "hello"),
+ ((str, fields(C).a), 42),
+ ((str, fields(C).b), "hello"),
+ ],
+ )
+ def test_drop_class(self, excl, value):
+ """
+ Return True on non-excluded classes and attributes.
+ """
+ e = exclude(*excl)
+ assert e(fields(C).a, value) is False
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_funcs.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_funcs.py
new file mode 100644
index 0000000000..4490ed815a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_funcs.py
@@ -0,0 +1,680 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Tests for `attr._funcs`.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+from collections import OrderedDict
+
+import pytest
+
+from hypothesis import assume, given
+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.exceptions import AttrsAttributeNotFoundError
+from attr.validators import instance_of
+
+from .strategies import nested_classes, simple_classes
+
+
+MAPPING_TYPES = (dict, OrderedDict)
+SEQUENCE_TYPES = (list, tuple)
+
+
+@pytest.fixture(scope="session", name="C")
+def _C():
+ """
+ Return a simple but fully featured attrs class with an x and a y attribute.
+ """
+ import attr
+
+ @attr.s
+ class C(object):
+ x = attr.ib()
+ y = attr.ib()
+
+ return C
+
+
+class TestAsDict(object):
+ """
+ Tests for `asdict`.
+ """
+
+ @given(st.sampled_from(MAPPING_TYPES))
+ def test_shallow(self, C, dict_factory):
+ """
+ Shallow asdict returns correct dict.
+ """
+ assert {"x": 1, "y": 2} == asdict(
+ C(x=1, y=2), False, dict_factory=dict_factory
+ )
+
+ @given(st.sampled_from(MAPPING_TYPES))
+ def test_recurse(self, C, dict_class):
+ """
+ Deep asdict returns correct dict.
+ """
+ assert {"x": {"x": 1, "y": 2}, "y": {"x": 3, "y": 4}} == asdict(
+ C(C(1, 2), C(3, 4)), dict_factory=dict_class
+ )
+
+ def test_nested_lists(self, C):
+ """
+ Test unstructuring deeply nested lists.
+ """
+ inner = C(1, 2)
+ outer = C([[inner]], None)
+
+ assert {"x": [[{"x": 1, "y": 2}]], "y": None} == asdict(outer)
+
+ def test_nested_dicts(self, C):
+ """
+ Test unstructuring deeply nested dictionaries.
+ """
+ inner = C(1, 2)
+ outer = C({1: {2: inner}}, None)
+
+ assert {"x": {1: {2: {"x": 1, "y": 2}}}, "y": None} == asdict(outer)
+
+ @given(nested_classes, st.sampled_from(MAPPING_TYPES))
+ def test_recurse_property(self, cls, dict_class):
+ """
+ Property tests for recursive asdict.
+ """
+ obj = cls()
+ obj_dict = asdict(obj, dict_factory=dict_class)
+
+ def assert_proper_dict_class(obj, obj_dict):
+ assert isinstance(obj_dict, dict_class)
+
+ for field in fields(obj.__class__):
+ field_val = getattr(obj, field.name)
+ if has(field_val.__class__):
+ # This field holds a class, recurse the assertions.
+ assert_proper_dict_class(field_val, obj_dict[field.name])
+ elif isinstance(field_val, Sequence):
+ dict_val = obj_dict[field.name]
+ for item, item_dict in zip(field_val, dict_val):
+ if has(item.__class__):
+ assert_proper_dict_class(item, item_dict)
+ elif isinstance(field_val, Mapping):
+ # This field holds a dictionary.
+ assert isinstance(obj_dict[field.name], dict_class)
+
+ for key, val in field_val.items():
+ if has(val.__class__):
+ assert_proper_dict_class(
+ val, obj_dict[field.name][key]
+ )
+
+ assert_proper_dict_class(obj, obj_dict)
+
+ @given(st.sampled_from(MAPPING_TYPES))
+ def test_filter(self, C, dict_factory):
+ """
+ Attributes that are supposed to be skipped are skipped.
+ """
+ assert {"x": {"x": 1}} == asdict(
+ C(C(1, 2), C(3, 4)),
+ filter=lambda a, v: a.name != "y",
+ dict_factory=dict_factory,
+ )
+
+ @given(container=st.sampled_from(SEQUENCE_TYPES))
+ def test_lists_tuples(self, container, C):
+ """
+ If recurse is True, also recurse into lists.
+ """
+ assert {
+ "x": 1,
+ "y": [{"x": 2, "y": 3}, {"x": 4, "y": 5}, "a"],
+ } == asdict(C(1, container([C(2, 3), C(4, 5), "a"])))
+
+ @given(container=st.sampled_from(SEQUENCE_TYPES))
+ def test_lists_tuples_retain_type(self, container, C):
+ """
+ If recurse and retain_collection_types are True, also recurse
+ into lists and do not convert them into list.
+ """
+ assert {
+ "x": 1,
+ "y": container([{"x": 2, "y": 3}, {"x": 4, "y": 5}, "a"]),
+ } == asdict(
+ C(1, container([C(2, 3), C(4, 5), "a"])),
+ retain_collection_types=True,
+ )
+
+ @given(set_type=st.sampled_from((set, frozenset)))
+ def test_sets_no_retain(self, C, set_type):
+ """
+ Set types are converted to lists if retain_collection_types=False.
+ """
+ d = asdict(
+ C(1, set_type((1, 2, 3))),
+ retain_collection_types=False,
+ recurse=True,
+ )
+
+ assert {"x": 1, "y": [1, 2, 3]} == d
+
+ @given(st.sampled_from(MAPPING_TYPES))
+ def test_dicts(self, C, dict_factory):
+ """
+ If recurse is True, also recurse into dicts.
+ """
+ res = asdict(C(1, {"a": C(4, 5)}), dict_factory=dict_factory)
+
+ assert {"x": 1, "y": {"a": {"x": 4, "y": 5}}} == res
+ assert isinstance(res, dict_factory)
+
+ @given(simple_classes(private_attrs=False), st.sampled_from(MAPPING_TYPES))
+ def test_roundtrip(self, cls, dict_class):
+ """
+ Test dumping to dicts and back for Hypothesis-generated classes.
+
+ Private attributes don't round-trip (the attribute name is different
+ than the initializer argument).
+ """
+ instance = cls()
+ dict_instance = asdict(instance, dict_factory=dict_class)
+
+ assert isinstance(dict_instance, dict_class)
+
+ roundtrip_instance = cls(**dict_instance)
+
+ assert instance == roundtrip_instance
+
+ @given(simple_classes())
+ def test_asdict_preserve_order(self, cls):
+ """
+ Field order should be preserved when dumping to an ordered_dict.
+ """
+ instance = cls()
+ dict_instance = asdict(instance, dict_factory=ordered_dict)
+
+ assert [a.name for a in fields(cls)] == list(dict_instance.keys())
+
+ def test_retain_keys_are_tuples(self):
+ """
+ retain_collect_types also retains keys.
+ """
+
+ @attr.s
+ class A(object):
+ a = attr.ib()
+
+ instance = A({(1,): 1})
+
+ assert {"a": {(1,): 1}} == attr.asdict(
+ instance, retain_collection_types=True
+ )
+
+ def test_tuple_keys(self):
+ """
+ If a key is collection type, retain_collection_types is False,
+ the key is serialized as a tuple.
+
+ See #646
+ """
+
+ @attr.s
+ class A(object):
+ a = attr.ib()
+
+ instance = A({(1,): 1})
+
+ assert {"a": {(1,): 1}} == attr.asdict(instance)
+
+
+class TestAsTuple(object):
+ """
+ Tests for `astuple`.
+ """
+
+ @given(st.sampled_from(SEQUENCE_TYPES))
+ def test_shallow(self, C, tuple_factory):
+ """
+ Shallow astuple returns correct dict.
+ """
+ assert tuple_factory([1, 2]) == astuple(
+ C(x=1, y=2), False, tuple_factory=tuple_factory
+ )
+
+ @given(st.sampled_from(SEQUENCE_TYPES))
+ def test_recurse(self, C, tuple_factory):
+ """
+ Deep astuple returns correct tuple.
+ """
+ assert tuple_factory(
+ [tuple_factory([1, 2]), tuple_factory([3, 4])]
+ ) == astuple(C(C(1, 2), C(3, 4)), tuple_factory=tuple_factory)
+
+ @given(nested_classes, st.sampled_from(SEQUENCE_TYPES))
+ def test_recurse_property(self, cls, tuple_class):
+ """
+ Property tests for recursive astuple.
+ """
+ obj = cls()
+ obj_tuple = astuple(obj, tuple_factory=tuple_class)
+
+ def assert_proper_tuple_class(obj, obj_tuple):
+ assert isinstance(obj_tuple, tuple_class)
+ for index, field in enumerate(fields(obj.__class__)):
+ field_val = getattr(obj, field.name)
+ if has(field_val.__class__):
+ # This field holds a class, recurse the assertions.
+ assert_proper_tuple_class(field_val, obj_tuple[index])
+
+ assert_proper_tuple_class(obj, obj_tuple)
+
+ @given(nested_classes, st.sampled_from(SEQUENCE_TYPES))
+ def test_recurse_retain(self, cls, tuple_class):
+ """
+ Property tests for asserting collection types are retained.
+ """
+ obj = cls()
+ obj_tuple = astuple(
+ obj, tuple_factory=tuple_class, retain_collection_types=True
+ )
+
+ def assert_proper_col_class(obj, obj_tuple):
+ # Iterate over all attributes, and if they are lists or mappings
+ # in the original, assert they are the same class in the dumped.
+ for index, field in enumerate(fields(obj.__class__)):
+ field_val = getattr(obj, field.name)
+ if has(field_val.__class__):
+ # This field holds a class, recurse the assertions.
+ assert_proper_col_class(field_val, obj_tuple[index])
+ elif isinstance(field_val, (list, tuple)):
+ # This field holds a sequence of something.
+ expected_type = type(obj_tuple[index])
+ assert type(field_val) is expected_type
+ for obj_e, obj_tuple_e in zip(field_val, obj_tuple[index]):
+ if has(obj_e.__class__):
+ assert_proper_col_class(obj_e, obj_tuple_e)
+ elif isinstance(field_val, dict):
+ orig = field_val
+ tupled = obj_tuple[index]
+ assert type(orig) is type(tupled)
+ for obj_e, obj_tuple_e in zip(
+ orig.items(), tupled.items()
+ ):
+ if has(obj_e[0].__class__): # Dict key
+ assert_proper_col_class(obj_e[0], obj_tuple_e[0])
+ if has(obj_e[1].__class__): # Dict value
+ assert_proper_col_class(obj_e[1], obj_tuple_e[1])
+
+ assert_proper_col_class(obj, obj_tuple)
+
+ @given(st.sampled_from(SEQUENCE_TYPES))
+ def test_filter(self, C, tuple_factory):
+ """
+ Attributes that are supposed to be skipped are skipped.
+ """
+ assert tuple_factory([tuple_factory([1])]) == astuple(
+ C(C(1, 2), C(3, 4)),
+ filter=lambda a, v: a.name != "y",
+ tuple_factory=tuple_factory,
+ )
+
+ @given(container=st.sampled_from(SEQUENCE_TYPES))
+ def test_lists_tuples(self, container, C):
+ """
+ If recurse is True, also recurse into lists.
+ """
+ assert (1, [(2, 3), (4, 5), "a"]) == astuple(
+ C(1, container([C(2, 3), C(4, 5), "a"]))
+ )
+
+ @given(st.sampled_from(SEQUENCE_TYPES))
+ def test_dicts(self, C, tuple_factory):
+ """
+ If recurse is True, also recurse into dicts.
+ """
+ res = astuple(C(1, {"a": C(4, 5)}), tuple_factory=tuple_factory)
+ assert tuple_factory([1, {"a": tuple_factory([4, 5])}]) == res
+ assert isinstance(res, tuple_factory)
+
+ @given(container=st.sampled_from(SEQUENCE_TYPES))
+ def test_lists_tuples_retain_type(self, container, C):
+ """
+ If recurse and retain_collection_types are True, also recurse
+ into lists and do not convert them into list.
+ """
+ assert (1, container([(2, 3), (4, 5), "a"])) == astuple(
+ C(1, container([C(2, 3), C(4, 5), "a"])),
+ retain_collection_types=True,
+ )
+
+ @given(container=st.sampled_from(MAPPING_TYPES))
+ def test_dicts_retain_type(self, container, C):
+ """
+ If recurse and retain_collection_types are True, also recurse
+ into lists and do not convert them into list.
+ """
+ assert (1, container({"a": (4, 5)})) == astuple(
+ C(1, container({"a": C(4, 5)})), retain_collection_types=True
+ )
+
+ @given(simple_classes(), st.sampled_from(SEQUENCE_TYPES))
+ def test_roundtrip(self, cls, tuple_class):
+ """
+ Test dumping to tuple and back for Hypothesis-generated classes.
+ """
+ instance = cls()
+ tuple_instance = astuple(instance, tuple_factory=tuple_class)
+
+ assert isinstance(tuple_instance, tuple_class)
+
+ roundtrip_instance = cls(*tuple_instance)
+
+ assert instance == roundtrip_instance
+
+ @given(set_type=st.sampled_from((set, frozenset)))
+ def test_sets_no_retain(self, C, set_type):
+ """
+ Set types are converted to lists if retain_collection_types=False.
+ """
+ d = astuple(
+ C(1, set_type((1, 2, 3))),
+ retain_collection_types=False,
+ recurse=True,
+ )
+
+ assert (1, [1, 2, 3]) == d
+
+
+class TestHas(object):
+ """
+ Tests for `has`.
+ """
+
+ def test_positive(self, C):
+ """
+ Returns `True` on decorated classes.
+ """
+ assert has(C)
+
+ def test_positive_empty(self):
+ """
+ Returns `True` on decorated classes even if there are no attributes.
+ """
+
+ @attr.s
+ class D(object):
+ pass
+
+ assert has(D)
+
+ def test_negative(self):
+ """
+ Returns `False` on non-decorated classes.
+ """
+ assert not has(object)
+
+
+class TestAssoc(object):
+ """
+ Tests for `assoc`.
+ """
+
+ @given(slots=st.booleans(), frozen=st.booleans())
+ def test_empty(self, slots, frozen):
+ """
+ Empty classes without changes get copied.
+ """
+
+ @attr.s(slots=slots, frozen=frozen)
+ class C(object):
+ pass
+
+ i1 = C()
+ with pytest.deprecated_call():
+ i2 = assoc(i1)
+
+ assert i1 is not i2
+ assert i1 == i2
+
+ @given(simple_classes())
+ def test_no_changes(self, C):
+ """
+ No changes means a verbatim copy.
+ """
+ i1 = C()
+ with pytest.deprecated_call():
+ i2 = assoc(i1)
+
+ assert i1 is not i2
+ assert i1 == i2
+
+ @given(simple_classes(), st.data())
+ def test_change(self, C, data):
+ """
+ Changes work.
+ """
+ # Take the first attribute, and change it.
+ assume(fields(C)) # Skip classes with no attributes.
+ field_names = [a.name for a in fields(C)]
+ original = C()
+ 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)
+
+ for k, v in change_dict.items():
+ assert getattr(changed, k) == v
+
+ @given(simple_classes())
+ def test_unknown(self, C):
+ """
+ Wanting to change an unknown attribute raises an
+ AttrsAttributeNotFoundError.
+ """
+ # No generated class will have a four letter attribute.
+ with pytest.raises(
+ AttrsAttributeNotFoundError
+ ) 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
+
+ def test_frozen(self):
+ """
+ Works on frozen classes.
+ """
+
+ @attr.s(frozen=True)
+ class C(object):
+ x = attr.ib()
+ y = attr.ib()
+
+ with pytest.deprecated_call():
+ 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):
+ """
+ Tests for `evolve`.
+ """
+
+ @given(slots=st.booleans(), frozen=st.booleans())
+ def test_empty(self, slots, frozen):
+ """
+ Empty classes without changes get copied.
+ """
+
+ @attr.s(slots=slots, frozen=frozen)
+ class C(object):
+ pass
+
+ i1 = C()
+ i2 = evolve(i1)
+
+ assert i1 is not i2
+ assert i1 == i2
+
+ @given(simple_classes())
+ def test_no_changes(self, C):
+ """
+ No changes means a verbatim copy.
+ """
+ i1 = C()
+ i2 = evolve(i1)
+
+ assert i1 is not i2
+ assert i1 == i2
+
+ @given(simple_classes(), st.data())
+ def test_change(self, C, data):
+ """
+ Changes work.
+ """
+ # Take the first attribute, and change it.
+ assume(fields(C)) # Skip classes with no attributes.
+ field_names = [a.name for a in fields(C)]
+ original = C()
+ chosen_names = data.draw(st.sets(st.sampled_from(field_names)))
+ # We pay special attention to private attributes, they should behave
+ # like in `__init__`.
+ change_dict = {
+ name.replace("_", ""): data.draw(st.integers())
+ for name in chosen_names
+ }
+ changed = evolve(original, **change_dict)
+ for name in chosen_names:
+ assert getattr(changed, name) == change_dict[name.replace("_", "")]
+
+ @given(simple_classes())
+ def test_unknown(self, C):
+ """
+ Wanting to change an unknown attribute raises an
+ AttrsAttributeNotFoundError.
+ """
+ # No generated class will have a four letter attribute.
+ with pytest.raises(TypeError) as e:
+ evolve(C(), aaaa=2)
+
+ if hasattr(C, "__attrs_init__"):
+ expected = (
+ "__attrs_init__() got an unexpected keyword argument 'aaaa'"
+ )
+ else:
+ expected = "__init__() got an unexpected keyword argument 'aaaa'"
+
+ assert e.value.args[0].endswith(expected)
+
+ def test_validator_failure(self):
+ """
+ TypeError isn't swallowed when validation fails within evolve.
+ """
+
+ @attr.s
+ class C(object):
+ 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))
+
+ def test_private(self):
+ """
+ evolve() acts as `__init__` with regards to private attributes.
+ """
+
+ @attr.s
+ class C(object):
+ _a = attr.ib()
+
+ assert evolve(C(1), a=2)._a == 2
+
+ with pytest.raises(TypeError):
+ evolve(C(1), _a=2)
+
+ with pytest.raises(TypeError):
+ evolve(C(1), a=3, _a=2)
+
+ def test_non_init_attrs(self):
+ """
+ evolve() handles `init=False` attributes.
+ """
+
+ @attr.s
+ class C(object):
+ a = attr.ib()
+ b = attr.ib(init=False, default=0)
+
+ assert evolve(C(1), a=2).a == 2
+
+ def test_regression_attrs_classes(self):
+ """
+ evolve() can evolve fields that are instances of attrs classes.
+
+ Regression test for #804
+ """
+
+ @attr.s
+ class Cls1(object):
+ param1 = attr.ib()
+
+ @attr.s
+ class Cls2(object):
+ param2 = attr.ib()
+
+ obj2a = Cls2(param2="a")
+ obj2b = Cls2(param2="b")
+
+ obj1a = Cls1(param1=obj2a)
+
+ assert Cls1(param1=Cls2(param2="b")) == attr.evolve(
+ obj1a, param1=obj2b
+ )
+
+ def test_dicts(self):
+ """
+ evolve() can replace an attrs class instance with a dict.
+
+ See #806
+ """
+
+ @attr.s
+ class Cls1(object):
+ param1 = attr.ib()
+
+ @attr.s
+ class Cls2(object):
+ param2 = attr.ib()
+
+ obj2a = Cls2(param2="a")
+ obj2b = {"foo": 42, "param2": 42}
+
+ obj1a = Cls1(param1=obj2a)
+
+ assert Cls1({"foo": 42, "param2": 42}) == attr.evolve(
+ obj1a, param1=obj2b
+ )
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_functional.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_functional.py
new file mode 100644
index 0000000000..9b6a27e2f4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_functional.py
@@ -0,0 +1,790 @@
+# SPDX-License-Identifier: MIT
+
+"""
+End-to-end tests.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+import inspect
+import pickle
+
+from copy import deepcopy
+
+import pytest
+import six
+
+from hypothesis import assume, 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):
+ x = attr.ib(validator=attr.validators.instance_of(int))
+ y = attr.ib()
+
+
+@attr.s(slots=True)
+class C1Slots(object):
+ x = attr.ib(validator=attr.validators.instance_of(int))
+ y = attr.ib()
+
+
+foo = None
+
+
+@attr.s()
+class C2(object):
+ x = attr.ib(default=foo)
+ y = attr.ib(default=attr.Factory(list))
+
+
+@attr.s(slots=True)
+class C2Slots(object):
+ x = attr.ib(default=foo)
+ y = attr.ib(default=attr.Factory(list))
+
+
+@attr.s
+class Base(object):
+ x = attr.ib()
+
+ def meth(self):
+ return self.x
+
+
+@attr.s(slots=True)
+class BaseSlots(object):
+ x = attr.ib()
+
+ def meth(self):
+ return self.x
+
+
+@attr.s
+class Sub(Base):
+ y = attr.ib()
+
+
+@attr.s(slots=True)
+class SubSlots(BaseSlots):
+ y = attr.ib()
+
+
+@attr.s(frozen=True, slots=True)
+class Frozen(object):
+ x = attr.ib()
+
+
+@attr.s
+class SubFrozen(Frozen):
+ y = attr.ib()
+
+
+@attr.s(frozen=True, slots=False)
+class FrozenNoSlots(object):
+ x = attr.ib()
+
+
+class Meta(type):
+ pass
+
+
+@attr.s
+@six.add_metaclass(Meta)
+class WithMeta(object):
+ pass
+
+
+@attr.s(slots=True)
+@six.add_metaclass(Meta)
+class WithMetaSlots(object):
+ pass
+
+
+FromMakeClass = attr.make_class("FromMakeClass", ["x"])
+
+
+class TestFunctional(object):
+ """
+ Functional tests.
+ """
+
+ @pytest.mark.parametrize("cls", [C2, C2Slots])
+ def test_fields(self, cls):
+ """
+ `attr.fields` works.
+ """
+ assert (
+ Attribute(
+ name="x",
+ default=foo,
+ validator=None,
+ repr=True,
+ cmp=None,
+ eq=True,
+ order=True,
+ hash=None,
+ init=True,
+ inherited=False,
+ ),
+ Attribute(
+ name="y",
+ default=attr.Factory(list),
+ validator=None,
+ repr=True,
+ cmp=None,
+ eq=True,
+ order=True,
+ hash=None,
+ init=True,
+ inherited=False,
+ ),
+ ) == attr.fields(cls)
+
+ @pytest.mark.parametrize("cls", [C1, C1Slots])
+ def test_asdict(self, cls):
+ """
+ `attr.asdict` works.
+ """
+ assert {"x": 1, "y": 2} == attr.asdict(cls(x=1, y=2))
+
+ @pytest.mark.parametrize("cls", [C1, C1Slots])
+ def test_validator(self, cls):
+ """
+ `instance_of` raises `TypeError` on type mismatch.
+ """
+ with pytest.raises(TypeError) as e:
+ cls("1", 2)
+
+ # 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),
+ attr.fields(C1).x,
+ int,
+ "1",
+ ) == e.value.args
+
+ @given(booleans())
+ def test_renaming(self, slots):
+ """
+ Private members are renamed but only in `__init__`.
+ """
+
+ @attr.s(slots=slots)
+ class C3(object):
+ _x = attr.ib()
+
+ assert "C3(_x=1)" == repr(C3(x=1))
+
+ @given(booleans(), booleans())
+ def test_programmatic(self, slots, frozen):
+ """
+ `attr.make_class` works.
+ """
+ PC = attr.make_class("PC", ["a", "b"], slots=slots, frozen=frozen)
+
+ assert (
+ Attribute(
+ name="a",
+ default=NOTHING,
+ validator=None,
+ repr=True,
+ cmp=None,
+ eq=True,
+ order=True,
+ hash=None,
+ init=True,
+ inherited=False,
+ ),
+ Attribute(
+ name="b",
+ default=NOTHING,
+ validator=None,
+ repr=True,
+ cmp=None,
+ eq=True,
+ order=True,
+ hash=None,
+ init=True,
+ inherited=False,
+ ),
+ ) == attr.fields(PC)
+
+ @pytest.mark.parametrize("cls", [Sub, SubSlots])
+ def test_subclassing_with_extra_attrs(self, cls):
+ """
+ Subclassing (where the subclass has extra attrs) does what you'd hope
+ for.
+ """
+ obj = object()
+ i = cls(x=obj, y=2)
+ 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)
+ else:
+ assert "SubSlots(x={obj}, y=2)".format(obj=obj) == repr(i)
+
+ @pytest.mark.parametrize("base", [Base, BaseSlots])
+ def test_subclass_without_extra_attrs(self, base):
+ """
+ Subclassing (where the subclass does not have extra attrs) still
+ behaves the same as a subclass with extra attrs.
+ """
+
+ class Sub2(base):
+ pass
+
+ obj = object()
+ i = Sub2(x=obj)
+ assert i.x is i.meth() is obj
+ assert "Sub2(x={obj})".format(obj=obj) == repr(i)
+
+ @pytest.mark.parametrize(
+ "frozen_class",
+ [
+ Frozen, # has slots=True
+ attr.make_class("FrozenToo", ["x"], slots=False, frozen=True),
+ ],
+ )
+ def test_frozen_instance(self, frozen_class):
+ """
+ Frozen instances can't be modified (easily).
+ """
+ frozen = frozen_class(1)
+
+ with pytest.raises(FrozenInstanceError) as e:
+ frozen.x = 2
+
+ with pytest.raises(FrozenInstanceError) as e:
+ del frozen.x
+
+ assert e.value.args[0] == "can't set attribute"
+ assert 1 == frozen.x
+
+ @pytest.mark.parametrize(
+ "cls",
+ [
+ C1,
+ C1Slots,
+ C2,
+ C2Slots,
+ Base,
+ BaseSlots,
+ Sub,
+ SubSlots,
+ Frozen,
+ FrozenNoSlots,
+ FromMakeClass,
+ ],
+ )
+ @pytest.mark.parametrize("protocol", range(2, pickle.HIGHEST_PROTOCOL + 1))
+ def test_pickle_attributes(self, cls, protocol):
+ """
+ Pickling/un-pickling of Attribute instances works.
+ """
+ for attribute in attr.fields(cls):
+ assert attribute == pickle.loads(pickle.dumps(attribute, protocol))
+
+ @pytest.mark.parametrize(
+ "cls",
+ [
+ C1,
+ C1Slots,
+ C2,
+ C2Slots,
+ Base,
+ BaseSlots,
+ Sub,
+ SubSlots,
+ Frozen,
+ FrozenNoSlots,
+ FromMakeClass,
+ ],
+ )
+ @pytest.mark.parametrize("protocol", range(2, pickle.HIGHEST_PROTOCOL + 1))
+ def test_pickle_object(self, cls, protocol):
+ """
+ Pickle object serialization works on all kinds of attrs classes.
+ """
+ if len(attr.fields(cls)) == 2:
+ obj = cls(123, 456)
+ else:
+ obj = cls(123)
+
+ assert repr(obj) == repr(pickle.loads(pickle.dumps(obj, protocol)))
+
+ def test_subclassing_frozen_gives_frozen(self):
+ """
+ The frozen-ness of classes is inherited. Subclasses of frozen classes
+ are also frozen and can be instantiated.
+ """
+ i = SubFrozen("foo", "bar")
+
+ assert i.x == "foo"
+ assert i.y == "bar"
+
+ with pytest.raises(FrozenInstanceError):
+ i.x = "baz"
+
+ @pytest.mark.parametrize("cls", [WithMeta, WithMetaSlots])
+ def test_metaclass_preserved(self, cls):
+ """
+ Metaclass data is preserved.
+ """
+ assert Meta == type(cls)
+
+ def test_default_decorator(self):
+ """
+ Default decorator sets the default and the respective method gets
+ called.
+ """
+
+ @attr.s
+ class C(object):
+ x = attr.ib(default=1)
+ y = attr.ib()
+
+ @y.default
+ def compute(self):
+ return self.x + 1
+
+ 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):
+ """
+ Subclasses can overwrite attributes of their base class.
+ """
+
+ @attr.s(slots=slots, frozen=frozen, weakref_slot=weakref_slot)
+ class SubOverwrite(Base):
+ x = attr.ib(default=attr.Factory(list))
+
+ assert SubOverwrite([]) == SubOverwrite()
+
+ def test_dict_patch_class(self):
+ """
+ dict-classes are never replaced.
+ """
+
+ class C(object):
+ x = attr.ib()
+
+ C_new = attr.s(C)
+
+ assert C_new is C
+
+ def test_hash_by_id(self):
+ """
+ With dict classes, hashing by ID is active for hash=False even on
+ Python 3. This is incorrect behavior but we have to retain it for
+ backward compatibility.
+ """
+
+ @attr.s(hash=False)
+ class HashByIDBackwardCompat(object):
+ x = attr.ib()
+
+ assert hash(HashByIDBackwardCompat(1)) != hash(
+ HashByIDBackwardCompat(1)
+ )
+
+ @attr.s(hash=False, eq=False)
+ class HashByID(object):
+ x = attr.ib()
+
+ assert hash(HashByID(1)) != hash(HashByID(1))
+
+ @attr.s(hash=True)
+ class HashByValues(object):
+ x = attr.ib()
+
+ assert hash(HashByValues(1)) == hash(HashByValues(1))
+
+ def test_handles_different_defaults(self):
+ """
+ Unhashable defaults + subclassing values work.
+ """
+
+ @attr.s
+ class Unhashable(object):
+ pass
+
+ @attr.s
+ class C(object):
+ 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):
+ 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):
+ pass
+
+ # Ensure both objects live long enough such that their ids/hashes
+ # can't be recycled. Thanks to Ask Hjorth Larsen for pointing that
+ # out.
+ c1 = C()
+ c2 = C()
+
+ assert hash(c1) != hash(c2)
+
+ def test_overwrite_base(self):
+ """
+ Base classes can overwrite each other and the attributes are added
+ in the order they are defined.
+ """
+
+ @attr.s
+ class C(object):
+ c = attr.ib(default=100)
+ x = attr.ib(default=1)
+ b = attr.ib(default=23)
+
+ @attr.s
+ class D(C):
+ a = attr.ib(default=42)
+ x = attr.ib(default=2)
+ d = attr.ib(default=3.14)
+
+ @attr.s
+ class E(D):
+ y = attr.ib(default=3)
+ z = attr.ib(default=4)
+
+ assert "E(c=100, b=23, a=42, x=2, d=3.14, y=3, z=4)" == repr(E())
+
+ @pytest.mark.parametrize("base_slots", [True, False])
+ @pytest.mark.parametrize("sub_slots", [True, False])
+ @pytest.mark.parametrize("base_frozen", [True, False])
+ @pytest.mark.parametrize("sub_frozen", [True, False])
+ @pytest.mark.parametrize("base_weakref_slot", [True, False])
+ @pytest.mark.parametrize("sub_weakref_slot", [True, False])
+ @pytest.mark.parametrize("base_converter", [True, False])
+ @pytest.mark.parametrize("sub_converter", [True, False])
+ def test_frozen_slots_combo(
+ self,
+ base_slots,
+ sub_slots,
+ base_frozen,
+ sub_frozen,
+ base_weakref_slot,
+ sub_weakref_slot,
+ base_converter,
+ sub_converter,
+ ):
+ """
+ A class with a single attribute, inheriting from another class
+ with a single attribute.
+ """
+
+ @attr.s(
+ frozen=base_frozen,
+ slots=base_slots,
+ weakref_slot=base_weakref_slot,
+ )
+ class Base(object):
+ a = attr.ib(converter=int if base_converter else None)
+
+ @attr.s(
+ frozen=sub_frozen, slots=sub_slots, weakref_slot=sub_weakref_slot
+ )
+ class Sub(Base):
+ b = attr.ib(converter=int if sub_converter else None)
+
+ i = Sub("1", "2")
+
+ assert i.a == (1 if base_converter else "1")
+ assert i.b == (2 if sub_converter else "2")
+
+ if base_frozen or sub_frozen:
+ with pytest.raises(FrozenInstanceError):
+ i.a = "2"
+
+ with pytest.raises(FrozenInstanceError):
+ i.b = "3"
+
+ def test_tuple_class_aliasing(self):
+ """
+ itemgetter and property are legal attribute names.
+ """
+
+ @attr.s
+ class C(object):
+ property = attr.ib()
+ itemgetter = attr.ib()
+ x = attr.ib()
+
+ assert "property" == attr.fields(C).property.name
+ 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
+ hash by id, and store the fields additionally in self.args.
+ """
+
+ @attr.s(auto_exc=True, slots=slots, frozen=frozen)
+ class FooError(Exception):
+ x = attr.ib()
+ y = attr.ib(init=False, default=42)
+ z = attr.ib(init=False)
+ a = attr.ib()
+
+ FooErrorMade = attr.make_class(
+ "FooErrorMade",
+ bases=(Exception,),
+ attrs={
+ "x": attr.ib(),
+ "y": attr.ib(init=False, default=42),
+ "z": attr.ib(init=False),
+ "a": attr.ib(),
+ },
+ auto_exc=True,
+ slots=slots,
+ frozen=frozen,
+ )
+
+ assert FooError(1, "foo") != FooError(1, "foo")
+ assert FooErrorMade(1, "foo") != FooErrorMade(1, "foo")
+
+ for cls in (FooError, FooErrorMade):
+ with pytest.raises(cls) as ei1:
+ raise cls(1, "foo")
+
+ with pytest.raises(cls) as ei2:
+ raise cls(1, "foo")
+
+ e1 = ei1.value
+ e2 = ei2.value
+
+ assert e1 is e1
+ assert e1 == e1
+ assert e2 == e2
+ assert e1 != e2
+ assert "(1, 'foo')" == str(e1) == str(e2)
+ assert (1, "foo") == e1.args == e2.args
+
+ hash(e1) == hash(e1)
+ hash(e2) == hash(e2)
+
+ if not frozen:
+ 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.
+
+ Easy to get wrong with tuple literals.
+ """
+
+ @attr.s(auto_exc=True, slots=slots, frozen=frozen)
+ class FooError(Exception):
+ x = attr.ib()
+
+ 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):
+ 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
+ )
+
+ 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)
+ )
+
+ with pytest.deprecated_call() as dc:
+
+ @attr.s
+ class C(object):
+ x = attr.ib(cmp=cmp, eq=eq, order=order)
+
+ assert rv == attr.fields(C).x.cmp
+
+ (w,) = dc.list
+
+ 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
+ APIs) but sets no validators, don't use the (slower) setattr in
+ __init__.
+
+ Regression test for #816.
+ """
+
+ @attr.s(on_setattr=attr.setters.validate)
+ class C(object):
+ x = attr.ib()
+
+ @attr.s(on_setattr=attr.setters.validate)
+ class D(C):
+ y = attr.ib()
+
+ src = inspect.getsource(D.__init__)
+
+ assert "setattr" not in src
+ assert "self.x = x" in src
+ 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):
+ x = attr.ib()
+
+ @attr.s(on_setattr=attr.setters.convert)
+ class D(C):
+ y = attr.ib()
+
+ src = inspect.getsource(D.__init__)
+
+ assert "setattr" not in src
+ assert "self.x = x" in src
+ 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
+ no validators or converters, don't use the (slower) setattr in
+ __init__.
+ """
+
+ @attr.define
+ class C(object):
+ x = attr.ib()
+
+ src = inspect.getsource(C.__init__)
+
+ assert "setattr" not in src
+ assert "self.x = x" in src
+ assert object.__setattr__ == C.__setattr__
+
+ @attr.define
+ class D(C):
+ y = attr.ib()
+
+ src = inspect.getsource(D.__init__)
+
+ assert "setattr" not in src
+ assert "self.x = x" in src
+ assert "self.y = y" in src
+ assert object.__setattr__ == D.__setattr__
+
+ def test_on_setattr_detect_inherited_validators(self):
+ """
+ _make_init detects the presence of a validator even if the field is
+ inherited.
+ """
+
+ @attr.s(on_setattr=attr.setters.validate)
+ class C(object):
+ x = attr.ib(validator=42)
+
+ @attr.s(on_setattr=attr.setters.validate)
+ class D(C):
+ y = attr.ib()
+
+ src = inspect.getsource(D.__init__)
+
+ assert "_setattr = _cached_setattr" in src
+ assert "_setattr('x', x)" in src
+ assert "_setattr('y', y)" in src
+ assert object.__setattr__ != D.__setattr__
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_hooks.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_hooks.py
new file mode 100644
index 0000000000..92fc2dcaab
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_hooks.py
@@ -0,0 +1,209 @@
+# SPDX-License-Identifier: MIT
+
+from datetime import datetime
+from typing import Dict, List
+
+import attr
+
+
+class TestTransformHook:
+ """
+ Tests for `attrs(tranform_value_serializer=func)`
+ """
+
+ def test_hook_applied(self):
+ """
+ The transform hook is applied to all attributes. Types can be missing,
+ explicitly set, or annotated.
+ """
+ results = []
+
+ def hook(cls, attribs):
+ attr.resolve_types(cls, attribs=attribs)
+ results[:] = [(a.name, a.type) for a in attribs]
+ return attribs
+
+ @attr.s(field_transformer=hook)
+ class C:
+ x = attr.ib()
+ y = attr.ib(type=int)
+ z: float = attr.ib()
+
+ assert results == [("x", None), ("y", int), ("z", float)]
+
+ def test_hook_applied_auto_attrib(self):
+ """
+ The transform hook is applied to all attributes and type annotations
+ are detected.
+ """
+ results = []
+
+ def hook(cls, attribs):
+ attr.resolve_types(cls, attribs=attribs)
+ results[:] = [(a.name, a.type) for a in attribs]
+ return attribs
+
+ @attr.s(auto_attribs=True, field_transformer=hook)
+ class C:
+ x: int
+ y: str = attr.ib()
+
+ assert results == [("x", int), ("y", str)]
+
+ def test_hook_applied_modify_attrib(self):
+ """
+ The transform hook can modify attributes.
+ """
+
+ def hook(cls, attribs):
+ attr.resolve_types(cls, attribs=attribs)
+ return [a.evolve(converter=a.type) for a in attribs]
+
+ @attr.s(auto_attribs=True, field_transformer=hook)
+ class C:
+ x: int = attr.ib(converter=int)
+ y: float
+
+ c = C(x="3", y="3.14")
+ assert c == C(x=3, y=3.14)
+
+ def test_hook_remove_field(self):
+ """
+ It is possible to remove fields via the hook.
+ """
+
+ def hook(cls, attribs):
+ attr.resolve_types(cls, attribs=attribs)
+ return [a for a in attribs if a.type is not int]
+
+ @attr.s(auto_attribs=True, field_transformer=hook)
+ class C:
+ x: int
+ y: float
+
+ assert attr.asdict(C(2.7)) == {"y": 2.7}
+
+ def test_hook_add_field(self):
+ """
+ It is possible to add fields via the hook.
+ """
+
+ def hook(cls, attribs):
+ a1 = attribs[0]
+ a2 = a1.evolve(name="new")
+ return [a1, a2]
+
+ @attr.s(auto_attribs=True, field_transformer=hook)
+ class C:
+ x: int
+
+ assert attr.asdict(C(1, 2)) == {"x": 1, "new": 2}
+
+ def test_hook_with_inheritance(self):
+ """
+ The hook receives all fields from base classes.
+ """
+
+ def hook(cls, attribs):
+ assert [a.name for a in attribs] == ["x", "y"]
+ # Remove Base' "x"
+ return attribs[1:]
+
+ @attr.s(auto_attribs=True)
+ class Base:
+ x: int
+
+ @attr.s(auto_attribs=True, field_transformer=hook)
+ class Sub(Base):
+ y: int
+
+ assert attr.asdict(Sub(2)) == {"y": 2}
+
+ def test_attrs_attrclass(self):
+ """
+ The list of attrs returned by a field_transformer is converted to
+ "AttrsClass" again.
+
+ Regression test for #821.
+ """
+
+ @attr.s(auto_attribs=True, field_transformer=lambda c, a: list(a))
+ class C:
+ x: int
+
+ fields_type = type(attr.fields(C))
+ assert fields_type.__name__ == "CAttributes"
+ assert issubclass(fields_type, tuple)
+
+
+class TestAsDictHook:
+ def test_asdict(self):
+ """
+ asdict() calls the hooks in attrs classes and in other datastructures
+ like lists or dicts.
+ """
+
+ def hook(inst, a, v):
+ if isinstance(v, datetime):
+ return v.isoformat()
+ return v
+
+ @attr.dataclass
+ class Child:
+ x: datetime
+ y: List[datetime]
+
+ @attr.dataclass
+ class Parent:
+ a: Child
+ b: List[Child]
+ c: Dict[str, Child]
+ d: Dict[str, datetime]
+
+ inst = Parent(
+ a=Child(1, [datetime(2020, 7, 1)]),
+ b=[Child(2, [datetime(2020, 7, 2)])],
+ c={"spam": Child(3, [datetime(2020, 7, 3)])},
+ d={"eggs": datetime(2020, 7, 4)},
+ )
+
+ result = attr.asdict(inst, value_serializer=hook)
+ assert result == {
+ "a": {"x": 1, "y": ["2020-07-01T00:00:00"]},
+ "b": [{"x": 2, "y": ["2020-07-02T00:00:00"]}],
+ "c": {"spam": {"x": 3, "y": ["2020-07-03T00:00:00"]}},
+ "d": {"eggs": "2020-07-04T00:00:00"},
+ }
+
+ def test_asdict_calls(self):
+ """
+ The correct instances and attribute names are passed to the hook.
+ """
+ calls = []
+
+ def hook(inst, a, v):
+ calls.append((inst, a.name if a else a, v))
+ return v
+
+ @attr.dataclass
+ class Child:
+ x: int
+
+ @attr.dataclass
+ class Parent:
+ a: Child
+ b: List[Child]
+ c: Dict[str, Child]
+
+ inst = Parent(a=Child(1), b=[Child(2)], c={"spam": Child(3)})
+
+ attr.asdict(inst, value_serializer=hook)
+ assert calls == [
+ (inst, "a", inst.a),
+ (inst.a, "x", inst.a.x),
+ (inst, "b", inst.b),
+ (inst.b[0], "x", inst.b[0].x),
+ (inst, "c", inst.c),
+ (None, None, "spam"),
+ (inst.c["spam"], "x", inst.c["spam"].x),
+ ]
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_import.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_import.py
new file mode 100644
index 0000000000..423124319c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_import.py
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: MIT
+
+
+class TestImportStar(object):
+ def test_from_attr_import_star(self):
+ """
+ import * from attr
+ """
+ # attr_import_star contains `from attr import *`, which cannot
+ # be done here because *-imports are only allowed on module level.
+ from . import attr_import_star # noqa: F401
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_init_subclass.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_init_subclass.py
new file mode 100644
index 0000000000..863e794377
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_init_subclass.py
@@ -0,0 +1,48 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Tests for `__init_subclass__` related tests.
+
+Python 3.6+ only.
+"""
+
+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
+ class both with dict and slotted classes.
+ """
+
+ @attr.s(slots=slots)
+ class Base:
+ def __init_subclass__(cls, param, **kw):
+ super().__init_subclass__(**kw)
+ cls.param = param
+
+ class Vanilla(Base, param="foo"):
+ pass
+
+ assert "foo" == Vanilla().param
+
+
+def test_init_subclass_attrs():
+ """
+ `__init_subclass__` works with attrs classes as long as slots=False.
+ """
+
+ @attr.s(slots=False)
+ class Base:
+ def __init_subclass__(cls, param, **kw):
+ super().__init_subclass__(**kw)
+ cls.param = param
+
+ @attr.s
+ class Attrs(Base, param="foo"):
+ pass
+
+ assert "foo" == Attrs().param
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_make.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_make.py
new file mode 100644
index 0000000000..729d3a71f0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_make.py
@@ -0,0 +1,2462 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Tests for `attr._make`.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+import copy
+import functools
+import gc
+import inspect
+import itertools
+import sys
+
+from operator import attrgetter
+
+import pytest
+
+from hypothesis import assume, given
+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._make import (
+ Attribute,
+ Factory,
+ _AndValidator,
+ _Attributes,
+ _ClassBuilder,
+ _CountingAttr,
+ _determine_attrib_eq_order,
+ _determine_attrs_eq_order,
+ _determine_whether_to_implement,
+ _transform_attrs,
+ and_,
+ fields,
+ fields_dict,
+ make_class,
+ validate,
+)
+from attr.exceptions import (
+ DefaultAlreadySetError,
+ NotAnAttrsClassError,
+ PythonTooOldError,
+)
+
+from .strategies import (
+ gen_attr_names,
+ list_of_attrs,
+ optional_bool,
+ simple_attrs,
+ simple_attrs_with_metadata,
+ simple_attrs_without_metadata,
+ simple_classes,
+)
+from .utils import simple_attr
+
+
+attrs_st = simple_attrs.map(lambda c: Attribute.from_counting_attr("name", c))
+
+
+class TestCountingAttr(object):
+ """
+ Tests for `attr`.
+ """
+
+ def test_returns_Attr(self):
+ """
+ Returns an instance of _CountingAttr.
+ """
+ a = attr.ib()
+
+ assert isinstance(a, _CountingAttr)
+
+ def test_validators_lists_to_wrapped_tuples(self):
+ """
+ If a list is passed as validator, it's just converted to a tuple.
+ """
+
+ def v1(_, __):
+ pass
+
+ def v2(_, __):
+ pass
+
+ a = attr.ib(validator=[v1, v2])
+
+ assert _AndValidator((v1, v2)) == a._validator
+
+ def test_validator_decorator_single(self):
+ """
+ If _CountingAttr.validator is used as a decorator and there is no
+ decorator set, the decorated method is used as the validator.
+ """
+ a = attr.ib()
+
+ @a.validator
+ def v():
+ pass
+
+ assert v == a._validator
+
+ @pytest.mark.parametrize(
+ "wrap", [lambda v: v, lambda v: [v], lambda v: and_(v)]
+ )
+ def test_validator_decorator(self, wrap):
+ """
+ If _CountingAttr.validator is used as a decorator and there is already
+ a decorator set, the decorators are composed using `and_`.
+ """
+
+ def v(_, __):
+ pass
+
+ a = attr.ib(validator=wrap(v))
+
+ @a.validator
+ def v2(self, _, __):
+ pass
+
+ assert _AndValidator((v, v2)) == a._validator
+
+ def test_default_decorator_already_set(self):
+ """
+ Raise DefaultAlreadySetError if the decorator is used after a default
+ has been set.
+ """
+ a = attr.ib(default=42)
+
+ with pytest.raises(DefaultAlreadySetError):
+
+ @a.default
+ def f(self):
+ pass
+
+ def test_default_decorator_sets(self):
+ """
+ Decorator wraps the method in a Factory with pass_self=True and sets
+ the default.
+ """
+ a = attr.ib()
+
+ @a.default
+ def f(self):
+ pass
+
+ assert Factory(f, True) == a._default
+
+
+def make_tc():
+ class TransformC(object):
+ z = attr.ib()
+ y = attr.ib()
+ x = attr.ib()
+ a = 42
+
+ return TransformC
+
+
+class TestTransformAttrs(object):
+ """
+ Tests for `_transform_attrs`.
+ """
+
+ def test_no_modifications(self):
+ """
+ Does not attach __attrs_attrs__ to the class.
+ """
+ C = make_tc()
+ _transform_attrs(C, None, False, False, True, None)
+
+ assert None is getattr(C, "__attrs_attrs__", None)
+
+ def test_normal(self):
+ """
+ Transforms every `_CountingAttr` and leaves others (a) be.
+ """
+ C = make_tc()
+ attrs, _, _ = _transform_attrs(C, None, False, False, True, None)
+
+ assert ["z", "y", "x"] == [a.name for a in attrs]
+
+ def test_empty(self):
+ """
+ No attributes works as expected.
+ """
+
+ @attr.s
+ class C(object):
+ pass
+
+ assert _Attributes(((), [], {})) == _transform_attrs(
+ C, None, False, False, True, None
+ )
+
+ def test_transforms_to_attribute(self):
+ """
+ All `_CountingAttr`s are transformed into `Attribute`s.
+ """
+ C = make_tc()
+ attrs, base_attrs, _ = _transform_attrs(
+ C, None, False, False, True, None
+ )
+
+ assert [] == base_attrs
+ assert 3 == len(attrs)
+ assert all(isinstance(a, Attribute) for a in attrs)
+
+ def test_conflicting_defaults(self):
+ """
+ Raises `ValueError` if attributes with defaults are followed by
+ mandatory attributes.
+ """
+
+ class C(object):
+ x = attr.ib(default=None)
+ y = attr.ib()
+
+ with pytest.raises(ValueError) as e:
+ _transform_attrs(C, None, False, False, True, None)
+ assert (
+ "No mandatory attributes allowed after an attribute with a "
+ "default value or factory. Attribute in question: 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)",
+ ) == 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.
+
+ Updates in the subclass *don't* affect the base class attributes.
+ """
+
+ @attr.s
+ class B(object):
+ b = attr.ib()
+
+ for b_a in B.__attrs_attrs__:
+ assert b_a.kw_only is False
+
+ class C(B):
+ x = attr.ib(default=None)
+ y = attr.ib()
+
+ attrs, base_attrs, _ = _transform_attrs(
+ C, None, False, True, True, None
+ )
+
+ assert len(attrs) == 3
+ assert len(base_attrs) == 1
+
+ for a in attrs:
+ assert a.kw_only is True
+
+ for b_a in B.__attrs_attrs__:
+ assert b_a.kw_only is False
+
+ def test_these(self):
+ """
+ If these is passed, use it and ignore body and base classes.
+ """
+
+ class Base(object):
+ z = attr.ib()
+
+ class C(Base):
+ y = attr.ib()
+
+ attrs, base_attrs, _ = _transform_attrs(
+ C, {"x": attr.ib()}, False, False, True, None
+ )
+
+ assert [] == base_attrs
+ assert (simple_attr("x"),) == attrs
+
+ def test_these_leave_body(self):
+ """
+ If these is passed, no attributes are removed from the body.
+ """
+
+ @attr.s(init=False, these={"x": attr.ib()})
+ class C(object):
+ x = 5
+
+ assert 5 == C().x
+ assert "C(x=5)" == repr(C())
+
+ def test_these_ordered(self):
+ """
+ If these is passed ordered attrs, their order respect instead of the
+ counter.
+ """
+ b = attr.ib(default=2)
+ a = attr.ib(default=1)
+
+ @attr.s(these=ordered_dict([("a", a), ("b", b)]))
+ class C(object):
+ pass
+
+ assert "C(a=1, b=2)" == repr(C())
+
+ def test_multiple_inheritance_old(self):
+ """
+ Old multiple inheritance attributre collection behavior is retained.
+
+ See #285
+ """
+
+ @attr.s
+ class A(object):
+ a1 = attr.ib(default="a1")
+ a2 = attr.ib(default="a2")
+
+ @attr.s
+ class B(A):
+ b1 = attr.ib(default="b1")
+ b2 = attr.ib(default="b2")
+
+ @attr.s
+ class C(B, A):
+ c1 = attr.ib(default="c1")
+ c2 = attr.ib(default="c2")
+
+ @attr.s
+ class D(A):
+ d1 = attr.ib(default="d1")
+ d2 = attr.ib(default="d2")
+
+ @attr.s
+ class E(C, D):
+ e1 = attr.ib(default="e1")
+ e2 = attr.ib(default="e2")
+
+ assert (
+ "E(a1='a1', a2='a2', b1='b1', b2='b2', c1='c1', c2='c2', d1='d1', "
+ "d2='d2', e1='e1', e2='e2')"
+ ) == repr(E())
+
+ def test_overwrite_proper_mro(self):
+ """
+ The proper MRO path works single overwrites too.
+ """
+
+ @attr.s(collect_by_mro=True)
+ class C(object):
+ x = attr.ib(default=1)
+
+ @attr.s(collect_by_mro=True)
+ class D(C):
+ x = attr.ib(default=2)
+
+ assert "D(x=2)" == repr(D())
+
+ def test_multiple_inheritance_proper_mro(self):
+ """
+ Attributes are collected according to the MRO.
+
+ See #428
+ """
+
+ @attr.s
+ class A(object):
+ a1 = attr.ib(default="a1")
+ a2 = attr.ib(default="a2")
+
+ @attr.s
+ class B(A):
+ b1 = attr.ib(default="b1")
+ b2 = attr.ib(default="b2")
+
+ @attr.s
+ class C(B, A):
+ c1 = attr.ib(default="c1")
+ c2 = attr.ib(default="c2")
+
+ @attr.s
+ class D(A):
+ d1 = attr.ib(default="d1")
+ d2 = attr.ib(default="d2")
+
+ @attr.s(collect_by_mro=True)
+ class E(C, D):
+ e1 = attr.ib(default="e1")
+ e2 = attr.ib(default="e2")
+
+ assert (
+ "E(a1='a1', a2='a2', d1='d1', d2='d2', b1='b1', b2='b2', c1='c1', "
+ "c2='c2', e1='e1', e2='e2')"
+ ) == repr(E())
+
+ def test_mro(self):
+ """
+ Attributes and methods are looked up the same way.
+
+ See #428
+ """
+
+ @attr.s(collect_by_mro=True)
+ class A(object):
+
+ x = attr.ib(10)
+
+ def xx(self):
+ return 10
+
+ @attr.s(collect_by_mro=True)
+ class B(A):
+ y = attr.ib(20)
+
+ @attr.s(collect_by_mro=True)
+ class C(A):
+ x = attr.ib(50)
+
+ def xx(self):
+ return 50
+
+ @attr.s(collect_by_mro=True)
+ class D(B, C):
+ pass
+
+ d = D()
+
+ assert d.x == d.xx()
+
+ def test_inherited(self):
+ """
+ Inherited Attributes have `.inherited` True, otherwise False.
+ """
+
+ @attr.s
+ class A(object):
+ a = attr.ib()
+
+ @attr.s
+ class B(A):
+ b = attr.ib()
+
+ @attr.s
+ class C(B):
+ a = attr.ib()
+ c = attr.ib()
+
+ f = attr.fields
+
+ assert False is f(A).a.inherited
+
+ assert True is f(B).a.inherited
+ assert False is f(B).b.inherited
+
+ assert False is f(C).a.inherited
+ assert True is f(C).b.inherited
+ assert False is f(C).c.inherited
+
+
+class TestAttributes(object):
+ """
+ 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):
+ x = attr.ib()
+
+ assert "x" == C.__attrs_attrs__[0].name
+ assert all(isinstance(a, Attribute) for a in C.__attrs_attrs__)
+
+ def test_empty(self):
+ """
+ No attributes, no problems.
+ """
+
+ @attr.s
+ class C3(object):
+ pass
+
+ assert "C3()" == repr(C3())
+ assert C3() == C3()
+
+ @given(attr=attrs_st, attr_name=sampled_from(Attribute.__slots__))
+ def test_immutable(self, attr, attr_name):
+ """
+ Attribute instances are immutable.
+ """
+ with pytest.raises(AttributeError):
+ setattr(attr, attr_name, 1)
+
+ @pytest.mark.parametrize(
+ "method_name", ["__repr__", "__eq__", "__hash__", "__init__"]
+ )
+ def test_adds_all_by_default(self, method_name):
+ """
+ If no further arguments are supplied, all add_XXX functions except
+ add_hash are applied. __hash__ is set to None.
+ """
+ # Set the method name to a sentinel and check whether it has been
+ # overwritten afterwards.
+ sentinel = object()
+
+ class C(object):
+ x = attr.ib()
+
+ setattr(C, method_name, sentinel)
+
+ C = attr.s(C)
+ meth = getattr(C, method_name)
+
+ assert sentinel != meth
+ if method_name == "__hash__":
+ assert meth is None
+
+ @pytest.mark.parametrize(
+ "arg_name, method_name",
+ [
+ ("repr", "__repr__"),
+ ("eq", "__eq__"),
+ ("order", "__le__"),
+ ("hash", "__hash__"),
+ ("init", "__init__"),
+ ],
+ )
+ def test_respects_add_arguments(self, arg_name, method_name):
+ """
+ If a certain `XXX` is `False`, `__XXX__` is not added to the class.
+ """
+ # Set the method name to a sentinel and check whether it has been
+ # overwritten afterwards.
+ sentinel = object()
+
+ am_args = {
+ "repr": True,
+ "eq": True,
+ "order": True,
+ "hash": True,
+ "init": True,
+ }
+ am_args[arg_name] = False
+ if arg_name == "eq":
+ am_args["order"] = False
+
+ class C(object):
+ x = attr.ib()
+
+ setattr(C, method_name, sentinel)
+
+ C = attr.s(**am_args)(C)
+
+ assert sentinel == getattr(C, method_name)
+
+ @pytest.mark.parametrize("init", [True, False])
+ def test_respects_init_attrs_init(self, init):
+ """
+ If init=False, adds __attrs_init__ to the class.
+ Otherwise, it does not.
+ """
+
+ class C(object):
+ 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__.
+ """
+
+ @attr.s(slots=slots_outer)
+ class C(object):
+ @attr.s(slots=slots_inner)
+ class D(object):
+ pass
+
+ assert "C.D()" == repr(C.D())
+ assert "GC.D()" == repr(GC.D())
+
+ @given(slots_outer=booleans(), slots_inner=booleans())
+ def test_repr_fake_qualname(self, slots_outer, slots_inner):
+ """
+ Setting repr_ns overrides a potentially guessed namespace.
+ """
+
+ @attr.s(slots=slots_outer)
+ class C(object):
+ @attr.s(repr_ns="C", slots=slots_inner)
+ class D(object):
+ 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__.
+ """
+
+ @attr.s(slots=slots_outer)
+ class C(object):
+ @attr.s(slots=slots_inner)
+ class D(object):
+ 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):
+ """
+ Verify that __attrs_pre_init__ gets called if defined.
+ """
+ monkeypatch.setattr(_config, "_run_validators", with_validation)
+
+ @attr.s
+ class C(object):
+ def __attrs_pre_init__(self2):
+ self2.z = 30
+
+ c = C()
+
+ assert 30 == getattr(c, "z", None)
+
+ @pytest.mark.parametrize("with_validation", [True, False])
+ def test_post_init(self, with_validation, monkeypatch):
+ """
+ Verify that __attrs_post_init__ gets called if defined.
+ """
+ monkeypatch.setattr(_config, "_run_validators", with_validation)
+
+ @attr.s
+ class C(object):
+ x = attr.ib()
+ y = attr.ib()
+
+ def __attrs_post_init__(self2):
+ self2.z = self2.x + self2.y
+
+ c = C(x=10, y=20)
+
+ assert 30 == getattr(c, "z", None)
+
+ @pytest.mark.parametrize("with_validation", [True, False])
+ def test_pre_post_init_order(self, with_validation, monkeypatch):
+ """
+ Verify that __attrs_post_init__ gets called if defined.
+ """
+ monkeypatch.setattr(_config, "_run_validators", with_validation)
+
+ @attr.s
+ class C(object):
+ x = attr.ib()
+
+ def __attrs_pre_init__(self2):
+ self2.z = 30
+
+ def __attrs_post_init__(self2):
+ self2.z += self2.x
+
+ c = C(x=10)
+
+ assert 40 == getattr(c, "z", None)
+
+ def test_types(self):
+ """
+ Sets the `Attribute.type` attr from type argument.
+ """
+
+ @attr.s
+ class C(object):
+ x = attr.ib(type=int)
+ y = attr.ib(type=str)
+ z = attr.ib()
+
+ assert int is fields(C).x.type
+ 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):
+ x = attr.ib()
+
+ x = getattr(C, "x", None)
+
+ assert not isinstance(x, _CountingAttr)
+
+ def test_factory_sugar(self):
+ """
+ Passing factory=f is syntactic sugar for passing default=Factory(f).
+ """
+
+ @attr.s
+ class C(object):
+ x = attr.ib(factory=list)
+
+ assert Factory(list) == attr.fields(C).x.default
+
+ def test_sugar_factory_mutex(self):
+ """
+ Passing both default and factory raises ValueError.
+ """
+ with pytest.raises(ValueError, match="mutually exclusive"):
+
+ @attr.s
+ class C(object):
+ x = attr.ib(factory=list, default=Factory(list))
+
+ def test_sugar_callable(self):
+ """
+ Factory has to be a callable to prevent people from passing Factory
+ into it.
+ """
+ with pytest.raises(ValueError, match="must be a callable"):
+
+ @attr.s
+ class C(object):
+ x = attr.ib(factory=Factory(list))
+
+ def test_inherited_does_not_affect_hashing_and_equality(self):
+ """
+ Whether or not an Attribute has been inherited doesn't affect how it's
+ hashed and compared.
+ """
+
+ @attr.s
+ class BaseClass(object):
+ x = attr.ib()
+
+ @attr.s
+ class SubClass(BaseClass):
+ pass
+
+ ba = attr.fields(BaseClass)[0]
+ sa = attr.fields(SubClass)[0]
+
+ assert ba == sa
+ assert hash(ba) == hash(sa)
+
+
+class TestKeywordOnlyAttributes(object):
+ """
+ Tests for keyword-only attributes.
+ """
+
+ def test_adds_keyword_only_arguments(self):
+ """
+ Attributes can be added as keyword-only.
+ """
+
+ @attr.s
+ class C(object):
+ a = attr.ib()
+ b = attr.ib(default=2, kw_only=True)
+ c = attr.ib(kw_only=True)
+ d = attr.ib(default=attr.Factory(lambda: 4), kw_only=True)
+
+ c = C(1, c=3)
+
+ assert c.a == 1
+ assert c.b == 2
+ assert c.c == 3
+ assert c.d == 4
+
+ def test_ignores_kw_only_when_init_is_false(self):
+ """
+ Specifying ``kw_only=True`` when ``init=False`` is essentially a no-op.
+ """
+
+ @attr.s
+ class C(object):
+ x = attr.ib(init=False, default=0, kw_only=True)
+ y = attr.ib()
+
+ c = C(1)
+
+ assert c.x == 0
+ assert c.y == 1
+
+ def test_keyword_only_attributes_presence(self):
+ """
+ Raises `TypeError` when keyword-only arguments are
+ not specified.
+ """
+
+ @attr.s
+ class C(object):
+ 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]
+
+ def test_keyword_only_attributes_unexpected(self):
+ """
+ Raises `TypeError` when unexpected keyword argument passed.
+ """
+
+ @attr.s
+ class C(object):
+ x = attr.ib(kw_only=True)
+
+ with pytest.raises(TypeError) as e:
+ C(x=5, y=10)
+
+ assert "got an unexpected keyword argument 'y'" in e.value.args[0]
+
+ def test_keyword_only_attributes_can_come_in_any_order(self):
+ """
+ Mandatory vs non-mandatory attr order only matters when they are part
+ of the __init__ signature and when they aren't kw_only (which are
+ moved to the end and can be mandatory or non-mandatory in any order,
+ as they will be specified as keyword args anyway).
+ """
+
+ @attr.s
+ class C(object):
+ a = attr.ib(kw_only=True)
+ b = attr.ib(kw_only=True, default="b")
+ c = attr.ib(kw_only=True)
+ d = attr.ib()
+ e = attr.ib(default="e")
+ f = attr.ib(kw_only=True)
+ g = attr.ib(kw_only=True, default="g")
+ h = attr.ib(kw_only=True)
+ i = attr.ib(init=False)
+
+ c = C("d", a="a", c="c", f="f", h="h")
+
+ assert c.a == "a"
+ assert c.b == "b"
+ assert c.c == "c"
+ assert c.d == "d"
+ assert c.e == "e"
+ assert c.f == "f"
+ assert c.g == "g"
+ assert c.h == "h"
+
+ def test_keyword_only_attributes_allow_subclassing(self):
+ """
+ Subclass can define keyword-only attributed without defaults,
+ when the base class has attributes with defaults.
+ """
+
+ @attr.s
+ class Base(object):
+ x = attr.ib(default=0)
+
+ @attr.s
+ class C(Base):
+ y = attr.ib(kw_only=True)
+
+ c = C(y=1)
+
+ assert c.x == 0
+ assert c.y == 1
+
+ def test_keyword_only_class_level(self):
+ """
+ `kw_only` can be provided at the attr.s level, converting all
+ attributes to `kw_only.`
+ """
+
+ @attr.s(kw_only=True)
+ class C(object):
+ x = attr.ib()
+ y = attr.ib(kw_only=True)
+
+ with pytest.raises(TypeError):
+ C(0, y=1)
+
+ c = C(x=0, y=1)
+
+ assert c.x == 0
+ assert c.y == 1
+
+ def test_keyword_only_class_level_subclassing(self):
+ """
+ Subclass `kw_only` propagates to attrs inherited from the base,
+ allowing non-default following default.
+ """
+
+ @attr.s
+ class Base(object):
+ x = attr.ib(default=0)
+
+ @attr.s(kw_only=True)
+ class C(Base):
+ y = attr.ib()
+
+ with pytest.raises(TypeError):
+ C(1)
+
+ c = C(x=0, y=1)
+
+ assert c.x == 0
+ assert c.y == 1
+
+ def test_init_false_attribute_after_keyword_attribute(self):
+ """
+ A positional attribute cannot follow a `kw_only` attribute,
+ but an `init=False` attribute can because it won't appear
+ in `__init__`
+ """
+
+ @attr.s
+ class KwArgBeforeInitFalse(object):
+ kwarg = attr.ib(kw_only=True)
+ non_init_function_default = attr.ib(init=False)
+ non_init_keyword_default = attr.ib(
+ init=False, default="default-by-keyword"
+ )
+
+ @non_init_function_default.default
+ def _init_to_init(self):
+ return self.kwarg + "b"
+
+ c = KwArgBeforeInitFalse(kwarg="a")
+
+ assert c.kwarg == "a"
+ assert c.non_init_function_default == "ab"
+ assert c.non_init_keyword_default == "default-by-keyword"
+
+ def test_init_false_attribute_after_keyword_attribute_with_inheritance(
+ self,
+ ):
+ """
+ A positional attribute cannot follow a `kw_only` attribute,
+ but an `init=False` attribute can because it won't appear
+ in `__init__`. This test checks that we allow this
+ even when the `kw_only` attribute appears in a parent class
+ """
+
+ @attr.s
+ class KwArgBeforeInitFalseParent(object):
+ kwarg = attr.ib(kw_only=True)
+
+ @attr.s
+ class KwArgBeforeInitFalseChild(KwArgBeforeInitFalseParent):
+ non_init_function_default = attr.ib(init=False)
+ non_init_keyword_default = attr.ib(
+ init=False, default="default-by-keyword"
+ )
+
+ @non_init_function_default.default
+ def _init_to_init(self):
+ return self.kwarg + "b"
+
+ c = KwArgBeforeInitFalseChild(kwarg="a")
+
+ assert c.kwarg == "a"
+ assert c.non_init_function_default == "ab"
+ 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):
+ @attr.s
+ class D(object):
+ pass
+
+
+class TestMakeClass(object):
+ """
+ Tests for `make_class`.
+ """
+
+ @pytest.mark.parametrize("ls", [list, tuple])
+ def test_simple(self, ls):
+ """
+ Passing a list of strings creates attributes with default args.
+ """
+ C1 = make_class("C1", ls(["a", "b"]))
+
+ @attr.s
+ class C2(object):
+ a = attr.ib()
+ b = attr.ib()
+
+ assert C1.__attrs_attrs__ == C2.__attrs_attrs__
+
+ def test_dict(self):
+ """
+ Passing a dict of name: _CountingAttr creates an equivalent class.
+ """
+ C1 = make_class(
+ "C1", {"a": attr.ib(default=42), "b": attr.ib(default=None)}
+ )
+
+ @attr.s
+ class C2(object):
+ a = attr.ib(default=42)
+ b = attr.ib(default=None)
+
+ assert C1.__attrs_attrs__ == C2.__attrs_attrs__
+
+ def test_attr_args(self):
+ """
+ attributes_arguments are passed to attributes
+ """
+ C = make_class("C", ["x"], repr=False)
+
+ assert repr(C(1)).startswith("<tests.test_make.C object at 0x")
+
+ def test_catches_wrong_attrs_type(self):
+ """
+ Raise `TypeError` if an invalid type for attrs is passed.
+ """
+ with pytest.raises(TypeError) as e:
+ make_class("C", object())
+
+ assert ("attrs argument must be a dict or a list.",) == e.value.args
+
+ def test_bases(self):
+ """
+ Parameter bases default to (object,) and subclasses correctly
+ """
+
+ class D(object):
+ pass
+
+ cls = make_class("C", {})
+
+ assert cls.__mro__[-1] == object
+
+ cls = make_class("C", {}, bases=(D,))
+
+ assert D in cls.__mro__
+ assert isinstance(cls(), D)
+
+ @pytest.mark.parametrize("slots", [True, False])
+ def test_clean_class(self, slots):
+ """
+ Attribute definitions do not appear on the class body.
+ """
+ C = make_class("C", ["x"], slots=slots)
+
+ x = getattr(C, "x", None)
+
+ assert not isinstance(x, _CountingAttr)
+
+ def test_missing_sys_getframe(self, monkeypatch):
+ """
+ `make_class()` does not fail when `sys._getframe()` is not available.
+ """
+ monkeypatch.delattr(sys, "_getframe")
+ C = make_class("C", ["x"])
+
+ assert 1 == len(C.__attrs_attrs__)
+
+ def test_make_class_ordered(self):
+ """
+ If `make_class()` is passed ordered attrs, their order is respected
+ instead of the counter.
+ """
+ b = attr.ib(default=2)
+ a = attr.ib(default=1)
+
+ C = attr.make_class("C", ordered_dict([("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.
+
+ https://github.com/python-attrs/attrs/issues/756
+ https://bugs.python.org/issue33188
+ """
+ from types import new_class
+ from typing import Generic, TypeVar
+
+ MyTypeVar = TypeVar("MyTypeVar")
+ MyParent = new_class("MyParent", (Generic[MyTypeVar],), {})
+
+ attr.make_class("test", {"id": attr.ib(type=str)}, (MyParent[int],))
+
+
+class TestFields(object):
+ """
+ Tests for `fields`.
+ """
+
+ @given(simple_classes())
+ def test_instance(self, C):
+ """
+ Raises `TypeError` on non-classes.
+ """
+ with pytest.raises(TypeError) as e:
+ fields(C())
+
+ assert "Passed object must be a class." == e.value.args[0]
+
+ def test_handler_non_attrs_class(self):
+ """
+ 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)
+ ) == e.value.args[0]
+
+ @given(simple_classes())
+ def test_fields(self, C):
+ """
+ Returns a list of `Attribute`a.
+ """
+ assert all(isinstance(a, Attribute) for a in fields(C))
+
+ @given(simple_classes())
+ def test_fields_properties(self, C):
+ """
+ Fields returns a tuple with properties.
+ """
+ for attribute in fields(C):
+ assert getattr(fields(C), attribute.name) is attribute
+
+
+class TestFieldsDict(object):
+ """
+ Tests for `fields_dict`.
+ """
+
+ @given(simple_classes())
+ def test_instance(self, C):
+ """
+ Raises `TypeError` on non-classes.
+ """
+ with pytest.raises(TypeError) as e:
+ fields_dict(C())
+
+ assert "Passed object must be a class." == e.value.args[0]
+
+ def test_handler_non_attrs_class(self):
+ """
+ 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)
+ ) == e.value.args[0]
+
+ @given(simple_classes())
+ def test_fields_dict(self, C):
+ """
+ Returns an ordered dict of ``{attribute_name: Attribute}``.
+ """
+ d = fields_dict(C)
+
+ assert isinstance(d, ordered_dict)
+ assert list(fields(C)) == list(d.values())
+ assert [a.name for a in fields(C)] == [field_name for field_name in d]
+
+
+class TestConverter(object):
+ """
+ Tests for attribute conversion.
+ """
+
+ def test_convert(self):
+ """
+ Return value of converter is used as the attribute's value.
+ """
+ C = make_class(
+ "C", {"x": attr.ib(converter=lambda v: v + 1), "y": attr.ib()}
+ )
+ c = C(1, 2)
+
+ assert c.x == 2
+ assert c.y == 2
+
+ @given(integers(), booleans())
+ def test_convert_property(self, val, init):
+ """
+ Property tests for attributes using converter.
+ """
+ C = make_class(
+ "C",
+ {
+ "y": attr.ib(),
+ "x": attr.ib(
+ init=init, default=val, converter=lambda v: v + 1
+ ),
+ },
+ )
+ c = C(2)
+
+ assert c.x == val + 1
+ assert c.y == 2
+
+ @given(integers(), booleans())
+ def test_converter_factory_property(self, val, init):
+ """
+ Property tests for attributes with converter, and a factory default.
+ """
+ C = make_class(
+ "C",
+ ordered_dict(
+ [
+ ("y", attr.ib()),
+ (
+ "x",
+ attr.ib(
+ init=init,
+ default=Factory(lambda: val),
+ converter=lambda v: v + 1,
+ ),
+ ),
+ ]
+ ),
+ )
+ c = C(2)
+
+ assert c.x == val + 1
+ assert c.y == 2
+
+ def test_factory_takes_self(self):
+ """
+ If takes_self on factories is True, self is passed.
+ """
+ C = make_class(
+ "C",
+ {
+ "x": attr.ib(
+ default=Factory((lambda self: self), takes_self=True)
+ )
+ },
+ )
+
+ i = C()
+
+ assert i is i.x
+
+ def test_factory_hashable(self):
+ """
+ Factory is hashable.
+ """
+ assert hash(Factory(None, False)) == hash(Factory(None, False))
+
+ def test_convert_before_validate(self):
+ """
+ Validation happens after conversion.
+ """
+
+ def validator(inst, attr, val):
+ raise RuntimeError("foo")
+
+ C = make_class(
+ "C",
+ {
+ "x": attr.ib(validator=validator, converter=lambda v: 1 / 0),
+ "y": attr.ib(),
+ },
+ )
+ with pytest.raises(ZeroDivisionError):
+ C(1, 2)
+
+ def test_frozen(self):
+ """
+ Converters circumvent immutability.
+ """
+ C = make_class(
+ "C", {"x": attr.ib(converter=lambda v: int(v))}, frozen=True
+ )
+ C("1")
+
+
+class TestValidate(object):
+ """
+ Tests for `validate`.
+ """
+
+ def test_success(self):
+ """
+ If the validator succeeds, nothing gets raised.
+ """
+ C = make_class(
+ "C", {"x": attr.ib(validator=lambda *a: None), "y": attr.ib()}
+ )
+ validate(C(1, 2))
+
+ def test_propagates(self):
+ """
+ The exception of the validator is handed through.
+ """
+
+ def raiser(_, __, value):
+ if value == 42:
+ raise FloatingPointError
+
+ C = make_class("C", {"x": attr.ib(validator=raiser)})
+ i = C(1)
+ i.x = 42
+
+ with pytest.raises(FloatingPointError):
+ validate(i)
+
+ def test_run_validators(self):
+ """
+ Setting `_run_validators` to False prevents validators from running.
+ """
+ _config._run_validators = False
+ obj = object()
+
+ def raiser(_, __, ___):
+ raise Exception(obj)
+
+ C = make_class("C", {"x": attr.ib(validator=raiser)})
+ c = C(1)
+ validate(c)
+ assert 1 == c.x
+ _config._run_validators = True
+
+ with pytest.raises(Exception):
+ validate(c)
+
+ with pytest.raises(Exception) as e:
+ C(1)
+ assert (obj,) == e.value.args
+
+ def test_multiple_validators(self):
+ """
+ If a list is passed as a validator, all of its items are treated as one
+ and must pass.
+ """
+
+ def v1(_, __, value):
+ if value == 23:
+ raise TypeError("omg")
+
+ def v2(_, __, value):
+ if value == 42:
+ raise ValueError("omg")
+
+ C = make_class("C", {"x": attr.ib(validator=[v1, v2])})
+
+ validate(C(1))
+
+ with pytest.raises(TypeError) as e:
+ C(23)
+
+ assert "omg" == e.value.args[0]
+
+ with pytest.raises(ValueError) as e:
+ C(42)
+
+ assert "omg" == e.value.args[0]
+
+ def test_multiple_empty(self):
+ """
+ Empty list/tuple for validator is the same as None.
+ """
+ C1 = make_class("C", {"x": attr.ib(validator=[])})
+ C2 = make_class("C", {"x": attr.ib(validator=None)})
+
+ assert inspect.getsource(C1.__init__) == inspect.getsource(C2.__init__)
+
+
+# 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"))
+)
+
+
+class TestMetadata(object):
+ """
+ Tests for metadata handling.
+ """
+
+ @given(sorted_lists_of_attrs)
+ def test_metadata_present(self, list_of_attrs):
+ """
+ Assert dictionaries are copied and present.
+ """
+ C = make_class("C", dict(zip(gen_attr_names(), list_of_attrs)))
+
+ for hyp_attr, class_attr in zip(list_of_attrs, fields(C)):
+ if hyp_attr.metadata is None:
+ # The default is a singleton empty dict.
+ assert class_attr.metadata is not None
+ assert len(class_attr.metadata) == 0
+ else:
+ assert hyp_attr.metadata == class_attr.metadata
+
+ # Once more, just to assert getting items and iteration.
+ for k in class_attr.metadata:
+ assert hyp_attr.metadata[k] == class_attr.metadata[k]
+ assert hyp_attr.metadata.get(k) == class_attr.metadata.get(
+ k
+ )
+
+ @given(simple_classes(), text())
+ def test_metadata_immutability(self, C, string):
+ """
+ The metadata dict should be best-effort immutable.
+ """
+ for a in fields(C):
+ with pytest.raises(TypeError):
+ a.metadata[string] = string
+ with pytest.raises(AttributeError):
+ a.metadata.update({string: string})
+ with pytest.raises(AttributeError):
+ a.metadata.clear()
+ with pytest.raises(AttributeError):
+ 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.
+ with pytest.raises((TypeError, IndexError)):
+ del a.metadata[k]
+ with pytest.raises(AttributeError):
+ a.metadata.pop(k)
+ with pytest.raises(AttributeError):
+ a.metadata.popitem()
+
+ @given(lists(simple_attrs_without_metadata, min_size=2, max_size=5))
+ def test_empty_metadata_singleton(self, list_of_attrs):
+ """
+ All empty metadata attributes share the same empty metadata dict.
+ """
+ C = make_class("C", dict(zip(gen_attr_names(), list_of_attrs)))
+ for a in fields(C)[1:]:
+ assert a.metadata is fields(C)[0].metadata
+
+ @given(lists(simple_attrs_without_metadata, min_size=2, max_size=5))
+ def test_empty_countingattr_metadata_independent(self, list_of_attrs):
+ """
+ All empty metadata attributes are independent before ``@attr.s``.
+ """
+ for x, y in itertools.combinations(list_of_attrs, 2):
+ assert x.metadata is not y.metadata
+
+ @given(lists(simple_attrs_with_metadata(), min_size=2, max_size=5))
+ def test_not_none_metadata(self, list_of_attrs):
+ """
+ Non-empty metadata attributes exist as fields after ``@attr.s``.
+ """
+ C = make_class("C", dict(zip(gen_attr_names(), list_of_attrs)))
+
+ assert len(fields(C)) > 0
+
+ for cls_a, raw_a in zip(fields(C), list_of_attrs):
+ assert cls_a.metadata != {}
+ assert cls_a.metadata == raw_a.metadata
+
+ def test_metadata(self):
+ """
+ If metadata that is not None is passed, it is used.
+
+ This is necessary for coverage because the previous test is
+ hypothesis-based.
+ """
+ md = {}
+ a = attr.ib(metadata=md)
+
+ assert md is a.metadata
+
+
+class TestClassBuilder(object):
+ """
+ Tests for `_ClassBuilder`.
+ """
+
+ def test_repr_str(self):
+ """
+ Trying to add a `__str__` without having a `__repr__` raises a
+ ValueError.
+ """
+ with pytest.raises(ValueError) as ei:
+ make_class("C", {}, repr=False, str=True)
+
+ assert (
+ "__str__ can only be generated if a __repr__ exists.",
+ ) == ei.value.args
+
+ def test_repr(self):
+ """
+ repr of builder itself makes sense.
+ """
+
+ class C(object):
+ pass
+
+ b = _ClassBuilder(
+ C,
+ None,
+ True,
+ True,
+ False,
+ False,
+ False,
+ False,
+ False,
+ False,
+ True,
+ None,
+ False,
+ None,
+ )
+
+ assert "<_ClassBuilder(cls=C)>" == repr(b)
+
+ def test_returns_self(self):
+ """
+ All methods return the builder for chaining.
+ """
+
+ class C(object):
+ x = attr.ib()
+
+ b = _ClassBuilder(
+ C,
+ None,
+ True,
+ True,
+ False,
+ False,
+ False,
+ False,
+ False,
+ False,
+ True,
+ None,
+ False,
+ None,
+ )
+
+ cls = (
+ b.add_eq()
+ .add_order()
+ .add_hash()
+ .add_init()
+ .add_attrs_init()
+ .add_repr("ns")
+ .add_str()
+ .build_class()
+ )
+
+ assert "ns.C(x=1)" == repr(cls(1))
+
+ @pytest.mark.parametrize(
+ "meth_name",
+ [
+ "__init__",
+ "__hash__",
+ "__repr__",
+ "__str__",
+ "__eq__",
+ "__ne__",
+ "__lt__",
+ "__le__",
+ "__gt__",
+ "__ge__",
+ ],
+ )
+ def test_attaches_meta_dunders(self, meth_name):
+ """
+ Generated methods have correct __module__, __name__, and __qualname__
+ attributes.
+ """
+
+ @attr.s(hash=True, str=True)
+ class C(object):
+ def organic(self):
+ pass
+
+ @attr.s(hash=True, str=True)
+ class D(object):
+ pass
+
+ meth_C = getattr(C, meth_name)
+ meth_D = getattr(D, meth_name)
+
+ 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__
+
+ def test_handles_missing_meta_on_class(self):
+ """
+ If the class hasn't a __module__ or __qualname__, the method hasn't
+ either.
+ """
+
+ class C(object):
+ pass
+
+ b = _ClassBuilder(
+ C,
+ these=None,
+ slots=False,
+ frozen=False,
+ weakref_slot=True,
+ getstate_setstate=False,
+ auto_attribs=False,
+ is_exc=False,
+ kw_only=False,
+ cache_hash=False,
+ collect_by_mro=True,
+ on_setattr=None,
+ has_custom_setattr=False,
+ field_transformer=None,
+ )
+ b._cls = {} # no __module__; no __qualname__
+
+ def fake_meth(self):
+ pass
+
+ fake_meth.__module__ = "42"
+ fake_meth.__qualname__ = "23"
+
+ rv = b._add_method_dunders(fake_meth)
+
+ assert "42" == rv.__module__ == fake_meth.__module__
+ assert "23" == rv.__qualname__ == fake_meth.__qualname__
+
+ def test_weakref_setstate(self):
+ """
+ __weakref__ is not set on in setstate because it's not writable in
+ slotted classes.
+ """
+
+ @attr.s(slots=True)
+ class C(object):
+ __weakref__ = attr.ib(
+ init=False, hash=False, repr=False, eq=False, order=False
+ )
+
+ assert C() == copy.deepcopy(C())
+
+ def test_no_references_to_original(self):
+ """
+ When subclassing a slotted class, there are no stray references to the
+ original class.
+ """
+
+ @attr.s(slots=True)
+ class C(object):
+ pass
+
+ @attr.s(slots=True)
+ class C2(C):
+ pass
+
+ # The original C2 is in a reference cycle, so force a collect:
+ gc.collect()
+
+ assert [C2] == C.__subclasses__()
+
+ def _get_copy_kwargs(include_slots=True):
+ """
+ Generate a list of compatible attr.s arguments for the `copy` tests.
+ """
+ options = ["frozen", "hash", "cache_hash"]
+
+ if include_slots:
+ options.extend(["slots", "weakref_slot"])
+
+ out_kwargs = []
+ for args in itertools.product([True, False], repeat=len(options)):
+ kwargs = dict(zip(options, args))
+
+ kwargs["hash"] = kwargs["hash"] or None
+
+ if kwargs["cache_hash"] and not (
+ kwargs["frozen"] or kwargs["hash"]
+ ):
+ continue
+
+ out_kwargs.append(kwargs)
+
+ return out_kwargs
+
+ @pytest.mark.parametrize("kwargs", _get_copy_kwargs())
+ def test_copy(self, kwargs):
+ """
+ Ensure that an attrs class can be copied successfully.
+ """
+
+ @attr.s(eq=True, **kwargs)
+ class C(object):
+ x = attr.ib()
+
+ a = C(1)
+ b = copy.deepcopy(a)
+
+ assert a == b
+
+ @pytest.mark.parametrize("kwargs", _get_copy_kwargs(include_slots=False))
+ def test_copy_custom_setstate(self, kwargs):
+ """
+ Ensure that non-slots classes respect a custom __setstate__.
+ """
+
+ @attr.s(eq=True, **kwargs)
+ class C(object):
+ x = attr.ib()
+
+ def __getstate__(self):
+ return self.__dict__
+
+ def __setstate__(self, state):
+ state["x"] *= 5
+ self.__dict__.update(state)
+
+ expected = C(25)
+ actual = copy.copy(C(5))
+
+ assert actual == expected
+
+
+class TestMakeOrder:
+ """
+ Tests for _make_order().
+ """
+
+ def test_subclasses_cannot_be_compared(self):
+ """
+ Calling comparison methods on subclasses raises a TypeError.
+
+ We use the actual operation so we get an error raised on Python 3.
+ """
+
+ @attr.s
+ class A(object):
+ a = attr.ib()
+
+ @attr.s
+ class B(A):
+ pass
+
+ a = A(42)
+ b = B(42)
+
+ assert a <= a
+ assert a >= a
+ assert not a < a
+ assert not a > a
+
+ assert (
+ NotImplemented
+ == a.__lt__(b)
+ == a.__le__(b)
+ == a.__gt__(b)
+ == 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
+
+
+class TestDetermineAttrsEqOrder(object):
+ def test_default(self):
+ """
+ If all are set to None, set both eq and order to the passed default.
+ """
+ assert (42, 42) == _determine_attrs_eq_order(None, None, None, 42)
+
+ @pytest.mark.parametrize("eq", [True, False])
+ def test_order_mirrors_eq_by_default(self, eq):
+ """
+ If order is None, it mirrors eq.
+ """
+ assert (eq, eq) == _determine_attrs_eq_order(None, eq, None, True)
+
+ def test_order_without_eq(self):
+ """
+ eq=False, order=True raises a meaningful ValueError.
+ """
+ with pytest.raises(
+ ValueError, match="`order` can only be True if `eq` is True too."
+ ):
+ _determine_attrs_eq_order(None, False, True, True)
+
+ @given(cmp=booleans(), eq=optional_bool, order=optional_bool)
+ def test_mix(self, cmp, eq, order):
+ """
+ If cmp is not None, eq and order must be None and vice versa.
+ """
+ assume(eq is not None or order is not None)
+
+ with pytest.raises(
+ ValueError, match="Don't mix `cmp` with `eq' and `order`."
+ ):
+ _determine_attrs_eq_order(cmp, eq, order, True)
+
+
+class TestDetermineAttribEqOrder(object):
+ def test_default(self):
+ """
+ If all are set to None, set both eq and order to the passed default.
+ """
+ assert (42, None, 42, None) == _determine_attrib_eq_order(
+ None, None, None, 42
+ )
+
+ def test_eq_callable_order_boolean(self):
+ """
+ eq=callable or order=callable need to transformed into eq/eq_key
+ or order/order_key.
+ """
+ assert (True, str.lower, False, None) == _determine_attrib_eq_order(
+ None, str.lower, False, True
+ )
+
+ def test_eq_callable_order_callable(self):
+ """
+ eq=callable or order=callable need to transformed into eq/eq_key
+ or order/order_key.
+ """
+ assert (True, str.lower, True, abs) == _determine_attrib_eq_order(
+ None, str.lower, abs, True
+ )
+
+ def test_eq_boolean_order_callable(self):
+ """
+ eq=callable or order=callable need to transformed into eq/eq_key
+ or order/order_key.
+ """
+ assert (True, None, True, str.lower) == _determine_attrib_eq_order(
+ None, True, str.lower, True
+ )
+
+ @pytest.mark.parametrize("eq", [True, False])
+ def test_order_mirrors_eq_by_default(self, eq):
+ """
+ If order is None, it mirrors eq.
+ """
+ assert (eq, None, eq, None) == _determine_attrib_eq_order(
+ None, eq, None, True
+ )
+
+ def test_order_without_eq(self):
+ """
+ eq=False, order=True raises a meaningful ValueError.
+ """
+ with pytest.raises(
+ ValueError, match="`order` can only be True if `eq` is True too."
+ ):
+ _determine_attrib_eq_order(None, False, True, True)
+
+ @given(cmp=booleans(), eq=optional_bool, order=optional_bool)
+ def test_mix(self, cmp, eq, order):
+ """
+ If cmp is not None, eq and order must be None and vice versa.
+ """
+ assume(eq is not None or order is not None)
+
+ with pytest.raises(
+ ValueError, match="Don't mix `cmp` with `eq' and `order`."
+ ):
+ _determine_attrib_eq_order(cmp, eq, order, True)
+
+
+class TestDocs:
+ @pytest.mark.parametrize(
+ "meth_name",
+ [
+ "__init__",
+ "__repr__",
+ "__eq__",
+ "__ne__",
+ "__lt__",
+ "__le__",
+ "__gt__",
+ "__ge__",
+ ],
+ )
+ def test_docs(self, meth_name):
+ """
+ Tests the presence and correctness of the documentation
+ for the generated methods
+ """
+
+ @attr.s
+ class A(object):
+ pass
+
+ if hasattr(A, "__qualname__"):
+ method = getattr(A, meth_name)
+ expected = "Method generated by attrs for class {}.".format(
+ 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):
+ pass
+
+
+class BareSlottedC(object):
+ __slots__ = ()
+
+
+@pytest.mark.skipif(PY2, reason="Auto-detection is Python 3-only.")
+class TestAutoDetect:
+ @pytest.mark.parametrize("C", (BareC, BareSlottedC))
+ def test_determine_detects_non_presence_correctly(self, C):
+ """
+ On an empty class, nothing should be detected.
+ """
+ assert True is _determine_whether_to_implement(
+ C, None, True, ("__init__",)
+ )
+ assert True is _determine_whether_to_implement(
+ C, None, True, ("__repr__",)
+ )
+ assert True is _determine_whether_to_implement(
+ C, None, True, ("__eq__", "__ne__")
+ )
+ assert True is _determine_whether_to_implement(
+ 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,
+ hash=None, eq=True, order=True.
+ """
+
+ @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+ class C(object):
+ x = attr.ib()
+
+ i = C(1)
+ o = object()
+
+ assert i.__init__ is not o.__init__
+ assert i.__repr__ is not o.__repr__
+ assert i.__eq__ is not o.__eq__
+ assert i.__ne__ is not o.__ne__
+ assert i.__le__ is not o.__le__
+ assert i.__lt__ is not o.__lt__
+ 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):
+ x = attr.ib()
+
+ def __init__(self):
+ object.__setattr__(self, "x", 42)
+
+ 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):
+ x = attr.ib()
+
+ def __repr__(self):
+ return "hi"
+
+ assert "hi" == repr(C(42))
+
+ @pytest.mark.parametrize("slots", [True, False])
+ @pytest.mark.parametrize("frozen", [True, False])
+ 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):
+ x = attr.ib()
+
+ def __hash__(self):
+ return 0xC0FFEE
+
+ 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):
+ x = attr.ib()
+
+ def __eq__(self, o):
+ raise ValueError("worked")
+
+ with pytest.raises(ValueError, match="worked"):
+ C(1) == C(1)
+
+ @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+ class D(object):
+ x = attr.ib()
+
+ def __ne__(self, o):
+ raise ValueError("worked")
+
+ 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,
+ don't write one.
+
+ It's surprisingly difficult to test this programmatically, so we do it
+ by hand.
+ """
+
+ def assert_not_set(cls, ex, meth_name):
+ __tracebackhide__ = True
+
+ a = getattr(cls, meth_name)
+ if meth_name == ex:
+ assert a == 42
+ else:
+ assert a is getattr(object, meth_name)
+
+ def assert_none_set(cls, ex):
+ __tracebackhide__ = True
+
+ for m in ("le", "lt", "ge", "gt"):
+ assert_not_set(cls, ex, "__" + m + "__")
+
+ @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+ class LE(object):
+ __le__ = 42
+
+ @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+ class LT(object):
+ __lt__ = 42
+
+ @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+ class GE(object):
+ __ge__ = 42
+
+ @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+ class GT(object):
+ __gt__ = 42
+
+ assert_none_set(LE, "__le__")
+ assert_none_set(LT, "__lt__")
+ 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):
+ x = attr.ib()
+
+ def __init__(self):
+ pytest.fail("should not be called")
+
+ 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):
+ x = attr.ib()
+
+ def __repr__(self):
+ pytest.fail("should not be called")
+
+ 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):
+ x = attr.ib()
+
+ def __hash__(self):
+ pytest.fail("should not be called")
+
+ 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):
+ x = attr.ib()
+
+ def __eq__(self, o):
+ pytest.fail("should not be called")
+
+ def __ne__(self, o):
+ pytest.fail("should not be called")
+
+ assert C(1) == C(1)
+
+ @pytest.mark.parametrize("slots", [True, False])
+ @pytest.mark.parametrize("frozen", [True, False])
+ @pytest.mark.parametrize(
+ "eq,order,cmp",
+ [
+ (True, None, None),
+ (True, True, None),
+ (None, True, None),
+ (None, None, True),
+ ],
+ )
+ def test_override_order(self, slots, frozen, eq, order, cmp):
+ """
+ If order=True is passed, ignore __le__, __lt__, __gt__, __ge__.
+
+ eq=True and cmp=True both imply order=True so test it too.
+ """
+
+ def meth(self, o):
+ pytest.fail("should not be called")
+
+ @attr.s(
+ cmp=cmp,
+ order=order,
+ eq=eq,
+ auto_detect=True,
+ slots=slots,
+ frozen=frozen,
+ )
+ class C(object):
+ x = attr.ib()
+ __le__ = __lt__ = __gt__ = __ge__ = meth
+
+ assert C(1) < C(2)
+ assert C(1) <= C(2)
+ 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):
+ """
+ functools.total_ordering works as expected if an order method and an eq
+ method are detected.
+
+ Ensure the order doesn't matter.
+ """
+
+ class C(object):
+ x = attr.ib()
+ own_eq_called = attr.ib(default=False)
+ own_le_called = attr.ib(default=False)
+
+ def __eq__(self, o):
+ self.own_eq_called = True
+ return self.x == o.x
+
+ def __le__(self, o):
+ self.own_le_called = True
+ return self.x <= o.x
+
+ if first:
+ C = functools.total_ordering(
+ attr.s(auto_detect=True, slots=slots)(C)
+ )
+ else:
+ C = attr.s(auto_detect=True, slots=slots)(
+ functools.total_ordering(C)
+ )
+
+ c1, c2 = C(1), C(2)
+
+ assert c1 < c2
+ assert c1.own_le_called
+
+ c1, c2 = C(1), C(2)
+
+ assert c2 > c1
+ assert c2.own_le_called
+
+ c1, c2 = C(1), C(2)
+
+ assert c2 != c1
+ assert c1 == c1
+
+ 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):
+ def __getstate__(self):
+ return ("hi",)
+
+ assert None is getattr(C(), "__setstate__", None)
+
+ @attr.s(slots=slots, auto_detect=True)
+ class C(object):
+ called = attr.ib(False)
+
+ def __setstate__(self, state):
+ self.called = True
+
+ i = C()
+
+ assert False is i.called
+
+ i.__setstate__(())
+
+ assert True is i.called
+ assert None is getattr(C(), "__getstate__", None)
+
+ @pytest.mark.skipif(PY310, reason="Pre-3.10 only.")
+ def test_match_args_pre_310(self):
+ """
+ __match_args__ is not created on Python versions older than 3.10.
+ """
+
+ @attr.s
+ class C(object):
+ 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):
+ """
+ Tests for match_args and __match_args__ generation.
+ """
+
+ def test_match_args(self):
+ """
+ __match_args__ is created by default on Python 3.10.
+ """
+
+ @attr.define
+ class C:
+ a = attr.field()
+
+ assert ("a",) == C.__match_args__
+
+ def test_explicit_match_args(self):
+ """
+ A custom __match_args__ set is not overwritten.
+ """
+
+ ma = ()
+
+ @attr.define
+ class C:
+ a = attr.field()
+ __match_args__ = ma
+
+ assert C(42).__match_args__ is ma
+
+ @pytest.mark.parametrize("match_args", [True, False])
+ def test_match_args_attr_set(self, match_args):
+ """
+ __match_args__ is set depending on match_args.
+ """
+
+ @attr.define(match_args=match_args)
+ class C:
+ a = attr.field()
+
+ if match_args:
+ assert hasattr(C, "__match_args__")
+ else:
+ assert not hasattr(C, "__match_args__")
+
+ def test_match_args_kw_only(self):
+ """
+ kw_only classes don't generate __match_args__.
+ kw_only fields are not included in __match_args__.
+ """
+
+ @attr.define
+ class C:
+ a = attr.field(kw_only=True)
+ b = attr.field()
+
+ assert C.__match_args__ == ("b",)
+
+ @attr.define(kw_only=True)
+ class C:
+ a = attr.field()
+ b = attr.field()
+
+ assert C.__match_args__ == ()
+
+ def test_match_args_argument(self):
+ """
+ match_args being False with inheritance.
+ """
+
+ @attr.define(match_args=False)
+ class X:
+ a = attr.field()
+
+ assert "__match_args__" not in X.__dict__
+
+ @attr.define(match_args=False)
+ class Y:
+ a = attr.field()
+ __match_args__ = ("b",)
+
+ assert Y.__match_args__ == ("b",)
+
+ @attr.define(match_args=False)
+ class Z(Y):
+ z = attr.field()
+
+ assert Z.__match_args__ == ("b",)
+
+ @attr.define
+ class A:
+ a = attr.field()
+ z = attr.field()
+
+ @attr.define(match_args=False)
+ class B(A):
+ b = attr.field()
+
+ assert B.__match_args__ == ("a", "z")
+
+ def test_make_class(self):
+ """
+ match_args generation with make_class.
+ """
+
+ C1 = make_class("C1", ["a", "b"])
+ assert ("a", "b") == C1.__match_args__
+
+ C1 = make_class("C1", ["a", "b"], match_args=False)
+ assert not hasattr(C1, "__match_args__")
+
+ C1 = make_class("C1", ["a", "b"], kw_only=True)
+ assert () == C1.__match_args__
+
+ C1 = make_class("C1", {"a": attr.ib(kw_only=True), "b": attr.ib()})
+ assert ("b",) == C1.__match_args__
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_mypy.yml b/testing/web-platform/tests/tools/third_party/attrs/tests/test_mypy.yml
new file mode 100644
index 0000000000..ca17b0a662
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_mypy.yml
@@ -0,0 +1,1395 @@
+- case: attr_s_with_type_argument
+ parametrized:
+ - 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(1)
+ C(a=1)
+ C(a="hi") # E: Argument "a" to "C" has incompatible type "str"; expected "int"
+- case: attr_s_with_type_annotations
+ main : |
+ import attr
+ @attr.s
+ class C:
+ a: int = attr.ib()
+ C() # E: Missing positional argument "a" in call to "C"
+ C(1)
+ C(a=1)
+ C(a="hi") # E: Argument "a" to "C" has incompatible type "str"; expected "int"
+
+- case: testAttrsSimple
+ main: |
+ import attr
+ @attr.s
+ class A:
+ a = attr.ib()
+ _b = attr.ib()
+ c = attr.ib(18)
+ _d = attr.ib(validator=None, default=18)
+ E = 18
+
+ def foo(self):
+ return self.a
+ reveal_type(A) # N: Revealed type is "def (a: Any, b: Any, c: Any =, d: Any =) -> main.A"
+ 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"
+
+- case: testAttrsAnnotated
+ main: |
+ import attr
+ from typing import List, ClassVar
+ @attr.s
+ class A:
+ a: int = attr.ib()
+ _b: List[int] = attr.ib()
+ c: str = attr.ib('18')
+ _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"
+ 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"
+
+- case: testAttrsPython2Annotations
+ main: |
+ import attr
+ from typing import List, ClassVar
+ @attr.s
+ class A:
+ a = attr.ib() # type: int
+ _b = attr.ib() # type: List[int]
+ c = attr.ib('18') # type: str
+ _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"
+ 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"
+
+- case: testAttrsAutoAttribs
+ main: |
+ import attr
+ from typing import List, ClassVar
+ @attr.s(auto_attribs=True)
+ class A:
+ a: int
+ _b: List[int]
+ c: str = '18'
+ _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"
+ 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"
+
+- case: testAttrsUntypedNoUntypedDefs
+ mypy_config: |
+ 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"
+ E = 18
+
+- case: testAttrsWrongReturnValue
+ main: |
+ import attr
+ @attr.s
+ class A:
+ x: int = attr.ib(8)
+ def foo(self) -> str:
+ return self.x # E: Incompatible return value type (got "int", expected "str")
+ @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")
+ @attr.dataclass
+ class C:
+ x: int = 8
+ def foo(self) -> str:
+ return self.x # E: Incompatible return value type (got "int", expected "str")
+ @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")
+
+- case: testAttrsSeriousNames
+ main: |
+ from attr import attrib, attrs
+ from typing import List
+ @attrs(init=True)
+ class A:
+ a = attrib()
+ _b: List[int] = attrib()
+ 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"
+ 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"
+
+- case: testAttrsDefaultErrors
+ main: |
+ import attr
+ @attr.s
+ class A:
+ x = attr.ib(default=17)
+ y = attr.ib() # E: Non-default attributes not allowed after default attributes.
+ @attr.s(auto_attribs=True)
+ class B:
+ x: int = 17
+ y: int # E: Non-default attributes not allowed after default attributes.
+ @attr.s(auto_attribs=True)
+ class C:
+ x: int = attr.ib(default=17)
+ y: int # E: Non-default attributes not allowed after default attributes.
+ @attr.s
+ class D:
+ x = attr.ib()
+ y = attr.ib() # E: Non-default attributes not allowed after default attributes.
+
+ @x.default
+ def foo(self):
+ return 17
+
+- case: testAttrsNotBooleans
+ main: |
+ import attr
+ x = True
+ @attr.s(cmp=x) # E: "cmp" argument must be True or False.
+ class A:
+ a = attr.ib(init=x) # E: "init" argument must be True or False.
+
+- case: testAttrsInitFalse
+ main: |
+ from attr import attrib, attrs
+ @attrs(auto_attribs=True, init=False)
+ class A:
+ a: int
+ _b: int
+ c: int = 18
+ _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"
+
+- case: testAttrsInitAttribFalse
+ main: |
+ from attr import attrib, attrs
+ @attrs
+ class A:
+ a = attrib(init=False)
+ b = attrib()
+ reveal_type(A) # N: Revealed type is "def (b: Any) -> main.A"
+
+- case: testAttrsCmpTrue
+ 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"
+
+ A(1) < A(2)
+ A(1) <= A(2)
+ A(1) > A(2)
+ A(1) >= A(2)
+ 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
+ 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)
+ 1 != A(1)
+
+- case: testAttrsEqFalse
+ main: |
+ from attr import attrib, attrs
+ @attrs(auto_attribs=True, eq=False)
+ class A:
+ a: int
+ reveal_type(A) # N: Revealed type is "def (a: builtins.int) -> main.A"
+ 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)
+ 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
+ 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)
+ 1 != A(1)
+
+- case: testAttrsOrderFalse
+ main: |
+ from attr import attrib, attrs
+ @attrs(auto_attribs=True, order=False)
+ class A:
+ 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)
+ 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
+ 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)
+ 1 != A(1)
+
+- case: testAttrsCmpEqOrderValues
+ main: |
+ from attr import attrib, attrs
+ @attrs(cmp=True)
+ class DeprecatedTrue:
+ ...
+
+ @attrs(cmp=False)
+ class DeprecatedFalse:
+ ...
+
+ @attrs(cmp=False, eq=True) # E: Don't mix "cmp" with "eq" and "order"
+ class Mixed:
+ ...
+
+ @attrs(order=True, eq=False) # E: eq must be True if order is True
+ class Confused:
+ ...
+
+
+- case: testAttrsInheritance
+ main: |
+ import attr
+ @attr.s
+ class A:
+ a: int = attr.ib()
+ @attr.s
+ class B:
+ b: str = attr.ib()
+ @attr.s
+ class C(A, B):
+ c: bool = attr.ib()
+ reveal_type(C) # N: Revealed type is "def (a: builtins.int, b: builtins.str, c: builtins.bool) -> main.C"
+
+- case: testAttrsNestedInClasses
+ main: |
+ import attr
+ @attr.s
+ class C:
+ y = attr.ib()
+ @attr.s
+ class D:
+ x: int = attr.ib()
+ reveal_type(C) # N: Revealed type is "def (y: Any) -> main.C"
+ reveal_type(C.D) # N: Revealed type is "def (x: builtins.int) -> main.C.D"
+
+- case: testAttrsInheritanceOverride
+ main: |
+ import attr
+
+ @attr.s
+ class A:
+ a: int = attr.ib()
+ x: int = attr.ib()
+
+ @attr.s
+ class B(A):
+ b: str = attr.ib()
+ x: int = attr.ib(default=22)
+
+ @attr.s
+ class C(B):
+ c: bool = attr.ib() # No error here because the x below overwrites the x above.
+ x: int = attr.ib()
+
+ reveal_type(A) # N: Revealed type is "def (a: builtins.int, x: builtins.int) -> main.A"
+ reveal_type(B) # N: Revealed type is "def (a: builtins.int, b: builtins.str, x: builtins.int =) -> main.B"
+ reveal_type(C) # N: Revealed type is "def (a: builtins.int, b: builtins.str, c: builtins.bool, x: builtins.int) -> main.C"
+
+- case: testAttrsTypeEquals
+ main: |
+ import attr
+
+ @attr.s
+ class A:
+ a = attr.ib(type=int)
+ b = attr.ib(18, type=int)
+ reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.int =) -> main.A"
+
+- case: testAttrsFrozen
+ main: |
+ import attr
+
+ @attr.s(frozen=True)
+ class A:
+ a = attr.ib()
+
+ a = A(5)
+ a.a = 16 # E: Property "a" defined in "A" is read-only
+- case: testAttrsNextGenFrozen
+ main: |
+ from attr import frozen, field
+
+ @frozen
+ class A:
+ a = field()
+
+ a = A(5)
+ a.a = 16 # E: Property "a" defined in "A" is read-only
+
+- case: testAttrsNextGenDetect
+ main: |
+ from attr import define, field
+
+ @define
+ class A:
+ a = field()
+
+ @define
+ class B:
+ a: int
+
+ @define
+ class C:
+ a: int = field()
+ b = field()
+
+ @define
+ class D:
+ 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"
+ reveal_type(D) # N: Revealed type is "def (b: Any) -> main.D"
+
+- case: testAttrsDataClass
+ main: |
+ import attr
+ from typing import List, ClassVar
+ @attr.dataclass
+ class A:
+ a: int
+ _b: List[str]
+ c: str = '18'
+ _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.str], c: builtins.str =, d: builtins.int =) -> main.A"
+ A(1, ['2'])
+
+- case: testAttrsTypeAlias
+ main: |
+ from typing import List
+ import attr
+ Alias = List[int]
+ @attr.s(auto_attribs=True)
+ class A:
+ Alias2 = List[str]
+ x: Alias
+ y: Alias2 = attr.ib()
+ reveal_type(A) # N: Revealed type is "def (x: builtins.list[builtins.int], y: builtins.list[builtins.str]) -> main.A"
+
+- case: testAttrsGeneric
+ main: |
+ from typing import TypeVar, Generic, List
+ import attr
+ T = TypeVar('T')
+ @attr.s(auto_attribs=True)
+ class A(Generic[T]):
+ x: List[T]
+ y: T = attr.ib()
+ def foo(self) -> List[T]:
+ return [self.y]
+ 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]"
+ 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"
+
+
+- case: testAttrsUntypedGenericInheritance
+ main: |
+ from typing import Generic, TypeVar
+ import attr
+
+ T = TypeVar("T")
+
+ @attr.s(auto_attribs=True)
+ class Base(Generic[T]):
+ attr: T
+
+ @attr.s(auto_attribs=True)
+ class Sub(Base):
+ pass
+
+ sub = Sub(attr=1)
+ reveal_type(sub) # N: Revealed type is "main.Sub"
+ reveal_type(sub.attr) # N: Revealed type is "Any"
+ skip: True # Need to investigate why this is broken
+
+- case: testAttrsGenericInheritance
+ main: |
+ from typing import Generic, TypeVar
+ import attr
+
+ S = TypeVar("S")
+ T = TypeVar("T")
+
+ @attr.s(auto_attribs=True)
+ class Base(Generic[T]):
+ attr: T
+
+ @attr.s(auto_attribs=True)
+ class Sub(Base[S]):
+ 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*"
+
+ 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*"
+
+- case: testAttrsGenericInheritance2
+ main: |
+ from typing import Generic, TypeVar
+ import attr
+
+ T1 = TypeVar("T1")
+ T2 = TypeVar("T2")
+ T3 = TypeVar("T3")
+
+ @attr.s(auto_attribs=True)
+ class Base(Generic[T1, T2, T3]):
+ one: T1
+ two: T2
+ three: T3
+
+ @attr.s(auto_attribs=True)
+ class Sub(Base[int, str, float]):
+ pass
+
+ sub = Sub(one=1, two='ok', three=3.14)
+ reveal_type(sub) # N: Revealed type is "main.Sub"
+ reveal_type(sub.one) # N: Revealed type is "builtins.int*"
+ reveal_type(sub.two) # N: Revealed type is "builtins.str*"
+ reveal_type(sub.three) # N: Revealed type is "builtins.float*"
+ skip: True # Need to investigate why this is broken
+
+- case: testAttrsMultiGenericInheritance
+ main: |
+ from typing import Generic, TypeVar
+ import attr
+
+ T = TypeVar("T")
+
+ @attr.s(auto_attribs=True, eq=False)
+ class Base(Generic[T]):
+ base_attr: T
+
+ S = TypeVar("S")
+
+ @attr.s(auto_attribs=True, eq=False)
+ class Middle(Base[int], Generic[S]):
+ middle_attr: S
+
+ @attr.s(auto_attribs=True, eq=False)
+ class Sub(Middle[str]):
+ pass
+
+ reveal_type(Sub.__init__)
+
+ sub = Sub(base_attr=1, middle_attr='ok')
+ reveal_type(sub) # N: Revealed type is "main.Sub"
+ reveal_type(sub.base_attr) # N: Revealed type is "builtins.int*"
+ reveal_type(sub.middle_attr) # N: Revealed type is "builtins.str*"
+ skip: True # Need to investigate why this is broken
+
+- case: testAttrsGenericClassmethod
+ main: |
+ from typing import TypeVar, Generic, Optional
+ import attr
+ T = TypeVar('T')
+ @attr.s(auto_attribs=True)
+ class A(Generic[T]):
+ x: Optional[T]
+ @classmethod
+ def clsmeth(cls) -> None:
+ reveal_type(cls) # N: Revealed type is "Type[main.A[T`1]]"
+
+- case: testAttrsForwardReference
+ main: |
+ from typing import Optional
+ import attr
+ @attr.s(auto_attribs=True)
+ class A:
+ parent: 'B'
+
+ @attr.s(auto_attribs=True)
+ class B:
+ parent: Optional[A]
+
+ reveal_type(A) # N: Revealed type is "def (parent: main.B) -> main.A"
+ reveal_type(B) # N: Revealed type is "def (parent: Union[main.A, None]) -> main.B"
+ A(B(None))
+
+- case: testAttrsForwardReferenceInClass
+ main: |
+ from typing import Optional
+ import attr
+ @attr.s(auto_attribs=True)
+ class A:
+ parent: A.B
+
+ @attr.s(auto_attribs=True)
+ class B:
+ parent: Optional[A]
+
+ reveal_type(A) # N: Revealed type is "def (parent: main.A.B) -> main.A"
+ reveal_type(A.B) # N: Revealed type is "def (parent: Union[main.A, None]) -> main.A.B"
+ A(A.B(None))
+
+- case: testAttrsImporting
+ main: |
+ from helper import A
+ reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.str) -> helper.A"
+ files:
+ - path: helper.py
+ content: |
+ import attr
+ @attr.s(auto_attribs=True)
+ class A:
+ a: int
+ b: str = attr.ib()
+
+- case: testAttrsOtherMethods
+ main: |
+ import attr
+ @attr.s(auto_attribs=True)
+ class A:
+ a: int
+ b: str = attr.ib()
+ @classmethod
+ def new(cls) -> A:
+ reveal_type(cls) # N: Revealed type is "Type[main.A]"
+ return cls(6, 'hello')
+ @classmethod
+ def bad(cls) -> A:
+ return cls(17) # E: Missing positional argument "b" in call to "A"
+ def foo(self) -> int:
+ return self.a
+ reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.str) -> main.A"
+ a = A.new()
+ reveal_type(a.foo) # N: Revealed type is "def () -> builtins.int"
+
+- case: testAttrsOtherOverloads
+ main: |
+ import attr
+ from typing import overload, Union
+
+ @attr.s
+ class A:
+ a = attr.ib()
+ b = attr.ib(default=3)
+
+ @classmethod
+ def other(cls) -> str:
+ return "..."
+
+ @overload
+ @classmethod
+ def foo(cls, x: int) -> int: ...
+
+ @overload
+ @classmethod
+ def foo(cls, x: str) -> str: ...
+
+ @classmethod
+ def foo(cls, x: Union[int, str]) -> Union[int, str]:
+ reveal_type(cls) # N: Revealed type is "Type[main.A]"
+ reveal_type(cls.other()) # N: Revealed type is "builtins.str"
+ return x
+
+ reveal_type(A.foo(3)) # N: Revealed type is "builtins.int"
+ reveal_type(A.foo("foo")) # N: Revealed type is "builtins.str"
+
+- case: testAttrsDefaultDecorator
+ main: |
+ import attr
+ @attr.s
+ class C(object):
+ x: int = attr.ib(default=1)
+ y: int = attr.ib()
+ @y.default
+ def name_does_not_matter(self):
+ return self.x + 1
+ C()
+
+- case: testAttrsValidatorDecorator
+ main: |
+ import attr
+ @attr.s
+ class C(object):
+ x = attr.ib()
+ @x.validator
+ def check(self, attribute, value):
+ if value > 42:
+ raise ValueError("x must be smaller or equal to 42")
+ C(42)
+ C(43)
+
+- case: testAttrsLocalVariablesInClassMethod
+ main: |
+ import attr
+ @attr.s(auto_attribs=True)
+ class A:
+ a: int
+ b: int = attr.ib()
+ @classmethod
+ def new(cls, foo: int) -> A:
+ a = foo
+ b = a
+ return cls(a, b)
+
+- case: testAttrsUnionForward
+ main: |
+ import attr
+ from typing import Union, List
+
+ @attr.s(auto_attribs=True)
+ class A:
+ frob: List['AOrB']
+
+ class B:
+ pass
+
+ AOrB = Union[A, B]
+
+ reveal_type(A) # N: Revealed type is "def (frob: builtins.list[Union[main.A, main.B]]) -> main.A"
+ reveal_type(B) # N: Revealed type is "def () -> main.B"
+
+ A([B()])
+
+- case: testAttrsUsingConverter
+ main: |
+ import attr
+ import helper
+
+ def converter2(s:int) -> str:
+ return 'hello'
+
+ @attr.s
+ class C:
+ x: str = attr.ib(converter=helper.converter)
+ y: str = attr.ib(converter=converter2)
+
+ # Because of the converter the __init__ takes an int, but the variable is a str.
+ reveal_type(C) # N: Revealed type is "def (x: builtins.int, y: builtins.int) -> main.C"
+ reveal_type(C(15, 16).x) # N: Revealed type is "builtins.str"
+ files:
+ - path: helper.py
+ content: |
+ def converter(s:int) -> str:
+ return 'hello'
+
+- case: testAttrsUsingBadConverter
+ mypy_config:
+ strict_optional = False
+ main: |
+ import attr
+ from typing import overload
+ @overload
+ def bad_overloaded_converter(x: int, y: int) -> int:
+ ...
+ @overload
+ def bad_overloaded_converter(x: str, y: str) -> str:
+ ...
+ def bad_overloaded_converter(x, y=7):
+ return x
+ def bad_converter() -> str:
+ return ''
+ @attr.dataclass
+ class A:
+ bad: str = attr.ib(converter=bad_converter)
+ 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"
+
+- case: testAttrsUsingBadConverterReprocess
+ mypy_config:
+ strict_optional = False
+ main: |
+ import attr
+ from typing import overload
+ forward: 'A'
+ @overload
+ def bad_overloaded_converter(x: int, y: int) -> int:
+ ...
+ @overload
+ def bad_overloaded_converter(x: str, y: str) -> str:
+ ...
+ def bad_overloaded_converter(x, y=7):
+ return x
+ def bad_converter() -> str:
+ return ''
+ @attr.dataclass
+ class A:
+ bad: str = attr.ib(converter=bad_converter)
+ 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"
+
+- case: testAttrsUsingUnsupportedConverter
+ main: |
+ import attr
+ class Thing:
+ def do_it(self, int) -> str:
+ ...
+ thing = Thing()
+ def factory(default: int):
+ ...
+ @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
+ reveal_type(C) # N: Revealed type is "def (x: Any, y: Any, z: Any) -> main.C"
+
+- case: testAttrsUsingConverterAndSubclass
+ main: |
+ import attr
+
+ def converter(s:int) -> str:
+ return 'hello'
+
+ @attr.s
+ class C:
+ x: str = attr.ib(converter=converter)
+
+ @attr.s
+ class A(C):
+ pass
+
+ # Because of the convert the __init__ takes an int, but the variable is a str.
+ reveal_type(A) # N: Revealed type is "def (x: builtins.int) -> main.A"
+ reveal_type(A(15).x) # N: Revealed type is "builtins.str"
+
+- case: testAttrsUsingConverterWithTypes
+ main: |
+ from typing import overload
+ import attr
+
+ @attr.dataclass
+ class A:
+ x: str
+
+ @attr.s
+ class C:
+ x: complex = attr.ib(converter=complex)
+ y: int = attr.ib(converter=int)
+ z: A = attr.ib(converter=A)
+
+ o = C("1", "2", "3")
+ o = C(1, 2, "3")
+
+- case: testAttrsCmpWithSubclasses
+ main: |
+ import attr
+ @attr.s
+ class A: pass
+ @attr.s
+ class B: pass
+ @attr.s
+ class C(A, B): pass
+ @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"
+
+ A() < A()
+ B() < B()
+ A() < B() # E: Unsupported operand types for < ("A" and "B")
+
+ C() > A()
+ C() > B()
+ C() > C()
+ C() > D() # E: Unsupported operand types for > ("C" and "D")
+
+ D() >= A()
+ D() >= B() # E: Unsupported operand types for >= ("D" and "B")
+ D() >= C() # E: Unsupported operand types for >= ("D" and "C")
+ 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")
+
+- case: testAttrsComplexSuperclass
+ main: |
+ import attr
+ @attr.s
+ class C:
+ x: int = attr.ib(default=1)
+ y: int = attr.ib()
+ @y.default
+ def name_does_not_matter(self):
+ return self.x + 1
+ @attr.s
+ class A(C):
+ z: int = attr.ib(default=18)
+ reveal_type(C) # N: Revealed type is "def (x: builtins.int =, y: builtins.int =) -> main.C"
+ reveal_type(A) # N: Revealed type is "def (x: builtins.int =, y: builtins.int =, z: builtins.int =) -> main.A"
+
+- case: testAttrsMultiAssign
+ main: |
+ import attr
+ @attr.s
+ class A:
+ x, y, z = attr.ib(), attr.ib(type=int), attr.ib(default=17)
+ reveal_type(A) # N: Revealed type is "def (x: Any, y: builtins.int, z: Any =) -> main.A"
+
+- case: testAttrsMultiAssign2
+ main: |
+ import attr
+ @attr.s
+ class A:
+ x = y = z = attr.ib() # E: Too many names for one attribute
+
+- case: testAttrsPrivateInit
+ main: |
+ import attr
+ @attr.s
+ class C(object):
+ _x = attr.ib(init=False, default=42)
+ C()
+ C(_x=42) # E: Unexpected keyword argument "_x" for "C"
+
+- case: testAttrsAutoMustBeAll
+ main: |
+ import attr
+ @attr.s(auto_attribs=True)
+ class A:
+ 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"
+
+- case: testAttrsRepeatedName
+ main: |
+ import attr
+ @attr.s
+ class A:
+ a = attr.ib(default=8)
+ b = attr.ib()
+ a = attr.ib()
+ reveal_type(A) # N: Revealed type is "def (b: Any, a: Any) -> main.A"
+ @attr.s
+ class B:
+ a: int = attr.ib(default=8)
+ b: int = attr.ib()
+ a: int = attr.ib() # E: Name "a" already defined on line 10
+ 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
+ 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
+
+ @attr.dataclass
+ class NonFrozenBase:
+ a: int
+
+ @attr.dataclass(frozen=True)
+ class FrozenBase:
+ a: int
+
+ @attr.dataclass(frozen=True)
+ class FrozenNonFrozen(NonFrozenBase):
+ b: int
+
+ @attr.dataclass(frozen=True)
+ class FrozenFrozen(FrozenBase):
+ b: int
+
+ @attr.dataclass
+ class NonFrozenFrozen(FrozenBase):
+ b: int
+
+ # Make sure these are untouched
+ 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
+
+ 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
+
+ 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
+
+ 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
+- case: testAttrsCallableAttributes
+ main: |
+ from typing import Callable
+ import attr
+ def blah(a: int, b: int) -> bool:
+ return True
+
+ @attr.s(auto_attribs=True)
+ class F:
+ _cb: Callable[[int, int], bool] = blah
+ def foo(self) -> bool:
+ return self._cb(5, 6)
+
+ @attr.s
+ class G:
+ _cb: Callable[[int, int], bool] = attr.ib(blah)
+ def foo(self) -> bool:
+ return self._cb(5, 6)
+
+ @attr.s(auto_attribs=True, frozen=True)
+ class FFrozen(F):
+ def bar(self) -> bool:
+ return self._cb(5, 6)
+
+- case: testAttrsWithFactory
+ main: |
+ from typing import List
+ import attr
+ def my_factory() -> int:
+ return 7
+ @attr.s
+ class A:
+ x: List[int] = attr.ib(factory=list)
+ y: int = attr.ib(factory=my_factory)
+ A()
+
+- case: testAttrsFactoryAndDefault
+ main: |
+ import attr
+ @attr.s
+ class A:
+ x: int = attr.ib(factory=int, default=7) # E: Can't pass both "default" and "factory".
+
+- case: testAttrsFactoryBadReturn
+ 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")
+
+- case: testAttrsDefaultAndInit
+ main: |
+ import attr
+
+ @attr.s
+ class C:
+ a = attr.ib(init=False, default=42)
+ 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.
+
+- case: testAttrsOptionalConverter
+ main: |
+ # flags: --strict-optional
+ import attr
+ from attr.converters import optional
+ from typing import Optional
+
+ def converter(s:int) -> str:
+ return 'hello'
+
+
+ @attr.s
+ class A:
+ y: Optional[int] = attr.ib(converter=optional(int))
+ z: Optional[str] = attr.ib(converter=optional(converter))
+
+
+ A(None, None)
+
+- case: testAttrsTypeVarNoCollision
+ main: |
+ from typing import TypeVar, Generic
+ import attr
+
+ T = TypeVar("T", bytes, str)
+
+ # Make sure the generated __le__ (and friends) don't use T for their arguments.
+ @attr.s(auto_attribs=True)
+ class A(Generic[T]):
+ v: T
+
+- case: testAttrsKwOnlyAttrib
+ main: |
+ import attr
+ @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(a=15)
+
+- case: testAttrsKwOnlyClass
+ main: |
+ import attr
+ @attr.s(kw_only=True, auto_attribs=True)
+ class A:
+ a: int
+ b: bool
+ A() # E: Missing named argument "a" for "A" # E: Missing named argument "b" for "A"
+ A(b=True, a=15)
+
+- case: testAttrsKwOnlyClassNoInit
+ main: |
+ import attr
+ @attr.s(kw_only=True)
+ class B:
+ a = attr.ib(init=False)
+ b = attr.ib()
+ B(b=True)
+
+- case: testAttrsKwOnlyWithDefault
+ main: |
+ import attr
+ @attr.s
+ class C:
+ a = attr.ib(0)
+ b = attr.ib(kw_only=True)
+ c = attr.ib(16, kw_only=True)
+ C(b=17)
+
+- case: testAttrsKwOnlyClassWithMixedDefaults
+ main: |
+ import attr
+ @attr.s(kw_only=True)
+ class D:
+ a = attr.ib(10)
+ b = attr.ib()
+ c = attr.ib(15)
+ D(b=17)
+
+
+- case: testAttrsKwOnlySubclass
+ main: |
+ import attr
+ @attr.s
+ class A2:
+ a = attr.ib(default=0)
+ @attr.s
+ class B2(A2):
+ b = attr.ib(kw_only=True)
+ B2(b=1)
+
+- case: testAttrsNonKwOnlyAfterKwOnly
+ main: |
+ import attr
+ @attr.s(kw_only=True)
+ class A:
+ a = attr.ib(default=0)
+ @attr.s
+ class B(A):
+ b = attr.ib()
+ @attr.s
+ class C:
+ 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
+ import attr
+ from typing import List
+
+ @attr.s
+ class B:
+ x: C = attr.ib()
+
+ class C(List[C]):
+ pass
+
+ reveal_type(B) # N: Revealed type is "def (x: main.C) -> main.B"
+
+- case: testDisallowUntypedWorksForwardBad
+ mypy_config:
+ disallow_untyped_defs = True
+ main: |
+ import attr
+
+ @attr.s
+ class B:
+ x = attr.ib() # E: Need type annotation for "x"
+
+ reveal_type(B) # N: Revealed type is "def (x: Any) -> main.B"
+
+- case: testAttrsDefaultDecoratorDeferred
+ main: |
+ defer: Yes
+
+ import attr
+ @attr.s
+ class C(object):
+ x: int = attr.ib(default=1)
+ y: int = attr.ib()
+ @y.default
+ def inc(self):
+ return self.x + 1
+
+ class Yes: ...
+
+- case: testAttrsValidatorDecoratorDeferred
+ main: |
+ defer: Yes
+
+ import attr
+ @attr.s
+ class C(object):
+ x = attr.ib()
+ @x.validator
+ def check(self, attribute, value):
+ if value > 42:
+ raise ValueError("x must be smaller or equal to 42")
+ C(42)
+ C(43)
+
+ class Yes: ...
+
+- case: testTypeInAttrUndefined
+ main: |
+ import attr
+
+ @attr.s
+ class C:
+ total = attr.ib(type=Bad) # E: Name "Bad" is not defined
+
+- case: testTypeInAttrForwardInRuntime
+ main: |
+ import attr
+
+ @attr.s
+ class C:
+ 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"
+ class Forward: ...
+
+- case: testDefaultInAttrForward
+ main: |
+ import attr
+
+ @attr.s
+ class C:
+ total = attr.ib(default=func())
+
+ def func() -> int: ...
+
+ C()
+ C(1)
+ C(1, 2) # E: Too many arguments for "C"
+
+- case: testTypeInAttrUndefinedFrozen
+ main: |
+ import attr
+
+ @attr.s(frozen=True)
+ class C:
+ total = attr.ib(type=Bad) # E: Name "Bad" is not defined
+
+ C(0).total = 1 # E: Property "total" defined in "C" is read-only
+
+- case: testTypeInAttrDeferredStar
+ main: |
+ import lib
+ files:
+ - path: lib.py
+ content: |
+ import attr
+ MYPY = False
+ if MYPY: # Force deferral
+ from other import *
+
+ @attr.s
+ 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"
+ - path: other.py
+ content: |
+ import lib
+
+- case: testAttrsDefaultsMroOtherFile
+ main: |
+ import a
+ files:
+ - path: a.py
+ content: |
+ import attr
+ from b import A1, A2
+
+ @attr.s
+ class Asdf(A1, A2): # E: Non-default attributes not allowed after default attributes.
+ pass
+ - path: b.py
+ content: |
+ import attr
+
+ @attr.s
+ class A1:
+ a: str = attr.ib('test')
+
+ @attr.s
+ class A2:
+ b: int = attr.ib()
+
+- case: testAttrsInheritanceNoAnnotation
+ main: |
+ import attr
+
+ @attr.s
+ class A:
+ foo = attr.ib() # type: int
+
+ x = 0
+ @attr.s
+ class B(A):
+ foo = x
+
+ reveal_type(B) # N: Revealed type is "def (foo: builtins.int) -> main.B"
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_next_gen.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_next_gen.py
new file mode 100644
index 0000000000..8395f9c028
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_next_gen.py
@@ -0,0 +1,440 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Python 3-only integration tests for provisional next generation APIs.
+"""
+
+import re
+
+from functools import partial
+
+import pytest
+
+import attr as _attr # don't use it by accident
+import attrs
+
+
+@attrs.define
+class C:
+ x: str
+ y: int
+
+
+class TestNextGen:
+ def test_simple(self):
+ """
+ Instantiation works.
+ """
+ C("1", 2)
+
+ def test_no_slots(self):
+ """
+ slots can be deactivated.
+ """
+
+ @attrs.define(slots=False)
+ class NoSlots:
+ x: int
+
+ ns = NoSlots(1)
+
+ assert {"x": 1} == getattr(ns, "__dict__")
+
+ def test_validates(self):
+ """
+ Validators at __init__ and __setattr__ work.
+ """
+
+ @attrs.define
+ class Validated:
+ x: int = attrs.field(validator=attrs.validators.instance_of(int))
+
+ v = Validated(1)
+
+ with pytest.raises(TypeError):
+ Validated(None)
+
+ with pytest.raises(TypeError):
+ v.x = "1"
+
+ def test_no_order(self):
+ """
+ Order is off by default but can be added.
+ """
+ with pytest.raises(TypeError):
+ C("1", 2) < C("2", 3)
+
+ @attrs.define(order=True)
+ class Ordered:
+ x: int
+
+ assert Ordered(1) < Ordered(2)
+
+ def test_override_auto_attribs_true(self):
+ """
+ Don't guess if auto_attrib is set explicitly.
+
+ Having an unannotated attrs.ib/attrs.field fails.
+ """
+ with pytest.raises(attrs.exceptions.UnannotatedAttributeError):
+
+ @attrs.define(auto_attribs=True)
+ class ThisFails:
+ x = attrs.field()
+ y: int
+
+ def test_override_auto_attribs_false(self):
+ """
+ Don't guess if auto_attrib is set explicitly.
+
+ Annotated fields that don't carry an attrs.ib are ignored.
+ """
+
+ @attrs.define(auto_attribs=False)
+ class NoFields:
+ x: int
+ y: int
+
+ assert NoFields() == NoFields()
+
+ def test_auto_attribs_detect(self):
+ """
+ define correctly detects if a class lacks type annotations.
+ """
+
+ @attrs.define
+ class OldSchool:
+ x = attrs.field()
+
+ assert OldSchool(1) == OldSchool(1)
+
+ # Test with maybe_cls = None
+ @attrs.define()
+ class OldSchool2:
+ x = attrs.field()
+
+ assert OldSchool2(1) == OldSchool2(1)
+
+ def test_auto_attribs_detect_fields_and_annotations(self):
+ """
+ define infers auto_attribs=True if fields have type annotations
+ """
+
+ @attrs.define
+ class NewSchool:
+ x: int
+ y: list = attrs.field()
+
+ @y.validator
+ def _validate_y(self, attribute, value):
+ if value < 0:
+ raise ValueError("y must be positive")
+
+ assert NewSchool(1, 1) == NewSchool(1, 1)
+ with pytest.raises(ValueError):
+ NewSchool(1, -1)
+ assert list(attrs.fields_dict(NewSchool).keys()) == ["x", "y"]
+
+ def test_auto_attribs_partially_annotated(self):
+ """
+ define infers auto_attribs=True if any type annotations are found
+ """
+
+ @attrs.define
+ class NewSchool:
+ x: int
+ y: list
+ z = 10
+
+ # fields are defined for any annotated attributes
+ assert NewSchool(1, []) == NewSchool(1, [])
+ assert list(attrs.fields_dict(NewSchool).keys()) == ["x", "y"]
+
+ # while the unannotated attributes are left as class vars
+ assert NewSchool.z == 10
+ assert "z" in NewSchool.__dict__
+
+ def test_auto_attribs_detect_annotations(self):
+ """
+ define correctly detects if a class has type annotations.
+ """
+
+ @attrs.define
+ class NewSchool:
+ x: int
+
+ assert NewSchool(1) == NewSchool(1)
+
+ # Test with maybe_cls = None
+ @attrs.define()
+ class NewSchool2:
+ x: int
+
+ assert NewSchool2(1) == NewSchool2(1)
+
+ def test_exception(self):
+ """
+ Exceptions are detected and correctly handled.
+ """
+
+ @attrs.define
+ class E(Exception):
+ msg: str
+ other: int
+
+ with pytest.raises(E) as ei:
+ raise E("yolo", 42)
+
+ e = ei.value
+
+ assert ("yolo", 42) == e.args
+ assert "yolo" == e.msg
+ assert 42 == e.other
+
+ def test_frozen(self):
+ """
+ attrs.frozen freezes classes.
+ """
+
+ @attrs.frozen
+ class F:
+ x: str
+
+ f = F(1)
+
+ with pytest.raises(attrs.exceptions.FrozenInstanceError):
+ f.x = 2
+
+ def test_auto_detect_eq(self):
+ """
+ auto_detect=True works for eq.
+
+ Regression test for #670.
+ """
+
+ @attrs.define
+ class C:
+ def __eq__(self, o):
+ raise ValueError()
+
+ with pytest.raises(ValueError):
+ C() == C()
+
+ def test_subclass_frozen(self):
+ """
+ It's possible to subclass an `attrs.frozen` class and the frozen-ness
+ is inherited.
+ """
+
+ @attrs.frozen
+ class A:
+ a: int
+
+ @attrs.frozen
+ class B(A):
+ b: int
+
+ @attrs.define(on_setattr=attrs.setters.NO_OP)
+ class C(B):
+ c: int
+
+ assert B(1, 2) == B(1, 2)
+ assert C(1, 2, 3) == C(1, 2, 3)
+
+ with pytest.raises(attrs.exceptions.FrozenInstanceError):
+ A(1).a = 1
+
+ with pytest.raises(attrs.exceptions.FrozenInstanceError):
+ B(1, 2).a = 1
+
+ with pytest.raises(attrs.exceptions.FrozenInstanceError):
+ B(1, 2).b = 2
+
+ with pytest.raises(attrs.exceptions.FrozenInstanceError):
+ C(1, 2, 3).c = 3
+
+ def test_catches_frozen_on_setattr(self):
+ """
+ Passing frozen=True and on_setattr hooks is caught, even if the
+ immutability is inherited.
+ """
+
+ @attrs.define(frozen=True)
+ class A:
+ pass
+
+ with pytest.raises(
+ ValueError, match="Frozen classes can't use on_setattr."
+ ):
+
+ @attrs.define(frozen=True, on_setattr=attrs.setters.validate)
+ class B:
+ pass
+
+ with pytest.raises(
+ ValueError,
+ match=re.escape(
+ "Frozen classes can't use on_setattr "
+ "(frozen-ness was inherited)."
+ ),
+ ):
+
+ @attrs.define(on_setattr=attrs.setters.validate)
+ class C(A):
+ pass
+
+ @pytest.mark.parametrize(
+ "decorator",
+ [
+ partial(_attr.s, frozen=True, slots=True, auto_exc=True),
+ attrs.frozen,
+ attrs.define,
+ attrs.mutable,
+ ],
+ )
+ def test_discard_context(self, decorator):
+ """
+ raise from None works.
+
+ Regression test for #703.
+ """
+
+ @decorator
+ class MyException(Exception):
+ x: str = attrs.field()
+
+ with pytest.raises(MyException) as ei:
+ try:
+ raise ValueError()
+ except ValueError:
+ raise MyException("foo") from None
+
+ assert "foo" == ei.value.x
+ assert ei.value.__cause__ is None
+
+ def test_converts_and_validates_by_default(self):
+ """
+ If no on_setattr is set, assume setters.convert, setters.validate.
+ """
+
+ @attrs.define
+ class C:
+ x: int = attrs.field(converter=int)
+
+ @x.validator
+ def _v(self, _, value):
+ if value < 10:
+ raise ValueError("must be >=10")
+
+ inst = C(10)
+
+ # Converts
+ inst.x = "11"
+
+ assert 11 == inst.x
+
+ # Validates
+ with pytest.raises(ValueError, match="must be >=10"):
+ inst.x = "9"
+
+ def test_mro_ng(self):
+ """
+ Attributes and methods are looked up the same way in NG by default.
+
+ See #428
+ """
+
+ @attrs.define
+ class A:
+
+ x: int = 10
+
+ def xx(self):
+ return 10
+
+ @attrs.define
+ class B(A):
+ y: int = 20
+
+ @attrs.define
+ class C(A):
+ x: int = 50
+
+ def xx(self):
+ return 50
+
+ @attrs.define
+ class D(B, C):
+ pass
+
+ d = D()
+
+ assert d.x == d.xx()
+
+
+class TestAsTuple:
+ def test_smoke(self):
+ """
+ `attrs.astuple` only changes defaults, so we just call it and compare.
+ """
+ inst = C("foo", 42)
+
+ assert attrs.astuple(inst) == _attr.astuple(inst)
+
+
+class TestAsDict:
+ def test_smoke(self):
+ """
+ `attrs.asdict` only changes defaults, so we just call it and compare.
+ """
+ inst = C("foo", {(1,): 42})
+
+ assert attrs.asdict(inst) == _attr.asdict(
+ inst, retain_collection_types=True
+ )
+
+
+class TestImports:
+ """
+ Verify our re-imports and mirroring works.
+ """
+
+ def test_converters(self):
+ """
+ Importing from attrs.converters works.
+ """
+ from attrs.converters import optional
+
+ assert optional is _attr.converters.optional
+
+ def test_exceptions(self):
+ """
+ Importing from attrs.exceptions works.
+ """
+ from attrs.exceptions import FrozenError
+
+ assert FrozenError is _attr.exceptions.FrozenError
+
+ def test_filters(self):
+ """
+ Importing from attrs.filters works.
+ """
+ from attrs.filters import include
+
+ assert include is _attr.filters.include
+
+ def test_setters(self):
+ """
+ Importing from attrs.setters works.
+ """
+ from attrs.setters import pipe
+
+ assert pipe is _attr.setters.pipe
+
+ def test_validators(self):
+ """
+ Importing from attrs.validators works.
+ """
+ from attrs.validators import and_
+
+ assert and_ is _attr.validators.and_
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_pattern_matching.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_pattern_matching.py
new file mode 100644
index 0000000000..590804a8a7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_pattern_matching.py
@@ -0,0 +1,101 @@
+# SPDX-License-Identifier: MIT
+
+# Keep this file SHORT, until Black can handle it.
+import pytest
+
+import attr
+
+
+class TestPatternMatching:
+ """
+ Pattern matching syntax test cases.
+ """
+
+ @pytest.mark.parametrize("dec", [attr.s, attr.define, attr.frozen])
+ def test_simple_match_case(self, dec):
+ """
+ Simple match case statement works as expected with all class
+ decorators.
+ """
+
+ @dec
+ class C(object):
+ a = attr.ib()
+
+ assert ("a",) == C.__match_args__
+
+ matched = False
+ c = C(a=1)
+ match c:
+ case C(a):
+ matched = True
+
+ assert matched
+ assert 1 == a
+
+ def test_explicit_match_args(self):
+ """
+ Does not overwrite a manually set empty __match_args__.
+ """
+
+ ma = ()
+
+ @attr.define
+ class C:
+ a = attr.field()
+ __match_args__ = ma
+
+ c = C(a=1)
+
+ msg = r"C\(\) accepts 0 positional sub-patterns \(1 given\)"
+ with pytest.raises(TypeError, match=msg):
+ match c:
+ case C(_):
+ pass
+
+ def test_match_args_kw_only(self):
+ """
+ kw_only classes don't generate __match_args__.
+ kw_only fields are not included in __match_args__.
+ """
+
+ @attr.define
+ class C:
+ a = attr.field(kw_only=True)
+ b = attr.field()
+
+ assert ("b",) == C.__match_args__
+
+ c = C(a=1, b=1)
+ msg = r"C\(\) accepts 1 positional sub-pattern \(2 given\)"
+ with pytest.raises(TypeError, match=msg):
+ match c:
+ case C(a, b):
+ pass
+
+ found = False
+ match c:
+ case C(b, a=a):
+ found = True
+
+ assert found
+
+ @attr.define(kw_only=True)
+ class C:
+ a = attr.field()
+ b = attr.field()
+
+ c = C(a=1, b=1)
+ msg = r"C\(\) accepts 0 positional sub-patterns \(2 given\)"
+ with pytest.raises(TypeError, match=msg):
+ match c:
+ case C(a, b):
+ pass
+
+ found = False
+ match c:
+ case C(a=a, b=b):
+ found = True
+
+ assert found
+ assert (1, 1) == (a, b)
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_pyright.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_pyright.py
new file mode 100644
index 0000000000..c30dcc5cb1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_pyright.py
@@ -0,0 +1,71 @@
+# SPDX-License-Identifier: MIT
+
+import json
+import os.path
+import shutil
+import subprocess
+import sys
+
+import pytest
+
+import attr
+
+
+if sys.version_info < (3, 6):
+ _found_pyright = False
+else:
+ _found_pyright = shutil.which("pyright")
+
+
+@attr.s(frozen=True)
+class PyrightDiagnostic(object):
+ severity = attr.ib()
+ message = attr.ib()
+
+
+@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.
+ """
+
+ test_file = os.path.dirname(__file__) + "/dataclass_transform_example.py"
+
+ pyright = subprocess.run(
+ ["pyright", "--outputjson", str(test_file)], capture_output=True
+ )
+ pyright_result = json.loads(pyright.stdout)
+
+ diagnostics = set(
+ PyrightDiagnostic(d["severity"], d["message"])
+ for d in pyright_result["generalDiagnostics"]
+ )
+
+ # Expected diagnostics as per pyright 1.1.135
+ expected_diagnostics = {
+ PyrightDiagnostic(
+ severity="information",
+ message='Type of "Define.__init__" is'
+ ' "(self: Define, a: str, b: int) -> None"',
+ ),
+ PyrightDiagnostic(
+ severity="information",
+ message='Type of "DefineConverter.__init__" is '
+ '"(self: DefineConverter, with_converter: int) -> None"',
+ ),
+ PyrightDiagnostic(
+ severity="information",
+ message='Type of "d.a" is "Literal[\'new\']"',
+ ),
+ PyrightDiagnostic(
+ severity="error",
+ message='Cannot assign member "a" for type '
+ '"FrozenDefine"\n\xa0\xa0"FrozenDefine" is frozen',
+ ),
+ PyrightDiagnostic(
+ severity="information",
+ message='Type of "d2.a" is "Literal[\'new\']"',
+ ),
+ }
+
+ assert diagnostics == expected_diagnostics
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_setattr.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_setattr.py
new file mode 100644
index 0000000000..aaedde5746
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_setattr.py
@@ -0,0 +1,437 @@
+# SPDX-License-Identifier: MIT
+
+from __future__ import absolute_import, division, print_function
+
+import pickle
+
+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):
+ x = attr.ib()
+
+
+@attr.s
+class WithOnSetAttrHook(object):
+ x = attr.ib(on_setattr=lambda *args: None)
+
+
+class TestSetAttr(object):
+ def test_change(self):
+ """
+ The return value of a hook overwrites the value. But they are not run
+ on __init__.
+ """
+
+ def hook(*a, **kw):
+ return "hooked!"
+
+ @attr.s
+ class Hooked(object):
+ x = attr.ib(on_setattr=hook)
+ y = attr.ib()
+
+ h = Hooked("x", "y")
+
+ assert "x" == h.x
+ assert "y" == h.y
+
+ h.x = "xxx"
+ h.y = "yyy"
+
+ assert "yyy" == h.y
+ assert "hooked!" == h.x
+
+ def test_frozen_attribute(self):
+ """
+ Frozen attributes raise FrozenAttributeError, others are not affected.
+ """
+
+ @attr.s
+ class PartiallyFrozen(object):
+ x = attr.ib(on_setattr=setters.frozen)
+ y = attr.ib()
+
+ pf = PartiallyFrozen("x", "y")
+
+ pf.y = "yyy"
+
+ assert "yyy" == pf.y
+
+ with pytest.raises(FrozenAttributeError):
+ pf.x = "xxx"
+
+ assert "x" == pf.x
+
+ @pytest.mark.parametrize(
+ "on_setattr",
+ [setters.validate, [setters.validate], setters.pipe(setters.validate)],
+ )
+ def test_validator(self, on_setattr):
+ """
+ Validators are run and they don't alter the value.
+ """
+
+ @attr.s(on_setattr=on_setattr)
+ class ValidatedAttribute(object):
+ x = attr.ib()
+ y = attr.ib(validator=[instance_of(str), matches_re("foo.*qux")])
+
+ va = ValidatedAttribute(42, "foobarqux")
+
+ with pytest.raises(TypeError) as ei:
+ va.y = 42
+
+ assert "foobarqux" == va.y
+
+ assert ei.value.args[0].startswith("'y' must be <")
+
+ with pytest.raises(ValueError) as ei:
+ va.y = "quxbarfoo"
+
+ assert ei.value.args[0].startswith("'y' must match regex '")
+
+ assert "foobarqux" == va.y
+
+ va.y = "foobazqux"
+
+ assert "foobazqux" == va.y
+
+ def test_pipe(self):
+ """
+ Multiple hooks are possible, in that case the last return value is
+ used. They can be supplied using the pipe functions or by passing a
+ list to on_setattr.
+ """
+
+ s = [setters.convert, lambda _, __, nv: nv + 1]
+
+ @attr.s
+ class Piped(object):
+ x1 = attr.ib(converter=int, on_setattr=setters.pipe(*s))
+ x2 = attr.ib(converter=int, on_setattr=s)
+
+ p = Piped("41", "22")
+
+ assert 41 == p.x1
+ assert 22 == p.x2
+
+ p.x1 = "41"
+ p.x2 = "22"
+
+ assert 42 == p.x1
+ assert 23 == p.x2
+
+ def test_make_class(self):
+ """
+ on_setattr of make_class gets forwarded.
+ """
+ C = attr.make_class("C", {"x": attr.ib()}, on_setattr=setters.frozen)
+
+ c = C(1)
+
+ with pytest.raises(FrozenAttributeError):
+ c.x = 2
+
+ def test_no_validator_no_converter(self):
+ """
+ validate and convert tolerate missing validators and converters.
+ """
+
+ @attr.s(on_setattr=[setters.convert, setters.validate])
+ class C(object):
+ x = attr.ib()
+
+ c = C(1)
+
+ c.x = 2
+
+ assert 2 == c.x
+
+ def test_validate_respects_run_validators_config(self):
+ """
+ If run validators is off, validate doesn't run them.
+ """
+
+ @attr.s(on_setattr=setters.validate)
+ class C(object):
+ x = attr.ib(validator=attr.validators.instance_of(int))
+
+ c = C(1)
+
+ attr.set_run_validators(False)
+
+ c.x = "1"
+
+ assert "1" == c.x
+
+ attr.set_run_validators(True)
+
+ with pytest.raises(TypeError) as ei:
+ c.x = "1"
+
+ assert ei.value.args[0].startswith("'x' must be <")
+
+ def test_frozen_on_setattr_class_is_caught(self):
+ """
+ @attr.s(on_setattr=X, frozen=True) raises an ValueError.
+ """
+ with pytest.raises(ValueError) as ei:
+
+ @attr.s(frozen=True, on_setattr=setters.validate)
+ class C(object):
+ x = attr.ib()
+
+ assert "Frozen classes can't use on_setattr." == ei.value.args[0]
+
+ def test_frozen_on_setattr_attribute_is_caught(self):
+ """
+ attr.ib(on_setattr=X) on a frozen class raises an ValueError.
+ """
+
+ with pytest.raises(ValueError) as ei:
+
+ @attr.s(frozen=True)
+ class C(object):
+ 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
+ is generated, the __setattr__ is set to object.__setattr__.
+
+ We do the double test because of Python 2.
+ """
+
+ def boom(*args):
+ pytest.fail("Must not be called.")
+
+ @attr.s
+ class Hooked(object):
+ 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 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):
+ """
+ Not an attrs class on purpose to prevent accidental resets that
+ would render the asserts meaningless.
+ """
+
+ def __setattr__(self, *args):
+ pass
+
+ @attr.s(slots=slots)
+ class B(A):
+ pass
+
+ assert B.__setattr__ == A.__setattr__
+
+ @attr.s(slots=slots)
+ class C(B):
+ pass
+
+ 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
+ __setattr__.
+ """
+ i = WithOnSetAttrHook(1)
+
+ assert True is i.__attrs_own_setattr__
+
+ i2 = pickle.loads(pickle.dumps(i))
+
+ assert True is i2.__attrs_own_setattr__
+
+ WOSAH = pickle.loads(pickle.dumps(WithOnSetAttrHook))
+
+ assert True is WOSAH.__attrs_own_setattr__
+
+ def test_slotted_class_can_have_custom_setattr(self):
+ """
+ A slotted class can define a custom setattr and it doesn't get
+ overwritten.
+
+ Regression test for #680.
+ """
+
+ @attr.s(slots=True)
+ class A(object):
+ def __setattr__(self, key, value):
+ raise SystemError
+
+ with pytest.raises(SystemError):
+ A().x = 1
+
+ @pytest.mark.xfail(raises=attr.exceptions.FrozenAttributeError)
+ def test_slotted_confused(self):
+ """
+ If we have a in-between non-attrs class, setattr reset detection
+ should still work, but currently doesn't.
+
+ It works with dict classes because we can look the finished class and
+ patch it. With slotted classes we have to deduce it ourselves.
+ """
+
+ @attr.s(slots=True)
+ class A(object):
+ x = attr.ib(on_setattr=setters.frozen)
+
+ class B(A):
+ pass
+
+ @attr.s(slots=True)
+ class C(B):
+ x = attr.ib()
+
+ 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
+ therefore write a custom __setattr__.
+ """
+ assert 1 == WithOnSetAttrHook(1).x
+
+ @attr.s(auto_detect=True, slots=slots)
+ class RemoveNeedForOurSetAttr(WithOnSetAttrHook):
+ x = attr.ib()
+
+ def __setattr__(self, name, val):
+ object.__setattr__(self, name, val * 2)
+
+ i = RemoveNeedForOurSetAttr(1)
+
+ 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
+ set auto_detect, leave is alone.
+ """
+
+ @attr.s(auto_detect=True, slots=slots)
+ class CustomSetAttr:
+ def __setattr__(self, _, __):
+ pass
+
+ 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.
+ """
+ with pytest.raises(
+ ValueError, match="Can't freeze a class with a custom __setattr__."
+ ):
+
+ @attr.s(auto_detect=True, slots=slots, frozen=True)
+ class CustomSetAttr(Frozen):
+ 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
+ rejected.
+ """
+ with pytest.raises(
+ ValueError,
+ match="Can't combine custom __setattr__ with on_setattr hooks.",
+ ):
+
+ @attr.s(auto_detect=True, slots=slots)
+ class HookAndCustomSetAttr(object):
+ x = attr.ib(on_setattr=lambda *args: None)
+
+ def __setattr__(self, _, __):
+ pass
+
+ @pytest.mark.parametrize("a_slots", [True, False])
+ @pytest.mark.parametrize("b_slots", [True, False])
+ @pytest.mark.parametrize("c_slots", [True, False])
+ def test_setattr_inherited_do_not_reset_intermediate(
+ self, a_slots, b_slots, c_slots
+ ):
+ """
+ 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__.
+ """
+
+ @attr.s(slots=a_slots)
+ class A(object):
+ x = attr.ib(on_setattr=setters.frozen)
+
+ @attr.s(slots=b_slots, auto_detect=True)
+ class B(A):
+ x = attr.ib(on_setattr=setters.NO_OP)
+
+ def __setattr__(self, key, value):
+ raise SystemError
+
+ @attr.s(slots=c_slots)
+ class C(B):
+ pass
+
+ assert getattr(A, "__attrs_own_setattr__", False) is True
+ assert getattr(B, "__attrs_own_setattr__", False) is False
+ assert getattr(C, "__attrs_own_setattr__", False) is False
+
+ with pytest.raises(SystemError):
+ C(1).x = 3
+
+ def test_docstring(self):
+ """
+ Generated __setattr__ has a useful docstring.
+ """
+ assert (
+ "Method generated by attrs for class WithOnSetAttrHook."
+ == WithOnSetAttrHook.__setattr__.__doc__
+ )
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_slots.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_slots.py
new file mode 100644
index 0000000000..baf9a40ddb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_slots.py
@@ -0,0 +1,740 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Unit tests for slots-related functionality.
+"""
+
+import pickle
+import sys
+import types
+import weakref
+
+import pytest
+
+import attr
+
+from attr._compat import PY2, PYPY, just_warn, make_set_closure_cell
+
+
+# Pympler doesn't work on PyPy.
+try:
+ from pympler.asizeof import asizeof
+
+ has_pympler = True
+except BaseException: # Won't be an import error.
+ has_pympler = False
+
+
+@attr.s
+class C1(object):
+ x = attr.ib(validator=attr.validators.instance_of(int))
+ y = attr.ib()
+
+ def method(self):
+ return self.x
+
+ @classmethod
+ def classmethod(cls):
+ return "clsmethod"
+
+ @staticmethod
+ def staticmethod():
+ return "staticmethod"
+
+ if not PY2:
+
+ def my_class(self):
+ return __class__
+
+ def my_super(self):
+ """Just to test out the no-arg super."""
+ return super().__repr__()
+
+
+@attr.s(slots=True, hash=True)
+class C1Slots(object):
+ x = attr.ib(validator=attr.validators.instance_of(int))
+ y = attr.ib()
+
+ def method(self):
+ return self.x
+
+ @classmethod
+ def classmethod(cls):
+ return "clsmethod"
+
+ @staticmethod
+ def staticmethod():
+ return "staticmethod"
+
+ if not PY2:
+
+ def my_class(self):
+ return __class__
+
+ def my_super(self):
+ """Just to test out the no-arg super."""
+ return super().__repr__()
+
+
+def test_slots_being_used():
+ """
+ The class is really using __slots__.
+ """
+ non_slot_instance = C1(x=1, y="test")
+ slot_instance = C1Slots(x=1, y="test")
+
+ assert "__dict__" not in dir(slot_instance)
+ assert "__slots__" in dir(slot_instance)
+
+ assert "__dict__" in dir(non_slot_instance)
+ assert "__slots__" not in dir(non_slot_instance)
+
+ assert set(["__weakref__", "x", "y"]) == set(slot_instance.__slots__)
+
+ if has_pympler:
+ assert asizeof(slot_instance) < asizeof(non_slot_instance)
+
+ non_slot_instance.t = "test"
+ with pytest.raises(AttributeError):
+ slot_instance.t = "test"
+
+ assert 1 == non_slot_instance.method()
+ assert 1 == slot_instance.method()
+
+ assert attr.fields(C1Slots) == attr.fields(C1)
+ assert attr.asdict(slot_instance) == attr.asdict(non_slot_instance)
+
+
+def test_basic_attr_funcs():
+ """
+ Comparison, `__eq__`, `__hash__`, `__repr__`, `attrs.asdict` work.
+ """
+ a = C1Slots(x=1, y=2)
+ b = C1Slots(x=1, y=3)
+ a_ = C1Slots(x=1, y=2)
+
+ # Comparison.
+ assert b > a
+
+ assert a_ == a
+
+ # Hashing.
+ hash(b) # Just to assert it doesn't raise.
+
+ # Repr.
+ assert "C1Slots(x=1, y=2)" == repr(a)
+
+ assert {"x": 1, "y": 2} == attr.asdict(a)
+
+
+def test_inheritance_from_nonslots():
+ """
+ Inheritance from a non-slotted class works.
+
+ Note that a slotted class inheriting from an ordinary class loses most of
+ the benefits of slotted classes, but it should still work.
+ """
+
+ @attr.s(slots=True, hash=True)
+ class C2Slots(C1):
+ z = attr.ib()
+
+ c2 = C2Slots(x=1, y=2, z="test")
+
+ assert 1 == c2.x
+ assert 2 == c2.y
+ assert "test" == c2.z
+
+ c2.t = "test" # This will work, using the base class.
+
+ assert "test" == c2.t
+
+ assert 1 == c2.method()
+ assert "clsmethod" == c2.classmethod()
+ assert "staticmethod" == c2.staticmethod()
+
+ assert set(["z"]) == set(C2Slots.__slots__)
+
+ c3 = C2Slots(x=1, y=3, z="test")
+
+ assert c3 > c2
+
+ c2_ = C2Slots(x=1, y=2, z="test")
+
+ assert c2 == c2_
+
+ assert "C2Slots(x=1, y=2, z='test')" == repr(c2)
+
+ hash(c2) # Just to assert it doesn't raise.
+
+ assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
+
+
+def test_nonslots_these():
+ """
+ Enhancing a dict class using 'these' works.
+
+ This will actually *replace* the class with another one, using slots.
+ """
+
+ class SimpleOrdinaryClass(object):
+ def __init__(self, x, y, z):
+ self.x = x
+ self.y = y
+ self.z = z
+
+ def method(self):
+ return self.x
+
+ @classmethod
+ def classmethod(cls):
+ return "clsmethod"
+
+ @staticmethod
+ def staticmethod():
+ return "staticmethod"
+
+ C2Slots = attr.s(
+ these={"x": attr.ib(), "y": attr.ib(), "z": attr.ib()},
+ init=False,
+ slots=True,
+ hash=True,
+ )(SimpleOrdinaryClass)
+
+ c2 = C2Slots(x=1, y=2, z="test")
+ assert 1 == c2.x
+ assert 2 == c2.y
+ assert "test" == c2.z
+ with pytest.raises(AttributeError):
+ c2.t = "test" # We have slots now.
+
+ assert 1 == c2.method()
+ assert "clsmethod" == c2.classmethod()
+ assert "staticmethod" == c2.staticmethod()
+
+ assert set(["__weakref__", "x", "y", "z"]) == set(C2Slots.__slots__)
+
+ c3 = C2Slots(x=1, y=3, z="test")
+ assert c3 > c2
+ c2_ = C2Slots(x=1, y=2, z="test")
+ assert c2 == c2_
+
+ assert "SimpleOrdinaryClass(x=1, y=2, z='test')" == repr(c2)
+
+ hash(c2) # Just to assert it doesn't raise.
+
+ assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
+
+
+def test_inheritance_from_slots():
+ """
+ Inheriting from an attrs slotted class works.
+ """
+
+ @attr.s(slots=True, hash=True)
+ class C2Slots(C1Slots):
+ z = attr.ib()
+
+ @attr.s(slots=True, hash=True)
+ class C2(C1):
+ z = attr.ib()
+
+ c2 = C2Slots(x=1, y=2, z="test")
+ assert 1 == c2.x
+ assert 2 == c2.y
+ assert "test" == c2.z
+
+ assert set(["z"]) == set(C2Slots.__slots__)
+
+ assert 1 == c2.method()
+ assert "clsmethod" == c2.classmethod()
+ assert "staticmethod" == c2.staticmethod()
+
+ with pytest.raises(AttributeError):
+ c2.t = "test"
+
+ non_slot_instance = C2(x=1, y=2, z="test")
+ if has_pympler:
+ assert asizeof(c2) < asizeof(non_slot_instance)
+
+ c3 = C2Slots(x=1, y=3, z="test")
+ assert c3 > c2
+ c2_ = C2Slots(x=1, y=2, z="test")
+ assert c2 == c2_
+
+ assert "C2Slots(x=1, y=2, z='test')" == repr(c2)
+
+ hash(c2) # Just to assert it doesn't raise.
+
+ assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
+
+
+def test_inheritance_from_slots_with_attribute_override():
+ """
+ Inheriting from a slotted class doesn't re-create existing slots
+ """
+
+ class HasXSlot(object):
+ __slots__ = ("x",)
+
+ @attr.s(slots=True, hash=True)
+ class C2Slots(C1Slots):
+ # y re-defined here but it shouldn't get a slot
+ y = attr.ib()
+ z = attr.ib()
+
+ @attr.s(slots=True, hash=True)
+ class NonAttrsChild(HasXSlot):
+ # Parent class has slot for "x" already, so we skip it
+ x = attr.ib()
+ y = attr.ib()
+ z = attr.ib()
+
+ c2 = C2Slots(1, 2, "test")
+ assert 1 == c2.x
+ assert 2 == c2.y
+ assert "test" == c2.z
+
+ assert {"z"} == set(C2Slots.__slots__)
+
+ na = NonAttrsChild(1, 2, "test")
+ assert 1 == na.x
+ assert 2 == na.y
+ assert "test" == na.z
+
+ assert {"__weakref__", "y", "z"} == set(NonAttrsChild.__slots__)
+
+
+def test_inherited_slot_reuses_slot_descriptor():
+ """
+ We reuse slot descriptor for an attr.ib defined in a slotted attr.s
+ """
+
+ class HasXSlot(object):
+ __slots__ = ("x",)
+
+ class OverridesX(HasXSlot):
+ @property
+ def x(self):
+ return None
+
+ @attr.s(slots=True)
+ class Child(OverridesX):
+ x = attr.ib()
+
+ assert Child.x is not OverridesX.x
+ assert Child.x is HasXSlot.x
+
+ c = Child(1)
+ assert 1 == c.x
+ assert set() == set(Child.__slots__)
+
+ ox = OverridesX()
+ assert ox.x is None
+
+
+def test_bare_inheritance_from_slots():
+ """
+ Inheriting from a bare attrs slotted class works.
+ """
+
+ @attr.s(
+ init=False, eq=False, order=False, hash=False, repr=False, slots=True
+ )
+ class C1BareSlots(object):
+ x = attr.ib(validator=attr.validators.instance_of(int))
+ y = attr.ib()
+
+ def method(self):
+ return self.x
+
+ @classmethod
+ def classmethod(cls):
+ return "clsmethod"
+
+ @staticmethod
+ def staticmethod():
+ return "staticmethod"
+
+ @attr.s(init=False, eq=False, order=False, hash=False, repr=False)
+ class C1Bare(object):
+ x = attr.ib(validator=attr.validators.instance_of(int))
+ y = attr.ib()
+
+ def method(self):
+ return self.x
+
+ @classmethod
+ def classmethod(cls):
+ return "clsmethod"
+
+ @staticmethod
+ def staticmethod():
+ return "staticmethod"
+
+ @attr.s(slots=True, hash=True)
+ class C2Slots(C1BareSlots):
+ z = attr.ib()
+
+ @attr.s(slots=True, hash=True)
+ class C2(C1Bare):
+ z = attr.ib()
+
+ c2 = C2Slots(x=1, y=2, z="test")
+ assert 1 == c2.x
+ assert 2 == c2.y
+ assert "test" == c2.z
+
+ assert 1 == c2.method()
+ assert "clsmethod" == c2.classmethod()
+ assert "staticmethod" == c2.staticmethod()
+
+ with pytest.raises(AttributeError):
+ c2.t = "test"
+
+ non_slot_instance = C2(x=1, y=2, z="test")
+ if has_pympler:
+ assert asizeof(c2) < asizeof(non_slot_instance)
+
+ c3 = C2Slots(x=1, y=3, z="test")
+ assert c3 > c2
+ c2_ = C2Slots(x=1, y=2, z="test")
+ assert c2 == c2_
+
+ assert "C2Slots(x=1, y=2, z='test')" == repr(c2)
+
+ hash(c2) # Just to assert it doesn't raise.
+
+ assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
+
+
+@pytest.mark.skipif(PY2, reason="closure cell rewriting is PY3-only.")
+class TestClosureCellRewriting(object):
+ def test_closure_cell_rewriting(self):
+ """
+ Slotted classes support proper closure cell rewriting.
+
+ This affects features like `__class__` and the no-arg super().
+ """
+ non_slot_instance = C1(x=1, y="test")
+ slot_instance = C1Slots(x=1, y="test")
+
+ assert non_slot_instance.my_class() is C1
+ assert slot_instance.my_class() is C1Slots
+
+ # Just assert they return something, and not an exception.
+ assert non_slot_instance.my_super()
+ assert slot_instance.my_super()
+
+ def test_inheritance(self):
+ """
+ Slotted classes support proper closure cell rewriting when inheriting.
+
+ This affects features like `__class__` and the no-arg super().
+ """
+
+ @attr.s
+ class C2(C1):
+ def my_subclass(self):
+ return __class__
+
+ @attr.s
+ class C2Slots(C1Slots):
+ def my_subclass(self):
+ return __class__
+
+ non_slot_instance = C2(x=1, y="test")
+ slot_instance = C2Slots(x=1, y="test")
+
+ assert non_slot_instance.my_class() is C1
+ assert slot_instance.my_class() is C1Slots
+
+ # Just assert they return something, and not an exception.
+ assert non_slot_instance.my_super()
+ assert slot_instance.my_super()
+
+ 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
+ static methods.
+ """
+ # Python can reuse closure cells, so we create new classes just for
+ # this test.
+
+ @attr.s(slots=slots)
+ class C:
+ @classmethod
+ def clsmethod(cls):
+ return __class__
+
+ assert C.clsmethod() is C
+
+ @attr.s(slots=slots)
+ class D:
+ @staticmethod
+ def statmethod():
+ return __class__
+
+ 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():
+ """
+ Instance is not weak-referenceable when `weakref_slot=False` in CPython.
+ """
+
+ @attr.s(slots=True, weakref_slot=False)
+ class C(object):
+ pass
+
+ c = C()
+
+ with pytest.raises(TypeError):
+ weakref.ref(c)
+
+
+@pytest.mark.skipif(
+ not PYPY, reason="slots without weakref_slot should only work on PyPy"
+)
+def test_implicitly_weakrefable():
+ """
+ Instance is weak-referenceable even when `weakref_slot=False` in PyPy.
+ """
+
+ @attr.s(slots=True, weakref_slot=False)
+ class C(object):
+ pass
+
+ c = C()
+ w = weakref.ref(c)
+
+ assert c is w()
+
+
+def test_weakrefable():
+ """
+ Instance is weak-referenceable when `weakref_slot=True`.
+ """
+
+ @attr.s(slots=True, weakref_slot=True)
+ class C(object):
+ pass
+
+ c = C()
+ w = weakref.ref(c)
+
+ assert c is w()
+
+
+def test_weakref_does_not_add_a_field():
+ """
+ `weakref_slot=True` does not add a field to the class.
+ """
+
+ @attr.s(slots=True, weakref_slot=True)
+ class C(object):
+ field = attr.ib()
+
+ assert [f.name for f in attr.fields(C)] == ["field"]
+
+
+def tests_weakref_does_not_add_when_inheriting_with_weakref():
+ """
+ `weakref_slot=True` does not add a new __weakref__ slot when inheriting
+ one.
+ """
+
+ @attr.s(slots=True, weakref_slot=True)
+ class C(object):
+ pass
+
+ @attr.s(slots=True, weakref_slot=True)
+ class D(C):
+ pass
+
+ d = D()
+ w = weakref.ref(d)
+
+ assert d is w()
+
+
+def tests_weakref_does_not_add_with_weakref_attribute():
+ """
+ `weakref_slot=True` does not add a new __weakref__ slot when an attribute
+ of that name exists.
+ """
+
+ @attr.s(slots=True, weakref_slot=True)
+ class C(object):
+ __weakref__ = attr.ib(
+ init=False, hash=False, repr=False, eq=False, order=False
+ )
+
+ c = C()
+ w = weakref.ref(c)
+
+ assert c is w()
+
+
+def test_slots_empty_cell():
+ """
+ Tests that no `ValueError: Cell is empty` exception is raised when
+ 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
+ `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
+ the closure cell was empty, we fixed it and this is the non-regression
+ test.
+ """
+
+ @attr.s(slots=True)
+ class C(object):
+ field = attr.ib()
+
+ def f(self, a):
+ super(C, self).__init__()
+
+ C(field=1)
+
+
+@attr.s(getstate_setstate=True)
+class C2(object):
+ x = attr.ib()
+
+
+@attr.s(slots=True, getstate_setstate=True)
+class C2Slots(object):
+ x = attr.ib()
+
+
+class TestPickle(object):
+ @pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL))
+ def test_pickleable_by_default(self, protocol):
+ """
+ If nothing else is passed, slotted classes can be pickled and
+ unpickled with all supported protocols.
+ """
+ i1 = C1Slots(1, 2)
+ i2 = pickle.loads(pickle.dumps(i1, protocol))
+
+ assert i1 == i2
+ assert i1 is not i2
+
+ def test_no_getstate_setstate_for_dict_classes(self):
+ """
+ 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)
+
+ def test_no_getstate_setstate_if_option_false(self):
+ """
+ Don't add getstate/setstate if getstate_setstate is False.
+ """
+
+ @attr.s(slots=True, getstate_setstate=False)
+ class C(object):
+ x = attr.ib()
+
+ i = C(42)
+
+ assert None is getattr(i, "__getstate__", None)
+ assert None is getattr(i, "__setstate__", None)
+
+ @pytest.mark.parametrize("cls", [C2(1), C2Slots(1)])
+ def test_getstate_set_state_force_true(self, cls):
+ """
+ If getstate_setstate is True, add them unconditionally.
+ """
+ assert None is not getattr(cls, "__getstate__", None)
+ assert None is not getattr(cls, "__setstate__", None)
+
+
+def test_slots_super_property_get():
+ """
+ On Python 2/3: the `super(self.__class__, self)` works.
+ """
+
+ @attr.s(slots=True)
+ class A(object):
+ x = attr.ib()
+
+ @property
+ def f(self):
+ return self.x
+
+ @attr.s(slots=True)
+ class B(A):
+ @property
+ def f(self):
+ return super(B, self).f ** 2
+
+ assert B(11).f == 121
+ assert B(17).f == 289
+
+
+@pytest.mark.skipif(PY2, reason="shortcut super() is PY3-only.")
+def test_slots_super_property_get_shurtcut():
+ """
+ On Python 3, the `super()` shortcut is allowed.
+ """
+
+ @attr.s(slots=True)
+ class A(object):
+ x = attr.ib()
+
+ @property
+ def f(self):
+ return self.x
+
+ @attr.s(slots=True)
+ class B(A):
+ @property
+ def f(self):
+ return super().f ** 2
+
+ assert B(11).f == 121
+ assert B(17).f == 289
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_validators.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_validators.py
new file mode 100644
index 0000000000..d7c6de8bad
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_validators.py
@@ -0,0 +1,952 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Tests for `attr.validators`.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+import re
+
+import pytest
+
+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 (
+ and_,
+ deep_iterable,
+ deep_mapping,
+ ge,
+ gt,
+ in_,
+ instance_of,
+ is_callable,
+ le,
+ lt,
+ matches_re,
+ max_len,
+ optional,
+ provides,
+)
+
+from .utils import simple_attr
+
+
+@pytest.fixture(scope="module")
+def zope_interface():
+ """Provides ``zope.interface`` if available, skipping the test if not."""
+ try:
+ import zope.interface
+ except ImportError:
+ raise pytest.skip(
+ "zope-related tests skipped when zope.interface is not installed"
+ )
+
+ return zope.interface
+
+
+class TestDisableValidators(object):
+ @pytest.fixture(autouse=True)
+ def reset_default(self):
+ """
+ Make sure validators are always enabled after a test.
+ """
+ yield
+ _config._run_validators = True
+
+ def test_default(self):
+ """
+ Run validators by default.
+ """
+ assert _config._run_validators is True
+
+ @pytest.mark.parametrize("value, expected", [(True, False), (False, True)])
+ def test_set_validators_diabled(self, value, expected):
+ """
+ Sets `_run_validators`.
+ """
+ validator_module.set_disabled(value)
+
+ assert _config._run_validators is expected
+
+ @pytest.mark.parametrize("value, expected", [(True, False), (False, True)])
+ def test_disabled(self, value, expected):
+ """
+ Returns `_run_validators`.
+ """
+ _config._run_validators = value
+
+ assert validator_module.get_disabled() is expected
+
+ def test_disabled_ctx(self):
+ """
+ The `disabled` context manager disables running validators,
+ but only within its context.
+ """
+ assert _config._run_validators is True
+
+ with validator_module.disabled():
+ assert _config._run_validators is False
+
+ assert _config._run_validators is True
+
+ def test_disabled_ctx_with_errors(self):
+ """
+ Running validators is re-enabled even if an error is raised.
+ """
+ assert _config._run_validators is True
+
+ with pytest.raises(ValueError):
+ with validator_module.disabled():
+ assert _config._run_validators is False
+
+ raise ValueError("haha!")
+
+ assert _config._run_validators is True
+
+
+class TestInstanceOf(object):
+ """
+ Tests for `instance_of`.
+ """
+
+ def test_in_all(self):
+ """
+ Verify that this validator is in ``__all__``.
+ """
+ assert instance_of.__name__ in validator_module.__all__
+
+ def test_success(self):
+ """
+ Nothing happens if types match.
+ """
+ v = instance_of(int)
+ v(None, simple_attr("test"), 42)
+
+ def test_subclass(self):
+ """
+ Subclasses are accepted too.
+ """
+ v = instance_of(int)
+ # yep, bools are a subclass of int :(
+ v(None, simple_attr("test"), True)
+
+ def test_fail(self):
+ """
+ Raises `TypeError` on wrong types.
+ """
+ v = instance_of(int)
+ a = simple_attr("test")
+ 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),
+ a,
+ int,
+ "42",
+ ) == e.value.args
+
+ def test_repr(self):
+ """
+ Returned validator has a useful `__repr__`.
+ """
+ v = instance_of(int)
+ assert (
+ "<instance_of validator for type <{type} 'int'>>".format(type=TYPE)
+ ) == repr(v)
+
+
+class TestMatchesRe(object):
+ """
+ Tests for `matches_re`.
+ """
+
+ def test_in_all(self):
+ """
+ validator is in ``__all__``.
+ """
+ assert matches_re.__name__ in validator_module.__all__
+
+ def test_match(self):
+ """
+ Silent on matches, raises ValueError on mismatches.
+ """
+
+ @attr.s
+ class ReTester(object):
+ str_match = attr.ib(validator=matches_re("a|ab"))
+
+ ReTester("ab") # shouldn't raise exceptions
+ with pytest.raises(TypeError):
+ ReTester(1)
+ with pytest.raises(ValueError):
+ ReTester("1")
+ with pytest.raises(ValueError):
+ ReTester("a1")
+
+ def test_flags(self):
+ """
+ Flags are propagated to the match function.
+ """
+
+ @attr.s
+ class MatchTester(object):
+ val = attr.ib(validator=matches_re("a", re.IGNORECASE, re.match))
+
+ MatchTester("A1") # test flags and using re.match
+
+ def test_precompiled_pattern(self):
+ """
+ Pre-compiled patterns are accepted.
+ """
+ pattern = re.compile("a")
+
+ @attr.s
+ class RePatternTester(object):
+ val = attr.ib(validator=matches_re(pattern))
+
+ RePatternTester("a")
+
+ def test_precompiled_pattern_no_flags(self):
+ """
+ A pre-compiled pattern cannot be combined with a 'flags' argument.
+ """
+ pattern = re.compile("")
+
+ with pytest.raises(
+ TypeError, match="can only be used with a string pattern"
+ ):
+ matches_re(pattern, flags=re.IGNORECASE)
+
+ def test_different_func(self):
+ """
+ Changing the match functions works.
+ """
+
+ @attr.s
+ class SearchTester(object):
+ val = attr.ib(validator=matches_re("a", 0, re.search))
+
+ SearchTester("bab") # re.search will match
+
+ def test_catches_invalid_func(self):
+ """
+ Invalid match functions are caught.
+ """
+ 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]
+ )
+
+ @pytest.mark.parametrize(
+ "func", [None, getattr(re, "fullmatch", None), re.match, re.search]
+ )
+ def test_accepts_all_valid_func(self, func):
+ """
+ Every valid match function is accepted.
+ """
+ matches_re("a", func=func)
+
+ def test_repr(self):
+ """
+ __repr__ is meaningful.
+ """
+ assert repr(matches_re("a")).startswith(
+ "<matches_re validator for pattern"
+ )
+
+
+def always_pass(_, __, ___):
+ """
+ Toy validator that always passes.
+ """
+
+
+def always_fail(_, __, ___):
+ """
+ Toy validator that always fails.
+ """
+ 0 / 0
+
+
+class TestAnd(object):
+ def test_in_all(self):
+ """
+ Verify that this validator is in ``__all__``.
+ """
+ assert and_.__name__ in validator_module.__all__
+
+ def test_success(self):
+ """
+ Succeeds if all wrapped validators succeed.
+ """
+ v = and_(instance_of(int), always_pass)
+
+ v(None, simple_attr("test"), 42)
+
+ def test_fail(self):
+ """
+ Fails if any wrapped validator fails.
+ """
+ v = and_(instance_of(int), always_fail)
+
+ with pytest.raises(ZeroDivisionError):
+ v(None, simple_attr("test"), 42)
+
+ def test_sugar(self):
+ """
+ `and_(v1, v2, v3)` and `[v1, v2, v3]` are equivalent.
+ """
+
+ @attr.s
+ class C(object):
+ a1 = attr.ib("a1", validator=and_(instance_of(int)))
+ a2 = attr.ib("a2", validator=[instance_of(int)])
+
+ assert C.__attrs_attrs__[0].validator == C.__attrs_attrs__[1].validator
+
+
+@pytest.fixture(scope="module")
+def ifoo(zope_interface):
+ """Provides a test ``zope.interface.Interface`` in ``zope`` tests."""
+
+ class IFoo(zope_interface.Interface):
+ """
+ An interface.
+ """
+
+ def f():
+ """
+ A function called f.
+ """
+
+ return IFoo
+
+
+class TestProvides(object):
+ """
+ Tests for `provides`.
+ """
+
+ def test_in_all(self):
+ """
+ Verify that this validator is in ``__all__``.
+ """
+ assert provides.__name__ in validator_module.__all__
+
+ def test_success(self, zope_interface, ifoo):
+ """
+ Nothing happens if value provides requested interface.
+ """
+
+ @zope_interface.implementer(ifoo)
+ class C(object):
+ def f(self):
+ pass
+
+ v = provides(ifoo)
+ v(None, simple_attr("x"), C())
+
+ def test_fail(self, ifoo):
+ """
+ Raises `TypeError` if interfaces isn't provided by value.
+ """
+ value = object()
+ a = simple_attr("x")
+
+ 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
+ ),
+ a,
+ ifoo,
+ value,
+ ) == e.value.args
+
+ def test_repr(self, ifoo):
+ """
+ Returned validator has a useful `__repr__`.
+ """
+ v = provides(ifoo)
+ assert (
+ "<provides validator for interface {interface!r}>".format(
+ interface=ifoo
+ )
+ ) == repr(v)
+
+
+@pytest.mark.parametrize(
+ "validator", [instance_of(int), [always_pass, instance_of(int)]]
+)
+class TestOptional(object):
+ """
+ Tests for `optional`.
+ """
+
+ def test_in_all(self, validator):
+ """
+ Verify that this validator is in ``__all__``.
+ """
+ assert optional.__name__ in validator_module.__all__
+
+ def test_success(self, validator):
+ """
+ Nothing happens if validator succeeds.
+ """
+ v = optional(validator)
+ v(None, simple_attr("test"), 42)
+
+ def test_success_with_none(self, validator):
+ """
+ Nothing happens if None.
+ """
+ v = optional(validator)
+ v(None, simple_attr("test"), None)
+
+ def test_fail(self, validator):
+ """
+ Raises `TypeError` on wrong types.
+ """
+ v = optional(validator)
+ a = simple_attr("test")
+ 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),
+ a,
+ int,
+ "42",
+ ) == e.value.args
+
+ def test_repr(self, validator):
+ """
+ Returned validator has a useful `__repr__`.
+ """
+ v = optional(validator)
+
+ 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)
+ else:
+ repr_s = (
+ "<optional validator for <instance_of validator for type "
+ "<{type} 'int'>> or None>"
+ ).format(type=TYPE)
+
+ assert repr_s == repr(v)
+
+
+class TestIn_(object):
+ """
+ Tests for `in_`.
+ """
+
+ def test_in_all(self):
+ """
+ Verify that this validator is in ``__all__``.
+ """
+ assert in_.__name__ in validator_module.__all__
+
+ def test_success_with_value(self):
+ """
+ If the value is in our options, nothing happens.
+ """
+ v = in_([1, 2, 3])
+ a = simple_attr("test")
+ v(1, a, 3)
+
+ def test_fail(self):
+ """
+ Raise ValueError if the value is outside our options.
+ """
+ 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
+
+ def test_fail_with_string(self):
+ """
+ Raise ValueError if the value is outside our options when the
+ options are specified as a string and the value is not a string.
+ """
+ v = in_("abc")
+ 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
+
+ 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)
+
+
+class TestDeepIterable(object):
+ """
+ Tests for `deep_iterable`.
+ """
+
+ def test_in_all(self):
+ """
+ Verify that this validator is in ``__all__``.
+ """
+ assert deep_iterable.__name__ in validator_module.__all__
+
+ def test_success_member_only(self):
+ """
+ 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):
+ """
+ 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",
+ (
+ (instance_of(int), 42),
+ (42, instance_of(list)),
+ (42, 42),
+ (42, None),
+ ),
+ )
+ def test_noncallable_validators(
+ self, member_validator, iterable_validator
+ ):
+ """
+ Raise `TypeError` if any validators are not callable.
+ """
+ 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__
+ )
+
+ assert message in e.value.args[0]
+ assert value == e.value.args[1]
+ assert message in e.value.msg
+ assert value == e.value.value
+
+ def test_fail_invalid_member(self):
+ """
+ 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):
+ """
+ Raise iterable validator error if an invalid iterable is found.
+ """
+ member_validator = instance_of(int)
+ iterable_validator = instance_of(tuple)
+ v = deep_iterable(member_validator, iterable_validator)
+ a = simple_attr("test")
+ with pytest.raises(TypeError):
+ v(None, a, [42])
+
+ def test_fail_invalid_member_and_iterable(self):
+ """
+ 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")
+ with pytest.raises(TypeError):
+ v(None, a, [42, "42"])
+
+ def test_repr_member_only(self):
+ """
+ Returned validator has a useful `__repr__`
+ when only member validator is set.
+ """
+ member_validator = instance_of(int)
+ member_repr = "<instance_of validator for type <{type} 'int'>>".format(
+ type=TYPE
+ )
+ 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)
+
+ def test_repr_member_and_iterable(self):
+ """
+ Returned validator has a useful `__repr__` when both member
+ and iterable validators are set.
+ """
+ member_validator = instance_of(int)
+ member_repr = "<instance_of validator for type <{type} 'int'>>".format(
+ type=TYPE
+ )
+ iterable_validator = instance_of(list)
+ iterable_repr = (
+ "<instance_of validator for type <{type} 'list'>>"
+ ).format(type=TYPE)
+ 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)
+ assert expected_repr == repr(v)
+
+
+class TestDeepMapping(object):
+ """
+ Tests for `deep_mapping`.
+ """
+
+ def test_in_all(self):
+ """
+ Verify that this validator is in ``__all__``.
+ """
+ assert deep_mapping.__name__ in validator_module.__all__
+
+ def test_success(self):
+ """
+ If both the key and value validators succeed, nothing happens.
+ """
+ key_validator = instance_of(str)
+ value_validator = instance_of(int)
+ v = deep_mapping(key_validator, value_validator)
+ a = simple_attr("test")
+ v(None, a, {"a": 6, "b": 7})
+
+ @pytest.mark.parametrize(
+ "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
+ ):
+ """
+ Raise `TypeError` if any validators are not callable.
+ """
+ with pytest.raises(TypeError) as e:
+ 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__
+ )
+
+ assert message in e.value.args[0]
+ assert value == e.value.args[1]
+ assert message in e.value.msg
+ assert value == e.value.value
+
+ def test_fail_invalid_mapping(self):
+ """
+ Raise `TypeError` if mapping validator fails.
+ """
+ key_validator = instance_of(str)
+ value_validator = instance_of(int)
+ mapping_validator = instance_of(dict)
+ v = deep_mapping(key_validator, value_validator, mapping_validator)
+ a = simple_attr("test")
+ with pytest.raises(TypeError):
+ v(None, a, None)
+
+ def test_fail_invalid_key(self):
+ """
+ Raise key validator error if an invalid key is found.
+ """
+ key_validator = instance_of(str)
+ value_validator = instance_of(int)
+ v = deep_mapping(key_validator, value_validator)
+ a = simple_attr("test")
+ with pytest.raises(TypeError):
+ v(None, a, {"a": 6, 42: 7})
+
+ def test_fail_invalid_member(self):
+ """
+ Raise key validator error if an invalid member value is found.
+ """
+ key_validator = instance_of(str)
+ value_validator = instance_of(int)
+ v = deep_mapping(key_validator, value_validator)
+ a = simple_attr("test")
+ with pytest.raises(TypeError):
+ v(None, a, {"a": "6", "b": 7})
+
+ def test_repr(self):
+ """
+ Returned validator has a useful `__repr__`.
+ """
+ key_validator = instance_of(str)
+ key_repr = "<instance_of validator for type <{type} 'str'>>".format(
+ type=TYPE
+ )
+ value_validator = instance_of(int)
+ value_repr = "<instance_of validator for type <{type} 'int'>>".format(
+ type=TYPE
+ )
+ 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)
+ assert expected_repr == repr(v)
+
+
+class TestIsCallable(object):
+ """
+ Tests for `is_callable`.
+ """
+
+ def test_in_all(self):
+ """
+ Verify that this validator is in ``__all__``.
+ """
+ assert is_callable.__name__ in validator_module.__all__
+
+ def test_success(self):
+ """
+ If the value is callable, nothing happens.
+ """
+ v = is_callable()
+ a = simple_attr("test")
+ v(None, a, isinstance)
+
+ def test_fail(self):
+ """
+ Raise TypeError if the value is not callable.
+ """
+ v = is_callable()
+ a = simple_attr("test")
+ with pytest.raises(TypeError) as e:
+ v(None, a, None)
+
+ value = None
+ message = "'test' must be callable (got {value} that is a {type_})."
+ expected_message = message.format(value=value, type_=value.__class__)
+
+ assert expected_message == e.value.args[0]
+ assert value == e.value.args[1]
+ assert expected_message == e.value.msg
+ assert value == e.value.value
+
+ def test_repr(self):
+ """
+ Returned validator has a useful `__repr__`.
+ """
+ v = is_callable()
+ assert "<is_callable validator>" == repr(v)
+
+ def test_exception_repr(self):
+ """
+ Verify that NotCallableError exception has a useful `__str__`.
+ """
+ from attr.exceptions import NotCallableError
+
+ instance = NotCallableError(msg="Some Message", value=42)
+ assert "Some Message" == str(instance)
+
+
+def test_hashability():
+ """
+ Validator classes are hashable.
+ """
+ for obj_name in dir(validator_module):
+ obj = getattr(validator_module, obj_name)
+ if not has(obj):
+ continue
+ hash_func = getattr(obj, "__hash__", None)
+ assert hash_func is not None
+ assert hash_func is not object.__hash__
+
+
+class TestLtLeGeGt:
+ """
+ Tests for `max_len`.
+ """
+
+ BOUND = 4
+
+ def test_in_all(self):
+ """
+ validator is in ``__all__``.
+ """
+ assert all(
+ f.__name__ in validator_module.__all__ for f in [lt, le, ge, gt]
+ )
+
+ @pytest.mark.parametrize("v", [lt, le, ge, gt])
+ def test_retrieve_bound(self, v):
+ """
+ The configured bound for the comparison can be extracted from the
+ Attribute.
+ """
+
+ @attr.s
+ class Tester(object):
+ value = attr.ib(validator=v(self.BOUND))
+
+ assert fields(Tester).value.validator.bound == self.BOUND
+
+ @pytest.mark.parametrize(
+ "v, value",
+ [
+ (lt, 3),
+ (le, 3),
+ (le, 4),
+ (ge, 4),
+ (ge, 5),
+ (gt, 5),
+ ],
+ )
+ def test_check_valid(self, v, value):
+ """Silent if value {op} bound."""
+
+ @attr.s
+ class Tester(object):
+ value = attr.ib(validator=v(self.BOUND))
+
+ Tester(value) # shouldn't raise exceptions
+
+ @pytest.mark.parametrize(
+ "v, value",
+ [
+ (lt, 4),
+ (le, 5),
+ (ge, 3),
+ (gt, 4),
+ ],
+ )
+ def test_check_invalid(self, v, value):
+ """Raise ValueError if value {op} bound."""
+
+ @attr.s
+ class Tester(object):
+ value = attr.ib(validator=v(self.BOUND))
+
+ with pytest.raises(ValueError):
+ Tester(value)
+
+ @pytest.mark.parametrize("v", [lt, le, ge, gt])
+ def test_repr(self, v):
+ """
+ __repr__ is meaningful.
+ """
+ nv = v(23)
+ assert repr(nv) == "<Validator for x {op} {bound}>".format(
+ op=nv.compare_op, bound=23
+ )
+
+
+class TestMaxLen:
+ """
+ Tests for `max_len`.
+ """
+
+ MAX_LENGTH = 4
+
+ def test_in_all(self):
+ """
+ validator is in ``__all__``.
+ """
+ assert max_len.__name__ in validator_module.__all__
+
+ def test_retrieve_max_len(self):
+ """
+ The configured max. length can be extracted from the Attribute
+ """
+
+ @attr.s
+ class Tester(object):
+ value = attr.ib(validator=max_len(self.MAX_LENGTH))
+
+ assert fields(Tester).value.validator.max_length == self.MAX_LENGTH
+
+ @pytest.mark.parametrize(
+ "value",
+ [
+ "",
+ "foo",
+ "spam",
+ [],
+ list(range(MAX_LENGTH)),
+ {"spam": 3, "eggs": 4},
+ ],
+ )
+ def test_check_valid(self, value):
+ """
+ Silent if len(value) <= max_len.
+ Values can be strings and other iterables.
+ """
+
+ @attr.s
+ class Tester(object):
+ value = attr.ib(validator=max_len(self.MAX_LENGTH))
+
+ Tester(value) # shouldn't raise exceptions
+
+ @pytest.mark.parametrize(
+ "value",
+ [
+ "bacon",
+ list(range(6)),
+ ],
+ )
+ def test_check_invalid(self, value):
+ """
+ Raise ValueError if len(value) > max_len.
+ """
+
+ @attr.s
+ class Tester(object):
+ value = attr.ib(validator=max_len(self.MAX_LENGTH))
+
+ with pytest.raises(ValueError):
+ Tester(value)
+
+ def test_repr(self):
+ """
+ __repr__ is meaningful.
+ """
+ assert repr(max_len(23)) == "<max_len validator for 23>"
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/test_version_info.py b/testing/web-platform/tests/tools/third_party/attrs/tests/test_version_info.py
new file mode 100644
index 0000000000..41f75f47a6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/test_version_info.py
@@ -0,0 +1,62 @@
+# 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")
+def fixture_vi():
+ return VersionInfo(19, 2, 0, "final")
+
+
+class TestVersionInfo:
+ def test_from_string_no_releaselevel(self, vi):
+ """
+ If there is no suffix, the releaselevel becomes "final" by default.
+ """
+ assert vi == VersionInfo._from_version_string("19.2.0")
+
+ def test_suffix_is_preserved(self):
+ """
+ If there is a suffix, it's preserved.
+ """
+ assert (
+ "dev0"
+ == 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):
+ """
+ Comparing with a tuple that has the wrong length raises an error.
+ """
+ assert vi != other
+
+ with pytest.raises(TypeError):
+ vi < other
+
+ @pytest.mark.parametrize("other", [[19, 2, 0, "final"]])
+ def test_wrong_type(self, vi, other):
+ """
+ Only compare to other VersionInfos or tuples.
+ """
+ assert vi != other
+
+ def test_order(self, vi):
+ """
+ Ordering works as expected.
+ """
+ assert vi < (20,)
+ assert vi < (19, 2, 1)
+ assert vi > (0,)
+ assert vi <= (19, 2)
+ assert vi >= (19, 2)
+ assert vi > (19, 2, 0, "dev0")
+ assert vi < (19, 2, 0, "post1")
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/typing_example.py b/testing/web-platform/tests/tools/third_party/attrs/tests/typing_example.py
new file mode 100644
index 0000000000..a85c768c10
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/typing_example.py
@@ -0,0 +1,420 @@
+# SPDX-License-Identifier: MIT
+
+import re
+
+from typing import Any, Dict, List, Tuple, Union
+
+import attr
+import attrs
+
+
+# Typing via "type" Argument ---
+
+
+@attr.s
+class C:
+ a = attr.ib(type=int)
+
+
+c = C(1)
+C(a=1)
+
+
+@attr.s
+class D:
+ x = attr.ib(type=List[int])
+
+
+@attr.s
+class E:
+ y = attr.ib(type="List[int]")
+
+
+@attr.s
+class F:
+ z = attr.ib(type=Any)
+
+
+# Typing via Annotations ---
+
+
+@attr.s
+class CC:
+ a: int = attr.ib()
+
+
+cc = CC(1)
+CC(a=1)
+
+
+@attr.s
+class DD:
+ x: List[int] = attr.ib()
+
+
+@attr.s
+class EE:
+ y: "List[int]" = attr.ib()
+
+
+@attr.s
+class FF:
+ z: Any = attr.ib()
+
+
+@attrs.define
+class FFF:
+ z: int
+
+
+FFF(1)
+
+
+# Inheritance --
+
+
+@attr.s
+class GG(DD):
+ y: str = attr.ib()
+
+
+GG(x=[1], y="foo")
+
+
+@attr.s
+class HH(DD, EE):
+ z: float = attr.ib()
+
+
+HH(x=[1], y=[], z=1.1)
+
+
+# same class
+c == cc
+
+
+# Exceptions
+@attr.s(auto_exc=True)
+class Error(Exception):
+ x: int = attr.ib()
+
+
+try:
+ raise Error(1)
+except Error as e:
+ e.x
+ e.args
+ str(e)
+
+
+@attrs.define
+class Error2(Exception):
+ x: int
+
+
+try:
+ raise Error2(1)
+except Error as e:
+ e.x
+ e.args
+ str(e)
+
+
+# Converters
+# XXX: Currently converters can only be functions so none of this works
+# although the stubs should be correct.
+
+# @attr.s
+# class ConvCOptional:
+# x: Optional[int] = attr.ib(converter=attr.converters.optional(int))
+
+
+# ConvCOptional(1)
+# ConvCOptional(None)
+
+
+# @attr.s
+# class ConvCDefaultIfNone:
+# x: int = attr.ib(converter=attr.converters.default_if_none(42))
+
+
+# ConvCDefaultIfNone(1)
+# ConvCDefaultIfNone(None)
+
+
+# @attr.s
+# class ConvCToBool:
+# x: int = attr.ib(converter=attr.converters.to_bool)
+
+
+# ConvCToBool(1)
+# ConvCToBool(True)
+# ConvCToBool("on")
+# ConvCToBool("yes")
+# ConvCToBool(0)
+# ConvCToBool(False)
+# ConvCToBool("n")
+
+
+# Validators
+@attr.s
+class Validated:
+ a = attr.ib(
+ type=List[C],
+ validator=attr.validators.deep_iterable(
+ attr.validators.instance_of(C), attr.validators.instance_of(list)
+ ),
+ )
+ a = attr.ib(
+ type=Tuple[C],
+ validator=attr.validators.deep_iterable(
+ attr.validators.instance_of(C), attr.validators.instance_of(tuple)
+ ),
+ )
+ b = attr.ib(
+ type=List[C],
+ validator=attr.validators.deep_iterable(
+ attr.validators.instance_of(C)
+ ),
+ )
+ c = attr.ib(
+ type=Dict[C, D],
+ validator=attr.validators.deep_mapping(
+ attr.validators.instance_of(C),
+ attr.validators.instance_of(D),
+ attr.validators.instance_of(dict),
+ ),
+ )
+ d = attr.ib(
+ type=Dict[C, D],
+ validator=attr.validators.deep_mapping(
+ attr.validators.instance_of(C), attr.validators.instance_of(D)
+ ),
+ )
+ e: str = attr.ib(validator=attr.validators.matches_re(re.compile(r"foo")))
+ f: str = attr.ib(
+ validator=attr.validators.matches_re(r"foo", flags=42, func=re.search)
+ )
+
+ # 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(
+ validator=attrs.validators.instance_of((int, C, str))
+ )
+
+
+@attr.define
+class Validated2:
+ num: int = attr.field(validator=attr.validators.ge(0))
+
+
+@attrs.define
+class Validated3:
+ num: int = attr.field(validator=attr.validators.ge(0))
+
+
+with attr.validators.disabled():
+ Validated2(num=-1)
+
+with attrs.validators.disabled():
+ Validated3(num=-1)
+
+try:
+ attr.validators.set_disabled(True)
+ Validated2(num=-1)
+finally:
+ attr.validators.set_disabled(False)
+
+
+# Custom repr()
+@attr.s
+class WithCustomRepr:
+ a: int = attr.ib(repr=True)
+ b: str = attr.ib(repr=False)
+ c: str = attr.ib(repr=lambda value: "c is for cookie")
+ d: bool = attr.ib(repr=str)
+
+
+@attrs.define
+class WithCustomRepr2:
+ a: int = attrs.field(repr=True)
+ b: str = attrs.field(repr=False)
+ c: str = attrs.field(repr=lambda value: "c is for cookie")
+ d: bool = attrs.field(repr=str)
+
+
+# Check some of our own types
+@attr.s(eq=True, order=False)
+class OrderFlags:
+ a: int = attr.ib(eq=False, order=False)
+ b: int = attr.ib(eq=True, order=True)
+
+
+# on_setattr hooks
+@attr.s(on_setattr=attr.setters.validate)
+class ValidatedSetter:
+ a: int
+ b: str = attr.ib(on_setattr=attr.setters.NO_OP)
+ c: bool = attr.ib(on_setattr=attr.setters.frozen)
+ d: int = attr.ib(on_setattr=[attr.setters.convert, attr.setters.validate])
+ e: bool = attr.ib(
+ on_setattr=attr.setters.pipe(
+ attr.setters.convert, attr.setters.validate
+ )
+ )
+
+
+@attrs.define(on_setattr=attr.setters.validate)
+class ValidatedSetter2:
+ a: int
+ b: str = attrs.field(on_setattr=attrs.setters.NO_OP)
+ c: bool = attrs.field(on_setattr=attrs.setters.frozen)
+ d: int = attrs.field(
+ on_setattr=[attrs.setters.convert, attrs.setters.validate]
+ )
+ e: bool = attrs.field(
+ on_setattr=attrs.setters.pipe(
+ attrs.setters.convert, attrs.setters.validate
+ )
+ )
+
+
+# field_transformer
+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]:
+ return attribs
+
+
+@attr.s(field_transformer=ft_hook)
+class TransformedAttrs:
+ x: int
+
+
+@attrs.define(field_transformer=ft_hook2)
+class TransformedAttrs2:
+ x: int
+
+
+# Auto-detect
+@attr.s(auto_detect=True)
+class AutoDetect:
+ x: int
+
+ def __init__(self, x: int):
+ self.x = x
+
+
+# Provisional APIs
+@attr.define(order=True)
+class NGClass:
+ x: int = attr.field(default=42)
+
+
+ngc = NGClass(1)
+
+
+@attr.mutable(slots=False)
+class NGClass2:
+ x: int
+
+
+ngc2 = NGClass2(1)
+
+
+@attr.frozen(str=True)
+class NGFrozen:
+ x: int
+
+
+ngf = NGFrozen(1)
+
+attr.fields(NGFrozen).x.evolve(eq=False)
+a = attr.fields(NGFrozen).x
+a.evolve(repr=False)
+
+
+attrs.fields(NGFrozen).x.evolve(eq=False)
+a = attrs.fields(NGFrozen).x
+a.evolve(repr=False)
+
+
+@attr.s(collect_by_mro=True)
+class MRO:
+ pass
+
+
+@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))
+
+
+@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))
+
+
+attrs.asdict(FactoryTest2())
+attr.asdict(FactoryTest(), tuple_keys=True)
+
+
+# Check match_args stub
+@attr.s(match_args=False)
+class MatchArgs:
+ a: int = attr.ib()
+ b: int = attr.ib()
+
+
+attr.asdict(FactoryTest())
+attr.asdict(FactoryTest(), retain_collection_types=False)
+
+
+# Check match_args stub
+@attrs.define(match_args=False)
+class MatchArgs2:
+ a: int
+ b: int
+
+
+# NG versions of asdict/astuple
+attrs.asdict(MatchArgs2(1, 2))
+attrs.astuple(MatchArgs2(1, 2))
+
+
+def importing_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_
+
+
+def importing_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_
+
+ assert optional and FrozenError and include and frozen and and_
diff --git a/testing/web-platform/tests/tools/third_party/attrs/tests/utils.py b/testing/web-platform/tests/tools/third_party/attrs/tests/utils.py
new file mode 100644
index 0000000000..a2fefbd606
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tests/utils.py
@@ -0,0 +1,86 @@
+# SPDX-License-Identifier: MIT
+
+"""
+Common helper functions for tests.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+from attr import Attribute
+from attr._make import NOTHING, make_class
+
+
+def simple_class(
+ eq=False,
+ order=False,
+ repr=False,
+ hash=False,
+ str=False,
+ slots=False,
+ frozen=False,
+ cache_hash=False,
+):
+ """
+ Return a new simple class.
+ """
+ return make_class(
+ "C",
+ ["a", "b"],
+ eq=eq or order,
+ order=order,
+ repr=repr,
+ hash=hash,
+ init=True,
+ slots=slots,
+ str=str,
+ frozen=frozen,
+ cache_hash=cache_hash,
+ )
+
+
+def simple_attr(
+ name,
+ default=NOTHING,
+ validator=None,
+ repr=True,
+ eq=True,
+ hash=None,
+ init=True,
+ converter=None,
+ kw_only=False,
+ inherited=False,
+):
+ """
+ Return an attribute with a name and no other bells and whistles.
+ """
+ return Attribute(
+ name=name,
+ default=default,
+ validator=validator,
+ repr=repr,
+ cmp=None,
+ eq=eq,
+ hash=hash,
+ init=init,
+ converter=converter,
+ kw_only=kw_only,
+ inherited=inherited,
+ )
+
+
+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/testing/web-platform/tests/tools/third_party/attrs/tox.ini b/testing/web-platform/tests/tools/third_party/attrs/tox.ini
new file mode 100644
index 0000000000..ddcbc4dbbc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/attrs/tox.ini
@@ -0,0 +1,129 @@
+[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
+
+
+[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]
+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}
+
+
+[testenv:py310]
+# 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
+extras = tests
+commands = coverage run -m pytest {posargs}
+
+
+[testenv:coverage-report]
+basepython = python3.10
+depends = py27,py37,py310
+skip_install = true
+deps = coverage[toml]>=5.4
+commands =
+ coverage combine
+ coverage report
+
+
+[testenv:pre-commit]
+basepython = python3.10
+skip_install = true
+deps =
+ pre-commit
+passenv = HOMEPATH # needed on Windows
+commands =
+ pre-commit run --all-files
+
+
+[testenv:manifest]
+basepython = python3.10
+deps = check-manifest
+skip_install = true
+commands = check-manifest
+
+
+[testenv:pypi-description]
+basepython = python3.8
+skip_install = true
+deps =
+ twine
+ pip >= 18.0.0
+commands =
+ pip wheel -w {envtmpdir}/build --no-deps .
+ twine check {envtmpdir}/build/*
+
+
+[testenv:changelog]
+basepython = python3.8
+deps = towncrier<21.3
+skip_install = true
+commands = towncrier --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]
+# 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
+commands =
+ nodeenv --prebuilt --node=lts --force {envdir}
+ npm install -g --no-package-lock --no-save pyright
+ pytest tests/test_pyright.py -vv
diff --git a/testing/web-platform/tests/tools/third_party/certifi/LICENSE b/testing/web-platform/tests/tools/third_party/certifi/LICENSE
new file mode 100644
index 0000000000..802b53ff11
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/certifi/LICENSE
@@ -0,0 +1,21 @@
+This packge contains a modified version of ca-bundle.crt:
+
+ca-bundle.crt -- Bundle of CA Root Certificates
+
+Certificate data from Mozilla as of: Thu Nov 3 19:04:19 2011#
+This is a bundle of X.509 certificates of public Certificate Authorities
+(CA). These were automatically extracted from Mozilla's root certificates
+file (certdata.txt). This file can be found in the mozilla source tree:
+http://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1#
+It contains the certificates in PEM format and therefore
+can be directly used with curl / libcurl / php_curl, or with
+an Apache+mod_ssl webserver for SSL client authentication.
+Just configure this file as the SSLCACertificateFile.#
+
+***** BEGIN LICENSE BLOCK *****
+This Source Code Form is subject to the terms of the Mozilla Public License,
+v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
+one at http://mozilla.org/MPL/2.0/.
+
+***** END LICENSE BLOCK *****
+@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $
diff --git a/testing/web-platform/tests/tools/third_party/certifi/MANIFEST.in b/testing/web-platform/tests/tools/third_party/certifi/MANIFEST.in
new file mode 100644
index 0000000000..6077b5ff84
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/certifi/MANIFEST.in
@@ -0,0 +1 @@
+include MANIFEST.in README.rst LICENSE certifi/cacert.pem
diff --git a/testing/web-platform/tests/tools/third_party/certifi/PKG-INFO b/testing/web-platform/tests/tools/third_party/certifi/PKG-INFO
new file mode 100644
index 0000000000..73f3643804
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/certifi/PKG-INFO
@@ -0,0 +1,69 @@
+Metadata-Version: 1.1
+Name: certifi
+Version: 2018.4.16
+Summary: Python package for providing Mozilla's CA Bundle.
+Home-page: http://certifi.io/
+Author: Kenneth Reitz
+Author-email: me@kennethreitz.com
+License: MPL-2.0
+Description: Certifi: Python SSL Certificates
+ ================================
+
+ `Certifi`_ is a carefully curated collection of Root Certificates for
+ validating the trustworthiness of SSL certificates while verifying the identity
+ of TLS hosts. It has been extracted from the `Requests`_ project.
+
+ Installation
+ ------------
+
+ ``certifi`` is available on PyPI. Simply install it with ``pip``::
+
+ $ pip install certifi
+
+ Usage
+ -----
+
+ To reference the installed certificate authority (CA) bundle, you can use the
+ built-in function::
+
+ >>> import certifi
+
+ >>> certifi.where()
+ '/usr/local/lib/python2.7/site-packages/certifi/cacert.pem'
+
+ Enjoy!
+
+ 1024-bit Root Certificates
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Browsers and certificate authorities have concluded that 1024-bit keys are
+ unacceptably weak for certificates, particularly root certificates. For this
+ reason, Mozilla has removed any weak (i.e. 1024-bit key) certificate from its
+ bundle, replacing it with an equivalent strong (i.e. 2048-bit or greater key)
+ certificate from the same CA. Because Mozilla removed these certificates from
+ its bundle, ``certifi`` removed them as well.
+
+ In previous versions, ``certifi`` provided the ``certifi.old_where()`` function
+ to intentionally re-add the 1024-bit roots back into your bundle. This was not
+ recommended in production and therefore was removed. To assist in migrating old
+ code, the function ``certifi.old_where()`` continues to exist as an alias of
+ ``certifi.where()``. Please update your code to use ``certifi.where()``
+ instead. ``certifi.old_where()`` will be removed in 2018.
+
+ .. _`Certifi`: http://certifi.io/en/latest/
+ .. _`Requests`: http://docs.python-requests.org/en/latest/
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
+Classifier: Natural Language :: English
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
diff --git a/testing/web-platform/tests/tools/third_party/certifi/README.rst b/testing/web-platform/tests/tools/third_party/certifi/README.rst
new file mode 100644
index 0000000000..64b3e38e10
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/certifi/README.rst
@@ -0,0 +1,46 @@
+Certifi: Python SSL Certificates
+================================
+
+`Certifi`_ is a carefully curated collection of Root Certificates for
+validating the trustworthiness of SSL certificates while verifying the identity
+of TLS hosts. It has been extracted from the `Requests`_ project.
+
+Installation
+------------
+
+``certifi`` is available on PyPI. Simply install it with ``pip``::
+
+ $ pip install certifi
+
+Usage
+-----
+
+To reference the installed certificate authority (CA) bundle, you can use the
+built-in function::
+
+ >>> import certifi
+
+ >>> certifi.where()
+ '/usr/local/lib/python2.7/site-packages/certifi/cacert.pem'
+
+Enjoy!
+
+1024-bit Root Certificates
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Browsers and certificate authorities have concluded that 1024-bit keys are
+unacceptably weak for certificates, particularly root certificates. For this
+reason, Mozilla has removed any weak (i.e. 1024-bit key) certificate from its
+bundle, replacing it with an equivalent strong (i.e. 2048-bit or greater key)
+certificate from the same CA. Because Mozilla removed these certificates from
+its bundle, ``certifi`` removed them as well.
+
+In previous versions, ``certifi`` provided the ``certifi.old_where()`` function
+to intentionally re-add the 1024-bit roots back into your bundle. This was not
+recommended in production and therefore was removed. To assist in migrating old
+code, the function ``certifi.old_where()`` continues to exist as an alias of
+``certifi.where()``. Please update your code to use ``certifi.where()``
+instead. ``certifi.old_where()`` will be removed in 2018.
+
+.. _`Certifi`: http://certifi.io/en/latest/
+.. _`Requests`: http://docs.python-requests.org/en/latest/
diff --git a/testing/web-platform/tests/tools/third_party/certifi/certifi/__init__.py b/testing/web-platform/tests/tools/third_party/certifi/certifi/__init__.py
new file mode 100644
index 0000000000..0c4963ef60
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/certifi/certifi/__init__.py
@@ -0,0 +1,3 @@
+from .core import where, old_where
+
+__version__ = "2018.04.16"
diff --git a/testing/web-platform/tests/tools/third_party/certifi/certifi/__main__.py b/testing/web-platform/tests/tools/third_party/certifi/certifi/__main__.py
new file mode 100644
index 0000000000..5f1da0dd0c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/certifi/certifi/__main__.py
@@ -0,0 +1,2 @@
+from certifi import where
+print(where())
diff --git a/testing/web-platform/tests/tools/third_party/certifi/certifi/cacert.pem b/testing/web-platform/tests/tools/third_party/certifi/certifi/cacert.pem
new file mode 100644
index 0000000000..2713f541c4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/certifi/certifi/cacert.pem
@@ -0,0 +1,4400 @@
+
+# Issuer: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA
+# Subject: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA
+# Label: "GlobalSign Root CA"
+# Serial: 4835703278459707669005204
+# MD5 Fingerprint: 3e:45:52:15:09:51:92:e1:b7:5d:37:9f:b1:87:29:8a
+# SHA1 Fingerprint: b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c
+# SHA256 Fingerprint: eb:d4:10:40:e4:bb:3e:c7:42:c9:e3:81:d3:1e:f2:a4:1a:48:b6:68:5c:96:e7:ce:f3:c1:df:6c:d4:33:1c:99
+-----BEGIN CERTIFICATE-----
+MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG
+A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv
+b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw
+MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i
+YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT
+aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ
+jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp
+xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp
+1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG
+snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ
+U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8
+9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E
+BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B
+AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz
+yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE
+38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP
+AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad
+DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME
+HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
+-----END CERTIFICATE-----
+
+# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2
+# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2
+# Label: "GlobalSign Root CA - R2"
+# Serial: 4835703278459682885658125
+# MD5 Fingerprint: 94:14:77:7e:3e:5e:fd:8f:30:bd:41:b0:cf:e7:d0:30
+# SHA1 Fingerprint: 75:e0:ab:b6:13:85:12:27:1c:04:f8:5f:dd:de:38:e4:b7:24:2e:fe
+# SHA256 Fingerprint: ca:42:dd:41:74:5f:d0:b8:1e:b9:02:36:2c:f9:d8:bf:71:9d:a1:bd:1b:1e:fc:94:6f:5b:4c:99:f4:2c:1b:9e
+-----BEGIN CERTIFICATE-----
+MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
+A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
+Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
+MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
+A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
+v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
+eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
+tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
+C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
+zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
+mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
+V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
+bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
+3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
+J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
+291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
+ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
+AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
+TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
+-----END CERTIFICATE-----
+
+# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only
+# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only
+# Label: "Verisign Class 3 Public Primary Certification Authority - G3"
+# Serial: 206684696279472310254277870180966723415
+# MD5 Fingerprint: cd:68:b6:a7:c7:c4:ce:75:e0:1d:4f:57:44:61:92:09
+# SHA1 Fingerprint: 13:2d:0d:45:53:4b:69:97:cd:b2:d5:c3:39:e2:55:76:60:9b:5c:c6
+# SHA256 Fingerprint: eb:04:cf:5e:b1:f3:9a:fa:76:2f:2b:b1:20:f2:96:cb:a5:20:c1:b9:7d:b1:58:95:65:b8:1c:b9:a1:7b:72:44
+-----BEGIN CERTIFICATE-----
+MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw
+CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl
+cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu
+LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT
+aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
+dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD
+VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT
+aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ
+bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu
+IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
+LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b
+N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t
+KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu
+kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm
+CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ
+Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu
+imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te
+2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe
+DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC
+/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p
+F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt
+TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ==
+-----END CERTIFICATE-----
+
+# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited
+# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited
+# Label: "Entrust.net Premium 2048 Secure Server CA"
+# Serial: 946069240
+# MD5 Fingerprint: ee:29:31:bc:32:7e:9a:e6:e8:b5:f7:51:b4:34:71:90
+# SHA1 Fingerprint: 50:30:06:09:1d:97:d4:f5:ae:39:f7:cb:e7:92:7d:7d:65:2d:34:31
+# SHA256 Fingerprint: 6d:c4:71:72:e0:1c:bc:b0:bf:62:58:0d:89:5f:e2:b8:ac:9a:d4:f8:73:80:1e:0c:10:b9:c8:37:d2:1e:b1:77
+-----BEGIN CERTIFICATE-----
+MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML
+RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp
+bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5
+IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp
+ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3
+MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3
+LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp
+YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG
+A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq
+K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe
+sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX
+MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT
+XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/
+HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH
+4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
+HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub
+j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo
+U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf
+zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b
+u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+
+bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er
+fF6adulZkMV8gzURZVE=
+-----END CERTIFICATE-----
+
+# Issuer: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust
+# Subject: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust
+# Label: "Baltimore CyberTrust Root"
+# Serial: 33554617
+# MD5 Fingerprint: ac:b6:94:a5:9c:17:e0:d7:91:52:9b:b1:97:06:a6:e4
+# SHA1 Fingerprint: d4:de:20:d0:5e:66:fc:53:fe:1a:50:88:2c:78:db:28:52:ca:e4:74
+# SHA256 Fingerprint: 16:af:57:a9:f6:76:b0:ab:12:60:95:aa:5e:ba:de:f2:2a:b3:11:19:d6:44:ac:95:cd:4b:93:db:f3:f2:6a:eb
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ
+RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD
+VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX
+DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y
+ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy
+VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr
+mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr
+IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK
+mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu
+XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy
+dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye
+jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1
+BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3
+DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92
+9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx
+jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0
+Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz
+ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS
+R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
+-----END CERTIFICATE-----
+
+# Issuer: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network
+# Subject: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network
+# Label: "AddTrust External Root"
+# Serial: 1
+# MD5 Fingerprint: 1d:35:54:04:85:78:b0:3f:42:42:4d:bf:20:73:0a:3f
+# SHA1 Fingerprint: 02:fa:f3:e2:91:43:54:68:60:78:57:69:4d:f5:e4:5b:68:85:18:68
+# SHA256 Fingerprint: 68:7f:a4:51:38:22:78:ff:f0:c8:b1:1f:8d:43:d5:76:67:1c:6e:b2:bc:ea:b4:13:fb:83:d9:65:d0:6d:2f:f2
+-----BEGIN CERTIFICATE-----
+MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU
+MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs
+IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290
+MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux
+FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h
+bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v
+dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt
+H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9
+uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX
+mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX
+a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN
+E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0
+WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD
+VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0
+Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU
+cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx
+IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN
+AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH
+YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
+6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC
+Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
+c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
+mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
+-----END CERTIFICATE-----
+
+# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.
+# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.
+# Label: "Entrust Root Certification Authority"
+# Serial: 1164660820
+# MD5 Fingerprint: d6:a5:c3:ed:5d:dd:3e:00:c1:3d:87:92:1f:1d:3f:e4
+# SHA1 Fingerprint: b3:1e:b1:b7:40:e3:6c:84:02:da:dc:37:d4:4d:f5:d4:67:49:52:f9
+# SHA256 Fingerprint: 73:c1:76:43:4f:1b:c6:d5:ad:f4:5b:0e:76:e7:27:28:7c:8d:e5:76:16:c1:e6:e6:14:1a:2b:2c:bc:7d:8e:4c
+-----BEGIN CERTIFICATE-----
+MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC
+VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0
+Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW
+KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl
+cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw
+NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw
+NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy
+ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV
+BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo
+Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4
+4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9
+KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI
+rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi
+94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB
+sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi
+gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo
+kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE
+vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA
+A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t
+O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua
+AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP
+9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/
+eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m
+0vdXcDazv/wor3ElhVsT/h5/WrQ8
+-----END CERTIFICATE-----
+
+# Issuer: CN=GeoTrust Global CA O=GeoTrust Inc.
+# Subject: CN=GeoTrust Global CA O=GeoTrust Inc.
+# Label: "GeoTrust Global CA"
+# Serial: 144470
+# MD5 Fingerprint: f7:75:ab:29:fb:51:4e:b7:77:5e:ff:05:3c:99:8e:f5
+# SHA1 Fingerprint: de:28:f4:a4:ff:e5:b9:2f:a3:c5:03:d1:a3:49:a7:f9:96:2a:82:12
+# SHA256 Fingerprint: ff:85:6a:2d:25:1d:cd:88:d3:66:56:f4:50:12:67:98:cf:ab:aa:de:40:79:9c:72:2d:e4:d2:b5:db:36:a7:3a
+-----BEGIN CERTIFICATE-----
+MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT
+MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i
+YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG
+EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg
+R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9
+9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq
+fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv
+iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU
+1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+
+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW
+MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA
+ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l
+uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn
+Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS
+tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF
+PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un
+hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV
+5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw==
+-----END CERTIFICATE-----
+
+# Issuer: CN=GeoTrust Universal CA O=GeoTrust Inc.
+# Subject: CN=GeoTrust Universal CA O=GeoTrust Inc.
+# Label: "GeoTrust Universal CA"
+# Serial: 1
+# MD5 Fingerprint: 92:65:58:8b:a2:1a:31:72:73:68:5c:b4:a5:7a:07:48
+# SHA1 Fingerprint: e6:21:f3:35:43:79:05:9a:4b:68:30:9d:8a:2f:74:22:15:87:ec:79
+# SHA256 Fingerprint: a0:45:9b:9f:63:b2:25:59:f5:fa:5d:4c:6d:b3:f9:f7:2f:f1:93:42:03:35:78:f0:73:bf:1d:1b:46:cb:b9:12
+-----BEGIN CERTIFICATE-----
+MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW
+MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy
+c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE
+BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0
+IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV
+VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8
+cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT
+QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh
+F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v
+c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w
+mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd
+VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX
+teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ
+f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe
+Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+
+nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB
+/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY
+MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG
+9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc
+aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX
+IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn
+ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z
+uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN
+Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja
+QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW
+koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9
+ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt
+DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm
+bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw=
+-----END CERTIFICATE-----
+
+# Issuer: CN=GeoTrust Universal CA 2 O=GeoTrust Inc.
+# Subject: CN=GeoTrust Universal CA 2 O=GeoTrust Inc.
+# Label: "GeoTrust Universal CA 2"
+# Serial: 1
+# MD5 Fingerprint: 34:fc:b8:d0:36:db:9e:14:b3:c2:f2:db:8f:e4:94:c7
+# SHA1 Fingerprint: 37:9a:19:7b:41:85:45:35:0c:a6:03:69:f3:3c:2e:af:47:4f:20:79
+# SHA256 Fingerprint: a0:23:4f:3b:c8:52:7c:a5:62:8e:ec:81:ad:5d:69:89:5d:a5:68:0d:c9:1d:1c:b8:47:7f:33:f8:78:b9:5b:0b
+-----BEGIN CERTIFICATE-----
+MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW
+MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy
+c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD
+VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1
+c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81
+WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG
+FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq
+XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL
+se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb
+KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd
+IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73
+y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt
+hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc
+QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4
+Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV
+HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ
+KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z
+dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ
+L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr
+Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo
+ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY
+T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz
+GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m
+1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV
+OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH
+6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX
+QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS
+-----END CERTIFICATE-----
+
+# Issuer: CN=Visa eCommerce Root O=VISA OU=Visa International Service Association
+# Subject: CN=Visa eCommerce Root O=VISA OU=Visa International Service Association
+# Label: "Visa eCommerce Root"
+# Serial: 25952180776285836048024890241505565794
+# MD5 Fingerprint: fc:11:b8:d8:08:93:30:00:6d:23:f9:7e:eb:52:1e:02
+# SHA1 Fingerprint: 70:17:9b:86:8c:00:a4:fa:60:91:52:22:3f:9f:3e:32:bd:e0:05:62
+# SHA256 Fingerprint: 69:fa:c9:bd:55:fb:0a:c7:8d:53:bb:ee:5c:f1:d5:97:98:9f:d0:aa:ab:20:a2:51:51:bd:f1:73:3e:e7:d1:22
+-----BEGIN CERTIFICATE-----
+MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBr
+MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl
+cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv
+bW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2WhcNMjIwNjI0MDAxNjEyWjBrMQsw
+CQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5h
+dGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1l
+cmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h
+2mCxlCfLF9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4E
+lpF7sDPwsRROEW+1QK8bRaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdV
+ZqW1LS7YgFmypw23RuwhY/81q6UCzyr0TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq
+299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI/k4+oKsGGelT84ATB+0t
+vz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzsGHxBvfaL
+dXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD
+AgEGMB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUF
+AAOCAQEAX/FBfXxcCLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcR
+zCSs00Rsca4BIGsDoo8Ytyk6feUWYFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3
+LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd
+7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBuYQa7FkKMcPcw
+++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt
+398znM/jra6O1I7mT1GvFpLgXPYHDw==
+-----END CERTIFICATE-----
+
+# Issuer: CN=AAA Certificate Services O=Comodo CA Limited
+# Subject: CN=AAA Certificate Services O=Comodo CA Limited
+# Label: "Comodo AAA Services root"
+# Serial: 1
+# MD5 Fingerprint: 49:79:04:b0:eb:87:19:ac:47:b0:bc:11:51:9b:74:d0
+# SHA1 Fingerprint: d1:eb:23:a4:6d:17:d6:8f:d9:25:64:c2:f1:f1:60:17:64:d8:e3:49
+# SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4
+-----BEGIN CERTIFICATE-----
+MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb
+MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
+GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj
+YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL
+MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
+BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM
+GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua
+BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe
+3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4
+YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR
+rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm
+ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU
+oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
+MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v
+QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t
+b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF
+AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q
+GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
+Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2
+G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi
+l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3
+smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
+-----END CERTIFICATE-----
+
+# Issuer: CN=QuoVadis Root Certification Authority O=QuoVadis Limited OU=Root Certification Authority
+# Subject: CN=QuoVadis Root Certification Authority O=QuoVadis Limited OU=Root Certification Authority
+# Label: "QuoVadis Root CA"
+# Serial: 985026699
+# MD5 Fingerprint: 27:de:36:fe:72:b7:00:03:00:9d:f4:f0:1e:6c:04:24
+# SHA1 Fingerprint: de:3f:40:bd:50:93:d3:9b:6c:60:f6:da:bc:07:62:01:00:89:76:c9
+# SHA256 Fingerprint: a4:5e:de:3b:bb:f0:9c:8a:e1:5c:72:ef:c0:72:68:d6:93:a2:1c:99:6f:d5:1e:67:ca:07:94:60:fd:6d:88:73
+-----BEGIN CERTIFICATE-----
+MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC
+TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0
+aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0
+aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz
+MzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw
+IwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR
+dW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp
+li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D
+rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ
+WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug
+F+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU
+xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC
+Ak4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv
+dmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw
+ggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl
+IG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh
+c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy
+ZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh
+Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI
+KwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T
+KbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq
+y+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p
+dGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD
+VQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL
+MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk
+fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8
+7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R
+cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y
+mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW
+xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK
+SnQ2+Q==
+-----END CERTIFICATE-----
+
+# Issuer: CN=QuoVadis Root CA 2 O=QuoVadis Limited
+# Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited
+# Label: "QuoVadis Root CA 2"
+# Serial: 1289
+# MD5 Fingerprint: 5e:39:7b:dd:f8:ba:ec:82:e9:ac:62:ba:0c:54:00:2b
+# SHA1 Fingerprint: ca:3a:fb:cf:12:40:36:4b:44:b2:16:20:88:80:48:39:19:93:7c:f7
+# SHA256 Fingerprint: 85:a0:dd:7d:d7:20:ad:b7:ff:05:f8:3d:54:2b:20:9d:c7:ff:45:28:f7:d6:77:b1:83:89:fe:a5:e5:c4:9e:86
+-----BEGIN CERTIFICATE-----
+MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x
+GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv
+b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV
+BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W
+YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa
+GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg
+Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J
+WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB
+rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp
++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1
+ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i
+Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz
+PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og
+/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH
+oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI
+yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud
+EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2
+A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL
+MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT
+ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f
+BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn
+g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl
+fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K
+WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha
+B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc
+hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR
+TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD
+mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z
+ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y
+4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza
+8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u
+-----END CERTIFICATE-----
+
+# Issuer: CN=QuoVadis Root CA 3 O=QuoVadis Limited
+# Subject: CN=QuoVadis Root CA 3 O=QuoVadis Limited
+# Label: "QuoVadis Root CA 3"
+# Serial: 1478
+# MD5 Fingerprint: 31:85:3c:62:94:97:63:b9:aa:fd:89:4e:af:6f:e0:cf
+# SHA1 Fingerprint: 1f:49:14:f7:d8:74:95:1d:dd:ae:02:c0:be:fd:3a:2d:82:75:51:85
+# SHA256 Fingerprint: 18:f1:fc:7f:20:5d:f8:ad:dd:eb:7f:e0:07:dd:57:e3:af:37:5a:9c:4d:8d:73:54:6b:f4:f1:fe:d1:e1:8d:35
+-----BEGIN CERTIFICATE-----
+MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x
+GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv
+b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV
+BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W
+YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM
+V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB
+4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr
+H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd
+8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv
+vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT
+mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe
+btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc
+T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt
+WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ
+c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A
+4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD
+VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG
+CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0
+aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0
+aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu
+dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw
+czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G
+A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC
+TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg
+Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0
+7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem
+d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd
++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B
+4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN
+t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x
+DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57
+k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s
+zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j
+Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT
+mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK
+4SVhM7JZG+Ju1zdXtg2pEto=
+-----END CERTIFICATE-----
+
+# Issuer: O=SECOM Trust.net OU=Security Communication RootCA1
+# Subject: O=SECOM Trust.net OU=Security Communication RootCA1
+# Label: "Security Communication Root CA"
+# Serial: 0
+# MD5 Fingerprint: f1:bc:63:6a:54:e0:b5:27:f5:cd:e7:1a:e3:4d:6e:4a
+# SHA1 Fingerprint: 36:b1:2b:49:f9:81:9e:d7:4c:9e:bc:38:0f:c6:56:8f:5d:ac:b2:f7
+# SHA256 Fingerprint: e7:5e:72:ed:9f:56:0e:ec:6e:b4:80:00:73:a4:3f:c3:ad:19:19:5a:39:22:82:01:78:95:97:4a:99:02:6b:6c
+-----BEGIN CERTIFICATE-----
+MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY
+MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t
+dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5
+WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD
+VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8
+9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ
+DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9
+Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N
+QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ
+xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G
+A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T
+AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG
+kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr
+Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5
+Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU
+JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot
+RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw==
+-----END CERTIFICATE-----
+
+# Issuer: CN=Sonera Class2 CA O=Sonera
+# Subject: CN=Sonera Class2 CA O=Sonera
+# Label: "Sonera Class 2 Root CA"
+# Serial: 29
+# MD5 Fingerprint: a3:ec:75:0f:2e:88:df:fa:48:01:4e:0b:5c:48:6f:fb
+# SHA1 Fingerprint: 37:f7:6d:e6:07:7c:90:c5:b1:3e:93:1a:b7:41:10:b4:f2:e4:9a:27
+# SHA256 Fingerprint: 79:08:b4:03:14:c1:38:10:0b:51:8d:07:35:80:7f:fb:fc:f8:51:8a:00:95:33:71:05:ba:38:6b:15:3d:d9:27
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP
+MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx
+MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV
+BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o
+Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt
+5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s
+3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej
+vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu
+8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw
+DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG
+MA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil
+zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/
+3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD
+FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6
+Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2
+ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M
+-----END CERTIFICATE-----
+
+# Issuer: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com
+# Subject: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com
+# Label: "XRamp Global CA Root"
+# Serial: 107108908803651509692980124233745014957
+# MD5 Fingerprint: a1:0b:44:b3:ca:10:d8:00:6e:9d:0f:d8:0f:92:0a:d1
+# SHA1 Fingerprint: b8:01:86:d1:eb:9c:86:a5:41:04:cf:30:54:f3:4c:52:b7:e5:58:c6
+# SHA256 Fingerprint: ce:cd:dc:90:50:99:d8:da:df:c5:b1:d2:09:b7:37:cb:e2:c1:8c:fb:2c:10:c0:ff:0b:cf:0d:32:86:fc:1a:a2
+-----BEGIN CERTIFICATE-----
+MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB
+gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk
+MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY
+UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx
+NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3
+dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy
+dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB
+dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6
+38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP
+KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q
+DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4
+qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa
+JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi
+PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P
+BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs
+jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0
+eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD
+ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR
+vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt
+qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa
+IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy
+i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ
+O+7ETPTsJ3xCwnR8gooJybQDJbw=
+-----END CERTIFICATE-----
+
+# Issuer: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority
+# Subject: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority
+# Label: "Go Daddy Class 2 CA"
+# Serial: 0
+# MD5 Fingerprint: 91:de:06:25:ab:da:fd:32:17:0c:bb:25:17:2a:84:67
+# SHA1 Fingerprint: 27:96:ba:e6:3f:18:01:e2:77:26:1b:a0:d7:77:70:02:8f:20:ee:e4
+# SHA256 Fingerprint: c3:84:6b:f2:4b:9e:93:ca:64:27:4c:0e:c6:7c:1e:cc:5e:02:4f:fc:ac:d2:d7:40:19:35:0e:81:fe:54:6a:e4
+-----BEGIN CERTIFICATE-----
+MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh
+MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE
+YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3
+MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo
+ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg
+MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN
+ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA
+PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w
+wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi
+EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY
+avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+
+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE
+sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h
+/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5
+IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj
+YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD
+ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy
+OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P
+TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ
+HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER
+dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf
+ReYNnyicsbkqWletNw+vHX/bvZ8=
+-----END CERTIFICATE-----
+
+# Issuer: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority
+# Subject: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority
+# Label: "Starfield Class 2 CA"
+# Serial: 0
+# MD5 Fingerprint: 32:4a:4b:bb:c8:63:69:9b:be:74:9a:c6:dd:1d:46:24
+# SHA1 Fingerprint: ad:7e:1c:28:b0:64:ef:8f:60:03:40:20:14:c3:d0:e3:37:0e:b5:8a
+# SHA256 Fingerprint: 14:65:fa:20:53:97:b8:76:fa:a6:f0:a9:95:8e:55:90:e4:0f:cc:7f:aa:4f:b7:c2:c8:67:75:21:fb:5f:b6:58
+-----BEGIN CERTIFICATE-----
+MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl
+MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp
+U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw
+NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE
+ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp
+ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3
+DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf
+8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN
++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0
+X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa
+K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA
+1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G
+A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR
+zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0
+YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD
+bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w
+DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3
+L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D
+eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl
+xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp
+VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY
+WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=
+-----END CERTIFICATE-----
+
+# Issuer: O=Government Root Certification Authority
+# Subject: O=Government Root Certification Authority
+# Label: "Taiwan GRCA"
+# Serial: 42023070807708724159991140556527066870
+# MD5 Fingerprint: 37:85:44:53:32:45:1f:20:f0:f3:95:e1:25:c4:43:4e
+# SHA1 Fingerprint: f4:8b:11:bf:de:ab:be:94:54:20:71:e6:41:de:6b:be:88:2b:40:b9
+# SHA256 Fingerprint: 76:00:29:5e:ef:e8:5b:9e:1f:d6:24:db:76:06:2a:aa:ae:59:81:8a:54:d2:77:4c:d4:c0:b2:c0:11:31:e1:b3
+-----BEGIN CERTIFICATE-----
+MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/
+MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj
+YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow
+PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp
+Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
+AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR
+IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q
+gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy
+yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts
+F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2
+jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx
+ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC
+VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK
+YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH
+EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN
+Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud
+DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE
+MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK
+UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ
+TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf
+qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK
+ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE
+JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7
+hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1
+EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm
+nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX
+udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz
+ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe
+LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl
+pYYsfPQS
+-----END CERTIFICATE-----
+
+# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
+# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
+# Label: "DigiCert Assured ID Root CA"
+# Serial: 17154717934120587862167794914071425081
+# MD5 Fingerprint: 87:ce:0b:7b:2a:0e:49:00:e1:58:71:9b:37:a8:93:72
+# SHA1 Fingerprint: 05:63:b8:63:0d:62:d7:5a:bb:c8:ab:1e:4b:df:b5:a8:99:b2:4d:43
+# SHA256 Fingerprint: 3e:90:99:b5:01:5e:8f:48:6c:00:bc:ea:9d:11:1e:e7:21:fa:ba:35:5a:89:bc:f1:df:69:56:1e:3d:c6:32:5c
+-----BEGIN CERTIFICATE-----
+MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv
+b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
+cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c
+JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP
+mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+
+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4
+VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/
+AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB
+AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
+BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun
+pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC
+dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf
+fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm
+NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx
+H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe
++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==
+-----END CERTIFICATE-----
+
+# Issuer: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com
+# Subject: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com
+# Label: "DigiCert Global Root CA"
+# Serial: 10944719598952040374951832963794454346
+# MD5 Fingerprint: 79:e4:a9:84:0d:7d:3a:96:d7:c0:4f:e2:43:4c:89:2e
+# SHA1 Fingerprint: a8:98:5d:3a:65:e5:e5:c4:b2:d7:d6:6d:40:c6:dd:2f:b1:9c:54:36
+# SHA256 Fingerprint: 43:48:a0:e9:44:4c:78:cb:26:5e:05:8d:5e:89:44:b4:d8:4f:96:62:bd:26:db:25:7f:89:34:a4:43:c7:01:61
+-----BEGIN CERTIFICATE-----
+MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
+QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
+MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
+b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
+CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
+nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
+43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
+T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
+gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
+BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
+TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
+DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
+hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
+06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
+PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
+YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
+CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
+-----END CERTIFICATE-----
+
+# Issuer: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com
+# Subject: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com
+# Label: "DigiCert High Assurance EV Root CA"
+# Serial: 3553400076410547919724730734378100087
+# MD5 Fingerprint: d4:74:de:57:5c:39:b2:d3:9c:85:83:c5:c0:65:49:8a
+# SHA1 Fingerprint: 5f:b7:ee:06:33:e2:59:db:ad:0c:4c:9a:e6:d3:8f:1a:61:c7:dc:25
+# SHA256 Fingerprint: 74:31:e5:f4:c3:c1:ce:46:90:77:4f:0b:61:e0:54:40:88:3b:a9:a0:1e:d0:0b:a6:ab:d7:80:6e:d3:b1:18:cf
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
+LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
+RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm
++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW
+PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM
+xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB
+Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3
+hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg
+EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF
+MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA
+FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec
+nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z
+eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF
+hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2
+Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
+vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
++OkuE6N36B9K
+-----END CERTIFICATE-----
+
+# Issuer: CN=Class 2 Primary CA O=Certplus
+# Subject: CN=Class 2 Primary CA O=Certplus
+# Label: "Certplus Class 2 Primary CA"
+# Serial: 177770208045934040241468760488327595043
+# MD5 Fingerprint: 88:2c:8c:52:b8:a2:3c:f3:f7:bb:03:ea:ae:ac:42:0b
+# SHA1 Fingerprint: 74:20:74:41:72:9c:dd:92:ec:79:31:d8:23:10:8d:c2:81:92:e2:bb
+# SHA256 Fingerprint: 0f:99:3c:8a:ef:97:ba:af:56:87:14:0e:d5:9a:d1:82:1b:b4:af:ac:f0:aa:9a:58:b5:d5:7a:33:8a:3a:fb:cb
+-----BEGIN CERTIFICATE-----
+MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw
+PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz
+cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9
+MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz
+IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ
+ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR
+VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL
+kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd
+EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas
+H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0
+HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud
+DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4
+QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu
+Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/
+AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8
+yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR
+FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA
+ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB
+kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7
+l7+ijrRU
+-----END CERTIFICATE-----
+
+# Issuer: CN=DST Root CA X3 O=Digital Signature Trust Co.
+# Subject: CN=DST Root CA X3 O=Digital Signature Trust Co.
+# Label: "DST Root CA X3"
+# Serial: 91299735575339953335919266965803778155
+# MD5 Fingerprint: 41:03:52:dc:0f:f7:50:1b:16:f0:02:8e:ba:6f:45:c5
+# SHA1 Fingerprint: da:c9:02:4f:54:d8:f6:df:94:93:5f:b1:73:26:38:ca:6a:d7:7c:13
+# SHA256 Fingerprint: 06:87:26:03:31:a7:24:03:d9:09:f1:05:e6:9b:cf:0d:32:e1:bd:24:93:ff:c6:d9:20:6d:11:bc:d6:77:07:39
+-----BEGIN CERTIFICATE-----
+MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
+PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
+Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
+rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
+OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
+xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
+7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
+aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
+SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
+ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
+AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
+R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
+JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
+Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
+-----END CERTIFICATE-----
+
+# Issuer: CN=SwissSign Gold CA - G2 O=SwissSign AG
+# Subject: CN=SwissSign Gold CA - G2 O=SwissSign AG
+# Label: "SwissSign Gold CA - G2"
+# Serial: 13492815561806991280
+# MD5 Fingerprint: 24:77:d9:a8:91:d1:3b:fa:88:2d:c2:ff:f8:cd:33:93
+# SHA1 Fingerprint: d8:c5:38:8a:b7:30:1b:1b:6e:d4:7a:e6:45:25:3a:6f:9f:1a:27:61
+# SHA256 Fingerprint: 62:dd:0b:e9:b9:f5:0a:16:3e:a0:f8:e7:5c:05:3b:1e:ca:57:ea:55:c8:68:8f:64:7c:68:81:f2:c8:35:7b:95
+-----BEGIN CERTIFICATE-----
+MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln
+biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF
+MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT
+d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8
+76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+
+bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c
+6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE
+emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd
+MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt
+MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y
+MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y
+FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi
+aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM
+gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB
+qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7
+lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn
+8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov
+L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6
+45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO
+UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5
+O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC
+bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv
+GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a
+77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC
+hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3
+92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp
+Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w
+ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt
+Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ
+-----END CERTIFICATE-----
+
+# Issuer: CN=SwissSign Silver CA - G2 O=SwissSign AG
+# Subject: CN=SwissSign Silver CA - G2 O=SwissSign AG
+# Label: "SwissSign Silver CA - G2"
+# Serial: 5700383053117599563
+# MD5 Fingerprint: e0:06:a1:c9:7d:cf:c9:fc:0d:c0:56:75:96:d8:62:13
+# SHA1 Fingerprint: 9b:aa:e5:9f:56:ee:21:cb:43:5a:be:25:93:df:a7:f0:40:d1:1d:cb
+# SHA256 Fingerprint: be:6c:4d:a2:bb:b9:ba:59:b6:f3:93:97:68:37:42:46:c3:c0:05:99:3f:a9:8f:02:0d:1d:ed:be:d4:8a:81:d5
+-----BEGIN CERTIFICATE-----
+MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE
+BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu
+IFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow
+RzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY
+U3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
+MIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv
+Fz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br
+YT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF
+nbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH
+6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt
+eJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/
+c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ
+MoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH
+HTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf
+jNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6
+5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB
+rDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
+F6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c
+wpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0
+cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB
+AHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp
+WJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9
+xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ
+2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ
+IseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8
+aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X
+em1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR
+dAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/
+OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+
+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy
+tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u
+-----END CERTIFICATE-----
+
+# Issuer: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc.
+# Subject: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc.
+# Label: "GeoTrust Primary Certification Authority"
+# Serial: 32798226551256963324313806436981982369
+# MD5 Fingerprint: 02:26:c3:01:5e:08:30:37:43:a9:d0:7d:cf:37:e6:bf
+# SHA1 Fingerprint: 32:3c:11:8e:1b:f7:b8:b6:52:54:e2:e2:10:0d:d6:02:90:37:f0:96
+# SHA256 Fingerprint: 37:d5:10:06:c5:12:ea:ab:62:64:21:f1:ec:8c:92:01:3f:c5:f8:2a:e9:8e:e5:33:eb:46:19:b8:de:b4:d0:6c
+-----BEGIN CERTIFICATE-----
+MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY
+MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo
+R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx
+MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK
+Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp
+ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9
+AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA
+ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0
+7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W
+kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI
+mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G
+A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ
+KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1
+6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl
+4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K
+oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj
+UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU
+AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk=
+-----END CERTIFICATE-----
+
+# Issuer: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only
+# Subject: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only
+# Label: "thawte Primary Root CA"
+# Serial: 69529181992039203566298953787712940909
+# MD5 Fingerprint: 8c:ca:dc:0b:22:ce:f5:be:72:ac:41:1a:11:a8:d8:12
+# SHA1 Fingerprint: 91:c6:d6:ee:3e:8a:c8:63:84:e5:48:c2:99:29:5c:75:6c:81:7b:81
+# SHA256 Fingerprint: 8d:72:2f:81:a9:c1:13:c0:79:1d:f1:36:a2:96:6d:b2:6c:95:0a:97:1d:b4:6b:41:99:f4:ea:54:b7:8b:fb:9f
+-----BEGIN CERTIFICATE-----
+MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB
+qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf
+Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw
+MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV
+BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw
+NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j
+LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG
+A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl
+IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs
+W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta
+3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk
+6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6
+Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J
+NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA
+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP
+r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU
+DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz
+YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX
+xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2
+/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/
+LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7
+jVaMaA==
+-----END CERTIFICATE-----
+
+# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only
+# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only
+# Label: "VeriSign Class 3 Public Primary Certification Authority - G5"
+# Serial: 33037644167568058970164719475676101450
+# MD5 Fingerprint: cb:17:e4:31:67:3e:e2:09:fe:45:57:93:f3:0a:fa:1c
+# SHA1 Fingerprint: 4e:b6:d5:78:49:9b:1c:cf:5f:58:1e:ad:56:be:3d:9b:67:44:a5:e5
+# SHA256 Fingerprint: 9a:cf:ab:7e:43:c8:d8:80:d0:6b:26:2a:94:de:ee:e4:b4:65:99:89:c3:d0:ca:f1:9b:af:64:05:e4:1a:b7:df
+-----BEGIN CERTIFICATE-----
+MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB
+yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL
+ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp
+U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW
+ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0
+aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL
+MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW
+ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln
+biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp
+U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y
+aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1
+nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex
+t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz
+SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG
+BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+
+rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/
+NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E
+BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH
+BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy
+aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv
+MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE
+p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y
+5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK
+WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ
+4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N
+hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq
+-----END CERTIFICATE-----
+
+# Issuer: CN=SecureTrust CA O=SecureTrust Corporation
+# Subject: CN=SecureTrust CA O=SecureTrust Corporation
+# Label: "SecureTrust CA"
+# Serial: 17199774589125277788362757014266862032
+# MD5 Fingerprint: dc:32:c3:a7:6d:25:57:c7:68:09:9d:ea:2d:a9:a2:d1
+# SHA1 Fingerprint: 87:82:c6:c3:04:35:3b:cf:d2:96:92:d2:59:3e:7d:44:d9:34:ff:11
+# SHA256 Fingerprint: f1:c1:b5:0a:e5:a2:0d:d8:03:0e:c9:f6:bc:24:82:3d:d3:67:b5:25:57:59:b4:e7:1b:61:fc:e9:f7:37:5d:73
+-----BEGIN CERTIFICATE-----
+MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI
+MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x
+FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz
+MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv
+cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz
+Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO
+0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao
+wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj
+7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS
+8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT
+BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB
+/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg
+JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC
+NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3
+6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/
+3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm
+D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS
+CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR
+3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=
+-----END CERTIFICATE-----
+
+# Issuer: CN=Secure Global CA O=SecureTrust Corporation
+# Subject: CN=Secure Global CA O=SecureTrust Corporation
+# Label: "Secure Global CA"
+# Serial: 9751836167731051554232119481456978597
+# MD5 Fingerprint: cf:f4:27:0d:d4:ed:dc:65:16:49:6d:3d:da:bf:6e:de
+# SHA1 Fingerprint: 3a:44:73:5a:e5:81:90:1f:24:86:61:46:1e:3b:9c:c4:5f:f5:3a:1b
+# SHA256 Fingerprint: 42:00:f5:04:3a:c8:59:0e:bb:52:7d:20:9e:d1:50:30:29:fb:cb:d4:1c:a1:b5:06:ec:27:f1:5a:de:7d:ac:69
+-----BEGIN CERTIFICATE-----
+MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK
+MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x
+GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx
+MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg
+Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ
+iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa
+/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ
+jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI
+HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7
+sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w
+gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF
+MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw
+KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG
+AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L
+URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO
+H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm
+I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY
+iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc
+f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW
+-----END CERTIFICATE-----
+
+# Issuer: CN=COMODO Certification Authority O=COMODO CA Limited
+# Subject: CN=COMODO Certification Authority O=COMODO CA Limited
+# Label: "COMODO Certification Authority"
+# Serial: 104350513648249232941998508985834464573
+# MD5 Fingerprint: 5c:48:dc:f7:42:72:ec:56:94:6d:1c:cc:71:35:80:75
+# SHA1 Fingerprint: 66:31:bf:9e:f7:4f:9e:b6:c9:d5:a6:0c:ba:6a:be:d1:f7:bd:ef:7b
+# SHA256 Fingerprint: 0c:2c:d6:3d:f7:80:6f:a3:99:ed:e8:09:11:6b:57:5b:f8:79:89:f0:65:18:f9:80:8c:86:05:03:17:8b:af:66
+-----BEGIN CERTIFICATE-----
+MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB
+gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV
+BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw
+MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl
+YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P
+RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0
+aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3
+UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI
+2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8
+Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp
++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+
+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O
+nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW
+/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g
+PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u
+QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY
+SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv
+IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/
+RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4
+zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd
+BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB
+ZQ==
+-----END CERTIFICATE-----
+
+# Issuer: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C.
+# Subject: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C.
+# Label: "Network Solutions Certificate Authority"
+# Serial: 116697915152937497490437556386812487904
+# MD5 Fingerprint: d3:f3:a6:16:c0:fa:6b:1d:59:b1:2d:96:4d:0e:11:2e
+# SHA1 Fingerprint: 74:f8:a3:c3:ef:e7:b3:90:06:4b:83:90:3c:21:64:60:20:e5:df:ce
+# SHA256 Fingerprint: 15:f0:ba:00:a3:ac:7a:f3:ac:88:4c:07:2b:10:11:a0:77:bd:77:c0:97:f4:01:64:b2:f8:59:8a:bd:83:86:0c
+-----BEGIN CERTIFICATE-----
+MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi
+MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu
+MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp
+dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV
+UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO
+ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz
+c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP
+OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl
+mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF
+BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4
+qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw
+gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB
+BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu
+bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp
+dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8
+6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/
+h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH
+/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv
+wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN
+pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey
+-----END CERTIFICATE-----
+
+# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited
+# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited
+# Label: "COMODO ECC Certification Authority"
+# Serial: 41578283867086692638256921589707938090
+# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23
+# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11
+# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7
+-----BEGIN CERTIFICATE-----
+MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL
+MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
+BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT
+IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw
+MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy
+ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N
+T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR
+FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J
+cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW
+BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/
+BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm
+fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv
+GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=
+-----END CERTIFICATE-----
+
+# Issuer: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed
+# Subject: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed
+# Label: "OISTE WISeKey Global Root GA CA"
+# Serial: 86718877871133159090080555911823548314
+# MD5 Fingerprint: bc:6c:51:33:a7:e9:d3:66:63:54:15:72:1b:21:92:93
+# SHA1 Fingerprint: 59:22:a1:e1:5a:ea:16:35:21:f8:98:39:6a:46:46:b0:44:1b:0f:a9
+# SHA256 Fingerprint: 41:c9:23:86:6a:b4:ca:d6:b7:ad:57:80:81:58:2e:02:07:97:a6:cb:df:4f:ff:78:ce:83:96:b3:89:37:d7:f5
+-----BEGIN CERTIFICATE-----
+MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB
+ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly
+aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl
+ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w
+NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G
+A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD
+VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX
+SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR
+VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2
+w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF
+mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg
+4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9
+4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw
+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw
+EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx
+SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2
+ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8
+vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa
+hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi
+Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ
+/L7fCg0=
+-----END CERTIFICATE-----
+
+# Issuer: CN=Certigna O=Dhimyotis
+# Subject: CN=Certigna O=Dhimyotis
+# Label: "Certigna"
+# Serial: 18364802974209362175
+# MD5 Fingerprint: ab:57:a6:5b:7d:42:82:19:b5:d8:58:26:28:5e:fd:ff
+# SHA1 Fingerprint: b1:2e:13:63:45:86:a4:6f:1a:b2:60:68:37:58:2d:c4:ac:fd:94:97
+# SHA256 Fingerprint: e3:b6:a2:db:2e:d7:ce:48:84:2f:7a:c5:32:41:c7:b7:1d:54:14:4b:fb:40:c1:1f:3f:1d:0b:42:f5:ee:a1:2d
+-----BEGIN CERTIFICATE-----
+MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV
+BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X
+DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ
+BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4
+QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny
+gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw
+zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q
+130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2
+JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw
+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw
+ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT
+AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj
+AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG
+9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h
+bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc
+fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu
+HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w
+t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw
+WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==
+-----END CERTIFICATE-----
+
+# Issuer: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center
+# Subject: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center
+# Label: "Deutsche Telekom Root CA 2"
+# Serial: 38
+# MD5 Fingerprint: 74:01:4a:91:b1:08:c4:58:ce:47:cd:f0:dd:11:53:08
+# SHA1 Fingerprint: 85:a4:08:c0:9c:19:3e:5d:51:58:7d:cd:d6:13:30:fd:8c:de:37:bf
+# SHA256 Fingerprint: b6:19:1a:50:d0:c3:97:7f:7d:a9:9b:cd:aa:c8:6a:22:7d:ae:b9:67:9e:c7:0b:a3:b0:c9:d9:22:71:c1:70:d3
+-----BEGIN CERTIFICATE-----
+MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc
+MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj
+IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB
+IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE
+RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl
+U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290
+IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU
+ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC
+QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr
+rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S
+NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc
+QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH
+txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP
+BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC
+AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp
+tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa
+IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl
+6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+
+xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU
+Cm26OWMohpLzGITY+9HPBVZkVw==
+-----END CERTIFICATE-----
+
+# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc
+# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc
+# Label: "Cybertrust Global Root"
+# Serial: 4835703278459682877484360
+# MD5 Fingerprint: 72:e4:4a:87:e3:69:40:80:77:ea:bc:e3:f4:ff:f0:e1
+# SHA1 Fingerprint: 5f:43:e5:b1:bf:f8:78:8c:ac:1c:c7:ca:4a:9a:c6:22:2b:cc:34:c6
+# SHA256 Fingerprint: 96:0a:df:00:63:e9:63:56:75:0c:29:65:dd:0a:08:67:da:0b:9c:bd:6e:77:71:4a:ea:fb:23:49:ab:39:3d:a3
+-----BEGIN CERTIFICATE-----
+MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG
+A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh
+bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE
+ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS
+b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5
+7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS
+J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y
+HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP
+t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz
+FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY
+XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/
+MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw
+hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js
+MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA
+A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj
+Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx
+XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o
+omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc
+A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW
+WL1WMRJOEcgh4LMRkWXbtKaIOM5V
+-----END CERTIFICATE-----
+
+# Issuer: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority
+# Subject: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority
+# Label: "ePKI Root Certification Authority"
+# Serial: 28956088682735189655030529057352760477
+# MD5 Fingerprint: 1b:2e:00:ca:26:06:90:3d:ad:fe:6f:15:68:d3:6b:b3
+# SHA1 Fingerprint: 67:65:0d:f1:7e:8e:7e:5b:82:40:a4:f4:56:4b:cf:e2:3d:69:c6:f0
+# SHA256 Fingerprint: c0:a6:f4:dc:63:a2:4b:fd:cf:54:ef:2a:6a:08:2a:0a:72:de:35:80:3e:2f:f5:ff:52:7a:e5:d8:72:06:df:d5
+-----BEGIN CERTIFICATE-----
+MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe
+MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0
+ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe
+Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw
+IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL
+SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH
+SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh
+ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X
+DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1
+TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ
+fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA
+sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU
+WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS
+nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH
+dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip
+NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC
+AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF
+MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH
+ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB
+uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl
+PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP
+JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/
+gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2
+j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6
+5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB
+o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS
+/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z
+Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE
+W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D
+hNQ+IIX3Sj0rnP0qCglN6oH4EZw=
+-----END CERTIFICATE-----
+
+# Issuer: O=certSIGN OU=certSIGN ROOT CA
+# Subject: O=certSIGN OU=certSIGN ROOT CA
+# Label: "certSIGN ROOT CA"
+# Serial: 35210227249154
+# MD5 Fingerprint: 18:98:c0:d6:e9:3a:fc:f9:b0:f5:0c:f7:4b:01:44:17
+# SHA1 Fingerprint: fa:b7:ee:36:97:26:62:fb:2d:b0:2a:f6:bf:03:fd:e8:7c:4b:2f:9b
+# SHA256 Fingerprint: ea:a9:62:c4:fa:4a:6b:af:eb:e4:15:19:6d:35:1c:cd:88:8d:4f:53:f3:fa:8a:e6:d7:c4:66:a9:4e:60:42:bb
+-----BEGIN CERTIFICATE-----
+MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT
+AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD
+QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP
+MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do
+0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ
+UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d
+RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ
+OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv
+JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C
+AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O
+BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ
+LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY
+MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ
+44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I
+Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw
+i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN
+9u6wWk5JRFRYX0KD
+-----END CERTIFICATE-----
+
+# Issuer: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only
+# Subject: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only
+# Label: "GeoTrust Primary Certification Authority - G3"
+# Serial: 28809105769928564313984085209975885599
+# MD5 Fingerprint: b5:e8:34:36:c9:10:44:58:48:70:6d:2e:83:d4:b8:05
+# SHA1 Fingerprint: 03:9e:ed:b8:0b:e7:a0:3c:69:53:89:3b:20:d2:d9:32:3a:4c:2a:fd
+# SHA256 Fingerprint: b4:78:b8:12:25:0d:f8:78:63:5c:2a:a7:ec:7d:15:5e:aa:62:5e:e8:29:16:e2:cd:29:43:61:88:6c:d1:fb:d4
+-----BEGIN CERTIFICATE-----
+MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB
+mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT
+MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s
+eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv
+cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ
+BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg
+MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0
+BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
+LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz
++uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm
+hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn
+5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W
+JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL
+DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC
+huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw
+HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB
+AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB
+zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN
+kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD
+AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH
+SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G
+spki4cErx5z481+oghLrGREt
+-----END CERTIFICATE-----
+
+# Issuer: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only
+# Subject: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only
+# Label: "thawte Primary Root CA - G2"
+# Serial: 71758320672825410020661621085256472406
+# MD5 Fingerprint: 74:9d:ea:60:24:c4:fd:22:53:3e:cc:3a:72:d9:29:4f
+# SHA1 Fingerprint: aa:db:bc:22:23:8f:c4:01:a1:27:bb:38:dd:f4:1d:db:08:9e:f0:12
+# SHA256 Fingerprint: a4:31:0d:50:af:18:a6:44:71:90:37:2a:86:af:af:8b:95:1f:fb:43:1d:83:7f:1e:56:88:b4:59:71:ed:15:57
+-----BEGIN CERTIFICATE-----
+MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp
+IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi
+BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw
+MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh
+d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig
+YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v
+dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/
+BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6
+papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E
+BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K
+DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3
+KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox
+XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg==
+-----END CERTIFICATE-----
+
+# Issuer: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only
+# Subject: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only
+# Label: "thawte Primary Root CA - G3"
+# Serial: 127614157056681299805556476275995414779
+# MD5 Fingerprint: fb:1b:5d:43:8a:94:cd:44:c6:76:f2:43:4b:47:e7:31
+# SHA1 Fingerprint: f1:8b:53:8d:1b:e9:03:b6:a6:f0:56:43:5b:17:15:89:ca:f3:6b:f2
+# SHA256 Fingerprint: 4b:03:f4:58:07:ad:70:f2:1b:fc:2c:ae:71:c9:fd:e4:60:4c:06:4c:f5:ff:b6:86:ba:e5:db:aa:d7:fd:d3:4c
+-----BEGIN CERTIFICATE-----
+MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB
+rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf
+Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw
+MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV
+BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa
+Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl
+LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u
+MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl
+ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm
+gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8
+YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf
+b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9
+9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S
+zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk
+OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV
+HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA
+2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW
+oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu
+t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c
+KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM
+m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu
+MdRAGmI0Nj81Aa6sY6A=
+-----END CERTIFICATE-----
+
+# Issuer: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only
+# Subject: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only
+# Label: "GeoTrust Primary Certification Authority - G2"
+# Serial: 80682863203381065782177908751794619243
+# MD5 Fingerprint: 01:5e:d8:6b:bd:6f:3d:8e:a1:31:f8:12:e0:98:73:6a
+# SHA1 Fingerprint: 8d:17:84:d5:37:f3:03:7d:ec:70:fe:57:8b:51:9a:99:e6:10:d7:b0
+# SHA256 Fingerprint: 5e:db:7a:c4:3b:82:a0:6a:87:61:e8:d7:be:49:79:eb:f2:61:1f:7d:d7:9b:f9:1c:1c:6b:56:6a:21:9e:d7:66
+-----BEGIN CERTIFICATE-----
+MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL
+MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj
+KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2
+MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0
+eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV
+BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw
+NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV
+BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH
+MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL
+So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal
+tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO
+BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG
+CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT
+qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz
+rD6ogRLQy7rQkgu2npaqBA+K
+-----END CERTIFICATE-----
+
+# Issuer: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only
+# Subject: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only
+# Label: "VeriSign Universal Root Certification Authority"
+# Serial: 85209574734084581917763752644031726877
+# MD5 Fingerprint: 8e:ad:b5:01:aa:4d:81:e4:8c:1d:d1:e1:14:00:95:19
+# SHA1 Fingerprint: 36:79:ca:35:66:87:72:30:4d:30:a5:fb:87:3b:0f:a7:7b:b7:0d:54
+# SHA256 Fingerprint: 23:99:56:11:27:a5:71:25:de:8c:ef:ea:61:0d:df:2f:a0:78:b5:c8:06:7f:4e:82:82:90:bf:b8:60:e8:4b:3c
+-----BEGIN CERTIFICATE-----
+MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB
+vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL
+ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp
+U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W
+ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe
+Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX
+MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0
+IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y
+IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh
+bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF
+9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH
+H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H
+LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN
+/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT
+rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud
+EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw
+WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs
+exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud
+DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4
+sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+
+seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz
+4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+
+BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR
+lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3
+7M2CYfE45k+XmCpajQ==
+-----END CERTIFICATE-----
+
+# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only
+# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only
+# Label: "VeriSign Class 3 Public Primary Certification Authority - G4"
+# Serial: 63143484348153506665311985501458640051
+# MD5 Fingerprint: 3a:52:e1:e7:fd:6f:3a:e3:6f:f3:6f:99:1b:f9:22:41
+# SHA1 Fingerprint: 22:d5:d8:df:8f:02:31:d1:8d:f7:9d:b7:cf:8a:2d:64:c9:3f:6c:3a
+# SHA256 Fingerprint: 69:dd:d7:ea:90:bb:57:c9:3e:13:5d:c8:5e:a6:fc:d5:48:0b:60:32:39:bd:c4:54:fc:75:8b:2a:26:cf:7f:79
+-----BEGIN CERTIFICATE-----
+MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL
+MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW
+ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln
+biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp
+U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y
+aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG
+A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp
+U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg
+SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln
+biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5
+IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm
+GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve
+fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw
+AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ
+aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj
+aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW
+kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC
+4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga
+FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA==
+-----END CERTIFICATE-----
+
+# Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services)
+# Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services)
+# Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny"
+# Serial: 80544274841616
+# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88
+# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91
+# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98
+-----BEGIN CERTIFICATE-----
+MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG
+EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3
+MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl
+cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR
+dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB
+pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM
+b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm
+aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz
+IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT
+lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz
+AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5
+VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG
+ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2
+BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG
+AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M
+U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh
+bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C
++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC
+bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F
+uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2
+XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=
+-----END CERTIFICATE-----
+
+# Issuer: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden
+# Subject: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden
+# Label: "Staat der Nederlanden Root CA - G2"
+# Serial: 10000012
+# MD5 Fingerprint: 7c:a5:0f:f8:5b:9a:7d:6d:30:ae:54:5a:e3:42:a2:8a
+# SHA1 Fingerprint: 59:af:82:79:91:86:c7:b4:75:07:cb:cf:03:57:46:eb:04:dd:b7:16
+# SHA256 Fingerprint: 66:8c:83:94:7d:a6:3b:72:4b:ec:e1:74:3c:31:a0:e6:ae:d0:db:8e:c5:b3:1b:e3:77:bb:78:4f:91:b6:71:6f
+-----BEGIN CERTIFICATE-----
+MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO
+TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh
+dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX
+DTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl
+ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv
+b3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291
+qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp
+uOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU
+Z5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE
+pMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp
+5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M
+UGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN
+GmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy
+5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv
+6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK
+eN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6
+B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/
+BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov
+L3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV
+HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG
+SIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS
+CZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen
+5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897
+IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK
+gnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL
++63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL
+vJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm
+bEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk
+N1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC
+Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z
+ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ==
+-----END CERTIFICATE-----
+
+# Issuer: CN=Hongkong Post Root CA 1 O=Hongkong Post
+# Subject: CN=Hongkong Post Root CA 1 O=Hongkong Post
+# Label: "Hongkong Post Root CA 1"
+# Serial: 1000
+# MD5 Fingerprint: a8:0d:6f:39:78:b9:43:6d:77:42:6d:98:5a:cc:23:ca
+# SHA1 Fingerprint: d6:da:a8:20:8d:09:d2:15:4d:24:b5:2f:cb:34:6e:b2:58:b2:8a:58
+# SHA256 Fingerprint: f9:e6:7d:33:6c:51:00:2a:c0:54:c6:32:02:2d:66:dd:a2:e7:e3:ff:f1:0a:d0:61:ed:31:d8:bb:b4:10:cf:b2
+-----BEGIN CERTIFICATE-----
+MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx
+FjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg
+Um9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG
+A1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr
+b25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ
+jVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn
+PzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh
+ZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9
+nnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h
+q5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED
+MA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC
+mEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3
+7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB
+oiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs
+EhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO
+fMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi
+AmvZWg==
+-----END CERTIFICATE-----
+
+# Issuer: CN=SecureSign RootCA11 O=Japan Certification Services, Inc.
+# Subject: CN=SecureSign RootCA11 O=Japan Certification Services, Inc.
+# Label: "SecureSign RootCA11"
+# Serial: 1
+# MD5 Fingerprint: b7:52:74:e2:92:b4:80:93:f2:75:e4:cc:d7:f2:ea:26
+# SHA1 Fingerprint: 3b:c4:9f:48:f8:f3:73:a0:9c:1e:bd:f8:5b:b1:c3:65:c7:d8:11:b3
+# SHA256 Fingerprint: bf:0f:ee:fb:9e:3a:58:1a:d5:f9:e9:db:75:89:98:57:43:d2:61:08:5c:4d:31:4f:6f:5d:72:59:aa:42:16:12
+-----BEGIN CERTIFICATE-----
+MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr
+MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG
+A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0
+MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp
+Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD
+QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz
+i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8
+h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV
+MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9
+UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni
+8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC
+h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD
+VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB
+AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm
+KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ
+X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr
+QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5
+pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN
+QSdJQO7e5iNEOdyhIta6A/I=
+-----END CERTIFICATE-----
+
+# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd.
+# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd.
+# Label: "Microsec e-Szigno Root CA 2009"
+# Serial: 14014712776195784473
+# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1
+# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e
+# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78
+-----BEGIN CERTIFICATE-----
+MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD
+VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0
+ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G
+CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y
+OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx
+FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp
+Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o
+dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP
+kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc
+cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U
+fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7
+N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC
+xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1
++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G
+A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM
+Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG
+SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h
+mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk
+ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775
+tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c
+2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t
+HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW
+-----END CERTIFICATE-----
+
+# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3
+# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3
+# Label: "GlobalSign Root CA - R3"
+# Serial: 4835703278459759426209954
+# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28
+# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad
+# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b
+-----BEGIN CERTIFICATE-----
+MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G
+A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp
+Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4
+MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG
+A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8
+RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT
+gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm
+KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd
+QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ
+XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw
+DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o
+LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU
+RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp
+jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK
+6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX
+mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs
+Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH
+WD9f
+-----END CERTIFICATE-----
+
+# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068
+# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068
+# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068"
+# Serial: 6047274297262753887
+# MD5 Fingerprint: 73:3a:74:7a:ec:bb:a3:96:a6:c2:e4:e2:c8:9b:c0:c3
+# SHA1 Fingerprint: ae:c5:fb:3f:c8:e1:bf:c4:e5:4f:03:07:5a:9a:e8:00:b7:f7:b6:fa
+# SHA256 Fingerprint: 04:04:80:28:bf:1f:28:64:d4:8f:9a:d4:d8:32:94:36:6a:82:88:56:55:3f:3b:14:30:3f:90:14:7f:5d:40:ef
+-----BEGIN CERTIFICATE-----
+MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE
+BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h
+cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy
+MzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg
+Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9
+thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM
+cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG
+L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i
+NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h
+X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b
+m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy
+Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja
+EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T
+KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF
+6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh
+OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD
+VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD
+VR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp
+cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv
+ACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl
+AGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF
+661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9
+am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1
+ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481
+PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS
+3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k
+SeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF
+3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM
+ZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g
+StRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz
+Q0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB
+jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V
+-----END CERTIFICATE-----
+
+# Issuer: CN=Izenpe.com O=IZENPE S.A.
+# Subject: CN=Izenpe.com O=IZENPE S.A.
+# Label: "Izenpe.com"
+# Serial: 917563065490389241595536686991402621
+# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73
+# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19
+# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f
+-----BEGIN CERTIFICATE-----
+MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4
+MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6
+ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD
+VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j
+b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq
+scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO
+xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H
+LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX
+uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD
+yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+
+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q
+rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN
+BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L
+hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB
+QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+
+HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu
+Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg
+QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB
+BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx
+MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA
+A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb
+laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56
+awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo
+JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw
+LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT
+VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk
+LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb
+UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/
+QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+
+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls
+QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw==
+-----END CERTIFICATE-----
+
+# Issuer: CN=Chambers of Commerce Root - 2008 O=AC Camerfirma S.A.
+# Subject: CN=Chambers of Commerce Root - 2008 O=AC Camerfirma S.A.
+# Label: "Chambers of Commerce Root - 2008"
+# Serial: 11806822484801597146
+# MD5 Fingerprint: 5e:80:9e:84:5a:0e:65:0b:17:02:f3:55:18:2a:3e:d7
+# SHA1 Fingerprint: 78:6a:74:ac:76:ab:14:7f:9c:6a:30:50:ba:9e:a8:7e:fe:9a:ce:3c
+# SHA256 Fingerprint: 06:3e:4a:fa:c4:91:df:d3:32:f3:08:9b:85:42:e9:46:17:d8:93:d7:fe:94:4e:10:a7:93:7e:e2:9d:96:93:c0
+-----BEGIN CERTIFICATE-----
+MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD
+VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0
+IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3
+MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz
+IG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz
+MTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj
+dXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw
+EAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp
+MCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G
+CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9
+28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq
+VKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q
+DuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR
+5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL
+ZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a
+Sd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl
+UlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s
++12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5
+Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj
+ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx
+hduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV
+HQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1
++HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN
+YWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t
+L2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy
+ZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt
+IDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV
+HSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w
+DQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW
+PJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF
+5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1
+glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH
+FoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2
+pSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD
+xvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG
+tjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq
+jktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De
+fhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg
+OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ
+d0jQ
+-----END CERTIFICATE-----
+
+# Issuer: CN=Global Chambersign Root - 2008 O=AC Camerfirma S.A.
+# Subject: CN=Global Chambersign Root - 2008 O=AC Camerfirma S.A.
+# Label: "Global Chambersign Root - 2008"
+# Serial: 14541511773111788494
+# MD5 Fingerprint: 9e:80:ff:78:01:0c:2e:c1:36:bd:fe:96:90:6e:08:f3
+# SHA1 Fingerprint: 4a:bd:ee:ec:95:0d:35:9c:89:ae:c7:52:a1:2c:5b:29:f6:d6:aa:0c
+# SHA256 Fingerprint: 13:63:35:43:93:34:a7:69:80:16:a0:d3:24:de:72:28:4e:07:9d:7b:52:20:bb:8f:bd:74:78:16:ee:be:ba:ca
+-----BEGIN CERTIFICATE-----
+MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD
+VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0
+IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3
+MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD
+aGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx
+MjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy
+cmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG
+A1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl
+BgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI
+hvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed
+KYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7
+G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2
+zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4
+ddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG
+HoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2
+Id3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V
+yJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e
+beksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r
+6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh
+wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog
+zCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW
+BBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr
+ru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp
+ZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk
+cmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt
+YSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC
+CQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow
+KAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI
+hvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ
+UohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz
+X1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x
+fxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz
+a2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd
+Yhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd
+SqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O
+AP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso
+M0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge
+v8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z
+09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B
+-----END CERTIFICATE-----
+
+# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc.
+# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc.
+# Label: "Go Daddy Root Certificate Authority - G2"
+# Serial: 0
+# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01
+# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b
+# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx
+EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT
+EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp
+ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz
+NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH
+EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE
+AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD
+E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH
+/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy
+DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh
+GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR
+tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA
+AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
+FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX
+WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu
+9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr
+gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo
+2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO
+LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI
+4uJEvlz36hz1
+-----END CERTIFICATE-----
+
+# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc.
+# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc.
+# Label: "Starfield Root Certificate Authority - G2"
+# Serial: 0
+# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96
+# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e
+# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5
+-----BEGIN CERTIFICATE-----
+MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx
+EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT
+HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs
+ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw
+MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6
+b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj
+aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp
+Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg
+nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1
+HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N
+Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN
+dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0
+HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO
+BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G
+CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU
+sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3
+4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg
+8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K
+pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1
+mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0
+-----END CERTIFICATE-----
+
+# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc.
+# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc.
+# Label: "Starfield Services Root Certificate Authority - G2"
+# Serial: 0
+# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2
+# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f
+# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5
+-----BEGIN CERTIFICATE-----
+MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx
+EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT
+HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs
+ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5
+MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD
+VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy
+ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy
+dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p
+OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2
+8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K
+Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe
+hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk
+6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw
+DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q
+AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI
+bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB
+ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z
+qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd
+iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn
+0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN
+sSi6
+-----END CERTIFICATE-----
+
+# Issuer: CN=AffirmTrust Commercial O=AffirmTrust
+# Subject: CN=AffirmTrust Commercial O=AffirmTrust
+# Label: "AffirmTrust Commercial"
+# Serial: 8608355977964138876
+# MD5 Fingerprint: 82:92:ba:5b:ef:cd:8a:6f:a6:3d:55:f9:84:f6:d6:b7
+# SHA1 Fingerprint: f9:b5:b6:32:45:5f:9c:be:ec:57:5f:80:dc:e9:6e:2c:c7:b2:78:b7
+# SHA256 Fingerprint: 03:76:ab:1d:54:c5:f9:80:3c:e4:b2:e2:01:a0:ee:7e:ef:7b:57:b6:36:e8:a9:3c:9b:8d:48:60:c9:6f:5f:a7
+-----BEGIN CERTIFICATE-----
+MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE
+BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz
+dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL
+MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp
+cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP
+Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr
+ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL
+MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1
+yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr
+VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/
+nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ
+KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG
+XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj
+vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt
+Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g
+N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC
+nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8=
+-----END CERTIFICATE-----
+
+# Issuer: CN=AffirmTrust Networking O=AffirmTrust
+# Subject: CN=AffirmTrust Networking O=AffirmTrust
+# Label: "AffirmTrust Networking"
+# Serial: 8957382827206547757
+# MD5 Fingerprint: 42:65:ca:be:01:9a:9a:4c:a9:8c:41:49:cd:c0:d5:7f
+# SHA1 Fingerprint: 29:36:21:02:8b:20:ed:02:f5:66:c5:32:d1:d6:ed:90:9f:45:00:2f
+# SHA256 Fingerprint: 0a:81:ec:5a:92:97:77:f1:45:90:4a:f3:8d:5d:50:9f:66:b5:e2:c5:8f:cd:b5:31:05:8b:0e:17:f3:f0:b4:1b
+-----BEGIN CERTIFICATE-----
+MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE
+BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz
+dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL
+MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp
+cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y
+YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua
+kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL
+QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp
+6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG
+yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i
+QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ
+KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO
+tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu
+QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ
+Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u
+olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48
+x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s=
+-----END CERTIFICATE-----
+
+# Issuer: CN=AffirmTrust Premium O=AffirmTrust
+# Subject: CN=AffirmTrust Premium O=AffirmTrust
+# Label: "AffirmTrust Premium"
+# Serial: 7893706540734352110
+# MD5 Fingerprint: c4:5d:0e:48:b6:ac:28:30:4e:0a:bc:f9:38:16:87:57
+# SHA1 Fingerprint: d8:a6:33:2c:e0:03:6f:b1:85:f6:63:4f:7d:6a:06:65:26:32:28:27
+# SHA256 Fingerprint: 70:a7:3f:7f:37:6b:60:07:42:48:90:45:34:b1:14:82:d5:bf:0e:69:8e:cc:49:8d:f5:25:77:eb:f2:e9:3b:9a
+-----BEGIN CERTIFICATE-----
+MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE
+BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz
+dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG
+A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U
+cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf
+qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ
+JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ
++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS
+s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5
+HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7
+70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG
+V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S
+qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S
+5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia
+C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX
+OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE
+FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
+BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2
+KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg
+Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B
+8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ
+MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc
+0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ
+u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF
+u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH
+YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8
+GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO
+RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e
+KeC2uAloGRwYQw==
+-----END CERTIFICATE-----
+
+# Issuer: CN=AffirmTrust Premium ECC O=AffirmTrust
+# Subject: CN=AffirmTrust Premium ECC O=AffirmTrust
+# Label: "AffirmTrust Premium ECC"
+# Serial: 8401224907861490260
+# MD5 Fingerprint: 64:b0:09:55:cf:b1:d5:99:e2:be:13:ab:a6:5d:ea:4d
+# SHA1 Fingerprint: b8:23:6b:00:2f:1d:16:86:53:01:55:6c:11:a4:37:ca:eb:ff:c3:bb
+# SHA256 Fingerprint: bd:71:fd:f6:da:97:e4:cf:62:d1:64:7a:dd:25:81:b0:7d:79:ad:f8:39:7e:b4:ec:ba:9c:5e:84:88:82:14:23
+-----BEGIN CERTIFICATE-----
+MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC
+VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ
+cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ
+BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt
+VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D
+0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9
+ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G
+A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G
+A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs
+aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I
+flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ==
+-----END CERTIFICATE-----
+
+# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority
+# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority
+# Label: "Certum Trusted Network CA"
+# Serial: 279744
+# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78
+# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e
+# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e
+-----BEGIN CERTIFICATE-----
+MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM
+MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D
+ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU
+cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3
+WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg
+Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw
+IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH
+UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM
+TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU
+BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM
+kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x
+AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV
+HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y
+sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL
+I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8
+J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY
+VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI
+03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw=
+-----END CERTIFICATE-----
+
+# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA
+# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA
+# Label: "TWCA Root Certification Authority"
+# Serial: 1
+# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79
+# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48
+# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44
+-----BEGIN CERTIFICATE-----
+MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES
+MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU
+V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz
+WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO
+LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm
+aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE
+AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH
+K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX
+RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z
+rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx
+3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq
+hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC
+MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls
+XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D
+lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn
+aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ
+YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw==
+-----END CERTIFICATE-----
+
+# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2
+# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2
+# Label: "Security Communication RootCA2"
+# Serial: 0
+# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43
+# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74
+# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl
+MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe
+U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX
+DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy
+dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj
+YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV
+OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr
+zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM
+VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ
+hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO
+ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw
+awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs
+OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
+DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF
+coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc
+okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8
+t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy
+1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/
+SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03
+-----END CERTIFICATE-----
+
+# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority
+# Subject: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority
+# Label: "Hellenic Academic and Research Institutions RootCA 2011"
+# Serial: 0
+# MD5 Fingerprint: 73:9f:4c:4b:73:5b:79:e9:fa:ba:1c:ef:6e:cb:d5:c9
+# SHA1 Fingerprint: fe:45:65:9b:79:03:5b:98:a1:61:b5:51:2e:ac:da:58:09:48:22:4d
+# SHA256 Fingerprint: bc:10:4f:15:a4:8b:e7:09:dc:a5:42:a7:e1:d4:b9:df:6f:05:45:27:e8:02:ea:a9:2d:59:54:44:25:8a:fe:71
+-----BEGIN CERTIFICATE-----
+MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix
+RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1
+dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p
+YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw
+NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK
+EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl
+cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl
+c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz
+dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ
+fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns
+bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD
+75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP
+FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV
+HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp
+5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu
+b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA
+A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p
+6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8
+TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7
+dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys
+Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI
+l7WdmplNsDz4SgCbZN2fOUvRJ9e4
+-----END CERTIFICATE-----
+
+# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967
+# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967
+# Label: "Actalis Authentication Root CA"
+# Serial: 6271844772424770508
+# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6
+# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac
+# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66
+-----BEGIN CERTIFICATE-----
+MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE
+BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w
+MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290
+IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC
+SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1
+ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv
+UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX
+4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9
+KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/
+gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb
+rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ
+51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F
+be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe
+KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F
+v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn
+fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7
+jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz
+ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt
+ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL
+e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70
+jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz
+WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V
+SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j
+pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX
+X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok
+fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R
+K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU
+ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU
+LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT
+LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==
+-----END CERTIFICATE-----
+
+# Issuer: O=Trustis Limited OU=Trustis FPS Root CA
+# Subject: O=Trustis Limited OU=Trustis FPS Root CA
+# Label: "Trustis FPS Root CA"
+# Serial: 36053640375399034304724988975563710553
+# MD5 Fingerprint: 30:c9:e7:1e:6b:e6:14:eb:65:b2:16:69:20:31:67:4d
+# SHA1 Fingerprint: 3b:c0:38:0b:33:c3:f6:a6:0c:86:15:22:93:d9:df:f5:4b:81:c0:04
+# SHA256 Fingerprint: c1:b4:82:99:ab:a5:20:8f:e9:63:0a:ce:55:ca:68:a0:3e:da:5a:51:9c:88:02:a0:d3:a6:73:be:8f:8e:55:7d
+-----BEGIN CERTIFICATE-----
+MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF
+MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL
+ExNUcnVzdGlzIEZQUyBSb290IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTEx
+MzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc
+MBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQRUN+
+AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihH
+iTHcDnlkH5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjj
+vSkCqPoc4Vu5g6hBSLwacY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA
+0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zto3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlB
+OrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEAAaNTMFEwDwYDVR0TAQH/
+BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAdBgNVHQ4E
+FgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01
+GX2cGE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmW
+zaD+vkAMXBJV+JOCyinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP4
+1BIy+Q7DsdwyhEQsb8tGD+pmQQ9P8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZE
+f1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHVl/9D7S3B2l0pKoU/rGXuhg8F
+jZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN
+ZetX2fNXlrtIzYE=
+-----END CERTIFICATE-----
+
+# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327
+# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327
+# Label: "Buypass Class 2 Root CA"
+# Serial: 2
+# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29
+# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99
+# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48
+-----BEGIN CERTIFICATE-----
+MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd
+MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg
+Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow
+TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw
+HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB
+BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr
+6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV
+L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91
+1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx
+MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ
+QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB
+arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr
+Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi
+FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS
+P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN
+9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP
+AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz
+uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h
+9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s
+A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t
+OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo
++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7
+KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2
+DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us
+H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ
+I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7
+5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h
+3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz
+Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA=
+-----END CERTIFICATE-----
+
+# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327
+# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327
+# Label: "Buypass Class 3 Root CA"
+# Serial: 2
+# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec
+# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57
+# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d
+-----BEGIN CERTIFICATE-----
+MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd
+MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg
+Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow
+TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw
+HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB
+BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y
+ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E
+N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9
+tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX
+0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c
+/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X
+KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY
+zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS
+O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D
+34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP
+K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3
+AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv
+Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj
+QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV
+cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS
+IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2
+HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa
+O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv
+033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u
+dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE
+kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41
+3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD
+u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq
+4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc=
+-----END CERTIFICATE-----
+
+# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center
+# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center
+# Label: "T-TeleSec GlobalRoot Class 3"
+# Serial: 1
+# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef
+# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1
+# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd
+-----BEGIN CERTIFICATE-----
+MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx
+KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd
+BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl
+YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1
+OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy
+aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50
+ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN
+8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/
+RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4
+hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5
+ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM
+EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj
+QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1
+A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy
+WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ
+1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30
+6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT
+91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml
+e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p
+TpPDpFQUWw==
+-----END CERTIFICATE-----
+
+# Issuer: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus
+# Subject: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus
+# Label: "EE Certification Centre Root CA"
+# Serial: 112324828676200291871926431888494945866
+# MD5 Fingerprint: 43:5e:88:d4:7d:1a:4a:7e:fd:84:2e:52:eb:01:d4:6f
+# SHA1 Fingerprint: c9:a8:b9:e7:55:80:5e:58:e3:53:77:a7:25:eb:af:c3:7b:27:cc:d7
+# SHA256 Fingerprint: 3e:84:ba:43:42:90:85:16:e7:75:73:c0:99:2f:09:79:ca:08:4e:46:85:68:1f:f1:95:cc:ba:8a:22:9b:8a:76
+-----BEGIN CERTIFICATE-----
+MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1
+MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1
+czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG
+CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy
+MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl
+ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS
+b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy
+euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO
+bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw
+WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d
+MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE
+1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD
+VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/
+zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB
+BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF
+BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV
+v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG
+E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u
+uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW
+iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v
+GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0=
+-----END CERTIFICATE-----
+
+# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH
+# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH
+# Label: "D-TRUST Root Class 3 CA 2 2009"
+# Serial: 623603
+# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f
+# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0
+# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1
+-----BEGIN CERTIFICATE-----
+MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF
+MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD
+bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha
+ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM
+HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03
+UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42
+tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R
+ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM
+lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp
+/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G
+A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G
+A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj
+dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy
+MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl
+cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js
+L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL
+BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni
+acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0
+o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K
+zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8
+PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y
+Johw1+qRzT65ysCQblrGXnRl11z+o+I=
+-----END CERTIFICATE-----
+
+# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH
+# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH
+# Label: "D-TRUST Root Class 3 CA 2 EV 2009"
+# Serial: 623604
+# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6
+# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83
+# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81
+-----BEGIN CERTIFICATE-----
+MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF
+MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD
+bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw
+NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV
+BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn
+ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0
+3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z
+qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR
+p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8
+HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw
+ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea
+HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw
+Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh
+c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E
+RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt
+dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku
+Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp
+3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05
+nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF
+CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na
+xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX
+KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1
+-----END CERTIFICATE-----
+
+# Issuer: CN=CA Disig Root R2 O=Disig a.s.
+# Subject: CN=CA Disig Root R2 O=Disig a.s.
+# Label: "CA Disig Root R2"
+# Serial: 10572350602393338211
+# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03
+# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71
+# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03
+-----BEGIN CERTIFICATE-----
+MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV
+BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu
+MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy
+MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx
+EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw
+ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe
+NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH
+PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I
+x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe
+QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR
+yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO
+QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912
+H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ
+QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD
+i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs
+nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1
+rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud
+DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI
+hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM
+tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf
+GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb
+lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka
++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal
+TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i
+nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3
+gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr
+G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os
+zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x
+L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL
+-----END CERTIFICATE-----
+
+# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV
+# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV
+# Label: "ACCVRAIZ1"
+# Serial: 6828503384748696800
+# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02
+# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17
+# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13
+-----BEGIN CERTIFICATE-----
+MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE
+AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw
+CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ
+BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND
+VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb
+qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY
+HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo
+G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA
+lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr
+IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/
+0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH
+k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47
+4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO
+m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa
+cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl
+uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI
+KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls
+ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG
+AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2
+VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT
+VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG
+CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA
+cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA
+QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA
+7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA
+cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA
+QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA
+czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu
+aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt
+aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud
+DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF
+BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp
+D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU
+JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m
+AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD
+vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms
+tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH
+7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h
+I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA
+h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF
+d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H
+pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7
+-----END CERTIFICATE-----
+
+# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA
+# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA
+# Label: "TWCA Global Root CA"
+# Serial: 3262
+# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96
+# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65
+# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b
+-----BEGIN CERTIFICATE-----
+MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx
+EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT
+VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5
+NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT
+B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF
+10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz
+0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh
+MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH
+zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc
+46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2
+yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi
+laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP
+oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA
+BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE
+qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm
+4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB
+/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL
+1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn
+LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF
+H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo
+RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+
+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh
+15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW
+6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW
+nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j
+wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz
+aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy
+KwbQBM0=
+-----END CERTIFICATE-----
+
+# Issuer: CN=TeliaSonera Root CA v1 O=TeliaSonera
+# Subject: CN=TeliaSonera Root CA v1 O=TeliaSonera
+# Label: "TeliaSonera Root CA v1"
+# Serial: 199041966741090107964904287217786801558
+# MD5 Fingerprint: 37:41:49:1b:18:56:9a:26:f5:ad:c2:66:fb:40:a5:4c
+# SHA1 Fingerprint: 43:13:bb:96:f1:d5:86:9b:c1:4e:6a:92:f6:cf:f6:34:69:87:82:37
+# SHA256 Fingerprint: dd:69:36:fe:21:f8:f0:77:c1:23:a1:a5:21:c1:22:24:f7:22:55:b7:3e:03:a7:26:06:93:e8:a2:4b:0f:a3:89
+-----BEGIN CERTIFICATE-----
+MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw
+NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv
+b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD
+VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2
+MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F
+VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1
+7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X
+Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+
+/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs
+81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm
+dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe
+Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu
+sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4
+pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs
+slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ
+arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD
+VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG
+9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl
+dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx
+0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj
+TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed
+Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7
+Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI
+OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7
+vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW
+t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn
+HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx
+SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY=
+-----END CERTIFICATE-----
+
+# Issuer: CN=E-Tugra Certification Authority O=E-Tu\u011fra EBG Bili\u015fim Teknolojileri ve Hizmetleri A.\u015e. OU=E-Tugra Sertifikasyon Merkezi
+# Subject: CN=E-Tugra Certification Authority O=E-Tu\u011fra EBG Bili\u015fim Teknolojileri ve Hizmetleri A.\u015e. OU=E-Tugra Sertifikasyon Merkezi
+# Label: "E-Tugra Certification Authority"
+# Serial: 7667447206703254355
+# MD5 Fingerprint: b8:a1:03:63:b0:bd:21:71:70:8a:6f:13:3a:bb:79:49
+# SHA1 Fingerprint: 51:c6:e7:08:49:06:6e:f3:92:d4:5c:a0:0d:6d:a3:62:8f:c3:52:39
+# SHA256 Fingerprint: b0:bf:d5:2b:b0:d7:d9:bd:92:bf:5d:4d:c1:3d:a2:55:c0:2c:54:2f:37:83:65:ea:89:39:11:f5:5e:55:f2:3c
+-----BEGIN CERTIFICATE-----
+MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV
+BAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC
+aWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV
+BAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1
+Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz
+MDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+
+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp
+em1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN
+ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY
+B4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH
+D5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF
+Q9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo
+q1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D
+k14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH
+fC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut
+dEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM
+ti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8
+zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn
+rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX
+U8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6
+Jyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5
+XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF
+Nzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR
+HTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY
+GwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c
+77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3
++GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK
+vJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6
+FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl
+yb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P
+AJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD
+y4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d
+NL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA==
+-----END CERTIFICATE-----
+
+# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center
+# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center
+# Label: "T-TeleSec GlobalRoot Class 2"
+# Serial: 1
+# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a
+# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9
+# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52
+-----BEGIN CERTIFICATE-----
+MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx
+KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd
+BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl
+YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1
+OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy
+aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50
+ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd
+AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC
+FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi
+1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq
+jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ
+wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj
+QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/
+WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy
+NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC
+uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw
+IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6
+g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN
+9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP
+BSeOE6Fuwg==
+-----END CERTIFICATE-----
+
+# Issuer: CN=Atos TrustedRoot 2011 O=Atos
+# Subject: CN=Atos TrustedRoot 2011 O=Atos
+# Label: "Atos TrustedRoot 2011"
+# Serial: 6643877497813316402
+# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56
+# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21
+# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE
+AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG
+EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM
+FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC
+REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp
+Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM
+VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+
+SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ
+4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L
+cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi
+eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV
+HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG
+A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3
+DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j
+vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP
+DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc
+maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D
+lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv
+KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed
+-----END CERTIFICATE-----
+
+# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited
+# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited
+# Label: "QuoVadis Root CA 1 G3"
+# Serial: 687049649626669250736271037606554624078720034195
+# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab
+# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67
+# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00
+MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV
+wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe
+rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341
+68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh
+4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp
+UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o
+abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc
+3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G
+KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt
+hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO
+Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt
+zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD
+ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC
+MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2
+cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN
+qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5
+YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv
+b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2
+8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k
+NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj
+ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp
+q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt
+nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD
+-----END CERTIFICATE-----
+
+# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited
+# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited
+# Label: "QuoVadis Root CA 2 G3"
+# Serial: 390156079458959257446133169266079962026824725800
+# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06
+# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36
+# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00
+MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf
+qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW
+n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym
+c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+
+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1
+o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j
+IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq
+IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz
+8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh
+vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l
+7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG
+cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD
+ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66
+AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC
+roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga
+W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n
+lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE
++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV
+csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd
+dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg
+KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM
+HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4
+WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M
+-----END CERTIFICATE-----
+
+# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited
+# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited
+# Label: "QuoVadis Root CA 3 G3"
+# Serial: 268090761170461462463995952157327242137089239581
+# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7
+# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d
+# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00
+MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR
+/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu
+FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR
+U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c
+ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR
+FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k
+A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw
+eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl
+sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp
+VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q
+A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+
+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD
+ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px
+KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI
+FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv
+oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg
+u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP
+0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf
+3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl
+8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+
+DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN
+PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/
+ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0
+-----END CERTIFICATE-----
+
+# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com
+# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com
+# Label: "DigiCert Assured ID Root G2"
+# Serial: 15385348160840213938643033620894905419
+# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d
+# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f
+# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85
+-----BEGIN CERTIFICATE-----
+MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv
+b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
+cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA
+n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc
+biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp
+EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA
+bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu
+YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB
+AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW
+BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI
+QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I
+0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni
+lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9
+B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv
+ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo
+IhNzbM8m9Yop5w==
+-----END CERTIFICATE-----
+
+# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com
+# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com
+# Label: "DigiCert Assured ID Root G3"
+# Serial: 15459312981008553731928384953135426796
+# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb
+# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89
+# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2
+-----BEGIN CERTIFICATE-----
+MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw
+CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
+ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg
+RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV
+UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
+Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq
+hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf
+Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q
+RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
+BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD
+AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY
+JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv
+6pZjamVFkpUBtA==
+-----END CERTIFICATE-----
+
+# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com
+# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com
+# Label: "DigiCert Global Root G2"
+# Serial: 4293743540046975378534879503202253541
+# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44
+# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4
+# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f
+-----BEGIN CERTIFICATE-----
+MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
+MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT
+MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
+b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI
+2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx
+1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ
+q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz
+tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ
+vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP
+BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV
+5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY
+1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4
+NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG
+Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91
+8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe
+pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl
+MrY=
+-----END CERTIFICATE-----
+
+# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com
+# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com
+# Label: "DigiCert Global Root G3"
+# Serial: 7089244469030293291760083333884364146
+# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca
+# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e
+# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0
+-----BEGIN CERTIFICATE-----
+MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw
+CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
+ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe
+Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw
+EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x
+IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF
+K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG
+fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO
+Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd
+BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx
+AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/
+oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8
+sycX
+-----END CERTIFICATE-----
+
+# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com
+# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com
+# Label: "DigiCert Trusted Root G4"
+# Serial: 7451500558977370777930084869016614236
+# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49
+# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4
+# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88
+-----BEGIN CERTIFICATE-----
+MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg
+RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV
+UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
+Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y
+ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If
+xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV
+ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO
+DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ
+jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/
+CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi
+EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM
+fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY
+uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK
+chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t
+9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD
+ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2
+SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd
++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc
+fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa
+sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N
+cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N
+0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie
+4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI
+r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1
+/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm
+gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+
+-----END CERTIFICATE-----
+
+# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited
+# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited
+# Label: "COMODO RSA Certification Authority"
+# Serial: 101909084537582093308941363524873193117
+# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18
+# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4
+# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34
+-----BEGIN CERTIFICATE-----
+MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB
+hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
+BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5
+MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
+EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
+Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR
+6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X
+pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC
+9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV
+/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf
+Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z
++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w
+qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah
+SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC
+u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf
+Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq
+crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E
+FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB
+/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl
+wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM
+4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV
+2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna
+FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ
+CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK
+boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke
+jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL
+S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb
+QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl
+0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB
+NVOFBkpdn627G190
+-----END CERTIFICATE-----
+
+# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network
+# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network
+# Label: "USERTrust RSA Certification Authority"
+# Serial: 2645093764781058787591871645665788717
+# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5
+# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e
+# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2
+-----BEGIN CERTIFICATE-----
+MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB
+iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
+cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
+BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw
+MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV
+BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU
+aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy
+dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B
+3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY
+tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/
+Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2
+VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT
+79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6
+c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT
+Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l
+c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee
+UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE
+Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd
+BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G
+A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF
+Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO
+VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3
+ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs
+8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR
+iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze
+Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ
+XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/
+qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB
+VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB
+L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG
+jjxDah2nGN59PRbxYvnKkKj9
+-----END CERTIFICATE-----
+
+# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network
+# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network
+# Label: "USERTrust ECC Certification Authority"
+# Serial: 123013823720199481456569720443997572134
+# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1
+# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0
+# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a
+-----BEGIN CERTIFICATE-----
+MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL
+MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl
+eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT
+JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx
+MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
+Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg
+VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm
+aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo
+I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng
+o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G
+A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD
+VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB
+zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW
+RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=
+-----END CERTIFICATE-----
+
+# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4
+# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4
+# Label: "GlobalSign ECC Root CA - R4"
+# Serial: 14367148294922964480859022125800977897474
+# MD5 Fingerprint: 20:f0:27:68:d1:7e:a0:9d:0e:e6:2a:ca:df:5c:89:8e
+# SHA1 Fingerprint: 69:69:56:2e:40:80:f4:24:a1:e7:19:9f:14:ba:f3:ee:58:ab:6a:bb
+# SHA256 Fingerprint: be:c9:49:11:c2:95:56:76:db:6c:0a:55:09:86:d7:6e:3b:a0:05:66:7c:44:2c:97:62:b4:fb:b7:73:de:22:8c
+-----BEGIN CERTIFICATE-----
+MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk
+MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH
+bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX
+DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD
+QSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ
+FspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw
+DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F
+uOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX
+kPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs
+ewv4n4Q=
+-----END CERTIFICATE-----
+
+# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5
+# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5
+# Label: "GlobalSign ECC Root CA - R5"
+# Serial: 32785792099990507226680698011560947931244
+# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08
+# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa
+# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24
+-----BEGIN CERTIFICATE-----
+MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk
+MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH
+bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX
+DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD
+QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu
+MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc
+8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke
+hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD
+VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI
+KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg
+515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO
+xwy8p2Fp8fc74SrL+SvzZpA3
+-----END CERTIFICATE-----
+
+# Issuer: CN=Staat der Nederlanden Root CA - G3 O=Staat der Nederlanden
+# Subject: CN=Staat der Nederlanden Root CA - G3 O=Staat der Nederlanden
+# Label: "Staat der Nederlanden Root CA - G3"
+# Serial: 10003001
+# MD5 Fingerprint: 0b:46:67:07:db:10:2f:19:8c:35:50:60:d1:0b:f4:37
+# SHA1 Fingerprint: d8:eb:6b:41:51:92:59:e0:f3:e7:85:00:c0:3d:b6:88:97:c9:ee:fc
+# SHA256 Fingerprint: 3c:4f:b0:b9:5a:b8:b3:00:32:f4:32:b8:6f:53:5f:e1:72:c1:85:d0:fd:39:86:58:37:cf:36:18:7f:a6:f4:28
+-----BEGIN CERTIFICATE-----
+MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO
+TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh
+dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX
+DTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl
+ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv
+b3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4yolQP
+cPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WW
+IkYFsO2tx1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqX
+xz8ecAgwoNzFs21v0IJyEavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFy
+KJLZWyNtZrVtB0LrpjPOktvA9mxjeM3KTj215VKb8b475lRgsGYeCasH/lSJEULR
+9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUurmkVLoR9BvUhTFXFkC4az
+5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU51nus6+N8
+6U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7
+Ngzp07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHP
+bMk7ccHViLVlvMDoFxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXt
+BznaqB16nzaeErAMZRKQFWDZJkBE41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTt
+XUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMBAAGjQjBAMA8GA1UdEwEB/wQF
+MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleuyjWcLhL75Lpd
+INyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD
+U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwp
+LiniyMMB8jPqKqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8
+Ipf3YF3qKS9Ysr1YvY2WTxB1v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixp
+gZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA8KCWAg8zxXHzniN9lLf9OtMJgwYh
+/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b8KKaa8MFSu1BYBQw
+0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0rmj1A
+fsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq
+4BZ+Extq1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR
+1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/
+QFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM
+94B7IWcnMFk=
+-----END CERTIFICATE-----
+
+# Issuer: CN=Staat der Nederlanden EV Root CA O=Staat der Nederlanden
+# Subject: CN=Staat der Nederlanden EV Root CA O=Staat der Nederlanden
+# Label: "Staat der Nederlanden EV Root CA"
+# Serial: 10000013
+# MD5 Fingerprint: fc:06:af:7b:e8:1a:f1:9a:b4:e8:d2:70:1f:c0:f5:ba
+# SHA1 Fingerprint: 76:e2:7e:c1:4f:db:82:c1:c0:a6:75:b5:05:be:3d:29:b4:ed:db:bb
+# SHA256 Fingerprint: 4d:24:91:41:4c:fe:95:67:46:ec:4c:ef:a6:cf:6f:72:e2:8a:13:29:43:2f:9d:8a:90:7a:c4:cb:5d:ad:c1:5a
+-----BEGIN CERTIFICATE-----
+MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJO
+TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFh
+dCBkZXIgTmVkZXJsYW5kZW4gRVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0y
+MjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5MMR4wHAYDVQQKDBVTdGFhdCBkZXIg
+TmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRlcmxhbmRlbiBFViBS
+b290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkkSzrS
+M4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nC
+UiY4iKTWO0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3d
+Z//BYY1jTw+bbRcwJu+r0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46p
+rfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13l
+pJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gVXJrm0w912fxBmJc+qiXb
+j5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr08C+eKxC
+KFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS
+/ZbV0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0X
+cgOPvZuM5l5Tnrmd74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH
+1vI4gnPah1vlPNOePqc7nvQDs/nxfRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrP
+px9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB
+/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwaivsnuL8wbqg7
+MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI
+eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u
+2dfOWBfoqSmuc0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHS
+v4ilf0X8rLiltTMMgsT7B/Zq5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTC
+wPTxGfARKbalGAKb12NMcIxHowNDXLldRqANb/9Zjr7dn3LDWyvfjFvO5QxGbJKy
+CqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tNf1zuacpzEPuKqf2e
+vTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi5Dp6
+Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIa
+Gl6I6lD4WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeL
+eG9QgkRQP2YGiqtDhFZKDyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8
+FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGyeUN51q1veieQA6TqJIc/2b3Z6fJfUEkc
+7uzXLg==
+-----END CERTIFICATE-----
+
+# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust
+# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust
+# Label: "IdenTrust Commercial Root CA 1"
+# Serial: 13298821034946342390520003877796839426
+# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7
+# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25
+# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK
+MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu
+VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw
+MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw
+JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT
+3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU
++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp
+S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1
+bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi
+T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL
+vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK
+Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK
+dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT
+c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv
+l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N
+iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB
+/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD
+ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH
+6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt
+LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93
+nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3
++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK
+W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT
+AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq
+l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG
+4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ
+mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A
+7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H
+-----END CERTIFICATE-----
+
+# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust
+# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust
+# Label: "IdenTrust Public Sector Root CA 1"
+# Serial: 13298821034946342390521976156843933698
+# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba
+# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd
+# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f
+-----BEGIN CERTIFICATE-----
+MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN
+MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu
+VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN
+MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0
+MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7
+ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy
+RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS
+bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF
+/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R
+3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw
+EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy
+9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V
+GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ
+2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV
+WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD
+W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/
+BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN
+AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj
+t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV
+DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9
+TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G
+lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW
+mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df
+WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5
++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ
+tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA
+GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv
+8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c
+-----END CERTIFICATE-----
+
+# Issuer: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only
+# Subject: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only
+# Label: "Entrust Root Certification Authority - G2"
+# Serial: 1246989352
+# MD5 Fingerprint: 4b:e2:c9:91:96:65:0c:f4:0e:5a:93:92:a0:0a:fe:b2
+# SHA1 Fingerprint: 8c:f4:27:fd:79:0c:3a:d1:66:06:8d:e8:1e:57:ef:bb:93:22:72:d4
+# SHA256 Fingerprint: 43:df:57:74:b0:3e:7f:ef:5f:e4:0d:93:1a:7b:ed:f1:bb:2e:6b:42:73:8c:4e:6d:38:41:10:3d:3a:a7:f3:39
+-----BEGIN CERTIFICATE-----
+MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC
+VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50
+cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs
+IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz
+dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy
+NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu
+dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt
+dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0
+aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj
+YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T
+RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN
+cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW
+wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1
+U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0
+jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN
+BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/
+jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ
+Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v
+1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R
+nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH
+VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g==
+-----END CERTIFICATE-----
+
+# Issuer: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only
+# Subject: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only
+# Label: "Entrust Root Certification Authority - EC1"
+# Serial: 51543124481930649114116133369
+# MD5 Fingerprint: b6:7e:1d:f0:58:c5:49:6c:24:3b:3d:ed:98:18:ed:bc
+# SHA1 Fingerprint: 20:d8:06:40:df:9b:25:f5:12:25:3a:11:ea:f7:59:8a:eb:14:b5:47
+# SHA256 Fingerprint: 02:ed:0e:b2:8c:14:da:45:16:5c:56:67:91:70:0d:64:51:d7:fb:56:f0:b2:ab:1d:3b:8e:b0:70:e5:6e:df:f5
+-----BEGIN CERTIFICATE-----
+MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG
+A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3
+d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu
+dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq
+RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy
+MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD
+VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0
+L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g
+Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD
+ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi
+A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt
+ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH
+Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O
+BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC
+R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX
+hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G
+-----END CERTIFICATE-----
+
+# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority
+# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority
+# Label: "CFCA EV ROOT"
+# Serial: 407555286
+# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30
+# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83
+# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd
+-----BEGIN CERTIFICATE-----
+MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD
+TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y
+aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx
+MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j
+aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP
+T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03
+sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL
+TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5
+/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp
+7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz
+EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt
+hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP
+a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot
+aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg
+TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV
+PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv
+cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL
+tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd
+BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB
+ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT
+ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL
+jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS
+ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy
+P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19
+xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d
+Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN
+5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe
+/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z
+AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ
+5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
+-----END CERTIFICATE-----
+
+# Issuer: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903
+# Subject: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903
+# Label: "Certinomis - Root CA"
+# Serial: 1
+# MD5 Fingerprint: 14:0a:fd:8d:a8:28:b5:38:69:db:56:7e:61:22:03:3f
+# SHA1 Fingerprint: 9d:70:bb:01:a5:a4:a0:18:11:2e:f7:1c:01:b9:32:c5:34:e7:88:a8
+# SHA256 Fingerprint: 2a:99:f5:bc:11:74:b7:3c:bb:1d:62:08:84:e0:1c:34:e5:1c:cb:39:78:da:12:5f:0e:33:26:88:83:bf:41:58
+-----BEGIN CERTIFICATE-----
+MIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjET
+MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAb
+BgNVBAMTFENlcnRpbm9taXMgLSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMz
+MTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMx
+FzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRDZXJ0aW5vbWlzIC0g
+Um9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQosP5L2
+fxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJfl
+LieY6pOod5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQV
+WZUKxkd8aRi5pwP5ynapz8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDF
+TKWrteoB4owuZH9kb/2jJZOLyKIOSY008B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb
+5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09xRLWtwHkziOC/7aOgFLSc
+CbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE6OXWk6Ri
+wsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJ
+wx3tFvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SG
+m/lg0h9tkQPTYKbVPZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4
+F2iw4lNVYC2vPsKD2NkJK/DAZNuHi5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZng
+WVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIB
+BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I6tNxIqSSaHh0
+2TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF
+AAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/
+0KGRHCwPT5iVWVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWw
+F6YSjNRieOpWauwK0kDDPAUwPk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZS
+g081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAXlCOotQqSD7J6wWAsOMwaplv/8gzj
+qh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJy29SWwNyhlCVCNSN
+h4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9Iff/
+ql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8V
+btaw5BngDwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwj
+Y/M50n92Uaf0yKHxDHYiI0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ
+8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nMcyrDflOR1m749fPH0FFNjkulW+YZFzvW
+gQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVrhkIGuUE=
+-----END CERTIFICATE-----
+
+# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed
+# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed
+# Label: "OISTE WISeKey Global Root GB CA"
+# Serial: 157768595616588414422159278966750757568
+# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d
+# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed
+# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6
+-----BEGIN CERTIFICATE-----
+MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt
+MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg
+Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i
+YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x
+CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG
+b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh
+bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3
+HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx
+WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX
+1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk
+u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P
+99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r
+M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw
+AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB
+BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh
+cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5
+gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO
+ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf
+aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic
+Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM=
+-----END CERTIFICATE-----
+
+# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A.
+# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A.
+# Label: "SZAFIR ROOT CA2"
+# Serial: 357043034767186914217277344587386743377558296292
+# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99
+# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de
+# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe
+-----BEGIN CERTIFICATE-----
+MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL
+BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6
+ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw
+NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L
+cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg
+Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN
+QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT
+3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw
+3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6
+3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5
+BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN
+XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD
+AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF
+AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw
+8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG
+nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP
+oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy
+d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg
+LvWpCz/UXeHPhJ/iGcJfitYgHuNztw==
+-----END CERTIFICATE-----
+
+# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority
+# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority
+# Label: "Certum Trusted Network CA 2"
+# Serial: 44979900017204383099463764357512596969
+# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2
+# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92
+# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04
+-----BEGIN CERTIFICATE-----
+MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB
+gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu
+QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG
+A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz
+OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ
+VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp
+ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3
+b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA
+DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn
+0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB
+OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE
+fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E
+Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m
+o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i
+sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW
+OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez
+Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS
+adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n
+3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD
+AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC
+AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ
+F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf
+CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29
+XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm
+djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/
+WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb
+AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq
+P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko
+b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj
+XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P
+5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi
+DrW5viSP
+-----END CERTIFICATE-----
+
+# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority
+# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority
+# Label: "Hellenic Academic and Research Institutions RootCA 2015"
+# Serial: 0
+# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce
+# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6
+# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36
+-----BEGIN CERTIFICATE-----
+MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix
+DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k
+IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT
+N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v
+dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG
+A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh
+ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx
+QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1
+dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA
+4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0
+AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10
+4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C
+ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV
+9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD
+gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6
+Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq
+NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko
+LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc
+Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV
+HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd
+ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I
+XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI
+M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot
+9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V
+Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea
+j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh
+X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ
+l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf
+bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4
+pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK
+e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0
+vm9qp/UsQu0yrbYhnr68
+-----END CERTIFICATE-----
+
+# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority
+# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority
+# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015"
+# Serial: 0
+# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef
+# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66
+# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33
+-----BEGIN CERTIFICATE-----
+MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN
+BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl
+c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl
+bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv
+b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ
+BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj
+YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5
+MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0
+dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg
+QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa
+jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC
+MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi
+C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep
+lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof
+TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR
+-----END CERTIFICATE-----
+
+# Issuer: CN=Certplus Root CA G1 O=Certplus
+# Subject: CN=Certplus Root CA G1 O=Certplus
+# Label: "Certplus Root CA G1"
+# Serial: 1491911565779898356709731176965615564637713
+# MD5 Fingerprint: 7f:09:9c:f7:d9:b9:5c:69:69:56:d5:37:3e:14:0d:42
+# SHA1 Fingerprint: 22:fd:d0:b7:fd:a2:4e:0d:ac:49:2c:a0:ac:a6:7b:6a:1f:e3:f7:66
+# SHA256 Fingerprint: 15:2a:40:2b:fc:df:2c:d5:48:05:4d:22:75:b3:9c:7f:ca:3e:c0:97:80:78:b0:f0:ea:76:e5:61:a6:c7:43:3e
+-----BEGIN CERTIFICATE-----
+MIIFazCCA1OgAwIBAgISESBVg+QtPlRWhS2DN7cs3EYRMA0GCSqGSIb3DQEBDQUA
+MD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy
+dHBsdXMgUm9vdCBDQSBHMTAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBa
+MD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy
+dHBsdXMgUm9vdCBDQSBHMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
+ANpQh7bauKk+nWT6VjOaVj0W5QOVsjQcmm1iBdTYj+eJZJ+622SLZOZ5KmHNr49a
+iZFluVj8tANfkT8tEBXgfs+8/H9DZ6itXjYj2JizTfNDnjl8KvzsiNWI7nC9hRYt
+6kuJPKNxQv4c/dMcLRC4hlTqQ7jbxofaqK6AJc96Jh2qkbBIb6613p7Y1/oA/caP
+0FG7Yn2ksYyy/yARujVjBYZHYEMzkPZHogNPlk2dT8Hq6pyi/jQu3rfKG3akt62f
+6ajUeD94/vI4CTYd0hYCyOwqaK/1jpTvLRN6HkJKHRUxrgwEV/xhc/MxVoYxgKDE
+EW4wduOU8F8ExKyHcomYxZ3MVwia9Az8fXoFOvpHgDm2z4QTd28n6v+WZxcIbekN
+1iNQMLAVdBM+5S//Ds3EC0pd8NgAM0lm66EYfFkuPSi5YXHLtaW6uOrc4nBvCGrc
+h2c0798wct3zyT8j/zXhviEpIDCB5BmlIOklynMxdCm+4kLV87ImZsdo/Rmz5yCT
+mehd4F6H50boJZwKKSTUzViGUkAksnsPmBIgJPaQbEfIDbsYIC7Z/fyL8inqh3SV
+4EJQeIQEQWGw9CEjjy3LKCHyamz0GqbFFLQ3ZU+V/YDI+HLlJWvEYLF7bY5KinPO
+WftwenMGE9nTdDckQQoRb5fc5+R+ob0V8rqHDz1oihYHAgMBAAGjYzBhMA4GA1Ud
+DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSowcCbkahDFXxd
+Bie0KlHYlwuBsTAfBgNVHSMEGDAWgBSowcCbkahDFXxdBie0KlHYlwuBsTANBgkq
+hkiG9w0BAQ0FAAOCAgEAnFZvAX7RvUz1isbwJh/k4DgYzDLDKTudQSk0YcbX8ACh
+66Ryj5QXvBMsdbRX7gp8CXrc1cqh0DQT+Hern+X+2B50ioUHj3/MeXrKls3N/U/7
+/SMNkPX0XtPGYX2eEeAC7gkE2Qfdpoq3DIMku4NQkv5gdRE+2J2winq14J2by5BS
+S7CTKtQ+FjPlnsZlFT5kOwQ/2wyPX1wdaR+v8+khjPPvl/aatxm2hHSco1S1cE5j
+2FddUyGbQJJD+tZ3VTNPZNX70Cxqjm0lpu+F6ALEUz65noe8zDUa3qHpimOHZR4R
+Kttjd5cUvpoUmRGywO6wT/gUITJDT5+rosuoD6o7BlXGEilXCNQ314cnrUlZp5Gr
+RHpejXDbl85IULFzk/bwg2D5zfHhMf1bfHEhYxQUqq/F3pN+aLHsIqKqkHWetUNy
+6mSjhEv9DKgma3GX7lZjZuhCVPnHHd/Qj1vfyDBviP4NxDMcU6ij/UgQ8uQKTuEV
+V/xuZDDCVRHc6qnNSlSsKWNEz0pAoNZoWRsz+e86i9sgktxChL8Bq4fA1SCC28a5
+g4VCXA9DO2pJNdWY9BW/+mGBDAkgGNLQFwzLSABQ6XaCjGTXOqAHVcweMcDvOrRl
+++O/QmueD6i9a5jc2NvLi6Td11n0bt3+qsOR0C5CB8AMTVPNJLFMWx5R9N/pkvo=
+-----END CERTIFICATE-----
+
+# Issuer: CN=Certplus Root CA G2 O=Certplus
+# Subject: CN=Certplus Root CA G2 O=Certplus
+# Label: "Certplus Root CA G2"
+# Serial: 1492087096131536844209563509228951875861589
+# MD5 Fingerprint: a7:ee:c4:78:2d:1b:ee:2d:b9:29:ce:d6:a7:96:32:31
+# SHA1 Fingerprint: 4f:65:8e:1f:e9:06:d8:28:02:e9:54:47:41:c9:54:25:5d:69:cc:1a
+# SHA256 Fingerprint: 6c:c0:50:41:e6:44:5e:74:69:6c:4c:fb:c9:f8:0f:54:3b:7e:ab:bb:44:b4:ce:6f:78:7c:6a:99:71:c4:2f:17
+-----BEGIN CERTIFICATE-----
+MIICHDCCAaKgAwIBAgISESDZkc6uo+jF5//pAq/Pc7xVMAoGCCqGSM49BAMDMD4x
+CzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs
+dXMgUm9vdCBDQSBHMjAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBaMD4x
+CzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs
+dXMgUm9vdCBDQSBHMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABM0PW1aC3/BFGtat
+93nwHcmsltaeTpwftEIRyoa/bfuFo8XlGVzX7qY/aWfYeOKmycTbLXku54uNAm8x
+Ik0G42ByRZ0OQneezs/lf4WbGOT8zC5y0xaTTsqZY1yhBSpsBqNjMGEwDgYDVR0P
+AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNqDYwJ5jtpMxjwj
+FNiPwyCrKGBZMB8GA1UdIwQYMBaAFNqDYwJ5jtpMxjwjFNiPwyCrKGBZMAoGCCqG
+SM49BAMDA2gAMGUCMHD+sAvZ94OX7PNVHdTcswYO/jOYnYs5kGuUIe22113WTNch
+p+e/IQ8rzfcq3IUHnQIxAIYUFuXcsGXCwI4Un78kFmjlvPl5adytRSv3tjFzzAal
+U5ORGpOucGpnutee5WEaXw==
+-----END CERTIFICATE-----
+
+# Issuer: CN=OpenTrust Root CA G1 O=OpenTrust
+# Subject: CN=OpenTrust Root CA G1 O=OpenTrust
+# Label: "OpenTrust Root CA G1"
+# Serial: 1492036577811947013770400127034825178844775
+# MD5 Fingerprint: 76:00:cc:81:29:cd:55:5e:88:6a:7a:2e:f7:4d:39:da
+# SHA1 Fingerprint: 79:91:e8:34:f7:e2:ee:dd:08:95:01:52:e9:55:2d:14:e9:58:d5:7e
+# SHA256 Fingerprint: 56:c7:71:28:d9:8c:18:d9:1b:4c:fd:ff:bc:25:ee:91:03:d4:75:8e:a2:ab:ad:82:6a:90:f3:45:7d:46:0e:b4
+-----BEGIN CERTIFICATE-----
+MIIFbzCCA1egAwIBAgISESCzkFU5fX82bWTCp59rY45nMA0GCSqGSIb3DQEBCwUA
+MEAxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9w
+ZW5UcnVzdCBSb290IENBIEcxMB4XDTE0MDUyNjA4NDU1MFoXDTM4MDExNTAwMDAw
+MFowQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwU
+T3BlblRydXN0IFJvb3QgQ0EgRzEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQD4eUbalsUwXopxAy1wpLuwxQjczeY1wICkES3d5oeuXT2R0odsN7faYp6b
+wiTXj/HbpqbfRm9RpnHLPhsxZ2L3EVs0J9V5ToybWL0iEA1cJwzdMOWo010hOHQX
+/uMftk87ay3bfWAfjH1MBcLrARYVmBSO0ZB3Ij/swjm4eTrwSSTilZHcYTSSjFR0
+77F9jAHiOH3BX2pfJLKOYheteSCtqx234LSWSE9mQxAGFiQD4eCcjsZGT44ameGP
+uY4zbGneWK2gDqdkVBFpRGZPTBKnjix9xNRbxQA0MMHZmf4yzgeEtE7NCv82TWLx
+p2NX5Ntqp66/K7nJ5rInieV+mhxNaMbBGN4zK1FGSxyO9z0M+Yo0FMT7MzUj8czx
+Kselu7Cizv5Ta01BG2Yospb6p64KTrk5M0ScdMGTHPjgniQlQ/GbI4Kq3ywgsNw2
+TgOzfALU5nsaqocTvz6hdLubDuHAk5/XpGbKuxs74zD0M1mKB3IDVedzagMxbm+W
+G+Oin6+Sx+31QrclTDsTBM8clq8cIqPQqwWyTBIjUtz9GVsnnB47ev1CI9sjgBPw
+vFEVVJSmdz7QdFG9URQIOTfLHzSpMJ1ShC5VkLG631UAC9hWLbFJSXKAqWLXwPYY
+EQRVzXR7z2FwefR7LFxckvzluFqrTJOVoSfupb7PcSNCupt2LQIDAQABo2MwYTAO
+BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUl0YhVyE1
+2jZVx/PxN3DlCPaTKbYwHwYDVR0jBBgwFoAUl0YhVyE12jZVx/PxN3DlCPaTKbYw
+DQYJKoZIhvcNAQELBQADggIBAB3dAmB84DWn5ph76kTOZ0BP8pNuZtQ5iSas000E
+PLuHIT839HEl2ku6q5aCgZG27dmxpGWX4m9kWaSW7mDKHyP7Rbr/jyTwyqkxf3kf
+gLMtMrpkZ2CvuVnN35pJ06iCsfmYlIrM4LvgBBuZYLFGZdwIorJGnkSI6pN+VxbS
+FXJfLkur1J1juONI5f6ELlgKn0Md/rcYkoZDSw6cMoYsYPXpSOqV7XAp8dUv/TW0
+V8/bhUiZucJvbI/NeJWsZCj9VrDDb8O+WVLhX4SPgPL0DTatdrOjteFkdjpY3H1P
+XlZs5VVZV6Xf8YpmMIzUUmI4d7S+KNfKNsSbBfD4Fdvb8e80nR14SohWZ25g/4/I
+i+GOvUKpMwpZQhISKvqxnUOOBZuZ2mKtVzazHbYNeS2WuOvyDEsMpZTGMKcmGS3t
+TAZQMPH9WD25SxdfGbRqhFS0OE85og2WaMMolP3tLR9Ka0OWLpABEPs4poEL0L91
+09S5zvE/bw4cHjdx5RiHdRk/ULlepEU0rbDK5uUTdg8xFKmOLZTW1YVNcxVPS/Ky
+Pu1svf0OnWZzsD2097+o4BGkxK51CUpjAEggpsadCwmKtODmzj7HPiY46SvepghJ
+AwSQiumPv+i2tCqjI40cHLI5kqiPAlxAOXXUc0ECd97N4EOH1uS6SsNsEn/+KuYj
+1oxx
+-----END CERTIFICATE-----
+
+# Issuer: CN=OpenTrust Root CA G2 O=OpenTrust
+# Subject: CN=OpenTrust Root CA G2 O=OpenTrust
+# Label: "OpenTrust Root CA G2"
+# Serial: 1492012448042702096986875987676935573415441
+# MD5 Fingerprint: 57:24:b6:59:24:6b:ae:c8:fe:1c:0c:20:f2:c0:4e:eb
+# SHA1 Fingerprint: 79:5f:88:60:c5:ab:7c:3d:92:e6:cb:f4:8d:e1:45:cd:11:ef:60:0b
+# SHA256 Fingerprint: 27:99:58:29:fe:6a:75:15:c1:bf:e8:48:f9:c4:76:1d:b1:6c:22:59:29:25:7b:f4:0d:08:94:f2:9e:a8:ba:f2
+-----BEGIN CERTIFICATE-----
+MIIFbzCCA1egAwIBAgISESChaRu/vbm9UpaPI+hIvyYRMA0GCSqGSIb3DQEBDQUA
+MEAxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9w
+ZW5UcnVzdCBSb290IENBIEcyMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAw
+MFowQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwU
+T3BlblRydXN0IFJvb3QgQ0EgRzIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQDMtlelM5QQgTJT32F+D3Y5z1zCU3UdSXqWON2ic2rxb95eolq5cSG+Ntmh
+/LzubKh8NBpxGuga2F8ORAbtp+Dz0mEL4DKiltE48MLaARf85KxP6O6JHnSrT78e
+CbY2albz4e6WiWYkBuTNQjpK3eCasMSCRbP+yatcfD7J6xcvDH1urqWPyKwlCm/6
+1UWY0jUJ9gNDlP7ZvyCVeYCYitmJNbtRG6Q3ffyZO6v/v6wNj0OxmXsWEH4db0fE
+FY8ElggGQgT4hNYdvJGmQr5J1WqIP7wtUdGejeBSzFfdNTVY27SPJIjki9/ca1TS
+gSuyzpJLHB9G+h3Ykst2Z7UJmQnlrBcUVXDGPKBWCgOz3GIZ38i1MH/1PCZ1Eb3X
+G7OHngevZXHloM8apwkQHZOJZlvoPGIytbU6bumFAYueQ4xncyhZW+vj3CzMpSZy
+YhK05pyDRPZRpOLAeiRXyg6lPzq1O4vldu5w5pLeFlwoW5cZJ5L+epJUzpM5ChaH
+vGOz9bGTXOBut9Dq+WIyiET7vycotjCVXRIouZW+j1MY5aIYFuJWpLIsEPUdN6b4
+t/bQWVyJ98LVtZR00dX+G7bw5tYee9I8y6jj9RjzIR9u701oBnstXW5DiabA+aC/
+gh7PU3+06yzbXfZqfUAkBXKJOAGTy3HCOV0GEfZvePg3DTmEJwIDAQABo2MwYTAO
+BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUajn6QiL3
+5okATV59M4PLuG53hq8wHwYDVR0jBBgwFoAUajn6QiL35okATV59M4PLuG53hq8w
+DQYJKoZIhvcNAQENBQADggIBAJjLq0A85TMCl38th6aP1F5Kr7ge57tx+4BkJamz
+Gj5oXScmp7oq4fBXgwpkTx4idBvpkF/wrM//T2h6OKQQbA2xx6R3gBi2oihEdqc0
+nXGEL8pZ0keImUEiyTCYYW49qKgFbdEfwFFEVn8nNQLdXpgKQuswv42hm1GqO+qT
+RmTFAHneIWv2V6CG1wZy7HBGS4tz3aAhdT7cHcCP009zHIXZ/n9iyJVvttN7jLpT
+wm+bREx50B1ws9efAvSyB7DH5fitIw6mVskpEndI2S9G/Tvw/HRwkqWOOAgfZDC2
+t0v7NqwQjqBSM2OdAzVWxWm9xiNaJ5T2pBL4LTM8oValX9YZ6e18CL13zSdkzJTa
+TkZQh+D5wVOAHrut+0dSixv9ovneDiK3PTNZbNTe9ZUGMg1RGUFcPk8G97krgCf2
+o6p6fAbhQ8MTOWIaNr3gKC6UAuQpLmBVrkA9sHSSXvAgZJY/X0VdiLWK2gKgW0VU
+3jg9CcCoSmVGFvyqv1ROTVu+OEO3KMqLM6oaJbolXCkvW0pujOotnCr2BXbgd5eA
+iN1nE28daCSLT7d0geX0YJ96Vdc+N9oWaz53rK4YcJUIeSkDiv7BO7M/Gg+kO14f
+WKGVyasvc0rQLW6aWQ9VGHgtPFGml4vmu7JwqkwR3v98KzfUetF3NI/n+UL3PIEM
+S1IK
+-----END CERTIFICATE-----
+
+# Issuer: CN=OpenTrust Root CA G3 O=OpenTrust
+# Subject: CN=OpenTrust Root CA G3 O=OpenTrust
+# Label: "OpenTrust Root CA G3"
+# Serial: 1492104908271485653071219941864171170455615
+# MD5 Fingerprint: 21:37:b4:17:16:92:7b:67:46:70:a9:96:d7:a8:13:24
+# SHA1 Fingerprint: 6e:26:64:f3:56:bf:34:55:bf:d1:93:3f:7c:01:de:d8:13:da:8a:a6
+# SHA256 Fingerprint: b7:c3:62:31:70:6e:81:07:8c:36:7c:b8:96:19:8f:1e:32:08:dd:92:69:49:dd:8f:57:09:a4:10:f7:5b:62:92
+-----BEGIN CERTIFICATE-----
+MIICITCCAaagAwIBAgISESDm+Ez8JLC+BUCs2oMbNGA/MAoGCCqGSM49BAMDMEAx
+CzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9wZW5U
+cnVzdCBSb290IENBIEczMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAwMFow
+QDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwUT3Bl
+blRydXN0IFJvb3QgQ0EgRzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARK7liuTcpm
+3gY6oxH84Bjwbhy6LTAMidnW7ptzg6kjFYwvWYpa3RTqnVkrQ7cG7DK2uu5Bta1d
+oYXM6h0UZqNnfkbilPPntlahFVmhTzeXuSIevRHr9LIfXsMUmuXZl5mjYzBhMA4G
+A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRHd8MUi2I5
+DMlv4VBN0BBY3JWIbTAfBgNVHSMEGDAWgBRHd8MUi2I5DMlv4VBN0BBY3JWIbTAK
+BggqhkjOPQQDAwNpADBmAjEAj6jcnboMBBf6Fek9LykBl7+BFjNAk2z8+e2AcG+q
+j9uEwov1NcoG3GRvaBbhj5G5AjEA2Euly8LQCGzpGPta3U1fJAuwACEl74+nBCZx
+4nxp5V2a+EEfOzmTk51V6s2N8fvB
+-----END CERTIFICATE-----
+
+# Issuer: CN=ISRG Root X1 O=Internet Security Research Group
+# Subject: CN=ISRG Root X1 O=Internet Security Research Group
+# Label: "ISRG Root X1"
+# Serial: 172886928669790476064670243504169061120
+# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e
+# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8
+# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6
+-----BEGIN CERTIFICATE-----
+MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
+TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
+cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
+WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
+ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
+MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
+h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
+A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
+T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
+B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
+B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
+KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
+OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
+jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
+qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
+rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
+hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
+ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
+3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
+NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
+ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
+TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
+jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
+oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
+4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
+mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
+emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
+-----END CERTIFICATE-----
+
+# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM
+# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM
+# Label: "AC RAIZ FNMT-RCM"
+# Serial: 485876308206448804701554682760554759
+# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d
+# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20
+# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa
+-----BEGIN CERTIFICATE-----
+MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx
+CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ
+WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ
+BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG
+Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/
+yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf
+BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz
+WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF
+tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z
+374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC
+IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL
+mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7
+wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS
+MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2
+ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet
+UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw
+AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H
+YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3
+LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD
+nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1
+RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM
+LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf
+77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N
+JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm
+fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp
+6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp
+1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B
+9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok
+RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv
+uu8wd+RU4riEmViAqhOLUTpPSPaLtrM=
+-----END CERTIFICATE-----
+
+# Issuer: CN=Amazon Root CA 1 O=Amazon
+# Subject: CN=Amazon Root CA 1 O=Amazon
+# Label: "Amazon Root CA 1"
+# Serial: 143266978916655856878034712317230054538369994
+# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6
+# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16
+# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e
+-----BEGIN CERTIFICATE-----
+MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
+ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6
+b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL
+MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv
+b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj
+ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM
+9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw
+IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6
+VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L
+93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm
+jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA
+A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI
+U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs
+N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv
+o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU
+5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy
+rqXRfboQnoZsG4q5WTP468SQvvG5
+-----END CERTIFICATE-----
+
+# Issuer: CN=Amazon Root CA 2 O=Amazon
+# Subject: CN=Amazon Root CA 2 O=Amazon
+# Label: "Amazon Root CA 2"
+# Serial: 143266982885963551818349160658925006970653239
+# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66
+# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a
+# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4
+-----BEGIN CERTIFICATE-----
+MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF
+ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6
+b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL
+MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv
+b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK
+gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ
+W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg
+1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K
+8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r
+2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me
+z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR
+8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj
+mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz
+7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6
++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI
+0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB
+Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm
+UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2
+LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY
++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS
+k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl
+7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm
+btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl
+urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+
+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63
+n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE
+76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H
+9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT
+4PsJYGw=
+-----END CERTIFICATE-----
+
+# Issuer: CN=Amazon Root CA 3 O=Amazon
+# Subject: CN=Amazon Root CA 3 O=Amazon
+# Label: "Amazon Root CA 3"
+# Serial: 143266986699090766294700635381230934788665930
+# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87
+# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e
+# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4
+-----BEGIN CERTIFICATE-----
+MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5
+MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
+Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
+A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
+Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl
+ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j
+QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr
+ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr
+BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM
+YyRIHN8wfdVoOw==
+-----END CERTIFICATE-----
+
+# Issuer: CN=Amazon Root CA 4 O=Amazon
+# Subject: CN=Amazon Root CA 4 O=Amazon
+# Label: "Amazon Root CA 4"
+# Serial: 143266989758080763974105200630763877849284878
+# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd
+# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be
+# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92
+-----BEGIN CERTIFICATE-----
+MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5
+MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
+Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
+A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
+Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi
+9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk
+M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB
+/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB
+MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw
+CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW
+1KyLa2tJElMzrdfkviT8tQp21KW8EA==
+-----END CERTIFICATE-----
+
+# Issuer: CN=LuxTrust Global Root 2 O=LuxTrust S.A.
+# Subject: CN=LuxTrust Global Root 2 O=LuxTrust S.A.
+# Label: "LuxTrust Global Root 2"
+# Serial: 59914338225734147123941058376788110305822489521
+# MD5 Fingerprint: b2:e1:09:00:61:af:f7:f1:91:6f:c4:ad:8d:5e:3b:7c
+# SHA1 Fingerprint: 1e:0e:56:19:0a:d1:8b:25:98:b2:04:44:ff:66:8a:04:17:99:5f:3f
+# SHA256 Fingerprint: 54:45:5f:71:29:c2:0b:14:47:c4:18:f9:97:16:8f:24:c5:8f:c5:02:3b:f5:da:5b:e2:eb:6e:1d:d8:90:2e:d5
+-----BEGIN CERTIFICATE-----
+MIIFwzCCA6ugAwIBAgIUCn6m30tEntpqJIWe5rgV0xZ/u7EwDQYJKoZIhvcNAQEL
+BQAwRjELMAkGA1UEBhMCTFUxFjAUBgNVBAoMDUx1eFRydXN0IFMuQS4xHzAdBgNV
+BAMMFkx1eFRydXN0IEdsb2JhbCBSb290IDIwHhcNMTUwMzA1MTMyMTU3WhcNMzUw
+MzA1MTMyMTU3WjBGMQswCQYDVQQGEwJMVTEWMBQGA1UECgwNTHV4VHJ1c3QgUy5B
+LjEfMB0GA1UEAwwWTHV4VHJ1c3QgR2xvYmFsIFJvb3QgMjCCAiIwDQYJKoZIhvcN
+AQEBBQADggIPADCCAgoCggIBANeFl78RmOnwYoNMPIf5U2o3C/IPPIfOb9wmKb3F
+ibrJgz337spbxm1Jc7TJRqMbNBM/wYlFV/TZsfs2ZUv7COJIcRHIbjuend+JZTem
+hfY7RBi2xjcwYkSSl2l9QjAk5A0MiWtj3sXh306pFGxT4GHO9hcvHTy95iJMHZP1
+EMShduxq3sVs35a0VkBCwGKSMKEtFZSg0iAGCW5qbeXrt77U8PEVfIvmTroTzEsn
+Xpk8F12PgX8zPU/TPxvsXD/wPEx1bvKm1Z3aLQdjAsZy6ZS8TEmVT4hSyNvoaYL4
+zDRbIvCGp4m9SAptZoFtyMhk+wHh9OHe2Z7d21vUKpkmFRseTJIpgp7VkoGSQXAZ
+96Tlk0u8d2cx3Rz9MXANF5kM+Qw5GSoXtTBxVdUPrljhPS80m8+f9niFwpN6cj5m
+j5wWEWCPnolvZ77gR1o7DJpni89Gxq44o/KnvObWhWszJHAiS8sIm7vI+AIpHb4g
+DEa/a4ebsypmQjVGbKq6rfmYe+lQVRQxv7HaLe2ArWgk+2mr2HETMOZns4dA/Yl+
+8kPREd8vZS9kzl8UubG/Mb2HeFpZZYiq/FkySIbWTLkpS5XTdvN3JW1CHDiDTf2j
+X5t/Lax5Gw5CMZdjpPuKadUiDTSQMC6otOBttpSsvItO13D8xTiOZCXhTTmQzsmH
+hFhxAgMBAAGjgagwgaUwDwYDVR0TAQH/BAUwAwEB/zBCBgNVHSAEOzA5MDcGByuB
+KwEBAQowLDAqBggrBgEFBQcCARYeaHR0cHM6Ly9yZXBvc2l0b3J5Lmx1eHRydXN0
+Lmx1MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBT/GCh2+UgFLKGu8SsbK7JT
++Et8szAdBgNVHQ4EFgQU/xgodvlIBSyhrvErGyuyU/hLfLMwDQYJKoZIhvcNAQEL
+BQADggIBAGoZFO1uecEsh9QNcH7X9njJCwROxLHOk3D+sFTAMs2ZMGQXvw/l4jP9
+BzZAcg4atmpZ1gDlaCDdLnINH2pkMSCEfUmmWjfrRcmF9dTHF5kH5ptV5AzoqbTO
+jFu1EVzPig4N1qx3gf4ynCSecs5U89BvolbW7MM3LGVYvlcAGvI1+ut7MV3CwRI9
+loGIlonBWVx65n9wNOeD4rHh4bhY79SV5GCc8JaXcozrhAIuZY+kt9J/Z93I055c
+qqmkoCUUBpvsT34tC38ddfEz2O3OuHVtPlu5mB0xDVbYQw8wkbIEa91WvpWAVWe+
+2M2D2RjuLg+GLZKecBPs3lHJQ3gCpU3I+V/EkVhGFndadKpAvAefMLmx9xIX3eP/
+JEAdemrRTxgKqpAd60Ae36EeRJIQmvKN4dFLRp7oRUKX6kWZ8+xm1QL68qZKJKre
+zrnK+T+Tb/mjuuqlPpmt/f97mfVl7vBZKGfXkJWkE4SphMHozs51k2MavDzq1WQf
+LSoSOcbDWjLtR5EWDrw4wVDej8oqkDQc7kGUnF4ZLvhFSZl0kbAEb+MEWrGrKqv+
+x9CWttrhSmQGbmBNvUJO/3jaJMobtNeWOWyu8Q6qp31IiyBMz2TWuJdGsE7RKlY6
+oJO9r4Ak4Ap+58rVyuiFVdw2KuGUaJPHZnJED4AhMmwlxyOAgwrr
+-----END CERTIFICATE-----
+
+# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM
+# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM
+# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1"
+# Serial: 1
+# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49
+# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca
+# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16
+-----BEGIN CERTIFICATE-----
+MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx
+GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp
+bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w
+KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0
+BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy
+dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG
+EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll
+IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU
+QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT
+TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg
+LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7
+a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr
+LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr
+N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X
+YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/
+iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f
+AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH
+V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
+BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh
+AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf
+IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4
+lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c
+8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf
+lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM=
+-----END CERTIFICATE-----
+
+# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD.
+# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD.
+# Label: "GDCA TrustAUTH R5 ROOT"
+# Serial: 9009899650740120186
+# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4
+# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4
+# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93
+-----BEGIN CERTIFICATE-----
+MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE
+BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ
+IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0
+MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV
+BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w
+HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj
+Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj
+TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u
+KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj
+qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm
+MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12
+ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP
+zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk
+L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC
+jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA
+HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC
+AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB
+/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg
+p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm
+DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5
+COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry
+L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf
+JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg
+IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io
+2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV
+09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ
+XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq
+T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe
+MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g==
+-----END CERTIFICATE-----
+
+# Issuer: CN=TrustCor RootCert CA-1 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority
+# Subject: CN=TrustCor RootCert CA-1 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority
+# Label: "TrustCor RootCert CA-1"
+# Serial: 15752444095811006489
+# MD5 Fingerprint: 6e:85:f1:dc:1a:00:d3:22:d5:b2:b2:ac:6b:37:05:45
+# SHA1 Fingerprint: ff:bd:cd:e7:82:c8:43:5e:3c:6f:26:86:5c:ca:a8:3a:45:5b:c3:0a
+# SHA256 Fingerprint: d4:0e:9c:86:cd:8f:e4:68:c1:77:69:59:f4:9e:a7:74:fa:54:86:84:b6:c4:06:f3:90:92:61:f4:dc:e2:57:5c
+-----BEGIN CERTIFICATE-----
+MIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYD
+VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk
+MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U
+cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29y
+IFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkxMjMxMTcyMzE2WjCB
+pDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFuYW1h
+IENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUG
+A1UECwweVHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZU
+cnVzdENvciBSb290Q2VydCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAv463leLCJhJrMxnHQFgKq1mqjQCj/IDHUHuO1CAmujIS2CNUSSUQIpid
+RtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4pQa81QBeCQryJ3pS/C3V
+seq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0JEsq1pme
+9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CV
+EY4hgLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorW
+hnAbJN7+KIor0Gqw/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/
+DeOxCbeKyKsZn3MzUOcwHwYDVR0jBBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcw
+DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD
+ggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5mDo4Nvu7Zp5I
+/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf
+ke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZ
+yonnMlo2HD6CqFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djts
+L1Ac59v2Z3kf9YKVmgenFK+P3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdN
+zl/HHk484IkzlQsPpTLWPFp5LBk=
+-----END CERTIFICATE-----
+
+# Issuer: CN=TrustCor RootCert CA-2 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority
+# Subject: CN=TrustCor RootCert CA-2 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority
+# Label: "TrustCor RootCert CA-2"
+# Serial: 2711694510199101698
+# MD5 Fingerprint: a2:e1:f8:18:0b:ba:45:d5:c7:41:2a:bb:37:52:45:64
+# SHA1 Fingerprint: b8:be:6d:cb:56:f1:55:b9:63:d4:12:ca:4e:06:34:c7:94:b2:1c:c0
+# SHA256 Fingerprint: 07:53:e9:40:37:8c:1b:d5:e3:83:6e:39:5d:ae:a5:cb:83:9e:50:46:f1:bd:0e:ae:19:51:cf:10:fe:c7:c9:65
+-----BEGIN CERTIFICATE-----
+MIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNV
+BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw
+IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy
+dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEfMB0GA1UEAwwWVHJ1c3RDb3Ig
+Um9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEyMzExNzI2MzlaMIGk
+MQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEg
+Q2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYD
+VQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRy
+dXN0Q29yIFJvb3RDZXJ0IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQCnIG7CKqJiJJWQdsg4foDSq8GbZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+
+QVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9NkRvRUqdw6VC0xK5mC8tkq
+1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1oYxOdqHp
+2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nK
+DOObXUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hape
+az6LMvYHL1cEksr1/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF
+3wP+TfSvPd9cW436cOGlfifHhi5qjxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88
+oWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQPeSghYA2FFn3XVDjxklb9tTNM
+g9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+CtgrKAmrhQhJ8Z3
+mjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh
+8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAd
+BgNVHQ4EFgQU2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6U
+nrybPZx9mCAZ5YwwYrIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw
+DQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/hOsh80QA9z+LqBrWyOrsGS2h60COX
+dKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnpkpfbsEZC89NiqpX+
+MWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv2wnL
+/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RX
+CI/hOWB3S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYa
+ZH9bDTMJBzN7Bj8RpFxwPIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW
+2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dvDDqPys/cA8GiCcjl/YBeyGBCARsaU1q7
+N6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYURpFHmygk71dSTlxCnKr3
+Sewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANExdqtvArB
+As8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp
+5KeXRKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu
+1uwJ
+-----END CERTIFICATE-----
+
+# Issuer: CN=TrustCor ECA-1 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority
+# Subject: CN=TrustCor ECA-1 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority
+# Label: "TrustCor ECA-1"
+# Serial: 9548242946988625984
+# MD5 Fingerprint: 27:92:23:1d:0a:f5:40:7c:e9:e6:6b:9d:d8:f5:e7:6c
+# SHA1 Fingerprint: 58:d1:df:95:95:67:6b:63:c0:f0:5b:1c:17:4d:8b:84:0b:c8:78:bd
+# SHA256 Fingerprint: 5a:88:5d:b1:9c:01:d9:12:c5:75:93:88:93:8c:af:bb:df:03:1a:b2:d4:8e:91:ee:15:58:9b:42:97:1d:03:9c
+-----BEGIN CERTIFICATE-----
+MIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYD
+VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk
+MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U
+cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxFzAVBgNVBAMMDlRydXN0Q29y
+IEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3MjgwN1owgZwxCzAJBgNV
+BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw
+IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy
+dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3Ig
+RUNBLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb
+3w9U73NjKYKtR8aja+3+XzP4Q1HpGjORMRegdMTUpwHmspI+ap3tDvl0mEDTPwOA
+BoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23xFUfJ3zSCNV2HykVh0A5
+3ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmcp0yJF4Ou
+owReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/
+wZ0+fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZF
+ZtS6mFjBAgMBAAGjYzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAf
+BgNVHSMEGDAWgBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/
+MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAQEABT41XBVwm8nHc2Fv
+civUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u/ukZMjgDfxT2
+AHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F
+hcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50
+soIipX1TH0XsJ5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BI
+WJZpTdwHjFGTot+fDz2LYLSCjaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1Wi
+tJ/X5g==
+-----END CERTIFICATE-----
+
+# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation
+# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation
+# Label: "SSL.com Root Certification Authority RSA"
+# Serial: 8875640296558310041
+# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29
+# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb
+# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69
+-----BEGIN CERTIFICATE-----
+MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE
+BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK
+DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp
+Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz
+OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv
+dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv
+bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN
+AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R
+xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX
+qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC
+C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3
+6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh
+/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF
+YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E
+JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc
+US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8
+ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm
++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi
+M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV
+HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G
+A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV
+cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc
+Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs
+PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/
+q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0
+cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr
+a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I
+H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y
+K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu
+nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf
+oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY
+Ic2wBlX7Jz9TkHCpBB5XJ7k=
+-----END CERTIFICATE-----
+
+# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation
+# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation
+# Label: "SSL.com Root Certification Authority ECC"
+# Serial: 8495723813297216424
+# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e
+# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a
+# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65
+-----BEGIN CERTIFICATE-----
+MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC
+VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T
+U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0
+aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz
+WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0
+b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS
+b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB
+BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI
+7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg
+CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud
+EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD
+VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T
+kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+
+gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl
+-----END CERTIFICATE-----
+
+# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation
+# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation
+# Label: "SSL.com EV Root Certification Authority RSA R2"
+# Serial: 6248227494352943350
+# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95
+# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a
+# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c
+-----BEGIN CERTIFICATE-----
+MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV
+BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE
+CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy
+dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy
+MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G
+A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD
+DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq
+M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf
+OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa
+4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9
+HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR
+aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA
+b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ
+Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV
+PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO
+pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu
+UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY
+MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV
+HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4
+9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW
+s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5
+Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg
+cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM
+79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz
+/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt
+ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm
+Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK
+QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ
+w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi
+S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07
+mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w==
+-----END CERTIFICATE-----
+
+# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation
+# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation
+# Label: "SSL.com EV Root Certification Authority ECC"
+# Serial: 3182246526754555285
+# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90
+# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d
+# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8
+-----BEGIN CERTIFICATE-----
+MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC
+VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T
+U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp
+Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx
+NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv
+dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv
+bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49
+AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA
+VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku
+WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP
+MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX
+5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ
+ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg
+h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg==
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/certifi/certifi/core.py b/testing/web-platform/tests/tools/third_party/certifi/certifi/core.py
new file mode 100644
index 0000000000..eab9d1d178
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/certifi/certifi/core.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+certifi.py
+~~~~~~~~~~
+
+This module returns the installation location of cacert.pem.
+"""
+import os
+import warnings
+
+
+class DeprecatedBundleWarning(DeprecationWarning):
+ """
+ The weak security bundle is being deprecated. Please bother your service
+ provider to get them to stop using cross-signed roots.
+ """
+
+
+def where():
+ f = os.path.dirname(__file__)
+
+ return os.path.join(f, 'cacert.pem')
+
+
+def old_where():
+ warnings.warn(
+ "The weak security bundle has been removed. certifi.old_where() is now an alias "
+ "of certifi.where(). Please update your code to use certifi.where() instead. "
+ "certifi.old_where() will be removed in 2018.",
+ DeprecatedBundleWarning
+ )
+ return where()
+
+if __name__ == '__main__':
+ print(where())
diff --git a/testing/web-platform/tests/tools/third_party/certifi/setup.cfg b/testing/web-platform/tests/tools/third_party/certifi/setup.cfg
new file mode 100644
index 0000000000..163eba3165
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/certifi/setup.cfg
@@ -0,0 +1,11 @@
+[bdist_wheel]
+universal = 1
+
+[metadata]
+license_file = LICENSE
+
+[egg_info]
+tag_build =
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/testing/web-platform/tests/tools/third_party/certifi/setup.py b/testing/web-platform/tests/tools/third_party/certifi/setup.py
new file mode 100755
index 0000000000..2c20c269f6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/certifi/setup.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from __future__ import with_statement
+import re
+import os
+import sys
+
+# While I generally consider it an antipattern to try and support both
+# setuptools and distutils with a single setup.py, in this specific instance
+# where certifi is a dependency of setuptools, it can create a circular
+# dependency when projects attempt to unbundle stuff from setuptools and pip.
+# Though we don't really support that, it makes things easier if we do this and
+# should hopefully cause less issues for end users.
+try:
+ from setuptools import setup
+except ImportError:
+ from distutils.core import setup
+
+
+version_regex = r'__version__ = ["\']([^"\']*)["\']'
+with open('certifi/__init__.py', 'r') as f:
+ text = f.read()
+ match = re.search(version_regex, text)
+
+ if match:
+ VERSION = match.group(1)
+ else:
+ raise RuntimeError("No version number found!")
+
+if sys.argv[-1] == 'publish':
+ os.system('python setup.py sdist bdist_wheel upload')
+ sys.exit()
+
+required = []
+setup(
+ name='certifi',
+ version=VERSION,
+ description='Python package for providing Mozilla\'s CA Bundle.',
+ long_description=open('README.rst').read(),
+ author='Kenneth Reitz',
+ author_email='me@kennethreitz.com',
+ url='http://certifi.io/',
+ packages=[
+ 'certifi',
+ ],
+ package_dir={'certifi': 'certifi'},
+ package_data={'certifi': ['*.pem']},
+ # data_files=[('certifi', ['certifi/cacert.pem'])],
+ include_package_data=True,
+ zip_safe=False,
+ license='MPL-2.0',
+ classifiers=(
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
+ 'Natural Language :: English',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ ),
+)
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/.coveragerc b/testing/web-platform/tests/tools/third_party/funcsigs/.coveragerc
new file mode 100644
index 0000000000..d83bfc220b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/.coveragerc
@@ -0,0 +1,6 @@
+[run]
+source=funcsigs
+omit=funcsigs/odict*
+
+[report]
+include=funcsigs*
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/.gitignore b/testing/web-platform/tests/tools/third_party/funcsigs/.gitignore
new file mode 100644
index 0000000000..c8d2af85d3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/.gitignore
@@ -0,0 +1,19 @@
+*~
+*.egg
+*.egg-info
+*.pyc
+*.pyo
+*.swp
+.DS_Store
+.coverage
+.tox/
+MANIFEST
+build/
+docs/.build/
+dist/
+env*/
+htmlcov/
+tmp/
+coverage.xml
+junit.xml
+.eggs/
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/.travis.yml b/testing/web-platform/tests/tools/third_party/funcsigs/.travis.yml
new file mode 100644
index 0000000000..c1e7abe0b4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/.travis.yml
@@ -0,0 +1,18 @@
+language: python
+python:
+ - 2.6
+ - 2.7
+ - 3.3
+ - 3.4
+ - 3.5
+ - nightly
+ - pypy
+# - pypy3
+install:
+ - pip install -U pip setuptools wheel
+ - pip install -r requirements/development.txt .
+script:
+ - coverage run setup.py test
+ - coverage report --show-missing
+after_success:
+ - coveralls
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/CHANGELOG b/testing/web-platform/tests/tools/third_party/funcsigs/CHANGELOG
new file mode 100644
index 0000000000..e1366d2668
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/CHANGELOG
@@ -0,0 +1,24 @@
+Changelog
+---------
+
+0.5
+```
+
+* Fix binding with self as a kwarg. (Robert Collins #14)
+
+0.4 (2013-12-20)
+````````````````
+* Fix unbound methods getting their first parameter curried
+* Publish Python wheel packages
+
+0.3 (2013-05-29)
+````````````````
+* Fix annotation formatting of builtin types on Python 2.x
+
+0.2 (2012-01-07)
+````````````````
+* PyPy compatability
+
+0.1 (2012-01-06)
+````````````````
+* Initial release
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/LICENSE b/testing/web-platform/tests/tools/third_party/funcsigs/LICENSE
new file mode 100644
index 0000000000..3e563d6fbd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2013 Aaron Iles
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/MANIFEST.in b/testing/web-platform/tests/tools/third_party/funcsigs/MANIFEST.in
new file mode 100644
index 0000000000..f0abb42f04
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/MANIFEST.in
@@ -0,0 +1,7 @@
+recursive-include docs *
+recursive-include tests *.py
+include *.py
+include CHANGELOG
+include LICENSE
+include MANIFEST.in
+include README.rst
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/Makefile b/testing/web-platform/tests/tools/third_party/funcsigs/Makefile
new file mode 100644
index 0000000000..e2329231b5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/Makefile
@@ -0,0 +1,39 @@
+SHELL := /bin/bash
+
+deps:
+ pip install --upgrade \
+ -r requirements/development.txt \
+ -r requirements/production.txt
+
+sdist:
+ python setup.py sdist
+ python setup.py bdist_wheel
+
+register:
+ python setup.py register
+ python setup.py sdist upload
+ python setup.py bdist_wheel upload
+
+site:
+ cd docs; make html
+
+test:
+ coverage run setup.py test
+
+unittest:
+ coverage run -m unittest discover
+
+lint:
+ flake8 --exit-zero funcsigs tests
+
+coverage:
+ coverage report --show-missing
+
+clean:
+ python setup.py clean --all
+ find . -type f -name "*.pyc" -exec rm '{}' +
+ find . -type d -name "__pycache__" -exec rmdir '{}' +
+ rm -rf *.egg-info .coverage
+ cd docs; make clean
+
+docs: site
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/README.rst b/testing/web-platform/tests/tools/third_party/funcsigs/README.rst
new file mode 100644
index 0000000000..5fbca27e6e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/README.rst
@@ -0,0 +1,353 @@
+.. funcsigs documentation master file, created by
+ sphinx-quickstart on Fri Apr 20 20:27:52 2012.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Introducing funcsigs
+====================
+
+The Funcsigs Package
+--------------------
+
+``funcsigs`` is a backport of the `PEP 362`_ function signature features from
+Python 3.3's `inspect`_ module. The backport is compatible with Python 2.6, 2.7
+as well as 3.3 and up. 3.2 was supported by version 0.4, but with setuptools and
+pip no longer supporting 3.2, we cannot make any statement about 3.2
+compatibility.
+
+Compatibility
+`````````````
+
+The ``funcsigs`` backport has been tested against:
+
+* CPython 2.6
+* CPython 2.7
+* CPython 3.3
+* CPython 3.4
+* CPython 3.5
+* CPython nightlies
+* PyPy and PyPy3(currently failing CI)
+
+Continuous integration testing is provided by `Travis CI`_.
+
+Under Python 2.x there is a compatibility issue when a function is assigned to
+the ``__wrapped__`` property of a class after it has been constructed.
+Similiarily there under PyPy directly passing the ``__call__`` method of a
+builtin is also a compatibility issues. Otherwise the functionality is
+believed to be uniform between both Python2 and Python3.
+
+Issues
+``````
+
+Source code for ``funcsigs`` is hosted on `GitHub`_. Any bug reports or feature
+requests can be made using GitHub's `issues system`_. |build_status| |coverage|
+
+Example
+-------
+
+To obtain a `Signature` object, pass the target function to the
+``funcsigs.signature`` function.
+
+.. code-block:: python
+
+ >>> from funcsigs import signature
+ >>> def foo(a, b=None, *args, **kwargs):
+ ... pass
+ ...
+ >>> sig = signature(foo)
+ >>> sig
+ <funcsigs.Signature object at 0x...>
+ >>> sig.parameters
+ OrderedDict([('a', <Parameter at 0x... 'a'>), ('b', <Parameter at 0x... 'b'>), ('args', <Parameter at 0x... 'args'>), ('kwargs', <Parameter at 0x... 'kwargs'>)])
+ >>> sig.return_annotation
+ <class 'funcsigs._empty'>
+
+Introspecting callables with the Signature object
+-------------------------------------------------
+
+.. note::
+
+ This section of documentation is a direct reproduction of the Python
+ standard library documentation for the inspect module.
+
+The Signature object represents the call signature of a callable object and its
+return annotation. To retrieve a Signature object, use the :func:`signature`
+function.
+
+.. function:: signature(callable)
+
+ Return a :class:`Signature` object for the given ``callable``::
+
+ >>> from funcsigs import signature
+ >>> def foo(a, *, b:int, **kwargs):
+ ... pass
+
+ >>> sig = signature(foo)
+
+ >>> str(sig)
+ '(a, *, b:int, **kwargs)'
+
+ >>> str(sig.parameters['b'])
+ 'b:int'
+
+ >>> sig.parameters['b'].annotation
+ <class 'int'>
+
+ Accepts a wide range of python callables, from plain functions and classes to
+ :func:`functools.partial` objects.
+
+ .. note::
+
+ Some callables may not be introspectable in certain implementations of
+ Python. For example, in CPython, built-in functions defined in C provide
+ no metadata about their arguments.
+
+
+.. class:: Signature
+
+ A Signature object represents the call signature of a function and its return
+ annotation. For each parameter accepted by the function it stores a
+ :class:`Parameter` object in its :attr:`parameters` collection.
+
+ Signature objects are *immutable*. Use :meth:`Signature.replace` to make a
+ modified copy.
+
+ .. attribute:: Signature.empty
+
+ A special class-level marker to specify absence of a return annotation.
+
+ .. attribute:: Signature.parameters
+
+ An ordered mapping of parameters' names to the corresponding
+ :class:`Parameter` objects.
+
+ .. attribute:: Signature.return_annotation
+
+ The "return" annotation for the callable. If the callable has no "return"
+ annotation, this attribute is set to :attr:`Signature.empty`.
+
+ .. method:: Signature.bind(*args, **kwargs)
+
+ Create a mapping from positional and keyword arguments to parameters.
+ Returns :class:`BoundArguments` if ``*args`` and ``**kwargs`` match the
+ signature, or raises a :exc:`TypeError`.
+
+ .. method:: Signature.bind_partial(*args, **kwargs)
+
+ Works the same way as :meth:`Signature.bind`, but allows the omission of
+ some required arguments (mimics :func:`functools.partial` behavior.)
+ Returns :class:`BoundArguments`, or raises a :exc:`TypeError` if the
+ passed arguments do not match the signature.
+
+ .. method:: Signature.replace(*[, parameters][, return_annotation])
+
+ Create a new Signature instance based on the instance replace was invoked
+ on. It is possible to pass different ``parameters`` and/or
+ ``return_annotation`` to override the corresponding properties of the base
+ signature. To remove return_annotation from the copied Signature, pass in
+ :attr:`Signature.empty`.
+
+ ::
+
+ >>> def test(a, b):
+ ... pass
+ >>> sig = signature(test)
+ >>> new_sig = sig.replace(return_annotation="new return anno")
+ >>> str(new_sig)
+ "(a, b) -> 'new return anno'"
+
+
+.. class:: Parameter
+
+ Parameter objects are *immutable*. Instead of modifying a Parameter object,
+ you can use :meth:`Parameter.replace` to create a modified copy.
+
+ .. attribute:: Parameter.empty
+
+ A special class-level marker to specify absence of default values and
+ annotations.
+
+ .. attribute:: Parameter.name
+
+ The name of the parameter as a string. Must be a valid python identifier
+ name (with the exception of ``POSITIONAL_ONLY`` parameters, which can have
+ it set to ``None``).
+
+ .. attribute:: Parameter.default
+
+ The default value for the parameter. If the parameter has no default
+ value, this attribute is set to :attr:`Parameter.empty`.
+
+ .. attribute:: Parameter.annotation
+
+ The annotation for the parameter. If the parameter has no annotation,
+ this attribute is set to :attr:`Parameter.empty`.
+
+ .. attribute:: Parameter.kind
+
+ Describes how argument values are bound to the parameter. Possible values
+ (accessible via :class:`Parameter`, like ``Parameter.KEYWORD_ONLY``):
+
+ +------------------------+----------------------------------------------+
+ | Name | Meaning |
+ +========================+==============================================+
+ | *POSITIONAL_ONLY* | Value must be supplied as a positional |
+ | | argument. |
+ | | |
+ | | Python has no explicit syntax for defining |
+ | | positional-only parameters, but many built-in|
+ | | and extension module functions (especially |
+ | | those that accept only one or two parameters)|
+ | | accept them. |
+ +------------------------+----------------------------------------------+
+ | *POSITIONAL_OR_KEYWORD*| Value may be supplied as either a keyword or |
+ | | positional argument (this is the standard |
+ | | binding behaviour for functions implemented |
+ | | in Python.) |
+ +------------------------+----------------------------------------------+
+ | *VAR_POSITIONAL* | A tuple of positional arguments that aren't |
+ | | bound to any other parameter. This |
+ | | corresponds to a ``*args`` parameter in a |
+ | | Python function definition. |
+ +------------------------+----------------------------------------------+
+ | *KEYWORD_ONLY* | Value must be supplied as a keyword argument.|
+ | | Keyword only parameters are those which |
+ | | appear after a ``*`` or ``*args`` entry in a |
+ | | Python function definition. |
+ +------------------------+----------------------------------------------+
+ | *VAR_KEYWORD* | A dict of keyword arguments that aren't bound|
+ | | to any other parameter. This corresponds to a|
+ | | ``**kwargs`` parameter in a Python function |
+ | | definition. |
+ +------------------------+----------------------------------------------+
+
+ Example: print all keyword-only arguments without default values::
+
+ >>> def foo(a, b, *, c, d=10):
+ ... pass
+
+ >>> sig = signature(foo)
+ >>> for param in sig.parameters.values():
+ ... if (param.kind == param.KEYWORD_ONLY and
+ ... param.default is param.empty):
+ ... print('Parameter:', param)
+ Parameter: c
+
+ .. method:: Parameter.replace(*[, name][, kind][, default][, annotation])
+
+ Create a new Parameter instance based on the instance replaced was invoked
+ on. To override a :class:`Parameter` attribute, pass the corresponding
+ argument. To remove a default value or/and an annotation from a
+ Parameter, pass :attr:`Parameter.empty`.
+
+ ::
+
+ >>> from funcsigs import Parameter
+ >>> param = Parameter('foo', Parameter.KEYWORD_ONLY, default=42)
+ >>> str(param)
+ 'foo=42'
+
+ >>> str(param.replace()) # Will create a shallow copy of 'param'
+ 'foo=42'
+
+ >>> str(param.replace(default=Parameter.empty, annotation='spam'))
+ "foo:'spam'"
+
+
+.. class:: BoundArguments
+
+ Result of a :meth:`Signature.bind` or :meth:`Signature.bind_partial` call.
+ Holds the mapping of arguments to the function's parameters.
+
+ .. attribute:: BoundArguments.arguments
+
+ An ordered, mutable mapping (:class:`collections.OrderedDict`) of
+ parameters' names to arguments' values. Contains only explicitly bound
+ arguments. Changes in :attr:`arguments` will reflect in :attr:`args` and
+ :attr:`kwargs`.
+
+ Should be used in conjunction with :attr:`Signature.parameters` for any
+ argument processing purposes.
+
+ .. note::
+
+ Arguments for which :meth:`Signature.bind` or
+ :meth:`Signature.bind_partial` relied on a default value are skipped.
+ However, if needed, it is easy to include them.
+
+ ::
+
+ >>> def foo(a, b=10):
+ ... pass
+
+ >>> sig = signature(foo)
+ >>> ba = sig.bind(5)
+
+ >>> ba.args, ba.kwargs
+ ((5,), {})
+
+ >>> for param in sig.parameters.values():
+ ... if param.name not in ba.arguments:
+ ... ba.arguments[param.name] = param.default
+
+ >>> ba.args, ba.kwargs
+ ((5, 10), {})
+
+
+ .. attribute:: BoundArguments.args
+
+ A tuple of positional arguments values. Dynamically computed from the
+ :attr:`arguments` attribute.
+
+ .. attribute:: BoundArguments.kwargs
+
+ A dict of keyword arguments values. Dynamically computed from the
+ :attr:`arguments` attribute.
+
+ The :attr:`args` and :attr:`kwargs` properties can be used to invoke
+ functions::
+
+ def test(a, *, b):
+ ...
+
+ sig = signature(test)
+ ba = sig.bind(10, b=20)
+ test(*ba.args, **ba.kwargs)
+
+
+.. seealso::
+
+ :pep:`362` - Function Signature Object.
+ The detailed specification, implementation details and examples.
+
+Copyright
+---------
+
+*funcsigs* is a derived work of CPython under the terms of the `PSF License
+Agreement`_. The original CPython inspect module, its unit tests and
+documentation are the copyright of the Python Software Foundation. The derived
+work is distributed under the `Apache License Version 2.0`_.
+
+.. _PSF License Agreement: http://docs.python.org/3/license.html#terms-and-conditions-for-accessing-or-otherwise-using-python
+.. _Apache License Version 2.0: http://opensource.org/licenses/Apache-2.0
+.. _GitHub: https://github.com/testing-cabal/funcsigs
+.. _PSF License Agreement: http://docs.python.org/3/license.html#terms-and-conditions-for-accessing-or-otherwise-using-python
+.. _Travis CI: http://travis-ci.org/
+.. _Read The Docs: http://funcsigs.readthedocs.org/
+.. _PEP 362: http://www.python.org/dev/peps/pep-0362/
+.. _inspect: http://docs.python.org/3/library/inspect.html#introspecting-callables-with-the-signature-object
+.. _issues system: https://github.com/testing-cabal/funcsigs/issues
+
+.. |build_status| image:: https://secure.travis-ci.org/aliles/funcsigs.png?branch=master
+ :target: http://travis-ci.org/#!/aliles/funcsigs
+ :alt: Current build status
+
+.. |coverage| image:: https://coveralls.io/repos/aliles/funcsigs/badge.png?branch=master
+ :target: https://coveralls.io/r/aliles/funcsigs?branch=master
+ :alt: Coverage status
+
+.. |pypi_version| image:: https://pypip.in/v/funcsigs/badge.png
+ :target: https://crate.io/packages/funcsigs/
+ :alt: Latest PyPI version
+
+
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/docs/Makefile b/testing/web-platform/tests/tools/third_party/funcsigs/docs/Makefile
new file mode 100644
index 0000000000..f7ab3d16b4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/docs/Makefile
@@ -0,0 +1,153 @@
+# 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) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+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 " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @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)
+
+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/funcsigs.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/funcsigs.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/funcsigs"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/funcsigs"
+ @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."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+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/testing/web-platform/tests/tools/third_party/funcsigs/docs/_templates/page.html b/testing/web-platform/tests/tools/third_party/funcsigs/docs/_templates/page.html
new file mode 100644
index 0000000000..5e1e00bcaf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/docs/_templates/page.html
@@ -0,0 +1,9 @@
+{% extends "!page.html" %}
+{% block extrahead %}
+ <a href="https://github.com/aliles/funcsigs">
+ <img style="position: absolute; top: 0; right: 0; border: 0;"
+ src="https://s3.amazonaws.com/github/ribbons/forkme_right_red_aa0000.png"
+ alt="Fork me on GitHub">
+ </a>
+ {{ super() }}
+{% endblock %}
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/docs/conf.py b/testing/web-platform/tests/tools/third_party/funcsigs/docs/conf.py
new file mode 100644
index 0000000000..c6e4194cc0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/docs/conf.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# funcsigs documentation build configuration file, created by
+# sphinx-quickstart on Fri Apr 20 20:27:52 2012.
+#
+# 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.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 = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'funcsigs'
+copyright = '2013, Aaron Iles'
+
+# 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.
+from funcsigs import __version__
+version = '.'.join(__version__.split('.')[:2])
+# The full version, including alpha/beta/rc tags.
+release = __version__
+
+# 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 = 'agogo'
+
+# 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 = []
+
+# 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 = 'funcsigsdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'funcsigs.tex', 'funcsigs Documentation',
+ 'Aaron Iles', '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
+
+# 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', 'funcsigs', 'funcsigs Documentation',
+ ['Aaron Iles'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'funcsigs', 'funcsigs Documentation',
+ 'Aaron Iles', 'funcsigs', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {
+ 'python3': ('http://docs.python.org/py3k', None),
+ 'python': ('http://docs.python.org/', None)
+}
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/funcsigs/__init__.py b/testing/web-platform/tests/tools/third_party/funcsigs/funcsigs/__init__.py
new file mode 100644
index 0000000000..5f5378b42a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/funcsigs/__init__.py
@@ -0,0 +1,829 @@
+# Copyright 2001-2013 Python Software Foundation; All Rights Reserved
+"""Function signature objects for callables
+
+Back port of Python 3.3's function signature tools from the inspect module,
+modified to be compatible with Python 2.6, 2.7 and 3.3+.
+"""
+from __future__ import absolute_import, division, print_function
+import itertools
+import functools
+import re
+import types
+
+try:
+ from collections import OrderedDict
+except ImportError:
+ from ordereddict import OrderedDict
+
+from funcsigs.version import __version__
+
+__all__ = ['BoundArguments', 'Parameter', 'Signature', 'signature']
+
+
+_WrapperDescriptor = type(type.__call__)
+_MethodWrapper = type(all.__call__)
+
+_NonUserDefinedCallables = (_WrapperDescriptor,
+ _MethodWrapper,
+ types.BuiltinFunctionType)
+
+
+def formatannotation(annotation, base_module=None):
+ if isinstance(annotation, type):
+ if annotation.__module__ in ('builtins', '__builtin__', base_module):
+ return annotation.__name__
+ return annotation.__module__+'.'+annotation.__name__
+ return repr(annotation)
+
+
+def _get_user_defined_method(cls, method_name, *nested):
+ try:
+ if cls is type:
+ return
+ meth = getattr(cls, method_name)
+ for name in nested:
+ meth = getattr(meth, name, meth)
+ except AttributeError:
+ return
+ else:
+ if not isinstance(meth, _NonUserDefinedCallables):
+ # Once '__signature__' will be added to 'C'-level
+ # callables, this check won't be necessary
+ return meth
+
+
+def signature(obj):
+ '''Get a signature object for the passed callable.'''
+
+ if not callable(obj):
+ raise TypeError('{0!r} is not a callable object'.format(obj))
+
+ if isinstance(obj, types.MethodType):
+ sig = signature(obj.__func__)
+ if obj.__self__ is None:
+ # Unbound method - preserve as-is.
+ return sig
+ else:
+ # Bound method. Eat self - if we can.
+ params = tuple(sig.parameters.values())
+
+ if not params or params[0].kind in (_VAR_KEYWORD, _KEYWORD_ONLY):
+ raise ValueError('invalid method signature')
+
+ kind = params[0].kind
+ if kind in (_POSITIONAL_OR_KEYWORD, _POSITIONAL_ONLY):
+ # Drop first parameter:
+ # '(p1, p2[, ...])' -> '(p2[, ...])'
+ params = params[1:]
+ else:
+ if kind is not _VAR_POSITIONAL:
+ # Unless we add a new parameter type we never
+ # get here
+ raise ValueError('invalid argument type')
+ # It's a var-positional parameter.
+ # Do nothing. '(*args[, ...])' -> '(*args[, ...])'
+
+ return sig.replace(parameters=params)
+
+ try:
+ sig = obj.__signature__
+ except AttributeError:
+ pass
+ else:
+ if sig is not None:
+ return sig
+
+ try:
+ # Was this function wrapped by a decorator?
+ wrapped = obj.__wrapped__
+ except AttributeError:
+ pass
+ else:
+ return signature(wrapped)
+
+ if isinstance(obj, types.FunctionType):
+ return Signature.from_function(obj)
+
+ if isinstance(obj, functools.partial):
+ sig = signature(obj.func)
+
+ new_params = OrderedDict(sig.parameters.items())
+
+ partial_args = obj.args or ()
+ partial_keywords = obj.keywords or {}
+ try:
+ ba = sig.bind_partial(*partial_args, **partial_keywords)
+ except TypeError as ex:
+ msg = 'partial object {0!r} has incorrect arguments'.format(obj)
+ raise ValueError(msg)
+
+ for arg_name, arg_value in ba.arguments.items():
+ param = new_params[arg_name]
+ if arg_name in partial_keywords:
+ # We set a new default value, because the following code
+ # is correct:
+ #
+ # >>> def foo(a): print(a)
+ # >>> print(partial(partial(foo, a=10), a=20)())
+ # 20
+ # >>> print(partial(partial(foo, a=10), a=20)(a=30))
+ # 30
+ #
+ # So, with 'partial' objects, passing a keyword argument is
+ # like setting a new default value for the corresponding
+ # parameter
+ #
+ # We also mark this parameter with '_partial_kwarg'
+ # flag. Later, in '_bind', the 'default' value of this
+ # parameter will be added to 'kwargs', to simulate
+ # the 'functools.partial' real call.
+ new_params[arg_name] = param.replace(default=arg_value,
+ _partial_kwarg=True)
+
+ elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL) and
+ not param._partial_kwarg):
+ new_params.pop(arg_name)
+
+ return sig.replace(parameters=new_params.values())
+
+ sig = None
+ if isinstance(obj, type):
+ # obj is a class or a metaclass
+
+ # First, let's see if it has an overloaded __call__ defined
+ # in its metaclass
+ call = _get_user_defined_method(type(obj), '__call__')
+ if call is not None:
+ sig = signature(call)
+ else:
+ # Now we check if the 'obj' class has a '__new__' method
+ new = _get_user_defined_method(obj, '__new__')
+ if new is not None:
+ sig = signature(new)
+ else:
+ # Finally, we should have at least __init__ implemented
+ init = _get_user_defined_method(obj, '__init__')
+ if init is not None:
+ sig = signature(init)
+ elif not isinstance(obj, _NonUserDefinedCallables):
+ # An object with __call__
+ # We also check that the 'obj' is not an instance of
+ # _WrapperDescriptor or _MethodWrapper to avoid
+ # infinite recursion (and even potential segfault)
+ call = _get_user_defined_method(type(obj), '__call__', 'im_func')
+ if call is not None:
+ sig = signature(call)
+
+ if sig is not None:
+ # For classes and objects we skip the first parameter of their
+ # __call__, __new__, or __init__ methods
+ return sig.replace(parameters=tuple(sig.parameters.values())[1:])
+
+ if isinstance(obj, types.BuiltinFunctionType):
+ # Raise a nicer error message for builtins
+ msg = 'no signature found for builtin function {0!r}'.format(obj)
+ raise ValueError(msg)
+
+ raise ValueError('callable {0!r} is not supported by signature'.format(obj))
+
+
+class _void(object):
+ '''A private marker - used in Parameter & Signature'''
+
+
+class _empty(object):
+ pass
+
+
+class _ParameterKind(int):
+ def __new__(self, *args, **kwargs):
+ obj = int.__new__(self, *args)
+ obj._name = kwargs['name']
+ return obj
+
+ def __str__(self):
+ return self._name
+
+ def __repr__(self):
+ return '<_ParameterKind: {0!r}>'.format(self._name)
+
+
+_POSITIONAL_ONLY = _ParameterKind(0, name='POSITIONAL_ONLY')
+_POSITIONAL_OR_KEYWORD = _ParameterKind(1, name='POSITIONAL_OR_KEYWORD')
+_VAR_POSITIONAL = _ParameterKind(2, name='VAR_POSITIONAL')
+_KEYWORD_ONLY = _ParameterKind(3, name='KEYWORD_ONLY')
+_VAR_KEYWORD = _ParameterKind(4, name='VAR_KEYWORD')
+
+
+class Parameter(object):
+ '''Represents a parameter in a function signature.
+
+ Has the following public attributes:
+
+ * name : str
+ The name of the parameter as a string.
+ * default : object
+ The default value for the parameter if specified. If the
+ parameter has no default value, this attribute is not set.
+ * annotation
+ The annotation for the parameter if specified. If the
+ parameter has no annotation, this attribute is not set.
+ * kind : str
+ Describes how argument values are bound to the parameter.
+ Possible values: `Parameter.POSITIONAL_ONLY`,
+ `Parameter.POSITIONAL_OR_KEYWORD`, `Parameter.VAR_POSITIONAL`,
+ `Parameter.KEYWORD_ONLY`, `Parameter.VAR_KEYWORD`.
+ '''
+
+ __slots__ = ('_name', '_kind', '_default', '_annotation', '_partial_kwarg')
+
+ POSITIONAL_ONLY = _POSITIONAL_ONLY
+ POSITIONAL_OR_KEYWORD = _POSITIONAL_OR_KEYWORD
+ VAR_POSITIONAL = _VAR_POSITIONAL
+ KEYWORD_ONLY = _KEYWORD_ONLY
+ VAR_KEYWORD = _VAR_KEYWORD
+
+ empty = _empty
+
+ def __init__(self, name, kind, default=_empty, annotation=_empty,
+ _partial_kwarg=False):
+
+ if kind not in (_POSITIONAL_ONLY, _POSITIONAL_OR_KEYWORD,
+ _VAR_POSITIONAL, _KEYWORD_ONLY, _VAR_KEYWORD):
+ raise ValueError("invalid value for 'Parameter.kind' attribute")
+ self._kind = kind
+
+ if default is not _empty:
+ if kind in (_VAR_POSITIONAL, _VAR_KEYWORD):
+ msg = '{0} parameters cannot have default values'.format(kind)
+ raise ValueError(msg)
+ self._default = default
+ self._annotation = annotation
+
+ if name is None:
+ if kind != _POSITIONAL_ONLY:
+ raise ValueError("None is not a valid name for a "
+ "non-positional-only parameter")
+ self._name = name
+ else:
+ name = str(name)
+ if kind != _POSITIONAL_ONLY and not re.match(r'[a-z_]\w*$', name, re.I):
+ msg = '{0!r} is not a valid parameter name'.format(name)
+ raise ValueError(msg)
+ self._name = name
+
+ self._partial_kwarg = _partial_kwarg
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def default(self):
+ return self._default
+
+ @property
+ def annotation(self):
+ return self._annotation
+
+ @property
+ def kind(self):
+ return self._kind
+
+ def replace(self, name=_void, kind=_void, annotation=_void,
+ default=_void, _partial_kwarg=_void):
+ '''Creates a customized copy of the Parameter.'''
+
+ if name is _void:
+ name = self._name
+
+ if kind is _void:
+ kind = self._kind
+
+ if annotation is _void:
+ annotation = self._annotation
+
+ if default is _void:
+ default = self._default
+
+ if _partial_kwarg is _void:
+ _partial_kwarg = self._partial_kwarg
+
+ return type(self)(name, kind, default=default, annotation=annotation,
+ _partial_kwarg=_partial_kwarg)
+
+ def __str__(self):
+ kind = self.kind
+
+ formatted = self._name
+ if kind == _POSITIONAL_ONLY:
+ if formatted is None:
+ formatted = ''
+ formatted = '<{0}>'.format(formatted)
+
+ # Add annotation and default value
+ if self._annotation is not _empty:
+ formatted = '{0}:{1}'.format(formatted,
+ formatannotation(self._annotation))
+
+ if self._default is not _empty:
+ formatted = '{0}={1}'.format(formatted, repr(self._default))
+
+ if kind == _VAR_POSITIONAL:
+ formatted = '*' + formatted
+ elif kind == _VAR_KEYWORD:
+ formatted = '**' + formatted
+
+ return formatted
+
+ def __repr__(self):
+ return '<{0} at {1:#x} {2!r}>'.format(self.__class__.__name__,
+ id(self), self.name)
+
+ def __hash__(self):
+ msg = "unhashable type: '{0}'".format(self.__class__.__name__)
+ raise TypeError(msg)
+
+ def __eq__(self, other):
+ return (issubclass(other.__class__, Parameter) and
+ self._name == other._name and
+ self._kind == other._kind and
+ self._default == other._default and
+ self._annotation == other._annotation)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+class BoundArguments(object):
+ '''Result of `Signature.bind` call. Holds the mapping of arguments
+ to the function's parameters.
+
+ Has the following public attributes:
+
+ * arguments : OrderedDict
+ An ordered mutable mapping of parameters' names to arguments' values.
+ Does not contain arguments' default values.
+ * signature : Signature
+ The Signature object that created this instance.
+ * args : tuple
+ Tuple of positional arguments values.
+ * kwargs : dict
+ Dict of keyword arguments values.
+ '''
+
+ def __init__(self, signature, arguments):
+ self.arguments = arguments
+ self._signature = signature
+
+ @property
+ def signature(self):
+ return self._signature
+
+ @property
+ def args(self):
+ args = []
+ for param_name, param in self._signature.parameters.items():
+ if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or
+ param._partial_kwarg):
+ # Keyword arguments mapped by 'functools.partial'
+ # (Parameter._partial_kwarg is True) are mapped
+ # in 'BoundArguments.kwargs', along with VAR_KEYWORD &
+ # KEYWORD_ONLY
+ break
+
+ try:
+ arg = self.arguments[param_name]
+ except KeyError:
+ # We're done here. Other arguments
+ # will be mapped in 'BoundArguments.kwargs'
+ break
+ else:
+ if param.kind == _VAR_POSITIONAL:
+ # *args
+ args.extend(arg)
+ else:
+ # plain argument
+ args.append(arg)
+
+ return tuple(args)
+
+ @property
+ def kwargs(self):
+ kwargs = {}
+ kwargs_started = False
+ for param_name, param in self._signature.parameters.items():
+ if not kwargs_started:
+ if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or
+ param._partial_kwarg):
+ kwargs_started = True
+ else:
+ if param_name not in self.arguments:
+ kwargs_started = True
+ continue
+
+ if not kwargs_started:
+ continue
+
+ try:
+ arg = self.arguments[param_name]
+ except KeyError:
+ pass
+ else:
+ if param.kind == _VAR_KEYWORD:
+ # **kwargs
+ kwargs.update(arg)
+ else:
+ # plain keyword argument
+ kwargs[param_name] = arg
+
+ return kwargs
+
+ def __hash__(self):
+ msg = "unhashable type: '{0}'".format(self.__class__.__name__)
+ raise TypeError(msg)
+
+ def __eq__(self, other):
+ return (issubclass(other.__class__, BoundArguments) and
+ self.signature == other.signature and
+ self.arguments == other.arguments)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+class Signature(object):
+ '''A Signature object represents the overall signature of a function.
+ It stores a Parameter object for each parameter accepted by the
+ function, as well as information specific to the function itself.
+
+ A Signature object has the following public attributes and methods:
+
+ * parameters : OrderedDict
+ An ordered mapping of parameters' names to the corresponding
+ Parameter objects (keyword-only arguments are in the same order
+ as listed in `code.co_varnames`).
+ * return_annotation : object
+ The annotation for the return type of the function if specified.
+ If the function has no annotation for its return type, this
+ attribute is not set.
+ * bind(*args, **kwargs) -> BoundArguments
+ Creates a mapping from positional and keyword arguments to
+ parameters.
+ * bind_partial(*args, **kwargs) -> BoundArguments
+ Creates a partial mapping from positional and keyword arguments
+ to parameters (simulating 'functools.partial' behavior.)
+ '''
+
+ __slots__ = ('_return_annotation', '_parameters')
+
+ _parameter_cls = Parameter
+ _bound_arguments_cls = BoundArguments
+
+ empty = _empty
+
+ def __init__(self, parameters=None, return_annotation=_empty,
+ __validate_parameters__=True):
+ '''Constructs Signature from the given list of Parameter
+ objects and 'return_annotation'. All arguments are optional.
+ '''
+
+ if parameters is None:
+ params = OrderedDict()
+ else:
+ if __validate_parameters__:
+ params = OrderedDict()
+ top_kind = _POSITIONAL_ONLY
+
+ for idx, param in enumerate(parameters):
+ kind = param.kind
+ if kind < top_kind:
+ msg = 'wrong parameter order: {0} before {1}'
+ msg = msg.format(top_kind, param.kind)
+ raise ValueError(msg)
+ else:
+ top_kind = kind
+
+ name = param.name
+ if name is None:
+ name = str(idx)
+ param = param.replace(name=name)
+
+ if name in params:
+ msg = 'duplicate parameter name: {0!r}'.format(name)
+ raise ValueError(msg)
+ params[name] = param
+ else:
+ params = OrderedDict(((param.name, param)
+ for param in parameters))
+
+ self._parameters = params
+ self._return_annotation = return_annotation
+
+ @classmethod
+ def from_function(cls, func):
+ '''Constructs Signature for the given python function'''
+
+ if not isinstance(func, types.FunctionType):
+ raise TypeError('{0!r} is not a Python function'.format(func))
+
+ Parameter = cls._parameter_cls
+
+ # Parameter information.
+ func_code = func.__code__
+ pos_count = func_code.co_argcount
+ arg_names = func_code.co_varnames
+ positional = tuple(arg_names[:pos_count])
+ keyword_only_count = getattr(func_code, 'co_kwonlyargcount', 0)
+ keyword_only = arg_names[pos_count:(pos_count + keyword_only_count)]
+ annotations = getattr(func, '__annotations__', {})
+ defaults = func.__defaults__
+ kwdefaults = getattr(func, '__kwdefaults__', None)
+
+ if defaults:
+ pos_default_count = len(defaults)
+ else:
+ pos_default_count = 0
+
+ parameters = []
+
+ # Non-keyword-only parameters w/o defaults.
+ non_default_count = pos_count - pos_default_count
+ for name in positional[:non_default_count]:
+ annotation = annotations.get(name, _empty)
+ parameters.append(Parameter(name, annotation=annotation,
+ kind=_POSITIONAL_OR_KEYWORD))
+
+ # ... w/ defaults.
+ for offset, name in enumerate(positional[non_default_count:]):
+ annotation = annotations.get(name, _empty)
+ parameters.append(Parameter(name, annotation=annotation,
+ kind=_POSITIONAL_OR_KEYWORD,
+ default=defaults[offset]))
+
+ # *args
+ if func_code.co_flags & 0x04:
+ name = arg_names[pos_count + keyword_only_count]
+ annotation = annotations.get(name, _empty)
+ parameters.append(Parameter(name, annotation=annotation,
+ kind=_VAR_POSITIONAL))
+
+ # Keyword-only parameters.
+ for name in keyword_only:
+ default = _empty
+ if kwdefaults is not None:
+ default = kwdefaults.get(name, _empty)
+
+ annotation = annotations.get(name, _empty)
+ parameters.append(Parameter(name, annotation=annotation,
+ kind=_KEYWORD_ONLY,
+ default=default))
+ # **kwargs
+ if func_code.co_flags & 0x08:
+ index = pos_count + keyword_only_count
+ if func_code.co_flags & 0x04:
+ index += 1
+
+ name = arg_names[index]
+ annotation = annotations.get(name, _empty)
+ parameters.append(Parameter(name, annotation=annotation,
+ kind=_VAR_KEYWORD))
+
+ return cls(parameters,
+ return_annotation=annotations.get('return', _empty),
+ __validate_parameters__=False)
+
+ @property
+ def parameters(self):
+ try:
+ return types.MappingProxyType(self._parameters)
+ except AttributeError:
+ return OrderedDict(self._parameters.items())
+
+ @property
+ def return_annotation(self):
+ return self._return_annotation
+
+ def replace(self, parameters=_void, return_annotation=_void):
+ '''Creates a customized copy of the Signature.
+ Pass 'parameters' and/or 'return_annotation' arguments
+ to override them in the new copy.
+ '''
+
+ if parameters is _void:
+ parameters = self.parameters.values()
+
+ if return_annotation is _void:
+ return_annotation = self._return_annotation
+
+ return type(self)(parameters,
+ return_annotation=return_annotation)
+
+ def __hash__(self):
+ msg = "unhashable type: '{0}'".format(self.__class__.__name__)
+ raise TypeError(msg)
+
+ def __eq__(self, other):
+ if (not issubclass(type(other), Signature) or
+ self.return_annotation != other.return_annotation or
+ len(self.parameters) != len(other.parameters)):
+ return False
+
+ other_positions = dict((param, idx)
+ for idx, param in enumerate(other.parameters.keys()))
+
+ for idx, (param_name, param) in enumerate(self.parameters.items()):
+ if param.kind == _KEYWORD_ONLY:
+ try:
+ other_param = other.parameters[param_name]
+ except KeyError:
+ return False
+ else:
+ if param != other_param:
+ return False
+ else:
+ try:
+ other_idx = other_positions[param_name]
+ except KeyError:
+ return False
+ else:
+ if (idx != other_idx or
+ param != other.parameters[param_name]):
+ return False
+
+ return True
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def _bind(self, args, kwargs, partial=False):
+ '''Private method. Don't use directly.'''
+
+ arguments = OrderedDict()
+
+ parameters = iter(self.parameters.values())
+ parameters_ex = ()
+ arg_vals = iter(args)
+
+ if partial:
+ # Support for binding arguments to 'functools.partial' objects.
+ # See 'functools.partial' case in 'signature()' implementation
+ # for details.
+ for param_name, param in self.parameters.items():
+ if (param._partial_kwarg and param_name not in kwargs):
+ # Simulating 'functools.partial' behavior
+ kwargs[param_name] = param.default
+
+ while True:
+ # Let's iterate through the positional arguments and corresponding
+ # parameters
+ try:
+ arg_val = next(arg_vals)
+ except StopIteration:
+ # No more positional arguments
+ try:
+ param = next(parameters)
+ except StopIteration:
+ # No more parameters. That's it. Just need to check that
+ # we have no `kwargs` after this while loop
+ break
+ else:
+ if param.kind == _VAR_POSITIONAL:
+ # That's OK, just empty *args. Let's start parsing
+ # kwargs
+ break
+ elif param.name in kwargs:
+ if param.kind == _POSITIONAL_ONLY:
+ msg = '{arg!r} parameter is positional only, ' \
+ 'but was passed as a keyword'
+ msg = msg.format(arg=param.name)
+ raise TypeError(msg)
+ parameters_ex = (param,)
+ break
+ elif (param.kind == _VAR_KEYWORD or
+ param.default is not _empty):
+ # That's fine too - we have a default value for this
+ # parameter. So, lets start parsing `kwargs`, starting
+ # with the current parameter
+ parameters_ex = (param,)
+ break
+ else:
+ if partial:
+ parameters_ex = (param,)
+ break
+ else:
+ msg = '{arg!r} parameter lacking default value'
+ msg = msg.format(arg=param.name)
+ raise TypeError(msg)
+ else:
+ # We have a positional argument to process
+ try:
+ param = next(parameters)
+ except StopIteration:
+ raise TypeError('too many positional arguments')
+ else:
+ if param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY):
+ # Looks like we have no parameter for this positional
+ # argument
+ raise TypeError('too many positional arguments')
+
+ if param.kind == _VAR_POSITIONAL:
+ # We have an '*args'-like argument, let's fill it with
+ # all positional arguments we have left and move on to
+ # the next phase
+ values = [arg_val]
+ values.extend(arg_vals)
+ arguments[param.name] = tuple(values)
+ break
+
+ if param.name in kwargs:
+ raise TypeError('multiple values for argument '
+ '{arg!r}'.format(arg=param.name))
+
+ arguments[param.name] = arg_val
+
+ # Now, we iterate through the remaining parameters to process
+ # keyword arguments
+ kwargs_param = None
+ for param in itertools.chain(parameters_ex, parameters):
+ if param.kind == _POSITIONAL_ONLY:
+ # This should never happen in case of a properly built
+ # Signature object (but let's have this check here
+ # to ensure correct behaviour just in case)
+ raise TypeError('{arg!r} parameter is positional only, '
+ 'but was passed as a keyword'. \
+ format(arg=param.name))
+
+ if param.kind == _VAR_KEYWORD:
+ # Memorize that we have a '**kwargs'-like parameter
+ kwargs_param = param
+ continue
+
+ param_name = param.name
+ try:
+ arg_val = kwargs.pop(param_name)
+ except KeyError:
+ # We have no value for this parameter. It's fine though,
+ # if it has a default value, or it is an '*args'-like
+ # parameter, left alone by the processing of positional
+ # arguments.
+ if (not partial and param.kind != _VAR_POSITIONAL and
+ param.default is _empty):
+ raise TypeError('{arg!r} parameter lacking default value'. \
+ format(arg=param_name))
+
+ else:
+ arguments[param_name] = arg_val
+
+ if kwargs:
+ if kwargs_param is not None:
+ # Process our '**kwargs'-like parameter
+ arguments[kwargs_param.name] = kwargs
+ else:
+ raise TypeError('too many keyword arguments %r' % kwargs)
+
+ return self._bound_arguments_cls(self, arguments)
+
+ def bind(*args, **kwargs):
+ '''Get a BoundArguments object, that maps the passed `args`
+ and `kwargs` to the function's signature. Raises `TypeError`
+ if the passed arguments can not be bound.
+ '''
+ return args[0]._bind(args[1:], kwargs)
+
+ def bind_partial(self, *args, **kwargs):
+ '''Get a BoundArguments object, that partially maps the
+ passed `args` and `kwargs` to the function's signature.
+ Raises `TypeError` if the passed arguments can not be bound.
+ '''
+ return self._bind(args, kwargs, partial=True)
+
+ def __str__(self):
+ result = []
+ render_kw_only_separator = True
+ for idx, param in enumerate(self.parameters.values()):
+ formatted = str(param)
+
+ kind = param.kind
+ if kind == _VAR_POSITIONAL:
+ # OK, we have an '*args'-like parameter, so we won't need
+ # a '*' to separate keyword-only arguments
+ render_kw_only_separator = False
+ elif kind == _KEYWORD_ONLY and render_kw_only_separator:
+ # We have a keyword-only parameter to render and we haven't
+ # rendered an '*args'-like parameter before, so add a '*'
+ # separator to the parameters list ("foo(arg1, *, arg2)" case)
+ result.append('*')
+ # This condition should be only triggered once, so
+ # reset the flag
+ render_kw_only_separator = False
+
+ result.append(formatted)
+
+ rendered = '({0})'.format(', '.join(result))
+
+ if self.return_annotation is not _empty:
+ anno = formatannotation(self.return_annotation)
+ rendered += ' -> {0}'.format(anno)
+
+ return rendered
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/funcsigs/version.py b/testing/web-platform/tests/tools/third_party/funcsigs/funcsigs/version.py
new file mode 100644
index 0000000000..7863915fa5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/funcsigs/version.py
@@ -0,0 +1 @@
+__version__ = "1.0.2"
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/requirements/development.txt b/testing/web-platform/tests/tools/third_party/funcsigs/requirements/development.txt
new file mode 100644
index 0000000000..40dedd92bf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/requirements/development.txt
@@ -0,0 +1,5 @@
+coverage
+coveralls
+flake8
+sphinx
+unittest2
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/setup.cfg b/testing/web-platform/tests/tools/third_party/funcsigs/setup.cfg
new file mode 100644
index 0000000000..5e4090017a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/setup.cfg
@@ -0,0 +1,2 @@
+[wheel]
+universal = 1
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/setup.py b/testing/web-platform/tests/tools/third_party/funcsigs/setup.py
new file mode 100644
index 0000000000..f3696888f9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/setup.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+from setuptools import setup
+import re
+import sys
+
+def load_version(filename='funcsigs/version.py'):
+ "Parse a __version__ number from a source file"
+ with open(filename) as source:
+ text = source.read()
+ match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", text)
+ if not match:
+ msg = "Unable to find version number in {}".format(filename)
+ raise RuntimeError(msg)
+ version = match.group(1)
+ return version
+
+
+setup(
+ name="funcsigs",
+ version=load_version(),
+ packages=['funcsigs'],
+ zip_safe=False,
+ author="Testing Cabal",
+ author_email="testing-in-python@lists.idyll.org",
+ url="http://funcsigs.readthedocs.org",
+ description="Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+",
+ long_description=open('README.rst').read(),
+ license="ASL",
+ extras_require = {
+ ':python_version<"2.7"': ['ordereddict'],
+ },
+ setup_requires = ["setuptools>=17.1"],
+ classifiers = [
+ 'Development Status :: 4 - Beta',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: Apache Software License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ 'Programming Language :: Python :: Implementation :: PyPy',
+ 'Topic :: Software Development :: Libraries :: Python Modules'
+ ],
+ tests_require = ['unittest2'],
+ test_suite = 'unittest2.collector',
+)
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/tests/__init__.py b/testing/web-platform/tests/tools/third_party/funcsigs/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/tests/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/tests/test_formatannotation.py b/testing/web-platform/tests/tools/third_party/funcsigs/tests/test_formatannotation.py
new file mode 100644
index 0000000000..4b98e6037d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/tests/test_formatannotation.py
@@ -0,0 +1,17 @@
+import funcsigs
+
+import unittest2 as unittest
+
+class TestFormatAnnotation(unittest.TestCase):
+ def test_string (self):
+ self.assertEqual(funcsigs.formatannotation("annotation"),
+ "'annotation'")
+
+ def test_builtin_type (self):
+ self.assertEqual(funcsigs.formatannotation(int),
+ "int")
+
+ def test_user_type (self):
+ class dummy (object): pass
+ self.assertEqual(funcsigs.formatannotation(dummy),
+ "tests.test_formatannotation.dummy")
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/tests/test_funcsigs.py b/testing/web-platform/tests/tools/third_party/funcsigs/tests/test_funcsigs.py
new file mode 100644
index 0000000000..a7b9cca767
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/tests/test_funcsigs.py
@@ -0,0 +1,91 @@
+import unittest2 as unittest
+
+import doctest
+import sys
+
+import funcsigs as inspect
+
+
+class TestFunctionSignatures(unittest.TestCase):
+
+ @staticmethod
+ def signature(func):
+ sig = inspect.signature(func)
+ return (tuple((param.name,
+ (Ellipsis if param.default is param.empty else param.default),
+ (Ellipsis if param.annotation is param.empty
+ else param.annotation),
+ str(param.kind).lower())
+ for param in sig.parameters.values()),
+ (Ellipsis if sig.return_annotation is sig.empty
+ else sig.return_annotation))
+
+ def test_zero_arguments(self):
+ def test():
+ pass
+ self.assertEqual(self.signature(test),
+ ((), Ellipsis))
+
+ def test_single_positional_argument(self):
+ def test(a):
+ pass
+ self.assertEqual(self.signature(test),
+ (((('a', Ellipsis, Ellipsis, "positional_or_keyword")),), Ellipsis))
+
+ def test_single_keyword_argument(self):
+ def test(a=None):
+ pass
+ self.assertEqual(self.signature(test),
+ (((('a', None, Ellipsis, "positional_or_keyword")),), Ellipsis))
+
+ def test_var_args(self):
+ def test(*args):
+ pass
+ self.assertEqual(self.signature(test),
+ (((('args', Ellipsis, Ellipsis, "var_positional")),), Ellipsis))
+
+ def test_keywords_args(self):
+ def test(**kwargs):
+ pass
+ self.assertEqual(self.signature(test),
+ (((('kwargs', Ellipsis, Ellipsis, "var_keyword")),), Ellipsis))
+
+ def test_multiple_arguments(self):
+ def test(a, b=None, *args, **kwargs):
+ pass
+ self.assertEqual(self.signature(test), ((
+ ('a', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('b', None, Ellipsis, "positional_or_keyword"),
+ ('args', Ellipsis, Ellipsis, "var_positional"),
+ ('kwargs', Ellipsis, Ellipsis, "var_keyword"),
+ ), Ellipsis))
+
+ def test_has_version(self):
+ self.assertTrue(inspect.__version__)
+
+ def test_readme(self):
+ # XXX: This fails but doesn't fail the build.
+ # (and the syntax isn't valid on all pythons so that seems a little
+ # hard to get right.
+ doctest.testfile('../README.rst')
+
+ def test_unbound_method(self):
+ self_kind = "positional_or_keyword"
+ class Test(object):
+ def method(self):
+ pass
+ def method_with_args(self, a):
+ pass
+ def method_with_varargs(*args):
+ pass
+ self.assertEqual(
+ self.signature(Test.method),
+ (((('self', Ellipsis, Ellipsis, self_kind)),), Ellipsis))
+ self.assertEqual(
+ self.signature(Test.method_with_args),
+ ((('self', Ellipsis, Ellipsis, self_kind),
+ ('a', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ), Ellipsis))
+ self.assertEqual(
+ self.signature(Test.method_with_varargs),
+ ((('args', Ellipsis, Ellipsis, "var_positional"),), Ellipsis))
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/tests/test_inspect.py b/testing/web-platform/tests/tools/third_party/funcsigs/tests/test_inspect.py
new file mode 100644
index 0000000000..98d6592fcc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/tests/test_inspect.py
@@ -0,0 +1,1002 @@
+# Copyright 2001-2013 Python Software Foundation; All Rights Reserved
+from __future__ import absolute_import, division, print_function
+import collections
+import functools
+import sys
+
+import unittest2 as unittest
+
+import funcsigs as inspect
+
+
+class TestSignatureObject(unittest.TestCase):
+ @staticmethod
+ def signature(func):
+ sig = inspect.signature(func)
+ return (tuple((param.name,
+ (Ellipsis if param.default is param.empty else param.default),
+ (Ellipsis if param.annotation is param.empty
+ else param.annotation),
+ str(param.kind).lower())
+ for param in sig.parameters.values()),
+ (Ellipsis if sig.return_annotation is sig.empty
+ else sig.return_annotation))
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_object(self):
+ S = inspect.Signature
+ P = inspect.Parameter
+
+ self.assertEqual(str(S()), '()')
+
+ def test(po, pk, *args, ko, **kwargs):
+ pass
+ sig = inspect.signature(test)
+ po = sig.parameters['po'].replace(kind=P.POSITIONAL_ONLY)
+ pk = sig.parameters['pk']
+ args = sig.parameters['args']
+ ko = sig.parameters['ko']
+ kwargs = sig.parameters['kwargs']
+
+ S((po, pk, args, ko, kwargs))
+
+ with self.assertRaisesRegex(ValueError, 'wrong parameter order'):
+ S((pk, po, args, ko, kwargs))
+
+ with self.assertRaisesRegex(ValueError, 'wrong parameter order'):
+ S((po, args, pk, ko, kwargs))
+
+ with self.assertRaisesRegex(ValueError, 'wrong parameter order'):
+ S((args, po, pk, ko, kwargs))
+
+ with self.assertRaisesRegex(ValueError, 'wrong parameter order'):
+ S((po, pk, args, kwargs, ko))
+
+ kwargs2 = kwargs.replace(name='args')
+ with self.assertRaisesRegex(ValueError, 'duplicate parameter name'):
+ S((po, pk, args, kwargs2, ko))
+""")
+
+ def test_signature_immutability(self):
+ def test(a):
+ pass
+ sig = inspect.signature(test)
+
+ with self.assertRaises(AttributeError):
+ sig.foo = 'bar'
+
+ # Python2 does not have MappingProxyType class
+ if sys.version_info[:2] < (3, 3):
+ return
+
+ with self.assertRaises(TypeError):
+ sig.parameters['a'] = None
+
+ def test_signature_on_noarg(self):
+ def test():
+ pass
+ self.assertEqual(self.signature(test), ((), Ellipsis))
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_on_wargs(self):
+ def test(a, b:'foo') -> 123:
+ pass
+ self.assertEqual(self.signature(test),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('b', Ellipsis, 'foo', "positional_or_keyword")),
+ 123))
+""")
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_on_wkwonly(self):
+ def test(*, a:float, b:str) -> int:
+ pass
+ self.assertEqual(self.signature(test),
+ ((('a', Ellipsis, float, "keyword_only"),
+ ('b', Ellipsis, str, "keyword_only")),
+ int))
+""")
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_on_complex_args(self):
+ def test(a, b:'foo'=10, *args:'bar', spam:'baz', ham=123, **kwargs:int):
+ pass
+ self.assertEqual(self.signature(test),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('b', 10, 'foo', "positional_or_keyword"),
+ ('args', Ellipsis, 'bar', "var_positional"),
+ ('spam', Ellipsis, 'baz', "keyword_only"),
+ ('ham', 123, Ellipsis, "keyword_only"),
+ ('kwargs', Ellipsis, int, "var_keyword")),
+ Ellipsis))
+""")
+
+ def test_signature_on_builtin_function(self):
+ with self.assertRaisesRegex(ValueError, 'not supported by signature'):
+ inspect.signature(type)
+ with self.assertRaisesRegex(ValueError, 'not supported by signature'):
+ # support for 'wrapper_descriptor'
+ inspect.signature(type.__call__)
+ if hasattr(sys, 'pypy_version_info'):
+ raise ValueError('not supported by signature')
+ with self.assertRaisesRegex(ValueError, 'not supported by signature'):
+ # support for 'method-wrapper'
+ inspect.signature(min.__call__)
+ if hasattr(sys, 'pypy_version_info'):
+ raise ValueError('not supported by signature')
+ with self.assertRaisesRegex(ValueError,
+ 'no signature found for builtin function'):
+ # support for 'method-wrapper'
+ inspect.signature(min)
+
+ def test_signature_on_non_function(self):
+ with self.assertRaisesRegex(TypeError, 'is not a callable object'):
+ inspect.signature(42)
+
+ with self.assertRaisesRegex(TypeError, 'is not a Python function'):
+ inspect.Signature.from_function(42)
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_on_method(self):
+ class Test:
+ def foo(self, arg1, arg2=1) -> int:
+ pass
+
+ meth = Test().foo
+
+ self.assertEqual(self.signature(meth),
+ ((('arg1', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('arg2', 1, Ellipsis, "positional_or_keyword")),
+ int))
+""")
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_on_classmethod(self):
+ class Test:
+ @classmethod
+ def foo(cls, arg1, *, arg2=1):
+ pass
+
+ meth = Test().foo
+ self.assertEqual(self.signature(meth),
+ ((('arg1', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('arg2', 1, Ellipsis, "keyword_only")),
+ Ellipsis))
+
+ meth = Test.foo
+ self.assertEqual(self.signature(meth),
+ ((('arg1', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('arg2', 1, Ellipsis, "keyword_only")),
+ Ellipsis))
+""")
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_on_staticmethod(self):
+ class Test:
+ @staticmethod
+ def foo(cls, *, arg):
+ pass
+
+ meth = Test().foo
+ self.assertEqual(self.signature(meth),
+ ((('cls', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('arg', Ellipsis, Ellipsis, "keyword_only")),
+ Ellipsis))
+
+ meth = Test.foo
+ self.assertEqual(self.signature(meth),
+ ((('cls', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('arg', Ellipsis, Ellipsis, "keyword_only")),
+ Ellipsis))
+""")
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_on_partial(self):
+ from functools import partial
+
+ def test():
+ pass
+
+ self.assertEqual(self.signature(partial(test)), ((), Ellipsis))
+
+ with self.assertRaisesRegex(ValueError, "has incorrect arguments"):
+ inspect.signature(partial(test, 1))
+
+ with self.assertRaisesRegex(ValueError, "has incorrect arguments"):
+ inspect.signature(partial(test, a=1))
+
+ def test(a, b, *, c, d):
+ pass
+
+ self.assertEqual(self.signature(partial(test)),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('b', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('c', Ellipsis, Ellipsis, "keyword_only"),
+ ('d', Ellipsis, Ellipsis, "keyword_only")),
+ Ellipsis))
+
+ self.assertEqual(self.signature(partial(test, 1)),
+ ((('b', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('c', Ellipsis, Ellipsis, "keyword_only"),
+ ('d', Ellipsis, Ellipsis, "keyword_only")),
+ Ellipsis))
+
+ self.assertEqual(self.signature(partial(test, 1, c=2)),
+ ((('b', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('c', 2, Ellipsis, "keyword_only"),
+ ('d', Ellipsis, Ellipsis, "keyword_only")),
+ Ellipsis))
+
+ self.assertEqual(self.signature(partial(test, b=1, c=2)),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('b', 1, Ellipsis, "positional_or_keyword"),
+ ('c', 2, Ellipsis, "keyword_only"),
+ ('d', Ellipsis, Ellipsis, "keyword_only")),
+ Ellipsis))
+
+ self.assertEqual(self.signature(partial(test, 0, b=1, c=2)),
+ ((('b', 1, Ellipsis, "positional_or_keyword"),
+ ('c', 2, Ellipsis, "keyword_only"),
+ ('d', Ellipsis, Ellipsis, "keyword_only"),),
+ Ellipsis))
+
+ def test(a, *args, b, **kwargs):
+ pass
+
+ self.assertEqual(self.signature(partial(test, 1)),
+ ((('args', Ellipsis, Ellipsis, "var_positional"),
+ ('b', Ellipsis, Ellipsis, "keyword_only"),
+ ('kwargs', Ellipsis, Ellipsis, "var_keyword")),
+ Ellipsis))
+
+ self.assertEqual(self.signature(partial(test, 1, 2, 3)),
+ ((('args', Ellipsis, Ellipsis, "var_positional"),
+ ('b', Ellipsis, Ellipsis, "keyword_only"),
+ ('kwargs', Ellipsis, Ellipsis, "var_keyword")),
+ Ellipsis))
+
+
+ self.assertEqual(self.signature(partial(test, 1, 2, 3, test=True)),
+ ((('args', Ellipsis, Ellipsis, "var_positional"),
+ ('b', Ellipsis, Ellipsis, "keyword_only"),
+ ('kwargs', Ellipsis, Ellipsis, "var_keyword")),
+ Ellipsis))
+
+ self.assertEqual(self.signature(partial(test, 1, 2, 3, test=1, b=0)),
+ ((('args', Ellipsis, Ellipsis, "var_positional"),
+ ('b', 0, Ellipsis, "keyword_only"),
+ ('kwargs', Ellipsis, Ellipsis, "var_keyword")),
+ Ellipsis))
+
+ self.assertEqual(self.signature(partial(test, b=0)),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('args', Ellipsis, Ellipsis, "var_positional"),
+ ('b', 0, Ellipsis, "keyword_only"),
+ ('kwargs', Ellipsis, Ellipsis, "var_keyword")),
+ Ellipsis))
+
+ self.assertEqual(self.signature(partial(test, b=0, test=1)),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('args', Ellipsis, Ellipsis, "var_positional"),
+ ('b', 0, Ellipsis, "keyword_only"),
+ ('kwargs', Ellipsis, Ellipsis, "var_keyword")),
+ Ellipsis))
+
+ def test(a, b, c:int) -> 42:
+ pass
+
+ sig = test.__signature__ = inspect.signature(test)
+
+ self.assertEqual(self.signature(partial(partial(test, 1))),
+ ((('b', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('c', Ellipsis, int, "positional_or_keyword")),
+ 42))
+
+ self.assertEqual(self.signature(partial(partial(test, 1), 2)),
+ ((('c', Ellipsis, int, "positional_or_keyword"),),
+ 42))
+
+ psig = inspect.signature(partial(partial(test, 1), 2))
+
+ def foo(a):
+ return a
+ _foo = partial(partial(foo, a=10), a=20)
+ self.assertEqual(self.signature(_foo),
+ ((('a', 20, Ellipsis, "positional_or_keyword"),),
+ Ellipsis))
+ # check that we don't have any side-effects in signature(),
+ # and the partial object is still functioning
+ self.assertEqual(_foo(), 20)
+
+ def foo(a, b, c):
+ return a, b, c
+ _foo = partial(partial(foo, 1, b=20), b=30)
+ self.assertEqual(self.signature(_foo),
+ ((('b', 30, Ellipsis, "positional_or_keyword"),
+ ('c', Ellipsis, Ellipsis, "positional_or_keyword")),
+ Ellipsis))
+ self.assertEqual(_foo(c=10), (1, 30, 10))
+ _foo = partial(_foo, 2) # now 'b' has two values -
+ # positional and keyword
+ with self.assertRaisesRegex(ValueError, "has incorrect arguments"):
+ inspect.signature(_foo)
+
+ def foo(a, b, c, *, d):
+ return a, b, c, d
+ _foo = partial(partial(foo, d=20, c=20), b=10, d=30)
+ self.assertEqual(self.signature(_foo),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('b', 10, Ellipsis, "positional_or_keyword"),
+ ('c', 20, Ellipsis, "positional_or_keyword"),
+ ('d', 30, Ellipsis, "keyword_only")),
+ Ellipsis))
+ ba = inspect.signature(_foo).bind(a=200, b=11)
+ self.assertEqual(_foo(*ba.args, **ba.kwargs), (200, 11, 20, 30))
+
+ def foo(a=1, b=2, c=3):
+ return a, b, c
+ _foo = partial(foo, a=10, c=13)
+ ba = inspect.signature(_foo).bind(11)
+ self.assertEqual(_foo(*ba.args, **ba.kwargs), (11, 2, 13))
+ ba = inspect.signature(_foo).bind(11, 12)
+ self.assertEqual(_foo(*ba.args, **ba.kwargs), (11, 12, 13))
+ ba = inspect.signature(_foo).bind(11, b=12)
+ self.assertEqual(_foo(*ba.args, **ba.kwargs), (11, 12, 13))
+ ba = inspect.signature(_foo).bind(b=12)
+ self.assertEqual(_foo(*ba.args, **ba.kwargs), (10, 12, 13))
+ _foo = partial(_foo, b=10)
+ ba = inspect.signature(_foo).bind(12, 14)
+ self.assertEqual(_foo(*ba.args, **ba.kwargs), (12, 14, 13))
+""")
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_on_decorated(self):
+ import functools
+
+ def decorator(func):
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs) -> int:
+ return func(*args, **kwargs)
+ return wrapper
+
+ class Foo:
+ @decorator
+ def bar(self, a, b):
+ pass
+
+ self.assertEqual(self.signature(Foo.bar),
+ ((('self', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('a', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('b', Ellipsis, Ellipsis, "positional_or_keyword")),
+ Ellipsis))
+
+ self.assertEqual(self.signature(Foo().bar),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('b', Ellipsis, Ellipsis, "positional_or_keyword")),
+ Ellipsis))
+
+ # Test that we handle method wrappers correctly
+ def decorator(func):
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs) -> int:
+ return func(42, *args, **kwargs)
+ sig = inspect.signature(func)
+ new_params = tuple(sig.parameters.values())[1:]
+ wrapper.__signature__ = sig.replace(parameters=new_params)
+ return wrapper
+
+ class Foo:
+ @decorator
+ def __call__(self, a, b):
+ pass
+
+ self.assertEqual(self.signature(Foo.__call__),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('b', Ellipsis, Ellipsis, "positional_or_keyword")),
+ Ellipsis))
+
+ self.assertEqual(self.signature(Foo().__call__),
+ ((('b', Ellipsis, Ellipsis, "positional_or_keyword"),),
+ Ellipsis))
+""")
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_on_class(self):
+ class C:
+ def __init__(self, a):
+ pass
+
+ self.assertEqual(self.signature(C),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),),
+ Ellipsis))
+
+ class CM(type):
+ def __call__(cls, a):
+ pass
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(self.signature(C),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),),
+ Ellipsis))
+
+ class CM(type):
+ def __new__(mcls, name, bases, dct, *, foo=1):
+ return super().__new__(mcls, name, bases, dct)
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(self.signature(C),
+ ((('b', Ellipsis, Ellipsis, "positional_or_keyword"),),
+ Ellipsis))
+
+ self.assertEqual(self.signature(CM),
+ ((('name', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('bases', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('dct', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('foo', 1, Ellipsis, "keyword_only")),
+ Ellipsis))
+
+ class CMM(type):
+ def __new__(mcls, name, bases, dct, *, foo=1):
+ return super().__new__(mcls, name, bases, dct)
+ def __call__(cls, nm, bs, dt):
+ return type(nm, bs, dt)
+ class CM(type, metaclass=CMM):
+ def __new__(mcls, name, bases, dct, *, bar=2):
+ return super().__new__(mcls, name, bases, dct)
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(self.signature(CMM),
+ ((('name', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('bases', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('dct', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('foo', 1, Ellipsis, "keyword_only")),
+ Ellipsis))
+
+ self.assertEqual(self.signature(CM),
+ ((('nm', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('bs', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('dt', Ellipsis, Ellipsis, "positional_or_keyword")),
+ Ellipsis))
+
+ self.assertEqual(self.signature(C),
+ ((('b', Ellipsis, Ellipsis, "positional_or_keyword"),),
+ Ellipsis))
+
+ class CM(type):
+ def __init__(cls, name, bases, dct, *, bar=2):
+ return super().__init__(name, bases, dct)
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(self.signature(CM),
+ ((('name', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('bases', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('dct', Ellipsis, Ellipsis, "positional_or_keyword"),
+ ('bar', 2, Ellipsis, "keyword_only")),
+ Ellipsis))
+""")
+
+ def test_signature_on_callable_objects(self):
+ class Foo(object):
+ def __call__(self, a):
+ pass
+
+ self.assertEqual(self.signature(Foo()),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),),
+ Ellipsis))
+
+ class Spam(object):
+ pass
+ with self.assertRaisesRegex(TypeError, "is not a callable object"):
+ inspect.signature(Spam())
+
+ class Bar(Spam, Foo):
+ pass
+
+ self.assertEqual(self.signature(Bar()),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),),
+ Ellipsis))
+
+ class ToFail(object):
+ __call__ = type
+ with self.assertRaisesRegex(ValueError, "not supported by signature"):
+ inspect.signature(ToFail())
+
+ if sys.version_info[0] < 3:
+ return
+
+ class Wrapped(object):
+ pass
+ Wrapped.__wrapped__ = lambda a: None
+ self.assertEqual(self.signature(Wrapped),
+ ((('a', Ellipsis, Ellipsis, "positional_or_keyword"),),
+ Ellipsis))
+
+ def test_signature_on_lambdas(self):
+ self.assertEqual(self.signature((lambda a=10: a)),
+ ((('a', 10, Ellipsis, "positional_or_keyword"),),
+ Ellipsis))
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_equality(self):
+ def foo(a, *, b:int) -> float: pass
+ self.assertNotEqual(inspect.signature(foo), 42)
+
+ def bar(a, *, b:int) -> float: pass
+ self.assertEqual(inspect.signature(foo), inspect.signature(bar))
+
+ def bar(a, *, b:int) -> int: pass
+ self.assertNotEqual(inspect.signature(foo), inspect.signature(bar))
+
+ def bar(a, *, b:int): pass
+ self.assertNotEqual(inspect.signature(foo), inspect.signature(bar))
+
+ def bar(a, *, b:int=42) -> float: pass
+ self.assertNotEqual(inspect.signature(foo), inspect.signature(bar))
+
+ def bar(a, *, c) -> float: pass
+ self.assertNotEqual(inspect.signature(foo), inspect.signature(bar))
+
+ def bar(a, b:int) -> float: pass
+ self.assertNotEqual(inspect.signature(foo), inspect.signature(bar))
+ def spam(b:int, a) -> float: pass
+ self.assertNotEqual(inspect.signature(spam), inspect.signature(bar))
+
+ def foo(*, a, b, c): pass
+ def bar(*, c, b, a): pass
+ self.assertEqual(inspect.signature(foo), inspect.signature(bar))
+
+ def foo(*, a=1, b, c): pass
+ def bar(*, c, b, a=1): pass
+ self.assertEqual(inspect.signature(foo), inspect.signature(bar))
+
+ def foo(pos, *, a=1, b, c): pass
+ def bar(pos, *, c, b, a=1): pass
+ self.assertEqual(inspect.signature(foo), inspect.signature(bar))
+
+ def foo(pos, *, a, b, c): pass
+ def bar(pos, *, c, b, a=1): pass
+ self.assertNotEqual(inspect.signature(foo), inspect.signature(bar))
+
+ def foo(pos, *args, a=42, b, c, **kwargs:int): pass
+ def bar(pos, *args, c, b, a=42, **kwargs:int): pass
+ self.assertEqual(inspect.signature(foo), inspect.signature(bar))
+""")
+
+ def test_signature_unhashable(self):
+ def foo(a): pass
+ sig = inspect.signature(foo)
+ with self.assertRaisesRegex(TypeError, 'unhashable type'):
+ hash(sig)
+
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_str(self):
+ def foo(a:int=1, *, b, c=None, **kwargs) -> 42:
+ pass
+ self.assertEqual(str(inspect.signature(foo)),
+ '(a:int=1, *, b, c=None, **kwargs) -> 42')
+
+ def foo(a:int=1, *args, b, c=None, **kwargs) -> 42:
+ pass
+ self.assertEqual(str(inspect.signature(foo)),
+ '(a:int=1, *args, b, c=None, **kwargs) -> 42')
+
+ def foo():
+ pass
+ self.assertEqual(str(inspect.signature(foo)), '()')
+""")
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_str_positional_only(self):
+ P = inspect.Parameter
+
+ def test(a_po, *, b, **kwargs):
+ return a_po, kwargs
+
+ sig = inspect.signature(test)
+ new_params = list(sig.parameters.values())
+ new_params[0] = new_params[0].replace(kind=P.POSITIONAL_ONLY)
+ test.__signature__ = sig.replace(parameters=new_params)
+
+ self.assertEqual(str(inspect.signature(test)),
+ '(<a_po>, *, b, **kwargs)')
+
+ sig = inspect.signature(test)
+ new_params = list(sig.parameters.values())
+ new_params[0] = new_params[0].replace(name=None)
+ test.__signature__ = sig.replace(parameters=new_params)
+ self.assertEqual(str(inspect.signature(test)),
+ '(<0>, *, b, **kwargs)')
+""")
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_replace_anno(self):
+ def test() -> 42:
+ pass
+
+ sig = inspect.signature(test)
+ sig = sig.replace(return_annotation=None)
+ self.assertIs(sig.return_annotation, None)
+ sig = sig.replace(return_annotation=sig.empty)
+ self.assertIs(sig.return_annotation, sig.empty)
+ sig = sig.replace(return_annotation=42)
+ self.assertEqual(sig.return_annotation, 42)
+ self.assertEqual(sig, inspect.signature(test))
+""")
+
+
+class TestParameterObject(unittest.TestCase):
+
+ def test_signature_parameter_kinds(self):
+ P = inspect.Parameter
+ self.assertTrue(P.POSITIONAL_ONLY < P.POSITIONAL_OR_KEYWORD < \
+ P.VAR_POSITIONAL < P.KEYWORD_ONLY < P.VAR_KEYWORD)
+
+ self.assertEqual(str(P.POSITIONAL_ONLY), 'POSITIONAL_ONLY')
+ self.assertTrue('POSITIONAL_ONLY' in repr(P.POSITIONAL_ONLY))
+
+ def test_signature_parameter_object(self):
+ p = inspect.Parameter('foo', default=10,
+ kind=inspect.Parameter.POSITIONAL_ONLY)
+ self.assertEqual(p.name, 'foo')
+ self.assertEqual(p.default, 10)
+ self.assertIs(p.annotation, p.empty)
+ self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY)
+
+ with self.assertRaisesRegex(ValueError, 'invalid value'):
+ inspect.Parameter('foo', default=10, kind='123')
+
+ with self.assertRaisesRegex(ValueError, 'not a valid parameter name'):
+ inspect.Parameter('1', kind=inspect.Parameter.VAR_KEYWORD)
+
+ with self.assertRaisesRegex(ValueError,
+ 'non-positional-only parameter'):
+ inspect.Parameter(None, kind=inspect.Parameter.VAR_KEYWORD)
+
+ with self.assertRaisesRegex(ValueError, 'cannot have default values'):
+ inspect.Parameter('a', default=42,
+ kind=inspect.Parameter.VAR_KEYWORD)
+
+ with self.assertRaisesRegex(ValueError, 'cannot have default values'):
+ inspect.Parameter('a', default=42,
+ kind=inspect.Parameter.VAR_POSITIONAL)
+
+ p = inspect.Parameter('a', default=42,
+ kind=inspect.Parameter.POSITIONAL_OR_KEYWORD)
+ with self.assertRaisesRegex(ValueError, 'cannot have default values'):
+ p.replace(kind=inspect.Parameter.VAR_POSITIONAL)
+
+ self.assertTrue(repr(p).startswith('<Parameter'))
+
+ def test_signature_parameter_equality(self):
+ P = inspect.Parameter
+ p = P('foo', default=42, kind=inspect.Parameter.KEYWORD_ONLY)
+
+ self.assertEqual(p, p)
+ self.assertNotEqual(p, 42)
+
+ self.assertEqual(p, P('foo', default=42,
+ kind=inspect.Parameter.KEYWORD_ONLY))
+
+ def test_signature_parameter_unhashable(self):
+ p = inspect.Parameter('foo', default=42,
+ kind=inspect.Parameter.KEYWORD_ONLY)
+
+ with self.assertRaisesRegex(TypeError, 'unhashable type'):
+ hash(p)
+
+ def test_signature_parameter_replace(self):
+ p = inspect.Parameter('foo', default=42,
+ kind=inspect.Parameter.KEYWORD_ONLY)
+
+ self.assertIsNot(p, p.replace())
+ self.assertEqual(p, p.replace())
+
+ p2 = p.replace(annotation=1)
+ self.assertEqual(p2.annotation, 1)
+ p2 = p2.replace(annotation=p2.empty)
+ self.assertEqual(p, p2)
+
+ p2 = p2.replace(name='bar')
+ self.assertEqual(p2.name, 'bar')
+ self.assertNotEqual(p2, p)
+
+ with self.assertRaisesRegex(ValueError, 'not a valid parameter name'):
+ p2 = p2.replace(name=p2.empty)
+
+ p2 = p2.replace(name='foo', default=None)
+ self.assertIs(p2.default, None)
+ self.assertNotEqual(p2, p)
+
+ p2 = p2.replace(name='foo', default=p2.empty)
+ self.assertIs(p2.default, p2.empty)
+
+
+ p2 = p2.replace(default=42, kind=p2.POSITIONAL_OR_KEYWORD)
+ self.assertEqual(p2.kind, p2.POSITIONAL_OR_KEYWORD)
+ self.assertNotEqual(p2, p)
+
+ with self.assertRaisesRegex(ValueError, 'invalid value for'):
+ p2 = p2.replace(kind=p2.empty)
+
+ p2 = p2.replace(kind=p2.KEYWORD_ONLY)
+ self.assertEqual(p2, p)
+
+ def test_signature_parameter_positional_only(self):
+ p = inspect.Parameter(None, kind=inspect.Parameter.POSITIONAL_ONLY)
+ self.assertEqual(str(p), '<>')
+
+ p = p.replace(name='1')
+ self.assertEqual(str(p), '<1>')
+
+ def test_signature_parameter_immutability(self):
+ p = inspect.Parameter(None, kind=inspect.Parameter.POSITIONAL_ONLY)
+
+ with self.assertRaises(AttributeError):
+ p.foo = 'bar'
+
+ with self.assertRaises(AttributeError):
+ p.kind = 123
+
+
+class TestSignatureBind(unittest.TestCase):
+ @staticmethod
+ def call(func, *args, **kwargs):
+ sig = inspect.signature(func)
+ ba = sig.bind(*args, **kwargs)
+ return func(*ba.args, **ba.kwargs)
+
+ def test_signature_bind_empty(self):
+ def test():
+ return 42
+
+ self.assertEqual(self.call(test), 42)
+ with self.assertRaisesRegex(TypeError, 'too many positional arguments'):
+ self.call(test, 1)
+ with self.assertRaisesRegex(TypeError, 'too many positional arguments'):
+ self.call(test, 1, spam=10)
+ with self.assertRaisesRegex(TypeError, 'too many keyword arguments'):
+ self.call(test, spam=1)
+
+ def test_signature_bind_var(self):
+ def test(*args, **kwargs):
+ return args, kwargs
+
+ self.assertEqual(self.call(test), ((), {}))
+ self.assertEqual(self.call(test, 1), ((1,), {}))
+ self.assertEqual(self.call(test, 1, 2), ((1, 2), {}))
+ self.assertEqual(self.call(test, foo='bar'), ((), {'foo': 'bar'}))
+ self.assertEqual(self.call(test, 1, foo='bar'), ((1,), {'foo': 'bar'}))
+ self.assertEqual(self.call(test, args=10), ((), {'args': 10}))
+ self.assertEqual(self.call(test, 1, 2, foo='bar'),
+ ((1, 2), {'foo': 'bar'}))
+
+ def test_signature_bind_just_args(self):
+ def test(a, b, c):
+ return a, b, c
+
+ self.assertEqual(self.call(test, 1, 2, 3), (1, 2, 3))
+
+ with self.assertRaisesRegex(TypeError, 'too many positional arguments'):
+ self.call(test, 1, 2, 3, 4)
+
+ with self.assertRaisesRegex(TypeError, "'b' parameter lacking default"):
+ self.call(test, 1)
+
+ with self.assertRaisesRegex(TypeError, "'a' parameter lacking default"):
+ self.call(test)
+
+ def test(a, b, c=10):
+ return a, b, c
+ self.assertEqual(self.call(test, 1, 2, 3), (1, 2, 3))
+ self.assertEqual(self.call(test, 1, 2), (1, 2, 10))
+
+ def test(a=1, b=2, c=3):
+ return a, b, c
+ self.assertEqual(self.call(test, a=10, c=13), (10, 2, 13))
+ self.assertEqual(self.call(test, a=10), (10, 2, 3))
+ self.assertEqual(self.call(test, b=10), (1, 10, 3))
+
+ def test_signature_bind_varargs_order(self):
+ def test(*args):
+ return args
+
+ self.assertEqual(self.call(test), ())
+ self.assertEqual(self.call(test, 1, 2, 3), (1, 2, 3))
+
+ def test_signature_bind_args_and_varargs(self):
+ def test(a, b, c=3, *args):
+ return a, b, c, args
+
+ self.assertEqual(self.call(test, 1, 2, 3, 4, 5), (1, 2, 3, (4, 5)))
+ self.assertEqual(self.call(test, 1, 2), (1, 2, 3, ()))
+ self.assertEqual(self.call(test, b=1, a=2), (2, 1, 3, ()))
+ self.assertEqual(self.call(test, 1, b=2), (1, 2, 3, ()))
+
+ with self.assertRaisesRegex(TypeError,
+ "multiple values for argument 'c'"):
+ self.call(test, 1, 2, 3, c=4)
+
+ def test_signature_bind_just_kwargs(self):
+ def test(**kwargs):
+ return kwargs
+
+ self.assertEqual(self.call(test), {})
+ self.assertEqual(self.call(test, foo='bar', spam='ham'),
+ {'foo': 'bar', 'spam': 'ham'})
+
+ def test_signature_bind_args_and_kwargs(self):
+ def test(a, b, c=3, **kwargs):
+ return a, b, c, kwargs
+
+ self.assertEqual(self.call(test, 1, 2), (1, 2, 3, {}))
+ self.assertEqual(self.call(test, 1, 2, foo='bar', spam='ham'),
+ (1, 2, 3, {'foo': 'bar', 'spam': 'ham'}))
+ self.assertEqual(self.call(test, b=2, a=1, foo='bar', spam='ham'),
+ (1, 2, 3, {'foo': 'bar', 'spam': 'ham'}))
+ self.assertEqual(self.call(test, a=1, b=2, foo='bar', spam='ham'),
+ (1, 2, 3, {'foo': 'bar', 'spam': 'ham'}))
+ self.assertEqual(self.call(test, 1, b=2, foo='bar', spam='ham'),
+ (1, 2, 3, {'foo': 'bar', 'spam': 'ham'}))
+ self.assertEqual(self.call(test, 1, b=2, c=4, foo='bar', spam='ham'),
+ (1, 2, 4, {'foo': 'bar', 'spam': 'ham'}))
+ self.assertEqual(self.call(test, 1, 2, 4, foo='bar'),
+ (1, 2, 4, {'foo': 'bar'}))
+ self.assertEqual(self.call(test, c=5, a=4, b=3),
+ (4, 3, 5, {}))
+
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_bind_kwonly(self):
+ def test(*, foo):
+ return foo
+ with self.assertRaisesRegex(TypeError,
+ 'too many positional arguments'):
+ self.call(test, 1)
+ self.assertEqual(self.call(test, foo=1), 1)
+
+ def test(a, *, foo=1, bar):
+ return foo
+ with self.assertRaisesRegex(TypeError,
+ "'bar' parameter lacking default value"):
+ self.call(test, 1)
+
+ def test(foo, *, bar):
+ return foo, bar
+ self.assertEqual(self.call(test, 1, bar=2), (1, 2))
+ self.assertEqual(self.call(test, bar=2, foo=1), (1, 2))
+
+ with self.assertRaisesRegex(TypeError,
+ 'too many keyword arguments'):
+ self.call(test, bar=2, foo=1, spam=10)
+
+ with self.assertRaisesRegex(TypeError,
+ 'too many positional arguments'):
+ self.call(test, 1, 2)
+
+ with self.assertRaisesRegex(TypeError,
+ 'too many positional arguments'):
+ self.call(test, 1, 2, bar=2)
+
+ with self.assertRaisesRegex(TypeError,
+ 'too many keyword arguments'):
+ self.call(test, 1, bar=2, spam='ham')
+
+ with self.assertRaisesRegex(TypeError,
+ "'bar' parameter lacking default value"):
+ self.call(test, 1)
+
+ def test(foo, *, bar, **bin):
+ return foo, bar, bin
+ self.assertEqual(self.call(test, 1, bar=2), (1, 2, {}))
+ self.assertEqual(self.call(test, foo=1, bar=2), (1, 2, {}))
+ self.assertEqual(self.call(test, 1, bar=2, spam='ham'),
+ (1, 2, {'spam': 'ham'}))
+ self.assertEqual(self.call(test, spam='ham', foo=1, bar=2),
+ (1, 2, {'spam': 'ham'}))
+ with self.assertRaisesRegex(TypeError,
+ "'foo' parameter lacking default value"):
+ self.call(test, spam='ham', bar=2)
+ self.assertEqual(self.call(test, 1, bar=2, bin=1, spam=10),
+ (1, 2, {'bin': 1, 'spam': 10}))
+""")
+#
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_bind_arguments(self):
+ def test(a, *args, b, z=100, **kwargs):
+ pass
+ sig = inspect.signature(test)
+ ba = sig.bind(10, 20, b=30, c=40, args=50, kwargs=60)
+ # we won't have 'z' argument in the bound arguments object, as we didn't
+ # pass it to the 'bind'
+ self.assertEqual(tuple(ba.arguments.items()),
+ (('a', 10), ('args', (20,)), ('b', 30),
+ ('kwargs', {'c': 40, 'args': 50, 'kwargs': 60})))
+ self.assertEqual(ba.kwargs,
+ {'b': 30, 'c': 40, 'args': 50, 'kwargs': 60})
+ self.assertEqual(ba.args, (10, 20))
+""")
+#
+ if sys.version_info[0] > 2:
+ exec("""
+def test_signature_bind_positional_only(self):
+ P = inspect.Parameter
+
+ def test(a_po, b_po, c_po=3, foo=42, *, bar=50, **kwargs):
+ return a_po, b_po, c_po, foo, bar, kwargs
+
+ sig = inspect.signature(test)
+ new_params = collections.OrderedDict(tuple(sig.parameters.items()))
+ for name in ('a_po', 'b_po', 'c_po'):
+ new_params[name] = new_params[name].replace(kind=P.POSITIONAL_ONLY)
+ new_sig = sig.replace(parameters=new_params.values())
+ test.__signature__ = new_sig
+
+ self.assertEqual(self.call(test, 1, 2, 4, 5, bar=6),
+ (1, 2, 4, 5, 6, {}))
+
+ with self.assertRaisesRegex(TypeError, "parameter is positional only"):
+ self.call(test, 1, 2, c_po=4)
+
+ with self.assertRaisesRegex(TypeError, "parameter is positional only"):
+ self.call(test, a_po=1, b_po=2)
+""")
+
+ def test_bind_self(self):
+ class F:
+ def f(a, self):
+ return a, self
+ an_f = F()
+ partial_f = functools.partial(F.f, an_f)
+ ba = inspect.signature(partial_f).bind(self=10)
+ self.assertEqual((an_f, 10), partial_f(*ba.args, **ba.kwargs))
+
+
+class TestBoundArguments(unittest.TestCase):
+
+ def test_signature_bound_arguments_unhashable(self):
+ def foo(a): pass
+ ba = inspect.signature(foo).bind(1)
+
+ with self.assertRaisesRegex(TypeError, 'unhashable type'):
+ hash(ba)
+
+ def test_signature_bound_arguments_equality(self):
+ def foo(a): pass
+ ba = inspect.signature(foo).bind(1)
+ self.assertEqual(ba, ba)
+
+ ba2 = inspect.signature(foo).bind(1)
+ self.assertEqual(ba, ba2)
+
+ ba3 = inspect.signature(foo).bind(2)
+ self.assertNotEqual(ba, ba3)
+ ba3.arguments['a'] = 1
+ self.assertEqual(ba, ba3)
+
+ def bar(b): pass
+ ba4 = inspect.signature(bar).bind(1)
+ self.assertNotEqual(ba, ba4)
diff --git a/testing/web-platform/tests/tools/third_party/funcsigs/tox.ini b/testing/web-platform/tests/tools/third_party/funcsigs/tox.ini
new file mode 100644
index 0000000000..1873c744a0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/funcsigs/tox.ini
@@ -0,0 +1,8 @@
+[tox]
+envlist = py26, py27, py33, py34, py35, py36, pypy, pypy3
+
+[testenv]
+deps = -rrequirements/development.txt
+commands =
+ coverage run setup.py test
+ coverage report --show-missing
diff --git a/testing/web-platform/tests/tools/third_party/h2/.coveragerc b/testing/web-platform/tests/tools/third_party/h2/.coveragerc
new file mode 100644
index 0000000000..153e38d3e0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/.coveragerc
@@ -0,0 +1,18 @@
+[run]
+branch = True
+source = h2
+
+[report]
+fail_under = 100
+show_missing = True
+exclude_lines =
+ pragma: no cover
+ .*:.* # Python \d.*
+ assert False, "Should not be reachable"
+ .*:.* # Platform-specific:
+
+[paths]
+source =
+ h2/
+ .tox/*/lib/python*/site-packages/h2
+ .tox/pypy*/site-packages/h2
diff --git a/testing/web-platform/tests/tools/third_party/h2/CONTRIBUTORS.rst b/testing/web-platform/tests/tools/third_party/h2/CONTRIBUTORS.rst
new file mode 100644
index 0000000000..5c4849fef0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/CONTRIBUTORS.rst
@@ -0,0 +1,115 @@
+Hyper-h2 is written and maintained by Cory Benfield and various contributors:
+
+Development Lead
+````````````````
+
+- Cory Benfield <cory@lukasa.co.uk>
+
+Contributors
+````````````
+
+In chronological order:
+
+- Robert Collins (@rbtcollins)
+
+ - Provided invaluable and substantial early input into API design and layout.
+ - Added code preventing ``Proxy-Authorization`` from getting added to HPACK
+ compression contexts.
+
+- Maximilian Hils (@maximilianhils)
+
+ - Added asyncio example.
+
+- Alex Chan (@alexwlchan)
+
+ - Fixed docstring, added URLs to README.
+
+- Glyph Lefkowitz (@glyph)
+
+ - Improved example Twisted server.
+
+- Thomas Kriechbaumer (@Kriechi)
+
+ - Fixed incorrect arguments being passed to ``StreamIDTooLowError``.
+ - Added new arguments to ``close_connection``.
+
+- WeiZheng Xu (@boyxuper)
+
+ - Reported a bug relating to hyper-h2's updating of the connection window in
+ response to SETTINGS_INITIAL_WINDOW_SIZE.
+
+- Evgeny Tataurov (@etataurov)
+
+ - Added the ``additional_data`` field to the ``ConnectionTerminated`` event.
+
+- Brett Cannon (@brettcannon)
+
+ - Changed Travis status icon to SVG.
+ - Documentation improvements.
+
+- Felix Yan (@felixonmars)
+
+ - Widened allowed version numbers of enum34.
+ - Updated test requirements.
+
+- Keith Dart (@kdart)
+
+ - Fixed curio example server flow control problems.
+
+- Gil Gonçalves (@LuRsT)
+
+ - Added code forbidding non-RFC 7540 pseudo-headers.
+
+- Louis Taylor (@kragniz)
+
+ - Cleaned up the README
+
+- Berker Peksag (@berkerpeksag)
+
+ - Improved the docstring for ``StreamIDTooLowError``.
+
+- Adrian Lewis (@aidylewis)
+
+ - Fixed the broken Twisted HEAD request example.
+ - Added verification logic for ensuring that responses to HEAD requests have
+ no body.
+
+- Lorenzo (@Mec-iS)
+
+ - Changed documentation to stop using dictionaries for header blocks.
+
+- Kracekumar Ramaraj (@kracekumar)
+
+ - Cleaned up Twisted example.
+
+- @mlvnd
+
+ - Cleaned up curio example.
+
+- Tom Offermann (@toffer)
+
+ - Added Tornado example.
+
+- Tarashish Mishra (@sunu)
+
+ - Added code to reject header fields with leading/trailing whitespace.
+ - Added code to remove leading/trailing whitespace from sent header fields.
+
+- Nate Prewitt (@nateprewitt)
+
+ - Added code to validate that trailers do not contain pseudo-header fields.
+
+- Chun-Han, Hsiao (@chhsiao90)
+
+ - Fixed a bug with invalid ``HTTP2-Settings`` header output in plaintext
+ upgrade.
+
+- Bhavishya (@bhavishyagopesh)
+
+ - Added support for equality testing to ``h2.settings.Settings`` objects.
+
+- Fred Thomsen (@fredthomsen)
+
+ - Added logging.
+ - Enhance equality testing of ``h2.settings.Settings`` objects with
+ ``hypothesis``.
diff --git a/testing/web-platform/tests/tools/third_party/h2/HISTORY.rst b/testing/web-platform/tests/tools/third_party/h2/HISTORY.rst
new file mode 100644
index 0000000000..5244cd8a94
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/HISTORY.rst
@@ -0,0 +1,760 @@
+Release History
+===============
+
+3.2.0 (2020-02-08)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Receiving DATA frames on closed (or reset) streams now properly emit a
+ WINDOW_UPDATE to keep the connection flow window topped up.
+
+API Changes (Backward-Incompatible)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- ``h2.config.logger`` now uses a `trace(...)` function, in addition
+ to `debug(...)`. If you defined a custom logger object, you need to handle
+ these new function calls.
+
+
+3.1.1 (2019-08-02)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Ignore WINDOW_UPDATE and RST_STREAM frames received after stream
+ closure.
+
+
+3.1.0 (2019-01-22)
+------------------
+
+API Changes (Backward-Incompatible)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- ``h2.connection.H2Connection.data_to_send`` first and only argument ``amt``
+ was renamed to ``amount``.
+- Support for Python 3.3 has been removed.
+
+API Changes (Backward-Compatible)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- ``h2.connection.H2Connection.send_data`` now supports ``data`` parameter
+ being a ``memoryview`` object.
+- Refactor ping-related events: a ``h2.events.PingReceived`` event is fired
+ when a PING frame is received and a ``h2.events.PingAckReceived`` event is
+ fired when a PING frame with an ACK flag is received.
+ ``h2.events.PingAcknowledged`` is deprecated in favour of the identical
+ ``h2.events.PingAckReceived``.
+- Added ``ENABLE_CONNECT_PROTOCOL`` to ``h2.settings.SettingCodes``.
+- Support ``CONNECT`` requests with a ``:protocol`` pseudo header
+ thereby supporting RFC 8441.
+- A limit to the number of closed streams kept in memory by the
+ connection is applied. It can be configured by
+ ``h2.connection.H2Connection.MAX_CLOSED_STREAMS``.
+
+Bugfixes
+~~~~~~~~
+
+- Debug logging when stream_id is None is now fixed and no longer errors.
+
+3.0.1 (2017-04-03)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- CONTINUATION frames sent on closed streams previously caused stream errors
+ of type STREAM_CLOSED. RFC 7540 § 6.10 requires that these be connection
+ errors of type PROTOCOL_ERROR, and so this release changes to match that
+ behaviour.
+- Remote peers incrementing their inbound connection window beyond the maximum
+ allowed value now cause stream-level errors, rather than connection-level
+ errors, allowing connections to stay up longer.
+- h2 now rejects receiving and sending request header blocks that are missing
+ any of the mandatory pseudo-header fields (:path, :scheme, and :method).
+- h2 now rejects receiving and sending request header blocks that have an empty
+ :path pseudo-header value.
+- h2 now rejects receiving and sending request header blocks that contain
+ response-only pseudo-headers, and vice versa.
+- h2 now correct respects user-initiated changes to the HEADER_TABLE_SIZE
+ local setting, and ensures that if users shrink or increase the header
+ table size it is policed appropriately.
+
+
+2.6.2 (2017-04-03)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- CONTINUATION frames sent on closed streams previously caused stream errors
+ of type STREAM_CLOSED. RFC 7540 § 6.10 requires that these be connection
+ errors of type PROTOCOL_ERROR, and so this release changes to match that
+ behaviour.
+- Remote peers incrementing their inbound connection window beyond the maximum
+ allowed value now cause stream-level errors, rather than connection-level
+ errors, allowing connections to stay up longer.
+- h2 now rejects receiving and sending request header blocks that are missing
+ any of the mandatory pseudo-header fields (:path, :scheme, and :method).
+- h2 now rejects receiving and sending request header blocks that have an empty
+ :path pseudo-header value.
+- h2 now rejects receiving and sending request header blocks that contain
+ response-only pseudo-headers, and vice versa.
+- h2 now correct respects user-initiated changes to the HEADER_TABLE_SIZE
+ local setting, and ensures that if users shrink or increase the header
+ table size it is policed appropriately.
+
+
+2.5.4 (2017-04-03)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- CONTINUATION frames sent on closed streams previously caused stream errors
+ of type STREAM_CLOSED. RFC 7540 § 6.10 requires that these be connection
+ errors of type PROTOCOL_ERROR, and so this release changes to match that
+ behaviour.
+- Remote peers incrementing their inbound connection window beyond the maximum
+ allowed value now cause stream-level errors, rather than connection-level
+ errors, allowing connections to stay up longer.
+- h2 now correct respects user-initiated changes to the HEADER_TABLE_SIZE
+ local setting, and ensures that if users shrink or increase the header
+ table size it is policed appropriately.
+
+
+3.0.0 (2017-03-24)
+------------------
+
+API Changes (Backward-Incompatible)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- By default, hyper-h2 now joins together received cookie header fields, per
+ RFC 7540 Section 8.1.2.5.
+- Added a ``normalize_inbound_headers`` flag to the ``H2Configuration`` object
+ that defaults to ``True``. Setting this to ``False`` changes the behaviour
+ from the previous point back to the v2 behaviour.
+- Removed deprecated fields from ``h2.errors`` module.
+- Removed deprecated fields from ``h2.settings`` module.
+- Removed deprecated ``client_side`` and ``header_encoding`` arguments from
+ ``H2Connection``.
+- Removed deprecated ``client_side`` and ``header_encoding`` properties from
+ ``H2Connection``.
+- ``dict`` objects are no longer allowed for user-supplied headers.
+- The default header encoding is now ``None``, not ``utf-8``: this means that
+ all events that carry headers now return those headers as byte strings by
+ default. The header encoding can be set back to ``utf-8`` to restore the old
+ behaviour.
+
+API Changes (Backward-Compatible)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Added new ``UnknownFrameReceived`` event that fires when unknown extension
+ frames have been received. This only fires when using hyperframe 5.0 or
+ later: earlier versions of hyperframe cause us to silently ignore extension
+ frames.
+
+Bugfixes
+~~~~~~~~
+
+None
+
+
+2.6.1 (2017-03-16)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Allowed hyperframe v5 support while continuing to ignore unexpected frames.
+
+
+2.5.3 (2017-03-16)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Allowed hyperframe v5 support while continuing to ignore unexpected frames.
+
+
+2.4.4 (2017-03-16)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Allowed hyperframe v5 support while continuing to ignore unexpected frames.
+
+
+2.6.0 (2017-02-28)
+------------------
+
+API Changes (Backward-Compatible)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Added a new ``h2.events.Event`` class that acts as a base class for all
+ events.
+- Rather than reject outbound Connection-specific headers, h2 will now
+ normalize the header block by removing them.
+- Implement equality for the ``h2.settings.Settings`` class.
+- Added ``h2.settings.SettingCodes``, an enum that is used to store all the
+ HTTP/2 setting codes. This allows us to use a better printed representation of
+ the setting code in most places that it is used.
+- The ``setting`` field in ``ChangedSetting`` for the ``RemoteSettingsChanged``
+ and ``SettingsAcknowledged`` events has been updated to be instances of
+ ``SettingCodes`` whenever they correspond to a known setting code. When they
+ are an unknown setting code, they are instead ``int``. As ``SettingCodes`` is
+ a subclass of ``int``, this is non-breaking.
+- Deprecated the other fields in ``h2.settings``. These will be removed in
+ 3.0.0.
+- Added an optional ``pad_length`` parameter to ``H2Connection.send_data``
+ to allow the user to include padding on a data frame.
+- Added a new parameter to the ``h2.config.H2Configuration`` initializer which
+ takes a logger. This allows us to log by providing a logger that conforms
+ to the requirements of this module so that it can be used in different
+ environments.
+
+Bugfixes
+~~~~~~~~
+
+- Correctly reject pushed request header blocks whenever they have malformed
+ request header blocks.
+- Correctly normalize pushed request header blocks whenever they have
+ normalizable header fields.
+- Remote peers are now allowed to send zero or any positive number as a value
+ for ``SETTINGS_MAX_HEADER_LIST_SIZE``, where previously sending zero would
+ raise a ``InvalidSettingsValueError``.
+- Resolved issue where the ``HTTP2-Settings`` header value for plaintext
+ upgrade that was emitted by ``initiate_upgrade_connection`` included the
+ *entire* ``SETTINGS`` frame, instead of just the payload.
+- Resolved issue where the ``HTTP2-Settings`` header value sent by a client for
+ plaintext upgrade would be ignored by ``initiate_upgrade_connection``, rather
+ than have those settings applied appropriately.
+- Resolved an issue whereby certain frames received from a peer in the CLOSED
+ state would trigger connection errors when RFC 7540 says they should have
+ triggered stream errors instead. Added more detailed stream closure tracking
+ to ensure we don't throw away connections unnecessarily.
+
+
+2.5.2 (2017-01-27)
+------------------
+
+- Resolved issue where the ``HTTP2-Settings`` header value for plaintext
+ upgrade that was emitted by ``initiate_upgrade_connection`` included the
+ *entire* ``SETTINGS`` frame, instead of just the payload.
+- Resolved issue where the ``HTTP2-Settings`` header value sent by a client for
+ plaintext upgrade would be ignored by ``initiate_upgrade_connection``, rather
+ than have those settings applied appropriately.
+
+
+2.4.3 (2017-01-27)
+------------------
+
+- Resolved issue where the ``HTTP2-Settings`` header value for plaintext
+ upgrade that was emitted by ``initiate_upgrade_connection`` included the
+ *entire* ``SETTINGS`` frame, instead of just the payload.
+- Resolved issue where the ``HTTP2-Settings`` header value sent by a client for
+ plaintext upgrade would be ignored by ``initiate_upgrade_connection``, rather
+ than have those settings applied appropriately.
+
+
+2.3.4 (2017-01-27)
+------------------
+
+- Resolved issue where the ``HTTP2-Settings`` header value for plaintext
+ upgrade that was emitted by ``initiate_upgrade_connection`` included the
+ *entire* ``SETTINGS`` frame, instead of just the payload.
+- Resolved issue where the ``HTTP2-Settings`` header value sent by a client for
+ plaintext upgrade would be ignored by ``initiate_upgrade_connection``, rather
+ than have those settings applied appropriately.
+
+
+2.5.1 (2016-12-17)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Remote peers are now allowed to send zero or any positive number as a value
+ for ``SETTINGS_MAX_HEADER_LIST_SIZE``, where previously sending zero would
+ raise a ``InvalidSettingsValueError``.
+
+
+2.5.0 (2016-10-25)
+------------------
+
+API Changes (Backward-Compatible)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Added a new ``H2Configuration`` object that allows rich configuration of
+ a ``H2Connection``. This object supersedes the prior keyword arguments to the
+ ``H2Connection`` object, which are now deprecated and will be removed in 3.0.
+- Added support for automated window management via the
+ ``acknowledge_received_data`` method. See the documentation for more details.
+- Added a ``DenialOfServiceError`` that is raised whenever a behaviour that
+ looks like a DoS attempt is encountered: for example, an overly large
+ decompressed header list. This is a subclass of ``ProtocolError``.
+- Added support for setting and managing ``SETTINGS_MAX_HEADER_LIST_SIZE``.
+ This setting is now defaulted to 64kB.
+- Added ``h2.errors.ErrorCodes``, an enum that is used to store all the HTTP/2
+ error codes. This allows us to use a better printed representation of the
+ error code in most places that it is used.
+- The ``error_code`` fields on ``ConnectionTerminated`` and ``StreamReset``
+ events have been updated to be instances of ``ErrorCodes`` whenever they
+ correspond to a known error code. When they are an unknown error code, they
+ are instead ``int``. As ``ErrorCodes`` is a subclass of ``int``, this is
+ non-breaking.
+- Deprecated the other fields in ``h2.errors``. These will be removed in 3.0.0.
+
+Bugfixes
+~~~~~~~~
+
+- Correctly reject request header blocks with neither :authority nor Host
+ headers, or header blocks which contain mismatched :authority and Host
+ headers, per RFC 7540 Section 8.1.2.3.
+- Correctly expect that responses to HEAD requests will have no body regardless
+ of the value of the Content-Length header, and reject those that do.
+- Correctly refuse to send header blocks that contain neither :authority nor
+ Host headers, or header blocks which contain mismatched :authority and Host
+ headers, per RFC 7540 Section 8.1.2.3.
+- Hyper-h2 will now reject header field names and values that contain leading
+ or trailing whitespace.
+- Correctly strip leading/trailing whitespace from header field names and
+ values.
+- Correctly refuse to send header blocks with a TE header whose value is not
+ ``trailers``, per RFC 7540 Section 8.1.2.2.
+- Correctly refuse to send header blocks with connection-specific headers,
+ per RFC 7540 Section 8.1.2.2.
+- Correctly refuse to send header blocks that contain duplicate pseudo-header
+ fields, or with pseudo-header fields that appear after ordinary header fields,
+ per RFC 7540 Section 8.1.2.1.
+
+ This may cause passing a dictionary as the header block to ``send_headers``
+ to throw a ``ProtocolError``, because dictionaries are unordered and so they
+ may trip this check. Passing dictionaries here is deprecated, and callers
+ should change to using a sequence of 2-tuples as their header blocks.
+- Correctly reject trailers that contain HTTP/2 pseudo-header fields, per RFC
+ 7540 Section 8.1.2.1.
+- Correctly refuse to send trailers that contain HTTP/2 pseudo-header fields,
+ per RFC 7540 Section 8.1.2.1.
+- Correctly reject responses that do not contain the ``:status`` header field,
+ per RFC 7540 Section 8.1.2.4.
+- Correctly refuse to send responses that do not contain the ``:status`` header
+ field, per RFC 7540 Section 8.1.2.4.
+- Correctly update the maximum frame size when the user updates the value of
+ that setting. Prior to this release, if the user updated the maximum frame
+ size hyper-h2 would ignore the update, preventing the remote peer from using
+ the higher frame sizes.
+
+2.4.2 (2016-10-25)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Correctly update the maximum frame size when the user updates the value of
+ that setting. Prior to this release, if the user updated the maximum frame
+ size hyper-h2 would ignore the update, preventing the remote peer from using
+ the higher frame sizes.
+
+2.3.3 (2016-10-25)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Correctly update the maximum frame size when the user updates the value of
+ that setting. Prior to this release, if the user updated the maximum frame
+ size hyper-h2 would ignore the update, preventing the remote peer from using
+ the higher frame sizes.
+
+2.2.7 (2016-10-25)
+------------------
+
+*Final 2.2.X release*
+
+Bugfixes
+~~~~~~~~
+
+- Correctly update the maximum frame size when the user updates the value of
+ that setting. Prior to this release, if the user updated the maximum frame
+ size hyper-h2 would ignore the update, preventing the remote peer from using
+ the higher frame sizes.
+
+2.4.1 (2016-08-23)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Correctly expect that responses to HEAD requests will have no body regardless
+ of the value of the Content-Length header, and reject those that do.
+
+2.3.2 (2016-08-23)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Correctly expect that responses to HEAD requests will have no body regardless
+ of the value of the Content-Length header, and reject those that do.
+
+2.4.0 (2016-07-01)
+------------------
+
+API Changes (Backward-Compatible)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Adds ``additional_data`` to ``H2Connection.close_connection``, allowing the
+ user to send additional debug data on the GOAWAY frame.
+- Adds ``last_stream_id`` to ``H2Connection.close_connection``, allowing the
+ user to manually control what the reported last stream ID is.
+- Add new method: ``prioritize``.
+- Add support for emitting stream priority information when sending headers
+ frames using three new keyword arguments: ``priority_weight``,
+ ``priority_depends_on``, and ``priority_exclusive``.
+- Add support for "related events": events that fire simultaneously on a single
+ frame.
+
+
+2.3.1 (2016-05-12)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Resolved ``AttributeError`` encountered when receiving more than one sequence
+ of CONTINUATION frames on a given connection.
+
+
+2.2.5 (2016-05-12)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Resolved ``AttributeError`` encountered when receiving more than one sequence
+ of CONTINUATION frames on a given connection.
+
+
+2.3.0 (2016-04-26)
+------------------
+
+API Changes (Backward-Compatible)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Added a new flag to the ``H2Connection`` constructor: ``header_encoding``,
+ that controls what encoding is used (if any) to decode the headers from bytes
+ to unicode. This defaults to UTF-8 for backward compatibility. To disable the
+ decode and use bytes exclusively, set the field to False, None, or the empty
+ string. This affects all headers, including those pushed by servers.
+- Bumped the minimum version of HPACK allowed from 2.0 to 2.2.
+- Added support for advertising RFC 7838 Alternative services.
+- Allowed users to provide ``hpack.HeaderTuple`` and
+ ``hpack.NeverIndexedHeaderTuple`` objects to all methods that send headers.
+- Changed all events that carry headers to emit ``hpack.HeaderTuple`` and
+ ``hpack.NeverIndexedHeaderTuple`` instead of plain tuples. This allows users
+ to maintain header indexing state.
+- Added support for plaintext upgrade with the ``initiate_upgrade_connection``
+ method.
+
+Bugfixes
+~~~~~~~~
+
+- Automatically ensure that all ``Authorization`` and ``Proxy-Authorization``
+ headers, as well as short ``Cookie`` headers, are prevented from being added
+ to encoding contexts.
+
+2.2.4 (2016-04-25)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Correctly forbid pseudo-headers that were not defined in RFC 7540.
+- Ignore AltSvc frames, rather than exploding when receiving them.
+
+2.1.5 (2016-04-25)
+------------------
+
+*Final 2.1.X release*
+
+Bugfixes
+~~~~~~~~
+
+- Correctly forbid pseudo-headers that were not defined in RFC 7540.
+- Ignore AltSvc frames, rather than exploding when receiving them.
+
+2.2.3 (2016-04-13)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Allowed the 4.X series of hyperframe releases as dependencies.
+
+2.1.4 (2016-04-13)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Allowed the 4.X series of hyperframe releases as dependencies.
+
+
+2.2.2 (2016-04-05)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Fixed issue where informational responses were erroneously not allowed to be
+ sent in the ``HALF_CLOSED_REMOTE`` state.
+- Fixed issue where informational responses were erroneously not allowed to be
+ received in the ``HALF_CLOSED_LOCAL`` state.
+- Fixed issue where we allowed information responses to be sent or received
+ after final responses.
+
+2.2.1 (2016-03-23)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Fixed issue where users using locales that did not default to UTF-8 were
+ unable to install source distributions of the package.
+
+2.2.0 (2016-03-23)
+------------------
+
+API Changes (Backward-Compatible)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Added support for sending informational responses (responses with 1XX status)
+ codes as part of the standard flow. HTTP/2 allows zero or more informational
+ responses with no upper limit: hyper-h2 does too.
+- Added support for receiving informational responses (responses with 1XX
+ status) codes as part of the standard flow. HTTP/2 allows zero or more
+ informational responses with no upper limit: hyper-h2 does too.
+- Added a new event: ``ReceivedInformationalResponse``. This response is fired
+ when informational responses (those with 1XX status codes).
+- Added an ``additional_data`` field to the ``ConnectionTerminated`` event that
+ carries any additional data sent on the GOAWAY frame. May be ``None`` if no
+ such data was sent.
+- Added the ``initial_values`` optional argument to the ``Settings`` object.
+
+Bugfixes
+~~~~~~~~
+
+- Correctly reject all of the connection-specific headers mentioned in RFC 7540
+ § 8.1.2.2, not just the ``Connection:`` header.
+- Defaulted the value of ``SETTINGS_MAX_CONCURRENT_STREAMS`` to 100, unless
+ explicitly overridden. This is a safe defensive initial value for this
+ setting.
+
+2.1.3 (2016-03-16)
+------------------
+
+Deprecations
+~~~~~~~~~~~~
+
+- Passing dictionaries to ``send_headers`` as the header block is deprecated,
+ and will be removed in 3.0.
+
+2.1.2 (2016-02-17)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Reject attempts to push streams on streams that were themselves pushed:
+ streams can only be pushed on streams that were initiated by the client.
+- Correctly allow CONTINUATION frames to extend the header block started by a
+ PUSH_PROMISE frame.
+- Changed our handling of frames received on streams that were reset by the
+ user.
+
+ Previously these would, at best, cause ProtocolErrors to be raised and the
+ connection to be torn down (rather defeating the point of resetting streams
+ at all) and, at worst, would cause subtle inconsistencies in state between
+ hyper-h2 and the remote peer that could lead to header block decoding errors
+ or flow control blockages.
+
+ Now when the user resets a stream all further frames received on that stream
+ are ignored except where they affect some form of connection-level state,
+ where they have their effect and are then ignored.
+- Fixed a bug whereby receiving a PUSH_PROMISE frame on a stream that was
+ closed would cause a RST_STREAM frame to be emitted on the closed-stream,
+ but not the newly-pushed one. Now this causes a ``ProtocolError``.
+
+2.1.1 (2016-02-05)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Added debug representations for all events.
+- Fixed problems with setup.py that caused trouble on older setuptools/pip
+ installs.
+
+2.1.0 (2016-02-02)
+------------------
+
+API Changes (Backward-Compatible)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Added new field to ``DataReceived``: ``flow_controlled_length``. This is the
+ length of the frame including padded data, allowing users to correctly track
+ changes to the flow control window.
+- Defined new ``UnsupportedFrameError``, thrown when frames that are known to
+ hyperframe but not supported by hyper-h2 are received. For
+ backward-compatibility reasons, this is a ``ProtocolError`` *and* a
+ ``KeyError``.
+
+Bugfixes
+~~~~~~~~
+
+- Hyper-h2 now correctly accounts for padding when maintaining flow control
+ windows.
+- Resolved a bug where hyper-h2 would mistakenly apply
+ SETTINGS_INITIAL_WINDOW_SIZE to the connection flow control window in
+ addition to the stream-level flow control windows.
+- Invalid Content-Length headers now throw ``ProtocolError`` exceptions and
+ correctly tear the connection down, instead of leaving the connection in an
+ indeterminate state.
+- Invalid header blocks now throw ``ProtocolError``, rather than a grab bag of
+ possible other exceptions.
+
+2.0.0 (2016-01-25)
+------------------
+
+API Changes (Breaking)
+~~~~~~~~~~~~~~~~~~~~~~
+
+- Attempts to open streams with invalid stream IDs, either by the remote peer
+ or by the user, are now rejected as a ``ProtocolError``. Previously these
+ were allowed, and would cause remote peers to error.
+- Receiving frames that have invalid padding now causes the connection to be
+ terminated with a ``ProtocolError`` being raised. Previously these passed
+ undetected.
+- Settings values set by both the user and the remote peer are now validated
+ when they're set. If they're invalid, a new ``InvalidSettingsValueError`` is
+ raised and, if set by the remote peer, a connection error is signaled.
+ Previously, it was possible to set invalid values. These would either be
+ caught when building frames, or would be allowed to stand.
+- Settings changes no longer require user action to be acknowledged: hyper-h2
+ acknowledges them automatically. This moves the location where some
+ exceptions may be thrown, and also causes the ``acknowledge_settings`` method
+ to be removed from the public API.
+- Removed a number of methods on the ``H2Connection`` object from the public,
+ semantically versioned API, by renaming them to have leading underscores.
+ Specifically, removed:
+
+ - ``get_stream_by_id``
+ - ``get_or_create_stream``
+ - ``begin_new_stream``
+ - ``receive_frame``
+ - ``acknowledge_settings``
+
+- Added full support for receiving CONTINUATION frames, including policing
+ logic about when and how they are received. Previously, receiving
+ CONTINUATION frames was not supported and would throw exceptions.
+- All public API functions on ``H2Connection`` except for ``receive_data`` no
+ longer return lists of events, because these lists were always empty. Events
+ are now only raised by ``receive_data``.
+- Calls to ``increment_flow_control_window`` with out of range values now raise
+ ``ValueError`` exceptions. Previously they would be allowed, or would cause
+ errors when serializing frames.
+
+API Changes (Backward-Compatible)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Added ``PriorityUpdated`` event for signaling priority changes.
+- Added ``get_next_available_stream_id`` function.
+- Receiving DATA frames on streams not in the OPEN or HALF_CLOSED_LOCAL states
+ now causes a stream reset, rather than a connection reset. The error is now
+ also classified as a ``StreamClosedError``, rather than a more generic
+ ``ProtocolError``.
+- Receiving HEADERS or PUSH_PROMISE frames in the HALF_CLOSED_REMOTE state now
+ causes a stream reset, rather than a connection reset.
+- Receiving frames that violate the max frame size now causes connection errors
+ with error code FRAME_SIZE_ERROR, not a generic PROTOCOL_ERROR. This
+ condition now also raises a ``FrameTooLargeError``, a new subclass of
+ ``ProtocolError``.
+- Made ``NoSuchStreamError`` a subclass of ``ProtocolError``.
+- The ``StreamReset`` event is now also fired whenever a protocol error from
+ the remote peer forces a stream to close early. This is only fired once.
+- The ``StreamReset`` event now carries a flag, ``remote_reset``, that is set
+ to ``True`` in all cases where ``StreamReset`` would previously have fired
+ (e.g. when the remote peer sent a RST_STREAM), and is set to ``False`` when
+ it fires because the remote peer made a protocol error.
+- Hyper-h2 now rejects attempts by peers to increment a flow control window by
+ zero bytes.
+- Hyper-h2 now rejects peers sending header blocks that are ill-formed for a
+ number of reasons as set out in RFC 7540 Section 8.1.2.
+- Attempting to send non-PRIORITY frames on closed streams now raises
+ ``StreamClosedError``.
+- Remote peers attempting to increase the flow control window beyond
+ ``2**31 - 1``, either by window increment or by settings frame, are now
+ rejected as ``ProtocolError``.
+- Local attempts to increase the flow control window beyond ``2**31 - 1`` by
+ window increment are now rejected as ``ProtocolError``.
+- The bytes that represent individual settings are now available in
+ ``h2.settings``, instead of needing users to import them from hyperframe.
+
+Bugfixes
+~~~~~~~~
+
+- RFC 7540 requires that a separate minimum stream ID be used for inbound and
+ outbound streams. Hyper-h2 now obeys this requirement.
+- Hyper-h2 now does a better job of reporting the last stream ID it has
+ partially handled when terminating connections.
+- Fixed an error in the arguments of ``StreamIDTooLowError``.
+- Prevent ``ValueError`` leaking from Hyperframe.
+- Prevent ``struct.error`` and ``InvalidFrameError`` leaking from Hyperframe.
+
+1.1.1 (2015-11-17)
+------------------
+
+Bugfixes
+~~~~~~~~
+
+- Forcibly lowercase all header names to improve compatibility with
+ implementations that demand lower-case header names.
+
+1.1.0 (2015-10-28)
+------------------
+
+API Changes (Backward-Compatible)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Added a new ``ConnectionTerminated`` event, which fires when GOAWAY frames
+ are received.
+- Added a subclass of ``NoSuchStreamError``, called ``StreamClosedError``, that
+ fires when actions are taken on a stream that is closed and has had its state
+ flushed from the system.
+- Added ``StreamIDTooLowError``, raised when the user or the remote peer
+ attempts to create a stream with an ID lower than one previously used in the
+ dialog. Inherits from ``ValueError`` for backward-compatibility reasons.
+
+Bugfixes
+~~~~~~~~
+
+- Do not throw ``ProtocolError`` when attempting to send multiple GOAWAY
+ frames on one connection.
+- We no longer forcefully change the decoder table size when settings changes
+ are ACKed, instead waiting for remote acknowledgement of the change.
+- Improve the performance of checking whether a stream is open.
+- We now attempt to lazily garbage collect closed streams, to avoid having the
+ state hang around indefinitely, leaking memory.
+- Avoid further per-stream allocations, leading to substantial performance
+ improvements when many short-lived streams are used.
+
+1.0.0 (2015-10-15)
+------------------
+
+- First production release!
diff --git a/testing/web-platform/tests/tools/third_party/h2/LICENSE b/testing/web-platform/tests/tools/third_party/h2/LICENSE
new file mode 100644
index 0000000000..7bb76c58ec
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015-2016 Cory Benfield and 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
+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/testing/web-platform/tests/tools/third_party/h2/MANIFEST.in b/testing/web-platform/tests/tools/third_party/h2/MANIFEST.in
new file mode 100644
index 0000000000..04400de6b7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/MANIFEST.in
@@ -0,0 +1,8 @@
+include README.rst LICENSE CONTRIBUTORS.rst HISTORY.rst tox.ini test_requirements.txt .coveragerc Makefile
+recursive-include test *.py *.sh
+graft docs
+prune docs/build
+graft visualizer
+recursive-include examples *.py *.crt *.key *.pem *.csr
+recursive-include utils *.sh
+recursive-include _travis *.sh
diff --git a/testing/web-platform/tests/tools/third_party/h2/Makefile b/testing/web-platform/tests/tools/third_party/h2/Makefile
new file mode 100644
index 0000000000..689077472f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/Makefile
@@ -0,0 +1,9 @@
+.PHONY: publish test
+
+publish:
+ rm -rf dist/
+ python setup.py sdist bdist_wheel
+ twine upload -s dist/*
+
+test:
+ py.test -n 4 --cov h2 test/
diff --git a/testing/web-platform/tests/tools/third_party/h2/README.rst b/testing/web-platform/tests/tools/third_party/h2/README.rst
new file mode 100644
index 0000000000..7140d37acc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/README.rst
@@ -0,0 +1,65 @@
+===============================
+hyper-h2: HTTP/2 Protocol Stack
+===============================
+
+.. image:: https://raw.github.com/Lukasa/hyper/development/docs/source/images/hyper.png
+
+.. image:: https://travis-ci.org/python-hyper/hyper-h2.svg?branch=master
+ :target: https://travis-ci.org/python-hyper/hyper-h2
+
+This repository contains a pure-Python implementation of a HTTP/2 protocol
+stack. It's written from the ground up to be embeddable in whatever program you
+choose to use, ensuring that you can speak HTTP/2 regardless of your
+programming paradigm.
+
+You use it like this:
+
+.. code-block:: python
+
+ import h2.connection
+
+ conn = h2.connection.H2Connection()
+ conn.send_headers(stream_id=stream_id, headers=headers)
+ conn.send_data(stream_id, data)
+ socket.sendall(conn.data_to_send())
+ events = conn.receive_data(socket_data)
+
+This repository does not provide a parsing layer, a network layer, or any rules
+about concurrency. Instead, it's a purely in-memory solution, defined in terms
+of data actions and HTTP/2 frames. This is one building block of a full Python
+HTTP implementation.
+
+To install it, just run:
+
+.. code-block:: console
+
+ $ pip install h2
+
+Documentation
+=============
+
+Documentation is available at http://python-hyper.org/h2/.
+
+Contributing
+============
+
+``hyper-h2`` welcomes contributions from anyone! Unlike many other projects we
+are happy to accept cosmetic contributions and small contributions, in addition
+to large feature requests and changes.
+
+Before you contribute (either by opening an issue or filing a pull request),
+please `read the contribution guidelines`_.
+
+.. _read the contribution guidelines: http://python-hyper.org/en/latest/contributing.html
+
+License
+=======
+
+``hyper-h2`` is made available under the MIT License. For more details, see the
+``LICENSE`` file in the repository.
+
+Authors
+=======
+
+``hyper-h2`` is maintained by Cory Benfield, with contributions from others. For
+more details about the contributors, please see ``CONTRIBUTORS.rst``.
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/Makefile b/testing/web-platform/tests/tools/third_party/h2/docs/Makefile
new file mode 100644
index 0000000000..32b233be83
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+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 " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @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)/*
+
+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/hyper-h2.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/hyper-h2.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/hyper-h2"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/hyper-h2"
+ @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."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @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."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+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."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/make.bat b/testing/web-platform/tests/tools/third_party/h2/docs/make.bat
new file mode 100644
index 0000000000..537686d817
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/make.bat
@@ -0,0 +1,242 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
+set I18NSPHINXOPTS=%SPHINXOPTS% source
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :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. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\hyper-h2.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\hyper-h2.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+:end
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/_static/.keep b/testing/web-platform/tests/tools/third_party/h2/docs/source/_static/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/_static/.keep
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/_static/h2.connection.H2ConnectionStateMachine.dot.png b/testing/web-platform/tests/tools/third_party/h2/docs/source/_static/h2.connection.H2ConnectionStateMachine.dot.png
new file mode 100644
index 0000000000..f2c814ec77
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/_static/h2.connection.H2ConnectionStateMachine.dot.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/_static/h2.stream.H2StreamStateMachine.dot.png b/testing/web-platform/tests/tools/third_party/h2/docs/source/_static/h2.stream.H2StreamStateMachine.dot.png
new file mode 100644
index 0000000000..85bcb68321
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/_static/h2.stream.H2StreamStateMachine.dot.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/advanced-usage.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/advanced-usage.rst
new file mode 100644
index 0000000000..40496f0eae
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/advanced-usage.rst
@@ -0,0 +1,325 @@
+Advanced Usage
+==============
+
+Priority
+--------
+
+.. versionadded:: 2.0.0
+
+`RFC 7540`_ has a fairly substantial and complex section describing how to
+build a HTTP/2 priority tree, and the effect that should have on sending data
+from a server.
+
+Hyper-h2 does not enforce any priority logic by default for servers. This is
+because scheduling data sends is outside the scope of this library, as it
+likely requires fairly substantial understanding of the scheduler being used.
+
+However, for servers that *do* want to follow the priority recommendations
+given by clients, the Hyper project provides `an implementation`_ of the
+`RFC 7540`_ priority tree that will be useful to plug into a server. That,
+combined with the :class:`PriorityUpdated <h2.events.PriorityUpdated>` event from
+this library, can be used to build a server that conforms to RFC 7540's
+recommendations for priority handling.
+
+Related Events
+--------------
+
+.. versionadded:: 2.4.0
+
+In the 2.4.0 release hyper-h2 added support for signaling "related events".
+These are a HTTP/2-only construct that exist because certain HTTP/2 events can
+occur simultaneously: that is, one HTTP/2 frame can cause multiple state
+transitions to occur at the same time. One example of this is a HEADERS frame
+that contains priority information and carries the END_STREAM flag: this would
+cause three events to fire (one of the various request/response received
+events, a :class:`PriorityUpdated <h2.events.PriorityUpdated>` event, and a
+:class:`StreamEnded <h2.events.StreamEnded>` event).
+
+Ordinarily hyper-h2's logic will emit those events to you one at a time. This
+means that you may attempt to process, for example, a
+:class:`DataReceived <h2.events.DataReceived>` event, not knowing that the next
+event out will be a :class:`StreamEnded <h2.events.StreamEnded>` event.
+hyper-h2 *does* know this, however, and so will forbid you from taking certain
+actions that are a violation of the HTTP/2 protocol.
+
+To avoid this asymmetry of information, events that can occur simultaneously
+now carry properties for their "related events". These allow users to find the
+events that can have occurred simultaneously with each other before the event
+is emitted by hyper-h2. The following objects have "related events":
+
+- :class:`RequestReceived <h2.events.RequestReceived>`:
+
+ - :data:`stream_ended <h2.events.RequestReceived.stream_ended>`: any
+ :class:`StreamEnded <h2.events.StreamEnded>` event that occurred at the
+ same time as receiving this request.
+
+ - :data:`priority_updated
+ <h2.events.RequestReceived.priority_updated>`: any
+ :class:`PriorityUpdated <h2.events.PriorityUpdated>` event that occurred
+ at the same time as receiving this request.
+
+- :class:`ResponseReceived <h2.events.ResponseReceived>`:
+
+ - :data:`stream_ended <h2.events.ResponseReceived.stream_ended>`: any
+ :class:`StreamEnded <h2.events.StreamEnded>` event that occurred at the
+ same time as receiving this response.
+
+ - :data:`priority_updated
+ <h2.events.ResponseReceived.priority_updated>`: any
+ :class:`PriorityUpdated <h2.events.PriorityUpdated>` event that occurred
+ at the same time as receiving this response.
+
+- :class:`TrailersReceived <h2.events.TrailersReceived>`:
+
+ - :data:`stream_ended <h2.events.TrailersReceived.stream_ended>`: any
+ :class:`StreamEnded <h2.events.StreamEnded>` event that occurred at the
+ same time as receiving this set of trailers. This will **always** be
+ present for trailers, as they must terminate streams.
+
+ - :data:`priority_updated
+ <h2.events.TrailersReceived.priority_updated>`: any
+ :class:`PriorityUpdated <h2.events.PriorityUpdated>` event that occurred
+ at the same time as receiving this response.
+
+- :class:`InformationalResponseReceived
+ <h2.events.InformationalResponseReceived>`:
+
+ - :data:`priority_updated
+ <h2.events.InformationalResponseReceived.priority_updated>`: any
+ :class:`PriorityUpdated <h2.events.PriorityUpdated>` event that occurred
+ at the same time as receiving this informational response.
+
+- :class:`DataReceived <h2.events.DataReceived>`:
+
+ - :data:`stream_ended <h2.events.DataReceived.stream_ended>`: any
+ :class:`StreamEnded <h2.events.StreamEnded>` event that occurred at the
+ same time as receiving this data.
+
+
+.. warning:: hyper-h2 does not know if you are looking for related events or
+ expecting to find events in the event stream. Therefore, it will
+ always emit "related events" in the event stream. If you are using
+ the "related events" event pattern, you will want to be careful to
+ avoid double-processing related events.
+
+.. _h2-connection-advanced:
+
+Connections: Advanced
+---------------------
+
+Thread Safety
+~~~~~~~~~~~~~
+
+``H2Connection`` objects are *not* thread-safe. They cannot safely be accessed
+from multiple threads at once. This is a deliberate design decision: it is not
+trivially possible to design the ``H2Connection`` object in a way that would
+be either lock-free or have the locks at a fine granularity.
+
+Your implementations should bear this in mind, and handle it appropriately. It
+should be simple enough to use locking alongside the ``H2Connection``: simply
+lock around the connection object itself. Because the ``H2Connection`` object
+does no I/O it should be entirely safe to do that. Alternatively, have a single
+thread take ownership of the ``H2Connection`` and use a message-passing
+interface to serialize access to the ``H2Connection``.
+
+If you are using a non-threaded concurrency approach (e.g. Twisted), this
+should not affect you.
+
+Internal Buffers
+~~~~~~~~~~~~~~~~
+
+In order to avoid doing I/O, the ``H2Connection`` employs an internal buffer.
+This buffer is *unbounded* in size: it can potentially grow infinitely. This
+means that, if you are not making sure to regularly empty it, you are at risk
+of exceeding the memory limit of a single process and finding your program
+crashes.
+
+It is highly recommended that you send data at regular intervals, ideally as
+soon as possible.
+
+.. _advanced-sending-data:
+
+Sending Data
+~~~~~~~~~~~~
+
+When sending data on the network, it's important to remember that you may not
+be able to send an unbounded amount of data at once. Particularly when using
+TCP, it is often the case that there are limits on how much data may be in
+flight at any one time. These limits can be very low, and your operating system
+will only buffer so much data in memory before it starts to complain.
+
+For this reason, it is possible to consume only a subset of the data available
+when you call :meth:`data_to_send <h2.connection.H2Connection.data_to_send>`.
+However, once you have pulled the data out of the ``H2Connection`` internal
+buffer, it is *not* possible to put it back on again. For that reason, it is
+adviseable that you confirm how much space is available in the OS buffer before
+sending.
+
+Alternatively, use tools made available by your framework. For example, the
+Python standard library :mod:`socket <python:socket>` module provides a
+:meth:`sendall <python:socket.socket.sendall>` method that will automatically
+block until all the data has been sent. This will enable you to always use the
+unbounded form of
+:meth:`data_to_send <h2.connection.H2Connection.data_to_send>`, and will help
+you avoid subtle bugs.
+
+When To Send
+~~~~~~~~~~~~
+
+In addition to knowing how much data to send (see :ref:`advanced-sending-data`)
+it is important to know when to send data. For hyper-h2, this amounts to
+knowing when to call :meth:`data_to_send
+<h2.connection.H2Connection.data_to_send>`.
+
+Hyper-h2 may write data into its send buffer at two times. The first is
+whenever :meth:`receive_data <h2.connection.H2Connection.receive_data>` is
+called. This data is sent in response to some control frames that require no
+user input: for example, responding to PING frames. The second time is in
+response to user action: whenever a user calls a method like
+:meth:`send_headers <h2.connection.H2Connection.send_headers>`, data may be
+written into the buffer.
+
+In a standard design for a hyper-h2 consumer, then, that means there are two
+places where you'll potentially want to send data. The first is in your
+"receive data" loop. This is where you take the data you receive, pass it into
+:meth:`receive_data <h2.connection.H2Connection.receive_data>`, and then
+dispatch events. For this loop, it is usually best to save sending data until
+the loop is complete: that allows you to empty the buffer only once.
+
+The other place you'll want to send the data is when initiating requests or
+taking any other active, unprompted action on the connection. In this instance,
+you'll want to make all the relevant ``send_*`` calls, and *then* call
+:meth:`data_to_send <h2.connection.H2Connection.data_to_send>`.
+
+Headers
+-------
+
+HTTP/2 defines several "special header fields" which are used to encode data
+that was previously sent in either the request or status line of HTTP/1.1.
+These header fields are distinguished from ordinary header fields because their
+field name begins with a ``:`` character. The special header fields defined in
+`RFC 7540`_ are:
+
+- ``:status``
+- ``:path``
+- ``:method``
+- ``:scheme``
+- ``:authority``
+
+`RFC 7540`_ **mandates** that all of these header fields appear *first* in the
+header block, before the ordinary header fields. This could cause difficulty if
+the :meth:`send_headers <h2.connection.H2Connection.send_headers>` method
+accepted a plain ``dict`` for the ``headers`` argument, because ``dict``
+objects are unordered. For this reason, we require that you provide a list of
+two-tuples.
+
+.. _RFC 7540: https://tools.ietf.org/html/rfc7540
+.. _an implementation: http://python-hyper.org/projects/priority/en/latest/
+
+Flow Control
+------------
+
+HTTP/2 defines a complex flow control system that uses a sliding window of
+data on both a per-stream and per-connection basis. Essentially, each
+implementation allows its peer to send a specific amount of data at any time
+(the "flow control window") before it must stop. Each stream has a separate
+window, and the connection as a whole has a window. Each window can be opened
+by an implementation by sending a ``WINDOW_UPDATE`` frame, either on a specific
+stream (causing the window for that stream to be opened), or on stream ``0``,
+which causes the window for the entire connection to be opened.
+
+In HTTP/2, only data in ``DATA`` frames is flow controlled. All other frames
+are exempt from flow control. Each ``DATA`` frame consumes both stream and
+connection flow control window bytes. This means that the maximum amount of
+data that can be sent on any one stream before a ``WINDOW_UPDATE`` frame is
+received is the *lower* of the stream and connection windows. The maximum
+amount of data that can be sent on *all* streams before a ``WINDOW_UPDATE``
+frame is received is the size of the connection flow control window.
+
+Working With Flow Control
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The amount of flow control window a ``DATA`` frame consumes is the sum of both
+its contained application data *and* the amount of padding used. hyper-h2 shows
+this to the user in a :class:`DataReceived <h2.events.DataReceived>` event by
+using the :data:`flow_controlled_length
+<h2.events.DataReceived.flow_controlled_length>` field. When working with flow
+control in hyper-h2, users *must* use this field: simply using
+``len(datareceived.data)`` can eventually lead to deadlock.
+
+When data has been received and given to the user in a :class:`DataReceived
+<h2.events.DataReceived>`, it is the responsibility of the user to re-open the
+flow control window when the user is ready for more data. hyper-h2 does not do
+this automatically to avoid flooding the user with data: if we did, the remote
+peer could send unbounded amounts of data that the user would need to buffer
+before processing.
+
+To re-open the flow control window, then, the user must call
+:meth:`increment_flow_control_window
+<h2.connection.H2Connection.increment_flow_control_window>` with the
+:data:`flow_controlled_length <h2.events.DataReceived.flow_controlled_length>`
+of the received data. hyper-h2 requires that you manage both the connection
+and the stream flow control windows separately, so you may need to increment
+both the stream the data was received on and stream ``0``.
+
+When sending data, a HTTP/2 implementation must not send more than flow control
+window available for that stream. As noted above, the maximum amount of data
+that can be sent on the stream is the minimum of the stream and the connection
+flow control windows. You can find out how much data you can send on a given
+stream by using the :meth:`local_flow_control_window
+<h2.connection.H2Connection.local_flow_control_window>` method, which will do
+all of these calculations for you. If you attempt to send more than this amount
+of data on a stream, hyper-h2 will throw a :class:`ProtocolError
+<h2.exceptions.ProtocolError>` and refuse to send the data.
+
+In hyper-h2, receiving a ``WINDOW_UPDATE`` frame causes a :class:`WindowUpdated
+<h2.events.WindowUpdated>` event to fire. This will notify you that there is
+potentially more room in a flow control window. Note that, just because an
+increment of a given size was received *does not* mean that that much more data
+can be sent: remember that both the connection and stream flow control windows
+constrain how much data can be sent.
+
+As a result, when a :class:`WindowUpdated <h2.events.WindowUpdated>` event
+fires with a non-zero stream ID, and the user has more data to send on that
+stream, the user should call :meth:`local_flow_control_window
+<h2.connection.H2Connection.local_flow_control_window>` to check if there
+really is more room to send data on that stream.
+
+When a :class:`WindowUpdated <h2.events.WindowUpdated>` event fires with a
+stream ID of ``0``, that may have unblocked *all* streams that are currently
+blocked. The user should use :meth:`local_flow_control_window
+<h2.connection.H2Connection.local_flow_control_window>` to check all blocked
+streams to see if more data is available.
+
+Auto Flow Control
+~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 2.5.0
+
+In most cases, there is no advantage for users in managing their own flow
+control strategies. While particular high performance or specific-use-case
+applications may gain value from directly controlling the emission of
+``WINDOW_UPDATE`` frames, the average application can use a
+lowest-common-denominator strategy to emit those frames. As of version 2.5.0,
+hyper-h2 now provides this automatic strategy for users, if they want to use
+it.
+
+This automatic strategy is built around a single method:
+:meth:`acknowledge_received_data
+<h2.connection.H2Connection.acknowledge_received_data>`. This method
+flags to the connection object that your application has dealt with a certain
+number of flow controlled bytes, and that the window should be incremented in
+some way. Whenever your application has "processed" some received bytes, this
+method should be called to signal that they have been processed.
+
+The key difference between this method and :meth:`increment_flow_control_window
+<h2.connection.H2Connection.increment_flow_control_window>` is that the method
+:meth:`acknowledge_received_data
+<h2.connection.H2Connection.acknowledge_received_data>` does not guarantee that
+it will emit a ``WINDOW_UPDATE`` frame, and if it does it will not necessarily
+emit them for *only* the stream or *only* the frame. Instead, the
+``WINDOW_UPDATE`` frames will be *coalesced*: they will be emitted only when
+a certain number of bytes have been freed up.
+
+For most applications, this method should be preferred to the manual flow
+control mechanism.
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/api.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/api.rst
new file mode 100644
index 0000000000..a46f8cce7d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/api.rst
@@ -0,0 +1,169 @@
+Hyper-h2 API
+============
+
+This document details the API of Hyper-h2.
+
+Semantic Versioning
+-------------------
+
+Hyper-h2 follows semantic versioning for its public API. Please note that the
+guarantees of semantic versioning apply only to the API that is *documented
+here*. Simply because a method or data field is not prefaced by an underscore
+does not make it part of Hyper-h2's public API. Anything not documented here is
+subject to change at any time.
+
+Connection
+----------
+
+.. autoclass:: h2.connection.H2Connection
+ :members:
+ :exclude-members: inbound_flow_control_window
+
+
+Configuration
+-------------
+
+.. autoclass:: h2.config.H2Configuration
+ :members:
+
+
+.. _h2-events-api:
+
+Events
+------
+
+.. autoclass:: h2.events.RequestReceived
+ :members:
+
+.. autoclass:: h2.events.ResponseReceived
+ :members:
+
+.. autoclass:: h2.events.TrailersReceived
+ :members:
+
+.. autoclass:: h2.events.InformationalResponseReceived
+ :members:
+
+.. autoclass:: h2.events.DataReceived
+ :members:
+
+.. autoclass:: h2.events.WindowUpdated
+ :members:
+
+.. autoclass:: h2.events.RemoteSettingsChanged
+ :members:
+
+.. autoclass:: h2.events.PingReceived
+ :members:
+
+.. autoclass:: h2.events.PingAckReceived
+ :members:
+
+.. autoclass:: h2.events.StreamEnded
+ :members:
+
+.. autoclass:: h2.events.StreamReset
+ :members:
+
+.. autoclass:: h2.events.PushedStreamReceived
+ :members:
+
+.. autoclass:: h2.events.SettingsAcknowledged
+ :members:
+
+.. autoclass:: h2.events.PriorityUpdated
+ :members:
+
+.. autoclass:: h2.events.ConnectionTerminated
+ :members:
+
+.. autoclass:: h2.events.AlternativeServiceAvailable
+ :members:
+
+.. autoclass:: h2.events.UnknownFrameReceived
+ :members:
+
+
+Exceptions
+----------
+
+.. autoclass:: h2.exceptions.H2Error
+ :members:
+
+.. autoclass:: h2.exceptions.NoSuchStreamError
+ :show-inheritance:
+ :members:
+
+.. autoclass:: h2.exceptions.StreamClosedError
+ :show-inheritance:
+ :members:
+
+.. autoclass:: h2.exceptions.RFC1122Error
+ :show-inheritance:
+ :members:
+
+
+Protocol Errors
+~~~~~~~~~~~~~~~
+
+.. autoclass:: h2.exceptions.ProtocolError
+ :show-inheritance:
+ :members:
+
+.. autoclass:: h2.exceptions.FrameTooLargeError
+ :show-inheritance:
+ :members:
+
+.. autoclass:: h2.exceptions.FrameDataMissingError
+ :show-inheritance:
+ :members:
+
+.. autoclass:: h2.exceptions.TooManyStreamsError
+ :show-inheritance:
+ :members:
+
+.. autoclass:: h2.exceptions.FlowControlError
+ :show-inheritance:
+ :members:
+
+.. autoclass:: h2.exceptions.StreamIDTooLowError
+ :show-inheritance:
+ :members:
+
+.. autoclass:: h2.exceptions.InvalidSettingsValueError
+ :members:
+
+.. autoclass:: h2.exceptions.NoAvailableStreamIDError
+ :show-inheritance:
+ :members:
+
+.. autoclass:: h2.exceptions.InvalidBodyLengthError
+ :show-inheritance:
+ :members:
+
+.. autoclass:: h2.exceptions.UnsupportedFrameError
+ :members:
+
+.. autoclass:: h2.exceptions.DenialOfServiceError
+ :show-inheritance:
+ :members:
+
+
+HTTP/2 Error Codes
+------------------
+
+.. automodule:: h2.errors
+ :members:
+
+
+Settings
+--------
+
+.. autoclass:: h2.settings.SettingCodes
+ :members:
+
+.. autoclass:: h2.settings.Settings
+ :inherited-members:
+
+.. autoclass:: h2.settings.ChangedSetting
+ :members:
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/asyncio-example.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/asyncio-example.rst
new file mode 100644
index 0000000000..d3afbfd051
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/asyncio-example.rst
@@ -0,0 +1,17 @@
+Asyncio Example Server
+======================
+
+This example is a basic HTTP/2 server written using `asyncio`_, using some
+functionality that was introduced in Python 3.5. This server represents
+basically just the same JSON-headers-returning server that was built in the
+:doc:`basic-usage` document.
+
+This example demonstrates some basic asyncio techniques.
+
+.. literalinclude:: ../../examples/asyncio/asyncio-server.py
+ :language: python
+ :linenos:
+ :encoding: utf-8
+
+
+.. _asyncio: https://docs.python.org/3/library/asyncio.html
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/basic-usage.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/basic-usage.rst
new file mode 100644
index 0000000000..b9aab6c6cf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/basic-usage.rst
@@ -0,0 +1,746 @@
+Getting Started: Writing Your Own HTTP/2 Server
+===============================================
+
+This document explains how to get started writing fully-fledged HTTP/2
+implementations using Hyper-h2 as the underlying protocol stack. It covers the
+basic concepts you need to understand, and talks you through writing a very
+simple HTTP/2 server.
+
+This document assumes you're moderately familiar with writing Python, and have
+*some* understanding of how computer networks work. If you don't, you'll find
+it a lot easier if you get some understanding of those concepts first and then
+return to this documentation.
+
+
+.. _h2-connection-basic:
+
+Connections
+-----------
+
+Hyper-h2's core object is the
+:class:`H2Connection <h2.connection.H2Connection>` object. This object is an
+abstract representation of the state of a single HTTP/2 connection, and holds
+all the important protocol state. When using Hyper-h2, this object will be the
+first thing you create and the object that does most of the heavy lifting.
+
+The interface to this object is relatively simple. For sending data, you
+call the object with methods indicating what actions you want to perform: for
+example, you may want to send headers (you'd use the
+:meth:`send_headers <h2.connection.H2Connection.send_headers>` method), or
+send data (you'd use the
+:meth:`send_data <h2.connection.H2Connection.send_data>` method). After you've
+decided what actions you want to perform, you get some bytes out of the object
+that represent the HTTP/2-encoded representation of your actions, and send them
+out over the network however you see fit.
+
+When you receive data from the network, you pass that data in to the
+``H2Connection`` object, which returns a list of *events*.
+These events, covered in more detail later in :ref:`h2-events-basic`, define
+the set of actions the remote peer has performed on the connection, as
+represented by the HTTP/2-encoded data you just passed to the object.
+
+Thus, you end up with a simple loop (which you may recognise as a more-specific
+form of an `event loop`_):
+
+ 1. First, you perform some actions.
+ 2. You send the data created by performing those actions to the network.
+ 3. You read data from the network.
+ 4. You decode those into events.
+ 5. The events cause you to trigger some actions: go back to step 1.
+
+Of course, HTTP/2 is more complex than that, but in the very simplest case you
+can write a fairly effective HTTP/2 tool using just that kind of loop. Later in
+this document, we'll do just that.
+
+Some important subtleties of ``H2Connection`` objects are covered in
+:doc:`advanced-usage`: see :ref:`h2-connection-advanced` for more information.
+However, one subtlety should be covered, and that is this: Hyper-h2's
+``H2Connection`` object doesn't do I/O. Let's talk briefly about why.
+
+I/O
+~~~
+
+Any useful HTTP/2 tool eventually needs to do I/O. This is because it's not
+very useful to be able to speak to other computers using a protocol like HTTP/2
+unless you actually *speak* to them sometimes.
+
+However, doing I/O is not a trivial thing: there are lots of different ways to
+do it, and once you choose a way to do it your code usually won't work well
+with the approaches you *didn't* choose.
+
+While there are lots of different ways to do I/O, when it comes down to it
+all HTTP/2 implementations transform bytes received into events, and events
+into bytes to send. So there's no reason to have lots of different versions of
+this core protocol code: one for Twisted, one for gevent, one for threading,
+and one for synchronous code.
+
+This is why we said at the top that Hyper-h2 is a *HTTP/2 Protocol Stack*, not
+a *fully-fledged implementation*. Hyper-h2 knows how to transform bytes into
+events and back, but that's it. The I/O and smarts might be different, but
+the core HTTP/2 logic is the same: that's what Hyper-h2 provides.
+
+Not doing I/O makes Hyper-h2 general, and also relatively simple. It has an
+easy-to-understand performance envelope, it's easy to test (and as a result
+easy to get correct behaviour out of), and it behaves in a reproducible way.
+These are all great traits to have in a library that is doing something quite
+complex.
+
+This document will talk you through how to build a relatively simple HTTP/2
+implementation using Hyper-h2, to give you an understanding of where it fits in
+your software.
+
+
+.. _h2-events-basic:
+
+Events
+------
+
+When writing a HTTP/2 implementation it's important to know what the remote
+peer is doing: if you didn't care, writing networked programs would be a lot
+easier!
+
+Hyper-h2 encodes the actions of the remote peer in the form of *events*. When
+you receive data from the remote peer and pass it into your ``H2Connection``
+object (see :ref:`h2-connection-basic`), the ``H2Connection`` returns a list
+of objects, each one representing a single event that has occurred. Each
+event refers to a single action the remote peer has taken.
+
+Some events are fairly high-level, referring to things that are more general
+than HTTP/2: for example, the
+:class:`RequestReceived <h2.events.RequestReceived>` event is a general HTTP
+concept, not just a HTTP/2 one. Other events are extremely HTTP/2-specific:
+for example, :class:`PushedStreamReceived <h2.events.PushedStreamReceived>`
+refers to Server Push, a very HTTP/2-specific concept.
+
+The reason these events exist is that Hyper-h2 is intended to be very general.
+This means that, in many cases, Hyper-h2 does not know exactly what to do in
+response to an event. Your code will need to handle these events, and make
+decisions about what to do. That's the major role of any HTTP/2 implementation
+built on top of Hyper-h2.
+
+A full list of events is available in :ref:`h2-events-api`. For the purposes
+of this example, we will handle only a small set of events.
+
+
+Writing Your Server
+-------------------
+
+Armed with the knowledge you just obtained, we're going to write a very simple
+HTTP/2 web server. The goal of this server is to write a server that can handle
+a HTTP GET, and that returns the headers sent by the client, encoded in JSON.
+Basically, something a lot like `httpbin.org/get`_. Nothing fancy, but this is
+a good way to get a handle on how you should interact with Hyper-h2.
+
+For the sake of simplicity, we're going to write this using the Python standard
+library, in Python 3. In reality, you'll probably want to use an asynchronous
+framework of some kind: see the `examples directory`_ in the repository for
+some examples of how you'd do that.
+
+Before we start, create a new file called ``h2server.py``: we'll use that as
+our workspace. Additionally, you should install Hyper-h2: follow the
+instructions in :doc:`installation`.
+
+Step 1: Sockets
+~~~~~~~~~~~~~~~
+
+To begin with, we need to make sure we can listen for incoming data and send it
+back. To do that, we need to use the `standard library's socket module`_. For
+now we're going to skip doing TLS: if you want to reach your server from your
+web browser, though, you'll need to add TLS and some other function. Consider
+looking at our examples in our `examples directory`_ instead.
+
+Let's begin. First, open up ``h2server.py``. We need to import the socket
+module and start listening for connections.
+
+This is not a socket tutorial, so we're not going to dive too deeply into how
+this works. If you want more detail about sockets, there are lots of good
+tutorials on the web that you should investigate.
+
+When you want to listen for incoming connections, the you need to *bind* an
+address first. So let's do that. Try setting up your file to look like this:
+
+.. code-block:: python
+
+ import socket
+
+ sock = socket.socket()
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind(('0.0.0.0', 8080))
+ sock.listen(5)
+
+ while True:
+ print(sock.accept())
+
+In a shell window, execute this program (``python h2server.py``). Then, open
+another shell and run ``curl http://localhost:8080/``. In the first shell, you
+should see something like this:
+
+.. code-block:: console
+
+ $ python h2server.py
+ (<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 58800)>, ('127.0.0.1', 58800))
+
+Run that ``curl`` command a few more times. You should see a few more similar
+lines appear. Note that the ``curl`` command itself will exit with an error.
+That's fine: it happens because we didn't send any data.
+
+Now go ahead and stop the server running by hitting Ctrl+C in the first shell.
+You should see a ``KeyboardInterrupt`` error take the process down.
+
+What's the program above doing? Well, first it creates a
+:func:`socket <python:socket.socket>` object. This socket is then *bound* to
+a specific address: ``('0.0.0.0', 8080)``. This is a special address: it means
+that this socket should be listening for any traffic to TCP port 8080. Don't
+worry about the call to ``setsockopt``: it just makes sure you can run this
+program repeatedly.
+
+We then loop forever calling the :meth:`accept <python:socket.socket.accept>`
+method on the socket. The accept method blocks until someone attempts to
+connect to our TCP port: when they do, it returns a tuple: the first element is
+a new socket object, the second element is a tuple of the address the new
+connection is from. You can see this in the output from our ``h2server.py``
+script.
+
+At this point, we have a script that can accept inbound connections. This is a
+good start! Let's start getting HTTP/2 involved.
+
+
+Step 2: Add a H2Connection
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Now that we can listen for socket information, we want to prepare our HTTP/2
+connection object and start handing it data. For now, let's just see what
+happens as we feed it data.
+
+To make HTTP/2 connections, we need a tool that knows how to speak HTTP/2.
+Most versions of curl in the wild don't, so let's install a Python tool. In
+your Python environment, run ``pip install hyper``. This will install a Python
+command-line HTTP/2 tool called ``hyper``. To confirm that it works, try
+running this command and verifying that the output looks similar to the one
+shown below:
+
+.. code-block:: console
+
+ $ hyper GET https://nghttp2.org/httpbin/get
+ {'args': {},
+ 'headers': {'Host': 'nghttp2.org'},
+ 'origin': '10.0.0.2',
+ 'url': 'https://nghttp2.org/httpbin/get'}
+
+Assuming it works, you're now ready to start sending HTTP/2 data.
+
+Back in our ``h2server.py`` script, we're going to want to start handling data.
+Let's add a function that takes a socket returned from ``accept``, and reads
+data from it. Let's call that function ``handle``. That function should create
+a :class:`H2Connection <h2.connection.H2Connection>` object and then loop on
+the socket, reading data and passing it to the connection.
+
+To read data from a socket we need to call ``recv``. The ``recv`` function
+takes a number as its argument, which is the *maximum* amount of data to be
+returned from a single call (note that ``recv`` will return as soon as any data
+is available, even if that amount is vastly less than the number you passed to
+it). For the purposes of writing this kind of software the specific value is
+not enormously useful, but should not be overly large. For that reason, when
+you're unsure, a number like 4096 or 65535 is a good bet. We'll use 65535 for
+this example.
+
+The function should look something like this:
+
+.. code-block:: python
+
+ import h2.connection
+ import h2.config
+
+ def handle(sock):
+ config = h2.config.H2Configuration(client_side=False)
+ conn = h2.connection.H2Connection(config=config)
+
+ while True:
+ data = sock.recv(65535)
+ print(conn.receive_data(data))
+
+Let's update our main loop so that it passes data on to our new data handling
+function. Your ``h2server.py`` should end up looking a like this:
+
+.. code-block:: python
+
+ import socket
+
+ import h2.connection
+ import h2.config
+
+ def handle(sock):
+ config = h2.config.H2Configuration(client_side=False)
+ conn = h2.connection.H2Connection(config=config)
+
+ while True:
+ data = sock.recv(65535)
+ if not data:
+ break
+
+ print(conn.receive_data(data))
+
+
+ sock = socket.socket()
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind(('0.0.0.0', 8080))
+ sock.listen(5)
+
+ while True:
+ handle(sock.accept()[0])
+
+Running that in one shell, in your other shell you can run
+``hyper --h2 GET http://localhost:8080/``. That shell should hang, and you
+should then see the following output from your ``h2server.py`` shell:
+
+.. code-block:: console
+
+ $ python h2server.py
+ [<h2.events.RemoteSettingsChanged object at 0x10c4ee390>]
+
+You'll then need to kill ``hyper`` and ``h2server.py`` with Ctrl+C. Feel free
+to do this a few times, to see how things behave.
+
+So, what did we see here? When the connection was opened, we used the
+:meth:`recv <python:socket.socket.recv>` method to read some data from the
+socket, in a loop. We then passed that data to the connection object, which
+returned us a single event object:
+:class:`RemoteSettingsChanged <h2.events.RemoteSettingsChanged>`.
+
+But what we didn't see was anything else. So it seems like all ``hyper`` did
+was change its settings, but nothing else. If you look at the other ``hyper``
+window, you'll notice that it hangs for a while and then eventually fails with
+a socket timeout. It was waiting for something: what?
+
+Well, it turns out that at the start of a connection, both sides need to send
+a bit of data, called "the HTTP/2 preamble". We don't need to get into too much
+detail here, but basically both sides need to send a single block of HTTP/2
+data that tells the other side what their settings are. ``hyper`` did that,
+but we didn't.
+
+Let's do that next.
+
+
+Step 3: Sending the Preamble
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Hyper-h2 makes doing connection setup really easy. All you need to do is call
+the
+:meth:`initiate_connection <h2.connection.H2Connection.initiate_connection>`
+method, and then send the corresponding data. Let's update our ``handle``
+function to do just that:
+
+.. code-block:: python
+
+ def handle(sock):
+ config = h2.config.H2Configuration(client_side=False)
+ conn = h2.connection.H2Connection(config=config)
+ conn.initiate_connection()
+ sock.sendall(conn.data_to_send())
+
+ while True:
+ data = sock.recv(65535)
+ print(conn.receive_data(data))
+
+
+The big change here is the call to ``initiate_connection``, but there's another
+new method in there:
+:meth:`data_to_send <h2.connection.H2Connection.data_to_send>`.
+
+When you make function calls on your ``H2Connection`` object, these will often
+want to cause HTTP/2 data to be written out to the network. But Hyper-h2
+doesn't do any I/O, so it can't do that itself. Instead, it writes it to an
+internal buffer. You can retrieve data from this buffer using the
+``data_to_send`` method. There are some subtleties about that method, but we
+don't need to worry about them right now: all we need to do is make sure we're
+sending whatever data is outstanding.
+
+Your ``h2server.py`` script should now look like this:
+
+.. code-block:: python
+
+ import socket
+
+ import h2.connection
+ import h2.config
+
+ def handle(sock):
+ config = h2.config.H2Configuration(client_side=False)
+ conn = h2.connection.H2Connection(config=config)
+ conn.initiate_connection()
+ sock.sendall(conn.data_to_send())
+
+ while True:
+ data = sock.recv(65535)
+ if not data:
+ break
+
+ print(conn.receive_data(data))
+
+
+ sock = socket.socket()
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind(('0.0.0.0', 8080))
+ sock.listen(5)
+
+ while True:
+ handle(sock.accept()[0])
+
+
+With this change made, rerun your ``h2server.py`` script and hit it with the
+same ``hyper`` command: ``hyper --h2 GET http://localhost:8080/``. The
+``hyper`` command still hangs, but this time we get a bit more output from our
+``h2server.py`` script:
+
+.. code-block:: console
+
+ $ python h2server.py
+ [<h2.events.RemoteSettingsChanged object at 0x10292d390>]
+ [<h2.events.SettingsAcknowledged object at 0x102b3a160>]
+ [<h2.events.RequestReceived object at 0x102b3a3c8>, <h2.events.StreamEnded object at 0x102b3a400>]
+
+So, what's happening?
+
+The first thing to note is that we're going around our loop more than once now.
+First, we receive some data that triggers a
+:class:`RemoteSettingsChanged <h2.events.RemoteSettingsChanged>` event.
+Then, we get some more data that triggers a
+:class:`SettingsAcknowledged <h2.events.SettingsAcknowledged>` event.
+Finally, even more data that triggers *two* events:
+:class:`RequestReceived <h2.events.RequestReceived>` and
+:class:`StreamEnded <h2.events.StreamEnded>`.
+
+So, what's happening is that ``hyper`` is telling us about its settings,
+acknowledging ours, and then sending us a request. Then it ends a *stream*,
+which is a HTTP/2 communications channel that holds a request and response
+pair.
+
+A stream isn't done until it's either *reset* or both sides *close* it:
+in this sense it's bi-directional. So what the ``StreamEnded`` event tells us
+is that ``hyper`` is closing its half of the stream: it won't send us any more
+data on that stream. That means the request is done.
+
+So why is ``hyper`` hanging? Well, we haven't sent a response yet: let's do
+that.
+
+
+Step 4: Handling Events
+~~~~~~~~~~~~~~~~~~~~~~~
+
+What we want to do is send a response when we receive a request. Happily, we
+get an event when we receive a request, so we can use that to be our signal.
+
+Let's define a new function that sends a response. For now, this response can
+just be a little bit of data that prints "it works!".
+
+The function should take the ``H2Connection`` object, and the event that
+signaled the request. Let's define it.
+
+.. code-block:: python
+
+ def send_response(conn, event):
+ stream_id = event.stream_id
+ conn.send_headers(
+ stream_id=stream_id,
+ headers=[
+ (':status', '200'),
+ ('server', 'basic-h2-server/1.0')
+ ],
+ )
+ conn.send_data(
+ stream_id=stream_id,
+ data=b'it works!',
+ end_stream=True
+ )
+
+So while this is only a short function, there's quite a lot going on here we
+need to unpack. Firstly, what's a stream ID? Earlier we discussed streams
+briefly, to say that they're a bi-directional communications channel that holds
+a request and response pair. Part of what makes HTTP/2 great is that there can
+be lots of streams going on at once, sending and receiving different requests
+and responses. To identify each stream, we use a *stream ID*. These are unique
+across the lifetime of a connection, and they go in ascending order.
+
+Most ``H2Connection`` functions take a stream ID: they require you to actively
+tell the connection which one to use. In this case, as a simple server, we will
+never need to choose a stream ID ourselves: the client will always choose one
+for us. That means we'll always be able to get the one we need off the events
+that fire.
+
+Next, we send some *headers*. In HTTP/2, a response is made up of some set of
+headers, and optionally some data. The headers have to come first: if you're a
+client then you'll be sending *request* headers, but in our case these headers
+are our *response* headers.
+
+Mostly these aren't very exciting, but you'll notice once special header in
+there: ``:status``. This is a HTTP/2-specific header, and it's used to hold the
+HTTP status code that used to go at the top of a HTTP response. Here, we're
+saying the response is ``200 OK``, which is successful.
+
+To send headers in Hyper-h2, you use the
+:meth:`send_headers <h2.connection.H2Connection.send_headers>` function.
+
+Next, we want to send the body data. To do that, we use the
+:meth:`send_data <h2.connection.H2Connection.send_data>` function. This also
+takes a stream ID. Note that the data is binary: Hyper-h2 does not work with
+unicode strings, so you *must* pass bytestrings to the ``H2Connection``. The
+one exception is headers: Hyper-h2 will automatically encode those into UTF-8.
+
+The last thing to note is that on our call to ``send_data``, we set
+``end_stream`` to ``True``. This tells Hyper-h2 (and the remote peer) that
+we're done with sending data: the response is over. Because we know that
+``hyper`` will have ended its side of the stream, when we end ours the stream
+will be totally done with.
+
+We're nearly ready to go with this: we just need to plumb this function in.
+Let's amend our ``handle`` function again:
+
+.. code-block:: python
+
+ import h2.events
+ import h2.config
+
+ def handle(sock):
+ config = h2.config.H2Configuration(client_side=False)
+ conn = h2.connection.H2Connection(config=config)
+ conn.initiate_connection()
+ sock.sendall(conn.data_to_send())
+
+ while True:
+ data = sock.recv(65535)
+ if not data:
+ break
+
+ events = conn.receive_data(data)
+ for event in events:
+ if isinstance(event, h2.events.RequestReceived):
+ send_response(conn, event)
+
+ data_to_send = conn.data_to_send()
+ if data_to_send:
+ sock.sendall(data_to_send)
+
+The changes here are all at the end. Now, when we receive some events, we
+look through them for the ``RequestReceived`` event. If we find it, we make
+sure we send a response.
+
+Then, at the bottom of the loop we check whether we have any data to send, and
+if we do, we send it. Then, we repeat again.
+
+With these changes, your ``h2server.py`` file should look like this:
+
+.. code-block:: python
+
+ import socket
+
+ import h2.connection
+ import h2.events
+ import h2.config
+
+ def send_response(conn, event):
+ stream_id = event.stream_id
+ conn.send_headers(
+ stream_id=stream_id,
+ headers=[
+ (':status', '200'),
+ ('server', 'basic-h2-server/1.0')
+ ],
+ )
+ conn.send_data(
+ stream_id=stream_id,
+ data=b'it works!',
+ end_stream=True
+ )
+
+ def handle(sock):
+ config = h2.config.H2Configuration(client_side=False)
+ conn = h2.connection.H2Connection(config=config)
+ conn.initiate_connection()
+ sock.sendall(conn.data_to_send())
+
+ while True:
+ data = sock.recv(65535)
+ if not data:
+ break
+
+ events = conn.receive_data(data)
+ for event in events:
+ if isinstance(event, h2.events.RequestReceived):
+ send_response(conn, event)
+
+ data_to_send = conn.data_to_send()
+ if data_to_send:
+ sock.sendall(data_to_send)
+
+
+ sock = socket.socket()
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind(('0.0.0.0', 8080))
+ sock.listen(5)
+
+ while True:
+ handle(sock.accept()[0])
+
+Alright. Let's run this, and then run our ``hyper`` command again.
+
+This time, nothing is printed from our server, and the ``hyper`` side prints
+``it works!``. Success! Try running it a few more times, and we can see that
+not only does it work the first time, it works the other times too!
+
+We can speak HTTP/2! Let's add the final step: returning the JSON-encoded
+request headers.
+
+Step 5: Returning Headers
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If we want to return the request headers in JSON, the first thing we have to do
+is find them. Handily, if you check the documentation for
+:class:`RequestReceived <h2.events.RequestReceived>` you'll find that this
+event carries, in addition to the stream ID, the request headers.
+
+This means we can make a really simple change to our ``send_response``
+function to take those headers and encode them as a JSON object. Let's do that:
+
+.. code-block:: python
+
+ import json
+
+ def send_response(conn, event):
+ stream_id = event.stream_id
+ response_data = json.dumps(dict(event.headers)).encode('utf-8')
+
+ conn.send_headers(
+ stream_id=stream_id,
+ headers=[
+ (':status', '200'),
+ ('server', 'basic-h2-server/1.0'),
+ ('content-length', str(len(response_data))),
+ ('content-type', 'application/json'),
+ ],
+ )
+ conn.send_data(
+ stream_id=stream_id,
+ data=response_data,
+ end_stream=True
+ )
+
+This is a really simple change, but it's all we need to do: a few extra headers
+and the JSON dump, but that's it.
+
+Section 6: Bringing It All Together
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This should be all we need!
+
+Let's take all the work we just did and throw that into our ``h2server.py``
+file, which should now look like this:
+
+.. code-block:: python
+
+ import json
+ import socket
+
+ import h2.connection
+ import h2.events
+ import h2.config
+
+ def send_response(conn, event):
+ stream_id = event.stream_id
+ response_data = json.dumps(dict(event.headers)).encode('utf-8')
+
+ conn.send_headers(
+ stream_id=stream_id,
+ headers=[
+ (':status', '200'),
+ ('server', 'basic-h2-server/1.0'),
+ ('content-length', str(len(response_data))),
+ ('content-type', 'application/json'),
+ ],
+ )
+ conn.send_data(
+ stream_id=stream_id,
+ data=response_data,
+ end_stream=True
+ )
+
+ def handle(sock):
+ config = h2.config.H2Configuration(client_side=False)
+ conn = h2.connection.H2Connection(config=config)
+ conn.initiate_connection()
+ sock.sendall(conn.data_to_send())
+
+ while True:
+ data = sock.recv(65535)
+ if not data:
+ break
+
+ events = conn.receive_data(data)
+ for event in events:
+ if isinstance(event, h2.events.RequestReceived):
+ send_response(conn, event)
+
+ data_to_send = conn.data_to_send()
+ if data_to_send:
+ sock.sendall(data_to_send)
+
+
+ sock = socket.socket()
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind(('0.0.0.0', 8080))
+ sock.listen(5)
+
+ while True:
+ handle(sock.accept()[0])
+
+Now, execute ``h2server.py`` and then point ``hyper`` at it again. You should
+see something like the following output from ``hyper``:
+
+.. code-block:: console
+
+ $ hyper --h2 GET http://localhost:8080/
+ {":scheme": "http", ":authority": "localhost", ":method": "GET", ":path": "/"}
+
+Here you can see the HTTP/2 request 'special headers' that ``hyper`` sends.
+These are similar to the ``:status`` header we have to send on our response:
+they encode important parts of the HTTP request in a clearly-defined way. If
+you were writing a client stack using Hyper-h2, you'd need to make sure you
+were sending those headers.
+
+Congratulations!
+~~~~~~~~~~~~~~~~
+
+Congratulations! You've written your first HTTP/2 server! If you want to extend
+it, there are a few directions you could investigate:
+
+- We didn't handle a few events that we saw were being raised: you could add
+ some methods to handle those appropriately.
+- Right now our server is single threaded, so it can only handle one client at
+ a time. Consider rewriting this server to use threads, or writing this
+ server again using your favourite asynchronous programming framework.
+
+ If you plan to use threads, you should know that a ``H2Connection`` object is
+ deliberately not thread-safe. As a possible design pattern, consider creating
+ threads and passing the sockets returned by ``accept`` to those threads, and
+ then letting those threads create their own ``H2Connection`` objects.
+- Take a look at some of our long-form code examples in :doc:`examples`.
+- Alternatively, try playing around with our examples in our repository's
+ `examples directory`_. These examples are a bit more fully-featured, and can
+ be reached from your web browser. Try adjusting what they do, or adding new
+ features to them!
+- You may want to make this server reachable from your web browser. To do that,
+ you'll need to add proper TLS support to your server. This can be tricky, and
+ in many cases requires `PyOpenSSL`_ in addition to the other libraries you
+ have installed. Check the `Eventlet example`_ to see what PyOpenSSL code is
+ required to TLS-ify your server.
+
+
+
+.. _event loop: https://en.wikipedia.org/wiki/Event_loop
+.. _httpbin.org/get: https://httpbin.org/get
+.. _examples directory: https://github.com/python-hyper/hyper-h2/tree/master/examples
+.. _standard library's socket module: https://docs.python.org/3.5/library/socket.html
+.. _Application Layer Protocol Negotiation: https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation
+.. _get your certificate here: https://raw.githubusercontent.com/python-hyper/hyper-h2/master/examples/twisted/server.crt
+.. _get your private key here: https://raw.githubusercontent.com/python-hyper/hyper-h2/master/examples/twisted/server.key
+.. _PyOpenSSL: http://pyopenssl.readthedocs.org/
+.. _Eventlet example: https://github.com/python-hyper/hyper-h2/blob/master/examples/eventlet/eventlet-server.py
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/conf.py b/testing/web-platform/tests/tools/third_party/h2/docs/source/conf.py
new file mode 100644
index 0000000000..a15a214634
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/conf.py
@@ -0,0 +1,270 @@
+# -*- coding: utf-8 -*-
+#
+# hyper-h2 documentation build configuration file, created by
+# sphinx-quickstart on Thu Sep 17 10:06:02 2015.
+#
+# 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
+import 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.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 = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'hyper-h2'
+copyright = u'2015, Cory Benfield'
+
+# 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.
+version = '3.2.0'
+# The full version, including alpha/beta/rc tags.
+release = '3.2.0'
+
+# 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 = []
+
+# 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 = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = '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']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# 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 = 'hyper-h2doc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ ('index', 'hyper-h2.tex', u'hyper-h2 Documentation',
+ u'Cory Benfield', '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
+
+# 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', 'hyper-h2', u'hyper-h2 Documentation',
+ [u'Cory Benfield'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'hyper-h2', u'hyper-h2 Documentation',
+ u'Cory Benfield', 'hyper-h2', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {
+ 'python': ('https://docs.python.org/3.5/', None),
+ 'hpack': ('https://python-hyper.org/hpack/en/stable/', None),
+ 'pyopenssl': ('https://pyopenssl.readthedocs.org/en/latest/', None),
+}
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/contributors.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/contributors.rst
new file mode 100644
index 0000000000..d84c791f6d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/contributors.rst
@@ -0,0 +1,4 @@
+Contributors
+============
+
+.. include:: ../../CONTRIBUTORS.rst
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/curio-example.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/curio-example.rst
new file mode 100644
index 0000000000..7cdb61608a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/curio-example.rst
@@ -0,0 +1,17 @@
+Curio Example Server
+====================
+
+This example is a basic HTTP/2 server written using `curio`_, David Beazley's
+example of how to build a concurrent networking framework using Python 3.5's
+new ``async``/``await`` syntax.
+
+This example is notable for demonstrating the correct use of HTTP/2 flow
+control with Hyper-h2. It is also a good example of the brand new syntax.
+
+.. literalinclude:: ../../examples/curio/curio-server.py
+ :language: python
+ :linenos:
+ :encoding: utf-8
+
+
+.. _curio: https://curio.readthedocs.org/en/latest/
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/eventlet-example.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/eventlet-example.rst
new file mode 100644
index 0000000000..a23b5e248f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/eventlet-example.rst
@@ -0,0 +1,19 @@
+Eventlet Example Server
+=======================
+
+This example is a basic HTTP/2 server written using the `eventlet`_ concurrent
+networking framework. This example is notable for demonstrating how to
+configure `PyOpenSSL`_, which `eventlet`_ uses for its TLS layer.
+
+In terms of HTTP/2 functionality, this example is very simple: it returns the
+request headers as a JSON document to the caller. It does not obey HTTP/2 flow
+control, which is a flaw, but it is otherwise functional.
+
+.. literalinclude:: ../../examples/eventlet/eventlet-server.py
+ :language: python
+ :linenos:
+ :encoding: utf-8
+
+
+.. _eventlet: http://eventlet.net/
+.. _PyOpenSSL: https://pyopenssl.readthedocs.org/en/stable/
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/examples.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/examples.rst
new file mode 100644
index 0000000000..ed7c5037bb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/examples.rst
@@ -0,0 +1,28 @@
+Code Examples
+=============
+
+This section of the documentation contains long-form code examples. These are
+intended as references for developers that would like to get an understanding
+of how Hyper-h2 fits in with various Python I/O frameworks.
+
+Example Servers
+---------------
+
+.. toctree::
+ :maxdepth: 2
+
+ asyncio-example
+ twisted-example
+ eventlet-example
+ curio-example
+ tornado-example
+ wsgi-example
+
+Example Clients
+---------------
+
+.. toctree::
+ :maxdepth: 2
+
+ twisted-head-example
+ twisted-post-example
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/index.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/index.rst
new file mode 100644
index 0000000000..be85dec7c3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/index.rst
@@ -0,0 +1,41 @@
+.. hyper-h2 documentation master file, created by
+ sphinx-quickstart on Thu Sep 17 10:06:02 2015.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Hyper-h2: A pure-Python HTTP/2 protocol stack
+=============================================
+
+Hyper-h2 is a HTTP/2 protocol stack, written entirely in Python. The goal of
+Hyper-h2 is to be a common HTTP/2 stack for the Python ecosystem,
+usable in all programs regardless of concurrency model or environment.
+
+To achieve this, Hyper-h2 is entirely self-contained: it does no I/O of any
+kind, leaving that up to a wrapper library to control. This ensures that it can
+seamlessly work in all kinds of environments, from single-threaded code to
+Twisted.
+
+Its goal is to be 100% compatible with RFC 7540, implementing a complete HTTP/2
+protocol stack build on a set of finite state machines. Its secondary goals are
+to be fast, clear, and efficient.
+
+For usage examples, see :doc:`basic-usage` or consult the examples in the
+repository.
+
+Contents
+--------
+
+.. toctree::
+ :maxdepth: 2
+
+ installation
+ basic-usage
+ negotiating-http2
+ examples
+ advanced-usage
+ low-level
+ api
+ testimonials
+ release-process
+ release-notes
+ contributors \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/installation.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/installation.rst
new file mode 100644
index 0000000000..683085f97b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/installation.rst
@@ -0,0 +1,18 @@
+Installation
+============
+
+Hyper-h2 is a pure-python project. This means installing it is extremely
+simple. To get the latest release from PyPI, simply run:
+
+.. code-block:: console
+
+ $ pip install h2
+
+Alternatively, feel free to download one of the release tarballs from
+`our GitHub page`_, extract it to your favourite directory, and then run
+
+.. code-block:: console
+
+ $ python setup.py install
+
+.. _our GitHub page: https://github.com/python-hyper/hyper-h2
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/low-level.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/low-level.rst
new file mode 100644
index 0000000000..824ba8e6ea
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/low-level.rst
@@ -0,0 +1,159 @@
+Low-Level Details
+=================
+
+.. warning:: This section of the documentation covers low-level implementation
+ details of hyper-h2. This is most likely to be of use to hyper-h2
+ developers and to other HTTP/2 implementers, though it could well
+ be of general interest. Feel free to peruse it, but if you're
+ looking for information about how to *use* hyper-h2 you should
+ consider looking elsewhere.
+
+State Machines
+--------------
+
+hyper-h2 is fundamentally built on top of a pair of interacting Finite State
+Machines. One of these FSMs manages per-connection state, and another manages
+per-stream state. Almost without exception (see :ref:`priority` for more
+details) every single frame is unconditionally translated into events for
+both state machines and those state machines are turned.
+
+The advantages of a system such as this is that the finite state machines can
+very densely encode the kinds of things that are allowed at any particular
+moment in a HTTP/2 connection. However, most importantly, almost all protocols
+are defined *in terms* of finite state machines: that is, protocol descriptions
+can be reduced to a number of states and inputs. That makes FSMs a very natural
+tool for implementing protocol stacks.
+
+Indeed, most protocol implementations that do not explicitly encode a finite
+state machine almost always *implicitly* encode a finite state machine, by
+using classes with a bunch of variables that amount to state-tracking
+variables, or by using the call-stack as an implicit state tracking mechanism.
+While these methods are not immediately problematic, they tend to lack
+*explicitness*, and can lead to subtle bugs of the form "protocol action X is
+incorrectly allowed in state Y".
+
+For these reasons, we have implemented two *explicit* finite state machines.
+These machines aim to encode most of the protocol-specific state, in particular
+regarding what frame is allowed at what time. This target goal is sometimes not
+achieved: in particular, as of this writing the *stream* FSM contains a number
+of other state variables that really ought to be rolled into the state machine
+itself in the form of new states, or in the form of a transformation of the
+FSM to use state *vectors* instead of state *scalars*.
+
+The following sections contain some implementers notes on these FSMs.
+
+Connection State Machine
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+The "outer" state machine, the first one that is encountered when sending or
+receiving data, is the connection state machine. This state machine tracks
+whole-connection state.
+
+This state machine is primarily intended to forbid certain actions on the basis
+of whether the implementation is acting as a client or a server. For example,
+clients are not permitted to send ``PUSH_PROMISE`` frames: this state machine
+forbids that by refusing to define a valid transition from the ``CLIENT_OPEN``
+state for the ``SEND_PUSH_PROMISE`` event.
+
+Otherwise, this particular state machine triggers no side-effects. It has a
+very coarse, high-level, functionality.
+
+A visual representation of this FSM is shown below:
+
+.. image:: _static/h2.connection.H2ConnectionStateMachine.dot.png
+ :alt: A visual representation of the connection FSM.
+ :target: _static/h2.connection.H2ConnectionStateMachine.dot.png
+
+
+.. _stream-state-machine:
+
+Stream State Machine
+~~~~~~~~~~~~~~~~~~~~
+
+Once the connection state machine has been spun, any frame that belongs to a
+stream is passed to the stream state machine for its given stream. Each stream
+has its own instance of the state machine, but all of them share the transition
+table: this is because the table itself is sufficiently large that having it be
+per-instance would be a ridiculous memory overhead.
+
+Unlike the connection state machine, the stream state machine is quite complex.
+This is because it frequently needs to encode some side-effects. The most
+common side-effect is emitting a ``RST_STREAM`` frame when an error is
+encountered: the need to do this means that far more transitions need to be
+encoded than for the connection state machine.
+
+Many of the side-effect functions in this state machine also raise
+:class:`ProtocolError <h2.exceptions.ProtocolError>` exceptions. This is almost
+always done on the basis of an extra state variable, which is an annoying code
+smell: it should always be possible for the state machine itself to police
+these using explicit state management. A future refactor will hopefully address
+this problem by making these additional state variables part of the state
+definitions in the FSM, which will lead to an expansion of the number of states
+but a greater degree of simplicity in understanding and tracking what is going
+on in the state machine.
+
+The other action taken by the side-effect functions defined here is returning
+:ref:`events <h2-events-basic>`. Most of these events are returned directly to
+the user, and reflect the specific state transition that has taken place, but
+some of the events are purely *internal*: they are used to signal to other
+parts of the hyper-h2 codebase what action has been taken.
+
+The major use of the internal events functionality at this time is for
+validating header blocks: there are different rules for request headers than
+there are for response headers, and different rules again for trailers. The
+internal events are used to determine *exactly what* kind of data the user is
+attempting to send, and using that information to do the correct kind of
+validation. This approach ensures that the final source of truth about what's
+happening at the protocol level lives inside the FSM, which is an extremely
+important design principle we want to continue to enshrine in hyper-h2.
+
+A visual representation of this FSM is shown below:
+
+.. image:: _static/h2.stream.H2StreamStateMachine.dot.png
+ :alt: A visual representation of the stream FSM.
+ :target: _static/h2.stream.H2StreamStateMachine.dot.png
+
+
+.. _priority:
+
+Priority
+~~~~~~~~
+
+In the :ref:`stream-state-machine` section we said that any frame that belongs
+to a stream is passed to the stream state machine. This turns out to be not
+quite true.
+
+Specifically, while ``PRIORITY`` frames are technically sent on a given stream
+(that is, `RFC 7540 Section 6.3`_ defines them as "always identifying a stream"
+and forbids the use of stream ID ``0`` for them), in practice they are almost
+completely exempt from the usual stream FSM behaviour. Specifically, the RFC
+has this to say:
+
+ The ``PRIORITY`` frame can be sent on a stream in any state, though it
+ cannot be sent between consecutive frames that comprise a single
+ header block (Section 4.3).
+
+Given that the consecutive header block requirement is handled outside of the
+FSMs, this section of the RFC essentially means that there is *never* a
+situation where it is invalid to receive a ``PRIORITY`` frame. This means that
+including it in the stream FSM would require that we allow ``SEND_PRIORITY``
+and ``RECV_PRIORITY`` in all states.
+
+This is not a totally onerous task: however, another key note is that hyper-h2
+uses the *absence* of a stream state machine to flag a closed stream. This is
+primarily for memory conservation reasons: if we needed to keep around an FSM
+for every stream we've ever seen, that would cause long-lived HTTP/2
+connections to consume increasingly large amounts of memory. On top of this,
+it would require us to create a stream FSM each time we received a ``PRIORITY``
+frame for a given stream, giving a malicious peer an easy route to force a
+hyper-h2 user to allocate nearly unbounded amounts of memory.
+
+For this reason, hyper-h2 circumvents the stream FSM entirely for ``PRIORITY``
+frames. Instead, these frames are treated as being connection-level frames that
+*just happen* to identify a specific stream. They do not bring streams into
+being, or in any sense interact with hyper-h2's view of streams. Their stream
+details are treated as strictly metadata that hyper-h2 is not interested in
+beyond being able to parse it out.
+
+
+.. _RFC 7540 Section 6.3: https://tools.ietf.org/html/rfc7540#section-6.3
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/negotiating-http2.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/negotiating-http2.rst
new file mode 100644
index 0000000000..20d58a71f1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/negotiating-http2.rst
@@ -0,0 +1,103 @@
+Negotiating HTTP/2
+==================
+
+`RFC 7540`_ specifies three methods of negotiating HTTP/2 connections. This document outlines how to use Hyper-h2 with each one.
+
+.. _starting-alpn:
+
+HTTPS URLs (ALPN)
+-------------------------
+
+Starting HTTP/2 for HTTPS URLs is outlined in `RFC 7540 Section 3.3`_. In this case, the client and server use a TLS extension to negotiate HTTP/2: `ALPN`_. How to use ALPN is currently not covered in this document: please consult the documentation for either the :mod:`ssl module <python:ssl>` in the standard library, or the :mod:`PyOpenSSL <pyopenssl:OpenSSL.SSL>` third-party modules, for more on this topic.
+
+This method is the simplest to use once the TLS connection is established. To use it with Hyper-h2, after you've established the connection and confirmed that HTTP/2 has been negotiated with `ALPN`_, create a :class:`H2Connection <h2.connection.H2Connection>` object and call :meth:`H2Connection.initiate_connection <h2.connection.H2Connection.initiate_connection>`. This will ensure that the appropriate preamble data is placed in the data buffer. You should then immediately send the data returned by :meth:`H2Connection.data_to_send <h2.connection.H2Connection.data_to_send>` on your TLS connection.
+
+At this point, you're free to use all the HTTP/2 functionality provided by Hyper-h2.
+
+.. note::
+ Although Hyper-h2 is not concerned with negotiating protocol versions, it is important to note that support for `ALPN`_ is not available in the standard library of Python versions < 2.7.9.
+ As a consequence, clients may encounter various errors due to protocol versions mismatch.
+
+Server Setup Example
+~~~~~~~~~~~~~~~~~~~~
+
+This example uses the APIs as defined in Python 3.5. If you are using an older version of Python you may not have access to the APIs used here. As noted above, please consult the documentation for the :mod:`ssl module <python:ssl>` to confirm.
+
+.. literalinclude:: ../../examples/fragments/server_https_setup_fragment.py
+ :language: python
+ :linenos:
+ :encoding: utf-8
+
+
+Client Setup Example
+~~~~~~~~~~~~~~~~~~~~
+
+The client example is very similar to the server example above. The :class:`SSLContext <python:ssl.SSLContext>` object requires some minor changes, as does the :class:`H2Connection <h2.connection.H2Connection>`, but the bulk of the code is the same.
+
+.. literalinclude:: ../../examples/fragments/client_https_setup_fragment.py
+ :language: python
+ :linenos:
+ :encoding: utf-8
+
+
+.. _starting-upgrade:
+
+HTTP URLs (Upgrade)
+-------------------
+
+Starting HTTP/2 for HTTP URLs is outlined in `RFC 7540 Section 3.2`_. In this case, the client and server use the HTTP Upgrade mechanism originally described in `RFC 7230 Section 6.7`_. The client sends its initial HTTP/1.1 request with two extra headers. The first is ``Upgrade: h2c``, which requests upgrade to cleartext HTTP/2. The second is a ``HTTP2-Settings`` header, which contains a specially formatted string that encodes a HTTP/2 Settings frame.
+
+To do this with Hyper-h2 you have two slightly different flows: one for clients, one for servers.
+
+Clients
+~~~~~~~
+
+For a client, when sending the first request you should manually add your ``Upgrade`` header. You should then create a :class:`H2Connection <h2.connection.H2Connection>` object and call :meth:`H2Connection.initiate_upgrade_connection <h2.connection.H2Connection.initiate_upgrade_connection>` with no arguments. This method will return a bytestring to use as the value of your ``HTTP2-Settings`` header.
+
+If the server returns a ``101`` status code, it has accepted the upgrade, and you should immediately send the data returned by :meth:`H2Connection.data_to_send <h2.connection.H2Connection.data_to_send>`. Now you should consume the entire ``101`` header block. All data after the ``101`` header block is HTTP/2 data that should be fed directly to :meth:`H2Connection.receive_data <h2.connection.H2Connection.receive_data>` and handled as normal with Hyper-h2.
+
+If the server does not return a ``101`` status code then it is not upgrading. Continue with HTTP/1.1 as normal: you may throw away your :class:`H2Connection <h2.connection.H2Connection>` object, as it is of no further use.
+
+The server will respond to your original request in HTTP/2. Please pay attention to the events received from Hyper-h2, as they will define the server's response.
+
+Client Example
+^^^^^^^^^^^^^^
+
+The code below demonstrates how to handle a plaintext upgrade from the perspective of the client. For the purposes of keeping the example code as simple and generic as possible it uses the synchronous socket API that comes with the Python standard library: if you want to use asynchronous I/O, you will need to translate this code to the appropriate idiom.
+
+.. literalinclude:: ../../examples/fragments/client_upgrade_fragment.py
+ :language: python
+ :linenos:
+ :encoding: utf-8
+
+
+Servers
+~~~~~~~
+
+If the first request you receive on a connection from the client contains an ``Upgrade`` header with the ``h2c`` token in it, and you're willing to upgrade, you should create a :class:`H2Connection <h2.connection.H2Connection>` object and call :meth:`H2Connection.initiate_upgrade_connection <h2.connection.H2Connection.initiate_upgrade_connection>` with the value of the ``HTTP2-Settings`` header (as a bytestring) as the only argument.
+
+Then, you should send back a ``101`` response that contains ``h2c`` in the ``Upgrade`` header. That response will inform the client that you're switching to HTTP/2. Then, you should immediately send the data that is returned to you by :meth:`H2Connection.data_to_send <h2.connection.H2Connection.data_to_send>` on the connection: this is a necessary part of the HTTP/2 upgrade process.
+
+At this point, you may now respond to the original HTTP/1.1 request in HTTP/2 by calling the appropriate methods on the :class:`H2Connection <h2.connection.H2Connection>` object. No further HTTP/1.1 may be sent on this connection: from this point onward, all data sent by you and the client will be HTTP/2 data.
+
+Server Example
+^^^^^^^^^^^^^^
+
+The code below demonstrates how to handle a plaintext upgrade from the perspective of the server. For the purposes of keeping the example code as simple and generic as possible it uses the synchronous socket API that comes with the Python standard library: if you want to use asynchronous I/O, you will need to translate this code to the appropriate idiom.
+
+.. literalinclude:: ../../examples/fragments/server_upgrade_fragment.py
+ :language: python
+ :linenos:
+ :encoding: utf-8
+
+
+Prior Knowledge
+---------------
+
+It's possible that you as a client know that a particular server supports HTTP/2, and that you do not need to perform any of the negotiations described above. In that case, you may follow the steps in :ref:`starting-alpn`, ignoring all references to ALPN: there's no need to perform the upgrade dance described in :ref:`starting-upgrade`.
+
+.. _RFC 7540: https://tools.ietf.org/html/rfc7540
+.. _RFC 7540 Section 3.2: https://tools.ietf.org/html/rfc7540#section-3.2
+.. _RFC 7540 Section 3.3: https://tools.ietf.org/html/rfc7540#section-3.3
+.. _ALPN: https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation
+.. _RFC 7230 Section 6.7: https://tools.ietf.org/html/rfc7230#section-6.7
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/release-notes.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/release-notes.rst
new file mode 100644
index 0000000000..fa425f1fef
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/release-notes.rst
@@ -0,0 +1,101 @@
+Release Notes
+=============
+
+This document contains release notes for Hyper-h2. In addition to the
+:ref:`detailed-release-notes` found at the bottom of this document, this
+document also includes a high-level prose overview of each major release after
+1.0.0.
+
+High Level Notes
+----------------
+
+3.0.0: 24 March 2017
+~~~~~~~~~~~~~~~~~~~~
+
+The Hyper-h2 team and the Hyper project are delighted to announce the release
+of Hyper-h2 version 3.0.0! Unlike the really notable 2.0.0 release, this
+release is proportionally quite small: however, it has the effect of removing a
+lot of cruft and complexity that has built up in the codebase over the lifetime
+of the v2 release series.
+
+This release was motivated primarily by discovering that applications that
+attempted to use both HTTP/1.1 and HTTP/2 using hyper-h2 would encounter
+problems with cookies, because hyper-h2 did not join together cookie headers as
+required by RFC 7540. Normally adding such behaviour would be a non-breaking
+change, but we previously had no flags to prevent normalization of received
+HTTP headers.
+
+Because it makes no sense for the cookie to be split *by default*, we needed to
+add a controlling flag and set it to true. The breaking nature of this change
+is very subtle, and it's possible most users would never notice, but
+nevertheless it *is* a breaking change and we need to treat it as such.
+
+Happily, we can take this opportunity to finalise a bunch of deprecations we'd
+made over the past year. The v2 release series was long-lived and successful,
+having had a series of releases across the past year-and-a-bit, and the Hyper
+team are very proud of it. However, it's time to open a new chapter, and remove
+the deprecated code.
+
+The past year has been enormously productive for the Hyper team. A total of 30
+v2 releases were made, an enormous amount of work. A good number of people have
+made their first contribution in this time, more than I can thank reasonably
+without taking up an unreasonable amount of space in this document, so instead
+I invite you to check out `our awesome contributor list`_.
+
+We're looking forward to the next chapter in hyper-h2: it's been a fun ride so
+far, and we hope even more of you come along and join in the fun over the next
+year!
+
+.. _our awesome contributor list: https://github.com/python-hyper/hyper-h2/graphs/contributors
+
+
+2.0.0: 25 January 2016
+~~~~~~~~~~~~~~~~~~~~~~
+
+The Hyper-h2 team and the Hyper project are delighted to announce the release
+of Hyper-h2 version 2.0.0! This is an enormous release that contains a gigantic
+collection of new features and fixes, with the goal of making it easier than
+ever to use Hyper-h2 to build a compliant HTTP/2 server or client.
+
+An enormous chunk of this work has been focused on tighter enforcement of
+restrictions in RFC 7540, ensuring that we correctly police the actions of
+remote peers, and error appropriately when those peers violate the
+specification. Several of these constitute breaking changes, because data that
+was previously received and handled without obvious error now raises
+``ProtocolError`` exceptions and causes the connection to be terminated.
+
+Additionally, the public API was cleaned up and had several helper methods that
+had been inavertently exposed removed from the public API. The team wants to
+stress that while Hyper-h2 follows semantic versioning, the guarantees of
+semver apply only to the public API as documented in :doc:`api`. Reducing the
+surface area of these APIs makes it easier for us to continue to ensure that
+the guarantees of semver are respected on our public API.
+
+We also attempted to clear up some of the warts that had appeared in the API,
+and add features that are helpful for implementing HTTP/2 endpoints. For
+example, the :class:`H2Connection <h2.connection.H2Connection>` object now
+exposes a method for generating the next stream ID that your client or server
+can use to initiate a connection (:meth:`get_next_available_stream_id
+<h2.connection.H2Connection.get_next_available_stream_id>`). We also removed
+some needless return values that were guaranteed to return empty lists, which
+were an attempt to make a forward-looking guarantee that was entirely unneeded.
+
+Altogether, this has been an extremely productive period for Hyper-h2, and a
+lot of great work has been done by the community. To that end, we'd also like
+to extend a great thankyou to those contributors who made their first contribution
+to the project between release 1.0.0 and 2.0.0. Many thanks to:
+`Thomas Kriechbaumer`_, `Alex Chan`_, `Maximilian Hils`_, and `Glyph`_. For a
+full historical list of contributors, see :doc:`contributors`.
+
+We're looking forward to the next few months of Python HTTP/2 work, and hoping
+that you'll find lots of excellent HTTP/2 applications to build with Hyper-h2!
+
+
+.. _Thomas Kriechbaumer: https://github.com/Kriechi
+.. _Alex Chan: https://github.com/alexwlchan
+.. _Maximilian Hils: https://github.com/mhils
+.. _Glyph: https://github.com/glyph
+
+
+.. _detailed-release-notes:
+.. include:: ../../HISTORY.rst
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/release-process.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/release-process.rst
new file mode 100644
index 0000000000..e7b46064d5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/release-process.rst
@@ -0,0 +1,56 @@
+Release Process
+===============
+
+Because of Hyper-h2's place at the bottom of the dependency tree, it is
+extremely important that the project maintains a diligent release schedule.
+This document outlines our process for managing releases.
+
+Versioning
+----------
+
+Hyper-h2 follows `semantic versioning`_ of its public API when it comes to
+numbering releases. The public API of Hyper-h2 is strictly limited to the
+entities listed in the :doc:`api` documentation: anything not mentioned in that
+document is not considered part of the public API and is not covered by the
+versioning guarantees given by semantic versioning.
+
+Maintenance
+-----------
+
+Hyper-h2 has the notion of a "release series", given by a major and minor
+version number: for example, there is the 2.1 release series. When each minor
+release is made and a release series is born, a branch is made off the release
+tag: for example, for the 2.1 release series, the 2.1.X branch.
+
+All changes merged into the master branch will be evaluated for whether they
+can be considered 'bugfixes' only (that is, they do not affect the public API).
+If they can, they will also be cherry-picked back to all active maintenance
+branches that require the bugfix. If the bugfix is not necessary, because the
+branch in question is unaffected by that bug, the bugfix will not be
+backported.
+
+Supported Release Series'
+-------------------------
+
+The developers of Hyper-h2 commit to supporting the following release series:
+
+- The most recent, as identified by the first two numbers in the highest
+ version currently released.
+- The immediately prior release series.
+
+The only exception to this policy is that no release series earlier than the
+2.1 series will be supported. In this context, "supported" means that they will
+continue to receive bugfix releases.
+
+For releases other than the ones identified above, no support is guaranteed.
+The developers may *choose* to support such a release series, but they do not
+promise to.
+
+The exception here is for security vulnerabilities. If a security vulnerability
+is identified in an out-of-support release series, the developers will do their
+best to patch it and issue an emergency release. For more information, see
+`our security documentation`_.
+
+
+.. _semantic versioning: http://semver.org/
+.. _our security documentation: http://python-hyper.org/en/latest/security.html
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/testimonials.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/testimonials.rst
new file mode 100644
index 0000000000..ec32fb9572
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/testimonials.rst
@@ -0,0 +1,9 @@
+Testimonials
+============
+
+Glyph Lefkowitz
+~~~~~~~~~~~~~~~
+
+Frankly, Hyper-h2 is almost SURREAL in how well-factored and decoupled the implementation is from I/O. If libraries in the Python ecosystem looked like this generally, Twisted would be a much better platform than it is. (Frankly, most of Twisted's _own_ protocol implementations should aspire to such cleanliness.)
+
+(`Source <https://twistedmatrix.com/pipermail/twisted-python/2015-November/029894.html>`_)
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/tornado-example.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/tornado-example.rst
new file mode 100644
index 0000000000..c7a80713a1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/tornado-example.rst
@@ -0,0 +1,16 @@
+Tornado Example Server
+======================
+
+This example is a basic HTTP/2 server written using the `Tornado`_ asynchronous
+networking library.
+
+The server returns the request headers as a JSON document to the caller, just
+like the example from the :doc:`basic-usage` document.
+
+.. literalinclude:: ../../examples/tornado/tornado-server.py
+ :language: python
+ :linenos:
+ :encoding: utf-8
+
+
+.. _Tornado: http://www.tornadoweb.org/
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/twisted-example.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/twisted-example.rst
new file mode 100644
index 0000000000..10d111628b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/twisted-example.rst
@@ -0,0 +1,18 @@
+Twisted Example Server
+======================
+
+This example is a basic HTTP/2 server written for the `Twisted`_ asynchronous
+networking framework. This is a relatively fleshed out example, and in
+particular it makes sure to obey HTTP/2 flow control rules.
+
+This server differs from some of the other example servers by serving files,
+rather than simply sending JSON responses. This makes the example lengthier,
+but also brings it closer to a real-world use-case.
+
+.. literalinclude:: ../../examples/twisted/twisted-server.py
+ :language: python
+ :linenos:
+ :encoding: utf-8
+
+
+.. _Twisted: https://twistedmatrix.com/ \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/twisted-head-example.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/twisted-head-example.rst
new file mode 100644
index 0000000000..df93b144e7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/twisted-head-example.rst
@@ -0,0 +1,17 @@
+Twisted Example Client: Head Requests
+=====================================
+
+This example is a basic HTTP/2 client written for the `Twisted`_ asynchronous
+networking framework.
+
+This client is fairly simple: it makes a hard-coded HEAD request to
+nghttp2.org/httpbin/ and prints out the response data. Its purpose is to demonstrate
+how to write a very basic HTTP/2 client implementation.
+
+.. literalinclude:: ../../examples/twisted/head_request.py
+ :language: python
+ :linenos:
+ :encoding: utf-8
+
+
+.. _Twisted: https://twistedmatrix.com/
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/twisted-post-example.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/twisted-post-example.rst
new file mode 100644
index 0000000000..7e3aba41a3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/twisted-post-example.rst
@@ -0,0 +1,18 @@
+Twisted Example Client: Post Requests
+=====================================
+
+This example is a basic HTTP/2 client written for the `Twisted`_ asynchronous
+networking framework.
+
+This client is fairly simple: it makes a hard-coded POST request to
+nghttp2.org/httpbin/post and prints out the response data, sending a file that is provided
+on the command line or the script itself. Its purpose is to demonstrate how to
+write a HTTP/2 client implementation that handles flow control.
+
+.. literalinclude:: ../../examples/twisted/post_request.py
+ :language: python
+ :linenos:
+ :encoding: utf-8
+
+
+.. _Twisted: https://twistedmatrix.com/
diff --git a/testing/web-platform/tests/tools/third_party/h2/docs/source/wsgi-example.rst b/testing/web-platform/tests/tools/third_party/h2/docs/source/wsgi-example.rst
new file mode 100644
index 0000000000..82513899b2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/docs/source/wsgi-example.rst
@@ -0,0 +1,23 @@
+Example HTTP/2-only WSGI Server
+===============================
+
+This example is a more complex HTTP/2 server that acts as a WSGI server,
+passing data to an arbitrary WSGI application. This example is written using
+`asyncio`_. The server supports most of PEP-3333, and so could in principle be
+used as a production WSGI server: however, that's *not recommended* as certain
+shortcuts have been taken to ensure ease of implementation and understanding.
+
+The main advantages of this example are:
+
+1. It properly demonstrates HTTP/2 flow control management.
+2. It demonstrates how to plug hyper-h2 into a larger, more complex
+ application.
+
+
+.. literalinclude:: ../../examples/asyncio/wsgi-server.py
+ :language: python
+ :linenos:
+ :encoding: utf-8
+
+
+.. _asyncio: https://docs.python.org/3/library/asyncio.html
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/asyncio/asyncio-server.py b/testing/web-platform/tests/tools/third_party/h2/examples/asyncio/asyncio-server.py
new file mode 100644
index 0000000000..278774644b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/asyncio/asyncio-server.py
@@ -0,0 +1,210 @@
+# -*- coding: utf-8 -*-
+"""
+asyncio-server.py
+~~~~~~~~~~~~~~~~~
+
+A fully-functional HTTP/2 server using asyncio. Requires Python 3.5+.
+
+This example demonstrates handling requests with bodies, as well as handling
+those without. In particular, it demonstrates the fact that DataReceived may
+be called multiple times, and that applications must handle that possibility.
+"""
+import asyncio
+import io
+import json
+import ssl
+import collections
+from typing import List, Tuple
+
+from h2.config import H2Configuration
+from h2.connection import H2Connection
+from h2.events import (
+ ConnectionTerminated, DataReceived, RemoteSettingsChanged,
+ RequestReceived, StreamEnded, StreamReset, WindowUpdated
+)
+from h2.errors import ErrorCodes
+from h2.exceptions import ProtocolError, StreamClosedError
+from h2.settings import SettingCodes
+
+
+RequestData = collections.namedtuple('RequestData', ['headers', 'data'])
+
+
+class H2Protocol(asyncio.Protocol):
+ def __init__(self):
+ config = H2Configuration(client_side=False, header_encoding='utf-8')
+ self.conn = H2Connection(config=config)
+ self.transport = None
+ self.stream_data = {}
+ self.flow_control_futures = {}
+
+ def connection_made(self, transport: asyncio.Transport):
+ self.transport = transport
+ self.conn.initiate_connection()
+ self.transport.write(self.conn.data_to_send())
+
+ def connection_lost(self, exc):
+ for future in self.flow_control_futures.values():
+ future.cancel()
+ self.flow_control_futures = {}
+
+ def data_received(self, data: bytes):
+ try:
+ events = self.conn.receive_data(data)
+ except ProtocolError as e:
+ self.transport.write(self.conn.data_to_send())
+ self.transport.close()
+ else:
+ self.transport.write(self.conn.data_to_send())
+ for event in events:
+ if isinstance(event, RequestReceived):
+ self.request_received(event.headers, event.stream_id)
+ elif isinstance(event, DataReceived):
+ self.receive_data(event.data, event.stream_id)
+ elif isinstance(event, StreamEnded):
+ self.stream_complete(event.stream_id)
+ elif isinstance(event, ConnectionTerminated):
+ self.transport.close()
+ elif isinstance(event, StreamReset):
+ self.stream_reset(event.stream_id)
+ elif isinstance(event, WindowUpdated):
+ self.window_updated(event.stream_id, event.delta)
+ elif isinstance(event, RemoteSettingsChanged):
+ if SettingCodes.INITIAL_WINDOW_SIZE in event.changed_settings:
+ self.window_updated(None, 0)
+
+ self.transport.write(self.conn.data_to_send())
+
+ def request_received(self, headers: List[Tuple[str, str]], stream_id: int):
+ headers = collections.OrderedDict(headers)
+ method = headers[':method']
+
+ # Store off the request data.
+ request_data = RequestData(headers, io.BytesIO())
+ self.stream_data[stream_id] = request_data
+
+ def stream_complete(self, stream_id: int):
+ """
+ When a stream is complete, we can send our response.
+ """
+ try:
+ request_data = self.stream_data[stream_id]
+ except KeyError:
+ # Just return, we probably 405'd this already
+ return
+
+ headers = request_data.headers
+ body = request_data.data.getvalue().decode('utf-8')
+
+ data = json.dumps(
+ {"headers": headers, "body": body}, indent=4
+ ).encode("utf8")
+
+ response_headers = (
+ (':status', '200'),
+ ('content-type', 'application/json'),
+ ('content-length', str(len(data))),
+ ('server', 'asyncio-h2'),
+ )
+ self.conn.send_headers(stream_id, response_headers)
+ asyncio.ensure_future(self.send_data(data, stream_id))
+
+ def receive_data(self, data: bytes, stream_id: int):
+ """
+ We've received some data on a stream. If that stream is one we're
+ expecting data on, save it off. Otherwise, reset the stream.
+ """
+ try:
+ stream_data = self.stream_data[stream_id]
+ except KeyError:
+ self.conn.reset_stream(
+ stream_id, error_code=ErrorCodes.PROTOCOL_ERROR
+ )
+ else:
+ stream_data.data.write(data)
+
+ def stream_reset(self, stream_id):
+ """
+ A stream reset was sent. Stop sending data.
+ """
+ if stream_id in self.flow_control_futures:
+ future = self.flow_control_futures.pop(stream_id)
+ future.cancel()
+
+ async def send_data(self, data, stream_id):
+ """
+ Send data according to the flow control rules.
+ """
+ while data:
+ while self.conn.local_flow_control_window(stream_id) < 1:
+ try:
+ await self.wait_for_flow_control(stream_id)
+ except asyncio.CancelledError:
+ return
+
+ chunk_size = min(
+ self.conn.local_flow_control_window(stream_id),
+ len(data),
+ self.conn.max_outbound_frame_size,
+ )
+
+ try:
+ self.conn.send_data(
+ stream_id,
+ data[:chunk_size],
+ end_stream=(chunk_size == len(data))
+ )
+ except (StreamClosedError, ProtocolError):
+ # The stream got closed and we didn't get told. We're done
+ # here.
+ break
+
+ self.transport.write(self.conn.data_to_send())
+ data = data[chunk_size:]
+
+ async def wait_for_flow_control(self, stream_id):
+ """
+ Waits for a Future that fires when the flow control window is opened.
+ """
+ f = asyncio.Future()
+ self.flow_control_futures[stream_id] = f
+ await f
+
+ def window_updated(self, stream_id, delta):
+ """
+ A window update frame was received. Unblock some number of flow control
+ Futures.
+ """
+ if stream_id and stream_id in self.flow_control_futures:
+ f = self.flow_control_futures.pop(stream_id)
+ f.set_result(delta)
+ elif not stream_id:
+ for f in self.flow_control_futures.values():
+ f.set_result(delta)
+
+ self.flow_control_futures = {}
+
+
+ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ssl_context.options |= (
+ ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION
+)
+ssl_context.load_cert_chain(certfile="cert.crt", keyfile="cert.key")
+ssl_context.set_alpn_protocols(["h2"])
+
+loop = asyncio.get_event_loop()
+# Each client connection will create a new protocol instance
+coro = loop.create_server(H2Protocol, '127.0.0.1', 8443, ssl=ssl_context)
+server = loop.run_until_complete(coro)
+
+# Serve requests until Ctrl+C is pressed
+print('Serving on {}'.format(server.sockets[0].getsockname()))
+try:
+ loop.run_forever()
+except KeyboardInterrupt:
+ pass
+
+# Close the server
+server.close()
+loop.run_until_complete(server.wait_closed())
+loop.close()
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/asyncio/cert.crt b/testing/web-platform/tests/tools/third_party/h2/examples/asyncio/cert.crt
new file mode 100644
index 0000000000..d6cf7d504d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/asyncio/cert.crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDhTCCAm2gAwIBAgIJAOrxh0dOYJLdMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNTA5MTkxNDE2
+NDRaFw0xNTEwMTkxNDE2NDRaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21l
+LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV
+BAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMqt
+A1iu8EN00FU0eBcBGlLVmNEgV7Jkbukra+kwS8j/U2y50QPGJc/FiIVDfuBqk5dL
+ACTNc6A/FQcXvWmOc5ixmC3QKKasMpuofqKz0V9C6irZdYXZ9rcsW0gHQIr989yd
+R+N1VbIlEVW/T9FJL3B2UD9GVIkUELzm47CSOWZvAxQUlsx8CUNuUCWqyZJoqTFN
+j0LeJDOWGCsug1Pkj0Q1x+jMVL6l6Zf6vMkLNOMsOsWsxUk+0L3tl/OzcTgUOCsw
+UzY59RIi6Rudrp0oaU8NuHr91yiSqPbKFlX10M9KwEEdnIpcxhND3dacrDycj3ux
+eWlqKync2vOFUkhwiaMCAwEAAaNQME4wHQYDVR0OBBYEFA0PN+PGoofZ+QIys2Jy
+1Zz94vBOMB8GA1UdIwQYMBaAFA0PN+PGoofZ+QIys2Jy1Zz94vBOMAwGA1UdEwQF
+MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEplethBoPpcP3EbR5Rz6snDDIcbtAJu
+Ngd0YZppGT+P0DYnPJva4vRG3bb84ZMSuppz5j67qD6DdWte8UXhK8BzWiHzwmQE
+QmbKyzzTMKQgTNFntpx5cgsSvTtrHpNYoMHzHOmyAOboNeM0DWiRXsYLkWTitLTN
+qbOpstwPubExbT9lPjLclntShT/lCupt+zsbnrR9YiqlYFY/fDzfAybZhrD5GMBY
+XdMPItwAc/sWvH31yztarjkLmld76AGCcO5r8cSR/cX98SicyfjOBbSco8GkjYNY
+582gTPkKGYpStuN7GNT5tZmxvMq935HRa2XZvlAIe8ufp8EHVoYiF3c=
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/asyncio/cert.key b/testing/web-platform/tests/tools/third_party/h2/examples/asyncio/cert.key
new file mode 100644
index 0000000000..bda69e836c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/asyncio/cert.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAyq0DWK7wQ3TQVTR4FwEaUtWY0SBXsmRu6Str6TBLyP9TbLnR
+A8Ylz8WIhUN+4GqTl0sAJM1zoD8VBxe9aY5zmLGYLdAopqwym6h+orPRX0LqKtl1
+hdn2tyxbSAdAiv3z3J1H43VVsiURVb9P0UkvcHZQP0ZUiRQQvObjsJI5Zm8DFBSW
+zHwJQ25QJarJkmipMU2PQt4kM5YYKy6DU+SPRDXH6MxUvqXpl/q8yQs04yw6xazF
+ST7Qve2X87NxOBQ4KzBTNjn1EiLpG52unShpTw24ev3XKJKo9soWVfXQz0rAQR2c
+ilzGE0Pd1pysPJyPe7F5aWorKdza84VSSHCJowIDAQABAoIBACp+nh4BB/VMz8Wd
+q7Q/EfLeQB1Q57JKpoqTBRwueSVai3ZXe4CMEi9/HkG6xiZtkiZ9njkZLq4hq9oB
+2z//kzMnwV2RsIRJxI6ohGy+wR51HD4BvEdlTPpY/Yabpqe92VyfSYxidKZWaU0O
+QMED1EODOw4ZQ+4928iPrJu//PMB4e7TFao0b9Fk/XLWtu5/tQZz9jsrlTi1zthh
+7n+oaGNhfTeIJJL4jrhTrKW1CLHXATtr9SJlfZ3wbMxQVeyj2wUlP1V0M6kBuhNj
+tbGbMpixD5iCNJ49Cm2PHg+wBOfS3ADGIpi3PcGw5mb8nB3N9eGBRPhLShAlq5Hi
+Lv4tyykCgYEA8u3b3xJ04pxWYN25ou/Sc8xzgDCK4XvDNdHVTuZDjLVA+VTVPzql
+lw7VvJArsx47MSPvsaX/+4hQXYtfnR7yJpx6QagvQ+z4ludnIZYrQwdUmb9pFL1s
+8UNj+3j9QFRPenIiIQ8qxxNIQ9w2HsVQ8scvc9CjYop/YYAPaQyHaL8CgYEA1ZSz
+CR4NcpfgRSILdhb1dLcyw5Qus1VOSAx3DYkhDkMiB8XZwgMdJjwehJo9yaqRCLE8
+Sw5znMnkfoZpu7+skrjK0FqmMpXMH9gIszHvFG8wSw/6+2HIWS19/wOu8dh95LuC
+0zurMk8rFqxgWMWF20afhgYrUz42cvUTo10FVB0CgYEAt7mW6W3PArfUSCxIwmb4
+VmXREKkl0ATHDYQl/Cb//YHzot467TgQll883QB4XF5HzBFurX9rSzO7/BN1e6I0
+52i+ubtWC9xD4fUetXMaQvZfUGxIL8xXgVxDWKQXfLiG54c8Mp6C7s6xf8kjEUCP
+yR1F0SSA/Pzb+8RbY0p7eocCgYA+1rs+SXtHZev0KyoYGnUpW+Uxqd17ofOgOxqj
+/t6c5Z+TjeCdtnDTGQkZlo/rT6XQWuUUaDIXxUbW+xEMzj4mBPyXBLS1WWFvVQ5q
+OpzO9E/PJeqAH6rkof/aEelc+oc/zvOU1o9uA+D3kMvgEm1psIOq2RHSMhGvDPA0
+NmAk+QKBgQCwd1681GagdIYSZUCBecnLtevXmIsJyDW2yR1NNcIe/ukcVQREMDvy
+5DDkhnGDgnV1D5gYcXb34g9vYvbfTnBMl/JXmMAAG1kIS+3pvHyN6f1poVe3yJV1
+yHVuvymnJxKnyaV0L3ntepVvV0vVNIkA3oauoUTLto6txBI+b/ImDA==
+-----END RSA PRIVATE KEY-----
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/asyncio/wsgi-server.py b/testing/web-platform/tests/tools/third_party/h2/examples/asyncio/wsgi-server.py
new file mode 100644
index 0000000000..9fdb2fa0fc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/asyncio/wsgi-server.py
@@ -0,0 +1,760 @@
+# -*- coding: utf-8 -*-
+"""
+asyncio-server.py
+~~~~~~~~~~~~~~~~~
+
+A fully-functional WSGI server, written using hyper-h2. Requires asyncio.
+
+To test it, try installing httpbin from pip (``pip install httpbin``) and then
+running the server (``python asyncio-server.py httpbin:app``).
+
+This server does not support HTTP/1.1: it is a HTTP/2-only WSGI server. The
+purpose of this code is to demonstrate how to integrate hyper-h2 into a more
+complex application, and to demonstrate several principles of concurrent
+programming.
+
+The architecture looks like this:
+
++---------------------------------+
+| 1x HTTP/2 Server Thread |
+| (running asyncio) |
++---------------------------------+
++---------------------------------+
+| N WSGI Application Threads |
+| (no asyncio) |
++---------------------------------+
+
+Essentially, we spin up an asyncio-based event loop in the main thread. This
+launches one HTTP/2 Protocol instance for each inbound connection, all of which
+will read and write data from within the main thread in an asynchronous manner.
+
+When each HTTP request comes in, the server will build the WSGI environment
+dictionary and create a ``Stream`` object. This object will hold the relevant
+state for the request/response pair and will act as the WSGI side of the logic.
+That object will then be passed to a background thread pool, and when a worker
+is available the WSGI logic will begin to be executed. This model ensures that
+the asyncio web server itself is never blocked by the WSGI application.
+
+The WSGI application and the HTTP/2 server communicate via an asyncio queue,
+together with locks and threading events. The locks themselves are implicit in
+asyncio's "call_soon_threadsafe", which allows for a background thread to
+register an action with the main asyncio thread. When the asyncio thread
+eventually takes the action in question it sets as threading event, signaling
+to the background thread that it is free to continue its work.
+
+To make the WSGI application work with flow control, there is a very important
+invariant that must be observed. Any WSGI action that would cause data to be
+emitted to the network MUST be accompanied by a threading Event that is not
+set until that data has been written to the transport. This ensures that the
+WSGI application *blocks* until the data is actually sent. The reason we
+require this invariant is that the HTTP/2 server may choose to re-order some
+data chunks for flow control reasons: that is, the application for stream X may
+have actually written its data first, but the server may elect to send the data
+for stream Y first. This means that it's vital that there not be *two* writes
+for stream X active at any one point or they may get reordered, which would be
+particularly terrible.
+
+Thus, the server must cooperate to ensure that each threading event only fires
+when the *complete* data for that event has been written to the asyncio
+transport. Any earlier will cause untold craziness.
+"""
+import asyncio
+import importlib
+import queue
+import ssl
+import sys
+import threading
+
+from h2.config import H2Configuration
+from h2.connection import H2Connection
+from h2.events import (
+ DataReceived, RequestReceived, WindowUpdated, StreamEnded, StreamReset
+)
+
+
+# Used to signal that a request has completed.
+#
+# This is a convenient way to do "in-band" signaling of stream completion
+# without doing anything so heavyweight as using a class. Essentially, we can
+# test identity against this empty object. In fact, this is so convenient that
+# we use this object for all streams, for data in both directions: in and out.
+END_DATA_SENTINEL = object()
+
+# The WSGI callable. Stored here so that the protocol instances can get hold
+# of the data.
+APPLICATION = None
+
+
+class H2Protocol(asyncio.Protocol):
+ def __init__(self):
+ config = H2Configuration(client_side=False, header_encoding='utf-8')
+
+ # Our server-side state machine.
+ self.conn = H2Connection(config=config)
+
+ # The backing transport.
+ self.transport = None
+
+ # A dictionary of ``Stream`` objects, keyed by their stream ID. This
+ # makes it easy to route data to the correct WSGI application instance.
+ self.streams = {}
+
+ # A queue of data emitted by WSGI applications that has not yet been
+ # sent. Each stream may only have one chunk of data in either this
+ # queue or the flow_controlled_data dictionary at any one time.
+ self._stream_data = asyncio.Queue()
+
+ # Data that has been pulled off the queue that is for a stream blocked
+ # behind flow control limitations. This is used to avoid spinning on
+ # _stream_data queue when a stream cannot have its data sent. Data that
+ # cannot be sent on the connection when it is popped off the queue gets
+ # placed here until the stream flow control window opens up again.
+ self._flow_controlled_data = {}
+
+ # A reference to the loop in which this protocol runs. This is needed
+ # to synchronise up with background threads.
+ self._loop = asyncio.get_event_loop()
+
+ # Any streams that have been remotely reset. We keep track of these to
+ # ensure that we don't emit data from a WSGI application whose stream
+ # has been cancelled.
+ self._reset_streams = set()
+
+ # Keep track of the loop sending task so we can kill it when the
+ # connection goes away.
+ self._send_loop_task = None
+
+ def connection_made(self, transport):
+ """
+ The connection has been made. Here we need to save off our transport,
+ do basic HTTP/2 connection setup, and then start our data writing
+ coroutine.
+ """
+ self.transport = transport
+ self.conn.initiate_connection()
+ self.transport.write(self.conn.data_to_send())
+ self._send_loop_task = self._loop.create_task(self.sending_loop())
+
+ def connection_lost(self, exc):
+ """
+ With the end of the connection, we just want to cancel our data sending
+ coroutine.
+ """
+ self._send_loop_task.cancel()
+
+ def data_received(self, data):
+ """
+ Process inbound data.
+ """
+ events = self.conn.receive_data(data)
+
+ for event in events:
+ if isinstance(event, RequestReceived):
+ self.request_received(event)
+ elif isinstance(event, DataReceived):
+ self.data_frame_received(event)
+ elif isinstance(event, WindowUpdated):
+ self.window_opened(event)
+ elif isinstance(event, StreamEnded):
+ self.end_stream(event)
+ elif isinstance(event, StreamReset):
+ self.reset_stream(event)
+
+ outbound_data = self.conn.data_to_send()
+ if outbound_data:
+ self.transport.write(outbound_data)
+
+ def window_opened(self, event):
+ """
+ The flow control window got opened.
+
+ This is important because it's possible that we were unable to send
+ some WSGI data because the flow control window was too small. If that
+ happens, the sending_loop coroutine starts buffering data.
+
+ As the window gets opened, we need to unbuffer the data. We do that by
+ placing the data chunks back on the back of the send queue and letting
+ the sending loop take another shot at sending them.
+
+ This system only works because we require that each stream only have
+ *one* data chunk in the sending queue at any time. The threading events
+ force this invariant to remain true.
+ """
+ if event.stream_id:
+ # This is specific to a single stream.
+ if event.stream_id in self._flow_controlled_data:
+ self._stream_data.put_nowait(
+ self._flow_controlled_data.pop(event.stream_id)
+ )
+ else:
+ # This event is specific to the connection. Free up *all* the
+ # streams. This is a bit tricky, but we *must not* yield the flow
+ # of control here or it all goes wrong.
+ for data in self._flow_controlled_data.values():
+ self._stream_data.put_nowait(data)
+
+ self._flow_controlled_data = {}
+
+ @asyncio.coroutine
+ def sending_loop(self):
+ """
+ A call that loops forever, attempting to send data. This sending loop
+ contains most of the flow-control smarts of this class: it pulls data
+ off of the asyncio queue and then attempts to send it.
+
+ The difficulties here are all around flow control. Specifically, a
+ chunk of data may be too large to send. In this case, what will happen
+ is that this coroutine will attempt to send what it can and will then
+ store the unsent data locally. When a flow control event comes in that
+ data will be freed up and placed back onto the asyncio queue, causing
+ it to pop back up into the sending logic of this coroutine.
+
+ This method explicitly *does not* handle HTTP/2 priority. That adds an
+ extra layer of complexity to what is already a fairly complex method,
+ and we'll look at how to do it another time.
+
+ This coroutine explicitly *does not end*.
+ """
+ while True:
+ stream_id, data, event = yield from self._stream_data.get()
+
+ # If this stream got reset, just drop the data on the floor. Note
+ # that we need to reset the event here to make sure that
+ # application doesn't lock up.
+ if stream_id in self._reset_streams:
+ event.set()
+
+ # Check if the body is done. If it is, this is really easy! Again,
+ # we *must* set the event here or the application will lock up.
+ if data is END_DATA_SENTINEL:
+ self.conn.end_stream(stream_id)
+ self.transport.write(self.conn.data_to_send())
+ event.set()
+ continue
+
+ # We need to send data, but not to exceed the flow control window.
+ # For that reason, grab only the data that fits: we'll buffer the
+ # rest.
+ window_size = self.conn.local_flow_control_window(stream_id)
+ chunk_size = min(window_size, len(data))
+ data_to_send = data[:chunk_size]
+ data_to_buffer = data[chunk_size:]
+
+ if data_to_send:
+ # There's a maximum frame size we have to respect. Because we
+ # aren't paying any attention to priority here, we can quite
+ # safely just split this string up into chunks of max frame
+ # size and blast them out.
+ #
+ # In a *real* application you'd want to consider priority here.
+ max_size = self.conn.max_outbound_frame_size
+ chunks = (
+ data_to_send[x:x+max_size]
+ for x in range(0, len(data_to_send), max_size)
+ )
+ for chunk in chunks:
+ self.conn.send_data(stream_id, chunk)
+ self.transport.write(self.conn.data_to_send())
+
+ # If there's data left to buffer, we should do that. Put it in a
+ # dictionary and *don't set the event*: the app must not generate
+ # any more data until we got rid of all of this data.
+ if data_to_buffer:
+ self._flow_controlled_data[stream_id] = (
+ stream_id, data_to_buffer, event
+ )
+ else:
+ # We sent everything. We can let the WSGI app progress.
+ event.set()
+
+ def request_received(self, event):
+ """
+ A HTTP/2 request has been received. We need to invoke the WSGI
+ application in a background thread to handle it.
+ """
+ # First, we are going to want an object to hold all the relevant state
+ # for this request/response. For that, we have a stream object. We
+ # need to store the stream object somewhere reachable for when data
+ # arrives later.
+ s = Stream(event.stream_id, self)
+ self.streams[event.stream_id] = s
+
+ # Next, we need to build the WSGI environ dictionary.
+ environ = _build_environ_dict(event.headers, s)
+
+ # Finally, we want to throw these arguments out to a threadpool and
+ # let it run.
+ self._loop.run_in_executor(
+ None,
+ s.run_in_threadpool,
+ APPLICATION,
+ environ,
+ )
+
+ def data_frame_received(self, event):
+ """
+ Data has been received by WSGI server and needs to be dispatched to a
+ running application.
+
+ Note that the flow control window is not modified here. That's
+ deliberate: see Stream.__next__ for a longer discussion of why.
+ """
+ # Grab the stream in question from our dictionary and pass it on.
+ stream = self.streams[event.stream_id]
+ stream.receive_data(event.data, event.flow_controlled_length)
+
+ def end_stream(self, event):
+ """
+ The stream data is complete.
+ """
+ stream = self.streams[event.stream_id]
+ stream.request_complete()
+
+ def reset_stream(self, event):
+ """
+ A stream got forcefully reset.
+
+ This is a tricky thing to deal with because WSGI doesn't really have a
+ good notion for it. Essentially, you have to let the application run
+ until completion, but not actually let it send any data.
+
+ We do that by discarding any data we currently have for it, and then
+ marking the stream as reset to allow us to spot when that stream is
+ trying to send data and drop that data on the floor.
+
+ We then *also* signal the WSGI application that no more data is
+ incoming, to ensure that it does not attempt to do further reads of the
+ data.
+ """
+ if event.stream_id in self._flow_controlled_data:
+ del self._flow_controlled_data
+
+ self._reset_streams.add(event.stream_id)
+ self.end_stream(event)
+
+ def data_for_stream(self, stream_id, data):
+ """
+ Thread-safe method called from outside the main asyncio thread in order
+ to send data on behalf of a WSGI application.
+
+ Places data being written by a stream on an asyncio queue. Returns a
+ threading event that will fire when that data is sent.
+ """
+ event = threading.Event()
+ self._loop.call_soon_threadsafe(
+ self._stream_data.put_nowait,
+ (stream_id, data, event)
+ )
+ return event
+
+ def send_response(self, stream_id, headers):
+ """
+ Thread-safe method called from outside the main asyncio thread in order
+ to send the HTTP response headers on behalf of a WSGI application.
+
+ Returns a threading event that will fire when the headers have been
+ emitted to the network.
+ """
+ event = threading.Event()
+
+ def _inner_send(stream_id, headers, event):
+ self.conn.send_headers(stream_id, headers, end_stream=False)
+ self.transport.write(self.conn.data_to_send())
+ event.set()
+
+ self._loop.call_soon_threadsafe(
+ _inner_send,
+ stream_id,
+ headers,
+ event
+ )
+ return event
+
+ def open_flow_control_window(self, stream_id, increment):
+ """
+ Opens a flow control window for the given stream by the given amount.
+ Called from a WSGI thread. Does not return an event because there's no
+ need to block on this action, it may take place at any time.
+ """
+ def _inner_open(stream_id, increment):
+ self.conn.increment_flow_control_window(increment, stream_id)
+ self.conn.increment_flow_control_window(increment, None)
+ self.transport.write(self.conn.data_to_send())
+
+ self._loop.call_soon_threadsafe(
+ _inner_open,
+ stream_id,
+ increment,
+ )
+
+
+class Stream:
+ """
+ This class holds all of the state for a single stream. It also provides
+ several of the callables used by the WSGI application. Finally, it provides
+ the logic for actually interfacing with the WSGI application.
+
+ For these reasons, the object has *strict* requirements on thread-safety.
+ While the object can be initialized in the main WSGI thread, the
+ ``run_in_threadpool`` method *must* be called from outside that thread. At
+ that point, the main WSGI thread may only call specific methods.
+ """
+ def __init__(self, stream_id, protocol):
+ self.stream_id = stream_id
+ self._protocol = protocol
+
+ # Queue for data that has been received from the network. This is a
+ # thread-safe queue, to allow both the WSGI application to block on
+ # receiving more data and to allow the asyncio server to keep sending
+ # more data.
+ #
+ # This queue is unbounded in size, but in practice it cannot contain
+ # too much data because the flow control window doesn't get adjusted
+ # unless data is removed from it.
+ self._received_data = queue.Queue()
+
+ # This buffer is used to hold partial chunks of data from
+ # _received_data that were not returned out of ``read`` and friends.
+ self._temp_buffer = b''
+
+ # Temporary variables that allow us to keep hold of the headers and
+ # response status until such time as the application needs us to send
+ # them.
+ self._response_status = b''
+ self._response_headers = []
+ self._headers_emitted = False
+
+ # Whether the application has received all the data from the network
+ # or not. This allows us to short-circuit some reads.
+ self._complete = False
+
+ def receive_data(self, data, flow_controlled_size):
+ """
+ Called by the H2Protocol when more data has been received from the
+ network.
+
+ Places the data directly on the queue in a thread-safe manner without
+ blocking. Does not introspect or process the data.
+ """
+ self._received_data.put_nowait((data, flow_controlled_size))
+
+ def request_complete(self):
+ """
+ Called by the H2Protocol when all the request data has been received.
+
+ This works by placing the ``END_DATA_SENTINEL`` on the queue. The
+ reading code knows, when it sees the ``END_DATA_SENTINEL``, to expect
+ no more data from the network. This ensures that the state of the
+ application only changes when it has finished processing the data from
+ the network, even though the server may have long-since finished
+ receiving all the data for this request.
+ """
+ self._received_data.put_nowait((END_DATA_SENTINEL, None))
+
+ def run_in_threadpool(self, wsgi_application, environ):
+ """
+ This method should be invoked in a threadpool. At the point this method
+ is invoked, the only safe methods to call from the original thread are
+ ``receive_data`` and ``request_complete``: any other method is unsafe.
+
+ This method handles the WSGI logic. It invokes the application callable
+ in this thread, passing control over to the WSGI application. It then
+ ensures that the data makes it back to the HTTP/2 connection via
+ the thread-safe APIs provided below.
+ """
+ result = wsgi_application(environ, self.start_response)
+
+ try:
+ for data in result:
+ self.write(data)
+ finally:
+ # This signals that we're done with data. The server will know that
+ # this allows it to clean up its state: we're done here.
+ self.write(END_DATA_SENTINEL)
+
+ # The next few methods are called by the WSGI application. Firstly, the
+ # three methods provided by the input stream.
+ def read(self, size=None):
+ """
+ Called by the WSGI application to read data.
+
+ This method is the one of two that explicitly pumps the input data
+ queue, which means it deals with the ``_complete`` flag and the
+ ``END_DATA_SENTINEL``.
+ """
+ # If we've already seen the END_DATA_SENTINEL, return immediately.
+ if self._complete:
+ return b''
+
+ # If we've been asked to read everything, just iterate over ourselves.
+ if size is None:
+ return b''.join(self)
+
+ # Otherwise, as long as we don't have enough data, spin looking for
+ # another data chunk.
+ data = b''
+ while len(data) < size:
+ try:
+ chunk = next(self)
+ except StopIteration:
+ break
+
+ # Concatenating strings this way is slow, but that's ok, this is
+ # just a demo.
+ data += chunk
+
+ # We have *at least* enough data to return, but we may have too much.
+ # If we do, throw it on a buffer: we'll use it later.
+ to_return = data[:size]
+ self._temp_buffer = data[size:]
+ return to_return
+
+ def readline(self, hint=None):
+ """
+ Called by the WSGI application to read a single line of data.
+
+ This method rigorously observes the ``hint`` parameter: it will only
+ ever read that much data. It then splits the data on a newline
+ character and throws everything it doesn't need into a buffer.
+ """
+ data = self.read(hint)
+ first_newline = data.find(b'\n')
+ if first_newline == -1:
+ # No newline, return all the data
+ return data
+
+ # We want to slice the data so that the head *includes* the first
+ # newline. Then, any data left in this line we don't care about should
+ # be prepended to the internal buffer.
+ head, tail = data[:first_newline + 1], data[first_newline + 1:]
+ self._temp_buffer = tail + self._temp_buffer
+
+ return head
+
+ def readlines(self, hint=None):
+ """
+ Called by the WSGI application to read several lines of data.
+
+ This method is really pretty stupid. It rigorously observes the
+ ``hint`` parameter, and quite happily returns the input split into
+ lines.
+ """
+ # This method is *crazy inefficient*, but it's also a pretty stupid
+ # method to call.
+ data = self.read(hint)
+ lines = data.split(b'\n')
+
+ # Split removes the newline character, but we want it, so put it back.
+ lines = [line + b'\n' for line in lines]
+
+ # Except if the last character was a newline character we now have an
+ # extra line that is just a newline: pull that out.
+ if lines[-1] == b'\n':
+ lines = lines[:-1]
+ return lines
+
+ def start_response(self, status, response_headers, exc_info=None):
+ """
+ This is the PEP-3333 mandated start_response callable.
+
+ All it does is store the headers for later sending, and return our
+ ```write`` callable.
+ """
+ if self._headers_emitted and exc_info is not None:
+ raise exc_info[1].with_traceback(exc_info[2])
+
+ assert not self._response_status or exc_info is not None
+ self._response_status = status
+ self._response_headers = response_headers
+
+ return self.write
+
+ def write(self, data):
+ """
+ Provides some data to write.
+
+ This function *blocks* until such time as the data is allowed by
+ HTTP/2 flow control. This allows a client to slow or pause the response
+ as needed.
+
+ This function is not supposed to be used, according to PEP-3333, but
+ once we have it it becomes quite convenient to use it, so this app
+ actually runs all writes through this function.
+ """
+ if not self._headers_emitted:
+ self._emit_headers()
+ event = self._protocol.data_for_stream(self.stream_id, data)
+ event.wait()
+ return
+
+ def _emit_headers(self):
+ """
+ Sends the response headers.
+
+ This is only called from the write callable and should only ever be
+ called once. It does some minor processing (converts the status line
+ into a status code because reason phrases are evil) and then passes
+ the headers on to the server. This call explicitly blocks until the
+ server notifies us that the headers have reached the network.
+ """
+ assert self._response_status and self._response_headers
+ assert not self._headers_emitted
+ self._headers_emitted = True
+
+ # We only need the status code
+ status = self._response_status.split(" ", 1)[0]
+ headers = [(":status", status)]
+ headers.extend(self._response_headers)
+ event = self._protocol.send_response(self.stream_id, headers)
+ event.wait()
+ return
+
+ # These two methods implement the iterator protocol. This allows a WSGI
+ # application to iterate over this Stream object to get the data.
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ # If the complete request has been read, abort immediately.
+ if self._complete:
+ raise StopIteration()
+
+ # If we have data stored in a temporary buffer for any reason, return
+ # that and clear the buffer.
+ #
+ # This can actually only happen when the application uses one of the
+ # read* callables, but that's fine.
+ if self._temp_buffer:
+ buffered_data = self._temp_buffer
+ self._temp_buffer = b''
+ return buffered_data
+
+ # Otherwise, pull data off the queue (blocking as needed). If this is
+ # the end of the request, we're done here: mark ourselves as complete
+ # and call it time. Otherwise, open the flow control window an
+ # appropriate amount and hand the chunk off.
+ chunk, chunk_size = self._received_data.get()
+ if chunk is END_DATA_SENTINEL:
+ self._complete = True
+ raise StopIteration()
+
+ # Let's talk a little bit about why we're opening the flow control
+ # window *here*, and not in the server thread.
+ #
+ # The purpose of HTTP/2 flow control is to allow for servers and
+ # clients to avoid needing to buffer data indefinitely because their
+ # peer is producing data faster than they can consume it. As a result,
+ # it's important that the flow control window be opened as late in the
+ # processing as possible. In this case, we open the flow control window
+ # exactly when the server hands the data to the application. This means
+ # that the flow control window essentially signals to the remote peer
+ # how much data hasn't even been *seen* by the application yet.
+ #
+ # If you wanted to be really clever you could consider not opening the
+ # flow control window until the application asks for the *next* chunk
+ # of data. That means that any buffers at the application level are now
+ # included in the flow control window processing. In my opinion, the
+ # advantage of that process does not outweigh the extra logical
+ # complexity involved in doing it, so we don't bother here.
+ #
+ # Another note: you'll notice that we don't include the _temp_buffer in
+ # our flow control considerations. This means you could in principle
+ # lead us to buffer slightly more than one connection flow control
+ # window's worth of data. That risk is considered acceptable for the
+ # much simpler logic available here.
+ #
+ # Finally, this is a pretty dumb flow control window management scheme:
+ # it causes us to emit a *lot* of window updates. A smarter server
+ # would want to use the content-length header to determine whether
+ # flow control window updates need to be emitted at all, and then to be
+ # more efficient about emitting them to avoid firing them off really
+ # frequently. For an example like this, there's very little gained by
+ # worrying about that.
+ self._protocol.open_flow_control_window(self.stream_id, chunk_size)
+
+ return chunk
+
+
+def _build_environ_dict(headers, stream):
+ """
+ Build the WSGI environ dictionary for a given request. To do that, we'll
+ temporarily create a dictionary for the headers. While this isn't actually
+ a valid way to represent headers, we know that the special headers we need
+ can only have one appearance in the block.
+
+ This code is arguably somewhat incautious: the conversion to dictionary
+ should only happen in a way that allows us to correctly join headers that
+ appear multiple times. That's acceptable in a demo app: in a productised
+ version you'd want to fix it.
+ """
+ header_dict = dict(headers)
+ path = header_dict.pop(u':path')
+ try:
+ path, query = path.split(u'?', 1)
+ except ValueError:
+ query = u""
+ server_name = header_dict.pop(u':authority')
+ try:
+ server_name, port = server_name.split(u':', 1)
+ except ValueError as e:
+ port = "8443"
+
+ environ = {
+ u'REQUEST_METHOD': header_dict.pop(u':method'),
+ u'SCRIPT_NAME': u'',
+ u'PATH_INFO': path,
+ u'QUERY_STRING': query,
+ u'SERVER_NAME': server_name,
+ u'SERVER_PORT': port,
+ u'SERVER_PROTOCOL': u'HTTP/2',
+ u'HTTPS': u"on",
+ u'SSL_PROTOCOL': u'TLSv1.2',
+ u'wsgi.version': (1, 0),
+ u'wsgi.url_scheme': header_dict.pop(u':scheme'),
+ u'wsgi.input': stream,
+ u'wsgi.errors': sys.stderr,
+ u'wsgi.multithread': True,
+ u'wsgi.multiprocess': False,
+ u'wsgi.run_once': False,
+ }
+ if u'content-type' in header_dict:
+ environ[u'CONTENT_TYPE'] = header_dict[u'content-type']
+ if u'content-length' in header_dict:
+ environ[u'CONTENT_LENGTH'] = header_dict[u'content-length']
+ for name, value in header_dict.items():
+ environ[u'HTTP_' + name.upper()] = value
+ return environ
+
+
+# Set up the WSGI app.
+application_string = sys.argv[1]
+path, func = application_string.split(':', 1)
+module = importlib.import_module(path)
+APPLICATION = getattr(module, func)
+
+# Set up TLS
+ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ssl_context.options |= (
+ ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION
+)
+ssl_context.set_ciphers("ECDHE+AESGCM")
+ssl_context.load_cert_chain(certfile="cert.crt", keyfile="cert.key")
+ssl_context.set_alpn_protocols(["h2"])
+
+# Do the asnycio bits
+loop = asyncio.get_event_loop()
+# Each client connection will create a new protocol instance
+coro = loop.create_server(H2Protocol, '127.0.0.1', 8443, ssl=ssl_context)
+server = loop.run_until_complete(coro)
+
+# Serve requests until Ctrl+C is pressed
+print('Serving on {}'.format(server.sockets[0].getsockname()))
+try:
+ loop.run_forever()
+except KeyboardInterrupt:
+ pass
+
+# Close the server
+server.close()
+loop.run_until_complete(server.wait_closed())
+loop.close()
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/curio/curio-server.py b/testing/web-platform/tests/tools/third_party/h2/examples/curio/curio-server.py
new file mode 100644
index 0000000000..f93d4db9d0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/curio/curio-server.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3.5
+# -*- coding: utf-8 -*-
+"""
+curio-server.py
+~~~~~~~~~~~~~~~
+
+A fully-functional HTTP/2 server written for curio.
+
+Requires Python 3.5+.
+"""
+import mimetypes
+import os
+import sys
+
+from curio import Event, spawn, socket, ssl, run
+
+import h2.config
+import h2.connection
+import h2.events
+
+
+# The maximum amount of a file we'll send in a single DATA frame.
+READ_CHUNK_SIZE = 8192
+
+
+async def create_listening_ssl_socket(address, certfile, keyfile):
+ """
+ Create and return a listening TLS socket on a given address.
+ """
+ ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ ssl_context.options |= (
+ ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION
+ )
+ ssl_context.set_ciphers("ECDHE+AESGCM")
+ ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile)
+ ssl_context.set_alpn_protocols(["h2"])
+
+ sock = socket.socket()
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock = await ssl_context.wrap_socket(sock)
+ sock.bind(address)
+ sock.listen()
+
+ return sock
+
+
+async def h2_server(address, root, certfile, keyfile):
+ """
+ Create an HTTP/2 server at the given address.
+ """
+ sock = await create_listening_ssl_socket(address, certfile, keyfile)
+ print("Now listening on %s:%d" % address)
+
+ async with sock:
+ while True:
+ client, _ = await sock.accept()
+ server = H2Server(client, root)
+ await spawn(server.run())
+
+
+class H2Server:
+ """
+ A basic HTTP/2 file server. This is essentially very similar to
+ SimpleHTTPServer from the standard library, but uses HTTP/2 instead of
+ HTTP/1.1.
+ """
+ def __init__(self, sock, root):
+ config = h2.config.H2Configuration(
+ client_side=False, header_encoding='utf-8'
+ )
+ self.sock = sock
+ self.conn = h2.connection.H2Connection(config=config)
+ self.root = root
+ self.flow_control_events = {}
+
+ async def run(self):
+ """
+ Loop over the connection, managing it appropriately.
+ """
+ self.conn.initiate_connection()
+ await self.sock.sendall(self.conn.data_to_send())
+
+ while True:
+ # 65535 is basically arbitrary here: this amounts to "give me
+ # whatever data you have".
+ data = await self.sock.recv(65535)
+ if not data:
+ break
+
+ events = self.conn.receive_data(data)
+ for event in events:
+ if isinstance(event, h2.events.RequestReceived):
+ await spawn(
+ self.request_received(event.headers, event.stream_id)
+ )
+ elif isinstance(event, h2.events.DataReceived):
+ self.conn.reset_stream(event.stream_id)
+ elif isinstance(event, h2.events.WindowUpdated):
+ await self.window_updated(event)
+
+ await self.sock.sendall(self.conn.data_to_send())
+
+ async def request_received(self, headers, stream_id):
+ """
+ Handle a request by attempting to serve a suitable file.
+ """
+ headers = dict(headers)
+ assert headers[':method'] == 'GET'
+
+ path = headers[':path'].lstrip('/')
+ full_path = os.path.join(self.root, path)
+
+ if not os.path.exists(full_path):
+ response_headers = (
+ (':status', '404'),
+ ('content-length', '0'),
+ ('server', 'curio-h2'),
+ )
+ self.conn.send_headers(
+ stream_id, response_headers, end_stream=True
+ )
+ await self.sock.sendall(self.conn.data_to_send())
+ else:
+ await self.send_file(full_path, stream_id)
+
+ async def send_file(self, file_path, stream_id):
+ """
+ Send a file, obeying the rules of HTTP/2 flow control.
+ """
+ filesize = os.stat(file_path).st_size
+ content_type, content_encoding = mimetypes.guess_type(file_path)
+ response_headers = [
+ (':status', '200'),
+ ('content-length', str(filesize)),
+ ('server', 'curio-h2'),
+ ]
+ if content_type:
+ response_headers.append(('content-type', content_type))
+ if content_encoding:
+ response_headers.append(('content-encoding', content_encoding))
+
+ self.conn.send_headers(stream_id, response_headers)
+ await self.sock.sendall(self.conn.data_to_send())
+
+ with open(file_path, 'rb', buffering=0) as f:
+ await self._send_file_data(f, stream_id)
+
+ async def _send_file_data(self, fileobj, stream_id):
+ """
+ Send the data portion of a file. Handles flow control rules.
+ """
+ while True:
+ while self.conn.local_flow_control_window(stream_id) < 1:
+ await self.wait_for_flow_control(stream_id)
+
+ chunk_size = min(
+ self.conn.local_flow_control_window(stream_id),
+ READ_CHUNK_SIZE,
+ )
+
+ data = fileobj.read(chunk_size)
+ keep_reading = (len(data) == chunk_size)
+
+ self.conn.send_data(stream_id, data, not keep_reading)
+ await self.sock.sendall(self.conn.data_to_send())
+
+ if not keep_reading:
+ break
+
+ async def wait_for_flow_control(self, stream_id):
+ """
+ Blocks until the flow control window for a given stream is opened.
+ """
+ evt = Event()
+ self.flow_control_events[stream_id] = evt
+ await evt.wait()
+
+ async def window_updated(self, event):
+ """
+ Unblock streams waiting on flow control, if needed.
+ """
+ stream_id = event.stream_id
+
+ if stream_id and stream_id in self.flow_control_events:
+ evt = self.flow_control_events.pop(stream_id)
+ await evt.set()
+ elif not stream_id:
+ # Need to keep a real list here to use only the events present at
+ # this time.
+ blocked_streams = list(self.flow_control_events.keys())
+ for stream_id in blocked_streams:
+ event = self.flow_control_events.pop(stream_id)
+ await event.set()
+ return
+
+
+if __name__ == '__main__':
+ host = sys.argv[2] if len(sys.argv) > 2 else "localhost"
+ print("Try GETting:")
+ print(" On OSX after 'brew install curl --with-c-ares --with-libidn --with-nghttp2 --with-openssl':")
+ print("/usr/local/opt/curl/bin/curl --tlsv1.2 --http2 -k https://localhost:5000/bundle.js")
+ print("Or open a browser to: https://localhost:5000/")
+ print(" (Accept all the warnings)")
+ run(h2_server((host, 5000), sys.argv[1],
+ "{}.crt.pem".format(host),
+ "{}.key".format(host)), with_monitor=True)
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/curio/localhost.crt.pem b/testing/web-platform/tests/tools/third_party/h2/examples/curio/localhost.crt.pem
new file mode 100644
index 0000000000..d6cf7d504d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/curio/localhost.crt.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDhTCCAm2gAwIBAgIJAOrxh0dOYJLdMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNTA5MTkxNDE2
+NDRaFw0xNTEwMTkxNDE2NDRaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21l
+LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV
+BAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMqt
+A1iu8EN00FU0eBcBGlLVmNEgV7Jkbukra+kwS8j/U2y50QPGJc/FiIVDfuBqk5dL
+ACTNc6A/FQcXvWmOc5ixmC3QKKasMpuofqKz0V9C6irZdYXZ9rcsW0gHQIr989yd
+R+N1VbIlEVW/T9FJL3B2UD9GVIkUELzm47CSOWZvAxQUlsx8CUNuUCWqyZJoqTFN
+j0LeJDOWGCsug1Pkj0Q1x+jMVL6l6Zf6vMkLNOMsOsWsxUk+0L3tl/OzcTgUOCsw
+UzY59RIi6Rudrp0oaU8NuHr91yiSqPbKFlX10M9KwEEdnIpcxhND3dacrDycj3ux
+eWlqKync2vOFUkhwiaMCAwEAAaNQME4wHQYDVR0OBBYEFA0PN+PGoofZ+QIys2Jy
+1Zz94vBOMB8GA1UdIwQYMBaAFA0PN+PGoofZ+QIys2Jy1Zz94vBOMAwGA1UdEwQF
+MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEplethBoPpcP3EbR5Rz6snDDIcbtAJu
+Ngd0YZppGT+P0DYnPJva4vRG3bb84ZMSuppz5j67qD6DdWte8UXhK8BzWiHzwmQE
+QmbKyzzTMKQgTNFntpx5cgsSvTtrHpNYoMHzHOmyAOboNeM0DWiRXsYLkWTitLTN
+qbOpstwPubExbT9lPjLclntShT/lCupt+zsbnrR9YiqlYFY/fDzfAybZhrD5GMBY
+XdMPItwAc/sWvH31yztarjkLmld76AGCcO5r8cSR/cX98SicyfjOBbSco8GkjYNY
+582gTPkKGYpStuN7GNT5tZmxvMq935HRa2XZvlAIe8ufp8EHVoYiF3c=
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/curio/localhost.key b/testing/web-platform/tests/tools/third_party/h2/examples/curio/localhost.key
new file mode 100644
index 0000000000..bda69e836c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/curio/localhost.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAyq0DWK7wQ3TQVTR4FwEaUtWY0SBXsmRu6Str6TBLyP9TbLnR
+A8Ylz8WIhUN+4GqTl0sAJM1zoD8VBxe9aY5zmLGYLdAopqwym6h+orPRX0LqKtl1
+hdn2tyxbSAdAiv3z3J1H43VVsiURVb9P0UkvcHZQP0ZUiRQQvObjsJI5Zm8DFBSW
+zHwJQ25QJarJkmipMU2PQt4kM5YYKy6DU+SPRDXH6MxUvqXpl/q8yQs04yw6xazF
+ST7Qve2X87NxOBQ4KzBTNjn1EiLpG52unShpTw24ev3XKJKo9soWVfXQz0rAQR2c
+ilzGE0Pd1pysPJyPe7F5aWorKdza84VSSHCJowIDAQABAoIBACp+nh4BB/VMz8Wd
+q7Q/EfLeQB1Q57JKpoqTBRwueSVai3ZXe4CMEi9/HkG6xiZtkiZ9njkZLq4hq9oB
+2z//kzMnwV2RsIRJxI6ohGy+wR51HD4BvEdlTPpY/Yabpqe92VyfSYxidKZWaU0O
+QMED1EODOw4ZQ+4928iPrJu//PMB4e7TFao0b9Fk/XLWtu5/tQZz9jsrlTi1zthh
+7n+oaGNhfTeIJJL4jrhTrKW1CLHXATtr9SJlfZ3wbMxQVeyj2wUlP1V0M6kBuhNj
+tbGbMpixD5iCNJ49Cm2PHg+wBOfS3ADGIpi3PcGw5mb8nB3N9eGBRPhLShAlq5Hi
+Lv4tyykCgYEA8u3b3xJ04pxWYN25ou/Sc8xzgDCK4XvDNdHVTuZDjLVA+VTVPzql
+lw7VvJArsx47MSPvsaX/+4hQXYtfnR7yJpx6QagvQ+z4ludnIZYrQwdUmb9pFL1s
+8UNj+3j9QFRPenIiIQ8qxxNIQ9w2HsVQ8scvc9CjYop/YYAPaQyHaL8CgYEA1ZSz
+CR4NcpfgRSILdhb1dLcyw5Qus1VOSAx3DYkhDkMiB8XZwgMdJjwehJo9yaqRCLE8
+Sw5znMnkfoZpu7+skrjK0FqmMpXMH9gIszHvFG8wSw/6+2HIWS19/wOu8dh95LuC
+0zurMk8rFqxgWMWF20afhgYrUz42cvUTo10FVB0CgYEAt7mW6W3PArfUSCxIwmb4
+VmXREKkl0ATHDYQl/Cb//YHzot467TgQll883QB4XF5HzBFurX9rSzO7/BN1e6I0
+52i+ubtWC9xD4fUetXMaQvZfUGxIL8xXgVxDWKQXfLiG54c8Mp6C7s6xf8kjEUCP
+yR1F0SSA/Pzb+8RbY0p7eocCgYA+1rs+SXtHZev0KyoYGnUpW+Uxqd17ofOgOxqj
+/t6c5Z+TjeCdtnDTGQkZlo/rT6XQWuUUaDIXxUbW+xEMzj4mBPyXBLS1WWFvVQ5q
+OpzO9E/PJeqAH6rkof/aEelc+oc/zvOU1o9uA+D3kMvgEm1psIOq2RHSMhGvDPA0
+NmAk+QKBgQCwd1681GagdIYSZUCBecnLtevXmIsJyDW2yR1NNcIe/ukcVQREMDvy
+5DDkhnGDgnV1D5gYcXb34g9vYvbfTnBMl/JXmMAAG1kIS+3pvHyN6f1poVe3yJV1
+yHVuvymnJxKnyaV0L3ntepVvV0vVNIkA3oauoUTLto6txBI+b/ImDA==
+-----END RSA PRIVATE KEY-----
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/eventlet/eventlet-server.py b/testing/web-platform/tests/tools/third_party/h2/examples/eventlet/eventlet-server.py
new file mode 100644
index 0000000000..a46cfb3d15
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/eventlet/eventlet-server.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+"""
+eventlet-server.py
+~~~~~~~~~~~~~~~~~~
+
+A fully-functional HTTP/2 server written for Eventlet.
+"""
+import collections
+import json
+
+import eventlet
+
+from eventlet.green.OpenSSL import SSL, crypto
+from h2.config import H2Configuration
+from h2.connection import H2Connection
+from h2.events import RequestReceived, DataReceived
+
+
+class ConnectionManager(object):
+ """
+ An object that manages a single HTTP/2 connection.
+ """
+ def __init__(self, sock):
+ config = H2Configuration(client_side=False)
+ self.sock = sock
+ self.conn = H2Connection(config=config)
+
+ def run_forever(self):
+ self.conn.initiate_connection()
+ self.sock.sendall(self.conn.data_to_send())
+
+ while True:
+ data = self.sock.recv(65535)
+ if not data:
+ break
+
+ events = self.conn.receive_data(data)
+
+ for event in events:
+ if isinstance(event, RequestReceived):
+ self.request_received(event.headers, event.stream_id)
+ elif isinstance(event, DataReceived):
+ self.conn.reset_stream(event.stream_id)
+
+ self.sock.sendall(self.conn.data_to_send())
+
+ def request_received(self, headers, stream_id):
+ headers = collections.OrderedDict(headers)
+ data = json.dumps({'headers': headers}, indent=4).encode('utf-8')
+
+ response_headers = (
+ (':status', '200'),
+ ('content-type', 'application/json'),
+ ('content-length', str(len(data))),
+ ('server', 'eventlet-h2'),
+ )
+ self.conn.send_headers(stream_id, response_headers)
+ self.conn.send_data(stream_id, data, end_stream=True)
+
+
+def alpn_callback(conn, protos):
+ if b'h2' in protos:
+ return b'h2'
+
+ raise RuntimeError("No acceptable protocol offered!")
+
+
+def npn_advertise_cb(conn):
+ return [b'h2']
+
+
+# Let's set up SSL. This is a lot of work in PyOpenSSL.
+options = (
+ SSL.OP_NO_COMPRESSION |
+ SSL.OP_NO_SSLv2 |
+ SSL.OP_NO_SSLv3 |
+ SSL.OP_NO_TLSv1 |
+ SSL.OP_NO_TLSv1_1
+)
+context = SSL.Context(SSL.SSLv23_METHOD)
+context.set_options(options)
+context.set_verify(SSL.VERIFY_NONE, lambda *args: True)
+context.use_privatekey_file('server.key')
+context.use_certificate_file('server.crt')
+context.set_npn_advertise_callback(npn_advertise_cb)
+context.set_alpn_select_callback(alpn_callback)
+context.set_cipher_list(
+ "ECDHE+AESGCM"
+)
+context.set_tmp_ecdh(crypto.get_elliptic_curve(u'prime256v1'))
+
+server = eventlet.listen(('0.0.0.0', 443))
+server = SSL.Connection(context, server)
+pool = eventlet.GreenPool()
+
+while True:
+ try:
+ new_sock, _ = server.accept()
+ manager = ConnectionManager(new_sock)
+ pool.spawn_n(manager.run_forever)
+ except (SystemExit, KeyboardInterrupt):
+ break
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/eventlet/server.crt b/testing/web-platform/tests/tools/third_party/h2/examples/eventlet/server.crt
new file mode 100644
index 0000000000..bc8a4c08d2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/eventlet/server.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDUjCCAjoCCQCQmNzzpQTCijANBgkqhkiG9w0BAQUFADBrMQswCQYDVQQGEwJH
+QjEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xETAPBgNVBAoTCGh5
+cGVyLWgyMREwDwYDVQQLEwhoeXBleS1oMjEUMBIGA1UEAxMLZXhhbXBsZS5jb20w
+HhcNMTUwOTE2MjAyOTA0WhcNMTYwOTE1MjAyOTA0WjBrMQswCQYDVQQGEwJHQjEP
+MA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xETAPBgNVBAoTCGh5cGVy
+LWgyMREwDwYDVQQLEwhoeXBleS1oMjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC74ZeB4Jdb5cnC9KXXLJuzjwTg
+45q5EeShDYQe0TbKgreiUP6clU3BR0fFAVedN1q/LOuQ1HhvrDk1l4TfGF2bpCIq
+K+U9CnzcQknvdpyyVeOLtSsCjOPk4xydHwkQxwJvHVdtJx4CzDDqGbHNHCF/9gpQ
+lsa3JZW+tIZLK0XMEPFQ4XFXgegxTStO7kBBPaVIgG9Ooqc2MG4rjMNUpxa28WF1
+SyqWTICf2N8T/C+fPzbQLKCWrFrKUP7WQlOaqPNQL9bCDhSTPRTwQOc2/MzVZ9gT
+Xr0Z+JMTXwkSMKO52adE1pmKt00jJ1ecZBiJFyjx0X6hH+/59dLbG/7No+PzAgMB
+AAEwDQYJKoZIhvcNAQEFBQADggEBAG3UhOCa0EemL2iY+C+PR6CwEHQ+n7vkBzNz
+gKOG+Q39spyzqU1qJAzBxLTE81bIQbDg0R8kcLWHVH2y4zViRxZ0jHUFKMgjONW+
+Aj4evic/2Y/LxpLxCajECq/jeMHYrmQONszf9pbc0+exrQpgnwd8asfsM3d/FJS2
+5DIWryCKs/61m9vYL8icWx/9cnfPkBoNv1ER+V1L1TH3ARvABh406SBaeqLTm/kG
+MNuKytKWJsQbNlxzWHVgkKzVsBKvYj0uIEJpClIhbe6XNYRDy8T8mKXVWhJuxH4p
+/agmCG3nxO8aCrUK/EVmbWmVIfCH3t7jlwMX1nJ8MsRE7Ydnk8I=
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/eventlet/server.key b/testing/web-platform/tests/tools/third_party/h2/examples/eventlet/server.key
new file mode 100644
index 0000000000..11f9ea094b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/eventlet/server.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAu+GXgeCXW+XJwvSl1yybs48E4OOauRHkoQ2EHtE2yoK3olD+
+nJVNwUdHxQFXnTdavyzrkNR4b6w5NZeE3xhdm6QiKivlPQp83EJJ73acslXji7Ur
+Aozj5OMcnR8JEMcCbx1XbSceAsww6hmxzRwhf/YKUJbGtyWVvrSGSytFzBDxUOFx
+V4HoMU0rTu5AQT2lSIBvTqKnNjBuK4zDVKcWtvFhdUsqlkyAn9jfE/wvnz820Cyg
+lqxaylD+1kJTmqjzUC/Wwg4Ukz0U8EDnNvzM1WfYE169GfiTE18JEjCjudmnRNaZ
+irdNIydXnGQYiRco8dF+oR/v+fXS2xv+zaPj8wIDAQABAoIBAQCsdq278+0c13d4
+tViSh4k5r1w8D9IUdp9XU2/nVgckqA9nOVAvbkJc3FC+P7gsQgbUHKj0XoVbhU1S
+q461t8kduPH/oiGhAcKR8WurHEdE0OC6ewhLJAeCMRQwCrAorXXHh7icIt9ClCuG
+iSWUcXEy5Cidx3oL3r1xvIbV85fzdDtE9RC1I/kMjAy63S47YGiqh5vYmJkCa8rG
+Dsd1sEMDPr63XJpqJj3uHRcPvySgXTa+ssTmUH8WJlPTjvDB5hnPz+lkk2JKVPNu
+8adzftZ6hSun+tsc4ZJp8XhGu/m/7MjxWh8MeupLHlXcOEsnj4uHQQsOM3zHojr3
+aDCZiC1pAoGBAOAhwe1ujoS2VJ5RXJ9KMs7eBER/02MDgWZjo54Jv/jFxPWGslKk
+QQceuTe+PruRm41nzvk3q4iZXt8pG0bvpgigN2epcVx/O2ouRsUWWBT0JrVlEzha
+TIvWjtZ5tSQExXgHL3VlM9+ka40l+NldLSPn25+prizaqhalWuvTpP23AoGBANaY
+VhEI6yhp0BBUSATEv9lRgkwx3EbcnXNXPQjDMOthsyfq7FxbdOBEK1rwSDyuE6Ij
+zQGcTOfdiur5Ttg0OQilTJIXJAlpoeecOQ9yGma08c5FMXVJJvcZUuWRZWg1ocQj
+/hx0WVE9NwOoKwTBERv8HX7vJOFRZyvgkJwFxoulAoGAe4m/1XoZrga9z2GzNs10
+AdgX7BW00x+MhH4pIiPnn1yK+nYa9jg4647Asnv3IfXZEnEEgRNxReKbi0+iDFBt
+aNW+lDGuHTi37AfD1EBDnpEQgO1MUcRb6rwBkTAWatsCaO00+HUmyX9cFLm4Vz7n
+caILyQ6CxZBlLgRIgDHxADMCgYEAtubsJGTHmZBmSCStpXLUWbOBLNQqfTM398DZ
+QoirP1PsUQ+IGUfSG/u+QCogR6fPEBkXeFHxsoY/Cvsm2lvYaKgK1VFn46Xm2vNq
+JuIH4pZCqp6LAv4weddZslT0a5eaowRSZ4o7PmTAaRuCXvD3VjTSJwhJFMo+90TV
+vEWn7gkCgYEAkk+unX9kYmKoUdLh22/tzQekBa8WqMxXDwzBCECTAs2GlpL/f73i
+zD15TnaNfLP6Q5RNb0N9tb0Gz1wSkwI1+jGAQLnh2K9X9cIVIqJn8Mf/KQa/wUDV
+Tb1j7FoGUEgX7vbsyWuTd8P76kNYyGqCss1XmbttcSolqpbIdlSUcO0=
+-----END RSA PRIVATE KEY-----
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/fragments/client_https_setup_fragment.py b/testing/web-platform/tests/tools/third_party/h2/examples/fragments/client_https_setup_fragment.py
new file mode 100644
index 0000000000..269194d6c7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/fragments/client_https_setup_fragment.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+"""
+Client HTTPS Setup
+~~~~~~~~~~~~~~~~~~
+
+This example code fragment demonstrates how to set up a HTTP/2 client that
+negotiates HTTP/2 using NPN and ALPN. For the sake of maximum explanatory value
+this code uses the synchronous, low-level sockets API: however, if you're not
+using sockets directly (e.g. because you're using asyncio), you should focus on
+the set up required for the SSLContext object. For other concurrency libraries
+you may need to use other setup (e.g. for Twisted you'll need to use
+IProtocolNegotiationFactory).
+
+This code requires Python 3.5 or later.
+"""
+import h2.connection
+import socket
+import ssl
+
+
+def establish_tcp_connection():
+ """
+ This function establishes a client-side TCP connection. How it works isn't
+ very important to this example. For the purpose of this example we connect
+ to localhost.
+ """
+ return socket.create_connection(('localhost', 443))
+
+
+def get_http2_ssl_context():
+ """
+ This function creates an SSLContext object that is suitably configured for
+ HTTP/2. If you're working with Python TLS directly, you'll want to do the
+ exact same setup as this function does.
+ """
+ # Get the basic context from the standard library.
+ ctx = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
+
+ # RFC 7540 Section 9.2: Implementations of HTTP/2 MUST use TLS version 1.2
+ # or higher. Disable TLS 1.1 and lower.
+ ctx.options |= (
+ ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
+ )
+
+ # RFC 7540 Section 9.2.1: A deployment of HTTP/2 over TLS 1.2 MUST disable
+ # compression.
+ ctx.options |= ssl.OP_NO_COMPRESSION
+
+ # RFC 7540 Section 9.2.2: "deployments of HTTP/2 that use TLS 1.2 MUST
+ # support TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256". In practice, the
+ # blocklist defined in this section allows only the AES GCM and ChaCha20
+ # cipher suites with ephemeral key negotiation.
+ ctx.set_ciphers("ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20")
+
+ # We want to negotiate using NPN and ALPN. ALPN is mandatory, but NPN may
+ # be absent, so allow that. This setup allows for negotiation of HTTP/1.1.
+ ctx.set_alpn_protocols(["h2", "http/1.1"])
+
+ try:
+ ctx.set_npn_protocols(["h2", "http/1.1"])
+ except NotImplementedError:
+ pass
+
+ return ctx
+
+
+def negotiate_tls(tcp_conn, context):
+ """
+ Given an established TCP connection and a HTTP/2-appropriate TLS context,
+ this function:
+
+ 1. wraps TLS around the TCP connection.
+ 2. confirms that HTTP/2 was negotiated and, if it was not, throws an error.
+ """
+ # Note that SNI is mandatory for HTTP/2, so you *must* pass the
+ # server_hostname argument.
+ tls_conn = context.wrap_socket(tcp_conn, server_hostname='localhost')
+
+ # Always prefer the result from ALPN to that from NPN.
+ # You can only check what protocol was negotiated once the handshake is
+ # complete.
+ negotiated_protocol = tls_conn.selected_alpn_protocol()
+ if negotiated_protocol is None:
+ negotiated_protocol = tls_conn.selected_npn_protocol()
+
+ if negotiated_protocol != "h2":
+ raise RuntimeError("Didn't negotiate HTTP/2!")
+
+ return tls_conn
+
+
+def main():
+ # Step 1: Set up your TLS context.
+ context = get_http2_ssl_context()
+
+ # Step 2: Create a TCP connection.
+ connection = establish_tcp_connection()
+
+ # Step 3: Wrap the connection in TLS and validate that we negotiated HTTP/2
+ tls_connection = negotiate_tls(connection, context)
+
+ # Step 4: Create a client-side H2 connection.
+ http2_connection = h2.connection.H2Connection()
+
+ # Step 5: Initiate the connection
+ http2_connection.initiate_connection()
+ tls_connection.sendall(http2_connection.data_to_send())
+
+ # The TCP, TLS, and HTTP/2 handshakes are now complete. You can enter your
+ # main loop now.
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/fragments/client_upgrade_fragment.py b/testing/web-platform/tests/tools/third_party/h2/examples/fragments/client_upgrade_fragment.py
new file mode 100644
index 0000000000..f45c002df7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/fragments/client_upgrade_fragment.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+"""
+Client Plaintext Upgrade
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+This example code fragment demonstrates how to set up a HTTP/2 client that uses
+the plaintext HTTP Upgrade mechanism to negotiate HTTP/2 connectivity. For
+maximum explanatory value it uses the synchronous socket API that comes with
+the Python standard library. In product code you will want to use an actual
+HTTP/1.1 client if possible.
+
+This code requires Python 3.5 or later.
+"""
+import h2.connection
+import socket
+
+
+def establish_tcp_connection():
+ """
+ This function establishes a client-side TCP connection. How it works isn't
+ very important to this example. For the purpose of this example we connect
+ to localhost.
+ """
+ return socket.create_connection(('localhost', 80))
+
+
+def send_initial_request(connection, settings):
+ """
+ For the sake of this upgrade demonstration, we're going to issue a GET
+ request against the root of the site. In principle the best request to
+ issue for an upgrade is actually ``OPTIONS *``, but this is remarkably
+ poorly supported and can break in weird ways.
+ """
+ # Craft our initial request per RFC 7540 Section 3.2. This requires two
+ # special header fields: the Upgrade headre, and the HTTP2-Settings header.
+ # The value of the HTTP2-Settings header field comes from h2.
+ request = (
+ b"GET / HTTP/1.1\r\n" +
+ b"Host: localhost\r\n" +
+ b"Upgrade: h2c\r\n" +
+ b"HTTP2-Settings: " + settings + b"\r\n" +
+ b"\r\n"
+ )
+ connection.sendall(request)
+
+
+def get_upgrade_response(connection):
+ """
+ This function reads from the socket until the HTTP/1.1 end-of-headers
+ sequence (CRLFCRLF) is received. It then checks what the status code of the
+ response is.
+
+ This is not a substitute for proper HTTP/1.1 parsing, but it's good enough
+ for example purposes.
+ """
+ data = b''
+ while b'\r\n\r\n' not in data:
+ data += connection.recv(8192)
+
+ headers, rest = data.split(b'\r\n\r\n', 1)
+
+ # An upgrade response begins HTTP/1.1 101 Switching Protocols. Look for the
+ # code. In production code you should also check that the upgrade is to
+ # h2c, but here we know we only offered one upgrade so there's only one
+ # possible upgrade in use.
+ split_headers = headers.split()
+ if split_headers[1] != b'101':
+ raise RuntimeError("Not upgrading!")
+
+ # We don't care about the HTTP/1.1 data anymore, but we do care about
+ # any other data we read from the socket: this is going to be HTTP/2 data
+ # that must be passed to the H2Connection.
+ return rest
+
+
+def main():
+ """
+ The client upgrade flow.
+ """
+ # Step 1: Establish the TCP connecton.
+ connection = establish_tcp_connection()
+
+ # Step 2: Create H2 Connection object, put it in upgrade mode, and get the
+ # value of the HTTP2-Settings header we want to use.
+ h2_connection = h2.connection.H2Connection()
+ settings_header_value = h2_connection.initiate_upgrade_connection()
+
+ # Step 3: Send the initial HTTP/1.1 request with the upgrade fields.
+ send_initial_request(connection, settings_header_value)
+
+ # Step 4: Read the HTTP/1.1 response, look for 101 response.
+ extra_data = get_upgrade_response(connection)
+
+ # Step 5: Immediately send the pending HTTP/2 data.
+ connection.sendall(h2_connection.data_to_send())
+
+ # Step 6: Feed the body data to the connection.
+ events = connection.receive_data(extra_data)
+
+ # Now you can enter your main loop, beginning by processing the first set
+ # of events above. These events may include ResponseReceived, which will
+ # contain the response to the request we made in Step 3.
+ main_loop(events)
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/fragments/server_https_setup_fragment.py b/testing/web-platform/tests/tools/third_party/h2/examples/fragments/server_https_setup_fragment.py
new file mode 100644
index 0000000000..9fc361f2c6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/fragments/server_https_setup_fragment.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+"""
+Server HTTPS Setup
+~~~~~~~~~~~~~~~~~~
+
+This example code fragment demonstrates how to set up a HTTP/2 server that
+negotiates HTTP/2 using NPN and ALPN. For the sake of maximum explanatory value
+this code uses the synchronous, low-level sockets API: however, if you're not
+using sockets directly (e.g. because you're using asyncio), you should focus on
+the set up required for the SSLContext object. For other concurrency libraries
+you may need to use other setup (e.g. for Twisted you'll need to use
+IProtocolNegotiationFactory).
+
+This code requires Python 3.5 or later.
+"""
+import h2.config
+import h2.connection
+import socket
+import ssl
+
+
+def establish_tcp_connection():
+ """
+ This function establishes a server-side TCP connection. How it works isn't
+ very important to this example.
+ """
+ bind_socket = socket.socket()
+ bind_socket.bind(('', 443))
+ bind_socket.listen(5)
+ return bind_socket.accept()[0]
+
+
+def get_http2_ssl_context():
+ """
+ This function creates an SSLContext object that is suitably configured for
+ HTTP/2. If you're working with Python TLS directly, you'll want to do the
+ exact same setup as this function does.
+ """
+ # Get the basic context from the standard library.
+ ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
+
+ # RFC 7540 Section 9.2: Implementations of HTTP/2 MUST use TLS version 1.2
+ # or higher. Disable TLS 1.1 and lower.
+ ctx.options |= (
+ ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
+ )
+
+ # RFC 7540 Section 9.2.1: A deployment of HTTP/2 over TLS 1.2 MUST disable
+ # compression.
+ ctx.options |= ssl.OP_NO_COMPRESSION
+
+ # RFC 7540 Section 9.2.2: "deployments of HTTP/2 that use TLS 1.2 MUST
+ # support TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256". In practice, the
+ # blocklist defined in this section allows only the AES GCM and ChaCha20
+ # cipher suites with ephemeral key negotiation.
+ ctx.set_ciphers("ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20")
+
+ # We want to negotiate using NPN and ALPN. ALPN is mandatory, but NPN may
+ # be absent, so allow that. This setup allows for negotiation of HTTP/1.1.
+ ctx.set_alpn_protocols(["h2", "http/1.1"])
+
+ try:
+ ctx.set_npn_protocols(["h2", "http/1.1"])
+ except NotImplementedError:
+ pass
+
+ return ctx
+
+
+def negotiate_tls(tcp_conn, context):
+ """
+ Given an established TCP connection and a HTTP/2-appropriate TLS context,
+ this function:
+
+ 1. wraps TLS around the TCP connection.
+ 2. confirms that HTTP/2 was negotiated and, if it was not, throws an error.
+ """
+ tls_conn = context.wrap_socket(tcp_conn, server_side=True)
+
+ # Always prefer the result from ALPN to that from NPN.
+ # You can only check what protocol was negotiated once the handshake is
+ # complete.
+ negotiated_protocol = tls_conn.selected_alpn_protocol()
+ if negotiated_protocol is None:
+ negotiated_protocol = tls_conn.selected_npn_protocol()
+
+ if negotiated_protocol != "h2":
+ raise RuntimeError("Didn't negotiate HTTP/2!")
+
+ return tls_conn
+
+
+def main():
+ # Step 1: Set up your TLS context.
+ context = get_http2_ssl_context()
+
+ # Step 2: Receive a TCP connection.
+ connection = establish_tcp_connection()
+
+ # Step 3: Wrap the connection in TLS and validate that we negotiated HTTP/2
+ tls_connection = negotiate_tls(connection, context)
+
+ # Step 4: Create a server-side H2 connection.
+ config = h2.config.H2Configuration(client_side=False)
+ http2_connection = h2.connection.H2Connection(config=config)
+
+ # Step 5: Initiate the connection
+ http2_connection.initiate_connection()
+ tls_connection.sendall(http2_connection.data_to_send())
+
+ # The TCP, TLS, and HTTP/2 handshakes are now complete. You can enter your
+ # main loop now.
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/fragments/server_upgrade_fragment.py b/testing/web-platform/tests/tools/third_party/h2/examples/fragments/server_upgrade_fragment.py
new file mode 100644
index 0000000000..7e8b1f0eea
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/fragments/server_upgrade_fragment.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+"""
+Server Plaintext Upgrade
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+This example code fragment demonstrates how to set up a HTTP/2 server that uses
+the plaintext HTTP Upgrade mechanism to negotiate HTTP/2 connectivity. For
+maximum explanatory value it uses the synchronous socket API that comes with
+the Python standard library. In product code you will want to use an actual
+HTTP/1.1 server library if possible.
+
+This code requires Python 3.5 or later.
+"""
+import h2.config
+import h2.connection
+import re
+import socket
+
+
+def establish_tcp_connection():
+ """
+ This function establishes a server-side TCP connection. How it works isn't
+ very important to this example.
+ """
+ bind_socket = socket.socket()
+ bind_socket.bind(('', 443))
+ bind_socket.listen(5)
+ return bind_socket.accept()[0]
+
+
+def receive_initial_request(connection):
+ """
+ We're going to receive a request. For the sake of this example, we're going
+ to assume that the first request has no body. If it doesn't have the
+ Upgrade: h2c header field and the HTTP2-Settings header field, we'll throw
+ errors.
+
+ In production code, you should use a proper HTTP/1.1 parser and actually
+ serve HTTP/1.1 requests!
+
+ Returns the value of the HTTP2-Settings header field.
+ """
+ data = b''
+ while not data.endswith(b'\r\n\r\n'):
+ data += connection.recv(8192)
+
+ match = re.search(b'Upgrade: h2c\r\n', data)
+ if match is None:
+ raise RuntimeError("HTTP/2 upgrade not requested!")
+
+ # We need to look for the HTTP2-Settings header field. Again, in production
+ # code you shouldn't use regular expressions for this, but it's good enough
+ # for the example.
+ match = re.search(b'HTTP2-Settings: (\\S+)\r\n', data)
+ if match is None:
+ raise RuntimeError("HTTP2-Settings header field not present!")
+
+ return match.group(1)
+
+
+def send_upgrade_response(connection):
+ """
+ This function writes the 101 Switching Protocols response.
+ """
+ response = (
+ b"HTTP/1.1 101 Switching Protocols\r\n"
+ b"Upgrade: h2c\r\n"
+ b"\r\n"
+ )
+ connection.sendall(response)
+
+
+def main():
+ """
+ The server upgrade flow.
+ """
+ # Step 1: Establish the TCP connecton.
+ connection = establish_tcp_connection()
+
+ # Step 2: Read the response. We expect this to request an upgrade.
+ settings_header_value = receive_initial_request(connection)
+
+ # Step 3: Create a H2Connection object in server mode, and pass it the
+ # value of the HTTP2-Settings header field.
+ config = h2.config.H2Configuration(client_side=False)
+ h2_connection = h2.connection.H2Connection(config=config)
+ h2_connection.initiate_upgrade_connection(
+ settings_header=settings_header_value
+ )
+
+ # Step 4: Send the 101 Switching Protocols response.
+ send_upgrade_response(connection)
+
+ # Step 5: Send pending HTTP/2 data.
+ connection.sendall(h2_connection.data_to_send())
+
+ # At this point, you can enter your main loop. The first step has to be to
+ # send the response to the initial HTTP/1.1 request you received on stream
+ # 1.
+ main_loop()
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/tornado/server.crt b/testing/web-platform/tests/tools/third_party/h2/examples/tornado/server.crt
new file mode 100644
index 0000000000..bc8a4c08d2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/tornado/server.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDUjCCAjoCCQCQmNzzpQTCijANBgkqhkiG9w0BAQUFADBrMQswCQYDVQQGEwJH
+QjEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xETAPBgNVBAoTCGh5
+cGVyLWgyMREwDwYDVQQLEwhoeXBleS1oMjEUMBIGA1UEAxMLZXhhbXBsZS5jb20w
+HhcNMTUwOTE2MjAyOTA0WhcNMTYwOTE1MjAyOTA0WjBrMQswCQYDVQQGEwJHQjEP
+MA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xETAPBgNVBAoTCGh5cGVy
+LWgyMREwDwYDVQQLEwhoeXBleS1oMjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC74ZeB4Jdb5cnC9KXXLJuzjwTg
+45q5EeShDYQe0TbKgreiUP6clU3BR0fFAVedN1q/LOuQ1HhvrDk1l4TfGF2bpCIq
+K+U9CnzcQknvdpyyVeOLtSsCjOPk4xydHwkQxwJvHVdtJx4CzDDqGbHNHCF/9gpQ
+lsa3JZW+tIZLK0XMEPFQ4XFXgegxTStO7kBBPaVIgG9Ooqc2MG4rjMNUpxa28WF1
+SyqWTICf2N8T/C+fPzbQLKCWrFrKUP7WQlOaqPNQL9bCDhSTPRTwQOc2/MzVZ9gT
+Xr0Z+JMTXwkSMKO52adE1pmKt00jJ1ecZBiJFyjx0X6hH+/59dLbG/7No+PzAgMB
+AAEwDQYJKoZIhvcNAQEFBQADggEBAG3UhOCa0EemL2iY+C+PR6CwEHQ+n7vkBzNz
+gKOG+Q39spyzqU1qJAzBxLTE81bIQbDg0R8kcLWHVH2y4zViRxZ0jHUFKMgjONW+
+Aj4evic/2Y/LxpLxCajECq/jeMHYrmQONszf9pbc0+exrQpgnwd8asfsM3d/FJS2
+5DIWryCKs/61m9vYL8icWx/9cnfPkBoNv1ER+V1L1TH3ARvABh406SBaeqLTm/kG
+MNuKytKWJsQbNlxzWHVgkKzVsBKvYj0uIEJpClIhbe6XNYRDy8T8mKXVWhJuxH4p
+/agmCG3nxO8aCrUK/EVmbWmVIfCH3t7jlwMX1nJ8MsRE7Ydnk8I=
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/tornado/server.key b/testing/web-platform/tests/tools/third_party/h2/examples/tornado/server.key
new file mode 100644
index 0000000000..11f9ea094b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/tornado/server.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAu+GXgeCXW+XJwvSl1yybs48E4OOauRHkoQ2EHtE2yoK3olD+
+nJVNwUdHxQFXnTdavyzrkNR4b6w5NZeE3xhdm6QiKivlPQp83EJJ73acslXji7Ur
+Aozj5OMcnR8JEMcCbx1XbSceAsww6hmxzRwhf/YKUJbGtyWVvrSGSytFzBDxUOFx
+V4HoMU0rTu5AQT2lSIBvTqKnNjBuK4zDVKcWtvFhdUsqlkyAn9jfE/wvnz820Cyg
+lqxaylD+1kJTmqjzUC/Wwg4Ukz0U8EDnNvzM1WfYE169GfiTE18JEjCjudmnRNaZ
+irdNIydXnGQYiRco8dF+oR/v+fXS2xv+zaPj8wIDAQABAoIBAQCsdq278+0c13d4
+tViSh4k5r1w8D9IUdp9XU2/nVgckqA9nOVAvbkJc3FC+P7gsQgbUHKj0XoVbhU1S
+q461t8kduPH/oiGhAcKR8WurHEdE0OC6ewhLJAeCMRQwCrAorXXHh7icIt9ClCuG
+iSWUcXEy5Cidx3oL3r1xvIbV85fzdDtE9RC1I/kMjAy63S47YGiqh5vYmJkCa8rG
+Dsd1sEMDPr63XJpqJj3uHRcPvySgXTa+ssTmUH8WJlPTjvDB5hnPz+lkk2JKVPNu
+8adzftZ6hSun+tsc4ZJp8XhGu/m/7MjxWh8MeupLHlXcOEsnj4uHQQsOM3zHojr3
+aDCZiC1pAoGBAOAhwe1ujoS2VJ5RXJ9KMs7eBER/02MDgWZjo54Jv/jFxPWGslKk
+QQceuTe+PruRm41nzvk3q4iZXt8pG0bvpgigN2epcVx/O2ouRsUWWBT0JrVlEzha
+TIvWjtZ5tSQExXgHL3VlM9+ka40l+NldLSPn25+prizaqhalWuvTpP23AoGBANaY
+VhEI6yhp0BBUSATEv9lRgkwx3EbcnXNXPQjDMOthsyfq7FxbdOBEK1rwSDyuE6Ij
+zQGcTOfdiur5Ttg0OQilTJIXJAlpoeecOQ9yGma08c5FMXVJJvcZUuWRZWg1ocQj
+/hx0WVE9NwOoKwTBERv8HX7vJOFRZyvgkJwFxoulAoGAe4m/1XoZrga9z2GzNs10
+AdgX7BW00x+MhH4pIiPnn1yK+nYa9jg4647Asnv3IfXZEnEEgRNxReKbi0+iDFBt
+aNW+lDGuHTi37AfD1EBDnpEQgO1MUcRb6rwBkTAWatsCaO00+HUmyX9cFLm4Vz7n
+caILyQ6CxZBlLgRIgDHxADMCgYEAtubsJGTHmZBmSCStpXLUWbOBLNQqfTM398DZ
+QoirP1PsUQ+IGUfSG/u+QCogR6fPEBkXeFHxsoY/Cvsm2lvYaKgK1VFn46Xm2vNq
+JuIH4pZCqp6LAv4weddZslT0a5eaowRSZ4o7PmTAaRuCXvD3VjTSJwhJFMo+90TV
+vEWn7gkCgYEAkk+unX9kYmKoUdLh22/tzQekBa8WqMxXDwzBCECTAs2GlpL/f73i
+zD15TnaNfLP6Q5RNb0N9tb0Gz1wSkwI1+jGAQLnh2K9X9cIVIqJn8Mf/KQa/wUDV
+Tb1j7FoGUEgX7vbsyWuTd8P76kNYyGqCss1XmbttcSolqpbIdlSUcO0=
+-----END RSA PRIVATE KEY-----
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/tornado/tornado-server.py b/testing/web-platform/tests/tools/third_party/h2/examples/tornado/tornado-server.py
new file mode 100755
index 0000000000..e7d08ab191
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/tornado/tornado-server.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+tornado-server.py
+~~~~~~~~~~~~~~~~~
+
+A fully-functional HTTP/2 server written for Tornado.
+"""
+import collections
+import json
+import ssl
+
+import tornado.gen
+import tornado.ioloop
+import tornado.iostream
+import tornado.tcpserver
+
+from h2.config import H2Configuration
+from h2.connection import H2Connection
+from h2.events import RequestReceived, DataReceived
+
+
+def create_ssl_context(certfile, keyfile):
+ ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ ssl_context.options |= (
+ ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION
+ )
+ ssl_context.set_ciphers("ECDHE+AESGCM")
+ ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile)
+ ssl_context.set_alpn_protocols(["h2"])
+ return ssl_context
+
+
+class H2Server(tornado.tcpserver.TCPServer):
+
+ @tornado.gen.coroutine
+ def handle_stream(self, stream, address):
+ handler = EchoHeadersHandler(stream)
+ yield handler.handle()
+
+
+class EchoHeadersHandler(object):
+
+ def __init__(self, stream):
+ self.stream = stream
+
+ config = H2Configuration(client_side=False)
+ self.conn = H2Connection(config=config)
+
+ @tornado.gen.coroutine
+ def handle(self):
+ self.conn.initiate_connection()
+ yield self.stream.write(self.conn.data_to_send())
+
+ while True:
+ try:
+ data = yield self.stream.read_bytes(65535, partial=True)
+ if not data:
+ break
+
+ events = self.conn.receive_data(data)
+ for event in events:
+ if isinstance(event, RequestReceived):
+ self.request_received(event.headers, event.stream_id)
+ elif isinstance(event, DataReceived):
+ self.conn.reset_stream(event.stream_id)
+
+ yield self.stream.write(self.conn.data_to_send())
+
+ except tornado.iostream.StreamClosedError:
+ break
+
+ def request_received(self, headers, stream_id):
+ headers = collections.OrderedDict(headers)
+ data = json.dumps({'headers': headers}, indent=4).encode('utf-8')
+
+ response_headers = (
+ (':status', '200'),
+ ('content-type', 'application/json'),
+ ('content-length', str(len(data))),
+ ('server', 'tornado-h2'),
+ )
+ self.conn.send_headers(stream_id, response_headers)
+ self.conn.send_data(stream_id, data, end_stream=True)
+
+
+if __name__ == '__main__':
+ ssl_context = create_ssl_context('server.crt', 'server.key')
+ server = H2Server(ssl_options=ssl_context)
+ server.listen(8888)
+ io_loop = tornado.ioloop.IOLoop.current()
+ io_loop.start()
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/twisted/head_request.py b/testing/web-platform/tests/tools/third_party/h2/examples/twisted/head_request.py
new file mode 100644
index 0000000000..4a7538024a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/twisted/head_request.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+"""
+head_request.py
+~~~~~~~~~~~~~~~
+
+A short example that demonstrates a client that makes HEAD requests to certain
+websites.
+
+This example is intended as a reproduction of nghttp2 issue 396, for the
+purposes of compatibility testing.
+"""
+from __future__ import print_function
+
+from twisted.internet import reactor
+from twisted.internet.endpoints import connectProtocol, SSL4ClientEndpoint
+from twisted.internet.protocol import Protocol
+from twisted.internet.ssl import optionsForClientTLS
+from hyperframe.frame import SettingsFrame
+from h2.connection import H2Connection
+from h2.events import (
+ ResponseReceived, DataReceived, StreamEnded,
+ StreamReset, SettingsAcknowledged,
+)
+
+
+AUTHORITY = u'nghttp2.org'
+PATH = '/httpbin/'
+SIZE = 4096
+
+
+class H2Protocol(Protocol):
+ def __init__(self):
+ self.conn = H2Connection()
+ self.known_proto = None
+ self.request_made = False
+
+ def connectionMade(self):
+ self.conn.initiate_connection()
+
+ # This reproduces the error in #396, by changing the header table size.
+ self.conn.update_settings({SettingsFrame.HEADER_TABLE_SIZE: SIZE})
+
+ self.transport.write(self.conn.data_to_send())
+
+ def dataReceived(self, data):
+ if not self.known_proto:
+ self.known_proto = self.transport.negotiatedProtocol
+ assert self.known_proto == b'h2'
+
+ events = self.conn.receive_data(data)
+
+ for event in events:
+ if isinstance(event, ResponseReceived):
+ self.handleResponse(event.headers, event.stream_id)
+ elif isinstance(event, DataReceived):
+ self.handleData(event.data, event.stream_id)
+ elif isinstance(event, StreamEnded):
+ self.endStream(event.stream_id)
+ elif isinstance(event, SettingsAcknowledged):
+ self.settingsAcked(event)
+ elif isinstance(event, StreamReset):
+ reactor.stop()
+ raise RuntimeError("Stream reset: %d" % event.error_code)
+ else:
+ print(event)
+
+ data = self.conn.data_to_send()
+ if data:
+ self.transport.write(data)
+
+ def settingsAcked(self, event):
+ # Having received the remote settings change, lets send our request.
+ if not self.request_made:
+ self.sendRequest()
+
+ def handleResponse(self, response_headers, stream_id):
+ for name, value in response_headers:
+ print("%s: %s" % (name.decode('utf-8'), value.decode('utf-8')))
+
+ print("")
+
+ def handleData(self, data, stream_id):
+ print(data, end='')
+
+ def endStream(self, stream_id):
+ self.conn.close_connection()
+ self.transport.write(self.conn.data_to_send())
+ self.transport.loseConnection()
+ reactor.stop()
+
+ def sendRequest(self):
+ request_headers = [
+ (':method', 'HEAD'),
+ (':authority', AUTHORITY),
+ (':scheme', 'https'),
+ (':path', PATH),
+ ('user-agent', 'hyper-h2/1.0.0'),
+ ]
+ self.conn.send_headers(1, request_headers, end_stream=True)
+ self.request_made = True
+
+options = optionsForClientTLS(
+ hostname=AUTHORITY,
+ acceptableProtocols=[b'h2'],
+)
+
+connectProtocol(
+ SSL4ClientEndpoint(reactor, AUTHORITY, 443, options),
+ H2Protocol()
+)
+reactor.run()
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/twisted/post_request.py b/testing/web-platform/tests/tools/third_party/h2/examples/twisted/post_request.py
new file mode 100644
index 0000000000..c817bac465
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/twisted/post_request.py
@@ -0,0 +1,249 @@
+# -*- coding: utf-8 -*-
+"""
+post_request.py
+~~~~~~~~~~~~~~~
+
+A short example that demonstrates a client that makes POST requests to certain
+websites.
+
+This example is intended to demonstrate how to handle uploading request bodies.
+In this instance, a file will be uploaded. In order to handle arbitrary files,
+this example also demonstrates how to obey HTTP/2 flow control rules.
+
+Takes one command-line argument: a path to a file in the filesystem to upload.
+If none is present, uploads this file.
+"""
+from __future__ import print_function
+
+import mimetypes
+import os
+import sys
+
+from twisted.internet import reactor, defer
+from twisted.internet.endpoints import connectProtocol, SSL4ClientEndpoint
+from twisted.internet.protocol import Protocol
+from twisted.internet.ssl import optionsForClientTLS
+from h2.connection import H2Connection
+from h2.events import (
+ ResponseReceived, DataReceived, StreamEnded, StreamReset, WindowUpdated,
+ SettingsAcknowledged,
+)
+
+
+AUTHORITY = u'nghttp2.org'
+PATH = '/httpbin/post'
+
+
+class H2Protocol(Protocol):
+ def __init__(self, file_path):
+ self.conn = H2Connection()
+ self.known_proto = None
+ self.request_made = False
+ self.request_complete = False
+ self.file_path = file_path
+ self.flow_control_deferred = None
+ self.fileobj = None
+ self.file_size = None
+
+ def connectionMade(self):
+ """
+ Called by Twisted when the TCP connection is established. We can start
+ sending some data now: we should open with the connection preamble.
+ """
+ self.conn.initiate_connection()
+ self.transport.write(self.conn.data_to_send())
+
+ def dataReceived(self, data):
+ """
+ Called by Twisted when data is received on the connection.
+
+ We need to check a few things here. Firstly, we want to validate that
+ we actually negotiated HTTP/2: if we didn't, we shouldn't proceed!
+
+ Then, we want to pass the data to the protocol stack and check what
+ events occurred.
+ """
+ if not self.known_proto:
+ self.known_proto = self.transport.negotiatedProtocol
+ assert self.known_proto == b'h2'
+
+ events = self.conn.receive_data(data)
+
+ for event in events:
+ if isinstance(event, ResponseReceived):
+ self.handleResponse(event.headers)
+ elif isinstance(event, DataReceived):
+ self.handleData(event.data)
+ elif isinstance(event, StreamEnded):
+ self.endStream()
+ elif isinstance(event, SettingsAcknowledged):
+ self.settingsAcked(event)
+ elif isinstance(event, StreamReset):
+ reactor.stop()
+ raise RuntimeError("Stream reset: %d" % event.error_code)
+ elif isinstance(event, WindowUpdated):
+ self.windowUpdated(event)
+
+ data = self.conn.data_to_send()
+ if data:
+ self.transport.write(data)
+
+ def settingsAcked(self, event):
+ """
+ Called when the remote party ACKs our settings. We send a SETTINGS
+ frame as part of the preamble, so if we want to be very polite we can
+ wait until the ACK for that frame comes before we start sending our
+ request.
+ """
+ if not self.request_made:
+ self.sendRequest()
+
+ def handleResponse(self, response_headers):
+ """
+ Handle the response by printing the response headers.
+ """
+ for name, value in response_headers:
+ print("%s: %s" % (name.decode('utf-8'), value.decode('utf-8')))
+
+ print("")
+
+ def handleData(self, data):
+ """
+ We handle data that's received by just printing it.
+ """
+ print(data, end='')
+
+ def endStream(self):
+ """
+ We call this when the stream is cleanly ended by the remote peer. That
+ means that the response is complete.
+
+ Because this code only makes a single HTTP/2 request, once we receive
+ the complete response we can safely tear the connection down and stop
+ the reactor. We do that as cleanly as possible.
+ """
+ self.request_complete = True
+ self.conn.close_connection()
+ self.transport.write(self.conn.data_to_send())
+ self.transport.loseConnection()
+
+ def windowUpdated(self, event):
+ """
+ We call this when the flow control window for the connection or the
+ stream has been widened. If there's a flow control deferred present
+ (that is, if we're blocked behind the flow control), we fire it.
+ Otherwise, we do nothing.
+ """
+ if self.flow_control_deferred is None:
+ return
+
+ # Make sure we remove the flow control deferred to avoid firing it
+ # more than once.
+ flow_control_deferred = self.flow_control_deferred
+ self.flow_control_deferred = None
+ flow_control_deferred.callback(None)
+
+ def connectionLost(self, reason=None):
+ """
+ Called by Twisted when the connection is gone. Regardless of whether
+ it was clean or not, we want to stop the reactor.
+ """
+ if self.fileobj is not None:
+ self.fileobj.close()
+
+ if reactor.running:
+ reactor.stop()
+
+ def sendRequest(self):
+ """
+ Send the POST request.
+
+ A POST request is made up of one headers frame, and then 0+ data
+ frames. This method begins by sending the headers, and then starts a
+ series of calls to send data.
+ """
+ # First, we need to work out how large the file is.
+ self.file_size = os.stat(self.file_path).st_size
+
+ # Next, we want to guess a content-type and content-encoding.
+ content_type, content_encoding = mimetypes.guess_type(self.file_path)
+
+ # Now we can build a header block.
+ request_headers = [
+ (':method', 'POST'),
+ (':authority', AUTHORITY),
+ (':scheme', 'https'),
+ (':path', PATH),
+ ('user-agent', 'hyper-h2/1.0.0'),
+ ('content-length', str(self.file_size)),
+ ]
+
+ if content_type is not None:
+ request_headers.append(('content-type', content_type))
+
+ if content_encoding is not None:
+ request_headers.append(('content-encoding', content_encoding))
+
+ self.conn.send_headers(1, request_headers)
+ self.request_made = True
+
+ # We can now open the file.
+ self.fileobj = open(self.file_path, 'rb')
+
+ # We now need to send all the relevant data. We do this by checking
+ # what the acceptable amount of data is to send, and sending it. If we
+ # find ourselves blocked behind flow control, we then place a deferred
+ # and wait until that deferred fires.
+ self.sendFileData()
+
+ def sendFileData(self):
+ """
+ Send some file data on the connection.
+ """
+ # Firstly, check what the flow control window is for stream 1.
+ window_size = self.conn.local_flow_control_window(stream_id=1)
+
+ # Next, check what the maximum frame size is.
+ max_frame_size = self.conn.max_outbound_frame_size
+
+ # We will send no more than the window size or the remaining file size
+ # of data in this call, whichever is smaller.
+ bytes_to_send = min(window_size, self.file_size)
+
+ # We now need to send a number of data frames.
+ while bytes_to_send > 0:
+ chunk_size = min(bytes_to_send, max_frame_size)
+ data_chunk = self.fileobj.read(chunk_size)
+ self.conn.send_data(stream_id=1, data=data_chunk)
+
+ bytes_to_send -= chunk_size
+ self.file_size -= chunk_size
+
+ # We've prepared a whole chunk of data to send. If the file is fully
+ # sent, we also want to end the stream: we're done here.
+ if self.file_size == 0:
+ self.conn.end_stream(stream_id=1)
+ else:
+ # We've still got data left to send but the window is closed. Save
+ # a Deferred that will call us when the window gets opened.
+ self.flow_control_deferred = defer.Deferred()
+ self.flow_control_deferred.addCallback(self.sendFileData)
+
+ self.transport.write(self.conn.data_to_send())
+
+
+try:
+ filename = sys.argv[1]
+except IndexError:
+ filename = __file__
+
+options = optionsForClientTLS(
+ hostname=AUTHORITY,
+ acceptableProtocols=[b'h2'],
+)
+
+connectProtocol(
+ SSL4ClientEndpoint(reactor, AUTHORITY, 443, options),
+ H2Protocol(filename)
+)
+reactor.run()
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/twisted/server.crt b/testing/web-platform/tests/tools/third_party/h2/examples/twisted/server.crt
new file mode 100644
index 0000000000..bc8a4c08d2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/twisted/server.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDUjCCAjoCCQCQmNzzpQTCijANBgkqhkiG9w0BAQUFADBrMQswCQYDVQQGEwJH
+QjEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xETAPBgNVBAoTCGh5
+cGVyLWgyMREwDwYDVQQLEwhoeXBleS1oMjEUMBIGA1UEAxMLZXhhbXBsZS5jb20w
+HhcNMTUwOTE2MjAyOTA0WhcNMTYwOTE1MjAyOTA0WjBrMQswCQYDVQQGEwJHQjEP
+MA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xETAPBgNVBAoTCGh5cGVy
+LWgyMREwDwYDVQQLEwhoeXBleS1oMjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC74ZeB4Jdb5cnC9KXXLJuzjwTg
+45q5EeShDYQe0TbKgreiUP6clU3BR0fFAVedN1q/LOuQ1HhvrDk1l4TfGF2bpCIq
+K+U9CnzcQknvdpyyVeOLtSsCjOPk4xydHwkQxwJvHVdtJx4CzDDqGbHNHCF/9gpQ
+lsa3JZW+tIZLK0XMEPFQ4XFXgegxTStO7kBBPaVIgG9Ooqc2MG4rjMNUpxa28WF1
+SyqWTICf2N8T/C+fPzbQLKCWrFrKUP7WQlOaqPNQL9bCDhSTPRTwQOc2/MzVZ9gT
+Xr0Z+JMTXwkSMKO52adE1pmKt00jJ1ecZBiJFyjx0X6hH+/59dLbG/7No+PzAgMB
+AAEwDQYJKoZIhvcNAQEFBQADggEBAG3UhOCa0EemL2iY+C+PR6CwEHQ+n7vkBzNz
+gKOG+Q39spyzqU1qJAzBxLTE81bIQbDg0R8kcLWHVH2y4zViRxZ0jHUFKMgjONW+
+Aj4evic/2Y/LxpLxCajECq/jeMHYrmQONszf9pbc0+exrQpgnwd8asfsM3d/FJS2
+5DIWryCKs/61m9vYL8icWx/9cnfPkBoNv1ER+V1L1TH3ARvABh406SBaeqLTm/kG
+MNuKytKWJsQbNlxzWHVgkKzVsBKvYj0uIEJpClIhbe6XNYRDy8T8mKXVWhJuxH4p
+/agmCG3nxO8aCrUK/EVmbWmVIfCH3t7jlwMX1nJ8MsRE7Ydnk8I=
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/twisted/server.csr b/testing/web-platform/tests/tools/third_party/h2/examples/twisted/server.csr
new file mode 100644
index 0000000000..cadb53a512
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/twisted/server.csr
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICsDCCAZgCAQAwazELMAkGA1UEBhMCR0IxDzANBgNVBAgTBkxvbmRvbjEPMA0G
+A1UEBxMGTG9uZG9uMREwDwYDVQQKEwhoeXBlci1oMjERMA8GA1UECxMIaHlwZXkt
+aDIxFDASBgNVBAMTC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAu+GXgeCXW+XJwvSl1yybs48E4OOauRHkoQ2EHtE2yoK3olD+nJVN
+wUdHxQFXnTdavyzrkNR4b6w5NZeE3xhdm6QiKivlPQp83EJJ73acslXji7UrAozj
+5OMcnR8JEMcCbx1XbSceAsww6hmxzRwhf/YKUJbGtyWVvrSGSytFzBDxUOFxV4Ho
+MU0rTu5AQT2lSIBvTqKnNjBuK4zDVKcWtvFhdUsqlkyAn9jfE/wvnz820Cyglqxa
+ylD+1kJTmqjzUC/Wwg4Ukz0U8EDnNvzM1WfYE169GfiTE18JEjCjudmnRNaZirdN
+IydXnGQYiRco8dF+oR/v+fXS2xv+zaPj8wIDAQABoAAwDQYJKoZIhvcNAQEFBQAD
+ggEBACZpSoZWxHU5uagpM2Vinh2E7CXiMAlBc6NXhQMD/3fycr9sX4d/+y9Gy3bL
+OfEOHBPlQVGrt05aiTh7m5s3HQfsH8l3RfKpfzCfoqd2ESVwgB092bJwY9fBnkw/
+UzIHvSnlaKc78h+POUoATOb4faQ8P04wzJHzckbCDI8zRzBZTMVGuiWUopq7K5Ce
+VSesbqHHnW9ob/apigKNE0k7et/28NOXNEP90tTsz98yN3TP+Nv9puwvT9JZOOoG
+0PZIQKJIaZ1NZoNQHLN9gXz012XWa99cBE0qNiBUugXlNhXjkIIM8FIhDQOREB18
+0KDxEma+A0quyjnDMwPSoZsMca4=
+-----END CERTIFICATE REQUEST-----
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/twisted/server.key b/testing/web-platform/tests/tools/third_party/h2/examples/twisted/server.key
new file mode 100644
index 0000000000..11f9ea094b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/twisted/server.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAu+GXgeCXW+XJwvSl1yybs48E4OOauRHkoQ2EHtE2yoK3olD+
+nJVNwUdHxQFXnTdavyzrkNR4b6w5NZeE3xhdm6QiKivlPQp83EJJ73acslXji7Ur
+Aozj5OMcnR8JEMcCbx1XbSceAsww6hmxzRwhf/YKUJbGtyWVvrSGSytFzBDxUOFx
+V4HoMU0rTu5AQT2lSIBvTqKnNjBuK4zDVKcWtvFhdUsqlkyAn9jfE/wvnz820Cyg
+lqxaylD+1kJTmqjzUC/Wwg4Ukz0U8EDnNvzM1WfYE169GfiTE18JEjCjudmnRNaZ
+irdNIydXnGQYiRco8dF+oR/v+fXS2xv+zaPj8wIDAQABAoIBAQCsdq278+0c13d4
+tViSh4k5r1w8D9IUdp9XU2/nVgckqA9nOVAvbkJc3FC+P7gsQgbUHKj0XoVbhU1S
+q461t8kduPH/oiGhAcKR8WurHEdE0OC6ewhLJAeCMRQwCrAorXXHh7icIt9ClCuG
+iSWUcXEy5Cidx3oL3r1xvIbV85fzdDtE9RC1I/kMjAy63S47YGiqh5vYmJkCa8rG
+Dsd1sEMDPr63XJpqJj3uHRcPvySgXTa+ssTmUH8WJlPTjvDB5hnPz+lkk2JKVPNu
+8adzftZ6hSun+tsc4ZJp8XhGu/m/7MjxWh8MeupLHlXcOEsnj4uHQQsOM3zHojr3
+aDCZiC1pAoGBAOAhwe1ujoS2VJ5RXJ9KMs7eBER/02MDgWZjo54Jv/jFxPWGslKk
+QQceuTe+PruRm41nzvk3q4iZXt8pG0bvpgigN2epcVx/O2ouRsUWWBT0JrVlEzha
+TIvWjtZ5tSQExXgHL3VlM9+ka40l+NldLSPn25+prizaqhalWuvTpP23AoGBANaY
+VhEI6yhp0BBUSATEv9lRgkwx3EbcnXNXPQjDMOthsyfq7FxbdOBEK1rwSDyuE6Ij
+zQGcTOfdiur5Ttg0OQilTJIXJAlpoeecOQ9yGma08c5FMXVJJvcZUuWRZWg1ocQj
+/hx0WVE9NwOoKwTBERv8HX7vJOFRZyvgkJwFxoulAoGAe4m/1XoZrga9z2GzNs10
+AdgX7BW00x+MhH4pIiPnn1yK+nYa9jg4647Asnv3IfXZEnEEgRNxReKbi0+iDFBt
+aNW+lDGuHTi37AfD1EBDnpEQgO1MUcRb6rwBkTAWatsCaO00+HUmyX9cFLm4Vz7n
+caILyQ6CxZBlLgRIgDHxADMCgYEAtubsJGTHmZBmSCStpXLUWbOBLNQqfTM398DZ
+QoirP1PsUQ+IGUfSG/u+QCogR6fPEBkXeFHxsoY/Cvsm2lvYaKgK1VFn46Xm2vNq
+JuIH4pZCqp6LAv4weddZslT0a5eaowRSZ4o7PmTAaRuCXvD3VjTSJwhJFMo+90TV
+vEWn7gkCgYEAkk+unX9kYmKoUdLh22/tzQekBa8WqMxXDwzBCECTAs2GlpL/f73i
+zD15TnaNfLP6Q5RNb0N9tb0Gz1wSkwI1+jGAQLnh2K9X9cIVIqJn8Mf/KQa/wUDV
+Tb1j7FoGUEgX7vbsyWuTd8P76kNYyGqCss1XmbttcSolqpbIdlSUcO0=
+-----END RSA PRIVATE KEY-----
diff --git a/testing/web-platform/tests/tools/third_party/h2/examples/twisted/twisted-server.py b/testing/web-platform/tests/tools/third_party/h2/examples/twisted/twisted-server.py
new file mode 100644
index 0000000000..75a271d9b7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/examples/twisted/twisted-server.py
@@ -0,0 +1,192 @@
+# -*- coding: utf-8 -*-
+"""
+twisted-server.py
+~~~~~~~~~~~~~~~~~
+
+A fully-functional HTTP/2 server written for Twisted.
+"""
+import functools
+import mimetypes
+import os
+import os.path
+import sys
+
+from OpenSSL import crypto
+from twisted.internet.defer import Deferred, inlineCallbacks
+from twisted.internet.protocol import Protocol, Factory
+from twisted.internet import endpoints, reactor, ssl
+from h2.config import H2Configuration
+from h2.connection import H2Connection
+from h2.events import (
+ RequestReceived, DataReceived, WindowUpdated
+)
+from h2.exceptions import ProtocolError
+
+
+def close_file(file, d):
+ file.close()
+
+
+READ_CHUNK_SIZE = 8192
+
+
+class H2Protocol(Protocol):
+ def __init__(self, root):
+ config = H2Configuration(client_side=False)
+ self.conn = H2Connection(config=config)
+ self.known_proto = None
+ self.root = root
+
+ self._flow_control_deferreds = {}
+
+ def connectionMade(self):
+ self.conn.initiate_connection()
+ self.transport.write(self.conn.data_to_send())
+
+ def dataReceived(self, data):
+ if not self.known_proto:
+ self.known_proto = True
+
+ try:
+ events = self.conn.receive_data(data)
+ except ProtocolError:
+ if self.conn.data_to_send:
+ self.transport.write(self.conn.data_to_send())
+ self.transport.loseConnection()
+ else:
+ for event in events:
+ if isinstance(event, RequestReceived):
+ self.requestReceived(event.headers, event.stream_id)
+ elif isinstance(event, DataReceived):
+ self.dataFrameReceived(event.stream_id)
+ elif isinstance(event, WindowUpdated):
+ self.windowUpdated(event)
+
+ if self.conn.data_to_send:
+ self.transport.write(self.conn.data_to_send())
+
+ def requestReceived(self, headers, stream_id):
+ headers = dict(headers) # Invalid conversion, fix later.
+ assert headers[b':method'] == b'GET'
+
+ path = headers[b':path'].lstrip(b'/')
+ full_path = os.path.join(self.root, path)
+
+ if not os.path.exists(full_path):
+ response_headers = (
+ (':status', '404'),
+ ('content-length', '0'),
+ ('server', 'twisted-h2'),
+ )
+ self.conn.send_headers(
+ stream_id, response_headers, end_stream=True
+ )
+ self.transport.write(self.conn.data_to_send())
+ else:
+ self.sendFile(full_path, stream_id)
+
+ return
+
+ def dataFrameReceived(self, stream_id):
+ self.conn.reset_stream(stream_id)
+ self.transport.write(self.conn.data_to_send())
+
+ def sendFile(self, file_path, stream_id):
+ filesize = os.stat(file_path).st_size
+ content_type, content_encoding = mimetypes.guess_type(
+ file_path.decode('utf-8')
+ )
+ response_headers = [
+ (':status', '200'),
+ ('content-length', str(filesize)),
+ ('server', 'twisted-h2'),
+ ]
+ if content_type:
+ response_headers.append(('content-type', content_type))
+ if content_encoding:
+ response_headers.append(('content-encoding', content_encoding))
+
+ self.conn.send_headers(stream_id, response_headers)
+ self.transport.write(self.conn.data_to_send())
+
+ f = open(file_path, 'rb')
+ d = self._send_file(f, stream_id)
+ d.addErrback(functools.partial(close_file, f))
+
+ def windowUpdated(self, event):
+ """
+ Handle a WindowUpdated event by firing any waiting data sending
+ callbacks.
+ """
+ stream_id = event.stream_id
+
+ if stream_id and stream_id in self._flow_control_deferreds:
+ d = self._flow_control_deferreds.pop(stream_id)
+ d.callback(event.delta)
+ elif not stream_id:
+ for d in self._flow_control_deferreds.values():
+ d.callback(event.delta)
+
+ self._flow_control_deferreds = {}
+
+ return
+
+ @inlineCallbacks
+ def _send_file(self, file, stream_id):
+ """
+ This callback sends more data for a given file on the stream.
+ """
+ keep_reading = True
+ while keep_reading:
+ while not self.conn.remote_flow_control_window(stream_id):
+ yield self.wait_for_flow_control(stream_id)
+
+ chunk_size = min(
+ self.conn.remote_flow_control_window(stream_id), READ_CHUNK_SIZE
+ )
+ data = file.read(chunk_size)
+ keep_reading = len(data) == chunk_size
+ self.conn.send_data(stream_id, data, not keep_reading)
+ self.transport.write(self.conn.data_to_send())
+
+ if not keep_reading:
+ break
+
+ file.close()
+
+ def wait_for_flow_control(self, stream_id):
+ """
+ Returns a Deferred that fires when the flow control window is opened.
+ """
+ d = Deferred()
+ self._flow_control_deferreds[stream_id] = d
+ return d
+
+
+class H2Factory(Factory):
+ def __init__(self, root):
+ self.root = root
+
+ def buildProtocol(self, addr):
+ print(H2Protocol)
+ return H2Protocol(self.root)
+
+
+root = sys.argv[1].encode('utf-8')
+
+with open('server.crt', 'r') as f:
+ cert_data = f.read()
+with open('server.key', 'r') as f:
+ key_data = f.read()
+
+cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_data)
+key = crypto.load_privatekey(crypto.FILETYPE_PEM, key_data)
+options = ssl.CertificateOptions(
+ privateKey=key,
+ certificate=cert,
+ acceptableProtocols=[b'h2'],
+)
+
+endpoint = endpoints.SSL4ServerEndpoint(reactor, 8080, options, backlog=128)
+endpoint.listen(H2Factory(root))
+reactor.run()
diff --git a/testing/web-platform/tests/tools/third_party/h2/h2/__init__.py b/testing/web-platform/tests/tools/third_party/h2/h2/__init__.py
new file mode 100644
index 0000000000..6290710e63
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/h2/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+"""
+h2
+~~
+
+A HTTP/2 implementation.
+"""
+__version__ = '3.2.0'
diff --git a/testing/web-platform/tests/tools/third_party/h2/h2/config.py b/testing/web-platform/tests/tools/third_party/h2/h2/config.py
new file mode 100644
index 0000000000..1c437ee24f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/h2/config.py
@@ -0,0 +1,170 @@
+# -*- coding: utf-8 -*-
+"""
+h2/config
+~~~~~~~~~
+
+Objects for controlling the configuration of the HTTP/2 stack.
+"""
+
+
+class _BooleanConfigOption(object):
+ """
+ Descriptor for handling a boolean config option. This will block
+ attempts to set boolean config options to non-bools.
+ """
+ def __init__(self, name):
+ self.name = name
+ self.attr_name = '_%s' % self.name
+
+ def __get__(self, instance, owner):
+ return getattr(instance, self.attr_name)
+
+ def __set__(self, instance, value):
+ if not isinstance(value, bool):
+ raise ValueError("%s must be a bool" % self.name)
+ setattr(instance, self.attr_name, value)
+
+
+class DummyLogger(object):
+ """
+ An Logger object that does not actual logging, hence a DummyLogger.
+
+ For the class the log operation is merely a no-op. The intent is to avoid
+ conditionals being sprinkled throughout the hyper-h2 code for calls to
+ logging functions when no logger is passed into the corresponding object.
+ """
+ def __init__(self, *vargs):
+ pass
+
+ def debug(self, *vargs, **kwargs):
+ """
+ No-op logging. Only level needed for now.
+ """
+ pass
+
+ def trace(self, *vargs, **kwargs):
+ """
+ No-op logging. Only level needed for now.
+ """
+ pass
+
+
+class H2Configuration(object):
+ """
+ An object that controls the way a single HTTP/2 connection behaves.
+
+ This object allows the users to customize behaviour. In particular, it
+ allows users to enable or disable optional features, or to otherwise handle
+ various unusual behaviours.
+
+ This object has very little behaviour of its own: it mostly just ensures
+ that configuration is self-consistent.
+
+ :param client_side: Whether this object is to be used on the client side of
+ a connection, or on the server side. Affects the logic used by the
+ state machine, the default settings values, the allowable stream IDs,
+ and several other properties. Defaults to ``True``.
+ :type client_side: ``bool``
+
+ :param header_encoding: Controls whether the headers emitted by this object
+ in events are transparently decoded to ``unicode`` strings, and what
+ encoding is used to do that decoding. This defaults to ``None``,
+ meaning that headers will be returned as bytes. To automatically
+ decode headers (that is, to return them as unicode strings), this can
+ be set to the string name of any encoding, e.g. ``'utf-8'``.
+
+ .. versionchanged:: 3.0.0
+ Changed default value from ``'utf-8'`` to ``None``
+
+ :type header_encoding: ``str``, ``False``, or ``None``
+
+ :param validate_outbound_headers: Controls whether the headers emitted
+ by this object are validated against the rules in RFC 7540.
+ Disabling this setting will cause outbound header validation to
+ be skipped, and allow the object to emit headers that may be illegal
+ according to RFC 7540. Defaults to ``True``.
+ :type validate_outbound_headers: ``bool``
+
+ :param normalize_outbound_headers: Controls whether the headers emitted
+ by this object are normalized before sending. Disabling this setting
+ will cause outbound header normalization to be skipped, and allow
+ the object to emit headers that may be illegal according to
+ RFC 7540. Defaults to ``True``.
+ :type normalize_outbound_headers: ``bool``
+
+ :param validate_inbound_headers: Controls whether the headers received
+ by this object are validated against the rules in RFC 7540.
+ Disabling this setting will cause inbound header validation to
+ be skipped, and allow the object to receive headers that may be illegal
+ according to RFC 7540. Defaults to ``True``.
+ :type validate_inbound_headers: ``bool``
+
+ :param normalize_inbound_headers: Controls whether the headers received by
+ this object are normalized according to the rules of RFC 7540.
+ Disabling this setting may lead to hyper-h2 emitting header blocks that
+ some RFCs forbid, e.g. with multiple cookie fields.
+
+ .. versionadded:: 3.0.0
+
+ :type normalize_inbound_headers: ``bool``
+
+ :param logger: A logger that conforms to the requirements for this module,
+ those being no I/O and no context switches, which is needed in order
+ to run in asynchronous operation.
+
+ .. versionadded:: 2.6.0
+
+ :type logger: ``logging.Logger``
+ """
+ client_side = _BooleanConfigOption('client_side')
+ validate_outbound_headers = _BooleanConfigOption(
+ 'validate_outbound_headers'
+ )
+ normalize_outbound_headers = _BooleanConfigOption(
+ 'normalize_outbound_headers'
+ )
+ validate_inbound_headers = _BooleanConfigOption(
+ 'validate_inbound_headers'
+ )
+ normalize_inbound_headers = _BooleanConfigOption(
+ 'normalize_inbound_headers'
+ )
+
+ def __init__(self,
+ client_side=True,
+ header_encoding=None,
+ validate_outbound_headers=True,
+ normalize_outbound_headers=True,
+ validate_inbound_headers=True,
+ normalize_inbound_headers=True,
+ logger=None):
+ self.client_side = client_side
+ self.header_encoding = header_encoding
+ self.validate_outbound_headers = validate_outbound_headers
+ self.normalize_outbound_headers = normalize_outbound_headers
+ self.validate_inbound_headers = validate_inbound_headers
+ self.normalize_inbound_headers = normalize_inbound_headers
+ self.logger = logger or DummyLogger(__name__)
+
+ @property
+ def header_encoding(self):
+ """
+ Controls whether the headers emitted by this object in events are
+ transparently decoded to ``unicode`` strings, and what encoding is used
+ to do that decoding. This defaults to ``None``, meaning that headers
+ will be returned as bytes. To automatically decode headers (that is, to
+ return them as unicode strings), this can be set to the string name of
+ any encoding, e.g. ``'utf-8'``.
+ """
+ return self._header_encoding
+
+ @header_encoding.setter
+ def header_encoding(self, value):
+ """
+ Enforces constraints on the value of header encoding.
+ """
+ if not isinstance(value, (bool, str, type(None))):
+ raise ValueError("header_encoding must be bool, string, or None")
+ if value is True:
+ raise ValueError("header_encoding cannot be True")
+ self._header_encoding = value
diff --git a/testing/web-platform/tests/tools/third_party/h2/h2/connection.py b/testing/web-platform/tests/tools/third_party/h2/h2/connection.py
new file mode 100644
index 0000000000..35e6fd6da8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/h2/connection.py
@@ -0,0 +1,2048 @@
+# -*- coding: utf-8 -*-
+"""
+h2/connection
+~~~~~~~~~~~~~
+
+An implementation of a HTTP/2 connection.
+"""
+import base64
+
+from enum import Enum, IntEnum
+
+from hyperframe.exceptions import InvalidPaddingError
+from hyperframe.frame import (
+ GoAwayFrame, WindowUpdateFrame, HeadersFrame, DataFrame, PingFrame,
+ PushPromiseFrame, SettingsFrame, RstStreamFrame, PriorityFrame,
+ ContinuationFrame, AltSvcFrame, ExtensionFrame
+)
+from hpack.hpack import Encoder, Decoder
+from hpack.exceptions import HPACKError, OversizedHeaderListError
+
+from .config import H2Configuration
+from .errors import ErrorCodes, _error_code_from_int
+from .events import (
+ WindowUpdated, RemoteSettingsChanged, PingReceived, PingAckReceived,
+ SettingsAcknowledged, ConnectionTerminated, PriorityUpdated,
+ AlternativeServiceAvailable, UnknownFrameReceived
+)
+from .exceptions import (
+ ProtocolError, NoSuchStreamError, FlowControlError, FrameTooLargeError,
+ TooManyStreamsError, StreamClosedError, StreamIDTooLowError,
+ NoAvailableStreamIDError, RFC1122Error, DenialOfServiceError
+)
+from .frame_buffer import FrameBuffer
+from .settings import Settings, SettingCodes
+from .stream import H2Stream, StreamClosedBy
+from .utilities import SizeLimitDict, guard_increment_window
+from .windows import WindowManager
+
+
+class ConnectionState(Enum):
+ IDLE = 0
+ CLIENT_OPEN = 1
+ SERVER_OPEN = 2
+ CLOSED = 3
+
+
+class ConnectionInputs(Enum):
+ SEND_HEADERS = 0
+ SEND_PUSH_PROMISE = 1
+ SEND_DATA = 2
+ SEND_GOAWAY = 3
+ SEND_WINDOW_UPDATE = 4
+ SEND_PING = 5
+ SEND_SETTINGS = 6
+ SEND_RST_STREAM = 7
+ SEND_PRIORITY = 8
+ RECV_HEADERS = 9
+ RECV_PUSH_PROMISE = 10
+ RECV_DATA = 11
+ RECV_GOAWAY = 12
+ RECV_WINDOW_UPDATE = 13
+ RECV_PING = 14
+ RECV_SETTINGS = 15
+ RECV_RST_STREAM = 16
+ RECV_PRIORITY = 17
+ SEND_ALTERNATIVE_SERVICE = 18 # Added in 2.3.0
+ RECV_ALTERNATIVE_SERVICE = 19 # Added in 2.3.0
+
+
+class AllowedStreamIDs(IntEnum):
+ EVEN = 0
+ ODD = 1
+
+
+class H2ConnectionStateMachine(object):
+ """
+ A single HTTP/2 connection state machine.
+
+ This state machine, while defined in its own class, is logically part of
+ the H2Connection class also defined in this file. The state machine itself
+ maintains very little state directly, instead focusing entirely on managing
+ state transitions.
+ """
+ # For the purposes of this state machine we treat HEADERS and their
+ # associated CONTINUATION frames as a single jumbo frame. The protocol
+ # allows/requires this by preventing other frames from being interleved in
+ # between HEADERS/CONTINUATION frames.
+ #
+ # The _transitions dictionary contains a mapping of tuples of
+ # (state, input) to tuples of (side_effect_function, end_state). This map
+ # contains all allowed transitions: anything not in this map is invalid
+ # and immediately causes a transition to ``closed``.
+
+ _transitions = {
+ # State: idle
+ (ConnectionState.IDLE, ConnectionInputs.SEND_HEADERS):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.IDLE, ConnectionInputs.RECV_HEADERS):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.IDLE, ConnectionInputs.SEND_SETTINGS):
+ (None, ConnectionState.IDLE),
+ (ConnectionState.IDLE, ConnectionInputs.RECV_SETTINGS):
+ (None, ConnectionState.IDLE),
+ (ConnectionState.IDLE, ConnectionInputs.SEND_WINDOW_UPDATE):
+ (None, ConnectionState.IDLE),
+ (ConnectionState.IDLE, ConnectionInputs.RECV_WINDOW_UPDATE):
+ (None, ConnectionState.IDLE),
+ (ConnectionState.IDLE, ConnectionInputs.SEND_PING):
+ (None, ConnectionState.IDLE),
+ (ConnectionState.IDLE, ConnectionInputs.RECV_PING):
+ (None, ConnectionState.IDLE),
+ (ConnectionState.IDLE, ConnectionInputs.SEND_GOAWAY):
+ (None, ConnectionState.CLOSED),
+ (ConnectionState.IDLE, ConnectionInputs.RECV_GOAWAY):
+ (None, ConnectionState.CLOSED),
+ (ConnectionState.IDLE, ConnectionInputs.SEND_PRIORITY):
+ (None, ConnectionState.IDLE),
+ (ConnectionState.IDLE, ConnectionInputs.RECV_PRIORITY):
+ (None, ConnectionState.IDLE),
+ (ConnectionState.IDLE, ConnectionInputs.SEND_ALTERNATIVE_SERVICE):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.IDLE, ConnectionInputs.RECV_ALTERNATIVE_SERVICE):
+ (None, ConnectionState.CLIENT_OPEN),
+
+ # State: open, client side.
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_HEADERS):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_DATA):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_GOAWAY):
+ (None, ConnectionState.CLOSED),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_WINDOW_UPDATE):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_PING):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_SETTINGS):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_PRIORITY):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_HEADERS):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_PUSH_PROMISE):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_DATA):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_GOAWAY):
+ (None, ConnectionState.CLOSED),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_WINDOW_UPDATE):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_PING):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_SETTINGS):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_RST_STREAM):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_RST_STREAM):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_PRIORITY):
+ (None, ConnectionState.CLIENT_OPEN),
+ (ConnectionState.CLIENT_OPEN,
+ ConnectionInputs.RECV_ALTERNATIVE_SERVICE):
+ (None, ConnectionState.CLIENT_OPEN),
+
+ # State: open, server side.
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_HEADERS):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_PUSH_PROMISE):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_DATA):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_GOAWAY):
+ (None, ConnectionState.CLOSED),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_WINDOW_UPDATE):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_PING):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_SETTINGS):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_PRIORITY):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_HEADERS):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_DATA):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_GOAWAY):
+ (None, ConnectionState.CLOSED),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_WINDOW_UPDATE):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_PING):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_SETTINGS):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_PRIORITY):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_RST_STREAM):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_RST_STREAM):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN,
+ ConnectionInputs.SEND_ALTERNATIVE_SERVICE):
+ (None, ConnectionState.SERVER_OPEN),
+ (ConnectionState.SERVER_OPEN,
+ ConnectionInputs.RECV_ALTERNATIVE_SERVICE):
+ (None, ConnectionState.SERVER_OPEN),
+
+ # State: closed
+ (ConnectionState.CLOSED, ConnectionInputs.SEND_GOAWAY):
+ (None, ConnectionState.CLOSED),
+ (ConnectionState.CLOSED, ConnectionInputs.RECV_GOAWAY):
+ (None, ConnectionState.CLOSED),
+ }
+
+ def __init__(self):
+ self.state = ConnectionState.IDLE
+
+ def process_input(self, input_):
+ """
+ Process a specific input in the state machine.
+ """
+ if not isinstance(input_, ConnectionInputs):
+ raise ValueError("Input must be an instance of ConnectionInputs")
+
+ try:
+ func, target_state = self._transitions[(self.state, input_)]
+ except KeyError:
+ old_state = self.state
+ self.state = ConnectionState.CLOSED
+ raise ProtocolError(
+ "Invalid input %s in state %s" % (input_, old_state)
+ )
+ else:
+ self.state = target_state
+ if func is not None: # pragma: no cover
+ return func()
+
+ return []
+
+
+class H2Connection(object):
+ """
+ A low-level HTTP/2 connection object. This handles building and receiving
+ frames and maintains both connection and per-stream state for all streams
+ on this connection.
+
+ This wraps a HTTP/2 Connection state machine implementation, ensuring that
+ frames can only be sent/received when the connection is in a valid state.
+ It also builds stream state machines on demand to ensure that the
+ constraints of those state machines are met as well. Attempts to create
+ frames that cannot be sent will raise a ``ProtocolError``.
+
+ .. versionchanged:: 2.3.0
+ Added the ``header_encoding`` keyword argument.
+
+ .. versionchanged:: 2.5.0
+ Added the ``config`` keyword argument. Deprecated the ``client_side``
+ and ``header_encoding`` parameters.
+
+ .. versionchanged:: 3.0.0
+ Removed deprecated parameters and properties.
+
+ :param config: The configuration for the HTTP/2 connection.
+
+ .. versionadded:: 2.5.0
+
+ :type config: :class:`H2Configuration <h2.config.H2Configuration>`
+ """
+ # The initial maximum outbound frame size. This can be changed by receiving
+ # a settings frame.
+ DEFAULT_MAX_OUTBOUND_FRAME_SIZE = 65535
+
+ # The initial maximum inbound frame size. This is somewhat arbitrarily
+ # chosen.
+ DEFAULT_MAX_INBOUND_FRAME_SIZE = 2**24
+
+ # The highest acceptable stream ID.
+ HIGHEST_ALLOWED_STREAM_ID = 2**31 - 1
+
+ # The largest acceptable window increment.
+ MAX_WINDOW_INCREMENT = 2**31 - 1
+
+ # The initial default value of SETTINGS_MAX_HEADER_LIST_SIZE.
+ DEFAULT_MAX_HEADER_LIST_SIZE = 2**16
+
+ # Keep in memory limited amount of results for streams closes
+ MAX_CLOSED_STREAMS = 2**16
+
+ def __init__(self, config=None):
+ self.state_machine = H2ConnectionStateMachine()
+ self.streams = {}
+ self.highest_inbound_stream_id = 0
+ self.highest_outbound_stream_id = 0
+ self.encoder = Encoder()
+ self.decoder = Decoder()
+
+ # This won't always actually do anything: for versions of HPACK older
+ # than 2.3.0 it does nothing. However, we have to try!
+ self.decoder.max_header_list_size = self.DEFAULT_MAX_HEADER_LIST_SIZE
+
+ #: The configuration for this HTTP/2 connection object.
+ #:
+ #: .. versionadded:: 2.5.0
+ self.config = config
+ if self.config is None:
+ self.config = H2Configuration(
+ client_side=True,
+ )
+
+ # Objects that store settings, including defaults.
+ #
+ # We set the MAX_CONCURRENT_STREAMS value to 100 because its default is
+ # unbounded, and that's a dangerous default because it allows
+ # essentially unbounded resources to be allocated regardless of how
+ # they will be used. 100 should be suitable for the average
+ # application. This default obviously does not apply to the remote
+ # peer's settings: the remote peer controls them!
+ #
+ # We also set MAX_HEADER_LIST_SIZE to a reasonable value. This is to
+ # advertise our defence against CVE-2016-6581. However, not all
+ # versions of HPACK will let us do it. That's ok: we should at least
+ # suggest that we're not vulnerable.
+ self.local_settings = Settings(
+ client=self.config.client_side,
+ initial_values={
+ SettingCodes.MAX_CONCURRENT_STREAMS: 100,
+ SettingCodes.MAX_HEADER_LIST_SIZE:
+ self.DEFAULT_MAX_HEADER_LIST_SIZE,
+ }
+ )
+ self.remote_settings = Settings(client=not self.config.client_side)
+
+ # The current value of the connection flow control windows on the
+ # connection.
+ self.outbound_flow_control_window = (
+ self.remote_settings.initial_window_size
+ )
+
+ #: The maximum size of a frame that can be emitted by this peer, in
+ #: bytes.
+ self.max_outbound_frame_size = self.remote_settings.max_frame_size
+
+ #: The maximum size of a frame that can be received by this peer, in
+ #: bytes.
+ self.max_inbound_frame_size = self.local_settings.max_frame_size
+
+ # Buffer for incoming data.
+ self.incoming_buffer = FrameBuffer(server=not self.config.client_side)
+
+ # A private variable to store a sequence of received header frames
+ # until completion.
+ self._header_frames = []
+
+ # Data that needs to be sent.
+ self._data_to_send = bytearray()
+
+ # Keeps track of how streams are closed.
+ # Used to ensure that we don't blow up in the face of frames that were
+ # in flight when a RST_STREAM was sent.
+ # Also used to determine whether we should consider a frame received
+ # while a stream is closed as either a stream error or a connection
+ # error.
+ self._closed_streams = SizeLimitDict(
+ size_limit=self.MAX_CLOSED_STREAMS
+ )
+
+ # The flow control window manager for the connection.
+ self._inbound_flow_control_window_manager = WindowManager(
+ max_window_size=self.local_settings.initial_window_size
+ )
+
+ # When in doubt use dict-dispatch.
+ self._frame_dispatch_table = {
+ HeadersFrame: self._receive_headers_frame,
+ PushPromiseFrame: self._receive_push_promise_frame,
+ SettingsFrame: self._receive_settings_frame,
+ DataFrame: self._receive_data_frame,
+ WindowUpdateFrame: self._receive_window_update_frame,
+ PingFrame: self._receive_ping_frame,
+ RstStreamFrame: self._receive_rst_stream_frame,
+ PriorityFrame: self._receive_priority_frame,
+ GoAwayFrame: self._receive_goaway_frame,
+ ContinuationFrame: self._receive_naked_continuation,
+ AltSvcFrame: self._receive_alt_svc_frame,
+ ExtensionFrame: self._receive_unknown_frame
+ }
+
+ def _prepare_for_sending(self, frames):
+ if not frames:
+ return
+ self._data_to_send += b''.join(f.serialize() for f in frames)
+ assert all(f.body_len <= self.max_outbound_frame_size for f in frames)
+
+ def _open_streams(self, remainder):
+ """
+ A common method of counting number of open streams. Returns the number
+ of streams that are open *and* that have (stream ID % 2) == remainder.
+ While it iterates, also deletes any closed streams.
+ """
+ count = 0
+ to_delete = []
+
+ for stream_id, stream in self.streams.items():
+ if stream.open and (stream_id % 2 == remainder):
+ count += 1
+ elif stream.closed:
+ to_delete.append(stream_id)
+
+ for stream_id in to_delete:
+ stream = self.streams.pop(stream_id)
+ self._closed_streams[stream_id] = stream.closed_by
+
+ return count
+
+ @property
+ def open_outbound_streams(self):
+ """
+ The current number of open outbound streams.
+ """
+ outbound_numbers = int(self.config.client_side)
+ return self._open_streams(outbound_numbers)
+
+ @property
+ def open_inbound_streams(self):
+ """
+ The current number of open inbound streams.
+ """
+ inbound_numbers = int(not self.config.client_side)
+ return self._open_streams(inbound_numbers)
+
+ @property
+ def inbound_flow_control_window(self):
+ """
+ The size of the inbound flow control window for the connection. This is
+ rarely publicly useful: instead, use :meth:`remote_flow_control_window
+ <h2.connection.H2Connection.remote_flow_control_window>`. This
+ shortcut is largely present to provide a shortcut to this data.
+ """
+ return self._inbound_flow_control_window_manager.current_window_size
+
+ def _begin_new_stream(self, stream_id, allowed_ids):
+ """
+ Initiate a new stream.
+
+ .. versionchanged:: 2.0.0
+ Removed this function from the public API.
+
+ :param stream_id: The ID of the stream to open.
+ :param allowed_ids: What kind of stream ID is allowed.
+ """
+ self.config.logger.debug(
+ "Attempting to initiate stream ID %d", stream_id
+ )
+ outbound = self._stream_id_is_outbound(stream_id)
+ highest_stream_id = (
+ self.highest_outbound_stream_id if outbound else
+ self.highest_inbound_stream_id
+ )
+
+ if stream_id <= highest_stream_id:
+ raise StreamIDTooLowError(stream_id, highest_stream_id)
+
+ if (stream_id % 2) != int(allowed_ids):
+ raise ProtocolError(
+ "Invalid stream ID for peer."
+ )
+
+ s = H2Stream(
+ stream_id,
+ config=self.config,
+ inbound_window_size=self.local_settings.initial_window_size,
+ outbound_window_size=self.remote_settings.initial_window_size
+ )
+ self.config.logger.debug("Stream ID %d created", stream_id)
+ s.max_inbound_frame_size = self.max_inbound_frame_size
+ s.max_outbound_frame_size = self.max_outbound_frame_size
+
+ self.streams[stream_id] = s
+ self.config.logger.debug("Current streams: %s", self.streams.keys())
+
+ if outbound:
+ self.highest_outbound_stream_id = stream_id
+ else:
+ self.highest_inbound_stream_id = stream_id
+
+ return s
+
+ def initiate_connection(self):
+ """
+ Provides any data that needs to be sent at the start of the connection.
+ Must be called for both clients and servers.
+ """
+ self.config.logger.debug("Initializing connection")
+ self.state_machine.process_input(ConnectionInputs.SEND_SETTINGS)
+ if self.config.client_side:
+ preamble = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'
+ else:
+ preamble = b''
+
+ f = SettingsFrame(0)
+ for setting, value in self.local_settings.items():
+ f.settings[setting] = value
+ self.config.logger.debug(
+ "Send Settings frame: %s", self.local_settings
+ )
+
+ self._data_to_send += preamble + f.serialize()
+
+ def initiate_upgrade_connection(self, settings_header=None):
+ """
+ Call to initialise the connection object for use with an upgraded
+ HTTP/2 connection (i.e. a connection negotiated using the
+ ``Upgrade: h2c`` HTTP header).
+
+ This method differs from :meth:`initiate_connection
+ <h2.connection.H2Connection.initiate_connection>` in several ways.
+ Firstly, it handles the additional SETTINGS frame that is sent in the
+ ``HTTP2-Settings`` header field. When called on a client connection,
+ this method will return a bytestring that the caller can put in the
+ ``HTTP2-Settings`` field they send on their initial request. When
+ called on a server connection, the user **must** provide the value they
+ received from the client in the ``HTTP2-Settings`` header field to the
+ ``settings_header`` argument, which will be used appropriately.
+
+ Additionally, this method sets up stream 1 in a half-closed state
+ appropriate for this side of the connection, to reflect the fact that
+ the request is already complete.
+
+ Finally, this method also prepares the appropriate preamble to be sent
+ after the upgrade.
+
+ .. versionadded:: 2.3.0
+
+ :param settings_header: (optional, server-only): The value of the
+ ``HTTP2-Settings`` header field received from the client.
+ :type settings_header: ``bytes``
+
+ :returns: For clients, a bytestring to put in the ``HTTP2-Settings``.
+ For servers, returns nothing.
+ :rtype: ``bytes`` or ``None``
+ """
+ self.config.logger.debug(
+ "Upgrade connection. Current settings: %s", self.local_settings
+ )
+
+ frame_data = None
+ # Begin by getting the preamble in place.
+ self.initiate_connection()
+
+ if self.config.client_side:
+ f = SettingsFrame(0)
+ for setting, value in self.local_settings.items():
+ f.settings[setting] = value
+
+ frame_data = f.serialize_body()
+ frame_data = base64.urlsafe_b64encode(frame_data)
+ elif settings_header:
+ # We have a settings header from the client. This needs to be
+ # applied, but we want to throw away the ACK. We do this by
+ # inserting the data into a Settings frame and then passing it to
+ # the state machine, but ignoring the return value.
+ settings_header = base64.urlsafe_b64decode(settings_header)
+ f = SettingsFrame(0)
+ f.parse_body(settings_header)
+ self._receive_settings_frame(f)
+
+ # Set up appropriate state. Stream 1 in a half-closed state:
+ # half-closed(local) for clients, half-closed(remote) for servers.
+ # Additionally, we need to set up the Connection state machine.
+ connection_input = (
+ ConnectionInputs.SEND_HEADERS if self.config.client_side
+ else ConnectionInputs.RECV_HEADERS
+ )
+ self.config.logger.debug("Process input %s", connection_input)
+ self.state_machine.process_input(connection_input)
+
+ # Set up stream 1.
+ self._begin_new_stream(stream_id=1, allowed_ids=AllowedStreamIDs.ODD)
+ self.streams[1].upgrade(self.config.client_side)
+ return frame_data
+
+ def _get_or_create_stream(self, stream_id, allowed_ids):
+ """
+ Gets a stream by its stream ID. Will create one if one does not already
+ exist. Use allowed_ids to circumvent the usual stream ID rules for
+ clients and servers.
+
+ .. versionchanged:: 2.0.0
+ Removed this function from the public API.
+ """
+ try:
+ return self.streams[stream_id]
+ except KeyError:
+ return self._begin_new_stream(stream_id, allowed_ids)
+
+ def _get_stream_by_id(self, stream_id):
+ """
+ Gets a stream by its stream ID. Raises NoSuchStreamError if the stream
+ ID does not correspond to a known stream and is higher than the current
+ maximum: raises if it is lower than the current maximum.
+
+ .. versionchanged:: 2.0.0
+ Removed this function from the public API.
+ """
+ try:
+ return self.streams[stream_id]
+ except KeyError:
+ outbound = self._stream_id_is_outbound(stream_id)
+ highest_stream_id = (
+ self.highest_outbound_stream_id if outbound else
+ self.highest_inbound_stream_id
+ )
+
+ if stream_id > highest_stream_id:
+ raise NoSuchStreamError(stream_id)
+ else:
+ raise StreamClosedError(stream_id)
+
+ def get_next_available_stream_id(self):
+ """
+ Returns an integer suitable for use as the stream ID for the next
+ stream created by this endpoint. For server endpoints, this stream ID
+ will be even. For client endpoints, this stream ID will be odd. If no
+ stream IDs are available, raises :class:`NoAvailableStreamIDError
+ <h2.exceptions.NoAvailableStreamIDError>`.
+
+ .. warning:: The return value from this function does not change until
+ the stream ID has actually been used by sending or pushing
+ headers on that stream. For that reason, it should be
+ called as close as possible to the actual use of the
+ stream ID.
+
+ .. versionadded:: 2.0.0
+
+ :raises: :class:`NoAvailableStreamIDError
+ <h2.exceptions.NoAvailableStreamIDError>`
+ :returns: The next free stream ID this peer can use to initiate a
+ stream.
+ :rtype: ``int``
+ """
+ # No streams have been opened yet, so return the lowest allowed stream
+ # ID.
+ if not self.highest_outbound_stream_id:
+ next_stream_id = 1 if self.config.client_side else 2
+ else:
+ next_stream_id = self.highest_outbound_stream_id + 2
+ self.config.logger.debug(
+ "Next available stream ID %d", next_stream_id
+ )
+ if next_stream_id > self.HIGHEST_ALLOWED_STREAM_ID:
+ raise NoAvailableStreamIDError("Exhausted allowed stream IDs")
+
+ return next_stream_id
+
+ def send_headers(self, stream_id, headers, end_stream=False,
+ priority_weight=None, priority_depends_on=None,
+ priority_exclusive=None):
+ """
+ Send headers on a given stream.
+
+ This function can be used to send request or response headers: the kind
+ that are sent depends on whether this connection has been opened as a
+ client or server connection, and whether the stream was opened by the
+ remote peer or not.
+
+ If this is a client connection, calling ``send_headers`` will send the
+ headers as a request. It will also implicitly open the stream being
+ used. If this is a client connection and ``send_headers`` has *already*
+ been called, this will send trailers instead.
+
+ If this is a server connection, calling ``send_headers`` will send the
+ headers as a response. It is a protocol error for a server to open a
+ stream by sending headers. If this is a server connection and
+ ``send_headers`` has *already* been called, this will send trailers
+ instead.
+
+ When acting as a server, you may call ``send_headers`` any number of
+ times allowed by the following rules, in this order:
+
+ - zero or more times with ``(':status', '1XX')`` (where ``1XX`` is a
+ placeholder for any 100-level status code).
+ - once with any other status header.
+ - zero or one time for trailers.
+
+ That is, you are allowed to send as many informational responses as you
+ like, followed by one complete response and zero or one HTTP trailer
+ blocks.
+
+ Clients may send one or two header blocks: one request block, and
+ optionally one trailer block.
+
+ If it is important to send HPACK "never indexed" header fields (as
+ defined in `RFC 7451 Section 7.1.3
+ <https://tools.ietf.org/html/rfc7541#section-7.1.3>`_), the user may
+ instead provide headers using the HPACK library's :class:`HeaderTuple
+ <hpack:hpack.HeaderTuple>` and :class:`NeverIndexedHeaderTuple
+ <hpack:hpack.NeverIndexedHeaderTuple>` objects.
+
+ This method also allows users to prioritize the stream immediately,
+ by sending priority information on the HEADERS frame directly. To do
+ this, any one of ``priority_weight``, ``priority_depends_on``, or
+ ``priority_exclusive`` must be set to a value that is not ``None``. For
+ more information on the priority fields, see :meth:`prioritize
+ <h2.connection.H2Connection.prioritize>`.
+
+ .. warning:: In HTTP/2, it is mandatory that all the HTTP/2 special
+ headers (that is, ones whose header keys begin with ``:``) appear
+ at the start of the header block, before any normal headers.
+
+ .. versionchanged:: 2.3.0
+ Added support for using :class:`HeaderTuple
+ <hpack:hpack.HeaderTuple>` objects to store headers.
+
+ .. versionchanged:: 2.4.0
+ Added the ability to provide priority keyword arguments:
+ ``priority_weight``, ``priority_depends_on``, and
+ ``priority_exclusive``.
+
+ :param stream_id: The stream ID to send the headers on. If this stream
+ does not currently exist, it will be created.
+ :type stream_id: ``int``
+
+ :param headers: The request/response headers to send.
+ :type headers: An iterable of two tuples of bytestrings or
+ :class:`HeaderTuple <hpack:hpack.HeaderTuple>` objects.
+
+ :param end_stream: Whether this headers frame should end the stream
+ immediately (that is, whether no more data will be sent after this
+ frame). Defaults to ``False``.
+ :type end_stream: ``bool``
+
+ :param priority_weight: Sets the priority weight of the stream. See
+ :meth:`prioritize <h2.connection.H2Connection.prioritize>` for more
+ about how this field works. Defaults to ``None``, which means that
+ no priority information will be sent.
+ :type priority_weight: ``int`` or ``None``
+
+ :param priority_depends_on: Sets which stream this one depends on for
+ priority purposes. See :meth:`prioritize
+ <h2.connection.H2Connection.prioritize>` for more about how this
+ field works. Defaults to ``None``, which means that no priority
+ information will be sent.
+ :type priority_depends_on: ``int`` or ``None``
+
+ :param priority_exclusive: Sets whether this stream exclusively depends
+ on the stream given in ``priority_depends_on`` for priority
+ purposes. See :meth:`prioritize
+ <h2.connection.H2Connection.prioritize>` for more about how this
+ field workds. Defaults to ``None``, which means that no priority
+ information will be sent.
+ :type priority_depends_on: ``bool`` or ``None``
+
+ :returns: Nothing
+ """
+ self.config.logger.debug(
+ "Send headers on stream ID %d", stream_id
+ )
+
+ # Check we can open the stream.
+ if stream_id not in self.streams:
+ max_open_streams = self.remote_settings.max_concurrent_streams
+ if (self.open_outbound_streams + 1) > max_open_streams:
+ raise TooManyStreamsError(
+ "Max outbound streams is %d, %d open" %
+ (max_open_streams, self.open_outbound_streams)
+ )
+
+ self.state_machine.process_input(ConnectionInputs.SEND_HEADERS)
+ stream = self._get_or_create_stream(
+ stream_id, AllowedStreamIDs(self.config.client_side)
+ )
+ frames = stream.send_headers(
+ headers, self.encoder, end_stream
+ )
+
+ # We may need to send priority information.
+ priority_present = (
+ (priority_weight is not None) or
+ (priority_depends_on is not None) or
+ (priority_exclusive is not None)
+ )
+
+ if priority_present:
+ if not self.config.client_side:
+ raise RFC1122Error("Servers SHOULD NOT prioritize streams.")
+
+ headers_frame = frames[0]
+ headers_frame.flags.add('PRIORITY')
+ frames[0] = _add_frame_priority(
+ headers_frame,
+ priority_weight,
+ priority_depends_on,
+ priority_exclusive
+ )
+
+ self._prepare_for_sending(frames)
+
+ def send_data(self, stream_id, data, end_stream=False, pad_length=None):
+ """
+ Send data on a given stream.
+
+ This method does no breaking up of data: if the data is larger than the
+ value returned by :meth:`local_flow_control_window
+ <h2.connection.H2Connection.local_flow_control_window>` for this stream
+ then a :class:`FlowControlError <h2.exceptions.FlowControlError>` will
+ be raised. If the data is larger than :data:`max_outbound_frame_size
+ <h2.connection.H2Connection.max_outbound_frame_size>` then a
+ :class:`FrameTooLargeError <h2.exceptions.FrameTooLargeError>` will be
+ raised.
+
+ Hyper-h2 does this to avoid buffering the data internally. If the user
+ has more data to send than hyper-h2 will allow, consider breaking it up
+ and buffering it externally.
+
+ :param stream_id: The ID of the stream on which to send the data.
+ :type stream_id: ``int``
+ :param data: The data to send on the stream.
+ :type data: ``bytes``
+ :param end_stream: (optional) Whether this is the last data to be sent
+ on the stream. Defaults to ``False``.
+ :type end_stream: ``bool``
+ :param pad_length: (optional) Length of the padding to apply to the
+ data frame. Defaults to ``None`` for no use of padding. Note that
+ a value of ``0`` results in padding of length ``0``
+ (with the "padding" flag set on the frame).
+
+ .. versionadded:: 2.6.0
+
+ :type pad_length: ``int``
+ :returns: Nothing
+ """
+ self.config.logger.debug(
+ "Send data on stream ID %d with len %d", stream_id, len(data)
+ )
+ frame_size = len(data)
+ if pad_length is not None:
+ if not isinstance(pad_length, int):
+ raise TypeError("pad_length must be an int")
+ if pad_length < 0 or pad_length > 255:
+ raise ValueError("pad_length must be within range: [0, 255]")
+ # Account for padding bytes plus the 1-byte padding length field.
+ frame_size += pad_length + 1
+ self.config.logger.debug(
+ "Frame size on stream ID %d is %d", stream_id, frame_size
+ )
+
+ if frame_size > self.local_flow_control_window(stream_id):
+ raise FlowControlError(
+ "Cannot send %d bytes, flow control window is %d." %
+ (frame_size, self.local_flow_control_window(stream_id))
+ )
+ elif frame_size > self.max_outbound_frame_size:
+ raise FrameTooLargeError(
+ "Cannot send frame size %d, max frame size is %d" %
+ (frame_size, self.max_outbound_frame_size)
+ )
+
+ self.state_machine.process_input(ConnectionInputs.SEND_DATA)
+ frames = self.streams[stream_id].send_data(
+ data, end_stream, pad_length=pad_length
+ )
+
+ self._prepare_for_sending(frames)
+
+ self.outbound_flow_control_window -= frame_size
+ self.config.logger.debug(
+ "Outbound flow control window size is %d",
+ self.outbound_flow_control_window
+ )
+ assert self.outbound_flow_control_window >= 0
+
+ def end_stream(self, stream_id):
+ """
+ Cleanly end a given stream.
+
+ This method ends a stream by sending an empty DATA frame on that stream
+ with the ``END_STREAM`` flag set.
+
+ :param stream_id: The ID of the stream to end.
+ :type stream_id: ``int``
+ :returns: Nothing
+ """
+ self.config.logger.debug("End stream ID %d", stream_id)
+ self.state_machine.process_input(ConnectionInputs.SEND_DATA)
+ frames = self.streams[stream_id].end_stream()
+ self._prepare_for_sending(frames)
+
+ def increment_flow_control_window(self, increment, stream_id=None):
+ """
+ Increment a flow control window, optionally for a single stream. Allows
+ the remote peer to send more data.
+
+ .. versionchanged:: 2.0.0
+ Rejects attempts to increment the flow control window by out of
+ range values with a ``ValueError``.
+
+ :param increment: The amount to increment the flow control window by.
+ :type increment: ``int``
+ :param stream_id: (optional) The ID of the stream that should have its
+ flow control window opened. If not present or ``None``, the
+ connection flow control window will be opened instead.
+ :type stream_id: ``int`` or ``None``
+ :returns: Nothing
+ :raises: ``ValueError``
+ """
+ if not (1 <= increment <= self.MAX_WINDOW_INCREMENT):
+ raise ValueError(
+ "Flow control increment must be between 1 and %d" %
+ self.MAX_WINDOW_INCREMENT
+ )
+
+ self.state_machine.process_input(ConnectionInputs.SEND_WINDOW_UPDATE)
+
+ if stream_id is not None:
+ stream = self.streams[stream_id]
+ frames = stream.increase_flow_control_window(
+ increment
+ )
+
+ self.config.logger.debug(
+ "Increase stream ID %d flow control window by %d",
+ stream_id, increment
+ )
+ else:
+ self._inbound_flow_control_window_manager.window_opened(increment)
+ f = WindowUpdateFrame(0)
+ f.window_increment = increment
+ frames = [f]
+
+ self.config.logger.debug(
+ "Increase connection flow control window by %d", increment
+ )
+
+ self._prepare_for_sending(frames)
+
+ def push_stream(self, stream_id, promised_stream_id, request_headers):
+ """
+ Push a response to the client by sending a PUSH_PROMISE frame.
+
+ If it is important to send HPACK "never indexed" header fields (as
+ defined in `RFC 7451 Section 7.1.3
+ <https://tools.ietf.org/html/rfc7541#section-7.1.3>`_), the user may
+ instead provide headers using the HPACK library's :class:`HeaderTuple
+ <hpack:hpack.HeaderTuple>` and :class:`NeverIndexedHeaderTuple
+ <hpack:hpack.NeverIndexedHeaderTuple>` objects.
+
+ :param stream_id: The ID of the stream that this push is a response to.
+ :type stream_id: ``int``
+ :param promised_stream_id: The ID of the stream that the pushed
+ response will be sent on.
+ :type promised_stream_id: ``int``
+ :param request_headers: The headers of the request that the pushed
+ response will be responding to.
+ :type request_headers: An iterable of two tuples of bytestrings or
+ :class:`HeaderTuple <hpack:hpack.HeaderTuple>` objects.
+ :returns: Nothing
+ """
+ self.config.logger.debug(
+ "Send Push Promise frame on stream ID %d", stream_id
+ )
+
+ if not self.remote_settings.enable_push:
+ raise ProtocolError("Remote peer has disabled stream push")
+
+ self.state_machine.process_input(ConnectionInputs.SEND_PUSH_PROMISE)
+ stream = self._get_stream_by_id(stream_id)
+
+ # We need to prevent users pushing streams in response to streams that
+ # they themselves have already pushed: see #163 and RFC 7540 § 6.6. The
+ # easiest way to do that is to assert that the stream_id is not even:
+ # this shortcut works because only servers can push and the state
+ # machine will enforce this.
+ if (stream_id % 2) == 0:
+ raise ProtocolError("Cannot recursively push streams.")
+
+ new_stream = self._begin_new_stream(
+ promised_stream_id, AllowedStreamIDs.EVEN
+ )
+ self.streams[promised_stream_id] = new_stream
+
+ frames = stream.push_stream_in_band(
+ promised_stream_id, request_headers, self.encoder
+ )
+ new_frames = new_stream.locally_pushed()
+ self._prepare_for_sending(frames + new_frames)
+
+ def ping(self, opaque_data):
+ """
+ Send a PING frame.
+
+ :param opaque_data: A bytestring of length 8 that will be sent in the
+ PING frame.
+ :returns: Nothing
+ """
+ self.config.logger.debug("Send Ping frame")
+
+ if not isinstance(opaque_data, bytes) or len(opaque_data) != 8:
+ raise ValueError("Invalid value for ping data: %r" % opaque_data)
+
+ self.state_machine.process_input(ConnectionInputs.SEND_PING)
+ f = PingFrame(0)
+ f.opaque_data = opaque_data
+ self._prepare_for_sending([f])
+
+ def reset_stream(self, stream_id, error_code=0):
+ """
+ Reset a stream.
+
+ This method forcibly closes a stream by sending a RST_STREAM frame for
+ a given stream. This is not a graceful closure. To gracefully end a
+ stream, try the :meth:`end_stream
+ <h2.connection.H2Connection.end_stream>` method.
+
+ :param stream_id: The ID of the stream to reset.
+ :type stream_id: ``int``
+ :param error_code: (optional) The error code to use to reset the
+ stream. Defaults to :data:`ErrorCodes.NO_ERROR
+ <h2.errors.ErrorCodes.NO_ERROR>`.
+ :type error_code: ``int``
+ :returns: Nothing
+ """
+ self.config.logger.debug("Reset stream ID %d", stream_id)
+ self.state_machine.process_input(ConnectionInputs.SEND_RST_STREAM)
+ stream = self._get_stream_by_id(stream_id)
+ frames = stream.reset_stream(error_code)
+ self._prepare_for_sending(frames)
+
+ def close_connection(self, error_code=0, additional_data=None,
+ last_stream_id=None):
+
+ """
+ Close a connection, emitting a GOAWAY frame.
+
+ .. versionchanged:: 2.4.0
+ Added ``additional_data`` and ``last_stream_id`` arguments.
+
+ :param error_code: (optional) The error code to send in the GOAWAY
+ frame.
+ :param additional_data: (optional) Additional debug data indicating
+ a reason for closing the connection. Must be a bytestring.
+ :param last_stream_id: (optional) The last stream which was processed
+ by the sender. Defaults to ``highest_inbound_stream_id``.
+ :returns: Nothing
+ """
+ self.config.logger.debug("Close connection")
+ self.state_machine.process_input(ConnectionInputs.SEND_GOAWAY)
+
+ # Additional_data must be bytes
+ if additional_data is not None:
+ assert isinstance(additional_data, bytes)
+
+ if last_stream_id is None:
+ last_stream_id = self.highest_inbound_stream_id
+
+ f = GoAwayFrame(
+ stream_id=0,
+ last_stream_id=last_stream_id,
+ error_code=error_code,
+ additional_data=(additional_data or b'')
+ )
+ self._prepare_for_sending([f])
+
+ def update_settings(self, new_settings):
+ """
+ Update the local settings. This will prepare and emit the appropriate
+ SETTINGS frame.
+
+ :param new_settings: A dictionary of {setting: new value}
+ """
+ self.config.logger.debug(
+ "Update connection settings to %s", new_settings
+ )
+ self.state_machine.process_input(ConnectionInputs.SEND_SETTINGS)
+ self.local_settings.update(new_settings)
+ s = SettingsFrame(0)
+ s.settings = new_settings
+ self._prepare_for_sending([s])
+
+ def advertise_alternative_service(self,
+ field_value,
+ origin=None,
+ stream_id=None):
+ """
+ Notify a client about an available Alternative Service.
+
+ An Alternative Service is defined in `RFC 7838
+ <https://tools.ietf.org/html/rfc7838>`_. An Alternative Service
+ notification informs a client that a given origin is also available
+ elsewhere.
+
+ Alternative Services can be advertised in two ways. Firstly, they can
+ be advertised explicitly: that is, a server can say "origin X is also
+ available at Y". To advertise like this, set the ``origin`` argument
+ and not the ``stream_id`` argument. Alternatively, they can be
+ advertised implicitly: that is, a server can say "the origin you're
+ contacting on stream X is also available at Y". To advertise like this,
+ set the ``stream_id`` argument and not the ``origin`` argument.
+
+ The explicit method of advertising can be done as long as the
+ connection is active. The implicit method can only be done after the
+ client has sent the request headers and before the server has sent the
+ response headers: outside of those points, Hyper-h2 will forbid sending
+ the Alternative Service advertisement by raising a ProtocolError.
+
+ The ``field_value`` parameter is specified in RFC 7838. Hyper-h2 does
+ not validate or introspect this argument: the user is required to
+ ensure that it's well-formed. ``field_value`` corresponds to RFC 7838's
+ "Alternative Service Field Value".
+
+ .. note:: It is strongly preferred to use the explicit method of
+ advertising Alternative Services. The implicit method of
+ advertising Alternative Services has a number of subtleties
+ and can lead to inconsistencies between the server and
+ client. Hyper-h2 allows both mechanisms, but caution is
+ strongly advised.
+
+ .. versionadded:: 2.3.0
+
+ :param field_value: The RFC 7838 Alternative Service Field Value. This
+ argument is not introspected by Hyper-h2: the user is responsible
+ for ensuring that it is well-formed.
+ :type field_value: ``bytes``
+
+ :param origin: The origin/authority to which the Alternative Service
+ being advertised applies. Must not be provided at the same time as
+ ``stream_id``.
+ :type origin: ``bytes`` or ``None``
+
+ :param stream_id: The ID of the stream which was sent to the authority
+ for which this Alternative Service advertisement applies. Must not
+ be provided at the same time as ``origin``.
+ :type stream_id: ``int`` or ``None``
+
+ :returns: Nothing.
+ """
+ if not isinstance(field_value, bytes):
+ raise ValueError("Field must be bytestring.")
+
+ if origin is not None and stream_id is not None:
+ raise ValueError("Must not provide both origin and stream_id")
+
+ self.state_machine.process_input(
+ ConnectionInputs.SEND_ALTERNATIVE_SERVICE
+ )
+
+ if origin is not None:
+ # This ALTSVC is sent on stream zero.
+ f = AltSvcFrame(stream_id=0)
+ f.origin = origin
+ f.field = field_value
+ frames = [f]
+ else:
+ stream = self._get_stream_by_id(stream_id)
+ frames = stream.advertise_alternative_service(field_value)
+
+ self._prepare_for_sending(frames)
+
+ def prioritize(self, stream_id, weight=None, depends_on=None,
+ exclusive=None):
+ """
+ Notify a server about the priority of a stream.
+
+ Stream priorities are a form of guidance to a remote server: they
+ inform the server about how important a given response is, so that the
+ server may allocate its resources (e.g. bandwidth, CPU time, etc.)
+ accordingly. This exists to allow clients to ensure that the most
+ important data arrives earlier, while less important data does not
+ starve out the more important data.
+
+ Stream priorities are explained in depth in `RFC 7540 Section 5.3
+ <https://tools.ietf.org/html/rfc7540#section-5.3>`_.
+
+ This method updates the priority information of a single stream. It may
+ be called well before a stream is actively in use, or well after a
+ stream is closed.
+
+ .. warning:: RFC 7540 allows for servers to change the priority of
+ streams. However, hyper-h2 **does not** allow server
+ stacks to do this. This is because most clients do not
+ adequately know how to respond when provided conflicting
+ priority information, and relatively little utility is
+ provided by making that functionality available.
+
+ .. note:: hyper-h2 **does not** maintain any information about the
+ RFC 7540 priority tree. That means that hyper-h2 does not
+ prevent incautious users from creating invalid priority
+ trees, particularly by creating priority loops. While some
+ basic error checking is provided by hyper-h2, users are
+ strongly recommended to understand their prioritisation
+ strategies before using the priority tools here.
+
+ .. note:: Priority information is strictly advisory. Servers are
+ allowed to disregard it entirely. Avoid relying on the idea
+ that your priority signaling will definitely be obeyed.
+
+ .. versionadded:: 2.4.0
+
+ :param stream_id: The ID of the stream to prioritize.
+ :type stream_id: ``int``
+
+ :param weight: The weight to give the stream. Defaults to ``16``, the
+ default weight of any stream. May be any value between ``1`` and
+ ``256`` inclusive. The relative weight of a stream indicates what
+ proportion of available resources will be allocated to that
+ stream.
+ :type weight: ``int``
+
+ :param depends_on: The ID of the stream on which this stream depends.
+ This stream will only be progressed if it is impossible to
+ progress the parent stream (the one on which this one depends).
+ Passing the value ``0`` means that this stream does not depend on
+ any other. Defaults to ``0``.
+ :type depends_on: ``int``
+
+ :param exclusive: Whether this stream is an exclusive dependency of its
+ "parent" stream (i.e. the stream given by ``depends_on``). If a
+ stream is an exclusive dependency of another, that means that all
+ previously-set children of the parent are moved to become children
+ of the new exclusively-dependent stream. Defaults to ``False``.
+ :type exclusive: ``bool``
+ """
+ if not self.config.client_side:
+ raise RFC1122Error("Servers SHOULD NOT prioritize streams.")
+
+ self.state_machine.process_input(
+ ConnectionInputs.SEND_PRIORITY
+ )
+
+ frame = PriorityFrame(stream_id)
+ frame = _add_frame_priority(frame, weight, depends_on, exclusive)
+
+ self._prepare_for_sending([frame])
+
+ def local_flow_control_window(self, stream_id):
+ """
+ Returns the maximum amount of data that can be sent on stream
+ ``stream_id``.
+
+ This value will never be larger than the total data that can be sent on
+ the connection: even if the given stream allows more data, the
+ connection window provides a logical maximum to the amount of data that
+ can be sent.
+
+ The maximum data that can be sent in a single data frame on a stream
+ is either this value, or the maximum frame size, whichever is
+ *smaller*.
+
+ :param stream_id: The ID of the stream whose flow control window is
+ being queried.
+ :type stream_id: ``int``
+ :returns: The amount of data in bytes that can be sent on the stream
+ before the flow control window is exhausted.
+ :rtype: ``int``
+ """
+ stream = self._get_stream_by_id(stream_id)
+ return min(
+ self.outbound_flow_control_window,
+ stream.outbound_flow_control_window
+ )
+
+ def remote_flow_control_window(self, stream_id):
+ """
+ Returns the maximum amount of data the remote peer can send on stream
+ ``stream_id``.
+
+ This value will never be larger than the total data that can be sent on
+ the connection: even if the given stream allows more data, the
+ connection window provides a logical maximum to the amount of data that
+ can be sent.
+
+ The maximum data that can be sent in a single data frame on a stream
+ is either this value, or the maximum frame size, whichever is
+ *smaller*.
+
+ :param stream_id: The ID of the stream whose flow control window is
+ being queried.
+ :type stream_id: ``int``
+ :returns: The amount of data in bytes that can be received on the
+ stream before the flow control window is exhausted.
+ :rtype: ``int``
+ """
+ stream = self._get_stream_by_id(stream_id)
+ return min(
+ self.inbound_flow_control_window,
+ stream.inbound_flow_control_window
+ )
+
+ def acknowledge_received_data(self, acknowledged_size, stream_id):
+ """
+ Inform the :class:`H2Connection <h2.connection.H2Connection>` that a
+ certain number of flow-controlled bytes have been processed, and that
+ the space should be handed back to the remote peer at an opportune
+ time.
+
+ .. versionadded:: 2.5.0
+
+ :param acknowledged_size: The total *flow-controlled size* of the data
+ that has been processed. Note that this must include the amount of
+ padding that was sent with that data.
+ :type acknowledged_size: ``int``
+ :param stream_id: The ID of the stream on which this data was received.
+ :type stream_id: ``int``
+ :returns: Nothing
+ :rtype: ``None``
+ """
+ self.config.logger.debug(
+ "Ack received data on stream ID %d with size %d",
+ stream_id, acknowledged_size
+ )
+ if stream_id <= 0:
+ raise ValueError(
+ "Stream ID %d is not valid for acknowledge_received_data" %
+ stream_id
+ )
+ if acknowledged_size < 0:
+ raise ValueError("Cannot acknowledge negative data")
+
+ frames = []
+
+ conn_manager = self._inbound_flow_control_window_manager
+ conn_increment = conn_manager.process_bytes(acknowledged_size)
+ if conn_increment:
+ f = WindowUpdateFrame(0)
+ f.window_increment = conn_increment
+ frames.append(f)
+
+ try:
+ stream = self._get_stream_by_id(stream_id)
+ except StreamClosedError:
+ # The stream is already gone. We're not worried about incrementing
+ # the window in this case.
+ pass
+ else:
+ # No point incrementing the windows of closed streams.
+ if stream.open:
+ frames.extend(
+ stream.acknowledge_received_data(acknowledged_size)
+ )
+
+ self._prepare_for_sending(frames)
+
+ def data_to_send(self, amount=None):
+ """
+ Returns some data for sending out of the internal data buffer.
+
+ This method is analogous to ``read`` on a file-like object, but it
+ doesn't block. Instead, it returns as much data as the user asks for,
+ or less if that much data is not available. It does not perform any
+ I/O, and so uses a different name.
+
+ :param amount: (optional) The maximum amount of data to return. If not
+ set, or set to ``None``, will return as much data as possible.
+ :type amount: ``int``
+ :returns: A bytestring containing the data to send on the wire.
+ :rtype: ``bytes``
+ """
+ if amount is None:
+ data = bytes(self._data_to_send)
+ self._data_to_send = bytearray()
+ return data
+ else:
+ data = bytes(self._data_to_send[:amount])
+ self._data_to_send = self._data_to_send[amount:]
+ return data
+
+ def clear_outbound_data_buffer(self):
+ """
+ Clears the outbound data buffer, such that if this call was immediately
+ followed by a call to
+ :meth:`data_to_send <h2.connection.H2Connection.data_to_send>`, that
+ call would return no data.
+
+ This method should not normally be used, but is made available to avoid
+ exposing implementation details.
+ """
+ self._data_to_send = bytearray()
+
+ def _acknowledge_settings(self):
+ """
+ Acknowledge settings that have been received.
+
+ .. versionchanged:: 2.0.0
+ Removed from public API, removed useless ``event`` parameter, made
+ automatic.
+
+ :returns: Nothing
+ """
+ self.state_machine.process_input(ConnectionInputs.SEND_SETTINGS)
+
+ changes = self.remote_settings.acknowledge()
+
+ if SettingCodes.INITIAL_WINDOW_SIZE in changes:
+ setting = changes[SettingCodes.INITIAL_WINDOW_SIZE]
+ self._flow_control_change_from_settings(
+ setting.original_value,
+ setting.new_value,
+ )
+
+ # HEADER_TABLE_SIZE changes by the remote part affect our encoder: cf.
+ # RFC 7540 Section 6.5.2.
+ if SettingCodes.HEADER_TABLE_SIZE in changes:
+ setting = changes[SettingCodes.HEADER_TABLE_SIZE]
+ self.encoder.header_table_size = setting.new_value
+
+ if SettingCodes.MAX_FRAME_SIZE in changes:
+ setting = changes[SettingCodes.MAX_FRAME_SIZE]
+ self.max_outbound_frame_size = setting.new_value
+ for stream in self.streams.values():
+ stream.max_outbound_frame_size = setting.new_value
+
+ f = SettingsFrame(0)
+ f.flags.add('ACK')
+ return [f]
+
+ def _flow_control_change_from_settings(self, old_value, new_value):
+ """
+ Update flow control windows in response to a change in the value of
+ SETTINGS_INITIAL_WINDOW_SIZE.
+
+ When this setting is changed, it automatically updates all flow control
+ windows by the delta in the settings values. Note that it does not
+ increment the *connection* flow control window, per section 6.9.2 of
+ RFC 7540.
+ """
+ delta = new_value - old_value
+
+ for stream in self.streams.values():
+ stream.outbound_flow_control_window = guard_increment_window(
+ stream.outbound_flow_control_window,
+ delta
+ )
+
+ def _inbound_flow_control_change_from_settings(self, old_value, new_value):
+ """
+ Update remote flow control windows in response to a change in the value
+ of SETTINGS_INITIAL_WINDOW_SIZE.
+
+ When this setting is changed, it automatically updates all remote flow
+ control windows by the delta in the settings values.
+ """
+ delta = new_value - old_value
+
+ for stream in self.streams.values():
+ stream._inbound_flow_control_change_from_settings(delta)
+
+ def receive_data(self, data):
+ """
+ Pass some received HTTP/2 data to the connection for handling.
+
+ :param data: The data received from the remote peer on the network.
+ :type data: ``bytes``
+ :returns: A list of events that the remote peer triggered by sending
+ this data.
+ """
+ self.config.logger.trace(
+ "Process received data on connection. Received data: %r", data
+ )
+
+ events = []
+ self.incoming_buffer.add_data(data)
+ self.incoming_buffer.max_frame_size = self.max_inbound_frame_size
+
+ try:
+ for frame in self.incoming_buffer:
+ events.extend(self._receive_frame(frame))
+ except InvalidPaddingError:
+ self._terminate_connection(ErrorCodes.PROTOCOL_ERROR)
+ raise ProtocolError("Received frame with invalid padding.")
+ except ProtocolError as e:
+ # For whatever reason, receiving the frame caused a protocol error.
+ # We should prepare to emit a GoAway frame before throwing the
+ # exception up further. No need for an event: the exception will
+ # do fine.
+ self._terminate_connection(e.error_code)
+ raise
+
+ return events
+
+ def _receive_frame(self, frame):
+ """
+ Handle a frame received on the connection.
+
+ .. versionchanged:: 2.0.0
+ Removed from the public API.
+ """
+ try:
+ # I don't love using __class__ here, maybe reconsider it.
+ frames, events = self._frame_dispatch_table[frame.__class__](frame)
+ except StreamClosedError as e:
+ # If the stream was closed by RST_STREAM, we just send a RST_STREAM
+ # to the remote peer. Otherwise, this is a connection error, and so
+ # we will re-raise to trigger one.
+ if self._stream_is_closed_by_reset(e.stream_id):
+ f = RstStreamFrame(e.stream_id)
+ f.error_code = e.error_code
+ self._prepare_for_sending([f])
+ events = e._events
+ else:
+ raise
+ except StreamIDTooLowError as e:
+ # The stream ID seems invalid. This may happen when the closed
+ # stream has been cleaned up, or when the remote peer has opened a
+ # new stream with a higher stream ID than this one, forcing it
+ # closed implicitly.
+ #
+ # Check how the stream was closed: depending on the mechanism, it
+ # is either a stream error or a connection error.
+ if self._stream_is_closed_by_reset(e.stream_id):
+ # Closed by RST_STREAM is a stream error.
+ f = RstStreamFrame(e.stream_id)
+ f.error_code = ErrorCodes.STREAM_CLOSED
+ self._prepare_for_sending([f])
+ events = []
+ elif self._stream_is_closed_by_end(e.stream_id):
+ # Closed by END_STREAM is a connection error.
+ raise StreamClosedError(e.stream_id)
+ else:
+ # Closed implicitly, also a connection error, but of type
+ # PROTOCOL_ERROR.
+ raise
+ else:
+ self._prepare_for_sending(frames)
+
+ return events
+
+ def _terminate_connection(self, error_code):
+ """
+ Terminate the connection early. Used in error handling blocks to send
+ GOAWAY frames.
+ """
+ f = GoAwayFrame(0)
+ f.last_stream_id = self.highest_inbound_stream_id
+ f.error_code = error_code
+ self.state_machine.process_input(ConnectionInputs.SEND_GOAWAY)
+ self._prepare_for_sending([f])
+
+ def _receive_headers_frame(self, frame):
+ """
+ Receive a headers frame on the connection.
+ """
+ # If necessary, check we can open the stream. Also validate that the
+ # stream ID is valid.
+ if frame.stream_id not in self.streams:
+ max_open_streams = self.local_settings.max_concurrent_streams
+ if (self.open_inbound_streams + 1) > max_open_streams:
+ raise TooManyStreamsError(
+ "Max outbound streams is %d, %d open" %
+ (max_open_streams, self.open_outbound_streams)
+ )
+
+ # Let's decode the headers. We handle headers as bytes internally up
+ # until we hang them off the event, at which point we may optionally
+ # convert them to unicode.
+ headers = _decode_headers(self.decoder, frame.data)
+
+ events = self.state_machine.process_input(
+ ConnectionInputs.RECV_HEADERS
+ )
+ stream = self._get_or_create_stream(
+ frame.stream_id, AllowedStreamIDs(not self.config.client_side)
+ )
+ frames, stream_events = stream.receive_headers(
+ headers,
+ 'END_STREAM' in frame.flags,
+ self.config.header_encoding
+ )
+
+ if 'PRIORITY' in frame.flags:
+ p_frames, p_events = self._receive_priority_frame(frame)
+ stream_events[0].priority_updated = p_events[0]
+ stream_events.extend(p_events)
+ assert not p_frames
+
+ return frames, events + stream_events
+
+ def _receive_push_promise_frame(self, frame):
+ """
+ Receive a push-promise frame on the connection.
+ """
+ if not self.local_settings.enable_push:
+ raise ProtocolError("Received pushed stream")
+
+ pushed_headers = _decode_headers(self.decoder, frame.data)
+
+ events = self.state_machine.process_input(
+ ConnectionInputs.RECV_PUSH_PROMISE
+ )
+
+ try:
+ stream = self._get_stream_by_id(frame.stream_id)
+ except NoSuchStreamError:
+ # We need to check if the parent stream was reset by us. If it was
+ # then we presume that the PUSH_PROMISE was in flight when we reset
+ # the parent stream. Rather than accept the new stream, just reset
+ # it.
+ #
+ # If this was closed naturally, however, we should call this a
+ # PROTOCOL_ERROR: pushing a stream on a naturally closed stream is
+ # a real problem because it creates a brand new stream that the
+ # remote peer now believes exists.
+ if (self._stream_closed_by(frame.stream_id) ==
+ StreamClosedBy.SEND_RST_STREAM):
+ f = RstStreamFrame(frame.promised_stream_id)
+ f.error_code = ErrorCodes.REFUSED_STREAM
+ return [f], events
+
+ raise ProtocolError("Attempted to push on closed stream.")
+
+ # We need to prevent peers pushing streams in response to streams that
+ # they themselves have already pushed: see #163 and RFC 7540 § 6.6. The
+ # easiest way to do that is to assert that the stream_id is not even:
+ # this shortcut works because only servers can push and the state
+ # machine will enforce this.
+ if (frame.stream_id % 2) == 0:
+ raise ProtocolError("Cannot recursively push streams.")
+
+ try:
+ frames, stream_events = stream.receive_push_promise_in_band(
+ frame.promised_stream_id,
+ pushed_headers,
+ self.config.header_encoding,
+ )
+ except StreamClosedError:
+ # The parent stream was reset by us, so we presume that
+ # PUSH_PROMISE was in flight when we reset the parent stream.
+ # So we just reset the new stream.
+ f = RstStreamFrame(frame.promised_stream_id)
+ f.error_code = ErrorCodes.REFUSED_STREAM
+ return [f], events
+
+ new_stream = self._begin_new_stream(
+ frame.promised_stream_id, AllowedStreamIDs.EVEN
+ )
+ self.streams[frame.promised_stream_id] = new_stream
+ new_stream.remotely_pushed(pushed_headers)
+
+ return frames, events + stream_events
+
+ def _handle_data_on_closed_stream(self, events, exc, frame):
+ # This stream is already closed - and yet we received a DATA frame.
+ # The received DATA frame counts towards the connection flow window.
+ # We need to manually to acknowledge the DATA frame to update the flow
+ # window of the connection. Otherwise the whole connection stalls due
+ # the inbound flow window being 0.
+ frames = []
+ conn_manager = self._inbound_flow_control_window_manager
+ conn_increment = conn_manager.process_bytes(
+ frame.flow_controlled_length
+ )
+ if conn_increment:
+ f = WindowUpdateFrame(0)
+ f.window_increment = conn_increment
+ frames.append(f)
+ self.config.logger.debug(
+ "Received DATA frame on closed stream %d - "
+ "auto-emitted a WINDOW_UPDATE by %d",
+ frame.stream_id, conn_increment
+ )
+ f = RstStreamFrame(exc.stream_id)
+ f.error_code = exc.error_code
+ frames.append(f)
+ self.config.logger.debug(
+ "Stream %d already CLOSED or cleaned up - "
+ "auto-emitted a RST_FRAME" % frame.stream_id
+ )
+ return frames, events + exc._events
+
+ def _receive_data_frame(self, frame):
+ """
+ Receive a data frame on the connection.
+ """
+ flow_controlled_length = frame.flow_controlled_length
+
+ events = self.state_machine.process_input(
+ ConnectionInputs.RECV_DATA
+ )
+ self._inbound_flow_control_window_manager.window_consumed(
+ flow_controlled_length
+ )
+
+ try:
+ stream = self._get_stream_by_id(frame.stream_id)
+ frames, stream_events = stream.receive_data(
+ frame.data,
+ 'END_STREAM' in frame.flags,
+ flow_controlled_length
+ )
+ except StreamClosedError as e:
+ # This stream is either marked as CLOSED or already gone from our
+ # internal state.
+ return self._handle_data_on_closed_stream(events, e, frame)
+
+ return frames, events + stream_events
+
+ def _receive_settings_frame(self, frame):
+ """
+ Receive a SETTINGS frame on the connection.
+ """
+ events = self.state_machine.process_input(
+ ConnectionInputs.RECV_SETTINGS
+ )
+
+ # This is an ack of the local settings.
+ if 'ACK' in frame.flags:
+ changed_settings = self._local_settings_acked()
+ ack_event = SettingsAcknowledged()
+ ack_event.changed_settings = changed_settings
+ events.append(ack_event)
+ return [], events
+
+ # Add the new settings.
+ self.remote_settings.update(frame.settings)
+ events.append(
+ RemoteSettingsChanged.from_settings(
+ self.remote_settings, frame.settings
+ )
+ )
+ frames = self._acknowledge_settings()
+
+ return frames, events
+
+ def _receive_window_update_frame(self, frame):
+ """
+ Receive a WINDOW_UPDATE frame on the connection.
+ """
+ # Validate the frame.
+ if not (1 <= frame.window_increment <= self.MAX_WINDOW_INCREMENT):
+ raise ProtocolError(
+ "Flow control increment must be between 1 and %d, received %d"
+ % (self.MAX_WINDOW_INCREMENT, frame.window_increment)
+ )
+
+ events = self.state_machine.process_input(
+ ConnectionInputs.RECV_WINDOW_UPDATE
+ )
+
+ if frame.stream_id:
+ stream = self._get_stream_by_id(frame.stream_id)
+ frames, stream_events = stream.receive_window_update(
+ frame.window_increment
+ )
+ else:
+ # Increment our local flow control window.
+ self.outbound_flow_control_window = guard_increment_window(
+ self.outbound_flow_control_window,
+ frame.window_increment
+ )
+
+ # FIXME: Should we split this into one event per active stream?
+ window_updated_event = WindowUpdated()
+ window_updated_event.stream_id = 0
+ window_updated_event.delta = frame.window_increment
+ stream_events = [window_updated_event]
+ frames = []
+
+ return frames, events + stream_events
+
+ def _receive_ping_frame(self, frame):
+ """
+ Receive a PING frame on the connection.
+ """
+ events = self.state_machine.process_input(
+ ConnectionInputs.RECV_PING
+ )
+ flags = []
+
+ if 'ACK' in frame.flags:
+ evt = PingAckReceived()
+ else:
+ evt = PingReceived()
+
+ # automatically ACK the PING with the same 'opaque data'
+ f = PingFrame(0)
+ f.flags = {'ACK'}
+ f.opaque_data = frame.opaque_data
+ flags.append(f)
+
+ evt.ping_data = frame.opaque_data
+ events.append(evt)
+
+ return flags, events
+
+ def _receive_rst_stream_frame(self, frame):
+ """
+ Receive a RST_STREAM frame on the connection.
+ """
+ events = self.state_machine.process_input(
+ ConnectionInputs.RECV_RST_STREAM
+ )
+ try:
+ stream = self._get_stream_by_id(frame.stream_id)
+ except NoSuchStreamError:
+ # The stream is missing. That's ok, we just do nothing here.
+ stream_frames = []
+ stream_events = []
+ else:
+ stream_frames, stream_events = stream.stream_reset(frame)
+
+ return stream_frames, events + stream_events
+
+ def _receive_priority_frame(self, frame):
+ """
+ Receive a PRIORITY frame on the connection.
+ """
+ events = self.state_machine.process_input(
+ ConnectionInputs.RECV_PRIORITY
+ )
+
+ event = PriorityUpdated()
+ event.stream_id = frame.stream_id
+ event.depends_on = frame.depends_on
+ event.exclusive = frame.exclusive
+
+ # Weight is an integer between 1 and 256, but the byte only allows
+ # 0 to 255: add one.
+ event.weight = frame.stream_weight + 1
+
+ # A stream may not depend on itself.
+ if event.depends_on == frame.stream_id:
+ raise ProtocolError(
+ "Stream %d may not depend on itself" % frame.stream_id
+ )
+ events.append(event)
+
+ return [], events
+
+ def _receive_goaway_frame(self, frame):
+ """
+ Receive a GOAWAY frame on the connection.
+ """
+ events = self.state_machine.process_input(
+ ConnectionInputs.RECV_GOAWAY
+ )
+
+ # Clear the outbound data buffer: we cannot send further data now.
+ self.clear_outbound_data_buffer()
+
+ # Fire an appropriate ConnectionTerminated event.
+ new_event = ConnectionTerminated()
+ new_event.error_code = _error_code_from_int(frame.error_code)
+ new_event.last_stream_id = frame.last_stream_id
+ new_event.additional_data = (frame.additional_data
+ if frame.additional_data else None)
+ events.append(new_event)
+
+ return [], events
+
+ def _receive_naked_continuation(self, frame):
+ """
+ A naked CONTINUATION frame has been received. This is always an error,
+ but the type of error it is depends on the state of the stream and must
+ transition the state of the stream, so we need to pass it to the
+ appropriate stream.
+ """
+ stream = self._get_stream_by_id(frame.stream_id)
+ stream.receive_continuation()
+ assert False, "Should not be reachable"
+
+ def _receive_alt_svc_frame(self, frame):
+ """
+ An ALTSVC frame has been received. This frame, specified in RFC 7838,
+ is used to advertise alternative places where the same service can be
+ reached.
+
+ This frame can optionally be received either on a stream or on stream
+ 0, and its semantics are different in each case.
+ """
+ events = self.state_machine.process_input(
+ ConnectionInputs.RECV_ALTERNATIVE_SERVICE
+ )
+ frames = []
+
+ if frame.stream_id:
+ # Given that it makes no sense to receive ALTSVC on a stream
+ # before that stream has been opened with a HEADERS frame, the
+ # ALTSVC frame cannot create a stream. If the stream is not
+ # present, we simply ignore the frame.
+ try:
+ stream = self._get_stream_by_id(frame.stream_id)
+ except (NoSuchStreamError, StreamClosedError):
+ pass
+ else:
+ stream_frames, stream_events = stream.receive_alt_svc(frame)
+ frames.extend(stream_frames)
+ events.extend(stream_events)
+ else:
+ # This frame is sent on stream 0. The origin field on the frame
+ # must be present, though if it isn't it's not a ProtocolError
+ # (annoyingly), we just need to ignore it.
+ if not frame.origin:
+ return frames, events
+
+ # If we're a server, we want to ignore this (RFC 7838 says so).
+ if not self.config.client_side:
+ return frames, events
+
+ event = AlternativeServiceAvailable()
+ event.origin = frame.origin
+ event.field_value = frame.field
+ events.append(event)
+
+ return frames, events
+
+ def _receive_unknown_frame(self, frame):
+ """
+ We have received a frame that we do not understand. This is almost
+ certainly an extension frame, though it's impossible to be entirely
+ sure.
+
+ RFC 7540 § 5.5 says that we MUST ignore unknown frame types: so we
+ do. We do notify the user that we received one, however.
+ """
+ # All we do here is log.
+ self.config.logger.debug(
+ "Received unknown extension frame (ID %d)", frame.stream_id
+ )
+ event = UnknownFrameReceived()
+ event.frame = frame
+ return [], [event]
+
+ def _local_settings_acked(self):
+ """
+ Handle the local settings being ACKed, update internal state.
+ """
+ changes = self.local_settings.acknowledge()
+
+ if SettingCodes.INITIAL_WINDOW_SIZE in changes:
+ setting = changes[SettingCodes.INITIAL_WINDOW_SIZE]
+ self._inbound_flow_control_change_from_settings(
+ setting.original_value,
+ setting.new_value,
+ )
+
+ if SettingCodes.MAX_HEADER_LIST_SIZE in changes:
+ setting = changes[SettingCodes.MAX_HEADER_LIST_SIZE]
+ self.decoder.max_header_list_size = setting.new_value
+
+ if SettingCodes.MAX_FRAME_SIZE in changes:
+ setting = changes[SettingCodes.MAX_FRAME_SIZE]
+ self.max_inbound_frame_size = setting.new_value
+
+ if SettingCodes.HEADER_TABLE_SIZE in changes:
+ setting = changes[SettingCodes.HEADER_TABLE_SIZE]
+ # This is safe across all hpack versions: some versions just won't
+ # respect it.
+ self.decoder.max_allowed_table_size = setting.new_value
+
+ return changes
+
+ def _stream_id_is_outbound(self, stream_id):
+ """
+ Returns ``True`` if the stream ID corresponds to an outbound stream
+ (one initiated by this peer), returns ``False`` otherwise.
+ """
+ return (stream_id % 2 == int(self.config.client_side))
+
+ def _stream_closed_by(self, stream_id):
+ """
+ Returns how the stream was closed.
+
+ The return value will be either a member of
+ ``h2.stream.StreamClosedBy`` or ``None``. If ``None``, the stream was
+ closed implicitly by the peer opening a stream with a higher stream ID
+ before opening this one.
+ """
+ if stream_id in self.streams:
+ return self.streams[stream_id].closed_by
+ if stream_id in self._closed_streams:
+ return self._closed_streams[stream_id]
+ return None
+
+ def _stream_is_closed_by_reset(self, stream_id):
+ """
+ Returns ``True`` if the stream was closed by sending or receiving a
+ RST_STREAM frame. Returns ``False`` otherwise.
+ """
+ return self._stream_closed_by(stream_id) in (
+ StreamClosedBy.RECV_RST_STREAM, StreamClosedBy.SEND_RST_STREAM
+ )
+
+ def _stream_is_closed_by_end(self, stream_id):
+ """
+ Returns ``True`` if the stream was closed by sending or receiving an
+ END_STREAM flag in a HEADERS or DATA frame. Returns ``False``
+ otherwise.
+ """
+ return self._stream_closed_by(stream_id) in (
+ StreamClosedBy.RECV_END_STREAM, StreamClosedBy.SEND_END_STREAM
+ )
+
+
+def _add_frame_priority(frame, weight=None, depends_on=None, exclusive=None):
+ """
+ Adds priority data to a given frame. Does not change any flags set on that
+ frame: if the caller is adding priority information to a HEADERS frame they
+ must set that themselves.
+
+ This method also deliberately sets defaults for anything missing.
+
+ This method validates the input values.
+ """
+ # A stream may not depend on itself.
+ if depends_on == frame.stream_id:
+ raise ProtocolError(
+ "Stream %d may not depend on itself" % frame.stream_id
+ )
+
+ # Weight must be between 1 and 256.
+ if weight is not None:
+ if weight > 256 or weight < 1:
+ raise ProtocolError(
+ "Weight must be between 1 and 256, not %d" % weight
+ )
+ else:
+ # Weight is an integer between 1 and 256, but the byte only allows
+ # 0 to 255: subtract one.
+ weight -= 1
+
+ # Set defaults for anything not provided.
+ weight = weight if weight is not None else 15
+ depends_on = depends_on if depends_on is not None else 0
+ exclusive = exclusive if exclusive is not None else False
+
+ frame.stream_weight = weight
+ frame.depends_on = depends_on
+ frame.exclusive = exclusive
+
+ return frame
+
+
+def _decode_headers(decoder, encoded_header_block):
+ """
+ Decode a HPACK-encoded header block, translating HPACK exceptions into
+ sensible hyper-h2 errors.
+
+ This only ever returns bytestring headers: hyper-h2 may emit them as
+ unicode later, but internally it processes them as bytestrings only.
+ """
+ try:
+ return decoder.decode(encoded_header_block, raw=True)
+ except OversizedHeaderListError as e:
+ # This is a symptom of a HPACK bomb attack: the user has
+ # disregarded our requirements on how large a header block we'll
+ # accept.
+ raise DenialOfServiceError("Oversized header block: %s" % e)
+ except (HPACKError, IndexError, TypeError, UnicodeDecodeError) as e:
+ # We should only need HPACKError here, but versions of HPACK older
+ # than 2.1.0 throw all three others as well. For maximum
+ # compatibility, catch all of them.
+ raise ProtocolError("Error decoding header block: %s" % e)
diff --git a/testing/web-platform/tests/tools/third_party/h2/h2/errors.py b/testing/web-platform/tests/tools/third_party/h2/h2/errors.py
new file mode 100644
index 0000000000..baef2001cd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/h2/errors.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+"""
+h2/errors
+~~~~~~~~~~~~~~~~~~~
+
+Global error code registry containing the established HTTP/2 error codes.
+
+The current registry is available at:
+https://tools.ietf.org/html/rfc7540#section-11.4
+"""
+import enum
+
+
+class ErrorCodes(enum.IntEnum):
+ """
+ All known HTTP/2 error codes.
+
+ .. versionadded:: 2.5.0
+ """
+ #: Graceful shutdown.
+ NO_ERROR = 0x0
+
+ #: Protocol error detected.
+ PROTOCOL_ERROR = 0x1
+
+ #: Implementation fault.
+ INTERNAL_ERROR = 0x2
+
+ #: Flow-control limits exceeded.
+ FLOW_CONTROL_ERROR = 0x3
+
+ #: Settings not acknowledged.
+ SETTINGS_TIMEOUT = 0x4
+
+ #: Frame received for closed stream.
+ STREAM_CLOSED = 0x5
+
+ #: Frame size incorrect.
+ FRAME_SIZE_ERROR = 0x6
+
+ #: Stream not processed.
+ REFUSED_STREAM = 0x7
+
+ #: Stream cancelled.
+ CANCEL = 0x8
+
+ #: Compression state not updated.
+ COMPRESSION_ERROR = 0x9
+
+ #: TCP connection error for CONNECT method.
+ CONNECT_ERROR = 0xa
+
+ #: Processing capacity exceeded.
+ ENHANCE_YOUR_CALM = 0xb
+
+ #: Negotiated TLS parameters not acceptable.
+ INADEQUATE_SECURITY = 0xc
+
+ #: Use HTTP/1.1 for the request.
+ HTTP_1_1_REQUIRED = 0xd
+
+
+def _error_code_from_int(code):
+ """
+ Given an integer error code, returns either one of :class:`ErrorCodes
+ <h2.errors.ErrorCodes>` or, if not present in the known set of codes,
+ returns the integer directly.
+ """
+ try:
+ return ErrorCodes(code)
+ except ValueError:
+ return code
+
+
+__all__ = ['ErrorCodes']
diff --git a/testing/web-platform/tests/tools/third_party/h2/h2/events.py b/testing/web-platform/tests/tools/third_party/h2/h2/events.py
new file mode 100644
index 0000000000..a06c9903d5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/h2/events.py
@@ -0,0 +1,648 @@
+# -*- coding: utf-8 -*-
+"""
+h2/events
+~~~~~~~~~
+
+Defines Event types for HTTP/2.
+
+Events are returned by the H2 state machine to allow implementations to keep
+track of events triggered by receiving data. Each time data is provided to the
+H2 state machine it processes the data and returns a list of Event objects.
+"""
+import binascii
+
+from .settings import ChangedSetting, _setting_code_from_int
+
+
+class Event(object):
+ """
+ Base class for h2 events.
+ """
+ pass
+
+
+class RequestReceived(Event):
+ """
+ The RequestReceived event is fired whenever request headers are received.
+ This event carries the HTTP headers for the given request and the stream ID
+ of the new stream.
+
+ .. versionchanged:: 2.3.0
+ Changed the type of ``headers`` to :class:`HeaderTuple
+ <hpack:hpack.HeaderTuple>`. This has no effect on current users.
+
+ .. versionchanged:: 2.4.0
+ Added ``stream_ended`` and ``priority_updated`` properties.
+ """
+ def __init__(self):
+ #: The Stream ID for the stream this request was made on.
+ self.stream_id = None
+
+ #: The request headers.
+ self.headers = None
+
+ #: If this request also ended the stream, the associated
+ #: :class:`StreamEnded <h2.events.StreamEnded>` event will be available
+ #: here.
+ #:
+ #: .. versionadded:: 2.4.0
+ self.stream_ended = None
+
+ #: If this request also had associated priority information, the
+ #: associated :class:`PriorityUpdated <h2.events.PriorityUpdated>`
+ #: event will be available here.
+ #:
+ #: .. versionadded:: 2.4.0
+ self.priority_updated = None
+
+ def __repr__(self):
+ return "<RequestReceived stream_id:%s, headers:%s>" % (
+ self.stream_id, self.headers
+ )
+
+
+class ResponseReceived(Event):
+ """
+ The ResponseReceived event is fired whenever response headers are received.
+ This event carries the HTTP headers for the given response and the stream
+ ID of the new stream.
+
+ .. versionchanged:: 2.3.0
+ Changed the type of ``headers`` to :class:`HeaderTuple
+ <hpack:hpack.HeaderTuple>`. This has no effect on current users.
+
+ .. versionchanged:: 2.4.0
+ Added ``stream_ended`` and ``priority_updated`` properties.
+ """
+ def __init__(self):
+ #: The Stream ID for the stream this response was made on.
+ self.stream_id = None
+
+ #: The response headers.
+ self.headers = None
+
+ #: If this response also ended the stream, the associated
+ #: :class:`StreamEnded <h2.events.StreamEnded>` event will be available
+ #: here.
+ #:
+ #: .. versionadded:: 2.4.0
+ self.stream_ended = None
+
+ #: If this response also had associated priority information, the
+ #: associated :class:`PriorityUpdated <h2.events.PriorityUpdated>`
+ #: event will be available here.
+ #:
+ #: .. versionadded:: 2.4.0
+ self.priority_updated = None
+
+ def __repr__(self):
+ return "<ResponseReceived stream_id:%s, headers:%s>" % (
+ self.stream_id, self.headers
+ )
+
+
+class TrailersReceived(Event):
+ """
+ The TrailersReceived event is fired whenever trailers are received on a
+ stream. Trailers are a set of headers sent after the body of the
+ request/response, and are used to provide information that wasn't known
+ ahead of time (e.g. content-length). This event carries the HTTP header
+ fields that form the trailers and the stream ID of the stream on which they
+ were received.
+
+ .. versionchanged:: 2.3.0
+ Changed the type of ``headers`` to :class:`HeaderTuple
+ <hpack:hpack.HeaderTuple>`. This has no effect on current users.
+
+ .. versionchanged:: 2.4.0
+ Added ``stream_ended`` and ``priority_updated`` properties.
+ """
+ def __init__(self):
+ #: The Stream ID for the stream on which these trailers were received.
+ self.stream_id = None
+
+ #: The trailers themselves.
+ self.headers = None
+
+ #: Trailers always end streams. This property has the associated
+ #: :class:`StreamEnded <h2.events.StreamEnded>` in it.
+ #:
+ #: .. versionadded:: 2.4.0
+ self.stream_ended = None
+
+ #: If the trailers also set associated priority information, the
+ #: associated :class:`PriorityUpdated <h2.events.PriorityUpdated>`
+ #: event will be available here.
+ #:
+ #: .. versionadded:: 2.4.0
+ self.priority_updated = None
+
+ def __repr__(self):
+ return "<TrailersReceived stream_id:%s, headers:%s>" % (
+ self.stream_id, self.headers
+ )
+
+
+class _HeadersSent(Event):
+ """
+ The _HeadersSent event is fired whenever headers are sent.
+
+ This is an internal event, used to determine validation steps on
+ outgoing header blocks.
+ """
+ pass
+
+
+class _ResponseSent(_HeadersSent):
+ """
+ The _ResponseSent event is fired whenever response headers are sent
+ on a stream.
+
+ This is an internal event, used to determine validation steps on
+ outgoing header blocks.
+ """
+ pass
+
+
+class _RequestSent(_HeadersSent):
+ """
+ The _RequestSent event is fired whenever request headers are sent
+ on a stream.
+
+ This is an internal event, used to determine validation steps on
+ outgoing header blocks.
+ """
+ pass
+
+
+class _TrailersSent(_HeadersSent):
+ """
+ The _TrailersSent event is fired whenever trailers are sent on a
+ stream. Trailers are a set of headers sent after the body of the
+ request/response, and are used to provide information that wasn't known
+ ahead of time (e.g. content-length).
+
+ This is an internal event, used to determine validation steps on
+ outgoing header blocks.
+ """
+ pass
+
+
+class _PushedRequestSent(_HeadersSent):
+ """
+ The _PushedRequestSent event is fired whenever pushed request headers are
+ sent.
+
+ This is an internal event, used to determine validation steps on outgoing
+ header blocks.
+ """
+ pass
+
+
+class InformationalResponseReceived(Event):
+ """
+ The InformationalResponseReceived event is fired when an informational
+ response (that is, one whose status code is a 1XX code) is received from
+ the remote peer.
+
+ The remote peer may send any number of these, from zero upwards. These
+ responses are most commonly sent in response to requests that have the
+ ``expect: 100-continue`` header field present. Most users can safely
+ ignore this event unless you are intending to use the
+ ``expect: 100-continue`` flow, or are for any reason expecting a different
+ 1XX status code.
+
+ .. versionadded:: 2.2.0
+
+ .. versionchanged:: 2.3.0
+ Changed the type of ``headers`` to :class:`HeaderTuple
+ <hpack:hpack.HeaderTuple>`. This has no effect on current users.
+
+ .. versionchanged:: 2.4.0
+ Added ``priority_updated`` property.
+ """
+ def __init__(self):
+ #: The Stream ID for the stream this informational response was made
+ #: on.
+ self.stream_id = None
+
+ #: The headers for this informational response.
+ self.headers = None
+
+ #: If this response also had associated priority information, the
+ #: associated :class:`PriorityUpdated <h2.events.PriorityUpdated>`
+ #: event will be available here.
+ #:
+ #: .. versionadded:: 2.4.0
+ self.priority_updated = None
+
+ def __repr__(self):
+ return "<InformationalResponseReceived stream_id:%s, headers:%s>" % (
+ self.stream_id, self.headers
+ )
+
+
+class DataReceived(Event):
+ """
+ The DataReceived event is fired whenever data is received on a stream from
+ the remote peer. The event carries the data itself, and the stream ID on
+ which the data was received.
+
+ .. versionchanged:: 2.4.0
+ Added ``stream_ended`` property.
+ """
+ def __init__(self):
+ #: The Stream ID for the stream this data was received on.
+ self.stream_id = None
+
+ #: The data itself.
+ self.data = None
+
+ #: The amount of data received that counts against the flow control
+ #: window. Note that padding counts against the flow control window, so
+ #: when adjusting flow control you should always use this field rather
+ #: than ``len(data)``.
+ self.flow_controlled_length = None
+
+ #: If this data chunk also completed the stream, the associated
+ #: :class:`StreamEnded <h2.events.StreamEnded>` event will be available
+ #: here.
+ #:
+ #: .. versionadded:: 2.4.0
+ self.stream_ended = None
+
+ def __repr__(self):
+ return (
+ "<DataReceived stream_id:%s, "
+ "flow_controlled_length:%s, "
+ "data:%s>" % (
+ self.stream_id,
+ self.flow_controlled_length,
+ _bytes_representation(self.data[:20]),
+ )
+ )
+
+
+class WindowUpdated(Event):
+ """
+ The WindowUpdated event is fired whenever a flow control window changes
+ size. HTTP/2 defines flow control windows for connections and streams: this
+ event fires for both connections and streams. The event carries the ID of
+ the stream to which it applies (set to zero if the window update applies to
+ the connection), and the delta in the window size.
+ """
+ def __init__(self):
+ #: The Stream ID of the stream whose flow control window was changed.
+ #: May be ``0`` if the connection window was changed.
+ self.stream_id = None
+
+ #: The window delta.
+ self.delta = None
+
+ def __repr__(self):
+ return "<WindowUpdated stream_id:%s, delta:%s>" % (
+ self.stream_id, self.delta
+ )
+
+
+class RemoteSettingsChanged(Event):
+ """
+ The RemoteSettingsChanged event is fired whenever the remote peer changes
+ its settings. It contains a complete inventory of changed settings,
+ including their previous values.
+
+ In HTTP/2, settings changes need to be acknowledged. hyper-h2 automatically
+ acknowledges settings changes for efficiency. However, it is possible that
+ the caller may not be happy with the changed setting.
+
+ When this event is received, the caller should confirm that the new
+ settings are acceptable. If they are not acceptable, the user should close
+ the connection with the error code :data:`PROTOCOL_ERROR
+ <h2.errors.ErrorCodes.PROTOCOL_ERROR>`.
+
+ .. versionchanged:: 2.0.0
+ Prior to this version the user needed to acknowledge settings changes.
+ This is no longer the case: hyper-h2 now automatically acknowledges
+ them.
+ """
+ def __init__(self):
+ #: A dictionary of setting byte to
+ #: :class:`ChangedSetting <h2.settings.ChangedSetting>`, representing
+ #: the changed settings.
+ self.changed_settings = {}
+
+ @classmethod
+ def from_settings(cls, old_settings, new_settings):
+ """
+ Build a RemoteSettingsChanged event from a set of changed settings.
+
+ :param old_settings: A complete collection of old settings, in the form
+ of a dictionary of ``{setting: value}``.
+ :param new_settings: All the changed settings and their new values, in
+ the form of a dictionary of ``{setting: value}``.
+ """
+ e = cls()
+ for setting, new_value in new_settings.items():
+ setting = _setting_code_from_int(setting)
+ original_value = old_settings.get(setting)
+ change = ChangedSetting(setting, original_value, new_value)
+ e.changed_settings[setting] = change
+
+ return e
+
+ def __repr__(self):
+ return "<RemoteSettingsChanged changed_settings:{%s}>" % (
+ ", ".join(repr(cs) for cs in self.changed_settings.values()),
+ )
+
+
+class PingReceived(Event):
+ """
+ The PingReceived event is fired whenever a PING is received. It contains
+ the 'opaque data' of the PING frame. A ping acknowledgment with the same
+ 'opaque data' is automatically emitted after receiving a ping.
+
+ .. versionadded:: 3.1.0
+ """
+ def __init__(self):
+ #: The data included on the ping.
+ self.ping_data = None
+
+ def __repr__(self):
+ return "<PingReceived ping_data:%s>" % (
+ _bytes_representation(self.ping_data),
+ )
+
+
+class PingAcknowledged(Event):
+ """
+ Same as PingAckReceived.
+
+ .. deprecated:: 3.1.0
+ """
+ def __init__(self):
+ #: The data included on the ping.
+ self.ping_data = None
+
+ def __repr__(self):
+ return "<PingAckReceived ping_data:%s>" % (
+ _bytes_representation(self.ping_data),
+ )
+
+
+class PingAckReceived(PingAcknowledged):
+ """
+ The PingAckReceived event is fired whenever a PING acknowledgment is
+ received. It contains the 'opaque data' of the PING+ACK frame, allowing the
+ user to correlate PINGs and calculate RTT.
+
+ .. versionadded:: 3.1.0
+ """
+ pass
+
+
+class StreamEnded(Event):
+ """
+ The StreamEnded event is fired whenever a stream is ended by a remote
+ party. The stream may not be fully closed if it has not been closed
+ locally, but no further data or headers should be expected on that stream.
+ """
+ def __init__(self):
+ #: The Stream ID of the stream that was closed.
+ self.stream_id = None
+
+ def __repr__(self):
+ return "<StreamEnded stream_id:%s>" % self.stream_id
+
+
+class StreamReset(Event):
+ """
+ The StreamReset event is fired in two situations. The first is when the
+ remote party forcefully resets the stream. The second is when the remote
+ party has made a protocol error which only affects a single stream. In this
+ case, Hyper-h2 will terminate the stream early and return this event.
+
+ .. versionchanged:: 2.0.0
+ This event is now fired when Hyper-h2 automatically resets a stream.
+ """
+ def __init__(self):
+ #: The Stream ID of the stream that was reset.
+ self.stream_id = None
+
+ #: The error code given. Either one of :class:`ErrorCodes
+ #: <h2.errors.ErrorCodes>` or ``int``
+ self.error_code = None
+
+ #: Whether the remote peer sent a RST_STREAM or we did.
+ self.remote_reset = True
+
+ def __repr__(self):
+ return "<StreamReset stream_id:%s, error_code:%s, remote_reset:%s>" % (
+ self.stream_id, self.error_code, self.remote_reset
+ )
+
+
+class PushedStreamReceived(Event):
+ """
+ The PushedStreamReceived event is fired whenever a pushed stream has been
+ received from a remote peer. The event carries on it the new stream ID, the
+ ID of the parent stream, and the request headers pushed by the remote peer.
+ """
+ def __init__(self):
+ #: The Stream ID of the stream created by the push.
+ self.pushed_stream_id = None
+
+ #: The Stream ID of the stream that the push is related to.
+ self.parent_stream_id = None
+
+ #: The request headers, sent by the remote party in the push.
+ self.headers = None
+
+ def __repr__(self):
+ return (
+ "<PushedStreamReceived pushed_stream_id:%s, parent_stream_id:%s, "
+ "headers:%s>" % (
+ self.pushed_stream_id,
+ self.parent_stream_id,
+ self.headers,
+ )
+ )
+
+
+class SettingsAcknowledged(Event):
+ """
+ The SettingsAcknowledged event is fired whenever a settings ACK is received
+ from the remote peer. The event carries on it the settings that were
+ acknowedged, in the same format as
+ :class:`h2.events.RemoteSettingsChanged`.
+ """
+ def __init__(self):
+ #: A dictionary of setting byte to
+ #: :class:`ChangedSetting <h2.settings.ChangedSetting>`, representing
+ #: the changed settings.
+ self.changed_settings = {}
+
+ def __repr__(self):
+ return "<SettingsAcknowledged changed_settings:{%s}>" % (
+ ", ".join(repr(cs) for cs in self.changed_settings.values()),
+ )
+
+
+class PriorityUpdated(Event):
+ """
+ The PriorityUpdated event is fired whenever a stream sends updated priority
+ information. This can occur when the stream is opened, or at any time
+ during the stream lifetime.
+
+ This event is purely advisory, and does not need to be acted on.
+
+ .. versionadded:: 2.0.0
+ """
+ def __init__(self):
+ #: The ID of the stream whose priority information is being updated.
+ self.stream_id = None
+
+ #: The new stream weight. May be the same as the original stream
+ #: weight. An integer between 1 and 256.
+ self.weight = None
+
+ #: The stream ID this stream now depends on. May be ``0``.
+ self.depends_on = None
+
+ #: Whether the stream *exclusively* depends on the parent stream. If it
+ #: does, this stream should inherit the current children of its new
+ #: parent.
+ self.exclusive = None
+
+ def __repr__(self):
+ return (
+ "<PriorityUpdated stream_id:%s, weight:%s, depends_on:%s, "
+ "exclusive:%s>" % (
+ self.stream_id,
+ self.weight,
+ self.depends_on,
+ self.exclusive
+ )
+ )
+
+
+class ConnectionTerminated(Event):
+ """
+ The ConnectionTerminated event is fired when a connection is torn down by
+ the remote peer using a GOAWAY frame. Once received, no further action may
+ be taken on the connection: a new connection must be established.
+ """
+ def __init__(self):
+ #: The error code cited when tearing down the connection. Should be
+ #: one of :class:`ErrorCodes <h2.errors.ErrorCodes>`, but may not be if
+ #: unknown HTTP/2 extensions are being used.
+ self.error_code = None
+
+ #: The stream ID of the last stream the remote peer saw. This can
+ #: provide an indication of what data, if any, never reached the remote
+ #: peer and so can safely be resent.
+ self.last_stream_id = None
+
+ #: Additional debug data that can be appended to GOAWAY frame.
+ self.additional_data = None
+
+ def __repr__(self):
+ return (
+ "<ConnectionTerminated error_code:%s, last_stream_id:%s, "
+ "additional_data:%s>" % (
+ self.error_code,
+ self.last_stream_id,
+ _bytes_representation(
+ self.additional_data[:20]
+ if self.additional_data else None)
+ )
+ )
+
+
+class AlternativeServiceAvailable(Event):
+ """
+ The AlternativeServiceAvailable event is fired when the remote peer
+ advertises an `RFC 7838 <https://tools.ietf.org/html/rfc7838>`_ Alternative
+ Service using an ALTSVC frame.
+
+ This event always carries the origin to which the ALTSVC information
+ applies. That origin is either supplied by the server directly, or inferred
+ by hyper-h2 from the ``:authority`` pseudo-header field that was sent by
+ the user when initiating a given stream.
+
+ This event also carries what RFC 7838 calls the "Alternative Service Field
+ Value", which is formatted like a HTTP header field and contains the
+ relevant alternative service information. Hyper-h2 does not parse or in any
+ way modify that information: the user is required to do that.
+
+ This event can only be fired on the client end of a connection.
+
+ .. versionadded:: 2.3.0
+ """
+ def __init__(self):
+ #: The origin to which the alternative service field value applies.
+ #: This field is either supplied by the server directly, or inferred by
+ #: hyper-h2 from the ``:authority`` pseudo-header field that was sent
+ #: by the user when initiating the stream on which the frame was
+ #: received.
+ self.origin = None
+
+ #: The ALTSVC field value. This contains information about the HTTP
+ #: alternative service being advertised by the server. Hyper-h2 does
+ #: not parse this field: it is left exactly as sent by the server. The
+ #: structure of the data in this field is given by `RFC 7838 Section 3
+ #: <https://tools.ietf.org/html/rfc7838#section-3>`_.
+ self.field_value = None
+
+ def __repr__(self):
+ return (
+ "<AlternativeServiceAvailable origin:%s, field_value:%s>" % (
+ self.origin.decode('utf-8', 'ignore'),
+ self.field_value.decode('utf-8', 'ignore'),
+ )
+ )
+
+
+class UnknownFrameReceived(Event):
+ """
+ The UnknownFrameReceived event is fired when the remote peer sends a frame
+ that hyper-h2 does not understand. This occurs primarily when the remote
+ peer is employing HTTP/2 extensions that hyper-h2 doesn't know anything
+ about.
+
+ RFC 7540 requires that HTTP/2 implementations ignore these frames. hyper-h2
+ does so. However, this event is fired to allow implementations to perform
+ special processing on those frames if needed (e.g. if the implementation
+ is capable of handling the frame itself).
+
+ .. versionadded:: 2.7.0
+ """
+ def __init__(self):
+ #: The hyperframe Frame object that encapsulates the received frame.
+ self.frame = None
+
+ def __repr__(self):
+ return "<UnknownFrameReceived>"
+
+
+def _bytes_representation(data):
+ """
+ Converts a bytestring into something that is safe to print on all Python
+ platforms.
+
+ This function is relatively expensive, so it should not be called on the
+ mainline of the code. It's safe to use in things like object repr methods
+ though.
+ """
+ if data is None:
+ return None
+
+ hex = binascii.hexlify(data)
+
+ # This is moderately clever: on all Python versions hexlify returns a byte
+ # string. On Python 3 we want an actual string, so we just check whether
+ # that's what we have.
+ if not isinstance(hex, str): # pragma: no cover
+ hex = hex.decode('ascii')
+
+ return hex
diff --git a/testing/web-platform/tests/tools/third_party/h2/h2/exceptions.py b/testing/web-platform/tests/tools/third_party/h2/h2/exceptions.py
new file mode 100644
index 0000000000..388f9e9a38
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/h2/exceptions.py
@@ -0,0 +1,186 @@
+# -*- coding: utf-8 -*-
+"""
+h2/exceptions
+~~~~~~~~~~~~~
+
+Exceptions for the HTTP/2 module.
+"""
+import h2.errors
+
+
+class H2Error(Exception):
+ """
+ The base class for all exceptions for the HTTP/2 module.
+ """
+
+
+class ProtocolError(H2Error):
+ """
+ An action was attempted in violation of the HTTP/2 protocol.
+ """
+ #: The error code corresponds to this kind of Protocol Error.
+ error_code = h2.errors.ErrorCodes.PROTOCOL_ERROR
+
+
+class FrameTooLargeError(ProtocolError):
+ """
+ The frame that we tried to send or that we received was too large.
+ """
+ #: This error code that corresponds to this kind of Protocol Error.
+ error_code = h2.errors.ErrorCodes.FRAME_SIZE_ERROR
+
+
+class FrameDataMissingError(ProtocolError):
+ """
+ The frame that we received is missing some data.
+
+ .. versionadded:: 2.0.0
+ """
+ #: The error code that corresponds to this kind of Protocol Error
+ error_code = h2.errors.ErrorCodes.FRAME_SIZE_ERROR
+
+
+class TooManyStreamsError(ProtocolError):
+ """
+ An attempt was made to open a stream that would lead to too many concurrent
+ streams.
+ """
+ pass
+
+
+class FlowControlError(ProtocolError):
+ """
+ An attempted action violates flow control constraints.
+ """
+ #: The error code that corresponds to this kind of
+ #: :class:`ProtocolError <h2.exceptions.ProtocolError>`
+ error_code = h2.errors.ErrorCodes.FLOW_CONTROL_ERROR
+
+
+class StreamIDTooLowError(ProtocolError):
+ """
+ An attempt was made to open a stream that had an ID that is lower than the
+ highest ID we have seen on this connection.
+ """
+ def __init__(self, stream_id, max_stream_id):
+ #: The ID of the stream that we attempted to open.
+ self.stream_id = stream_id
+
+ #: The current highest-seen stream ID.
+ self.max_stream_id = max_stream_id
+
+ def __str__(self):
+ return "StreamIDTooLowError: %d is lower than %d" % (
+ self.stream_id, self.max_stream_id
+ )
+
+
+class NoAvailableStreamIDError(ProtocolError):
+ """
+ There are no available stream IDs left to the connection. All stream IDs
+ have been exhausted.
+
+ .. versionadded:: 2.0.0
+ """
+ pass
+
+
+class NoSuchStreamError(ProtocolError):
+ """
+ A stream-specific action referenced a stream that does not exist.
+
+ .. versionchanged:: 2.0.0
+ Became a subclass of :class:`ProtocolError
+ <h2.exceptions.ProtocolError>`
+ """
+ def __init__(self, stream_id):
+ #: The stream ID that corresponds to the non-existent stream.
+ self.stream_id = stream_id
+
+
+class StreamClosedError(NoSuchStreamError):
+ """
+ A more specific form of
+ :class:`NoSuchStreamError <h2.exceptions.NoSuchStreamError>`. Indicates
+ that the stream has since been closed, and that all state relating to that
+ stream has been removed.
+ """
+ def __init__(self, stream_id):
+ #: The stream ID that corresponds to the nonexistent stream.
+ self.stream_id = stream_id
+
+ #: The relevant HTTP/2 error code.
+ self.error_code = h2.errors.ErrorCodes.STREAM_CLOSED
+
+ # Any events that internal code may need to fire. Not relevant to
+ # external users that may receive a StreamClosedError.
+ self._events = []
+
+
+class InvalidSettingsValueError(ProtocolError, ValueError):
+ """
+ An attempt was made to set an invalid Settings value.
+
+ .. versionadded:: 2.0.0
+ """
+ def __init__(self, msg, error_code):
+ super(InvalidSettingsValueError, self).__init__(msg)
+ self.error_code = error_code
+
+
+class InvalidBodyLengthError(ProtocolError):
+ """
+ The remote peer sent more or less data that the Content-Length header
+ indicated.
+
+ .. versionadded:: 2.0.0
+ """
+ def __init__(self, expected, actual):
+ self.expected_length = expected
+ self.actual_length = actual
+
+ def __str__(self):
+ return "InvalidBodyLengthError: Expected %d bytes, received %d" % (
+ self.expected_length, self.actual_length
+ )
+
+
+class UnsupportedFrameError(ProtocolError, KeyError):
+ """
+ The remote peer sent a frame that is unsupported in this context.
+
+ .. versionadded:: 2.1.0
+ """
+ # TODO: Remove the KeyError in 3.0.0
+ pass
+
+
+class RFC1122Error(H2Error):
+ """
+ Emitted when users attempt to do something that is literally allowed by the
+ relevant RFC, but is sufficiently ill-defined that it's unwise to allow
+ users to actually do it.
+
+ While there is some disagreement about whether or not we should be liberal
+ in what accept, it is a truth universally acknowledged that we should be
+ conservative in what emit.
+
+ .. versionadded:: 2.4.0
+ """
+ # shazow says I'm going to regret naming the exception this way. If that
+ # turns out to be true, TELL HIM NOTHING.
+ pass
+
+
+class DenialOfServiceError(ProtocolError):
+ """
+ Emitted when the remote peer exhibits a behaviour that is likely to be an
+ attempt to perform a Denial of Service attack on the implementation. This
+ is a form of ProtocolError that carries a different error code, and allows
+ more easy detection of this kind of behaviour.
+
+ .. versionadded:: 2.5.0
+ """
+ #: The error code that corresponds to this kind of
+ #: :class:`ProtocolError <h2.exceptions.ProtocolError>`
+ error_code = h2.errors.ErrorCodes.ENHANCE_YOUR_CALM
diff --git a/testing/web-platform/tests/tools/third_party/h2/h2/frame_buffer.py b/testing/web-platform/tests/tools/third_party/h2/h2/frame_buffer.py
new file mode 100644
index 0000000000..e79f6ec2de
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/h2/frame_buffer.py
@@ -0,0 +1,175 @@
+# -*- coding: utf-8 -*-
+"""
+h2/frame_buffer
+~~~~~~~~~~~~~~~
+
+A data structure that provides a way to iterate over a byte buffer in terms of
+frames.
+"""
+from hyperframe.exceptions import InvalidFrameError
+from hyperframe.frame import (
+ Frame, HeadersFrame, ContinuationFrame, PushPromiseFrame
+)
+
+from .exceptions import (
+ ProtocolError, FrameTooLargeError, FrameDataMissingError
+)
+
+# To avoid a DOS attack based on sending loads of continuation frames, we limit
+# the maximum number we're perpared to receive. In this case, we'll set the
+# limit to 64, which means the largest encoded header block we can receive by
+# default is 262144 bytes long, and the largest possible *at all* is 1073741760
+# bytes long.
+#
+# This value seems reasonable for now, but in future we may want to evaluate
+# making it configurable.
+CONTINUATION_BACKLOG = 64
+
+
+class FrameBuffer(object):
+ """
+ This is a data structure that expects to act as a buffer for HTTP/2 data
+ that allows iteraton in terms of H2 frames.
+ """
+ def __init__(self, server=False):
+ self.data = b''
+ self.max_frame_size = 0
+ self._preamble = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' if server else b''
+ self._preamble_len = len(self._preamble)
+ self._headers_buffer = []
+
+ def add_data(self, data):
+ """
+ Add more data to the frame buffer.
+
+ :param data: A bytestring containing the byte buffer.
+ """
+ if self._preamble_len:
+ data_len = len(data)
+ of_which_preamble = min(self._preamble_len, data_len)
+
+ if self._preamble[:of_which_preamble] != data[:of_which_preamble]:
+ raise ProtocolError("Invalid HTTP/2 preamble.")
+
+ data = data[of_which_preamble:]
+ self._preamble_len -= of_which_preamble
+ self._preamble = self._preamble[of_which_preamble:]
+
+ self.data += data
+
+ def _parse_frame_header(self, data):
+ """
+ Parses the frame header from the data. Either returns a tuple of
+ (frame, length), or throws an exception. The returned frame may be None
+ if the frame is of unknown type.
+ """
+ try:
+ frame, length = Frame.parse_frame_header(data[:9])
+ except ValueError as e:
+ # The frame header is invalid. This is a ProtocolError
+ raise ProtocolError("Invalid frame header received: %s" % str(e))
+
+ return frame, length
+
+ def _validate_frame_length(self, length):
+ """
+ Confirm that the frame is an appropriate length.
+ """
+ if length > self.max_frame_size:
+ raise FrameTooLargeError(
+ "Received overlong frame: length %d, max %d" %
+ (length, self.max_frame_size)
+ )
+
+ def _update_header_buffer(self, f):
+ """
+ Updates the internal header buffer. Returns a frame that should replace
+ the current one. May throw exceptions if this frame is invalid.
+ """
+ # Check if we're in the middle of a headers block. If we are, this
+ # frame *must* be a CONTINUATION frame with the same stream ID as the
+ # leading HEADERS or PUSH_PROMISE frame. Anything else is a
+ # ProtocolError. If the frame *is* valid, append it to the header
+ # buffer.
+ if self._headers_buffer:
+ stream_id = self._headers_buffer[0].stream_id
+ valid_frame = (
+ f is not None and
+ isinstance(f, ContinuationFrame) and
+ f.stream_id == stream_id
+ )
+ if not valid_frame:
+ raise ProtocolError("Invalid frame during header block.")
+
+ # Append the frame to the buffer.
+ self._headers_buffer.append(f)
+ if len(self._headers_buffer) > CONTINUATION_BACKLOG:
+ raise ProtocolError("Too many continuation frames received.")
+
+ # If this is the end of the header block, then we want to build a
+ # mutant HEADERS frame that's massive. Use the original one we got,
+ # then set END_HEADERS and set its data appopriately. If it's not
+ # the end of the block, lose the current frame: we can't yield it.
+ if 'END_HEADERS' in f.flags:
+ f = self._headers_buffer[0]
+ f.flags.add('END_HEADERS')
+ f.data = b''.join(x.data for x in self._headers_buffer)
+ self._headers_buffer = []
+ else:
+ f = None
+ elif (isinstance(f, (HeadersFrame, PushPromiseFrame)) and
+ 'END_HEADERS' not in f.flags):
+ # This is the start of a headers block! Save the frame off and then
+ # act like we didn't receive one.
+ self._headers_buffer.append(f)
+ f = None
+
+ return f
+
+ # The methods below support the iterator protocol.
+ def __iter__(self):
+ return self
+
+ def next(self): # Python 2
+ # First, check that we have enough data to successfully parse the
+ # next frame header. If not, bail. Otherwise, parse it.
+ if len(self.data) < 9:
+ raise StopIteration()
+
+ try:
+ f, length = self._parse_frame_header(self.data)
+ except InvalidFrameError: # pragma: no cover
+ raise ProtocolError("Received frame with invalid frame header.")
+
+ # Next, check that we have enough length to parse the frame body. If
+ # not, bail, leaving the frame header data in the buffer for next time.
+ if len(self.data) < length + 9:
+ raise StopIteration()
+
+ # Confirm the frame has an appropriate length.
+ self._validate_frame_length(length)
+
+ # Don't try to parse the body if we didn't get a frame we know about:
+ # there's nothing we can do with it anyway.
+ if f is not None:
+ try:
+ f.parse_body(memoryview(self.data[9:9+length]))
+ except InvalidFrameError:
+ raise FrameDataMissingError("Frame data missing or invalid")
+
+ # At this point, as we know we'll use or discard the entire frame, we
+ # can update the data.
+ self.data = self.data[9+length:]
+
+ # Pass the frame through the header buffer.
+ f = self._update_header_buffer(f)
+
+ # If we got a frame we didn't understand or shouldn't yield, rather
+ # than return None it'd be better if we just tried to get the next
+ # frame in the sequence instead. Recurse back into ourselves to do
+ # that. This is safe because the amount of work we have to do here is
+ # strictly bounded by the length of the buffer.
+ return f if f is not None else self.next()
+
+ def __next__(self): # Python 3
+ return self.next()
diff --git a/testing/web-platform/tests/tools/third_party/h2/h2/settings.py b/testing/web-platform/tests/tools/third_party/h2/h2/settings.py
new file mode 100644
index 0000000000..bf87c94f4e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/h2/settings.py
@@ -0,0 +1,339 @@
+# -*- coding: utf-8 -*-
+"""
+h2/settings
+~~~~~~~~~~~
+
+This module contains a HTTP/2 settings object. This object provides a simple
+API for manipulating HTTP/2 settings, keeping track of both the current active
+state of the settings and the unacknowledged future values of the settings.
+"""
+import collections
+import enum
+
+from hyperframe.frame import SettingsFrame
+
+from h2.errors import ErrorCodes
+from h2.exceptions import InvalidSettingsValueError
+
+try:
+ from collections.abc import MutableMapping
+except ImportError: # pragma: no cover
+ # Python 2.7 compatibility
+ from collections import MutableMapping
+
+
+class SettingCodes(enum.IntEnum):
+ """
+ All known HTTP/2 setting codes.
+
+ .. versionadded:: 2.6.0
+ """
+
+ #: Allows the sender to inform the remote endpoint of the maximum size of
+ #: the header compression table used to decode header blocks, in octets.
+ HEADER_TABLE_SIZE = SettingsFrame.HEADER_TABLE_SIZE
+
+ #: This setting can be used to disable server push. To disable server push
+ #: on a client, set this to 0.
+ ENABLE_PUSH = SettingsFrame.ENABLE_PUSH
+
+ #: Indicates the maximum number of concurrent streams that the sender will
+ #: allow.
+ MAX_CONCURRENT_STREAMS = SettingsFrame.MAX_CONCURRENT_STREAMS
+
+ #: Indicates the sender's initial window size (in octets) for stream-level
+ #: flow control.
+ INITIAL_WINDOW_SIZE = SettingsFrame.INITIAL_WINDOW_SIZE
+
+ #: Indicates the size of the largest frame payload that the sender is
+ #: willing to receive, in octets.
+ MAX_FRAME_SIZE = SettingsFrame.MAX_FRAME_SIZE
+
+ #: This advisory setting informs a peer of the maximum size of header list
+ #: that the sender is prepared to accept, in octets. The value is based on
+ #: the uncompressed size of header fields, including the length of the name
+ #: and value in octets plus an overhead of 32 octets for each header field.
+ MAX_HEADER_LIST_SIZE = SettingsFrame.MAX_HEADER_LIST_SIZE
+
+ #: This setting can be used to enable the connect protocol. To enable on a
+ #: client set this to 1.
+ ENABLE_CONNECT_PROTOCOL = SettingsFrame.ENABLE_CONNECT_PROTOCOL
+
+
+def _setting_code_from_int(code):
+ """
+ Given an integer setting code, returns either one of :class:`SettingCodes
+ <h2.settings.SettingCodes>` or, if not present in the known set of codes,
+ returns the integer directly.
+ """
+ try:
+ return SettingCodes(code)
+ except ValueError:
+ return code
+
+
+class ChangedSetting:
+
+ def __init__(self, setting, original_value, new_value):
+ #: The setting code given. Either one of :class:`SettingCodes
+ #: <h2.settings.SettingCodes>` or ``int``
+ #:
+ #: .. versionchanged:: 2.6.0
+ self.setting = setting
+
+ #: The original value before being changed.
+ self.original_value = original_value
+
+ #: The new value after being changed.
+ self.new_value = new_value
+
+ def __repr__(self):
+ return (
+ "ChangedSetting(setting=%s, original_value=%s, "
+ "new_value=%s)"
+ ) % (
+ self.setting,
+ self.original_value,
+ self.new_value
+ )
+
+
+class Settings(MutableMapping):
+ """
+ An object that encapsulates HTTP/2 settings state.
+
+ HTTP/2 Settings are a complex beast. Each party, remote and local, has its
+ own settings and a view of the other party's settings. When a settings
+ frame is emitted by a peer it cannot assume that the new settings values
+ are in place until the remote peer acknowledges the setting. In principle,
+ multiple settings changes can be "in flight" at the same time, all with
+ different values.
+
+ This object encapsulates this mess. It provides a dict-like interface to
+ settings, which return the *current* values of the settings in question.
+ Additionally, it keeps track of the stack of proposed values: each time an
+ acknowledgement is sent/received, it updates the current values with the
+ stack of proposed values. On top of all that, it validates the values to
+ make sure they're allowed, and raises :class:`InvalidSettingsValueError
+ <h2.exceptions.InvalidSettingsValueError>` if they are not.
+
+ Finally, this object understands what the default values of the HTTP/2
+ settings are, and sets those defaults appropriately.
+
+ .. versionchanged:: 2.2.0
+ Added the ``initial_values`` parameter.
+
+ .. versionchanged:: 2.5.0
+ Added the ``max_header_list_size`` property.
+
+ :param client: (optional) Whether these settings should be defaulted for a
+ client implementation or a server implementation. Defaults to ``True``.
+ :type client: ``bool``
+ :param initial_values: (optional) Any initial values the user would like
+ set, rather than RFC 7540's defaults.
+ :type initial_vales: ``MutableMapping``
+ """
+ def __init__(self, client=True, initial_values=None):
+ # Backing object for the settings. This is a dictionary of
+ # (setting: [list of values]), where the first value in the list is the
+ # current value of the setting. Strictly this doesn't use lists but
+ # instead uses collections.deque to avoid repeated memory allocations.
+ #
+ # This contains the default values for HTTP/2.
+ self._settings = {
+ SettingCodes.HEADER_TABLE_SIZE: collections.deque([4096]),
+ SettingCodes.ENABLE_PUSH: collections.deque([int(client)]),
+ SettingCodes.INITIAL_WINDOW_SIZE: collections.deque([65535]),
+ SettingCodes.MAX_FRAME_SIZE: collections.deque([16384]),
+ SettingCodes.ENABLE_CONNECT_PROTOCOL: collections.deque([0]),
+ }
+ if initial_values is not None:
+ for key, value in initial_values.items():
+ invalid = _validate_setting(key, value)
+ if invalid:
+ raise InvalidSettingsValueError(
+ "Setting %d has invalid value %d" % (key, value),
+ error_code=invalid
+ )
+ self._settings[key] = collections.deque([value])
+
+ def acknowledge(self):
+ """
+ The settings have been acknowledged, either by the user (remote
+ settings) or by the remote peer (local settings).
+
+ :returns: A dict of {setting: ChangedSetting} that were applied.
+ """
+ changed_settings = {}
+
+ # If there is more than one setting in the list, we have a setting
+ # value outstanding. Update them.
+ for k, v in self._settings.items():
+ if len(v) > 1:
+ old_setting = v.popleft()
+ new_setting = v[0]
+ changed_settings[k] = ChangedSetting(
+ k, old_setting, new_setting
+ )
+
+ return changed_settings
+
+ # Provide easy-access to well known settings.
+ @property
+ def header_table_size(self):
+ """
+ The current value of the :data:`HEADER_TABLE_SIZE
+ <h2.settings.SettingCodes.HEADER_TABLE_SIZE>` setting.
+ """
+ return self[SettingCodes.HEADER_TABLE_SIZE]
+
+ @header_table_size.setter
+ def header_table_size(self, value):
+ self[SettingCodes.HEADER_TABLE_SIZE] = value
+
+ @property
+ def enable_push(self):
+ """
+ The current value of the :data:`ENABLE_PUSH
+ <h2.settings.SettingCodes.ENABLE_PUSH>` setting.
+ """
+ return self[SettingCodes.ENABLE_PUSH]
+
+ @enable_push.setter
+ def enable_push(self, value):
+ self[SettingCodes.ENABLE_PUSH] = value
+
+ @property
+ def initial_window_size(self):
+ """
+ The current value of the :data:`INITIAL_WINDOW_SIZE
+ <h2.settings.SettingCodes.INITIAL_WINDOW_SIZE>` setting.
+ """
+ return self[SettingCodes.INITIAL_WINDOW_SIZE]
+
+ @initial_window_size.setter
+ def initial_window_size(self, value):
+ self[SettingCodes.INITIAL_WINDOW_SIZE] = value
+
+ @property
+ def max_frame_size(self):
+ """
+ The current value of the :data:`MAX_FRAME_SIZE
+ <h2.settings.SettingCodes.MAX_FRAME_SIZE>` setting.
+ """
+ return self[SettingCodes.MAX_FRAME_SIZE]
+
+ @max_frame_size.setter
+ def max_frame_size(self, value):
+ self[SettingCodes.MAX_FRAME_SIZE] = value
+
+ @property
+ def max_concurrent_streams(self):
+ """
+ The current value of the :data:`MAX_CONCURRENT_STREAMS
+ <h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS>` setting.
+ """
+ return self.get(SettingCodes.MAX_CONCURRENT_STREAMS, 2**32+1)
+
+ @max_concurrent_streams.setter
+ def max_concurrent_streams(self, value):
+ self[SettingCodes.MAX_CONCURRENT_STREAMS] = value
+
+ @property
+ def max_header_list_size(self):
+ """
+ The current value of the :data:`MAX_HEADER_LIST_SIZE
+ <h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE>` setting. If not set,
+ returns ``None``, which means unlimited.
+
+ .. versionadded:: 2.5.0
+ """
+ return self.get(SettingCodes.MAX_HEADER_LIST_SIZE, None)
+
+ @max_header_list_size.setter
+ def max_header_list_size(self, value):
+ self[SettingCodes.MAX_HEADER_LIST_SIZE] = value
+
+ @property
+ def enable_connect_protocol(self):
+ """
+ The current value of the :data:`ENABLE_CONNECT_PROTOCOL
+ <h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL>` setting.
+ """
+ return self[SettingCodes.ENABLE_CONNECT_PROTOCOL]
+
+ @enable_connect_protocol.setter
+ def enable_connect_protocol(self, value):
+ self[SettingCodes.ENABLE_CONNECT_PROTOCOL] = value
+
+ # Implement the MutableMapping API.
+ def __getitem__(self, key):
+ val = self._settings[key][0]
+
+ # Things that were created when a setting was received should stay
+ # KeyError'd.
+ if val is None:
+ raise KeyError
+
+ return val
+
+ def __setitem__(self, key, value):
+ invalid = _validate_setting(key, value)
+ if invalid:
+ raise InvalidSettingsValueError(
+ "Setting %d has invalid value %d" % (key, value),
+ error_code=invalid
+ )
+
+ try:
+ items = self._settings[key]
+ except KeyError:
+ items = collections.deque([None])
+ self._settings[key] = items
+
+ items.append(value)
+
+ def __delitem__(self, key):
+ del self._settings[key]
+
+ def __iter__(self):
+ return self._settings.__iter__()
+
+ def __len__(self):
+ return len(self._settings)
+
+ def __eq__(self, other):
+ if isinstance(other, Settings):
+ return self._settings == other._settings
+ else:
+ return NotImplemented
+
+ def __ne__(self, other):
+ if isinstance(other, Settings):
+ return not self == other
+ else:
+ return NotImplemented
+
+
+def _validate_setting(setting, value): # noqa: C901
+ """
+ Confirms that a specific setting has a well-formed value. If the setting is
+ invalid, returns an error code. Otherwise, returns 0 (NO_ERROR).
+ """
+ if setting == SettingCodes.ENABLE_PUSH:
+ if value not in (0, 1):
+ return ErrorCodes.PROTOCOL_ERROR
+ elif setting == SettingCodes.INITIAL_WINDOW_SIZE:
+ if not 0 <= value <= 2147483647: # 2^31 - 1
+ return ErrorCodes.FLOW_CONTROL_ERROR
+ elif setting == SettingCodes.MAX_FRAME_SIZE:
+ if not 16384 <= value <= 16777215: # 2^14 and 2^24 - 1
+ return ErrorCodes.PROTOCOL_ERROR
+ elif setting == SettingCodes.MAX_HEADER_LIST_SIZE:
+ if value < 0:
+ return ErrorCodes.PROTOCOL_ERROR
+ elif setting == SettingCodes.ENABLE_CONNECT_PROTOCOL:
+ if value not in (0, 1):
+ return ErrorCodes.PROTOCOL_ERROR
+
+ return 0
diff --git a/testing/web-platform/tests/tools/third_party/h2/h2/stream.py b/testing/web-platform/tests/tools/third_party/h2/h2/stream.py
new file mode 100644
index 0000000000..1cb91786d4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/h2/stream.py
@@ -0,0 +1,1369 @@
+# -*- coding: utf-8 -*-
+"""
+h2/stream
+~~~~~~~~~
+
+An implementation of a HTTP/2 stream.
+"""
+from enum import Enum, IntEnum
+from hpack import HeaderTuple
+from hyperframe.frame import (
+ HeadersFrame, ContinuationFrame, DataFrame, WindowUpdateFrame,
+ RstStreamFrame, PushPromiseFrame, AltSvcFrame
+)
+
+from .errors import ErrorCodes, _error_code_from_int
+from .events import (
+ RequestReceived, ResponseReceived, DataReceived, WindowUpdated,
+ StreamEnded, PushedStreamReceived, StreamReset, TrailersReceived,
+ InformationalResponseReceived, AlternativeServiceAvailable,
+ _ResponseSent, _RequestSent, _TrailersSent, _PushedRequestSent
+)
+from .exceptions import (
+ ProtocolError, StreamClosedError, InvalidBodyLengthError, FlowControlError
+)
+from .utilities import (
+ guard_increment_window, is_informational_response, authority_from_headers,
+ validate_headers, validate_outbound_headers, normalize_outbound_headers,
+ HeaderValidationFlags, extract_method_header, normalize_inbound_headers
+)
+from .windows import WindowManager
+
+
+class StreamState(IntEnum):
+ IDLE = 0
+ RESERVED_REMOTE = 1
+ RESERVED_LOCAL = 2
+ OPEN = 3
+ HALF_CLOSED_REMOTE = 4
+ HALF_CLOSED_LOCAL = 5
+ CLOSED = 6
+
+
+class StreamInputs(Enum):
+ SEND_HEADERS = 0
+ SEND_PUSH_PROMISE = 1
+ SEND_RST_STREAM = 2
+ SEND_DATA = 3
+ SEND_WINDOW_UPDATE = 4
+ SEND_END_STREAM = 5
+ RECV_HEADERS = 6
+ RECV_PUSH_PROMISE = 7
+ RECV_RST_STREAM = 8
+ RECV_DATA = 9
+ RECV_WINDOW_UPDATE = 10
+ RECV_END_STREAM = 11
+ RECV_CONTINUATION = 12 # Added in 2.0.0
+ SEND_INFORMATIONAL_HEADERS = 13 # Added in 2.2.0
+ RECV_INFORMATIONAL_HEADERS = 14 # Added in 2.2.0
+ SEND_ALTERNATIVE_SERVICE = 15 # Added in 2.3.0
+ RECV_ALTERNATIVE_SERVICE = 16 # Added in 2.3.0
+ UPGRADE_CLIENT = 17 # Added 2.3.0
+ UPGRADE_SERVER = 18 # Added 2.3.0
+
+
+class StreamClosedBy(Enum):
+ SEND_END_STREAM = 0
+ RECV_END_STREAM = 1
+ SEND_RST_STREAM = 2
+ RECV_RST_STREAM = 3
+
+
+# This array is initialized once, and is indexed by the stream states above.
+# It indicates whether a stream in the given state is open. The reason we do
+# this is that we potentially check whether a stream in a given state is open
+# quite frequently: given that we check so often, we should do so in the
+# fastest and most performant way possible.
+STREAM_OPEN = [False for _ in range(0, len(StreamState))]
+STREAM_OPEN[StreamState.OPEN] = True
+STREAM_OPEN[StreamState.HALF_CLOSED_LOCAL] = True
+STREAM_OPEN[StreamState.HALF_CLOSED_REMOTE] = True
+
+
+class H2StreamStateMachine(object):
+ """
+ A single HTTP/2 stream state machine.
+
+ This stream object implements basically the state machine described in
+ RFC 7540 section 5.1.
+
+ :param stream_id: The stream ID of this stream. This is stored primarily
+ for logging purposes.
+ """
+ def __init__(self, stream_id):
+ self.state = StreamState.IDLE
+ self.stream_id = stream_id
+
+ #: Whether this peer is the client side of this stream.
+ self.client = None
+
+ # Whether trailers have been sent/received on this stream or not.
+ self.headers_sent = None
+ self.trailers_sent = None
+ self.headers_received = None
+ self.trailers_received = None
+
+ # How the stream was closed. One of StreamClosedBy.
+ self.stream_closed_by = None
+
+ def process_input(self, input_):
+ """
+ Process a specific input in the state machine.
+ """
+ if not isinstance(input_, StreamInputs):
+ raise ValueError("Input must be an instance of StreamInputs")
+
+ try:
+ func, target_state = _transitions[(self.state, input_)]
+ except KeyError:
+ old_state = self.state
+ self.state = StreamState.CLOSED
+ raise ProtocolError(
+ "Invalid input %s in state %s" % (input_, old_state)
+ )
+ else:
+ previous_state = self.state
+ self.state = target_state
+ if func is not None:
+ try:
+ return func(self, previous_state)
+ except ProtocolError:
+ self.state = StreamState.CLOSED
+ raise
+ except AssertionError as e: # pragma: no cover
+ self.state = StreamState.CLOSED
+ raise ProtocolError(e)
+
+ return []
+
+ def request_sent(self, previous_state):
+ """
+ Fires when a request is sent.
+ """
+ self.client = True
+ self.headers_sent = True
+ event = _RequestSent()
+
+ return [event]
+
+ def response_sent(self, previous_state):
+ """
+ Fires when something that should be a response is sent. This 'response'
+ may actually be trailers.
+ """
+ if not self.headers_sent:
+ if self.client is True or self.client is None:
+ raise ProtocolError("Client cannot send responses.")
+ self.headers_sent = True
+ event = _ResponseSent()
+ else:
+ assert not self.trailers_sent
+ self.trailers_sent = True
+ event = _TrailersSent()
+
+ return [event]
+
+ def request_received(self, previous_state):
+ """
+ Fires when a request is received.
+ """
+ assert not self.headers_received
+ assert not self.trailers_received
+
+ self.client = False
+ self.headers_received = True
+ event = RequestReceived()
+
+ event.stream_id = self.stream_id
+ return [event]
+
+ def response_received(self, previous_state):
+ """
+ Fires when a response is received. Also disambiguates between responses
+ and trailers.
+ """
+ if not self.headers_received:
+ assert self.client is True
+ self.headers_received = True
+ event = ResponseReceived()
+ else:
+ assert not self.trailers_received
+ self.trailers_received = True
+ event = TrailersReceived()
+
+ event.stream_id = self.stream_id
+ return [event]
+
+ def data_received(self, previous_state):
+ """
+ Fires when data is received.
+ """
+ event = DataReceived()
+ event.stream_id = self.stream_id
+ return [event]
+
+ def window_updated(self, previous_state):
+ """
+ Fires when a window update frame is received.
+ """
+ event = WindowUpdated()
+ event.stream_id = self.stream_id
+ return [event]
+
+ def stream_half_closed(self, previous_state):
+ """
+ Fires when an END_STREAM flag is received in the OPEN state,
+ transitioning this stream to a HALF_CLOSED_REMOTE state.
+ """
+ event = StreamEnded()
+ event.stream_id = self.stream_id
+ return [event]
+
+ def stream_ended(self, previous_state):
+ """
+ Fires when a stream is cleanly ended.
+ """
+ self.stream_closed_by = StreamClosedBy.RECV_END_STREAM
+ event = StreamEnded()
+ event.stream_id = self.stream_id
+ return [event]
+
+ def stream_reset(self, previous_state):
+ """
+ Fired when a stream is forcefully reset.
+ """
+ self.stream_closed_by = StreamClosedBy.RECV_RST_STREAM
+ event = StreamReset()
+ event.stream_id = self.stream_id
+ return [event]
+
+ def send_new_pushed_stream(self, previous_state):
+ """
+ Fires on the newly pushed stream, when pushed by the local peer.
+
+ No event here, but definitionally this peer must be a server.
+ """
+ assert self.client is None
+ self.client = False
+ self.headers_received = True
+ return []
+
+ def recv_new_pushed_stream(self, previous_state):
+ """
+ Fires on the newly pushed stream, when pushed by the remote peer.
+
+ No event here, but definitionally this peer must be a client.
+ """
+ assert self.client is None
+ self.client = True
+ self.headers_sent = True
+ return []
+
+ def send_push_promise(self, previous_state):
+ """
+ Fires on the already-existing stream when a PUSH_PROMISE frame is sent.
+ We may only send PUSH_PROMISE frames if we're a server.
+ """
+ if self.client is True:
+ raise ProtocolError("Cannot push streams from client peers.")
+
+ event = _PushedRequestSent()
+ return [event]
+
+ def recv_push_promise(self, previous_state):
+ """
+ Fires on the already-existing stream when a PUSH_PROMISE frame is
+ received. We may only receive PUSH_PROMISE frames if we're a client.
+
+ Fires a PushedStreamReceived event.
+ """
+ if not self.client:
+ if self.client is None: # pragma: no cover
+ msg = "Idle streams cannot receive pushes"
+ else: # pragma: no cover
+ msg = "Cannot receive pushed streams as a server"
+ raise ProtocolError(msg)
+
+ event = PushedStreamReceived()
+ event.parent_stream_id = self.stream_id
+ return [event]
+
+ def send_end_stream(self, previous_state):
+ """
+ Called when an attempt is made to send END_STREAM in the
+ HALF_CLOSED_REMOTE state.
+ """
+ self.stream_closed_by = StreamClosedBy.SEND_END_STREAM
+
+ def send_reset_stream(self, previous_state):
+ """
+ Called when an attempt is made to send RST_STREAM in a non-closed
+ stream state.
+ """
+ self.stream_closed_by = StreamClosedBy.SEND_RST_STREAM
+
+ def reset_stream_on_error(self, previous_state):
+ """
+ Called when we need to forcefully emit another RST_STREAM frame on
+ behalf of the state machine.
+
+ If this is the first time we've done this, we should also hang an event
+ off the StreamClosedError so that the user can be informed. We know
+ it's the first time we've done this if the stream is currently in a
+ state other than CLOSED.
+ """
+ self.stream_closed_by = StreamClosedBy.SEND_RST_STREAM
+
+ error = StreamClosedError(self.stream_id)
+
+ event = StreamReset()
+ event.stream_id = self.stream_id
+ event.error_code = ErrorCodes.STREAM_CLOSED
+ event.remote_reset = False
+ error._events = [event]
+ raise error
+
+ def recv_on_closed_stream(self, previous_state):
+ """
+ Called when an unexpected frame is received on an already-closed
+ stream.
+
+ An endpoint that receives an unexpected frame should treat it as
+ a stream error or connection error with type STREAM_CLOSED, depending
+ on the specific frame. The error handling is done at a higher level:
+ this just raises the appropriate error.
+ """
+ raise StreamClosedError(self.stream_id)
+
+ def send_on_closed_stream(self, previous_state):
+ """
+ Called when an attempt is made to send data on an already-closed
+ stream.
+
+ This essentially overrides the standard logic by throwing a
+ more-specific error: StreamClosedError. This is a ProtocolError, so it
+ matches the standard API of the state machine, but provides more detail
+ to the user.
+ """
+ raise StreamClosedError(self.stream_id)
+
+ def recv_push_on_closed_stream(self, previous_state):
+ """
+ Called when a PUSH_PROMISE frame is received on a full stop
+ stream.
+
+ If the stream was closed by us sending a RST_STREAM frame, then we
+ presume that the PUSH_PROMISE was in flight when we reset the parent
+ stream. Rathen than accept the new stream, we just reset it.
+ Otherwise, we should call this a PROTOCOL_ERROR: pushing a stream on a
+ naturally closed stream is a real problem because it creates a brand
+ new stream that the remote peer now believes exists.
+ """
+ assert self.stream_closed_by is not None
+
+ if self.stream_closed_by == StreamClosedBy.SEND_RST_STREAM:
+ raise StreamClosedError(self.stream_id)
+ else:
+ raise ProtocolError("Attempted to push on closed stream.")
+
+ def send_push_on_closed_stream(self, previous_state):
+ """
+ Called when an attempt is made to push on an already-closed stream.
+
+ This essentially overrides the standard logic by providing a more
+ useful error message. It's necessary because simply indicating that the
+ stream is closed is not enough: there is now a new stream that is not
+ allowed to be there. The only recourse is to tear the whole connection
+ down.
+ """
+ raise ProtocolError("Attempted to push on closed stream.")
+
+ def send_informational_response(self, previous_state):
+ """
+ Called when an informational header block is sent (that is, a block
+ where the :status header has a 1XX value).
+
+ Only enforces that these are sent *before* final headers are sent.
+ """
+ if self.headers_sent:
+ raise ProtocolError("Information response after final response")
+
+ event = _ResponseSent()
+ return [event]
+
+ def recv_informational_response(self, previous_state):
+ """
+ Called when an informational header block is received (that is, a block
+ where the :status header has a 1XX value).
+ """
+ if self.headers_received:
+ raise ProtocolError("Informational response after final response")
+
+ event = InformationalResponseReceived()
+ event.stream_id = self.stream_id
+ return [event]
+
+ def recv_alt_svc(self, previous_state):
+ """
+ Called when receiving an ALTSVC frame.
+
+ RFC 7838 allows us to receive ALTSVC frames at any stream state, which
+ is really absurdly overzealous. For that reason, we want to limit the
+ states in which we can actually receive it. It's really only sensible
+ to receive it after we've sent our own headers and before the server
+ has sent its header block: the server can't guarantee that we have any
+ state around after it completes its header block, and the server
+ doesn't know what origin we're talking about before we've sent ours.
+
+ For that reason, this function applies a few extra checks on both state
+ and some of the little state variables we keep around. If those suggest
+ an unreasonable situation for the ALTSVC frame to have been sent in,
+ we quietly ignore it (as RFC 7838 suggests).
+
+ This function is also *not* always called by the state machine. In some
+ states (IDLE, RESERVED_LOCAL, CLOSED) we don't bother to call it,
+ because we know the frame cannot be valid in that state (IDLE because
+ the server cannot know what origin the stream applies to, CLOSED
+ because the server cannot assume we still have state around,
+ RESERVED_LOCAL because by definition if we're in the RESERVED_LOCAL
+ state then *we* are the server).
+ """
+ # Servers can't receive ALTSVC frames, but RFC 7838 tells us to ignore
+ # them.
+ if self.client is False:
+ return []
+
+ # If we've received the response headers from the server they can't
+ # guarantee we still have any state around. Other implementations
+ # (like nghttp2) ignore ALTSVC in this state, so we will too.
+ if self.headers_received:
+ return []
+
+ # Otherwise, this is a sensible enough frame to have received. Return
+ # the event and let it get populated.
+ return [AlternativeServiceAvailable()]
+
+ def send_alt_svc(self, previous_state):
+ """
+ Called when sending an ALTSVC frame on this stream.
+
+ For consistency with the restrictions we apply on receiving ALTSVC
+ frames in ``recv_alt_svc``, we want to restrict when users can send
+ ALTSVC frames to the situations when we ourselves would accept them.
+
+ That means: when we are a server, when we have received the request
+ headers, and when we have not yet sent our own response headers.
+ """
+ # We should not send ALTSVC after we've sent response headers, as the
+ # client may have disposed of its state.
+ if self.headers_sent:
+ raise ProtocolError(
+ "Cannot send ALTSVC after sending response headers."
+ )
+
+ return
+
+
+# STATE MACHINE
+#
+# The stream state machine is defined here to avoid the need to allocate it
+# repeatedly for each stream. It cannot be defined in the stream class because
+# it needs to be able to reference the callbacks defined on the class, but
+# because Python's scoping rules are weird the class object is not actually in
+# scope during the body of the class object.
+#
+# For the sake of clarity, we reproduce the RFC 7540 state machine here:
+#
+# +--------+
+# send PP | | recv PP
+# ,--------| idle |--------.
+# / | | \
+# v +--------+ v
+# +----------+ | +----------+
+# | | | send H / | |
+# ,------| reserved | | recv H | reserved |------.
+# | | (local) | | | (remote) | |
+# | +----------+ v +----------+ |
+# | | +--------+ | |
+# | | recv ES | | send ES | |
+# | send H | ,-------| open |-------. | recv H |
+# | | / | | \ | |
+# | v v +--------+ v v |
+# | +----------+ | +----------+ |
+# | | half | | | half | |
+# | | closed | | send R / | closed | |
+# | | (remote) | | recv R | (local) | |
+# | +----------+ | +----------+ |
+# | | | | |
+# | | send ES / | recv ES / | |
+# | | send R / v send R / | |
+# | | recv R +--------+ recv R | |
+# | send R / `----------->| |<-----------' send R / |
+# | recv R | closed | recv R |
+# `----------------------->| |<----------------------'
+# +--------+
+#
+# send: endpoint sends this frame
+# recv: endpoint receives this frame
+#
+# H: HEADERS frame (with implied CONTINUATIONs)
+# PP: PUSH_PROMISE frame (with implied CONTINUATIONs)
+# ES: END_STREAM flag
+# R: RST_STREAM frame
+#
+# For the purposes of this state machine we treat HEADERS and their
+# associated CONTINUATION frames as a single jumbo frame. The protocol
+# allows/requires this by preventing other frames from being interleved in
+# between HEADERS/CONTINUATION frames. However, if a CONTINUATION frame is
+# received without a prior HEADERS frame, it *will* be passed to this state
+# machine. The state machine should always reject that frame, either as an
+# invalid transition or because the stream is closed.
+#
+# There is a confusing relationship around PUSH_PROMISE frames. The state
+# machine above considers them to be frames belonging to the new stream,
+# which is *somewhat* true. However, they are sent with the stream ID of
+# their related stream, and are only sendable in some cases.
+# For this reason, our state machine implementation below allows for
+# PUSH_PROMISE frames both in the IDLE state (as in the diagram), but also
+# in the OPEN, HALF_CLOSED_LOCAL, and HALF_CLOSED_REMOTE states.
+# Essentially, for hyper-h2, PUSH_PROMISE frames are effectively sent on
+# two streams.
+#
+# The _transitions dictionary contains a mapping of tuples of
+# (state, input) to tuples of (side_effect_function, end_state). This
+# map contains all allowed transitions: anything not in this map is
+# invalid and immediately causes a transition to ``closed``.
+_transitions = {
+ # State: idle
+ (StreamState.IDLE, StreamInputs.SEND_HEADERS):
+ (H2StreamStateMachine.request_sent, StreamState.OPEN),
+ (StreamState.IDLE, StreamInputs.RECV_HEADERS):
+ (H2StreamStateMachine.request_received, StreamState.OPEN),
+ (StreamState.IDLE, StreamInputs.RECV_DATA):
+ (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED),
+ (StreamState.IDLE, StreamInputs.SEND_PUSH_PROMISE):
+ (H2StreamStateMachine.send_new_pushed_stream,
+ StreamState.RESERVED_LOCAL),
+ (StreamState.IDLE, StreamInputs.RECV_PUSH_PROMISE):
+ (H2StreamStateMachine.recv_new_pushed_stream,
+ StreamState.RESERVED_REMOTE),
+ (StreamState.IDLE, StreamInputs.RECV_ALTERNATIVE_SERVICE):
+ (None, StreamState.IDLE),
+ (StreamState.IDLE, StreamInputs.UPGRADE_CLIENT):
+ (H2StreamStateMachine.request_sent, StreamState.HALF_CLOSED_LOCAL),
+ (StreamState.IDLE, StreamInputs.UPGRADE_SERVER):
+ (H2StreamStateMachine.request_received,
+ StreamState.HALF_CLOSED_REMOTE),
+
+ # State: reserved local
+ (StreamState.RESERVED_LOCAL, StreamInputs.SEND_HEADERS):
+ (H2StreamStateMachine.response_sent, StreamState.HALF_CLOSED_REMOTE),
+ (StreamState.RESERVED_LOCAL, StreamInputs.RECV_DATA):
+ (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED),
+ (StreamState.RESERVED_LOCAL, StreamInputs.SEND_WINDOW_UPDATE):
+ (None, StreamState.RESERVED_LOCAL),
+ (StreamState.RESERVED_LOCAL, StreamInputs.RECV_WINDOW_UPDATE):
+ (H2StreamStateMachine.window_updated, StreamState.RESERVED_LOCAL),
+ (StreamState.RESERVED_LOCAL, StreamInputs.SEND_RST_STREAM):
+ (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED),
+ (StreamState.RESERVED_LOCAL, StreamInputs.RECV_RST_STREAM):
+ (H2StreamStateMachine.stream_reset, StreamState.CLOSED),
+ (StreamState.RESERVED_LOCAL, StreamInputs.SEND_ALTERNATIVE_SERVICE):
+ (H2StreamStateMachine.send_alt_svc, StreamState.RESERVED_LOCAL),
+ (StreamState.RESERVED_LOCAL, StreamInputs.RECV_ALTERNATIVE_SERVICE):
+ (None, StreamState.RESERVED_LOCAL),
+
+ # State: reserved remote
+ (StreamState.RESERVED_REMOTE, StreamInputs.RECV_HEADERS):
+ (H2StreamStateMachine.response_received,
+ StreamState.HALF_CLOSED_LOCAL),
+ (StreamState.RESERVED_REMOTE, StreamInputs.RECV_DATA):
+ (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED),
+ (StreamState.RESERVED_REMOTE, StreamInputs.SEND_WINDOW_UPDATE):
+ (None, StreamState.RESERVED_REMOTE),
+ (StreamState.RESERVED_REMOTE, StreamInputs.RECV_WINDOW_UPDATE):
+ (H2StreamStateMachine.window_updated, StreamState.RESERVED_REMOTE),
+ (StreamState.RESERVED_REMOTE, StreamInputs.SEND_RST_STREAM):
+ (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED),
+ (StreamState.RESERVED_REMOTE, StreamInputs.RECV_RST_STREAM):
+ (H2StreamStateMachine.stream_reset, StreamState.CLOSED),
+ (StreamState.RESERVED_REMOTE, StreamInputs.RECV_ALTERNATIVE_SERVICE):
+ (H2StreamStateMachine.recv_alt_svc, StreamState.RESERVED_REMOTE),
+
+ # State: open
+ (StreamState.OPEN, StreamInputs.SEND_HEADERS):
+ (H2StreamStateMachine.response_sent, StreamState.OPEN),
+ (StreamState.OPEN, StreamInputs.RECV_HEADERS):
+ (H2StreamStateMachine.response_received, StreamState.OPEN),
+ (StreamState.OPEN, StreamInputs.SEND_DATA):
+ (None, StreamState.OPEN),
+ (StreamState.OPEN, StreamInputs.RECV_DATA):
+ (H2StreamStateMachine.data_received, StreamState.OPEN),
+ (StreamState.OPEN, StreamInputs.SEND_END_STREAM):
+ (None, StreamState.HALF_CLOSED_LOCAL),
+ (StreamState.OPEN, StreamInputs.RECV_END_STREAM):
+ (H2StreamStateMachine.stream_half_closed,
+ StreamState.HALF_CLOSED_REMOTE),
+ (StreamState.OPEN, StreamInputs.SEND_WINDOW_UPDATE):
+ (None, StreamState.OPEN),
+ (StreamState.OPEN, StreamInputs.RECV_WINDOW_UPDATE):
+ (H2StreamStateMachine.window_updated, StreamState.OPEN),
+ (StreamState.OPEN, StreamInputs.SEND_RST_STREAM):
+ (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED),
+ (StreamState.OPEN, StreamInputs.RECV_RST_STREAM):
+ (H2StreamStateMachine.stream_reset, StreamState.CLOSED),
+ (StreamState.OPEN, StreamInputs.SEND_PUSH_PROMISE):
+ (H2StreamStateMachine.send_push_promise, StreamState.OPEN),
+ (StreamState.OPEN, StreamInputs.RECV_PUSH_PROMISE):
+ (H2StreamStateMachine.recv_push_promise, StreamState.OPEN),
+ (StreamState.OPEN, StreamInputs.SEND_INFORMATIONAL_HEADERS):
+ (H2StreamStateMachine.send_informational_response, StreamState.OPEN),
+ (StreamState.OPEN, StreamInputs.RECV_INFORMATIONAL_HEADERS):
+ (H2StreamStateMachine.recv_informational_response, StreamState.OPEN),
+ (StreamState.OPEN, StreamInputs.SEND_ALTERNATIVE_SERVICE):
+ (H2StreamStateMachine.send_alt_svc, StreamState.OPEN),
+ (StreamState.OPEN, StreamInputs.RECV_ALTERNATIVE_SERVICE):
+ (H2StreamStateMachine.recv_alt_svc, StreamState.OPEN),
+
+ # State: half-closed remote
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_HEADERS):
+ (H2StreamStateMachine.response_sent, StreamState.HALF_CLOSED_REMOTE),
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_HEADERS):
+ (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED),
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_DATA):
+ (None, StreamState.HALF_CLOSED_REMOTE),
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_DATA):
+ (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED),
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_END_STREAM):
+ (H2StreamStateMachine.send_end_stream, StreamState.CLOSED),
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_WINDOW_UPDATE):
+ (None, StreamState.HALF_CLOSED_REMOTE),
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_WINDOW_UPDATE):
+ (H2StreamStateMachine.window_updated, StreamState.HALF_CLOSED_REMOTE),
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_RST_STREAM):
+ (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED),
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_RST_STREAM):
+ (H2StreamStateMachine.stream_reset, StreamState.CLOSED),
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_PUSH_PROMISE):
+ (H2StreamStateMachine.send_push_promise,
+ StreamState.HALF_CLOSED_REMOTE),
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_PUSH_PROMISE):
+ (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED),
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_INFORMATIONAL_HEADERS):
+ (H2StreamStateMachine.send_informational_response,
+ StreamState.HALF_CLOSED_REMOTE),
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_ALTERNATIVE_SERVICE):
+ (H2StreamStateMachine.send_alt_svc, StreamState.HALF_CLOSED_REMOTE),
+ (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_ALTERNATIVE_SERVICE):
+ (H2StreamStateMachine.recv_alt_svc, StreamState.HALF_CLOSED_REMOTE),
+
+ # State: half-closed local
+ (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_HEADERS):
+ (H2StreamStateMachine.response_received,
+ StreamState.HALF_CLOSED_LOCAL),
+ (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_DATA):
+ (H2StreamStateMachine.data_received, StreamState.HALF_CLOSED_LOCAL),
+ (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_END_STREAM):
+ (H2StreamStateMachine.stream_ended, StreamState.CLOSED),
+ (StreamState.HALF_CLOSED_LOCAL, StreamInputs.SEND_WINDOW_UPDATE):
+ (None, StreamState.HALF_CLOSED_LOCAL),
+ (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_WINDOW_UPDATE):
+ (H2StreamStateMachine.window_updated, StreamState.HALF_CLOSED_LOCAL),
+ (StreamState.HALF_CLOSED_LOCAL, StreamInputs.SEND_RST_STREAM):
+ (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED),
+ (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_RST_STREAM):
+ (H2StreamStateMachine.stream_reset, StreamState.CLOSED),
+ (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_PUSH_PROMISE):
+ (H2StreamStateMachine.recv_push_promise,
+ StreamState.HALF_CLOSED_LOCAL),
+ (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_INFORMATIONAL_HEADERS):
+ (H2StreamStateMachine.recv_informational_response,
+ StreamState.HALF_CLOSED_LOCAL),
+ (StreamState.HALF_CLOSED_LOCAL, StreamInputs.SEND_ALTERNATIVE_SERVICE):
+ (H2StreamStateMachine.send_alt_svc, StreamState.HALF_CLOSED_LOCAL),
+ (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_ALTERNATIVE_SERVICE):
+ (H2StreamStateMachine.recv_alt_svc, StreamState.HALF_CLOSED_LOCAL),
+
+ # State: closed
+ (StreamState.CLOSED, StreamInputs.RECV_END_STREAM):
+ (None, StreamState.CLOSED),
+ (StreamState.CLOSED, StreamInputs.RECV_ALTERNATIVE_SERVICE):
+ (None, StreamState.CLOSED),
+
+ # RFC 7540 Section 5.1 defines how the end point should react when
+ # receiving a frame on a closed stream with the following statements:
+ #
+ # > An endpoint that receives any frame other than PRIORITY after receiving
+ # > a RST_STREAM MUST treat that as a stream error of type STREAM_CLOSED.
+ # > An endpoint that receives any frames after receiving a frame with the
+ # > END_STREAM flag set MUST treat that as a connection error of type
+ # > STREAM_CLOSED.
+ (StreamState.CLOSED, StreamInputs.RECV_HEADERS):
+ (H2StreamStateMachine.recv_on_closed_stream, StreamState.CLOSED),
+ (StreamState.CLOSED, StreamInputs.RECV_DATA):
+ (H2StreamStateMachine.recv_on_closed_stream, StreamState.CLOSED),
+
+ # > WINDOW_UPDATE or RST_STREAM frames can be received in this state
+ # > for a short period after a DATA or HEADERS frame containing a
+ # > END_STREAM flag is sent, as instructed in RFC 7540 Section 5.1. But we
+ # > don't have access to a clock so we just always allow it.
+ (StreamState.CLOSED, StreamInputs.RECV_WINDOW_UPDATE):
+ (None, StreamState.CLOSED),
+ (StreamState.CLOSED, StreamInputs.RECV_RST_STREAM):
+ (None, StreamState.CLOSED),
+
+ # > A receiver MUST treat the receipt of a PUSH_PROMISE on a stream that is
+ # > neither "open" nor "half-closed (local)" as a connection error of type
+ # > PROTOCOL_ERROR.
+ (StreamState.CLOSED, StreamInputs.RECV_PUSH_PROMISE):
+ (H2StreamStateMachine.recv_push_on_closed_stream, StreamState.CLOSED),
+
+ # Also, users should be forbidden from sending on closed streams.
+ (StreamState.CLOSED, StreamInputs.SEND_HEADERS):
+ (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED),
+ (StreamState.CLOSED, StreamInputs.SEND_PUSH_PROMISE):
+ (H2StreamStateMachine.send_push_on_closed_stream, StreamState.CLOSED),
+ (StreamState.CLOSED, StreamInputs.SEND_RST_STREAM):
+ (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED),
+ (StreamState.CLOSED, StreamInputs.SEND_DATA):
+ (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED),
+ (StreamState.CLOSED, StreamInputs.SEND_WINDOW_UPDATE):
+ (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED),
+ (StreamState.CLOSED, StreamInputs.SEND_END_STREAM):
+ (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED),
+}
+
+
+class H2Stream(object):
+ """
+ A low-level HTTP/2 stream object. This handles building and receiving
+ frames and maintains per-stream state.
+
+ This wraps a HTTP/2 Stream state machine implementation, ensuring that
+ frames can only be sent/received when the stream is in a valid state.
+ Attempts to create frames that cannot be sent will raise a
+ ``ProtocolError``.
+ """
+ def __init__(self,
+ stream_id,
+ config,
+ inbound_window_size,
+ outbound_window_size):
+ self.state_machine = H2StreamStateMachine(stream_id)
+ self.stream_id = stream_id
+ self.max_outbound_frame_size = None
+ self.request_method = None
+
+ # The current value of the outbound stream flow control window
+ self.outbound_flow_control_window = outbound_window_size
+
+ # The flow control manager.
+ self._inbound_window_manager = WindowManager(inbound_window_size)
+
+ # The expected content length, if any.
+ self._expected_content_length = None
+
+ # The actual received content length. Always tracked.
+ self._actual_content_length = 0
+
+ # The authority we believe this stream belongs to.
+ self._authority = None
+
+ # The configuration for this stream.
+ self.config = config
+
+ def __repr__(self):
+ return "<%s id:%d state:%r>" % (
+ type(self).__name__,
+ self.stream_id,
+ self.state_machine.state
+ )
+
+ @property
+ def inbound_flow_control_window(self):
+ """
+ The size of the inbound flow control window for the stream. This is
+ rarely publicly useful: instead, use :meth:`remote_flow_control_window
+ <h2.stream.H2Stream.remote_flow_control_window>`. This shortcut is
+ largely present to provide a shortcut to this data.
+ """
+ return self._inbound_window_manager.current_window_size
+
+ @property
+ def open(self):
+ """
+ Whether the stream is 'open' in any sense: that is, whether it counts
+ against the number of concurrent streams.
+ """
+ # RFC 7540 Section 5.1.2 defines 'open' for this purpose to mean either
+ # the OPEN state or either of the HALF_CLOSED states. Perplexingly,
+ # this excludes the reserved states.
+ # For more detail on why we're doing this in this slightly weird way,
+ # see the comment on ``STREAM_OPEN`` at the top of the file.
+ return STREAM_OPEN[self.state_machine.state]
+
+ @property
+ def closed(self):
+ """
+ Whether the stream is closed.
+ """
+ return self.state_machine.state == StreamState.CLOSED
+
+ @property
+ def closed_by(self):
+ """
+ Returns how the stream was closed, as one of StreamClosedBy.
+ """
+ return self.state_machine.stream_closed_by
+
+ def upgrade(self, client_side):
+ """
+ Called by the connection to indicate that this stream is the initial
+ request/response of an upgraded connection. Places the stream into an
+ appropriate state.
+ """
+ self.config.logger.debug("Upgrading %r", self)
+
+ assert self.stream_id == 1
+ input_ = (
+ StreamInputs.UPGRADE_CLIENT if client_side
+ else StreamInputs.UPGRADE_SERVER
+ )
+
+ # This may return events, we deliberately don't want them.
+ self.state_machine.process_input(input_)
+ return
+
+ def send_headers(self, headers, encoder, end_stream=False):
+ """
+ Returns a list of HEADERS/CONTINUATION frames to emit as either headers
+ or trailers.
+ """
+ self.config.logger.debug("Send headers %s on %r", headers, self)
+
+ # Because encoding headers makes an irreversible change to the header
+ # compression context, we make the state transition before we encode
+ # them.
+
+ # First, check if we're a client. If we are, no problem: if we aren't,
+ # we need to scan the header block to see if this is an informational
+ # response.
+ input_ = StreamInputs.SEND_HEADERS
+ if ((not self.state_machine.client) and
+ is_informational_response(headers)):
+ if end_stream:
+ raise ProtocolError(
+ "Cannot set END_STREAM on informational responses."
+ )
+
+ input_ = StreamInputs.SEND_INFORMATIONAL_HEADERS
+
+ events = self.state_machine.process_input(input_)
+
+ hf = HeadersFrame(self.stream_id)
+ hdr_validation_flags = self._build_hdr_validation_flags(events)
+ frames = self._build_headers_frames(
+ headers, encoder, hf, hdr_validation_flags
+ )
+
+ if end_stream:
+ # Not a bug: the END_STREAM flag is valid on the initial HEADERS
+ # frame, not the CONTINUATION frames that follow.
+ self.state_machine.process_input(StreamInputs.SEND_END_STREAM)
+ frames[0].flags.add('END_STREAM')
+
+ if self.state_machine.trailers_sent and not end_stream:
+ raise ProtocolError("Trailers must have END_STREAM set.")
+
+ if self.state_machine.client and self._authority is None:
+ self._authority = authority_from_headers(headers)
+
+ # store request method for _initialize_content_length
+ self.request_method = extract_method_header(headers)
+
+ return frames
+
+ def push_stream_in_band(self, related_stream_id, headers, encoder):
+ """
+ Returns a list of PUSH_PROMISE/CONTINUATION frames to emit as a pushed
+ stream header. Called on the stream that has the PUSH_PROMISE frame
+ sent on it.
+ """
+ self.config.logger.debug("Push stream %r", self)
+
+ # Because encoding headers makes an irreversible change to the header
+ # compression context, we make the state transition *first*.
+
+ events = self.state_machine.process_input(
+ StreamInputs.SEND_PUSH_PROMISE
+ )
+
+ ppf = PushPromiseFrame(self.stream_id)
+ ppf.promised_stream_id = related_stream_id
+ hdr_validation_flags = self._build_hdr_validation_flags(events)
+ frames = self._build_headers_frames(
+ headers, encoder, ppf, hdr_validation_flags
+ )
+
+ return frames
+
+ def locally_pushed(self):
+ """
+ Mark this stream as one that was pushed by this peer. Must be called
+ immediately after initialization. Sends no frames, simply updates the
+ state machine.
+ """
+ # This does not trigger any events.
+ events = self.state_machine.process_input(
+ StreamInputs.SEND_PUSH_PROMISE
+ )
+ assert not events
+ return []
+
+ def send_data(self, data, end_stream=False, pad_length=None):
+ """
+ Prepare some data frames. Optionally end the stream.
+
+ .. warning:: Does not perform flow control checks.
+ """
+ self.config.logger.debug(
+ "Send data on %r with end stream set to %s", self, end_stream
+ )
+
+ self.state_machine.process_input(StreamInputs.SEND_DATA)
+
+ df = DataFrame(self.stream_id)
+ df.data = data
+ if end_stream:
+ self.state_machine.process_input(StreamInputs.SEND_END_STREAM)
+ df.flags.add('END_STREAM')
+ if pad_length is not None:
+ df.flags.add('PADDED')
+ df.pad_length = pad_length
+
+ # Subtract flow_controlled_length to account for possible padding
+ self.outbound_flow_control_window -= df.flow_controlled_length
+ assert self.outbound_flow_control_window >= 0
+
+ return [df]
+
+ def end_stream(self):
+ """
+ End a stream without sending data.
+ """
+ self.config.logger.debug("End stream %r", self)
+
+ self.state_machine.process_input(StreamInputs.SEND_END_STREAM)
+ df = DataFrame(self.stream_id)
+ df.flags.add('END_STREAM')
+ return [df]
+
+ def advertise_alternative_service(self, field_value):
+ """
+ Advertise an RFC 7838 alternative service. The semantics of this are
+ better documented in the ``H2Connection`` class.
+ """
+ self.config.logger.debug(
+ "Advertise alternative service of %r for %r", field_value, self
+ )
+ self.state_machine.process_input(StreamInputs.SEND_ALTERNATIVE_SERVICE)
+ asf = AltSvcFrame(self.stream_id)
+ asf.field = field_value
+ return [asf]
+
+ def increase_flow_control_window(self, increment):
+ """
+ Increase the size of the flow control window for the remote side.
+ """
+ self.config.logger.debug(
+ "Increase flow control window for %r by %d",
+ self, increment
+ )
+ self.state_machine.process_input(StreamInputs.SEND_WINDOW_UPDATE)
+ self._inbound_window_manager.window_opened(increment)
+
+ wuf = WindowUpdateFrame(self.stream_id)
+ wuf.window_increment = increment
+ return [wuf]
+
+ def receive_push_promise_in_band(self,
+ promised_stream_id,
+ headers,
+ header_encoding):
+ """
+ Receives a push promise frame sent on this stream, pushing a remote
+ stream. This is called on the stream that has the PUSH_PROMISE sent
+ on it.
+ """
+ self.config.logger.debug(
+ "Receive Push Promise on %r for remote stream %d",
+ self, promised_stream_id
+ )
+ events = self.state_machine.process_input(
+ StreamInputs.RECV_PUSH_PROMISE
+ )
+ events[0].pushed_stream_id = promised_stream_id
+
+ hdr_validation_flags = self._build_hdr_validation_flags(events)
+ events[0].headers = self._process_received_headers(
+ headers, hdr_validation_flags, header_encoding
+ )
+ return [], events
+
+ def remotely_pushed(self, pushed_headers):
+ """
+ Mark this stream as one that was pushed by the remote peer. Must be
+ called immediately after initialization. Sends no frames, simply
+ updates the state machine.
+ """
+ self.config.logger.debug("%r pushed by remote peer", self)
+ events = self.state_machine.process_input(
+ StreamInputs.RECV_PUSH_PROMISE
+ )
+ self._authority = authority_from_headers(pushed_headers)
+ return [], events
+
+ def receive_headers(self, headers, end_stream, header_encoding):
+ """
+ Receive a set of headers (or trailers).
+ """
+ if is_informational_response(headers):
+ if end_stream:
+ raise ProtocolError(
+ "Cannot set END_STREAM on informational responses"
+ )
+ input_ = StreamInputs.RECV_INFORMATIONAL_HEADERS
+ else:
+ input_ = StreamInputs.RECV_HEADERS
+
+ events = self.state_machine.process_input(input_)
+
+ if end_stream:
+ es_events = self.state_machine.process_input(
+ StreamInputs.RECV_END_STREAM
+ )
+ events[0].stream_ended = es_events[0]
+ events += es_events
+
+ self._initialize_content_length(headers)
+
+ if isinstance(events[0], TrailersReceived):
+ if not end_stream:
+ raise ProtocolError("Trailers must have END_STREAM set")
+
+ hdr_validation_flags = self._build_hdr_validation_flags(events)
+ events[0].headers = self._process_received_headers(
+ headers, hdr_validation_flags, header_encoding
+ )
+ return [], events
+
+ def receive_data(self, data, end_stream, flow_control_len):
+ """
+ Receive some data.
+ """
+ self.config.logger.debug(
+ "Receive data on %r with end stream %s and flow control length "
+ "set to %d", self, end_stream, flow_control_len
+ )
+ events = self.state_machine.process_input(StreamInputs.RECV_DATA)
+ self._inbound_window_manager.window_consumed(flow_control_len)
+ self._track_content_length(len(data), end_stream)
+
+ if end_stream:
+ es_events = self.state_machine.process_input(
+ StreamInputs.RECV_END_STREAM
+ )
+ events[0].stream_ended = es_events[0]
+ events.extend(es_events)
+
+ events[0].data = data
+ events[0].flow_controlled_length = flow_control_len
+ return [], events
+
+ def receive_window_update(self, increment):
+ """
+ Handle a WINDOW_UPDATE increment.
+ """
+ self.config.logger.debug(
+ "Receive Window Update on %r for increment of %d",
+ self, increment
+ )
+ events = self.state_machine.process_input(
+ StreamInputs.RECV_WINDOW_UPDATE
+ )
+ frames = []
+
+ # If we encounter a problem with incrementing the flow control window,
+ # this should be treated as a *stream* error, not a *connection* error.
+ # That means we need to catch the error and forcibly close the stream.
+ if events:
+ events[0].delta = increment
+ try:
+ self.outbound_flow_control_window = guard_increment_window(
+ self.outbound_flow_control_window,
+ increment
+ )
+ except FlowControlError:
+ # Ok, this is bad. We're going to need to perform a local
+ # reset.
+ event = StreamReset()
+ event.stream_id = self.stream_id
+ event.error_code = ErrorCodes.FLOW_CONTROL_ERROR
+ event.remote_reset = False
+
+ events = [event]
+ frames = self.reset_stream(event.error_code)
+
+ return frames, events
+
+ def receive_continuation(self):
+ """
+ A naked CONTINUATION frame has been received. This is always an error,
+ but the type of error it is depends on the state of the stream and must
+ transition the state of the stream, so we need to handle it.
+ """
+ self.config.logger.debug("Receive Continuation frame on %r", self)
+ self.state_machine.process_input(
+ StreamInputs.RECV_CONTINUATION
+ )
+ assert False, "Should not be reachable"
+
+ def receive_alt_svc(self, frame):
+ """
+ An Alternative Service frame was received on the stream. This frame
+ inherits the origin associated with this stream.
+ """
+ self.config.logger.debug(
+ "Receive Alternative Service frame on stream %r", self
+ )
+
+ # If the origin is present, RFC 7838 says we have to ignore it.
+ if frame.origin:
+ return [], []
+
+ events = self.state_machine.process_input(
+ StreamInputs.RECV_ALTERNATIVE_SERVICE
+ )
+
+ # There are lots of situations where we want to ignore the ALTSVC
+ # frame. If we need to pay attention, we'll have an event and should
+ # fill it out.
+ if events:
+ assert isinstance(events[0], AlternativeServiceAvailable)
+ events[0].origin = self._authority
+ events[0].field_value = frame.field
+
+ return [], events
+
+ def reset_stream(self, error_code=0):
+ """
+ Close the stream locally. Reset the stream with an error code.
+ """
+ self.config.logger.debug(
+ "Local reset %r with error code: %d", self, error_code
+ )
+ self.state_machine.process_input(StreamInputs.SEND_RST_STREAM)
+
+ rsf = RstStreamFrame(self.stream_id)
+ rsf.error_code = error_code
+ return [rsf]
+
+ def stream_reset(self, frame):
+ """
+ Handle a stream being reset remotely.
+ """
+ self.config.logger.debug(
+ "Remote reset %r with error code: %d", self, frame.error_code
+ )
+ events = self.state_machine.process_input(StreamInputs.RECV_RST_STREAM)
+
+ if events:
+ # We don't fire an event if this stream is already closed.
+ events[0].error_code = _error_code_from_int(frame.error_code)
+
+ return [], events
+
+ def acknowledge_received_data(self, acknowledged_size):
+ """
+ The user has informed us that they've processed some amount of data
+ that was received on this stream. Pass that to the window manager and
+ potentially return some WindowUpdate frames.
+ """
+ self.config.logger.debug(
+ "Acknowledge received data with size %d on %r",
+ acknowledged_size, self
+ )
+ increment = self._inbound_window_manager.process_bytes(
+ acknowledged_size
+ )
+ if increment:
+ f = WindowUpdateFrame(self.stream_id)
+ f.window_increment = increment
+ return [f]
+
+ return []
+
+ def _build_hdr_validation_flags(self, events):
+ """
+ Constructs a set of header validation flags for use when normalizing
+ and validating header blocks.
+ """
+ is_trailer = isinstance(
+ events[0], (_TrailersSent, TrailersReceived)
+ )
+ is_response_header = isinstance(
+ events[0],
+ (
+ _ResponseSent,
+ ResponseReceived,
+ InformationalResponseReceived
+ )
+ )
+ is_push_promise = isinstance(
+ events[0], (PushedStreamReceived, _PushedRequestSent)
+ )
+
+ return HeaderValidationFlags(
+ is_client=self.state_machine.client,
+ is_trailer=is_trailer,
+ is_response_header=is_response_header,
+ is_push_promise=is_push_promise,
+ )
+
+ def _build_headers_frames(self,
+ headers,
+ encoder,
+ first_frame,
+ hdr_validation_flags):
+ """
+ Helper method to build headers or push promise frames.
+ """
+ # We need to lowercase the header names, and to ensure that secure
+ # header fields are kept out of compression contexts.
+ if self.config.normalize_outbound_headers:
+ headers = normalize_outbound_headers(
+ headers, hdr_validation_flags
+ )
+ if self.config.validate_outbound_headers:
+ headers = validate_outbound_headers(
+ headers, hdr_validation_flags
+ )
+
+ encoded_headers = encoder.encode(headers)
+
+ # Slice into blocks of max_outbound_frame_size. Be careful with this:
+ # it only works right because we never send padded frames or priority
+ # information on the frames. Revisit this if we do.
+ header_blocks = [
+ encoded_headers[i:i+self.max_outbound_frame_size]
+ for i in range(
+ 0, len(encoded_headers), self.max_outbound_frame_size
+ )
+ ]
+
+ frames = []
+ first_frame.data = header_blocks[0]
+ frames.append(first_frame)
+
+ for block in header_blocks[1:]:
+ cf = ContinuationFrame(self.stream_id)
+ cf.data = block
+ frames.append(cf)
+
+ frames[-1].flags.add('END_HEADERS')
+ return frames
+
+ def _process_received_headers(self,
+ headers,
+ header_validation_flags,
+ header_encoding):
+ """
+ When headers have been received from the remote peer, run a processing
+ pipeline on them to transform them into the appropriate form for
+ attaching to an event.
+ """
+ if self.config.normalize_inbound_headers:
+ headers = normalize_inbound_headers(
+ headers, header_validation_flags
+ )
+
+ if self.config.validate_inbound_headers:
+ headers = validate_headers(headers, header_validation_flags)
+
+ if header_encoding:
+ headers = _decode_headers(headers, header_encoding)
+
+ # The above steps are all generators, so we need to concretize the
+ # headers now.
+ return list(headers)
+
+ def _initialize_content_length(self, headers):
+ """
+ Checks the headers for a content-length header and initializes the
+ _expected_content_length field from it. It's not an error for no
+ Content-Length header to be present.
+ """
+ if self.request_method == b'HEAD':
+ self._expected_content_length = 0
+ return
+
+ for n, v in headers:
+ if n == b'content-length':
+ try:
+ self._expected_content_length = int(v, 10)
+ except ValueError:
+ raise ProtocolError(
+ "Invalid content-length header: %s" % v
+ )
+
+ return
+
+ def _track_content_length(self, length, end_stream):
+ """
+ Update the expected content length in response to data being received.
+ Validates that the appropriate amount of data is sent. Always updates
+ the received data, but only validates the length against the
+ content-length header if one was sent.
+
+ :param length: The length of the body chunk received.
+ :param end_stream: If this is the last body chunk received.
+ """
+ self._actual_content_length += length
+ actual = self._actual_content_length
+ expected = self._expected_content_length
+
+ if expected is not None:
+ if expected < actual:
+ raise InvalidBodyLengthError(expected, actual)
+
+ if end_stream and expected != actual:
+ raise InvalidBodyLengthError(expected, actual)
+
+ def _inbound_flow_control_change_from_settings(self, delta):
+ """
+ We changed SETTINGS_INITIAL_WINDOW_SIZE, which means we need to
+ update the target window size for flow control. For our flow control
+ strategy, this means we need to do two things: we need to adjust the
+ current window size, but we also need to set the target maximum window
+ size to the new value.
+ """
+ new_max_size = self._inbound_window_manager.max_window_size + delta
+ self._inbound_window_manager.window_opened(delta)
+ self._inbound_window_manager.max_window_size = new_max_size
+
+
+def _decode_headers(headers, encoding):
+ """
+ Given an iterable of header two-tuples and an encoding, decodes those
+ headers using that encoding while preserving the type of the header tuple.
+ This ensures that the use of ``HeaderTuple`` is preserved.
+ """
+ for header in headers:
+ # This function expects to work on decoded headers, which are always
+ # HeaderTuple objects.
+ assert isinstance(header, HeaderTuple)
+
+ name, value = header
+ name = name.decode(encoding)
+ value = value.decode(encoding)
+ yield header.__class__(name, value)
diff --git a/testing/web-platform/tests/tools/third_party/h2/h2/utilities.py b/testing/web-platform/tests/tools/third_party/h2/h2/utilities.py
new file mode 100644
index 0000000000..06c916eea7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/h2/utilities.py
@@ -0,0 +1,660 @@
+# -*- coding: utf-8 -*-
+"""
+h2/utilities
+~~~~~~~~~~~~
+
+Utility functions that do not belong in a separate module.
+"""
+import collections
+import re
+from string import whitespace
+import sys
+
+from hpack import HeaderTuple, NeverIndexedHeaderTuple
+
+from .exceptions import ProtocolError, FlowControlError
+
+UPPER_RE = re.compile(b"[A-Z]")
+
+# A set of headers that are hop-by-hop or connection-specific and thus
+# forbidden in HTTP/2. This list comes from RFC 7540 § 8.1.2.2.
+CONNECTION_HEADERS = frozenset([
+ b'connection', u'connection',
+ b'proxy-connection', u'proxy-connection',
+ b'keep-alive', u'keep-alive',
+ b'transfer-encoding', u'transfer-encoding',
+ b'upgrade', u'upgrade',
+])
+
+
+_ALLOWED_PSEUDO_HEADER_FIELDS = frozenset([
+ b':method', u':method',
+ b':scheme', u':scheme',
+ b':authority', u':authority',
+ b':path', u':path',
+ b':status', u':status',
+ b':protocol', u':protocol',
+])
+
+
+_SECURE_HEADERS = frozenset([
+ # May have basic credentials which are vulnerable to dictionary attacks.
+ b'authorization', u'authorization',
+ b'proxy-authorization', u'proxy-authorization',
+])
+
+
+_REQUEST_ONLY_HEADERS = frozenset([
+ b':scheme', u':scheme',
+ b':path', u':path',
+ b':authority', u':authority',
+ b':method', u':method',
+ b':protocol', u':protocol',
+])
+
+
+_RESPONSE_ONLY_HEADERS = frozenset([b':status', u':status'])
+
+
+# A Set of pseudo headers that are only valid if the method is
+# CONNECT, see RFC 8441 § 5
+_CONNECT_REQUEST_ONLY_HEADERS = frozenset([b':protocol', u':protocol'])
+
+
+if sys.version_info[0] == 2: # Python 2.X
+ _WHITESPACE = frozenset(whitespace)
+else: # Python 3.3+
+ _WHITESPACE = frozenset(map(ord, whitespace))
+
+
+def _secure_headers(headers, hdr_validation_flags):
+ """
+ Certain headers are at risk of being attacked during the header compression
+ phase, and so need to be kept out of header compression contexts. This
+ function automatically transforms certain specific headers into HPACK
+ never-indexed fields to ensure they don't get added to header compression
+ contexts.
+
+ This function currently implements two rules:
+
+ - 'authorization' and 'proxy-authorization' fields are automatically made
+ never-indexed.
+ - Any 'cookie' header field shorter than 20 bytes long is made
+ never-indexed.
+
+ These fields are the most at-risk. These rules are inspired by Firefox
+ and nghttp2.
+ """
+ for header in headers:
+ if header[0] in _SECURE_HEADERS:
+ yield NeverIndexedHeaderTuple(*header)
+ elif header[0] in (b'cookie', u'cookie') and len(header[1]) < 20:
+ yield NeverIndexedHeaderTuple(*header)
+ else:
+ yield header
+
+
+def extract_method_header(headers):
+ """
+ Extracts the request method from the headers list.
+ """
+ for k, v in headers:
+ if k in (b':method', u':method'):
+ if not isinstance(v, bytes):
+ return v.encode('utf-8')
+ else:
+ return v
+
+
+def is_informational_response(headers):
+ """
+ Searches a header block for a :status header to confirm that a given
+ collection of headers are an informational response. Assumes the header
+ block is well formed: that is, that the HTTP/2 special headers are first
+ in the block, and so that it can stop looking when it finds the first
+ header field whose name does not begin with a colon.
+
+ :param headers: The HTTP/2 header block.
+ :returns: A boolean indicating if this is an informational response.
+ """
+ for n, v in headers:
+ if isinstance(n, bytes):
+ sigil = b':'
+ status = b':status'
+ informational_start = b'1'
+ else:
+ sigil = u':'
+ status = u':status'
+ informational_start = u'1'
+
+ # If we find a non-special header, we're done here: stop looping.
+ if not n.startswith(sigil):
+ return False
+
+ # This isn't the status header, bail.
+ if n != status:
+ continue
+
+ # If the first digit is a 1, we've got informational headers.
+ return v.startswith(informational_start)
+
+
+def guard_increment_window(current, increment):
+ """
+ Increments a flow control window, guarding against that window becoming too
+ large.
+
+ :param current: The current value of the flow control window.
+ :param increment: The increment to apply to that window.
+ :returns: The new value of the window.
+ :raises: ``FlowControlError``
+ """
+ # The largest value the flow control window may take.
+ LARGEST_FLOW_CONTROL_WINDOW = 2**31 - 1
+
+ new_size = current + increment
+
+ if new_size > LARGEST_FLOW_CONTROL_WINDOW:
+ raise FlowControlError(
+ "May not increment flow control window past %d" %
+ LARGEST_FLOW_CONTROL_WINDOW
+ )
+
+ return new_size
+
+
+def authority_from_headers(headers):
+ """
+ Given a header set, searches for the authority header and returns the
+ value.
+
+ Note that this doesn't terminate early, so should only be called if the
+ headers are for a client request. Otherwise, will loop over the entire
+ header set, which is potentially unwise.
+
+ :param headers: The HTTP header set.
+ :returns: The value of the authority header, or ``None``.
+ :rtype: ``bytes`` or ``None``.
+ """
+ for n, v in headers:
+ # This gets run against headers that come both from HPACK and from the
+ # user, so we may have unicode floating around in here. We only want
+ # bytes.
+ if n in (b':authority', u':authority'):
+ return v.encode('utf-8') if not isinstance(v, bytes) else v
+
+ return None
+
+
+# Flags used by the validate_headers pipeline to determine which checks
+# should be applied to a given set of headers.
+HeaderValidationFlags = collections.namedtuple(
+ 'HeaderValidationFlags',
+ ['is_client', 'is_trailer', 'is_response_header', 'is_push_promise']
+)
+
+
+def validate_headers(headers, hdr_validation_flags):
+ """
+ Validates a header sequence against a set of constraints from RFC 7540.
+
+ :param headers: The HTTP header set.
+ :param hdr_validation_flags: An instance of HeaderValidationFlags.
+ """
+ # This validation logic is built on a sequence of generators that are
+ # iterated over to provide the final header list. This reduces some of the
+ # overhead of doing this checking. However, it's worth noting that this
+ # checking remains somewhat expensive, and attempts should be made wherever
+ # possible to reduce the time spent doing them.
+ #
+ # For example, we avoid tuple upacking in loops because it represents a
+ # fixed cost that we don't want to spend, instead indexing into the header
+ # tuples.
+ headers = _reject_uppercase_header_fields(
+ headers, hdr_validation_flags
+ )
+ headers = _reject_surrounding_whitespace(
+ headers, hdr_validation_flags
+ )
+ headers = _reject_te(
+ headers, hdr_validation_flags
+ )
+ headers = _reject_connection_header(
+ headers, hdr_validation_flags
+ )
+ headers = _reject_pseudo_header_fields(
+ headers, hdr_validation_flags
+ )
+ headers = _check_host_authority_header(
+ headers, hdr_validation_flags
+ )
+ headers = _check_path_header(headers, hdr_validation_flags)
+
+ return headers
+
+
+def _reject_uppercase_header_fields(headers, hdr_validation_flags):
+ """
+ Raises a ProtocolError if any uppercase character is found in a header
+ block.
+ """
+ for header in headers:
+ if UPPER_RE.search(header[0]):
+ raise ProtocolError(
+ "Received uppercase header name %s." % header[0])
+ yield header
+
+
+def _reject_surrounding_whitespace(headers, hdr_validation_flags):
+ """
+ Raises a ProtocolError if any header name or value is surrounded by
+ whitespace characters.
+ """
+ # For compatibility with RFC 7230 header fields, we need to allow the field
+ # value to be an empty string. This is ludicrous, but technically allowed.
+ # The field name may not be empty, though, so we can safely assume that it
+ # must have at least one character in it and throw exceptions if it
+ # doesn't.
+ for header in headers:
+ if header[0][0] in _WHITESPACE or header[0][-1] in _WHITESPACE:
+ raise ProtocolError(
+ "Received header name surrounded by whitespace %r" % header[0])
+ if header[1] and ((header[1][0] in _WHITESPACE) or
+ (header[1][-1] in _WHITESPACE)):
+ raise ProtocolError(
+ "Received header value surrounded by whitespace %r" % header[1]
+ )
+ yield header
+
+
+def _reject_te(headers, hdr_validation_flags):
+ """
+ Raises a ProtocolError if the TE header is present in a header block and
+ its value is anything other than "trailers".
+ """
+ for header in headers:
+ if header[0] in (b'te', u'te'):
+ if header[1].lower() not in (b'trailers', u'trailers'):
+ raise ProtocolError(
+ "Invalid value for Transfer-Encoding header: %s" %
+ header[1]
+ )
+
+ yield header
+
+
+def _reject_connection_header(headers, hdr_validation_flags):
+ """
+ Raises a ProtocolError if the Connection header is present in a header
+ block.
+ """
+ for header in headers:
+ if header[0] in CONNECTION_HEADERS:
+ raise ProtocolError(
+ "Connection-specific header field present: %s." % header[0]
+ )
+
+ yield header
+
+
+def _custom_startswith(test_string, bytes_prefix, unicode_prefix):
+ """
+ Given a string that might be a bytestring or a Unicode string,
+ return True if it starts with the appropriate prefix.
+ """
+ if isinstance(test_string, bytes):
+ return test_string.startswith(bytes_prefix)
+ else:
+ return test_string.startswith(unicode_prefix)
+
+
+def _assert_header_in_set(string_header, bytes_header, header_set):
+ """
+ Given a set of header names, checks whether the string or byte version of
+ the header name is present. Raises a Protocol error with the appropriate
+ error if it's missing.
+ """
+ if not (string_header in header_set or bytes_header in header_set):
+ raise ProtocolError(
+ "Header block missing mandatory %s header" % string_header
+ )
+
+
+def _reject_pseudo_header_fields(headers, hdr_validation_flags):
+ """
+ Raises a ProtocolError if duplicate pseudo-header fields are found in a
+ header block or if a pseudo-header field appears in a block after an
+ ordinary header field.
+
+ Raises a ProtocolError if pseudo-header fields are found in trailers.
+ """
+ seen_pseudo_header_fields = set()
+ seen_regular_header = False
+ method = None
+
+ for header in headers:
+ if _custom_startswith(header[0], b':', u':'):
+ if header[0] in seen_pseudo_header_fields:
+ raise ProtocolError(
+ "Received duplicate pseudo-header field %s" % header[0]
+ )
+
+ seen_pseudo_header_fields.add(header[0])
+
+ if seen_regular_header:
+ raise ProtocolError(
+ "Received pseudo-header field out of sequence: %s" %
+ header[0]
+ )
+
+ if header[0] not in _ALLOWED_PSEUDO_HEADER_FIELDS:
+ raise ProtocolError(
+ "Received custom pseudo-header field %s" % header[0]
+ )
+
+ if header[0] in (b':method', u':method'):
+ if not isinstance(header[1], bytes):
+ method = header[1].encode('utf-8')
+ else:
+ method = header[1]
+
+ else:
+ seen_regular_header = True
+
+ yield header
+
+ # Check the pseudo-headers we got to confirm they're acceptable.
+ _check_pseudo_header_field_acceptability(
+ seen_pseudo_header_fields, method, hdr_validation_flags
+ )
+
+
+def _check_pseudo_header_field_acceptability(pseudo_headers,
+ method,
+ hdr_validation_flags):
+ """
+ Given the set of pseudo-headers present in a header block and the
+ validation flags, confirms that RFC 7540 allows them.
+ """
+ # Pseudo-header fields MUST NOT appear in trailers - RFC 7540 § 8.1.2.1
+ if hdr_validation_flags.is_trailer and pseudo_headers:
+ raise ProtocolError(
+ "Received pseudo-header in trailer %s" % pseudo_headers
+ )
+
+ # If ':status' pseudo-header is not there in a response header, reject it.
+ # Similarly, if ':path', ':method', or ':scheme' are not there in a request
+ # header, reject it. Additionally, if a response contains any request-only
+ # headers or vice-versa, reject it.
+ # Relevant RFC section: RFC 7540 § 8.1.2.4
+ # https://tools.ietf.org/html/rfc7540#section-8.1.2.4
+ if hdr_validation_flags.is_response_header:
+ _assert_header_in_set(u':status', b':status', pseudo_headers)
+ invalid_response_headers = pseudo_headers & _REQUEST_ONLY_HEADERS
+ if invalid_response_headers:
+ raise ProtocolError(
+ "Encountered request-only headers %s" %
+ invalid_response_headers
+ )
+ elif (not hdr_validation_flags.is_response_header and
+ not hdr_validation_flags.is_trailer):
+ # This is a request, so we need to have seen :path, :method, and
+ # :scheme.
+ _assert_header_in_set(u':path', b':path', pseudo_headers)
+ _assert_header_in_set(u':method', b':method', pseudo_headers)
+ _assert_header_in_set(u':scheme', b':scheme', pseudo_headers)
+ invalid_request_headers = pseudo_headers & _RESPONSE_ONLY_HEADERS
+ if invalid_request_headers:
+ raise ProtocolError(
+ "Encountered response-only headers %s" %
+ invalid_request_headers
+ )
+ if method != b'CONNECT':
+ invalid_headers = pseudo_headers & _CONNECT_REQUEST_ONLY_HEADERS
+ if invalid_headers:
+ raise ProtocolError(
+ "Encountered connect-request-only headers %s" %
+ invalid_headers
+ )
+
+
+def _validate_host_authority_header(headers):
+ """
+ Given the :authority and Host headers from a request block that isn't
+ a trailer, check that:
+ 1. At least one of these headers is set.
+ 2. If both headers are set, they match.
+
+ :param headers: The HTTP header set.
+ :raises: ``ProtocolError``
+ """
+ # We use None as a sentinel value. Iterate over the list of headers,
+ # and record the value of these headers (if present). We don't need
+ # to worry about receiving duplicate :authority headers, as this is
+ # enforced by the _reject_pseudo_header_fields() pipeline.
+ #
+ # TODO: We should also guard against receiving duplicate Host headers,
+ # and against sending duplicate headers.
+ authority_header_val = None
+ host_header_val = None
+
+ for header in headers:
+ if header[0] in (b':authority', u':authority'):
+ authority_header_val = header[1]
+ elif header[0] in (b'host', u'host'):
+ host_header_val = header[1]
+
+ yield header
+
+ # If we have not-None values for these variables, then we know we saw
+ # the corresponding header.
+ authority_present = (authority_header_val is not None)
+ host_present = (host_header_val is not None)
+
+ # It is an error for a request header block to contain neither
+ # an :authority header nor a Host header.
+ if not authority_present and not host_present:
+ raise ProtocolError(
+ "Request header block does not have an :authority or Host header."
+ )
+
+ # If we receive both headers, they should definitely match.
+ if authority_present and host_present:
+ if authority_header_val != host_header_val:
+ raise ProtocolError(
+ "Request header block has mismatched :authority and "
+ "Host headers: %r / %r"
+ % (authority_header_val, host_header_val)
+ )
+
+
+def _check_host_authority_header(headers, hdr_validation_flags):
+ """
+ Raises a ProtocolError if a header block arrives that does not contain an
+ :authority or a Host header, or if a header block contains both fields,
+ but their values do not match.
+ """
+ # We only expect to see :authority and Host headers on request header
+ # blocks that aren't trailers, so skip this validation if this is a
+ # response header or we're looking at trailer blocks.
+ skip_validation = (
+ hdr_validation_flags.is_response_header or
+ hdr_validation_flags.is_trailer
+ )
+ if skip_validation:
+ return headers
+
+ return _validate_host_authority_header(headers)
+
+
+def _check_path_header(headers, hdr_validation_flags):
+ """
+ Raise a ProtocolError if a header block arrives or is sent that contains an
+ empty :path header.
+ """
+ def inner():
+ for header in headers:
+ if header[0] in (b':path', u':path'):
+ if not header[1]:
+ raise ProtocolError("An empty :path header is forbidden")
+
+ yield header
+
+ # We only expect to see :authority and Host headers on request header
+ # blocks that aren't trailers, so skip this validation if this is a
+ # response header or we're looking at trailer blocks.
+ skip_validation = (
+ hdr_validation_flags.is_response_header or
+ hdr_validation_flags.is_trailer
+ )
+ if skip_validation:
+ return headers
+ else:
+ return inner()
+
+
+def _lowercase_header_names(headers, hdr_validation_flags):
+ """
+ Given an iterable of header two-tuples, rebuilds that iterable with the
+ header names lowercased. This generator produces tuples that preserve the
+ original type of the header tuple for tuple and any ``HeaderTuple``.
+ """
+ for header in headers:
+ if isinstance(header, HeaderTuple):
+ yield header.__class__(header[0].lower(), header[1])
+ else:
+ yield (header[0].lower(), header[1])
+
+
+def _strip_surrounding_whitespace(headers, hdr_validation_flags):
+ """
+ Given an iterable of header two-tuples, strip both leading and trailing
+ whitespace from both header names and header values. This generator
+ produces tuples that preserve the original type of the header tuple for
+ tuple and any ``HeaderTuple``.
+ """
+ for header in headers:
+ if isinstance(header, HeaderTuple):
+ yield header.__class__(header[0].strip(), header[1].strip())
+ else:
+ yield (header[0].strip(), header[1].strip())
+
+
+def _strip_connection_headers(headers, hdr_validation_flags):
+ """
+ Strip any connection headers as per RFC7540 § 8.1.2.2.
+ """
+ for header in headers:
+ if header[0] not in CONNECTION_HEADERS:
+ yield header
+
+
+def _check_sent_host_authority_header(headers, hdr_validation_flags):
+ """
+ Raises an InvalidHeaderBlockError if we try to send a header block
+ that does not contain an :authority or a Host header, or if
+ the header block contains both fields, but their values do not match.
+ """
+ # We only expect to see :authority and Host headers on request header
+ # blocks that aren't trailers, so skip this validation if this is a
+ # response header or we're looking at trailer blocks.
+ skip_validation = (
+ hdr_validation_flags.is_response_header or
+ hdr_validation_flags.is_trailer
+ )
+ if skip_validation:
+ return headers
+
+ return _validate_host_authority_header(headers)
+
+
+def _combine_cookie_fields(headers, hdr_validation_flags):
+ """
+ RFC 7540 § 8.1.2.5 allows HTTP/2 clients to split the Cookie header field,
+ which must normally appear only once, into multiple fields for better
+ compression. However, they MUST be joined back up again when received.
+ This normalization step applies that transform. The side-effect is that
+ all cookie fields now appear *last* in the header block.
+ """
+ # There is a problem here about header indexing. Specifically, it's
+ # possible that all these cookies are sent with different header indexing
+ # values. At this point it shouldn't matter too much, so we apply our own
+ # logic and make them never-indexed.
+ cookies = []
+ for header in headers:
+ if header[0] == b'cookie':
+ cookies.append(header[1])
+ else:
+ yield header
+ if cookies:
+ cookie_val = b'; '.join(cookies)
+ yield NeverIndexedHeaderTuple(b'cookie', cookie_val)
+
+
+def normalize_outbound_headers(headers, hdr_validation_flags):
+ """
+ Normalizes a header sequence that we are about to send.
+
+ :param headers: The HTTP header set.
+ :param hdr_validation_flags: An instance of HeaderValidationFlags.
+ """
+ headers = _lowercase_header_names(headers, hdr_validation_flags)
+ headers = _strip_surrounding_whitespace(headers, hdr_validation_flags)
+ headers = _strip_connection_headers(headers, hdr_validation_flags)
+ headers = _secure_headers(headers, hdr_validation_flags)
+
+ return headers
+
+
+def normalize_inbound_headers(headers, hdr_validation_flags):
+ """
+ Normalizes a header sequence that we have received.
+
+ :param headers: The HTTP header set.
+ :param hdr_validation_flags: An instance of HeaderValidationFlags
+ """
+ headers = _combine_cookie_fields(headers, hdr_validation_flags)
+ return headers
+
+
+def validate_outbound_headers(headers, hdr_validation_flags):
+ """
+ Validates and normalizes a header sequence that we are about to send.
+
+ :param headers: The HTTP header set.
+ :param hdr_validation_flags: An instance of HeaderValidationFlags.
+ """
+ headers = _reject_te(
+ headers, hdr_validation_flags
+ )
+ headers = _reject_connection_header(
+ headers, hdr_validation_flags
+ )
+ headers = _reject_pseudo_header_fields(
+ headers, hdr_validation_flags
+ )
+ headers = _check_sent_host_authority_header(
+ headers, hdr_validation_flags
+ )
+ headers = _check_path_header(headers, hdr_validation_flags)
+
+ return headers
+
+
+class SizeLimitDict(collections.OrderedDict):
+
+ def __init__(self, *args, **kwargs):
+ self._size_limit = kwargs.pop("size_limit", None)
+ super(SizeLimitDict, self).__init__(*args, **kwargs)
+
+ self._check_size_limit()
+
+ def __setitem__(self, key, value):
+ super(SizeLimitDict, self).__setitem__(key, value)
+
+ self._check_size_limit()
+
+ def _check_size_limit(self):
+ if self._size_limit is not None:
+ while len(self) > self._size_limit:
+ self.popitem(last=False)
diff --git a/testing/web-platform/tests/tools/third_party/h2/h2/windows.py b/testing/web-platform/tests/tools/third_party/h2/h2/windows.py
new file mode 100644
index 0000000000..6656975f48
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/h2/windows.py
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+"""
+h2/windows
+~~~~~~~~~~
+
+Defines tools for managing HTTP/2 flow control windows.
+
+The objects defined in this module are used to automatically manage HTTP/2
+flow control windows. Specifically, they keep track of what the size of the
+window is, how much data has been consumed from that window, and how much data
+the user has already used. It then implements a basic algorithm that attempts
+to manage the flow control window without user input, trying to ensure that it
+does not emit too many WINDOW_UPDATE frames.
+"""
+from __future__ import division
+
+from .exceptions import FlowControlError
+
+
+# The largest acceptable value for a HTTP/2 flow control window.
+LARGEST_FLOW_CONTROL_WINDOW = 2**31 - 1
+
+
+class WindowManager(object):
+ """
+ A basic HTTP/2 window manager.
+
+ :param max_window_size: The maximum size of the flow control window.
+ :type max_window_size: ``int``
+ """
+ def __init__(self, max_window_size):
+ assert max_window_size <= LARGEST_FLOW_CONTROL_WINDOW
+ self.max_window_size = max_window_size
+ self.current_window_size = max_window_size
+ self._bytes_processed = 0
+
+ def window_consumed(self, size):
+ """
+ We have received a certain number of bytes from the remote peer. This
+ necessarily shrinks the flow control window!
+
+ :param size: The number of flow controlled bytes we received from the
+ remote peer.
+ :type size: ``int``
+ :returns: Nothing.
+ :rtype: ``None``
+ """
+ self.current_window_size -= size
+ if self.current_window_size < 0:
+ raise FlowControlError("Flow control window shrunk below 0")
+
+ def window_opened(self, size):
+ """
+ The flow control window has been incremented, either because of manual
+ flow control management or because of the user changing the flow
+ control settings. This can have the effect of increasing what we
+ consider to be the "maximum" flow control window size.
+
+ This does not increase our view of how many bytes have been processed,
+ only of how much space is in the window.
+
+ :param size: The increment to the flow control window we received.
+ :type size: ``int``
+ :returns: Nothing
+ :rtype: ``None``
+ """
+ self.current_window_size += size
+
+ if self.current_window_size > LARGEST_FLOW_CONTROL_WINDOW:
+ raise FlowControlError(
+ "Flow control window mustn't exceed %d" %
+ LARGEST_FLOW_CONTROL_WINDOW
+ )
+
+ if self.current_window_size > self.max_window_size:
+ self.max_window_size = self.current_window_size
+
+ def process_bytes(self, size):
+ """
+ The application has informed us that it has processed a certain number
+ of bytes. This may cause us to want to emit a window update frame. If
+ we do want to emit a window update frame, this method will return the
+ number of bytes that we should increment the window by.
+
+ :param size: The number of flow controlled bytes that the application
+ has processed.
+ :type size: ``int``
+ :returns: The number of bytes to increment the flow control window by,
+ or ``None``.
+ :rtype: ``int`` or ``None``
+ """
+ self._bytes_processed += size
+ return self._maybe_update_window()
+
+ def _maybe_update_window(self):
+ """
+ Run the algorithm.
+
+ Our current algorithm can be described like this.
+
+ 1. If no bytes have been processed, we immediately return 0. There is
+ no meaningful way for us to hand space in the window back to the
+ remote peer, so let's not even try.
+ 2. If there is no space in the flow control window, and we have
+ processed at least 1024 bytes (or 1/4 of the window, if the window
+ is smaller), we will emit a window update frame. This is to avoid
+ the risk of blocking a stream altogether.
+ 3. If there is space in the flow control window, and we have processed
+ at least 1/2 of the window worth of bytes, we will emit a window
+ update frame. This is to minimise the number of window update frames
+ we have to emit.
+
+ In a healthy system with large flow control windows, this will
+ irregularly emit WINDOW_UPDATE frames. This prevents us starving the
+ connection by emitting eleventy bajillion WINDOW_UPDATE frames,
+ especially in situations where the remote peer is sending a lot of very
+ small DATA frames.
+ """
+ # TODO: Can the window be smaller than 1024 bytes? If not, we can
+ # streamline this algorithm.
+ if not self._bytes_processed:
+ return None
+
+ max_increment = (self.max_window_size - self.current_window_size)
+ increment = 0
+
+ # Note that, even though we may increment less than _bytes_processed,
+ # we still want to set it to zero whenever we emit an increment. This
+ # is because we'll always increment up to the maximum we can.
+ if (self.current_window_size == 0) and (
+ self._bytes_processed > min(1024, self.max_window_size // 4)):
+ increment = min(self._bytes_processed, max_increment)
+ self._bytes_processed = 0
+ elif self._bytes_processed >= (self.max_window_size // 2):
+ increment = min(self._bytes_processed, max_increment)
+ self._bytes_processed = 0
+
+ self.current_window_size += increment
+ return increment
diff --git a/testing/web-platform/tests/tools/third_party/h2/setup.cfg b/testing/web-platform/tests/tools/third_party/h2/setup.cfg
new file mode 100644
index 0000000000..3670507ff1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/setup.cfg
@@ -0,0 +1,10 @@
+[tool:pytest]
+testpaths = test
+
+[wheel]
+universal = 1
+
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/testing/web-platform/tests/tools/third_party/h2/setup.py b/testing/web-platform/tests/tools/third_party/h2/setup.py
new file mode 100644
index 0000000000..1ce95d5796
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/setup.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import codecs
+import os
+import re
+import sys
+
+try:
+ from setuptools import setup
+except ImportError:
+ from distutils.core import setup
+
+# Get the version
+version_regex = r'__version__ = ["\']([^"\']*)["\']'
+with open('h2/__init__.py', 'r') as f:
+ text = f.read()
+ match = re.search(version_regex, text)
+
+ if match:
+ version = match.group(1)
+ else:
+ raise RuntimeError("No version number found!")
+
+# Stealing this from Kenneth Reitz
+if sys.argv[-1] == 'publish':
+ os.system('python setup.py sdist upload')
+ sys.exit()
+
+packages = [
+ 'h2',
+]
+
+readme = codecs.open('README.rst', encoding='utf-8').read()
+history = codecs.open('HISTORY.rst', encoding='utf-8').read()
+
+setup(
+ name='h2',
+ version=version,
+ description='HTTP/2 State-Machine based protocol implementation',
+ long_description=u'\n\n'.join([readme, history]),
+ author='Cory Benfield',
+ author_email='cory@lukasa.co.uk',
+ url='https://github.com/python-hyper/hyper-h2',
+ project_urls={
+ 'Documentation': 'https://python-hyper.org/projects/h2',
+ 'Source': 'https://github.com/python-hyper/hyper-h2',
+ },
+ packages=packages,
+ package_data={'': ['LICENSE', 'README.rst', 'CONTRIBUTORS.rst', 'HISTORY.rst', 'NOTICES']},
+ package_dir={'h2': 'h2'},
+ include_package_data=True,
+ license='MIT License',
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ 'Programming Language :: Python :: Implementation :: PyPy',
+ ],
+ install_requires=[
+ 'hyperframe>=5.2.0, <6',
+ 'hpack>=3.0,<4',
+ ],
+ extras_require={
+ ':python_version == "2.7"': ['enum34>=1.1.6, <2'],
+ }
+)
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/conftest.py b/testing/web-platform/tests/tools/third_party/h2/test/conftest.py
new file mode 100644
index 0000000000..c646ad361c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/conftest.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+from hypothesis import settings, HealthCheck
+
+import pytest
+import helpers
+
+# Set up a CI profile that allows slow example generation.
+settings.register_profile(
+ "travis",
+ settings(suppress_health_check=[HealthCheck.too_slow])
+)
+
+
+@pytest.fixture
+def frame_factory():
+ return helpers.FrameFactory()
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/coroutine_tests.py b/testing/web-platform/tests/tools/third_party/h2/test/coroutine_tests.py
new file mode 100644
index 0000000000..0f48c02d99
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/coroutine_tests.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+"""
+coroutine_tests
+~~~~~~~~~~~~~~~
+
+This file gives access to a coroutine-based test class. This allows each test
+case to be defined as a pair of interacting coroutines, sending data to each
+other by yielding the flow of control.
+
+The advantage of this method is that we avoid the difficulty of using threads
+in Python, as well as the pain of using sockets and events to communicate and
+organise the communication. This makes the tests entirely deterministic and
+makes them behave identically on all platforms, as well as ensuring they both
+succeed and fail quickly.
+"""
+import itertools
+import functools
+
+import pytest
+
+
+class CoroutineTestCase(object):
+ """
+ A base class for tests that use interacting coroutines.
+
+ The run_until_complete method takes a number of coroutines as arguments.
+ Each one is, in order, passed the output of the previous coroutine until
+ one is exhausted. If a coroutine does not initially yield data (that is,
+ its first action is to receive data), the calling code should prime it by
+ using the 'server' decorator on this class.
+ """
+ def run_until_complete(self, *coroutines):
+ """
+ Executes a set of coroutines that communicate between each other. Each
+ one is, in order, passed the output of the previous coroutine until
+ one is exhausted. If a coroutine does not initially yield data (that
+ is, its first action is to receive data), the calling code should prime
+ it by using the 'server' decorator on this class.
+
+ Once a coroutine is exhausted, the method performs a final check to
+ ensure that all other coroutines are exhausted. This ensures that all
+ assertions in those coroutines got executed.
+ """
+ looping_coroutines = itertools.cycle(coroutines)
+ data = None
+
+ for coro in looping_coroutines:
+ try:
+ data = coro.send(data)
+ except StopIteration:
+ break
+
+ for coro in coroutines:
+ try:
+ next(coro)
+ except StopIteration:
+ continue
+ else:
+ pytest.fail("Coroutine %s not exhausted" % coro)
+
+ def server(self, func):
+ """
+ A decorator that marks a test coroutine as a 'server' coroutine: that
+ is, one whose first action is to consume data, rather than one that
+ initially emits data. The effect of this decorator is simply to prime
+ the coroutine.
+ """
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ c = func(*args, **kwargs)
+ next(c)
+ return c
+
+ return wrapper
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/helpers.py b/testing/web-platform/tests/tools/third_party/h2/test/helpers.py
new file mode 100644
index 0000000000..2a4e909321
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/helpers.py
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+"""
+helpers
+~~~~~~~
+
+This module contains helpers for the h2 tests.
+"""
+from hyperframe.frame import (
+ HeadersFrame, DataFrame, SettingsFrame, WindowUpdateFrame, PingFrame,
+ GoAwayFrame, RstStreamFrame, PushPromiseFrame, PriorityFrame,
+ ContinuationFrame, AltSvcFrame
+)
+from hpack.hpack import Encoder
+
+
+SAMPLE_SETTINGS = {
+ SettingsFrame.HEADER_TABLE_SIZE: 4096,
+ SettingsFrame.ENABLE_PUSH: 1,
+ SettingsFrame.MAX_CONCURRENT_STREAMS: 2,
+}
+
+
+class FrameFactory(object):
+ """
+ A class containing lots of helper methods and state to build frames. This
+ allows test cases to easily build correct HTTP/2 frames to feed to
+ hyper-h2.
+ """
+ def __init__(self):
+ self.encoder = Encoder()
+
+ def refresh_encoder(self):
+ self.encoder = Encoder()
+
+ def preamble(self):
+ return b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'
+
+ def build_headers_frame(self,
+ headers,
+ flags=[],
+ stream_id=1,
+ **priority_kwargs):
+ """
+ Builds a single valid headers frame out of the contained headers.
+ """
+ f = HeadersFrame(stream_id)
+ f.data = self.encoder.encode(headers)
+ f.flags.add('END_HEADERS')
+ for flag in flags:
+ f.flags.add(flag)
+
+ for k, v in priority_kwargs.items():
+ setattr(f, k, v)
+
+ return f
+
+ def build_continuation_frame(self, header_block, flags=[], stream_id=1):
+ """
+ Builds a single continuation frame out of the binary header block.
+ """
+ f = ContinuationFrame(stream_id)
+ f.data = header_block
+ f.flags = set(flags)
+
+ return f
+
+ def build_data_frame(self, data, flags=None, stream_id=1, padding_len=0):
+ """
+ Builds a single data frame out of a chunk of data.
+ """
+ flags = set(flags) if flags is not None else set()
+ f = DataFrame(stream_id)
+ f.data = data
+ f.flags = flags
+
+ if padding_len:
+ flags.add('PADDED')
+ f.pad_length = padding_len
+
+ return f
+
+ def build_settings_frame(self, settings, ack=False):
+ """
+ Builds a single settings frame.
+ """
+ f = SettingsFrame(0)
+ if ack:
+ f.flags.add('ACK')
+
+ f.settings = settings
+ return f
+
+ def build_window_update_frame(self, stream_id, increment):
+ """
+ Builds a single WindowUpdate frame.
+ """
+ f = WindowUpdateFrame(stream_id)
+ f.window_increment = increment
+ return f
+
+ def build_ping_frame(self, ping_data, flags=None):
+ """
+ Builds a single Ping frame.
+ """
+ f = PingFrame(0)
+ f.opaque_data = ping_data
+ if flags:
+ f.flags = set(flags)
+
+ return f
+
+ def build_goaway_frame(self,
+ last_stream_id,
+ error_code=0,
+ additional_data=b''):
+ """
+ Builds a single GOAWAY frame.
+ """
+ f = GoAwayFrame(0)
+ f.error_code = error_code
+ f.last_stream_id = last_stream_id
+ f.additional_data = additional_data
+ return f
+
+ def build_rst_stream_frame(self, stream_id, error_code=0):
+ """
+ Builds a single RST_STREAM frame.
+ """
+ f = RstStreamFrame(stream_id)
+ f.error_code = error_code
+ return f
+
+ def build_push_promise_frame(self,
+ stream_id,
+ promised_stream_id,
+ headers,
+ flags=[]):
+ """
+ Builds a single PUSH_PROMISE frame.
+ """
+ f = PushPromiseFrame(stream_id)
+ f.promised_stream_id = promised_stream_id
+ f.data = self.encoder.encode(headers)
+ f.flags = set(flags)
+ f.flags.add('END_HEADERS')
+ return f
+
+ def build_priority_frame(self,
+ stream_id,
+ weight,
+ depends_on=0,
+ exclusive=False):
+ """
+ Builds a single priority frame.
+ """
+ f = PriorityFrame(stream_id)
+ f.depends_on = depends_on
+ f.stream_weight = weight
+ f.exclusive = exclusive
+ return f
+
+ def build_alt_svc_frame(self, stream_id, origin, field):
+ """
+ Builds a single ALTSVC frame.
+ """
+ f = AltSvcFrame(stream_id)
+ f.origin = origin
+ f.field = field
+ return f
+
+ def change_table_size(self, new_size):
+ """
+ Causes the encoder to send a dynamic size update in the next header
+ block it sends.
+ """
+ self.encoder.header_table_size = new_size
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_basic_logic.py b/testing/web-platform/tests/tools/third_party/h2/test/test_basic_logic.py
new file mode 100644
index 0000000000..7df99a6a57
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_basic_logic.py
@@ -0,0 +1,1877 @@
+# -*- coding: utf-8 -*-
+"""
+test_basic_logic
+~~~~~~~~~~~~~~~~
+
+Test the basic logic of the h2 state machines.
+"""
+import random
+import sys
+
+import hyperframe
+import pytest
+
+import h2.config
+import h2.connection
+import h2.errors
+import h2.events
+import h2.exceptions
+import h2.frame_buffer
+import h2.settings
+import h2.stream
+
+import helpers
+
+from hypothesis import given
+from hypothesis.strategies import integers
+
+
+IS_PYTHON3 = sys.version_info >= (3, 0)
+
+
+class TestBasicClient(object):
+ """
+ Basic client-side tests.
+ """
+ example_request_headers = [
+ (u':authority', u'example.com'),
+ (u':path', u'/'),
+ (u':scheme', u'https'),
+ (u':method', u'GET'),
+ ]
+ bytes_example_request_headers = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'GET'),
+ ]
+ example_response_headers = [
+ (u':status', u'200'),
+ (u'server', u'fake-serv/0.1.0')
+ ]
+ bytes_example_response_headers = [
+ (b':status', b'200'),
+ (b'server', b'fake-serv/0.1.0')
+ ]
+
+ def test_begin_connection(self, frame_factory):
+ """
+ Client connections emit the HTTP/2 preamble.
+ """
+ c = h2.connection.H2Connection()
+ expected_settings = frame_factory.build_settings_frame(
+ c.local_settings
+ )
+ expected_data = (
+ b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' + expected_settings.serialize()
+ )
+
+ events = c.initiate_connection()
+ assert not events
+ assert c.data_to_send() == expected_data
+
+ def test_sending_headers(self):
+ """
+ Single headers frames are correctly encoded.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ # Clear the data, then send headers.
+ c.clear_outbound_data_buffer()
+ events = c.send_headers(1, self.example_request_headers)
+ assert not events
+ assert c.data_to_send() == (
+ b'\x00\x00\r\x01\x04\x00\x00\x00\x01'
+ b'A\x88/\x91\xd3]\x05\\\x87\xa7\x84\x87\x82'
+ )
+
+ def test_sending_data(self):
+ """
+ Single data frames are encoded correctly.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers)
+
+ # Clear the data, then send some data.
+ c.clear_outbound_data_buffer()
+ events = c.send_data(1, b'some data')
+ assert not events
+ data_to_send = c.data_to_send()
+ assert (
+ data_to_send == b'\x00\x00\t\x00\x00\x00\x00\x00\x01some data'
+ )
+
+ buffer = h2.frame_buffer.FrameBuffer(server=False)
+ buffer.max_frame_size = 65535
+ buffer.add_data(data_to_send)
+ data_frame = list(buffer)[0]
+ sanity_check_data_frame(
+ data_frame=data_frame,
+ expected_flow_controlled_length=len(b'some data'),
+ expect_padded_flag=False,
+ expected_data_frame_pad_length=0
+ )
+
+ def test_sending_data_in_memoryview(self):
+ """
+ Support memoryview for sending data.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers)
+
+ # Clear the data, then send some data.
+ c.clear_outbound_data_buffer()
+ events = c.send_data(1, memoryview(b'some data'))
+ assert not events
+ data_to_send = c.data_to_send()
+ assert (
+ data_to_send == b'\x00\x00\t\x00\x00\x00\x00\x00\x01some data'
+ )
+
+ def test_sending_data_with_padding(self):
+ """
+ Single data frames with padding are encoded correctly.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers)
+
+ # Clear the data, then send some data.
+ c.clear_outbound_data_buffer()
+ events = c.send_data(1, b'some data', pad_length=5)
+ assert not events
+ data_to_send = c.data_to_send()
+ assert data_to_send == (
+ b'\x00\x00\x0f\x00\x08\x00\x00\x00\x01'
+ b'\x05some data\x00\x00\x00\x00\x00'
+ )
+
+ buffer = h2.frame_buffer.FrameBuffer(server=False)
+ buffer.max_frame_size = 65535
+ buffer.add_data(data_to_send)
+ data_frame = list(buffer)[0]
+ sanity_check_data_frame(
+ data_frame=data_frame,
+ expected_flow_controlled_length=len(b'some data') + 1 + 5,
+ expect_padded_flag=True,
+ expected_data_frame_pad_length=5
+ )
+
+ def test_sending_data_with_zero_length_padding(self):
+ """
+ Single data frames with zero-length padding are encoded
+ correctly.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers)
+
+ # Clear the data, then send some data.
+ c.clear_outbound_data_buffer()
+ events = c.send_data(1, b'some data', pad_length=0)
+ assert not events
+ data_to_send = c.data_to_send()
+ assert data_to_send == (
+ b'\x00\x00\x0a\x00\x08\x00\x00\x00\x01'
+ b'\x00some data'
+ )
+
+ buffer = h2.frame_buffer.FrameBuffer(server=False)
+ buffer.max_frame_size = 65535
+ buffer.add_data(data_to_send)
+ data_frame = list(buffer)[0]
+ sanity_check_data_frame(
+ data_frame=data_frame,
+ expected_flow_controlled_length=len(b'some data') + 1,
+ expect_padded_flag=True,
+ expected_data_frame_pad_length=0
+ )
+
+ @pytest.mark.parametrize("expected_error,pad_length", [
+ (None, 0),
+ (None, 255),
+ (None, None),
+ (ValueError, -1),
+ (ValueError, 256),
+ (TypeError, 'invalid'),
+ (TypeError, ''),
+ (TypeError, '10'),
+ (TypeError, {}),
+ (TypeError, ['1', '2', '3']),
+ (TypeError, []),
+ (TypeError, 1.5),
+ (TypeError, 1.0),
+ (TypeError, -1.0),
+ ])
+ def test_sending_data_with_invalid_padding_length(self,
+ expected_error,
+ pad_length):
+ """
+ ``send_data`` with a ``pad_length`` parameter that is an integer
+ outside the range of [0, 255] throws a ``ValueError``, and a
+ ``pad_length`` parameter which is not an ``integer`` type
+ throws a ``TypeError``.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers)
+
+ c.clear_outbound_data_buffer()
+ if expected_error is not None:
+ with pytest.raises(expected_error):
+ c.send_data(1, b'some data', pad_length=pad_length)
+ else:
+ c.send_data(1, b'some data', pad_length=pad_length)
+
+ def test_closing_stream_sending_data(self, frame_factory):
+ """
+ We can close a stream with a data frame.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers)
+
+ f = frame_factory.build_data_frame(
+ data=b'some data',
+ flags=['END_STREAM'],
+ )
+
+ # Clear the data, then send some data.
+ c.clear_outbound_data_buffer()
+ events = c.send_data(1, b'some data', end_stream=True)
+ assert not events
+ assert c.data_to_send() == f.serialize()
+
+ def test_receiving_a_response(self, frame_factory):
+ """
+ When receiving a response, the ResponseReceived event fires.
+ """
+ config = h2.config.H2Configuration(header_encoding='utf-8')
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+
+ # Clear the data
+ f = frame_factory.build_headers_frame(
+ self.example_response_headers
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.ResponseReceived)
+ assert event.stream_id == 1
+ assert event.headers == self.example_response_headers
+
+ def test_receiving_a_response_bytes(self, frame_factory):
+ """
+ When receiving a response, the ResponseReceived event fires with bytes
+ headers if the encoding is set appropriately.
+ """
+ config = h2.config.H2Configuration(header_encoding=False)
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+
+ # Clear the data
+ f = frame_factory.build_headers_frame(
+ self.example_response_headers
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.ResponseReceived)
+ assert event.stream_id == 1
+ assert event.headers == self.bytes_example_response_headers
+
+ def test_receiving_a_response_change_encoding(self, frame_factory):
+ """
+ When receiving a response, the ResponseReceived event fires with bytes
+ headers if the encoding is set appropriately, but if this changes then
+ the change reflects it.
+ """
+ config = h2.config.H2Configuration(header_encoding=False)
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+
+ f = frame_factory.build_headers_frame(
+ self.example_response_headers
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.ResponseReceived)
+ assert event.stream_id == 1
+ assert event.headers == self.bytes_example_response_headers
+
+ c.send_headers(3, self.example_request_headers, end_stream=True)
+ c.config.header_encoding = 'utf-8'
+ f = frame_factory.build_headers_frame(
+ self.example_response_headers,
+ stream_id=3,
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.ResponseReceived)
+ assert event.stream_id == 3
+ assert event.headers == self.example_response_headers
+
+ def test_end_stream_without_data(self, frame_factory):
+ """
+ Ending a stream without data emits a zero-length DATA frame with
+ END_STREAM set.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=False)
+
+ # Clear the data
+ c.clear_outbound_data_buffer()
+ f = frame_factory.build_data_frame(b'', flags=['END_STREAM'])
+ events = c.end_stream(1)
+
+ assert not events
+ assert c.data_to_send() == f.serialize()
+
+ def test_cannot_send_headers_on_lower_stream_id(self):
+ """
+ Once stream ID x has been used, cannot use stream ID y where y < x.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(3, self.example_request_headers, end_stream=False)
+
+ with pytest.raises(h2.exceptions.StreamIDTooLowError) as e:
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+
+ assert e.value.stream_id == 1
+ assert e.value.max_stream_id == 3
+
+ def test_receiving_pushed_stream(self, frame_factory):
+ """
+ Pushed streams fire a PushedStreamReceived event, followed by
+ ResponseReceived when the response headers are received.
+ """
+ config = h2.config.H2Configuration(header_encoding='utf-8')
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=False)
+
+ f1 = frame_factory.build_headers_frame(
+ self.example_response_headers
+ )
+ f2 = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=self.example_request_headers,
+ flags=['END_HEADERS'],
+ )
+ f3 = frame_factory.build_headers_frame(
+ self.example_response_headers,
+ stream_id=2,
+ )
+ data = b''.join(x.serialize() for x in [f1, f2, f3])
+
+ events = c.receive_data(data)
+
+ assert len(events) == 3
+ stream_push_event = events[1]
+ response_event = events[2]
+ assert isinstance(stream_push_event, h2.events.PushedStreamReceived)
+ assert isinstance(response_event, h2.events.ResponseReceived)
+
+ assert stream_push_event.pushed_stream_id == 2
+ assert stream_push_event.parent_stream_id == 1
+ assert (
+ stream_push_event.headers == self.example_request_headers
+ )
+ assert response_event.stream_id == 2
+ assert response_event.headers == self.example_response_headers
+
+ def test_receiving_pushed_stream_bytes(self, frame_factory):
+ """
+ Pushed headers are not decoded if the header encoding is set to False.
+ """
+ config = h2.config.H2Configuration(header_encoding=False)
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=False)
+
+ f1 = frame_factory.build_headers_frame(
+ self.example_response_headers
+ )
+ f2 = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=self.example_request_headers,
+ flags=['END_HEADERS'],
+ )
+ f3 = frame_factory.build_headers_frame(
+ self.example_response_headers,
+ stream_id=2,
+ )
+ data = b''.join(x.serialize() for x in [f1, f2, f3])
+
+ events = c.receive_data(data)
+
+ assert len(events) == 3
+ stream_push_event = events[1]
+ response_event = events[2]
+ assert isinstance(stream_push_event, h2.events.PushedStreamReceived)
+ assert isinstance(response_event, h2.events.ResponseReceived)
+
+ assert stream_push_event.pushed_stream_id == 2
+ assert stream_push_event.parent_stream_id == 1
+ assert (
+ stream_push_event.headers == self.bytes_example_request_headers
+ )
+ assert response_event.stream_id == 2
+ assert response_event.headers == self.bytes_example_response_headers
+
+ def test_cannot_receive_pushed_stream_when_enable_push_is_0(self,
+ frame_factory):
+ """
+ If we have set SETTINGS_ENABLE_PUSH to 0, receiving PUSH_PROMISE frames
+ triggers the connection to be closed.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.local_settings.enable_push = 0
+ c.send_headers(1, self.example_request_headers, end_stream=False)
+
+ f1 = frame_factory.build_settings_frame({}, ack=True)
+ f2 = frame_factory.build_headers_frame(
+ self.example_response_headers
+ )
+ f3 = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=self.example_request_headers,
+ flags=['END_HEADERS'],
+ )
+ c.receive_data(f1.serialize())
+ c.receive_data(f2.serialize())
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f3.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ 0, h2.errors.ErrorCodes.PROTOCOL_ERROR
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_receiving_response_no_body(self, frame_factory):
+ """
+ Receiving a response without a body fires two events, ResponseReceived
+ and StreamEnded.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+
+ f = frame_factory.build_headers_frame(
+ self.example_response_headers,
+ flags=['END_STREAM']
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 2
+ response_event = events[0]
+ end_stream = events[1]
+
+ assert isinstance(response_event, h2.events.ResponseReceived)
+ assert isinstance(end_stream, h2.events.StreamEnded)
+
+ def test_oversize_headers(self):
+ """
+ Sending headers that are oversized generates a stream of CONTINUATION
+ frames.
+ """
+ all_bytes = [chr(x) for x in range(0, 256)]
+ if IS_PYTHON3:
+ all_bytes = [x.encode('latin1') for x in all_bytes]
+
+ large_binary_string = b''.join(
+ random.choice(all_bytes) for _ in range(0, 256)
+ )
+ test_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':method', 'GET'),
+ (':scheme', 'https'),
+ ('key', large_binary_string)
+ ]
+ c = h2.connection.H2Connection()
+
+ # Greatly shrink the max frame size to force us over.
+ c.max_outbound_frame_size = 48
+ c.initiate_connection()
+ c.send_headers(1, test_headers, end_stream=True)
+
+ # Use the frame buffer here, because we don't care about decoding
+ # the headers. Don't send all the data in because that will force the
+ # frame buffer to stop caching the CONTINUATION frames, so instead
+ # send all but one byte.
+ buffer = h2.frame_buffer.FrameBuffer(server=True)
+ buffer.max_frame_size = 65535
+ data = c.data_to_send()
+ buffer.add_data(data[:-1])
+
+ # Drain the buffer, confirming that it only provides a single frame
+ # (the settings frame)
+ assert len(list(buffer)) == 1
+
+ # Get the cached frames.
+ frames = buffer._headers_buffer
+
+ # Split the frames up.
+ headers_frame = frames[0]
+ continuation_frames = frames[1:]
+
+ assert isinstance(headers_frame, hyperframe.frame.HeadersFrame)
+ assert all(
+ map(
+ lambda f: isinstance(f, hyperframe.frame.ContinuationFrame),
+ continuation_frames)
+ )
+ assert all(
+ map(lambda f: len(f.data) <= c.max_outbound_frame_size, frames)
+ )
+
+ assert frames[0].flags == {'END_STREAM'}
+
+ buffer.add_data(data[-1:])
+ headers = list(buffer)[0]
+ assert isinstance(headers, hyperframe.frame.HeadersFrame)
+
+ def test_handle_stream_reset(self, frame_factory):
+ """
+ Streams being remotely reset fires a StreamReset event.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_rst_stream_frame(
+ stream_id=1,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.StreamReset)
+ assert event.stream_id == 1
+ assert event.error_code is h2.errors.ErrorCodes.STREAM_CLOSED
+ assert isinstance(event.error_code, h2.errors.ErrorCodes)
+ assert event.remote_reset
+
+ def test_handle_stream_reset_with_unknown_erorr_code(self, frame_factory):
+ """
+ Streams being remotely reset with unknown error codes behave exactly as
+ they do with known error codes, but the error code on the event is an
+ int, instead of being an ErrorCodes.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_rst_stream_frame(stream_id=1, error_code=0xFA)
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.StreamReset)
+ assert event.stream_id == 1
+ assert event.error_code == 250
+ assert not isinstance(event.error_code, h2.errors.ErrorCodes)
+ assert event.remote_reset
+
+ def test_can_consume_partial_data_from_connection(self):
+ """
+ We can do partial reads from the connection.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ assert len(c.data_to_send(2)) == 2
+ assert len(c.data_to_send(3)) == 3
+ assert 0 < len(c.data_to_send(500)) < 500
+ assert len(c.data_to_send(10)) == 0
+ assert len(c.data_to_send()) == 0
+
+ def test_we_can_update_settings(self, frame_factory):
+ """
+ Updating the settings emits a SETTINGS frame.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.clear_outbound_data_buffer()
+
+ new_settings = {
+ h2.settings.SettingCodes.HEADER_TABLE_SIZE: 52,
+ h2.settings.SettingCodes.ENABLE_PUSH: 0,
+ }
+ events = c.update_settings(new_settings)
+ assert not events
+
+ f = frame_factory.build_settings_frame(new_settings)
+ assert c.data_to_send() == f.serialize()
+
+ def test_settings_get_acked_correctly(self, frame_factory):
+ """
+ When settings changes are ACKed, they contain the changed settings.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ new_settings = {
+ h2.settings.SettingCodes.HEADER_TABLE_SIZE: 52,
+ h2.settings.SettingCodes.ENABLE_PUSH: 0,
+ }
+ c.update_settings(new_settings)
+
+ f = frame_factory.build_settings_frame({}, ack=True)
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.SettingsAcknowledged)
+ assert len(event.changed_settings) == len(new_settings)
+ for setting, value in new_settings.items():
+ assert event.changed_settings[setting].new_value == value
+
+ def test_cannot_create_new_outbound_stream_over_limit(self, frame_factory):
+ """
+ When the number of outbound streams exceeds the remote peer's
+ MAX_CONCURRENT_STREAMS setting, attempting to open new streams fails.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ f = frame_factory.build_settings_frame(
+ {h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 1}
+ )
+ c.receive_data(f.serialize())[0]
+
+ c.send_headers(1, self.example_request_headers)
+
+ with pytest.raises(h2.exceptions.TooManyStreamsError):
+ c.send_headers(3, self.example_request_headers)
+
+ def test_can_receive_trailers(self, frame_factory):
+ """
+ When two HEADERS blocks are received in the same stream from a
+ server, the second set are trailers.
+ """
+ config = h2.config.H2Configuration(header_encoding='utf-8')
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers)
+ f = frame_factory.build_headers_frame(self.example_response_headers)
+ c.receive_data(f.serialize())
+
+ # Send in trailers.
+ trailers = [('content-length', '0')]
+ f = frame_factory.build_headers_frame(
+ trailers,
+ flags=['END_STREAM'],
+ )
+ events = c.receive_data(f.serialize())
+ assert len(events) == 2
+
+ event = events[0]
+ assert isinstance(event, h2.events.TrailersReceived)
+ assert event.headers == trailers
+ assert event.stream_id == 1
+
+ def test_reject_trailers_not_ending_stream(self, frame_factory):
+ """
+ When trailers are received without the END_STREAM flag being present,
+ this is a ProtocolError.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers)
+ f = frame_factory.build_headers_frame(self.example_response_headers)
+ c.receive_data(f.serialize())
+
+ # Send in trailers.
+ c.clear_outbound_data_buffer()
+ trailers = [('content-length', '0')]
+ f = frame_factory.build_headers_frame(
+ trailers,
+ flags=[],
+ )
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=0, error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_can_send_trailers(self, frame_factory):
+ """
+ When a second set of headers are sent, they are properly trailers.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.clear_outbound_data_buffer()
+ c.send_headers(1, self.example_request_headers)
+
+ # Now send trailers.
+ trailers = [('content-length', '0')]
+ c.send_headers(1, trailers, end_stream=True)
+
+ frame_factory.refresh_encoder()
+ f1 = frame_factory.build_headers_frame(
+ self.example_request_headers,
+ )
+ f2 = frame_factory.build_headers_frame(
+ trailers,
+ flags=['END_STREAM'],
+ )
+ assert c.data_to_send() == f1.serialize() + f2.serialize()
+
+ def test_trailers_must_have_end_stream(self, frame_factory):
+ """
+ A set of trailers must carry the END_STREAM flag.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ # Send headers.
+ c.send_headers(1, self.example_request_headers)
+
+ # Now send trailers.
+ trailers = [('content-length', '0')]
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.send_headers(1, trailers)
+
+ def test_headers_are_lowercase(self, frame_factory):
+ """
+ When headers are sent, they are forced to lower-case.
+ """
+ weird_headers = self.example_request_headers + [
+ ('ChAnGiNg-CaSe', 'AlsoHere'),
+ ('alllowercase', 'alllowercase'),
+ ('ALLCAPS', 'ALLCAPS'),
+ ]
+ expected_headers = self.example_request_headers + [
+ ('changing-case', 'AlsoHere'),
+ ('alllowercase', 'alllowercase'),
+ ('allcaps', 'ALLCAPS'),
+ ]
+
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.clear_outbound_data_buffer()
+
+ c.send_headers(1, weird_headers)
+ expected_frame = frame_factory.build_headers_frame(
+ headers=expected_headers
+ )
+
+ assert c.data_to_send() == expected_frame.serialize()
+
+ @given(frame_size=integers(min_value=2**14, max_value=(2**24 - 1)))
+ def test_changing_max_frame_size(self, frame_factory, frame_size):
+ """
+ When the user changes the max frame size and the change is ACKed, the
+ remote peer is now bound by the new frame size.
+ """
+ # We need to refresh the encoder because hypothesis has a problem with
+ # integrating with py.test, meaning that we use the same frame factory
+ # for all tests.
+ # See https://github.com/HypothesisWorks/hypothesis-python/issues/377
+ frame_factory.refresh_encoder()
+
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ # Set up the stream.
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+ headers_frame = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ )
+ c.receive_data(headers_frame.serialize())
+
+ # Change the max frame size.
+ c.update_settings(
+ {h2.settings.SettingCodes.MAX_FRAME_SIZE: frame_size}
+ )
+ settings_ack = frame_factory.build_settings_frame({}, ack=True)
+ c.receive_data(settings_ack.serialize())
+
+ # Greatly increase the flow control windows: we're not here to test
+ # flow control today.
+ c.increment_flow_control_window(increment=(2 * frame_size) + 1)
+ c.increment_flow_control_window(
+ increment=(2 * frame_size) + 1, stream_id=1
+ )
+
+ # Send one DATA frame that is exactly the max frame size: confirm it's
+ # fine.
+ data = frame_factory.build_data_frame(
+ data=(b'\x00' * frame_size),
+ )
+ events = c.receive_data(data.serialize())
+ assert len(events) == 1
+ assert isinstance(events[0], h2.events.DataReceived)
+ assert events[0].flow_controlled_length == frame_size
+
+ # Send one that is one byte too large: confirm a protocol error is
+ # raised.
+ data.data += b'\x00'
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(data.serialize())
+
+ def test_cookies_are_joined_on_push(self, frame_factory):
+ """
+ RFC 7540 Section 8.1.2.5 requires that we join multiple Cookie headers
+ in a header block together when they're received on a push.
+ """
+ # This is a moderately varied set of cookie headers: some combined,
+ # some split.
+ cookie_headers = [
+ ('cookie',
+ 'username=John Doe; expires=Thu, 18 Dec 2013 12:00:00 UTC'),
+ ('cookie', 'path=1'),
+ ('cookie', 'test1=val1; test2=val2')
+ ]
+ expected = (
+ 'username=John Doe; expires=Thu, 18 Dec 2013 12:00:00 UTC; '
+ 'path=1; test1=val1; test2=val2'
+ )
+
+ config = h2.config.H2Configuration(header_encoding='utf-8')
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+
+ f = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=self.example_request_headers + cookie_headers
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ e = events[0]
+
+ cookie_fields = [(n, v) for n, v in e.headers if n == 'cookie']
+ assert len(cookie_fields) == 1
+
+ _, v = cookie_fields[0]
+ assert v == expected
+
+ def test_cookies_arent_joined_without_normalization(self, frame_factory):
+ """
+ If inbound header normalization is disabled, cookie headers aren't
+ joined.
+ """
+ # This is a moderately varied set of cookie headers: some combined,
+ # some split.
+ cookie_headers = [
+ ('cookie',
+ 'username=John Doe; expires=Thu, 18 Dec 2013 12:00:00 UTC'),
+ ('cookie', 'path=1'),
+ ('cookie', 'test1=val1; test2=val2')
+ ]
+
+ config = h2.config.H2Configuration(
+ client_side=True,
+ normalize_inbound_headers=False,
+ header_encoding='utf-8'
+ )
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+
+ f = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=self.example_request_headers + cookie_headers
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ e = events[0]
+
+ received_cookies = [(n, v) for n, v in e.headers if n == 'cookie']
+ assert len(received_cookies) == 3
+ assert cookie_headers == received_cookies
+
+
+class TestBasicServer(object):
+ """
+ Basic server-side tests.
+ """
+ example_request_headers = [
+ (u':authority', u'example.com'),
+ (u':path', u'/'),
+ (u':scheme', u'https'),
+ (u':method', u'GET'),
+ ]
+ bytes_example_request_headers = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'GET'),
+ ]
+ example_response_headers = [
+ (':status', '200'),
+ ('server', 'hyper-h2/0.1.0')
+ ]
+ server_config = h2.config.H2Configuration(
+ client_side=False, header_encoding='utf-8'
+ )
+
+ def test_ignores_preamble(self):
+ """
+ The preamble does not cause any events or frames to be written.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ preamble = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'
+
+ events = c.receive_data(preamble)
+ assert not events
+ assert not c.data_to_send()
+
+ @pytest.mark.parametrize("chunk_size", range(1, 24))
+ def test_drip_feed_preamble(self, chunk_size):
+ """
+ The preamble can be sent in in less than a single buffer.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ preamble = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'
+ events = []
+
+ for i in range(0, len(preamble), chunk_size):
+ events += c.receive_data(preamble[i:i+chunk_size])
+
+ assert not events
+ assert not c.data_to_send()
+
+ def test_initiate_connection_sends_server_preamble(self, frame_factory):
+ """
+ For server-side connections, initiate_connection sends a server
+ preamble.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ expected_settings = frame_factory.build_settings_frame(
+ c.local_settings
+ )
+ expected_data = expected_settings.serialize()
+
+ events = c.initiate_connection()
+ assert not events
+ assert c.data_to_send() == expected_data
+
+ def test_headers_event(self, frame_factory):
+ """
+ When a headers frame is received a RequestReceived event fires.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(self.example_request_headers)
+ data = f.serialize()
+ events = c.receive_data(data)
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.RequestReceived)
+ assert event.stream_id == 1
+ assert event.headers == self.example_request_headers
+
+ def test_headers_event_bytes(self, frame_factory):
+ """
+ When a headers frame is received a RequestReceived event fires with
+ bytes headers if the encoding is set appropriately.
+ """
+ config = h2.config.H2Configuration(
+ client_side=False, header_encoding=False
+ )
+ c = h2.connection.H2Connection(config=config)
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(self.example_request_headers)
+ data = f.serialize()
+ events = c.receive_data(data)
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.RequestReceived)
+ assert event.stream_id == 1
+ assert event.headers == self.bytes_example_request_headers
+
+ def test_data_event(self, frame_factory):
+ """
+ Test that data received on a stream fires a DataReceived event.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ f1 = frame_factory.build_headers_frame(
+ self.example_request_headers, stream_id=3
+ )
+ f2 = frame_factory.build_data_frame(
+ b'some request data',
+ stream_id=3,
+ )
+ data = b''.join(map(lambda f: f.serialize(), [f1, f2]))
+ events = c.receive_data(data)
+
+ assert len(events) == 2
+ event = events[1]
+
+ assert isinstance(event, h2.events.DataReceived)
+ assert event.stream_id == 3
+ assert event.data == b'some request data'
+ assert event.flow_controlled_length == 17
+
+ def test_data_event_with_padding(self, frame_factory):
+ """
+ Test that data received on a stream fires a DataReceived event that
+ accounts for padding.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ f1 = frame_factory.build_headers_frame(
+ self.example_request_headers, stream_id=3
+ )
+ f2 = frame_factory.build_data_frame(
+ b'some request data',
+ stream_id=3,
+ padding_len=20
+ )
+ data = b''.join(map(lambda f: f.serialize(), [f1, f2]))
+ events = c.receive_data(data)
+
+ assert len(events) == 2
+ event = events[1]
+
+ assert isinstance(event, h2.events.DataReceived)
+ assert event.stream_id == 3
+ assert event.data == b'some request data'
+ assert event.flow_controlled_length == 17 + 20 + 1
+
+ def test_receiving_ping_frame(self, frame_factory):
+ """
+ Ping frames should be immediately ACKed.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ ping_data = b'\x01' * 8
+ sent_frame = frame_factory.build_ping_frame(ping_data)
+ expected_frame = frame_factory.build_ping_frame(
+ ping_data, flags=["ACK"]
+ )
+ expected_data = expected_frame.serialize()
+
+ c.clear_outbound_data_buffer()
+ events = c.receive_data(sent_frame.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.PingReceived)
+ assert event.ping_data == ping_data
+
+ assert c.data_to_send() == expected_data
+
+ def test_receiving_settings_frame_event(self, frame_factory):
+ """
+ Settings frames should cause a RemoteSettingsChanged event to fire.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_settings_frame(
+ settings=helpers.SAMPLE_SETTINGS
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.RemoteSettingsChanged)
+ assert len(event.changed_settings) == len(helpers.SAMPLE_SETTINGS)
+
+ def test_acknowledging_settings(self, frame_factory):
+ """
+ Acknowledging settings causes appropriate Settings frame to be emitted.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ received_frame = frame_factory.build_settings_frame(
+ settings=helpers.SAMPLE_SETTINGS
+ )
+ expected_frame = frame_factory.build_settings_frame(
+ settings={}, ack=True
+ )
+ expected_data = expected_frame.serialize()
+
+ c.clear_outbound_data_buffer()
+ events = c.receive_data(received_frame.serialize())
+
+ assert len(events) == 1
+ assert c.data_to_send() == expected_data
+
+ def test_close_connection(self, frame_factory):
+ """
+ Closing the connection with no error code emits a GOAWAY frame with
+ error code 0.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_goaway_frame(last_stream_id=0)
+ expected_data = f.serialize()
+
+ c.clear_outbound_data_buffer()
+ events = c.close_connection()
+
+ assert not events
+ assert c.data_to_send() == expected_data
+
+ @pytest.mark.parametrize("error_code", h2.errors.ErrorCodes)
+ def test_close_connection_with_error_code(self, frame_factory, error_code):
+ """
+ Closing the connection with an error code emits a GOAWAY frame with
+ that error code.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_goaway_frame(
+ error_code=error_code, last_stream_id=0
+ )
+ expected_data = f.serialize()
+
+ c.clear_outbound_data_buffer()
+ events = c.close_connection(error_code)
+
+ assert not events
+ assert c.data_to_send() == expected_data
+
+ @pytest.mark.parametrize("last_stream_id,output", [
+ (None, 23),
+ (0, 0),
+ (42, 42)
+ ])
+ def test_close_connection_with_last_stream_id(self, frame_factory,
+ last_stream_id, output):
+ """
+ Closing the connection with last_stream_id set emits a GOAWAY frame
+ with that value.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ headers_frame = frame_factory.build_headers_frame(
+ [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ],
+ stream_id=23)
+ c.receive_data(headers_frame.serialize())
+
+ f = frame_factory.build_goaway_frame(
+ last_stream_id=output
+ )
+ expected_data = f.serialize()
+
+ c.clear_outbound_data_buffer()
+ events = c.close_connection(last_stream_id=last_stream_id)
+
+ assert not events
+ assert c.data_to_send() == expected_data
+
+ @pytest.mark.parametrize("additional_data,output", [
+ (None, b''),
+ (b'', b''),
+ (b'foobar', b'foobar')
+ ])
+ def test_close_connection_with_additional_data(self, frame_factory,
+ additional_data, output):
+ """
+ Closing the connection with additional debug data emits a GOAWAY frame
+ with that data attached.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_goaway_frame(
+ last_stream_id=0, additional_data=output
+ )
+ expected_data = f.serialize()
+
+ c.clear_outbound_data_buffer()
+ events = c.close_connection(additional_data=additional_data)
+
+ assert not events
+ assert c.data_to_send() == expected_data
+
+ def test_reset_stream(self, frame_factory):
+ """
+ Resetting a stream with no error code emits a RST_STREAM frame with
+ error code 0.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_headers_frame(self.example_request_headers)
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ expected_frame = frame_factory.build_rst_stream_frame(stream_id=1)
+ expected_data = expected_frame.serialize()
+
+ events = c.reset_stream(stream_id=1)
+
+ assert not events
+ assert c.data_to_send() == expected_data
+
+ @pytest.mark.parametrize("error_code", h2.errors.ErrorCodes)
+ def test_reset_stream_with_error_code(self, frame_factory, error_code):
+ """
+ Resetting a stream with an error code emits a RST_STREAM frame with
+ that error code.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers,
+ stream_id=3
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ expected_frame = frame_factory.build_rst_stream_frame(
+ stream_id=3, error_code=error_code
+ )
+ expected_data = expected_frame.serialize()
+
+ events = c.reset_stream(stream_id=3, error_code=error_code)
+
+ assert not events
+ assert c.data_to_send() == expected_data
+
+ def test_cannot_reset_nonexistent_stream(self, frame_factory):
+ """
+ Resetting nonexistent streams raises NoSuchStreamError.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers,
+ stream_id=3
+ )
+ c.receive_data(f.serialize())
+
+ with pytest.raises(h2.exceptions.NoSuchStreamError) as e:
+ c.reset_stream(stream_id=1)
+
+ assert e.value.stream_id == 1
+
+ with pytest.raises(h2.exceptions.NoSuchStreamError) as e:
+ c.reset_stream(stream_id=5)
+
+ assert e.value.stream_id == 5
+
+ def test_basic_sending_ping_frame_logic(self, frame_factory):
+ """
+ Sending ping frames serializes a ping frame on stream 0 with
+ approriate opaque data.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ ping_data = b'\x01\x02\x03\x04\x05\x06\x07\x08'
+
+ expected_frame = frame_factory.build_ping_frame(ping_data)
+ expected_data = expected_frame.serialize()
+
+ events = c.ping(ping_data)
+
+ assert not events
+ assert c.data_to_send() == expected_data
+
+ @pytest.mark.parametrize(
+ 'opaque_data',
+ [
+ b'',
+ b'\x01\x02\x03\x04\x05\x06\x07',
+ u'abcdefgh',
+ b'too many bytes',
+ ]
+ )
+ def test_ping_frame_opaque_data_must_be_length_8_bytestring(self,
+ frame_factory,
+ opaque_data):
+ """
+ Sending a ping frame only works with 8-byte bytestrings.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ with pytest.raises(ValueError):
+ c.ping(opaque_data)
+
+ def test_receiving_ping_acknowledgement(self, frame_factory):
+ """
+ Receiving a PING acknowledgement fires a PingAckReceived event.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ ping_data = b'\x01\x02\x03\x04\x05\x06\x07\x08'
+
+ f = frame_factory.build_ping_frame(
+ ping_data, flags=['ACK']
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.PingAckReceived)
+ assert isinstance(event, h2.events.PingAcknowledged) # deprecated
+ assert event.ping_data == ping_data
+
+ def test_stream_ended_remotely(self, frame_factory):
+ """
+ When the remote stream ends with a non-empty data frame a DataReceived
+ event and a StreamEnded event are fired.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ f1 = frame_factory.build_headers_frame(
+ self.example_request_headers, stream_id=3
+ )
+ f2 = frame_factory.build_data_frame(
+ b'some request data',
+ flags=['END_STREAM'],
+ stream_id=3,
+ )
+ data = b''.join(map(lambda f: f.serialize(), [f1, f2]))
+ events = c.receive_data(data)
+
+ assert len(events) == 3
+ data_event = events[1]
+ stream_ended_event = events[2]
+
+ assert isinstance(data_event, h2.events.DataReceived)
+ assert isinstance(stream_ended_event, h2.events.StreamEnded)
+ stream_ended_event.stream_id == 3
+
+ def test_can_push_stream(self, frame_factory):
+ """
+ Pushing a stream causes a PUSH_PROMISE frame to be emitted.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers
+ )
+ c.receive_data(f.serialize())
+
+ frame_factory.refresh_encoder()
+ expected_frame = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=self.example_request_headers,
+ flags=['END_HEADERS'],
+ )
+
+ c.clear_outbound_data_buffer()
+ c.push_stream(
+ stream_id=1,
+ promised_stream_id=2,
+ request_headers=self.example_request_headers
+ )
+
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_cannot_push_streams_when_disabled(self, frame_factory):
+ """
+ When the remote peer has disabled stream pushing, we should fail.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_settings_frame(
+ {h2.settings.SettingCodes.ENABLE_PUSH: 0}
+ )
+ c.receive_data(f.serialize())
+
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers
+ )
+ c.receive_data(f.serialize())
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.push_stream(
+ stream_id=1,
+ promised_stream_id=2,
+ request_headers=self.example_request_headers
+ )
+
+ def test_settings_remote_change_header_table_size(self, frame_factory):
+ """
+ Acknowledging a remote HEADER_TABLE_SIZE settings change causes us to
+ change the header table size of our encoder.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ assert c.encoder.header_table_size == 4096
+
+ received_frame = frame_factory.build_settings_frame(
+ {h2.settings.SettingCodes.HEADER_TABLE_SIZE: 80}
+ )
+ c.receive_data(received_frame.serialize())[0]
+
+ assert c.encoder.header_table_size == 80
+
+ def test_settings_local_change_header_table_size(self, frame_factory):
+ """
+ The remote peer acknowledging a local HEADER_TABLE_SIZE settings change
+ does not cause us to change the header table size of our decoder.
+
+ For an explanation of why this test is this way around, see issue #37.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ assert c.decoder.header_table_size == 4096
+
+ expected_frame = frame_factory.build_settings_frame({}, ack=True)
+ c.update_settings(
+ {h2.settings.SettingCodes.HEADER_TABLE_SIZE: 80}
+ )
+ c.receive_data(expected_frame.serialize())
+ c.clear_outbound_data_buffer()
+
+ assert c.decoder.header_table_size == 4096
+
+ def test_restricting_outbound_frame_size_by_settings(self, frame_factory):
+ """
+ The remote peer can shrink the maximum outbound frame size using
+ settings.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(self.example_request_headers)
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.FrameTooLargeError):
+ c.send_data(1, b'\x01' * 17000)
+
+ received_frame = frame_factory.build_settings_frame(
+ {h2.settings.SettingCodes.MAX_FRAME_SIZE: 17001}
+ )
+ c.receive_data(received_frame.serialize())
+
+ c.send_data(1, b'\x01' * 17000)
+ assert c.data_to_send()
+
+ def test_restricting_inbound_frame_size_by_settings(self, frame_factory):
+ """
+ We throw ProtocolErrors and tear down connections if oversize frames
+ are received.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ h = frame_factory.build_headers_frame(self.example_request_headers)
+ c.receive_data(h.serialize())
+ c.clear_outbound_data_buffer()
+
+ data_frame = frame_factory.build_data_frame(b'\x01' * 17000)
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(data_frame.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=1, error_code=h2.errors.ErrorCodes.FRAME_SIZE_ERROR
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_cannot_receive_new_streams_over_limit(self, frame_factory):
+ """
+ When the number of inbound streams exceeds our MAX_CONCURRENT_STREAMS
+ setting, their attempt to open new streams fails.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.update_settings(
+ {h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 1}
+ )
+ f = frame_factory.build_settings_frame({}, ack=True)
+ c.receive_data(f.serialize())
+
+ f = frame_factory.build_headers_frame(
+ stream_id=1,
+ headers=self.example_request_headers,
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ stream_id=3,
+ headers=self.example_request_headers,
+ )
+ with pytest.raises(h2.exceptions.TooManyStreamsError):
+ c.receive_data(f.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=1, error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_can_receive_trailers(self, frame_factory):
+ """
+ When two HEADERS blocks are received in the same stream from a
+ client, the second set are trailers.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_headers_frame(self.example_request_headers)
+ c.receive_data(f.serialize())
+
+ # Send in trailers.
+ trailers = [('content-length', '0')]
+ f = frame_factory.build_headers_frame(
+ trailers,
+ flags=['END_STREAM'],
+ )
+ events = c.receive_data(f.serialize())
+ assert len(events) == 2
+
+ event = events[0]
+ assert isinstance(event, h2.events.TrailersReceived)
+ assert event.headers == trailers
+ assert event.stream_id == 1
+
+ def test_reject_trailers_not_ending_stream(self, frame_factory):
+ """
+ When trailers are received without the END_STREAM flag being present,
+ this is a ProtocolError.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_headers_frame(self.example_request_headers)
+ c.receive_data(f.serialize())
+
+ # Send in trailers.
+ c.clear_outbound_data_buffer()
+ trailers = [('content-length', '0')]
+ f = frame_factory.build_headers_frame(
+ trailers,
+ flags=[],
+ )
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=1, error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_can_send_trailers(self, frame_factory):
+ """
+ When a second set of headers are sent, they are properly trailers.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_headers_frame(self.example_request_headers)
+ c.receive_data(f.serialize())
+
+ # Send headers.
+ c.clear_outbound_data_buffer()
+ c.send_headers(1, self.example_response_headers)
+
+ # Now send trailers.
+ trailers = [('content-length', '0')]
+ c.send_headers(1, trailers, end_stream=True)
+
+ frame_factory.refresh_encoder()
+ f1 = frame_factory.build_headers_frame(
+ self.example_response_headers,
+ )
+ f2 = frame_factory.build_headers_frame(
+ trailers,
+ flags=['END_STREAM'],
+ )
+ assert c.data_to_send() == f1.serialize() + f2.serialize()
+
+ def test_trailers_must_have_end_stream(self, frame_factory):
+ """
+ A set of trailers must carry the END_STREAM flag.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_headers_frame(self.example_request_headers)
+ c.receive_data(f.serialize())
+
+ # Send headers.
+ c.send_headers(1, self.example_response_headers)
+
+ # Now send trailers.
+ trailers = [('content-length', '0')]
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.send_headers(1, trailers)
+
+ @pytest.mark.parametrize("frame_id", range(12, 256))
+ def test_unknown_frames_are_ignored(self, frame_factory, frame_id):
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_data_frame(data=b'abcdefghtdst')
+ f.type = frame_id
+
+ events = c.receive_data(f.serialize())
+ assert not c.data_to_send()
+ assert len(events) == 1
+ assert isinstance(events[0], h2.events.UnknownFrameReceived)
+ assert isinstance(events[0].frame, hyperframe.frame.ExtensionFrame)
+
+ def test_can_send_goaway_repeatedly(self, frame_factory):
+ """
+ We can send a GOAWAY frame as many times as we like.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ c.close_connection()
+ c.close_connection()
+ c.close_connection()
+
+ f = frame_factory.build_goaway_frame(last_stream_id=0)
+
+ assert c.data_to_send() == (f.serialize() * 3)
+
+ def test_receiving_goaway_frame(self, frame_factory):
+ """
+ Receiving a GOAWAY frame causes a ConnectionTerminated event to be
+ fired and transitions the connection to the CLOSED state, and clears
+ the outbound data buffer.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_goaway_frame(
+ last_stream_id=5, error_code=h2.errors.ErrorCodes.SETTINGS_TIMEOUT
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.ConnectionTerminated)
+ assert event.error_code == h2.errors.ErrorCodes.SETTINGS_TIMEOUT
+ assert isinstance(event.error_code, h2.errors.ErrorCodes)
+ assert event.last_stream_id == 5
+ assert event.additional_data is None
+ assert c.state_machine.state == h2.connection.ConnectionState.CLOSED
+
+ assert not c.data_to_send()
+
+ def test_receiving_multiple_goaway_frames(self, frame_factory):
+ """
+ Multiple GOAWAY frames can be received at once, and are allowed. Each
+ one fires a ConnectionTerminated event.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_goaway_frame(last_stream_id=0)
+ events = c.receive_data(f.serialize() * 3)
+
+ assert len(events) == 3
+ assert all(
+ isinstance(event, h2.events.ConnectionTerminated)
+ for event in events
+ )
+
+ def test_receiving_goaway_frame_with_additional_data(self, frame_factory):
+ """
+ GOAWAY frame can contain additional data,
+ it should be available via ConnectionTerminated event.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ additional_data = b'debug data'
+ f = frame_factory.build_goaway_frame(last_stream_id=0,
+ additional_data=additional_data)
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.ConnectionTerminated)
+ assert event.additional_data == additional_data
+
+ def test_receiving_goaway_frame_with_unknown_error(self, frame_factory):
+ """
+ Receiving a GOAWAY frame with an unknown error code behaves exactly the
+ same as receiving one we know about, but the code is reported as an
+ integer instead of as an ErrorCodes.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_goaway_frame(
+ last_stream_id=5, error_code=0xFA
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.ConnectionTerminated)
+ assert event.error_code == 250
+ assert not isinstance(event.error_code, h2.errors.ErrorCodes)
+ assert event.last_stream_id == 5
+ assert event.additional_data is None
+ assert c.state_machine.state == h2.connection.ConnectionState.CLOSED
+
+ assert not c.data_to_send()
+
+ def test_cookies_are_joined(self, frame_factory):
+ """
+ RFC 7540 Section 8.1.2.5 requires that we join multiple Cookie headers
+ in a header block together.
+ """
+ # This is a moderately varied set of cookie headers: some combined,
+ # some split.
+ cookie_headers = [
+ ('cookie',
+ 'username=John Doe; expires=Thu, 18 Dec 2013 12:00:00 UTC'),
+ ('cookie', 'path=1'),
+ ('cookie', 'test1=val1; test2=val2')
+ ]
+ expected = (
+ 'username=John Doe; expires=Thu, 18 Dec 2013 12:00:00 UTC; '
+ 'path=1; test1=val1; test2=val2'
+ )
+
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers + cookie_headers
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ e = events[0]
+
+ cookie_fields = [(n, v) for n, v in e.headers if n == 'cookie']
+ assert len(cookie_fields) == 1
+
+ _, v = cookie_fields[0]
+ assert v == expected
+
+ def test_cookies_arent_joined_without_normalization(self, frame_factory):
+ """
+ If inbound header normalization is disabled, cookie headers aren't
+ joined.
+ """
+ # This is a moderately varied set of cookie headers: some combined,
+ # some split.
+ cookie_headers = [
+ ('cookie',
+ 'username=John Doe; expires=Thu, 18 Dec 2013 12:00:00 UTC'),
+ ('cookie', 'path=1'),
+ ('cookie', 'test1=val1; test2=val2')
+ ]
+
+ config = h2.config.H2Configuration(
+ client_side=False,
+ normalize_inbound_headers=False,
+ header_encoding='utf-8'
+ )
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers + cookie_headers
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ e = events[0]
+
+ received_cookies = [(n, v) for n, v in e.headers if n == 'cookie']
+ assert len(received_cookies) == 3
+ assert cookie_headers == received_cookies
+
+ def test_stream_repr(self):
+ """
+ Ensure stream string representation is appropriate.
+ """
+ s = h2.stream.H2Stream(4, None, 12, 14)
+ assert repr(s) == "<H2Stream id:4 state:<StreamState.IDLE: 0>>"
+
+
+def sanity_check_data_frame(data_frame,
+ expected_flow_controlled_length,
+ expect_padded_flag,
+ expected_data_frame_pad_length):
+ """
+ ``data_frame`` is a frame of type ``hyperframe.frame.DataFrame``,
+ and the ``flags`` and ``flow_controlled_length`` of ``data_frame``
+ match expectations.
+ """
+
+ assert isinstance(data_frame, hyperframe.frame.DataFrame)
+
+ assert data_frame.flow_controlled_length == expected_flow_controlled_length
+
+ if expect_padded_flag:
+ assert 'PADDED' in data_frame.flags
+ else:
+ assert 'PADDED' not in data_frame.flags
+
+ assert data_frame.pad_length == expected_data_frame_pad_length
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_closed_streams.py b/testing/web-platform/tests/tools/third_party/h2/test/test_closed_streams.py
new file mode 100644
index 0000000000..631a66787a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_closed_streams.py
@@ -0,0 +1,555 @@
+# -*- coding: utf-8 -*-
+"""
+test_closed_streams
+~~~~~~~~~~~~~~~~~~~
+
+Tests that we handle closed streams correctly.
+"""
+import pytest
+
+import h2.config
+import h2.connection
+import h2.errors
+import h2.events
+import h2.exceptions
+
+
+class TestClosedStreams(object):
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ]
+ example_response_headers = [
+ (':status', '200'),
+ ('server', 'fake-serv/0.1.0')
+ ]
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ def test_can_receive_multiple_rst_stream_frames(self, frame_factory):
+ """
+ Multiple RST_STREAM frames can be received, either at once or well
+ after one another. Only the first fires an event.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+
+ f = frame_factory.build_rst_stream_frame(stream_id=1)
+ events = c.receive_data(f.serialize() * 3)
+
+ # Force an iteration over all the streams to remove them.
+ c.open_outbound_streams
+
+ # Receive more data.
+ events += c.receive_data(f.serialize() * 3)
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.StreamReset)
+
+ def test_receiving_low_stream_id_causes_goaway(self, frame_factory):
+ """
+ The remote peer creating a stream with a lower ID than one we've seen
+ causes a GOAWAY frame.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.initiate_connection()
+
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers,
+ stream_id=3,
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers,
+ stream_id=1,
+ )
+
+ with pytest.raises(h2.exceptions.StreamIDTooLowError) as e:
+ c.receive_data(f.serialize())
+
+ assert e.value.stream_id == 1
+ assert e.value.max_stream_id == 3
+
+ f = frame_factory.build_goaway_frame(
+ last_stream_id=3,
+ error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
+ )
+ assert c.data_to_send() == f.serialize()
+
+ def test_closed_stream_not_present_in_streams_dict(self, frame_factory):
+ """
+ When streams have been closed, they get removed from the streams
+ dictionary the next time we count the open streams.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.initiate_connection()
+
+ f = frame_factory.build_headers_frame(self.example_request_headers)
+ c.receive_data(f.serialize())
+ c.push_stream(1, 2, self.example_request_headers)
+ c.reset_stream(1)
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_rst_stream_frame(stream_id=2)
+ c.receive_data(f.serialize())
+
+ # Force a count of the streams.
+ assert not c.open_outbound_streams
+
+ # The streams dictionary should be empty.
+ assert not c.streams
+
+ def test_receive_rst_stream_on_closed_stream(self, frame_factory):
+ """
+ RST_STREAM frame should be ignored if stream is in a closed state.
+ See RFC 7540 Section 5.1 (closed state)
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ # Client sends request
+ c.send_headers(1, self.example_request_headers)
+
+ # Some time passes and client sends DATA frame and closes stream,
+ # so it is in a half-closed state
+ c.send_data(1, b'some data', end_stream=True)
+
+ # Server received HEADERS frame but DATA frame is still on the way.
+ # Stream is in open state on the server-side. In this state server is
+ # allowed to end stream and reset it - this trick helps immediately
+ # close stream on the server-side.
+ headers_frame = frame_factory.build_headers_frame(
+ [(':status', '200')],
+ flags=['END_STREAM'],
+ stream_id=1,
+ )
+ events = c.receive_data(headers_frame.serialize())
+ assert len(events) == 2
+ response_received, stream_ended = events
+ assert isinstance(response_received, h2.events.ResponseReceived)
+ assert isinstance(stream_ended, h2.events.StreamEnded)
+
+ rst_stream_frame = frame_factory.build_rst_stream_frame(stream_id=1)
+ events = c.receive_data(rst_stream_frame.serialize())
+ assert not events
+
+ def test_receive_window_update_on_closed_stream(self, frame_factory):
+ """
+ WINDOW_UPDATE frame should be ignored if stream is in a closed state.
+ See RFC 7540 Section 5.1 (closed state)
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ # Client sends request
+ c.send_headers(1, self.example_request_headers)
+
+ # Some time passes and client sends DATA frame and closes stream,
+ # so it is in a half-closed state
+ c.send_data(1, b'some data', end_stream=True)
+
+ # Server received HEADERS frame but DATA frame is still on the way.
+ # Stream is in open state on the server-side. In this state server is
+ # allowed to end stream and after that acknowledge received data by
+ # sending WINDOW_UPDATE frames.
+ headers_frame = frame_factory.build_headers_frame(
+ [(':status', '200')],
+ flags=['END_STREAM'],
+ stream_id=1,
+ )
+ events = c.receive_data(headers_frame.serialize())
+ assert len(events) == 2
+ response_received, stream_ended = events
+ assert isinstance(response_received, h2.events.ResponseReceived)
+ assert isinstance(stream_ended, h2.events.StreamEnded)
+
+ window_update_frame = frame_factory.build_window_update_frame(
+ stream_id=1,
+ increment=1,
+ )
+ events = c.receive_data(window_update_frame.serialize())
+ assert not events
+
+
+class TestStreamsClosedByEndStream(object):
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ]
+ example_response_headers = [
+ (':status', '200'),
+ ('server', 'fake-serv/0.1.0')
+ ]
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ @pytest.mark.parametrize(
+ "frame",
+ [
+ lambda self, ff: ff.build_headers_frame(
+ self.example_request_headers, flags=['END_STREAM']),
+ lambda self, ff: ff.build_headers_frame(
+ self.example_request_headers),
+ ]
+ )
+ @pytest.mark.parametrize("clear_streams", [True, False])
+ def test_frames_after_recv_end_will_error(self,
+ frame_factory,
+ frame,
+ clear_streams):
+ """
+ A stream that is closed by receiving END_STREAM raises
+ ProtocolError when it receives an unexpected frame.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.initiate_connection()
+
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers, flags=['END_STREAM']
+ )
+ c.receive_data(f.serialize())
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_response_headers,
+ end_stream=True
+ )
+
+ if clear_streams:
+ # Call open_inbound_streams to force the connection to clean
+ # closed streams.
+ c.open_inbound_streams
+
+ c.clear_outbound_data_buffer()
+
+ f = frame(self, frame_factory)
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ f = frame_factory.build_goaway_frame(
+ last_stream_id=1,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ )
+ assert c.data_to_send() == f.serialize()
+
+ @pytest.mark.parametrize(
+ "frame",
+ [
+ lambda self, ff: ff.build_headers_frame(
+ self.example_response_headers, flags=['END_STREAM']),
+ lambda self, ff: ff.build_headers_frame(
+ self.example_response_headers),
+ ]
+ )
+ @pytest.mark.parametrize("clear_streams", [True, False])
+ def test_frames_after_send_end_will_error(self,
+ frame_factory,
+ frame,
+ clear_streams):
+ """
+ A stream that is closed by sending END_STREAM raises
+ ProtocolError when it receives an unexpected frame.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers,
+ end_stream=True)
+
+ f = frame_factory.build_headers_frame(
+ self.example_response_headers, flags=['END_STREAM']
+ )
+ c.receive_data(f.serialize())
+
+ if clear_streams:
+ # Call open_outbound_streams to force the connection to clean
+ # closed streams.
+ c.open_outbound_streams
+
+ c.clear_outbound_data_buffer()
+
+ f = frame(self, frame_factory)
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ f = frame_factory.build_goaway_frame(
+ last_stream_id=0,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ )
+ assert c.data_to_send() == f.serialize()
+
+ @pytest.mark.parametrize(
+ "frame",
+ [
+ lambda self, ff: ff.build_window_update_frame(1, 1),
+ lambda self, ff: ff.build_rst_stream_frame(1)
+ ]
+ )
+ def test_frames_after_send_end_will_be_ignored(self,
+ frame_factory,
+ frame):
+ """
+ A stream that is closed by sending END_STREAM will raise
+ ProtocolError when received unexpected frame.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.initiate_connection()
+
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers, flags=['END_STREAM']
+ )
+ c.receive_data(f.serialize())
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_response_headers,
+ end_stream=True
+ )
+
+ c.clear_outbound_data_buffer()
+
+ f = frame(self, frame_factory)
+ events = c.receive_data(f.serialize())
+
+ assert not events
+
+
+class TestStreamsClosedByRstStream(object):
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ]
+ example_response_headers = [
+ (':status', '200'),
+ ('server', 'fake-serv/0.1.0')
+ ]
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ @pytest.mark.parametrize(
+ "frame",
+ [
+ lambda self, ff: ff.build_headers_frame(
+ self.example_request_headers),
+ lambda self, ff: ff.build_headers_frame(
+ self.example_request_headers, flags=['END_STREAM']),
+ ]
+ )
+ def test_resets_further_frames_after_recv_reset(self,
+ frame_factory,
+ frame):
+ """
+ A stream that is closed by receive RST_STREAM can receive further
+ frames: it simply sends RST_STREAM for it, and additionally
+ WINDOW_UPDATE for DATA frames.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.initiate_connection()
+
+ header_frame = frame_factory.build_headers_frame(
+ self.example_request_headers, flags=['END_STREAM']
+ )
+ c.receive_data(header_frame.serialize())
+
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_response_headers,
+ end_stream=False
+ )
+
+ rst_frame = frame_factory.build_rst_stream_frame(
+ 1, h2.errors.ErrorCodes.STREAM_CLOSED
+ )
+ c.receive_data(rst_frame.serialize())
+ c.clear_outbound_data_buffer()
+
+ f = frame(self, frame_factory)
+ events = c.receive_data(f.serialize())
+
+ rst_frame = frame_factory.build_rst_stream_frame(
+ 1, h2.errors.ErrorCodes.STREAM_CLOSED
+ )
+ assert not events
+ assert c.data_to_send() == rst_frame.serialize()
+
+ events = c.receive_data(f.serialize() * 3)
+ assert not events
+ assert c.data_to_send() == rst_frame.serialize() * 3
+
+ # Iterate over the streams to make sure it's gone, then confirm the
+ # behaviour is unchanged.
+ c.open_outbound_streams
+
+ events = c.receive_data(f.serialize() * 3)
+ assert not events
+ assert c.data_to_send() == rst_frame.serialize() * 3
+
+ def test_resets_further_data_frames_after_recv_reset(self,
+ frame_factory):
+ """
+ A stream that is closed by receive RST_STREAM can receive further
+ DATA frames: it simply sends WINDOW_UPDATE for the connection flow
+ window, and RST_STREAM for the stream.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.initiate_connection()
+
+ header_frame = frame_factory.build_headers_frame(
+ self.example_request_headers, flags=['END_STREAM']
+ )
+ c.receive_data(header_frame.serialize())
+
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_response_headers,
+ end_stream=False
+ )
+
+ rst_frame = frame_factory.build_rst_stream_frame(
+ 1, h2.errors.ErrorCodes.STREAM_CLOSED
+ )
+ c.receive_data(rst_frame.serialize())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_data_frame(
+ data=b'some data'
+ )
+
+ events = c.receive_data(f.serialize())
+ assert not events
+
+ expected = frame_factory.build_rst_stream_frame(
+ stream_id=1,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ ).serialize()
+ assert c.data_to_send() == expected
+
+ events = c.receive_data(f.serialize() * 3)
+ assert not events
+ assert c.data_to_send() == expected * 3
+
+ # Iterate over the streams to make sure it's gone, then confirm the
+ # behaviour is unchanged.
+ c.open_outbound_streams
+
+ events = c.receive_data(f.serialize() * 3)
+ assert not events
+ assert c.data_to_send() == expected * 3
+
+ @pytest.mark.parametrize(
+ "frame",
+ [
+ lambda self, ff: ff.build_headers_frame(
+ self.example_request_headers),
+ lambda self, ff: ff.build_headers_frame(
+ self.example_request_headers, flags=['END_STREAM']),
+ ]
+ )
+ def test_resets_further_frames_after_send_reset(self,
+ frame_factory,
+ frame):
+ """
+ A stream that is closed by sent RST_STREAM can receive further frames:
+ it simply sends RST_STREAM for it.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.initiate_connection()
+
+ header_frame = frame_factory.build_headers_frame(
+ self.example_request_headers, flags=['END_STREAM']
+ )
+ c.receive_data(header_frame.serialize())
+
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_response_headers,
+ end_stream=False
+ )
+
+ c.reset_stream(1, h2.errors.ErrorCodes.INTERNAL_ERROR)
+
+ rst_frame = frame_factory.build_rst_stream_frame(
+ 1, h2.errors.ErrorCodes.STREAM_CLOSED
+ )
+ c.clear_outbound_data_buffer()
+
+ f = frame(self, frame_factory)
+ events = c.receive_data(f.serialize())
+
+ rst_frame = frame_factory.build_rst_stream_frame(
+ 1, h2.errors.ErrorCodes.STREAM_CLOSED
+ )
+ assert not events
+ assert c.data_to_send() == rst_frame.serialize()
+
+ events = c.receive_data(f.serialize() * 3)
+ assert not events
+ assert c.data_to_send() == rst_frame.serialize() * 3
+
+ # Iterate over the streams to make sure it's gone, then confirm the
+ # behaviour is unchanged.
+ c.open_outbound_streams
+
+ events = c.receive_data(f.serialize() * 3)
+ assert not events
+ assert c.data_to_send() == rst_frame.serialize() * 3
+
+ def test_resets_further_data_frames_after_send_reset(self,
+ frame_factory):
+ """
+ A stream that is closed by sent RST_STREAM can receive further
+ data frames: it simply sends WINDOW_UPDATE and RST_STREAM for it.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.initiate_connection()
+
+ header_frame = frame_factory.build_headers_frame(
+ self.example_request_headers, flags=['END_STREAM']
+ )
+ c.receive_data(header_frame.serialize())
+
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_response_headers,
+ end_stream=False
+ )
+
+ c.reset_stream(1, h2.errors.ErrorCodes.INTERNAL_ERROR)
+
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_data_frame(
+ data=b'some data'
+ )
+ events = c.receive_data(f.serialize())
+ assert not events
+ expected = frame_factory.build_rst_stream_frame(
+ stream_id=1,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ ).serialize()
+ assert c.data_to_send() == expected
+
+ events = c.receive_data(f.serialize() * 3)
+ assert not events
+ assert c.data_to_send() == expected * 3
+
+ # Iterate over the streams to make sure it's gone, then confirm the
+ # behaviour is unchanged.
+ c.open_outbound_streams
+
+ events = c.receive_data(f.serialize() * 3)
+ assert not events
+ assert c.data_to_send() == expected * 3
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_complex_logic.py b/testing/web-platform/tests/tools/third_party/h2/test/test_complex_logic.py
new file mode 100644
index 0000000000..ff90bb8bf2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_complex_logic.py
@@ -0,0 +1,586 @@
+# -*- coding: utf-8 -*-
+"""
+test_complex_logic
+~~~~~~~~~~~~~~~~
+
+More complex tests that try to do more.
+
+Certain tests don't really eliminate incorrect behaviour unless they do quite
+a bit. These tests should live here, to keep the pain in once place rather than
+hide it in the other parts of the test suite.
+"""
+import pytest
+
+import h2
+import h2.config
+import h2.connection
+
+
+class TestComplexClient(object):
+ """
+ Complex tests for client-side stacks.
+ """
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ]
+ example_response_headers = [
+ (':status', '200'),
+ ('server', 'fake-serv/0.1.0')
+ ]
+
+ def test_correctly_count_server_streams(self, frame_factory):
+ """
+ We correctly count the number of server streams, both inbound and
+ outbound.
+ """
+ # This test makes no sense unless you do both inbound and outbound,
+ # because it's important to confirm that we count them correctly.
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ expected_inbound_streams = expected_outbound_streams = 0
+
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ for stream_id in range(1, 15, 2):
+ # Open an outbound stream
+ c.send_headers(stream_id, self.example_request_headers)
+ expected_outbound_streams += 1
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ # Receive a pushed stream (to create an inbound one). This doesn't
+ # open until we also receive headers.
+ f = frame_factory.build_push_promise_frame(
+ stream_id=stream_id,
+ promised_stream_id=stream_id+1,
+ headers=self.example_request_headers,
+ )
+ c.receive_data(f.serialize())
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ f = frame_factory.build_headers_frame(
+ stream_id=stream_id+1,
+ headers=self.example_response_headers,
+ )
+ c.receive_data(f.serialize())
+ expected_inbound_streams += 1
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ for stream_id in range(13, 0, -2):
+ # Close an outbound stream.
+ c.end_stream(stream_id)
+
+ # Stream doesn't close until both sides close it.
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ f = frame_factory.build_headers_frame(
+ stream_id=stream_id,
+ headers=self.example_response_headers,
+ flags=['END_STREAM'],
+ )
+ c.receive_data(f.serialize())
+ expected_outbound_streams -= 1
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ # Pushed streams can only be closed remotely.
+ f = frame_factory.build_data_frame(
+ stream_id=stream_id+1,
+ data=b'the content',
+ flags=['END_STREAM'],
+ )
+ c.receive_data(f.serialize())
+ expected_inbound_streams -= 1
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ assert c.open_inbound_streams == 0
+ assert c.open_outbound_streams == 0
+
+
+class TestComplexServer(object):
+ """
+ Complex tests for server-side stacks.
+ """
+ example_request_headers = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'GET'),
+ ]
+ example_response_headers = [
+ (b':status', b'200'),
+ (b'server', b'fake-serv/0.1.0')
+ ]
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ def test_correctly_count_server_streams(self, frame_factory):
+ """
+ We correctly count the number of server streams, both inbound and
+ outbound.
+ """
+ # This test makes no sense unless you do both inbound and outbound,
+ # because it's important to confirm that we count them correctly.
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ expected_inbound_streams = expected_outbound_streams = 0
+
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ for stream_id in range(1, 15, 2):
+ # Receive an inbound stream.
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ stream_id=stream_id,
+ )
+ c.receive_data(f.serialize())
+ expected_inbound_streams += 1
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ # Push a stream (to create a outbound one). This doesn't open
+ # until we send our response headers.
+ c.push_stream(stream_id, stream_id+1, self.example_request_headers)
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ c.send_headers(stream_id+1, self.example_response_headers)
+ expected_outbound_streams += 1
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ for stream_id in range(13, 0, -2):
+ # Close an inbound stream.
+ f = frame_factory.build_data_frame(
+ data=b'',
+ flags=['END_STREAM'],
+ stream_id=stream_id,
+ )
+ c.receive_data(f.serialize())
+
+ # Stream doesn't close until both sides close it.
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ c.send_data(stream_id, b'', end_stream=True)
+ expected_inbound_streams -= 1
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ # Pushed streams, however, we can close ourselves.
+ c.send_data(
+ stream_id=stream_id+1,
+ data=b'',
+ end_stream=True,
+ )
+ expected_outbound_streams -= 1
+ assert c.open_inbound_streams == expected_inbound_streams
+ assert c.open_outbound_streams == expected_outbound_streams
+
+ assert c.open_inbound_streams == 0
+ assert c.open_outbound_streams == 0
+
+
+class TestContinuationFrames(object):
+ """
+ Tests for the relatively complex CONTINUATION frame logic.
+ """
+ example_request_headers = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'GET'),
+ ]
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ def _build_continuation_sequence(self, headers, block_size, frame_factory):
+ f = frame_factory.build_headers_frame(headers)
+ header_data = f.data
+ chunks = [
+ header_data[x:x+block_size]
+ for x in range(0, len(header_data), block_size)
+ ]
+ f.data = chunks.pop(0)
+ frames = [
+ frame_factory.build_continuation_frame(c) for c in chunks
+ ]
+ f.flags = {'END_STREAM'}
+ frames[-1].flags.add('END_HEADERS')
+ frames.insert(0, f)
+ return frames
+
+ def test_continuation_frame_basic(self, frame_factory):
+ """
+ Test that we correctly decode a header block split across continuation
+ frames.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ frames = self._build_continuation_sequence(
+ headers=self.example_request_headers,
+ block_size=5,
+ frame_factory=frame_factory,
+ )
+ data = b''.join(f.serialize() for f in frames)
+ events = c.receive_data(data)
+
+ assert len(events) == 2
+ first_event, second_event = events
+
+ assert isinstance(first_event, h2.events.RequestReceived)
+ assert first_event.headers == self.example_request_headers
+ assert first_event.stream_id == 1
+
+ assert isinstance(second_event, h2.events.StreamEnded)
+ assert second_event.stream_id == 1
+
+ @pytest.mark.parametrize('stream_id', [3, 1])
+ def test_continuation_cannot_interleave_headers(self,
+ frame_factory,
+ stream_id):
+ """
+ We cannot interleave a new headers block with a CONTINUATION sequence.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ frames = self._build_continuation_sequence(
+ headers=self.example_request_headers,
+ block_size=5,
+ frame_factory=frame_factory,
+ )
+ assert len(frames) > 2 # This is mostly defensive.
+
+ bogus_frame = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ stream_id=stream_id,
+ flags=['END_STREAM'],
+ )
+ frames.insert(len(frames) - 2, bogus_frame)
+ data = b''.join(f.serialize() for f in frames)
+
+ with pytest.raises(h2.exceptions.ProtocolError) as e:
+ c.receive_data(data)
+
+ assert "invalid frame" in str(e.value).lower()
+
+ def test_continuation_cannot_interleave_data(self, frame_factory):
+ """
+ We cannot interleave a data frame with a CONTINUATION sequence.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ frames = self._build_continuation_sequence(
+ headers=self.example_request_headers,
+ block_size=5,
+ frame_factory=frame_factory,
+ )
+ assert len(frames) > 2 # This is mostly defensive.
+
+ bogus_frame = frame_factory.build_data_frame(
+ data=b'hello',
+ stream_id=1,
+ )
+ frames.insert(len(frames) - 2, bogus_frame)
+ data = b''.join(f.serialize() for f in frames)
+
+ with pytest.raises(h2.exceptions.ProtocolError) as e:
+ c.receive_data(data)
+
+ assert "invalid frame" in str(e.value).lower()
+
+ def test_continuation_cannot_interleave_unknown_frame(self, frame_factory):
+ """
+ We cannot interleave an unknown frame with a CONTINUATION sequence.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ frames = self._build_continuation_sequence(
+ headers=self.example_request_headers,
+ block_size=5,
+ frame_factory=frame_factory,
+ )
+ assert len(frames) > 2 # This is mostly defensive.
+
+ bogus_frame = frame_factory.build_data_frame(
+ data=b'hello',
+ stream_id=1,
+ )
+ bogus_frame.type = 88
+ frames.insert(len(frames) - 2, bogus_frame)
+ data = b''.join(f.serialize() for f in frames)
+
+ with pytest.raises(h2.exceptions.ProtocolError) as e:
+ c.receive_data(data)
+
+ assert "invalid frame" in str(e.value).lower()
+
+ def test_continuation_frame_multiple_blocks(self, frame_factory):
+ """
+ Test that we correctly decode several header blocks split across
+ continuation frames.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ for stream_id in range(1, 7, 2):
+ frames = self._build_continuation_sequence(
+ headers=self.example_request_headers,
+ block_size=2,
+ frame_factory=frame_factory,
+ )
+ for frame in frames:
+ frame.stream_id = stream_id
+
+ data = b''.join(f.serialize() for f in frames)
+ events = c.receive_data(data)
+
+ assert len(events) == 2
+ first_event, second_event = events
+
+ assert isinstance(first_event, h2.events.RequestReceived)
+ assert first_event.headers == self.example_request_headers
+ assert first_event.stream_id == stream_id
+
+ assert isinstance(second_event, h2.events.StreamEnded)
+ assert second_event.stream_id == stream_id
+
+
+class TestContinuationFramesPushPromise(object):
+ """
+ Tests for the relatively complex CONTINUATION frame logic working with
+ PUSH_PROMISE frames.
+ """
+ example_request_headers = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'GET'),
+ ]
+ example_response_headers = [
+ (b':status', b'200'),
+ (b'server', b'fake-serv/0.1.0')
+ ]
+
+ def _build_continuation_sequence(self, headers, block_size, frame_factory):
+ f = frame_factory.build_push_promise_frame(
+ stream_id=1, promised_stream_id=2, headers=headers
+ )
+ header_data = f.data
+ chunks = [
+ header_data[x:x+block_size]
+ for x in range(0, len(header_data), block_size)
+ ]
+ f.data = chunks.pop(0)
+ frames = [
+ frame_factory.build_continuation_frame(c) for c in chunks
+ ]
+ f.flags = {'END_STREAM'}
+ frames[-1].flags.add('END_HEADERS')
+ frames.insert(0, f)
+ return frames
+
+ def test_continuation_frame_basic_push_promise(self, frame_factory):
+ """
+ Test that we correctly decode a header block split across continuation
+ frames when that header block is initiated with a PUSH_PROMISE.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ frames = self._build_continuation_sequence(
+ headers=self.example_request_headers,
+ block_size=5,
+ frame_factory=frame_factory,
+ )
+ data = b''.join(f.serialize() for f in frames)
+ events = c.receive_data(data)
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.PushedStreamReceived)
+ assert event.headers == self.example_request_headers
+ assert event.parent_stream_id == 1
+ assert event.pushed_stream_id == 2
+
+ @pytest.mark.parametrize('stream_id', [3, 1, 2])
+ def test_continuation_cannot_interleave_headers_pp(self,
+ frame_factory,
+ stream_id):
+ """
+ We cannot interleave a new headers block with a CONTINUATION sequence
+ when the headers block is based on a PUSH_PROMISE frame.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ frames = self._build_continuation_sequence(
+ headers=self.example_request_headers,
+ block_size=5,
+ frame_factory=frame_factory,
+ )
+ assert len(frames) > 2 # This is mostly defensive.
+
+ bogus_frame = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ stream_id=stream_id,
+ flags=['END_STREAM'],
+ )
+ frames.insert(len(frames) - 2, bogus_frame)
+ data = b''.join(f.serialize() for f in frames)
+
+ with pytest.raises(h2.exceptions.ProtocolError) as e:
+ c.receive_data(data)
+
+ assert "invalid frame" in str(e.value).lower()
+
+ def test_continuation_cannot_interleave_data(self, frame_factory):
+ """
+ We cannot interleave a data frame with a CONTINUATION sequence when
+ that sequence began with a PUSH_PROMISE frame.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ frames = self._build_continuation_sequence(
+ headers=self.example_request_headers,
+ block_size=5,
+ frame_factory=frame_factory,
+ )
+ assert len(frames) > 2 # This is mostly defensive.
+
+ bogus_frame = frame_factory.build_data_frame(
+ data=b'hello',
+ stream_id=1,
+ )
+ frames.insert(len(frames) - 2, bogus_frame)
+ data = b''.join(f.serialize() for f in frames)
+
+ with pytest.raises(h2.exceptions.ProtocolError) as e:
+ c.receive_data(data)
+
+ assert "invalid frame" in str(e.value).lower()
+
+ def test_continuation_cannot_interleave_unknown_frame(self, frame_factory):
+ """
+ We cannot interleave an unknown frame with a CONTINUATION sequence when
+ that sequence began with a PUSH_PROMISE frame.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ frames = self._build_continuation_sequence(
+ headers=self.example_request_headers,
+ block_size=5,
+ frame_factory=frame_factory,
+ )
+ assert len(frames) > 2 # This is mostly defensive.
+
+ bogus_frame = frame_factory.build_data_frame(
+ data=b'hello',
+ stream_id=1,
+ )
+ bogus_frame.type = 88
+ frames.insert(len(frames) - 2, bogus_frame)
+ data = b''.join(f.serialize() for f in frames)
+
+ with pytest.raises(h2.exceptions.ProtocolError) as e:
+ c.receive_data(data)
+
+ assert "invalid frame" in str(e.value).lower()
+
+ @pytest.mark.parametrize('evict', [True, False])
+ def test_stream_remotely_closed_disallows_push_promise(self,
+ evict,
+ frame_factory):
+ """
+ Streams closed normally by the remote peer disallow PUSH_PROMISE
+ frames, and cause a GOAWAY.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_request_headers,
+ end_stream=True
+ )
+
+ f = frame_factory.build_headers_frame(
+ stream_id=1,
+ headers=self.example_response_headers,
+ flags=['END_STREAM']
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ if evict:
+ # This is annoyingly stateful, but enumerating the list of open
+ # streams will force us to flush state.
+ assert not c.open_outbound_streams
+
+ f = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=self.example_request_headers,
+ )
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ f = frame_factory.build_goaway_frame(
+ last_stream_id=0,
+ error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
+ )
+ assert c.data_to_send() == f.serialize()
+
+ def test_continuation_frame_multiple_push_promise(self, frame_factory):
+ """
+ Test that we correctly decode header blocks split across continuation
+ frames when those header block is initiated with a PUSH_PROMISE, for
+ more than one pushed stream.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ for promised_stream_id in range(2, 8, 2):
+ frames = self._build_continuation_sequence(
+ headers=self.example_request_headers,
+ block_size=2,
+ frame_factory=frame_factory,
+ )
+ frames[0].promised_stream_id = promised_stream_id
+ data = b''.join(f.serialize() for f in frames)
+ events = c.receive_data(data)
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.PushedStreamReceived)
+ assert event.headers == self.example_request_headers
+ assert event.parent_stream_id == 1
+ assert event.pushed_stream_id == promised_stream_id
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_config.py b/testing/web-platform/tests/tools/third_party/h2/test/test_config.py
new file mode 100644
index 0000000000..8eb7fdc862
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_config.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+"""
+test_config
+~~~~~~~~~~~
+
+Test the configuration object.
+"""
+import logging
+import pytest
+
+import h2.config
+
+
+class TestH2Config(object):
+ """
+ Tests of the H2 config object.
+ """
+ def test_defaults(self):
+ """
+ The default values of the HTTP/2 config object are sensible.
+ """
+ config = h2.config.H2Configuration()
+ assert config.client_side
+ assert config.header_encoding is None
+ assert isinstance(config.logger, h2.config.DummyLogger)
+
+ boolean_config_options = [
+ 'client_side',
+ 'validate_outbound_headers',
+ 'normalize_outbound_headers',
+ 'validate_inbound_headers',
+ 'normalize_inbound_headers',
+ ]
+
+ @pytest.mark.parametrize('option_name', boolean_config_options)
+ @pytest.mark.parametrize('value', [None, 'False', 1])
+ def test_boolean_config_options_reject_non_bools_init(
+ self, option_name, value
+ ):
+ """
+ The boolean config options raise an error if you try to set a value
+ that isn't a boolean via the initializer.
+ """
+ with pytest.raises(ValueError):
+ h2.config.H2Configuration(**{option_name: value})
+
+ @pytest.mark.parametrize('option_name', boolean_config_options)
+ @pytest.mark.parametrize('value', [None, 'False', 1])
+ def test_boolean_config_options_reject_non_bools_attr(
+ self, option_name, value
+ ):
+ """
+ The boolean config options raise an error if you try to set a value
+ that isn't a boolean via attribute setter.
+ """
+ config = h2.config.H2Configuration()
+ with pytest.raises(ValueError):
+ setattr(config, option_name, value)
+
+ @pytest.mark.parametrize('option_name', boolean_config_options)
+ @pytest.mark.parametrize('value', [True, False])
+ def test_boolean_config_option_is_reflected_init(self, option_name, value):
+ """
+ The value of the boolean config options, when set, is reflected
+ in the value via the initializer.
+ """
+ config = h2.config.H2Configuration(**{option_name: value})
+ assert getattr(config, option_name) == value
+
+ @pytest.mark.parametrize('option_name', boolean_config_options)
+ @pytest.mark.parametrize('value', [True, False])
+ def test_boolean_config_option_is_reflected_attr(self, option_name, value):
+ """
+ The value of the boolean config options, when set, is reflected
+ in the value via attribute setter.
+ """
+ config = h2.config.H2Configuration()
+ setattr(config, option_name, value)
+ assert getattr(config, option_name) == value
+
+ @pytest.mark.parametrize('header_encoding', [True, 1, object()])
+ def test_header_encoding_must_be_false_str_none_init(
+ self, header_encoding
+ ):
+ """
+ The value of the ``header_encoding`` setting must be False, a string,
+ or None via the initializer.
+ """
+ with pytest.raises(ValueError):
+ h2.config.H2Configuration(header_encoding=header_encoding)
+
+ @pytest.mark.parametrize('header_encoding', [True, 1, object()])
+ def test_header_encoding_must_be_false_str_none_attr(
+ self, header_encoding
+ ):
+ """
+ The value of the ``header_encoding`` setting must be False, a string,
+ or None via attribute setter.
+ """
+ config = h2.config.H2Configuration()
+ with pytest.raises(ValueError):
+ config.header_encoding = header_encoding
+
+ @pytest.mark.parametrize('header_encoding', [False, 'ascii', None])
+ def test_header_encoding_is_reflected_init(self, header_encoding):
+ """
+ The value of ``header_encoding``, when set, is reflected in the value
+ via the initializer.
+ """
+ config = h2.config.H2Configuration(header_encoding=header_encoding)
+ assert config.header_encoding == header_encoding
+
+ @pytest.mark.parametrize('header_encoding', [False, 'ascii', None])
+ def test_header_encoding_is_reflected_attr(self, header_encoding):
+ """
+ The value of ``header_encoding``, when set, is reflected in the value
+ via the attribute setter.
+ """
+ config = h2.config.H2Configuration()
+ config.header_encoding = header_encoding
+ assert config.header_encoding == header_encoding
+
+ def test_logger_instance_is_reflected(self):
+ """
+ The value of ``logger``, when set, is reflected in the value.
+ """
+ logger = logging.Logger('hyper-h2.test')
+ config = h2.config.H2Configuration()
+ config.logger = logger
+ assert config.logger is logger
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_events.py b/testing/web-platform/tests/tools/third_party/h2/test/test_events.py
new file mode 100644
index 0000000000..a6e8d83790
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_events.py
@@ -0,0 +1,367 @@
+# -*- coding: utf-8 -*-
+"""
+test_events.py
+~~~~~~~~~~~~~~
+
+Specific tests for any function that is logically self-contained as part of
+events.py.
+"""
+import inspect
+import sys
+
+from hypothesis import given
+from hypothesis.strategies import (
+ integers, lists, tuples
+)
+import pytest
+
+import h2.errors
+import h2.events
+import h2.settings
+
+
+# We define a fairly complex Hypothesis strategy here. We want to build a list
+# of two tuples of (Setting, value). For Setting we want to make sure we can
+# handle settings that the rest of hyper knows nothing about, so we want to
+# use integers from 0 to (2**16-1). For values, they're from 0 to (2**32-1).
+# Define that strategy here for clarity.
+SETTINGS_STRATEGY = lists(
+ tuples(
+ integers(min_value=0, max_value=2**16-1),
+ integers(min_value=0, max_value=2**32-1),
+ )
+)
+
+
+class TestRemoteSettingsChanged(object):
+ """
+ Validate the function of the RemoteSettingsChanged event.
+ """
+ @given(SETTINGS_STRATEGY)
+ def test_building_settings_from_scratch(self, settings_list):
+ """
+ Missing old settings are defaulted to None.
+ """
+ settings_dict = dict(settings_list)
+ e = h2.events.RemoteSettingsChanged.from_settings(
+ old_settings={},
+ new_settings=settings_dict,
+ )
+
+ for setting, new_value in settings_dict.items():
+ assert e.changed_settings[setting].setting == setting
+ assert e.changed_settings[setting].original_value is None
+ assert e.changed_settings[setting].new_value == new_value
+
+ @given(SETTINGS_STRATEGY, SETTINGS_STRATEGY)
+ def test_only_reports_changed_settings(self,
+ old_settings_list,
+ new_settings_list):
+ """
+ Settings that were not changed are not reported.
+ """
+ old_settings_dict = dict(old_settings_list)
+ new_settings_dict = dict(new_settings_list)
+ e = h2.events.RemoteSettingsChanged.from_settings(
+ old_settings=old_settings_dict,
+ new_settings=new_settings_dict,
+ )
+
+ assert len(e.changed_settings) == len(new_settings_dict)
+ assert (
+ sorted(list(e.changed_settings.keys())) ==
+ sorted(list(new_settings_dict.keys()))
+ )
+
+ @given(SETTINGS_STRATEGY, SETTINGS_STRATEGY)
+ def test_correctly_reports_changed_settings(self,
+ old_settings_list,
+ new_settings_list):
+ """
+ Settings that are changed are correctly reported.
+ """
+ old_settings_dict = dict(old_settings_list)
+ new_settings_dict = dict(new_settings_list)
+ e = h2.events.RemoteSettingsChanged.from_settings(
+ old_settings=old_settings_dict,
+ new_settings=new_settings_dict,
+ )
+
+ for setting, new_value in new_settings_dict.items():
+ original_value = old_settings_dict.get(setting)
+ assert e.changed_settings[setting].setting == setting
+ assert e.changed_settings[setting].original_value == original_value
+ assert e.changed_settings[setting].new_value == new_value
+
+
+class TestEventReprs(object):
+ """
+ Events have useful representations.
+ """
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ]
+ example_informational_headers = [
+ (':status', '100'),
+ ('server', 'fake-serv/0.1.0')
+ ]
+ example_response_headers = [
+ (':status', '200'),
+ ('server', 'fake-serv/0.1.0')
+ ]
+
+ def test_requestreceived_repr(self):
+ """
+ RequestReceived has a useful debug representation.
+ """
+ e = h2.events.RequestReceived()
+ e.stream_id = 5
+ e.headers = self.example_request_headers
+
+ assert repr(e) == (
+ "<RequestReceived stream_id:5, headers:["
+ "(':authority', 'example.com'), "
+ "(':path', '/'), "
+ "(':scheme', 'https'), "
+ "(':method', 'GET')]>"
+ )
+
+ def test_responsereceived_repr(self):
+ """
+ ResponseReceived has a useful debug representation.
+ """
+ e = h2.events.ResponseReceived()
+ e.stream_id = 500
+ e.headers = self.example_response_headers
+
+ assert repr(e) == (
+ "<ResponseReceived stream_id:500, headers:["
+ "(':status', '200'), "
+ "('server', 'fake-serv/0.1.0')]>"
+ )
+
+ def test_trailersreceived_repr(self):
+ """
+ TrailersReceived has a useful debug representation.
+ """
+ e = h2.events.TrailersReceived()
+ e.stream_id = 62
+ e.headers = self.example_response_headers
+
+ assert repr(e) == (
+ "<TrailersReceived stream_id:62, headers:["
+ "(':status', '200'), "
+ "('server', 'fake-serv/0.1.0')]>"
+ )
+
+ def test_informationalresponsereceived_repr(self):
+ """
+ InformationalResponseReceived has a useful debug representation.
+ """
+ e = h2.events.InformationalResponseReceived()
+ e.stream_id = 62
+ e.headers = self.example_informational_headers
+
+ assert repr(e) == (
+ "<InformationalResponseReceived stream_id:62, headers:["
+ "(':status', '100'), "
+ "('server', 'fake-serv/0.1.0')]>"
+ )
+
+ def test_datareceived_repr(self):
+ """
+ DataReceived has a useful debug representation.
+ """
+ e = h2.events.DataReceived()
+ e.stream_id = 888
+ e.data = b"abcdefghijklmnopqrstuvwxyz"
+ e.flow_controlled_length = 88
+
+ assert repr(e) == (
+ "<DataReceived stream_id:888, flow_controlled_length:88, "
+ "data:6162636465666768696a6b6c6d6e6f7071727374>"
+ )
+
+ def test_windowupdated_repr(self):
+ """
+ WindowUpdated has a useful debug representation.
+ """
+ e = h2.events.WindowUpdated()
+ e.stream_id = 0
+ e.delta = 2**16
+
+ assert repr(e) == "<WindowUpdated stream_id:0, delta:65536>"
+
+ def test_remotesettingschanged_repr(self):
+ """
+ RemoteSettingsChanged has a useful debug representation.
+ """
+ e = h2.events.RemoteSettingsChanged()
+ e.changed_settings = {
+ h2.settings.SettingCodes.INITIAL_WINDOW_SIZE:
+ h2.settings.ChangedSetting(
+ h2.settings.SettingCodes.INITIAL_WINDOW_SIZE, 2**16, 2**15
+ ),
+ }
+
+ assert repr(e) == (
+ "<RemoteSettingsChanged changed_settings:{ChangedSetting("
+ "setting=SettingCodes.INITIAL_WINDOW_SIZE, original_value=65536, "
+ "new_value=32768)}>"
+ )
+
+ def test_pingreceived_repr(self):
+ """
+ PingReceived has a useful debug representation.
+ """
+ e = h2.events.PingReceived()
+ e.ping_data = b'abcdefgh'
+
+ assert repr(e) == "<PingReceived ping_data:6162636465666768>"
+
+ def test_pingackreceived_repr(self):
+ """
+ PingAckReceived has a useful debug representation.
+ """
+ e = h2.events.PingAckReceived()
+ e.ping_data = b'abcdefgh'
+
+ assert repr(e) == "<PingAckReceived ping_data:6162636465666768>"
+
+ def test_streamended_repr(self):
+ """
+ StreamEnded has a useful debug representation.
+ """
+ e = h2.events.StreamEnded()
+ e.stream_id = 99
+
+ assert repr(e) == "<StreamEnded stream_id:99>"
+
+ def test_streamreset_repr(self):
+ """
+ StreamEnded has a useful debug representation.
+ """
+ e = h2.events.StreamReset()
+ e.stream_id = 919
+ e.error_code = h2.errors.ErrorCodes.ENHANCE_YOUR_CALM
+ e.remote_reset = False
+
+ assert repr(e) == (
+ "<StreamReset stream_id:919, "
+ "error_code:ErrorCodes.ENHANCE_YOUR_CALM, remote_reset:False>"
+ )
+
+ def test_pushedstreamreceived_repr(self):
+ """
+ PushedStreamReceived has a useful debug representation.
+ """
+ e = h2.events.PushedStreamReceived()
+ e.pushed_stream_id = 50
+ e.parent_stream_id = 11
+ e.headers = self.example_request_headers
+
+ assert repr(e) == (
+ "<PushedStreamReceived pushed_stream_id:50, parent_stream_id:11, "
+ "headers:["
+ "(':authority', 'example.com'), "
+ "(':path', '/'), "
+ "(':scheme', 'https'), "
+ "(':method', 'GET')]>"
+ )
+
+ def test_settingsacknowledged_repr(self):
+ """
+ SettingsAcknowledged has a useful debug representation.
+ """
+ e = h2.events.SettingsAcknowledged()
+ e.changed_settings = {
+ h2.settings.SettingCodes.INITIAL_WINDOW_SIZE:
+ h2.settings.ChangedSetting(
+ h2.settings.SettingCodes.INITIAL_WINDOW_SIZE, 2**16, 2**15
+ ),
+ }
+
+ assert repr(e) == (
+ "<SettingsAcknowledged changed_settings:{ChangedSetting("
+ "setting=SettingCodes.INITIAL_WINDOW_SIZE, original_value=65536, "
+ "new_value=32768)}>"
+ )
+
+ def test_priorityupdated_repr(self):
+ """
+ PriorityUpdated has a useful debug representation.
+ """
+ e = h2.events.PriorityUpdated()
+ e.stream_id = 87
+ e.weight = 32
+ e.depends_on = 8
+ e.exclusive = True
+
+ assert repr(e) == (
+ "<PriorityUpdated stream_id:87, weight:32, depends_on:8, "
+ "exclusive:True>"
+ )
+
+ @pytest.mark.parametrize("additional_data,data_repr", [
+ (None, "None"),
+ (b'some data', "736f6d652064617461")
+ ])
+ def test_connectionterminated_repr(self, additional_data, data_repr):
+ """
+ ConnectionTerminated has a useful debug representation.
+ """
+ e = h2.events.ConnectionTerminated()
+ e.error_code = h2.errors.ErrorCodes.INADEQUATE_SECURITY
+ e.last_stream_id = 33
+ e.additional_data = additional_data
+
+ assert repr(e) == (
+ "<ConnectionTerminated error_code:ErrorCodes.INADEQUATE_SECURITY, "
+ "last_stream_id:33, additional_data:%s>" % data_repr
+ )
+
+ def test_alternativeserviceavailable_repr(self):
+ """
+ AlternativeServiceAvailable has a useful debug representation.
+ """
+ e = h2.events.AlternativeServiceAvailable()
+ e.origin = b"example.com"
+ e.field_value = b'h2=":8000"; ma=60'
+
+ assert repr(e) == (
+ '<AlternativeServiceAvailable origin:example.com, '
+ 'field_value:h2=":8000"; ma=60>'
+ )
+
+ def test_unknownframereceived_repr(self):
+ """
+ UnknownFrameReceived has a useful debug representation.
+ """
+ e = h2.events.UnknownFrameReceived()
+ assert repr(e) == '<UnknownFrameReceived>'
+
+
+def all_events():
+ """
+ Generates all the classes (i.e., events) defined in h2.events.
+ """
+ for _, obj in inspect.getmembers(sys.modules['h2.events']):
+
+ # We are only interested in objects that are defined in h2.events;
+ # objects that are imported from other modules are not of interest.
+ if hasattr(obj, '__module__') and (obj.__module__ != 'h2.events'):
+ continue
+
+ if inspect.isclass(obj):
+ yield obj
+
+
+@pytest.mark.parametrize('event', all_events())
+def test_all_events_subclass_from_event(event):
+ """
+ Every event defined in h2.events subclasses from h2.events.Event.
+ """
+ assert (event is h2.events.Event) or issubclass(event, h2.events.Event)
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_exceptions.py b/testing/web-platform/tests/tools/third_party/h2/test/test_exceptions.py
new file mode 100644
index 0000000000..1890459978
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_exceptions.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+"""
+test_exceptions
+~~~~~~~~~~~~~~~
+
+Tests that verify logic local to exceptions.
+"""
+import h2.exceptions
+
+
+class TestExceptions(object):
+ def test_stream_id_too_low_prints_properly(self):
+ x = h2.exceptions.StreamIDTooLowError(5, 10)
+
+ assert "StreamIDTooLowError: 5 is lower than 10" == str(x)
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_flow_control_window.py b/testing/web-platform/tests/tools/third_party/h2/test/test_flow_control_window.py
new file mode 100644
index 0000000000..24b345aac3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_flow_control_window.py
@@ -0,0 +1,952 @@
+# -*- coding: utf-8 -*-
+"""
+test_flow_control
+~~~~~~~~~~~~~~~~~
+
+Tests of the flow control management in h2
+"""
+import pytest
+
+from hypothesis import given
+from hypothesis.strategies import integers
+
+import h2.config
+import h2.connection
+import h2.errors
+import h2.events
+import h2.exceptions
+import h2.settings
+
+
+class TestFlowControl(object):
+ """
+ Tests of the flow control management in the connection objects.
+ """
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ]
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ DEFAULT_FLOW_WINDOW = 65535
+
+ def test_flow_control_initializes_properly(self):
+ """
+ The flow control window for a stream should initially be the default
+ flow control value.
+ """
+ c = h2.connection.H2Connection()
+ c.send_headers(1, self.example_request_headers)
+
+ assert c.local_flow_control_window(1) == self.DEFAULT_FLOW_WINDOW
+ assert c.remote_flow_control_window(1) == self.DEFAULT_FLOW_WINDOW
+
+ def test_flow_control_decreases_with_sent_data(self):
+ """
+ When data is sent on a stream, the flow control window should drop.
+ """
+ c = h2.connection.H2Connection()
+ c.send_headers(1, self.example_request_headers)
+ c.send_data(1, b'some data')
+
+ remaining_length = self.DEFAULT_FLOW_WINDOW - len(b'some data')
+ assert (c.local_flow_control_window(1) == remaining_length)
+
+ @pytest.mark.parametrize("pad_length", [5, 0])
+ def test_flow_control_decreases_with_sent_data_with_padding(self,
+ pad_length):
+ """
+ When padded data is sent on a stream, the flow control window drops
+ by the length of the padding plus 1 for the 1-byte padding length
+ field.
+ """
+ c = h2.connection.H2Connection()
+ c.send_headers(1, self.example_request_headers)
+
+ c.send_data(1, b'some data', pad_length=pad_length)
+ remaining_length = (
+ self.DEFAULT_FLOW_WINDOW - len(b'some data') - pad_length - 1
+ )
+ assert c.local_flow_control_window(1) == remaining_length
+
+ def test_flow_control_decreases_with_received_data(self, frame_factory):
+ """
+ When data is received on a stream, the remote flow control window
+ should drop.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f1 = frame_factory.build_headers_frame(self.example_request_headers)
+ f2 = frame_factory.build_data_frame(b'some data')
+
+ c.receive_data(f1.serialize() + f2.serialize())
+
+ remaining_length = self.DEFAULT_FLOW_WINDOW - len(b'some data')
+ assert (c.remote_flow_control_window(1) == remaining_length)
+
+ def test_flow_control_decreases_with_padded_data(self, frame_factory):
+ """
+ When padded data is received on a stream, the remote flow control
+ window drops by an amount that includes the padding.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f1 = frame_factory.build_headers_frame(self.example_request_headers)
+ f2 = frame_factory.build_data_frame(b'some data', padding_len=10)
+
+ c.receive_data(f1.serialize() + f2.serialize())
+
+ remaining_length = (
+ self.DEFAULT_FLOW_WINDOW - len(b'some data') - 10 - 1
+ )
+ assert (c.remote_flow_control_window(1) == remaining_length)
+
+ def test_flow_control_is_limited_by_connection(self):
+ """
+ The flow control window is limited by the flow control of the
+ connection.
+ """
+ c = h2.connection.H2Connection()
+ c.send_headers(1, self.example_request_headers)
+ c.send_data(1, b'some data')
+ c.send_headers(3, self.example_request_headers)
+
+ remaining_length = self.DEFAULT_FLOW_WINDOW - len(b'some data')
+ assert (c.local_flow_control_window(3) == remaining_length)
+
+ def test_remote_flow_control_is_limited_by_connection(self, frame_factory):
+ """
+ The remote flow control window is limited by the flow control of the
+ connection.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ f1 = frame_factory.build_headers_frame(self.example_request_headers)
+ f2 = frame_factory.build_data_frame(b'some data')
+ f3 = frame_factory.build_headers_frame(
+ self.example_request_headers,
+ stream_id=3,
+ )
+ c.receive_data(f1.serialize() + f2.serialize() + f3.serialize())
+
+ remaining_length = self.DEFAULT_FLOW_WINDOW - len(b'some data')
+ assert (c.remote_flow_control_window(3) == remaining_length)
+
+ def test_cannot_send_more_data_than_window(self):
+ """
+ Sending more data than the remaining flow control window raises a
+ FlowControlError.
+ """
+ c = h2.connection.H2Connection()
+ c.send_headers(1, self.example_request_headers)
+ c.outbound_flow_control_window = 5
+
+ with pytest.raises(h2.exceptions.FlowControlError):
+ c.send_data(1, b'some data')
+
+ def test_increasing_connection_window_allows_sending(self, frame_factory):
+ """
+ Confirm that sending a WindowUpdate frame on the connection frees
+ up space for further frames.
+ """
+ c = h2.connection.H2Connection()
+ c.send_headers(1, self.example_request_headers)
+ c.outbound_flow_control_window = 5
+
+ with pytest.raises(h2.exceptions.FlowControlError):
+ c.send_data(1, b'some data')
+
+ f = frame_factory.build_window_update_frame(
+ stream_id=0,
+ increment=5,
+ )
+ c.receive_data(f.serialize())
+
+ c.clear_outbound_data_buffer()
+ c.send_data(1, b'some data')
+ assert c.data_to_send()
+
+ def test_increasing_stream_window_allows_sending(self, frame_factory):
+ """
+ Confirm that sending a WindowUpdate frame on the connection frees
+ up space for further frames.
+ """
+ c = h2.connection.H2Connection()
+ c.send_headers(1, self.example_request_headers)
+ c._get_stream_by_id(1).outbound_flow_control_window = 5
+
+ with pytest.raises(h2.exceptions.FlowControlError):
+ c.send_data(1, b'some data')
+
+ f = frame_factory.build_window_update_frame(
+ stream_id=1,
+ increment=5,
+ )
+ c.receive_data(f.serialize())
+
+ c.clear_outbound_data_buffer()
+ c.send_data(1, b'some data')
+ assert c.data_to_send()
+
+ def test_flow_control_shrinks_in_response_to_settings(self, frame_factory):
+ """
+ Acknowledging SETTINGS_INITIAL_WINDOW_SIZE shrinks the flow control
+ window.
+ """
+ c = h2.connection.H2Connection()
+ c.send_headers(1, self.example_request_headers)
+
+ assert c.local_flow_control_window(1) == 65535
+
+ f = frame_factory.build_settings_frame(
+ settings={h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: 1280}
+ )
+ c.receive_data(f.serialize())
+
+ assert c.local_flow_control_window(1) == 1280
+
+ def test_flow_control_grows_in_response_to_settings(self, frame_factory):
+ """
+ Acknowledging SETTINGS_INITIAL_WINDOW_SIZE grows the flow control
+ window.
+ """
+ c = h2.connection.H2Connection()
+ c.send_headers(1, self.example_request_headers)
+
+ # Greatly increase the connection flow control window.
+ f = frame_factory.build_window_update_frame(
+ stream_id=0, increment=128000
+ )
+ c.receive_data(f.serialize())
+
+ # The stream flow control window is the bottleneck here.
+ assert c.local_flow_control_window(1) == 65535
+
+ f = frame_factory.build_settings_frame(
+ settings={h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: 128000}
+ )
+ c.receive_data(f.serialize())
+
+ # The stream window is still the bottleneck, but larger now.
+ assert c.local_flow_control_window(1) == 128000
+
+ def test_flow_control_settings_blocked_by_conn_window(self, frame_factory):
+ """
+ Changing SETTINGS_INITIAL_WINDOW_SIZE does not affect the effective
+ flow control window if the connection window isn't changed.
+ """
+ c = h2.connection.H2Connection()
+ c.send_headers(1, self.example_request_headers)
+
+ assert c.local_flow_control_window(1) == 65535
+
+ f = frame_factory.build_settings_frame(
+ settings={h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: 128000}
+ )
+ c.receive_data(f.serialize())
+
+ assert c.local_flow_control_window(1) == 65535
+
+ def test_new_streams_have_flow_control_per_settings(self, frame_factory):
+ """
+ After a SETTINGS_INITIAL_WINDOW_SIZE change is received, new streams
+ have appropriate new flow control windows.
+ """
+ c = h2.connection.H2Connection()
+
+ f = frame_factory.build_settings_frame(
+ settings={h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: 128000}
+ )
+ c.receive_data(f.serialize())
+
+ # Greatly increase the connection flow control window.
+ f = frame_factory.build_window_update_frame(
+ stream_id=0, increment=128000
+ )
+ c.receive_data(f.serialize())
+
+ c.send_headers(1, self.example_request_headers)
+ assert c.local_flow_control_window(1) == 128000
+
+ def test_window_update_no_stream(self, frame_factory):
+ """
+ WindowUpdate frames received without streams fire an appropriate
+ WindowUpdated event.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_window_update_frame(
+ stream_id=0,
+ increment=5
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.WindowUpdated)
+ assert event.stream_id == 0
+ assert event.delta == 5
+
+ def test_window_update_with_stream(self, frame_factory):
+ """
+ WindowUpdate frames received with streams fire an appropriate
+ WindowUpdated event.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ f1 = frame_factory.build_headers_frame(self.example_request_headers)
+ f2 = frame_factory.build_window_update_frame(
+ stream_id=1,
+ increment=66
+ )
+ data = b''.join(map(lambda f: f.serialize(), [f1, f2]))
+ events = c.receive_data(data)
+
+ assert len(events) == 2
+ event = events[1]
+
+ assert isinstance(event, h2.events.WindowUpdated)
+ assert event.stream_id == 1
+ assert event.delta == 66
+
+ def test_we_can_increment_stream_flow_control(self, frame_factory):
+ """
+ It is possible for the user to increase the flow control window for
+ streams.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+ c.clear_outbound_data_buffer()
+
+ expected_frame = frame_factory.build_window_update_frame(
+ stream_id=1,
+ increment=5
+ )
+
+ events = c.increment_flow_control_window(increment=5, stream_id=1)
+ assert not events
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_we_can_increment_connection_flow_control(self, frame_factory):
+ """
+ It is possible for the user to increase the flow control window for
+ the entire connection.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+ c.clear_outbound_data_buffer()
+
+ expected_frame = frame_factory.build_window_update_frame(
+ stream_id=0,
+ increment=5
+ )
+
+ events = c.increment_flow_control_window(increment=5)
+ assert not events
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_we_enforce_our_flow_control_window(self, frame_factory):
+ """
+ The user can set a low flow control window, which leads to connection
+ teardown if violated.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ # Change the flow control window to 80 bytes.
+ c.update_settings(
+ {h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: 80}
+ )
+ f = frame_factory.build_settings_frame({}, ack=True)
+ c.receive_data(f.serialize())
+
+ # Receive a new stream.
+ f = frame_factory.build_headers_frame(self.example_request_headers)
+ c.receive_data(f.serialize())
+
+ # Attempt to violate the flow control window.
+ c.clear_outbound_data_buffer()
+ f = frame_factory.build_data_frame(b'\x01' * 100)
+
+ with pytest.raises(h2.exceptions.FlowControlError):
+ c.receive_data(f.serialize())
+
+ # Verify we tear down appropriately.
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=1,
+ error_code=h2.errors.ErrorCodes.FLOW_CONTROL_ERROR,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_shrink_remote_flow_control_settings(self, frame_factory):
+ """
+ The remote peer acknowledging our SETTINGS_INITIAL_WINDOW_SIZE shrinks
+ the flow control window.
+ """
+ c = h2.connection.H2Connection()
+ c.send_headers(1, self.example_request_headers)
+
+ assert c.remote_flow_control_window(1) == 65535
+
+ c.update_settings({h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: 1280})
+
+ f = frame_factory.build_settings_frame({}, ack=True)
+ c.receive_data(f.serialize())
+
+ assert c.remote_flow_control_window(1) == 1280
+
+ def test_grow_remote_flow_control_settings(self, frame_factory):
+ """
+ The remote peer acknowledging our SETTINGS_INITIAL_WINDOW_SIZE grows
+ the flow control window.
+ """
+ c = h2.connection.H2Connection()
+ c.send_headers(1, self.example_request_headers)
+
+ # Increase the connection flow control window greatly.
+ c.increment_flow_control_window(increment=128000)
+
+ assert c.remote_flow_control_window(1) == 65535
+
+ c.update_settings(
+ {h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: 128000}
+ )
+ f = frame_factory.build_settings_frame({}, ack=True)
+ c.receive_data(f.serialize())
+
+ assert c.remote_flow_control_window(1) == 128000
+
+ def test_new_streams_have_remote_flow_control(self, frame_factory):
+ """
+ After a SETTINGS_INITIAL_WINDOW_SIZE change is acknowledged by the
+ remote peer, new streams have appropriate new flow control windows.
+ """
+ c = h2.connection.H2Connection()
+
+ c.update_settings(
+ {h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: 128000}
+ )
+ f = frame_factory.build_settings_frame({}, ack=True)
+ c.receive_data(f.serialize())
+
+ # Increase the connection flow control window greatly.
+ c.increment_flow_control_window(increment=128000)
+
+ c.send_headers(1, self.example_request_headers)
+ assert c.remote_flow_control_window(1) == 128000
+
+ @pytest.mark.parametrize(
+ 'increment', [0, -15, 2**31]
+ )
+ def test_reject_bad_attempts_to_increment_flow_control(self, increment):
+ """
+ Attempting to increment a flow control increment outside the valid
+ range causes a ValueError to be raised.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+ c.clear_outbound_data_buffer()
+
+ # Fails both on and off streams.
+ with pytest.raises(ValueError):
+ c.increment_flow_control_window(increment=increment, stream_id=1)
+
+ with pytest.raises(ValueError):
+ c.increment_flow_control_window(increment=increment)
+
+ @pytest.mark.parametrize('stream_id', [0, 1])
+ def test_reject_bad_remote_increments(self, frame_factory, stream_id):
+ """
+ Remote peers attempting to increment flow control outside the valid
+ range cause connection errors of type PROTOCOL_ERROR.
+ """
+ # The only number that can be encoded in a WINDOW_UPDATE frame but
+ # isn't valid is 0.
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_window_update_frame(
+ stream_id=stream_id, increment=0
+ )
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=0,
+ error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_reject_increasing_connection_window_too_far(self, frame_factory):
+ """
+ Attempts by the remote peer to increase the connection flow control
+ window beyond 2**31 - 1 are rejected.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.clear_outbound_data_buffer()
+
+ increment = 2**31 - c.outbound_flow_control_window
+
+ f = frame_factory.build_window_update_frame(
+ stream_id=0, increment=increment
+ )
+
+ with pytest.raises(h2.exceptions.FlowControlError):
+ c.receive_data(f.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=0,
+ error_code=h2.errors.ErrorCodes.FLOW_CONTROL_ERROR,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_reject_increasing_stream_window_too_far(self, frame_factory):
+ """
+ Attempts by the remote peer to increase the stream flow control window
+ beyond 2**31 - 1 are rejected.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers)
+ c.clear_outbound_data_buffer()
+
+ increment = 2**31 - c.outbound_flow_control_window
+
+ f = frame_factory.build_window_update_frame(
+ stream_id=1, increment=increment
+ )
+
+ events = c.receive_data(f.serialize())
+ assert len(events) == 1
+
+ event = events[0]
+ assert isinstance(event, h2.events.StreamReset)
+ assert event.stream_id == 1
+ assert event.error_code == h2.errors.ErrorCodes.FLOW_CONTROL_ERROR
+ assert not event.remote_reset
+
+ expected_frame = frame_factory.build_rst_stream_frame(
+ stream_id=1,
+ error_code=h2.errors.ErrorCodes.FLOW_CONTROL_ERROR,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_reject_overlarge_conn_window_settings(self, frame_factory):
+ """
+ SETTINGS frames cannot change the size of the connection flow control
+ window.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ # Go one byte smaller than the limit.
+ increment = 2**31 - 1 - c.outbound_flow_control_window
+
+ f = frame_factory.build_window_update_frame(
+ stream_id=0, increment=increment
+ )
+ c.receive_data(f.serialize())
+
+ # Receive an increment to the initial window size.
+ f = frame_factory.build_settings_frame(
+ settings={
+ h2.settings.SettingCodes.INITIAL_WINDOW_SIZE:
+ self.DEFAULT_FLOW_WINDOW + 1
+ }
+ )
+ c.clear_outbound_data_buffer()
+
+ # No error is encountered.
+ events = c.receive_data(f.serialize())
+ assert len(events) == 1
+ assert isinstance(events[0], h2.events.RemoteSettingsChanged)
+
+ expected_frame = frame_factory.build_settings_frame(
+ settings={},
+ ack=True
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_reject_overlarge_stream_window_settings(self, frame_factory):
+ """
+ Remote attempts to create overlarge stream windows via SETTINGS frames
+ are rejected.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers)
+
+ # Go one byte smaller than the limit.
+ increment = 2**31 - 1 - c.outbound_flow_control_window
+
+ f = frame_factory.build_window_update_frame(
+ stream_id=1, increment=increment
+ )
+ c.receive_data(f.serialize())
+
+ # Receive an increment to the initial window size.
+ f = frame_factory.build_settings_frame(
+ settings={
+ h2.settings.SettingCodes.INITIAL_WINDOW_SIZE:
+ self.DEFAULT_FLOW_WINDOW + 1
+ }
+ )
+ c.clear_outbound_data_buffer()
+ with pytest.raises(h2.exceptions.FlowControlError):
+ c.receive_data(f.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=0,
+ error_code=h2.errors.ErrorCodes.FLOW_CONTROL_ERROR,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_reject_local_overlarge_increase_connection_window(self):
+ """
+ Local attempts to increase the connection window too far are rejected.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ increment = 2**31 - c.inbound_flow_control_window
+
+ with pytest.raises(h2.exceptions.FlowControlError):
+ c.increment_flow_control_window(increment=increment)
+
+ def test_reject_local_overlarge_increase_stream_window(self):
+ """
+ Local attempts to increase the connection window too far are rejected.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers)
+
+ increment = 2**31 - c.inbound_flow_control_window
+
+ with pytest.raises(h2.exceptions.FlowControlError):
+ c.increment_flow_control_window(increment=increment, stream_id=1)
+
+ def test_send_update_on_closed_streams(self, frame_factory):
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers)
+ c.reset_stream(1)
+
+ c.clear_outbound_data_buffer()
+ c.open_outbound_streams
+ c.open_inbound_streams
+
+ f = frame_factory.build_data_frame(b'some data'*1500)
+ events = c.receive_data(f.serialize()*3)
+ assert not events
+
+ expected = frame_factory.build_rst_stream_frame(
+ stream_id=1,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ ).serialize() * 2 + frame_factory.build_window_update_frame(
+ stream_id=0,
+ increment=40500,
+ ).serialize() + frame_factory.build_rst_stream_frame(
+ stream_id=1,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ ).serialize()
+ assert c.data_to_send() == expected
+
+ f = frame_factory.build_data_frame(b'')
+ events = c.receive_data(f.serialize())
+ assert not events
+
+ expected = frame_factory.build_rst_stream_frame(
+ stream_id=1,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ ).serialize()
+ assert c.data_to_send() == expected
+
+
+class TestAutomaticFlowControl(object):
+ """
+ Tests for the automatic flow control logic.
+ """
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ]
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ DEFAULT_FLOW_WINDOW = 65535
+
+ def _setup_connection_and_send_headers(self, frame_factory):
+ """
+ Setup a server-side H2Connection and send a headers frame, and then
+ clear the outbound data buffer. Also increase the maximum frame size.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ c.update_settings(
+ {h2.settings.SettingCodes.MAX_FRAME_SIZE: self.DEFAULT_FLOW_WINDOW}
+ )
+ settings_frame = frame_factory.build_settings_frame(
+ settings={}, ack=True
+ )
+ c.receive_data(settings_frame.serialize())
+ c.clear_outbound_data_buffer()
+
+ headers_frame = frame_factory.build_headers_frame(
+ headers=self.example_request_headers
+ )
+ c.receive_data(headers_frame.serialize())
+ c.clear_outbound_data_buffer()
+ return c
+
+ @given(stream_id=integers(max_value=0))
+ def test_must_acknowledge_for_stream(self, frame_factory, stream_id):
+ """
+ Flow control acknowledgements must be done on a stream ID that is
+ greater than zero.
+ """
+ # We need to refresh the encoder because hypothesis has a problem with
+ # integrating with py.test, meaning that we use the same frame factory
+ # for all tests.
+ # See https://github.com/HypothesisWorks/hypothesis-python/issues/377
+ frame_factory.refresh_encoder()
+
+ # Create a connection in a state that might actually accept
+ # data acknolwedgement.
+ c = self._setup_connection_and_send_headers(frame_factory)
+ data_frame = frame_factory.build_data_frame(
+ b'some data', flags=['END_STREAM']
+ )
+ c.receive_data(data_frame.serialize())
+
+ with pytest.raises(ValueError):
+ c.acknowledge_received_data(
+ acknowledged_size=5, stream_id=stream_id
+ )
+
+ @given(size=integers(max_value=-1))
+ def test_cannot_acknowledge_less_than_zero(self, frame_factory, size):
+ """
+ The user must acknowledge at least 0 bytes.
+ """
+ # We need to refresh the encoder because hypothesis has a problem with
+ # integrating with py.test, meaning that we use the same frame factory
+ # for all tests.
+ # See https://github.com/HypothesisWorks/hypothesis-python/issues/377
+ frame_factory.refresh_encoder()
+
+ # Create a connection in a state that might actually accept
+ # data acknolwedgement.
+ c = self._setup_connection_and_send_headers(frame_factory)
+ data_frame = frame_factory.build_data_frame(
+ b'some data', flags=['END_STREAM']
+ )
+ c.receive_data(data_frame.serialize())
+
+ with pytest.raises(ValueError):
+ c.acknowledge_received_data(acknowledged_size=size, stream_id=1)
+
+ def test_acknowledging_small_chunks_does_nothing(self, frame_factory):
+ """
+ When a small amount of data is received and acknowledged, no window
+ update is emitted.
+ """
+ c = self._setup_connection_and_send_headers(frame_factory)
+
+ data_frame = frame_factory.build_data_frame(
+ b'some data', flags=['END_STREAM']
+ )
+ data_event = c.receive_data(data_frame.serialize())[0]
+
+ c.acknowledge_received_data(
+ data_event.flow_controlled_length, stream_id=1
+ )
+
+ assert not c.data_to_send()
+
+ def test_acknowledging_no_data_does_nothing(self, frame_factory):
+ """
+ If a user accidentally acknowledges no data, nothing happens.
+ """
+ c = self._setup_connection_and_send_headers(frame_factory)
+
+ # Send an empty data frame, just to give the user impetus to ack the
+ # data.
+ data_frame = frame_factory.build_data_frame(b'')
+ c.receive_data(data_frame.serialize())
+
+ c.acknowledge_received_data(0, stream_id=1)
+ assert not c.data_to_send()
+
+ @pytest.mark.parametrize('force_cleanup', (True, False))
+ def test_acknowledging_data_on_closed_stream(self,
+ frame_factory,
+ force_cleanup):
+ """
+ When acknowledging data on a stream that has just been closed, no
+ acknowledgement is given for that stream, only for the connection.
+ """
+ c = self._setup_connection_and_send_headers(frame_factory)
+
+ data_to_send = b'\x00' * self.DEFAULT_FLOW_WINDOW
+ data_frame = frame_factory.build_data_frame(data_to_send)
+ c.receive_data(data_frame.serialize())
+
+ rst_frame = frame_factory.build_rst_stream_frame(
+ stream_id=1
+ )
+ c.receive_data(rst_frame.serialize())
+ c.clear_outbound_data_buffer()
+
+ if force_cleanup:
+ # Check how many streams are open to force the old one to be
+ # cleaned up.
+ assert c.open_outbound_streams == 0
+
+ c.acknowledge_received_data(2048, stream_id=1)
+
+ expected = frame_factory.build_window_update_frame(
+ stream_id=0, increment=2048
+ )
+ assert c.data_to_send() == expected.serialize()
+
+ def test_acknowledging_streams_we_never_saw(self, frame_factory):
+ """
+ If the user acknowledges a stream ID we've never seen, that raises a
+ NoSuchStreamError.
+ """
+ c = self._setup_connection_and_send_headers(frame_factory)
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.NoSuchStreamError):
+ c.acknowledge_received_data(2048, stream_id=101)
+
+ @given(integers(min_value=1025, max_value=DEFAULT_FLOW_WINDOW))
+ def test_acknowledging_1024_bytes_when_empty_increments(self,
+ frame_factory,
+ increment):
+ """
+ If the flow control window is empty and we acknowledge 1024 bytes or
+ more, we will emit a WINDOW_UPDATE frame just to move the connection
+ forward.
+ """
+ # We need to refresh the encoder because hypothesis has a problem with
+ # integrating with py.test, meaning that we use the same frame factory
+ # for all tests.
+ # See https://github.com/HypothesisWorks/hypothesis-python/issues/377
+ frame_factory.refresh_encoder()
+
+ c = self._setup_connection_and_send_headers(frame_factory)
+
+ data_to_send = b'\x00' * self.DEFAULT_FLOW_WINDOW
+ data_frame = frame_factory.build_data_frame(data_to_send)
+ c.receive_data(data_frame.serialize())
+
+ c.acknowledge_received_data(increment, stream_id=1)
+
+ first_expected = frame_factory.build_window_update_frame(
+ stream_id=0, increment=increment
+ )
+ second_expected = frame_factory.build_window_update_frame(
+ stream_id=1, increment=increment
+ )
+ expected_data = b''.join(
+ [first_expected.serialize(), second_expected.serialize()]
+ )
+ assert c.data_to_send() == expected_data
+
+ # This test needs to use a lower cap, because otherwise the algo will
+ # increment the stream window anyway.
+ @given(integers(min_value=1025, max_value=(DEFAULT_FLOW_WINDOW // 4) - 1))
+ def test_connection_only_empty(self, frame_factory, increment):
+ """
+ If the connection flow control window is empty, but the stream flow
+ control windows aren't, and 1024 bytes or more are acknowledged by the
+ user, we increment the connection window only.
+ """
+ # We need to refresh the encoder because hypothesis has a problem with
+ # integrating with py.test, meaning that we use the same frame factory
+ # for all tests.
+ # See https://github.com/HypothesisWorks/hypothesis-python/issues/377
+ frame_factory.refresh_encoder()
+
+ # Here we'll use 4 streams. Set them up.
+ c = self._setup_connection_and_send_headers(frame_factory)
+
+ for stream_id in [3, 5, 7]:
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers, stream_id=stream_id
+ )
+ c.receive_data(f.serialize())
+
+ # Now we send 1/4 of the connection window per stream. Annoyingly,
+ # that's an odd number, so we need to round the last frame up.
+ data_to_send = b'\x00' * (self.DEFAULT_FLOW_WINDOW // 4)
+ for stream_id in [1, 3, 5]:
+ f = frame_factory.build_data_frame(
+ data_to_send, stream_id=stream_id
+ )
+ c.receive_data(f.serialize())
+
+ data_to_send = b'\x00' * c.remote_flow_control_window(7)
+ data_frame = frame_factory.build_data_frame(data_to_send, stream_id=7)
+ c.receive_data(data_frame.serialize())
+
+ # Ok, now the actual test.
+ c.acknowledge_received_data(increment, stream_id=1)
+
+ expected_data = frame_factory.build_window_update_frame(
+ stream_id=0, increment=increment
+ ).serialize()
+ assert c.data_to_send() == expected_data
+
+ @given(integers(min_value=1025, max_value=DEFAULT_FLOW_WINDOW))
+ def test_mixing_update_forms(self, frame_factory, increment):
+ """
+ If the user mixes ackowledging data with manually incrementing windows,
+ we still keep track of what's going on.
+ """
+ # We need to refresh the encoder because hypothesis has a problem with
+ # integrating with py.test, meaning that we use the same frame factory
+ # for all tests.
+ # See https://github.com/HypothesisWorks/hypothesis-python/issues/377
+ frame_factory.refresh_encoder()
+
+ # Empty the flow control window.
+ c = self._setup_connection_and_send_headers(frame_factory)
+ data_to_send = b'\x00' * self.DEFAULT_FLOW_WINDOW
+ data_frame = frame_factory.build_data_frame(data_to_send)
+ c.receive_data(data_frame.serialize())
+
+ # Manually increment the connection flow control window back to fully
+ # open, but leave the stream window closed.
+ c.increment_flow_control_window(
+ stream_id=None, increment=self.DEFAULT_FLOW_WINDOW
+ )
+ c.clear_outbound_data_buffer()
+
+ # Now, acknowledge the receipt of that data. This should cause the
+ # stream window to be widened, but not the connection window, because
+ # it is already open.
+ c.acknowledge_received_data(increment, stream_id=1)
+
+ # We expect to see one window update frame only, for the stream.
+ expected_data = frame_factory.build_window_update_frame(
+ stream_id=1, increment=increment
+ ).serialize()
+ assert c.data_to_send() == expected_data
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_h2_upgrade.py b/testing/web-platform/tests/tools/third_party/h2/test/test_h2_upgrade.py
new file mode 100644
index 0000000000..d63d44f3f7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_h2_upgrade.py
@@ -0,0 +1,302 @@
+# -*- coding: utf-8 -*-
+"""
+test_h2_upgrade.py
+~~~~~~~~~~~~~~~~~~
+
+This module contains tests that exercise the HTTP Upgrade functionality of
+hyper-h2, ensuring that clients and servers can upgrade their plaintext
+HTTP/1.1 connections to HTTP/2.
+"""
+import base64
+
+import pytest
+
+import h2.config
+import h2.connection
+import h2.errors
+import h2.events
+import h2.exceptions
+
+
+class TestClientUpgrade(object):
+ """
+ Tests of the client-side of the HTTP/2 upgrade dance.
+ """
+ example_request_headers = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'GET'),
+ ]
+ example_response_headers = [
+ (b':status', b'200'),
+ (b'server', b'fake-serv/0.1.0')
+ ]
+
+ def test_returns_http2_settings(self, frame_factory):
+ """
+ Calling initiate_upgrade_connection returns a base64url encoded
+ Settings frame with the settings used by the connection.
+ """
+ conn = h2.connection.H2Connection()
+ data = conn.initiate_upgrade_connection()
+
+ # The base64 encoding must not be padded.
+ assert not data.endswith(b'=')
+
+ # However, SETTINGS frames should never need to be padded.
+ decoded_frame = base64.urlsafe_b64decode(data)
+ expected_frame = frame_factory.build_settings_frame(
+ settings=conn.local_settings
+ )
+ assert decoded_frame == expected_frame.serialize_body()
+
+ def test_emits_preamble(self, frame_factory):
+ """
+ Calling initiate_upgrade_connection emits the connection preamble.
+ """
+ conn = h2.connection.H2Connection()
+ conn.initiate_upgrade_connection()
+
+ data = conn.data_to_send()
+ assert data.startswith(frame_factory.preamble())
+
+ data = data[len(frame_factory.preamble()):]
+ expected_frame = frame_factory.build_settings_frame(
+ settings=conn.local_settings
+ )
+ assert data == expected_frame.serialize()
+
+ def test_can_receive_response(self, frame_factory):
+ """
+ After upgrading, we can safely receive a response.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_upgrade_connection()
+ c.clear_outbound_data_buffer()
+
+ f1 = frame_factory.build_headers_frame(
+ stream_id=1,
+ headers=self.example_response_headers,
+ )
+ f2 = frame_factory.build_data_frame(
+ stream_id=1,
+ data=b'some data',
+ flags=['END_STREAM']
+ )
+ events = c.receive_data(f1.serialize() + f2.serialize())
+ assert len(events) == 3
+
+ assert isinstance(events[0], h2.events.ResponseReceived)
+ assert isinstance(events[1], h2.events.DataReceived)
+ assert isinstance(events[2], h2.events.StreamEnded)
+
+ assert events[0].headers == self.example_response_headers
+ assert events[1].data == b'some data'
+ assert all(e.stream_id == 1 for e in events)
+
+ assert not c.data_to_send()
+
+ def test_can_receive_pushed_stream(self, frame_factory):
+ """
+ After upgrading, we can safely receive a pushed stream.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_upgrade_connection()
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=self.example_request_headers,
+ )
+ events = c.receive_data(f.serialize())
+ assert len(events) == 1
+
+ assert isinstance(events[0], h2.events.PushedStreamReceived)
+ assert events[0].headers == self.example_request_headers
+ assert events[0].parent_stream_id == 1
+ assert events[0].pushed_stream_id == 2
+
+ def test_cannot_send_headers_stream_1(self, frame_factory):
+ """
+ After upgrading, we cannot send headers on stream 1.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_upgrade_connection()
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ def test_cannot_send_data_stream_1(self, frame_factory):
+ """
+ After upgrading, we cannot send data on stream 1.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_upgrade_connection()
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.send_data(stream_id=1, data=b'some data')
+
+
+class TestServerUpgrade(object):
+ """
+ Tests of the server-side of the HTTP/2 upgrade dance.
+ """
+ example_request_headers = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'GET'),
+ ]
+ example_response_headers = [
+ (b':status', b'200'),
+ (b'server', b'fake-serv/0.1.0')
+ ]
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ def test_returns_nothing(self, frame_factory):
+ """
+ Calling initiate_upgrade_connection returns nothing.
+ """
+ conn = h2.connection.H2Connection(config=self.server_config)
+ curl_header = b"AAMAAABkAAQAAP__"
+ data = conn.initiate_upgrade_connection(curl_header)
+ assert data is None
+
+ def test_emits_preamble(self, frame_factory):
+ """
+ Calling initiate_upgrade_connection emits the connection preamble.
+ """
+ conn = h2.connection.H2Connection(config=self.server_config)
+ conn.initiate_upgrade_connection()
+
+ data = conn.data_to_send()
+ expected_frame = frame_factory.build_settings_frame(
+ settings=conn.local_settings
+ )
+ assert data == expected_frame.serialize()
+
+ def test_can_send_response(self, frame_factory):
+ """
+ After upgrading, we can safely send a response.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_upgrade_connection()
+ c.clear_outbound_data_buffer()
+
+ c.send_headers(stream_id=1, headers=self.example_response_headers)
+ c.send_data(stream_id=1, data=b'some data', end_stream=True)
+
+ f1 = frame_factory.build_headers_frame(
+ stream_id=1,
+ headers=self.example_response_headers,
+ )
+ f2 = frame_factory.build_data_frame(
+ stream_id=1,
+ data=b'some data',
+ flags=['END_STREAM']
+ )
+
+ expected_data = f1.serialize() + f2.serialize()
+ assert c.data_to_send() == expected_data
+
+ def test_can_push_stream(self, frame_factory):
+ """
+ After upgrading, we can safely push a stream.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_upgrade_connection()
+ c.clear_outbound_data_buffer()
+
+ c.push_stream(
+ stream_id=1,
+ promised_stream_id=2,
+ request_headers=self.example_request_headers
+ )
+
+ f = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=self.example_request_headers,
+ )
+ assert c.data_to_send() == f.serialize()
+
+ def test_cannot_receive_headers_stream_1(self, frame_factory):
+ """
+ After upgrading, we cannot receive headers on stream 1.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_upgrade_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ stream_id=1,
+ headers=self.example_request_headers,
+ )
+ c.receive_data(f.serialize())
+
+ expected_frame = frame_factory.build_rst_stream_frame(
+ stream_id=1,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_cannot_receive_data_stream_1(self, frame_factory):
+ """
+ After upgrading, we cannot receive data on stream 1.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_upgrade_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_data_frame(
+ stream_id=1,
+ data=b'some data',
+ )
+ c.receive_data(f.serialize())
+
+ expected = frame_factory.build_rst_stream_frame(
+ stream_id=1,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ ).serialize()
+ assert c.data_to_send() == expected
+
+ def test_client_settings_are_applied(self, frame_factory):
+ """
+ The settings provided by the client are applied and immediately
+ ACK'ed.
+ """
+ server = h2.connection.H2Connection(config=self.server_config)
+ client = h2.connection.H2Connection()
+
+ # As a precaution, let's confirm that the server and client, at the
+ # start of the connection, do not agree on their initial settings
+ # state.
+ assert (
+ client.local_settings != server.remote_settings
+ )
+
+ # Get the client header data and pass it to the server.
+ header_data = client.initiate_upgrade_connection()
+ server.initiate_upgrade_connection(header_data)
+
+ # This gets complex, but here we go.
+ # RFC 7540 § 3.2.1 says that "explicit acknowledgement" of the settings
+ # in the header is "not necessary". That's annoyingly vague, but we
+ # interpret that to mean "should not be sent". So to test that this
+ # worked we need to test that the server has only sent the preamble,
+ # and has not sent a SETTINGS ack, and also that the server has the
+ # correct settings.
+ expected_frame = frame_factory.build_settings_frame(
+ server.local_settings
+ )
+ assert server.data_to_send() == expected_frame.serialize()
+
+ assert (
+ client.local_settings == server.remote_settings
+ )
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_head_request.py b/testing/web-platform/tests/tools/third_party/h2/test/test_head_request.py
new file mode 100644
index 0000000000..ef73007254
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_head_request.py
@@ -0,0 +1,55 @@
+# -*- coding; utf-8 -*-
+"""
+test_head_request
+~~~~~~~~~~~~~~~~~
+"""
+import h2.connection
+import pytest
+
+
+class TestHeadRequest(object):
+ example_request_headers = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'HEAD'),
+ ]
+
+ example_response_headers = [
+ (b':status', b'200'),
+ (b'server', b'fake-serv/0.1.0'),
+ (b'content_length', b'1'),
+ ]
+
+ def test_non_zero_content_and_no_body(self, frame_factory):
+
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+
+ f = frame_factory.build_headers_frame(
+ self.example_response_headers,
+ flags=['END_STREAM']
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 2
+ event = events[0]
+
+ assert isinstance(event, h2.events.ResponseReceived)
+ assert event.stream_id == 1
+ assert event.headers == self.example_response_headers
+
+ def test_reject_non_zero_content_and_body(self, frame_factory):
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers)
+
+ headers = frame_factory.build_headers_frame(
+ self.example_response_headers
+ )
+ data = frame_factory.build_data_frame(data=b'\x01')
+
+ c.receive_data(headers.serialize())
+ with pytest.raises(h2.exceptions.InvalidBodyLengthError):
+ c.receive_data(data.serialize())
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_header_indexing.py b/testing/web-platform/tests/tools/third_party/h2/test/test_header_indexing.py
new file mode 100644
index 0000000000..23fd06f15b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_header_indexing.py
@@ -0,0 +1,637 @@
+# -*- coding: utf-8 -*-
+"""
+test_header_indexing.py
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains tests that use HPACK header tuples that provide additional
+metadata to the hpack module about how to encode the headers.
+"""
+import pytest
+
+from hpack import HeaderTuple, NeverIndexedHeaderTuple
+
+import h2.config
+import h2.connection
+
+
+def assert_header_blocks_actually_equal(block_a, block_b):
+ """
+ Asserts that two header bocks are really, truly equal, down to the types
+ of their tuples. Doesn't return anything.
+ """
+ assert len(block_a) == len(block_b)
+
+ for a, b in zip(block_a, block_b):
+ assert a == b
+ assert a.__class__ is b.__class__
+
+
+class TestHeaderIndexing(object):
+ """
+ Test that Hyper-h2 can correctly handle never indexed header fields using
+ the appropriate hpack data structures.
+ """
+ example_request_headers = [
+ HeaderTuple(u':authority', u'example.com'),
+ HeaderTuple(u':path', u'/'),
+ HeaderTuple(u':scheme', u'https'),
+ HeaderTuple(u':method', u'GET'),
+ ]
+ bytes_example_request_headers = [
+ HeaderTuple(b':authority', b'example.com'),
+ HeaderTuple(b':path', b'/'),
+ HeaderTuple(b':scheme', b'https'),
+ HeaderTuple(b':method', b'GET'),
+ ]
+
+ extended_request_headers = [
+ HeaderTuple(u':authority', u'example.com'),
+ HeaderTuple(u':path', u'/'),
+ HeaderTuple(u':scheme', u'https'),
+ HeaderTuple(u':method', u'GET'),
+ NeverIndexedHeaderTuple(u'authorization', u'realpassword'),
+ ]
+ bytes_extended_request_headers = [
+ HeaderTuple(b':authority', b'example.com'),
+ HeaderTuple(b':path', b'/'),
+ HeaderTuple(b':scheme', b'https'),
+ HeaderTuple(b':method', b'GET'),
+ NeverIndexedHeaderTuple(b'authorization', b'realpassword'),
+ ]
+
+ example_response_headers = [
+ HeaderTuple(u':status', u'200'),
+ HeaderTuple(u'server', u'fake-serv/0.1.0')
+ ]
+ bytes_example_response_headers = [
+ HeaderTuple(b':status', b'200'),
+ HeaderTuple(b'server', b'fake-serv/0.1.0')
+ ]
+
+ extended_response_headers = [
+ HeaderTuple(u':status', u'200'),
+ HeaderTuple(u'server', u'fake-serv/0.1.0'),
+ NeverIndexedHeaderTuple(u'secure', u'you-bet'),
+ ]
+ bytes_extended_response_headers = [
+ HeaderTuple(b':status', b'200'),
+ HeaderTuple(b'server', b'fake-serv/0.1.0'),
+ NeverIndexedHeaderTuple(b'secure', b'you-bet'),
+ ]
+
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ @pytest.mark.parametrize(
+ 'headers', (
+ example_request_headers,
+ bytes_example_request_headers,
+ extended_request_headers,
+ bytes_extended_request_headers,
+ )
+ )
+ def test_sending_header_tuples(self, headers, frame_factory):
+ """
+ Providing HeaderTuple and HeaderTuple subclasses preserves the metadata
+ about indexing.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ # Clear the data, then send headers.
+ c.clear_outbound_data_buffer()
+ c.send_headers(1, headers)
+
+ f = frame_factory.build_headers_frame(headers=headers)
+ assert c.data_to_send() == f.serialize()
+
+ @pytest.mark.parametrize(
+ 'headers', (
+ example_request_headers,
+ bytes_example_request_headers,
+ extended_request_headers,
+ bytes_extended_request_headers,
+ )
+ )
+ def test_header_tuples_in_pushes(self, headers, frame_factory):
+ """
+ Providing HeaderTuple and HeaderTuple subclasses to push promises
+ preserves metadata about indexing.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ # We can use normal headers for the request.
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers
+ )
+ c.receive_data(f.serialize())
+
+ frame_factory.refresh_encoder()
+ expected_frame = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=headers,
+ flags=['END_HEADERS'],
+ )
+
+ c.clear_outbound_data_buffer()
+ c.push_stream(
+ stream_id=1,
+ promised_stream_id=2,
+ request_headers=headers
+ )
+
+ assert c.data_to_send() == expected_frame.serialize()
+
+ @pytest.mark.parametrize(
+ 'headers,encoding', (
+ (example_request_headers, 'utf-8'),
+ (bytes_example_request_headers, None),
+ (extended_request_headers, 'utf-8'),
+ (bytes_extended_request_headers, None),
+ )
+ )
+ def test_header_tuples_are_decoded_request(self,
+ headers,
+ encoding,
+ frame_factory):
+ """
+ The indexing status of the header is preserved when emitting
+ RequestReceived events.
+ """
+ config = h2.config.H2Configuration(
+ client_side=False, header_encoding=encoding
+ )
+ c = h2.connection.H2Connection(config=config)
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(headers)
+ data = f.serialize()
+ events = c.receive_data(data)
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.RequestReceived)
+ assert_header_blocks_actually_equal(headers, event.headers)
+
+ @pytest.mark.parametrize(
+ 'headers,encoding', (
+ (example_response_headers, 'utf-8'),
+ (bytes_example_response_headers, None),
+ (extended_response_headers, 'utf-8'),
+ (bytes_extended_response_headers, None),
+ )
+ )
+ def test_header_tuples_are_decoded_response(self,
+ headers,
+ encoding,
+ frame_factory):
+ """
+ The indexing status of the header is preserved when emitting
+ ResponseReceived events.
+ """
+ config = h2.config.H2Configuration(
+ header_encoding=encoding
+ )
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ f = frame_factory.build_headers_frame(headers)
+ data = f.serialize()
+ events = c.receive_data(data)
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.ResponseReceived)
+ assert_header_blocks_actually_equal(headers, event.headers)
+
+ @pytest.mark.parametrize(
+ 'headers,encoding', (
+ (example_response_headers, 'utf-8'),
+ (bytes_example_response_headers, None),
+ (extended_response_headers, 'utf-8'),
+ (bytes_extended_response_headers, None),
+ )
+ )
+ def test_header_tuples_are_decoded_info_response(self,
+ headers,
+ encoding,
+ frame_factory):
+ """
+ The indexing status of the header is preserved when emitting
+ InformationalResponseReceived events.
+ """
+ # Manipulate the headers to send 100 Continue. We need to copy the list
+ # to avoid breaking the example headers.
+ headers = headers[:]
+ if encoding:
+ headers[0] = HeaderTuple(u':status', u'100')
+ else:
+ headers[0] = HeaderTuple(b':status', b'100')
+
+ config = h2.config.H2Configuration(
+ header_encoding=encoding
+ )
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ f = frame_factory.build_headers_frame(headers)
+ data = f.serialize()
+ events = c.receive_data(data)
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.InformationalResponseReceived)
+ assert_header_blocks_actually_equal(headers, event.headers)
+
+ @pytest.mark.parametrize(
+ 'headers,encoding', (
+ (example_response_headers, 'utf-8'),
+ (bytes_example_response_headers, None),
+ (extended_response_headers, 'utf-8'),
+ (bytes_extended_response_headers, None),
+ )
+ )
+ def test_header_tuples_are_decoded_trailers(self,
+ headers,
+ encoding,
+ frame_factory):
+ """
+ The indexing status of the header is preserved when emitting
+ TrailersReceived events.
+ """
+ # Manipulate the headers to remove the status, which shouldn't be in
+ # the trailers. We need to copy the list to avoid breaking the example
+ # headers.
+ headers = headers[1:]
+
+ config = h2.config.H2Configuration(
+ header_encoding=encoding
+ )
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+ f = frame_factory.build_headers_frame(self.example_response_headers)
+ data = f.serialize()
+ c.receive_data(data)
+
+ f = frame_factory.build_headers_frame(headers, flags=['END_STREAM'])
+ data = f.serialize()
+ events = c.receive_data(data)
+
+ assert len(events) == 2
+ event = events[0]
+
+ assert isinstance(event, h2.events.TrailersReceived)
+ assert_header_blocks_actually_equal(headers, event.headers)
+
+ @pytest.mark.parametrize(
+ 'headers,encoding', (
+ (example_request_headers, 'utf-8'),
+ (bytes_example_request_headers, None),
+ (extended_request_headers, 'utf-8'),
+ (bytes_extended_request_headers, None),
+ )
+ )
+ def test_header_tuples_are_decoded_push_promise(self,
+ headers,
+ encoding,
+ frame_factory):
+ """
+ The indexing status of the header is preserved when emitting
+ PushedStreamReceived events.
+ """
+ config = h2.config.H2Configuration(
+ header_encoding=encoding
+ )
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ f = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=headers,
+ flags=['END_HEADERS'],
+ )
+ data = f.serialize()
+ events = c.receive_data(data)
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.PushedStreamReceived)
+ assert_header_blocks_actually_equal(headers, event.headers)
+
+
+class TestSecureHeaders(object):
+ """
+ Certain headers should always be transformed to their never-indexed form.
+ """
+ example_request_headers = [
+ (u':authority', u'example.com'),
+ (u':path', u'/'),
+ (u':scheme', u'https'),
+ (u':method', u'GET'),
+ ]
+ bytes_example_request_headers = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'GET'),
+ ]
+ possible_auth_headers = [
+ (u'authorization', u'test'),
+ (u'Authorization', u'test'),
+ (u'authorization', u'really long test'),
+ HeaderTuple(u'authorization', u'test'),
+ HeaderTuple(u'Authorization', u'test'),
+ HeaderTuple(u'authorization', u'really long test'),
+ NeverIndexedHeaderTuple(u'authorization', u'test'),
+ NeverIndexedHeaderTuple(u'Authorization', u'test'),
+ NeverIndexedHeaderTuple(u'authorization', u'really long test'),
+ (b'authorization', b'test'),
+ (b'Authorization', b'test'),
+ (b'authorization', b'really long test'),
+ HeaderTuple(b'authorization', b'test'),
+ HeaderTuple(b'Authorization', b'test'),
+ HeaderTuple(b'authorization', b'really long test'),
+ NeverIndexedHeaderTuple(b'authorization', b'test'),
+ NeverIndexedHeaderTuple(b'Authorization', b'test'),
+ NeverIndexedHeaderTuple(b'authorization', b'really long test'),
+ (u'proxy-authorization', u'test'),
+ (u'Proxy-Authorization', u'test'),
+ (u'proxy-authorization', u'really long test'),
+ HeaderTuple(u'proxy-authorization', u'test'),
+ HeaderTuple(u'Proxy-Authorization', u'test'),
+ HeaderTuple(u'proxy-authorization', u'really long test'),
+ NeverIndexedHeaderTuple(u'proxy-authorization', u'test'),
+ NeverIndexedHeaderTuple(u'Proxy-Authorization', u'test'),
+ NeverIndexedHeaderTuple(u'proxy-authorization', u'really long test'),
+ (b'proxy-authorization', b'test'),
+ (b'Proxy-Authorization', b'test'),
+ (b'proxy-authorization', b'really long test'),
+ HeaderTuple(b'proxy-authorization', b'test'),
+ HeaderTuple(b'Proxy-Authorization', b'test'),
+ HeaderTuple(b'proxy-authorization', b'really long test'),
+ NeverIndexedHeaderTuple(b'proxy-authorization', b'test'),
+ NeverIndexedHeaderTuple(b'Proxy-Authorization', b'test'),
+ NeverIndexedHeaderTuple(b'proxy-authorization', b'really long test'),
+ ]
+ secured_cookie_headers = [
+ (u'cookie', u'short'),
+ (u'Cookie', u'short'),
+ (u'cookie', u'nineteen byte cooki'),
+ HeaderTuple(u'cookie', u'short'),
+ HeaderTuple(u'Cookie', u'short'),
+ HeaderTuple(u'cookie', u'nineteen byte cooki'),
+ NeverIndexedHeaderTuple(u'cookie', u'short'),
+ NeverIndexedHeaderTuple(u'Cookie', u'short'),
+ NeverIndexedHeaderTuple(u'cookie', u'nineteen byte cooki'),
+ NeverIndexedHeaderTuple(u'cookie', u'longer manually secured cookie'),
+ (b'cookie', b'short'),
+ (b'Cookie', b'short'),
+ (b'cookie', b'nineteen byte cooki'),
+ HeaderTuple(b'cookie', b'short'),
+ HeaderTuple(b'Cookie', b'short'),
+ HeaderTuple(b'cookie', b'nineteen byte cooki'),
+ NeverIndexedHeaderTuple(b'cookie', b'short'),
+ NeverIndexedHeaderTuple(b'Cookie', b'short'),
+ NeverIndexedHeaderTuple(b'cookie', b'nineteen byte cooki'),
+ NeverIndexedHeaderTuple(b'cookie', b'longer manually secured cookie'),
+ ]
+ unsecured_cookie_headers = [
+ (u'cookie', u'twenty byte cookie!!'),
+ (u'Cookie', u'twenty byte cookie!!'),
+ (u'cookie', u'substantially longer than 20 byte cookie'),
+ HeaderTuple(u'cookie', u'twenty byte cookie!!'),
+ HeaderTuple(u'cookie', u'twenty byte cookie!!'),
+ HeaderTuple(u'Cookie', u'twenty byte cookie!!'),
+ (b'cookie', b'twenty byte cookie!!'),
+ (b'Cookie', b'twenty byte cookie!!'),
+ (b'cookie', b'substantially longer than 20 byte cookie'),
+ HeaderTuple(b'cookie', b'twenty byte cookie!!'),
+ HeaderTuple(b'cookie', b'twenty byte cookie!!'),
+ HeaderTuple(b'Cookie', b'twenty byte cookie!!'),
+ ]
+
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ @pytest.mark.parametrize(
+ 'headers', (example_request_headers, bytes_example_request_headers)
+ )
+ @pytest.mark.parametrize('auth_header', possible_auth_headers)
+ def test_authorization_headers_never_indexed(self,
+ headers,
+ auth_header,
+ frame_factory):
+ """
+ Authorization and Proxy-Authorization headers are always forced to be
+ never-indexed, regardless of their form.
+ """
+ # Regardless of what we send, we expect it to be never indexed.
+ send_headers = headers + [auth_header]
+ expected_headers = headers + [
+ NeverIndexedHeaderTuple(auth_header[0].lower(), auth_header[1])
+ ]
+
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ # Clear the data, then send headers.
+ c.clear_outbound_data_buffer()
+ c.send_headers(1, send_headers)
+
+ f = frame_factory.build_headers_frame(headers=expected_headers)
+ assert c.data_to_send() == f.serialize()
+
+ @pytest.mark.parametrize(
+ 'headers', (example_request_headers, bytes_example_request_headers)
+ )
+ @pytest.mark.parametrize('auth_header', possible_auth_headers)
+ def test_authorization_headers_never_indexed_push(self,
+ headers,
+ auth_header,
+ frame_factory):
+ """
+ Authorization and Proxy-Authorization headers are always forced to be
+ never-indexed, regardless of their form, when pushed by a server.
+ """
+ # Regardless of what we send, we expect it to be never indexed.
+ send_headers = headers + [auth_header]
+ expected_headers = headers + [
+ NeverIndexedHeaderTuple(auth_header[0].lower(), auth_header[1])
+ ]
+
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ # We can use normal headers for the request.
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers
+ )
+ c.receive_data(f.serialize())
+
+ frame_factory.refresh_encoder()
+ expected_frame = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=expected_headers,
+ flags=['END_HEADERS'],
+ )
+
+ c.clear_outbound_data_buffer()
+ c.push_stream(
+ stream_id=1,
+ promised_stream_id=2,
+ request_headers=send_headers
+ )
+
+ assert c.data_to_send() == expected_frame.serialize()
+
+ @pytest.mark.parametrize(
+ 'headers', (example_request_headers, bytes_example_request_headers)
+ )
+ @pytest.mark.parametrize('cookie_header', secured_cookie_headers)
+ def test_short_cookie_headers_never_indexed(self,
+ headers,
+ cookie_header,
+ frame_factory):
+ """
+ Short cookie headers, and cookies provided as NeverIndexedHeaderTuple,
+ are never indexed.
+ """
+ # Regardless of what we send, we expect it to be never indexed.
+ send_headers = headers + [cookie_header]
+ expected_headers = headers + [
+ NeverIndexedHeaderTuple(cookie_header[0].lower(), cookie_header[1])
+ ]
+
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ # Clear the data, then send headers.
+ c.clear_outbound_data_buffer()
+ c.send_headers(1, send_headers)
+
+ f = frame_factory.build_headers_frame(headers=expected_headers)
+ assert c.data_to_send() == f.serialize()
+
+ @pytest.mark.parametrize(
+ 'headers', (example_request_headers, bytes_example_request_headers)
+ )
+ @pytest.mark.parametrize('cookie_header', secured_cookie_headers)
+ def test_short_cookie_headers_never_indexed_push(self,
+ headers,
+ cookie_header,
+ frame_factory):
+ """
+ Short cookie headers, and cookies provided as NeverIndexedHeaderTuple,
+ are never indexed when pushed by servers.
+ """
+ # Regardless of what we send, we expect it to be never indexed.
+ send_headers = headers + [cookie_header]
+ expected_headers = headers + [
+ NeverIndexedHeaderTuple(cookie_header[0].lower(), cookie_header[1])
+ ]
+
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ # We can use normal headers for the request.
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers
+ )
+ c.receive_data(f.serialize())
+
+ frame_factory.refresh_encoder()
+ expected_frame = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=expected_headers,
+ flags=['END_HEADERS'],
+ )
+
+ c.clear_outbound_data_buffer()
+ c.push_stream(
+ stream_id=1,
+ promised_stream_id=2,
+ request_headers=send_headers
+ )
+
+ assert c.data_to_send() == expected_frame.serialize()
+
+ @pytest.mark.parametrize(
+ 'headers', (example_request_headers, bytes_example_request_headers)
+ )
+ @pytest.mark.parametrize('cookie_header', unsecured_cookie_headers)
+ def test_long_cookie_headers_can_be_indexed(self,
+ headers,
+ cookie_header,
+ frame_factory):
+ """
+ Longer cookie headers can be indexed.
+ """
+ # Regardless of what we send, we expect it to be indexed.
+ send_headers = headers + [cookie_header]
+ expected_headers = headers + [
+ HeaderTuple(cookie_header[0].lower(), cookie_header[1])
+ ]
+
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ # Clear the data, then send headers.
+ c.clear_outbound_data_buffer()
+ c.send_headers(1, send_headers)
+
+ f = frame_factory.build_headers_frame(headers=expected_headers)
+ assert c.data_to_send() == f.serialize()
+
+ @pytest.mark.parametrize(
+ 'headers', (example_request_headers, bytes_example_request_headers)
+ )
+ @pytest.mark.parametrize('cookie_header', unsecured_cookie_headers)
+ def test_long_cookie_headers_can_be_indexed_push(self,
+ headers,
+ cookie_header,
+ frame_factory):
+ """
+ Longer cookie headers can be indexed.
+ """
+ # Regardless of what we send, we expect it to be never indexed.
+ send_headers = headers + [cookie_header]
+ expected_headers = headers + [
+ HeaderTuple(cookie_header[0].lower(), cookie_header[1])
+ ]
+
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ # We can use normal headers for the request.
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers
+ )
+ c.receive_data(f.serialize())
+
+ frame_factory.refresh_encoder()
+ expected_frame = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=expected_headers,
+ flags=['END_HEADERS'],
+ )
+
+ c.clear_outbound_data_buffer()
+ c.push_stream(
+ stream_id=1,
+ promised_stream_id=2,
+ request_headers=send_headers
+ )
+
+ assert c.data_to_send() == expected_frame.serialize()
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_informational_responses.py b/testing/web-platform/tests/tools/third_party/h2/test/test_informational_responses.py
new file mode 100644
index 0000000000..e18c44bcb4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_informational_responses.py
@@ -0,0 +1,444 @@
+# -*- coding: utf-8 -*-
+"""
+test_informational_responses
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Tests that validate that hyper-h2 correctly handles informational (1XX)
+responses in its state machine.
+"""
+import pytest
+
+import h2.config
+import h2.connection
+import h2.events
+import h2.exceptions
+
+
+class TestReceivingInformationalResponses(object):
+ """
+ Tests for receiving informational responses.
+ """
+ example_request_headers = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'GET'),
+ (b'expect', b'100-continue'),
+ ]
+ example_informational_headers = [
+ (b':status', b'100'),
+ (b'server', b'fake-serv/0.1.0')
+ ]
+ example_response_headers = [
+ (b':status', b'200'),
+ (b'server', b'fake-serv/0.1.0')
+ ]
+ example_trailers = [
+ (b'trailer', b'you-bet'),
+ ]
+
+ @pytest.mark.parametrize('end_stream', (True, False))
+ def test_single_informational_response(self, frame_factory, end_stream):
+ """
+ When receiving a informational response, the appropriate event is
+ signaled.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_request_headers,
+ end_stream=end_stream
+ )
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_informational_headers,
+ stream_id=1,
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.InformationalResponseReceived)
+ assert event.headers == self.example_informational_headers
+ assert event.stream_id == 1
+
+ @pytest.mark.parametrize('end_stream', (True, False))
+ def test_receiving_multiple_header_blocks(self, frame_factory, end_stream):
+ """
+ At least three header blocks can be received: informational, headers,
+ trailers.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_request_headers,
+ end_stream=end_stream
+ )
+
+ f1 = frame_factory.build_headers_frame(
+ headers=self.example_informational_headers,
+ stream_id=1,
+ )
+ f2 = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ stream_id=1,
+ )
+ f3 = frame_factory.build_headers_frame(
+ headers=self.example_trailers,
+ stream_id=1,
+ flags=['END_STREAM'],
+ )
+ events = c.receive_data(
+ f1.serialize() + f2.serialize() + f3.serialize()
+ )
+
+ assert len(events) == 4
+
+ assert isinstance(events[0], h2.events.InformationalResponseReceived)
+ assert events[0].headers == self.example_informational_headers
+ assert events[0].stream_id == 1
+
+ assert isinstance(events[1], h2.events.ResponseReceived)
+ assert events[1].headers == self.example_response_headers
+ assert events[1].stream_id == 1
+
+ assert isinstance(events[2], h2.events.TrailersReceived)
+ assert events[2].headers == self.example_trailers
+ assert events[2].stream_id == 1
+
+ @pytest.mark.parametrize('end_stream', (True, False))
+ def test_receiving_multiple_informational_responses(self,
+ frame_factory,
+ end_stream):
+ """
+ More than one informational response is allowed.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_request_headers,
+ end_stream=end_stream
+ )
+
+ f1 = frame_factory.build_headers_frame(
+ headers=self.example_informational_headers,
+ stream_id=1,
+ )
+ f2 = frame_factory.build_headers_frame(
+ headers=[(':status', '101')],
+ stream_id=1,
+ )
+ events = c.receive_data(f1.serialize() + f2.serialize())
+
+ assert len(events) == 2
+
+ assert isinstance(events[0], h2.events.InformationalResponseReceived)
+ assert events[0].headers == self.example_informational_headers
+ assert events[0].stream_id == 1
+
+ assert isinstance(events[1], h2.events.InformationalResponseReceived)
+ assert events[1].headers == [(b':status', b'101')]
+ assert events[1].stream_id == 1
+
+ @pytest.mark.parametrize('end_stream', (True, False))
+ def test_receive_provisional_response_with_end_stream(self,
+ frame_factory,
+ end_stream):
+ """
+ Receiving provisional responses with END_STREAM set causes
+ ProtocolErrors.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_request_headers,
+ end_stream=end_stream
+ )
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_informational_headers,
+ stream_id=1,
+ flags=['END_STREAM']
+ )
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ expected = frame_factory.build_goaway_frame(
+ last_stream_id=0,
+ error_code=1,
+ )
+ assert c.data_to_send() == expected.serialize()
+
+ @pytest.mark.parametrize('end_stream', (True, False))
+ def test_receiving_out_of_order_headers(self, frame_factory, end_stream):
+ """
+ When receiving a informational response after the actual response
+ headers we consider it a ProtocolError and raise it.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_request_headers,
+ end_stream=end_stream
+ )
+
+ f1 = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ stream_id=1,
+ )
+ f2 = frame_factory.build_headers_frame(
+ headers=self.example_informational_headers,
+ stream_id=1,
+ )
+ c.receive_data(f1.serialize())
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f2.serialize())
+
+ expected = frame_factory.build_goaway_frame(
+ last_stream_id=0,
+ error_code=1,
+ )
+ assert c.data_to_send() == expected.serialize()
+
+
+class TestSendingInformationalResponses(object):
+ """
+ Tests for sending informational responses.
+ """
+ example_request_headers = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'GET'),
+ (b'expect', b'100-continue'),
+ ]
+ unicode_informational_headers = [
+ (u':status', u'100'),
+ (u'server', u'fake-serv/0.1.0')
+ ]
+ bytes_informational_headers = [
+ (b':status', b'100'),
+ (b'server', b'fake-serv/0.1.0')
+ ]
+ example_response_headers = [
+ (b':status', b'200'),
+ (b'server', b'fake-serv/0.1.0')
+ ]
+ example_trailers = [
+ (b'trailer', b'you-bet'),
+ ]
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ @pytest.mark.parametrize(
+ 'hdrs', (unicode_informational_headers, bytes_informational_headers),
+ )
+ @pytest.mark.parametrize('end_stream', (True, False))
+ def test_single_informational_response(self,
+ frame_factory,
+ hdrs,
+ end_stream):
+ """
+ When sending a informational response, the appropriate frames are
+ emitted.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ flags = ['END_STREAM'] if end_stream else []
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ stream_id=1,
+ flags=flags,
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+ frame_factory.refresh_encoder()
+
+ c.send_headers(
+ stream_id=1,
+ headers=hdrs
+ )
+
+ f = frame_factory.build_headers_frame(
+ headers=hdrs,
+ stream_id=1,
+ )
+ assert c.data_to_send() == f.serialize()
+
+ @pytest.mark.parametrize(
+ 'hdrs', (unicode_informational_headers, bytes_informational_headers),
+ )
+ @pytest.mark.parametrize('end_stream', (True, False))
+ def test_sending_multiple_header_blocks(self,
+ frame_factory,
+ hdrs,
+ end_stream):
+ """
+ At least three header blocks can be sent: informational, headers,
+ trailers.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ flags = ['END_STREAM'] if end_stream else []
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ stream_id=1,
+ flags=flags,
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+ frame_factory.refresh_encoder()
+
+ # Send the three header blocks.
+ c.send_headers(
+ stream_id=1,
+ headers=hdrs
+ )
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_response_headers
+ )
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_trailers,
+ end_stream=True
+ )
+
+ # Check that we sent them properly.
+ f1 = frame_factory.build_headers_frame(
+ headers=hdrs,
+ stream_id=1,
+ )
+ f2 = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ stream_id=1,
+ )
+ f3 = frame_factory.build_headers_frame(
+ headers=self.example_trailers,
+ stream_id=1,
+ flags=['END_STREAM']
+ )
+ assert (
+ c.data_to_send() ==
+ f1.serialize() + f2.serialize() + f3.serialize()
+ )
+
+ @pytest.mark.parametrize(
+ 'hdrs', (unicode_informational_headers, bytes_informational_headers),
+ )
+ @pytest.mark.parametrize('end_stream', (True, False))
+ def test_sending_multiple_informational_responses(self,
+ frame_factory,
+ hdrs,
+ end_stream):
+ """
+ More than one informational response is allowed.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ flags = ['END_STREAM'] if end_stream else []
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ stream_id=1,
+ flags=flags,
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+ frame_factory.refresh_encoder()
+
+ # Send two informational responses.
+ c.send_headers(
+ stream_id=1,
+ headers=hdrs,
+ )
+ c.send_headers(
+ stream_id=1,
+ headers=[(':status', '101')]
+ )
+
+ # Check we sent them both.
+ f1 = frame_factory.build_headers_frame(
+ headers=hdrs,
+ stream_id=1,
+ )
+ f2 = frame_factory.build_headers_frame(
+ headers=[(':status', '101')],
+ stream_id=1,
+ )
+ assert c.data_to_send() == f1.serialize() + f2.serialize()
+
+ @pytest.mark.parametrize(
+ 'hdrs', (unicode_informational_headers, bytes_informational_headers),
+ )
+ @pytest.mark.parametrize('end_stream', (True, False))
+ def test_send_provisional_response_with_end_stream(self,
+ frame_factory,
+ hdrs,
+ end_stream):
+ """
+ Sending provisional responses with END_STREAM set causes
+ ProtocolErrors.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ flags = ['END_STREAM'] if end_stream else []
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ stream_id=1,
+ flags=flags,
+ )
+ c.receive_data(f.serialize())
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.send_headers(
+ stream_id=1,
+ headers=hdrs,
+ end_stream=True,
+ )
+
+ @pytest.mark.parametrize(
+ 'hdrs', (unicode_informational_headers, bytes_informational_headers),
+ )
+ @pytest.mark.parametrize('end_stream', (True, False))
+ def test_reject_sending_out_of_order_headers(self,
+ frame_factory,
+ hdrs,
+ end_stream):
+ """
+ When sending an informational response after the actual response
+ headers we consider it a ProtocolError and raise it.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ flags = ['END_STREAM'] if end_stream else []
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ stream_id=1,
+ flags=flags,
+ )
+ c.receive_data(f.serialize())
+
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_response_headers
+ )
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.send_headers(
+ stream_id=1,
+ headers=hdrs
+ )
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_interacting_stacks.py b/testing/web-platform/tests/tools/third_party/h2/test/test_interacting_stacks.py
new file mode 100644
index 0000000000..90776829c8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_interacting_stacks.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+"""
+test_interacting_stacks
+~~~~~~~~~~~~~~~~~~~~~~~
+
+These tests run two entities, a client and a server, in parallel threads. These
+two entities talk to each other, running what amounts to a number of carefully
+controlled simulations of real flows.
+
+This is to ensure that the stack as a whole behaves intelligently in both
+client and server cases.
+
+These tests are long, complex, and somewhat brittle, so they aren't in general
+recommended for writing the majority of test cases. Their purposes is primarily
+to validate that the top-level API of the library behaves as described.
+
+We should also consider writing helper functions to reduce the complexity of
+these tests, so that they can be written more easily, as they are remarkably
+useful.
+"""
+import coroutine_tests
+
+import h2.config
+import h2.connection
+import h2.events
+import h2.settings
+
+
+class TestCommunication(coroutine_tests.CoroutineTestCase):
+ """
+ Test that two communicating state machines can work together.
+ """
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ def test_basic_request_response(self):
+ """
+ A request issued by hyper-h2 can be responded to by hyper-h2.
+ """
+ request_headers = [
+ (b':method', b'GET'),
+ (b':path', b'/'),
+ (b':authority', b'example.com'),
+ (b':scheme', b'https'),
+ (b'user-agent', b'test-client/0.1.0'),
+ ]
+ response_headers = [
+ (b':status', b'204'),
+ (b'server', b'test-server/0.1.0'),
+ (b'content-length', b'0'),
+ ]
+
+ def client():
+ c = h2.connection.H2Connection()
+
+ # Do the handshake. First send the preamble.
+ c.initiate_connection()
+ data = yield c.data_to_send()
+
+ # Next, handle the remote preamble.
+ events = c.receive_data(data)
+ assert len(events) == 2
+ assert isinstance(events[0], h2.events.SettingsAcknowledged)
+ assert isinstance(events[1], h2.events.RemoteSettingsChanged)
+ changed = events[1].changed_settings
+ assert (
+ changed[
+ h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS
+ ].new_value == 100
+ )
+
+ # Send a request.
+ events = c.send_headers(1, request_headers, end_stream=True)
+ assert not events
+ data = yield c.data_to_send()
+
+ # Validate the response.
+ events = c.receive_data(data)
+ assert len(events) == 2
+ assert isinstance(events[0], h2.events.ResponseReceived)
+ assert events[0].stream_id == 1
+ assert events[0].headers == response_headers
+ assert isinstance(events[1], h2.events.StreamEnded)
+ assert events[1].stream_id == 1
+
+ @self.server
+ def server():
+ c = h2.connection.H2Connection(config=self.server_config)
+
+ # First, read for the preamble.
+ data = yield
+ events = c.receive_data(data)
+ assert len(events) == 1
+ assert isinstance(events[0], h2.events.RemoteSettingsChanged)
+ changed = events[0].changed_settings
+ assert (
+ changed[
+ h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS
+ ].new_value == 100
+ )
+
+ # Send our preamble back.
+ c.initiate_connection()
+ data = yield c.data_to_send()
+
+ # Listen for the request.
+ events = c.receive_data(data)
+ assert len(events) == 3
+ assert isinstance(events[0], h2.events.SettingsAcknowledged)
+ assert isinstance(events[1], h2.events.RequestReceived)
+ assert events[1].stream_id == 1
+ assert events[1].headers == request_headers
+ assert isinstance(events[2], h2.events.StreamEnded)
+ assert events[2].stream_id == 1
+
+ # Send our response.
+ events = c.send_headers(1, response_headers, end_stream=True)
+ assert not events
+ yield c.data_to_send()
+
+ self.run_until_complete(client(), server())
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_invalid_content_lengths.py b/testing/web-platform/tests/tools/third_party/h2/test/test_invalid_content_lengths.py
new file mode 100644
index 0000000000..fe682fcc27
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_invalid_content_lengths.py
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+"""
+test_invalid_content_lengths.py
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains tests that use invalid content lengths, and validates that
+they fail appropriately.
+"""
+import pytest
+
+import h2.config
+import h2.connection
+import h2.errors
+import h2.events
+import h2.exceptions
+
+
+class TestInvalidContentLengths(object):
+ """
+ Hyper-h2 raises Protocol Errors when the content-length sent by a remote
+ peer is not valid.
+ """
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'POST'),
+ ('content-length', '15'),
+ ]
+ example_response_headers = [
+ (':status', '200'),
+ ('server', 'fake-serv/0.1.0')
+ ]
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ def test_too_much_data(self, frame_factory):
+ """
+ Remote peers sending data in excess of content-length causes Protocol
+ Errors.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ headers = frame_factory.build_headers_frame(
+ headers=self.example_request_headers
+ )
+ first_data = frame_factory.build_data_frame(data=b'\x01'*15)
+ c.receive_data(headers.serialize() + first_data.serialize())
+ c.clear_outbound_data_buffer()
+
+ second_data = frame_factory.build_data_frame(data=b'\x01')
+ with pytest.raises(h2.exceptions.InvalidBodyLengthError) as exp:
+ c.receive_data(second_data.serialize())
+
+ assert exp.value.expected_length == 15
+ assert exp.value.actual_length == 16
+ assert str(exp.value) == (
+ "InvalidBodyLengthError: Expected 15 bytes, received 16"
+ )
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=1,
+ error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_insufficient_data(self, frame_factory):
+ """
+ Remote peers sending less data than content-length causes Protocol
+ Errors.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ headers = frame_factory.build_headers_frame(
+ headers=self.example_request_headers
+ )
+ first_data = frame_factory.build_data_frame(data=b'\x01'*13)
+ c.receive_data(headers.serialize() + first_data.serialize())
+ c.clear_outbound_data_buffer()
+
+ second_data = frame_factory.build_data_frame(
+ data=b'\x01',
+ flags=['END_STREAM'],
+ )
+ with pytest.raises(h2.exceptions.InvalidBodyLengthError) as exp:
+ c.receive_data(second_data.serialize())
+
+ assert exp.value.expected_length == 15
+ assert exp.value.actual_length == 14
+ assert str(exp.value) == (
+ "InvalidBodyLengthError: Expected 15 bytes, received 14"
+ )
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=1,
+ error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_insufficient_data_empty_frame(self, frame_factory):
+ """
+ Remote peers sending less data than content-length where the last data
+ frame is empty causes Protocol Errors.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ headers = frame_factory.build_headers_frame(
+ headers=self.example_request_headers
+ )
+ first_data = frame_factory.build_data_frame(data=b'\x01'*14)
+ c.receive_data(headers.serialize() + first_data.serialize())
+ c.clear_outbound_data_buffer()
+
+ second_data = frame_factory.build_data_frame(
+ data=b'',
+ flags=['END_STREAM'],
+ )
+ with pytest.raises(h2.exceptions.InvalidBodyLengthError) as exp:
+ c.receive_data(second_data.serialize())
+
+ assert exp.value.expected_length == 15
+ assert exp.value.actual_length == 14
+ assert str(exp.value) == (
+ "InvalidBodyLengthError: Expected 15 bytes, received 14"
+ )
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=1,
+ error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_invalid_frame_sequences.py b/testing/web-platform/tests/tools/third_party/h2/test/test_invalid_frame_sequences.py
new file mode 100644
index 0000000000..12b70c4a6b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_invalid_frame_sequences.py
@@ -0,0 +1,488 @@
+# -*- coding: utf-8 -*-
+"""
+test_invalid_frame_sequences.py
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains tests that use invalid frame sequences, and validates that
+they fail appropriately.
+"""
+import pytest
+
+import h2.config
+import h2.connection
+import h2.errors
+import h2.events
+import h2.exceptions
+
+
+class TestInvalidFrameSequences(object):
+ """
+ Invalid frame sequences, either sent or received, cause ProtocolErrors to
+ be thrown.
+ """
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ]
+ example_response_headers = [
+ (':status', '200'),
+ ('server', 'fake-serv/0.1.0')
+ ]
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ def test_cannot_send_on_closed_stream(self):
+ """
+ When we've closed a stream locally, we cannot send further data.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.send_data(1, b'some data')
+
+ def test_missing_preamble_errors(self):
+ """
+ Server side connections require the preamble.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ encoded_headers_frame = (
+ b'\x00\x00\r\x01\x04\x00\x00\x00\x01'
+ b'A\x88/\x91\xd3]\x05\\\x87\xa7\x84\x87\x82'
+ )
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(encoded_headers_frame)
+
+ def test_server_connections_reject_even_streams(self, frame_factory):
+ """
+ Servers do not allow clients to initiate even-numbered streams.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers, stream_id=2
+ )
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ def test_clients_reject_odd_stream_pushes(self, frame_factory):
+ """
+ Clients do not allow servers to push odd numbered streams.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(1, self.example_request_headers, end_stream=True)
+
+ f = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ headers=self.example_request_headers,
+ promised_stream_id=3
+ )
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ def test_can_handle_frames_with_invalid_padding(self, frame_factory):
+ """
+ Frames with invalid padding cause connection teardown.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(self.example_request_headers)
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ invalid_data_frame = (
+ b'\x00\x00\x05\x00\x0b\x00\x00\x00\x01\x06\x54\x65\x73\x74'
+ )
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(invalid_data_frame)
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=1, error_code=1
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_receiving_frames_with_insufficent_size(self, frame_factory):
+ """
+ Frames with not enough data cause connection teardown.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ invalid_window_update_frame = (
+ b'\x00\x00\x03\x08\x00\x00\x00\x00\x00\x00\x00\x02'
+ )
+
+ with pytest.raises(h2.exceptions.FrameDataMissingError):
+ c.receive_data(invalid_window_update_frame)
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=0, error_code=h2.errors.ErrorCodes.FRAME_SIZE_ERROR
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_reject_data_on_closed_streams(self, frame_factory):
+ """
+ When a stream is not open to the remote peer, we reject receiving data
+ frames from them.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers,
+ flags=['END_STREAM']
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ bad_frame = frame_factory.build_data_frame(
+ data=b'some data'
+ )
+ c.receive_data(bad_frame.serialize())
+
+ expected = frame_factory.build_rst_stream_frame(
+ stream_id=1,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ ).serialize()
+ assert c.data_to_send() == expected
+
+ def test_unexpected_continuation_on_closed_stream(self, frame_factory):
+ """
+ CONTINUATION frames received on closed streams cause connection errors
+ of type PROTOCOL_ERROR.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers,
+ flags=['END_STREAM']
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ bad_frame = frame_factory.build_continuation_frame(
+ header_block=b'hello'
+ )
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(bad_frame.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
+ last_stream_id=1
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_prevent_continuation_dos(self, frame_factory):
+ """
+ Receiving too many CONTINUATION frames in one block causes a protocol
+ error.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers,
+ )
+ f.flags = {'END_STREAM'}
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ # Send 63 additional frames.
+ for _ in range(0, 63):
+ extra_frame = frame_factory.build_continuation_frame(
+ header_block=b'hello'
+ )
+ c.receive_data(extra_frame.serialize())
+
+ # The final continuation frame should cause a protocol error.
+ extra_frame = frame_factory.build_continuation_frame(
+ header_block=b'hello'
+ )
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(extra_frame.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=0,
+ error_code=0x1,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ # These settings are a bit annoyingly anonymous, but trust me, they're bad.
+ @pytest.mark.parametrize(
+ "settings",
+ [
+ {0x2: 5},
+ {0x4: 2**31},
+ {0x5: 5},
+ {0x5: 2**24},
+ ]
+ )
+ def test_reject_invalid_settings_values(self, frame_factory, settings):
+ """
+ When a SETTINGS frame is received with invalid settings values it
+ causes connection teardown with the appropriate error code.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_settings_frame(settings=settings)
+
+ with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e:
+ c.receive_data(f.serialize())
+
+ assert e.value.error_code == (
+ h2.errors.ErrorCodes.FLOW_CONTROL_ERROR if 0x4 in settings else
+ h2.errors.ErrorCodes.PROTOCOL_ERROR
+ )
+
+ def test_invalid_frame_headers_are_protocol_errors(self, frame_factory):
+ """
+ When invalid frame headers are received they cause ProtocolErrors to be
+ raised.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers
+ )
+
+ # Do some annoying bit twiddling here: the stream ID is currently set
+ # to '1', change it to '0'. Grab the first 9 bytes (the frame header),
+ # replace any instances of the byte '\x01', and then graft it onto the
+ # remaining bytes.
+ frame_data = f.serialize()
+ frame_data = frame_data[:9].replace(b'\x01', b'\x00') + frame_data[9:]
+
+ with pytest.raises(h2.exceptions.ProtocolError) as e:
+ c.receive_data(frame_data)
+
+ assert "Stream ID must be non-zero" in str(e.value)
+
+ def test_get_stream_reset_event_on_auto_reset(self, frame_factory):
+ """
+ When hyper-h2 resets a stream automatically, a StreamReset event fires.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers,
+ flags=['END_STREAM']
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ bad_frame = frame_factory.build_data_frame(
+ data=b'some data'
+ )
+ events = c.receive_data(bad_frame.serialize())
+
+ expected = frame_factory.build_rst_stream_frame(
+ stream_id=1,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ ).serialize()
+ assert c.data_to_send() == expected
+
+ assert len(events) == 1
+ event = events[0]
+ assert isinstance(event, h2.events.StreamReset)
+ assert event.stream_id == 1
+ assert event.error_code == h2.errors.ErrorCodes.STREAM_CLOSED
+ assert not event.remote_reset
+
+ def test_one_one_stream_reset(self, frame_factory):
+ """
+ When hyper-h2 resets a stream automatically, a StreamReset event fires,
+ but only for the first reset: the others are silent.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(
+ self.example_request_headers,
+ flags=['END_STREAM']
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ bad_frame = frame_factory.build_data_frame(
+ data=b'some data'
+ )
+ # Receive 5 frames.
+ events = c.receive_data(bad_frame.serialize() * 5)
+
+ expected = frame_factory.build_rst_stream_frame(
+ stream_id=1,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ ).serialize()
+ assert c.data_to_send() == expected * 5
+
+ assert len(events) == 1
+ event = events[0]
+ assert isinstance(event, h2.events.StreamReset)
+ assert event.stream_id == 1
+ assert event.error_code == h2.errors.ErrorCodes.STREAM_CLOSED
+ assert not event.remote_reset
+
+ @pytest.mark.parametrize('value', ['', 'twelve'])
+ def test_error_on_invalid_content_length(self, frame_factory, value):
+ """
+ When an invalid content-length is received, a ProtocolError is thrown.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ stream_id=1,
+ headers=self.example_request_headers + [('content-length', value)]
+ )
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=1,
+ error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_invalid_header_data_protocol_error(self, frame_factory):
+ """
+ If an invalid header block is received, we raise a ProtocolError.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ stream_id=1,
+ headers=self.example_request_headers
+ )
+ f.data = b'\x00\x00\x00\x00'
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=0,
+ error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_invalid_push_promise_data_protocol_error(self, frame_factory):
+ """
+ If an invalid header block is received on a PUSH_PROMISE, we raise a
+ ProtocolError.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=self.example_request_headers
+ )
+ f.data = b'\x00\x00\x00\x00'
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=0,
+ error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_cannot_receive_push_on_pushed_stream(self, frame_factory):
+ """
+ If a PUSH_PROMISE frame is received with the parent stream ID being a
+ pushed stream, this is rejected with a PROTOCOL_ERROR.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_request_headers,
+ end_stream=True
+ )
+
+ f1 = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=self.example_request_headers,
+ )
+ f2 = frame_factory.build_headers_frame(
+ stream_id=2,
+ headers=self.example_response_headers,
+ )
+ c.receive_data(f1.serialize() + f2.serialize())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_push_promise_frame(
+ stream_id=2,
+ promised_stream_id=4,
+ headers=self.example_request_headers,
+ )
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=2,
+ error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_cannot_send_push_on_pushed_stream(self, frame_factory):
+ """
+ If a user tries to send a PUSH_PROMISE frame with the parent stream ID
+ being a pushed stream, this is rejected with a PROTOCOL_ERROR.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_headers_frame(
+ stream_id=1, headers=self.example_request_headers
+ )
+ c.receive_data(f.serialize())
+
+ c.push_stream(
+ stream_id=1,
+ promised_stream_id=2,
+ request_headers=self.example_request_headers
+ )
+ c.send_headers(stream_id=2, headers=self.example_response_headers)
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.push_stream(
+ stream_id=2,
+ promised_stream_id=4,
+ request_headers=self.example_request_headers
+ )
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_invalid_headers.py b/testing/web-platform/tests/tools/third_party/h2/test/test_invalid_headers.py
new file mode 100644
index 0000000000..a379950733
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_invalid_headers.py
@@ -0,0 +1,952 @@
+# -*- coding: utf-8 -*-
+"""
+test_invalid_headers.py
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains tests that use invalid header blocks, and validates that
+they fail appropriately.
+"""
+import itertools
+
+import pytest
+
+import h2.config
+import h2.connection
+import h2.errors
+import h2.events
+import h2.exceptions
+import h2.settings
+import h2.utilities
+
+import hyperframe.frame
+
+from hypothesis import given
+from hypothesis.strategies import binary, lists, tuples
+
+HEADERS_STRATEGY = lists(tuples(binary(min_size=1), binary()))
+
+
+class TestInvalidFrameSequences(object):
+ """
+ Invalid header sequences cause ProtocolErrors to be thrown when received.
+ """
+ base_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ('user-agent', 'someua/0.0.1'),
+ ]
+ invalid_header_blocks = [
+ base_request_headers + [('Uppercase', 'name')],
+ base_request_headers + [(':late', 'pseudo-header')],
+ [(':path', 'duplicate-pseudo-header')] + base_request_headers,
+ base_request_headers + [('connection', 'close')],
+ base_request_headers + [('proxy-connection', 'close')],
+ base_request_headers + [('keep-alive', 'close')],
+ base_request_headers + [('transfer-encoding', 'gzip')],
+ base_request_headers + [('upgrade', 'super-protocol/1.1')],
+ base_request_headers + [('te', 'chunked')],
+ base_request_headers + [('host', 'notexample.com')],
+ base_request_headers + [(' name', 'name with leading space')],
+ base_request_headers + [('name ', 'name with trailing space')],
+ base_request_headers + [('name', ' value with leading space')],
+ base_request_headers + [('name', 'value with trailing space ')],
+ [header for header in base_request_headers
+ if header[0] != ':authority'],
+ [(':protocol', 'websocket')] + base_request_headers,
+ ]
+ server_config = h2.config.H2Configuration(
+ client_side=False, header_encoding='utf-8'
+ )
+
+ @pytest.mark.parametrize('headers', invalid_header_blocks)
+ def test_headers_event(self, frame_factory, headers):
+ """
+ Test invalid headers are rejected with PROTOCOL_ERROR.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(headers)
+ data = f.serialize()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(data)
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=1, error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ @pytest.mark.parametrize('headers', invalid_header_blocks)
+ def test_push_promise_event(self, frame_factory, headers):
+ """
+ If a PUSH_PROMISE header frame is received with an invalid header block
+ it is rejected with a PROTOCOL_ERROR.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(
+ stream_id=1, headers=self.base_request_headers, end_stream=True
+ )
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=headers
+ )
+ data = f.serialize()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(data)
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=0, error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ @pytest.mark.parametrize('headers', invalid_header_blocks)
+ def test_push_promise_skipping_validation(self, frame_factory, headers):
+ """
+ If we have ``validate_inbound_headers`` disabled, then invalid header
+ blocks in push promise frames are allowed to pass.
+ """
+ config = h2.config.H2Configuration(
+ client_side=True,
+ validate_inbound_headers=False,
+ header_encoding='utf-8'
+ )
+
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.send_headers(
+ stream_id=1, headers=self.base_request_headers, end_stream=True
+ )
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=headers
+ )
+ data = f.serialize()
+
+ events = c.receive_data(data)
+ assert len(events) == 1
+ pp_event = events[0]
+ assert pp_event.headers == headers
+
+ @pytest.mark.parametrize('headers', invalid_header_blocks)
+ def test_headers_event_skipping_validation(self, frame_factory, headers):
+ """
+ If we have ``validate_inbound_headers`` disabled, then all of these
+ invalid header blocks are allowed to pass.
+ """
+ config = h2.config.H2Configuration(
+ client_side=False,
+ validate_inbound_headers=False,
+ header_encoding='utf-8'
+ )
+
+ c = h2.connection.H2Connection(config=config)
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(headers)
+ data = f.serialize()
+
+ events = c.receive_data(data)
+ assert len(events) == 1
+ request_event = events[0]
+ assert request_event.headers == headers
+
+ def test_transfer_encoding_trailers_is_valid(self, frame_factory):
+ """
+ Transfer-Encoding trailers is allowed by the filter.
+ """
+ headers = (
+ self.base_request_headers + [('te', 'trailers')]
+ )
+
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(headers)
+ data = f.serialize()
+
+ events = c.receive_data(data)
+ assert len(events) == 1
+ request_event = events[0]
+ assert request_event.headers == headers
+
+ def test_pseudo_headers_rejected_in_trailer(self, frame_factory):
+ """
+ Ensure we reject pseudo headers included in trailers
+ """
+ trailers = [(':path', '/'), ('extra', 'value')]
+
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ header_frame = frame_factory.build_headers_frame(
+ self.base_request_headers
+ )
+ trailer_frame = frame_factory.build_headers_frame(
+ trailers, flags=["END_STREAM"]
+ )
+ head = header_frame.serialize()
+ trailer = trailer_frame.serialize()
+
+ c.receive_data(head)
+ # Raise exception if pseudo header in trailer
+ with pytest.raises(h2.exceptions.ProtocolError) as e:
+ c.receive_data(trailer)
+ assert "pseudo-header in trailer" in str(e.value)
+
+ # Test appropriate response frame is generated
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=1, error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+
+class TestSendingInvalidFrameSequences(object):
+ """
+ Trying to send invalid header sequences cause ProtocolErrors to
+ be thrown.
+ """
+ base_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ('user-agent', 'someua/0.0.1'),
+ ]
+ invalid_header_blocks = [
+ base_request_headers + [(':late', 'pseudo-header')],
+ [(':path', 'duplicate-pseudo-header')] + base_request_headers,
+ base_request_headers + [('te', 'chunked')],
+ base_request_headers + [('host', 'notexample.com')],
+ [header for header in base_request_headers
+ if header[0] != ':authority'],
+ ]
+ strippable_header_blocks = [
+ base_request_headers + [('connection', 'close')],
+ base_request_headers + [('proxy-connection', 'close')],
+ base_request_headers + [('keep-alive', 'close')],
+ base_request_headers + [('transfer-encoding', 'gzip')],
+ base_request_headers + [('upgrade', 'super-protocol/1.1')]
+ ]
+ all_header_blocks = invalid_header_blocks + strippable_header_blocks
+
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ @pytest.mark.parametrize('headers', invalid_header_blocks)
+ def test_headers_event(self, frame_factory, headers):
+ """
+ Test sending invalid headers raise a ProtocolError.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ # Clear the data, then try to send headers.
+ c.clear_outbound_data_buffer()
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.send_headers(1, headers)
+
+ @pytest.mark.parametrize('headers', invalid_header_blocks)
+ def test_send_push_promise(self, frame_factory, headers):
+ """
+ Sending invalid headers in a push promise raises a ProtocolError.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ header_frame = frame_factory.build_headers_frame(
+ self.base_request_headers
+ )
+ c.receive_data(header_frame.serialize())
+
+ # Clear the data, then try to send a push promise.
+ c.clear_outbound_data_buffer()
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.push_stream(
+ stream_id=1, promised_stream_id=2, request_headers=headers
+ )
+
+ @pytest.mark.parametrize('headers', all_header_blocks)
+ def test_headers_event_skipping_validation(self, frame_factory, headers):
+ """
+ If we have ``validate_outbound_headers`` disabled, then all of these
+ invalid header blocks are allowed to pass.
+ """
+ config = h2.config.H2Configuration(
+ validate_outbound_headers=False
+ )
+
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+
+ # Clear the data, then send headers.
+ c.clear_outbound_data_buffer()
+ c.send_headers(1, headers)
+
+ # Ensure headers are still normalized.
+ norm_headers = h2.utilities.normalize_outbound_headers(headers, None)
+ f = frame_factory.build_headers_frame(norm_headers)
+ assert c.data_to_send() == f.serialize()
+
+ @pytest.mark.parametrize('headers', all_header_blocks)
+ def test_push_promise_skipping_validation(self, frame_factory, headers):
+ """
+ If we have ``validate_outbound_headers`` disabled, then all of these
+ invalid header blocks are allowed to pass.
+ """
+ config = h2.config.H2Configuration(
+ client_side=False,
+ validate_outbound_headers=False,
+ )
+
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ header_frame = frame_factory.build_headers_frame(
+ self.base_request_headers
+ )
+ c.receive_data(header_frame.serialize())
+
+ # Create push promise frame with normalized headers.
+ frame_factory.refresh_encoder()
+ norm_headers = h2.utilities.normalize_outbound_headers(headers, None)
+ pp_frame = frame_factory.build_push_promise_frame(
+ stream_id=1, promised_stream_id=2, headers=norm_headers
+ )
+
+ # Clear the data, then send a push promise.
+ c.clear_outbound_data_buffer()
+ c.push_stream(
+ stream_id=1, promised_stream_id=2, request_headers=headers
+ )
+ assert c.data_to_send() == pp_frame.serialize()
+
+ @pytest.mark.parametrize('headers', all_header_blocks)
+ def test_headers_event_skip_normalization(self, frame_factory, headers):
+ """
+ If we have ``normalize_outbound_headers`` disabled, then all of these
+ invalid header blocks are sent through unmodified.
+ """
+ config = h2.config.H2Configuration(
+ validate_outbound_headers=False,
+ normalize_outbound_headers=False
+ )
+
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+
+ f = frame_factory.build_headers_frame(
+ headers,
+ stream_id=1,
+ )
+
+ # Clear the data, then send headers.
+ c.clear_outbound_data_buffer()
+ c.send_headers(1, headers)
+ assert c.data_to_send() == f.serialize()
+
+ @pytest.mark.parametrize('headers', all_header_blocks)
+ def test_push_promise_skip_normalization(self, frame_factory, headers):
+ """
+ If we have ``normalize_outbound_headers`` disabled, then all of these
+ invalid header blocks are allowed to pass unmodified.
+ """
+ config = h2.config.H2Configuration(
+ client_side=False,
+ validate_outbound_headers=False,
+ normalize_outbound_headers=False,
+ )
+
+ c = h2.connection.H2Connection(config=config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ header_frame = frame_factory.build_headers_frame(
+ self.base_request_headers
+ )
+ c.receive_data(header_frame.serialize())
+
+ frame_factory.refresh_encoder()
+ pp_frame = frame_factory.build_push_promise_frame(
+ stream_id=1, promised_stream_id=2, headers=headers
+ )
+
+ # Clear the data, then send a push promise.
+ c.clear_outbound_data_buffer()
+ c.push_stream(
+ stream_id=1, promised_stream_id=2, request_headers=headers
+ )
+ assert c.data_to_send() == pp_frame.serialize()
+
+ @pytest.mark.parametrize('headers', strippable_header_blocks)
+ def test_strippable_headers(self, frame_factory, headers):
+ """
+ Test connection related headers are removed before sending.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ # Clear the data, then try to send headers.
+ c.clear_outbound_data_buffer()
+ c.send_headers(1, headers)
+
+ f = frame_factory.build_headers_frame(self.base_request_headers)
+ assert c.data_to_send() == f.serialize()
+
+
+class TestFilter(object):
+ """
+ Test the filter function directly.
+
+ These tests exists to confirm the behaviour of the filter function in a
+ wide range of scenarios. Many of these scenarios may not be legal for
+ HTTP/2 and so may never hit the function, but it's worth validating that it
+ behaves as expected anyway.
+ """
+ validation_functions = [
+ h2.utilities.validate_headers,
+ h2.utilities.validate_outbound_headers
+ ]
+
+ hdr_validation_combos = [
+ h2.utilities.HeaderValidationFlags(
+ is_client, is_trailer, is_response_header, is_push_promise
+ )
+ for is_client, is_trailer, is_response_header, is_push_promise in (
+ itertools.product([True, False], repeat=4)
+ )
+ ]
+
+ hdr_validation_response_headers = [
+ flags for flags in hdr_validation_combos
+ if flags.is_response_header
+ ]
+
+ hdr_validation_request_headers_no_trailer = [
+ flags for flags in hdr_validation_combos
+ if not (flags.is_trailer or flags.is_response_header)
+ ]
+
+ invalid_request_header_blocks_bytes = (
+ # First, missing :method
+ (
+ (b':authority', b'google.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ ),
+ # Next, missing :path
+ (
+ (b':authority', b'google.com'),
+ (b':method', b'GET'),
+ (b':scheme', b'https'),
+ ),
+ # Next, missing :scheme
+ (
+ (b':authority', b'google.com'),
+ (b':method', b'GET'),
+ (b':path', b'/'),
+ ),
+ # Finally, path present but empty.
+ (
+ (b':authority', b'google.com'),
+ (b':method', b'GET'),
+ (b':scheme', b'https'),
+ (b':path', b''),
+ ),
+ )
+ invalid_request_header_blocks_unicode = (
+ # First, missing :method
+ (
+ (u':authority', u'google.com'),
+ (u':path', u'/'),
+ (u':scheme', u'https'),
+ ),
+ # Next, missing :path
+ (
+ (u':authority', u'google.com'),
+ (u':method', u'GET'),
+ (u':scheme', u'https'),
+ ),
+ # Next, missing :scheme
+ (
+ (u':authority', u'google.com'),
+ (u':method', u'GET'),
+ (u':path', u'/'),
+ ),
+ # Finally, path present but empty.
+ (
+ (u':authority', u'google.com'),
+ (u':method', u'GET'),
+ (u':scheme', u'https'),
+ (u':path', u''),
+ ),
+ )
+
+ # All headers that are forbidden from either request or response blocks.
+ forbidden_request_headers_bytes = (b':status',)
+ forbidden_request_headers_unicode = (u':status',)
+ forbidden_response_headers_bytes = (
+ b':path', b':scheme', b':authority', b':method'
+ )
+ forbidden_response_headers_unicode = (
+ u':path', u':scheme', u':authority', u':method'
+ )
+
+ @pytest.mark.parametrize('validation_function', validation_functions)
+ @pytest.mark.parametrize('hdr_validation_flags', hdr_validation_combos)
+ @given(headers=HEADERS_STRATEGY)
+ def test_range_of_acceptable_outputs(self,
+ headers,
+ validation_function,
+ hdr_validation_flags):
+ """
+ The header validation functions either return the data unchanged
+ or throw a ProtocolError.
+ """
+ try:
+ assert headers == list(validation_function(
+ headers, hdr_validation_flags))
+ except h2.exceptions.ProtocolError:
+ assert True
+
+ @pytest.mark.parametrize('hdr_validation_flags', hdr_validation_combos)
+ def test_invalid_pseudo_headers(self, hdr_validation_flags):
+ headers = [(b':custom', b'value')]
+ with pytest.raises(h2.exceptions.ProtocolError):
+ list(h2.utilities.validate_headers(headers, hdr_validation_flags))
+
+ @pytest.mark.parametrize('validation_function', validation_functions)
+ @pytest.mark.parametrize(
+ 'hdr_validation_flags', hdr_validation_request_headers_no_trailer
+ )
+ def test_matching_authority_host_headers(self,
+ validation_function,
+ hdr_validation_flags):
+ """
+ If a header block has :authority and Host headers and they match,
+ the headers should pass through unchanged.
+ """
+ headers = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'GET'),
+ (b'host', b'example.com'),
+ ]
+ assert headers == list(h2.utilities.validate_headers(
+ headers, hdr_validation_flags
+ ))
+
+ @pytest.mark.parametrize(
+ 'hdr_validation_flags', hdr_validation_response_headers
+ )
+ def test_response_header_without_status(self, hdr_validation_flags):
+ headers = [(b'content-length', b'42')]
+ with pytest.raises(h2.exceptions.ProtocolError):
+ list(h2.utilities.validate_headers(headers, hdr_validation_flags))
+
+ @pytest.mark.parametrize(
+ 'hdr_validation_flags', hdr_validation_request_headers_no_trailer
+ )
+ @pytest.mark.parametrize(
+ 'header_block',
+ (
+ invalid_request_header_blocks_bytes +
+ invalid_request_header_blocks_unicode
+ )
+ )
+ def test_outbound_req_header_missing_pseudo_headers(self,
+ hdr_validation_flags,
+ header_block):
+ with pytest.raises(h2.exceptions.ProtocolError):
+ list(
+ h2.utilities.validate_outbound_headers(
+ header_block, hdr_validation_flags
+ )
+ )
+
+ @pytest.mark.parametrize(
+ 'hdr_validation_flags', hdr_validation_request_headers_no_trailer
+ )
+ @pytest.mark.parametrize(
+ 'header_block', invalid_request_header_blocks_bytes
+ )
+ def test_inbound_req_header_missing_pseudo_headers(self,
+ hdr_validation_flags,
+ header_block):
+ with pytest.raises(h2.exceptions.ProtocolError):
+ list(
+ h2.utilities.validate_headers(
+ header_block, hdr_validation_flags
+ )
+ )
+
+ @pytest.mark.parametrize(
+ 'hdr_validation_flags', hdr_validation_request_headers_no_trailer
+ )
+ @pytest.mark.parametrize(
+ 'invalid_header',
+ forbidden_request_headers_bytes + forbidden_request_headers_unicode
+ )
+ def test_outbound_req_header_extra_pseudo_headers(self,
+ hdr_validation_flags,
+ invalid_header):
+ """
+ Outbound request header blocks containing the forbidden request headers
+ fail validation.
+ """
+ headers = [
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':authority', b'google.com'),
+ (b':method', b'GET'),
+ ]
+ headers.append((invalid_header, b'some value'))
+ with pytest.raises(h2.exceptions.ProtocolError):
+ list(
+ h2.utilities.validate_outbound_headers(
+ headers, hdr_validation_flags
+ )
+ )
+
+ @pytest.mark.parametrize(
+ 'hdr_validation_flags', hdr_validation_request_headers_no_trailer
+ )
+ @pytest.mark.parametrize(
+ 'invalid_header',
+ forbidden_request_headers_bytes
+ )
+ def test_inbound_req_header_extra_pseudo_headers(self,
+ hdr_validation_flags,
+ invalid_header):
+ """
+ Inbound request header blocks containing the forbidden request headers
+ fail validation.
+ """
+ headers = [
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':authority', b'google.com'),
+ (b':method', b'GET'),
+ ]
+ headers.append((invalid_header, b'some value'))
+ with pytest.raises(h2.exceptions.ProtocolError):
+ list(h2.utilities.validate_headers(headers, hdr_validation_flags))
+
+ @pytest.mark.parametrize(
+ 'hdr_validation_flags', hdr_validation_response_headers
+ )
+ @pytest.mark.parametrize(
+ 'invalid_header',
+ forbidden_response_headers_bytes + forbidden_response_headers_unicode
+ )
+ def test_outbound_resp_header_extra_pseudo_headers(self,
+ hdr_validation_flags,
+ invalid_header):
+ """
+ Outbound response header blocks containing the forbidden response
+ headers fail validation.
+ """
+ headers = [(b':status', b'200')]
+ headers.append((invalid_header, b'some value'))
+ with pytest.raises(h2.exceptions.ProtocolError):
+ list(
+ h2.utilities.validate_outbound_headers(
+ headers, hdr_validation_flags
+ )
+ )
+
+ @pytest.mark.parametrize(
+ 'hdr_validation_flags', hdr_validation_response_headers
+ )
+ @pytest.mark.parametrize(
+ 'invalid_header',
+ forbidden_response_headers_bytes
+ )
+ def test_inbound_resp_header_extra_pseudo_headers(self,
+ hdr_validation_flags,
+ invalid_header):
+ """
+ Inbound response header blocks containing the forbidden response
+ headers fail validation.
+ """
+ headers = [(b':status', b'200')]
+ headers.append((invalid_header, b'some value'))
+ with pytest.raises(h2.exceptions.ProtocolError):
+ list(h2.utilities.validate_headers(headers, hdr_validation_flags))
+
+
+class TestOversizedHeaders(object):
+ """
+ Tests that oversized header blocks are correctly rejected. This replicates
+ the "HPACK Bomb" attack, and confirms that we're resistant against it.
+ """
+ request_header_block = [
+ (b':method', b'GET'),
+ (b':authority', b'example.com'),
+ (b':scheme', b'https'),
+ (b':path', b'/'),
+ ]
+
+ response_header_block = [
+ (b':status', b'200'),
+ ]
+
+ # The first header block contains a single header that fills the header
+ # table. To do that, we'll give it a single-character header name and a
+ # 4063 byte header value. This will make it exactly the size of the header
+ # table. It must come last, so that it evicts all other headers.
+ # This block must be appended to either a request or response block.
+ first_header_block = [
+ (b'a', b'a' * 4063),
+ ]
+
+ # The second header "block" is actually a custom HEADERS frame body that
+ # simply repeatedly refers to the first entry for 16kB. Each byte has the
+ # high bit set (0x80), and then uses the remaining 7 bits to encode the
+ # number 62 (0x3e), leading to a repeat of the byte 0xbe.
+ second_header_block = b'\xbe' * 2**14
+
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ def test_hpack_bomb_request(self, frame_factory):
+ """
+ A HPACK bomb request causes the connection to be torn down with the
+ error code ENHANCE_YOUR_CALM.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ self.request_header_block + self.first_header_block
+ )
+ data = f.serialize()
+ c.receive_data(data)
+
+ # Build the attack payload.
+ attack_frame = hyperframe.frame.HeadersFrame(stream_id=3)
+ attack_frame.data = self.second_header_block
+ attack_frame.flags.add('END_HEADERS')
+ data = attack_frame.serialize()
+
+ with pytest.raises(h2.exceptions.DenialOfServiceError):
+ c.receive_data(data)
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=1, error_code=h2.errors.ErrorCodes.ENHANCE_YOUR_CALM
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_hpack_bomb_response(self, frame_factory):
+ """
+ A HPACK bomb response causes the connection to be torn down with the
+ error code ENHANCE_YOUR_CALM.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(
+ stream_id=1, headers=self.request_header_block
+ )
+ c.send_headers(
+ stream_id=3, headers=self.request_header_block
+ )
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ self.response_header_block + self.first_header_block
+ )
+ data = f.serialize()
+ c.receive_data(data)
+
+ # Build the attack payload.
+ attack_frame = hyperframe.frame.HeadersFrame(stream_id=3)
+ attack_frame.data = self.second_header_block
+ attack_frame.flags.add('END_HEADERS')
+ data = attack_frame.serialize()
+
+ with pytest.raises(h2.exceptions.DenialOfServiceError):
+ c.receive_data(data)
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=0, error_code=h2.errors.ErrorCodes.ENHANCE_YOUR_CALM
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_hpack_bomb_push(self, frame_factory):
+ """
+ A HPACK bomb push causes the connection to be torn down with the
+ error code ENHANCE_YOUR_CALM.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(
+ stream_id=1, headers=self.request_header_block
+ )
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ self.response_header_block + self.first_header_block
+ )
+ data = f.serialize()
+ c.receive_data(data)
+
+ # Build the attack payload. We need to shrink it by four bytes because
+ # the promised_stream_id consumes four bytes of body.
+ attack_frame = hyperframe.frame.PushPromiseFrame(stream_id=3)
+ attack_frame.promised_stream_id = 2
+ attack_frame.data = self.second_header_block[:-4]
+ attack_frame.flags.add('END_HEADERS')
+ data = attack_frame.serialize()
+
+ with pytest.raises(h2.exceptions.DenialOfServiceError):
+ c.receive_data(data)
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=0, error_code=h2.errors.ErrorCodes.ENHANCE_YOUR_CALM
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_reject_headers_when_list_size_shrunk(self, frame_factory):
+ """
+ When we've shrunk the header list size, we reject new header blocks
+ that violate the new size.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ # Receive the first request, which causes no problem.
+ f = frame_factory.build_headers_frame(
+ stream_id=1,
+ headers=self.request_header_block
+ )
+ data = f.serialize()
+ c.receive_data(data)
+
+ # Now, send a settings change. It's un-ACKed at this time. A new
+ # request arrives, also without incident.
+ c.update_settings({h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: 50})
+ c.clear_outbound_data_buffer()
+ f = frame_factory.build_headers_frame(
+ stream_id=3,
+ headers=self.request_header_block
+ )
+ data = f.serialize()
+ c.receive_data(data)
+
+ # We get a SETTINGS ACK.
+ f = frame_factory.build_settings_frame({}, ack=True)
+ data = f.serialize()
+ c.receive_data(data)
+
+ # Now a third request comes in. This explodes.
+ f = frame_factory.build_headers_frame(
+ stream_id=5,
+ headers=self.request_header_block
+ )
+ data = f.serialize()
+
+ with pytest.raises(h2.exceptions.DenialOfServiceError):
+ c.receive_data(data)
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=3, error_code=h2.errors.ErrorCodes.ENHANCE_YOUR_CALM
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_reject_headers_when_table_size_shrunk(self, frame_factory):
+ """
+ When we've shrunk the header table size, we reject header blocks that
+ do not respect the change.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ # Receive the first request, which causes no problem.
+ f = frame_factory.build_headers_frame(
+ stream_id=1,
+ headers=self.request_header_block
+ )
+ data = f.serialize()
+ c.receive_data(data)
+
+ # Now, send a settings change. It's un-ACKed at this time. A new
+ # request arrives, also without incident.
+ c.update_settings({h2.settings.SettingCodes.HEADER_TABLE_SIZE: 128})
+ c.clear_outbound_data_buffer()
+ f = frame_factory.build_headers_frame(
+ stream_id=3,
+ headers=self.request_header_block
+ )
+ data = f.serialize()
+ c.receive_data(data)
+
+ # We get a SETTINGS ACK.
+ f = frame_factory.build_settings_frame({}, ack=True)
+ data = f.serialize()
+ c.receive_data(data)
+
+ # Now a third request comes in. This explodes, as it does not contain
+ # a dynamic table size update.
+ f = frame_factory.build_headers_frame(
+ stream_id=5,
+ headers=self.request_header_block
+ )
+ data = f.serialize()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(data)
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=3, error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ def test_reject_headers_exceeding_table_size(self, frame_factory):
+ """
+ When the remote peer sends a dynamic table size update that exceeds our
+ setting, we reject it.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ # Receive the first request, which causes no problem.
+ f = frame_factory.build_headers_frame(
+ stream_id=1,
+ headers=self.request_header_block
+ )
+ data = f.serialize()
+ c.receive_data(data)
+
+ # Now a second request comes in that sets the table size too high.
+ # This explodes.
+ frame_factory.change_table_size(c.local_settings.header_table_size + 1)
+ f = frame_factory.build_headers_frame(
+ stream_id=5,
+ headers=self.request_header_block
+ )
+ data = f.serialize()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(data)
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=1, error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR
+ )
+ assert c.data_to_send() == expected_frame.serialize()
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_priority.py b/testing/web-platform/tests/tools/third_party/h2/test/test_priority.py
new file mode 100644
index 0000000000..cbc7332253
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_priority.py
@@ -0,0 +1,358 @@
+# -*- coding: utf-8 -*-
+"""
+test_priority
+~~~~~~~~~~~~~
+
+Test the priority logic of Hyper-h2.
+"""
+import pytest
+
+import h2.config
+import h2.connection
+import h2.errors
+import h2.events
+import h2.exceptions
+import h2.stream
+
+
+class TestPriority(object):
+ """
+ Basic priority tests.
+ """
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ]
+ example_response_headers = [
+ (':status', '200'),
+ ('server', 'pytest-h2'),
+ ]
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ def test_receiving_priority_emits_priority_update(self, frame_factory):
+ """
+ Receiving a priority frame emits a PriorityUpdated event.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_priority_frame(
+ stream_id=1,
+ weight=255,
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ assert not c.data_to_send()
+
+ event = events[0]
+ assert isinstance(event, h2.events.PriorityUpdated)
+ assert event.stream_id == 1
+ assert event.depends_on == 0
+ assert event.weight == 256
+ assert event.exclusive is False
+
+ def test_headers_with_priority_info(self, frame_factory):
+ """
+ Receiving a HEADERS frame with priority information on it emits a
+ PriorityUpdated event.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ stream_id=3,
+ flags=['PRIORITY'],
+ stream_weight=15,
+ depends_on=1,
+ exclusive=True,
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 2
+ assert not c.data_to_send()
+
+ event = events[1]
+ assert isinstance(event, h2.events.PriorityUpdated)
+ assert event.stream_id == 3
+ assert event.depends_on == 1
+ assert event.weight == 16
+ assert event.exclusive is True
+
+ def test_streams_may_not_depend_on_themselves(self, frame_factory):
+ """
+ A stream adjusted to depend on itself causes a Protocol Error.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ stream_id=3,
+ flags=['PRIORITY'],
+ stream_weight=15,
+ depends_on=1,
+ exclusive=True,
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_priority_frame(
+ stream_id=3,
+ depends_on=3,
+ weight=15
+ )
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.receive_data(f.serialize())
+
+ expected_frame = frame_factory.build_goaway_frame(
+ last_stream_id=3,
+ error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
+ )
+ assert c.data_to_send() == expected_frame.serialize()
+
+ @pytest.mark.parametrize(
+ 'depends_on,weight,exclusive',
+ [
+ (0, 256, False),
+ (3, 128, False),
+ (3, 128, True),
+ ]
+ )
+ def test_can_prioritize_stream(self, depends_on, weight, exclusive,
+ frame_factory):
+ """
+ hyper-h2 can emit priority frames.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ c.send_headers(headers=self.example_request_headers, stream_id=1)
+ c.send_headers(headers=self.example_request_headers, stream_id=3)
+ c.clear_outbound_data_buffer()
+
+ c.prioritize(
+ stream_id=1,
+ depends_on=depends_on,
+ weight=weight,
+ exclusive=exclusive
+ )
+
+ f = frame_factory.build_priority_frame(
+ stream_id=1,
+ weight=weight - 1,
+ depends_on=depends_on,
+ exclusive=exclusive,
+ )
+ assert c.data_to_send() == f.serialize()
+
+ @pytest.mark.parametrize(
+ 'depends_on,weight,exclusive',
+ [
+ (0, 256, False),
+ (1, 128, False),
+ (1, 128, True),
+ ]
+ )
+ def test_emit_headers_with_priority_info(self, depends_on, weight,
+ exclusive, frame_factory):
+ """
+ It is possible to send a headers frame with priority information on
+ it.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.clear_outbound_data_buffer()
+
+ c.send_headers(
+ headers=self.example_request_headers,
+ stream_id=3,
+ priority_weight=weight,
+ priority_depends_on=depends_on,
+ priority_exclusive=exclusive,
+ )
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ stream_id=3,
+ flags=['PRIORITY'],
+ stream_weight=weight - 1,
+ depends_on=depends_on,
+ exclusive=exclusive,
+ )
+ assert c.data_to_send() == f.serialize()
+
+ def test_may_not_prioritize_stream_to_depend_on_self(self, frame_factory):
+ """
+ A stream adjusted to depend on itself causes a Protocol Error.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.send_headers(
+ headers=self.example_request_headers,
+ stream_id=3,
+ priority_weight=255,
+ priority_depends_on=0,
+ priority_exclusive=False,
+ )
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.prioritize(
+ stream_id=3,
+ depends_on=3,
+ )
+
+ assert not c.data_to_send()
+
+ def test_may_not_initially_set_stream_depend_on_self(self, frame_factory):
+ """
+ A stream that starts by depending on itself causes a Protocol Error.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.send_headers(
+ headers=self.example_request_headers,
+ stream_id=3,
+ priority_depends_on=3,
+ )
+
+ assert not c.data_to_send()
+
+ @pytest.mark.parametrize('weight', [0, -15, 257])
+ def test_prioritize_requires_valid_weight(self, weight):
+ """
+ A call to prioritize with an invalid weight causes a ProtocolError.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.prioritize(stream_id=1, weight=weight)
+
+ assert not c.data_to_send()
+
+ @pytest.mark.parametrize('weight', [0, -15, 257])
+ def test_send_headers_requires_valid_weight(self, weight):
+ """
+ A call to send_headers with an invalid weight causes a ProtocolError.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_request_headers,
+ priority_weight=weight
+ )
+
+ assert not c.data_to_send()
+
+ def test_prioritize_defaults(self, frame_factory):
+ """
+ When prioritize() is called with no explicit arguments, it emits a
+ weight of 16, depending on stream zero non-exclusively.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.clear_outbound_data_buffer()
+
+ c.prioritize(stream_id=1)
+
+ f = frame_factory.build_priority_frame(
+ stream_id=1,
+ weight=15,
+ depends_on=0,
+ exclusive=False,
+ )
+ assert c.data_to_send() == f.serialize()
+
+ @pytest.mark.parametrize(
+ 'priority_kwargs',
+ [
+ {'priority_weight': 16},
+ {'priority_depends_on': 0},
+ {'priority_exclusive': False},
+ ]
+ )
+ def test_send_headers_defaults(self, priority_kwargs, frame_factory):
+ """
+ When send_headers() is called with only one explicit argument, it emits
+ default values for everything else.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.clear_outbound_data_buffer()
+
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_request_headers,
+ **priority_kwargs
+ )
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ stream_id=1,
+ flags=['PRIORITY'],
+ stream_weight=15,
+ depends_on=0,
+ exclusive=False,
+ )
+ assert c.data_to_send() == f.serialize()
+
+ def test_servers_cannot_prioritize(self, frame_factory):
+ """
+ Server stacks are not allowed to call ``prioritize()``.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ stream_id=1,
+ headers=self.example_request_headers,
+ )
+ c.receive_data(f.serialize())
+
+ with pytest.raises(h2.exceptions.RFC1122Error):
+ c.prioritize(stream_id=1)
+
+ def test_servers_cannot_prioritize_with_headers(self, frame_factory):
+ """
+ Server stacks are not allowed to prioritize on headers either.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ stream_id=1,
+ headers=self.example_request_headers,
+ )
+ c.receive_data(f.serialize())
+
+ with pytest.raises(h2.exceptions.RFC1122Error):
+ c.send_headers(
+ stream_id=1,
+ headers=self.example_response_headers,
+ priority_weight=16,
+ )
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_related_events.py b/testing/web-platform/tests/tools/third_party/h2/test/test_related_events.py
new file mode 100644
index 0000000000..eb6b878905
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_related_events.py
@@ -0,0 +1,370 @@
+# -*- coding: utf-8 -*-
+"""
+test_related_events.py
+~~~~~~~~~~~~~~~~~~~~~~
+
+Specific tests to validate the "related events" logic used by certain events
+inside hyper-h2.
+"""
+import h2.config
+import h2.connection
+import h2.events
+
+
+class TestRelatedEvents(object):
+ """
+ Related events correlate all those events that happen on a single frame.
+ """
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ]
+
+ example_response_headers = [
+ (':status', '200'),
+ ('server', 'fake-serv/0.1.0')
+ ]
+
+ informational_response_headers = [
+ (':status', '100'),
+ ('server', 'fake-serv/0.1.0')
+ ]
+
+ example_trailers = [
+ ('another', 'field'),
+ ]
+
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ def test_request_received_related_all(self, frame_factory):
+ """
+ RequestReceived has two possible related events: PriorityUpdated and
+ StreamEnded, all fired when a single HEADERS frame is received.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ input_frame = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ flags=['END_STREAM', 'PRIORITY'],
+ stream_weight=15,
+ depends_on=0,
+ exclusive=False,
+ )
+ events = c.receive_data(input_frame.serialize())
+
+ assert len(events) == 3
+ base_event = events[0]
+ other_events = events[1:]
+
+ assert base_event.stream_ended in other_events
+ assert isinstance(base_event.stream_ended, h2.events.StreamEnded)
+ assert base_event.priority_updated in other_events
+ assert isinstance(
+ base_event.priority_updated, h2.events.PriorityUpdated
+ )
+
+ def test_request_received_related_priority(self, frame_factory):
+ """
+ RequestReceived can be related to PriorityUpdated.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ input_frame = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ flags=['PRIORITY'],
+ stream_weight=15,
+ depends_on=0,
+ exclusive=False,
+ )
+ events = c.receive_data(input_frame.serialize())
+
+ assert len(events) == 2
+ base_event = events[0]
+ priority_updated_event = events[1]
+
+ assert base_event.priority_updated is priority_updated_event
+ assert base_event.stream_ended is None
+ assert isinstance(
+ base_event.priority_updated, h2.events.PriorityUpdated
+ )
+
+ def test_request_received_related_stream_ended(self, frame_factory):
+ """
+ RequestReceived can be related to StreamEnded.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ input_frame = frame_factory.build_headers_frame(
+ headers=self.example_request_headers,
+ flags=['END_STREAM'],
+ )
+ events = c.receive_data(input_frame.serialize())
+
+ assert len(events) == 2
+ base_event = events[0]
+ stream_ended_event = events[1]
+
+ assert base_event.stream_ended is stream_ended_event
+ assert base_event.priority_updated is None
+ assert isinstance(base_event.stream_ended, h2.events.StreamEnded)
+
+ def test_response_received_related_nothing(self, frame_factory):
+ """
+ ResponseReceived is ordinarily related to no events.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ input_frame = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ )
+ events = c.receive_data(input_frame.serialize())
+
+ assert len(events) == 1
+ base_event = events[0]
+
+ assert base_event.stream_ended is None
+ assert base_event.priority_updated is None
+
+ def test_response_received_related_all(self, frame_factory):
+ """
+ ResponseReceived has two possible related events: PriorityUpdated and
+ StreamEnded, all fired when a single HEADERS frame is received.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ input_frame = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ flags=['END_STREAM', 'PRIORITY'],
+ stream_weight=15,
+ depends_on=0,
+ exclusive=False,
+ )
+ events = c.receive_data(input_frame.serialize())
+
+ assert len(events) == 3
+ base_event = events[0]
+ other_events = events[1:]
+
+ assert base_event.stream_ended in other_events
+ assert isinstance(base_event.stream_ended, h2.events.StreamEnded)
+ assert base_event.priority_updated in other_events
+ assert isinstance(
+ base_event.priority_updated, h2.events.PriorityUpdated
+ )
+
+ def test_response_received_related_priority(self, frame_factory):
+ """
+ ResponseReceived can be related to PriorityUpdated.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ input_frame = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ flags=['PRIORITY'],
+ stream_weight=15,
+ depends_on=0,
+ exclusive=False,
+ )
+ events = c.receive_data(input_frame.serialize())
+
+ assert len(events) == 2
+ base_event = events[0]
+ priority_updated_event = events[1]
+
+ assert base_event.priority_updated is priority_updated_event
+ assert base_event.stream_ended is None
+ assert isinstance(
+ base_event.priority_updated, h2.events.PriorityUpdated
+ )
+
+ def test_response_received_related_stream_ended(self, frame_factory):
+ """
+ ResponseReceived can be related to StreamEnded.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ input_frame = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ flags=['END_STREAM'],
+ )
+ events = c.receive_data(input_frame.serialize())
+
+ assert len(events) == 2
+ base_event = events[0]
+ stream_ended_event = events[1]
+
+ assert base_event.stream_ended is stream_ended_event
+ assert base_event.priority_updated is None
+ assert isinstance(base_event.stream_ended, h2.events.StreamEnded)
+
+ def test_trailers_received_related_all(self, frame_factory):
+ """
+ TrailersReceived has two possible related events: PriorityUpdated and
+ StreamEnded, all fired when a single HEADERS frame is received.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ )
+ c.receive_data(f.serialize())
+
+ input_frame = frame_factory.build_headers_frame(
+ headers=self.example_trailers,
+ flags=['END_STREAM', 'PRIORITY'],
+ stream_weight=15,
+ depends_on=0,
+ exclusive=False,
+ )
+ events = c.receive_data(input_frame.serialize())
+
+ assert len(events) == 3
+ base_event = events[0]
+ other_events = events[1:]
+
+ assert base_event.stream_ended in other_events
+ assert isinstance(base_event.stream_ended, h2.events.StreamEnded)
+ assert base_event.priority_updated in other_events
+ assert isinstance(
+ base_event.priority_updated, h2.events.PriorityUpdated
+ )
+
+ def test_trailers_received_related_stream_ended(self, frame_factory):
+ """
+ TrailersReceived can be related to StreamEnded by itself.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ )
+ c.receive_data(f.serialize())
+
+ input_frame = frame_factory.build_headers_frame(
+ headers=self.example_trailers,
+ flags=['END_STREAM'],
+ )
+ events = c.receive_data(input_frame.serialize())
+
+ assert len(events) == 2
+ base_event = events[0]
+ stream_ended_event = events[1]
+
+ assert base_event.stream_ended is stream_ended_event
+ assert base_event.priority_updated is None
+ assert isinstance(base_event.stream_ended, h2.events.StreamEnded)
+
+ def test_informational_response_related_nothing(self, frame_factory):
+ """
+ InformationalResponseReceived in the standard case is related to
+ nothing.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ input_frame = frame_factory.build_headers_frame(
+ headers=self.informational_response_headers,
+ )
+ events = c.receive_data(input_frame.serialize())
+
+ assert len(events) == 1
+ base_event = events[0]
+
+ assert base_event.priority_updated is None
+
+ def test_informational_response_received_related_all(self, frame_factory):
+ """
+ InformationalResponseReceived has one possible related event:
+ PriorityUpdated, fired when a single HEADERS frame is received.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ input_frame = frame_factory.build_headers_frame(
+ headers=self.informational_response_headers,
+ flags=['PRIORITY'],
+ stream_weight=15,
+ depends_on=0,
+ exclusive=False,
+ )
+ events = c.receive_data(input_frame.serialize())
+
+ assert len(events) == 2
+ base_event = events[0]
+ priority_updated_event = events[1]
+
+ assert base_event.priority_updated is priority_updated_event
+ assert isinstance(
+ base_event.priority_updated, h2.events.PriorityUpdated
+ )
+
+ def test_data_received_normally_relates_to_nothing(self, frame_factory):
+ """
+ A plain DATA frame leads to DataReceieved with no related events.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ )
+ c.receive_data(f.serialize())
+
+ input_frame = frame_factory.build_data_frame(
+ data=b'some data',
+ )
+ events = c.receive_data(input_frame.serialize())
+
+ assert len(events) == 1
+ base_event = events[0]
+
+ assert base_event.stream_ended is None
+
+ def test_data_received_related_stream_ended(self, frame_factory):
+ """
+ DataReceived can be related to StreamEnded by itself.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ )
+ c.receive_data(f.serialize())
+
+ input_frame = frame_factory.build_data_frame(
+ data=b'some data',
+ flags=['END_STREAM'],
+ )
+ events = c.receive_data(input_frame.serialize())
+
+ assert len(events) == 2
+ base_event = events[0]
+ stream_ended_event = events[1]
+
+ assert base_event.stream_ended is stream_ended_event
+ assert isinstance(base_event.stream_ended, h2.events.StreamEnded)
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_rfc7838.py b/testing/web-platform/tests/tools/third_party/h2/test/test_rfc7838.py
new file mode 100644
index 0000000000..d7704e2345
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_rfc7838.py
@@ -0,0 +1,447 @@
+# -*- coding: utf-8 -*-
+"""
+test_rfc7838
+~~~~~~~~~~~~
+
+Test the RFC 7838 ALTSVC support.
+"""
+import pytest
+
+import h2.config
+import h2.connection
+import h2.events
+import h2.exceptions
+
+
+class TestRFC7838Client(object):
+ """
+ Tests that the client supports receiving the RFC 7838 AltSvc frame.
+ """
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ]
+ example_response_headers = [
+ (u':status', u'200'),
+ (u'server', u'fake-serv/0.1.0')
+ ]
+
+ def test_receiving_altsvc_stream_zero(self, frame_factory):
+ """
+ An ALTSVC frame received on stream zero correctly transposes all the
+ fields from the frames.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_alt_svc_frame(
+ stream_id=0, origin=b"example.com", field=b'h2=":8000"; ma=60'
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.AlternativeServiceAvailable)
+ assert event.origin == b"example.com"
+ assert event.field_value == b'h2=":8000"; ma=60'
+
+ # No data gets sent.
+ assert not c.data_to_send()
+
+ def test_receiving_altsvc_stream_zero_no_origin(self, frame_factory):
+ """
+ An ALTSVC frame received on stream zero without an origin field is
+ ignored.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_alt_svc_frame(
+ stream_id=0, origin=b"", field=b'h2=":8000"; ma=60'
+ )
+ events = c.receive_data(f.serialize())
+
+ assert not events
+ assert not c.data_to_send()
+
+ def test_receiving_altsvc_on_stream(self, frame_factory):
+ """
+ An ALTSVC frame received on a stream correctly transposes all the
+ fields from the frame and attaches the expected origin.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_alt_svc_frame(
+ stream_id=1, origin=b"", field=b'h2=":8000"; ma=60'
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.AlternativeServiceAvailable)
+ assert event.origin == b"example.com"
+ assert event.field_value == b'h2=":8000"; ma=60'
+
+ # No data gets sent.
+ assert not c.data_to_send()
+
+ def test_receiving_altsvc_on_stream_with_origin(self, frame_factory):
+ """
+ An ALTSVC frame received on a stream with an origin field present gets
+ ignored.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_alt_svc_frame(
+ stream_id=1, origin=b"example.com", field=b'h2=":8000"; ma=60'
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 0
+ assert not c.data_to_send()
+
+ def test_receiving_altsvc_on_stream_not_yet_opened(self, frame_factory):
+ """
+ When an ALTSVC frame is received on a stream the client hasn't yet
+ opened, the frame is ignored.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.clear_outbound_data_buffer()
+
+ # We'll test this twice, once on a client-initiated stream ID and once
+ # on a server initiated one.
+ f1 = frame_factory.build_alt_svc_frame(
+ stream_id=1, origin=b"", field=b'h2=":8000"; ma=60'
+ )
+ f2 = frame_factory.build_alt_svc_frame(
+ stream_id=2, origin=b"", field=b'h2=":8000"; ma=60'
+ )
+ events = c.receive_data(f1.serialize() + f2.serialize())
+
+ assert len(events) == 0
+ assert not c.data_to_send()
+
+ def test_receiving_altsvc_before_sending_headers(self, frame_factory):
+ """
+ When an ALTSVC frame is received but the client hasn't sent headers yet
+ it gets ignored.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ # We need to create the idle stream. We have to do it by calling
+ # a private API. While this can't naturally happen in hyper-h2 (we
+ # don't currently have a mechanism by which this could occur), it could
+ # happen in the future and we defend against it.
+ c._begin_new_stream(
+ stream_id=1, allowed_ids=h2.connection.AllowedStreamIDs.ODD
+ )
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_alt_svc_frame(
+ stream_id=1, origin=b"", field=b'h2=":8000"; ma=60'
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 0
+ assert not c.data_to_send()
+
+ def test_receiving_altsvc_after_receiving_headers(self, frame_factory):
+ """
+ When an ALTSVC frame is received but the server has already sent
+ headers it gets ignored.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_response_headers
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_alt_svc_frame(
+ stream_id=1, origin=b"", field=b'h2=":8000"; ma=60'
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 0
+ assert not c.data_to_send()
+
+ def test_receiving_altsvc_on_closed_stream(self, frame_factory):
+ """
+ When an ALTSVC frame is received on a closed stream, we ignore it.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(
+ stream_id=1, headers=self.example_request_headers, end_stream=True
+ )
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ flags=['END_STREAM'],
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_alt_svc_frame(
+ stream_id=1, origin=b"", field=b'h2=":8000"; ma=60'
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 0
+ assert not c.data_to_send()
+
+ def test_receiving_altsvc_on_pushed_stream(self, frame_factory):
+ """
+ When an ALTSVC frame is received on a stream that the server pushed,
+ the frame is accepted.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+
+ f = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=self.example_request_headers
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_alt_svc_frame(
+ stream_id=2, origin=b"", field=b'h2=":8000"; ma=60'
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 1
+ event = events[0]
+
+ assert isinstance(event, h2.events.AlternativeServiceAvailable)
+ assert event.origin == b"example.com"
+ assert event.field_value == b'h2=":8000"; ma=60'
+
+ # No data gets sent.
+ assert not c.data_to_send()
+
+ def test_cannot_send_explicit_alternative_service(self, frame_factory):
+ """
+ A client cannot send an explicit alternative service.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.advertise_alternative_service(
+ field_value=b'h2=":8000"; ma=60',
+ origin=b"example.com",
+ )
+
+ def test_cannot_send_implicit_alternative_service(self, frame_factory):
+ """
+ A client cannot send an implicit alternative service.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.advertise_alternative_service(
+ field_value=b'h2=":8000"; ma=60',
+ stream_id=1,
+ )
+
+
+class TestRFC7838Server(object):
+ """
+ Tests that the server supports sending the RFC 7838 AltSvc frame.
+ """
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ]
+ example_response_headers = [
+ (u':status', u'200'),
+ (u'server', u'fake-serv/0.1.0')
+ ]
+
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ def test_receiving_altsvc_as_server_stream_zero(self, frame_factory):
+ """
+ When an ALTSVC frame is received on stream zero and we are a server,
+ we ignore it.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_alt_svc_frame(
+ stream_id=0, origin=b"example.com", field=b'h2=":8000"; ma=60'
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 0
+ assert not c.data_to_send()
+
+ def test_receiving_altsvc_as_server_on_stream(self, frame_factory):
+ """
+ When an ALTSVC frame is received on a stream and we are a server, we
+ ignore it.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_alt_svc_frame(
+ stream_id=1, origin=b"", field=b'h2=":8000"; ma=60'
+ )
+ events = c.receive_data(f.serialize())
+
+ assert len(events) == 0
+ assert not c.data_to_send()
+
+ def test_sending_explicit_alternative_service(self, frame_factory):
+ """
+ A server can send an explicit alternative service.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ c.advertise_alternative_service(
+ field_value=b'h2=":8000"; ma=60',
+ origin=b"example.com",
+ )
+
+ f = frame_factory.build_alt_svc_frame(
+ stream_id=0, origin=b"example.com", field=b'h2=":8000"; ma=60'
+ )
+ assert c.data_to_send() == f.serialize()
+
+ def test_sending_implicit_alternative_service(self, frame_factory):
+ """
+ A server can send an implicit alternative service.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ c.advertise_alternative_service(
+ field_value=b'h2=":8000"; ma=60',
+ stream_id=1,
+ )
+
+ f = frame_factory.build_alt_svc_frame(
+ stream_id=1, origin=b"", field=b'h2=":8000"; ma=60'
+ )
+ assert c.data_to_send() == f.serialize()
+
+ def test_no_implicit_alternative_service_before_headers(self,
+ frame_factory):
+ """
+ If headers haven't been received yet, the server forbids sending an
+ implicit alternative service.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.advertise_alternative_service(
+ field_value=b'h2=":8000"; ma=60',
+ stream_id=1,
+ )
+
+ def test_no_implicit_alternative_service_after_response(self,
+ frame_factory):
+ """
+ If the server has sent response headers, hyper-h2 forbids sending an
+ implicit alternative service.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers
+ )
+ c.receive_data(f.serialize())
+ c.send_headers(stream_id=1, headers=self.example_response_headers)
+ c.clear_outbound_data_buffer()
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ c.advertise_alternative_service(
+ field_value=b'h2=":8000"; ma=60',
+ stream_id=1,
+ )
+
+ def test_cannot_provide_origin_and_stream_id(self, frame_factory):
+ """
+ The user cannot provide both the origin and stream_id arguments when
+ advertising alternative services.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers
+ )
+ c.receive_data(f.serialize())
+
+ with pytest.raises(ValueError):
+ c.advertise_alternative_service(
+ field_value=b'h2=":8000"; ma=60',
+ origin=b"example.com",
+ stream_id=1,
+ )
+
+ def test_cannot_provide_unicode_altsvc_field(self, frame_factory):
+ """
+ The user cannot provide the field value for alternative services as a
+ unicode string.
+ """
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+
+ with pytest.raises(ValueError):
+ c.advertise_alternative_service(
+ field_value=u'h2=":8000"; ma=60',
+ origin=b"example.com",
+ )
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_settings.py b/testing/web-platform/tests/tools/third_party/h2/test/test_settings.py
new file mode 100644
index 0000000000..d19f93a7c2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_settings.py
@@ -0,0 +1,470 @@
+# -*- coding: utf-8 -*-
+"""
+test_settings
+~~~~~~~~~~~~~
+
+Test the Settings object.
+"""
+import pytest
+
+import h2.errors
+import h2.exceptions
+import h2.settings
+
+from hypothesis import given, assume
+from hypothesis.strategies import (
+ integers, booleans, fixed_dictionaries, builds
+)
+
+
+class TestSettings(object):
+ """
+ Test the Settings object behaves as expected.
+ """
+ def test_settings_defaults_client(self):
+ """
+ The Settings object begins with the appropriate defaults for clients.
+ """
+ s = h2.settings.Settings(client=True)
+
+ assert s[h2.settings.SettingCodes.HEADER_TABLE_SIZE] == 4096
+ assert s[h2.settings.SettingCodes.ENABLE_PUSH] == 1
+ assert s[h2.settings.SettingCodes.INITIAL_WINDOW_SIZE] == 65535
+ assert s[h2.settings.SettingCodes.MAX_FRAME_SIZE] == 16384
+ assert s[h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL] == 0
+
+ def test_settings_defaults_server(self):
+ """
+ The Settings object begins with the appropriate defaults for servers.
+ """
+ s = h2.settings.Settings(client=False)
+
+ assert s[h2.settings.SettingCodes.HEADER_TABLE_SIZE] == 4096
+ assert s[h2.settings.SettingCodes.ENABLE_PUSH] == 0
+ assert s[h2.settings.SettingCodes.INITIAL_WINDOW_SIZE] == 65535
+ assert s[h2.settings.SettingCodes.MAX_FRAME_SIZE] == 16384
+ assert s[h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL] == 0
+
+ @pytest.mark.parametrize('client', [True, False])
+ def test_can_set_initial_values(self, client):
+ """
+ The Settings object can be provided initial values that override the
+ defaults.
+ """
+ overrides = {
+ h2.settings.SettingCodes.HEADER_TABLE_SIZE: 8080,
+ h2.settings.SettingCodes.MAX_FRAME_SIZE: 16388,
+ h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 100,
+ h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: 2**16,
+ h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL: 1,
+ }
+ s = h2.settings.Settings(client=client, initial_values=overrides)
+
+ assert s[h2.settings.SettingCodes.HEADER_TABLE_SIZE] == 8080
+ assert s[h2.settings.SettingCodes.ENABLE_PUSH] == bool(client)
+ assert s[h2.settings.SettingCodes.INITIAL_WINDOW_SIZE] == 65535
+ assert s[h2.settings.SettingCodes.MAX_FRAME_SIZE] == 16388
+ assert s[h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS] == 100
+ assert s[h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE] == 2**16
+ assert s[h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL] == 1
+
+ @pytest.mark.parametrize(
+ 'setting,value',
+ [
+ (h2.settings.SettingCodes.ENABLE_PUSH, 2),
+ (h2.settings.SettingCodes.ENABLE_PUSH, -1),
+ (h2.settings.SettingCodes.INITIAL_WINDOW_SIZE, -1),
+ (h2.settings.SettingCodes.INITIAL_WINDOW_SIZE, 2**34),
+ (h2.settings.SettingCodes.MAX_FRAME_SIZE, 1),
+ (h2.settings.SettingCodes.MAX_FRAME_SIZE, 2**30),
+ (h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE, -1),
+ (h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL, -1),
+ ]
+ )
+ def test_cannot_set_invalid_initial_values(self, setting, value):
+ """
+ The Settings object can be provided initial values that override the
+ defaults.
+ """
+ overrides = {setting: value}
+
+ with pytest.raises(h2.exceptions.InvalidSettingsValueError):
+ h2.settings.Settings(initial_values=overrides)
+
+ def test_applying_value_doesnt_take_effect_immediately(self):
+ """
+ When a value is applied to the settings object, it doesn't immediately
+ take effect.
+ """
+ s = h2.settings.Settings(client=True)
+ s[h2.settings.SettingCodes.HEADER_TABLE_SIZE] == 8000
+
+ assert s[h2.settings.SettingCodes.HEADER_TABLE_SIZE] == 4096
+
+ def test_acknowledging_values(self):
+ """
+ When we acknowledge settings, the values change.
+ """
+ s = h2.settings.Settings(client=True)
+ old_settings = dict(s)
+
+ new_settings = {
+ h2.settings.SettingCodes.HEADER_TABLE_SIZE: 4000,
+ h2.settings.SettingCodes.ENABLE_PUSH: 0,
+ h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: 60,
+ h2.settings.SettingCodes.MAX_FRAME_SIZE: 16385,
+ h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL: 1,
+ }
+ s.update(new_settings)
+
+ assert dict(s) == old_settings
+ s.acknowledge()
+ assert dict(s) == new_settings
+
+ def test_acknowledging_returns_the_changed_settings(self):
+ """
+ Acknowledging settings returns the changes.
+ """
+ s = h2.settings.Settings(client=True)
+ s[h2.settings.SettingCodes.HEADER_TABLE_SIZE] = 8000
+ s[h2.settings.SettingCodes.ENABLE_PUSH] = 0
+
+ changes = s.acknowledge()
+ assert len(changes) == 2
+
+ table_size_change = (
+ changes[h2.settings.SettingCodes.HEADER_TABLE_SIZE]
+ )
+ push_change = changes[h2.settings.SettingCodes.ENABLE_PUSH]
+
+ assert table_size_change.setting == (
+ h2.settings.SettingCodes.HEADER_TABLE_SIZE
+ )
+ assert table_size_change.original_value == 4096
+ assert table_size_change.new_value == 8000
+
+ assert push_change.setting == h2.settings.SettingCodes.ENABLE_PUSH
+ assert push_change.original_value == 1
+ assert push_change.new_value == 0
+
+ def test_acknowledging_only_returns_changed_settings(self):
+ """
+ Acknowledging settings does not return unchanged settings.
+ """
+ s = h2.settings.Settings(client=True)
+ s[h2.settings.SettingCodes.INITIAL_WINDOW_SIZE] = 70
+
+ changes = s.acknowledge()
+ assert len(changes) == 1
+ assert list(changes.keys()) == [
+ h2.settings.SettingCodes.INITIAL_WINDOW_SIZE
+ ]
+
+ def test_deleting_values_deletes_all_of_them(self):
+ """
+ When we delete a key we lose all state about it.
+ """
+ s = h2.settings.Settings(client=True)
+ s[h2.settings.SettingCodes.HEADER_TABLE_SIZE] == 8000
+
+ del s[h2.settings.SettingCodes.HEADER_TABLE_SIZE]
+
+ with pytest.raises(KeyError):
+ s[h2.settings.SettingCodes.HEADER_TABLE_SIZE]
+
+ def test_length_correctly_reported(self):
+ """
+ Length is related only to the number of keys.
+ """
+ s = h2.settings.Settings(client=True)
+ assert len(s) == 5
+
+ s[h2.settings.SettingCodes.HEADER_TABLE_SIZE] == 8000
+ assert len(s) == 5
+
+ s.acknowledge()
+ assert len(s) == 5
+
+ del s[h2.settings.SettingCodes.HEADER_TABLE_SIZE]
+ assert len(s) == 4
+
+ def test_new_values_work(self):
+ """
+ New values initially don't appear
+ """
+ s = h2.settings.Settings(client=True)
+ s[80] = 81
+
+ with pytest.raises(KeyError):
+ s[80]
+
+ def test_new_values_follow_basic_acknowledgement_rules(self):
+ """
+ A new value properly appears when acknowledged.
+ """
+ s = h2.settings.Settings(client=True)
+ s[80] = 81
+ changed_settings = s.acknowledge()
+
+ assert s[80] == 81
+ assert len(changed_settings) == 1
+
+ changed = changed_settings[80]
+ assert changed.setting == 80
+ assert changed.original_value is None
+ assert changed.new_value == 81
+
+ def test_single_values_arent_affected_by_acknowledgement(self):
+ """
+ When acknowledged, unchanged settings remain unchanged.
+ """
+ s = h2.settings.Settings(client=True)
+ assert s[h2.settings.SettingCodes.HEADER_TABLE_SIZE] == 4096
+
+ s.acknowledge()
+ assert s[h2.settings.SettingCodes.HEADER_TABLE_SIZE] == 4096
+
+ def test_settings_getters(self):
+ """
+ Getters exist for well-known settings.
+ """
+ s = h2.settings.Settings(client=True)
+
+ assert s.header_table_size == (
+ s[h2.settings.SettingCodes.HEADER_TABLE_SIZE]
+ )
+ assert s.enable_push == s[h2.settings.SettingCodes.ENABLE_PUSH]
+ assert s.initial_window_size == (
+ s[h2.settings.SettingCodes.INITIAL_WINDOW_SIZE]
+ )
+ assert s.max_frame_size == s[h2.settings.SettingCodes.MAX_FRAME_SIZE]
+ assert s.max_concurrent_streams == 2**32 + 1 # A sensible default.
+ assert s.max_header_list_size is None
+ assert s.enable_connect_protocol == s[
+ h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL
+ ]
+
+ def test_settings_setters(self):
+ """
+ Setters exist for well-known settings.
+ """
+ s = h2.settings.Settings(client=True)
+
+ s.header_table_size = 0
+ s.enable_push = 1
+ s.initial_window_size = 2
+ s.max_frame_size = 16385
+ s.max_concurrent_streams = 4
+ s.max_header_list_size = 2**16
+ s.enable_connect_protocol = 1
+
+ s.acknowledge()
+ assert s[h2.settings.SettingCodes.HEADER_TABLE_SIZE] == 0
+ assert s[h2.settings.SettingCodes.ENABLE_PUSH] == 1
+ assert s[h2.settings.SettingCodes.INITIAL_WINDOW_SIZE] == 2
+ assert s[h2.settings.SettingCodes.MAX_FRAME_SIZE] == 16385
+ assert s[h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS] == 4
+ assert s[h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE] == 2**16
+ assert s[h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL] == 1
+
+ @given(integers())
+ def test_cannot_set_invalid_values_for_enable_push(self, val):
+ """
+ SETTINGS_ENABLE_PUSH only allows two values: 0, 1.
+ """
+ assume(val not in (0, 1))
+ s = h2.settings.Settings()
+
+ with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e:
+ s.enable_push = val
+
+ s.acknowledge()
+ assert e.value.error_code == h2.errors.ErrorCodes.PROTOCOL_ERROR
+ assert s.enable_push == 1
+
+ with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e:
+ s[h2.settings.SettingCodes.ENABLE_PUSH] = val
+
+ s.acknowledge()
+ assert e.value.error_code == h2.errors.ErrorCodes.PROTOCOL_ERROR
+ assert s[h2.settings.SettingCodes.ENABLE_PUSH] == 1
+
+ @given(integers())
+ def test_cannot_set_invalid_vals_for_initial_window_size(self, val):
+ """
+ SETTINGS_INITIAL_WINDOW_SIZE only allows values between 0 and 2**32 - 1
+ inclusive.
+ """
+ s = h2.settings.Settings()
+
+ if 0 <= val <= 2**31 - 1:
+ s.initial_window_size = val
+ s.acknowledge()
+ assert s.initial_window_size == val
+ else:
+ with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e:
+ s.initial_window_size = val
+
+ s.acknowledge()
+ assert (
+ e.value.error_code == h2.errors.ErrorCodes.FLOW_CONTROL_ERROR
+ )
+ assert s.initial_window_size == 65535
+
+ with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e:
+ s[h2.settings.SettingCodes.INITIAL_WINDOW_SIZE] = val
+
+ s.acknowledge()
+ assert (
+ e.value.error_code == h2.errors.ErrorCodes.FLOW_CONTROL_ERROR
+ )
+ assert s[h2.settings.SettingCodes.INITIAL_WINDOW_SIZE] == 65535
+
+ @given(integers())
+ def test_cannot_set_invalid_values_for_max_frame_size(self, val):
+ """
+ SETTINGS_MAX_FRAME_SIZE only allows values between 2**14 and 2**24 - 1.
+ """
+ s = h2.settings.Settings()
+
+ if 2**14 <= val <= 2**24 - 1:
+ s.max_frame_size = val
+ s.acknowledge()
+ assert s.max_frame_size == val
+ else:
+ with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e:
+ s.max_frame_size = val
+
+ s.acknowledge()
+ assert e.value.error_code == h2.errors.ErrorCodes.PROTOCOL_ERROR
+ assert s.max_frame_size == 16384
+
+ with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e:
+ s[h2.settings.SettingCodes.MAX_FRAME_SIZE] = val
+
+ s.acknowledge()
+ assert e.value.error_code == h2.errors.ErrorCodes.PROTOCOL_ERROR
+ assert s[h2.settings.SettingCodes.MAX_FRAME_SIZE] == 16384
+
+ @given(integers())
+ def test_cannot_set_invalid_values_for_max_header_list_size(self, val):
+ """
+ SETTINGS_MAX_HEADER_LIST_SIZE only allows non-negative values.
+ """
+ s = h2.settings.Settings()
+
+ if val >= 0:
+ s.max_header_list_size = val
+ s.acknowledge()
+ assert s.max_header_list_size == val
+ else:
+ with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e:
+ s.max_header_list_size = val
+
+ s.acknowledge()
+ assert e.value.error_code == h2.errors.ErrorCodes.PROTOCOL_ERROR
+ assert s.max_header_list_size is None
+
+ with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e:
+ s[h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE] = val
+
+ s.acknowledge()
+ assert e.value.error_code == h2.errors.ErrorCodes.PROTOCOL_ERROR
+
+ with pytest.raises(KeyError):
+ s[h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE]
+
+ @given(integers())
+ def test_cannot_set_invalid_values_for_enable_connect_protocol(self, val):
+ """
+ SETTINGS_ENABLE_CONNECT_PROTOCOL only allows two values: 0, 1.
+ """
+ assume(val not in (0, 1))
+ s = h2.settings.Settings()
+
+ with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e:
+ s.enable_connect_protocol = val
+
+ s.acknowledge()
+ assert e.value.error_code == h2.errors.ErrorCodes.PROTOCOL_ERROR
+ assert s.enable_connect_protocol == 0
+
+ with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e:
+ s[h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL] = val
+
+ s.acknowledge()
+ assert e.value.error_code == h2.errors.ErrorCodes.PROTOCOL_ERROR
+ assert s[h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL] == 0
+
+
+class TestSettingsEquality(object):
+ """
+ A class defining tests for the standard implementation of == and != .
+ """
+
+ SettingsStrategy = builds(
+ h2.settings.Settings,
+ client=booleans(),
+ initial_values=fixed_dictionaries({
+ h2.settings.SettingCodes.HEADER_TABLE_SIZE:
+ integers(0, 2**32 - 1),
+ h2.settings.SettingCodes.ENABLE_PUSH: integers(0, 1),
+ h2.settings.SettingCodes.INITIAL_WINDOW_SIZE:
+ integers(0, 2**31 - 1),
+ h2.settings.SettingCodes.MAX_FRAME_SIZE:
+ integers(2**14, 2**24 - 1),
+ h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS:
+ integers(0, 2**32 - 1),
+ h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE:
+ integers(0, 2**32 - 1),
+ })
+ )
+
+ @given(settings=SettingsStrategy)
+ def test_equality_reflexive(self, settings):
+ """
+ An object compares equal to itself using the == operator and the !=
+ operator.
+ """
+ assert (settings == settings)
+ assert not (settings != settings)
+
+ @given(settings=SettingsStrategy, o_settings=SettingsStrategy)
+ def test_equality_multiple(self, settings, o_settings):
+ """
+ Two objects compare themselves using the == operator and the !=
+ operator.
+ """
+ if settings == o_settings:
+ assert settings == o_settings
+ assert not (settings != o_settings)
+ else:
+ assert settings != o_settings
+ assert not (settings == o_settings)
+
+ @given(settings=SettingsStrategy)
+ def test_another_type_equality(self, settings):
+ """
+ The object does not compare equal to an object of an unrelated type
+ (which does not implement the comparison) using the == operator.
+ """
+ obj = object()
+ assert (settings != obj)
+ assert not (settings == obj)
+
+ @given(settings=SettingsStrategy)
+ def test_delegated_eq(self, settings):
+ """
+ The result of comparison is delegated to the right-hand operand if
+ it is of an unrelated type.
+ """
+ class Delegate(object):
+ def __eq__(self, other):
+ return [self]
+
+ def __ne__(self, other):
+ return [self]
+
+ delg = Delegate()
+ assert (settings == delg) == [delg]
+ assert (settings != delg) == [delg]
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_state_machines.py b/testing/web-platform/tests/tools/third_party/h2/test/test_state_machines.py
new file mode 100644
index 0000000000..034ae909d2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_state_machines.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+"""
+test_state_machines
+~~~~~~~~~~~~~~~~~~~
+
+These tests validate the state machines directly. Writing meaningful tests for
+this case can be tricky, so the majority of these tests use Hypothesis to try
+to talk about general behaviours rather than specific cases.
+"""
+import pytest
+
+import h2.connection
+import h2.exceptions
+import h2.stream
+
+from hypothesis import given
+from hypothesis.strategies import sampled_from
+
+
+class TestConnectionStateMachine(object):
+ """
+ Tests of the connection state machine.
+ """
+ @given(state=sampled_from(h2.connection.ConnectionState),
+ input_=sampled_from(h2.connection.ConnectionInputs))
+ def test_state_transitions(self, state, input_):
+ c = h2.connection.H2ConnectionStateMachine()
+ c.state = state
+
+ try:
+ c.process_input(input_)
+ except h2.exceptions.ProtocolError:
+ assert c.state == h2.connection.ConnectionState.CLOSED
+ else:
+ assert c.state in h2.connection.ConnectionState
+
+ def test_state_machine_only_allows_connection_states(self):
+ """
+ The Connection state machine only allows ConnectionState inputs.
+ """
+ c = h2.connection.H2ConnectionStateMachine()
+
+ with pytest.raises(ValueError):
+ c.process_input(1)
+
+ @pytest.mark.parametrize(
+ "state",
+ (
+ s for s in h2.connection.ConnectionState
+ if s != h2.connection.ConnectionState.CLOSED
+ ),
+ )
+ @pytest.mark.parametrize(
+ "input_",
+ [
+ h2.connection.ConnectionInputs.RECV_PRIORITY,
+ h2.connection.ConnectionInputs.SEND_PRIORITY
+ ]
+ )
+ def test_priority_frames_allowed_in_all_states(self, state, input_):
+ """
+ Priority frames can be sent/received in all connection states except
+ closed.
+ """
+ c = h2.connection.H2ConnectionStateMachine()
+ c.state = state
+
+ c.process_input(input_)
+
+
+class TestStreamStateMachine(object):
+ """
+ Tests of the stream state machine.
+ """
+ @given(state=sampled_from(h2.stream.StreamState),
+ input_=sampled_from(h2.stream.StreamInputs))
+ def test_state_transitions(self, state, input_):
+ s = h2.stream.H2StreamStateMachine(stream_id=1)
+ s.state = state
+
+ try:
+ s.process_input(input_)
+ except h2.exceptions.StreamClosedError:
+ # This can only happen for streams that started in the closed
+ # state OR where the input was RECV_DATA and the state was not
+ # OPEN or HALF_CLOSED_LOCAL OR where the state was
+ # HALF_CLOSED_REMOTE and a frame was received.
+ if state == h2.stream.StreamState.CLOSED:
+ assert s.state == h2.stream.StreamState.CLOSED
+ elif input_ == h2.stream.StreamInputs.RECV_DATA:
+ assert s.state == h2.stream.StreamState.CLOSED
+ assert state not in (
+ h2.stream.StreamState.OPEN,
+ h2.stream.StreamState.HALF_CLOSED_LOCAL,
+ )
+ elif state == h2.stream.StreamState.HALF_CLOSED_REMOTE:
+ assert input_ in (
+ h2.stream.StreamInputs.RECV_HEADERS,
+ h2.stream.StreamInputs.RECV_PUSH_PROMISE,
+ h2.stream.StreamInputs.RECV_DATA,
+ h2.stream.StreamInputs.RECV_CONTINUATION,
+ )
+ except h2.exceptions.ProtocolError:
+ assert s.state == h2.stream.StreamState.CLOSED
+ else:
+ assert s.state in h2.stream.StreamState
+
+ def test_state_machine_only_allows_stream_states(self):
+ """
+ The Stream state machine only allows StreamState inputs.
+ """
+ s = h2.stream.H2StreamStateMachine(stream_id=1)
+
+ with pytest.raises(ValueError):
+ s.process_input(1)
+
+ def test_stream_state_machine_forbids_pushes_on_server_streams(self):
+ """
+ Streams where this peer is a server do not allow receiving pushed
+ frames.
+ """
+ s = h2.stream.H2StreamStateMachine(stream_id=1)
+ s.process_input(h2.stream.StreamInputs.RECV_HEADERS)
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ s.process_input(h2.stream.StreamInputs.RECV_PUSH_PROMISE)
+
+ def test_stream_state_machine_forbids_sending_pushes_from_clients(self):
+ """
+ Streams where this peer is a client do not allow sending pushed frames.
+ """
+ s = h2.stream.H2StreamStateMachine(stream_id=1)
+ s.process_input(h2.stream.StreamInputs.SEND_HEADERS)
+
+ with pytest.raises(h2.exceptions.ProtocolError):
+ s.process_input(h2.stream.StreamInputs.SEND_PUSH_PROMISE)
+
+ @pytest.mark.parametrize(
+ "input_",
+ [
+ h2.stream.StreamInputs.SEND_HEADERS,
+ h2.stream.StreamInputs.SEND_PUSH_PROMISE,
+ h2.stream.StreamInputs.SEND_RST_STREAM,
+ h2.stream.StreamInputs.SEND_DATA,
+ h2.stream.StreamInputs.SEND_WINDOW_UPDATE,
+ h2.stream.StreamInputs.SEND_END_STREAM,
+ ]
+ )
+ def test_cannot_send_on_closed_streams(self, input_):
+ """
+ Sending anything but a PRIORITY frame is forbidden on closed streams.
+ """
+ c = h2.stream.H2StreamStateMachine(stream_id=1)
+ c.state = h2.stream.StreamState.CLOSED
+
+ expected_error = (
+ h2.exceptions.ProtocolError
+ if input_ == h2.stream.StreamInputs.SEND_PUSH_PROMISE
+ else h2.exceptions.StreamClosedError
+ )
+
+ with pytest.raises(expected_error):
+ c.process_input(input_)
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_stream_reset.py b/testing/web-platform/tests/tools/third_party/h2/test/test_stream_reset.py
new file mode 100644
index 0000000000..778445515f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_stream_reset.py
@@ -0,0 +1,137 @@
+# -*- coding: utf-8 -*-
+"""
+test_stream_reset
+~~~~~~~~~~~~~~~~~
+
+More complex tests that exercise stream resetting functionality to validate
+that connection state is appropriately maintained.
+
+Specifically, these tests validate that streams that have been reset accurately
+keep track of connection-level state.
+"""
+import pytest
+
+import h2.connection
+import h2.errors
+import h2.events
+
+
+class TestStreamReset(object):
+ """
+ Tests for resetting streams.
+ """
+ example_request_headers = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'GET'),
+ ]
+ example_response_headers = [
+ (b':status', b'200'),
+ (b'server', b'fake-serv/0.1.0'),
+ (b'content-length', b'0')
+ ]
+
+ def test_reset_stream_keeps_header_state_correct(self, frame_factory):
+ """
+ A stream that has been reset still affects the header decoder.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+ c.reset_stream(stream_id=1)
+ c.send_headers(stream_id=3, headers=self.example_request_headers)
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_response_headers, stream_id=1
+ )
+ rst_frame = frame_factory.build_rst_stream_frame(
+ 1, h2.errors.ErrorCodes.STREAM_CLOSED
+ )
+ events = c.receive_data(f.serialize())
+ assert not events
+ assert c.data_to_send() == rst_frame.serialize()
+
+ # This works because the header state should be intact from the headers
+ # frame that was send on stream 1, so they should decode cleanly.
+ f = frame_factory.build_headers_frame(
+ headers=self.example_response_headers, stream_id=3
+ )
+ event = c.receive_data(f.serialize())[0]
+
+ assert isinstance(event, h2.events.ResponseReceived)
+ assert event.stream_id == 3
+ assert event.headers == self.example_response_headers
+
+ @pytest.mark.parametrize('close_id,other_id', [(1, 3), (3, 1)])
+ def test_reset_stream_keeps_flow_control_correct(self,
+ close_id,
+ other_id,
+ frame_factory):
+ """
+ A stream that has been reset does not affect the connection flow
+ control window.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+ c.send_headers(stream_id=3, headers=self.example_request_headers)
+
+ # Record the initial window size.
+ initial_window = c.remote_flow_control_window(stream_id=other_id)
+
+ f = frame_factory.build_headers_frame(
+ headers=self.example_response_headers, stream_id=close_id
+ )
+ c.receive_data(f.serialize())
+ c.reset_stream(stream_id=close_id)
+ c.clear_outbound_data_buffer()
+
+ f = frame_factory.build_data_frame(
+ data=b'some data',
+ stream_id=close_id
+ )
+ c.receive_data(f.serialize())
+
+ expected = frame_factory.build_rst_stream_frame(
+ stream_id=close_id,
+ error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
+ ).serialize()
+ assert c.data_to_send() == expected
+
+ new_window = c.remote_flow_control_window(stream_id=other_id)
+ assert initial_window - len(b'some data') == new_window
+
+ @pytest.mark.parametrize('clear_streams', [True, False])
+ def test_reset_stream_automatically_resets_pushed_streams(self,
+ frame_factory,
+ clear_streams):
+ """
+ Resetting a stream causes RST_STREAM frames to be automatically emitted
+ to close any streams pushed after the reset.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ c.send_headers(stream_id=1, headers=self.example_request_headers)
+ c.reset_stream(stream_id=1)
+ c.clear_outbound_data_buffer()
+
+ if clear_streams:
+ # Call open_outbound_streams to force the connection to clean
+ # closed streams.
+ c.open_outbound_streams
+
+ f = frame_factory.build_push_promise_frame(
+ stream_id=1,
+ promised_stream_id=2,
+ headers=self.example_request_headers,
+ )
+ events = c.receive_data(f.serialize())
+ assert not events
+
+ f = frame_factory.build_rst_stream_frame(
+ stream_id=2,
+ error_code=h2.errors.ErrorCodes.REFUSED_STREAM,
+ )
+ assert c.data_to_send() == f.serialize()
diff --git a/testing/web-platform/tests/tools/third_party/h2/test/test_utility_functions.py b/testing/web-platform/tests/tools/third_party/h2/test/test_utility_functions.py
new file mode 100644
index 0000000000..4cb0b2ae60
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test/test_utility_functions.py
@@ -0,0 +1,226 @@
+# -*- coding: utf-8 -*-
+"""
+test_utility_functions
+~~~~~~~~~~~~~~~~~~~~~~
+
+Tests for the various utility functions provided by hyper-h2.
+"""
+import pytest
+
+import h2.config
+import h2.connection
+import h2.errors
+import h2.events
+import h2.exceptions
+from h2.utilities import SizeLimitDict, extract_method_header
+
+# These tests require a non-list-returning range function.
+try:
+ range = xrange
+except NameError:
+ range = range
+
+
+class TestGetNextAvailableStreamID(object):
+ """
+ Tests for the ``H2Connection.get_next_available_stream_id`` method.
+ """
+ example_request_headers = [
+ (':authority', 'example.com'),
+ (':path', '/'),
+ (':scheme', 'https'),
+ (':method', 'GET'),
+ ]
+ example_response_headers = [
+ (':status', '200'),
+ ('server', 'fake-serv/0.1.0')
+ ]
+ server_config = h2.config.H2Configuration(client_side=False)
+
+ def test_returns_correct_sequence_for_clients(self, frame_factory):
+ """
+ For a client connection, the correct sequence of stream IDs is
+ returned.
+ """
+ # Running the exhaustive version of this test (all 1 billion available
+ # stream IDs) is too painful. For that reason, we validate that the
+ # original sequence is right for the first few thousand, and then just
+ # check that it terminates properly.
+ #
+ # Make sure that the streams get cleaned up: 8k streams floating
+ # around would make this test memory-hard, and it's not supposed to be
+ # a test of how much RAM your machine has.
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+ initial_sequence = range(1, 2**13, 2)
+
+ for expected_stream_id in initial_sequence:
+ stream_id = c.get_next_available_stream_id()
+ assert stream_id == expected_stream_id
+
+ c.send_headers(
+ stream_id=stream_id,
+ headers=self.example_request_headers,
+ end_stream=True
+ )
+ f = frame_factory.build_headers_frame(
+ headers=self.example_response_headers,
+ stream_id=stream_id,
+ flags=['END_STREAM'],
+ )
+ c.receive_data(f.serialize())
+ c.clear_outbound_data_buffer()
+
+ # Jump up to the last available stream ID. Don't clean up the stream
+ # here because who cares about one stream.
+ last_client_id = 2**31 - 1
+ c.send_headers(
+ stream_id=last_client_id,
+ headers=self.example_request_headers,
+ end_stream=True
+ )
+
+ with pytest.raises(h2.exceptions.NoAvailableStreamIDError):
+ c.get_next_available_stream_id()
+
+ def test_returns_correct_sequence_for_servers(self, frame_factory):
+ """
+ For a server connection, the correct sequence of stream IDs is
+ returned.
+ """
+ # Running the exhaustive version of this test (all 1 billion available
+ # stream IDs) is too painful. For that reason, we validate that the
+ # original sequence is right for the first few thousand, and then just
+ # check that it terminates properly.
+ #
+ # Make sure that the streams get cleaned up: 8k streams floating
+ # around would make this test memory-hard, and it's not supposed to be
+ # a test of how much RAM your machine has.
+ c = h2.connection.H2Connection(config=self.server_config)
+ c.initiate_connection()
+ c.receive_data(frame_factory.preamble())
+ f = frame_factory.build_headers_frame(
+ headers=self.example_request_headers
+ )
+ c.receive_data(f.serialize())
+
+ initial_sequence = range(2, 2**13, 2)
+
+ for expected_stream_id in initial_sequence:
+ stream_id = c.get_next_available_stream_id()
+ assert stream_id == expected_stream_id
+
+ c.push_stream(
+ stream_id=1,
+ promised_stream_id=stream_id,
+ request_headers=self.example_request_headers
+ )
+ c.send_headers(
+ stream_id=stream_id,
+ headers=self.example_response_headers,
+ end_stream=True
+ )
+ c.clear_outbound_data_buffer()
+
+ # Jump up to the last available stream ID. Don't clean up the stream
+ # here because who cares about one stream.
+ last_server_id = 2**31 - 2
+ c.push_stream(
+ stream_id=1,
+ promised_stream_id=last_server_id,
+ request_headers=self.example_request_headers,
+ )
+
+ with pytest.raises(h2.exceptions.NoAvailableStreamIDError):
+ c.get_next_available_stream_id()
+
+ def test_does_not_increment_without_stream_send(self):
+ """
+ If a new stream isn't actually created, the next stream ID doesn't
+ change.
+ """
+ c = h2.connection.H2Connection()
+ c.initiate_connection()
+
+ first_stream_id = c.get_next_available_stream_id()
+ second_stream_id = c.get_next_available_stream_id()
+
+ assert first_stream_id == second_stream_id
+
+ c.send_headers(
+ stream_id=first_stream_id,
+ headers=self.example_request_headers
+ )
+
+ third_stream_id = c.get_next_available_stream_id()
+ assert third_stream_id == (first_stream_id + 2)
+
+
+class TestExtractHeader(object):
+
+ example_request_headers = [
+ (u':authority', u'example.com'),
+ (u':path', u'/'),
+ (u':scheme', u'https'),
+ (u':method', u'GET'),
+ ]
+ example_headers_with_bytes = [
+ (b':authority', b'example.com'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b':method', b'GET'),
+ ]
+
+ @pytest.mark.parametrize(
+ 'headers', [example_request_headers, example_headers_with_bytes]
+ )
+ def test_extract_header_method(self, headers):
+ assert extract_method_header(headers) == b'GET'
+
+
+def test_size_limit_dict_limit():
+ dct = SizeLimitDict(size_limit=2)
+
+ dct[1] = 1
+ dct[2] = 2
+
+ assert len(dct) == 2
+ assert dct[1] == 1
+ assert dct[2] == 2
+
+ dct[3] = 3
+
+ assert len(dct) == 2
+ assert dct[2] == 2
+ assert dct[3] == 3
+ assert 1 not in dct
+
+
+def test_size_limit_dict_limit_init():
+ initial_dct = {
+ 1: 1,
+ 2: 2,
+ 3: 3,
+ }
+
+ dct = SizeLimitDict(initial_dct, size_limit=2)
+
+ assert len(dct) == 2
+
+
+def test_size_limit_dict_no_limit():
+ dct = SizeLimitDict(size_limit=None)
+
+ dct[1] = 1
+ dct[2] = 2
+
+ assert len(dct) == 2
+ assert dct[1] == 1
+ assert dct[2] == 2
+
+ dct[3] = 3
+
+ assert len(dct) == 3
+ assert dct[1] == 1
+ assert dct[2] == 2
+ assert dct[3] == 3
diff --git a/testing/web-platform/tests/tools/third_party/h2/test_requirements.txt b/testing/web-platform/tests/tools/third_party/h2/test_requirements.txt
new file mode 100644
index 0000000000..e0eefa28ac
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/test_requirements.txt
@@ -0,0 +1,5 @@
+pytest==4.6.5 # rq.filter: < 5
+pytest-cov==2.8.1
+pytest-xdist==1.31.0
+coverage==4.5.4
+hypothesis
diff --git a/testing/web-platform/tests/tools/third_party/h2/tox.ini b/testing/web-platform/tests/tools/third_party/h2/tox.ini
new file mode 100644
index 0000000000..971f9a07e9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/tox.ini
@@ -0,0 +1,48 @@
+[tox]
+envlist = py27, py34, py35, py36, py37, py38, pypy, lint, packaging, docs
+
+[testenv]
+deps= -r{toxinidir}/test_requirements.txt
+commands=
+ coverage run -m py.test {posargs}
+ coverage report
+
+[testenv:pypy]
+# temporarily disable coverage testing on PyPy due to performance problems
+commands= py.test {posargs}
+
+[testenv:lint]
+basepython=python3.7
+deps = flake8==3.7.8
+commands = flake8 --max-complexity 10 h2 test
+
+[testenv:docs]
+basepython=python3.7
+deps = sphinx==2.2.0
+changedir = {toxinidir}/docs
+whitelist_externals = rm
+commands =
+ rm -rf build
+ sphinx-build -nW -b html -d build/doctrees source build/html
+
+[testenv:graphs]
+basepython=python2.7
+deps = graphviz==0.13
+commands =
+ python visualizer/visualize.py -i docs/source/_static
+
+[testenv:packaging]
+basepython=python3.7
+deps =
+ check-manifest==0.39
+ readme-renderer==24.0
+commands =
+ check-manifest
+ python setup.py check --metadata --restructuredtext --strict
+
+[testenv:h2spec]
+basepython=python3.6
+deps = twisted[tls]==19.7.0
+whitelist_externals = {toxinidir}/test/h2spectest.sh
+commands =
+ {toxinidir}/test/h2spectest.sh
diff --git a/testing/web-platform/tests/tools/third_party/h2/utils/backport.sh b/testing/web-platform/tests/tools/third_party/h2/utils/backport.sh
new file mode 100755
index 0000000000..273c1479b2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/utils/backport.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+
+# This script is invoked as follows: the first argument is the target branch
+# for the backport. All following arguments are considered the "commit spec",
+# and will be passed to cherry-pick.
+
+TARGET_BRANCH="$1"
+PR_BRANCH="backport-${TARGET_BRANCH}"
+COMMIT_SPEC="${@:2}"
+
+if ! git checkout "$TARGET_BRANCH"; then
+ echo "Failed to checkout $TARGET_BRANCH"
+ exit 1
+fi
+
+if ! git pull --ff-only; then
+ echo "Unable to update $TARGET_BRANCH"
+ exit 2
+fi
+
+if ! git checkout -b "$PR_BRANCH"; then
+ echo "Failed to open new branch $PR_BRANCH"
+ exit 3
+fi
+
+if ! git cherry-pick -x $COMMIT_SPEC; then
+ echo "Cherry-pick failed. Please fix up manually."
+else
+ echo "Clean backport. Add changelog and open PR."
+fi
+
diff --git a/testing/web-platform/tests/tools/third_party/h2/visualizer/NOTICES.visualizer b/testing/web-platform/tests/tools/third_party/h2/visualizer/NOTICES.visualizer
new file mode 100644
index 0000000000..202ca64e17
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/visualizer/NOTICES.visualizer
@@ -0,0 +1,24 @@
+This module contains code inspired by and borrowed from Automat. That code was
+made available under the following license:
+
+Copyright (c) 2014
+Rackspace
+
+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/testing/web-platform/tests/tools/third_party/h2/visualizer/visualize.py b/testing/web-platform/tests/tools/third_party/h2/visualizer/visualize.py
new file mode 100644
index 0000000000..1fd3f179cb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/h2/visualizer/visualize.py
@@ -0,0 +1,252 @@
+# -*- coding: utf-8 -*-
+"""
+State Machine Visualizer
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+This code provides a module that can use graphviz to visualise the state
+machines included in hyper-h2. These visualisations can be used as part of the
+documentation of hyper-h2, and as a reference material to understand how the
+state machines function.
+
+The code in this module is heavily inspired by code in Automat, which can be
+found here: https://github.com/glyph/automat. For details on the licensing of
+Automat, please see the NOTICES.visualizer file in this folder.
+
+This module is very deliberately not shipped with the rest of hyper-h2. This is
+because it is of minimal value to users who are installing hyper-h2: its use
+is only really for the developers of hyper-h2.
+"""
+from __future__ import print_function
+import argparse
+import collections
+import sys
+
+import graphviz
+import graphviz.files
+
+import h2.connection
+import h2.stream
+
+
+StateMachine = collections.namedtuple(
+ 'StateMachine', ['fqdn', 'machine', 'states', 'inputs', 'transitions']
+)
+
+
+# This is all the state machines we currently know about and will render.
+# If any new state machines are added, they should be inserted here.
+STATE_MACHINES = [
+ StateMachine(
+ fqdn='h2.connection.H2ConnectionStateMachine',
+ machine=h2.connection.H2ConnectionStateMachine,
+ states=h2.connection.ConnectionState,
+ inputs=h2.connection.ConnectionInputs,
+ transitions=h2.connection.H2ConnectionStateMachine._transitions,
+ ),
+ StateMachine(
+ fqdn='h2.stream.H2StreamStateMachine',
+ machine=h2.stream.H2StreamStateMachine,
+ states=h2.stream.StreamState,
+ inputs=h2.stream.StreamInputs,
+ transitions=h2.stream._transitions,
+ ),
+]
+
+
+def quote(s):
+ return '"{}"'.format(s.replace('"', r'\"'))
+
+
+def html(s):
+ return '<{}>'.format(s)
+
+
+def element(name, *children, **attrs):
+ """
+ Construct a string from the HTML element description.
+ """
+ formatted_attributes = ' '.join(
+ '{}={}'.format(key, quote(str(value)))
+ for key, value in sorted(attrs.items())
+ )
+ formatted_children = ''.join(children)
+ return u'<{name} {attrs}>{children}</{name}>'.format(
+ name=name,
+ attrs=formatted_attributes,
+ children=formatted_children
+ )
+
+
+def row_for_output(event, side_effect):
+ """
+ Given an output tuple (an event and its side effect), generates a table row
+ from it.
+ """
+ point_size = {'point-size': '9'}
+ event_cell = element(
+ "td",
+ element("font", enum_member_name(event), **point_size)
+ )
+ side_effect_name = (
+ function_name(side_effect) if side_effect is not None else "None"
+ )
+ side_effect_cell = element(
+ "td",
+ element("font", side_effect_name, **point_size)
+ )
+ return element("tr", event_cell, side_effect_cell)
+
+
+def table_maker(initial_state, final_state, outputs, port):
+ """
+ Construct an HTML table to label a state transition.
+ """
+ header = "{} -&gt; {}".format(
+ enum_member_name(initial_state), enum_member_name(final_state)
+ )
+ header_row = element(
+ "tr",
+ element(
+ "td",
+ element(
+ "font",
+ header,
+ face="menlo-italic"
+ ),
+ port=port,
+ colspan="2",
+ )
+ )
+ rows = [header_row]
+ rows.extend(row_for_output(*output) for output in outputs)
+ return element("table", *rows)
+
+
+def enum_member_name(state):
+ """
+ All enum member names have the form <EnumClassName>.<EnumMemberName>. For
+ our rendering we only want the member name, so we take their representation
+ and split it.
+ """
+ return str(state).split('.', 1)[1]
+
+
+def function_name(func):
+ """
+ Given a side-effect function, return its string name.
+ """
+ return func.__name__
+
+
+def build_digraph(state_machine):
+ """
+ Produce a L{graphviz.Digraph} object from a state machine.
+ """
+ digraph = graphviz.Digraph(node_attr={'fontname': 'Menlo'},
+ edge_attr={'fontname': 'Menlo'},
+ graph_attr={'dpi': '200'})
+
+ # First, add the states as nodes.
+ seen_first_state = False
+ for state in state_machine.states:
+ if not seen_first_state:
+ state_shape = "bold"
+ font_name = "Menlo-Bold"
+ else:
+ state_shape = ""
+ font_name = "Menlo"
+ digraph.node(enum_member_name(state),
+ fontame=font_name,
+ shape="ellipse",
+ style=state_shape,
+ color="blue")
+ seen_first_state = True
+
+ # We frequently have vary many inputs that all trigger the same state
+ # transition, and only differ in terms of their input and side-effect. It
+ # would be polite to say that graphviz does not handle this very well. So
+ # instead we *collapse* the state transitions all into the one edge, and
+ # then provide a label that displays a table of all the inputs and their
+ # associated side effects.
+ transitions = collections.defaultdict(list)
+ for transition in state_machine.transitions.items():
+ initial_state, event = transition[0]
+ side_effect, final_state = transition[1]
+ transition_key = (initial_state, final_state)
+ transitions[transition_key].append((event, side_effect))
+
+ for n, (transition_key, outputs) in enumerate(transitions.items()):
+ this_transition = "t{}".format(n)
+ initial_state, final_state = transition_key
+
+ port = "tableport"
+ table = table_maker(
+ initial_state=initial_state,
+ final_state=final_state,
+ outputs=outputs,
+ port=port
+ )
+
+ digraph.node(this_transition,
+ label=html(table), margin="0.2", shape="none")
+
+ digraph.edge(enum_member_name(initial_state),
+ '{}:{}:w'.format(this_transition, port),
+ arrowhead="none")
+ digraph.edge('{}:{}:e'.format(this_transition, port),
+ enum_member_name(final_state))
+
+ return digraph
+
+
+def main():
+ """
+ Renders all the state machines in hyper-h2 into images.
+ """
+ program_name = sys.argv[0]
+ argv = sys.argv[1:]
+
+ description = """
+ Visualize hyper-h2 state machines as graphs.
+ """
+ epilog = """
+ You must have the graphviz tool suite installed. Please visit
+ http://www.graphviz.org for more information.
+ """
+
+ argument_parser = argparse.ArgumentParser(
+ prog=program_name,
+ description=description,
+ epilog=epilog
+ )
+ argument_parser.add_argument(
+ '--image-directory',
+ '-i',
+ help="Where to write out image files.",
+ default=".h2_visualize"
+ )
+ argument_parser.add_argument(
+ '--view',
+ '-v',
+ help="View rendered graphs with default image viewer",
+ default=False,
+ action="store_true"
+ )
+ args = argument_parser.parse_args(argv)
+
+ for state_machine in STATE_MACHINES:
+ print(state_machine.fqdn, '...discovered')
+
+ digraph = build_digraph(state_machine)
+
+ if args.image_directory:
+ digraph.format = "png"
+ digraph.render(filename="{}.dot".format(state_machine.fqdn),
+ directory=args.image_directory,
+ view=args.view,
+ cleanup=True)
+ print(state_machine.fqdn, "...wrote image into", args.image_directory)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/testing/web-platform/tests/tools/third_party/hpack/CONTRIBUTORS.rst b/testing/web-platform/tests/tools/third_party/hpack/CONTRIBUTORS.rst
new file mode 100644
index 0000000000..f56d156eea
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/CONTRIBUTORS.rst
@@ -0,0 +1,62 @@
+Hyper is written and maintained by Cory Benfield and various contributors:
+
+Development Lead
+````````````````
+
+- Cory Benfield <cory@lukasa.co.uk>
+
+Contributors (hpack)
+````````````````````
+In chronological order:
+
+- Sriram Ganesan (@elricL)
+
+ - Implemented the Huffman encoding/decoding logic.
+
+- Tatsuhiro Tsujikawa (@tatsuhiro-t)
+
+ - Improved compression efficiency.
+
+- Jim Carreer (@jimcarreer)
+
+ - Support for 'never indexed' header fields.
+ - Refactor of header table code.
+ - Add support for returning bytestring headers instead of UTF-8 decoded ones.
+
+- Eugene Obukhov (@irvind)
+
+ - Improved decoding efficiency.
+
+- Ian Foote (@Ian-Foote)
+
+ - 25% performance improvement to integer decode.
+
+- Davey Shafik (@dshafik)
+
+ - More testing.
+
+- Seth Michael Larson (@SethMichaelLarson)
+
+ - Code cleanups.
+
+Contributors (hyper)
+````````````````````
+
+In chronological order:
+
+- Alek Storm (@alekstorm)
+
+ - Implemented Python 2.7 support.
+ - Implemented HTTP/2 draft 10 support.
+ - Implemented server push.
+
+- Tetsuya Morimoto (@t2y)
+
+ - Fixed a bug where large or incomplete frames were not handled correctly.
+ - Added hyper command-line tool.
+ - General code cleanups.
+
+- Jerome De Cuyper (@jdecuyper)
+
+ - Updated documentation and tests.
+
diff --git a/testing/web-platform/tests/tools/third_party/hpack/HISTORY.rst b/testing/web-platform/tests/tools/third_party/hpack/HISTORY.rst
new file mode 100644
index 0000000000..37b2d9c009
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/HISTORY.rst
@@ -0,0 +1,134 @@
+Release History
+===============
+
+3.0.0 (2017-03-29)
+------------------
+
+**API Changes (Backward Incompatible)**
+
+- Removed nghttp2 support. This support had rotted and was essentially
+ non-functional, so it has now been removed until someone has time to re-add
+ the support in a functional form.
+- Attempts by the encoder to exceed the maximum allowed header table size via
+ dynamic table size updates (or the absence thereof) are now forbidden.
+
+**API Changes (Backward Compatible)**
+
+- Added a new ``InvalidTableSizeError`` thrown when the encoder does not
+ respect the maximum table size set by the user.
+- Added a ``Decoder.max_allowed_table_size`` field that sets the maximum
+ allowed size of the decoder header table. See the documentation for an
+ indication of how this should be used.
+
+**Bugfixes**
+
+- Up to 25% performance improvement decoding HPACK-packed integers, depending
+ on the platform.
+- HPACK now tolerates receiving multiple header table size changes in sequence,
+ rather than only one.
+- HPACK now forbids header table size changes anywhere but first in a header
+ block, as required by RFC 7541 § 4.2.
+- Other miscellaneous performance improvements.
+
+2.3.0 (2016-08-04)
+------------------
+
+**Security Fixes**
+
+- CVE-2016-6581: HPACK Bomb. This release now enforces a maximum value of the
+ decompressed size of the header list. This is to avoid the so-called "HPACK
+ Bomb" vulnerability, which is caused when a malicious peer sends a compressed
+ HPACK body that decompresses to a gigantic header list size.
+
+ This also adds a ``OversizedHeaderListError``, which is thrown by the
+ ``decode`` method if the maximum header list size is being violated. This
+ places the HPACK decoder into a broken state: it must not be used after this
+ exception is thrown.
+
+ This also adds a ``max_header_list_size`` to the ``Decoder`` object. This
+ controls the maximum allowable decompressed size of the header list. By
+ default this is set to 64kB.
+
+2.2.0 (2016-04-20)
+------------------
+
+**API Changes (Backward Compatible)**
+
+- Added ``HeaderTuple`` and ``NeverIndexedHeaderTuple`` classes that signal
+ whether a given header field may ever be indexed in HTTP/2 header
+ compression.
+- Changed ``Decoder.decode()`` to return the newly added ``HeaderTuple`` class
+ and subclass. These objects behave like two-tuples, so this change does not
+ break working code.
+
+**Bugfixes**
+
+- Improve Huffman decoding speed by 4x using an approach borrowed from nghttp2.
+- Improve HPACK decoding speed by 10% by caching header table sizes.
+
+2.1.1 (2016-03-16)
+------------------
+
+**Bugfixes**
+
+- When passing a dictionary or dictionary subclass to ``Encoder.encode``, HPACK
+ now ensures that HTTP/2 special headers (headers whose names begin with
+ ``:`` characters) appear first in the header block.
+
+2.1.0 (2016-02-02)
+------------------
+
+**API Changes (Backward Compatible)**
+
+- Added new ``InvalidTableIndex`` exception, a subclass of
+ ``HPACKDecodingError``.
+- Instead of throwing ``IndexError`` when encountering invalid encoded integers
+ HPACK now throws ``HPACKDecodingError``.
+- Instead of throwing ``UnicodeDecodeError`` when encountering headers that are
+ not UTF-8 encoded, HPACK now throws ``HPACKDecodingError``.
+- Instead of throwing ``IndexError`` when encountering invalid table offsets,
+ HPACK now throws ``InvalidTableIndex``.
+- Added ``raw`` flag to ``decode``, allowing ``decode`` to return bytes instead
+ of attempting to decode the headers as UTF-8.
+
+**Bugfixes**
+
+- ``memoryview`` objects are now used when decoding HPACK, improving the
+ performance by avoiding unnecessary data copies.
+
+2.0.1 (2015-11-09)
+------------------
+
+- Fixed a bug where the Python HPACK implementation would only emit header
+ table size changes for the total change between one header block and another,
+ rather than for the entire sequence of changes.
+
+2.0.0 (2015-10-12)
+------------------
+
+- Remove unused ``HPACKEncodingError``.
+- Add the shortcut ability to import the public API (``Encoder``, ``Decoder``,
+ ``HPACKError``, ``HPACKDecodingError``) directly, rather than from
+ ``hpack.hpack``.
+
+1.1.0 (2015-07-07)
+------------------
+
+- Add support for emitting 'never indexed' header fields, by using an optional
+ third element in the header tuple. With thanks to @jimcarreer!
+
+1.0.1 (2015-04-19)
+------------------
+
+- Header fields that have names matching header table entries are now added to
+ the header table. This improves compression efficiency at the cost of
+ slightly more table operations. With thanks to `Tatsuhiro Tsujikawa`_.
+
+.. _Tatsuhiro Tsujikawa: https://github.com/tatsuhiro-t
+
+1.0.0 (2015-04-13)
+------------------
+
+- Initial fork of the code from `hyper`_.
+
+.. _hyper: https://hyper.readthedocs.org/
diff --git a/testing/web-platform/tests/tools/third_party/hpack/LICENSE b/testing/web-platform/tests/tools/third_party/hpack/LICENSE
new file mode 100644
index 0000000000..d24c351e18
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Cory Benfield
+
+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/testing/web-platform/tests/tools/third_party/hpack/MANIFEST.in b/testing/web-platform/tests/tools/third_party/hpack/MANIFEST.in
new file mode 100644
index 0000000000..2f464676cb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/MANIFEST.in
@@ -0,0 +1,2 @@
+include README.rst LICENSE CONTRIBUTORS.rst HISTORY.rst
+
diff --git a/testing/web-platform/tests/tools/third_party/hpack/PKG-INFO b/testing/web-platform/tests/tools/third_party/hpack/PKG-INFO
new file mode 100644
index 0000000000..c2a3a1a7f5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/PKG-INFO
@@ -0,0 +1,199 @@
+Metadata-Version: 1.1
+Name: hpack
+Version: 3.0.0
+Summary: Pure-Python HPACK header compression
+Home-page: http://hyper.rtfd.org
+Author: Cory Benfield
+Author-email: cory@lukasa.co.uk
+License: MIT License
+Description: ========================================
+ hpack: HTTP/2 Header Encoding for Python
+ ========================================
+
+ .. image:: https://raw.github.com/Lukasa/hyper/development/docs/source/images/hyper.png
+
+ .. image:: https://travis-ci.org/python-hyper/hpack.png?branch=master
+ :target: https://travis-ci.org/python-hyper/hpack
+
+ This module contains a pure-Python HTTP/2 header encoding (HPACK) logic for use
+ in Python programs that implement HTTP/2. It also contains a compatibility
+ layer that automatically enables the use of ``nghttp2`` if it's available.
+
+ Documentation
+ =============
+
+ Documentation is available at http://python-hyper.org/hpack/.
+
+ Contributing
+ ============
+
+ ``hpack`` welcomes contributions from anyone! Unlike many other projects we are
+ happy to accept cosmetic contributions and small contributions, in addition to
+ large feature requests and changes.
+
+ Before you contribute (either by opening an issue or filing a pull request),
+ please `read the contribution guidelines`_.
+
+ .. _read the contribution guidelines: http://hyper.readthedocs.org/en/development/contributing.html
+
+ License
+ =======
+
+ ``hpack`` is made available under the MIT License. For more details, see the
+ ``LICENSE`` file in the repository.
+
+ Authors
+ =======
+
+ ``hpack`` is maintained by Cory Benfield, with contributions from others. For
+ more details about the contributors, please see ``CONTRIBUTORS.rst``.
+
+
+ Release History
+ ===============
+
+ 3.0.0 (2017-03-29)
+ ------------------
+
+ **API Changes (Backward Incompatible)**
+
+ - Removed nghttp2 support. This support had rotted and was essentially
+ non-functional, so it has now been removed until someone has time to re-add
+ the support in a functional form.
+ - Attempts by the encoder to exceed the maximum allowed header table size via
+ dynamic table size updates (or the absence thereof) are now forbidden.
+
+ **API Changes (Backward Compatible)**
+
+ - Added a new ``InvalidTableSizeError`` thrown when the encoder does not
+ respect the maximum table size set by the user.
+ - Added a ``Decoder.max_allowed_table_size`` field that sets the maximum
+ allowed size of the decoder header table. See the documentation for an
+ indication of how this should be used.
+
+ **Bugfixes**
+
+ - Up to 25% performance improvement decoding HPACK-packed integers, depending
+ on the platform.
+ - HPACK now tolerates receiving multiple header table size changes in sequence,
+ rather than only one.
+ - HPACK now forbids header table size changes anywhere but first in a header
+ block, as required by RFC 7541 § 4.2.
+ - Other miscellaneous performance improvements.
+
+ 2.3.0 (2016-08-04)
+ ------------------
+
+ **Security Fixes**
+
+ - CVE-2016-6581: HPACK Bomb. This release now enforces a maximum value of the
+ decompressed size of the header list. This is to avoid the so-called "HPACK
+ Bomb" vulnerability, which is caused when a malicious peer sends a compressed
+ HPACK body that decompresses to a gigantic header list size.
+
+ This also adds a ``OversizedHeaderListError``, which is thrown by the
+ ``decode`` method if the maximum header list size is being violated. This
+ places the HPACK decoder into a broken state: it must not be used after this
+ exception is thrown.
+
+ This also adds a ``max_header_list_size`` to the ``Decoder`` object. This
+ controls the maximum allowable decompressed size of the header list. By
+ default this is set to 64kB.
+
+ 2.2.0 (2016-04-20)
+ ------------------
+
+ **API Changes (Backward Compatible)**
+
+ - Added ``HeaderTuple`` and ``NeverIndexedHeaderTuple`` classes that signal
+ whether a given header field may ever be indexed in HTTP/2 header
+ compression.
+ - Changed ``Decoder.decode()`` to return the newly added ``HeaderTuple`` class
+ and subclass. These objects behave like two-tuples, so this change does not
+ break working code.
+
+ **Bugfixes**
+
+ - Improve Huffman decoding speed by 4x using an approach borrowed from nghttp2.
+ - Improve HPACK decoding speed by 10% by caching header table sizes.
+
+ 2.1.1 (2016-03-16)
+ ------------------
+
+ **Bugfixes**
+
+ - When passing a dictionary or dictionary subclass to ``Encoder.encode``, HPACK
+ now ensures that HTTP/2 special headers (headers whose names begin with
+ ``:`` characters) appear first in the header block.
+
+ 2.1.0 (2016-02-02)
+ ------------------
+
+ **API Changes (Backward Compatible)**
+
+ - Added new ``InvalidTableIndex`` exception, a subclass of
+ ``HPACKDecodingError``.
+ - Instead of throwing ``IndexError`` when encountering invalid encoded integers
+ HPACK now throws ``HPACKDecodingError``.
+ - Instead of throwing ``UnicodeDecodeError`` when encountering headers that are
+ not UTF-8 encoded, HPACK now throws ``HPACKDecodingError``.
+ - Instead of throwing ``IndexError`` when encountering invalid table offsets,
+ HPACK now throws ``InvalidTableIndex``.
+ - Added ``raw`` flag to ``decode``, allowing ``decode`` to return bytes instead
+ of attempting to decode the headers as UTF-8.
+
+ **Bugfixes**
+
+ - ``memoryview`` objects are now used when decoding HPACK, improving the
+ performance by avoiding unnecessary data copies.
+
+ 2.0.1 (2015-11-09)
+ ------------------
+
+ - Fixed a bug where the Python HPACK implementation would only emit header
+ table size changes for the total change between one header block and another,
+ rather than for the entire sequence of changes.
+
+ 2.0.0 (2015-10-12)
+ ------------------
+
+ - Remove unused ``HPACKEncodingError``.
+ - Add the shortcut ability to import the public API (``Encoder``, ``Decoder``,
+ ``HPACKError``, ``HPACKDecodingError``) directly, rather than from
+ ``hpack.hpack``.
+
+ 1.1.0 (2015-07-07)
+ ------------------
+
+ - Add support for emitting 'never indexed' header fields, by using an optional
+ third element in the header tuple. With thanks to @jimcarreer!
+
+ 1.0.1 (2015-04-19)
+ ------------------
+
+ - Header fields that have names matching header table entries are now added to
+ the header table. This improves compression efficiency at the cost of
+ slightly more table operations. With thanks to `Tatsuhiro Tsujikawa`_.
+
+ .. _Tatsuhiro Tsujikawa: https://github.com/tatsuhiro-t
+
+ 1.0.0 (2015-04-13)
+ ------------------
+
+ - Initial fork of the code from `hyper`_.
+
+ .. _hyper: https://hyper.readthedocs.org/
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+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.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: Implementation :: CPython
diff --git a/testing/web-platform/tests/tools/third_party/hpack/README.rst b/testing/web-platform/tests/tools/third_party/hpack/README.rst
new file mode 100644
index 0000000000..1a04397b94
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/README.rst
@@ -0,0 +1,41 @@
+========================================
+hpack: HTTP/2 Header Encoding for Python
+========================================
+
+.. image:: https://raw.github.com/Lukasa/hyper/development/docs/source/images/hyper.png
+
+.. image:: https://travis-ci.org/python-hyper/hpack.png?branch=master
+ :target: https://travis-ci.org/python-hyper/hpack
+
+This module contains a pure-Python HTTP/2 header encoding (HPACK) logic for use
+in Python programs that implement HTTP/2. It also contains a compatibility
+layer that automatically enables the use of ``nghttp2`` if it's available.
+
+Documentation
+=============
+
+Documentation is available at http://python-hyper.org/hpack/.
+
+Contributing
+============
+
+``hpack`` welcomes contributions from anyone! Unlike many other projects we are
+happy to accept cosmetic contributions and small contributions, in addition to
+large feature requests and changes.
+
+Before you contribute (either by opening an issue or filing a pull request),
+please `read the contribution guidelines`_.
+
+.. _read the contribution guidelines: http://hyper.readthedocs.org/en/development/contributing.html
+
+License
+=======
+
+``hpack`` is made available under the MIT License. For more details, see the
+``LICENSE`` file in the repository.
+
+Authors
+=======
+
+``hpack`` is maintained by Cory Benfield, with contributions from others. For
+more details about the contributors, please see ``CONTRIBUTORS.rst``.
diff --git a/testing/web-platform/tests/tools/third_party/hpack/hpack/__init__.py b/testing/web-platform/tests/tools/third_party/hpack/hpack/__init__.py
new file mode 100644
index 0000000000..22edde2ce3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/hpack/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+"""
+hpack
+~~~~~
+
+HTTP/2 header encoding for Python.
+"""
+from .hpack import Encoder, Decoder
+from .struct import HeaderTuple, NeverIndexedHeaderTuple
+from .exceptions import (
+ HPACKError, HPACKDecodingError, InvalidTableIndex, OversizedHeaderListError
+)
+
+__all__ = [
+ 'Encoder', 'Decoder', 'HPACKError', 'HPACKDecodingError',
+ 'InvalidTableIndex', 'HeaderTuple', 'NeverIndexedHeaderTuple',
+ 'OversizedHeaderListError'
+]
+
+__version__ = '3.0.0'
diff --git a/testing/web-platform/tests/tools/third_party/hpack/hpack/compat.py b/testing/web-platform/tests/tools/third_party/hpack/hpack/compat.py
new file mode 100644
index 0000000000..4fcaad439f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/hpack/compat.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+"""
+hpack/compat
+~~~~~~~~~~~~
+
+Normalizes the Python 2/3 API for internal use.
+"""
+import sys
+
+
+_ver = sys.version_info
+is_py2 = _ver[0] == 2
+is_py3 = _ver[0] == 3
+
+if is_py2:
+ def to_byte(char):
+ return ord(char)
+
+ def decode_hex(b):
+ return b.decode('hex')
+
+ def to_bytes(b):
+ if isinstance(b, memoryview):
+ return b.tobytes()
+ else:
+ return bytes(b)
+
+ unicode = unicode # noqa
+ bytes = str
+
+elif is_py3:
+ def to_byte(char):
+ return char
+
+ def decode_hex(b):
+ return bytes.fromhex(b)
+
+ def to_bytes(b):
+ return bytes(b)
+
+ unicode = str
+ bytes = bytes
diff --git a/testing/web-platform/tests/tools/third_party/hpack/hpack/exceptions.py b/testing/web-platform/tests/tools/third_party/hpack/hpack/exceptions.py
new file mode 100644
index 0000000000..571ba98f2c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/hpack/exceptions.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+"""
+hyper/http20/exceptions
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This defines exceptions used in the HTTP/2 portion of hyper.
+"""
+
+
+class HPACKError(Exception):
+ """
+ The base class for all ``hpack`` exceptions.
+ """
+ pass
+
+
+class HPACKDecodingError(HPACKError):
+ """
+ An error has been encountered while performing HPACK decoding.
+ """
+ pass
+
+
+class InvalidTableIndex(HPACKDecodingError):
+ """
+ An invalid table index was received.
+ """
+ pass
+
+
+class OversizedHeaderListError(HPACKDecodingError):
+ """
+ A header list that was larger than we allow has been received. This may be
+ a DoS attack.
+
+ .. versionadded:: 2.3.0
+ """
+ pass
+
+
+class InvalidTableSizeError(HPACKDecodingError):
+ """
+ An attempt was made to change the decoder table size to a value larger than
+ allowed, or the list was shrunk and the remote peer didn't shrink their
+ table size.
+
+ .. versionadded:: 3.0.0
+ """
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/hpack/hpack/hpack.py b/testing/web-platform/tests/tools/third_party/hpack/hpack/hpack.py
new file mode 100644
index 0000000000..f8e808bec9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/hpack/hpack.py
@@ -0,0 +1,629 @@
+# -*- coding: utf-8 -*-
+"""
+hpack/hpack
+~~~~~~~~~~~
+
+Implements the HPACK header compression algorithm as detailed by the IETF.
+"""
+import logging
+
+from .table import HeaderTable, table_entry_size
+from .compat import to_byte, to_bytes
+from .exceptions import (
+ HPACKDecodingError, OversizedHeaderListError, InvalidTableSizeError
+)
+from .huffman import HuffmanEncoder
+from .huffman_constants import (
+ REQUEST_CODES, REQUEST_CODES_LENGTH
+)
+from .huffman_table import decode_huffman
+from .struct import HeaderTuple, NeverIndexedHeaderTuple
+
+log = logging.getLogger(__name__)
+
+INDEX_NONE = b'\x00'
+INDEX_NEVER = b'\x10'
+INDEX_INCREMENTAL = b'\x40'
+
+# Precompute 2^i for 1-8 for use in prefix calcs.
+# Zero index is not used but there to save a subtraction
+# as prefix numbers are not zero indexed.
+_PREFIX_BIT_MAX_NUMBERS = [(2 ** i) - 1 for i in range(9)]
+
+try: # pragma: no cover
+ basestring = basestring
+except NameError: # pragma: no cover
+ basestring = (str, bytes)
+
+
+# We default the maximum header list we're willing to accept to 64kB. That's a
+# lot of headers, but if applications want to raise it they can do.
+DEFAULT_MAX_HEADER_LIST_SIZE = 2 ** 16
+
+
+def _unicode_if_needed(header, raw):
+ """
+ Provides a header as a unicode string if raw is False, otherwise returns
+ it as a bytestring.
+ """
+ name = to_bytes(header[0])
+ value = to_bytes(header[1])
+ if not raw:
+ name = name.decode('utf-8')
+ value = value.decode('utf-8')
+ return header.__class__(name, value)
+
+
+def encode_integer(integer, prefix_bits):
+ """
+ This encodes an integer according to the wacky integer encoding rules
+ defined in the HPACK spec.
+ """
+ log.debug("Encoding %d with %d bits", integer, prefix_bits)
+
+ if integer < 0:
+ raise ValueError(
+ "Can only encode positive integers, got %s" % integer
+ )
+
+ if prefix_bits < 1 or prefix_bits > 8:
+ raise ValueError(
+ "Prefix bits must be between 1 and 8, got %s" % prefix_bits
+ )
+
+ max_number = _PREFIX_BIT_MAX_NUMBERS[prefix_bits]
+
+ if integer < max_number:
+ return bytearray([integer]) # Seriously?
+ else:
+ elements = [max_number]
+ integer -= max_number
+
+ while integer >= 128:
+ elements.append((integer & 127) + 128)
+ integer >>= 7
+
+ elements.append(integer)
+
+ return bytearray(elements)
+
+
+def decode_integer(data, prefix_bits):
+ """
+ This decodes an integer according to the wacky integer encoding rules
+ defined in the HPACK spec. Returns a tuple of the decoded integer and the
+ number of bytes that were consumed from ``data`` in order to get that
+ integer.
+ """
+ if prefix_bits < 1 or prefix_bits > 8:
+ raise ValueError(
+ "Prefix bits must be between 1 and 8, got %s" % prefix_bits
+ )
+
+ max_number = _PREFIX_BIT_MAX_NUMBERS[prefix_bits]
+ index = 1
+ shift = 0
+ mask = (0xFF >> (8 - prefix_bits))
+
+ try:
+ number = to_byte(data[0]) & mask
+ if number == max_number:
+ while True:
+ next_byte = to_byte(data[index])
+ index += 1
+
+ if next_byte >= 128:
+ number += (next_byte - 128) << shift
+ else:
+ number += next_byte << shift
+ break
+ shift += 7
+
+ except IndexError:
+ raise HPACKDecodingError(
+ "Unable to decode HPACK integer representation from %r" % data
+ )
+
+ log.debug("Decoded %d, consumed %d bytes", number, index)
+
+ return number, index
+
+
+def _dict_to_iterable(header_dict):
+ """
+ This converts a dictionary to an iterable of two-tuples. This is a
+ HPACK-specific function becuase it pulls "special-headers" out first and
+ then emits them.
+ """
+ assert isinstance(header_dict, dict)
+ keys = sorted(
+ header_dict.keys(),
+ key=lambda k: not _to_bytes(k).startswith(b':')
+ )
+ for key in keys:
+ yield key, header_dict[key]
+
+
+def _to_bytes(string):
+ """
+ Convert string to bytes.
+ """
+ if not isinstance(string, basestring): # pragma: no cover
+ string = str(string)
+
+ return string if isinstance(string, bytes) else string.encode('utf-8')
+
+
+class Encoder(object):
+ """
+ An HPACK encoder object. This object takes HTTP headers and emits encoded
+ HTTP/2 header blocks.
+ """
+
+ def __init__(self):
+ self.header_table = HeaderTable()
+ self.huffman_coder = HuffmanEncoder(
+ REQUEST_CODES, REQUEST_CODES_LENGTH
+ )
+ self.table_size_changes = []
+
+ @property
+ def header_table_size(self):
+ """
+ Controls the size of the HPACK header table.
+ """
+ return self.header_table.maxsize
+
+ @header_table_size.setter
+ def header_table_size(self, value):
+ self.header_table.maxsize = value
+ if self.header_table.resized:
+ self.table_size_changes.append(value)
+
+ def encode(self, headers, huffman=True):
+ """
+ Takes a set of headers and encodes them into a HPACK-encoded header
+ block.
+
+ :param headers: The headers to encode. Must be either an iterable of
+ tuples, an iterable of :class:`HeaderTuple
+ <hpack.struct.HeaderTuple>`, or a ``dict``.
+
+ If an iterable of tuples, the tuples may be either
+ two-tuples or three-tuples. If they are two-tuples, the
+ tuples must be of the format ``(name, value)``. If they
+ are three-tuples, they must be of the format
+ ``(name, value, sensitive)``, where ``sensitive`` is a
+ boolean value indicating whether the header should be
+ added to header tables anywhere. If not present,
+ ``sensitive`` defaults to ``False``.
+
+ If an iterable of :class:`HeaderTuple
+ <hpack.struct.HeaderTuple>`, the tuples must always be
+ two-tuples. Instead of using ``sensitive`` as a third
+ tuple entry, use :class:`NeverIndexedHeaderTuple
+ <hpack.struct.NeverIndexedHeaderTuple>` to request that
+ the field never be indexed.
+
+ .. warning:: HTTP/2 requires that all special headers
+ (headers whose names begin with ``:`` characters)
+ appear at the *start* of the header block. While
+ this method will ensure that happens for ``dict``
+ subclasses, callers using any other iterable of
+ tuples **must** ensure they place their special
+ headers at the start of the iterable.
+
+ For efficiency reasons users should prefer to use
+ iterables of two-tuples: fixing the ordering of
+ dictionary headers is an expensive operation that
+ should be avoided if possible.
+
+ :param huffman: (optional) Whether to Huffman-encode any header sent as
+ a literal value. Except for use when debugging, it is
+ recommended that this be left enabled.
+
+ :returns: A bytestring containing the HPACK-encoded header block.
+ """
+ # Transforming the headers into a header block is a procedure that can
+ # be modeled as a chain or pipe. First, the headers are encoded. This
+ # encoding can be done a number of ways. If the header name-value pair
+ # are already in the header table we can represent them using the
+ # indexed representation: the same is true if they are in the static
+ # table. Otherwise, a literal representation will be used.
+ header_block = []
+
+ # Turn the headers into a list of tuples if possible. This is the
+ # natural way to interact with them in HPACK. Because dictionaries are
+ # un-ordered, we need to make sure we grab the "special" headers first.
+ if isinstance(headers, dict):
+ headers = _dict_to_iterable(headers)
+
+ # Before we begin, if the header table size has been changed we need
+ # to signal all changes since last emission appropriately.
+ if self.header_table.resized:
+ header_block.append(self._encode_table_size_change())
+ self.header_table.resized = False
+
+ # Add each header to the header block
+ for header in headers:
+ sensitive = False
+ if isinstance(header, HeaderTuple):
+ sensitive = not header.indexable
+ elif len(header) > 2:
+ sensitive = header[2]
+
+ header = (_to_bytes(header[0]), _to_bytes(header[1]))
+ header_block.append(self.add(header, sensitive, huffman))
+
+ header_block = b''.join(header_block)
+
+ log.debug("Encoded header block to %s", header_block)
+
+ return header_block
+
+ def add(self, to_add, sensitive, huffman=False):
+ """
+ This function takes a header key-value tuple and serializes it.
+ """
+ log.debug("Adding %s to the header table", to_add)
+
+ name, value = to_add
+
+ # Set our indexing mode
+ indexbit = INDEX_INCREMENTAL if not sensitive else INDEX_NEVER
+
+ # Search for a matching header in the header table.
+ match = self.header_table.search(name, value)
+
+ if match is None:
+ # Not in the header table. Encode using the literal syntax,
+ # and add it to the header table.
+ encoded = self._encode_literal(name, value, indexbit, huffman)
+ if not sensitive:
+ self.header_table.add(name, value)
+ return encoded
+
+ # The header is in the table, break out the values. If we matched
+ # perfectly, we can use the indexed representation: otherwise we
+ # can use the indexed literal.
+ index, name, perfect = match
+
+ if perfect:
+ # Indexed representation.
+ encoded = self._encode_indexed(index)
+ else:
+ # Indexed literal. We are going to add header to the
+ # header table unconditionally. It is a future todo to
+ # filter out headers which are known to be ineffective for
+ # indexing since they just take space in the table and
+ # pushed out other valuable headers.
+ encoded = self._encode_indexed_literal(
+ index, value, indexbit, huffman
+ )
+ if not sensitive:
+ self.header_table.add(name, value)
+
+ return encoded
+
+ def _encode_indexed(self, index):
+ """
+ Encodes a header using the indexed representation.
+ """
+ field = encode_integer(index, 7)
+ field[0] |= 0x80 # we set the top bit
+ return bytes(field)
+
+ def _encode_literal(self, name, value, indexbit, huffman=False):
+ """
+ Encodes a header with a literal name and literal value. If ``indexing``
+ is True, the header will be added to the header table: otherwise it
+ will not.
+ """
+ if huffman:
+ name = self.huffman_coder.encode(name)
+ value = self.huffman_coder.encode(value)
+
+ name_len = encode_integer(len(name), 7)
+ value_len = encode_integer(len(value), 7)
+
+ if huffman:
+ name_len[0] |= 0x80
+ value_len[0] |= 0x80
+
+ return b''.join(
+ [indexbit, bytes(name_len), name, bytes(value_len), value]
+ )
+
+ def _encode_indexed_literal(self, index, value, indexbit, huffman=False):
+ """
+ Encodes a header with an indexed name and a literal value and performs
+ incremental indexing.
+ """
+ if indexbit != INDEX_INCREMENTAL:
+ prefix = encode_integer(index, 4)
+ else:
+ prefix = encode_integer(index, 6)
+
+ prefix[0] |= ord(indexbit)
+
+ if huffman:
+ value = self.huffman_coder.encode(value)
+
+ value_len = encode_integer(len(value), 7)
+
+ if huffman:
+ value_len[0] |= 0x80
+
+ return b''.join([bytes(prefix), bytes(value_len), value])
+
+ def _encode_table_size_change(self):
+ """
+ Produces the encoded form of all header table size change context
+ updates.
+ """
+ block = b''
+ for size_bytes in self.table_size_changes:
+ size_bytes = encode_integer(size_bytes, 5)
+ size_bytes[0] |= 0x20
+ block += bytes(size_bytes)
+ self.table_size_changes = []
+ return block
+
+
+class Decoder(object):
+ """
+ An HPACK decoder object.
+
+ .. versionchanged:: 2.3.0
+ Added ``max_header_list_size`` argument.
+
+ :param max_header_list_size: The maximum decompressed size we will allow
+ for any single header block. This is a protection against DoS attacks
+ that attempt to force the application to expand a relatively small
+ amount of data into a really large header list, allowing enormous
+ amounts of memory to be allocated.
+
+ If this amount of data is exceeded, a `OversizedHeaderListError
+ <hpack.OversizedHeaderListError>` exception will be raised. At this
+ point the connection should be shut down, as the HPACK state will no
+ longer be useable.
+
+ Defaults to 64kB.
+ :type max_header_list_size: ``int``
+ """
+ def __init__(self, max_header_list_size=DEFAULT_MAX_HEADER_LIST_SIZE):
+ self.header_table = HeaderTable()
+
+ #: The maximum decompressed size we will allow for any single header
+ #: block. This is a protection against DoS attacks that attempt to
+ #: force the application to expand a relatively small amount of data
+ #: into a really large header list, allowing enormous amounts of memory
+ #: to be allocated.
+ #:
+ #: If this amount of data is exceeded, a `OversizedHeaderListError
+ #: <hpack.OversizedHeaderListError>` exception will be raised. At this
+ #: point the connection should be shut down, as the HPACK state will no
+ #: longer be usable.
+ #:
+ #: Defaults to 64kB.
+ #:
+ #: .. versionadded:: 2.3.0
+ self.max_header_list_size = max_header_list_size
+
+ #: Maximum allowed header table size.
+ #:
+ #: A HTTP/2 implementation should set this to the most recent value of
+ #: SETTINGS_HEADER_TABLE_SIZE that it sent *and has received an ACK
+ #: for*. Once this setting is set, the actual header table size will be
+ #: checked at the end of each decoding run and whenever it is changed,
+ #: to confirm that it fits in this size.
+ self.max_allowed_table_size = self.header_table.maxsize
+
+ @property
+ def header_table_size(self):
+ """
+ Controls the size of the HPACK header table.
+ """
+ return self.header_table.maxsize
+
+ @header_table_size.setter
+ def header_table_size(self, value):
+ self.header_table.maxsize = value
+
+ def decode(self, data, raw=False):
+ """
+ Takes an HPACK-encoded header block and decodes it into a header set.
+
+ :param data: A bytestring representing a complete HPACK-encoded header
+ block.
+ :param raw: (optional) Whether to return the headers as tuples of raw
+ byte strings or to decode them as UTF-8 before returning
+ them. The default value is False, which returns tuples of
+ Unicode strings
+ :returns: A list of two-tuples of ``(name, value)`` representing the
+ HPACK-encoded headers, in the order they were decoded.
+ :raises HPACKDecodingError: If an error is encountered while decoding
+ the header block.
+ """
+ log.debug("Decoding %s", data)
+
+ data_mem = memoryview(data)
+ headers = []
+ data_len = len(data)
+ inflated_size = 0
+ current_index = 0
+
+ while current_index < data_len:
+ # Work out what kind of header we're decoding.
+ # If the high bit is 1, it's an indexed field.
+ current = to_byte(data[current_index])
+ indexed = True if current & 0x80 else False
+
+ # Otherwise, if the second-highest bit is 1 it's a field that does
+ # alter the header table.
+ literal_index = True if current & 0x40 else False
+
+ # Otherwise, if the third-highest bit is 1 it's an encoding context
+ # update.
+ encoding_update = True if current & 0x20 else False
+
+ if indexed:
+ header, consumed = self._decode_indexed(
+ data_mem[current_index:]
+ )
+ elif literal_index:
+ # It's a literal header that does affect the header table.
+ header, consumed = self._decode_literal_index(
+ data_mem[current_index:]
+ )
+ elif encoding_update:
+ # It's an update to the encoding context. These are forbidden
+ # in a header block after any actual header.
+ if headers:
+ raise HPACKDecodingError(
+ "Table size update not at the start of the block"
+ )
+ consumed = self._update_encoding_context(
+ data_mem[current_index:]
+ )
+ header = None
+ else:
+ # It's a literal header that does not affect the header table.
+ header, consumed = self._decode_literal_no_index(
+ data_mem[current_index:]
+ )
+
+ if header:
+ headers.append(header)
+ inflated_size += table_entry_size(*header)
+
+ if inflated_size > self.max_header_list_size:
+ raise OversizedHeaderListError(
+ "A header list larger than %d has been received" %
+ self.max_header_list_size
+ )
+
+ current_index += consumed
+
+ # Confirm that the table size is lower than the maximum. We do this
+ # here to ensure that we catch when the max has been *shrunk* and the
+ # remote peer hasn't actually done that.
+ self._assert_valid_table_size()
+
+ try:
+ return [_unicode_if_needed(h, raw) for h in headers]
+ except UnicodeDecodeError:
+ raise HPACKDecodingError("Unable to decode headers as UTF-8.")
+
+ def _assert_valid_table_size(self):
+ """
+ Check that the table size set by the encoder is lower than the maximum
+ we expect to have.
+ """
+ if self.header_table_size > self.max_allowed_table_size:
+ raise InvalidTableSizeError(
+ "Encoder did not shrink table size to within the max"
+ )
+
+ def _update_encoding_context(self, data):
+ """
+ Handles a byte that updates the encoding context.
+ """
+ # We've been asked to resize the header table.
+ new_size, consumed = decode_integer(data, 5)
+ if new_size > self.max_allowed_table_size:
+ raise InvalidTableSizeError(
+ "Encoder exceeded max allowable table size"
+ )
+ self.header_table_size = new_size
+ return consumed
+
+ def _decode_indexed(self, data):
+ """
+ Decodes a header represented using the indexed representation.
+ """
+ index, consumed = decode_integer(data, 7)
+ header = HeaderTuple(*self.header_table.get_by_index(index))
+ log.debug("Decoded %s, consumed %d", header, consumed)
+ return header, consumed
+
+ def _decode_literal_no_index(self, data):
+ return self._decode_literal(data, False)
+
+ def _decode_literal_index(self, data):
+ return self._decode_literal(data, True)
+
+ def _decode_literal(self, data, should_index):
+ """
+ Decodes a header represented with a literal.
+ """
+ total_consumed = 0
+
+ # When should_index is true, if the low six bits of the first byte are
+ # nonzero, the header name is indexed.
+ # When should_index is false, if the low four bits of the first byte
+ # are nonzero the header name is indexed.
+ if should_index:
+ indexed_name = to_byte(data[0]) & 0x3F
+ name_len = 6
+ not_indexable = False
+ else:
+ high_byte = to_byte(data[0])
+ indexed_name = high_byte & 0x0F
+ name_len = 4
+ not_indexable = high_byte & 0x10
+
+ if indexed_name:
+ # Indexed header name.
+ index, consumed = decode_integer(data, name_len)
+ name = self.header_table.get_by_index(index)[0]
+
+ total_consumed = consumed
+ length = 0
+ else:
+ # Literal header name. The first byte was consumed, so we need to
+ # move forward.
+ data = data[1:]
+
+ length, consumed = decode_integer(data, 7)
+ name = data[consumed:consumed + length]
+ if len(name) != length:
+ raise HPACKDecodingError("Truncated header block")
+
+ if to_byte(data[0]) & 0x80:
+ name = decode_huffman(name)
+ total_consumed = consumed + length + 1 # Since we moved forward 1.
+
+ data = data[consumed + length:]
+
+ # The header value is definitely length-based.
+ length, consumed = decode_integer(data, 7)
+ value = data[consumed:consumed + length]
+ if len(value) != length:
+ raise HPACKDecodingError("Truncated header block")
+
+ if to_byte(data[0]) & 0x80:
+ value = decode_huffman(value)
+
+ # Updated the total consumed length.
+ total_consumed += length + consumed
+
+ # If we have been told never to index the header field, encode that in
+ # the tuple we use.
+ if not_indexable:
+ header = NeverIndexedHeaderTuple(name, value)
+ else:
+ header = HeaderTuple(name, value)
+
+ # If we've been asked to index this, add it to the header table.
+ if should_index:
+ self.header_table.add(name, value)
+
+ log.debug(
+ "Decoded %s, total consumed %d bytes, indexed %s",
+ header,
+ total_consumed,
+ should_index
+ )
+
+ return header, total_consumed
diff --git a/testing/web-platform/tests/tools/third_party/hpack/hpack/huffman.py b/testing/web-platform/tests/tools/third_party/hpack/hpack/huffman.py
new file mode 100644
index 0000000000..159569cf63
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/hpack/huffman.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+"""
+hpack/huffman_decoder
+~~~~~~~~~~~~~~~~~~~~~
+
+An implementation of a bitwise prefix tree specially built for decoding
+Huffman-coded content where we already know the Huffman table.
+"""
+from .compat import to_byte, decode_hex
+
+
+class HuffmanEncoder(object):
+ """
+ Encodes a string according to the Huffman encoding table defined in the
+ HPACK specification.
+ """
+ def __init__(self, huffman_code_list, huffman_code_list_lengths):
+ self.huffman_code_list = huffman_code_list
+ self.huffman_code_list_lengths = huffman_code_list_lengths
+
+ def encode(self, bytes_to_encode):
+ """
+ Given a string of bytes, encodes them according to the HPACK Huffman
+ specification.
+ """
+ # If handed the empty string, just immediately return.
+ if not bytes_to_encode:
+ return b''
+
+ final_num = 0
+ final_int_len = 0
+
+ # Turn each byte into its huffman code. These codes aren't necessarily
+ # octet aligned, so keep track of how far through an octet we are. To
+ # handle this cleanly, just use a single giant integer.
+ for char in bytes_to_encode:
+ byte = to_byte(char)
+ bin_int_len = self.huffman_code_list_lengths[byte]
+ bin_int = self.huffman_code_list[byte] & (
+ 2 ** (bin_int_len + 1) - 1
+ )
+ final_num <<= bin_int_len
+ final_num |= bin_int
+ final_int_len += bin_int_len
+
+ # Pad out to an octet with ones.
+ bits_to_be_padded = (8 - (final_int_len % 8)) % 8
+ final_num <<= bits_to_be_padded
+ final_num |= (1 << bits_to_be_padded) - 1
+
+ # Convert the number to hex and strip off the leading '0x' and the
+ # trailing 'L', if present.
+ final_num = hex(final_num)[2:].rstrip('L')
+
+ # If this is odd, prepend a zero.
+ final_num = '0' + final_num if len(final_num) % 2 != 0 else final_num
+
+ # This number should have twice as many digits as bytes. If not, we're
+ # missing some leading zeroes. Work out how many bytes we want and how
+ # many digits we have, then add the missing zero digits to the front.
+ total_bytes = (final_int_len + bits_to_be_padded) // 8
+ expected_digits = total_bytes * 2
+
+ if len(final_num) != expected_digits:
+ missing_digits = expected_digits - len(final_num)
+ final_num = ('0' * missing_digits) + final_num
+
+ return decode_hex(final_num)
diff --git a/testing/web-platform/tests/tools/third_party/hpack/hpack/huffman_constants.py b/testing/web-platform/tests/tools/third_party/hpack/hpack/huffman_constants.py
new file mode 100644
index 0000000000..c2b3bb283e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/hpack/huffman_constants.py
@@ -0,0 +1,288 @@
+# -*- coding: utf-8 -*-
+"""
+hpack/huffman_constants
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Defines the constant Huffman table. This takes up an upsetting amount of space,
+but c'est la vie.
+"""
+
+REQUEST_CODES = [
+ 0x1ff8,
+ 0x7fffd8,
+ 0xfffffe2,
+ 0xfffffe3,
+ 0xfffffe4,
+ 0xfffffe5,
+ 0xfffffe6,
+ 0xfffffe7,
+ 0xfffffe8,
+ 0xffffea,
+ 0x3ffffffc,
+ 0xfffffe9,
+ 0xfffffea,
+ 0x3ffffffd,
+ 0xfffffeb,
+ 0xfffffec,
+ 0xfffffed,
+ 0xfffffee,
+ 0xfffffef,
+ 0xffffff0,
+ 0xffffff1,
+ 0xffffff2,
+ 0x3ffffffe,
+ 0xffffff3,
+ 0xffffff4,
+ 0xffffff5,
+ 0xffffff6,
+ 0xffffff7,
+ 0xffffff8,
+ 0xffffff9,
+ 0xffffffa,
+ 0xffffffb,
+ 0x14,
+ 0x3f8,
+ 0x3f9,
+ 0xffa,
+ 0x1ff9,
+ 0x15,
+ 0xf8,
+ 0x7fa,
+ 0x3fa,
+ 0x3fb,
+ 0xf9,
+ 0x7fb,
+ 0xfa,
+ 0x16,
+ 0x17,
+ 0x18,
+ 0x0,
+ 0x1,
+ 0x2,
+ 0x19,
+ 0x1a,
+ 0x1b,
+ 0x1c,
+ 0x1d,
+ 0x1e,
+ 0x1f,
+ 0x5c,
+ 0xfb,
+ 0x7ffc,
+ 0x20,
+ 0xffb,
+ 0x3fc,
+ 0x1ffa,
+ 0x21,
+ 0x5d,
+ 0x5e,
+ 0x5f,
+ 0x60,
+ 0x61,
+ 0x62,
+ 0x63,
+ 0x64,
+ 0x65,
+ 0x66,
+ 0x67,
+ 0x68,
+ 0x69,
+ 0x6a,
+ 0x6b,
+ 0x6c,
+ 0x6d,
+ 0x6e,
+ 0x6f,
+ 0x70,
+ 0x71,
+ 0x72,
+ 0xfc,
+ 0x73,
+ 0xfd,
+ 0x1ffb,
+ 0x7fff0,
+ 0x1ffc,
+ 0x3ffc,
+ 0x22,
+ 0x7ffd,
+ 0x3,
+ 0x23,
+ 0x4,
+ 0x24,
+ 0x5,
+ 0x25,
+ 0x26,
+ 0x27,
+ 0x6,
+ 0x74,
+ 0x75,
+ 0x28,
+ 0x29,
+ 0x2a,
+ 0x7,
+ 0x2b,
+ 0x76,
+ 0x2c,
+ 0x8,
+ 0x9,
+ 0x2d,
+ 0x77,
+ 0x78,
+ 0x79,
+ 0x7a,
+ 0x7b,
+ 0x7ffe,
+ 0x7fc,
+ 0x3ffd,
+ 0x1ffd,
+ 0xffffffc,
+ 0xfffe6,
+ 0x3fffd2,
+ 0xfffe7,
+ 0xfffe8,
+ 0x3fffd3,
+ 0x3fffd4,
+ 0x3fffd5,
+ 0x7fffd9,
+ 0x3fffd6,
+ 0x7fffda,
+ 0x7fffdb,
+ 0x7fffdc,
+ 0x7fffdd,
+ 0x7fffde,
+ 0xffffeb,
+ 0x7fffdf,
+ 0xffffec,
+ 0xffffed,
+ 0x3fffd7,
+ 0x7fffe0,
+ 0xffffee,
+ 0x7fffe1,
+ 0x7fffe2,
+ 0x7fffe3,
+ 0x7fffe4,
+ 0x1fffdc,
+ 0x3fffd8,
+ 0x7fffe5,
+ 0x3fffd9,
+ 0x7fffe6,
+ 0x7fffe7,
+ 0xffffef,
+ 0x3fffda,
+ 0x1fffdd,
+ 0xfffe9,
+ 0x3fffdb,
+ 0x3fffdc,
+ 0x7fffe8,
+ 0x7fffe9,
+ 0x1fffde,
+ 0x7fffea,
+ 0x3fffdd,
+ 0x3fffde,
+ 0xfffff0,
+ 0x1fffdf,
+ 0x3fffdf,
+ 0x7fffeb,
+ 0x7fffec,
+ 0x1fffe0,
+ 0x1fffe1,
+ 0x3fffe0,
+ 0x1fffe2,
+ 0x7fffed,
+ 0x3fffe1,
+ 0x7fffee,
+ 0x7fffef,
+ 0xfffea,
+ 0x3fffe2,
+ 0x3fffe3,
+ 0x3fffe4,
+ 0x7ffff0,
+ 0x3fffe5,
+ 0x3fffe6,
+ 0x7ffff1,
+ 0x3ffffe0,
+ 0x3ffffe1,
+ 0xfffeb,
+ 0x7fff1,
+ 0x3fffe7,
+ 0x7ffff2,
+ 0x3fffe8,
+ 0x1ffffec,
+ 0x3ffffe2,
+ 0x3ffffe3,
+ 0x3ffffe4,
+ 0x7ffffde,
+ 0x7ffffdf,
+ 0x3ffffe5,
+ 0xfffff1,
+ 0x1ffffed,
+ 0x7fff2,
+ 0x1fffe3,
+ 0x3ffffe6,
+ 0x7ffffe0,
+ 0x7ffffe1,
+ 0x3ffffe7,
+ 0x7ffffe2,
+ 0xfffff2,
+ 0x1fffe4,
+ 0x1fffe5,
+ 0x3ffffe8,
+ 0x3ffffe9,
+ 0xffffffd,
+ 0x7ffffe3,
+ 0x7ffffe4,
+ 0x7ffffe5,
+ 0xfffec,
+ 0xfffff3,
+ 0xfffed,
+ 0x1fffe6,
+ 0x3fffe9,
+ 0x1fffe7,
+ 0x1fffe8,
+ 0x7ffff3,
+ 0x3fffea,
+ 0x3fffeb,
+ 0x1ffffee,
+ 0x1ffffef,
+ 0xfffff4,
+ 0xfffff5,
+ 0x3ffffea,
+ 0x7ffff4,
+ 0x3ffffeb,
+ 0x7ffffe6,
+ 0x3ffffec,
+ 0x3ffffed,
+ 0x7ffffe7,
+ 0x7ffffe8,
+ 0x7ffffe9,
+ 0x7ffffea,
+ 0x7ffffeb,
+ 0xffffffe,
+ 0x7ffffec,
+ 0x7ffffed,
+ 0x7ffffee,
+ 0x7ffffef,
+ 0x7fffff0,
+ 0x3ffffee,
+ 0x3fffffff,
+]
+
+REQUEST_CODES_LENGTH = [
+ 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28,
+ 28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+ 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6,
+ 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10,
+ 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6,
+ 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, 6, 6, 5,
+ 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28,
+ 20, 22, 20, 20, 22, 22, 22, 23, 22, 23, 23, 23, 23, 23, 24, 23,
+ 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, 24,
+ 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23,
+ 21, 21, 22, 21, 23, 22, 23, 23, 20, 22, 22, 22, 23, 22, 22, 23,
+ 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, 26, 24, 25,
+ 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27,
+ 20, 24, 20, 21, 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23,
+ 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, 27, 27, 27, 27, 26,
+ 30,
+]
diff --git a/testing/web-platform/tests/tools/third_party/hpack/hpack/huffman_table.py b/testing/web-platform/tests/tools/third_party/hpack/hpack/huffman_table.py
new file mode 100644
index 0000000000..c199ef5a3f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/hpack/huffman_table.py
@@ -0,0 +1,4739 @@
+# -*- coding: utf-8 -*-
+"""
+hpack/huffman_table
+~~~~~~~~~~~~~~~~~~~
+
+This implementation of a Huffman decoding table for HTTP/2 is essentially a
+Python port of the work originally done for nghttp2's Huffman decoding. For
+this reason, while this file is made available under the MIT license as is the
+rest of this module, this file is undoubtedly a derivative work of the nghttp2
+file ``nghttp2_hd_huffman_data.c``, obtained from
+https://github.com/tatsuhiro-t/nghttp2/ at commit
+d2b55ad1a245e1d1964579fa3fac36ebf3939e72. That work is made available under
+the Apache 2.0 license under the following terms:
+
+ Copyright (c) 2013 Tatsuhiro Tsujikawa
+
+ 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.
+
+The essence of this approach is that it builds a finite state machine out of
+4-bit nibbles of Huffman coded data. The input function passes 4 bits worth of
+data to the state machine each time, which uses those 4 bits of data along with
+the current accumulated state data to process the data given.
+
+For the sake of efficiency, the in-memory representation of the states,
+transitions, and result values of the state machine are represented as a long
+list containing three-tuples. This list is enormously long, and viewing it as
+an in-memory representation is not very clear, but it is laid out here in a way
+that is intended to be *somewhat* more clear.
+
+Essentially, the list is structured as 256 collections of 16 entries (one for
+each nibble) of three-tuples. Each collection is called a "node", and the
+zeroth collection is called the "root node". The state machine tracks one
+value: the "state" byte.
+
+For each nibble passed to the state machine, it first multiplies the "state"
+byte by 16 and adds the numerical value of the nibble. This number is the index
+into the large flat list.
+
+The three-tuple that is found by looking up that index consists of three
+values:
+
+- a new state value, used for subsequent decoding
+- a collection of flags, used to determine whether data is emitted or whether
+ the state machine is complete.
+- the byte value to emit, assuming that emitting a byte is required.
+
+The flags are consulted, if necessary a byte is emitted, and then the next
+nibble is used. This continues until the state machine believes it has
+completely Huffman-decoded the data.
+
+This approach has relatively little indirection, and therefore performs
+relatively well, particularly on implementations like PyPy where the cost of
+loops at the Python-level is not too expensive. The total number of loop
+iterations is 4x the number of bytes passed to the decoder.
+"""
+from .exceptions import HPACKDecodingError
+
+
+# This defines the state machine "class" at the top of the file. The reason we
+# do this is to keep the terrifing monster state table at the *bottom* of the
+# file so you don't have to actually *look* at the damn thing.
+def decode_huffman(huffman_string):
+ """
+ Given a bytestring of Huffman-encoded data for HPACK, returns a bytestring
+ of the decompressed data.
+ """
+ if not huffman_string:
+ return b''
+
+ state = 0
+ flags = 0
+ decoded_bytes = bytearray()
+
+ # Perversely, bytearrays are a lot more convenient across Python 2 and
+ # Python 3 because they behave *the same way* on both platforms. Given that
+ # we really do want numerical bytes when we iterate here, let's use a
+ # bytearray.
+ huffman_string = bytearray(huffman_string)
+
+ # This loop is unrolled somewhat. Because we use a nibble, not a byte, we
+ # need to handle each nibble twice. We unroll that: it makes the loop body
+ # a bit longer, but that's ok.
+ for input_byte in huffman_string:
+ index = (state * 16) + (input_byte >> 4)
+ state, flags, output_byte = HUFFMAN_TABLE[index]
+
+ if flags & HUFFMAN_FAIL:
+ raise HPACKDecodingError("Invalid Huffman String")
+
+ if flags & HUFFMAN_EMIT_SYMBOL:
+ decoded_bytes.append(output_byte)
+
+ index = (state * 16) + (input_byte & 0x0F)
+ state, flags, output_byte = HUFFMAN_TABLE[index]
+
+ if flags & HUFFMAN_FAIL:
+ raise HPACKDecodingError("Invalid Huffman String")
+
+ if flags & HUFFMAN_EMIT_SYMBOL:
+ decoded_bytes.append(output_byte)
+
+ if not (flags & HUFFMAN_COMPLETE):
+ raise HPACKDecodingError("Incomplete Huffman string")
+
+ return bytes(decoded_bytes)
+
+
+# Some decoder flags to control state transitions.
+HUFFMAN_COMPLETE = 1
+HUFFMAN_EMIT_SYMBOL = (1 << 1)
+HUFFMAN_FAIL = (1 << 2)
+
+# This is the monster table. Avert your eyes, children.
+HUFFMAN_TABLE = [
+ # Node 0 (Root Node, never emits symbols.)
+ (4, 0, 0),
+ (5, 0, 0),
+ (7, 0, 0),
+ (8, 0, 0),
+ (11, 0, 0),
+ (12, 0, 0),
+ (16, 0, 0),
+ (19, 0, 0),
+ (25, 0, 0),
+ (28, 0, 0),
+ (32, 0, 0),
+ (35, 0, 0),
+ (42, 0, 0),
+ (49, 0, 0),
+ (57, 0, 0),
+ (64, HUFFMAN_COMPLETE, 0),
+
+ # Node 1
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 48),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 49),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 50),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 97),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 99),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 101),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 105),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 111),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 115),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 116),
+ (13, 0, 0),
+ (14, 0, 0),
+ (17, 0, 0),
+ (18, 0, 0),
+ (20, 0, 0),
+ (21, 0, 0),
+
+ # Node 2
+ (1, HUFFMAN_EMIT_SYMBOL, 48),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 48),
+ (1, HUFFMAN_EMIT_SYMBOL, 49),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 49),
+ (1, HUFFMAN_EMIT_SYMBOL, 50),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 50),
+ (1, HUFFMAN_EMIT_SYMBOL, 97),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 97),
+ (1, HUFFMAN_EMIT_SYMBOL, 99),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 99),
+ (1, HUFFMAN_EMIT_SYMBOL, 101),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 101),
+ (1, HUFFMAN_EMIT_SYMBOL, 105),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 105),
+ (1, HUFFMAN_EMIT_SYMBOL, 111),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 111),
+
+ # Node 3
+ (2, HUFFMAN_EMIT_SYMBOL, 48),
+ (9, HUFFMAN_EMIT_SYMBOL, 48),
+ (23, HUFFMAN_EMIT_SYMBOL, 48),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 48),
+ (2, HUFFMAN_EMIT_SYMBOL, 49),
+ (9, HUFFMAN_EMIT_SYMBOL, 49),
+ (23, HUFFMAN_EMIT_SYMBOL, 49),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 49),
+ (2, HUFFMAN_EMIT_SYMBOL, 50),
+ (9, HUFFMAN_EMIT_SYMBOL, 50),
+ (23, HUFFMAN_EMIT_SYMBOL, 50),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 50),
+ (2, HUFFMAN_EMIT_SYMBOL, 97),
+ (9, HUFFMAN_EMIT_SYMBOL, 97),
+ (23, HUFFMAN_EMIT_SYMBOL, 97),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 97),
+
+ # Node 4
+ (3, HUFFMAN_EMIT_SYMBOL, 48),
+ (6, HUFFMAN_EMIT_SYMBOL, 48),
+ (10, HUFFMAN_EMIT_SYMBOL, 48),
+ (15, HUFFMAN_EMIT_SYMBOL, 48),
+ (24, HUFFMAN_EMIT_SYMBOL, 48),
+ (31, HUFFMAN_EMIT_SYMBOL, 48),
+ (41, HUFFMAN_EMIT_SYMBOL, 48),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 48),
+ (3, HUFFMAN_EMIT_SYMBOL, 49),
+ (6, HUFFMAN_EMIT_SYMBOL, 49),
+ (10, HUFFMAN_EMIT_SYMBOL, 49),
+ (15, HUFFMAN_EMIT_SYMBOL, 49),
+ (24, HUFFMAN_EMIT_SYMBOL, 49),
+ (31, HUFFMAN_EMIT_SYMBOL, 49),
+ (41, HUFFMAN_EMIT_SYMBOL, 49),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 49),
+
+ # Node 5
+ (3, HUFFMAN_EMIT_SYMBOL, 50),
+ (6, HUFFMAN_EMIT_SYMBOL, 50),
+ (10, HUFFMAN_EMIT_SYMBOL, 50),
+ (15, HUFFMAN_EMIT_SYMBOL, 50),
+ (24, HUFFMAN_EMIT_SYMBOL, 50),
+ (31, HUFFMAN_EMIT_SYMBOL, 50),
+ (41, HUFFMAN_EMIT_SYMBOL, 50),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 50),
+ (3, HUFFMAN_EMIT_SYMBOL, 97),
+ (6, HUFFMAN_EMIT_SYMBOL, 97),
+ (10, HUFFMAN_EMIT_SYMBOL, 97),
+ (15, HUFFMAN_EMIT_SYMBOL, 97),
+ (24, HUFFMAN_EMIT_SYMBOL, 97),
+ (31, HUFFMAN_EMIT_SYMBOL, 97),
+ (41, HUFFMAN_EMIT_SYMBOL, 97),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 97),
+
+ # Node 6
+ (2, HUFFMAN_EMIT_SYMBOL, 99),
+ (9, HUFFMAN_EMIT_SYMBOL, 99),
+ (23, HUFFMAN_EMIT_SYMBOL, 99),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 99),
+ (2, HUFFMAN_EMIT_SYMBOL, 101),
+ (9, HUFFMAN_EMIT_SYMBOL, 101),
+ (23, HUFFMAN_EMIT_SYMBOL, 101),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 101),
+ (2, HUFFMAN_EMIT_SYMBOL, 105),
+ (9, HUFFMAN_EMIT_SYMBOL, 105),
+ (23, HUFFMAN_EMIT_SYMBOL, 105),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 105),
+ (2, HUFFMAN_EMIT_SYMBOL, 111),
+ (9, HUFFMAN_EMIT_SYMBOL, 111),
+ (23, HUFFMAN_EMIT_SYMBOL, 111),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 111),
+
+ # Node 7
+ (3, HUFFMAN_EMIT_SYMBOL, 99),
+ (6, HUFFMAN_EMIT_SYMBOL, 99),
+ (10, HUFFMAN_EMIT_SYMBOL, 99),
+ (15, HUFFMAN_EMIT_SYMBOL, 99),
+ (24, HUFFMAN_EMIT_SYMBOL, 99),
+ (31, HUFFMAN_EMIT_SYMBOL, 99),
+ (41, HUFFMAN_EMIT_SYMBOL, 99),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 99),
+ (3, HUFFMAN_EMIT_SYMBOL, 101),
+ (6, HUFFMAN_EMIT_SYMBOL, 101),
+ (10, HUFFMAN_EMIT_SYMBOL, 101),
+ (15, HUFFMAN_EMIT_SYMBOL, 101),
+ (24, HUFFMAN_EMIT_SYMBOL, 101),
+ (31, HUFFMAN_EMIT_SYMBOL, 101),
+ (41, HUFFMAN_EMIT_SYMBOL, 101),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 101),
+
+ # Node 8
+ (3, HUFFMAN_EMIT_SYMBOL, 105),
+ (6, HUFFMAN_EMIT_SYMBOL, 105),
+ (10, HUFFMAN_EMIT_SYMBOL, 105),
+ (15, HUFFMAN_EMIT_SYMBOL, 105),
+ (24, HUFFMAN_EMIT_SYMBOL, 105),
+ (31, HUFFMAN_EMIT_SYMBOL, 105),
+ (41, HUFFMAN_EMIT_SYMBOL, 105),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 105),
+ (3, HUFFMAN_EMIT_SYMBOL, 111),
+ (6, HUFFMAN_EMIT_SYMBOL, 111),
+ (10, HUFFMAN_EMIT_SYMBOL, 111),
+ (15, HUFFMAN_EMIT_SYMBOL, 111),
+ (24, HUFFMAN_EMIT_SYMBOL, 111),
+ (31, HUFFMAN_EMIT_SYMBOL, 111),
+ (41, HUFFMAN_EMIT_SYMBOL, 111),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 111),
+
+ # Node 9
+ (1, HUFFMAN_EMIT_SYMBOL, 115),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 115),
+ (1, HUFFMAN_EMIT_SYMBOL, 116),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 116),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 32),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 37),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 45),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 46),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 47),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 51),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 52),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 53),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 54),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 55),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 56),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 57),
+
+ # Node 10
+ (2, HUFFMAN_EMIT_SYMBOL, 115),
+ (9, HUFFMAN_EMIT_SYMBOL, 115),
+ (23, HUFFMAN_EMIT_SYMBOL, 115),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 115),
+ (2, HUFFMAN_EMIT_SYMBOL, 116),
+ (9, HUFFMAN_EMIT_SYMBOL, 116),
+ (23, HUFFMAN_EMIT_SYMBOL, 116),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 116),
+ (1, HUFFMAN_EMIT_SYMBOL, 32),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 32),
+ (1, HUFFMAN_EMIT_SYMBOL, 37),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 37),
+ (1, HUFFMAN_EMIT_SYMBOL, 45),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 45),
+ (1, HUFFMAN_EMIT_SYMBOL, 46),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 46),
+
+ # Node 11
+ (3, HUFFMAN_EMIT_SYMBOL, 115),
+ (6, HUFFMAN_EMIT_SYMBOL, 115),
+ (10, HUFFMAN_EMIT_SYMBOL, 115),
+ (15, HUFFMAN_EMIT_SYMBOL, 115),
+ (24, HUFFMAN_EMIT_SYMBOL, 115),
+ (31, HUFFMAN_EMIT_SYMBOL, 115),
+ (41, HUFFMAN_EMIT_SYMBOL, 115),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 115),
+ (3, HUFFMAN_EMIT_SYMBOL, 116),
+ (6, HUFFMAN_EMIT_SYMBOL, 116),
+ (10, HUFFMAN_EMIT_SYMBOL, 116),
+ (15, HUFFMAN_EMIT_SYMBOL, 116),
+ (24, HUFFMAN_EMIT_SYMBOL, 116),
+ (31, HUFFMAN_EMIT_SYMBOL, 116),
+ (41, HUFFMAN_EMIT_SYMBOL, 116),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 116),
+
+ # Node 12
+ (2, HUFFMAN_EMIT_SYMBOL, 32),
+ (9, HUFFMAN_EMIT_SYMBOL, 32),
+ (23, HUFFMAN_EMIT_SYMBOL, 32),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 32),
+ (2, HUFFMAN_EMIT_SYMBOL, 37),
+ (9, HUFFMAN_EMIT_SYMBOL, 37),
+ (23, HUFFMAN_EMIT_SYMBOL, 37),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 37),
+ (2, HUFFMAN_EMIT_SYMBOL, 45),
+ (9, HUFFMAN_EMIT_SYMBOL, 45),
+ (23, HUFFMAN_EMIT_SYMBOL, 45),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 45),
+ (2, HUFFMAN_EMIT_SYMBOL, 46),
+ (9, HUFFMAN_EMIT_SYMBOL, 46),
+ (23, HUFFMAN_EMIT_SYMBOL, 46),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 46),
+
+ # Node 13
+ (3, HUFFMAN_EMIT_SYMBOL, 32),
+ (6, HUFFMAN_EMIT_SYMBOL, 32),
+ (10, HUFFMAN_EMIT_SYMBOL, 32),
+ (15, HUFFMAN_EMIT_SYMBOL, 32),
+ (24, HUFFMAN_EMIT_SYMBOL, 32),
+ (31, HUFFMAN_EMIT_SYMBOL, 32),
+ (41, HUFFMAN_EMIT_SYMBOL, 32),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 32),
+ (3, HUFFMAN_EMIT_SYMBOL, 37),
+ (6, HUFFMAN_EMIT_SYMBOL, 37),
+ (10, HUFFMAN_EMIT_SYMBOL, 37),
+ (15, HUFFMAN_EMIT_SYMBOL, 37),
+ (24, HUFFMAN_EMIT_SYMBOL, 37),
+ (31, HUFFMAN_EMIT_SYMBOL, 37),
+ (41, HUFFMAN_EMIT_SYMBOL, 37),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 37),
+
+ # Node 14
+ (3, HUFFMAN_EMIT_SYMBOL, 45),
+ (6, HUFFMAN_EMIT_SYMBOL, 45),
+ (10, HUFFMAN_EMIT_SYMBOL, 45),
+ (15, HUFFMAN_EMIT_SYMBOL, 45),
+ (24, HUFFMAN_EMIT_SYMBOL, 45),
+ (31, HUFFMAN_EMIT_SYMBOL, 45),
+ (41, HUFFMAN_EMIT_SYMBOL, 45),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 45),
+ (3, HUFFMAN_EMIT_SYMBOL, 46),
+ (6, HUFFMAN_EMIT_SYMBOL, 46),
+ (10, HUFFMAN_EMIT_SYMBOL, 46),
+ (15, HUFFMAN_EMIT_SYMBOL, 46),
+ (24, HUFFMAN_EMIT_SYMBOL, 46),
+ (31, HUFFMAN_EMIT_SYMBOL, 46),
+ (41, HUFFMAN_EMIT_SYMBOL, 46),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 46),
+
+ # Node 15
+ (1, HUFFMAN_EMIT_SYMBOL, 47),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 47),
+ (1, HUFFMAN_EMIT_SYMBOL, 51),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 51),
+ (1, HUFFMAN_EMIT_SYMBOL, 52),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 52),
+ (1, HUFFMAN_EMIT_SYMBOL, 53),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 53),
+ (1, HUFFMAN_EMIT_SYMBOL, 54),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 54),
+ (1, HUFFMAN_EMIT_SYMBOL, 55),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 55),
+ (1, HUFFMAN_EMIT_SYMBOL, 56),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 56),
+ (1, HUFFMAN_EMIT_SYMBOL, 57),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 57),
+
+ # Node 16
+ (2, HUFFMAN_EMIT_SYMBOL, 47),
+ (9, HUFFMAN_EMIT_SYMBOL, 47),
+ (23, HUFFMAN_EMIT_SYMBOL, 47),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 47),
+ (2, HUFFMAN_EMIT_SYMBOL, 51),
+ (9, HUFFMAN_EMIT_SYMBOL, 51),
+ (23, HUFFMAN_EMIT_SYMBOL, 51),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 51),
+ (2, HUFFMAN_EMIT_SYMBOL, 52),
+ (9, HUFFMAN_EMIT_SYMBOL, 52),
+ (23, HUFFMAN_EMIT_SYMBOL, 52),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 52),
+ (2, HUFFMAN_EMIT_SYMBOL, 53),
+ (9, HUFFMAN_EMIT_SYMBOL, 53),
+ (23, HUFFMAN_EMIT_SYMBOL, 53),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 53),
+
+ # Node 17
+ (3, HUFFMAN_EMIT_SYMBOL, 47),
+ (6, HUFFMAN_EMIT_SYMBOL, 47),
+ (10, HUFFMAN_EMIT_SYMBOL, 47),
+ (15, HUFFMAN_EMIT_SYMBOL, 47),
+ (24, HUFFMAN_EMIT_SYMBOL, 47),
+ (31, HUFFMAN_EMIT_SYMBOL, 47),
+ (41, HUFFMAN_EMIT_SYMBOL, 47),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 47),
+ (3, HUFFMAN_EMIT_SYMBOL, 51),
+ (6, HUFFMAN_EMIT_SYMBOL, 51),
+ (10, HUFFMAN_EMIT_SYMBOL, 51),
+ (15, HUFFMAN_EMIT_SYMBOL, 51),
+ (24, HUFFMAN_EMIT_SYMBOL, 51),
+ (31, HUFFMAN_EMIT_SYMBOL, 51),
+ (41, HUFFMAN_EMIT_SYMBOL, 51),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 51),
+
+ # Node 18
+ (3, HUFFMAN_EMIT_SYMBOL, 52),
+ (6, HUFFMAN_EMIT_SYMBOL, 52),
+ (10, HUFFMAN_EMIT_SYMBOL, 52),
+ (15, HUFFMAN_EMIT_SYMBOL, 52),
+ (24, HUFFMAN_EMIT_SYMBOL, 52),
+ (31, HUFFMAN_EMIT_SYMBOL, 52),
+ (41, HUFFMAN_EMIT_SYMBOL, 52),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 52),
+ (3, HUFFMAN_EMIT_SYMBOL, 53),
+ (6, HUFFMAN_EMIT_SYMBOL, 53),
+ (10, HUFFMAN_EMIT_SYMBOL, 53),
+ (15, HUFFMAN_EMIT_SYMBOL, 53),
+ (24, HUFFMAN_EMIT_SYMBOL, 53),
+ (31, HUFFMAN_EMIT_SYMBOL, 53),
+ (41, HUFFMAN_EMIT_SYMBOL, 53),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 53),
+
+ # Node 19
+ (2, HUFFMAN_EMIT_SYMBOL, 54),
+ (9, HUFFMAN_EMIT_SYMBOL, 54),
+ (23, HUFFMAN_EMIT_SYMBOL, 54),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 54),
+ (2, HUFFMAN_EMIT_SYMBOL, 55),
+ (9, HUFFMAN_EMIT_SYMBOL, 55),
+ (23, HUFFMAN_EMIT_SYMBOL, 55),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 55),
+ (2, HUFFMAN_EMIT_SYMBOL, 56),
+ (9, HUFFMAN_EMIT_SYMBOL, 56),
+ (23, HUFFMAN_EMIT_SYMBOL, 56),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 56),
+ (2, HUFFMAN_EMIT_SYMBOL, 57),
+ (9, HUFFMAN_EMIT_SYMBOL, 57),
+ (23, HUFFMAN_EMIT_SYMBOL, 57),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 57),
+
+ # Node 20
+ (3, HUFFMAN_EMIT_SYMBOL, 54),
+ (6, HUFFMAN_EMIT_SYMBOL, 54),
+ (10, HUFFMAN_EMIT_SYMBOL, 54),
+ (15, HUFFMAN_EMIT_SYMBOL, 54),
+ (24, HUFFMAN_EMIT_SYMBOL, 54),
+ (31, HUFFMAN_EMIT_SYMBOL, 54),
+ (41, HUFFMAN_EMIT_SYMBOL, 54),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 54),
+ (3, HUFFMAN_EMIT_SYMBOL, 55),
+ (6, HUFFMAN_EMIT_SYMBOL, 55),
+ (10, HUFFMAN_EMIT_SYMBOL, 55),
+ (15, HUFFMAN_EMIT_SYMBOL, 55),
+ (24, HUFFMAN_EMIT_SYMBOL, 55),
+ (31, HUFFMAN_EMIT_SYMBOL, 55),
+ (41, HUFFMAN_EMIT_SYMBOL, 55),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 55),
+
+ # Node 21
+ (3, HUFFMAN_EMIT_SYMBOL, 56),
+ (6, HUFFMAN_EMIT_SYMBOL, 56),
+ (10, HUFFMAN_EMIT_SYMBOL, 56),
+ (15, HUFFMAN_EMIT_SYMBOL, 56),
+ (24, HUFFMAN_EMIT_SYMBOL, 56),
+ (31, HUFFMAN_EMIT_SYMBOL, 56),
+ (41, HUFFMAN_EMIT_SYMBOL, 56),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 56),
+ (3, HUFFMAN_EMIT_SYMBOL, 57),
+ (6, HUFFMAN_EMIT_SYMBOL, 57),
+ (10, HUFFMAN_EMIT_SYMBOL, 57),
+ (15, HUFFMAN_EMIT_SYMBOL, 57),
+ (24, HUFFMAN_EMIT_SYMBOL, 57),
+ (31, HUFFMAN_EMIT_SYMBOL, 57),
+ (41, HUFFMAN_EMIT_SYMBOL, 57),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 57),
+
+ # Node 22
+ (26, 0, 0),
+ (27, 0, 0),
+ (29, 0, 0),
+ (30, 0, 0),
+ (33, 0, 0),
+ (34, 0, 0),
+ (36, 0, 0),
+ (37, 0, 0),
+ (43, 0, 0),
+ (46, 0, 0),
+ (50, 0, 0),
+ (53, 0, 0),
+ (58, 0, 0),
+ (61, 0, 0),
+ (65, 0, 0),
+ (68, HUFFMAN_COMPLETE, 0),
+
+ # Node 23
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 61),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 65),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 95),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 98),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 100),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 102),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 103),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 104),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 108),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 109),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 110),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 112),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 114),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 117),
+ (38, 0, 0),
+ (39, 0, 0),
+
+ # Node 24
+ (1, HUFFMAN_EMIT_SYMBOL, 61),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 61),
+ (1, HUFFMAN_EMIT_SYMBOL, 65),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 65),
+ (1, HUFFMAN_EMIT_SYMBOL, 95),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 95),
+ (1, HUFFMAN_EMIT_SYMBOL, 98),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 98),
+ (1, HUFFMAN_EMIT_SYMBOL, 100),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 100),
+ (1, HUFFMAN_EMIT_SYMBOL, 102),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 102),
+ (1, HUFFMAN_EMIT_SYMBOL, 103),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 103),
+ (1, HUFFMAN_EMIT_SYMBOL, 104),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 104),
+
+ # Node 25
+ (2, HUFFMAN_EMIT_SYMBOL, 61),
+ (9, HUFFMAN_EMIT_SYMBOL, 61),
+ (23, HUFFMAN_EMIT_SYMBOL, 61),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 61),
+ (2, HUFFMAN_EMIT_SYMBOL, 65),
+ (9, HUFFMAN_EMIT_SYMBOL, 65),
+ (23, HUFFMAN_EMIT_SYMBOL, 65),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 65),
+ (2, HUFFMAN_EMIT_SYMBOL, 95),
+ (9, HUFFMAN_EMIT_SYMBOL, 95),
+ (23, HUFFMAN_EMIT_SYMBOL, 95),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 95),
+ (2, HUFFMAN_EMIT_SYMBOL, 98),
+ (9, HUFFMAN_EMIT_SYMBOL, 98),
+ (23, HUFFMAN_EMIT_SYMBOL, 98),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 98),
+
+ # Node 26
+ (3, HUFFMAN_EMIT_SYMBOL, 61),
+ (6, HUFFMAN_EMIT_SYMBOL, 61),
+ (10, HUFFMAN_EMIT_SYMBOL, 61),
+ (15, HUFFMAN_EMIT_SYMBOL, 61),
+ (24, HUFFMAN_EMIT_SYMBOL, 61),
+ (31, HUFFMAN_EMIT_SYMBOL, 61),
+ (41, HUFFMAN_EMIT_SYMBOL, 61),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 61),
+ (3, HUFFMAN_EMIT_SYMBOL, 65),
+ (6, HUFFMAN_EMIT_SYMBOL, 65),
+ (10, HUFFMAN_EMIT_SYMBOL, 65),
+ (15, HUFFMAN_EMIT_SYMBOL, 65),
+ (24, HUFFMAN_EMIT_SYMBOL, 65),
+ (31, HUFFMAN_EMIT_SYMBOL, 65),
+ (41, HUFFMAN_EMIT_SYMBOL, 65),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 65),
+
+ # Node 27
+ (3, HUFFMAN_EMIT_SYMBOL, 95),
+ (6, HUFFMAN_EMIT_SYMBOL, 95),
+ (10, HUFFMAN_EMIT_SYMBOL, 95),
+ (15, HUFFMAN_EMIT_SYMBOL, 95),
+ (24, HUFFMAN_EMIT_SYMBOL, 95),
+ (31, HUFFMAN_EMIT_SYMBOL, 95),
+ (41, HUFFMAN_EMIT_SYMBOL, 95),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 95),
+ (3, HUFFMAN_EMIT_SYMBOL, 98),
+ (6, HUFFMAN_EMIT_SYMBOL, 98),
+ (10, HUFFMAN_EMIT_SYMBOL, 98),
+ (15, HUFFMAN_EMIT_SYMBOL, 98),
+ (24, HUFFMAN_EMIT_SYMBOL, 98),
+ (31, HUFFMAN_EMIT_SYMBOL, 98),
+ (41, HUFFMAN_EMIT_SYMBOL, 98),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 98),
+
+ # Node 28
+ (2, HUFFMAN_EMIT_SYMBOL, 100),
+ (9, HUFFMAN_EMIT_SYMBOL, 100),
+ (23, HUFFMAN_EMIT_SYMBOL, 100),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 100),
+ (2, HUFFMAN_EMIT_SYMBOL, 102),
+ (9, HUFFMAN_EMIT_SYMBOL, 102),
+ (23, HUFFMAN_EMIT_SYMBOL, 102),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 102),
+ (2, HUFFMAN_EMIT_SYMBOL, 103),
+ (9, HUFFMAN_EMIT_SYMBOL, 103),
+ (23, HUFFMAN_EMIT_SYMBOL, 103),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 103),
+ (2, HUFFMAN_EMIT_SYMBOL, 104),
+ (9, HUFFMAN_EMIT_SYMBOL, 104),
+ (23, HUFFMAN_EMIT_SYMBOL, 104),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 104),
+
+ # Node 29
+ (3, HUFFMAN_EMIT_SYMBOL, 100),
+ (6, HUFFMAN_EMIT_SYMBOL, 100),
+ (10, HUFFMAN_EMIT_SYMBOL, 100),
+ (15, HUFFMAN_EMIT_SYMBOL, 100),
+ (24, HUFFMAN_EMIT_SYMBOL, 100),
+ (31, HUFFMAN_EMIT_SYMBOL, 100),
+ (41, HUFFMAN_EMIT_SYMBOL, 100),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 100),
+ (3, HUFFMAN_EMIT_SYMBOL, 102),
+ (6, HUFFMAN_EMIT_SYMBOL, 102),
+ (10, HUFFMAN_EMIT_SYMBOL, 102),
+ (15, HUFFMAN_EMIT_SYMBOL, 102),
+ (24, HUFFMAN_EMIT_SYMBOL, 102),
+ (31, HUFFMAN_EMIT_SYMBOL, 102),
+ (41, HUFFMAN_EMIT_SYMBOL, 102),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 102),
+
+ # Node 30
+ (3, HUFFMAN_EMIT_SYMBOL, 103),
+ (6, HUFFMAN_EMIT_SYMBOL, 103),
+ (10, HUFFMAN_EMIT_SYMBOL, 103),
+ (15, HUFFMAN_EMIT_SYMBOL, 103),
+ (24, HUFFMAN_EMIT_SYMBOL, 103),
+ (31, HUFFMAN_EMIT_SYMBOL, 103),
+ (41, HUFFMAN_EMIT_SYMBOL, 103),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 103),
+ (3, HUFFMAN_EMIT_SYMBOL, 104),
+ (6, HUFFMAN_EMIT_SYMBOL, 104),
+ (10, HUFFMAN_EMIT_SYMBOL, 104),
+ (15, HUFFMAN_EMIT_SYMBOL, 104),
+ (24, HUFFMAN_EMIT_SYMBOL, 104),
+ (31, HUFFMAN_EMIT_SYMBOL, 104),
+ (41, HUFFMAN_EMIT_SYMBOL, 104),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 104),
+
+ # Node 31
+ (1, HUFFMAN_EMIT_SYMBOL, 108),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 108),
+ (1, HUFFMAN_EMIT_SYMBOL, 109),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 109),
+ (1, HUFFMAN_EMIT_SYMBOL, 110),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 110),
+ (1, HUFFMAN_EMIT_SYMBOL, 112),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 112),
+ (1, HUFFMAN_EMIT_SYMBOL, 114),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 114),
+ (1, HUFFMAN_EMIT_SYMBOL, 117),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 117),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 58),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 66),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 67),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 68),
+
+ # Node 32
+ (2, HUFFMAN_EMIT_SYMBOL, 108),
+ (9, HUFFMAN_EMIT_SYMBOL, 108),
+ (23, HUFFMAN_EMIT_SYMBOL, 108),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 108),
+ (2, HUFFMAN_EMIT_SYMBOL, 109),
+ (9, HUFFMAN_EMIT_SYMBOL, 109),
+ (23, HUFFMAN_EMIT_SYMBOL, 109),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 109),
+ (2, HUFFMAN_EMIT_SYMBOL, 110),
+ (9, HUFFMAN_EMIT_SYMBOL, 110),
+ (23, HUFFMAN_EMIT_SYMBOL, 110),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 110),
+ (2, HUFFMAN_EMIT_SYMBOL, 112),
+ (9, HUFFMAN_EMIT_SYMBOL, 112),
+ (23, HUFFMAN_EMIT_SYMBOL, 112),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 112),
+
+ # Node 33
+ (3, HUFFMAN_EMIT_SYMBOL, 108),
+ (6, HUFFMAN_EMIT_SYMBOL, 108),
+ (10, HUFFMAN_EMIT_SYMBOL, 108),
+ (15, HUFFMAN_EMIT_SYMBOL, 108),
+ (24, HUFFMAN_EMIT_SYMBOL, 108),
+ (31, HUFFMAN_EMIT_SYMBOL, 108),
+ (41, HUFFMAN_EMIT_SYMBOL, 108),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 108),
+ (3, HUFFMAN_EMIT_SYMBOL, 109),
+ (6, HUFFMAN_EMIT_SYMBOL, 109),
+ (10, HUFFMAN_EMIT_SYMBOL, 109),
+ (15, HUFFMAN_EMIT_SYMBOL, 109),
+ (24, HUFFMAN_EMIT_SYMBOL, 109),
+ (31, HUFFMAN_EMIT_SYMBOL, 109),
+ (41, HUFFMAN_EMIT_SYMBOL, 109),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 109),
+
+ # Node 34
+ (3, HUFFMAN_EMIT_SYMBOL, 110),
+ (6, HUFFMAN_EMIT_SYMBOL, 110),
+ (10, HUFFMAN_EMIT_SYMBOL, 110),
+ (15, HUFFMAN_EMIT_SYMBOL, 110),
+ (24, HUFFMAN_EMIT_SYMBOL, 110),
+ (31, HUFFMAN_EMIT_SYMBOL, 110),
+ (41, HUFFMAN_EMIT_SYMBOL, 110),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 110),
+ (3, HUFFMAN_EMIT_SYMBOL, 112),
+ (6, HUFFMAN_EMIT_SYMBOL, 112),
+ (10, HUFFMAN_EMIT_SYMBOL, 112),
+ (15, HUFFMAN_EMIT_SYMBOL, 112),
+ (24, HUFFMAN_EMIT_SYMBOL, 112),
+ (31, HUFFMAN_EMIT_SYMBOL, 112),
+ (41, HUFFMAN_EMIT_SYMBOL, 112),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 112),
+
+ # Node 35
+ (2, HUFFMAN_EMIT_SYMBOL, 114),
+ (9, HUFFMAN_EMIT_SYMBOL, 114),
+ (23, HUFFMAN_EMIT_SYMBOL, 114),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 114),
+ (2, HUFFMAN_EMIT_SYMBOL, 117),
+ (9, HUFFMAN_EMIT_SYMBOL, 117),
+ (23, HUFFMAN_EMIT_SYMBOL, 117),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 117),
+ (1, HUFFMAN_EMIT_SYMBOL, 58),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 58),
+ (1, HUFFMAN_EMIT_SYMBOL, 66),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 66),
+ (1, HUFFMAN_EMIT_SYMBOL, 67),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 67),
+ (1, HUFFMAN_EMIT_SYMBOL, 68),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 68),
+
+ # Node 36
+ (3, HUFFMAN_EMIT_SYMBOL, 114),
+ (6, HUFFMAN_EMIT_SYMBOL, 114),
+ (10, HUFFMAN_EMIT_SYMBOL, 114),
+ (15, HUFFMAN_EMIT_SYMBOL, 114),
+ (24, HUFFMAN_EMIT_SYMBOL, 114),
+ (31, HUFFMAN_EMIT_SYMBOL, 114),
+ (41, HUFFMAN_EMIT_SYMBOL, 114),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 114),
+ (3, HUFFMAN_EMIT_SYMBOL, 117),
+ (6, HUFFMAN_EMIT_SYMBOL, 117),
+ (10, HUFFMAN_EMIT_SYMBOL, 117),
+ (15, HUFFMAN_EMIT_SYMBOL, 117),
+ (24, HUFFMAN_EMIT_SYMBOL, 117),
+ (31, HUFFMAN_EMIT_SYMBOL, 117),
+ (41, HUFFMAN_EMIT_SYMBOL, 117),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 117),
+
+ # Node 37
+ (2, HUFFMAN_EMIT_SYMBOL, 58),
+ (9, HUFFMAN_EMIT_SYMBOL, 58),
+ (23, HUFFMAN_EMIT_SYMBOL, 58),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 58),
+ (2, HUFFMAN_EMIT_SYMBOL, 66),
+ (9, HUFFMAN_EMIT_SYMBOL, 66),
+ (23, HUFFMAN_EMIT_SYMBOL, 66),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 66),
+ (2, HUFFMAN_EMIT_SYMBOL, 67),
+ (9, HUFFMAN_EMIT_SYMBOL, 67),
+ (23, HUFFMAN_EMIT_SYMBOL, 67),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 67),
+ (2, HUFFMAN_EMIT_SYMBOL, 68),
+ (9, HUFFMAN_EMIT_SYMBOL, 68),
+ (23, HUFFMAN_EMIT_SYMBOL, 68),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 68),
+
+ # Node 38
+ (3, HUFFMAN_EMIT_SYMBOL, 58),
+ (6, HUFFMAN_EMIT_SYMBOL, 58),
+ (10, HUFFMAN_EMIT_SYMBOL, 58),
+ (15, HUFFMAN_EMIT_SYMBOL, 58),
+ (24, HUFFMAN_EMIT_SYMBOL, 58),
+ (31, HUFFMAN_EMIT_SYMBOL, 58),
+ (41, HUFFMAN_EMIT_SYMBOL, 58),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 58),
+ (3, HUFFMAN_EMIT_SYMBOL, 66),
+ (6, HUFFMAN_EMIT_SYMBOL, 66),
+ (10, HUFFMAN_EMIT_SYMBOL, 66),
+ (15, HUFFMAN_EMIT_SYMBOL, 66),
+ (24, HUFFMAN_EMIT_SYMBOL, 66),
+ (31, HUFFMAN_EMIT_SYMBOL, 66),
+ (41, HUFFMAN_EMIT_SYMBOL, 66),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 66),
+
+ # Node 39
+ (3, HUFFMAN_EMIT_SYMBOL, 67),
+ (6, HUFFMAN_EMIT_SYMBOL, 67),
+ (10, HUFFMAN_EMIT_SYMBOL, 67),
+ (15, HUFFMAN_EMIT_SYMBOL, 67),
+ (24, HUFFMAN_EMIT_SYMBOL, 67),
+ (31, HUFFMAN_EMIT_SYMBOL, 67),
+ (41, HUFFMAN_EMIT_SYMBOL, 67),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 67),
+ (3, HUFFMAN_EMIT_SYMBOL, 68),
+ (6, HUFFMAN_EMIT_SYMBOL, 68),
+ (10, HUFFMAN_EMIT_SYMBOL, 68),
+ (15, HUFFMAN_EMIT_SYMBOL, 68),
+ (24, HUFFMAN_EMIT_SYMBOL, 68),
+ (31, HUFFMAN_EMIT_SYMBOL, 68),
+ (41, HUFFMAN_EMIT_SYMBOL, 68),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 68),
+
+ # Node 40
+ (44, 0, 0),
+ (45, 0, 0),
+ (47, 0, 0),
+ (48, 0, 0),
+ (51, 0, 0),
+ (52, 0, 0),
+ (54, 0, 0),
+ (55, 0, 0),
+ (59, 0, 0),
+ (60, 0, 0),
+ (62, 0, 0),
+ (63, 0, 0),
+ (66, 0, 0),
+ (67, 0, 0),
+ (69, 0, 0),
+ (72, HUFFMAN_COMPLETE, 0),
+
+ # Node 41
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 69),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 70),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 71),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 72),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 73),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 74),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 75),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 76),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 77),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 78),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 79),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 80),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 81),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 82),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 83),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 84),
+
+ # Node 42
+ (1, HUFFMAN_EMIT_SYMBOL, 69),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 69),
+ (1, HUFFMAN_EMIT_SYMBOL, 70),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 70),
+ (1, HUFFMAN_EMIT_SYMBOL, 71),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 71),
+ (1, HUFFMAN_EMIT_SYMBOL, 72),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 72),
+ (1, HUFFMAN_EMIT_SYMBOL, 73),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 73),
+ (1, HUFFMAN_EMIT_SYMBOL, 74),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 74),
+ (1, HUFFMAN_EMIT_SYMBOL, 75),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 75),
+ (1, HUFFMAN_EMIT_SYMBOL, 76),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 76),
+
+ # Node 43
+ (2, HUFFMAN_EMIT_SYMBOL, 69),
+ (9, HUFFMAN_EMIT_SYMBOL, 69),
+ (23, HUFFMAN_EMIT_SYMBOL, 69),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 69),
+ (2, HUFFMAN_EMIT_SYMBOL, 70),
+ (9, HUFFMAN_EMIT_SYMBOL, 70),
+ (23, HUFFMAN_EMIT_SYMBOL, 70),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 70),
+ (2, HUFFMAN_EMIT_SYMBOL, 71),
+ (9, HUFFMAN_EMIT_SYMBOL, 71),
+ (23, HUFFMAN_EMIT_SYMBOL, 71),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 71),
+ (2, HUFFMAN_EMIT_SYMBOL, 72),
+ (9, HUFFMAN_EMIT_SYMBOL, 72),
+ (23, HUFFMAN_EMIT_SYMBOL, 72),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 72),
+
+ # Node 44
+ (3, HUFFMAN_EMIT_SYMBOL, 69),
+ (6, HUFFMAN_EMIT_SYMBOL, 69),
+ (10, HUFFMAN_EMIT_SYMBOL, 69),
+ (15, HUFFMAN_EMIT_SYMBOL, 69),
+ (24, HUFFMAN_EMIT_SYMBOL, 69),
+ (31, HUFFMAN_EMIT_SYMBOL, 69),
+ (41, HUFFMAN_EMIT_SYMBOL, 69),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 69),
+ (3, HUFFMAN_EMIT_SYMBOL, 70),
+ (6, HUFFMAN_EMIT_SYMBOL, 70),
+ (10, HUFFMAN_EMIT_SYMBOL, 70),
+ (15, HUFFMAN_EMIT_SYMBOL, 70),
+ (24, HUFFMAN_EMIT_SYMBOL, 70),
+ (31, HUFFMAN_EMIT_SYMBOL, 70),
+ (41, HUFFMAN_EMIT_SYMBOL, 70),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 70),
+
+ # Node 45
+ (3, HUFFMAN_EMIT_SYMBOL, 71),
+ (6, HUFFMAN_EMIT_SYMBOL, 71),
+ (10, HUFFMAN_EMIT_SYMBOL, 71),
+ (15, HUFFMAN_EMIT_SYMBOL, 71),
+ (24, HUFFMAN_EMIT_SYMBOL, 71),
+ (31, HUFFMAN_EMIT_SYMBOL, 71),
+ (41, HUFFMAN_EMIT_SYMBOL, 71),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 71),
+ (3, HUFFMAN_EMIT_SYMBOL, 72),
+ (6, HUFFMAN_EMIT_SYMBOL, 72),
+ (10, HUFFMAN_EMIT_SYMBOL, 72),
+ (15, HUFFMAN_EMIT_SYMBOL, 72),
+ (24, HUFFMAN_EMIT_SYMBOL, 72),
+ (31, HUFFMAN_EMIT_SYMBOL, 72),
+ (41, HUFFMAN_EMIT_SYMBOL, 72),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 72),
+
+ # Node 46
+ (2, HUFFMAN_EMIT_SYMBOL, 73),
+ (9, HUFFMAN_EMIT_SYMBOL, 73),
+ (23, HUFFMAN_EMIT_SYMBOL, 73),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 73),
+ (2, HUFFMAN_EMIT_SYMBOL, 74),
+ (9, HUFFMAN_EMIT_SYMBOL, 74),
+ (23, HUFFMAN_EMIT_SYMBOL, 74),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 74),
+ (2, HUFFMAN_EMIT_SYMBOL, 75),
+ (9, HUFFMAN_EMIT_SYMBOL, 75),
+ (23, HUFFMAN_EMIT_SYMBOL, 75),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 75),
+ (2, HUFFMAN_EMIT_SYMBOL, 76),
+ (9, HUFFMAN_EMIT_SYMBOL, 76),
+ (23, HUFFMAN_EMIT_SYMBOL, 76),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 76),
+
+ # Node 47
+ (3, HUFFMAN_EMIT_SYMBOL, 73),
+ (6, HUFFMAN_EMIT_SYMBOL, 73),
+ (10, HUFFMAN_EMIT_SYMBOL, 73),
+ (15, HUFFMAN_EMIT_SYMBOL, 73),
+ (24, HUFFMAN_EMIT_SYMBOL, 73),
+ (31, HUFFMAN_EMIT_SYMBOL, 73),
+ (41, HUFFMAN_EMIT_SYMBOL, 73),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 73),
+ (3, HUFFMAN_EMIT_SYMBOL, 74),
+ (6, HUFFMAN_EMIT_SYMBOL, 74),
+ (10, HUFFMAN_EMIT_SYMBOL, 74),
+ (15, HUFFMAN_EMIT_SYMBOL, 74),
+ (24, HUFFMAN_EMIT_SYMBOL, 74),
+ (31, HUFFMAN_EMIT_SYMBOL, 74),
+ (41, HUFFMAN_EMIT_SYMBOL, 74),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 74),
+
+ # Node 48
+ (3, HUFFMAN_EMIT_SYMBOL, 75),
+ (6, HUFFMAN_EMIT_SYMBOL, 75),
+ (10, HUFFMAN_EMIT_SYMBOL, 75),
+ (15, HUFFMAN_EMIT_SYMBOL, 75),
+ (24, HUFFMAN_EMIT_SYMBOL, 75),
+ (31, HUFFMAN_EMIT_SYMBOL, 75),
+ (41, HUFFMAN_EMIT_SYMBOL, 75),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 75),
+ (3, HUFFMAN_EMIT_SYMBOL, 76),
+ (6, HUFFMAN_EMIT_SYMBOL, 76),
+ (10, HUFFMAN_EMIT_SYMBOL, 76),
+ (15, HUFFMAN_EMIT_SYMBOL, 76),
+ (24, HUFFMAN_EMIT_SYMBOL, 76),
+ (31, HUFFMAN_EMIT_SYMBOL, 76),
+ (41, HUFFMAN_EMIT_SYMBOL, 76),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 76),
+
+ # Node 49
+ (1, HUFFMAN_EMIT_SYMBOL, 77),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 77),
+ (1, HUFFMAN_EMIT_SYMBOL, 78),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 78),
+ (1, HUFFMAN_EMIT_SYMBOL, 79),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 79),
+ (1, HUFFMAN_EMIT_SYMBOL, 80),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 80),
+ (1, HUFFMAN_EMIT_SYMBOL, 81),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 81),
+ (1, HUFFMAN_EMIT_SYMBOL, 82),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 82),
+ (1, HUFFMAN_EMIT_SYMBOL, 83),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 83),
+ (1, HUFFMAN_EMIT_SYMBOL, 84),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 84),
+
+ # Node 50
+ (2, HUFFMAN_EMIT_SYMBOL, 77),
+ (9, HUFFMAN_EMIT_SYMBOL, 77),
+ (23, HUFFMAN_EMIT_SYMBOL, 77),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 77),
+ (2, HUFFMAN_EMIT_SYMBOL, 78),
+ (9, HUFFMAN_EMIT_SYMBOL, 78),
+ (23, HUFFMAN_EMIT_SYMBOL, 78),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 78),
+ (2, HUFFMAN_EMIT_SYMBOL, 79),
+ (9, HUFFMAN_EMIT_SYMBOL, 79),
+ (23, HUFFMAN_EMIT_SYMBOL, 79),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 79),
+ (2, HUFFMAN_EMIT_SYMBOL, 80),
+ (9, HUFFMAN_EMIT_SYMBOL, 80),
+ (23, HUFFMAN_EMIT_SYMBOL, 80),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 80),
+
+ # Node 51
+ (3, HUFFMAN_EMIT_SYMBOL, 77),
+ (6, HUFFMAN_EMIT_SYMBOL, 77),
+ (10, HUFFMAN_EMIT_SYMBOL, 77),
+ (15, HUFFMAN_EMIT_SYMBOL, 77),
+ (24, HUFFMAN_EMIT_SYMBOL, 77),
+ (31, HUFFMAN_EMIT_SYMBOL, 77),
+ (41, HUFFMAN_EMIT_SYMBOL, 77),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 77),
+ (3, HUFFMAN_EMIT_SYMBOL, 78),
+ (6, HUFFMAN_EMIT_SYMBOL, 78),
+ (10, HUFFMAN_EMIT_SYMBOL, 78),
+ (15, HUFFMAN_EMIT_SYMBOL, 78),
+ (24, HUFFMAN_EMIT_SYMBOL, 78),
+ (31, HUFFMAN_EMIT_SYMBOL, 78),
+ (41, HUFFMAN_EMIT_SYMBOL, 78),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 78),
+
+ # Node 52
+ (3, HUFFMAN_EMIT_SYMBOL, 79),
+ (6, HUFFMAN_EMIT_SYMBOL, 79),
+ (10, HUFFMAN_EMIT_SYMBOL, 79),
+ (15, HUFFMAN_EMIT_SYMBOL, 79),
+ (24, HUFFMAN_EMIT_SYMBOL, 79),
+ (31, HUFFMAN_EMIT_SYMBOL, 79),
+ (41, HUFFMAN_EMIT_SYMBOL, 79),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 79),
+ (3, HUFFMAN_EMIT_SYMBOL, 80),
+ (6, HUFFMAN_EMIT_SYMBOL, 80),
+ (10, HUFFMAN_EMIT_SYMBOL, 80),
+ (15, HUFFMAN_EMIT_SYMBOL, 80),
+ (24, HUFFMAN_EMIT_SYMBOL, 80),
+ (31, HUFFMAN_EMIT_SYMBOL, 80),
+ (41, HUFFMAN_EMIT_SYMBOL, 80),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 80),
+
+ # Node 53
+ (2, HUFFMAN_EMIT_SYMBOL, 81),
+ (9, HUFFMAN_EMIT_SYMBOL, 81),
+ (23, HUFFMAN_EMIT_SYMBOL, 81),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 81),
+ (2, HUFFMAN_EMIT_SYMBOL, 82),
+ (9, HUFFMAN_EMIT_SYMBOL, 82),
+ (23, HUFFMAN_EMIT_SYMBOL, 82),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 82),
+ (2, HUFFMAN_EMIT_SYMBOL, 83),
+ (9, HUFFMAN_EMIT_SYMBOL, 83),
+ (23, HUFFMAN_EMIT_SYMBOL, 83),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 83),
+ (2, HUFFMAN_EMIT_SYMBOL, 84),
+ (9, HUFFMAN_EMIT_SYMBOL, 84),
+ (23, HUFFMAN_EMIT_SYMBOL, 84),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 84),
+
+ # Node 54
+ (3, HUFFMAN_EMIT_SYMBOL, 81),
+ (6, HUFFMAN_EMIT_SYMBOL, 81),
+ (10, HUFFMAN_EMIT_SYMBOL, 81),
+ (15, HUFFMAN_EMIT_SYMBOL, 81),
+ (24, HUFFMAN_EMIT_SYMBOL, 81),
+ (31, HUFFMAN_EMIT_SYMBOL, 81),
+ (41, HUFFMAN_EMIT_SYMBOL, 81),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 81),
+ (3, HUFFMAN_EMIT_SYMBOL, 82),
+ (6, HUFFMAN_EMIT_SYMBOL, 82),
+ (10, HUFFMAN_EMIT_SYMBOL, 82),
+ (15, HUFFMAN_EMIT_SYMBOL, 82),
+ (24, HUFFMAN_EMIT_SYMBOL, 82),
+ (31, HUFFMAN_EMIT_SYMBOL, 82),
+ (41, HUFFMAN_EMIT_SYMBOL, 82),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 82),
+
+ # Node 55
+ (3, HUFFMAN_EMIT_SYMBOL, 83),
+ (6, HUFFMAN_EMIT_SYMBOL, 83),
+ (10, HUFFMAN_EMIT_SYMBOL, 83),
+ (15, HUFFMAN_EMIT_SYMBOL, 83),
+ (24, HUFFMAN_EMIT_SYMBOL, 83),
+ (31, HUFFMAN_EMIT_SYMBOL, 83),
+ (41, HUFFMAN_EMIT_SYMBOL, 83),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 83),
+ (3, HUFFMAN_EMIT_SYMBOL, 84),
+ (6, HUFFMAN_EMIT_SYMBOL, 84),
+ (10, HUFFMAN_EMIT_SYMBOL, 84),
+ (15, HUFFMAN_EMIT_SYMBOL, 84),
+ (24, HUFFMAN_EMIT_SYMBOL, 84),
+ (31, HUFFMAN_EMIT_SYMBOL, 84),
+ (41, HUFFMAN_EMIT_SYMBOL, 84),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 84),
+
+ # Node 56
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 85),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 86),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 87),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 89),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 106),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 107),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 113),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 118),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 119),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 120),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 121),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 122),
+ (70, 0, 0),
+ (71, 0, 0),
+ (73, 0, 0),
+ (74, HUFFMAN_COMPLETE, 0),
+
+ # Node 57
+ (1, HUFFMAN_EMIT_SYMBOL, 85),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 85),
+ (1, HUFFMAN_EMIT_SYMBOL, 86),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 86),
+ (1, HUFFMAN_EMIT_SYMBOL, 87),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 87),
+ (1, HUFFMAN_EMIT_SYMBOL, 89),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 89),
+ (1, HUFFMAN_EMIT_SYMBOL, 106),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 106),
+ (1, HUFFMAN_EMIT_SYMBOL, 107),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 107),
+ (1, HUFFMAN_EMIT_SYMBOL, 113),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 113),
+ (1, HUFFMAN_EMIT_SYMBOL, 118),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 118),
+
+ # Node 58
+ (2, HUFFMAN_EMIT_SYMBOL, 85),
+ (9, HUFFMAN_EMIT_SYMBOL, 85),
+ (23, HUFFMAN_EMIT_SYMBOL, 85),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 85),
+ (2, HUFFMAN_EMIT_SYMBOL, 86),
+ (9, HUFFMAN_EMIT_SYMBOL, 86),
+ (23, HUFFMAN_EMIT_SYMBOL, 86),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 86),
+ (2, HUFFMAN_EMIT_SYMBOL, 87),
+ (9, HUFFMAN_EMIT_SYMBOL, 87),
+ (23, HUFFMAN_EMIT_SYMBOL, 87),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 87),
+ (2, HUFFMAN_EMIT_SYMBOL, 89),
+ (9, HUFFMAN_EMIT_SYMBOL, 89),
+ (23, HUFFMAN_EMIT_SYMBOL, 89),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 89),
+
+ # Node 59
+ (3, HUFFMAN_EMIT_SYMBOL, 85),
+ (6, HUFFMAN_EMIT_SYMBOL, 85),
+ (10, HUFFMAN_EMIT_SYMBOL, 85),
+ (15, HUFFMAN_EMIT_SYMBOL, 85),
+ (24, HUFFMAN_EMIT_SYMBOL, 85),
+ (31, HUFFMAN_EMIT_SYMBOL, 85),
+ (41, HUFFMAN_EMIT_SYMBOL, 85),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 85),
+ (3, HUFFMAN_EMIT_SYMBOL, 86),
+ (6, HUFFMAN_EMIT_SYMBOL, 86),
+ (10, HUFFMAN_EMIT_SYMBOL, 86),
+ (15, HUFFMAN_EMIT_SYMBOL, 86),
+ (24, HUFFMAN_EMIT_SYMBOL, 86),
+ (31, HUFFMAN_EMIT_SYMBOL, 86),
+ (41, HUFFMAN_EMIT_SYMBOL, 86),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 86),
+
+ # Node 60
+ (3, HUFFMAN_EMIT_SYMBOL, 87),
+ (6, HUFFMAN_EMIT_SYMBOL, 87),
+ (10, HUFFMAN_EMIT_SYMBOL, 87),
+ (15, HUFFMAN_EMIT_SYMBOL, 87),
+ (24, HUFFMAN_EMIT_SYMBOL, 87),
+ (31, HUFFMAN_EMIT_SYMBOL, 87),
+ (41, HUFFMAN_EMIT_SYMBOL, 87),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 87),
+ (3, HUFFMAN_EMIT_SYMBOL, 89),
+ (6, HUFFMAN_EMIT_SYMBOL, 89),
+ (10, HUFFMAN_EMIT_SYMBOL, 89),
+ (15, HUFFMAN_EMIT_SYMBOL, 89),
+ (24, HUFFMAN_EMIT_SYMBOL, 89),
+ (31, HUFFMAN_EMIT_SYMBOL, 89),
+ (41, HUFFMAN_EMIT_SYMBOL, 89),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 89),
+
+ # Node 61
+ (2, HUFFMAN_EMIT_SYMBOL, 106),
+ (9, HUFFMAN_EMIT_SYMBOL, 106),
+ (23, HUFFMAN_EMIT_SYMBOL, 106),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 106),
+ (2, HUFFMAN_EMIT_SYMBOL, 107),
+ (9, HUFFMAN_EMIT_SYMBOL, 107),
+ (23, HUFFMAN_EMIT_SYMBOL, 107),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 107),
+ (2, HUFFMAN_EMIT_SYMBOL, 113),
+ (9, HUFFMAN_EMIT_SYMBOL, 113),
+ (23, HUFFMAN_EMIT_SYMBOL, 113),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 113),
+ (2, HUFFMAN_EMIT_SYMBOL, 118),
+ (9, HUFFMAN_EMIT_SYMBOL, 118),
+ (23, HUFFMAN_EMIT_SYMBOL, 118),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 118),
+
+ # Node 62
+ (3, HUFFMAN_EMIT_SYMBOL, 106),
+ (6, HUFFMAN_EMIT_SYMBOL, 106),
+ (10, HUFFMAN_EMIT_SYMBOL, 106),
+ (15, HUFFMAN_EMIT_SYMBOL, 106),
+ (24, HUFFMAN_EMIT_SYMBOL, 106),
+ (31, HUFFMAN_EMIT_SYMBOL, 106),
+ (41, HUFFMAN_EMIT_SYMBOL, 106),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 106),
+ (3, HUFFMAN_EMIT_SYMBOL, 107),
+ (6, HUFFMAN_EMIT_SYMBOL, 107),
+ (10, HUFFMAN_EMIT_SYMBOL, 107),
+ (15, HUFFMAN_EMIT_SYMBOL, 107),
+ (24, HUFFMAN_EMIT_SYMBOL, 107),
+ (31, HUFFMAN_EMIT_SYMBOL, 107),
+ (41, HUFFMAN_EMIT_SYMBOL, 107),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 107),
+
+ # Node 63
+ (3, HUFFMAN_EMIT_SYMBOL, 113),
+ (6, HUFFMAN_EMIT_SYMBOL, 113),
+ (10, HUFFMAN_EMIT_SYMBOL, 113),
+ (15, HUFFMAN_EMIT_SYMBOL, 113),
+ (24, HUFFMAN_EMIT_SYMBOL, 113),
+ (31, HUFFMAN_EMIT_SYMBOL, 113),
+ (41, HUFFMAN_EMIT_SYMBOL, 113),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 113),
+ (3, HUFFMAN_EMIT_SYMBOL, 118),
+ (6, HUFFMAN_EMIT_SYMBOL, 118),
+ (10, HUFFMAN_EMIT_SYMBOL, 118),
+ (15, HUFFMAN_EMIT_SYMBOL, 118),
+ (24, HUFFMAN_EMIT_SYMBOL, 118),
+ (31, HUFFMAN_EMIT_SYMBOL, 118),
+ (41, HUFFMAN_EMIT_SYMBOL, 118),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 118),
+
+ # Node 64
+ (1, HUFFMAN_EMIT_SYMBOL, 119),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 119),
+ (1, HUFFMAN_EMIT_SYMBOL, 120),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 120),
+ (1, HUFFMAN_EMIT_SYMBOL, 121),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 121),
+ (1, HUFFMAN_EMIT_SYMBOL, 122),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 122),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 38),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 42),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 44),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 59),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 88),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 90),
+ (75, 0, 0),
+ (78, 0, 0),
+
+ # Node 65
+ (2, HUFFMAN_EMIT_SYMBOL, 119),
+ (9, HUFFMAN_EMIT_SYMBOL, 119),
+ (23, HUFFMAN_EMIT_SYMBOL, 119),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 119),
+ (2, HUFFMAN_EMIT_SYMBOL, 120),
+ (9, HUFFMAN_EMIT_SYMBOL, 120),
+ (23, HUFFMAN_EMIT_SYMBOL, 120),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 120),
+ (2, HUFFMAN_EMIT_SYMBOL, 121),
+ (9, HUFFMAN_EMIT_SYMBOL, 121),
+ (23, HUFFMAN_EMIT_SYMBOL, 121),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 121),
+ (2, HUFFMAN_EMIT_SYMBOL, 122),
+ (9, HUFFMAN_EMIT_SYMBOL, 122),
+ (23, HUFFMAN_EMIT_SYMBOL, 122),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 122),
+
+ # Node 66
+ (3, HUFFMAN_EMIT_SYMBOL, 119),
+ (6, HUFFMAN_EMIT_SYMBOL, 119),
+ (10, HUFFMAN_EMIT_SYMBOL, 119),
+ (15, HUFFMAN_EMIT_SYMBOL, 119),
+ (24, HUFFMAN_EMIT_SYMBOL, 119),
+ (31, HUFFMAN_EMIT_SYMBOL, 119),
+ (41, HUFFMAN_EMIT_SYMBOL, 119),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 119),
+ (3, HUFFMAN_EMIT_SYMBOL, 120),
+ (6, HUFFMAN_EMIT_SYMBOL, 120),
+ (10, HUFFMAN_EMIT_SYMBOL, 120),
+ (15, HUFFMAN_EMIT_SYMBOL, 120),
+ (24, HUFFMAN_EMIT_SYMBOL, 120),
+ (31, HUFFMAN_EMIT_SYMBOL, 120),
+ (41, HUFFMAN_EMIT_SYMBOL, 120),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 120),
+
+ # Node 67
+ (3, HUFFMAN_EMIT_SYMBOL, 121),
+ (6, HUFFMAN_EMIT_SYMBOL, 121),
+ (10, HUFFMAN_EMIT_SYMBOL, 121),
+ (15, HUFFMAN_EMIT_SYMBOL, 121),
+ (24, HUFFMAN_EMIT_SYMBOL, 121),
+ (31, HUFFMAN_EMIT_SYMBOL, 121),
+ (41, HUFFMAN_EMIT_SYMBOL, 121),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 121),
+ (3, HUFFMAN_EMIT_SYMBOL, 122),
+ (6, HUFFMAN_EMIT_SYMBOL, 122),
+ (10, HUFFMAN_EMIT_SYMBOL, 122),
+ (15, HUFFMAN_EMIT_SYMBOL, 122),
+ (24, HUFFMAN_EMIT_SYMBOL, 122),
+ (31, HUFFMAN_EMIT_SYMBOL, 122),
+ (41, HUFFMAN_EMIT_SYMBOL, 122),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 122),
+
+ # Node 68
+ (1, HUFFMAN_EMIT_SYMBOL, 38),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 38),
+ (1, HUFFMAN_EMIT_SYMBOL, 42),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 42),
+ (1, HUFFMAN_EMIT_SYMBOL, 44),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 44),
+ (1, HUFFMAN_EMIT_SYMBOL, 59),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 59),
+ (1, HUFFMAN_EMIT_SYMBOL, 88),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 88),
+ (1, HUFFMAN_EMIT_SYMBOL, 90),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 90),
+ (76, 0, 0),
+ (77, 0, 0),
+ (79, 0, 0),
+ (81, 0, 0),
+
+ # Node 69
+ (2, HUFFMAN_EMIT_SYMBOL, 38),
+ (9, HUFFMAN_EMIT_SYMBOL, 38),
+ (23, HUFFMAN_EMIT_SYMBOL, 38),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 38),
+ (2, HUFFMAN_EMIT_SYMBOL, 42),
+ (9, HUFFMAN_EMIT_SYMBOL, 42),
+ (23, HUFFMAN_EMIT_SYMBOL, 42),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 42),
+ (2, HUFFMAN_EMIT_SYMBOL, 44),
+ (9, HUFFMAN_EMIT_SYMBOL, 44),
+ (23, HUFFMAN_EMIT_SYMBOL, 44),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 44),
+ (2, HUFFMAN_EMIT_SYMBOL, 59),
+ (9, HUFFMAN_EMIT_SYMBOL, 59),
+ (23, HUFFMAN_EMIT_SYMBOL, 59),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 59),
+
+ # Node 70
+ (3, HUFFMAN_EMIT_SYMBOL, 38),
+ (6, HUFFMAN_EMIT_SYMBOL, 38),
+ (10, HUFFMAN_EMIT_SYMBOL, 38),
+ (15, HUFFMAN_EMIT_SYMBOL, 38),
+ (24, HUFFMAN_EMIT_SYMBOL, 38),
+ (31, HUFFMAN_EMIT_SYMBOL, 38),
+ (41, HUFFMAN_EMIT_SYMBOL, 38),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 38),
+ (3, HUFFMAN_EMIT_SYMBOL, 42),
+ (6, HUFFMAN_EMIT_SYMBOL, 42),
+ (10, HUFFMAN_EMIT_SYMBOL, 42),
+ (15, HUFFMAN_EMIT_SYMBOL, 42),
+ (24, HUFFMAN_EMIT_SYMBOL, 42),
+ (31, HUFFMAN_EMIT_SYMBOL, 42),
+ (41, HUFFMAN_EMIT_SYMBOL, 42),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 42),
+
+ # Node 71
+ (3, HUFFMAN_EMIT_SYMBOL, 44),
+ (6, HUFFMAN_EMIT_SYMBOL, 44),
+ (10, HUFFMAN_EMIT_SYMBOL, 44),
+ (15, HUFFMAN_EMIT_SYMBOL, 44),
+ (24, HUFFMAN_EMIT_SYMBOL, 44),
+ (31, HUFFMAN_EMIT_SYMBOL, 44),
+ (41, HUFFMAN_EMIT_SYMBOL, 44),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 44),
+ (3, HUFFMAN_EMIT_SYMBOL, 59),
+ (6, HUFFMAN_EMIT_SYMBOL, 59),
+ (10, HUFFMAN_EMIT_SYMBOL, 59),
+ (15, HUFFMAN_EMIT_SYMBOL, 59),
+ (24, HUFFMAN_EMIT_SYMBOL, 59),
+ (31, HUFFMAN_EMIT_SYMBOL, 59),
+ (41, HUFFMAN_EMIT_SYMBOL, 59),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 59),
+
+ # Node 72
+ (2, HUFFMAN_EMIT_SYMBOL, 88),
+ (9, HUFFMAN_EMIT_SYMBOL, 88),
+ (23, HUFFMAN_EMIT_SYMBOL, 88),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 88),
+ (2, HUFFMAN_EMIT_SYMBOL, 90),
+ (9, HUFFMAN_EMIT_SYMBOL, 90),
+ (23, HUFFMAN_EMIT_SYMBOL, 90),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 90),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 33),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 34),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 40),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 41),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 63),
+ (80, 0, 0),
+ (82, 0, 0),
+ (84, 0, 0),
+
+ # Node 73
+ (3, HUFFMAN_EMIT_SYMBOL, 88),
+ (6, HUFFMAN_EMIT_SYMBOL, 88),
+ (10, HUFFMAN_EMIT_SYMBOL, 88),
+ (15, HUFFMAN_EMIT_SYMBOL, 88),
+ (24, HUFFMAN_EMIT_SYMBOL, 88),
+ (31, HUFFMAN_EMIT_SYMBOL, 88),
+ (41, HUFFMAN_EMIT_SYMBOL, 88),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 88),
+ (3, HUFFMAN_EMIT_SYMBOL, 90),
+ (6, HUFFMAN_EMIT_SYMBOL, 90),
+ (10, HUFFMAN_EMIT_SYMBOL, 90),
+ (15, HUFFMAN_EMIT_SYMBOL, 90),
+ (24, HUFFMAN_EMIT_SYMBOL, 90),
+ (31, HUFFMAN_EMIT_SYMBOL, 90),
+ (41, HUFFMAN_EMIT_SYMBOL, 90),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 90),
+
+ # Node 74
+ (1, HUFFMAN_EMIT_SYMBOL, 33),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 33),
+ (1, HUFFMAN_EMIT_SYMBOL, 34),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 34),
+ (1, HUFFMAN_EMIT_SYMBOL, 40),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 40),
+ (1, HUFFMAN_EMIT_SYMBOL, 41),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 41),
+ (1, HUFFMAN_EMIT_SYMBOL, 63),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 63),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 39),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 43),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 124),
+ (83, 0, 0),
+ (85, 0, 0),
+ (88, 0, 0),
+
+ # Node 75
+ (2, HUFFMAN_EMIT_SYMBOL, 33),
+ (9, HUFFMAN_EMIT_SYMBOL, 33),
+ (23, HUFFMAN_EMIT_SYMBOL, 33),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 33),
+ (2, HUFFMAN_EMIT_SYMBOL, 34),
+ (9, HUFFMAN_EMIT_SYMBOL, 34),
+ (23, HUFFMAN_EMIT_SYMBOL, 34),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 34),
+ (2, HUFFMAN_EMIT_SYMBOL, 40),
+ (9, HUFFMAN_EMIT_SYMBOL, 40),
+ (23, HUFFMAN_EMIT_SYMBOL, 40),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 40),
+ (2, HUFFMAN_EMIT_SYMBOL, 41),
+ (9, HUFFMAN_EMIT_SYMBOL, 41),
+ (23, HUFFMAN_EMIT_SYMBOL, 41),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 41),
+
+ # Node 76
+ (3, HUFFMAN_EMIT_SYMBOL, 33),
+ (6, HUFFMAN_EMIT_SYMBOL, 33),
+ (10, HUFFMAN_EMIT_SYMBOL, 33),
+ (15, HUFFMAN_EMIT_SYMBOL, 33),
+ (24, HUFFMAN_EMIT_SYMBOL, 33),
+ (31, HUFFMAN_EMIT_SYMBOL, 33),
+ (41, HUFFMAN_EMIT_SYMBOL, 33),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 33),
+ (3, HUFFMAN_EMIT_SYMBOL, 34),
+ (6, HUFFMAN_EMIT_SYMBOL, 34),
+ (10, HUFFMAN_EMIT_SYMBOL, 34),
+ (15, HUFFMAN_EMIT_SYMBOL, 34),
+ (24, HUFFMAN_EMIT_SYMBOL, 34),
+ (31, HUFFMAN_EMIT_SYMBOL, 34),
+ (41, HUFFMAN_EMIT_SYMBOL, 34),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 34),
+
+ # Node 77
+ (3, HUFFMAN_EMIT_SYMBOL, 40),
+ (6, HUFFMAN_EMIT_SYMBOL, 40),
+ (10, HUFFMAN_EMIT_SYMBOL, 40),
+ (15, HUFFMAN_EMIT_SYMBOL, 40),
+ (24, HUFFMAN_EMIT_SYMBOL, 40),
+ (31, HUFFMAN_EMIT_SYMBOL, 40),
+ (41, HUFFMAN_EMIT_SYMBOL, 40),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 40),
+ (3, HUFFMAN_EMIT_SYMBOL, 41),
+ (6, HUFFMAN_EMIT_SYMBOL, 41),
+ (10, HUFFMAN_EMIT_SYMBOL, 41),
+ (15, HUFFMAN_EMIT_SYMBOL, 41),
+ (24, HUFFMAN_EMIT_SYMBOL, 41),
+ (31, HUFFMAN_EMIT_SYMBOL, 41),
+ (41, HUFFMAN_EMIT_SYMBOL, 41),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 41),
+
+ # Node 78
+ (2, HUFFMAN_EMIT_SYMBOL, 63),
+ (9, HUFFMAN_EMIT_SYMBOL, 63),
+ (23, HUFFMAN_EMIT_SYMBOL, 63),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 63),
+ (1, HUFFMAN_EMIT_SYMBOL, 39),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 39),
+ (1, HUFFMAN_EMIT_SYMBOL, 43),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 43),
+ (1, HUFFMAN_EMIT_SYMBOL, 124),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 124),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 35),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 62),
+ (86, 0, 0),
+ (87, 0, 0),
+ (89, 0, 0),
+ (90, 0, 0),
+
+ # Node 79
+ (3, HUFFMAN_EMIT_SYMBOL, 63),
+ (6, HUFFMAN_EMIT_SYMBOL, 63),
+ (10, HUFFMAN_EMIT_SYMBOL, 63),
+ (15, HUFFMAN_EMIT_SYMBOL, 63),
+ (24, HUFFMAN_EMIT_SYMBOL, 63),
+ (31, HUFFMAN_EMIT_SYMBOL, 63),
+ (41, HUFFMAN_EMIT_SYMBOL, 63),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 63),
+ (2, HUFFMAN_EMIT_SYMBOL, 39),
+ (9, HUFFMAN_EMIT_SYMBOL, 39),
+ (23, HUFFMAN_EMIT_SYMBOL, 39),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 39),
+ (2, HUFFMAN_EMIT_SYMBOL, 43),
+ (9, HUFFMAN_EMIT_SYMBOL, 43),
+ (23, HUFFMAN_EMIT_SYMBOL, 43),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 43),
+
+ # Node 80
+ (3, HUFFMAN_EMIT_SYMBOL, 39),
+ (6, HUFFMAN_EMIT_SYMBOL, 39),
+ (10, HUFFMAN_EMIT_SYMBOL, 39),
+ (15, HUFFMAN_EMIT_SYMBOL, 39),
+ (24, HUFFMAN_EMIT_SYMBOL, 39),
+ (31, HUFFMAN_EMIT_SYMBOL, 39),
+ (41, HUFFMAN_EMIT_SYMBOL, 39),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 39),
+ (3, HUFFMAN_EMIT_SYMBOL, 43),
+ (6, HUFFMAN_EMIT_SYMBOL, 43),
+ (10, HUFFMAN_EMIT_SYMBOL, 43),
+ (15, HUFFMAN_EMIT_SYMBOL, 43),
+ (24, HUFFMAN_EMIT_SYMBOL, 43),
+ (31, HUFFMAN_EMIT_SYMBOL, 43),
+ (41, HUFFMAN_EMIT_SYMBOL, 43),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 43),
+
+ # Node 81
+ (2, HUFFMAN_EMIT_SYMBOL, 124),
+ (9, HUFFMAN_EMIT_SYMBOL, 124),
+ (23, HUFFMAN_EMIT_SYMBOL, 124),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 124),
+ (1, HUFFMAN_EMIT_SYMBOL, 35),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 35),
+ (1, HUFFMAN_EMIT_SYMBOL, 62),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 62),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 0),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 36),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 64),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 91),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 93),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 126),
+ (91, 0, 0),
+ (92, 0, 0),
+
+ # Node 82
+ (3, HUFFMAN_EMIT_SYMBOL, 124),
+ (6, HUFFMAN_EMIT_SYMBOL, 124),
+ (10, HUFFMAN_EMIT_SYMBOL, 124),
+ (15, HUFFMAN_EMIT_SYMBOL, 124),
+ (24, HUFFMAN_EMIT_SYMBOL, 124),
+ (31, HUFFMAN_EMIT_SYMBOL, 124),
+ (41, HUFFMAN_EMIT_SYMBOL, 124),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 124),
+ (2, HUFFMAN_EMIT_SYMBOL, 35),
+ (9, HUFFMAN_EMIT_SYMBOL, 35),
+ (23, HUFFMAN_EMIT_SYMBOL, 35),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 35),
+ (2, HUFFMAN_EMIT_SYMBOL, 62),
+ (9, HUFFMAN_EMIT_SYMBOL, 62),
+ (23, HUFFMAN_EMIT_SYMBOL, 62),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 62),
+
+ # Node 83
+ (3, HUFFMAN_EMIT_SYMBOL, 35),
+ (6, HUFFMAN_EMIT_SYMBOL, 35),
+ (10, HUFFMAN_EMIT_SYMBOL, 35),
+ (15, HUFFMAN_EMIT_SYMBOL, 35),
+ (24, HUFFMAN_EMIT_SYMBOL, 35),
+ (31, HUFFMAN_EMIT_SYMBOL, 35),
+ (41, HUFFMAN_EMIT_SYMBOL, 35),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 35),
+ (3, HUFFMAN_EMIT_SYMBOL, 62),
+ (6, HUFFMAN_EMIT_SYMBOL, 62),
+ (10, HUFFMAN_EMIT_SYMBOL, 62),
+ (15, HUFFMAN_EMIT_SYMBOL, 62),
+ (24, HUFFMAN_EMIT_SYMBOL, 62),
+ (31, HUFFMAN_EMIT_SYMBOL, 62),
+ (41, HUFFMAN_EMIT_SYMBOL, 62),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 62),
+
+ # Node 84
+ (1, HUFFMAN_EMIT_SYMBOL, 0),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 0),
+ (1, HUFFMAN_EMIT_SYMBOL, 36),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 36),
+ (1, HUFFMAN_EMIT_SYMBOL, 64),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 64),
+ (1, HUFFMAN_EMIT_SYMBOL, 91),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 91),
+ (1, HUFFMAN_EMIT_SYMBOL, 93),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 93),
+ (1, HUFFMAN_EMIT_SYMBOL, 126),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 126),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 94),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 125),
+ (93, 0, 0),
+ (94, 0, 0),
+
+ # Node 85
+ (2, HUFFMAN_EMIT_SYMBOL, 0),
+ (9, HUFFMAN_EMIT_SYMBOL, 0),
+ (23, HUFFMAN_EMIT_SYMBOL, 0),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 0),
+ (2, HUFFMAN_EMIT_SYMBOL, 36),
+ (9, HUFFMAN_EMIT_SYMBOL, 36),
+ (23, HUFFMAN_EMIT_SYMBOL, 36),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 36),
+ (2, HUFFMAN_EMIT_SYMBOL, 64),
+ (9, HUFFMAN_EMIT_SYMBOL, 64),
+ (23, HUFFMAN_EMIT_SYMBOL, 64),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 64),
+ (2, HUFFMAN_EMIT_SYMBOL, 91),
+ (9, HUFFMAN_EMIT_SYMBOL, 91),
+ (23, HUFFMAN_EMIT_SYMBOL, 91),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 91),
+
+ # Node 86
+ (3, HUFFMAN_EMIT_SYMBOL, 0),
+ (6, HUFFMAN_EMIT_SYMBOL, 0),
+ (10, HUFFMAN_EMIT_SYMBOL, 0),
+ (15, HUFFMAN_EMIT_SYMBOL, 0),
+ (24, HUFFMAN_EMIT_SYMBOL, 0),
+ (31, HUFFMAN_EMIT_SYMBOL, 0),
+ (41, HUFFMAN_EMIT_SYMBOL, 0),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 0),
+ (3, HUFFMAN_EMIT_SYMBOL, 36),
+ (6, HUFFMAN_EMIT_SYMBOL, 36),
+ (10, HUFFMAN_EMIT_SYMBOL, 36),
+ (15, HUFFMAN_EMIT_SYMBOL, 36),
+ (24, HUFFMAN_EMIT_SYMBOL, 36),
+ (31, HUFFMAN_EMIT_SYMBOL, 36),
+ (41, HUFFMAN_EMIT_SYMBOL, 36),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 36),
+
+ # Node 87
+ (3, HUFFMAN_EMIT_SYMBOL, 64),
+ (6, HUFFMAN_EMIT_SYMBOL, 64),
+ (10, HUFFMAN_EMIT_SYMBOL, 64),
+ (15, HUFFMAN_EMIT_SYMBOL, 64),
+ (24, HUFFMAN_EMIT_SYMBOL, 64),
+ (31, HUFFMAN_EMIT_SYMBOL, 64),
+ (41, HUFFMAN_EMIT_SYMBOL, 64),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 64),
+ (3, HUFFMAN_EMIT_SYMBOL, 91),
+ (6, HUFFMAN_EMIT_SYMBOL, 91),
+ (10, HUFFMAN_EMIT_SYMBOL, 91),
+ (15, HUFFMAN_EMIT_SYMBOL, 91),
+ (24, HUFFMAN_EMIT_SYMBOL, 91),
+ (31, HUFFMAN_EMIT_SYMBOL, 91),
+ (41, HUFFMAN_EMIT_SYMBOL, 91),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 91),
+
+ # Node 88
+ (2, HUFFMAN_EMIT_SYMBOL, 93),
+ (9, HUFFMAN_EMIT_SYMBOL, 93),
+ (23, HUFFMAN_EMIT_SYMBOL, 93),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 93),
+ (2, HUFFMAN_EMIT_SYMBOL, 126),
+ (9, HUFFMAN_EMIT_SYMBOL, 126),
+ (23, HUFFMAN_EMIT_SYMBOL, 126),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 126),
+ (1, HUFFMAN_EMIT_SYMBOL, 94),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 94),
+ (1, HUFFMAN_EMIT_SYMBOL, 125),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 125),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 60),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 96),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 123),
+ (95, 0, 0),
+
+ # Node 89
+ (3, HUFFMAN_EMIT_SYMBOL, 93),
+ (6, HUFFMAN_EMIT_SYMBOL, 93),
+ (10, HUFFMAN_EMIT_SYMBOL, 93),
+ (15, HUFFMAN_EMIT_SYMBOL, 93),
+ (24, HUFFMAN_EMIT_SYMBOL, 93),
+ (31, HUFFMAN_EMIT_SYMBOL, 93),
+ (41, HUFFMAN_EMIT_SYMBOL, 93),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 93),
+ (3, HUFFMAN_EMIT_SYMBOL, 126),
+ (6, HUFFMAN_EMIT_SYMBOL, 126),
+ (10, HUFFMAN_EMIT_SYMBOL, 126),
+ (15, HUFFMAN_EMIT_SYMBOL, 126),
+ (24, HUFFMAN_EMIT_SYMBOL, 126),
+ (31, HUFFMAN_EMIT_SYMBOL, 126),
+ (41, HUFFMAN_EMIT_SYMBOL, 126),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 126),
+
+ # Node 90
+ (2, HUFFMAN_EMIT_SYMBOL, 94),
+ (9, HUFFMAN_EMIT_SYMBOL, 94),
+ (23, HUFFMAN_EMIT_SYMBOL, 94),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 94),
+ (2, HUFFMAN_EMIT_SYMBOL, 125),
+ (9, HUFFMAN_EMIT_SYMBOL, 125),
+ (23, HUFFMAN_EMIT_SYMBOL, 125),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 125),
+ (1, HUFFMAN_EMIT_SYMBOL, 60),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 60),
+ (1, HUFFMAN_EMIT_SYMBOL, 96),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 96),
+ (1, HUFFMAN_EMIT_SYMBOL, 123),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 123),
+ (96, 0, 0),
+ (110, 0, 0),
+
+ # Node 91
+ (3, HUFFMAN_EMIT_SYMBOL, 94),
+ (6, HUFFMAN_EMIT_SYMBOL, 94),
+ (10, HUFFMAN_EMIT_SYMBOL, 94),
+ (15, HUFFMAN_EMIT_SYMBOL, 94),
+ (24, HUFFMAN_EMIT_SYMBOL, 94),
+ (31, HUFFMAN_EMIT_SYMBOL, 94),
+ (41, HUFFMAN_EMIT_SYMBOL, 94),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 94),
+ (3, HUFFMAN_EMIT_SYMBOL, 125),
+ (6, HUFFMAN_EMIT_SYMBOL, 125),
+ (10, HUFFMAN_EMIT_SYMBOL, 125),
+ (15, HUFFMAN_EMIT_SYMBOL, 125),
+ (24, HUFFMAN_EMIT_SYMBOL, 125),
+ (31, HUFFMAN_EMIT_SYMBOL, 125),
+ (41, HUFFMAN_EMIT_SYMBOL, 125),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 125),
+
+ # Node 92
+ (2, HUFFMAN_EMIT_SYMBOL, 60),
+ (9, HUFFMAN_EMIT_SYMBOL, 60),
+ (23, HUFFMAN_EMIT_SYMBOL, 60),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 60),
+ (2, HUFFMAN_EMIT_SYMBOL, 96),
+ (9, HUFFMAN_EMIT_SYMBOL, 96),
+ (23, HUFFMAN_EMIT_SYMBOL, 96),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 96),
+ (2, HUFFMAN_EMIT_SYMBOL, 123),
+ (9, HUFFMAN_EMIT_SYMBOL, 123),
+ (23, HUFFMAN_EMIT_SYMBOL, 123),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 123),
+ (97, 0, 0),
+ (101, 0, 0),
+ (111, 0, 0),
+ (133, 0, 0),
+
+ # Node 93
+ (3, HUFFMAN_EMIT_SYMBOL, 60),
+ (6, HUFFMAN_EMIT_SYMBOL, 60),
+ (10, HUFFMAN_EMIT_SYMBOL, 60),
+ (15, HUFFMAN_EMIT_SYMBOL, 60),
+ (24, HUFFMAN_EMIT_SYMBOL, 60),
+ (31, HUFFMAN_EMIT_SYMBOL, 60),
+ (41, HUFFMAN_EMIT_SYMBOL, 60),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 60),
+ (3, HUFFMAN_EMIT_SYMBOL, 96),
+ (6, HUFFMAN_EMIT_SYMBOL, 96),
+ (10, HUFFMAN_EMIT_SYMBOL, 96),
+ (15, HUFFMAN_EMIT_SYMBOL, 96),
+ (24, HUFFMAN_EMIT_SYMBOL, 96),
+ (31, HUFFMAN_EMIT_SYMBOL, 96),
+ (41, HUFFMAN_EMIT_SYMBOL, 96),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 96),
+
+ # Node 94
+ (3, HUFFMAN_EMIT_SYMBOL, 123),
+ (6, HUFFMAN_EMIT_SYMBOL, 123),
+ (10, HUFFMAN_EMIT_SYMBOL, 123),
+ (15, HUFFMAN_EMIT_SYMBOL, 123),
+ (24, HUFFMAN_EMIT_SYMBOL, 123),
+ (31, HUFFMAN_EMIT_SYMBOL, 123),
+ (41, HUFFMAN_EMIT_SYMBOL, 123),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 123),
+ (98, 0, 0),
+ (99, 0, 0),
+ (102, 0, 0),
+ (105, 0, 0),
+ (112, 0, 0),
+ (119, 0, 0),
+ (134, 0, 0),
+ (153, 0, 0),
+
+ # Node 95
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 92),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 195),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 208),
+ (100, 0, 0),
+ (103, 0, 0),
+ (104, 0, 0),
+ (106, 0, 0),
+ (107, 0, 0),
+ (113, 0, 0),
+ (116, 0, 0),
+ (120, 0, 0),
+ (126, 0, 0),
+ (135, 0, 0),
+ (142, 0, 0),
+ (154, 0, 0),
+ (169, 0, 0),
+
+ # Node 96
+ (1, HUFFMAN_EMIT_SYMBOL, 92),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 92),
+ (1, HUFFMAN_EMIT_SYMBOL, 195),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 195),
+ (1, HUFFMAN_EMIT_SYMBOL, 208),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 208),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 128),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 130),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 131),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 162),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 184),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 194),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 224),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 226),
+ (108, 0, 0),
+ (109, 0, 0),
+
+ # Node 97
+ (2, HUFFMAN_EMIT_SYMBOL, 92),
+ (9, HUFFMAN_EMIT_SYMBOL, 92),
+ (23, HUFFMAN_EMIT_SYMBOL, 92),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 92),
+ (2, HUFFMAN_EMIT_SYMBOL, 195),
+ (9, HUFFMAN_EMIT_SYMBOL, 195),
+ (23, HUFFMAN_EMIT_SYMBOL, 195),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 195),
+ (2, HUFFMAN_EMIT_SYMBOL, 208),
+ (9, HUFFMAN_EMIT_SYMBOL, 208),
+ (23, HUFFMAN_EMIT_SYMBOL, 208),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 208),
+ (1, HUFFMAN_EMIT_SYMBOL, 128),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 128),
+ (1, HUFFMAN_EMIT_SYMBOL, 130),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 130),
+
+ # Node 98
+ (3, HUFFMAN_EMIT_SYMBOL, 92),
+ (6, HUFFMAN_EMIT_SYMBOL, 92),
+ (10, HUFFMAN_EMIT_SYMBOL, 92),
+ (15, HUFFMAN_EMIT_SYMBOL, 92),
+ (24, HUFFMAN_EMIT_SYMBOL, 92),
+ (31, HUFFMAN_EMIT_SYMBOL, 92),
+ (41, HUFFMAN_EMIT_SYMBOL, 92),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 92),
+ (3, HUFFMAN_EMIT_SYMBOL, 195),
+ (6, HUFFMAN_EMIT_SYMBOL, 195),
+ (10, HUFFMAN_EMIT_SYMBOL, 195),
+ (15, HUFFMAN_EMIT_SYMBOL, 195),
+ (24, HUFFMAN_EMIT_SYMBOL, 195),
+ (31, HUFFMAN_EMIT_SYMBOL, 195),
+ (41, HUFFMAN_EMIT_SYMBOL, 195),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 195),
+
+ # Node 99
+ (3, HUFFMAN_EMIT_SYMBOL, 208),
+ (6, HUFFMAN_EMIT_SYMBOL, 208),
+ (10, HUFFMAN_EMIT_SYMBOL, 208),
+ (15, HUFFMAN_EMIT_SYMBOL, 208),
+ (24, HUFFMAN_EMIT_SYMBOL, 208),
+ (31, HUFFMAN_EMIT_SYMBOL, 208),
+ (41, HUFFMAN_EMIT_SYMBOL, 208),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 208),
+ (2, HUFFMAN_EMIT_SYMBOL, 128),
+ (9, HUFFMAN_EMIT_SYMBOL, 128),
+ (23, HUFFMAN_EMIT_SYMBOL, 128),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 128),
+ (2, HUFFMAN_EMIT_SYMBOL, 130),
+ (9, HUFFMAN_EMIT_SYMBOL, 130),
+ (23, HUFFMAN_EMIT_SYMBOL, 130),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 130),
+
+ # Node 100
+ (3, HUFFMAN_EMIT_SYMBOL, 128),
+ (6, HUFFMAN_EMIT_SYMBOL, 128),
+ (10, HUFFMAN_EMIT_SYMBOL, 128),
+ (15, HUFFMAN_EMIT_SYMBOL, 128),
+ (24, HUFFMAN_EMIT_SYMBOL, 128),
+ (31, HUFFMAN_EMIT_SYMBOL, 128),
+ (41, HUFFMAN_EMIT_SYMBOL, 128),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 128),
+ (3, HUFFMAN_EMIT_SYMBOL, 130),
+ (6, HUFFMAN_EMIT_SYMBOL, 130),
+ (10, HUFFMAN_EMIT_SYMBOL, 130),
+ (15, HUFFMAN_EMIT_SYMBOL, 130),
+ (24, HUFFMAN_EMIT_SYMBOL, 130),
+ (31, HUFFMAN_EMIT_SYMBOL, 130),
+ (41, HUFFMAN_EMIT_SYMBOL, 130),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 130),
+
+ # Node 101
+ (1, HUFFMAN_EMIT_SYMBOL, 131),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 131),
+ (1, HUFFMAN_EMIT_SYMBOL, 162),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 162),
+ (1, HUFFMAN_EMIT_SYMBOL, 184),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 184),
+ (1, HUFFMAN_EMIT_SYMBOL, 194),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 194),
+ (1, HUFFMAN_EMIT_SYMBOL, 224),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 224),
+ (1, HUFFMAN_EMIT_SYMBOL, 226),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 226),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 153),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 161),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 167),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 172),
+
+ # Node 102
+ (2, HUFFMAN_EMIT_SYMBOL, 131),
+ (9, HUFFMAN_EMIT_SYMBOL, 131),
+ (23, HUFFMAN_EMIT_SYMBOL, 131),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 131),
+ (2, HUFFMAN_EMIT_SYMBOL, 162),
+ (9, HUFFMAN_EMIT_SYMBOL, 162),
+ (23, HUFFMAN_EMIT_SYMBOL, 162),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 162),
+ (2, HUFFMAN_EMIT_SYMBOL, 184),
+ (9, HUFFMAN_EMIT_SYMBOL, 184),
+ (23, HUFFMAN_EMIT_SYMBOL, 184),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 184),
+ (2, HUFFMAN_EMIT_SYMBOL, 194),
+ (9, HUFFMAN_EMIT_SYMBOL, 194),
+ (23, HUFFMAN_EMIT_SYMBOL, 194),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 194),
+
+ # Node 103
+ (3, HUFFMAN_EMIT_SYMBOL, 131),
+ (6, HUFFMAN_EMIT_SYMBOL, 131),
+ (10, HUFFMAN_EMIT_SYMBOL, 131),
+ (15, HUFFMAN_EMIT_SYMBOL, 131),
+ (24, HUFFMAN_EMIT_SYMBOL, 131),
+ (31, HUFFMAN_EMIT_SYMBOL, 131),
+ (41, HUFFMAN_EMIT_SYMBOL, 131),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 131),
+ (3, HUFFMAN_EMIT_SYMBOL, 162),
+ (6, HUFFMAN_EMIT_SYMBOL, 162),
+ (10, HUFFMAN_EMIT_SYMBOL, 162),
+ (15, HUFFMAN_EMIT_SYMBOL, 162),
+ (24, HUFFMAN_EMIT_SYMBOL, 162),
+ (31, HUFFMAN_EMIT_SYMBOL, 162),
+ (41, HUFFMAN_EMIT_SYMBOL, 162),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 162),
+
+ # Node 104
+ (3, HUFFMAN_EMIT_SYMBOL, 184),
+ (6, HUFFMAN_EMIT_SYMBOL, 184),
+ (10, HUFFMAN_EMIT_SYMBOL, 184),
+ (15, HUFFMAN_EMIT_SYMBOL, 184),
+ (24, HUFFMAN_EMIT_SYMBOL, 184),
+ (31, HUFFMAN_EMIT_SYMBOL, 184),
+ (41, HUFFMAN_EMIT_SYMBOL, 184),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 184),
+ (3, HUFFMAN_EMIT_SYMBOL, 194),
+ (6, HUFFMAN_EMIT_SYMBOL, 194),
+ (10, HUFFMAN_EMIT_SYMBOL, 194),
+ (15, HUFFMAN_EMIT_SYMBOL, 194),
+ (24, HUFFMAN_EMIT_SYMBOL, 194),
+ (31, HUFFMAN_EMIT_SYMBOL, 194),
+ (41, HUFFMAN_EMIT_SYMBOL, 194),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 194),
+
+ # Node 105
+ (2, HUFFMAN_EMIT_SYMBOL, 224),
+ (9, HUFFMAN_EMIT_SYMBOL, 224),
+ (23, HUFFMAN_EMIT_SYMBOL, 224),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 224),
+ (2, HUFFMAN_EMIT_SYMBOL, 226),
+ (9, HUFFMAN_EMIT_SYMBOL, 226),
+ (23, HUFFMAN_EMIT_SYMBOL, 226),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 226),
+ (1, HUFFMAN_EMIT_SYMBOL, 153),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 153),
+ (1, HUFFMAN_EMIT_SYMBOL, 161),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 161),
+ (1, HUFFMAN_EMIT_SYMBOL, 167),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 167),
+ (1, HUFFMAN_EMIT_SYMBOL, 172),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 172),
+
+ # Node 106
+ (3, HUFFMAN_EMIT_SYMBOL, 224),
+ (6, HUFFMAN_EMIT_SYMBOL, 224),
+ (10, HUFFMAN_EMIT_SYMBOL, 224),
+ (15, HUFFMAN_EMIT_SYMBOL, 224),
+ (24, HUFFMAN_EMIT_SYMBOL, 224),
+ (31, HUFFMAN_EMIT_SYMBOL, 224),
+ (41, HUFFMAN_EMIT_SYMBOL, 224),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 224),
+ (3, HUFFMAN_EMIT_SYMBOL, 226),
+ (6, HUFFMAN_EMIT_SYMBOL, 226),
+ (10, HUFFMAN_EMIT_SYMBOL, 226),
+ (15, HUFFMAN_EMIT_SYMBOL, 226),
+ (24, HUFFMAN_EMIT_SYMBOL, 226),
+ (31, HUFFMAN_EMIT_SYMBOL, 226),
+ (41, HUFFMAN_EMIT_SYMBOL, 226),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 226),
+
+ # Node 107
+ (2, HUFFMAN_EMIT_SYMBOL, 153),
+ (9, HUFFMAN_EMIT_SYMBOL, 153),
+ (23, HUFFMAN_EMIT_SYMBOL, 153),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 153),
+ (2, HUFFMAN_EMIT_SYMBOL, 161),
+ (9, HUFFMAN_EMIT_SYMBOL, 161),
+ (23, HUFFMAN_EMIT_SYMBOL, 161),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 161),
+ (2, HUFFMAN_EMIT_SYMBOL, 167),
+ (9, HUFFMAN_EMIT_SYMBOL, 167),
+ (23, HUFFMAN_EMIT_SYMBOL, 167),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 167),
+ (2, HUFFMAN_EMIT_SYMBOL, 172),
+ (9, HUFFMAN_EMIT_SYMBOL, 172),
+ (23, HUFFMAN_EMIT_SYMBOL, 172),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 172),
+
+ # Node 108
+ (3, HUFFMAN_EMIT_SYMBOL, 153),
+ (6, HUFFMAN_EMIT_SYMBOL, 153),
+ (10, HUFFMAN_EMIT_SYMBOL, 153),
+ (15, HUFFMAN_EMIT_SYMBOL, 153),
+ (24, HUFFMAN_EMIT_SYMBOL, 153),
+ (31, HUFFMAN_EMIT_SYMBOL, 153),
+ (41, HUFFMAN_EMIT_SYMBOL, 153),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 153),
+ (3, HUFFMAN_EMIT_SYMBOL, 161),
+ (6, HUFFMAN_EMIT_SYMBOL, 161),
+ (10, HUFFMAN_EMIT_SYMBOL, 161),
+ (15, HUFFMAN_EMIT_SYMBOL, 161),
+ (24, HUFFMAN_EMIT_SYMBOL, 161),
+ (31, HUFFMAN_EMIT_SYMBOL, 161),
+ (41, HUFFMAN_EMIT_SYMBOL, 161),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 161),
+
+ # Node 109
+ (3, HUFFMAN_EMIT_SYMBOL, 167),
+ (6, HUFFMAN_EMIT_SYMBOL, 167),
+ (10, HUFFMAN_EMIT_SYMBOL, 167),
+ (15, HUFFMAN_EMIT_SYMBOL, 167),
+ (24, HUFFMAN_EMIT_SYMBOL, 167),
+ (31, HUFFMAN_EMIT_SYMBOL, 167),
+ (41, HUFFMAN_EMIT_SYMBOL, 167),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 167),
+ (3, HUFFMAN_EMIT_SYMBOL, 172),
+ (6, HUFFMAN_EMIT_SYMBOL, 172),
+ (10, HUFFMAN_EMIT_SYMBOL, 172),
+ (15, HUFFMAN_EMIT_SYMBOL, 172),
+ (24, HUFFMAN_EMIT_SYMBOL, 172),
+ (31, HUFFMAN_EMIT_SYMBOL, 172),
+ (41, HUFFMAN_EMIT_SYMBOL, 172),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 172),
+
+ # Node 110
+ (114, 0, 0),
+ (115, 0, 0),
+ (117, 0, 0),
+ (118, 0, 0),
+ (121, 0, 0),
+ (123, 0, 0),
+ (127, 0, 0),
+ (130, 0, 0),
+ (136, 0, 0),
+ (139, 0, 0),
+ (143, 0, 0),
+ (146, 0, 0),
+ (155, 0, 0),
+ (162, 0, 0),
+ (170, 0, 0),
+ (180, 0, 0),
+
+ # Node 111
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 176),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 177),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 179),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 209),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 216),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 217),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 227),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 229),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 230),
+ (122, 0, 0),
+ (124, 0, 0),
+ (125, 0, 0),
+ (128, 0, 0),
+ (129, 0, 0),
+ (131, 0, 0),
+ (132, 0, 0),
+
+ # Node 112
+ (1, HUFFMAN_EMIT_SYMBOL, 176),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 176),
+ (1, HUFFMAN_EMIT_SYMBOL, 177),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 177),
+ (1, HUFFMAN_EMIT_SYMBOL, 179),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 179),
+ (1, HUFFMAN_EMIT_SYMBOL, 209),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 209),
+ (1, HUFFMAN_EMIT_SYMBOL, 216),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 216),
+ (1, HUFFMAN_EMIT_SYMBOL, 217),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 217),
+ (1, HUFFMAN_EMIT_SYMBOL, 227),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 227),
+ (1, HUFFMAN_EMIT_SYMBOL, 229),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 229),
+
+ # Node 113
+ (2, HUFFMAN_EMIT_SYMBOL, 176),
+ (9, HUFFMAN_EMIT_SYMBOL, 176),
+ (23, HUFFMAN_EMIT_SYMBOL, 176),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 176),
+ (2, HUFFMAN_EMIT_SYMBOL, 177),
+ (9, HUFFMAN_EMIT_SYMBOL, 177),
+ (23, HUFFMAN_EMIT_SYMBOL, 177),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 177),
+ (2, HUFFMAN_EMIT_SYMBOL, 179),
+ (9, HUFFMAN_EMIT_SYMBOL, 179),
+ (23, HUFFMAN_EMIT_SYMBOL, 179),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 179),
+ (2, HUFFMAN_EMIT_SYMBOL, 209),
+ (9, HUFFMAN_EMIT_SYMBOL, 209),
+ (23, HUFFMAN_EMIT_SYMBOL, 209),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 209),
+
+ # Node 114
+ (3, HUFFMAN_EMIT_SYMBOL, 176),
+ (6, HUFFMAN_EMIT_SYMBOL, 176),
+ (10, HUFFMAN_EMIT_SYMBOL, 176),
+ (15, HUFFMAN_EMIT_SYMBOL, 176),
+ (24, HUFFMAN_EMIT_SYMBOL, 176),
+ (31, HUFFMAN_EMIT_SYMBOL, 176),
+ (41, HUFFMAN_EMIT_SYMBOL, 176),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 176),
+ (3, HUFFMAN_EMIT_SYMBOL, 177),
+ (6, HUFFMAN_EMIT_SYMBOL, 177),
+ (10, HUFFMAN_EMIT_SYMBOL, 177),
+ (15, HUFFMAN_EMIT_SYMBOL, 177),
+ (24, HUFFMAN_EMIT_SYMBOL, 177),
+ (31, HUFFMAN_EMIT_SYMBOL, 177),
+ (41, HUFFMAN_EMIT_SYMBOL, 177),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 177),
+
+ # Node 115
+ (3, HUFFMAN_EMIT_SYMBOL, 179),
+ (6, HUFFMAN_EMIT_SYMBOL, 179),
+ (10, HUFFMAN_EMIT_SYMBOL, 179),
+ (15, HUFFMAN_EMIT_SYMBOL, 179),
+ (24, HUFFMAN_EMIT_SYMBOL, 179),
+ (31, HUFFMAN_EMIT_SYMBOL, 179),
+ (41, HUFFMAN_EMIT_SYMBOL, 179),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 179),
+ (3, HUFFMAN_EMIT_SYMBOL, 209),
+ (6, HUFFMAN_EMIT_SYMBOL, 209),
+ (10, HUFFMAN_EMIT_SYMBOL, 209),
+ (15, HUFFMAN_EMIT_SYMBOL, 209),
+ (24, HUFFMAN_EMIT_SYMBOL, 209),
+ (31, HUFFMAN_EMIT_SYMBOL, 209),
+ (41, HUFFMAN_EMIT_SYMBOL, 209),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 209),
+
+ # Node 116
+ (2, HUFFMAN_EMIT_SYMBOL, 216),
+ (9, HUFFMAN_EMIT_SYMBOL, 216),
+ (23, HUFFMAN_EMIT_SYMBOL, 216),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 216),
+ (2, HUFFMAN_EMIT_SYMBOL, 217),
+ (9, HUFFMAN_EMIT_SYMBOL, 217),
+ (23, HUFFMAN_EMIT_SYMBOL, 217),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 217),
+ (2, HUFFMAN_EMIT_SYMBOL, 227),
+ (9, HUFFMAN_EMIT_SYMBOL, 227),
+ (23, HUFFMAN_EMIT_SYMBOL, 227),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 227),
+ (2, HUFFMAN_EMIT_SYMBOL, 229),
+ (9, HUFFMAN_EMIT_SYMBOL, 229),
+ (23, HUFFMAN_EMIT_SYMBOL, 229),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 229),
+
+ # Node 117
+ (3, HUFFMAN_EMIT_SYMBOL, 216),
+ (6, HUFFMAN_EMIT_SYMBOL, 216),
+ (10, HUFFMAN_EMIT_SYMBOL, 216),
+ (15, HUFFMAN_EMIT_SYMBOL, 216),
+ (24, HUFFMAN_EMIT_SYMBOL, 216),
+ (31, HUFFMAN_EMIT_SYMBOL, 216),
+ (41, HUFFMAN_EMIT_SYMBOL, 216),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 216),
+ (3, HUFFMAN_EMIT_SYMBOL, 217),
+ (6, HUFFMAN_EMIT_SYMBOL, 217),
+ (10, HUFFMAN_EMIT_SYMBOL, 217),
+ (15, HUFFMAN_EMIT_SYMBOL, 217),
+ (24, HUFFMAN_EMIT_SYMBOL, 217),
+ (31, HUFFMAN_EMIT_SYMBOL, 217),
+ (41, HUFFMAN_EMIT_SYMBOL, 217),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 217),
+
+ # Node 118
+ (3, HUFFMAN_EMIT_SYMBOL, 227),
+ (6, HUFFMAN_EMIT_SYMBOL, 227),
+ (10, HUFFMAN_EMIT_SYMBOL, 227),
+ (15, HUFFMAN_EMIT_SYMBOL, 227),
+ (24, HUFFMAN_EMIT_SYMBOL, 227),
+ (31, HUFFMAN_EMIT_SYMBOL, 227),
+ (41, HUFFMAN_EMIT_SYMBOL, 227),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 227),
+ (3, HUFFMAN_EMIT_SYMBOL, 229),
+ (6, HUFFMAN_EMIT_SYMBOL, 229),
+ (10, HUFFMAN_EMIT_SYMBOL, 229),
+ (15, HUFFMAN_EMIT_SYMBOL, 229),
+ (24, HUFFMAN_EMIT_SYMBOL, 229),
+ (31, HUFFMAN_EMIT_SYMBOL, 229),
+ (41, HUFFMAN_EMIT_SYMBOL, 229),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 229),
+
+ # Node 119
+ (1, HUFFMAN_EMIT_SYMBOL, 230),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 230),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 129),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 132),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 133),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 134),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 136),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 146),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 154),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 156),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 160),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 163),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 164),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 169),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 170),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 173),
+
+ # Node 120
+ (2, HUFFMAN_EMIT_SYMBOL, 230),
+ (9, HUFFMAN_EMIT_SYMBOL, 230),
+ (23, HUFFMAN_EMIT_SYMBOL, 230),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 230),
+ (1, HUFFMAN_EMIT_SYMBOL, 129),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 129),
+ (1, HUFFMAN_EMIT_SYMBOL, 132),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 132),
+ (1, HUFFMAN_EMIT_SYMBOL, 133),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 133),
+ (1, HUFFMAN_EMIT_SYMBOL, 134),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 134),
+ (1, HUFFMAN_EMIT_SYMBOL, 136),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 136),
+ (1, HUFFMAN_EMIT_SYMBOL, 146),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 146),
+
+ # Node 121
+ (3, HUFFMAN_EMIT_SYMBOL, 230),
+ (6, HUFFMAN_EMIT_SYMBOL, 230),
+ (10, HUFFMAN_EMIT_SYMBOL, 230),
+ (15, HUFFMAN_EMIT_SYMBOL, 230),
+ (24, HUFFMAN_EMIT_SYMBOL, 230),
+ (31, HUFFMAN_EMIT_SYMBOL, 230),
+ (41, HUFFMAN_EMIT_SYMBOL, 230),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 230),
+ (2, HUFFMAN_EMIT_SYMBOL, 129),
+ (9, HUFFMAN_EMIT_SYMBOL, 129),
+ (23, HUFFMAN_EMIT_SYMBOL, 129),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 129),
+ (2, HUFFMAN_EMIT_SYMBOL, 132),
+ (9, HUFFMAN_EMIT_SYMBOL, 132),
+ (23, HUFFMAN_EMIT_SYMBOL, 132),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 132),
+
+ # Node 122
+ (3, HUFFMAN_EMIT_SYMBOL, 129),
+ (6, HUFFMAN_EMIT_SYMBOL, 129),
+ (10, HUFFMAN_EMIT_SYMBOL, 129),
+ (15, HUFFMAN_EMIT_SYMBOL, 129),
+ (24, HUFFMAN_EMIT_SYMBOL, 129),
+ (31, HUFFMAN_EMIT_SYMBOL, 129),
+ (41, HUFFMAN_EMIT_SYMBOL, 129),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 129),
+ (3, HUFFMAN_EMIT_SYMBOL, 132),
+ (6, HUFFMAN_EMIT_SYMBOL, 132),
+ (10, HUFFMAN_EMIT_SYMBOL, 132),
+ (15, HUFFMAN_EMIT_SYMBOL, 132),
+ (24, HUFFMAN_EMIT_SYMBOL, 132),
+ (31, HUFFMAN_EMIT_SYMBOL, 132),
+ (41, HUFFMAN_EMIT_SYMBOL, 132),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 132),
+
+ # Node 123
+ (2, HUFFMAN_EMIT_SYMBOL, 133),
+ (9, HUFFMAN_EMIT_SYMBOL, 133),
+ (23, HUFFMAN_EMIT_SYMBOL, 133),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 133),
+ (2, HUFFMAN_EMIT_SYMBOL, 134),
+ (9, HUFFMAN_EMIT_SYMBOL, 134),
+ (23, HUFFMAN_EMIT_SYMBOL, 134),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 134),
+ (2, HUFFMAN_EMIT_SYMBOL, 136),
+ (9, HUFFMAN_EMIT_SYMBOL, 136),
+ (23, HUFFMAN_EMIT_SYMBOL, 136),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 136),
+ (2, HUFFMAN_EMIT_SYMBOL, 146),
+ (9, HUFFMAN_EMIT_SYMBOL, 146),
+ (23, HUFFMAN_EMIT_SYMBOL, 146),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 146),
+
+ # Node 124
+ (3, HUFFMAN_EMIT_SYMBOL, 133),
+ (6, HUFFMAN_EMIT_SYMBOL, 133),
+ (10, HUFFMAN_EMIT_SYMBOL, 133),
+ (15, HUFFMAN_EMIT_SYMBOL, 133),
+ (24, HUFFMAN_EMIT_SYMBOL, 133),
+ (31, HUFFMAN_EMIT_SYMBOL, 133),
+ (41, HUFFMAN_EMIT_SYMBOL, 133),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 133),
+ (3, HUFFMAN_EMIT_SYMBOL, 134),
+ (6, HUFFMAN_EMIT_SYMBOL, 134),
+ (10, HUFFMAN_EMIT_SYMBOL, 134),
+ (15, HUFFMAN_EMIT_SYMBOL, 134),
+ (24, HUFFMAN_EMIT_SYMBOL, 134),
+ (31, HUFFMAN_EMIT_SYMBOL, 134),
+ (41, HUFFMAN_EMIT_SYMBOL, 134),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 134),
+
+ # Node 125
+ (3, HUFFMAN_EMIT_SYMBOL, 136),
+ (6, HUFFMAN_EMIT_SYMBOL, 136),
+ (10, HUFFMAN_EMIT_SYMBOL, 136),
+ (15, HUFFMAN_EMIT_SYMBOL, 136),
+ (24, HUFFMAN_EMIT_SYMBOL, 136),
+ (31, HUFFMAN_EMIT_SYMBOL, 136),
+ (41, HUFFMAN_EMIT_SYMBOL, 136),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 136),
+ (3, HUFFMAN_EMIT_SYMBOL, 146),
+ (6, HUFFMAN_EMIT_SYMBOL, 146),
+ (10, HUFFMAN_EMIT_SYMBOL, 146),
+ (15, HUFFMAN_EMIT_SYMBOL, 146),
+ (24, HUFFMAN_EMIT_SYMBOL, 146),
+ (31, HUFFMAN_EMIT_SYMBOL, 146),
+ (41, HUFFMAN_EMIT_SYMBOL, 146),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 146),
+
+ # Node 126
+ (1, HUFFMAN_EMIT_SYMBOL, 154),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 154),
+ (1, HUFFMAN_EMIT_SYMBOL, 156),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 156),
+ (1, HUFFMAN_EMIT_SYMBOL, 160),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 160),
+ (1, HUFFMAN_EMIT_SYMBOL, 163),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 163),
+ (1, HUFFMAN_EMIT_SYMBOL, 164),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 164),
+ (1, HUFFMAN_EMIT_SYMBOL, 169),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 169),
+ (1, HUFFMAN_EMIT_SYMBOL, 170),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 170),
+ (1, HUFFMAN_EMIT_SYMBOL, 173),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 173),
+
+ # Node 127
+ (2, HUFFMAN_EMIT_SYMBOL, 154),
+ (9, HUFFMAN_EMIT_SYMBOL, 154),
+ (23, HUFFMAN_EMIT_SYMBOL, 154),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 154),
+ (2, HUFFMAN_EMIT_SYMBOL, 156),
+ (9, HUFFMAN_EMIT_SYMBOL, 156),
+ (23, HUFFMAN_EMIT_SYMBOL, 156),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 156),
+ (2, HUFFMAN_EMIT_SYMBOL, 160),
+ (9, HUFFMAN_EMIT_SYMBOL, 160),
+ (23, HUFFMAN_EMIT_SYMBOL, 160),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 160),
+ (2, HUFFMAN_EMIT_SYMBOL, 163),
+ (9, HUFFMAN_EMIT_SYMBOL, 163),
+ (23, HUFFMAN_EMIT_SYMBOL, 163),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 163),
+
+ # Node 128
+ (3, HUFFMAN_EMIT_SYMBOL, 154),
+ (6, HUFFMAN_EMIT_SYMBOL, 154),
+ (10, HUFFMAN_EMIT_SYMBOL, 154),
+ (15, HUFFMAN_EMIT_SYMBOL, 154),
+ (24, HUFFMAN_EMIT_SYMBOL, 154),
+ (31, HUFFMAN_EMIT_SYMBOL, 154),
+ (41, HUFFMAN_EMIT_SYMBOL, 154),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 154),
+ (3, HUFFMAN_EMIT_SYMBOL, 156),
+ (6, HUFFMAN_EMIT_SYMBOL, 156),
+ (10, HUFFMAN_EMIT_SYMBOL, 156),
+ (15, HUFFMAN_EMIT_SYMBOL, 156),
+ (24, HUFFMAN_EMIT_SYMBOL, 156),
+ (31, HUFFMAN_EMIT_SYMBOL, 156),
+ (41, HUFFMAN_EMIT_SYMBOL, 156),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 156),
+
+ # Node 129
+ (3, HUFFMAN_EMIT_SYMBOL, 160),
+ (6, HUFFMAN_EMIT_SYMBOL, 160),
+ (10, HUFFMAN_EMIT_SYMBOL, 160),
+ (15, HUFFMAN_EMIT_SYMBOL, 160),
+ (24, HUFFMAN_EMIT_SYMBOL, 160),
+ (31, HUFFMAN_EMIT_SYMBOL, 160),
+ (41, HUFFMAN_EMIT_SYMBOL, 160),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 160),
+ (3, HUFFMAN_EMIT_SYMBOL, 163),
+ (6, HUFFMAN_EMIT_SYMBOL, 163),
+ (10, HUFFMAN_EMIT_SYMBOL, 163),
+ (15, HUFFMAN_EMIT_SYMBOL, 163),
+ (24, HUFFMAN_EMIT_SYMBOL, 163),
+ (31, HUFFMAN_EMIT_SYMBOL, 163),
+ (41, HUFFMAN_EMIT_SYMBOL, 163),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 163),
+
+ # Node 130
+ (2, HUFFMAN_EMIT_SYMBOL, 164),
+ (9, HUFFMAN_EMIT_SYMBOL, 164),
+ (23, HUFFMAN_EMIT_SYMBOL, 164),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 164),
+ (2, HUFFMAN_EMIT_SYMBOL, 169),
+ (9, HUFFMAN_EMIT_SYMBOL, 169),
+ (23, HUFFMAN_EMIT_SYMBOL, 169),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 169),
+ (2, HUFFMAN_EMIT_SYMBOL, 170),
+ (9, HUFFMAN_EMIT_SYMBOL, 170),
+ (23, HUFFMAN_EMIT_SYMBOL, 170),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 170),
+ (2, HUFFMAN_EMIT_SYMBOL, 173),
+ (9, HUFFMAN_EMIT_SYMBOL, 173),
+ (23, HUFFMAN_EMIT_SYMBOL, 173),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 173),
+
+ # Node 131
+ (3, HUFFMAN_EMIT_SYMBOL, 164),
+ (6, HUFFMAN_EMIT_SYMBOL, 164),
+ (10, HUFFMAN_EMIT_SYMBOL, 164),
+ (15, HUFFMAN_EMIT_SYMBOL, 164),
+ (24, HUFFMAN_EMIT_SYMBOL, 164),
+ (31, HUFFMAN_EMIT_SYMBOL, 164),
+ (41, HUFFMAN_EMIT_SYMBOL, 164),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 164),
+ (3, HUFFMAN_EMIT_SYMBOL, 169),
+ (6, HUFFMAN_EMIT_SYMBOL, 169),
+ (10, HUFFMAN_EMIT_SYMBOL, 169),
+ (15, HUFFMAN_EMIT_SYMBOL, 169),
+ (24, HUFFMAN_EMIT_SYMBOL, 169),
+ (31, HUFFMAN_EMIT_SYMBOL, 169),
+ (41, HUFFMAN_EMIT_SYMBOL, 169),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 169),
+
+ # Node 132
+ (3, HUFFMAN_EMIT_SYMBOL, 170),
+ (6, HUFFMAN_EMIT_SYMBOL, 170),
+ (10, HUFFMAN_EMIT_SYMBOL, 170),
+ (15, HUFFMAN_EMIT_SYMBOL, 170),
+ (24, HUFFMAN_EMIT_SYMBOL, 170),
+ (31, HUFFMAN_EMIT_SYMBOL, 170),
+ (41, HUFFMAN_EMIT_SYMBOL, 170),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 170),
+ (3, HUFFMAN_EMIT_SYMBOL, 173),
+ (6, HUFFMAN_EMIT_SYMBOL, 173),
+ (10, HUFFMAN_EMIT_SYMBOL, 173),
+ (15, HUFFMAN_EMIT_SYMBOL, 173),
+ (24, HUFFMAN_EMIT_SYMBOL, 173),
+ (31, HUFFMAN_EMIT_SYMBOL, 173),
+ (41, HUFFMAN_EMIT_SYMBOL, 173),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 173),
+
+ # Node 133
+ (137, 0, 0),
+ (138, 0, 0),
+ (140, 0, 0),
+ (141, 0, 0),
+ (144, 0, 0),
+ (145, 0, 0),
+ (147, 0, 0),
+ (150, 0, 0),
+ (156, 0, 0),
+ (159, 0, 0),
+ (163, 0, 0),
+ (166, 0, 0),
+ (171, 0, 0),
+ (174, 0, 0),
+ (181, 0, 0),
+ (190, 0, 0),
+
+ # Node 134
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 178),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 181),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 185),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 186),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 187),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 189),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 190),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 196),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 198),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 228),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 232),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 233),
+ (148, 0, 0),
+ (149, 0, 0),
+ (151, 0, 0),
+ (152, 0, 0),
+
+ # Node 135
+ (1, HUFFMAN_EMIT_SYMBOL, 178),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 178),
+ (1, HUFFMAN_EMIT_SYMBOL, 181),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 181),
+ (1, HUFFMAN_EMIT_SYMBOL, 185),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 185),
+ (1, HUFFMAN_EMIT_SYMBOL, 186),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 186),
+ (1, HUFFMAN_EMIT_SYMBOL, 187),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 187),
+ (1, HUFFMAN_EMIT_SYMBOL, 189),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 189),
+ (1, HUFFMAN_EMIT_SYMBOL, 190),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 190),
+ (1, HUFFMAN_EMIT_SYMBOL, 196),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 196),
+
+ # Node 136
+ (2, HUFFMAN_EMIT_SYMBOL, 178),
+ (9, HUFFMAN_EMIT_SYMBOL, 178),
+ (23, HUFFMAN_EMIT_SYMBOL, 178),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 178),
+ (2, HUFFMAN_EMIT_SYMBOL, 181),
+ (9, HUFFMAN_EMIT_SYMBOL, 181),
+ (23, HUFFMAN_EMIT_SYMBOL, 181),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 181),
+ (2, HUFFMAN_EMIT_SYMBOL, 185),
+ (9, HUFFMAN_EMIT_SYMBOL, 185),
+ (23, HUFFMAN_EMIT_SYMBOL, 185),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 185),
+ (2, HUFFMAN_EMIT_SYMBOL, 186),
+ (9, HUFFMAN_EMIT_SYMBOL, 186),
+ (23, HUFFMAN_EMIT_SYMBOL, 186),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 186),
+
+ # Node 137
+ (3, HUFFMAN_EMIT_SYMBOL, 178),
+ (6, HUFFMAN_EMIT_SYMBOL, 178),
+ (10, HUFFMAN_EMIT_SYMBOL, 178),
+ (15, HUFFMAN_EMIT_SYMBOL, 178),
+ (24, HUFFMAN_EMIT_SYMBOL, 178),
+ (31, HUFFMAN_EMIT_SYMBOL, 178),
+ (41, HUFFMAN_EMIT_SYMBOL, 178),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 178),
+ (3, HUFFMAN_EMIT_SYMBOL, 181),
+ (6, HUFFMAN_EMIT_SYMBOL, 181),
+ (10, HUFFMAN_EMIT_SYMBOL, 181),
+ (15, HUFFMAN_EMIT_SYMBOL, 181),
+ (24, HUFFMAN_EMIT_SYMBOL, 181),
+ (31, HUFFMAN_EMIT_SYMBOL, 181),
+ (41, HUFFMAN_EMIT_SYMBOL, 181),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 181),
+
+ # Node 138
+ (3, HUFFMAN_EMIT_SYMBOL, 185),
+ (6, HUFFMAN_EMIT_SYMBOL, 185),
+ (10, HUFFMAN_EMIT_SYMBOL, 185),
+ (15, HUFFMAN_EMIT_SYMBOL, 185),
+ (24, HUFFMAN_EMIT_SYMBOL, 185),
+ (31, HUFFMAN_EMIT_SYMBOL, 185),
+ (41, HUFFMAN_EMIT_SYMBOL, 185),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 185),
+ (3, HUFFMAN_EMIT_SYMBOL, 186),
+ (6, HUFFMAN_EMIT_SYMBOL, 186),
+ (10, HUFFMAN_EMIT_SYMBOL, 186),
+ (15, HUFFMAN_EMIT_SYMBOL, 186),
+ (24, HUFFMAN_EMIT_SYMBOL, 186),
+ (31, HUFFMAN_EMIT_SYMBOL, 186),
+ (41, HUFFMAN_EMIT_SYMBOL, 186),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 186),
+
+ # Node 139
+ (2, HUFFMAN_EMIT_SYMBOL, 187),
+ (9, HUFFMAN_EMIT_SYMBOL, 187),
+ (23, HUFFMAN_EMIT_SYMBOL, 187),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 187),
+ (2, HUFFMAN_EMIT_SYMBOL, 189),
+ (9, HUFFMAN_EMIT_SYMBOL, 189),
+ (23, HUFFMAN_EMIT_SYMBOL, 189),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 189),
+ (2, HUFFMAN_EMIT_SYMBOL, 190),
+ (9, HUFFMAN_EMIT_SYMBOL, 190),
+ (23, HUFFMAN_EMIT_SYMBOL, 190),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 190),
+ (2, HUFFMAN_EMIT_SYMBOL, 196),
+ (9, HUFFMAN_EMIT_SYMBOL, 196),
+ (23, HUFFMAN_EMIT_SYMBOL, 196),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 196),
+
+ # Node 140
+ (3, HUFFMAN_EMIT_SYMBOL, 187),
+ (6, HUFFMAN_EMIT_SYMBOL, 187),
+ (10, HUFFMAN_EMIT_SYMBOL, 187),
+ (15, HUFFMAN_EMIT_SYMBOL, 187),
+ (24, HUFFMAN_EMIT_SYMBOL, 187),
+ (31, HUFFMAN_EMIT_SYMBOL, 187),
+ (41, HUFFMAN_EMIT_SYMBOL, 187),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 187),
+ (3, HUFFMAN_EMIT_SYMBOL, 189),
+ (6, HUFFMAN_EMIT_SYMBOL, 189),
+ (10, HUFFMAN_EMIT_SYMBOL, 189),
+ (15, HUFFMAN_EMIT_SYMBOL, 189),
+ (24, HUFFMAN_EMIT_SYMBOL, 189),
+ (31, HUFFMAN_EMIT_SYMBOL, 189),
+ (41, HUFFMAN_EMIT_SYMBOL, 189),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 189),
+
+ # Node 141
+ (3, HUFFMAN_EMIT_SYMBOL, 190),
+ (6, HUFFMAN_EMIT_SYMBOL, 190),
+ (10, HUFFMAN_EMIT_SYMBOL, 190),
+ (15, HUFFMAN_EMIT_SYMBOL, 190),
+ (24, HUFFMAN_EMIT_SYMBOL, 190),
+ (31, HUFFMAN_EMIT_SYMBOL, 190),
+ (41, HUFFMAN_EMIT_SYMBOL, 190),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 190),
+ (3, HUFFMAN_EMIT_SYMBOL, 196),
+ (6, HUFFMAN_EMIT_SYMBOL, 196),
+ (10, HUFFMAN_EMIT_SYMBOL, 196),
+ (15, HUFFMAN_EMIT_SYMBOL, 196),
+ (24, HUFFMAN_EMIT_SYMBOL, 196),
+ (31, HUFFMAN_EMIT_SYMBOL, 196),
+ (41, HUFFMAN_EMIT_SYMBOL, 196),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 196),
+
+ # Node 142
+ (1, HUFFMAN_EMIT_SYMBOL, 198),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 198),
+ (1, HUFFMAN_EMIT_SYMBOL, 228),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 228),
+ (1, HUFFMAN_EMIT_SYMBOL, 232),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 232),
+ (1, HUFFMAN_EMIT_SYMBOL, 233),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 233),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 1),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 135),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 137),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 138),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 139),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 140),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 141),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 143),
+
+ # Node 143
+ (2, HUFFMAN_EMIT_SYMBOL, 198),
+ (9, HUFFMAN_EMIT_SYMBOL, 198),
+ (23, HUFFMAN_EMIT_SYMBOL, 198),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 198),
+ (2, HUFFMAN_EMIT_SYMBOL, 228),
+ (9, HUFFMAN_EMIT_SYMBOL, 228),
+ (23, HUFFMAN_EMIT_SYMBOL, 228),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 228),
+ (2, HUFFMAN_EMIT_SYMBOL, 232),
+ (9, HUFFMAN_EMIT_SYMBOL, 232),
+ (23, HUFFMAN_EMIT_SYMBOL, 232),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 232),
+ (2, HUFFMAN_EMIT_SYMBOL, 233),
+ (9, HUFFMAN_EMIT_SYMBOL, 233),
+ (23, HUFFMAN_EMIT_SYMBOL, 233),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 233),
+
+ # Node 144
+ (3, HUFFMAN_EMIT_SYMBOL, 198),
+ (6, HUFFMAN_EMIT_SYMBOL, 198),
+ (10, HUFFMAN_EMIT_SYMBOL, 198),
+ (15, HUFFMAN_EMIT_SYMBOL, 198),
+ (24, HUFFMAN_EMIT_SYMBOL, 198),
+ (31, HUFFMAN_EMIT_SYMBOL, 198),
+ (41, HUFFMAN_EMIT_SYMBOL, 198),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 198),
+ (3, HUFFMAN_EMIT_SYMBOL, 228),
+ (6, HUFFMAN_EMIT_SYMBOL, 228),
+ (10, HUFFMAN_EMIT_SYMBOL, 228),
+ (15, HUFFMAN_EMIT_SYMBOL, 228),
+ (24, HUFFMAN_EMIT_SYMBOL, 228),
+ (31, HUFFMAN_EMIT_SYMBOL, 228),
+ (41, HUFFMAN_EMIT_SYMBOL, 228),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 228),
+
+ # Node 145
+ (3, HUFFMAN_EMIT_SYMBOL, 232),
+ (6, HUFFMAN_EMIT_SYMBOL, 232),
+ (10, HUFFMAN_EMIT_SYMBOL, 232),
+ (15, HUFFMAN_EMIT_SYMBOL, 232),
+ (24, HUFFMAN_EMIT_SYMBOL, 232),
+ (31, HUFFMAN_EMIT_SYMBOL, 232),
+ (41, HUFFMAN_EMIT_SYMBOL, 232),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 232),
+ (3, HUFFMAN_EMIT_SYMBOL, 233),
+ (6, HUFFMAN_EMIT_SYMBOL, 233),
+ (10, HUFFMAN_EMIT_SYMBOL, 233),
+ (15, HUFFMAN_EMIT_SYMBOL, 233),
+ (24, HUFFMAN_EMIT_SYMBOL, 233),
+ (31, HUFFMAN_EMIT_SYMBOL, 233),
+ (41, HUFFMAN_EMIT_SYMBOL, 233),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 233),
+
+ # Node 146
+ (1, HUFFMAN_EMIT_SYMBOL, 1),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 1),
+ (1, HUFFMAN_EMIT_SYMBOL, 135),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 135),
+ (1, HUFFMAN_EMIT_SYMBOL, 137),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 137),
+ (1, HUFFMAN_EMIT_SYMBOL, 138),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 138),
+ (1, HUFFMAN_EMIT_SYMBOL, 139),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 139),
+ (1, HUFFMAN_EMIT_SYMBOL, 140),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 140),
+ (1, HUFFMAN_EMIT_SYMBOL, 141),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 141),
+ (1, HUFFMAN_EMIT_SYMBOL, 143),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 143),
+
+ # Node 147
+ (2, HUFFMAN_EMIT_SYMBOL, 1),
+ (9, HUFFMAN_EMIT_SYMBOL, 1),
+ (23, HUFFMAN_EMIT_SYMBOL, 1),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 1),
+ (2, HUFFMAN_EMIT_SYMBOL, 135),
+ (9, HUFFMAN_EMIT_SYMBOL, 135),
+ (23, HUFFMAN_EMIT_SYMBOL, 135),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 135),
+ (2, HUFFMAN_EMIT_SYMBOL, 137),
+ (9, HUFFMAN_EMIT_SYMBOL, 137),
+ (23, HUFFMAN_EMIT_SYMBOL, 137),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 137),
+ (2, HUFFMAN_EMIT_SYMBOL, 138),
+ (9, HUFFMAN_EMIT_SYMBOL, 138),
+ (23, HUFFMAN_EMIT_SYMBOL, 138),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 138),
+
+ # Node 148
+ (3, HUFFMAN_EMIT_SYMBOL, 1),
+ (6, HUFFMAN_EMIT_SYMBOL, 1),
+ (10, HUFFMAN_EMIT_SYMBOL, 1),
+ (15, HUFFMAN_EMIT_SYMBOL, 1),
+ (24, HUFFMAN_EMIT_SYMBOL, 1),
+ (31, HUFFMAN_EMIT_SYMBOL, 1),
+ (41, HUFFMAN_EMIT_SYMBOL, 1),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 1),
+ (3, HUFFMAN_EMIT_SYMBOL, 135),
+ (6, HUFFMAN_EMIT_SYMBOL, 135),
+ (10, HUFFMAN_EMIT_SYMBOL, 135),
+ (15, HUFFMAN_EMIT_SYMBOL, 135),
+ (24, HUFFMAN_EMIT_SYMBOL, 135),
+ (31, HUFFMAN_EMIT_SYMBOL, 135),
+ (41, HUFFMAN_EMIT_SYMBOL, 135),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 135),
+
+ # Node 149
+ (3, HUFFMAN_EMIT_SYMBOL, 137),
+ (6, HUFFMAN_EMIT_SYMBOL, 137),
+ (10, HUFFMAN_EMIT_SYMBOL, 137),
+ (15, HUFFMAN_EMIT_SYMBOL, 137),
+ (24, HUFFMAN_EMIT_SYMBOL, 137),
+ (31, HUFFMAN_EMIT_SYMBOL, 137),
+ (41, HUFFMAN_EMIT_SYMBOL, 137),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 137),
+ (3, HUFFMAN_EMIT_SYMBOL, 138),
+ (6, HUFFMAN_EMIT_SYMBOL, 138),
+ (10, HUFFMAN_EMIT_SYMBOL, 138),
+ (15, HUFFMAN_EMIT_SYMBOL, 138),
+ (24, HUFFMAN_EMIT_SYMBOL, 138),
+ (31, HUFFMAN_EMIT_SYMBOL, 138),
+ (41, HUFFMAN_EMIT_SYMBOL, 138),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 138),
+
+ # Node 150
+ (2, HUFFMAN_EMIT_SYMBOL, 139),
+ (9, HUFFMAN_EMIT_SYMBOL, 139),
+ (23, HUFFMAN_EMIT_SYMBOL, 139),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 139),
+ (2, HUFFMAN_EMIT_SYMBOL, 140),
+ (9, HUFFMAN_EMIT_SYMBOL, 140),
+ (23, HUFFMAN_EMIT_SYMBOL, 140),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 140),
+ (2, HUFFMAN_EMIT_SYMBOL, 141),
+ (9, HUFFMAN_EMIT_SYMBOL, 141),
+ (23, HUFFMAN_EMIT_SYMBOL, 141),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 141),
+ (2, HUFFMAN_EMIT_SYMBOL, 143),
+ (9, HUFFMAN_EMIT_SYMBOL, 143),
+ (23, HUFFMAN_EMIT_SYMBOL, 143),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 143),
+
+ # Node 151
+ (3, HUFFMAN_EMIT_SYMBOL, 139),
+ (6, HUFFMAN_EMIT_SYMBOL, 139),
+ (10, HUFFMAN_EMIT_SYMBOL, 139),
+ (15, HUFFMAN_EMIT_SYMBOL, 139),
+ (24, HUFFMAN_EMIT_SYMBOL, 139),
+ (31, HUFFMAN_EMIT_SYMBOL, 139),
+ (41, HUFFMAN_EMIT_SYMBOL, 139),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 139),
+ (3, HUFFMAN_EMIT_SYMBOL, 140),
+ (6, HUFFMAN_EMIT_SYMBOL, 140),
+ (10, HUFFMAN_EMIT_SYMBOL, 140),
+ (15, HUFFMAN_EMIT_SYMBOL, 140),
+ (24, HUFFMAN_EMIT_SYMBOL, 140),
+ (31, HUFFMAN_EMIT_SYMBOL, 140),
+ (41, HUFFMAN_EMIT_SYMBOL, 140),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 140),
+
+ # Node 152
+ (3, HUFFMAN_EMIT_SYMBOL, 141),
+ (6, HUFFMAN_EMIT_SYMBOL, 141),
+ (10, HUFFMAN_EMIT_SYMBOL, 141),
+ (15, HUFFMAN_EMIT_SYMBOL, 141),
+ (24, HUFFMAN_EMIT_SYMBOL, 141),
+ (31, HUFFMAN_EMIT_SYMBOL, 141),
+ (41, HUFFMAN_EMIT_SYMBOL, 141),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 141),
+ (3, HUFFMAN_EMIT_SYMBOL, 143),
+ (6, HUFFMAN_EMIT_SYMBOL, 143),
+ (10, HUFFMAN_EMIT_SYMBOL, 143),
+ (15, HUFFMAN_EMIT_SYMBOL, 143),
+ (24, HUFFMAN_EMIT_SYMBOL, 143),
+ (31, HUFFMAN_EMIT_SYMBOL, 143),
+ (41, HUFFMAN_EMIT_SYMBOL, 143),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 143),
+
+ # Node 153
+ (157, 0, 0),
+ (158, 0, 0),
+ (160, 0, 0),
+ (161, 0, 0),
+ (164, 0, 0),
+ (165, 0, 0),
+ (167, 0, 0),
+ (168, 0, 0),
+ (172, 0, 0),
+ (173, 0, 0),
+ (175, 0, 0),
+ (177, 0, 0),
+ (182, 0, 0),
+ (185, 0, 0),
+ (191, 0, 0),
+ (207, 0, 0),
+
+ # Node 154
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 147),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 149),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 150),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 151),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 152),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 155),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 157),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 158),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 165),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 166),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 168),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 174),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 175),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 180),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 182),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 183),
+
+ # Node 155
+ (1, HUFFMAN_EMIT_SYMBOL, 147),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 147),
+ (1, HUFFMAN_EMIT_SYMBOL, 149),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 149),
+ (1, HUFFMAN_EMIT_SYMBOL, 150),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 150),
+ (1, HUFFMAN_EMIT_SYMBOL, 151),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 151),
+ (1, HUFFMAN_EMIT_SYMBOL, 152),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 152),
+ (1, HUFFMAN_EMIT_SYMBOL, 155),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 155),
+ (1, HUFFMAN_EMIT_SYMBOL, 157),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 157),
+ (1, HUFFMAN_EMIT_SYMBOL, 158),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 158),
+
+ # Node 156
+ (2, HUFFMAN_EMIT_SYMBOL, 147),
+ (9, HUFFMAN_EMIT_SYMBOL, 147),
+ (23, HUFFMAN_EMIT_SYMBOL, 147),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 147),
+ (2, HUFFMAN_EMIT_SYMBOL, 149),
+ (9, HUFFMAN_EMIT_SYMBOL, 149),
+ (23, HUFFMAN_EMIT_SYMBOL, 149),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 149),
+ (2, HUFFMAN_EMIT_SYMBOL, 150),
+ (9, HUFFMAN_EMIT_SYMBOL, 150),
+ (23, HUFFMAN_EMIT_SYMBOL, 150),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 150),
+ (2, HUFFMAN_EMIT_SYMBOL, 151),
+ (9, HUFFMAN_EMIT_SYMBOL, 151),
+ (23, HUFFMAN_EMIT_SYMBOL, 151),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 151),
+
+ # Node 157
+ (3, HUFFMAN_EMIT_SYMBOL, 147),
+ (6, HUFFMAN_EMIT_SYMBOL, 147),
+ (10, HUFFMAN_EMIT_SYMBOL, 147),
+ (15, HUFFMAN_EMIT_SYMBOL, 147),
+ (24, HUFFMAN_EMIT_SYMBOL, 147),
+ (31, HUFFMAN_EMIT_SYMBOL, 147),
+ (41, HUFFMAN_EMIT_SYMBOL, 147),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 147),
+ (3, HUFFMAN_EMIT_SYMBOL, 149),
+ (6, HUFFMAN_EMIT_SYMBOL, 149),
+ (10, HUFFMAN_EMIT_SYMBOL, 149),
+ (15, HUFFMAN_EMIT_SYMBOL, 149),
+ (24, HUFFMAN_EMIT_SYMBOL, 149),
+ (31, HUFFMAN_EMIT_SYMBOL, 149),
+ (41, HUFFMAN_EMIT_SYMBOL, 149),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 149),
+
+ # Node 158
+ (3, HUFFMAN_EMIT_SYMBOL, 150),
+ (6, HUFFMAN_EMIT_SYMBOL, 150),
+ (10, HUFFMAN_EMIT_SYMBOL, 150),
+ (15, HUFFMAN_EMIT_SYMBOL, 150),
+ (24, HUFFMAN_EMIT_SYMBOL, 150),
+ (31, HUFFMAN_EMIT_SYMBOL, 150),
+ (41, HUFFMAN_EMIT_SYMBOL, 150),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 150),
+ (3, HUFFMAN_EMIT_SYMBOL, 151),
+ (6, HUFFMAN_EMIT_SYMBOL, 151),
+ (10, HUFFMAN_EMIT_SYMBOL, 151),
+ (15, HUFFMAN_EMIT_SYMBOL, 151),
+ (24, HUFFMAN_EMIT_SYMBOL, 151),
+ (31, HUFFMAN_EMIT_SYMBOL, 151),
+ (41, HUFFMAN_EMIT_SYMBOL, 151),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 151),
+
+ # Node 159
+ (2, HUFFMAN_EMIT_SYMBOL, 152),
+ (9, HUFFMAN_EMIT_SYMBOL, 152),
+ (23, HUFFMAN_EMIT_SYMBOL, 152),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 152),
+ (2, HUFFMAN_EMIT_SYMBOL, 155),
+ (9, HUFFMAN_EMIT_SYMBOL, 155),
+ (23, HUFFMAN_EMIT_SYMBOL, 155),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 155),
+ (2, HUFFMAN_EMIT_SYMBOL, 157),
+ (9, HUFFMAN_EMIT_SYMBOL, 157),
+ (23, HUFFMAN_EMIT_SYMBOL, 157),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 157),
+ (2, HUFFMAN_EMIT_SYMBOL, 158),
+ (9, HUFFMAN_EMIT_SYMBOL, 158),
+ (23, HUFFMAN_EMIT_SYMBOL, 158),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 158),
+
+ # Node 160
+ (3, HUFFMAN_EMIT_SYMBOL, 152),
+ (6, HUFFMAN_EMIT_SYMBOL, 152),
+ (10, HUFFMAN_EMIT_SYMBOL, 152),
+ (15, HUFFMAN_EMIT_SYMBOL, 152),
+ (24, HUFFMAN_EMIT_SYMBOL, 152),
+ (31, HUFFMAN_EMIT_SYMBOL, 152),
+ (41, HUFFMAN_EMIT_SYMBOL, 152),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 152),
+ (3, HUFFMAN_EMIT_SYMBOL, 155),
+ (6, HUFFMAN_EMIT_SYMBOL, 155),
+ (10, HUFFMAN_EMIT_SYMBOL, 155),
+ (15, HUFFMAN_EMIT_SYMBOL, 155),
+ (24, HUFFMAN_EMIT_SYMBOL, 155),
+ (31, HUFFMAN_EMIT_SYMBOL, 155),
+ (41, HUFFMAN_EMIT_SYMBOL, 155),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 155),
+
+ # Node 161
+ (3, HUFFMAN_EMIT_SYMBOL, 157),
+ (6, HUFFMAN_EMIT_SYMBOL, 157),
+ (10, HUFFMAN_EMIT_SYMBOL, 157),
+ (15, HUFFMAN_EMIT_SYMBOL, 157),
+ (24, HUFFMAN_EMIT_SYMBOL, 157),
+ (31, HUFFMAN_EMIT_SYMBOL, 157),
+ (41, HUFFMAN_EMIT_SYMBOL, 157),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 157),
+ (3, HUFFMAN_EMIT_SYMBOL, 158),
+ (6, HUFFMAN_EMIT_SYMBOL, 158),
+ (10, HUFFMAN_EMIT_SYMBOL, 158),
+ (15, HUFFMAN_EMIT_SYMBOL, 158),
+ (24, HUFFMAN_EMIT_SYMBOL, 158),
+ (31, HUFFMAN_EMIT_SYMBOL, 158),
+ (41, HUFFMAN_EMIT_SYMBOL, 158),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 158),
+
+ # Node 162
+ (1, HUFFMAN_EMIT_SYMBOL, 165),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 165),
+ (1, HUFFMAN_EMIT_SYMBOL, 166),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 166),
+ (1, HUFFMAN_EMIT_SYMBOL, 168),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 168),
+ (1, HUFFMAN_EMIT_SYMBOL, 174),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 174),
+ (1, HUFFMAN_EMIT_SYMBOL, 175),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 175),
+ (1, HUFFMAN_EMIT_SYMBOL, 180),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 180),
+ (1, HUFFMAN_EMIT_SYMBOL, 182),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 182),
+ (1, HUFFMAN_EMIT_SYMBOL, 183),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 183),
+
+ # Node 163
+ (2, HUFFMAN_EMIT_SYMBOL, 165),
+ (9, HUFFMAN_EMIT_SYMBOL, 165),
+ (23, HUFFMAN_EMIT_SYMBOL, 165),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 165),
+ (2, HUFFMAN_EMIT_SYMBOL, 166),
+ (9, HUFFMAN_EMIT_SYMBOL, 166),
+ (23, HUFFMAN_EMIT_SYMBOL, 166),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 166),
+ (2, HUFFMAN_EMIT_SYMBOL, 168),
+ (9, HUFFMAN_EMIT_SYMBOL, 168),
+ (23, HUFFMAN_EMIT_SYMBOL, 168),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 168),
+ (2, HUFFMAN_EMIT_SYMBOL, 174),
+ (9, HUFFMAN_EMIT_SYMBOL, 174),
+ (23, HUFFMAN_EMIT_SYMBOL, 174),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 174),
+
+ # Node 164
+ (3, HUFFMAN_EMIT_SYMBOL, 165),
+ (6, HUFFMAN_EMIT_SYMBOL, 165),
+ (10, HUFFMAN_EMIT_SYMBOL, 165),
+ (15, HUFFMAN_EMIT_SYMBOL, 165),
+ (24, HUFFMAN_EMIT_SYMBOL, 165),
+ (31, HUFFMAN_EMIT_SYMBOL, 165),
+ (41, HUFFMAN_EMIT_SYMBOL, 165),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 165),
+ (3, HUFFMAN_EMIT_SYMBOL, 166),
+ (6, HUFFMAN_EMIT_SYMBOL, 166),
+ (10, HUFFMAN_EMIT_SYMBOL, 166),
+ (15, HUFFMAN_EMIT_SYMBOL, 166),
+ (24, HUFFMAN_EMIT_SYMBOL, 166),
+ (31, HUFFMAN_EMIT_SYMBOL, 166),
+ (41, HUFFMAN_EMIT_SYMBOL, 166),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 166),
+
+ # Node 165
+ (3, HUFFMAN_EMIT_SYMBOL, 168),
+ (6, HUFFMAN_EMIT_SYMBOL, 168),
+ (10, HUFFMAN_EMIT_SYMBOL, 168),
+ (15, HUFFMAN_EMIT_SYMBOL, 168),
+ (24, HUFFMAN_EMIT_SYMBOL, 168),
+ (31, HUFFMAN_EMIT_SYMBOL, 168),
+ (41, HUFFMAN_EMIT_SYMBOL, 168),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 168),
+ (3, HUFFMAN_EMIT_SYMBOL, 174),
+ (6, HUFFMAN_EMIT_SYMBOL, 174),
+ (10, HUFFMAN_EMIT_SYMBOL, 174),
+ (15, HUFFMAN_EMIT_SYMBOL, 174),
+ (24, HUFFMAN_EMIT_SYMBOL, 174),
+ (31, HUFFMAN_EMIT_SYMBOL, 174),
+ (41, HUFFMAN_EMIT_SYMBOL, 174),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 174),
+
+ # Node 166
+ (2, HUFFMAN_EMIT_SYMBOL, 175),
+ (9, HUFFMAN_EMIT_SYMBOL, 175),
+ (23, HUFFMAN_EMIT_SYMBOL, 175),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 175),
+ (2, HUFFMAN_EMIT_SYMBOL, 180),
+ (9, HUFFMAN_EMIT_SYMBOL, 180),
+ (23, HUFFMAN_EMIT_SYMBOL, 180),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 180),
+ (2, HUFFMAN_EMIT_SYMBOL, 182),
+ (9, HUFFMAN_EMIT_SYMBOL, 182),
+ (23, HUFFMAN_EMIT_SYMBOL, 182),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 182),
+ (2, HUFFMAN_EMIT_SYMBOL, 183),
+ (9, HUFFMAN_EMIT_SYMBOL, 183),
+ (23, HUFFMAN_EMIT_SYMBOL, 183),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 183),
+
+ # Node 167
+ (3, HUFFMAN_EMIT_SYMBOL, 175),
+ (6, HUFFMAN_EMIT_SYMBOL, 175),
+ (10, HUFFMAN_EMIT_SYMBOL, 175),
+ (15, HUFFMAN_EMIT_SYMBOL, 175),
+ (24, HUFFMAN_EMIT_SYMBOL, 175),
+ (31, HUFFMAN_EMIT_SYMBOL, 175),
+ (41, HUFFMAN_EMIT_SYMBOL, 175),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 175),
+ (3, HUFFMAN_EMIT_SYMBOL, 180),
+ (6, HUFFMAN_EMIT_SYMBOL, 180),
+ (10, HUFFMAN_EMIT_SYMBOL, 180),
+ (15, HUFFMAN_EMIT_SYMBOL, 180),
+ (24, HUFFMAN_EMIT_SYMBOL, 180),
+ (31, HUFFMAN_EMIT_SYMBOL, 180),
+ (41, HUFFMAN_EMIT_SYMBOL, 180),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 180),
+
+ # Node 168
+ (3, HUFFMAN_EMIT_SYMBOL, 182),
+ (6, HUFFMAN_EMIT_SYMBOL, 182),
+ (10, HUFFMAN_EMIT_SYMBOL, 182),
+ (15, HUFFMAN_EMIT_SYMBOL, 182),
+ (24, HUFFMAN_EMIT_SYMBOL, 182),
+ (31, HUFFMAN_EMIT_SYMBOL, 182),
+ (41, HUFFMAN_EMIT_SYMBOL, 182),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 182),
+ (3, HUFFMAN_EMIT_SYMBOL, 183),
+ (6, HUFFMAN_EMIT_SYMBOL, 183),
+ (10, HUFFMAN_EMIT_SYMBOL, 183),
+ (15, HUFFMAN_EMIT_SYMBOL, 183),
+ (24, HUFFMAN_EMIT_SYMBOL, 183),
+ (31, HUFFMAN_EMIT_SYMBOL, 183),
+ (41, HUFFMAN_EMIT_SYMBOL, 183),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 183),
+
+ # Node 169
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 188),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 191),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 197),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 231),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 239),
+ (176, 0, 0),
+ (178, 0, 0),
+ (179, 0, 0),
+ (183, 0, 0),
+ (184, 0, 0),
+ (186, 0, 0),
+ (187, 0, 0),
+ (192, 0, 0),
+ (199, 0, 0),
+ (208, 0, 0),
+ (223, 0, 0),
+
+ # Node 170
+ (1, HUFFMAN_EMIT_SYMBOL, 188),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 188),
+ (1, HUFFMAN_EMIT_SYMBOL, 191),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 191),
+ (1, HUFFMAN_EMIT_SYMBOL, 197),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 197),
+ (1, HUFFMAN_EMIT_SYMBOL, 231),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 231),
+ (1, HUFFMAN_EMIT_SYMBOL, 239),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 239),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 9),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 142),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 144),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 145),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 148),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 159),
+
+ # Node 171
+ (2, HUFFMAN_EMIT_SYMBOL, 188),
+ (9, HUFFMAN_EMIT_SYMBOL, 188),
+ (23, HUFFMAN_EMIT_SYMBOL, 188),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 188),
+ (2, HUFFMAN_EMIT_SYMBOL, 191),
+ (9, HUFFMAN_EMIT_SYMBOL, 191),
+ (23, HUFFMAN_EMIT_SYMBOL, 191),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 191),
+ (2, HUFFMAN_EMIT_SYMBOL, 197),
+ (9, HUFFMAN_EMIT_SYMBOL, 197),
+ (23, HUFFMAN_EMIT_SYMBOL, 197),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 197),
+ (2, HUFFMAN_EMIT_SYMBOL, 231),
+ (9, HUFFMAN_EMIT_SYMBOL, 231),
+ (23, HUFFMAN_EMIT_SYMBOL, 231),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 231),
+
+ # Node 172
+ (3, HUFFMAN_EMIT_SYMBOL, 188),
+ (6, HUFFMAN_EMIT_SYMBOL, 188),
+ (10, HUFFMAN_EMIT_SYMBOL, 188),
+ (15, HUFFMAN_EMIT_SYMBOL, 188),
+ (24, HUFFMAN_EMIT_SYMBOL, 188),
+ (31, HUFFMAN_EMIT_SYMBOL, 188),
+ (41, HUFFMAN_EMIT_SYMBOL, 188),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 188),
+ (3, HUFFMAN_EMIT_SYMBOL, 191),
+ (6, HUFFMAN_EMIT_SYMBOL, 191),
+ (10, HUFFMAN_EMIT_SYMBOL, 191),
+ (15, HUFFMAN_EMIT_SYMBOL, 191),
+ (24, HUFFMAN_EMIT_SYMBOL, 191),
+ (31, HUFFMAN_EMIT_SYMBOL, 191),
+ (41, HUFFMAN_EMIT_SYMBOL, 191),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 191),
+
+ # Node 173
+ (3, HUFFMAN_EMIT_SYMBOL, 197),
+ (6, HUFFMAN_EMIT_SYMBOL, 197),
+ (10, HUFFMAN_EMIT_SYMBOL, 197),
+ (15, HUFFMAN_EMIT_SYMBOL, 197),
+ (24, HUFFMAN_EMIT_SYMBOL, 197),
+ (31, HUFFMAN_EMIT_SYMBOL, 197),
+ (41, HUFFMAN_EMIT_SYMBOL, 197),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 197),
+ (3, HUFFMAN_EMIT_SYMBOL, 231),
+ (6, HUFFMAN_EMIT_SYMBOL, 231),
+ (10, HUFFMAN_EMIT_SYMBOL, 231),
+ (15, HUFFMAN_EMIT_SYMBOL, 231),
+ (24, HUFFMAN_EMIT_SYMBOL, 231),
+ (31, HUFFMAN_EMIT_SYMBOL, 231),
+ (41, HUFFMAN_EMIT_SYMBOL, 231),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 231),
+
+ # Node 174
+ (2, HUFFMAN_EMIT_SYMBOL, 239),
+ (9, HUFFMAN_EMIT_SYMBOL, 239),
+ (23, HUFFMAN_EMIT_SYMBOL, 239),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 239),
+ (1, HUFFMAN_EMIT_SYMBOL, 9),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 9),
+ (1, HUFFMAN_EMIT_SYMBOL, 142),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 142),
+ (1, HUFFMAN_EMIT_SYMBOL, 144),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 144),
+ (1, HUFFMAN_EMIT_SYMBOL, 145),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 145),
+ (1, HUFFMAN_EMIT_SYMBOL, 148),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 148),
+ (1, HUFFMAN_EMIT_SYMBOL, 159),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 159),
+
+ # Node 175
+ (3, HUFFMAN_EMIT_SYMBOL, 239),
+ (6, HUFFMAN_EMIT_SYMBOL, 239),
+ (10, HUFFMAN_EMIT_SYMBOL, 239),
+ (15, HUFFMAN_EMIT_SYMBOL, 239),
+ (24, HUFFMAN_EMIT_SYMBOL, 239),
+ (31, HUFFMAN_EMIT_SYMBOL, 239),
+ (41, HUFFMAN_EMIT_SYMBOL, 239),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 239),
+ (2, HUFFMAN_EMIT_SYMBOL, 9),
+ (9, HUFFMAN_EMIT_SYMBOL, 9),
+ (23, HUFFMAN_EMIT_SYMBOL, 9),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 9),
+ (2, HUFFMAN_EMIT_SYMBOL, 142),
+ (9, HUFFMAN_EMIT_SYMBOL, 142),
+ (23, HUFFMAN_EMIT_SYMBOL, 142),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 142),
+
+ # Node 176
+ (3, HUFFMAN_EMIT_SYMBOL, 9),
+ (6, HUFFMAN_EMIT_SYMBOL, 9),
+ (10, HUFFMAN_EMIT_SYMBOL, 9),
+ (15, HUFFMAN_EMIT_SYMBOL, 9),
+ (24, HUFFMAN_EMIT_SYMBOL, 9),
+ (31, HUFFMAN_EMIT_SYMBOL, 9),
+ (41, HUFFMAN_EMIT_SYMBOL, 9),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 9),
+ (3, HUFFMAN_EMIT_SYMBOL, 142),
+ (6, HUFFMAN_EMIT_SYMBOL, 142),
+ (10, HUFFMAN_EMIT_SYMBOL, 142),
+ (15, HUFFMAN_EMIT_SYMBOL, 142),
+ (24, HUFFMAN_EMIT_SYMBOL, 142),
+ (31, HUFFMAN_EMIT_SYMBOL, 142),
+ (41, HUFFMAN_EMIT_SYMBOL, 142),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 142),
+
+ # Node 177
+ (2, HUFFMAN_EMIT_SYMBOL, 144),
+ (9, HUFFMAN_EMIT_SYMBOL, 144),
+ (23, HUFFMAN_EMIT_SYMBOL, 144),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 144),
+ (2, HUFFMAN_EMIT_SYMBOL, 145),
+ (9, HUFFMAN_EMIT_SYMBOL, 145),
+ (23, HUFFMAN_EMIT_SYMBOL, 145),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 145),
+ (2, HUFFMAN_EMIT_SYMBOL, 148),
+ (9, HUFFMAN_EMIT_SYMBOL, 148),
+ (23, HUFFMAN_EMIT_SYMBOL, 148),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 148),
+ (2, HUFFMAN_EMIT_SYMBOL, 159),
+ (9, HUFFMAN_EMIT_SYMBOL, 159),
+ (23, HUFFMAN_EMIT_SYMBOL, 159),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 159),
+
+ # Node 178
+ (3, HUFFMAN_EMIT_SYMBOL, 144),
+ (6, HUFFMAN_EMIT_SYMBOL, 144),
+ (10, HUFFMAN_EMIT_SYMBOL, 144),
+ (15, HUFFMAN_EMIT_SYMBOL, 144),
+ (24, HUFFMAN_EMIT_SYMBOL, 144),
+ (31, HUFFMAN_EMIT_SYMBOL, 144),
+ (41, HUFFMAN_EMIT_SYMBOL, 144),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 144),
+ (3, HUFFMAN_EMIT_SYMBOL, 145),
+ (6, HUFFMAN_EMIT_SYMBOL, 145),
+ (10, HUFFMAN_EMIT_SYMBOL, 145),
+ (15, HUFFMAN_EMIT_SYMBOL, 145),
+ (24, HUFFMAN_EMIT_SYMBOL, 145),
+ (31, HUFFMAN_EMIT_SYMBOL, 145),
+ (41, HUFFMAN_EMIT_SYMBOL, 145),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 145),
+
+ # Node 179
+ (3, HUFFMAN_EMIT_SYMBOL, 148),
+ (6, HUFFMAN_EMIT_SYMBOL, 148),
+ (10, HUFFMAN_EMIT_SYMBOL, 148),
+ (15, HUFFMAN_EMIT_SYMBOL, 148),
+ (24, HUFFMAN_EMIT_SYMBOL, 148),
+ (31, HUFFMAN_EMIT_SYMBOL, 148),
+ (41, HUFFMAN_EMIT_SYMBOL, 148),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 148),
+ (3, HUFFMAN_EMIT_SYMBOL, 159),
+ (6, HUFFMAN_EMIT_SYMBOL, 159),
+ (10, HUFFMAN_EMIT_SYMBOL, 159),
+ (15, HUFFMAN_EMIT_SYMBOL, 159),
+ (24, HUFFMAN_EMIT_SYMBOL, 159),
+ (31, HUFFMAN_EMIT_SYMBOL, 159),
+ (41, HUFFMAN_EMIT_SYMBOL, 159),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 159),
+
+ # Node 180
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 171),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 206),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 215),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 225),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 236),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 237),
+ (188, 0, 0),
+ (189, 0, 0),
+ (193, 0, 0),
+ (196, 0, 0),
+ (200, 0, 0),
+ (203, 0, 0),
+ (209, 0, 0),
+ (216, 0, 0),
+ (224, 0, 0),
+ (238, 0, 0),
+
+ # Node 181
+ (1, HUFFMAN_EMIT_SYMBOL, 171),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 171),
+ (1, HUFFMAN_EMIT_SYMBOL, 206),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 206),
+ (1, HUFFMAN_EMIT_SYMBOL, 215),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 215),
+ (1, HUFFMAN_EMIT_SYMBOL, 225),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 225),
+ (1, HUFFMAN_EMIT_SYMBOL, 236),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 236),
+ (1, HUFFMAN_EMIT_SYMBOL, 237),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 237),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 199),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 207),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 234),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 235),
+
+ # Node 182
+ (2, HUFFMAN_EMIT_SYMBOL, 171),
+ (9, HUFFMAN_EMIT_SYMBOL, 171),
+ (23, HUFFMAN_EMIT_SYMBOL, 171),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 171),
+ (2, HUFFMAN_EMIT_SYMBOL, 206),
+ (9, HUFFMAN_EMIT_SYMBOL, 206),
+ (23, HUFFMAN_EMIT_SYMBOL, 206),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 206),
+ (2, HUFFMAN_EMIT_SYMBOL, 215),
+ (9, HUFFMAN_EMIT_SYMBOL, 215),
+ (23, HUFFMAN_EMIT_SYMBOL, 215),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 215),
+ (2, HUFFMAN_EMIT_SYMBOL, 225),
+ (9, HUFFMAN_EMIT_SYMBOL, 225),
+ (23, HUFFMAN_EMIT_SYMBOL, 225),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 225),
+
+ # Node 183
+ (3, HUFFMAN_EMIT_SYMBOL, 171),
+ (6, HUFFMAN_EMIT_SYMBOL, 171),
+ (10, HUFFMAN_EMIT_SYMBOL, 171),
+ (15, HUFFMAN_EMIT_SYMBOL, 171),
+ (24, HUFFMAN_EMIT_SYMBOL, 171),
+ (31, HUFFMAN_EMIT_SYMBOL, 171),
+ (41, HUFFMAN_EMIT_SYMBOL, 171),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 171),
+ (3, HUFFMAN_EMIT_SYMBOL, 206),
+ (6, HUFFMAN_EMIT_SYMBOL, 206),
+ (10, HUFFMAN_EMIT_SYMBOL, 206),
+ (15, HUFFMAN_EMIT_SYMBOL, 206),
+ (24, HUFFMAN_EMIT_SYMBOL, 206),
+ (31, HUFFMAN_EMIT_SYMBOL, 206),
+ (41, HUFFMAN_EMIT_SYMBOL, 206),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 206),
+
+ # Node 184
+ (3, HUFFMAN_EMIT_SYMBOL, 215),
+ (6, HUFFMAN_EMIT_SYMBOL, 215),
+ (10, HUFFMAN_EMIT_SYMBOL, 215),
+ (15, HUFFMAN_EMIT_SYMBOL, 215),
+ (24, HUFFMAN_EMIT_SYMBOL, 215),
+ (31, HUFFMAN_EMIT_SYMBOL, 215),
+ (41, HUFFMAN_EMIT_SYMBOL, 215),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 215),
+ (3, HUFFMAN_EMIT_SYMBOL, 225),
+ (6, HUFFMAN_EMIT_SYMBOL, 225),
+ (10, HUFFMAN_EMIT_SYMBOL, 225),
+ (15, HUFFMAN_EMIT_SYMBOL, 225),
+ (24, HUFFMAN_EMIT_SYMBOL, 225),
+ (31, HUFFMAN_EMIT_SYMBOL, 225),
+ (41, HUFFMAN_EMIT_SYMBOL, 225),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 225),
+
+ # Node 185
+ (2, HUFFMAN_EMIT_SYMBOL, 236),
+ (9, HUFFMAN_EMIT_SYMBOL, 236),
+ (23, HUFFMAN_EMIT_SYMBOL, 236),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 236),
+ (2, HUFFMAN_EMIT_SYMBOL, 237),
+ (9, HUFFMAN_EMIT_SYMBOL, 237),
+ (23, HUFFMAN_EMIT_SYMBOL, 237),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 237),
+ (1, HUFFMAN_EMIT_SYMBOL, 199),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 199),
+ (1, HUFFMAN_EMIT_SYMBOL, 207),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 207),
+ (1, HUFFMAN_EMIT_SYMBOL, 234),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 234),
+ (1, HUFFMAN_EMIT_SYMBOL, 235),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 235),
+
+ # Node 186
+ (3, HUFFMAN_EMIT_SYMBOL, 236),
+ (6, HUFFMAN_EMIT_SYMBOL, 236),
+ (10, HUFFMAN_EMIT_SYMBOL, 236),
+ (15, HUFFMAN_EMIT_SYMBOL, 236),
+ (24, HUFFMAN_EMIT_SYMBOL, 236),
+ (31, HUFFMAN_EMIT_SYMBOL, 236),
+ (41, HUFFMAN_EMIT_SYMBOL, 236),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 236),
+ (3, HUFFMAN_EMIT_SYMBOL, 237),
+ (6, HUFFMAN_EMIT_SYMBOL, 237),
+ (10, HUFFMAN_EMIT_SYMBOL, 237),
+ (15, HUFFMAN_EMIT_SYMBOL, 237),
+ (24, HUFFMAN_EMIT_SYMBOL, 237),
+ (31, HUFFMAN_EMIT_SYMBOL, 237),
+ (41, HUFFMAN_EMIT_SYMBOL, 237),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 237),
+
+ # Node 187
+ (2, HUFFMAN_EMIT_SYMBOL, 199),
+ (9, HUFFMAN_EMIT_SYMBOL, 199),
+ (23, HUFFMAN_EMIT_SYMBOL, 199),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 199),
+ (2, HUFFMAN_EMIT_SYMBOL, 207),
+ (9, HUFFMAN_EMIT_SYMBOL, 207),
+ (23, HUFFMAN_EMIT_SYMBOL, 207),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 207),
+ (2, HUFFMAN_EMIT_SYMBOL, 234),
+ (9, HUFFMAN_EMIT_SYMBOL, 234),
+ (23, HUFFMAN_EMIT_SYMBOL, 234),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 234),
+ (2, HUFFMAN_EMIT_SYMBOL, 235),
+ (9, HUFFMAN_EMIT_SYMBOL, 235),
+ (23, HUFFMAN_EMIT_SYMBOL, 235),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 235),
+
+ # Node 188
+ (3, HUFFMAN_EMIT_SYMBOL, 199),
+ (6, HUFFMAN_EMIT_SYMBOL, 199),
+ (10, HUFFMAN_EMIT_SYMBOL, 199),
+ (15, HUFFMAN_EMIT_SYMBOL, 199),
+ (24, HUFFMAN_EMIT_SYMBOL, 199),
+ (31, HUFFMAN_EMIT_SYMBOL, 199),
+ (41, HUFFMAN_EMIT_SYMBOL, 199),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 199),
+ (3, HUFFMAN_EMIT_SYMBOL, 207),
+ (6, HUFFMAN_EMIT_SYMBOL, 207),
+ (10, HUFFMAN_EMIT_SYMBOL, 207),
+ (15, HUFFMAN_EMIT_SYMBOL, 207),
+ (24, HUFFMAN_EMIT_SYMBOL, 207),
+ (31, HUFFMAN_EMIT_SYMBOL, 207),
+ (41, HUFFMAN_EMIT_SYMBOL, 207),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 207),
+
+ # Node 189
+ (3, HUFFMAN_EMIT_SYMBOL, 234),
+ (6, HUFFMAN_EMIT_SYMBOL, 234),
+ (10, HUFFMAN_EMIT_SYMBOL, 234),
+ (15, HUFFMAN_EMIT_SYMBOL, 234),
+ (24, HUFFMAN_EMIT_SYMBOL, 234),
+ (31, HUFFMAN_EMIT_SYMBOL, 234),
+ (41, HUFFMAN_EMIT_SYMBOL, 234),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 234),
+ (3, HUFFMAN_EMIT_SYMBOL, 235),
+ (6, HUFFMAN_EMIT_SYMBOL, 235),
+ (10, HUFFMAN_EMIT_SYMBOL, 235),
+ (15, HUFFMAN_EMIT_SYMBOL, 235),
+ (24, HUFFMAN_EMIT_SYMBOL, 235),
+ (31, HUFFMAN_EMIT_SYMBOL, 235),
+ (41, HUFFMAN_EMIT_SYMBOL, 235),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 235),
+
+ # Node 190
+ (194, 0, 0),
+ (195, 0, 0),
+ (197, 0, 0),
+ (198, 0, 0),
+ (201, 0, 0),
+ (202, 0, 0),
+ (204, 0, 0),
+ (205, 0, 0),
+ (210, 0, 0),
+ (213, 0, 0),
+ (217, 0, 0),
+ (220, 0, 0),
+ (225, 0, 0),
+ (231, 0, 0),
+ (239, 0, 0),
+ (246, 0, 0),
+
+ # Node 191
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 192),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 193),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 200),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 201),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 202),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 205),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 210),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 213),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 218),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 219),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 238),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 240),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 242),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 243),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 255),
+ (206, 0, 0),
+
+ # Node 192
+ (1, HUFFMAN_EMIT_SYMBOL, 192),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 192),
+ (1, HUFFMAN_EMIT_SYMBOL, 193),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 193),
+ (1, HUFFMAN_EMIT_SYMBOL, 200),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 200),
+ (1, HUFFMAN_EMIT_SYMBOL, 201),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 201),
+ (1, HUFFMAN_EMIT_SYMBOL, 202),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 202),
+ (1, HUFFMAN_EMIT_SYMBOL, 205),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 205),
+ (1, HUFFMAN_EMIT_SYMBOL, 210),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 210),
+ (1, HUFFMAN_EMIT_SYMBOL, 213),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 213),
+
+ # Node 193
+ (2, HUFFMAN_EMIT_SYMBOL, 192),
+ (9, HUFFMAN_EMIT_SYMBOL, 192),
+ (23, HUFFMAN_EMIT_SYMBOL, 192),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 192),
+ (2, HUFFMAN_EMIT_SYMBOL, 193),
+ (9, HUFFMAN_EMIT_SYMBOL, 193),
+ (23, HUFFMAN_EMIT_SYMBOL, 193),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 193),
+ (2, HUFFMAN_EMIT_SYMBOL, 200),
+ (9, HUFFMAN_EMIT_SYMBOL, 200),
+ (23, HUFFMAN_EMIT_SYMBOL, 200),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 200),
+ (2, HUFFMAN_EMIT_SYMBOL, 201),
+ (9, HUFFMAN_EMIT_SYMBOL, 201),
+ (23, HUFFMAN_EMIT_SYMBOL, 201),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 201),
+
+ # Node 194
+ (3, HUFFMAN_EMIT_SYMBOL, 192),
+ (6, HUFFMAN_EMIT_SYMBOL, 192),
+ (10, HUFFMAN_EMIT_SYMBOL, 192),
+ (15, HUFFMAN_EMIT_SYMBOL, 192),
+ (24, HUFFMAN_EMIT_SYMBOL, 192),
+ (31, HUFFMAN_EMIT_SYMBOL, 192),
+ (41, HUFFMAN_EMIT_SYMBOL, 192),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 192),
+ (3, HUFFMAN_EMIT_SYMBOL, 193),
+ (6, HUFFMAN_EMIT_SYMBOL, 193),
+ (10, HUFFMAN_EMIT_SYMBOL, 193),
+ (15, HUFFMAN_EMIT_SYMBOL, 193),
+ (24, HUFFMAN_EMIT_SYMBOL, 193),
+ (31, HUFFMAN_EMIT_SYMBOL, 193),
+ (41, HUFFMAN_EMIT_SYMBOL, 193),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 193),
+
+ # Node 195
+ (3, HUFFMAN_EMIT_SYMBOL, 200),
+ (6, HUFFMAN_EMIT_SYMBOL, 200),
+ (10, HUFFMAN_EMIT_SYMBOL, 200),
+ (15, HUFFMAN_EMIT_SYMBOL, 200),
+ (24, HUFFMAN_EMIT_SYMBOL, 200),
+ (31, HUFFMAN_EMIT_SYMBOL, 200),
+ (41, HUFFMAN_EMIT_SYMBOL, 200),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 200),
+ (3, HUFFMAN_EMIT_SYMBOL, 201),
+ (6, HUFFMAN_EMIT_SYMBOL, 201),
+ (10, HUFFMAN_EMIT_SYMBOL, 201),
+ (15, HUFFMAN_EMIT_SYMBOL, 201),
+ (24, HUFFMAN_EMIT_SYMBOL, 201),
+ (31, HUFFMAN_EMIT_SYMBOL, 201),
+ (41, HUFFMAN_EMIT_SYMBOL, 201),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 201),
+
+ # Node 196
+ (2, HUFFMAN_EMIT_SYMBOL, 202),
+ (9, HUFFMAN_EMIT_SYMBOL, 202),
+ (23, HUFFMAN_EMIT_SYMBOL, 202),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 202),
+ (2, HUFFMAN_EMIT_SYMBOL, 205),
+ (9, HUFFMAN_EMIT_SYMBOL, 205),
+ (23, HUFFMAN_EMIT_SYMBOL, 205),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 205),
+ (2, HUFFMAN_EMIT_SYMBOL, 210),
+ (9, HUFFMAN_EMIT_SYMBOL, 210),
+ (23, HUFFMAN_EMIT_SYMBOL, 210),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 210),
+ (2, HUFFMAN_EMIT_SYMBOL, 213),
+ (9, HUFFMAN_EMIT_SYMBOL, 213),
+ (23, HUFFMAN_EMIT_SYMBOL, 213),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 213),
+
+ # Node 197
+ (3, HUFFMAN_EMIT_SYMBOL, 202),
+ (6, HUFFMAN_EMIT_SYMBOL, 202),
+ (10, HUFFMAN_EMIT_SYMBOL, 202),
+ (15, HUFFMAN_EMIT_SYMBOL, 202),
+ (24, HUFFMAN_EMIT_SYMBOL, 202),
+ (31, HUFFMAN_EMIT_SYMBOL, 202),
+ (41, HUFFMAN_EMIT_SYMBOL, 202),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 202),
+ (3, HUFFMAN_EMIT_SYMBOL, 205),
+ (6, HUFFMAN_EMIT_SYMBOL, 205),
+ (10, HUFFMAN_EMIT_SYMBOL, 205),
+ (15, HUFFMAN_EMIT_SYMBOL, 205),
+ (24, HUFFMAN_EMIT_SYMBOL, 205),
+ (31, HUFFMAN_EMIT_SYMBOL, 205),
+ (41, HUFFMAN_EMIT_SYMBOL, 205),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 205),
+
+ # Node 198
+ (3, HUFFMAN_EMIT_SYMBOL, 210),
+ (6, HUFFMAN_EMIT_SYMBOL, 210),
+ (10, HUFFMAN_EMIT_SYMBOL, 210),
+ (15, HUFFMAN_EMIT_SYMBOL, 210),
+ (24, HUFFMAN_EMIT_SYMBOL, 210),
+ (31, HUFFMAN_EMIT_SYMBOL, 210),
+ (41, HUFFMAN_EMIT_SYMBOL, 210),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 210),
+ (3, HUFFMAN_EMIT_SYMBOL, 213),
+ (6, HUFFMAN_EMIT_SYMBOL, 213),
+ (10, HUFFMAN_EMIT_SYMBOL, 213),
+ (15, HUFFMAN_EMIT_SYMBOL, 213),
+ (24, HUFFMAN_EMIT_SYMBOL, 213),
+ (31, HUFFMAN_EMIT_SYMBOL, 213),
+ (41, HUFFMAN_EMIT_SYMBOL, 213),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 213),
+
+ # Node 199
+ (1, HUFFMAN_EMIT_SYMBOL, 218),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 218),
+ (1, HUFFMAN_EMIT_SYMBOL, 219),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 219),
+ (1, HUFFMAN_EMIT_SYMBOL, 238),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 238),
+ (1, HUFFMAN_EMIT_SYMBOL, 240),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 240),
+ (1, HUFFMAN_EMIT_SYMBOL, 242),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 242),
+ (1, HUFFMAN_EMIT_SYMBOL, 243),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 243),
+ (1, HUFFMAN_EMIT_SYMBOL, 255),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 255),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 203),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 204),
+
+ # Node 200
+ (2, HUFFMAN_EMIT_SYMBOL, 218),
+ (9, HUFFMAN_EMIT_SYMBOL, 218),
+ (23, HUFFMAN_EMIT_SYMBOL, 218),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 218),
+ (2, HUFFMAN_EMIT_SYMBOL, 219),
+ (9, HUFFMAN_EMIT_SYMBOL, 219),
+ (23, HUFFMAN_EMIT_SYMBOL, 219),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 219),
+ (2, HUFFMAN_EMIT_SYMBOL, 238),
+ (9, HUFFMAN_EMIT_SYMBOL, 238),
+ (23, HUFFMAN_EMIT_SYMBOL, 238),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 238),
+ (2, HUFFMAN_EMIT_SYMBOL, 240),
+ (9, HUFFMAN_EMIT_SYMBOL, 240),
+ (23, HUFFMAN_EMIT_SYMBOL, 240),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 240),
+
+ # Node 201
+ (3, HUFFMAN_EMIT_SYMBOL, 218),
+ (6, HUFFMAN_EMIT_SYMBOL, 218),
+ (10, HUFFMAN_EMIT_SYMBOL, 218),
+ (15, HUFFMAN_EMIT_SYMBOL, 218),
+ (24, HUFFMAN_EMIT_SYMBOL, 218),
+ (31, HUFFMAN_EMIT_SYMBOL, 218),
+ (41, HUFFMAN_EMIT_SYMBOL, 218),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 218),
+ (3, HUFFMAN_EMIT_SYMBOL, 219),
+ (6, HUFFMAN_EMIT_SYMBOL, 219),
+ (10, HUFFMAN_EMIT_SYMBOL, 219),
+ (15, HUFFMAN_EMIT_SYMBOL, 219),
+ (24, HUFFMAN_EMIT_SYMBOL, 219),
+ (31, HUFFMAN_EMIT_SYMBOL, 219),
+ (41, HUFFMAN_EMIT_SYMBOL, 219),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 219),
+
+ # Node 202
+ (3, HUFFMAN_EMIT_SYMBOL, 238),
+ (6, HUFFMAN_EMIT_SYMBOL, 238),
+ (10, HUFFMAN_EMIT_SYMBOL, 238),
+ (15, HUFFMAN_EMIT_SYMBOL, 238),
+ (24, HUFFMAN_EMIT_SYMBOL, 238),
+ (31, HUFFMAN_EMIT_SYMBOL, 238),
+ (41, HUFFMAN_EMIT_SYMBOL, 238),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 238),
+ (3, HUFFMAN_EMIT_SYMBOL, 240),
+ (6, HUFFMAN_EMIT_SYMBOL, 240),
+ (10, HUFFMAN_EMIT_SYMBOL, 240),
+ (15, HUFFMAN_EMIT_SYMBOL, 240),
+ (24, HUFFMAN_EMIT_SYMBOL, 240),
+ (31, HUFFMAN_EMIT_SYMBOL, 240),
+ (41, HUFFMAN_EMIT_SYMBOL, 240),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 240),
+
+ # Node 203
+ (2, HUFFMAN_EMIT_SYMBOL, 242),
+ (9, HUFFMAN_EMIT_SYMBOL, 242),
+ (23, HUFFMAN_EMIT_SYMBOL, 242),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 242),
+ (2, HUFFMAN_EMIT_SYMBOL, 243),
+ (9, HUFFMAN_EMIT_SYMBOL, 243),
+ (23, HUFFMAN_EMIT_SYMBOL, 243),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 243),
+ (2, HUFFMAN_EMIT_SYMBOL, 255),
+ (9, HUFFMAN_EMIT_SYMBOL, 255),
+ (23, HUFFMAN_EMIT_SYMBOL, 255),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 255),
+ (1, HUFFMAN_EMIT_SYMBOL, 203),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 203),
+ (1, HUFFMAN_EMIT_SYMBOL, 204),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 204),
+
+ # Node 204
+ (3, HUFFMAN_EMIT_SYMBOL, 242),
+ (6, HUFFMAN_EMIT_SYMBOL, 242),
+ (10, HUFFMAN_EMIT_SYMBOL, 242),
+ (15, HUFFMAN_EMIT_SYMBOL, 242),
+ (24, HUFFMAN_EMIT_SYMBOL, 242),
+ (31, HUFFMAN_EMIT_SYMBOL, 242),
+ (41, HUFFMAN_EMIT_SYMBOL, 242),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 242),
+ (3, HUFFMAN_EMIT_SYMBOL, 243),
+ (6, HUFFMAN_EMIT_SYMBOL, 243),
+ (10, HUFFMAN_EMIT_SYMBOL, 243),
+ (15, HUFFMAN_EMIT_SYMBOL, 243),
+ (24, HUFFMAN_EMIT_SYMBOL, 243),
+ (31, HUFFMAN_EMIT_SYMBOL, 243),
+ (41, HUFFMAN_EMIT_SYMBOL, 243),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 243),
+
+ # Node 205
+ (3, HUFFMAN_EMIT_SYMBOL, 255),
+ (6, HUFFMAN_EMIT_SYMBOL, 255),
+ (10, HUFFMAN_EMIT_SYMBOL, 255),
+ (15, HUFFMAN_EMIT_SYMBOL, 255),
+ (24, HUFFMAN_EMIT_SYMBOL, 255),
+ (31, HUFFMAN_EMIT_SYMBOL, 255),
+ (41, HUFFMAN_EMIT_SYMBOL, 255),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 255),
+ (2, HUFFMAN_EMIT_SYMBOL, 203),
+ (9, HUFFMAN_EMIT_SYMBOL, 203),
+ (23, HUFFMAN_EMIT_SYMBOL, 203),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 203),
+ (2, HUFFMAN_EMIT_SYMBOL, 204),
+ (9, HUFFMAN_EMIT_SYMBOL, 204),
+ (23, HUFFMAN_EMIT_SYMBOL, 204),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 204),
+
+ # Node 206
+ (3, HUFFMAN_EMIT_SYMBOL, 203),
+ (6, HUFFMAN_EMIT_SYMBOL, 203),
+ (10, HUFFMAN_EMIT_SYMBOL, 203),
+ (15, HUFFMAN_EMIT_SYMBOL, 203),
+ (24, HUFFMAN_EMIT_SYMBOL, 203),
+ (31, HUFFMAN_EMIT_SYMBOL, 203),
+ (41, HUFFMAN_EMIT_SYMBOL, 203),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 203),
+ (3, HUFFMAN_EMIT_SYMBOL, 204),
+ (6, HUFFMAN_EMIT_SYMBOL, 204),
+ (10, HUFFMAN_EMIT_SYMBOL, 204),
+ (15, HUFFMAN_EMIT_SYMBOL, 204),
+ (24, HUFFMAN_EMIT_SYMBOL, 204),
+ (31, HUFFMAN_EMIT_SYMBOL, 204),
+ (41, HUFFMAN_EMIT_SYMBOL, 204),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 204),
+
+ # Node 207
+ (211, 0, 0),
+ (212, 0, 0),
+ (214, 0, 0),
+ (215, 0, 0),
+ (218, 0, 0),
+ (219, 0, 0),
+ (221, 0, 0),
+ (222, 0, 0),
+ (226, 0, 0),
+ (228, 0, 0),
+ (232, 0, 0),
+ (235, 0, 0),
+ (240, 0, 0),
+ (243, 0, 0),
+ (247, 0, 0),
+ (250, 0, 0),
+
+ # Node 208
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 211),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 212),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 214),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 221),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 222),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 223),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 241),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 244),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 245),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 246),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 247),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 248),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 250),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 251),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 252),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 253),
+
+ # Node 209
+ (1, HUFFMAN_EMIT_SYMBOL, 211),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 211),
+ (1, HUFFMAN_EMIT_SYMBOL, 212),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 212),
+ (1, HUFFMAN_EMIT_SYMBOL, 214),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 214),
+ (1, HUFFMAN_EMIT_SYMBOL, 221),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 221),
+ (1, HUFFMAN_EMIT_SYMBOL, 222),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 222),
+ (1, HUFFMAN_EMIT_SYMBOL, 223),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 223),
+ (1, HUFFMAN_EMIT_SYMBOL, 241),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 241),
+ (1, HUFFMAN_EMIT_SYMBOL, 244),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 244),
+
+ # Node 210
+ (2, HUFFMAN_EMIT_SYMBOL, 211),
+ (9, HUFFMAN_EMIT_SYMBOL, 211),
+ (23, HUFFMAN_EMIT_SYMBOL, 211),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 211),
+ (2, HUFFMAN_EMIT_SYMBOL, 212),
+ (9, HUFFMAN_EMIT_SYMBOL, 212),
+ (23, HUFFMAN_EMIT_SYMBOL, 212),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 212),
+ (2, HUFFMAN_EMIT_SYMBOL, 214),
+ (9, HUFFMAN_EMIT_SYMBOL, 214),
+ (23, HUFFMAN_EMIT_SYMBOL, 214),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 214),
+ (2, HUFFMAN_EMIT_SYMBOL, 221),
+ (9, HUFFMAN_EMIT_SYMBOL, 221),
+ (23, HUFFMAN_EMIT_SYMBOL, 221),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 221),
+
+ # Node 211
+ (3, HUFFMAN_EMIT_SYMBOL, 211),
+ (6, HUFFMAN_EMIT_SYMBOL, 211),
+ (10, HUFFMAN_EMIT_SYMBOL, 211),
+ (15, HUFFMAN_EMIT_SYMBOL, 211),
+ (24, HUFFMAN_EMIT_SYMBOL, 211),
+ (31, HUFFMAN_EMIT_SYMBOL, 211),
+ (41, HUFFMAN_EMIT_SYMBOL, 211),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 211),
+ (3, HUFFMAN_EMIT_SYMBOL, 212),
+ (6, HUFFMAN_EMIT_SYMBOL, 212),
+ (10, HUFFMAN_EMIT_SYMBOL, 212),
+ (15, HUFFMAN_EMIT_SYMBOL, 212),
+ (24, HUFFMAN_EMIT_SYMBOL, 212),
+ (31, HUFFMAN_EMIT_SYMBOL, 212),
+ (41, HUFFMAN_EMIT_SYMBOL, 212),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 212),
+
+ # Node 212
+ (3, HUFFMAN_EMIT_SYMBOL, 214),
+ (6, HUFFMAN_EMIT_SYMBOL, 214),
+ (10, HUFFMAN_EMIT_SYMBOL, 214),
+ (15, HUFFMAN_EMIT_SYMBOL, 214),
+ (24, HUFFMAN_EMIT_SYMBOL, 214),
+ (31, HUFFMAN_EMIT_SYMBOL, 214),
+ (41, HUFFMAN_EMIT_SYMBOL, 214),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 214),
+ (3, HUFFMAN_EMIT_SYMBOL, 221),
+ (6, HUFFMAN_EMIT_SYMBOL, 221),
+ (10, HUFFMAN_EMIT_SYMBOL, 221),
+ (15, HUFFMAN_EMIT_SYMBOL, 221),
+ (24, HUFFMAN_EMIT_SYMBOL, 221),
+ (31, HUFFMAN_EMIT_SYMBOL, 221),
+ (41, HUFFMAN_EMIT_SYMBOL, 221),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 221),
+
+ # Node 213
+ (2, HUFFMAN_EMIT_SYMBOL, 222),
+ (9, HUFFMAN_EMIT_SYMBOL, 222),
+ (23, HUFFMAN_EMIT_SYMBOL, 222),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 222),
+ (2, HUFFMAN_EMIT_SYMBOL, 223),
+ (9, HUFFMAN_EMIT_SYMBOL, 223),
+ (23, HUFFMAN_EMIT_SYMBOL, 223),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 223),
+ (2, HUFFMAN_EMIT_SYMBOL, 241),
+ (9, HUFFMAN_EMIT_SYMBOL, 241),
+ (23, HUFFMAN_EMIT_SYMBOL, 241),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 241),
+ (2, HUFFMAN_EMIT_SYMBOL, 244),
+ (9, HUFFMAN_EMIT_SYMBOL, 244),
+ (23, HUFFMAN_EMIT_SYMBOL, 244),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 244),
+
+ # Node 214
+ (3, HUFFMAN_EMIT_SYMBOL, 222),
+ (6, HUFFMAN_EMIT_SYMBOL, 222),
+ (10, HUFFMAN_EMIT_SYMBOL, 222),
+ (15, HUFFMAN_EMIT_SYMBOL, 222),
+ (24, HUFFMAN_EMIT_SYMBOL, 222),
+ (31, HUFFMAN_EMIT_SYMBOL, 222),
+ (41, HUFFMAN_EMIT_SYMBOL, 222),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 222),
+ (3, HUFFMAN_EMIT_SYMBOL, 223),
+ (6, HUFFMAN_EMIT_SYMBOL, 223),
+ (10, HUFFMAN_EMIT_SYMBOL, 223),
+ (15, HUFFMAN_EMIT_SYMBOL, 223),
+ (24, HUFFMAN_EMIT_SYMBOL, 223),
+ (31, HUFFMAN_EMIT_SYMBOL, 223),
+ (41, HUFFMAN_EMIT_SYMBOL, 223),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 223),
+
+ # Node 215
+ (3, HUFFMAN_EMIT_SYMBOL, 241),
+ (6, HUFFMAN_EMIT_SYMBOL, 241),
+ (10, HUFFMAN_EMIT_SYMBOL, 241),
+ (15, HUFFMAN_EMIT_SYMBOL, 241),
+ (24, HUFFMAN_EMIT_SYMBOL, 241),
+ (31, HUFFMAN_EMIT_SYMBOL, 241),
+ (41, HUFFMAN_EMIT_SYMBOL, 241),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 241),
+ (3, HUFFMAN_EMIT_SYMBOL, 244),
+ (6, HUFFMAN_EMIT_SYMBOL, 244),
+ (10, HUFFMAN_EMIT_SYMBOL, 244),
+ (15, HUFFMAN_EMIT_SYMBOL, 244),
+ (24, HUFFMAN_EMIT_SYMBOL, 244),
+ (31, HUFFMAN_EMIT_SYMBOL, 244),
+ (41, HUFFMAN_EMIT_SYMBOL, 244),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 244),
+
+ # Node 216
+ (1, HUFFMAN_EMIT_SYMBOL, 245),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 245),
+ (1, HUFFMAN_EMIT_SYMBOL, 246),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 246),
+ (1, HUFFMAN_EMIT_SYMBOL, 247),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 247),
+ (1, HUFFMAN_EMIT_SYMBOL, 248),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 248),
+ (1, HUFFMAN_EMIT_SYMBOL, 250),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 250),
+ (1, HUFFMAN_EMIT_SYMBOL, 251),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 251),
+ (1, HUFFMAN_EMIT_SYMBOL, 252),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 252),
+ (1, HUFFMAN_EMIT_SYMBOL, 253),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 253),
+
+ # Node 217
+ (2, HUFFMAN_EMIT_SYMBOL, 245),
+ (9, HUFFMAN_EMIT_SYMBOL, 245),
+ (23, HUFFMAN_EMIT_SYMBOL, 245),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 245),
+ (2, HUFFMAN_EMIT_SYMBOL, 246),
+ (9, HUFFMAN_EMIT_SYMBOL, 246),
+ (23, HUFFMAN_EMIT_SYMBOL, 246),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 246),
+ (2, HUFFMAN_EMIT_SYMBOL, 247),
+ (9, HUFFMAN_EMIT_SYMBOL, 247),
+ (23, HUFFMAN_EMIT_SYMBOL, 247),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 247),
+ (2, HUFFMAN_EMIT_SYMBOL, 248),
+ (9, HUFFMAN_EMIT_SYMBOL, 248),
+ (23, HUFFMAN_EMIT_SYMBOL, 248),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 248),
+
+ # Node 218
+ (3, HUFFMAN_EMIT_SYMBOL, 245),
+ (6, HUFFMAN_EMIT_SYMBOL, 245),
+ (10, HUFFMAN_EMIT_SYMBOL, 245),
+ (15, HUFFMAN_EMIT_SYMBOL, 245),
+ (24, HUFFMAN_EMIT_SYMBOL, 245),
+ (31, HUFFMAN_EMIT_SYMBOL, 245),
+ (41, HUFFMAN_EMIT_SYMBOL, 245),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 245),
+ (3, HUFFMAN_EMIT_SYMBOL, 246),
+ (6, HUFFMAN_EMIT_SYMBOL, 246),
+ (10, HUFFMAN_EMIT_SYMBOL, 246),
+ (15, HUFFMAN_EMIT_SYMBOL, 246),
+ (24, HUFFMAN_EMIT_SYMBOL, 246),
+ (31, HUFFMAN_EMIT_SYMBOL, 246),
+ (41, HUFFMAN_EMIT_SYMBOL, 246),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 246),
+
+ # Node 219
+ (3, HUFFMAN_EMIT_SYMBOL, 247),
+ (6, HUFFMAN_EMIT_SYMBOL, 247),
+ (10, HUFFMAN_EMIT_SYMBOL, 247),
+ (15, HUFFMAN_EMIT_SYMBOL, 247),
+ (24, HUFFMAN_EMIT_SYMBOL, 247),
+ (31, HUFFMAN_EMIT_SYMBOL, 247),
+ (41, HUFFMAN_EMIT_SYMBOL, 247),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 247),
+ (3, HUFFMAN_EMIT_SYMBOL, 248),
+ (6, HUFFMAN_EMIT_SYMBOL, 248),
+ (10, HUFFMAN_EMIT_SYMBOL, 248),
+ (15, HUFFMAN_EMIT_SYMBOL, 248),
+ (24, HUFFMAN_EMIT_SYMBOL, 248),
+ (31, HUFFMAN_EMIT_SYMBOL, 248),
+ (41, HUFFMAN_EMIT_SYMBOL, 248),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 248),
+
+ # Node 220
+ (2, HUFFMAN_EMIT_SYMBOL, 250),
+ (9, HUFFMAN_EMIT_SYMBOL, 250),
+ (23, HUFFMAN_EMIT_SYMBOL, 250),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 250),
+ (2, HUFFMAN_EMIT_SYMBOL, 251),
+ (9, HUFFMAN_EMIT_SYMBOL, 251),
+ (23, HUFFMAN_EMIT_SYMBOL, 251),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 251),
+ (2, HUFFMAN_EMIT_SYMBOL, 252),
+ (9, HUFFMAN_EMIT_SYMBOL, 252),
+ (23, HUFFMAN_EMIT_SYMBOL, 252),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 252),
+ (2, HUFFMAN_EMIT_SYMBOL, 253),
+ (9, HUFFMAN_EMIT_SYMBOL, 253),
+ (23, HUFFMAN_EMIT_SYMBOL, 253),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 253),
+
+ # Node 221
+ (3, HUFFMAN_EMIT_SYMBOL, 250),
+ (6, HUFFMAN_EMIT_SYMBOL, 250),
+ (10, HUFFMAN_EMIT_SYMBOL, 250),
+ (15, HUFFMAN_EMIT_SYMBOL, 250),
+ (24, HUFFMAN_EMIT_SYMBOL, 250),
+ (31, HUFFMAN_EMIT_SYMBOL, 250),
+ (41, HUFFMAN_EMIT_SYMBOL, 250),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 250),
+ (3, HUFFMAN_EMIT_SYMBOL, 251),
+ (6, HUFFMAN_EMIT_SYMBOL, 251),
+ (10, HUFFMAN_EMIT_SYMBOL, 251),
+ (15, HUFFMAN_EMIT_SYMBOL, 251),
+ (24, HUFFMAN_EMIT_SYMBOL, 251),
+ (31, HUFFMAN_EMIT_SYMBOL, 251),
+ (41, HUFFMAN_EMIT_SYMBOL, 251),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 251),
+
+ # Node 222
+ (3, HUFFMAN_EMIT_SYMBOL, 252),
+ (6, HUFFMAN_EMIT_SYMBOL, 252),
+ (10, HUFFMAN_EMIT_SYMBOL, 252),
+ (15, HUFFMAN_EMIT_SYMBOL, 252),
+ (24, HUFFMAN_EMIT_SYMBOL, 252),
+ (31, HUFFMAN_EMIT_SYMBOL, 252),
+ (41, HUFFMAN_EMIT_SYMBOL, 252),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 252),
+ (3, HUFFMAN_EMIT_SYMBOL, 253),
+ (6, HUFFMAN_EMIT_SYMBOL, 253),
+ (10, HUFFMAN_EMIT_SYMBOL, 253),
+ (15, HUFFMAN_EMIT_SYMBOL, 253),
+ (24, HUFFMAN_EMIT_SYMBOL, 253),
+ (31, HUFFMAN_EMIT_SYMBOL, 253),
+ (41, HUFFMAN_EMIT_SYMBOL, 253),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 253),
+
+ # Node 223
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 254),
+ (227, 0, 0),
+ (229, 0, 0),
+ (230, 0, 0),
+ (233, 0, 0),
+ (234, 0, 0),
+ (236, 0, 0),
+ (237, 0, 0),
+ (241, 0, 0),
+ (242, 0, 0),
+ (244, 0, 0),
+ (245, 0, 0),
+ (248, 0, 0),
+ (249, 0, 0),
+ (251, 0, 0),
+ (252, 0, 0),
+
+ # Node 224
+ (1, HUFFMAN_EMIT_SYMBOL, 254),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 254),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 2),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 3),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 4),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 5),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 6),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 7),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 8),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 11),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 12),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 14),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 15),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 16),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 17),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 18),
+
+ # Node 225
+ (2, HUFFMAN_EMIT_SYMBOL, 254),
+ (9, HUFFMAN_EMIT_SYMBOL, 254),
+ (23, HUFFMAN_EMIT_SYMBOL, 254),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 254),
+ (1, HUFFMAN_EMIT_SYMBOL, 2),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 2),
+ (1, HUFFMAN_EMIT_SYMBOL, 3),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 3),
+ (1, HUFFMAN_EMIT_SYMBOL, 4),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 4),
+ (1, HUFFMAN_EMIT_SYMBOL, 5),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 5),
+ (1, HUFFMAN_EMIT_SYMBOL, 6),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 6),
+ (1, HUFFMAN_EMIT_SYMBOL, 7),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 7),
+
+ # Node 226
+ (3, HUFFMAN_EMIT_SYMBOL, 254),
+ (6, HUFFMAN_EMIT_SYMBOL, 254),
+ (10, HUFFMAN_EMIT_SYMBOL, 254),
+ (15, HUFFMAN_EMIT_SYMBOL, 254),
+ (24, HUFFMAN_EMIT_SYMBOL, 254),
+ (31, HUFFMAN_EMIT_SYMBOL, 254),
+ (41, HUFFMAN_EMIT_SYMBOL, 254),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 254),
+ (2, HUFFMAN_EMIT_SYMBOL, 2),
+ (9, HUFFMAN_EMIT_SYMBOL, 2),
+ (23, HUFFMAN_EMIT_SYMBOL, 2),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 2),
+ (2, HUFFMAN_EMIT_SYMBOL, 3),
+ (9, HUFFMAN_EMIT_SYMBOL, 3),
+ (23, HUFFMAN_EMIT_SYMBOL, 3),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 3),
+
+ # Node 227
+ (3, HUFFMAN_EMIT_SYMBOL, 2),
+ (6, HUFFMAN_EMIT_SYMBOL, 2),
+ (10, HUFFMAN_EMIT_SYMBOL, 2),
+ (15, HUFFMAN_EMIT_SYMBOL, 2),
+ (24, HUFFMAN_EMIT_SYMBOL, 2),
+ (31, HUFFMAN_EMIT_SYMBOL, 2),
+ (41, HUFFMAN_EMIT_SYMBOL, 2),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 2),
+ (3, HUFFMAN_EMIT_SYMBOL, 3),
+ (6, HUFFMAN_EMIT_SYMBOL, 3),
+ (10, HUFFMAN_EMIT_SYMBOL, 3),
+ (15, HUFFMAN_EMIT_SYMBOL, 3),
+ (24, HUFFMAN_EMIT_SYMBOL, 3),
+ (31, HUFFMAN_EMIT_SYMBOL, 3),
+ (41, HUFFMAN_EMIT_SYMBOL, 3),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 3),
+
+ # Node 228
+ (2, HUFFMAN_EMIT_SYMBOL, 4),
+ (9, HUFFMAN_EMIT_SYMBOL, 4),
+ (23, HUFFMAN_EMIT_SYMBOL, 4),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 4),
+ (2, HUFFMAN_EMIT_SYMBOL, 5),
+ (9, HUFFMAN_EMIT_SYMBOL, 5),
+ (23, HUFFMAN_EMIT_SYMBOL, 5),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 5),
+ (2, HUFFMAN_EMIT_SYMBOL, 6),
+ (9, HUFFMAN_EMIT_SYMBOL, 6),
+ (23, HUFFMAN_EMIT_SYMBOL, 6),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 6),
+ (2, HUFFMAN_EMIT_SYMBOL, 7),
+ (9, HUFFMAN_EMIT_SYMBOL, 7),
+ (23, HUFFMAN_EMIT_SYMBOL, 7),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 7),
+
+ # Node 229
+ (3, HUFFMAN_EMIT_SYMBOL, 4),
+ (6, HUFFMAN_EMIT_SYMBOL, 4),
+ (10, HUFFMAN_EMIT_SYMBOL, 4),
+ (15, HUFFMAN_EMIT_SYMBOL, 4),
+ (24, HUFFMAN_EMIT_SYMBOL, 4),
+ (31, HUFFMAN_EMIT_SYMBOL, 4),
+ (41, HUFFMAN_EMIT_SYMBOL, 4),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 4),
+ (3, HUFFMAN_EMIT_SYMBOL, 5),
+ (6, HUFFMAN_EMIT_SYMBOL, 5),
+ (10, HUFFMAN_EMIT_SYMBOL, 5),
+ (15, HUFFMAN_EMIT_SYMBOL, 5),
+ (24, HUFFMAN_EMIT_SYMBOL, 5),
+ (31, HUFFMAN_EMIT_SYMBOL, 5),
+ (41, HUFFMAN_EMIT_SYMBOL, 5),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 5),
+
+ # Node 230
+ (3, HUFFMAN_EMIT_SYMBOL, 6),
+ (6, HUFFMAN_EMIT_SYMBOL, 6),
+ (10, HUFFMAN_EMIT_SYMBOL, 6),
+ (15, HUFFMAN_EMIT_SYMBOL, 6),
+ (24, HUFFMAN_EMIT_SYMBOL, 6),
+ (31, HUFFMAN_EMIT_SYMBOL, 6),
+ (41, HUFFMAN_EMIT_SYMBOL, 6),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 6),
+ (3, HUFFMAN_EMIT_SYMBOL, 7),
+ (6, HUFFMAN_EMIT_SYMBOL, 7),
+ (10, HUFFMAN_EMIT_SYMBOL, 7),
+ (15, HUFFMAN_EMIT_SYMBOL, 7),
+ (24, HUFFMAN_EMIT_SYMBOL, 7),
+ (31, HUFFMAN_EMIT_SYMBOL, 7),
+ (41, HUFFMAN_EMIT_SYMBOL, 7),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 7),
+
+ # Node 231
+ (1, HUFFMAN_EMIT_SYMBOL, 8),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 8),
+ (1, HUFFMAN_EMIT_SYMBOL, 11),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 11),
+ (1, HUFFMAN_EMIT_SYMBOL, 12),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 12),
+ (1, HUFFMAN_EMIT_SYMBOL, 14),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 14),
+ (1, HUFFMAN_EMIT_SYMBOL, 15),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 15),
+ (1, HUFFMAN_EMIT_SYMBOL, 16),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 16),
+ (1, HUFFMAN_EMIT_SYMBOL, 17),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 17),
+ (1, HUFFMAN_EMIT_SYMBOL, 18),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 18),
+
+ # Node 232
+ (2, HUFFMAN_EMIT_SYMBOL, 8),
+ (9, HUFFMAN_EMIT_SYMBOL, 8),
+ (23, HUFFMAN_EMIT_SYMBOL, 8),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 8),
+ (2, HUFFMAN_EMIT_SYMBOL, 11),
+ (9, HUFFMAN_EMIT_SYMBOL, 11),
+ (23, HUFFMAN_EMIT_SYMBOL, 11),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 11),
+ (2, HUFFMAN_EMIT_SYMBOL, 12),
+ (9, HUFFMAN_EMIT_SYMBOL, 12),
+ (23, HUFFMAN_EMIT_SYMBOL, 12),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 12),
+ (2, HUFFMAN_EMIT_SYMBOL, 14),
+ (9, HUFFMAN_EMIT_SYMBOL, 14),
+ (23, HUFFMAN_EMIT_SYMBOL, 14),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 14),
+
+ # Node 233
+ (3, HUFFMAN_EMIT_SYMBOL, 8),
+ (6, HUFFMAN_EMIT_SYMBOL, 8),
+ (10, HUFFMAN_EMIT_SYMBOL, 8),
+ (15, HUFFMAN_EMIT_SYMBOL, 8),
+ (24, HUFFMAN_EMIT_SYMBOL, 8),
+ (31, HUFFMAN_EMIT_SYMBOL, 8),
+ (41, HUFFMAN_EMIT_SYMBOL, 8),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 8),
+ (3, HUFFMAN_EMIT_SYMBOL, 11),
+ (6, HUFFMAN_EMIT_SYMBOL, 11),
+ (10, HUFFMAN_EMIT_SYMBOL, 11),
+ (15, HUFFMAN_EMIT_SYMBOL, 11),
+ (24, HUFFMAN_EMIT_SYMBOL, 11),
+ (31, HUFFMAN_EMIT_SYMBOL, 11),
+ (41, HUFFMAN_EMIT_SYMBOL, 11),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 11),
+
+ # Node 234
+ (3, HUFFMAN_EMIT_SYMBOL, 12),
+ (6, HUFFMAN_EMIT_SYMBOL, 12),
+ (10, HUFFMAN_EMIT_SYMBOL, 12),
+ (15, HUFFMAN_EMIT_SYMBOL, 12),
+ (24, HUFFMAN_EMIT_SYMBOL, 12),
+ (31, HUFFMAN_EMIT_SYMBOL, 12),
+ (41, HUFFMAN_EMIT_SYMBOL, 12),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 12),
+ (3, HUFFMAN_EMIT_SYMBOL, 14),
+ (6, HUFFMAN_EMIT_SYMBOL, 14),
+ (10, HUFFMAN_EMIT_SYMBOL, 14),
+ (15, HUFFMAN_EMIT_SYMBOL, 14),
+ (24, HUFFMAN_EMIT_SYMBOL, 14),
+ (31, HUFFMAN_EMIT_SYMBOL, 14),
+ (41, HUFFMAN_EMIT_SYMBOL, 14),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 14),
+
+ # Node 235
+ (2, HUFFMAN_EMIT_SYMBOL, 15),
+ (9, HUFFMAN_EMIT_SYMBOL, 15),
+ (23, HUFFMAN_EMIT_SYMBOL, 15),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 15),
+ (2, HUFFMAN_EMIT_SYMBOL, 16),
+ (9, HUFFMAN_EMIT_SYMBOL, 16),
+ (23, HUFFMAN_EMIT_SYMBOL, 16),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 16),
+ (2, HUFFMAN_EMIT_SYMBOL, 17),
+ (9, HUFFMAN_EMIT_SYMBOL, 17),
+ (23, HUFFMAN_EMIT_SYMBOL, 17),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 17),
+ (2, HUFFMAN_EMIT_SYMBOL, 18),
+ (9, HUFFMAN_EMIT_SYMBOL, 18),
+ (23, HUFFMAN_EMIT_SYMBOL, 18),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 18),
+
+ # Node 236
+ (3, HUFFMAN_EMIT_SYMBOL, 15),
+ (6, HUFFMAN_EMIT_SYMBOL, 15),
+ (10, HUFFMAN_EMIT_SYMBOL, 15),
+ (15, HUFFMAN_EMIT_SYMBOL, 15),
+ (24, HUFFMAN_EMIT_SYMBOL, 15),
+ (31, HUFFMAN_EMIT_SYMBOL, 15),
+ (41, HUFFMAN_EMIT_SYMBOL, 15),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 15),
+ (3, HUFFMAN_EMIT_SYMBOL, 16),
+ (6, HUFFMAN_EMIT_SYMBOL, 16),
+ (10, HUFFMAN_EMIT_SYMBOL, 16),
+ (15, HUFFMAN_EMIT_SYMBOL, 16),
+ (24, HUFFMAN_EMIT_SYMBOL, 16),
+ (31, HUFFMAN_EMIT_SYMBOL, 16),
+ (41, HUFFMAN_EMIT_SYMBOL, 16),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 16),
+
+ # Node 237
+ (3, HUFFMAN_EMIT_SYMBOL, 17),
+ (6, HUFFMAN_EMIT_SYMBOL, 17),
+ (10, HUFFMAN_EMIT_SYMBOL, 17),
+ (15, HUFFMAN_EMIT_SYMBOL, 17),
+ (24, HUFFMAN_EMIT_SYMBOL, 17),
+ (31, HUFFMAN_EMIT_SYMBOL, 17),
+ (41, HUFFMAN_EMIT_SYMBOL, 17),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 17),
+ (3, HUFFMAN_EMIT_SYMBOL, 18),
+ (6, HUFFMAN_EMIT_SYMBOL, 18),
+ (10, HUFFMAN_EMIT_SYMBOL, 18),
+ (15, HUFFMAN_EMIT_SYMBOL, 18),
+ (24, HUFFMAN_EMIT_SYMBOL, 18),
+ (31, HUFFMAN_EMIT_SYMBOL, 18),
+ (41, HUFFMAN_EMIT_SYMBOL, 18),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 18),
+
+ # Node 238
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 19),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 20),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 21),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 23),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 24),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 25),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 26),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 27),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 28),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 29),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 30),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 31),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 127),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 220),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 249),
+ (253, 0, 0),
+
+ # Node 239
+ (1, HUFFMAN_EMIT_SYMBOL, 19),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 19),
+ (1, HUFFMAN_EMIT_SYMBOL, 20),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 20),
+ (1, HUFFMAN_EMIT_SYMBOL, 21),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 21),
+ (1, HUFFMAN_EMIT_SYMBOL, 23),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 23),
+ (1, HUFFMAN_EMIT_SYMBOL, 24),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 24),
+ (1, HUFFMAN_EMIT_SYMBOL, 25),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 25),
+ (1, HUFFMAN_EMIT_SYMBOL, 26),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 26),
+ (1, HUFFMAN_EMIT_SYMBOL, 27),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 27),
+
+ # Node 240
+ (2, HUFFMAN_EMIT_SYMBOL, 19),
+ (9, HUFFMAN_EMIT_SYMBOL, 19),
+ (23, HUFFMAN_EMIT_SYMBOL, 19),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 19),
+ (2, HUFFMAN_EMIT_SYMBOL, 20),
+ (9, HUFFMAN_EMIT_SYMBOL, 20),
+ (23, HUFFMAN_EMIT_SYMBOL, 20),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 20),
+ (2, HUFFMAN_EMIT_SYMBOL, 21),
+ (9, HUFFMAN_EMIT_SYMBOL, 21),
+ (23, HUFFMAN_EMIT_SYMBOL, 21),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 21),
+ (2, HUFFMAN_EMIT_SYMBOL, 23),
+ (9, HUFFMAN_EMIT_SYMBOL, 23),
+ (23, HUFFMAN_EMIT_SYMBOL, 23),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 23),
+
+ # Node 241
+ (3, HUFFMAN_EMIT_SYMBOL, 19),
+ (6, HUFFMAN_EMIT_SYMBOL, 19),
+ (10, HUFFMAN_EMIT_SYMBOL, 19),
+ (15, HUFFMAN_EMIT_SYMBOL, 19),
+ (24, HUFFMAN_EMIT_SYMBOL, 19),
+ (31, HUFFMAN_EMIT_SYMBOL, 19),
+ (41, HUFFMAN_EMIT_SYMBOL, 19),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 19),
+ (3, HUFFMAN_EMIT_SYMBOL, 20),
+ (6, HUFFMAN_EMIT_SYMBOL, 20),
+ (10, HUFFMAN_EMIT_SYMBOL, 20),
+ (15, HUFFMAN_EMIT_SYMBOL, 20),
+ (24, HUFFMAN_EMIT_SYMBOL, 20),
+ (31, HUFFMAN_EMIT_SYMBOL, 20),
+ (41, HUFFMAN_EMIT_SYMBOL, 20),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 20),
+
+ # Node 242
+ (3, HUFFMAN_EMIT_SYMBOL, 21),
+ (6, HUFFMAN_EMIT_SYMBOL, 21),
+ (10, HUFFMAN_EMIT_SYMBOL, 21),
+ (15, HUFFMAN_EMIT_SYMBOL, 21),
+ (24, HUFFMAN_EMIT_SYMBOL, 21),
+ (31, HUFFMAN_EMIT_SYMBOL, 21),
+ (41, HUFFMAN_EMIT_SYMBOL, 21),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 21),
+ (3, HUFFMAN_EMIT_SYMBOL, 23),
+ (6, HUFFMAN_EMIT_SYMBOL, 23),
+ (10, HUFFMAN_EMIT_SYMBOL, 23),
+ (15, HUFFMAN_EMIT_SYMBOL, 23),
+ (24, HUFFMAN_EMIT_SYMBOL, 23),
+ (31, HUFFMAN_EMIT_SYMBOL, 23),
+ (41, HUFFMAN_EMIT_SYMBOL, 23),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 23),
+
+ # Node 243
+ (2, HUFFMAN_EMIT_SYMBOL, 24),
+ (9, HUFFMAN_EMIT_SYMBOL, 24),
+ (23, HUFFMAN_EMIT_SYMBOL, 24),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 24),
+ (2, HUFFMAN_EMIT_SYMBOL, 25),
+ (9, HUFFMAN_EMIT_SYMBOL, 25),
+ (23, HUFFMAN_EMIT_SYMBOL, 25),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 25),
+ (2, HUFFMAN_EMIT_SYMBOL, 26),
+ (9, HUFFMAN_EMIT_SYMBOL, 26),
+ (23, HUFFMAN_EMIT_SYMBOL, 26),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 26),
+ (2, HUFFMAN_EMIT_SYMBOL, 27),
+ (9, HUFFMAN_EMIT_SYMBOL, 27),
+ (23, HUFFMAN_EMIT_SYMBOL, 27),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 27),
+
+ # Node 244
+ (3, HUFFMAN_EMIT_SYMBOL, 24),
+ (6, HUFFMAN_EMIT_SYMBOL, 24),
+ (10, HUFFMAN_EMIT_SYMBOL, 24),
+ (15, HUFFMAN_EMIT_SYMBOL, 24),
+ (24, HUFFMAN_EMIT_SYMBOL, 24),
+ (31, HUFFMAN_EMIT_SYMBOL, 24),
+ (41, HUFFMAN_EMIT_SYMBOL, 24),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 24),
+ (3, HUFFMAN_EMIT_SYMBOL, 25),
+ (6, HUFFMAN_EMIT_SYMBOL, 25),
+ (10, HUFFMAN_EMIT_SYMBOL, 25),
+ (15, HUFFMAN_EMIT_SYMBOL, 25),
+ (24, HUFFMAN_EMIT_SYMBOL, 25),
+ (31, HUFFMAN_EMIT_SYMBOL, 25),
+ (41, HUFFMAN_EMIT_SYMBOL, 25),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 25),
+
+ # Node 245
+ (3, HUFFMAN_EMIT_SYMBOL, 26),
+ (6, HUFFMAN_EMIT_SYMBOL, 26),
+ (10, HUFFMAN_EMIT_SYMBOL, 26),
+ (15, HUFFMAN_EMIT_SYMBOL, 26),
+ (24, HUFFMAN_EMIT_SYMBOL, 26),
+ (31, HUFFMAN_EMIT_SYMBOL, 26),
+ (41, HUFFMAN_EMIT_SYMBOL, 26),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 26),
+ (3, HUFFMAN_EMIT_SYMBOL, 27),
+ (6, HUFFMAN_EMIT_SYMBOL, 27),
+ (10, HUFFMAN_EMIT_SYMBOL, 27),
+ (15, HUFFMAN_EMIT_SYMBOL, 27),
+ (24, HUFFMAN_EMIT_SYMBOL, 27),
+ (31, HUFFMAN_EMIT_SYMBOL, 27),
+ (41, HUFFMAN_EMIT_SYMBOL, 27),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 27),
+
+ # Node 246
+ (1, HUFFMAN_EMIT_SYMBOL, 28),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 28),
+ (1, HUFFMAN_EMIT_SYMBOL, 29),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 29),
+ (1, HUFFMAN_EMIT_SYMBOL, 30),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 30),
+ (1, HUFFMAN_EMIT_SYMBOL, 31),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 31),
+ (1, HUFFMAN_EMIT_SYMBOL, 127),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 127),
+ (1, HUFFMAN_EMIT_SYMBOL, 220),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 220),
+ (1, HUFFMAN_EMIT_SYMBOL, 249),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 249),
+ (254, 0, 0),
+ (255, 0, 0),
+
+ # Node 247
+ (2, HUFFMAN_EMIT_SYMBOL, 28),
+ (9, HUFFMAN_EMIT_SYMBOL, 28),
+ (23, HUFFMAN_EMIT_SYMBOL, 28),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 28),
+ (2, HUFFMAN_EMIT_SYMBOL, 29),
+ (9, HUFFMAN_EMIT_SYMBOL, 29),
+ (23, HUFFMAN_EMIT_SYMBOL, 29),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 29),
+ (2, HUFFMAN_EMIT_SYMBOL, 30),
+ (9, HUFFMAN_EMIT_SYMBOL, 30),
+ (23, HUFFMAN_EMIT_SYMBOL, 30),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 30),
+ (2, HUFFMAN_EMIT_SYMBOL, 31),
+ (9, HUFFMAN_EMIT_SYMBOL, 31),
+ (23, HUFFMAN_EMIT_SYMBOL, 31),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 31),
+
+ # Node 248
+ (3, HUFFMAN_EMIT_SYMBOL, 28),
+ (6, HUFFMAN_EMIT_SYMBOL, 28),
+ (10, HUFFMAN_EMIT_SYMBOL, 28),
+ (15, HUFFMAN_EMIT_SYMBOL, 28),
+ (24, HUFFMAN_EMIT_SYMBOL, 28),
+ (31, HUFFMAN_EMIT_SYMBOL, 28),
+ (41, HUFFMAN_EMIT_SYMBOL, 28),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 28),
+ (3, HUFFMAN_EMIT_SYMBOL, 29),
+ (6, HUFFMAN_EMIT_SYMBOL, 29),
+ (10, HUFFMAN_EMIT_SYMBOL, 29),
+ (15, HUFFMAN_EMIT_SYMBOL, 29),
+ (24, HUFFMAN_EMIT_SYMBOL, 29),
+ (31, HUFFMAN_EMIT_SYMBOL, 29),
+ (41, HUFFMAN_EMIT_SYMBOL, 29),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 29),
+
+ # Node 249
+ (3, HUFFMAN_EMIT_SYMBOL, 30),
+ (6, HUFFMAN_EMIT_SYMBOL, 30),
+ (10, HUFFMAN_EMIT_SYMBOL, 30),
+ (15, HUFFMAN_EMIT_SYMBOL, 30),
+ (24, HUFFMAN_EMIT_SYMBOL, 30),
+ (31, HUFFMAN_EMIT_SYMBOL, 30),
+ (41, HUFFMAN_EMIT_SYMBOL, 30),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 30),
+ (3, HUFFMAN_EMIT_SYMBOL, 31),
+ (6, HUFFMAN_EMIT_SYMBOL, 31),
+ (10, HUFFMAN_EMIT_SYMBOL, 31),
+ (15, HUFFMAN_EMIT_SYMBOL, 31),
+ (24, HUFFMAN_EMIT_SYMBOL, 31),
+ (31, HUFFMAN_EMIT_SYMBOL, 31),
+ (41, HUFFMAN_EMIT_SYMBOL, 31),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 31),
+
+ # Node 250
+ (2, HUFFMAN_EMIT_SYMBOL, 127),
+ (9, HUFFMAN_EMIT_SYMBOL, 127),
+ (23, HUFFMAN_EMIT_SYMBOL, 127),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 127),
+ (2, HUFFMAN_EMIT_SYMBOL, 220),
+ (9, HUFFMAN_EMIT_SYMBOL, 220),
+ (23, HUFFMAN_EMIT_SYMBOL, 220),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 220),
+ (2, HUFFMAN_EMIT_SYMBOL, 249),
+ (9, HUFFMAN_EMIT_SYMBOL, 249),
+ (23, HUFFMAN_EMIT_SYMBOL, 249),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 249),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 10),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 13),
+ (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 22),
+ (0, HUFFMAN_FAIL, 0),
+
+ # Node 251
+ (3, HUFFMAN_EMIT_SYMBOL, 127),
+ (6, HUFFMAN_EMIT_SYMBOL, 127),
+ (10, HUFFMAN_EMIT_SYMBOL, 127),
+ (15, HUFFMAN_EMIT_SYMBOL, 127),
+ (24, HUFFMAN_EMIT_SYMBOL, 127),
+ (31, HUFFMAN_EMIT_SYMBOL, 127),
+ (41, HUFFMAN_EMIT_SYMBOL, 127),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 127),
+ (3, HUFFMAN_EMIT_SYMBOL, 220),
+ (6, HUFFMAN_EMIT_SYMBOL, 220),
+ (10, HUFFMAN_EMIT_SYMBOL, 220),
+ (15, HUFFMAN_EMIT_SYMBOL, 220),
+ (24, HUFFMAN_EMIT_SYMBOL, 220),
+ (31, HUFFMAN_EMIT_SYMBOL, 220),
+ (41, HUFFMAN_EMIT_SYMBOL, 220),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 220),
+
+ # Node 252
+ (3, HUFFMAN_EMIT_SYMBOL, 249),
+ (6, HUFFMAN_EMIT_SYMBOL, 249),
+ (10, HUFFMAN_EMIT_SYMBOL, 249),
+ (15, HUFFMAN_EMIT_SYMBOL, 249),
+ (24, HUFFMAN_EMIT_SYMBOL, 249),
+ (31, HUFFMAN_EMIT_SYMBOL, 249),
+ (41, HUFFMAN_EMIT_SYMBOL, 249),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 249),
+ (1, HUFFMAN_EMIT_SYMBOL, 10),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 10),
+ (1, HUFFMAN_EMIT_SYMBOL, 13),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 13),
+ (1, HUFFMAN_EMIT_SYMBOL, 22),
+ (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 22),
+ (0, HUFFMAN_FAIL, 0),
+ (0, HUFFMAN_FAIL, 0),
+
+ # Node 253
+ (2, HUFFMAN_EMIT_SYMBOL, 10),
+ (9, HUFFMAN_EMIT_SYMBOL, 10),
+ (23, HUFFMAN_EMIT_SYMBOL, 10),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 10),
+ (2, HUFFMAN_EMIT_SYMBOL, 13),
+ (9, HUFFMAN_EMIT_SYMBOL, 13),
+ (23, HUFFMAN_EMIT_SYMBOL, 13),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 13),
+ (2, HUFFMAN_EMIT_SYMBOL, 22),
+ (9, HUFFMAN_EMIT_SYMBOL, 22),
+ (23, HUFFMAN_EMIT_SYMBOL, 22),
+ (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 22),
+ (0, HUFFMAN_FAIL, 0),
+ (0, HUFFMAN_FAIL, 0),
+ (0, HUFFMAN_FAIL, 0),
+ (0, HUFFMAN_FAIL, 0),
+
+ # Node 254
+ (3, HUFFMAN_EMIT_SYMBOL, 10),
+ (6, HUFFMAN_EMIT_SYMBOL, 10),
+ (10, HUFFMAN_EMIT_SYMBOL, 10),
+ (15, HUFFMAN_EMIT_SYMBOL, 10),
+ (24, HUFFMAN_EMIT_SYMBOL, 10),
+ (31, HUFFMAN_EMIT_SYMBOL, 10),
+ (41, HUFFMAN_EMIT_SYMBOL, 10),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 10),
+ (3, HUFFMAN_EMIT_SYMBOL, 13),
+ (6, HUFFMAN_EMIT_SYMBOL, 13),
+ (10, HUFFMAN_EMIT_SYMBOL, 13),
+ (15, HUFFMAN_EMIT_SYMBOL, 13),
+ (24, HUFFMAN_EMIT_SYMBOL, 13),
+ (31, HUFFMAN_EMIT_SYMBOL, 13),
+ (41, HUFFMAN_EMIT_SYMBOL, 13),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 13),
+
+ # Node 255
+ (3, HUFFMAN_EMIT_SYMBOL, 22),
+ (6, HUFFMAN_EMIT_SYMBOL, 22),
+ (10, HUFFMAN_EMIT_SYMBOL, 22),
+ (15, HUFFMAN_EMIT_SYMBOL, 22),
+ (24, HUFFMAN_EMIT_SYMBOL, 22),
+ (31, HUFFMAN_EMIT_SYMBOL, 22),
+ (41, HUFFMAN_EMIT_SYMBOL, 22),
+ (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 22),
+ (0, HUFFMAN_FAIL, 0),
+ (0, HUFFMAN_FAIL, 0),
+ (0, HUFFMAN_FAIL, 0),
+ (0, HUFFMAN_FAIL, 0),
+ (0, HUFFMAN_FAIL, 0),
+ (0, HUFFMAN_FAIL, 0),
+ (0, HUFFMAN_FAIL, 0),
+ (0, HUFFMAN_FAIL, 0),
+]
diff --git a/testing/web-platform/tests/tools/third_party/hpack/hpack/struct.py b/testing/web-platform/tests/tools/third_party/hpack/hpack/struct.py
new file mode 100644
index 0000000000..e860cd756e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/hpack/struct.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+"""
+hpack/struct
+~~~~~~~~~~~~
+
+Contains structures for representing header fields with associated metadata.
+"""
+
+
+class HeaderTuple(tuple):
+ """
+ A data structure that stores a single header field.
+
+ HTTP headers can be thought of as tuples of ``(field name, field value)``.
+ A single header block is a sequence of such tuples.
+
+ In HTTP/2, however, certain bits of additional information are required for
+ compressing these headers: in particular, whether the header field can be
+ safely added to the HPACK compression context.
+
+ This class stores a header that can be added to the compression context. In
+ all other ways it behaves exactly like a tuple.
+ """
+ __slots__ = ()
+
+ indexable = True
+
+ def __new__(_cls, *args):
+ return tuple.__new__(_cls, args)
+
+
+class NeverIndexedHeaderTuple(HeaderTuple):
+ """
+ A data structure that stores a single header field that cannot be added to
+ a HTTP/2 header compression context.
+ """
+ __slots__ = ()
+
+ indexable = False
diff --git a/testing/web-platform/tests/tools/third_party/hpack/hpack/table.py b/testing/web-platform/tests/tools/third_party/hpack/hpack/table.py
new file mode 100644
index 0000000000..9a89c72118
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/hpack/table.py
@@ -0,0 +1,215 @@
+# -*- coding: utf-8 -*-
+# flake8: noqa
+from collections import deque
+import logging
+
+from .exceptions import InvalidTableIndex
+
+log = logging.getLogger(__name__)
+
+
+def table_entry_size(name, value):
+ """
+ Calculates the size of a single entry
+
+ This size is mostly irrelevant to us and defined
+ specifically to accommodate memory management for
+ lower level implementations. The 32 extra bytes are
+ considered the "maximum" overhead that would be
+ required to represent each entry in the table.
+
+ See RFC7541 Section 4.1
+ """
+ return 32 + len(name) + len(value)
+
+
+class HeaderTable(object):
+ """
+ Implements the combined static and dynamic header table
+
+ The name and value arguments for all the functions
+ should ONLY be byte strings (b'') however this is not
+ strictly enforced in the interface.
+
+ See RFC7541 Section 2.3
+ """
+ #: Default maximum size of the dynamic table. See
+ #: RFC7540 Section 6.5.2.
+ DEFAULT_SIZE = 4096
+
+ #: Constant list of static headers. See RFC7541 Section
+ #: 2.3.1 and Appendix A
+ STATIC_TABLE = (
+ (b':authority' , b'' ), # noqa
+ (b':method' , b'GET' ), # noqa
+ (b':method' , b'POST' ), # noqa
+ (b':path' , b'/' ), # noqa
+ (b':path' , b'/index.html' ), # noqa
+ (b':scheme' , b'http' ), # noqa
+ (b':scheme' , b'https' ), # noqa
+ (b':status' , b'200' ), # noqa
+ (b':status' , b'204' ), # noqa
+ (b':status' , b'206' ), # noqa
+ (b':status' , b'304' ), # noqa
+ (b':status' , b'400' ), # noqa
+ (b':status' , b'404' ), # noqa
+ (b':status' , b'500' ), # noqa
+ (b'accept-charset' , b'' ), # noqa
+ (b'accept-encoding' , b'gzip, deflate'), # noqa
+ (b'accept-language' , b'' ), # noqa
+ (b'accept-ranges' , b'' ), # noqa
+ (b'accept' , b'' ), # noqa
+ (b'access-control-allow-origin' , b'' ), # noqa
+ (b'age' , b'' ), # noqa
+ (b'allow' , b'' ), # noqa
+ (b'authorization' , b'' ), # noqa
+ (b'cache-control' , b'' ), # noqa
+ (b'content-disposition' , b'' ), # noqa
+ (b'content-encoding' , b'' ), # noqa
+ (b'content-language' , b'' ), # noqa
+ (b'content-length' , b'' ), # noqa
+ (b'content-location' , b'' ), # noqa
+ (b'content-range' , b'' ), # noqa
+ (b'content-type' , b'' ), # noqa
+ (b'cookie' , b'' ), # noqa
+ (b'date' , b'' ), # noqa
+ (b'etag' , b'' ), # noqa
+ (b'expect' , b'' ), # noqa
+ (b'expires' , b'' ), # noqa
+ (b'from' , b'' ), # noqa
+ (b'host' , b'' ), # noqa
+ (b'if-match' , b'' ), # noqa
+ (b'if-modified-since' , b'' ), # noqa
+ (b'if-none-match' , b'' ), # noqa
+ (b'if-range' , b'' ), # noqa
+ (b'if-unmodified-since' , b'' ), # noqa
+ (b'last-modified' , b'' ), # noqa
+ (b'link' , b'' ), # noqa
+ (b'location' , b'' ), # noqa
+ (b'max-forwards' , b'' ), # noqa
+ (b'proxy-authenticate' , b'' ), # noqa
+ (b'proxy-authorization' , b'' ), # noqa
+ (b'range' , b'' ), # noqa
+ (b'referer' , b'' ), # noqa
+ (b'refresh' , b'' ), # noqa
+ (b'retry-after' , b'' ), # noqa
+ (b'server' , b'' ), # noqa
+ (b'set-cookie' , b'' ), # noqa
+ (b'strict-transport-security' , b'' ), # noqa
+ (b'transfer-encoding' , b'' ), # noqa
+ (b'user-agent' , b'' ), # noqa
+ (b'vary' , b'' ), # noqa
+ (b'via' , b'' ), # noqa
+ (b'www-authenticate' , b'' ), # noqa
+ ) # noqa
+
+ STATIC_TABLE_LENGTH = len(STATIC_TABLE)
+
+ def __init__(self):
+ self._maxsize = HeaderTable.DEFAULT_SIZE
+ self._current_size = 0
+ self.resized = False
+ self.dynamic_entries = deque()
+
+ def get_by_index(self, index):
+ """
+ Returns the entry specified by index
+
+ Note that the table is 1-based ie an index of 0 is
+ invalid. This is due to the fact that a zero value
+ index signals that a completely unindexed header
+ follows.
+
+ The entry will either be from the static table or
+ the dynamic table depending on the value of index.
+ """
+ original_index = index
+ index -= 1
+ if 0 <= index:
+ if index < HeaderTable.STATIC_TABLE_LENGTH:
+ return HeaderTable.STATIC_TABLE[index]
+
+ index -= HeaderTable.STATIC_TABLE_LENGTH
+ if index < len(self.dynamic_entries):
+ return self.dynamic_entries[index]
+
+ raise InvalidTableIndex("Invalid table index %d" % original_index)
+
+ def __repr__(self):
+ return "HeaderTable(%d, %s, %r)" % (
+ self._maxsize,
+ self.resized,
+ self.dynamic_entries
+ )
+
+ def add(self, name, value):
+ """
+ Adds a new entry to the table
+
+ We reduce the table size if the entry will make the
+ table size greater than maxsize.
+ """
+ # We just clear the table if the entry is too big
+ size = table_entry_size(name, value)
+ if size > self._maxsize:
+ self.dynamic_entries.clear()
+ self._current_size = 0
+ else:
+ # Add new entry
+ self.dynamic_entries.appendleft((name, value))
+ self._current_size += size
+ self._shrink()
+
+ def search(self, name, value):
+ """
+ Searches the table for the entry specified by name
+ and value
+
+ Returns one of the following:
+ - ``None``, no match at all
+ - ``(index, name, None)`` for partial matches on name only.
+ - ``(index, name, value)`` for perfect matches.
+ """
+ offset = HeaderTable.STATIC_TABLE_LENGTH + 1
+ partial = None
+ for (i, (n, v)) in enumerate(HeaderTable.STATIC_TABLE):
+ if n == name:
+ if v == value:
+ return i + 1, n, v
+ elif partial is None:
+ partial = (i + 1, n, None)
+ for (i, (n, v)) in enumerate(self.dynamic_entries):
+ if n == name:
+ if v == value:
+ return i + offset, n, v
+ elif partial is None:
+ partial = (i + offset, n, None)
+ return partial
+
+ @property
+ def maxsize(self):
+ return self._maxsize
+
+ @maxsize.setter
+ def maxsize(self, newmax):
+ newmax = int(newmax)
+ log.debug("Resizing header table to %d from %d", newmax, self._maxsize)
+ oldmax = self._maxsize
+ self._maxsize = newmax
+ self.resized = (newmax != oldmax)
+ if newmax <= 0:
+ self.dynamic_entries.clear()
+ self._current_size = 0
+ elif oldmax > newmax:
+ self._shrink()
+
+ def _shrink(self):
+ """
+ Shrinks the dynamic table to be at or below maxsize
+ """
+ cursize = self._current_size
+ while cursize > self._maxsize:
+ name, value = self.dynamic_entries.pop()
+ cursize -= table_entry_size(name, value)
+ log.debug("Evicting %s: %s from the header table", name, value)
+ self._current_size = cursize
diff --git a/testing/web-platform/tests/tools/third_party/hpack/setup.cfg b/testing/web-platform/tests/tools/third_party/hpack/setup.cfg
new file mode 100644
index 0000000000..b1d2b88c0a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/setup.cfg
@@ -0,0 +1,12 @@
+[wheel]
+universal = 1
+
+[flake8]
+max-complexity = 10
+exclude =
+ hpack/huffman_constants.py
+
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/testing/web-platform/tests/tools/third_party/hpack/setup.py b/testing/web-platform/tests/tools/third_party/hpack/setup.py
new file mode 100644
index 0000000000..7ffc4beb3d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/setup.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import os
+import re
+import sys
+
+try:
+ from setuptools import setup
+except ImportError:
+ from distutils.core import setup
+
+# Get the version
+version_regex = r'__version__ = ["\']([^"\']*)["\']'
+with open('hpack/__init__.py', 'r') as f:
+ text = f.read()
+ match = re.search(version_regex, text)
+
+ if match:
+ version = match.group(1)
+ else:
+ raise RuntimeError("No version number found!")
+
+# Stealing this from Kenneth Reitz
+if sys.argv[-1] == 'publish':
+ os.system('python setup.py sdist upload')
+ sys.exit()
+
+packages = ['hpack']
+
+setup(
+ name='hpack',
+ version=version,
+ description='Pure-Python HPACK header compression',
+ long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(),
+ author='Cory Benfield',
+ author_email='cory@lukasa.co.uk',
+ url='http://hyper.rtfd.org',
+ packages=packages,
+ package_data={'': ['LICENSE', 'README.rst', 'CONTRIBUTORS.rst', 'HISTORY.rst', 'NOTICES']},
+ package_dir={'hpack': 'hpack'},
+ include_package_data=True,
+ license='MIT License',
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ ],
+)
diff --git a/testing/web-platform/tests/tools/third_party/hpack/test/test_encode_decode.py b/testing/web-platform/tests/tools/third_party/hpack/test/test_encode_decode.py
new file mode 100644
index 0000000000..94820f2e9e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/test/test_encode_decode.py
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+"""
+Test for the integer encoding/decoding functionality in the HPACK library.
+"""
+import pytest
+
+from hypothesis import given
+from hypothesis.strategies import integers, binary, one_of
+
+from hpack.hpack import encode_integer, decode_integer
+from hpack.exceptions import HPACKDecodingError
+
+
+class TestIntegerEncoding(object):
+ # These tests are stolen from the HPACK spec.
+ def test_encoding_10_with_5_bit_prefix(self):
+ val = encode_integer(10, 5)
+ assert len(val) == 1
+ assert val == bytearray(b'\x0a')
+
+ def test_encoding_1337_with_5_bit_prefix(self):
+ val = encode_integer(1337, 5)
+ assert len(val) == 3
+ assert val == bytearray(b'\x1f\x9a\x0a')
+
+ def test_encoding_42_with_8_bit_prefix(self):
+ val = encode_integer(42, 8)
+ assert len(val) == 1
+ assert val == bytearray(b'\x2a')
+
+
+class TestIntegerDecoding(object):
+ # These tests are stolen from the HPACK spec.
+ def test_decoding_10_with_5_bit_prefix(self):
+ val = decode_integer(b'\x0a', 5)
+ assert val == (10, 1)
+
+ def test_encoding_1337_with_5_bit_prefix(self):
+ val = decode_integer(b'\x1f\x9a\x0a', 5)
+ assert val == (1337, 3)
+
+ def test_encoding_42_with_8_bit_prefix(self):
+ val = decode_integer(b'\x2a', 8)
+ assert val == (42, 1)
+
+ def test_decode_empty_string_fails(self):
+ with pytest.raises(HPACKDecodingError):
+ decode_integer(b'', 8)
+
+ def test_decode_insufficient_data_fails(self):
+ with pytest.raises(HPACKDecodingError):
+ decode_integer(b'\x1f', 5)
+
+
+class TestEncodingProperties(object):
+ """
+ Property-based tests for our integer encoder and decoder.
+ """
+ @given(
+ integer=integers(min_value=0),
+ prefix_bits=integers(min_value=1, max_value=8)
+ )
+ def test_encode_positive_integer_always_valid(self, integer, prefix_bits):
+ """
+ So long as the prefix bits are between 1 and 8, any positive integer
+ can be represented.
+ """
+ result = encode_integer(integer, prefix_bits)
+ assert isinstance(result, bytearray)
+ assert len(result) > 0
+
+ @given(
+ integer=integers(max_value=-1),
+ prefix_bits=integers(min_value=1, max_value=8)
+ )
+ def test_encode_fails_for_negative_integers(self, integer, prefix_bits):
+ """
+ If the integer to encode is negative, the encoder fails.
+ """
+ with pytest.raises(ValueError):
+ encode_integer(integer, prefix_bits)
+
+ @given(
+ integer=integers(min_value=0),
+ prefix_bits=one_of(
+ integers(max_value=0),
+ integers(min_value=9)
+ )
+ )
+ def test_encode_fails_for_invalid_prefixes(self, integer, prefix_bits):
+ """
+ If the prefix is out of the range [1,8], the encoder fails.
+ """
+ with pytest.raises(ValueError):
+ encode_integer(integer, prefix_bits)
+
+ @given(
+ prefix_bits=one_of(
+ integers(max_value=0),
+ integers(min_value=9)
+ )
+ )
+ def test_decode_fails_for_invalid_prefixes(self, prefix_bits):
+ """
+ If the prefix is out of the range [1,8], the encoder fails.
+ """
+ with pytest.raises(ValueError):
+ decode_integer(b'\x00', prefix_bits)
+
+ @given(
+ data=binary(),
+ prefix_bits=integers(min_value=1, max_value=8)
+ )
+ def test_decode_either_succeeds_or_raises_error(self, data, prefix_bits):
+ """
+ Attempting to decode data either returns a positive integer or throws a
+ HPACKDecodingError.
+ """
+ try:
+ result, consumed = decode_integer(data, prefix_bits)
+ except HPACKDecodingError:
+ pass
+ else:
+ assert isinstance(result, int)
+ assert result >= 0
+ assert consumed > 0
+
+ @given(
+ integer=integers(min_value=0),
+ prefix_bits=integers(min_value=1, max_value=8)
+ )
+ def test_encode_decode_round_trips(self, integer, prefix_bits):
+ """
+ Given valid data, the encoder and decoder can round trip.
+ """
+ encoded_result = encode_integer(integer, prefix_bits)
+ decoded_integer, consumed = decode_integer(
+ bytes(encoded_result), prefix_bits
+ )
+ assert integer == decoded_integer
+ assert consumed > 0
diff --git a/testing/web-platform/tests/tools/third_party/hpack/test/test_hpack.py b/testing/web-platform/tests/tools/third_party/hpack/test/test_hpack.py
new file mode 100644
index 0000000000..c3333b4144
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/test/test_hpack.py
@@ -0,0 +1,828 @@
+# -*- coding: utf-8 -*-
+from hpack.hpack import Encoder, Decoder, _dict_to_iterable, _to_bytes
+from hpack.exceptions import (
+ HPACKDecodingError, InvalidTableIndex, OversizedHeaderListError,
+ InvalidTableSizeError
+)
+from hpack.struct import HeaderTuple, NeverIndexedHeaderTuple
+import itertools
+import pytest
+
+from hypothesis import given
+from hypothesis.strategies import text, binary, sets, one_of
+
+try:
+ unicode = unicode
+except NameError:
+ unicode = str
+
+
+class TestHPACKEncoder(object):
+ # These tests are stolen entirely from the IETF specification examples.
+ def test_literal_header_field_with_indexing(self):
+ """
+ The header field representation uses a literal name and a literal
+ value.
+ """
+ e = Encoder()
+ header_set = {'custom-key': 'custom-header'}
+ result = b'\x40\x0acustom-key\x0dcustom-header'
+
+ assert e.encode(header_set, huffman=False) == result
+ assert list(e.header_table.dynamic_entries) == [
+ (n.encode('utf-8'), v.encode('utf-8'))
+ for n, v in header_set.items()
+ ]
+
+ def test_sensitive_headers(self):
+ """
+ Test encoding header values
+ """
+ e = Encoder()
+ result = (b'\x82\x14\x88\x63\xa1\xa9' +
+ b'\x32\x08\x73\xd0\xc7\x10' +
+ b'\x87\x25\xa8\x49\xe9\xea' +
+ b'\x5f\x5f\x89\x41\x6a\x41' +
+ b'\x92\x6e\xe5\x35\x52\x9f')
+ header_set = [
+ (':method', 'GET', True),
+ (':path', '/jimiscool/', True),
+ ('customkey', 'sensitiveinfo', True),
+ ]
+ assert e.encode(header_set, huffman=True) == result
+
+ def test_non_sensitive_headers_with_header_tuples(self):
+ """
+ A header field stored in a HeaderTuple emits a representation that
+ allows indexing.
+ """
+ e = Encoder()
+ result = (b'\x82\x44\x88\x63\xa1\xa9' +
+ b'\x32\x08\x73\xd0\xc7\x40' +
+ b'\x87\x25\xa8\x49\xe9\xea' +
+ b'\x5f\x5f\x89\x41\x6a\x41' +
+ b'\x92\x6e\xe5\x35\x52\x9f')
+ header_set = [
+ HeaderTuple(':method', 'GET'),
+ HeaderTuple(':path', '/jimiscool/'),
+ HeaderTuple('customkey', 'sensitiveinfo'),
+ ]
+ assert e.encode(header_set, huffman=True) == result
+
+ def test_sensitive_headers_with_header_tuples(self):
+ """
+ A header field stored in a NeverIndexedHeaderTuple emits a
+ representation that forbids indexing.
+ """
+ e = Encoder()
+ result = (b'\x82\x14\x88\x63\xa1\xa9' +
+ b'\x32\x08\x73\xd0\xc7\x10' +
+ b'\x87\x25\xa8\x49\xe9\xea' +
+ b'\x5f\x5f\x89\x41\x6a\x41' +
+ b'\x92\x6e\xe5\x35\x52\x9f')
+ header_set = [
+ NeverIndexedHeaderTuple(':method', 'GET'),
+ NeverIndexedHeaderTuple(':path', '/jimiscool/'),
+ NeverIndexedHeaderTuple('customkey', 'sensitiveinfo'),
+ ]
+ assert e.encode(header_set, huffman=True) == result
+
+ def test_header_table_size_getter(self):
+ e = Encoder()
+ assert e.header_table_size == 4096
+
+ def test_indexed_literal_header_field_with_indexing(self):
+ """
+ The header field representation uses an indexed name and a literal
+ value and performs incremental indexing.
+ """
+ e = Encoder()
+ header_set = {':path': '/sample/path'}
+ result = b'\x44\x0c/sample/path'
+
+ assert e.encode(header_set, huffman=False) == result
+ assert list(e.header_table.dynamic_entries) == [
+ (n.encode('utf-8'), v.encode('utf-8'))
+ for n, v in header_set.items()
+ ]
+
+ def test_indexed_header_field(self):
+ """
+ The header field representation uses an indexed header field, from
+ the static table.
+ """
+ e = Encoder()
+ header_set = {':method': 'GET'}
+ result = b'\x82'
+
+ assert e.encode(header_set, huffman=False) == result
+ assert list(e.header_table.dynamic_entries) == []
+
+ def test_indexed_header_field_from_static_table(self):
+ e = Encoder()
+ e.header_table_size = 0
+ header_set = {':method': 'GET'}
+ result = b'\x82'
+
+ # Make sure we don't emit an encoding context update.
+ e.header_table.resized = False
+
+ assert e.encode(header_set, huffman=False) == result
+ assert list(e.header_table.dynamic_entries) == []
+
+ def test_request_examples_without_huffman(self):
+ """
+ This section shows several consecutive header sets, corresponding to
+ HTTP requests, on the same connection.
+ """
+ e = Encoder()
+ first_header_set = [
+ (':method', 'GET',),
+ (':scheme', 'http',),
+ (':path', '/',),
+ (':authority', 'www.example.com'),
+ ]
+ # We should have :authority in first_header_table since we index it
+ first_header_table = [(':authority', 'www.example.com')]
+ first_result = b'\x82\x86\x84\x41\x0fwww.example.com'
+
+ assert e.encode(first_header_set, huffman=False) == first_result
+ assert list(e.header_table.dynamic_entries) == [
+ (n.encode('utf-8'), v.encode('utf-8'))
+ for n, v in first_header_table
+ ]
+
+ second_header_set = [
+ (':method', 'GET',),
+ (':scheme', 'http',),
+ (':path', '/',),
+ (':authority', 'www.example.com',),
+ ('cache-control', 'no-cache'),
+ ]
+ second_header_table = [
+ ('cache-control', 'no-cache'),
+ (':authority', 'www.example.com')
+ ]
+ second_result = b'\x82\x86\x84\xbeX\x08no-cache'
+
+ assert e.encode(second_header_set, huffman=False) == second_result
+ assert list(e.header_table.dynamic_entries) == [
+ (n.encode('utf-8'), v.encode('utf-8'))
+ for n, v in second_header_table
+ ]
+
+ third_header_set = [
+ (':method', 'GET',),
+ (':scheme', 'https',),
+ (':path', '/index.html',),
+ (':authority', 'www.example.com',),
+ ('custom-key', 'custom-value'),
+ ]
+ third_result = (
+ b'\x82\x87\x85\xbf@\ncustom-key\x0ccustom-value'
+ )
+
+ assert e.encode(third_header_set, huffman=False) == third_result
+ # Don't check the header table here, it's just too complex to be
+ # reliable. Check its length though.
+ assert len(e.header_table.dynamic_entries) == 3
+
+ def test_request_examples_with_huffman(self):
+ """
+ This section shows the same examples as the previous section, but
+ using Huffman encoding for the literal values.
+ """
+ e = Encoder()
+ first_header_set = [
+ (':method', 'GET',),
+ (':scheme', 'http',),
+ (':path', '/',),
+ (':authority', 'www.example.com'),
+ ]
+ first_header_table = [(':authority', 'www.example.com')]
+ first_result = (
+ b'\x82\x86\x84\x41\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff'
+ )
+
+ assert e.encode(first_header_set, huffman=True) == first_result
+ assert list(e.header_table.dynamic_entries) == [
+ (n.encode('utf-8'), v.encode('utf-8'))
+ for n, v in first_header_table
+ ]
+
+ second_header_table = [
+ ('cache-control', 'no-cache'),
+ (':authority', 'www.example.com')
+ ]
+ second_header_set = [
+ (':method', 'GET',),
+ (':scheme', 'http',),
+ (':path', '/',),
+ (':authority', 'www.example.com',),
+ ('cache-control', 'no-cache'),
+ ]
+ second_result = b'\x82\x86\x84\xbeX\x86\xa8\xeb\x10d\x9c\xbf'
+
+ assert e.encode(second_header_set, huffman=True) == second_result
+ assert list(e.header_table.dynamic_entries) == [
+ (n.encode('utf-8'), v.encode('utf-8'))
+ for n, v in second_header_table
+ ]
+
+ third_header_set = [
+ (':method', 'GET',),
+ (':scheme', 'https',),
+ (':path', '/index.html',),
+ (':authority', 'www.example.com',),
+ ('custom-key', 'custom-value'),
+ ]
+ third_result = (
+ b'\x82\x87\x85\xbf'
+ b'@\x88%\xa8I\xe9[\xa9}\x7f\x89%\xa8I\xe9[\xb8\xe8\xb4\xbf'
+ )
+
+ assert e.encode(third_header_set, huffman=True) == third_result
+ assert len(e.header_table.dynamic_entries) == 3
+
+ # These tests are custom, for hyper.
+ def test_resizing_header_table(self):
+ # We need to encode a substantial number of headers, to populate the
+ # header table.
+ e = Encoder()
+ header_set = [
+ (':method', 'GET'),
+ (':scheme', 'https'),
+ (':path', '/some/path'),
+ (':authority', 'www.example.com'),
+ ('custom-key', 'custom-value'),
+ (
+ "user-agent",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) "
+ "Gecko/20100101 Firefox/16.0",
+ ),
+ (
+ "accept",
+ "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;"
+ "q=0.8",
+ ),
+ ('X-Lukasa-Test', '88989'),
+ ]
+ e.encode(header_set, huffman=True)
+
+ # Resize the header table to a size so small that nothing can be in it.
+ e.header_table_size = 40
+ assert len(e.header_table.dynamic_entries) == 0
+
+ def test_resizing_header_table_sends_multiple_updates(self):
+ e = Encoder()
+
+ e.header_table_size = 40
+ e.header_table_size = 100
+ e.header_table_size = 40
+
+ header_set = [(':method', 'GET')]
+ out = e.encode(header_set, huffman=True)
+ assert out == b'\x3F\x09\x3F\x45\x3F\x09\x82'
+
+ def test_resizing_header_table_to_same_size_ignored(self):
+ e = Encoder()
+
+ # These size changes should be ignored
+ e.header_table_size = 4096
+ e.header_table_size = 4096
+ e.header_table_size = 4096
+
+ # These size changes should be encoded
+ e.header_table_size = 40
+ e.header_table_size = 100
+ e.header_table_size = 40
+
+ header_set = [(':method', 'GET')]
+ out = e.encode(header_set, huffman=True)
+ assert out == b'\x3F\x09\x3F\x45\x3F\x09\x82'
+
+ def test_resizing_header_table_sends_context_update(self):
+ e = Encoder()
+
+ # Resize the header table to a size so small that nothing can be in it.
+ e.header_table_size = 40
+
+ # Now, encode a header set. Just a small one, with a well-defined
+ # output.
+ header_set = [(':method', 'GET')]
+ out = e.encode(header_set, huffman=True)
+
+ assert out == b'?\t\x82'
+
+ def test_setting_table_size_to_the_same_does_nothing(self):
+ e = Encoder()
+
+ # Set the header table size to the default.
+ e.header_table_size = 4096
+
+ # Now encode a header set. Just a small one, with a well-defined
+ # output.
+ header_set = [(':method', 'GET')]
+ out = e.encode(header_set, huffman=True)
+
+ assert out == b'\x82'
+
+ def test_evicting_header_table_objects(self):
+ e = Encoder()
+
+ # Set the header table size large enough to include one header.
+ e.header_table_size = 66
+ header_set = [('a', 'b'), ('long-custom-header', 'longish value')]
+ e.encode(header_set)
+
+ assert len(e.header_table.dynamic_entries) == 1
+
+
+class TestHPACKDecoder(object):
+ # These tests are stolen entirely from the IETF specification examples.
+ def test_literal_header_field_with_indexing(self):
+ """
+ The header field representation uses a literal name and a literal
+ value.
+ """
+ d = Decoder()
+ header_set = [('custom-key', 'custom-header')]
+ data = b'\x40\x0acustom-key\x0dcustom-header'
+
+ assert d.decode(data) == header_set
+ assert list(d.header_table.dynamic_entries) == [
+ (n.encode('utf-8'), v.encode('utf-8')) for n, v in header_set
+ ]
+
+ def test_raw_decoding(self):
+ """
+ The header field representation is decoded as a raw byte string instead
+ of UTF-8
+ """
+ d = Decoder()
+ header_set = [
+ (b'\x00\x01\x99\x30\x11\x22\x55\x21\x89\x14', b'custom-header')
+ ]
+ data = (
+ b'\x40\x0a\x00\x01\x99\x30\x11\x22\x55\x21\x89\x14\x0d'
+ b'custom-header'
+ )
+
+ assert d.decode(data, raw=True) == header_set
+
+ def test_literal_header_field_without_indexing(self):
+ """
+ The header field representation uses an indexed name and a literal
+ value.
+ """
+ d = Decoder()
+ header_set = [(':path', '/sample/path')]
+ data = b'\x04\x0c/sample/path'
+
+ assert d.decode(data) == header_set
+ assert list(d.header_table.dynamic_entries) == []
+
+ def test_header_table_size_getter(self):
+ d = Decoder()
+ assert d.header_table_size
+
+ def test_indexed_header_field(self):
+ """
+ The header field representation uses an indexed header field, from
+ the static table.
+ """
+ d = Decoder()
+ header_set = [(':method', 'GET')]
+ data = b'\x82'
+
+ assert d.decode(data) == header_set
+ assert list(d.header_table.dynamic_entries) == []
+
+ def test_request_examples_without_huffman(self):
+ """
+ This section shows several consecutive header sets, corresponding to
+ HTTP requests, on the same connection.
+ """
+ d = Decoder()
+ first_header_set = [
+ (':method', 'GET',),
+ (':scheme', 'http',),
+ (':path', '/',),
+ (':authority', 'www.example.com'),
+ ]
+ # The first_header_table doesn't contain 'authority'
+ first_data = b'\x82\x86\x84\x01\x0fwww.example.com'
+
+ assert d.decode(first_data) == first_header_set
+ assert list(d.header_table.dynamic_entries) == []
+
+ # This request takes advantage of the differential encoding of header
+ # sets.
+ second_header_set = [
+ (':method', 'GET',),
+ (':scheme', 'http',),
+ (':path', '/',),
+ (':authority', 'www.example.com',),
+ ('cache-control', 'no-cache'),
+ ]
+ second_data = (
+ b'\x82\x86\x84\x01\x0fwww.example.com\x0f\t\x08no-cache'
+ )
+
+ assert d.decode(second_data) == second_header_set
+ assert list(d.header_table.dynamic_entries) == []
+
+ third_header_set = [
+ (':method', 'GET',),
+ (':scheme', 'https',),
+ (':path', '/index.html',),
+ (':authority', 'www.example.com',),
+ ('custom-key', 'custom-value'),
+ ]
+ third_data = (
+ b'\x82\x87\x85\x01\x0fwww.example.com@\ncustom-key\x0ccustom-value'
+ )
+
+ assert d.decode(third_data) == third_header_set
+ # Don't check the header table here, it's just too complex to be
+ # reliable. Check its length though.
+ assert len(d.header_table.dynamic_entries) == 1
+
+ def test_request_examples_with_huffman(self):
+ """
+ This section shows the same examples as the previous section, but
+ using Huffman encoding for the literal values.
+ """
+ d = Decoder()
+
+ first_header_set = [
+ (':method', 'GET',),
+ (':scheme', 'http',),
+ (':path', '/',),
+ (':authority', 'www.example.com'),
+ ]
+ first_data = (
+ b'\x82\x86\x84\x01\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff'
+ )
+
+ assert d.decode(first_data) == first_header_set
+ assert list(d.header_table.dynamic_entries) == []
+
+ second_header_set = [
+ (':method', 'GET',),
+ (':scheme', 'http',),
+ (':path', '/',),
+ (':authority', 'www.example.com',),
+ ('cache-control', 'no-cache'),
+ ]
+ second_data = (
+ b'\x82\x86\x84\x01\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff'
+ b'\x0f\t\x86\xa8\xeb\x10d\x9c\xbf'
+ )
+
+ assert d.decode(second_data) == second_header_set
+ assert list(d.header_table.dynamic_entries) == []
+
+ third_header_set = [
+ (':method', 'GET',),
+ (':scheme', 'https',),
+ (':path', '/index.html',),
+ (':authority', 'www.example.com',),
+ ('custom-key', 'custom-value'),
+ ]
+ third_data = (
+ b'\x82\x87\x85\x01\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff@'
+ b'\x88%\xa8I\xe9[\xa9}\x7f\x89%\xa8I\xe9[\xb8\xe8\xb4\xbf'
+ )
+
+ assert d.decode(third_data) == third_header_set
+ assert len(d.header_table.dynamic_entries) == 1
+
+ # These tests are custom, for hyper.
+ def test_resizing_header_table(self):
+ # We need to decode a substantial number of headers, to populate the
+ # header table. This string isn't magic: it's the output from the
+ # equivalent test for the Encoder.
+ d = Decoder()
+ data = (
+ b'\x82\x87D\x87a\x07\xa4\xacV4\xcfA\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0'
+ b'\xab\x90\xf4\xff@\x88%\xa8I\xe9[\xa9}\x7f\x89%\xa8I\xe9[\xb8\xe8'
+ b'\xb4\xbfz\xbc\xd0\x7ff\xa2\x81\xb0\xda\xe0S\xfa\xd02\x1a\xa4\x9d'
+ b'\x13\xfd\xa9\x92\xa4\x96\x854\x0c\x8aj\xdc\xa7\xe2\x81\x02\xef}'
+ b'\xa9g{\x81qp\x7fjb):\x9d\x81\x00 \x00@\x150\x9a\xc2\xca\x7f,\x05'
+ b'\xc5\xc1S\xb0I|\xa5\x89\xd3M\x1fC\xae\xba\x0cA\xa4\xc7\xa9\x8f3'
+ b'\xa6\x9a?\xdf\x9ah\xfa\x1du\xd0b\r&=Ly\xa6\x8f\xbe\xd0\x01w\xfe'
+ b'\xbeX\xf9\xfb\xed\x00\x17{@\x8a\xfc[=\xbdF\x81\xad\xbc\xa8O\x84y'
+ b'\xe7\xde\x7f'
+ )
+ d.decode(data)
+
+ # Resize the header table to a size so small that nothing can be in it.
+ d.header_table_size = 40
+ assert len(d.header_table.dynamic_entries) == 0
+
+ def test_apache_trafficserver(self):
+ # This test reproduces the bug in #110, using exactly the same header
+ # data.
+ d = Decoder()
+ data = (
+ b'\x10\x07:status\x03200@\x06server\tATS/6.0.0'
+ b'@\x04date\x1dTue, 31 Mar 2015 08:09:51 GMT'
+ b'@\x0ccontent-type\ttext/html@\x0econtent-length\x0542468'
+ b'@\rlast-modified\x1dTue, 31 Mar 2015 01:55:51 GMT'
+ b'@\x04vary\x0fAccept-Encoding@\x04etag\x0f"5519fea7-a5e4"'
+ b'@\x08x-served\x05Nginx@\x14x-subdomain-tryfiles\x04True'
+ b'@\x07x-deity\thydra-lts@\raccept-ranges\x05bytes@\x03age\x010'
+ b'@\x19strict-transport-security\rmax-age=86400'
+ b'@\x03via2https/1.1 ATS (ApacheTrafficServer/6.0.0 [cSsNfU])'
+ )
+ expect = [
+ (':status', '200'),
+ ('server', 'ATS/6.0.0'),
+ ('date', 'Tue, 31 Mar 2015 08:09:51 GMT'),
+ ('content-type', 'text/html'),
+ ('content-length', '42468'),
+ ('last-modified', 'Tue, 31 Mar 2015 01:55:51 GMT'),
+ ('vary', 'Accept-Encoding'),
+ ('etag', '"5519fea7-a5e4"'),
+ ('x-served', 'Nginx'),
+ ('x-subdomain-tryfiles', 'True'),
+ ('x-deity', 'hydra-lts'),
+ ('accept-ranges', 'bytes'),
+ ('age', '0'),
+ ('strict-transport-security', 'max-age=86400'),
+ ('via', 'https/1.1 ATS (ApacheTrafficServer/6.0.0 [cSsNfU])'),
+ ]
+
+ result = d.decode(data)
+
+ assert result == expect
+ # The status header shouldn't be indexed.
+ assert len(d.header_table.dynamic_entries) == len(expect) - 1
+
+ def test_utf8_errors_raise_hpack_decoding_error(self):
+ d = Decoder()
+
+ # Invalid UTF-8 data.
+ data = b'\x82\x86\x84\x01\x10www.\x07\xaa\xd7\x95\xd7\xa8\xd7\x94.com'
+
+ with pytest.raises(HPACKDecodingError):
+ d.decode(data)
+
+ def test_invalid_indexed_literal(self):
+ d = Decoder()
+
+ # Refer to an index that is too large.
+ data = b'\x82\x86\x84\x7f\x0a\x0fwww.example.com'
+ with pytest.raises(InvalidTableIndex):
+ d.decode(data)
+
+ def test_invalid_indexed_header(self):
+ d = Decoder()
+
+ # Refer to an indexed header that is too large.
+ data = b'\xBE\x86\x84\x01\x0fwww.example.com'
+ with pytest.raises(InvalidTableIndex):
+ d.decode(data)
+
+ def test_literal_header_field_with_indexing_emits_headertuple(self):
+ """
+ A header field with indexing emits a HeaderTuple.
+ """
+ d = Decoder()
+ data = b'\x00\x0acustom-key\x0dcustom-header'
+
+ headers = d.decode(data)
+ assert len(headers) == 1
+
+ header = headers[0]
+ assert isinstance(header, HeaderTuple)
+ assert not isinstance(header, NeverIndexedHeaderTuple)
+
+ def test_literal_never_indexed_emits_neverindexedheadertuple(self):
+ """
+ A literal header field that must never be indexed emits a
+ NeverIndexedHeaderTuple.
+ """
+ d = Decoder()
+ data = b'\x10\x0acustom-key\x0dcustom-header'
+
+ headers = d.decode(data)
+ assert len(headers) == 1
+
+ header = headers[0]
+ assert isinstance(header, NeverIndexedHeaderTuple)
+
+ def test_indexed_never_indexed_emits_neverindexedheadertuple(self):
+ """
+ A header field with an indexed name that must never be indexed emits a
+ NeverIndexedHeaderTuple.
+ """
+ d = Decoder()
+ data = b'\x14\x0c/sample/path'
+
+ headers = d.decode(data)
+ assert len(headers) == 1
+
+ header = headers[0]
+ assert isinstance(header, NeverIndexedHeaderTuple)
+
+ def test_max_header_list_size(self):
+ """
+ If the header block is larger than the max_header_list_size, the HPACK
+ decoder throws an OversizedHeaderListError.
+ """
+ d = Decoder(max_header_list_size=44)
+ data = b'\x14\x0c/sample/path'
+
+ with pytest.raises(OversizedHeaderListError):
+ d.decode(data)
+
+ def test_can_decode_multiple_header_table_size_changes(self):
+ """
+ If multiple header table size changes are sent in at once, they are
+ successfully decoded.
+ """
+ d = Decoder()
+ data = b'?a?\xe1\x1f\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99'
+ expect = [
+ (':method', 'GET'),
+ (':scheme', 'https'),
+ (':path', '/'),
+ (':authority', '127.0.0.1:8443')
+ ]
+
+ assert d.decode(data) == expect
+
+ def test_header_table_size_change_above_maximum(self):
+ """
+ If a header table size change is received that exceeds the maximum
+ allowed table size, it is rejected.
+ """
+ d = Decoder()
+ d.max_allowed_table_size = 127
+ data = b'?a\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99'
+
+ with pytest.raises(InvalidTableSizeError):
+ d.decode(data)
+
+ def test_table_size_not_adjusting(self):
+ """
+ If the header table size is shrunk, and then the remote peer doesn't
+ join in the shrinking, then an error is raised.
+ """
+ d = Decoder()
+ d.max_allowed_table_size = 128
+ data = b'\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99'
+
+ with pytest.raises(InvalidTableSizeError):
+ d.decode(data)
+
+ def test_table_size_last_rejected(self):
+ """
+ If a header table size change comes last in the header block, it is
+ forbidden.
+ """
+ d = Decoder()
+ data = b'\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99?a'
+
+ with pytest.raises(HPACKDecodingError):
+ d.decode(data)
+
+ def test_table_size_middle_rejected(self):
+ """
+ If a header table size change comes anywhere but first in the header
+ block, it is forbidden.
+ """
+ d = Decoder()
+ data = b'\x82?a\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99'
+
+ with pytest.raises(HPACKDecodingError):
+ d.decode(data)
+
+ def test_truncated_header_name(self):
+ """
+ If a header name is truncated an error is raised.
+ """
+ d = Decoder()
+ # This is a simple header block that has a bad ending. The interesting
+ # part begins on the second line. This indicates a string that has
+ # literal name and value. The name is a 5 character huffman-encoded
+ # string that is only three bytes long.
+ data = (
+ b'\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99'
+ b'\x00\x85\xf2\xb2J'
+ )
+
+ with pytest.raises(HPACKDecodingError):
+ d.decode(data)
+
+ def test_truncated_header_value(self):
+ """
+ If a header value is truncated an error is raised.
+ """
+ d = Decoder()
+ # This is a simple header block that has a bad ending. The interesting
+ # part begins on the second line. This indicates a string that has
+ # literal name and value. The name is a 5 character huffman-encoded
+ # string, but the entire EOS character has been written over the end.
+ # This causes hpack to see the header value as being supposed to be
+ # 622462 bytes long, which it clearly is not, and so this must fail.
+ data = (
+ b'\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99'
+ b'\x00\x85\xf2\xb2J\x87\xff\xff\xff\xfd%B\x7f'
+ )
+
+ with pytest.raises(HPACKDecodingError):
+ d.decode(data)
+
+
+class TestDictToIterable(object):
+ """
+ The dict_to_iterable function has some subtle requirements: validates that
+ everything behaves as expected.
+
+ As much as possible this tries to be exhaustive.
+ """
+ keys = one_of(
+ text().filter(lambda k: k and not k.startswith(u':')),
+ binary().filter(lambda k: k and not k.startswith(b':'))
+ )
+
+ @given(
+ special_keys=sets(keys),
+ boring_keys=sets(keys),
+ )
+ def test_ordering(self, special_keys, boring_keys):
+ """
+ _dict_to_iterable produces an iterable where all the keys beginning
+ with a colon are emitted first.
+ """
+ def _prepend_colon(k):
+ if isinstance(k, unicode):
+ return u':' + k
+ else:
+ return b':' + k
+
+ special_keys = set(map(_prepend_colon, special_keys))
+ input_dict = {
+ k: b'testval' for k in itertools.chain(
+ special_keys,
+ boring_keys
+ )
+ }
+ filtered = _dict_to_iterable(input_dict)
+
+ received_special = set()
+ received_boring = set()
+
+ for _ in special_keys:
+ k, _ = next(filtered)
+ received_special.add(k)
+ for _ in boring_keys:
+ k, _ = next(filtered)
+ received_boring.add(k)
+
+ assert special_keys == received_special
+ assert boring_keys == received_boring
+
+ @given(
+ special_keys=sets(keys),
+ boring_keys=sets(keys),
+ )
+ def test_ordering_applies_to_encoding(self, special_keys, boring_keys):
+ """
+ When encoding a dictionary the special keys all appear first.
+ """
+ def _prepend_colon(k):
+ if isinstance(k, unicode):
+ return u':' + k
+ else:
+ return b':' + k
+
+ special_keys = set(map(_prepend_colon, special_keys))
+ input_dict = {
+ k: b'testval' for k in itertools.chain(
+ special_keys,
+ boring_keys
+ )
+ }
+ e = Encoder()
+ d = Decoder()
+ encoded = e.encode(input_dict)
+ decoded = iter(d.decode(encoded, raw=True))
+
+ received_special = set()
+ received_boring = set()
+ expected_special = set(map(_to_bytes, special_keys))
+ expected_boring = set(map(_to_bytes, boring_keys))
+
+ for _ in special_keys:
+ k, _ = next(decoded)
+ received_special.add(k)
+ for _ in boring_keys:
+ k, _ = next(decoded)
+ received_boring.add(k)
+
+ assert expected_special == received_special
+ assert expected_boring == received_boring
diff --git a/testing/web-platform/tests/tools/third_party/hpack/test/test_hpack_integration.py b/testing/web-platform/tests/tools/third_party/hpack/test/test_hpack_integration.py
new file mode 100644
index 0000000000..8b8de650d2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/test/test_hpack_integration.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+"""
+This module defines substantial HPACK integration tests. These can take a very
+long time to run, so they're outside the main test suite, but they need to be
+run before every change to HPACK.
+"""
+from hpack.hpack import Decoder, Encoder
+from hpack.struct import HeaderTuple
+from binascii import unhexlify
+from pytest import skip
+
+
+class TestHPACKDecoderIntegration(object):
+ def test_can_decode_a_story(self, story):
+ d = Decoder()
+
+ # We test against draft 9 of the HPACK spec.
+ if story['draft'] != 9:
+ skip("We test against draft 9, not draft %d" % story['draft'])
+
+ for case in story['cases']:
+ try:
+ d.header_table_size = case['header_table_size']
+ except KeyError:
+ pass
+ decoded_headers = d.decode(unhexlify(case['wire']))
+
+ # The correct headers are a list of dicts, which is annoying.
+ correct_headers = [
+ (item[0], item[1])
+ for header in case['headers']
+ for item in header.items()
+ ]
+ correct_headers = correct_headers
+ assert correct_headers == decoded_headers
+ assert all(
+ isinstance(header, HeaderTuple) for header in decoded_headers
+ )
+
+ def test_can_encode_a_story_no_huffman(self, raw_story):
+ d = Decoder()
+ e = Encoder()
+
+ for case in raw_story['cases']:
+ # The input headers are a list of dicts, which is annoying.
+ input_headers = [
+ (item[0], item[1])
+ for header in case['headers']
+ for item in header.items()
+ ]
+
+ encoded = e.encode(input_headers, huffman=False)
+ decoded_headers = d.decode(encoded)
+
+ assert input_headers == decoded_headers
+ assert all(
+ isinstance(header, HeaderTuple) for header in decoded_headers
+ )
+
+ def test_can_encode_a_story_with_huffman(self, raw_story):
+ d = Decoder()
+ e = Encoder()
+
+ for case in raw_story['cases']:
+ # The input headers are a list of dicts, which is annoying.
+ input_headers = [
+ (item[0], item[1])
+ for header in case['headers']
+ for item in header.items()
+ ]
+
+ encoded = e.encode(input_headers, huffman=True)
+ decoded_headers = d.decode(encoded)
+
+ assert input_headers == decoded_headers
diff --git a/testing/web-platform/tests/tools/third_party/hpack/test/test_huffman.py b/testing/web-platform/tests/tools/third_party/hpack/test/test_huffman.py
new file mode 100644
index 0000000000..1b8c2f1238
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/test/test_huffman.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+from hpack.exceptions import HPACKDecodingError
+from hpack.huffman_table import decode_huffman
+from hpack.huffman import HuffmanEncoder
+from hpack.huffman_constants import REQUEST_CODES, REQUEST_CODES_LENGTH
+
+from hypothesis import given, example
+from hypothesis.strategies import binary
+
+
+class TestHuffman(object):
+
+ def test_request_huffman_decoder(self):
+ assert (
+ decode_huffman(b'\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff') ==
+ b"www.example.com"
+ )
+ assert decode_huffman(b'\xa8\xeb\x10d\x9c\xbf') == b"no-cache"
+ assert decode_huffman(b'%\xa8I\xe9[\xa9}\x7f') == b"custom-key"
+ assert (
+ decode_huffman(b'%\xa8I\xe9[\xb8\xe8\xb4\xbf') == b"custom-value"
+ )
+
+ def test_request_huffman_encode(self):
+ encoder = HuffmanEncoder(REQUEST_CODES, REQUEST_CODES_LENGTH)
+ assert (
+ encoder.encode(b"www.example.com") ==
+ b'\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff'
+ )
+ assert encoder.encode(b"no-cache") == b'\xa8\xeb\x10d\x9c\xbf'
+ assert encoder.encode(b"custom-key") == b'%\xa8I\xe9[\xa9}\x7f'
+ assert (
+ encoder.encode(b"custom-value") == b'%\xa8I\xe9[\xb8\xe8\xb4\xbf'
+ )
+
+
+class TestHuffmanDecoder(object):
+ @given(data=binary())
+ @example(b'\xff')
+ @example(b'\x5f\xff\xff\xff\xff')
+ @example(b'\x00\x3f\xff\xff\xff')
+ def test_huffman_decoder_properly_handles_all_bytestrings(self, data):
+ """
+ When given random bytestrings, either we get HPACKDecodingError or we
+ get a bytestring back.
+ """
+ # The examples aren't special, they're just known to hit specific error
+ # paths through the state machine. Basically, they are strings that are
+ # definitely invalid.
+ try:
+ result = decode_huffman(data)
+ except HPACKDecodingError:
+ result = b''
+
+ assert isinstance(result, bytes)
diff --git a/testing/web-platform/tests/tools/third_party/hpack/test/test_struct.py b/testing/web-platform/tests/tools/third_party/hpack/test/test_struct.py
new file mode 100644
index 0000000000..613b8c6bae
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/test/test_struct.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+"""
+test_struct
+~~~~~~~~~~~
+
+Tests for the Header tuples.
+"""
+import pytest
+
+from hpack.struct import HeaderTuple, NeverIndexedHeaderTuple
+
+
+class TestHeaderTuple(object):
+ def test_is_tuple(self):
+ """
+ HeaderTuple objects are tuples.
+ """
+ h = HeaderTuple('name', 'value')
+ assert isinstance(h, tuple)
+
+ def test_unpacks_properly(self):
+ """
+ HeaderTuple objects unpack like tuples.
+ """
+ h = HeaderTuple('name', 'value')
+ k, v = h
+
+ assert k == 'name'
+ assert v == 'value'
+
+ def test_header_tuples_are_indexable(self):
+ """
+ HeaderTuple objects can be indexed.
+ """
+ h = HeaderTuple('name', 'value')
+ assert h.indexable
+
+ def test_never_indexed_tuples_are_not_indexable(self):
+ """
+ NeverIndexedHeaderTuple objects cannot be indexed.
+ """
+ h = NeverIndexedHeaderTuple('name', 'value')
+ assert not h.indexable
+
+ @pytest.mark.parametrize('cls', (HeaderTuple, NeverIndexedHeaderTuple))
+ def test_equal_to_tuples(self, cls):
+ """
+ HeaderTuples and NeverIndexedHeaderTuples are equal to equivalent
+ tuples.
+ """
+ t1 = ('name', 'value')
+ t2 = cls('name', 'value')
+
+ assert t1 == t2
+ assert t1 is not t2
+
+ @pytest.mark.parametrize('cls', (HeaderTuple, NeverIndexedHeaderTuple))
+ def test_equal_to_self(self, cls):
+ """
+ HeaderTuples and NeverIndexedHeaderTuples are always equal when
+ compared to the same class.
+ """
+ t1 = cls('name', 'value')
+ t2 = cls('name', 'value')
+
+ assert t1 == t2
+ assert t1 is not t2
+
+ def test_equal_for_different_indexes(self):
+ """
+ HeaderTuples compare equal to equivalent NeverIndexedHeaderTuples.
+ """
+ t1 = HeaderTuple('name', 'value')
+ t2 = NeverIndexedHeaderTuple('name', 'value')
+
+ assert t1 == t2
+ assert t1 is not t2
diff --git a/testing/web-platform/tests/tools/third_party/hpack/test/test_table.py b/testing/web-platform/tests/tools/third_party/hpack/test/test_table.py
new file mode 100644
index 0000000000..d77c30a2fb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hpack/test/test_table.py
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+from hpack.table import HeaderTable, table_entry_size
+from hpack.exceptions import InvalidTableIndex
+import pytest
+import sys
+_ver = sys.version_info
+is_py2 = _ver[0] == 2
+is_py3 = _ver[0] == 3
+
+
+class TestPackageFunctions(object):
+ def test_table_entry_size(self):
+ res = table_entry_size(b'TestName', b'TestValue')
+ assert res == 49
+
+
+class TestHeaderTable(object):
+ def test_get_by_index_dynamic_table(self):
+ tbl = HeaderTable()
+ off = len(HeaderTable.STATIC_TABLE)
+ val = (b'TestName', b'TestValue')
+ tbl.add(*val)
+ res = tbl.get_by_index(off + 1)
+ assert res == val
+
+ def test_get_by_index_static_table(self):
+ tbl = HeaderTable()
+ exp = (b':authority', b'')
+ res = tbl.get_by_index(1)
+ assert res == exp
+ idx = len(HeaderTable.STATIC_TABLE)
+ exp = (b'www-authenticate', b'')
+ res = tbl.get_by_index(idx)
+ assert res == exp
+
+ def test_get_by_index_zero_index(self):
+ tbl = HeaderTable()
+ with pytest.raises(InvalidTableIndex):
+ tbl.get_by_index(0)
+
+ def test_get_by_index_out_of_range(self):
+ tbl = HeaderTable()
+ off = len(HeaderTable.STATIC_TABLE)
+ tbl.add(b'TestName', b'TestValue')
+ with pytest.raises(InvalidTableIndex) as e:
+ tbl.get_by_index(off + 2)
+
+ assert (
+ "InvalidTableIndex: Invalid table index %d" % (off + 2) in str(e)
+ )
+
+ def test_repr(self):
+ tbl = HeaderTable()
+ tbl.add(b'TestName1', b'TestValue1')
+ tbl.add(b'TestName2', b'TestValue2')
+ tbl.add(b'TestName2', b'TestValue2')
+ # Meh, I hate that I have to do this to test
+ # repr
+ if is_py3:
+ exp = (
+ "HeaderTable(4096, False, deque(["
+ "(b'TestName2', b'TestValue2'), "
+ "(b'TestName2', b'TestValue2'), "
+ "(b'TestName1', b'TestValue1')"
+ "]))"
+ )
+ else:
+ exp = (
+ "HeaderTable(4096, False, deque(["
+ "('TestName2', 'TestValue2'), "
+ "('TestName2', 'TestValue2'), "
+ "('TestName1', 'TestValue1')"
+ "]))"
+ )
+ res = repr(tbl)
+ assert res == exp
+
+ def test_add_to_large(self):
+ tbl = HeaderTable()
+ # Max size to small to hold the value we specify
+ tbl.maxsize = 1
+ tbl.add(b'TestName', b'TestValue')
+ # Table length should be 0
+ assert len(tbl.dynamic_entries) == 0
+
+ def test_search_in_static_full(self):
+ tbl = HeaderTable()
+ itm = (b':authority', b'')
+ exp = (1, itm[0], itm[1])
+ res = tbl.search(itm[0], itm[1])
+ assert res == exp
+
+ def test_search_in_static_partial(self):
+ tbl = HeaderTable()
+ exp = (1, b':authority', None)
+ res = tbl.search(b':authority', b'NotInTable')
+ assert res == exp
+
+ def test_search_in_dynamic_full(self):
+ tbl = HeaderTable()
+ idx = len(HeaderTable.STATIC_TABLE) + 1
+ tbl.add(b'TestName', b'TestValue')
+ exp = (idx, b'TestName', b'TestValue')
+ res = tbl.search(b'TestName', b'TestValue')
+ assert res == exp
+
+ def test_search_in_dynamic_partial(self):
+ tbl = HeaderTable()
+ idx = len(HeaderTable.STATIC_TABLE) + 1
+ tbl.add(b'TestName', b'TestValue')
+ exp = (idx, b'TestName', None)
+ res = tbl.search(b'TestName', b'NotInTable')
+ assert res == exp
+
+ def test_search_no_match(self):
+ tbl = HeaderTable()
+ tbl.add(b'TestName', b'TestValue')
+ res = tbl.search(b'NotInTable', b'NotInTable')
+ assert res is None
+
+ def test_maxsize_prop_getter(self):
+ tbl = HeaderTable()
+ assert tbl.maxsize == HeaderTable.DEFAULT_SIZE
+
+ def test_maxsize_prop_setter(self):
+ tbl = HeaderTable()
+ exp = int(HeaderTable.DEFAULT_SIZE / 2)
+ tbl.maxsize = exp
+ assert tbl.resized is True
+ assert tbl.maxsize == exp
+ tbl.resized = False
+ tbl.maxsize = exp
+ assert tbl.resized is False
+ assert tbl.maxsize == exp
+
+ def test_size(self):
+ tbl = HeaderTable()
+ for i in range(3):
+ tbl.add(b'TestName', b'TestValue')
+ res = tbl._current_size
+ assert res == 147
+
+ def test_shrink_maxsize_is_zero(self):
+ tbl = HeaderTable()
+ tbl.add(b'TestName', b'TestValue')
+ assert len(tbl.dynamic_entries) == 1
+ tbl.maxsize = 0
+ assert len(tbl.dynamic_entries) == 0
+
+ def test_shrink_maxsize(self):
+ tbl = HeaderTable()
+ for i in range(3):
+ tbl.add(b'TestName', b'TestValue')
+
+ assert tbl._current_size == 147
+ tbl.maxsize = 146
+ assert len(tbl.dynamic_entries) == 2
+ assert tbl._current_size == 98
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/.appveyor.yml b/testing/web-platform/tests/tools/third_party/html5lib/.appveyor.yml
new file mode 100644
index 0000000000..984e2b7fa5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/.appveyor.yml
@@ -0,0 +1,31 @@
+# To activate, change the Appveyor settings to use `.appveyor.yml`.
+environment:
+ global:
+ PATH: "C:\\Python27\\Scripts\\;%PATH%"
+ PYTEST_COMMAND: "coverage run -m pytest"
+ matrix:
+ - TOXENV: py27-base
+ - TOXENV: py27-optional
+ - TOXENV: py33-base
+ - TOXENV: py33-optional
+ - TOXENV: py34-base
+ - TOXENV: py34-optional
+ - TOXENV: py35-base
+ - TOXENV: py35-optional
+ - TOXENV: py36-base
+ - TOXENV: py36-optional
+
+install:
+ - git submodule update --init --recursive
+ - python -m pip install tox codecov
+
+build: off
+
+test_script:
+ - tox
+
+after_test:
+ - python debug-info.py
+
+on_success:
+ - codecov
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/.coveragerc b/testing/web-platform/tests/tools/third_party/html5lib/.coveragerc
new file mode 100644
index 0000000000..6facf35239
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/.coveragerc
@@ -0,0 +1,8 @@
+[run]
+branch = True
+source = html5lib
+
+[paths]
+source =
+ html5lib
+ .tox/*/lib/python*/site-packages/html5lib
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/.gitignore b/testing/web-platform/tests/tools/third_party/html5lib/.gitignore
new file mode 100644
index 0000000000..ecd62df31b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/.gitignore
@@ -0,0 +1,85 @@
+# Copyright (c) 2014 GitHub, Inc.
+#
+# 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.
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+doc/_build/
+
+# PyBuilder
+target/
+
+# Generated by parse.py -p
+stats.prof
+
+# IDE
+.idea
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/.prospector.yaml b/testing/web-platform/tests/tools/third_party/html5lib/.prospector.yaml
new file mode 100644
index 0000000000..7e8efe1a62
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/.prospector.yaml
@@ -0,0 +1,21 @@
+strictness: veryhigh
+doc-warnings: false
+test-warnings: false
+
+max-line-length: 139
+
+requirements:
+ - requirements.txt
+ - requirements-test.txt
+ - requirements-optional.txt
+
+ignore-paths:
+ - parse.py
+ - utils/
+
+python-targets:
+ - 2
+ - 3
+
+mccabe:
+ run: false
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/.pylintrc b/testing/web-platform/tests/tools/third_party/html5lib/.pylintrc
new file mode 100644
index 0000000000..ea74d5db3f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/.pylintrc
@@ -0,0 +1,10 @@
+[MASTER]
+ignore=tests
+
+[MESSAGES CONTROL]
+# messages up to fixme should probably be fixed somehow
+disable = redefined-builtin,attribute-defined-outside-init,anomalous-backslash-in-string,no-self-use,redefined-outer-name,bad-continuation,wrong-import-order,superfluous-parens,no-member,duplicate-code,super-init-not-called,abstract-method,property-on-old-class,wrong-import-position,no-name-in-module,no-init,bad-mcs-classmethod-argument,bad-classmethod-argument,fixme,invalid-name,import-error,too-few-public-methods,too-many-ancestors,too-many-arguments,too-many-boolean-expressions,too-many-branches,too-many-instance-attributes,too-many-locals,too-many-lines,too-many-public-methods,too-many-return-statements,too-many-statements,missing-docstring,line-too-long,locally-disabled,locally-enabled,bad-builtin,deprecated-lambda
+
+[FORMAT]
+max-line-length=139
+single-line-if-stmt=no
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/.pytest.expect b/testing/web-platform/tests/tools/third_party/html5lib/.pytest.expect
new file mode 100644
index 0000000000..0fa326f035
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/.pytest.expect
@@ -0,0 +1,1322 @@
+pytest-expect file v1
+(2, 7, 11, 'final', 0)
+b'html5lib/tests/test_encoding.py::test_encoding::[110]': FAIL
+b'html5lib/tests/test_encoding.py::test_encoding::[111]': FAIL
+u'html5lib/tests/testdata/tokenizer/test2.test::0::dataState': FAIL
+u'html5lib/tests/testdata/tokenizer/test3.test::228::dataState': FAIL
+u'html5lib/tests/testdata/tokenizer/test3.test::231::dataState': FAIL
+u'html5lib/tests/testdata/tokenizer/test3.test::232::dataState': FAIL
+u'html5lib/tests/testdata/tokenizer/test3.test::234::dataState': FAIL
+u'html5lib/tests/testdata/tokenizer/test3.test::235::dataState': FAIL
+u'html5lib/tests/testdata/tokenizer/test3.test::237::dataState': FAIL
+u'html5lib/tests/testdata/tokenizer/test3.test::240::dataState': FAIL
+u'html5lib/tests/testdata/tokenizer/test3.test::241::dataState': FAIL
+u'html5lib/tests/testdata/tokenizer/test3.test::243::dataState': FAIL
+u'html5lib/tests/testdata/tokenizer/test3.test::244::dataState': FAIL
+u'html5lib/tests/testdata/tokenizer/test3.test::246::dataState': FAIL
+u'html5lib/tests/testdata/tokenizer/test3.test::258::dataState': FAIL
+u'html5lib/tests/testdata/tokenizer/test3.test::656::dataState': FAIL
+u'html5lib/tests/testdata/tree-construction/adoption01.dat::17::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/adoption01.dat::17::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/adoption01.dat::17::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/adoption01.dat::17::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/adoption01.dat::17::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/adoption01.dat::17::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/adoption01.dat::17::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/adoption01.dat::17::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::0::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::0::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::0::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::0::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::0::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::0::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::0::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::0::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::18::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::18::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::18::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::18::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::18::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::18::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::18::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::18::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::19::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::19::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::19::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::19::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::19::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::19::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::19::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::19::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::1::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::1::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::1::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::1::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::1::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::1::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::1::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::1::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::22::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::22::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::22::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::22::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::22::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::22::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::22::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::22::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::23::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::23::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::23::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::23::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::23::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::23::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::23::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::23::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::26::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::26::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::26::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::26::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::26::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::26::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::26::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::26::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::27::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::27::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::27::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::27::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::27::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::27::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::27::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::27::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::2::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::2::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::2::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::2::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::2::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::2::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::2::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::2::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::30::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::30::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::30::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::30::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::30::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::30::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::30::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::30::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::31::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::31::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::31::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::31::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::31::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::31::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::31::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::31::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::34::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::34::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::34::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::34::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::34::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::34::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::34::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::34::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::35::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::35::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::35::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::35::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::35::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::35::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::35::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::35::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::38::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::38::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::38::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::38::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::38::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::38::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::38::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::38::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::39::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::39::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::39::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::39::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::39::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::39::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::39::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::39::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::3::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::3::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::3::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::3::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::3::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::3::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::3::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::3::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::40::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::40::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::40::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::40::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::40::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::40::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::40::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::40::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::41::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::41::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::41::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::41::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::41::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::41::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::41::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::41::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::47::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::47::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::47::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::47::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::47::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::47::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::47::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::47::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::48::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::48::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::48::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::48::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::48::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::48::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::48::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/foreign-fragment.dat::48::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::0::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::0::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::0::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::0::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::0::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::0::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::0::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::0::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::1::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::1::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::1::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::1::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::1::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::1::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::1::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::1::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::2::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::2::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::2::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::2::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::2::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::2::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::2::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::2::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::3::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::3::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::3::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::3::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::3::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::3::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::3::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/isindex.dat::3::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::3::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::3::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::3::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::3::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::3::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::3::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::3::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::3::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::4::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::4::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::4::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::4::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::4::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::4::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::4::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::4::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::5::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::5::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::5::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::5::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::5::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::5::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::5::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/menuitem-element.dat::5::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/namespace-sensitivity.dat::0::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/namespace-sensitivity.dat::0::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/namespace-sensitivity.dat::0::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/namespace-sensitivity.dat::0::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/namespace-sensitivity.dat::0::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/namespace-sensitivity.dat::0::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/namespace-sensitivity.dat::0::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/namespace-sensitivity.dat::0::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::0::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::0::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::0::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::0::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::0::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::0::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::0::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::0::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::10::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::10::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::10::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::10::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::10::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::10::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::10::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::10::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::12::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::12::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::12::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::12::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::12::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::12::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::12::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::12::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::15::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::15::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::15::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::15::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::15::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::15::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::15::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::15::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::17::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::17::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::17::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::17::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::17::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::17::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::17::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::17::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::1::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::1::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::1::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::1::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::1::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::1::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::1::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::1::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::20::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::20::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::20::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::20::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::20::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::20::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::20::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::20::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::2::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::2::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::2::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::2::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::2::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::2::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::2::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::2::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::3::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::3::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::3::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::3::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::3::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::3::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::3::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::3::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::5::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::5::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::5::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::5::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::5::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::5::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::5::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::5::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::7::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::7::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::7::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::7::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::7::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::7::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::7::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/ruby.dat::7::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/adoption01.dat::0::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/adoption01.dat::0::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/adoption01.dat::0::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/adoption01.dat::0::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/adoption01.dat::0::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/adoption01.dat::0::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/adoption01.dat::0::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/adoption01.dat::0::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/ark.dat::0::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/ark.dat::0::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/ark.dat::0::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/ark.dat::0::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/ark.dat::0::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/ark.dat::0::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/ark.dat::0::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/ark.dat::0::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::0::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::0::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::0::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::0::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::0::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::0::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::0::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::0::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::1::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::1::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::1::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::1::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::1::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::1::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::1::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/scripted/webkit01.dat::1::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::0::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::0::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::0::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::0::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::0::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::0::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::0::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::0::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::100::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::100::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::100::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::100::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::100::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::100::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::100::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::100::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::101::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::101::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::101::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::101::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::101::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::101::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::101::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::101::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::102::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::102::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::102::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::102::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::102::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::102::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::102::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::102::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::103::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::103::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::103::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::103::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::103::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::103::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::103::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::103::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::104::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::104::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::104::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::104::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::104::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::104::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::104::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::104::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::105::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::105::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::105::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::105::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::105::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::105::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::105::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::105::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::106::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::106::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::106::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::106::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::106::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::106::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::106::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::106::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::107::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::107::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::107::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::107::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::107::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::107::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::107::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::107::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::10::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::10::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::10::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::10::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::10::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::10::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::10::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::10::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::11::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::11::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::11::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::11::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::11::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::11::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::11::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::11::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::12::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::12::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::12::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::12::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::12::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::12::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::12::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::12::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::13::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::13::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::13::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::13::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::13::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::13::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::13::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::13::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::14::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::14::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::14::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::14::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::14::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::14::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::14::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::14::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::15::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::15::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::15::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::15::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::15::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::15::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::15::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::15::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::16::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::16::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::16::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::16::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::16::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::16::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::16::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::16::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::17::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::17::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::17::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::17::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::17::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::17::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::17::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::17::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::18::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::18::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::18::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::18::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::18::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::18::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::18::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::18::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::19::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::19::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::19::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::19::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::19::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::19::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::19::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::19::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::1::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::1::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::1::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::1::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::1::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::1::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::1::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::1::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::20::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::20::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::20::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::20::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::20::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::20::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::20::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::20::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::21::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::21::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::21::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::21::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::21::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::21::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::21::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::21::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::22::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::22::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::22::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::22::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::22::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::22::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::22::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::22::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::23::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::23::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::23::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::23::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::23::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::23::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::23::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::23::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::24::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::24::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::24::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::24::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::24::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::24::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::24::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::24::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::25::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::25::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::25::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::25::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::25::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::25::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::25::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::25::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::26::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::26::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::26::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::26::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::26::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::26::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::26::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::26::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::27::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::27::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::27::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::27::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::27::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::27::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::27::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::27::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::28::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::28::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::28::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::28::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::28::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::28::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::28::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::28::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::29::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::29::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::29::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::29::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::29::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::29::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::29::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::29::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::2::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::2::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::2::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::2::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::2::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::2::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::2::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::2::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::30::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::30::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::30::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::30::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::30::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::30::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::30::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::30::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::31::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::31::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::31::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::31::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::31::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::31::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::31::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::31::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::32::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::32::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::32::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::32::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::32::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::32::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::32::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::32::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::33::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::33::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::33::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::33::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::33::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::33::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::33::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::33::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::34::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::34::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::34::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::34::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::34::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::34::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::34::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::34::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::35::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::35::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::35::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::35::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::35::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::35::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::35::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::35::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::36::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::36::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::36::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::36::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::36::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::36::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::36::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::36::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::37::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::37::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::37::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::37::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::37::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::37::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::37::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::37::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::38::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::38::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::38::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::38::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::38::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::38::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::38::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::38::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::3::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::3::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::3::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::3::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::3::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::3::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::3::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::3::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::40::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::40::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::40::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::40::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::40::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::40::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::40::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::40::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::41::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::41::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::41::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::41::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::41::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::41::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::41::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::41::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::42::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::42::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::42::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::42::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::42::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::42::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::42::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::42::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::43::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::43::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::43::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::43::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::43::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::43::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::43::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::43::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::44::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::44::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::44::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::44::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::44::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::44::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::44::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::44::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::45::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::45::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::45::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::45::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::45::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::45::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::45::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::45::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::46::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::46::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::46::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::46::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::46::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::46::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::46::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::46::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::47::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::47::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::47::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::47::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::47::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::47::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::47::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::47::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::48::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::48::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::48::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::48::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::48::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::48::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::48::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::48::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::49::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::49::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::49::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::49::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::49::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::49::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::49::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::49::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::4::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::4::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::4::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::4::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::4::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::4::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::4::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::4::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::50::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::50::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::50::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::50::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::50::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::50::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::50::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::50::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::51::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::51::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::51::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::51::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::51::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::51::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::51::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::51::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::52::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::52::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::52::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::52::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::52::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::52::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::52::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::52::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::53::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::53::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::53::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::53::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::53::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::53::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::53::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::53::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::54::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::54::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::54::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::54::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::54::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::54::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::54::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::54::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::55::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::55::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::55::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::55::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::55::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::55::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::55::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::55::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::56::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::56::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::56::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::56::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::56::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::56::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::56::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::56::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::57::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::57::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::57::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::57::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::57::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::57::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::57::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::57::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::58::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::58::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::58::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::58::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::58::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::58::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::58::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::58::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::59::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::59::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::59::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::59::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::59::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::59::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::59::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::59::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::5::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::5::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::5::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::5::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::5::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::5::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::5::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::5::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::60::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::60::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::60::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::60::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::60::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::60::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::60::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::60::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::61::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::61::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::61::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::61::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::61::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::61::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::61::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::61::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::62::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::62::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::62::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::62::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::62::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::62::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::62::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::62::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::63::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::63::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::63::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::63::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::63::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::63::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::63::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::63::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::64::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::64::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::64::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::64::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::64::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::64::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::64::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::64::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::65::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::65::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::65::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::65::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::65::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::65::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::65::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::65::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::66::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::66::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::66::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::66::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::66::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::66::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::66::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::66::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::67::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::67::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::67::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::67::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::67::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::67::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::67::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::67::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::68::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::68::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::68::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::68::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::68::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::68::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::68::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::68::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::69::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::69::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::69::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::69::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::69::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::69::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::69::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::69::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::6::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::6::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::6::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::6::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::6::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::6::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::6::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::6::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::70::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::70::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::70::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::70::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::70::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::70::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::70::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::70::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::71::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::71::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::71::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::71::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::71::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::71::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::71::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::71::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::72::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::72::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::72::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::72::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::72::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::72::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::72::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::72::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::73::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::73::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::73::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::73::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::73::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::73::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::73::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::73::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::74::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::74::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::74::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::74::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::74::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::74::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::74::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::74::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::75::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::75::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::75::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::75::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::75::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::75::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::75::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::75::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::76::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::76::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::76::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::76::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::76::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::76::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::76::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::76::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::77::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::77::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::77::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::77::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::77::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::77::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::77::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::77::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::78::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::78::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::78::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::78::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::78::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::78::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::78::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::78::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::79::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::79::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::79::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::79::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::79::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::79::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::79::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::79::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::80::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::80::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::80::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::80::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::80::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::80::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::80::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::80::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::81::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::81::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::81::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::81::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::81::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::81::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::81::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::81::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::82::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::82::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::82::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::82::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::82::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::82::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::82::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::82::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::83::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::83::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::83::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::83::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::83::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::83::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::83::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::83::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::84::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::84::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::84::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::84::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::84::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::84::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::84::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::84::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::85::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::85::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::85::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::85::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::85::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::85::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::85::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::85::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::86::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::86::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::86::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::86::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::86::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::86::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::86::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::86::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::87::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::87::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::87::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::87::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::87::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::87::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::87::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::87::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::88::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::88::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::88::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::88::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::88::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::88::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::88::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::88::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::89::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::89::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::89::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::89::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::89::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::89::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::89::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::89::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::8::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::8::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::8::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::8::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::8::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::8::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::8::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::8::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::90::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::90::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::90::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::90::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::90::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::90::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::90::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::90::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::91::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::91::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::91::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::91::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::91::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::91::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::91::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::91::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::92::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::92::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::92::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::92::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::92::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::92::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::92::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::92::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::93::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::93::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::93::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::93::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::93::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::93::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::93::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::93::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::94::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::94::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::94::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::94::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::94::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::94::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::94::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::94::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::95::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::95::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::95::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::95::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::95::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::95::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::95::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::95::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::96::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::96::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::96::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::96::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::96::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::96::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::96::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::96::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::97::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::97::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::97::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::97::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::97::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::97::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::97::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::97::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::98::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::98::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::98::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::98::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::98::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::98::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::98::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::98::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::99::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::99::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::99::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::99::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::99::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::99::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::99::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::99::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::9::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::9::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::9::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::9::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::9::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::9::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::9::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/template.dat::9::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::2::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::2::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::2::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::2::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::2::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::2::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::2::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::2::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::4::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::4::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::4::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::4::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::4::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::4::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::4::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::4::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::5::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::5::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::5::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::5::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::5::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::5::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::5::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::5::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::6::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::6::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::6::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::6::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::6::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::6::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::6::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests11.dat::6::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::14::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::14::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::14::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::14::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::14::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::14::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::14::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::14::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::17::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::17::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::17::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::17::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::17::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::17::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::17::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::17::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::7::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::7::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::7::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::7::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::7::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::7::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::7::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests19.dat::7::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::6::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::6::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::6::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::6::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::6::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::6::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::6::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::6::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::7::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::7::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::7::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::7::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::7::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::7::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::7::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests2.dat::7::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests25.dat::7::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests25.dat::7::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests25.dat::7::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests25.dat::7::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests25.dat::7::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests25.dat::7::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/tests25.dat::7::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/tests25.dat::7::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::14::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::14::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::14::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::14::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::14::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::14::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::14::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::14::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::15::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::15::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::15::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::15::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::15::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::15::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::15::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::15::lxml::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::16::DOM::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::16::DOM::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::16::ElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::16::ElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::16::cElementTree::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::16::cElementTree::parser::void-namespace': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::16::lxml::parser::namespaced': FAIL
+u'html5lib/tests/testdata/tree-construction/webkit02.dat::16::lxml::parser::void-namespace': FAIL
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/.travis.yml b/testing/web-platform/tests/tools/third_party/html5lib/.travis.yml
new file mode 100644
index 0000000000..5727e0947e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/.travis.yml
@@ -0,0 +1,32 @@
+language: python
+python:
+ - "pypy"
+ - "3.6"
+ - "3.5"
+ - "3.4"
+ - "3.3"
+ - "2.7"
+
+sudo: false
+
+cache: pip
+
+env:
+ global:
+ - PYTEST_COMMAND="coverage run -m pytest"
+ matrix:
+ - TOXENV=optional
+ - TOXENV=base
+ - TOXENV=six19-optional
+
+install:
+ - pip install tox codecov
+
+script:
+ - tox
+
+after_script:
+ - python debug-info.py
+
+after_success:
+ - codecov
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/AUTHORS.rst b/testing/web-platform/tests/tools/third_party/html5lib/AUTHORS.rst
new file mode 100644
index 0000000000..904013908d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/AUTHORS.rst
@@ -0,0 +1,66 @@
+Credits
+=======
+
+``html5lib`` is written and maintained by:
+
+- James Graham
+- Sam Sneddon
+- Łukasz Langa
+- Will Kahn-Greene
+
+
+Patches and suggestions
+-----------------------
+(In chronological order, by first commit:)
+
+- Anne van Kesteren
+- Lachlan Hunt
+- lantis63
+- Sam Ruby
+- Thomas Broyer
+- Tim Fletcher
+- Mark Pilgrim
+- Ryan King
+- Philip Taylor
+- Edward Z. Yang
+- fantasai
+- Philip Jägenstedt
+- Ms2ger
+- Mohammad Taha Jahangir
+- Andy Wingo
+- Andreas Madsack
+- Karim Valiev
+- Juan Carlos Garcia Segovia
+- Mike West
+- Marc DM
+- Simon Sapin
+- Michael[tm] Smith
+- Ritwik Gupta
+- Marc Abramowitz
+- Tony Lopes
+- lilbludevil
+- Kevin
+- Drew Hubl
+- Austin Kumbera
+- Jim Baker
+- Jon Dufresne
+- Donald Stufft
+- Alex Gaynor
+- Nik Nyby
+- Jakub Wilk
+- Sigmund Cherem
+- Gabi Davar
+- Florian Mounier
+- neumond
+- Vitalik Verhovodov
+- Kovid Goyal
+- Adam Chainz
+- John Vandenberg
+- Eric Amorde
+- Benedikt Morbach
+- Jonathan Vanasco
+- Tom Most
+- Ville Skyttä
+- Hugo van Kemenade
+- Mark Vasilkov
+
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/CHANGES.rst b/testing/web-platform/tests/tools/third_party/html5lib/CHANGES.rst
new file mode 100644
index 0000000000..fcb22475cd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/CHANGES.rst
@@ -0,0 +1,359 @@
+Change Log
+----------
+
+1.1
+~~~
+
+UNRELEASED
+
+Breaking changes:
+
+* Drop support for Python 3.3. (#358)
+* Drop support for Python 3.4. (#421)
+
+Deprecations:
+
+* Deprecate the ``html5lib`` sanitizer (``html5lib.serialize(sanitize=True)`` and
+ ``html5lib.filters.sanitizer``). We recommend users migrate to `Bleach
+ <https://github.com/mozilla/bleach>`. Please let us know if Bleach doesn't suffice for your
+ use. (#443)
+
+Other changes:
+
+* Try to import from ``collections.abc`` to remove DeprecationWarning and ensure
+ ``html5lib`` keeps working in future Python versions. (#403)
+* Drop optional ``datrie`` dependency. (#442)
+
+
+1.0.1
+~~~~~
+
+Released on December 7, 2017
+
+Breaking changes:
+
+* Drop support for Python 2.6. (#330) (Thank you, Hugo, Will Kahn-Greene!)
+* Remove ``utils/spider.py`` (#353) (Thank you, Jon Dufresne!)
+
+Features:
+
+* Improve documentation. (#300, #307) (Thank you, Jon Dufresne, Tom Most,
+ Will Kahn-Greene!)
+* Add iframe seamless boolean attribute. (Thank you, Ritwik Gupta!)
+* Add itemscope as a boolean attribute. (#194) (Thank you, Jonathan Vanasco!)
+* Support Python 3.6. (#333) (Thank you, Jon Dufresne!)
+* Add CI support for Windows using AppVeyor. (Thank you, John Vandenberg!)
+* Improve testing and CI and add code coverage (#323, #334), (Thank you, Jon
+ Dufresne, John Vandenberg, Sam Sneddon, Will Kahn-Greene!)
+* Semver-compliant version number.
+
+Bug fixes:
+
+* Add support for setuptools < 18.5 to support environment markers. (Thank you,
+ John Vandenberg!)
+* Add explicit dependency for six >= 1.9. (Thank you, Eric Amorde!)
+* Fix regexes to work with Python 3.7 regex adjustments. (#318, #379) (Thank
+ you, Benedikt Morbach, Ville Skyttä, Mark Vasilkov!)
+* Fix alphabeticalattributes filter namespace bug. (#324) (Thank you, Will
+ Kahn-Greene!)
+* Include license file in generated wheel package. (#350) (Thank you, Jon
+ Dufresne!)
+* Fix annotation-xml typo. (#339) (Thank you, Will Kahn-Greene!)
+* Allow uppercase hex chararcters in CSS colour check. (#377) (Thank you,
+ Komal Dembla, Hugo!)
+
+
+1.0
+~~~
+
+Released and unreleased on December 7, 2017. Badly packaged release.
+
+
+0.999999999/1.0b10
+~~~~~~~~~~~~~~~~~~
+
+Released on July 15, 2016
+
+* Fix attribute order going to the tree builder to be document order
+ instead of reverse document order(!).
+
+
+0.99999999/1.0b9
+~~~~~~~~~~~~~~~~
+
+Released on July 14, 2016
+
+* **Added ordereddict as a mandatory dependency on Python 2.6.**
+
+* Added ``lxml``, ``genshi``, ``datrie``, ``charade``, and ``all``
+ extras that will do the right thing based on the specific
+ interpreter implementation.
+
+* Now requires the ``mock`` package for the testsuite.
+
+* Cease supporting DATrie under PyPy.
+
+* **Remove PullDOM support, as this hasn't ever been properly
+ tested, doesn't entirely work, and as far as I can tell is
+ completely unused by anyone.**
+
+* Move testsuite to ``py.test``.
+
+* **Fix #124: move to webencodings for decoding the input byte stream;
+ this makes html5lib compliant with the Encoding Standard, and
+ introduces a required dependency on webencodings.**
+
+* **Cease supporting Python 3.2 (in both CPython and PyPy forms).**
+
+* **Fix comments containing double-dash with lxml 3.5 and above.**
+
+* **Use scripting disabled by default (as we don't implement
+ scripting).**
+
+* **Fix #11, avoiding the XSS bug potentially caused by serializer
+ allowing attribute values to be escaped out of in old browser versions,
+ changing the quote_attr_values option on serializer to take one of
+ three values, "always" (the old True value), "legacy" (the new option,
+ and the new default), and "spec" (the old False value, and the old
+ default).**
+
+* **Fix #72 by rewriting the sanitizer to apply only to treewalkers
+ (instead of the tokenizer); as such, this will require amending all
+ callers of it to use it via the treewalker API.**
+
+* **Drop support of charade, now that chardet is supported once more.**
+
+* **Replace the charset keyword argument on parse and related methods
+ with a set of keyword arguments: override_encoding, transport_encoding,
+ same_origin_parent_encoding, likely_encoding, and default_encoding.**
+
+* **Move filters._base, treebuilder._base, and treewalkers._base to .base
+ to clarify their status as public.**
+
+* **Get rid of the sanitizer package. Merge sanitizer.sanitize into the
+ sanitizer.htmlsanitizer module and move that to sanitizer. This means
+ anyone who used sanitizer.sanitize or sanitizer.HTMLSanitizer needs no
+ code changes.**
+
+* **Rename treewalkers.lxmletree to .etree_lxml and
+ treewalkers.genshistream to .genshi to have a consistent API.**
+
+* Move a whole load of stuff (inputstream, ihatexml, trie, tokenizer,
+ utils) to be underscore prefixed to clarify their status as private.
+
+
+0.9999999/1.0b8
+~~~~~~~~~~~~~~~
+
+Released on September 10, 2015
+
+* Fix #195: fix the sanitizer to drop broken URLs (it threw an
+ exception between 0.9999 and 0.999999).
+
+
+0.999999/1.0b7
+~~~~~~~~~~~~~~
+
+Released on July 7, 2015
+
+* Fix #189: fix the sanitizer to allow relative URLs again (as it did
+ prior to 0.9999/1.0b5).
+
+
+0.99999/1.0b6
+~~~~~~~~~~~~~
+
+Released on April 30, 2015
+
+* Fix #188: fix the sanitizer to not throw an exception when sanitizing
+ bogus data URLs.
+
+
+0.9999/1.0b5
+~~~~~~~~~~~~
+
+Released on April 29, 2015
+
+* Fix #153: Sanitizer fails to treat some attributes as URLs. Despite how
+ this sounds, this has no known security implications. No known version
+ of IE (5.5 to current), Firefox (3 to current), Safari (6 to current),
+ Chrome (1 to current), or Opera (12 to current) will run any script
+ provided in these attributes.
+
+* Pass error message to the ParseError exception in strict parsing mode.
+
+* Allow data URIs in the sanitizer, with a whitelist of content-types.
+
+* Add support for Python implementations that don't support lone
+ surrogates (read: Jython). Fixes #2.
+
+* Remove localization of error messages. This functionality was totally
+ unused (and untested that everything was localizable), so we may as
+ well follow numerous browsers in not supporting translating technical
+ strings.
+
+* Expose treewalkers.pprint as a public API.
+
+* Add a documentEncoding property to HTML5Parser, fix #121.
+
+
+0.999
+~~~~~
+
+Released on December 23, 2013
+
+* Fix #127: add work-around for CPython issue #20007: .read(0) on
+ http.client.HTTPResponse drops the rest of the content.
+
+* Fix #115: lxml treewalker can now deal with fragments containing, at
+ their root level, text nodes with non-ASCII characters on Python 2.
+
+
+0.99
+~~~~
+
+Released on September 10, 2013
+
+* No library changes from 1.0b3; released as 0.99 as pip has changed
+ behaviour from 1.4 to avoid installing pre-release versions per
+ PEP 440.
+
+
+1.0b3
+~~~~~
+
+Released on July 24, 2013
+
+* Removed ``RecursiveTreeWalker`` from ``treewalkers._base``. Any
+ implementation using it should be moved to
+ ``NonRecursiveTreeWalker``, as everything bundled with html5lib has
+ for years.
+
+* Fix #67 so that ``BufferedStream`` to correctly returns a bytes
+ object, thereby fixing any case where html5lib is passed a
+ non-seekable RawIOBase-like object.
+
+
+1.0b2
+~~~~~
+
+Released on June 27, 2013
+
+* Removed reordering of attributes within the serializer. There is now
+ an ``alphabetical_attributes`` option which preserves the previous
+ behaviour through a new filter. This allows attribute order to be
+ preserved through html5lib if the tree builder preserves order.
+
+* Removed ``dom2sax`` from DOM treebuilders. It has been replaced by
+ ``treeadapters.sax.to_sax`` which is generic and supports any
+ treewalker; it also resolves all known bugs with ``dom2sax``.
+
+* Fix treewalker assertions on hitting bytes strings on
+ Python 2. Previous to 1.0b1, treewalkers coped with mixed
+ bytes/unicode data on Python 2; this reintroduces this prior
+ behaviour on Python 2. Behaviour is unchanged on Python 3.
+
+
+1.0b1
+~~~~~
+
+Released on May 17, 2013
+
+* Implementation updated to implement the `HTML specification
+ <http://www.whatwg.org/specs/web-apps/current-work/>`_ as of 5th May
+ 2013 (`SVN <http://svn.whatwg.org/webapps/>`_ revision r7867).
+
+* Python 3.2+ supported in a single codebase using the ``six`` library.
+
+* Removed support for Python 2.5 and older.
+
+* Removed the deprecated Beautiful Soup 3 treebuilder.
+ ``beautifulsoup4`` can use ``html5lib`` as a parser instead. Note that
+ since it doesn't support namespaces, foreign content like SVG and
+ MathML is parsed incorrectly.
+
+* Removed ``simpletree`` from the package. The default tree builder is
+ now ``etree`` (using the ``xml.etree.cElementTree`` implementation if
+ available, and ``xml.etree.ElementTree`` otherwise).
+
+* Removed the ``XHTMLSerializer`` as it never actually guaranteed its
+ output was well-formed XML, and hence provided little of use.
+
+* Removed default DOM treebuilder, so ``html5lib.treebuilders.dom`` is no
+ longer supported. ``html5lib.treebuilders.getTreeBuilder("dom")`` will
+ return the default DOM treebuilder, which uses ``xml.dom.minidom``.
+
+* Optional heuristic character encoding detection now based on
+ ``charade`` for Python 2.6 - 3.3 compatibility.
+
+* Optional ``Genshi`` treewalker support fixed.
+
+* Many bugfixes, including:
+
+ * #33: null in attribute value breaks XML AttValue;
+
+ * #4: nested, indirect descendant, <button> causes infinite loop;
+
+ * `Google Code 215
+ <http://code.google.com/p/html5lib/issues/detail?id=215>`_: Properly
+ detect seekable streams;
+
+ * `Google Code 206
+ <http://code.google.com/p/html5lib/issues/detail?id=206>`_: add
+ support for <video preload=...>, <audio preload=...>;
+
+ * `Google Code 205
+ <http://code.google.com/p/html5lib/issues/detail?id=205>`_: add
+ support for <video poster=...>;
+
+ * `Google Code 202
+ <http://code.google.com/p/html5lib/issues/detail?id=202>`_: Unicode
+ file breaks InputStream.
+
+* Source code is now mostly PEP 8 compliant.
+
+* Test harness has been improved and now depends on ``nose``.
+
+* Documentation updated and moved to https://html5lib.readthedocs.io/.
+
+
+0.95
+~~~~
+
+Released on February 11, 2012
+
+
+0.90
+~~~~
+
+Released on January 17, 2010
+
+
+0.11.1
+~~~~~~
+
+Released on June 12, 2008
+
+
+0.11
+~~~~
+
+Released on June 10, 2008
+
+
+0.10
+~~~~
+
+Released on October 7, 2007
+
+
+0.9
+~~~
+
+Released on March 11, 2007
+
+
+0.2
+~~~
+
+Released on January 8, 2007
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/CONTRIBUTING.rst b/testing/web-platform/tests/tools/third_party/html5lib/CONTRIBUTING.rst
new file mode 100644
index 0000000000..8c5e198535
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/CONTRIBUTING.rst
@@ -0,0 +1,60 @@
+Contributing
+============
+
+Pull requests are more than welcome — both to the library and to the
+documentation. Some useful information:
+
+- We aim to follow PEP 8 in the library, but ignoring the
+ 79-character-per-line limit, instead following a soft limit of 99,
+ but allowing lines over this where it is the readable thing to do.
+
+- We aim to follow PEP 257 for all docstrings, and make them properly
+ parseable by Sphinx while generating API documentation.
+
+- We keep ``pyflakes`` reporting no errors or warnings at all times.
+
+- We keep the master branch passing all tests at all times on all
+ supported versions.
+
+`Travis CI <https://travis-ci.org/html5lib/html5lib-python/>`_ is run
+against all pull requests and should enforce all of the above.
+
+We use `Opera Critic <https://critic.hoppipolla.co.uk/>`_ as an external
+code-review tool, which uses your GitHub login to authenticate. You'll
+get email notifications for issues raised in the review.
+
+
+Patch submission guidelines
+---------------------------
+
+- **Create a new Git branch specific to your change.** Do not put
+ multiple fixes/features in the same pull request. If you find an
+ unrelated bug, create a distinct branch and submit a separate pull
+ request for the bugfix. This makes life much easier for maintainers
+ and will speed up merging your patches.
+
+- **Write a test** whenever possible. Following existing tests is often
+ easiest, and a good way to tell whether the feature you're modifying
+ is easily testable.
+
+- **Make sure documentation is updated.** Keep docstrings current, and
+ if necessary, update the Sphinx documentation in ``doc/``.
+
+- **Add a changelog entry** at the top of ``CHANGES.rst`` following
+ existing entries' styles.
+
+- **Run tests with tox** if possible, to make sure your changes are
+ compatible with all supported Python versions.
+
+- **Squash commits** before submitting the pull request so that a single
+ commit contains the entire change, and only that change (see the first
+ bullet).
+
+- **Don't rebase after creating the pull request.** Merge with upstream,
+ if necessary, and use ``git commit --fixup`` for fixing issues raised
+ in a Critic review or by a failing Travis build. The reviewer will
+ squash and rebase your pull request while accepting it. Even though
+ GitHub won't recognize the pull request as accepted, the squashed
+ commits will properly specify you as the author.
+
+- **Attribute yourself** in ``AUTHORS.rst``.
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/LICENSE b/testing/web-platform/tests/tools/third_party/html5lib/LICENSE
new file mode 100644
index 0000000000..c87fa7a000
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2006-2013 James Graham and other 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 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/testing/web-platform/tests/tools/third_party/html5lib/MANIFEST.in b/testing/web-platform/tests/tools/third_party/html5lib/MANIFEST.in
new file mode 100644
index 0000000000..4b3ffe3ed9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/MANIFEST.in
@@ -0,0 +1,10 @@
+include LICENSE
+include AUTHORS.rst
+include CHANGES.rst
+include README.rst
+include requirements*.txt
+include .pytest.expect
+include tox.ini
+include pytest.ini
+graft html5lib/tests/testdata
+recursive-include html5lib/tests *.py
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/README.rst b/testing/web-platform/tests/tools/third_party/html5lib/README.rst
new file mode 100644
index 0000000000..d367905da0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/README.rst
@@ -0,0 +1,151 @@
+html5lib
+========
+
+.. image:: https://travis-ci.org/html5lib/html5lib-python.svg?branch=master
+ :target: https://travis-ci.org/html5lib/html5lib-python
+
+
+html5lib is a pure-python library for parsing HTML. It is designed to
+conform to the WHATWG HTML specification, as is implemented by all major
+web browsers.
+
+
+Usage
+-----
+
+Simple usage follows this pattern:
+
+.. code-block:: python
+
+ import html5lib
+ with open("mydocument.html", "rb") as f:
+ document = html5lib.parse(f)
+
+or:
+
+.. code-block:: python
+
+ import html5lib
+ document = html5lib.parse("<p>Hello World!")
+
+By default, the ``document`` will be an ``xml.etree`` element instance.
+Whenever possible, html5lib chooses the accelerated ``ElementTree``
+implementation (i.e. ``xml.etree.cElementTree`` on Python 2.x).
+
+Two other tree types are supported: ``xml.dom.minidom`` and
+``lxml.etree``. To use an alternative format, specify the name of
+a treebuilder:
+
+.. code-block:: python
+
+ import html5lib
+ with open("mydocument.html", "rb") as f:
+ lxml_etree_document = html5lib.parse(f, treebuilder="lxml")
+
+When using with ``urllib2`` (Python 2), the charset from HTTP should be
+pass into html5lib as follows:
+
+.. code-block:: python
+
+ from contextlib import closing
+ from urllib2 import urlopen
+ import html5lib
+
+ with closing(urlopen("http://example.com/")) as f:
+ document = html5lib.parse(f, transport_encoding=f.info().getparam("charset"))
+
+When using with ``urllib.request`` (Python 3), the charset from HTTP
+should be pass into html5lib as follows:
+
+.. code-block:: python
+
+ from urllib.request import urlopen
+ import html5lib
+
+ with urlopen("http://example.com/") as f:
+ document = html5lib.parse(f, transport_encoding=f.info().get_content_charset())
+
+To have more control over the parser, create a parser object explicitly.
+For instance, to make the parser raise exceptions on parse errors, use:
+
+.. code-block:: python
+
+ import html5lib
+ with open("mydocument.html", "rb") as f:
+ parser = html5lib.HTMLParser(strict=True)
+ document = parser.parse(f)
+
+When you're instantiating parser objects explicitly, pass a treebuilder
+class as the ``tree`` keyword argument to use an alternative document
+format:
+
+.. code-block:: python
+
+ import html5lib
+ parser = html5lib.HTMLParser(tree=html5lib.getTreeBuilder("dom"))
+ minidom_document = parser.parse("<p>Hello World!")
+
+More documentation is available at https://html5lib.readthedocs.io/.
+
+
+Installation
+------------
+
+html5lib works on CPython 2.7+, CPython 3.5+ and PyPy. To install:
+
+.. code-block:: bash
+
+ $ pip install html5lib
+
+The goal is to support a (non-strict) superset of the versions that `pip
+supports
+<https://pip.pypa.io/en/stable/installing/#python-and-os-compatibility>`_.
+
+Optional Dependencies
+---------------------
+
+The following third-party libraries may be used for additional
+functionality:
+
+- ``lxml`` is supported as a tree format (for both building and
+ walking) under CPython (but *not* PyPy where it is known to cause
+ segfaults);
+
+- ``genshi`` has a treewalker (but not builder); and
+
+- ``chardet`` can be used as a fallback when character encoding cannot
+ be determined.
+
+
+Bugs
+----
+
+Please report any bugs on the `issue tracker
+<https://github.com/html5lib/html5lib-python/issues>`_.
+
+
+Tests
+-----
+
+Unit tests require the ``pytest`` and ``mock`` libraries and can be
+run using the ``py.test`` command in the root directory.
+
+Test data are contained in a separate `html5lib-tests
+<https://github.com/html5lib/html5lib-tests>`_ repository and included
+as a submodule, thus for git checkouts they must be initialized::
+
+ $ git submodule init
+ $ git submodule update
+
+If you have all compatible Python implementations available on your
+system, you can run tests on all of them using the ``tox`` utility,
+which can be found on PyPI.
+
+
+Questions?
+----------
+
+There's a mailing list available for support on Google Groups,
+`html5lib-discuss <http://groups.google.com/group/html5lib-discuss>`_,
+though you may get a quicker response asking on IRC in `#whatwg on
+irc.freenode.net <http://wiki.whatwg.org/wiki/IRC>`_.
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/bench_html.py b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/bench_html.py
new file mode 100644
index 0000000000..cfe53c6733
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/bench_html.py
@@ -0,0 +1,57 @@
+import io
+import os
+import sys
+
+import pyperf
+
+sys.path[0:0] = [os.path.join(os.path.dirname(__file__), "..")]
+import html5lib # noqa: E402
+
+
+def bench_parse(fh, treebuilder):
+ fh.seek(0)
+ html5lib.parse(fh, treebuilder=treebuilder, useChardet=False)
+
+
+def bench_serialize(loops, fh, treebuilder):
+ fh.seek(0)
+ doc = html5lib.parse(fh, treebuilder=treebuilder, useChardet=False)
+
+ range_it = range(loops)
+ t0 = pyperf.perf_counter()
+
+ for loops in range_it:
+ html5lib.serialize(doc, tree=treebuilder, encoding="ascii", inject_meta_charset=False)
+
+ return pyperf.perf_counter() - t0
+
+
+BENCHMARKS = ["parse", "serialize"]
+
+
+def add_cmdline_args(cmd, args):
+ if args.benchmark:
+ cmd.append(args.benchmark)
+
+
+if __name__ == "__main__":
+ runner = pyperf.Runner(add_cmdline_args=add_cmdline_args)
+ runner.metadata["description"] = "Run benchmarks based on Anolis"
+ runner.argparser.add_argument("benchmark", nargs="?", choices=BENCHMARKS)
+
+ args = runner.parse_args()
+ if args.benchmark:
+ benchmarks = (args.benchmark,)
+ else:
+ benchmarks = BENCHMARKS
+
+ with open(os.path.join(os.path.dirname(__file__), "data", "html.html"), "rb") as fh:
+ source = io.BytesIO(fh.read())
+
+ if "parse" in benchmarks:
+ for tb in ("etree", "dom", "lxml"):
+ runner.bench_func("html_parse_%s" % tb, bench_parse, source, tb)
+
+ if "serialize" in benchmarks:
+ for tb in ("etree", "dom", "lxml"):
+ runner.bench_time_func("html_serialize_%s" % tb, bench_serialize, source, tb)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/bench_wpt.py b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/bench_wpt.py
new file mode 100644
index 0000000000..d5da006984
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/bench_wpt.py
@@ -0,0 +1,45 @@
+import io
+import os
+import sys
+
+import pyperf
+
+sys.path[0:0] = [os.path.join(os.path.dirname(__file__), "..")]
+import html5lib # noqa: E402
+
+
+def bench_html5lib(fh):
+ fh.seek(0)
+ html5lib.parse(fh, treebuilder="etree", useChardet=False)
+
+
+def add_cmdline_args(cmd, args):
+ if args.benchmark:
+ cmd.append(args.benchmark)
+
+
+BENCHMARKS = {}
+for root, dirs, files in os.walk(os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "wpt")):
+ for f in files:
+ if f.endswith(".html"):
+ BENCHMARKS[f[: -len(".html")]] = os.path.join(root, f)
+
+
+if __name__ == "__main__":
+ runner = pyperf.Runner(add_cmdline_args=add_cmdline_args)
+ runner.metadata["description"] = "Run parser benchmarks from WPT"
+ runner.argparser.add_argument("benchmark", nargs="?", choices=sorted(BENCHMARKS))
+
+ args = runner.parse_args()
+ if args.benchmark:
+ benchmarks = (args.benchmark,)
+ else:
+ benchmarks = sorted(BENCHMARKS)
+
+ for bench in benchmarks:
+ name = "wpt_%s" % bench
+ path = BENCHMARKS[bench]
+ with open(path, "rb") as fh:
+ fh2 = io.BytesIO(fh.read())
+
+ runner.bench_func(name, bench_html5lib, fh2)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/README.md b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/README.md
new file mode 100644
index 0000000000..5b896cbb7c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/README.md
@@ -0,0 +1,8 @@
+The files in this data are derived from:
+
+ * `html.html`: from [html](http://github.com/whatwg/html), revision
+ 77db356a293f2b152b648c836b6989d17afe42bb. This is the first 5000 lines of `source`. (This is
+ representative of the input to [Anolis](https://bitbucket.org/ms2ger/anolis/); first 5000 lines
+ chosen to make it parse in a reasonable time.)
+
+ * `wpt`: see `wpt/README.md`.
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/html.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/html.html
new file mode 100644
index 0000000000..d2bb1be745
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/html.html
@@ -0,0 +1,5000 @@
+<!-- EDITOR NOTES -*- mode: Text; fill-column: 100 -*-
+ !
+ ! Adding a new element involves editing the following sections:
+ ! - section for the element itself
+ ! - descriptions of the element's categories
+ ! - images/content-venn.svg
+ ! - syntax, if it's void or otherwise special
+ ! - parser, if it's not phrasing-level
+ ! - rendering
+ ! - obsolete section
+ ! - element, attribute, content model, and interface indexes
+ ! - adding it to the section with ARIA mappings
+ !
+ !-->
+
+<!--
+ ! http://lists.w3.org/Archives/Public/www-archive/2014Apr/0034.html
+ !-->
+
+<!--START complete--><!--START dev-html-->
+<!DOCTYPE html>
+<!--SET FINGERPRINT=<span title="fingerprinting vector" class="fingerprint"><img src="images/fingerprint.png" alt="(This is a fingerprinting vector.)" width=46 height=64></span>-->
+<html lang="en-GB-x-hixie" class="big">
+ <head>
+ <title>HTML Standard</title>
+ <script>
+ var loadTimer = new Date();
+ var current_revision = "r" + "$Revision: 1 $".substr(11);
+ current_revision = current_revision.substr(0, current_revision.length - 2);
+ var last_known_revision = current_revision;
+ function F( /* varargs... */) {
+ var fragment = document.createDocumentFragment();
+ for (var index = 0; index < arguments.length; index += 1) {
+ if (arguments[index] instanceof Array) {
+ fragment.appendChild(F.apply(this, arguments[index]));
+ } else if (typeof arguments[index] == 'string') {
+ fragment.appendChild(document.createTextNode(arguments[index]));
+ } else {
+ fragment.appendChild(arguments[index]);
+ }
+ }
+ return fragment;
+ }
+ function E(name, /* optional */ attributes /*, varargs... */) {
+ var element = document.createElement(name);
+ var index = 1;
+ if ((arguments.length > 1) && (typeof attributes != 'string') &&
+ (!(attributes instanceof Node)) && (!(attributes instanceof Array))) {
+ for (var attName in attributes) {
+ if (typeof attributes[attName] == 'boolean') {
+ if (attributes[attName])
+ element.setAttribute(attName, '');
+ } else if (typeof attributes[attName] == 'function') {
+ element[attName] = attributes[attName];
+ } else {
+ element.setAttribute(attName, attributes[attName]);
+ }
+ }
+ index = 2;
+ }
+ for (; index < arguments.length; index += 1) {
+ if (arguments[index] instanceof Array) {
+ element.appendChild(F.apply(this, arguments[index]));
+ } else if (typeof arguments[index] == 'string') {
+ element.appendChild(document.createTextNode(arguments[index]));
+ } else {
+ element.appendChild(arguments[index]);
+ }
+ }
+ return element;
+ }
+ function getCookie(name) {
+ var params = location.search.substr(1).split("&");
+ for (var index = 0; index < params.length; index++) {
+ if (params[index] == name)
+ return "1";
+ var data = params[index].split("=");
+ if (data[0] == name)
+ return unescape(data[1]);
+ }
+ var cookies = document.cookie.split("; ");
+ for (var index = 0; index < cookies.length; index++) {
+ var data = cookies[index].split("=");
+ if (data[0] == name)
+ return unescape(data[1]);
+ }
+ return null;
+ }
+ var currentAlert;
+ var currentAlertTimeout;
+ function showAlert(s, href) {
+ if (!currentAlert) {
+ currentAlert = document.createElement('div');
+ currentAlert.id = 'alert';
+ var x = document.createElement('button');
+ x.textContent = '\u2573';
+ x.onclick = closeAlert2;
+ currentAlert.appendChild(x);
+ currentAlert.appendChild(document.createElement('span'));
+ currentAlert.onmousemove = function () {
+ clearTimeout(currentAlertTimeout);
+ currentAlert.className = '';
+ currentAlertTimeout = setTimeout(closeAlert, 10000);
+ }
+ document.body.appendChild(currentAlert);
+ } else {
+ clearTimeout(currentAlertTimeout);
+ currentAlert.className = '';
+ }
+ currentAlert.lastChild.textContent = '';
+ currentAlert.lastChild.appendChild(F(s));
+ if (href) {
+ var link = document.createElement('a');
+ link.href = href;
+ link.textContent = href;
+ currentAlert.lastChild.appendChild(F(' ', link));
+ }
+ currentAlertTimeout = setTimeout(closeAlert, 10000);
+ }
+ function closeAlert() {
+ clearTimeout(currentAlertTimeout);
+ if (currentAlert) {
+ currentAlert.className = 'closed';
+ currentAlertTimeout = setTimeout(closeAlert2, 3000);
+ }
+ }
+ function closeAlert2() {
+ clearTimeout(currentAlertTimeout);
+ if (currentAlert) {
+ currentAlert.parentNode.removeChild(currentAlert);
+ currentAlert = null;
+ }
+ }
+ window.addEventListener('keydown', function (event) {
+ if (event.keyCode == 27) {
+ if (currentAlert)
+ closeAlert2();
+ } else {
+ closeAlert();
+ }
+ }, false);
+ window.addEventListener('scroll', function (event) {
+ closeAlert();
+ }, false);
+ function load(script) {
+ var e = document.createElement('script');
+ e.setAttribute('src', '//www.whatwg.org/specs/web-apps/current-work/' + script);
+ document.body.appendChild(e);
+ }
+
+ var startedInit = 0;
+ function init() {
+ startedInit = 1;
+ if (location.search == '?slow-browser')
+ return;
+ load('reviewer.js');
+ if (document.documentElement.className == "big" || document.documentElement.className == "big split index")
+ load('toc.js');
+ load('updater.js');
+ load('dfn.js');
+ load('status.js');
+ if (getCookie('profile') == '1')
+ document.getElementsByTagName('h2')[0].textContent += '; load: ' + (new Date() - loadTimer) + 'ms';
+ }
+ if (document.documentElement.className == "")
+ setTimeout(function () {
+ if (!startedInit)
+ showAlert("Too slow? Try reading the multipage copy of the spec instead:", "http://whatwg.org/html");
+ }, 6000);
+
+ window.addEventListener('keypress', function (event) {
+ if ((event.which == 114) && (event.metaKey)) {
+ if (!confirm('Are you sure you want to reload this page?'))
+ event.preventDefault();
+ }
+ }, false);
+
+ </script>
+ <link rel="stylesheet" href="//www.whatwg.org/style/specification">
+ <link rel="icon" href="//www.whatwg.org/images/icon">
+ <style>
+ .proposal { border: blue solid; padding: 1em; }
+ .bad, .bad *:not(.XXX) { color: gray; border-color: gray; background: transparent; }
+ #updatesStatus { display: none; z-index: 10; }
+ #updatesStatus.relevant { display: block; position: fixed; right: 1em; top: 1em; padding: 0.5em; font: bold small sans-serif; min-width: 25em; width: 30%; max-width: 40em; height: auto; border: ridge 4px gray; background: #EEEEEE; color: black; }
+ div.head .logo { width: 11em; margin-bottom: 20em; }
+
+ #configUI { position: absolute; z-index: 20; top: auto; right: 0; width: 11em; padding: 0 0.5em 0 0.5em; font-size: small; background: gray; background: rgba(32,32,32,0.9); color: white; border-radius: 1em 0 0 1em; -moz-border-radius: 1em 0 0 1em; }
+ #configUI p { margin: 0.75em 0; padding: 0.3em; }
+ #configUI p label { display: block; }
+ #configUI #updateUI, #configUI .loginUI { text-align: center; }
+ #configUI input[type=button] { display: block; margin: auto; }
+ #configUI :link, #configUI :visited { color: white; }
+ #configUI :link:hover, #configUI :visited:hover { background: transparent; }
+
+ #alert { position: fixed; top: 20%; left: 20%; right: 20%; font-size: 2em; padding: 0.5em; z-index: 40; background: gray; background: rgba(32,32,32,0.9); color: white; border-radius: 1em; -moz-border-radius: 1em; -webkit-transition: opacity 1s linear; }
+ #alert.closed { opacity: 0; }
+ #alert button { position: absolute; top: -1em; right: 2em; border-radius: 1em 1em 0 0; border: none; line-height: 0.9; color: white; background: rgb(64,64,64); font-size: 0.6em; font-weight: 900; cursor: pointer; }
+ #alert :link, #alert :visited { color: white; }
+ #alert :link:hover, #alert :visited:hover { background: transparent; }
+ @media print { #configUI { display: none; } }
+
+ .rfc2119 { font-variant: small-caps; text-shadow: 0 0 0.5em yellow; position: static; }
+ .rfc2119::after { position: absolute; left: 0; width: 25px; text-align: center; color: yellow; text-shadow: 0.075em 0.075em 0.2em black; }
+ .rfc2119.m\ust::after { content: '\2605'; }
+ .rfc2119.s\hould::after { content: '\2606'; }
+ [hidden] { display: none; }
+
+ .fingerprint { float: right; }
+
+ .applies thead th > * { display: block; }
+ .applies thead code { display: block; }
+ .applies td { text-align: center; }
+ .applies .yes { background: yellow; }
+
+ .matrix, .matrix td { border: hidden; text-align: right; }
+ .matrix { margin-left: 2em; }
+
+ .vertical-summary-table tr > th[rowspan="2"]:first-child + th,
+ .vertical-summary-table tr > td[rowspan="2"]:first-child + td { border-bottom: hidden; }
+
+ .dice-example { border-collapse: collapse; border-style: hidden solid solid hidden; border-width: thin; margin-left: 3em; }
+ .dice-example caption { width: 30em; font-size: smaller; font-style: italic; padding: 0.75em 0; text-align: left; }
+ .dice-example td, .dice-example th { border: solid thin; width: 1.35em; height: 1.05em; text-align: center; padding: 0; }
+
+ td.eg { border-width: thin; text-align: center; }
+
+ #table-example-1 { border: solid thin; border-collapse: collapse; margin-left: 3em; }
+ #table-example-1 * { font-family: "Essays1743", serif; line-height: 1.01em; }
+ #table-example-1 caption { padding-bottom: 0.5em; }
+ #table-example-1 thead, #table-example-1 tbody { border: none; }
+ #table-example-1 th, #table-example-1 td { border: solid thin; }
+ #table-example-1 th { font-weight: normal; }
+ #table-example-1 td { border-style: none solid; vertical-align: top; }
+ #table-example-1 th { padding: 0.5em; vertical-align: middle; text-align: center; }
+ #table-example-1 tbody tr:first-child td { padding-top: 0.5em; }
+ #table-example-1 tbody tr:last-child td { padding-bottom: 1.5em; }
+ #table-example-1 tbody td:first-child { padding-left: 2.5em; padding-right: 0; width: 9em; }
+ #table-example-1 tbody td:first-child::after { content: leader(". "); }
+ #table-example-1 tbody td { padding-left: 2em; padding-right: 2em; }
+ #table-example-1 tbody td:first-child + td { width: 10em; }
+ #table-example-1 tbody td:first-child + td ~ td { width: 2.5em; }
+ #table-example-1 tbody td:first-child + td + td + td ~ td { width: 1.25em; }
+
+ .apple-table-examples { border: none; border-collapse: separate; border-spacing: 1.5em 0em; width: 40em; margin-left: 3em; }
+ .apple-table-examples * { font-family: "Times", serif; }
+ .apple-table-examples td, .apple-table-examples th { border: none; white-space: nowrap; padding-top: 0; padding-bottom: 0; }
+ .apple-table-examples tbody th:first-child { border-left: none; width: 100%; }
+ .apple-table-examples thead th:first-child ~ th { font-size: smaller; font-weight: bolder; border-bottom: solid 2px; text-align: center; }
+ .apple-table-examples tbody th::after, .apple-table-examples tfoot th::after { content: leader(". ") }
+ .apple-table-examples tbody th, .apple-table-examples tfoot th { font: inherit; text-align: left; }
+ .apple-table-examples td { text-align: right; vertical-align: top; }
+ .apple-table-examples.e1 tbody tr:last-child td { border-bottom: solid 1px; }
+ .apple-table-examples.e1 tbody + tbody tr:last-child td { border-bottom: double 3px; }
+ .apple-table-examples.e2 th[scope=row] { padding-left: 1em; }
+ .apple-table-examples sup { line-height: 0; }
+
+ .three-column-nowrap tr > td:first-child,
+ .three-column-nowrap tr > td:first-child + td,
+ .three-column-nowrap tr > td:first-child + td + td { white-space: nowrap; }
+
+ .details-example img { vertical-align: top; }
+
+ #base64-table {
+ white-space: nowrap;
+ font-size: 0.6em;
+ column-width: 6em;
+ column-count: 5;
+ column-gap: 1em;
+ -moz-column-width: 6em;
+ -moz-column-count: 5;
+ -moz-column-gap: 1em;
+ -webkit-column-width: 6em;
+ -webkit-column-count: 5;
+ -webkit-column-gap: 1em;
+ }
+ #base64-table thead { display: none; }
+ #base64-table * { border: none; }
+ #base64-table tbody td:first-child:after { content: ':'; }
+ #base64-table tbody td:last-child { text-align: right; }
+
+ #named-character-references-table {
+ white-space: nowrap;
+ font-size: 0.6em;
+ column-width: 30em;
+ column-gap: 1em;
+ -moz-column-width: 30em;
+ -moz-column-gap: 1em;
+ -webkit-column-width: 30em;
+ -webkit-column-gap: 1em;
+ }
+ #named-character-references-table > table > tbody > tr > td:first-child + td,
+ #named-character-references-table > table > tbody > tr > td:last-child { text-align: center; }
+ #named-character-references-table > table > tbody > tr > td:last-child:hover > span { position: absolute; top: auto; left: auto; margin-left: 0.5em; line-height: 1.2; font-size: 5em; border: outset; padding: 0.25em 0.5em; background: white; width: 1.25em; height: auto; text-align: center; }
+ #named-character-references-table > table > tbody > tr#entity-CounterClockwiseContourIntegral > td:first-child { font-size: 0.5em; }
+
+ .glyph.control { color: red; }
+
+ @font-face {
+ font-family: 'Essays1743';
+ src: url('//www.whatwg.org/specs/web-apps/current-work/fonts/Essays1743.ttf');
+ }
+ @font-face {
+ font-family: 'Essays1743';
+ font-weight: bold;
+ src: url('//www.whatwg.org/specs/web-apps/current-work/fonts/Essays1743-Bold.ttf');
+ }
+ @font-face {
+ font-family: 'Essays1743';
+ font-style: italic;
+ src: url('//www.whatwg.org/specs/web-apps/current-work/fonts/Essays1743-Italic.ttf');
+ }
+ @font-face {
+ font-family: 'Essays1743';
+ font-style: italic;
+ font-weight: bold;
+ src: url('//www.whatwg.org/specs/web-apps/current-work/fonts/Essays1743-BoldItalic.ttf');
+ }
+ </style>
+ <link rel="stylesheet" href="status.css">
+ </head>
+ <body onload="init()">
+ <header class="head with-buttons" id="head">
+ <p><a href="//www.whatwg.org/" class="logo"><img width="101" height="101" alt="WHATWG" src="/images/logo"></a></p>
+ <hgroup>
+ <h1 class="allcaps">HTML</h1>
+ <h2 class="no-num no-toc">Living Standard &mdash; Last Updated <span class="pubdate">[DATE: 01 Jan 1901]</span></h2>
+ </hgroup>
+ <div>
+ <div>
+ <a href="//whatwg.org/html"><span><strong>Multipage Version</strong> <code>whatwg.org/html</code></span></a>
+ <a href="//whatwg.org/c"><span><strong>One-Page Version</strong> <code>whatwg.org/c</code></span></a>
+ <a href="//whatwg.org/pdf"><span><strong>PDF Version</strong> <code>whatwg.org/pdf</code></span></a>
+ <a href="http://developers.whatwg.org/"><span><strong>Developer Version</strong> <code>developers.whatwg.org</code></span></a>
+ </div>
+ <div>
+ <a class="misc" href="//whatwg.org/faq"><span><strong>FAQ</strong> <code>whatwg.org/faq</code></span></a>
+ <a class="misc" href="http://validator.whatwg.org/"><span><strong>Validators</strong> <code>validator.whatwg.org</code></span></a>
+ </div>
+ <div>
+ <a class="comms" href="//www.whatwg.org/mailing-list"><span><strong>Join our Mailing List</strong> <code>whatwg@whatwg.org</code></span></a>
+ <a class="comms" href="http://wiki.whatwg.org/wiki/IRC"><span><strong>Join us on IRC</strong> <code>#whatwg on Freenode</code></span></a>
+ <a class="comms" href="http://forums.whatwg.org/"><span><strong>Join our Forums</strong> <code>forums.whatwg.org</code></span></a>
+ </div>
+ <div>
+ <!--<a class="changes" href="http://svn.whatwg.org/webapps"><span><strong>SVN Repository</strong> <code>svn.whatwg.org/webapps</code></span></a>-->
+ <a class="changes" href="http://html5.org/tools/web-apps-tracker"><span><strong>Change Log</strong> <code>html5.org's tracker</code></span></a>
+ <a class="changes" href="http://twitter.com/WHATWG"><span><strong>Twitter Updates</strong> <code>@WHATWG</code></span></a>
+ </div>
+ <div>
+ <a class="feedback" href="https://www.w3.org/Bugs/Public/buglist.cgi?bug_status=UNCONFIRMED&amp;bug_status=NEW&amp;bug_status=ASSIGNED&amp;bug_status=REOPENED&amp;component=HTML&amp;product=WHATWG"><span><strong>View Open Bugs</strong> <code>filed in Bugzilla</code></span></a>
+ <a class="feedback" href="//www.whatwg.org/newbug"><span><strong>File a Bug</strong> <code>whatwg.org/newbug</code></span></a>
+ <a class="feedback" href="http://ian.hixie.ch/+"><span><strong>E-mail the Editor</strong> <code>ian@hixie.ch</code></span></a>
+ </div>
+ </div>
+ </header>
+
+ <hr>
+
+ <div id="configUI"></div>
+
+ <h2 class="no-num no-toc" id="contents">Table of contents</h2>
+ <!--toc-->
+
+ <hr>
+
+<!--
+ <pre class="idl">
+ interface Screen { }; // CSSOM
+ interface URL { }; // URL API
+ interface Blob { }; // File API
+ interface File : Blob { }; // File API
+ interface FileList { }; // File API
+ interface WebGLRenderingContext { }; // WebGL
+ interface XMLDocument { }; // DOM
+ interface HTMLCollection { }; // DOM
+ interface DOMTokenList { }; // DOM
+ interface DOMSettableTokenList { attribute any value; }; // DOM
+ interface SVGMatrix { }; // SVG
+ // fake interfaces that map to JS object types:
+ interface ArrayBuffer { };
+ interface Int8Array { };
+ interface Uint8Array { };
+ interface Uint8ClampedArray { };
+ interface Int16Array { };
+ interface Uint16Array { };
+ interface Int32Array { };
+ interface Uint32Array { };
+ interface Float32Array { };
+ interface Float64Array { };
+ interface Uint8ClampedArray { };
+ </pre>
+-->
+
+ <h2 id="introduction">Introduction</h2>
+
+ <div class="nodev">
+
+ <h3 id="abstract">Where does this specification fit?</h3>
+
+ <p>This specification defines a big part of the Web platform, in lots of detail. Its place in the
+ Web platform specification stack relative to other specifications can be best summed up as
+ follows:</p>
+
+ <p><img src="images/abstract.png" width="398" height="359" alt="It consists of everything else, above such core technologies as HTTP, URI/IRIs, DOM, XML, Unicode, and ECMAScript; below presentation-layer technologies like CSS and the NPAPI; and to the side of technologies like Geolocation, SVG, MathML, and XHR."></p>
+
+ </div>
+
+
+ <h3 id="is-this-html5?">Is this HTML5?</h3>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>In short: Yes.</p>
+
+ <p>In more length: The term "HTML5" is widely used as a buzzword to refer to modern Web
+ technologies, many of which (though by no means all) are developed at the WHATWG. This document is
+ one such; others are available from <a href="http://www.whatwg.org/specs/">the WHATWG
+ specification index</a>.</p>
+
+ <p class="note">Although we have asked them to stop doing so, the W3C also republishes some parts
+ of this specification as separate documents. There are numerous differences between this
+ specification and the W3C forks; some minor, some major. Unfortunately these are not currently
+ accurately documented anywhere, so there is no way to know which are intentional and which are
+ not.</p>
+
+
+ <h3>Background</h3>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>HTML is the World Wide Web's core markup language. Originally, HTML was primarily designed as a
+ language for semantically describing scientific documents. Its general design, however, has
+ enabled it to be adapted, over the subsequent years, to describe a number of other types of
+ documents and even applications.</p>
+
+
+ <h3>Audience</h3>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>This specification is intended for authors of documents and scripts that use the features
+ defined in this specification<span class="nodev">, implementors of tools that operate on pages that
+ use the features defined in this specification, and individuals wishing to establish the
+ correctness of documents or implementations with respect to the requirements of this
+ specification</span>.</p>
+
+ <p>This document is probably not suited to readers who do not already have at least a passing
+ familiarity with Web technologies, as in places it sacrifices clarity for precision, and brevity
+ for completeness. More approachable tutorials and authoring guides can provide a gentler
+ introduction to the topic.</p>
+
+ <p>In particular, familiarity with the basics of DOM is necessary for a complete understanding of
+ some of the more technical parts of this specification. An understanding of Web IDL, HTTP, XML,
+ Unicode, character encodings, JavaScript, and CSS will also be helpful in places but is not
+ essential.</p>
+
+
+ <h3>Scope</h3>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>This specification is limited to providing a semantic-level markup language and associated
+ semantic-level scripting APIs for authoring accessible pages on the Web ranging from static
+ documents to dynamic applications.</p>
+
+ <p>The scope of this specification does not include providing mechanisms for media-specific
+ customization of presentation (although default rendering rules for Web browsers are included at
+ the end of this specification, and several mechanisms for hooking into CSS are provided as part of
+ the language).</p>
+
+ <p>The scope of this specification is not to describe an entire operating system. In particular,
+ hardware configuration software, image manipulation tools, and applications that users would be
+ expected to use with high-end workstations on a daily basis are out of scope. In terms of
+ applications, this specification is targeted specifically at applications that would be expected
+ to be used by users on an occasional basis, or regularly but from disparate locations, with low
+ CPU requirements. Examples of such applications include online purchasing systems, searching
+ systems, games (especially multiplayer online games), public telephone books or address books,
+ communications software (e-mail clients, instant messaging clients, discussion software), document
+ editing software, etc.</p>
+
+
+ <h3>History</h3>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>For its first five years (1990-1995), HTML went through a number of revisions and experienced a
+ number of extensions, primarily hosted first at CERN, and then at the IETF.</p>
+
+ <p>With the creation of the W3C, HTML's development changed venue again. A first abortive attempt
+ at extending HTML in 1995 known as HTML 3.0 then made way to a more pragmatic approach known as
+ HTML 3.2, which was completed in 1997. HTML4 quickly followed later that same year.</p>
+
+ <p>The following year, the W3C membership decided to stop evolving HTML and instead begin work on
+ an XML-based equivalent, called XHTML. <!-- http://www.w3.org/MarkUp/future/#summary --> This
+ effort started with a reformulation of HTML4 in XML, known as XHTML 1.0, which added no new
+ features except the new serialisation, and which was completed in 2000. After XHTML 1.0, the W3C's
+ focus turned to making it easier for other working groups to extend XHTML, under the banner of
+ XHTML Modularization. In parallel with this, the W3C also worked on a new language that was not
+ compatible with the earlier HTML and XHTML languages, calling it XHTML2.</p>
+
+ <p>Around the time that HTML's evolution was stopped in 1998, parts of the API for HTML developed
+ by browser vendors were specified and published under the name DOM Level 1 (in 1998) and DOM Level
+ 2 Core and DOM Level 2 HTML (starting in 2000 and culminating in 2003). These efforts then petered
+ out, with some DOM Level 3 specifications published in 2004 but the working group being closed
+ before all the Level 3 drafts were completed.</p>
+
+ <p>In 2003, the publication of XForms, a technology which was positioned as the next generation of
+ Web forms, sparked a renewed interest in evolving HTML itself, rather than finding replacements
+ for it. This interest was borne from the realization that XML's deployment as a Web technology was
+ limited to entirely new technologies (like RSS and later Atom), rather than as a replacement for
+ existing deployed technologies (like HTML).</p>
+
+ <p>A proof of concept to show that it was possible to extend HTML4's forms to provide many of the
+ features that XForms 1.0 introduced, without requiring browsers to implement rendering engines
+ that were incompatible with existing HTML Web pages, was the first result of this renewed
+ interest. At this early stage, while the draft was already publicly available, and input was
+ already being solicited from all sources, the specification was only under Opera Software's
+ copyright.</p>
+
+ <p>The idea that HTML's evolution should be reopened was tested at a W3C workshop in 2004, where
+ some of the principles that underlie the HTML5 work (described below), as well as the
+ aforementioned early draft proposal covering just forms-related features, were presented to the
+ W3C jointly by Mozilla and Opera. The proposal was rejected on the grounds that the proposal
+ conflicted with the previously chosen direction for the Web's evolution; the W3C staff and
+ membership voted to continue developing XML-based replacements instead.</p>
+
+ <p>Shortly thereafter, Apple, Mozilla, and Opera jointly announced their intent to continue
+ working on the effort under the umbrella of a new venue called the WHATWG. A public mailing list
+ was created, and the draft was moved to the WHATWG site. The copyright was subsequently amended to
+ be jointly owned by all three vendors, and to allow reuse of the specification.</p>
+
+ <p>The WHATWG was based on several core principles, in particular that technologies need to be
+ backwards compatible, that specifications and implementations need to match even if this means
+ changing the specification rather than the implementations, and that specifications need to be
+ detailed enough that implementations can achieve complete interoperability without
+ reverse-engineering each other.</p>
+
+ <p>The latter requirement in particular required that the scope of the HTML5 specification include
+ what had previously been specified in three separate documents: HTML4, XHTML1, and DOM2 HTML. It
+ also meant including significantly more detail than had previously been considered the norm.</p>
+
+ <p>In 2006, the W3C indicated an interest to participate in the development of HTML5 after all,
+ and in 2007 formed a working group chartered to work with the WHATWG on the development of the
+ HTML5 specification. Apple, Mozilla, and Opera allowed the W3C to publish the specification under
+ the W3C copyright, while keeping a version with the less restrictive license on the WHATWG
+ site.</p>
+
+ <p>For a number of years, both groups then worked together. In 2011, however, the groups came to
+ the conclusion that they had different goals: the W3C wanted to publish a "finished" version of
+ "HTML5", while the WHATWG wanted to continue working on a Living Standard for HTML, continuously
+ maintaining the specification rather than freezing it in a state with known problems, and adding
+ new features as needed to evolve the platform.</p>
+
+ <p>Since then, the WHATWG has been working on this specification (amongst others), and the W3C has
+ been copying fixes made by the WHATWG into their fork of the document, as well as making other
+ changes, some intentional and some not, with no documentation listing or explaining the
+ differences.</p>
+
+
+
+ <h3>Design notes</h3>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>It must be admitted that many aspects of HTML appear at first glance to be nonsensical and
+ inconsistent.</p>
+
+ <p>HTML, its supporting DOM APIs, as well as many of its supporting technologies, have been
+ developed over a period of several decades by a wide array of people with different priorities
+ who, in many cases, did not know of each other's existence.</p>
+
+ <p>Features have thus arisen from many sources, and have not always been designed in especially
+ consistent ways. Furthermore, because of the unique characteristics of the Web, implementation
+ bugs have often become de-facto, and now de-jure, standards, as content is often unintentionally
+ written in ways that rely on them before they can be fixed.</p>
+
+ <p>Despite all this, efforts have been made to adhere to certain design goals. These are described
+ in the next few subsections.</p>
+
+
+
+ <h4>Serializability of script execution</h4>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>To avoid exposing Web authors to the complexities of multithreading, the HTML and DOM APIs are
+ designed such that no script can ever detect the simultaneous execution of other scripts. Even
+ with <span data-x="Worker">workers</span>, the intent is that the behavior of implementations can
+ be thought of as completely serializing the execution of all scripts in all <span data-x="browsing
+ context">browsing contexts</span>.</p>
+
+ <p class="note">The <code
+ data-x="dom-navigator-yieldForStorageUpdates">navigator.yieldForStorageUpdates()</code> method, in
+ this model, is equivalent to allowing other scripts to run while the calling script is
+ blocked.</p>
+
+
+
+ <h4>Compliance with other specifications</h4>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>This specification interacts with and relies on a wide variety of other specifications. In
+ certain circumstances, unfortunately, conflicting needs have led to this specification violating
+ the requirements of these other specifications. Whenever this has occurred, the transgressions
+ have each been noted as a "<dfn>willful violation</dfn>", and the reason for the violation has
+ been noted.</p>
+
+
+
+ <h4>Extensibility</h4>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>HTML has a wide array of extensibility mechanisms that can be used for adding semantics in a
+ safe manner:</p>
+
+ <ul>
+
+ <li><p>Authors can use the <code data-x="attr-class">class</code> attribute to extend elements,
+ effectively creating their own elements, while using the most applicable existing "real" HTML
+ element, so that browsers and other tools that don't know of the extension can still support it
+ somewhat well. This is the tack used by microformats, for example.</p></li>
+
+ <li><p>Authors can include data for inline client-side scripts or server-side site-wide scripts
+ to process using the <code data-x="attr-data-*">data-*=""</code> attributes. These are guaranteed
+ to never be touched by browsers, and allow scripts to include data on HTML elements that scripts
+ can then look for and process.</p></li>
+
+ <li><p>Authors can use the <code data-x="meta">&lt;meta name="" content=""></code> mechanism to
+ include page-wide metadata by registering <span data-x="concept-meta-extensions">extensions to
+ the predefined set of metadata names</span>.</p></li>
+
+ <li><p>Authors can use the <code data-x="attr-hyperlink-rel">rel=""</code> mechanism to annotate
+ links with specific meanings by registering <span data-x="concept-rel-extensions">extensions to
+ the predefined set of link types</span>. This is also used by microformats.</p></li>
+
+ <li><p>Authors can embed raw data using the <code data-x="script">&lt;script type=""></code>
+ mechanism with a custom type, for further handling by inline or server-side scripts.</p></li>
+
+ <li><p>Authors can create <span data-x="plugin">plugins</span> and invoke them using the
+ <code>embed</code> element. This is how Flash works.</p></li>
+
+ <li><p>Authors can extend APIs using the JavaScript prototyping mechanism. This is widely used by
+ script libraries, for instance.</p></li>
+
+ <li><p>Authors can use the microdata feature (the <code
+ data-x="attr-itemscope">itemscope=""</code> and <code data-x="attr-itemprop">itemprop=""</code>
+ attributes) to embed nested name-value pairs of data to be shared with other applications and
+ sites.</p></li>
+
+ </ul>
+
+
+
+
+ <h3>HTML vs XHTML</h3>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>This specification defines an abstract language for describing documents and applications, and
+ some APIs for interacting with in-memory representations of resources that use this language.</p>
+
+ <p>The in-memory representation is known as "DOM HTML", or "the DOM" for short.</p>
+
+ <p>There are various concrete syntaxes that can be used to transmit resources that use this
+ abstract language, two of which are defined in this specification.</p>
+
+ <p>The first such concrete syntax is the HTML syntax. This is the format suggested for most
+ authors. It is compatible with most legacy Web browsers. If a document is transmitted with the
+ <code>text/html</code> <span>MIME type</span>, then it will be processed as an HTML document by
+ Web browsers. This specification defines the latest HTML syntax, known simply as "HTML".</p>
+
+ <p>The second concrete syntax is the XHTML syntax, which is an application of XML. When a document
+ is transmitted with an <span>XML MIME type</span>, such as <code>application/xhtml+xml</code>,
+ then it is treated as an XML document by Web browsers, to be parsed by an XML processor. Authors
+ are reminded that the processing for XML and HTML differs; in particular, even minor syntax errors
+ will prevent a document labeled as XML from being rendered fully, whereas they would be ignored in
+ the HTML syntax. This specification defines the latest XHTML syntax, known simply as "XHTML".</p>
+
+ <p>The DOM, the HTML syntax, and the XHTML syntax cannot all represent the same content. For
+ example, namespaces cannot be represented using the HTML syntax, but they are supported in the DOM
+ and in the XHTML syntax. Similarly, documents that use the <code>noscript</code> feature can be
+ represented using the HTML syntax, but cannot be represented with the DOM or in the XHTML syntax.
+ Comments that contain the string "<code data-x="">--&gt;</code>" can only be represented in the
+ DOM, not in the HTML and XHTML syntaxes.</p>
+
+
+ <h3>Structure of this specification</h3>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>This specification is divided into the following major sections:</p>
+
+ <dl>
+
+
+ <dt><a href="#introduction">Introduction</a></dt>
+
+ <dd>Non-normative materials providing a context for the HTML standard.</dd>
+
+
+ <dt><a href="#infrastructure">Common infrastructure</a></dt>
+
+ <dd>The conformance classes, algorithms, definitions, and the common underpinnings of the rest of
+ the specification.</dd>
+
+
+ <dt><a href="#dom">Semantics, structure, and APIs of HTML documents</a></dt>
+
+ <dd>Documents are built from elements. These elements form a tree using the DOM. This section
+ defines the features of this DOM, as well as introducing the features common to all elements, and
+ the concepts used in defining elements.</dd>
+
+
+ <dt><a href="#semantics">The elements of HTML</a></dt>
+
+ <dd>Each element has a predefined meaning, which is explained in this section. Rules for authors
+ on how to use the element<span class="nodev">, along with user agent requirements for how to
+ handle each element,</span> are also given. This includes large signature features of HTML such
+ as video playback and subtitles, form controls and form submission, and a 2D graphics API known
+ as the HTML canvas.</dd>
+
+
+ <dt><a href="#microdata">Microdata</a></dt>
+
+ <dd>This specification introduces a mechanism for adding machine-readable annotations to
+ documents, so that tools can extract trees of name-value pairs from the document. This section
+ describes this mechanism<span class="nodev"> and some algorithms that can be used to convert HTML
+ documents into other formats</span>. This section also defines some sample Microdata vocabularies
+ for contact information, calendar events, and licensing works.</dd>
+
+
+ <dt><a href="#editing">User interaction</a></dt>
+
+ <dd>HTML documents can provide a number of mechanisms for users to interact with and modify
+ content, which are described in this section, such as how focus works, and drag-and-drop.</dd>
+
+
+ <dt><a href="#browsers">Loading Web pages</a></dt>
+
+ <dd>HTML documents do not exist in a vacuum &mdash; this section defines many of the features
+ that affect environments that deal with multiple pages, such as Web browsers and offline
+ caching of Web applications.</dd>
+
+
+ <dt><a href="#webappapis">Web application APIs</a></dt>
+
+ <dd>This section introduces basic features for scripting of applications in HTML.</dd>
+
+
+ <dt><a href="#workers">Web workers</a></dt>
+
+ <dd>This section defines an API for background threads in JavaScript.</dd>
+
+
+ <dt><a href="#comms">The communication APIs</a></dt>
+
+ <dd>This section describes some mechanisms that applications written in HTML can use to
+ communicate with other applications from different domains running on the same client. It also
+ introduces a server-push event stream mechanism known as Server Sent Events or
+ <code>EventSource</code>, and a two-way full-duplex socket protocol for scripts known as Web
+ Sockets.
+
+ </dd>
+
+
+ <dt><a href="#webstorage">Web storage</a></dt>
+
+ <dd>This section defines a client-side storage mechanism based on name-value pairs.</dd>
+
+
+ <dt><a href="#syntax">The HTML syntax</a></dt>
+ <dt><a href="#xhtml">The XHTML syntax</a></dt>
+
+ <dd>All of these features would be for naught if they couldn't be represented in a serialized
+ form and sent to other people, and so these sections define the syntaxes of HTML and XHTML<span
+ class="nodev">, along with rules for how to parse content using those syntaxes</span>.</dd>
+
+
+ <dt><a href="#rendering">Rendering</a></dt>
+
+ <dd>This section defines the default rendering rules for Web browsers.</dd>
+
+
+ </dl>
+
+ <p>There are also some appendices, listing <a href="#obsolete">obsolete features</a> and <a
+ href="#iana">IANA considerations</a>, and several indices.</p>
+
+
+
+ <h4>How to read this specification</h4>
+
+ <p>This specification should be read like all other specifications. First, it should be read
+ cover-to-cover, multiple times. Then, it should be read backwards at least once. Then it should be
+ read by picking random sections from the contents list and following all the cross-references.</p>
+
+ <p>As described in the conformance requirements section below, this specification describes
+ conformance criteria for a variety of conformance classes. In particular, there are conformance
+ requirements that apply to <em>producers</em>, for example authors and the documents they create,
+ and there are conformance requirements that apply to <em>consumers</em>, for example Web browsers.
+ They can be distinguished by what they are requiring: a requirement on a producer states what is
+ allowed, while a requirement on a consumer states how software is to act.</p>
+
+ <div class="example">
+
+ <p>For example, "the <code data-x="">foo</code> attribute's value must be a <span>valid
+ integer</span>" is a requirement on producers, as it lays out the allowed values; in contrast,
+ the requirement "the <code data-x="">foo</code> attribute's value must be parsed using the
+ <span>rules for parsing integers</span>" is a requirement on consumers, as it describes how to
+ process the content.</p>
+
+ </div>
+
+ <p><strong>Requirements on producers have no bearing whatsoever on consumers.</strong></p>
+
+ <div class="example">
+
+ <p>Continuing the above example, a requirement stating that a particular attribute's value is
+ constrained to being a <span>valid integer</span> emphatically does <em>not</em> imply anything
+ about the requirements on consumers. It might be that the consumers are in fact required to treat
+ the attribute as an opaque string, completely unaffected by whether the value conforms to the
+ requirements or not. It might be (as in the previous example) that the consumers are required to
+ parse the value using specific rules that define how invalid (non-numeric in this case) values
+ are to be processed.</p>
+
+ </div>
+
+
+
+ <h4>Typographic conventions</h4>
+
+ <p>This is a definition, requirement, or explanation.</p>
+
+ <p class="note">This is a note.</p>
+
+ <p class="example">This is an example.</p>
+
+ <p class="&#x0058;&#x0058;&#x0058;">This is an open issue.</p>
+
+ <p class="warning">This is a warning.</p>
+
+ <pre class="idl extract">interface <dfn data-x="">Example</dfn> {
+ // this is an IDL definition
+};</pre>
+
+ <dl class="domintro">
+
+ <dt><var data-x="">variable</var> = <var data-x="">object</var> . <code data-x="">method</code>( [ <var data-x="">optionalArgument</var> ] )</dt>
+
+ <dd>
+
+ <p>This is a note to authors describing the usage of an interface.</p>
+
+ </dd>
+
+ </dl>
+
+ <pre class="css">/* this is a CSS fragment */</pre>
+
+ <p>The defining instance of a term is marked up like <dfn data-x="x-this">this</dfn>. Uses of that
+ term are marked up like <span data-x="x-this">this</span> or like <i data-x="x-this">this</i>.</p>
+
+ <p>The defining instance of an element, attribute, or API is marked up like <dfn
+ data-x="x-that"><code>this</code></dfn>. References to that element, attribute, or API are marked
+ up like <code data-x="x-that">this</code>.</p>
+
+ <p>Other code fragments are marked up <code data-x="">like this</code>.</p>
+
+ <p>Variables are marked up like <var data-x="">this</var>.</p>
+
+ <p>In an algorithm, steps in <span data-x="synchronous section">synchronous sections</span> are
+ marked with &#x231B;.</p>
+
+ <p>In some cases, requirements are given in the form of lists with conditions and corresponding
+ requirements. In such cases, the requirements that apply to a condition are always the first set
+ of requirements that follow the condition, even in the case of there being multiple sets of
+ conditions for those requirements. Such cases are presented as follows:</p>
+
+ <dl class="switch">
+
+ <dt>This is a condition
+ <dt>This is another condition
+ <dd>This is the requirement that applies to the conditions above.
+
+ <dt>This is a third condition
+ <dd>This is the requirement that applies to the third condition.
+
+ </dl>
+
+
+
+ <h3 id="fingerprint">Privacy concerns</h3>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>Some features of HTML trade user convenience for a measure of user privacy.</p>
+
+ <p>In general, due to the Internet's architecture, a user can be distinguished from another by the
+ user's IP address. IP addresses do not perfectly match to a user; as a user moves from device to
+ device, or from network to network, their IP address will change; similarly, NAT routing, proxy
+ servers, and shared computers enable packets that appear to all come from a single IP address to
+ actually map to multiple users. Technologies such as onion routing can be used to further
+ anonymise requests so that requests from a single user at one node on the Internet appear to come
+ from many disparate parts of the network.</p>
+
+ <p>However, the IP address used for a user's requests is not the only mechanism by which a user's
+ requests could be related to each other. Cookies, for example, are designed specifically to enable
+ this, and are the basis of most of the Web's session features that enable you to log into a site
+ with which you have an account.</p>
+
+ <p>There are other mechanisms that are more subtle. Certain characteristics of a user's system can
+ be used to distinguish groups of users from each other; by collecting enough such information, an
+ individual user's browser's "digital fingerprint" can be computed, which can be as good, if not
+ better, as an IP address in ascertaining which requests are from the same user.</p>
+
+ <p>Grouping requests in this manner, especially across multiple sites, can be used for both benign
+ (and even arguably positive) purposes, as well as for malevolent purposes. An example of a
+ reasonably benign purpose would be determining whether a particular person seems to prefer sites
+ with dog illustrations as opposed to sites with cat illustrations (based on how often they visit
+ the sites in question) and then automatically using the preferred illustrations on subsequent
+ visits to participating sites. Malevolent purposes, however, could include governments combining
+ information such as the person's home address (determined from the addresses they use when getting
+ driving directions on one site) with their apparent political affiliations (determined by
+ examining the forum sites that they participate in) to determine whether the person should be
+ prevented from voting in an election.</p>
+
+ <p>Since the malevolent purposes can be remarkably evil, user agent implementors are encouraged to
+ consider how to provide their users with tools to minimise leaking information that could be used
+ to fingerprint a user.</p>
+
+ <p>Unfortunately, as the first paragraph in this section implies, sometimes there is great benefit
+ to be derived from exposing the very information that can also be used for fingerprinting
+ purposes, so it's not as easy as simply blocking all possible leaks. For instance, the ability to
+ log into a site to post under a specific identity requires that the user's requests be
+ identifiable as all being from the same user, more or less by definition. More subtly, though,
+ information such as how wide text is, which is necessary for many effects that involve drawing
+ text onto a canvas (e.g. any effect that involves drawing a border around the text) also leaks
+ information that can be used to group a user's requests. (In this case, by potentially exposing,
+ via a brute force search, which fonts a user has installed, information which can vary
+ considerably from user to user.)</p>
+
+ <p>Features in this specification which can be <dfn data-x="fingerprinting vector">used to
+ fingerprint the user</dfn> are marked as this paragraph is.
+ <!--INSERT FINGERPRINT-->
+ </p>
+
+ <p>Other features in the platform can be used for the same purpose, though, including, though not
+ limited to:</p>
+
+ <ul>
+
+ <li>The exact list of which features a user agents supports.</li>
+
+ <li>The maximum allowed stack depth for recursion in script.</li>
+
+ <li>Features that describe the user's environment, like Media Queries and the <code>Screen</code>
+ object. <a href="#refsMQ">[MQ]</a> <a href="#refsCSSOMVIEW">[CSSOMVIEW]</a></li>
+
+ <li>The user's time zone.</li>
+
+ </ul>
+
+
+
+ <h3>A quick introduction to HTML</h3>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>A basic HTML document looks like this:</p>
+
+ <pre id="intro-early-example">&lt;!DOCTYPE html>
+&lt;html>
+ &lt;head>
+ &lt;title>Sample page&lt;/title>
+ &lt;/head>
+ &lt;body>
+ &lt;h1>Sample page&lt;/h1>
+ &lt;p>This is a &lt;a href="demo.html">simple&lt;/a> sample.&lt;/p>
+ &lt;!-- this is a comment -->
+ &lt;/body>
+&lt;/html></pre>
+
+ <p>HTML documents consist of a tree of elements and text. Each element is denoted in the source by
+ a <span data-x="syntax-start-tag">start tag</span>, such as "<code data-x="">&lt;body></code>", and
+ an <span data-x="syntax-end-tag">end tag</span>, such as "<code data-x="">&lt;/body></code>".
+ (Certain start tags and end tags can in certain cases be <span
+ data-x="syntax-tag-omission">omitted</span> and are implied by other tags.)</p>
+
+ <p>Tags have to be nested such that elements are all completely within each other, without
+ overlapping:</p>
+
+ <pre class="bad">&lt;p>This is &lt;em>very &lt;strong>wrong&lt;/em>!&lt;/strong>&lt;/p></pre>
+ <pre>&lt;p>This &lt;em>is &lt;strong>correct&lt;/strong>.&lt;/em>&lt;/p></pre>
+
+ <p>This specification defines a set of elements that can be used in HTML, along with rules about
+ the ways in which the elements can be nested.</p>
+
+ <p>Elements can have attributes, which control how the elements work. In the example below, there
+ is a <span>hyperlink</span>, formed using the <code>a</code> element and its <code
+ data-x="attr-hyperlink-href">href</code> attribute:</p>
+
+ <pre>&lt;a href="demo.html">simple&lt;/a></pre>
+
+ <p><span data-x="syntax-attributes">Attributes</span> are placed inside the start tag, and consist
+ of a <span data-x="syntax-attribute-name">name</span> and a <span
+ data-x="syntax-attribute-value">value</span>, separated by an "<code data-x="">=</code>" character.
+ The attribute value can remain <a href="#unquoted">unquoted</a> if it doesn't contain <span
+ data-x="space character">space characters</span> or any of <code data-x="">"</code> <code
+ data-x="">'</code> <code data-x="">`</code> <code data-x="">=</code> <code data-x="">&lt;</code> or
+ <code data-x="">&gt;</code>. Otherwise, it has to be quoted using either single or double quotes.
+ The value, along with the "<code data-x="">=</code>" character, can be omitted altogether if the
+ value is the empty string.</p>
+
+ <pre>&lt;!-- empty attributes -->
+&lt;input name=address disabled>
+&lt;input name=address disabled="">
+
+&lt;!-- attributes with a value -->
+&lt;input name=address maxlength=200>
+&lt;input name=address maxlength='200'>
+&lt;input name=address maxlength="200"></pre>
+
+ <p>HTML user agents (e.g. Web browsers) then <i>parse</i> this markup, turning it into a DOM
+ (Document Object Model) tree. A DOM tree is an in-memory representation of a document.</p>
+
+ <p>DOM trees contain several kinds of nodes, in particular a <code>DocumentType</code> node,
+ <code>Element</code> nodes, <code>Text</code> nodes, <code>Comment</code> nodes, and in some cases
+ <code>ProcessingInstruction</code> nodes.</p>
+
+ <p>The <a href="#intro-early-example">markup snippet at the top of this section</a> would be
+ turned into the following DOM tree:</p>
+
+ <ul class="domTree"><li class="t10">DOCTYPE: <code data-x="">html</code></li><li class="t1"><code>html</code><ul><li class="t1"><code>head</code><ul><li class="t3"><code>#text</code>: <span data-x="">&#x23CE;&#x2423;&#x2423;</span></li><li class="t1"><code>title</code><ul><li class="t3"><code>#text</code>: <span data-x="">Sample page</span></li></ul></li><li class="t3"><code>#text</code>: <span data-x="">&#x23CE;&#x2423;</span></li></ul></li><li class="t3"><code>#text</code>: <span data-x="">&#x23CE;&#x2423;</span></li><li class="t1"><code>body</code><ul><li class="t3"><code>#text</code>: <span data-x="">&#x23CE;&#x2423;&#x2423;</span></li><li class="t1"><code>h1</code><ul><li class="t3"><code>#text</code>: <span data-x="">Sample page</span></li></ul></li><li class="t3"><code>#text</code>: <span data-x="">&#x23CE;&#x2423;&#x2423;</span></li><li class="t1"><code>p</code><ul><li class="t3"><code>#text</code>: <span data-x="">This is a <!--grammar-check-override--></span></li><li class="t1"><code>a</code> <span data-x="" class="t2"><code class="attribute name">href</code>="<code class="attribute value">demo.html</code>"</span><ul><li class="t3"><code>#text</code>: <span data-x="">simple</span></li></ul></li><li class="t3"><code>#text</code>: <span data-x=""> sample.</span></li></ul></li><li class="t3"><code>#text</code>: <span data-x="">&#x23CE;&#x2423;&#x2423;</span></li><li class="t8"><code>#comment</code>: <span data-x=""> this is a comment </span></li><li class="t3"><code>#text</code>: <span data-x="">&#x23CE;&#x2423;&#x23CE;</span></li></ul></li></ul></li></ul>
+
+ <p>The <span>root element</span> of this tree is the <code>html</code> element, which is the
+ element always found at the root of HTML documents. It contains two elements, <code>head</code>
+ and <code>body</code>, as well as a <code>Text</code> node between them.</p>
+
+ <p>There are many more <code>Text</code> nodes in the DOM tree than one would initially expect,
+ because the source contains a number of spaces (represented here by "&#x2423;") and line breaks
+ ("&#x23CE;") that all end up as <code>Text</code> nodes in the DOM. However, for historical
+ reasons not all of the spaces and line breaks in the original markup appear in the DOM. In
+ particular, all the whitespace before <code>head</code> start tag ends up being dropped silently,
+ and all the whitespace after the <code>body</code> end tag ends up placed at the end of the
+ <code>body</code>.</p>
+
+ <p>The <code>head</code> element contains a <code>title</code> element, which itself contains a
+ <code>Text</code> node with the text "Sample page". Similarly, the <code>body</code> element
+ contains an <code>h1</code> element, a <code>p</code> element, and a comment.</p>
+
+ <hr>
+
+ <p>This DOM tree can be manipulated from scripts in the page. Scripts (typically in JavaScript)
+ are small programs that can be embedded using the <code>script</code> element or using <span>event
+ handler content attributes</span>. For example, here is a form with a script that sets the value
+ of the form's <code>output</code> element to say "Hello World":</p>
+
+ <pre>&lt;<span>form</span> <span data-x="attr-form-name">name</span>="main">
+ Result: &lt;<span>output</span> <span data-x="attr-fe-name">name</span>="result">&lt;/output>
+ &lt;<span>script</span>>
+ <span data-x="Document">document</span>.<span data-x="dom-document-forms">forms</span>.main.<span data-x="dom-form-elements">elements</span>.result.<span data-x="dom-output-value">value</span> = 'Hello World';
+ &lt;/script>
+&lt;/form></pre>
+
+ <p>Each element in the DOM tree is represented by an object, and these objects have APIs so that
+ they can be manipulated. For instance, a link (e.g. the <code>a</code> element in the tree above)
+ can have its "<code data-x="attr-hyperlink-href">href</code>" attribute changed in several
+ ways:</p>
+
+ <pre>var a = <span data-x="Document">document</span>.<span data-x="dom-document-links">links</span>[0]; // obtain the first link in the document
+a.<span data-x="dom-url-href">href</span> = 'sample.html'; // change the destination URL of the link
+a.<span data-x="dom-url-protocol">protocol</span> = 'https'; // change just the scheme part of the URL
+a.setAttribute('href', 'http://example.com/'); // change the content attribute directly</pre>
+
+ <p>Since DOM trees are used as the way to represent HTML documents when they are processed and
+ presented by implementations (especially interactive implementations like Web browsers), this
+ specification is mostly phrased in terms of DOM trees, instead of the markup described above.</p>
+
+ <hr>
+
+ <p>HTML documents represent a media-independent description of interactive content. HTML documents
+ might be rendered to a screen, or through a speech synthesiser, or on a braille display. To
+ influence exactly how such rendering takes place, authors can use a styling language such as
+ CSS.</p>
+
+ <p>In the following example, the page has been made yellow-on-blue using CSS.</p>
+
+ <pre>&lt;!DOCTYPE html>
+&lt;html>
+ &lt;head>
+ &lt;title>Sample styled page&lt;/title>
+ &lt;style>
+ body { background: navy; color: yellow; }
+ &lt;/style>
+ &lt;/head>
+ &lt;body>
+ &lt;h1>Sample styled page&lt;/h1>
+ &lt;p>This page is just a demo.&lt;/p>
+ &lt;/body>
+&lt;/html></pre>
+
+ <p>For more details on how to use HTML, authors are encouraged to consult tutorials and guides.
+ Some of the examples included in this specification might also be of use, but the novice author is
+ cautioned that this specification, by necessity, defines the language with a level of detail that
+ might be difficult to understand at first.</p>
+
+
+
+<!--ADD-TOPIC:Security-->
+ <h4>Writing secure applications with HTML</h4>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>When HTML is used to create interactive sites, care needs to be taken to avoid introducing
+ vulnerabilities through which attackers can compromise the integrity of the site itself or of the
+ site's users.</p>
+
+ <p>A comprehensive study of this matter is beyond the scope of this document, and authors are
+ strongly encouraged to study the matter in more detail. However, this section attempts to provide
+ a quick introduction to some common pitfalls in HTML application development.</p>
+
+ <p>The security model of the Web is based on the concept of "origins", and correspondingly many of
+ the potential attacks on the Web involve cross-origin actions. <a
+ href="#refsORIGIN">[ORIGIN]</a></p>
+
+ <dl>
+
+ <dt>Not validating user input</dt>
+ <dt>Cross-site scripting (XSS)</dt>
+ <dt>SQL injection</dt>
+
+ <dd>
+
+ <p>When accepting untrusted input, e.g. user-generated content such as text comments, values in
+ URL parameters, messages from third-party sites, etc, it is imperative that the data be
+ validated before use, and properly escaped when displayed. Failing to do this can allow a
+ hostile user to perform a variety of attacks, ranging from the potentially benign, such as
+ providing bogus user information like a negative age, to the serious, such as running scripts
+ every time a user looks at a page that includes the information, potentially propagating the
+ attack in the process, to the catastrophic, such as deleting all data in the server.</p>
+
+ <p>When writing filters to validate user input, it is imperative that filters always be
+ whitelist-based, allowing known-safe constructs and disallowing all other input. Blacklist-based
+ filters that disallow known-bad inputs and allow everything else are not secure, as not
+ everything that is bad is yet known (for example, because it might be invented in the
+ future).</p>
+
+ <div class="example">
+
+ <p>For example, suppose a page looked at its URL's query string to determine what to display,
+ and the site then redirected the user to that page to display a message, as in:</p>
+
+ <pre>&lt;ul>
+ &lt;li>&lt;a href="message.cgi?say=Hello">Say Hello&lt;/a>
+ &lt;li>&lt;a href="message.cgi?say=Welcome">Say Welcome&lt;/a>
+ &lt;li>&lt;a href="message.cgi?say=Kittens">Say Kittens&lt;/a>
+&lt;/ul></pre>
+
+ <p>If the message was just displayed to the user without escaping, a hostile attacker could
+ then craft a URL that contained a script element:</p>
+
+ <pre>http://example.com/message.cgi?say=%3Cscript%3Ealert%28%27Oh%20no%21%27%29%3C/script%3E</pre>
+
+ <p>If the attacker then convinced a victim user to visit this page, a script of the attacker's
+ choosing would run on the page. Such a script could do any number of hostile actions, limited
+ only by what the site offers: if the site is an e-commerce shop, for instance, such a script
+ could cause the user to unknowingly make arbitrarily many unwanted purchases.</p>
+
+ <p>This is called a cross-site scripting attack.</p>
+
+ </div>
+
+ <p>There are many constructs that can be used to try to trick a site into executing code. Here
+ are some that authors are encouraged to consider when writing whitelist filters:</p>
+
+ <ul>
+
+ <li>When allowing harmless-seeming elements like <code>img</code>, it is important to whitelist
+ any provided attributes as well. If one allowed all attributes then an attacker could, for
+ instance, use the <code data-x="handler-onload">onload</code> attribute to run arbitrary
+ script.</li>
+
+ <li>When allowing URLs to be provided (e.g. for links), the scheme of each URL also needs to be
+ explicitly whitelisted, as there are many schemes that can be abused. The most prominent
+ example is "<code data-x="javascript-protocol">javascript:</code>", but user agents can
+ implement (and indeed, have historically implemented) others.</li> <!-- IE had vbscript:,
+ Netscape had livescript:, etc. -->
+
+ <li>Allowing a <code>base</code> element to be inserted means any <code>script</code> elements
+ in the page with relative links can be hijacked, and similarly that any form submissions can
+ get redirected to a hostile site.</li>
+
+ </ul>
+
+ </dd>
+
+
+ <dt>Cross-site request forgery (CSRF)</dt>
+
+ <dd>
+
+ <p>If a site allows a user to make form submissions with user-specific side-effects, for example
+ posting messages on a forum under the user's name, making purchases, or applying for a passport,
+ it is important to verify that the request was made by the user intentionally, rather than by
+ another site tricking the user into making the request unknowingly.</p>
+
+ <p>This problem exists because HTML forms can be submitted to other origins.</p>
+
+ <p>Sites can prevent such attacks by populating forms with user-specific hidden tokens, or by
+ checking <code data-x="http-origin">Origin</code> headers on all requests.</p>
+
+ </dd>
+
+
+
+ <dt>Clickjacking</dt>
+
+ <dd>
+
+ <p>A page that provides users with an interface to perform actions that the user might not wish
+ to perform needs to be designed so as to avoid the possibility that users can be tricked into
+ activating the interface.</p>
+
+ <p>One way that a user could be so tricked is if a hostile site places the victim site in a
+ small <code>iframe</code> and then convinces the user to click, for instance by having the user
+ play a reaction game. Once the user is playing the game, the hostile site can quickly position
+ the iframe under the mouse cursor just as the user is about to click, thus tricking the user
+ into clicking the victim site's interface.</p>
+
+ <p>To avoid this, sites that do not expect to be used in frames are encouraged to only enable
+ their interface if they detect that they are not in a frame (e.g. by comparing the <code
+ data-x="dom-window">window</code> object to the value of the <code data-x="dom-top">top</code>
+ attribute).</p>
+
+ </dd>
+
+ </dl>
+<!--REMOVE-TOPIC:Security-->
+
+
+ <h4>Common pitfalls to avoid when using the scripting APIs</h4>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>Scripts in HTML have "run-to-completion" semantics, meaning that the browser will generally run
+ the script uninterrupted before doing anything else, such as firing further events or continuing
+ to parse the document.</p>
+
+ <p>On the other hand, parsing of HTML files happens asynchronously and incrementally, meaning that
+ the parser can pause at any point to let scripts run. This is generally a good thing, but it does
+ mean that authors need to be careful to avoid hooking event handlers after the events could have
+ possibly fired.</p>
+
+ <p>There are two techniques for doing this reliably: use <span>event handler content
+ attributes</span>, or create the element and add the event handlers in the same script. The latter
+ is safe because, as mentioned earlier, scripts are run to completion before further events can
+ fire.</p>
+
+ <div class="example">
+
+ <p>One way this could manifest itself is with <code>img</code> elements and the <code
+ data-x="event-load">load</code> event. The event could fire as soon as the element has been
+ parsed, especially if the image has already been cached (which is common).</p>
+
+ <p>Here, the author uses the <code data-x="handler-onload">onload</code> handler on an
+ <code>img</code> element to catch the <code data-x="event-load">load</code> event:</p>
+
+ <pre>&lt;img src="games.png" alt="Games" onload="gamesLogoHasLoaded(event)"></pre>
+
+ <p>If the element is being added by script, then so long as the event handlers are added in the
+ same script, the event will still not be missed:</p>
+
+ <pre>&lt;script>
+ var img = new Image();
+ img.src = 'games.png';
+ img.alt = 'Games';
+ img.onload = gamesLogoHasLoaded;
+ // img.addEventListener('load', gamesLogoHasLoaded, false); // would work also
+&lt;/script></pre>
+
+ <p>However, if the author first created the <code>img</code> element and then in a separate
+ script added the event listeners, there's a chance that the <code data-x="event-load">load</code>
+ event would be fired in between, leading it to be missed:</p>
+
+ <pre class="bad">&lt;!-- Do not use this style, it has a race condition! -->
+ &lt;img id="games" src="games.png" alt="Games">
+ &lt;!-- the 'load' event might fire here while the parser is taking a
+ break, in which case you will not see it! -->
+ &lt;script>
+ var img = document.getElementById('games');
+ img.onload = gamesLogoHasLoaded; // might never fire!
+ &lt;/script></pre>
+
+ </div>
+
+
+
+ <h4>How to catch mistakes when writing HTML: validators and conformance checkers</h4>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>Authors are encouraged to make use of conformance checkers (also known as <i>validators</i>) to
+ catch common mistakes. The WHATWG maintains a list of such tools at: <a
+ href="http://validator.whatwg.org/">http://validator.whatwg.org/</a></p>
+
+
+
+ <h3>Conformance requirements for authors</h3>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>Unlike previous versions of the HTML specification, this specification defines in some detail
+ the required processing for invalid documents as well as valid documents.</p> <!-- This has led to
+ some questioning the purpose of conformance criteria: if there is no ambiguity in how something
+ will be processed, why disallow it? -->
+
+ <p>However, even though the processing of invalid content is in most cases well-defined,
+ conformance requirements for documents are still important: in practice, interoperability (the
+ situation in which all implementations process particular content in a reliable and identical or
+ equivalent way) is not the only goal of document conformance requirements. This section details
+ some of the more common reasons for still distinguishing between a conforming document and one
+ with errors.</p>
+
+
+ <h4>Presentational markup</h4>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>The majority of presentational features from previous versions of HTML are no longer allowed.
+ Presentational markup in general has been found to have a number of problems:</p>
+
+ <dl>
+
+ <dt>The use of presentational elements leads to poorer accessibility</dt>
+
+ <dd>
+
+ <p>While it is possible to use presentational markup in a way that provides users of assistive
+ technologies (ATs) with an acceptable experience (e.g. using ARIA), doing so is significantly
+ more difficult than doing so when using semantically-appropriate markup. Furthermore, even using
+ such techniques doesn't help make pages accessible for non-AT non-graphical users, such as users
+ of text-mode browsers.</p>
+
+ <p>Using media-independent markup, on the other hand, provides an easy way for documents to be
+ authored in such a way that they work for more users (e.g. text browsers).</p>
+
+ </dd>
+
+
+ <dt>Higher cost of maintenance</dt>
+
+ <dd>
+
+ <p>It is significantly easier to maintain a site written in such a way that the markup is
+ style-independent. For example, changing the colour of a site that uses
+ <code>&lt;font&nbsp;color=""></code> throughout requires changes across the entire site, whereas
+ a similar change to a site based on CSS can be done by changing a single file.</p>
+
+ </dd>
+
+
+ <dt>Larger document sizes</dt>
+
+ <dd>
+
+ <p>Presentational markup tends to be much more redundant, and thus results in larger document
+ sizes.</p>
+
+ </dd>
+
+ </dl>
+
+ <p>For those reasons, presentational markup has been removed from HTML in this version. This
+ change should not come as a surprise; HTML4 deprecated presentational markup many years ago and
+ provided a mode (HTML4 Transitional) to help authors move away from presentational markup; later,
+ XHTML 1.1 went further and obsoleted those features altogether.</p>
+
+ <p>The only remaining presentational markup features in HTML are the <code
+ data-x="attr-style">style</code> attribute and the <code>style</code> element. Use of the <code
+ data-x="attr-style">style</code> attribute is somewhat discouraged in production environments, but
+ it can be useful for rapid prototyping (where its rules can be directly moved into a separate
+ style sheet later) and for providing specific styles in unusual cases where a separate style sheet
+ would be inconvenient. Similarly, the <code>style</code> element can be useful in syndication or
+ for page-specific styles, but in general an external style sheet is likely to be more convenient
+ when the styles apply to multiple pages.</p>
+
+ <p>It is also worth noting that some elements that were previously presentational have been
+ redefined in this specification to be media-independent: <code>b</code>, <code>i</code>,
+ <code>hr</code>, <code>s</code>, <code>small</code>, and <code>u</code>.</p>
+
+
+ <h4>Syntax errors</h4>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>The syntax of HTML is constrained to avoid a wide variety of problems.</p>
+
+ <dl>
+
+ <dt>Unintuitive error-handling behavior</dt>
+
+ <dd>
+
+ <p>Certain invalid syntax constructs, when parsed, result in DOM trees that are highly
+ unintuitive.</p>
+
+ <div class="example">
+
+ <p>For example, the following markup fragment results in a DOM with an <code>hr</code> element
+ that is an <em>earlier</em> sibling of the corresponding <code>table</code> element:</p>
+
+ <pre class="bad">&lt;table>&lt;hr>...</pre>
+
+ </div>
+
+ </dd>
+
+
+ <dt>Errors with optional error recovery</dt>
+
+ <dd>
+
+ <p>To allow user agents to be used in controlled environments without having to implement the
+ more bizarre and convoluted error handling rules, user agents are permitted to fail whenever
+ encountering a <span>parse error</span>.</p>
+
+ </dd>
+
+
+ <dt>Errors where the error-handling behavior is not compatible with streaming user agents</dt>
+
+ <dd>
+
+ <p>Some error-handling behavior, such as the behavior for the <code
+ data-x="">&lt;table>&lt;hr>...</code> example mentioned above, are incompatible with streaming
+ user agents (user agents that process HTML files in one pass, without storing state). To avoid
+ interoperability problems with such user agents, any syntax resulting in such behavior is
+ considered invalid.</p>
+
+ </dd>
+
+
+ <dt>Errors that can result in infoset coercion</dt>
+
+ <dd>
+
+ <p>When a user agent based on XML is connected to an HTML parser, it is possible that certain
+ invariants that XML enforces, such as comments never containing two consecutive hyphens, will be
+ violated by an HTML file. Handling this can require that the parser coerce the HTML DOM into an
+ XML-compatible infoset. Most syntax constructs that require such handling are considered
+ invalid.</p>
+
+ </dd>
+
+
+ <dt>Errors that result in disproportionally poor performance</dt>
+
+ <dd>
+
+ <p>Certain syntax constructs can result in disproportionally poor performance. To discourage the
+ use of such constructs, they are typically made non-conforming.</p>
+
+ <div class="example">
+
+ <p>For example, the following markup results in poor performance, since all the unclosed
+ <code>i</code> elements have to be reconstructed in each paragraph, resulting in progressively
+ more elements in each paragraph:</p>
+
+ <pre class="bad">&lt;p>&lt;i>He dreamt.
+&lt;p>&lt;i>He dreamt that he ate breakfast.
+&lt;p>&lt;i>Then lunch.
+&lt;p>&lt;i>And finally dinner.</pre>
+
+ <p>The resulting DOM for this fragment would be:</p>
+
+ <ul class="domTree"><li class="t1"><code>p</code><ul><li class="t1"><code>i</code><ul><li class="t3"><code>#text</code>: <span data-x="">He dreamt.</span></li></ul></li></ul></li><li class="t1"><code>p</code><ul><li class="t1"><code>i</code><ul><li class="t1"><code>i</code><ul><li class="t3"><code>#text</code>: <span data-x="">He dreamt that he ate breakfast.</span></li></ul></li></ul></li></ul></li><li class="t1"><code>p</code><ul><li class="t1"><code>i</code><ul><li class="t1"><code>i</code><ul><li class="t1"><code>i</code><ul><li class="t3"><code>#text</code>: <span data-x="">Then lunch.</span></li></ul></li></ul></li></ul></li></ul></li><li class="t1"><code>p</code><ul><li class="t1"><code>i</code><ul><li class="t1"><code>i</code><ul><li class="t1"><code>i</code><ul><li class="t1"><code>i</code><ul><li class="t3"><code>#text</code>: <span data-x="">And finally dinner.</span></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul>
+
+ </div>
+
+ </dd>
+
+
+ <dt>Errors involving fragile syntax constructs</dt>
+
+ <dd>
+
+ <p>There are syntax constructs that, for historical reasons, are relatively fragile. To help
+ reduce the number of users who accidentally run into such problems, they are made
+ non-conforming.</p>
+
+ <div class="example">
+
+ <p>For example, the parsing of certain named character references in attributes happens even
+ with the closing semicolon being omitted. It is safe to include an ampersand followed by
+ letters that do not form a named character reference, but if the letters are changed to a
+ string that <em>does</em> form a named character reference, they will be interpreted as that
+ character instead.</p>
+
+ <p>In this fragment, the attribute's value is "<code data-x="">?bill&amp;ted</code>":</p>
+
+ <pre class="bad">&lt;a href="?bill&amp;ted">Bill and Ted&lt;/a></pre>
+
+ <p>In the following fragment, however, the attribute's value is actually "<code
+ data-x="">?art&copy;</code>", <em>not</em> the intended "<code data-x="">?art&amp;copy</code>",
+ because even without the final semicolon, "<code data-x="">&amp;copy</code>" is handled the same
+ as "<code data-x="">&amp;copy;</code>" and thus gets interpreted as "<code
+ data-x="">&copy;</code>":</p>
+
+ <pre class="bad">&lt;a href="?art&amp;copy">Art and Copy&lt;/a></pre>
+
+ <p>To avoid this problem, all named character references are required to end with a semicolon,
+ and uses of named character references without a semicolon are flagged as errors.</p>
+
+ <p>Thus, the correct way to express the above cases is as
+ follows:</p>
+
+ <pre>&lt;a href="?bill&amp;ted">Bill and Ted&lt;/a> &lt;!-- &amp;ted is ok, since it's not a named character reference --></pre>
+ <pre>&lt;a href="?art&amp;amp;copy">Art and Copy&lt;/a> &lt;!-- the &amp; has to be escaped, since &amp;copy <em>is</em> a named character reference --></pre>
+
+ </div>
+
+ </dd>
+
+
+ <dt>Errors involving known interoperability problems in legacy user agents</dt>
+
+ <dd>
+
+ <p>Certain syntax constructs are known to cause especially subtle or serious problems in legacy
+ user agents, and are therefore marked as non-conforming to help authors avoid them.</p>
+
+ <div class="example">
+
+ <p>For example, this is why the U+0060 GRAVE ACCENT character (`) is not allowed in unquoted
+ attributes. In certain legacy user agents, <!-- namely IE --> it is sometimes treated as a
+ quote character.</p>
+
+ </div>
+
+ <div class="example">
+
+ <p>Another example of this is the DOCTYPE, which is required to trigger <span>no-quirks
+ mode</span>, because the behavior of legacy user agents in <span>quirks mode</span> is often
+ largely undocumented.</p>
+
+ </div>
+
+ </dd>
+
+
+<!--ADD-TOPIC:Security-->
+ <dt>Errors that risk exposing authors to security attacks</dt>
+
+ <dd>
+
+ <p>Certain restrictions exist purely to avoid known security problems.</p>
+
+ <div class="example">
+
+ <p>For example, the restriction on using UTF-7 exists purely to avoid authors falling prey to a
+ known cross-site-scripting attack using UTF-7. <a href="#refsUTF7">[UTF7]</a></p>
+
+ </div>
+
+ </dd>
+<!--REMOVE-TOPIC:Security-->
+
+
+ <dt>Cases where the author's intent is unclear</dt>
+
+ <dd>
+
+ <p>Markup where the author's intent is very unclear is often made non-conforming. Correcting
+ these errors early makes later maintenance easier.</p>
+
+ <div class="example">
+
+ <p>For example, it is unclear whether the author intended the following to be an
+ <code>h1</code> heading or an <code>h2</code> heading:</p>
+
+ <pre class="bad">&lt;h1>Contact details&lt;/h2></pre>
+
+ </div>
+
+ </dd>
+
+
+ <dt>Cases that are likely to be typos</dt>
+
+ <dd>
+
+ <p>When a user makes a simple typo, it is helpful if the error can be caught early, as this can
+ save the author a lot of debugging time. This specification therefore usually considers it an
+ error to use element names, attribute names, and so forth, that do not match the names defined
+ in this specification.</p>
+
+ <div class="example">
+
+ <p>For example, if the author typed <code>&lt;capton></code> instead of
+ <code>&lt;caption></code>, this would be flagged as an error and the author could correct the
+ typo immediately.</p>
+
+ </div>
+
+ </dd>
+
+
+ <dt>Errors that could interfere with new syntax in the future</dt>
+
+ <dd>
+
+ <p>In order to allow the language syntax to be extended in the future, certain otherwise
+ harmless features are disallowed.</p>
+
+ <div class="example">
+
+ <p>For example, "attributes" in end tags are ignored currently, but they are invalid, in case a
+ future change to the language makes use of that syntax feature without conflicting with
+ already-deployed (and valid!) content.</p>
+
+ </div>
+
+ </dd>
+
+
+ </dl>
+
+ <p>Some authors find it helpful to be in the practice of always quoting all attributes and always
+ including all optional tags, preferring the consistency derived from such custom over the minor
+ benefits of terseness afforded by making use of the flexibility of the HTML syntax. To aid such
+ authors, conformance checkers can provide modes of operation wherein such conventions are
+ enforced.</p>
+
+
+
+ <h4>Restrictions on content models and on attribute values</h4>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>Beyond the syntax of the language, this specification also places restrictions on how elements
+ and attributes can be specified. These restrictions are present for similar reasons:</p>
+
+ <dl>
+
+
+ <dt>Errors involving content with dubious semantics</dt>
+
+ <dd>
+
+ <p>To avoid misuse of elements with defined meanings, content models are defined that restrict
+ how elements can be nested when such nestings would be of dubious value.</p>
+
+ <p class="example">For example, this specification disallows nesting a <code>section</code>
+ element inside a <code>kbd</code> element, since it is highly unlikely for an author to indicate
+ that an entire section should be keyed in.</p>
+
+ </dd>
+
+
+ <dt>Errors that involve a conflict in expressed semantics</dt>
+
+ <dd>
+
+ <p>Similarly, to draw the author's attention to mistakes in the use of elements, clear
+ contradictions in the semantics expressed are also considered conformance errors.</p>
+
+ <div class="example">
+
+ <p>In the fragments below, for example, the semantics are nonsensical: a separator cannot
+ simultaneously be a cell, nor can a radio button be a progress bar.</p>
+
+ <pre class="bad">&lt;hr role="cell"></pre>
+ <pre class="bad">&lt;input type=radio role=progressbar></pre>
+
+ </div>
+
+ <p class="example">Another example is the restrictions on the content models of the
+ <code>ul</code> element, which only allows <code>li</code> element children. Lists by definition
+ consist just of zero or more list items, so if a <code>ul</code> element contains something
+ other than an <code>li</code> element, it's not clear what was meant.</p>
+
+ </dd>
+
+
+ <dt>Cases where the default styles are likely to lead to confusion</dt>
+
+ <dd>
+
+ <p>Certain elements have default styles or behaviors that make certain combinations likely to
+ lead to confusion. Where these have equivalent alternatives without this problem, the confusing
+ combinations are disallowed.</p>
+
+ <p class="example">For example, <code>div</code> elements are rendered as block boxes, and
+ <code>span</code> elements as inline boxes. Putting a block box in an inline box is
+ unnecessarily confusing; since either nesting just <code>div</code> elements, or nesting just
+ <code>span</code> elements, or nesting <code>span</code> elements inside <code>div</code>
+ elements all serve the same purpose as nesting a <code>div</code> element in a <code>span</code>
+ element, but only the latter involves a block box in an inline box, the latter combination is
+ disallowed.</p>
+
+ <p class="example">Another example would be the way <span>interactive content</span> cannot be
+ nested. For example, a <code>button</code> element cannot contain a <code>textarea</code>
+ element. This is because the default behavior of such nesting interactive elements would be
+ highly confusing to users. Instead of nesting these elements, they can be placed side by
+ side.</p>
+
+ </dd>
+
+
+ <dt>Errors that indicate a likely misunderstanding of the specification</dt>
+
+ <dd>
+
+ <p>Sometimes, something is disallowed because allowing it would likely cause author
+ confusion.</p>
+
+ <p class="example">For example, setting the <code data-x="attr-fe-disabled">disabled</code>
+ attribute to the value "<code data-x="">false</code>" is disallowed, because despite the
+ appearance of meaning that the element is enabled, it in fact means that the element is
+ <em>disabled</em> (what matters for implementations is the presence of the attribute, not its
+ value).</p>
+
+ </dd>
+
+
+ <dt>Errors involving limits that have been imposed merely to simplify the language</dt>
+
+ <dd>
+
+ <p>Some conformance errors simplify the language that authors need to learn.</p>
+
+ <p class="example">For example, the <code>area</code> element's <code
+ data-x="attr-area-shape">shape</code> attribute, despite accepting both <code
+ data-x="attr-area-shape-keyword-circ">circ</code> and <code
+ data-x="attr-area-shape-keyword-circle">circle</code> values in practice as synonyms, disallows
+ the use of the <code data-x="attr-area-shape-keyword-circ">circ</code> value, so as to simplify
+ tutorials and other learning aids. There would be no benefit to allowing both, but it would
+ cause extra confusion when teaching the language.</p>
+
+ </dd>
+
+
+ <dt>Errors that involve peculiarities of the parser</dt>
+
+ <dd>
+
+ <p>Certain elements are parsed in somewhat eccentric ways (typically for historical reasons),
+ and their content model restrictions are intended to avoid exposing the author to these
+ issues.</p>
+
+ <div class="example">
+
+ <p>For example, a <code>form</code> element isn't allowed inside <span>phrasing content</span>,
+ because when parsed as HTML, a <code>form</code> element's start tag will imply a
+ <code>p</code> element's end tag. Thus, the following markup results in two <span
+ data-x="paragraph">paragraphs</span>, not one:</p>
+
+ <pre>&lt;p>Welcome. &lt;form>&lt;label>Name:&lt;/label> &lt;input>&lt;/form></pre>
+
+ <p>It is parsed exactly like the following:</p>
+
+ <pre>&lt;p>Welcome. &lt;/p>&lt;form>&lt;label>Name:&lt;/label> &lt;input>&lt;/form></pre>
+
+ </div>
+
+ </dd>
+
+
+ <dt>Errors that would likely result in scripts failing in hard-to-debug ways</dt>
+
+ <dd>
+
+ <p>Some errors are intended to help prevent script problems that would be hard to debug.</p>
+
+ <p class="example">This is why, for instance, it is non-conforming to have two <code
+ data-x="attr-id">id</code> attributes with the same value. Duplicate IDs lead to the wrong
+ element being selected, with sometimes disastrous effects whose cause is hard to determine.</p>
+
+ </dd>
+
+
+ <dt>Errors that waste authoring time</dt>
+
+ <dd>
+
+ <p>Some constructs are disallowed because historically they have been the cause of a lot of
+ wasted authoring time, and by encouraging authors to avoid making them, authors can save time in
+ future efforts.</p>
+
+ <p class="example">For example, a <code>script</code> element's <code
+ data-x="attr-script-src">src</code> attribute causes the element's contents to be ignored.
+ However, this isn't obvious, especially if the element's contents appear to be executable script
+ &mdash; which can lead to authors spending a lot of time trying to debug the inline script
+ without realizing that it is not executing. To reduce this problem, this specification makes it
+ non-conforming to have executable script in a <code>script</code> element when the <code
+ data-x="attr-script-src">src</code> attribute is present. This means that authors who are
+ validating their documents are less likely to waste time with this kind of mistake.</p>
+
+ </dd>
+
+
+ <dt>Errors that involve areas that affect authors migrating to and from XHTML</dt>
+
+ <dd>
+
+ <p>Some authors like to write files that can be interpreted as both XML and HTML with similar
+ results. Though this practice is discouraged in general due to the myriad of subtle
+ complications involved (especially when involving scripting, styling, or any kind of automated
+ serialisation), this specification has a few restrictions intended to at least somewhat mitigate
+ the difficulties. This makes it easier for authors to use this as a transitionary step when
+ migrating between HTML and XHTML.</p>
+
+ <p class="example">For example, there are somewhat complicated rules surrounding the <code
+ data-x="attr-lang">lang</code> and <code data-x="attr-xml-lang">xml:lang</code> attributes
+ intended to keep the two synchronized.</p>
+
+ <p class="example">Another example would be the restrictions on the values of <code
+ data-x="">xmlns</code> attributes in the HTML serialisation, which are intended to ensure that
+ elements in conforming documents end up in the same namespaces whether processed as HTML or
+ XML.</p>
+
+ </dd>
+
+
+ <dt>Errors that involve areas reserved for future expansion</dt>
+
+ <dd>
+
+ <p>As with the restrictions on the syntax intended to allow for new syntax in future revisions
+ of the language, some restrictions on the content models of elements and values of attributes
+ are intended to allow for future expansion of the HTML vocabulary.</p>
+
+ <p class="example">For example, limiting the values of the <code
+ data-x="attr-hyperlink-target">target</code> attribute that start with an U+005F LOW LINE
+ character (_) to only specific predefined values allows new predefined values to be introduced
+ at a future time without conflicting with author-defined values.</p>
+
+ </dd>
+
+
+ <dt>Errors that indicate a mis-use of other specifications</dt>
+
+ <dd>
+
+ <p>Certain restrictions are intended to support the restrictions made by other
+ specifications.</p>
+
+ <p class="example">For example, requiring that attributes that take media queries use only
+ <em>valid</em> media queries reinforces the importance of following the conformance rules of
+ that specification.</p>
+
+ </dd>
+
+ </dl>
+
+
+
+ <h3>Suggested reading</h3>
+
+ <!-- NON-NORMATIVE SECTION -->
+
+ <p>The following documents might be of interest to readers of this specification.</p>
+
+ <dl>
+
+ <dt><cite>Character Model for the World Wide Web 1.0: Fundamentals</cite> <a href="#refsCHARMOD">[CHARMOD]</a></dt>
+
+ <dd><blockquote><p>This Architectural Specification provides authors of specifications, software
+ developers, and content developers with a common reference for interoperable text manipulation on
+ the World Wide Web, building on the Universal Character Set, defined jointly by the Unicode
+ Standard and ISO/IEC 10646. Topics addressed include use of the terms 'character', 'encoding' and
+ 'string', a reference processing model, choice and identification of character encodings,
+ character escaping, and string indexing.</p></blockquote></dd>
+
+ <dt><cite>Unicode Security Considerations</cite> <a href="#refsUTR36">[UTR36]</a></dt>
+
+ <dd><blockquote><p>Because Unicode contains such a large number of characters and incorporates
+ the varied writing systems of the world, incorrect usage can expose programs or systems to
+ possible security attacks. This is especially important as more and more products are
+ internationalized. This document describes some of the security considerations that programmers,
+ system analysts, standards developers, and users should take into account, and provides specific
+ recommendations to reduce the risk of problems.</p></blockquote></dd>
+
+ <dt><cite>Web Content Accessibility Guidelines (WCAG) 2.0</cite> <a href="#refsWCAG">[WCAG]</a></dt>
+
+ <dd><blockquote><p>Web Content Accessibility Guidelines (WCAG) 2.0 covers a wide range of
+ recommendations for making Web content more accessible. Following these guidelines will make
+ content accessible to a wider range of people with disabilities, including blindness and low
+ vision, deafness and hearing loss, learning disabilities, cognitive limitations, limited
+ movement, speech disabilities, photosensitivity and combinations of these. Following these
+ guidelines will also often make your Web content more usable to users in
+ general.</p></blockquote></dd>
+
+ <dt class="nodev"><cite>Authoring Tool Accessibility Guidelines (ATAG) 2.0</cite> <a href="#refsATAG">[ATAG]</a></dt>
+
+ <dd class="nodev"><blockquote><p>This specification provides guidelines for designing Web content
+ authoring tools that are more accessible for people with disabilities. An authoring tool that
+ conforms to these guidelines will promote accessibility by providing an accessible user interface
+ to authors with disabilities as well as by enabling, supporting, and promoting the production of
+ accessible Web content by all authors.</p></blockquote></dd>
+
+ <dt class="nodev"><cite>User Agent Accessibility Guidelines (UAAG) 2.0</cite> <a href="#refsUAAG">[UAAG]</a></dt>
+
+ <dd class="nodev"><blockquote><p>This document provides guidelines for designing user agents that
+ lower barriers to Web accessibility for people with disabilities. User agents include browsers
+ and other types of software that retrieve and render Web content. A user agent that conforms to
+ these guidelines will promote accessibility through its own user interface and through other
+ internal facilities, including its ability to communicate with other technologies (especially
+ assistive technologies). Furthermore, all users, not just users with disabilities, should find
+ conforming user agents to be more usable.</p></blockquote></dd>
+
+ </dl>
+
+
+
+ <h2 id="infrastructure">Common infrastructure</h2>
+
+ <h3>Terminology</h3>
+
+ <p>This specification refers to both HTML and XML attributes and IDL attributes, often in the same
+ context. When it is not clear which is being referred to, they are referred to as <dfn
+ data-x="">content attributes</dfn> for HTML and XML attributes, and <dfn data-x="">IDL
+ attributes</dfn> for those defined on IDL interfaces. Similarly, the term "properties" is used for
+ both JavaScript object properties and CSS properties. When these are ambiguous they are qualified
+ as <dfn data-x="">object properties</dfn> and <dfn data-x="">CSS properties</dfn> respectively.</p>
+
+ <p>Generally, when the specification states that a feature applies to <span>the HTML syntax</span>
+ or <span>the XHTML syntax</span>, it also includes the other. When a feature specifically only
+ applies to one of the two languages, it is called out by explicitly stating that it does not apply
+ to the other format, as in "for HTML, ... (this does not apply to XHTML)".</p>
+
+ <p>This specification uses the term <dfn data-x="">document</dfn> to refer to any use of HTML,
+ ranging from short static documents to long essays or reports with rich multimedia, as well as to
+ fully-fledged interactive applications. The term is used to refer both to <code>Document</code>
+ objects and their descendant DOM trees, and to serialised byte streams using the <span data-x="the
+ HTML syntax">HTML syntax</span> or <span data-x="the XHTML syntax">XHTML syntax</span>, depending
+ on context.</p>
+
+ <p>In the context of the DOM structures, the terms <span data-x="HTML documents">HTML
+ document</span> and <span data-x="XML documents">XML document</span> are used as defined in the DOM
+ specification, and refer specifically to two different modes that <code>Document</code> objects
+ can find themselves in. <a href="#refsDOM">[DOM]</a> (Such uses are always hyperlinked to their
+ definition.)</p>
+
+ <p>In the context of byte streams, the term HTML document refers to resources labeled as
+ <code>text/html</code>, and the term XML document refers to resources labeled with an <span>XML
+ MIME type</span>.</p>
+
+ <p>The term <dfn>XHTML document</dfn> is used to refer to both <code>Document</code>s in the <span
+ data-x="XML documents">XML document</span> mode that contains element nodes in the <span>HTML
+ namespace</span>, and byte streams labeled with an <span>XML MIME type</span> that contain
+ elements from the <span>HTML namespace</span>, depending on context.</p>
+
+ <hr>
+
+ <p>For simplicity, terms such as <dfn data-x="">shown</dfn>, <dfn data-x="">displayed</dfn>, and
+ <dfn data-x="">visible</dfn> might sometimes be used when referring to the way a document is
+ rendered to the user. These terms are not meant to imply a visual medium; they must be considered
+ to apply to other media in equivalent ways.</p>
+
+ <div class="nodev">
+
+ <p>When an algorithm B says to return to another algorithm A, it implies that A called B. Upon
+ returning to A, the implementation must continue from where it left off in calling B.</p>
+
+ </div>
+
+ <!-- should find somewhere more appropriate to put this -->
+ <p>The term "transparent black" refers to the colour with red, green, blue, and alpha channels all
+ set to zero.</p>
+
+
+ <h4>Resources</h4>
+
+ <p>The specification uses the term <dfn data-x="">supported</dfn> when referring to whether a user
+ agent has an implementation capable of decoding the semantics of an external resource. A format or
+ type is said to be <i>supported</i> if the implementation can process an external resource of that
+ format or type without critical aspects of the resource being ignored. Whether a specific resource
+ is <i>supported</i> can depend on what features of the resource's format are in use.</p>
+
+ <p class="example">For example, a PNG image would be considered to be in a supported format if its
+ pixel data could be decoded and rendered, even if, unbeknownst to the implementation, the image
+ also contained animation data.</p>
+
+ <p class="example">An MPEG-4 video file would not be considered to be in a supported format if the
+ compression format used was not supported, even if the implementation could determine the
+ dimensions of the movie from the file's metadata.</p>
+
+ <p>What some specifications, in particular the HTTP specification, refer to as a
+ <i>representation</i> is referred to in this specification as a <dfn data-x="">resource</dfn>. <a
+ href="#refsHTTP">[HTTP]</a></p>
+
+ <p>The term <dfn>MIME type</dfn> is used to refer to what is sometimes called an <i>Internet media
+ type</i> in protocol literature. The term <i>media type</i> in this specification is used to refer
+ to the type of media intended for presentation, as used by the CSS specifications. <a
+ href="#refsRFC2046">[RFC2046]</a> <a href="#refsMQ">[MQ]</a></p>
+
+ <p>A string is a <dfn>valid MIME type</dfn> if it matches the <code data-x="">media-type</code>
+ rule defined in section 3.7 "Media Types" of RFC 2616. In particular, a <span>valid MIME
+ type</span> may include MIME type parameters. <a href="#refsHTTP">[HTTP]</a></p>
+
+ <p>A string is a <dfn>valid MIME type with no parameters</dfn> if it matches the <code
+ data-x="">media-type</code> rule defined in section 3.7 "Media Types" of RFC 2616, but does not
+ contain any U+003B SEMICOLON characters (;). In other words, if it consists only of a type and
+ subtype, with no MIME Type parameters. <a href="#refsHTTP">[HTTP]</a></p>
+
+ <p>The term <dfn>HTML MIME type</dfn> is used to refer to the <span>MIME type</span>
+ <code>text/html</code>.</p>
+
+ <p>A resource's <dfn>critical subresources</dfn> are those that the resource needs to have
+ available to be correctly processed. Which resources are considered critical or not is defined by
+ the specification that defines the resource's format.</p>
+
+ <p>The term <dfn data-x="data protocol"><code data-x="">data:</code> URL</dfn> refers to <span
+ data-x="URL">URLs</span> that use the <code data-x="">data:</code> scheme. <a
+ href="#refsRFC2397">[RFC2397]</a></p>
+
+
+ <h4>XML</h4>
+
+ <p id="html-namespace">To ease migration from HTML to XHTML, UAs conforming to this specification
+ will place elements in HTML in the <code>http://www.w3.org/1999/xhtml</code> namespace, at least
+ for the purposes of the DOM and CSS. The term "<dfn>HTML elements</dfn>", when used in this
+ specification, refers to any element in that namespace, and thus refers to both HTML and XHTML
+ elements.</p>
+
+ <p>Except where otherwise stated, all elements defined or mentioned in this specification are in
+ the <span>HTML namespace</span> ("<code>http://www.w3.org/1999/xhtml</code>"), and all attributes
+ defined or mentioned in this specification have no namespace.</p>
+
+ <p>The term <dfn>element type</dfn> is used to refer to the set of elements that have a given
+ local name and namespace. For example, <code>button</code> elements are elements with the element
+ type <code>button</code>, meaning they have the local name "<code data-x="">button</code>" and
+ (implicitly as defined above) the <span>HTML namespace</span>.</p>
+
+ <p>Attribute names are said to be <dfn>XML-compatible</dfn> if they match the <a
+ href="http://www.w3.org/TR/xml/#NT-Name"><code data-x="">Name</code></a> production defined in XML
+ and they contain no U+003A COLON characters (:). <a href="#refsXML">[XML]</a></p>
+
+ <p>The term <dfn>XML MIME type</dfn> is used to refer to the <span data-x="MIME type">MIME
+ types</span> <code data-x="">text/xml</code>, <code data-x="">application/xml</code>, and any
+ <span>MIME type</span> whose subtype ends with the four characters "<code data-x="">+xml</code>".
+ <a href="#refsRFC3023">[RFC3023]</a></p>
+
+
+ <h4>DOM trees</h4>
+
+ <p>The <dfn>root element of a <code>Document</code> object</dfn> is that <code>Document</code>'s
+ first element child, if any. If it does not have one then the <code>Document</code> has no root
+ element.</p>
+
+ <p>The term <dfn>root element</dfn>, when not referring to a <code>Document</code> object's root
+ element, means the furthest ancestor element node of whatever node is being discussed, or the node
+ itself if it has no ancestors. When the node is a part of the document, then the node's <span>root
+ element</span> is indeed the document's root element; however, if the node is not currently part
+ of the document tree, the root element will be an orphaned node.</p>
+
+ <p>When an element's <span>root element</span> is the <span>root element of a
+ <code>Document</code> object</span>, it is said to be <dfn>in a <code>Document</code></dfn>. An
+ element is said to have been <dfn data-x="insert an element into a document">inserted into a
+ document</dfn> when its <span>root element</span> changes and is now the document's <span>root
+ element</span>. Analogously, an element is said to have been <dfn data-x="remove an element from a
+ document">removed from a document</dfn> when its <span>root element</span> changes from being the
+ document's <span>root element</span> to being another element.</p>
+
+ <p>A node's <dfn>home subtree</dfn> is the subtree rooted at that node's <span>root
+ element</span>. When a node is <span>in a <code>Document</code></span>, its <span>home
+ subtree</span> is that <code>Document</code>'s tree.</p>
+
+ <p>The <code>Document</code> of a <code>Node</code> (such as an element) is the
+ <code>Document</code> that the <code>Node</code>'s <code
+ data-x="dom-Node-ownerDocument">ownerDocument</code> IDL attribute returns. When a
+ <code>Node</code> is <span>in a <code>Document</code></span> then that <code>Document</code> is
+ always the <code>Node</code>'s <code>Document</code>, and the <code>Node</code>'s <code
+ data-x="dom-Node-ownerDocument">ownerDocument</code> IDL attribute thus always returns that
+ <code>Document</code>.</p>
+
+ <p>The <code>Document</code> of a content attribute is the <code>Document</code> of the
+ attribute's element.</p>
+
+ <p>The term <dfn>tree order</dfn> means a pre-order, depth-first traversal of DOM nodes involved
+ (through the <code data-x="dom-Node-parentNode">parentNode</code>/<code
+ data-x="dom-Node-childNodes">childNodes</code> relationship).</p>
+
+ <p>When it is stated that some element or attribute is <dfn data-x="ignore">ignored</dfn>, or
+ treated as some other value, or handled as if it was something else, this refers only to the
+ processing of the node after it is in the DOM. <span class="nodev">A user agent must not mutate the
+ DOM in such situations.</span></p>
+
+ <p>A content attribute is said to <dfn data-x="">change</dfn> value only if its new value is
+ different than its previous value; setting an attribute to a value it already has does not change
+ it.</p>
+
+ <p>The term <dfn data-x="">empty</dfn>, when used of an attribute value, <code>Text</code> node, or
+ string, means that the length of the text is zero (i.e. not even containing spaces or <span>control
+ characters</span>).</p>
+
+
+ <h4>Scripting</h4>
+
+ <p>The construction "a <code>Foo</code> object", where <code>Foo</code> is actually an interface,
+ is sometimes used instead of the more accurate "an object implementing the interface
+ <code>Foo</code>".</p>
+
+ <p>An IDL attribute is said to be <dfn data-x="">getting</dfn> when its value is being retrieved
+ (e.g. by author script), and is said to be <dfn data-x="">setting</dfn> when a new value is
+ assigned to it.</p>
+
+ <p>If a DOM object is said to be <dfn>live</dfn>, then the attributes and methods on that object
+ <span class="nodev">must</span> operate on the actual underlying data, not a snapshot of the
+ data.</p>
+
+ <p>In the contexts of events, the terms <i>fire</i> and <i>dispatch</i> are used as defined in the
+ DOM specification: <dfn data-x="concept-event-fire">firing</dfn> an event means to create and <span
+ data-x="concept-event-dispatch">dispatch</span> it, and <dfn
+ data-x="concept-event-dispatch">dispatching</dfn> an event means to follow the steps that propagate
+ the event through the tree. The term <dfn data-x="concept-events-trusted">trusted event</dfn> is
+ used to refer to events whose <code data-x="dom-event-isTrusted">isTrusted</code> attribute is
+ initialised to true. <a href="#refsDOM">[DOM]</a></p>
+
+
+ <h4>Plugins</h4>
+
+ <p>The term <dfn>plugin</dfn> refers to a user-agent defined set of content handlers used by the
+ user agent that can take part in the user agent's rendering of a <code>Document</code> object, but
+ that neither act as <span data-x="child browsing context">child browsing contexts</span> of the
+ <code>Document</code> nor introduce any <code>Node</code> objects to the <code>Document</code>'s
+ DOM.</p>
+
+ <p>Typically such content handlers are provided by third parties, though a user agent can also
+ designate built-in content handlers as plugins.</p>
+
+ <div class="nodev">
+
+ <p>A user agent must not consider the types <code>text/plain</code> and
+ <code>application/octet-stream</code> as having a registered <span>plugin</span>.</p> <!-- because
+ of the way <object> elements handles those types, if nothing else (it also doesn't make any sense
+ to have a plugin registered for those types, of course) -->
+
+ </div>
+
+ <p class="example">One example of a plugin would be a PDF viewer that is instantiated in a
+ <span>browsing context</span> when the user navigates to a PDF file. This would count as a plugin
+ regardless of whether the party that implemented the PDF viewer component was the same as that
+ which implemented the user agent itself. However, a PDF viewer application that launches separate
+ from the user agent (as opposed to using the same interface) is not a plugin by this
+ definition.</p>
+
+ <p class="note">This specification does not define a mechanism for interacting with plugins, as it
+ is expected to be user-agent- and platform-specific. Some UAs might opt to support a plugin
+ mechanism such as the Netscape Plugin API; others might use remote content converters or have
+ built-in support for certain types. Indeed, this specification doesn't require user agents to
+ support plugins at all. <a href="#refsNPAPI">[NPAPI]</a></p>
+
+ <p>A plugin can be <dfn data-x="concept-plugin-secure">secured</dfn> if it honors the semantics of
+ the <code data-x="attr-iframe-sandbox">sandbox</code> attribute.</p>
+
+ <p class="example">For example, a secured plugin would prevent its contents from creating pop-up
+ windows when the plugin is instantiated inside a sandboxed <code>iframe</code>.</p>
+
+ <div class="nodev">
+
+ <p class="warning">Browsers should take extreme care when interacting with external content
+ intended for <span data-x="plugin">plugins</span>. When third-party software is run with the same
+ privileges as the user agent itself, vulnerabilities in the third-party software become as
+ dangerous as those in the user agent.</p>
+
+ <p>Since different users having differents sets of <span data-x="plugin">plugins</span> provides a
+ fingerprinting vector that increases the chances of users being uniquely identified, user agents
+ are encouraged to support the exact same set of <span data-x="plugin">plugins</span> for each
+ user.
+ <!--INSERT FINGERPRINT-->
+ </p>
+
+ </div>
+
+
+
+ <h4 id="encoding-terminology">Character encodings</h4>
+
+ <p>A <dfn data-x="encoding">character encoding</dfn>, or just <i>encoding</i> where that is not
+ ambiguous, is a defined way to convert between byte streams and Unicode strings, as defined in the
+ WHATWG Encoding standard. An <span>encoding</span> has an <dfn>encoding name</dfn> and one or more
+ <dfn data-x="encoding label">encoding labels</dfn>, referred to as the encoding's <i>name</i> and
+ <i>labels</i> in the Encoding standard. <a href="#refsENCODING">[ENCODING]</a></p>
+
+ <p>An <dfn>ASCII-compatible character encoding</dfn> is a single-byte or variable-length
+ <span>encoding</span> in which the bytes 0x09, 0x0A, 0x0C, 0x0D, 0x20 - 0x22, 0x26, 0x27, 0x2C -
+ 0x3F, 0x41 - 0x5A, and 0x61 - 0x7A<!-- is that list ok? do any character sets we want to support
+ do things outside that range? -->, ignoring bytes that are the second and later bytes of multibyte
+ sequences, all correspond to single-byte sequences that map to the same Unicode characters as
+ those bytes in Windows-1252<!--ANSI_X3.4-1968 (US-ASCII)-->. <a href="#refsENCODING">[ENCODING]</a></p>
+
+ <p class="note">This includes such encodings as Shift_JIS, HZ-GB-2312, and variants of ISO-2022,
+ even though it is possible in these encodings for bytes like 0x70 to be part of longer sequences
+ that are unrelated to their interpretation as ASCII. It excludes UTF-16 variants, as well as
+ obsolete legacy encodings such as UTF-7, GSM03.38, and EBCDIC variants.</p>
+
+ <!--
+ We'll have to change that if anyone comes up with a way to have a document that is valid as two
+ different encodings at once, with different <meta charset> elements applying in each case.
+ -->
+
+ <p>The term <dfn>a UTF-16 encoding</dfn> refers to any variant of UTF-16: UTF-16LE or UTF-16BE,
+ regardless of the presence or absence of a BOM. <a href="#refsENCODING">[ENCODING]</a></p>
+
+ <p>The term <dfn>code unit</dfn> is used as defined in the Web IDL specification: a 16 bit
+ unsigned integer, the smallest atomic component of a <code>DOMString</code>. (This is a narrower
+ definition than the one used in Unicode, and is not the same as a <i>code point</i>.) <a
+ href="#refsWEBIDL">[WEBIDL]</a></p>
+
+ <p>The term <dfn>Unicode code point</dfn> means a <i data-x="">Unicode scalar value</i> where
+ possible, and an isolated surrogate code point when not. When a conformance requirement is defined
+ in terms of characters or Unicode code points, a pair of <span data-x="code unit">code units</span>
+ consisting of a high surrogate followed by a low surrogate must be treated as the single code
+ point represented by the surrogate pair, but isolated surrogates must each be treated as the
+ single code point with the value of the surrogate. <a href="#refsUNICODE">[UNICODE]</a></p>
+
+ <p>In this specification, the term <dfn>character</dfn>, when not qualified as <em>Unicode</em>
+ character, is synonymous with the term <span>Unicode code point</span>.</p>
+
+ <p>The term <dfn>Unicode character</dfn> is used to mean a <i data-x="">Unicode scalar value</i>
+ (i.e. any Unicode code point that is not a surrogate code point). <a
+ href="#refsUNICODE">[UNICODE]</a></p>
+
+ <p>The <dfn>code-unit length</dfn> of a string is the number of <span data-x="code unit">code
+ units</span> in that string.</p>
+
+ <p class="note">This complexity results from the historical decision to define the DOM API in
+ terms of 16 bit (UTF-16) <span data-x="code unit">code units</span>, rather than in terms of <span
+ data-x="Unicode character">Unicode characters</span>.</p>
+
+
+
+ <div class="nodev">
+
+ <h3>Conformance requirements</h3>
+
+ <p>All diagrams, examples, and notes in this specification are non-normative, as are all sections
+ explicitly marked non-normative. Everything else in this specification is normative.</p>
+
+ <p>The key words "MUST", "MUST NOT", <!--"REQUIRED",--> <!--"SHALL", "SHALL NOT",--> "SHOULD", "SHOULD
+ NOT", <!--"RECOMMENDED", "NOT RECOMMENDED",--> "MAY", and "OPTIONAL" in the normative parts of
+ this document are to be interpreted as described in RFC2119. The key word "OPTIONALLY" in the
+ normative parts of this document is to be interpreted with the same normative meaning as "MAY" and
+ "OPTIONAL". For readability, these words do not appear in all uppercase letters in this
+ specification. <a href="#refsRFC2119">[RFC2119]</a></p>
+
+ <p>Requirements phrased in the imperative as part of algorithms (such as "strip any leading space
+ characters" or "return false and abort these steps") are to be interpreted with the meaning of the
+ key word ("must", "should", "may", etc) used in introducing the algorithm.</p>
+
+ <div class="example">
+
+ <p>For example, were the spec to say:</p>
+
+ <pre>To eat an orange, the user must:
+1. Peel the orange.
+2. Separate each slice of the orange.
+3. Eat the orange slices.</pre>
+
+ <p>...it would be equivalent to the following:</p>
+
+ <pre>To eat an orange:
+1. The user must peel the orange.
+2. The user must separate each slice of the orange.
+3. The user must eat the orange slices.</pre>
+
+ <p>Here the key word is "must".</p>
+
+ <p>The former (imperative) style is generally preferred in this specification for stylistic
+ reasons.</p>
+
+ </div>
+
+ <p>Conformance requirements phrased as algorithms or specific steps may be implemented in any
+ manner, so long as the end result is equivalent. (In particular, the algorithms defined in this
+ specification are intended to be easy to follow, and not intended to be performant.)</p>
+
+ </div>
+
+
+
+ <div class="nodev">
+
+ <h4>Conformance classes</h4>
+
+ <p>This specification describes the conformance criteria for <span class="nodev">user agents
+ (relevant to implementors) and</span> documents<span class="nodev"> (relevant to authors and
+ authoring tool implementors)</span>.</p>
+
+ <p><dfn>Conforming documents</dfn> are those that comply with all the conformance criteria for
+ documents. For readability, some of these conformance requirements are phrased as conformance
+ requirements on authors; such requirements are implicitly requirements on documents: by
+ definition, all documents are assumed to have had an author. (In some cases, that author may
+ itself be a user agent &mdash; such user agents are subject to additional rules, as explained
+ below.)</p>
+
+ <p class="example">For example, if a requirement states that "authors must not use the <code
+ data-x="">foobar</code> element", it would imply that documents are not allowed to contain elements
+ named <code data-x="">foobar</code>.</p>
+
+ <p class="note impl">There is no implied relationship between document conformance requirements
+ and implementation conformance requirements. User agents are not free to handle non-conformant
+ documents as they please; the processing model described in this specification applies to
+ implementations regardless of the conformity of the input documents.</p>
+
+ <p>User agents fall into several (overlapping) categories with different conformance
+ requirements.</p>
+
+ <dl>
+
+ <dt id="interactive">Web browsers and other interactive user agents</dt>
+
+ <dd>
+
+ <p>Web browsers that support <span>the XHTML syntax</span> must process elements and attributes
+ from the <span>HTML namespace</span> found in XML documents as described in this specification,
+ so that users can interact with them, unless the semantics of those elements have been
+ overridden by other specifications.</p>
+
+ <p class="example">A conforming XHTML processor would, upon finding an XHTML <code>script</code>
+ element in an XML document, execute the script contained in that element. However, if the
+ element is found within a transformation expressed in XSLT (assuming the user agent also
+ supports XSLT), then the processor would instead treat the <code>script</code> element as an
+ opaque element that forms part of the transform.</p>
+
+ <p>Web browsers that support <span>the HTML syntax</span> must process documents labeled with an
+ <span>HTML MIME type</span> as described in this specification, so that users can interact with
+ them.</p>
+
+ <p>User agents that support scripting must also be conforming implementations of the IDL
+ fragments in this specification, as described in the Web IDL specification. <a
+ href="#refsWEBIDL">[WEBIDL]</a></p>
+
+ <p class="note">Unless explicitly stated, specifications that override the semantics of HTML
+ elements do not override the requirements on DOM objects representing those elements. For
+ example, the <code>script</code> element in the example above would still implement the
+ <code>HTMLScriptElement</code> interface.</p>
+
+ </dd>
+
+ <dt id="non-interactive">Non-interactive presentation user agents</dt>
+
+ <dd>
+
+ <p>User agents that process HTML and XHTML documents purely to render non-interactive versions
+ of them must comply to the same conformance criteria as Web browsers, except that they are
+ exempt from requirements regarding user interaction.</p>
+
+ <p class="note">Typical examples of non-interactive presentation user agents are printers
+ (static UAs) and overhead displays (dynamic UAs). It is expected that most static
+ non-interactive presentation user agents will also opt to <a href="#non-scripted">lack scripting
+ support</a>.</p>
+
+ <p class="example">A non-interactive but dynamic presentation UA would still execute scripts,
+ allowing forms to be dynamically submitted, and so forth. However, since the concept of "focus"
+ is irrelevant when the user cannot interact with the document, the UA would not need to support
+ any of the focus-related DOM APIs.</p>
+
+ </dd>
+
+ <dt id="renderingUA">Visual user agents that support the suggested default rendering</dt>
+
+ <dd>
+
+ <p>User agents, whether interactive or not, may be designated (possibly as a user option) as
+ supporting the suggested default rendering defined by this specification.</p>
+
+ <p>This is not required. In particular, even user agents that do implement the suggested default
+ rendering are encouraged to offer settings that override this default to improve the experience
+ for the user, e.g. changing the colour contrast, using different focus styles, or otherwise
+ making the experience more accessible and usable to the user.</p>
+
+ <p>User agents that are designated as supporting the suggested default rendering must, while so
+ designated, implement the rules in <a href="#rendering">the rendering section</a> that that
+ section defines as the behavior that user agents are <em>expected</em> to implement.</p>
+
+ </dd>
+
+ <dt id="non-scripted">User agents with no scripting support</dt>
+
+ <dd>
+
+ <p>Implementations that do not support scripting (or which have their scripting features
+ disabled entirely) are exempt from supporting the events and DOM interfaces mentioned in this
+ specification. For the parts of this specification that are defined in terms of an events model
+ or in terms of the DOM, such user agents must still act as if events and the DOM were
+ supported.</p>
+
+ <p class="note">Scripting can form an integral part of an application. Web browsers that do not
+ support scripting, or that have scripting disabled, might be unable to fully convey the author's
+ intent.</p>
+
+ </dd>
+
+
+ <dt>Conformance checkers</dt>
+
+ <dd id="conformance-checkers">
+
+ <p>Conformance checkers must verify that a document conforms to the applicable conformance
+ criteria described in this specification. Automated conformance checkers are exempt from
+ detecting errors that require interpretation of the author's intent (for example, while a
+ document is non-conforming if the content of a <code>blockquote</code> element is not a quote,
+ conformance checkers running without the input of human judgement do not have to check that
+ <code>blockquote</code> elements only contain quoted material).</p>
+
+ <p>Conformance checkers must check that the input document conforms when parsed without a
+ <span>browsing context</span> (meaning that no scripts are run, and that the parser's
+ <span>scripting flag</span> is disabled), and should also check that the input document conforms
+ when parsed with a <span>browsing context</span> in which scripts execute, and that the scripts
+ never cause non-conforming states to occur other than transiently during script execution
+ itself. (This is only a "SHOULD" and not a "MUST" requirement because it has been proven to be
+ impossible. <a href="#refsCOMPUTABLE">[COMPUTABLE]</a>)</p>
+
+ <p>The term "HTML validator" can be used to refer to a conformance checker that itself conforms
+ to the applicable requirements of this specification.</p>
+
+ <div class="note">
+
+ <p>XML DTDs cannot express all the conformance requirements of this specification. Therefore, a
+ validating XML processor and a DTD cannot constitute a conformance checker. Also, since neither
+ of the two authoring formats defined in this specification are applications of SGML, a
+ validating SGML system cannot constitute a conformance checker either.</p>
+
+ <p>To put it another way, there are three types of conformance criteria:</p>
+
+ <ol>
+
+ <li>Criteria that can be expressed in a DTD.</li>
+
+ <li>Criteria that cannot be expressed by a DTD, but can still be checked by a machine.</li>
+
+ <li>Criteria that can only be checked by a human.</li>
+
+ </ol>
+
+ <p>A conformance checker must check for the first two. A simple DTD-based validator only checks
+ for the first class of errors and is therefore not a conforming conformance checker according
+ to this specification.</p>
+
+ </div>
+ </dd>
+
+
+ <dt>Data mining tools</dt>
+
+ <dd id="data-mining">
+
+ <p>Applications and tools that process HTML and XHTML documents for reasons other than to either
+ render the documents or check them for conformance should act in accordance with the semantics
+ of the documents that they process.</p>
+
+ <p class="example">A tool that generates <span data-x="outline">document outlines</span> but
+ increases the nesting level for each paragraph and does not increase the nesting level for each
+ section would not be conforming.</p>
+
+ </dd>
+
+
+ <dt id="editors">Authoring tools and markup generators</dt>
+
+ <dd>
+
+ <p>Authoring tools and markup generators must generate <span>conforming documents</span>.
+ Conformance criteria that apply to authors also apply to authoring tools, where appropriate.</p>
+
+ <p>Authoring tools are exempt from the strict requirements of using elements only for their
+ specified purpose, but only to the extent that authoring tools are not yet able to determine
+ author intent. However, authoring tools must not automatically misuse elements or encourage
+ their users to do so.</p>
+
+ <p class="example">For example, it is not conforming to use an <code>address</code> element for
+ arbitrary contact information; that element can only be used for marking up contact information
+ for the author of the document or section. However, since an authoring tool is likely unable to
+ determine the difference, an authoring tool is exempt from that requirement. This does not mean,
+ though, that authoring tools can use <code>address</code> elements for any block of italics text
+ (for instance); it just means that the authoring tool doesn't have to verify that when the user
+ uses a tool for inserting contact information for a section, that the user really is doing that
+ and not inserting something else instead.</p>
+
+ <p class="note">In terms of conformance checking, an editor has to output documents that conform
+ to the same extent that a conformance checker will verify.</p>
+
+ <p>When an authoring tool is used to edit a non-conforming document, it may preserve the
+ conformance errors in sections of the document that were not edited during the editing session
+ (i.e. an editing tool is allowed to round-trip erroneous content). However, an authoring tool
+ must not claim that the output is conformant if errors have been so preserved.</p>
+
+ <p>Authoring tools are expected to come in two broad varieties: tools that work from structure
+ or semantic data, and tools that work on a What-You-See-Is-What-You-Get media-specific editing
+ basis (WYSIWYG).</p>
+
+ <p>The former is the preferred mechanism for tools that author HTML, since the structure in the
+ source information can be used to make informed choices regarding which HTML elements and
+ attributes are most appropriate.</p>
+
+ <p>However, WYSIWYG tools are legitimate. WYSIWYG tools should use elements they know are
+ appropriate, and should not use elements that they do not know to be appropriate. This might in
+ certain extreme cases mean limiting the use of flow elements to just a few elements, like
+ <code>div</code>, <code>b</code>, <code>i</code>, and <code>span</code> and making liberal use
+ of the <code data-x="attr-style">style</code> attribute.</p>
+
+ <p>All authoring tools, whether WYSIWYG or not, should make a best effort attempt at enabling
+ users to create well-structured, semantically rich, media-independent content.</p>
+
+ </dd>
+
+ </dl>
+
+ <p id="hardwareLimitations">User agents may impose implementation-specific limits on otherwise
+ unconstrained inputs, e.g. to prevent denial of service attacks, to guard against running out of
+ memory, or to work around platform-specific limitations.
+ <!--INSERT FINGERPRINT-->
+ </p>
+
+ <p>For compatibility with existing content and prior specifications, this specification describes
+ two authoring formats: one based on XML (referred to as <span>the XHTML syntax</span>), and one
+ using a <a href="#writing">custom format</a> inspired by SGML (referred to as <span>the HTML
+ syntax</span>). Implementations must support at least one of these two formats, although
+ supporting both is encouraged.</p>
+
+ <p>Some conformance requirements are phrased as requirements on elements, attributes, methods or
+ objects. Such requirements fall into two categories: those describing content model restrictions,
+ and those describing implementation behavior. Those in the former category are requirements on
+ documents and authoring tools. Those in the second category are requirements on user agents.
+ Similarly, some conformance requirements are phrased as requirements on authors; such requirements
+ are to be interpreted as conformance requirements on the documents that authors produce. (In other
+ words, this specification does not distinguish between conformance criteria on authors and
+ conformance criteria on documents.)</p>
+
+ </div>
+
+
+ <div class="nodev">
+
+ <h4>Dependencies</h4>
+
+ <p>This specification relies on several other underlying specifications.</p>
+
+ <dl>
+
+ <dt>Unicode and Encoding</dt>
+
+ <dd>
+
+ <p>The Unicode character set is used to represent textual data, and the WHATWG Encoding standard
+ defines requirements around <span data-x="encoding">character encodings</span>. <a
+ href="#refsUNICODE">[UNICODE]</a></p>
+
+ <p class="note">This specification <a href="#encoding-terminology">introduces terminology</a>
+ based on the terms defined in those specifications, as described earlier.</p>
+
+ <p>The following terms are used as defined in the WHATWG Encoding standard: <a
+ href="#refsENCODING">[ENCODING]</a></p>
+
+ <ul class="brief">
+
+ <li><dfn>Getting an encoding</dfn>
+
+ <li>The <dfn>encoder</dfn> and <dfn>decoder</dfn> algorithms for various encodings, including
+ the <dfn>UTF-8 encoder</dfn> and <dfn>UTF-8 decoder</dfn>
+
+ <li>The generic <dfn>decode</dfn> algorithm which takes a byte stream and an encoding and
+ returns a character stream
+
+ <li>The <dfn>UTF-8 decode</dfn> algorithm which takes a byte stream and returns a character
+ stream, additionally stripping one leading UTF-8 Byte Order Mark (BOM), if any
+
+ </ul>
+
+ <p class="note">The <span>UTF-8 decoder</span> is distinct from the <i>UTF-8 decode
+ algorithm</i>. The latter first strips a Byte Order Mark (BOM), if any, and then invokes the
+ former.</p>
+
+ <p>For readability, character encodings are sometimes referenced in this specification with a
+ case that differs from the canonical case given in the WHATWG Encoding standard. (For example,
+ "UTF-16LE" instead of "utf-16le".)</p>
+
+ </dd>
+
+
+ <dt>XML</dt>
+
+ <dd>
+
+ <p>Implementations that support <span>the XHTML syntax</span> must support some version of XML,
+ as well as its corresponding namespaces specification, because that syntax uses an XML
+ serialisation with namespaces. <a href="#refsXML">[XML]</a> <a href="#refsXMLNS">[XMLNS]</a></p>
+
+ </dd>
+
+
+ <dt>URLs</dt>
+
+ <dd>
+
+ <p>The following terms are defined in the WHATWG URL standard: <a href="#refsURL">[URL]</a></p>
+
+ <ul class="brief">
+ <li><dfn>URL</dfn>
+ <li><dfn>Absolute URL</dfn>
+ <li><dfn>Relative URL</dfn>
+ <li><dfn data-x="concept-url-scheme-relative">Relative schemes</dfn>
+ <li>The <dfn>URL parser</dfn>
+ <li><dfn>Parsed URL</dfn>
+ <li>The <dfn data-x="concept-url-scheme">scheme</dfn> component of a <span>parsed URL</span>
+ <li>The <dfn data-x="concept-url-scheme-data">scheme data</dfn> component of a <span>parsed URL</span>
+ <li>The <dfn data-x="concept-url-username">username</dfn> component of a <span>parsed URL</span>
+ <li>The <dfn data-x="concept-url-password">password</dfn> component of a <span>parsed URL</span>
+ <li>The <dfn data-x="concept-url-host">host</dfn> component of a <span>parsed URL</span>
+ <li>The <dfn data-x="concept-url-port">port</dfn> component of a <span>parsed URL</span>
+ <li>The <dfn data-x="concept-url-path">path</dfn> component of a <span>parsed URL</span>
+ <li>The <dfn data-x="concept-url-query">query</dfn> component of a <span>parsed URL</span>
+ <li>The <dfn data-x="concept-url-fragment">fragment</dfn> component of a <span>parsed URL</span>
+ <li><dfn data-x="concept-url-parse-error">Parse errors</dfn> from the <span>URL parser</span>
+ <li>The <dfn data-x="concept-url-serializer">URL serializer</dfn>
+ <li><dfn>Default encode set</dfn>
+ <li><dfn>Percent encode</dfn>
+ <li><dfn>UTF-8 percent encode</dfn>
+ <li><dfn>Percent decode</dfn>
+ <li><dfn>Decoder error</dfn>
+ <li>The <dfn>domain label to ASCII</dfn> algorithm</li>
+ <li>The <dfn>domain label to Unicode</dfn> algorithm</li>
+ <li><dfn><code>URLUtils</code></dfn> interface
+ <li><dfn><code>URLUtilsReadOnly</code></dfn> interface
+ <li><dfn data-x="dom-url-href"><code>href</code> attribute</dfn>
+ <li><dfn data-x="dom-url-protocol"><code>protocol</code> attribute</dfn>
+ <li>The <dfn data-x="concept-uu-get-the-base">get the base</dfn> hook for <code>URLUtils</code>
+ <li>The <dfn data-x="concept-uu-update">update steps</dfn> hook for <code>URLUtils</code>
+ <li>The <dfn data-x="concept-uu-set-the-input">set the input</dfn> algorithm for <code>URLUtils</code>
+ <li>The <dfn data-x="concept-uu-query-encoding">query encoding</dfn> of an <code>URLUtils</code> object
+ <li>The <dfn data-x="concept-uu-input">input</dfn> of an <code>URLUtils</code> object
+ <li>The <dfn data-x="concept-uu-url">url</dfn> of an <code>URLUtils</code> object
+ </ul>
+
+ </dd>
+
+
+ <dt>Cookies</dt>
+
+ <dd>
+
+ <p>The following terms are defined in the Cookie specification: <a
+ href="#refsCOOKIES">[COOKIES]</a></p>
+
+ <ul class="brief">
+ <li><dfn>cookie-string</dfn>
+ <li><dfn>receives a set-cookie-string</dfn>
+ </ul>
+
+ </dd>
+
+
+ <dt>Fetch</dt>
+
+ <dd>
+
+ <p>The following terms are defined in the WHATWG Fetch specification: <a href="#refsFETCH">[FETCH]</a></p>
+
+ <ul class="brief">
+ <li><dfn>cross-origin request</dfn>
+ <li><dfn>cross-origin request status</dfn>
+ <li><dfn>custom request headers</dfn>
+ <li><dfn>simple cross-origin request</dfn>
+ <li><dfn>redirect steps</dfn>
+ <li><dfn>omit credentials flag</dfn>
+ <li><dfn>resource sharing check</dfn>
+ </ul>
+
+ <p class="note">This specification does not yet use the "fetch" algorithm from the WHATWG Fetch
+ specification. It will be updated to do so in due course.</p>
+
+ </dd>
+
+
+<!--TOPIC:DOM APIs-->
+
+ <dt>Web IDL</dt>
+
+ <dd>
+
+ <p>The IDL fragments in this specification must be interpreted as required for conforming IDL
+ fragments, as described in the Web IDL specification. <a href="#refsWEBIDL">[WEBIDL]</a></p>
+
+ <p>The terms <dfn>supported property indices</dfn>, <dfn>determine the value of an indexed
+ property</dfn>, <dfn>support named properties</dfn>, <dfn>supported property names</dfn>,
+ <dfn>unenumerable</dfn>, <dfn>determine the value of a named property</dfn>, <dfn>platform array
+ objects</dfn>, and <dfn data-x="dfn-read-only-array">read only</dfn> (when applied to arrays)
+ are used as defined in the Web IDL specification. The algorithm to <dfn>convert a DOMString to a
+ sequence of Unicode characters</dfn> is similarly that defined in the Web IDL specification.</p>
+
+ <p>When this specification requires a user agent to <dfn>create a <code>Date</code> object</dfn>
+ representing a particular time (which could be the special value Not-a-Number), the milliseconds
+ component of that time, if any, must be truncated to an integer, and the time value of the newly
+ created <code>Date</code> object must represent the resulting truncated time.</p>
+
+ <p class="example">For instance, given the time 23045 millionths of a second after 01:00 UTC on
+ January 1st 2000, i.e. the time 2000-01-01T00:00:00.023045Z, then the <code>Date</code> object
+ created representing that time would represent the same time as that created representing the
+ time 2000-01-01T00:00:00.023Z, 45 millionths earlier. If the given time is NaN, then the result
+ is a <code>Date</code> object that represents a time value NaN (indicating that the object does
+ not represent a specific instant of time).</p>
+
+ </dd>
+
+
+ <dt>JavaScript</dt>
+
+ <dd>
+
+ <p>Some parts of the language described by this specification only support JavaScript as the
+ underlying scripting language. <a href="#refsECMA262">[ECMA262]</a></p>
+
+ <p class="note">The term "JavaScript" is used to refer to ECMA262, rather than the official term
+ ECMAScript, since the term JavaScript is more widely known. Similarly, the <span>MIME
+ type</span> used to refer to JavaScript in this specification is <code
+ data-x="">text/javascript</code>, since that is the most commonly used type, <span data-x="willful
+ violation">despite it being an officially obsoleted type</span> according to RFC 4329. <a
+ href="#refsRFC4329">[RFC4329]</a></p>
+
+ <p>The term <dfn>JavaScript global environment</dfn> refers to the <i data-x="">global
+ environment</i> concept defined in the ECMAScript specification.</p>
+
+ <p>The ECMAScript <dfn data-x="js-SyntaxError"><code>SyntaxError</code></dfn> exception is also
+ defined in the ECMAScript specification. <a href="#refsECMA262">[ECMA262]</a></p>
+
+ <p>The <dfn>ArrayBuffer</dfn> and related object types and underlying concepts from the
+ ECMAScript Specification are used for several features in this specification. <a
+ href="#refsECMA262">[ECMA262]</a></p>
+
+ <p>The following helper IDL is used for referring to <code>ArrayBuffer</code>-related types:</p>
+
+ <pre class="idl">typedef (<dfn>Int8Array</dfn> or <dfn>Uint8Array</dfn> or <dfn>Uint8ClampedArray</dfn> or
+ <dfn>Int16Array</dfn> or <dfn>Uint16Array</dfn> or
+ <dfn>Int32Array</dfn> or <dfn>Uint32Array</dfn> or
+ <dfn>Float32Array</dfn> or <dfn>Float64Array</dfn> or
+ <dfn>DataView</dfn>) <dfn>ArrayBufferView</dfn>;</pre>
+
+ <p class="note">In particular, the <code>Uint8ClampedArray</code> type is used by some <span
+ data-x="ImageData">2D canvas APIs</span>, and the <a href="#network"><code>WebSocket</code>
+ API</a> uses <code>ArrayBuffer</code> objects for handling binary frames.</p>
+
+ </dd>
+
+
+ <dt>DOM</dt>
+
+ <dd>
+
+ <p>The Document Object Model (DOM) is a representation &mdash; a model &mdash; of a document and
+ its content. The DOM is not just an API; the conformance criteria of HTML implementations are
+ defined, in this specification, in terms of operations on the DOM. <a
+ href="#refsDOM">[DOM]</a></p>
+
+ <p>Implementations must support DOM and the events defined in DOM Events, because this
+ specification is defined in terms of the DOM, and some of the features are defined as extensions
+ to the DOM interfaces. <a href="#refsDOM">[DOM]</a> <a href="#refsDOMEVENTS">[DOMEVENTS]</a></p>
+
+ <p>In particular, the following features are defined in the DOM specification: <a
+ href="#refsDOM">[DOM]</a></p> <!-- aka DOM Core or DOMCORE -->
+
+ <ul class="brief">
+
+ <li><dfn><code>Attr</code></dfn> interface</li>
+ <li><dfn><code>Comment</code></dfn> interface</li>
+ <li><dfn><code>DOMImplementation</code></dfn> interface</li>
+ <li><dfn data-x="DOM Document"><code>Document</code></dfn> interface</li>
+ <li><dfn><code>XMLDocument</code></dfn> interface</li>
+ <li><dfn><code>DocumentFragment</code></dfn> interface</li>
+ <li><dfn><code>DocumentType</code></dfn> interface</li>
+ <li><dfn><code>DOMException</code></dfn> interface</li>
+ <li><dfn><code>ChildNode</code></dfn> interface</li>
+ <li><dfn><code>Element</code></dfn> interface</li>
+ <li><dfn><code>Node</code></dfn> interface</li>
+ <li><dfn><code>NodeList</code></dfn> interface</li>
+ <li><dfn><code>ProcessingInstruction</code></dfn> interface</li>
+ <li><dfn><code>Text</code></dfn> interface</li>
+
+ <li><dfn><code>HTMLCollection</code></dfn> interface</li>
+ <li><dfn data-x="dom-HTMLCollection-item"><code>item()</code></dfn> method</li>
+ <li>The terms <dfn>collections</dfn> and <dfn>represented by the collection</dfn></li>
+
+ <li><dfn><code>DOMTokenList</code></dfn> interface</li>
+ <li><dfn><code>DOMSettableTokenList</code></dfn> interface</li>
+
+ <li><dfn data-x="dom-DOMImplementation-createDocument"><code>createDocument()</code></dfn> method</li>
+ <li><dfn data-x="dom-DOMImplementation-createHTMLDocument"><code>createHTMLDocument()</code></dfn> method</li>
+ <li><dfn data-x="dom-Document-createElement"><code>createElement()</code></dfn> method</li>
+ <li><dfn data-x="dom-Document-createElementNS"><code>createElementNS()</code></dfn> method</li>
+ <li><dfn data-x="dom-Document-getElementById"><code>getElementById()</code></dfn> method</li>
+ <li><dfn data-x="dom-Node-insertBefore"><code>insertBefore()</code></dfn> method</li>
+
+ <li><dfn data-x="dom-Node-ownerDocument"><code>ownerDocument</code></dfn> attribute</li>
+ <li><dfn data-x="dom-Node-childNodes"><code>childNodes</code></dfn> attribute</li>
+ <li><dfn data-x="dom-Node-localName"><code>localName</code></dfn> attribute</li>
+ <li><dfn data-x="dom-Node-parentNode"><code>parentNode</code></dfn> attribute</li>
+ <li><dfn data-x="dom-Node-namespaceURI"><code>namespaceURI</code></dfn> attribute</li>
+ <li><dfn data-x="dom-Element-tagName"><code>tagName</code></dfn> attribute</li>
+ <li><dfn data-x="dom-Element-id"><code>id</code></dfn> attribute</li>
+ <li><dfn><code>textContent</code></dfn> attribute</li>
+
+ <li>The <dfn data-x="concept-node-insert">insert</dfn>, <dfn data-x="concept-node-append">append</dfn>, <dfn data-x="concept-node-remove">remove</dfn>, <dfn data-x="concept-node-replace">replace</dfn>, and <dfn data-x="concept-node-adopt">adopt</dfn> algorithms for nodes</li>
+ <li>The <dfn>nodes are inserted</dfn> and <dfn>nodes are removed</dfn> concepts</li>
+ <li>An element's <dfn data-x="concept-node-adopt-ext">adopting steps</dfn></li>
+ <li>The <dfn>attribute list</dfn> concept.</li>
+ <li>The <dfn data-x="concept-cd-data">data</dfn> of a text node.</li>
+
+ <li><dfn><code>Event</code></dfn> interface</li>
+ <li><dfn><code>EventTarget</code></dfn> interface</li>
+ <li><dfn><code>EventInit</code></dfn> dictionary type</li>
+ <li><dfn data-x="dom-Event-target"><code>target</code></dfn> attribute</li>
+ <li><dfn data-x="dom-Event-isTrusted"><code>isTrusted</code></dfn> attribute</li>
+ <li>The <dfn data-x="concept-event-type">type</dfn> of an event</li>
+ <li>The concept of an <dfn data-x=concept-event-listener>event listener</dfn> and the <span data-x=concept-event-listener>event listeners</span> associated with an <code>EventTarget</code></li>
+ <li>The concept of a <dfn>target override</dfn></li>
+ <li>The concept of a regular <dfn>event parent</dfn> and a <dfn>cross-boundary event parent</dfn></li> <!-- see bug 18780 -->
+
+ <li>The <dfn data-x="document's character encoding">encoding</dfn> (herein the <i>character encoding</i>) and <dfn data-x="concept-document-content-type">content type</dfn> of a <code>Document</code></li>
+ <li>The distinction between <dfn>XML documents</dfn> and <dfn>HTML documents</dfn></li>
+ <li>The terms <dfn>quirks mode</dfn>, <dfn>limited-quirks mode</dfn>, and <dfn>no-quirks mode</dfn></li>
+ <li>The algorithm to <dfn data-x="concept-node-clone">clone</dfn> a <code>Node</code>, and the concept of <dfn data-x="concept-node-clone-ext">cloning steps</dfn> used by that algorithm</li>
+ <li>The concept of <dfn>base URL change steps</dfn> and the definition of what happens when an element is <dfn>affected by a base URL change</dfn></li>
+ <li>The concept of an element's <dfn data-x="concept-id">unique identifier (ID)</dfn></li>
+
+ <li>The concept of a DOM <dfn data-x="concept-range">range</dfn>, and the terms <dfn data-x="concept-range-start">start</dfn>, <dfn data-x="concept-range-end">end</dfn>, and <dfn data-x="concept-range-bp">boundary point</dfn> as applied to ranges.</li>
+
+ <li><dfn><code>MutationObserver</code></dfn> interface</li>
+ <li>The <dfn data-x="concept-mo-invoke">invoke <code>MutationObserver</code> objects</dfn> algorithm</li>
+
+ <li><dfn>Promise</dfn> interface</li>
+ <li>The <dfn data-x="concept-resolver">resolver</dfn> concept</li>
+ <li>The <dfn data-x="concept-resolver-fulfill">fulfill</dfn> and <dfn data-x="concept-resolver-reject">reject</dfn> algorithms</li>
+
+ </ul>
+
+ <p>The term <dfn>throw</dfn> in this specification is used as defined in the DOM specification.
+ The following <code>DOMException</code> types are defined in the DOM specification: <a
+ href="#refsDOM">[DOM]</a></p>
+
+ <ol class="brief">
+ <li value="1"><dfn><code>IndexSizeError</code></dfn></li>
+ <li value="3"><dfn><code>HierarchyRequestError</code></dfn></li>
+ <li value="4"><dfn><code>WrongDocumentError</code></dfn></li>
+ <li value="5"><dfn><code>InvalidCharacterError</code></dfn></li>
+ <li value="7"><dfn><code>NoModificationAllowedError</code></dfn></li>
+ <li value="8"><dfn><code>NotFoundError</code></dfn></li>
+ <li value="9"><dfn><code>NotSupportedError</code></dfn></li>
+ <li value="11"><dfn><code>InvalidStateError</code></dfn></li>
+ <li value="12"><dfn><code>SyntaxError</code></dfn></li>
+ <li value="13"><dfn><code>InvalidModificationError</code></dfn></li>
+ <li value="14"><dfn><code>NamespaceError</code></dfn></li>
+ <li value="15"><dfn><code>InvalidAccessError</code></dfn></li>
+ <li value="18"><dfn><code>SecurityError</code></dfn></li>
+ <li value="19"><dfn><code>NetworkError</code></dfn></li>
+ <li value="20"><dfn><code>AbortError</code></dfn></li>
+ <li value="21"><dfn><code>URLMismatchError</code></dfn></li>
+ <li value="22"><dfn><code>QuotaExceededError</code></dfn></li>
+ <li value="23"><dfn><code>TimeoutError</code></dfn></li>
+ <li value="24"><dfn><code>InvalidNodeTypeError</code></dfn></li>
+ <li value="25"><dfn><code>DataCloneError</code></dfn></li>
+ </ol>
+
+ <p class="example">For example, to <i>throw a <code>TimeoutError</code> exception</i>, a user
+ agent would construct a <code>DOMException</code> object whose type was the string "<code
+ data-x="">TimeoutError</code>" (and whose code was the number 23, for legacy reasons) and
+ actually throw that object as an exception.</p>
+
+ <p>The following features are defined in the DOM Events specification: <a
+ href="#refsDOMEVENTS">[DOMEVENTS]</a></p>
+
+ <ul class="brief">
+
+ <li><dfn><code>MouseEvent</code></dfn> interface</li>
+ <li><dfn><code>MouseEventInit</code></dfn> dictionary type</li>
+
+ <li>The <dfn><code>FocusEvent</code></dfn> interface and its <dfn data-x="dom-FocusEvent-relatedTarget"><code>relatedTarget</code></dfn> attribute</li>
+
+ <li>The <dfn><code>UIEvent</code></dfn> interface's <dfn data-x="dom-UIEvent-detail"><code>detail</code></dfn> attribute</li>
+
+ <li><dfn data-x="event-click"><code>click</code></dfn> event</li>
+ <li><dfn data-x="event-dblclick"><code>dblclick</code></dfn> event</li>
+ <li><dfn data-x="event-mousedown"><code>mousedown</code></dfn> event</li>
+ <li><dfn data-x="event-mouseenter"><code>mouseenter</code></dfn> event</li>
+ <li><dfn data-x="event-mouseleave"><code>mouseleave</code></dfn> event</li>
+ <li><dfn data-x="event-mousemove"><code>mousemove</code></dfn> event</li>
+ <li><dfn data-x="event-mouseout"><code>mouseout</code></dfn> event</li>
+ <li><dfn data-x="event-mouseover"><code>mouseover</code></dfn> event</li>
+ <li><dfn data-x="event-mouseup"><code>mouseup</code></dfn> event</li>
+ <li><dfn data-x="event-mousewheel"><code>mousewheel</code></dfn> event</li>
+
+ <li><dfn data-x="event-keydown"><code>keydown</code></dfn> event</li>
+ <li><dfn data-x="event-keyup"><code>keyup</code></dfn> event</li>
+ <li><dfn data-x="event-keypress"><code>keypress</code></dfn> event</li>
+
+ </ul>
+
+ <p>The following features are defined in the Touch Events specification: <a
+ href="#refsTOUCH">[TOUCH]</a></p>
+
+ <ul class="brief">
+
+ <li><dfn><code>Touch</code></dfn> interface</li>
+
+ <li><dfn>Touch point</dfn> concept</li>
+
+ </ul>
+
+ <p>This specification sometimes uses the term <dfn data-x="">name</dfn> to refer to the event's
+ <code data-x="dom-event-type">type</code>; as in, "an event named <code data-x="">click</code>"
+ or "if the event name is <code data-x="">keypress</code>". The terms "name" and "type" for
+ events are synonymous.</p>
+
+ <p>The following features are defined in the DOM Parsing and Serialisation specification: <a
+ href="#refsDOMPARSING">[DOMPARSING]</a></p>
+
+ <ul class="brief">
+ <li><dfn data-x="dom-innerHTML"><code>innerHTML</code></dfn></li>
+ <li><dfn data-x="dom-outerHTML"><code>outerHTML</code></dfn></li>
+ </ul>
+
+ <p class="note">User agents are also encouraged to implement the features described in the
+ <cite>HTML Editing APIs</cite> and <cite><code>UndoManager</code> and DOM Transaction</cite>
+ specifications.
+ <a href="#refsEDITING">[EDITING]</a>
+ <a href="#refsUNDO">[UNDO]</a>
+ </p>
+
+ <p>The following parts of the Fullscreen specification are referenced from this specification,
+ in part to define the rendering of <code>dialog</code> elements, and also to define how the
+ Fullscreen API interacts with the sandboxing features in HTML: <a
+ href="#refsFULLSCREEN">[FULLSCREEN]</a></p>
+
+ <ul class="brief">
+ <li>The <dfn>top layer</dfn> concept</li>
+ <li><dfn data-x="dom-element-requestFullscreen"><code>requestFullscreen()</code></dfn>
+ <li>The <dfn>fullscreen enabled flag</dfn></li>
+ <li>The <dfn>fully exit fullscreen</dfn> algorithm</li>
+ </ul>
+
+ </dd>
+
+
+
+ <dt>File API</dt>
+
+ <dd>
+
+ <p>This specification uses the following features defined in the File API specification: <a
+ href="#refsFILEAPI">[FILEAPI]</a></p>
+
+ <ul class="brief">
+
+ <li><dfn><code>Blob</code></dfn></li>
+ <li><dfn><code>File</code></dfn></li>
+ <li><dfn><code>FileList</code></dfn></li>
+ <li><dfn data-x="dom-Blob-close"><code>Blob.close()</code></dfn></li>
+ <li><dfn data-x="dom-Blob-type"><code>Blob.type</code></dfn></li>
+ <li>The concept of <dfn data-x="file-error-read">read errors</dfn></li>
+ </ul>
+
+ </dd>
+
+
+ <dt>XMLHttpRequest</dt>
+
+ <dd>
+
+ <p>This specification references the XMLHttpRequest specification to describe how the two
+ specifications interact and to use its <code>ProgressEvent</code> features. The following
+ features and terms are defined in the XMLHttpRequest specification: <a
+ href="#refsXHR">[XHR]</a></p>
+
+ <ul class="brief">
+
+ <li><dfn><code>XMLHttpRequest</code></dfn>
+ <li><dfn><code>ProgressEvent</code></dfn>
+ <li><dfn data-x="fire a progress event">Fire a progress event named <var data-x="">e</var></dfn>
+
+ </ul>
+
+ </dd>
+
+
+<!--TOPIC:HTML-->
+
+ <dt>Media Queries</dt>
+
+ <dd>
+
+ <p>Implementations must support the Media Queries language. <a href="#refsMQ">[MQ]</a></p>
+
+ </dd>
+
+
+ <dt>CSS modules</dt>
+
+ <dd>
+
+ <p>While support for CSS as a whole is not required of implementations of this specification
+ (though it is encouraged, at least for Web browsers), some features are defined in terms of
+ specific CSS requirements.</p>
+
+ <p>In particular, some features require that a string be <dfn>parsed as a CSS &lt;color&gt;
+ value</dfn>. When parsing a CSS value, user agents are required by the CSS specifications to
+ apply some error handling rules. These apply to this specification also. <a
+ href="#refsCSSCOLOR">[CSSCOLOR]</a> <a href="#refsCSS">[CSS]</a></p>
+
+ <p class="example">For example, user agents are required to close all open constructs upon
+ finding the end of a style sheet unexpectedly. Thus, when parsing the string "<code
+ data-x="">rgb(0,0,0</code>" (with a missing close-parenthesis) for a colour value, the close
+ parenthesis is implied by this error handling rule, and a value is obtained (the colour 'black').
+ However, the similar construct "<code data-x="">rgb(0,0,</code>" (with both a missing parenthesis
+ and a missing "blue" value) cannot be parsed, as closing the open construct does not result in a
+ viable value.</p>
+
+ <p>The term <dfn>CSS element reference identifier</dfn> is used as defined in the <cite>CSS
+ Image Values and Replaced Content</cite> specification to define the API that declares
+ identifiers for use with the CSS 'element()' function. <a
+ href="#refsCSSIMAGES">[CSSIMAGES]</a></p>
+
+ <p>Similarly, the term <dfn>provides a paint source</dfn> is used as defined in the <cite>CSS
+ Image Values and Replaced Content</cite> specification to define the interaction of certain HTML
+ elements with the CSS 'element()' function. <a href="#refsCSSIMAGES">[CSSIMAGES]</a></p>
+
+ <p>The term <dfn>default object size</dfn> is also defined in the <cite>CSS Image Values and
+ Replaced Content</cite> specification. <a href="#refsCSSIMAGES">[CSSIMAGES]</a></p>
+
+ <p>Implementations that support scripting must support the CSS Object Model. The following
+ features and terms are defined in the CSSOM specifications: <a href="#refsCSSOM">[CSSOM]</a> <a
+ href="#refsCSSOMVIEW">[CSSOMVIEW]</a>
+
+ <ul class="brief">
+ <li><dfn><code>Screen</code></dfn></li>
+ <li><dfn><code>LinkStyle</code></dfn></li>
+ <li><dfn><code>CSSStyleDeclaration</code></dfn></li>
+ <li><dfn data-x="dom-CSSStyleDeclaration-cssText"><code>cssText</code></dfn> attribute of <code>CSSStyleDeclaration</code></li>
+ <li><dfn><code>StyleSheet</code></dfn></li>
+ <li>The terms <dfn>create a CSS style sheet</dfn>, <dfn>remove a CSS style sheet</dfn>, and <dfn>associated CSS style sheet</dfn></li>
+ <li><dfn data-x="CSS style sheet">CSS style sheets</dfn> and their properties:
+ <dfn data-x="concept-css-style-sheet-type">type</dfn>,
+ <dfn data-x="concept-css-style-sheet-location">location</dfn>,
+ <dfn data-x="concept-css-style-sheet-parent-CSS-style-sheet">parent CSS style sheet</dfn>,
+ <dfn data-x="concept-css-style-sheet-owner-node">owner node</dfn>,
+ <dfn data-x="concept-css-style-sheet-owner-CSS-rule">owner CSS rule</dfn>,
+ <dfn data-x="concept-css-style-sheet-media">media</dfn>,
+ <dfn data-x="concept-css-style-sheet-title">title</dfn>,
+ <dfn data-x="concept-css-style-sheet-alternate-flag">alternate flag</dfn>,
+ <dfn data-x="concept-css-style-sheet-disabeld-flag">disabled flag</dfn>,
+ <dfn data-x="concept-css-style-sheet-CSS-rules">CSS rules</dfn>,
+ <dfn data-x="concept-css-style-sheet-origin-clean-flag">origin-clean flag</dfn>
+ </li>
+ <li><dfn>Alternative style sheet sets</dfn> and the <dfn>preferred style sheet set</dfn></li>
+ <li><dfn>Serializing a CSS value</dfn></li>
+ <li><dfn>Scroll an element into view</dfn></li>
+ <li><dfn>Scroll to the beginning of the document</dfn></li>
+ <li>The <dfn data-x="event-resize"><code>resize</code></dfn> event</li>
+ <li>The <dfn data-x="event-scroll"><code>scroll</code></dfn> event</li>
+ </ul>
+
+ <p>The term <dfn>environment encoding</dfn> is defined in the <cite>CSS Syntax</cite>
+ specifications. <a href="#refsCSSSYNTAX">[CSSSYNTAX]</a></p>
+
+ <p>The term <dfn>CSS styling attribute</dfn> is defined in the <cite>CSS Style Attributes</cite>
+ specification. <a href="#refsCSSATTR">[CSSATTR]</a></p>
+
+ <p>The <code>CanvasRenderingContext2D</code> object's use of fonts depends on the features
+ described in the CSS <cite>Fonts</cite> and <cite>Font Load Events</cite> specifications, including in particular
+ <dfn><code>FontLoader</code></dfn>. <a href="#refsCSSFONTS">[CSSFONTS]</a> <a
+ href="#refsCSSFONTLOAD">[CSSFONTLOAD]</a></p>
+
+ </dd>
+
+
+<!--TOPIC:Canvas-->
+
+ <dt>SVG</dt>
+
+ <dd>
+
+ <p>The following interface is defined in the SVG specification: <a href="#refsSVG">[SVG]</a></p>
+
+ <ul class="brief">
+ <li><dfn><code>SVGMatrix</code></dfn>
+ </ul>
+
+ <!-- mention that the parser supports it? -->
+
+ </dd>
+
+
+ <dt>WebGL</dt>
+
+ <dd>
+
+ <p>The following interface is defined in the WebGL specification: <a
+ href="#refsWEBGL">[WEBGL]</a></p>
+
+ <ul class="brief">
+ <li><dfn><code>WebGLRenderingContext</code></dfn>
+ </ul>
+
+ </dd>
+
+
+<!--TOPIC:HTML-->
+
+ <!-- mention that the parser supports mathml? -->
+
+
+<!--TOPIC:Video Text Tracks-->
+
+ <dt>WebVTT</dt>
+
+ <dd>
+
+ <p>Implementations may support <dfn>WebVTT</dfn> as a text track format for subtitles, captions,
+ chapter titles, metadata, etc, for media resources. <a href="#refsWEBVTT">[WEBVTT]</a></p>
+
+ <p>The following terms, used in this specification, are defined in the WebVTT specification:</p>
+
+ <ul class="brief">
+ <li><dfn>WebVTT file</dfn>
+ <li><dfn>WebVTT file using cue text</dfn>
+ <li><dfn>WebVTT file using chapter title text</dfn>
+ <li><dfn>WebVTT file using only nested cues</dfn>
+ <li><dfn>WebVTT parser</dfn>
+ <li>The <dfn>rules for updating the display of WebVTT text tracks</dfn>
+ <li>The <dfn>rules for interpreting WebVTT cue text</dfn>
+ <li>The WebVTT <dfn>text track cue writing direction</dfn>
+ </ul>
+
+ </dd>
+
+
+<!--TOPIC:WebSocket API-->
+
+ <dt>The WebSocket protocol</dt>
+
+ <dd>
+
+ <p>The following terms are defined in the WebSocket protocol specification: <a
+ href="#refsWSP">[WSP]</a></p>
+
+ <ul class="brief">
+
+ <li><dfn>establish a WebSocket connection</dfn>
+ <li><dfn>the WebSocket connection is established</dfn>
+ <li><dfn>validate the server's response</dfn>
+ <li><dfn>extensions in use</dfn>
+ <li><dfn>subprotocol in use</dfn>
+ <li><dfn>headers to send appropriate cookies</dfn>
+ <li><dfn>cookies set during the server's opening handshake</dfn>
+ <li><dfn>a WebSocket message has been received</dfn>
+ <li><dfn>send a WebSocket Message</dfn>
+ <li><dfn>fail the WebSocket connection</dfn>
+ <li><dfn>close the WebSocket connection</dfn>
+ <li><dfn>start the WebSocket closing handshake</dfn>
+ <li><dfn>the WebSocket closing handshake is started</dfn>
+ <li><dfn>the WebSocket connection is closed</dfn> (possibly <i data-x="">cleanly</i>)
+ <li><dfn>the WebSocket connection close code</dfn>
+ <li><dfn>the WebSocket connection close reason</dfn>
+
+ </ul>
+
+ </dd>
+
+
+<!--TOPIC:HTML-->
+
+ <dt>ARIA</dt>
+
+ <dd>
+
+ <p>The terms <dfn>strong native semantics</dfn> is used as defined in the ARIA specification.
+ The term <dfn>default implicit ARIA semantics</dfn> has the same meaning as the term <i>implicit
+ WAI-ARIA semantics</i> as used in the ARIA specification. <a href="#refsARIA">[ARIA]</a></p>
+
+ <p>The <dfn data-x="attr-aria-role"><code>role</code></dfn> and <code data-x="">aria-*</code>
+ attributes are defined in the ARIA specification. <a href="#refsARIA">[ARIA]</a></p>
+
+
+ </dd>
+
+
+ </dl>
+
+ <p>This specification does not <em>require</em> support of any particular network protocol, style
+ sheet language, scripting language, or any of the DOM specifications beyond those required in the
+ list above. However, the language described by this specification is biased towards CSS as the
+ styling language, JavaScript as the scripting language, and HTTP as the network protocol, and
+ several features assume that those languages and protocols are in use.</p>
+
+ <p>A user agent that implements the HTTP protocol must implement the Web Origin Concept
+ specification and the HTTP State Management Mechanism specification (Cookies) as well. <a
+ href="#refsHTTP">[HTTP]</a> <a href="#refsORIGIN">[ORIGIN]</a> <a
+ href="#refsCOOKIES">[COOKIES]</a></p>
+
+ <p class="note">This specification might have certain additional requirements on character
+ encodings, image formats, audio formats, and video formats in the respective sections.</p>
+
+ </div>
+
+ </div>
+
+
+ <h4>Extensibility</h4>
+
+ <p>Vendor-specific proprietary user agent extensions to this specification are strongly
+ discouraged. Documents must not use such extensions, as doing so reduces interoperability and
+ fragments the user base, allowing only users of specific user agents to access the content in
+ question.</p>
+
+ <div class="nodev">
+
+ <p>If such extensions are nonetheless needed, e.g. for experimental purposes, then vendors are
+ strongly urged to use one of the following extension mechanisms:</p>
+
+ <ul>
+
+ <li><p>For markup-level features that can be limited to the XML serialisation and need not be
+ supported in the HTML serialisation, vendors should use the namespace mechanism to define custom
+ namespaces in which the non-standard elements and attributes are supported.</p>
+
+ <li>
+
+ <p>For markup-level features that are intended for use with <span>the HTML syntax</span>,
+ extensions should be limited to new attributes of the form "<code data-x="">x-<var
+ data-x="">vendor</var>-<var data-x="">feature</var></code>", where <var data-x="">vendor</var> is a
+ short string that identifies the vendor responsible for the extension, and <var
+ data-x="">feature</var> is the name of the feature. New element names should not be created.
+ Using attributes for such extensions exclusively allows extensions from multiple vendors to
+ co-exist on the same element, which would not be possible with elements. Using the "<code
+ data-x="">x-<var data-x="">vendor</var>-<var data-x="">feature</var></code>" form allows extensions
+ to be made without risk of conflicting with future additions to the specification.</p>
+
+ <div class="example">
+
+ <p>For instance, a browser named "FerretBrowser" could use "ferret" as a vendor prefix, while a
+ browser named "Mellblom Browser" could use "mb". If both of these browsers invented extensions
+ that turned elements into scratch-and-sniff areas, an author experimenting with these features
+ could write:</p>
+
+ <pre>&lt;p>This smells of lemons!
+&lt;span x-ferret-smellovision x-ferret-smellcode="LEM01"
+ x-mb-outputsmell x-mb-smell="lemon juice">&lt;/span>&lt;/p></pre>
+
+ </div>
+
+ </li>
+
+ </ul>
+
+ <p>Attribute names beginning with the two characters "<code data-x="">x-</code>" are reserved for
+ user agent use and are guaranteed to never be formally added to the HTML language. For
+ flexibility, attributes names containing underscores (the U+005F LOW LINE character) are also
+ reserved for experimental purposes and are guaranteed to never be formally added to the HTML
+ language.</p>
+
+ <p class="note">Pages that use such attributes are by definition non-conforming.</p>
+
+ <p>For DOM extensions, e.g. new methods and IDL attributes, the new members should be prefixed by
+ vendor-specific strings to prevent clashes with future versions of this specification.</p>
+
+ <p>For events, experimental event types should be prefixed with vendor-specific strings.</p>
+
+ <div class="example">
+
+ <p>For example, if a user agent called "Pleas<!--e h-->old" were to add an event to indicate when
+ the user is going up in an elevator, it could use the prefix "<code data-x="">pleasold</code>" and
+ thus name the event "<code data-x="">pleasoldgoingup</code>", possibly with an event handler
+ attribute named "<code data-x="">onpleasoldgoingup</code>".</p>
+
+ </div>
+
+ <p>All extensions must be defined so that the use of extensions neither contradicts nor causes the
+ non-conformance of functionality defined in the specification.</p> <!-- thanks to QA Framework -->
+
+ <div class="example">
+
+ <p>For example, while strongly discouraged from doing so, an implementation "Foo Browser" could
+ add a new IDL attribute "<code data-x="">fooTypeTime</code>" to a control's DOM interface that
+ returned the time it took the user to select the current value of a control (say). On the other
+ hand, defining a new control that appears in a form's <code
+ data-x="dom-form-elements">elements</code> array would be in violation of the above requirement,
+ as it would violate the definition of <code data-x="dom-form-elements">elements</code> given in
+ this specification.</p>
+
+ </div>
+
+ <p>When adding new <span data-x="reflect">reflecting</span> IDL attributes corresponding to content
+ attributes of the form "<code data-x="">x-<var data-x="">vendor</var>-<var
+ data-x="">feature</var></code>", the IDL attribute should be named "<code data-x=""><var
+ data-x="">vendor</var><var data-x="">Feature</var></code>" (i.e. the "<code data-x="">x</code>" is
+ dropped from the IDL attribute's name).</p>
+
+ </div>
+
+ <hr>
+
+ <p>When vendor-neutral extensions to this specification are needed, either this specification can
+ be updated accordingly, or an extension specification can be written that overrides the
+ requirements in this specification. When someone applying this specification to their activities
+ decides that they will recognise the requirements of such an extension specification, it becomes
+ an <dfn data-x="other applicable specifications">applicable specification</dfn> for the purposes of
+ conformance requirements in this specification.</p>
+
+ <p class="note">Someone could write a specification that defines any arbitrary byte stream as
+ conforming, and then claim that their random junk is conforming. However, that does not mean that
+ their random junk actually is conforming for everyone's purposes: if someone else decides that
+ that specification does not apply to their work, then they can quite legitimately say that the
+ aforementioned random junk is just that, junk, and not conforming at all. As far as conformance
+ goes, what matters in a particular community is what that community <em>agrees</em> is
+ applicable.</p>
+
+ <div class="nodev">
+
+ <hr>
+
+ <p>User agents must treat elements and attributes that they do not understand as semantically
+ neutral; leaving them in the DOM (for DOM processors), and styling them according to CSS (for CSS
+ processors), but not inferring any meaning from them.</p>
+
+<!--ADD-TOPIC:Security-->
+ <p>When support for a feature is disabled (e.g. as an emergency measure to mitigate a security
+ problem, or to aid in development, or for performance reasons), user agents must act as if they
+ had no support for the feature whatsoever, and as if the feature was not mentioned in this
+ specification. For example, if a particular feature is accessed via an attribute in a Web IDL
+ interface, the attribute itself would be omitted from the objects that implement that interface
+ &mdash; leaving the attribute on the object but making it return null or throw an exception is
+ insufficient.</p>
+<!--REMOVE-TOPIC:Security-->
+
+ </div>
+
+
+ <div class="nodev">
+
+ <h4>Interactions with XPath and XSLT</h4>
+
+ <p id="xpath-1.0-processors">Implementations of XPath 1.0 that operate on <span>HTML
+ documents</span> parsed or created in the manners described in this specification (e.g. as part of
+ the <code data-x="">document.evaluate()</code> API) must act as if the following edit was applied
+ to the XPath 1.0 specification.</p>
+
+ <p>First, remove this paragraph:</p>
+
+ <blockquote cite="http://www.w3.org/TR/1999/REC-xpath-19991116#node-tests">
+
+ <p>A <a href="http://www.w3.org/TR/REC-xml-names#NT-QName">QName</a> in the node test is expanded
+ into an <a href="http://www.w3.org/TR/1999/REC-xpath-19991116#dt-expanded-name">expanded-name</a>
+ using the namespace declarations from the expression context. This is the same way expansion is
+ done for element type names in start and end-tags except that the default namespace declared with
+ <code data-x="">xmlns</code> is not used: if the <a
+ href="http://www.w3.org/TR/REC-xml-names#NT-QName">QName</a> does not have a prefix, then the
+ namespace URI is null (this is the same way attribute names are expanded). It is an error if the
+ <a href="http://www.w3.org/TR/REC-xml-names#NT-QName">QName</a> has a prefix for which there is
+ no namespace declaration in the expression context.</p>
+
+ </blockquote>
+
+ <p>Then, insert in its place the following:</p>
+
+ <blockquote cite="http://www.w3.org/Bugs/Public/show_bug.cgi?id=7059#c37">
+
+ <p>A QName in the node test is expanded into an expanded-name using the namespace declarations
+ from the expression context. If the QName has a prefix, then there must be a<!-- added 2009-10-27
+ - http://www.w3.org/Bugs/Public/show_bug.cgi?id=8062 --> namespace declaration for this prefix in
+ the expression context, and the corresponding<!-- typo fixed 2009-10-27 -
+ http://www.w3.org/Bugs/Public/show_bug.cgi?id=8063 --> namespace URI is the one that is
+ associated with this prefix. It is an error if the QName has a prefix for which there is no
+ namespace declaration in the expression context. </p>
+
+ <p>If the QName has no prefix and the principal node type of the axis is element, then the
+ default element namespace is used. Otherwise if the QName has no prefix, the namespace URI is
+ null. The default element namespace is a member of the context for the XPath expression. The
+ value of the default element namespace when executing an XPath expression through the DOM3 XPath
+ API is determined in the following way:</p>
+
+ <ol>
+
+ <li>If the context node is from an HTML DOM, the default element namespace is
+ "http://www.w3.org/1999/xhtml".</li>
+
+ <li>Otherwise, the default element namespace URI is null.</li>
+
+ </ol>
+
+ <p class="note">This is equivalent to adding the default element namespace feature of XPath 2.0
+ to XPath 1.0, and using the HTML namespace as the default element namespace for HTML documents.
+ It is motivated by the desire to have implementations be compatible with legacy HTML content
+ while still supporting the changes that this specification introduces to HTML regarding the
+ namespace used for HTML elements, and by the desire to use XPath 1.0 rather than XPath 2.0.</p>
+
+ </blockquote>
+
+ <p class="note">This change is a <span>willful violation</span> of the XPath 1.0 specification,
+ motivated by desire to have implementations be compatible with legacy content while still
+ supporting the changes that this specification introduces to HTML regarding which namespace is
+ used for HTML elements. <a href="#refsXPATH10">[XPATH10]</a></p> <!-- note: version matters for
+ this ref -->
+
+ <hr>
+
+ <p id="dom-based-xslt-1.0-processors">XSLT 1.0 processors outputting to a DOM when the output
+ method is "html" (either explicitly or via the defaulting rule in XSLT 1.0) are affected as
+ follows:</p>
+
+ <p>If the transformation program outputs an element in no namespace, the processor must, prior to
+ constructing the corresponding DOM element node, change the namespace of the element to the
+ <span>HTML namespace</span>, <span data-x="converted to ASCII lowercase">ASCII-lowercase</span> the
+ element's local name, and <span data-x="converted to ASCII lowercase">ASCII-lowercase</span> the
+ names of any non-namespaced attributes on the element.</p>
+
+ <p class="note">This requirement is a <span>willful violation</span> of the XSLT 1.0
+ specification, required because this specification changes the namespaces and case-sensitivity
+ rules of HTML in a manner that would otherwise be incompatible with DOM-based XSLT
+ transformations. (Processors that serialise the output are unaffected.) <a
+ href="#refsXSLT10">[XSLT10]</a></p> <!-- note: version matters for this ref -->
+
+ <hr>
+
+ <p>This specification does not specify precisely how XSLT processing interacts with the <span>HTML
+ parser</span> infrastructure (for example, whether an XSLT processor acts as if it puts any
+ elements into a <span>stack of open elements</span>). However, XSLT processors must <span>stop
+ parsing</span> if they successfully complete, and must set the <span>current document
+ readiness</span> first to "<code data-x="">interactive</code>"<!-- this synchronously fires an
+ event --> and then to "<code data-x="">complete</code>"<!-- this also synchronously fires an event
+ --> if they are aborted.</p>
+
+ <hr>
+
+ <p>This specification does not specify how XSLT interacts with the <span
+ data-x="navigate">navigation</span> algorithm, how it fits in with the <span>event loop</span>, nor
+ how error pages are to be handled (e.g. whether XSLT errors are to replace an incremental XSLT
+ output, or are rendered inline, etc).</p>
+
+ <p class="note">There are also additional non-normative comments regarding the interaction of XSLT
+ and HTML <a href="#scriptTagXSLT">in the <code>script</code> element section</a>, and of
+ XSLT, XPath, and HTML <a href="#template-XSLT-XPath">in the <code>template</code> element
+ section</a>.</p>
+
+ </div>
+
+
+
+
+ <h3>Case-sensitivity and string comparison</h3>
+
+ <p>Comparing two strings in a <dfn>case-sensitive</dfn> manner means comparing them exactly, code
+ point for code point.</p>
+
+ <p>Comparing two strings in an <dfn>ASCII case-insensitive</dfn> manner means comparing them
+ exactly, code point for code point, except that the characters in the range U+0041 to U+005A (i.e.
+ LATIN CAPITAL LETTER A to LATIN CAPITAL LETTER Z) and the corresponding characters in the range
+ U+0061 to U+007A (i.e. LATIN SMALL LETTER A to LATIN SMALL LETTER Z) are considered to also
+ match.</p>
+
+ <p>Comparing two strings in a <dfn>compatibility caseless</dfn> manner means using the Unicode
+ <i>compatibility caseless match</i> operation to compare the two strings, with no language-specific tailoirings. <a
+ href="#refsUNICODE">[UNICODE]</a></p>
+
+ <p>Except where otherwise stated, string comparisons must be performed in a
+ <span>case-sensitive</span> manner.</p>
+
+
+ <div class="nodev">
+
+ <p><dfn data-x="converted to ASCII uppercase">Converting a string to ASCII uppercase</dfn> means
+ replacing all characters in the range U+0061 to U+007A (i.e. LATIN SMALL LETTER A to LATIN SMALL
+ LETTER Z) with the corresponding characters in the range U+0041 to U+005A (i.e. LATIN CAPITAL
+ LETTER A to LATIN CAPITAL LETTER Z).</p>
+
+ <p><dfn data-x="converted to ASCII lowercase">Converting a string to ASCII lowercase</dfn> means
+ replacing all characters in the range U+0041 to U+005A (i.e. LATIN CAPITAL LETTER A to LATIN
+ CAPITAL LETTER Z) with the corresponding characters in the range U+0061 to U+007A (i.e. LATIN
+ SMALL LETTER A to LATIN SMALL LETTER Z).</p>
+
+ </div>
+
+
+ <p>A string <var data-x="">pattern</var> is a <dfn>prefix match</dfn> for a string <var
+ data-x="">s</var> when <var data-x="">pattern</var> is not longer than <var data-x="">s</var> and
+ truncating <var data-x="">s</var> to <var data-x="">pattern</var>'s length leaves the two strings as
+ matches of each other.</p>
+
+
+
+ <h3>Common microsyntaxes</h3>
+
+ <p>There are various places in HTML that accept particular data types, such as dates or numbers.
+ This section describes what the conformance criteria for content in those formats is, and how to
+ parse them.</p>
+
+ <div class="nodev">
+
+ <p class="note">Implementors are strongly urged to carefully examine any third-party libraries
+ they might consider using to implement the parsing of syntaxes described below. For example, date
+ libraries are likely to implement error handling behavior that differs from what is required in
+ this specification, since error-handling behavior is often not defined in specifications that
+ describe date syntaxes similar to those used in this specification, and thus implementations tend
+ to vary greatly in how they handle errors.</p>
+
+ </div>
+
+
+ <div class="nodev">
+
+ <h4>Common parser idioms</h4>
+
+ </div>
+
+ <p>The <dfn data-x="space character">space characters</dfn>, for the purposes of this
+ specification, are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), U+000A LINE FEED (LF), U+000C
+ FORM FEED (FF), and U+000D CARRIAGE RETURN (CR).</p>
+
+ <p>The <dfn data-x="White_Space">White_Space characters</dfn> are those that have the Unicode
+ property "White_Space" in the Unicode <code data-x="">PropList.txt</code> data file. <a
+ href="#refsUNICODE">[UNICODE]</a></p>
+
+ <p class="note">This should not be confused with the "White_Space" value (abbreviated "WS") of the
+ "Bidi_Class" property in the <code data-x="">Unicode.txt</code> data file.</p>
+
+ <p>The <dfn>control characters</dfn> are those whose Unicode "General_Category" property has the
+ value "Cc" in the Unicode <code data-x="">UnicodeData.txt</code> data file. <a
+ href="#refsUNICODE">[UNICODE]</a></p>
+
+ <p>The <dfn>uppercase ASCII letters</dfn> are the characters in the range U+0041 LATIN CAPITAL
+ LETTER A to U+005A LATIN CAPITAL LETTER Z.</p>
+
+ <p>The <dfn>lowercase ASCII letters</dfn> are the characters in the range U+0061 LATIN SMALL
+ LETTER A to U+007A LATIN SMALL LETTER Z.</p>
+
+ <p>The <dfn>ASCII digits</dfn> are the characters in the range U+0030 DIGIT ZERO (0) to U+0039
+ DIGIT NINE (9).</p>
+
+ <p>The <dfn>alphanumeric ASCII characters</dfn> are those that are either <span>uppercase ASCII
+ letters</span>, <span>lowercase ASCII letters</span>, or <span>ASCII digits</span>.</p>
+
+ <p>The <dfn>ASCII hex digits</dfn> are the characters in the ranges U+0030 DIGIT ZERO (0) to
+ U+0039 DIGIT NINE (9), U+0041 LATIN CAPITAL LETTER A to U+0046 LATIN CAPITAL LETTER F, and U+0061
+ LATIN SMALL LETTER A to U+0066 LATIN SMALL LETTER F.</p>
+
+ <p>The <dfn>uppercase ASCII hex digits</dfn> are the characters in the ranges U+0030 DIGIT ZERO (0) to
+ U+0039 DIGIT NINE (9) and U+0041 LATIN CAPITAL LETTER A to U+0046 LATIN CAPITAL LETTER F only.</p>
+
+ <p>The <dfn>lowercase ASCII hex digits</dfn> are the characters in the ranges U+0030 DIGIT ZERO
+ (0) to U+0039 DIGIT NINE (9) and U+0061 LATIN SMALL LETTER A to U+0066 LATIN SMALL LETTER F
+ only.</p>
+
+ <div class="nodev">
+
+ <p>Some of the micro-parsers described below follow the pattern of having an <var
+ data-x="">input</var> variable that holds the string being parsed, and having a <var
+ data-x="">position</var> variable pointing at the next character to parse in <var
+ data-x="">input</var>.</p>
+
+ <p>For parsers based on this pattern, a step that requires the user agent to <dfn>collect a
+ sequence of characters</dfn> means that the following algorithm must be run, with <var
+ data-x="">characters</var> being the set of characters that can be collected:</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">input</var> and <var data-x="">position</var> be the same variables as
+ those of the same name in the algorithm that invoked these steps.</p></li>
+
+ <li><p>Let <var data-x="">result</var> be the empty string.</p></li>
+
+ <li><p>While <var data-x="">position</var> doesn't point past the end of <var data-x="">input</var>
+ and the character at <var data-x="">position</var> is one of the <var data-x="">characters</var>,
+ append that character to the end of <var data-x="">result</var> and advance <var
+ data-x="">position</var> to the next character in <var data-x="">input</var>.</p></li>
+
+ <li><p>Return <var data-x="">result</var>.</p></li>
+
+ </ol>
+
+ <p>The step <dfn>skip whitespace</dfn> means that the user agent must <span>collect a sequence of
+ characters</span> that are <span data-x="space character">space characters</span>. The step
+ <dfn>skip White_Space characters</dfn> means that the user agent must <span>collect a sequence of
+ characters</span> that are <span>White_Space</span> characters. In both cases, the collected
+ characters are not used. <a href="#refsUNICODE">[UNICODE]</a></p>
+
+ <p>When a user agent is to <dfn>strip line breaks</dfn> from a string, the user agent must remove
+ any U+000A LINE FEED (LF) and U+000D CARRIAGE RETURN (CR) characters from that string.</p>
+
+ <p>When a user agent is to <dfn>strip leading and trailing whitespace</dfn> from a string, the
+ user agent must remove all <span data-x="space character">space characters</span> that are at the
+ start or end of the string.</p>
+
+ <p>When a user agent is to <dfn>strip and collapse whitespace</dfn> in a string, it must replace
+ any sequence of one or more consecutive <span data-x="space character">space characters</span> in
+ that string with a single U+0020 SPACE character, and then <span>strip leading and trailing
+ whitespace</span> from that string.</p>
+
+ <p>When a user agent has to <dfn>strictly split a string</dfn> on a particular delimiter character
+ <var data-x="">delimiter</var>, it must use the following algorithm:</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">input</var> be the string being parsed.</p></li>
+
+ <li><p>Let <var data-x="">position</var> be a pointer into <var data-x="">input</var>, initially
+ pointing at the start of the string.</p></li>
+
+ <li><p>Let <var data-x="">tokens</var> be an ordered list of tokens, initially empty.</p></li>
+
+ <li><p>While <var data-x="">position</var> is not past the end of <var data-x="">input</var>:</p>
+
+ <ol>
+
+ <li><p><span>Collect a sequence of characters</span> that are not the <var
+ data-x="">delimiter</var> character.</p></li>
+
+ <li><p>Append the string collected in the previous step to <var data-x="">tokens</var>.</p></li>
+
+ <li><p>Advance <var data-x="">position</var> to the next character in <var
+ data-x="">input</var>.</p></li> <!-- skips past the delimiter -->
+
+ </ol>
+
+ </li>
+
+ <li><p>Return <var data-x="">tokens</var>.</p></li>
+
+ </ol>
+
+ <p class="note">For the special cases of splitting a string <span data-x="split a string on
+ spaces">on spaces</span> and <span data-x="split a string on commas">on commas</span>, this
+ algorithm does not apply (those algorithms also perform <span data-x="strip leading and trailing
+ whitespace">whitespace trimming</span>).</p>
+
+ </div>
+
+
+
+ <h4>Boolean attributes</h4>
+
+ <p>A number of attributes are <dfn data-x="boolean attribute">boolean attributes</dfn>. The
+ presence of a boolean attribute on an element represents the true value, and the absence of the
+ attribute represents the false value.</p>
+
+ <p>If the attribute is present, its value must either be the empty string or a value that is an
+ <span>ASCII case-insensitive</span> match for the attribute's canonical name, with no leading or
+ trailing whitespace.</p>
+
+ <p class="note">The values "true" and "false" are not allowed on boolean attributes. To represent
+ a false value, the attribute has to be omitted altogether.</p>
+
+ <div class="example">
+
+ <p>Here is an example of a checkbox that is checked and disabled. The <code
+ data-x="attr-input-checked">checked</code> and <code data-x="attr-fe-disabled">disabled</code>
+ attributes are the boolean attributes.</p>
+
+ <pre>&lt;label>&lt;input type=checkbox checked name=cheese disabled> Cheese&lt;/label></pre>
+
+ <p>This could be equivalently written as this:
+
+ <pre>&lt;label>&lt;input type=checkbox checked=checked name=cheese disabled=disabled> Cheese&lt;/label></pre>
+
+ <p>You can also mix styles; the following is still equivalent:</p>
+
+ <pre>&lt;label>&lt;input type='checkbox' checked name=cheese disabled=""> Cheese&lt;/label></pre>
+
+ </div>
+
+
+
+ <h4>Keywords and enumerated attributes</h4>
+
+ <p>Some attributes are defined as taking one of a finite set of keywords. Such attributes are
+ called <dfn data-x="enumerated attribute">enumerated attributes</dfn>. The keywords are each
+ defined to map to a particular <em>state</em> (several keywords might map to the same state, in
+ which case some of the keywords are synonyms of each other; additionally, some of the keywords can
+ be said to be non-conforming, and are only in the specification for historical reasons). In
+ addition, two default states can be given. The first is the <i>invalid value default</i>, the
+ second is the <i>missing value default</i>.</p>
+
+ <p>If an enumerated attribute is specified, the attribute's value must be an <span>ASCII
+ case-insensitive</span> match for one of the given keywords that are not said to be
+ non-conforming, with no leading or trailing whitespace.</p>
+
+ <p>When the attribute is specified, if its value is an <span>ASCII case-insensitive</span> match
+ for one of the given keywords then that keyword's state is the state that the attribute
+ represents. If the attribute value matches none of the given keywords, but the attribute has an
+ <i>invalid value default</i>, then the attribute represents that state. Otherwise, if the
+ attribute value matches none of the keywords but there is a <i>missing value default</i> state
+ defined, then <em>that</em> is the state represented by the attribute. Otherwise, there is no
+ default, and invalid values mean that there is no state represented.</p>
+
+ <p>When the attribute is <em>not</em> specified, if there is a <i>missing value default</i> state
+ defined, then that is the state represented by the (missing) attribute. Otherwise, the absence of
+ the attribute means that there is no state represented.</p>
+
+ <p class="note">The empty string can be a valid keyword.</p>
+
+
+ <h4>Numbers</h4>
+
+ <h5>Signed integers</h5>
+
+ <p>A string is a <dfn>valid integer</dfn> if it consists of one or more <span>ASCII digits</span>,
+ optionally prefixed with a U+002D HYPHEN-MINUS character (-).</p>
+
+ <p>A <span>valid integer</span> without a U+002D HYPHEN-MINUS (-) prefix represents the number
+ that is represented in base ten by that string of digits. A <span>valid integer</span>
+ <em>with</em> a U+002D HYPHEN-MINUS (-) prefix represents the number represented in base ten by
+ the string of digits that follows the U+002D HYPHEN-MINUS, subtracted from zero.</p>
+
+ <div class="nodev">
+
+ <p>The <dfn>rules for parsing integers</dfn> are as given in the following algorithm. When
+ invoked, the steps must be followed in the order given, aborting at the first step that returns a
+ value. This algorithm will return either an integer or an error.</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">input</var> be the string being parsed.</p></li>
+
+ <li><p>Let <var data-x="">position</var> be a pointer into <var data-x="">input</var>, initially
+ pointing at the start of the string.</p></li>
+
+ <li><p>Let <var data-x="">sign</var> have the value "positive".</p></li>
+
+ <li><p><span>Skip whitespace</span>.</p></li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, return an
+ error.</p></li>
+
+ <li>
+
+ <p>If the character indicated by <var data-x="">position</var> (the first character) is a U+002D
+ HYPHEN-MINUS character (-):</p>
+
+ <ol>
+
+ <li>Let <var data-x="">sign</var> be "negative".</li>
+
+ <li>Advance <var data-x="">position</var> to the next character.</li>
+
+ <li>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, return an
+ error.</li>
+
+ </ol>
+
+ <p>Otherwise, if the character indicated by <var data-x="">position</var> (the first character)
+ is a U+002B PLUS SIGN character (+):</p>
+
+ <ol>
+
+ <li>Advance <var data-x="">position</var> to the next character. (The "<code data-x="">+</code>"
+ is ignored, but it is not conforming.)</li>
+
+ <li>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, return an
+ error.</li>
+
+ </ol>
+
+ </li>
+
+ <li><p>If the character indicated by <var data-x="">position</var> is not an <span data-x="ASCII
+ digits">ASCII digit</span>, then return an error.</p></li>
+
+ <!-- Ok. At this point we know we have a number. It might have
+ trailing garbage which we'll ignore, but it's a number, and we
+ won't return an error. -->
+
+ <li><p><span>Collect a sequence of characters</span> that are <span>ASCII digits</span>, and
+ interpret the resulting sequence as a base-ten integer. Let <var data-x="">value</var> be that
+ integer.</p></li>
+
+ <li><p>If <var data-x="">sign</var> is "positive", return <var
+ data-x="">value</var>, otherwise return the result of subtracting
+ <var data-x="">value</var> from zero.</p></li>
+
+ </ol>
+
+ </div>
+
+
+ <h5>Non-negative integers</h5>
+
+ <p>A string is a <dfn>valid non-negative integer</dfn> if it consists of one or more <span>ASCII
+ digits</span>.</p>
+
+ <p>A <span>valid non-negative integer</span> represents the number that is represented in base ten
+ by that string of digits.</p>
+
+ <div class="nodev">
+
+ <p>The <dfn>rules for parsing non-negative integers</dfn> are as given in the following algorithm.
+ When invoked, the steps must be followed in the order given, aborting at the first step that
+ returns a value. This algorithm will return either zero, a positive integer, or an error.</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">input</var> be the string being parsed.</p></li>
+
+ <li><p>Let <var data-x="">value</var> be the result of parsing <var data-x="">input</var> using the
+ <span>rules for parsing integers</span>.</p></li>
+
+ <li><p>If <var data-x="">value</var> is an error, return an error.</p></li>
+
+ <li><p>If <var data-x="">value</var> is less than zero, return an error.</p></li>
+
+ <li><p>Return <var data-x="">value</var>.</p></li>
+
+ </ol>
+
+ <!-- Implications: A leading + is ignored. A leading - is ignored if the value is zero. -->
+
+ </div>
+
+
+ <h5>Floating-point numbers</h5>
+
+ <p>A string is a <dfn>valid floating-point number</dfn> if it consists of:</p>
+
+ <ol class="brief">
+
+ <li>Optionally, a U+002D HYPHEN-MINUS character (-).</li>
+
+ <li>One or both of the following, in the given order:
+
+ <ol>
+
+ <li>A series of one or more <span>ASCII digits</span>.</li>
+
+ <li>
+
+ <ol>
+
+ <li>A single U+002E FULL STOP character (.).</li>
+
+ <li>A series of one or more <span>ASCII digits</span>.</li>
+
+ </ol>
+
+ </li>
+
+ </ol>
+
+ </li>
+
+ <li>Optionally:
+
+ <ol>
+
+ <li>Either a U+0065 LATIN SMALL LETTER E character (e) or a U+0045 LATIN CAPITAL LETTER E
+ character (E).</li>
+
+ <li>Optionally, a U+002D HYPHEN-MINUS character (-) or U+002B PLUS SIGN character (+).</li>
+
+ <li>A series of one or more <span>ASCII digits</span>.</li>
+
+ </ol>
+
+ </li>
+
+ </ol>
+
+ <p>A <span>valid floating-point number</span> represents the number obtained by multiplying the
+ significand by ten raised to the power of the exponent, where the significand is the first number,
+ interpreted as base ten (including the decimal point and the number after the decimal point, if
+ any, and interpreting the significand as a negative number if the whole string starts with a
+ U+002D HYPHEN-MINUS character (-) and the number is not zero), and where the exponent is the
+ number after the E, if any (interpreted as a negative number if there is a U+002D HYPHEN-MINUS
+ character (-) between the E and the number and the number is not zero, or else ignoring a U+002B
+ PLUS SIGN character (+) between the E and the number if there is one). If there is no E, then the
+ exponent is treated as zero.</p>
+
+ <p class="note">The Infinity and Not-a-Number (NaN) values are not <span data-x="valid
+ floating-point number">valid floating-point numbers</span>.</p>
+
+ <div class="nodev">
+
+ <p>The <dfn data-x="best representation of the number as a floating-point number">best
+ representation of the number <var data-x="">n</var> as a floating-point number</dfn> is the string
+ obtained from applying the JavaScript operator ToString to <var data-x="">n</var>. The JavaScript
+ operator ToString is not uniquely determined. When there are multiple possible strings that could
+ be obtained from the JavaScript operator ToString for a particular value, the user agent must
+ always return the same string for that value (though it may differ from the value used by other
+ user agents).</p>
+
+ <p>The <dfn>rules for parsing floating-point number values</dfn> are as given in the following
+ algorithm. This algorithm must be aborted at the first step that returns something. This algorithm
+ will return either a number or an error.</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">input</var> be the string being parsed.</p></li>
+
+ <li><p>Let <var data-x="">position</var> be a pointer into <var data-x="">input</var>, initially
+ pointing at the start of the string.</p></li>
+
+ <li><p>Let <var data-x="">value</var> have the value 1.</li>
+
+ <li><p>Let <var data-x="">divisor</var> have the value 1.</p></li>
+
+ <li><p>Let <var data-x="">exponent</var> have the value 1.</p></li>
+
+ <li><p><span>Skip whitespace</span>.</p></li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, return an
+ error.</p></li>
+
+ <li>
+
+ <p>If the character indicated by <var data-x="">position</var> is a U+002D HYPHEN-MINUS character
+ (-):</p>
+
+ <ol>
+
+ <li>Change <var data-x="">value</var> and <var data-x="">divisor</var> to &#x2212;1.</li>
+
+ <li>Advance <var data-x="">position</var> to the next character.</li>
+
+ <li>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, return an
+ error.</li>
+
+ </ol>
+
+ <p>Otherwise, if the character indicated by <var data-x="">position</var> (the first character)
+ is a U+002B PLUS SIGN character (+):</p>
+
+ <ol>
+
+ <li>Advance <var data-x="">position</var> to the next character. (The "<code data-x="">+</code>"
+ is ignored, but it is not conforming.)</li>
+
+ <li>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, return an
+ error.</li>
+
+ </ol>
+
+ </li>
+
+ <li><p>If the character indicated by <var data-x="">position</var> is a U+002E FULL STOP (.), and
+ that is not the last character in <var data-x="">input</var>, and the character after the
+ character indicated by <var data-x="">position</var> is an <span data-x="ASCII digits">ASCII
+ digit</span>, then set <var data-x="">value</var> to zero and jump to the step labeled
+ <i>fraction</i>.</p> <!-- we have to check there's a number so that ".e1" fails to parse but ".0"
+ does not -->
+
+ <li><p>If the character indicated by <var data-x="">position</var> is not an <span data-x="ASCII
+ digits">ASCII digit</span>, then return an error.</p></li>
+
+ <li><p><span>Collect a sequence of characters</span> that are <span>ASCII digits</span>, and
+ interpret the resulting sequence as a base-ten integer. Multiply <var data-x="">value</var> by
+ that integer.</p></li>
+
+ <li>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, jump to the
+ step labeled <i>conversion</i>.</li>
+
+ <li><p><i>Fraction</i>: If the character indicated by <var data-x="">position</var> is a U+002E
+ FULL STOP (.), run these substeps:</p>
+
+ <ol>
+
+ <li><p>Advance <var data-x="">position</var> to the next character.</p></li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, or if the
+ character indicated by <var data-x="">position</var> is not an <span data-x="ASCII digits">ASCII
+ digit</span>, U+0065 LATIN SMALL LETTER E (e), or U+0045 LATIN CAPITAL LETTER E (E), then jump
+ to the step labeled <i>conversion</i>.</li>
+
+ <li><p>If the character indicated by <var data-x="">position</var> is a U+0065 LATIN SMALL
+ LETTER E character (e) or a U+0045 LATIN CAPITAL LETTER E character (E), skip the remainder of
+ these substeps.</p>
+
+ <li><p><i>Fraction loop</i>: Multiply <var data-x="">divisor</var> by ten.</p></li>
+
+ <li>Add the value of the character indicated by <var data-x="">position</var>, interpreted as a
+ base-ten digit (0..9) and divided by <var data-x="">divisor</var>, to <var
+ data-x="">value</var>.</li>
+
+ <li><p>Advance <var data-x="">position</var> to the next character.</p></li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, then jump
+ to the step labeled <i>conversion</i>.</li>
+
+ <li><p>If the character indicated by <var data-x="">position</var> is an <span data-x="ASCII
+ digits">ASCII digit</span>, jump back to the step labeled <i>fraction loop</i> in these
+ substeps.</p></li>
+
+ </ol>
+
+ </li>
+
+ <li><p>If the character indicated by <var data-x="">position</var> is a U+0065 LATIN SMALL LETTER
+ E character (e) or a U+0045 LATIN CAPITAL LETTER E character (E), run these substeps:</p>
+
+ <ol>
+
+ <li><p>Advance <var data-x="">position</var> to the next character.</p></li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, then jump
+ to the step labeled <i>conversion</i>.</li>
+
+ <li>
+
+ <p>If the character indicated by <var data-x="">position</var> is a U+002D HYPHEN-MINUS
+ character (-):</p>
+
+ <ol>
+
+ <li>Change <var data-x="">exponent</var> to &#x2212;1.</li>
+
+ <li>Advance <var data-x="">position</var> to the next character.</li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, then
+ jump to the step labeled <i>conversion</i>.</li>
+
+ </ol>
+
+ <p>Otherwise, if the character indicated by <var data-x="">position</var> is a U+002B PLUS SIGN
+ character (+):</p>
+
+ <ol>
+
+ <li>Advance <var data-x="">position</var> to the next character.</li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, then
+ jump to the step labeled <i>conversion</i>.</li>
+
+ </ol>
+
+ </li>
+
+ <li><p>If the character indicated by <var data-x="">position</var> is not an <span data-x="ASCII
+ digits">ASCII digit</span>, then jump to the step labeled <i>conversion</i>.</li>
+
+ <li><p><span>Collect a sequence of characters</span> that are <span>ASCII digits</span>, and
+ interpret the resulting sequence as a base-ten integer. Multiply <var data-x="">exponent</var>
+ by that integer.</p></li>
+
+ <li><p>Multiply <var data-x="">value</var> by ten raised to the <var data-x="">exponent</var>th
+ power.</p></li>
+
+ </ol>
+
+ </li>
+
+ <li><p><i>Conversion</i>: Let <var data-x="">S</var> be the set of finite IEEE 754
+ double-precision floating-point values except &#x2212;0, but with two special values added: 2<sup
+ data-x="">1024</sup> and &#x2212;2<sup data-x="">1024</sup>.</p></li>
+
+ <li><p>Let <var data-x="">rounded-value</var> be the number in <var data-x="">S</var> that is
+ closest to <var data-x="">value</var>, selecting the number with an even significand if there are
+ two equally close values. (The two special values 2<sup data-x="">1024</sup> and &#x2212;2<sup
+ data-x="">1024</sup> are considered to have even significands for this purpose.)</p></li>
+
+ <li><p>If <var data-x="">rounded-value</var> is 2<sup data-x="">1024</sup> or &#x2212;2<sup
+ data-x="">1024</sup>, return an error.</p></li>
+
+ <li><p>Return <var data-x="">rounded-value</var>.</p></li>
+
+ </ol>
+
+ </div>
+
+
+<div class="nodev">
+ <h5 id="percentages-and-dimensions">Percentages and lengths</h5>
+
+ <p>The <dfn>rules for parsing dimension values</dfn> are as given in the following algorithm. When
+ invoked, the steps must be followed in the order given, aborting at the first step that returns a
+ value. This algorithm will return either a number greater than or equal to 1.0, or an error; if a
+ number is returned, then it is further categorised as either a percentage or a length.</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">input</var> be the string being parsed.</p></li>
+
+ <li><p>Let <var data-x="">position</var> be a pointer into <var data-x="">input</var>, initially
+ pointing at the start of the string.</p></li>
+
+ <li><p><span>Skip whitespace</span>.</p></li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, return an
+ error.</p></li>
+
+ <li><p>If the character indicated by <var data-x="">position</var> is a U+002B PLUS SIGN character
+ (+), advance <var data-x="">position</var> to the next character.</li>
+
+ <li><p><span>Collect a sequence of characters</span> that are U+0030 DIGIT ZERO (0) characters,
+ and discard them.</p></li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, return an
+ error.</p></li>
+
+ <li><p>If the character indicated by <var data-x="">position</var> is not one of U+0031 DIGIT ONE
+ (1) to U+0039 DIGIT NINE (9), then return an error.</p></li>
+
+ <!-- Ok. At this point we know we have a number. It might have trailing garbage which we'll
+ ignore, but it's a number, and we won't return an error. -->
+
+ <li><p><span>Collect a sequence of characters</span> that are <span>ASCII digits</span>, and
+ interpret the resulting sequence as a base-ten integer. Let <var data-x="">value</var> be that
+ number.</li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, return <var
+ data-x="">value</var> as a length.</p></li>
+
+ <li>
+
+ <p>If the character indicated by <var data-x="">position</var> is a U+002E FULL STOP character
+ (.):</p>
+
+ <ol>
+
+ <li><p>Advance <var data-x="">position</var> to the next character.</p></li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, or if the
+ character indicated by <var data-x="">position</var> is not an <span data-x="ASCII digits">ASCII
+ digit</span>, then return <var data-x="">value</var> as a length.</li>
+
+ <li><p>Let <var data-x="">divisor</var> have the value 1.</p></li>
+
+ <li><p><i>Fraction loop</i>: Multiply <var data-x="">divisor</var> by ten.</p></li>
+
+ <li>Add the value of the character indicated by <var data-x="">position</var>, interpreted as a
+ base-ten digit (0..9) and divided by <var data-x="">divisor</var>, to <var
+ data-x="">value</var>.</li>
+
+ <li><p>Advance <var data-x="">position</var> to the next character.</p></li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, then
+ return <var data-x="">value</var> as a length.</li>
+
+ <li><p>If the character indicated by <var data-x="">position</var> is an <span data-x="ASCII
+ digits">ASCII digit</span>, return to the step labeled <i>fraction loop</i> in these
+ substeps.</p></li>
+
+ </ol>
+
+ </li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, return <var
+ data-x="">value</var> as a length.</p></li>
+
+ <li><p>If the character indicated by <var data-x="">position</var> is a U+0025 PERCENT SIGN
+ character (%), return <var data-x="">value</var> as a percentage.</p></li>
+
+ <li><p>Return <var data-x="">value</var> as a length.</p></li>
+
+ </ol>
+
+ </div>
+
+
+ <h5>Lists of integers</h5>
+
+ <p>A <dfn>valid list of integers</dfn> is a number of <span data-x="valid integer">valid
+ integers</span> separated by U+002C COMMA characters, with no other characters (e.g. no <span
+ data-x="space character">space characters</span>). In addition, there might be restrictions on the
+ number of integers that can be given, or on the range of values allowed.</p>
+
+ <div class="nodev">
+
+ <p>The <dfn>rules for parsing a list of integers</dfn> are as follows:</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">input</var> be the string being parsed.</p></li>
+
+ <li><p>Let <var data-x="">position</var> be a pointer into <var data-x="">input</var>, initially
+ pointing at the start of the string.</p></li>
+
+ <li><p>Let <var data-x="">numbers</var> be an initially empty list of integers. This list will be
+ the result of this algorithm.</p></li>
+
+ <li><p>If there is a character in the string <var data-x="">input</var> at position <var
+ data-x="">position</var>, and it is either a U+0020 SPACE, U+002C COMMA, or U+003B SEMICOLON
+ character, then advance <var data-x="">position</var> to the next character in <var
+ data-x="">input</var>, or to beyond the end of the string if there are no more
+ characters.</p></li>
+
+ <li><p>If <var data-x="">position</var> points to beyond the end of <var data-x="">input</var>,
+ return <var data-x="">numbers</var> and abort.</p></li>
+
+ <li><p>If the character in the string <var data-x="">input</var> at position <var
+ data-x="">position</var> is a U+0020 SPACE, U+002C COMMA, or U+003B SEMICOLON character, then
+ return to step 4.</li>
+
+ <li><p>Let <var data-x="">negated</var> be false.</p></li> <li><p>Let <var data-x="">value</var> be
+ 0.</p></li>
+
+ <li><p>Let <var data-x="">started</var> be false. This variable is set to true when the parser
+ sees a number or a U+002D HYPHEN-MINUS character (-).</p></li>
+
+ <li><p>Let <var data-x="">got number</var> be false. This variable is set to true when the parser
+ sees a number.</p></li>
+
+ <li><p>Let <var data-x="">finished</var> be false. This variable is set to true to switch parser
+ into a mode where it ignores characters until the next separator.</p></li>
+
+ <li><p>Let <var data-x="">bogus</var> be false.</p></li>
+
+ <li><p><i>Parser</i>: If the character in the string <var data-x="">input</var> at position <var
+ data-x="">position</var> is:</p>
+
+ <dl class="switch">
+
+ <dt>A U+002D HYPHEN-MINUS character</dt>
+
+ <dd>
+
+ <p>Follow these substeps:</p>
+
+ <ol>
+
+ <li>If <var data-x="">got number</var> is true, let <var data-x="">finished</var> be true.</li>
+
+ <li>If <var data-x="">finished</var> is true, skip to the next step in the overall set of
+ steps.</li>
+
+ <li>If <var data-x="">started</var> is true, let <var data-x="">negated</var> be false.</li>
+
+ <li>Otherwise, if <var data-x="">started</var> is false and if <var data-x="">bogus</var> is
+ false, let <var data-x="">negated</var> be true.</li>
+
+ <li>Let <var data-x="">started</var> be true.</li>
+
+ </ol>
+
+ </dd>
+
+ <dt>An <span data-x="ASCII digits">ASCII digit</span></dt>
+
+ <dd>
+
+ <p>Follow these substeps:</p>
+
+ <ol>
+
+ <li>If <var data-x="">finished</var> is true, skip to the next step in the overall set of
+ steps.</li>
+
+ <li>Multiply <var data-x="">value</var> by ten.</li>
+
+ <li>Add the value of the digit, interpreted in base ten, to <var data-x="">value</var>.</li>
+
+ <li>Let <var data-x="">started</var> be true.</li>
+
+ <li>Let <var data-x="">got number</var> be true.</li>
+
+ </ol>
+
+ </dd>
+
+
+ <dt>A U+0020 SPACE character</dt>
+ <dt>A U+002C COMMA character</dt>
+ <dt>A U+003B SEMICOLON character</dt>
+
+ <dd>
+
+ <p>Follow these substeps:</p>
+
+ <ol>
+
+ <li>If <var data-x="">got number</var> is false, return the <var data-x="">numbers</var> list
+ and abort. This happens if an entry in the list has no digits, as in "<code
+ data-x="">1,2,x,4</code>".</li>
+
+ <li>If <var data-x="">negated</var> is true, then negate <var data-x="">value</var>.</li>
+
+ <li>Append <var data-x="">value</var> to the <var data-x="">numbers</var> list.</li>
+
+ <li>Jump to step 4 in the overall set of steps.</li>
+
+ </ol>
+
+ </dd>
+
+
+ <!-- <dt>A U+002E FULL STOP character</dt> -->
+ <dt>A character in the range U+0001 to U+001F, <!-- space --> U+0021 to U+002B, <!-- comma --> U+002D to U+002F, <!-- digits --> U+003A, <!-- semicolon --> U+003C to U+0040, <!-- a-z --> U+005B to U+0060, <!-- A-Z --> U+007b to U+007F
+ (i.e. any other non-alphabetic ASCII character)</dt>
+
+ <!--
+ Test: http://www.hixie.ch/tests/adhoc/html/flow/image-maps/004-demo.html
+ IE6 on Wine treats the following characters like this also: U+1-U+1f, U+21-U+2b, U+2d-U+2f, U+3a,
+ U+3c-U+40, U+5b-U+60, U+7b-U+82, U+84-U+89, U+8b, U+8d, U+8f-U+99, U+9b, U+9d, U+a0-U+bf, U+d7,
+ U+f7, U+1f6-U+1f9, U+218-U+24f, U+2a9-U+385, U+387, U+38b, U+38d, U+3a2, U+3cf, U+3d7-U+3d9, U+3db,
+ U+3dd, U+3df, U+3e1, U+3f4-U+400, U+40d, U+450, U+45d, U+482-U+48f, U+4c5-U+4c6, U+4c9-U+4ca,
+ U+4cd-U+4cf, U+4ec-U+4ed, U+4f6-U+4f7, U+4fa-U+530, U+557-U+560, U+588-U+5cf, U+5eb-U+5ef,
+ U+5f3-U+620, U+63b-U+640, U+64b-U+670, U+6b8-U+6b9, U+6bf, U+6cf, U+6d4, U+6d6-U+904, U+93a-U+957,
+ U+962-U+984, U+98d-U+98e, U+991-U+992, U+9a9, U+9b1, U+9b3-U+9b5, U+9ba-U+9db, U+9de, U+9e2-U+9ef,
+ U+9f2-U+a04, U+a0b-U+a0e, U+a11-U+a12, U+a29, U+a31, U+a34, U+a37, U+a3a-U+a58, U+a5d, U+a5f-U+a84,
+ U+a8c, U+a8e, U+a92, U+aa9, U+ab1, U+ab4, U+aba-U+adf, U+ae1-U+b04, U+b0d-U+b0e, U+b11-U+b12,
+ U+b29, U+b31, U+b34-U+b35, U+b3a-U+b5b, U+b5e, U+b62-U+b84, U+b8b-U+b8d, U+b91, U+b96-U+b98, U+b9b,
+ U+b9d, U+ba0-U+ba2, U+ba5-U+ba7, U+bab-U+bad, U+bb6, U+bba-U+c04, U+c0d, U+c11, U+c29, U+c34,
+ U+c3a-U+c5f, U+c62-U+c84, U+c8d, U+c91, U+ca9, U+cb4, U+cba-U+cdd, U+cdf, U+ce2-U+d04, U+d0d,
+ U+d11, U+d29, U+d3a-U+d5f, U+d62-U+e00, U+e2f, U+e31, U+e34-U+e3f, U+e46-U+e80, U+e83, U+e85-U+e86,
+ U+e89, U+e8b-U+e8c, U+e8e-U+e93, U+e98, U+ea0, U+ea4, U+ea6, U+ea8-U+ea9, U+eac, U+eaf-U+edb,
+ U+ede-U+109f, U+10c6-U+10cf, U+10f7-U+10ff, U+115a-U+115e, U+11a3-U+11a7, U+11fa-U+1dff,
+ U+1e9b-U+1e9f, U+1efa-U+1eff, U+1f16-U+1f17, U+1f1e-U+1f1f, U+1f46-U+1f47, U+1f4e-U+1f4f, U+1f58,
+ U+1f5a, U+1f5c, U+1f5e, U+1f7e-U+1f7f, U+1fb5, U+1fbd-U+1fc1, U+1fc5, U+1fcd-U+1fcf, U+1fd4-U+1fd5,
+ U+1fdc-U+1fdf, U+1fed-U+1ff1, U+1ff5, U+1ffd-U+249b, U+24ea-U+3004, U+3006-U+3040, U+3095-U+309a,
+ U+309f-U+30a0, U+30fb, U+30ff-U+3104, U+312d-U+3130, U+318f-U+4dff, U+9fa6-U+abff, U+d7a4-U+d7ff,
+ U+e000-U+f8ff, U+fa2e-U+faff, U+fb07-U+fb12, U+fb18-U+fb1e, U+fb37, U+fb3d, U+fb3f, U+fb42, U+fb45,
+ U+fbb2-U+fbd2, U+fbe9, U+fce1, U+fd3e-U+fd4f, U+fd90-U+fd91, U+fdc8-U+fdef, U+fdfc-U+fe7f,
+ U+fefd-U+ff20, U+ff3b-U+ff40, U+ff5b-U+ff65, U+ffa0, U+ffbf-U+ffc1, U+ffc8-U+ffc9, U+ffd0-U+ffd1,
+ U+ffd8-U+ffd9, U+ffdd-U+ffff
+ IE7 on Win2003 treats the following characters like this also instead: U+1-U+1f, U+21-U+2b,
+ U+2d-U+2f, U+3a, U+3c-U+40, U+5b-U+60, U+7b-U+82, U+84-U+89, U+8b, U+8d, U+8f-U+99, U+9b, U+9d,
+ U+a0-U+a9, U+ab-U+b4, U+b6-U+b9, U+bb-U+bf, U+d7, U+f7, U+220-U+221, U+234-U+24f, U+2ae-U+2af,
+ U+2b9-U+2ba, U+2c2-U+2df, U+2e5-U+2ed, U+2ef-U+344, U+346-U+379, U+37b-U+385, U+387, U+38b, U+38d,
+ U+3a2, U+3cf, U+3d8-U+3d9, U+3f4-U+3ff, U+482-U+48b, U+4c5-U+4c6, U+4c9-U+4ca, U+4cd-U+4cf,
+ U+4f6-U+4f7, U+4fa-U+530, U+557-U+558, U+55a-U+560, U+588-U+5cf, U+5eb-U+5ef, U+5f3-U+620,
+ U+63b-U+640, U+656-U+66f, U+6d4, U+6dd-U+6e0, U+6e9-U+6ec, U+6ee-U+6f9, U+6fd-U+70f, U+72d-U+72f,
+ U+740-U+77f, U+7b1-U+900, U+904, U+93a-U+93c, U+94d - U+94f, U+951-U+957, U+964-U+980, U+984,
+ U+98d-U+98e, U+991-U+992, U+9a9, U+9b1, U+9b3-U+9b5, U+9ba-U+9bd, U+9c5-U+9c6, U+9c9-U+9ca,
+ U+9cd-U+9d6, U+9d8-U+9db, U+9de, U+9e4-U+9ef, U+9f2-U+a01, U+a03-U+a04, U+a0b-U+a0e, U+a11-U+a12,
+ U+a29, U+a31, U+a34, U+a37, U+a3a-U+a3d, U+a43-U+a46, U+a49-U+a4a, U+a4d-U+a58, U+a5d, U+a5f-U+a6f,
+ U+a75-U+a80, U+a84, U+a8c, U+a8e, U+a92, U+aa9, U+ab1, U+ab4, U+aba-U+abc, U+ac6, U+aca,
+ U+acd-U+acf, U+ad1-U+adf, U+ae1-U+b00, U+b04, U+b0d-U+b0e, U+b11-U+b12, U+b29, U+b31, U+b34-U+b35,
+ U+b3a-U+b3c, U+b44-U+b46, U+b49 - U+b4a, U+b4d-U+b55, U+b58-U+b5b, U+b5e, U+b62-U+b81, U+b84,
+ U+b8b-U+b8d, U+b91, U+b96-U+b98, U+b9b, U+b9d, U+ba0 - U+ba2, U+ba5-U+ba7, U+bab-U+bad, U+bb6,
+ U+bba-U+bbd, U+bc3-U+bc5, U+bc9, U+bcd-U+bd6, U+bd8-U+c00, U+c04, U+c0d, U+c11, U+c29, U+c34,
+ U+c3a-U+c3d, U+c45, U+c49, U+c4d-U+c54, U+c57-U+c5f, U+c62-U+c81, U+c84, U+c8d, U+c91, U+ca9,
+ U+cb4, U+cba-U+cbd, U+cc5, U+cc9, U+ccd-U+cd4, U+cd7-U+cdd, U+cdf, U+ce2-U+d01, U+d04, U+d0d,
+ U+d11, U+d29, U+d3a-U+d3d, U+d44-U+d45, U+d49, U+d4d-U+d56, U+d58-U+d5f, U+d62-U+d81, U+d84,
+ U+d97-U+d99, U+db2, U+dbc, U+dbe - U+dbf, U+dc7-U+dce, U+dd5, U+dd7, U+de0-U+df1, U+df4-U+e00,
+ U+e3b-U+e3f, U+e4f-U+e80, U+e83, U+e85-U+e86, U+e89, U+e8b-U+e8c, U+e8e-U+e93, U+e98, U+ea0, U+ea4,
+ U+ea6, U+ea8-U+ea9, U+eac, U+eba, U+ebe-U+ebf, U+ec5-U+ecc, U+ece-U+edb, U+ede-U+eff, U+f01-U+f3f,
+ U+f48, U+f6b-U+f70, U+f82-U+f87, U+f8c-U+f8f, U+f98, U+fbd-U+fff, U+1022, U+1028, U+102b,
+ U+1033-U+1035, U+1037, U+1039-U+104f, U+105a-U+109f, U+10c6-U+10cf, U+10f7-U+10ff, U+115a - U+115e,
+ U+11a3-U+11a7, U+11fa-U+11ff, U+1207, U+1247, U+1249, U+124e-U+124f, U+1257, U+1259, U+125e-U+125f,
+ U+1287, U+1289, U+128e-U+128f, U+12af, U+12b1, U+12b6-U+12b7, U+12bf, U+12c1, U+12c6-U+12c7,
+ U+12cf, U+12d7, U+12ef, U+130f, U+1311, U+1316-U+1317, U+131f, U+1347, U+135b-U+139f,
+ U+13f5-U+1400, U+166d-U+166e, U+1677-U+1680, U+169b - U+169f, U+16eb-U+177f, U+17c9-U+181f, U+1843,
+ U+1878-U+187f, U+18aa-U+1dff, U+1e9c-U+1e9f, U+1efa-U+1eff, U+1f16-U+1f17, U+1f1e-U+1f1f,
+ U+1f46-U+1f47, U+1f4e-U+1f4f, U+1f58, U+1f5a, U+1f5c, U+1f5e, U+1f7e-U+1f7f, U+1fb5, U+1fbd,
+ U+1fbf-U+1fc1, U+1fc5, U+1fcd-U+1fcf, U+1fd4-U+1fd5, U+1fdc-U+1fdf, U+1fed-U+1ff1, U+1ff5,
+ U+1ffd-U+207e, U+2080-U+2101, U+2103-U+2106, U+2108-U+2109, U+2114, U+2116-U+2118, U+211e-U+2123,
+ U+2125, U+2127, U+2129, U+212e, U+2132, U+213a-U+215f, U+2184-U+3005, U+3008-U+3020, U+302a-U+3037,
+ U+303b-U+3104, U+312d-U+3130, U+318f - U+319f, U+31b8-U+33ff, U+4db6-U+4dff, U+9fa6-U+9fff,
+ U+a48d-U+abff, U+d7a4-U+d7ff, U+e000-U+f8ff, U+fa2e-U+faff, U+fb07-U+fb12, U+fb18-U+fb1c, U+fb1e,
+ U+fb29, U+fb37, U+fb3d, U+fb3f, U+fb42, U+fb45, U+fbb2-U+fbd2, U+fd3e-U+fd4f, U+fd90-U+fd91,
+ U+fdc8-U+fdef, U+fdfc-U+fe6f, U+fe73, U+fe75, U+fefd-U+ff20, U+ff3b-U+ff40, U+ff5b-U+ff9f,
+ U+ffbf-U+ffc1, U+ffc8-U+ffc9, U+ffd0-U+ffd1, U+ffd8-U+ffd9, U+ffdd-U+ffff
+-->
+
+ <dd>
+
+ <p>Follow these substeps:</p>
+
+ <ol>
+
+ <li>If <var data-x="">got number</var> is true, let <var data-x="">finished</var> be true.</li>
+
+ <li>If <var data-x="">finished</var> is true, skip to the next step in the overall set of
+ steps.</li>
+
+ <li>Let <var data-x="">negated</var> be false.</li>
+
+ </ol>
+
+ </dd>
+
+
+ <dt>Any other character</dt>
+ <!-- alphabetic a-z A-Z, and non-ASCII -->
+
+ <dd>
+
+ <p>Follow these substeps:</p>
+
+ <ol>
+
+ <li>If <var data-x="">finished</var> is true, skip to the next step in the overall set of
+ steps.</li>
+
+ <li>Let <var data-x="">negated</var> be false.</li>
+
+ <li>Let <var data-x="">bogus</var> be true.</li>
+
+ <li>If <var data-x="">started</var> is true, then return the <var data-x="">numbers</var> list,
+ and abort. (The value in <var data-x="">value</var> is not appended to the list first; it is
+ dropped.)</li>
+
+ </ol>
+
+ </dd>
+
+ </dl>
+
+ </li>
+
+ <li><p>Advance <var data-x="">position</var> to the next character in <var data-x="">input</var>,
+ or to beyond the end of the string if there are no more characters.</p></li>
+
+ <li><p>If <var data-x="">position</var> points to a character (and not to beyond the end of <var
+ data-x="">input</var>), jump to the big <i>Parser</i> step above.</p></li>
+
+ <li><p>If <var data-x="">negated</var> is true, then negate <var data-x="">value</var>.</li>
+
+ <li><p>If <var data-x="">got number</var> is true, then append <var data-x="">value</var> to the
+ <var data-x="">numbers</var> list.</li>
+
+ <li><p>Return the <var data-x="">numbers</var> list and abort.</p></li>
+
+ </ol>
+
+ </div>
+
+
+ <div class="nodev">
+
+ <h5>Lists of dimensions</h5>
+
+ <!-- no definition of a type since no conforming feature uses this syntax (it's only used in
+ cols="" and rows="" on <frameset> elements -->
+
+ <p>The <dfn>rules for parsing a list of dimensions</dfn> are as follows. These rules return a list
+ of zero or more pairs consisting of a number and a unit, the unit being one of <i>percentage</i>,
+ <i>relative</i>, and <i>absolute</i>.</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">raw input</var> be the string being parsed.</p></li>
+
+ <li><p>If the last character in <var data-x="">raw input</var> is a U+002C COMMA character (,),
+ then remove that character from <var data-x="">raw input</var>.</p></li>
+
+ <li><p><span data-x="split a string on commas">Split the string <var data-x="">raw input</var> on
+ commas</span>. Let <var data-x="">raw tokens</var> be the resulting list of tokens.</p></li>
+
+ <li><p>Let <var data-x="">result</var> be an empty list of number/unit pairs.</p></li>
+
+ <li>
+
+ <p>For each token in <var data-x="">raw tokens</var>, run the following substeps:</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">input</var> be the token.</p></li>
+
+ <li><p>Let <var data-x="">position</var> be a pointer into <var data-x="">input</var>,
+ initially pointing at the start of the string.</p></li>
+
+ <li><p>Let <var data-x="">value</var> be the number 0.</p></li>
+
+ <li><p>Let <var data-x="">unit</var> be <i>absolute</i>.</p></li>
+
+ <li><p>If <var data-x="">position</var> is past the end of <var data-x="">input</var>, set <var
+ data-x="">unit</var> to <i>relative</i> and jump to the last substep.</p></li>
+
+ <li><p>If the character at <var data-x="">position</var> is an <span data-x="ASCII
+ digits">ASCII digit</span>, <span>collect a sequence of characters</span> that are <span>ASCII
+ digits</span>, interpret the resulting sequence as an integer in base ten, and increment <var
+ data-x="">value</var> by that integer.</p></li>
+
+ <li>
+
+ <p>If the character at <var data-x="">position</var> is a U+002E FULL STOP character (.), run
+ these substeps:</p>
+
+ <ol>
+
+ <li><p><span>Collect a sequence of characters</span> consisting of <span data-x="space
+ character">space characters</span> and <span>ASCII digits</span>. Let <var data-x="">s</var>
+ be the resulting sequence.</p></li>
+
+ <li><p>Remove all <span data-x="space character">space characters</span> in <var
+ data-x="">s</var>.</p></li>
+
+ <li>
+
+ <p>If <var data-x="">s</var> is not the empty string, run these subsubsteps:</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">length</var> be the number of characters in <var
+ data-x="">s</var> (after the spaces were removed).</p></li>
+
+ <li><p>Let <var data-x="">fraction</var> be the result of interpreting <var
+ data-x="">s</var> as a base-ten integer, and then dividing that number by <span
+ data-x="">10<sup data-x=""><var data-x="">length</var></sup></span>.</li>
+
+ <li><p>Increment <var data-x="">value</var> by <var data-x="">fraction</var>.</p></li>
+
+ </ol>
+
+ </li>
+
+ </ol>
+
+ </li>
+
+ <li><p><span>Skip whitespace</span>.</p></li>
+
+ <li>
+
+ <p>If the character at <var data-x="">position</var> is a U+0025 PERCENT SIGN character (%),
+ then set <var data-x="">unit</var> to <i>percentage</i>.</p>
+
+ <p>Otherwise, if the character at <var data-x="">position</var> is a U+002A ASTERISK character
+ (*), then set <var data-x="">unit</var> to <i>relative</i>.</p>
+
+ </li>
+
+ <!-- the remaining characters in /input/ are ignored -->
+
+ <li><p>Add an entry to <var data-x="">result</var> consisting of the number given by <var
+ data-x="">value</var> and the unit given by <var data-x="">unit</var>.</p></li>
+
+ </ol>
+
+ </li>
+
+ <li><p>Return the list <var data-x="">result</var>.</p></li>
+
+ </ol>
+
+ </div>
+
+
+ <h4>Dates and times</h4>
+
+ <p>In the algorithms below, the <dfn>number of days in month <var data-x="">month</var> of year
+ <var data-x="">year</var></dfn> is: <em>31</em> if <var data-x="">month</var> is 1, 3, 5, 7, 8,
+ 10, or 12; <em>30</em> if <var data-x="">month</var> is 4, 6, 9, or 11; <em>29</em> if <var
+ data-x="">month</var> is 2 and <var data-x="">year</var> is a number divisible by 400, or if <var
+ data-x="">year</var> is a number divisible by 4 but not by 100; and <em>28</em> otherwise. This
+ takes into account leap years in the Gregorian calendar. <a
+ href="#refsGREGORIAN">[GREGORIAN]</a></p>
+
+ <p>When <span>ASCII digits</span> are used in the date and time syntaxes defined in this section,
+ they express numbers in base ten.</p>
+
+ <div class="nodev">
+
+ <p class="note">While the formats described here are intended to be subsets of the corresponding
+ ISO8601 formats, this specification defines parsing rules in much more detail than ISO8601.
+ Implementors are therefore encouraged to carefully examine any date parsing libraries before using
+ them to implement the parsing rules described below; ISO8601 libraries might not parse dates and
+ times in exactly the same manner. <a href="#refsISO8601">[ISO8601]</a></p>
+
+ </div>
+
+ <p>Where this specification refers to the <dfn>proleptic Gregorian calendar</dfn>, it means the
+ modern Gregorian calendar, extrapolated backwards to year 1. A date in the <span>proleptic
+ Gregorian calendar</span>, sometimes explicitly referred to as a <dfn>proleptic-Gregorian
+ date</dfn>, is one that is described using that calendar even if that calendar was not in use at
+ the time (or place) in question. <a href="#refsGREGORIAN">[GREGORIAN]</a></p>
+
+ <p class="note">The use of the Gregorian calendar as the wire format in this specification is an
+ arbitrary choice resulting from the cultural biases of those involved in the decision. See also
+ the section discussing <a href="#input-author-notes">date, time, and number formats</a> in forms
+ <span class="nodev">(for authors), <a href="#input-impl-notes">implemention notes regarding
+ localization of form controls</a>,</span> and the <code>time</code> element.</p>
+
+
+ <h5>Months</h5>
+
+ <p>A <dfn data-x="concept-month">month</dfn> consists of a specific <span>proleptic-Gregorian
+ date</span> with no time-zone information and no date information beyond a year and a month. <a
+ href="#refsGREGORIAN">[GREGORIAN]</a></p>
+
+ <p>A string is a <dfn>valid month string</dfn> representing a year <var data-x="">year</var> and
+ month <var data-x="">month</var> if it consists of the following components in the given order:</p>
+
+ <ol>
+
+ <li>Four or more <span>ASCII digits</span>, representing <var data-x="">year</var>, where <var
+ data-x="">year</var>&nbsp;&gt;&nbsp;0</li>
+
+ <li>A U+002D HYPHEN-MINUS character (-)</li>
+
+ <li>Two <span>ASCII digits</span>, representing the month <var data-x="">month</var>, in the range
+ 1&nbsp;&le;&nbsp;<var data-x="">month</var>&nbsp;&le;&nbsp;12</li>
+
+ </ol>
+
+ <div class="nodev">
+
+ <p>The rules to <dfn>parse a month string</dfn> are as follows. This will return either a year and
+ month, or nothing. If at any point the algorithm says that it "fails", this means that it is
+ aborted at that point and returns nothing.</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">input</var> be the string being parsed.</p></li>
+
+ <li><p>Let <var data-x="">position</var> be a pointer into <var data-x="">input</var>, initially
+ pointing at the start of the string.</p></li>
+
+ <li><p><span>Parse a month component</span> to obtain <var data-x="">year</var> and <var
+ data-x="">month</var>. If this returns nothing, then fail.</p>
+
+ <li><p>If <var data-x="">position</var> is <em>not</em> beyond the
+ end of <var data-x="">input</var>, then fail.</p></li>
+
+ <li><p>Return <var data-x="">year</var> and <var data-x="">month</var>.</p></li>
+
+ </ol>
+
+ <p>The rules to <dfn>parse a month component</dfn>, given an <var data-x="">input</var> string and
+ a <var data-x="">position</var>, are as follows. This will return either a year and a month, or
+ nothing. If at any point the algorithm says that it "fails", this means that it is aborted at that
+ point and returns nothing.</p>
+
+ <ol>
+
+ <li><p><span>Collect a sequence of characters</span> that are <span>ASCII digits</span>. If the
+ collected sequence is not at least four characters long, then fail. Otherwise, interpret the
+ resulting sequence as a base-ten integer. Let that number be the <var
+ data-x="">year</var>.</p></li>
+
+ <li><p>If <var data-x="">year</var> is not a number greater than zero, then fail.</p></li>
+
+ <li><p>If <var data-x="">position</var> is beyond the end of <var data-x="">input</var> or if the
+ character at <var data-x="">position</var> is not a U+002D HYPHEN-MINUS character, then fail.
+ Otherwise, move <var data-x="">position</var> forwards one character.</p></li>
+
+ <li><p><span>Collect a sequence of characters</span> that are <span>ASCII digits</span>. If the
+ collected sequence is not exactly two characters long, then fail. Otherwise, interpret the
+ resulting sequence as a base-ten integer. Let that number be the <var
+ data-x="">month</var>.</p></li>
+
+ <li><p>If <var data-x="">month</var> is not a number in the range 1&nbsp;&le;&nbsp;<var
+ data-x="">month</var>&nbsp;&le;&nbsp;12, then fail.</p></li>
+
+ <li><p>Return <var data-x="">year</var> and <var data-x="">month</var>.</p></li>
+
+ </ol>
+
+ </div>
+
+
+ <h5>Dates</h5>
+
+ <p>A <dfn data-x="concept-date">date</dfn> consists of a specific <span>proleptic-Gregorian
+ date</span> with no time-zone information, consisting of a year, a month, and a day. <a
+ href="#refsGREGORIAN">[GREGORIAN]</a></p>
+
+ <p>A string is a <dfn>valid date string</dfn> representing a year <var data-x="">year</var>, month
+ <var data-x="">month</var>, and day <var data-x="">day</var> if it consists of the following
+ components in the given order:</p>
+
+ <ol>
+
+ <li>A <span>valid month string</span>, representing <var data-x="">year</var> and <var
+ data-x="">month</var></li>
+
+ <li>A U+002D HYPHEN-MINUS character (-)</li>
+
+ <li>Two <span>ASCII digits</span>, representing <var data-x="">day</var>, in the range
+ 1&nbsp;&le;&nbsp;<var data-x="">day</var>&nbsp;&le;&nbsp;<var data-x="">maxday</var> where <var
+ data-x="">maxday</var> is the <span data-x="number of days in month month of year year">number of
+ days in the month <var data-x="">month</var> and year <var data-x="">year</var></span></li>
+
+ </ol>
+
+ <div class="nodev">
+
+ <p>The rules to <dfn>parse a date string</dfn> are as follows. This will return either a date, or
+ nothing. If at any point the algorithm says that it "fails", this means that it is aborted at that
+ point and returns nothing.</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">input</var> be the string being parsed.</p></li>
+
+ <li><p>Let <var data-x="">position</var> be a pointer into <var data-x="">input</var>, initially
+ pointing at the start of the string.</p></li>
+
+ <li><p><span>Parse a date component</span> to obtain <var data-x="">year</var>, <var
+ data-x="">month</var>, and <var data-x="">day</var>. If this returns nothing, then fail.</p>
+
+ <li><p>If <var data-x="">position</var> is <em>not</em> beyond the end of <var
+ data-x="">input</var>, then fail.</p></li>
+
+ <li><p>Let <var data-x="">date</var> be the date with year <var data-x="">year</var>, month <var
+ data-x="">month</var>, and day <var data-x="">day</var>.</p></li>
+
+ <li><p>Return <var data-x="">date</var>.</p></li>
+
+ </ol>
+
+ <p>The rules to <dfn>parse a date component</dfn>, given an <var data-x="">input</var> string and a
+ <var data-x="">position</var>, are as follows. This will return either a year, a month, and a day,
+ or nothing. If at any point the algorithm says that it "fails", this means that it is aborted at
+ that point and returns nothing.</p>
+
+ <ol>
+
+ <li><p><span>Parse a month component</span> to obtain <var data-x="">year</var> and <var
+ data-x="">month</var>. If this returns nothing, then fail.</li>
+
+ <li><p>Let <var data-x="">maxday</var> be the <span>number of days in month <var
+ data-x="">month</var> of year <var data-x="">year</var></span>.</p></li>
+
+ <li><p>If <var data-x="">position</var> is beyond the end of <var data-x="">input</var> or if the
+ character at <var data-x="">position</var> is not a U+002D HYPHEN-MINUS character, then fail.
+ Otherwise, move <var data-x="">position</var> forwards one character.</p></li>
+
+ <li><p><span>Collect a sequence of characters</span> that are <span>ASCII digits</span>. If the
+ collected sequence is not exactly two characters long, then fail. Otherwise, interpret the
+ resulting sequence as a base-ten integer. Let that number be the <var
+ data-x="">day</var>.</p></li>
+
+ <li><p>If <var data-x="">day</var> is not a number in the range 1&nbsp;&le;&nbsp;<var
+ data-x="">day</var>&nbsp;&le;&nbsp;<var data-x="">maxday</var>, then fail.</li>
+
+ <li><p>Return <var data-x="">year</var>, <var data-x="">month</var>, and <var
+ data-x="">day</var>.</p></li>
+
+ </ol>
+
+ </div>
+
+
+ <h5>Yearless dates</h5>
+
+ <p>A <dfn data-x="concept-yearless-date">yearless date</dfn> consists of a Gregorian month and a
+ day within that month, but with no associated year. <a href="#refsGREGORIAN">[GREGORIAN]</a></p>
+
+ <p>A string is a <dfn>valid yearless date string</dfn> representing a month <var
+ data-x="">month</var> and a day <var data-x="">day</var> if it consists of the following components
+ in the given order:</p>
+
+ <ol>
+
+ <li>Optionally, two U+002D HYPHEN-MINUS characters (-)</li>
+
+ <li>Two <span>ASCII digits</span>, representing the month <var data-x="">month</var>, in the range
+ 1&nbsp;&le;&nbsp;<var data-x="">month</var>&nbsp;&le;&nbsp;12</li>
+
+ <li>A U+002D HYPHEN-MINUS character (-)</li>
+
+ <li>Two <span>ASCII digits</span>, representing <var data-x="">day</var>, in the range
+ 1&nbsp;&le;&nbsp;<var data-x="">day</var>&nbsp;&le;&nbsp;<var data-x="">maxday</var> where <var
+ data-x="">maxday</var> is the <span data-x="number of days in month month of year year">number of
+ days</span> in the month <var data-x="">month</var> and any arbitrary leap year (e.g. 4 or
+ 2000)</li>
+
+ </ol>
+
+ <p class="note">In other words, if the <var data-x="">month</var> is "<code data-x="">02</code>",
+ meaning February, then the day can be 29, as if the year was a leap year.</p>
+
+ <div class="nodev">
+
+ <p>The rules to <dfn>parse a yearless date string</dfn> are as follows. This will return either a
+ month and a day, or nothing. If at any point the algorithm says that it "fails", this means that
+ it is aborted at that point and returns nothing.</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">input</var> be the string being parsed.</p></li>
+
+ <li><p>Let <var data-x="">position</var> be a pointer into <var data-x="">input</var>, initially
+ pointing at the start of the string.</p></li>
+
+ <li><p><span>Parse a yearless date component</span> to obtain <var data-x="">month</var> and <var
+ data-x="">day</var>. If this returns nothing, then fail.</p>
+
+ <li><p>If <var data-x="">position</var> is <em>not</em> beyond the end of <var
+ data-x="">input</var>, then fail.</p></li>
+
+ <li><p>Return <var data-x="">month</var> and <var data-x="">day</var>.</p></li>
+
+ </ol>
+
+ <p>The rules to <dfn>parse a yearless date component</dfn>, given an <var data-x="">input</var>
+ string and a <var data-x="">position</var>, are as follows. This will return either a month and a
+ day, or nothing. If at any point the algorithm says that it "fails", this means that it is aborted
+ at that point and returns nothing.</p>
+
+ <ol>
+
+ <li><p><span>Collect a sequence of characters</span> that are U+002D HYPHEN-MINUS characters (-).
+ If the collected sequence is not exactly zero or two characters long, then fail.</p></li>
+
+ <li><p><span>Collect a sequence of characters</span> that are <span>ASCII digits</span>. If the
+ collected sequence is not exactly two characters long, then fail. Otherwise, interpret the
+ resulting sequence as a base-ten integer. Let that number be the <var
+ data-x="">month</var>.</p></li>
+
+ <li><p>If <var data-x="">month</var> is not a number in the range 1&nbsp;&le;&nbsp;<var
+ data-x="">month</var>&nbsp;&le;&nbsp;12, then fail.</p></li>
+
+ <li><p>Let <var data-x="">maxday</var> be the <span data-x="number of days in month month of year
+ year">number of days</span> in month <var data-x="">month</var> of any arbitrary leap year (e.g. 4
+ or 2000).</p></li>
+
+ <li><p>If <var data-x="">position</var> is beyond the end of <var data-x="">input</var> or if the
+ character at <var data-x="">position</var> is not a U+002D HYPHEN-MINUS character, then fail.
+ Otherwise, move <var data-x="">position</var> forwards one character.</p></li>
+
+ <li><p><span>Collect a sequence of characters</span> that are <span>ASCII digits</span>. If the
+ collected sequence is not exactly two characters long, then fail. Otherwise, interpret the
+ resulting sequence as a base-ten integer. Let that number be the <var
+ data-x="">day</var>.</p></li>
+
+ <li><p>If <var data-x="">day</var> is not a number in the range 1&nbsp;&le;&nbsp;<var
+ data-x="">day</var>&nbsp;&le;&nbsp;<var data-x="">maxday</var>, then fail.</li>
+
+ <li><p>Return <var data-x="">month</var> and <var data-x="">day</var>.</p></li>
+
+ </ol>
+
+ </div>
+
+
+ <h5>Times</h5>
+
+ <p>A <dfn data-x="concept-time">time</dfn> consists of a specific time with no time-zone
+ information, consisting of an hour, a minute, a second, and a fraction of a second.</p>
+
+ <p>A string is a <dfn>valid time string</dfn> representing an hour <var data-x="">hour</var>, a
+ minute <var data-x="">minute</var>, and a second <var data-x="">second</var> if it consists of the
+ following components in the given order:</p>
+
+ <ol>
+
+ <li>Two <span>ASCII digits</span>, representing <var data-x="">hour</var>, in the range
+ 0&nbsp;&le;&nbsp;<var data-x="">hour</var>&nbsp;&le;&nbsp;23</li>
+
+ <li>A U+003A COLON character (:)</li>
+
+ <li>Two <span>ASCII digits</span>, representing <var data-x="">minute</var>, in the range
+ 0&nbsp;&le;&nbsp;<var data-x="">minute</var>&nbsp;&le;&nbsp;59</li>
+
+ <li>If <var data-x="">second</var> is non-zero, or optionally if <var data-x="">second</var> is
+ zero:
+
+ <ol>
+
+ <li>A U+003A COLON character (:)</li>
+
+ <li>Two <span>ASCII digits</span>, representing the integer part of <var data-x="">second</var>,
+ in the range 0&nbsp;&le;&nbsp;<var data-x="">s</var>&nbsp;&le;&nbsp;59</li>
+
+ <li>If <var data-x="">second</var> is not an integer, or optionally if <var
+ data-x="">second</var> is an integer:
+
+ <ol>
+
+ <li>A 002E FULL STOP character (.)</li>
+
+ <li>One, two, or three <span>ASCII digits</span>, representing the fractional part of <var
+ data-x="">second</var></li>
+
+ </ol>
+
+ </li>
+
+ </ol>
+
+ </li>
+
+ </ol>
+
+ <p class="note">The <var data-x="">second</var> component cannot be 60 or 61; leap seconds cannot
+ be represented.</p>
+
+ <div class="nodev">
+
+ <p>The rules to <dfn>parse a time string</dfn> are as follows. This will return either a time, or
+ nothing. If at any point the algorithm says that it "fails", this means that it is aborted at that
+ point and returns nothing.</p>
+
+ <ol>
+
+ <li><p>Let <var data-x="">input</var> be the string being parsed.</p></li>
+
+ <li><p>Let <var data-x="">position</var> be a pointer into <var data-x="">input</var>, initially
+ pointing at the start of the string.</p></li>
+
+ <li><p><span>Parse a time component</span> to obtain <var data-x="">hour</var>, <var
+ data-x="">minute</var>, and <var data-x="">second</var>. If this returns nothing, then fail.</p>
+
+ <li><p>If <var data-x="">position</var> is <em>not</em> beyond the end of <var
+ data-x="">input</var>, then fail.</p></li>
+
+ <li><p>Let <var data-x="">time</var> be the time with hour <var data-x="">hour</var>, minute <var
+ data-x="">minute</var>, and second <var data-x="">second</var>.</p></li>
+
+ <li><p>Return <var data-x="">time</var>.</p></li>
+
+ </ol>
+
+ <p>The rules to <dfn>parse a time component</dfn>, given an <var data-x="">input</var> string and a
+ <var data-x="">position</var>, are as follows. This will return either an hour, a minute, and a
+ second, or nothing. If at any point the algorithm says that it "fails", this means that it is
+ aborted at that point and returns nothing.</p>
+
+ <ol>
+
+ <li><p><span>Collect a sequence of characters</span> that are <span>ASCII digits</span>. If the
+ collected sequence is not exactly two characters long, then fail. Otherwise, interpret the
+ resulting sequence as a base-ten integer. Let that number be the <var
+ data-x="">hour</var>.</p></li>
+
+ <li>If <var data-x="">hour</var> is not a number in the range 0&nbsp;&le;&nbsp;<var
+ data-x="">hour</var>&nbsp;&le;&nbsp;23, then fail.</li>
+
+ <li><p>If <var data-x="">position</var> is beyond the end of <var data-x="">input</var> or if the
+ character at <var data-x="">position</var> is not a U+003A COLON character, then fail. Otherwise,
+ move <var data-x="">position</var> forwards one character.</p></li>
+
+ <li><p><span>Collect a sequence of characters</span> that are <span>ASCII digits</span>. If the
+ collected sequence is not exactly two characters long, then fail. Otherwise, interpret the
+ resulting sequence as a base-ten integer. Let that number be the <var
+ data-x="">minute</var>.</p></li>
+
+ <li>If <var data-x="">minute</var> is not a number in the range 0&nbsp;&le;&nbsp;<var
+ data-x="">minute</var>&nbsp;&le;&nbsp;59, then fail.</li>
+
+ <li><p>Let <var data-x="">second</var> be a string with the value "0".</p></li>
+
+ <li>
+
+ <p>If <var data-x="">position</var> is not beyond the end of <var data-x="">input</var> and the
+ character at <var data-x="">position</var> is a U+003A COLON, then run these substeps:</p>
+
+ <ol>
+
+ <li><p>Advance <var data-x="">position</var> to the next character in <var
+ data-x="">input</var>.</p></li>
+
+ <li><p>If <var data-x="">position</var> is beyond the end of <var data-x="">input</var>, or at
+ the last character in <var data-x="">input</var>, or if the next <em>two</em> characters in <var
+ data-x="">input</var> starting at <var data-x="">position</var> are not both <span>ASCII
+ digits</span>, then fail.</p></li>
+
+ <li><p><span>Collect a sequence of characters</span> that are either <span>ASCII digits</span>
+ or U+002E FULL STOP characters. If the collected sequence is three characters long, or if it is
+ longer than three characters long and the third character is not a U+002E FULL STOP character,
+ or if it has more than one U+002E FULL STOP character, then fail. Otherwise, let the collected
+ string be <var data-x="">second</var> instead of its previous value.</p></li>
+
+ </ol>
+
+ </li>
+
+ <li><p>Interpret <var data-x="">second</var> as a base-ten number (possibly with a fractional
+ part). Let <var data-x="">second</var> be that number instead of the string version.</p></li>
+
+ <li><p>If <var data-x="">second</var> is not a number in the range 0&nbsp;&le;&nbsp;<var
+ data-x="">second</var>&nbsp;&lt;&nbsp;60, then fail.</p></li>
+
+ <li><p>Return <var data-x="">hour</var>, <var data-x="">minute</var>, and <var
+ data-x="">second</var>.</p></li>
+
+ </ol>
+
+ </div>
+
+
+ <h5>Local dates and times</h5>
+
+ <p>A <dfn data-x="concept-datetime-local">local date and time</dfn> consists of a specific
+ <span>proleptic-Gregorian date</span>, consisting of a year, a month, and a day, and a time,
+ consisting of an hour, a minute, a second, and a fraction of a second, but expressed without a
+ time zone. <a href="#refsGREGORIAN">[GREGORIAN]</a></p>
+
+ <p>A string is a <dfn>valid local date and time string</dfn> representing a date and time if it
+ consists of the following components in the given order:</p>
+
+ <ol>
+
+ <li>A <span>valid date string</span> representing the date</li>
+
+ <li>A U+0054 LATIN CAPITAL LETTER T character (T) or a U+0020 SPACE character</li>
+
+ <li>A <span>valid time string</span> representing the time</li>
+
+ </ol>
+
+ <p>A string is a <dfn>valid normalised local date and time string</dfn> representing a date and
+ time if it consists of the following components in the given order:</p>
+
+ <ol>
+
+ <li>A <span>valid date string</span> representing the date</li>
+
+ <li>A U+0054 LATIN CAPITAL LETTER T character (T)</li>
+
+ <li>A <span>valid time string</span> representing the time, expressed as the shortest possible
+ string for the given time (e.g. omitting the seconds component entirely if the given time is zero
+ seconds past the minute)</li>
+
+ </ol>
+
+ <div class="nodev">
+
+ <p>The rules to <dfn>parse a local date and time string</dfn> are as follows. This will return
+ either a date and time, or nothing. If at any point the algorithm says that it "fails", this means
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/LICENSE.md b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/LICENSE.md
new file mode 100644
index 0000000000..ad4858c874
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/LICENSE.md
@@ -0,0 +1,11 @@
+# The 3-Clause BSD License
+
+Copyright 2019 web-platform-tests contributors
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/README.md b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/README.md
new file mode 100644
index 0000000000..61b656941b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/README.md
@@ -0,0 +1,52 @@
+This directory contains a number of tests from
+[web-platform-tests](https://github.com/web-platform-tests/wpt) at
+77585330fd7da01392aec01cf5fed7aa22597180, chosen from the files processed by the manifest script.
+
+These files are split into two directories:
+
+ * `weighted`, a set of 15 tests curated from a random weighted sample of 30, weighted by parse
+ time as of html5lib 1.0.1. The curation was performed primarily as many of the slowest files are
+ very similar and therefore provide little extra coverage while it is relatively probable both
+ with be chosen. This provides a set of files which significantly contribute to the manifest
+ generation time.
+
+ * `random`, a further set of 15 tests, this time a random unweighted sample of 15. This provides a
+ set of files much closer to the average file in WPT.
+
+The files are sourced from the following:
+
+`weighted`:
+
+ * `css/compositing/test-plan/test-plan.src.html`
+ * `css/css-flexbox/align-content-wrap-002.html`
+ * `css/css-grid/grid-definition/grid-auto-fill-rows-001.html`
+ * `css/css-grid/masonry.tentative/masonry-item-placement-006.html`
+ * `css/css-images/image-orientation/reference/image-orientation-from-image-content-images-ref.html`
+ * `css/css-position/position-sticky-table-th-bottom-ref.html`
+ * `css/css-text/white-space/pre-float-001.html`
+ * `css/css-ui/resize-004.html`
+ * `css/css-will-change/will-change-abspos-cb-001.html`
+ * `css/filter-effects/filter-turbulence-invalid-001.html`
+ * `css/vendor-imports/mozilla/mozilla-central-reftests/css21/pagination/moz-css21-table-page-break-inside-avoid-2.html`
+ * `encoding/legacy-mb-tchinese/big5/big5_chars_extra.html`
+ * `html/canvas/element/compositing/2d.composite.image.destination-over.html`
+ * `html/semantics/embedded-content/the-canvas-element/toBlob.png.html`
+ * `referrer-policy/4K-1/gen/top.http-rp/unsafe-url/fetch.http.html`
+
+`random`:
+
+ * `content-security-policy/frame-ancestors/frame-ancestors-self-allow.html`
+ * `css/css-backgrounds/reference/background-origin-007-ref.html`
+ * `css/css-fonts/idlharness.html`
+ * `css/css-position/static-position/htb-ltr-ltr.html`
+ * `css/vendor-imports/mozilla/mozilla-central-reftests/css21/pagination/moz-css21-float-page-break-inside-avoid-6.html`
+ * `css/vendor-imports/mozilla/mozilla-central-reftests/shapes1/shape-outside-content-box-002.html`
+ * `encoding/legacy-mb-korean/euc-kr/euckr-encode-form.html`
+ * `html/browsers/browsing-the-web/unloading-documents/beforeunload-on-history-back-1.html`
+ * `html/browsers/the-window-object/apis-for-creating-and-navigating-browsing-contexts-by-name/non_automated/001.html`
+ * `html/editing/dnd/overlay/heavy-styling-005.html`
+ * `html/rendering/non-replaced-elements/lists/li-type-unsupported-ref.html`
+ * `html/semantics/grouping-content/the-dl-element/grouping-dl.html`
+ * `trusted-types/worker-constructor.https.html`
+ * `webvtt/rendering/cues-with-video/processing-model/selectors/cue/background_shorthand_css_relative_url.html`
+ * `IndexedDB/idbindex_get8.htm`
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/001.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/001.html
new file mode 100644
index 0000000000..7b0f21ec04
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/001.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<title>Accessing named windows from outside the unit of related browsing contexts</title>
+<a href="001-1.html" target="test_name">Click here</a>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/background-origin-007-ref.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/background-origin-007-ref.html
new file mode 100644
index 0000000000..d3a1d05328
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/background-origin-007-ref.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Backgrounds and Borders Reference</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<style>
+ div {
+ background-color: green;
+ height: 55px;
+ left: 5px;
+ position: relative;
+ top: 5px;
+ width: 55px;
+ }
+</style>
+<body>
+ <p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+ <div></div>
+</body>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/background_shorthand_css_relative_url.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/background_shorthand_css_relative_url.html
new file mode 100644
index 0000000000..2397fec005
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/background_shorthand_css_relative_url.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>WebVTT rendering, ::cue, background shorthand, background image URL with relative path from CSS file</title>
+<link rel="match" href="background_shorthand_css_relative_url-ref.html">
+<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" />
+<style>
+html { overflow:hidden }
+body { margin:0 }
+::cue {
+ font-family: Ahem, sans-serif;
+ background: #0f0 url('../../media/background.gif') repeat-x top left;
+ color: green;
+}
+</style>
+<script src="/common/reftest-wait.js"></script>
+<video width="320" height="180" autoplay onplaying="this.onplaying = null; this.pause(); takeScreenshot();">
+ <source src="/media/white.webm" type="video/webm">
+ <source src="/media/white.mp4" type="video/mp4">
+ <track src="../../support/test.vtt">
+ <script>
+ document.getElementsByTagName('track')[0].track.mode = 'showing';
+ </script>
+</video>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/beforeunload-on-history-back-1.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/beforeunload-on-history-back-1.html
new file mode 100644
index 0000000000..4403cfa8e9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/beforeunload-on-history-back-1.html
@@ -0,0 +1,5 @@
+<!doctype html>
+001-1
+<script>
+addEventListener("beforeunload", function() {top.t.step(function() {top.beforeunload_fired = true})}, false);
+</script>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/euckr-encode-form.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/euckr-encode-form.html
new file mode 100644
index 0000000000..545f8ac93f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/euckr-encode-form.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="euc-kr"> <!-- test breaks if the server overrides this -->
+<title>EUC-KR encoding (form)</title>
+<meta name="timeout" content="long">
+<meta name="variant" content="?1-1000">
+<meta name="variant" content="?1001-2000">
+<meta name="variant" content="?2001-3000">
+<meta name="variant" content="?3001-4000">
+<meta name="variant" content="?4001-5000">
+<meta name="variant" content="?5001-6000">
+<meta name="variant" content="?6001-7000">
+<meta name="variant" content="?7001-8000">
+<meta name="variant" content="?8001-9000">
+<meta name="variant" content="?9001-10000">
+<meta name="variant" content="?10001-11000">
+<meta name="variant" content="?11001-12000">
+<meta name="variant" content="?12001-13000">
+<meta name="variant" content="?13001-14000">
+<meta name="variant" content="?14001-15000">
+<meta name="variant" content="?15001-16000">
+<meta name="variant" content="?16001-17000">
+<meta name="variant" content="?17001-last">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/subset-tests.js"></script>
+<script src="euckr_index.js"></script>
+<script src="euckr-encoder.js"></script>
+<link rel="author" title="Richard Ishida" href="mailto:ishida@w3.org">
+<link rel="help" href="https://encoding.spec.whatwg.org/#euc-kr">
+<meta name="assert" content="The browser produces the expected byte sequences for all characters in the euc-kr encoding after 0x9F when encoding bytes for a URL produced by a form, using the encoder steps in the specification.">
+<style>
+ iframe { display:none }
+ form { display:none }
+</style>
+</head>
+<body>
+<div id="log"></div>
+<script src="../../resources/ranges.js"></script>
+<script>
+var errors = false;
+var encoder = euckrEncoder;
+var ranges = rangesAll;
+var separator = ",";
+function expect(result, codepoint) {
+ return "%" + result.replace(/ /g, "%");
+}
+</script>
+<script src="../../resources/encode-form-common.js"></script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/frame-ancestors-self-allow.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/frame-ancestors-self-allow.html
new file mode 100644
index 0000000000..a8a295dfc4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/frame-ancestors-self-allow.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="support/frame-ancestors-test.sub.js"></script>
+</head>
+<body>
+ <script>
+ test = async_test("A 'frame-ancestors' CSP directive with a value 'self' should allow rendering.");
+
+ sameOriginFrameShouldBeAllowed("'self'");
+ </script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/grouping-dl.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/grouping-dl.html
new file mode 100644
index 0000000000..2394d6a929
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/grouping-dl.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>the dl element</title>
+ <link rel="author" title="dzenana" href="mailto:dzenana.trenutak@gmail.com">
+ <link rel="help" href="https://html.spec.whatwg.org/multipage/#the-dl-element">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script>
+ "use strict";
+
+ // check that prototype matches spec's DOM interface
+ test(function () {
+ var testElement = document.createElement("dl");
+ assert_equals(Object.getPrototypeOf(testElement), HTMLDListElement.prototype, "HTMLDListElement.prototype should be used for dl");
+ }, "The prototype for dl is HTMLDListElement.prototype");
+
+ // Not checking: effects of markup on defining groups and the name-pair values within those groups
+
+ </script>
+</head>
+<body>
+ <h1>Description</h1>
+ <p>This test validates the dl element.</p>
+
+ <div id="log"></div>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/heavy-styling-005.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/heavy-styling-005.html
new file mode 100644
index 0000000000..2bbdb3cf73
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/heavy-styling-005.html
@@ -0,0 +1,15 @@
+<!DOCTYPe html>
+<meta charset='utf-8'>
+<title>drag and drop – feedback overlay for heavily styled elements – 005</title>
+<style>
+a {
+ display: block;
+ height: 200px;
+ width: 200px;
+ background-color: rgba(0,0,255,0.5);
+}
+</style>
+
+<p>Drag the blue box below downwards. The drag placeholder should resemble the blue box, including the text within it.</p>
+
+<a draggable="true" ondragstart="event.dataTransfer.effectAllowed ='copy'">TEST</a>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/htb-ltr-ltr.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/htb-ltr-ltr.html
new file mode 100644
index 0000000000..5a19c0e9cc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/htb-ltr-ltr.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css2/visudet.html#abs-non-replaced-width" />
+<link rel="match" href="htb-ref.html">
+<meta name="assert" content="This test checks the static position of an out of flow absolute positioned element, under various conditions." />
+<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" />
+<style>
+.container {
+ position: relative;
+ background: green;
+ color: green;
+ font: 16px/1 Ahem;
+ border: solid black 3px;
+ width: 400px;
+ margin: 16px 0;
+}
+.red { color: red; }
+.cb { position: relative; }
+.rtl { direction: rtl; }
+.ltr { direction: ltr; }
+.inline { display: inline; }
+.abs { position: absolute; }
+
+.indent { text-indent: 20px; }
+* { text-indent: initial; }
+</style>
+
+There should be no red.
+<div class="container ltr">
+ XXX<span class="ltr">XX<div class="abs inline">XXXXX</div><span class="red">XXXXX</span></span>
+</div>
+
+<div class="container ltr indent">
+ XXX<span class="ltr">XX<div class="abs inline">XXXXX</div><span class="red">XXXXX</span></span>
+</div>
+
+<div class="container ltr">
+ XXX<span class="ltr">XX<div class="abs block">XXXXX</div><br><span class="red">XXXXX</span></span>
+</div>
+
+<div class="container ltr indent">
+ XXX<span class="ltr">XX<div class="abs block">XXXXX</div><br><span class="red">XXXXX</span></span>
+</div>
+
+<div class="container ltr">
+ XXX<span class="ltr cb">XX<div class="abs inline">XXXXX</div><span class="red">XXXXX</span></span>
+</div>
+
+<div class="container ltr indent">
+ XXX<span class="ltr cb">XX<div class="abs inline">XXXXX</div><span class="red">XXXXX</span></span>
+</div>
+
+<div class="container ltr">
+ XXX<span class="ltr cb">XX<div class="abs block">XXXXX</div><br><span class="red">XXXXX</span></span>
+</div>
+
+<div class="container ltr indent">
+ XXX<span class="ltr cb">XX<div class="abs block">XXXXX</div><br><span class="red">XXXXX</span></span>
+</div>
+
+<div class="container ltr">
+ <span class="cb">XXX<span class="ltr">XX<div class="abs inline">XXXXX</div><span class="red">XXXXX</span></span></span>
+</div>
+
+<div class="container ltr indent">
+ <span class="cb">XXX<span class="ltr">XX<div class="abs inline">XXXXX</div><span class="red">XXXXX</span></span></span>
+</div>
+
+<div class="container ltr">
+ <span class="cb">XXX<span class="ltr">XX<div class="abs block">XXXXX</div><br><span class="red">XXXXX</span></span></span>
+</div>
+
+<div class="container ltr indent">
+ <span class="cb">XXX<span class="ltr">XX<div class="abs block">XXXXX</div><br><span class="red">XXXXX</span></span></span>
+</div>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/idbindex_get8.htm b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/idbindex_get8.htm
new file mode 100644
index 0000000000..9bfc48422f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/idbindex_get8.htm
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>IDBIndex.get() - throw InvalidStateError on index deleted by aborted upgrade</title>
+<link rel="help" href="https://w3c.github.io/IndexedDB/#dom-idb">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=support.js></script>
+<div id="log"></div>
+<script>
+ var db,
+ t = async_test();
+
+ var open_rq = createdb(t);
+ open_rq.onupgradeneeded = function(e) {
+ db = e.target.result;
+ var store = db.createObjectStore("store", { keyPath: "key" });
+ var index = store.createIndex("index", "indexedProperty");
+ store.add({ key: 1, indexedProperty: "data" });
+
+ e.target.transaction.abort();
+
+ assert_throws_dom("InvalidStateError", function(){
+ index.get("data");
+ });
+ t.done();
+ }
+</script>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/idlharness.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/idlharness.html
new file mode 100644
index 0000000000..ecc601bcf6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/idlharness.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<title>CSS Fonts IDL tests</title>
+<link rel="help" href="https://drafts.csswg.org/css-fonts-4/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+
+<style>
+ div { display: block; }
+</style>
+<style>
+ @font-face {
+ font-family: fwf;
+ src: url(support/fonts/FontWithFancyFeatures.otf);
+ }
+</style>
+
+<script>
+ "use strict";
+
+ idl_test(
+ ["css-fonts"],
+ ["cssom"],
+ idl_array => {
+ idl_array.add_objects({
+ CSSRule: ['cssRule'],
+ CSSFontFaceRule: ['cssFontFaceRule'],
+ });
+ self.cssRule = document.styleSheets[0].cssRules[0];
+ self.cssFontFaceRule = document.styleSheets[1].cssRules[0];
+ }
+ );
+</script>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/li-type-unsupported-ref.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/li-type-unsupported-ref.html
new file mode 100644
index 0000000000..4fbc5aca97
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/li-type-unsupported-ref.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>li@type: unsupported types</title>
+<li>first item</li>
+<li>second item</li>
+<ol>
+ <li>first ordered item</li>
+ <li>second ordered item</li>
+</ol>
+<ul>
+ <li>first unordered item</li>
+ <li>second unordered item</li>
+</ul>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/moz-css21-float-page-break-inside-avoid-6.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/moz-css21-float-page-break-inside-avoid-6.html
new file mode 100644
index 0000000000..3cd0a5fb1c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/moz-css21-float-page-break-inside-avoid-6.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="en-US" class="reftest-paged">
+<head>
+ <title>CSS Test: CSS 2.1 page-break-inside:avoid</title>
+ <link rel="author" title="Mats Palmgren" href="https://bugzilla.mozilla.org/show_bug.cgi?id=685012">
+ <link rel="help" href="http://www.w3.org/TR/CSS21/page.html#propdef-page-break-inside">
+ <link rel="match" href="moz-css21-float-page-break-inside-avoid-6-ref.html">
+ <meta name="flags" content="paged">
+<style type="text/css">
+@page { size:5in 3in; margin:0.5in; }
+html,body {
+ color:black; background-color:white; font-size:16px; padding:0; margin:0; height:100%;
+}
+p { height:60%; width:90%; margin:0; background-color:blue; border:1px solid black; }
+.test { page-break-inside:avoid; float:left; }
+</style>
+</head>
+<body><p>1</p><p class="test">2</p></body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/shape-outside-content-box-002.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/shape-outside-content-box-002.html
new file mode 100644
index 0000000000..e2040763df
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/shape-outside-content-box-002.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+ <title>CSS Shape Test: float right, content-box</title>
+ <link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+ <link rel="author" title="Mozilla" href="http://www.mozilla.org/">
+ <link rel="help" href="https://drafts.csswg.org/css-shapes-1/#shapes-from-box-values">
+ <link rel="match" href="shape-outside-content-box-002-ref.html">
+ <meta name="flags" content="">
+ <meta name="assert" content="Test the boxes are wrapping around the right float shape defined by the content-box value.">
+ <style>
+ .container {
+ direction: rtl;
+ width: 175px;
+ line-height: 0;
+ }
+
+ .shape {
+ float: right;
+ shape-outside: content-box;
+ box-sizing: content-box;
+ height: 25px;
+ width: 25px;
+ padding: 25px;
+ border: 25px solid lightgreen;
+ margin: 25px;
+ background-color: orange;
+ }
+
+ .box {
+ display: inline-block;
+ width: 50px;
+ height: 25px;
+ background-color: blue;
+ }
+
+ .longbox {
+ display: inline-block;
+ width: 175px;
+ height: 25px;
+ background-color: blue;
+ }
+ </style>
+
+ <main class="container">
+ <div class="shape"></div>
+ <div class="shape"></div>
+ <div class="longbox"></div> <!-- Saturate the margin space -->
+ <div class="longbox"></div> <!-- Saturate the border space -->
+ <div class="longbox"></div> <!-- Saturate the padding space -->
+ <div class="box"></div>
+ <div class="longbox"></div> <!-- Saturate the padding space -->
+ <div class="longbox"></div> <!-- Saturate the border space -->
+ <div class="longbox"></div> <!-- Saturate the margin space -->
+
+ <div class="longbox"></div> <!-- Saturate the margin space -->
+ <div class="longbox"></div> <!-- Saturate the border space -->
+ <div class="longbox"></div> <!-- Saturate the padding space -->
+ <div class="box"></div>
+ <div class="longbox"></div> <!-- Saturate the padding space -->
+ <div class="longbox"></div> <!-- Saturate the border space -->
+ <div class="longbox"></div> <!-- Saturate the margin space -->
+ </main>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/worker-constructor.https.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/worker-constructor.https.html
new file mode 100644
index 0000000000..6e127b11a5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/random/worker-constructor.https.html
@@ -0,0 +1,86 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script';">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+
+const test_url = "support/WorkerGlobalScope-importScripts.https.js"
+const trusted_url = trustedTypes.createPolicy("anythinggoes", {
+ createScriptURL: x => x}).createScriptURL(test_url);
+const default_url = "support/WorkerGlobalScope-importScripts.potato.js"
+
+async function service_worker(url) {
+ if (!('serviceWorker' in navigator)) return Promise.resolve();
+
+ const scope = 'support/some/scope/for/this/test';
+ const reg = await navigator.serviceWorker.getRegistration(scope);
+ if (reg) await reg.unregister();
+ return await navigator.serviceWorker.register(url, {scope});
+}
+
+// Most tests below don't need promises, but the ones related to
+// ServiceWorkers do. Since we can't mix promise and non-promise tests,
+// we'll just run the non-promise tests in the main function and return
+// an empty-resolved promise for those.
+// Since an active default policy will affect all subsequent DOM operations,
+// we're wrapping policy creation in a promise_test. Together, this will
+// force proper serialization of all tests.
+//
+// Generally, we don't actually care what the workers here do, we'll merely
+// check whether creation succeeds.
+
+promise_test(t => {
+ new Worker(trusted_url);
+ return Promise.resolve();
+}, "Create Worker via ScriptTestUrl");
+
+promise_test(t => {
+ new SharedWorker(trusted_url);
+ return Promise.resolve();
+}, "Create SharedWorker via ScriptTestUrl");
+
+promise_test(t => {
+ return service_worker(trusted_url);
+}, "Create ServiceWorker via ScriptTestUrl");
+
+promise_test(t => {
+ assert_throws_js(TypeError, () => new Worker(test_url));
+ return Promise.resolve();
+}, "Block Worker creation via string");
+
+promise_test(t => {
+ assert_throws_js(TypeError, () => new SharedWorker(test_url));
+ return Promise.resolve();
+}, "Block SharedWorker creation via string");
+
+promise_test(t => {
+ return promise_rejects_js(t, TypeError, service_worker(test_url));
+}, "Block ServiceWorker creation via String");
+
+// Tests with default policy.
+promise_test(t => {
+ trustedTypes.createPolicy("default", {
+ createScriptURL: s => s.replace("potato", "https") });
+ return Promise.resolve();
+}, "Setup default policy.");
+
+promise_test(t => {
+ new Worker(default_url);
+ return Promise.resolve();
+}, "Create Worker via string with default policy.");
+
+promise_test(t => {
+ new SharedWorker(default_url);
+ return Promise.resolve();
+}, "Create SharedWorker via string with default policy.");
+
+promise_test(t => {
+ return service_worker(default_url);
+}, "Create ServiceWorker via string with default policy.");
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/2d.composite.image.destination-over.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/2d.composite.image.destination-over.html
new file mode 100644
index 0000000000..d742f84dfb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/2d.composite.image.destination-over.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.composite.image.destination-over</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/html/canvas/resources/canvas-tests.js"></script>
+<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
+<body class="show_output">
+
+<h1>2d.composite.image.destination-over</h1>
+<p class="desc"></p>
+
+
+<p class="output">Actual output:</p>
+<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
+<p class="output expectedtext">Expected output:<p><img src="2d.composite.image.destination-over.png" class="output expected" id="expected" alt="">
+<ul id="d"></ul>
+<script>
+var t = async_test("");
+_addTest(function(canvas, ctx) {
+
+
+ctx.fillStyle = 'rgba(0, 255, 255, 0.5)';
+ctx.fillRect(0, 0, 100, 50);
+ctx.globalCompositeOperation = 'destination-over';
+ctx.drawImage(document.getElementById('yellow75.png'), 0, 0);
+_assertPixelApprox(canvas, 50,25, 109,255,146,223, "50,25", "109,255,146,223", 5);
+
+
+});
+</script>
+<img src="/images/yellow75.png" id="yellow75.png" class="resource">
+
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/align-content-wrap-002.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/align-content-wrap-002.html
new file mode 100644
index 0000000000..a15f7ea844
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/align-content-wrap-002.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-flexbox/#propdef-align-content" />
+<title>css-flexbox: Tests align-content with flex-wrap: wrap</title>
+<style>
+.flex-horizontal {
+ width:600px;
+ display:flex;
+ height:100px;
+ background:gray;
+ margin-bottom:100px;
+}
+.flex-vertical {
+ width:100px;
+ display:flex;
+ flex-direction: column;
+ height:600px;
+ background:gray;
+ margin-top:200px;
+ margin-bottom:100px;
+}
+.item-horizontal {
+ width:150px;
+ background:yellow;
+ margin:10px;
+ flex:none;
+}
+.item-vertical {
+ height:150px;
+ background:yellow;
+ margin:10px;
+ flex:none;
+}
+.content1-horizontal {
+ width:100px;
+ height:150px;
+ background:red;
+}
+.content2-horizontal {
+ width:100px;
+ height:100px;
+ background:red;
+}
+.content3-horizontal {
+ width:100px;
+ height:50px;
+ background:red;
+}
+.content1-vertical {
+ width:150px;
+ height:100px;
+ background:red;
+}
+.content2-vertical {
+ width:100px;
+ height:100px;
+ background:red;
+}
+.content3-vertical {
+ width:50px;
+ height:100px;
+ background:red;
+}
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/check-layout-th.js"></script>
+<body onload="checkLayout('.flex-horizontal, .flex-vertical');">
+<div id=log></div>
+<p>Test for crbug.com/362848: Flex box word-wrap is not adhering to spec</p>
+<div class="flex-horizontal">
+ <div class="item-horizontal" data-expected-height="80"><div class="content1-horizontal"></div></div>
+ <div class="item-horizontal" data-expected-height="80"><div class="content2-horizontal"></div></div>
+ <div class="item-horizontal" data-expected-height="80"><div class="content3-horizontal"></div></div>
+</div>
+
+<div class="flex-horizontal" style="flex-wrap:wrap;">
+ <div class="item-horizontal" data-expected-height="150"><div class="content1-horizontal"></div></div>
+ <div class="item-horizontal" data-expected-height="150"><div class="content2-horizontal"></div></div>
+ <div class="item-horizontal" data-expected-height="150"><div class="content3-horizontal"></div></div>
+</div>
+
+<div class="flex-horizontal" style="flex-wrap:wrap;">
+ <div class="item-horizontal" data-expected-height="150"><div class="content1-horizontal"></div></div>
+ <div class="item-horizontal" data-expected-height="150"><div class="content2-horizontal"></div></div>
+ <div class="item-horizontal" data-expected-height="150"><div class="content3-horizontal"></div></div>
+ <div class="item-horizontal" data-expected-height="150"><div class="content1-horizontal"></div></div>
+ <div class="item-horizontal" data-expected-height="150"><div class="content2-horizontal"></div></div>
+</div>
+
+<div class="flex-vertical">
+ <div class="item-vertical" data-expected-width="80"><div class="content1-vertical"></div></div>
+ <div class="item-vertical" data-expected-width="80"><div class="content2-vertical"></div></div>
+ <div class="item-vertical" data-expected-width="80"><div class="content3-vertical"></div></div>
+</div>
+
+<div class="flex-vertical" style="flex-wrap:wrap;">
+ <div class="item-vertical" data-expected-width="150"><div class="content1-vertical"></div></div>
+ <div class="item-vertical" data-expected-width="150"><div class="content2-vertical"></div></div>
+ <div class="item-vertical" data-expected-width="150"><div class="content3-vertical"></div></div>
+</div>
+
+<div class="flex-vertical" style="flex-wrap:wrap;">
+ <div class="item-vertical" data-expected-width="150"><div class="content1-vertical"></div></div>
+ <div class="item-vertical" data-expected-width="150"><div class="content2-vertical"></div></div>
+ <div class="item-vertical" data-expected-width="150"><div class="content3-vertical"></div></div>
+ <div class="item-vertical" data-expected-width="150"><div class="content1-vertical"></div></div>
+ <div class="item-vertical" data-expected-width="150"><div class="content2-vertical"></div></div>
+</div>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/big5_chars_extra.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/big5_chars_extra.html
new file mode 100644
index 0000000000..5ea8e5740d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/big5_chars_extra.html
@@ -0,0 +1 @@
+<!doctype html><html><head><meta charset="big5"><title>big5 characters</title></head><body><span data-cp="C0" data-bytes="88 59">Y</span> <span data-cp="C1" data-bytes="88 57">W</span> <span data-cp="C8" data-bytes="88 5D">]</span> <span data-cp="C9" data-bytes="88 5B">[</span> <span data-cp="CA" data-bytes="88 66">f</span> <span data-cp="D2" data-bytes="88 61">a</span> <span data-cp="D3" data-bytes="88 5F">_</span> <span data-cp="E0" data-bytes="88 6A">j</span> <span data-cp="E1" data-bytes="88 68">h</span> <span data-cp="E8" data-bytes="88 6F">o</span> <span data-cp="E9" data-bytes="88 6D">m</span> <span data-cp="EA" data-bytes="88 A7"></span> <span data-cp="EC" data-bytes="88 73">s</span> <span data-cp="ED" data-bytes="88 71">q</span> <span data-cp="F2" data-bytes="88 77">w</span> <span data-cp="F3" data-bytes="88 75">u</span> <span data-cp="F9" data-bytes="88 7B">{</span> <span data-cp="FA" data-bytes="88 79">y</span> <span data-cp="FC" data-bytes="88 A2"></span> <span data-cp="100" data-bytes="88 56">V</span> <span data-cp="101" data-bytes="88 67">g</span> <span data-cp="112" data-bytes="88 5A">Z</span> <span data-cp="113" data-bytes="88 6C">l</span> <span data-cp="11A" data-bytes="88 5C">\</span> <span data-cp="11B" data-bytes="88 6E">n</span> <span data-cp="12B" data-bytes="88 70">p</span> <span data-cp="14C" data-bytes="88 5E">^</span> <span data-cp="14D" data-bytes="88 74">t</span> <span data-cp="16B" data-bytes="88 78">x</span> <span data-cp="1CD" data-bytes="88 58">X</span> <span data-cp="1CE" data-bytes="88 69">i</span> <span data-cp="1D0" data-bytes="88 72">r</span> <span data-cp="1D1" data-bytes="88 60">`</span> <span data-cp="1D2" data-bytes="88 76">v</span> <span data-cp="1D4" data-bytes="88 7A">z</span> <span data-cp="1D6" data-bytes="88 7C">|</span> <span data-cp="1D8" data-bytes="88 7D">}</span> <span data-cp="1DA" data-bytes="88 7E">~</span> <span data-cp="1DC" data-bytes="88 A1"></span> <span data-cp="251" data-bytes="88 6B">k</span> <span data-cp="261" data-bytes="88 A8"></span> <span data-cp="1EBE" data-bytes="88 63">c</span> <span data-cp="1EBF" data-bytes="88 A4"></span> <span data-cp="1EC0" data-bytes="88 65">e</span> <span data-cp="1EC1" data-bytes="88 A6"></span> <span data-cp="23DA" data-bytes="88 A9"></span> <span data-cp="23DB" data-bytes="88 AA"></span> <span data-cp="31C0" data-bytes="88 40">@</span> <span data-cp="31C1" data-bytes="88 41">A</span> <span data-cp="31C2" data-bytes="88 42">B</span> <span data-cp="31C3" data-bytes="88 43">C</span> <span data-cp="31C4" data-bytes="88 44">D</span> <span data-cp="31C5" data-bytes="88 46">F</span> <span data-cp="31C6" data-bytes="88 49">I</span> <span data-cp="31C7" data-bytes="88 4A">J</span> <span data-cp="31C8" data-bytes="88 4D">M</span> <span data-cp="31C9" data-bytes="88 4F">O</span> <span data-cp="31CA" data-bytes="88 50">P</span> <span data-cp="31CB" data-bytes="88 51">Q</span> <span data-cp="31CC" data-bytes="88 52">R</span> <span data-cp="31CD" data-bytes="88 54">T</span> <span data-cp="31CE" data-bytes="88 55">U</span> <span data-cp="3435" data-bytes="92 77">w</span> <span data-cp="3440" data-bytes="96 DF"></span> <span data-cp="344A" data-bytes="8C F4"></span> <span data-cp="344C" data-bytes="89 D5"></span> <span data-cp="3464" data-bytes="93 CD"></span> <span data-cp="3473" data-bytes="9B DF"></span> <span data-cp="347D" data-bytes="89 DA"></span> <span data-cp="347E" data-bytes="8F 59">Y</span> <span data-cp="3493" data-bytes="89 DB"></span> <span data-cp="3496" data-bytes="8F 5D">]</span> <span data-cp="34A5" data-bytes="89 DC"></span> <span data-cp="34AF" data-bytes="96 F7"></span> <span data-cp="34BC" data-bytes="8A DA"></span> <span data-cp="34C1" data-bytes="8B DC"></span> <span data-cp="34C8" data-bytes="97 DB"></span> <span data-cp="34DF" data-bytes="9E 53">S</span> <span data-cp="34E4" data-bytes="9D AA"></span> <span data-cp="34E6" data-bytes="87 BE"></span> <span data-cp="34FB" data-bytes="9B EA"></span> <span data-cp="3506" data-bytes="8A 6E">n</span> <span data-cp="353E" data-bytes="8B C8"></span> <span data-cp="3551" data-bytes="89 E8"></span> <span data-cp="3553" data-bytes="89 EA"></span> <span data-cp="3559" data-bytes="8C 4B">K</span> <span data-cp="356D" data-bytes="89 ED"></span> <span data-cp="3570" data-bytes="94 DD"></span> <span data-cp="3572" data-bytes="89 EE"></span> <span data-cp="3577" data-bytes="9E B4"></span> <span data-cp="3578" data-bytes="8A D3"></span> <span data-cp="3584" data-bytes="92 DB"></span> <span data-cp="3597" data-bytes="94 DB"></span> <span data-cp="3598" data-bytes="89 F9"></span> <span data-cp="35A5" data-bytes="89 FB"></span> <span data-cp="35AD" data-bytes="9E FC"></span> <span data-cp="35BF" data-bytes="89 FC"></span> <span data-cp="35C1" data-bytes="89 BF"></span> <span data-cp="35C5" data-bytes="89 FE"></span> <span data-cp="35C7" data-bytes="89 E6"></span> <span data-cp="35CA" data-bytes="9D 46">F</span> <span data-cp="35CE" data-bytes="9D EE"></span> <span data-cp="35D2" data-bytes="A0 7E">~</span> <span data-cp="35D6" data-bytes="A0 68">h</span> <span data-cp="35DB" data-bytes="98 E9"></span> <span data-cp="35DD" data-bytes="8B 68">h</span> <span data-cp="35F1" data-bytes="8D FD"></span> <span data-cp="35F2" data-bytes="8B BE"></span> <span data-cp="35F3" data-bytes="9F D9"></span> <span data-cp="35FB" data-bytes="8A EB"></span> <span data-cp="35FE" data-bytes="9F D7"></span> <span data-cp="3609" data-bytes="8B 6A">j</span> <span data-cp="3618" data-bytes="9C 5C">\</span> <span data-cp="361A" data-bytes="8B B1"></span> <span data-cp="3625" data-bytes="87 70">p</span> <span data-cp="362D" data-bytes="9D F3"></span> <span data-cp="3635" data-bytes="A0 D0"></span> <span data-cp="363E" data-bytes="92 E9"></span> <span data-cp="3647" data-bytes="9A EC"></span> <span data-cp="3648" data-bytes="8F AB"></span> <span data-cp="364E" data-bytes="8E 45">E</span> <span data-cp="365F" data-bytes="9C 6F">o</span> <span data-cp="3661" data-bytes="8D 5C">\</span> <span data-cp="367A" data-bytes="9E DE"></span> <span data-cp="3681" data-bytes="89 EF"></span> <span data-cp="369A" data-bytes="96 E9"></span> <span data-cp="36A5" data-bytes="9E BB"></span> <span data-cp="36AA" data-bytes="94 DE"></span> <span data-cp="36AC" data-bytes="9E B8"></span> <span data-cp="36B0" data-bytes="97 BA"></span> <span data-cp="36B5" data-bytes="95 D6"></span> <span data-cp="36B9" data-bytes="9C BB"></span> <span data-cp="36BC" data-bytes="97 DA"></span> <span data-cp="36C1" data-bytes="8F 45">E</span> <span data-cp="36C4" data-bytes="91 58">X</span> <span data-cp="36C7" data-bytes="98 56">V</span> <span data-cp="36C8" data-bytes="9B 4D">M</span> <span data-cp="36D3" data-bytes="93 5B">[</span> <span data-cp="36D4" data-bytes="95 C7"></span> <span data-cp="36D6" data-bytes="97 E7"></span> <span data-cp="36DD" data-bytes="93 59">Y</span> <span data-cp="36E1" data-bytes="91 F5"></span> <span data-cp="36E2" data-bytes="97 B8"></span> <span data-cp="36F5" data-bytes="92 FA"></span> <span data-cp="3701" data-bytes="93 57">W</span> <span data-cp="3703" data-bytes="8B A6"></span> <span data-cp="370A" data-bytes="97 B0"></span> <span data-cp="371C" data-bytes="9C A1"></span> <span data-cp="3722" data-bytes="91 F2"></span> <span data-cp="3723" data-bytes="91 F9"></span> <span data-cp="3725" data-bytes="8F F1"></span> <span data-cp="372C" data-bytes="97 45">E</span> <span data-cp="372D" data-bytes="98 53">S</span> <span data-cp="3733" data-bytes="92 51">Q</span> <span data-cp="373A" data-bytes="9D AD"></span> <span data-cp="3762" data-bytes="9B C2"></span> <span data-cp="376F" data-bytes="9A 7B">{</span> <span data-cp="3797" data-bytes="8B 60">`</span> <span data-cp="37A0" data-bytes="93 4B">K</span> <span data-cp="37B9" data-bytes="9A BD"></span> <span data-cp="37BE" data-bytes="91 B7"></span> <span data-cp="37D6" data-bytes="8D 4B">K</span> <span data-cp="37F2" data-bytes="95 B4"></span> <span data-cp="37FB" data-bytes="9E F0"></span> <span data-cp="380F" data-bytes="8D 64">d</span> <span data-cp="3819" data-bytes="92 69">i</span> <span data-cp="3820" data-bytes="8D 67">g</span> <span data-cp="3838" data-bytes="8D 68">h</span> <span data-cp="3863" data-bytes="93 EB"></span> <span data-cp="3875" data-bytes="87 7A">z</span> <span data-cp="38C3" data-bytes="91 66">f</span> <span data-cp="38D1" data-bytes="93 DD"></span> <span data-cp="38D4" data-bytes="8D 52">R</span> <span data-cp="38FA" data-bytes="8B CC"></span> <span data-cp="3908" data-bytes="8D 6D">m</span> <span data-cp="3914" data-bytes="8D 6E">n</span> <span data-cp="3927" data-bytes="96 A8"></span> <span data-cp="393F" data-bytes="8D 6F">o</span> <span data-cp="394D" data-bytes="8D 70">p</span> <span data-cp="3978" data-bytes="8C F3"></span> <span data-cp="3980" data-bytes="90 60">`</span> <span data-cp="3989" data-bytes="8D 74">t</span> <span data-cp="398A" data-bytes="97 C3"></span> <span data-cp="3992" data-bytes="8A D0"></span> <span data-cp="3999" data-bytes="92 74">t</span> <span data-cp="399B" data-bytes="9B BE"></span> <span data-cp="39A1" data-bytes="9C C8"></span> <span data-cp="39A4" data-bytes="9C BA"></span> <span data-cp="39B8" data-bytes="8D 78">x</span> <span data-cp="39DC" data-bytes="9E B9"></span> <span data-cp="39E2" data-bytes="95 5A">Z</span> <span data-cp="39E5" data-bytes="91 B4"></span> <span data-cp="39EC" data-bytes="8A 48">H</span> <span data-cp="39F8" data-bytes="8D 7D">}</span> <span data-cp="39FB" data-bytes="8A 7D">}</span> <span data-cp="39FE" data-bytes="8A C2"></span> <span data-cp="3A03" data-bytes="8D A1"></span> <span data-cp="3A06" data-bytes="8A D1"></span> <span data-cp="3A18" data-bytes="8B 47">G</span> <span data-cp="3A29" data-bytes="93 A4"></span> <span data-cp="3A2A" data-bytes="9E DA"></span> <span data-cp="3A34" data-bytes="8A 51">Q</span> <span data-cp="3A4B" data-bytes="8D A6"></span> <span data-cp="3A52" data-bytes="9E C5"></span> <span data-cp="3A5C" data-bytes="A0 78">x</span> <span data-cp="3A5E" data-bytes="94 B5"></span> <span data-cp="3A67" data-bytes="8A 6B">k</span> <span data-cp="3A97" data-bytes="8D AB"></span> <span data-cp="3ABD" data-bytes="8D AD"></span> <span data-cp="3AE0" data-bytes="93 C1"></span> <span data-cp="3AF0" data-bytes="90 6F">o</span> <span data-cp="3AF2" data-bytes="8D B0"></span> <span data-cp="3AF5" data-bytes="87 A2"></span> <span data-cp="3AFB" data-bytes="94 7E">~</span> <span data-cp="3B0E" data-bytes="90 FA"></span> <span data-cp="3B19" data-bytes="94 79">y</span> <span data-cp="3B22" data-bytes="8D B2"></span> <span data-cp="3B39" data-bytes="99 7B">{</span> <span data-cp="3B42" data-bytes="8D B4"></span> <span data-cp="3B58" data-bytes="8D B7"></span> <span data-cp="3B60" data-bytes="91 B3"></span> <span data-cp="3B71" data-bytes="8D BB"></span> <span data-cp="3B72" data-bytes="8D BA"></span> <span data-cp="3B7B" data-bytes="8D BC"></span> <span data-cp="3B7C" data-bytes="90 44">D</span> <span data-cp="3B95" data-bytes="87 4B">K</span> <span data-cp="3B96" data-bytes="93 E4"></span> <span data-cp="3B99" data-bytes="93 E0"></span> <span data-cp="3BBC" data-bytes="8D C3"></span> <span data-cp="3BBE" data-bytes="9B B8"></span> <span data-cp="3BC4" data-bytes="93 E9"></span> <span data-cp="3BD7" data-bytes="93 F6"></span> <span data-cp="3BDD" data-bytes="8D C5"></span> <span data-cp="3BEC" data-bytes="8D CA"></span> <span data-cp="3BF2" data-bytes="8D CC"></span> <span data-cp="3BF4" data-bytes="93 B5"></span> <span data-cp="3C11" data-bytes="9C F8"></span> <span data-cp="3C15" data-bytes="92 52">R</span> <span data-cp="3C18" data-bytes="A0 E8"></span> <span data-cp="3C54" data-bytes="9C A5"></span> <span data-cp="3C8B" data-bytes="8C 56">V</span> <span data-cp="3CCB" data-bytes="8D D6"></span> <span data-cp="3CCD" data-bytes="97 C0"></span> <span data-cp="3CD1" data-bytes="A0 DE"></span> <span data-cp="3CD6" data-bytes="97 D2"></span> <span data-cp="3CEF" data-bytes="8D DB"></span> <span data-cp="3D12" data-bytes="8C EA"></span> <span data-cp="3D13" data-bytes="8E AF"></span> <span data-cp="3D1D" data-bytes="91 B5"></span> <span data-cp="3D46" data-bytes="8D EB"></span> <span data-cp="3D4C" data-bytes="97 C6"></span> <span data-cp="3D51" data-bytes="90 FC"></span> <span data-cp="3D62" data-bytes="96 D6"></span> <span data-cp="3D69" data-bytes="97 C5"></span> <span data-cp="3D6A" data-bytes="8D EF"></span> <span data-cp="3D6F" data-bytes="97 D7"></span> <span data-cp="3D75" data-bytes="8D F0"></span> <span data-cp="3D7D" data-bytes="96 A6"></span> <span data-cp="3D88" data-bytes="8C DF"></span> <span data-cp="3D8A" data-bytes="8D F3"></span> <span data-cp="3D8F" data-bytes="94 49">I</span> <span data-cp="3D91" data-bytes="8D F5"></span> <span data-cp="3DA5" data-bytes="98 72">r</span> <span data-cp="3DAD" data-bytes="8E 6B">k</span> <span data-cp="3DBF" data-bytes="8F 50">P</span> <span data-cp="3DC6" data-bytes="9D CC"></span> <span data-cp="3DC9" data-bytes="8C 44">D</span> <span data-cp="3DCC" data-bytes="99 6E">n</span> <span data-cp="3DCD" data-bytes="94 A1"></span> <span data-cp="3DD3" data-bytes="8F 63">c</span> <span data-cp="3DDB" data-bytes="A0 DA"></span> <span data-cp="3DE7" data-bytes="92 53">S</span> <span data-cp="3DEB" data-bytes="9D B5"></span> <span data-cp="3DF3" data-bytes="98 79">y</span> <span data-cp="3DF4" data-bytes="87 6A">j</span> <span data-cp="3DF7" data-bytes="9D 5D">]</span> <span data-cp="3DFC" data-bytes="8D 63">c</span> <span data-cp="3DFD" data-bytes="96 69">i</span> <span data-cp="3E06" data-bytes="9F 70">p</span> <span data-cp="3E43" data-bytes="8A C7"></span> <span data-cp="3E48" data-bytes="89 D7"></span> <span data-cp="3E74" data-bytes="9E DD"></span> <span data-cp="3EA9" data-bytes="98 BC"></span> <span data-cp="3EAD" data-bytes="95 B0"></span> <span data-cp="3EB1" data-bytes="94 64">d</span> <span data-cp="3EB8" data-bytes="93 6F">o</span> <span data-cp="3EBF" data-bytes="94 B9"></span> <span data-cp="3EC2" data-bytes="95 EC"></span> <span data-cp="3EC7" data-bytes="91 EE"></span> <span data-cp="3ECA" data-bytes="98 C3"></span> <span data-cp="3ECC" data-bytes="95 F6"></span> <span data-cp="3ED0" data-bytes="8F FD"></span> <span data-cp="3ED1" data-bytes="98 C5"></span> <span data-cp="3ED6" data-bytes="97 66">f</span> <span data-cp="3EDA" data-bytes="97 DD"></span> <span data-cp="3EDB" data-bytes="8C AA"></span> <span data-cp="3EDE" data-bytes="92 D2"></span> <span data-cp="3EE1" data-bytes="97 61">a</span> <span data-cp="3EE2" data-bytes="98 CB"></span> <span data-cp="3EE7" data-bytes="95 F0"></span> <span data-cp="3EE9" data-bytes="97 5D">]</span> <span data-cp="3EEB" data-bytes="91 E3"></span> <span data-cp="3EEC" data-bytes="87 7E">~</span> <span data-cp="3EF0" data-bytes="98 CC"></span> <span data-cp="3EF3" data-bytes="94 69">i</span> <span data-cp="3EF4" data-bytes="98 CD"></span> <span data-cp="3EFA" data-bytes="98 CE"></span> <span data-cp="3EFC" data-bytes="95 FC"></span> <span data-cp="3EFF" data-bytes="94 A3"></span> <span data-cp="3F00" data-bytes="96 62">b</span> <span data-cp="3F06" data-bytes="94 63">c</span> <span data-cp="3F07" data-bytes="8D 47">G</span> <span data-cp="3F0E" data-bytes="98 D0"></span> <span data-cp="3F53" data-bytes="98 D1"></span> <span data-cp="3F58" data-bytes="94 75">u</span> <span data-cp="3F63" data-bytes="94 72">r</span> <span data-cp="3F7C" data-bytes="98 D6"></span> <span data-cp="3F93" data-bytes="8A F0"></span> <span data-cp="3FC0" data-bytes="98 D9"></span> <span data-cp="3FC8" data-bytes="8D 5A">Z</span> <span data-cp="3FD7" data-bytes="98 DB"></span> <span data-cp="3FDC" data-bytes="98 DD"></span> <span data-cp="3FE5" data-bytes="98 A8"></span> <span data-cp="3FED" data-bytes="8A 6D">m</span> <span data-cp="3FF9" data-bytes="8A FB"></span> <span data-cp="3FFA" data-bytes="8A AE"></span> <span data-cp="4009" data-bytes="8C 5D">]</span> <span data-cp="401D" data-bytes="98 E4"></span> <span data-cp="4039" data-bytes="98 E6"></span> <span data-cp="4045" data-bytes="98 E8"></span> <span data-cp="4053" data-bytes="8A 4D">M</span> <span data-cp="4057" data-bytes="92 57">W</span> <span data-cp="4062" data-bytes="95 DF"></span> <span data-cp="4065" data-bytes="A0 AC"></span> <span data-cp="406A" data-bytes="98 EB"></span> <span data-cp="406F" data-bytes="98 EC"></span> <span data-cp="4071" data-bytes="8C C3"></span> <span data-cp="40A8" data-bytes="98 F4"></span> <span data-cp="40B4" data-bytes="87 D9"></span> <span data-cp="40BB" data-bytes="8A B8"></span> <span data-cp="40BF" data-bytes="9E E7"></span> <span data-cp="40C8" data-bytes="94 BC"></span> <span data-cp="40DF" data-bytes="9C C6"></span> <span data-cp="40F8" data-bytes="8D 4A">J</span> <span data-cp="40FA" data-bytes="9E 7E">~</span> <span data-cp="4102" data-bytes="8D 44">D</span> <span data-cp="4103" data-bytes="98 FE"></span> <span data-cp="4109" data-bytes="99 40">@</span> <span data-cp="410E" data-bytes="94 C9"></span> <span data-cp="4131" data-bytes="87 C6"></span> <span data-cp="4132" data-bytes="94 D3"></span> <span data-cp="4167" data-bytes="99 46">F</span> <span data-cp="416C" data-bytes="90 C0"></span> <span data-cp="416E" data-bytes="94 D1"></span> <span data-cp="417C" data-bytes="8D 4E">N</span> <span data-cp="417F" data-bytes="95 73">s</span> <span data-cp="4181" data-bytes="87 CE"></span> <span data-cp="4190" data-bytes="93 C2"></span> <span data-cp="41B2" data-bytes="99 48">H</span> <span data-cp="41C4" data-bytes="99 4B">K</span> <span data-cp="41CA" data-bytes="8E 55">U</span> <span data-cp="41CF" data-bytes="99 4E">N</span> <span data-cp="41DB" data-bytes="8E FE"></span> <span data-cp="41ED" data-bytes="8D 5F">_</span> <span data-cp="41EF" data-bytes="8E 59">Y</span> <span data-cp="41F9" data-bytes="94 EC"></span> <span data-cp="4211" data-bytes="94 EF"></span> <span data-cp="4223" data-bytes="8C 60">`</span> <span data-cp="4240" data-bytes="8F 74">t</span> <span data-cp="4260" data-bytes="99 55">U</span> <span data-cp="426A" data-bytes="95 44">D</span> <span data-cp="4276" data-bytes="8C CB"></span> <span data-cp="427A" data-bytes="99 56">V</span> <span data-cp="428C" data-bytes="99 59">Y</span> <span data-cp="4294" data-bytes="99 5B">[</span> <span data-cp="42A2" data-bytes="8C C4"></span> <span data-cp="42B9" data-bytes="90 B7"></span> <span data-cp="42BC" data-bytes="97 43">C</span> <span data-cp="42F4" data-bytes="95 CD"></span> <span data-cp="42FB" data-bytes="97 C9"></span> <span data-cp="430A" data-bytes="87 AA"></span> <span data-cp="432B" data-bytes="8E B9"></span> <span data-cp="436E" data-bytes="95 C6"></span> <span data-cp="4397" data-bytes="99 67">g</span> <span data-cp="439A" data-bytes="8C E3"></span> <span data-cp="43BA" data-bytes="8A B9"></span> <span data-cp="43C1" data-bytes="8D FC"></span> <span data-cp="43D9" data-bytes="8A 76">v</span> <span data-cp="43DF" data-bytes="9D 51">Q</span> <span data-cp="43ED" data-bytes="99 73">s</span> <span data-cp="43F0" data-bytes="87 40">@</span> <span data-cp="43F2" data-bytes="9D 4F">O</span> <span data-cp="4401" data-bytes="99 7A">z</span> <span data-cp="4402" data-bytes="95 64">d</span> <span data-cp="4413" data-bytes="99 A1"></span> <span data-cp="4425" data-bytes="99 A5"></span> <span data-cp="442D" data-bytes="99 A7"></span> <span data-cp="447A" data-bytes="8E ED"></span> <span data-cp="448F" data-bytes="99 AD"></span> <span data-cp="449F" data-bytes="94 6E">n</span> <span data-cp="44A0" data-bytes="8F 70">p</span> <span data-cp="44B0" data-bytes="99 B3"></span> <span data-cp="44B7" data-bytes="A0 53">S</span> <span data-cp="44BD" data-bytes="8D 5E">^</span> <span data-cp="44C0" data-bytes="96 5C">\</span> <span data-cp="44C3" data-bytes="8C E0"></span> <span data-cp="44CE" data-bytes="97 FE"></span> <span data-cp="44DD" data-bytes="92 BD"></span> <span data-cp="44DE" data-bytes="8D 5D">]</span> <span data-cp="44DF" data-bytes="97 FD"></span> <span data-cp="44E1" data-bytes="87 DB"></span> <span data-cp="44E4" data-bytes="8F 64">d</span> <span data-cp="44EA" data-bytes="95 62">b</span> <span data-cp="44EB" data-bytes="97 CD"></span> <span data-cp="44EC" data-bytes="9E 64">d</span> <span data-cp="44F4" data-bytes="92 4C">L</span> <span data-cp="4503" data-bytes="8E C9"></span> <span data-cp="4504" data-bytes="99 BC"></span> <span data-cp="4509" data-bytes="9D A5"></span> <span data-cp="450B" data-bytes="8F 54">T</span> <span data-cp="4516" data-bytes="8F 7C">|</span> <span data-cp="451B" data-bytes="8D 55">U</span> <span data-cp="451D" data-bytes="8E A2"></span> <span data-cp="4527" data-bytes="8F 7A">z</span> <span data-cp="452E" data-bytes="97 AE"></span> <span data-cp="4533" data-bytes="96 C8"></span> <span data-cp="4536" data-bytes="8C E4"></span> <span data-cp="453B" data-bytes="99 C3"></span> <span data-cp="453D" data-bytes="90 D6"></span> <span data-cp="453F" data-bytes="9C BE"></span> <span data-cp="4543" data-bytes="8F 76">v</span> <span data-cp="4551" data-bytes="94 70">p</span> <span data-cp="4558" data-bytes="8C EF"></span> <span data-cp="455C" data-bytes="8E C7"></span> <span data-cp="4561" data-bytes="8D 54">T</span> <span data-cp="4562" data-bytes="A0 F9"></span> <span data-cp="456A" data-bytes="8F A9"></span> <span data-cp="456D" data-bytes="8D 51">Q</span> <span data-cp="4577" data-bytes="99 C7"></span> <span data-cp="4578" data-bytes="87 44">D</span> <span data-cp="4585" data-bytes="90 D7"></span> <span data-cp="45A6" data-bytes="87 43">C</span> <span data-cp="45B3" data-bytes="87 47">G</span> <span data-cp="45DA" data-bytes="87 58">X</span> <span data-cp="45E9" data-bytes="9E DF"></span> <span data-cp="45EA" data-bytes="8D 59">Y</span> <span data-cp="4603" data-bytes="87 42">B</span> <span data-cp="4606" data-bytes="99 CE"></span> <span data-cp="460F" data-bytes="8F BA"></span> <span data-cp="4615" data-bytes="8F EB"></span> <span data-cp="4617" data-bytes="99 CF"></span> <span data-cp="465B" data-bytes="8F C2"></span> <span data-cp="467A" data-bytes="92 C9"></span> <span data-cp="4680" data-bytes="97 DC"></span> <span data-cp="46A1" data-bytes="87 5D">]</span> <span data-cp="46AE" data-bytes="87 CC"></span> <span data-cp="46BB" data-bytes="8D 45">E</span> <span data-cp="46CF" data-bytes="95 B3"></span> <span data-cp="46D0" data-bytes="9C 79">y</span> <span data-cp="46F5" data-bytes="95 B2"></span> <span data-cp="46F7" data-bytes="8D 4C">L</span> <span data-cp="4713" data-bytes="8F DB"></span> <span data-cp="4718" data-bytes="9B E3"></span> <span data-cp="4736" data-bytes="87 4C">L</span> <span data-cp="4744" data-bytes="87 4D">M</span> <span data-cp="474E" data-bytes="9E 7A">z</span> <span data-cp="474F" data-bytes="87 57">W</span> <span data-cp="477C" data-bytes="9B EE"></span> <span data-cp="4798" data-bytes="99 DE"></span> <span data-cp="47D5" data-bytes="8A 52">R</span> <span data-cp="47ED" data-bytes="99 E1"></span> <span data-cp="47F4" data-bytes="8A 67">g</span> <span data-cp="4800" data-bytes="8B B5"></span> <span data-cp="480B" data-bytes="8A AC"></span> <span data-cp="4837" data-bytes="99 E9"></span> <span data-cp="4871" data-bytes="97 DE"></span> <span data-cp="489B" data-bytes="95 D1"></span> <span data-cp="48AD" data-bytes="99 F5"></span> <span data-cp="48D0" data-bytes="9B A9"></span> <span data-cp="48F3" data-bytes="9E A4"></span> <span data-cp="48FA" data-bytes="9D 49">I</span> <span data-cp="4906" data-bytes="95 DB"></span> <span data-cp="4911" data-bytes="89 C5"></span> <span data-cp="491E" data-bytes="99 F8"></span> <span data-cp="4925" data-bytes="96 64">d</span> <span data-cp="492A" data-bytes="90 55">U</span> <span data-cp="492D" data-bytes="96 D4"></span> <span data-cp="492F" data-bytes="87 C4"></span> <span data-cp="4930" data-bytes="87 AE"></span> <span data-cp="4935" data-bytes="97 7C">|</span> <span data-cp="493C" data-bytes="96 4D">M</span> <span data-cp="493E" data-bytes="97 E1"></span> <span data-cp="4945" data-bytes="9A 48">H</span> <span data-cp="4951" data-bytes="9A 49">I</span> <span data-cp="4965" data-bytes="90 AA"></span> <span data-cp="496A" data-bytes="9A 50">P</span> <span data-cp="4972" data-bytes="93 47">G</span> <span data-cp="4989" data-bytes="8E D8"></span> <span data-cp="49A1" data-bytes="90 C9"></span> <span data-cp="49A7" data-bytes="9A 55">U</span> <span data-cp="49DF" data-bytes="90 BC"></span> <span data-cp="49E5" data-bytes="9A 58">X</span> <span data-cp="49E7" data-bytes="8B B8"></span> <span data-cp="4A0F" data-bytes="90 D5"></span> <span data-cp="4A1D" data-bytes="96 41">A</span> <span data-cp="4A24" data-bytes="9A 5A">Z</span> <span data-cp="4A35" data-bytes="9A 5C">\</span> <span data-cp="4A96" data-bytes="97 C2"></span> <span data-cp="4AA4" data-bytes="87 5C">\</span> <span data-cp="4AB4" data-bytes="8A BB"></span> <span data-cp="4AB8" data-bytes="9B AA"></span> <span data-cp="4AD1" data-bytes="90 F5"></span> <span data-cp="4AE4" data-bytes="9A 60">`</span> <span data-cp="4AFF" data-bytes="91 45">E</span> <span data-cp="4B10" data-bytes="8C 58">X</span> <span data-cp="4B19" data-bytes="9A 63">c</span> <span data-cp="4B20" data-bytes="8C 49">I</span> <span data-cp="4B2C" data-bytes="8B B6"></span> <span data-cp="4B6F" data-bytes="96 6B">k</span> <span data-cp="4B70" data-bytes="9A 6E">n</span> <span data-cp="4B72" data-bytes="91 4F">O</span> <span data-cp="4B7B" data-bytes="97 46">F</span> <span data-cp="4B7E" data-bytes="A0 E6"></span> <span data-cp="4B8E" data-bytes="92 D7"></span> <span data-cp="4B90" data-bytes="96 75">u</span> <span data-cp="4B93" data-bytes="93 D4"></span> <span data-cp="4B96" data-bytes="91 BB"></span> <span data-cp="4B97" data-bytes="96 79">y</span> <span data-cp="4B9D" data-bytes="9A 70">p</span> <span data-cp="4BBD" data-bytes="96 78">x</span> <span data-cp="4BBE" data-bytes="91 CD"></span> <span data-cp="4BC0" data-bytes="9C 4A">J</span> <span data-cp="4C04" data-bytes="A0 6F">o</span> <span data-cp="4C07" data-bytes="A0 6A">j</span> <span data-cp="4C0E" data-bytes="91 5F">_</span> <span data-cp="4C32" data-bytes="87 41">A</span> <span data-cp="4C3B" data-bytes="9F A5"></span> <span data-cp="4C3E" data-bytes="89 BA"></span> <span data-cp="4C40" data-bytes="87 4F">O</span> <span data-cp="4C47" data-bytes="87 4E">N</span> <span data-cp="4C57" data-bytes="87 55">U</span> <span data-cp="4C5B" data-bytes="9E CD"></span> <span data-cp="4C6D" data-bytes="9A 79">y</span> <span data-cp="4C77" data-bytes="8C F2"></span> <span data-cp="4C7B" data-bytes="8D 57">W</span> <span data-cp="4C7D" data-bytes="9D CE"></span> <span data-cp="4C81" data-bytes="8C D2"></span> <span data-cp="4C85" data-bytes="87 59">Y</span> <span data-cp="4CA4" data-bytes="9D 73">s</span> <span data-cp="4CAE" data-bytes="96 B9"></span> <span data-cp="4CB0" data-bytes="96 BC"></span> <span data-cp="4CB7" data-bytes="9C D1"></span> <span data-cp="4CCD" data-bytes="89 B7"></span> <span data-cp="4CE1" data-bytes="9E EE"></span> <span data-cp="4CE2" data-bytes="87 49">I</span> <span data-cp="4D07" data-bytes="87 5B">[</span> <span data-cp="4D09" data-bytes="9E C9"></span> <span data-cp="4D34" data-bytes="91 AE"></span> <span data-cp="4D76" data-bytes="8D 58">X</span> <span data-cp="4D77" data-bytes="87 46">F</span> <span data-cp="4D89" data-bytes="8D 56">V</span> <span data-cp="4D91" data-bytes="9D 78">x</span> <span data-cp="4D9C" data-bytes="9D 7B">{</span> <span data-cp="4E04" data-bytes="9E B3"></span> <span data-cp="4E1A" data-bytes="9E B2"></span> <span data-cp="4E1C" data-bytes="9D D6"></span> <span data-cp="4E21" data-bytes="99 4F">O</span> <span data-cp="4E24" data-bytes="89 CE"></span> <span data-cp="4E28" data-bytes="8B C0"></span> <span data-cp="4E2A" data-bytes="9F C4"></span> <span data-cp="4E2C" data-bytes="8B D4"></span> <span data-cp="4E2F" data-bytes="8C 72">r</span> <span data-cp="4E37" data-bytes="8B F9"></span> <span data-cp="4E3D" data-bytes="89 46">F</span> <span data-cp="4E5B" data-bytes="8B C6"></span> <span data-cp="4E6A" data-bytes="9C 57">W</span> <span data-cp="4E78" data-bytes="9A FB"></span> <span data-cp="4E80" data-bytes="89 D0"></span> <span data-cp="4E81" data-bytes="89 CF"></span> <span data-cp="4E87" data-bytes="89 D1"></span> <span data-cp="4E89" data-bytes="89 E2"></span> <span data-cp="4E98" data-bytes="92 7E">~</span> <span data-cp="4E9A" data-bytes="9D BA"></span> <span data-cp="4EA3" data-bytes="8C 6F">o</span> <span data-cp="4EBB" data-bytes="8B C7"></span> <span data-cp="4EBC" data-bytes="92 6B">k</span> <span data-cp="4EBF" data-bytes="89 D2"></span> <span data-cp="4ECE" data-bytes="9F CF"></span> <span data-cp="4EEA" data-bytes="9D A9"></span> <span data-cp="4EEB" data-bytes="89 D3"></span> <span data-cp="4EEE" data-bytes="99 E2"></span> <span data-cp="4EF8" data-bytes="92 67">g</span> <span data-cp="4F03" data-bytes="92 A4"></span> <span data-cp="4F17" data-bytes="8C 73">s</span> <span data-cp="4F1A" data-bytes="89 4E">N</span> <span data-cp="4F28" data-bytes="89 4F">O</span> <span data-cp="4F29" data-bytes="92 78">x</span> <span data-cp="4F32" data-bytes="91 B6"></span> <span data-cp="4F37" data-bytes="89 D4"></span> <span data-cp="4F39" data-bytes="9F D2"></span> <span data-cp="4F42" data-bytes="92 A7"></span> <span data-cp="4F45" data-bytes="95 A2"></span> <span data-cp="4F4B" data-bytes="92 6E">n</span> <span data-cp="4F72" data-bytes="96 EA"></span> <span data-cp="4F8A" data-bytes="92 6F">o</span> <span data-cp="4FA2" data-bytes="92 A3"></span> <span data-cp="4FA8" data-bytes="89 50">P</span> <span data-cp="4FB0" data-bytes="98 66">f</span> <span data-cp="4FB4" data-bytes="8C F8"></span> <span data-cp="4FBB" data-bytes="9C 53">S</span> <span data-cp="4FBD" data-bytes="89 D6"></span> <span data-cp="4FC8" data-bytes="98 B2"></span> <span data-cp="4FCC" data-bytes="92 AB"></span> <span data-cp="4FE4" data-bytes="96 DE"></span> <span data-cp="4FE5" data-bytes="92 AC"></span> <span data-cp="4FF0" data-bytes="8C 70">p</span> <span data-cp="4FF2" data-bytes="9F 6E">n</span> <span data-cp="4FF9" data-bytes="8E F2"></span> <span data-cp="4FFD" data-bytes="9F 6C">l</span> <span data-cp="5003" data-bytes="89 D8"></span> <span data-cp="502E" data-bytes="92 A8"></span> <span data-cp="5034" data-bytes="91 63">c</span> <span data-cp="503B" data-bytes="8C 40">@</span> <span data-cp="5056" data-bytes="9F 73">s</span> <span data-cp="5058" data-bytes="92 AD"></span> <span data-cp="5066" data-bytes="9B E9"></span> <span data-cp="506C" data-bytes="92 A9"></span> <span data-cp="5081" data-bytes="92 AA"></span> <span data-cp="5088" data-bytes="89 D9"></span> <span data-cp="50A6" data-bytes="9F A8"></span> <span data-cp="50BC" data-bytes="8C 71">q</span> <span data-cp="50CD" data-bytes="92 A1"></span> <span data-cp="50D0" data-bytes="90 E3"></span> <span data-cp="50D9" data-bytes="A0 A6"></span> <span data-cp="50DF" data-bytes="94 AB"></span> <span data-cp="50ED" data-bytes="9F CB"></span> <span data-cp="50F4" data-bytes="97 C4"></span> <span data-cp="50FC" data-bytes="92 AE"></span> <span data-cp="510D" data-bytes="92 A2"></span> <span data-cp="512B" data-bytes="92 68">h</span> <span data-cp="5156" data-bytes="89 51">Q</span> <span data-cp="5159" data-bytes="92 AF"></span> <span data-cp="515B" data-bytes="92 B0"></span> <span data-cp="515D" data-bytes="92 B1"></span> <span data-cp="515E" data-bytes="92 B2"></span> <span data-cp="5174" data-bytes="89 52">R</span> <span data-cp="5179" data-bytes="94 5A">Z</span> <span data-cp="5186" data-bytes="89 DD"></span> <span data-cp="519A" data-bytes="9E 52">R</span> <span data-cp="519C" data-bytes="89 53">S</span> <span data-cp="51A7" data-bytes="9E 55">U</span> <span data-cp="51A8" data-bytes="92 BA"></span> <span data-cp="51AE" data-bytes="8C 5B">[</span> <span data-cp="51B4" data-bytes="9A 68">h</span> <span data-cp="51C3" data-bytes="92 BB"></span> <span data-cp="51D2" data-bytes="9B B4"></span> <span data-cp="51DB" data-bytes="89 DF"></span> <span data-cp="51E4" data-bytes="89 54">T</span> <span data-cp="51FC" data-bytes="89 E0"></span> <span data-cp="51FE" data-bytes="9F 4F">O</span> <span data-cp="5205" data-bytes="89 E1"></span> <span data-cp="521F" data-bytes="9F CD"></span> <span data-cp="5220" data-bytes="A0 E7"></span> <span data-cp="5227" data-bytes="89 A6"></span> <span data-cp="5234" data-bytes="9E FA"></span> <span data-cp="524F" data-bytes="87 BC"></span> <span data-cp="5259" data-bytes="92 C4"></span> <span data-cp="5260" data-bytes="9F 6F">o</span> <span data-cp="5268" data-bytes="8B B0"></span> <span data-cp="5273" data-bytes="9F AC"></span> <span data-cp="5279" data-bytes="89 E3"></span> <span data-cp="528F" data-bytes="9B D3"></span> <span data-cp="5290" data-bytes="89 E4"></span> <span data-cp="529A" data-bytes="9F D5"></span> <span data-cp="52A1" data-bytes="89 55">U</span> <span data-cp="52A4" data-bytes="92 C5"></span> <span data-cp="52A8" data-bytes="89 56">V</span> <span data-cp="52CC" data-bytes="9E DC"></span> <span data-cp="52D1" data-bytes="9F 71">q</span> <span data-cp="52E1" data-bytes="92 C7"></span> <span data-cp="5301" data-bytes="9A 4C">L</span> <span data-cp="5324" data-bytes="8C 68">h</span> <span data-cp="5327" data-bytes="89 E5"></span> <span data-cp="532C" data-bytes="9F 7D">}</span> <span data-cp="5332" data-bytes="A0 A9"></span> <span data-cp="533B" data-bytes="89 57">W</span> <span data-cp="534E" data-bytes="89 58">X</span> <span data-cp="535D" data-bytes="8B E3"></span> <span data-cp="535F" data-bytes="8B 61">a</span> <span data-cp="5364" data-bytes="9A F1"></span> <span data-cp="5367" data-bytes="9E B7"></span> <span data-cp="537D" data-bytes="9E BA"></span> <span data-cp="53A2" data-bytes="9C E0"></span> <span data-cp="53A9" data-bytes="89 E7"></span> <span data-cp="53AA" data-bytes="A0 7A">z</span> <span data-cp="53B0" data-bytes="89 E9"></span> <span data-cp="53C2" data-bytes="89 EB"></span> <span data-cp="53CC" data-bytes="90 C8"></span> <span data-cp="53D0" data-bytes="92 DA"></span> <span data-cp="53D1" data-bytes="89 59">Y</span> <span data-cp="53D2" data-bytes="9C F5"></span> <span data-cp="53D8" data-bytes="89 5A">Z</span> <span data-cp="53DA" data-bytes="9F A2"></span> <span data-cp="53F7" data-bytes="8F AD"></span> <span data-cp="5414" data-bytes="96 EF"></span> <span data-cp="5416" data-bytes="9D EC"></span> <span data-cp="541A" data-bytes="9D CA"></span> <span data-cp="5423" data-bytes="89 EC"></span> <span data-cp="5432" data-bytes="9D E2"></span> <span data-cp="5434" data-bytes="8C 75">u</span> <span data-cp="544B" data-bytes="9E C0"></span> <span data-cp="544C" data-bytes="87 C5"></span> <span data-cp="544D" data-bytes="9E 56">V</span> <span data-cp="5469" data-bytes="9F 79">y</span> <span data-cp="546A" data-bytes="9A C7"></span> <span data-cp="5485" data-bytes="98 A1"></span> <span data-cp="5493" data-bytes="89 F0"></span> <span data-cp="5494" data-bytes="9E 47">G</span> <span data-cp="5497" data-bytes="9D F7"></span> <span data-cp="549C" data-bytes="9F D3"></span> <span data-cp="549E" data-bytes="9A CA"></span> <span data-cp="54A3" data-bytes="89 F1"></span> <span data-cp="54B2" data-bytes="8E 5A">Z</span> <span data-cp="54B4" data-bytes="89 F2"></span> <span data-cp="54B9" data-bytes="89 F3"></span> <span data-cp="54CB" data-bytes="92 5D">]</span> <span data-cp="54CC" data-bytes="8B 51">Q</span> <span data-cp="54CD" data-bytes="92 E0"></span> <span data-cp="54D0" data-bytes="89 F4"></span> <span data-cp="54DA" data-bytes="9F D4"></span> <span data-cp="54E3" data-bytes="8A 79">y</span> <span data-cp="54EF" data-bytes="89 F5"></span> <span data-cp="5502" data-bytes="97 A7"></span> <span data-cp="550D" data-bytes="93 BA"></span> <span data-cp="5513" data-bytes="9E 58">X</span> <span data-cp="5518" data-bytes="89 F6"></span> <span data-cp="551E" data-bytes="9E 57">W</span> <span data-cp="5523" data-bytes="89 F7"></span> <span data-cp="5525" data-bytes="8A 41">A</span> <span data-cp="5528" data-bytes="89 F8"></span> <span data-cp="553F" data-bytes="89 FA"></span> <span data-cp="5569" data-bytes="9E 4E">N</span> <span data-cp="556B" data-bytes="94 DC"></span> <span data-cp="5571" data-bytes="95 DA"></span> <span data-cp="5572" data-bytes="9D F8"></span> <span data-cp="5573" data-bytes="9F 6A">j</span> <span data-cp="5579" data-bytes="8A B7"></span> <span data-cp="5590" data-bytes="8A 46">F</span> <span data-cp="55B0" data-bytes="91 48">H</span> <span data-cp="55B4" data-bytes="92 DE"></span> <span data-cp="55B9" data-bytes="8B 53">S</span> <span data-cp="55BA" data-bytes="9D F6"></span> <span data-cp="55BC" data-bytes="9B DA"></span> <span data-cp="55C1" data-bytes="9D 7E">~</span> <span data-cp="55D7" data-bytes="89 FD"></span> <span data-cp="55D8" data-bytes="99 E4"></span> <span data-cp="55DE" data-bytes="9E 43">C</span> <span data-cp="55EA" data-bytes="9D E9"></span> <span data-cp="55EC" data-bytes="8F 52">R</span> <span data-cp="55F0" data-bytes="9D F5"></span> <span data-cp="55F1" data-bytes="9D F0"></span> <span data-cp="55F5" data-bytes="99 E7"></span> <span data-cp="55FB" data-bytes="8B BD"></span> <span data-cp="5605" data-bytes="9D EF"></span> <span data-cp="5611" data-bytes="9F B7"></span> <span data-cp="561E" data-bytes="9D D0"></span> <span data-cp="5620" data-bytes="9F EB"></span> <span data-cp="5621" data-bytes="8D A9"></span> <span data-cp="5622" data-bytes="9D CF"></span> <span data-cp="5623" data-bytes="98 E1"></span> <span data-cp="5625" data-bytes="9D E5"></span> <span data-cp="562D" data-bytes="9D C8"></span> <span data-cp="5643" data-bytes="9D EB"></span> <span data-cp="5650" data-bytes="9A A2"></span> <span data-cp="5652" data-bytes="8A D6"></span> <span data-cp="5654" data-bytes="9A 5F">_</span> <span data-cp="565D" data-bytes="9E F5"></span> <span data-cp="5661" data-bytes="8F B7"></span> <span data-cp="567A" data-bytes="9A D2"></span> <span data-cp="567B" data-bytes="9E 6A">j</span> <span data-cp="567C" data-bytes="9E E8"></span> <span data-cp="5689" data-bytes="8B BF"></span> <span data-cp="568A" data-bytes="91 C2"></span> <span data-cp="568B" data-bytes="9D 62">b</span> <span data-cp="5692" data-bytes="92 60">`</span> <span data-cp="569E" data-bytes="92 5E">^</span> <span data-cp="569F" data-bytes="91 C1"></span> <span data-cp="56A1" data-bytes="8A C5"></span> <span data-cp="56A4" data-bytes="97 A3"></span> <span data-cp="56AF" data-bytes="8B 6C">l</span> <span data-cp="56B1" data-bytes="8D 7E">~</span> <span data-cp="56B9" data-bytes="9C 54">T</span> <span data-cp="56BF" data-bytes="9D BD"></span> <span data-cp="56D6" data-bytes="9C C5"></span> <span data-cp="56E2" data-bytes="89 5B">[</span> <span data-cp="56FB" data-bytes="87 65">e</span> <span data-cp="56FD" data-bytes="98 C7"></span> <span data-cp="5715" data-bytes="9C EE"></span> <span data-cp="571D" data-bytes="92 E2"></span> <span data-cp="5732" data-bytes="94 A7"></span> <span data-cp="573D" data-bytes="8C CC"></span> <span data-cp="573F" data-bytes="9B D4"></span> <span data-cp="5754" data-bytes="99 E5"></span> <span data-cp="5757" data-bytes="9A C2"></span> <span data-cp="575B" data-bytes="91 FB"></span> <span data-cp="575F" data-bytes="A0 73">s</span> <span data-cp="5767" data-bytes="9F 72">r</span> <span data-cp="577A" data-bytes="9F CC"></span> <span data-cp="577E" data-bytes="98 A5"></span> <span data-cp="577F" data-bytes="92 E8"></span> <span data-cp="5788" data-bytes="9B BC"></span> <span data-cp="578A" data-bytes="96 F3"></span> <span data-cp="578D" data-bytes="92 E7"></span> <span data-cp="579C" data-bytes="8B 7D">}</span> <span data-cp="57A1" data-bytes="9B F4"></span> <span data-cp="57A7" data-bytes="9E F7"></span> <span data-cp="57AA" data-bytes="9E C1"></span> <span data-cp="57B3" data-bytes="87 C3"></span> <span data-cp="57B4" data-bytes="99 6F">o</span> <span data-cp="57BB" data-bytes="96 F1"></span> <span data-cp="57BE" data-bytes="8E 41">A</span> <span data-cp="57C4" data-bytes="95 4A">J</span> <span data-cp="57C8" data-bytes="97 E6"></span> <span data-cp="57D7" data-bytes="96 F5"></span> <span data-cp="57DD" data-bytes="92 E6"></span> <span data-cp="57DE" data-bytes="9F 42">B</span> <span data-cp="57EF" data-bytes="99 A9"></span> <span data-cp="5812" data-bytes="97 E5"></span> <span data-cp="5818" data-bytes="87 C8"></span> <span data-cp="5822" data-bytes="96 7D">}</span> <span data-cp="583A" data-bytes="99 A2"></span> <span data-cp="5840" data-bytes="9A BB"></span> <span data-cp="5844" data-bytes="9A 65">e</span> <span data-cp="5847" data-bytes="94 4E">N</span> <span data-cp="585F" data-bytes="99 DF"></span> <span data-cp="5869" data-bytes="98 E3"></span> <span data-cp="586C" data-bytes="92 54">T</span> <span data-cp="5872" data-bytes="96 7B">{</span> <span data-cp="5873" data-bytes="8A AF"></span> <span data-cp="5892" data-bytes="8C 77">w</span> <span data-cp="5896" data-bytes="87 B0"></span> <span data-cp="5899" data-bytes="8B AF"></span> <span data-cp="589A" data-bytes="9E BD"></span> <span data-cp="58A7" data-bytes="9E E6"></span> <span data-cp="58B0" data-bytes="8E E1"></span> <span data-cp="58B5" data-bytes="9B 7D">}</span> <span data-cp="58B6" data-bytes="9C 7E">~</span> <span data-cp="58CB" data-bytes="92 EA"></span> <span data-cp="58D0" data-bytes="8C 78">x</span> <span data-cp="58F0" data-bytes="89 5C">\</span> <span data-cp="58F2" data-bytes="98 F0"></span> <span data-cp="58F3" data-bytes="96 F2"></span> <span data-cp="5902" data-bytes="8B C1"></span> <span data-cp="5904" data-bytes="89 5D">]</span> <span data-cp="5905" data-bytes="89 DE"></span> <span data-cp="5907" data-bytes="89 5E">^</span> <span data-cp="591D" data-bytes="87 68">h</span> <span data-cp="5932" data-bytes="89 5F">_</span> <span data-cp="5934" data-bytes="89 60">`</span> <span data-cp="5965" data-bytes="9B CD"></span> <span data-cp="5975" data-bytes="9D D3"></span> <span data-cp="5989" data-bytes="98 4C">L</span> <span data-cp="5994" data-bytes="97 52">R</span> <span data-cp="599A" data-bytes="95 C3"></span> <span data-cp="599F" data-bytes="9B B6"></span> <span data-cp="59AC" data-bytes="9A B9"></span> <span data-cp="59B0" data-bytes="97 B3"></span> <span data-cp="59B7" data-bytes="9F 74">t</span> <span data-cp="59B8" data-bytes="92 F1"></span> <span data-cp="59BF" data-bytes="8C FA"></span> <span data-cp="59C4" data-bytes="97 DF"></span> <span data-cp="59EB" data-bytes="98 77">w</span> <span data-cp="59EF" data-bytes="98 54">T</span> <span data-cp="59F0" data-bytes="95 C5"></span> <span data-cp="59F8" data-bytes="9D 55">U</span> <span data-cp="5A02" data-bytes="95 7E">~</span> <span data-cp="5A0B" data-bytes="97 42">B</span> <span data-cp="5A0D" data-bytes="94 E6"></span> <span data-cp="5A12" data-bytes="92 F5"></span> <span data-cp="5A1A" data-bytes="8C C5"></span> <span data-cp="5A21" data-bytes="92 FD"></span> <span data-cp="5A27" data-bytes="9C 51">Q</span> <span data-cp="5A2A" data-bytes="94 E9"></span> <span data-cp="5A2B" data-bytes="98 5C">\</span> <span data-cp="5A2C" data-bytes="92 F0"></span> <span data-cp="5A3D" data-bytes="94 4C">L</span> <span data-cp="5A45" data-bytes="91 6B">k</span> <span data-cp="5A54" data-bytes="8B 78">x</span> <span data-cp="5A59" data-bytes="94 E2"></span> <span data-cp="5A61" data-bytes="98 4F">O</span> <span data-cp="5A67" data-bytes="9C D0"></span> <span data-cp="5A68" data-bytes="92 71">q</span> <span data-cp="5A6B" data-bytes="93 65">e</span> <span data-cp="5A6E" data-bytes="98 5B">[</span> <span data-cp="5A71" data-bytes="98 50">P</span> <span data-cp="5A79" data-bytes="97 BC"></span> <span data-cp="5A7E" data-bytes="92 F3"></span> <span data-cp="5A81" data-bytes="93 40">@</span> <span data-cp="5A82" data-bytes="98 4D">M</span> <span data-cp="5A86" data-bytes="95 72">r</span> <span data-cp="5A99" data-bytes="92 EB"></span> <span data-cp="5AA1" data-bytes="97 B7"></span> <span data-cp="5AA4" data-bytes="87 6F">o</span> <span data-cp="5AC3" data-bytes="90 A7"></span> <span data-cp="5ACE" data-bytes="97 41">A</span> <span data-cp="5ACF" data-bytes="92 F4"></span> <span data-cp="5AD1" data-bytes="87 72">r</span> <span data-cp="5AE4" data-bytes="95 77">w</span> <span data-cp="5AF0" data-bytes="9E E2"></span> <span data-cp="5AF2" data-bytes="8F 78">x</span> <span data-cp="5AFE" data-bytes="96 72">r</span> <span data-cp="5B0D" data-bytes="9E B5"></span> <span data-cp="5B11" data-bytes="96 4B">K</span> <span data-cp="5B15" data-bytes="8C AC"></span> <span data-cp="5B1F" data-bytes="A0 FA"></span> <span data-cp="5B28" data-bytes="96 FC"></span> <span data-cp="5B2B" data-bytes="95 75">u</span> <span data-cp="5B41" data-bytes="90 DA"></span> <span data-cp="5B44" data-bytes="93 67">g</span> <span data-cp="5B4A" data-bytes="90 DF"></span> <span data-cp="5B4F" data-bytes="93 54">T</span> <span data-cp="5B66" data-bytes="89 61">a</span> <span data-cp="5B68" data-bytes="8B B4"></span> <span data-cp="5B6D" data-bytes="9D C0"></span> <span data-cp="5B74" data-bytes="8E 48">H</span> <span data-cp="5B90" data-bytes="9E 67">g</span> <span data-cp="5B96" data-bytes="8C D9"></span> <span data-cp="5B9E" data-bytes="89 62">b</span> <span data-cp="5B9F" data-bytes="89 63">c</span> <span data-cp="5BB7" data-bytes="87 73">s</span> <span data-cp="5BC3" data-bytes="9F 6B">k</span> <span data-cp="5BDB" data-bytes="87 6D">m</span> <span data-cp="5C10" data-bytes="9C BC"></span> <span data-cp="5C1C" data-bytes="8B 5D">]</span> <span data-cp="5C1E" data-bytes="93 4C">L</span> <span data-cp="5C20" data-bytes="9A E2"></span> <span data-cp="5C23" data-bytes="8B C9"></span> <span data-cp="5C4A" data-bytes="9F C9"></span> <span data-cp="5C53" data-bytes="9F 44">D</span> <span data-cp="5C5E" data-bytes="98 ED"></span> <span data-cp="5C78" data-bytes="8C E9"></span> <span data-cp="5C99" data-bytes="8D F2"></span> <span data-cp="5C9A" data-bytes="89 64">d</span> <span data-cp="5C9E" data-bytes="93 4D">M</span> <span data-cp="5CC1" data-bytes="A0 F2"></span> <span data-cp="5CC2" data-bytes="98 68">h</span> <span data-cp="5CD1" data-bytes="9F 58">X</span> <span data-cp="5CD5" data-bytes="8C E6"></span> <span data-cp="5CE5" data-bytes="8D 73">s</span> <span data-cp="5CF5" data-bytes="8C 48">H</span> <span data-cp="5CFC" data-bytes="87 74">t</span> <span data-cp="5D15" data-bytes="8D A8"></span> <span data-cp="5D2C" data-bytes="9C 75">u</span> <span data-cp="5D2F" data-bytes="98 78">x</span> <span data-cp="5D3E" data-bytes="8D 60">`</span> <span data-cp="5D48" data-bytes="8D 61">a</span> <span data-cp="5D56" data-bytes="8D 62">b</span> <span data-cp="5D57" data-bytes="A0 A1"></span> <span data-cp="5D5B" data-bytes="9C 40">@</span> <span data-cp="5D70" data-bytes="98 AD"></span> <span data-cp="5D74" data-bytes="9E EA"></span> <span data-cp="5D78" data-bytes="8C EC"></span> <span data-cp="5D7B" data-bytes="8C D4"></span> <span data-cp="5D85" data-bytes="9C EB"></span> <span data-cp="5D8E" data-bytes="9F 51">Q</span> <span data-cp="5DA4" data-bytes="8D 65">e</span> <span data-cp="5DAB" data-bytes="9C F1"></span> <span data-cp="5DB9" data-bytes="8D 66">f</span> <span data-cp="5DC1" data-bytes="96 54">T</span> <span data-cp="5DF5" data-bytes="9F CE"></span> <span data-cp="5E0B" data-bytes="9A E4"></span> <span data-cp="5E12" data-bytes="9F 75">u</span> <span data-cp="5E42" data-bytes="8D 69">i</span> <span data-cp="5E48" data-bytes="93 4F">O</span> <span data-cp="5E5E" data-bytes="93 4E">N</span> <span data-cp="5E86" data-bytes="89 65">e</span> <span data-cp="5E92" data-bytes="8C 7A">z</span> <span data-cp="5E99" data-bytes="8C 7B">{</span> <span data-cp="5EBD" data-bytes="8D 6A">j</span> <span data-cp="5ECD" data-bytes="93 53">S</span> <span data-cp="5ED0" data-bytes="9D FB"></span> <span data-cp="5EF8" data-bytes="90 59">Y</span> <span data-cp="5F0C" data-bytes="93 61">a</span> <span data-cp="5F0E" data-bytes="93 62">b</span> <span data-cp="5F25" data-bytes="8D 6B">k</span> <span data-cp="5F3B" data-bytes="8C FE"></span> <span data-cp="5F4D" data-bytes="95 B8"></span> <span data-cp="5F51" data-bytes="8B CA"></span> <span data-cp="5F5C" data-bytes="98 7A">z</span> <span data-cp="5F83" data-bytes="8D 6C">l</span> <span data-cp="5FB1" data-bytes="9B 70">p</span> <span data-cp="5FBA" data-bytes="A0 51">Q</span> <span data-cp="5FC2" data-bytes="8C 7C">|</span> <span data-cp="5FC4" data-bytes="8B CB"></span> <span data-cp="5FDB" data-bytes="93 6E">n</span> <span data-cp="603B" data-bytes="89 66">f</span> <span data-cp="6062" data-bytes="9E A9"></span> <span data-cp="6075" data-bytes="93 7A">z</span> <span data-cp="6077" data-bytes="A0 E0"></span> <span data-cp="607E" data-bytes="93 6B">k</span> <span data-cp="60A4" data-bytes="A0 DC"></span> <span data-cp="60A7" data-bytes="94 68">h</span> <span data-cp="60D7" data-bytes="8D 71">q</span> <span data-cp="60DE" data-bytes="9B EC"></span> <span data-cp="60E3" data-bytes="99 BA"></span> <span data-cp="60E7" data-bytes="9A D0"></span> <span data-cp="60E8" data-bytes="9A 61">a</span> <span data-cp="60E9" data-bytes="A0 E5"></span> <span data-cp="60FD" data-bytes="A0 5B">[</span> <span data-cp="6107" data-bytes="96 AC"></span> <span data-cp="610C" data-bytes="97 40">@</span> <span data-cp="6119" data-bytes="9E F1"></span> <span data-cp="6122" data-bytes="8C 4D">M</span> <span data-cp="6130" data-bytes="9F 7E">~</span> <span data-cp="613D" data-bytes="8D 72">r</span> <span data-cp="6150" data-bytes="96 A9"></span> <span data-cp="6159" data-bytes="A0 6E">n</span> <span data-cp="616F" data-bytes="A0 74">t</span> <span data-cp="617D" data-bytes="A0 71">q</span> <span data-cp="6195" data-bytes="9C 50">P</span> <span data-cp="6198" data-bytes="93 79">y</span> <span data-cp="6199" data-bytes="93 78">x</span> <span data-cp="619C" data-bytes="A0 DD"></span> <span data-cp="61B7" data-bytes="8D 75">u</span> <span data-cp="61B9" data-bytes="8D 76">v</span> <span data-cp="61C0" data-bytes="93 74">t</span> <span data-cp="61CF" data-bytes="8D 77">w</span> <span data-cp="61DA" data-bytes="90 C3"></span> <span data-cp="61E2" data-bytes="A0 79">y</span> <span data-cp="622C" data-bytes="8D 79">y</span> <span data-cp="6237" data-bytes="8B FC"></span> <span data-cp="6239" data-bytes="A0 76">v</span> <span data-cp="624C" data-bytes="8B CD"></span> <span data-cp="6268" data-bytes="9F 5A">Z</span> <span data-cp="6282" data-bytes="9F F4"></span> <span data-cp="6285" data-bytes="9F BA"></span> <span data-cp="6290" data-bytes="8D 7A">z</span> <span data-cp="629D" data-bytes="9E 45">E</span> <span data-cp="62A4" data-bytes="93 B0"></span> <span data-cp="62A6" data-bytes="A0 75">u</span> <span data-cp="62C1" data-bytes="87 DD"></span> <span data-cp="62C3" data-bytes="9B 46">F</span> <span data-cp="62CE" data-bytes="A0 77">w</span> <span data-cp="62D0" data-bytes="9D C4"></span> <span data-cp="62E5" data-bytes="8D 7B">{</span> <span data-cp="6318" data-bytes="8D 7C">|</span> <span data-cp="632E" data-bytes="9E D6"></span> <span data-cp="6331" data-bytes="93 AC"></span> <span data-cp="6335" data-bytes="9F 5B">[</span> <span data-cp="6337" data-bytes="93 A9"></span> <span data-cp="6364" data-bytes="A0 7C">|</span> <span data-cp="6379" data-bytes="8A C1"></span> <span data-cp="637F" data-bytes="9F B4"></span> <span data-cp="63B9" data-bytes="9E 4C">L</span> <span data-cp="63C1" data-bytes="8F C5"></span> <span data-cp="63D1" data-bytes="93 AD"></span> <span data-cp="63DE" data-bytes="9D C3"></span> <span data-cp="63E2" data-bytes="8D A2"></span> <span data-cp="63E6" data-bytes="9D 4A">J</span> <span data-cp="63FB" data-bytes="8D A3"></span> <span data-cp="63FC" data-bytes="9E 4B">K</span> <span data-cp="63FE" data-bytes="9E 4D">M</span> <span data-cp="6407" data-bytes="8D A4"></span> <span data-cp="6432" data-bytes="8A FD"></span> <span data-cp="643B" data-bytes="93 B2"></span> <span data-cp="645A" data-bytes="8D A5"></span> <span data-cp="6471" data-bytes="93 A1"></span> <span data-cp="647C" data-bytes="8A C6"></span> <span data-cp="648D" data-bytes="8A 5B">[</span> <span data-cp="6491" data-bytes="89 4D">M</span> <span data-cp="64B4" data-bytes="8A 78">x</span> <span data-cp="64B6" data-bytes="93 AB"></span> <span data-cp="64C0" data-bytes="8D A7"></span> <span data-cp="64D3" data-bytes="9F 45">E</span> <span data-cp="64DD" data-bytes="8A 56">V</span> <span data-cp="64E7" data-bytes="8E E6"></span> <span data-cp="64EA" data-bytes="8A A4"></span> <span data-cp="650A" data-bytes="89 43">C</span> <span data-cp="6511" data-bytes="93 F3"></span> <span data-cp="651F" data-bytes="9E A2"></span> <span data-cp="6530" data-bytes="9D C7"></span> <span data-cp="6535" data-bytes="8B CE"></span> <span data-cp="656B" data-bytes="93 B3"></span> <span data-cp="6586" data-bytes="8D AC"></span> <span data-cp="6589" data-bytes="89 67">g</span> <span data-cp="658B" data-bytes="8C 7E">~</span> <span data-cp="65BE" data-bytes="9C F3"></span> <span data-cp="65D4" data-bytes="95 BB"></span> <span data-cp="65FF" data-bytes="8D AE"></span> <span data-cp="661E" data-bytes="93 DB"></span> <span data-cp="6630" data-bytes="93 D5"></span> <span data-cp="6648" data-bytes="9B 71">q</span> <span data-cp="664D" data-bytes="87 64">d</span> <span data-cp="6653" data-bytes="8D AF"></span> <span data-cp="6660" data-bytes="87 B5"></span> <span data-cp="6663" data-bytes="93 D8"></span> <span data-cp="666B" data-bytes="93 D3"></span> <span data-cp="667D" data-bytes="8E 76">v</span> <span data-cp="668E" data-bytes="93 D1"></span> <span data-cp="6692" data-bytes="8D B1"></span> <span data-cp="669A" data-bytes="98 59">Y</span> <span data-cp="66B6" data-bytes="9C BF"></span> <span data-cp="66BF" data-bytes="9B 72">r</span> <span data-cp="66CE" data-bytes="93 BE"></span> <span data-cp="66E7" data-bytes="8C DB"></span> <span data-cp="66F1" data-bytes="9D F1"></span> <span data-cp="670C" data-bytes="A0 BB"></span> <span data-cp="670E" data-bytes="9B 7E">~</span> <span data-cp="6716" data-bytes="8D B3"></span> <span data-cp="6719" data-bytes="8C 52">R</span> <span data-cp="671E" data-bytes="9A E8"></span> <span data-cp="6725" data-bytes="8E DC"></span> <span data-cp="6736" data-bytes="9C F9"></span> <span data-cp="6761" data-bytes="98 E7"></span> <span data-cp="676B" data-bytes="8C CA"></span> <span data-cp="676E" data-bytes="87 75">u</span> <span data-cp="6782" data-bytes="87 BA"></span> <span data-cp="678F" data-bytes="93 E5"></span> <span data-cp="67A0" data-bytes="9A 59">Y</span> <span data-cp="67A4" data-bytes="8D B5"></span> <span data-cp="67BF" data-bytes="8F 7D">}</span> <span data-cp="67D6" data-bytes="95 47">G</span> <span data-cp="67F9" data-bytes="92 50">P</span> <span data-cp="67FE" data-bytes="89 68">h</span> <span data-cp="6800" data-bytes="8D B6"></span> <span data-cp="6802" data-bytes="A0 7D">}</span> <span data-cp="6803" data-bytes="98 FC"></span> <span data-cp="6804" data-bytes="89 69">i</span> <span data-cp="6810" data-bytes="92 56">V</span> <span data-cp="681E" data-bytes="93 E8"></span> <span data-cp="6836" data-bytes="9C E3"></span> <span data-cp="6847" data-bytes="96 40">@</span> <span data-cp="684A" data-bytes="8D B8"></span> <span data-cp="6855" data-bytes="9B 4A">J</span> <span data-cp="6856" data-bytes="8F B9"></span> <span data-cp="6865" data-bytes="89 6A">j</span> <span data-cp="6884" data-bytes="8D B9"></span> <span data-cp="6888" data-bytes="91 7E">~</span> <span data-cp="6898" data-bytes="93 F4"></span> <span data-cp="68B6" data-bytes="93 E7"></span> <span data-cp="68B9" data-bytes="97 EF"></span> <span data-cp="68C5" data-bytes="96 A5"></span> <span data-cp="6909" data-bytes="8D BD"></span> <span data-cp="6918" data-bytes="9B A1"></span> <span data-cp="6919" data-bytes="8C A2"></span> <span data-cp="691A" data-bytes="9A B7"></span> <span data-cp="691B" data-bytes="8E FC"></span> <span data-cp="692C" data-bytes="9F A1"></span> <span data-cp="6943" data-bytes="8D BE"></span> <span data-cp="6946" data-bytes="89 A4"></span> <span data-cp="6955" data-bytes="9A D9"></span> <span data-cp="6964" data-bytes="8D C0"></span> <span data-cp="6967" data-bytes="97 F0"></span> <span data-cp="6972" data-bytes="93 B4"></span> <span data-cp="6980" data-bytes="9F A7"></span> <span data-cp="6985" data-bytes="8D C2"></span> <span data-cp="698A" data-bytes="99 B6"></span> <span data-cp="699F" data-bytes="8D C1"></span> <span data-cp="69A2" data-bytes="8E 46">F</span> <span data-cp="69B2" data-bytes="A0 D1"></span> <span data-cp="69C0" data-bytes="9F CA"></span> <span data-cp="69D1" data-bytes="92 CF"></span> <span data-cp="69D5" data-bytes="9C F4"></span> <span data-cp="69D6" data-bytes="8D C4"></span> <span data-cp="69E9" data-bytes="9B 4C">L</span> <span data-cp="6A03" data-bytes="9C DE"></span> <span data-cp="6A0C" data-bytes="98 6C">l</span> <span data-cp="6A1A" data-bytes="97 F9"></span> <span data-cp="6A1C" data-bytes="95 58">X</span> <span data-cp="6A29" data-bytes="87 B6"></span> <span data-cp="6A2B" data-bytes="98 5E">^</span> <span data-cp="6A2D" data-bytes="94 CD"></span> <span data-cp="6A33" data-bytes="93 EE"></span> <span data-cp="6A43" data-bytes="8C A3"></span> <span data-cp="6A4C" data-bytes="93 F5"></span> <span data-cp="6A52" data-bytes="93 EF"></span> <span data-cp="6A53" data-bytes="8E EA"></span> <span data-cp="6A57" data-bytes="8F 5B">[</span> <span data-cp="6A63" data-bytes="8C 5E">^</span> <span data-cp="6A65" data-bytes="8D C6"></span> <span data-cp="6A71" data-bytes="8D C8"></span> <span data-cp="6A74" data-bytes="8D C7"></span> <span data-cp="6A7A" data-bytes="93 F7"></span> <span data-cp="6A82" data-bytes="8D C9"></span> <span data-cp="6A8F" data-bytes="96 70">p</span> <span data-cp="6A99" data-bytes="8D CB"></span> <span data-cp="6AA7" data-bytes="8F 65">e</span> <span data-cp="6AAB" data-bytes="8D CD"></span> <span data-cp="6AB1" data-bytes="9D A8"></span> <span data-cp="6AB2" data-bytes="94 F9"></span> <span data-cp="6AB5" data-bytes="8D CE"></span> <span data-cp="6ABE" data-bytes="93 EA"></span> <span data-cp="6AC9" data-bytes="93 F0"></span> <span data-cp="6ACA" data-bytes="9F B6"></span> <span data-cp="6AD4" data-bytes="8D CF"></span> <span data-cp="6AD8" data-bytes="97 63">c</span> <span data-cp="6AF6" data-bytes="8D D0"></span> <span data-cp="6B05" data-bytes="93 F1"></span> <span data-cp="6B52" data-bytes="9F DB"></span> <span data-cp="6B57" data-bytes="93 F8"></span> <span data-cp="6B6F" data-bytes="8B F7"></span> <span data-cp="6B7A" data-bytes="8B CF"></span> <span data-cp="6B81" data-bytes="8D D1"></span> <span data-cp="6BC1" data-bytes="8D D2"></span> <span data-cp="6BEA" data-bytes="8D D3"></span> <span data-cp="6BFA" data-bytes="9F E7"></span> <span data-cp="6C1C" data-bytes="90 BD"></span> <span data-cp="6C31" data-bytes="9F D0"></span> <span data-cp="6C35" data-bytes="8B D0"></span> <span data-cp="6C39" data-bytes="9C AE"></span> <span data-cp="6C3A" data-bytes="8B D1"></span> <span data-cp="6C3D" data-bytes="8A DB"></span> <span data-cp="6C4A" data-bytes="9E FD"></span> <span data-cp="6C58" data-bytes="95 CE"></span> <span data-cp="6C75" data-bytes="8D D4"></span> <span data-cp="6C7F" data-bytes="8E E3"></span> <span data-cp="6C9F" data-bytes="90 76">v</span> <span data-cp="6CA2" data-bytes="98 C6"></span> <span data-cp="6CAA" data-bytes="8D D5"></span> <span data-cp="6CAF" data-bytes="97 D1"></span> <span data-cp="6CB2" data-bytes="9E B6"></span> <span data-cp="6CCE" data-bytes="A0 42">B</span> <span data-cp="6CDF" data-bytes="98 73">s</span> <span data-cp="6CEA" data-bytes="9F FC"></span> <span data-cp="6CFF" data-bytes="8C A5"></span> <span data-cp="6D02" data-bytes="8D D7"></span> <span data-cp="6D05" data-bytes="92 FB"></span> <span data-cp="6D06" data-bytes="8D D8"></span> <span data-cp="6D24" data-bytes="94 4F">O</span> <span data-cp="6D26" data-bytes="8D D9"></span> <span data-cp="6D4E" data-bytes="89 6B">k</span> <span data-cp="6D57" data-bytes="97 CE"></span> <span data-cp="6D67" data-bytes="94 47">G</span> <span data-cp="6D72" data-bytes="92 B7"></span> <span data-cp="6D81" data-bytes="8D DA"></span> <span data-cp="6D8F" data-bytes="9C 5A">Z</span> <span data-cp="6DA4" data-bytes="8D DC"></span> <span data-cp="6DA5" data-bytes="94 44">D</span> <span data-cp="6DB1" data-bytes="8D DD"></span> <span data-cp="6DB9" data-bytes="A0 D6"></span> <span data-cp="6DFE" data-bytes="8C 41">A</span> <span data-cp="6E02" data-bytes="97 D5"></span> <span data-cp="6E04" data-bytes="94 4A">J</span> <span data-cp="6E0A" data-bytes="94 4D">M</span> <span data-cp="6E0F" data-bytes="97 CB"></span> <span data-cp="6E15" data-bytes="8D DE"></span> <span data-cp="6E18" data-bytes="8D DF"></span> <span data-cp="6E29" data-bytes="8D E0"></span> <span data-cp="6E57" data-bytes="8C DD"></span> <span data-cp="6E76" data-bytes="92 B3"></span> <span data-cp="6E86" data-bytes="8D E1"></span> <span data-cp="6E8B" data-bytes="95 D3"></span> <span data-cp="6E9A" data-bytes="89 C1"></span> <span data-cp="6EB8" data-bytes="9C B7"></span> <span data-cp="6EBB" data-bytes="8D E3"></span> <span data-cp="6EDA" data-bytes="8D E5"></span> <span data-cp="6EDD" data-bytes="89 47">G</span> <span data-cp="6EE2" data-bytes="8D E4"></span> <span data-cp="6EE8" data-bytes="8D E7"></span> <span data-cp="6EE9" data-bytes="8D E8"></span> <span data-cp="6F0B" data-bytes="94 45">E</span> <span data-cp="6F0C" data-bytes="97 D6"></span> <span data-cp="6F17" data-bytes="98 44">D</span> <span data-cp="6F24" data-bytes="8D E9"></span> <span data-cp="6F34" data-bytes="8D EA"></span> <span data-cp="6F56" data-bytes="9D A7"></span> <span data-cp="6F79" data-bytes="95 D2"></span> <span data-cp="6F81" data-bytes="8D ED"></span> <span data-cp="6FB5" data-bytes="9C DC"></span> <span data-cp="6FB6" data-bytes="9B F6"></span> <span data-cp="6FBB" data-bytes="95 CF"></span> <span data-cp="6FBE" data-bytes="8D EE"></span> <span data-cp="6FD9" data-bytes="96 EC"></span> <span data-cp="6FDA" data-bytes="96 EB"></span> <span data-cp="6FF6" data-bytes="90 B6"></span> <span data-cp="7003" data-bytes="98 AB"></span> <span data-cp="701E" data-bytes="96 ED"></span> <span data-cp="702C" data-bytes="8D F4"></span> <span data-cp="704D" data-bytes="8C 67">g</span> <span data-cp="7050" data-bytes="8D F6"></span> <span data-cp="7054" data-bytes="8D F7"></span> <span data-cp="705C" data-bytes="8F FA"></span> <span data-cp="7067" data-bytes="97 D0"></span> <span data-cp="706C" data-bytes="8B D2"></span> <span data-cp="706E" data-bytes="87 DE"></span> <span data-cp="706F" data-bytes="8D F8"></span> <span data-cp="7075" data-bytes="90 D9"></span> <span data-cp="7077" data-bytes="8C 47">G</span> <span data-cp="707F" data-bytes="8D F9"></span> <span data-cp="7089" data-bytes="8D FA"></span> <span data-cp="708F" data-bytes="90 A6"></span> <span data-cp="70A0" data-bytes="99 70">p</span> <span data-cp="70A3" data-bytes="91 EB"></span> <span data-cp="70A5" data-bytes="97 70">p</span> <span data-cp="70A6" data-bytes="98 6F">o</span> <span data-cp="70B9" data-bytes="98 F2"></span> <span data-cp="70BB" data-bytes="9A FC"></span> <span data-cp="70BC" data-bytes="89 6C">l</span> <span data-cp="70C0" data-bytes="99 5E">^</span> <span data-cp="70C4" data-bytes="95 BD"></span> <span data-cp="70D0" data-bytes="91 E6"></span> <span data-cp="70F1" data-bytes="94 54">T</span> <span data-cp="70F5" data-bytes="99 B8"></span> <span data-cp="70FE" data-bytes="97 E9"></span> <span data-cp="7105" data-bytes="93 46">F</span> <span data-cp="711D" data-bytes="98 63">c</span> <span data-cp="7129" data-bytes="95 BC"></span> <span data-cp="7133" data-bytes="98 70">p</span> <span data-cp="7134" data-bytes="96 F6"></span> <span data-cp="7135" data-bytes="8E A9"></span> <span data-cp="713B" data-bytes="94 51">Q</span> <span data-cp="713E" data-bytes="8E 43">C</span> <span data-cp="7140" data-bytes="8B 5A">Z</span> <span data-cp="7151" data-bytes="9B F5"></span> <span data-cp="7157" data-bytes="8C EE"></span> <span data-cp="7162" data-bytes="A0 DF"></span> <span data-cp="716B" data-bytes="97 7E">~</span> <span data-cp="7171" data-bytes="9B D5"></span> <span data-cp="7173" data-bytes="9A C3"></span> <span data-cp="7175" data-bytes="97 C8"></span> <span data-cp="7176" data-bytes="A0 DB"></span> <span data-cp="7177" data-bytes="91 D0"></span> <span data-cp="717A" data-bytes="9F E4"></span> <span data-cp="717C" data-bytes="8F DD"></span> <span data-cp="717E" data-bytes="91 E9"></span> <span data-cp="7188" data-bytes="98 E0"></span> <span data-cp="718C" data-bytes="92 CA"></span> <span data-cp="718E" data-bytes="98 57">W</span> <span data-cp="7191" data-bytes="8C 51">Q</span> <span data-cp="7198" data-bytes="9B 49">I</span> <span data-cp="71A2" data-bytes="9D 76">v</span> <span data-cp="71A3" data-bytes="9E AF"></span> <span data-cp="71AD" data-bytes="9C CC"></span> <span data-cp="71B7" data-bytes="8D F1"></span> <span data-cp="71D1" data-bytes="8E 53">S</span> <span data-cp="71DF" data-bytes="9C 62">b</span> <span data-cp="71EB" data-bytes="96 F9"></span> <span data-cp="71F5" data-bytes="98 BF"></span> <span data-cp="71F6" data-bytes="9E 49">I</span> <span data-cp="7200" data-bytes="8C A7"></span> <span data-cp="7201" data-bytes="9B 76">v</span> <span data-cp="7209" data-bytes="9B CA"></span> <span data-cp="720F" data-bytes="92 DC"></span> <span data-cp="7216" data-bytes="91 CC"></span> <span data-cp="7217" data-bytes="91 E2"></span> <span data-cp="7225" data-bytes="87 5F">_</span> <span data-cp="722B" data-bytes="8B D3"></span> <span data-cp="7250" data-bytes="94 55">U</span> <span data-cp="725C" data-bytes="8D BF"></span> <span data-cp="7266" data-bytes="9E 78">x</span> <span data-cp="7287" data-bytes="94 56">V</span> <span data-cp="728F" data-bytes="9D 61">a</span> <span data-cp="7294" data-bytes="94 57">W</span> <span data-cp="729F" data-bytes="99 66">f</span> <span data-cp="72AD" data-bytes="8B D5"></span> <span data-cp="72B2" data-bytes="A0 69">i</span> <span data-cp="72CD" data-bytes="98 B4"></span> <span data-cp="72E2" data-bytes="A0 49">I</span> <span data-cp="7302" data-bytes="A0 4C">L</span> <span data-cp="7304" data-bytes="9E 65">e</span> <span data-cp="7310" data-bytes="98 B5"></span> <span data-cp="732A" data-bytes="99 75">u</span> <span data-cp="732C" data-bytes="A0 65">e</span> <span data-cp="7338" data-bytes="98 B7"></span> <span data-cp="7339" data-bytes="98 B8"></span> <span data-cp="7341" data-bytes="98 BA"></span> <span data-cp="7348" data-bytes="98 BB"></span> <span data-cp="734F" data-bytes="9F BC"></span> <span data-cp="7371" data-bytes="A0 4A">J</span> <span data-cp="7374" data-bytes="9E C7"></span> <span data-cp="738C" data-bytes="8C A9"></span> <span data-cp="738F" data-bytes="98 AE"></span> <span data-cp="7398" data-bytes="92 D6"></span> <span data-cp="739E" data-bytes="91 D4"></span> <span data-cp="73BA" data-bytes="8C 53">S</span> <span data-cp="73C4" data-bytes="87 BF"></span> <span data-cp="73D0" data-bytes="94 C5"></span> <span data-cp="73E1" data-bytes="98 C1"></span> <span data-cp="73E2" data-bytes="97 5C">\</span> <span data-cp="73E6" data-bytes="97 73">s</span> <span data-cp="73F3" data-bytes="97 64">d</span> <span data-cp="73F9" data-bytes="96 4E">N</span> <span data-cp="73FB" data-bytes="97 65">e</span> <span data-cp="7402" data-bytes="8C 5A">Z</span> <span data-cp="7411" data-bytes="89 A1"></span> <span data-cp="7412" data-bytes="95 FA"></span> <span data-cp="7414" data-bytes="92 D4"></span> <span data-cp="7419" data-bytes="98 C8"></span> <span data-cp="741C" data-bytes="90 EF"></span> <span data-cp="741E" data-bytes="98 C9"></span> <span data-cp="741F" data-bytes="98 CA"></span> <span data-cp="7437" data-bytes="94 6D">m</span> <span data-cp="7438" data-bytes="94 B7"></span> <span data-cp="743C" data-bytes="94 6B">k</span> <span data-cp="7443" data-bytes="92 FC"></span> <span data-cp="7445" data-bytes="95 EB"></span> <span data-cp="7448" data-bytes="97 6E">n</span> <span data-cp="744C" data-bytes="87 B8"></span> <span data-cp="7456" data-bytes="92 D5"></span> <span data-cp="7461" data-bytes="87 78">x</span> <span data-cp="7468" data-bytes="94 7A">z</span> <span data-cp="746B" data-bytes="95 FB"></span> <span data-cp="7479" data-bytes="92 D1"></span> <span data-cp="747A" data-bytes="94 5D">]</span> <span data-cp="748C" data-bytes="93 44">D</span> <span data-cp="748D" data-bytes="8E A6"></span> <span data-cp="7499" data-bytes="92 D3"></span> <span data-cp="749B" data-bytes="94 B8"></span> <span data-cp="749D" data-bytes="87 79">y</span> <span data-cp="74B4" data-bytes="97 5E">^</span> <span data-cp="74B9" data-bytes="8C AD"></span> <span data-cp="74C6" data-bytes="87 C1"></span> <span data-cp="74CC" data-bytes="94 6A">j</span> <span data-cp="74D0" data-bytes="93 E3"></span> <span data-cp="74D3" data-bytes="98 CF"></span> <span data-cp="74E7" data-bytes="A0 D9"></span> <span data-cp="74F0" data-bytes="A0 BF"></span> <span data-cp="74F1" data-bytes="A0 4D">M</span> <span data-cp="74F2" data-bytes="A0 B8"></span> <span data-cp="74F8" data-bytes="A0 CE"></span> <span data-cp="7505" data-bytes="A0 B7"></span> <span data-cp="7519" data-bytes="89 C3"></span> <span data-cp="7534" data-bytes="9D F4"></span> <span data-cp="7535" data-bytes="89 6D">m</span> <span data-cp="753B" data-bytes="9C 7B">{</span> <span data-cp="7542" data-bytes="98 D2"></span> <span data-cp="7546" data-bytes="9F A9"></span> <span data-cp="7551" data-bytes="97 D9"></span> <span data-cp="7553" data-bytes="A0 C4"></span> <span data-cp="7555" data-bytes="94 76">v</span> <span data-cp="7560" data-bytes="99 78">x</span> <span data-cp="756D" data-bytes="98 D3"></span> <span data-cp="7572" data-bytes="98 D4"></span> <span data-cp="757A" data-bytes="9F B9"></span> <span data-cp="7583" data-bytes="94 71">q</span> <span data-cp="758D" data-bytes="98 D5"></span> <span data-cp="75B1" data-bytes="9E 5C">\</span> <span data-cp="75C3" data-bytes="A0 44">D</span> <span data-cp="75C8" data-bytes="98 D7"></span> <span data-cp="75DC" data-bytes="98 D8"></span> <span data-cp="75F9" data-bytes="9E EF"></span> <span data-cp="7607" data-bytes="9F FE"></span> <span data-cp="763B" data-bytes="9D DD"></span> <span data-cp="7640" data-bytes="9E E1"></span> <span data-cp="764D" data-bytes="98 DA"></span> <span data-cp="764E" data-bytes="9D DF"></span> <span data-cp="7654" data-bytes="9E EB"></span> <span data-cp="7666" data-bytes="9E 59">Y</span> <span data-cp="7667" data-bytes="A0 5C">\</span> <span data-cp="7673" data-bytes="94 77">w</span> <span data-cp="7674" data-bytes="98 DC"></span> <span data-cp="767A" data-bytes="98 DE"></span> <span data-cp="76D6" data-bytes="9F C2"></span> <span data-cp="76D9" data-bytes="8C 6B">k</span> <span data-cp="770C" data-bytes="98 C4"></span> <span data-cp="770E" data-bytes="94 B0"></span> <span data-cp="770F" data-bytes="94 B1"></span> <span data-cp="7724" data-bytes="A0 C1"></span> <span data-cp="772B" data-bytes="A0 CD"></span> <span data-cp="7743" data-bytes="98 E5"></span> <span data-cp="7772" data-bytes="91 E4"></span> <span data-cp="7777" data-bytes="8F C7"></span> <span data-cp="7778" data-bytes="94 AE"></span> <span data-cp="777A" data-bytes="8A 4F">O</span> <span data-cp="777B" data-bytes="94 B2"></span> <span data-cp="7793" data-bytes="8F D4"></span> <span data-cp="7798" data-bytes="98 EA"></span> <span data-cp="77B9" data-bytes="9D E0"></span> <span data-cp="77BE" data-bytes="98 EE"></span> <span data-cp="77C3" data-bytes="95 C4"></span> <span data-cp="77CB" data-bytes="98 EF"></span> <span data-cp="77D7" data-bytes="9B 78">x</span> <span data-cp="77DD" data-bytes="8C 6E">n</span> <span data-cp="77FE" data-bytes="A0 AE"></span> <span data-cp="7808" data-bytes="9D 4C">L</span> <span data-cp="7818" data-bytes="98 F1"></span> <span data-cp="781C" data-bytes="98 F3"></span> <span data-cp="781E" data-bytes="94 C1"></span> <span data-cp="7839" data-bytes="98 F5"></span> <span data-cp="783D" data-bytes="96 E2"></span> <span data-cp="7842" data-bytes="94 50">P</span> <span data-cp="7844" data-bytes="96 A2"></span> <span data-cp="7847" data-bytes="98 F6"></span> <span data-cp="784B" data-bytes="96 E5"></span> <span data-cp="7851" data-bytes="98 F7"></span> <span data-cp="7853" data-bytes="A0 46">F</span> <span data-cp="7854" data-bytes="96 E3"></span> <span data-cp="7866" data-bytes="98 F8"></span> <span data-cp="787A" data-bytes="9E E4"></span> <span data-cp="7888" data-bytes="94 C3"></span> <span data-cp="788D" data-bytes="94 C2"></span> <span data-cp="78B6" data-bytes="96 E4"></span> <span data-cp="78B8" data-bytes="89 AC"></span> <span data-cp="78B9" data-bytes="96 DB"></span> <span data-cp="78D2" data-bytes="94 C4"></span> <span data-cp="78D8" data-bytes="9F FB"></span> <span data-cp="78E4" data-bytes="8C 59">Y</span> <span data-cp="78EE" data-bytes="93 C9"></span> <span data-cp="78F0" data-bytes="94 E8"></span> <span data-cp="78F5" data-bytes="90 C5"></span> <span data-cp="7906" data-bytes="A0 A8"></span> <span data-cp="7932" data-bytes="98 FD"></span> <span data-cp="7933" data-bytes="98 FB"></span> <span data-cp="7936" data-bytes="8E BF"></span> <span data-cp="793B" data-bytes="8B D8"></span> <span data-cp="7958" data-bytes="8F 68">h</span> <span data-cp="7959" data-bytes="94 C6"></span> <span data-cp="7962" data-bytes="9D EA"></span> <span data-cp="797E" data-bytes="9C DA"></span> <span data-cp="7983" data-bytes="9C 72">r</span> <span data-cp="7987" data-bytes="89 C9"></span> <span data-cp="7991" data-bytes="99 41">A</span> <span data-cp="7999" data-bytes="99 42">B</span> <span data-cp="799B" data-bytes="94 CA"></span> <span data-cp="799F" data-bytes="91 D7"></span> <span data-cp="79A5" data-bytes="94 CC"></span> <span data-cp="79C4" data-bytes="97 A8"></span> <span data-cp="79CA" data-bytes="8C DE"></span> <span data-cp="79D0" data-bytes="87 B3"></span> <span data-cp="79E2" data-bytes="96 D1"></span> <span data-cp="79E3" data-bytes="9C BD"></span> <span data-cp="79F1" data-bytes="94 D5"></span> <span data-cp="79F4" data-bytes="94 D0"></span> <span data-cp="7A06" data-bytes="99 44">D</span> <span data-cp="7A2A" data-bytes="8C 63">c</span> <span data-cp="7A2C" data-bytes="87 BB"></span> <span data-cp="7A2D" data-bytes="A0 B3"></span> <span data-cp="7A32" data-bytes="87 B4"></span> <span data-cp="7A3A" data-bytes="94 CF"></span> <span data-cp="7A3E" data-bytes="9F FA"></span> <span data-cp="7A43" data-bytes="91 E5"></span> <span data-cp="7A45" data-bytes="9C 6A">j</span> <span data-cp="7A49" data-bytes="8E 49">I</span> <span data-cp="7A65" data-bytes="8E 4C">L</span> <span data-cp="7A72" data-bytes="87 C9"></span> <span data-cp="7A7D" data-bytes="8E 4D">M</span> <span data-cp="7A83" data-bytes="9A 73">s</span> <span data-cp="7A91" data-bytes="99 47">G</span> <span data-cp="7A93" data-bytes="8C B1"></span> <span data-cp="7AB0" data-bytes="8E 50">P</span> <span data-cp="7ABB" data-bytes="8E 4F">O</span> <span data-cp="7ABC" data-bytes="99 49">I</span> <span data-cp="7AC2" data-bytes="8E 51">Q</span> <span data-cp="7AC3" data-bytes="8E 52">R</span> <span data-cp="7AC8" data-bytes="9A B2"></span> <span data-cp="7AC9" data-bytes="89 A5"></span> <span data-cp="7ACF" data-bytes="99 4C">L</span> <span data-cp="7AD3" data-bytes="9F F8"></span> <span data-cp="7ADA" data-bytes="8E 56">V</span> <span data-cp="7ADB" data-bytes="99 4D">M</span> <span data-cp="7ADC" data-bytes="91 CA"></span> <span data-cp="7ADD" data-bytes="8E 57">W</span> <span data-cp="7AE2" data-bytes="94 E1"></span> <span data-cp="7AE7" data-bytes="90 47">G</span> <span data-cp="7AE9" data-bytes="8F D8"></span> <span data-cp="7AEA" data-bytes="8E 58">X</span> <span data-cp="7AFC" data-bytes="87 A3"></span> <span data-cp="7AFE" data-bytes="94 EB"></span> <span data-cp="7B0B" data-bytes="8E 5C">\</span> <span data-cp="7B0C" data-bytes="95 53">S</span> <span data-cp="7B14" data-bytes="9F E5"></span> <span data-cp="7B1F" data-bytes="9F 56">V</span> <span data-cp="7B27" data-bytes="95 4F">O</span> <span data-cp="7B29" data-bytes="8E 5E">^</span> <span data-cp="7B39" data-bytes="99 6A">j</span> <span data-cp="7B42" data-bytes="9C 64">d</span> <span data-cp="7B43" data-bytes="9C D9"></span> <span data-cp="7B51" data-bytes="9D 5A">Z</span> <span data-cp="7B55" data-bytes="8E 5D">]</span> <span data-cp="7B62" data-bytes="99 50">P</span> <span data-cp="7B6C" data-bytes="99 51">Q</span> <span data-cp="7B6F" data-bytes="8E 62">b</span> <span data-cp="7B7B" data-bytes="99 52">R</span> <span data-cp="7B92" data-bytes="8E 68">h</span> <span data-cp="7BA2" data-bytes="8E 61">a</span> <span data-cp="7BA3" data-bytes="9F 59">Y</span> <span data-cp="7BAE" data-bytes="87 D0"></span> <span data-cp="7BB2" data-bytes="8B B3"></span> <span data-cp="7BB8" data-bytes="8E 69">i</span> <span data-cp="7BC5" data-bytes="87 B9"></span> <span data-cp="7BCF" data-bytes="9F 5D">]</span> <span data-cp="7BD0" data-bytes="8E 66">f</span> <span data-cp="7BEC" data-bytes="8C B2"></span> <span data-cp="7BFA" data-bytes="8E 6E">n</span> <span data-cp="7BFC" data-bytes="9F 64">d</span> <span data-cp="7C06" data-bytes="8E 6F">o</span> <span data-cp="7C12" data-bytes="99 53">S</span> <span data-cp="7C1B" data-bytes="99 54">T</span> <span data-cp="7C35" data-bytes="8E 70">p</span> <span data-cp="7C42" data-bytes="9F 61">a</span> <span data-cp="7C44" data-bytes="8E 72">r</span> <span data-cp="7C51" data-bytes="A0 6B">k</span> <span data-cp="7C56" data-bytes="9F 40">@</span> <span data-cp="7C5D" data-bytes="94 ED"></span> <span data-cp="7C6D" data-bytes="94 EE"></span> <span data-cp="7C70" data-bytes="9F BD"></span> <span data-cp="7C74" data-bytes="8E 7B">{</span> <span data-cp="7C7B" data-bytes="99 57">W</span> <span data-cp="7C7C" data-bytes="94 F7"></span> <span data-cp="7C7E" data-bytes="9F 5F">_</span> <span data-cp="7C83" data-bytes="8E 73">s</span> <span data-cp="7C86" data-bytes="9F 62">b</span> <span data-cp="7C8E" data-bytes="94 F6"></span> <span data-cp="7C9C" data-bytes="99 58">X</span> <span data-cp="7CA6" data-bytes="8E 75">u</span> <span data-cp="7CAC" data-bytes="90 72">r</span> <span data-cp="7CAE" data-bytes="94 F8"></span> <span data-cp="7CB8" data-bytes="99 5A">Z</span> <span data-cp="7CC2" data-bytes="A0 B0"></span> <span data-cp="7CC3" data-bytes="8C B3"></span> <span data-cp="7CC7" data-bytes="8E 79">y</span> <span data-cp="7CC9" data-bytes="8E 78">x</span> <span data-cp="7CCD" data-bytes="94 F3"></span> <span data-cp="7CCE" data-bytes="8E 7E">~</span> <span data-cp="7CD3" data-bytes="98 AF"></span> <span data-cp="7CDA" data-bytes="A0 B2"></span> <span data-cp="7CE6" data-bytes="8E 7A">z</span> <span data-cp="7CED" data-bytes="99 5C">\</span> <span data-cp="7CF3" data-bytes="8E 7C">|</span> <span data-cp="7CF5" data-bytes="8E 7D">}</span> <span data-cp="7CF9" data-bytes="8B D9"></span> <span data-cp="7CFC" data-bytes="89 A2"></span> <span data-cp="7D25" data-bytes="9E D7"></span> <span data-cp="7D4D" data-bytes="A0 B6"></span> <span data-cp="7D5A" data-bytes="9E 42">B</span> <span data-cp="7D5D" data-bytes="8E A4"></span> <span data-cp="7D89" data-bytes="8E A7"></span> <span data-cp="7D8B" data-bytes="8C 76">v</span> <span data-cp="7D95" data-bytes="87 67">g</span> <span data-cp="7D97" data-bytes="95 42">B</span> <span data-cp="7DA4" data-bytes="98 7D">}</span> <span data-cp="7DA8" data-bytes="97 55">U</span> <span data-cp="7DAB" data-bytes="8E A8"></span> <span data-cp="7DB3" data-bytes="8E AA"></span> <span data-cp="7DCD" data-bytes="89 A3"></span> <span data-cp="7DCF" data-bytes="99 60">`</span> <span data-cp="7DD0" data-bytes="99 62">b</span> <span data-cp="7DD2" data-bytes="8E AB"></span> <span data-cp="7DD3" data-bytes="94 FC"></span> <span data-cp="7DD4" data-bytes="99 61">a</span> <span data-cp="7DDC" data-bytes="94 FA"></span> <span data-cp="7DE4" data-bytes="8E AE"></span> <span data-cp="7DE5" data-bytes="8E B2"></span> <span data-cp="7DF5" data-bytes="8E B0"></span> <span data-cp="7DFD" data-bytes="99 63">c</span> <span data-cp="7DFE" data-bytes="97 AA"></span> <span data-cp="7E07" data-bytes="94 FB"></span> <span data-cp="7E1D" data-bytes="8E B4"></span> <span data-cp="7E27" data-bytes="8E BB"></span> <span data-cp="7E5B" data-bytes="8C DC"></span> <span data-cp="7E65" data-bytes="98 76">v</span> <span data-cp="7E67" data-bytes="8E A1"></span> <span data-cp="7E6C" data-bytes="8C B4"></span> <span data-cp="7E6E" data-bytes="8E B7"></span> <span data-cp="7E7F" data-bytes="9D A6"></span> <span data-cp="7E87" data-bytes="9B 7B">{</span> <span data-cp="7E8E" data-bytes="9E B0"></span> <span data-cp="7E92" data-bytes="8E B8"></span> <span data-cp="7E9F" data-bytes="9D 70">p</span> <span data-cp="7EA4" data-bytes="89 6E">n</span> <span data-cp="7EAC" data-bytes="89 6F">o</span> <span data-cp="7EBA" data-bytes="89 70">p</span> <span data-cp="7EC7" data-bytes="89 71">q</span> <span data-cp="7ECF" data-bytes="89 72">r</span> <span data-cp="7EDF" data-bytes="89 73">s</span> <span data-cp="7F06" data-bytes="89 74">t</span> <span data-cp="7F37" data-bytes="89 75">u</span> <span data-cp="7F40" data-bytes="8E BC"></span> <span data-cp="7F41" data-bytes="8E BD"></span> <span data-cp="7F47" data-bytes="8E BE"></span> <span data-cp="7F49" data-bytes="9D D1"></span> <span data-cp="7F4E" data-bytes="94 FD"></span> <span data-cp="7F52" data-bytes="8B D7"></span> <span data-cp="7F53" data-bytes="8B DA"></span> <span data-cp="7F71" data-bytes="A0 E2"></span> <span data-cp="7F78" data-bytes="9F E9"></span> <span data-cp="7F93" data-bytes="8A E7"></span> <span data-cp="7F97" data-bytes="8E C2"></span> <span data-cp="7FA3" data-bytes="8E C4"></span> <span data-cp="7FAE" data-bytes="99 64">d</span> <span data-cp="7FB4" data-bytes="99 65">e</span> <span data-cp="7FDD" data-bytes="95 4E">N</span> <span data-cp="7FE7" data-bytes="98 B3"></span> <span data-cp="7FFA" data-bytes="8E CB"></span> <span data-cp="8002" data-bytes="8B DF"></span> <span data-cp="8005" data-bytes="8E CD"></span> <span data-cp="8008" data-bytes="8E CE"></span> <span data-cp="801D" data-bytes="8E CF"></span> <span data-cp="8020" data-bytes="99 68">h</span> <span data-cp="8025" data-bytes="99 69">i</span> <span data-cp="8028" data-bytes="8E D0"></span> <span data-cp="802E" data-bytes="99 6B">k</span> <span data-cp="802F" data-bytes="8E D1"></span> <span data-cp="8031" data-bytes="99 6C">l</span> <span data-cp="803B" data-bytes="8E D4"></span> <span data-cp="803C" data-bytes="8E D5"></span> <span data-cp="8054" data-bytes="99 6D">m</span> <span data-cp="805B" data-bytes="A0 BE"></span> <span data-cp="8061" data-bytes="8E D6"></span> <span data-cp="8062" data-bytes="A0 BC"></span> <span data-cp="8063" data-bytes="A0 B5"></span> <span data-cp="8066" data-bytes="A0 B4"></span> <span data-cp="8080" data-bytes="8B E0"></span> <span data-cp="809F" data-bytes="89 B5"></span> <span data-cp="80A7" data-bytes="8E DD"></span> <span data-cp="80B6" data-bytes="9E 5D">]</span> <span data-cp="80B7" data-bytes="99 71">q</span> <span data-cp="80BC" data-bytes="89 AE"></span> <span data-cp="80BD" data-bytes="9D E8"></span> <span data-cp="80C6" data-bytes="95 65">e</span> <span data-cp="80E9" data-bytes="99 72">r</span> <span data-cp="80EC" data-bytes="8B 5C">\</span> <span data-cp="80F6" data-bytes="89 B1"></span> <span data-cp="8103" data-bytes="A0 C0"></span> <span data-cp="8107" data-bytes="8E DF"></span> <span data-cp="8109" data-bytes="95 66">f</span> <span data-cp="810C" data-bytes="99 74">t</span> <span data-cp="810E" data-bytes="99 76">v</span> <span data-cp="8112" data-bytes="99 77">w</span> <span data-cp="8114" data-bytes="99 79">y</span> <span data-cp="8117" data-bytes="9D DA"></span> <span data-cp="811A" data-bytes="8E E0"></span> <span data-cp="812A" data-bytes="93 5C">\</span> <span data-cp="8132" data-bytes="9D E6"></span> <span data-cp="8134" data-bytes="8B 5F">_</span> <span data-cp="8137" data-bytes="95 63">c</span> <span data-cp="8142" data-bytes="95 67">g</span> <span data-cp="8148" data-bytes="9D E3"></span> <span data-cp="8156" data-bytes="99 7C">|</span> <span data-cp="8159" data-bytes="99 7D">}</span> <span data-cp="815A" data-bytes="99 7E">~</span> <span data-cp="816C" data-bytes="8C FB"></span> <span data-cp="816D" data-bytes="8B 5B">[</span> <span data-cp="817C" data-bytes="99 A3"></span> <span data-cp="8184" data-bytes="99 A4"></span> <span data-cp="8193" data-bytes="99 A6"></span> <span data-cp="81A5" data-bytes="99 A8"></span> <span data-cp="81AA" data-bytes="8A BE"></span> <span data-cp="81B6" data-bytes="9E 61">a</span> <span data-cp="81C1" data-bytes="99 AA"></span> <span data-cp="81C8" data-bytes="A0 C8"></span> <span data-cp="81E4" data-bytes="99 AB"></span> <span data-cp="81F6" data-bytes="98 C2"></span> <span data-cp="8218" data-bytes="8E E8"></span> <span data-cp="821A" data-bytes="A0 BA"></span> <span data-cp="8229" data-bytes="8E EE"></span> <span data-cp="822D" data-bytes="9E BF"></span> <span data-cp="823E" data-bytes="89 C2"></span> <span data-cp="8254" data-bytes="99 AC"></span> <span data-cp="8262" data-bytes="95 6B">k</span> <span data-cp="8265" data-bytes="95 6C">l</span> <span data-cp="8276" data-bytes="99 AF"></span> <span data-cp="8279" data-bytes="99 4A">J</span> <span data-cp="827A" data-bytes="89 76">v</span> <span data-cp="827B" data-bytes="8F 48">H</span> <span data-cp="82A6" data-bytes="99 AE"></span> <span data-cp="82AA" data-bytes="8E FB"></span> <span data-cp="82BF" data-bytes="8C D0"></span> <span data-cp="82C4" data-bytes="8B 52">R</span> <span data-cp="82CA" data-bytes="99 B0"></span> <span data-cp="82CF" data-bytes="89 77">w</span> <span data-cp="82D0" data-bytes="8F 41">A</span> <span data-cp="82D8" data-bytes="99 B1"></span> <span data-cp="82E2" data-bytes="8F 49">I</span> <span data-cp="82F7" data-bytes="9D E4"></span> <span data-cp="82F8" data-bytes="8C B5"></span> <span data-cp="82FD" data-bytes="9B 54">T</span> <span data-cp="82FF" data-bytes="99 B2"></span> <span data-cp="830B" data-bytes="9E 68">h</span> <span data-cp="8318" data-bytes="8F 4A">J</span> <span data-cp="831A" data-bytes="8F 42">B</span> <span data-cp="831D" data-bytes="8F 51">Q</span> <span data-cp="833D" data-bytes="98 46">F</span> <span data-cp="8357" data-bytes="99 B4"></span> <span data-cp="8362" data-bytes="8E F5"></span> <span data-cp="8366" data-bytes="9C CD"></span> <span data-cp="836F" data-bytes="89 78">x</span> <span data-cp="8385" data-bytes="8F 53">S</span> <span data-cp="8391" data-bytes="8F 6F">o</span> <span data-cp="839C" data-bytes="8E 63">c</span> <span data-cp="83AC" data-bytes="8F 56">V</span> <span data-cp="83BE" data-bytes="9F C6"></span> <span data-cp="83C1" data-bytes="8F 57">W</span> <span data-cp="83CF" data-bytes="9C 77">w</span> <span data-cp="83D3" data-bytes="8F 58">X</span> <span data-cp="83ED" data-bytes="98 48">H</span> <span data-cp="8405" data-bytes="99 B7"></span> <span data-cp="840F" data-bytes="8F 6E">n</span> <span data-cp="8414" data-bytes="96 65">e</span> <span data-cp="8418" data-bytes="9D E7"></span> <span data-cp="841C" data-bytes="9E 62">b</span> <span data-cp="8420" data-bytes="96 CC"></span> <span data-cp="8421" data-bytes="8E 67">g</span> <span data-cp="8426" data-bytes="98 7E">~</span> <span data-cp="843E" data-bytes="97 FC"></span> <span data-cp="8448" data-bytes="98 F9"></span> <span data-cp="844A" data-bytes="8F 66">f</span> <span data-cp="8453" data-bytes="95 6E">n</span> <span data-cp="8455" data-bytes="92 45">E</span> <span data-cp="8458" data-bytes="8F 60">`</span> <span data-cp="845C" data-bytes="9E D1"></span> <span data-cp="8464" data-bytes="99 B9"></span> <span data-cp="8471" data-bytes="8F 62">b</span> <span data-cp="8472" data-bytes="97 4C">L</span> <span data-cp="847F" data-bytes="91 C7"></span> <span data-cp="8480" data-bytes="95 5F">_</span> <span data-cp="8484" data-bytes="87 AB"></span> <span data-cp="8488" data-bytes="99 BB"></span> <span data-cp="8492" data-bytes="8E 6D">m</span> <span data-cp="8493" data-bytes="8F 71">q</span> <span data-cp="8496" data-bytes="94 CB"></span> <span data-cp="84A3" data-bytes="95 B1"></span> <span data-cp="84A8" data-bytes="8F 69">i</span> <span data-cp="84AD" data-bytes="9A F2"></span> <span data-cp="84BD" data-bytes="96 C3"></span> <span data-cp="84BE" data-bytes="99 BD"></span> <span data-cp="84DA" data-bytes="A0 CF"></span> <span data-cp="84DE" data-bytes="8F 6D">m</span> <span data-cp="84E1" data-bytes="99 BE"></span> <span data-cp="84E2" data-bytes="8E F4"></span> <span data-cp="84E4" data-bytes="8F 72">r</span> <span data-cp="84E5" data-bytes="95 E4"></span> <span data-cp="84F8" data-bytes="99 BF"></span> <span data-cp="8503" data-bytes="92 42">B</span> <span data-cp="8504" data-bytes="87 D7"></span> <span data-cp="8510" data-bytes="99 C0"></span> <span data-cp="8534" data-bytes="8F 77">w</span> <span data-cp="8538" data-bytes="99 C1"></span> <span data-cp="854B" data-bytes="8F 40">@</span> <span data-cp="8552" data-bytes="99 C2"></span> <span data-cp="855A" data-bytes="8F 5C">\</span> <span data-cp="855F" data-bytes="8C BD"></span> <span data-cp="856F" data-bytes="99 C4"></span> <span data-cp="8570" data-bytes="99 C5"></span> <span data-cp="8573" data-bytes="8F 7B">{</span> <span data-cp="8593" data-bytes="87 76">v</span> <span data-cp="8597" data-bytes="8C B6"></span> <span data-cp="85C1" data-bytes="8F A3"></span> <span data-cp="85D6" data-bytes="8C CE"></span> <span data-cp="85E0" data-bytes="99 C6"></span> <span data-cp="85EE" data-bytes="96 CD"></span> <span data-cp="85FC" data-bytes="96 C7"></span> <span data-cp="8602" data-bytes="8F A5"></span> <span data-cp="860F" data-bytes="8C 61">a</span> <span data-cp="8610" data-bytes="95 70">p</span> <span data-cp="8613" data-bytes="87 AF"></span> <span data-cp="8614" data-bytes="93 68">h</span> <span data-cp="8616" data-bytes="8F 7E">~</span> <span data-cp="8628" data-bytes="8F AA"></span> <span data-cp="862F" data-bytes="A0 50">P</span> <span data-cp="8642" data-bytes="90 D3"></span> <span data-cp="8645" data-bytes="95 56">V</span> <span data-cp="866C" data-bytes="8F B8"></span> <span data-cp="8672" data-bytes="99 C8"></span> <span data-cp="867E" data-bytes="8F AF"></span> <span data-cp="8692" data-bytes="99 C9"></span> <span data-cp="86A0" data-bytes="95 79">y</span> <span data-cp="86AD" data-bytes="9F 49">I</span> <span data-cp="86B2" data-bytes="99 CA"></span> <span data-cp="86EF" data-bytes="99 CB"></span> <span data-cp="8770" data-bytes="9D D5"></span> <span data-cp="8771" data-bytes="8F B0"></span> <span data-cp="8786" data-bytes="9E 5F">_</span> <span data-cp="878B" data-bytes="99 CD"></span> <span data-cp="878C" data-bytes="A0 C9"></span> <span data-cp="87A5" data-bytes="9A DB"></span> <span data-cp="87A9" data-bytes="A0 C6"></span> <span data-cp="87B1" data-bytes="8F B4"></span> <span data-cp="87C1" data-bytes="A0 D7"></span> <span data-cp="87CE" data-bytes="A0 C7"></span> <span data-cp="87D6" data-bytes="A0 43">C</span> <span data-cp="87DA" data-bytes="8F B5"></span> <span data-cp="87EE" data-bytes="8F B2"></span> <span data-cp="87F5" data-bytes="A0 61">a</span> <span data-cp="8804" data-bytes="9E 5E">^</span> <span data-cp="880F" data-bytes="8F B6"></span> <span data-cp="8818" data-bytes="9F E8"></span> <span data-cp="8827" data-bytes="9C B2"></span> <span data-cp="882D" data-bytes="95 7C">|</span> <span data-cp="8842" data-bytes="9F C7"></span> <span data-cp="8845" data-bytes="8F BB"></span> <span data-cp="8846" data-bytes="8F BC"></span> <span data-cp="884F" data-bytes="8F EC"></span> <span data-cp="885E" data-bytes="8F C0"></span> <span data-cp="8860" data-bytes="93 6A">j</span> <span data-cp="8864" data-bytes="8B E4"></span> <span data-cp="8865" data-bytes="9C 7C">|</span> <span data-cp="886E" data-bytes="95 A1"></span> <span data-cp="8887" data-bytes="95 A3"></span> <span data-cp="888F" data-bytes="8C 45">E</span> <span data-cp="8890" data-bytes="8C B8"></span> <span data-cp="889C" data-bytes="8F C1"></span> <span data-cp="889D" data-bytes="87 B7"></span> <span data-cp="88A0" data-bytes="A0 52">R</span> <span data-cp="88AE" data-bytes="99 D0"></span> <span data-cp="88B4" data-bytes="8F C3"></span> <span data-cp="88B5" data-bytes="8F C4"></span> <span data-cp="88BF" data-bytes="95 A4"></span> <span data-cp="88C5" data-bytes="8F C6"></span> <span data-cp="88C7" data-bytes="9E 60">`</span> <span data-cp="88E6" data-bytes="95 A5"></span> <span data-cp="88F5" data-bytes="9C B3"></span> <span data-cp="88FF" data-bytes="99 D1"></span> <span data-cp="8924" data-bytes="99 D2"></span> <span data-cp="8943" data-bytes="9C C2"></span> <span data-cp="8947" data-bytes="99 D3"></span> <span data-cp="894D" data-bytes="95 A7"></span> <span data-cp="8954" data-bytes="95 A9"></span> <span data-cp="8965" data-bytes="95 A6"></span> <span data-cp="8977" data-bytes="9C 5D">]</span> <span data-cp="8980" data-bytes="98 E2"></span> <span data-cp="8987" data-bytes="8F C9"></span> <span data-cp="8989" data-bytes="A0 C2"></span> <span data-cp="898A" data-bytes="8F CA"></span> <span data-cp="8991" data-bytes="99 D4"></span> <span data-cp="8994" data-bytes="A0 B9"></span> <span data-cp="89A5" data-bytes="9B 58">X</span> <span data-cp="89A6" data-bytes="8F CB"></span> <span data-cp="89A7" data-bytes="8F CD"></span> <span data-cp="89A9" data-bytes="8F CC"></span> <span data-cp="89BC" data-bytes="8F CE"></span> <span data-cp="89C1" data-bytes="8B E5"></span> <span data-cp="89C6" data-bytes="89 79">y</span> <span data-cp="89E7" data-bytes="8F D0"></span> <span data-cp="8A1C" data-bytes="95 B6"></span> <span data-cp="8A29" data-bytes="99 D6"></span> <span data-cp="8A2B" data-bytes="95 E5"></span> <span data-cp="8A38" data-bytes="99 D7"></span> <span data-cp="8A3D" data-bytes="95 B5"></span> <span data-cp="8A49" data-bytes="A0 CA"></span> <span data-cp="8A67" data-bytes="9F FD"></span> <span data-cp="8A7E" data-bytes="A0 58">X</span> <span data-cp="8A90" data-bytes="8F D6"></span> <span data-cp="8A94" data-bytes="99 D8"></span> <span data-cp="8A9C" data-bytes="8F D3"></span> <span data-cp="8AA9" data-bytes="8F E5"></span> <span data-cp="8AAF" data-bytes="8F E9"></span> <span data-cp="8AB4" data-bytes="99 D9"></span> <span data-cp="8ACC" data-bytes="8C F7"></span> <span data-cp="8ADA" data-bytes="92 7C">|</span> <span data-cp="8AEA" data-bytes="9C 45">E</span> <span data-cp="8AF9" data-bytes="8C E8"></span> <span data-cp="8B0C" data-bytes="8F DE"></span> <span data-cp="8B1F" data-bytes="8F DF"></span> <span data-cp="8B2D" data-bytes="A0 4B">K</span> <span data-cp="8B3F" data-bytes="8F E2"></span> <span data-cp="8B43" data-bytes="A0 CC"></span> <span data-cp="8B4C" data-bytes="8F E3"></span> <span data-cp="8B4D" data-bytes="8F E4"></span> <span data-cp="8B5E" data-bytes="9B C4"></span> <span data-cp="8B62" data-bytes="9B FC"></span> <span data-cp="8B69" data-bytes="96 4C">L</span> <span data-cp="8B81" data-bytes="9A F6"></span> <span data-cp="8B83" data-bytes="8C AE"></span> <span data-cp="8B8F" data-bytes="87 CB"></span> <span data-cp="8B90" data-bytes="8F E7"></span> <span data-cp="8B9B" data-bytes="8F E8"></span> <span data-cp="8BA0" data-bytes="8B E7"></span> <span data-cp="8BBE" data-bytes="89 7A">z</span> <span data-cp="8BE2" data-bytes="89 7B">{</span> <span data-cp="8C51" data-bytes="99 DA"></span> <span data-cp="8C9B" data-bytes="8F ED"></span> <span data-cp="8C9F" data-bytes="95 C0"></span> <span data-cp="8CAD" data-bytes="A0 CB"></span> <span data-cp="8CCD" data-bytes="9E 48">H</span> <span data-cp="8CD4" data-bytes="99 DB"></span> <span data-cp="8CD6" data-bytes="8F F3"></span> <span data-cp="8CDB" data-bytes="8F F9"></span> <span data-cp="8CE9" data-bytes="95 C1"></span> <span data-cp="8CEB" data-bytes="A0 4E">N</span> <span data-cp="8CF2" data-bytes="99 DC"></span> <span data-cp="8CF7" data-bytes="A0 64">d</span> <span data-cp="8D03" data-bytes="8F F7"></span> <span data-cp="8D0B" data-bytes="89 B0"></span> <span data-cp="8D0C" data-bytes="A0 48">H</span> <span data-cp="8D11" data-bytes="8F FB"></span> <span data-cp="8D12" data-bytes="8F F6"></span> <span data-cp="8D18" data-bytes="9D DC"></span> <span data-cp="8D1C" data-bytes="99 DD"></span> <span data-cp="8D1D" data-bytes="8B E8"></span> <span data-cp="8D77" data-bytes="8F FE"></span> <span data-cp="8D7A" data-bytes="92 C1"></span> <span data-cp="8D82" data-bytes="9F D6"></span> <span data-cp="8DA6" data-bytes="A0 D2"></span> <span data-cp="8DA9" data-bytes="90 40">@</span> <span data-cp="8DC0" data-bytes="8A C4"></span> <span data-cp="8DC3" data-bytes="99 E0"></span> <span data-cp="8DD4" data-bytes="9F F0"></span> <span data-cp="8E01" data-bytes="9F F3"></span> <span data-cp="8E0E" data-bytes="9D BF"></span> <span data-cp="8E28" data-bytes="9F F6"></span> <span data-cp="8E2A" data-bytes="95 C8"></span> <span data-cp="8E2D" data-bytes="9E 5A">Z</span> <span data-cp="8E3A" data-bytes="99 E3"></span> <span data-cp="8E46" data-bytes="8A 4A">J</span> <span data-cp="8E4F" data-bytes="9F F1"></span> <span data-cp="8E68" data-bytes="8A A7"></span> <span data-cp="8E71" data-bytes="99 E6"></span> <span data-cp="8E75" data-bytes="9F F7"></span> <span data-cp="8E77" data-bytes="9F ED"></span> <span data-cp="8E7E" data-bytes="8A 5C">\</span> <span data-cp="8E80" data-bytes="9D AE"></span> <span data-cp="8EA7" data-bytes="95 C9"></span> <span data-cp="8EAD" data-bytes="90 48">H</span> <span data-cp="8EB0" data-bytes="99 E8"></span> <span data-cp="8EB6" data-bytes="90 49">I</span> <span data-cp="8EB9" data-bytes="8C BA"></span> <span data-cp="8EBC" data-bytes="90 B1"></span> <span data-cp="8EC3" data-bytes="90 4A">J</span> <span data-cp="8ECE" data-bytes="99 EA"></span> <span data-cp="8EDA" data-bytes="9B D1"></span> <span data-cp="8EE2" data-bytes="99 EB"></span> <span data-cp="8EE4" data-bytes="99 EC"></span> <span data-cp="8EED" data-bytes="99 ED"></span> <span data-cp="8EF2" data-bytes="99 EE"></span> <span data-cp="8F0B" data-bytes="9D 57">W</span> <span data-cp="8F19" data-bytes="90 4C">L</span> <span data-cp="8F2D" data-bytes="90 4D">M</span> <span data-cp="8F30" data-bytes="95 CB"></span> <span data-cp="8F36" data-bytes="9C 42">B</span> <span data-cp="8F41" data-bytes="97 E2"></span> <span data-cp="8F4A" data-bytes="95 CC"></span> <span data-cp="8F5C" data-bytes="9F 78">x</span> <span data-cp="8F66" data-bytes="89 7C">|</span> <span data-cp="8F67" data-bytes="89 7D">}</span> <span data-cp="8F6E" data-bytes="89 7E">~</span> <span data-cp="8F93" data-bytes="99 5D">]</span> <span data-cp="8FA0" data-bytes="9B 5A">Z</span> <span data-cp="8FA5" data-bytes="90 50">P</span> <span data-cp="8FA7" data-bytes="8C 4F">O</span> <span data-cp="8FB3" data-bytes="90 54">T</span> <span data-cp="8FB6" data-bytes="9A A8"></span> <span data-cp="8FB7" data-bytes="99 EF"></span> <span data-cp="8FB9" data-bytes="9D A3"></span> <span data-cp="8FBA" data-bytes="9D A1"></span> <span data-cp="8FBB" data-bytes="99 43">C</span> <span data-cp="8FBC" data-bytes="99 45">E</span> <span data-cp="8FBE" data-bytes="9D 7D">}</span> <span data-cp="8FC1" data-bytes="99 F0"></span> <span data-cp="8FCA" data-bytes="99 F1"></span> <span data-cp="8FCC" data-bytes="99 F2"></span> <span data-cp="8FCF" data-bytes="8C BC"></span> <span data-cp="8FD0" data-bytes="9D 60">`</span> <span data-cp="8FDA" data-bytes="A0 A3"></span> <span data-cp="8FF9" data-bytes="90 5B">[</span> <span data-cp="9008" data-bytes="9E DB"></span> <span data-cp="9012" data-bytes="9D 79">y</span> <span data-cp="9033" data-bytes="99 F3"></span> <span data-cp="9037" data-bytes="90 62">b</span> <span data-cp="9046" data-bytes="87 BD"></span> <span data-cp="904C" data-bytes="9F 55">U</span> <span data-cp="9056" data-bytes="9B F9"></span> <span data-cp="9061" data-bytes="90 65">e</span> <span data-cp="9064" data-bytes="96 E0"></span> <span data-cp="906C" data-bytes="98 BE"></span> <span data-cp="9097" data-bytes="95 D9"></span> <span data-cp="90A8" data-bytes="90 68">h</span> <span data-cp="90AE" data-bytes="90 6C">l</span> <span data-cp="90BB" data-bytes="95 D8"></span> <span data-cp="90C4" data-bytes="90 6A">j</span> <span data-cp="90FD" data-bytes="90 6D">m</span> <span data-cp="9104" data-bytes="9C 68">h</span> <span data-cp="9151" data-bytes="9F B2"></span> <span data-cp="9159" data-bytes="9F AE"></span> <span data-cp="915C" data-bytes="9F B0"></span> <span data-cp="915E" data-bytes="89 AD"></span> <span data-cp="9167" data-bytes="90 6E">n</span> <span data-cp="9170" data-bytes="9E 71">q</span> <span data-cp="9176" data-bytes="9E 4A">J</span> <span data-cp="917C" data-bytes="9F DC"></span> <span data-cp="918C" data-bytes="89 AB"></span> <span data-cp="918E" data-bytes="9F B8"></span> <span data-cp="91A9" data-bytes="90 70">p</span> <span data-cp="91B6" data-bytes="8B 63">c</span> <span data-cp="91BB" data-bytes="95 DC"></span> <span data-cp="91C4" data-bytes="90 71">q</span> <span data-cp="91D4" data-bytes="9B DE"></span> <span data-cp="91DF" data-bytes="89 49">I</span> <span data-cp="91E5" data-bytes="96 5B">[</span> <span data-cp="91F6" data-bytes="8C 50">P</span> <span data-cp="91FA" data-bytes="94 A6"></span> <span data-cp="91FE" data-bytes="8F D5"></span> <span data-cp="9208" data-bytes="9E 73">s</span> <span data-cp="920E" data-bytes="90 75">u</span> <span data-cp="9213" data-bytes="99 F7"></span> <span data-cp="9218" data-bytes="87 B2"></span> <span data-cp="9221" data-bytes="8C BF"></span> <span data-cp="9228" data-bytes="99 F9"></span> <span data-cp="922A" data-bytes="96 63">c</span> <span data-cp="922B" data-bytes="95 B9"></span> <span data-cp="9235" data-bytes="94 D4"></span> <span data-cp="9241" data-bytes="90 77">w</span> <span data-cp="9244" data-bytes="90 AB"></span> <span data-cp="9255" data-bytes="9D 4D">M</span> <span data-cp="9258" data-bytes="99 FA"></span> <span data-cp="925D" data-bytes="92 E3"></span> <span data-cp="925F" data-bytes="97 BB"></span> <span data-cp="9262" data-bytes="90 78">x</span> <span data-cp="926B" data-bytes="99 FB"></span> <span data-cp="926E" data-bytes="97 E0"></span> <span data-cp="9277" data-bytes="96 DC"></span> <span data-cp="9281" data-bytes="9C A8"></span> <span data-cp="9284" data-bytes="97 72">r</span> <span data-cp="9289" data-bytes="94 40">@</span> <span data-cp="928F" data-bytes="92 F2"></span> <span data-cp="92AE" data-bytes="99 FD"></span> <span data-cp="92B1" data-bytes="99 FC"></span> <span data-cp="92B9" data-bytes="90 7A">z</span> <span data-cp="92BA" data-bytes="96 4A">J</span> <span data-cp="92BE" data-bytes="96 D8"></span> <span data-cp="92BF" data-bytes="99 FE"></span> <span data-cp="92D4" data-bytes="90 4B">K</span> <span data-cp="92E3" data-bytes="9A 40">@</span> <span data-cp="92E5" data-bytes="97 5B">[</span> <span data-cp="92EB" data-bytes="9A 41">A</span> <span data-cp="92EC" data-bytes="91 DD"></span> <span data-cp="92F2" data-bytes="93 FC"></span> <span data-cp="92F3" data-bytes="9A 42">B</span> <span data-cp="92F4" data-bytes="9A 43">C</span> <span data-cp="92F6" data-bytes="96 59">Y</span> <span data-cp="92FD" data-bytes="9A 44">D</span> <span data-cp="9303" data-bytes="90 51">Q</span> <span data-cp="9307" data-bytes="94 BF"></span> <span data-cp="932C" data-bytes="90 A2"></span> <span data-cp="9330" data-bytes="9C AB"></span> <span data-cp="9331" data-bytes="97 76">v</span> <span data-cp="9342" data-bytes="94 A8"></span> <span data-cp="9343" data-bytes="9A 45">E</span> <span data-cp="9345" data-bytes="9D E1"></span> <span data-cp="9348" data-bytes="96 D9"></span> <span data-cp="935F" data-bytes="97 74">t</span> <span data-cp="9366" data-bytes="92 E5"></span> <span data-cp="9368" data-bytes="96 45">E</span> <span data-cp="9369" data-bytes="91 DA"></span> <span data-cp="936B" data-bytes="90 A3"></span> <span data-cp="936E" data-bytes="92 C8"></span> <span data-cp="9373" data-bytes="90 AF"></span> <span data-cp="9374" data-bytes="97 BF"></span> <span data-cp="9378" data-bytes="91 4C">L</span> <span data-cp="937D" data-bytes="96 7A">z</span> <span data-cp="9381" data-bytes="91 DE"></span> <span data-cp="9384" data-bytes="9A 46">F</span> <span data-cp="9386" data-bytes="97 79">y</span> <span data-cp="9387" data-bytes="94 6C">l</span> <span data-cp="9390" data-bytes="98 58">X</span> <span data-cp="939C" data-bytes="92 66">f</span> <span data-cp="93A0" data-bytes="93 FB"></span> <span data-cp="93AD" data-bytes="9A 47">G</span> <span data-cp="93B8" data-bytes="97 49">I</span> <span data-cp="93BB" data-bytes="97 48">H</span> <span data-cp="93BD" data-bytes="93 4A">J</span> <span data-cp="93BF" data-bytes="9C E2"></span> <span data-cp="93C6" data-bytes="92 64">d</span> <span data-cp="93CB" data-bytes="91 DF"></span> <span data-cp="93DB" data-bytes="96 D7"></span> <span data-cp="93E0" data-bytes="93 43">C</span> <span data-cp="93F3" data-bytes="91 DB"></span> <span data-cp="93F4" data-bytes="8C 6A">j</span> <span data-cp="9401" data-bytes="97 AF"></span> <span data-cp="9404" data-bytes="95 DD"></span> <span data-cp="9408" data-bytes="93 48">H</span> <span data-cp="9417" data-bytes="9A 4B">K</span> <span data-cp="941D" data-bytes="9A 4D">M</span> <span data-cp="9424" data-bytes="91 BC"></span> <span data-cp="9425" data-bytes="90 E2"></span> <span data-cp="9426" data-bytes="90 B4"></span> <span data-cp="9427" data-bytes="95 E1"></span> <span data-cp="942D" data-bytes="9A 4E">N</span> <span data-cp="942F" data-bytes="87 AD"></span> <span data-cp="943E" data-bytes="9A 4F">O</span> <span data-cp="944D" data-bytes="96 DD"></span> <span data-cp="9454" data-bytes="9A 51">Q</span> <span data-cp="9458" data-bytes="96 A7"></span> <span data-cp="945B" data-bytes="90 B0"></span> <span data-cp="9465" data-bytes="9C 4E">N</span> <span data-cp="9467" data-bytes="94 43">C</span> <span data-cp="946C" data-bytes="8E BA"></span> <span data-cp="9479" data-bytes="9A 52">R</span> <span data-cp="9485" data-bytes="8B E9"></span> <span data-cp="949F" data-bytes="9C AF"></span> <span data-cp="94A2" data-bytes="8B FD"></span> <span data-cp="94C1" data-bytes="9A BC"></span> <span data-cp="94C3" data-bytes="9A B8"></span> <span data-cp="94DC" data-bytes="9A AE"></span> <span data-cp="94F6" data-bytes="9A A7"></span> <span data-cp="952D" data-bytes="9A 53">S</span> <span data-cp="9547" data-bytes="9D 74">t</span> <span data-cp="9578" data-bytes="8B EA"></span> <span data-cp="957F" data-bytes="8B EB"></span> <span data-cp="9585" data-bytes="90 B2"></span> <span data-cp="9596" data-bytes="95 E9"></span> <span data-cp="9597" data-bytes="95 E8"></span> <span data-cp="9599" data-bytes="95 E6"></span> <span data-cp="95A0" data-bytes="90 B5"></span> <span data-cp="95A2" data-bytes="9A 54">T</span> <span data-cp="95A6" data-bytes="90 B3"></span> <span data-cp="95A7" data-bytes="95 E7"></span> <span data-cp="95AA" data-bytes="8B 50">P</span> <span data-cp="95E8" data-bytes="8B EC"></span> <span data-cp="95F4" data-bytes="9A 56">V</span> <span data-cp="961D" data-bytes="8B FB"></span> <span data-cp="9633" data-bytes="9A 57">W</span> <span data-cp="9638" data-bytes="A0 AA"></span> <span data-cp="9641" data-bytes="9F A6"></span> <span data-cp="9645" data-bytes="99 CC"></span> <span data-cp="9656" data-bytes="9C 59">Y</span> <span data-cp="9669" data-bytes="99 B5"></span> <span data-cp="967B" data-bytes="90 BE"></span> <span data-cp="9681" data-bytes="9F AF"></span> <span data-cp="968F" data-bytes="95 F2"></span> <span data-cp="9696" data-bytes="90 BF"></span> <span data-cp="96A3" data-bytes="90 C1"></span> <span data-cp="96B6" data-bytes="90 C4"></span> <span data-cp="96BD" data-bytes="90 C7"></span> <span data-cp="96F4" data-bytes="92 E4"></span> <span data-cp="9703" data-bytes="9F 52">R</span> <span data-cp="971B" data-bytes="90 DB"></span> <span data-cp="9721" data-bytes="A0 66">f</span> <span data-cp="9731" data-bytes="90 D2"></span> <span data-cp="9734" data-bytes="87 6B">k</span> <span data-cp="9736" data-bytes="90 D4"></span> <span data-cp="9740" data-bytes="9A 5B">[</span> <span data-cp="9741" data-bytes="95 FD"></span> <span data-cp="974A" data-bytes="87 B1"></span> <span data-cp="9751" data-bytes="8B C4"></span> <span data-cp="9755" data-bytes="8C 66">f</span> <span data-cp="9757" data-bytes="90 DE"></span> <span data-cp="975C" data-bytes="90 DC"></span> <span data-cp="975D" data-bytes="96 44">D</span> <span data-cp="975F" data-bytes="90 E1"></span> <span data-cp="976D" data-bytes="9E 46">F</span> <span data-cp="9771" data-bytes="96 51">Q</span> <span data-cp="9789" data-bytes="90 E6"></span> <span data-cp="979B" data-bytes="96 50">P</span> <span data-cp="979F" data-bytes="90 E7"></span> <span data-cp="97B1" data-bytes="90 E8"></span> <span data-cp="97B2" data-bytes="9A 5D">]</span> <span data-cp="97B4" data-bytes="9F 7A">z</span> <span data-cp="97B8" data-bytes="9B 5C">\</span> <span data-cp="97BA" data-bytes="9F 7C">|</span> <span data-cp="97BE" data-bytes="90 E9"></span> <span data-cp="97C0" data-bytes="90 EA"></span> <span data-cp="97C2" data-bytes="9A 5E">^</span> <span data-cp="97C8" data-bytes="9F 76">v</span> <span data-cp="97D2" data-bytes="90 EB"></span> <span data-cp="97E0" data-bytes="90 EC"></span> <span data-cp="97E6" data-bytes="8B EE"></span> <span data-cp="97EE" data-bytes="90 EE"></span> <span data-cp="97F2" data-bytes="91 C6"></span> <span data-cp="97F5" data-bytes="90 F2"></span> <span data-cp="97FF" data-bytes="90 F1"></span> <span data-cp="9815" data-bytes="8A 74">t</span> <span data-cp="981F" data-bytes="96 57">W</span> <span data-cp="9823" data-bytes="9C EF"></span> <span data-cp="982E" data-bytes="9F DF"></span> <span data-cp="9833" data-bytes="90 F7"></span> <span data-cp="9834" data-bytes="90 F6"></span> <span data-cp="9847" data-bytes="9B 5E">^</span> <span data-cp="984B" data-bytes="90 F8"></span> <span data-cp="9856" data-bytes="8C FC"></span> <span data-cp="9866" data-bytes="90 F9"></span> <span data-cp="9868" data-bytes="8C C9"></span> <span data-cp="9875" data-bytes="8B EF"></span> <span data-cp="98B4" data-bytes="9F E0"></span> <span data-cp="98B7" data-bytes="91 42">B</span> <span data-cp="98B9" data-bytes="9A 62">b</span> <span data-cp="98C3" data-bytes="95 69">i</span> <span data-cp="98C7" data-bytes="91 44">D</span> <span data-cp="98C8" data-bytes="91 43">C</span> <span data-cp="98CA" data-bytes="91 41">A</span> <span data-cp="98CE" data-bytes="8B F0"></span> <span data-cp="98DC" data-bytes="96 60">`</span> <span data-cp="98DE" data-bytes="8B F1"></span> <span data-cp="98E0" data-bytes="99 F6"></span> <span data-cp="98E1" data-bytes="91 49">I</span> <span data-cp="98E6" data-bytes="91 4A">J</span> <span data-cp="98EC" data-bytes="91 4B">K</span> <span data-cp="98F1" data-bytes="9A 64">d</span> <span data-cp="98F5" data-bytes="8A BF"></span> <span data-cp="990E" data-bytes="9A 66">f</span> <span data-cp="9919" data-bytes="9A 67">g</span> <span data-cp="991C" data-bytes="9A 69">i</span> <span data-cp="9937" data-bytes="9A 6A">j</span> <span data-cp="9938" data-bytes="96 52">R</span> <span data-cp="9939" data-bytes="91 4D">M</span> <span data-cp="993B" data-bytes="96 66">f</span> <span data-cp="9940" data-bytes="9F 7B">{</span> <span data-cp="9942" data-bytes="9A 6B">k</span> <span data-cp="994A" data-bytes="A0 6C">l</span> <span data-cp="994D" data-bytes="96 67">g</span> <span data-cp="995D" data-bytes="9A 6C">l</span> <span data-cp="9962" data-bytes="9A 6D">m</span> <span data-cp="9963" data-bytes="8B F2"></span> <span data-cp="999B" data-bytes="96 6A">j</span> <span data-cp="99AA" data-bytes="96 6C">l</span> <span data-cp="99B8" data-bytes="91 C4"></span> <span data-cp="99BC" data-bytes="96 77">w</span> <span data-cp="99C4" data-bytes="99 F4"></span> <span data-cp="99C5" data-bytes="9A 6F">o</span> <span data-cp="99D6" data-bytes="9B C6"></span> <span data-cp="99DA" data-bytes="9F AB"></span> <span data-cp="99E0" data-bytes="8C BE"></span> <span data-cp="99E1" data-bytes="8E C1"></span> <span data-cp="99E6" data-bytes="95 55">U</span> <span data-cp="99F5" data-bytes="91 52">R</span> <span data-cp="9A0C" data-bytes="91 53">S</span> <span data-cp="9A10" data-bytes="91 55">U</span> <span data-cp="9A1F" data-bytes="95 5D">]</span> <span data-cp="9A21" data-bytes="96 71">q</span> <span data-cp="9A26" data-bytes="9C 6D">m</span> <span data-cp="9A2F" data-bytes="96 73">s</span> <span data-cp="9A3B" data-bytes="91 54">T</span> <span data-cp="9A3C" data-bytes="9A 71">q</span> <span data-cp="9A58" data-bytes="91 56">V</span> <span data-cp="9A5C" data-bytes="96 6D">m</span> <span data-cp="9A63" data-bytes="95 57">W</span> <span data-cp="9A6C" data-bytes="89 C6"></span> <span data-cp="9A8F" data-bytes="89 C7"></span> <span data-cp="9AB2" data-bytes="8A 6A">j</span> <span data-cp="9AB6" data-bytes="8B 57">W</span> <span data-cp="9ABA" data-bytes="9F E1"></span> <span data-cp="9ABD" data-bytes="9B 5F">_</span> <span data-cp="9AD7" data-bytes="A0 5D">]</span> <span data-cp="9AE0" data-bytes="91 5B">[</span> <span data-cp="9AE2" data-bytes="91 5C">\</span> <span data-cp="9AF4" data-bytes="91 5E">^</span> <span data-cp="9AFF" data-bytes="9F 5C">\</span> <span data-cp="9B02" data-bytes="9F 57">W</span> <span data-cp="9B09" data-bytes="9F 65">e</span> <span data-cp="9B0F" data-bytes="9A 72">r</span> <span data-cp="9B14" data-bytes="91 60">`</span> <span data-cp="9B2A" data-bytes="9F 5E">^</span> <span data-cp="9B2D" data-bytes="91 61">a</span> <span data-cp="9B2E" data-bytes="9F 60">`</span> <span data-cp="9B34" data-bytes="91 64">d</span> <span data-cp="9B39" data-bytes="9F 41">A</span> <span data-cp="9B40" data-bytes="91 69">i</span> <span data-cp="9B50" data-bytes="91 68">h</span> <span data-cp="9B69" data-bytes="9A 74">t</span> <span data-cp="9B7F" data-bytes="96 B2"></span> <span data-cp="9B81" data-bytes="9A 75">u</span> <span data-cp="9B8B" data-bytes="9E E9"></span> <span data-cp="9B8D" data-bytes="8B BA"></span> <span data-cp="9B8E" data-bytes="91 6D">m</span> <span data-cp="9B8F" data-bytes="A0 60">`</span> <span data-cp="9B97" data-bytes="9F DE"></span> <span data-cp="9B9D" data-bytes="9F C3"></span> <span data-cp="9B9F" data-bytes="96 B5"></span> <span data-cp="9BB0" data-bytes="A0 67">g</span> <span data-cp="9BCF" data-bytes="96 B3"></span> <span data-cp="9BDD" data-bytes="9A 76">v</span> <span data-cp="9BE9" data-bytes="95 D5"></span> <span data-cp="9BED" data-bytes="9E CA"></span> <span data-cp="9BF1" data-bytes="9A 77">w</span> <span data-cp="9BF4" data-bytes="9A 78">x</span> <span data-cp="9BFF" data-bytes="91 70">p</span> <span data-cp="9C02" data-bytes="91 6F">o</span> <span data-cp="9C0A" data-bytes="9F A3"></span> <span data-cp="9C0C" data-bytes="91 71">q</span> <span data-cp="9C10" data-bytes="96 B1"></span> <span data-cp="9C15" data-bytes="9F 63">c</span> <span data-cp="9C1B" data-bytes="9F 67">g</span> <span data-cp="9C1F" data-bytes="8B B9"></span> <span data-cp="9C20" data-bytes="9A 7A">z</span> <span data-cp="9C26" data-bytes="8B 56">V</span> <span data-cp="9C2F" data-bytes="9A DA"></span> <span data-cp="9C35" data-bytes="96 B0"></span> <span data-cp="9C3A" data-bytes="9A 7E">~</span> <span data-cp="9C45" data-bytes="9D DE"></span> <span data-cp="9C4F" data-bytes="96 AD"></span> <span data-cp="9C53" data-bytes="96 AE"></span> <span data-cp="9C5D" data-bytes="9E A1"></span> <span data-cp="9C72" data-bytes="9E 50">P</span> <span data-cp="9C7B" data-bytes="96 AF"></span> <span data-cp="9C7C" data-bytes="8B F4"></span> <span data-cp="9D02" data-bytes="9F A4"></span> <span data-cp="9D0C" data-bytes="96 BD"></span> <span data-cp="9D16" data-bytes="96 F4"></span> <span data-cp="9D21" data-bytes="96 B8"></span> <span data-cp="9D39" data-bytes="91 A7"></span> <span data-cp="9D44" data-bytes="A0 5E">^</span> <span data-cp="9D49" data-bytes="9A 7D">}</span> <span data-cp="9D4E" data-bytes="89 48">H</span> <span data-cp="9D50" data-bytes="9E B1"></span> <span data-cp="9D5E" data-bytes="9D DB"></span> <span data-cp="9D6D" data-bytes="95 BF"></span> <span data-cp="9D6E" data-bytes="8A 73">s</span> <span data-cp="9D7C" data-bytes="9E FE"></span> <span data-cp="9D7E" data-bytes="91 7A">z</span> <span data-cp="9D83" data-bytes="91 7B">{</span> <span data-cp="9D93" data-bytes="9A A3"></span> <span data-cp="9DA5" data-bytes="96 C2"></span> <span data-cp="9DAB" data-bytes="9F 77">w</span> <span data-cp="9DBD" data-bytes="9A A4"></span> <span data-cp="9DC0" data-bytes="9A A5"></span> <span data-cp="9DC4" data-bytes="91 A1"></span> <span data-cp="9DC9" data-bytes="89 B8"></span> <span data-cp="9DD4" data-bytes="91 73">s</span> <span data-cp="9DF0" data-bytes="9C 6B">k</span> <span data-cp="9DFC" data-bytes="9A A6"></span> <span data-cp="9E0A" data-bytes="89 BD"></span> <span data-cp="9E0C" data-bytes="89 B9"></span> <span data-cp="9E0E" data-bytes="91 7D">}</span> <span data-cp="9E18" data-bytes="96 BB"></span> <span data-cp="9E1C" data-bytes="9F F2"></span> <span data-cp="9E1F" data-bytes="8B F5"></span> <span data-cp="9E7B" data-bytes="9A A9"></span> <span data-cp="9E81" data-bytes="9F 54">T</span> <span data-cp="9E84" data-bytes="9F E3"></span> <span data-cp="9E85" data-bytes="9E ED"></span> <span data-cp="9E90" data-bytes="91 AA"></span> <span data-cp="9E95" data-bytes="91 AB"></span> <span data-cp="9E96" data-bytes="A0 70">p</span> <span data-cp="9E98" data-bytes="9F 6D">m</span> <span data-cp="9E9E" data-bytes="91 AC"></span> <span data-cp="9EA2" data-bytes="91 AD"></span> <span data-cp="9EA6" data-bytes="A0 FD"></span> <span data-cp="9EA8" data-bytes="9F E2"></span> <span data-cp="9EAA" data-bytes="91 AF"></span> <span data-cp="9EAB" data-bytes="9E 41">A</span> <span data-cp="9EAC" data-bytes="9A AA"></span> <span data-cp="9EAF" data-bytes="91 B0"></span> <span data-cp="9EB1" data-bytes="9A AB"></span> <span data-cp="9EBD" data-bytes="9A AC"></span> <span data-cp="9EBF" data-bytes="9A 4A">J</span> <span data-cp="9EC1" data-bytes="91 B2"></span> <span data-cp="9EC4" data-bytes="8B F6"></span> <span data-cp="9EC6" data-bytes="9A AD"></span> <span data-cp="9EC7" data-bytes="89 B6"></span> <span data-cp="9EE2" data-bytes="9A AF"></span> <span data-cp="9EF1" data-bytes="9A B0"></span> <span data-cp="9EF8" data-bytes="9A B1"></span> <span data-cp="9EFE" data-bytes="9A A1"></span> <span data-cp="9F02" data-bytes="91 B9"></span> <span data-cp="9F08" data-bytes="91 BA"></span> <span data-cp="9F16" data-bytes="91 BF"></span> <span data-cp="9F17" data-bytes="91 BE"></span> <span data-cp="9F26" data-bytes="A0 41">A</span> <span data-cp="9F27" data-bytes="8B B7"></span> <span data-cp="9F39" data-bytes="91 C0"></span> <span data-cp="9F44" data-bytes="9A B3"></span> <span data-cp="9F45" data-bytes="91 C3"></span> <span data-cp="9F50" data-bytes="A0 FC"></span> <span data-cp="9F53" data-bytes="9F EE"></span> <span data-cp="9F5A" data-bytes="9F 69">i</span> <span data-cp="9F62" data-bytes="91 C8"></span> <span data-cp="9F69" data-bytes="91 C9"></span> <span data-cp="9F7F" data-bytes="8D E6"></span> <span data-cp="9F8E" data-bytes="91 CB"></span> <span data-cp="9F96" data-bytes="87 AC"></span> <span data-cp="9F97" data-bytes="87 A4"></span> <span data-cp="9F99" data-bytes="89 C8"></span> <span data-cp="9F9F" data-bytes="8D AA"></span> <span data-cp="9FA5" data-bytes="9F DD"></span> <span data-cp="9FA6" data-bytes="8C 43">C</span> <span data-cp="9FA7" data-bytes="8C 6D">m</span> <span data-cp="9FA8" data-bytes="8C 74">t</span> <span data-cp="9FA9" data-bytes="8C B7"></span> <span data-cp="9FAA" data-bytes="8C B9"></span> <span data-cp="9FAB" data-bytes="8C BB"></span> <span data-cp="9FAC" data-bytes="8C C0"></span> <span data-cp="9FAD" data-bytes="8C D7"></span> <span data-cp="9FAE" data-bytes="8C D8"></span> <span data-cp="9FAF" data-bytes="8C DA"></span> <span data-cp="9FB2" data-bytes="8C ED"></span> <span data-cp="9FB3" data-bytes="8D 48">H</span> <span data-cp="9FC7" data-bytes="87 C2"></span> <span data-cp="9FC8" data-bytes="87 D2"></span> <span data-cp="9FC9" data-bytes="87 D6"></span> <span data-cp="9FCA" data-bytes="87 DA"></span> <span data-cp="9FCB" data-bytes="87 DF"></span> <span data-cp="F907" data-bytes="8B F8"></span> </body></html>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/fetch.http.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/fetch.http.html
new file mode 100644
index 0000000000..d0cb7206f8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/fetch.http.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! Generated by `common/security-features/tools/generate.py --spec referrer-policy/4K-1/` -->
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/security-features/resources/common.sub.js"></script>
+ <script src="../../../../generic/test-case.sub.js"></script>
+ <script src="../../../generic/test-case.sub.js"></script>
+ </head>
+ <body>
+ <script>
+ TestCase(
+ [
+ {
+ "expectation": "stripped-referrer",
+ "origin": "cross-http",
+ "redirection": "keep-origin",
+ "source_context_list": [],
+ "source_scheme": "http",
+ "subresource": "fetch",
+ "subresource_policy_deliveries": [],
+ "test_description": "Referrer Policy: Expects stripped-referrer for fetch to cross-http origin and keep-origin redirection from http context."
+ },
+ {
+ "expectation": "stripped-referrer",
+ "origin": "cross-http",
+ "redirection": "no-redirect",
+ "source_context_list": [],
+ "source_scheme": "http",
+ "subresource": "fetch",
+ "subresource_policy_deliveries": [],
+ "test_description": "Referrer Policy: Expects stripped-referrer for fetch to cross-http origin and no-redirect redirection from http context."
+ },
+ {
+ "expectation": "stripped-referrer",
+ "origin": "cross-http",
+ "redirection": "swap-origin",
+ "source_context_list": [],
+ "source_scheme": "http",
+ "subresource": "fetch",
+ "subresource_policy_deliveries": [],
+ "test_description": "Referrer Policy: Expects stripped-referrer for fetch to cross-http origin and swap-origin redirection from http context."
+ },
+ {
+ "expectation": "stripped-referrer",
+ "origin": "cross-https",
+ "redirection": "keep-origin",
+ "source_context_list": [],
+ "source_scheme": "http",
+ "subresource": "fetch",
+ "subresource_policy_deliveries": [],
+ "test_description": "Referrer Policy: Expects stripped-referrer for fetch to cross-https origin and keep-origin redirection from http context."
+ },
+ {
+ "expectation": "stripped-referrer",
+ "origin": "cross-https",
+ "redirection": "no-redirect",
+ "source_context_list": [],
+ "source_scheme": "http",
+ "subresource": "fetch",
+ "subresource_policy_deliveries": [],
+ "test_description": "Referrer Policy: Expects stripped-referrer for fetch to cross-https origin and no-redirect redirection from http context."
+ },
+ {
+ "expectation": "stripped-referrer",
+ "origin": "cross-https",
+ "redirection": "swap-origin",
+ "source_context_list": [],
+ "source_scheme": "http",
+ "subresource": "fetch",
+ "subresource_policy_deliveries": [],
+ "test_description": "Referrer Policy: Expects stripped-referrer for fetch to cross-https origin and swap-origin redirection from http context."
+ },
+ {
+ "expectation": "stripped-referrer",
+ "origin": "same-http",
+ "redirection": "keep-origin",
+ "source_context_list": [],
+ "source_scheme": "http",
+ "subresource": "fetch",
+ "subresource_policy_deliveries": [],
+ "test_description": "Referrer Policy: Expects stripped-referrer for fetch to same-http origin and keep-origin redirection from http context."
+ },
+ {
+ "expectation": "stripped-referrer",
+ "origin": "same-http",
+ "redirection": "no-redirect",
+ "source_context_list": [],
+ "source_scheme": "http",
+ "subresource": "fetch",
+ "subresource_policy_deliveries": [],
+ "test_description": "Referrer Policy: Expects stripped-referrer for fetch to same-http origin and no-redirect redirection from http context."
+ },
+ {
+ "expectation": "stripped-referrer",
+ "origin": "same-http",
+ "redirection": "swap-origin",
+ "source_context_list": [],
+ "source_scheme": "http",
+ "subresource": "fetch",
+ "subresource_policy_deliveries": [],
+ "test_description": "Referrer Policy: Expects stripped-referrer for fetch to same-http origin and swap-origin redirection from http context."
+ },
+ {
+ "expectation": "stripped-referrer",
+ "origin": "same-https",
+ "redirection": "keep-origin",
+ "source_context_list": [],
+ "source_scheme": "http",
+ "subresource": "fetch",
+ "subresource_policy_deliveries": [],
+ "test_description": "Referrer Policy: Expects stripped-referrer for fetch to same-https origin and keep-origin redirection from http context."
+ },
+ {
+ "expectation": "stripped-referrer",
+ "origin": "same-https",
+ "redirection": "no-redirect",
+ "source_context_list": [],
+ "source_scheme": "http",
+ "subresource": "fetch",
+ "subresource_policy_deliveries": [],
+ "test_description": "Referrer Policy: Expects stripped-referrer for fetch to same-https origin and no-redirect redirection from http context."
+ },
+ {
+ "expectation": "stripped-referrer",
+ "origin": "same-https",
+ "redirection": "swap-origin",
+ "source_context_list": [],
+ "source_scheme": "http",
+ "subresource": "fetch",
+ "subresource_policy_deliveries": [],
+ "test_description": "Referrer Policy: Expects stripped-referrer for fetch to same-https origin and swap-origin redirection from http context."
+ }
+ ],
+ new SanityChecker()
+ ).start();
+ </script>
+ <div id="log"></div>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/filter-turbulence-invalid-001.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/filter-turbulence-invalid-001.html
new file mode 100644
index 0000000000..7400c8b379
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/filter-turbulence-invalid-001.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<title>CSS Filter Effects: feTurbulence with negative values from baseFrequency</title>
+<link rel="help" href="https://drafts.fxtf.org/filter-effects/#element-attrdef-feturbulence-basefrequency">
+<link rel="help" href="https://crbug.com/1068863"/>
+<link rel="match" href="reference/filter-turbulence-invalid-001-ref.html">
+<meta name="assert" content="This test checks that negative baseFrequency values are unsupported for feTurbulence.">
+<style>
+.target {
+ display: inline-block;
+ width: 100px;
+ height: 100px;
+ background-color: red;
+}
+</style>
+<div class="target" style="filter: url(#fn1)"></div>
+<div class="target" style="filter: url(#fn2)"></div>
+<div class="target" style="filter: url(#tb1)"></div>
+<div class="target" style="filter: url(#tb2)"></div>
+<svg height="0" color-interpolation-filters="sRGB">
+ <!-- type=fractalNoise -->
+ <filter id="fn1" x="0" y="0" width="1" height="1">
+ <feTurbulence type="fractalNoise" baseFrequency="-1 1"/>
+ <feComponentTransfer>
+ <feFuncR type="discrete" tableValues="1 0 1"/>
+ <feFuncG type="discrete" tableValues="0 0.502 0"/> <!-- map [0..1/3] -> 0; [1/3...2/3] -> 0.502; [2/3..1] -> 0 -->
+ <feFuncB type="discrete" tableValues="0"/>
+ <feFuncA type="discrete" tableValues="0 1 0"/>
+ </feComponentTransfer>
+ </filter>
+
+ <filter id="fn2" x="0" y="0" width="1" height="1">
+ <feTurbulence type="fractalNoise" baseFrequency="1 -1"/>
+ <feComponentTransfer>
+ <feFuncR type="discrete" tableValues="1 0 1"/>
+ <feFuncG type="discrete" tableValues="0 0.502 0"/> <!-- map [0..1/3] -> 0; [1/3...2/3] -> 0.502; [2/3..1] -> 0 -->
+ <feFuncB type="discrete" tableValues="0"/>
+ <feFuncA type="discrete" tableValues="0 1 0"/>
+ </feComponentTransfer>
+ </filter>
+
+ <!-- type=turbulence -->
+ <filter id="tb1" x="0" y="0" width="1" height="1">
+ <feTurbulence type="turbulence" baseFrequency="-1 1"/>
+ <feColorMatrix values="1 0 0 0 0, 0 1 0 0 0.502, 0 0 1 0 0, 0 0 0 1 1"/>
+ </filter>
+
+ <filter id="tb2" x="0" y="0" width="1" height="1">
+ <feTurbulence type="turbulence" baseFrequency="1 -1"/>
+ <feColorMatrix values="1 0 0 0 0, 0 1 0 0 0.502, 0 0 1 0 0, 0 0 0 1 1"/>
+ </filter>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/grid-auto-fill-rows-001.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/grid-auto-fill-rows-001.html
new file mode 100644
index 0000000000..afce3f5fa9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/grid-auto-fill-rows-001.html
@@ -0,0 +1,184 @@
+<!DOCTYPE html>
+<title>CSS Grid: auto-fill rows</title>
+<link rel="author" title="Sergio Villar" href="mailto: svillar@igalia.com">
+<link rel="help" href="https://drafts.csswg.org/css-grid-1/#valdef-repeat-auto-fill">
+<link rel="help" href="https://drafts.csswg.org/css-grid-1/#propdef-grid-auto-columns">
+<link rel="help" href="https://drafts.csswg.org/css-grid-1/#propdef-grid-auto-rows">
+<link rel="help" href="https://drafts.csswg.org/css-grid-1/#propdef-grid-template-rows">
+<link rel="help" href="https://drafts.csswg.org/css-grid-1/#propdef-grid-row">
+<link rel="help" href="https://drafts.csswg.org/css-align-3/#propdef-grid-row-gap">
+<link rel="help" href="https://crbug.com/619930">
+<link rel="help" href="https://crbug.com/589460">
+<link rel="help" href="https://crbug.com/648814">
+<meta name="assert" content="Check that auto-fill rows are properly computed in a grid container"/>
+<link href="/css/support/grid.css" rel="stylesheet">
+<style>
+
+.grid {
+ border: 2px solid magenta;
+ height: 200px;
+ width: 25px;
+ align-content: start;
+ grid-auto-rows: 157px;
+ grid-auto-columns: 25px;
+
+ float: left;
+ position: relative;
+ margin-right: 2px;
+}
+
+.gridOnlyAutoRepeat { grid-template-rows: repeat(auto-fill, 30px [autobar]); }
+.gridAutoRepeatAndFixedBefore { grid-template-rows: 10px [foo] 20% [bar] repeat(auto-fill, [autofoo] 35px); }
+.gridAutoRepeatAndFixedAfter { grid-template-rows: repeat(auto-fill, [first] 30px [last]) [foo] minmax(60px, 80px) [bar] minmax(45px, max-content); }
+.gridAutoRepeatAndFixed { grid-template-rows: [start] repeat(2, 50px [a]) [middle] repeat(auto-fill, [autofoo] 15px [autobar]) minmax(5%, 10%) [end]; }
+.gridMultipleNames { grid-template-rows: [start] 20px [foo] 50% repeat(auto-fill, [bar] 20px [start foo]) [foo] 10% [end bar]; }
+.gridMultipleTracks { grid-template-rows: [start] 20px repeat(auto-fill, [a] 2em [b c] 10% [d]) [e] minmax(75px, 1fr) [last]; }
+
+.item { background-color: blue; }
+.item:nth-child(2) { background: green; }
+.item:nth-child(3) { background: orange; }
+
+.gap { grid-row-gap: 20px; }
+
+</style>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/check-layout-th.js"></script>
+
+<body onload="checkLayout('.grid')">
+<div id="log"></div>
+
+<p>This test checks that repeat(auto-fill, ) syntax works as expected.</p>
+
+<div class="grid gridOnlyAutoRepeat">
+ <div class="item" style="grid-row: 1 / span 6" data-offset-y="0" data-offset-x="0" data-expected-height="180" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridOnlyAutoRepeat">
+ <div class="item" style="grid-row: 1 / span 6 autobar" data-offset-y="0" data-offset-x="0" data-expected-height="180" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridOnlyAutoRepeat gap">
+ <div class="item" style="grid-row: 1 / span 5" data-offset-y="0" data-offset-x="0" data-expected-height="357" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridOnlyAutoRepeat gap">
+ <div class="item" style="grid-row: autobar 2 / span 3" data-offset-y="100" data-offset-x="0" data-expected-height="257" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridOnlyAutoRepeat gap" style="height: auto; max-height: 90px;" data-expected-height="94" data-expected-width="29">
+ <div class="item" data-offset-y="0" data-offset-x="0" data-expected-height="30" data-expected-width="25"></div>
+ <div class="item" data-offset-y="50" data-offset-x="0" data-expected-height="30" data-expected-width="25"></div>
+ <div class="item" data-offset-y="100" data-offset-x="0" data-expected-height="157" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridOnlyAutoRepeat gap" style="height: auto; max-height: 90px; min-height: 130px;" data-expected-height="134" data-expected-width="29">
+ <div class="item" data-offset-y="0" data-offset-x="0" data-expected-height="30" data-expected-width="25"></div>
+ <div class="item" data-offset-y="50" data-offset-x="0" data-expected-height="30" data-expected-width="25"></div>
+ <div class="item" data-offset-y="100" data-offset-x="0" data-expected-height="30" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixedBefore">
+ <div class="item" style="grid-row: 1 / span 6" data-offset-y="0" data-offset-x="0" data-expected-height="190" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixedBefore">
+ <div class="item" style="grid-row: foo / autofoo" data-offset-y="10" data-offset-x="0" data-expected-height="40" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixedBefore">
+ <div class="item" style="grid-row: bar / 5 autofoo" data-offset-y="50" data-offset-x="0" data-expected-height="297" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixedBefore gap">
+ <div class="item" style="grid-row: 1 / span 4" data-offset-y="0" data-offset-x="0" data-expected-height="180" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixedBefore gap">
+ <div class="item" style="grid-row: span 3 / 2 autofoo" data-offset-y="0" data-offset-x="0" data-expected-height="125" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixedBefore gap">
+ <div class="item" style="grid-row: notPresent / 3 autofoo" data-offset-y="377" data-offset-x="0" data-expected-height="157" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixedAfter">
+ <div class="item" style="grid-row: 1 / span 4" data-offset-y="0" data-offset-x="0" data-expected-height="185" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixedAfter">
+ <div class="item" style="grid-row: first / last 2" data-offset-y="0" data-offset-x="0" data-expected-height="60" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixedAfter">
+ <div class="item" style="grid-row: last 2 / foo" data-offset-y="60" data-offset-x="0" data-expected-height="80" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixedAfter gap">
+ <div class="item" style="grid-row: 1 / span 3" data-offset-y="0" data-offset-x="0" data-expected-height="195" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixedAfter gap">
+ <div class="item" style="grid-row: 3 / span 1 bar" data-offset-y="130" data-offset-x="0" data-expected-height="222" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixedAfter gap">
+ <div class="item" style="grid-row: first / foo" data-offset-y="0" data-offset-x="0" data-expected-height="30" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixed">
+ <div class="item" style="grid-row: 1 / span 8" data-offset-y="0" data-offset-x="0" data-expected-height="195" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixed">
+ <div class="item" style="grid-row: a / autobar 2" data-offset-y="50" data-offset-x="0" data-expected-height="80" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixed">
+ <div class="item" style="grid-row: autofoo / end" data-offset-y="100" data-offset-x="0" data-expected-height="95" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixed gap">
+ <div class="item" style="grid-row: 1 / span 4" data-offset-y="0" data-offset-x="0" data-expected-height="195" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridAutoRepeatAndFixed gap">
+ <div class="item" style="grid-row: autobar / -1" data-offset-y="175" data-offset-x="0" data-expected-height="20" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridMultipleNames">
+ <div class="item" style="grid-row: 1 / -1" data-offset-y="0" data-offset-x="0" data-expected-height="200" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridMultipleNames">
+ <div class="item" style="grid-row: foo 3 / 4 bar" data-offset-y="160" data-offset-x="0" data-expected-height="40" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridMultipleNames">
+ <div class="item" style="grid-row: -6 / span 2 start" data-offset-y="20" data-offset-x="0" data-expected-height="140" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridMultipleNames gap">
+ <div class="item" style="grid-row: -4 / -2" data-offset-y="40" data-offset-x="0" data-expected-height="140" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridMultipleNames gap">
+ <div class="item" style="grid-row: bar / foo 2" data-offset-y="160" data-offset-x="0" data-expected-height="20" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridMultipleNames gap">
+ <div class="item" style="grid-row: foo / bar 2" data-offset-y="40" data-offset-x="0" data-expected-height="180" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridMultipleTracks">
+ <div class="item" style="grid-row: a / 2 c" data-offset-y="20" data-offset-x="0" data-expected-height="84" data-expected-width="25"></div>
+ <div class="item" style="grid-row: 3 / e; grid-column: 2;" data-offset-y="52" data-offset-x="25" data-expected-height="72" data-expected-width="25"></div>
+</div>
+
+<div class="grid gridMultipleTracks gap">
+ <div class="item" style="grid-row: a / c" data-offset-y="40" data-offset-x="0" data-expected-height="32" data-expected-width="25"></div>
+ <div class="item" style="grid-row: 3 / last; grid-column: 2;" data-offset-y="92" data-offset-x="25" data-expected-height="115" data-expected-width="25"></div>
+</div>
+
+</body>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/image-orientation-from-image-content-images-ref.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/image-orientation-from-image-content-images-ref.html
new file mode 100644
index 0000000000..c0d29909f9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/image-orientation-from-image-content-images-ref.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>CSS Images Module Level 3: image-orientation: from-image for content images</title>
+<link rel="author" title="Stephen Chenney" href="mailto:schenney@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-images-3/#propdef-image-orientation">
+<style>
+ div.image {
+ display: inline-block;
+ }
+ div.container {
+ display: inline-block;
+ width: 100px;
+ vertical-align: top;
+ }
+ img {
+ width: 100px;
+ height: 100px;
+ background-repeat: no-repeat;
+ }
+ body {
+ overflow: hidden;
+ }
+</style>
+</head>
+<body >
+ <p>The images should rotate respecting their EXIF orientation because
+ image-orientation: from-image is specified.
+ </p>
+ <div class="container">
+ <div class="image" style="content: url(../support/exif-orientation-1-ul-pre-rotated.jpg)"></div>
+ <br>Normal
+ </div>
+ <div class="container">
+ <div class="image" style="content: url(../support/exif-orientation-2-ur-pre-rotated.jpg)"></div>
+ <br>Flipped horizontally
+ </div>
+ <div class="container">
+ <div class="image" style="content: url(../support/exif-orientation-3-lr-pre-rotated.jpg)"></div>
+ <br>Rotated 180&deg;
+ </div>
+ <div class="container">
+ <div class="image" style="content: url(../support/exif-orientation-4-lol-pre-rotated.jpg)"></div>
+ <br>Flipped vertically
+ </div>
+ <br>
+ <div class="container">
+ <div class="image" style="content: url(../support/exif-orientation-5-lu-pre-rotated.jpg)"></div>
+ <br>Rotated 90&deg; CCW and flipped vertically
+ </div>
+ <div class="container">
+ <div class="image" style="content: url(../support/exif-orientation-6-ru-pre-rotated.jpg)"></div>
+ <br>Rotated 90&deg; CW
+ </div>
+ <div class="container">
+ <div class="image" style="content: url(../support/exif-orientation-7-rl-pre-rotated.jpg)"></div>
+ <br>Rotated 90&deg; CW and flipped vertically
+ </div>
+ <div class="container">
+ <div class="image" style="content: url(../support/exif-orientation-8-llo-pre-rotated.jpg)"></div>
+ <br>Rotated 90&deg; CCW
+ </div>
+ <br>
+ <div class="container">
+ <img style="background-image: url(../support/exif-orientation-5-lu-pre-rotated.jpg)"></img>
+ <br>Rotated 90&deg; CCW and flipped vertically
+ </div>
+ <div class="container">
+ <img style="background-image: url(../support/exif-orientation-6-ru-pre-rotated.jpg)"></img>
+ <br>Rotated 90&deg; CW
+ </div>
+ <div class="container">
+ <img style="background-image: url(../support/exif-orientation-7-rl-pre-rotated.jpg)"></img>
+ <br>Rotated 90&deg; CW and flipped vertically
+ </div>
+ <div class="container">
+ <img style="background-image: url(../support/exif-orientation-8-llo-pre-rotated.jpg)"></img>
+ <br>Rotated 90&deg; CCW
+ </div>
+ <br>
+ <div class="container">
+ <div class="image" style="content: url(../support/exif-orientation-9-u-pre-rotated.jpg)"></div>
+ <br>Undefined (invalid value)
+ </div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/masonry-item-placement-006.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/masonry-item-placement-006.html
new file mode 100644
index 0000000000..0082d72df2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/masonry-item-placement-006.html
@@ -0,0 +1,149 @@
+<!DOCTYPE HTML>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html><head>
+ <meta charset="utf-8">
+ <title>CSS Grid Test: Masonry item placement</title>
+ <link rel="author" title="Mats Palmgren" href="mailto:mats@mozilla.com">
+ <link rel="help" href="https://drafts.csswg.org/css-grid-2">
+ <link rel="match" href="masonry-item-placement-006-ref.html">
+ <style>
+html,body {
+ color:black; background-color:white; font:15px/1 monospace; padding:0; margin:0;
+}
+
+grid {
+ display: inline-grid;
+ gap: 1px 2px;
+ grid-template-columns: repeat(4,20px);
+ grid-template-rows: masonry;
+ color: #444;
+ border: 1px solid;
+ padding: 2px;
+}
+
+item {
+ background-color: #444;
+ color: #fff;
+}
+.next > grid {
+ masonry-auto-flow: next;
+}
+</style>
+</head>
+<body>
+
+<grid>
+ <item style="padding-top:30px">1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>5</item>
+ <item>6</item>
+</grid>
+
+<grid>
+ <item>1</item>
+ <item style="padding-top:30px">2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>5</item>
+ <item>6</item>
+</grid>
+
+<grid>
+ <item style="padding-top:30px">1</item>
+ <item style="padding-top:30px">2</item>
+ <item style="padding-top:10px">3</item>
+ <item>4</item>
+ <item>5</item>
+ <item>6</item>
+</grid>
+
+<grid>
+ <item>1</item>
+ <item style="padding-top:30px">2</item>
+ <item style="padding-top:10px">3</item>
+ <item style="grid-column:span 2">4</item>
+ <item>5</item>
+ <item>6</item>
+</grid>
+
+<grid>
+ <item>1</item>
+ <item style="padding-top:30px">2</item>
+ <item style="padding-top:10px">3</item>
+ <item style="grid-column:span 3">4</item>
+ <item>5</item>
+ <item>6</item>
+</grid>
+
+<grid>
+ <item>1</item>
+ <item style="padding-top:30px">2</item>
+ <item style="padding-top:10px">3</item>
+ <item style="grid-column:span 4">4</item>
+ <item>5</item>
+ <item>6</item>
+</grid>
+
+<span class="next">
+<grid>
+ <item style="padding-top:30px">1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>5</item>
+ <item>6</item>
+</grid>
+
+<grid>
+ <item>1</item>
+ <item style="padding-top:30px">2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>5</item>
+ <item>6</item>
+</grid>
+
+<grid>
+ <item style="padding-top:30px">1</item>
+ <item style="padding-top:30px">2</item>
+ <item style="padding-top:10px">3</item>
+ <item>4</item>
+ <item>5</item>
+ <item>6</item>
+</grid>
+
+<grid>
+ <item>1</item>
+ <item style="padding-top:30px">2</item>
+ <item style="padding-top:10px">3</item>
+ <item style="grid-column:span 2">4</item>
+ <item>5</item>
+ <item>6</item>
+</grid>
+
+<grid>
+ <item>1</item>
+ <item style="padding-top:30px">2</item>
+ <item style="padding-top:10px">3</item>
+ <item style="grid-column:span 3">4</item>
+ <item>5</item>
+ <item>6</item>
+</grid>
+
+<grid>
+ <item>1</item>
+ <item style="padding-top:30px">2</item>
+ <item style="padding-top:10px">3</item>
+ <item style="grid-column:span 4">4</item>
+ <item>5</item>
+ <item>6</item>
+</grid>
+</span>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/moz-css21-table-page-break-inside-avoid-2.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/moz-css21-table-page-break-inside-avoid-2.html
new file mode 100644
index 0000000000..cc6a55933f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/moz-css21-table-page-break-inside-avoid-2.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html lang="en-US" class="reftest-paged">
+<head>
+ <title>CSS Test: CSS 2.1 page-break-inside:avoid</title>
+ <link rel="author" title="Mats Palmgren" href="https://bugzilla.mozilla.org/show_bug.cgi?id=685012">
+ <link rel="help" href="http://www.w3.org/TR/CSS21/page.html#propdef-page-break-inside">
+ <link rel="match" href="moz-css21-table-page-break-inside-avoid-2-ref.html">
+ <meta name="flags" content="paged">
+<style type="text/css">
+@page { size:5in 3in; margin:0.5in; }
+p { height: 1in; width: 1in; margin:0; background-color:blue; }
+.test { page-break-inside:avoid; }
+</style>
+</head>
+<body>
+<table border="1">
+<tbody>
+<tr><td><p>1</p></td></tr>
+</tbody>
+</table>
+<div style= "page-break-after: always"></div>
+<table border="1" class="test">
+<tbody></tbody>
+<tbody>
+<tr><td><p>2</p><p>3</p></td></tr>
+</tbody>
+</table>
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/position-sticky-table-th-bottom-ref.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/position-sticky-table-th-bottom-ref.html
new file mode 100644
index 0000000000..2aa5c08a55
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/position-sticky-table-th-bottom-ref.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<title>Reference for position:sticky bottom constraint should behave correctly for &lt;th&gt; elements</title>
+
+<style>
+.group {
+ display: inline-block;
+ position: relative;
+ width: 150px;
+ height: 200px;
+}
+
+.scroller {
+ position: relative;
+ width: 100px;
+ height: 150px;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.contents {
+ height: 550px;
+}
+
+.indicator {
+ position: absolute;
+ background-color: green;
+ left: 0;
+ height: 50px;
+ width: 50px;
+}
+</style>
+
+<script>
+window.addEventListener('load', function() {
+ document.getElementById('scroller1').scrollTop = 0;
+ document.getElementById('scroller2').scrollTop = 75;
+ document.getElementById('scroller3').scrollTop = 200;
+});
+</script>
+
+<div class="group">
+ <div id="scroller1" class="scroller">
+ <div class="indicator" style="top: 100px;"></div>
+ <div class="contents"></div>
+ </div>
+</div>
+
+<div class="group">
+ <div id="scroller2" class="scroller">
+ <div class="indicator" style="top: 150px;"></div>
+ <div class="contents"></div>
+ </div>
+</div>
+
+<div class="group">
+ <div id="scroller3" class="scroller">
+ <div class="indicator" style="top: 250px;"></div>
+ <div class="contents"></div>
+ </div>
+</div>
+
+<div>You should see three green boxes above. No red should be visible.</div>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/pre-float-001.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/pre-float-001.html
new file mode 100644
index 0000000000..8dd08d8099
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/pre-float-001.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<title>CSS test preserved spaces and floats interaction</title>
+<link rel="author" title="Koji Ishii" href="kojii@chromium.org">
+<link rel="match" href="reference/pre-float-001-ref.html">
+<link rel="help" href="https://drafts.csswg.org/css-text-3/#white-space-property">
+<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" />
+<style>
+html {
+ font-family: Ahem;
+ font-size: 20px;
+ line-height: 1;
+}
+.container {
+ white-space: pre;
+ width: 10ch;
+ margin-bottom: 1em;
+}
+.float {
+ float: left;
+ width: 3ch;
+ height: 2em;
+ background: orange;
+}
+</style>
+<body>
+ <div class="float"></div>
+ <div class="container">123456 <br>123456</div>
+ <div class="float"></div>
+ <div class="container">1234567 <br>1234567</div>
+ <div class="float"></div>
+ <div class="container">1234567 <br>1234567</div>
+ <div class="float"></div>
+ <div class="container">1234567 <br>1234567</div>
+ <div class="float"></div>
+ <div class="container">12345678 <br>12345678</div>
+</body>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/resize-004.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/resize-004.html
new file mode 100644
index 0000000000..3a1f561749
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/resize-004.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Basic User Interface Test: resize initial value - none</title>
+<link rel="author" title="Intel" href="http://www.intel.com/">
+<link rel="author" title="Shiyou Tan" href="mailto:shiyoux.tan@intel.com">
+<link rel="help" title="8.1. 'resize' property" href="http://www.w3.org/TR/css3-ui/#resize">
+<meta name="flags" content="interact">
+<meta name="assert" content="Test checks that the resize property initial value is none">
+<style>
+ #test {
+ border: 2px solid blue;
+ height: 100px;
+ overflow: auto;
+ width: 100px;
+ }
+</style>
+<body>
+ <p>Test passes if <strong>neither</strong> the height <strong>nor</strong> the width of the blue border square can be adjusted(for instance by dragging the bottom-right corner).</p>
+ <div id="test"></div>
+</body>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/test-plan.src.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/test-plan.src.html
new file mode 100644
index 0000000000..c29f268837
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/test-plan.src.html
@@ -0,0 +1,1616 @@
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Compositing and Blending Test Plan</title>
+ <meta charset='utf-8'>
+ <script src='http://www.w3.org/Tools/respec/respec-w3c-common'
+ async class='remove'></script>
+ <script class='remove'>
+ var respecConfig = {
+ specStatus: "unofficial",
+ shortName: "compositing-1-test-plan",
+ editors: [
+ {
+ name: "Mirela Budaes",
+ company: "Adobe Systems, Inc.",
+ companyURL: "http://www.adobe.com/"
+ },
+ {
+ name: "Horia Olaru",
+ company: "Adobe Systems, Inc.",
+ companyURL: "http://www.adobe.com/"
+ },
+ {
+ name: "Mihai Tica",
+ company: "Adobe Systems, Inc.",
+ companyURL: "http://www.adobe.com/"
+ },
+
+ ]
+ };
+ </script>
+ <style>
+ table
+ {
+ border-collapse:collapse;
+ }
+ table, td, th
+ {
+ border:1px solid black;
+ padding: 13px;
+ }
+ table
+ {
+ width: 100%;
+ }
+ img
+ {
+ width: 400px;
+ }
+ </style>
+ </head>
+ <body>
+ <section id='abstract'>
+ <p>
+ This document is intended to be used as a guideline for the testing
+ activities related to the Compositing and Blending spec [[!compositing-1]]. Its main
+ goal is to provide an overview of the general testing areas and an informative
+ description of possible test cases.
+ </p>
+ <p>
+ This document is not meant to replace the spec in determining the
+ normative and non-normative assertions to be tested, but rather
+ complement it.
+ </p>
+ </section>
+ <section>
+ <h2>Goals</h2>
+ <section>
+ <h3>Providing guidance on testing</h3>
+ <p>
+ In order to increase the quality of the test contributions, this
+ document offers a set of test cases description for conducting testing (see
+ <a href="#test-cases-description" class="sectionRef"></a>).
+ </p>
+ </section>
+ <section>
+ <h3>Creating automation-friendly tests</h3>
+ <p>
+ In terms of actual tests produced for the CSS Compositing and Blending, the main goal
+ is to ensure that most tests are automatable (i.e. they're either
+ reftests or use <code>testharness.js</code>). Even where manual tests
+ are absolutely necessary they should be written so that they can be
+ easily automated &ndash; as there are on-going efforts to make
+ WebDriver [[webdriver]] automated tests a first class citizen in W3C
+ testing. This means that even if a manual test requires user
+ interaction, the validation or PASS/FAIL conditions should still be
+ clear enough as to allow automatic validation if said interaction is
+ later automated.
+ </p>
+ </section>
+ </section>
+ <section>
+ <h2>Approach</h2>
+ <p>
+ Since CSS blending has only three new CSS properties,
+ the approach is to deep dive into every aspect of the spec as much as possible.
+
+ Tests will be created for the testing areas listed in <a href="#testig-areas" class="sectionRef"></a>
+ and having as guidance the test cases description from <a href="#test-cases-description" class="sectionRef"></a>.
+ </p>
+ </section>
+ <section>
+ <h2>Testing areas</h2>
+ <section>
+ <h3>Explicit testing areas</h3>
+ <p>
+ These testing areas cover things explicitly defined in the normative sections of the Blending and Compositing spec. Please note that while detailed, this list is not necessarily
+ exhaustive and some normative behaviors may not be contained in it.
+ When in doubt, consult the Blending and Compositing spec or ask a question on the
+ <a href="http://lists.w3.org/Archives/Public/www-style/">mailing
+ list</a>.
+ </p>
+ <p>Below is the list of explicit testing areas:</p>
+ <ol>
+ <li>Proper parsing of the CSS properties and rendering according to the spec
+ <ul><code>mix-blend-mode</code></ul>
+ <ul><code>isolation</code></ul>
+ <ul><code>background-blend-mode</code></ul>
+ </li>
+ <li>SVG blending</li>
+ <li>Canvas 2D blending</li>
+ </ol>
+ </section>
+ <section>
+ <h3>Implicit testing areas</h3>
+ <p>
+ These are testing areas either normatively defined in other specs
+ that explicitly refer to the Blending and Compositing spec (e.g. [[!css3-transforms]])
+ or simply not explicitly defined, but implied by various aspects of
+ the spec (e.g. processing model, CSS 2.1 compliance, etc.).
+ Please note that while detailed, this list is not necessarily
+ exhaustive and some normative behaviors may not be contained in it.
+ When in doubt, consult the Blending and Compositing spec or ask a question on the
+ <a href="http://lists.w3.org/Archives/Public/www-style/">mailing
+ list</a>.
+ </p>
+ <p>Below is the list of implicit testing areas:</p>
+ <ol>
+ <li>Blending different types of elements
+ <ul>
+ <li><code>&lt;video&gt;</code></li>
+ <li><code>&lt;canvas&gt;</code></li>
+ <li><code>&lt;table&gt;</code></li>
+ </ul>
+ </li>
+ <li>Blending elements with specific style rules applied
+ <ul>
+ <li><code>transforms</code></li>
+ <li><code>transitions</code> </li>
+ <li><code>animations</code> </li>
+ </ul>
+ </li>
+ </ol>
+ </section>
+ </section>
+ <section>
+ <h2>Test cases description</h2>
+ <section>
+ <h3>Test cases for <code>mix-blend-mode</code></h3>
+ <p>
+ The following diagram describes a list of notations to be used later on in the document as well as the general document structure the test cases will follow. The test cases should not be limited to this structure. This should be a wireframe and people are encouraged to come up with complex test cases as well.
+ </p>
+ <p>
+ <img id="test_outline" src="test_template.png" alt="Mix-blend-mode sample elements">
+ </p>
+ <p>The intended structure of the document is the following:</p>
+ <pre>
+&lt;body&gt;
+ &lt;div id="[P]"&gt;
+ &lt;div id="[IN-S]"&gt;&lt;/div&gt;
+ &lt;div id="[IN-P]"&gt;
+ &lt;div id="[B]"&gt;
+ &lt;div id="[CB]"&gt;&lt;/div&gt;
+ &lt;/div&gt;
+ &lt;/div&gt;
+ &lt;/div&gt;
+&lt;/body&gt;
+ </pre>
+ <p> Unless otherwise stated, test cases assume the following properties for the elements: <br>
+ <ul>
+ <li> default value for the <code>background-color</code> of the <code>body</code></li>
+ <li> <code>background-color</code> set to a fully opaque color for all the other elements </li>
+ </ul>
+ </p>
+ <p>The CSS associated to the elements used in the tests shouldn't use properties that creates a stacking context, except the ones specified in the test case descriptions.</p>
+ <p>Every test case has a description of the elements used. The notation from the image is used in the test case description too (e.g. for parent element the notation is [P]). Each test case uses only a subset of the elements while the other elements should just be removed.
+ </p></p>
+ <section>
+ <h4>An element with <code>mix-blend-mode</code> other than normal creates a stacking context</h4>
+ <p>Refers to the following assertion in the <a href="https://drafts.fxtf.org/compositing-1/#mix-blend-mode" >spec</a>: <q>Applying a blendmode other than ‘normal’ to the element must establish a new stacking context [CSS21].</q></p>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Simple <code>&lt;div&gt;</td>
+ <td>1 element required: <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal
+ </td>
+ <td>The element [B] creates a stacking context</td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4>An element with <code>mix-blend-mode</code> blends with the content within the current stacking context</h4>
+ <p>Refers to the following assertion in the <a href="https://drafts.fxtf.org/compositing-1/#csscompositingrules_CSS">spec</a>: <q>An element that has blending applied, must blend with all the underlying content of the stacking context [CSS21] that that element belongs to.</q> </p>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Blending simple elements </td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal
+ </td>
+ <td>The color of the parent element [P] mixes with the color of the child element [B].</td>
+ </tr>
+ <tr>
+ <td>Blending <code>&lt;video&gt;</code></td>
+ <td>2 elements required: <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="Sibling of the element [B]">[IN-S]</a> <br>
+ [B] - <code>&lt;video&gt;</code> element with <code>mix-blend-mode</code> other than normal <br>
+ [IN-S] - sibling(of the element [B]) visually overlaping the <code>&lt;video&gt;</code> element <br>
+ [IN-S] has some text inside
+ </td>
+ <td>The content of the <code>video</code> element [B] mixes with the colors of the sibling element and the text from [IN-S].</td>
+ </tr>
+ <tr>
+ <td>Blending with a sibling</td>
+ <td>3 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a>, <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="Sibling of the element [B]">[IN-S]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal <br>
+ [IN-S] - sibling of the element [B] <br>
+ The [IN-S] element visually overlaps the [B] element
+ </td>
+ <td>The colors of the parent element [P] and the sibling element [IN-S] mixes with the color of the blended element [B].</td>
+ </tr>
+ <tr>
+ <td>Blending with two levels of ascendants</td>
+ <td>3 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a>, <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="child of the element [P]">[IN-P]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal <br>
+ [IN-P] - Intermediate child element between the parent [P] and the child [B]
+ </td>
+ <td>The colors of the parent element [P] and the child element [IN-P] mixes with the color of the blended element [B].</td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4>An element with <code>mix-blend-mode</code> doesn't blend with anything outside the current stacking context</h4>
+ <p>Refers to the following assertion in the <a href="https://drafts.fxtf.org/compositing-1/#csscompositingrules_CSS">spec</a>: <q> An element that has blending applied, must blend with all the underlying content of the stacking context [CSS21] that that element belongs to.</q></p>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Blending child overflows the parent</td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal <br>
+ The blending element [B] has content that lies outside the parent element. <br>
+ Set the <code>background-color</code> of the <code>body</code> to a value other than default</td>
+ <td>The color of the parent element mixes with the color of the child element. <br>
+ The area of the child element outside of the parent element doesn't mix with the color of the <code>body</code></td>
+ </tr>
+ <tr>
+ <td>Parent with transparent pixels</td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ The element has some text inside and default value for <code>background-color</code> <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal <br>
+ The <code>background-color</code> of the <code>body</code> has a value other than default</td>
+ <td>The color of the text from the parent element [P] mixes with the color of the child element [B]. <br>
+ No blending between the color of the <code>body</code> and the color of the blending element [B].
+ </td>
+ </tr>
+ <tr>
+ <td>Parent with <code>border-radius</code></td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [P] has <code>border-radius</code> specified (e.g.50%). <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal <br>
+ [B] has content that lies outside the parent element, over a rounded corner. <br>
+ The <code>background-color</code> of the <code>body</code> has a value other than default. </td>
+ <td>The color of the parent element mixes with the color of the child element. <br>
+ The area of the child element which draws over the rounded corner doesn't mix with the color of the <code>body</code></td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4>An element with <code>mix-blend-mode</code> other than normal must cause a group to be isolated</h4>
+ <p>Refers to the following assertion in the <a href="https://drafts.fxtf.org/compositing-1/#isolation" >spec</a>: <q>operations that cause the creation of stacking context [CSS21] must cause a group to be isolated.</q> </p>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Child of the blended element has opacity</td>
+ <td>3 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a>, <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="Child of the lement [B]">[CB]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal <br>
+ [CB] - child of the element [B] with <code>opacity</code> less than one. </td>
+ <td>The group created by the two child elements([B] and [CB]) is blended with the parent element [P]. <br>
+ No blending between [B] and [CB]</td>
+ </tr>
+ <tr>
+ <td>Overflowed child of the blended element</td>
+ <td>3 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a>, <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="Child of the lement [B]">[CB]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal <br>
+ [CB] - child of the element [B] with content that lies outside the parent element [B].
+ </td>
+ <td>The group created by the two child elements([B] and [CB]) is blended with the parent element [P]. <br>
+ No blending between [B] and [CB]. There is only one color for the entire element [CB] </td>
+ </tr>
+ <tr>
+ <td>Blended element with transparent pixels</td>
+ <td>3 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a>, <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="Child of the lement [B]">[CB]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal and transparent <code>background-color</code> <br>
+ [CB] - child of the element [B]
+ </td>
+ <td>The group created by the two child elements([B] and [CB]) is blended with the parent element [P]. <br>
+ No blending between [B] and [CB]. </td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4>An element with <code>mix-blend-mode</code> must work properly with css transforms</h4>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Parent with 3D transform</td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with <code>3D transform</code> <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal
+ <td>The color of the parent element [P] mixes with the color of the child element [B] <br>
+ The element (and the content) of the element [P] is properly transformed
+ </td>
+ </tr>
+ <tr>
+ <td>Blended element with 3D transform</td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a>, <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="Child of the lement [B]">[CB]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal and <code>3D transform</code> <br>
+ [CB] - child of the element [B] </td>
+ <td> The color of the parent element [P] mixes with the color of the child element [B] <br>
+ The element (and the content) of the element [P] is properly transformed </td>
+ </tr>
+ <tr>
+ <td>Both parent and blended element with 3D transform</td>
+ <td> 2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with <code>3D transform</code> <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal and <code>3D transform</code>
+ </td>
+ <td>The color of the parent element [P] mixes with the color of the child element [B] <br>
+ The elements (and the content) of the elements [P] and [B] are properly transformed</td>
+ </tr>
+ <tr>
+ <td>Blended element with transform and preserve-3d</td>
+ <td>3 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a>, <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="Child of the lement [B]">[CB]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal and transform with <code>transform-style:preserve-3d</code> <br>
+ [CB] - child of the element [B]. It has 3D transform property</td>
+ <td> The child element [CB] will NOT preserve its 3D position. <br>
+ <code>mix-blend-mode</code> override the behavior of <code>transform-style:preserve-3d</code>:
+ creates a flattened representation of the descendant elements <br>
+ The color of the group created by the child elements([B] and [CB]) will blend with the color of the parent element [P] </td>
+ </tr>
+ <tr>
+ <td>Blended element with transform and perspective</td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal and transform with <code>perspective</code> set to positive length </td>
+ <td>The colors of the parent and the child are mixed ([P] and [B]) <br>
+ The element (and the content) of the element [B] is properly transformed
+ </td>
+ </tr>
+ <tr>
+ <td>Sibling with 3D transform between the parent and the blended element</td>
+ <td>3 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a>, <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="Sibling of the element [B]">[IN-S]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal <br>
+ [IN-S] - Sibling(of the element [B]) with <code>3D transform</code> between the parent [P] and the child [B]
+ </td>
+ <td>The colors of the parent element [P] and the transformed sibling element [IN-S] mixes with the color of the blended element [B].<br>
+ The element (and the content) of the element [IN-S] is properly transformed
+ </td>
+ </tr>
+ <tr>
+ <td>Parent with 3D transform and transition</td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with <code>3D transform</code> and transition <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal
+ <td>The color of the parent element [P] mixes with the color of the child element [B] <br>
+ The element (and the content) of the element [P] is properly transformed
+ </td>
+ </tr>
+ <tr>
+ <td>Sibling with 3D transform(and transition) between the parent and the blended element</td>
+ <td>3 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a>, <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="Sibling of the element [B]">[IN-S]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal <br>
+ [IN-S] - sibling(of the element [B]) with <code>3D transform</code> and transition between the parent [P] and the child [B]
+ </td>
+ <td>The colors of the parent element [P] and the transformed sibling element [IN-S] mixes with the color of the blended element [B].<br>
+ The element (and the content) of the element [IN-S] is properly transformed
+ </td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4>An element with <code>mix-blend-mode</code> must work properly with elements with <code>overflow</code> property</h4>
+ <table>
+ <tr>
+ <td>Parent element with <code>overflow:scroll</code> </td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [P] has <code>overflow:scroll</code> <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal tat overflows the parents [P] dimensions so that it creates scrolling for the parent
+ <td>The color of the parent element [P] mixes with the color of the child element [B]. <br>
+ The scrolling mechanism is not affected.
+ </td>
+ </tr>
+ <tr>
+ <td>Blended element with <code>overflow:scroll</code></td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal, <code>overflow:scroll</code> and a child element that creates overflow for [B]</td>
+ <td>The color of the parent element [P] mixes with the color of the child element [B] <br>
+ The scrolling mechanism is not affected.
+ </td>
+ </tr>
+ <tr>
+ <td>Parent element with <code>overflow:scroll</code> and blended with <code>position:fixed</code> </td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [P] has <code>overflow:scroll</code> <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal, <code>position:fixed</code> and should overflow the parents [P] dimensions so that it creates scrolling for the parent</td>
+ <td>The color of the parent element [P] mixes with the color of the child element [B] <br>
+ The blending happens when scrolling the content of the parent element [P] too. <br>
+ The scrolling mechanism is not affected.
+ </td>
+ </tr>
+ <tr>
+ <td>Parent with <code>overflow:hidden</code> and <code>border-radius</code></td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [P] has <code>overflow:hidden</code> and <code>border-radius</code> specified (e.g.50%) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal with content that lies outside the parent element, over a rounded corner <br>
+ Set the <code>background-color</code> of the <code>body</code> to a value other than default.</td>
+ <td>The color of the parent element mixes with the color of the child element. <br>
+ The area of the child element which draws over the rounded corner is properly cut </td>
+ </tr>
+ <tr>
+ <td>Blended element with <code>overflow:hidden</code> and <code>border-radius</code></td>
+ <td>3 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="Child of the lement [B]">[CB]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal, <code>overflow:hidden</code> and <code>border-radius</code> specified (e.g.50%). <br>
+ [CB] - child of the element [B], with content that lies outside the parent element, over a rounded corner. <br> </td>
+ <td>The group created by the two child elements([B] and [CB]) is blended with the parent element [P]. <br>
+ No blending between [B] and [CB]. <br>
+ [CB] is properly clipped so no overflow is visible.</td>
+ </tr>
+ <tr>
+ <td>Intermediate child with <code>overflow:hidden</code> and <code>border-radius</code> between the parent and the blended element</td>
+ <td>3 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a>, <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="child of the element [P]">[IN-P]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal that overflows the parents [IN-P] dimensions
+ [IN-P] - child(of the element [P]) with <code>overflow:hidden</code> and <code>border-radius</code> specified (e.g.50%)
+ </td>
+ <td>The colors of the parent element [P] and the child element [IN-P] mixes with the color of the blended element [B]. <br>
+ [B] is is properly clipped so no overflow is visible
+ </td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4>Other test cases</h4>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Blended element with <code>border-image</code> </td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal and <code>border-image</code> specified as a png file
+ </td>
+ <td>The color of the parent element [P] mixes with the color of the child element. <br>
+ The color of the <code>border-image</code> mixes with the color of the parent element [P].
+ </td>
+ </tr>
+ <tr>
+ <td>Blending with <code>&lt;canvas&gt;</code> </td>
+ <td>2 elements required: <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="Sibling of the element [B]">[IN-S]</a> <br>
+ [B] - <code>&lt;canvas&gt;</code> element with <code>mix-blend-mode</code> other than normal <br>
+ [IN-S] - Sibling of the <code>&lt;canvas&gt;</code> element with some text <br>
+ The [IN-S] element overlaps the <code>&lt;canvas&gt;</code> element
+ </td>
+ <td>The content of the <code>&lt;canvas&gt;</code> element mixes with the color of the sibling element and the text [IN-S].</td>
+ </tr>
+ <tr>
+ <td>Blended <code>&lt;canvas&gt;</code></td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - Child <code>&lt;canvas&gt;</code> element with <code>mix-blend-mode</code> other than normal
+ </td>
+ <td>The color of the <code>&lt;canvas&gt;</code> element [B] mixes with the color of the parent element [P] .</td>
+ </tr>
+ <tr>
+ <td>Blended <code>&lt;video&gt;</code></td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - <code>&lt;video&gt;</code> element with <code>mix-blend-mode</code> other than normal
+ </td>
+ <td>The color of the <code>&lt;video&gt;</code> element mixes with the color of the parent element [P] .</td>
+ </tr>
+ <tr>
+ <td>Blending with <code>&lt;iframe&gt;</code> </td>
+ <td>2 elements required: <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> and <a href="#test_outline" title="Sibling of the element [B]">[IN-S]</a> <br>
+ [B] - <code>&lt;iframe&gt;</code> element with <code>mix-blend-mode</code> other than normal <br>
+ [IN-S] - sibling(of the element [B]) with some text <br>
+ The [IN-S] element visually overlaps the <code>&lt;iframe&gt;</code> element
+ </td>
+ <td>The color of the <code>&lt;iframe&gt;</code> element mixes with the color of the sibling element and the text [IN-S].</td>
+ </tr>
+ <tr>
+ <td>Blended <code>&lt;iframe&gt;</code></td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - <code>&lt;iframe&gt;</code> element with <code>mix-blend-mode</code> other than normal
+ </td>
+ <td>The color of the <code>&lt;iframe&gt;</code> element [B] mixes with the color of the parent element [P]. </td>
+ </tr>
+ <tr>
+ <td>Blended element with <code>mask</code> property</td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal and <code>mask</code> property specified to an SVG image (e.g. circle)</td>
+ <td>The colors of the parent and the masked child are mixed ([P] and [B])</td>
+ </tr>
+ <tr>
+ <td>Blended element with <code>clip-path</code> property </td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal and <code>clip-path</code> property specified to a basic shape (e.g. ellipse)</td>
+ <td>The colors of the parent and the clipped child are mixed ([P] and [B])</td>
+ </tr>
+ <tr>
+ <td>Blended element with <code>filter</code> property</td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal and <code>filter</code> property value other than none </td>
+ <td>The filter is applied and the result is mixed with the parent element</td>
+ </tr>
+ <tr>
+ <td>Blended element with <code>transition</code></td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal and <code>transition-property</code> for <code>opacity</code> </td>
+ <td>The transition is applied and the result is mixed with the parent element</td>
+ </tr>
+ <tr>
+ <td>Blended element with <code>animation</code></td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - element with <code>mix-blend-mode</code> other than normal and <code>animation</code> specified</td>
+ <td>The animation is applied to the child element and the result is mixed with the parent element</td>
+ </tr>
+ <tr>
+ <td>Image element</td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - <code>&lt;img&gt;</code> element (.jpeg or .gif image) with <code>mix-blend-mode</code> other than normal</td>
+ <td>The color of the <code>&lt;img&gt;</code> is mixed with the color of the <code>&lt;div&gt;</code>.</td>
+ </tr>
+ <tr>
+ <td>SVG element</td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - SVG element with <code>mix-blend-mode</code> other than normal</td>
+ <td>The color of the SVG is mixed with the color of the <code>&lt;div&gt;</code>.</td>
+ </tr>
+ <tr>
+ <td>Paragraph element</td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - paragraph element with <code>mix-blend-mode</code> other than normal</td>
+ <td>The color of the text from the paragraph element is mixed with the color of the <code>&lt;div&gt;</code></td>
+ </tr>
+ <tr>
+ <td>Paragraph element and background-image</td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ and <code>background-image</code> <br>
+ [B] - Child <code>p</code> element with some text and <code>mix-blend-mode</code> other than normal</td>
+ <td>The color of the text from the <code>p</code> element is mixed with the background image of the <code>&lt;div&gt;</code>.</td>
+ </tr>
+ <tr>
+ <td>Set blending from JavaScript</td>
+ <td>2 elements required: <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [B] - Child <code>&lt;div&gt;</code> element with no <code>mix-blend-mode</code> specified<br>
+ From JavaScript, set the <code>mix-blend-mode</code> property for the child <code>&lt;div&gt;</code> to a value other than normal</td>
+ <td>The colors of the <code>&lt;div&gt;</code> elements are mixed.</td>
+ </tr>
+ </table>
+ </section>
+ </section>
+ <section>
+ <h3>Test cases for SVG elements with <code>mix-blend-mode</code></h4>
+ <section>
+ <h4><code>mix-blend-mode</code> with simple SVG graphical elements</h4>
+ <p>Refers to the following assertion in the <a href="https://drafts.fxtf.org/compositing-1/#mix-blend-mode" >spec</a> : <q><code>mix-blend-mode</code> applies to svg, g, use, image, path, rect, circle, ellipse, line, polyline, polygon, text, tspan, and marker.</q></p>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Circle with SVG background</td>
+ <td>Set a background color for the SVG.<br>
+ Create 16 <code>circle</code> elements and fill them with a solid color.
+ <br>Apply each <code>mix-blend-mode</code> on them.</td>
+ <td>The color of the <code>circle</code> is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Ellipse with SVG background</td>
+ <td>Set a background color for the SVG.<br>
+ Create an <code>ellipse</code> element and fill it with a solid color.
+ <br>Apply a <code>mix-blend-mode</code> on it other than <code>normal</code>.</td>
+ <td>The color of the <code>ellipse</code> is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Image with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create an <code>image</code> element and apply a <code>mix-blend-mode</code> other than <code>normal</code>.</td>
+ <td>The <code>image</code> is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Line with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>line</code> element and fill it with a solid color.
+ <br>Apply a <code>mix-blend-mode</code> on it other than <code>normal</code>.</td>
+ <td>The color of the <code>line</code> is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Path with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>path</code> element and fill it with a solid color.
+ <br>Apply a <code>mix-blend-mode</code> on it other than <code>normal</code>.</td>
+ <td>The color of the <code>path</code> is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Polygon with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>polygon</code> element and fill it with a solid color.
+ <br>Apply a <code>mix-blend-mode</code> on it other than <code>normal</code>.</td>
+ <td>The color of the <code>polygon</code> is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Polyline with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>polyline</code> element and fill it with a solid color.
+ <br>Apply a <code>mix-blend-mode</code> on it other than <code>normal</code>.</td>
+ <td>The color of the <code>polyline</code> is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Rect with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>rect</code> element and fill it with a solid color.
+ <br>Apply a <code>mix-blend-mode</code> on it other than <code>normal</code>.</td>
+ <td>The color of the <code>rect</code> is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Text with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>text</code> element and apply a <code>mix-blend-mode</code> other than <code>normal</code>.</td>
+ <td>The text is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Text having tspan with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>text</code> element and a <code>tspan</code> inside it.
+ <br>Apply a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>tspan</code>.</td>
+ <td>The text is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Gradient with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>rect</code> element and fill it with a <code>gradient</code>.
+ <br>Apply a <code>mix-blend-mode</code> on it other than normal.</td>
+ <td>The gradient is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Pattern with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>rect</code> element and fill it with a <code>pattern</code>.
+ <br>Apply a <code>mix-blend-mode</code> on it other than normal.</td>
+ <td>The pattern is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Set blending on an element from JavaScript</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>rect</code> element and fill it with a solid color.
+ <br>Apply a <code>mix-blend-mode</code> (other than <code>normal</code>) on it from JavaScript.</td>
+ <td>The color of the <code>rect</code> is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Marker with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>line</code> element containing a marker.
+ <br>Apply a <code>mix-blend-mode</code> other than <code>normal</code> on the marker.</td>
+ <td>The marker color is mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>Metadata with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>metadata</code> element containing an embedded pdf.
+ <br>Apply a <code>mix-blend-mode</code> other than <code>normal</code> on the marker.</td>
+ <td>The metadata content is not mixed with the color of the background.</td>
+ </tr>
+ <tr>
+ <td>ForeignObject with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>foreignObject</code> element containing a simple xhtml file.
+ <br>Apply a <code>mix-blend-mode</code> other than <code>normal</code> on the marker.</td>
+ <td>The foreignObject content is not mixed with the color of the background.</td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4><code>mix-blend-mode</code> with SVG groups</h4>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Group of overlapping elements with SVG background</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>group</code> element containing two overlapping <code>rect</code> elements, each filled with a different solid color.
+ <br>Apply a <code>mix-blend-mode</code> other than <code>normal</code> on the group.</td>
+ <td>The <code>group</code> is mixed as a whole with the color of the background.</td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4><code>mix-blend-mode</code> with isolated groups</h4>
+ <p>Refers to the following assertion in the <a href="https://drafts.fxtf.org/compositing-1/#mix-blend-mode" >spec</a>:
+ <br><q>By default, every element must create a non-isolated group.<br>
+ However, certain operations in SVG will create isolated groups.<br>
+ If one of the following features is used, the group must become isolated:
+ <ul>
+ <li>opacity</li>
+ <li>filters</li>
+ <li>3D transforms (2D transforms must NOT cause isolation)</li>
+ <li>blending</li>
+ <li>masking</li>
+ </ul>
+ </q>
+ </p>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Blending two elements in an isolated group</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>group</code> element containing two overlapping <code>rect</code> elements, each filled with a different solid color.<br>
+ Apply <code>opacity</code> less than 1 on the group and a <code>mix-blend-mode</code> other than <code>normal</code> on the second rect.</td>
+ <td>Only the intersection of the <code>rect</code> elements should mix.</td>
+ </tr>
+ <tr>
+ <td>Blending in a group with opacity</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>group</code> element containing a <code>rect</code> element filled with a different solid color.<br>
+ Apply <code>opacity</code> less than 1 on the group and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will not mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blending in a group with filter</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>group</code> element containing a <code>rect</code> element filled with a different solid color.<br>
+ Apply a <code>filter</code> on the group and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will not mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blending in a group with 2D transform</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>group</code> element containing a <code>rect</code> element filled with a different solid color.<br>
+ Apply a <code>transform</code> on the group and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blending in a group with 3D transform</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>group</code> element containing a <code>rect</code> element filled with a different solid color.<br>
+ Apply a 3d transform on the group and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will not mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blending in a group with a mask</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>group</code> element containing a <code>rect</code> element filled with a different solid color.<br>
+ Apply a <code>mask</code> on the group and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will not mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blending in a group with mix-blend-mode</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>group</code> element containing a <code>rect</code> element filled with a different solid color.<br>
+ Apply a <code>mix-blend-mode</code> other than <code>normal</code> on the group and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will not mix with the content behind it.</td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4>Other test cases for SVG</h4>
+ <table>
+ <tr>
+ <td>Blend with element having opacity</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>rect</code> element filled with a different solid color.<br>
+ Apply <code>opacity</code> less than 1 and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blend with element having stroke</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>rect</code> element filled with a different solid color.<br>
+ Apply a <code>stroke</code> and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blend with element having stroke-opacity</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>rect</code> element filled with a different solid color.<br>
+ Apply a <code>stroke</code>, <code>stroke-opacity</code> less than 1 and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blend with element having stroke-dasharray</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>rect</code> element filled with a different solid color.<br>
+ Apply a <code>stroke-dasharray</code> and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blend with element having transform</td>
+ <td>Set a background color for the SVG.<br>
+ Create an <code>image</code> element. Apply a <code>transform</code> (any combination of <code>translate</code>, <code>rotate</code>, <code>scale</code>, <code>skew</code>) and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>image</code>.</td>
+ <td>The <code>image</code> will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blend with SVG having viewbox and preserveAspectRatio set</td>
+ <td>Set a background color for the SVG, as well as <code>viewbox</code> and <code>preserveAspectRatio</code>.<br>
+ Create a <code>rect</code> element filled with a different solid color and apply a <code>mix-blend-mode</code> other than <code>normal</code> on it.</td>
+ <td>The <code>rect</code> will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blend with an element having color-profile set</td>
+ <td>Set a background color for the SVG.<br>
+ Create an <code>image</code> element. Apply a <code>color-profile</code> (<code>sRGB</code>, for example) and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>image</code>.</td>
+ <td>The <code>image</code> will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blend with an element having overflow</td>
+ <td>Set a background color for the SVG.<br>
+ Create an <code>image</code> larger than the SVG.<br>
+ Apply <code>overflow</code> (<code>visible</code>, <code>hidden</code>, <code>scroll</code>) and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>image</code>.</td>
+ <td>The <code>image</code> will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blend with an element having clip-path</td>
+ <td>Set a background color for the SVG.<br>
+ Create an <code>image</code> element. Apply a <code>clip-path</code> and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>image</code>.</td>
+ <td>The <code>image</code> will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blend with an element having a mask</td>
+ <td>Set a background color for the SVG.<br>
+ Create an <code>image</code> element.<br>
+ Apply a <code>mask</code> and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>image</code>.</td>
+ <td>The <code>image</code> will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blend with an element having a filter</td>
+ <td>Set a background color for the SVG.<br>
+ Create an <code>image</code> element.<br>
+ Apply a <code>filter</code> and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>image</code>.</td>
+ <td>The <code>image</code> will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blend with an animated element</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>rect</code> element filled with a different solid color.<br>
+ Apply an <code>animateTransform</code> and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Set blending from an SVG script element</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>rect</code> element and fill it with a solid color.<br>
+ Apply a <code>mix-blend-mode</code> (other than <code>normal</code>) on it from an svg <code>script</code> element.</td>
+ <td>The <code>rect</code> will mix with the content behind it.</td>
+ </tr>
+ </table>
+ </section>
+ </section>
+ <section>
+ <h3>Test cases for <code>background-blend-mode</code></h3>
+ <section>
+ <h4>Blending between the background layers and the background color for an element with <code>background-blend-mode</code> </h4>
+ <p>Refers to the following assertion in the <a href="https://drafts.fxtf.org/compositing-1/#background-blend-mode">spec</a>: <q>Each background layer must blend with the element's background layer that are below it and the element's background color.</q></p>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Images with different formats</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ Tests should be created for <code>&lt;image&gt;</code> with different formats such as PNG, JPEG or SVG
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code></td>
+ </tr>
+ <tr>
+ <td>Gradient and background color</td>
+ <td>
+ Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;gradient&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code></td>
+ </tr>
+ <tr>
+ <td>Image and gradient</td>
+ <td>
+ Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code> on top of a <code>&lt;gradient&gt;</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>&lt;image&gt;</code> is mixed with the content of the <code>&lt;gradient&gt;</code>
+ </td>
+ </tr>
+ <tr>
+ <td>Gradient and image</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to a <code>&lt;gradient&gt;</code> on top of an <code>&lt;image&gt;</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>&lt;image&gt;</code> is mixed with the content of the <code>&lt;gradient&gt;</code></td>
+ </tr>
+ <tr>
+ <td>Two gradients</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to a <code>&lt;gradient&gt;</code> on top of another <code>&lt;gradient&gt;</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul></td>
+ <td>The content of the two gradients is mixed</td>
+ </tr>
+ <tr>
+ <td>Two images</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code> on top of another <code>&lt;image&gt;</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul></td>
+ <td>The content of the two images is mixed</td>
+ </tr>
+ <tr>
+ <td>Image and background color with transparency</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code> with transparency(e.g. PNG images)</li>
+ <li><code>background-color</code> set to a transparent color</li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code></td>
+ </tr>
+ <tr>
+ <td>Cross-fade image and gradient</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to a <q>cross-fade()</q> image on top of a <code>&lt;gradient&gt;</code> </li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the cross-faded image is mixed with the content of the <code>&lt;gradient&gt;</code></td>
+ </tr>
+ <tr>
+ <td>SVG image and background color</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to a data URI for an SVG image </li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the image is mixed with the color of the background</td>
+ </tr>
+ <tr>
+ <td>Animated gif image and background color</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an animated gif image</li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the image is mixed with the color of the background</td>
+ </tr>
+ <tr>
+ <td>Set <code>background-blend-mode</code> from JavaScript</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to a <code>gradient</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li>no <code>background-blend-mode</code> explicitly specified</li>
+ From JavaScript, set the <code>background-blend-mode</code> property to a value other than normal.
+ </ul>
+ </td>
+ <td>The content of the gradient is mixed with the color of the background</td>
+ </tr>
+ <tr>
+ <td><code>background-blend-mode</code> on element with 3D transform</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ <li><code>transform</code> set to a 3D function like rotateX, rotateY or translateZ</li>
+ </ul>
+ </td>
+ <td>The content of the image is mixed with the color of the background</td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4>Background layers do not blend with content outside the background (or behind the element)</h4>
+ <p>Refers to the following assertion in the <a href="https://drafts.fxtf.org/compositing-1/#background-blend-mode">spec</a>: <q>Background layer must not blend with the content that is behind the element instead they must act as if they are rendered into an isolated group.</q>
+ </p>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>One background layer</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The <code>background-image</code> is not mixed with anything outside the element</td>
+ </tr>
+ <tr>
+ <td>Two elements</td>
+ <td>2 elements required: a parent element with a child. <br>
+ Each one with the following properties:
+ <ul>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ <td>No blending between the background colors of the two elements</td>
+ </tr>
+ <tr>
+ <td>Parent and child with <code>background-blend-mode</code></td>
+ <td>2 elements required: a parent element with a child <br>
+ Parent properties: <br>
+ <ul>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ Child properties: <br>
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ <td>The content of the image from the child element does not mixes with the background color from the parent element</td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4> <code>background-blend-mode</code> list values apply to the corresponding background layer</h4>
+ <p>Refers to the following assertion in the <a href="https://drafts.fxtf.org/compositing-1/#background-blend-mode">spec</a>: <q>The ‘background-blend-mode’ list must be applied in the same order as ‘background-image’[CSS3BG]. This means that the first element in the list will apply to the layer that is on top.</q>
+ </p>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Different blend modes applied between layers</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image-list&gt;</code> containing three images: (e.g. I1, I2 and I3 ) </li>
+ <li><code>background-blend-mode</code> set to different <code>blendmode</code> for every image: (e.g. multiply, difference, screen) </li>
+ </ul></td>
+ <td>The content of the three images is correctly mixed <br>
+ (multiply for I1, difference for I2 and screen for I3)
+ </td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4><code>background-blend-mode</code> list values are repeated if the list is shorter than the background layer list</h4>
+ <p>Refers to the following assertion in the <a href="https://drafts.fxtf.org/compositing-1/#background-blend-mode">spec</a>: <q>If a property doesn't have enough comma-separated values to match the number of layers, the UA must calculate its used value by repeating the list of values until there are enough.</q>
+ </p>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Blend mode list repeat</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image-list&gt;</code> containing three images</li>
+ <li><code>background-blend-mode</code> set to two different <code>blendmode</code> values</li>
+ </ul></td>
+ <td>The unspecified blend modes should be obtained by repeating the blend mode list from the beginning</td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4>The default <code>background-blend-mode</code> value for the <code>background</code> shorthand is 'normal' </h4>
+ <p>Refers to the following assertion in the <a href="https://drafts.fxtf.org/compositing-1/#background-blend-mode">spec</a>: <q>If the ‘background’ [CSS3BG] shorthand is used, the ‘background-blend-mode’ property for that element must be reset to its initial value.</q>
+ </p>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Default blend mode for 'background' shorthand</td>
+ <td>Element with
+ <ul>
+ <li><code>background</code> property set to an image and a color</li>
+ <li>No value explicitly set for <code>background-blend-mode</code> </li>
+ </ul></td>
+ <td> The computed value of <code>background-blend-mode</code> is 'normal'
+ </td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4><code>background-blend-mode</code> for an element with <code>background-position</code></h4>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td><code>background-position</code> percentage</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-position</code> specified in percentage, such as 50% 50%</li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code> <br>
+ The <code>background-image</code> is correctly positioned
+ </td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4><code>background-blend-mode</code> for an element with <code>background-size</code></h4>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Background size defined in pixels</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-size</code> specified in pixels</li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code> <br>
+ The <code>background-image</code> has the correct size
+ </td>
+ </tr>
+ <tr>
+ <td>Background size defined in percentage (second phase)</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-size</code> specified in percentage</li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code> <br>
+ The <code>background-image</code> has the correct size
+ </td>
+ </tr>
+ <tr>
+ <td>Background size cover</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-size</code> set to <code>cover</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code> <br>
+ The <code>background-image</code> has the correct size
+ </td>
+ </tr>
+ <tr>
+ <td>Background size contain</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-size</code> set to <code>contain</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code> <br>
+ The <code>background-image</code> has the correct size
+ </td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4><code>background-blend-mode</code> for an element with <code>background-repeat</code></h4>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td><code>background-repeat</code> set to no-repeat</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-repeat</code> set to <code>no-repeat</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code> <br>
+ The <code>background-image</code> is not repeated
+ </td>
+ </tr>
+ <tr>
+ <td><code>background-repeat</code> set to space</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-repeat</code> set to <code>space</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code> <br>
+ </td>
+ </tr>
+ <tr>
+ <td><code>background-repeat</code> set to round</td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-repeat</code> set to <code>round</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code> <br>
+ </td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4><code>background-blend-mode</code> for an element with <code>background-clip</code></h4>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td><code>background-clip</code> set to <code>padding-box</code></td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-clip</code> set to <code>padding-box</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code> <br>
+ No background is drawn below the border (background extends to the outside edge of the padding)
+ </td>
+ </tr>
+ <tr>
+ <td><code>background-clip</code> set to <code>content-box</code></td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-clip</code> set to <code>content-box</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code> <br>
+ The background is painted within (clipped to) the content box
+ </td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4><code>background-blend-mode</code> for an element with <code>background-origin</code></h4>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td><code>background-origin</code> set to <code>border-box</code></td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-origin</code> set to <code>border-box</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code> <br>
+ The background extends to the outside edge of the border (but underneath the border in z-ordering)
+ </td>
+ </tr>
+ <tr>
+ <td><code>background-origin</code> set to <code>content-box</code></td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-origin</code> set to <code>content-box</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code> <br>
+ The background is painted within (clipped to) the content box
+ </td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4><code>background-blend-mode</code> for an element with <code>background-attachement</code></h4>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td><code>background-attachment</code> set to <code>fixed</code></td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to an <code>&lt;image&gt;</code></li>
+ <li><code>background-color</code> set to a fully opaque color</li>
+ <li><code>background-attachment</code> set to <code>fixed</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The content of the <code>background-image</code> is mixed with the color of the <code>background-color</code> <br>
+ The background image will not scroll with its containing element, instead remaining stationary within the viewport
+ </td>
+ </tr>
+ <tr>
+ <td>2 background images with <code>background-attachment</code> set to <code>fixed, scroll</code></td>
+ <td>Element with
+ <ul>
+ <li><code>background-image</code> set to 2 <code>&lt;image&gt;</code>(s)</li>
+ <li><code>background-attachment</code> set to <code>fixed, scroll</code></li>
+ <li><code>background-blend-mode</code> other than normal</li>
+ </ul>
+ </td>
+ <td>The background images will be mixed when they overlap while scrolling
+ </td>
+ </tr>
+ </table>
+ </section>
+ </section>
+ <section>
+ <h3>Test cases for <code>isolation</code></h3>
+ <section>
+ <h4>An element with <code>isolation:isolate</code> creates a stacking context</h4>
+ <p>Refers to the following assertion in the <a href="https://drafts.fxtf.org/compositing-1/#csscompositingrules_CSS">spec</a>: <q>For CSS, setting ‘isolation’ to ‘isolate’ will turn the element into a stacking context [CSS21].</q></p>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Isolation isolate</td>
+ <td>Have an element with <code>isolation</code> set to <code>isolate</code></td>
+ <td>The element creates a stacking context.</td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4>An element with <code>isolation:isolate</code> creates an isolated group for blended children</h4>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Isolation of blended child which overflows</td>
+ <td>3 elements required:
+ <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a>,
+ <a href="#test_outline" title="child of the element [P]">[IN-P]</a> and
+ <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>) <br>
+ [IN-P] - Intermediate child element between the parent [P] and the child [B]<br>
+ This element has <code>isolation:isolate</code> set.<br>
+ [B] - element with <code>mix-blend-mode</code> other than <code>normal</code> <br>
+ The blending element [B] has content that lies outside the parent element. <br>
+ </td>
+ <td>
+ The color of the child element [B] mixes with the color of the intermediate element [IN-P], where they overlap.<br>
+ The area of the child element outside of the intermediate parent element does not mix with the color of the parent element [P], or of the <code>body</code>.
+ </td>
+ </tr>
+ <tr>
+ <td>Isolation on intermediate element with transparent pixels</td>
+ <td>3 elements required:
+ <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a>,
+ <a href="#test_outline" title="child of the element [P]">[IN-P]</a> and
+ <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>); the element <code>background-color</code> is other than <code>transparent</code><br>
+ [IN-P] - Intermediate child element between the parent [P] and the child [B]<br>
+ The intermediate element has text content, default value for <code>background-color</code> and <code>isolation:isolate</code> set<br>
+ [B] - element with <code>mix-blend-mode</code> other than <code>normal</d <br>
+ <td>
+ The color of the child element [B] mixes with the color of the intermediate element [IN-P], where they overlap.<br>
+ There is no blending between the color of the parent element [P] and the color of the blended element [B].
+ </td>
+ </tr>
+ <tr>
+ <td>Isolate inside a stacking context created by a 3d transform</td>
+ <td>
+ 3 elements required:
+ <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a>,
+ <a href="#test_outline" title="child of the element [P]">[IN-P]</a> and
+ <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a <code>3D transform</code> applied<br>
+ [IN-P] - Intermediate child element between the parent [P] and the child [B]<br>
+ The intermediate element has <code>isolation:isolate</code> set<br>
+ [B] - element with <code>mix-blend-mode</code> other than <code>normal</code><br>
+ </td>
+ <td>
+ The color of the child element [B] mixes with the color of the intermediate element [IN-P], where they overlap.<br>
+ There is no blending between the color of the parent element [P] and the color of the blended element [B].
+ </td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4>An element with <code>isolation:auto</code> set does not change the elements existing stacking context behavior</h4>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Isolation auto</td>
+ <td>Have an element with <code>isolation</code> explicitly set to <code>auto</code>, and no other style that would create a stacking context</td>
+ <td>The element does not create a stacking context - the computed value of its <code>z-index</code> is value <code>auto</code></td>
+ </tr>
+ <tr>
+ <td>Stacking context not affected by isolation</td>
+ <td>2 elements required:
+ <a href="#test_outline" title="parent element with a property that creates stacking context">[P]</a> and
+ <a href="#test_outline" title="Element with mix-blend-mode property other than normal">[B]</a> <br>
+ [P] - parent element with a property that creates a stacking context (e.g. <code>position:fixed</code>); This element has <code>isolation</code> explicitly set to <code>auto</code> <br>
+ [B] - element with <code>mix-blend-mode</code> other than <code>normal</code> <br>
+ The blending element [B] has content that lies outside the parent element. <br>
+ Set the <code>background-color</code> of the <code>body</code> to a value other than default
+ </td>
+ <td>The color of the parent element mixes with the color of the child element. <br>
+ The area of the child element outside of the parent element doesn't mix with the color of the <code>body</code>.<br>
+ In other words, setting the <code>isolation</code> to <code>auto</code> does not affect the creation of a stacking context by other properties.
+ </td>
+ </tr>
+ </table>
+ </section>
+ </section>
+ <section>
+ <h4>Test cases for <code>isolation</code> in SVG</h4>
+ <section>
+ <h4>In SVG, an element with <code>isolation:isolate</code> creates an isolated group for blended children</h4>
+ <p>Refers to the following assertion in the <a href="https://drafts.fxtf.org/compositing-1/#isolation">spec</a>: <q>In SVG, this defines whether an element is isolated or not.</q></p>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Blending in an isolated group</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>group</code> element containing a <code>rect</code> element filled with a different solid color.<br>
+ Apply <code>isolation:isolate</code> on the group and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will not mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blending two elements in an isolated group</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>group</code> element containing two overlapping <code>rect</code> elements, each filled with a different solid color.<br>
+ Apply <code>isolation:isolate</code> on the group and a <code>mix-blend-mode</code> other than <code>normal</code> on the second rect.</td>
+ <td>Only the intersection of the <code>rect</code> elements should mix.</td>
+ </tr>
+ <tr>
+ <td>Blending in an isolated group with 2D transform</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>group</code> element containing a <code>rect</code> element filled with a different solid color.<br>
+ Apply <code>isolation:isolate</code> and 2D transform on the group and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will not mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Set isolation on an element from JavaScript</td>
+ <td>Set a background color for the SVG.
+ <br>Create a <code>rect</code> element and fill it with a solid color and a <code>mix-blend-mode</code> other than <code>normal</code>.
+ <br>Apply <code>isolation:isolate</code> on it from JavaScript.</td>
+ <td>The <code>rect</code> will not mix with the content behind it.</td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h4>In SVG, an element with <code>isolation:auto</code> set does not change the rendering behaviour</h4>
+ <table>
+ <tr>
+ <th>Test name</th>
+ <th>Elements and styles</th>
+ <th>Expected result</th>
+ </tr>
+ <tr>
+ <td>Blending a group with <code>isolation:auto</code></td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>group</code> element containing a <code>rect</code> element filled with a different solid color.<br>
+ Apply <code>isolation:auto</code> on the group and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The element will mix with the content behind it.</td>
+ </tr>
+ <tr>
+ <td>Blending in a group with opacity</td>
+ <td>Set a background color for the SVG.<br>
+ Create a <code>group</code> element containing a <code>rect</code> element filled with a different solid color.<br>
+ Apply <code>opacity</code> less than 1 and <code>isolation:auto</code> on the group and a <code>mix-blend-mode</code> other than <code>normal</code> on the <code>rect</code>.</td>
+ <td>The <code>rect</code> will not mix with the content behind it.</td>
+ </tr>
+ </table>
+ </section>
+ </section>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/toBlob.png.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/toBlob.png.html
new file mode 100644
index 0000000000..1533bfdb6c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/toBlob.png.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<title>Canvas test: toBlob.png</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<canvas id="c"></canvas>
+<script>
+async_test(function() {
+ on_event(window, "load", this.step_func(function() {
+ var canvas = document.getElementById('c');
+ var ctx = canvas.getContext('2d');
+ canvas.toBlob(this.step_func_done(function(data) {
+ assert_equals(data.type, "image/png");
+ }), 'image/png');
+ }));
+}, "toBlob with image/png returns a PNG Blob");
+</script>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/will-change-abspos-cb-001.html b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/will-change-abspos-cb-001.html
new file mode 100644
index 0000000000..d59e443310
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/benchmarks/data/wpt/weighted/will-change-abspos-cb-001.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>CSS Test: will-change: position turns an element in an abspos containing block.</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Boris Zbarsky" href="mailto:bzbarsky@mit.edu">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1498873">
+<link rel="help" href="https://drafts.csswg.org/css-will-change/#will-change">
+<link rel="match" href="will-change-abspos-cb-001-ref.html">
+<style>
+ .container {
+ border: 1px solid green;
+ width: 100px;
+ height: 100px;
+ margin-top: 100px;
+ display: flex;
+ will-change: position;
+ }
+ .abspos {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: orange;
+ height: 20px;
+ width: 20px;
+ }
+</style>
+<div class="container">
+ <div class="abspos"></div>
+</div>
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/debug-info.py b/testing/web-platform/tests/tools/third_party/html5lib/debug-info.py
new file mode 100644
index 0000000000..b47b8ebfa2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/debug-info.py
@@ -0,0 +1,37 @@
+from __future__ import print_function, unicode_literals
+
+import platform
+import sys
+
+
+info = {
+ "impl": platform.python_implementation(),
+ "version": platform.python_version(),
+ "revision": platform.python_revision(),
+ "maxunicode": sys.maxunicode,
+ "maxsize": sys.maxsize
+}
+
+search_modules = ["chardet", "genshi", "html5lib", "lxml", "six"]
+found_modules = []
+
+for m in search_modules:
+ try:
+ __import__(m)
+ except ImportError:
+ pass
+ else:
+ found_modules.append(m)
+
+info["modules"] = ", ".join(found_modules)
+
+
+print("""html5lib debug info:
+
+Python %(version)s (revision: %(revision)s)
+Implementation: %(impl)s
+
+sys.maxunicode: %(maxunicode)X
+sys.maxsize: %(maxsize)X
+
+Installed modules: %(modules)s""" % info)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/doc/Makefile b/testing/web-platform/tests/tools/third_party/html5lib/doc/Makefile
new file mode 100644
index 0000000000..e0e58667e7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/doc/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+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 " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @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)/*
+
+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/html5lib.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/html5lib.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/html5lib"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/html5lib"
+ @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."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @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."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+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."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/doc/changes.rst b/testing/web-platform/tests/tools/third_party/html5lib/doc/changes.rst
new file mode 100644
index 0000000000..ded3b705d0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/doc/changes.rst
@@ -0,0 +1,3 @@
+.. :changelog:
+
+.. include:: ../CHANGES.rst
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/doc/conf.py b/testing/web-platform/tests/tools/third_party/html5lib/doc/conf.py
new file mode 100644
index 0000000000..22ebab4faa
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/doc/conf.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# html5lib documentation build configuration file, created by
+# sphinx-quickstart on Wed May 8 00:04:49 2013.
+#
+# 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
+import os
+
+# -- General configuration -----------------------------------------------------
+
+# 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.viewcode']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'html5lib'
+copyright = '2006 - 2013, James Graham, Sam Sneddon, and contributors'
+
+# 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.
+version = '1.0'
+# The full version, including alpha/beta/rc tags.
+sys.path.append(os.path.abspath('..'))
+from html5lib import __version__ # noqa
+release = __version__
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build', 'theme']
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# -- 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'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'html5libdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'html5lib.tex', 'html5lib Documentation',
+ 'James Graham, Sam Sneddon, and contributors', 'manual'),
+]
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'html5lib', 'html5lib Documentation',
+ ['James Graham, Sam Sneddon, and contributors'], 1)
+]
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'html5lib', 'html5lib Documentation',
+ 'James Graham, Sam Sneddon, and contributors', 'html5lib', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+
+class CExtMock(object):
+ """Required for autodoc on readthedocs.org where you cannot build C extensions."""
+ def __init__(self, *args, **kwargs):
+ pass
+
+ def __call__(self, *args, **kwargs):
+ return CExtMock()
+
+ @classmethod
+ def __getattr__(cls, name):
+ if name in ('__file__', '__path__'):
+ return '/dev/null'
+ else:
+ return CExtMock()
+
+
+try:
+ import lxml # noqa
+except ImportError:
+ sys.modules['lxml'] = CExtMock()
+ sys.modules['lxml.etree'] = CExtMock()
+ print("warning: lxml modules mocked.")
+
+try:
+ import genshi # noqa
+except ImportError:
+ sys.modules['genshi'] = CExtMock()
+ sys.modules['genshi.core'] = CExtMock()
+ print("warning: genshi modules mocked.")
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.filters.rst b/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.filters.rst
new file mode 100644
index 0000000000..d70e4552f0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.filters.rst
@@ -0,0 +1,58 @@
+filters Package
+===============
+
+:mod:`base` Module
+-------------------
+
+.. automodule:: html5lib.filters.base
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`alphabeticalattributes` Module
+------------------------------------
+
+.. automodule:: html5lib.filters.alphabeticalattributes
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`inject_meta_charset` Module
+---------------------------------
+
+.. automodule:: html5lib.filters.inject_meta_charset
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`lint` Module
+------------------
+
+.. automodule:: html5lib.filters.lint
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`optionaltags` Module
+--------------------------
+
+.. automodule:: html5lib.filters.optionaltags
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`sanitizer` Module
+-----------------------
+
+.. automodule:: html5lib.filters.sanitizer
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`whitespace` Module
+------------------------
+
+.. automodule:: html5lib.filters.whitespace
+ :members:
+ :show-inheritance:
+ :special-members: __init__
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.rst b/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.rst
new file mode 100644
index 0000000000..d7c75c5842
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.rst
@@ -0,0 +1,38 @@
+html5lib Package
+================
+
+.. automodule:: html5lib
+ :members: __version__
+
+:mod:`constants` Module
+-----------------------
+
+.. automodule:: html5lib.constants
+ :members:
+ :show-inheritance:
+
+:mod:`html5parser` Module
+-------------------------
+
+.. automodule:: html5lib.html5parser
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`serializer` Module
+------------------------
+
+.. automodule:: html5lib.serializer
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+Subpackages
+-----------
+
+.. toctree::
+
+ html5lib.filters
+ html5lib.treebuilders
+ html5lib.treewalkers
+ html5lib.treeadapters
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.treeadapters.rst b/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.treeadapters.rst
new file mode 100644
index 0000000000..1d3a9fba2f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.treeadapters.rst
@@ -0,0 +1,20 @@
+treeadapters Package
+====================
+
+:mod:`~html5lib.treeadapters` Package
+-------------------------------------
+
+.. automodule:: html5lib.treeadapters
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+.. automodule:: html5lib.treeadapters.genshi
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+.. automodule:: html5lib.treeadapters.sax
+ :members:
+ :show-inheritance:
+ :special-members: __init__
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.treebuilders.rst b/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.treebuilders.rst
new file mode 100644
index 0000000000..1a051e50bd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.treebuilders.rst
@@ -0,0 +1,42 @@
+treebuilders Package
+====================
+
+:mod:`treebuilders` Package
+---------------------------
+
+.. automodule:: html5lib.treebuilders
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`base` Module
+-------------------
+
+.. automodule:: html5lib.treebuilders.base
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`dom` Module
+-----------------
+
+.. automodule:: html5lib.treebuilders.dom
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`etree` Module
+-------------------
+
+.. automodule:: html5lib.treebuilders.etree
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`etree_lxml` Module
+------------------------
+
+.. automodule:: html5lib.treebuilders.etree_lxml
+ :members:
+ :show-inheritance:
+ :special-members: __init__
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.treewalkers.rst b/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.treewalkers.rst
new file mode 100644
index 0000000000..4afef47609
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/doc/html5lib.treewalkers.rst
@@ -0,0 +1,50 @@
+treewalkers Package
+===================
+
+:mod:`treewalkers` Package
+--------------------------
+
+.. automodule:: html5lib.treewalkers
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`base` Module
+------------------
+
+.. automodule:: html5lib.treewalkers.base
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`dom` Module
+-----------------
+
+.. automodule:: html5lib.treewalkers.dom
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`etree` Module
+-------------------
+
+.. automodule:: html5lib.treewalkers.etree
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`etree_lxml` Module
+------------------------
+
+.. automodule:: html5lib.treewalkers.etree_lxml
+ :members:
+ :show-inheritance:
+ :special-members: __init__
+
+:mod:`genshi` Module
+--------------------
+
+.. automodule:: html5lib.treewalkers.genshi
+ :members:
+ :show-inheritance:
+ :special-members: __init__
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/doc/index.rst b/testing/web-platform/tests/tools/third_party/html5lib/doc/index.rst
new file mode 100644
index 0000000000..27104b1469
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/doc/index.rst
@@ -0,0 +1,22 @@
+Overview
+========
+
+.. include:: ../README.rst
+ :start-line: 6
+
+.. toctree::
+ :maxdepth: 2
+
+ movingparts
+ modules
+ changes
+ License <license>
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/doc/license.rst b/testing/web-platform/tests/tools/third_party/html5lib/doc/license.rst
new file mode 100644
index 0000000000..7e6291f3b9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/doc/license.rst
@@ -0,0 +1,4 @@
+License
+=======
+
+.. include:: ../LICENSE
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/doc/make.bat b/testing/web-platform/tests/tools/third_party/html5lib/doc/make.bat
new file mode 100644
index 0000000000..e88c769ce3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/doc/make.bat
@@ -0,0 +1,242 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :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. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\html5lib.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\html5lib.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+:end
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/doc/modules.rst b/testing/web-platform/tests/tools/third_party/html5lib/doc/modules.rst
new file mode 100644
index 0000000000..59fbcc86bc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/doc/modules.rst
@@ -0,0 +1,7 @@
+html5lib
+========
+
+.. toctree::
+ :maxdepth: 4
+
+ html5lib
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/doc/movingparts.rst b/testing/web-platform/tests/tools/third_party/html5lib/doc/movingparts.rst
new file mode 100644
index 0000000000..6ba367a27a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/doc/movingparts.rst
@@ -0,0 +1,165 @@
+The moving parts
+================
+
+html5lib consists of a number of components, which are responsible for
+handling its features.
+
+Parsing uses a *tree builder* to generate a *tree*, the in-memory representation of the document.
+Several tree representations are supported, as are translations to other formats via *tree adapters*.
+The tree may be translated to a token stream with a *tree walker*, from which :class:`~html5lib.serializer.HTMLSerializer` produces a stream of bytes.
+The token stream may also be transformed by use of *filters* to accomplish tasks like sanitization.
+
+Tree builders
+-------------
+
+The parser reads HTML by tokenizing the content and building a tree that
+the user can later access. html5lib can build three types of trees:
+
+* ``etree`` - this is the default; builds a tree based on :mod:`xml.etree`,
+ which can be found in the standard library. Whenever possible, the
+ accelerated ``ElementTree`` implementation (i.e.
+ ``xml.etree.cElementTree`` on Python 2.x) is used.
+
+* ``dom`` - builds a tree based on :mod:`xml.dom.minidom`.
+
+* ``lxml`` - uses the :mod:`lxml.etree` implementation of the ``ElementTree``
+ API. The performance gains are relatively small compared to using the
+ accelerated ``ElementTree`` module.
+
+You can specify the builder by name when using the shorthand API:
+
+.. code-block:: python
+
+ import html5lib
+ with open("mydocument.html", "rb") as f:
+ lxml_etree_document = html5lib.parse(f, treebuilder="lxml")
+
+To get a builder class by name, use the :func:`~html5lib.treebuilders.getTreeBuilder` function.
+
+When instantiating a :class:`~html5lib.html5parser.HTMLParser` object, you must pass a tree builder class via the ``tree`` keyword attribute:
+
+.. code-block:: python
+
+ import html5lib
+ TreeBuilder = html5lib.getTreeBuilder("dom")
+ parser = html5lib.HTMLParser(tree=TreeBuilder)
+ minidom_document = parser.parse("<p>Hello World!")
+
+The implementation of builders can be found in `html5lib/treebuilders/
+<https://github.com/html5lib/html5lib-python/tree/master/html5lib/treebuilders>`_.
+
+
+Tree walkers
+------------
+
+In addition to manipulating a tree directly, you can use a tree walker to generate a streaming view of it.
+html5lib provides walkers for ``etree``, ``dom``, and ``lxml`` trees, as well as ``genshi`` `markup streams <https://genshi.edgewall.org/wiki/Documentation/streams.html>`_.
+
+The implementation of walkers can be found in `html5lib/treewalkers/
+<https://github.com/html5lib/html5lib-python/tree/master/html5lib/treewalkers>`_.
+
+html5lib provides :class:`~html5lib.serializer.HTMLSerializer` for generating a stream of bytes from a token stream, and several filters which manipulate the stream.
+
+HTMLSerializer
+~~~~~~~~~~~~~~
+
+The serializer lets you write HTML back as a stream of bytes.
+
+.. code-block:: pycon
+
+ >>> import html5lib
+ >>> element = html5lib.parse('<p xml:lang="pl">Witam wszystkich')
+ >>> walker = html5lib.getTreeWalker("etree")
+ >>> stream = walker(element)
+ >>> s = html5lib.serializer.HTMLSerializer()
+ >>> output = s.serialize(stream)
+ >>> for item in output:
+ ... print("%r" % item)
+ '<p'
+ ' '
+ 'xml:lang'
+ '='
+ 'pl'
+ '>'
+ 'Witam wszystkich'
+
+You can customize the serializer behaviour in a variety of ways. Consult
+the :class:`~html5lib.serializer.HTMLSerializer` documentation.
+
+
+Filters
+~~~~~~~
+
+html5lib provides several filters:
+
+* :class:`alphabeticalattributes.Filter
+ <html5lib.filters.alphabeticalattributes.Filter>` sorts attributes on
+ tags to be in alphabetical order
+
+* :class:`inject_meta_charset.Filter
+ <html5lib.filters.inject_meta_charset.Filter>` sets a user-specified
+ encoding in the correct ``<meta>`` tag in the ``<head>`` section of
+ the document
+
+* :class:`lint.Filter <html5lib.filters.lint.Filter>` raises
+ :exc:`AssertionError` exceptions on invalid tag and attribute names, invalid
+ PCDATA, etc.
+
+* :class:`optionaltags.Filter <html5lib.filters.optionaltags.Filter>`
+ removes tags from the token stream which are not necessary to produce valid
+ HTML
+
+* :class:`sanitizer.Filter <html5lib.filters.sanitizer.Filter>` removes
+ unsafe markup and CSS. Elements that are known to be safe are passed
+ through and the rest is converted to visible text. The default
+ configuration of the sanitizer follows the `WHATWG Sanitization Rules
+ <http://wiki.whatwg.org/wiki/Sanitization_rules>`_.
+
+* :class:`whitespace.Filter <html5lib.filters.whitespace.Filter>`
+ collapses all whitespace characters to single spaces unless they're in
+ ``<pre/>`` or ``<textarea/>`` tags.
+
+To use a filter, simply wrap it around a token stream:
+
+.. code-block:: python
+
+ >>> import html5lib
+ >>> from html5lib.filters import sanitizer
+ >>> dom = html5lib.parse("<p><script>alert('Boo!')", treebuilder="dom")
+ >>> walker = html5lib.getTreeWalker("dom")
+ >>> stream = walker(dom)
+ >>> clean_stream = sanitizer.Filter(stream)
+
+
+Tree adapters
+-------------
+
+Tree adapters can be used to translate between tree formats.
+Two adapters are provided by html5lib:
+
+* :func:`html5lib.treeadapters.genshi.to_genshi()` generates a `Genshi markup stream <https://genshi.edgewall.org/wiki/Documentation/streams.html>`_.
+* :func:`html5lib.treeadapters.sax.to_sax()` calls a SAX handler based on the tree.
+
+Encoding discovery
+------------------
+
+Parsed trees are always Unicode. However a large variety of input
+encodings are supported. The encoding of the document is determined in
+the following way:
+
+* The encoding may be explicitly specified by passing the name of the
+ encoding as the encoding parameter to the
+ :meth:`~html5lib.html5parser.HTMLParser.parse` method on
+ :class:`~html5lib.html5parser.HTMLParser` objects.
+
+* If no encoding is specified, the parser will attempt to detect the
+ encoding from a ``<meta>`` element in the first 512 bytes of the
+ document (this is only a partial implementation of the current HTML
+ specification).
+
+* If no encoding can be found and the :mod:`chardet` library is available, an
+ attempt will be made to sniff the encoding from the byte pattern.
+
+* If all else fails, the default encoding will be used. This is usually
+ `Windows-1252 <http://en.wikipedia.org/wiki/Windows-1252>`_, which is
+ a common fallback used by Web browsers.
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/flake8-run.sh b/testing/web-platform/tests/tools/third_party/html5lib/flake8-run.sh
new file mode 100755
index 0000000000..d926494699
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/flake8-run.sh
@@ -0,0 +1,9 @@
+#!/bin/bash -e
+
+if [[ ! -x $(which flake8) ]]; then
+ echo "fatal: flake8 not found on $PATH. Exiting."
+ exit 1
+fi
+
+flake8 `dirname $0`
+exit $?
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/__init__.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/__init__.py
new file mode 100644
index 0000000000..7b854f9900
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/__init__.py
@@ -0,0 +1,35 @@
+"""
+HTML parsing library based on the `WHATWG HTML specification
+<https://whatwg.org/html>`_. The parser is designed to be compatible with
+existing HTML found in the wild and implements well-defined error recovery that
+is largely compatible with modern desktop web browsers.
+
+Example usage::
+
+ import html5lib
+ with open("my_document.html", "rb") as f:
+ tree = html5lib.parse(f)
+
+For convenience, this module re-exports the following names:
+
+* :func:`~.html5parser.parse`
+* :func:`~.html5parser.parseFragment`
+* :class:`~.html5parser.HTMLParser`
+* :func:`~.treebuilders.getTreeBuilder`
+* :func:`~.treewalkers.getTreeWalker`
+* :func:`~.serializer.serialize`
+"""
+
+from __future__ import absolute_import, division, unicode_literals
+
+from .html5parser import HTMLParser, parse, parseFragment
+from .treebuilders import getTreeBuilder
+from .treewalkers import getTreeWalker
+from .serializer import serialize
+
+__all__ = ["HTMLParser", "parse", "parseFragment", "getTreeBuilder",
+ "getTreeWalker", "serialize"]
+
+# this has to be at the top level, see how setup.py parses this
+#: Distribution version number.
+__version__ = "1.2-dev"
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_ihatexml.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_ihatexml.py
new file mode 100644
index 0000000000..3ff803c195
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_ihatexml.py
@@ -0,0 +1,289 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import re
+import warnings
+
+from .constants import DataLossWarning
+
+baseChar = """
+[#x0041-#x005A] | [#x0061-#x007A] | [#x00C0-#x00D6] | [#x00D8-#x00F6] |
+[#x00F8-#x00FF] | [#x0100-#x0131] | [#x0134-#x013E] | [#x0141-#x0148] |
+[#x014A-#x017E] | [#x0180-#x01C3] | [#x01CD-#x01F0] | [#x01F4-#x01F5] |
+[#x01FA-#x0217] | [#x0250-#x02A8] | [#x02BB-#x02C1] | #x0386 |
+[#x0388-#x038A] | #x038C | [#x038E-#x03A1] | [#x03A3-#x03CE] |
+[#x03D0-#x03D6] | #x03DA | #x03DC | #x03DE | #x03E0 | [#x03E2-#x03F3] |
+[#x0401-#x040C] | [#x040E-#x044F] | [#x0451-#x045C] | [#x045E-#x0481] |
+[#x0490-#x04C4] | [#x04C7-#x04C8] | [#x04CB-#x04CC] | [#x04D0-#x04EB] |
+[#x04EE-#x04F5] | [#x04F8-#x04F9] | [#x0531-#x0556] | #x0559 |
+[#x0561-#x0586] | [#x05D0-#x05EA] | [#x05F0-#x05F2] | [#x0621-#x063A] |
+[#x0641-#x064A] | [#x0671-#x06B7] | [#x06BA-#x06BE] | [#x06C0-#x06CE] |
+[#x06D0-#x06D3] | #x06D5 | [#x06E5-#x06E6] | [#x0905-#x0939] | #x093D |
+[#x0958-#x0961] | [#x0985-#x098C] | [#x098F-#x0990] | [#x0993-#x09A8] |
+[#x09AA-#x09B0] | #x09B2 | [#x09B6-#x09B9] | [#x09DC-#x09DD] |
+[#x09DF-#x09E1] | [#x09F0-#x09F1] | [#x0A05-#x0A0A] | [#x0A0F-#x0A10] |
+[#x0A13-#x0A28] | [#x0A2A-#x0A30] | [#x0A32-#x0A33] | [#x0A35-#x0A36] |
+[#x0A38-#x0A39] | [#x0A59-#x0A5C] | #x0A5E | [#x0A72-#x0A74] |
+[#x0A85-#x0A8B] | #x0A8D | [#x0A8F-#x0A91] | [#x0A93-#x0AA8] |
+[#x0AAA-#x0AB0] | [#x0AB2-#x0AB3] | [#x0AB5-#x0AB9] | #x0ABD | #x0AE0 |
+[#x0B05-#x0B0C] | [#x0B0F-#x0B10] | [#x0B13-#x0B28] | [#x0B2A-#x0B30] |
+[#x0B32-#x0B33] | [#x0B36-#x0B39] | #x0B3D | [#x0B5C-#x0B5D] |
+[#x0B5F-#x0B61] | [#x0B85-#x0B8A] | [#x0B8E-#x0B90] | [#x0B92-#x0B95] |
+[#x0B99-#x0B9A] | #x0B9C | [#x0B9E-#x0B9F] | [#x0BA3-#x0BA4] |
+[#x0BA8-#x0BAA] | [#x0BAE-#x0BB5] | [#x0BB7-#x0BB9] | [#x0C05-#x0C0C] |
+[#x0C0E-#x0C10] | [#x0C12-#x0C28] | [#x0C2A-#x0C33] | [#x0C35-#x0C39] |
+[#x0C60-#x0C61] | [#x0C85-#x0C8C] | [#x0C8E-#x0C90] | [#x0C92-#x0CA8] |
+[#x0CAA-#x0CB3] | [#x0CB5-#x0CB9] | #x0CDE | [#x0CE0-#x0CE1] |
+[#x0D05-#x0D0C] | [#x0D0E-#x0D10] | [#x0D12-#x0D28] | [#x0D2A-#x0D39] |
+[#x0D60-#x0D61] | [#x0E01-#x0E2E] | #x0E30 | [#x0E32-#x0E33] |
+[#x0E40-#x0E45] | [#x0E81-#x0E82] | #x0E84 | [#x0E87-#x0E88] | #x0E8A |
+#x0E8D | [#x0E94-#x0E97] | [#x0E99-#x0E9F] | [#x0EA1-#x0EA3] | #x0EA5 |
+#x0EA7 | [#x0EAA-#x0EAB] | [#x0EAD-#x0EAE] | #x0EB0 | [#x0EB2-#x0EB3] |
+#x0EBD | [#x0EC0-#x0EC4] | [#x0F40-#x0F47] | [#x0F49-#x0F69] |
+[#x10A0-#x10C5] | [#x10D0-#x10F6] | #x1100 | [#x1102-#x1103] |
+[#x1105-#x1107] | #x1109 | [#x110B-#x110C] | [#x110E-#x1112] | #x113C |
+#x113E | #x1140 | #x114C | #x114E | #x1150 | [#x1154-#x1155] | #x1159 |
+[#x115F-#x1161] | #x1163 | #x1165 | #x1167 | #x1169 | [#x116D-#x116E] |
+[#x1172-#x1173] | #x1175 | #x119E | #x11A8 | #x11AB | [#x11AE-#x11AF] |
+[#x11B7-#x11B8] | #x11BA | [#x11BC-#x11C2] | #x11EB | #x11F0 | #x11F9 |
+[#x1E00-#x1E9B] | [#x1EA0-#x1EF9] | [#x1F00-#x1F15] | [#x1F18-#x1F1D] |
+[#x1F20-#x1F45] | [#x1F48-#x1F4D] | [#x1F50-#x1F57] | #x1F59 | #x1F5B |
+#x1F5D | [#x1F5F-#x1F7D] | [#x1F80-#x1FB4] | [#x1FB6-#x1FBC] | #x1FBE |
+[#x1FC2-#x1FC4] | [#x1FC6-#x1FCC] | [#x1FD0-#x1FD3] | [#x1FD6-#x1FDB] |
+[#x1FE0-#x1FEC] | [#x1FF2-#x1FF4] | [#x1FF6-#x1FFC] | #x2126 |
+[#x212A-#x212B] | #x212E | [#x2180-#x2182] | [#x3041-#x3094] |
+[#x30A1-#x30FA] | [#x3105-#x312C] | [#xAC00-#xD7A3]"""
+
+ideographic = """[#x4E00-#x9FA5] | #x3007 | [#x3021-#x3029]"""
+
+combiningCharacter = """
+[#x0300-#x0345] | [#x0360-#x0361] | [#x0483-#x0486] | [#x0591-#x05A1] |
+[#x05A3-#x05B9] | [#x05BB-#x05BD] | #x05BF | [#x05C1-#x05C2] | #x05C4 |
+[#x064B-#x0652] | #x0670 | [#x06D6-#x06DC] | [#x06DD-#x06DF] |
+[#x06E0-#x06E4] | [#x06E7-#x06E8] | [#x06EA-#x06ED] | [#x0901-#x0903] |
+#x093C | [#x093E-#x094C] | #x094D | [#x0951-#x0954] | [#x0962-#x0963] |
+[#x0981-#x0983] | #x09BC | #x09BE | #x09BF | [#x09C0-#x09C4] |
+[#x09C7-#x09C8] | [#x09CB-#x09CD] | #x09D7 | [#x09E2-#x09E3] | #x0A02 |
+#x0A3C | #x0A3E | #x0A3F | [#x0A40-#x0A42] | [#x0A47-#x0A48] |
+[#x0A4B-#x0A4D] | [#x0A70-#x0A71] | [#x0A81-#x0A83] | #x0ABC |
+[#x0ABE-#x0AC5] | [#x0AC7-#x0AC9] | [#x0ACB-#x0ACD] | [#x0B01-#x0B03] |
+#x0B3C | [#x0B3E-#x0B43] | [#x0B47-#x0B48] | [#x0B4B-#x0B4D] |
+[#x0B56-#x0B57] | [#x0B82-#x0B83] | [#x0BBE-#x0BC2] | [#x0BC6-#x0BC8] |
+[#x0BCA-#x0BCD] | #x0BD7 | [#x0C01-#x0C03] | [#x0C3E-#x0C44] |
+[#x0C46-#x0C48] | [#x0C4A-#x0C4D] | [#x0C55-#x0C56] | [#x0C82-#x0C83] |
+[#x0CBE-#x0CC4] | [#x0CC6-#x0CC8] | [#x0CCA-#x0CCD] | [#x0CD5-#x0CD6] |
+[#x0D02-#x0D03] | [#x0D3E-#x0D43] | [#x0D46-#x0D48] | [#x0D4A-#x0D4D] |
+#x0D57 | #x0E31 | [#x0E34-#x0E3A] | [#x0E47-#x0E4E] | #x0EB1 |
+[#x0EB4-#x0EB9] | [#x0EBB-#x0EBC] | [#x0EC8-#x0ECD] | [#x0F18-#x0F19] |
+#x0F35 | #x0F37 | #x0F39 | #x0F3E | #x0F3F | [#x0F71-#x0F84] |
+[#x0F86-#x0F8B] | [#x0F90-#x0F95] | #x0F97 | [#x0F99-#x0FAD] |
+[#x0FB1-#x0FB7] | #x0FB9 | [#x20D0-#x20DC] | #x20E1 | [#x302A-#x302F] |
+#x3099 | #x309A"""
+
+digit = """
+[#x0030-#x0039] | [#x0660-#x0669] | [#x06F0-#x06F9] | [#x0966-#x096F] |
+[#x09E6-#x09EF] | [#x0A66-#x0A6F] | [#x0AE6-#x0AEF] | [#x0B66-#x0B6F] |
+[#x0BE7-#x0BEF] | [#x0C66-#x0C6F] | [#x0CE6-#x0CEF] | [#x0D66-#x0D6F] |
+[#x0E50-#x0E59] | [#x0ED0-#x0ED9] | [#x0F20-#x0F29]"""
+
+extender = """
+#x00B7 | #x02D0 | #x02D1 | #x0387 | #x0640 | #x0E46 | #x0EC6 | #x3005 |
+#[#x3031-#x3035] | [#x309D-#x309E] | [#x30FC-#x30FE]"""
+
+letter = " | ".join([baseChar, ideographic])
+
+# Without the
+name = " | ".join([letter, digit, ".", "-", "_", combiningCharacter,
+ extender])
+nameFirst = " | ".join([letter, "_"])
+
+reChar = re.compile(r"#x([\d|A-F]{4,4})")
+reCharRange = re.compile(r"\[#x([\d|A-F]{4,4})-#x([\d|A-F]{4,4})\]")
+
+
+def charStringToList(chars):
+ charRanges = [item.strip() for item in chars.split(" | ")]
+ rv = []
+ for item in charRanges:
+ foundMatch = False
+ for regexp in (reChar, reCharRange):
+ match = regexp.match(item)
+ if match is not None:
+ rv.append([hexToInt(item) for item in match.groups()])
+ if len(rv[-1]) == 1:
+ rv[-1] = rv[-1] * 2
+ foundMatch = True
+ break
+ if not foundMatch:
+ assert len(item) == 1
+
+ rv.append([ord(item)] * 2)
+ rv = normaliseCharList(rv)
+ return rv
+
+
+def normaliseCharList(charList):
+ charList = sorted(charList)
+ for item in charList:
+ assert item[1] >= item[0]
+ rv = []
+ i = 0
+ while i < len(charList):
+ j = 1
+ rv.append(charList[i])
+ while i + j < len(charList) and charList[i + j][0] <= rv[-1][1] + 1:
+ rv[-1][1] = charList[i + j][1]
+ j += 1
+ i += j
+ return rv
+
+
+# We don't really support characters above the BMP :(
+max_unicode = int("FFFF", 16)
+
+
+def missingRanges(charList):
+ rv = []
+ if charList[0] != 0:
+ rv.append([0, charList[0][0] - 1])
+ for i, item in enumerate(charList[:-1]):
+ rv.append([item[1] + 1, charList[i + 1][0] - 1])
+ if charList[-1][1] != max_unicode:
+ rv.append([charList[-1][1] + 1, max_unicode])
+ return rv
+
+
+def listToRegexpStr(charList):
+ rv = []
+ for item in charList:
+ if item[0] == item[1]:
+ rv.append(escapeRegexp(chr(item[0])))
+ else:
+ rv.append(escapeRegexp(chr(item[0])) + "-" +
+ escapeRegexp(chr(item[1])))
+ return "[%s]" % "".join(rv)
+
+
+def hexToInt(hex_str):
+ return int(hex_str, 16)
+
+
+def escapeRegexp(string):
+ specialCharacters = (".", "^", "$", "*", "+", "?", "{", "}",
+ "[", "]", "|", "(", ")", "-")
+ for char in specialCharacters:
+ string = string.replace(char, "\\" + char)
+
+ return string
+
+# output from the above
+nonXmlNameBMPRegexp = re.compile('[\x00-,/:-@\\[-\\^`\\{-\xb6\xb8-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u02cf\u02d2-\u02ff\u0346-\u035f\u0362-\u0385\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482\u0487-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u0590\u05a2\u05ba\u05be\u05c0\u05c3\u05c5-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u063f\u0653-\u065f\u066a-\u066f\u06b8-\u06b9\u06bf\u06cf\u06d4\u06e9\u06ee-\u06ef\u06fa-\u0900\u0904\u093a-\u093b\u094e-\u0950\u0955-\u0957\u0964-\u0965\u0970-\u0980\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09bb\u09bd\u09c5-\u09c6\u09c9-\u09ca\u09ce-\u09d6\u09d8-\u09db\u09de\u09e4-\u09e5\u09f2-\u0a01\u0a03-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a3b\u0a3d\u0a43-\u0a46\u0a49-\u0a4a\u0a4e-\u0a58\u0a5d\u0a5f-\u0a65\u0a75-\u0a80\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abb\u0ac6\u0aca\u0ace-\u0adf\u0ae1-\u0ae5\u0af0-\u0b00\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3b\u0b44-\u0b46\u0b49-\u0b4a\u0b4e-\u0b55\u0b58-\u0b5b\u0b5e\u0b62-\u0b65\u0b70-\u0b81\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0bbd\u0bc3-\u0bc5\u0bc9\u0bce-\u0bd6\u0bd8-\u0be6\u0bf0-\u0c00\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c3d\u0c45\u0c49\u0c4e-\u0c54\u0c57-\u0c5f\u0c62-\u0c65\u0c70-\u0c81\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cbd\u0cc5\u0cc9\u0cce-\u0cd4\u0cd7-\u0cdd\u0cdf\u0ce2-\u0ce5\u0cf0-\u0d01\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d3d\u0d44-\u0d45\u0d49\u0d4e-\u0d56\u0d58-\u0d5f\u0d62-\u0d65\u0d70-\u0e00\u0e2f\u0e3b-\u0e3f\u0e4f\u0e5a-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eba\u0ebe-\u0ebf\u0ec5\u0ec7\u0ece-\u0ecf\u0eda-\u0f17\u0f1a-\u0f1f\u0f2a-\u0f34\u0f36\u0f38\u0f3a-\u0f3d\u0f48\u0f6a-\u0f70\u0f85\u0f8c-\u0f8f\u0f96\u0f98\u0fae-\u0fb0\u0fb8\u0fba-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u20cf\u20dd-\u20e0\u20e2-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3004\u3006\u3008-\u3020\u3030\u3036-\u3040\u3095-\u3098\u309b-\u309c\u309f-\u30a0\u30fb\u30ff-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') # noqa
+
+nonXmlNameFirstBMPRegexp = re.compile('[\x00-@\\[-\\^`\\{-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u0385\u0387\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u0640\u064b-\u0670\u06b8-\u06b9\u06bf\u06cf\u06d4\u06d6-\u06e4\u06e7-\u0904\u093a-\u093c\u093e-\u0957\u0962-\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09db\u09de\u09e2-\u09ef\u09f2-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a58\u0a5d\u0a5f-\u0a71\u0a75-\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abc\u0abe-\u0adf\u0ae1-\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3c\u0b3e-\u0b5b\u0b5e\u0b62-\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c5f\u0c62-\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cdd\u0cdf\u0ce2-\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d5f\u0d62-\u0e00\u0e2f\u0e31\u0e34-\u0e3f\u0e46-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eb1\u0eb4-\u0ebc\u0ebe-\u0ebf\u0ec5-\u0f3f\u0f48\u0f6a-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3006\u3008-\u3020\u302a-\u3040\u3095-\u30a0\u30fb-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') # noqa
+
+# Simpler things
+nonPubidCharRegexp = re.compile("[^\x20\x0D\x0Aa-zA-Z0-9\\-'()+,./:=?;!*#@$_%]")
+
+
+class InfosetFilter(object):
+ replacementRegexp = re.compile(r"U[\dA-F]{5,5}")
+
+ def __init__(self,
+ dropXmlnsLocalName=False,
+ dropXmlnsAttrNs=False,
+ preventDoubleDashComments=False,
+ preventDashAtCommentEnd=False,
+ replaceFormFeedCharacters=True,
+ preventSingleQuotePubid=False):
+
+ self.dropXmlnsLocalName = dropXmlnsLocalName
+ self.dropXmlnsAttrNs = dropXmlnsAttrNs
+
+ self.preventDoubleDashComments = preventDoubleDashComments
+ self.preventDashAtCommentEnd = preventDashAtCommentEnd
+
+ self.replaceFormFeedCharacters = replaceFormFeedCharacters
+
+ self.preventSingleQuotePubid = preventSingleQuotePubid
+
+ self.replaceCache = {}
+
+ def coerceAttribute(self, name, namespace=None):
+ if self.dropXmlnsLocalName and name.startswith("xmlns:"):
+ warnings.warn("Attributes cannot begin with xmlns", DataLossWarning)
+ return None
+ elif (self.dropXmlnsAttrNs and
+ namespace == "http://www.w3.org/2000/xmlns/"):
+ warnings.warn("Attributes cannot be in the xml namespace", DataLossWarning)
+ return None
+ else:
+ return self.toXmlName(name)
+
+ def coerceElement(self, name):
+ return self.toXmlName(name)
+
+ def coerceComment(self, data):
+ if self.preventDoubleDashComments:
+ while "--" in data:
+ warnings.warn("Comments cannot contain adjacent dashes", DataLossWarning)
+ data = data.replace("--", "- -")
+ if data.endswith("-"):
+ warnings.warn("Comments cannot end in a dash", DataLossWarning)
+ data += " "
+ return data
+
+ def coerceCharacters(self, data):
+ if self.replaceFormFeedCharacters:
+ for _ in range(data.count("\x0C")):
+ warnings.warn("Text cannot contain U+000C", DataLossWarning)
+ data = data.replace("\x0C", " ")
+ # Other non-xml characters
+ return data
+
+ def coercePubid(self, data):
+ dataOutput = data
+ for char in nonPubidCharRegexp.findall(data):
+ warnings.warn("Coercing non-XML pubid", DataLossWarning)
+ replacement = self.getReplacementCharacter(char)
+ dataOutput = dataOutput.replace(char, replacement)
+ if self.preventSingleQuotePubid and dataOutput.find("'") >= 0:
+ warnings.warn("Pubid cannot contain single quote", DataLossWarning)
+ dataOutput = dataOutput.replace("'", self.getReplacementCharacter("'"))
+ return dataOutput
+
+ def toXmlName(self, name):
+ nameFirst = name[0]
+ nameRest = name[1:]
+ m = nonXmlNameFirstBMPRegexp.match(nameFirst)
+ if m:
+ warnings.warn("Coercing non-XML name: %s" % name, DataLossWarning)
+ nameFirstOutput = self.getReplacementCharacter(nameFirst)
+ else:
+ nameFirstOutput = nameFirst
+
+ nameRestOutput = nameRest
+ replaceChars = set(nonXmlNameBMPRegexp.findall(nameRest))
+ for char in replaceChars:
+ warnings.warn("Coercing non-XML name: %s" % name, DataLossWarning)
+ replacement = self.getReplacementCharacter(char)
+ nameRestOutput = nameRestOutput.replace(char, replacement)
+ return nameFirstOutput + nameRestOutput
+
+ def getReplacementCharacter(self, char):
+ if char in self.replaceCache:
+ replacement = self.replaceCache[char]
+ else:
+ replacement = self.escapeChar(char)
+ return replacement
+
+ def fromXmlName(self, name):
+ for item in set(self.replacementRegexp.findall(name)):
+ name = name.replace(item, self.unescapeChar(item))
+ return name
+
+ def escapeChar(self, char):
+ replacement = "U%05X" % ord(char)
+ self.replaceCache[char] = replacement
+ return replacement
+
+ def unescapeChar(self, charcode):
+ return chr(int(charcode[1:], 16))
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_inputstream.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_inputstream.py
new file mode 100644
index 0000000000..0207dd211b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_inputstream.py
@@ -0,0 +1,918 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from six import text_type
+from six.moves import http_client, urllib
+
+import codecs
+import re
+from io import BytesIO, StringIO
+
+import webencodings
+
+from .constants import EOF, spaceCharacters, asciiLetters, asciiUppercase
+from .constants import _ReparseException
+from . import _utils
+
+# Non-unicode versions of constants for use in the pre-parser
+spaceCharactersBytes = frozenset([item.encode("ascii") for item in spaceCharacters])
+asciiLettersBytes = frozenset([item.encode("ascii") for item in asciiLetters])
+asciiUppercaseBytes = frozenset([item.encode("ascii") for item in asciiUppercase])
+spacesAngleBrackets = spaceCharactersBytes | frozenset([b">", b"<"])
+
+
+invalid_unicode_no_surrogate = "[\u0001-\u0008\u000B\u000E-\u001F\u007F-\u009F\uFDD0-\uFDEF\uFFFE\uFFFF\U0001FFFE\U0001FFFF\U0002FFFE\U0002FFFF\U0003FFFE\U0003FFFF\U0004FFFE\U0004FFFF\U0005FFFE\U0005FFFF\U0006FFFE\U0006FFFF\U0007FFFE\U0007FFFF\U0008FFFE\U0008FFFF\U0009FFFE\U0009FFFF\U000AFFFE\U000AFFFF\U000BFFFE\U000BFFFF\U000CFFFE\U000CFFFF\U000DFFFE\U000DFFFF\U000EFFFE\U000EFFFF\U000FFFFE\U000FFFFF\U0010FFFE\U0010FFFF]" # noqa
+
+if _utils.supports_lone_surrogates:
+ # Use one extra step of indirection and create surrogates with
+ # eval. Not using this indirection would introduce an illegal
+ # unicode literal on platforms not supporting such lone
+ # surrogates.
+ assert invalid_unicode_no_surrogate[-1] == "]" and invalid_unicode_no_surrogate.count("]") == 1
+ invalid_unicode_re = re.compile(invalid_unicode_no_surrogate[:-1] +
+ eval('"\\uD800-\\uDFFF"') + # pylint:disable=eval-used
+ "]")
+else:
+ invalid_unicode_re = re.compile(invalid_unicode_no_surrogate)
+
+non_bmp_invalid_codepoints = {0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE,
+ 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF,
+ 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE,
+ 0x8FFFF, 0x9FFFE, 0x9FFFF, 0xAFFFE, 0xAFFFF,
+ 0xBFFFE, 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE,
+ 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF,
+ 0x10FFFE, 0x10FFFF}
+
+ascii_punctuation_re = re.compile("[\u0009-\u000D\u0020-\u002F\u003A-\u0040\u005C\u005B-\u0060\u007B-\u007E]")
+
+# Cache for charsUntil()
+charsUntilRegEx = {}
+
+
+class BufferedStream(object):
+ """Buffering for streams that do not have buffering of their own
+
+ The buffer is implemented as a list of chunks on the assumption that
+ joining many strings will be slow since it is O(n**2)
+ """
+
+ def __init__(self, stream):
+ self.stream = stream
+ self.buffer = []
+ self.position = [-1, 0] # chunk number, offset
+
+ def tell(self):
+ pos = 0
+ for chunk in self.buffer[:self.position[0]]:
+ pos += len(chunk)
+ pos += self.position[1]
+ return pos
+
+ def seek(self, pos):
+ assert pos <= self._bufferedBytes()
+ offset = pos
+ i = 0
+ while len(self.buffer[i]) < offset:
+ offset -= len(self.buffer[i])
+ i += 1
+ self.position = [i, offset]
+
+ def read(self, bytes):
+ if not self.buffer:
+ return self._readStream(bytes)
+ elif (self.position[0] == len(self.buffer) and
+ self.position[1] == len(self.buffer[-1])):
+ return self._readStream(bytes)
+ else:
+ return self._readFromBuffer(bytes)
+
+ def _bufferedBytes(self):
+ return sum([len(item) for item in self.buffer])
+
+ def _readStream(self, bytes):
+ data = self.stream.read(bytes)
+ self.buffer.append(data)
+ self.position[0] += 1
+ self.position[1] = len(data)
+ return data
+
+ def _readFromBuffer(self, bytes):
+ remainingBytes = bytes
+ rv = []
+ bufferIndex = self.position[0]
+ bufferOffset = self.position[1]
+ while bufferIndex < len(self.buffer) and remainingBytes != 0:
+ assert remainingBytes > 0
+ bufferedData = self.buffer[bufferIndex]
+
+ if remainingBytes <= len(bufferedData) - bufferOffset:
+ bytesToRead = remainingBytes
+ self.position = [bufferIndex, bufferOffset + bytesToRead]
+ else:
+ bytesToRead = len(bufferedData) - bufferOffset
+ self.position = [bufferIndex, len(bufferedData)]
+ bufferIndex += 1
+ rv.append(bufferedData[bufferOffset:bufferOffset + bytesToRead])
+ remainingBytes -= bytesToRead
+
+ bufferOffset = 0
+
+ if remainingBytes:
+ rv.append(self._readStream(remainingBytes))
+
+ return b"".join(rv)
+
+
+def HTMLInputStream(source, **kwargs):
+ # Work around Python bug #20007: read(0) closes the connection.
+ # http://bugs.python.org/issue20007
+ if (isinstance(source, http_client.HTTPResponse) or
+ # Also check for addinfourl wrapping HTTPResponse
+ (isinstance(source, urllib.response.addbase) and
+ isinstance(source.fp, http_client.HTTPResponse))):
+ isUnicode = False
+ elif hasattr(source, "read"):
+ isUnicode = isinstance(source.read(0), text_type)
+ else:
+ isUnicode = isinstance(source, text_type)
+
+ if isUnicode:
+ encodings = [x for x in kwargs if x.endswith("_encoding")]
+ if encodings:
+ raise TypeError("Cannot set an encoding with a unicode input, set %r" % encodings)
+
+ return HTMLUnicodeInputStream(source, **kwargs)
+ else:
+ return HTMLBinaryInputStream(source, **kwargs)
+
+
+class HTMLUnicodeInputStream(object):
+ """Provides a unicode stream of characters to the HTMLTokenizer.
+
+ This class takes care of character encoding and removing or replacing
+ incorrect byte-sequences and also provides column and line tracking.
+
+ """
+
+ _defaultChunkSize = 10240
+
+ def __init__(self, source):
+ """Initialises the HTMLInputStream.
+
+ HTMLInputStream(source, [encoding]) -> Normalized stream from source
+ for use by html5lib.
+
+ source can be either a file-object, local filename or a string.
+
+ The optional encoding parameter must be a string that indicates
+ the encoding. If specified, that encoding will be used,
+ regardless of any BOM or later declaration (such as in a meta
+ element)
+
+ """
+
+ if not _utils.supports_lone_surrogates:
+ # Such platforms will have already checked for such
+ # surrogate errors, so no need to do this checking.
+ self.reportCharacterErrors = None
+ elif len("\U0010FFFF") == 1:
+ self.reportCharacterErrors = self.characterErrorsUCS4
+ else:
+ self.reportCharacterErrors = self.characterErrorsUCS2
+
+ # List of where new lines occur
+ self.newLines = [0]
+
+ self.charEncoding = (lookupEncoding("utf-8"), "certain")
+ self.dataStream = self.openStream(source)
+
+ self.reset()
+
+ def reset(self):
+ self.chunk = ""
+ self.chunkSize = 0
+ self.chunkOffset = 0
+ self.errors = []
+
+ # number of (complete) lines in previous chunks
+ self.prevNumLines = 0
+ # number of columns in the last line of the previous chunk
+ self.prevNumCols = 0
+
+ # Deal with CR LF and surrogates split over chunk boundaries
+ self._bufferedCharacter = None
+
+ def openStream(self, source):
+ """Produces a file object from source.
+
+ source can be either a file object, local filename or a string.
+
+ """
+ # Already a file object
+ if hasattr(source, 'read'):
+ stream = source
+ else:
+ stream = StringIO(source)
+
+ return stream
+
+ def _position(self, offset):
+ chunk = self.chunk
+ nLines = chunk.count('\n', 0, offset)
+ positionLine = self.prevNumLines + nLines
+ lastLinePos = chunk.rfind('\n', 0, offset)
+ if lastLinePos == -1:
+ positionColumn = self.prevNumCols + offset
+ else:
+ positionColumn = offset - (lastLinePos + 1)
+ return (positionLine, positionColumn)
+
+ def position(self):
+ """Returns (line, col) of the current position in the stream."""
+ line, col = self._position(self.chunkOffset)
+ return (line + 1, col)
+
+ def char(self):
+ """ Read one character from the stream or queue if available. Return
+ EOF when EOF is reached.
+ """
+ # Read a new chunk from the input stream if necessary
+ if self.chunkOffset >= self.chunkSize:
+ if not self.readChunk():
+ return EOF
+
+ chunkOffset = self.chunkOffset
+ char = self.chunk[chunkOffset]
+ self.chunkOffset = chunkOffset + 1
+
+ return char
+
+ def readChunk(self, chunkSize=None):
+ if chunkSize is None:
+ chunkSize = self._defaultChunkSize
+
+ self.prevNumLines, self.prevNumCols = self._position(self.chunkSize)
+
+ self.chunk = ""
+ self.chunkSize = 0
+ self.chunkOffset = 0
+
+ data = self.dataStream.read(chunkSize)
+
+ # Deal with CR LF and surrogates broken across chunks
+ if self._bufferedCharacter:
+ data = self._bufferedCharacter + data
+ self._bufferedCharacter = None
+ elif not data:
+ # We have no more data, bye-bye stream
+ return False
+
+ if len(data) > 1:
+ lastv = ord(data[-1])
+ if lastv == 0x0D or 0xD800 <= lastv <= 0xDBFF:
+ self._bufferedCharacter = data[-1]
+ data = data[:-1]
+
+ if self.reportCharacterErrors:
+ self.reportCharacterErrors(data)
+
+ # Replace invalid characters
+ data = data.replace("\r\n", "\n")
+ data = data.replace("\r", "\n")
+
+ self.chunk = data
+ self.chunkSize = len(data)
+
+ return True
+
+ def characterErrorsUCS4(self, data):
+ for _ in range(len(invalid_unicode_re.findall(data))):
+ self.errors.append("invalid-codepoint")
+
+ def characterErrorsUCS2(self, data):
+ # Someone picked the wrong compile option
+ # You lose
+ skip = False
+ for match in invalid_unicode_re.finditer(data):
+ if skip:
+ continue
+ codepoint = ord(match.group())
+ pos = match.start()
+ # Pretty sure there should be endianness issues here
+ if _utils.isSurrogatePair(data[pos:pos + 2]):
+ # We have a surrogate pair!
+ char_val = _utils.surrogatePairToCodepoint(data[pos:pos + 2])
+ if char_val in non_bmp_invalid_codepoints:
+ self.errors.append("invalid-codepoint")
+ skip = True
+ elif (codepoint >= 0xD800 and codepoint <= 0xDFFF and
+ pos == len(data) - 1):
+ self.errors.append("invalid-codepoint")
+ else:
+ skip = False
+ self.errors.append("invalid-codepoint")
+
+ def charsUntil(self, characters, opposite=False):
+ """ Returns a string of characters from the stream up to but not
+ including any character in 'characters' or EOF. 'characters' must be
+ a container that supports the 'in' method and iteration over its
+ characters.
+ """
+
+ # Use a cache of regexps to find the required characters
+ try:
+ chars = charsUntilRegEx[(characters, opposite)]
+ except KeyError:
+ if __debug__:
+ for c in characters:
+ assert(ord(c) < 128)
+ regex = "".join(["\\x%02x" % ord(c) for c in characters])
+ if not opposite:
+ regex = "^%s" % regex
+ chars = charsUntilRegEx[(characters, opposite)] = re.compile("[%s]+" % regex)
+
+ rv = []
+
+ while True:
+ # Find the longest matching prefix
+ m = chars.match(self.chunk, self.chunkOffset)
+ if m is None:
+ # If nothing matched, and it wasn't because we ran out of chunk,
+ # then stop
+ if self.chunkOffset != self.chunkSize:
+ break
+ else:
+ end = m.end()
+ # If not the whole chunk matched, return everything
+ # up to the part that didn't match
+ if end != self.chunkSize:
+ rv.append(self.chunk[self.chunkOffset:end])
+ self.chunkOffset = end
+ break
+ # If the whole remainder of the chunk matched,
+ # use it all and read the next chunk
+ rv.append(self.chunk[self.chunkOffset:])
+ if not self.readChunk():
+ # Reached EOF
+ break
+
+ r = "".join(rv)
+ return r
+
+ def unget(self, char):
+ # Only one character is allowed to be ungotten at once - it must
+ # be consumed again before any further call to unget
+ if char is not EOF:
+ if self.chunkOffset == 0:
+ # unget is called quite rarely, so it's a good idea to do
+ # more work here if it saves a bit of work in the frequently
+ # called char and charsUntil.
+ # So, just prepend the ungotten character onto the current
+ # chunk:
+ self.chunk = char + self.chunk
+ self.chunkSize += 1
+ else:
+ self.chunkOffset -= 1
+ assert self.chunk[self.chunkOffset] == char
+
+
+class HTMLBinaryInputStream(HTMLUnicodeInputStream):
+ """Provides a unicode stream of characters to the HTMLTokenizer.
+
+ This class takes care of character encoding and removing or replacing
+ incorrect byte-sequences and also provides column and line tracking.
+
+ """
+
+ def __init__(self, source, override_encoding=None, transport_encoding=None,
+ same_origin_parent_encoding=None, likely_encoding=None,
+ default_encoding="windows-1252", useChardet=True):
+ """Initialises the HTMLInputStream.
+
+ HTMLInputStream(source, [encoding]) -> Normalized stream from source
+ for use by html5lib.
+
+ source can be either a file-object, local filename or a string.
+
+ The optional encoding parameter must be a string that indicates
+ the encoding. If specified, that encoding will be used,
+ regardless of any BOM or later declaration (such as in a meta
+ element)
+
+ """
+ # Raw Stream - for unicode objects this will encode to utf-8 and set
+ # self.charEncoding as appropriate
+ self.rawStream = self.openStream(source)
+
+ HTMLUnicodeInputStream.__init__(self, self.rawStream)
+
+ # Encoding Information
+ # Number of bytes to use when looking for a meta element with
+ # encoding information
+ self.numBytesMeta = 1024
+ # Number of bytes to use when using detecting encoding using chardet
+ self.numBytesChardet = 100
+ # Things from args
+ self.override_encoding = override_encoding
+ self.transport_encoding = transport_encoding
+ self.same_origin_parent_encoding = same_origin_parent_encoding
+ self.likely_encoding = likely_encoding
+ self.default_encoding = default_encoding
+
+ # Determine encoding
+ self.charEncoding = self.determineEncoding(useChardet)
+ assert self.charEncoding[0] is not None
+
+ # Call superclass
+ self.reset()
+
+ def reset(self):
+ self.dataStream = self.charEncoding[0].codec_info.streamreader(self.rawStream, 'replace')
+ HTMLUnicodeInputStream.reset(self)
+
+ def openStream(self, source):
+ """Produces a file object from source.
+
+ source can be either a file object, local filename or a string.
+
+ """
+ # Already a file object
+ if hasattr(source, 'read'):
+ stream = source
+ else:
+ stream = BytesIO(source)
+
+ try:
+ stream.seek(stream.tell())
+ except Exception:
+ stream = BufferedStream(stream)
+
+ return stream
+
+ def determineEncoding(self, chardet=True):
+ # BOMs take precedence over everything
+ # This will also read past the BOM if present
+ charEncoding = self.detectBOM(), "certain"
+ if charEncoding[0] is not None:
+ return charEncoding
+
+ # If we've been overridden, we've been overridden
+ charEncoding = lookupEncoding(self.override_encoding), "certain"
+ if charEncoding[0] is not None:
+ return charEncoding
+
+ # Now check the transport layer
+ charEncoding = lookupEncoding(self.transport_encoding), "certain"
+ if charEncoding[0] is not None:
+ return charEncoding
+
+ # Look for meta elements with encoding information
+ charEncoding = self.detectEncodingMeta(), "tentative"
+ if charEncoding[0] is not None:
+ return charEncoding
+
+ # Parent document encoding
+ charEncoding = lookupEncoding(self.same_origin_parent_encoding), "tentative"
+ if charEncoding[0] is not None and not charEncoding[0].name.startswith("utf-16"):
+ return charEncoding
+
+ # "likely" encoding
+ charEncoding = lookupEncoding(self.likely_encoding), "tentative"
+ if charEncoding[0] is not None:
+ return charEncoding
+
+ # Guess with chardet, if available
+ if chardet:
+ try:
+ from chardet.universaldetector import UniversalDetector
+ except ImportError:
+ pass
+ else:
+ buffers = []
+ detector = UniversalDetector()
+ while not detector.done:
+ buffer = self.rawStream.read(self.numBytesChardet)
+ assert isinstance(buffer, bytes)
+ if not buffer:
+ break
+ buffers.append(buffer)
+ detector.feed(buffer)
+ detector.close()
+ encoding = lookupEncoding(detector.result['encoding'])
+ self.rawStream.seek(0)
+ if encoding is not None:
+ return encoding, "tentative"
+
+ # Try the default encoding
+ charEncoding = lookupEncoding(self.default_encoding), "tentative"
+ if charEncoding[0] is not None:
+ return charEncoding
+
+ # Fallback to html5lib's default if even that hasn't worked
+ return lookupEncoding("windows-1252"), "tentative"
+
+ def changeEncoding(self, newEncoding):
+ assert self.charEncoding[1] != "certain"
+ newEncoding = lookupEncoding(newEncoding)
+ if newEncoding is None:
+ return
+ if newEncoding.name in ("utf-16be", "utf-16le"):
+ newEncoding = lookupEncoding("utf-8")
+ assert newEncoding is not None
+ elif newEncoding == self.charEncoding[0]:
+ self.charEncoding = (self.charEncoding[0], "certain")
+ else:
+ self.rawStream.seek(0)
+ self.charEncoding = (newEncoding, "certain")
+ self.reset()
+ raise _ReparseException("Encoding changed from %s to %s" % (self.charEncoding[0], newEncoding))
+
+ def detectBOM(self):
+ """Attempts to detect at BOM at the start of the stream. If
+ an encoding can be determined from the BOM return the name of the
+ encoding otherwise return None"""
+ bomDict = {
+ codecs.BOM_UTF8: 'utf-8',
+ codecs.BOM_UTF16_LE: 'utf-16le', codecs.BOM_UTF16_BE: 'utf-16be',
+ codecs.BOM_UTF32_LE: 'utf-32le', codecs.BOM_UTF32_BE: 'utf-32be'
+ }
+
+ # Go to beginning of file and read in 4 bytes
+ string = self.rawStream.read(4)
+ assert isinstance(string, bytes)
+
+ # Try detecting the BOM using bytes from the string
+ encoding = bomDict.get(string[:3]) # UTF-8
+ seek = 3
+ if not encoding:
+ # Need to detect UTF-32 before UTF-16
+ encoding = bomDict.get(string) # UTF-32
+ seek = 4
+ if not encoding:
+ encoding = bomDict.get(string[:2]) # UTF-16
+ seek = 2
+
+ # Set the read position past the BOM if one was found, otherwise
+ # set it to the start of the stream
+ if encoding:
+ self.rawStream.seek(seek)
+ return lookupEncoding(encoding)
+ else:
+ self.rawStream.seek(0)
+ return None
+
+ def detectEncodingMeta(self):
+ """Report the encoding declared by the meta element
+ """
+ buffer = self.rawStream.read(self.numBytesMeta)
+ assert isinstance(buffer, bytes)
+ parser = EncodingParser(buffer)
+ self.rawStream.seek(0)
+ encoding = parser.getEncoding()
+
+ if encoding is not None and encoding.name in ("utf-16be", "utf-16le"):
+ encoding = lookupEncoding("utf-8")
+
+ return encoding
+
+
+class EncodingBytes(bytes):
+ """String-like object with an associated position and various extra methods
+ If the position is ever greater than the string length then an exception is
+ raised"""
+ def __new__(self, value):
+ assert isinstance(value, bytes)
+ return bytes.__new__(self, value.lower())
+
+ def __init__(self, value):
+ # pylint:disable=unused-argument
+ self._position = -1
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ p = self._position = self._position + 1
+ if p >= len(self):
+ raise StopIteration
+ elif p < 0:
+ raise TypeError
+ return self[p:p + 1]
+
+ def next(self):
+ # Py2 compat
+ return self.__next__()
+
+ def previous(self):
+ p = self._position
+ if p >= len(self):
+ raise StopIteration
+ elif p < 0:
+ raise TypeError
+ self._position = p = p - 1
+ return self[p:p + 1]
+
+ def setPosition(self, position):
+ if self._position >= len(self):
+ raise StopIteration
+ self._position = position
+
+ def getPosition(self):
+ if self._position >= len(self):
+ raise StopIteration
+ if self._position >= 0:
+ return self._position
+ else:
+ return None
+
+ position = property(getPosition, setPosition)
+
+ def getCurrentByte(self):
+ return self[self.position:self.position + 1]
+
+ currentByte = property(getCurrentByte)
+
+ def skip(self, chars=spaceCharactersBytes):
+ """Skip past a list of characters"""
+ p = self.position # use property for the error-checking
+ while p < len(self):
+ c = self[p:p + 1]
+ if c not in chars:
+ self._position = p
+ return c
+ p += 1
+ self._position = p
+ return None
+
+ def skipUntil(self, chars):
+ p = self.position
+ while p < len(self):
+ c = self[p:p + 1]
+ if c in chars:
+ self._position = p
+ return c
+ p += 1
+ self._position = p
+ return None
+
+ def matchBytes(self, bytes):
+ """Look for a sequence of bytes at the start of a string. If the bytes
+ are found return True and advance the position to the byte after the
+ match. Otherwise return False and leave the position alone"""
+ rv = self.startswith(bytes, self.position)
+ if rv:
+ self.position += len(bytes)
+ return rv
+
+ def jumpTo(self, bytes):
+ """Look for the next sequence of bytes matching a given sequence. If
+ a match is found advance the position to the last byte of the match"""
+ try:
+ self._position = self.index(bytes, self.position) + len(bytes) - 1
+ except ValueError:
+ raise StopIteration
+ return True
+
+
+class EncodingParser(object):
+ """Mini parser for detecting character encoding from meta elements"""
+
+ def __init__(self, data):
+ """string - the data to work on for encoding detection"""
+ self.data = EncodingBytes(data)
+ self.encoding = None
+
+ def getEncoding(self):
+ if b"<meta" not in self.data:
+ return None
+
+ methodDispatch = (
+ (b"<!--", self.handleComment),
+ (b"<meta", self.handleMeta),
+ (b"</", self.handlePossibleEndTag),
+ (b"<!", self.handleOther),
+ (b"<?", self.handleOther),
+ (b"<", self.handlePossibleStartTag))
+ for _ in self.data:
+ keepParsing = True
+ try:
+ self.data.jumpTo(b"<")
+ except StopIteration:
+ break
+ for key, method in methodDispatch:
+ if self.data.matchBytes(key):
+ try:
+ keepParsing = method()
+ break
+ except StopIteration:
+ keepParsing = False
+ break
+ if not keepParsing:
+ break
+
+ return self.encoding
+
+ def handleComment(self):
+ """Skip over comments"""
+ return self.data.jumpTo(b"-->")
+
+ def handleMeta(self):
+ if self.data.currentByte not in spaceCharactersBytes:
+ # if we have <meta not followed by a space so just keep going
+ return True
+ # We have a valid meta element we want to search for attributes
+ hasPragma = False
+ pendingEncoding = None
+ while True:
+ # Try to find the next attribute after the current position
+ attr = self.getAttribute()
+ if attr is None:
+ return True
+ else:
+ if attr[0] == b"http-equiv":
+ hasPragma = attr[1] == b"content-type"
+ if hasPragma and pendingEncoding is not None:
+ self.encoding = pendingEncoding
+ return False
+ elif attr[0] == b"charset":
+ tentativeEncoding = attr[1]
+ codec = lookupEncoding(tentativeEncoding)
+ if codec is not None:
+ self.encoding = codec
+ return False
+ elif attr[0] == b"content":
+ contentParser = ContentAttrParser(EncodingBytes(attr[1]))
+ tentativeEncoding = contentParser.parse()
+ if tentativeEncoding is not None:
+ codec = lookupEncoding(tentativeEncoding)
+ if codec is not None:
+ if hasPragma:
+ self.encoding = codec
+ return False
+ else:
+ pendingEncoding = codec
+
+ def handlePossibleStartTag(self):
+ return self.handlePossibleTag(False)
+
+ def handlePossibleEndTag(self):
+ next(self.data)
+ return self.handlePossibleTag(True)
+
+ def handlePossibleTag(self, endTag):
+ data = self.data
+ if data.currentByte not in asciiLettersBytes:
+ # If the next byte is not an ascii letter either ignore this
+ # fragment (possible start tag case) or treat it according to
+ # handleOther
+ if endTag:
+ data.previous()
+ self.handleOther()
+ return True
+
+ c = data.skipUntil(spacesAngleBrackets)
+ if c == b"<":
+ # return to the first step in the overall "two step" algorithm
+ # reprocessing the < byte
+ data.previous()
+ else:
+ # Read all attributes
+ attr = self.getAttribute()
+ while attr is not None:
+ attr = self.getAttribute()
+ return True
+
+ def handleOther(self):
+ return self.data.jumpTo(b">")
+
+ def getAttribute(self):
+ """Return a name,value pair for the next attribute in the stream,
+ if one is found, or None"""
+ data = self.data
+ # Step 1 (skip chars)
+ c = data.skip(spaceCharactersBytes | frozenset([b"/"]))
+ assert c is None or len(c) == 1
+ # Step 2
+ if c in (b">", None):
+ return None
+ # Step 3
+ attrName = []
+ attrValue = []
+ # Step 4 attribute name
+ while True:
+ if c == b"=" and attrName:
+ break
+ elif c in spaceCharactersBytes:
+ # Step 6!
+ c = data.skip()
+ break
+ elif c in (b"/", b">"):
+ return b"".join(attrName), b""
+ elif c in asciiUppercaseBytes:
+ attrName.append(c.lower())
+ elif c is None:
+ return None
+ else:
+ attrName.append(c)
+ # Step 5
+ c = next(data)
+ # Step 7
+ if c != b"=":
+ data.previous()
+ return b"".join(attrName), b""
+ # Step 8
+ next(data)
+ # Step 9
+ c = data.skip()
+ # Step 10
+ if c in (b"'", b'"'):
+ # 10.1
+ quoteChar = c
+ while True:
+ # 10.2
+ c = next(data)
+ # 10.3
+ if c == quoteChar:
+ next(data)
+ return b"".join(attrName), b"".join(attrValue)
+ # 10.4
+ elif c in asciiUppercaseBytes:
+ attrValue.append(c.lower())
+ # 10.5
+ else:
+ attrValue.append(c)
+ elif c == b">":
+ return b"".join(attrName), b""
+ elif c in asciiUppercaseBytes:
+ attrValue.append(c.lower())
+ elif c is None:
+ return None
+ else:
+ attrValue.append(c)
+ # Step 11
+ while True:
+ c = next(data)
+ if c in spacesAngleBrackets:
+ return b"".join(attrName), b"".join(attrValue)
+ elif c in asciiUppercaseBytes:
+ attrValue.append(c.lower())
+ elif c is None:
+ return None
+ else:
+ attrValue.append(c)
+
+
+class ContentAttrParser(object):
+ def __init__(self, data):
+ assert isinstance(data, bytes)
+ self.data = data
+
+ def parse(self):
+ try:
+ # Check if the attr name is charset
+ # otherwise return
+ self.data.jumpTo(b"charset")
+ self.data.position += 1
+ self.data.skip()
+ if not self.data.currentByte == b"=":
+ # If there is no = sign keep looking for attrs
+ return None
+ self.data.position += 1
+ self.data.skip()
+ # Look for an encoding between matching quote marks
+ if self.data.currentByte in (b'"', b"'"):
+ quoteMark = self.data.currentByte
+ self.data.position += 1
+ oldPosition = self.data.position
+ if self.data.jumpTo(quoteMark):
+ return self.data[oldPosition:self.data.position]
+ else:
+ return None
+ else:
+ # Unquoted value
+ oldPosition = self.data.position
+ try:
+ self.data.skipUntil(spaceCharactersBytes)
+ return self.data[oldPosition:self.data.position]
+ except StopIteration:
+ # Return the whole remaining value
+ return self.data[oldPosition:]
+ except StopIteration:
+ return None
+
+
+def lookupEncoding(encoding):
+ """Return the python codec name corresponding to an encoding or None if the
+ string doesn't correspond to a valid encoding."""
+ if isinstance(encoding, bytes):
+ try:
+ encoding = encoding.decode("ascii")
+ except UnicodeDecodeError:
+ return None
+
+ if encoding is not None:
+ try:
+ return webencodings.lookup(encoding)
+ except AttributeError:
+ return None
+ else:
+ return None
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_tokenizer.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_tokenizer.py
new file mode 100644
index 0000000000..4748a19795
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_tokenizer.py
@@ -0,0 +1,1735 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from six import unichr as chr
+
+from collections import deque, OrderedDict
+from sys import version_info
+
+from .constants import spaceCharacters
+from .constants import entities
+from .constants import asciiLetters, asciiUpper2Lower
+from .constants import digits, hexDigits, EOF
+from .constants import tokenTypes, tagTokenTypes
+from .constants import replacementCharacters
+
+from ._inputstream import HTMLInputStream
+
+from ._trie import Trie
+
+entitiesTrie = Trie(entities)
+
+if version_info >= (3, 7):
+ attributeMap = dict
+else:
+ attributeMap = OrderedDict
+
+
+class HTMLTokenizer(object):
+ """ This class takes care of tokenizing HTML.
+
+ * self.currentToken
+ Holds the token that is currently being processed.
+
+ * self.state
+ Holds a reference to the method to be invoked... XXX
+
+ * self.stream
+ Points to HTMLInputStream object.
+ """
+
+ def __init__(self, stream, parser=None, **kwargs):
+
+ self.stream = HTMLInputStream(stream, **kwargs)
+ self.parser = parser
+
+ # Setup the initial tokenizer state
+ self.escapeFlag = False
+ self.lastFourChars = []
+ self.state = self.dataState
+ self.escape = False
+
+ # The current token being created
+ self.currentToken = None
+ super(HTMLTokenizer, self).__init__()
+
+ def __iter__(self):
+ """ This is where the magic happens.
+
+ We do our usually processing through the states and when we have a token
+ to return we yield the token which pauses processing until the next token
+ is requested.
+ """
+ self.tokenQueue = deque([])
+ # Start processing. When EOF is reached self.state will return False
+ # instead of True and the loop will terminate.
+ while self.state():
+ while self.stream.errors:
+ yield {"type": tokenTypes["ParseError"], "data": self.stream.errors.pop(0)}
+ while self.tokenQueue:
+ yield self.tokenQueue.popleft()
+
+ def consumeNumberEntity(self, isHex):
+ """This function returns either U+FFFD or the character based on the
+ decimal or hexadecimal representation. It also discards ";" if present.
+ If not present self.tokenQueue.append({"type": tokenTypes["ParseError"]}) is invoked.
+ """
+
+ allowed = digits
+ radix = 10
+ if isHex:
+ allowed = hexDigits
+ radix = 16
+
+ charStack = []
+
+ # Consume all the characters that are in range while making sure we
+ # don't hit an EOF.
+ c = self.stream.char()
+ while c in allowed and c is not EOF:
+ charStack.append(c)
+ c = self.stream.char()
+
+ # Convert the set of characters consumed to an int.
+ charAsInt = int("".join(charStack), radix)
+
+ # Certain characters get replaced with others
+ if charAsInt in replacementCharacters:
+ char = replacementCharacters[charAsInt]
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "illegal-codepoint-for-numeric-entity",
+ "datavars": {"charAsInt": charAsInt}})
+ elif ((0xD800 <= charAsInt <= 0xDFFF) or
+ (charAsInt > 0x10FFFF)):
+ char = "\uFFFD"
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "illegal-codepoint-for-numeric-entity",
+ "datavars": {"charAsInt": charAsInt}})
+ else:
+ # Should speed up this check somehow (e.g. move the set to a constant)
+ if ((0x0001 <= charAsInt <= 0x0008) or
+ (0x000E <= charAsInt <= 0x001F) or
+ (0x007F <= charAsInt <= 0x009F) or
+ (0xFDD0 <= charAsInt <= 0xFDEF) or
+ charAsInt in frozenset([0x000B, 0xFFFE, 0xFFFF, 0x1FFFE,
+ 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE,
+ 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE,
+ 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE,
+ 0x7FFFF, 0x8FFFE, 0x8FFFF, 0x9FFFE,
+ 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE,
+ 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE,
+ 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE,
+ 0xFFFFF, 0x10FFFE, 0x10FFFF])):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data":
+ "illegal-codepoint-for-numeric-entity",
+ "datavars": {"charAsInt": charAsInt}})
+ try:
+ # Try/except needed as UCS-2 Python builds' unichar only works
+ # within the BMP.
+ char = chr(charAsInt)
+ except ValueError:
+ v = charAsInt - 0x10000
+ char = chr(0xD800 | (v >> 10)) + chr(0xDC00 | (v & 0x3FF))
+
+ # Discard the ; if present. Otherwise, put it back on the queue and
+ # invoke parseError on parser.
+ if c != ";":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "numeric-entity-without-semicolon"})
+ self.stream.unget(c)
+
+ return char
+
+ def consumeEntity(self, allowedChar=None, fromAttribute=False):
+ # Initialise to the default output for when no entity is matched
+ output = "&"
+
+ charStack = [self.stream.char()]
+ if (charStack[0] in spaceCharacters or charStack[0] in (EOF, "<", "&") or
+ (allowedChar is not None and allowedChar == charStack[0])):
+ self.stream.unget(charStack[0])
+
+ elif charStack[0] == "#":
+ # Read the next character to see if it's hex or decimal
+ hex = False
+ charStack.append(self.stream.char())
+ if charStack[-1] in ("x", "X"):
+ hex = True
+ charStack.append(self.stream.char())
+
+ # charStack[-1] should be the first digit
+ if (hex and charStack[-1] in hexDigits) \
+ or (not hex and charStack[-1] in digits):
+ # At least one digit found, so consume the whole number
+ self.stream.unget(charStack[-1])
+ output = self.consumeNumberEntity(hex)
+ else:
+ # No digits found
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "expected-numeric-entity"})
+ self.stream.unget(charStack.pop())
+ output = "&" + "".join(charStack)
+
+ else:
+ # At this point in the process might have named entity. Entities
+ # are stored in the global variable "entities".
+ #
+ # Consume characters and compare to these to a substring of the
+ # entity names in the list until the substring no longer matches.
+ while (charStack[-1] is not EOF):
+ if not entitiesTrie.has_keys_with_prefix("".join(charStack)):
+ break
+ charStack.append(self.stream.char())
+
+ # At this point we have a string that starts with some characters
+ # that may match an entity
+ # Try to find the longest entity the string will match to take care
+ # of &noti for instance.
+ try:
+ entityName = entitiesTrie.longest_prefix("".join(charStack[:-1]))
+ entityLength = len(entityName)
+ except KeyError:
+ entityName = None
+
+ if entityName is not None:
+ if entityName[-1] != ";":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "named-entity-without-semicolon"})
+ if (entityName[-1] != ";" and fromAttribute and
+ (charStack[entityLength] in asciiLetters or
+ charStack[entityLength] in digits or
+ charStack[entityLength] == "=")):
+ self.stream.unget(charStack.pop())
+ output = "&" + "".join(charStack)
+ else:
+ output = entities[entityName]
+ self.stream.unget(charStack.pop())
+ output += "".join(charStack[entityLength:])
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-named-entity"})
+ self.stream.unget(charStack.pop())
+ output = "&" + "".join(charStack)
+
+ if fromAttribute:
+ self.currentToken["data"][-1][1] += output
+ else:
+ if output in spaceCharacters:
+ tokenType = "SpaceCharacters"
+ else:
+ tokenType = "Characters"
+ self.tokenQueue.append({"type": tokenTypes[tokenType], "data": output})
+
+ def processEntityInAttribute(self, allowedChar):
+ """This method replaces the need for "entityInAttributeValueState".
+ """
+ self.consumeEntity(allowedChar=allowedChar, fromAttribute=True)
+
+ def emitCurrentToken(self):
+ """This method is a generic handler for emitting the tags. It also sets
+ the state to "data" because that's what's needed after a token has been
+ emitted.
+ """
+ token = self.currentToken
+ # Add token to the queue to be yielded
+ if (token["type"] in tagTokenTypes):
+ token["name"] = token["name"].translate(asciiUpper2Lower)
+ if token["type"] == tokenTypes["StartTag"]:
+ raw = token["data"]
+ data = attributeMap(raw)
+ if len(raw) > len(data):
+ # we had some duplicated attribute, fix so first wins
+ data.update(raw[::-1])
+ token["data"] = data
+
+ if token["type"] == tokenTypes["EndTag"]:
+ if token["data"]:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "attributes-in-end-tag"})
+ if token["selfClosing"]:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "self-closing-flag-on-end-tag"})
+ self.tokenQueue.append(token)
+ self.state = self.dataState
+
+ # Below are the various tokenizer states worked out.
+ def dataState(self):
+ data = self.stream.char()
+ if data == "&":
+ self.state = self.entityDataState
+ elif data == "<":
+ self.state = self.tagOpenState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "\u0000"})
+ elif data is EOF:
+ # Tokenization ends.
+ return False
+ elif data in spaceCharacters:
+ # Directly after emitting a token you switch back to the "data
+ # state". At that point spaceCharacters are important so they are
+ # emitted separately.
+ self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data":
+ data + self.stream.charsUntil(spaceCharacters, True)})
+ # No need to update lastFourChars here, since the first space will
+ # have already been appended to lastFourChars and will have broken
+ # any <!-- or --> sequences
+ else:
+ chars = self.stream.charsUntil(("&", "<", "\u0000"))
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
+ data + chars})
+ return True
+
+ def entityDataState(self):
+ self.consumeEntity()
+ self.state = self.dataState
+ return True
+
+ def rcdataState(self):
+ data = self.stream.char()
+ if data == "&":
+ self.state = self.characterReferenceInRcdata
+ elif data == "<":
+ self.state = self.rcdataLessThanSignState
+ elif data == EOF:
+ # Tokenization ends.
+ return False
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "\uFFFD"})
+ elif data in spaceCharacters:
+ # Directly after emitting a token you switch back to the "data
+ # state". At that point spaceCharacters are important so they are
+ # emitted separately.
+ self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data":
+ data + self.stream.charsUntil(spaceCharacters, True)})
+ # No need to update lastFourChars here, since the first space will
+ # have already been appended to lastFourChars and will have broken
+ # any <!-- or --> sequences
+ else:
+ chars = self.stream.charsUntil(("&", "<", "\u0000"))
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
+ data + chars})
+ return True
+
+ def characterReferenceInRcdata(self):
+ self.consumeEntity()
+ self.state = self.rcdataState
+ return True
+
+ def rawtextState(self):
+ data = self.stream.char()
+ if data == "<":
+ self.state = self.rawtextLessThanSignState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "\uFFFD"})
+ elif data == EOF:
+ # Tokenization ends.
+ return False
+ else:
+ chars = self.stream.charsUntil(("<", "\u0000"))
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
+ data + chars})
+ return True
+
+ def scriptDataState(self):
+ data = self.stream.char()
+ if data == "<":
+ self.state = self.scriptDataLessThanSignState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "\uFFFD"})
+ elif data == EOF:
+ # Tokenization ends.
+ return False
+ else:
+ chars = self.stream.charsUntil(("<", "\u0000"))
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
+ data + chars})
+ return True
+
+ def plaintextState(self):
+ data = self.stream.char()
+ if data == EOF:
+ # Tokenization ends.
+ return False
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "\uFFFD"})
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
+ data + self.stream.charsUntil("\u0000")})
+ return True
+
+ def tagOpenState(self):
+ data = self.stream.char()
+ if data == "!":
+ self.state = self.markupDeclarationOpenState
+ elif data == "/":
+ self.state = self.closeTagOpenState
+ elif data in asciiLetters:
+ self.currentToken = {"type": tokenTypes["StartTag"],
+ "name": data, "data": [],
+ "selfClosing": False,
+ "selfClosingAcknowledged": False}
+ self.state = self.tagNameState
+ elif data == ">":
+ # XXX In theory it could be something besides a tag name. But
+ # do we really care?
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-tag-name-but-got-right-bracket"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<>"})
+ self.state = self.dataState
+ elif data == "?":
+ # XXX In theory it could be something besides a tag name. But
+ # do we really care?
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-tag-name-but-got-question-mark"})
+ self.stream.unget(data)
+ self.state = self.bogusCommentState
+ else:
+ # XXX
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-tag-name"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
+ self.stream.unget(data)
+ self.state = self.dataState
+ return True
+
+ def closeTagOpenState(self):
+ data = self.stream.char()
+ if data in asciiLetters:
+ self.currentToken = {"type": tokenTypes["EndTag"], "name": data,
+ "data": [], "selfClosing": False}
+ self.state = self.tagNameState
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-closing-tag-but-got-right-bracket"})
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-closing-tag-but-got-eof"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "</"})
+ self.state = self.dataState
+ else:
+ # XXX data can be _'_...
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-closing-tag-but-got-char",
+ "datavars": {"data": data}})
+ self.stream.unget(data)
+ self.state = self.bogusCommentState
+ return True
+
+ def tagNameState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.beforeAttributeNameState
+ elif data == ">":
+ self.emitCurrentToken()
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-tag-name"})
+ self.state = self.dataState
+ elif data == "/":
+ self.state = self.selfClosingStartTagState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["name"] += "\uFFFD"
+ else:
+ self.currentToken["name"] += data
+ # (Don't use charsUntil here, because tag names are
+ # very short and it's faster to not do anything fancy)
+ return True
+
+ def rcdataLessThanSignState(self):
+ data = self.stream.char()
+ if data == "/":
+ self.temporaryBuffer = ""
+ self.state = self.rcdataEndTagOpenState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
+ self.stream.unget(data)
+ self.state = self.rcdataState
+ return True
+
+ def rcdataEndTagOpenState(self):
+ data = self.stream.char()
+ if data in asciiLetters:
+ self.temporaryBuffer += data
+ self.state = self.rcdataEndTagNameState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "</"})
+ self.stream.unget(data)
+ self.state = self.rcdataState
+ return True
+
+ def rcdataEndTagNameState(self):
+ appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower()
+ data = self.stream.char()
+ if data in spaceCharacters and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing": False}
+ self.state = self.beforeAttributeNameState
+ elif data == "/" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing": False}
+ self.state = self.selfClosingStartTagState
+ elif data == ">" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing": False}
+ self.emitCurrentToken()
+ self.state = self.dataState
+ elif data in asciiLetters:
+ self.temporaryBuffer += data
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "</" + self.temporaryBuffer})
+ self.stream.unget(data)
+ self.state = self.rcdataState
+ return True
+
+ def rawtextLessThanSignState(self):
+ data = self.stream.char()
+ if data == "/":
+ self.temporaryBuffer = ""
+ self.state = self.rawtextEndTagOpenState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
+ self.stream.unget(data)
+ self.state = self.rawtextState
+ return True
+
+ def rawtextEndTagOpenState(self):
+ data = self.stream.char()
+ if data in asciiLetters:
+ self.temporaryBuffer += data
+ self.state = self.rawtextEndTagNameState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "</"})
+ self.stream.unget(data)
+ self.state = self.rawtextState
+ return True
+
+ def rawtextEndTagNameState(self):
+ appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower()
+ data = self.stream.char()
+ if data in spaceCharacters and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing": False}
+ self.state = self.beforeAttributeNameState
+ elif data == "/" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing": False}
+ self.state = self.selfClosingStartTagState
+ elif data == ">" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing": False}
+ self.emitCurrentToken()
+ self.state = self.dataState
+ elif data in asciiLetters:
+ self.temporaryBuffer += data
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "</" + self.temporaryBuffer})
+ self.stream.unget(data)
+ self.state = self.rawtextState
+ return True
+
+ def scriptDataLessThanSignState(self):
+ data = self.stream.char()
+ if data == "/":
+ self.temporaryBuffer = ""
+ self.state = self.scriptDataEndTagOpenState
+ elif data == "!":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<!"})
+ self.state = self.scriptDataEscapeStartState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
+ self.stream.unget(data)
+ self.state = self.scriptDataState
+ return True
+
+ def scriptDataEndTagOpenState(self):
+ data = self.stream.char()
+ if data in asciiLetters:
+ self.temporaryBuffer += data
+ self.state = self.scriptDataEndTagNameState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "</"})
+ self.stream.unget(data)
+ self.state = self.scriptDataState
+ return True
+
+ def scriptDataEndTagNameState(self):
+ appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower()
+ data = self.stream.char()
+ if data in spaceCharacters and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing": False}
+ self.state = self.beforeAttributeNameState
+ elif data == "/" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing": False}
+ self.state = self.selfClosingStartTagState
+ elif data == ">" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing": False}
+ self.emitCurrentToken()
+ self.state = self.dataState
+ elif data in asciiLetters:
+ self.temporaryBuffer += data
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "</" + self.temporaryBuffer})
+ self.stream.unget(data)
+ self.state = self.scriptDataState
+ return True
+
+ def scriptDataEscapeStartState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
+ self.state = self.scriptDataEscapeStartDashState
+ else:
+ self.stream.unget(data)
+ self.state = self.scriptDataState
+ return True
+
+ def scriptDataEscapeStartDashState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
+ self.state = self.scriptDataEscapedDashDashState
+ else:
+ self.stream.unget(data)
+ self.state = self.scriptDataState
+ return True
+
+ def scriptDataEscapedState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
+ self.state = self.scriptDataEscapedDashState
+ elif data == "<":
+ self.state = self.scriptDataEscapedLessThanSignState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "\uFFFD"})
+ elif data == EOF:
+ self.state = self.dataState
+ else:
+ chars = self.stream.charsUntil(("<", "-", "\u0000"))
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
+ data + chars})
+ return True
+
+ def scriptDataEscapedDashState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
+ self.state = self.scriptDataEscapedDashDashState
+ elif data == "<":
+ self.state = self.scriptDataEscapedLessThanSignState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "\uFFFD"})
+ self.state = self.scriptDataEscapedState
+ elif data == EOF:
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ self.state = self.scriptDataEscapedState
+ return True
+
+ def scriptDataEscapedDashDashState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
+ elif data == "<":
+ self.state = self.scriptDataEscapedLessThanSignState
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ">"})
+ self.state = self.scriptDataState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "\uFFFD"})
+ self.state = self.scriptDataEscapedState
+ elif data == EOF:
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ self.state = self.scriptDataEscapedState
+ return True
+
+ def scriptDataEscapedLessThanSignState(self):
+ data = self.stream.char()
+ if data == "/":
+ self.temporaryBuffer = ""
+ self.state = self.scriptDataEscapedEndTagOpenState
+ elif data in asciiLetters:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<" + data})
+ self.temporaryBuffer = data
+ self.state = self.scriptDataDoubleEscapeStartState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
+ self.stream.unget(data)
+ self.state = self.scriptDataEscapedState
+ return True
+
+ def scriptDataEscapedEndTagOpenState(self):
+ data = self.stream.char()
+ if data in asciiLetters:
+ self.temporaryBuffer = data
+ self.state = self.scriptDataEscapedEndTagNameState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "</"})
+ self.stream.unget(data)
+ self.state = self.scriptDataEscapedState
+ return True
+
+ def scriptDataEscapedEndTagNameState(self):
+ appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower()
+ data = self.stream.char()
+ if data in spaceCharacters and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing": False}
+ self.state = self.beforeAttributeNameState
+ elif data == "/" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing": False}
+ self.state = self.selfClosingStartTagState
+ elif data == ">" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing": False}
+ self.emitCurrentToken()
+ self.state = self.dataState
+ elif data in asciiLetters:
+ self.temporaryBuffer += data
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "</" + self.temporaryBuffer})
+ self.stream.unget(data)
+ self.state = self.scriptDataEscapedState
+ return True
+
+ def scriptDataDoubleEscapeStartState(self):
+ data = self.stream.char()
+ if data in (spaceCharacters | frozenset(("/", ">"))):
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ if self.temporaryBuffer.lower() == "script":
+ self.state = self.scriptDataDoubleEscapedState
+ else:
+ self.state = self.scriptDataEscapedState
+ elif data in asciiLetters:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ self.temporaryBuffer += data
+ else:
+ self.stream.unget(data)
+ self.state = self.scriptDataEscapedState
+ return True
+
+ def scriptDataDoubleEscapedState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
+ self.state = self.scriptDataDoubleEscapedDashState
+ elif data == "<":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
+ self.state = self.scriptDataDoubleEscapedLessThanSignState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "\uFFFD"})
+ elif data == EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-script-in-script"})
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ return True
+
+ def scriptDataDoubleEscapedDashState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
+ self.state = self.scriptDataDoubleEscapedDashDashState
+ elif data == "<":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
+ self.state = self.scriptDataDoubleEscapedLessThanSignState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "\uFFFD"})
+ self.state = self.scriptDataDoubleEscapedState
+ elif data == EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-script-in-script"})
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ self.state = self.scriptDataDoubleEscapedState
+ return True
+
+ def scriptDataDoubleEscapedDashDashState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
+ elif data == "<":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
+ self.state = self.scriptDataDoubleEscapedLessThanSignState
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ">"})
+ self.state = self.scriptDataState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": "\uFFFD"})
+ self.state = self.scriptDataDoubleEscapedState
+ elif data == EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-script-in-script"})
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ self.state = self.scriptDataDoubleEscapedState
+ return True
+
+ def scriptDataDoubleEscapedLessThanSignState(self):
+ data = self.stream.char()
+ if data == "/":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "/"})
+ self.temporaryBuffer = ""
+ self.state = self.scriptDataDoubleEscapeEndState
+ else:
+ self.stream.unget(data)
+ self.state = self.scriptDataDoubleEscapedState
+ return True
+
+ def scriptDataDoubleEscapeEndState(self):
+ data = self.stream.char()
+ if data in (spaceCharacters | frozenset(("/", ">"))):
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ if self.temporaryBuffer.lower() == "script":
+ self.state = self.scriptDataEscapedState
+ else:
+ self.state = self.scriptDataDoubleEscapedState
+ elif data in asciiLetters:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ self.temporaryBuffer += data
+ else:
+ self.stream.unget(data)
+ self.state = self.scriptDataDoubleEscapedState
+ return True
+
+ def beforeAttributeNameState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.stream.charsUntil(spaceCharacters, True)
+ elif data in asciiLetters:
+ self.currentToken["data"].append([data, ""])
+ self.state = self.attributeNameState
+ elif data == ">":
+ self.emitCurrentToken()
+ elif data == "/":
+ self.state = self.selfClosingStartTagState
+ elif data in ("'", '"', "=", "<"):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "invalid-character-in-attribute-name"})
+ self.currentToken["data"].append([data, ""])
+ self.state = self.attributeNameState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"].append(["\uFFFD", ""])
+ self.state = self.attributeNameState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-attribute-name-but-got-eof"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"].append([data, ""])
+ self.state = self.attributeNameState
+ return True
+
+ def attributeNameState(self):
+ data = self.stream.char()
+ leavingThisState = True
+ emitToken = False
+ if data == "=":
+ self.state = self.beforeAttributeValueState
+ elif data in asciiLetters:
+ self.currentToken["data"][-1][0] += data +\
+ self.stream.charsUntil(asciiLetters, True)
+ leavingThisState = False
+ elif data == ">":
+ # XXX If we emit here the attributes are converted to a dict
+ # without being checked and when the code below runs we error
+ # because data is a dict not a list
+ emitToken = True
+ elif data in spaceCharacters:
+ self.state = self.afterAttributeNameState
+ elif data == "/":
+ self.state = self.selfClosingStartTagState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"][-1][0] += "\uFFFD"
+ leavingThisState = False
+ elif data in ("'", '"', "<"):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data":
+ "invalid-character-in-attribute-name"})
+ self.currentToken["data"][-1][0] += data
+ leavingThisState = False
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "eof-in-attribute-name"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"][-1][0] += data
+ leavingThisState = False
+
+ if leavingThisState:
+ # Attributes are not dropped at this stage. That happens when the
+ # start tag token is emitted so values can still be safely appended
+ # to attributes, but we do want to report the parse error in time.
+ self.currentToken["data"][-1][0] = (
+ self.currentToken["data"][-1][0].translate(asciiUpper2Lower))
+ for name, _ in self.currentToken["data"][:-1]:
+ if self.currentToken["data"][-1][0] == name:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "duplicate-attribute"})
+ break
+ # XXX Fix for above XXX
+ if emitToken:
+ self.emitCurrentToken()
+ return True
+
+ def afterAttributeNameState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.stream.charsUntil(spaceCharacters, True)
+ elif data == "=":
+ self.state = self.beforeAttributeValueState
+ elif data == ">":
+ self.emitCurrentToken()
+ elif data in asciiLetters:
+ self.currentToken["data"].append([data, ""])
+ self.state = self.attributeNameState
+ elif data == "/":
+ self.state = self.selfClosingStartTagState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"].append(["\uFFFD", ""])
+ self.state = self.attributeNameState
+ elif data in ("'", '"', "<"):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "invalid-character-after-attribute-name"})
+ self.currentToken["data"].append([data, ""])
+ self.state = self.attributeNameState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-end-of-tag-but-got-eof"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"].append([data, ""])
+ self.state = self.attributeNameState
+ return True
+
+ def beforeAttributeValueState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.stream.charsUntil(spaceCharacters, True)
+ elif data == "\"":
+ self.state = self.attributeValueDoubleQuotedState
+ elif data == "&":
+ self.state = self.attributeValueUnQuotedState
+ self.stream.unget(data)
+ elif data == "'":
+ self.state = self.attributeValueSingleQuotedState
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-attribute-value-but-got-right-bracket"})
+ self.emitCurrentToken()
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"][-1][1] += "\uFFFD"
+ self.state = self.attributeValueUnQuotedState
+ elif data in ("=", "<", "`"):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "equals-in-unquoted-attribute-value"})
+ self.currentToken["data"][-1][1] += data
+ self.state = self.attributeValueUnQuotedState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-attribute-value-but-got-eof"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"][-1][1] += data
+ self.state = self.attributeValueUnQuotedState
+ return True
+
+ def attributeValueDoubleQuotedState(self):
+ data = self.stream.char()
+ if data == "\"":
+ self.state = self.afterAttributeValueState
+ elif data == "&":
+ self.processEntityInAttribute('"')
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"][-1][1] += "\uFFFD"
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-attribute-value-double-quote"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"][-1][1] += data +\
+ self.stream.charsUntil(("\"", "&", "\u0000"))
+ return True
+
+ def attributeValueSingleQuotedState(self):
+ data = self.stream.char()
+ if data == "'":
+ self.state = self.afterAttributeValueState
+ elif data == "&":
+ self.processEntityInAttribute("'")
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"][-1][1] += "\uFFFD"
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-attribute-value-single-quote"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"][-1][1] += data +\
+ self.stream.charsUntil(("'", "&", "\u0000"))
+ return True
+
+ def attributeValueUnQuotedState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.beforeAttributeNameState
+ elif data == "&":
+ self.processEntityInAttribute(">")
+ elif data == ">":
+ self.emitCurrentToken()
+ elif data in ('"', "'", "=", "<", "`"):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-character-in-unquoted-attribute-value"})
+ self.currentToken["data"][-1][1] += data
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"][-1][1] += "\uFFFD"
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-attribute-value-no-quotes"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"][-1][1] += data + self.stream.charsUntil(
+ frozenset(("&", ">", '"', "'", "=", "<", "`", "\u0000")) | spaceCharacters)
+ return True
+
+ def afterAttributeValueState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.beforeAttributeNameState
+ elif data == ">":
+ self.emitCurrentToken()
+ elif data == "/":
+ self.state = self.selfClosingStartTagState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-EOF-after-attribute-value"})
+ self.stream.unget(data)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-character-after-attribute-value"})
+ self.stream.unget(data)
+ self.state = self.beforeAttributeNameState
+ return True
+
+ def selfClosingStartTagState(self):
+ data = self.stream.char()
+ if data == ">":
+ self.currentToken["selfClosing"] = True
+ self.emitCurrentToken()
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data":
+ "unexpected-EOF-after-solidus-in-tag"})
+ self.stream.unget(data)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-character-after-solidus-in-tag"})
+ self.stream.unget(data)
+ self.state = self.beforeAttributeNameState
+ return True
+
+ def bogusCommentState(self):
+ # Make a new comment token and give it as value all the characters
+ # until the first > or EOF (charsUntil checks for EOF automatically)
+ # and emit it.
+ data = self.stream.charsUntil(">")
+ data = data.replace("\u0000", "\uFFFD")
+ self.tokenQueue.append(
+ {"type": tokenTypes["Comment"], "data": data})
+
+ # Eat the character directly after the bogus comment which is either a
+ # ">" or an EOF.
+ self.stream.char()
+ self.state = self.dataState
+ return True
+
+ def markupDeclarationOpenState(self):
+ charStack = [self.stream.char()]
+ if charStack[-1] == "-":
+ charStack.append(self.stream.char())
+ if charStack[-1] == "-":
+ self.currentToken = {"type": tokenTypes["Comment"], "data": ""}
+ self.state = self.commentStartState
+ return True
+ elif charStack[-1] in ('d', 'D'):
+ matched = True
+ for expected in (('o', 'O'), ('c', 'C'), ('t', 'T'),
+ ('y', 'Y'), ('p', 'P'), ('e', 'E')):
+ charStack.append(self.stream.char())
+ if charStack[-1] not in expected:
+ matched = False
+ break
+ if matched:
+ self.currentToken = {"type": tokenTypes["Doctype"],
+ "name": "",
+ "publicId": None, "systemId": None,
+ "correct": True}
+ self.state = self.doctypeState
+ return True
+ elif (charStack[-1] == "[" and
+ self.parser is not None and
+ self.parser.tree.openElements and
+ self.parser.tree.openElements[-1].namespace != self.parser.tree.defaultNamespace):
+ matched = True
+ for expected in ["C", "D", "A", "T", "A", "["]:
+ charStack.append(self.stream.char())
+ if charStack[-1] != expected:
+ matched = False
+ break
+ if matched:
+ self.state = self.cdataSectionState
+ return True
+
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-dashes-or-doctype"})
+
+ while charStack:
+ self.stream.unget(charStack.pop())
+ self.state = self.bogusCommentState
+ return True
+
+ def commentStartState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.state = self.commentStartDashState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"] += "\uFFFD"
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "incorrect-comment"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-comment"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["data"] += data
+ self.state = self.commentState
+ return True
+
+ def commentStartDashState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.state = self.commentEndState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"] += "-\uFFFD"
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "incorrect-comment"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-comment"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["data"] += "-" + data
+ self.state = self.commentState
+ return True
+
+ def commentState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.state = self.commentEndDashState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"] += "\uFFFD"
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "eof-in-comment"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["data"] += data + \
+ self.stream.charsUntil(("-", "\u0000"))
+ return True
+
+ def commentEndDashState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.state = self.commentEndState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"] += "-\uFFFD"
+ self.state = self.commentState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-comment-end-dash"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["data"] += "-" + data
+ self.state = self.commentState
+ return True
+
+ def commentEndState(self):
+ data = self.stream.char()
+ if data == ">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"] += "--\uFFFD"
+ self.state = self.commentState
+ elif data == "!":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-bang-after-double-dash-in-comment"})
+ self.state = self.commentEndBangState
+ elif data == "-":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-dash-after-double-dash-in-comment"})
+ self.currentToken["data"] += data
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-comment-double-dash"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ # XXX
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-comment"})
+ self.currentToken["data"] += "--" + data
+ self.state = self.commentState
+ return True
+
+ def commentEndBangState(self):
+ data = self.stream.char()
+ if data == ">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data == "-":
+ self.currentToken["data"] += "--!"
+ self.state = self.commentEndDashState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"] += "--!\uFFFD"
+ self.state = self.commentState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-comment-end-bang-state"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["data"] += "--!" + data
+ self.state = self.commentState
+ return True
+
+ def doctypeState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.beforeDoctypeNameState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-doctype-name-but-got-eof"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "need-space-after-doctype"})
+ self.stream.unget(data)
+ self.state = self.beforeDoctypeNameState
+ return True
+
+ def beforeDoctypeNameState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ pass
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-doctype-name-but-got-right-bracket"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["name"] = "\uFFFD"
+ self.state = self.doctypeNameState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-doctype-name-but-got-eof"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["name"] = data
+ self.state = self.doctypeNameState
+ return True
+
+ def doctypeNameState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower)
+ self.state = self.afterDoctypeNameState
+ elif data == ">":
+ self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower)
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["name"] += "\uFFFD"
+ self.state = self.doctypeNameState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype-name"})
+ self.currentToken["correct"] = False
+ self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower)
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["name"] += data
+ return True
+
+ def afterDoctypeNameState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ pass
+ elif data == ">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.currentToken["correct"] = False
+ self.stream.unget(data)
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ if data in ("p", "P"):
+ matched = True
+ for expected in (("u", "U"), ("b", "B"), ("l", "L"),
+ ("i", "I"), ("c", "C")):
+ data = self.stream.char()
+ if data not in expected:
+ matched = False
+ break
+ if matched:
+ self.state = self.afterDoctypePublicKeywordState
+ return True
+ elif data in ("s", "S"):
+ matched = True
+ for expected in (("y", "Y"), ("s", "S"), ("t", "T"),
+ ("e", "E"), ("m", "M")):
+ data = self.stream.char()
+ if data not in expected:
+ matched = False
+ break
+ if matched:
+ self.state = self.afterDoctypeSystemKeywordState
+ return True
+
+ # All the characters read before the current 'data' will be
+ # [a-zA-Z], so they're garbage in the bogus doctype and can be
+ # discarded; only the latest character might be '>' or EOF
+ # and needs to be ungetted
+ self.stream.unget(data)
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-space-or-right-bracket-in-doctype", "datavars":
+ {"data": data}})
+ self.currentToken["correct"] = False
+ self.state = self.bogusDoctypeState
+
+ return True
+
+ def afterDoctypePublicKeywordState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.beforeDoctypePublicIdentifierState
+ elif data in ("'", '"'):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.stream.unget(data)
+ self.state = self.beforeDoctypePublicIdentifierState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.stream.unget(data)
+ self.state = self.beforeDoctypePublicIdentifierState
+ return True
+
+ def beforeDoctypePublicIdentifierState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ pass
+ elif data == "\"":
+ self.currentToken["publicId"] = ""
+ self.state = self.doctypePublicIdentifierDoubleQuotedState
+ elif data == "'":
+ self.currentToken["publicId"] = ""
+ self.state = self.doctypePublicIdentifierSingleQuotedState
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-end-of-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["correct"] = False
+ self.state = self.bogusDoctypeState
+ return True
+
+ def doctypePublicIdentifierDoubleQuotedState(self):
+ data = self.stream.char()
+ if data == "\"":
+ self.state = self.afterDoctypePublicIdentifierState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["publicId"] += "\uFFFD"
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-end-of-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["publicId"] += data
+ return True
+
+ def doctypePublicIdentifierSingleQuotedState(self):
+ data = self.stream.char()
+ if data == "'":
+ self.state = self.afterDoctypePublicIdentifierState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["publicId"] += "\uFFFD"
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-end-of-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["publicId"] += data
+ return True
+
+ def afterDoctypePublicIdentifierState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.betweenDoctypePublicAndSystemIdentifiersState
+ elif data == ">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data == '"':
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["systemId"] = ""
+ self.state = self.doctypeSystemIdentifierDoubleQuotedState
+ elif data == "'":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["systemId"] = ""
+ self.state = self.doctypeSystemIdentifierSingleQuotedState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["correct"] = False
+ self.state = self.bogusDoctypeState
+ return True
+
+ def betweenDoctypePublicAndSystemIdentifiersState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ pass
+ elif data == ">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data == '"':
+ self.currentToken["systemId"] = ""
+ self.state = self.doctypeSystemIdentifierDoubleQuotedState
+ elif data == "'":
+ self.currentToken["systemId"] = ""
+ self.state = self.doctypeSystemIdentifierSingleQuotedState
+ elif data == EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["correct"] = False
+ self.state = self.bogusDoctypeState
+ return True
+
+ def afterDoctypeSystemKeywordState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.beforeDoctypeSystemIdentifierState
+ elif data in ("'", '"'):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.stream.unget(data)
+ self.state = self.beforeDoctypeSystemIdentifierState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.stream.unget(data)
+ self.state = self.beforeDoctypeSystemIdentifierState
+ return True
+
+ def beforeDoctypeSystemIdentifierState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ pass
+ elif data == "\"":
+ self.currentToken["systemId"] = ""
+ self.state = self.doctypeSystemIdentifierDoubleQuotedState
+ elif data == "'":
+ self.currentToken["systemId"] = ""
+ self.state = self.doctypeSystemIdentifierSingleQuotedState
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["correct"] = False
+ self.state = self.bogusDoctypeState
+ return True
+
+ def doctypeSystemIdentifierDoubleQuotedState(self):
+ data = self.stream.char()
+ if data == "\"":
+ self.state = self.afterDoctypeSystemIdentifierState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["systemId"] += "\uFFFD"
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-end-of-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["systemId"] += data
+ return True
+
+ def doctypeSystemIdentifierSingleQuotedState(self):
+ data = self.stream.char()
+ if data == "'":
+ self.state = self.afterDoctypeSystemIdentifierState
+ elif data == "\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["systemId"] += "\uFFFD"
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-end-of-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["systemId"] += data
+ return True
+
+ def afterDoctypeSystemIdentifierState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ pass
+ elif data == ">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.state = self.bogusDoctypeState
+ return True
+
+ def bogusDoctypeState(self):
+ data = self.stream.char()
+ if data == ">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ # XXX EMIT
+ self.stream.unget(data)
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ pass
+ return True
+
+ def cdataSectionState(self):
+ data = []
+ while True:
+ data.append(self.stream.charsUntil("]"))
+ data.append(self.stream.charsUntil(">"))
+ char = self.stream.char()
+ if char == EOF:
+ break
+ else:
+ assert char == ">"
+ if data[-1][-2:] == "]]":
+ data[-1] = data[-1][:-2]
+ break
+ else:
+ data.append(char)
+
+ data = "".join(data) # pylint:disable=redefined-variable-type
+ # Deal with null here rather than in the parser
+ nullCount = data.count("\u0000")
+ if nullCount > 0:
+ for _ in range(nullCount):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ data = data.replace("\u0000", "\uFFFD")
+ if data:
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": data})
+ self.state = self.dataState
+ return True
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_trie/__init__.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_trie/__init__.py
new file mode 100644
index 0000000000..07bad5d31c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_trie/__init__.py
@@ -0,0 +1,5 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from .py import Trie
+
+__all__ = ["Trie"]
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_trie/_base.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_trie/_base.py
new file mode 100644
index 0000000000..6b71975f08
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_trie/_base.py
@@ -0,0 +1,40 @@
+from __future__ import absolute_import, division, unicode_literals
+
+try:
+ from collections.abc import Mapping
+except ImportError: # Python 2.7
+ from collections import Mapping
+
+
+class Trie(Mapping):
+ """Abstract base class for tries"""
+
+ def keys(self, prefix=None):
+ # pylint:disable=arguments-differ
+ keys = super(Trie, self).keys()
+
+ if prefix is None:
+ return set(keys)
+
+ return {x for x in keys if x.startswith(prefix)}
+
+ def has_keys_with_prefix(self, prefix):
+ for key in self.keys():
+ if key.startswith(prefix):
+ return True
+
+ return False
+
+ def longest_prefix(self, prefix):
+ if prefix in self:
+ return prefix
+
+ for i in range(1, len(prefix) + 1):
+ if prefix[:-i] in self:
+ return prefix[:-i]
+
+ raise KeyError(prefix)
+
+ def longest_prefix_item(self, prefix):
+ lprefix = self.longest_prefix(prefix)
+ return (lprefix, self[lprefix])
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_trie/py.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_trie/py.py
new file mode 100644
index 0000000000..c2ba3da757
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_trie/py.py
@@ -0,0 +1,67 @@
+from __future__ import absolute_import, division, unicode_literals
+from six import text_type
+
+from bisect import bisect_left
+
+from ._base import Trie as ABCTrie
+
+
+class Trie(ABCTrie):
+ def __init__(self, data):
+ if not all(isinstance(x, text_type) for x in data.keys()):
+ raise TypeError("All keys must be strings")
+
+ self._data = data
+ self._keys = sorted(data.keys())
+ self._cachestr = ""
+ self._cachepoints = (0, len(data))
+
+ def __contains__(self, key):
+ return key in self._data
+
+ def __len__(self):
+ return len(self._data)
+
+ def __iter__(self):
+ return iter(self._data)
+
+ def __getitem__(self, key):
+ return self._data[key]
+
+ def keys(self, prefix=None):
+ if prefix is None or prefix == "" or not self._keys:
+ return set(self._keys)
+
+ if prefix.startswith(self._cachestr):
+ lo, hi = self._cachepoints
+ start = i = bisect_left(self._keys, prefix, lo, hi)
+ else:
+ start = i = bisect_left(self._keys, prefix)
+
+ keys = set()
+ if start == len(self._keys):
+ return keys
+
+ while self._keys[i].startswith(prefix):
+ keys.add(self._keys[i])
+ i += 1
+
+ self._cachestr = prefix
+ self._cachepoints = (start, i)
+
+ return keys
+
+ def has_keys_with_prefix(self, prefix):
+ if prefix in self._data:
+ return True
+
+ if prefix.startswith(self._cachestr):
+ lo, hi = self._cachepoints
+ i = bisect_left(self._keys, prefix, lo, hi)
+ else:
+ i = bisect_left(self._keys, prefix)
+
+ if i == len(self._keys):
+ return False
+
+ return self._keys[i].startswith(prefix)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_utils.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_utils.py
new file mode 100644
index 0000000000..9ea5794214
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/_utils.py
@@ -0,0 +1,159 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from types import ModuleType
+
+try:
+ from collections.abc import Mapping
+except ImportError:
+ from collections import Mapping
+
+from six import text_type, PY3
+
+if PY3:
+ import xml.etree.ElementTree as default_etree
+else:
+ try:
+ import xml.etree.cElementTree as default_etree
+ except ImportError:
+ import xml.etree.ElementTree as default_etree
+
+
+__all__ = ["default_etree", "MethodDispatcher", "isSurrogatePair",
+ "surrogatePairToCodepoint", "moduleFactoryFactory",
+ "supports_lone_surrogates"]
+
+
+# Platforms not supporting lone surrogates (\uD800-\uDFFF) should be
+# caught by the below test. In general this would be any platform
+# using UTF-16 as its encoding of unicode strings, such as
+# Jython. This is because UTF-16 itself is based on the use of such
+# surrogates, and there is no mechanism to further escape such
+# escapes.
+try:
+ _x = eval('"\\uD800"') # pylint:disable=eval-used
+ if not isinstance(_x, text_type):
+ # We need this with u"" because of http://bugs.jython.org/issue2039
+ _x = eval('u"\\uD800"') # pylint:disable=eval-used
+ assert isinstance(_x, text_type)
+except Exception:
+ supports_lone_surrogates = False
+else:
+ supports_lone_surrogates = True
+
+
+class MethodDispatcher(dict):
+ """Dict with 2 special properties:
+
+ On initiation, keys that are lists, sets or tuples are converted to
+ multiple keys so accessing any one of the items in the original
+ list-like object returns the matching value
+
+ md = MethodDispatcher({("foo", "bar"):"baz"})
+ md["foo"] == "baz"
+
+ A default value which can be set through the default attribute.
+ """
+
+ def __init__(self, items=()):
+ _dictEntries = []
+ for name, value in items:
+ if isinstance(name, (list, tuple, frozenset, set)):
+ for item in name:
+ _dictEntries.append((item, value))
+ else:
+ _dictEntries.append((name, value))
+ dict.__init__(self, _dictEntries)
+ assert len(self) == len(_dictEntries)
+ self.default = None
+
+ def __getitem__(self, key):
+ return dict.get(self, key, self.default)
+
+ def __get__(self, instance, owner=None):
+ return BoundMethodDispatcher(instance, self)
+
+
+class BoundMethodDispatcher(Mapping):
+ """Wraps a MethodDispatcher, binding its return values to `instance`"""
+ def __init__(self, instance, dispatcher):
+ self.instance = instance
+ self.dispatcher = dispatcher
+
+ def __getitem__(self, key):
+ # see https://docs.python.org/3/reference/datamodel.html#object.__get__
+ # on a function, __get__ is used to bind a function to an instance as a bound method
+ return self.dispatcher[key].__get__(self.instance)
+
+ def get(self, key, default):
+ if key in self.dispatcher:
+ return self[key]
+ else:
+ return default
+
+ def __iter__(self):
+ return iter(self.dispatcher)
+
+ def __len__(self):
+ return len(self.dispatcher)
+
+ def __contains__(self, key):
+ return key in self.dispatcher
+
+
+# Some utility functions to deal with weirdness around UCS2 vs UCS4
+# python builds
+
+def isSurrogatePair(data):
+ return (len(data) == 2 and
+ ord(data[0]) >= 0xD800 and ord(data[0]) <= 0xDBFF and
+ ord(data[1]) >= 0xDC00 and ord(data[1]) <= 0xDFFF)
+
+
+def surrogatePairToCodepoint(data):
+ char_val = (0x10000 + (ord(data[0]) - 0xD800) * 0x400 +
+ (ord(data[1]) - 0xDC00))
+ return char_val
+
+# Module Factory Factory (no, this isn't Java, I know)
+# Here to stop this being duplicated all over the place.
+
+
+def moduleFactoryFactory(factory):
+ moduleCache = {}
+
+ def moduleFactory(baseModule, *args, **kwargs):
+ if isinstance(ModuleType.__name__, type("")):
+ name = "_%s_factory" % baseModule.__name__
+ else:
+ name = b"_%s_factory" % baseModule.__name__
+
+ kwargs_tuple = tuple(kwargs.items())
+
+ try:
+ return moduleCache[name][args][kwargs_tuple]
+ except KeyError:
+ mod = ModuleType(name)
+ objs = factory(baseModule, *args, **kwargs)
+ mod.__dict__.update(objs)
+ if "name" not in moduleCache:
+ moduleCache[name] = {}
+ if "args" not in moduleCache[name]:
+ moduleCache[name][args] = {}
+ if "kwargs" not in moduleCache[name][args]:
+ moduleCache[name][args][kwargs_tuple] = {}
+ moduleCache[name][args][kwargs_tuple] = mod
+ return mod
+
+ return moduleFactory
+
+
+def memoize(func):
+ cache = {}
+
+ def wrapped(*args, **kwargs):
+ key = (tuple(args), tuple(kwargs.items()))
+ if key not in cache:
+ cache[key] = func(*args, **kwargs)
+ return cache[key]
+
+ return wrapped
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/constants.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/constants.py
new file mode 100644
index 0000000000..fe3e237cd8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/constants.py
@@ -0,0 +1,2946 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import string
+
+EOF = None
+
+E = {
+ "null-character":
+ "Null character in input stream, replaced with U+FFFD.",
+ "invalid-codepoint":
+ "Invalid codepoint in stream.",
+ "incorrectly-placed-solidus":
+ "Solidus (/) incorrectly placed in tag.",
+ "incorrect-cr-newline-entity":
+ "Incorrect CR newline entity, replaced with LF.",
+ "illegal-windows-1252-entity":
+ "Entity used with illegal number (windows-1252 reference).",
+ "cant-convert-numeric-entity":
+ "Numeric entity couldn't be converted to character "
+ "(codepoint U+%(charAsInt)08x).",
+ "illegal-codepoint-for-numeric-entity":
+ "Numeric entity represents an illegal codepoint: "
+ "U+%(charAsInt)08x.",
+ "numeric-entity-without-semicolon":
+ "Numeric entity didn't end with ';'.",
+ "expected-numeric-entity-but-got-eof":
+ "Numeric entity expected. Got end of file instead.",
+ "expected-numeric-entity":
+ "Numeric entity expected but none found.",
+ "named-entity-without-semicolon":
+ "Named entity didn't end with ';'.",
+ "expected-named-entity":
+ "Named entity expected. Got none.",
+ "attributes-in-end-tag":
+ "End tag contains unexpected attributes.",
+ 'self-closing-flag-on-end-tag':
+ "End tag contains unexpected self-closing flag.",
+ "expected-tag-name-but-got-right-bracket":
+ "Expected tag name. Got '>' instead.",
+ "expected-tag-name-but-got-question-mark":
+ "Expected tag name. Got '?' instead. (HTML doesn't "
+ "support processing instructions.)",
+ "expected-tag-name":
+ "Expected tag name. Got something else instead",
+ "expected-closing-tag-but-got-right-bracket":
+ "Expected closing tag. Got '>' instead. Ignoring '</>'.",
+ "expected-closing-tag-but-got-eof":
+ "Expected closing tag. Unexpected end of file.",
+ "expected-closing-tag-but-got-char":
+ "Expected closing tag. Unexpected character '%(data)s' found.",
+ "eof-in-tag-name":
+ "Unexpected end of file in the tag name.",
+ "expected-attribute-name-but-got-eof":
+ "Unexpected end of file. Expected attribute name instead.",
+ "eof-in-attribute-name":
+ "Unexpected end of file in attribute name.",
+ "invalid-character-in-attribute-name":
+ "Invalid character in attribute name",
+ "duplicate-attribute":
+ "Dropped duplicate attribute on tag.",
+ "expected-end-of-tag-name-but-got-eof":
+ "Unexpected end of file. Expected = or end of tag.",
+ "expected-attribute-value-but-got-eof":
+ "Unexpected end of file. Expected attribute value.",
+ "expected-attribute-value-but-got-right-bracket":
+ "Expected attribute value. Got '>' instead.",
+ 'equals-in-unquoted-attribute-value':
+ "Unexpected = in unquoted attribute",
+ 'unexpected-character-in-unquoted-attribute-value':
+ "Unexpected character in unquoted attribute",
+ "invalid-character-after-attribute-name":
+ "Unexpected character after attribute name.",
+ "unexpected-character-after-attribute-value":
+ "Unexpected character after attribute value.",
+ "eof-in-attribute-value-double-quote":
+ "Unexpected end of file in attribute value (\").",
+ "eof-in-attribute-value-single-quote":
+ "Unexpected end of file in attribute value (').",
+ "eof-in-attribute-value-no-quotes":
+ "Unexpected end of file in attribute value.",
+ "unexpected-EOF-after-solidus-in-tag":
+ "Unexpected end of file in tag. Expected >",
+ "unexpected-character-after-solidus-in-tag":
+ "Unexpected character after / in tag. Expected >",
+ "expected-dashes-or-doctype":
+ "Expected '--' or 'DOCTYPE'. Not found.",
+ "unexpected-bang-after-double-dash-in-comment":
+ "Unexpected ! after -- in comment",
+ "unexpected-space-after-double-dash-in-comment":
+ "Unexpected space after -- in comment",
+ "incorrect-comment":
+ "Incorrect comment.",
+ "eof-in-comment":
+ "Unexpected end of file in comment.",
+ "eof-in-comment-end-dash":
+ "Unexpected end of file in comment (-)",
+ "unexpected-dash-after-double-dash-in-comment":
+ "Unexpected '-' after '--' found in comment.",
+ "eof-in-comment-double-dash":
+ "Unexpected end of file in comment (--).",
+ "eof-in-comment-end-space-state":
+ "Unexpected end of file in comment.",
+ "eof-in-comment-end-bang-state":
+ "Unexpected end of file in comment.",
+ "unexpected-char-in-comment":
+ "Unexpected character in comment found.",
+ "need-space-after-doctype":
+ "No space after literal string 'DOCTYPE'.",
+ "expected-doctype-name-but-got-right-bracket":
+ "Unexpected > character. Expected DOCTYPE name.",
+ "expected-doctype-name-but-got-eof":
+ "Unexpected end of file. Expected DOCTYPE name.",
+ "eof-in-doctype-name":
+ "Unexpected end of file in DOCTYPE name.",
+ "eof-in-doctype":
+ "Unexpected end of file in DOCTYPE.",
+ "expected-space-or-right-bracket-in-doctype":
+ "Expected space or '>'. Got '%(data)s'",
+ "unexpected-end-of-doctype":
+ "Unexpected end of DOCTYPE.",
+ "unexpected-char-in-doctype":
+ "Unexpected character in DOCTYPE.",
+ "eof-in-innerhtml":
+ "XXX innerHTML EOF",
+ "unexpected-doctype":
+ "Unexpected DOCTYPE. Ignored.",
+ "non-html-root":
+ "html needs to be the first start tag.",
+ "expected-doctype-but-got-eof":
+ "Unexpected End of file. Expected DOCTYPE.",
+ "unknown-doctype":
+ "Erroneous DOCTYPE.",
+ "expected-doctype-but-got-chars":
+ "Unexpected non-space characters. Expected DOCTYPE.",
+ "expected-doctype-but-got-start-tag":
+ "Unexpected start tag (%(name)s). Expected DOCTYPE.",
+ "expected-doctype-but-got-end-tag":
+ "Unexpected end tag (%(name)s). Expected DOCTYPE.",
+ "end-tag-after-implied-root":
+ "Unexpected end tag (%(name)s) after the (implied) root element.",
+ "expected-named-closing-tag-but-got-eof":
+ "Unexpected end of file. Expected end tag (%(name)s).",
+ "two-heads-are-not-better-than-one":
+ "Unexpected start tag head in existing head. Ignored.",
+ "unexpected-end-tag":
+ "Unexpected end tag (%(name)s). Ignored.",
+ "unexpected-start-tag-out-of-my-head":
+ "Unexpected start tag (%(name)s) that can be in head. Moved.",
+ "unexpected-start-tag":
+ "Unexpected start tag (%(name)s).",
+ "missing-end-tag":
+ "Missing end tag (%(name)s).",
+ "missing-end-tags":
+ "Missing end tags (%(name)s).",
+ "unexpected-start-tag-implies-end-tag":
+ "Unexpected start tag (%(startName)s) "
+ "implies end tag (%(endName)s).",
+ "unexpected-start-tag-treated-as":
+ "Unexpected start tag (%(originalName)s). Treated as %(newName)s.",
+ "deprecated-tag":
+ "Unexpected start tag %(name)s. Don't use it!",
+ "unexpected-start-tag-ignored":
+ "Unexpected start tag %(name)s. Ignored.",
+ "expected-one-end-tag-but-got-another":
+ "Unexpected end tag (%(gotName)s). "
+ "Missing end tag (%(expectedName)s).",
+ "end-tag-too-early":
+ "End tag (%(name)s) seen too early. Expected other end tag.",
+ "end-tag-too-early-named":
+ "Unexpected end tag (%(gotName)s). Expected end tag (%(expectedName)s).",
+ "end-tag-too-early-ignored":
+ "End tag (%(name)s) seen too early. Ignored.",
+ "adoption-agency-1.1":
+ "End tag (%(name)s) violates step 1, "
+ "paragraph 1 of the adoption agency algorithm.",
+ "adoption-agency-1.2":
+ "End tag (%(name)s) violates step 1, "
+ "paragraph 2 of the adoption agency algorithm.",
+ "adoption-agency-1.3":
+ "End tag (%(name)s) violates step 1, "
+ "paragraph 3 of the adoption agency algorithm.",
+ "adoption-agency-4.4":
+ "End tag (%(name)s) violates step 4, "
+ "paragraph 4 of the adoption agency algorithm.",
+ "unexpected-end-tag-treated-as":
+ "Unexpected end tag (%(originalName)s). Treated as %(newName)s.",
+ "no-end-tag":
+ "This element (%(name)s) has no end tag.",
+ "unexpected-implied-end-tag-in-table":
+ "Unexpected implied end tag (%(name)s) in the table phase.",
+ "unexpected-implied-end-tag-in-table-body":
+ "Unexpected implied end tag (%(name)s) in the table body phase.",
+ "unexpected-char-implies-table-voodoo":
+ "Unexpected non-space characters in "
+ "table context caused voodoo mode.",
+ "unexpected-hidden-input-in-table":
+ "Unexpected input with type hidden in table context.",
+ "unexpected-form-in-table":
+ "Unexpected form in table context.",
+ "unexpected-start-tag-implies-table-voodoo":
+ "Unexpected start tag (%(name)s) in "
+ "table context caused voodoo mode.",
+ "unexpected-end-tag-implies-table-voodoo":
+ "Unexpected end tag (%(name)s) in "
+ "table context caused voodoo mode.",
+ "unexpected-cell-in-table-body":
+ "Unexpected table cell start tag (%(name)s) "
+ "in the table body phase.",
+ "unexpected-cell-end-tag":
+ "Got table cell end tag (%(name)s) "
+ "while required end tags are missing.",
+ "unexpected-end-tag-in-table-body":
+ "Unexpected end tag (%(name)s) in the table body phase. Ignored.",
+ "unexpected-implied-end-tag-in-table-row":
+ "Unexpected implied end tag (%(name)s) in the table row phase.",
+ "unexpected-end-tag-in-table-row":
+ "Unexpected end tag (%(name)s) in the table row phase. Ignored.",
+ "unexpected-select-in-select":
+ "Unexpected select start tag in the select phase "
+ "treated as select end tag.",
+ "unexpected-input-in-select":
+ "Unexpected input start tag in the select phase.",
+ "unexpected-start-tag-in-select":
+ "Unexpected start tag token (%(name)s in the select phase. "
+ "Ignored.",
+ "unexpected-end-tag-in-select":
+ "Unexpected end tag (%(name)s) in the select phase. Ignored.",
+ "unexpected-table-element-start-tag-in-select-in-table":
+ "Unexpected table element start tag (%(name)s) in the select in table phase.",
+ "unexpected-table-element-end-tag-in-select-in-table":
+ "Unexpected table element end tag (%(name)s) in the select in table phase.",
+ "unexpected-char-after-body":
+ "Unexpected non-space characters in the after body phase.",
+ "unexpected-start-tag-after-body":
+ "Unexpected start tag token (%(name)s)"
+ " in the after body phase.",
+ "unexpected-end-tag-after-body":
+ "Unexpected end tag token (%(name)s)"
+ " in the after body phase.",
+ "unexpected-char-in-frameset":
+ "Unexpected characters in the frameset phase. Characters ignored.",
+ "unexpected-start-tag-in-frameset":
+ "Unexpected start tag token (%(name)s)"
+ " in the frameset phase. Ignored.",
+ "unexpected-frameset-in-frameset-innerhtml":
+ "Unexpected end tag token (frameset) "
+ "in the frameset phase (innerHTML).",
+ "unexpected-end-tag-in-frameset":
+ "Unexpected end tag token (%(name)s)"
+ " in the frameset phase. Ignored.",
+ "unexpected-char-after-frameset":
+ "Unexpected non-space characters in the "
+ "after frameset phase. Ignored.",
+ "unexpected-start-tag-after-frameset":
+ "Unexpected start tag (%(name)s)"
+ " in the after frameset phase. Ignored.",
+ "unexpected-end-tag-after-frameset":
+ "Unexpected end tag (%(name)s)"
+ " in the after frameset phase. Ignored.",
+ "unexpected-end-tag-after-body-innerhtml":
+ "Unexpected end tag after body(innerHtml)",
+ "expected-eof-but-got-char":
+ "Unexpected non-space characters. Expected end of file.",
+ "expected-eof-but-got-start-tag":
+ "Unexpected start tag (%(name)s)"
+ ". Expected end of file.",
+ "expected-eof-but-got-end-tag":
+ "Unexpected end tag (%(name)s)"
+ ". Expected end of file.",
+ "eof-in-table":
+ "Unexpected end of file. Expected table content.",
+ "eof-in-select":
+ "Unexpected end of file. Expected select content.",
+ "eof-in-frameset":
+ "Unexpected end of file. Expected frameset content.",
+ "eof-in-script-in-script":
+ "Unexpected end of file. Expected script content.",
+ "eof-in-foreign-lands":
+ "Unexpected end of file. Expected foreign content",
+ "non-void-element-with-trailing-solidus":
+ "Trailing solidus not allowed on element %(name)s",
+ "unexpected-html-element-in-foreign-content":
+ "Element %(name)s not allowed in a non-html context",
+ "unexpected-end-tag-before-html":
+ "Unexpected end tag (%(name)s) before html.",
+ "unexpected-inhead-noscript-tag":
+ "Element %(name)s not allowed in a inhead-noscript context",
+ "eof-in-head-noscript":
+ "Unexpected end of file. Expected inhead-noscript content",
+ "char-in-head-noscript":
+ "Unexpected non-space character. Expected inhead-noscript content",
+ "XXX-undefined-error":
+ "Undefined error (this sucks and should be fixed)",
+}
+
+namespaces = {
+ "html": "http://www.w3.org/1999/xhtml",
+ "mathml": "http://www.w3.org/1998/Math/MathML",
+ "svg": "http://www.w3.org/2000/svg",
+ "xlink": "http://www.w3.org/1999/xlink",
+ "xml": "http://www.w3.org/XML/1998/namespace",
+ "xmlns": "http://www.w3.org/2000/xmlns/"
+}
+
+scopingElements = frozenset([
+ (namespaces["html"], "applet"),
+ (namespaces["html"], "caption"),
+ (namespaces["html"], "html"),
+ (namespaces["html"], "marquee"),
+ (namespaces["html"], "object"),
+ (namespaces["html"], "table"),
+ (namespaces["html"], "td"),
+ (namespaces["html"], "th"),
+ (namespaces["mathml"], "mi"),
+ (namespaces["mathml"], "mo"),
+ (namespaces["mathml"], "mn"),
+ (namespaces["mathml"], "ms"),
+ (namespaces["mathml"], "mtext"),
+ (namespaces["mathml"], "annotation-xml"),
+ (namespaces["svg"], "foreignObject"),
+ (namespaces["svg"], "desc"),
+ (namespaces["svg"], "title"),
+])
+
+formattingElements = frozenset([
+ (namespaces["html"], "a"),
+ (namespaces["html"], "b"),
+ (namespaces["html"], "big"),
+ (namespaces["html"], "code"),
+ (namespaces["html"], "em"),
+ (namespaces["html"], "font"),
+ (namespaces["html"], "i"),
+ (namespaces["html"], "nobr"),
+ (namespaces["html"], "s"),
+ (namespaces["html"], "small"),
+ (namespaces["html"], "strike"),
+ (namespaces["html"], "strong"),
+ (namespaces["html"], "tt"),
+ (namespaces["html"], "u")
+])
+
+specialElements = frozenset([
+ (namespaces["html"], "address"),
+ (namespaces["html"], "applet"),
+ (namespaces["html"], "area"),
+ (namespaces["html"], "article"),
+ (namespaces["html"], "aside"),
+ (namespaces["html"], "base"),
+ (namespaces["html"], "basefont"),
+ (namespaces["html"], "bgsound"),
+ (namespaces["html"], "blockquote"),
+ (namespaces["html"], "body"),
+ (namespaces["html"], "br"),
+ (namespaces["html"], "button"),
+ (namespaces["html"], "caption"),
+ (namespaces["html"], "center"),
+ (namespaces["html"], "col"),
+ (namespaces["html"], "colgroup"),
+ (namespaces["html"], "command"),
+ (namespaces["html"], "dd"),
+ (namespaces["html"], "details"),
+ (namespaces["html"], "dir"),
+ (namespaces["html"], "div"),
+ (namespaces["html"], "dl"),
+ (namespaces["html"], "dt"),
+ (namespaces["html"], "embed"),
+ (namespaces["html"], "fieldset"),
+ (namespaces["html"], "figure"),
+ (namespaces["html"], "footer"),
+ (namespaces["html"], "form"),
+ (namespaces["html"], "frame"),
+ (namespaces["html"], "frameset"),
+ (namespaces["html"], "h1"),
+ (namespaces["html"], "h2"),
+ (namespaces["html"], "h3"),
+ (namespaces["html"], "h4"),
+ (namespaces["html"], "h5"),
+ (namespaces["html"], "h6"),
+ (namespaces["html"], "head"),
+ (namespaces["html"], "header"),
+ (namespaces["html"], "hr"),
+ (namespaces["html"], "html"),
+ (namespaces["html"], "iframe"),
+ # Note that image is commented out in the spec as "this isn't an
+ # element that can end up on the stack, so it doesn't matter,"
+ (namespaces["html"], "image"),
+ (namespaces["html"], "img"),
+ (namespaces["html"], "input"),
+ (namespaces["html"], "isindex"),
+ (namespaces["html"], "li"),
+ (namespaces["html"], "link"),
+ (namespaces["html"], "listing"),
+ (namespaces["html"], "marquee"),
+ (namespaces["html"], "menu"),
+ (namespaces["html"], "meta"),
+ (namespaces["html"], "nav"),
+ (namespaces["html"], "noembed"),
+ (namespaces["html"], "noframes"),
+ (namespaces["html"], "noscript"),
+ (namespaces["html"], "object"),
+ (namespaces["html"], "ol"),
+ (namespaces["html"], "p"),
+ (namespaces["html"], "param"),
+ (namespaces["html"], "plaintext"),
+ (namespaces["html"], "pre"),
+ (namespaces["html"], "script"),
+ (namespaces["html"], "section"),
+ (namespaces["html"], "select"),
+ (namespaces["html"], "style"),
+ (namespaces["html"], "table"),
+ (namespaces["html"], "tbody"),
+ (namespaces["html"], "td"),
+ (namespaces["html"], "textarea"),
+ (namespaces["html"], "tfoot"),
+ (namespaces["html"], "th"),
+ (namespaces["html"], "thead"),
+ (namespaces["html"], "title"),
+ (namespaces["html"], "tr"),
+ (namespaces["html"], "ul"),
+ (namespaces["html"], "wbr"),
+ (namespaces["html"], "xmp"),
+ (namespaces["svg"], "foreignObject")
+])
+
+htmlIntegrationPointElements = frozenset([
+ (namespaces["mathml"], "annotation-xml"),
+ (namespaces["svg"], "foreignObject"),
+ (namespaces["svg"], "desc"),
+ (namespaces["svg"], "title")
+])
+
+mathmlTextIntegrationPointElements = frozenset([
+ (namespaces["mathml"], "mi"),
+ (namespaces["mathml"], "mo"),
+ (namespaces["mathml"], "mn"),
+ (namespaces["mathml"], "ms"),
+ (namespaces["mathml"], "mtext")
+])
+
+adjustSVGAttributes = {
+ "attributename": "attributeName",
+ "attributetype": "attributeType",
+ "basefrequency": "baseFrequency",
+ "baseprofile": "baseProfile",
+ "calcmode": "calcMode",
+ "clippathunits": "clipPathUnits",
+ "contentscripttype": "contentScriptType",
+ "contentstyletype": "contentStyleType",
+ "diffuseconstant": "diffuseConstant",
+ "edgemode": "edgeMode",
+ "externalresourcesrequired": "externalResourcesRequired",
+ "filterres": "filterRes",
+ "filterunits": "filterUnits",
+ "glyphref": "glyphRef",
+ "gradienttransform": "gradientTransform",
+ "gradientunits": "gradientUnits",
+ "kernelmatrix": "kernelMatrix",
+ "kernelunitlength": "kernelUnitLength",
+ "keypoints": "keyPoints",
+ "keysplines": "keySplines",
+ "keytimes": "keyTimes",
+ "lengthadjust": "lengthAdjust",
+ "limitingconeangle": "limitingConeAngle",
+ "markerheight": "markerHeight",
+ "markerunits": "markerUnits",
+ "markerwidth": "markerWidth",
+ "maskcontentunits": "maskContentUnits",
+ "maskunits": "maskUnits",
+ "numoctaves": "numOctaves",
+ "pathlength": "pathLength",
+ "patterncontentunits": "patternContentUnits",
+ "patterntransform": "patternTransform",
+ "patternunits": "patternUnits",
+ "pointsatx": "pointsAtX",
+ "pointsaty": "pointsAtY",
+ "pointsatz": "pointsAtZ",
+ "preservealpha": "preserveAlpha",
+ "preserveaspectratio": "preserveAspectRatio",
+ "primitiveunits": "primitiveUnits",
+ "refx": "refX",
+ "refy": "refY",
+ "repeatcount": "repeatCount",
+ "repeatdur": "repeatDur",
+ "requiredextensions": "requiredExtensions",
+ "requiredfeatures": "requiredFeatures",
+ "specularconstant": "specularConstant",
+ "specularexponent": "specularExponent",
+ "spreadmethod": "spreadMethod",
+ "startoffset": "startOffset",
+ "stddeviation": "stdDeviation",
+ "stitchtiles": "stitchTiles",
+ "surfacescale": "surfaceScale",
+ "systemlanguage": "systemLanguage",
+ "tablevalues": "tableValues",
+ "targetx": "targetX",
+ "targety": "targetY",
+ "textlength": "textLength",
+ "viewbox": "viewBox",
+ "viewtarget": "viewTarget",
+ "xchannelselector": "xChannelSelector",
+ "ychannelselector": "yChannelSelector",
+ "zoomandpan": "zoomAndPan"
+}
+
+adjustMathMLAttributes = {"definitionurl": "definitionURL"}
+
+adjustForeignAttributes = {
+ "xlink:actuate": ("xlink", "actuate", namespaces["xlink"]),
+ "xlink:arcrole": ("xlink", "arcrole", namespaces["xlink"]),
+ "xlink:href": ("xlink", "href", namespaces["xlink"]),
+ "xlink:role": ("xlink", "role", namespaces["xlink"]),
+ "xlink:show": ("xlink", "show", namespaces["xlink"]),
+ "xlink:title": ("xlink", "title", namespaces["xlink"]),
+ "xlink:type": ("xlink", "type", namespaces["xlink"]),
+ "xml:base": ("xml", "base", namespaces["xml"]),
+ "xml:lang": ("xml", "lang", namespaces["xml"]),
+ "xml:space": ("xml", "space", namespaces["xml"]),
+ "xmlns": (None, "xmlns", namespaces["xmlns"]),
+ "xmlns:xlink": ("xmlns", "xlink", namespaces["xmlns"])
+}
+
+unadjustForeignAttributes = {(ns, local): qname for qname, (prefix, local, ns) in
+ adjustForeignAttributes.items()}
+
+spaceCharacters = frozenset([
+ "\t",
+ "\n",
+ "\u000C",
+ " ",
+ "\r"
+])
+
+tableInsertModeElements = frozenset([
+ "table",
+ "tbody",
+ "tfoot",
+ "thead",
+ "tr"
+])
+
+asciiLowercase = frozenset(string.ascii_lowercase)
+asciiUppercase = frozenset(string.ascii_uppercase)
+asciiLetters = frozenset(string.ascii_letters)
+digits = frozenset(string.digits)
+hexDigits = frozenset(string.hexdigits)
+
+asciiUpper2Lower = {ord(c): ord(c.lower()) for c in string.ascii_uppercase}
+
+# Heading elements need to be ordered
+headingElements = (
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6"
+)
+
+voidElements = frozenset([
+ "base",
+ "command",
+ "event-source",
+ "link",
+ "meta",
+ "hr",
+ "br",
+ "img",
+ "embed",
+ "param",
+ "area",
+ "col",
+ "input",
+ "source",
+ "track"
+])
+
+cdataElements = frozenset(['title', 'textarea'])
+
+rcdataElements = frozenset([
+ 'style',
+ 'script',
+ 'xmp',
+ 'iframe',
+ 'noembed',
+ 'noframes',
+ 'noscript'
+])
+
+booleanAttributes = {
+ "": frozenset(["irrelevant", "itemscope"]),
+ "style": frozenset(["scoped"]),
+ "img": frozenset(["ismap"]),
+ "audio": frozenset(["autoplay", "controls"]),
+ "video": frozenset(["autoplay", "controls"]),
+ "script": frozenset(["defer", "async"]),
+ "details": frozenset(["open"]),
+ "datagrid": frozenset(["multiple", "disabled"]),
+ "command": frozenset(["hidden", "disabled", "checked", "default"]),
+ "hr": frozenset(["noshade"]),
+ "menu": frozenset(["autosubmit"]),
+ "fieldset": frozenset(["disabled", "readonly"]),
+ "option": frozenset(["disabled", "readonly", "selected"]),
+ "optgroup": frozenset(["disabled", "readonly"]),
+ "button": frozenset(["disabled", "autofocus"]),
+ "input": frozenset(["disabled", "readonly", "required", "autofocus", "checked", "ismap"]),
+ "select": frozenset(["disabled", "readonly", "autofocus", "multiple"]),
+ "output": frozenset(["disabled", "readonly"]),
+ "iframe": frozenset(["seamless"]),
+}
+
+# entitiesWindows1252 has to be _ordered_ and needs to have an index. It
+# therefore can't be a frozenset.
+entitiesWindows1252 = (
+ 8364, # 0x80 0x20AC EURO SIGN
+ 65533, # 0x81 UNDEFINED
+ 8218, # 0x82 0x201A SINGLE LOW-9 QUOTATION MARK
+ 402, # 0x83 0x0192 LATIN SMALL LETTER F WITH HOOK
+ 8222, # 0x84 0x201E DOUBLE LOW-9 QUOTATION MARK
+ 8230, # 0x85 0x2026 HORIZONTAL ELLIPSIS
+ 8224, # 0x86 0x2020 DAGGER
+ 8225, # 0x87 0x2021 DOUBLE DAGGER
+ 710, # 0x88 0x02C6 MODIFIER LETTER CIRCUMFLEX ACCENT
+ 8240, # 0x89 0x2030 PER MILLE SIGN
+ 352, # 0x8A 0x0160 LATIN CAPITAL LETTER S WITH CARON
+ 8249, # 0x8B 0x2039 SINGLE LEFT-POINTING ANGLE QUOTATION MARK
+ 338, # 0x8C 0x0152 LATIN CAPITAL LIGATURE OE
+ 65533, # 0x8D UNDEFINED
+ 381, # 0x8E 0x017D LATIN CAPITAL LETTER Z WITH CARON
+ 65533, # 0x8F UNDEFINED
+ 65533, # 0x90 UNDEFINED
+ 8216, # 0x91 0x2018 LEFT SINGLE QUOTATION MARK
+ 8217, # 0x92 0x2019 RIGHT SINGLE QUOTATION MARK
+ 8220, # 0x93 0x201C LEFT DOUBLE QUOTATION MARK
+ 8221, # 0x94 0x201D RIGHT DOUBLE QUOTATION MARK
+ 8226, # 0x95 0x2022 BULLET
+ 8211, # 0x96 0x2013 EN DASH
+ 8212, # 0x97 0x2014 EM DASH
+ 732, # 0x98 0x02DC SMALL TILDE
+ 8482, # 0x99 0x2122 TRADE MARK SIGN
+ 353, # 0x9A 0x0161 LATIN SMALL LETTER S WITH CARON
+ 8250, # 0x9B 0x203A SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
+ 339, # 0x9C 0x0153 LATIN SMALL LIGATURE OE
+ 65533, # 0x9D UNDEFINED
+ 382, # 0x9E 0x017E LATIN SMALL LETTER Z WITH CARON
+ 376 # 0x9F 0x0178 LATIN CAPITAL LETTER Y WITH DIAERESIS
+)
+
+xmlEntities = frozenset(['lt;', 'gt;', 'amp;', 'apos;', 'quot;'])
+
+entities = {
+ "AElig": "\xc6",
+ "AElig;": "\xc6",
+ "AMP": "&",
+ "AMP;": "&",
+ "Aacute": "\xc1",
+ "Aacute;": "\xc1",
+ "Abreve;": "\u0102",
+ "Acirc": "\xc2",
+ "Acirc;": "\xc2",
+ "Acy;": "\u0410",
+ "Afr;": "\U0001d504",
+ "Agrave": "\xc0",
+ "Agrave;": "\xc0",
+ "Alpha;": "\u0391",
+ "Amacr;": "\u0100",
+ "And;": "\u2a53",
+ "Aogon;": "\u0104",
+ "Aopf;": "\U0001d538",
+ "ApplyFunction;": "\u2061",
+ "Aring": "\xc5",
+ "Aring;": "\xc5",
+ "Ascr;": "\U0001d49c",
+ "Assign;": "\u2254",
+ "Atilde": "\xc3",
+ "Atilde;": "\xc3",
+ "Auml": "\xc4",
+ "Auml;": "\xc4",
+ "Backslash;": "\u2216",
+ "Barv;": "\u2ae7",
+ "Barwed;": "\u2306",
+ "Bcy;": "\u0411",
+ "Because;": "\u2235",
+ "Bernoullis;": "\u212c",
+ "Beta;": "\u0392",
+ "Bfr;": "\U0001d505",
+ "Bopf;": "\U0001d539",
+ "Breve;": "\u02d8",
+ "Bscr;": "\u212c",
+ "Bumpeq;": "\u224e",
+ "CHcy;": "\u0427",
+ "COPY": "\xa9",
+ "COPY;": "\xa9",
+ "Cacute;": "\u0106",
+ "Cap;": "\u22d2",
+ "CapitalDifferentialD;": "\u2145",
+ "Cayleys;": "\u212d",
+ "Ccaron;": "\u010c",
+ "Ccedil": "\xc7",
+ "Ccedil;": "\xc7",
+ "Ccirc;": "\u0108",
+ "Cconint;": "\u2230",
+ "Cdot;": "\u010a",
+ "Cedilla;": "\xb8",
+ "CenterDot;": "\xb7",
+ "Cfr;": "\u212d",
+ "Chi;": "\u03a7",
+ "CircleDot;": "\u2299",
+ "CircleMinus;": "\u2296",
+ "CirclePlus;": "\u2295",
+ "CircleTimes;": "\u2297",
+ "ClockwiseContourIntegral;": "\u2232",
+ "CloseCurlyDoubleQuote;": "\u201d",
+ "CloseCurlyQuote;": "\u2019",
+ "Colon;": "\u2237",
+ "Colone;": "\u2a74",
+ "Congruent;": "\u2261",
+ "Conint;": "\u222f",
+ "ContourIntegral;": "\u222e",
+ "Copf;": "\u2102",
+ "Coproduct;": "\u2210",
+ "CounterClockwiseContourIntegral;": "\u2233",
+ "Cross;": "\u2a2f",
+ "Cscr;": "\U0001d49e",
+ "Cup;": "\u22d3",
+ "CupCap;": "\u224d",
+ "DD;": "\u2145",
+ "DDotrahd;": "\u2911",
+ "DJcy;": "\u0402",
+ "DScy;": "\u0405",
+ "DZcy;": "\u040f",
+ "Dagger;": "\u2021",
+ "Darr;": "\u21a1",
+ "Dashv;": "\u2ae4",
+ "Dcaron;": "\u010e",
+ "Dcy;": "\u0414",
+ "Del;": "\u2207",
+ "Delta;": "\u0394",
+ "Dfr;": "\U0001d507",
+ "DiacriticalAcute;": "\xb4",
+ "DiacriticalDot;": "\u02d9",
+ "DiacriticalDoubleAcute;": "\u02dd",
+ "DiacriticalGrave;": "`",
+ "DiacriticalTilde;": "\u02dc",
+ "Diamond;": "\u22c4",
+ "DifferentialD;": "\u2146",
+ "Dopf;": "\U0001d53b",
+ "Dot;": "\xa8",
+ "DotDot;": "\u20dc",
+ "DotEqual;": "\u2250",
+ "DoubleContourIntegral;": "\u222f",
+ "DoubleDot;": "\xa8",
+ "DoubleDownArrow;": "\u21d3",
+ "DoubleLeftArrow;": "\u21d0",
+ "DoubleLeftRightArrow;": "\u21d4",
+ "DoubleLeftTee;": "\u2ae4",
+ "DoubleLongLeftArrow;": "\u27f8",
+ "DoubleLongLeftRightArrow;": "\u27fa",
+ "DoubleLongRightArrow;": "\u27f9",
+ "DoubleRightArrow;": "\u21d2",
+ "DoubleRightTee;": "\u22a8",
+ "DoubleUpArrow;": "\u21d1",
+ "DoubleUpDownArrow;": "\u21d5",
+ "DoubleVerticalBar;": "\u2225",
+ "DownArrow;": "\u2193",
+ "DownArrowBar;": "\u2913",
+ "DownArrowUpArrow;": "\u21f5",
+ "DownBreve;": "\u0311",
+ "DownLeftRightVector;": "\u2950",
+ "DownLeftTeeVector;": "\u295e",
+ "DownLeftVector;": "\u21bd",
+ "DownLeftVectorBar;": "\u2956",
+ "DownRightTeeVector;": "\u295f",
+ "DownRightVector;": "\u21c1",
+ "DownRightVectorBar;": "\u2957",
+ "DownTee;": "\u22a4",
+ "DownTeeArrow;": "\u21a7",
+ "Downarrow;": "\u21d3",
+ "Dscr;": "\U0001d49f",
+ "Dstrok;": "\u0110",
+ "ENG;": "\u014a",
+ "ETH": "\xd0",
+ "ETH;": "\xd0",
+ "Eacute": "\xc9",
+ "Eacute;": "\xc9",
+ "Ecaron;": "\u011a",
+ "Ecirc": "\xca",
+ "Ecirc;": "\xca",
+ "Ecy;": "\u042d",
+ "Edot;": "\u0116",
+ "Efr;": "\U0001d508",
+ "Egrave": "\xc8",
+ "Egrave;": "\xc8",
+ "Element;": "\u2208",
+ "Emacr;": "\u0112",
+ "EmptySmallSquare;": "\u25fb",
+ "EmptyVerySmallSquare;": "\u25ab",
+ "Eogon;": "\u0118",
+ "Eopf;": "\U0001d53c",
+ "Epsilon;": "\u0395",
+ "Equal;": "\u2a75",
+ "EqualTilde;": "\u2242",
+ "Equilibrium;": "\u21cc",
+ "Escr;": "\u2130",
+ "Esim;": "\u2a73",
+ "Eta;": "\u0397",
+ "Euml": "\xcb",
+ "Euml;": "\xcb",
+ "Exists;": "\u2203",
+ "ExponentialE;": "\u2147",
+ "Fcy;": "\u0424",
+ "Ffr;": "\U0001d509",
+ "FilledSmallSquare;": "\u25fc",
+ "FilledVerySmallSquare;": "\u25aa",
+ "Fopf;": "\U0001d53d",
+ "ForAll;": "\u2200",
+ "Fouriertrf;": "\u2131",
+ "Fscr;": "\u2131",
+ "GJcy;": "\u0403",
+ "GT": ">",
+ "GT;": ">",
+ "Gamma;": "\u0393",
+ "Gammad;": "\u03dc",
+ "Gbreve;": "\u011e",
+ "Gcedil;": "\u0122",
+ "Gcirc;": "\u011c",
+ "Gcy;": "\u0413",
+ "Gdot;": "\u0120",
+ "Gfr;": "\U0001d50a",
+ "Gg;": "\u22d9",
+ "Gopf;": "\U0001d53e",
+ "GreaterEqual;": "\u2265",
+ "GreaterEqualLess;": "\u22db",
+ "GreaterFullEqual;": "\u2267",
+ "GreaterGreater;": "\u2aa2",
+ "GreaterLess;": "\u2277",
+ "GreaterSlantEqual;": "\u2a7e",
+ "GreaterTilde;": "\u2273",
+ "Gscr;": "\U0001d4a2",
+ "Gt;": "\u226b",
+ "HARDcy;": "\u042a",
+ "Hacek;": "\u02c7",
+ "Hat;": "^",
+ "Hcirc;": "\u0124",
+ "Hfr;": "\u210c",
+ "HilbertSpace;": "\u210b",
+ "Hopf;": "\u210d",
+ "HorizontalLine;": "\u2500",
+ "Hscr;": "\u210b",
+ "Hstrok;": "\u0126",
+ "HumpDownHump;": "\u224e",
+ "HumpEqual;": "\u224f",
+ "IEcy;": "\u0415",
+ "IJlig;": "\u0132",
+ "IOcy;": "\u0401",
+ "Iacute": "\xcd",
+ "Iacute;": "\xcd",
+ "Icirc": "\xce",
+ "Icirc;": "\xce",
+ "Icy;": "\u0418",
+ "Idot;": "\u0130",
+ "Ifr;": "\u2111",
+ "Igrave": "\xcc",
+ "Igrave;": "\xcc",
+ "Im;": "\u2111",
+ "Imacr;": "\u012a",
+ "ImaginaryI;": "\u2148",
+ "Implies;": "\u21d2",
+ "Int;": "\u222c",
+ "Integral;": "\u222b",
+ "Intersection;": "\u22c2",
+ "InvisibleComma;": "\u2063",
+ "InvisibleTimes;": "\u2062",
+ "Iogon;": "\u012e",
+ "Iopf;": "\U0001d540",
+ "Iota;": "\u0399",
+ "Iscr;": "\u2110",
+ "Itilde;": "\u0128",
+ "Iukcy;": "\u0406",
+ "Iuml": "\xcf",
+ "Iuml;": "\xcf",
+ "Jcirc;": "\u0134",
+ "Jcy;": "\u0419",
+ "Jfr;": "\U0001d50d",
+ "Jopf;": "\U0001d541",
+ "Jscr;": "\U0001d4a5",
+ "Jsercy;": "\u0408",
+ "Jukcy;": "\u0404",
+ "KHcy;": "\u0425",
+ "KJcy;": "\u040c",
+ "Kappa;": "\u039a",
+ "Kcedil;": "\u0136",
+ "Kcy;": "\u041a",
+ "Kfr;": "\U0001d50e",
+ "Kopf;": "\U0001d542",
+ "Kscr;": "\U0001d4a6",
+ "LJcy;": "\u0409",
+ "LT": "<",
+ "LT;": "<",
+ "Lacute;": "\u0139",
+ "Lambda;": "\u039b",
+ "Lang;": "\u27ea",
+ "Laplacetrf;": "\u2112",
+ "Larr;": "\u219e",
+ "Lcaron;": "\u013d",
+ "Lcedil;": "\u013b",
+ "Lcy;": "\u041b",
+ "LeftAngleBracket;": "\u27e8",
+ "LeftArrow;": "\u2190",
+ "LeftArrowBar;": "\u21e4",
+ "LeftArrowRightArrow;": "\u21c6",
+ "LeftCeiling;": "\u2308",
+ "LeftDoubleBracket;": "\u27e6",
+ "LeftDownTeeVector;": "\u2961",
+ "LeftDownVector;": "\u21c3",
+ "LeftDownVectorBar;": "\u2959",
+ "LeftFloor;": "\u230a",
+ "LeftRightArrow;": "\u2194",
+ "LeftRightVector;": "\u294e",
+ "LeftTee;": "\u22a3",
+ "LeftTeeArrow;": "\u21a4",
+ "LeftTeeVector;": "\u295a",
+ "LeftTriangle;": "\u22b2",
+ "LeftTriangleBar;": "\u29cf",
+ "LeftTriangleEqual;": "\u22b4",
+ "LeftUpDownVector;": "\u2951",
+ "LeftUpTeeVector;": "\u2960",
+ "LeftUpVector;": "\u21bf",
+ "LeftUpVectorBar;": "\u2958",
+ "LeftVector;": "\u21bc",
+ "LeftVectorBar;": "\u2952",
+ "Leftarrow;": "\u21d0",
+ "Leftrightarrow;": "\u21d4",
+ "LessEqualGreater;": "\u22da",
+ "LessFullEqual;": "\u2266",
+ "LessGreater;": "\u2276",
+ "LessLess;": "\u2aa1",
+ "LessSlantEqual;": "\u2a7d",
+ "LessTilde;": "\u2272",
+ "Lfr;": "\U0001d50f",
+ "Ll;": "\u22d8",
+ "Lleftarrow;": "\u21da",
+ "Lmidot;": "\u013f",
+ "LongLeftArrow;": "\u27f5",
+ "LongLeftRightArrow;": "\u27f7",
+ "LongRightArrow;": "\u27f6",
+ "Longleftarrow;": "\u27f8",
+ "Longleftrightarrow;": "\u27fa",
+ "Longrightarrow;": "\u27f9",
+ "Lopf;": "\U0001d543",
+ "LowerLeftArrow;": "\u2199",
+ "LowerRightArrow;": "\u2198",
+ "Lscr;": "\u2112",
+ "Lsh;": "\u21b0",
+ "Lstrok;": "\u0141",
+ "Lt;": "\u226a",
+ "Map;": "\u2905",
+ "Mcy;": "\u041c",
+ "MediumSpace;": "\u205f",
+ "Mellintrf;": "\u2133",
+ "Mfr;": "\U0001d510",
+ "MinusPlus;": "\u2213",
+ "Mopf;": "\U0001d544",
+ "Mscr;": "\u2133",
+ "Mu;": "\u039c",
+ "NJcy;": "\u040a",
+ "Nacute;": "\u0143",
+ "Ncaron;": "\u0147",
+ "Ncedil;": "\u0145",
+ "Ncy;": "\u041d",
+ "NegativeMediumSpace;": "\u200b",
+ "NegativeThickSpace;": "\u200b",
+ "NegativeThinSpace;": "\u200b",
+ "NegativeVeryThinSpace;": "\u200b",
+ "NestedGreaterGreater;": "\u226b",
+ "NestedLessLess;": "\u226a",
+ "NewLine;": "\n",
+ "Nfr;": "\U0001d511",
+ "NoBreak;": "\u2060",
+ "NonBreakingSpace;": "\xa0",
+ "Nopf;": "\u2115",
+ "Not;": "\u2aec",
+ "NotCongruent;": "\u2262",
+ "NotCupCap;": "\u226d",
+ "NotDoubleVerticalBar;": "\u2226",
+ "NotElement;": "\u2209",
+ "NotEqual;": "\u2260",
+ "NotEqualTilde;": "\u2242\u0338",
+ "NotExists;": "\u2204",
+ "NotGreater;": "\u226f",
+ "NotGreaterEqual;": "\u2271",
+ "NotGreaterFullEqual;": "\u2267\u0338",
+ "NotGreaterGreater;": "\u226b\u0338",
+ "NotGreaterLess;": "\u2279",
+ "NotGreaterSlantEqual;": "\u2a7e\u0338",
+ "NotGreaterTilde;": "\u2275",
+ "NotHumpDownHump;": "\u224e\u0338",
+ "NotHumpEqual;": "\u224f\u0338",
+ "NotLeftTriangle;": "\u22ea",
+ "NotLeftTriangleBar;": "\u29cf\u0338",
+ "NotLeftTriangleEqual;": "\u22ec",
+ "NotLess;": "\u226e",
+ "NotLessEqual;": "\u2270",
+ "NotLessGreater;": "\u2278",
+ "NotLessLess;": "\u226a\u0338",
+ "NotLessSlantEqual;": "\u2a7d\u0338",
+ "NotLessTilde;": "\u2274",
+ "NotNestedGreaterGreater;": "\u2aa2\u0338",
+ "NotNestedLessLess;": "\u2aa1\u0338",
+ "NotPrecedes;": "\u2280",
+ "NotPrecedesEqual;": "\u2aaf\u0338",
+ "NotPrecedesSlantEqual;": "\u22e0",
+ "NotReverseElement;": "\u220c",
+ "NotRightTriangle;": "\u22eb",
+ "NotRightTriangleBar;": "\u29d0\u0338",
+ "NotRightTriangleEqual;": "\u22ed",
+ "NotSquareSubset;": "\u228f\u0338",
+ "NotSquareSubsetEqual;": "\u22e2",
+ "NotSquareSuperset;": "\u2290\u0338",
+ "NotSquareSupersetEqual;": "\u22e3",
+ "NotSubset;": "\u2282\u20d2",
+ "NotSubsetEqual;": "\u2288",
+ "NotSucceeds;": "\u2281",
+ "NotSucceedsEqual;": "\u2ab0\u0338",
+ "NotSucceedsSlantEqual;": "\u22e1",
+ "NotSucceedsTilde;": "\u227f\u0338",
+ "NotSuperset;": "\u2283\u20d2",
+ "NotSupersetEqual;": "\u2289",
+ "NotTilde;": "\u2241",
+ "NotTildeEqual;": "\u2244",
+ "NotTildeFullEqual;": "\u2247",
+ "NotTildeTilde;": "\u2249",
+ "NotVerticalBar;": "\u2224",
+ "Nscr;": "\U0001d4a9",
+ "Ntilde": "\xd1",
+ "Ntilde;": "\xd1",
+ "Nu;": "\u039d",
+ "OElig;": "\u0152",
+ "Oacute": "\xd3",
+ "Oacute;": "\xd3",
+ "Ocirc": "\xd4",
+ "Ocirc;": "\xd4",
+ "Ocy;": "\u041e",
+ "Odblac;": "\u0150",
+ "Ofr;": "\U0001d512",
+ "Ograve": "\xd2",
+ "Ograve;": "\xd2",
+ "Omacr;": "\u014c",
+ "Omega;": "\u03a9",
+ "Omicron;": "\u039f",
+ "Oopf;": "\U0001d546",
+ "OpenCurlyDoubleQuote;": "\u201c",
+ "OpenCurlyQuote;": "\u2018",
+ "Or;": "\u2a54",
+ "Oscr;": "\U0001d4aa",
+ "Oslash": "\xd8",
+ "Oslash;": "\xd8",
+ "Otilde": "\xd5",
+ "Otilde;": "\xd5",
+ "Otimes;": "\u2a37",
+ "Ouml": "\xd6",
+ "Ouml;": "\xd6",
+ "OverBar;": "\u203e",
+ "OverBrace;": "\u23de",
+ "OverBracket;": "\u23b4",
+ "OverParenthesis;": "\u23dc",
+ "PartialD;": "\u2202",
+ "Pcy;": "\u041f",
+ "Pfr;": "\U0001d513",
+ "Phi;": "\u03a6",
+ "Pi;": "\u03a0",
+ "PlusMinus;": "\xb1",
+ "Poincareplane;": "\u210c",
+ "Popf;": "\u2119",
+ "Pr;": "\u2abb",
+ "Precedes;": "\u227a",
+ "PrecedesEqual;": "\u2aaf",
+ "PrecedesSlantEqual;": "\u227c",
+ "PrecedesTilde;": "\u227e",
+ "Prime;": "\u2033",
+ "Product;": "\u220f",
+ "Proportion;": "\u2237",
+ "Proportional;": "\u221d",
+ "Pscr;": "\U0001d4ab",
+ "Psi;": "\u03a8",
+ "QUOT": "\"",
+ "QUOT;": "\"",
+ "Qfr;": "\U0001d514",
+ "Qopf;": "\u211a",
+ "Qscr;": "\U0001d4ac",
+ "RBarr;": "\u2910",
+ "REG": "\xae",
+ "REG;": "\xae",
+ "Racute;": "\u0154",
+ "Rang;": "\u27eb",
+ "Rarr;": "\u21a0",
+ "Rarrtl;": "\u2916",
+ "Rcaron;": "\u0158",
+ "Rcedil;": "\u0156",
+ "Rcy;": "\u0420",
+ "Re;": "\u211c",
+ "ReverseElement;": "\u220b",
+ "ReverseEquilibrium;": "\u21cb",
+ "ReverseUpEquilibrium;": "\u296f",
+ "Rfr;": "\u211c",
+ "Rho;": "\u03a1",
+ "RightAngleBracket;": "\u27e9",
+ "RightArrow;": "\u2192",
+ "RightArrowBar;": "\u21e5",
+ "RightArrowLeftArrow;": "\u21c4",
+ "RightCeiling;": "\u2309",
+ "RightDoubleBracket;": "\u27e7",
+ "RightDownTeeVector;": "\u295d",
+ "RightDownVector;": "\u21c2",
+ "RightDownVectorBar;": "\u2955",
+ "RightFloor;": "\u230b",
+ "RightTee;": "\u22a2",
+ "RightTeeArrow;": "\u21a6",
+ "RightTeeVector;": "\u295b",
+ "RightTriangle;": "\u22b3",
+ "RightTriangleBar;": "\u29d0",
+ "RightTriangleEqual;": "\u22b5",
+ "RightUpDownVector;": "\u294f",
+ "RightUpTeeVector;": "\u295c",
+ "RightUpVector;": "\u21be",
+ "RightUpVectorBar;": "\u2954",
+ "RightVector;": "\u21c0",
+ "RightVectorBar;": "\u2953",
+ "Rightarrow;": "\u21d2",
+ "Ropf;": "\u211d",
+ "RoundImplies;": "\u2970",
+ "Rrightarrow;": "\u21db",
+ "Rscr;": "\u211b",
+ "Rsh;": "\u21b1",
+ "RuleDelayed;": "\u29f4",
+ "SHCHcy;": "\u0429",
+ "SHcy;": "\u0428",
+ "SOFTcy;": "\u042c",
+ "Sacute;": "\u015a",
+ "Sc;": "\u2abc",
+ "Scaron;": "\u0160",
+ "Scedil;": "\u015e",
+ "Scirc;": "\u015c",
+ "Scy;": "\u0421",
+ "Sfr;": "\U0001d516",
+ "ShortDownArrow;": "\u2193",
+ "ShortLeftArrow;": "\u2190",
+ "ShortRightArrow;": "\u2192",
+ "ShortUpArrow;": "\u2191",
+ "Sigma;": "\u03a3",
+ "SmallCircle;": "\u2218",
+ "Sopf;": "\U0001d54a",
+ "Sqrt;": "\u221a",
+ "Square;": "\u25a1",
+ "SquareIntersection;": "\u2293",
+ "SquareSubset;": "\u228f",
+ "SquareSubsetEqual;": "\u2291",
+ "SquareSuperset;": "\u2290",
+ "SquareSupersetEqual;": "\u2292",
+ "SquareUnion;": "\u2294",
+ "Sscr;": "\U0001d4ae",
+ "Star;": "\u22c6",
+ "Sub;": "\u22d0",
+ "Subset;": "\u22d0",
+ "SubsetEqual;": "\u2286",
+ "Succeeds;": "\u227b",
+ "SucceedsEqual;": "\u2ab0",
+ "SucceedsSlantEqual;": "\u227d",
+ "SucceedsTilde;": "\u227f",
+ "SuchThat;": "\u220b",
+ "Sum;": "\u2211",
+ "Sup;": "\u22d1",
+ "Superset;": "\u2283",
+ "SupersetEqual;": "\u2287",
+ "Supset;": "\u22d1",
+ "THORN": "\xde",
+ "THORN;": "\xde",
+ "TRADE;": "\u2122",
+ "TSHcy;": "\u040b",
+ "TScy;": "\u0426",
+ "Tab;": "\t",
+ "Tau;": "\u03a4",
+ "Tcaron;": "\u0164",
+ "Tcedil;": "\u0162",
+ "Tcy;": "\u0422",
+ "Tfr;": "\U0001d517",
+ "Therefore;": "\u2234",
+ "Theta;": "\u0398",
+ "ThickSpace;": "\u205f\u200a",
+ "ThinSpace;": "\u2009",
+ "Tilde;": "\u223c",
+ "TildeEqual;": "\u2243",
+ "TildeFullEqual;": "\u2245",
+ "TildeTilde;": "\u2248",
+ "Topf;": "\U0001d54b",
+ "TripleDot;": "\u20db",
+ "Tscr;": "\U0001d4af",
+ "Tstrok;": "\u0166",
+ "Uacute": "\xda",
+ "Uacute;": "\xda",
+ "Uarr;": "\u219f",
+ "Uarrocir;": "\u2949",
+ "Ubrcy;": "\u040e",
+ "Ubreve;": "\u016c",
+ "Ucirc": "\xdb",
+ "Ucirc;": "\xdb",
+ "Ucy;": "\u0423",
+ "Udblac;": "\u0170",
+ "Ufr;": "\U0001d518",
+ "Ugrave": "\xd9",
+ "Ugrave;": "\xd9",
+ "Umacr;": "\u016a",
+ "UnderBar;": "_",
+ "UnderBrace;": "\u23df",
+ "UnderBracket;": "\u23b5",
+ "UnderParenthesis;": "\u23dd",
+ "Union;": "\u22c3",
+ "UnionPlus;": "\u228e",
+ "Uogon;": "\u0172",
+ "Uopf;": "\U0001d54c",
+ "UpArrow;": "\u2191",
+ "UpArrowBar;": "\u2912",
+ "UpArrowDownArrow;": "\u21c5",
+ "UpDownArrow;": "\u2195",
+ "UpEquilibrium;": "\u296e",
+ "UpTee;": "\u22a5",
+ "UpTeeArrow;": "\u21a5",
+ "Uparrow;": "\u21d1",
+ "Updownarrow;": "\u21d5",
+ "UpperLeftArrow;": "\u2196",
+ "UpperRightArrow;": "\u2197",
+ "Upsi;": "\u03d2",
+ "Upsilon;": "\u03a5",
+ "Uring;": "\u016e",
+ "Uscr;": "\U0001d4b0",
+ "Utilde;": "\u0168",
+ "Uuml": "\xdc",
+ "Uuml;": "\xdc",
+ "VDash;": "\u22ab",
+ "Vbar;": "\u2aeb",
+ "Vcy;": "\u0412",
+ "Vdash;": "\u22a9",
+ "Vdashl;": "\u2ae6",
+ "Vee;": "\u22c1",
+ "Verbar;": "\u2016",
+ "Vert;": "\u2016",
+ "VerticalBar;": "\u2223",
+ "VerticalLine;": "|",
+ "VerticalSeparator;": "\u2758",
+ "VerticalTilde;": "\u2240",
+ "VeryThinSpace;": "\u200a",
+ "Vfr;": "\U0001d519",
+ "Vopf;": "\U0001d54d",
+ "Vscr;": "\U0001d4b1",
+ "Vvdash;": "\u22aa",
+ "Wcirc;": "\u0174",
+ "Wedge;": "\u22c0",
+ "Wfr;": "\U0001d51a",
+ "Wopf;": "\U0001d54e",
+ "Wscr;": "\U0001d4b2",
+ "Xfr;": "\U0001d51b",
+ "Xi;": "\u039e",
+ "Xopf;": "\U0001d54f",
+ "Xscr;": "\U0001d4b3",
+ "YAcy;": "\u042f",
+ "YIcy;": "\u0407",
+ "YUcy;": "\u042e",
+ "Yacute": "\xdd",
+ "Yacute;": "\xdd",
+ "Ycirc;": "\u0176",
+ "Ycy;": "\u042b",
+ "Yfr;": "\U0001d51c",
+ "Yopf;": "\U0001d550",
+ "Yscr;": "\U0001d4b4",
+ "Yuml;": "\u0178",
+ "ZHcy;": "\u0416",
+ "Zacute;": "\u0179",
+ "Zcaron;": "\u017d",
+ "Zcy;": "\u0417",
+ "Zdot;": "\u017b",
+ "ZeroWidthSpace;": "\u200b",
+ "Zeta;": "\u0396",
+ "Zfr;": "\u2128",
+ "Zopf;": "\u2124",
+ "Zscr;": "\U0001d4b5",
+ "aacute": "\xe1",
+ "aacute;": "\xe1",
+ "abreve;": "\u0103",
+ "ac;": "\u223e",
+ "acE;": "\u223e\u0333",
+ "acd;": "\u223f",
+ "acirc": "\xe2",
+ "acirc;": "\xe2",
+ "acute": "\xb4",
+ "acute;": "\xb4",
+ "acy;": "\u0430",
+ "aelig": "\xe6",
+ "aelig;": "\xe6",
+ "af;": "\u2061",
+ "afr;": "\U0001d51e",
+ "agrave": "\xe0",
+ "agrave;": "\xe0",
+ "alefsym;": "\u2135",
+ "aleph;": "\u2135",
+ "alpha;": "\u03b1",
+ "amacr;": "\u0101",
+ "amalg;": "\u2a3f",
+ "amp": "&",
+ "amp;": "&",
+ "and;": "\u2227",
+ "andand;": "\u2a55",
+ "andd;": "\u2a5c",
+ "andslope;": "\u2a58",
+ "andv;": "\u2a5a",
+ "ang;": "\u2220",
+ "ange;": "\u29a4",
+ "angle;": "\u2220",
+ "angmsd;": "\u2221",
+ "angmsdaa;": "\u29a8",
+ "angmsdab;": "\u29a9",
+ "angmsdac;": "\u29aa",
+ "angmsdad;": "\u29ab",
+ "angmsdae;": "\u29ac",
+ "angmsdaf;": "\u29ad",
+ "angmsdag;": "\u29ae",
+ "angmsdah;": "\u29af",
+ "angrt;": "\u221f",
+ "angrtvb;": "\u22be",
+ "angrtvbd;": "\u299d",
+ "angsph;": "\u2222",
+ "angst;": "\xc5",
+ "angzarr;": "\u237c",
+ "aogon;": "\u0105",
+ "aopf;": "\U0001d552",
+ "ap;": "\u2248",
+ "apE;": "\u2a70",
+ "apacir;": "\u2a6f",
+ "ape;": "\u224a",
+ "apid;": "\u224b",
+ "apos;": "'",
+ "approx;": "\u2248",
+ "approxeq;": "\u224a",
+ "aring": "\xe5",
+ "aring;": "\xe5",
+ "ascr;": "\U0001d4b6",
+ "ast;": "*",
+ "asymp;": "\u2248",
+ "asympeq;": "\u224d",
+ "atilde": "\xe3",
+ "atilde;": "\xe3",
+ "auml": "\xe4",
+ "auml;": "\xe4",
+ "awconint;": "\u2233",
+ "awint;": "\u2a11",
+ "bNot;": "\u2aed",
+ "backcong;": "\u224c",
+ "backepsilon;": "\u03f6",
+ "backprime;": "\u2035",
+ "backsim;": "\u223d",
+ "backsimeq;": "\u22cd",
+ "barvee;": "\u22bd",
+ "barwed;": "\u2305",
+ "barwedge;": "\u2305",
+ "bbrk;": "\u23b5",
+ "bbrktbrk;": "\u23b6",
+ "bcong;": "\u224c",
+ "bcy;": "\u0431",
+ "bdquo;": "\u201e",
+ "becaus;": "\u2235",
+ "because;": "\u2235",
+ "bemptyv;": "\u29b0",
+ "bepsi;": "\u03f6",
+ "bernou;": "\u212c",
+ "beta;": "\u03b2",
+ "beth;": "\u2136",
+ "between;": "\u226c",
+ "bfr;": "\U0001d51f",
+ "bigcap;": "\u22c2",
+ "bigcirc;": "\u25ef",
+ "bigcup;": "\u22c3",
+ "bigodot;": "\u2a00",
+ "bigoplus;": "\u2a01",
+ "bigotimes;": "\u2a02",
+ "bigsqcup;": "\u2a06",
+ "bigstar;": "\u2605",
+ "bigtriangledown;": "\u25bd",
+ "bigtriangleup;": "\u25b3",
+ "biguplus;": "\u2a04",
+ "bigvee;": "\u22c1",
+ "bigwedge;": "\u22c0",
+ "bkarow;": "\u290d",
+ "blacklozenge;": "\u29eb",
+ "blacksquare;": "\u25aa",
+ "blacktriangle;": "\u25b4",
+ "blacktriangledown;": "\u25be",
+ "blacktriangleleft;": "\u25c2",
+ "blacktriangleright;": "\u25b8",
+ "blank;": "\u2423",
+ "blk12;": "\u2592",
+ "blk14;": "\u2591",
+ "blk34;": "\u2593",
+ "block;": "\u2588",
+ "bne;": "=\u20e5",
+ "bnequiv;": "\u2261\u20e5",
+ "bnot;": "\u2310",
+ "bopf;": "\U0001d553",
+ "bot;": "\u22a5",
+ "bottom;": "\u22a5",
+ "bowtie;": "\u22c8",
+ "boxDL;": "\u2557",
+ "boxDR;": "\u2554",
+ "boxDl;": "\u2556",
+ "boxDr;": "\u2553",
+ "boxH;": "\u2550",
+ "boxHD;": "\u2566",
+ "boxHU;": "\u2569",
+ "boxHd;": "\u2564",
+ "boxHu;": "\u2567",
+ "boxUL;": "\u255d",
+ "boxUR;": "\u255a",
+ "boxUl;": "\u255c",
+ "boxUr;": "\u2559",
+ "boxV;": "\u2551",
+ "boxVH;": "\u256c",
+ "boxVL;": "\u2563",
+ "boxVR;": "\u2560",
+ "boxVh;": "\u256b",
+ "boxVl;": "\u2562",
+ "boxVr;": "\u255f",
+ "boxbox;": "\u29c9",
+ "boxdL;": "\u2555",
+ "boxdR;": "\u2552",
+ "boxdl;": "\u2510",
+ "boxdr;": "\u250c",
+ "boxh;": "\u2500",
+ "boxhD;": "\u2565",
+ "boxhU;": "\u2568",
+ "boxhd;": "\u252c",
+ "boxhu;": "\u2534",
+ "boxminus;": "\u229f",
+ "boxplus;": "\u229e",
+ "boxtimes;": "\u22a0",
+ "boxuL;": "\u255b",
+ "boxuR;": "\u2558",
+ "boxul;": "\u2518",
+ "boxur;": "\u2514",
+ "boxv;": "\u2502",
+ "boxvH;": "\u256a",
+ "boxvL;": "\u2561",
+ "boxvR;": "\u255e",
+ "boxvh;": "\u253c",
+ "boxvl;": "\u2524",
+ "boxvr;": "\u251c",
+ "bprime;": "\u2035",
+ "breve;": "\u02d8",
+ "brvbar": "\xa6",
+ "brvbar;": "\xa6",
+ "bscr;": "\U0001d4b7",
+ "bsemi;": "\u204f",
+ "bsim;": "\u223d",
+ "bsime;": "\u22cd",
+ "bsol;": "\\",
+ "bsolb;": "\u29c5",
+ "bsolhsub;": "\u27c8",
+ "bull;": "\u2022",
+ "bullet;": "\u2022",
+ "bump;": "\u224e",
+ "bumpE;": "\u2aae",
+ "bumpe;": "\u224f",
+ "bumpeq;": "\u224f",
+ "cacute;": "\u0107",
+ "cap;": "\u2229",
+ "capand;": "\u2a44",
+ "capbrcup;": "\u2a49",
+ "capcap;": "\u2a4b",
+ "capcup;": "\u2a47",
+ "capdot;": "\u2a40",
+ "caps;": "\u2229\ufe00",
+ "caret;": "\u2041",
+ "caron;": "\u02c7",
+ "ccaps;": "\u2a4d",
+ "ccaron;": "\u010d",
+ "ccedil": "\xe7",
+ "ccedil;": "\xe7",
+ "ccirc;": "\u0109",
+ "ccups;": "\u2a4c",
+ "ccupssm;": "\u2a50",
+ "cdot;": "\u010b",
+ "cedil": "\xb8",
+ "cedil;": "\xb8",
+ "cemptyv;": "\u29b2",
+ "cent": "\xa2",
+ "cent;": "\xa2",
+ "centerdot;": "\xb7",
+ "cfr;": "\U0001d520",
+ "chcy;": "\u0447",
+ "check;": "\u2713",
+ "checkmark;": "\u2713",
+ "chi;": "\u03c7",
+ "cir;": "\u25cb",
+ "cirE;": "\u29c3",
+ "circ;": "\u02c6",
+ "circeq;": "\u2257",
+ "circlearrowleft;": "\u21ba",
+ "circlearrowright;": "\u21bb",
+ "circledR;": "\xae",
+ "circledS;": "\u24c8",
+ "circledast;": "\u229b",
+ "circledcirc;": "\u229a",
+ "circleddash;": "\u229d",
+ "cire;": "\u2257",
+ "cirfnint;": "\u2a10",
+ "cirmid;": "\u2aef",
+ "cirscir;": "\u29c2",
+ "clubs;": "\u2663",
+ "clubsuit;": "\u2663",
+ "colon;": ":",
+ "colone;": "\u2254",
+ "coloneq;": "\u2254",
+ "comma;": ",",
+ "commat;": "@",
+ "comp;": "\u2201",
+ "compfn;": "\u2218",
+ "complement;": "\u2201",
+ "complexes;": "\u2102",
+ "cong;": "\u2245",
+ "congdot;": "\u2a6d",
+ "conint;": "\u222e",
+ "copf;": "\U0001d554",
+ "coprod;": "\u2210",
+ "copy": "\xa9",
+ "copy;": "\xa9",
+ "copysr;": "\u2117",
+ "crarr;": "\u21b5",
+ "cross;": "\u2717",
+ "cscr;": "\U0001d4b8",
+ "csub;": "\u2acf",
+ "csube;": "\u2ad1",
+ "csup;": "\u2ad0",
+ "csupe;": "\u2ad2",
+ "ctdot;": "\u22ef",
+ "cudarrl;": "\u2938",
+ "cudarrr;": "\u2935",
+ "cuepr;": "\u22de",
+ "cuesc;": "\u22df",
+ "cularr;": "\u21b6",
+ "cularrp;": "\u293d",
+ "cup;": "\u222a",
+ "cupbrcap;": "\u2a48",
+ "cupcap;": "\u2a46",
+ "cupcup;": "\u2a4a",
+ "cupdot;": "\u228d",
+ "cupor;": "\u2a45",
+ "cups;": "\u222a\ufe00",
+ "curarr;": "\u21b7",
+ "curarrm;": "\u293c",
+ "curlyeqprec;": "\u22de",
+ "curlyeqsucc;": "\u22df",
+ "curlyvee;": "\u22ce",
+ "curlywedge;": "\u22cf",
+ "curren": "\xa4",
+ "curren;": "\xa4",
+ "curvearrowleft;": "\u21b6",
+ "curvearrowright;": "\u21b7",
+ "cuvee;": "\u22ce",
+ "cuwed;": "\u22cf",
+ "cwconint;": "\u2232",
+ "cwint;": "\u2231",
+ "cylcty;": "\u232d",
+ "dArr;": "\u21d3",
+ "dHar;": "\u2965",
+ "dagger;": "\u2020",
+ "daleth;": "\u2138",
+ "darr;": "\u2193",
+ "dash;": "\u2010",
+ "dashv;": "\u22a3",
+ "dbkarow;": "\u290f",
+ "dblac;": "\u02dd",
+ "dcaron;": "\u010f",
+ "dcy;": "\u0434",
+ "dd;": "\u2146",
+ "ddagger;": "\u2021",
+ "ddarr;": "\u21ca",
+ "ddotseq;": "\u2a77",
+ "deg": "\xb0",
+ "deg;": "\xb0",
+ "delta;": "\u03b4",
+ "demptyv;": "\u29b1",
+ "dfisht;": "\u297f",
+ "dfr;": "\U0001d521",
+ "dharl;": "\u21c3",
+ "dharr;": "\u21c2",
+ "diam;": "\u22c4",
+ "diamond;": "\u22c4",
+ "diamondsuit;": "\u2666",
+ "diams;": "\u2666",
+ "die;": "\xa8",
+ "digamma;": "\u03dd",
+ "disin;": "\u22f2",
+ "div;": "\xf7",
+ "divide": "\xf7",
+ "divide;": "\xf7",
+ "divideontimes;": "\u22c7",
+ "divonx;": "\u22c7",
+ "djcy;": "\u0452",
+ "dlcorn;": "\u231e",
+ "dlcrop;": "\u230d",
+ "dollar;": "$",
+ "dopf;": "\U0001d555",
+ "dot;": "\u02d9",
+ "doteq;": "\u2250",
+ "doteqdot;": "\u2251",
+ "dotminus;": "\u2238",
+ "dotplus;": "\u2214",
+ "dotsquare;": "\u22a1",
+ "doublebarwedge;": "\u2306",
+ "downarrow;": "\u2193",
+ "downdownarrows;": "\u21ca",
+ "downharpoonleft;": "\u21c3",
+ "downharpoonright;": "\u21c2",
+ "drbkarow;": "\u2910",
+ "drcorn;": "\u231f",
+ "drcrop;": "\u230c",
+ "dscr;": "\U0001d4b9",
+ "dscy;": "\u0455",
+ "dsol;": "\u29f6",
+ "dstrok;": "\u0111",
+ "dtdot;": "\u22f1",
+ "dtri;": "\u25bf",
+ "dtrif;": "\u25be",
+ "duarr;": "\u21f5",
+ "duhar;": "\u296f",
+ "dwangle;": "\u29a6",
+ "dzcy;": "\u045f",
+ "dzigrarr;": "\u27ff",
+ "eDDot;": "\u2a77",
+ "eDot;": "\u2251",
+ "eacute": "\xe9",
+ "eacute;": "\xe9",
+ "easter;": "\u2a6e",
+ "ecaron;": "\u011b",
+ "ecir;": "\u2256",
+ "ecirc": "\xea",
+ "ecirc;": "\xea",
+ "ecolon;": "\u2255",
+ "ecy;": "\u044d",
+ "edot;": "\u0117",
+ "ee;": "\u2147",
+ "efDot;": "\u2252",
+ "efr;": "\U0001d522",
+ "eg;": "\u2a9a",
+ "egrave": "\xe8",
+ "egrave;": "\xe8",
+ "egs;": "\u2a96",
+ "egsdot;": "\u2a98",
+ "el;": "\u2a99",
+ "elinters;": "\u23e7",
+ "ell;": "\u2113",
+ "els;": "\u2a95",
+ "elsdot;": "\u2a97",
+ "emacr;": "\u0113",
+ "empty;": "\u2205",
+ "emptyset;": "\u2205",
+ "emptyv;": "\u2205",
+ "emsp13;": "\u2004",
+ "emsp14;": "\u2005",
+ "emsp;": "\u2003",
+ "eng;": "\u014b",
+ "ensp;": "\u2002",
+ "eogon;": "\u0119",
+ "eopf;": "\U0001d556",
+ "epar;": "\u22d5",
+ "eparsl;": "\u29e3",
+ "eplus;": "\u2a71",
+ "epsi;": "\u03b5",
+ "epsilon;": "\u03b5",
+ "epsiv;": "\u03f5",
+ "eqcirc;": "\u2256",
+ "eqcolon;": "\u2255",
+ "eqsim;": "\u2242",
+ "eqslantgtr;": "\u2a96",
+ "eqslantless;": "\u2a95",
+ "equals;": "=",
+ "equest;": "\u225f",
+ "equiv;": "\u2261",
+ "equivDD;": "\u2a78",
+ "eqvparsl;": "\u29e5",
+ "erDot;": "\u2253",
+ "erarr;": "\u2971",
+ "escr;": "\u212f",
+ "esdot;": "\u2250",
+ "esim;": "\u2242",
+ "eta;": "\u03b7",
+ "eth": "\xf0",
+ "eth;": "\xf0",
+ "euml": "\xeb",
+ "euml;": "\xeb",
+ "euro;": "\u20ac",
+ "excl;": "!",
+ "exist;": "\u2203",
+ "expectation;": "\u2130",
+ "exponentiale;": "\u2147",
+ "fallingdotseq;": "\u2252",
+ "fcy;": "\u0444",
+ "female;": "\u2640",
+ "ffilig;": "\ufb03",
+ "fflig;": "\ufb00",
+ "ffllig;": "\ufb04",
+ "ffr;": "\U0001d523",
+ "filig;": "\ufb01",
+ "fjlig;": "fj",
+ "flat;": "\u266d",
+ "fllig;": "\ufb02",
+ "fltns;": "\u25b1",
+ "fnof;": "\u0192",
+ "fopf;": "\U0001d557",
+ "forall;": "\u2200",
+ "fork;": "\u22d4",
+ "forkv;": "\u2ad9",
+ "fpartint;": "\u2a0d",
+ "frac12": "\xbd",
+ "frac12;": "\xbd",
+ "frac13;": "\u2153",
+ "frac14": "\xbc",
+ "frac14;": "\xbc",
+ "frac15;": "\u2155",
+ "frac16;": "\u2159",
+ "frac18;": "\u215b",
+ "frac23;": "\u2154",
+ "frac25;": "\u2156",
+ "frac34": "\xbe",
+ "frac34;": "\xbe",
+ "frac35;": "\u2157",
+ "frac38;": "\u215c",
+ "frac45;": "\u2158",
+ "frac56;": "\u215a",
+ "frac58;": "\u215d",
+ "frac78;": "\u215e",
+ "frasl;": "\u2044",
+ "frown;": "\u2322",
+ "fscr;": "\U0001d4bb",
+ "gE;": "\u2267",
+ "gEl;": "\u2a8c",
+ "gacute;": "\u01f5",
+ "gamma;": "\u03b3",
+ "gammad;": "\u03dd",
+ "gap;": "\u2a86",
+ "gbreve;": "\u011f",
+ "gcirc;": "\u011d",
+ "gcy;": "\u0433",
+ "gdot;": "\u0121",
+ "ge;": "\u2265",
+ "gel;": "\u22db",
+ "geq;": "\u2265",
+ "geqq;": "\u2267",
+ "geqslant;": "\u2a7e",
+ "ges;": "\u2a7e",
+ "gescc;": "\u2aa9",
+ "gesdot;": "\u2a80",
+ "gesdoto;": "\u2a82",
+ "gesdotol;": "\u2a84",
+ "gesl;": "\u22db\ufe00",
+ "gesles;": "\u2a94",
+ "gfr;": "\U0001d524",
+ "gg;": "\u226b",
+ "ggg;": "\u22d9",
+ "gimel;": "\u2137",
+ "gjcy;": "\u0453",
+ "gl;": "\u2277",
+ "glE;": "\u2a92",
+ "gla;": "\u2aa5",
+ "glj;": "\u2aa4",
+ "gnE;": "\u2269",
+ "gnap;": "\u2a8a",
+ "gnapprox;": "\u2a8a",
+ "gne;": "\u2a88",
+ "gneq;": "\u2a88",
+ "gneqq;": "\u2269",
+ "gnsim;": "\u22e7",
+ "gopf;": "\U0001d558",
+ "grave;": "`",
+ "gscr;": "\u210a",
+ "gsim;": "\u2273",
+ "gsime;": "\u2a8e",
+ "gsiml;": "\u2a90",
+ "gt": ">",
+ "gt;": ">",
+ "gtcc;": "\u2aa7",
+ "gtcir;": "\u2a7a",
+ "gtdot;": "\u22d7",
+ "gtlPar;": "\u2995",
+ "gtquest;": "\u2a7c",
+ "gtrapprox;": "\u2a86",
+ "gtrarr;": "\u2978",
+ "gtrdot;": "\u22d7",
+ "gtreqless;": "\u22db",
+ "gtreqqless;": "\u2a8c",
+ "gtrless;": "\u2277",
+ "gtrsim;": "\u2273",
+ "gvertneqq;": "\u2269\ufe00",
+ "gvnE;": "\u2269\ufe00",
+ "hArr;": "\u21d4",
+ "hairsp;": "\u200a",
+ "half;": "\xbd",
+ "hamilt;": "\u210b",
+ "hardcy;": "\u044a",
+ "harr;": "\u2194",
+ "harrcir;": "\u2948",
+ "harrw;": "\u21ad",
+ "hbar;": "\u210f",
+ "hcirc;": "\u0125",
+ "hearts;": "\u2665",
+ "heartsuit;": "\u2665",
+ "hellip;": "\u2026",
+ "hercon;": "\u22b9",
+ "hfr;": "\U0001d525",
+ "hksearow;": "\u2925",
+ "hkswarow;": "\u2926",
+ "hoarr;": "\u21ff",
+ "homtht;": "\u223b",
+ "hookleftarrow;": "\u21a9",
+ "hookrightarrow;": "\u21aa",
+ "hopf;": "\U0001d559",
+ "horbar;": "\u2015",
+ "hscr;": "\U0001d4bd",
+ "hslash;": "\u210f",
+ "hstrok;": "\u0127",
+ "hybull;": "\u2043",
+ "hyphen;": "\u2010",
+ "iacute": "\xed",
+ "iacute;": "\xed",
+ "ic;": "\u2063",
+ "icirc": "\xee",
+ "icirc;": "\xee",
+ "icy;": "\u0438",
+ "iecy;": "\u0435",
+ "iexcl": "\xa1",
+ "iexcl;": "\xa1",
+ "iff;": "\u21d4",
+ "ifr;": "\U0001d526",
+ "igrave": "\xec",
+ "igrave;": "\xec",
+ "ii;": "\u2148",
+ "iiiint;": "\u2a0c",
+ "iiint;": "\u222d",
+ "iinfin;": "\u29dc",
+ "iiota;": "\u2129",
+ "ijlig;": "\u0133",
+ "imacr;": "\u012b",
+ "image;": "\u2111",
+ "imagline;": "\u2110",
+ "imagpart;": "\u2111",
+ "imath;": "\u0131",
+ "imof;": "\u22b7",
+ "imped;": "\u01b5",
+ "in;": "\u2208",
+ "incare;": "\u2105",
+ "infin;": "\u221e",
+ "infintie;": "\u29dd",
+ "inodot;": "\u0131",
+ "int;": "\u222b",
+ "intcal;": "\u22ba",
+ "integers;": "\u2124",
+ "intercal;": "\u22ba",
+ "intlarhk;": "\u2a17",
+ "intprod;": "\u2a3c",
+ "iocy;": "\u0451",
+ "iogon;": "\u012f",
+ "iopf;": "\U0001d55a",
+ "iota;": "\u03b9",
+ "iprod;": "\u2a3c",
+ "iquest": "\xbf",
+ "iquest;": "\xbf",
+ "iscr;": "\U0001d4be",
+ "isin;": "\u2208",
+ "isinE;": "\u22f9",
+ "isindot;": "\u22f5",
+ "isins;": "\u22f4",
+ "isinsv;": "\u22f3",
+ "isinv;": "\u2208",
+ "it;": "\u2062",
+ "itilde;": "\u0129",
+ "iukcy;": "\u0456",
+ "iuml": "\xef",
+ "iuml;": "\xef",
+ "jcirc;": "\u0135",
+ "jcy;": "\u0439",
+ "jfr;": "\U0001d527",
+ "jmath;": "\u0237",
+ "jopf;": "\U0001d55b",
+ "jscr;": "\U0001d4bf",
+ "jsercy;": "\u0458",
+ "jukcy;": "\u0454",
+ "kappa;": "\u03ba",
+ "kappav;": "\u03f0",
+ "kcedil;": "\u0137",
+ "kcy;": "\u043a",
+ "kfr;": "\U0001d528",
+ "kgreen;": "\u0138",
+ "khcy;": "\u0445",
+ "kjcy;": "\u045c",
+ "kopf;": "\U0001d55c",
+ "kscr;": "\U0001d4c0",
+ "lAarr;": "\u21da",
+ "lArr;": "\u21d0",
+ "lAtail;": "\u291b",
+ "lBarr;": "\u290e",
+ "lE;": "\u2266",
+ "lEg;": "\u2a8b",
+ "lHar;": "\u2962",
+ "lacute;": "\u013a",
+ "laemptyv;": "\u29b4",
+ "lagran;": "\u2112",
+ "lambda;": "\u03bb",
+ "lang;": "\u27e8",
+ "langd;": "\u2991",
+ "langle;": "\u27e8",
+ "lap;": "\u2a85",
+ "laquo": "\xab",
+ "laquo;": "\xab",
+ "larr;": "\u2190",
+ "larrb;": "\u21e4",
+ "larrbfs;": "\u291f",
+ "larrfs;": "\u291d",
+ "larrhk;": "\u21a9",
+ "larrlp;": "\u21ab",
+ "larrpl;": "\u2939",
+ "larrsim;": "\u2973",
+ "larrtl;": "\u21a2",
+ "lat;": "\u2aab",
+ "latail;": "\u2919",
+ "late;": "\u2aad",
+ "lates;": "\u2aad\ufe00",
+ "lbarr;": "\u290c",
+ "lbbrk;": "\u2772",
+ "lbrace;": "{",
+ "lbrack;": "[",
+ "lbrke;": "\u298b",
+ "lbrksld;": "\u298f",
+ "lbrkslu;": "\u298d",
+ "lcaron;": "\u013e",
+ "lcedil;": "\u013c",
+ "lceil;": "\u2308",
+ "lcub;": "{",
+ "lcy;": "\u043b",
+ "ldca;": "\u2936",
+ "ldquo;": "\u201c",
+ "ldquor;": "\u201e",
+ "ldrdhar;": "\u2967",
+ "ldrushar;": "\u294b",
+ "ldsh;": "\u21b2",
+ "le;": "\u2264",
+ "leftarrow;": "\u2190",
+ "leftarrowtail;": "\u21a2",
+ "leftharpoondown;": "\u21bd",
+ "leftharpoonup;": "\u21bc",
+ "leftleftarrows;": "\u21c7",
+ "leftrightarrow;": "\u2194",
+ "leftrightarrows;": "\u21c6",
+ "leftrightharpoons;": "\u21cb",
+ "leftrightsquigarrow;": "\u21ad",
+ "leftthreetimes;": "\u22cb",
+ "leg;": "\u22da",
+ "leq;": "\u2264",
+ "leqq;": "\u2266",
+ "leqslant;": "\u2a7d",
+ "les;": "\u2a7d",
+ "lescc;": "\u2aa8",
+ "lesdot;": "\u2a7f",
+ "lesdoto;": "\u2a81",
+ "lesdotor;": "\u2a83",
+ "lesg;": "\u22da\ufe00",
+ "lesges;": "\u2a93",
+ "lessapprox;": "\u2a85",
+ "lessdot;": "\u22d6",
+ "lesseqgtr;": "\u22da",
+ "lesseqqgtr;": "\u2a8b",
+ "lessgtr;": "\u2276",
+ "lesssim;": "\u2272",
+ "lfisht;": "\u297c",
+ "lfloor;": "\u230a",
+ "lfr;": "\U0001d529",
+ "lg;": "\u2276",
+ "lgE;": "\u2a91",
+ "lhard;": "\u21bd",
+ "lharu;": "\u21bc",
+ "lharul;": "\u296a",
+ "lhblk;": "\u2584",
+ "ljcy;": "\u0459",
+ "ll;": "\u226a",
+ "llarr;": "\u21c7",
+ "llcorner;": "\u231e",
+ "llhard;": "\u296b",
+ "lltri;": "\u25fa",
+ "lmidot;": "\u0140",
+ "lmoust;": "\u23b0",
+ "lmoustache;": "\u23b0",
+ "lnE;": "\u2268",
+ "lnap;": "\u2a89",
+ "lnapprox;": "\u2a89",
+ "lne;": "\u2a87",
+ "lneq;": "\u2a87",
+ "lneqq;": "\u2268",
+ "lnsim;": "\u22e6",
+ "loang;": "\u27ec",
+ "loarr;": "\u21fd",
+ "lobrk;": "\u27e6",
+ "longleftarrow;": "\u27f5",
+ "longleftrightarrow;": "\u27f7",
+ "longmapsto;": "\u27fc",
+ "longrightarrow;": "\u27f6",
+ "looparrowleft;": "\u21ab",
+ "looparrowright;": "\u21ac",
+ "lopar;": "\u2985",
+ "lopf;": "\U0001d55d",
+ "loplus;": "\u2a2d",
+ "lotimes;": "\u2a34",
+ "lowast;": "\u2217",
+ "lowbar;": "_",
+ "loz;": "\u25ca",
+ "lozenge;": "\u25ca",
+ "lozf;": "\u29eb",
+ "lpar;": "(",
+ "lparlt;": "\u2993",
+ "lrarr;": "\u21c6",
+ "lrcorner;": "\u231f",
+ "lrhar;": "\u21cb",
+ "lrhard;": "\u296d",
+ "lrm;": "\u200e",
+ "lrtri;": "\u22bf",
+ "lsaquo;": "\u2039",
+ "lscr;": "\U0001d4c1",
+ "lsh;": "\u21b0",
+ "lsim;": "\u2272",
+ "lsime;": "\u2a8d",
+ "lsimg;": "\u2a8f",
+ "lsqb;": "[",
+ "lsquo;": "\u2018",
+ "lsquor;": "\u201a",
+ "lstrok;": "\u0142",
+ "lt": "<",
+ "lt;": "<",
+ "ltcc;": "\u2aa6",
+ "ltcir;": "\u2a79",
+ "ltdot;": "\u22d6",
+ "lthree;": "\u22cb",
+ "ltimes;": "\u22c9",
+ "ltlarr;": "\u2976",
+ "ltquest;": "\u2a7b",
+ "ltrPar;": "\u2996",
+ "ltri;": "\u25c3",
+ "ltrie;": "\u22b4",
+ "ltrif;": "\u25c2",
+ "lurdshar;": "\u294a",
+ "luruhar;": "\u2966",
+ "lvertneqq;": "\u2268\ufe00",
+ "lvnE;": "\u2268\ufe00",
+ "mDDot;": "\u223a",
+ "macr": "\xaf",
+ "macr;": "\xaf",
+ "male;": "\u2642",
+ "malt;": "\u2720",
+ "maltese;": "\u2720",
+ "map;": "\u21a6",
+ "mapsto;": "\u21a6",
+ "mapstodown;": "\u21a7",
+ "mapstoleft;": "\u21a4",
+ "mapstoup;": "\u21a5",
+ "marker;": "\u25ae",
+ "mcomma;": "\u2a29",
+ "mcy;": "\u043c",
+ "mdash;": "\u2014",
+ "measuredangle;": "\u2221",
+ "mfr;": "\U0001d52a",
+ "mho;": "\u2127",
+ "micro": "\xb5",
+ "micro;": "\xb5",
+ "mid;": "\u2223",
+ "midast;": "*",
+ "midcir;": "\u2af0",
+ "middot": "\xb7",
+ "middot;": "\xb7",
+ "minus;": "\u2212",
+ "minusb;": "\u229f",
+ "minusd;": "\u2238",
+ "minusdu;": "\u2a2a",
+ "mlcp;": "\u2adb",
+ "mldr;": "\u2026",
+ "mnplus;": "\u2213",
+ "models;": "\u22a7",
+ "mopf;": "\U0001d55e",
+ "mp;": "\u2213",
+ "mscr;": "\U0001d4c2",
+ "mstpos;": "\u223e",
+ "mu;": "\u03bc",
+ "multimap;": "\u22b8",
+ "mumap;": "\u22b8",
+ "nGg;": "\u22d9\u0338",
+ "nGt;": "\u226b\u20d2",
+ "nGtv;": "\u226b\u0338",
+ "nLeftarrow;": "\u21cd",
+ "nLeftrightarrow;": "\u21ce",
+ "nLl;": "\u22d8\u0338",
+ "nLt;": "\u226a\u20d2",
+ "nLtv;": "\u226a\u0338",
+ "nRightarrow;": "\u21cf",
+ "nVDash;": "\u22af",
+ "nVdash;": "\u22ae",
+ "nabla;": "\u2207",
+ "nacute;": "\u0144",
+ "nang;": "\u2220\u20d2",
+ "nap;": "\u2249",
+ "napE;": "\u2a70\u0338",
+ "napid;": "\u224b\u0338",
+ "napos;": "\u0149",
+ "napprox;": "\u2249",
+ "natur;": "\u266e",
+ "natural;": "\u266e",
+ "naturals;": "\u2115",
+ "nbsp": "\xa0",
+ "nbsp;": "\xa0",
+ "nbump;": "\u224e\u0338",
+ "nbumpe;": "\u224f\u0338",
+ "ncap;": "\u2a43",
+ "ncaron;": "\u0148",
+ "ncedil;": "\u0146",
+ "ncong;": "\u2247",
+ "ncongdot;": "\u2a6d\u0338",
+ "ncup;": "\u2a42",
+ "ncy;": "\u043d",
+ "ndash;": "\u2013",
+ "ne;": "\u2260",
+ "neArr;": "\u21d7",
+ "nearhk;": "\u2924",
+ "nearr;": "\u2197",
+ "nearrow;": "\u2197",
+ "nedot;": "\u2250\u0338",
+ "nequiv;": "\u2262",
+ "nesear;": "\u2928",
+ "nesim;": "\u2242\u0338",
+ "nexist;": "\u2204",
+ "nexists;": "\u2204",
+ "nfr;": "\U0001d52b",
+ "ngE;": "\u2267\u0338",
+ "nge;": "\u2271",
+ "ngeq;": "\u2271",
+ "ngeqq;": "\u2267\u0338",
+ "ngeqslant;": "\u2a7e\u0338",
+ "nges;": "\u2a7e\u0338",
+ "ngsim;": "\u2275",
+ "ngt;": "\u226f",
+ "ngtr;": "\u226f",
+ "nhArr;": "\u21ce",
+ "nharr;": "\u21ae",
+ "nhpar;": "\u2af2",
+ "ni;": "\u220b",
+ "nis;": "\u22fc",
+ "nisd;": "\u22fa",
+ "niv;": "\u220b",
+ "njcy;": "\u045a",
+ "nlArr;": "\u21cd",
+ "nlE;": "\u2266\u0338",
+ "nlarr;": "\u219a",
+ "nldr;": "\u2025",
+ "nle;": "\u2270",
+ "nleftarrow;": "\u219a",
+ "nleftrightarrow;": "\u21ae",
+ "nleq;": "\u2270",
+ "nleqq;": "\u2266\u0338",
+ "nleqslant;": "\u2a7d\u0338",
+ "nles;": "\u2a7d\u0338",
+ "nless;": "\u226e",
+ "nlsim;": "\u2274",
+ "nlt;": "\u226e",
+ "nltri;": "\u22ea",
+ "nltrie;": "\u22ec",
+ "nmid;": "\u2224",
+ "nopf;": "\U0001d55f",
+ "not": "\xac",
+ "not;": "\xac",
+ "notin;": "\u2209",
+ "notinE;": "\u22f9\u0338",
+ "notindot;": "\u22f5\u0338",
+ "notinva;": "\u2209",
+ "notinvb;": "\u22f7",
+ "notinvc;": "\u22f6",
+ "notni;": "\u220c",
+ "notniva;": "\u220c",
+ "notnivb;": "\u22fe",
+ "notnivc;": "\u22fd",
+ "npar;": "\u2226",
+ "nparallel;": "\u2226",
+ "nparsl;": "\u2afd\u20e5",
+ "npart;": "\u2202\u0338",
+ "npolint;": "\u2a14",
+ "npr;": "\u2280",
+ "nprcue;": "\u22e0",
+ "npre;": "\u2aaf\u0338",
+ "nprec;": "\u2280",
+ "npreceq;": "\u2aaf\u0338",
+ "nrArr;": "\u21cf",
+ "nrarr;": "\u219b",
+ "nrarrc;": "\u2933\u0338",
+ "nrarrw;": "\u219d\u0338",
+ "nrightarrow;": "\u219b",
+ "nrtri;": "\u22eb",
+ "nrtrie;": "\u22ed",
+ "nsc;": "\u2281",
+ "nsccue;": "\u22e1",
+ "nsce;": "\u2ab0\u0338",
+ "nscr;": "\U0001d4c3",
+ "nshortmid;": "\u2224",
+ "nshortparallel;": "\u2226",
+ "nsim;": "\u2241",
+ "nsime;": "\u2244",
+ "nsimeq;": "\u2244",
+ "nsmid;": "\u2224",
+ "nspar;": "\u2226",
+ "nsqsube;": "\u22e2",
+ "nsqsupe;": "\u22e3",
+ "nsub;": "\u2284",
+ "nsubE;": "\u2ac5\u0338",
+ "nsube;": "\u2288",
+ "nsubset;": "\u2282\u20d2",
+ "nsubseteq;": "\u2288",
+ "nsubseteqq;": "\u2ac5\u0338",
+ "nsucc;": "\u2281",
+ "nsucceq;": "\u2ab0\u0338",
+ "nsup;": "\u2285",
+ "nsupE;": "\u2ac6\u0338",
+ "nsupe;": "\u2289",
+ "nsupset;": "\u2283\u20d2",
+ "nsupseteq;": "\u2289",
+ "nsupseteqq;": "\u2ac6\u0338",
+ "ntgl;": "\u2279",
+ "ntilde": "\xf1",
+ "ntilde;": "\xf1",
+ "ntlg;": "\u2278",
+ "ntriangleleft;": "\u22ea",
+ "ntrianglelefteq;": "\u22ec",
+ "ntriangleright;": "\u22eb",
+ "ntrianglerighteq;": "\u22ed",
+ "nu;": "\u03bd",
+ "num;": "#",
+ "numero;": "\u2116",
+ "numsp;": "\u2007",
+ "nvDash;": "\u22ad",
+ "nvHarr;": "\u2904",
+ "nvap;": "\u224d\u20d2",
+ "nvdash;": "\u22ac",
+ "nvge;": "\u2265\u20d2",
+ "nvgt;": ">\u20d2",
+ "nvinfin;": "\u29de",
+ "nvlArr;": "\u2902",
+ "nvle;": "\u2264\u20d2",
+ "nvlt;": "<\u20d2",
+ "nvltrie;": "\u22b4\u20d2",
+ "nvrArr;": "\u2903",
+ "nvrtrie;": "\u22b5\u20d2",
+ "nvsim;": "\u223c\u20d2",
+ "nwArr;": "\u21d6",
+ "nwarhk;": "\u2923",
+ "nwarr;": "\u2196",
+ "nwarrow;": "\u2196",
+ "nwnear;": "\u2927",
+ "oS;": "\u24c8",
+ "oacute": "\xf3",
+ "oacute;": "\xf3",
+ "oast;": "\u229b",
+ "ocir;": "\u229a",
+ "ocirc": "\xf4",
+ "ocirc;": "\xf4",
+ "ocy;": "\u043e",
+ "odash;": "\u229d",
+ "odblac;": "\u0151",
+ "odiv;": "\u2a38",
+ "odot;": "\u2299",
+ "odsold;": "\u29bc",
+ "oelig;": "\u0153",
+ "ofcir;": "\u29bf",
+ "ofr;": "\U0001d52c",
+ "ogon;": "\u02db",
+ "ograve": "\xf2",
+ "ograve;": "\xf2",
+ "ogt;": "\u29c1",
+ "ohbar;": "\u29b5",
+ "ohm;": "\u03a9",
+ "oint;": "\u222e",
+ "olarr;": "\u21ba",
+ "olcir;": "\u29be",
+ "olcross;": "\u29bb",
+ "oline;": "\u203e",
+ "olt;": "\u29c0",
+ "omacr;": "\u014d",
+ "omega;": "\u03c9",
+ "omicron;": "\u03bf",
+ "omid;": "\u29b6",
+ "ominus;": "\u2296",
+ "oopf;": "\U0001d560",
+ "opar;": "\u29b7",
+ "operp;": "\u29b9",
+ "oplus;": "\u2295",
+ "or;": "\u2228",
+ "orarr;": "\u21bb",
+ "ord;": "\u2a5d",
+ "order;": "\u2134",
+ "orderof;": "\u2134",
+ "ordf": "\xaa",
+ "ordf;": "\xaa",
+ "ordm": "\xba",
+ "ordm;": "\xba",
+ "origof;": "\u22b6",
+ "oror;": "\u2a56",
+ "orslope;": "\u2a57",
+ "orv;": "\u2a5b",
+ "oscr;": "\u2134",
+ "oslash": "\xf8",
+ "oslash;": "\xf8",
+ "osol;": "\u2298",
+ "otilde": "\xf5",
+ "otilde;": "\xf5",
+ "otimes;": "\u2297",
+ "otimesas;": "\u2a36",
+ "ouml": "\xf6",
+ "ouml;": "\xf6",
+ "ovbar;": "\u233d",
+ "par;": "\u2225",
+ "para": "\xb6",
+ "para;": "\xb6",
+ "parallel;": "\u2225",
+ "parsim;": "\u2af3",
+ "parsl;": "\u2afd",
+ "part;": "\u2202",
+ "pcy;": "\u043f",
+ "percnt;": "%",
+ "period;": ".",
+ "permil;": "\u2030",
+ "perp;": "\u22a5",
+ "pertenk;": "\u2031",
+ "pfr;": "\U0001d52d",
+ "phi;": "\u03c6",
+ "phiv;": "\u03d5",
+ "phmmat;": "\u2133",
+ "phone;": "\u260e",
+ "pi;": "\u03c0",
+ "pitchfork;": "\u22d4",
+ "piv;": "\u03d6",
+ "planck;": "\u210f",
+ "planckh;": "\u210e",
+ "plankv;": "\u210f",
+ "plus;": "+",
+ "plusacir;": "\u2a23",
+ "plusb;": "\u229e",
+ "pluscir;": "\u2a22",
+ "plusdo;": "\u2214",
+ "plusdu;": "\u2a25",
+ "pluse;": "\u2a72",
+ "plusmn": "\xb1",
+ "plusmn;": "\xb1",
+ "plussim;": "\u2a26",
+ "plustwo;": "\u2a27",
+ "pm;": "\xb1",
+ "pointint;": "\u2a15",
+ "popf;": "\U0001d561",
+ "pound": "\xa3",
+ "pound;": "\xa3",
+ "pr;": "\u227a",
+ "prE;": "\u2ab3",
+ "prap;": "\u2ab7",
+ "prcue;": "\u227c",
+ "pre;": "\u2aaf",
+ "prec;": "\u227a",
+ "precapprox;": "\u2ab7",
+ "preccurlyeq;": "\u227c",
+ "preceq;": "\u2aaf",
+ "precnapprox;": "\u2ab9",
+ "precneqq;": "\u2ab5",
+ "precnsim;": "\u22e8",
+ "precsim;": "\u227e",
+ "prime;": "\u2032",
+ "primes;": "\u2119",
+ "prnE;": "\u2ab5",
+ "prnap;": "\u2ab9",
+ "prnsim;": "\u22e8",
+ "prod;": "\u220f",
+ "profalar;": "\u232e",
+ "profline;": "\u2312",
+ "profsurf;": "\u2313",
+ "prop;": "\u221d",
+ "propto;": "\u221d",
+ "prsim;": "\u227e",
+ "prurel;": "\u22b0",
+ "pscr;": "\U0001d4c5",
+ "psi;": "\u03c8",
+ "puncsp;": "\u2008",
+ "qfr;": "\U0001d52e",
+ "qint;": "\u2a0c",
+ "qopf;": "\U0001d562",
+ "qprime;": "\u2057",
+ "qscr;": "\U0001d4c6",
+ "quaternions;": "\u210d",
+ "quatint;": "\u2a16",
+ "quest;": "?",
+ "questeq;": "\u225f",
+ "quot": "\"",
+ "quot;": "\"",
+ "rAarr;": "\u21db",
+ "rArr;": "\u21d2",
+ "rAtail;": "\u291c",
+ "rBarr;": "\u290f",
+ "rHar;": "\u2964",
+ "race;": "\u223d\u0331",
+ "racute;": "\u0155",
+ "radic;": "\u221a",
+ "raemptyv;": "\u29b3",
+ "rang;": "\u27e9",
+ "rangd;": "\u2992",
+ "range;": "\u29a5",
+ "rangle;": "\u27e9",
+ "raquo": "\xbb",
+ "raquo;": "\xbb",
+ "rarr;": "\u2192",
+ "rarrap;": "\u2975",
+ "rarrb;": "\u21e5",
+ "rarrbfs;": "\u2920",
+ "rarrc;": "\u2933",
+ "rarrfs;": "\u291e",
+ "rarrhk;": "\u21aa",
+ "rarrlp;": "\u21ac",
+ "rarrpl;": "\u2945",
+ "rarrsim;": "\u2974",
+ "rarrtl;": "\u21a3",
+ "rarrw;": "\u219d",
+ "ratail;": "\u291a",
+ "ratio;": "\u2236",
+ "rationals;": "\u211a",
+ "rbarr;": "\u290d",
+ "rbbrk;": "\u2773",
+ "rbrace;": "}",
+ "rbrack;": "]",
+ "rbrke;": "\u298c",
+ "rbrksld;": "\u298e",
+ "rbrkslu;": "\u2990",
+ "rcaron;": "\u0159",
+ "rcedil;": "\u0157",
+ "rceil;": "\u2309",
+ "rcub;": "}",
+ "rcy;": "\u0440",
+ "rdca;": "\u2937",
+ "rdldhar;": "\u2969",
+ "rdquo;": "\u201d",
+ "rdquor;": "\u201d",
+ "rdsh;": "\u21b3",
+ "real;": "\u211c",
+ "realine;": "\u211b",
+ "realpart;": "\u211c",
+ "reals;": "\u211d",
+ "rect;": "\u25ad",
+ "reg": "\xae",
+ "reg;": "\xae",
+ "rfisht;": "\u297d",
+ "rfloor;": "\u230b",
+ "rfr;": "\U0001d52f",
+ "rhard;": "\u21c1",
+ "rharu;": "\u21c0",
+ "rharul;": "\u296c",
+ "rho;": "\u03c1",
+ "rhov;": "\u03f1",
+ "rightarrow;": "\u2192",
+ "rightarrowtail;": "\u21a3",
+ "rightharpoondown;": "\u21c1",
+ "rightharpoonup;": "\u21c0",
+ "rightleftarrows;": "\u21c4",
+ "rightleftharpoons;": "\u21cc",
+ "rightrightarrows;": "\u21c9",
+ "rightsquigarrow;": "\u219d",
+ "rightthreetimes;": "\u22cc",
+ "ring;": "\u02da",
+ "risingdotseq;": "\u2253",
+ "rlarr;": "\u21c4",
+ "rlhar;": "\u21cc",
+ "rlm;": "\u200f",
+ "rmoust;": "\u23b1",
+ "rmoustache;": "\u23b1",
+ "rnmid;": "\u2aee",
+ "roang;": "\u27ed",
+ "roarr;": "\u21fe",
+ "robrk;": "\u27e7",
+ "ropar;": "\u2986",
+ "ropf;": "\U0001d563",
+ "roplus;": "\u2a2e",
+ "rotimes;": "\u2a35",
+ "rpar;": ")",
+ "rpargt;": "\u2994",
+ "rppolint;": "\u2a12",
+ "rrarr;": "\u21c9",
+ "rsaquo;": "\u203a",
+ "rscr;": "\U0001d4c7",
+ "rsh;": "\u21b1",
+ "rsqb;": "]",
+ "rsquo;": "\u2019",
+ "rsquor;": "\u2019",
+ "rthree;": "\u22cc",
+ "rtimes;": "\u22ca",
+ "rtri;": "\u25b9",
+ "rtrie;": "\u22b5",
+ "rtrif;": "\u25b8",
+ "rtriltri;": "\u29ce",
+ "ruluhar;": "\u2968",
+ "rx;": "\u211e",
+ "sacute;": "\u015b",
+ "sbquo;": "\u201a",
+ "sc;": "\u227b",
+ "scE;": "\u2ab4",
+ "scap;": "\u2ab8",
+ "scaron;": "\u0161",
+ "sccue;": "\u227d",
+ "sce;": "\u2ab0",
+ "scedil;": "\u015f",
+ "scirc;": "\u015d",
+ "scnE;": "\u2ab6",
+ "scnap;": "\u2aba",
+ "scnsim;": "\u22e9",
+ "scpolint;": "\u2a13",
+ "scsim;": "\u227f",
+ "scy;": "\u0441",
+ "sdot;": "\u22c5",
+ "sdotb;": "\u22a1",
+ "sdote;": "\u2a66",
+ "seArr;": "\u21d8",
+ "searhk;": "\u2925",
+ "searr;": "\u2198",
+ "searrow;": "\u2198",
+ "sect": "\xa7",
+ "sect;": "\xa7",
+ "semi;": ";",
+ "seswar;": "\u2929",
+ "setminus;": "\u2216",
+ "setmn;": "\u2216",
+ "sext;": "\u2736",
+ "sfr;": "\U0001d530",
+ "sfrown;": "\u2322",
+ "sharp;": "\u266f",
+ "shchcy;": "\u0449",
+ "shcy;": "\u0448",
+ "shortmid;": "\u2223",
+ "shortparallel;": "\u2225",
+ "shy": "\xad",
+ "shy;": "\xad",
+ "sigma;": "\u03c3",
+ "sigmaf;": "\u03c2",
+ "sigmav;": "\u03c2",
+ "sim;": "\u223c",
+ "simdot;": "\u2a6a",
+ "sime;": "\u2243",
+ "simeq;": "\u2243",
+ "simg;": "\u2a9e",
+ "simgE;": "\u2aa0",
+ "siml;": "\u2a9d",
+ "simlE;": "\u2a9f",
+ "simne;": "\u2246",
+ "simplus;": "\u2a24",
+ "simrarr;": "\u2972",
+ "slarr;": "\u2190",
+ "smallsetminus;": "\u2216",
+ "smashp;": "\u2a33",
+ "smeparsl;": "\u29e4",
+ "smid;": "\u2223",
+ "smile;": "\u2323",
+ "smt;": "\u2aaa",
+ "smte;": "\u2aac",
+ "smtes;": "\u2aac\ufe00",
+ "softcy;": "\u044c",
+ "sol;": "/",
+ "solb;": "\u29c4",
+ "solbar;": "\u233f",
+ "sopf;": "\U0001d564",
+ "spades;": "\u2660",
+ "spadesuit;": "\u2660",
+ "spar;": "\u2225",
+ "sqcap;": "\u2293",
+ "sqcaps;": "\u2293\ufe00",
+ "sqcup;": "\u2294",
+ "sqcups;": "\u2294\ufe00",
+ "sqsub;": "\u228f",
+ "sqsube;": "\u2291",
+ "sqsubset;": "\u228f",
+ "sqsubseteq;": "\u2291",
+ "sqsup;": "\u2290",
+ "sqsupe;": "\u2292",
+ "sqsupset;": "\u2290",
+ "sqsupseteq;": "\u2292",
+ "squ;": "\u25a1",
+ "square;": "\u25a1",
+ "squarf;": "\u25aa",
+ "squf;": "\u25aa",
+ "srarr;": "\u2192",
+ "sscr;": "\U0001d4c8",
+ "ssetmn;": "\u2216",
+ "ssmile;": "\u2323",
+ "sstarf;": "\u22c6",
+ "star;": "\u2606",
+ "starf;": "\u2605",
+ "straightepsilon;": "\u03f5",
+ "straightphi;": "\u03d5",
+ "strns;": "\xaf",
+ "sub;": "\u2282",
+ "subE;": "\u2ac5",
+ "subdot;": "\u2abd",
+ "sube;": "\u2286",
+ "subedot;": "\u2ac3",
+ "submult;": "\u2ac1",
+ "subnE;": "\u2acb",
+ "subne;": "\u228a",
+ "subplus;": "\u2abf",
+ "subrarr;": "\u2979",
+ "subset;": "\u2282",
+ "subseteq;": "\u2286",
+ "subseteqq;": "\u2ac5",
+ "subsetneq;": "\u228a",
+ "subsetneqq;": "\u2acb",
+ "subsim;": "\u2ac7",
+ "subsub;": "\u2ad5",
+ "subsup;": "\u2ad3",
+ "succ;": "\u227b",
+ "succapprox;": "\u2ab8",
+ "succcurlyeq;": "\u227d",
+ "succeq;": "\u2ab0",
+ "succnapprox;": "\u2aba",
+ "succneqq;": "\u2ab6",
+ "succnsim;": "\u22e9",
+ "succsim;": "\u227f",
+ "sum;": "\u2211",
+ "sung;": "\u266a",
+ "sup1": "\xb9",
+ "sup1;": "\xb9",
+ "sup2": "\xb2",
+ "sup2;": "\xb2",
+ "sup3": "\xb3",
+ "sup3;": "\xb3",
+ "sup;": "\u2283",
+ "supE;": "\u2ac6",
+ "supdot;": "\u2abe",
+ "supdsub;": "\u2ad8",
+ "supe;": "\u2287",
+ "supedot;": "\u2ac4",
+ "suphsol;": "\u27c9",
+ "suphsub;": "\u2ad7",
+ "suplarr;": "\u297b",
+ "supmult;": "\u2ac2",
+ "supnE;": "\u2acc",
+ "supne;": "\u228b",
+ "supplus;": "\u2ac0",
+ "supset;": "\u2283",
+ "supseteq;": "\u2287",
+ "supseteqq;": "\u2ac6",
+ "supsetneq;": "\u228b",
+ "supsetneqq;": "\u2acc",
+ "supsim;": "\u2ac8",
+ "supsub;": "\u2ad4",
+ "supsup;": "\u2ad6",
+ "swArr;": "\u21d9",
+ "swarhk;": "\u2926",
+ "swarr;": "\u2199",
+ "swarrow;": "\u2199",
+ "swnwar;": "\u292a",
+ "szlig": "\xdf",
+ "szlig;": "\xdf",
+ "target;": "\u2316",
+ "tau;": "\u03c4",
+ "tbrk;": "\u23b4",
+ "tcaron;": "\u0165",
+ "tcedil;": "\u0163",
+ "tcy;": "\u0442",
+ "tdot;": "\u20db",
+ "telrec;": "\u2315",
+ "tfr;": "\U0001d531",
+ "there4;": "\u2234",
+ "therefore;": "\u2234",
+ "theta;": "\u03b8",
+ "thetasym;": "\u03d1",
+ "thetav;": "\u03d1",
+ "thickapprox;": "\u2248",
+ "thicksim;": "\u223c",
+ "thinsp;": "\u2009",
+ "thkap;": "\u2248",
+ "thksim;": "\u223c",
+ "thorn": "\xfe",
+ "thorn;": "\xfe",
+ "tilde;": "\u02dc",
+ "times": "\xd7",
+ "times;": "\xd7",
+ "timesb;": "\u22a0",
+ "timesbar;": "\u2a31",
+ "timesd;": "\u2a30",
+ "tint;": "\u222d",
+ "toea;": "\u2928",
+ "top;": "\u22a4",
+ "topbot;": "\u2336",
+ "topcir;": "\u2af1",
+ "topf;": "\U0001d565",
+ "topfork;": "\u2ada",
+ "tosa;": "\u2929",
+ "tprime;": "\u2034",
+ "trade;": "\u2122",
+ "triangle;": "\u25b5",
+ "triangledown;": "\u25bf",
+ "triangleleft;": "\u25c3",
+ "trianglelefteq;": "\u22b4",
+ "triangleq;": "\u225c",
+ "triangleright;": "\u25b9",
+ "trianglerighteq;": "\u22b5",
+ "tridot;": "\u25ec",
+ "trie;": "\u225c",
+ "triminus;": "\u2a3a",
+ "triplus;": "\u2a39",
+ "trisb;": "\u29cd",
+ "tritime;": "\u2a3b",
+ "trpezium;": "\u23e2",
+ "tscr;": "\U0001d4c9",
+ "tscy;": "\u0446",
+ "tshcy;": "\u045b",
+ "tstrok;": "\u0167",
+ "twixt;": "\u226c",
+ "twoheadleftarrow;": "\u219e",
+ "twoheadrightarrow;": "\u21a0",
+ "uArr;": "\u21d1",
+ "uHar;": "\u2963",
+ "uacute": "\xfa",
+ "uacute;": "\xfa",
+ "uarr;": "\u2191",
+ "ubrcy;": "\u045e",
+ "ubreve;": "\u016d",
+ "ucirc": "\xfb",
+ "ucirc;": "\xfb",
+ "ucy;": "\u0443",
+ "udarr;": "\u21c5",
+ "udblac;": "\u0171",
+ "udhar;": "\u296e",
+ "ufisht;": "\u297e",
+ "ufr;": "\U0001d532",
+ "ugrave": "\xf9",
+ "ugrave;": "\xf9",
+ "uharl;": "\u21bf",
+ "uharr;": "\u21be",
+ "uhblk;": "\u2580",
+ "ulcorn;": "\u231c",
+ "ulcorner;": "\u231c",
+ "ulcrop;": "\u230f",
+ "ultri;": "\u25f8",
+ "umacr;": "\u016b",
+ "uml": "\xa8",
+ "uml;": "\xa8",
+ "uogon;": "\u0173",
+ "uopf;": "\U0001d566",
+ "uparrow;": "\u2191",
+ "updownarrow;": "\u2195",
+ "upharpoonleft;": "\u21bf",
+ "upharpoonright;": "\u21be",
+ "uplus;": "\u228e",
+ "upsi;": "\u03c5",
+ "upsih;": "\u03d2",
+ "upsilon;": "\u03c5",
+ "upuparrows;": "\u21c8",
+ "urcorn;": "\u231d",
+ "urcorner;": "\u231d",
+ "urcrop;": "\u230e",
+ "uring;": "\u016f",
+ "urtri;": "\u25f9",
+ "uscr;": "\U0001d4ca",
+ "utdot;": "\u22f0",
+ "utilde;": "\u0169",
+ "utri;": "\u25b5",
+ "utrif;": "\u25b4",
+ "uuarr;": "\u21c8",
+ "uuml": "\xfc",
+ "uuml;": "\xfc",
+ "uwangle;": "\u29a7",
+ "vArr;": "\u21d5",
+ "vBar;": "\u2ae8",
+ "vBarv;": "\u2ae9",
+ "vDash;": "\u22a8",
+ "vangrt;": "\u299c",
+ "varepsilon;": "\u03f5",
+ "varkappa;": "\u03f0",
+ "varnothing;": "\u2205",
+ "varphi;": "\u03d5",
+ "varpi;": "\u03d6",
+ "varpropto;": "\u221d",
+ "varr;": "\u2195",
+ "varrho;": "\u03f1",
+ "varsigma;": "\u03c2",
+ "varsubsetneq;": "\u228a\ufe00",
+ "varsubsetneqq;": "\u2acb\ufe00",
+ "varsupsetneq;": "\u228b\ufe00",
+ "varsupsetneqq;": "\u2acc\ufe00",
+ "vartheta;": "\u03d1",
+ "vartriangleleft;": "\u22b2",
+ "vartriangleright;": "\u22b3",
+ "vcy;": "\u0432",
+ "vdash;": "\u22a2",
+ "vee;": "\u2228",
+ "veebar;": "\u22bb",
+ "veeeq;": "\u225a",
+ "vellip;": "\u22ee",
+ "verbar;": "|",
+ "vert;": "|",
+ "vfr;": "\U0001d533",
+ "vltri;": "\u22b2",
+ "vnsub;": "\u2282\u20d2",
+ "vnsup;": "\u2283\u20d2",
+ "vopf;": "\U0001d567",
+ "vprop;": "\u221d",
+ "vrtri;": "\u22b3",
+ "vscr;": "\U0001d4cb",
+ "vsubnE;": "\u2acb\ufe00",
+ "vsubne;": "\u228a\ufe00",
+ "vsupnE;": "\u2acc\ufe00",
+ "vsupne;": "\u228b\ufe00",
+ "vzigzag;": "\u299a",
+ "wcirc;": "\u0175",
+ "wedbar;": "\u2a5f",
+ "wedge;": "\u2227",
+ "wedgeq;": "\u2259",
+ "weierp;": "\u2118",
+ "wfr;": "\U0001d534",
+ "wopf;": "\U0001d568",
+ "wp;": "\u2118",
+ "wr;": "\u2240",
+ "wreath;": "\u2240",
+ "wscr;": "\U0001d4cc",
+ "xcap;": "\u22c2",
+ "xcirc;": "\u25ef",
+ "xcup;": "\u22c3",
+ "xdtri;": "\u25bd",
+ "xfr;": "\U0001d535",
+ "xhArr;": "\u27fa",
+ "xharr;": "\u27f7",
+ "xi;": "\u03be",
+ "xlArr;": "\u27f8",
+ "xlarr;": "\u27f5",
+ "xmap;": "\u27fc",
+ "xnis;": "\u22fb",
+ "xodot;": "\u2a00",
+ "xopf;": "\U0001d569",
+ "xoplus;": "\u2a01",
+ "xotime;": "\u2a02",
+ "xrArr;": "\u27f9",
+ "xrarr;": "\u27f6",
+ "xscr;": "\U0001d4cd",
+ "xsqcup;": "\u2a06",
+ "xuplus;": "\u2a04",
+ "xutri;": "\u25b3",
+ "xvee;": "\u22c1",
+ "xwedge;": "\u22c0",
+ "yacute": "\xfd",
+ "yacute;": "\xfd",
+ "yacy;": "\u044f",
+ "ycirc;": "\u0177",
+ "ycy;": "\u044b",
+ "yen": "\xa5",
+ "yen;": "\xa5",
+ "yfr;": "\U0001d536",
+ "yicy;": "\u0457",
+ "yopf;": "\U0001d56a",
+ "yscr;": "\U0001d4ce",
+ "yucy;": "\u044e",
+ "yuml": "\xff",
+ "yuml;": "\xff",
+ "zacute;": "\u017a",
+ "zcaron;": "\u017e",
+ "zcy;": "\u0437",
+ "zdot;": "\u017c",
+ "zeetrf;": "\u2128",
+ "zeta;": "\u03b6",
+ "zfr;": "\U0001d537",
+ "zhcy;": "\u0436",
+ "zigrarr;": "\u21dd",
+ "zopf;": "\U0001d56b",
+ "zscr;": "\U0001d4cf",
+ "zwj;": "\u200d",
+ "zwnj;": "\u200c",
+}
+
+replacementCharacters = {
+ 0x0: "\uFFFD",
+ 0x0d: "\u000D",
+ 0x80: "\u20AC",
+ 0x81: "\u0081",
+ 0x82: "\u201A",
+ 0x83: "\u0192",
+ 0x84: "\u201E",
+ 0x85: "\u2026",
+ 0x86: "\u2020",
+ 0x87: "\u2021",
+ 0x88: "\u02C6",
+ 0x89: "\u2030",
+ 0x8A: "\u0160",
+ 0x8B: "\u2039",
+ 0x8C: "\u0152",
+ 0x8D: "\u008D",
+ 0x8E: "\u017D",
+ 0x8F: "\u008F",
+ 0x90: "\u0090",
+ 0x91: "\u2018",
+ 0x92: "\u2019",
+ 0x93: "\u201C",
+ 0x94: "\u201D",
+ 0x95: "\u2022",
+ 0x96: "\u2013",
+ 0x97: "\u2014",
+ 0x98: "\u02DC",
+ 0x99: "\u2122",
+ 0x9A: "\u0161",
+ 0x9B: "\u203A",
+ 0x9C: "\u0153",
+ 0x9D: "\u009D",
+ 0x9E: "\u017E",
+ 0x9F: "\u0178",
+}
+
+tokenTypes = {
+ "Doctype": 0,
+ "Characters": 1,
+ "SpaceCharacters": 2,
+ "StartTag": 3,
+ "EndTag": 4,
+ "EmptyTag": 5,
+ "Comment": 6,
+ "ParseError": 7
+}
+
+tagTokenTypes = frozenset([tokenTypes["StartTag"], tokenTypes["EndTag"],
+ tokenTypes["EmptyTag"]])
+
+
+prefixes = {v: k for k, v in namespaces.items()}
+prefixes["http://www.w3.org/1998/Math/MathML"] = "math"
+
+
+class DataLossWarning(UserWarning):
+ """Raised when the current tree is unable to represent the input data"""
+ pass
+
+
+class _ReparseException(Exception):
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/__init__.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/alphabeticalattributes.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/alphabeticalattributes.py
new file mode 100644
index 0000000000..5ba926e3b0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/alphabeticalattributes.py
@@ -0,0 +1,29 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from . import base
+
+from collections import OrderedDict
+
+
+def _attr_key(attr):
+ """Return an appropriate key for an attribute for sorting
+
+ Attributes have a namespace that can be either ``None`` or a string. We
+ can't compare the two because they're different types, so we convert
+ ``None`` to an empty string first.
+
+ """
+ return (attr[0][0] or ''), attr[0][1]
+
+
+class Filter(base.Filter):
+ """Alphabetizes attributes for elements"""
+ def __iter__(self):
+ for token in base.Filter.__iter__(self):
+ if token["type"] in ("StartTag", "EmptyTag"):
+ attrs = OrderedDict()
+ for name, value in sorted(token["data"].items(),
+ key=_attr_key):
+ attrs[name] = value
+ token["data"] = attrs
+ yield token
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/base.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/base.py
new file mode 100644
index 0000000000..c7dbaed0fa
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/base.py
@@ -0,0 +1,12 @@
+from __future__ import absolute_import, division, unicode_literals
+
+
+class Filter(object):
+ def __init__(self, source):
+ self.source = source
+
+ def __iter__(self):
+ return iter(self.source)
+
+ def __getattr__(self, name):
+ return getattr(self.source, name)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/inject_meta_charset.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/inject_meta_charset.py
new file mode 100644
index 0000000000..aefb5c842c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/inject_meta_charset.py
@@ -0,0 +1,73 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from . import base
+
+
+class Filter(base.Filter):
+ """Injects ``<meta charset=ENCODING>`` tag into head of document"""
+ def __init__(self, source, encoding):
+ """Creates a Filter
+
+ :arg source: the source token stream
+
+ :arg encoding: the encoding to set
+
+ """
+ base.Filter.__init__(self, source)
+ self.encoding = encoding
+
+ def __iter__(self):
+ state = "pre_head"
+ meta_found = (self.encoding is None)
+ pending = []
+
+ for token in base.Filter.__iter__(self):
+ type = token["type"]
+ if type == "StartTag":
+ if token["name"].lower() == "head":
+ state = "in_head"
+
+ elif type == "EmptyTag":
+ if token["name"].lower() == "meta":
+ # replace charset with actual encoding
+ has_http_equiv_content_type = False
+ for (namespace, name), value in token["data"].items():
+ if namespace is not None:
+ continue
+ elif name.lower() == 'charset':
+ token["data"][(namespace, name)] = self.encoding
+ meta_found = True
+ break
+ elif name == 'http-equiv' and value.lower() == 'content-type':
+ has_http_equiv_content_type = True
+ else:
+ if has_http_equiv_content_type and (None, "content") in token["data"]:
+ token["data"][(None, "content")] = 'text/html; charset=%s' % self.encoding
+ meta_found = True
+
+ elif token["name"].lower() == "head" and not meta_found:
+ # insert meta into empty head
+ yield {"type": "StartTag", "name": "head",
+ "data": token["data"]}
+ yield {"type": "EmptyTag", "name": "meta",
+ "data": {(None, "charset"): self.encoding}}
+ yield {"type": "EndTag", "name": "head"}
+ meta_found = True
+ continue
+
+ elif type == "EndTag":
+ if token["name"].lower() == "head" and pending:
+ # insert meta into head (if necessary) and flush pending queue
+ yield pending.pop(0)
+ if not meta_found:
+ yield {"type": "EmptyTag", "name": "meta",
+ "data": {(None, "charset"): self.encoding}}
+ while pending:
+ yield pending.pop(0)
+ meta_found = True
+ state = "post_head"
+
+ if state == "in_head":
+ pending.append(token)
+ else:
+ yield token
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/lint.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/lint.py
new file mode 100644
index 0000000000..acd4d7a2af
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/lint.py
@@ -0,0 +1,93 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from six import text_type
+
+from . import base
+from ..constants import namespaces, voidElements
+
+from ..constants import spaceCharacters
+spaceCharacters = "".join(spaceCharacters)
+
+
+class Filter(base.Filter):
+ """Lints the token stream for errors
+
+ If it finds any errors, it'll raise an ``AssertionError``.
+
+ """
+ def __init__(self, source, require_matching_tags=True):
+ """Creates a Filter
+
+ :arg source: the source token stream
+
+ :arg require_matching_tags: whether or not to require matching tags
+
+ """
+ super(Filter, self).__init__(source)
+ self.require_matching_tags = require_matching_tags
+
+ def __iter__(self):
+ open_elements = []
+ for token in base.Filter.__iter__(self):
+ type = token["type"]
+ if type in ("StartTag", "EmptyTag"):
+ namespace = token["namespace"]
+ name = token["name"]
+ assert namespace is None or isinstance(namespace, text_type)
+ assert namespace != ""
+ assert isinstance(name, text_type)
+ assert name != ""
+ assert isinstance(token["data"], dict)
+ if (not namespace or namespace == namespaces["html"]) and name in voidElements:
+ assert type == "EmptyTag"
+ else:
+ assert type == "StartTag"
+ if type == "StartTag" and self.require_matching_tags:
+ open_elements.append((namespace, name))
+ for (namespace, name), value in token["data"].items():
+ assert namespace is None or isinstance(namespace, text_type)
+ assert namespace != ""
+ assert isinstance(name, text_type)
+ assert name != ""
+ assert isinstance(value, text_type)
+
+ elif type == "EndTag":
+ namespace = token["namespace"]
+ name = token["name"]
+ assert namespace is None or isinstance(namespace, text_type)
+ assert namespace != ""
+ assert isinstance(name, text_type)
+ assert name != ""
+ if (not namespace or namespace == namespaces["html"]) and name in voidElements:
+ assert False, "Void element reported as EndTag token: %(tag)s" % {"tag": name}
+ elif self.require_matching_tags:
+ start = open_elements.pop()
+ assert start == (namespace, name)
+
+ elif type == "Comment":
+ data = token["data"]
+ assert isinstance(data, text_type)
+
+ elif type in ("Characters", "SpaceCharacters"):
+ data = token["data"]
+ assert isinstance(data, text_type)
+ assert data != ""
+ if type == "SpaceCharacters":
+ assert data.strip(spaceCharacters) == ""
+
+ elif type == "Doctype":
+ name = token["name"]
+ assert name is None or isinstance(name, text_type)
+ assert token["publicId"] is None or isinstance(name, text_type)
+ assert token["systemId"] is None or isinstance(name, text_type)
+
+ elif type == "Entity":
+ assert isinstance(token["name"], text_type)
+
+ elif type == "SerializerError":
+ assert isinstance(token["data"], text_type)
+
+ else:
+ assert False, "Unknown token type: %(type)s" % {"type": type}
+
+ yield token
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/optionaltags.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/optionaltags.py
new file mode 100644
index 0000000000..4a865012c1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/optionaltags.py
@@ -0,0 +1,207 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from . import base
+
+
+class Filter(base.Filter):
+ """Removes optional tags from the token stream"""
+ def slider(self):
+ previous1 = previous2 = None
+ for token in self.source:
+ if previous1 is not None:
+ yield previous2, previous1, token
+ previous2 = previous1
+ previous1 = token
+ if previous1 is not None:
+ yield previous2, previous1, None
+
+ def __iter__(self):
+ for previous, token, next in self.slider():
+ type = token["type"]
+ if type == "StartTag":
+ if (token["data"] or
+ not self.is_optional_start(token["name"], previous, next)):
+ yield token
+ elif type == "EndTag":
+ if not self.is_optional_end(token["name"], next):
+ yield token
+ else:
+ yield token
+
+ def is_optional_start(self, tagname, previous, next):
+ type = next and next["type"] or None
+ if tagname in 'html':
+ # An html element's start tag may be omitted if the first thing
+ # inside the html element is not a space character or a comment.
+ return type not in ("Comment", "SpaceCharacters")
+ elif tagname == 'head':
+ # A head element's start tag may be omitted if the first thing
+ # inside the head element is an element.
+ # XXX: we also omit the start tag if the head element is empty
+ if type in ("StartTag", "EmptyTag"):
+ return True
+ elif type == "EndTag":
+ return next["name"] == "head"
+ elif tagname == 'body':
+ # A body element's start tag may be omitted if the first thing
+ # inside the body element is not a space character or a comment,
+ # except if the first thing inside the body element is a script
+ # or style element and the node immediately preceding the body
+ # element is a head element whose end tag has been omitted.
+ if type in ("Comment", "SpaceCharacters"):
+ return False
+ elif type == "StartTag":
+ # XXX: we do not look at the preceding event, so we never omit
+ # the body element's start tag if it's followed by a script or
+ # a style element.
+ return next["name"] not in ('script', 'style')
+ else:
+ return True
+ elif tagname == 'colgroup':
+ # A colgroup element's start tag may be omitted if the first thing
+ # inside the colgroup element is a col element, and if the element
+ # is not immediately preceded by another colgroup element whose
+ # end tag has been omitted.
+ if type in ("StartTag", "EmptyTag"):
+ # XXX: we do not look at the preceding event, so instead we never
+ # omit the colgroup element's end tag when it is immediately
+ # followed by another colgroup element. See is_optional_end.
+ return next["name"] == "col"
+ else:
+ return False
+ elif tagname == 'tbody':
+ # A tbody element's start tag may be omitted if the first thing
+ # inside the tbody element is a tr element, and if the element is
+ # not immediately preceded by a tbody, thead, or tfoot element
+ # whose end tag has been omitted.
+ if type == "StartTag":
+ # omit the thead and tfoot elements' end tag when they are
+ # immediately followed by a tbody element. See is_optional_end.
+ if previous and previous['type'] == 'EndTag' and \
+ previous['name'] in ('tbody', 'thead', 'tfoot'):
+ return False
+ return next["name"] == 'tr'
+ else:
+ return False
+ return False
+
+ def is_optional_end(self, tagname, next):
+ type = next and next["type"] or None
+ if tagname in ('html', 'head', 'body'):
+ # An html element's end tag may be omitted if the html element
+ # is not immediately followed by a space character or a comment.
+ return type not in ("Comment", "SpaceCharacters")
+ elif tagname in ('li', 'optgroup', 'tr'):
+ # A li element's end tag may be omitted if the li element is
+ # immediately followed by another li element or if there is
+ # no more content in the parent element.
+ # An optgroup element's end tag may be omitted if the optgroup
+ # element is immediately followed by another optgroup element,
+ # or if there is no more content in the parent element.
+ # A tr element's end tag may be omitted if the tr element is
+ # immediately followed by another tr element, or if there is
+ # no more content in the parent element.
+ if type == "StartTag":
+ return next["name"] == tagname
+ else:
+ return type == "EndTag" or type is None
+ elif tagname in ('dt', 'dd'):
+ # A dt element's end tag may be omitted if the dt element is
+ # immediately followed by another dt element or a dd element.
+ # A dd element's end tag may be omitted if the dd element is
+ # immediately followed by another dd element or a dt element,
+ # or if there is no more content in the parent element.
+ if type == "StartTag":
+ return next["name"] in ('dt', 'dd')
+ elif tagname == 'dd':
+ return type == "EndTag" or type is None
+ else:
+ return False
+ elif tagname == 'p':
+ # A p element's end tag may be omitted if the p element is
+ # immediately followed by an address, article, aside,
+ # blockquote, datagrid, dialog, dir, div, dl, fieldset,
+ # footer, form, h1, h2, h3, h4, h5, h6, header, hr, menu,
+ # nav, ol, p, pre, section, table, or ul, element, or if
+ # there is no more content in the parent element.
+ if type in ("StartTag", "EmptyTag"):
+ return next["name"] in ('address', 'article', 'aside',
+ 'blockquote', 'datagrid', 'dialog',
+ 'dir', 'div', 'dl', 'fieldset', 'footer',
+ 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
+ 'header', 'hr', 'menu', 'nav', 'ol',
+ 'p', 'pre', 'section', 'table', 'ul')
+ else:
+ return type == "EndTag" or type is None
+ elif tagname == 'option':
+ # An option element's end tag may be omitted if the option
+ # element is immediately followed by another option element,
+ # or if it is immediately followed by an <code>optgroup</code>
+ # element, or if there is no more content in the parent
+ # element.
+ if type == "StartTag":
+ return next["name"] in ('option', 'optgroup')
+ else:
+ return type == "EndTag" or type is None
+ elif tagname in ('rt', 'rp'):
+ # An rt element's end tag may be omitted if the rt element is
+ # immediately followed by an rt or rp element, or if there is
+ # no more content in the parent element.
+ # An rp element's end tag may be omitted if the rp element is
+ # immediately followed by an rt or rp element, or if there is
+ # no more content in the parent element.
+ if type == "StartTag":
+ return next["name"] in ('rt', 'rp')
+ else:
+ return type == "EndTag" or type is None
+ elif tagname == 'colgroup':
+ # A colgroup element's end tag may be omitted if the colgroup
+ # element is not immediately followed by a space character or
+ # a comment.
+ if type in ("Comment", "SpaceCharacters"):
+ return False
+ elif type == "StartTag":
+ # XXX: we also look for an immediately following colgroup
+ # element. See is_optional_start.
+ return next["name"] != 'colgroup'
+ else:
+ return True
+ elif tagname in ('thead', 'tbody'):
+ # A thead element's end tag may be omitted if the thead element
+ # is immediately followed by a tbody or tfoot element.
+ # A tbody element's end tag may be omitted if the tbody element
+ # is immediately followed by a tbody or tfoot element, or if
+ # there is no more content in the parent element.
+ # A tfoot element's end tag may be omitted if the tfoot element
+ # is immediately followed by a tbody element, or if there is no
+ # more content in the parent element.
+ # XXX: we never omit the end tag when the following element is
+ # a tbody. See is_optional_start.
+ if type == "StartTag":
+ return next["name"] in ['tbody', 'tfoot']
+ elif tagname == 'tbody':
+ return type == "EndTag" or type is None
+ else:
+ return False
+ elif tagname == 'tfoot':
+ # A tfoot element's end tag may be omitted if the tfoot element
+ # is immediately followed by a tbody element, or if there is no
+ # more content in the parent element.
+ # XXX: we never omit the end tag when the following element is
+ # a tbody. See is_optional_start.
+ if type == "StartTag":
+ return next["name"] == 'tbody'
+ else:
+ return type == "EndTag" or type is None
+ elif tagname in ('td', 'th'):
+ # A td element's end tag may be omitted if the td element is
+ # immediately followed by a td or th element, or if there is
+ # no more content in the parent element.
+ # A th element's end tag may be omitted if the th element is
+ # immediately followed by a td or th element, or if there is
+ # no more content in the parent element.
+ if type == "StartTag":
+ return next["name"] in ('td', 'th')
+ else:
+ return type == "EndTag" or type is None
+ return False
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/sanitizer.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/sanitizer.py
new file mode 100644
index 0000000000..70ef90665e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/sanitizer.py
@@ -0,0 +1,916 @@
+"""Deprecated from html5lib 1.1.
+
+See `here <https://github.com/html5lib/html5lib-python/issues/443>`_ for
+information about its deprecation; `Bleach <https://github.com/mozilla/bleach>`_
+is recommended as a replacement. Please let us know in the aforementioned issue
+if Bleach is unsuitable for your needs.
+
+"""
+from __future__ import absolute_import, division, unicode_literals
+
+import re
+import warnings
+from xml.sax.saxutils import escape, unescape
+
+from six.moves import urllib_parse as urlparse
+
+from . import base
+from ..constants import namespaces, prefixes
+
+__all__ = ["Filter"]
+
+
+_deprecation_msg = (
+ "html5lib's sanitizer is deprecated; see " +
+ "https://github.com/html5lib/html5lib-python/issues/443 and please let " +
+ "us know if Bleach is unsuitable for your needs"
+)
+
+warnings.warn(_deprecation_msg, DeprecationWarning)
+
+allowed_elements = frozenset((
+ (namespaces['html'], 'a'),
+ (namespaces['html'], 'abbr'),
+ (namespaces['html'], 'acronym'),
+ (namespaces['html'], 'address'),
+ (namespaces['html'], 'area'),
+ (namespaces['html'], 'article'),
+ (namespaces['html'], 'aside'),
+ (namespaces['html'], 'audio'),
+ (namespaces['html'], 'b'),
+ (namespaces['html'], 'big'),
+ (namespaces['html'], 'blockquote'),
+ (namespaces['html'], 'br'),
+ (namespaces['html'], 'button'),
+ (namespaces['html'], 'canvas'),
+ (namespaces['html'], 'caption'),
+ (namespaces['html'], 'center'),
+ (namespaces['html'], 'cite'),
+ (namespaces['html'], 'code'),
+ (namespaces['html'], 'col'),
+ (namespaces['html'], 'colgroup'),
+ (namespaces['html'], 'command'),
+ (namespaces['html'], 'datagrid'),
+ (namespaces['html'], 'datalist'),
+ (namespaces['html'], 'dd'),
+ (namespaces['html'], 'del'),
+ (namespaces['html'], 'details'),
+ (namespaces['html'], 'dfn'),
+ (namespaces['html'], 'dialog'),
+ (namespaces['html'], 'dir'),
+ (namespaces['html'], 'div'),
+ (namespaces['html'], 'dl'),
+ (namespaces['html'], 'dt'),
+ (namespaces['html'], 'em'),
+ (namespaces['html'], 'event-source'),
+ (namespaces['html'], 'fieldset'),
+ (namespaces['html'], 'figcaption'),
+ (namespaces['html'], 'figure'),
+ (namespaces['html'], 'footer'),
+ (namespaces['html'], 'font'),
+ (namespaces['html'], 'form'),
+ (namespaces['html'], 'header'),
+ (namespaces['html'], 'h1'),
+ (namespaces['html'], 'h2'),
+ (namespaces['html'], 'h3'),
+ (namespaces['html'], 'h4'),
+ (namespaces['html'], 'h5'),
+ (namespaces['html'], 'h6'),
+ (namespaces['html'], 'hr'),
+ (namespaces['html'], 'i'),
+ (namespaces['html'], 'img'),
+ (namespaces['html'], 'input'),
+ (namespaces['html'], 'ins'),
+ (namespaces['html'], 'keygen'),
+ (namespaces['html'], 'kbd'),
+ (namespaces['html'], 'label'),
+ (namespaces['html'], 'legend'),
+ (namespaces['html'], 'li'),
+ (namespaces['html'], 'm'),
+ (namespaces['html'], 'map'),
+ (namespaces['html'], 'menu'),
+ (namespaces['html'], 'meter'),
+ (namespaces['html'], 'multicol'),
+ (namespaces['html'], 'nav'),
+ (namespaces['html'], 'nextid'),
+ (namespaces['html'], 'ol'),
+ (namespaces['html'], 'output'),
+ (namespaces['html'], 'optgroup'),
+ (namespaces['html'], 'option'),
+ (namespaces['html'], 'p'),
+ (namespaces['html'], 'pre'),
+ (namespaces['html'], 'progress'),
+ (namespaces['html'], 'q'),
+ (namespaces['html'], 's'),
+ (namespaces['html'], 'samp'),
+ (namespaces['html'], 'section'),
+ (namespaces['html'], 'select'),
+ (namespaces['html'], 'small'),
+ (namespaces['html'], 'sound'),
+ (namespaces['html'], 'source'),
+ (namespaces['html'], 'spacer'),
+ (namespaces['html'], 'span'),
+ (namespaces['html'], 'strike'),
+ (namespaces['html'], 'strong'),
+ (namespaces['html'], 'sub'),
+ (namespaces['html'], 'sup'),
+ (namespaces['html'], 'table'),
+ (namespaces['html'], 'tbody'),
+ (namespaces['html'], 'td'),
+ (namespaces['html'], 'textarea'),
+ (namespaces['html'], 'time'),
+ (namespaces['html'], 'tfoot'),
+ (namespaces['html'], 'th'),
+ (namespaces['html'], 'thead'),
+ (namespaces['html'], 'tr'),
+ (namespaces['html'], 'tt'),
+ (namespaces['html'], 'u'),
+ (namespaces['html'], 'ul'),
+ (namespaces['html'], 'var'),
+ (namespaces['html'], 'video'),
+ (namespaces['mathml'], 'maction'),
+ (namespaces['mathml'], 'math'),
+ (namespaces['mathml'], 'merror'),
+ (namespaces['mathml'], 'mfrac'),
+ (namespaces['mathml'], 'mi'),
+ (namespaces['mathml'], 'mmultiscripts'),
+ (namespaces['mathml'], 'mn'),
+ (namespaces['mathml'], 'mo'),
+ (namespaces['mathml'], 'mover'),
+ (namespaces['mathml'], 'mpadded'),
+ (namespaces['mathml'], 'mphantom'),
+ (namespaces['mathml'], 'mprescripts'),
+ (namespaces['mathml'], 'mroot'),
+ (namespaces['mathml'], 'mrow'),
+ (namespaces['mathml'], 'mspace'),
+ (namespaces['mathml'], 'msqrt'),
+ (namespaces['mathml'], 'mstyle'),
+ (namespaces['mathml'], 'msub'),
+ (namespaces['mathml'], 'msubsup'),
+ (namespaces['mathml'], 'msup'),
+ (namespaces['mathml'], 'mtable'),
+ (namespaces['mathml'], 'mtd'),
+ (namespaces['mathml'], 'mtext'),
+ (namespaces['mathml'], 'mtr'),
+ (namespaces['mathml'], 'munder'),
+ (namespaces['mathml'], 'munderover'),
+ (namespaces['mathml'], 'none'),
+ (namespaces['svg'], 'a'),
+ (namespaces['svg'], 'animate'),
+ (namespaces['svg'], 'animateColor'),
+ (namespaces['svg'], 'animateMotion'),
+ (namespaces['svg'], 'animateTransform'),
+ (namespaces['svg'], 'clipPath'),
+ (namespaces['svg'], 'circle'),
+ (namespaces['svg'], 'defs'),
+ (namespaces['svg'], 'desc'),
+ (namespaces['svg'], 'ellipse'),
+ (namespaces['svg'], 'font-face'),
+ (namespaces['svg'], 'font-face-name'),
+ (namespaces['svg'], 'font-face-src'),
+ (namespaces['svg'], 'g'),
+ (namespaces['svg'], 'glyph'),
+ (namespaces['svg'], 'hkern'),
+ (namespaces['svg'], 'linearGradient'),
+ (namespaces['svg'], 'line'),
+ (namespaces['svg'], 'marker'),
+ (namespaces['svg'], 'metadata'),
+ (namespaces['svg'], 'missing-glyph'),
+ (namespaces['svg'], 'mpath'),
+ (namespaces['svg'], 'path'),
+ (namespaces['svg'], 'polygon'),
+ (namespaces['svg'], 'polyline'),
+ (namespaces['svg'], 'radialGradient'),
+ (namespaces['svg'], 'rect'),
+ (namespaces['svg'], 'set'),
+ (namespaces['svg'], 'stop'),
+ (namespaces['svg'], 'svg'),
+ (namespaces['svg'], 'switch'),
+ (namespaces['svg'], 'text'),
+ (namespaces['svg'], 'title'),
+ (namespaces['svg'], 'tspan'),
+ (namespaces['svg'], 'use'),
+))
+
+allowed_attributes = frozenset((
+ # HTML attributes
+ (None, 'abbr'),
+ (None, 'accept'),
+ (None, 'accept-charset'),
+ (None, 'accesskey'),
+ (None, 'action'),
+ (None, 'align'),
+ (None, 'alt'),
+ (None, 'autocomplete'),
+ (None, 'autofocus'),
+ (None, 'axis'),
+ (None, 'background'),
+ (None, 'balance'),
+ (None, 'bgcolor'),
+ (None, 'bgproperties'),
+ (None, 'border'),
+ (None, 'bordercolor'),
+ (None, 'bordercolordark'),
+ (None, 'bordercolorlight'),
+ (None, 'bottompadding'),
+ (None, 'cellpadding'),
+ (None, 'cellspacing'),
+ (None, 'ch'),
+ (None, 'challenge'),
+ (None, 'char'),
+ (None, 'charoff'),
+ (None, 'choff'),
+ (None, 'charset'),
+ (None, 'checked'),
+ (None, 'cite'),
+ (None, 'class'),
+ (None, 'clear'),
+ (None, 'color'),
+ (None, 'cols'),
+ (None, 'colspan'),
+ (None, 'compact'),
+ (None, 'contenteditable'),
+ (None, 'controls'),
+ (None, 'coords'),
+ (None, 'data'),
+ (None, 'datafld'),
+ (None, 'datapagesize'),
+ (None, 'datasrc'),
+ (None, 'datetime'),
+ (None, 'default'),
+ (None, 'delay'),
+ (None, 'dir'),
+ (None, 'disabled'),
+ (None, 'draggable'),
+ (None, 'dynsrc'),
+ (None, 'enctype'),
+ (None, 'end'),
+ (None, 'face'),
+ (None, 'for'),
+ (None, 'form'),
+ (None, 'frame'),
+ (None, 'galleryimg'),
+ (None, 'gutter'),
+ (None, 'headers'),
+ (None, 'height'),
+ (None, 'hidefocus'),
+ (None, 'hidden'),
+ (None, 'high'),
+ (None, 'href'),
+ (None, 'hreflang'),
+ (None, 'hspace'),
+ (None, 'icon'),
+ (None, 'id'),
+ (None, 'inputmode'),
+ (None, 'ismap'),
+ (None, 'keytype'),
+ (None, 'label'),
+ (None, 'leftspacing'),
+ (None, 'lang'),
+ (None, 'list'),
+ (None, 'longdesc'),
+ (None, 'loop'),
+ (None, 'loopcount'),
+ (None, 'loopend'),
+ (None, 'loopstart'),
+ (None, 'low'),
+ (None, 'lowsrc'),
+ (None, 'max'),
+ (None, 'maxlength'),
+ (None, 'media'),
+ (None, 'method'),
+ (None, 'min'),
+ (None, 'multiple'),
+ (None, 'name'),
+ (None, 'nohref'),
+ (None, 'noshade'),
+ (None, 'nowrap'),
+ (None, 'open'),
+ (None, 'optimum'),
+ (None, 'pattern'),
+ (None, 'ping'),
+ (None, 'point-size'),
+ (None, 'poster'),
+ (None, 'pqg'),
+ (None, 'preload'),
+ (None, 'prompt'),
+ (None, 'radiogroup'),
+ (None, 'readonly'),
+ (None, 'rel'),
+ (None, 'repeat-max'),
+ (None, 'repeat-min'),
+ (None, 'replace'),
+ (None, 'required'),
+ (None, 'rev'),
+ (None, 'rightspacing'),
+ (None, 'rows'),
+ (None, 'rowspan'),
+ (None, 'rules'),
+ (None, 'scope'),
+ (None, 'selected'),
+ (None, 'shape'),
+ (None, 'size'),
+ (None, 'span'),
+ (None, 'src'),
+ (None, 'start'),
+ (None, 'step'),
+ (None, 'style'),
+ (None, 'summary'),
+ (None, 'suppress'),
+ (None, 'tabindex'),
+ (None, 'target'),
+ (None, 'template'),
+ (None, 'title'),
+ (None, 'toppadding'),
+ (None, 'type'),
+ (None, 'unselectable'),
+ (None, 'usemap'),
+ (None, 'urn'),
+ (None, 'valign'),
+ (None, 'value'),
+ (None, 'variable'),
+ (None, 'volume'),
+ (None, 'vspace'),
+ (None, 'vrml'),
+ (None, 'width'),
+ (None, 'wrap'),
+ (namespaces['xml'], 'lang'),
+ # MathML attributes
+ (None, 'actiontype'),
+ (None, 'align'),
+ (None, 'columnalign'),
+ (None, 'columnalign'),
+ (None, 'columnalign'),
+ (None, 'columnlines'),
+ (None, 'columnspacing'),
+ (None, 'columnspan'),
+ (None, 'depth'),
+ (None, 'display'),
+ (None, 'displaystyle'),
+ (None, 'equalcolumns'),
+ (None, 'equalrows'),
+ (None, 'fence'),
+ (None, 'fontstyle'),
+ (None, 'fontweight'),
+ (None, 'frame'),
+ (None, 'height'),
+ (None, 'linethickness'),
+ (None, 'lspace'),
+ (None, 'mathbackground'),
+ (None, 'mathcolor'),
+ (None, 'mathvariant'),
+ (None, 'mathvariant'),
+ (None, 'maxsize'),
+ (None, 'minsize'),
+ (None, 'other'),
+ (None, 'rowalign'),
+ (None, 'rowalign'),
+ (None, 'rowalign'),
+ (None, 'rowlines'),
+ (None, 'rowspacing'),
+ (None, 'rowspan'),
+ (None, 'rspace'),
+ (None, 'scriptlevel'),
+ (None, 'selection'),
+ (None, 'separator'),
+ (None, 'stretchy'),
+ (None, 'width'),
+ (None, 'width'),
+ (namespaces['xlink'], 'href'),
+ (namespaces['xlink'], 'show'),
+ (namespaces['xlink'], 'type'),
+ # SVG attributes
+ (None, 'accent-height'),
+ (None, 'accumulate'),
+ (None, 'additive'),
+ (None, 'alphabetic'),
+ (None, 'arabic-form'),
+ (None, 'ascent'),
+ (None, 'attributeName'),
+ (None, 'attributeType'),
+ (None, 'baseProfile'),
+ (None, 'bbox'),
+ (None, 'begin'),
+ (None, 'by'),
+ (None, 'calcMode'),
+ (None, 'cap-height'),
+ (None, 'class'),
+ (None, 'clip-path'),
+ (None, 'color'),
+ (None, 'color-rendering'),
+ (None, 'content'),
+ (None, 'cx'),
+ (None, 'cy'),
+ (None, 'd'),
+ (None, 'dx'),
+ (None, 'dy'),
+ (None, 'descent'),
+ (None, 'display'),
+ (None, 'dur'),
+ (None, 'end'),
+ (None, 'fill'),
+ (None, 'fill-opacity'),
+ (None, 'fill-rule'),
+ (None, 'font-family'),
+ (None, 'font-size'),
+ (None, 'font-stretch'),
+ (None, 'font-style'),
+ (None, 'font-variant'),
+ (None, 'font-weight'),
+ (None, 'from'),
+ (None, 'fx'),
+ (None, 'fy'),
+ (None, 'g1'),
+ (None, 'g2'),
+ (None, 'glyph-name'),
+ (None, 'gradientUnits'),
+ (None, 'hanging'),
+ (None, 'height'),
+ (None, 'horiz-adv-x'),
+ (None, 'horiz-origin-x'),
+ (None, 'id'),
+ (None, 'ideographic'),
+ (None, 'k'),
+ (None, 'keyPoints'),
+ (None, 'keySplines'),
+ (None, 'keyTimes'),
+ (None, 'lang'),
+ (None, 'marker-end'),
+ (None, 'marker-mid'),
+ (None, 'marker-start'),
+ (None, 'markerHeight'),
+ (None, 'markerUnits'),
+ (None, 'markerWidth'),
+ (None, 'mathematical'),
+ (None, 'max'),
+ (None, 'min'),
+ (None, 'name'),
+ (None, 'offset'),
+ (None, 'opacity'),
+ (None, 'orient'),
+ (None, 'origin'),
+ (None, 'overline-position'),
+ (None, 'overline-thickness'),
+ (None, 'panose-1'),
+ (None, 'path'),
+ (None, 'pathLength'),
+ (None, 'points'),
+ (None, 'preserveAspectRatio'),
+ (None, 'r'),
+ (None, 'refX'),
+ (None, 'refY'),
+ (None, 'repeatCount'),
+ (None, 'repeatDur'),
+ (None, 'requiredExtensions'),
+ (None, 'requiredFeatures'),
+ (None, 'restart'),
+ (None, 'rotate'),
+ (None, 'rx'),
+ (None, 'ry'),
+ (None, 'slope'),
+ (None, 'stemh'),
+ (None, 'stemv'),
+ (None, 'stop-color'),
+ (None, 'stop-opacity'),
+ (None, 'strikethrough-position'),
+ (None, 'strikethrough-thickness'),
+ (None, 'stroke'),
+ (None, 'stroke-dasharray'),
+ (None, 'stroke-dashoffset'),
+ (None, 'stroke-linecap'),
+ (None, 'stroke-linejoin'),
+ (None, 'stroke-miterlimit'),
+ (None, 'stroke-opacity'),
+ (None, 'stroke-width'),
+ (None, 'systemLanguage'),
+ (None, 'target'),
+ (None, 'text-anchor'),
+ (None, 'to'),
+ (None, 'transform'),
+ (None, 'type'),
+ (None, 'u1'),
+ (None, 'u2'),
+ (None, 'underline-position'),
+ (None, 'underline-thickness'),
+ (None, 'unicode'),
+ (None, 'unicode-range'),
+ (None, 'units-per-em'),
+ (None, 'values'),
+ (None, 'version'),
+ (None, 'viewBox'),
+ (None, 'visibility'),
+ (None, 'width'),
+ (None, 'widths'),
+ (None, 'x'),
+ (None, 'x-height'),
+ (None, 'x1'),
+ (None, 'x2'),
+ (namespaces['xlink'], 'actuate'),
+ (namespaces['xlink'], 'arcrole'),
+ (namespaces['xlink'], 'href'),
+ (namespaces['xlink'], 'role'),
+ (namespaces['xlink'], 'show'),
+ (namespaces['xlink'], 'title'),
+ (namespaces['xlink'], 'type'),
+ (namespaces['xml'], 'base'),
+ (namespaces['xml'], 'lang'),
+ (namespaces['xml'], 'space'),
+ (None, 'y'),
+ (None, 'y1'),
+ (None, 'y2'),
+ (None, 'zoomAndPan'),
+))
+
+attr_val_is_uri = frozenset((
+ (None, 'href'),
+ (None, 'src'),
+ (None, 'cite'),
+ (None, 'action'),
+ (None, 'longdesc'),
+ (None, 'poster'),
+ (None, 'background'),
+ (None, 'datasrc'),
+ (None, 'dynsrc'),
+ (None, 'lowsrc'),
+ (None, 'ping'),
+ (namespaces['xlink'], 'href'),
+ (namespaces['xml'], 'base'),
+))
+
+svg_attr_val_allows_ref = frozenset((
+ (None, 'clip-path'),
+ (None, 'color-profile'),
+ (None, 'cursor'),
+ (None, 'fill'),
+ (None, 'filter'),
+ (None, 'marker'),
+ (None, 'marker-start'),
+ (None, 'marker-mid'),
+ (None, 'marker-end'),
+ (None, 'mask'),
+ (None, 'stroke'),
+))
+
+svg_allow_local_href = frozenset((
+ (None, 'altGlyph'),
+ (None, 'animate'),
+ (None, 'animateColor'),
+ (None, 'animateMotion'),
+ (None, 'animateTransform'),
+ (None, 'cursor'),
+ (None, 'feImage'),
+ (None, 'filter'),
+ (None, 'linearGradient'),
+ (None, 'pattern'),
+ (None, 'radialGradient'),
+ (None, 'textpath'),
+ (None, 'tref'),
+ (None, 'set'),
+ (None, 'use')
+))
+
+allowed_css_properties = frozenset((
+ 'azimuth',
+ 'background-color',
+ 'border-bottom-color',
+ 'border-collapse',
+ 'border-color',
+ 'border-left-color',
+ 'border-right-color',
+ 'border-top-color',
+ 'clear',
+ 'color',
+ 'cursor',
+ 'direction',
+ 'display',
+ 'elevation',
+ 'float',
+ 'font',
+ 'font-family',
+ 'font-size',
+ 'font-style',
+ 'font-variant',
+ 'font-weight',
+ 'height',
+ 'letter-spacing',
+ 'line-height',
+ 'overflow',
+ 'pause',
+ 'pause-after',
+ 'pause-before',
+ 'pitch',
+ 'pitch-range',
+ 'richness',
+ 'speak',
+ 'speak-header',
+ 'speak-numeral',
+ 'speak-punctuation',
+ 'speech-rate',
+ 'stress',
+ 'text-align',
+ 'text-decoration',
+ 'text-indent',
+ 'unicode-bidi',
+ 'vertical-align',
+ 'voice-family',
+ 'volume',
+ 'white-space',
+ 'width',
+))
+
+allowed_css_keywords = frozenset((
+ 'auto',
+ 'aqua',
+ 'black',
+ 'block',
+ 'blue',
+ 'bold',
+ 'both',
+ 'bottom',
+ 'brown',
+ 'center',
+ 'collapse',
+ 'dashed',
+ 'dotted',
+ 'fuchsia',
+ 'gray',
+ 'green',
+ '!important',
+ 'italic',
+ 'left',
+ 'lime',
+ 'maroon',
+ 'medium',
+ 'none',
+ 'navy',
+ 'normal',
+ 'nowrap',
+ 'olive',
+ 'pointer',
+ 'purple',
+ 'red',
+ 'right',
+ 'solid',
+ 'silver',
+ 'teal',
+ 'top',
+ 'transparent',
+ 'underline',
+ 'white',
+ 'yellow',
+))
+
+allowed_svg_properties = frozenset((
+ 'fill',
+ 'fill-opacity',
+ 'fill-rule',
+ 'stroke',
+ 'stroke-width',
+ 'stroke-linecap',
+ 'stroke-linejoin',
+ 'stroke-opacity',
+))
+
+allowed_protocols = frozenset((
+ 'ed2k',
+ 'ftp',
+ 'http',
+ 'https',
+ 'irc',
+ 'mailto',
+ 'news',
+ 'gopher',
+ 'nntp',
+ 'telnet',
+ 'webcal',
+ 'xmpp',
+ 'callto',
+ 'feed',
+ 'urn',
+ 'aim',
+ 'rsync',
+ 'tag',
+ 'ssh',
+ 'sftp',
+ 'rtsp',
+ 'afs',
+ 'data',
+))
+
+allowed_content_types = frozenset((
+ 'image/png',
+ 'image/jpeg',
+ 'image/gif',
+ 'image/webp',
+ 'image/bmp',
+ 'text/plain',
+))
+
+
+data_content_type = re.compile(r'''
+ ^
+ # Match a content type <application>/<type>
+ (?P<content_type>[-a-zA-Z0-9.]+/[-a-zA-Z0-9.]+)
+ # Match any character set and encoding
+ (?:(?:;charset=(?:[-a-zA-Z0-9]+)(?:;(?:base64))?)
+ |(?:;(?:base64))?(?:;charset=(?:[-a-zA-Z0-9]+))?)
+ # Assume the rest is data
+ ,.*
+ $
+ ''',
+ re.VERBOSE)
+
+
+class Filter(base.Filter):
+ """Sanitizes token stream of XHTML+MathML+SVG and of inline style attributes"""
+ def __init__(self,
+ source,
+ allowed_elements=allowed_elements,
+ allowed_attributes=allowed_attributes,
+ allowed_css_properties=allowed_css_properties,
+ allowed_css_keywords=allowed_css_keywords,
+ allowed_svg_properties=allowed_svg_properties,
+ allowed_protocols=allowed_protocols,
+ allowed_content_types=allowed_content_types,
+ attr_val_is_uri=attr_val_is_uri,
+ svg_attr_val_allows_ref=svg_attr_val_allows_ref,
+ svg_allow_local_href=svg_allow_local_href):
+ """Creates a Filter
+
+ :arg allowed_elements: set of elements to allow--everything else will
+ be escaped
+
+ :arg allowed_attributes: set of attributes to allow in
+ elements--everything else will be stripped
+
+ :arg allowed_css_properties: set of CSS properties to allow--everything
+ else will be stripped
+
+ :arg allowed_css_keywords: set of CSS keywords to allow--everything
+ else will be stripped
+
+ :arg allowed_svg_properties: set of SVG properties to allow--everything
+ else will be removed
+
+ :arg allowed_protocols: set of allowed protocols for URIs
+
+ :arg allowed_content_types: set of allowed content types for ``data`` URIs.
+
+ :arg attr_val_is_uri: set of attributes that have URI values--values
+ that have a scheme not listed in ``allowed_protocols`` are removed
+
+ :arg svg_attr_val_allows_ref: set of SVG attributes that can have
+ references
+
+ :arg svg_allow_local_href: set of SVG elements that can have local
+ hrefs--these are removed
+
+ """
+ super(Filter, self).__init__(source)
+
+ warnings.warn(_deprecation_msg, DeprecationWarning)
+
+ self.allowed_elements = allowed_elements
+ self.allowed_attributes = allowed_attributes
+ self.allowed_css_properties = allowed_css_properties
+ self.allowed_css_keywords = allowed_css_keywords
+ self.allowed_svg_properties = allowed_svg_properties
+ self.allowed_protocols = allowed_protocols
+ self.allowed_content_types = allowed_content_types
+ self.attr_val_is_uri = attr_val_is_uri
+ self.svg_attr_val_allows_ref = svg_attr_val_allows_ref
+ self.svg_allow_local_href = svg_allow_local_href
+
+ def __iter__(self):
+ for token in base.Filter.__iter__(self):
+ token = self.sanitize_token(token)
+ if token:
+ yield token
+
+ # Sanitize the +html+, escaping all elements not in ALLOWED_ELEMENTS, and
+ # stripping out all attributes not in ALLOWED_ATTRIBUTES. Style attributes
+ # are parsed, and a restricted set, specified by ALLOWED_CSS_PROPERTIES and
+ # ALLOWED_CSS_KEYWORDS, are allowed through. attributes in ATTR_VAL_IS_URI
+ # are scanned, and only URI schemes specified in ALLOWED_PROTOCOLS are
+ # allowed.
+ #
+ # sanitize_html('<script> do_nasty_stuff() </script>')
+ # => &lt;script> do_nasty_stuff() &lt;/script>
+ # sanitize_html('<a href="javascript: sucker();">Click here for $100</a>')
+ # => <a>Click here for $100</a>
+ def sanitize_token(self, token):
+
+ # accommodate filters which use token_type differently
+ token_type = token["type"]
+ if token_type in ("StartTag", "EndTag", "EmptyTag"):
+ name = token["name"]
+ namespace = token["namespace"]
+ if ((namespace, name) in self.allowed_elements or
+ (namespace is None and
+ (namespaces["html"], name) in self.allowed_elements)):
+ return self.allowed_token(token)
+ else:
+ return self.disallowed_token(token)
+ elif token_type == "Comment":
+ pass
+ else:
+ return token
+
+ def allowed_token(self, token):
+ if "data" in token:
+ attrs = token["data"]
+ attr_names = set(attrs.keys())
+
+ # Remove forbidden attributes
+ for to_remove in (attr_names - self.allowed_attributes):
+ del token["data"][to_remove]
+ attr_names.remove(to_remove)
+
+ # Remove attributes with disallowed URL values
+ for attr in (attr_names & self.attr_val_is_uri):
+ assert attr in attrs
+ # I don't have a clue where this regexp comes from or why it matches those
+ # characters, nor why we call unescape. I just know it's always been here.
+ # Should you be worried by this comment in a sanitizer? Yes. On the other hand, all
+ # this will do is remove *more* than it otherwise would.
+ val_unescaped = re.sub("[`\x00-\x20\x7f-\xa0\\s]+", '',
+ unescape(attrs[attr])).lower()
+ # remove replacement characters from unescaped characters
+ val_unescaped = val_unescaped.replace("\ufffd", "")
+ try:
+ uri = urlparse.urlparse(val_unescaped)
+ except ValueError:
+ uri = None
+ del attrs[attr]
+ if uri and uri.scheme:
+ if uri.scheme not in self.allowed_protocols:
+ del attrs[attr]
+ if uri.scheme == 'data':
+ m = data_content_type.match(uri.path)
+ if not m:
+ del attrs[attr]
+ elif m.group('content_type') not in self.allowed_content_types:
+ del attrs[attr]
+
+ for attr in self.svg_attr_val_allows_ref:
+ if attr in attrs:
+ attrs[attr] = re.sub(r'url\s*\(\s*[^#\s][^)]+?\)',
+ ' ',
+ unescape(attrs[attr]))
+ if (token["name"] in self.svg_allow_local_href and
+ (namespaces['xlink'], 'href') in attrs and re.search(r'^\s*[^#\s].*',
+ attrs[(namespaces['xlink'], 'href')])):
+ del attrs[(namespaces['xlink'], 'href')]
+ if (None, 'style') in attrs:
+ attrs[(None, 'style')] = self.sanitize_css(attrs[(None, 'style')])
+ token["data"] = attrs
+ return token
+
+ def disallowed_token(self, token):
+ token_type = token["type"]
+ if token_type == "EndTag":
+ token["data"] = "</%s>" % token["name"]
+ elif token["data"]:
+ assert token_type in ("StartTag", "EmptyTag")
+ attrs = []
+ for (ns, name), v in token["data"].items():
+ attrs.append(' %s="%s"' % (name if ns is None else "%s:%s" % (prefixes[ns], name), escape(v)))
+ token["data"] = "<%s%s>" % (token["name"], ''.join(attrs))
+ else:
+ token["data"] = "<%s>" % token["name"]
+ if token.get("selfClosing"):
+ token["data"] = token["data"][:-1] + "/>"
+
+ token["type"] = "Characters"
+
+ del token["name"]
+ return token
+
+ def sanitize_css(self, style):
+ # disallow urls
+ style = re.compile(r'url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ', style)
+
+ # gauntlet
+ if not re.match(r"""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style):
+ return ''
+ if not re.match(r"^\s*([-\w]+\s*:[^:;]*(;\s*|$))*$", style):
+ return ''
+
+ clean = []
+ for prop, value in re.findall(r"([-\w]+)\s*:\s*([^:;]*)", style):
+ if not value:
+ continue
+ if prop.lower() in self.allowed_css_properties:
+ clean.append(prop + ': ' + value + ';')
+ elif prop.split('-')[0].lower() in ['background', 'border', 'margin',
+ 'padding']:
+ for keyword in value.split():
+ if keyword not in self.allowed_css_keywords and \
+ not re.match(r"^(#[0-9a-fA-F]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$", keyword): # noqa
+ break
+ else:
+ clean.append(prop + ': ' + value + ';')
+ elif prop.lower() in self.allowed_svg_properties:
+ clean.append(prop + ': ' + value + ';')
+
+ return ' '.join(clean)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/whitespace.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/whitespace.py
new file mode 100644
index 0000000000..0d12584b45
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/filters/whitespace.py
@@ -0,0 +1,38 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import re
+
+from . import base
+from ..constants import rcdataElements, spaceCharacters
+spaceCharacters = "".join(spaceCharacters)
+
+SPACES_REGEX = re.compile("[%s]+" % spaceCharacters)
+
+
+class Filter(base.Filter):
+ """Collapses whitespace except in pre, textarea, and script elements"""
+ spacePreserveElements = frozenset(["pre", "textarea"] + list(rcdataElements))
+
+ def __iter__(self):
+ preserve = 0
+ for token in base.Filter.__iter__(self):
+ type = token["type"]
+ if type == "StartTag" \
+ and (preserve or token["name"] in self.spacePreserveElements):
+ preserve += 1
+
+ elif type == "EndTag" and preserve:
+ preserve -= 1
+
+ elif not preserve and type == "SpaceCharacters" and token["data"]:
+ # Test on token["data"] above to not introduce spaces where there were not
+ token["data"] = " "
+
+ elif not preserve and type == "Characters":
+ token["data"] = collapse_spaces(token["data"])
+
+ yield token
+
+
+def collapse_spaces(text):
+ return SPACES_REGEX.sub(' ', text)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/html5parser.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/html5parser.py
new file mode 100644
index 0000000000..74d829d984
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/html5parser.py
@@ -0,0 +1,2795 @@
+from __future__ import absolute_import, division, unicode_literals
+from six import with_metaclass, viewkeys
+
+import types
+
+from . import _inputstream
+from . import _tokenizer
+
+from . import treebuilders
+from .treebuilders.base import Marker
+
+from . import _utils
+from .constants import (
+ spaceCharacters, asciiUpper2Lower,
+ specialElements, headingElements, cdataElements, rcdataElements,
+ tokenTypes, tagTokenTypes,
+ namespaces,
+ htmlIntegrationPointElements, mathmlTextIntegrationPointElements,
+ adjustForeignAttributes as adjustForeignAttributesMap,
+ adjustMathMLAttributes, adjustSVGAttributes,
+ E,
+ _ReparseException
+)
+
+
+def parse(doc, treebuilder="etree", namespaceHTMLElements=True, **kwargs):
+ """Parse an HTML document as a string or file-like object into a tree
+
+ :arg doc: the document to parse as a string or file-like object
+
+ :arg treebuilder: the treebuilder to use when parsing
+
+ :arg namespaceHTMLElements: whether or not to namespace HTML elements
+
+ :returns: parsed tree
+
+ Example:
+
+ >>> from html5lib.html5parser import parse
+ >>> parse('<html><body><p>This is a doc</p></body></html>')
+ <Element u'{http://www.w3.org/1999/xhtml}html' at 0x7feac4909db0>
+
+ """
+ tb = treebuilders.getTreeBuilder(treebuilder)
+ p = HTMLParser(tb, namespaceHTMLElements=namespaceHTMLElements)
+ return p.parse(doc, **kwargs)
+
+
+def parseFragment(doc, container="div", treebuilder="etree", namespaceHTMLElements=True, **kwargs):
+ """Parse an HTML fragment as a string or file-like object into a tree
+
+ :arg doc: the fragment to parse as a string or file-like object
+
+ :arg container: the container context to parse the fragment in
+
+ :arg treebuilder: the treebuilder to use when parsing
+
+ :arg namespaceHTMLElements: whether or not to namespace HTML elements
+
+ :returns: parsed tree
+
+ Example:
+
+ >>> from html5lib.html5libparser import parseFragment
+ >>> parseFragment('<b>this is a fragment</b>')
+ <Element u'DOCUMENT_FRAGMENT' at 0x7feac484b090>
+
+ """
+ tb = treebuilders.getTreeBuilder(treebuilder)
+ p = HTMLParser(tb, namespaceHTMLElements=namespaceHTMLElements)
+ return p.parseFragment(doc, container=container, **kwargs)
+
+
+def method_decorator_metaclass(function):
+ class Decorated(type):
+ def __new__(meta, classname, bases, classDict):
+ for attributeName, attribute in classDict.items():
+ if isinstance(attribute, types.FunctionType):
+ attribute = function(attribute)
+
+ classDict[attributeName] = attribute
+ return type.__new__(meta, classname, bases, classDict)
+ return Decorated
+
+
+class HTMLParser(object):
+ """HTML parser
+
+ Generates a tree structure from a stream of (possibly malformed) HTML.
+
+ """
+
+ def __init__(self, tree=None, strict=False, namespaceHTMLElements=True, debug=False):
+ """
+ :arg tree: a treebuilder class controlling the type of tree that will be
+ returned. Built in treebuilders can be accessed through
+ html5lib.treebuilders.getTreeBuilder(treeType)
+
+ :arg strict: raise an exception when a parse error is encountered
+
+ :arg namespaceHTMLElements: whether or not to namespace HTML elements
+
+ :arg debug: whether or not to enable debug mode which logs things
+
+ Example:
+
+ >>> from html5lib.html5parser import HTMLParser
+ >>> parser = HTMLParser() # generates parser with etree builder
+ >>> parser = HTMLParser('lxml', strict=True) # generates parser with lxml builder which is strict
+
+ """
+
+ # Raise an exception on the first error encountered
+ self.strict = strict
+
+ if tree is None:
+ tree = treebuilders.getTreeBuilder("etree")
+ self.tree = tree(namespaceHTMLElements)
+ self.errors = []
+
+ self.phases = {name: cls(self, self.tree) for name, cls in
+ getPhases(debug).items()}
+
+ def _parse(self, stream, innerHTML=False, container="div", scripting=False, **kwargs):
+
+ self.innerHTMLMode = innerHTML
+ self.container = container
+ self.scripting = scripting
+ self.tokenizer = _tokenizer.HTMLTokenizer(stream, parser=self, **kwargs)
+ self.reset()
+
+ try:
+ self.mainLoop()
+ except _ReparseException:
+ self.reset()
+ self.mainLoop()
+
+ def reset(self):
+ self.tree.reset()
+ self.firstStartTag = False
+ self.errors = []
+ self.log = [] # only used with debug mode
+ # "quirks" / "limited quirks" / "no quirks"
+ self.compatMode = "no quirks"
+
+ if self.innerHTMLMode:
+ self.innerHTML = self.container.lower()
+
+ if self.innerHTML in cdataElements:
+ self.tokenizer.state = self.tokenizer.rcdataState
+ elif self.innerHTML in rcdataElements:
+ self.tokenizer.state = self.tokenizer.rawtextState
+ elif self.innerHTML == 'plaintext':
+ self.tokenizer.state = self.tokenizer.plaintextState
+ else:
+ # state already is data state
+ # self.tokenizer.state = self.tokenizer.dataState
+ pass
+ self.phase = self.phases["beforeHtml"]
+ self.phase.insertHtmlElement()
+ self.resetInsertionMode()
+ else:
+ self.innerHTML = False # pylint:disable=redefined-variable-type
+ self.phase = self.phases["initial"]
+
+ self.lastPhase = None
+
+ self.beforeRCDataPhase = None
+
+ self.framesetOK = True
+
+ @property
+ def documentEncoding(self):
+ """Name of the character encoding that was used to decode the input stream, or
+ :obj:`None` if that is not determined yet
+
+ """
+ if not hasattr(self, 'tokenizer'):
+ return None
+ return self.tokenizer.stream.charEncoding[0].name
+
+ def isHTMLIntegrationPoint(self, element):
+ if (element.name == "annotation-xml" and
+ element.namespace == namespaces["mathml"]):
+ return ("encoding" in element.attributes and
+ element.attributes["encoding"].translate(
+ asciiUpper2Lower) in
+ ("text/html", "application/xhtml+xml"))
+ else:
+ return (element.namespace, element.name) in htmlIntegrationPointElements
+
+ def isMathMLTextIntegrationPoint(self, element):
+ return (element.namespace, element.name) in mathmlTextIntegrationPointElements
+
+ def mainLoop(self):
+ CharactersToken = tokenTypes["Characters"]
+ SpaceCharactersToken = tokenTypes["SpaceCharacters"]
+ StartTagToken = tokenTypes["StartTag"]
+ EndTagToken = tokenTypes["EndTag"]
+ CommentToken = tokenTypes["Comment"]
+ DoctypeToken = tokenTypes["Doctype"]
+ ParseErrorToken = tokenTypes["ParseError"]
+
+ for token in self.tokenizer:
+ prev_token = None
+ new_token = token
+ while new_token is not None:
+ prev_token = new_token
+ currentNode = self.tree.openElements[-1] if self.tree.openElements else None
+ currentNodeNamespace = currentNode.namespace if currentNode else None
+ currentNodeName = currentNode.name if currentNode else None
+
+ type = new_token["type"]
+
+ if type == ParseErrorToken:
+ self.parseError(new_token["data"], new_token.get("datavars", {}))
+ new_token = None
+ else:
+ if (len(self.tree.openElements) == 0 or
+ currentNodeNamespace == self.tree.defaultNamespace or
+ (self.isMathMLTextIntegrationPoint(currentNode) and
+ ((type == StartTagToken and
+ token["name"] not in frozenset(["mglyph", "malignmark"])) or
+ type in (CharactersToken, SpaceCharactersToken))) or
+ (currentNodeNamespace == namespaces["mathml"] and
+ currentNodeName == "annotation-xml" and
+ type == StartTagToken and
+ token["name"] == "svg") or
+ (self.isHTMLIntegrationPoint(currentNode) and
+ type in (StartTagToken, CharactersToken, SpaceCharactersToken))):
+ phase = self.phase
+ else:
+ phase = self.phases["inForeignContent"]
+
+ if type == CharactersToken:
+ new_token = phase.processCharacters(new_token)
+ elif type == SpaceCharactersToken:
+ new_token = phase.processSpaceCharacters(new_token)
+ elif type == StartTagToken:
+ new_token = phase.processStartTag(new_token)
+ elif type == EndTagToken:
+ new_token = phase.processEndTag(new_token)
+ elif type == CommentToken:
+ new_token = phase.processComment(new_token)
+ elif type == DoctypeToken:
+ new_token = phase.processDoctype(new_token)
+
+ if (type == StartTagToken and prev_token["selfClosing"] and
+ not prev_token["selfClosingAcknowledged"]):
+ self.parseError("non-void-element-with-trailing-solidus",
+ {"name": prev_token["name"]})
+
+ # When the loop finishes it's EOF
+ reprocess = True
+ phases = []
+ while reprocess:
+ phases.append(self.phase)
+ reprocess = self.phase.processEOF()
+ if reprocess:
+ assert self.phase not in phases
+
+ def parse(self, stream, *args, **kwargs):
+ """Parse a HTML document into a well-formed tree
+
+ :arg stream: a file-like object or string containing the HTML to be parsed
+
+ The optional encoding parameter must be a string that indicates
+ the encoding. If specified, that encoding will be used,
+ regardless of any BOM or later declaration (such as in a meta
+ element).
+
+ :arg scripting: treat noscript elements as if JavaScript was turned on
+
+ :returns: parsed tree
+
+ Example:
+
+ >>> from html5lib.html5parser import HTMLParser
+ >>> parser = HTMLParser()
+ >>> parser.parse('<html><body><p>This is a doc</p></body></html>')
+ <Element u'{http://www.w3.org/1999/xhtml}html' at 0x7feac4909db0>
+
+ """
+ self._parse(stream, False, None, *args, **kwargs)
+ return self.tree.getDocument()
+
+ def parseFragment(self, stream, *args, **kwargs):
+ """Parse a HTML fragment into a well-formed tree fragment
+
+ :arg container: name of the element we're setting the innerHTML
+ property if set to None, default to 'div'
+
+ :arg stream: a file-like object or string containing the HTML to be parsed
+
+ The optional encoding parameter must be a string that indicates
+ the encoding. If specified, that encoding will be used,
+ regardless of any BOM or later declaration (such as in a meta
+ element)
+
+ :arg scripting: treat noscript elements as if JavaScript was turned on
+
+ :returns: parsed tree
+
+ Example:
+
+ >>> from html5lib.html5libparser import HTMLParser
+ >>> parser = HTMLParser()
+ >>> parser.parseFragment('<b>this is a fragment</b>')
+ <Element u'DOCUMENT_FRAGMENT' at 0x7feac484b090>
+
+ """
+ self._parse(stream, True, *args, **kwargs)
+ return self.tree.getFragment()
+
+ def parseError(self, errorcode="XXX-undefined-error", datavars=None):
+ # XXX The idea is to make errorcode mandatory.
+ if datavars is None:
+ datavars = {}
+ self.errors.append((self.tokenizer.stream.position(), errorcode, datavars))
+ if self.strict:
+ raise ParseError(E[errorcode] % datavars)
+
+ def adjustMathMLAttributes(self, token):
+ adjust_attributes(token, adjustMathMLAttributes)
+
+ def adjustSVGAttributes(self, token):
+ adjust_attributes(token, adjustSVGAttributes)
+
+ def adjustForeignAttributes(self, token):
+ adjust_attributes(token, adjustForeignAttributesMap)
+
+ def reparseTokenNormal(self, token):
+ # pylint:disable=unused-argument
+ self.parser.phase()
+
+ def resetInsertionMode(self):
+ # The name of this method is mostly historical. (It's also used in the
+ # specification.)
+ last = False
+ newModes = {
+ "select": "inSelect",
+ "td": "inCell",
+ "th": "inCell",
+ "tr": "inRow",
+ "tbody": "inTableBody",
+ "thead": "inTableBody",
+ "tfoot": "inTableBody",
+ "caption": "inCaption",
+ "colgroup": "inColumnGroup",
+ "table": "inTable",
+ "head": "inBody",
+ "body": "inBody",
+ "frameset": "inFrameset",
+ "html": "beforeHead"
+ }
+ for node in self.tree.openElements[::-1]:
+ nodeName = node.name
+ new_phase = None
+ if node == self.tree.openElements[0]:
+ assert self.innerHTML
+ last = True
+ nodeName = self.innerHTML
+ # Check for conditions that should only happen in the innerHTML
+ # case
+ if nodeName in ("select", "colgroup", "head", "html"):
+ assert self.innerHTML
+
+ if not last and node.namespace != self.tree.defaultNamespace:
+ continue
+
+ if nodeName in newModes:
+ new_phase = self.phases[newModes[nodeName]]
+ break
+ elif last:
+ new_phase = self.phases["inBody"]
+ break
+
+ self.phase = new_phase
+
+ def parseRCDataRawtext(self, token, contentType):
+ # Generic RCDATA/RAWTEXT Parsing algorithm
+ assert contentType in ("RAWTEXT", "RCDATA")
+
+ self.tree.insertElement(token)
+
+ if contentType == "RAWTEXT":
+ self.tokenizer.state = self.tokenizer.rawtextState
+ else:
+ self.tokenizer.state = self.tokenizer.rcdataState
+
+ self.originalPhase = self.phase
+
+ self.phase = self.phases["text"]
+
+
+@_utils.memoize
+def getPhases(debug):
+ def log(function):
+ """Logger that records which phase processes each token"""
+ type_names = {value: key for key, value in tokenTypes.items()}
+
+ def wrapped(self, *args, **kwargs):
+ if function.__name__.startswith("process") and len(args) > 0:
+ token = args[0]
+ info = {"type": type_names[token['type']]}
+ if token['type'] in tagTokenTypes:
+ info["name"] = token['name']
+
+ self.parser.log.append((self.parser.tokenizer.state.__name__,
+ self.parser.phase.__class__.__name__,
+ self.__class__.__name__,
+ function.__name__,
+ info))
+ return function(self, *args, **kwargs)
+ else:
+ return function(self, *args, **kwargs)
+ return wrapped
+
+ def getMetaclass(use_metaclass, metaclass_func):
+ if use_metaclass:
+ return method_decorator_metaclass(metaclass_func)
+ else:
+ return type
+
+ # pylint:disable=unused-argument
+ class Phase(with_metaclass(getMetaclass(debug, log))):
+ """Base class for helper object that implements each phase of processing
+ """
+ __slots__ = ("parser", "tree", "__startTagCache", "__endTagCache")
+
+ def __init__(self, parser, tree):
+ self.parser = parser
+ self.tree = tree
+ self.__startTagCache = {}
+ self.__endTagCache = {}
+
+ def processEOF(self):
+ raise NotImplementedError
+
+ def processComment(self, token):
+ # For most phases the following is correct. Where it's not it will be
+ # overridden.
+ self.tree.insertComment(token, self.tree.openElements[-1])
+
+ def processDoctype(self, token):
+ self.parser.parseError("unexpected-doctype")
+
+ def processCharacters(self, token):
+ self.tree.insertText(token["data"])
+
+ def processSpaceCharacters(self, token):
+ self.tree.insertText(token["data"])
+
+ def processStartTag(self, token):
+ # Note the caching is done here rather than BoundMethodDispatcher as doing it there
+ # requires a circular reference to the Phase, and this ends up with a significant
+ # (CPython 2.7, 3.8) GC cost when parsing many short inputs
+ name = token["name"]
+ # In Py2, using `in` is quicker in general than try/except KeyError
+ # In Py3, `in` is quicker when there are few cache hits (typically short inputs)
+ if name in self.__startTagCache:
+ func = self.__startTagCache[name]
+ else:
+ func = self.__startTagCache[name] = self.startTagHandler[name]
+ # bound the cache size in case we get loads of unknown tags
+ while len(self.__startTagCache) > len(self.startTagHandler) * 1.1:
+ # this makes the eviction policy random on Py < 3.7 and FIFO >= 3.7
+ self.__startTagCache.pop(next(iter(self.__startTagCache)))
+ return func(token)
+
+ def startTagHtml(self, token):
+ if not self.parser.firstStartTag and token["name"] == "html":
+ self.parser.parseError("non-html-root")
+ # XXX Need a check here to see if the first start tag token emitted is
+ # this token... If it's not, invoke self.parser.parseError().
+ for attr, value in token["data"].items():
+ if attr not in self.tree.openElements[0].attributes:
+ self.tree.openElements[0].attributes[attr] = value
+ self.parser.firstStartTag = False
+
+ def processEndTag(self, token):
+ # Note the caching is done here rather than BoundMethodDispatcher as doing it there
+ # requires a circular reference to the Phase, and this ends up with a significant
+ # (CPython 2.7, 3.8) GC cost when parsing many short inputs
+ name = token["name"]
+ # In Py2, using `in` is quicker in general than try/except KeyError
+ # In Py3, `in` is quicker when there are few cache hits (typically short inputs)
+ if name in self.__endTagCache:
+ func = self.__endTagCache[name]
+ else:
+ func = self.__endTagCache[name] = self.endTagHandler[name]
+ # bound the cache size in case we get loads of unknown tags
+ while len(self.__endTagCache) > len(self.endTagHandler) * 1.1:
+ # this makes the eviction policy random on Py < 3.7 and FIFO >= 3.7
+ self.__endTagCache.pop(next(iter(self.__endTagCache)))
+ return func(token)
+
+ class InitialPhase(Phase):
+ __slots__ = tuple()
+
+ def processSpaceCharacters(self, token):
+ pass
+
+ def processComment(self, token):
+ self.tree.insertComment(token, self.tree.document)
+
+ def processDoctype(self, token):
+ name = token["name"]
+ publicId = token["publicId"]
+ systemId = token["systemId"]
+ correct = token["correct"]
+
+ if (name != "html" or publicId is not None or
+ systemId is not None and systemId != "about:legacy-compat"):
+ self.parser.parseError("unknown-doctype")
+
+ if publicId is None:
+ publicId = ""
+
+ self.tree.insertDoctype(token)
+
+ if publicId != "":
+ publicId = publicId.translate(asciiUpper2Lower)
+
+ if (not correct or token["name"] != "html" or
+ publicId.startswith(
+ ("+//silmaril//dtd html pro v0r11 19970101//",
+ "-//advasoft ltd//dtd html 3.0 aswedit + extensions//",
+ "-//as//dtd html 3.0 aswedit + extensions//",
+ "-//ietf//dtd html 2.0 level 1//",
+ "-//ietf//dtd html 2.0 level 2//",
+ "-//ietf//dtd html 2.0 strict level 1//",
+ "-//ietf//dtd html 2.0 strict level 2//",
+ "-//ietf//dtd html 2.0 strict//",
+ "-//ietf//dtd html 2.0//",
+ "-//ietf//dtd html 2.1e//",
+ "-//ietf//dtd html 3.0//",
+ "-//ietf//dtd html 3.2 final//",
+ "-//ietf//dtd html 3.2//",
+ "-//ietf//dtd html 3//",
+ "-//ietf//dtd html level 0//",
+ "-//ietf//dtd html level 1//",
+ "-//ietf//dtd html level 2//",
+ "-//ietf//dtd html level 3//",
+ "-//ietf//dtd html strict level 0//",
+ "-//ietf//dtd html strict level 1//",
+ "-//ietf//dtd html strict level 2//",
+ "-//ietf//dtd html strict level 3//",
+ "-//ietf//dtd html strict//",
+ "-//ietf//dtd html//",
+ "-//metrius//dtd metrius presentational//",
+ "-//microsoft//dtd internet explorer 2.0 html strict//",
+ "-//microsoft//dtd internet explorer 2.0 html//",
+ "-//microsoft//dtd internet explorer 2.0 tables//",
+ "-//microsoft//dtd internet explorer 3.0 html strict//",
+ "-//microsoft//dtd internet explorer 3.0 html//",
+ "-//microsoft//dtd internet explorer 3.0 tables//",
+ "-//netscape comm. corp.//dtd html//",
+ "-//netscape comm. corp.//dtd strict html//",
+ "-//o'reilly and associates//dtd html 2.0//",
+ "-//o'reilly and associates//dtd html extended 1.0//",
+ "-//o'reilly and associates//dtd html extended relaxed 1.0//",
+ "-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//",
+ "-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//",
+ "-//spyglass//dtd html 2.0 extended//",
+ "-//sq//dtd html 2.0 hotmetal + extensions//",
+ "-//sun microsystems corp.//dtd hotjava html//",
+ "-//sun microsystems corp.//dtd hotjava strict html//",
+ "-//w3c//dtd html 3 1995-03-24//",
+ "-//w3c//dtd html 3.2 draft//",
+ "-//w3c//dtd html 3.2 final//",
+ "-//w3c//dtd html 3.2//",
+ "-//w3c//dtd html 3.2s draft//",
+ "-//w3c//dtd html 4.0 frameset//",
+ "-//w3c//dtd html 4.0 transitional//",
+ "-//w3c//dtd html experimental 19960712//",
+ "-//w3c//dtd html experimental 970421//",
+ "-//w3c//dtd w3 html//",
+ "-//w3o//dtd w3 html 3.0//",
+ "-//webtechs//dtd mozilla html 2.0//",
+ "-//webtechs//dtd mozilla html//")) or
+ publicId in ("-//w3o//dtd w3 html strict 3.0//en//",
+ "-/w3c/dtd html 4.0 transitional/en",
+ "html") or
+ publicId.startswith(
+ ("-//w3c//dtd html 4.01 frameset//",
+ "-//w3c//dtd html 4.01 transitional//")) and
+ systemId is None or
+ systemId and systemId.lower() == "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd"):
+ self.parser.compatMode = "quirks"
+ elif (publicId.startswith(
+ ("-//w3c//dtd xhtml 1.0 frameset//",
+ "-//w3c//dtd xhtml 1.0 transitional//")) or
+ publicId.startswith(
+ ("-//w3c//dtd html 4.01 frameset//",
+ "-//w3c//dtd html 4.01 transitional//")) and
+ systemId is not None):
+ self.parser.compatMode = "limited quirks"
+
+ self.parser.phase = self.parser.phases["beforeHtml"]
+
+ def anythingElse(self):
+ self.parser.compatMode = "quirks"
+ self.parser.phase = self.parser.phases["beforeHtml"]
+
+ def processCharacters(self, token):
+ self.parser.parseError("expected-doctype-but-got-chars")
+ self.anythingElse()
+ return token
+
+ def processStartTag(self, token):
+ self.parser.parseError("expected-doctype-but-got-start-tag",
+ {"name": token["name"]})
+ self.anythingElse()
+ return token
+
+ def processEndTag(self, token):
+ self.parser.parseError("expected-doctype-but-got-end-tag",
+ {"name": token["name"]})
+ self.anythingElse()
+ return token
+
+ def processEOF(self):
+ self.parser.parseError("expected-doctype-but-got-eof")
+ self.anythingElse()
+ return True
+
+ class BeforeHtmlPhase(Phase):
+ __slots__ = tuple()
+
+ # helper methods
+ def insertHtmlElement(self):
+ self.tree.insertRoot(impliedTagToken("html", "StartTag"))
+ self.parser.phase = self.parser.phases["beforeHead"]
+
+ # other
+ def processEOF(self):
+ self.insertHtmlElement()
+ return True
+
+ def processComment(self, token):
+ self.tree.insertComment(token, self.tree.document)
+
+ def processSpaceCharacters(self, token):
+ pass
+
+ def processCharacters(self, token):
+ self.insertHtmlElement()
+ return token
+
+ def processStartTag(self, token):
+ if token["name"] == "html":
+ self.parser.firstStartTag = True
+ self.insertHtmlElement()
+ return token
+
+ def processEndTag(self, token):
+ if token["name"] not in ("head", "body", "html", "br"):
+ self.parser.parseError("unexpected-end-tag-before-html",
+ {"name": token["name"]})
+ else:
+ self.insertHtmlElement()
+ return token
+
+ class BeforeHeadPhase(Phase):
+ __slots__ = tuple()
+
+ def processEOF(self):
+ self.startTagHead(impliedTagToken("head", "StartTag"))
+ return True
+
+ def processSpaceCharacters(self, token):
+ pass
+
+ def processCharacters(self, token):
+ self.startTagHead(impliedTagToken("head", "StartTag"))
+ return token
+
+ def startTagHtml(self, token):
+ return self.parser.phases["inBody"].processStartTag(token)
+
+ def startTagHead(self, token):
+ self.tree.insertElement(token)
+ self.tree.headPointer = self.tree.openElements[-1]
+ self.parser.phase = self.parser.phases["inHead"]
+
+ def startTagOther(self, token):
+ self.startTagHead(impliedTagToken("head", "StartTag"))
+ return token
+
+ def endTagImplyHead(self, token):
+ self.startTagHead(impliedTagToken("head", "StartTag"))
+ return token
+
+ def endTagOther(self, token):
+ self.parser.parseError("end-tag-after-implied-root",
+ {"name": token["name"]})
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", startTagHtml),
+ ("head", startTagHead)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ (("head", "body", "html", "br"), endTagImplyHead)
+ ])
+ endTagHandler.default = endTagOther
+
+ class InHeadPhase(Phase):
+ __slots__ = tuple()
+
+ # the real thing
+ def processEOF(self):
+ self.anythingElse()
+ return True
+
+ def processCharacters(self, token):
+ self.anythingElse()
+ return token
+
+ def startTagHtml(self, token):
+ return self.parser.phases["inBody"].processStartTag(token)
+
+ def startTagHead(self, token):
+ self.parser.parseError("two-heads-are-not-better-than-one")
+
+ def startTagBaseLinkCommand(self, token):
+ self.tree.insertElement(token)
+ self.tree.openElements.pop()
+ token["selfClosingAcknowledged"] = True
+
+ def startTagMeta(self, token):
+ self.tree.insertElement(token)
+ self.tree.openElements.pop()
+ token["selfClosingAcknowledged"] = True
+
+ attributes = token["data"]
+ if self.parser.tokenizer.stream.charEncoding[1] == "tentative":
+ if "charset" in attributes:
+ self.parser.tokenizer.stream.changeEncoding(attributes["charset"])
+ elif ("content" in attributes and
+ "http-equiv" in attributes and
+ attributes["http-equiv"].lower() == "content-type"):
+ # Encoding it as UTF-8 here is a hack, as really we should pass
+ # the abstract Unicode string, and just use the
+ # ContentAttrParser on that, but using UTF-8 allows all chars
+ # to be encoded and as a ASCII-superset works.
+ data = _inputstream.EncodingBytes(attributes["content"].encode("utf-8"))
+ parser = _inputstream.ContentAttrParser(data)
+ codec = parser.parse()
+ self.parser.tokenizer.stream.changeEncoding(codec)
+
+ def startTagTitle(self, token):
+ self.parser.parseRCDataRawtext(token, "RCDATA")
+
+ def startTagNoFramesStyle(self, token):
+ # Need to decide whether to implement the scripting-disabled case
+ self.parser.parseRCDataRawtext(token, "RAWTEXT")
+
+ def startTagNoscript(self, token):
+ if self.parser.scripting:
+ self.parser.parseRCDataRawtext(token, "RAWTEXT")
+ else:
+ self.tree.insertElement(token)
+ self.parser.phase = self.parser.phases["inHeadNoscript"]
+
+ def startTagScript(self, token):
+ self.tree.insertElement(token)
+ self.parser.tokenizer.state = self.parser.tokenizer.scriptDataState
+ self.parser.originalPhase = self.parser.phase
+ self.parser.phase = self.parser.phases["text"]
+
+ def startTagOther(self, token):
+ self.anythingElse()
+ return token
+
+ def endTagHead(self, token):
+ node = self.parser.tree.openElements.pop()
+ assert node.name == "head", "Expected head got %s" % node.name
+ self.parser.phase = self.parser.phases["afterHead"]
+
+ def endTagHtmlBodyBr(self, token):
+ self.anythingElse()
+ return token
+
+ def endTagOther(self, token):
+ self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
+
+ def anythingElse(self):
+ self.endTagHead(impliedTagToken("head"))
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", startTagHtml),
+ ("title", startTagTitle),
+ (("noframes", "style"), startTagNoFramesStyle),
+ ("noscript", startTagNoscript),
+ ("script", startTagScript),
+ (("base", "basefont", "bgsound", "command", "link"),
+ startTagBaseLinkCommand),
+ ("meta", startTagMeta),
+ ("head", startTagHead)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ ("head", endTagHead),
+ (("br", "html", "body"), endTagHtmlBodyBr)
+ ])
+ endTagHandler.default = endTagOther
+
+ class InHeadNoscriptPhase(Phase):
+ __slots__ = tuple()
+
+ def processEOF(self):
+ self.parser.parseError("eof-in-head-noscript")
+ self.anythingElse()
+ return True
+
+ def processComment(self, token):
+ return self.parser.phases["inHead"].processComment(token)
+
+ def processCharacters(self, token):
+ self.parser.parseError("char-in-head-noscript")
+ self.anythingElse()
+ return token
+
+ def processSpaceCharacters(self, token):
+ return self.parser.phases["inHead"].processSpaceCharacters(token)
+
+ def startTagHtml(self, token):
+ return self.parser.phases["inBody"].processStartTag(token)
+
+ def startTagBaseLinkCommand(self, token):
+ return self.parser.phases["inHead"].processStartTag(token)
+
+ def startTagHeadNoscript(self, token):
+ self.parser.parseError("unexpected-start-tag", {"name": token["name"]})
+
+ def startTagOther(self, token):
+ self.parser.parseError("unexpected-inhead-noscript-tag", {"name": token["name"]})
+ self.anythingElse()
+ return token
+
+ def endTagNoscript(self, token):
+ node = self.parser.tree.openElements.pop()
+ assert node.name == "noscript", "Expected noscript got %s" % node.name
+ self.parser.phase = self.parser.phases["inHead"]
+
+ def endTagBr(self, token):
+ self.parser.parseError("unexpected-inhead-noscript-tag", {"name": token["name"]})
+ self.anythingElse()
+ return token
+
+ def endTagOther(self, token):
+ self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
+
+ def anythingElse(self):
+ # Caller must raise parse error first!
+ self.endTagNoscript(impliedTagToken("noscript"))
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", startTagHtml),
+ (("basefont", "bgsound", "link", "meta", "noframes", "style"), startTagBaseLinkCommand),
+ (("head", "noscript"), startTagHeadNoscript),
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ ("noscript", endTagNoscript),
+ ("br", endTagBr),
+ ])
+ endTagHandler.default = endTagOther
+
+ class AfterHeadPhase(Phase):
+ __slots__ = tuple()
+
+ def processEOF(self):
+ self.anythingElse()
+ return True
+
+ def processCharacters(self, token):
+ self.anythingElse()
+ return token
+
+ def startTagHtml(self, token):
+ return self.parser.phases["inBody"].processStartTag(token)
+
+ def startTagBody(self, token):
+ self.parser.framesetOK = False
+ self.tree.insertElement(token)
+ self.parser.phase = self.parser.phases["inBody"]
+
+ def startTagFrameset(self, token):
+ self.tree.insertElement(token)
+ self.parser.phase = self.parser.phases["inFrameset"]
+
+ def startTagFromHead(self, token):
+ self.parser.parseError("unexpected-start-tag-out-of-my-head",
+ {"name": token["name"]})
+ self.tree.openElements.append(self.tree.headPointer)
+ self.parser.phases["inHead"].processStartTag(token)
+ for node in self.tree.openElements[::-1]:
+ if node.name == "head":
+ self.tree.openElements.remove(node)
+ break
+
+ def startTagHead(self, token):
+ self.parser.parseError("unexpected-start-tag", {"name": token["name"]})
+
+ def startTagOther(self, token):
+ self.anythingElse()
+ return token
+
+ def endTagHtmlBodyBr(self, token):
+ self.anythingElse()
+ return token
+
+ def endTagOther(self, token):
+ self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
+
+ def anythingElse(self):
+ self.tree.insertElement(impliedTagToken("body", "StartTag"))
+ self.parser.phase = self.parser.phases["inBody"]
+ self.parser.framesetOK = True
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", startTagHtml),
+ ("body", startTagBody),
+ ("frameset", startTagFrameset),
+ (("base", "basefont", "bgsound", "link", "meta", "noframes", "script",
+ "style", "title"),
+ startTagFromHead),
+ ("head", startTagHead)
+ ])
+ startTagHandler.default = startTagOther
+ endTagHandler = _utils.MethodDispatcher([(("body", "html", "br"),
+ endTagHtmlBodyBr)])
+ endTagHandler.default = endTagOther
+
+ class InBodyPhase(Phase):
+ # http://www.whatwg.org/specs/web-apps/current-work/#parsing-main-inbody
+ # the really-really-really-very crazy mode
+ __slots__ = ("processSpaceCharacters",)
+
+ def __init__(self, *args, **kwargs):
+ super(InBodyPhase, self).__init__(*args, **kwargs)
+ # Set this to the default handler
+ self.processSpaceCharacters = self.processSpaceCharactersNonPre
+
+ def isMatchingFormattingElement(self, node1, node2):
+ return (node1.name == node2.name and
+ node1.namespace == node2.namespace and
+ node1.attributes == node2.attributes)
+
+ # helper
+ def addFormattingElement(self, token):
+ self.tree.insertElement(token)
+ element = self.tree.openElements[-1]
+
+ matchingElements = []
+ for node in self.tree.activeFormattingElements[::-1]:
+ if node is Marker:
+ break
+ elif self.isMatchingFormattingElement(node, element):
+ matchingElements.append(node)
+
+ assert len(matchingElements) <= 3
+ if len(matchingElements) == 3:
+ self.tree.activeFormattingElements.remove(matchingElements[-1])
+ self.tree.activeFormattingElements.append(element)
+
+ # the real deal
+ def processEOF(self):
+ allowed_elements = frozenset(("dd", "dt", "li", "p", "tbody", "td",
+ "tfoot", "th", "thead", "tr", "body",
+ "html"))
+ for node in self.tree.openElements[::-1]:
+ if node.name not in allowed_elements:
+ self.parser.parseError("expected-closing-tag-but-got-eof")
+ break
+ # Stop parsing
+
+ def processSpaceCharactersDropNewline(self, token):
+ # Sometimes (start of <pre>, <listing>, and <textarea> blocks) we
+ # want to drop leading newlines
+ data = token["data"]
+ self.processSpaceCharacters = self.processSpaceCharactersNonPre
+ if (data.startswith("\n") and
+ self.tree.openElements[-1].name in ("pre", "listing", "textarea") and
+ not self.tree.openElements[-1].hasContent()):
+ data = data[1:]
+ if data:
+ self.tree.reconstructActiveFormattingElements()
+ self.tree.insertText(data)
+
+ def processCharacters(self, token):
+ if token["data"] == "\u0000":
+ # The tokenizer should always emit null on its own
+ return
+ self.tree.reconstructActiveFormattingElements()
+ self.tree.insertText(token["data"])
+ # This must be bad for performance
+ if (self.parser.framesetOK and
+ any([char not in spaceCharacters
+ for char in token["data"]])):
+ self.parser.framesetOK = False
+
+ def processSpaceCharactersNonPre(self, token):
+ self.tree.reconstructActiveFormattingElements()
+ self.tree.insertText(token["data"])
+
+ def startTagProcessInHead(self, token):
+ return self.parser.phases["inHead"].processStartTag(token)
+
+ def startTagBody(self, token):
+ self.parser.parseError("unexpected-start-tag", {"name": "body"})
+ if (len(self.tree.openElements) == 1 or
+ self.tree.openElements[1].name != "body"):
+ assert self.parser.innerHTML
+ else:
+ self.parser.framesetOK = False
+ for attr, value in token["data"].items():
+ if attr not in self.tree.openElements[1].attributes:
+ self.tree.openElements[1].attributes[attr] = value
+
+ def startTagFrameset(self, token):
+ self.parser.parseError("unexpected-start-tag", {"name": "frameset"})
+ if (len(self.tree.openElements) == 1 or self.tree.openElements[1].name != "body"):
+ assert self.parser.innerHTML
+ elif not self.parser.framesetOK:
+ pass
+ else:
+ if self.tree.openElements[1].parent:
+ self.tree.openElements[1].parent.removeChild(self.tree.openElements[1])
+ while self.tree.openElements[-1].name != "html":
+ self.tree.openElements.pop()
+ self.tree.insertElement(token)
+ self.parser.phase = self.parser.phases["inFrameset"]
+
+ def startTagCloseP(self, token):
+ if self.tree.elementInScope("p", variant="button"):
+ self.endTagP(impliedTagToken("p"))
+ self.tree.insertElement(token)
+
+ def startTagPreListing(self, token):
+ if self.tree.elementInScope("p", variant="button"):
+ self.endTagP(impliedTagToken("p"))
+ self.tree.insertElement(token)
+ self.parser.framesetOK = False
+ self.processSpaceCharacters = self.processSpaceCharactersDropNewline
+
+ def startTagForm(self, token):
+ if self.tree.formPointer:
+ self.parser.parseError("unexpected-start-tag", {"name": "form"})
+ else:
+ if self.tree.elementInScope("p", variant="button"):
+ self.endTagP(impliedTagToken("p"))
+ self.tree.insertElement(token)
+ self.tree.formPointer = self.tree.openElements[-1]
+
+ def startTagListItem(self, token):
+ self.parser.framesetOK = False
+
+ stopNamesMap = {"li": ["li"],
+ "dt": ["dt", "dd"],
+ "dd": ["dt", "dd"]}
+ stopNames = stopNamesMap[token["name"]]
+ for node in reversed(self.tree.openElements):
+ if node.name in stopNames:
+ self.parser.phase.processEndTag(
+ impliedTagToken(node.name, "EndTag"))
+ break
+ if (node.nameTuple in specialElements and
+ node.name not in ("address", "div", "p")):
+ break
+
+ if self.tree.elementInScope("p", variant="button"):
+ self.parser.phase.processEndTag(
+ impliedTagToken("p", "EndTag"))
+
+ self.tree.insertElement(token)
+
+ def startTagPlaintext(self, token):
+ if self.tree.elementInScope("p", variant="button"):
+ self.endTagP(impliedTagToken("p"))
+ self.tree.insertElement(token)
+ self.parser.tokenizer.state = self.parser.tokenizer.plaintextState
+
+ def startTagHeading(self, token):
+ if self.tree.elementInScope("p", variant="button"):
+ self.endTagP(impliedTagToken("p"))
+ if self.tree.openElements[-1].name in headingElements:
+ self.parser.parseError("unexpected-start-tag", {"name": token["name"]})
+ self.tree.openElements.pop()
+ self.tree.insertElement(token)
+
+ def startTagA(self, token):
+ afeAElement = self.tree.elementInActiveFormattingElements("a")
+ if afeAElement:
+ self.parser.parseError("unexpected-start-tag-implies-end-tag",
+ {"startName": "a", "endName": "a"})
+ self.endTagFormatting(impliedTagToken("a"))
+ if afeAElement in self.tree.openElements:
+ self.tree.openElements.remove(afeAElement)
+ if afeAElement in self.tree.activeFormattingElements:
+ self.tree.activeFormattingElements.remove(afeAElement)
+ self.tree.reconstructActiveFormattingElements()
+ self.addFormattingElement(token)
+
+ def startTagFormatting(self, token):
+ self.tree.reconstructActiveFormattingElements()
+ self.addFormattingElement(token)
+
+ def startTagNobr(self, token):
+ self.tree.reconstructActiveFormattingElements()
+ if self.tree.elementInScope("nobr"):
+ self.parser.parseError("unexpected-start-tag-implies-end-tag",
+ {"startName": "nobr", "endName": "nobr"})
+ self.processEndTag(impliedTagToken("nobr"))
+ # XXX Need tests that trigger the following
+ self.tree.reconstructActiveFormattingElements()
+ self.addFormattingElement(token)
+
+ def startTagButton(self, token):
+ if self.tree.elementInScope("button"):
+ self.parser.parseError("unexpected-start-tag-implies-end-tag",
+ {"startName": "button", "endName": "button"})
+ self.processEndTag(impliedTagToken("button"))
+ return token
+ else:
+ self.tree.reconstructActiveFormattingElements()
+ self.tree.insertElement(token)
+ self.parser.framesetOK = False
+
+ def startTagAppletMarqueeObject(self, token):
+ self.tree.reconstructActiveFormattingElements()
+ self.tree.insertElement(token)
+ self.tree.activeFormattingElements.append(Marker)
+ self.parser.framesetOK = False
+
+ def startTagXmp(self, token):
+ if self.tree.elementInScope("p", variant="button"):
+ self.endTagP(impliedTagToken("p"))
+ self.tree.reconstructActiveFormattingElements()
+ self.parser.framesetOK = False
+ self.parser.parseRCDataRawtext(token, "RAWTEXT")
+
+ def startTagTable(self, token):
+ if self.parser.compatMode != "quirks":
+ if self.tree.elementInScope("p", variant="button"):
+ self.processEndTag(impliedTagToken("p"))
+ self.tree.insertElement(token)
+ self.parser.framesetOK = False
+ self.parser.phase = self.parser.phases["inTable"]
+
+ def startTagVoidFormatting(self, token):
+ self.tree.reconstructActiveFormattingElements()
+ self.tree.insertElement(token)
+ self.tree.openElements.pop()
+ token["selfClosingAcknowledged"] = True
+ self.parser.framesetOK = False
+
+ def startTagInput(self, token):
+ framesetOK = self.parser.framesetOK
+ self.startTagVoidFormatting(token)
+ if ("type" in token["data"] and
+ token["data"]["type"].translate(asciiUpper2Lower) == "hidden"):
+ # input type=hidden doesn't change framesetOK
+ self.parser.framesetOK = framesetOK
+
+ def startTagParamSource(self, token):
+ self.tree.insertElement(token)
+ self.tree.openElements.pop()
+ token["selfClosingAcknowledged"] = True
+
+ def startTagHr(self, token):
+ if self.tree.elementInScope("p", variant="button"):
+ self.endTagP(impliedTagToken("p"))
+ self.tree.insertElement(token)
+ self.tree.openElements.pop()
+ token["selfClosingAcknowledged"] = True
+ self.parser.framesetOK = False
+
+ def startTagImage(self, token):
+ # No really...
+ self.parser.parseError("unexpected-start-tag-treated-as",
+ {"originalName": "image", "newName": "img"})
+ self.processStartTag(impliedTagToken("img", "StartTag",
+ attributes=token["data"],
+ selfClosing=token["selfClosing"]))
+
+ def startTagIsIndex(self, token):
+ self.parser.parseError("deprecated-tag", {"name": "isindex"})
+ if self.tree.formPointer:
+ return
+ form_attrs = {}
+ if "action" in token["data"]:
+ form_attrs["action"] = token["data"]["action"]
+ self.processStartTag(impliedTagToken("form", "StartTag",
+ attributes=form_attrs))
+ self.processStartTag(impliedTagToken("hr", "StartTag"))
+ self.processStartTag(impliedTagToken("label", "StartTag"))
+ # XXX Localization ...
+ if "prompt" in token["data"]:
+ prompt = token["data"]["prompt"]
+ else:
+ prompt = "This is a searchable index. Enter search keywords: "
+ self.processCharacters(
+ {"type": tokenTypes["Characters"], "data": prompt})
+ attributes = token["data"].copy()
+ if "action" in attributes:
+ del attributes["action"]
+ if "prompt" in attributes:
+ del attributes["prompt"]
+ attributes["name"] = "isindex"
+ self.processStartTag(impliedTagToken("input", "StartTag",
+ attributes=attributes,
+ selfClosing=token["selfClosing"]))
+ self.processEndTag(impliedTagToken("label"))
+ self.processStartTag(impliedTagToken("hr", "StartTag"))
+ self.processEndTag(impliedTagToken("form"))
+
+ def startTagTextarea(self, token):
+ self.tree.insertElement(token)
+ self.parser.tokenizer.state = self.parser.tokenizer.rcdataState
+ self.processSpaceCharacters = self.processSpaceCharactersDropNewline
+ self.parser.framesetOK = False
+
+ def startTagIFrame(self, token):
+ self.parser.framesetOK = False
+ self.startTagRawtext(token)
+
+ def startTagNoscript(self, token):
+ if self.parser.scripting:
+ self.startTagRawtext(token)
+ else:
+ self.startTagOther(token)
+
+ def startTagRawtext(self, token):
+ """iframe, noembed noframes, noscript(if scripting enabled)"""
+ self.parser.parseRCDataRawtext(token, "RAWTEXT")
+
+ def startTagOpt(self, token):
+ if self.tree.openElements[-1].name == "option":
+ self.parser.phase.processEndTag(impliedTagToken("option"))
+ self.tree.reconstructActiveFormattingElements()
+ self.parser.tree.insertElement(token)
+
+ def startTagSelect(self, token):
+ self.tree.reconstructActiveFormattingElements()
+ self.tree.insertElement(token)
+ self.parser.framesetOK = False
+ if self.parser.phase in (self.parser.phases["inTable"],
+ self.parser.phases["inCaption"],
+ self.parser.phases["inColumnGroup"],
+ self.parser.phases["inTableBody"],
+ self.parser.phases["inRow"],
+ self.parser.phases["inCell"]):
+ self.parser.phase = self.parser.phases["inSelectInTable"]
+ else:
+ self.parser.phase = self.parser.phases["inSelect"]
+
+ def startTagRpRt(self, token):
+ if self.tree.elementInScope("ruby"):
+ self.tree.generateImpliedEndTags()
+ if self.tree.openElements[-1].name != "ruby":
+ self.parser.parseError()
+ self.tree.insertElement(token)
+
+ def startTagMath(self, token):
+ self.tree.reconstructActiveFormattingElements()
+ self.parser.adjustMathMLAttributes(token)
+ self.parser.adjustForeignAttributes(token)
+ token["namespace"] = namespaces["mathml"]
+ self.tree.insertElement(token)
+ # Need to get the parse error right for the case where the token
+ # has a namespace not equal to the xmlns attribute
+ if token["selfClosing"]:
+ self.tree.openElements.pop()
+ token["selfClosingAcknowledged"] = True
+
+ def startTagSvg(self, token):
+ self.tree.reconstructActiveFormattingElements()
+ self.parser.adjustSVGAttributes(token)
+ self.parser.adjustForeignAttributes(token)
+ token["namespace"] = namespaces["svg"]
+ self.tree.insertElement(token)
+ # Need to get the parse error right for the case where the token
+ # has a namespace not equal to the xmlns attribute
+ if token["selfClosing"]:
+ self.tree.openElements.pop()
+ token["selfClosingAcknowledged"] = True
+
+ def startTagMisplaced(self, token):
+ """ Elements that should be children of other elements that have a
+ different insertion mode; here they are ignored
+ "caption", "col", "colgroup", "frame", "frameset", "head",
+ "option", "optgroup", "tbody", "td", "tfoot", "th", "thead",
+ "tr", "noscript"
+ """
+ self.parser.parseError("unexpected-start-tag-ignored", {"name": token["name"]})
+
+ def startTagOther(self, token):
+ self.tree.reconstructActiveFormattingElements()
+ self.tree.insertElement(token)
+
+ def endTagP(self, token):
+ if not self.tree.elementInScope("p", variant="button"):
+ self.startTagCloseP(impliedTagToken("p", "StartTag"))
+ self.parser.parseError("unexpected-end-tag", {"name": "p"})
+ self.endTagP(impliedTagToken("p", "EndTag"))
+ else:
+ self.tree.generateImpliedEndTags("p")
+ if self.tree.openElements[-1].name != "p":
+ self.parser.parseError("unexpected-end-tag", {"name": "p"})
+ node = self.tree.openElements.pop()
+ while node.name != "p":
+ node = self.tree.openElements.pop()
+
+ def endTagBody(self, token):
+ if not self.tree.elementInScope("body"):
+ self.parser.parseError()
+ return
+ elif self.tree.openElements[-1].name != "body":
+ for node in self.tree.openElements[2:]:
+ if node.name not in frozenset(("dd", "dt", "li", "optgroup",
+ "option", "p", "rp", "rt",
+ "tbody", "td", "tfoot",
+ "th", "thead", "tr", "body",
+ "html")):
+ # Not sure this is the correct name for the parse error
+ self.parser.parseError(
+ "expected-one-end-tag-but-got-another",
+ {"gotName": "body", "expectedName": node.name})
+ break
+ self.parser.phase = self.parser.phases["afterBody"]
+
+ def endTagHtml(self, token):
+ # We repeat the test for the body end tag token being ignored here
+ if self.tree.elementInScope("body"):
+ self.endTagBody(impliedTagToken("body"))
+ return token
+
+ def endTagBlock(self, token):
+ # Put us back in the right whitespace handling mode
+ if token["name"] == "pre":
+ self.processSpaceCharacters = self.processSpaceCharactersNonPre
+ inScope = self.tree.elementInScope(token["name"])
+ if inScope:
+ self.tree.generateImpliedEndTags()
+ if self.tree.openElements[-1].name != token["name"]:
+ self.parser.parseError("end-tag-too-early", {"name": token["name"]})
+ if inScope:
+ node = self.tree.openElements.pop()
+ while node.name != token["name"]:
+ node = self.tree.openElements.pop()
+
+ def endTagForm(self, token):
+ node = self.tree.formPointer
+ self.tree.formPointer = None
+ if node is None or not self.tree.elementInScope(node):
+ self.parser.parseError("unexpected-end-tag",
+ {"name": "form"})
+ else:
+ self.tree.generateImpliedEndTags()
+ if self.tree.openElements[-1] != node:
+ self.parser.parseError("end-tag-too-early-ignored",
+ {"name": "form"})
+ self.tree.openElements.remove(node)
+
+ def endTagListItem(self, token):
+ if token["name"] == "li":
+ variant = "list"
+ else:
+ variant = None
+ if not self.tree.elementInScope(token["name"], variant=variant):
+ self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
+ else:
+ self.tree.generateImpliedEndTags(exclude=token["name"])
+ if self.tree.openElements[-1].name != token["name"]:
+ self.parser.parseError(
+ "end-tag-too-early",
+ {"name": token["name"]})
+ node = self.tree.openElements.pop()
+ while node.name != token["name"]:
+ node = self.tree.openElements.pop()
+
+ def endTagHeading(self, token):
+ for item in headingElements:
+ if self.tree.elementInScope(item):
+ self.tree.generateImpliedEndTags()
+ break
+ if self.tree.openElements[-1].name != token["name"]:
+ self.parser.parseError("end-tag-too-early", {"name": token["name"]})
+
+ for item in headingElements:
+ if self.tree.elementInScope(item):
+ item = self.tree.openElements.pop()
+ while item.name not in headingElements:
+ item = self.tree.openElements.pop()
+ break
+
+ def endTagFormatting(self, token):
+ """The much-feared adoption agency algorithm"""
+ # http://svn.whatwg.org/webapps/complete.html#adoptionAgency revision 7867
+ # XXX Better parseError messages appreciated.
+
+ # Step 1
+ outerLoopCounter = 0
+
+ # Step 2
+ while outerLoopCounter < 8:
+
+ # Step 3
+ outerLoopCounter += 1
+
+ # Step 4:
+
+ # Let the formatting element be the last element in
+ # the list of active formatting elements that:
+ # - is between the end of the list and the last scope
+ # marker in the list, if any, or the start of the list
+ # otherwise, and
+ # - has the same tag name as the token.
+ formattingElement = self.tree.elementInActiveFormattingElements(
+ token["name"])
+ if (not formattingElement or
+ (formattingElement in self.tree.openElements and
+ not self.tree.elementInScope(formattingElement.name))):
+ # If there is no such node, then abort these steps
+ # and instead act as described in the "any other
+ # end tag" entry below.
+ self.endTagOther(token)
+ return
+
+ # Otherwise, if there is such a node, but that node is
+ # not in the stack of open elements, then this is a
+ # parse error; remove the element from the list, and
+ # abort these steps.
+ elif formattingElement not in self.tree.openElements:
+ self.parser.parseError("adoption-agency-1.2", {"name": token["name"]})
+ self.tree.activeFormattingElements.remove(formattingElement)
+ return
+
+ # Otherwise, if there is such a node, and that node is
+ # also in the stack of open elements, but the element
+ # is not in scope, then this is a parse error; ignore
+ # the token, and abort these steps.
+ elif not self.tree.elementInScope(formattingElement.name):
+ self.parser.parseError("adoption-agency-4.4", {"name": token["name"]})
+ return
+
+ # Otherwise, there is a formatting element and that
+ # element is in the stack and is in scope. If the
+ # element is not the current node, this is a parse
+ # error. In any case, proceed with the algorithm as
+ # written in the following steps.
+ else:
+ if formattingElement != self.tree.openElements[-1]:
+ self.parser.parseError("adoption-agency-1.3", {"name": token["name"]})
+
+ # Step 5:
+
+ # Let the furthest block be the topmost node in the
+ # stack of open elements that is lower in the stack
+ # than the formatting element, and is an element in
+ # the special category. There might not be one.
+ afeIndex = self.tree.openElements.index(formattingElement)
+ furthestBlock = None
+ for element in self.tree.openElements[afeIndex:]:
+ if element.nameTuple in specialElements:
+ furthestBlock = element
+ break
+
+ # Step 6:
+
+ # If there is no furthest block, then the UA must
+ # first pop all the nodes from the bottom of the stack
+ # of open elements, from the current node up to and
+ # including the formatting element, then remove the
+ # formatting element from the list of active
+ # formatting elements, and finally abort these steps.
+ if furthestBlock is None:
+ element = self.tree.openElements.pop()
+ while element != formattingElement:
+ element = self.tree.openElements.pop()
+ self.tree.activeFormattingElements.remove(element)
+ return
+
+ # Step 7
+ commonAncestor = self.tree.openElements[afeIndex - 1]
+
+ # Step 8:
+ # The bookmark is supposed to help us identify where to reinsert
+ # nodes in step 15. We have to ensure that we reinsert nodes after
+ # the node before the active formatting element. Note the bookmark
+ # can move in step 9.7
+ bookmark = self.tree.activeFormattingElements.index(formattingElement)
+
+ # Step 9
+ lastNode = node = furthestBlock
+ innerLoopCounter = 0
+
+ index = self.tree.openElements.index(node)
+ while innerLoopCounter < 3:
+ innerLoopCounter += 1
+ # Node is element before node in open elements
+ index -= 1
+ node = self.tree.openElements[index]
+ if node not in self.tree.activeFormattingElements:
+ self.tree.openElements.remove(node)
+ continue
+ # Step 9.6
+ if node == formattingElement:
+ break
+ # Step 9.7
+ if lastNode == furthestBlock:
+ bookmark = self.tree.activeFormattingElements.index(node) + 1
+ # Step 9.8
+ clone = node.cloneNode()
+ # Replace node with clone
+ self.tree.activeFormattingElements[
+ self.tree.activeFormattingElements.index(node)] = clone
+ self.tree.openElements[
+ self.tree.openElements.index(node)] = clone
+ node = clone
+ # Step 9.9
+ # Remove lastNode from its parents, if any
+ if lastNode.parent:
+ lastNode.parent.removeChild(lastNode)
+ node.appendChild(lastNode)
+ # Step 9.10
+ lastNode = node
+
+ # Step 10
+ # Foster parent lastNode if commonAncestor is a
+ # table, tbody, tfoot, thead, or tr we need to foster
+ # parent the lastNode
+ if lastNode.parent:
+ lastNode.parent.removeChild(lastNode)
+
+ if commonAncestor.name in frozenset(("table", "tbody", "tfoot", "thead", "tr")):
+ parent, insertBefore = self.tree.getTableMisnestedNodePosition()
+ parent.insertBefore(lastNode, insertBefore)
+ else:
+ commonAncestor.appendChild(lastNode)
+
+ # Step 11
+ clone = formattingElement.cloneNode()
+
+ # Step 12
+ furthestBlock.reparentChildren(clone)
+
+ # Step 13
+ furthestBlock.appendChild(clone)
+
+ # Step 14
+ self.tree.activeFormattingElements.remove(formattingElement)
+ self.tree.activeFormattingElements.insert(bookmark, clone)
+
+ # Step 15
+ self.tree.openElements.remove(formattingElement)
+ self.tree.openElements.insert(
+ self.tree.openElements.index(furthestBlock) + 1, clone)
+
+ def endTagAppletMarqueeObject(self, token):
+ if self.tree.elementInScope(token["name"]):
+ self.tree.generateImpliedEndTags()
+ if self.tree.openElements[-1].name != token["name"]:
+ self.parser.parseError("end-tag-too-early", {"name": token["name"]})
+
+ if self.tree.elementInScope(token["name"]):
+ element = self.tree.openElements.pop()
+ while element.name != token["name"]:
+ element = self.tree.openElements.pop()
+ self.tree.clearActiveFormattingElements()
+
+ def endTagBr(self, token):
+ self.parser.parseError("unexpected-end-tag-treated-as",
+ {"originalName": "br", "newName": "br element"})
+ self.tree.reconstructActiveFormattingElements()
+ self.tree.insertElement(impliedTagToken("br", "StartTag"))
+ self.tree.openElements.pop()
+
+ def endTagOther(self, token):
+ for node in self.tree.openElements[::-1]:
+ if node.name == token["name"]:
+ self.tree.generateImpliedEndTags(exclude=token["name"])
+ if self.tree.openElements[-1].name != token["name"]:
+ self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
+ while self.tree.openElements.pop() != node:
+ pass
+ break
+ else:
+ if node.nameTuple in specialElements:
+ self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
+ break
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", Phase.startTagHtml),
+ (("base", "basefont", "bgsound", "command", "link", "meta",
+ "script", "style", "title"),
+ startTagProcessInHead),
+ ("body", startTagBody),
+ ("frameset", startTagFrameset),
+ (("address", "article", "aside", "blockquote", "center", "details",
+ "dir", "div", "dl", "fieldset", "figcaption", "figure",
+ "footer", "header", "hgroup", "main", "menu", "nav", "ol", "p",
+ "section", "summary", "ul"),
+ startTagCloseP),
+ (headingElements, startTagHeading),
+ (("pre", "listing"), startTagPreListing),
+ ("form", startTagForm),
+ (("li", "dd", "dt"), startTagListItem),
+ ("plaintext", startTagPlaintext),
+ ("a", startTagA),
+ (("b", "big", "code", "em", "font", "i", "s", "small", "strike",
+ "strong", "tt", "u"), startTagFormatting),
+ ("nobr", startTagNobr),
+ ("button", startTagButton),
+ (("applet", "marquee", "object"), startTagAppletMarqueeObject),
+ ("xmp", startTagXmp),
+ ("table", startTagTable),
+ (("area", "br", "embed", "img", "keygen", "wbr"),
+ startTagVoidFormatting),
+ (("param", "source", "track"), startTagParamSource),
+ ("input", startTagInput),
+ ("hr", startTagHr),
+ ("image", startTagImage),
+ ("isindex", startTagIsIndex),
+ ("textarea", startTagTextarea),
+ ("iframe", startTagIFrame),
+ ("noscript", startTagNoscript),
+ (("noembed", "noframes"), startTagRawtext),
+ ("select", startTagSelect),
+ (("rp", "rt"), startTagRpRt),
+ (("option", "optgroup"), startTagOpt),
+ (("math"), startTagMath),
+ (("svg"), startTagSvg),
+ (("caption", "col", "colgroup", "frame", "head",
+ "tbody", "td", "tfoot", "th", "thead",
+ "tr"), startTagMisplaced)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ ("body", endTagBody),
+ ("html", endTagHtml),
+ (("address", "article", "aside", "blockquote", "button", "center",
+ "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure",
+ "footer", "header", "hgroup", "listing", "main", "menu", "nav", "ol", "pre",
+ "section", "summary", "ul"), endTagBlock),
+ ("form", endTagForm),
+ ("p", endTagP),
+ (("dd", "dt", "li"), endTagListItem),
+ (headingElements, endTagHeading),
+ (("a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small",
+ "strike", "strong", "tt", "u"), endTagFormatting),
+ (("applet", "marquee", "object"), endTagAppletMarqueeObject),
+ ("br", endTagBr),
+ ])
+ endTagHandler.default = endTagOther
+
+ class TextPhase(Phase):
+ __slots__ = tuple()
+
+ def processCharacters(self, token):
+ self.tree.insertText(token["data"])
+
+ def processEOF(self):
+ self.parser.parseError("expected-named-closing-tag-but-got-eof",
+ {"name": self.tree.openElements[-1].name})
+ self.tree.openElements.pop()
+ self.parser.phase = self.parser.originalPhase
+ return True
+
+ def startTagOther(self, token):
+ assert False, "Tried to process start tag %s in RCDATA/RAWTEXT mode" % token['name']
+
+ def endTagScript(self, token):
+ node = self.tree.openElements.pop()
+ assert node.name == "script"
+ self.parser.phase = self.parser.originalPhase
+ # The rest of this method is all stuff that only happens if
+ # document.write works
+
+ def endTagOther(self, token):
+ self.tree.openElements.pop()
+ self.parser.phase = self.parser.originalPhase
+
+ startTagHandler = _utils.MethodDispatcher([])
+ startTagHandler.default = startTagOther
+ endTagHandler = _utils.MethodDispatcher([
+ ("script", endTagScript)])
+ endTagHandler.default = endTagOther
+
+ class InTablePhase(Phase):
+ # http://www.whatwg.org/specs/web-apps/current-work/#in-table
+ __slots__ = tuple()
+
+ # helper methods
+ def clearStackToTableContext(self):
+ # "clear the stack back to a table context"
+ while self.tree.openElements[-1].name not in ("table", "html"):
+ # self.parser.parseError("unexpected-implied-end-tag-in-table",
+ # {"name": self.tree.openElements[-1].name})
+ self.tree.openElements.pop()
+ # When the current node is <html> it's an innerHTML case
+
+ # processing methods
+ def processEOF(self):
+ if self.tree.openElements[-1].name != "html":
+ self.parser.parseError("eof-in-table")
+ else:
+ assert self.parser.innerHTML
+ # Stop parsing
+
+ def processSpaceCharacters(self, token):
+ originalPhase = self.parser.phase
+ self.parser.phase = self.parser.phases["inTableText"]
+ self.parser.phase.originalPhase = originalPhase
+ self.parser.phase.processSpaceCharacters(token)
+
+ def processCharacters(self, token):
+ originalPhase = self.parser.phase
+ self.parser.phase = self.parser.phases["inTableText"]
+ self.parser.phase.originalPhase = originalPhase
+ self.parser.phase.processCharacters(token)
+
+ def insertText(self, token):
+ # If we get here there must be at least one non-whitespace character
+ # Do the table magic!
+ self.tree.insertFromTable = True
+ self.parser.phases["inBody"].processCharacters(token)
+ self.tree.insertFromTable = False
+
+ def startTagCaption(self, token):
+ self.clearStackToTableContext()
+ self.tree.activeFormattingElements.append(Marker)
+ self.tree.insertElement(token)
+ self.parser.phase = self.parser.phases["inCaption"]
+
+ def startTagColgroup(self, token):
+ self.clearStackToTableContext()
+ self.tree.insertElement(token)
+ self.parser.phase = self.parser.phases["inColumnGroup"]
+
+ def startTagCol(self, token):
+ self.startTagColgroup(impliedTagToken("colgroup", "StartTag"))
+ return token
+
+ def startTagRowGroup(self, token):
+ self.clearStackToTableContext()
+ self.tree.insertElement(token)
+ self.parser.phase = self.parser.phases["inTableBody"]
+
+ def startTagImplyTbody(self, token):
+ self.startTagRowGroup(impliedTagToken("tbody", "StartTag"))
+ return token
+
+ def startTagTable(self, token):
+ self.parser.parseError("unexpected-start-tag-implies-end-tag",
+ {"startName": "table", "endName": "table"})
+ self.parser.phase.processEndTag(impliedTagToken("table"))
+ if not self.parser.innerHTML:
+ return token
+
+ def startTagStyleScript(self, token):
+ return self.parser.phases["inHead"].processStartTag(token)
+
+ def startTagInput(self, token):
+ if ("type" in token["data"] and
+ token["data"]["type"].translate(asciiUpper2Lower) == "hidden"):
+ self.parser.parseError("unexpected-hidden-input-in-table")
+ self.tree.insertElement(token)
+ # XXX associate with form
+ self.tree.openElements.pop()
+ else:
+ self.startTagOther(token)
+
+ def startTagForm(self, token):
+ self.parser.parseError("unexpected-form-in-table")
+ if self.tree.formPointer is None:
+ self.tree.insertElement(token)
+ self.tree.formPointer = self.tree.openElements[-1]
+ self.tree.openElements.pop()
+
+ def startTagOther(self, token):
+ self.parser.parseError("unexpected-start-tag-implies-table-voodoo", {"name": token["name"]})
+ # Do the table magic!
+ self.tree.insertFromTable = True
+ self.parser.phases["inBody"].processStartTag(token)
+ self.tree.insertFromTable = False
+
+ def endTagTable(self, token):
+ if self.tree.elementInScope("table", variant="table"):
+ self.tree.generateImpliedEndTags()
+ if self.tree.openElements[-1].name != "table":
+ self.parser.parseError("end-tag-too-early-named",
+ {"gotName": "table",
+ "expectedName": self.tree.openElements[-1].name})
+ while self.tree.openElements[-1].name != "table":
+ self.tree.openElements.pop()
+ self.tree.openElements.pop()
+ self.parser.resetInsertionMode()
+ else:
+ # innerHTML case
+ assert self.parser.innerHTML
+ self.parser.parseError()
+
+ def endTagIgnore(self, token):
+ self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
+
+ def endTagOther(self, token):
+ self.parser.parseError("unexpected-end-tag-implies-table-voodoo", {"name": token["name"]})
+ # Do the table magic!
+ self.tree.insertFromTable = True
+ self.parser.phases["inBody"].processEndTag(token)
+ self.tree.insertFromTable = False
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", Phase.startTagHtml),
+ ("caption", startTagCaption),
+ ("colgroup", startTagColgroup),
+ ("col", startTagCol),
+ (("tbody", "tfoot", "thead"), startTagRowGroup),
+ (("td", "th", "tr"), startTagImplyTbody),
+ ("table", startTagTable),
+ (("style", "script"), startTagStyleScript),
+ ("input", startTagInput),
+ ("form", startTagForm)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ ("table", endTagTable),
+ (("body", "caption", "col", "colgroup", "html", "tbody", "td",
+ "tfoot", "th", "thead", "tr"), endTagIgnore)
+ ])
+ endTagHandler.default = endTagOther
+
+ class InTableTextPhase(Phase):
+ __slots__ = ("originalPhase", "characterTokens")
+
+ def __init__(self, *args, **kwargs):
+ super(InTableTextPhase, self).__init__(*args, **kwargs)
+ self.originalPhase = None
+ self.characterTokens = []
+
+ def flushCharacters(self):
+ data = "".join([item["data"] for item in self.characterTokens])
+ if any([item not in spaceCharacters for item in data]):
+ token = {"type": tokenTypes["Characters"], "data": data}
+ self.parser.phases["inTable"].insertText(token)
+ elif data:
+ self.tree.insertText(data)
+ self.characterTokens = []
+
+ def processComment(self, token):
+ self.flushCharacters()
+ self.parser.phase = self.originalPhase
+ return token
+
+ def processEOF(self):
+ self.flushCharacters()
+ self.parser.phase = self.originalPhase
+ return True
+
+ def processCharacters(self, token):
+ if token["data"] == "\u0000":
+ return
+ self.characterTokens.append(token)
+
+ def processSpaceCharacters(self, token):
+ # pretty sure we should never reach here
+ self.characterTokens.append(token)
+ # assert False
+
+ def processStartTag(self, token):
+ self.flushCharacters()
+ self.parser.phase = self.originalPhase
+ return token
+
+ def processEndTag(self, token):
+ self.flushCharacters()
+ self.parser.phase = self.originalPhase
+ return token
+
+ class InCaptionPhase(Phase):
+ # http://www.whatwg.org/specs/web-apps/current-work/#in-caption
+ __slots__ = tuple()
+
+ def ignoreEndTagCaption(self):
+ return not self.tree.elementInScope("caption", variant="table")
+
+ def processEOF(self):
+ self.parser.phases["inBody"].processEOF()
+
+ def processCharacters(self, token):
+ return self.parser.phases["inBody"].processCharacters(token)
+
+ def startTagTableElement(self, token):
+ self.parser.parseError()
+ # XXX Have to duplicate logic here to find out if the tag is ignored
+ ignoreEndTag = self.ignoreEndTagCaption()
+ self.parser.phase.processEndTag(impliedTagToken("caption"))
+ if not ignoreEndTag:
+ return token
+
+ def startTagOther(self, token):
+ return self.parser.phases["inBody"].processStartTag(token)
+
+ def endTagCaption(self, token):
+ if not self.ignoreEndTagCaption():
+ # AT this code is quite similar to endTagTable in "InTable"
+ self.tree.generateImpliedEndTags()
+ if self.tree.openElements[-1].name != "caption":
+ self.parser.parseError("expected-one-end-tag-but-got-another",
+ {"gotName": "caption",
+ "expectedName": self.tree.openElements[-1].name})
+ while self.tree.openElements[-1].name != "caption":
+ self.tree.openElements.pop()
+ self.tree.openElements.pop()
+ self.tree.clearActiveFormattingElements()
+ self.parser.phase = self.parser.phases["inTable"]
+ else:
+ # innerHTML case
+ assert self.parser.innerHTML
+ self.parser.parseError()
+
+ def endTagTable(self, token):
+ self.parser.parseError()
+ ignoreEndTag = self.ignoreEndTagCaption()
+ self.parser.phase.processEndTag(impliedTagToken("caption"))
+ if not ignoreEndTag:
+ return token
+
+ def endTagIgnore(self, token):
+ self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
+
+ def endTagOther(self, token):
+ return self.parser.phases["inBody"].processEndTag(token)
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", Phase.startTagHtml),
+ (("caption", "col", "colgroup", "tbody", "td", "tfoot", "th",
+ "thead", "tr"), startTagTableElement)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ ("caption", endTagCaption),
+ ("table", endTagTable),
+ (("body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th",
+ "thead", "tr"), endTagIgnore)
+ ])
+ endTagHandler.default = endTagOther
+
+ class InColumnGroupPhase(Phase):
+ # http://www.whatwg.org/specs/web-apps/current-work/#in-column
+ __slots__ = tuple()
+
+ def ignoreEndTagColgroup(self):
+ return self.tree.openElements[-1].name == "html"
+
+ def processEOF(self):
+ if self.tree.openElements[-1].name == "html":
+ assert self.parser.innerHTML
+ return
+ else:
+ ignoreEndTag = self.ignoreEndTagColgroup()
+ self.endTagColgroup(impliedTagToken("colgroup"))
+ if not ignoreEndTag:
+ return True
+
+ def processCharacters(self, token):
+ ignoreEndTag = self.ignoreEndTagColgroup()
+ self.endTagColgroup(impliedTagToken("colgroup"))
+ if not ignoreEndTag:
+ return token
+
+ def startTagCol(self, token):
+ self.tree.insertElement(token)
+ self.tree.openElements.pop()
+ token["selfClosingAcknowledged"] = True
+
+ def startTagOther(self, token):
+ ignoreEndTag = self.ignoreEndTagColgroup()
+ self.endTagColgroup(impliedTagToken("colgroup"))
+ if not ignoreEndTag:
+ return token
+
+ def endTagColgroup(self, token):
+ if self.ignoreEndTagColgroup():
+ # innerHTML case
+ assert self.parser.innerHTML
+ self.parser.parseError()
+ else:
+ self.tree.openElements.pop()
+ self.parser.phase = self.parser.phases["inTable"]
+
+ def endTagCol(self, token):
+ self.parser.parseError("no-end-tag", {"name": "col"})
+
+ def endTagOther(self, token):
+ ignoreEndTag = self.ignoreEndTagColgroup()
+ self.endTagColgroup(impliedTagToken("colgroup"))
+ if not ignoreEndTag:
+ return token
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", Phase.startTagHtml),
+ ("col", startTagCol)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ ("colgroup", endTagColgroup),
+ ("col", endTagCol)
+ ])
+ endTagHandler.default = endTagOther
+
+ class InTableBodyPhase(Phase):
+ # http://www.whatwg.org/specs/web-apps/current-work/#in-table0
+ __slots__ = tuple()
+
+ # helper methods
+ def clearStackToTableBodyContext(self):
+ while self.tree.openElements[-1].name not in ("tbody", "tfoot",
+ "thead", "html"):
+ # self.parser.parseError("unexpected-implied-end-tag-in-table",
+ # {"name": self.tree.openElements[-1].name})
+ self.tree.openElements.pop()
+ if self.tree.openElements[-1].name == "html":
+ assert self.parser.innerHTML
+
+ # the rest
+ def processEOF(self):
+ self.parser.phases["inTable"].processEOF()
+
+ def processSpaceCharacters(self, token):
+ return self.parser.phases["inTable"].processSpaceCharacters(token)
+
+ def processCharacters(self, token):
+ return self.parser.phases["inTable"].processCharacters(token)
+
+ def startTagTr(self, token):
+ self.clearStackToTableBodyContext()
+ self.tree.insertElement(token)
+ self.parser.phase = self.parser.phases["inRow"]
+
+ def startTagTableCell(self, token):
+ self.parser.parseError("unexpected-cell-in-table-body",
+ {"name": token["name"]})
+ self.startTagTr(impliedTagToken("tr", "StartTag"))
+ return token
+
+ def startTagTableOther(self, token):
+ # XXX AT Any ideas on how to share this with endTagTable?
+ if (self.tree.elementInScope("tbody", variant="table") or
+ self.tree.elementInScope("thead", variant="table") or
+ self.tree.elementInScope("tfoot", variant="table")):
+ self.clearStackToTableBodyContext()
+ self.endTagTableRowGroup(
+ impliedTagToken(self.tree.openElements[-1].name))
+ return token
+ else:
+ # innerHTML case
+ assert self.parser.innerHTML
+ self.parser.parseError()
+
+ def startTagOther(self, token):
+ return self.parser.phases["inTable"].processStartTag(token)
+
+ def endTagTableRowGroup(self, token):
+ if self.tree.elementInScope(token["name"], variant="table"):
+ self.clearStackToTableBodyContext()
+ self.tree.openElements.pop()
+ self.parser.phase = self.parser.phases["inTable"]
+ else:
+ self.parser.parseError("unexpected-end-tag-in-table-body",
+ {"name": token["name"]})
+
+ def endTagTable(self, token):
+ if (self.tree.elementInScope("tbody", variant="table") or
+ self.tree.elementInScope("thead", variant="table") or
+ self.tree.elementInScope("tfoot", variant="table")):
+ self.clearStackToTableBodyContext()
+ self.endTagTableRowGroup(
+ impliedTagToken(self.tree.openElements[-1].name))
+ return token
+ else:
+ # innerHTML case
+ assert self.parser.innerHTML
+ self.parser.parseError()
+
+ def endTagIgnore(self, token):
+ self.parser.parseError("unexpected-end-tag-in-table-body",
+ {"name": token["name"]})
+
+ def endTagOther(self, token):
+ return self.parser.phases["inTable"].processEndTag(token)
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", Phase.startTagHtml),
+ ("tr", startTagTr),
+ (("td", "th"), startTagTableCell),
+ (("caption", "col", "colgroup", "tbody", "tfoot", "thead"),
+ startTagTableOther)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ (("tbody", "tfoot", "thead"), endTagTableRowGroup),
+ ("table", endTagTable),
+ (("body", "caption", "col", "colgroup", "html", "td", "th",
+ "tr"), endTagIgnore)
+ ])
+ endTagHandler.default = endTagOther
+
+ class InRowPhase(Phase):
+ # http://www.whatwg.org/specs/web-apps/current-work/#in-row
+ __slots__ = tuple()
+
+ # helper methods (XXX unify this with other table helper methods)
+ def clearStackToTableRowContext(self):
+ while self.tree.openElements[-1].name not in ("tr", "html"):
+ self.parser.parseError("unexpected-implied-end-tag-in-table-row",
+ {"name": self.tree.openElements[-1].name})
+ self.tree.openElements.pop()
+
+ def ignoreEndTagTr(self):
+ return not self.tree.elementInScope("tr", variant="table")
+
+ # the rest
+ def processEOF(self):
+ self.parser.phases["inTable"].processEOF()
+
+ def processSpaceCharacters(self, token):
+ return self.parser.phases["inTable"].processSpaceCharacters(token)
+
+ def processCharacters(self, token):
+ return self.parser.phases["inTable"].processCharacters(token)
+
+ def startTagTableCell(self, token):
+ self.clearStackToTableRowContext()
+ self.tree.insertElement(token)
+ self.parser.phase = self.parser.phases["inCell"]
+ self.tree.activeFormattingElements.append(Marker)
+
+ def startTagTableOther(self, token):
+ ignoreEndTag = self.ignoreEndTagTr()
+ self.endTagTr(impliedTagToken("tr"))
+ # XXX how are we sure it's always ignored in the innerHTML case?
+ if not ignoreEndTag:
+ return token
+
+ def startTagOther(self, token):
+ return self.parser.phases["inTable"].processStartTag(token)
+
+ def endTagTr(self, token):
+ if not self.ignoreEndTagTr():
+ self.clearStackToTableRowContext()
+ self.tree.openElements.pop()
+ self.parser.phase = self.parser.phases["inTableBody"]
+ else:
+ # innerHTML case
+ assert self.parser.innerHTML
+ self.parser.parseError()
+
+ def endTagTable(self, token):
+ ignoreEndTag = self.ignoreEndTagTr()
+ self.endTagTr(impliedTagToken("tr"))
+ # Reprocess the current tag if the tr end tag was not ignored
+ # XXX how are we sure it's always ignored in the innerHTML case?
+ if not ignoreEndTag:
+ return token
+
+ def endTagTableRowGroup(self, token):
+ if self.tree.elementInScope(token["name"], variant="table"):
+ self.endTagTr(impliedTagToken("tr"))
+ return token
+ else:
+ self.parser.parseError()
+
+ def endTagIgnore(self, token):
+ self.parser.parseError("unexpected-end-tag-in-table-row",
+ {"name": token["name"]})
+
+ def endTagOther(self, token):
+ return self.parser.phases["inTable"].processEndTag(token)
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", Phase.startTagHtml),
+ (("td", "th"), startTagTableCell),
+ (("caption", "col", "colgroup", "tbody", "tfoot", "thead",
+ "tr"), startTagTableOther)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ ("tr", endTagTr),
+ ("table", endTagTable),
+ (("tbody", "tfoot", "thead"), endTagTableRowGroup),
+ (("body", "caption", "col", "colgroup", "html", "td", "th"),
+ endTagIgnore)
+ ])
+ endTagHandler.default = endTagOther
+
+ class InCellPhase(Phase):
+ # http://www.whatwg.org/specs/web-apps/current-work/#in-cell
+ __slots__ = tuple()
+
+ # helper
+ def closeCell(self):
+ if self.tree.elementInScope("td", variant="table"):
+ self.endTagTableCell(impliedTagToken("td"))
+ elif self.tree.elementInScope("th", variant="table"):
+ self.endTagTableCell(impliedTagToken("th"))
+
+ # the rest
+ def processEOF(self):
+ self.parser.phases["inBody"].processEOF()
+
+ def processCharacters(self, token):
+ return self.parser.phases["inBody"].processCharacters(token)
+
+ def startTagTableOther(self, token):
+ if (self.tree.elementInScope("td", variant="table") or
+ self.tree.elementInScope("th", variant="table")):
+ self.closeCell()
+ return token
+ else:
+ # innerHTML case
+ assert self.parser.innerHTML
+ self.parser.parseError()
+
+ def startTagOther(self, token):
+ return self.parser.phases["inBody"].processStartTag(token)
+
+ def endTagTableCell(self, token):
+ if self.tree.elementInScope(token["name"], variant="table"):
+ self.tree.generateImpliedEndTags(token["name"])
+ if self.tree.openElements[-1].name != token["name"]:
+ self.parser.parseError("unexpected-cell-end-tag",
+ {"name": token["name"]})
+ while True:
+ node = self.tree.openElements.pop()
+ if node.name == token["name"]:
+ break
+ else:
+ self.tree.openElements.pop()
+ self.tree.clearActiveFormattingElements()
+ self.parser.phase = self.parser.phases["inRow"]
+ else:
+ self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
+
+ def endTagIgnore(self, token):
+ self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
+
+ def endTagImply(self, token):
+ if self.tree.elementInScope(token["name"], variant="table"):
+ self.closeCell()
+ return token
+ else:
+ # sometimes innerHTML case
+ self.parser.parseError()
+
+ def endTagOther(self, token):
+ return self.parser.phases["inBody"].processEndTag(token)
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", Phase.startTagHtml),
+ (("caption", "col", "colgroup", "tbody", "td", "tfoot", "th",
+ "thead", "tr"), startTagTableOther)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ (("td", "th"), endTagTableCell),
+ (("body", "caption", "col", "colgroup", "html"), endTagIgnore),
+ (("table", "tbody", "tfoot", "thead", "tr"), endTagImply)
+ ])
+ endTagHandler.default = endTagOther
+
+ class InSelectPhase(Phase):
+ __slots__ = tuple()
+
+ # http://www.whatwg.org/specs/web-apps/current-work/#in-select
+ def processEOF(self):
+ if self.tree.openElements[-1].name != "html":
+ self.parser.parseError("eof-in-select")
+ else:
+ assert self.parser.innerHTML
+
+ def processCharacters(self, token):
+ if token["data"] == "\u0000":
+ return
+ self.tree.insertText(token["data"])
+
+ def startTagOption(self, token):
+ # We need to imply </option> if <option> is the current node.
+ if self.tree.openElements[-1].name == "option":
+ self.tree.openElements.pop()
+ self.tree.insertElement(token)
+
+ def startTagOptgroup(self, token):
+ if self.tree.openElements[-1].name == "option":
+ self.tree.openElements.pop()
+ if self.tree.openElements[-1].name == "optgroup":
+ self.tree.openElements.pop()
+ self.tree.insertElement(token)
+
+ def startTagSelect(self, token):
+ self.parser.parseError("unexpected-select-in-select")
+ self.endTagSelect(impliedTagToken("select"))
+
+ def startTagInput(self, token):
+ self.parser.parseError("unexpected-input-in-select")
+ if self.tree.elementInScope("select", variant="select"):
+ self.endTagSelect(impliedTagToken("select"))
+ return token
+ else:
+ assert self.parser.innerHTML
+
+ def startTagScript(self, token):
+ return self.parser.phases["inHead"].processStartTag(token)
+
+ def startTagOther(self, token):
+ self.parser.parseError("unexpected-start-tag-in-select",
+ {"name": token["name"]})
+
+ def endTagOption(self, token):
+ if self.tree.openElements[-1].name == "option":
+ self.tree.openElements.pop()
+ else:
+ self.parser.parseError("unexpected-end-tag-in-select",
+ {"name": "option"})
+
+ def endTagOptgroup(self, token):
+ # </optgroup> implicitly closes <option>
+ if (self.tree.openElements[-1].name == "option" and
+ self.tree.openElements[-2].name == "optgroup"):
+ self.tree.openElements.pop()
+ # It also closes </optgroup>
+ if self.tree.openElements[-1].name == "optgroup":
+ self.tree.openElements.pop()
+ # But nothing else
+ else:
+ self.parser.parseError("unexpected-end-tag-in-select",
+ {"name": "optgroup"})
+
+ def endTagSelect(self, token):
+ if self.tree.elementInScope("select", variant="select"):
+ node = self.tree.openElements.pop()
+ while node.name != "select":
+ node = self.tree.openElements.pop()
+ self.parser.resetInsertionMode()
+ else:
+ # innerHTML case
+ assert self.parser.innerHTML
+ self.parser.parseError()
+
+ def endTagOther(self, token):
+ self.parser.parseError("unexpected-end-tag-in-select",
+ {"name": token["name"]})
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", Phase.startTagHtml),
+ ("option", startTagOption),
+ ("optgroup", startTagOptgroup),
+ ("select", startTagSelect),
+ (("input", "keygen", "textarea"), startTagInput),
+ ("script", startTagScript)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ ("option", endTagOption),
+ ("optgroup", endTagOptgroup),
+ ("select", endTagSelect)
+ ])
+ endTagHandler.default = endTagOther
+
+ class InSelectInTablePhase(Phase):
+ __slots__ = tuple()
+
+ def processEOF(self):
+ self.parser.phases["inSelect"].processEOF()
+
+ def processCharacters(self, token):
+ return self.parser.phases["inSelect"].processCharacters(token)
+
+ def startTagTable(self, token):
+ self.parser.parseError("unexpected-table-element-start-tag-in-select-in-table", {"name": token["name"]})
+ self.endTagOther(impliedTagToken("select"))
+ return token
+
+ def startTagOther(self, token):
+ return self.parser.phases["inSelect"].processStartTag(token)
+
+ def endTagTable(self, token):
+ self.parser.parseError("unexpected-table-element-end-tag-in-select-in-table", {"name": token["name"]})
+ if self.tree.elementInScope(token["name"], variant="table"):
+ self.endTagOther(impliedTagToken("select"))
+ return token
+
+ def endTagOther(self, token):
+ return self.parser.phases["inSelect"].processEndTag(token)
+
+ startTagHandler = _utils.MethodDispatcher([
+ (("caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"),
+ startTagTable)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ (("caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"),
+ endTagTable)
+ ])
+ endTagHandler.default = endTagOther
+
+ class InForeignContentPhase(Phase):
+ __slots__ = tuple()
+
+ breakoutElements = frozenset(["b", "big", "blockquote", "body", "br",
+ "center", "code", "dd", "div", "dl", "dt",
+ "em", "embed", "h1", "h2", "h3",
+ "h4", "h5", "h6", "head", "hr", "i", "img",
+ "li", "listing", "menu", "meta", "nobr",
+ "ol", "p", "pre", "ruby", "s", "small",
+ "span", "strong", "strike", "sub", "sup",
+ "table", "tt", "u", "ul", "var"])
+
+ def adjustSVGTagNames(self, token):
+ replacements = {"altglyph": "altGlyph",
+ "altglyphdef": "altGlyphDef",
+ "altglyphitem": "altGlyphItem",
+ "animatecolor": "animateColor",
+ "animatemotion": "animateMotion",
+ "animatetransform": "animateTransform",
+ "clippath": "clipPath",
+ "feblend": "feBlend",
+ "fecolormatrix": "feColorMatrix",
+ "fecomponenttransfer": "feComponentTransfer",
+ "fecomposite": "feComposite",
+ "feconvolvematrix": "feConvolveMatrix",
+ "fediffuselighting": "feDiffuseLighting",
+ "fedisplacementmap": "feDisplacementMap",
+ "fedistantlight": "feDistantLight",
+ "feflood": "feFlood",
+ "fefunca": "feFuncA",
+ "fefuncb": "feFuncB",
+ "fefuncg": "feFuncG",
+ "fefuncr": "feFuncR",
+ "fegaussianblur": "feGaussianBlur",
+ "feimage": "feImage",
+ "femerge": "feMerge",
+ "femergenode": "feMergeNode",
+ "femorphology": "feMorphology",
+ "feoffset": "feOffset",
+ "fepointlight": "fePointLight",
+ "fespecularlighting": "feSpecularLighting",
+ "fespotlight": "feSpotLight",
+ "fetile": "feTile",
+ "feturbulence": "feTurbulence",
+ "foreignobject": "foreignObject",
+ "glyphref": "glyphRef",
+ "lineargradient": "linearGradient",
+ "radialgradient": "radialGradient",
+ "textpath": "textPath"}
+
+ if token["name"] in replacements:
+ token["name"] = replacements[token["name"]]
+
+ def processCharacters(self, token):
+ if token["data"] == "\u0000":
+ token["data"] = "\uFFFD"
+ elif (self.parser.framesetOK and
+ any(char not in spaceCharacters for char in token["data"])):
+ self.parser.framesetOK = False
+ Phase.processCharacters(self, token)
+
+ def processStartTag(self, token):
+ currentNode = self.tree.openElements[-1]
+ if (token["name"] in self.breakoutElements or
+ (token["name"] == "font" and
+ set(token["data"].keys()) & {"color", "face", "size"})):
+ self.parser.parseError("unexpected-html-element-in-foreign-content",
+ {"name": token["name"]})
+ while (self.tree.openElements[-1].namespace !=
+ self.tree.defaultNamespace and
+ not self.parser.isHTMLIntegrationPoint(self.tree.openElements[-1]) and
+ not self.parser.isMathMLTextIntegrationPoint(self.tree.openElements[-1])):
+ self.tree.openElements.pop()
+ return token
+
+ else:
+ if currentNode.namespace == namespaces["mathml"]:
+ self.parser.adjustMathMLAttributes(token)
+ elif currentNode.namespace == namespaces["svg"]:
+ self.adjustSVGTagNames(token)
+ self.parser.adjustSVGAttributes(token)
+ self.parser.adjustForeignAttributes(token)
+ token["namespace"] = currentNode.namespace
+ self.tree.insertElement(token)
+ if token["selfClosing"]:
+ self.tree.openElements.pop()
+ token["selfClosingAcknowledged"] = True
+
+ def processEndTag(self, token):
+ nodeIndex = len(self.tree.openElements) - 1
+ node = self.tree.openElements[-1]
+ if node.name.translate(asciiUpper2Lower) != token["name"]:
+ self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
+
+ while True:
+ if node.name.translate(asciiUpper2Lower) == token["name"]:
+ # XXX this isn't in the spec but it seems necessary
+ if self.parser.phase == self.parser.phases["inTableText"]:
+ self.parser.phase.flushCharacters()
+ self.parser.phase = self.parser.phase.originalPhase
+ while self.tree.openElements.pop() != node:
+ assert self.tree.openElements
+ new_token = None
+ break
+ nodeIndex -= 1
+
+ node = self.tree.openElements[nodeIndex]
+ if node.namespace != self.tree.defaultNamespace:
+ continue
+ else:
+ new_token = self.parser.phase.processEndTag(token)
+ break
+ return new_token
+
+ class AfterBodyPhase(Phase):
+ __slots__ = tuple()
+
+ def processEOF(self):
+ # Stop parsing
+ pass
+
+ def processComment(self, token):
+ # This is needed because data is to be appended to the <html> element
+ # here and not to whatever is currently open.
+ self.tree.insertComment(token, self.tree.openElements[0])
+
+ def processCharacters(self, token):
+ self.parser.parseError("unexpected-char-after-body")
+ self.parser.phase = self.parser.phases["inBody"]
+ return token
+
+ def startTagHtml(self, token):
+ return self.parser.phases["inBody"].processStartTag(token)
+
+ def startTagOther(self, token):
+ self.parser.parseError("unexpected-start-tag-after-body",
+ {"name": token["name"]})
+ self.parser.phase = self.parser.phases["inBody"]
+ return token
+
+ def endTagHtml(self, name):
+ if self.parser.innerHTML:
+ self.parser.parseError("unexpected-end-tag-after-body-innerhtml")
+ else:
+ self.parser.phase = self.parser.phases["afterAfterBody"]
+
+ def endTagOther(self, token):
+ self.parser.parseError("unexpected-end-tag-after-body",
+ {"name": token["name"]})
+ self.parser.phase = self.parser.phases["inBody"]
+ return token
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", startTagHtml)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([("html", endTagHtml)])
+ endTagHandler.default = endTagOther
+
+ class InFramesetPhase(Phase):
+ # http://www.whatwg.org/specs/web-apps/current-work/#in-frameset
+ __slots__ = tuple()
+
+ def processEOF(self):
+ if self.tree.openElements[-1].name != "html":
+ self.parser.parseError("eof-in-frameset")
+ else:
+ assert self.parser.innerHTML
+
+ def processCharacters(self, token):
+ self.parser.parseError("unexpected-char-in-frameset")
+
+ def startTagFrameset(self, token):
+ self.tree.insertElement(token)
+
+ def startTagFrame(self, token):
+ self.tree.insertElement(token)
+ self.tree.openElements.pop()
+
+ def startTagNoframes(self, token):
+ return self.parser.phases["inBody"].processStartTag(token)
+
+ def startTagOther(self, token):
+ self.parser.parseError("unexpected-start-tag-in-frameset",
+ {"name": token["name"]})
+
+ def endTagFrameset(self, token):
+ if self.tree.openElements[-1].name == "html":
+ # innerHTML case
+ self.parser.parseError("unexpected-frameset-in-frameset-innerhtml")
+ else:
+ self.tree.openElements.pop()
+ if (not self.parser.innerHTML and
+ self.tree.openElements[-1].name != "frameset"):
+ # If we're not in innerHTML mode and the current node is not a
+ # "frameset" element (anymore) then switch.
+ self.parser.phase = self.parser.phases["afterFrameset"]
+
+ def endTagOther(self, token):
+ self.parser.parseError("unexpected-end-tag-in-frameset",
+ {"name": token["name"]})
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", Phase.startTagHtml),
+ ("frameset", startTagFrameset),
+ ("frame", startTagFrame),
+ ("noframes", startTagNoframes)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ ("frameset", endTagFrameset)
+ ])
+ endTagHandler.default = endTagOther
+
+ class AfterFramesetPhase(Phase):
+ # http://www.whatwg.org/specs/web-apps/current-work/#after3
+ __slots__ = tuple()
+
+ def processEOF(self):
+ # Stop parsing
+ pass
+
+ def processCharacters(self, token):
+ self.parser.parseError("unexpected-char-after-frameset")
+
+ def startTagNoframes(self, token):
+ return self.parser.phases["inHead"].processStartTag(token)
+
+ def startTagOther(self, token):
+ self.parser.parseError("unexpected-start-tag-after-frameset",
+ {"name": token["name"]})
+
+ def endTagHtml(self, token):
+ self.parser.phase = self.parser.phases["afterAfterFrameset"]
+
+ def endTagOther(self, token):
+ self.parser.parseError("unexpected-end-tag-after-frameset",
+ {"name": token["name"]})
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", Phase.startTagHtml),
+ ("noframes", startTagNoframes)
+ ])
+ startTagHandler.default = startTagOther
+
+ endTagHandler = _utils.MethodDispatcher([
+ ("html", endTagHtml)
+ ])
+ endTagHandler.default = endTagOther
+
+ class AfterAfterBodyPhase(Phase):
+ __slots__ = tuple()
+
+ def processEOF(self):
+ pass
+
+ def processComment(self, token):
+ self.tree.insertComment(token, self.tree.document)
+
+ def processSpaceCharacters(self, token):
+ return self.parser.phases["inBody"].processSpaceCharacters(token)
+
+ def processCharacters(self, token):
+ self.parser.parseError("expected-eof-but-got-char")
+ self.parser.phase = self.parser.phases["inBody"]
+ return token
+
+ def startTagHtml(self, token):
+ return self.parser.phases["inBody"].processStartTag(token)
+
+ def startTagOther(self, token):
+ self.parser.parseError("expected-eof-but-got-start-tag",
+ {"name": token["name"]})
+ self.parser.phase = self.parser.phases["inBody"]
+ return token
+
+ def processEndTag(self, token):
+ self.parser.parseError("expected-eof-but-got-end-tag",
+ {"name": token["name"]})
+ self.parser.phase = self.parser.phases["inBody"]
+ return token
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", startTagHtml)
+ ])
+ startTagHandler.default = startTagOther
+
+ class AfterAfterFramesetPhase(Phase):
+ __slots__ = tuple()
+
+ def processEOF(self):
+ pass
+
+ def processComment(self, token):
+ self.tree.insertComment(token, self.tree.document)
+
+ def processSpaceCharacters(self, token):
+ return self.parser.phases["inBody"].processSpaceCharacters(token)
+
+ def processCharacters(self, token):
+ self.parser.parseError("expected-eof-but-got-char")
+
+ def startTagHtml(self, token):
+ return self.parser.phases["inBody"].processStartTag(token)
+
+ def startTagNoFrames(self, token):
+ return self.parser.phases["inHead"].processStartTag(token)
+
+ def startTagOther(self, token):
+ self.parser.parseError("expected-eof-but-got-start-tag",
+ {"name": token["name"]})
+
+ def processEndTag(self, token):
+ self.parser.parseError("expected-eof-but-got-end-tag",
+ {"name": token["name"]})
+
+ startTagHandler = _utils.MethodDispatcher([
+ ("html", startTagHtml),
+ ("noframes", startTagNoFrames)
+ ])
+ startTagHandler.default = startTagOther
+
+ # pylint:enable=unused-argument
+
+ return {
+ "initial": InitialPhase,
+ "beforeHtml": BeforeHtmlPhase,
+ "beforeHead": BeforeHeadPhase,
+ "inHead": InHeadPhase,
+ "inHeadNoscript": InHeadNoscriptPhase,
+ "afterHead": AfterHeadPhase,
+ "inBody": InBodyPhase,
+ "text": TextPhase,
+ "inTable": InTablePhase,
+ "inTableText": InTableTextPhase,
+ "inCaption": InCaptionPhase,
+ "inColumnGroup": InColumnGroupPhase,
+ "inTableBody": InTableBodyPhase,
+ "inRow": InRowPhase,
+ "inCell": InCellPhase,
+ "inSelect": InSelectPhase,
+ "inSelectInTable": InSelectInTablePhase,
+ "inForeignContent": InForeignContentPhase,
+ "afterBody": AfterBodyPhase,
+ "inFrameset": InFramesetPhase,
+ "afterFrameset": AfterFramesetPhase,
+ "afterAfterBody": AfterAfterBodyPhase,
+ "afterAfterFrameset": AfterAfterFramesetPhase,
+ # XXX after after frameset
+ }
+
+
+def adjust_attributes(token, replacements):
+ needs_adjustment = viewkeys(token['data']) & viewkeys(replacements)
+ if needs_adjustment:
+ token['data'] = type(token['data'])((replacements.get(k, k), v)
+ for k, v in token['data'].items())
+
+
+def impliedTagToken(name, type="EndTag", attributes=None,
+ selfClosing=False):
+ if attributes is None:
+ attributes = {}
+ return {"type": tokenTypes[type], "name": name, "data": attributes,
+ "selfClosing": selfClosing}
+
+
+class ParseError(Exception):
+ """Error in parsed document"""
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/serializer.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/serializer.py
new file mode 100644
index 0000000000..c66df68392
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/serializer.py
@@ -0,0 +1,409 @@
+from __future__ import absolute_import, division, unicode_literals
+from six import text_type
+
+import re
+
+from codecs import register_error, xmlcharrefreplace_errors
+
+from .constants import voidElements, booleanAttributes, spaceCharacters
+from .constants import rcdataElements, entities, xmlEntities
+from . import treewalkers, _utils
+from xml.sax.saxutils import escape
+
+_quoteAttributeSpecChars = "".join(spaceCharacters) + "\"'=<>`"
+_quoteAttributeSpec = re.compile("[" + _quoteAttributeSpecChars + "]")
+_quoteAttributeLegacy = re.compile("[" + _quoteAttributeSpecChars +
+ "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n"
+ "\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15"
+ "\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
+ "\x20\x2f\x60\xa0\u1680\u180e\u180f\u2000"
+ "\u2001\u2002\u2003\u2004\u2005\u2006\u2007"
+ "\u2008\u2009\u200a\u2028\u2029\u202f\u205f"
+ "\u3000]")
+
+
+_encode_entity_map = {}
+_is_ucs4 = len("\U0010FFFF") == 1
+for k, v in list(entities.items()):
+ # skip multi-character entities
+ if ((_is_ucs4 and len(v) > 1) or
+ (not _is_ucs4 and len(v) > 2)):
+ continue
+ if v != "&":
+ if len(v) == 2:
+ v = _utils.surrogatePairToCodepoint(v)
+ else:
+ v = ord(v)
+ if v not in _encode_entity_map or k.islower():
+ # prefer &lt; over &LT; and similarly for &amp;, &gt;, etc.
+ _encode_entity_map[v] = k
+
+
+def htmlentityreplace_errors(exc):
+ if isinstance(exc, (UnicodeEncodeError, UnicodeTranslateError)):
+ res = []
+ codepoints = []
+ skip = False
+ for i, c in enumerate(exc.object[exc.start:exc.end]):
+ if skip:
+ skip = False
+ continue
+ index = i + exc.start
+ if _utils.isSurrogatePair(exc.object[index:min([exc.end, index + 2])]):
+ codepoint = _utils.surrogatePairToCodepoint(exc.object[index:index + 2])
+ skip = True
+ else:
+ codepoint = ord(c)
+ codepoints.append(codepoint)
+ for cp in codepoints:
+ e = _encode_entity_map.get(cp)
+ if e:
+ res.append("&")
+ res.append(e)
+ if not e.endswith(";"):
+ res.append(";")
+ else:
+ res.append("&#x%s;" % (hex(cp)[2:]))
+ return ("".join(res), exc.end)
+ else:
+ return xmlcharrefreplace_errors(exc)
+
+
+register_error("htmlentityreplace", htmlentityreplace_errors)
+
+
+def serialize(input, tree="etree", encoding=None, **serializer_opts):
+ """Serializes the input token stream using the specified treewalker
+
+ :arg input: the token stream to serialize
+
+ :arg tree: the treewalker to use
+
+ :arg encoding: the encoding to use
+
+ :arg serializer_opts: any options to pass to the
+ :py:class:`html5lib.serializer.HTMLSerializer` that gets created
+
+ :returns: the tree serialized as a string
+
+ Example:
+
+ >>> from html5lib.html5parser import parse
+ >>> from html5lib.serializer import serialize
+ >>> token_stream = parse('<html><body><p>Hi!</p></body></html>')
+ >>> serialize(token_stream, omit_optional_tags=False)
+ '<html><head></head><body><p>Hi!</p></body></html>'
+
+ """
+ # XXX: Should we cache this?
+ walker = treewalkers.getTreeWalker(tree)
+ s = HTMLSerializer(**serializer_opts)
+ return s.render(walker(input), encoding)
+
+
+class HTMLSerializer(object):
+
+ # attribute quoting options
+ quote_attr_values = "legacy" # be secure by default
+ quote_char = '"'
+ use_best_quote_char = True
+
+ # tag syntax options
+ omit_optional_tags = True
+ minimize_boolean_attributes = True
+ use_trailing_solidus = False
+ space_before_trailing_solidus = True
+
+ # escaping options
+ escape_lt_in_attrs = False
+ escape_rcdata = False
+ resolve_entities = True
+
+ # miscellaneous options
+ alphabetical_attributes = False
+ inject_meta_charset = True
+ strip_whitespace = False
+ sanitize = False
+
+ options = ("quote_attr_values", "quote_char", "use_best_quote_char",
+ "omit_optional_tags", "minimize_boolean_attributes",
+ "use_trailing_solidus", "space_before_trailing_solidus",
+ "escape_lt_in_attrs", "escape_rcdata", "resolve_entities",
+ "alphabetical_attributes", "inject_meta_charset",
+ "strip_whitespace", "sanitize")
+
+ def __init__(self, **kwargs):
+ """Initialize HTMLSerializer
+
+ :arg inject_meta_charset: Whether or not to inject the meta charset.
+
+ Defaults to ``True``.
+
+ :arg quote_attr_values: Whether to quote attribute values that don't
+ require quoting per legacy browser behavior (``"legacy"``), when
+ required by the standard (``"spec"``), or always (``"always"``).
+
+ Defaults to ``"legacy"``.
+
+ :arg quote_char: Use given quote character for attribute quoting.
+
+ Defaults to ``"`` which will use double quotes unless attribute
+ value contains a double quote, in which case single quotes are
+ used.
+
+ :arg escape_lt_in_attrs: Whether or not to escape ``<`` in attribute
+ values.
+
+ Defaults to ``False``.
+
+ :arg escape_rcdata: Whether to escape characters that need to be
+ escaped within normal elements within rcdata elements such as
+ style.
+
+ Defaults to ``False``.
+
+ :arg resolve_entities: Whether to resolve named character entities that
+ appear in the source tree. The XML predefined entities &lt; &gt;
+ &amp; &quot; &apos; are unaffected by this setting.
+
+ Defaults to ``True``.
+
+ :arg strip_whitespace: Whether to remove semantically meaningless
+ whitespace. (This compresses all whitespace to a single space
+ except within ``pre``.)
+
+ Defaults to ``False``.
+
+ :arg minimize_boolean_attributes: Shortens boolean attributes to give
+ just the attribute value, for example::
+
+ <input disabled="disabled">
+
+ becomes::
+
+ <input disabled>
+
+ Defaults to ``True``.
+
+ :arg use_trailing_solidus: Includes a close-tag slash at the end of the
+ start tag of void elements (empty elements whose end tag is
+ forbidden). E.g. ``<hr/>``.
+
+ Defaults to ``False``.
+
+ :arg space_before_trailing_solidus: Places a space immediately before
+ the closing slash in a tag using a trailing solidus. E.g.
+ ``<hr />``. Requires ``use_trailing_solidus=True``.
+
+ Defaults to ``True``.
+
+ :arg sanitize: Strip all unsafe or unknown constructs from output.
+ See :py:class:`html5lib.filters.sanitizer.Filter`.
+
+ Defaults to ``False``.
+
+ :arg omit_optional_tags: Omit start/end tags that are optional.
+
+ Defaults to ``True``.
+
+ :arg alphabetical_attributes: Reorder attributes to be in alphabetical order.
+
+ Defaults to ``False``.
+
+ """
+ unexpected_args = frozenset(kwargs) - frozenset(self.options)
+ if len(unexpected_args) > 0:
+ raise TypeError("__init__() got an unexpected keyword argument '%s'" % next(iter(unexpected_args)))
+ if 'quote_char' in kwargs:
+ self.use_best_quote_char = False
+ for attr in self.options:
+ setattr(self, attr, kwargs.get(attr, getattr(self, attr)))
+ self.errors = []
+ self.strict = False
+
+ def encode(self, string):
+ assert(isinstance(string, text_type))
+ if self.encoding:
+ return string.encode(self.encoding, "htmlentityreplace")
+ else:
+ return string
+
+ def encodeStrict(self, string):
+ assert(isinstance(string, text_type))
+ if self.encoding:
+ return string.encode(self.encoding, "strict")
+ else:
+ return string
+
+ def serialize(self, treewalker, encoding=None):
+ # pylint:disable=too-many-nested-blocks
+ self.encoding = encoding
+ in_cdata = False
+ self.errors = []
+
+ if encoding and self.inject_meta_charset:
+ from .filters.inject_meta_charset import Filter
+ treewalker = Filter(treewalker, encoding)
+ # Alphabetical attributes is here under the assumption that none of
+ # the later filters add or change order of attributes; it needs to be
+ # before the sanitizer so escaped elements come out correctly
+ if self.alphabetical_attributes:
+ from .filters.alphabeticalattributes import Filter
+ treewalker = Filter(treewalker)
+ # WhitespaceFilter should be used before OptionalTagFilter
+ # for maximum efficiently of this latter filter
+ if self.strip_whitespace:
+ from .filters.whitespace import Filter
+ treewalker = Filter(treewalker)
+ if self.sanitize:
+ from .filters.sanitizer import Filter
+ treewalker = Filter(treewalker)
+ if self.omit_optional_tags:
+ from .filters.optionaltags import Filter
+ treewalker = Filter(treewalker)
+
+ for token in treewalker:
+ type = token["type"]
+ if type == "Doctype":
+ doctype = "<!DOCTYPE %s" % token["name"]
+
+ if token["publicId"]:
+ doctype += ' PUBLIC "%s"' % token["publicId"]
+ elif token["systemId"]:
+ doctype += " SYSTEM"
+ if token["systemId"]:
+ if token["systemId"].find('"') >= 0:
+ if token["systemId"].find("'") >= 0:
+ self.serializeError("System identifier contains both single and double quote characters")
+ quote_char = "'"
+ else:
+ quote_char = '"'
+ doctype += " %s%s%s" % (quote_char, token["systemId"], quote_char)
+
+ doctype += ">"
+ yield self.encodeStrict(doctype)
+
+ elif type in ("Characters", "SpaceCharacters"):
+ if type == "SpaceCharacters" or in_cdata:
+ if in_cdata and token["data"].find("</") >= 0:
+ self.serializeError("Unexpected </ in CDATA")
+ yield self.encode(token["data"])
+ else:
+ yield self.encode(escape(token["data"]))
+
+ elif type in ("StartTag", "EmptyTag"):
+ name = token["name"]
+ yield self.encodeStrict("<%s" % name)
+ if name in rcdataElements and not self.escape_rcdata:
+ in_cdata = True
+ elif in_cdata:
+ self.serializeError("Unexpected child element of a CDATA element")
+ for (_, attr_name), attr_value in token["data"].items():
+ # TODO: Add namespace support here
+ k = attr_name
+ v = attr_value
+ yield self.encodeStrict(' ')
+
+ yield self.encodeStrict(k)
+ if not self.minimize_boolean_attributes or \
+ (k not in booleanAttributes.get(name, tuple()) and
+ k not in booleanAttributes.get("", tuple())):
+ yield self.encodeStrict("=")
+ if self.quote_attr_values == "always" or len(v) == 0:
+ quote_attr = True
+ elif self.quote_attr_values == "spec":
+ quote_attr = _quoteAttributeSpec.search(v) is not None
+ elif self.quote_attr_values == "legacy":
+ quote_attr = _quoteAttributeLegacy.search(v) is not None
+ else:
+ raise ValueError("quote_attr_values must be one of: "
+ "'always', 'spec', or 'legacy'")
+ v = v.replace("&", "&amp;")
+ if self.escape_lt_in_attrs:
+ v = v.replace("<", "&lt;")
+ if quote_attr:
+ quote_char = self.quote_char
+ if self.use_best_quote_char:
+ if "'" in v and '"' not in v:
+ quote_char = '"'
+ elif '"' in v and "'" not in v:
+ quote_char = "'"
+ if quote_char == "'":
+ v = v.replace("'", "&#39;")
+ else:
+ v = v.replace('"', "&quot;")
+ yield self.encodeStrict(quote_char)
+ yield self.encode(v)
+ yield self.encodeStrict(quote_char)
+ else:
+ yield self.encode(v)
+ if name in voidElements and self.use_trailing_solidus:
+ if self.space_before_trailing_solidus:
+ yield self.encodeStrict(" /")
+ else:
+ yield self.encodeStrict("/")
+ yield self.encode(">")
+
+ elif type == "EndTag":
+ name = token["name"]
+ if name in rcdataElements:
+ in_cdata = False
+ elif in_cdata:
+ self.serializeError("Unexpected child element of a CDATA element")
+ yield self.encodeStrict("</%s>" % name)
+
+ elif type == "Comment":
+ data = token["data"]
+ if data.find("--") >= 0:
+ self.serializeError("Comment contains --")
+ yield self.encodeStrict("<!--%s-->" % token["data"])
+
+ elif type == "Entity":
+ name = token["name"]
+ key = name + ";"
+ if key not in entities:
+ self.serializeError("Entity %s not recognized" % name)
+ if self.resolve_entities and key not in xmlEntities:
+ data = entities[key]
+ else:
+ data = "&%s;" % name
+ yield self.encodeStrict(data)
+
+ else:
+ self.serializeError(token["data"])
+
+ def render(self, treewalker, encoding=None):
+ """Serializes the stream from the treewalker into a string
+
+ :arg treewalker: the treewalker to serialize
+
+ :arg encoding: the string encoding to use
+
+ :returns: the serialized tree
+
+ Example:
+
+ >>> from html5lib import parse, getTreeWalker
+ >>> from html5lib.serializer import HTMLSerializer
+ >>> token_stream = parse('<html><body>Hi!</body></html>')
+ >>> walker = getTreeWalker('etree')
+ >>> serializer = HTMLSerializer(omit_optional_tags=False)
+ >>> serializer.render(walker(token_stream))
+ '<html><head></head><body>Hi!</body></html>'
+
+ """
+ if encoding:
+ return b"".join(list(self.serialize(treewalker, encoding)))
+ else:
+ return "".join(list(self.serialize(treewalker)))
+
+ def serializeError(self, data="XXX ERROR MESSAGE NEEDED"):
+ # XXX The idea is to make data mandatory.
+ self.errors.append(data)
+ if self.strict:
+ raise SerializeError
+
+
+class SerializeError(Exception):
+ """Error in serialized tree"""
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/__init__.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/__init__.py
new file mode 100644
index 0000000000..b8ce2de32e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/__init__.py
@@ -0,0 +1 @@
+from __future__ import absolute_import, division, unicode_literals
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/conftest.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/conftest.py
new file mode 100644
index 0000000000..dad167c583
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/conftest.py
@@ -0,0 +1,108 @@
+from __future__ import print_function
+import os.path
+import sys
+
+import pkg_resources
+import pytest
+
+from .tree_construction import TreeConstructionFile
+from .tokenizer import TokenizerFile
+from .sanitizer import SanitizerFile
+
+_dir = os.path.abspath(os.path.dirname(__file__))
+_root = os.path.join(_dir, "..", "..")
+_testdata = os.path.join(_dir, "testdata")
+_tree_construction = os.path.join(_testdata, "tree-construction")
+_tokenizer = os.path.join(_testdata, "tokenizer")
+_sanitizer_testdata = os.path.join(_dir, "sanitizer-testdata")
+
+
+def fail_if_missing_pytest_expect():
+ """Throws an exception halting pytest if pytest-expect isn't working"""
+ try:
+ from pytest_expect import expect # noqa
+ except ImportError:
+ header = '*' * 78
+ print(
+ '\n' +
+ header + '\n' +
+ 'ERROR: Either pytest-expect or its dependency u-msgpack-python is not\n' +
+ 'installed. Please install them both before running pytest.\n' +
+ header + '\n',
+ file=sys.stderr
+ )
+ raise
+
+
+fail_if_missing_pytest_expect()
+
+
+def pytest_configure(config):
+ msgs = []
+
+ if not os.path.exists(_testdata):
+ msg = "testdata not available! "
+ if os.path.exists(os.path.join(_root, ".git")):
+ msg += ("Please run git submodule update --init --recursive " +
+ "and then run tests again.")
+ else:
+ msg += ("The testdata doesn't appear to be included with this package, " +
+ "so finding the right version will be hard. :(")
+ msgs.append(msg)
+
+ if config.option.update_xfail:
+ # Check for optional requirements
+ req_file = os.path.join(_root, "requirements-optional.txt")
+ if os.path.exists(req_file):
+ with open(req_file, "r") as fp:
+ for line in fp:
+ if (line.strip() and
+ not (line.startswith("-r") or
+ line.startswith("#"))):
+ if ";" in line:
+ spec, marker = line.strip().split(";", 1)
+ else:
+ spec, marker = line.strip(), None
+ req = pkg_resources.Requirement.parse(spec)
+ if marker and not pkg_resources.evaluate_marker(marker):
+ msgs.append("%s not available in this environment" % spec)
+ else:
+ try:
+ installed = pkg_resources.working_set.find(req)
+ except pkg_resources.VersionConflict:
+ msgs.append("Outdated version of %s installed, need %s" % (req.name, spec))
+ else:
+ if not installed:
+ msgs.append("Need %s" % spec)
+
+ # Check cElementTree
+ import xml.etree.ElementTree as ElementTree
+
+ try:
+ import xml.etree.cElementTree as cElementTree
+ except ImportError:
+ msgs.append("cElementTree unable to be imported")
+ else:
+ if cElementTree.Element is ElementTree.Element:
+ msgs.append("cElementTree is just an alias for ElementTree")
+
+ if msgs:
+ pytest.exit("\n".join(msgs))
+
+
+def pytest_collect_file(path, parent):
+ dir = os.path.abspath(path.dirname)
+ dir_and_parents = set()
+ while dir not in dir_and_parents:
+ dir_and_parents.add(dir)
+ dir = os.path.dirname(dir)
+
+ if _tree_construction in dir_and_parents:
+ if path.ext == ".dat":
+ return TreeConstructionFile(path, parent)
+ elif _tokenizer in dir_and_parents:
+ if path.ext == ".test":
+ return TokenizerFile(path, parent)
+ elif _sanitizer_testdata in dir_and_parents:
+ if path.ext == ".dat":
+ return SanitizerFile(path, parent)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/sanitizer-testdata/tests1.dat b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/sanitizer-testdata/tests1.dat
new file mode 100644
index 0000000000..74e8833686
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/sanitizer-testdata/tests1.dat
@@ -0,0 +1,433 @@
+[
+ {
+ "name": "IE_Comments",
+ "input": "<!--[if gte IE 4]><script>alert('XSS');</script><![endif]-->",
+ "output": ""
+ },
+
+ {
+ "name": "IE_Comments_2",
+ "input": "<![if !IE 5]><script>alert('XSS');</script><![endif]>",
+ "output": "&lt;script&gt;alert('XSS');&lt;/script&gt;"
+ },
+
+ {
+ "name": "allow_colons_in_path_component",
+ "input": "<a href=\"./this:that\">foo</a>",
+ "output": "<a href='./this:that'>foo</a>"
+ },
+
+ {
+ "name": "background_attribute",
+ "input": "<div background=\"javascript:alert('XSS')\"></div>",
+ "output": "<div></div>"
+ },
+
+ {
+ "name": "bgsound",
+ "input": "<bgsound src=\"javascript:alert('XSS');\" />",
+ "output": "&lt;bgsound src=\"javascript:alert('XSS');\"&gt;&lt;/bgsound&gt;"
+ },
+
+ {
+ "name": "div_background_image_unicode_encoded",
+ "input": "<div style=\"background-image:\u00a5\u00a2\u006C\u0028'\u006a\u0061\u00a6\u0061\u00a3\u0063\u00a2\u0069\u00a0\u00a4\u003a\u0061\u006c\u0065\u00a2\u00a4\u0028.1027\u0058.1053\u0053\u0027\u0029'\u0029\">foo</div>",
+ "output": "<div style=''>foo</div>"
+ },
+
+ {
+ "name": "div_expression",
+ "input": "<div style=\"width: expression(alert('XSS'));\">foo</div>",
+ "output": "<div style=''>foo</div>"
+ },
+
+ {
+ "name": "double_open_angle_brackets",
+ "input": "<img src=http://ha.ckers.org/scriptlet.html <",
+ "output": ""
+ },
+
+ {
+ "name": "double_open_angle_brackets_2",
+ "input": "<script src=http://ha.ckers.org/scriptlet.html <",
+ "output": ""
+ },
+
+ {
+ "name": "grave_accents",
+ "input": "<img src=`javascript:alert('XSS')` />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "img_dynsrc_lowsrc",
+ "input": "<img dynsrc=\"javascript:alert('XSS')\" />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "img_vbscript",
+ "input": "<img src='vbscript:msgbox(\"XSS\")' />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "input_image",
+ "input": "<input type=\"image\" src=\"javascript:alert('XSS');\" />",
+ "output": "<input type='image'/>"
+ },
+
+ {
+ "name": "link_stylesheets",
+ "input": "<link rel=\"stylesheet\" href=\"javascript:alert('XSS');\" />",
+ "output": "&lt;link href=\"javascript:alert('XSS');\" rel=\"stylesheet\"&gt;"
+ },
+
+ {
+ "name": "link_stylesheets_2",
+ "input": "<link rel=\"stylesheet\" href=\"http://ha.ckers.org/xss.css\" />",
+ "output": "&lt;link href=\"http://ha.ckers.org/xss.css\" rel=\"stylesheet\"&gt;"
+ },
+
+ {
+ "name": "list_style_image",
+ "input": "<li style=\"list-style-image: url(javascript:alert('XSS'))\">foo</li>",
+ "output": "<li style=''>foo</li>"
+ },
+
+ {
+ "name": "no_closing_script_tags",
+ "input": "<script src=http://ha.ckers.org/xss.js?<b>",
+ "output": "&lt;script src=\"http://ha.ckers.org/xss.js?&amp;lt;b\"&gt;&lt;/script&gt;"
+ },
+
+ {
+ "name": "non_alpha_non_digit",
+ "input": "<script/XSS src=\"http://ha.ckers.org/xss.js\"></script>",
+ "output": "&lt;script src=\"http://ha.ckers.org/xss.js\" xss=\"\"&gt;&lt;/script&gt;"
+ },
+
+ {
+ "name": "non_alpha_non_digit_2",
+ "input": "<a onclick!\\#$%&()*~+-_.,:;?@[/|\\]^`=alert(\"XSS\")>foo</a>",
+ "output": "<a>foo</a>"
+ },
+
+ {
+ "name": "non_alpha_non_digit_3",
+ "input": "<img/src=\"http://ha.ckers.org/xss.js\"/>",
+ "output": "<img src='http://ha.ckers.org/xss.js'/>"
+ },
+
+ {
+ "name": "non_alpha_non_digit_II",
+ "input": "<a href!\\#$%&()*~+-_.,:;?@[/|]^`=alert('XSS')>foo</a>",
+ "output": "<a>foo</a>"
+ },
+
+ {
+ "name": "non_alpha_non_digit_III",
+ "input": "<a/href=\"javascript:alert('XSS');\">foo</a>",
+ "output": "<a>foo</a>"
+ },
+
+ {
+ "name": "platypus",
+ "input": "<a href=\"http://www.ragingplatypus.com/\" style=\"display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;\">never trust your upstream platypus</a>",
+ "output": "<a href='http://www.ragingplatypus.com/' style='display: block; width: 100%; height: 100%; background-color: black; background-x: center; background-y: center;'>never trust your upstream platypus</a>"
+ },
+
+ {
+ "name": "protocol_resolution_in_script_tag",
+ "input": "<script src=//ha.ckers.org/.j></script>",
+ "output": "&lt;script src=\"//ha.ckers.org/.j\"&gt;&lt;/script&gt;"
+ },
+
+ {
+ "name": "should_allow_anchors",
+ "input": "<a href='foo' onclick='bar'><script>baz</script></a>",
+ "output": "<a href='foo'>&lt;script&gt;baz&lt;/script&gt;</a>"
+ },
+
+ {
+ "name": "should_allow_image_alt_attribute",
+ "input": "<img alt='foo' onclick='bar' />",
+ "output": "<img alt='foo'/>"
+ },
+
+ {
+ "name": "should_allow_image_height_attribute",
+ "input": "<img height='foo' onclick='bar' />",
+ "output": "<img height='foo'/>"
+ },
+
+ {
+ "name": "should_allow_image_src_attribute",
+ "input": "<img src='foo' onclick='bar' />",
+ "output": "<img src='foo'/>"
+ },
+
+ {
+ "name": "should_allow_image_width_attribute",
+ "input": "<img width='foo' onclick='bar' />",
+ "output": "<img width='foo'/>"
+ },
+
+ {
+ "name": "should_handle_blank_text",
+ "input": "",
+ "output": ""
+ },
+
+ {
+ "name": "should_handle_malformed_image_tags",
+ "input": "<img \"\"\"><script>alert(\"XSS\")</script>\">",
+ "output": "<img/>&lt;script&gt;alert(\"XSS\")&lt;/script&gt;\"&gt;"
+ },
+
+ {
+ "name": "should_handle_non_html",
+ "input": "abc",
+ "output": "abc"
+ },
+
+ {
+ "name": "should_not_fall_for_ridiculous_hack",
+ "input": "<img\nsrc\n=\n\"\nj\na\nv\na\ns\nc\nr\ni\np\nt\n:\na\nl\ne\nr\nt\n(\n'\nX\nS\nS\n'\n)\n\"\n />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_0",
+ "input": "<img src=\"javascript:alert('XSS');\" />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_1",
+ "input": "<img src=javascript:alert('XSS') />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_10",
+ "input": "<img src=\"jav&#x0A;ascript:alert('XSS');\" />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_11",
+ "input": "<img src=\"jav&#x0D;ascript:alert('XSS');\" />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_12",
+ "input": "<img src=\" &#14; javascript:alert('XSS');\" />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_13",
+ "input": "<img src=\"&#x20;javascript:alert('XSS');\" />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_14",
+ "input": "<img src=\"&#xA0;javascript:alert('XSS');\" />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_2",
+ "input": "<img src=\"JaVaScRiPt:alert('XSS')\" />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_3",
+ "input": "<img src='javascript:alert(&quot;XSS&quot;)' />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_4",
+ "input": "<img src='javascript:alert(String.fromCharCode(88,83,83))' />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_5",
+ "input": "<img src='&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;' />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_6",
+ "input": "<img src='&#0000106;&#0000097;&#0000118;&#0000097;&#0000115;&#0000099;&#0000114;&#0000105;&#0000112;&#0000116;&#0000058;&#0000097;&#0000108;&#0000101;&#0000114;&#0000116;&#0000040;&#0000039;&#0000088;&#0000083;&#0000083;&#0000039;&#0000041' />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_7",
+ "input": "<img src='&#x6A;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3A;&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x27;&#x58;&#x53;&#x53;&#x27;&#x29' />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_8",
+ "input": "<img src=\"jav\tascript:alert('XSS');\" />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_not_fall_for_xss_image_hack_9",
+ "input": "<img src=\"jav&#x09;ascript:alert('XSS');\" />",
+ "output": "<img/>"
+ },
+
+ {
+ "name": "should_sanitize_half_open_scripts",
+ "input": "<img src=\"javascript:alert('XSS')\"",
+ "output": ""
+ },
+
+ {
+ "name": "should_sanitize_invalid_script_tag",
+ "input": "<script/XSS SRC=\"http://ha.ckers.org/xss.js\"></script>",
+ "output": "&lt;script src=\"http://ha.ckers.org/xss.js\" xss=\"\"&gt;&lt;/script&gt;"
+ },
+
+ {
+ "name": "should_sanitize_script_tag_with_multiple_open_brackets",
+ "input": "<<script>alert(\"XSS\");//<</script>",
+ "output": "&lt;&lt;script&gt;alert(\"XSS\");//&lt;&lt;/script&gt;"
+ },
+
+ {
+ "name": "should_sanitize_script_tag_with_multiple_open_brackets_2",
+ "input": "<iframe src=http://ha.ckers.org/scriptlet.html\n<",
+ "output": ""
+ },
+
+ {
+ "name": "should_sanitize_tag_broken_up_by_null",
+ "input": "<scr\u0000ipt>alert(\"XSS\")</scr\u0000ipt>",
+ "output": "&lt;scr\ufffdipt&gt;alert(\"XSS\")&lt;/scr\ufffdipt&gt;"
+ },
+
+ {
+ "name": "should_sanitize_unclosed_script",
+ "input": "<script src=http://ha.ckers.org/xss.js?<b>",
+ "output": "&lt;script src=\"http://ha.ckers.org/xss.js?&amp;lt;b\"&gt;&lt;/script&gt;"
+ },
+
+ {
+ "name": "should_strip_href_attribute_in_a_with_bad_protocols",
+ "input": "<a href=\"javascript:XSS\" title=\"1\">boo</a>",
+ "output": "<a title='1'>boo</a>"
+ },
+
+ {
+ "name": "should_strip_href_attribute_in_a_with_bad_protocols_and_whitespace",
+ "input": "<a href=\" javascript:XSS\" title=\"1\">boo</a>",
+ "output": "<a title='1'>boo</a>"
+ },
+
+ {
+ "name": "should_strip_src_attribute_in_img_with_bad_protocols",
+ "input": "<img src=\"javascript:XSS\" title=\"1\">boo</img>",
+ "output": "<img title='1'/>boo"
+ },
+
+ {
+ "name": "should_strip_src_attribute_in_img_with_bad_protocols_and_whitespace",
+ "input": "<img src=\" javascript:XSS\" title=\"1\">boo</img>",
+ "output": "<img title='1'/>boo"
+ },
+
+ {
+ "name": "xml_base",
+ "input": "<div xml:base=\"javascript:alert('XSS');//\">foo</div>",
+ "output": "<div>foo</div>"
+ },
+
+ {
+ "name": "xul",
+ "input": "<p style=\"-moz-binding:url('http://ha.ckers.org/xssmoz.xml#xss')\">fubar</p>",
+ "output": "<p style=''>fubar</p>"
+ },
+
+ {
+ "name": "quotes_in_attributes",
+ "input": "<img src='foo' title='\"foo\" bar' />",
+ "output": "<img src='foo' title='\"foo\" bar'/>"
+ },
+
+ {
+ "name": "uri_refs_in_svg_attributes",
+ "input": "<svg><rect fill='url(#foo)' />",
+ "output": "<svg><rect fill='url(#foo)'></rect></svg>"
+ },
+
+ {
+ "name": "absolute_uri_refs_in_svg_attributes",
+ "input": "<svg><rect fill='url(http://bad.com/) #fff' />",
+ "output": "<svg><rect fill=' #fff'></rect></svg>"
+ },
+
+ {
+ "name": "uri_ref_with_space_in svg_attribute",
+ "input": "<svg><rect fill='url(\n#foo)' />",
+ "output": "<svg><rect fill='url(\n#foo)'></rect></svg>"
+ },
+
+ {
+ "name": "absolute_uri_ref_with_space_in svg_attribute",
+ "input": "<svg><rect fill=\"url(\nhttp://bad.com/)\" />",
+ "output": "<svg><rect fill=' '></rect></svg>"
+ },
+
+ {
+ "name": "allow_html5_image_tag",
+ "input": "<image src='foo' />",
+ "output": "<img src='foo'/>"
+ },
+
+ {
+ "name": "style_attr_end_with_nothing",
+ "input": "<div style=\"color: blue\" />",
+ "output": "<div style='color: blue;'></div>"
+ },
+
+ {
+ "name": "style_attr_end_with_space",
+ "input": "<div style=\"color: blue \" />",
+ "output": "<div style='color: blue ;'></div>"
+ },
+
+ {
+ "name": "style_attr_end_with_semicolon",
+ "input": "<div style=\"color: blue;\" />",
+ "output": "<div style='color: blue;'></div>"
+ },
+
+ {
+ "name": "style_attr_end_with_semicolon_space",
+ "input": "<div style=\"color: blue; \" />",
+ "output": "<div style='color: blue;'></div>"
+ },
+
+ {
+ "name": "attributes_with_embedded_quotes",
+ "input": "<img src=doesntexist.jpg\"'onerror=\"alert(1) />",
+ "output": "<img src='doesntexist.jpg\"&#39;onerror=\"alert(1)'/>"
+ },
+
+ {
+ "name": "attributes_with_embedded_quotes_II",
+ "input": "<img src=notthere.jpg\"\"onerror=\"alert(2) />",
+ "output": "<img src='notthere.jpg\"\"onerror=\"alert(2)'/>"
+ }
+]
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/sanitizer.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/sanitizer.py
new file mode 100644
index 0000000000..bb4834214f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/sanitizer.py
@@ -0,0 +1,51 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import codecs
+import json
+
+import pytest
+
+from html5lib import parseFragment, serialize
+
+
+class SanitizerFile(pytest.File):
+ def collect(self):
+ with codecs.open(str(self.fspath), "r", encoding="utf-8") as fp:
+ tests = json.load(fp)
+ for i, test in enumerate(tests):
+ yield SanitizerTest(str(i), self, test=test)
+
+
+class SanitizerTest(pytest.Item):
+ def __init__(self, name, parent, test):
+ super(SanitizerTest, self).__init__(name, parent)
+ self.obj = lambda: 1 # this is to hack around skipif needing a function!
+ self.test = test
+
+ def runtest(self):
+ input = self.test["input"]
+ expected = self.test["output"]
+
+ parsed = parseFragment(input)
+ with pytest.deprecated_call():
+ serialized = serialize(parsed,
+ sanitize=True,
+ omit_optional_tags=False,
+ use_trailing_solidus=True,
+ space_before_trailing_solidus=False,
+ quote_attr_values="always",
+ quote_char="'",
+ alphabetical_attributes=True)
+ errorMsg = "\n".join(["\n\nInput:", input,
+ "\nExpected:", expected,
+ "\nReceived:", serialized])
+ assert expected == serialized, errorMsg
+
+ def repr_failure(self, excinfo):
+ traceback = excinfo.traceback
+ ntraceback = traceback.cut(path=__file__)
+ excinfo.traceback = ntraceback.filter()
+
+ return excinfo.getrepr(funcargs=True,
+ showlocals=False,
+ style="short", tbfilter=False)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/core.test b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/core.test
new file mode 100644
index 0000000000..55294b6831
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/core.test
@@ -0,0 +1,395 @@
+{
+ "tests": [
+ {
+ "expected": [
+ "<span title='test \"with\" &amp;quot;'>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "test \"with\" &quot;"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value escaping"
+ },
+ {
+ "expected": [
+ "<span title=foo>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "foo"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value non-quoting"
+ },
+ {
+ "expected": [
+ "<span title=\"foo<bar\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "foo<bar"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value non-quoting (with <)"
+ },
+ {
+ "expected": [
+ "<span title=\"foo=bar\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "foo=bar"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value quoting (with =)"
+ },
+ {
+ "expected": [
+ "<span title=\"foo>bar\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "foo>bar"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value quoting (with >)"
+ },
+ {
+ "expected": [
+ "<span title='foo\"bar'>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "foo\"bar"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value quoting (with \")"
+ },
+ {
+ "expected": [
+ "<span title=\"foo'bar\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "foo'bar"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value quoting (with ')"
+ },
+ {
+ "expected": [
+ "<span title=\"foo'bar&quot;baz\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "foo'bar\"baz"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value quoting (with both \" and ')"
+ },
+ {
+ "expected": [
+ "<span title=\"foo bar\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "foo bar"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value quoting (with space)"
+ },
+ {
+ "expected": [
+ "<span title=\"foo\tbar\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "foo\tbar"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value quoting (with tab)"
+ },
+ {
+ "expected": [
+ "<span title=\"foo\nbar\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "foo\nbar"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value quoting (with LF)"
+ },
+ {
+ "expected": [
+ "<span title=\"foo\rbar\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "foo\rbar"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value quoting (with CR)"
+ },
+ {
+ "expected": [
+ "<span title=\"foo\u000bbar\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "foo\u000bbar"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value non-quoting (with linetab)"
+ },
+ {
+ "expected": [
+ "<span title=\"foo\fbar\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "foo\fbar"
+ }
+ ]
+ ]
+ ],
+ "description": "proper attribute value quoting (with form feed)"
+ },
+ {
+ "expected": [
+ "<img>"
+ ],
+ "input": [
+ [
+ "EmptyTag",
+ "img",
+ {}
+ ]
+ ],
+ "description": "void element (as EmptyTag token)"
+ },
+ {
+ "expected": [
+ "<!DOCTYPE foo>"
+ ],
+ "input": [
+ [
+ "Doctype",
+ "foo"
+ ]
+ ],
+ "description": "doctype in error"
+ },
+ {
+ "expected": [
+ "a&lt;b&gt;c&amp;d"
+ ],
+ "input": [
+ [
+ "Characters",
+ "a<b>c&d"
+ ]
+ ],
+ "description": "character data",
+ "options": {
+ "encoding": "utf-8"
+ }
+ },
+ {
+ "expected": [
+ "<script>a<b>c&d"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "script",
+ {}
+ ],
+ [
+ "Characters",
+ "a<b>c&d"
+ ]
+ ],
+ "description": "rcdata"
+ },
+ {
+ "expected": [
+ "<!DOCTYPE HTML>"
+ ],
+ "input": [
+ [
+ "Doctype",
+ "HTML"
+ ]
+ ],
+ "description": "doctype"
+ },
+ {
+ "expected": [
+ "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">"
+ ],
+ "input": [
+ [
+ "Doctype",
+ "HTML",
+ "-//W3C//DTD HTML 4.01//EN",
+ "http://www.w3.org/TR/html4/strict.dtd"
+ ]
+ ],
+ "description": "HTML 4.01 DOCTYPE"
+ },
+ {
+ "expected": [
+ "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">"
+ ],
+ "input": [
+ [
+ "Doctype",
+ "HTML",
+ "-//W3C//DTD HTML 4.01//EN"
+ ]
+ ],
+ "description": "HTML 4.01 DOCTYPE without system identifier"
+ },
+ {
+ "expected": [
+ "<!DOCTYPE html SYSTEM \"http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd\">"
+ ],
+ "input": [
+ [
+ "Doctype",
+ "html",
+ "",
+ "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd"
+ ]
+ ],
+ "description": "IBM DOCTYPE without public identifier"
+ }
+ ]
+}
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/injectmeta.test b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/injectmeta.test
new file mode 100644
index 0000000000..399590c3f3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/injectmeta.test
@@ -0,0 +1,350 @@
+{
+ "tests": [
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ]
+ ],
+ "description": "no encoding",
+ "options": {
+ "inject_meta_charset": true
+ }
+ },
+ {
+ "expected": [
+ "<meta charset=utf-8>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ]
+ ],
+ "description": "empytag head",
+ "options": {
+ "encoding": "utf-8",
+ "inject_meta_charset": true
+ }
+ },
+ {
+ "expected": [
+ "<meta charset=utf-8><title>foo</title>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "title",
+ {}
+ ],
+ [
+ "Characters",
+ "foo"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "title"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ]
+ ],
+ "description": "head w/title",
+ "options": {
+ "encoding": "utf-8",
+ "inject_meta_charset": true
+ }
+ },
+ {
+ "expected": [
+ "<meta charset=utf-8>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "EmptyTag",
+ "meta",
+ [
+ {
+ "namespace": null,
+ "name": "charset",
+ "value": "ascii"
+ }
+ ]
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ]
+ ],
+ "description": "head w/meta-charset",
+ "options": {
+ "encoding": "utf-8",
+ "inject_meta_charset": true
+ }
+ },
+ {
+ "expected": [
+ "<meta charset=utf-8><meta charset=utf-8>",
+ "<head><meta charset=utf-8><meta charset=ascii>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "EmptyTag",
+ "meta",
+ [
+ {
+ "namespace": null,
+ "name": "charset",
+ "value": "ascii"
+ }
+ ]
+ ],
+ [
+ "EmptyTag",
+ "meta",
+ [
+ {
+ "namespace": null,
+ "name": "charset",
+ "value": "ascii"
+ }
+ ]
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ]
+ ],
+ "description": "head w/ two meta-charset",
+ "options": {
+ "encoding": "utf-8",
+ "inject_meta_charset": true
+ }
+ },
+ {
+ "expected": [
+ "<meta charset=utf-8><meta content=noindex name=robots>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "EmptyTag",
+ "meta",
+ [
+ {
+ "namespace": null,
+ "name": "name",
+ "value": "robots"
+ },
+ {
+ "namespace": null,
+ "name": "content",
+ "value": "noindex"
+ }
+ ]
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ]
+ ],
+ "description": "head w/robots",
+ "options": {
+ "encoding": "utf-8",
+ "inject_meta_charset": true
+ }
+ },
+ {
+ "expected": [
+ "<meta content=noindex name=robots><meta charset=utf-8>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "EmptyTag",
+ "meta",
+ [
+ {
+ "namespace": null,
+ "name": "name",
+ "value": "robots"
+ },
+ {
+ "namespace": null,
+ "name": "content",
+ "value": "noindex"
+ }
+ ]
+ ],
+ [
+ "EmptyTag",
+ "meta",
+ [
+ {
+ "namespace": null,
+ "name": "charset",
+ "value": "ascii"
+ }
+ ]
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ]
+ ],
+ "description": "head w/robots & charset",
+ "options": {
+ "encoding": "utf-8",
+ "inject_meta_charset": true
+ }
+ },
+ {
+ "expected": [
+ "<meta content=\"text/html; charset=utf-8\" http-equiv=content-type>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "EmptyTag",
+ "meta",
+ [
+ {
+ "namespace": null,
+ "name": "http-equiv",
+ "value": "content-type"
+ },
+ {
+ "namespace": null,
+ "name": "content",
+ "value": "text/html; charset=ascii"
+ }
+ ]
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ]
+ ],
+ "description": "head w/ charset in http-equiv content-type",
+ "options": {
+ "encoding": "utf-8",
+ "inject_meta_charset": true
+ }
+ },
+ {
+ "expected": [
+ "<meta content=noindex name=robots><meta content=\"text/html; charset=utf-8\" http-equiv=content-type>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "EmptyTag",
+ "meta",
+ [
+ {
+ "namespace": null,
+ "name": "name",
+ "value": "robots"
+ },
+ {
+ "namespace": null,
+ "name": "content",
+ "value": "noindex"
+ }
+ ]
+ ],
+ [
+ "EmptyTag",
+ "meta",
+ [
+ {
+ "namespace": null,
+ "name": "http-equiv",
+ "value": "content-type"
+ },
+ {
+ "namespace": null,
+ "name": "content",
+ "value": "text/html; charset=ascii"
+ }
+ ]
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ]
+ ],
+ "description": "head w/robots & charset in http-equiv content-type",
+ "options": {
+ "encoding": "utf-8",
+ "inject_meta_charset": true
+ }
+ }
+ ]
+} \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/optionaltags.test b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/optionaltags.test
new file mode 100644
index 0000000000..e67725ca26
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/optionaltags.test
@@ -0,0 +1,3254 @@
+{
+ "tests": [
+ {
+ "expected": [
+ "<html lang=en>foo"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "html",
+ [
+ {
+ "namespace": null,
+ "name": "lang",
+ "value": "en"
+ }
+ ]
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "html start-tag followed by text, with attributes"
+ },
+ {
+ "expected": [
+ "<html><!--foo-->"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "html",
+ {}
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "html start-tag followed by comment"
+ },
+ {
+ "expected": [
+ "<html> foo"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "html",
+ {}
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "html start-tag followed by space character"
+ },
+ {
+ "expected": [
+ "foo"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "html",
+ {}
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "html start-tag followed by text"
+ },
+ {
+ "expected": [
+ "<foo>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "html",
+ {}
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "html start-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "html",
+ {}
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "html start-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "html",
+ {}
+ ]
+ ],
+ "description": "html start-tag at EOF (shouldn't ever happen?!)"
+ },
+ {
+ "expected": [
+ "</html><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "html"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "html end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</html> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "html"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "html end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "html"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "html end-tag followed by text"
+ },
+ {
+ "expected": [
+ "<foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "html"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "html end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "html"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "html end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "html"
+ ]
+ ],
+ "description": "html end-tag at EOF"
+ },
+ {
+ "expected": [
+ "<head><!--foo-->"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "head start-tag followed by comment"
+ },
+ {
+ "expected": [
+ "<head> foo"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "head start-tag followed by space character"
+ },
+ {
+ "expected": [
+ "<head>foo"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "head start-tag followed by text"
+ },
+ {
+ "expected": [
+ "<foo>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "head start-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<head></foo>",
+ "</foo>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "head start-tag followed by end-tag (shouldn't ever happen?!)"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ]
+ ],
+ "description": "empty head element"
+ },
+ {
+ "expected": [
+ "<meta>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ],
+ [
+ "EmptyTag",
+ "meta",
+ {}
+ ]
+ ],
+ "description": "head start-tag followed by empty-tag"
+ },
+ {
+ "expected": [
+ "<head>",
+ ""
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "head",
+ {}
+ ]
+ ],
+ "description": "head start-tag at EOF (shouldn't ever happen?!)"
+ },
+ {
+ "expected": [
+ "</head><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "head end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</head> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "head end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "head end-tag followed by text"
+ },
+ {
+ "expected": [
+ "<foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "head end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "head end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "head"
+ ]
+ ],
+ "description": "head end-tag at EOF"
+ },
+ {
+ "expected": [
+ "<body><!--foo-->"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "body",
+ {}
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "body start-tag followed by comment"
+ },
+ {
+ "expected": [
+ "<body> foo"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "body",
+ {}
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "body start-tag followed by space character"
+ },
+ {
+ "expected": [
+ "foo"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "body",
+ {}
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "body start-tag followed by text"
+ },
+ {
+ "expected": [
+ "<foo>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "body",
+ {}
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "body start-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "body",
+ {}
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "body start-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "body",
+ {}
+ ]
+ ],
+ "description": "body start-tag at EOF (shouldn't ever happen?!)"
+ },
+ {
+ "expected": [
+ "</body><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "body"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "body end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</body> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "body"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "body end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "body"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "body end-tag followed by text"
+ },
+ {
+ "expected": [
+ "<foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "body"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "body end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "body"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "body end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "body"
+ ]
+ ],
+ "description": "body end-tag at EOF"
+ },
+ {
+ "expected": [
+ "</li><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "li"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "li end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</li> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "li"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "li end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "</li>foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "li"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "li end-tag followed by text"
+ },
+ {
+ "expected": [
+ "</li><foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "li"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "li end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<li>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "li"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "li",
+ {}
+ ]
+ ],
+ "description": "li end-tag followed by li start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "li"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "li end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "li"
+ ]
+ ],
+ "description": "li end-tag at EOF"
+ },
+ {
+ "expected": [
+ "</dt><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dt"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "dt end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</dt> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dt"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "dt end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "</dt>foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dt"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "dt end-tag followed by text"
+ },
+ {
+ "expected": [
+ "</dt><foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dt"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "dt end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<dt>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dt"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "dt",
+ {}
+ ]
+ ],
+ "description": "dt end-tag followed by dt start-tag"
+ },
+ {
+ "expected": [
+ "<dd>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dt"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "dd",
+ {}
+ ]
+ ],
+ "description": "dt end-tag followed by dd start-tag"
+ },
+ {
+ "expected": [
+ "</dt></foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dt"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "dt end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ "</dt>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dt"
+ ]
+ ],
+ "description": "dt end-tag at EOF"
+ },
+ {
+ "expected": [
+ "</dd><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dd"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "dd end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</dd> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dd"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "dd end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "</dd>foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dd"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "dd end-tag followed by text"
+ },
+ {
+ "expected": [
+ "</dd><foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dd"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "dd end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<dd>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dd"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "dd",
+ {}
+ ]
+ ],
+ "description": "dd end-tag followed by dd start-tag"
+ },
+ {
+ "expected": [
+ "<dt>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dd"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "dt",
+ {}
+ ]
+ ],
+ "description": "dd end-tag followed by dt start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dd"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "dd end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "dd"
+ ]
+ ],
+ "description": "dd end-tag at EOF"
+ },
+ {
+ "expected": [
+ "</p><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "p end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</p> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "p end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "</p>foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "p end-tag followed by text"
+ },
+ {
+ "expected": [
+ "</p><foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<address>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "address",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by address start-tag"
+ },
+ {
+ "expected": [
+ "<article>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "article",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by article start-tag"
+ },
+ {
+ "expected": [
+ "<aside>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "aside",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by aside start-tag"
+ },
+ {
+ "expected": [
+ "<blockquote>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "blockquote",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by blockquote start-tag"
+ },
+ {
+ "expected": [
+ "<datagrid>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "datagrid",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by datagrid start-tag"
+ },
+ {
+ "expected": [
+ "<dialog>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "dialog",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by dialog start-tag"
+ },
+ {
+ "expected": [
+ "<dir>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "dir",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by dir start-tag"
+ },
+ {
+ "expected": [
+ "<div>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "div",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by div start-tag"
+ },
+ {
+ "expected": [
+ "<dl>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "dl",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by dl start-tag"
+ },
+ {
+ "expected": [
+ "<fieldset>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "fieldset",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by fieldset start-tag"
+ },
+ {
+ "expected": [
+ "<footer>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "footer",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by footer start-tag"
+ },
+ {
+ "expected": [
+ "<form>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "form",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by form start-tag"
+ },
+ {
+ "expected": [
+ "<h1>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "h1",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by h1 start-tag"
+ },
+ {
+ "expected": [
+ "<h2>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "h2",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by h2 start-tag"
+ },
+ {
+ "expected": [
+ "<h3>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "h3",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by h3 start-tag"
+ },
+ {
+ "expected": [
+ "<h4>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "h4",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by h4 start-tag"
+ },
+ {
+ "expected": [
+ "<h5>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "h5",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by h5 start-tag"
+ },
+ {
+ "expected": [
+ "<h6>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "h6",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by h6 start-tag"
+ },
+ {
+ "expected": [
+ "<header>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "header",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by header start-tag"
+ },
+ {
+ "expected": [
+ "<hr>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "EmptyTag",
+ "hr",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by hr empty-tag"
+ },
+ {
+ "expected": [
+ "<menu>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "menu",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by menu start-tag"
+ },
+ {
+ "expected": [
+ "<nav>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "nav",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by nav start-tag"
+ },
+ {
+ "expected": [
+ "<ol>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "ol",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by ol start-tag"
+ },
+ {
+ "expected": [
+ "<p>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "p",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by p start-tag"
+ },
+ {
+ "expected": [
+ "<pre>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "pre",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by pre start-tag"
+ },
+ {
+ "expected": [
+ "<section>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "section",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by section start-tag"
+ },
+ {
+ "expected": [
+ "<table>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "table",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by table start-tag"
+ },
+ {
+ "expected": [
+ "<ul>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "ul",
+ {}
+ ]
+ ],
+ "description": "p end-tag followed by ul start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "p end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "p"
+ ]
+ ],
+ "description": "p end-tag at EOF"
+ },
+ {
+ "expected": [
+ "</optgroup><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "optgroup"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "optgroup end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</optgroup> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "optgroup"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "optgroup end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "</optgroup>foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "optgroup"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "optgroup end-tag followed by text"
+ },
+ {
+ "expected": [
+ "</optgroup><foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "optgroup"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "optgroup end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<optgroup>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "optgroup"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "optgroup",
+ {}
+ ]
+ ],
+ "description": "optgroup end-tag followed by optgroup start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "optgroup"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "optgroup end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "optgroup"
+ ]
+ ],
+ "description": "optgroup end-tag at EOF"
+ },
+ {
+ "expected": [
+ "</option><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "option"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "option end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</option> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "option"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "option end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "</option>foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "option"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "option end-tag followed by text"
+ },
+ {
+ "expected": [
+ "<optgroup>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "option"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "optgroup",
+ {}
+ ]
+ ],
+ "description": "option end-tag followed by optgroup start-tag"
+ },
+ {
+ "expected": [
+ "</option><foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "option"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "option end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<option>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "option"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "option",
+ {}
+ ]
+ ],
+ "description": "option end-tag followed by option start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "option"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "option end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "option"
+ ]
+ ],
+ "description": "option end-tag at EOF"
+ },
+ {
+ "expected": [
+ "<colgroup><!--foo-->"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup",
+ {}
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "colgroup start-tag followed by comment"
+ },
+ {
+ "expected": [
+ "<colgroup> foo"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup",
+ {}
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "colgroup start-tag followed by space character"
+ },
+ {
+ "expected": [
+ "<colgroup>foo"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup",
+ {}
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "colgroup start-tag followed by text"
+ },
+ {
+ "expected": [
+ "<colgroup><foo>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup",
+ {}
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "colgroup start-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<table><col>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "table",
+ {}
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup",
+ {}
+ ],
+ [
+ "EmptyTag",
+ "col",
+ {}
+ ]
+ ],
+ "description": "first colgroup in a table with a col child"
+ },
+ {
+ "expected": [
+ "</colgroup><col>",
+ "<colgroup><col>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup",
+ {}
+ ],
+ [
+ "EmptyTag",
+ "http://www.w3.org/1999/xhtml",
+ "col",
+ {}
+ ]
+ ],
+ "description": "colgroup with a col child, following another colgroup"
+ },
+ {
+ "expected": [
+ "<colgroup></foo>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup",
+ {}
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "colgroup start-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ "<colgroup>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup",
+ {}
+ ]
+ ],
+ "description": "colgroup start-tag at EOF"
+ },
+ {
+ "expected": [
+ "</colgroup><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "colgroup end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</colgroup> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "colgroup end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "colgroup end-tag followed by text"
+ },
+ {
+ "expected": [
+ "<foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "colgroup end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "colgroup end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "colgroup"
+ ]
+ ],
+ "description": "colgroup end-tag at EOF"
+ },
+ {
+ "expected": [
+ "</thead><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "thead"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "thead end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</thead> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "thead"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "thead end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "</thead>foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "thead"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "thead end-tag followed by text"
+ },
+ {
+ "expected": [
+ "</thead><foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "thead"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "thead end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<tbody>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "thead"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody",
+ {}
+ ]
+ ],
+ "description": "thead end-tag followed by tbody start-tag"
+ },
+ {
+ "expected": [
+ "<tfoot>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "thead"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tfoot",
+ {}
+ ]
+ ],
+ "description": "thead end-tag followed by tfoot start-tag"
+ },
+ {
+ "expected": [
+ "</thead></foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "thead"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "thead end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ "</thead>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "thead"
+ ]
+ ],
+ "description": "thead end-tag at EOF"
+ },
+ {
+ "expected": [
+ "<tbody><!--foo-->"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody",
+ {}
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "tbody start-tag followed by comment"
+ },
+ {
+ "expected": [
+ "<tbody> foo"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody",
+ {}
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "tbody start-tag followed by space character"
+ },
+ {
+ "expected": [
+ "<tbody>foo"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody",
+ {}
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "tbody start-tag followed by text"
+ },
+ {
+ "expected": [
+ "<tbody><foo>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody",
+ {}
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "tbody start-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<table><tr>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "table",
+ {}
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody",
+ {}
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tr",
+ {}
+ ]
+ ],
+ "description": "first tbody in a table with a tr child"
+ },
+ {
+ "expected": [
+ "<tbody><tr>",
+ "</tbody><tr>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody",
+ {}
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tr",
+ {}
+ ]
+ ],
+ "description": "tbody with a tr child, following another tbody"
+ },
+ {
+ "expected": [
+ "<tbody><tr>",
+ "</thead><tr>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "thead"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody",
+ {}
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tr",
+ {}
+ ]
+ ],
+ "description": "tbody with a tr child, following a thead"
+ },
+ {
+ "expected": [
+ "<tbody><tr>",
+ "</tfoot><tr>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tfoot"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody",
+ {}
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tr",
+ {}
+ ]
+ ],
+ "description": "tbody with a tr child, following a tfoot"
+ },
+ {
+ "expected": [
+ "<tbody></foo>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody",
+ {}
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "tbody start-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ "<tbody>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody",
+ {}
+ ]
+ ],
+ "description": "tbody start-tag at EOF"
+ },
+ {
+ "expected": [
+ "</tbody><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "tbody end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</tbody> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "tbody end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "</tbody>foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "tbody end-tag followed by text"
+ },
+ {
+ "expected": [
+ "</tbody><foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "tbody end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<tbody>",
+ "</tbody>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody",
+ {}
+ ]
+ ],
+ "description": "tbody end-tag followed by tbody start-tag"
+ },
+ {
+ "expected": [
+ "<tfoot>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tfoot",
+ {}
+ ]
+ ],
+ "description": "tbody end-tag followed by tfoot start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "tbody end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody"
+ ]
+ ],
+ "description": "tbody end-tag at EOF"
+ },
+ {
+ "expected": [
+ "</tfoot><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tfoot"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "tfoot end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</tfoot> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tfoot"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "tfoot end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "</tfoot>foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tfoot"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "tfoot end-tag followed by text"
+ },
+ {
+ "expected": [
+ "</tfoot><foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tfoot"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "tfoot end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<tbody>",
+ "</tfoot>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tfoot"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tbody",
+ {}
+ ]
+ ],
+ "description": "tfoot end-tag followed by tbody start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tfoot"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "tfoot end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tfoot"
+ ]
+ ],
+ "description": "tfoot end-tag at EOF"
+ },
+ {
+ "expected": [
+ "</tr><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tr"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "tr end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</tr> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tr"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "tr end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "</tr>foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tr"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "tr end-tag followed by text"
+ },
+ {
+ "expected": [
+ "</tr><foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tr"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "tr end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<tr>",
+ "</tr>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tr"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "tr",
+ {}
+ ]
+ ],
+ "description": "tr end-tag followed by tr start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tr"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "tr end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "tr"
+ ]
+ ],
+ "description": "tr end-tag at EOF"
+ },
+ {
+ "expected": [
+ "</td><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "td"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "td end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</td> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "td"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "td end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "</td>foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "td"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "td end-tag followed by text"
+ },
+ {
+ "expected": [
+ "</td><foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "td"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "td end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<td>",
+ "</td>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "td"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "td",
+ {}
+ ]
+ ],
+ "description": "td end-tag followed by td start-tag"
+ },
+ {
+ "expected": [
+ "<th>",
+ "</td>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "td"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "th",
+ {}
+ ]
+ ],
+ "description": "td end-tag followed by th start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "td"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "td end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "td"
+ ]
+ ],
+ "description": "td end-tag at EOF"
+ },
+ {
+ "expected": [
+ "</th><!--foo-->"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "th"
+ ],
+ [
+ "Comment",
+ "foo"
+ ]
+ ],
+ "description": "th end-tag followed by comment"
+ },
+ {
+ "expected": [
+ "</th> foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "th"
+ ],
+ [
+ "Characters",
+ " foo"
+ ]
+ ],
+ "description": "th end-tag followed by space character"
+ },
+ {
+ "expected": [
+ "</th>foo"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "th"
+ ],
+ [
+ "Characters",
+ "foo"
+ ]
+ ],
+ "description": "th end-tag followed by text"
+ },
+ {
+ "expected": [
+ "</th><foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "th"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo",
+ {}
+ ]
+ ],
+ "description": "th end-tag followed by start-tag"
+ },
+ {
+ "expected": [
+ "<th>",
+ "</th>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "th"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "th",
+ {}
+ ]
+ ],
+ "description": "th end-tag followed by th start-tag"
+ },
+ {
+ "expected": [
+ "<td>",
+ "</th>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "th"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "td",
+ {}
+ ]
+ ],
+ "description": "th end-tag followed by td start-tag"
+ },
+ {
+ "expected": [
+ "</foo>"
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "th"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "foo"
+ ]
+ ],
+ "description": "th end-tag followed by end-tag"
+ },
+ {
+ "expected": [
+ ""
+ ],
+ "input": [
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "th"
+ ]
+ ],
+ "description": "th end-tag at EOF"
+ }
+ ]
+} \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/options.test b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/options.test
new file mode 100644
index 0000000000..a22eebfcf3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/options.test
@@ -0,0 +1,334 @@
+{
+ "tests": [
+ {
+ "expected": [
+ "<span title='test &#39;with&#39; quote_char'>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "test 'with' quote_char"
+ }
+ ]
+ ]
+ ],
+ "description": "quote_char=\"'\"",
+ "options": {
+ "quote_char": "'"
+ }
+ },
+ {
+ "expected": [
+ "<button disabled>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "button",
+ [
+ {
+ "namespace": null,
+ "name": "disabled",
+ "value": "disabled"
+ }
+ ]
+ ]
+ ],
+ "description": "quote_attr_values='always'",
+ "options": {
+ "quote_attr_values": "always"
+ }
+ },
+ {
+ "expected": [
+ "<div itemscope>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "div",
+ [
+ {
+ "namespace": null,
+ "name": "itemscope",
+ "value": "itemscope"
+ }
+ ]
+ ]
+ ],
+ "description": "quote_attr_values='always' with itemscope",
+ "options": {
+ "quote_attr_values": "always"
+ }
+ },
+ {
+ "expected": [
+ "<div irrelevant>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "div",
+ [
+ {
+ "namespace": null,
+ "name": "irrelevant",
+ "value": "irrelevant"
+ }
+ ]
+ ]
+ ],
+ "description": "quote_attr_values='always' with irrelevant",
+ "options": {
+ "quote_attr_values": "always"
+ }
+ },
+ {
+ "expected": [
+ "<div class=\"foo\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "div",
+ [
+ {
+ "namespace": null,
+ "name": "class",
+ "value": "foo"
+ }
+ ]
+ ]
+ ],
+ "description": "non-minimized quote_attr_values='always'",
+ "options": {
+ "quote_attr_values": "always"
+ }
+ },
+ {
+ "expected": [
+ "<div class=foo>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "div",
+ [
+ {
+ "namespace": null,
+ "name": "class",
+ "value": "foo"
+ }
+ ]
+ ]
+ ],
+ "description": "non-minimized quote_attr_values='legacy'",
+ "options": {
+ "quote_attr_values": "legacy"
+ }
+ },
+ {
+ "expected": [
+ "<div class=foo>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "div",
+ [
+ {
+ "namespace": null,
+ "name": "class",
+ "value": "foo"
+ }
+ ]
+ ]
+ ],
+ "description": "non-minimized quote_attr_values='spec'",
+ "options": {
+ "quote_attr_values": "spec"
+ }
+ },
+ {
+ "expected": [
+ "<img />"
+ ],
+ "input": [
+ [
+ "EmptyTag",
+ "img",
+ {}
+ ]
+ ],
+ "description": "use_trailing_solidus=true with void element",
+ "options": {
+ "use_trailing_solidus": true
+ }
+ },
+ {
+ "expected": [
+ "<div>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "div",
+ {}
+ ]
+ ],
+ "description": "use_trailing_solidus=true with non-void element",
+ "options": {
+ "use_trailing_solidus": true
+ }
+ },
+ {
+ "expected": [
+ "<div itemscope=itemscope>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "div",
+ [
+ {
+ "namespace": null,
+ "name": "itemscope",
+ "value": "itemscope"
+ }
+ ]
+ ]
+ ],
+ "description": "minimize_boolean_attributes=false",
+ "options": {
+ "minimize_boolean_attributes": false
+ }
+ },
+ {
+ "expected": [
+ "<div irrelevant=irrelevant>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "div",
+ [
+ {
+ "namespace": null,
+ "name": "irrelevant",
+ "value": "irrelevant"
+ }
+ ]
+ ]
+ ],
+ "description": "minimize_boolean_attributes=false",
+ "options": {
+ "minimize_boolean_attributes": false
+ }
+ },
+ {
+ "expected": [
+ "<div itemscope=\"\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "div",
+ [
+ {
+ "namespace": null,
+ "name": "itemscope",
+ "value": ""
+ }
+ ]
+ ]
+ ],
+ "description": "minimize_boolean_attributes=false with empty value",
+ "options": {
+ "minimize_boolean_attributes": false
+ }
+ },
+ {
+ "expected": [
+ "<div irrelevant=\"\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "div",
+ [
+ {
+ "namespace": null,
+ "name": "irrelevant",
+ "value": ""
+ }
+ ]
+ ]
+ ],
+ "description": "minimize_boolean_attributes=false with empty value",
+ "options": {
+ "minimize_boolean_attributes": false
+ }
+ },
+ {
+ "expected": [
+ "<a title=\"a&lt;b>c&amp;d\">"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "a",
+ [
+ {
+ "namespace": null,
+ "name": "title",
+ "value": "a<b>c&d"
+ }
+ ]
+ ]
+ ],
+ "description": "escape less than signs in attribute values",
+ "options": {
+ "escape_lt_in_attrs": true
+ }
+ },
+ {
+ "expected": [
+ "<script>a&lt;b&gt;c&amp;d"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "script",
+ {}
+ ],
+ [
+ "Characters",
+ "a<b>c&d"
+ ]
+ ],
+ "description": "rcdata",
+ "options": {
+ "escape_rcdata": true
+ }
+ }
+ ]
+} \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/whitespace.test b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/whitespace.test
new file mode 100644
index 0000000000..dac3a69e27
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/serializer-testdata/whitespace.test
@@ -0,0 +1,198 @@
+{
+ "tests": [
+ {
+ "expected": [
+ " foo"
+ ],
+ "input": [
+ [
+ "Characters",
+ "\t\r\n\f foo"
+ ]
+ ],
+ "description": "bare text with leading spaces",
+ "options": {
+ "strip_whitespace": true
+ }
+ },
+ {
+ "expected": [
+ "foo "
+ ],
+ "input": [
+ [
+ "Characters",
+ "foo \t\r\n\f"
+ ]
+ ],
+ "description": "bare text with trailing spaces",
+ "options": {
+ "strip_whitespace": true
+ }
+ },
+ {
+ "expected": [
+ "foo bar"
+ ],
+ "input": [
+ [
+ "Characters",
+ "foo \t\r\n\f bar"
+ ]
+ ],
+ "description": "bare text with inner spaces",
+ "options": {
+ "strip_whitespace": true
+ }
+ },
+ {
+ "expected": [
+ "<pre>\t\r\n\f foo \t\r\n\f bar \t\r\n\f</pre>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "pre",
+ {}
+ ],
+ [
+ "Characters",
+ "\t\r\n\f foo \t\r\n\f bar \t\r\n\f"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "pre"
+ ]
+ ],
+ "description": "text within <pre>",
+ "options": {
+ "strip_whitespace": true
+ }
+ },
+ {
+ "expected": [
+ "<pre>\t\r\n\f fo<span>o \t\r\n\f b</span>ar \t\r\n\f</pre>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "pre",
+ {}
+ ],
+ [
+ "Characters",
+ "\t\r\n\f fo"
+ ],
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "span",
+ {}
+ ],
+ [
+ "Characters",
+ "o \t\r\n\f b"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "span"
+ ],
+ [
+ "Characters",
+ "ar \t\r\n\f"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "pre"
+ ]
+ ],
+ "description": "text within <pre>, with inner markup",
+ "options": {
+ "strip_whitespace": true
+ }
+ },
+ {
+ "expected": [
+ "<textarea>\t\r\n\f foo \t\r\n\f bar \t\r\n\f</textarea>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "textarea",
+ {}
+ ],
+ [
+ "Characters",
+ "\t\r\n\f foo \t\r\n\f bar \t\r\n\f"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "textarea"
+ ]
+ ],
+ "description": "text within <textarea>",
+ "options": {
+ "strip_whitespace": true
+ }
+ },
+ {
+ "expected": [
+ "<script>\t\r\n\f foo \t\r\n\f bar \t\r\n\f</script>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "script",
+ {}
+ ],
+ [
+ "Characters",
+ "\t\r\n\f foo \t\r\n\f bar \t\r\n\f"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "script"
+ ]
+ ],
+ "description": "text within <script>",
+ "options": {
+ "strip_whitespace": true
+ }
+ },
+ {
+ "expected": [
+ "<style>\t\r\n\f foo \t\r\n\f bar \t\r\n\f</style>"
+ ],
+ "input": [
+ [
+ "StartTag",
+ "http://www.w3.org/1999/xhtml",
+ "style",
+ {}
+ ],
+ [
+ "Characters",
+ "\t\r\n\f foo \t\r\n\f bar \t\r\n\f"
+ ],
+ [
+ "EndTag",
+ "http://www.w3.org/1999/xhtml",
+ "style"
+ ]
+ ],
+ "description": "text within <style>",
+ "options": {
+ "strip_whitespace": true
+ }
+ }
+ ]
+} \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/support.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/support.py
new file mode 100644
index 0000000000..9cd5afbe69
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/support.py
@@ -0,0 +1,199 @@
+from __future__ import absolute_import, division, unicode_literals
+
+# pylint:disable=wrong-import-position
+
+import os
+import sys
+import codecs
+import glob
+import xml.sax.handler
+
+base_path = os.path.split(__file__)[0]
+
+test_dir = os.path.join(base_path, 'testdata')
+sys.path.insert(0, os.path.abspath(os.path.join(base_path,
+ os.path.pardir,
+ os.path.pardir)))
+
+from html5lib import treebuilders, treewalkers, treeadapters # noqa
+del base_path
+
+# Build a dict of available trees
+treeTypes = {}
+
+# DOM impls
+treeTypes["DOM"] = {
+ "builder": treebuilders.getTreeBuilder("dom"),
+ "walker": treewalkers.getTreeWalker("dom")
+}
+
+# ElementTree impls
+import xml.etree.ElementTree as ElementTree # noqa
+treeTypes['ElementTree'] = {
+ "builder": treebuilders.getTreeBuilder("etree", ElementTree, fullTree=True),
+ "walker": treewalkers.getTreeWalker("etree", ElementTree)
+}
+
+try:
+ import xml.etree.cElementTree as cElementTree # noqa
+except ImportError:
+ treeTypes['cElementTree'] = None
+else:
+ # On Python 3.3 and above cElementTree is an alias, don't run them twice.
+ if cElementTree.Element is ElementTree.Element:
+ treeTypes['cElementTree'] = None
+ else:
+ treeTypes['cElementTree'] = {
+ "builder": treebuilders.getTreeBuilder("etree", cElementTree, fullTree=True),
+ "walker": treewalkers.getTreeWalker("etree", cElementTree)
+ }
+
+try:
+ import lxml.etree as lxml # noqa
+except ImportError:
+ treeTypes['lxml'] = None
+else:
+ treeTypes['lxml'] = {
+ "builder": treebuilders.getTreeBuilder("lxml"),
+ "walker": treewalkers.getTreeWalker("lxml")
+ }
+
+# Genshi impls
+try:
+ import genshi # noqa
+except ImportError:
+ treeTypes["genshi"] = None
+else:
+ treeTypes["genshi"] = {
+ "builder": treebuilders.getTreeBuilder("dom"),
+ "adapter": lambda tree: treeadapters.genshi.to_genshi(treewalkers.getTreeWalker("dom")(tree)),
+ "walker": treewalkers.getTreeWalker("genshi")
+ }
+
+# pylint:enable=wrong-import-position
+
+
+def get_data_files(subdirectory, files='*.dat', search_dir=test_dir):
+ return sorted(glob.glob(os.path.join(search_dir, subdirectory, files)))
+
+
+class DefaultDict(dict):
+ def __init__(self, default, *args, **kwargs):
+ self.default = default
+ dict.__init__(self, *args, **kwargs)
+
+ def __getitem__(self, key):
+ return dict.get(self, key, self.default)
+
+
+class TestData(object):
+ def __init__(self, filename, newTestHeading="data", encoding="utf8"):
+ if encoding is None:
+ self.f = open(filename, mode="rb")
+ else:
+ self.f = codecs.open(filename, encoding=encoding)
+ self.encoding = encoding
+ self.newTestHeading = newTestHeading
+
+ def __iter__(self):
+ data = DefaultDict(None)
+ key = None
+ for line in self.f:
+ heading = self.isSectionHeading(line)
+ if heading:
+ if data and heading == self.newTestHeading:
+ # Remove trailing newline
+ data[key] = data[key][:-1]
+ yield self.normaliseOutput(data)
+ data = DefaultDict(None)
+ key = heading
+ data[key] = "" if self.encoding else b""
+ elif key is not None:
+ data[key] += line
+ if data:
+ yield self.normaliseOutput(data)
+
+ def isSectionHeading(self, line):
+ """If the current heading is a test section heading return the heading,
+ otherwise return False"""
+ # print(line)
+ if line.startswith("#" if self.encoding else b"#"):
+ return line[1:].strip()
+ else:
+ return False
+
+ def normaliseOutput(self, data):
+ # Remove trailing newlines
+ for key, value in data.items():
+ if value.endswith("\n" if self.encoding else b"\n"):
+ data[key] = value[:-1]
+ return data
+
+
+def convert(stripChars):
+ def convertData(data):
+ """convert the output of str(document) to the format used in the testcases"""
+ data = data.split("\n")
+ rv = []
+ for line in data:
+ if line.startswith("|"):
+ rv.append(line[stripChars:])
+ else:
+ rv.append(line)
+ return "\n".join(rv)
+ return convertData
+
+
+convertExpected = convert(2)
+
+
+def errorMessage(input, expected, actual):
+ msg = ("Input:\n%s\nExpected:\n%s\nReceived\n%s\n" %
+ (repr(input), repr(expected), repr(actual)))
+ if sys.version_info[0] == 2:
+ msg = msg.encode("ascii", "backslashreplace")
+ return msg
+
+
+class TracingSaxHandler(xml.sax.handler.ContentHandler):
+ def __init__(self):
+ xml.sax.handler.ContentHandler.__init__(self)
+ self.visited = []
+
+ def startDocument(self):
+ self.visited.append('startDocument')
+
+ def endDocument(self):
+ self.visited.append('endDocument')
+
+ def startPrefixMapping(self, prefix, uri):
+ # These are ignored as their order is not guaranteed
+ pass
+
+ def endPrefixMapping(self, prefix):
+ # These are ignored as their order is not guaranteed
+ pass
+
+ def startElement(self, name, attrs):
+ self.visited.append(('startElement', name, attrs))
+
+ def endElement(self, name):
+ self.visited.append(('endElement', name))
+
+ def startElementNS(self, name, qname, attrs):
+ self.visited.append(('startElementNS', name, qname, dict(attrs)))
+
+ def endElementNS(self, name, qname):
+ self.visited.append(('endElementNS', name, qname))
+
+ def characters(self, content):
+ self.visited.append(('characters', content))
+
+ def ignorableWhitespace(self, whitespace):
+ self.visited.append(('ignorableWhitespace', whitespace))
+
+ def processingInstruction(self, target, data):
+ self.visited.append(('processingInstruction', target, data))
+
+ def skippedEntity(self, name):
+ self.visited.append(('skippedEntity', name))
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_alphabeticalattributes.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_alphabeticalattributes.py
new file mode 100644
index 0000000000..7d5b8e0f65
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_alphabeticalattributes.py
@@ -0,0 +1,78 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from collections import OrderedDict
+
+import pytest
+
+import html5lib
+from html5lib.filters.alphabeticalattributes import Filter
+from html5lib.serializer import HTMLSerializer
+
+
+@pytest.mark.parametrize('msg, attrs, expected_attrs', [
+ (
+ 'no attrs',
+ {},
+ {}
+ ),
+ (
+ 'one attr',
+ {(None, 'alt'): 'image'},
+ OrderedDict([((None, 'alt'), 'image')])
+ ),
+ (
+ 'multiple attrs',
+ {
+ (None, 'src'): 'foo',
+ (None, 'alt'): 'image',
+ (None, 'style'): 'border: 1px solid black;'
+ },
+ OrderedDict([
+ ((None, 'alt'), 'image'),
+ ((None, 'src'), 'foo'),
+ ((None, 'style'), 'border: 1px solid black;')
+ ])
+ ),
+])
+def test_alphabetizing(msg, attrs, expected_attrs):
+ tokens = [{'type': 'StartTag', 'name': 'img', 'data': attrs}]
+ output_tokens = list(Filter(tokens))
+
+ attrs = output_tokens[0]['data']
+ assert attrs == expected_attrs
+
+
+def test_with_different_namespaces():
+ tokens = [{
+ 'type': 'StartTag',
+ 'name': 'pattern',
+ 'data': {
+ (None, 'id'): 'patt1',
+ ('http://www.w3.org/1999/xlink', 'href'): '#patt2'
+ }
+ }]
+ output_tokens = list(Filter(tokens))
+
+ attrs = output_tokens[0]['data']
+ assert attrs == OrderedDict([
+ ((None, 'id'), 'patt1'),
+ (('http://www.w3.org/1999/xlink', 'href'), '#patt2')
+ ])
+
+
+def test_with_serializer():
+ """Verify filter works in the context of everything else"""
+ parser = html5lib.HTMLParser()
+ dom = parser.parseFragment('<svg><pattern xlink:href="#patt2" id="patt1"></svg>')
+ walker = html5lib.getTreeWalker('etree')
+ ser = HTMLSerializer(
+ alphabetical_attributes=True,
+ quote_attr_values='always'
+ )
+
+ # FIXME(willkg): The "xlink" namespace gets dropped by the serializer. When
+ # that gets fixed, we can fix this expected result.
+ assert (
+ ser.render(walker(dom)) ==
+ '<svg><pattern id="patt1" href="#patt2"></pattern></svg>'
+ )
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_encoding.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_encoding.py
new file mode 100644
index 0000000000..47c4814a47
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_encoding.py
@@ -0,0 +1,117 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import os
+
+import pytest
+
+from .support import get_data_files, test_dir, errorMessage, TestData as _TestData
+from html5lib import HTMLParser, _inputstream
+
+
+def test_basic_prescan_length():
+ data = "<title>Caf\u00E9</title><!--a--><meta charset='utf-8'>".encode('utf-8')
+ pad = 1024 - len(data) + 1
+ data = data.replace(b"-a-", b"-" + (b"a" * pad) + b"-")
+ assert len(data) == 1024 # Sanity
+ stream = _inputstream.HTMLBinaryInputStream(data, useChardet=False)
+ assert 'utf-8' == stream.charEncoding[0].name
+
+
+def test_parser_reparse():
+ data = "<title>Caf\u00E9</title><!--a--><meta charset='utf-8'>".encode('utf-8')
+ pad = 10240 - len(data) + 1
+ data = data.replace(b"-a-", b"-" + (b"a" * pad) + b"-")
+ assert len(data) == 10240 # Sanity
+ stream = _inputstream.HTMLBinaryInputStream(data, useChardet=False)
+ assert 'windows-1252' == stream.charEncoding[0].name
+ p = HTMLParser(namespaceHTMLElements=False)
+ doc = p.parse(data, useChardet=False)
+ assert 'utf-8' == p.documentEncoding
+ assert doc.find(".//title").text == "Caf\u00E9"
+
+
+@pytest.mark.parametrize("expected,data,kwargs", [
+ ("utf-16le", b"\xFF\xFE", {"override_encoding": "iso-8859-2"}),
+ ("utf-16be", b"\xFE\xFF", {"override_encoding": "iso-8859-2"}),
+ ("utf-8", b"\xEF\xBB\xBF", {"override_encoding": "iso-8859-2"}),
+ ("iso-8859-2", b"", {"override_encoding": "iso-8859-2", "transport_encoding": "iso-8859-3"}),
+ ("iso-8859-2", b"<meta charset=iso-8859-3>", {"transport_encoding": "iso-8859-2"}),
+ ("iso-8859-2", b"<meta charset=iso-8859-2>", {"same_origin_parent_encoding": "iso-8859-3"}),
+ ("iso-8859-2", b"", {"same_origin_parent_encoding": "iso-8859-2", "likely_encoding": "iso-8859-3"}),
+ ("iso-8859-2", b"", {"same_origin_parent_encoding": "utf-16", "likely_encoding": "iso-8859-2"}),
+ ("iso-8859-2", b"", {"same_origin_parent_encoding": "utf-16be", "likely_encoding": "iso-8859-2"}),
+ ("iso-8859-2", b"", {"same_origin_parent_encoding": "utf-16le", "likely_encoding": "iso-8859-2"}),
+ ("iso-8859-2", b"", {"likely_encoding": "iso-8859-2", "default_encoding": "iso-8859-3"}),
+ ("iso-8859-2", b"", {"default_encoding": "iso-8859-2"}),
+ ("windows-1252", b"", {"default_encoding": "totally-bogus-string"}),
+ ("windows-1252", b"", {}),
+])
+def test_parser_args(expected, data, kwargs):
+ stream = _inputstream.HTMLBinaryInputStream(data, useChardet=False, **kwargs)
+ assert expected == stream.charEncoding[0].name
+ p = HTMLParser()
+ p.parse(data, useChardet=False, **kwargs)
+ assert expected == p.documentEncoding
+
+
+@pytest.mark.parametrize("kwargs", [
+ {"override_encoding": "iso-8859-2"},
+ {"override_encoding": None},
+ {"transport_encoding": "iso-8859-2"},
+ {"transport_encoding": None},
+ {"same_origin_parent_encoding": "iso-8859-2"},
+ {"same_origin_parent_encoding": None},
+ {"likely_encoding": "iso-8859-2"},
+ {"likely_encoding": None},
+ {"default_encoding": "iso-8859-2"},
+ {"default_encoding": None},
+ {"foo_encoding": "iso-8859-2"},
+ {"foo_encoding": None},
+])
+def test_parser_args_raises(kwargs):
+ with pytest.raises(TypeError) as exc_info:
+ p = HTMLParser()
+ p.parse("", useChardet=False, **kwargs)
+ assert exc_info.value.args[0].startswith("Cannot set an encoding with a unicode input")
+
+
+def param_encoding():
+ for filename in get_data_files("encoding"):
+ tests = _TestData(filename, b"data", encoding=None)
+ for test in tests:
+ yield test[b'data'], test[b'encoding']
+
+
+@pytest.mark.parametrize("data, encoding", param_encoding())
+def test_parser_encoding(data, encoding):
+ p = HTMLParser()
+ assert p.documentEncoding is None
+ p.parse(data, useChardet=False)
+ encoding = encoding.lower().decode("ascii")
+
+ assert encoding == p.documentEncoding, errorMessage(data, encoding, p.documentEncoding)
+
+
+@pytest.mark.parametrize("data, encoding", param_encoding())
+def test_prescan_encoding(data, encoding):
+ stream = _inputstream.HTMLBinaryInputStream(data, useChardet=False)
+ encoding = encoding.lower().decode("ascii")
+
+ # Very crude way to ignore irrelevant tests
+ if len(data) > stream.numBytesMeta:
+ return
+
+ assert encoding == stream.charEncoding[0].name, errorMessage(data, encoding, stream.charEncoding[0].name)
+
+
+# pylint:disable=wrong-import-position
+try:
+ import chardet # noqa
+except ImportError:
+ print("chardet not found, skipping chardet tests")
+else:
+ def test_chardet():
+ with open(os.path.join(test_dir, "encoding", "chardet", "test_big5.txt"), "rb") as fp:
+ encoding = _inputstream.HTMLInputStream(fp.read()).charEncoding
+ assert encoding[0].name == "big5"
+# pylint:enable=wrong-import-position
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_meta.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_meta.py
new file mode 100644
index 0000000000..dd02dd7fb7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_meta.py
@@ -0,0 +1,41 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import six
+from mock import Mock
+
+from . import support
+
+
+def _createReprMock(r):
+ """Creates a mock with a __repr__ returning r
+
+ Also provides __str__ mock with default mock behaviour"""
+ mock = Mock()
+ mock.__repr__ = Mock()
+ mock.__repr__.return_value = r
+ mock.__str__ = Mock(wraps=mock.__str__)
+ return mock
+
+
+def test_errorMessage():
+ # Create mock objects to take repr of
+ input = _createReprMock("1")
+ expected = _createReprMock("2")
+ actual = _createReprMock("3")
+
+ # Run the actual test
+ r = support.errorMessage(input, expected, actual)
+
+ # Assertions!
+ if six.PY2:
+ assert b"Input:\n1\nExpected:\n2\nReceived\n3\n" == r
+ else:
+ assert six.PY3
+ assert "Input:\n1\nExpected:\n2\nReceived\n3\n" == r
+
+ assert input.__repr__.call_count == 1
+ assert expected.__repr__.call_count == 1
+ assert actual.__repr__.call_count == 1
+ assert not input.__str__.called
+ assert not expected.__str__.called
+ assert not actual.__str__.called
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_optionaltags_filter.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_optionaltags_filter.py
new file mode 100644
index 0000000000..cd2821497f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_optionaltags_filter.py
@@ -0,0 +1,7 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from html5lib.filters.optionaltags import Filter
+
+
+def test_empty():
+ assert list(Filter([])) == []
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_parser2.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_parser2.py
new file mode 100644
index 0000000000..879d2447df
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_parser2.py
@@ -0,0 +1,94 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from six import PY2, text_type
+
+import io
+
+from . import support # noqa
+
+from html5lib.constants import namespaces
+from html5lib import parse, parseFragment, HTMLParser
+
+
+# tests that aren't autogenerated from text files
+def test_assertDoctypeCloneable():
+ doc = parse('<!DOCTYPE HTML>', treebuilder="dom")
+ assert doc.cloneNode(True) is not None
+
+
+def test_line_counter():
+ # http://groups.google.com/group/html5lib-discuss/browse_frm/thread/f4f00e4a2f26d5c0
+ assert parse("<pre>\nx\n&gt;\n</pre>") is not None
+
+
+def test_namespace_html_elements_0_dom():
+ doc = parse("<html></html>",
+ treebuilder="dom",
+ namespaceHTMLElements=True)
+ assert doc.childNodes[0].namespaceURI == namespaces["html"]
+
+
+def test_namespace_html_elements_1_dom():
+ doc = parse("<html></html>",
+ treebuilder="dom",
+ namespaceHTMLElements=False)
+ assert doc.childNodes[0].namespaceURI is None
+
+
+def test_namespace_html_elements_0_etree():
+ doc = parse("<html></html>",
+ treebuilder="etree",
+ namespaceHTMLElements=True)
+ assert doc.tag == "{%s}html" % (namespaces["html"],)
+
+
+def test_namespace_html_elements_1_etree():
+ doc = parse("<html></html>",
+ treebuilder="etree",
+ namespaceHTMLElements=False)
+ assert doc.tag == "html"
+
+
+def test_unicode_file():
+ assert parse(io.StringIO("a")) is not None
+
+
+def test_debug_log():
+ parser = HTMLParser(debug=True)
+ parser.parse("<!doctype html><title>a</title><p>b<script>c</script>d</p>e")
+
+ expected = [('dataState', 'InitialPhase', 'InitialPhase', 'processDoctype', {'type': 'Doctype'}),
+ ('dataState', 'BeforeHtmlPhase', 'BeforeHtmlPhase', 'processStartTag', {'name': 'title', 'type': 'StartTag'}),
+ ('dataState', 'BeforeHeadPhase', 'BeforeHeadPhase', 'processStartTag', {'name': 'title', 'type': 'StartTag'}),
+ ('dataState', 'InHeadPhase', 'InHeadPhase', 'processStartTag', {'name': 'title', 'type': 'StartTag'}),
+ ('rcdataState', 'TextPhase', 'TextPhase', 'processCharacters', {'type': 'Characters'}),
+ ('dataState', 'TextPhase', 'TextPhase', 'processEndTag', {'name': 'title', 'type': 'EndTag'}),
+ ('dataState', 'InHeadPhase', 'InHeadPhase', 'processStartTag', {'name': 'p', 'type': 'StartTag'}),
+ ('dataState', 'AfterHeadPhase', 'AfterHeadPhase', 'processStartTag', {'name': 'p', 'type': 'StartTag'}),
+ ('dataState', 'InBodyPhase', 'InBodyPhase', 'processStartTag', {'name': 'p', 'type': 'StartTag'}),
+ ('dataState', 'InBodyPhase', 'InBodyPhase', 'processCharacters', {'type': 'Characters'}),
+ ('dataState', 'InBodyPhase', 'InBodyPhase', 'processStartTag', {'name': 'script', 'type': 'StartTag'}),
+ ('dataState', 'InBodyPhase', 'InHeadPhase', 'processStartTag', {'name': 'script', 'type': 'StartTag'}),
+ ('scriptDataState', 'TextPhase', 'TextPhase', 'processCharacters', {'type': 'Characters'}),
+ ('dataState', 'TextPhase', 'TextPhase', 'processEndTag', {'name': 'script', 'type': 'EndTag'}),
+ ('dataState', 'InBodyPhase', 'InBodyPhase', 'processCharacters', {'type': 'Characters'}),
+ ('dataState', 'InBodyPhase', 'InBodyPhase', 'processEndTag', {'name': 'p', 'type': 'EndTag'}),
+ ('dataState', 'InBodyPhase', 'InBodyPhase', 'processCharacters', {'type': 'Characters'})]
+
+ if PY2:
+ for i, log in enumerate(expected):
+ log = [x.encode("ascii") if isinstance(x, text_type) else x for x in log]
+ expected[i] = tuple(log)
+
+ assert parser.log == expected
+
+
+def test_no_duplicate_clone():
+ frag = parseFragment("<b><em><foo><foob><fooc><aside></b></em>")
+ assert len(frag) == 2
+
+
+def test_self_closing_col():
+ parser = HTMLParser()
+ parser.parseFragment('<table><colgroup><col /></colgroup></table>')
+ assert not parser.errors
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_sanitizer.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_sanitizer.py
new file mode 100644
index 0000000000..f3faeb8050
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_sanitizer.py
@@ -0,0 +1,133 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import pytest
+
+from html5lib import constants, parseFragment, serialize
+from html5lib.filters import sanitizer
+
+
+def sanitize_html(stream):
+ parsed = parseFragment(stream)
+ with pytest.deprecated_call():
+ serialized = serialize(parsed,
+ sanitize=True,
+ omit_optional_tags=False,
+ use_trailing_solidus=True,
+ space_before_trailing_solidus=False,
+ quote_attr_values="always",
+ quote_char='"',
+ alphabetical_attributes=True)
+ return serialized
+
+
+def test_should_handle_astral_plane_characters():
+ sanitized = sanitize_html("<p>&#x1d4b5; &#x1d538;</p>")
+ expected = '<p>\U0001d4b5 \U0001d538</p>'
+ assert expected == sanitized
+
+
+def test_should_allow_relative_uris():
+ sanitized = sanitize_html('<p><a href="/example.com"></a></p>')
+ expected = '<p><a href="/example.com"></a></p>'
+ assert expected == sanitized
+
+
+def test_invalid_data_uri():
+ sanitized = sanitize_html('<audio controls="" src="data:foobar"></audio>')
+ expected = '<audio controls></audio>'
+ assert expected == sanitized
+
+
+def test_invalid_ipv6_url():
+ sanitized = sanitize_html('<a href="h://]">')
+ expected = "<a></a>"
+ assert expected == sanitized
+
+
+def test_data_uri_disallowed_type():
+ sanitized = sanitize_html('<audio controls="" src="data:text/html,<html>"></audio>')
+ expected = "<audio controls></audio>"
+ assert expected == sanitized
+
+
+def param_sanitizer():
+ for ns, tag_name in sanitizer.allowed_elements:
+ if ns != constants.namespaces["html"]:
+ continue
+ if tag_name in ['caption', 'col', 'colgroup', 'optgroup', 'option', 'table', 'tbody', 'td',
+ 'tfoot', 'th', 'thead', 'tr', 'select']:
+ continue # TODO
+ if tag_name == 'image':
+ yield ("test_should_allow_%s_tag" % tag_name,
+ "<img title=\"1\"/>foo &lt;bad&gt;bar&lt;/bad&gt; baz",
+ "<%s title='1'>foo <bad>bar</bad> baz</%s>" % (tag_name, tag_name))
+ elif tag_name == 'br':
+ yield ("test_should_allow_%s_tag" % tag_name,
+ "<br title=\"1\"/>foo &lt;bad&gt;bar&lt;/bad&gt; baz<br/>",
+ "<%s title='1'>foo <bad>bar</bad> baz</%s>" % (tag_name, tag_name))
+ elif tag_name in constants.voidElements:
+ yield ("test_should_allow_%s_tag" % tag_name,
+ "<%s title=\"1\"/>foo &lt;bad&gt;bar&lt;/bad&gt; baz" % tag_name,
+ "<%s title='1'>foo <bad>bar</bad> baz</%s>" % (tag_name, tag_name))
+ else:
+ yield ("test_should_allow_%s_tag" % tag_name,
+ "<%s title=\"1\">foo &lt;bad&gt;bar&lt;/bad&gt; baz</%s>" % (tag_name, tag_name),
+ "<%s title='1'>foo <bad>bar</bad> baz</%s>" % (tag_name, tag_name))
+
+ for ns, attribute_name in sanitizer.allowed_attributes:
+ if ns is not None:
+ continue
+ if attribute_name != attribute_name.lower():
+ continue # TODO
+ if attribute_name == 'style':
+ continue
+ attribute_value = 'foo'
+ if attribute_name in sanitizer.attr_val_is_uri:
+ attribute_value = '%s://sub.domain.tld/path/object.ext' % sanitizer.allowed_protocols[0]
+ yield ("test_should_allow_%s_attribute" % attribute_name,
+ "<p %s=\"%s\">foo &lt;bad&gt;bar&lt;/bad&gt; baz</p>" % (attribute_name, attribute_value),
+ "<p %s='%s'>foo <bad>bar</bad> baz</p>" % (attribute_name, attribute_value))
+
+ for protocol in sanitizer.allowed_protocols:
+ rest_of_uri = '//sub.domain.tld/path/object.ext'
+ if protocol == 'data':
+ rest_of_uri = 'image/png;base64,aGVsbG8gd29ybGQ='
+ yield ("test_should_allow_uppercase_%s_uris" % protocol,
+ "<img src=\"%s:%s\">foo</a>" % (protocol, rest_of_uri),
+ """<img src="%s:%s">foo</a>""" % (protocol, rest_of_uri))
+
+ for protocol in sanitizer.allowed_protocols:
+ rest_of_uri = '//sub.domain.tld/path/object.ext'
+ if protocol == 'data':
+ rest_of_uri = 'image/png;base64,aGVsbG8gd29ybGQ='
+ protocol = protocol.upper()
+ yield ("test_should_allow_uppercase_%s_uris" % protocol,
+ "<img src=\"%s:%s\">foo</a>" % (protocol, rest_of_uri),
+ """<img src="%s:%s">foo</a>""" % (protocol, rest_of_uri))
+
+
+@pytest.mark.parametrize("expected, input",
+ (pytest.param(expected, input, id=id)
+ for id, expected, input in param_sanitizer()))
+def test_sanitizer(expected, input):
+ parsed = parseFragment(expected)
+ expected = serialize(parsed,
+ omit_optional_tags=False,
+ use_trailing_solidus=True,
+ space_before_trailing_solidus=False,
+ quote_attr_values="always",
+ quote_char='"',
+ alphabetical_attributes=True)
+ assert expected == sanitize_html(input)
+
+
+def test_lowercase_color_codes_in_style():
+ sanitized = sanitize_html("<p style=\"border: 1px solid #a2a2a2;\"></p>")
+ expected = '<p style=\"border: 1px solid #a2a2a2;\"></p>'
+ assert expected == sanitized
+
+
+def test_uppercase_color_codes_in_style():
+ sanitized = sanitize_html("<p style=\"border: 1px solid #A2A2A2;\"></p>")
+ expected = '<p style=\"border: 1px solid #A2A2A2;\"></p>'
+ assert expected == sanitized
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_serializer.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_serializer.py
new file mode 100644
index 0000000000..bce6245905
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_serializer.py
@@ -0,0 +1,226 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import os
+import json
+
+import pytest
+
+from .support import get_data_files
+
+from html5lib import constants
+from html5lib.filters.lint import Filter as Lint
+from html5lib.serializer import HTMLSerializer, serialize
+from html5lib.treewalkers.base import TreeWalker
+
+# pylint:disable=wrong-import-position
+optionals_loaded = []
+
+try:
+ from lxml import etree
+ optionals_loaded.append("lxml")
+except ImportError:
+ pass
+# pylint:enable=wrong-import-position
+
+default_namespace = constants.namespaces["html"]
+
+
+class JsonWalker(TreeWalker):
+ def __iter__(self):
+ for token in self.tree:
+ type = token[0]
+ if type == "StartTag":
+ if len(token) == 4:
+ namespace, name, attrib = token[1:4]
+ else:
+ namespace = default_namespace
+ name, attrib = token[1:3]
+ yield self.startTag(namespace, name, self._convertAttrib(attrib))
+ elif type == "EndTag":
+ if len(token) == 3:
+ namespace, name = token[1:3]
+ else:
+ namespace = default_namespace
+ name = token[1]
+ yield self.endTag(namespace, name)
+ elif type == "EmptyTag":
+ if len(token) == 4:
+ namespace, name, attrib = token[1:]
+ else:
+ namespace = default_namespace
+ name, attrib = token[1:]
+ for token in self.emptyTag(namespace, name, self._convertAttrib(attrib)):
+ yield token
+ elif type == "Comment":
+ yield self.comment(token[1])
+ elif type in ("Characters", "SpaceCharacters"):
+ for token in self.text(token[1]):
+ yield token
+ elif type == "Doctype":
+ if len(token) == 4:
+ yield self.doctype(token[1], token[2], token[3])
+ elif len(token) == 3:
+ yield self.doctype(token[1], token[2])
+ else:
+ yield self.doctype(token[1])
+ else:
+ raise ValueError("Unknown token type: " + type)
+
+ def _convertAttrib(self, attribs):
+ """html5lib tree-walkers use a dict of (namespace, name): value for
+ attributes, but JSON cannot represent this. Convert from the format
+ in the serializer tests (a list of dicts with "namespace", "name",
+ and "value" as keys) to html5lib's tree-walker format."""
+ attrs = {}
+ for attrib in attribs:
+ name = (attrib["namespace"], attrib["name"])
+ assert(name not in attrs)
+ attrs[name] = attrib["value"]
+ return attrs
+
+
+def serialize_html(input, options):
+ options = {str(k): v for k, v in options.items()}
+ encoding = options.get("encoding", None)
+ if "encoding" in options:
+ del options["encoding"]
+ stream = Lint(JsonWalker(input), False)
+ serializer = HTMLSerializer(alphabetical_attributes=True, **options)
+ return serializer.render(stream, encoding)
+
+
+def throwsWithLatin1(input):
+ with pytest.raises(UnicodeEncodeError):
+ serialize_html(input, {"encoding": "iso-8859-1"})
+
+
+def testDoctypeName():
+ throwsWithLatin1([["Doctype", "\u0101"]])
+
+
+def testDoctypePublicId():
+ throwsWithLatin1([["Doctype", "potato", "\u0101"]])
+
+
+def testDoctypeSystemId():
+ throwsWithLatin1([["Doctype", "potato", "potato", "\u0101"]])
+
+
+def testCdataCharacters():
+ test_serializer([["StartTag", "http://www.w3.org/1999/xhtml", "style", {}], ["Characters", "\u0101"]],
+ ["<style>&amacr;"], {"encoding": "iso-8859-1"})
+
+
+def testCharacters():
+ test_serializer([["Characters", "\u0101"]],
+ ["&amacr;"], {"encoding": "iso-8859-1"})
+
+
+def testStartTagName():
+ throwsWithLatin1([["StartTag", "http://www.w3.org/1999/xhtml", "\u0101", []]])
+
+
+def testAttributeName():
+ throwsWithLatin1([["StartTag", "http://www.w3.org/1999/xhtml", "span", [{"namespace": None, "name": "\u0101", "value": "potato"}]]])
+
+
+def testAttributeValue():
+ test_serializer([["StartTag", "http://www.w3.org/1999/xhtml", "span",
+ [{"namespace": None, "name": "potato", "value": "\u0101"}]]],
+ ["<span potato=&amacr;>"], {"encoding": "iso-8859-1"})
+
+
+def testEndTagName():
+ throwsWithLatin1([["EndTag", "http://www.w3.org/1999/xhtml", "\u0101"]])
+
+
+def testComment():
+ throwsWithLatin1([["Comment", "\u0101"]])
+
+
+def testThrowsUnknownOption():
+ with pytest.raises(TypeError):
+ HTMLSerializer(foobar=None)
+
+
+@pytest.mark.parametrize("c", list("\t\n\u000C\x20\r\"'=<>`"))
+def testSpecQuoteAttribute(c):
+ input_ = [["StartTag", "http://www.w3.org/1999/xhtml", "span",
+ [{"namespace": None, "name": "foo", "value": c}]]]
+ if c == '"':
+ output_ = ["<span foo='%s'>" % c]
+ else:
+ output_ = ['<span foo="%s">' % c]
+ options_ = {"quote_attr_values": "spec"}
+ test_serializer(input_, output_, options_)
+
+
+@pytest.mark.parametrize("c", list("\t\n\u000C\x20\r\"'=<>`"
+ "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n"
+ "\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15"
+ "\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
+ "\x20\x2f\x60\xa0\u1680\u180e\u180f\u2000"
+ "\u2001\u2002\u2003\u2004\u2005\u2006\u2007"
+ "\u2008\u2009\u200a\u2028\u2029\u202f\u205f"
+ "\u3000"))
+def testLegacyQuoteAttribute(c):
+ input_ = [["StartTag", "http://www.w3.org/1999/xhtml", "span",
+ [{"namespace": None, "name": "foo", "value": c}]]]
+ if c == '"':
+ output_ = ["<span foo='%s'>" % c]
+ else:
+ output_ = ['<span foo="%s">' % c]
+ options_ = {"quote_attr_values": "legacy"}
+ test_serializer(input_, output_, options_)
+
+
+@pytest.fixture
+def lxml_parser():
+ return etree.XMLParser(resolve_entities=False)
+
+
+@pytest.mark.skipif("lxml" not in optionals_loaded, reason="lxml not importable")
+def testEntityReplacement(lxml_parser):
+ doc = '<!DOCTYPE html SYSTEM "about:legacy-compat"><html>&beta;</html>'
+ tree = etree.fromstring(doc, parser=lxml_parser).getroottree()
+ result = serialize(tree, tree="lxml", omit_optional_tags=False)
+ assert result == '<!DOCTYPE html SYSTEM "about:legacy-compat"><html>\u03B2</html>'
+
+
+@pytest.mark.skipif("lxml" not in optionals_loaded, reason="lxml not importable")
+def testEntityXML(lxml_parser):
+ doc = '<!DOCTYPE html SYSTEM "about:legacy-compat"><html>&gt;</html>'
+ tree = etree.fromstring(doc, parser=lxml_parser).getroottree()
+ result = serialize(tree, tree="lxml", omit_optional_tags=False)
+ assert result == '<!DOCTYPE html SYSTEM "about:legacy-compat"><html>&gt;</html>'
+
+
+@pytest.mark.skipif("lxml" not in optionals_loaded, reason="lxml not importable")
+def testEntityNoResolve(lxml_parser):
+ doc = '<!DOCTYPE html SYSTEM "about:legacy-compat"><html>&beta;</html>'
+ tree = etree.fromstring(doc, parser=lxml_parser).getroottree()
+ result = serialize(tree, tree="lxml", omit_optional_tags=False,
+ resolve_entities=False)
+ assert result == '<!DOCTYPE html SYSTEM "about:legacy-compat"><html>&beta;</html>'
+
+
+def param_serializer():
+ for filename in get_data_files('serializer-testdata', '*.test', os.path.dirname(__file__)):
+ with open(filename) as fp:
+ tests = json.load(fp)
+ for test in tests['tests']:
+ yield test["input"], test["expected"], test.get("options", {})
+
+
+@pytest.mark.parametrize("input, expected, options", param_serializer())
+def test_serializer(input, expected, options):
+ encoding = options.get("encoding", None)
+
+ if encoding:
+ expected = list(map(lambda x: x.encode(encoding), expected))
+
+ result = serialize_html(input, options)
+ if len(expected) == 1:
+ assert expected[0] == result, "Expected:\n%s\nActual:\n%s\nOptions:\n%s" % (expected[0], result, str(options))
+ elif result not in expected:
+ assert False, "Expected: %s, Received: %s" % (expected, result)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_stream.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_stream.py
new file mode 100644
index 0000000000..efe9b472f5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_stream.py
@@ -0,0 +1,325 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from . import support # noqa
+
+import codecs
+import sys
+from io import BytesIO, StringIO
+
+import pytest
+
+import six
+from six.moves import http_client, urllib
+
+from html5lib._inputstream import (BufferedStream, HTMLInputStream,
+ HTMLUnicodeInputStream, HTMLBinaryInputStream)
+from html5lib._utils import supports_lone_surrogates
+
+
+def test_basic():
+ s = b"abc"
+ fp = BufferedStream(BytesIO(s))
+ read = fp.read(10)
+ assert read == s
+
+
+def test_read_length():
+ fp = BufferedStream(BytesIO(b"abcdef"))
+ read1 = fp.read(1)
+ assert read1 == b"a"
+ read2 = fp.read(2)
+ assert read2 == b"bc"
+ read3 = fp.read(3)
+ assert read3 == b"def"
+ read4 = fp.read(4)
+ assert read4 == b""
+
+
+def test_tell():
+ fp = BufferedStream(BytesIO(b"abcdef"))
+ read1 = fp.read(1)
+ assert read1 == b"a"
+ assert fp.tell() == 1
+ read2 = fp.read(2)
+ assert read2 == b"bc"
+ assert fp.tell() == 3
+ read3 = fp.read(3)
+ assert read3 == b"def"
+ assert fp.tell() == 6
+ read4 = fp.read(4)
+ assert read4 == b""
+ assert fp.tell() == 6
+
+
+def test_seek():
+ fp = BufferedStream(BytesIO(b"abcdef"))
+ read1 = fp.read(1)
+ assert read1 == b"a"
+ fp.seek(0)
+ read2 = fp.read(1)
+ assert read2 == b"a"
+ read3 = fp.read(2)
+ assert read3 == b"bc"
+ fp.seek(2)
+ read4 = fp.read(2)
+ assert read4 == b"cd"
+ fp.seek(4)
+ read5 = fp.read(2)
+ assert read5 == b"ef"
+
+
+def test_seek_tell():
+ fp = BufferedStream(BytesIO(b"abcdef"))
+ read1 = fp.read(1)
+ assert read1 == b"a"
+ assert fp.tell() == 1
+ fp.seek(0)
+ read2 = fp.read(1)
+ assert read2 == b"a"
+ assert fp.tell() == 1
+ read3 = fp.read(2)
+ assert read3 == b"bc"
+ assert fp.tell() == 3
+ fp.seek(2)
+ read4 = fp.read(2)
+ assert read4 == b"cd"
+ assert fp.tell() == 4
+ fp.seek(4)
+ read5 = fp.read(2)
+ assert read5 == b"ef"
+ assert fp.tell() == 6
+
+
+class HTMLUnicodeInputStreamShortChunk(HTMLUnicodeInputStream):
+ _defaultChunkSize = 2
+
+
+class HTMLBinaryInputStreamShortChunk(HTMLBinaryInputStream):
+ _defaultChunkSize = 2
+
+
+def test_char_ascii():
+ stream = HTMLInputStream(b"'", override_encoding='ascii')
+ assert stream.charEncoding[0].name == 'windows-1252'
+ assert stream.char() == "'"
+
+
+def test_char_utf8():
+ stream = HTMLInputStream('\u2018'.encode('utf-8'), override_encoding='utf-8')
+ assert stream.charEncoding[0].name == 'utf-8'
+ assert stream.char() == '\u2018'
+
+
+def test_char_win1252():
+ stream = HTMLInputStream("\xa9\xf1\u2019".encode('windows-1252'))
+ assert stream.charEncoding[0].name == 'windows-1252'
+ assert stream.char() == "\xa9"
+ assert stream.char() == "\xf1"
+ assert stream.char() == "\u2019"
+
+
+def test_bom():
+ stream = HTMLInputStream(codecs.BOM_UTF8 + b"'")
+ assert stream.charEncoding[0].name == 'utf-8'
+ assert stream.char() == "'"
+
+
+def test_utf_16():
+ stream = HTMLInputStream((' ' * 1025).encode('utf-16'))
+ assert stream.charEncoding[0].name in ['utf-16le', 'utf-16be']
+ assert len(stream.charsUntil(' ', True)) == 1025
+
+
+def test_newlines():
+ stream = HTMLBinaryInputStreamShortChunk(codecs.BOM_UTF8 + b"a\nbb\r\nccc\rddddxe")
+ assert stream.position() == (1, 0)
+ assert stream.charsUntil('c') == "a\nbb\n"
+ assert stream.position() == (3, 0)
+ assert stream.charsUntil('x') == "ccc\ndddd"
+ assert stream.position() == (4, 4)
+ assert stream.charsUntil('e') == "x"
+ assert stream.position() == (4, 5)
+
+
+def test_newlines2():
+ size = HTMLUnicodeInputStream._defaultChunkSize
+ stream = HTMLInputStream("\r" * size + "\n")
+ assert stream.charsUntil('x') == "\n" * size
+
+
+def test_position():
+ stream = HTMLBinaryInputStreamShortChunk(codecs.BOM_UTF8 + b"a\nbb\nccc\nddde\nf\ngh")
+ assert stream.position() == (1, 0)
+ assert stream.charsUntil('c') == "a\nbb\n"
+ assert stream.position() == (3, 0)
+ stream.unget("\n")
+ assert stream.position() == (2, 2)
+ assert stream.charsUntil('c') == "\n"
+ assert stream.position() == (3, 0)
+ stream.unget("\n")
+ assert stream.position() == (2, 2)
+ assert stream.char() == "\n"
+ assert stream.position() == (3, 0)
+ assert stream.charsUntil('e') == "ccc\nddd"
+ assert stream.position() == (4, 3)
+ assert stream.charsUntil('h') == "e\nf\ng"
+ assert stream.position() == (6, 1)
+
+
+def test_position2():
+ stream = HTMLUnicodeInputStreamShortChunk("abc\nd")
+ assert stream.position() == (1, 0)
+ assert stream.char() == "a"
+ assert stream.position() == (1, 1)
+ assert stream.char() == "b"
+ assert stream.position() == (1, 2)
+ assert stream.char() == "c"
+ assert stream.position() == (1, 3)
+ assert stream.char() == "\n"
+ assert stream.position() == (2, 0)
+ assert stream.char() == "d"
+ assert stream.position() == (2, 1)
+
+
+def test_python_issue_20007():
+ """
+ Make sure we have a work-around for Python bug #20007
+ http://bugs.python.org/issue20007
+ """
+ class FakeSocket(object):
+ def makefile(self, _mode, _bufsize=None):
+ # pylint:disable=unused-argument
+ return BytesIO(b"HTTP/1.1 200 Ok\r\n\r\nText")
+
+ source = http_client.HTTPResponse(FakeSocket())
+ source.begin()
+ stream = HTMLInputStream(source)
+ assert stream.charsUntil(" ") == "Text"
+
+
+def test_python_issue_20007_b():
+ """
+ Make sure we have a work-around for Python bug #20007
+ http://bugs.python.org/issue20007
+ """
+ if six.PY2:
+ return
+
+ class FakeSocket(object):
+ def makefile(self, _mode, _bufsize=None):
+ # pylint:disable=unused-argument
+ return BytesIO(b"HTTP/1.1 200 Ok\r\n\r\nText")
+
+ source = http_client.HTTPResponse(FakeSocket())
+ source.begin()
+ wrapped = urllib.response.addinfourl(source, source.msg, "http://example.com")
+ stream = HTMLInputStream(wrapped)
+ assert stream.charsUntil(" ") == "Text"
+
+
+@pytest.mark.parametrize("inp,num",
+ [("\u0000", 0),
+ ("\u0001", 1),
+ ("\u0008", 1),
+ ("\u0009", 0),
+ ("\u000A", 0),
+ ("\u000B", 1),
+ ("\u000C", 0),
+ ("\u000D", 0),
+ ("\u000E", 1),
+ ("\u001F", 1),
+ ("\u0020", 0),
+ ("\u007E", 0),
+ ("\u007F", 1),
+ ("\u009F", 1),
+ ("\u00A0", 0),
+ ("\uFDCF", 0),
+ ("\uFDD0", 1),
+ ("\uFDEF", 1),
+ ("\uFDF0", 0),
+ ("\uFFFD", 0),
+ ("\uFFFE", 1),
+ ("\uFFFF", 1),
+ ("\U0001FFFD", 0),
+ ("\U0001FFFE", 1),
+ ("\U0001FFFF", 1),
+ ("\U0002FFFD", 0),
+ ("\U0002FFFE", 1),
+ ("\U0002FFFF", 1),
+ ("\U0003FFFD", 0),
+ ("\U0003FFFE", 1),
+ ("\U0003FFFF", 1),
+ ("\U0004FFFD", 0),
+ ("\U0004FFFE", 1),
+ ("\U0004FFFF", 1),
+ ("\U0005FFFD", 0),
+ ("\U0005FFFE", 1),
+ ("\U0005FFFF", 1),
+ ("\U0006FFFD", 0),
+ ("\U0006FFFE", 1),
+ ("\U0006FFFF", 1),
+ ("\U0007FFFD", 0),
+ ("\U0007FFFE", 1),
+ ("\U0007FFFF", 1),
+ ("\U0008FFFD", 0),
+ ("\U0008FFFE", 1),
+ ("\U0008FFFF", 1),
+ ("\U0009FFFD", 0),
+ ("\U0009FFFE", 1),
+ ("\U0009FFFF", 1),
+ ("\U000AFFFD", 0),
+ ("\U000AFFFE", 1),
+ ("\U000AFFFF", 1),
+ ("\U000BFFFD", 0),
+ ("\U000BFFFE", 1),
+ ("\U000BFFFF", 1),
+ ("\U000CFFFD", 0),
+ ("\U000CFFFE", 1),
+ ("\U000CFFFF", 1),
+ ("\U000DFFFD", 0),
+ ("\U000DFFFE", 1),
+ ("\U000DFFFF", 1),
+ ("\U000EFFFD", 0),
+ ("\U000EFFFE", 1),
+ ("\U000EFFFF", 1),
+ ("\U000FFFFD", 0),
+ ("\U000FFFFE", 1),
+ ("\U000FFFFF", 1),
+ ("\U0010FFFD", 0),
+ ("\U0010FFFE", 1),
+ ("\U0010FFFF", 1),
+ ("\x01\x01\x01", 3),
+ ("a\x01a\x01a\x01a", 3)])
+def test_invalid_codepoints(inp, num):
+ stream = HTMLUnicodeInputStream(StringIO(inp))
+ for _i in range(len(inp)):
+ stream.char()
+ assert len(stream.errors) == num
+
+
+@pytest.mark.skipif(not supports_lone_surrogates, reason="doesn't support lone surrogates")
+@pytest.mark.parametrize("inp,num",
+ [("'\\uD7FF'", 0),
+ ("'\\uD800'", 1),
+ ("'\\uDBFF'", 1),
+ ("'\\uDC00'", 1),
+ ("'\\uDFFF'", 1),
+ ("'\\uE000'", 0),
+ ("'\\uD800\\uD800\\uD800'", 3),
+ ("'a\\uD800a\\uD800a\\uD800a'", 3),
+ ("'\\uDFFF\\uDBFF'", 2),
+ pytest.param(
+ "'\\uDBFF\\uDFFF'", 2,
+ marks=pytest.mark.skipif(
+ sys.maxunicode == 0xFFFF,
+ reason="narrow Python"))])
+def test_invalid_codepoints_surrogates(inp, num):
+ inp = eval(inp) # pylint:disable=eval-used
+ fp = StringIO(inp)
+ if ord(max(fp.read())) > 0xFFFF:
+ pytest.skip("StringIO altered string")
+ fp.seek(0)
+ stream = HTMLUnicodeInputStream(fp)
+ for _i in range(len(inp)):
+ stream.char()
+ assert len(stream.errors) == num
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_tokenizer2.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_tokenizer2.py
new file mode 100644
index 0000000000..158d847a26
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_tokenizer2.py
@@ -0,0 +1,66 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import io
+
+from six import unichr, text_type
+
+from html5lib._tokenizer import HTMLTokenizer
+from html5lib.constants import tokenTypes
+
+
+def ignore_parse_errors(toks):
+ for tok in toks:
+ if tok['type'] != tokenTypes['ParseError']:
+ yield tok
+
+
+def test_maintain_attribute_order():
+ # generate loads to maximize the chance a hash-based mutation will occur
+ attrs = [(unichr(x), text_type(i)) for i, x in enumerate(range(ord('a'), ord('z')))]
+ stream = io.StringIO("<span " + " ".join("%s='%s'" % (x, i) for x, i in attrs) + ">")
+
+ toks = HTMLTokenizer(stream)
+ out = list(ignore_parse_errors(toks))
+
+ assert len(out) == 1
+ assert out[0]['type'] == tokenTypes['StartTag']
+
+ attrs_tok = out[0]['data']
+ assert len(attrs_tok) == len(attrs)
+
+ for (in_name, in_value), (out_name, out_value) in zip(attrs, attrs_tok.items()):
+ assert in_name == out_name
+ assert in_value == out_value
+
+
+def test_duplicate_attribute():
+ stream = io.StringIO("<span a=1 a=2 a=3>")
+
+ toks = HTMLTokenizer(stream)
+ out = list(ignore_parse_errors(toks))
+
+ assert len(out) == 1
+ assert out[0]['type'] == tokenTypes['StartTag']
+
+ attrs_tok = out[0]['data']
+ assert len(attrs_tok) == 1
+ assert list(attrs_tok.items()) == [('a', '1')]
+
+
+def test_maintain_duplicate_attribute_order():
+ # generate loads to maximize the chance a hash-based mutation will occur
+ attrs = [(unichr(x), text_type(i)) for i, x in enumerate(range(ord('a'), ord('z')))]
+ stream = io.StringIO("<span " + " ".join("%s='%s'" % (x, i) for x, i in attrs) + " a=100>")
+
+ toks = HTMLTokenizer(stream)
+ out = list(ignore_parse_errors(toks))
+
+ assert len(out) == 1
+ assert out[0]['type'] == tokenTypes['StartTag']
+
+ attrs_tok = out[0]['data']
+ assert len(attrs_tok) == len(attrs)
+
+ for (in_name, in_value), (out_name, out_value) in zip(attrs, attrs_tok.items()):
+ assert in_name == out_name
+ assert in_value == out_value
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_treeadapters.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_treeadapters.py
new file mode 100644
index 0000000000..95e56c00c9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_treeadapters.py
@@ -0,0 +1,40 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from . import support # noqa
+
+import html5lib
+from html5lib.treeadapters import sax
+from html5lib.treewalkers import getTreeWalker
+
+
+def test_to_sax():
+ handler = support.TracingSaxHandler()
+ tree = html5lib.parse("""<html xml:lang="en">
+ <title>Directory Listing</title>
+ <a href="/"><b/></p>
+ """, treebuilder="etree")
+ walker = getTreeWalker("etree")
+ sax.to_sax(walker(tree), handler)
+ expected = [
+ 'startDocument',
+ ('startElementNS', ('http://www.w3.org/1999/xhtml', 'html'),
+ 'html', {(None, 'xml:lang'): 'en'}),
+ ('startElementNS', ('http://www.w3.org/1999/xhtml', 'head'), 'head', {}),
+ ('startElementNS', ('http://www.w3.org/1999/xhtml', 'title'), 'title', {}),
+ ('characters', 'Directory Listing'),
+ ('endElementNS', ('http://www.w3.org/1999/xhtml', 'title'), 'title'),
+ ('characters', '\n '),
+ ('endElementNS', ('http://www.w3.org/1999/xhtml', 'head'), 'head'),
+ ('startElementNS', ('http://www.w3.org/1999/xhtml', 'body'), 'body', {}),
+ ('startElementNS', ('http://www.w3.org/1999/xhtml', 'a'), 'a', {(None, 'href'): '/'}),
+ ('startElementNS', ('http://www.w3.org/1999/xhtml', 'b'), 'b', {}),
+ ('startElementNS', ('http://www.w3.org/1999/xhtml', 'p'), 'p', {}),
+ ('endElementNS', ('http://www.w3.org/1999/xhtml', 'p'), 'p'),
+ ('characters', '\n '),
+ ('endElementNS', ('http://www.w3.org/1999/xhtml', 'b'), 'b'),
+ ('endElementNS', ('http://www.w3.org/1999/xhtml', 'a'), 'a'),
+ ('endElementNS', ('http://www.w3.org/1999/xhtml', 'body'), 'body'),
+ ('endElementNS', ('http://www.w3.org/1999/xhtml', 'html'), 'html'),
+ 'endDocument',
+ ]
+ assert expected == handler.visited
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_treewalkers.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_treewalkers.py
new file mode 100644
index 0000000000..780ca964ba
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_treewalkers.py
@@ -0,0 +1,205 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import itertools
+import sys
+
+from six import unichr, text_type
+import pytest
+
+try:
+ import lxml.etree
+except ImportError:
+ pass
+
+from .support import treeTypes
+
+from html5lib import html5parser, treewalkers
+from html5lib.filters.lint import Filter as Lint
+
+import re
+attrlist = re.compile(r"^(\s+)\w+=.*(\n\1\w+=.*)+", re.M)
+
+
+def sortattrs(x):
+ lines = x.group(0).split("\n")
+ lines.sort()
+ return "\n".join(lines)
+
+
+def test_all_tokens():
+ expected = [
+ {'data': {}, 'type': 'StartTag', 'namespace': 'http://www.w3.org/1999/xhtml', 'name': 'html'},
+ {'data': {}, 'type': 'StartTag', 'namespace': 'http://www.w3.org/1999/xhtml', 'name': 'head'},
+ {'type': 'EndTag', 'namespace': 'http://www.w3.org/1999/xhtml', 'name': 'head'},
+ {'data': {}, 'type': 'StartTag', 'namespace': 'http://www.w3.org/1999/xhtml', 'name': 'body'},
+ {'data': 'a', 'type': 'Characters'},
+ {'data': {}, 'type': 'StartTag', 'namespace': 'http://www.w3.org/1999/xhtml', 'name': 'div'},
+ {'data': 'b', 'type': 'Characters'},
+ {'type': 'EndTag', 'namespace': 'http://www.w3.org/1999/xhtml', 'name': 'div'},
+ {'data': 'c', 'type': 'Characters'},
+ {'type': 'EndTag', 'namespace': 'http://www.w3.org/1999/xhtml', 'name': 'body'},
+ {'type': 'EndTag', 'namespace': 'http://www.w3.org/1999/xhtml', 'name': 'html'}
+ ]
+ for _, treeCls in sorted(treeTypes.items()):
+ if treeCls is None:
+ continue
+ p = html5parser.HTMLParser(tree=treeCls["builder"])
+ document = p.parse("<html><head></head><body>a<div>b</div>c</body></html>")
+ document = treeCls.get("adapter", lambda x: x)(document)
+ output = Lint(treeCls["walker"](document))
+ for expectedToken, outputToken in zip(expected, output):
+ assert expectedToken == outputToken
+
+
+def set_attribute_on_first_child(docfrag, name, value, treeName):
+ """naively sets an attribute on the first child of the document
+ fragment passed in"""
+ setter = {'ElementTree': lambda d: d[0].set,
+ 'DOM': lambda d: d.firstChild.setAttribute}
+ setter['cElementTree'] = setter['ElementTree']
+ try:
+ setter.get(treeName, setter['DOM'])(docfrag)(name, value)
+ except AttributeError:
+ setter['ElementTree'](docfrag)(name, value)
+
+
+def param_treewalker_six_mix():
+ """Str/Unicode mix. If str attrs added to tree"""
+
+ # On Python 2.x string literals are of type str. Unless, like this
+ # file, the programmer imports unicode_literals from __future__.
+ # In that case, string literals become objects of type unicode.
+
+ # This test simulates a Py2 user, modifying attributes on a document
+ # fragment but not using the u'' syntax nor importing unicode_literals
+ sm_tests = [
+ ('<a href="http://example.com">Example</a>',
+ [(str('class'), str('test123'))],
+ '<a>\n class="test123"\n href="http://example.com"\n "Example"'),
+
+ ('<link href="http://example.com/cow">',
+ [(str('rel'), str('alternate'))],
+ '<link>\n href="http://example.com/cow"\n rel="alternate"\n "Example"')
+ ]
+
+ for tree in sorted(treeTypes.items()):
+ for intext, attrs, expected in sm_tests:
+ yield intext, expected, attrs, tree
+
+
+@pytest.mark.parametrize("intext, expected, attrs_to_add, tree", param_treewalker_six_mix())
+def test_treewalker_six_mix(intext, expected, attrs_to_add, tree):
+ """tests what happens when we add attributes to the intext"""
+ treeName, treeClass = tree
+ if treeClass is None:
+ pytest.skip("Treebuilder not loaded")
+ parser = html5parser.HTMLParser(tree=treeClass["builder"])
+ document = parser.parseFragment(intext)
+ for nom, val in attrs_to_add:
+ set_attribute_on_first_child(document, nom, val, treeName)
+
+ document = treeClass.get("adapter", lambda x: x)(document)
+ output = treewalkers.pprint(treeClass["walker"](document))
+ output = attrlist.sub(sortattrs, output)
+ if output not in expected:
+ raise AssertionError("TreewalkerEditTest: %s\nExpected:\n%s\nReceived:\n%s" % (treeName, expected, output))
+
+
+@pytest.mark.parametrize("tree,char", itertools.product(sorted(treeTypes.items()), ["x", "\u1234"]))
+def test_fragment_single_char(tree, char):
+ expected = [
+ {'data': char, 'type': 'Characters'}
+ ]
+
+ treeName, treeClass = tree
+ if treeClass is None:
+ pytest.skip("Treebuilder not loaded")
+
+ parser = html5parser.HTMLParser(tree=treeClass["builder"])
+ document = parser.parseFragment(char)
+ document = treeClass.get("adapter", lambda x: x)(document)
+ output = Lint(treeClass["walker"](document))
+
+ assert list(output) == expected
+
+
+@pytest.mark.skipif(treeTypes["lxml"] is None, reason="lxml not importable")
+def test_lxml_xml():
+ expected = [
+ {'data': {}, 'name': 'div', 'namespace': None, 'type': 'StartTag'},
+ {'data': {}, 'name': 'div', 'namespace': None, 'type': 'StartTag'},
+ {'name': 'div', 'namespace': None, 'type': 'EndTag'},
+ {'name': 'div', 'namespace': None, 'type': 'EndTag'}
+ ]
+
+ lxmltree = lxml.etree.fromstring('<div><div></div></div>')
+ walker = treewalkers.getTreeWalker('lxml')
+ output = Lint(walker(lxmltree))
+
+ assert list(output) == expected
+
+
+@pytest.mark.parametrize("treeName",
+ [pytest.param(treeName, marks=[getattr(pytest.mark, treeName),
+ pytest.mark.skipif(
+ treeName != "lxml" or
+ sys.version_info < (3, 7), reason="dict order undef")])
+ for treeName in sorted(treeTypes.keys())])
+def test_maintain_attribute_order(treeName):
+ treeAPIs = treeTypes[treeName]
+ if treeAPIs is None:
+ pytest.skip("Treebuilder not loaded")
+
+ # generate loads to maximize the chance a hash-based mutation will occur
+ attrs = [(unichr(x), text_type(i)) for i, x in enumerate(range(ord('a'), ord('z')))]
+ data = "<span " + " ".join("%s='%s'" % (x, i) for x, i in attrs) + ">"
+
+ parser = html5parser.HTMLParser(tree=treeAPIs["builder"])
+ document = parser.parseFragment(data)
+
+ document = treeAPIs.get("adapter", lambda x: x)(document)
+ output = list(Lint(treeAPIs["walker"](document)))
+
+ assert len(output) == 2
+ assert output[0]['type'] == 'StartTag'
+ assert output[1]['type'] == "EndTag"
+
+ attrs_out = output[0]['data']
+ assert len(attrs) == len(attrs_out)
+
+ for (in_name, in_value), (out_name, out_value) in zip(attrs, attrs_out.items()):
+ assert (None, in_name) == out_name
+ assert in_value == out_value
+
+
+@pytest.mark.parametrize("treeName",
+ [pytest.param(treeName, marks=[getattr(pytest.mark, treeName),
+ pytest.mark.skipif(
+ treeName != "lxml" or
+ sys.version_info < (3, 7), reason="dict order undef")])
+ for treeName in sorted(treeTypes.keys())])
+def test_maintain_attribute_order_adjusted(treeName):
+ treeAPIs = treeTypes[treeName]
+ if treeAPIs is None:
+ pytest.skip("Treebuilder not loaded")
+
+ # generate loads to maximize the chance a hash-based mutation will occur
+ data = "<svg a=1 refx=2 b=3 xml:lang=4 c=5>"
+
+ parser = html5parser.HTMLParser(tree=treeAPIs["builder"])
+ document = parser.parseFragment(data)
+
+ document = treeAPIs.get("adapter", lambda x: x)(document)
+ output = list(Lint(treeAPIs["walker"](document)))
+
+ assert len(output) == 2
+ assert output[0]['type'] == 'StartTag'
+ assert output[1]['type'] == "EndTag"
+
+ attrs_out = output[0]['data']
+
+ assert list(attrs_out.items()) == [((None, 'a'), '1'),
+ ((None, 'refX'), '2'),
+ ((None, 'b'), '3'),
+ (('http://www.w3.org/XML/1998/namespace', 'lang'), '4'),
+ ((None, 'c'), '5')]
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_whitespace_filter.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_whitespace_filter.py
new file mode 100644
index 0000000000..e9da6140a9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/test_whitespace_filter.py
@@ -0,0 +1,125 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from html5lib.filters.whitespace import Filter
+from html5lib.constants import spaceCharacters
+spaceCharacters = "".join(spaceCharacters)
+
+
+def runTest(input, expected):
+ output = list(Filter(input))
+ errorMsg = "\n".join(["\n\nInput:", str(input),
+ "\nExpected:", str(expected),
+ "\nReceived:", str(output)])
+ assert expected == output, errorMsg
+
+
+def runTestUnmodifiedOutput(input):
+ runTest(input, input)
+
+
+def testPhrasingElements():
+ runTestUnmodifiedOutput(
+ [{"type": "Characters", "data": "This is a "},
+ {"type": "StartTag", "name": "span", "data": []},
+ {"type": "Characters", "data": "phrase"},
+ {"type": "EndTag", "name": "span", "data": []},
+ {"type": "SpaceCharacters", "data": " "},
+ {"type": "Characters", "data": "with"},
+ {"type": "SpaceCharacters", "data": " "},
+ {"type": "StartTag", "name": "em", "data": []},
+ {"type": "Characters", "data": "emphasised text"},
+ {"type": "EndTag", "name": "em", "data": []},
+ {"type": "Characters", "data": " and an "},
+ {"type": "StartTag", "name": "img", "data": [["alt", "image"]]},
+ {"type": "Characters", "data": "."}])
+
+
+def testLeadingWhitespace():
+ runTest(
+ [{"type": "StartTag", "name": "p", "data": []},
+ {"type": "SpaceCharacters", "data": spaceCharacters},
+ {"type": "Characters", "data": "foo"},
+ {"type": "EndTag", "name": "p", "data": []}],
+ [{"type": "StartTag", "name": "p", "data": []},
+ {"type": "SpaceCharacters", "data": " "},
+ {"type": "Characters", "data": "foo"},
+ {"type": "EndTag", "name": "p", "data": []}])
+
+
+def testLeadingWhitespaceAsCharacters():
+ runTest(
+ [{"type": "StartTag", "name": "p", "data": []},
+ {"type": "Characters", "data": spaceCharacters + "foo"},
+ {"type": "EndTag", "name": "p", "data": []}],
+ [{"type": "StartTag", "name": "p", "data": []},
+ {"type": "Characters", "data": " foo"},
+ {"type": "EndTag", "name": "p", "data": []}])
+
+
+def testTrailingWhitespace():
+ runTest(
+ [{"type": "StartTag", "name": "p", "data": []},
+ {"type": "Characters", "data": "foo"},
+ {"type": "SpaceCharacters", "data": spaceCharacters},
+ {"type": "EndTag", "name": "p", "data": []}],
+ [{"type": "StartTag", "name": "p", "data": []},
+ {"type": "Characters", "data": "foo"},
+ {"type": "SpaceCharacters", "data": " "},
+ {"type": "EndTag", "name": "p", "data": []}])
+
+
+def testTrailingWhitespaceAsCharacters():
+ runTest(
+ [{"type": "StartTag", "name": "p", "data": []},
+ {"type": "Characters", "data": "foo" + spaceCharacters},
+ {"type": "EndTag", "name": "p", "data": []}],
+ [{"type": "StartTag", "name": "p", "data": []},
+ {"type": "Characters", "data": "foo "},
+ {"type": "EndTag", "name": "p", "data": []}])
+
+
+def testWhitespace():
+ runTest(
+ [{"type": "StartTag", "name": "p", "data": []},
+ {"type": "Characters", "data": "foo" + spaceCharacters + "bar"},
+ {"type": "EndTag", "name": "p", "data": []}],
+ [{"type": "StartTag", "name": "p", "data": []},
+ {"type": "Characters", "data": "foo bar"},
+ {"type": "EndTag", "name": "p", "data": []}])
+
+
+def testLeadingWhitespaceInPre():
+ runTestUnmodifiedOutput(
+ [{"type": "StartTag", "name": "pre", "data": []},
+ {"type": "SpaceCharacters", "data": spaceCharacters},
+ {"type": "Characters", "data": "foo"},
+ {"type": "EndTag", "name": "pre", "data": []}])
+
+
+def testLeadingWhitespaceAsCharactersInPre():
+ runTestUnmodifiedOutput(
+ [{"type": "StartTag", "name": "pre", "data": []},
+ {"type": "Characters", "data": spaceCharacters + "foo"},
+ {"type": "EndTag", "name": "pre", "data": []}])
+
+
+def testTrailingWhitespaceInPre():
+ runTestUnmodifiedOutput(
+ [{"type": "StartTag", "name": "pre", "data": []},
+ {"type": "Characters", "data": "foo"},
+ {"type": "SpaceCharacters", "data": spaceCharacters},
+ {"type": "EndTag", "name": "pre", "data": []}])
+
+
+def testTrailingWhitespaceAsCharactersInPre():
+ runTestUnmodifiedOutput(
+ [{"type": "StartTag", "name": "pre", "data": []},
+ {"type": "Characters", "data": "foo" + spaceCharacters},
+ {"type": "EndTag", "name": "pre", "data": []}])
+
+
+def testWhitespaceInPre():
+ runTestUnmodifiedOutput(
+ [{"type": "StartTag", "name": "pre", "data": []},
+ {"type": "Characters", "data": "foo" + spaceCharacters + "bar"},
+ {"type": "EndTag", "name": "pre", "data": []}])
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/tokenizer.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/tokenizer.py
new file mode 100644
index 0000000000..47264cc325
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/tokenizer.py
@@ -0,0 +1,253 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import codecs
+import json
+import warnings
+import re
+
+import pytest
+from six import unichr
+
+from html5lib._tokenizer import HTMLTokenizer
+from html5lib import constants, _utils
+
+
+class TokenizerTestParser(object):
+ def __init__(self, initialState, lastStartTag=None):
+ self.tokenizer = HTMLTokenizer
+ self._state = initialState
+ self._lastStartTag = lastStartTag
+
+ def parse(self, stream, encoding=None, innerHTML=False):
+ # pylint:disable=unused-argument
+ tokenizer = self.tokenizer(stream, encoding)
+ self.outputTokens = []
+
+ tokenizer.state = getattr(tokenizer, self._state)
+ if self._lastStartTag is not None:
+ tokenizer.currentToken = {"type": "startTag",
+ "name": self._lastStartTag}
+
+ types = {v: k for k, v in constants.tokenTypes.items()}
+ for token in tokenizer:
+ getattr(self, 'process%s' % types[token["type"]])(token)
+
+ return self.outputTokens
+
+ def processDoctype(self, token):
+ self.outputTokens.append(["DOCTYPE", token["name"], token["publicId"],
+ token["systemId"], token["correct"]])
+
+ def processStartTag(self, token):
+ self.outputTokens.append(["StartTag", token["name"],
+ token["data"], token["selfClosing"]])
+
+ def processEmptyTag(self, token):
+ if token["name"] not in constants.voidElements:
+ self.outputTokens.append("ParseError")
+ self.outputTokens.append(["StartTag", token["name"], dict(token["data"][::-1])])
+
+ def processEndTag(self, token):
+ self.outputTokens.append(["EndTag", token["name"],
+ token["selfClosing"]])
+
+ def processComment(self, token):
+ self.outputTokens.append(["Comment", token["data"]])
+
+ def processSpaceCharacters(self, token):
+ self.outputTokens.append(["Character", token["data"]])
+ self.processSpaceCharacters = self.processCharacters
+
+ def processCharacters(self, token):
+ self.outputTokens.append(["Character", token["data"]])
+
+ def processEOF(self, token):
+ pass
+
+ def processParseError(self, token):
+ self.outputTokens.append(["ParseError", token["data"]])
+
+
+def concatenateCharacterTokens(tokens):
+ outputTokens = []
+ for token in tokens:
+ if "ParseError" not in token and token[0] == "Character":
+ if (outputTokens and "ParseError" not in outputTokens[-1] and
+ outputTokens[-1][0] == "Character"):
+ outputTokens[-1][1] += token[1]
+ else:
+ outputTokens.append(token)
+ else:
+ outputTokens.append(token)
+ return outputTokens
+
+
+def normalizeTokens(tokens):
+ # TODO: convert tests to reflect arrays
+ for i, token in enumerate(tokens):
+ if token[0] == 'ParseError':
+ tokens[i] = token[0]
+ return tokens
+
+
+def tokensMatch(expectedTokens, receivedTokens, ignoreErrorOrder,
+ ignoreErrors=False):
+ """Test whether the test has passed or failed
+
+ If the ignoreErrorOrder flag is set to true we don't test the relative
+ positions of parse errors and non parse errors
+ """
+ checkSelfClosing = False
+ for token in expectedTokens:
+ if (token[0] == "StartTag" and len(token) == 4 or
+ token[0] == "EndTag" and len(token) == 3):
+ checkSelfClosing = True
+ break
+
+ if not checkSelfClosing:
+ for token in receivedTokens:
+ if token[0] == "StartTag" or token[0] == "EndTag":
+ token.pop()
+
+ if not ignoreErrorOrder and not ignoreErrors:
+ expectedTokens = concatenateCharacterTokens(expectedTokens)
+ return expectedTokens == receivedTokens
+ else:
+ # Sort the tokens into two groups; non-parse errors and parse errors
+ tokens = {"expected": [[], []], "received": [[], []]}
+ for tokenType, tokenList in zip(list(tokens.keys()),
+ (expectedTokens, receivedTokens)):
+ for token in tokenList:
+ if token != "ParseError":
+ tokens[tokenType][0].append(token)
+ else:
+ if not ignoreErrors:
+ tokens[tokenType][1].append(token)
+ tokens[tokenType][0] = concatenateCharacterTokens(tokens[tokenType][0])
+ return tokens["expected"] == tokens["received"]
+
+
+_surrogateRe = re.compile(r"\\u([0-9A-Fa-f]{4})(?:\\u([0-9A-Fa-f]{4}))?")
+
+
+def unescape(test):
+ def decode(inp):
+ """Decode \\uXXXX escapes
+
+ This decodes \\uXXXX escapes, possibly into non-BMP characters when
+ two surrogate character escapes are adjacent to each other.
+ """
+ # This cannot be implemented using the unicode_escape codec
+ # because that requires its input be ISO-8859-1, and we need
+ # arbitrary unicode as input.
+ def repl(m):
+ if m.group(2) is not None:
+ high = int(m.group(1), 16)
+ low = int(m.group(2), 16)
+ if 0xD800 <= high <= 0xDBFF and 0xDC00 <= low <= 0xDFFF:
+ cp = ((high - 0xD800) << 10) + (low - 0xDC00) + 0x10000
+ return unichr(cp)
+ else:
+ return unichr(high) + unichr(low)
+ else:
+ return unichr(int(m.group(1), 16))
+ try:
+ return _surrogateRe.sub(repl, inp)
+ except ValueError:
+ # This occurs when unichr throws ValueError, which should
+ # only be for a lone-surrogate.
+ if _utils.supports_lone_surrogates:
+ raise
+ return None
+
+ test["input"] = decode(test["input"])
+ for token in test["output"]:
+ if token == "ParseError":
+ continue
+ else:
+ token[1] = decode(token[1])
+ if len(token) > 2:
+ for key, value in token[2]:
+ del token[2][key]
+ token[2][decode(key)] = decode(value)
+ return test
+
+
+def _doCapitalize(match):
+ return match.group(1).upper()
+
+
+_capitalizeRe = re.compile(r"\W+(\w)").sub
+
+
+def capitalize(s):
+ s = s.lower()
+ s = _capitalizeRe(_doCapitalize, s)
+ return s
+
+
+class TokenizerFile(pytest.File):
+ def collect(self):
+ with codecs.open(str(self.fspath), "r", encoding="utf-8") as fp:
+ tests = json.load(fp)
+ if 'tests' in tests:
+ for i, test in enumerate(tests['tests']):
+ yield TokenizerTestCollector(str(i), self, testdata=test)
+
+
+class TokenizerTestCollector(pytest.Collector):
+ def __init__(self, name, parent=None, config=None, session=None, testdata=None):
+ super(TokenizerTestCollector, self).__init__(name, parent, config, session)
+ if 'initialStates' not in testdata:
+ testdata["initialStates"] = ["Data state"]
+ if 'doubleEscaped' in testdata:
+ testdata = unescape(testdata)
+ self.testdata = testdata
+
+ def collect(self):
+ for initialState in self.testdata["initialStates"]:
+ initialState = capitalize(initialState)
+ item = TokenizerTest(initialState,
+ self,
+ self.testdata,
+ initialState)
+ if self.testdata["input"] is None:
+ item.add_marker(pytest.mark.skipif(True, reason="Relies on lone surrogates"))
+ yield item
+
+
+class TokenizerTest(pytest.Item):
+ def __init__(self, name, parent, test, initialState):
+ super(TokenizerTest, self).__init__(name, parent)
+ self.obj = lambda: 1 # this is to hack around skipif needing a function!
+ self.test = test
+ self.initialState = initialState
+
+ def runtest(self):
+ warnings.resetwarnings()
+ warnings.simplefilter("error")
+
+ expected = self.test['output']
+ if 'lastStartTag' not in self.test:
+ self.test['lastStartTag'] = None
+ parser = TokenizerTestParser(self.initialState,
+ self.test['lastStartTag'])
+ tokens = parser.parse(self.test['input'])
+ received = normalizeTokens(tokens)
+ errorMsg = "\n".join(["\n\nInitial state:",
+ self.initialState,
+ "\nInput:", self.test['input'],
+ "\nExpected:", repr(expected),
+ "\nreceived:", repr(tokens)])
+ errorMsg = errorMsg
+ ignoreErrorOrder = self.test.get('ignoreErrorOrder', False)
+ assert tokensMatch(expected, received, ignoreErrorOrder, True), errorMsg
+
+ def repr_failure(self, excinfo):
+ traceback = excinfo.traceback
+ ntraceback = traceback.cut(path=__file__)
+ excinfo.traceback = ntraceback.filter()
+
+ return excinfo.getrepr(funcargs=True,
+ showlocals=False,
+ style="short", tbfilter=False)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/tokenizertotree.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/tokenizertotree.py
new file mode 100644
index 0000000000..8528e8766a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/tokenizertotree.py
@@ -0,0 +1,69 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import sys
+import os
+import json
+import re
+
+import html5lib
+from . import support
+from . import test_tokenizer
+
+p = html5lib.HTMLParser()
+
+unnamespaceExpected = re.compile(r"^(\|\s*)<html ([^>]+)>", re.M).sub
+
+
+def main(out_path):
+ if not os.path.exists(out_path):
+ sys.stderr.write("Path %s does not exist" % out_path)
+ sys.exit(1)
+
+ for filename in support.get_data_files('tokenizer', '*.test'):
+ run_file(filename, out_path)
+
+
+def run_file(filename, out_path):
+ try:
+ tests_data = json.load(open(filename, "r"))
+ except ValueError:
+ sys.stderr.write("Failed to load %s\n" % filename)
+ return
+ name = os.path.splitext(os.path.split(filename)[1])[0]
+ output_file = open(os.path.join(out_path, "tokenizer_%s.dat" % name), "w")
+
+ if 'tests' in tests_data:
+ for test_data in tests_data['tests']:
+ if 'initialStates' not in test_data:
+ test_data["initialStates"] = ["Data state"]
+
+ for initial_state in test_data["initialStates"]:
+ if initial_state != "Data state":
+ # don't support this yet
+ continue
+ test = make_test(test_data)
+ output_file.write(test)
+
+ output_file.close()
+
+
+def make_test(test_data):
+ if 'doubleEscaped' in test_data:
+ test_data = test_tokenizer.unescape_test(test_data)
+
+ rv = []
+ rv.append("#data")
+ rv.append(test_data["input"].encode("utf8"))
+ rv.append("#errors")
+ tree = p.parse(test_data["input"])
+ output = p.tree.testSerializer(tree)
+ output = "\n".join(("| " + line[3:]) if line.startswith("| ") else line
+ for line in output.split("\n"))
+ output = unnamespaceExpected(r"\1<\2>", output)
+ rv.append(output.encode("utf8"))
+ rv.append("")
+ return "\n".join(rv)
+
+
+if __name__ == "__main__":
+ main(sys.argv[1])
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/tree_construction.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/tree_construction.py
new file mode 100644
index 0000000000..1ef6e7250c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/tree_construction.py
@@ -0,0 +1,205 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import itertools
+import re
+import warnings
+from difflib import unified_diff
+
+import pytest
+
+from .support import TestData, convert, convertExpected, treeTypes
+from html5lib import html5parser, constants, treewalkers
+from html5lib.filters.lint import Filter as Lint
+
+_attrlist_re = re.compile(r"^(\s+)\w+=.*(\n\1\w+=.*)+", re.M)
+
+
+def sortattrs(s):
+ def replace(m):
+ lines = m.group(0).split("\n")
+ lines.sort()
+ return "\n".join(lines)
+ return _attrlist_re.sub(replace, s)
+
+
+class TreeConstructionFile(pytest.File):
+ def collect(self):
+ tests = TestData(str(self.fspath), "data")
+ for i, test in enumerate(tests):
+ yield TreeConstructionTest(str(i), self, testdata=test)
+
+
+class TreeConstructionTest(pytest.Collector):
+ def __init__(self, name, parent=None, config=None, session=None, testdata=None):
+ super(TreeConstructionTest, self).__init__(name, parent, config, session)
+ self.testdata = testdata
+
+ def collect(self):
+ for treeName, treeAPIs in sorted(treeTypes.items()):
+ for x in itertools.chain(self._getParserTests(treeName, treeAPIs),
+ self._getTreeWalkerTests(treeName, treeAPIs)):
+ yield x
+
+ def _getParserTests(self, treeName, treeAPIs):
+ if treeAPIs is not None and "adapter" in treeAPIs:
+ return
+ for namespaceHTMLElements in (True, False):
+ if namespaceHTMLElements:
+ nodeid = "%s::parser::namespaced" % treeName
+ else:
+ nodeid = "%s::parser::void-namespace" % treeName
+ item = ParserTest(nodeid,
+ self,
+ self.testdata,
+ treeAPIs["builder"] if treeAPIs is not None else None,
+ namespaceHTMLElements)
+ item.add_marker(getattr(pytest.mark, treeName))
+ item.add_marker(pytest.mark.parser)
+ if namespaceHTMLElements:
+ item.add_marker(pytest.mark.namespaced)
+ yield item
+
+ def _getTreeWalkerTests(self, treeName, treeAPIs):
+ nodeid = "%s::treewalker" % treeName
+ item = TreeWalkerTest(nodeid,
+ self,
+ self.testdata,
+ treeAPIs)
+ item.add_marker(getattr(pytest.mark, treeName))
+ item.add_marker(pytest.mark.treewalker)
+ yield item
+
+
+def convertTreeDump(data):
+ return "\n".join(convert(3)(data).split("\n")[1:])
+
+
+namespaceExpected = re.compile(r"^(\s*)<(\S+)>", re.M).sub
+
+
+class ParserTest(pytest.Item):
+ def __init__(self, name, parent, test, treeClass, namespaceHTMLElements):
+ super(ParserTest, self).__init__(name, parent)
+ self.test = test
+ self.treeClass = treeClass
+ self.namespaceHTMLElements = namespaceHTMLElements
+
+ def runtest(self):
+ if self.treeClass is None:
+ pytest.skip("Treebuilder not loaded")
+
+ p = html5parser.HTMLParser(tree=self.treeClass,
+ namespaceHTMLElements=self.namespaceHTMLElements)
+
+ input = self.test['data']
+ fragmentContainer = self.test['document-fragment']
+ expected = convertExpected(self.test['document'])
+ expectedErrors = self.test['errors'].split("\n") if self.test['errors'] else []
+
+ scripting = False
+ if 'script-on' in self.test:
+ scripting = True
+
+ with warnings.catch_warnings():
+ warnings.simplefilter("error")
+ try:
+ if fragmentContainer:
+ document = p.parseFragment(input, fragmentContainer, scripting=scripting)
+ else:
+ document = p.parse(input, scripting=scripting)
+ except constants.DataLossWarning:
+ pytest.skip("data loss warning")
+
+ output = convertTreeDump(p.tree.testSerializer(document))
+
+ expected = expected
+ if self.namespaceHTMLElements:
+ expected = namespaceExpected(r"\1<html \2>", expected)
+
+ errorMsg = "\n".join(["\n\nInput:", input, "\nExpected:", expected,
+ "\nReceived:", output])
+ assert expected == output, errorMsg
+
+ errStr = []
+ for (line, col), errorcode, datavars in p.errors:
+ assert isinstance(datavars, dict), "%s, %s" % (errorcode, repr(datavars))
+ errStr.append("Line: %i Col: %i %s" % (line, col,
+ constants.E[errorcode] % datavars))
+
+ errorMsg2 = "\n".join(["\n\nInput:", input,
+ "\nExpected errors (" + str(len(expectedErrors)) + "):\n" + "\n".join(expectedErrors),
+ "\nActual errors (" + str(len(p.errors)) + "):\n" + "\n".join(errStr)])
+ if False: # we're currently not testing parse errors
+ assert len(p.errors) == len(expectedErrors), errorMsg2
+
+ def repr_failure(self, excinfo):
+ traceback = excinfo.traceback
+ ntraceback = traceback.cut(path=__file__)
+ excinfo.traceback = ntraceback.filter()
+
+ return excinfo.getrepr(funcargs=True,
+ showlocals=False,
+ style="short", tbfilter=False)
+
+
+class TreeWalkerTest(pytest.Item):
+ def __init__(self, name, parent, test, treeAPIs):
+ super(TreeWalkerTest, self).__init__(name, parent)
+ self.test = test
+ self.treeAPIs = treeAPIs
+
+ def runtest(self):
+ if self.treeAPIs is None:
+ pytest.skip("Treebuilder not loaded")
+
+ p = html5parser.HTMLParser(tree=self.treeAPIs["builder"])
+
+ input = self.test['data']
+ fragmentContainer = self.test['document-fragment']
+ expected = convertExpected(self.test['document'])
+
+ scripting = False
+ if 'script-on' in self.test:
+ scripting = True
+
+ with warnings.catch_warnings():
+ warnings.simplefilter("error")
+ try:
+ if fragmentContainer:
+ document = p.parseFragment(input, fragmentContainer, scripting=scripting)
+ else:
+ document = p.parse(input, scripting=scripting)
+ except constants.DataLossWarning:
+ pytest.skip("data loss warning")
+
+ poutput = convertTreeDump(p.tree.testSerializer(document))
+ namespace_expected = namespaceExpected(r"\1<html \2>", expected)
+ if poutput != namespace_expected:
+ pytest.skip("parser output incorrect")
+
+ document = self.treeAPIs.get("adapter", lambda x: x)(document)
+
+ try:
+ output = treewalkers.pprint(Lint(self.treeAPIs["walker"](document)))
+ output = sortattrs(output)
+ expected = sortattrs(expected)
+ diff = "".join(unified_diff([line + "\n" for line in expected.splitlines()],
+ [line + "\n" for line in output.splitlines()],
+ "Expected", "Received"))
+ assert expected == output, "\n".join([
+ "", "Input:", input,
+ "", "Expected:", expected,
+ "", "Received:", output,
+ "", "Diff:", diff,
+ ])
+ except NotImplementedError:
+ pytest.skip("tree walker NotImplementedError")
+
+ def repr_failure(self, excinfo):
+ traceback = excinfo.traceback
+ ntraceback = traceback.cut(path=__file__)
+ excinfo.traceback = ntraceback.filter()
+
+ return excinfo.getrepr(funcargs=True,
+ showlocals=False,
+ style="short", tbfilter=False)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/us-ascii.html b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/us-ascii.html
new file mode 100644
index 0000000000..728cb6baf9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/us-ascii.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<title>Test</title>
+<p>Hello World! \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/utf-8-bom.html b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/utf-8-bom.html
new file mode 100644
index 0000000000..6ac5efcedf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/tests/utf-8-bom.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<title>Test</title>
+<p>Hello World! © \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treeadapters/__init__.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treeadapters/__init__.py
new file mode 100644
index 0000000000..dfeb0ba5e1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treeadapters/__init__.py
@@ -0,0 +1,30 @@
+"""Tree adapters let you convert from one tree structure to another
+
+Example:
+
+.. code-block:: python
+
+ import html5lib
+ from html5lib.treeadapters import genshi
+
+ doc = '<html><body>Hi!</body></html>'
+ treebuilder = html5lib.getTreeBuilder('etree')
+ parser = html5lib.HTMLParser(tree=treebuilder)
+ tree = parser.parse(doc)
+ TreeWalker = html5lib.getTreeWalker('etree')
+
+ genshi_tree = genshi.to_genshi(TreeWalker(tree))
+
+"""
+from __future__ import absolute_import, division, unicode_literals
+
+from . import sax
+
+__all__ = ["sax"]
+
+try:
+ from . import genshi # noqa
+except ImportError:
+ pass
+else:
+ __all__.append("genshi")
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treeadapters/genshi.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treeadapters/genshi.py
new file mode 100644
index 0000000000..61d5fb6ac4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treeadapters/genshi.py
@@ -0,0 +1,54 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from genshi.core import QName, Attrs
+from genshi.core import START, END, TEXT, COMMENT, DOCTYPE
+
+
+def to_genshi(walker):
+ """Convert a tree to a genshi tree
+
+ :arg walker: the treewalker to use to walk the tree to convert it
+
+ :returns: generator of genshi nodes
+
+ """
+ text = []
+ for token in walker:
+ type = token["type"]
+ if type in ("Characters", "SpaceCharacters"):
+ text.append(token["data"])
+ elif text:
+ yield TEXT, "".join(text), (None, -1, -1)
+ text = []
+
+ if type in ("StartTag", "EmptyTag"):
+ if token["namespace"]:
+ name = "{%s}%s" % (token["namespace"], token["name"])
+ else:
+ name = token["name"]
+ attrs = Attrs([(QName("{%s}%s" % attr if attr[0] is not None else attr[1]), value)
+ for attr, value in token["data"].items()])
+ yield (START, (QName(name), attrs), (None, -1, -1))
+ if type == "EmptyTag":
+ type = "EndTag"
+
+ if type == "EndTag":
+ if token["namespace"]:
+ name = "{%s}%s" % (token["namespace"], token["name"])
+ else:
+ name = token["name"]
+
+ yield END, QName(name), (None, -1, -1)
+
+ elif type == "Comment":
+ yield COMMENT, token["data"], (None, -1, -1)
+
+ elif type == "Doctype":
+ yield DOCTYPE, (token["name"], token["publicId"],
+ token["systemId"]), (None, -1, -1)
+
+ else:
+ pass # FIXME: What to do?
+
+ if text:
+ yield TEXT, "".join(text), (None, -1, -1)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treeadapters/sax.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treeadapters/sax.py
new file mode 100644
index 0000000000..f4ccea5a25
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treeadapters/sax.py
@@ -0,0 +1,50 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from xml.sax.xmlreader import AttributesNSImpl
+
+from ..constants import adjustForeignAttributes, unadjustForeignAttributes
+
+prefix_mapping = {}
+for prefix, localName, namespace in adjustForeignAttributes.values():
+ if prefix is not None:
+ prefix_mapping[prefix] = namespace
+
+
+def to_sax(walker, handler):
+ """Call SAX-like content handler based on treewalker walker
+
+ :arg walker: the treewalker to use to walk the tree to convert it
+
+ :arg handler: SAX handler to use
+
+ """
+ handler.startDocument()
+ for prefix, namespace in prefix_mapping.items():
+ handler.startPrefixMapping(prefix, namespace)
+
+ for token in walker:
+ type = token["type"]
+ if type == "Doctype":
+ continue
+ elif type in ("StartTag", "EmptyTag"):
+ attrs = AttributesNSImpl(token["data"],
+ unadjustForeignAttributes)
+ handler.startElementNS((token["namespace"], token["name"]),
+ token["name"],
+ attrs)
+ if type == "EmptyTag":
+ handler.endElementNS((token["namespace"], token["name"]),
+ token["name"])
+ elif type == "EndTag":
+ handler.endElementNS((token["namespace"], token["name"]),
+ token["name"])
+ elif type in ("Characters", "SpaceCharacters"):
+ handler.characters(token["data"])
+ elif type == "Comment":
+ pass
+ else:
+ assert False, "Unknown token type"
+
+ for prefix, namespace in prefix_mapping.items():
+ handler.endPrefixMapping(prefix)
+ handler.endDocument()
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/__init__.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/__init__.py
new file mode 100644
index 0000000000..d44447eaf5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/__init__.py
@@ -0,0 +1,88 @@
+"""A collection of modules for building different kinds of trees from HTML
+documents.
+
+To create a treebuilder for a new type of tree, you need to do
+implement several things:
+
+1. A set of classes for various types of elements: Document, Doctype, Comment,
+ Element. These must implement the interface of ``base.treebuilders.Node``
+ (although comment nodes have a different signature for their constructor,
+ see ``treebuilders.etree.Comment``) Textual content may also be implemented
+ as another node type, or not, as your tree implementation requires.
+
+2. A treebuilder object (called ``TreeBuilder`` by convention) that inherits
+ from ``treebuilders.base.TreeBuilder``. This has 4 required attributes:
+
+ * ``documentClass`` - the class to use for the bottommost node of a document
+ * ``elementClass`` - the class to use for HTML Elements
+ * ``commentClass`` - the class to use for comments
+ * ``doctypeClass`` - the class to use for doctypes
+
+ It also has one required method:
+
+ * ``getDocument`` - Returns the root node of the complete document tree
+
+3. If you wish to run the unit tests, you must also create a ``testSerializer``
+ method on your treebuilder which accepts a node and returns a string
+ containing Node and its children serialized according to the format used in
+ the unittests
+
+"""
+
+from __future__ import absolute_import, division, unicode_literals
+
+from .._utils import default_etree
+
+treeBuilderCache = {}
+
+
+def getTreeBuilder(treeType, implementation=None, **kwargs):
+ """Get a TreeBuilder class for various types of trees with built-in support
+
+ :arg treeType: the name of the tree type required (case-insensitive). Supported
+ values are:
+
+ * "dom" - A generic builder for DOM implementations, defaulting to a
+ xml.dom.minidom based implementation.
+ * "etree" - A generic builder for tree implementations exposing an
+ ElementTree-like interface, defaulting to xml.etree.cElementTree if
+ available and xml.etree.ElementTree if not.
+ * "lxml" - A etree-based builder for lxml.etree, handling limitations
+ of lxml's implementation.
+
+ :arg implementation: (Currently applies to the "etree" and "dom" tree
+ types). A module implementing the tree type e.g. xml.etree.ElementTree
+ or xml.etree.cElementTree.
+
+ :arg kwargs: Any additional options to pass to the TreeBuilder when
+ creating it.
+
+ Example:
+
+ >>> from html5lib.treebuilders import getTreeBuilder
+ >>> builder = getTreeBuilder('etree')
+
+ """
+
+ treeType = treeType.lower()
+ if treeType not in treeBuilderCache:
+ if treeType == "dom":
+ from . import dom
+ # Come up with a sane default (pref. from the stdlib)
+ if implementation is None:
+ from xml.dom import minidom
+ implementation = minidom
+ # NEVER cache here, caching is done in the dom submodule
+ return dom.getDomModule(implementation, **kwargs).TreeBuilder
+ elif treeType == "lxml":
+ from . import etree_lxml
+ treeBuilderCache[treeType] = etree_lxml.TreeBuilder
+ elif treeType == "etree":
+ from . import etree
+ if implementation is None:
+ implementation = default_etree
+ # NEVER cache here, caching is done in the etree submodule
+ return etree.getETreeModule(implementation, **kwargs).TreeBuilder
+ else:
+ raise ValueError("""Unrecognised treebuilder "%s" """ % treeType)
+ return treeBuilderCache.get(treeType)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/base.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/base.py
new file mode 100644
index 0000000000..e4a3d710d9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/base.py
@@ -0,0 +1,417 @@
+from __future__ import absolute_import, division, unicode_literals
+from six import text_type
+
+from ..constants import scopingElements, tableInsertModeElements, namespaces
+
+# The scope markers are inserted when entering object elements,
+# marquees, table cells, and table captions, and are used to prevent formatting
+# from "leaking" into tables, object elements, and marquees.
+Marker = None
+
+listElementsMap = {
+ None: (frozenset(scopingElements), False),
+ "button": (frozenset(scopingElements | {(namespaces["html"], "button")}), False),
+ "list": (frozenset(scopingElements | {(namespaces["html"], "ol"),
+ (namespaces["html"], "ul")}), False),
+ "table": (frozenset([(namespaces["html"], "html"),
+ (namespaces["html"], "table")]), False),
+ "select": (frozenset([(namespaces["html"], "optgroup"),
+ (namespaces["html"], "option")]), True)
+}
+
+
+class Node(object):
+ """Represents an item in the tree"""
+ def __init__(self, name):
+ """Creates a Node
+
+ :arg name: The tag name associated with the node
+
+ """
+ # The tag name associated with the node
+ self.name = name
+ # The parent of the current node (or None for the document node)
+ self.parent = None
+ # The value of the current node (applies to text nodes and comments)
+ self.value = None
+ # A dict holding name -> value pairs for attributes of the node
+ self.attributes = {}
+ # A list of child nodes of the current node. This must include all
+ # elements but not necessarily other node types.
+ self.childNodes = []
+ # A list of miscellaneous flags that can be set on the node.
+ self._flags = []
+
+ def __str__(self):
+ attributesStr = " ".join(["%s=\"%s\"" % (name, value)
+ for name, value in
+ self.attributes.items()])
+ if attributesStr:
+ return "<%s %s>" % (self.name, attributesStr)
+ else:
+ return "<%s>" % (self.name)
+
+ def __repr__(self):
+ return "<%s>" % (self.name)
+
+ def appendChild(self, node):
+ """Insert node as a child of the current node
+
+ :arg node: the node to insert
+
+ """
+ raise NotImplementedError
+
+ def insertText(self, data, insertBefore=None):
+ """Insert data as text in the current node, positioned before the
+ start of node insertBefore or to the end of the node's text.
+
+ :arg data: the data to insert
+
+ :arg insertBefore: True if you want to insert the text before the node
+ and False if you want to insert it after the node
+
+ """
+ raise NotImplementedError
+
+ def insertBefore(self, node, refNode):
+ """Insert node as a child of the current node, before refNode in the
+ list of child nodes. Raises ValueError if refNode is not a child of
+ the current node
+
+ :arg node: the node to insert
+
+ :arg refNode: the child node to insert the node before
+
+ """
+ raise NotImplementedError
+
+ def removeChild(self, node):
+ """Remove node from the children of the current node
+
+ :arg node: the child node to remove
+
+ """
+ raise NotImplementedError
+
+ def reparentChildren(self, newParent):
+ """Move all the children of the current node to newParent.
+ This is needed so that trees that don't store text as nodes move the
+ text in the correct way
+
+ :arg newParent: the node to move all this node's children to
+
+ """
+ # XXX - should this method be made more general?
+ for child in self.childNodes:
+ newParent.appendChild(child)
+ self.childNodes = []
+
+ def cloneNode(self):
+ """Return a shallow copy of the current node i.e. a node with the same
+ name and attributes but with no parent or child nodes
+ """
+ raise NotImplementedError
+
+ def hasContent(self):
+ """Return true if the node has children or text, false otherwise
+ """
+ raise NotImplementedError
+
+
+class ActiveFormattingElements(list):
+ def append(self, node):
+ equalCount = 0
+ if node != Marker:
+ for element in self[::-1]:
+ if element == Marker:
+ break
+ if self.nodesEqual(element, node):
+ equalCount += 1
+ if equalCount == 3:
+ self.remove(element)
+ break
+ list.append(self, node)
+
+ def nodesEqual(self, node1, node2):
+ if not node1.nameTuple == node2.nameTuple:
+ return False
+
+ if not node1.attributes == node2.attributes:
+ return False
+
+ return True
+
+
+class TreeBuilder(object):
+ """Base treebuilder implementation
+
+ * documentClass - the class to use for the bottommost node of a document
+ * elementClass - the class to use for HTML Elements
+ * commentClass - the class to use for comments
+ * doctypeClass - the class to use for doctypes
+
+ """
+ # pylint:disable=not-callable
+
+ # Document class
+ documentClass = None
+
+ # The class to use for creating a node
+ elementClass = None
+
+ # The class to use for creating comments
+ commentClass = None
+
+ # The class to use for creating doctypes
+ doctypeClass = None
+
+ # Fragment class
+ fragmentClass = None
+
+ def __init__(self, namespaceHTMLElements):
+ """Create a TreeBuilder
+
+ :arg namespaceHTMLElements: whether or not to namespace HTML elements
+
+ """
+ if namespaceHTMLElements:
+ self.defaultNamespace = "http://www.w3.org/1999/xhtml"
+ else:
+ self.defaultNamespace = None
+ self.reset()
+
+ def reset(self):
+ self.openElements = []
+ self.activeFormattingElements = ActiveFormattingElements()
+
+ # XXX - rename these to headElement, formElement
+ self.headPointer = None
+ self.formPointer = None
+
+ self.insertFromTable = False
+
+ self.document = self.documentClass()
+
+ def elementInScope(self, target, variant=None):
+
+ # If we pass a node in we match that. if we pass a string
+ # match any node with that name
+ exactNode = hasattr(target, "nameTuple")
+ if not exactNode:
+ if isinstance(target, text_type):
+ target = (namespaces["html"], target)
+ assert isinstance(target, tuple)
+
+ listElements, invert = listElementsMap[variant]
+
+ for node in reversed(self.openElements):
+ if exactNode and node == target:
+ return True
+ elif not exactNode and node.nameTuple == target:
+ return True
+ elif (invert ^ (node.nameTuple in listElements)):
+ return False
+
+ assert False # We should never reach this point
+
+ def reconstructActiveFormattingElements(self):
+ # Within this algorithm the order of steps described in the
+ # specification is not quite the same as the order of steps in the
+ # code. It should still do the same though.
+
+ # Step 1: stop the algorithm when there's nothing to do.
+ if not self.activeFormattingElements:
+ return
+
+ # Step 2 and step 3: we start with the last element. So i is -1.
+ i = len(self.activeFormattingElements) - 1
+ entry = self.activeFormattingElements[i]
+ if entry == Marker or entry in self.openElements:
+ return
+
+ # Step 6
+ while entry != Marker and entry not in self.openElements:
+ if i == 0:
+ # This will be reset to 0 below
+ i = -1
+ break
+ i -= 1
+ # Step 5: let entry be one earlier in the list.
+ entry = self.activeFormattingElements[i]
+
+ while True:
+ # Step 7
+ i += 1
+
+ # Step 8
+ entry = self.activeFormattingElements[i]
+ clone = entry.cloneNode() # Mainly to get a new copy of the attributes
+
+ # Step 9
+ element = self.insertElement({"type": "StartTag",
+ "name": clone.name,
+ "namespace": clone.namespace,
+ "data": clone.attributes})
+
+ # Step 10
+ self.activeFormattingElements[i] = element
+
+ # Step 11
+ if element == self.activeFormattingElements[-1]:
+ break
+
+ def clearActiveFormattingElements(self):
+ entry = self.activeFormattingElements.pop()
+ while self.activeFormattingElements and entry != Marker:
+ entry = self.activeFormattingElements.pop()
+
+ def elementInActiveFormattingElements(self, name):
+ """Check if an element exists between the end of the active
+ formatting elements and the last marker. If it does, return it, else
+ return false"""
+
+ for item in self.activeFormattingElements[::-1]:
+ # Check for Marker first because if it's a Marker it doesn't have a
+ # name attribute.
+ if item == Marker:
+ break
+ elif item.name == name:
+ return item
+ return False
+
+ def insertRoot(self, token):
+ element = self.createElement(token)
+ self.openElements.append(element)
+ self.document.appendChild(element)
+
+ def insertDoctype(self, token):
+ name = token["name"]
+ publicId = token["publicId"]
+ systemId = token["systemId"]
+
+ doctype = self.doctypeClass(name, publicId, systemId)
+ self.document.appendChild(doctype)
+
+ def insertComment(self, token, parent=None):
+ if parent is None:
+ parent = self.openElements[-1]
+ parent.appendChild(self.commentClass(token["data"]))
+
+ def createElement(self, token):
+ """Create an element but don't insert it anywhere"""
+ name = token["name"]
+ namespace = token.get("namespace", self.defaultNamespace)
+ element = self.elementClass(name, namespace)
+ element.attributes = token["data"]
+ return element
+
+ def _getInsertFromTable(self):
+ return self._insertFromTable
+
+ def _setInsertFromTable(self, value):
+ """Switch the function used to insert an element from the
+ normal one to the misnested table one and back again"""
+ self._insertFromTable = value
+ if value:
+ self.insertElement = self.insertElementTable
+ else:
+ self.insertElement = self.insertElementNormal
+
+ insertFromTable = property(_getInsertFromTable, _setInsertFromTable)
+
+ def insertElementNormal(self, token):
+ name = token["name"]
+ assert isinstance(name, text_type), "Element %s not unicode" % name
+ namespace = token.get("namespace", self.defaultNamespace)
+ element = self.elementClass(name, namespace)
+ element.attributes = token["data"]
+ self.openElements[-1].appendChild(element)
+ self.openElements.append(element)
+ return element
+
+ def insertElementTable(self, token):
+ """Create an element and insert it into the tree"""
+ element = self.createElement(token)
+ if self.openElements[-1].name not in tableInsertModeElements:
+ return self.insertElementNormal(token)
+ else:
+ # We should be in the InTable mode. This means we want to do
+ # special magic element rearranging
+ parent, insertBefore = self.getTableMisnestedNodePosition()
+ if insertBefore is None:
+ parent.appendChild(element)
+ else:
+ parent.insertBefore(element, insertBefore)
+ self.openElements.append(element)
+ return element
+
+ def insertText(self, data, parent=None):
+ """Insert text data."""
+ if parent is None:
+ parent = self.openElements[-1]
+
+ if (not self.insertFromTable or (self.insertFromTable and
+ self.openElements[-1].name
+ not in tableInsertModeElements)):
+ parent.insertText(data)
+ else:
+ # We should be in the InTable mode. This means we want to do
+ # special magic element rearranging
+ parent, insertBefore = self.getTableMisnestedNodePosition()
+ parent.insertText(data, insertBefore)
+
+ def getTableMisnestedNodePosition(self):
+ """Get the foster parent element, and sibling to insert before
+ (or None) when inserting a misnested table node"""
+ # The foster parent element is the one which comes before the most
+ # recently opened table element
+ # XXX - this is really inelegant
+ lastTable = None
+ fosterParent = None
+ insertBefore = None
+ for elm in self.openElements[::-1]:
+ if elm.name == "table":
+ lastTable = elm
+ break
+ if lastTable:
+ # XXX - we should really check that this parent is actually a
+ # node here
+ if lastTable.parent:
+ fosterParent = lastTable.parent
+ insertBefore = lastTable
+ else:
+ fosterParent = self.openElements[
+ self.openElements.index(lastTable) - 1]
+ else:
+ fosterParent = self.openElements[0]
+ return fosterParent, insertBefore
+
+ def generateImpliedEndTags(self, exclude=None):
+ name = self.openElements[-1].name
+ # XXX td, th and tr are not actually needed
+ if (name in frozenset(("dd", "dt", "li", "option", "optgroup", "p", "rp", "rt")) and
+ name != exclude):
+ self.openElements.pop()
+ # XXX This is not entirely what the specification says. We should
+ # investigate it more closely.
+ self.generateImpliedEndTags(exclude)
+
+ def getDocument(self):
+ """Return the final tree"""
+ return self.document
+
+ def getFragment(self):
+ """Return the final fragment"""
+ # assert self.innerHTML
+ fragment = self.fragmentClass()
+ self.openElements[0].reparentChildren(fragment)
+ return fragment
+
+ def testSerializer(self, node):
+ """Serialize the subtree of node in the format required by unit tests
+
+ :arg node: the node from which to start serializing
+
+ """
+ raise NotImplementedError
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/dom.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/dom.py
new file mode 100644
index 0000000000..d8b5300465
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/dom.py
@@ -0,0 +1,239 @@
+from __future__ import absolute_import, division, unicode_literals
+
+
+try:
+ from collections.abc import MutableMapping
+except ImportError: # Python 2.7
+ from collections import MutableMapping
+from xml.dom import minidom, Node
+import weakref
+
+from . import base
+from .. import constants
+from ..constants import namespaces
+from .._utils import moduleFactoryFactory
+
+
+def getDomBuilder(DomImplementation):
+ Dom = DomImplementation
+
+ class AttrList(MutableMapping):
+ def __init__(self, element):
+ self.element = element
+
+ def __iter__(self):
+ return iter(self.element.attributes.keys())
+
+ def __setitem__(self, name, value):
+ if isinstance(name, tuple):
+ raise NotImplementedError
+ else:
+ attr = self.element.ownerDocument.createAttribute(name)
+ attr.value = value
+ self.element.attributes[name] = attr
+
+ def __len__(self):
+ return len(self.element.attributes)
+
+ def items(self):
+ return list(self.element.attributes.items())
+
+ def values(self):
+ return list(self.element.attributes.values())
+
+ def __getitem__(self, name):
+ if isinstance(name, tuple):
+ raise NotImplementedError
+ else:
+ return self.element.attributes[name].value
+
+ def __delitem__(self, name):
+ if isinstance(name, tuple):
+ raise NotImplementedError
+ else:
+ del self.element.attributes[name]
+
+ class NodeBuilder(base.Node):
+ def __init__(self, element):
+ base.Node.__init__(self, element.nodeName)
+ self.element = element
+
+ namespace = property(lambda self: hasattr(self.element, "namespaceURI") and
+ self.element.namespaceURI or None)
+
+ def appendChild(self, node):
+ node.parent = self
+ self.element.appendChild(node.element)
+
+ def insertText(self, data, insertBefore=None):
+ text = self.element.ownerDocument.createTextNode(data)
+ if insertBefore:
+ self.element.insertBefore(text, insertBefore.element)
+ else:
+ self.element.appendChild(text)
+
+ def insertBefore(self, node, refNode):
+ self.element.insertBefore(node.element, refNode.element)
+ node.parent = self
+
+ def removeChild(self, node):
+ if node.element.parentNode == self.element:
+ self.element.removeChild(node.element)
+ node.parent = None
+
+ def reparentChildren(self, newParent):
+ while self.element.hasChildNodes():
+ child = self.element.firstChild
+ self.element.removeChild(child)
+ newParent.element.appendChild(child)
+ self.childNodes = []
+
+ def getAttributes(self):
+ return AttrList(self.element)
+
+ def setAttributes(self, attributes):
+ if attributes:
+ for name, value in list(attributes.items()):
+ if isinstance(name, tuple):
+ if name[0] is not None:
+ qualifiedName = (name[0] + ":" + name[1])
+ else:
+ qualifiedName = name[1]
+ self.element.setAttributeNS(name[2], qualifiedName,
+ value)
+ else:
+ self.element.setAttribute(
+ name, value)
+ attributes = property(getAttributes, setAttributes)
+
+ def cloneNode(self):
+ return NodeBuilder(self.element.cloneNode(False))
+
+ def hasContent(self):
+ return self.element.hasChildNodes()
+
+ def getNameTuple(self):
+ if self.namespace is None:
+ return namespaces["html"], self.name
+ else:
+ return self.namespace, self.name
+
+ nameTuple = property(getNameTuple)
+
+ class TreeBuilder(base.TreeBuilder): # pylint:disable=unused-variable
+ def documentClass(self):
+ self.dom = Dom.getDOMImplementation().createDocument(None, None, None)
+ return weakref.proxy(self)
+
+ def insertDoctype(self, token):
+ name = token["name"]
+ publicId = token["publicId"]
+ systemId = token["systemId"]
+
+ domimpl = Dom.getDOMImplementation()
+ doctype = domimpl.createDocumentType(name, publicId, systemId)
+ self.document.appendChild(NodeBuilder(doctype))
+ if Dom == minidom:
+ doctype.ownerDocument = self.dom
+
+ def elementClass(self, name, namespace=None):
+ if namespace is None and self.defaultNamespace is None:
+ node = self.dom.createElement(name)
+ else:
+ node = self.dom.createElementNS(namespace, name)
+
+ return NodeBuilder(node)
+
+ def commentClass(self, data):
+ return NodeBuilder(self.dom.createComment(data))
+
+ def fragmentClass(self):
+ return NodeBuilder(self.dom.createDocumentFragment())
+
+ def appendChild(self, node):
+ self.dom.appendChild(node.element)
+
+ def testSerializer(self, element):
+ return testSerializer(element)
+
+ def getDocument(self):
+ return self.dom
+
+ def getFragment(self):
+ return base.TreeBuilder.getFragment(self).element
+
+ def insertText(self, data, parent=None):
+ data = data
+ if parent != self:
+ base.TreeBuilder.insertText(self, data, parent)
+ else:
+ # HACK: allow text nodes as children of the document node
+ if hasattr(self.dom, '_child_node_types'):
+ # pylint:disable=protected-access
+ if Node.TEXT_NODE not in self.dom._child_node_types:
+ self.dom._child_node_types = list(self.dom._child_node_types)
+ self.dom._child_node_types.append(Node.TEXT_NODE)
+ self.dom.appendChild(self.dom.createTextNode(data))
+
+ implementation = DomImplementation
+ name = None
+
+ def testSerializer(element):
+ element.normalize()
+ rv = []
+
+ def serializeElement(element, indent=0):
+ if element.nodeType == Node.DOCUMENT_TYPE_NODE:
+ if element.name:
+ if element.publicId or element.systemId:
+ publicId = element.publicId or ""
+ systemId = element.systemId or ""
+ rv.append("""|%s<!DOCTYPE %s "%s" "%s">""" %
+ (' ' * indent, element.name, publicId, systemId))
+ else:
+ rv.append("|%s<!DOCTYPE %s>" % (' ' * indent, element.name))
+ else:
+ rv.append("|%s<!DOCTYPE >" % (' ' * indent,))
+ elif element.nodeType == Node.DOCUMENT_NODE:
+ rv.append("#document")
+ elif element.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
+ rv.append("#document-fragment")
+ elif element.nodeType == Node.COMMENT_NODE:
+ rv.append("|%s<!-- %s -->" % (' ' * indent, element.nodeValue))
+ elif element.nodeType == Node.TEXT_NODE:
+ rv.append("|%s\"%s\"" % (' ' * indent, element.nodeValue))
+ else:
+ if (hasattr(element, "namespaceURI") and
+ element.namespaceURI is not None):
+ name = "%s %s" % (constants.prefixes[element.namespaceURI],
+ element.nodeName)
+ else:
+ name = element.nodeName
+ rv.append("|%s<%s>" % (' ' * indent, name))
+ if element.hasAttributes():
+ attributes = []
+ for i in range(len(element.attributes)):
+ attr = element.attributes.item(i)
+ name = attr.nodeName
+ value = attr.value
+ ns = attr.namespaceURI
+ if ns:
+ name = "%s %s" % (constants.prefixes[ns], attr.localName)
+ else:
+ name = attr.nodeName
+ attributes.append((name, value))
+
+ for name, value in sorted(attributes):
+ rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value))
+ indent += 2
+ for child in element.childNodes:
+ serializeElement(child, indent)
+ serializeElement(element, 0)
+
+ return "\n".join(rv)
+
+ return locals()
+
+
+# The actual means to get a module!
+getDomModule = moduleFactoryFactory(getDomBuilder)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/etree.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/etree.py
new file mode 100644
index 0000000000..086bed4eed
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/etree.py
@@ -0,0 +1,343 @@
+from __future__ import absolute_import, division, unicode_literals
+# pylint:disable=protected-access
+
+from six import text_type
+
+import re
+
+from copy import copy
+
+from . import base
+from .. import _ihatexml
+from .. import constants
+from ..constants import namespaces
+from .._utils import moduleFactoryFactory
+
+tag_regexp = re.compile("{([^}]*)}(.*)")
+
+
+def getETreeBuilder(ElementTreeImplementation, fullTree=False):
+ ElementTree = ElementTreeImplementation
+ ElementTreeCommentType = ElementTree.Comment("asd").tag
+
+ class Element(base.Node):
+ def __init__(self, name, namespace=None):
+ self._name = name
+ self._namespace = namespace
+ self._element = ElementTree.Element(self._getETreeTag(name,
+ namespace))
+ if namespace is None:
+ self.nameTuple = namespaces["html"], self._name
+ else:
+ self.nameTuple = self._namespace, self._name
+ self.parent = None
+ self._childNodes = []
+ self._flags = []
+
+ def _getETreeTag(self, name, namespace):
+ if namespace is None:
+ etree_tag = name
+ else:
+ etree_tag = "{%s}%s" % (namespace, name)
+ return etree_tag
+
+ def _setName(self, name):
+ self._name = name
+ self._element.tag = self._getETreeTag(self._name, self._namespace)
+
+ def _getName(self):
+ return self._name
+
+ name = property(_getName, _setName)
+
+ def _setNamespace(self, namespace):
+ self._namespace = namespace
+ self._element.tag = self._getETreeTag(self._name, self._namespace)
+
+ def _getNamespace(self):
+ return self._namespace
+
+ namespace = property(_getNamespace, _setNamespace)
+
+ def _getAttributes(self):
+ return self._element.attrib
+
+ def _setAttributes(self, attributes):
+ el_attrib = self._element.attrib
+ el_attrib.clear()
+ if attributes:
+ # calling .items _always_ allocates, and the above truthy check is cheaper than the
+ # allocation on average
+ for key, value in attributes.items():
+ if isinstance(key, tuple):
+ name = "{%s}%s" % (key[2], key[1])
+ else:
+ name = key
+ el_attrib[name] = value
+
+ attributes = property(_getAttributes, _setAttributes)
+
+ def _getChildNodes(self):
+ return self._childNodes
+
+ def _setChildNodes(self, value):
+ del self._element[:]
+ self._childNodes = []
+ for element in value:
+ self.insertChild(element)
+
+ childNodes = property(_getChildNodes, _setChildNodes)
+
+ def hasContent(self):
+ """Return true if the node has children or text"""
+ return bool(self._element.text or len(self._element))
+
+ def appendChild(self, node):
+ self._childNodes.append(node)
+ self._element.append(node._element)
+ node.parent = self
+
+ def insertBefore(self, node, refNode):
+ index = list(self._element).index(refNode._element)
+ self._element.insert(index, node._element)
+ node.parent = self
+
+ def removeChild(self, node):
+ self._childNodes.remove(node)
+ self._element.remove(node._element)
+ node.parent = None
+
+ def insertText(self, data, insertBefore=None):
+ if not(len(self._element)):
+ if not self._element.text:
+ self._element.text = ""
+ self._element.text += data
+ elif insertBefore is None:
+ # Insert the text as the tail of the last child element
+ if not self._element[-1].tail:
+ self._element[-1].tail = ""
+ self._element[-1].tail += data
+ else:
+ # Insert the text before the specified node
+ children = list(self._element)
+ index = children.index(insertBefore._element)
+ if index > 0:
+ if not self._element[index - 1].tail:
+ self._element[index - 1].tail = ""
+ self._element[index - 1].tail += data
+ else:
+ if not self._element.text:
+ self._element.text = ""
+ self._element.text += data
+
+ def cloneNode(self):
+ element = type(self)(self.name, self.namespace)
+ if self._element.attrib:
+ element._element.attrib = copy(self._element.attrib)
+ return element
+
+ def reparentChildren(self, newParent):
+ if newParent.childNodes:
+ newParent.childNodes[-1]._element.tail += self._element.text
+ else:
+ if not newParent._element.text:
+ newParent._element.text = ""
+ if self._element.text is not None:
+ newParent._element.text += self._element.text
+ self._element.text = ""
+ base.Node.reparentChildren(self, newParent)
+
+ class Comment(Element):
+ def __init__(self, data):
+ # Use the superclass constructor to set all properties on the
+ # wrapper element
+ self._element = ElementTree.Comment(data)
+ self.parent = None
+ self._childNodes = []
+ self._flags = []
+
+ def _getData(self):
+ return self._element.text
+
+ def _setData(self, value):
+ self._element.text = value
+
+ data = property(_getData, _setData)
+
+ class DocumentType(Element):
+ def __init__(self, name, publicId, systemId):
+ Element.__init__(self, "<!DOCTYPE>")
+ self._element.text = name
+ self.publicId = publicId
+ self.systemId = systemId
+
+ def _getPublicId(self):
+ return self._element.get("publicId", "")
+
+ def _setPublicId(self, value):
+ if value is not None:
+ self._element.set("publicId", value)
+
+ publicId = property(_getPublicId, _setPublicId)
+
+ def _getSystemId(self):
+ return self._element.get("systemId", "")
+
+ def _setSystemId(self, value):
+ if value is not None:
+ self._element.set("systemId", value)
+
+ systemId = property(_getSystemId, _setSystemId)
+
+ class Document(Element):
+ def __init__(self):
+ Element.__init__(self, "DOCUMENT_ROOT")
+
+ class DocumentFragment(Element):
+ def __init__(self):
+ Element.__init__(self, "DOCUMENT_FRAGMENT")
+
+ def testSerializer(element):
+ rv = []
+
+ def serializeElement(element, indent=0):
+ if not(hasattr(element, "tag")):
+ element = element.getroot()
+ if element.tag == "<!DOCTYPE>":
+ if element.get("publicId") or element.get("systemId"):
+ publicId = element.get("publicId") or ""
+ systemId = element.get("systemId") or ""
+ rv.append("""<!DOCTYPE %s "%s" "%s">""" %
+ (element.text, publicId, systemId))
+ else:
+ rv.append("<!DOCTYPE %s>" % (element.text,))
+ elif element.tag == "DOCUMENT_ROOT":
+ rv.append("#document")
+ if element.text is not None:
+ rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text))
+ if element.tail is not None:
+ raise TypeError("Document node cannot have tail")
+ if hasattr(element, "attrib") and len(element.attrib):
+ raise TypeError("Document node cannot have attributes")
+ elif element.tag == ElementTreeCommentType:
+ rv.append("|%s<!-- %s -->" % (' ' * indent, element.text))
+ else:
+ assert isinstance(element.tag, text_type), \
+ "Expected unicode, got %s, %s" % (type(element.tag), element.tag)
+ nsmatch = tag_regexp.match(element.tag)
+
+ if nsmatch is None:
+ name = element.tag
+ else:
+ ns, name = nsmatch.groups()
+ prefix = constants.prefixes[ns]
+ name = "%s %s" % (prefix, name)
+ rv.append("|%s<%s>" % (' ' * indent, name))
+
+ if hasattr(element, "attrib"):
+ attributes = []
+ for name, value in element.attrib.items():
+ nsmatch = tag_regexp.match(name)
+ if nsmatch is not None:
+ ns, name = nsmatch.groups()
+ prefix = constants.prefixes[ns]
+ attr_string = "%s %s" % (prefix, name)
+ else:
+ attr_string = name
+ attributes.append((attr_string, value))
+
+ for name, value in sorted(attributes):
+ rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value))
+ if element.text:
+ rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text))
+ indent += 2
+ for child in element:
+ serializeElement(child, indent)
+ if element.tail:
+ rv.append("|%s\"%s\"" % (' ' * (indent - 2), element.tail))
+ serializeElement(element, 0)
+
+ return "\n".join(rv)
+
+ def tostring(element): # pylint:disable=unused-variable
+ """Serialize an element and its child nodes to a string"""
+ rv = []
+ filter = _ihatexml.InfosetFilter()
+
+ def serializeElement(element):
+ if isinstance(element, ElementTree.ElementTree):
+ element = element.getroot()
+
+ if element.tag == "<!DOCTYPE>":
+ if element.get("publicId") or element.get("systemId"):
+ publicId = element.get("publicId") or ""
+ systemId = element.get("systemId") or ""
+ rv.append("""<!DOCTYPE %s PUBLIC "%s" "%s">""" %
+ (element.text, publicId, systemId))
+ else:
+ rv.append("<!DOCTYPE %s>" % (element.text,))
+ elif element.tag == "DOCUMENT_ROOT":
+ if element.text is not None:
+ rv.append(element.text)
+ if element.tail is not None:
+ raise TypeError("Document node cannot have tail")
+ if hasattr(element, "attrib") and len(element.attrib):
+ raise TypeError("Document node cannot have attributes")
+
+ for child in element:
+ serializeElement(child)
+
+ elif element.tag == ElementTreeCommentType:
+ rv.append("<!--%s-->" % (element.text,))
+ else:
+ # This is assumed to be an ordinary element
+ if not element.attrib:
+ rv.append("<%s>" % (filter.fromXmlName(element.tag),))
+ else:
+ attr = " ".join(["%s=\"%s\"" % (
+ filter.fromXmlName(name), value)
+ for name, value in element.attrib.items()])
+ rv.append("<%s %s>" % (element.tag, attr))
+ if element.text:
+ rv.append(element.text)
+
+ for child in element:
+ serializeElement(child)
+
+ rv.append("</%s>" % (element.tag,))
+
+ if element.tail:
+ rv.append(element.tail)
+
+ serializeElement(element)
+
+ return "".join(rv)
+
+ class TreeBuilder(base.TreeBuilder): # pylint:disable=unused-variable
+ documentClass = Document
+ doctypeClass = DocumentType
+ elementClass = Element
+ commentClass = Comment
+ fragmentClass = DocumentFragment
+ implementation = ElementTreeImplementation
+
+ def testSerializer(self, element):
+ return testSerializer(element)
+
+ def getDocument(self):
+ if fullTree:
+ return self.document._element
+ else:
+ if self.defaultNamespace is not None:
+ return self.document._element.find(
+ "{%s}html" % self.defaultNamespace)
+ else:
+ return self.document._element.find("html")
+
+ def getFragment(self):
+ return base.TreeBuilder.getFragment(self)._element
+
+ return locals()
+
+
+getETreeModule = moduleFactoryFactory(getETreeBuilder)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/etree_lxml.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/etree_lxml.py
new file mode 100644
index 0000000000..e73de61a85
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treebuilders/etree_lxml.py
@@ -0,0 +1,392 @@
+"""Module for supporting the lxml.etree library. The idea here is to use as much
+of the native library as possible, without using fragile hacks like custom element
+names that break between releases. The downside of this is that we cannot represent
+all possible trees; specifically the following are known to cause problems:
+
+Text or comments as siblings of the root element
+Docypes with no name
+
+When any of these things occur, we emit a DataLossWarning
+"""
+
+from __future__ import absolute_import, division, unicode_literals
+# pylint:disable=protected-access
+
+import warnings
+import re
+import sys
+
+try:
+ from collections.abc import MutableMapping
+except ImportError:
+ from collections import MutableMapping
+
+from . import base
+from ..constants import DataLossWarning
+from .. import constants
+from . import etree as etree_builders
+from .. import _ihatexml
+
+import lxml.etree as etree
+from six import PY3, binary_type
+
+
+fullTree = True
+tag_regexp = re.compile("{([^}]*)}(.*)")
+
+comment_type = etree.Comment("asd").tag
+
+
+class DocumentType(object):
+ def __init__(self, name, publicId, systemId):
+ self.name = name
+ self.publicId = publicId
+ self.systemId = systemId
+
+
+class Document(object):
+ def __init__(self):
+ self._elementTree = None
+ self._childNodes = []
+
+ def appendChild(self, element):
+ last = self._elementTree.getroot()
+ for last in self._elementTree.getroot().itersiblings():
+ pass
+
+ last.addnext(element._element)
+
+ def _getChildNodes(self):
+ return self._childNodes
+
+ childNodes = property(_getChildNodes)
+
+
+def testSerializer(element):
+ rv = []
+ infosetFilter = _ihatexml.InfosetFilter(preventDoubleDashComments=True)
+
+ def serializeElement(element, indent=0):
+ if not hasattr(element, "tag"):
+ if hasattr(element, "getroot"):
+ # Full tree case
+ rv.append("#document")
+ if element.docinfo.internalDTD:
+ if not (element.docinfo.public_id or
+ element.docinfo.system_url):
+ dtd_str = "<!DOCTYPE %s>" % element.docinfo.root_name
+ else:
+ dtd_str = """<!DOCTYPE %s "%s" "%s">""" % (
+ element.docinfo.root_name,
+ element.docinfo.public_id,
+ element.docinfo.system_url)
+ rv.append("|%s%s" % (' ' * (indent + 2), dtd_str))
+ next_element = element.getroot()
+ while next_element.getprevious() is not None:
+ next_element = next_element.getprevious()
+ while next_element is not None:
+ serializeElement(next_element, indent + 2)
+ next_element = next_element.getnext()
+ elif isinstance(element, str) or isinstance(element, bytes):
+ # Text in a fragment
+ assert isinstance(element, str) or sys.version_info[0] == 2
+ rv.append("|%s\"%s\"" % (' ' * indent, element))
+ else:
+ # Fragment case
+ rv.append("#document-fragment")
+ for next_element in element:
+ serializeElement(next_element, indent + 2)
+ elif element.tag == comment_type:
+ rv.append("|%s<!-- %s -->" % (' ' * indent, element.text))
+ if hasattr(element, "tail") and element.tail:
+ rv.append("|%s\"%s\"" % (' ' * indent, element.tail))
+ else:
+ assert isinstance(element, etree._Element)
+ nsmatch = etree_builders.tag_regexp.match(element.tag)
+ if nsmatch is not None:
+ ns = nsmatch.group(1)
+ tag = nsmatch.group(2)
+ prefix = constants.prefixes[ns]
+ rv.append("|%s<%s %s>" % (' ' * indent, prefix,
+ infosetFilter.fromXmlName(tag)))
+ else:
+ rv.append("|%s<%s>" % (' ' * indent,
+ infosetFilter.fromXmlName(element.tag)))
+
+ if hasattr(element, "attrib"):
+ attributes = []
+ for name, value in element.attrib.items():
+ nsmatch = tag_regexp.match(name)
+ if nsmatch is not None:
+ ns, name = nsmatch.groups()
+ name = infosetFilter.fromXmlName(name)
+ prefix = constants.prefixes[ns]
+ attr_string = "%s %s" % (prefix, name)
+ else:
+ attr_string = infosetFilter.fromXmlName(name)
+ attributes.append((attr_string, value))
+
+ for name, value in sorted(attributes):
+ rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value))
+
+ if element.text:
+ rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text))
+ indent += 2
+ for child in element:
+ serializeElement(child, indent)
+ if hasattr(element, "tail") and element.tail:
+ rv.append("|%s\"%s\"" % (' ' * (indent - 2), element.tail))
+ serializeElement(element, 0)
+
+ return "\n".join(rv)
+
+
+def tostring(element):
+ """Serialize an element and its child nodes to a string"""
+ rv = []
+
+ def serializeElement(element):
+ if not hasattr(element, "tag"):
+ if element.docinfo.internalDTD:
+ if element.docinfo.doctype:
+ dtd_str = element.docinfo.doctype
+ else:
+ dtd_str = "<!DOCTYPE %s>" % element.docinfo.root_name
+ rv.append(dtd_str)
+ serializeElement(element.getroot())
+
+ elif element.tag == comment_type:
+ rv.append("<!--%s-->" % (element.text,))
+
+ else:
+ # This is assumed to be an ordinary element
+ if not element.attrib:
+ rv.append("<%s>" % (element.tag,))
+ else:
+ attr = " ".join(["%s=\"%s\"" % (name, value)
+ for name, value in element.attrib.items()])
+ rv.append("<%s %s>" % (element.tag, attr))
+ if element.text:
+ rv.append(element.text)
+
+ for child in element:
+ serializeElement(child)
+
+ rv.append("</%s>" % (element.tag,))
+
+ if hasattr(element, "tail") and element.tail:
+ rv.append(element.tail)
+
+ serializeElement(element)
+
+ return "".join(rv)
+
+
+class TreeBuilder(base.TreeBuilder):
+ documentClass = Document
+ doctypeClass = DocumentType
+ elementClass = None
+ commentClass = None
+ fragmentClass = Document
+ implementation = etree
+
+ def __init__(self, namespaceHTMLElements, fullTree=False):
+ builder = etree_builders.getETreeModule(etree, fullTree=fullTree)
+ infosetFilter = self.infosetFilter = _ihatexml.InfosetFilter(preventDoubleDashComments=True)
+ self.namespaceHTMLElements = namespaceHTMLElements
+
+ class Attributes(MutableMapping):
+ def __init__(self, element):
+ self._element = element
+
+ def _coerceKey(self, key):
+ if isinstance(key, tuple):
+ name = "{%s}%s" % (key[2], infosetFilter.coerceAttribute(key[1]))
+ else:
+ name = infosetFilter.coerceAttribute(key)
+ return name
+
+ def __getitem__(self, key):
+ value = self._element._element.attrib[self._coerceKey(key)]
+ if not PY3 and isinstance(value, binary_type):
+ value = value.decode("ascii")
+ return value
+
+ def __setitem__(self, key, value):
+ self._element._element.attrib[self._coerceKey(key)] = value
+
+ def __delitem__(self, key):
+ del self._element._element.attrib[self._coerceKey(key)]
+
+ def __iter__(self):
+ return iter(self._element._element.attrib)
+
+ def __len__(self):
+ return len(self._element._element.attrib)
+
+ def clear(self):
+ return self._element._element.attrib.clear()
+
+ class Element(builder.Element):
+ def __init__(self, name, namespace):
+ name = infosetFilter.coerceElement(name)
+ builder.Element.__init__(self, name, namespace=namespace)
+ self._attributes = Attributes(self)
+
+ def _setName(self, name):
+ self._name = infosetFilter.coerceElement(name)
+ self._element.tag = self._getETreeTag(
+ self._name, self._namespace)
+
+ def _getName(self):
+ return infosetFilter.fromXmlName(self._name)
+
+ name = property(_getName, _setName)
+
+ def _getAttributes(self):
+ return self._attributes
+
+ def _setAttributes(self, value):
+ attributes = self.attributes
+ attributes.clear()
+ attributes.update(value)
+
+ attributes = property(_getAttributes, _setAttributes)
+
+ def insertText(self, data, insertBefore=None):
+ data = infosetFilter.coerceCharacters(data)
+ builder.Element.insertText(self, data, insertBefore)
+
+ def cloneNode(self):
+ element = type(self)(self.name, self.namespace)
+ if self._element.attrib:
+ element._element.attrib.update(self._element.attrib)
+ return element
+
+ class Comment(builder.Comment):
+ def __init__(self, data):
+ data = infosetFilter.coerceComment(data)
+ builder.Comment.__init__(self, data)
+
+ def _setData(self, data):
+ data = infosetFilter.coerceComment(data)
+ self._element.text = data
+
+ def _getData(self):
+ return self._element.text
+
+ data = property(_getData, _setData)
+
+ self.elementClass = Element
+ self.commentClass = Comment
+ # self.fragmentClass = builder.DocumentFragment
+ base.TreeBuilder.__init__(self, namespaceHTMLElements)
+
+ def reset(self):
+ base.TreeBuilder.reset(self)
+ self.insertComment = self.insertCommentInitial
+ self.initial_comments = []
+ self.doctype = None
+
+ def testSerializer(self, element):
+ return testSerializer(element)
+
+ def getDocument(self):
+ if fullTree:
+ return self.document._elementTree
+ else:
+ return self.document._elementTree.getroot()
+
+ def getFragment(self):
+ fragment = []
+ element = self.openElements[0]._element
+ if element.text:
+ fragment.append(element.text)
+ fragment.extend(list(element))
+ if element.tail:
+ fragment.append(element.tail)
+ return fragment
+
+ def insertDoctype(self, token):
+ name = token["name"]
+ publicId = token["publicId"]
+ systemId = token["systemId"]
+
+ if not name:
+ warnings.warn("lxml cannot represent empty doctype", DataLossWarning)
+ self.doctype = None
+ else:
+ coercedName = self.infosetFilter.coerceElement(name)
+ if coercedName != name:
+ warnings.warn("lxml cannot represent non-xml doctype", DataLossWarning)
+
+ doctype = self.doctypeClass(coercedName, publicId, systemId)
+ self.doctype = doctype
+
+ def insertCommentInitial(self, data, parent=None):
+ assert parent is None or parent is self.document
+ assert self.document._elementTree is None
+ self.initial_comments.append(data)
+
+ def insertCommentMain(self, data, parent=None):
+ if (parent == self.document and
+ self.document._elementTree.getroot()[-1].tag == comment_type):
+ warnings.warn("lxml cannot represent adjacent comments beyond the root elements", DataLossWarning)
+ super(TreeBuilder, self).insertComment(data, parent)
+
+ def insertRoot(self, token):
+ # Because of the way libxml2 works, it doesn't seem to be possible to
+ # alter information like the doctype after the tree has been parsed.
+ # Therefore we need to use the built-in parser to create our initial
+ # tree, after which we can add elements like normal
+ docStr = ""
+ if self.doctype:
+ assert self.doctype.name
+ docStr += "<!DOCTYPE %s" % self.doctype.name
+ if (self.doctype.publicId is not None or
+ self.doctype.systemId is not None):
+ docStr += (' PUBLIC "%s" ' %
+ (self.infosetFilter.coercePubid(self.doctype.publicId or "")))
+ if self.doctype.systemId:
+ sysid = self.doctype.systemId
+ if sysid.find("'") >= 0 and sysid.find('"') >= 0:
+ warnings.warn("DOCTYPE system cannot contain single and double quotes", DataLossWarning)
+ sysid = sysid.replace("'", 'U00027')
+ if sysid.find("'") >= 0:
+ docStr += '"%s"' % sysid
+ else:
+ docStr += "'%s'" % sysid
+ else:
+ docStr += "''"
+ docStr += ">"
+ if self.doctype.name != token["name"]:
+ warnings.warn("lxml cannot represent doctype with a different name to the root element", DataLossWarning)
+ docStr += "<THIS_SHOULD_NEVER_APPEAR_PUBLICLY/>"
+ root = etree.fromstring(docStr)
+
+ # Append the initial comments:
+ for comment_token in self.initial_comments:
+ comment = self.commentClass(comment_token["data"])
+ root.addprevious(comment._element)
+
+ # Create the root document and add the ElementTree to it
+ self.document = self.documentClass()
+ self.document._elementTree = root.getroottree()
+
+ # Give the root element the right name
+ name = token["name"]
+ namespace = token.get("namespace", self.defaultNamespace)
+ if namespace is None:
+ etree_tag = name
+ else:
+ etree_tag = "{%s}%s" % (namespace, name)
+ root.tag = etree_tag
+
+ # Add the root element to the internal child/open data structures
+ root_element = self.elementClass(name, namespace)
+ root_element._element = root
+ self.document._childNodes.append(root_element)
+ self.openElements.append(root_element)
+
+ # Reset to the default insert comment function
+ self.insertComment = self.insertCommentMain
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/__init__.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/__init__.py
new file mode 100644
index 0000000000..b2d3aac313
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/__init__.py
@@ -0,0 +1,154 @@
+"""A collection of modules for iterating through different kinds of
+tree, generating tokens identical to those produced by the tokenizer
+module.
+
+To create a tree walker for a new type of tree, you need to
+implement a tree walker object (called TreeWalker by convention) that
+implements a 'serialize' method which takes a tree as sole argument and
+returns an iterator which generates tokens.
+"""
+
+from __future__ import absolute_import, division, unicode_literals
+
+from .. import constants
+from .._utils import default_etree
+
+__all__ = ["getTreeWalker", "pprint"]
+
+treeWalkerCache = {}
+
+
+def getTreeWalker(treeType, implementation=None, **kwargs):
+ """Get a TreeWalker class for various types of tree with built-in support
+
+ :arg str treeType: the name of the tree type required (case-insensitive).
+ Supported values are:
+
+ * "dom": The xml.dom.minidom DOM implementation
+ * "etree": A generic walker for tree implementations exposing an
+ elementtree-like interface (known to work with ElementTree,
+ cElementTree and lxml.etree).
+ * "lxml": Optimized walker for lxml.etree
+ * "genshi": a Genshi stream
+
+ :arg implementation: A module implementing the tree type e.g.
+ xml.etree.ElementTree or cElementTree (Currently applies to the "etree"
+ tree type only).
+
+ :arg kwargs: keyword arguments passed to the etree walker--for other
+ walkers, this has no effect
+
+ :returns: a TreeWalker class
+
+ """
+
+ treeType = treeType.lower()
+ if treeType not in treeWalkerCache:
+ if treeType == "dom":
+ from . import dom
+ treeWalkerCache[treeType] = dom.TreeWalker
+ elif treeType == "genshi":
+ from . import genshi
+ treeWalkerCache[treeType] = genshi.TreeWalker
+ elif treeType == "lxml":
+ from . import etree_lxml
+ treeWalkerCache[treeType] = etree_lxml.TreeWalker
+ elif treeType == "etree":
+ from . import etree
+ if implementation is None:
+ implementation = default_etree
+ # XXX: NEVER cache here, caching is done in the etree submodule
+ return etree.getETreeModule(implementation, **kwargs).TreeWalker
+ return treeWalkerCache.get(treeType)
+
+
+def concatenateCharacterTokens(tokens):
+ pendingCharacters = []
+ for token in tokens:
+ type = token["type"]
+ if type in ("Characters", "SpaceCharacters"):
+ pendingCharacters.append(token["data"])
+ else:
+ if pendingCharacters:
+ yield {"type": "Characters", "data": "".join(pendingCharacters)}
+ pendingCharacters = []
+ yield token
+ if pendingCharacters:
+ yield {"type": "Characters", "data": "".join(pendingCharacters)}
+
+
+def pprint(walker):
+ """Pretty printer for tree walkers
+
+ Takes a TreeWalker instance and pretty prints the output of walking the tree.
+
+ :arg walker: a TreeWalker instance
+
+ """
+ output = []
+ indent = 0
+ for token in concatenateCharacterTokens(walker):
+ type = token["type"]
+ if type in ("StartTag", "EmptyTag"):
+ # tag name
+ if token["namespace"] and token["namespace"] != constants.namespaces["html"]:
+ if token["namespace"] in constants.prefixes:
+ ns = constants.prefixes[token["namespace"]]
+ else:
+ ns = token["namespace"]
+ name = "%s %s" % (ns, token["name"])
+ else:
+ name = token["name"]
+ output.append("%s<%s>" % (" " * indent, name))
+ indent += 2
+ # attributes (sorted for consistent ordering)
+ attrs = token["data"]
+ for (namespace, localname), value in sorted(attrs.items()):
+ if namespace:
+ if namespace in constants.prefixes:
+ ns = constants.prefixes[namespace]
+ else:
+ ns = namespace
+ name = "%s %s" % (ns, localname)
+ else:
+ name = localname
+ output.append("%s%s=\"%s\"" % (" " * indent, name, value))
+ # self-closing
+ if type == "EmptyTag":
+ indent -= 2
+
+ elif type == "EndTag":
+ indent -= 2
+
+ elif type == "Comment":
+ output.append("%s<!-- %s -->" % (" " * indent, token["data"]))
+
+ elif type == "Doctype":
+ if token["name"]:
+ if token["publicId"]:
+ output.append("""%s<!DOCTYPE %s "%s" "%s">""" %
+ (" " * indent,
+ token["name"],
+ token["publicId"],
+ token["systemId"] if token["systemId"] else ""))
+ elif token["systemId"]:
+ output.append("""%s<!DOCTYPE %s "" "%s">""" %
+ (" " * indent,
+ token["name"],
+ token["systemId"]))
+ else:
+ output.append("%s<!DOCTYPE %s>" % (" " * indent,
+ token["name"]))
+ else:
+ output.append("%s<!DOCTYPE >" % (" " * indent,))
+
+ elif type == "Characters":
+ output.append("%s\"%s\"" % (" " * indent, token["data"]))
+
+ elif type == "SpaceCharacters":
+ assert False, "concatenateCharacterTokens should have got rid of all Space tokens"
+
+ else:
+ raise ValueError("Unknown token type, %s" % type)
+
+ return "\n".join(output)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/base.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/base.py
new file mode 100644
index 0000000000..80c474c4e9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/base.py
@@ -0,0 +1,252 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from xml.dom import Node
+from ..constants import namespaces, voidElements, spaceCharacters
+
+__all__ = ["DOCUMENT", "DOCTYPE", "TEXT", "ELEMENT", "COMMENT", "ENTITY", "UNKNOWN",
+ "TreeWalker", "NonRecursiveTreeWalker"]
+
+DOCUMENT = Node.DOCUMENT_NODE
+DOCTYPE = Node.DOCUMENT_TYPE_NODE
+TEXT = Node.TEXT_NODE
+ELEMENT = Node.ELEMENT_NODE
+COMMENT = Node.COMMENT_NODE
+ENTITY = Node.ENTITY_NODE
+UNKNOWN = "<#UNKNOWN#>"
+
+spaceCharacters = "".join(spaceCharacters)
+
+
+class TreeWalker(object):
+ """Walks a tree yielding tokens
+
+ Tokens are dicts that all have a ``type`` field specifying the type of the
+ token.
+
+ """
+ def __init__(self, tree):
+ """Creates a TreeWalker
+
+ :arg tree: the tree to walk
+
+ """
+ self.tree = tree
+
+ def __iter__(self):
+ raise NotImplementedError
+
+ def error(self, msg):
+ """Generates an error token with the given message
+
+ :arg msg: the error message
+
+ :returns: SerializeError token
+
+ """
+ return {"type": "SerializeError", "data": msg}
+
+ def emptyTag(self, namespace, name, attrs, hasChildren=False):
+ """Generates an EmptyTag token
+
+ :arg namespace: the namespace of the token--can be ``None``
+
+ :arg name: the name of the element
+
+ :arg attrs: the attributes of the element as a dict
+
+ :arg hasChildren: whether or not to yield a SerializationError because
+ this tag shouldn't have children
+
+ :returns: EmptyTag token
+
+ """
+ yield {"type": "EmptyTag", "name": name,
+ "namespace": namespace,
+ "data": attrs}
+ if hasChildren:
+ yield self.error("Void element has children")
+
+ def startTag(self, namespace, name, attrs):
+ """Generates a StartTag token
+
+ :arg namespace: the namespace of the token--can be ``None``
+
+ :arg name: the name of the element
+
+ :arg attrs: the attributes of the element as a dict
+
+ :returns: StartTag token
+
+ """
+ return {"type": "StartTag",
+ "name": name,
+ "namespace": namespace,
+ "data": attrs}
+
+ def endTag(self, namespace, name):
+ """Generates an EndTag token
+
+ :arg namespace: the namespace of the token--can be ``None``
+
+ :arg name: the name of the element
+
+ :returns: EndTag token
+
+ """
+ return {"type": "EndTag",
+ "name": name,
+ "namespace": namespace}
+
+ def text(self, data):
+ """Generates SpaceCharacters and Characters tokens
+
+ Depending on what's in the data, this generates one or more
+ ``SpaceCharacters`` and ``Characters`` tokens.
+
+ For example:
+
+ >>> from html5lib.treewalkers.base import TreeWalker
+ >>> # Give it an empty tree just so it instantiates
+ >>> walker = TreeWalker([])
+ >>> list(walker.text(''))
+ []
+ >>> list(walker.text(' '))
+ [{u'data': ' ', u'type': u'SpaceCharacters'}]
+ >>> list(walker.text(' abc ')) # doctest: +NORMALIZE_WHITESPACE
+ [{u'data': ' ', u'type': u'SpaceCharacters'},
+ {u'data': u'abc', u'type': u'Characters'},
+ {u'data': u' ', u'type': u'SpaceCharacters'}]
+
+ :arg data: the text data
+
+ :returns: one or more ``SpaceCharacters`` and ``Characters`` tokens
+
+ """
+ data = data
+ middle = data.lstrip(spaceCharacters)
+ left = data[:len(data) - len(middle)]
+ if left:
+ yield {"type": "SpaceCharacters", "data": left}
+ data = middle
+ middle = data.rstrip(spaceCharacters)
+ right = data[len(middle):]
+ if middle:
+ yield {"type": "Characters", "data": middle}
+ if right:
+ yield {"type": "SpaceCharacters", "data": right}
+
+ def comment(self, data):
+ """Generates a Comment token
+
+ :arg data: the comment
+
+ :returns: Comment token
+
+ """
+ return {"type": "Comment", "data": data}
+
+ def doctype(self, name, publicId=None, systemId=None):
+ """Generates a Doctype token
+
+ :arg name:
+
+ :arg publicId:
+
+ :arg systemId:
+
+ :returns: the Doctype token
+
+ """
+ return {"type": "Doctype",
+ "name": name,
+ "publicId": publicId,
+ "systemId": systemId}
+
+ def entity(self, name):
+ """Generates an Entity token
+
+ :arg name: the entity name
+
+ :returns: an Entity token
+
+ """
+ return {"type": "Entity", "name": name}
+
+ def unknown(self, nodeType):
+ """Handles unknown node types"""
+ return self.error("Unknown node type: " + nodeType)
+
+
+class NonRecursiveTreeWalker(TreeWalker):
+ def getNodeDetails(self, node):
+ raise NotImplementedError
+
+ def getFirstChild(self, node):
+ raise NotImplementedError
+
+ def getNextSibling(self, node):
+ raise NotImplementedError
+
+ def getParentNode(self, node):
+ raise NotImplementedError
+
+ def __iter__(self):
+ currentNode = self.tree
+ while currentNode is not None:
+ details = self.getNodeDetails(currentNode)
+ type, details = details[0], details[1:]
+ hasChildren = False
+
+ if type == DOCTYPE:
+ yield self.doctype(*details)
+
+ elif type == TEXT:
+ for token in self.text(*details):
+ yield token
+
+ elif type == ELEMENT:
+ namespace, name, attributes, hasChildren = details
+ if (not namespace or namespace == namespaces["html"]) and name in voidElements:
+ for token in self.emptyTag(namespace, name, attributes,
+ hasChildren):
+ yield token
+ hasChildren = False
+ else:
+ yield self.startTag(namespace, name, attributes)
+
+ elif type == COMMENT:
+ yield self.comment(details[0])
+
+ elif type == ENTITY:
+ yield self.entity(details[0])
+
+ elif type == DOCUMENT:
+ hasChildren = True
+
+ else:
+ yield self.unknown(details[0])
+
+ if hasChildren:
+ firstChild = self.getFirstChild(currentNode)
+ else:
+ firstChild = None
+
+ if firstChild is not None:
+ currentNode = firstChild
+ else:
+ while currentNode is not None:
+ details = self.getNodeDetails(currentNode)
+ type, details = details[0], details[1:]
+ if type == ELEMENT:
+ namespace, name, attributes, hasChildren = details
+ if (namespace and namespace != namespaces["html"]) or name not in voidElements:
+ yield self.endTag(namespace, name)
+ if self.tree is currentNode:
+ currentNode = None
+ break
+ nextSibling = self.getNextSibling(currentNode)
+ if nextSibling is not None:
+ currentNode = nextSibling
+ break
+ else:
+ currentNode = self.getParentNode(currentNode)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/dom.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/dom.py
new file mode 100644
index 0000000000..b0c89b001f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/dom.py
@@ -0,0 +1,43 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from xml.dom import Node
+
+from . import base
+
+
+class TreeWalker(base.NonRecursiveTreeWalker):
+ def getNodeDetails(self, node):
+ if node.nodeType == Node.DOCUMENT_TYPE_NODE:
+ return base.DOCTYPE, node.name, node.publicId, node.systemId
+
+ elif node.nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE):
+ return base.TEXT, node.nodeValue
+
+ elif node.nodeType == Node.ELEMENT_NODE:
+ attrs = {}
+ for attr in list(node.attributes.keys()):
+ attr = node.getAttributeNode(attr)
+ if attr.namespaceURI:
+ attrs[(attr.namespaceURI, attr.localName)] = attr.value
+ else:
+ attrs[(None, attr.name)] = attr.value
+ return (base.ELEMENT, node.namespaceURI, node.nodeName,
+ attrs, node.hasChildNodes())
+
+ elif node.nodeType == Node.COMMENT_NODE:
+ return base.COMMENT, node.nodeValue
+
+ elif node.nodeType in (Node.DOCUMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE):
+ return (base.DOCUMENT,)
+
+ else:
+ return base.UNKNOWN, node.nodeType
+
+ def getFirstChild(self, node):
+ return node.firstChild
+
+ def getNextSibling(self, node):
+ return node.nextSibling
+
+ def getParentNode(self, node):
+ return node.parentNode
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/etree.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/etree.py
new file mode 100644
index 0000000000..44653372d6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/etree.py
@@ -0,0 +1,131 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from collections import OrderedDict
+import re
+
+from six import string_types
+
+from . import base
+from .._utils import moduleFactoryFactory
+
+tag_regexp = re.compile("{([^}]*)}(.*)")
+
+
+def getETreeBuilder(ElementTreeImplementation):
+ ElementTree = ElementTreeImplementation
+ ElementTreeCommentType = ElementTree.Comment("asd").tag
+
+ class TreeWalker(base.NonRecursiveTreeWalker): # pylint:disable=unused-variable
+ """Given the particular ElementTree representation, this implementation,
+ to avoid using recursion, returns "nodes" as tuples with the following
+ content:
+
+ 1. The current element
+
+ 2. The index of the element relative to its parent
+
+ 3. A stack of ancestor elements
+
+ 4. A flag "text", "tail" or None to indicate if the current node is a
+ text node; either the text or tail of the current element (1)
+ """
+ def getNodeDetails(self, node):
+ if isinstance(node, tuple): # It might be the root Element
+ elt, _, _, flag = node
+ if flag in ("text", "tail"):
+ return base.TEXT, getattr(elt, flag)
+ else:
+ node = elt
+
+ if not(hasattr(node, "tag")):
+ node = node.getroot()
+
+ if node.tag in ("DOCUMENT_ROOT", "DOCUMENT_FRAGMENT"):
+ return (base.DOCUMENT,)
+
+ elif node.tag == "<!DOCTYPE>":
+ return (base.DOCTYPE, node.text,
+ node.get("publicId"), node.get("systemId"))
+
+ elif node.tag == ElementTreeCommentType:
+ return base.COMMENT, node.text
+
+ else:
+ assert isinstance(node.tag, string_types), type(node.tag)
+ # This is assumed to be an ordinary element
+ match = tag_regexp.match(node.tag)
+ if match:
+ namespace, tag = match.groups()
+ else:
+ namespace = None
+ tag = node.tag
+ attrs = OrderedDict()
+ for name, value in list(node.attrib.items()):
+ match = tag_regexp.match(name)
+ if match:
+ attrs[(match.group(1), match.group(2))] = value
+ else:
+ attrs[(None, name)] = value
+ return (base.ELEMENT, namespace, tag,
+ attrs, len(node) or node.text)
+
+ def getFirstChild(self, node):
+ if isinstance(node, tuple):
+ element, key, parents, flag = node
+ else:
+ element, key, parents, flag = node, None, [], None
+
+ if flag in ("text", "tail"):
+ return None
+ else:
+ if element.text:
+ return element, key, parents, "text"
+ elif len(element):
+ parents.append(element)
+ return element[0], 0, parents, None
+ else:
+ return None
+
+ def getNextSibling(self, node):
+ if isinstance(node, tuple):
+ element, key, parents, flag = node
+ else:
+ return None
+
+ if flag == "text":
+ if len(element):
+ parents.append(element)
+ return element[0], 0, parents, None
+ else:
+ return None
+ else:
+ if element.tail and flag != "tail":
+ return element, key, parents, "tail"
+ elif key < len(parents[-1]) - 1:
+ return parents[-1][key + 1], key + 1, parents, None
+ else:
+ return None
+
+ def getParentNode(self, node):
+ if isinstance(node, tuple):
+ element, key, parents, flag = node
+ else:
+ return None
+
+ if flag == "text":
+ if not parents:
+ return element
+ else:
+ return element, key, parents, None
+ else:
+ parent = parents.pop()
+ if not parents:
+ return parent
+ else:
+ assert list(parents[-1]).count(parent) == 1
+ return parent, list(parents[-1]).index(parent), parents, None
+
+ return locals()
+
+
+getETreeModule = moduleFactoryFactory(getETreeBuilder)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/etree_lxml.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/etree_lxml.py
new file mode 100644
index 0000000000..a614ac5b3f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/etree_lxml.py
@@ -0,0 +1,215 @@
+from __future__ import absolute_import, division, unicode_literals
+from six import text_type
+
+from collections import OrderedDict
+
+from lxml import etree
+from ..treebuilders.etree import tag_regexp
+
+from . import base
+
+from .. import _ihatexml
+
+
+def ensure_str(s):
+ if s is None:
+ return None
+ elif isinstance(s, text_type):
+ return s
+ else:
+ return s.decode("ascii", "strict")
+
+
+class Root(object):
+ def __init__(self, et):
+ self.elementtree = et
+ self.children = []
+
+ try:
+ if et.docinfo.internalDTD:
+ self.children.append(Doctype(self,
+ ensure_str(et.docinfo.root_name),
+ ensure_str(et.docinfo.public_id),
+ ensure_str(et.docinfo.system_url)))
+ except AttributeError:
+ pass
+
+ try:
+ node = et.getroot()
+ except AttributeError:
+ node = et
+
+ while node.getprevious() is not None:
+ node = node.getprevious()
+ while node is not None:
+ self.children.append(node)
+ node = node.getnext()
+
+ self.text = None
+ self.tail = None
+
+ def __getitem__(self, key):
+ return self.children[key]
+
+ def getnext(self):
+ return None
+
+ def __len__(self):
+ return 1
+
+
+class Doctype(object):
+ def __init__(self, root_node, name, public_id, system_id):
+ self.root_node = root_node
+ self.name = name
+ self.public_id = public_id
+ self.system_id = system_id
+
+ self.text = None
+ self.tail = None
+
+ def getnext(self):
+ return self.root_node.children[1]
+
+
+class FragmentRoot(Root):
+ def __init__(self, children):
+ self.children = [FragmentWrapper(self, child) for child in children]
+ self.text = self.tail = None
+
+ def getnext(self):
+ return None
+
+
+class FragmentWrapper(object):
+ def __init__(self, fragment_root, obj):
+ self.root_node = fragment_root
+ self.obj = obj
+ if hasattr(self.obj, 'text'):
+ self.text = ensure_str(self.obj.text)
+ else:
+ self.text = None
+ if hasattr(self.obj, 'tail'):
+ self.tail = ensure_str(self.obj.tail)
+ else:
+ self.tail = None
+
+ def __getattr__(self, name):
+ return getattr(self.obj, name)
+
+ def getnext(self):
+ siblings = self.root_node.children
+ idx = siblings.index(self)
+ if idx < len(siblings) - 1:
+ return siblings[idx + 1]
+ else:
+ return None
+
+ def __getitem__(self, key):
+ return self.obj[key]
+
+ def __bool__(self):
+ return bool(self.obj)
+
+ def getparent(self):
+ return None
+
+ def __str__(self):
+ return str(self.obj)
+
+ def __unicode__(self):
+ return str(self.obj)
+
+ def __len__(self):
+ return len(self.obj)
+
+
+class TreeWalker(base.NonRecursiveTreeWalker):
+ def __init__(self, tree):
+ # pylint:disable=redefined-variable-type
+ if isinstance(tree, list):
+ self.fragmentChildren = set(tree)
+ tree = FragmentRoot(tree)
+ else:
+ self.fragmentChildren = set()
+ tree = Root(tree)
+ base.NonRecursiveTreeWalker.__init__(self, tree)
+ self.filter = _ihatexml.InfosetFilter()
+
+ def getNodeDetails(self, node):
+ if isinstance(node, tuple): # Text node
+ node, key = node
+ assert key in ("text", "tail"), "Text nodes are text or tail, found %s" % key
+ return base.TEXT, ensure_str(getattr(node, key))
+
+ elif isinstance(node, Root):
+ return (base.DOCUMENT,)
+
+ elif isinstance(node, Doctype):
+ return base.DOCTYPE, node.name, node.public_id, node.system_id
+
+ elif isinstance(node, FragmentWrapper) and not hasattr(node, "tag"):
+ return base.TEXT, ensure_str(node.obj)
+
+ elif node.tag == etree.Comment:
+ return base.COMMENT, ensure_str(node.text)
+
+ elif node.tag == etree.Entity:
+ return base.ENTITY, ensure_str(node.text)[1:-1] # strip &;
+
+ else:
+ # This is assumed to be an ordinary element
+ match = tag_regexp.match(ensure_str(node.tag))
+ if match:
+ namespace, tag = match.groups()
+ else:
+ namespace = None
+ tag = ensure_str(node.tag)
+ attrs = OrderedDict()
+ for name, value in list(node.attrib.items()):
+ name = ensure_str(name)
+ value = ensure_str(value)
+ match = tag_regexp.match(name)
+ if match:
+ attrs[(match.group(1), match.group(2))] = value
+ else:
+ attrs[(None, name)] = value
+ return (base.ELEMENT, namespace, self.filter.fromXmlName(tag),
+ attrs, len(node) > 0 or node.text)
+
+ def getFirstChild(self, node):
+ assert not isinstance(node, tuple), "Text nodes have no children"
+
+ assert len(node) or node.text, "Node has no children"
+ if node.text:
+ return (node, "text")
+ else:
+ return node[0]
+
+ def getNextSibling(self, node):
+ if isinstance(node, tuple): # Text node
+ node, key = node
+ assert key in ("text", "tail"), "Text nodes are text or tail, found %s" % key
+ if key == "text":
+ # XXX: we cannot use a "bool(node) and node[0] or None" construct here
+ # because node[0] might evaluate to False if it has no child element
+ if len(node):
+ return node[0]
+ else:
+ return None
+ else: # tail
+ return node.getnext()
+
+ return (node, "tail") if node.tail else node.getnext()
+
+ def getParentNode(self, node):
+ if isinstance(node, tuple): # Text node
+ node, key = node
+ assert key in ("text", "tail"), "Text nodes are text or tail, found %s" % key
+ if key == "text":
+ return node
+ # else: fallback to "normal" processing
+ elif node in self.fragmentChildren:
+ return None
+
+ return node.getparent()
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/genshi.py b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/genshi.py
new file mode 100644
index 0000000000..7483be27d4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/html5lib/treewalkers/genshi.py
@@ -0,0 +1,69 @@
+from __future__ import absolute_import, division, unicode_literals
+
+from genshi.core import QName
+from genshi.core import START, END, XML_NAMESPACE, DOCTYPE, TEXT
+from genshi.core import START_NS, END_NS, START_CDATA, END_CDATA, PI, COMMENT
+
+from . import base
+
+from ..constants import voidElements, namespaces
+
+
+class TreeWalker(base.TreeWalker):
+ def __iter__(self):
+ # Buffer the events so we can pass in the following one
+ previous = None
+ for event in self.tree:
+ if previous is not None:
+ for token in self.tokens(previous, event):
+ yield token
+ previous = event
+
+ # Don't forget the final event!
+ if previous is not None:
+ for token in self.tokens(previous, None):
+ yield token
+
+ def tokens(self, event, next):
+ kind, data, _ = event
+ if kind == START:
+ tag, attribs = data
+ name = tag.localname
+ namespace = tag.namespace
+ converted_attribs = {}
+ for k, v in attribs:
+ if isinstance(k, QName):
+ converted_attribs[(k.namespace, k.localname)] = v
+ else:
+ converted_attribs[(None, k)] = v
+
+ if namespace == namespaces["html"] and name in voidElements:
+ for token in self.emptyTag(namespace, name, converted_attribs,
+ not next or next[0] != END or
+ next[1] != tag):
+ yield token
+ else:
+ yield self.startTag(namespace, name, converted_attribs)
+
+ elif kind == END:
+ name = data.localname
+ namespace = data.namespace
+ if namespace != namespaces["html"] or name not in voidElements:
+ yield self.endTag(namespace, name)
+
+ elif kind == COMMENT:
+ yield self.comment(data)
+
+ elif kind == TEXT:
+ for token in self.text(data):
+ yield token
+
+ elif kind == DOCTYPE:
+ yield self.doctype(*data)
+
+ elif kind in (XML_NAMESPACE, DOCTYPE, START_NS, END_NS,
+ START_CDATA, END_CDATA, PI):
+ pass
+
+ else:
+ yield self.unknown(kind)
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/parse.py b/testing/web-platform/tests/tools/third_party/html5lib/parse.py
new file mode 100755
index 0000000000..e6806b4607
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/parse.py
@@ -0,0 +1,236 @@
+#!/usr/bin/env python
+"""
+Parse a document to a tree, with optional profiling
+"""
+
+import argparse
+import sys
+import traceback
+
+from html5lib import html5parser
+from html5lib import treebuilders, serializer, treewalkers
+from html5lib import constants
+from html5lib import _utils
+
+
+def parse():
+ parser = get_parser()
+ opts = parser.parse_args()
+ encoding = "utf8"
+
+ try:
+ f = opts.filename
+ # Try opening from the internet
+ if f.startswith('http://'):
+ try:
+ import urllib.request
+ import urllib.parse
+ import urllib.error
+ import cgi
+ f = urllib.request.urlopen(f)
+ contentType = f.headers.get('content-type')
+ if contentType:
+ (mediaType, params) = cgi.parse_header(contentType)
+ encoding = params.get('charset')
+ except Exception:
+ pass
+ elif f == '-':
+ f = sys.stdin
+ if sys.version_info[0] >= 3:
+ encoding = None
+ else:
+ try:
+ # Try opening from file system
+ f = open(f, "rb")
+ except IOError as e:
+ sys.stderr.write("Unable to open file: %s\n" % e)
+ sys.exit(1)
+ except IndexError:
+ sys.stderr.write("No filename provided. Use -h for help\n")
+ sys.exit(1)
+
+ treebuilder = treebuilders.getTreeBuilder(opts.treebuilder)
+
+ p = html5parser.HTMLParser(tree=treebuilder, debug=opts.log)
+
+ if opts.fragment:
+ parseMethod = p.parseFragment
+ else:
+ parseMethod = p.parse
+
+ if opts.profile:
+ import cProfile
+ import pstats
+ cProfile.runctx("run(parseMethod, f, encoding, scripting)", None,
+ {"run": run,
+ "parseMethod": parseMethod,
+ "f": f,
+ "encoding": encoding,
+ "scripting": opts.scripting},
+ "stats.prof")
+ # XXX - We should use a temp file here
+ stats = pstats.Stats('stats.prof')
+ stats.strip_dirs()
+ stats.sort_stats('time')
+ stats.print_stats()
+ elif opts.time:
+ import time
+ t0 = time.time()
+ document = run(parseMethod, f, encoding, opts.scripting)
+ t1 = time.time()
+ if document:
+ printOutput(p, document, opts)
+ t2 = time.time()
+ sys.stderr.write("\n\nRun took: %fs (plus %fs to print the output)" % (t1 - t0, t2 - t1))
+ else:
+ sys.stderr.write("\n\nRun took: %fs" % (t1 - t0))
+ else:
+ document = run(parseMethod, f, encoding, opts.scripting)
+ if document:
+ printOutput(p, document, opts)
+
+
+def run(parseMethod, f, encoding, scripting):
+ try:
+ document = parseMethod(f, override_encoding=encoding, scripting=scripting)
+ except Exception:
+ document = None
+ traceback.print_exc()
+ return document
+
+
+def printOutput(parser, document, opts):
+ if opts.encoding:
+ print("Encoding:", parser.tokenizer.stream.charEncoding)
+
+ for item in parser.log:
+ print(item)
+
+ if document is not None:
+ if opts.xml:
+ tb = opts.treebuilder.lower()
+ if tb == "dom":
+ document.writexml(sys.stdout, encoding="utf-8")
+ elif tb == "lxml":
+ import lxml.etree
+ sys.stdout.write(lxml.etree.tostring(document, encoding="unicode"))
+ elif tb == "etree":
+ sys.stdout.write(_utils.default_etree.tostring(document, encoding="unicode"))
+ elif opts.tree:
+ if not hasattr(document, '__getitem__'):
+ document = [document]
+ for fragment in document:
+ print(parser.tree.testSerializer(fragment))
+ elif opts.html:
+ kwargs = {}
+ for opt in serializer.HTMLSerializer.options:
+ try:
+ kwargs[opt] = getattr(opts, opt)
+ except Exception:
+ pass
+ if not kwargs['quote_char']:
+ del kwargs['quote_char']
+
+ if opts.sanitize:
+ kwargs["sanitize"] = True
+
+ tokens = treewalkers.getTreeWalker(opts.treebuilder)(document)
+ if sys.version_info[0] >= 3:
+ encoding = None
+ else:
+ encoding = "utf-8"
+ for text in serializer.HTMLSerializer(**kwargs).serialize(tokens, encoding=encoding):
+ sys.stdout.write(text)
+ if not text.endswith('\n'):
+ sys.stdout.write('\n')
+ if opts.error:
+ errList = []
+ for pos, errorcode, datavars in parser.errors:
+ errList.append("Line %i Col %i" % pos + " " + constants.E.get(errorcode, 'Unknown error "%s"' % errorcode) % datavars)
+ sys.stdout.write("\nParse errors:\n" + "\n".join(errList) + "\n")
+
+
+def get_parser():
+ parser = argparse.ArgumentParser(description=__doc__)
+
+ parser.add_argument("-p", "--profile", action="store_true",
+ help="Use the hotshot profiler to "
+ "produce a detailed log of the run")
+
+ parser.add_argument("-t", "--time",
+ action="store_true",
+ help="Time the run using time.time (may not be accurate on all platforms, especially for short runs)")
+
+ parser.add_argument("-b", "--treebuilder",
+ default="etree")
+
+ parser.add_argument("-e", "--error", action="store_true",
+ help="Print a list of parse errors")
+
+ parser.add_argument("-f", "--fragment", action="store_true",
+ help="Parse as a fragment")
+
+ parser.add_argument("-s", "--scripting", action="store_true",
+ help="Handle noscript tags as if scripting was enabled")
+
+ parser.add_argument("--tree", action="store_true",
+ help="Output as debug tree")
+
+ parser.add_argument("-x", "--xml", action="store_true",
+ help="Output as xml")
+
+ parser.add_argument("--no-html", action="store_false",
+ dest="html", help="Don't output html")
+
+ parser.add_argument("-c", "--encoding", action="store_true",
+ help="Print character encoding used")
+
+ parser.add_argument("--inject-meta-charset", action="store_true",
+ help="inject <meta charset>")
+
+ parser.add_argument("--strip-whitespace", action="store_true",
+ help="strip whitespace")
+
+ parser.add_argument("--omit-optional-tags", action="store_true",
+ help="omit optional tags")
+
+ parser.add_argument("--quote-attr-values", action="store_true",
+ help="quote attribute values")
+
+ parser.add_argument("--use-best-quote-char", action="store_true",
+ help="use best quote character")
+
+ parser.add_argument("--quote-char",
+ help="quote character")
+
+ parser.add_argument("--no-minimize-boolean-attributes",
+ action="store_false",
+ dest="minimize_boolean_attributes",
+ help="minimize boolean attributes")
+
+ parser.add_argument("--use-trailing-solidus", action="store_true",
+ help="use trailing solidus")
+
+ parser.add_argument("--space-before-trailing-solidus",
+ action="store_true",
+ help="add space before trailing solidus")
+
+ parser.add_argument("--escape-lt-in-attrs", action="store_true",
+ help="escape less than signs in attribute values")
+
+ parser.add_argument("--escape-rcdata", action="store_true",
+ help="escape rcdata element values")
+
+ parser.add_argument("--sanitize", action="store_true",
+ help="sanitize")
+
+ parser.add_argument("-l", "--log", action="store_true",
+ help="log state transitions")
+
+ parser.add_argument("filename")
+
+ return parser
+
+
+if __name__ == "__main__":
+ parse()
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/pytest.ini b/testing/web-platform/tests/tools/third_party/html5lib/pytest.ini
new file mode 100644
index 0000000000..8824977a8f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/pytest.ini
@@ -0,0 +1,17 @@
+[pytest]
+# Output fails, errors, xpass, and warnings; ignore doctest; make warnings errors
+addopts = -rfEXw -p no:doctest --strict
+
+# Make xpass results be considered fail
+xfail_strict = true
+
+# Document our markers
+markers =
+ DOM: mark a test as a DOM tree test
+ ElementTree: mark a test as a ElementTree tree test
+ cElementTree: mark a test as a cElementTree tree test
+ lxml: mark a test as a lxml tree test
+ genshi: mark a test as a genshi tree test
+ parser: mark a test as a parser test
+ namespaced: mark a test as a namespaced parser test
+ treewalker: mark a test as a treewalker test
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/requirements-install.sh b/testing/web-platform/tests/tools/third_party/html5lib/requirements-install.sh
new file mode 100755
index 0000000000..b7a8d96dd6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/requirements-install.sh
@@ -0,0 +1,15 @@
+#!/bin/bash -ex
+
+if [[ $SIX_VERSION ]]; then
+ pip install six==$SIX_VERSION
+fi
+
+pip install -r requirements-test.txt
+
+if [[ $USE_OPTIONAL == "true" ]]; then
+ pip install -r requirements-optional.txt
+fi
+
+if [[ $CI == "true" ]]; then
+ pip install codecov
+fi
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/requirements-optional.txt b/testing/web-platform/tests/tools/third_party/html5lib/requirements-optional.txt
new file mode 100644
index 0000000000..2e78c952c1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/requirements-optional.txt
@@ -0,0 +1,13 @@
+-r requirements.txt
+
+# We support a Genshi treewalker that can be used to serialize Genshi
+# streams.
+genshi
+
+# chardet can be used as a fallback in case we are unable to determine
+# the encoding of a document.
+chardet>=2.2
+
+# lxml is supported with its own treebuilder ("lxml") and otherwise
+# uses the standard ElementTree support
+lxml ; platform_python_implementation == 'CPython'
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/requirements-test.txt b/testing/web-platform/tests/tools/third_party/html5lib/requirements-test.txt
new file mode 100644
index 0000000000..703d0e690f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/requirements-test.txt
@@ -0,0 +1,10 @@
+-r requirements.txt
+
+tox>=3.15.1,<4
+flake8>=3.8.1,<3.9
+pytest>=4.6.10,<5 ; python_version < '3'
+pytest>=5.4.2,<6 ; python_version >= '3'
+coverage>=5.1,<6
+pytest-expect>=1.1.0,<2
+mock>=3.0.5,<4 ; python_version < '3.6'
+mock>=4.0.2,<5 ; python_version >= '3.6'
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/requirements.txt b/testing/web-platform/tests/tools/third_party/html5lib/requirements.txt
new file mode 100644
index 0000000000..ae7ec3d08b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/requirements.txt
@@ -0,0 +1,2 @@
+six>=1.9
+webencodings
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/setup.cfg b/testing/web-platform/tests/tools/third_party/html5lib/setup.cfg
new file mode 100644
index 0000000000..0b2bb9c79b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/setup.cfg
@@ -0,0 +1,11 @@
+[bdist_wheel]
+universal = 1
+
+[pep8]
+ignore = N
+max-line-length = 139
+exclude = .git,__pycache__,.tox,doc
+
+[flake8]
+ignore = N, W504
+max-line-length = 139
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/setup.py b/testing/web-platform/tests/tools/third_party/html5lib/setup.py
new file mode 100644
index 0000000000..f84c128496
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/setup.py
@@ -0,0 +1,127 @@
+from __future__ import print_function
+
+import ast
+import codecs
+import sys
+
+from os.path import join, dirname
+from setuptools import setup, find_packages, __version__ as setuptools_version
+from pkg_resources import parse_version
+
+import pkg_resources
+
+try:
+ import _markerlib.markers
+except ImportError:
+ _markerlib = None
+
+
+# _markerlib.default_environment() obtains its data from _VARS
+# and wraps it in another dict, but _markerlib_evaluate writes
+# to the dict while it is iterating the keys, causing an error
+# on Python 3 only.
+# Replace _markerlib.default_environment to return a custom dict
+# that has all the necessary markers, and ignores any writes.
+
+class Python3MarkerDict(dict):
+
+ def __setitem__(self, key, value):
+ pass
+
+ def pop(self, i=-1):
+ return self[i]
+
+
+if _markerlib and sys.version_info[0] == 3:
+ env = _markerlib.markers._VARS
+ for key in list(env.keys()):
+ new_key = key.replace('.', '_')
+ if new_key != key:
+ env[new_key] = env[key]
+
+ _markerlib.markers._VARS = Python3MarkerDict(env)
+
+ def default_environment():
+ return _markerlib.markers._VARS
+
+ _markerlib.default_environment = default_environment
+
+# Avoid the very buggy pkg_resources.parser, which doesn't consistently
+# recognise the markers needed by this setup.py
+# Change this to setuptools 20.10.0 to support all markers.
+if pkg_resources:
+ if parse_version(setuptools_version) < parse_version('18.5'):
+ MarkerEvaluation = pkg_resources.MarkerEvaluation
+
+ del pkg_resources.parser
+ pkg_resources.evaluate_marker = MarkerEvaluation._markerlib_evaluate
+ MarkerEvaluation.evaluate_marker = MarkerEvaluation._markerlib_evaluate
+
+classifiers = [
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ '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 :: Implementation :: CPython',
+ 'Programming Language :: Python :: Implementation :: PyPy',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ 'Topic :: Text Processing :: Markup :: HTML'
+]
+
+here = dirname(__file__)
+with codecs.open(join(here, 'README.rst'), 'r', 'utf8') as readme_file:
+ with codecs.open(join(here, 'CHANGES.rst'), 'r', 'utf8') as changes_file:
+ long_description = readme_file.read() + '\n' + changes_file.read()
+
+version = None
+with open(join(here, "html5lib", "__init__.py"), "rb") as init_file:
+ t = ast.parse(init_file.read(), filename="__init__.py", mode="exec")
+ assert isinstance(t, ast.Module)
+ assignments = filter(lambda x: isinstance(x, ast.Assign), t.body)
+ for a in assignments:
+ if (len(a.targets) == 1 and
+ isinstance(a.targets[0], ast.Name) and
+ a.targets[0].id == "__version__" and
+ isinstance(a.value, ast.Str)):
+ version = a.value.s
+
+setup(name='html5lib',
+ version=version,
+ url='https://github.com/html5lib/html5lib-python',
+ license="MIT License",
+ description='HTML parser based on the WHATWG HTML specification',
+ long_description=long_description,
+ classifiers=classifiers,
+ maintainer='James Graham',
+ maintainer_email='james@hoppipolla.co.uk',
+ packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
+ install_requires=[
+ 'six>=1.9',
+ 'webencodings',
+ ],
+ python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
+ extras_require={
+ # A conditional extra will only install these items when the extra is
+ # requested and the condition matches.
+ "lxml:platform_python_implementation == 'CPython'": ["lxml"],
+
+ # Standard extras, will be installed when the extra is requested.
+ "genshi": ["genshi"],
+ "chardet": ["chardet>=2.2"],
+
+ # The all extra combines a standard extra which will be used anytime
+ # the all extra is requested, and it extends it with a conditional
+ # extra that will be installed whenever the condition matches and the
+ # all extra is requested.
+ "all": ["genshi", "chardet>=2.2"],
+ "all:platform_python_implementation == 'CPython'": ["lxml"],
+ },
+ )
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/tox.ini b/testing/web-platform/tests/tools/third_party/html5lib/tox.ini
new file mode 100644
index 0000000000..58758cea13
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/tox.ini
@@ -0,0 +1,20 @@
+[tox]
+envlist = py{27,35,36,37,38,py,py3}-{base,six19,optional}
+
+[testenv]
+deps =
+ optional: -r{toxinidir}/requirements-optional.txt
+ -r{toxinidir}/requirements-test.txt
+ doc: Sphinx
+
+passenv =
+ PYTEST_COMMAND
+ COVERAGE_RUN_OPTIONS
+commands =
+ six19: pip install six==1.9
+ {env:PYTEST_COMMAND:{envbindir}/py.test} {posargs}
+ flake8 {toxinidir}
+
+[testenv:doc]
+changedir = doc
+commands = sphinx-build -b html . _build
diff --git a/testing/web-platform/tests/tools/third_party/html5lib/utils/entities.py b/testing/web-platform/tests/tools/third_party/html5lib/utils/entities.py
new file mode 100644
index 0000000000..6e8ca45806
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/html5lib/utils/entities.py
@@ -0,0 +1,101 @@
+import json
+
+import html5lib
+
+
+def parse(path="html5ents.xml"):
+ return html5lib.parse(open(path), treebuilder="lxml")
+
+
+def entity_table(tree):
+ return {entity_name("".join(tr[0].xpath(".//text()"))):
+ entity_characters(tr[1].text)
+ for tr in tree.xpath("//h:tbody/h:tr",
+ namespaces={"h": "http://www.w3.org/1999/xhtml"})}
+
+
+def entity_name(inp):
+ return inp.strip()
+
+
+def entity_characters(inp):
+ return "".join(codepoint_to_character(item)
+ for item in inp.split()
+ if item)
+
+
+def codepoint_to_character(inp):
+ return ("\\U000" + inp[2:]).decode("unicode-escape")
+
+
+def make_tests_json(entities):
+ test_list = make_test_list(entities)
+ tests_json = {"tests":
+ [make_test(*item) for item in test_list]
+ }
+ return tests_json
+
+
+def make_test(name, characters, good):
+ return {
+ "description": test_description(name, good),
+ "input": "&%s" % name,
+ "output": test_expected(name, characters, good)
+ }
+
+
+def test_description(name, good):
+ with_semicolon = name.endswith(";")
+ semicolon_text = {True: "with a semi-colon",
+ False: "without a semi-colon"}[with_semicolon]
+ if good:
+ text = "Named entity: %s %s" % (name, semicolon_text)
+ else:
+ text = "Bad named entity: %s %s" % (name, semicolon_text)
+ return text
+
+
+def test_expected(name, characters, good):
+ rv = []
+ if not good or not name.endswith(";"):
+ rv.append("ParseError")
+ rv.append(["Character", characters])
+ return rv
+
+
+def make_test_list(entities):
+ tests = []
+ for entity_name, characters in entities.items():
+ if entity_name.endswith(";") and not subentity_exists(entity_name, entities):
+ tests.append((entity_name[:-1], "&" + entity_name[:-1], False))
+ tests.append((entity_name, characters, True))
+ return sorted(tests)
+
+
+def subentity_exists(entity_name, entities):
+ for i in range(1, len(entity_name)):
+ if entity_name[:-i] in entities:
+ return True
+ return False
+
+
+def make_entities_code(entities):
+ entities_text = "\n".join(" \"%s\": u\"%s\"," % (
+ name, entities[name].encode(
+ "unicode-escape").replace("\"", "\\\""))
+ for name in sorted(entities.keys()))
+ return """entities = {
+%s
+}""" % entities_text
+
+
+def main():
+ entities = entity_table(parse())
+ tests_json = make_tests_json(entities)
+ json.dump(tests_json, open("namedEntities.test", "w"), indent=4)
+ code = make_entities_code(entities)
+ open("entities_constants.py", "w").write(code)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/CONTRIBUTORS.rst b/testing/web-platform/tests/tools/third_party/hyperframe/CONTRIBUTORS.rst
new file mode 100644
index 0000000000..aa7ab8b637
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/CONTRIBUTORS.rst
@@ -0,0 +1,56 @@
+Hyper is written and maintained by Cory Benfield and various contributors:
+
+Development Lead
+````````````````
+
+- Cory Benfield <cory@lukasa.co.uk>
+
+Contributors
+````````````
+
+In chronological order:
+
+- Sriram Ganesan (@elricL)
+
+ - Implemented the Huffman encoding/decoding logic.
+
+- Alek Storm (@alekstorm)
+
+ - Implemented Python 2.7 support.
+ - Implemented HTTP/2 draft 10 support.
+ - Implemented server push.
+
+- Tetsuya Morimoto (@t2y)
+
+ - Fixed a bug where large or incomplete frames were not handled correctly.
+ - Added hyper command-line tool.
+ - General code cleanups.
+
+- Jerome De Cuyper (@jdecuyper)
+
+ - Updated documentation and tests.
+
+- Maximilian Hils (@mhils)
+
+ - Added repr for frames.
+ - Improved frame initialization code.
+ - Added flag validation.
+
+- Thomas Kriechbaumer (@Kriechi)
+
+ - Improved initialization code.
+ - Fixed bugs in frame initialization code.
+ - Improved frame repr for frames with non-printable bodies.
+
+- Davey Shafik (@dshafik)
+
+ - Fixed Alt Svc frame stream association.
+
+- Seth Michael Larson (@SethMichaelLarson)
+
+ - Performance improvements to serialization and parsing.
+
+- Fred Thomsen (@fredthomsen)
+
+ - Support for memoryview in DataFrames.
+
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/HISTORY.rst b/testing/web-platform/tests/tools/third_party/hyperframe/HISTORY.rst
new file mode 100644
index 0000000000..172b2b91ba
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/HISTORY.rst
@@ -0,0 +1,179 @@
+Release History
+===============
+
+6.0.0dev0
+---------
+
+5.2.0 (2019-01-18)
+------------------
+
+**API Changes (Backward-compatible)**
+
+- Add a new ENABLE_CONNECT_PROTOCOL settings paramter.
+
+**Other Changes**
+
+- Fix collections.abc deprecation.
+- Drop support for Python 3.3 and support 3.7.
+
+5.1.0 (2017-04-24)
+------------------
+
+**API Changes (Backward-compatible)**
+
+- Added support for ``DataFrame.data`` being a ``memoryview`` object.
+
+5.0.0 (2017-03-07)
+------------------
+
+**Backwards Incompatible API Changes**
+
+- Added support for unknown extension frames. These will be returned in the new
+ ``ExtensionFrame`` object. The flag information for these frames is persisted
+ in ``flag_byte`` if needed.
+
+4.0.2 (2017-02-20)
+------------------
+
+**Bugfixes**
+
+- Fixed AltSvc stream association, which was incorrectly set to ``'both'``:
+ should have been ``'either'``.
+- Fixed a bug where stream IDs on received frames were allowed to be 32-bit,
+ instead of 31-bit.
+- Fixed a bug with frames that had the ``PADDING`` flag set but zero-length
+ padding, whose flow-controlled length was calculated wrongly.
+- Miscellaneous performance improvements to serialization and parsing logic.
+
+4.0.1 (2016-03-13)
+------------------
+
+**Bugfixes**
+
+- Fixed bug with the repr of ``AltSvcFrame``, where building it could throw
+ exceptions if the frame had been received from the network.
+
+4.0.0 (2016-03-13)
+------------------
+
+**Backwards Incompatible API Changes**
+
+- Updated old ALTSVC frame definition to match the newly specified RFC 7838.
+- Remove BLOCKED frame, which was never actually specified.
+- Removed previously deprecated ``SettingsFrame.SETTINGS_MAX_FRAME_SIZE`` and
+ ``SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE``.
+
+3.2.0 (2016-02-02)
+------------------
+
+**API Changes (Backward-compatible)**
+
+- Invalid PING frame bodies now raise ``InvalidFrameError``, not
+ ``ValueError``. Note that ``InvalidFrameError`` is a ``ValueError`` subclass.
+- Invalid RST_STREAM frame bodies now raise ``InvalidFramError``, not
+ ``ValueError``. Note that ``InvalidFrameError`` is a ``ValueError`` subclass.
+- Canonicalized the names of ``SettingsFrame.SETTINGS_MAX_FRAME_SIZE`` and
+ ``SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE`` to match their peers, by
+ adding new properties ``SettingsFrame.MAX_FRAME_SIZE`` and
+ ``SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE``. The old names are still
+ present, but will be deprecated in 4.0.0.
+
+**Bugfixes**
+
+- The change in ``3.1.0`` that ensured that ``InvalidFrameError`` would be
+ thrown did not affect certain invalid values in ALT_SVC frames. This has been
+ fixed: ``ValueError`` will no longer be thrown from invalid ALT_SVC bodies.
+
+3.1.1 (2016-01-18)
+------------------
+
+**Bugfixes**
+
+- Correctly error when receiving Ping frames that have insufficient data.
+
+3.1.0 (2016-01-13)
+------------------
+
+**API Changes**
+
+- Added new ``InvalidFrameError`` that is thrown instead of ``struct.error``
+ when parsing a frame.
+
+**Bugfixes**
+
+- Fixed error when trying to serialize frames that use Priority information
+ with the defaults for that information.
+- Fixed errors when displaying the repr of frames with non-printable bodies.
+
+3.0.1 (2016-01-08)
+------------------
+
+**Bugfixes**
+
+- Fix issue where unpadded DATA, PUSH_PROMISE and HEADERS frames that had empty
+ bodies would raise ``InvalidPaddingError`` exceptions when parsed.
+
+3.0.0 (2016-01-08)
+------------------
+
+**Backwards Incompatible API Changes**
+
+- Parsing padded frames that have invalid padding sizes now throws an
+ ``InvalidPaddingError``.
+
+2.2.0 (2015-10-15)
+------------------
+
+**API Changes**
+
+- When an unknown frame is encountered, ``parse_frame_header`` now throws a
+ ``ValueError`` subclass: ``UnknownFrameError``. This subclass contains the
+ frame type and the length of the frame body.
+
+2.1.0 (2015-10-06)
+------------------
+
+**API Changes**
+
+- Frames parsed from binary data now carry a ``body_len`` attribute that
+ matches the frame length (minus the frame header).
+
+2.0.0 (2015-09-21)
+------------------
+
+**API Changes**
+
+- Attempting to parse unrecognised frames now throws ``ValueError`` instead of
+ ``KeyError``. Thanks to @Kriechi!
+- Flags are now validated for correctness, preventing setting flags that
+ ``hyperframe`` does not recognise and that would not serialize. Thanks to
+ @mhils!
+- Frame properties can now be initialized in the constructors. Thanks to @mhils
+ and @Kriechi!
+- Frames that cannot be sent on a stream now have their stream ID defaulted
+ to ``0``. Thanks to @Kriechi!
+
+**Other Changes**
+
+- Frames have a more useful repr. Thanks to @mhils!
+
+1.1.1 (2015-07-20)
+------------------
+
+- Fix a bug where ``FRAME_MAX_LEN`` was one byte too small.
+
+1.1.0 (2015-06-28)
+------------------
+
+- Add ``body_len`` property to frames to enable introspection of the actual
+ frame length. Thanks to @jdecuyper!
+
+1.0.1 (2015-06-27)
+------------------
+
+- Fix bug where the frame header would have an incorrect length added to it.
+
+1.0.0 (2015-04-12)
+------------------
+
+- Initial extraction from hyper.
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/LICENSE b/testing/web-platform/tests/tools/third_party/hyperframe/LICENSE
new file mode 100644
index 0000000000..d24c351e18
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Cory Benfield
+
+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/testing/web-platform/tests/tools/third_party/hyperframe/MANIFEST.in b/testing/web-platform/tests/tools/third_party/hyperframe/MANIFEST.in
new file mode 100644
index 0000000000..2f464676cb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/MANIFEST.in
@@ -0,0 +1,2 @@
+include README.rst LICENSE CONTRIBUTORS.rst HISTORY.rst
+
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/PKG-INFO b/testing/web-platform/tests/tools/third_party/hyperframe/PKG-INFO
new file mode 100644
index 0000000000..cfd53f7e93
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/PKG-INFO
@@ -0,0 +1,242 @@
+Metadata-Version: 1.1
+Name: hyperframe
+Version: 5.2.0
+Summary: HTTP/2 framing layer for Python
+Home-page: https://python-hyper.org/hyperframe/en/latest/
+Author: Cory Benfield
+Author-email: cory@lukasa.co.uk
+License: MIT License
+Description: ======================================
+ hyperframe: Pure-Python HTTP/2 framing
+ ======================================
+
+ .. image:: https://travis-ci.org/python-hyper/hyperframe.png?branch=master
+ :target: https://travis-ci.org/python-hyper/hyperframe
+
+ This library contains the HTTP/2 framing code used in the `hyper`_ project. It
+ provides a pure-Python codebase that is capable of decoding a binary stream
+ into HTTP/2 frames.
+
+ This library is used directly by `hyper`_ and a number of other projects to
+ provide HTTP/2 frame decoding logic.
+
+ Contributing
+ ============
+
+ hyperframe welcomes contributions from anyone! Unlike many other projects we
+ are happy to accept cosmetic contributions and small contributions, in addition
+ to large feature requests and changes.
+
+ Before you contribute (either by opening an issue or filing a pull request),
+ please `read the contribution guidelines`_.
+
+ .. _read the contribution guidelines: http://hyper.readthedocs.org/en/development/contributing.html
+
+ License
+ =======
+
+ hyperframe is made available under the MIT License. For more details, see the
+ ``LICENSE`` file in the repository.
+
+ Authors
+ =======
+
+ hyperframe is maintained by Cory Benfield, with contributions from others. For
+ more details about the contributors, please see ``CONTRIBUTORS.rst``.
+
+ .. _hyper: http://python-hyper.org/
+
+
+ Release History
+ ===============
+
+ 6.0.0dev0
+ ---------
+
+ 5.2.0 (2019-01-18)
+ ------------------
+
+ **API Changes (Backward-compatible)**
+
+ - Add a new ENABLE_CONNECT_PROTOCOL settings paramter.
+
+ **Other Changes**
+
+ - Fix collections.abc deprecation.
+ - Drop support for Python 3.3 and support 3.7.
+
+ 5.1.0 (2017-04-24)
+ ------------------
+
+ **API Changes (Backward-compatible)**
+
+ - Added support for ``DataFrame.data`` being a ``memoryview`` object.
+
+ 5.0.0 (2017-03-07)
+ ------------------
+
+ **Backwards Incompatible API Changes**
+
+ - Added support for unknown extension frames. These will be returned in the new
+ ``ExtensionFrame`` object. The flag information for these frames is persisted
+ in ``flag_byte`` if needed.
+
+ 4.0.2 (2017-02-20)
+ ------------------
+
+ **Bugfixes**
+
+ - Fixed AltSvc stream association, which was incorrectly set to ``'both'``:
+ should have been ``'either'``.
+ - Fixed a bug where stream IDs on received frames were allowed to be 32-bit,
+ instead of 31-bit.
+ - Fixed a bug with frames that had the ``PADDING`` flag set but zero-length
+ padding, whose flow-controlled length was calculated wrongly.
+ - Miscellaneous performance improvements to serialization and parsing logic.
+
+ 4.0.1 (2016-03-13)
+ ------------------
+
+ **Bugfixes**
+
+ - Fixed bug with the repr of ``AltSvcFrame``, where building it could throw
+ exceptions if the frame had been received from the network.
+
+ 4.0.0 (2016-03-13)
+ ------------------
+
+ **Backwards Incompatible API Changes**
+
+ - Updated old ALTSVC frame definition to match the newly specified RFC 7838.
+ - Remove BLOCKED frame, which was never actually specified.
+ - Removed previously deprecated ``SettingsFrame.SETTINGS_MAX_FRAME_SIZE`` and
+ ``SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE``.
+
+ 3.2.0 (2016-02-02)
+ ------------------
+
+ **API Changes (Backward-compatible)**
+
+ - Invalid PING frame bodies now raise ``InvalidFrameError``, not
+ ``ValueError``. Note that ``InvalidFrameError`` is a ``ValueError`` subclass.
+ - Invalid RST_STREAM frame bodies now raise ``InvalidFramError``, not
+ ``ValueError``. Note that ``InvalidFrameError`` is a ``ValueError`` subclass.
+ - Canonicalized the names of ``SettingsFrame.SETTINGS_MAX_FRAME_SIZE`` and
+ ``SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE`` to match their peers, by
+ adding new properties ``SettingsFrame.MAX_FRAME_SIZE`` and
+ ``SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE``. The old names are still
+ present, but will be deprecated in 4.0.0.
+
+ **Bugfixes**
+
+ - The change in ``3.1.0`` that ensured that ``InvalidFrameError`` would be
+ thrown did not affect certain invalid values in ALT_SVC frames. This has been
+ fixed: ``ValueError`` will no longer be thrown from invalid ALT_SVC bodies.
+
+ 3.1.1 (2016-01-18)
+ ------------------
+
+ **Bugfixes**
+
+ - Correctly error when receiving Ping frames that have insufficient data.
+
+ 3.1.0 (2016-01-13)
+ ------------------
+
+ **API Changes**
+
+ - Added new ``InvalidFrameError`` that is thrown instead of ``struct.error``
+ when parsing a frame.
+
+ **Bugfixes**
+
+ - Fixed error when trying to serialize frames that use Priority information
+ with the defaults for that information.
+ - Fixed errors when displaying the repr of frames with non-printable bodies.
+
+ 3.0.1 (2016-01-08)
+ ------------------
+
+ **Bugfixes**
+
+ - Fix issue where unpadded DATA, PUSH_PROMISE and HEADERS frames that had empty
+ bodies would raise ``InvalidPaddingError`` exceptions when parsed.
+
+ 3.0.0 (2016-01-08)
+ ------------------
+
+ **Backwards Incompatible API Changes**
+
+ - Parsing padded frames that have invalid padding sizes now throws an
+ ``InvalidPaddingError``.
+
+ 2.2.0 (2015-10-15)
+ ------------------
+
+ **API Changes**
+
+ - When an unknown frame is encountered, ``parse_frame_header`` now throws a
+ ``ValueError`` subclass: ``UnknownFrameError``. This subclass contains the
+ frame type and the length of the frame body.
+
+ 2.1.0 (2015-10-06)
+ ------------------
+
+ **API Changes**
+
+ - Frames parsed from binary data now carry a ``body_len`` attribute that
+ matches the frame length (minus the frame header).
+
+ 2.0.0 (2015-09-21)
+ ------------------
+
+ **API Changes**
+
+ - Attempting to parse unrecognised frames now throws ``ValueError`` instead of
+ ``KeyError``. Thanks to @Kriechi!
+ - Flags are now validated for correctness, preventing setting flags that
+ ``hyperframe`` does not recognise and that would not serialize. Thanks to
+ @mhils!
+ - Frame properties can now be initialized in the constructors. Thanks to @mhils
+ and @Kriechi!
+ - Frames that cannot be sent on a stream now have their stream ID defaulted
+ to ``0``. Thanks to @Kriechi!
+
+ **Other Changes**
+
+ - Frames have a more useful repr. Thanks to @mhils!
+
+ 1.1.1 (2015-07-20)
+ ------------------
+
+ - Fix a bug where ``FRAME_MAX_LEN`` was one byte too small.
+
+ 1.1.0 (2015-06-28)
+ ------------------
+
+ - Add ``body_len`` property to frames to enable introspection of the actual
+ frame length. Thanks to @jdecuyper!
+
+ 1.0.1 (2015-06-27)
+ ------------------
+
+ - Fix bug where the frame header would have an incorrect length added to it.
+
+ 1.0.0 (2015-04-12)
+ ------------------
+
+ - Initial extraction from hyper.
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+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 :: Implementation :: CPython
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/README.rst b/testing/web-platform/tests/tools/third_party/hyperframe/README.rst
new file mode 100644
index 0000000000..385b39af9b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/README.rst
@@ -0,0 +1,39 @@
+======================================
+hyperframe: Pure-Python HTTP/2 framing
+======================================
+
+.. image:: https://travis-ci.org/python-hyper/hyperframe.png?branch=master
+ :target: https://travis-ci.org/python-hyper/hyperframe
+
+This library contains the HTTP/2 framing code used in the `hyper`_ project. It
+provides a pure-Python codebase that is capable of decoding a binary stream
+into HTTP/2 frames.
+
+This library is used directly by `hyper`_ and a number of other projects to
+provide HTTP/2 frame decoding logic.
+
+Contributing
+============
+
+hyperframe welcomes contributions from anyone! Unlike many other projects we
+are happy to accept cosmetic contributions and small contributions, in addition
+to large feature requests and changes.
+
+Before you contribute (either by opening an issue or filing a pull request),
+please `read the contribution guidelines`_.
+
+.. _read the contribution guidelines: http://hyper.readthedocs.org/en/development/contributing.html
+
+License
+=======
+
+hyperframe is made available under the MIT License. For more details, see the
+``LICENSE`` file in the repository.
+
+Authors
+=======
+
+hyperframe is maintained by Cory Benfield, with contributions from others. For
+more details about the contributors, please see ``CONTRIBUTORS.rst``.
+
+.. _hyper: http://python-hyper.org/
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/__init__.py b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/__init__.py
new file mode 100644
index 0000000000..7620b4bdf7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+"""
+hyperframe
+~~~~~~~~~~
+
+A module for providing a pure-Python HTTP/2 framing layer.
+"""
+__version__ = '5.2.0'
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/exceptions.py b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/exceptions.py
new file mode 100644
index 0000000000..dd30369c70
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/exceptions.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+"""
+hyperframe/exceptions
+~~~~~~~~~~~~~~~~~~~~~
+
+Defines the exceptions that can be thrown by hyperframe.
+"""
+
+
+class UnknownFrameError(ValueError):
+ """
+ An frame of unknown type was received.
+ """
+ def __init__(self, frame_type, length):
+ #: The type byte of the unknown frame that was received.
+ self.frame_type = frame_type
+
+ #: The length of the data portion of the unknown frame.
+ self.length = length
+
+ def __str__(self):
+ return (
+ "UnknownFrameError: Unknown frame type 0x%X received, "
+ "length %d bytes" % (self.frame_type, self.length)
+ )
+
+
+class InvalidPaddingError(ValueError):
+ """
+ A frame with invalid padding was received.
+ """
+ pass
+
+
+class InvalidFrameError(ValueError):
+ """
+ Parsing a frame failed because the data was not laid out appropriately.
+
+ .. versionadded:: 3.0.2
+ """
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/flags.py b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/flags.py
new file mode 100644
index 0000000000..1660bd1800
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/flags.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+"""
+hyperframe/flags
+~~~~~~~~~~~~~~~~
+
+Defines basic Flag and Flags data structures.
+"""
+import collections
+
+try:
+ from collections.abc import MutableSet
+except ImportError: # pragma: no cover
+ # Python 2.7 compatibility
+ from collections import MutableSet
+
+Flag = collections.namedtuple("Flag", ["name", "bit"])
+
+
+class Flags(MutableSet):
+ """
+ A simple MutableSet implementation that will only accept known flags as
+ elements.
+
+ Will behave like a regular set(), except that a ValueError will be thrown
+ when .add()ing unexpected flags.
+ """
+ def __init__(self, defined_flags):
+ self._valid_flags = set(flag.name for flag in defined_flags)
+ self._flags = set()
+
+ def __contains__(self, x):
+ return self._flags.__contains__(x)
+
+ def __iter__(self):
+ return self._flags.__iter__()
+
+ def __len__(self):
+ return self._flags.__len__()
+
+ def discard(self, value):
+ return self._flags.discard(value)
+
+ def add(self, value):
+ if value not in self._valid_flags:
+ raise ValueError(
+ "Unexpected flag: {}. Valid flags are: {}".format(
+ value, self._valid_flags
+ )
+ )
+ return self._flags.add(value)
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/frame.py b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/frame.py
new file mode 100644
index 0000000000..795057279b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/frame.py
@@ -0,0 +1,822 @@
+# -*- coding: utf-8 -*-
+"""
+hyperframe/frame
+~~~~~~~~~~~~~~~~
+
+Defines framing logic for HTTP/2. Provides both classes to represent framed
+data and logic for aiding the connection when it comes to reading from the
+socket.
+"""
+import struct
+import binascii
+
+from .exceptions import (
+ UnknownFrameError, InvalidPaddingError, InvalidFrameError
+)
+from .flags import Flag, Flags
+
+
+# The maximum initial length of a frame. Some frames have shorter maximum
+# lengths.
+FRAME_MAX_LEN = (2 ** 14)
+
+# The maximum allowed length of a frame.
+FRAME_MAX_ALLOWED_LEN = (2 ** 24) - 1
+
+# Stream association enumerations.
+_STREAM_ASSOC_HAS_STREAM = "has-stream"
+_STREAM_ASSOC_NO_STREAM = "no-stream"
+_STREAM_ASSOC_EITHER = "either"
+
+# Structs for packing and unpacking
+_STRUCT_HBBBL = struct.Struct(">HBBBL")
+_STRUCT_LL = struct.Struct(">LL")
+_STRUCT_HL = struct.Struct(">HL")
+_STRUCT_LB = struct.Struct(">LB")
+_STRUCT_L = struct.Struct(">L")
+_STRUCT_H = struct.Struct(">H")
+_STRUCT_B = struct.Struct(">B")
+
+
+class Frame(object):
+ """
+ The base class for all HTTP/2 frames.
+ """
+ #: The flags defined on this type of frame.
+ defined_flags = []
+
+ #: The byte used to define the type of the frame.
+ type = None
+
+ # If 'has-stream', the frame's stream_id must be non-zero. If 'no-stream',
+ # it must be zero. If 'either', it's not checked.
+ stream_association = None
+
+ def __init__(self, stream_id, flags=()):
+ #: The stream identifier for the stream this frame was received on.
+ #: Set to 0 for frames sent on the connection (stream-id 0).
+ self.stream_id = stream_id
+
+ #: The flags set for this frame.
+ self.flags = Flags(self.defined_flags)
+
+ #: The frame length, excluding the nine-byte header.
+ self.body_len = 0
+
+ for flag in flags:
+ self.flags.add(flag)
+
+ if (not self.stream_id and
+ self.stream_association == _STREAM_ASSOC_HAS_STREAM):
+ raise ValueError('Stream ID must be non-zero')
+ if (self.stream_id and
+ self.stream_association == _STREAM_ASSOC_NO_STREAM):
+ raise ValueError('Stream ID must be zero')
+
+ def __repr__(self):
+ flags = ", ".join(self.flags) or "None"
+ body = binascii.hexlify(self.serialize_body()).decode('ascii')
+ if len(body) > 20:
+ body = body[:20] + "..."
+ return (
+ "{type}(Stream: {stream}; Flags: {flags}): {body}"
+ ).format(
+ type=type(self).__name__,
+ stream=self.stream_id,
+ flags=flags,
+ body=body
+ )
+
+ @staticmethod
+ def parse_frame_header(header, strict=False):
+ """
+ Takes a 9-byte frame header and returns a tuple of the appropriate
+ Frame object and the length that needs to be read from the socket.
+
+ This populates the flags field, and determines how long the body is.
+
+ :param strict: Whether to raise an exception when encountering a frame
+ not defined by spec and implemented by hyperframe.
+
+ :raises hyperframe.exceptions.UnknownFrameError: If a frame of unknown
+ type is received.
+
+ .. versionchanged:: 5.0.0
+ Added :param:`strict` to accommodate :class:`ExtensionFrame`
+ """
+ try:
+ fields = _STRUCT_HBBBL.unpack(header)
+ except struct.error:
+ raise InvalidFrameError("Invalid frame header")
+
+ # First 24 bits are frame length.
+ length = (fields[0] << 8) + fields[1]
+ type = fields[2]
+ flags = fields[3]
+ stream_id = fields[4] & 0x7FFFFFFF
+
+ try:
+ frame = FRAMES[type](stream_id)
+ except KeyError:
+ if strict:
+ raise UnknownFrameError(type, length)
+ frame = ExtensionFrame(type=type, stream_id=stream_id)
+
+ frame.parse_flags(flags)
+ return (frame, length)
+
+ def parse_flags(self, flag_byte):
+ for flag, flag_bit in self.defined_flags:
+ if flag_byte & flag_bit:
+ self.flags.add(flag)
+
+ return self.flags
+
+ def serialize(self):
+ """
+ Convert a frame into a bytestring, representing the serialized form of
+ the frame.
+ """
+ body = self.serialize_body()
+ self.body_len = len(body)
+
+ # Build the common frame header.
+ # First, get the flags.
+ flags = 0
+
+ for flag, flag_bit in self.defined_flags:
+ if flag in self.flags:
+ flags |= flag_bit
+
+ header = _STRUCT_HBBBL.pack(
+ (self.body_len >> 8) & 0xFFFF, # Length spread over top 24 bits
+ self.body_len & 0xFF,
+ self.type,
+ flags,
+ self.stream_id & 0x7FFFFFFF # Stream ID is 32 bits.
+ )
+
+ return header + body
+
+ def serialize_body(self):
+ raise NotImplementedError()
+
+ def parse_body(self, data):
+ """
+ Given the body of a frame, parses it into frame data. This populates
+ the non-header parts of the frame: that is, it does not populate the
+ stream ID or flags.
+
+ :param data: A memoryview object containing the body data of the frame.
+ Must not contain *more* data than the length returned by
+ :meth:`parse_frame_header
+ <hyperframe.frame.Frame.parse_frame_header>`.
+ """
+ raise NotImplementedError()
+
+
+class Padding(object):
+ """
+ Mixin for frames that contain padding. Defines extra fields that can be
+ used and set by frames that can be padded.
+ """
+ def __init__(self, stream_id, pad_length=0, **kwargs):
+ super(Padding, self).__init__(stream_id, **kwargs)
+
+ #: The length of the padding to use.
+ self.pad_length = pad_length
+
+ def serialize_padding_data(self):
+ if 'PADDED' in self.flags:
+ return _STRUCT_B.pack(self.pad_length)
+ return b''
+
+ def parse_padding_data(self, data):
+ if 'PADDED' in self.flags:
+ try:
+ self.pad_length = struct.unpack('!B', data[:1])[0]
+ except struct.error:
+ raise InvalidFrameError("Invalid Padding data")
+ return 1
+ return 0
+
+ @property
+ def total_padding(self):
+ return self.pad_length
+
+
+class Priority(object):
+ """
+ Mixin for frames that contain priority data. Defines extra fields that can
+ be used and set by frames that contain priority data.
+ """
+ def __init__(self,
+ stream_id,
+ depends_on=0x0,
+ stream_weight=0x0,
+ exclusive=False,
+ **kwargs):
+ super(Priority, self).__init__(stream_id, **kwargs)
+
+ #: The stream ID of the stream on which this stream depends.
+ self.depends_on = depends_on
+
+ #: The weight of the stream. This is an integer between 0 and 256.
+ self.stream_weight = stream_weight
+
+ #: Whether the exclusive bit was set.
+ self.exclusive = exclusive
+
+ def serialize_priority_data(self):
+ return _STRUCT_LB.pack(
+ self.depends_on + (0x80000000 if self.exclusive else 0),
+ self.stream_weight
+ )
+
+ def parse_priority_data(self, data):
+ try:
+ self.depends_on, self.stream_weight = _STRUCT_LB.unpack(data[:5])
+ except struct.error:
+ raise InvalidFrameError("Invalid Priority data")
+
+ self.exclusive = True if self.depends_on >> 31 else False
+ self.depends_on &= 0x7FFFFFFF
+ return 5
+
+
+class DataFrame(Padding, Frame):
+ """
+ DATA frames convey arbitrary, variable-length sequences of octets
+ associated with a stream. One or more DATA frames are used, for instance,
+ to carry HTTP request or response payloads.
+ """
+ #: The flags defined for DATA frames.
+ defined_flags = [
+ Flag('END_STREAM', 0x01),
+ Flag('PADDED', 0x08),
+ ]
+
+ #: The type byte for data frames.
+ type = 0x0
+
+ stream_association = _STREAM_ASSOC_HAS_STREAM
+
+ def __init__(self, stream_id, data=b'', **kwargs):
+ super(DataFrame, self).__init__(stream_id, **kwargs)
+
+ #: The data contained on this frame.
+ self.data = data
+
+ def serialize_body(self):
+ padding_data = self.serialize_padding_data()
+ padding = b'\0' * self.total_padding
+ if isinstance(self.data, memoryview):
+ self.data = self.data.tobytes()
+ return b''.join([padding_data, self.data, padding])
+
+ def parse_body(self, data):
+ padding_data_length = self.parse_padding_data(data)
+ self.data = (
+ data[padding_data_length:len(data)-self.total_padding].tobytes()
+ )
+ self.body_len = len(data)
+
+ if self.total_padding and self.total_padding >= self.body_len:
+ raise InvalidPaddingError("Padding is too long.")
+
+ @property
+ def flow_controlled_length(self):
+ """
+ The length of the frame that needs to be accounted for when considering
+ flow control.
+ """
+ padding_len = 0
+ if 'PADDED' in self.flags:
+ # Account for extra 1-byte padding length field, which is still
+ # present if possibly zero-valued.
+ padding_len = self.total_padding + 1
+ return len(self.data) + padding_len
+
+
+class PriorityFrame(Priority, Frame):
+ """
+ The PRIORITY frame specifies the sender-advised priority of a stream. It
+ can be sent at any time for an existing stream. This enables
+ reprioritisation of existing streams.
+ """
+ #: The flags defined for PRIORITY frames.
+ defined_flags = []
+
+ #: The type byte defined for PRIORITY frames.
+ type = 0x02
+
+ stream_association = _STREAM_ASSOC_HAS_STREAM
+
+ def serialize_body(self):
+ return self.serialize_priority_data()
+
+ def parse_body(self, data):
+ self.parse_priority_data(data)
+ self.body_len = len(data)
+
+
+class RstStreamFrame(Frame):
+ """
+ The RST_STREAM frame allows for abnormal termination of a stream. When sent
+ by the initiator of a stream, it indicates that they wish to cancel the
+ stream or that an error condition has occurred. When sent by the receiver
+ of a stream, it indicates that either the receiver is rejecting the stream,
+ requesting that the stream be cancelled or that an error condition has
+ occurred.
+ """
+ #: The flags defined for RST_STREAM frames.
+ defined_flags = []
+
+ #: The type byte defined for RST_STREAM frames.
+ type = 0x03
+
+ stream_association = _STREAM_ASSOC_HAS_STREAM
+
+ def __init__(self, stream_id, error_code=0, **kwargs):
+ super(RstStreamFrame, self).__init__(stream_id, **kwargs)
+
+ #: The error code used when resetting the stream.
+ self.error_code = error_code
+
+ def serialize_body(self):
+ return _STRUCT_L.pack(self.error_code)
+
+ def parse_body(self, data):
+ if len(data) != 4:
+ raise InvalidFrameError(
+ "RST_STREAM must have 4 byte body: actual length %s." %
+ len(data)
+ )
+
+ try:
+ self.error_code = _STRUCT_L.unpack(data)[0]
+ except struct.error: # pragma: no cover
+ raise InvalidFrameError("Invalid RST_STREAM body")
+
+ self.body_len = 4
+
+
+class SettingsFrame(Frame):
+ """
+ The SETTINGS frame conveys configuration parameters that affect how
+ endpoints communicate. The parameters are either constraints on peer
+ behavior or preferences.
+
+ Settings are not negotiated. Settings describe characteristics of the
+ sending peer, which are used by the receiving peer. Different values for
+ the same setting can be advertised by each peer. For example, a client
+ might set a high initial flow control window, whereas a server might set a
+ lower value to conserve resources.
+ """
+ #: The flags defined for SETTINGS frames.
+ defined_flags = [Flag('ACK', 0x01)]
+
+ #: The type byte defined for SETTINGS frames.
+ type = 0x04
+
+ stream_association = _STREAM_ASSOC_NO_STREAM
+
+ # We need to define the known settings, they may as well be class
+ # attributes.
+ #: The byte that signals the SETTINGS_HEADER_TABLE_SIZE setting.
+ HEADER_TABLE_SIZE = 0x01
+ #: The byte that signals the SETTINGS_ENABLE_PUSH setting.
+ ENABLE_PUSH = 0x02
+ #: The byte that signals the SETTINGS_MAX_CONCURRENT_STREAMS setting.
+ MAX_CONCURRENT_STREAMS = 0x03
+ #: The byte that signals the SETTINGS_INITIAL_WINDOW_SIZE setting.
+ INITIAL_WINDOW_SIZE = 0x04
+ #: The byte that signals the SETTINGS_MAX_FRAME_SIZE setting.
+ MAX_FRAME_SIZE = 0x05
+ #: The byte that signals the SETTINGS_MAX_HEADER_LIST_SIZE setting.
+ MAX_HEADER_LIST_SIZE = 0x06
+ #: The byte that signals SETTINGS_ENABLE_CONNECT_PROTOCOL setting.
+ ENABLE_CONNECT_PROTOCOL = 0x08
+
+ def __init__(self, stream_id=0, settings=None, **kwargs):
+ super(SettingsFrame, self).__init__(stream_id, **kwargs)
+
+ if settings and "ACK" in kwargs.get("flags", ()):
+ raise ValueError("Settings must be empty if ACK flag is set.")
+
+ #: A dictionary of the setting type byte to the value of the setting.
+ self.settings = settings or {}
+
+ def serialize_body(self):
+ return b''.join([_STRUCT_HL.pack(setting & 0xFF, value)
+ for setting, value in self.settings.items()])
+
+ def parse_body(self, data):
+ body_len = 0
+ for i in range(0, len(data), 6):
+ try:
+ name, value = _STRUCT_HL.unpack(data[i:i+6])
+ except struct.error:
+ raise InvalidFrameError("Invalid SETTINGS body")
+
+ self.settings[name] = value
+ body_len += 6
+
+ self.body_len = body_len
+
+
+class PushPromiseFrame(Padding, Frame):
+ """
+ The PUSH_PROMISE frame is used to notify the peer endpoint in advance of
+ streams the sender intends to initiate.
+ """
+ #: The flags defined for PUSH_PROMISE frames.
+ defined_flags = [
+ Flag('END_HEADERS', 0x04),
+ Flag('PADDED', 0x08)
+ ]
+
+ #: The type byte defined for PUSH_PROMISE frames.
+ type = 0x05
+
+ stream_association = _STREAM_ASSOC_HAS_STREAM
+
+ def __init__(self, stream_id, promised_stream_id=0, data=b'', **kwargs):
+ super(PushPromiseFrame, self).__init__(stream_id, **kwargs)
+
+ #: The stream ID that is promised by this frame.
+ self.promised_stream_id = promised_stream_id
+
+ #: The HPACK-encoded header block for the simulated request on the new
+ #: stream.
+ self.data = data
+
+ def serialize_body(self):
+ padding_data = self.serialize_padding_data()
+ padding = b'\0' * self.total_padding
+ data = _STRUCT_L.pack(self.promised_stream_id)
+ return b''.join([padding_data, data, self.data, padding])
+
+ def parse_body(self, data):
+ padding_data_length = self.parse_padding_data(data)
+
+ try:
+ self.promised_stream_id = _STRUCT_L.unpack(
+ data[padding_data_length:padding_data_length + 4]
+ )[0]
+ except struct.error:
+ raise InvalidFrameError("Invalid PUSH_PROMISE body")
+
+ self.data = data[padding_data_length + 4:].tobytes()
+ self.body_len = len(data)
+
+ if self.total_padding and self.total_padding >= self.body_len:
+ raise InvalidPaddingError("Padding is too long.")
+
+
+class PingFrame(Frame):
+ """
+ The PING frame is a mechanism for measuring a minimal round-trip time from
+ the sender, as well as determining whether an idle connection is still
+ functional. PING frames can be sent from any endpoint.
+ """
+ #: The flags defined for PING frames.
+ defined_flags = [Flag('ACK', 0x01)]
+
+ #: The type byte defined for PING frames.
+ type = 0x06
+
+ stream_association = _STREAM_ASSOC_NO_STREAM
+
+ def __init__(self, stream_id=0, opaque_data=b'', **kwargs):
+ super(PingFrame, self).__init__(stream_id, **kwargs)
+
+ #: The opaque data sent in this PING frame, as a bytestring.
+ self.opaque_data = opaque_data
+
+ def serialize_body(self):
+ if len(self.opaque_data) > 8:
+ raise InvalidFrameError(
+ "PING frame may not have more than 8 bytes of data, got %s" %
+ self.opaque_data
+ )
+
+ data = self.opaque_data
+ data += b'\x00' * (8 - len(self.opaque_data))
+ return data
+
+ def parse_body(self, data):
+ if len(data) != 8:
+ raise InvalidFrameError(
+ "PING frame must have 8 byte length: got %s" % len(data)
+ )
+
+ self.opaque_data = data.tobytes()
+ self.body_len = 8
+
+
+class GoAwayFrame(Frame):
+ """
+ The GOAWAY frame informs the remote peer to stop creating streams on this
+ connection. It can be sent from the client or the server. Once sent, the
+ sender will ignore frames sent on new streams for the remainder of the
+ connection.
+ """
+ #: The flags defined for GOAWAY frames.
+ defined_flags = []
+
+ #: The type byte defined for GOAWAY frames.
+ type = 0x07
+
+ stream_association = _STREAM_ASSOC_NO_STREAM
+
+ def __init__(self,
+ stream_id=0,
+ last_stream_id=0,
+ error_code=0,
+ additional_data=b'',
+ **kwargs):
+ super(GoAwayFrame, self).__init__(stream_id, **kwargs)
+
+ #: The last stream ID definitely seen by the remote peer.
+ self.last_stream_id = last_stream_id
+
+ #: The error code for connection teardown.
+ self.error_code = error_code
+
+ #: Any additional data sent in the GOAWAY.
+ self.additional_data = additional_data
+
+ def serialize_body(self):
+ data = _STRUCT_LL.pack(
+ self.last_stream_id & 0x7FFFFFFF,
+ self.error_code
+ )
+ data += self.additional_data
+
+ return data
+
+ def parse_body(self, data):
+ try:
+ self.last_stream_id, self.error_code = _STRUCT_LL.unpack(
+ data[:8]
+ )
+ except struct.error:
+ raise InvalidFrameError("Invalid GOAWAY body.")
+
+ self.body_len = len(data)
+
+ if len(data) > 8:
+ self.additional_data = data[8:].tobytes()
+
+
+class WindowUpdateFrame(Frame):
+ """
+ The WINDOW_UPDATE frame is used to implement flow control.
+
+ Flow control operates at two levels: on each individual stream and on the
+ entire connection.
+
+ Both types of flow control are hop by hop; that is, only between the two
+ endpoints. Intermediaries do not forward WINDOW_UPDATE frames between
+ dependent connections. However, throttling of data transfer by any receiver
+ can indirectly cause the propagation of flow control information toward the
+ original sender.
+ """
+ #: The flags defined for WINDOW_UPDATE frames.
+ defined_flags = []
+
+ #: The type byte defined for WINDOW_UPDATE frames.
+ type = 0x08
+
+ stream_association = _STREAM_ASSOC_EITHER
+
+ def __init__(self, stream_id, window_increment=0, **kwargs):
+ super(WindowUpdateFrame, self).__init__(stream_id, **kwargs)
+
+ #: The amount the flow control window is to be incremented.
+ self.window_increment = window_increment
+
+ def serialize_body(self):
+ return _STRUCT_L.pack(self.window_increment & 0x7FFFFFFF)
+
+ def parse_body(self, data):
+ try:
+ self.window_increment = _STRUCT_L.unpack(data)[0]
+ except struct.error:
+ raise InvalidFrameError("Invalid WINDOW_UPDATE body")
+
+ self.body_len = 4
+
+
+class HeadersFrame(Padding, Priority, Frame):
+ """
+ The HEADERS frame carries name-value pairs. It is used to open a stream.
+ HEADERS frames can be sent on a stream in the "open" or "half closed
+ (remote)" states.
+
+ The HeadersFrame class is actually basically a data frame in this
+ implementation, because of the requirement to control the sizes of frames.
+ A header block fragment that doesn't fit in an entire HEADERS frame needs
+ to be followed with CONTINUATION frames. From the perspective of the frame
+ building code the header block is an opaque data segment.
+ """
+ #: The flags defined for HEADERS frames.
+ defined_flags = [
+ Flag('END_STREAM', 0x01),
+ Flag('END_HEADERS', 0x04),
+ Flag('PADDED', 0x08),
+ Flag('PRIORITY', 0x20),
+ ]
+
+ #: The type byte defined for HEADERS frames.
+ type = 0x01
+
+ stream_association = _STREAM_ASSOC_HAS_STREAM
+
+ def __init__(self, stream_id, data=b'', **kwargs):
+ super(HeadersFrame, self).__init__(stream_id, **kwargs)
+
+ #: The HPACK-encoded header block.
+ self.data = data
+
+ def serialize_body(self):
+ padding_data = self.serialize_padding_data()
+ padding = b'\0' * self.total_padding
+
+ if 'PRIORITY' in self.flags:
+ priority_data = self.serialize_priority_data()
+ else:
+ priority_data = b''
+
+ return b''.join([padding_data, priority_data, self.data, padding])
+
+ def parse_body(self, data):
+ padding_data_length = self.parse_padding_data(data)
+ data = data[padding_data_length:]
+
+ if 'PRIORITY' in self.flags:
+ priority_data_length = self.parse_priority_data(data)
+ else:
+ priority_data_length = 0
+
+ self.body_len = len(data)
+ self.data = (
+ data[priority_data_length:len(data)-self.total_padding].tobytes()
+ )
+
+ if self.total_padding and self.total_padding >= self.body_len:
+ raise InvalidPaddingError("Padding is too long.")
+
+
+class ContinuationFrame(Frame):
+ """
+ The CONTINUATION frame is used to continue a sequence of header block
+ fragments. Any number of CONTINUATION frames can be sent on an existing
+ stream, as long as the preceding frame on the same stream is one of
+ HEADERS, PUSH_PROMISE or CONTINUATION without the END_HEADERS flag set.
+
+ Much like the HEADERS frame, hyper treats this as an opaque data frame with
+ different flags and a different type.
+ """
+ #: The flags defined for CONTINUATION frames.
+ defined_flags = [Flag('END_HEADERS', 0x04)]
+
+ #: The type byte defined for CONTINUATION frames.
+ type = 0x09
+
+ stream_association = _STREAM_ASSOC_HAS_STREAM
+
+ def __init__(self, stream_id, data=b'', **kwargs):
+ super(ContinuationFrame, self).__init__(stream_id, **kwargs)
+
+ #: The HPACK-encoded header block.
+ self.data = data
+
+ def serialize_body(self):
+ return self.data
+
+ def parse_body(self, data):
+ self.data = data.tobytes()
+ self.body_len = len(data)
+
+
+class AltSvcFrame(Frame):
+ """
+ The ALTSVC frame is used to advertise alternate services that the current
+ host, or a different one, can understand. This frame is standardised as
+ part of RFC 7838.
+
+ This frame does no work to validate that the ALTSVC field parameter is
+ acceptable per the rules of RFC 7838.
+
+ .. note:: If the ``stream_id`` of this frame is nonzero, the origin field
+ must have zero length. Conversely, if the ``stream_id`` of this
+ frame is zero, the origin field must have nonzero length. Put
+ another way, a valid ALTSVC frame has ``stream_id != 0`` XOR
+ ``len(origin) != 0``.
+ """
+ type = 0xA
+
+ stream_association = _STREAM_ASSOC_EITHER
+
+ def __init__(self, stream_id, origin=b'', field=b'', **kwargs):
+ super(AltSvcFrame, self).__init__(stream_id, **kwargs)
+
+ if not isinstance(origin, bytes):
+ raise ValueError("AltSvc origin must be bytestring.")
+ if not isinstance(field, bytes):
+ raise ValueError("AltSvc field must be a bytestring.")
+ self.origin = origin
+ self.field = field
+
+ def serialize_body(self):
+ origin_len = _STRUCT_H.pack(len(self.origin))
+ return b''.join([origin_len, self.origin, self.field])
+
+ def parse_body(self, data):
+ try:
+ origin_len = _STRUCT_H.unpack(data[0:2])[0]
+ self.origin = data[2:2+origin_len].tobytes()
+
+ if len(self.origin) != origin_len:
+ raise InvalidFrameError("Invalid ALTSVC frame body.")
+
+ self.field = data[2+origin_len:].tobytes()
+ except (struct.error, ValueError):
+ raise InvalidFrameError("Invalid ALTSVC frame body.")
+
+ self.body_len = len(data)
+
+
+class ExtensionFrame(Frame):
+ """
+ ExtensionFrame is used to wrap frames which are not natively interpretable
+ by hyperframe.
+
+ Although certain byte prefixes are ordained by specification to have
+ certain contextual meanings, frames with other prefixes are not prohibited,
+ and may be used to communicate arbitrary meaning between HTTP/2 peers.
+
+ Thus, hyperframe, rather than raising an exception when such a frame is
+ encountered, wraps it in a generic frame to be properly acted upon by
+ upstream consumers which might have additional context on how to use it.
+
+ .. versionadded:: 5.0.0
+ """
+
+ stream_association = _STREAM_ASSOC_EITHER
+
+ def __init__(self, type, stream_id, **kwargs):
+ super(ExtensionFrame, self).__init__(stream_id, **kwargs)
+ self.type = type
+ self.flag_byte = None
+
+ def parse_flags(self, flag_byte):
+ """
+ For extension frames, we parse the flags by just storing a flag byte.
+ """
+ self.flag_byte = flag_byte
+
+ def parse_body(self, data):
+ self.body = data.tobytes()
+ self.body_len = len(data)
+
+ def serialize(self):
+ """
+ A broad override of the serialize method that ensures that the data
+ comes back out exactly as it came in. This should not be used in most
+ user code: it exists only as a helper method if frames need to be
+ reconstituted.
+ """
+ # Build the frame header.
+ # First, get the flags.
+ flags = self.flag_byte
+
+ header = _STRUCT_HBBBL.pack(
+ (self.body_len >> 8) & 0xFFFF, # Length spread over top 24 bits
+ self.body_len & 0xFF,
+ self.type,
+ flags,
+ self.stream_id & 0x7FFFFFFF # Stream ID is 32 bits.
+ )
+
+ return header + self.body
+
+
+_FRAME_CLASSES = [
+ DataFrame,
+ HeadersFrame,
+ PriorityFrame,
+ RstStreamFrame,
+ SettingsFrame,
+ PushPromiseFrame,
+ PingFrame,
+ GoAwayFrame,
+ WindowUpdateFrame,
+ ContinuationFrame,
+ AltSvcFrame,
+]
+#: FRAMES maps the type byte for each frame to the class used to represent that
+#: frame.
+FRAMES = {cls.type: cls for cls in _FRAME_CLASSES}
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/setup.cfg b/testing/web-platform/tests/tools/third_party/hyperframe/setup.cfg
new file mode 100644
index 0000000000..50220e135b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/setup.cfg
@@ -0,0 +1,10 @@
+[wheel]
+universal = 1
+
+[tool:pytest]
+testpaths = test
+
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/setup.py b/testing/web-platform/tests/tools/third_party/hyperframe/setup.py
new file mode 100644
index 0000000000..1ab7212141
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/setup.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import itertools
+import os
+import re
+import sys
+
+try:
+ from setuptools import setup
+except ImportError:
+ from distutils.core import setup
+
+# Get the version
+version_regex = r'__version__ = ["\']([^"\']*)["\']'
+with open('hyperframe/__init__.py', 'r') as f:
+ text = f.read()
+ match = re.search(version_regex, text)
+
+ if match:
+ version = match.group(1)
+ else:
+ raise RuntimeError("No version number found!")
+
+# Stealing this from Kenneth Reitz
+if sys.argv[-1] == 'publish':
+ os.system('python setup.py sdist upload')
+ sys.exit()
+
+
+packages = ['hyperframe']
+
+setup(
+ name='hyperframe',
+ version=version,
+ description='HTTP/2 framing layer for Python',
+ long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(),
+ author='Cory Benfield',
+ author_email='cory@lukasa.co.uk',
+ url='https://python-hyper.org/hyperframe/en/latest/',
+ packages=packages,
+ package_data={'': ['LICENSE', 'README.rst', 'CONTRIBUTORS.rst', 'HISTORY.rst']},
+ package_dir={'hyperframe': 'hyperframe'},
+ include_package_data=True,
+ license='MIT License',
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ ],
+)
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/test/test_flags.py b/testing/web-platform/tests/tools/third_party/hyperframe/test/test_flags.py
new file mode 100644
index 0000000000..62a6a30f67
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/test/test_flags.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+from hyperframe.frame import (
+ Flags, Flag,
+)
+import pytest
+
+
+class TestFlags(object):
+ def test_add(self):
+ flags = Flags([Flag("VALID_FLAG", 0x00)])
+ assert not flags
+
+ flags.add("VALID_FLAG")
+ flags.add("VALID_FLAG")
+ assert "VALID_FLAG" in flags
+ assert list(flags) == ["VALID_FLAG"]
+ assert len(flags) == 1
+
+ def test_remove(self):
+ flags = Flags([Flag("VALID_FLAG", 0x00)])
+ flags.add("VALID_FLAG")
+
+ flags.discard("VALID_FLAG")
+ assert "VALID_FLAG" not in flags
+ assert list(flags) == []
+ assert len(flags) == 0
+
+ # discarding elements not in the set should not throw an exception
+ flags.discard("END_STREAM")
+
+ def test_validation(self):
+ flags = Flags([Flag("VALID_FLAG", 0x00)])
+ flags.add("VALID_FLAG")
+ with pytest.raises(ValueError):
+ flags.add("INVALID_FLAG")
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/test/test_frames.py b/testing/web-platform/tests/tools/third_party/hyperframe/test/test_frames.py
new file mode 100644
index 0000000000..abfecb692d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/hyperframe/test/test_frames.py
@@ -0,0 +1,791 @@
+# -*- coding: utf-8 -*-
+from hyperframe.frame import (
+ Frame, Flags, DataFrame, PriorityFrame, RstStreamFrame, SettingsFrame,
+ PushPromiseFrame, PingFrame, GoAwayFrame, WindowUpdateFrame, HeadersFrame,
+ ContinuationFrame, AltSvcFrame, ExtensionFrame
+)
+from hyperframe.exceptions import (
+ UnknownFrameError, InvalidPaddingError, InvalidFrameError
+)
+import pytest
+
+
+def decode_frame(frame_data):
+ f, length = Frame.parse_frame_header(frame_data[:9])
+ f.parse_body(memoryview(frame_data[9:9 + length]))
+ assert 9 + length == len(frame_data)
+ return f
+
+
+class TestGeneralFrameBehaviour(object):
+ def test_base_frame_ignores_flags(self):
+ f = Frame(stream_id=0)
+ flags = f.parse_flags(0xFF)
+ assert not flags
+ assert isinstance(flags, Flags)
+
+ def test_base_frame_cant_serialize(self):
+ f = Frame(stream_id=0)
+ with pytest.raises(NotImplementedError):
+ f.serialize()
+
+ def test_base_frame_cant_parse_body(self):
+ data = b''
+ f = Frame(stream_id=0)
+ with pytest.raises(NotImplementedError):
+ f.parse_body(data)
+
+ def test_parse_frame_header_unknown_type_strict(self):
+ with pytest.raises(UnknownFrameError) as excinfo:
+ Frame.parse_frame_header(
+ b'\x00\x00\x59\xFF\x00\x00\x00\x00\x01',
+ strict=True
+ )
+ exception = excinfo.value
+ assert exception.frame_type == 0xFF
+ assert exception.length == 0x59
+ assert str(exception) == (
+ "UnknownFrameError: Unknown frame type 0xFF received, "
+ "length 89 bytes"
+ )
+
+ def test_parse_frame_header_ignore_first_bit_of_stream_id(self):
+ s = b'\x00\x00\x00\x06\x01\x80\x00\x00\x00'
+ f, _ = Frame.parse_frame_header(s)
+
+ assert f.stream_id == 0
+
+ def test_parse_frame_header_unknown_type(self):
+ frame, length = Frame.parse_frame_header(
+ b'\x00\x00\x59\xFF\x00\x00\x00\x00\x01'
+ )
+ assert frame.type == 0xFF
+ assert length == 0x59
+ assert isinstance(frame, ExtensionFrame)
+ assert frame.stream_id == 1
+
+ def test_flags_are_persisted(self):
+ frame, length = Frame.parse_frame_header(
+ b'\x00\x00\x59\xFF\x09\x00\x00\x00\x01'
+ )
+ assert frame.type == 0xFF
+ assert length == 0x59
+ assert frame.flag_byte == 0x09
+
+ def test_parse_body_unknown_type(self):
+ frame = decode_frame(
+ b'\x00\x00\x0C\xFF\x00\x00\x00\x00\x01hello world!'
+ )
+ assert frame.body == b'hello world!'
+ assert frame.body_len == 12
+ assert frame.stream_id == 1
+
+ def test_can_round_trip_unknown_frames(self):
+ frame_data = b'\x00\x00\x0C\xFF\x00\x00\x00\x00\x01hello world!'
+ f = decode_frame(frame_data)
+ assert f.serialize() == frame_data
+
+ def test_repr(self, monkeypatch):
+ f = Frame(stream_id=0)
+ monkeypatch.setattr(Frame, "serialize_body", lambda _: b"body")
+ assert repr(f) == "Frame(Stream: 0; Flags: None): 626f6479"
+
+ monkeypatch.setattr(Frame, "serialize_body", lambda _: b"A"*25)
+ assert repr(f) == (
+ "Frame(Stream: 0; Flags: None): {}...".format("41"*10)
+ )
+
+ def test_cannot_parse_invalid_frame_header(self):
+ with pytest.raises(InvalidFrameError):
+ Frame.parse_frame_header(b'\x00\x00\x08\x00\x01\x00\x00\x00')
+
+
+class TestDataFrame(object):
+ payload = b'\x00\x00\x08\x00\x01\x00\x00\x00\x01testdata'
+ payload_with_padding = (
+ b'\x00\x00\x13\x00\x09\x00\x00\x00\x01\x0Atestdata' + b'\0' * 10
+ )
+
+ def test_data_frame_has_correct_flags(self):
+ f = DataFrame(1)
+ flags = f.parse_flags(0xFF)
+ assert flags == set([
+ 'END_STREAM', 'PADDED'
+ ])
+
+ @pytest.mark.parametrize('data', [
+ b'testdata',
+ memoryview(b'testdata')
+ ])
+ def test_data_frame_serializes_properly(self, data):
+ f = DataFrame(1)
+ f.flags = set(['END_STREAM'])
+ f.data = data
+
+ s = f.serialize()
+ assert s == self.payload
+
+ def test_data_frame_with_padding_serializes_properly(self):
+ f = DataFrame(1)
+ f.flags = set(['END_STREAM', 'PADDED'])
+ f.data = b'testdata'
+ f.pad_length = 10
+
+ s = f.serialize()
+ assert s == self.payload_with_padding
+
+ def test_data_frame_parses_properly(self):
+ f = decode_frame(self.payload)
+
+ assert isinstance(f, DataFrame)
+ assert f.flags == set(['END_STREAM'])
+ assert f.pad_length == 0
+ assert f.data == b'testdata'
+ assert f.body_len == 8
+
+ def test_data_frame_with_padding_parses_properly(self):
+ f = decode_frame(self.payload_with_padding)
+
+ assert isinstance(f, DataFrame)
+ assert f.flags == set(['END_STREAM', 'PADDED'])
+ assert f.pad_length == 10
+ assert f.data == b'testdata'
+ assert f.body_len == 19
+
+ def test_data_frame_with_invalid_padding_errors(self):
+ with pytest.raises(InvalidFrameError):
+ decode_frame(self.payload_with_padding[:9])
+
+ def test_data_frame_with_padding_calculates_flow_control_len(self):
+ f = DataFrame(1)
+ f.flags = set(['PADDED'])
+ f.data = b'testdata'
+ f.pad_length = 10
+
+ assert f.flow_controlled_length == 19
+
+ def test_data_frame_zero_length_padding_calculates_flow_control_len(self):
+ f = DataFrame(1)
+ f.flags = set(['PADDED'])
+ f.data = b'testdata'
+ f.pad_length = 0
+
+ assert f.flow_controlled_length == len(b'testdata') + 1
+
+ def test_data_frame_without_padding_calculates_flow_control_len(self):
+ f = DataFrame(1)
+ f.data = b'testdata'
+
+ assert f.flow_controlled_length == 8
+
+ def test_data_frame_comes_on_a_stream(self):
+ with pytest.raises(ValueError):
+ DataFrame(0)
+
+ def test_long_data_frame(self):
+ f = DataFrame(1)
+
+ # Use more than 256 bytes of data to force setting higher bits.
+ f.data = b'\x01' * 300
+ data = f.serialize()
+
+ # The top three bytes should be numerically equal to 300. That means
+ # they should read 00 01 2C.
+ # The weird double index trick is to ensure this test behaves equally
+ # on Python 2 and Python 3.
+ assert data[0] == b'\x00'[0]
+ assert data[1] == b'\x01'[0]
+ assert data[2] == b'\x2C'[0]
+
+ def test_body_length_behaves_correctly(self):
+ f = DataFrame(1)
+
+ f.data = b'\x01' * 300
+
+ # Initially the body length is zero. For now this is incidental, but
+ # I'm going to test it to ensure that the behaviour is codified. We
+ # should change this test if we change that.
+ assert f.body_len == 0
+
+ f.serialize()
+ assert f.body_len == 300
+
+ def test_data_frame_with_invalid_padding_fails_to_parse(self):
+ # This frame has a padding length of 6 bytes, but a total length of
+ # only 5.
+ data = b'\x00\x00\x05\x00\x0b\x00\x00\x00\x01\x06\x54\x65\x73\x74'
+
+ with pytest.raises(InvalidPaddingError):
+ decode_frame(data)
+
+ def test_data_frame_with_no_length_parses(self):
+ # Fixes issue with empty data frames raising InvalidPaddingError.
+ f = DataFrame(1)
+ f.data = b''
+ data = f.serialize()
+
+ new_frame = decode_frame(data)
+ assert new_frame.data == b''
+
+
+class TestPriorityFrame(object):
+ payload = b'\x00\x00\x05\x02\x00\x00\x00\x00\x01\x80\x00\x00\x04\x40'
+
+ def test_priority_frame_has_no_flags(self):
+ f = PriorityFrame(1)
+ flags = f.parse_flags(0xFF)
+ assert flags == set()
+ assert isinstance(flags, Flags)
+
+ def test_priority_frame_default_serializes_properly(self):
+ f = PriorityFrame(1)
+
+ assert f.serialize() == (
+ b'\x00\x00\x05\x02\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00'
+ )
+
+ def test_priority_frame_with_all_data_serializes_properly(self):
+ f = PriorityFrame(1)
+ f.depends_on = 0x04
+ f.stream_weight = 64
+ f.exclusive = True
+
+ assert f.serialize() == self.payload
+
+ def test_priority_frame_with_all_data_parses_properly(self):
+ f = decode_frame(self.payload)
+
+ assert isinstance(f, PriorityFrame)
+ assert f.flags == set()
+ assert f.depends_on == 4
+ assert f.stream_weight == 64
+ assert f.exclusive is True
+ assert f.body_len == 5
+
+ def test_priority_frame_comes_on_a_stream(self):
+ with pytest.raises(ValueError):
+ PriorityFrame(0)
+
+ def test_short_priority_frame_errors(self):
+ with pytest.raises(InvalidFrameError):
+ decode_frame(self.payload[:-2])
+
+
+class TestRstStreamFrame(object):
+ def test_rst_stream_frame_has_no_flags(self):
+ f = RstStreamFrame(1)
+ flags = f.parse_flags(0xFF)
+ assert not flags
+ assert isinstance(flags, Flags)
+
+ def test_rst_stream_frame_serializes_properly(self):
+ f = RstStreamFrame(1)
+ f.error_code = 420
+
+ s = f.serialize()
+ assert s == b'\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x00\x01\xa4'
+
+ def test_rst_stream_frame_parses_properly(self):
+ s = b'\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x00\x01\xa4'
+ f = decode_frame(s)
+
+ assert isinstance(f, RstStreamFrame)
+ assert f.flags == set()
+ assert f.error_code == 420
+ assert f.body_len == 4
+
+ def test_rst_stream_frame_comes_on_a_stream(self):
+ with pytest.raises(ValueError):
+ RstStreamFrame(0)
+
+ def test_rst_stream_frame_must_have_body_length_four(self):
+ f = RstStreamFrame(1)
+ with pytest.raises(ValueError):
+ f.parse_body(b'\x01')
+
+
+class TestSettingsFrame(object):
+ serialized = (
+ b'\x00\x00\x2A\x04\x01\x00\x00\x00\x00' + # Frame header
+ b'\x00\x01\x00\x00\x10\x00' + # HEADER_TABLE_SIZE
+ b'\x00\x02\x00\x00\x00\x00' + # ENABLE_PUSH
+ b'\x00\x03\x00\x00\x00\x64' + # MAX_CONCURRENT_STREAMS
+ b'\x00\x04\x00\x00\xFF\xFF' + # INITIAL_WINDOW_SIZE
+ b'\x00\x05\x00\x00\x40\x00' + # MAX_FRAME_SIZE
+ b'\x00\x06\x00\x00\xFF\xFF' + # MAX_HEADER_LIST_SIZE
+ b'\x00\x08\x00\x00\x00\x01' # ENABLE_CONNECT_PROTOCOL
+ )
+
+ settings = {
+ SettingsFrame.HEADER_TABLE_SIZE: 4096,
+ SettingsFrame.ENABLE_PUSH: 0,
+ SettingsFrame.MAX_CONCURRENT_STREAMS: 100,
+ SettingsFrame.INITIAL_WINDOW_SIZE: 65535,
+ SettingsFrame.MAX_FRAME_SIZE: 16384,
+ SettingsFrame.MAX_HEADER_LIST_SIZE: 65535,
+ SettingsFrame.ENABLE_CONNECT_PROTOCOL: 1,
+ }
+
+ def test_settings_frame_has_only_one_flag(self):
+ f = SettingsFrame()
+ flags = f.parse_flags(0xFF)
+ assert flags == set(['ACK'])
+
+ def test_settings_frame_serializes_properly(self):
+ f = SettingsFrame()
+ f.parse_flags(0xFF)
+ f.settings = self.settings
+
+ s = f.serialize()
+ assert s == self.serialized
+
+ def test_settings_frame_with_settings(self):
+ f = SettingsFrame(settings=self.settings)
+ assert f.settings == self.settings
+
+ def test_settings_frame_without_settings(self):
+ f = SettingsFrame()
+ assert f.settings == {}
+
+ def test_settings_frame_with_ack(self):
+ f = SettingsFrame(flags=('ACK',))
+ assert 'ACK' in f.flags
+
+ def test_settings_frame_ack_and_settings(self):
+ with pytest.raises(ValueError):
+ SettingsFrame(settings=self.settings, flags=('ACK',))
+
+ def test_settings_frame_parses_properly(self):
+ f = decode_frame(self.serialized)
+
+ assert isinstance(f, SettingsFrame)
+ assert f.flags == set(['ACK'])
+ assert f.settings == self.settings
+ assert f.body_len == 42
+
+ def test_settings_frames_never_have_streams(self):
+ with pytest.raises(ValueError):
+ SettingsFrame(stream_id=1)
+
+ def test_short_settings_frame_errors(self):
+ with pytest.raises(InvalidFrameError):
+ decode_frame(self.serialized[:-2])
+
+
+class TestPushPromiseFrame(object):
+ def test_push_promise_frame_flags(self):
+ f = PushPromiseFrame(1)
+ flags = f.parse_flags(0xFF)
+
+ assert flags == set(['END_HEADERS', 'PADDED'])
+
+ def test_push_promise_frame_serializes_properly(self):
+ f = PushPromiseFrame(1)
+ f.flags = set(['END_HEADERS'])
+ f.promised_stream_id = 4
+ f.data = b'hello world'
+
+ s = f.serialize()
+ assert s == (
+ b'\x00\x00\x0F\x05\x04\x00\x00\x00\x01' +
+ b'\x00\x00\x00\x04' +
+ b'hello world'
+ )
+
+ def test_push_promise_frame_parses_properly(self):
+ s = (
+ b'\x00\x00\x0F\x05\x04\x00\x00\x00\x01' +
+ b'\x00\x00\x00\x04' +
+ b'hello world'
+ )
+ f = decode_frame(s)
+
+ assert isinstance(f, PushPromiseFrame)
+ assert f.flags == set(['END_HEADERS'])
+ assert f.promised_stream_id == 4
+ assert f.data == b'hello world'
+ assert f.body_len == 15
+
+ def test_push_promise_frame_with_invalid_padding_fails_to_parse(self):
+ # This frame has a padding length of 6 bytes, but a total length of
+ # only 5.
+ data = b'\x00\x00\x05\x05\x08\x00\x00\x00\x01\x06\x54\x65\x73\x74'
+
+ with pytest.raises(InvalidPaddingError):
+ decode_frame(data)
+
+ def test_push_promise_frame_with_no_length_parses(self):
+ # Fixes issue with empty data frames raising InvalidPaddingError.
+ f = PushPromiseFrame(1)
+ f.data = b''
+ data = f.serialize()
+
+ new_frame = decode_frame(data)
+ assert new_frame.data == b''
+
+ def test_short_push_promise_errors(self):
+ s = (
+ b'\x00\x00\x0F\x05\x04\x00\x00\x00\x01' +
+ b'\x00\x00\x00' # One byte short
+ )
+
+ with pytest.raises(InvalidFrameError):
+ decode_frame(s)
+
+
+class TestPingFrame(object):
+ def test_ping_frame_has_only_one_flag(self):
+ f = PingFrame()
+ flags = f.parse_flags(0xFF)
+
+ assert flags == set(['ACK'])
+
+ def test_ping_frame_serializes_properly(self):
+ f = PingFrame()
+ f.parse_flags(0xFF)
+ f.opaque_data = b'\x01\x02'
+
+ s = f.serialize()
+ assert s == (
+ b'\x00\x00\x08\x06\x01\x00\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00'
+ b'\x00'
+ )
+
+ def test_no_more_than_8_octets(self):
+ f = PingFrame()
+ f.opaque_data = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09'
+
+ with pytest.raises(ValueError):
+ f.serialize()
+
+ def test_ping_frame_parses_properly(self):
+ s = (
+ b'\x00\x00\x08\x06\x01\x00\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00'
+ b'\x00'
+ )
+ f = decode_frame(s)
+
+ assert isinstance(f, PingFrame)
+ assert f.flags == set(['ACK'])
+ assert f.opaque_data == b'\x01\x02\x00\x00\x00\x00\x00\x00'
+ assert f.body_len == 8
+
+ def test_ping_frame_never_has_a_stream(self):
+ with pytest.raises(ValueError):
+ PingFrame(stream_id=1)
+
+ def test_ping_frame_has_no_more_than_body_length_8(self):
+ f = PingFrame()
+ with pytest.raises(ValueError):
+ f.parse_body(b'\x01\x02\x03\x04\x05\x06\x07\x08\x09')
+
+ def test_ping_frame_has_no_less_than_body_length_8(self):
+ f = PingFrame()
+ with pytest.raises(ValueError):
+ f.parse_body(b'\x01\x02\x03\x04\x05\x06\x07')
+
+
+class TestGoAwayFrame(object):
+ def test_go_away_has_no_flags(self):
+ f = GoAwayFrame()
+ flags = f.parse_flags(0xFF)
+
+ assert not flags
+ assert isinstance(flags, Flags)
+
+ def test_goaway_serializes_properly(self):
+ f = GoAwayFrame()
+ f.last_stream_id = 64
+ f.error_code = 32
+ f.additional_data = b'hello'
+
+ s = f.serialize()
+ assert s == (
+ b'\x00\x00\x0D\x07\x00\x00\x00\x00\x00' + # Frame header
+ b'\x00\x00\x00\x40' + # Last Stream ID
+ b'\x00\x00\x00\x20' + # Error Code
+ b'hello' # Additional data
+ )
+
+ def test_goaway_frame_parses_properly(self):
+ s = (
+ b'\x00\x00\x0D\x07\x00\x00\x00\x00\x00' + # Frame header
+ b'\x00\x00\x00\x40' + # Last Stream ID
+ b'\x00\x00\x00\x20' + # Error Code
+ b'hello' # Additional data
+ )
+ f = decode_frame(s)
+
+ assert isinstance(f, GoAwayFrame)
+ assert f.flags == set()
+ assert f.additional_data == b'hello'
+ assert f.body_len == 13
+
+ s = (
+ b'\x00\x00\x08\x07\x00\x00\x00\x00\x00' + # Frame header
+ b'\x00\x00\x00\x40' + # Last Stream ID
+ b'\x00\x00\x00\x20' + # Error Code
+ b'' # Additional data
+ )
+ f = decode_frame(s)
+
+ assert isinstance(f, GoAwayFrame)
+ assert f.flags == set()
+ assert f.additional_data == b''
+ assert f.body_len == 8
+
+ def test_goaway_frame_never_has_a_stream(self):
+ with pytest.raises(ValueError):
+ GoAwayFrame(stream_id=1)
+
+ def test_short_goaway_frame_errors(self):
+ s = (
+ b'\x00\x00\x0D\x07\x00\x00\x00\x00\x00' + # Frame header
+ b'\x00\x00\x00\x40' + # Last Stream ID
+ b'\x00\x00\x00' # short Error Code
+ )
+ with pytest.raises(InvalidFrameError):
+ decode_frame(s)
+
+
+class TestWindowUpdateFrame(object):
+ def test_window_update_has_no_flags(self):
+ f = WindowUpdateFrame(0)
+ flags = f.parse_flags(0xFF)
+
+ assert not flags
+ assert isinstance(flags, Flags)
+
+ def test_window_update_serializes_properly(self):
+ f = WindowUpdateFrame(0)
+ f.window_increment = 512
+
+ s = f.serialize()
+ assert s == b'\x00\x00\x04\x08\x00\x00\x00\x00\x00\x00\x00\x02\x00'
+
+ def test_windowupdate_frame_parses_properly(self):
+ s = b'\x00\x00\x04\x08\x00\x00\x00\x00\x00\x00\x00\x02\x00'
+ f = decode_frame(s)
+
+ assert isinstance(f, WindowUpdateFrame)
+ assert f.flags == set()
+ assert f.window_increment == 512
+ assert f.body_len == 4
+
+ def test_short_windowupdate_frame_errors(self):
+ s = b'\x00\x00\x04\x08\x00\x00\x00\x00\x00\x00\x00\x02' # -1 byte
+
+ with pytest.raises(InvalidFrameError):
+ decode_frame(s)
+
+
+class TestHeadersFrame(object):
+ def test_headers_frame_flags(self):
+ f = HeadersFrame(1)
+ flags = f.parse_flags(0xFF)
+
+ assert flags == set(['END_STREAM', 'END_HEADERS',
+ 'PADDED', 'PRIORITY'])
+
+ def test_headers_frame_serializes_properly(self):
+ f = HeadersFrame(1)
+ f.flags = set(['END_STREAM', 'END_HEADERS'])
+ f.data = b'hello world'
+
+ s = f.serialize()
+ assert s == (
+ b'\x00\x00\x0B\x01\x05\x00\x00\x00\x01' +
+ b'hello world'
+ )
+
+ def test_headers_frame_parses_properly(self):
+ s = (
+ b'\x00\x00\x0B\x01\x05\x00\x00\x00\x01' +
+ b'hello world'
+ )
+ f = decode_frame(s)
+
+ assert isinstance(f, HeadersFrame)
+ assert f.flags == set(['END_STREAM', 'END_HEADERS'])
+ assert f.data == b'hello world'
+ assert f.body_len == 11
+
+ def test_headers_frame_with_priority_parses_properly(self):
+ # This test also tests that we can receive a HEADERS frame with no
+ # actual headers on it. This is technically possible.
+ s = (
+ b'\x00\x00\x05\x01\x20\x00\x00\x00\x01' +
+ b'\x80\x00\x00\x04\x40'
+ )
+ f = decode_frame(s)
+
+ assert isinstance(f, HeadersFrame)
+ assert f.flags == set(['PRIORITY'])
+ assert f.data == b''
+ assert f.depends_on == 4
+ assert f.stream_weight == 64
+ assert f.exclusive is True
+ assert f.body_len == 5
+
+ def test_headers_frame_with_priority_serializes_properly(self):
+ # This test also tests that we can receive a HEADERS frame with no
+ # actual headers on it. This is technically possible.
+ s = (
+ b'\x00\x00\x05\x01\x20\x00\x00\x00\x01' +
+ b'\x80\x00\x00\x04\x40'
+ )
+ f = HeadersFrame(1)
+ f.flags = set(['PRIORITY'])
+ f.data = b''
+ f.depends_on = 4
+ f.stream_weight = 64
+ f.exclusive = True
+
+ assert f.serialize() == s
+
+ def test_headers_frame_with_invalid_padding_fails_to_parse(self):
+ # This frame has a padding length of 6 bytes, but a total length of
+ # only 5.
+ data = b'\x00\x00\x05\x01\x08\x00\x00\x00\x01\x06\x54\x65\x73\x74'
+
+ with pytest.raises(InvalidPaddingError):
+ decode_frame(data)
+
+ def test_headers_frame_with_no_length_parses(self):
+ # Fixes issue with empty data frames raising InvalidPaddingError.
+ f = HeadersFrame(1)
+ f.data = b''
+ data = f.serialize()
+
+ new_frame = decode_frame(data)
+ assert new_frame.data == b''
+
+
+class TestContinuationFrame(object):
+ def test_continuation_frame_flags(self):
+ f = ContinuationFrame(1)
+ flags = f.parse_flags(0xFF)
+
+ assert flags == set(['END_HEADERS'])
+
+ def test_continuation_frame_serializes(self):
+ f = ContinuationFrame(1)
+ f.parse_flags(0x04)
+ f.data = b'hello world'
+
+ s = f.serialize()
+ assert s == (
+ b'\x00\x00\x0B\x09\x04\x00\x00\x00\x01' +
+ b'hello world'
+ )
+
+ def test_continuation_frame_parses_properly(self):
+ s = b'\x00\x00\x0B\x09\x04\x00\x00\x00\x01hello world'
+ f = decode_frame(s)
+
+ assert isinstance(f, ContinuationFrame)
+ assert f.flags == set(['END_HEADERS'])
+ assert f.data == b'hello world'
+ assert f.body_len == 11
+
+
+class TestAltSvcFrame(object):
+ payload_with_origin = (
+ b'\x00\x00\x31' # Length
+ b'\x0A' # Type
+ b'\x00' # Flags
+ b'\x00\x00\x00\x00' # Stream ID
+ b'\x00\x0B' # Origin len
+ b'example.com' # Origin
+ b'h2="alt.example.com:8000", h2=":443"' # Field Value
+ )
+ payload_without_origin = (
+ b'\x00\x00\x13' # Length
+ b'\x0A' # Type
+ b'\x00' # Flags
+ b'\x00\x00\x00\x01' # Stream ID
+ b'\x00\x00' # Origin len
+ b'' # Origin
+ b'h2=":8000"; ma=60' # Field Value
+ )
+ payload_with_origin_and_stream = (
+ b'\x00\x00\x36' # Length
+ b'\x0A' # Type
+ b'\x00' # Flags
+ b'\x00\x00\x00\x01' # Stream ID
+ b'\x00\x0B' # Origin len
+ b'example.com' # Origin
+ b'Alt-Svc: h2=":443"; ma=2592000; persist=1' # Field Value
+ )
+
+ def test_altsvc_frame_flags(self):
+ f = AltSvcFrame(stream_id=0)
+ flags = f.parse_flags(0xFF)
+
+ assert flags == set()
+
+ def test_altsvc_frame_with_origin_serializes_properly(self):
+ f = AltSvcFrame(stream_id=0)
+ f.origin = b'example.com'
+ f.field = b'h2="alt.example.com:8000", h2=":443"'
+
+ s = f.serialize()
+ assert s == self.payload_with_origin
+
+ def test_altsvc_frame_with_origin_parses_properly(self):
+ f = decode_frame(self.payload_with_origin)
+
+ assert isinstance(f, AltSvcFrame)
+ assert f.origin == b'example.com'
+ assert f.field == b'h2="alt.example.com:8000", h2=":443"'
+ assert f.body_len == 49
+ assert f.stream_id == 0
+
+ def test_altsvc_frame_without_origin_serializes_properly(self):
+ f = AltSvcFrame(stream_id=1, origin=b'', field=b'h2=":8000"; ma=60')
+ s = f.serialize()
+ assert s == self.payload_without_origin
+
+ def test_altsvc_frame_without_origin_parses_properly(self):
+ f = decode_frame(self.payload_without_origin)
+
+ assert isinstance(f, AltSvcFrame)
+ assert f.origin == b''
+ assert f.field == b'h2=":8000"; ma=60'
+ assert f.body_len == 19
+ assert f.stream_id == 1
+
+ def test_altsvc_frame_without_origin_parses_with_good_repr(self):
+ f = decode_frame(self.payload_without_origin)
+
+ assert repr(f) == (
+ "AltSvcFrame(Stream: 1; Flags: None): 000068323d223a383030..."
+ )
+
+ def test_altsvc_frame_with_origin_and_stream_serializes_properly(self):
+ # This frame is not valid, but we allow it to be serialized anyway.
+ f = AltSvcFrame(stream_id=1)
+ f.origin = b'example.com'
+ f.field = b'Alt-Svc: h2=":443"; ma=2592000; persist=1'
+
+ assert f.serialize() == self.payload_with_origin_and_stream
+
+ def test_short_altsvc_frame_errors(self):
+ with pytest.raises(InvalidFrameError):
+ decode_frame(self.payload_with_origin[:12])
+
+ with pytest.raises(InvalidFrameError):
+ decode_frame(self.payload_with_origin[:10])
+
+ def test_altsvc_with_unicode_origin_fails(self):
+ with pytest.raises(ValueError):
+ AltSvcFrame(
+ stream_id=0, origin=u'hello', field=b'h2=":8000"; ma=60'
+
+ )
+
+ def test_altsvc_with_unicode_field_fails(self):
+ with pytest.raises(ValueError):
+ AltSvcFrame(
+ stream_id=0, origin=b'hello', field=u'h2=":8000"; ma=60'
+ )
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/.github/workflows/main.yml b/testing/web-platform/tests/tools/third_party/importlib_metadata/.github/workflows/main.yml
new file mode 100644
index 0000000000..b56320fd00
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/.github/workflows/main.yml
@@ -0,0 +1,126 @@
+name: Automated Tests
+
+on: [push, pull_request]
+
+jobs:
+ test:
+ strategy:
+ matrix:
+ python: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9]
+ platform: [ubuntu-latest, macos-latest, windows-latest]
+ runs-on: ${{ matrix.platform }}
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python }}
+ - name: Install tox
+ run: |
+ python -m pip install tox
+ - name: Run tests
+ run: tox
+ env:
+ TOXENV: python
+
+ qa:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.9
+ - name: Install tox
+ run: |
+ python -m pip install tox
+ - name: Run checks
+ run: tox
+ env:
+ TOXENV: qa
+
+ coverage:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.9
+ - name: Install tox
+ run: |
+ python -m pip install tox
+ - name: Evaluate coverage
+ run: tox
+ env:
+ TOXENV: cov
+
+ benchmark:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.9
+ - name: Install tox
+ run: |
+ python -m pip install tox
+ - name: Run benchmarks
+ run: tox
+ env:
+ TOXENV: perf
+
+ diffcov:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.9
+ - name: Install tox
+ run: |
+ python -m pip install tox
+ - name: Evaluate coverage
+ run: tox
+ env:
+ TOXENV: diffcov
+
+ docs:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.9
+ - name: Install tox
+ run: |
+ python -m pip install tox
+ - name: Build docs
+ run: tox
+ env:
+ TOXENV: docs
+
+ release:
+ needs: test
+ if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.9
+ - name: Install tox
+ run: |
+ python -m pip install tox
+ - name: Release
+ run: tox -e release
+ env:
+ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/.gitignore b/testing/web-platform/tests/tools/third_party/importlib_metadata/.gitignore
new file mode 100644
index 0000000000..ae864d6125
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/.gitignore
@@ -0,0 +1,13 @@
+build
+/coverage.xml
+/diffcov.html
+htmlcov
+importlib_metadata.egg-info
+.mypy_cache
+/.coverage
+/.DS_Store
+artifacts
+.eggs
+.doctrees
+dist
+pip-wheel-metadata
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/.readthedocs.yml b/testing/web-platform/tests/tools/third_party/importlib_metadata/.readthedocs.yml
new file mode 100644
index 0000000000..8ae4468428
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/.readthedocs.yml
@@ -0,0 +1,5 @@
+python:
+ version: 3
+ extra_requirements:
+ - docs
+ pip_install: true
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/LICENSE b/testing/web-platform/tests/tools/third_party/importlib_metadata/LICENSE
new file mode 100644
index 0000000000..be7e092b0b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2017-2019 Jason R. Coombs, Barry Warsaw
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/MANIFEST.in b/testing/web-platform/tests/tools/third_party/importlib_metadata/MANIFEST.in
new file mode 100644
index 0000000000..3fcf6d633a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/MANIFEST.in
@@ -0,0 +1,5 @@
+include *.py MANIFEST.in LICENSE README.rst
+global-include *.txt *.rst *.ini *.cfg *.toml *.whl *.egg
+exclude .gitignore
+prune build
+prune .tox
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/README.rst b/testing/web-platform/tests/tools/third_party/importlib_metadata/README.rst
new file mode 100644
index 0000000000..5655d9ab98
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/README.rst
@@ -0,0 +1,42 @@
+=========================
+ ``importlib_metadata``
+=========================
+
+``importlib_metadata`` is a library to access the metadata for a
+Python package.
+
+As of Python 3.8, this functionality has been added to the
+`Python standard library
+<https://docs.python.org/3/library/importlib.metadata.html>`_.
+This package supplies backports of that functionality including
+improvements added to subsequent Python versions.
+
+
+Usage
+=====
+
+See the `online documentation <https://importlib_metadata.readthedocs.io/>`_
+for usage details.
+
+`Finder authors
+<https://docs.python.org/3/reference/import.html#finders-and-loaders>`_ can
+also add support for custom package installers. See the above documentation
+for details.
+
+
+Caveats
+=======
+
+This project primarily supports third-party packages installed by PyPA
+tools (or other conforming packages). It does not support:
+
+- Packages in the stdlib.
+- Packages installed without metadata.
+
+Project details
+===============
+
+ * Project home: https://github.com/python/importlib_metadata
+ * Report bugs at: https://github.com/python/importlib_metadata/issues
+ * Code hosting: https://github.com/python/importlib_metadata
+ * Documentation: https://importlib_metadata.readthedocs.io/
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/codecov.yml b/testing/web-platform/tests/tools/third_party/importlib_metadata/codecov.yml
new file mode 100644
index 0000000000..66c7f4bd19
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/codecov.yml
@@ -0,0 +1,2 @@
+codecov:
+ token: 5eb1bc45-1b7f-43e6-8bc1-f2b02833dba9
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/coverage.ini b/testing/web-platform/tests/tools/third_party/importlib_metadata/coverage.ini
new file mode 100644
index 0000000000..b4d3102f42
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/coverage.ini
@@ -0,0 +1,24 @@
+[run]
+branch = true
+parallel = true
+omit =
+ setup*
+ .tox/*/lib/python*
+ */tests/*.py
+ */testing/*.py
+ /usr/local/*
+ */mod.py
+plugins =
+ coverplug
+
+[report]
+exclude_lines =
+ pragma: nocover
+ raise NotImplementedError
+ raise AssertionError
+ assert\s
+ nocoverpy${PYV}
+
+[paths]
+source =
+ importlib_metadata
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/coverplug.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/coverplug.py
new file mode 100644
index 0000000000..0b0c7cb549
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/coverplug.py
@@ -0,0 +1,21 @@
+"""Coverage plugin to add exclude lines based on the Python version."""
+
+import sys
+
+from coverage import CoveragePlugin
+
+
+class MyConfigPlugin(CoveragePlugin):
+ def configure(self, config):
+ opt_name = 'report:exclude_lines'
+ exclude_lines = config.get_option(opt_name)
+ # Python >= 3.6 has os.PathLike.
+ if sys.version_info >= (3, 6):
+ exclude_lines.append('pragma: >=36')
+ else:
+ exclude_lines.append('pragma: <=35')
+ config.set_option(opt_name, exclude_lines)
+
+
+def coverage_init(reg, options):
+ reg.add_configurer(MyConfigPlugin())
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/__init__.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/changelog.rst b/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/changelog.rst
new file mode 100644
index 0000000000..396535744a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/changelog.rst
@@ -0,0 +1,314 @@
+=========================
+ importlib_metadata NEWS
+=========================
+
+v2.1.0
+======
+
+* #253: When querying for package metadata, the lookup
+ now honors
+ `package normalization rules <https://packaging.python.org/specifications/recording-installed-packages/>`_.
+
+v2.0.0
+======
+
+* ``importlib_metadata`` no longer presents a
+ ``__version__`` attribute. Consumers wishing to
+ resolve the version of the package should query it
+ directly with
+ ``importlib_metadata.version('importlib-metadata')``.
+ Closes #71.
+
+v1.7.0
+======
+
+* ``PathNotFoundError`` now has a custom ``__str__``
+ mentioning "package metadata" being missing to help
+ guide users to the cause when the package is installed
+ but no metadata is present. Closes #124.
+
+v1.6.1
+======
+
+* Added ``Distribution._local()`` as a provisional
+ demonstration of how to load metadata for a local
+ package. Implicitly requires that
+ `pep517 <https://pypi.org/project/pep517>`_ is
+ installed. Ref #42.
+* Ensure inputs to FastPath are Unicode. Closes #121.
+* Tests now rely on ``importlib.resources.files`` (and
+ backport) instead of the older ``path`` function.
+* Support any iterable from ``find_distributions``.
+ Closes #122.
+
+v1.6.0
+======
+
+* Added ``module`` and ``attr`` attributes to ``EntryPoint``
+
+v1.5.2
+======
+
+* Fix redundant entries from ``FastPath.zip_children``.
+ Closes #117.
+
+v1.5.1
+======
+
+* Improve reliability and consistency of compatibility
+ imports for contextlib and pathlib when running tests.
+ Closes #116.
+
+v1.5.0
+======
+
+* Additional performance optimizations in FastPath now
+ saves an additional 20% on a typical call.
+* Correct for issue where PyOxidizer finder has no
+ ``__module__`` attribute. Closes #110.
+
+v1.4.0
+======
+
+* Through careful optimization, ``distribution()`` is
+ 3-4x faster. Thanks to Antony Lee for the
+ contribution. Closes #95.
+
+* When searching through ``sys.path``, if any error
+ occurs attempting to list a path entry, that entry
+ is skipped, making the system much more lenient
+ to errors. Closes #94.
+
+v1.3.0
+======
+
+* Improve custom finders documentation. Closes #105.
+
+v1.2.0
+======
+
+* Once again, drop support for Python 3.4. Ref #104.
+
+v1.1.3
+======
+
+* Restored support for Python 3.4 due to improper version
+ compatibility declarations in the v1.1.0 and v1.1.1
+ releases. Closes #104.
+
+v1.1.2
+======
+
+* Repaired project metadata to correctly declare the
+ ``python_requires`` directive. Closes #103.
+
+v1.1.1
+======
+
+* Fixed ``repr(EntryPoint)`` on PyPy 3 also. Closes #102.
+
+v1.1.0
+======
+
+* Dropped support for Python 3.4.
+* EntryPoints are now pickleable. Closes #96.
+* Fixed ``repr(EntryPoint)`` on PyPy 2. Closes #97.
+
+v1.0.0
+======
+
+* Project adopts semver for versioning.
+
+* Removed compatibility shim introduced in 0.23.
+
+* For better compatibility with the stdlib implementation and to
+ avoid the same distributions being discovered by the stdlib and
+ backport implementations, the backport now disables the
+ stdlib DistributionFinder during initialization (import time).
+ Closes #91 and closes #100.
+
+0.23
+====
+* Added a compatibility shim to prevent failures on beta releases
+ of Python before the signature changed to accept the
+ "context" parameter on find_distributions. This workaround
+ will have a limited lifespan, not to extend beyond release of
+ Python 3.8 final.
+
+0.22
+====
+* Renamed ``package`` parameter to ``distribution_name``
+ as `recommended <https://bugs.python.org/issue34632#msg349423>`_
+ in the following functions: ``distribution``, ``metadata``,
+ ``version``, ``files``, and ``requires``. This
+ backward-incompatible change is expected to have little impact
+ as these functions are assumed to be primarily used with
+ positional parameters.
+
+0.21
+====
+* ``importlib.metadata`` now exposes the ``DistributionFinder``
+ metaclass and references it in the docs for extending the
+ search algorithm.
+* Add ``Distribution.at`` for constructing a Distribution object
+ from a known metadata directory on the file system. Closes #80.
+* Distribution finders now receive a context object that
+ supplies ``.path`` and ``.name`` properties. This change
+ introduces a fundamental backward incompatibility for
+ any projects implementing a ``find_distributions`` method
+ on a ``MetaPathFinder``. This new layer of abstraction
+ allows this context to be supplied directly or constructed
+ on demand and opens the opportunity for a
+ ``find_distributions`` method to solicit additional
+ context from the caller. Closes #85.
+
+0.20
+====
+* Clarify in the docs that calls to ``.files`` could return
+ ``None`` when the metadata is not present. Closes #69.
+* Return all requirements and not just the first for dist-info
+ packages. Closes #67.
+
+0.19
+====
+* Restrain over-eager egg metadata resolution.
+* Add support for entry points with colons in the name. Closes #75.
+
+0.18
+====
+* Parse entry points case sensitively. Closes #68
+* Add a version constraint on the backport configparser package. Closes #66
+
+0.17
+====
+* Fix a permission problem in the tests on Windows.
+
+0.16
+====
+* Don't crash if there exists an EGG-INFO directory on sys.path.
+
+0.15
+====
+* Fix documentation.
+
+0.14
+====
+* Removed ``local_distribution`` function from the API.
+ **This backward-incompatible change removes this
+ behavior summarily**. Projects should remove their
+ reliance on this behavior. A replacement behavior is
+ under review in the `pep517 project
+ <https://github.com/pypa/pep517>`_. Closes #42.
+
+0.13
+====
+* Update docstrings to match PEP 8. Closes #63.
+* Merged modules into one module. Closes #62.
+
+0.12
+====
+* Add support for eggs. !65; Closes #19.
+
+0.11
+====
+* Support generic zip files (not just wheels). Closes #59
+* Support zip files with multiple distributions in them. Closes #60
+* Fully expose the public API in ``importlib_metadata.__all__``.
+
+0.10
+====
+* The ``Distribution`` ABC is now officially part of the public API.
+ Closes #37.
+* Fixed support for older single file egg-info formats. Closes #43.
+* Fixed a testing bug when ``$CWD`` has spaces in the path. Closes #50.
+* Add Python 3.8 to the ``tox`` testing matrix.
+
+0.9
+===
+* Fixed issue where entry points without an attribute would raise an
+ Exception. Closes #40.
+* Removed unused ``name`` parameter from ``entry_points()``. Closes #44.
+* ``DistributionFinder`` classes must now be instantiated before
+ being placed on ``sys.meta_path``.
+
+0.8
+===
+* This library can now discover/enumerate all installed packages. **This
+ backward-incompatible change alters the protocol finders must
+ implement to support distribution package discovery.** Closes #24.
+* The signature of ``find_distributions()`` on custom installer finders
+ should now accept two parameters, ``name`` and ``path`` and
+ these parameters must supply defaults.
+* The ``entry_points()`` method no longer accepts a package name
+ but instead returns all entry points in a dictionary keyed by the
+ ``EntryPoint.group``. The ``resolve`` method has been removed. Instead,
+ call ``EntryPoint.load()``, which has the same semantics as
+ ``pkg_resources`` and ``entrypoints``. **This is a backward incompatible
+ change.**
+* Metadata is now always returned as Unicode text regardless of
+ Python version. Closes #29.
+* This library can now discover metadata for a 'local' package (found
+ in the current-working directory). Closes #27.
+* Added ``files()`` function for resolving files from a distribution.
+* Added a new ``requires()`` function, which returns the requirements
+ for a package suitable for parsing by
+ ``packaging.requirements.Requirement``. Closes #18.
+* The top-level ``read_text()`` function has been removed. Use
+ ``PackagePath.read_text()`` on instances returned by the ``files()``
+ function. **This is a backward incompatible change.**
+* Release dates are now automatically injected into the changelog
+ based on SCM tags.
+
+0.7
+===
+* Fixed issue where packages with dashes in their names would
+ not be discovered. Closes #21.
+* Distribution lookup is now case-insensitive. Closes #20.
+* Wheel distributions can no longer be discovered by their module
+ name. Like Path distributions, they must be indicated by their
+ distribution package name.
+
+0.6
+===
+* Removed ``importlib_metadata.distribution`` function. Now
+ the public interface is primarily the utility functions exposed
+ in ``importlib_metadata.__all__``. Closes #14.
+* Added two new utility functions ``read_text`` and
+ ``metadata``.
+
+0.5
+===
+* Updated README and removed details about Distribution
+ class, now considered private. Closes #15.
+* Added test suite support for Python 3.4+.
+* Fixed SyntaxErrors on Python 3.4 and 3.5. !12
+* Fixed errors on Windows joining Path elements. !15
+
+0.4
+===
+* Housekeeping.
+
+0.3
+===
+* Added usage documentation. Closes #8
+* Add support for getting metadata from wheels on ``sys.path``. Closes #9
+
+0.2
+===
+* Added ``importlib_metadata.entry_points()``. Closes #1
+* Added ``importlib_metadata.resolve()``. Closes #12
+* Add support for Python 2.7. Closes #4
+
+0.1
+===
+* Initial release.
+
+
+..
+ Local Variables:
+ mode: change-log-mode
+ indent-tabs-mode: nil
+ sentence-end-double-space: t
+ fill-column: 78
+ coding: utf-8
+ End:
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/conf.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/conf.py
new file mode 100644
index 0000000000..129a7a4eae
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/conf.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# importlib_metadata documentation build configuration file, created by
+# sphinx-quickstart on Thu Nov 30 10:21:00 2017.
+#
+# 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.
+
+# 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.
+#
+# import os
+# import sys
+# 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 = [
+ 'rst.linker',
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.coverage',
+ '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(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'importlib_metadata'
+copyright = '2017-2019, Jason R. Coombs, Barry Warsaw'
+author = 'Jason R. Coombs, Barry Warsaw'
+
+# 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.
+version = '0.1'
+# The full version, including alpha/beta/rc tags.
+release = '0.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'default'
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# This is required for the alabaster theme
+# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
+html_sidebars = {
+ '**': [
+ 'relations.html', # needs 'show_related': True theme option to display
+ 'searchbox.html',
+ ]
+ }
+
+
+# -- Options for HTMLHelp output ------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'importlib_metadatadoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+ }
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'importlib_metadata.tex',
+ 'importlib\\_metadata Documentation',
+ 'Brett Cannon, Barry Warsaw', 'manual'),
+ ]
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'importlib_metadata', 'importlib_metadata Documentation',
+ [author], 1)
+ ]
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'importlib_metadata', 'importlib_metadata Documentation',
+ author, 'importlib_metadata', 'One line description of project.',
+ 'Miscellaneous'),
+ ]
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {
+ 'python': ('https://docs.python.org/3', None),
+ 'importlib_resources': (
+ 'https://importlib-resources.readthedocs.io/en/latest/', None
+ ),
+ }
+
+
+# For rst.linker, inject release dates into changelog.rst
+link_files = {
+ 'changelog.rst': dict(
+ replace=[
+ dict(
+ pattern=r'^(?m)((?P<scm_version>v?\d+(\.\d+){1,2}))\n[-=]+\n',
+ with_scm='{text}\n{rev[timestamp]:%Y-%m-%d}\n\n',
+ ),
+ ],
+ ),
+ }
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/index.rst b/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/index.rst
new file mode 100644
index 0000000000..57332f5e8d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/index.rst
@@ -0,0 +1,52 @@
+===============================
+ Welcome to importlib_metadata
+===============================
+
+``importlib_metadata`` is a library which provides an API for accessing an
+installed package's metadata (see :pep:`566`), such as its entry points or its top-level
+name. This functionality intends to replace most uses of ``pkg_resources``
+`entry point API`_ and `metadata API`_. Along with :mod:`importlib.resources` in
+Python 3.7 and newer (backported as :doc:`importlib_resources <importlib_resources:index>` for older
+versions of Python), this can eliminate the need to use the older and less
+efficient ``pkg_resources`` package.
+
+``importlib_metadata`` supplies a backport of
+:doc:`importlib.metadata <library/importlib.metadata>` as found in
+Python 3.8 and later for earlier Python releases. Users of
+Python 3.8 and beyond are encouraged to use the standard library module
+when possible and fall back to ``importlib_metadata`` when necessary.
+When imported on Python 3.8 and later, ``importlib_metadata`` replaces the
+DistributionFinder behavior from the stdlib, but leaves the API in tact.
+Developers looking for detailed API descriptions should refer to the Python
+3.8 standard library documentation.
+
+The documentation here includes a general :ref:`usage <using>` guide.
+
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ using.rst
+ changelog (links).rst
+
+
+Project details
+===============
+
+ * Project home: https://github.com/python/importlib_metadata
+ * Report bugs at: https://github.com/python/importlib_metadata/issues
+ * Code hosting: https://github.com/python/importlib_metadata
+ * Documentation: https://importlib_metadata.readthedocs.io/
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+
+.. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points
+.. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/using.rst b/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/using.rst
new file mode 100644
index 0000000000..11965147f4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/docs/using.rst
@@ -0,0 +1,260 @@
+.. _using:
+
+=================================
+ Using :mod:`!importlib_metadata`
+=================================
+
+``importlib_metadata`` is a library that provides for access to installed
+package metadata. Built in part on Python's import system, this library
+intends to replace similar functionality in the `entry point
+API`_ and `metadata API`_ of ``pkg_resources``. Along with
+:mod:`importlib.resources` in Python 3.7
+and newer (backported as :doc:`importlib_resources <importlib_resources:index>` for older versions of
+Python), this can eliminate the need to use the older and less efficient
+``pkg_resources`` package.
+
+By "installed package" we generally mean a third-party package installed into
+Python's ``site-packages`` directory via tools such as `pip
+<https://pypi.org/project/pip/>`_. Specifically,
+it means a package with either a discoverable ``dist-info`` or ``egg-info``
+directory, and metadata defined by :pep:`566` or its older specifications.
+By default, package metadata can live on the file system or in zip archives on
+:data:`sys.path`. Through an extension mechanism, the metadata can live almost
+anywhere.
+
+
+Overview
+========
+
+Let's say you wanted to get the version string for a package you've installed
+using ``pip``. We start by creating a virtual environment and installing
+something into it::
+
+ $ python3 -m venv example
+ $ source example/bin/activate
+ (example) $ pip install importlib_metadata
+ (example) $ pip install wheel
+
+You can get the version string for ``wheel`` by running the following::
+
+ (example) $ python
+ >>> from importlib_metadata import version
+ >>> version('wheel')
+ '0.32.3'
+
+You can also get the set of entry points keyed by group, such as
+``console_scripts``, ``distutils.commands`` and others. Each group contains a
+sequence of :ref:`EntryPoint <entry-points>` objects.
+
+You can get the :ref:`metadata for a distribution <metadata>`::
+
+ >>> list(metadata('wheel'))
+ ['Metadata-Version', 'Name', 'Version', 'Summary', 'Home-page', 'Author', 'Author-email', 'Maintainer', 'Maintainer-email', 'License', 'Project-URL', 'Project-URL', 'Project-URL', 'Keywords', 'Platform', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Requires-Python', 'Provides-Extra', 'Requires-Dist', 'Requires-Dist']
+
+You can also get a :ref:`distribution's version number <version>`, list its
+:ref:`constituent files <files>`, and get a list of the distribution's
+:ref:`requirements`.
+
+
+Functional API
+==============
+
+This package provides the following functionality via its public API.
+
+
+.. _entry-points:
+
+Entry points
+------------
+
+The ``entry_points()`` function returns a dictionary of all entry points,
+keyed by group. Entry points are represented by ``EntryPoint`` instances;
+each ``EntryPoint`` has a ``.name``, ``.group``, and ``.value`` attributes and
+a ``.load()`` method to resolve the value. There are also ``.module``,
+``.attr``, and ``.extras`` attributes for getting the components of the
+``.value`` attribute::
+
+ >>> eps = entry_points()
+ >>> list(eps)
+ ['console_scripts', 'distutils.commands', 'distutils.setup_keywords', 'egg_info.writers', 'setuptools.installation']
+ >>> scripts = eps['console_scripts']
+ >>> wheel = [ep for ep in scripts if ep.name == 'wheel'][0]
+ >>> wheel
+ EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts')
+ >>> wheel.module
+ 'wheel.cli'
+ >>> wheel.attr
+ 'main'
+ >>> wheel.extras
+ []
+ >>> main = wheel.load()
+ >>> main
+ <function main at 0x103528488>
+
+The ``group`` and ``name`` are arbitrary values defined by the package author
+and usually a client will wish to resolve all entry points for a particular
+group. Read `the setuptools docs
+<https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins>`_
+for more information on entry points, their definition, and usage.
+
+
+.. _metadata:
+
+Distribution metadata
+---------------------
+
+Every distribution includes some metadata, which you can extract using the
+``metadata()`` function::
+
+ >>> wheel_metadata = metadata('wheel')
+
+The keys of the returned data structure [#f1]_ name the metadata keywords, and
+their values are returned unparsed from the distribution metadata::
+
+ >>> wheel_metadata['Requires-Python']
+ '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'
+
+
+.. _version:
+
+Distribution versions
+---------------------
+
+The ``version()`` function is the quickest way to get a distribution's version
+number, as a string::
+
+ >>> version('wheel')
+ '0.32.3'
+
+
+.. _files:
+
+Distribution files
+------------------
+
+You can also get the full set of files contained within a distribution. The
+``files()`` function takes a distribution package name and returns all of the
+files installed by this distribution. Each file object returned is a
+``PackagePath``, a :class:`pathlib.Path` derived object with additional ``dist``,
+``size``, and ``hash`` properties as indicated by the metadata. For example::
+
+ >>> util = [p for p in files('wheel') if 'util.py' in str(p)][0]
+ >>> util
+ PackagePath('wheel/util.py')
+ >>> util.size
+ 859
+ >>> util.dist
+ <importlib_metadata._hooks.PathDistribution object at 0x101e0cef0>
+ >>> util.hash
+ <FileHash mode: sha256 value: bYkw5oMccfazVCoYQwKkkemoVyMAFoR34mmKBx8R1NI>
+
+Once you have the file, you can also read its contents::
+
+ >>> print(util.read_text())
+ import base64
+ import sys
+ ...
+ def as_bytes(s):
+ if isinstance(s, text_type):
+ return s.encode('utf-8')
+ return s
+
+In the case where the metadata file listing files
+(RECORD or SOURCES.txt) is missing, ``files()`` will
+return ``None``. The caller may wish to wrap calls to
+``files()`` in `always_iterable
+<https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.always_iterable>`_
+or otherwise guard against this condition if the target
+distribution is not known to have the metadata present.
+
+.. _requirements:
+
+Distribution requirements
+-------------------------
+
+To get the full set of requirements for a distribution, use the ``requires()``
+function::
+
+ >>> requires('wheel')
+ ["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]
+
+
+Distributions
+=============
+
+While the above API is the most common and convenient usage, you can get all
+of that information from the ``Distribution`` class. A ``Distribution`` is an
+abstract object that represents the metadata for a Python package. You can
+get the ``Distribution`` instance::
+
+ >>> from importlib_metadata import distribution
+ >>> dist = distribution('wheel')
+
+Thus, an alternative way to get the version number is through the
+``Distribution`` instance::
+
+ >>> dist.version
+ '0.32.3'
+
+There are all kinds of additional metadata available on the ``Distribution``
+instance::
+
+ >>> d.metadata['Requires-Python']
+ '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'
+ >>> d.metadata['License']
+ 'MIT'
+
+The full set of available metadata is not described here. See :pep:`566`
+for additional details.
+
+
+Extending the search algorithm
+==============================
+
+Because package metadata is not available through :data:`sys.path` searches, or
+package loaders directly, the metadata for a package is found through import
+system `finders`_. To find a distribution package's metadata,
+``importlib.metadata`` queries the list of :term:`meta path finders <meta path finder>` on
+:data:`sys.meta_path`.
+
+By default ``importlib_metadata`` installs a finder for distribution packages
+found on the file system. This finder doesn't actually find any *packages*,
+but it can find the packages' metadata.
+
+The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the
+interface expected of finders by Python's import system.
+``importlib_metadata`` extends this protocol by looking for an optional
+``find_distributions`` callable on the finders from
+:data:`sys.meta_path` and presents this extended interface as the
+``DistributionFinder`` abstract base class, which defines this abstract
+method::
+
+ @abc.abstractmethod
+ def find_distributions(context=DistributionFinder.Context()):
+ """Return an iterable of all Distribution instances capable of
+ loading the metadata for packages for the indicated ``context``.
+ """
+
+The ``DistributionFinder.Context`` object provides ``.path`` and ``.name``
+properties indicating the path to search and name to match and may
+supply other relevant context.
+
+What this means in practice is that to support finding distribution package
+metadata in locations other than the file system, subclass
+``Distribution`` and implement the abstract methods. Then from
+a custom finder, return instances of this derived ``Distribution`` in the
+``find_distributions()`` method.
+
+
+.. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points
+.. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api
+.. _`finders`: https://docs.python.org/3/reference/import.html#finders-and-loaders
+
+
+.. rubric:: Footnotes
+
+.. [#f1] Technically, the returned distribution metadata object is an
+ :class:`email.message.EmailMessage`
+ instance, but this is an implementation detail, and not part of the
+ stable API. You should only use dictionary-like methods and syntax
+ to access the metadata contents.
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/importlib_metadata/__init__.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/importlib_metadata/__init__.py
new file mode 100644
index 0000000000..d5cbc2d03c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/importlib_metadata/__init__.py
@@ -0,0 +1,627 @@
+from __future__ import unicode_literals, absolute_import
+
+import io
+import os
+import re
+import abc
+import csv
+import sys
+import zipp
+import operator
+import functools
+import itertools
+import posixpath
+import collections
+
+from ._compat import (
+ install,
+ NullFinder,
+ ConfigParser,
+ suppress,
+ map,
+ FileNotFoundError,
+ IsADirectoryError,
+ NotADirectoryError,
+ PermissionError,
+ pathlib,
+ ModuleNotFoundError,
+ MetaPathFinder,
+ email_message_from_string,
+ PyPy_repr,
+ unique_ordered,
+ str,
+ )
+from importlib import import_module
+from itertools import starmap
+
+
+__metaclass__ = type
+
+
+__all__ = [
+ 'Distribution',
+ 'DistributionFinder',
+ 'PackageNotFoundError',
+ 'distribution',
+ 'distributions',
+ 'entry_points',
+ 'files',
+ 'metadata',
+ 'requires',
+ 'version',
+ ]
+
+
+class PackageNotFoundError(ModuleNotFoundError):
+ """The package was not found."""
+
+ def __str__(self):
+ tmpl = "No package metadata was found for {self.name}"
+ return tmpl.format(**locals())
+
+ @property
+ def name(self):
+ name, = self.args
+ return name
+
+
+class EntryPoint(
+ PyPy_repr,
+ collections.namedtuple('EntryPointBase', 'name value group')):
+ """An entry point as defined by Python packaging conventions.
+
+ See `the packaging docs on entry points
+ <https://packaging.python.org/specifications/entry-points/>`_
+ for more information.
+ """
+
+ pattern = re.compile(
+ r'(?P<module>[\w.]+)\s*'
+ r'(:\s*(?P<attr>[\w.]+))?\s*'
+ r'(?P<extras>\[.*\])?\s*$'
+ )
+ """
+ A regular expression describing the syntax for an entry point,
+ which might look like:
+
+ - module
+ - package.module
+ - package.module:attribute
+ - package.module:object.attribute
+ - package.module:attr [extra1, extra2]
+
+ Other combinations are possible as well.
+
+ The expression is lenient about whitespace around the ':',
+ following the attr, and following any extras.
+ """
+
+ def load(self):
+ """Load the entry point from its definition. If only a module
+ is indicated by the value, return that module. Otherwise,
+ return the named object.
+ """
+ match = self.pattern.match(self.value)
+ module = import_module(match.group('module'))
+ attrs = filter(None, (match.group('attr') or '').split('.'))
+ return functools.reduce(getattr, attrs, module)
+
+ @property
+ def module(self):
+ match = self.pattern.match(self.value)
+ return match.group('module')
+
+ @property
+ def attr(self):
+ match = self.pattern.match(self.value)
+ return match.group('attr')
+
+ @property
+ def extras(self):
+ match = self.pattern.match(self.value)
+ return list(re.finditer(r'\w+', match.group('extras') or ''))
+
+ @classmethod
+ def _from_config(cls, config):
+ return [
+ cls(name, value, group)
+ for group in config.sections()
+ for name, value in config.items(group)
+ ]
+
+ @classmethod
+ def _from_text(cls, text):
+ config = ConfigParser(delimiters='=')
+ # case sensitive: https://stackoverflow.com/q/1611799/812183
+ config.optionxform = str
+ try:
+ config.read_string(text)
+ except AttributeError: # pragma: nocover
+ # Python 2 has no read_string
+ config.readfp(io.StringIO(text))
+ return EntryPoint._from_config(config)
+
+ def __iter__(self):
+ """
+ Supply iter so one may construct dicts of EntryPoints easily.
+ """
+ return iter((self.name, self))
+
+ def __reduce__(self):
+ return (
+ self.__class__,
+ (self.name, self.value, self.group),
+ )
+
+
+class PackagePath(pathlib.PurePosixPath):
+ """A reference to a path in a package"""
+
+ def read_text(self, encoding='utf-8'):
+ with self.locate().open(encoding=encoding) as stream:
+ return stream.read()
+
+ def read_binary(self):
+ with self.locate().open('rb') as stream:
+ return stream.read()
+
+ def locate(self):
+ """Return a path-like object for this path"""
+ return self.dist.locate_file(self)
+
+
+class FileHash:
+ def __init__(self, spec):
+ self.mode, _, self.value = spec.partition('=')
+
+ def __repr__(self):
+ return '<FileHash mode: {} value: {}>'.format(self.mode, self.value)
+
+
+class Distribution:
+ """A Python distribution package."""
+
+ @abc.abstractmethod
+ def read_text(self, filename):
+ """Attempt to load metadata file given by the name.
+
+ :param filename: The name of the file in the distribution info.
+ :return: The text if found, otherwise None.
+ """
+
+ @abc.abstractmethod
+ def locate_file(self, path):
+ """
+ Given a path to a file in this distribution, return a path
+ to it.
+ """
+
+ @classmethod
+ def from_name(cls, name):
+ """Return the Distribution for the given package name.
+
+ :param name: The name of the distribution package to search for.
+ :return: The Distribution instance (or subclass thereof) for the named
+ package, if found.
+ :raises PackageNotFoundError: When the named package's distribution
+ metadata cannot be found.
+ """
+ for resolver in cls._discover_resolvers():
+ dists = resolver(DistributionFinder.Context(name=name))
+ dist = next(iter(dists), None)
+ if dist is not None:
+ return dist
+ else:
+ raise PackageNotFoundError(name)
+
+ @classmethod
+ def discover(cls, **kwargs):
+ """Return an iterable of Distribution objects for all packages.
+
+ Pass a ``context`` or pass keyword arguments for constructing
+ a context.
+
+ :context: A ``DistributionFinder.Context`` object.
+ :return: Iterable of Distribution objects for all packages.
+ """
+ context = kwargs.pop('context', None)
+ if context and kwargs:
+ raise ValueError("cannot accept context and kwargs")
+ context = context or DistributionFinder.Context(**kwargs)
+ return itertools.chain.from_iterable(
+ resolver(context)
+ for resolver in cls._discover_resolvers()
+ )
+
+ @staticmethod
+ def at(path):
+ """Return a Distribution for the indicated metadata path
+
+ :param path: a string or path-like object
+ :return: a concrete Distribution instance for the path
+ """
+ return PathDistribution(pathlib.Path(path))
+
+ @staticmethod
+ def _discover_resolvers():
+ """Search the meta_path for resolvers."""
+ declared = (
+ getattr(finder, 'find_distributions', None)
+ for finder in sys.meta_path
+ )
+ return filter(None, declared)
+
+ @classmethod
+ def _local(cls, root='.'):
+ from pep517 import build, meta
+ system = build.compat_system(root)
+ builder = functools.partial(
+ meta.build,
+ source_dir=root,
+ system=system,
+ )
+ return PathDistribution(zipp.Path(meta.build_as_zip(builder)))
+
+ @property
+ def metadata(self):
+ """Return the parsed metadata for this Distribution.
+
+ The returned object will have keys that name the various bits of
+ metadata. See PEP 566 for details.
+ """
+ text = (
+ self.read_text('METADATA')
+ or self.read_text('PKG-INFO')
+ # This last clause is here to support old egg-info files. Its
+ # effect is to just end up using the PathDistribution's self._path
+ # (which points to the egg-info file) attribute unchanged.
+ or self.read_text('')
+ )
+ return email_message_from_string(text)
+
+ @property
+ def version(self):
+ """Return the 'Version' metadata for the distribution package."""
+ return self.metadata['Version']
+
+ @property
+ def entry_points(self):
+ return EntryPoint._from_text(self.read_text('entry_points.txt'))
+
+ @property
+ def files(self):
+ """Files in this distribution.
+
+ :return: List of PackagePath for this distribution or None
+
+ Result is `None` if the metadata file that enumerates files
+ (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
+ missing.
+ Result may be empty if the metadata exists but is empty.
+ """
+ file_lines = self._read_files_distinfo() or self._read_files_egginfo()
+
+ def make_file(name, hash=None, size_str=None):
+ result = PackagePath(name)
+ result.hash = FileHash(hash) if hash else None
+ result.size = int(size_str) if size_str else None
+ result.dist = self
+ return result
+
+ return file_lines and list(starmap(make_file, csv.reader(file_lines)))
+
+ def _read_files_distinfo(self):
+ """
+ Read the lines of RECORD
+ """
+ text = self.read_text('RECORD')
+ return text and text.splitlines()
+
+ def _read_files_egginfo(self):
+ """
+ SOURCES.txt might contain literal commas, so wrap each line
+ in quotes.
+ """
+ text = self.read_text('SOURCES.txt')
+ return text and map('"{}"'.format, text.splitlines())
+
+ @property
+ def requires(self):
+ """Generated requirements specified for this Distribution"""
+ reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
+ return reqs and list(reqs)
+
+ def _read_dist_info_reqs(self):
+ return self.metadata.get_all('Requires-Dist')
+
+ def _read_egg_info_reqs(self):
+ source = self.read_text('requires.txt')
+ return source and self._deps_from_requires_text(source)
+
+ @classmethod
+ def _deps_from_requires_text(cls, source):
+ section_pairs = cls._read_sections(source.splitlines())
+ sections = {
+ section: list(map(operator.itemgetter('line'), results))
+ for section, results in
+ itertools.groupby(section_pairs, operator.itemgetter('section'))
+ }
+ return cls._convert_egg_info_reqs_to_simple_reqs(sections)
+
+ @staticmethod
+ def _read_sections(lines):
+ section = None
+ for line in filter(None, lines):
+ section_match = re.match(r'\[(.*)\]$', line)
+ if section_match:
+ section = section_match.group(1)
+ continue
+ yield locals()
+
+ @staticmethod
+ def _convert_egg_info_reqs_to_simple_reqs(sections):
+ """
+ Historically, setuptools would solicit and store 'extra'
+ requirements, including those with environment markers,
+ in separate sections. More modern tools expect each
+ dependency to be defined separately, with any relevant
+ extras and environment markers attached directly to that
+ requirement. This method converts the former to the
+ latter. See _test_deps_from_requires_text for an example.
+ """
+ def make_condition(name):
+ return name and 'extra == "{name}"'.format(name=name)
+
+ def parse_condition(section):
+ section = section or ''
+ extra, sep, markers = section.partition(':')
+ if extra and markers:
+ markers = '({markers})'.format(markers=markers)
+ conditions = list(filter(None, [markers, make_condition(extra)]))
+ return '; ' + ' and '.join(conditions) if conditions else ''
+
+ for section, deps in sections.items():
+ for dep in deps:
+ yield dep + parse_condition(section)
+
+
+class DistributionFinder(MetaPathFinder):
+ """
+ A MetaPathFinder capable of discovering installed distributions.
+ """
+
+ class Context:
+ """
+ Keyword arguments presented by the caller to
+ ``distributions()`` or ``Distribution.discover()``
+ to narrow the scope of a search for distributions
+ in all DistributionFinders.
+
+ Each DistributionFinder may expect any parameters
+ and should attempt to honor the canonical
+ parameters defined below when appropriate.
+ """
+
+ name = None
+ """
+ Specific name for which a distribution finder should match.
+ A name of ``None`` matches all distributions.
+ """
+
+ def __init__(self, **kwargs):
+ vars(self).update(kwargs)
+
+ @property
+ def path(self):
+ """
+ The path that a distribution finder should search.
+
+ Typically refers to Python package paths and defaults
+ to ``sys.path``.
+ """
+ return vars(self).get('path', sys.path)
+
+ @abc.abstractmethod
+ def find_distributions(self, context=Context()):
+ """
+ Find distributions.
+
+ Return an iterable of all Distribution instances capable of
+ loading the metadata for packages matching the ``context``,
+ a DistributionFinder.Context instance.
+ """
+
+
+class FastPath:
+ """
+ Micro-optimized class for searching a path for
+ children.
+ """
+
+ def __init__(self, root):
+ self.root = str(root)
+ self.base = os.path.basename(self.root).lower()
+
+ def joinpath(self, child):
+ return pathlib.Path(self.root, child)
+
+ def children(self):
+ with suppress(Exception):
+ return os.listdir(self.root or '')
+ with suppress(Exception):
+ return self.zip_children()
+ return []
+
+ def zip_children(self):
+ zip_path = zipp.Path(self.root)
+ names = zip_path.root.namelist()
+ self.joinpath = zip_path.joinpath
+
+ return unique_ordered(
+ child.split(posixpath.sep, 1)[0]
+ for child in names
+ )
+
+ def is_egg(self, search):
+ base = self.base
+ return (
+ base == search.versionless_egg_name
+ or base.startswith(search.prefix)
+ and base.endswith('.egg'))
+
+ def search(self, name):
+ for child in self.children():
+ n_low = child.lower()
+ if (n_low in name.exact_matches
+ or n_low.replace('.', '_').startswith(name.prefix)
+ and n_low.endswith(name.suffixes)
+ # legacy case:
+ or self.is_egg(name) and n_low == 'egg-info'):
+ yield self.joinpath(child)
+
+
+class Prepared:
+ """
+ A prepared search for metadata on a possibly-named package.
+ """
+ normalized = ''
+ prefix = ''
+ suffixes = '.dist-info', '.egg-info'
+ exact_matches = [''][:0]
+ versionless_egg_name = ''
+
+ def __init__(self, name):
+ self.name = name
+ if name is None:
+ return
+ self.normalized = self.normalize(name)
+ self.prefix = self.normalized + '-'
+ self.exact_matches = [
+ self.normalized + suffix for suffix in self.suffixes]
+ self.versionless_egg_name = self.normalized + '.egg'
+
+ @staticmethod
+ def normalize(name):
+ """
+ PEP 503 normalization plus dashes as underscores.
+ """
+ return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
+
+
+@install
+class MetadataPathFinder(NullFinder, DistributionFinder):
+ """A degenerate finder for distribution packages on the file system.
+
+ This finder supplies only a find_distributions() method for versions
+ of Python that do not have a PathFinder find_distributions().
+ """
+
+ def find_distributions(self, context=DistributionFinder.Context()):
+ """
+ Find distributions.
+
+ Return an iterable of all Distribution instances capable of
+ loading the metadata for packages matching ``context.name``
+ (or all names if ``None`` indicated) along the paths in the list
+ of directories ``context.path``.
+ """
+ found = self._search_paths(context.name, context.path)
+ return map(PathDistribution, found)
+
+ @classmethod
+ def _search_paths(cls, name, paths):
+ """Find metadata directories in paths heuristically."""
+ return itertools.chain.from_iterable(
+ path.search(Prepared(name))
+ for path in map(FastPath, paths)
+ )
+
+
+class PathDistribution(Distribution):
+ def __init__(self, path):
+ """Construct a distribution from a path to the metadata directory.
+
+ :param path: A pathlib.Path or similar object supporting
+ .joinpath(), __div__, .parent, and .read_text().
+ """
+ self._path = path
+
+ def read_text(self, filename):
+ with suppress(FileNotFoundError, IsADirectoryError, KeyError,
+ NotADirectoryError, PermissionError):
+ return self._path.joinpath(filename).read_text(encoding='utf-8')
+ read_text.__doc__ = Distribution.read_text.__doc__
+
+ def locate_file(self, path):
+ return self._path.parent / path
+
+
+def distribution(distribution_name):
+ """Get the ``Distribution`` instance for the named package.
+
+ :param distribution_name: The name of the distribution package as a string.
+ :return: A ``Distribution`` instance (or subclass thereof).
+ """
+ return Distribution.from_name(distribution_name)
+
+
+def distributions(**kwargs):
+ """Get all ``Distribution`` instances in the current environment.
+
+ :return: An iterable of ``Distribution`` instances.
+ """
+ return Distribution.discover(**kwargs)
+
+
+def metadata(distribution_name):
+ """Get the metadata for the named package.
+
+ :param distribution_name: The name of the distribution package to query.
+ :return: An email.Message containing the parsed metadata.
+ """
+ return Distribution.from_name(distribution_name).metadata
+
+
+def version(distribution_name):
+ """Get the version string for the named package.
+
+ :param distribution_name: The name of the distribution package to query.
+ :return: The version string for the package as defined in the package's
+ "Version" metadata key.
+ """
+ return distribution(distribution_name).version
+
+
+def entry_points():
+ """Return EntryPoint objects for all installed packages.
+
+ :return: EntryPoint objects for all installed packages.
+ """
+ eps = itertools.chain.from_iterable(
+ dist.entry_points for dist in distributions())
+ by_group = operator.attrgetter('group')
+ ordered = sorted(eps, key=by_group)
+ grouped = itertools.groupby(ordered, by_group)
+ return {
+ group: tuple(eps)
+ for group, eps in grouped
+ }
+
+
+def files(distribution_name):
+ """Return a list of files for the named package.
+
+ :param distribution_name: The name of the distribution package to query.
+ :return: List of files composing the distribution.
+ """
+ return distribution(distribution_name).files
+
+
+def requires(distribution_name):
+ """
+ Return a list of requirements for the named package.
+
+ :return: An iterator of requirements, suitable for
+ packaging.requirement.Requirement.
+ """
+ return distribution(distribution_name).requires
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/importlib_metadata/_compat.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/importlib_metadata/_compat.py
new file mode 100644
index 0000000000..303d4a22e8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/importlib_metadata/_compat.py
@@ -0,0 +1,152 @@
+from __future__ import absolute_import, unicode_literals
+
+import io
+import abc
+import sys
+import email
+
+
+if sys.version_info > (3,): # pragma: nocover
+ import builtins
+ from configparser import ConfigParser
+ import contextlib
+ FileNotFoundError = builtins.FileNotFoundError
+ IsADirectoryError = builtins.IsADirectoryError
+ NotADirectoryError = builtins.NotADirectoryError
+ PermissionError = builtins.PermissionError
+ map = builtins.map
+ from itertools import filterfalse
+else: # pragma: nocover
+ from backports.configparser import ConfigParser
+ from itertools import imap as map # type: ignore
+ from itertools import ifilterfalse as filterfalse
+ import contextlib2 as contextlib
+ FileNotFoundError = IOError, OSError
+ IsADirectoryError = IOError, OSError
+ NotADirectoryError = IOError, OSError
+ PermissionError = IOError, OSError
+
+str = type('')
+
+suppress = contextlib.suppress
+
+if sys.version_info > (3, 5): # pragma: nocover
+ import pathlib
+else: # pragma: nocover
+ import pathlib2 as pathlib
+
+try:
+ ModuleNotFoundError = builtins.FileNotFoundError
+except (NameError, AttributeError): # pragma: nocover
+ ModuleNotFoundError = ImportError # type: ignore
+
+
+if sys.version_info >= (3,): # pragma: nocover
+ from importlib.abc import MetaPathFinder
+else: # pragma: nocover
+ class MetaPathFinder(object):
+ __metaclass__ = abc.ABCMeta
+
+
+__metaclass__ = type
+__all__ = [
+ 'install', 'NullFinder', 'MetaPathFinder', 'ModuleNotFoundError',
+ 'pathlib', 'ConfigParser', 'map', 'suppress', 'FileNotFoundError',
+ 'NotADirectoryError', 'email_message_from_string',
+ ]
+
+
+def install(cls):
+ """
+ Class decorator for installation on sys.meta_path.
+
+ Adds the backport DistributionFinder to sys.meta_path and
+ attempts to disable the finder functionality of the stdlib
+ DistributionFinder.
+ """
+ sys.meta_path.append(cls())
+ disable_stdlib_finder()
+ return cls
+
+
+def disable_stdlib_finder():
+ """
+ Give the backport primacy for discovering path-based distributions
+ by monkey-patching the stdlib O_O.
+
+ See #91 for more background for rationale on this sketchy
+ behavior.
+ """
+ def matches(finder):
+ return (
+ getattr(finder, '__module__', None) == '_frozen_importlib_external'
+ and hasattr(finder, 'find_distributions')
+ )
+ for finder in filter(matches, sys.meta_path): # pragma: nocover
+ del finder.find_distributions
+
+
+class NullFinder:
+ """
+ A "Finder" (aka "MetaClassFinder") that never finds any modules,
+ but may find distributions.
+ """
+ @staticmethod
+ def find_spec(*args, **kwargs):
+ return None
+
+ # In Python 2, the import system requires finders
+ # to have a find_module() method, but this usage
+ # is deprecated in Python 3 in favor of find_spec().
+ # For the purposes of this finder (i.e. being present
+ # on sys.meta_path but having no other import
+ # system functionality), the two methods are identical.
+ find_module = find_spec
+
+
+def py2_message_from_string(text): # nocoverpy3
+ # Work around https://bugs.python.org/issue25545 where
+ # email.message_from_string cannot handle Unicode on Python 2.
+ io_buffer = io.StringIO(text)
+ return email.message_from_file(io_buffer)
+
+
+email_message_from_string = (
+ py2_message_from_string
+ if sys.version_info < (3,) else
+ email.message_from_string
+ )
+
+
+class PyPy_repr:
+ """
+ Override repr for EntryPoint objects on PyPy to avoid __iter__ access.
+ Ref #97, #102.
+ """
+ affected = hasattr(sys, 'pypy_version_info')
+
+ def __compat_repr__(self): # pragma: nocover
+ def make_param(name):
+ value = getattr(self, name)
+ return '{name}={value!r}'.format(**locals())
+ params = ', '.join(map(make_param, self._fields))
+ return 'EntryPoint({params})'.format(**locals())
+
+ if affected: # pragma: nocover
+ __repr__ = __compat_repr__
+ del affected
+
+
+# from itertools recipes
+def unique_everseen(iterable): # pragma: nocover
+ "List unique elements, preserving order. Remember all elements ever seen."
+ seen = set()
+ seen_add = seen.add
+
+ for element in filterfalse(seen.__contains__, iterable):
+ seen_add(element)
+ yield element
+
+
+unique_ordered = (
+ unique_everseen if sys.version_info < (3, 7) else dict.fromkeys)
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/prepare/example/example/__init__.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/prepare/example/example/__init__.py
new file mode 100644
index 0000000000..ba73b74339
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/prepare/example/example/__init__.py
@@ -0,0 +1,2 @@
+def main():
+ return 'example'
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/prepare/example/setup.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/prepare/example/setup.py
new file mode 100644
index 0000000000..8663ad389a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/prepare/example/setup.py
@@ -0,0 +1,10 @@
+from setuptools import setup
+setup(
+ name='example',
+ version='21.12',
+ license='Apache Software License',
+ packages=['example'],
+ entry_points={
+ 'console_scripts': ['example = example:main', 'Example=example:main'],
+ },
+ )
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/pyproject.toml b/testing/web-platform/tests/tools/third_party/importlib_metadata/pyproject.toml
new file mode 100644
index 0000000000..e5c3a6a455
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/pyproject.toml
@@ -0,0 +1,2 @@
+[build-system]
+requires = ["setuptools>=30.3", "wheel", "setuptools_scm"]
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/setup.cfg b/testing/web-platform/tests/tools/third_party/importlib_metadata/setup.cfg
new file mode 100644
index 0000000000..fa10c8d358
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/setup.cfg
@@ -0,0 +1,47 @@
+[metadata]
+name = importlib_metadata
+author = Jason R. Coombs
+author_email = jaraco@jaraco.com
+url = http://importlib-metadata.readthedocs.io/
+description = Read metadata from Python packages
+long_description = file: README.rst
+license = Apache Software License
+classifiers =
+ Development Status :: 3 - Alpha
+ Intended Audience :: Developers
+ License :: OSI Approved :: Apache Software License
+ Topic :: Software Development :: Libraries
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 2
+
+[options]
+python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*
+setup_requires = setuptools-scm
+install_requires =
+ zipp>=0.5
+ pathlib2; python_version < '3'
+ contextlib2; python_version < '3'
+ configparser>=3.5; python_version < '3'
+packages = importlib_metadata
+
+[mypy]
+ignore_missing_imports = True
+# XXX We really should use the default `True` value here, but it causes too
+# many warnings, so for now just disable it. E.g. a package's __spec__ is
+# defined as Optional[ModuleSpec] so we can't just blindly pull attributes off
+# of that attribute. The real fix is to add conditionals or asserts proving
+# that package.__spec__ is not None.
+strict_optional = False
+
+[wheel]
+universal=1
+
+[options.extras_require]
+testing =
+ importlib_resources>=1.3; python_version < "3.9"
+ packaging
+ pep517
+ unittest2; python_version < "3"
+docs =
+ sphinx
+ rst.linker
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/setup.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/setup.py
new file mode 100644
index 0000000000..d5d43d7c93
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/setup.py
@@ -0,0 +1,3 @@
+from setuptools import setup
+
+setup(use_scm_version=True)
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/__init__.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/data/__init__.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/data/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/data/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/data/example-21.12-py3-none-any.whl b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/data/example-21.12-py3-none-any.whl
new file mode 100644
index 0000000000..641ab07f7a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/data/example-21.12-py3-none-any.whl
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/data/example-21.12-py3.6.egg b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/data/example-21.12-py3.6.egg
new file mode 100644
index 0000000000..cdb298a19b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/data/example-21.12-py3.6.egg
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/fixtures.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/fixtures.py
new file mode 100644
index 0000000000..0d834c6580
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/fixtures.py
@@ -0,0 +1,263 @@
+from __future__ import unicode_literals
+
+import os
+import sys
+import shutil
+import tempfile
+import textwrap
+
+from .py39compat import FS_NONASCII
+
+from importlib_metadata._compat import pathlib, contextlib
+
+
+__metaclass__ = type
+
+
+@contextlib.contextmanager
+def tempdir():
+ tmpdir = tempfile.mkdtemp()
+ try:
+ yield pathlib.Path(tmpdir)
+ finally:
+ shutil.rmtree(tmpdir)
+
+
+@contextlib.contextmanager
+def save_cwd():
+ orig = os.getcwd()
+ try:
+ yield
+ finally:
+ os.chdir(orig)
+
+
+@contextlib.contextmanager
+def tempdir_as_cwd():
+ with tempdir() as tmp:
+ with save_cwd():
+ os.chdir(str(tmp))
+ yield tmp
+
+
+@contextlib.contextmanager
+def install_finder(finder):
+ sys.meta_path.append(finder)
+ try:
+ yield
+ finally:
+ sys.meta_path.remove(finder)
+
+
+class Fixtures:
+ def setUp(self):
+ self.fixtures = contextlib.ExitStack()
+ self.addCleanup(self.fixtures.close)
+
+
+class SiteDir(Fixtures):
+ def setUp(self):
+ super(SiteDir, self).setUp()
+ self.site_dir = self.fixtures.enter_context(tempdir())
+
+
+class OnSysPath(Fixtures):
+ @staticmethod
+ @contextlib.contextmanager
+ def add_sys_path(dir):
+ sys.path[:0] = [str(dir)]
+ try:
+ yield
+ finally:
+ sys.path.remove(str(dir))
+
+ def setUp(self):
+ super(OnSysPath, self).setUp()
+ self.fixtures.enter_context(self.add_sys_path(self.site_dir))
+
+
+class DistInfoPkg(OnSysPath, SiteDir):
+ files = {
+ "distinfo_pkg-1.0.0.dist-info": {
+ "METADATA": """
+ Name: distinfo-pkg
+ Author: Steven Ma
+ Version: 1.0.0
+ Requires-Dist: wheel >= 1.0
+ Requires-Dist: pytest; extra == 'test'
+ """,
+ "RECORD": "mod.py,sha256=abc,20\n",
+ "entry_points.txt": """
+ [entries]
+ main = mod:main
+ ns:sub = mod:main
+ """
+ },
+ "mod.py": """
+ def main():
+ print("hello world")
+ """,
+ }
+
+ def setUp(self):
+ super(DistInfoPkg, self).setUp()
+ build_files(DistInfoPkg.files, self.site_dir)
+
+
+class DistInfoPkgWithDot(OnSysPath, SiteDir):
+ files = {
+ "pkg_dot-1.0.0.dist-info": {
+ "METADATA": """
+ Name: pkg.dot
+ Version: 1.0.0
+ """,
+ },
+ }
+
+ def setUp(self):
+ super(DistInfoPkgWithDot, self).setUp()
+ build_files(DistInfoPkgWithDot.files, self.site_dir)
+
+
+class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir):
+ files = {
+ "pkg.dot-1.0.0.dist-info": {
+ "METADATA": """
+ Name: pkg.dot
+ Version: 1.0.0
+ """,
+ },
+ }
+
+ def setUp(self):
+ super(DistInfoPkgWithDotLegacy, self).setUp()
+ build_files(DistInfoPkgWithDotLegacy.files, self.site_dir)
+
+
+class DistInfoPkgOffPath(SiteDir):
+ def setUp(self):
+ super(DistInfoPkgOffPath, self).setUp()
+ build_files(DistInfoPkg.files, self.site_dir)
+
+
+class EggInfoPkg(OnSysPath, SiteDir):
+ files = {
+ "egginfo_pkg.egg-info": {
+ "PKG-INFO": """
+ Name: egginfo-pkg
+ Author: Steven Ma
+ License: Unknown
+ Version: 1.0.0
+ Classifier: Intended Audience :: Developers
+ Classifier: Topic :: Software Development :: Libraries
+ """,
+ "SOURCES.txt": """
+ mod.py
+ egginfo_pkg.egg-info/top_level.txt
+ """,
+ "entry_points.txt": """
+ [entries]
+ main = mod:main
+ """,
+ "requires.txt": """
+ wheel >= 1.0; python_version >= "2.7"
+ [test]
+ pytest
+ """,
+ "top_level.txt": "mod\n"
+ },
+ "mod.py": """
+ def main():
+ print("hello world")
+ """,
+ }
+
+ def setUp(self):
+ super(EggInfoPkg, self).setUp()
+ build_files(EggInfoPkg.files, prefix=self.site_dir)
+
+
+class EggInfoFile(OnSysPath, SiteDir):
+ files = {
+ "egginfo_file.egg-info": """
+ Metadata-Version: 1.0
+ Name: egginfo_file
+ Version: 0.1
+ Summary: An example package
+ Home-page: www.example.com
+ Author: Eric Haffa-Vee
+ Author-email: eric@example.coms
+ License: UNKNOWN
+ Description: UNKNOWN
+ Platform: UNKNOWN
+ """,
+ }
+
+ def setUp(self):
+ super(EggInfoFile, self).setUp()
+ build_files(EggInfoFile.files, prefix=self.site_dir)
+
+
+class LocalPackage:
+ files = {
+ "setup.py": """
+ import setuptools
+ setuptools.setup(name="local-pkg", version="2.0.1")
+ """,
+ }
+
+ def setUp(self):
+ self.fixtures = contextlib.ExitStack()
+ self.addCleanup(self.fixtures.close)
+ self.fixtures.enter_context(tempdir_as_cwd())
+ build_files(self.files)
+
+
+def build_files(file_defs, prefix=pathlib.Path()):
+ """Build a set of files/directories, as described by the
+
+ file_defs dictionary. Each key/value pair in the dictionary is
+ interpreted as a filename/contents pair. If the contents value is a
+ dictionary, a directory is created, and the dictionary interpreted
+ as the files within it, recursively.
+
+ For example:
+
+ {"README.txt": "A README file",
+ "foo": {
+ "__init__.py": "",
+ "bar": {
+ "__init__.py": "",
+ },
+ "baz.py": "# Some code",
+ }
+ }
+ """
+ for name, contents in file_defs.items():
+ full_name = prefix / name
+ if isinstance(contents, dict):
+ full_name.mkdir()
+ build_files(contents, prefix=full_name)
+ else:
+ if isinstance(contents, bytes):
+ with full_name.open('wb') as f:
+ f.write(contents)
+ else:
+ with full_name.open('w') as f:
+ f.write(DALS(contents))
+
+
+class FileBuilder:
+ def unicode_filename(self):
+ return FS_NONASCII or \
+ self.skip("File system does not support non-ascii.")
+
+
+def DALS(str):
+ "Dedent and left-strip"
+ return textwrap.dedent(str).lstrip()
+
+
+class NullFinder:
+ def find_module(self, name):
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/py39compat.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/py39compat.py
new file mode 100644
index 0000000000..a175d4c355
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/py39compat.py
@@ -0,0 +1,4 @@
+try:
+ from test.support.os_helpers import FS_NONASCII
+except ImportError:
+ from test.support import FS_NONASCII # noqa
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_api.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_api.py
new file mode 100644
index 0000000000..efa9799642
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_api.py
@@ -0,0 +1,196 @@
+import re
+import textwrap
+
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+from . import fixtures
+from importlib_metadata import (
+ Distribution, PackageNotFoundError, distribution,
+ entry_points, files, metadata, requires, version,
+ )
+
+try:
+ from collections.abc import Iterator
+except ImportError:
+ from collections import Iterator # noqa: F401
+
+try:
+ from builtins import str as text
+except ImportError:
+ from __builtin__ import unicode as text
+
+
+class APITests(
+ fixtures.EggInfoPkg,
+ fixtures.DistInfoPkg,
+ fixtures.DistInfoPkgWithDot,
+ fixtures.EggInfoFile,
+ unittest.TestCase):
+
+ version_pattern = r'\d+\.\d+(\.\d)?'
+
+ def test_retrieves_version_of_self(self):
+ pkg_version = version('egginfo-pkg')
+ assert isinstance(pkg_version, text)
+ assert re.match(self.version_pattern, pkg_version)
+
+ def test_retrieves_version_of_distinfo_pkg(self):
+ pkg_version = version('distinfo-pkg')
+ assert isinstance(pkg_version, text)
+ assert re.match(self.version_pattern, pkg_version)
+
+ def test_for_name_does_not_exist(self):
+ with self.assertRaises(PackageNotFoundError):
+ distribution('does-not-exist')
+
+ def test_name_normalization(self):
+ names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot'
+ for name in names:
+ with self.subTest(name):
+ assert distribution(name).metadata['Name'] == 'pkg.dot'
+
+ def test_for_top_level(self):
+ self.assertEqual(
+ distribution('egginfo-pkg').read_text('top_level.txt').strip(),
+ 'mod')
+
+ def test_read_text(self):
+ top_level = [
+ path for path in files('egginfo-pkg')
+ if path.name == 'top_level.txt'
+ ][0]
+ self.assertEqual(top_level.read_text(), 'mod\n')
+
+ def test_entry_points(self):
+ entries = dict(entry_points()['entries'])
+ ep = entries['main']
+ self.assertEqual(ep.value, 'mod:main')
+ self.assertEqual(ep.extras, [])
+
+ def test_metadata_for_this_package(self):
+ md = metadata('egginfo-pkg')
+ assert md['author'] == 'Steven Ma'
+ assert md['LICENSE'] == 'Unknown'
+ assert md['Name'] == 'egginfo-pkg'
+ classifiers = md.get_all('Classifier')
+ assert 'Topic :: Software Development :: Libraries' in classifiers
+
+ def test_importlib_metadata_version(self):
+ resolved = version('importlib-metadata')
+ assert re.match(self.version_pattern, resolved)
+
+ @staticmethod
+ def _test_files(files):
+ root = files[0].root
+ for file in files:
+ assert file.root == root
+ assert not file.hash or file.hash.value
+ assert not file.hash or file.hash.mode == 'sha256'
+ assert not file.size or file.size >= 0
+ assert file.locate().exists()
+ assert isinstance(file.read_binary(), bytes)
+ if file.name.endswith('.py'):
+ file.read_text()
+
+ def test_file_hash_repr(self):
+ try:
+ assertRegex = self.assertRegex
+ except AttributeError:
+ # Python 2
+ assertRegex = self.assertRegexpMatches
+
+ util = [
+ p for p in files('distinfo-pkg')
+ if p.name == 'mod.py'
+ ][0]
+ assertRegex(
+ repr(util.hash),
+ '<FileHash mode: sha256 value: .*>')
+
+ def test_files_dist_info(self):
+ self._test_files(files('distinfo-pkg'))
+
+ def test_files_egg_info(self):
+ self._test_files(files('egginfo-pkg'))
+
+ def test_version_egg_info_file(self):
+ self.assertEqual(version('egginfo-file'), '0.1')
+
+ def test_requires_egg_info_file(self):
+ requirements = requires('egginfo-file')
+ self.assertIsNone(requirements)
+
+ def test_requires_egg_info(self):
+ deps = requires('egginfo-pkg')
+ assert len(deps) == 2
+ assert any(
+ dep == 'wheel >= 1.0; python_version >= "2.7"'
+ for dep in deps
+ )
+
+ def test_requires_dist_info(self):
+ deps = requires('distinfo-pkg')
+ assert len(deps) == 2
+ assert all(deps)
+ assert 'wheel >= 1.0' in deps
+ assert "pytest; extra == 'test'" in deps
+
+ def test_more_complex_deps_requires_text(self):
+ requires = textwrap.dedent("""
+ dep1
+ dep2
+
+ [:python_version < "3"]
+ dep3
+
+ [extra1]
+ dep4
+
+ [extra2:python_version < "3"]
+ dep5
+ """)
+ deps = sorted(Distribution._deps_from_requires_text(requires))
+ expected = [
+ 'dep1',
+ 'dep2',
+ 'dep3; python_version < "3"',
+ 'dep4; extra == "extra1"',
+ 'dep5; (python_version < "3") and extra == "extra2"',
+ ]
+ # It's important that the environment marker expression be
+ # wrapped in parentheses to avoid the following 'and' binding more
+ # tightly than some other part of the environment expression.
+
+ assert deps == expected
+
+
+class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase):
+ def test_name_normalization(self):
+ names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot'
+ for name in names:
+ with self.subTest(name):
+ assert distribution(name).metadata['Name'] == 'pkg.dot'
+
+
+class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase):
+ def test_find_distributions_specified_path(self):
+ dists = Distribution.discover(path=[str(self.site_dir)])
+ assert any(
+ dist.metadata['Name'] == 'distinfo-pkg'
+ for dist in dists
+ )
+
+ def test_distribution_at_pathlib(self):
+ """Demonstrate how to load metadata direct from a directory.
+ """
+ dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
+ dist = Distribution.at(dist_info_path)
+ assert dist.version == '1.0.0'
+
+ def test_distribution_at_str(self):
+ dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
+ dist = Distribution.at(str(dist_info_path))
+ assert dist.version == '1.0.0'
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_integration.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_integration.py
new file mode 100644
index 0000000000..377574c448
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_integration.py
@@ -0,0 +1,54 @@
+# coding: utf-8
+
+from __future__ import unicode_literals
+
+import unittest
+import packaging.requirements
+import packaging.version
+
+from . import fixtures
+from importlib_metadata import (
+ Distribution,
+ _compat,
+ version,
+ )
+
+
+class IntegrationTests(fixtures.DistInfoPkg, unittest.TestCase):
+
+ def test_package_spec_installed(self):
+ """
+ Illustrate the recommended procedure to determine if
+ a specified version of a package is installed.
+ """
+ def is_installed(package_spec):
+ req = packaging.requirements.Requirement(package_spec)
+ return version(req.name) in req.specifier
+
+ assert is_installed('distinfo-pkg==1.0')
+ assert is_installed('distinfo-pkg>=1.0,<2.0')
+ assert not is_installed('distinfo-pkg<1.0')
+
+
+class FinderTests(fixtures.Fixtures, unittest.TestCase):
+
+ def test_finder_without_module(self):
+ class ModuleFreeFinder(fixtures.NullFinder):
+ """
+ A finder without an __module__ attribute
+ """
+ def __getattribute__(self, name):
+ if name == '__module__':
+ raise AttributeError(name)
+ return super().__getattribute__(name)
+
+ self.fixtures.enter_context(
+ fixtures.install_finder(ModuleFreeFinder()))
+ _compat.disable_stdlib_finder()
+
+
+class LocalProjectTests(fixtures.LocalPackage, unittest.TestCase):
+ def test_find_local(self):
+ dist = Distribution._local()
+ assert dist.metadata['Name'] == 'local-pkg'
+ assert dist.version == '2.0.1'
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_main.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_main.py
new file mode 100644
index 0000000000..847750bc30
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_main.py
@@ -0,0 +1,285 @@
+# coding: utf-8
+from __future__ import unicode_literals
+
+import re
+import json
+import pickle
+import textwrap
+import unittest
+import importlib
+import importlib_metadata
+import pyfakefs.fake_filesystem_unittest as ffs
+
+from . import fixtures
+from importlib_metadata import (
+ Distribution, EntryPoint, MetadataPathFinder,
+ PackageNotFoundError, distributions,
+ entry_points, metadata, version,
+ )
+
+try:
+ from builtins import str as text
+except ImportError:
+ from __builtin__ import unicode as text
+
+
+class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
+ version_pattern = r'\d+\.\d+(\.\d)?'
+
+ def test_retrieves_version_of_self(self):
+ dist = Distribution.from_name('distinfo-pkg')
+ assert isinstance(dist.version, text)
+ assert re.match(self.version_pattern, dist.version)
+
+ def test_for_name_does_not_exist(self):
+ with self.assertRaises(PackageNotFoundError):
+ Distribution.from_name('does-not-exist')
+
+ def test_package_not_found_mentions_metadata(self):
+ """
+ When a package is not found, that could indicate that the
+ packgae is not installed or that it is installed without
+ metadata. Ensure the exception mentions metadata to help
+ guide users toward the cause. See #124.
+ """
+ with self.assertRaises(PackageNotFoundError) as ctx:
+ Distribution.from_name('does-not-exist')
+
+ assert "metadata" in str(ctx.exception)
+
+ def test_new_style_classes(self):
+ self.assertIsInstance(Distribution, type)
+ self.assertIsInstance(MetadataPathFinder, type)
+
+
+class ImportTests(fixtures.DistInfoPkg, unittest.TestCase):
+ def test_import_nonexistent_module(self):
+ # Ensure that the MetadataPathFinder does not crash an import of a
+ # non-existent module.
+ with self.assertRaises(ImportError):
+ importlib.import_module('does_not_exist')
+
+ def test_resolve(self):
+ entries = dict(entry_points()['entries'])
+ ep = entries['main']
+ self.assertEqual(ep.load().__name__, "main")
+
+ def test_entrypoint_with_colon_in_name(self):
+ entries = dict(entry_points()['entries'])
+ ep = entries['ns:sub']
+ self.assertEqual(ep.value, 'mod:main')
+
+ def test_resolve_without_attr(self):
+ ep = EntryPoint(
+ name='ep',
+ value='importlib_metadata',
+ group='grp',
+ )
+ assert ep.load() is importlib_metadata
+
+
+class NameNormalizationTests(
+ fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
+ @staticmethod
+ def pkg_with_dashes(site_dir):
+ """
+ Create minimal metadata for a package with dashes
+ in the name (and thus underscores in the filename).
+ """
+ metadata_dir = site_dir / 'my_pkg.dist-info'
+ metadata_dir.mkdir()
+ metadata = metadata_dir / 'METADATA'
+ with metadata.open('w') as strm:
+ strm.write('Version: 1.0\n')
+ return 'my-pkg'
+
+ def test_dashes_in_dist_name_found_as_underscores(self):
+ """
+ For a package with a dash in the name, the dist-info metadata
+ uses underscores in the name. Ensure the metadata loads.
+ """
+ pkg_name = self.pkg_with_dashes(self.site_dir)
+ assert version(pkg_name) == '1.0'
+
+ @staticmethod
+ def pkg_with_mixed_case(site_dir):
+ """
+ Create minimal metadata for a package with mixed case
+ in the name.
+ """
+ metadata_dir = site_dir / 'CherryPy.dist-info'
+ metadata_dir.mkdir()
+ metadata = metadata_dir / 'METADATA'
+ with metadata.open('w') as strm:
+ strm.write('Version: 1.0\n')
+ return 'CherryPy'
+
+ def test_dist_name_found_as_any_case(self):
+ """
+ Ensure the metadata loads when queried with any case.
+ """
+ pkg_name = self.pkg_with_mixed_case(self.site_dir)
+ assert version(pkg_name) == '1.0'
+ assert version(pkg_name.lower()) == '1.0'
+ assert version(pkg_name.upper()) == '1.0'
+
+
+class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
+ @staticmethod
+ def pkg_with_non_ascii_description(site_dir):
+ """
+ Create minimal metadata for a package with non-ASCII in
+ the description.
+ """
+ metadata_dir = site_dir / 'portend.dist-info'
+ metadata_dir.mkdir()
+ metadata = metadata_dir / 'METADATA'
+ with metadata.open('w', encoding='utf-8') as fp:
+ fp.write('Description: pôrˈtend\n')
+ return 'portend'
+
+ @staticmethod
+ def pkg_with_non_ascii_description_egg_info(site_dir):
+ """
+ Create minimal metadata for an egg-info package with
+ non-ASCII in the description.
+ """
+ metadata_dir = site_dir / 'portend.dist-info'
+ metadata_dir.mkdir()
+ metadata = metadata_dir / 'METADATA'
+ with metadata.open('w', encoding='utf-8') as fp:
+ fp.write(textwrap.dedent("""
+ Name: portend
+
+ pôrˈtend
+ """).lstrip())
+ return 'portend'
+
+ def test_metadata_loads(self):
+ pkg_name = self.pkg_with_non_ascii_description(self.site_dir)
+ meta = metadata(pkg_name)
+ assert meta['Description'] == 'pôrˈtend'
+
+ def test_metadata_loads_egg_info(self):
+ pkg_name = self.pkg_with_non_ascii_description_egg_info(self.site_dir)
+ meta = metadata(pkg_name)
+ assert meta.get_payload() == 'pôrˈtend\n'
+
+
+class DiscoveryTests(fixtures.EggInfoPkg,
+ fixtures.DistInfoPkg,
+ unittest.TestCase):
+
+ def test_package_discovery(self):
+ dists = list(distributions())
+ assert all(
+ isinstance(dist, Distribution)
+ for dist in dists
+ )
+ assert any(
+ dist.metadata['Name'] == 'egginfo-pkg'
+ for dist in dists
+ )
+ assert any(
+ dist.metadata['Name'] == 'distinfo-pkg'
+ for dist in dists
+ )
+
+ def test_invalid_usage(self):
+ with self.assertRaises(ValueError):
+ list(distributions(context='something', name='else'))
+
+
+class DirectoryTest(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
+ def test_egg_info(self):
+ # make an `EGG-INFO` directory that's unrelated
+ self.site_dir.joinpath('EGG-INFO').mkdir()
+ # used to crash with `IsADirectoryError`
+ with self.assertRaises(PackageNotFoundError):
+ version('unknown-package')
+
+ def test_egg(self):
+ egg = self.site_dir.joinpath('foo-3.6.egg')
+ egg.mkdir()
+ with self.add_sys_path(egg):
+ with self.assertRaises(PackageNotFoundError):
+ version('foo')
+
+
+class MissingSysPath(fixtures.OnSysPath, unittest.TestCase):
+ site_dir = '/does-not-exist'
+
+ def test_discovery(self):
+ """
+ Discovering distributions should succeed even if
+ there is an invalid path on sys.path.
+ """
+ importlib_metadata.distributions()
+
+
+class InaccessibleSysPath(fixtures.OnSysPath, ffs.TestCase):
+ site_dir = '/access-denied'
+
+ def setUp(self):
+ super(InaccessibleSysPath, self).setUp()
+ self.setUpPyfakefs()
+ self.fs.create_dir(self.site_dir, perm_bits=000)
+
+ def test_discovery(self):
+ """
+ Discovering distributions should succeed even if
+ there is an invalid path on sys.path.
+ """
+ list(importlib_metadata.distributions())
+
+
+class TestEntryPoints(unittest.TestCase):
+ def __init__(self, *args):
+ super(TestEntryPoints, self).__init__(*args)
+ self.ep = importlib_metadata.EntryPoint('name', 'value', 'group')
+
+ def test_entry_point_pickleable(self):
+ revived = pickle.loads(pickle.dumps(self.ep))
+ assert revived == self.ep
+
+ def test_immutable(self):
+ """EntryPoints should be immutable"""
+ with self.assertRaises(AttributeError):
+ self.ep.name = 'badactor'
+
+ def test_repr(self):
+ assert 'EntryPoint' in repr(self.ep)
+ assert 'name=' in repr(self.ep)
+ assert "'name'" in repr(self.ep)
+
+ def test_hashable(self):
+ """EntryPoints should be hashable"""
+ hash(self.ep)
+
+ def test_json_dump(self):
+ """
+ json should not expect to be able to dump an EntryPoint
+ """
+ with self.assertRaises(Exception):
+ json.dumps(self.ep)
+
+ def test_module(self):
+ assert self.ep.module == 'value'
+
+ def test_attr(self):
+ assert self.ep.attr is None
+
+
+class FileSystem(
+ fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder,
+ unittest.TestCase):
+ def test_unicode_dir_on_sys_path(self):
+ """
+ Ensure a Unicode subdirectory of a directory on sys.path
+ does not crash.
+ """
+ fixtures.build_files(
+ {self.unicode_filename(): {}},
+ prefix=self.site_dir,
+ )
+ list(distributions())
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_zip.py b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_zip.py
new file mode 100644
index 0000000000..5cebcd02f7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/tests/test_zip.py
@@ -0,0 +1,80 @@
+import sys
+import unittest
+
+from importlib_metadata import (
+ distribution, entry_points, files, PackageNotFoundError,
+ version, distributions,
+ )
+
+try:
+ from importlib import resources
+ getattr(resources, 'files')
+ getattr(resources, 'as_file')
+except (ImportError, AttributeError):
+ import importlib_resources as resources
+
+try:
+ from contextlib import ExitStack
+except ImportError:
+ from contextlib2 import ExitStack
+
+
+class TestZip(unittest.TestCase):
+ root = 'tests.data'
+
+ def _fixture_on_path(self, filename):
+ pkg_file = resources.files(self.root).joinpath(filename)
+ file = self.resources.enter_context(resources.as_file(pkg_file))
+ assert file.name.startswith('example-'), file.name
+ sys.path.insert(0, str(file))
+ self.resources.callback(sys.path.pop, 0)
+
+ def setUp(self):
+ # Find the path to the example-*.whl so we can add it to the front of
+ # sys.path, where we'll then try to find the metadata thereof.
+ self.resources = ExitStack()
+ self.addCleanup(self.resources.close)
+ self._fixture_on_path('example-21.12-py3-none-any.whl')
+
+ def test_zip_version(self):
+ self.assertEqual(version('example'), '21.12')
+
+ def test_zip_version_does_not_match(self):
+ with self.assertRaises(PackageNotFoundError):
+ version('definitely-not-installed')
+
+ def test_zip_entry_points(self):
+ scripts = dict(entry_points()['console_scripts'])
+ entry_point = scripts['example']
+ self.assertEqual(entry_point.value, 'example:main')
+ entry_point = scripts['Example']
+ self.assertEqual(entry_point.value, 'example:main')
+
+ def test_missing_metadata(self):
+ self.assertIsNone(distribution('example').read_text('does not exist'))
+
+ def test_case_insensitive(self):
+ self.assertEqual(version('Example'), '21.12')
+
+ def test_files(self):
+ for file in files('example'):
+ path = str(file.dist.locate_file(file))
+ assert '.whl/' in path, path
+
+ def test_one_distribution(self):
+ dists = list(distributions(path=sys.path[:1]))
+ assert len(dists) == 1
+
+
+class TestEgg(TestZip):
+ def setUp(self):
+ # Find the path to the example-*.egg so we can add it to the front of
+ # sys.path, where we'll then try to find the metadata thereof.
+ self.resources = ExitStack()
+ self.addCleanup(self.resources.close)
+ self._fixture_on_path('example-21.12-py3.6.egg')
+
+ def test_files(self):
+ for file in files('example'):
+ path = str(file.dist.locate_file(file))
+ assert '.egg/' in path, path
diff --git a/testing/web-platform/tests/tools/third_party/importlib_metadata/tox.ini b/testing/web-platform/tests/tools/third_party/importlib_metadata/tox.ini
new file mode 100644
index 0000000000..1f0e975783
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/importlib_metadata/tox.ini
@@ -0,0 +1,97 @@
+[tox]
+envlist = {py27,py35,py36,py37,py38}{,-cov,-diffcov},qa,docs,perf
+skip_missing_interpreters = True
+minversion = 3.2
+# Ensure that a late version of pip is used even on tox-venv.
+requires =
+ tox-pip-version>=0.0.6
+
+[testenv]
+pip_version = pip
+commands =
+ !cov,!diffcov: python -m unittest discover {posargs}
+ cov,diffcov: python -m coverage run {[coverage]rc} -m unittest discover {posargs}
+ cov,diffcov: python -m coverage combine {[coverage]rc}
+ cov: python -m coverage html {[coverage]rc}
+ cov: python -m coverage xml {[coverage]rc}
+ cov: python -m coverage report -m {[coverage]rc} --fail-under=100
+ diffcov: python -m coverage xml {[coverage]rc}
+ diffcov: diff-cover coverage.xml --html-report diffcov.html
+ diffcov: diff-cover coverage.xml --fail-under=100
+usedevelop = True
+passenv =
+ PYTHON*
+ LANG*
+ LC_*
+ PYV
+deps =
+ cov,diffcov: coverage>=4.5
+ diffcov: diff_cover
+ pyfakefs
+setenv =
+ cov: COVERAGE_PROCESS_START={[coverage]rcfile}
+ cov: COVERAGE_OPTIONS="-p"
+ cov: COVERAGE_FILE={toxinidir}/.coverage
+ py27: PYV=2
+ py35,py36,py37,py38: PYV=3
+ # workaround deprecation warnings in pip's vendored packages
+ PYTHONWARNINGS=ignore:Using or importing the ABCs:DeprecationWarning:pip._vendor
+extras =
+ testing
+
+
+[testenv:qa]
+basepython = python3.7
+commands =
+ python -m flake8 importlib_metadata
+ mypy importlib_metadata
+deps =
+ mypy
+ flake8
+ flufl.flake8
+extras =
+
+
+[testenv:docs]
+basepython = python3
+commands =
+ sphinx-build docs build/sphinx/html
+extras =
+ docs
+
+
+[testenv:perf]
+use_develop = False
+deps =
+ ipython
+commands =
+ python -m timeit -s 'import importlib_metadata' -- 'importlib_metadata.distribution("ipython")'
+
+
+[testenv:release]
+basepython = python3
+deps =
+ twine
+ wheel
+ setuptools
+ keyring
+ setuptools_scm
+passenv =
+ TWINE_PASSWORD
+setenv =
+ TWINE_USERNAME = {env:TWINE_USERNAME:__token__}
+commands =
+ python setup.py sdist bdist_wheel
+ python -m twine {posargs} upload dist/*
+
+
+[coverage]
+rcfile = {toxinidir}/coverage.ini
+rc = --rcfile="{[coverage]rcfile}"
+
+
+[flake8]
+hang-closing = True
+jobs = 1
+max-line-length = 79
+enable-extensions = U4
diff --git a/testing/web-platform/tests/tools/third_party/iniconfig/.gitignore b/testing/web-platform/tests/tools/third_party/iniconfig/.gitignore
new file mode 100644
index 0000000000..89e6234380
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/.gitignore
@@ -0,0 +1,8 @@
+*.egg-info
+*.pyc
+.cache/
+.eggs/
+build/
+dist/
+__pycache__
+.tox/
diff --git a/testing/web-platform/tests/tools/third_party/iniconfig/.landscape.yml b/testing/web-platform/tests/tools/third_party/iniconfig/.landscape.yml
new file mode 100644
index 0000000000..5212ddea41
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/.landscape.yml
@@ -0,0 +1,5 @@
+pep8:
+ full: true
+python-targets:
+ - 2
+ - 3
diff --git a/testing/web-platform/tests/tools/third_party/iniconfig/.travis.yml b/testing/web-platform/tests/tools/third_party/iniconfig/.travis.yml
new file mode 100644
index 0000000000..e3fee06c9f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/.travis.yml
@@ -0,0 +1,18 @@
+language: python
+python:
+- '2.7'
+- '3.4'
+- '3.5'
+- nightly
+- pypy
+install: pip install setuptools_scm tox
+script: tox -e py
+deploy:
+ provider: pypi
+ user: ronny
+ password:
+ secure: DsRVX99HA6+3JoXOVP/nPXeabJy2P73ws7Ager/e4rx3p3jS74bId09XsBU46bAT9ANmRWPR8y5DRi5Zlq0WQ2uXoR55wmsdu2KUegk6bDIS4Iop8DFxY8Kjou9s8RZbDTP27LfuYXKMO1rDW/xa6EhiotYRodekeZUz3P3MYjIi6rBV2Rz3vwmInpkKOti7AFwAsCGmCCK13irmPJEp5nwl3RgeKu2AGaolw9eypJXeNLUcNDVQ88ZUUXQCkwgq7a1BkK6NMeQLMrWAE1bD3amCbVXHCR9TaVx1ZH1dnha5Jcfj3gEFucTmInWWes5u9rypvsCkSxKtSqdiUA7BMJq7XykV7nGNplGLm2sq4+KSYlf3gZXg4XNXQkNOi4EBtRvathfFziD2SZgdtjiQX2neh0dMjf9czc/uCYkKYCFLeozdw2oQQ+BsxhQfsmU2ILGCFHyFikmDbBqZOWfQE5TN3itQqV3TFK8sOHQ8iy3MDShs+lBk9AUwbCA5YbRh8hJKhgXyEsDpisC417Pj22+TbutTj7v3Rmpe/st4hoL740grWc3PSVUBaypG0RsoafSDZWnYnTC+0aakd6QEb5S9wnMkP94kijYjjF6yUInuT05wdbQv5XcSXqAdGzBqB5jNNdfwgWVCOlwGfjnvzKllhF3PmWPW/nfmQpGOQh4=
+ on:
+ tags: true
+ distributions: sdist bdist_wheel
+ repo: RonnyPfannschmidt/iniconfig
diff --git a/testing/web-platform/tests/tools/third_party/iniconfig/CHANGELOG b/testing/web-platform/tests/tools/third_party/iniconfig/CHANGELOG
new file mode 100644
index 0000000000..679919fcd2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/CHANGELOG
@@ -0,0 +1,32 @@
+1.1.1
+=========
+
+* fix version determination (thanks @florimondmanca)
+
+1.1.0
+=====
+
+- typing stubs (thanks @bluetech)
+- ci fixes
+
+1.0.1
+======
+
+pytest 5+ support
+
+1.0
+====
+
+- re-sync with pylib codebase
+
+0.2
+==================
+
+- added ability to ask "name in iniconfig", i.e. to check
+ if a section is contained.
+
+- fix bug in "name=value" parsing where value was "x=3"
+
+- allow for ': ' to delimit name=value pairs, so that e.g. .pypirc files
+ like http://docs.python.org/distutils/packageindex.html
+ can be successfully parsed
diff --git a/testing/web-platform/tests/tools/third_party/iniconfig/LICENSE b/testing/web-platform/tests/tools/third_party/iniconfig/LICENSE
new file mode 100644
index 0000000000..31ecdfb1db
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/LICENSE
@@ -0,0 +1,19 @@
+
+ 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/testing/web-platform/tests/tools/third_party/iniconfig/MANIFEST.in b/testing/web-platform/tests/tools/third_party/iniconfig/MANIFEST.in
new file mode 100644
index 0000000000..06be514ae5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/MANIFEST.in
@@ -0,0 +1,5 @@
+include LICENSE
+include example.ini
+include tox.ini
+include src/iniconfig/py.typed
+recursive-include src *.pyi
diff --git a/testing/web-platform/tests/tools/third_party/iniconfig/README.txt b/testing/web-platform/tests/tools/third_party/iniconfig/README.txt
new file mode 100644
index 0000000000..6bbad9a8d9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/README.txt
@@ -0,0 +1,51 @@
+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/testing/web-platform/tests/tools/third_party/iniconfig/example.ini b/testing/web-platform/tests/tools/third_party/iniconfig/example.ini
new file mode 100644
index 0000000000..65481d2074
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/example.ini
@@ -0,0 +1,10 @@
+
+# content of example.ini
+[section1] # comment
+name1=value1 # comment
+name1b=value1,value2 # comment
+
+[section2]
+name2=
+ line1
+ line2
diff --git a/testing/web-platform/tests/tools/third_party/iniconfig/pyproject.toml b/testing/web-platform/tests/tools/third_party/iniconfig/pyproject.toml
new file mode 100644
index 0000000000..b2725d8f65
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/pyproject.toml
@@ -0,0 +1,5 @@
+[build-system]
+requires = ["setuptools>=41.2.0", "wheel", "setuptools_scm>3"]
+
+
+[tool.setuptools_scm] \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/iniconfig/setup.cfg b/testing/web-platform/tests/tools/third_party/iniconfig/setup.cfg
new file mode 100644
index 0000000000..3c6e79cf31
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal=1
diff --git a/testing/web-platform/tests/tools/third_party/iniconfig/setup.py b/testing/web-platform/tests/tools/third_party/iniconfig/setup.py
new file mode 100644
index 0000000000..f46f3214de
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/setup.py
@@ -0,0 +1,46 @@
+"""
+iniconfig: brain-dead simple config-ini parsing.
+
+compatible CPython 2.3 through to CPython 3.2, Jython, PyPy
+
+(c) 2010 Ronny Pfannschmidt, Holger Krekel
+"""
+
+from setuptools import setup
+
+
+def main():
+ with open('README.txt') as fp:
+ readme = fp.read()
+ setup(
+ name='iniconfig',
+ packages=['iniconfig'],
+ package_dir={'': 'src'},
+ description='iniconfig: brain-dead simple config-ini parsing',
+ long_description=readme,
+ use_scm_version=True,
+ url='http://github.com/RonnyPfannschmidt/iniconfig',
+ license='MIT License',
+ platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
+ author='Ronny Pfannschmidt, Holger Krekel',
+ author_email=(
+ 'opensource@ronnypfannschmidt.de, holger.krekel@gmail.com'),
+ classifiers=[
+ 'Development Status :: 4 - Beta',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: POSIX',
+ 'Operating System :: Microsoft :: Windows',
+ 'Operating System :: MacOS :: MacOS X',
+ 'Topic :: Software Development :: Libraries',
+ 'Topic :: Utilities',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 3',
+ ],
+ include_package_data=True,
+ zip_safe=False,
+ )
+
+if __name__ == '__main__':
+ main()
diff --git a/testing/web-platform/tests/tools/third_party/iniconfig/src/iniconfig/__init__.py b/testing/web-platform/tests/tools/third_party/iniconfig/src/iniconfig/__init__.py
new file mode 100644
index 0000000000..6ad9eaf868
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/src/iniconfig/__init__.py
@@ -0,0 +1,165 @@
+""" 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/testing/web-platform/tests/tools/third_party/iniconfig/src/iniconfig/__init__.pyi b/testing/web-platform/tests/tools/third_party/iniconfig/src/iniconfig/__init__.pyi
new file mode 100644
index 0000000000..b6284bec3f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/src/iniconfig/__init__.pyi
@@ -0,0 +1,31 @@
+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/testing/web-platform/tests/tools/third_party/iniconfig/src/iniconfig/py.typed b/testing/web-platform/tests/tools/third_party/iniconfig/src/iniconfig/py.typed
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/src/iniconfig/py.typed
diff --git a/testing/web-platform/tests/tools/third_party/iniconfig/testing/conftest.py b/testing/web-platform/tests/tools/third_party/iniconfig/testing/conftest.py
new file mode 100644
index 0000000000..d265a29f86
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/testing/conftest.py
@@ -0,0 +1,2 @@
+
+option_doctestglob = "README.txt"
diff --git a/testing/web-platform/tests/tools/third_party/iniconfig/testing/test_iniconfig.py b/testing/web-platform/tests/tools/third_party/iniconfig/testing/test_iniconfig.py
new file mode 100644
index 0000000000..fe12421e5a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/testing/test_iniconfig.py
@@ -0,0 +1,314 @@
+import py
+import pytest
+from iniconfig import IniConfig, ParseError, __all__ as ALL
+from iniconfig import iscommentline
+from textwrap import dedent
+
+
+check_tokens = {
+ 'section': (
+ '[section]',
+ [(0, 'section', None, None)]
+ ),
+ 'value': (
+ 'value = 1',
+ [(0, None, 'value', '1')]
+ ),
+ 'value in section': (
+ '[section]\nvalue=1',
+ [(0, 'section', None, None), (1, 'section', 'value', '1')]
+ ),
+ 'value with continuation': (
+ 'names =\n Alice\n Bob',
+ [(0, None, 'names', 'Alice\nBob')]
+ ),
+ 'value with aligned continuation': (
+ 'names = Alice\n'
+ ' Bob',
+ [(0, None, 'names', 'Alice\nBob')]
+ ),
+ 'blank line': (
+ '[section]\n\nvalue=1',
+ [(0, 'section', None, None), (2, 'section', 'value', '1')]
+ ),
+ 'comment': (
+ '# comment',
+ []
+ ),
+ 'comment on value': (
+ 'value = 1',
+ [(0, None, 'value', '1')]
+ ),
+
+ 'comment on section': (
+ '[section] #comment',
+ [(0, 'section', None, None)]
+ ),
+ 'comment2': (
+ '; comment',
+ []
+ ),
+
+ 'comment2 on section': (
+ '[section] ;comment',
+ [(0, 'section', None, None)]
+ ),
+ 'pseudo section syntax in value': (
+ 'name = value []',
+ [(0, None, 'name', 'value []')]
+ ),
+ 'assignment in value': (
+ 'value = x = 3',
+ [(0, None, 'value', 'x = 3')]
+ ),
+ 'use of colon for name-values': (
+ 'name: y',
+ [(0, None, 'name', 'y')]
+ ),
+ 'use of colon without space': (
+ 'value:y=5',
+ [(0, None, 'value', 'y=5')]
+ ),
+ 'equality gets precedence': (
+ 'value=xyz:5',
+ [(0, None, 'value', 'xyz:5')]
+ ),
+
+}
+
+
+@pytest.fixture(params=sorted(check_tokens))
+def input_expected(request):
+ return check_tokens[request.param]
+
+
+@pytest.fixture
+def input(input_expected):
+ return input_expected[0]
+
+
+@pytest.fixture
+def expected(input_expected):
+ return input_expected[1]
+
+
+def parse(input):
+ # only for testing purposes - _parse() does not use state except path
+ ini = object.__new__(IniConfig)
+ ini.path = "sample"
+ return ini._parse(input.splitlines(True))
+
+
+def parse_a_error(input):
+ return py.test.raises(ParseError, parse, input)
+
+
+def test_tokenize(input, expected):
+ parsed = parse(input)
+ assert parsed == expected
+
+
+def test_parse_empty():
+ parsed = parse("")
+ assert not parsed
+ ini = IniConfig("sample", "")
+ assert not ini.sections
+
+
+def test_ParseError():
+ e = ParseError("filename", 0, "hello")
+ assert str(e) == "filename:1: hello"
+
+
+def test_continuation_needs_perceeding_token():
+ excinfo = parse_a_error(' Foo')
+ assert excinfo.value.lineno == 0
+
+
+def test_continuation_cant_be_after_section():
+ excinfo = parse_a_error('[section]\n Foo')
+ assert excinfo.value.lineno == 1
+
+
+def test_section_cant_be_empty():
+ excinfo = parse_a_error('[]')
+ assert excinfo.value.lineno == 0
+
+
+@py.test.mark.parametrize('line', [
+ '!!',
+ ])
+def test_error_on_weird_lines(line):
+ parse_a_error(line)
+
+
+def test_iniconfig_from_file(tmpdir):
+ path = tmpdir/'test.txt'
+ path.write('[metadata]\nname=1')
+
+ config = IniConfig(path=path)
+ assert list(config.sections) == ['metadata']
+ config = IniConfig(path, "[diff]")
+ assert list(config.sections) == ['diff']
+ with pytest.raises(TypeError):
+ IniConfig(data=path.read())
+
+
+def test_iniconfig_section_first(tmpdir):
+ with pytest.raises(ParseError) as excinfo:
+ IniConfig("x", data='name=1')
+ assert excinfo.value.msg == "no section header defined"
+
+
+def test_iniconig_section_duplicate_fails():
+ with pytest.raises(ParseError) as excinfo:
+ IniConfig("x", data='[section]\n[section]')
+ assert 'duplicate section' in str(excinfo.value)
+
+
+def test_iniconfig_duplicate_key_fails():
+ with pytest.raises(ParseError) as excinfo:
+ IniConfig("x", data='[section]\nname = Alice\nname = bob')
+
+ assert 'duplicate name' in str(excinfo.value)
+
+
+def test_iniconfig_lineof():
+ config = IniConfig("x.ini", data=(
+ '[section]\n'
+ 'value = 1\n'
+ '[section2]\n'
+ '# comment\n'
+ 'value =2'
+ ))
+
+ assert config.lineof('missing') is None
+ assert config.lineof('section') == 1
+ assert config.lineof('section2') == 3
+ assert config.lineof('section', 'value') == 2
+ assert config.lineof('section2', 'value') == 5
+
+ assert config['section'].lineof('value') == 2
+ assert config['section2'].lineof('value') == 5
+
+
+def test_iniconfig_get_convert():
+ config = IniConfig("x", data='[section]\nint = 1\nfloat = 1.1')
+ assert config.get('section', 'int') == '1'
+ assert config.get('section', 'int', convert=int) == 1
+
+
+def test_iniconfig_get_missing():
+ config = IniConfig("x", data='[section]\nint = 1\nfloat = 1.1')
+ assert config.get('section', 'missing', default=1) == 1
+ assert config.get('section', 'missing') is None
+
+
+def test_section_get():
+ config = IniConfig("x", data='[section]\nvalue=1')
+ section = config['section']
+ assert section.get('value', convert=int) == 1
+ assert section.get('value', 1) == "1"
+ assert section.get('missing', 2) == 2
+
+
+def test_missing_section():
+ config = IniConfig("x", data='[section]\nvalue=1')
+ with pytest.raises(KeyError):
+ config["other"]
+
+
+def test_section_getitem():
+ config = IniConfig("x", data='[section]\nvalue=1')
+ assert config['section']['value'] == '1'
+ assert config['section']['value'] == '1'
+
+
+def test_section_iter():
+ config = IniConfig("x", data='[section]\nvalue=1')
+ names = list(config['section'])
+ assert names == ['value']
+ items = list(config['section'].items())
+ assert items == [('value', '1')]
+
+
+def test_config_iter():
+ config = IniConfig("x.ini", data=dedent('''
+ [section1]
+ value=1
+ [section2]
+ value=2
+ '''))
+ l = list(config)
+ assert len(l) == 2
+ assert l[0].name == 'section1'
+ assert l[0]['value'] == '1'
+ assert l[1].name == 'section2'
+ assert l[1]['value'] == '2'
+
+
+def test_config_contains():
+ config = IniConfig("x.ini", data=dedent('''
+ [section1]
+ value=1
+ [section2]
+ value=2
+ '''))
+ assert 'xyz' not in config
+ assert 'section1' in config
+ assert 'section2' in config
+
+
+def test_iter_file_order():
+ config = IniConfig("x.ini", data="""
+[section2] #cpython dict ordered before section
+value = 1
+value2 = 2 # dict ordered before value
+[section]
+a = 1
+b = 2
+""")
+ l = list(config)
+ secnames = [x.name for x in l]
+ assert secnames == ['section2', 'section']
+ assert list(config['section2']) == ['value', 'value2']
+ assert list(config['section']) == ['a', 'b']
+
+
+def test_example_pypirc():
+ config = IniConfig("pypirc", data=dedent('''
+ [distutils]
+ index-servers =
+ pypi
+ other
+
+ [pypi]
+ repository: <repository-url>
+ username: <username>
+ password: <password>
+
+ [other]
+ repository: http://example.com/pypi
+ username: <username>
+ password: <password>
+ '''))
+ distutils, pypi, other = list(config)
+ assert distutils["index-servers"] == "pypi\nother"
+ assert pypi['repository'] == '<repository-url>'
+ assert pypi['username'] == '<username>'
+ assert pypi['password'] == '<password>'
+ assert ['repository', 'username', 'password'] == list(other)
+
+
+def test_api_import():
+ assert ALL == ['IniConfig', 'ParseError']
+
+
+@pytest.mark.parametrize("line", [
+ "#qwe",
+ " #qwe",
+ ";qwe",
+ " ;qwe",
+])
+def test_iscommentline_true(line):
+ assert iscommentline(line)
diff --git a/testing/web-platform/tests/tools/third_party/iniconfig/tox.ini b/testing/web-platform/tests/tools/third_party/iniconfig/tox.ini
new file mode 100644
index 0000000000..298838bee0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/iniconfig/tox.ini
@@ -0,0 +1,14 @@
+[tox]
+envlist=py27,py26,py33,py34,py35
+
+
+[testenv]
+commands=
+ pytest {posargs}
+deps=
+ pytest
+
+
+[pytest]
+testpaths=
+ testing
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/.gitignore b/testing/web-platform/tests/tools/third_party/more-itertools/.gitignore
new file mode 100644
index 0000000000..229891fb43
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/.gitignore
@@ -0,0 +1,34 @@
+*.py[co]
+
+# Packages
+*.egg
+*.eggs
+*.egg-info
+dist
+build
+eggs
+parts
+bin
+var
+sdist
+develop-eggs
+.installed.cfg
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+.noseids
+
+# Docs by Sphinx
+_build
+
+# Environment
+.env
+
+# IDE files
+.idea
+.vscode
+.DS_Store
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/.travis.yml b/testing/web-platform/tests/tools/third_party/more-itertools/.travis.yml
new file mode 100644
index 0000000000..008fb0c67e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/.travis.yml
@@ -0,0 +1,26 @@
+sudo: false
+
+language: "python"
+
+python:
+ - "2.7"
+ - "3.4"
+ - "3.5"
+ - "3.6"
+ - "3.7-dev"
+ - "pypy-5.4.1"
+ - "pypy3"
+
+install:
+ - "pip install ."
+ - "pip install -U coveralls flake8"
+
+script:
+ - "coverage run --include='more_itertools/*.py' --omit='more_itertools/tests/*' setup.py test"
+ - "flake8 ."
+
+notifications:
+ email: false
+
+after_success:
+ - "coveralls"
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/LICENSE b/testing/web-platform/tests/tools/third_party/more-itertools/LICENSE
new file mode 100644
index 0000000000..0a523bece3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2012 Erik Rose
+
+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/testing/web-platform/tests/tools/third_party/more-itertools/MANIFEST.in b/testing/web-platform/tests/tools/third_party/more-itertools/MANIFEST.in
new file mode 100644
index 0000000000..ec800e3e02
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/MANIFEST.in
@@ -0,0 +1,8 @@
+include README.rst
+include LICENSE
+include docs/*.rst
+include docs/Makefile
+include docs/make.bat
+include docs/conf.py
+include fabfile.py
+include tox.ini
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/README.rst b/testing/web-platform/tests/tools/third_party/more-itertools/README.rst
new file mode 100644
index 0000000000..252b394737
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/README.rst
@@ -0,0 +1,59 @@
+==============
+More Itertools
+==============
+
+.. image:: https://coveralls.io/repos/github/erikrose/more-itertools/badge.svg?branch=master
+ :target: https://coveralls.io/github/erikrose/more-itertools?branch=master
+
+Python's ``itertools`` library is a gem - you can compose elegant solutions
+for a variety of problems with the functions it provides. In ``more-itertools``
+we collect additional building blocks, recipes, and routines for working with
+Python iterables.
+
+Getting started
+===============
+
+To get started, install the library with `pip <https://pip.pypa.io/en/stable/>`_:
+
+.. code-block:: shell
+
+ pip install more-itertools
+
+The recipes from the `itertools docs <https://docs.python.org/3/library/itertools.html#itertools-recipes>`_
+are included in the top-level package:
+
+.. code-block:: python
+
+ >>> from more_itertools import flatten
+ >>> iterable = [(0, 1), (2, 3)]
+ >>> list(flatten(iterable))
+ [0, 1, 2, 3]
+
+Several new recipes are available as well:
+
+.. code-block:: python
+
+ >>> from more_itertools import chunked
+ >>> iterable = [0, 1, 2, 3, 4, 5, 6, 7, 8]
+ >>> list(chunked(iterable, 3))
+ [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
+
+ >>> from more_itertools import spy
+ >>> iterable = (x * x for x in range(1, 6))
+ >>> head, iterable = spy(iterable, n=3)
+ >>> list(head)
+ [1, 4, 9]
+ >>> list(iterable)
+ [1, 4, 9, 16, 25]
+
+
+
+For the full listing of functions, see the `API documentation <https://more-itertools.readthedocs.io/en/latest/api.html>`_.
+
+Development
+===========
+
+``more-itertools`` is maintained by `@erikrose <https://github.com/erikrose>`_
+and `@bbayles <https://github.com/bbayles>`_, with help from `many others <https://github.com/erikrose/more-itertools/graphs/contributors>`_.
+If you have a problem or suggestion, please file a bug or pull request in this
+repository. Thanks for contributing!
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/docs/Makefile b/testing/web-platform/tests/tools/third_party/more-itertools/docs/Makefile
new file mode 100644
index 0000000000..47888da7b7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/docs/Makefile
@@ -0,0 +1,153 @@
+# 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) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+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 " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @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)/*
+
+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/more-itertools.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/more-itertools.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/more-itertools"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/more-itertools"
+ @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."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+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/testing/web-platform/tests/tools/third_party/more-itertools/docs/api.rst b/testing/web-platform/tests/tools/third_party/more-itertools/docs/api.rst
new file mode 100644
index 0000000000..63e5d7f450
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/docs/api.rst
@@ -0,0 +1,234 @@
+=============
+API Reference
+=============
+
+.. automodule:: more_itertools
+
+Grouping
+========
+
+These tools yield groups of items from a source iterable.
+
+----
+
+**New itertools**
+
+.. autofunction:: chunked
+.. autofunction:: sliced
+.. autofunction:: distribute
+.. autofunction:: divide
+.. autofunction:: split_at
+.. autofunction:: split_before
+.. autofunction:: split_after
+.. autofunction:: bucket
+
+----
+
+**Itertools recipes**
+
+.. autofunction:: grouper
+.. autofunction:: partition
+
+
+Lookahead and lookback
+======================
+
+These tools peek at an iterable's values without advancing it.
+
+----
+
+**New itertools**
+
+
+.. autofunction:: spy
+.. autoclass:: peekable
+.. autoclass:: seekable
+
+
+Windowing
+=========
+
+These tools yield windows of items from an iterable.
+
+----
+
+**New itertools**
+
+.. autofunction:: windowed
+.. autofunction:: stagger
+
+----
+
+**Itertools recipes**
+
+.. autofunction:: pairwise
+
+
+Augmenting
+==========
+
+These tools yield items from an iterable, plus additional data.
+
+----
+
+**New itertools**
+
+.. autofunction:: count_cycle
+.. autofunction:: intersperse
+.. autofunction:: padded
+.. autofunction:: adjacent
+.. autofunction:: groupby_transform
+
+----
+
+**Itertools recipes**
+
+.. autofunction:: padnone
+.. autofunction:: ncycles
+
+
+Combining
+=========
+
+These tools combine multiple iterables.
+
+----
+
+**New itertools**
+
+.. autofunction:: collapse
+.. autofunction:: sort_together
+.. autofunction:: interleave
+.. autofunction:: interleave_longest
+.. autofunction:: collate(*iterables, key=lambda a: a, reverse=False)
+.. autofunction:: zip_offset(*iterables, offsets, longest=False, fillvalue=None)
+
+----
+
+**Itertools recipes**
+
+.. autofunction:: dotproduct
+.. autofunction:: flatten
+.. autofunction:: roundrobin
+.. autofunction:: prepend
+
+
+Summarizing
+===========
+
+These tools return summarized or aggregated data from an iterable.
+
+----
+
+**New itertools**
+
+.. autofunction:: ilen
+.. autofunction:: first(iterable[, default])
+.. autofunction:: one
+.. autofunction:: unique_to_each
+.. autofunction:: locate(iterable, pred=bool)
+.. autofunction:: consecutive_groups(iterable, ordering=lambda x: x)
+.. autofunction:: exactly_n(iterable, n, predicate=bool)
+.. autoclass:: run_length
+.. autofunction:: map_reduce
+
+----
+
+**Itertools recipes**
+
+.. autofunction:: all_equal
+.. autofunction:: first_true
+.. autofunction:: nth
+.. autofunction:: quantify(iterable, pred=bool)
+
+
+Selecting
+=========
+
+These tools yield certain items from an iterable.
+
+----
+
+**New itertools**
+
+.. autofunction:: islice_extended(start, stop, step)
+.. autofunction:: strip
+.. autofunction:: lstrip
+.. autofunction:: rstrip
+
+----
+
+**Itertools recipes**
+
+.. autofunction:: take
+.. autofunction:: tail
+.. autofunction:: unique_everseen
+.. autofunction:: unique_justseen
+
+
+Combinatorics
+=============
+
+These tools yield combinatorial arrangements of items from iterables.
+
+----
+
+**New itertools**
+
+.. autofunction:: distinct_permutations
+.. autofunction:: circular_shifts
+
+----
+
+**Itertools recipes**
+
+.. autofunction:: powerset
+.. autofunction:: random_product
+.. autofunction:: random_permutation
+.. autofunction:: random_combination
+.. autofunction:: random_combination_with_replacement
+.. autofunction:: nth_combination
+
+
+Wrapping
+========
+
+These tools provide wrappers to smooth working with objects that produce or
+consume iterables.
+
+----
+
+**New itertools**
+
+.. autofunction:: always_iterable
+.. autofunction:: consumer
+.. autofunction:: with_iter
+
+----
+
+**Itertools recipes**
+
+.. autofunction:: iter_except
+
+
+Others
+======
+
+**New itertools**
+
+.. autofunction:: numeric_range(start, stop, step)
+.. autofunction:: always_reversible
+.. autofunction:: side_effect
+.. autofunction:: iterate
+.. autofunction:: difference(iterable, func=operator.sub)
+.. autofunction:: make_decorator
+.. autoclass:: SequenceView
+
+----
+
+**Itertools recipes**
+
+.. autofunction:: consume
+.. autofunction:: accumulate(iterable, func=operator.add)
+.. autofunction:: tabulate
+.. autofunction:: repeatfunc
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/docs/conf.py b/testing/web-platform/tests/tools/third_party/more-itertools/docs/conf.py
new file mode 100644
index 0000000000..e38c71aeaa
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/docs/conf.py
@@ -0,0 +1,244 @@
+# -*- coding: utf-8 -*-
+#
+# more-itertools documentation build configuration file, created by
+# sphinx-quickstart on Mon Jun 25 20:42:39 2012.
+#
+# 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
+
+import sphinx_rtd_theme
+
+# 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.viewcode']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'more-itertools'
+copyright = u'2012, Erik Rose'
+
+# 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.
+version = '4.2.0'
+# The full version, including alpha/beta/rc tags.
+release = version
+
+# 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 = 'sphinx_rtd_theme'
+
+# 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 = [sphinx_rtd_theme.get_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 = 'more-itertoolsdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'more-itertools.tex', u'more-itertools Documentation',
+ u'Erik Rose', '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
+
+# 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', 'more-itertools', u'more-itertools Documentation',
+ [u'Erik Rose'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'more-itertools', u'more-itertools Documentation',
+ u'Erik Rose', 'more-itertools', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/docs/index.rst b/testing/web-platform/tests/tools/third_party/more-itertools/docs/index.rst
new file mode 100644
index 0000000000..091461ff7d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/docs/index.rst
@@ -0,0 +1,16 @@
+.. include:: ../README.rst
+
+Contents
+========
+
+.. toctree::
+ :maxdepth: 2
+
+ api
+
+.. toctree::
+ :maxdepth: 1
+
+ license
+ testing
+ versions
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/docs/license.rst b/testing/web-platform/tests/tools/third_party/more-itertools/docs/license.rst
new file mode 100644
index 0000000000..123c0f54dc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/docs/license.rst
@@ -0,0 +1,16 @@
+=======
+License
+=======
+
+more-itertools is under the MIT License. See the LICENSE file.
+
+Conditions for Contributors
+===========================
+
+By contributing to this software project, you are agreeing to the following
+terms and conditions for your contributions: First, you agree your
+contributions are submitted under the MIT license. Second, you represent you
+are authorized to make the contributions and grant the license. If your
+employer has rights to intellectual property that includes your contributions,
+you represent that you have received permission to make contributions and grant
+the required license on behalf of that employer.
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/docs/make.bat b/testing/web-platform/tests/tools/third_party/more-itertools/docs/make.bat
new file mode 100644
index 0000000000..8023c0aa67
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/docs/make.bat
@@ -0,0 +1,190 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :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. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over 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
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\more-itertools.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\more-itertools.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+:end
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/docs/testing.rst b/testing/web-platform/tests/tools/third_party/more-itertools/docs/testing.rst
new file mode 100644
index 0000000000..bdd4219951
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/docs/testing.rst
@@ -0,0 +1,19 @@
+=======
+Testing
+=======
+
+To run install dependencies and run tests, use this command::
+
+ python setup.py test
+
+Multiple Python Versions
+========================
+
+To run the tests on all the versions of Python more-itertools supports, install
+tox::
+
+ pip install tox
+
+Then, run the tests::
+
+ tox
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/docs/versions.rst b/testing/web-platform/tests/tools/third_party/more-itertools/docs/versions.rst
new file mode 100644
index 0000000000..e50ac4393d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/docs/versions.rst
@@ -0,0 +1,237 @@
+===============
+Version History
+===============
+
+.. automodule:: more_itertools
+
+4.2.0
+-----
+
+* New itertools:
+ * :func:`map_reduce` (thanks to pylang)
+ * :func:`prepend` (from the `Python 3.7 docs <https://docs.python.org/3.7/library/itertools.html#itertools-recipes>`_)
+
+* Improvements to existing itertools:
+ * :func:`bucket` now complies with PEP 479 (thanks to irmen)
+
+* Other changes:
+ * Python 3.7 is now supported (thanks to irmen)
+ * Python 3.3 is no longer supported
+ * The test suite no longer requires third-party modules to run
+ * The API docs now include links to source code
+
+4.1.0
+-----
+
+* New itertools:
+ * :func:`split_at` (thanks to michael-celani)
+ * :func:`circular_shifts` (thanks to hiqua)
+ * :func:`make_decorator` - see the blog post `Yo, I heard you like decorators <https://sites.google.com/site/bbayles/index/decorator_factory>`_
+ for a tour (thanks to pylang)
+ * :func:`always_reversible` (thanks to michael-celani)
+ * :func:`nth_combination` (from the `Python 3.7 docs <https://docs.python.org/3.7/library/itertools.html#itertools-recipes>`_)
+
+* Improvements to existing itertools:
+ * :func:`seekable` now has an ``elements`` method to return cached items.
+ * The performance tradeoffs between :func:`roundrobin` and
+ :func:`interleave_longest` are now documented (thanks michael-celani,
+ pylang, and MSeifert04)
+
+4.0.1
+-----
+
+* No code changes - this release fixes how the docs display on PyPI.
+
+4.0.0
+-----
+
+* New itertools:
+ * :func:`consecutive_groups` (Based on the example in the `Python 2.4 docs <https://docs.python.org/release/2.4.4/lib/itertools-example.html>`_)
+ * :func:`seekable` (If you're looking for how to "reset" an iterator,
+ you're in luck!)
+ * :func:`exactly_n` (thanks to michael-celani)
+ * :func:`run_length.encode` and :func:`run_length.decode`
+ * :func:`difference`
+
+* Improvements to existing itertools:
+ * The number of items between filler elements in :func:`intersperse` can
+ now be specified (thanks to pylang)
+ * :func:`distinct_permutations` and :func:`peekable` got some minor
+ adjustments (thanks to MSeifert04)
+ * :func:`always_iterable` now returns an iterator object. It also now
+ allows different types to be considered iterable (thanks to jaraco)
+ * :func:`bucket` can now limit the keys it stores in memory
+ * :func:`one` now allows for custom exceptions (thanks to kalekundert)
+
+* Other changes:
+ * A few typos were fixed (thanks to EdwardBetts)
+ * All tests can now be run with ``python setup.py test``
+
+The major version update is due to the change in the return value of :func:`always_iterable`.
+It now always returns iterator objects:
+
+.. code-block:: python
+
+ >>> from more_itertools import always_iterable
+ # Non-iterable objects are wrapped with iter(tuple(obj))
+ >>> always_iterable(12345)
+ <tuple_iterator object at 0x7fb24c9488d0>
+ >>> list(always_iterable(12345))
+ [12345]
+ # Iterable objects are wrapped with iter()
+ >>> always_iterable([1, 2, 3, 4, 5])
+ <list_iterator object at 0x7fb24c948c50>
+
+3.2.0
+-----
+
+* New itertools:
+ * :func:`lstrip`, :func:`rstrip`, and :func:`strip`
+ (thanks to MSeifert04 and pylang)
+ * :func:`islice_extended`
+* Improvements to existing itertools:
+ * Some bugs with slicing :func:`peekable`-wrapped iterables were fixed
+
+3.1.0
+-----
+
+* New itertools:
+ * :func:`numeric_range` (Thanks to BebeSparkelSparkel and MSeifert04)
+ * :func:`count_cycle` (Thanks to BebeSparkelSparkel)
+ * :func:`locate` (Thanks to pylang and MSeifert04)
+* Improvements to existing itertools:
+ * A few itertools are now slightly faster due to some function
+ optimizations. (Thanks to MSeifert04)
+* The docs have been substantially revised with installation notes,
+ categories for library functions, links, and more. (Thanks to pylang)
+
+
+3.0.0
+-----
+
+* Removed itertools:
+ * ``context`` has been removed due to a design flaw - see below for
+ replacement options. (thanks to NeilGirdhar)
+* Improvements to existing itertools:
+ * ``side_effect`` now supports ``before`` and ``after`` keyword
+ arguments. (Thanks to yardsale8)
+* PyPy and PyPy3 are now supported.
+
+The major version change is due to the removal of the ``context`` function.
+Replace it with standard ``with`` statement context management:
+
+.. code-block:: python
+
+ # Don't use context() anymore
+ file_obj = StringIO()
+ consume(print(x, file=f) for f in context(file_obj) for x in u'123')
+
+ # Use a with statement instead
+ file_obj = StringIO()
+ with file_obj as f:
+ consume(print(x, file=f) for x in u'123')
+
+2.6.0
+-----
+
+* New itertools:
+ * ``adjacent`` and ``groupby_transform`` (Thanks to diazona)
+ * ``always_iterable`` (Thanks to jaraco)
+ * (Removed in 3.0.0) ``context`` (Thanks to yardsale8)
+ * ``divide`` (Thanks to mozbhearsum)
+* Improvements to existing itertools:
+ * ``ilen`` is now slightly faster. (Thanks to wbolster)
+ * ``peekable`` can now prepend items to an iterable. (Thanks to diazona)
+
+2.5.0
+-----
+
+* New itertools:
+ * ``distribute`` (Thanks to mozbhearsum and coady)
+ * ``sort_together`` (Thanks to clintval)
+ * ``stagger`` and ``zip_offset`` (Thanks to joshbode)
+ * ``padded``
+* Improvements to existing itertools:
+ * ``peekable`` now handles negative indexes and slices with negative
+ components properly.
+ * ``intersperse`` is now slightly faster. (Thanks to pylang)
+ * ``windowed`` now accepts a ``step`` keyword argument.
+ (Thanks to pylang)
+* Python 3.6 is now supported.
+
+2.4.1
+-----
+
+* Move docs 100% to readthedocs.io.
+
+2.4
+-----
+
+* New itertools:
+ * ``accumulate``, ``all_equal``, ``first_true``, ``partition``, and
+ ``tail`` from the itertools documentation.
+ * ``bucket`` (Thanks to Rosuav and cvrebert)
+ * ``collapse`` (Thanks to abarnet)
+ * ``interleave`` and ``interleave_longest`` (Thanks to abarnet)
+ * ``side_effect`` (Thanks to nvie)
+ * ``sliced`` (Thanks to j4mie and coady)
+ * ``split_before`` and ``split_after`` (Thanks to astronouth7303)
+ * ``spy`` (Thanks to themiurgo and mathieulongtin)
+* Improvements to existing itertools:
+ * ``chunked`` is now simpler and more friendly to garbage collection.
+ (Contributed by coady, with thanks to piskvorky)
+ * ``collate`` now delegates to ``heapq.merge`` when possible.
+ (Thanks to kmike and julianpistorius)
+ * ``peekable``-wrapped iterables are now indexable and sliceable.
+ Iterating through ``peekable``-wrapped iterables is also faster.
+ * ``one`` and ``unique_to_each`` have been simplified.
+ (Thanks to coady)
+
+
+2.3
+-----
+
+* Added ``one`` from ``jaraco.util.itertools``. (Thanks, jaraco!)
+* Added ``distinct_permutations`` and ``unique_to_each``. (Contributed by
+ bbayles)
+* Added ``windowed``. (Contributed by bbayles, with thanks to buchanae,
+ jaraco, and abarnert)
+* Simplified the implementation of ``chunked``. (Thanks, nvie!)
+* Python 3.5 is now supported. Python 2.6 is no longer supported.
+* Python 3 is now supported directly; there is no 2to3 step.
+
+2.2
+-----
+
+* Added ``iterate`` and ``with_iter``. (Thanks, abarnert!)
+
+2.1
+-----
+
+* Added (tested!) implementations of the recipes from the itertools
+ documentation. (Thanks, Chris Lonnen!)
+* Added ``ilen``. (Thanks for the inspiration, Matt Basta!)
+
+2.0
+-----
+
+* ``chunked`` now returns lists rather than tuples. After all, they're
+ homogeneous. This slightly backward-incompatible change is the reason for
+ the major version bump.
+* Added ``@consumer``.
+* Improved test machinery.
+
+1.1
+-----
+
+* Added ``first`` function.
+* Added Python 3 support.
+* Added a default arg to ``peekable.peek()``.
+* Noted how to easily test whether a peekable iterator is exhausted.
+* Rewrote documentation.
+
+1.0
+-----
+
+* Initial release, with ``collate``, ``peekable``, and ``chunked``. Could
+ really use better docs.
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/__init__.py b/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/__init__.py
new file mode 100644
index 0000000000..bba462c3db
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/__init__.py
@@ -0,0 +1,2 @@
+from more_itertools.more import * # noqa
+from more_itertools.recipes import * # noqa
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/more.py b/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/more.py
new file mode 100644
index 0000000000..d517250242
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/more.py
@@ -0,0 +1,2068 @@
+from __future__ import print_function
+
+from collections import Counter, defaultdict, deque
+from functools import partial, wraps
+from heapq import merge
+from itertools import (
+ chain,
+ compress,
+ count,
+ cycle,
+ dropwhile,
+ groupby,
+ islice,
+ repeat,
+ takewhile,
+ tee
+)
+from operator import itemgetter, lt, gt, sub
+from sys import maxsize, version_info
+try:
+ from collections.abc import Sequence
+except ImportError:
+ from collections import Sequence
+
+from six import binary_type, string_types, text_type
+from six.moves import filter, map, range, zip, zip_longest
+
+from .recipes import consume, flatten, take
+
+__all__ = [
+ 'adjacent',
+ 'always_iterable',
+ 'always_reversible',
+ 'bucket',
+ 'chunked',
+ 'circular_shifts',
+ 'collapse',
+ 'collate',
+ 'consecutive_groups',
+ 'consumer',
+ 'count_cycle',
+ 'difference',
+ 'distinct_permutations',
+ 'distribute',
+ 'divide',
+ 'exactly_n',
+ 'first',
+ 'groupby_transform',
+ 'ilen',
+ 'interleave_longest',
+ 'interleave',
+ 'intersperse',
+ 'islice_extended',
+ 'iterate',
+ 'locate',
+ 'lstrip',
+ 'make_decorator',
+ 'map_reduce',
+ 'numeric_range',
+ 'one',
+ 'padded',
+ 'peekable',
+ 'rstrip',
+ 'run_length',
+ 'seekable',
+ 'SequenceView',
+ 'side_effect',
+ 'sliced',
+ 'sort_together',
+ 'split_at',
+ 'split_after',
+ 'split_before',
+ 'spy',
+ 'stagger',
+ 'strip',
+ 'unique_to_each',
+ 'windowed',
+ 'with_iter',
+ 'zip_offset',
+]
+
+_marker = object()
+
+
+def chunked(iterable, n):
+ """Break *iterable* into lists of length *n*:
+
+ >>> list(chunked([1, 2, 3, 4, 5, 6], 3))
+ [[1, 2, 3], [4, 5, 6]]
+
+ If the length of *iterable* is not evenly divisible by *n*, the last
+ returned list will be shorter:
+
+ >>> list(chunked([1, 2, 3, 4, 5, 6, 7, 8], 3))
+ [[1, 2, 3], [4, 5, 6], [7, 8]]
+
+ To use a fill-in value instead, see the :func:`grouper` recipe.
+
+ :func:`chunked` is useful for splitting up a computation on a large number
+ of keys into batches, to be pickled and sent off to worker processes. One
+ example is operations on rows in MySQL, which does not implement
+ server-side cursors properly and would otherwise load the entire dataset
+ into RAM on the client.
+
+ """
+ return iter(partial(take, n, iter(iterable)), [])
+
+
+def first(iterable, default=_marker):
+ """Return the first item of *iterable*, or *default* if *iterable* is
+ empty.
+
+ >>> first([0, 1, 2, 3])
+ 0
+ >>> first([], 'some default')
+ 'some default'
+
+ If *default* is not provided and there are no items in the iterable,
+ raise ``ValueError``.
+
+ :func:`first` is useful when you have a generator of expensive-to-retrieve
+ values and want any arbitrary one. It is marginally shorter than
+ ``next(iter(iterable), default)``.
+
+ """
+ try:
+ return next(iter(iterable))
+ except StopIteration:
+ # I'm on the edge about raising ValueError instead of StopIteration. At
+ # the moment, ValueError wins, because the caller could conceivably
+ # want to do something different with flow control when I raise the
+ # exception, and it's weird to explicitly catch StopIteration.
+ if default is _marker:
+ raise ValueError('first() was called on an empty iterable, and no '
+ 'default value was provided.')
+ return default
+
+
+class peekable(object):
+ """Wrap an iterator to allow lookahead and prepending elements.
+
+ Call :meth:`peek` on the result to get the value that will be returned
+ by :func:`next`. This won't advance the iterator:
+
+ >>> p = peekable(['a', 'b'])
+ >>> p.peek()
+ 'a'
+ >>> next(p)
+ 'a'
+
+ Pass :meth:`peek` a default value to return that instead of raising
+ ``StopIteration`` when the iterator is exhausted.
+
+ >>> p = peekable([])
+ >>> p.peek('hi')
+ 'hi'
+
+ peekables also offer a :meth:`prepend` method, which "inserts" items
+ at the head of the iterable:
+
+ >>> p = peekable([1, 2, 3])
+ >>> p.prepend(10, 11, 12)
+ >>> next(p)
+ 10
+ >>> p.peek()
+ 11
+ >>> list(p)
+ [11, 12, 1, 2, 3]
+
+ peekables can be indexed. Index 0 is the item that will be returned by
+ :func:`next`, index 1 is the item after that, and so on:
+ The values up to the given index will be cached.
+
+ >>> p = peekable(['a', 'b', 'c', 'd'])
+ >>> p[0]
+ 'a'
+ >>> p[1]
+ 'b'
+ >>> next(p)
+ 'a'
+
+ Negative indexes are supported, but be aware that they will cache the
+ remaining items in the source iterator, which may require significant
+ storage.
+
+ To check whether a peekable is exhausted, check its truth value:
+
+ >>> p = peekable(['a', 'b'])
+ >>> if p: # peekable has items
+ ... list(p)
+ ['a', 'b']
+ >>> if not p: # peekable is exhaused
+ ... list(p)
+ []
+
+ """
+ def __init__(self, iterable):
+ self._it = iter(iterable)
+ self._cache = deque()
+
+ def __iter__(self):
+ return self
+
+ def __bool__(self):
+ try:
+ self.peek()
+ except StopIteration:
+ return False
+ return True
+
+ def __nonzero__(self):
+ # For Python 2 compatibility
+ return self.__bool__()
+
+ def peek(self, default=_marker):
+ """Return the item that will be next returned from ``next()``.
+
+ Return ``default`` if there are no items left. If ``default`` is not
+ provided, raise ``StopIteration``.
+
+ """
+ if not self._cache:
+ try:
+ self._cache.append(next(self._it))
+ except StopIteration:
+ if default is _marker:
+ raise
+ return default
+ return self._cache[0]
+
+ def prepend(self, *items):
+ """Stack up items to be the next ones returned from ``next()`` or
+ ``self.peek()``. The items will be returned in
+ first in, first out order::
+
+ >>> p = peekable([1, 2, 3])
+ >>> p.prepend(10, 11, 12)
+ >>> next(p)
+ 10
+ >>> list(p)
+ [11, 12, 1, 2, 3]
+
+ It is possible, by prepending items, to "resurrect" a peekable that
+ previously raised ``StopIteration``.
+
+ >>> p = peekable([])
+ >>> next(p)
+ Traceback (most recent call last):
+ ...
+ StopIteration
+ >>> p.prepend(1)
+ >>> next(p)
+ 1
+ >>> next(p)
+ Traceback (most recent call last):
+ ...
+ StopIteration
+
+ """
+ self._cache.extendleft(reversed(items))
+
+ def __next__(self):
+ if self._cache:
+ return self._cache.popleft()
+
+ return next(self._it)
+
+ next = __next__ # For Python 2 compatibility
+
+ def _get_slice(self, index):
+ # Normalize the slice's arguments
+ step = 1 if (index.step is None) else index.step
+ if step > 0:
+ start = 0 if (index.start is None) else index.start
+ stop = maxsize if (index.stop is None) else index.stop
+ elif step < 0:
+ start = -1 if (index.start is None) else index.start
+ stop = (-maxsize - 1) if (index.stop is None) else index.stop
+ else:
+ raise ValueError('slice step cannot be zero')
+
+ # If either the start or stop index is negative, we'll need to cache
+ # the rest of the iterable in order to slice from the right side.
+ if (start < 0) or (stop < 0):
+ self._cache.extend(self._it)
+ # Otherwise we'll need to find the rightmost index and cache to that
+ # point.
+ else:
+ n = min(max(start, stop) + 1, maxsize)
+ cache_len = len(self._cache)
+ if n >= cache_len:
+ self._cache.extend(islice(self._it, n - cache_len))
+
+ return list(self._cache)[index]
+
+ def __getitem__(self, index):
+ if isinstance(index, slice):
+ return self._get_slice(index)
+
+ cache_len = len(self._cache)
+ if index < 0:
+ self._cache.extend(self._it)
+ elif index >= cache_len:
+ self._cache.extend(islice(self._it, index + 1 - cache_len))
+
+ return self._cache[index]
+
+
+def _collate(*iterables, **kwargs):
+ """Helper for ``collate()``, called when the user is using the ``reverse``
+ or ``key`` keyword arguments on Python versions below 3.5.
+
+ """
+ key = kwargs.pop('key', lambda a: a)
+ reverse = kwargs.pop('reverse', False)
+
+ min_or_max = partial(max if reverse else min, key=itemgetter(0))
+ peekables = [peekable(it) for it in iterables]
+ peekables = [p for p in peekables if p] # Kill empties.
+ while peekables:
+ _, p = min_or_max((key(p.peek()), p) for p in peekables)
+ yield next(p)
+ peekables = [x for x in peekables if x]
+
+
+def collate(*iterables, **kwargs):
+ """Return a sorted merge of the items from each of several already-sorted
+ *iterables*.
+
+ >>> list(collate('ACDZ', 'AZ', 'JKL'))
+ ['A', 'A', 'C', 'D', 'J', 'K', 'L', 'Z', 'Z']
+
+ Works lazily, keeping only the next value from each iterable in memory. Use
+ :func:`collate` to, for example, perform a n-way mergesort of items that
+ don't fit in memory.
+
+ If a *key* function is specified, the iterables will be sorted according
+ to its result:
+
+ >>> key = lambda s: int(s) # Sort by numeric value, not by string
+ >>> list(collate(['1', '10'], ['2', '11'], key=key))
+ ['1', '2', '10', '11']
+
+
+ If the *iterables* are sorted in descending order, set *reverse* to
+ ``True``:
+
+ >>> list(collate([5, 3, 1], [4, 2, 0], reverse=True))
+ [5, 4, 3, 2, 1, 0]
+
+ If the elements of the passed-in iterables are out of order, you might get
+ unexpected results.
+
+ On Python 2.7, this function delegates to :func:`heapq.merge` if neither
+ of the keyword arguments are specified. On Python 3.5+, this function
+ is an alias for :func:`heapq.merge`.
+
+ """
+ if not kwargs:
+ return merge(*iterables)
+
+ return _collate(*iterables, **kwargs)
+
+
+# If using Python version 3.5 or greater, heapq.merge() will be faster than
+# collate - use that instead.
+if version_info >= (3, 5, 0):
+ _collate_docstring = collate.__doc__
+ collate = partial(merge)
+ collate.__doc__ = _collate_docstring
+
+
+def consumer(func):
+ """Decorator that automatically advances a PEP-342-style "reverse iterator"
+ to its first yield point so you don't have to call ``next()`` on it
+ manually.
+
+ >>> @consumer
+ ... def tally():
+ ... i = 0
+ ... while True:
+ ... print('Thing number %s is %s.' % (i, (yield)))
+ ... i += 1
+ ...
+ >>> t = tally()
+ >>> t.send('red')
+ Thing number 0 is red.
+ >>> t.send('fish')
+ Thing number 1 is fish.
+
+ Without the decorator, you would have to call ``next(t)`` before
+ ``t.send()`` could be used.
+
+ """
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ gen = func(*args, **kwargs)
+ next(gen)
+ return gen
+ return wrapper
+
+
+def ilen(iterable):
+ """Return the number of items in *iterable*.
+
+ >>> ilen(x for x in range(1000000) if x % 3 == 0)
+ 333334
+
+ This consumes the iterable, so handle with care.
+
+ """
+ # maxlen=1 only stores the last item in the deque
+ d = deque(enumerate(iterable, 1), maxlen=1)
+ # since we started enumerate at 1,
+ # the first item of the last pair will be the length of the iterable
+ # (assuming there were items)
+ return d[0][0] if d else 0
+
+
+def iterate(func, start):
+ """Return ``start``, ``func(start)``, ``func(func(start))``, ...
+
+ >>> from itertools import islice
+ >>> list(islice(iterate(lambda x: 2*x, 1), 10))
+ [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
+
+ """
+ while True:
+ yield start
+ start = func(start)
+
+
+def with_iter(context_manager):
+ """Wrap an iterable in a ``with`` statement, so it closes once exhausted.
+
+ For example, this will close the file when the iterator is exhausted::
+
+ upper_lines = (line.upper() for line in with_iter(open('foo')))
+
+ Any context manager which returns an iterable is a candidate for
+ ``with_iter``.
+
+ """
+ with context_manager as iterable:
+ for item in iterable:
+ yield item
+
+
+def one(iterable, too_short=None, too_long=None):
+ """Return the first item from *iterable*, which is expected to contain only
+ that item. Raise an exception if *iterable* is empty or has more than one
+ item.
+
+ :func:`one` is useful for ensuring that an iterable contains only one item.
+ For example, it can be used to retrieve the result of a database query
+ that is expected to return a single row.
+
+ If *iterable* is empty, ``ValueError`` will be raised. You may specify a
+ different exception with the *too_short* keyword:
+
+ >>> it = []
+ >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ ValueError: too many items in iterable (expected 1)'
+ >>> too_short = IndexError('too few items')
+ >>> one(it, too_short=too_short) # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ IndexError: too few items
+
+ Similarly, if *iterable* contains more than one item, ``ValueError`` will
+ be raised. You may specify a different exception with the *too_long*
+ keyword:
+
+ >>> it = ['too', 'many']
+ >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ ValueError: too many items in iterable (expected 1)'
+ >>> too_long = RuntimeError
+ >>> one(it, too_long=too_long) # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ RuntimeError
+
+ Note that :func:`one` attempts to advance *iterable* twice to ensure there
+ is only one item. If there is more than one, both items will be discarded.
+ See :func:`spy` or :func:`peekable` to check iterable contents less
+ destructively.
+
+ """
+ it = iter(iterable)
+
+ try:
+ value = next(it)
+ except StopIteration:
+ raise too_short or ValueError('too few items in iterable (expected 1)')
+
+ try:
+ next(it)
+ except StopIteration:
+ pass
+ else:
+ raise too_long or ValueError('too many items in iterable (expected 1)')
+
+ return value
+
+
+def distinct_permutations(iterable):
+ """Yield successive distinct permutations of the elements in *iterable*.
+
+ >>> sorted(distinct_permutations([1, 0, 1]))
+ [(0, 1, 1), (1, 0, 1), (1, 1, 0)]
+
+ Equivalent to ``set(permutations(iterable))``, except duplicates are not
+ generated and thrown away. For larger input sequences this is much more
+ efficient.
+
+ Duplicate permutations arise when there are duplicated elements in the
+ input iterable. The number of items returned is
+ `n! / (x_1! * x_2! * ... * x_n!)`, where `n` is the total number of
+ items input, and each `x_i` is the count of a distinct item in the input
+ sequence.
+
+ """
+ def perm_unique_helper(item_counts, perm, i):
+ """Internal helper function
+
+ :arg item_counts: Stores the unique items in ``iterable`` and how many
+ times they are repeated
+ :arg perm: The permutation that is being built for output
+ :arg i: The index of the permutation being modified
+
+ The output permutations are built up recursively; the distinct items
+ are placed until their repetitions are exhausted.
+ """
+ if i < 0:
+ yield tuple(perm)
+ else:
+ for item in item_counts:
+ if item_counts[item] <= 0:
+ continue
+ perm[i] = item
+ item_counts[item] -= 1
+ for x in perm_unique_helper(item_counts, perm, i - 1):
+ yield x
+ item_counts[item] += 1
+
+ item_counts = Counter(iterable)
+ length = sum(item_counts.values())
+
+ return perm_unique_helper(item_counts, [None] * length, length - 1)
+
+
+def intersperse(e, iterable, n=1):
+ """Intersperse filler element *e* among the items in *iterable*, leaving
+ *n* items between each filler element.
+
+ >>> list(intersperse('!', [1, 2, 3, 4, 5]))
+ [1, '!', 2, '!', 3, '!', 4, '!', 5]
+
+ >>> list(intersperse(None, [1, 2, 3, 4, 5], n=2))
+ [1, 2, None, 3, 4, None, 5]
+
+ """
+ if n == 0:
+ raise ValueError('n must be > 0')
+ elif n == 1:
+ # interleave(repeat(e), iterable) -> e, x_0, e, e, x_1, e, x_2...
+ # islice(..., 1, None) -> x_0, e, e, x_1, e, x_2...
+ return islice(interleave(repeat(e), iterable), 1, None)
+ else:
+ # interleave(filler, chunks) -> [e], [x_0, x_1], [e], [x_2, x_3]...
+ # islice(..., 1, None) -> [x_0, x_1], [e], [x_2, x_3]...
+ # flatten(...) -> x_0, x_1, e, x_2, x_3...
+ filler = repeat([e])
+ chunks = chunked(iterable, n)
+ return flatten(islice(interleave(filler, chunks), 1, None))
+
+
+def unique_to_each(*iterables):
+ """Return the elements from each of the input iterables that aren't in the
+ other input iterables.
+
+ For example, suppose you have a set of packages, each with a set of
+ dependencies::
+
+ {'pkg_1': {'A', 'B'}, 'pkg_2': {'B', 'C'}, 'pkg_3': {'B', 'D'}}
+
+ If you remove one package, which dependencies can also be removed?
+
+ If ``pkg_1`` is removed, then ``A`` is no longer necessary - it is not
+ associated with ``pkg_2`` or ``pkg_3``. Similarly, ``C`` is only needed for
+ ``pkg_2``, and ``D`` is only needed for ``pkg_3``::
+
+ >>> unique_to_each({'A', 'B'}, {'B', 'C'}, {'B', 'D'})
+ [['A'], ['C'], ['D']]
+
+ If there are duplicates in one input iterable that aren't in the others
+ they will be duplicated in the output. Input order is preserved::
+
+ >>> unique_to_each("mississippi", "missouri")
+ [['p', 'p'], ['o', 'u', 'r']]
+
+ It is assumed that the elements of each iterable are hashable.
+
+ """
+ pool = [list(it) for it in iterables]
+ counts = Counter(chain.from_iterable(map(set, pool)))
+ uniques = {element for element in counts if counts[element] == 1}
+ return [list(filter(uniques.__contains__, it)) for it in pool]
+
+
+def windowed(seq, n, fillvalue=None, step=1):
+ """Return a sliding window of width *n* over the given iterable.
+
+ >>> all_windows = windowed([1, 2, 3, 4, 5], 3)
+ >>> list(all_windows)
+ [(1, 2, 3), (2, 3, 4), (3, 4, 5)]
+
+ When the window is larger than the iterable, *fillvalue* is used in place
+ of missing values::
+
+ >>> list(windowed([1, 2, 3], 4))
+ [(1, 2, 3, None)]
+
+ Each window will advance in increments of *step*:
+
+ >>> list(windowed([1, 2, 3, 4, 5, 6], 3, fillvalue='!', step=2))
+ [(1, 2, 3), (3, 4, 5), (5, 6, '!')]
+
+ """
+ if n < 0:
+ raise ValueError('n must be >= 0')
+ if n == 0:
+ yield tuple()
+ return
+ if step < 1:
+ raise ValueError('step must be >= 1')
+
+ it = iter(seq)
+ window = deque([], n)
+ append = window.append
+
+ # Initial deque fill
+ for _ in range(n):
+ append(next(it, fillvalue))
+ yield tuple(window)
+
+ # Appending new items to the right causes old items to fall off the left
+ i = 0
+ for item in it:
+ append(item)
+ i = (i + 1) % step
+ if i % step == 0:
+ yield tuple(window)
+
+ # If there are items from the iterable in the window, pad with the given
+ # value and emit them.
+ if (i % step) and (step - i < n):
+ for _ in range(step - i):
+ append(fillvalue)
+ yield tuple(window)
+
+
+class bucket(object):
+ """Wrap *iterable* and return an object that buckets it iterable into
+ child iterables based on a *key* function.
+
+ >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3']
+ >>> s = bucket(iterable, key=lambda x: x[0])
+ >>> a_iterable = s['a']
+ >>> next(a_iterable)
+ 'a1'
+ >>> next(a_iterable)
+ 'a2'
+ >>> list(s['b'])
+ ['b1', 'b2', 'b3']
+
+ The original iterable will be advanced and its items will be cached until
+ they are used by the child iterables. This may require significant storage.
+
+ By default, attempting to select a bucket to which no items belong will
+ exhaust the iterable and cache all values.
+ If you specify a *validator* function, selected buckets will instead be
+ checked against it.
+
+ >>> from itertools import count
+ >>> it = count(1, 2) # Infinite sequence of odd numbers
+ >>> key = lambda x: x % 10 # Bucket by last digit
+ >>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only
+ >>> s = bucket(it, key=key, validator=validator)
+ >>> 2 in s
+ False
+ >>> list(s[2])
+ []
+
+ """
+ def __init__(self, iterable, key, validator=None):
+ self._it = iter(iterable)
+ self._key = key
+ self._cache = defaultdict(deque)
+ self._validator = validator or (lambda x: True)
+
+ def __contains__(self, value):
+ if not self._validator(value):
+ return False
+
+ try:
+ item = next(self[value])
+ except StopIteration:
+ return False
+ else:
+ self._cache[value].appendleft(item)
+
+ return True
+
+ def _get_values(self, value):
+ """
+ Helper to yield items from the parent iterator that match *value*.
+ Items that don't match are stored in the local cache as they
+ are encountered.
+ """
+ while True:
+ # If we've cached some items that match the target value, emit
+ # the first one and evict it from the cache.
+ if self._cache[value]:
+ yield self._cache[value].popleft()
+ # Otherwise we need to advance the parent iterator to search for
+ # a matching item, caching the rest.
+ else:
+ while True:
+ try:
+ item = next(self._it)
+ except StopIteration:
+ return
+ item_value = self._key(item)
+ if item_value == value:
+ yield item
+ break
+ elif self._validator(item_value):
+ self._cache[item_value].append(item)
+
+ def __getitem__(self, value):
+ if not self._validator(value):
+ return iter(())
+
+ return self._get_values(value)
+
+
+def spy(iterable, n=1):
+ """Return a 2-tuple with a list containing the first *n* elements of
+ *iterable*, and an iterator with the same items as *iterable*.
+ This allows you to "look ahead" at the items in the iterable without
+ advancing it.
+
+ There is one item in the list by default:
+
+ >>> iterable = 'abcdefg'
+ >>> head, iterable = spy(iterable)
+ >>> head
+ ['a']
+ >>> list(iterable)
+ ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+
+ You may use unpacking to retrieve items instead of lists:
+
+ >>> (head,), iterable = spy('abcdefg')
+ >>> head
+ 'a'
+ >>> (first, second), iterable = spy('abcdefg', 2)
+ >>> first
+ 'a'
+ >>> second
+ 'b'
+
+ The number of items requested can be larger than the number of items in
+ the iterable:
+
+ >>> iterable = [1, 2, 3, 4, 5]
+ >>> head, iterable = spy(iterable, 10)
+ >>> head
+ [1, 2, 3, 4, 5]
+ >>> list(iterable)
+ [1, 2, 3, 4, 5]
+
+ """
+ it = iter(iterable)
+ head = take(n, it)
+
+ return head, chain(head, it)
+
+
+def interleave(*iterables):
+ """Return a new iterable yielding from each iterable in turn,
+ until the shortest is exhausted.
+
+ >>> list(interleave([1, 2, 3], [4, 5], [6, 7, 8]))
+ [1, 4, 6, 2, 5, 7]
+
+ For a version that doesn't terminate after the shortest iterable is
+ exhausted, see :func:`interleave_longest`.
+
+ """
+ return chain.from_iterable(zip(*iterables))
+
+
+def interleave_longest(*iterables):
+ """Return a new iterable yielding from each iterable in turn,
+ skipping any that are exhausted.
+
+ >>> list(interleave_longest([1, 2, 3], [4, 5], [6, 7, 8]))
+ [1, 4, 6, 2, 5, 7, 3, 8]
+
+ This function produces the same output as :func:`roundrobin`, but may
+ perform better for some inputs (in particular when the number of iterables
+ is large).
+
+ """
+ i = chain.from_iterable(zip_longest(*iterables, fillvalue=_marker))
+ return (x for x in i if x is not _marker)
+
+
+def collapse(iterable, base_type=None, levels=None):
+ """Flatten an iterable with multiple levels of nesting (e.g., a list of
+ lists of tuples) into non-iterable types.
+
+ >>> iterable = [(1, 2), ([3, 4], [[5], [6]])]
+ >>> list(collapse(iterable))
+ [1, 2, 3, 4, 5, 6]
+
+ String types are not considered iterable and will not be collapsed.
+ To avoid collapsing other types, specify *base_type*:
+
+ >>> iterable = ['ab', ('cd', 'ef'), ['gh', 'ij']]
+ >>> list(collapse(iterable, base_type=tuple))
+ ['ab', ('cd', 'ef'), 'gh', 'ij']
+
+ Specify *levels* to stop flattening after a certain level:
+
+ >>> iterable = [('a', ['b']), ('c', ['d'])]
+ >>> list(collapse(iterable)) # Fully flattened
+ ['a', 'b', 'c', 'd']
+ >>> list(collapse(iterable, levels=1)) # Only one level flattened
+ ['a', ['b'], 'c', ['d']]
+
+ """
+ def walk(node, level):
+ if (
+ ((levels is not None) and (level > levels)) or
+ isinstance(node, string_types) or
+ ((base_type is not None) and isinstance(node, base_type))
+ ):
+ yield node
+ return
+
+ try:
+ tree = iter(node)
+ except TypeError:
+ yield node
+ return
+ else:
+ for child in tree:
+ for x in walk(child, level + 1):
+ yield x
+
+ for x in walk(iterable, 0):
+ yield x
+
+
+def side_effect(func, iterable, chunk_size=None, before=None, after=None):
+ """Invoke *func* on each item in *iterable* (or on each *chunk_size* group
+ of items) before yielding the item.
+
+ `func` must be a function that takes a single argument. Its return value
+ will be discarded.
+
+ *before* and *after* are optional functions that take no arguments. They
+ will be executed before iteration starts and after it ends, respectively.
+
+ `side_effect` can be used for logging, updating progress bars, or anything
+ that is not functionally "pure."
+
+ Emitting a status message:
+
+ >>> from more_itertools import consume
+ >>> func = lambda item: print('Received {}'.format(item))
+ >>> consume(side_effect(func, range(2)))
+ Received 0
+ Received 1
+
+ Operating on chunks of items:
+
+ >>> pair_sums = []
+ >>> func = lambda chunk: pair_sums.append(sum(chunk))
+ >>> list(side_effect(func, [0, 1, 2, 3, 4, 5], 2))
+ [0, 1, 2, 3, 4, 5]
+ >>> list(pair_sums)
+ [1, 5, 9]
+
+ Writing to a file-like object:
+
+ >>> from io import StringIO
+ >>> from more_itertools import consume
+ >>> f = StringIO()
+ >>> func = lambda x: print(x, file=f)
+ >>> before = lambda: print(u'HEADER', file=f)
+ >>> after = f.close
+ >>> it = [u'a', u'b', u'c']
+ >>> consume(side_effect(func, it, before=before, after=after))
+ >>> f.closed
+ True
+
+ """
+ try:
+ if before is not None:
+ before()
+
+ if chunk_size is None:
+ for item in iterable:
+ func(item)
+ yield item
+ else:
+ for chunk in chunked(iterable, chunk_size):
+ func(chunk)
+ for item in chunk:
+ yield item
+ finally:
+ if after is not None:
+ after()
+
+
+def sliced(seq, n):
+ """Yield slices of length *n* from the sequence *seq*.
+
+ >>> list(sliced((1, 2, 3, 4, 5, 6), 3))
+ [(1, 2, 3), (4, 5, 6)]
+
+ If the length of the sequence is not divisible by the requested slice
+ length, the last slice will be shorter.
+
+ >>> list(sliced((1, 2, 3, 4, 5, 6, 7, 8), 3))
+ [(1, 2, 3), (4, 5, 6), (7, 8)]
+
+ This function will only work for iterables that support slicing.
+ For non-sliceable iterables, see :func:`chunked`.
+
+ """
+ return takewhile(bool, (seq[i: i + n] for i in count(0, n)))
+
+
+def split_at(iterable, pred):
+ """Yield lists of items from *iterable*, where each list is delimited by
+ an item where callable *pred* returns ``True``. The lists do not include
+ the delimiting items.
+
+ >>> list(split_at('abcdcba', lambda x: x == 'b'))
+ [['a'], ['c', 'd', 'c'], ['a']]
+
+ >>> list(split_at(range(10), lambda n: n % 2 == 1))
+ [[0], [2], [4], [6], [8], []]
+ """
+ buf = []
+ for item in iterable:
+ if pred(item):
+ yield buf
+ buf = []
+ else:
+ buf.append(item)
+ yield buf
+
+
+def split_before(iterable, pred):
+ """Yield lists of items from *iterable*, where each list starts with an
+ item where callable *pred* returns ``True``:
+
+ >>> list(split_before('OneTwo', lambda s: s.isupper()))
+ [['O', 'n', 'e'], ['T', 'w', 'o']]
+
+ >>> list(split_before(range(10), lambda n: n % 3 == 0))
+ [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
+
+ """
+ buf = []
+ for item in iterable:
+ if pred(item) and buf:
+ yield buf
+ buf = []
+ buf.append(item)
+ yield buf
+
+
+def split_after(iterable, pred):
+ """Yield lists of items from *iterable*, where each list ends with an
+ item where callable *pred* returns ``True``:
+
+ >>> list(split_after('one1two2', lambda s: s.isdigit()))
+ [['o', 'n', 'e', '1'], ['t', 'w', 'o', '2']]
+
+ >>> list(split_after(range(10), lambda n: n % 3 == 0))
+ [[0], [1, 2, 3], [4, 5, 6], [7, 8, 9]]
+
+ """
+ buf = []
+ for item in iterable:
+ buf.append(item)
+ if pred(item) and buf:
+ yield buf
+ buf = []
+ if buf:
+ yield buf
+
+
+def padded(iterable, fillvalue=None, n=None, next_multiple=False):
+ """Yield the elements from *iterable*, followed by *fillvalue*, such that
+ at least *n* items are emitted.
+
+ >>> list(padded([1, 2, 3], '?', 5))
+ [1, 2, 3, '?', '?']
+
+ If *next_multiple* is ``True``, *fillvalue* will be emitted until the
+ number of items emitted is a multiple of *n*::
+
+ >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True))
+ [1, 2, 3, 4, None, None]
+
+ If *n* is ``None``, *fillvalue* will be emitted indefinitely.
+
+ """
+ it = iter(iterable)
+ if n is None:
+ for item in chain(it, repeat(fillvalue)):
+ yield item
+ elif n < 1:
+ raise ValueError('n must be at least 1')
+ else:
+ item_count = 0
+ for item in it:
+ yield item
+ item_count += 1
+
+ remaining = (n - item_count) % n if next_multiple else n - item_count
+ for _ in range(remaining):
+ yield fillvalue
+
+
+def distribute(n, iterable):
+ """Distribute the items from *iterable* among *n* smaller iterables.
+
+ >>> group_1, group_2 = distribute(2, [1, 2, 3, 4, 5, 6])
+ >>> list(group_1)
+ [1, 3, 5]
+ >>> list(group_2)
+ [2, 4, 6]
+
+ If the length of *iterable* is not evenly divisible by *n*, then the
+ length of the returned iterables will not be identical:
+
+ >>> children = distribute(3, [1, 2, 3, 4, 5, 6, 7])
+ >>> [list(c) for c in children]
+ [[1, 4, 7], [2, 5], [3, 6]]
+
+ If the length of *iterable* is smaller than *n*, then the last returned
+ iterables will be empty:
+
+ >>> children = distribute(5, [1, 2, 3])
+ >>> [list(c) for c in children]
+ [[1], [2], [3], [], []]
+
+ This function uses :func:`itertools.tee` and may require significant
+ storage. If you need the order items in the smaller iterables to match the
+ original iterable, see :func:`divide`.
+
+ """
+ if n < 1:
+ raise ValueError('n must be at least 1')
+
+ children = tee(iterable, n)
+ return [islice(it, index, None, n) for index, it in enumerate(children)]
+
+
+def stagger(iterable, offsets=(-1, 0, 1), longest=False, fillvalue=None):
+ """Yield tuples whose elements are offset from *iterable*.
+ The amount by which the `i`-th item in each tuple is offset is given by
+ the `i`-th item in *offsets*.
+
+ >>> list(stagger([0, 1, 2, 3]))
+ [(None, 0, 1), (0, 1, 2), (1, 2, 3)]
+ >>> list(stagger(range(8), offsets=(0, 2, 4)))
+ [(0, 2, 4), (1, 3, 5), (2, 4, 6), (3, 5, 7)]
+
+ By default, the sequence will end when the final element of a tuple is the
+ last item in the iterable. To continue until the first element of a tuple
+ is the last item in the iterable, set *longest* to ``True``::
+
+ >>> list(stagger([0, 1, 2, 3], longest=True))
+ [(None, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, None), (3, None, None)]
+
+ By default, ``None`` will be used to replace offsets beyond the end of the
+ sequence. Specify *fillvalue* to use some other value.
+
+ """
+ children = tee(iterable, len(offsets))
+
+ return zip_offset(
+ *children, offsets=offsets, longest=longest, fillvalue=fillvalue
+ )
+
+
+def zip_offset(*iterables, **kwargs):
+ """``zip`` the input *iterables* together, but offset the `i`-th iterable
+ by the `i`-th item in *offsets*.
+
+ >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1)))
+ [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e')]
+
+ This can be used as a lightweight alternative to SciPy or pandas to analyze
+ data sets in which somes series have a lead or lag relationship.
+
+ By default, the sequence will end when the shortest iterable is exhausted.
+ To continue until the longest iterable is exhausted, set *longest* to
+ ``True``.
+
+ >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1), longest=True))
+ [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e'), (None, 'f')]
+
+ By default, ``None`` will be used to replace offsets beyond the end of the
+ sequence. Specify *fillvalue* to use some other value.
+
+ """
+ offsets = kwargs['offsets']
+ longest = kwargs.get('longest', False)
+ fillvalue = kwargs.get('fillvalue', None)
+
+ if len(iterables) != len(offsets):
+ raise ValueError("Number of iterables and offsets didn't match")
+
+ staggered = []
+ for it, n in zip(iterables, offsets):
+ if n < 0:
+ staggered.append(chain(repeat(fillvalue, -n), it))
+ elif n > 0:
+ staggered.append(islice(it, n, None))
+ else:
+ staggered.append(it)
+
+ if longest:
+ return zip_longest(*staggered, fillvalue=fillvalue)
+
+ return zip(*staggered)
+
+
+def sort_together(iterables, key_list=(0,), reverse=False):
+ """Return the input iterables sorted together, with *key_list* as the
+ priority for sorting. All iterables are trimmed to the length of the
+ shortest one.
+
+ This can be used like the sorting function in a spreadsheet. If each
+ iterable represents a column of data, the key list determines which
+ columns are used for sorting.
+
+ By default, all iterables are sorted using the ``0``-th iterable::
+
+ >>> iterables = [(4, 3, 2, 1), ('a', 'b', 'c', 'd')]
+ >>> sort_together(iterables)
+ [(1, 2, 3, 4), ('d', 'c', 'b', 'a')]
+
+ Set a different key list to sort according to another iterable.
+ Specifying mutliple keys dictates how ties are broken::
+
+ >>> iterables = [(3, 1, 2), (0, 1, 0), ('c', 'b', 'a')]
+ >>> sort_together(iterables, key_list=(1, 2))
+ [(2, 3, 1), (0, 0, 1), ('a', 'c', 'b')]
+
+ Set *reverse* to ``True`` to sort in descending order.
+
+ >>> sort_together([(1, 2, 3), ('c', 'b', 'a')], reverse=True)
+ [(3, 2, 1), ('a', 'b', 'c')]
+
+ """
+ return list(zip(*sorted(zip(*iterables),
+ key=itemgetter(*key_list),
+ reverse=reverse)))
+
+
+def divide(n, iterable):
+ """Divide the elements from *iterable* into *n* parts, maintaining
+ order.
+
+ >>> group_1, group_2 = divide(2, [1, 2, 3, 4, 5, 6])
+ >>> list(group_1)
+ [1, 2, 3]
+ >>> list(group_2)
+ [4, 5, 6]
+
+ If the length of *iterable* is not evenly divisible by *n*, then the
+ length of the returned iterables will not be identical:
+
+ >>> children = divide(3, [1, 2, 3, 4, 5, 6, 7])
+ >>> [list(c) for c in children]
+ [[1, 2, 3], [4, 5], [6, 7]]
+
+ If the length of the iterable is smaller than n, then the last returned
+ iterables will be empty:
+
+ >>> children = divide(5, [1, 2, 3])
+ >>> [list(c) for c in children]
+ [[1], [2], [3], [], []]
+
+ This function will exhaust the iterable before returning and may require
+ significant storage. If order is not important, see :func:`distribute`,
+ which does not first pull the iterable into memory.
+
+ """
+ if n < 1:
+ raise ValueError('n must be at least 1')
+
+ seq = tuple(iterable)
+ q, r = divmod(len(seq), n)
+
+ ret = []
+ for i in range(n):
+ start = (i * q) + (i if i < r else r)
+ stop = ((i + 1) * q) + (i + 1 if i + 1 < r else r)
+ ret.append(iter(seq[start:stop]))
+
+ return ret
+
+
+def always_iterable(obj, base_type=(text_type, binary_type)):
+ """If *obj* is iterable, return an iterator over its items::
+
+ >>> obj = (1, 2, 3)
+ >>> list(always_iterable(obj))
+ [1, 2, 3]
+
+ If *obj* is not iterable, return a one-item iterable containing *obj*::
+
+ >>> obj = 1
+ >>> list(always_iterable(obj))
+ [1]
+
+ If *obj* is ``None``, return an empty iterable:
+
+ >>> obj = None
+ >>> list(always_iterable(None))
+ []
+
+ By default, binary and text strings are not considered iterable::
+
+ >>> obj = 'foo'
+ >>> list(always_iterable(obj))
+ ['foo']
+
+ If *base_type* is set, objects for which ``isinstance(obj, base_type)``
+ returns ``True`` won't be considered iterable.
+
+ >>> obj = {'a': 1}
+ >>> list(always_iterable(obj)) # Iterate over the dict's keys
+ ['a']
+ >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit
+ [{'a': 1}]
+
+ Set *base_type* to ``None`` to avoid any special handling and treat objects
+ Python considers iterable as iterable:
+
+ >>> obj = 'foo'
+ >>> list(always_iterable(obj, base_type=None))
+ ['f', 'o', 'o']
+ """
+ if obj is None:
+ return iter(())
+
+ if (base_type is not None) and isinstance(obj, base_type):
+ return iter((obj,))
+
+ try:
+ return iter(obj)
+ except TypeError:
+ return iter((obj,))
+
+
+def adjacent(predicate, iterable, distance=1):
+ """Return an iterable over `(bool, item)` tuples where the `item` is
+ drawn from *iterable* and the `bool` indicates whether
+ that item satisfies the *predicate* or is adjacent to an item that does.
+
+ For example, to find whether items are adjacent to a ``3``::
+
+ >>> list(adjacent(lambda x: x == 3, range(6)))
+ [(False, 0), (False, 1), (True, 2), (True, 3), (True, 4), (False, 5)]
+
+ Set *distance* to change what counts as adjacent. For example, to find
+ whether items are two places away from a ``3``:
+
+ >>> list(adjacent(lambda x: x == 3, range(6), distance=2))
+ [(False, 0), (True, 1), (True, 2), (True, 3), (True, 4), (True, 5)]
+
+ This is useful for contextualizing the results of a search function.
+ For example, a code comparison tool might want to identify lines that
+ have changed, but also surrounding lines to give the viewer of the diff
+ context.
+
+ The predicate function will only be called once for each item in the
+ iterable.
+
+ See also :func:`groupby_transform`, which can be used with this function
+ to group ranges of items with the same `bool` value.
+
+ """
+ # Allow distance=0 mainly for testing that it reproduces results with map()
+ if distance < 0:
+ raise ValueError('distance must be at least 0')
+
+ i1, i2 = tee(iterable)
+ padding = [False] * distance
+ selected = chain(padding, map(predicate, i1), padding)
+ adjacent_to_selected = map(any, windowed(selected, 2 * distance + 1))
+ return zip(adjacent_to_selected, i2)
+
+
+def groupby_transform(iterable, keyfunc=None, valuefunc=None):
+ """An extension of :func:`itertools.groupby` that transforms the values of
+ *iterable* after grouping them.
+ *keyfunc* is a function used to compute a grouping key for each item.
+ *valuefunc* is a function for transforming the items after grouping.
+
+ >>> iterable = 'AaaABbBCcA'
+ >>> keyfunc = lambda x: x.upper()
+ >>> valuefunc = lambda x: x.lower()
+ >>> grouper = groupby_transform(iterable, keyfunc, valuefunc)
+ >>> [(k, ''.join(g)) for k, g in grouper]
+ [('A', 'aaaa'), ('B', 'bbb'), ('C', 'cc'), ('A', 'a')]
+
+ *keyfunc* and *valuefunc* default to identity functions if they are not
+ specified.
+
+ :func:`groupby_transform` is useful when grouping elements of an iterable
+ using a separate iterable as the key. To do this, :func:`zip` the iterables
+ and pass a *keyfunc* that extracts the first element and a *valuefunc*
+ that extracts the second element::
+
+ >>> from operator import itemgetter
+ >>> keys = [0, 0, 1, 1, 1, 2, 2, 2, 3]
+ >>> values = 'abcdefghi'
+ >>> iterable = zip(keys, values)
+ >>> grouper = groupby_transform(iterable, itemgetter(0), itemgetter(1))
+ >>> [(k, ''.join(g)) for k, g in grouper]
+ [(0, 'ab'), (1, 'cde'), (2, 'fgh'), (3, 'i')]
+
+ Note that the order of items in the iterable is significant.
+ Only adjacent items are grouped together, so if you don't want any
+ duplicate groups, you should sort the iterable by the key function.
+
+ """
+ valuefunc = (lambda x: x) if valuefunc is None else valuefunc
+ return ((k, map(valuefunc, g)) for k, g in groupby(iterable, keyfunc))
+
+
+def numeric_range(*args):
+ """An extension of the built-in ``range()`` function whose arguments can
+ be any orderable numeric type.
+
+ With only *stop* specified, *start* defaults to ``0`` and *step*
+ defaults to ``1``. The output items will match the type of *stop*:
+
+ >>> list(numeric_range(3.5))
+ [0.0, 1.0, 2.0, 3.0]
+
+ With only *start* and *stop* specified, *step* defaults to ``1``. The
+ output items will match the type of *start*:
+
+ >>> from decimal import Decimal
+ >>> start = Decimal('2.1')
+ >>> stop = Decimal('5.1')
+ >>> list(numeric_range(start, stop))
+ [Decimal('2.1'), Decimal('3.1'), Decimal('4.1')]
+
+ With *start*, *stop*, and *step* specified the output items will match
+ the type of ``start + step``:
+
+ >>> from fractions import Fraction
+ >>> start = Fraction(1, 2) # Start at 1/2
+ >>> stop = Fraction(5, 2) # End at 5/2
+ >>> step = Fraction(1, 2) # Count by 1/2
+ >>> list(numeric_range(start, stop, step))
+ [Fraction(1, 2), Fraction(1, 1), Fraction(3, 2), Fraction(2, 1)]
+
+ If *step* is zero, ``ValueError`` is raised. Negative steps are supported:
+
+ >>> list(numeric_range(3, -1, -1.0))
+ [3.0, 2.0, 1.0, 0.0]
+
+ Be aware of the limitations of floating point numbers; the representation
+ of the yielded numbers may be surprising.
+
+ """
+ argc = len(args)
+ if argc == 1:
+ stop, = args
+ start = type(stop)(0)
+ step = 1
+ elif argc == 2:
+ start, stop = args
+ step = 1
+ elif argc == 3:
+ start, stop, step = args
+ else:
+ err_msg = 'numeric_range takes at most 3 arguments, got {}'
+ raise TypeError(err_msg.format(argc))
+
+ values = (start + (step * n) for n in count())
+ if step > 0:
+ return takewhile(partial(gt, stop), values)
+ elif step < 0:
+ return takewhile(partial(lt, stop), values)
+ else:
+ raise ValueError('numeric_range arg 3 must not be zero')
+
+
+def count_cycle(iterable, n=None):
+ """Cycle through the items from *iterable* up to *n* times, yielding
+ the number of completed cycles along with each item. If *n* is omitted the
+ process repeats indefinitely.
+
+ >>> list(count_cycle('AB', 3))
+ [(0, 'A'), (0, 'B'), (1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')]
+
+ """
+ iterable = tuple(iterable)
+ if not iterable:
+ return iter(())
+ counter = count() if n is None else range(n)
+ return ((i, item) for i in counter for item in iterable)
+
+
+def locate(iterable, pred=bool):
+ """Yield the index of each item in *iterable* for which *pred* returns
+ ``True``.
+
+ *pred* defaults to :func:`bool`, which will select truthy items:
+
+ >>> list(locate([0, 1, 1, 0, 1, 0, 0]))
+ [1, 2, 4]
+
+ Set *pred* to a custom function to, e.g., find the indexes for a particular
+ item:
+
+ >>> list(locate(['a', 'b', 'c', 'b'], lambda x: x == 'b'))
+ [1, 3]
+
+ Use with :func:`windowed` to find the indexes of a sub-sequence:
+
+ >>> from more_itertools import windowed
+ >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]
+ >>> sub = [1, 2, 3]
+ >>> pred = lambda w: w == tuple(sub) # windowed() returns tuples
+ >>> list(locate(windowed(iterable, len(sub)), pred=pred))
+ [1, 5, 9]
+
+ Use with :func:`seekable` to find indexes and then retrieve the associated
+ items:
+
+ >>> from itertools import count
+ >>> from more_itertools import seekable
+ >>> source = (3 * n + 1 if (n % 2) else n // 2 for n in count())
+ >>> it = seekable(source)
+ >>> pred = lambda x: x > 100
+ >>> indexes = locate(it, pred=pred)
+ >>> i = next(indexes)
+ >>> it.seek(i)
+ >>> next(it)
+ 106
+
+ """
+ return compress(count(), map(pred, iterable))
+
+
+def lstrip(iterable, pred):
+ """Yield the items from *iterable*, but strip any from the beginning
+ for which *pred* returns ``True``.
+
+ For example, to remove a set of items from the start of an iterable:
+
+ >>> iterable = (None, False, None, 1, 2, None, 3, False, None)
+ >>> pred = lambda x: x in {None, False, ''}
+ >>> list(lstrip(iterable, pred))
+ [1, 2, None, 3, False, None]
+
+ This function is analogous to to :func:`str.lstrip`, and is essentially
+ an wrapper for :func:`itertools.dropwhile`.
+
+ """
+ return dropwhile(pred, iterable)
+
+
+def rstrip(iterable, pred):
+ """Yield the items from *iterable*, but strip any from the end
+ for which *pred* returns ``True``.
+
+ For example, to remove a set of items from the end of an iterable:
+
+ >>> iterable = (None, False, None, 1, 2, None, 3, False, None)
+ >>> pred = lambda x: x in {None, False, ''}
+ >>> list(rstrip(iterable, pred))
+ [None, False, None, 1, 2, None, 3]
+
+ This function is analogous to :func:`str.rstrip`.
+
+ """
+ cache = []
+ cache_append = cache.append
+ for x in iterable:
+ if pred(x):
+ cache_append(x)
+ else:
+ for y in cache:
+ yield y
+ del cache[:]
+ yield x
+
+
+def strip(iterable, pred):
+ """Yield the items from *iterable*, but strip any from the
+ beginning and end for which *pred* returns ``True``.
+
+ For example, to remove a set of items from both ends of an iterable:
+
+ >>> iterable = (None, False, None, 1, 2, None, 3, False, None)
+ >>> pred = lambda x: x in {None, False, ''}
+ >>> list(strip(iterable, pred))
+ [1, 2, None, 3]
+
+ This function is analogous to :func:`str.strip`.
+
+ """
+ return rstrip(lstrip(iterable, pred), pred)
+
+
+def islice_extended(iterable, *args):
+ """An extension of :func:`itertools.islice` that supports negative values
+ for *stop*, *start*, and *step*.
+
+ >>> iterable = iter('abcdefgh')
+ >>> list(islice_extended(iterable, -4, -1))
+ ['e', 'f', 'g']
+
+ Slices with negative values require some caching of *iterable*, but this
+ function takes care to minimize the amount of memory required.
+
+ For example, you can use a negative step with an infinite iterator:
+
+ >>> from itertools import count
+ >>> list(islice_extended(count(), 110, 99, -2))
+ [110, 108, 106, 104, 102, 100]
+
+ """
+ s = slice(*args)
+ start = s.start
+ stop = s.stop
+ if s.step == 0:
+ raise ValueError('step argument must be a non-zero integer or None.')
+ step = s.step or 1
+
+ it = iter(iterable)
+
+ if step > 0:
+ start = 0 if (start is None) else start
+
+ if (start < 0):
+ # Consume all but the last -start items
+ cache = deque(enumerate(it, 1), maxlen=-start)
+ len_iter = cache[-1][0] if cache else 0
+
+ # Adjust start to be positive
+ i = max(len_iter + start, 0)
+
+ # Adjust stop to be positive
+ if stop is None:
+ j = len_iter
+ elif stop >= 0:
+ j = min(stop, len_iter)
+ else:
+ j = max(len_iter + stop, 0)
+
+ # Slice the cache
+ n = j - i
+ if n <= 0:
+ return
+
+ for index, item in islice(cache, 0, n, step):
+ yield item
+ elif (stop is not None) and (stop < 0):
+ # Advance to the start position
+ next(islice(it, start, start), None)
+
+ # When stop is negative, we have to carry -stop items while
+ # iterating
+ cache = deque(islice(it, -stop), maxlen=-stop)
+
+ for index, item in enumerate(it):
+ cached_item = cache.popleft()
+ if index % step == 0:
+ yield cached_item
+ cache.append(item)
+ else:
+ # When both start and stop are positive we have the normal case
+ for item in islice(it, start, stop, step):
+ yield item
+ else:
+ start = -1 if (start is None) else start
+
+ if (stop is not None) and (stop < 0):
+ # Consume all but the last items
+ n = -stop - 1
+ cache = deque(enumerate(it, 1), maxlen=n)
+ len_iter = cache[-1][0] if cache else 0
+
+ # If start and stop are both negative they are comparable and
+ # we can just slice. Otherwise we can adjust start to be negative
+ # and then slice.
+ if start < 0:
+ i, j = start, stop
+ else:
+ i, j = min(start - len_iter, -1), None
+
+ for index, item in list(cache)[i:j:step]:
+ yield item
+ else:
+ # Advance to the stop position
+ if stop is not None:
+ m = stop + 1
+ next(islice(it, m, m), None)
+
+ # stop is positive, so if start is negative they are not comparable
+ # and we need the rest of the items.
+ if start < 0:
+ i = start
+ n = None
+ # stop is None and start is positive, so we just need items up to
+ # the start index.
+ elif stop is None:
+ i = None
+ n = start + 1
+ # Both stop and start are positive, so they are comparable.
+ else:
+ i = None
+ n = start - stop
+ if n <= 0:
+ return
+
+ cache = list(islice(it, n))
+
+ for item in cache[i::step]:
+ yield item
+
+
+def always_reversible(iterable):
+ """An extension of :func:`reversed` that supports all iterables, not
+ just those which implement the ``Reversible`` or ``Sequence`` protocols.
+
+ >>> print(*always_reversible(x for x in range(3)))
+ 2 1 0
+
+ If the iterable is already reversible, this function returns the
+ result of :func:`reversed()`. If the iterable is not reversible,
+ this function will cache the remaining items in the iterable and
+ yield them in reverse order, which may require significant storage.
+ """
+ try:
+ return reversed(iterable)
+ except TypeError:
+ return reversed(list(iterable))
+
+
+def consecutive_groups(iterable, ordering=lambda x: x):
+ """Yield groups of consecutive items using :func:`itertools.groupby`.
+ The *ordering* function determines whether two items are adjacent by
+ returning their position.
+
+ By default, the ordering function is the identity function. This is
+ suitable for finding runs of numbers:
+
+ >>> iterable = [1, 10, 11, 12, 20, 30, 31, 32, 33, 40]
+ >>> for group in consecutive_groups(iterable):
+ ... print(list(group))
+ [1]
+ [10, 11, 12]
+ [20]
+ [30, 31, 32, 33]
+ [40]
+
+ For finding runs of adjacent letters, try using the :meth:`index` method
+ of a string of letters:
+
+ >>> from string import ascii_lowercase
+ >>> iterable = 'abcdfgilmnop'
+ >>> ordering = ascii_lowercase.index
+ >>> for group in consecutive_groups(iterable, ordering):
+ ... print(list(group))
+ ['a', 'b', 'c', 'd']
+ ['f', 'g']
+ ['i']
+ ['l', 'm', 'n', 'o', 'p']
+
+ """
+ for k, g in groupby(
+ enumerate(iterable), key=lambda x: x[0] - ordering(x[1])
+ ):
+ yield map(itemgetter(1), g)
+
+
+def difference(iterable, func=sub):
+ """By default, compute the first difference of *iterable* using
+ :func:`operator.sub`.
+
+ >>> iterable = [0, 1, 3, 6, 10]
+ >>> list(difference(iterable))
+ [0, 1, 2, 3, 4]
+
+ This is the opposite of :func:`accumulate`'s default behavior:
+
+ >>> from more_itertools import accumulate
+ >>> iterable = [0, 1, 2, 3, 4]
+ >>> list(accumulate(iterable))
+ [0, 1, 3, 6, 10]
+ >>> list(difference(accumulate(iterable)))
+ [0, 1, 2, 3, 4]
+
+ By default *func* is :func:`operator.sub`, but other functions can be
+ specified. They will be applied as follows::
+
+ A, B, C, D, ... --> A, func(B, A), func(C, B), func(D, C), ...
+
+ For example, to do progressive division:
+
+ >>> iterable = [1, 2, 6, 24, 120] # Factorial sequence
+ >>> func = lambda x, y: x // y
+ >>> list(difference(iterable, func))
+ [1, 2, 3, 4, 5]
+
+ """
+ a, b = tee(iterable)
+ try:
+ item = next(b)
+ except StopIteration:
+ return iter([])
+ return chain([item], map(lambda x: func(x[1], x[0]), zip(a, b)))
+
+
+class SequenceView(Sequence):
+ """Return a read-only view of the sequence object *target*.
+
+ :class:`SequenceView` objects are analagous to Python's built-in
+ "dictionary view" types. They provide a dynamic view of a sequence's items,
+ meaning that when the sequence updates, so does the view.
+
+ >>> seq = ['0', '1', '2']
+ >>> view = SequenceView(seq)
+ >>> view
+ SequenceView(['0', '1', '2'])
+ >>> seq.append('3')
+ >>> view
+ SequenceView(['0', '1', '2', '3'])
+
+ Sequence views support indexing, slicing, and length queries. They act
+ like the underlying sequence, except they don't allow assignment:
+
+ >>> view[1]
+ '1'
+ >>> view[1:-1]
+ ['1', '2']
+ >>> len(view)
+ 4
+
+ Sequence views are useful as an alternative to copying, as they don't
+ require (much) extra storage.
+
+ """
+ def __init__(self, target):
+ if not isinstance(target, Sequence):
+ raise TypeError
+ self._target = target
+
+ def __getitem__(self, index):
+ return self._target[index]
+
+ def __len__(self):
+ return len(self._target)
+
+ def __repr__(self):
+ return '{}({})'.format(self.__class__.__name__, repr(self._target))
+
+
+class seekable(object):
+ """Wrap an iterator to allow for seeking backward and forward. This
+ progressively caches the items in the source iterable so they can be
+ re-visited.
+
+ Call :meth:`seek` with an index to seek to that position in the source
+ iterable.
+
+ To "reset" an iterator, seek to ``0``:
+
+ >>> from itertools import count
+ >>> it = seekable((str(n) for n in count()))
+ >>> next(it), next(it), next(it)
+ ('0', '1', '2')
+ >>> it.seek(0)
+ >>> next(it), next(it), next(it)
+ ('0', '1', '2')
+ >>> next(it)
+ '3'
+
+ You can also seek forward:
+
+ >>> it = seekable((str(n) for n in range(20)))
+ >>> it.seek(10)
+ >>> next(it)
+ '10'
+ >>> it.seek(20) # Seeking past the end of the source isn't a problem
+ >>> list(it)
+ []
+ >>> it.seek(0) # Resetting works even after hitting the end
+ >>> next(it), next(it), next(it)
+ ('0', '1', '2')
+
+ The cache grows as the source iterable progresses, so beware of wrapping
+ very large or infinite iterables.
+
+ You may view the contents of the cache with the :meth:`elements` method.
+ That returns a :class:`SequenceView`, a view that updates automatically:
+
+ >>> it = seekable((str(n) for n in range(10)))
+ >>> next(it), next(it), next(it)
+ ('0', '1', '2')
+ >>> elements = it.elements()
+ >>> elements
+ SequenceView(['0', '1', '2'])
+ >>> next(it)
+ '3'
+ >>> elements
+ SequenceView(['0', '1', '2', '3'])
+
+ """
+
+ def __init__(self, iterable):
+ self._source = iter(iterable)
+ self._cache = []
+ self._index = None
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ if self._index is not None:
+ try:
+ item = self._cache[self._index]
+ except IndexError:
+ self._index = None
+ else:
+ self._index += 1
+ return item
+
+ item = next(self._source)
+ self._cache.append(item)
+ return item
+
+ next = __next__
+
+ def elements(self):
+ return SequenceView(self._cache)
+
+ def seek(self, index):
+ self._index = index
+ remainder = index - len(self._cache)
+ if remainder > 0:
+ consume(self, remainder)
+
+
+class run_length(object):
+ """
+ :func:`run_length.encode` compresses an iterable with run-length encoding.
+ It yields groups of repeated items with the count of how many times they
+ were repeated:
+
+ >>> uncompressed = 'abbcccdddd'
+ >>> list(run_length.encode(uncompressed))
+ [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
+
+ :func:`run_length.decode` decompresses an iterable that was previously
+ compressed with run-length encoding. It yields the items of the
+ decompressed iterable:
+
+ >>> compressed = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
+ >>> list(run_length.decode(compressed))
+ ['a', 'b', 'b', 'c', 'c', 'c', 'd', 'd', 'd', 'd']
+
+ """
+
+ @staticmethod
+ def encode(iterable):
+ return ((k, ilen(g)) for k, g in groupby(iterable))
+
+ @staticmethod
+ def decode(iterable):
+ return chain.from_iterable(repeat(k, n) for k, n in iterable)
+
+
+def exactly_n(iterable, n, predicate=bool):
+ """Return ``True`` if exactly ``n`` items in the iterable are ``True``
+ according to the *predicate* function.
+
+ >>> exactly_n([True, True, False], 2)
+ True
+ >>> exactly_n([True, True, False], 1)
+ False
+ >>> exactly_n([0, 1, 2, 3, 4, 5], 3, lambda x: x < 3)
+ True
+
+ The iterable will be advanced until ``n + 1`` truthy items are encountered,
+ so avoid calling it on infinite iterables.
+
+ """
+ return len(take(n + 1, filter(predicate, iterable))) == n
+
+
+def circular_shifts(iterable):
+ """Return a list of circular shifts of *iterable*.
+
+ >>> circular_shifts(range(4))
+ [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)]
+ """
+ lst = list(iterable)
+ return take(len(lst), windowed(cycle(lst), len(lst)))
+
+
+def make_decorator(wrapping_func, result_index=0):
+ """Return a decorator version of *wrapping_func*, which is a function that
+ modifies an iterable. *result_index* is the position in that function's
+ signature where the iterable goes.
+
+ This lets you use itertools on the "production end," i.e. at function
+ definition. This can augment what the function returns without changing the
+ function's code.
+
+ For example, to produce a decorator version of :func:`chunked`:
+
+ >>> from more_itertools import chunked
+ >>> chunker = make_decorator(chunked, result_index=0)
+ >>> @chunker(3)
+ ... def iter_range(n):
+ ... return iter(range(n))
+ ...
+ >>> list(iter_range(9))
+ [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
+
+ To only allow truthy items to be returned:
+
+ >>> truth_serum = make_decorator(filter, result_index=1)
+ >>> @truth_serum(bool)
+ ... def boolean_test():
+ ... return [0, 1, '', ' ', False, True]
+ ...
+ >>> list(boolean_test())
+ [1, ' ', True]
+
+ The :func:`peekable` and :func:`seekable` wrappers make for practical
+ decorators:
+
+ >>> from more_itertools import peekable
+ >>> peekable_function = make_decorator(peekable)
+ >>> @peekable_function()
+ ... def str_range(*args):
+ ... return (str(x) for x in range(*args))
+ ...
+ >>> it = str_range(1, 20, 2)
+ >>> next(it), next(it), next(it)
+ ('1', '3', '5')
+ >>> it.peek()
+ '7'
+ >>> next(it)
+ '7'
+
+ """
+ # See https://sites.google.com/site/bbayles/index/decorator_factory for
+ # notes on how this works.
+ def decorator(*wrapping_args, **wrapping_kwargs):
+ def outer_wrapper(f):
+ def inner_wrapper(*args, **kwargs):
+ result = f(*args, **kwargs)
+ wrapping_args_ = list(wrapping_args)
+ wrapping_args_.insert(result_index, result)
+ return wrapping_func(*wrapping_args_, **wrapping_kwargs)
+
+ return inner_wrapper
+
+ return outer_wrapper
+
+ return decorator
+
+
+def map_reduce(iterable, keyfunc, valuefunc=None, reducefunc=None):
+ """Return a dictionary that maps the items in *iterable* to categories
+ defined by *keyfunc*, transforms them with *valuefunc*, and
+ then summarizes them by category with *reducefunc*.
+
+ *valuefunc* defaults to the identity function if it is unspecified.
+ If *reducefunc* is unspecified, no summarization takes place:
+
+ >>> keyfunc = lambda x: x.upper()
+ >>> result = map_reduce('abbccc', keyfunc)
+ >>> sorted(result.items())
+ [('A', ['a']), ('B', ['b', 'b']), ('C', ['c', 'c', 'c'])]
+
+ Specifying *valuefunc* transforms the categorized items:
+
+ >>> keyfunc = lambda x: x.upper()
+ >>> valuefunc = lambda x: 1
+ >>> result = map_reduce('abbccc', keyfunc, valuefunc)
+ >>> sorted(result.items())
+ [('A', [1]), ('B', [1, 1]), ('C', [1, 1, 1])]
+
+ Specifying *reducefunc* summarizes the categorized items:
+
+ >>> keyfunc = lambda x: x.upper()
+ >>> valuefunc = lambda x: 1
+ >>> reducefunc = sum
+ >>> result = map_reduce('abbccc', keyfunc, valuefunc, reducefunc)
+ >>> sorted(result.items())
+ [('A', 1), ('B', 2), ('C', 3)]
+
+ You may want to filter the input iterable before applying the map/reduce
+ proecdure:
+
+ >>> all_items = range(30)
+ >>> items = [x for x in all_items if 10 <= x <= 20] # Filter
+ >>> keyfunc = lambda x: x % 2 # Evens map to 0; odds to 1
+ >>> categories = map_reduce(items, keyfunc=keyfunc)
+ >>> sorted(categories.items())
+ [(0, [10, 12, 14, 16, 18, 20]), (1, [11, 13, 15, 17, 19])]
+ >>> summaries = map_reduce(items, keyfunc=keyfunc, reducefunc=sum)
+ >>> sorted(summaries.items())
+ [(0, 90), (1, 75)]
+
+ Note that all items in the iterable are gathered into a list before the
+ summarization step, which may require significant storage.
+
+ The returned object is a :obj:`collections.defaultdict` with the
+ ``default_factory`` set to ``None``, such that it behaves like a normal
+ dictionary.
+
+ """
+ valuefunc = (lambda x: x) if (valuefunc is None) else valuefunc
+
+ ret = defaultdict(list)
+ for item in iterable:
+ key = keyfunc(item)
+ value = valuefunc(item)
+ ret[key].append(value)
+
+ if reducefunc is not None:
+ for key, value_list in ret.items():
+ ret[key] = reducefunc(value_list)
+
+ ret.default_factory = None
+ return ret
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/recipes.py b/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/recipes.py
new file mode 100644
index 0000000000..3a7706cb91
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/recipes.py
@@ -0,0 +1,565 @@
+"""Imported from the recipes section of the itertools documentation.
+
+All functions taken from the recipes section of the itertools library docs
+[1]_.
+Some backward-compatible usability improvements have been made.
+
+.. [1] http://docs.python.org/library/itertools.html#recipes
+
+"""
+from collections import deque
+from itertools import (
+ chain, combinations, count, cycle, groupby, islice, repeat, starmap, tee
+)
+import operator
+from random import randrange, sample, choice
+
+from six import PY2
+from six.moves import filter, filterfalse, map, range, zip, zip_longest
+
+__all__ = [
+ 'accumulate',
+ 'all_equal',
+ 'consume',
+ 'dotproduct',
+ 'first_true',
+ 'flatten',
+ 'grouper',
+ 'iter_except',
+ 'ncycles',
+ 'nth',
+ 'nth_combination',
+ 'padnone',
+ 'pairwise',
+ 'partition',
+ 'powerset',
+ 'prepend',
+ 'quantify',
+ 'random_combination_with_replacement',
+ 'random_combination',
+ 'random_permutation',
+ 'random_product',
+ 'repeatfunc',
+ 'roundrobin',
+ 'tabulate',
+ 'tail',
+ 'take',
+ 'unique_everseen',
+ 'unique_justseen',
+]
+
+
+def accumulate(iterable, func=operator.add):
+ """
+ Return an iterator whose items are the accumulated results of a function
+ (specified by the optional *func* argument) that takes two arguments.
+ By default, returns accumulated sums with :func:`operator.add`.
+
+ >>> list(accumulate([1, 2, 3, 4, 5])) # Running sum
+ [1, 3, 6, 10, 15]
+ >>> list(accumulate([1, 2, 3], func=operator.mul)) # Running product
+ [1, 2, 6]
+ >>> list(accumulate([0, 1, -1, 2, 3, 2], func=max)) # Running maximum
+ [0, 1, 1, 2, 3, 3]
+
+ This function is available in the ``itertools`` module for Python 3.2 and
+ greater.
+
+ """
+ it = iter(iterable)
+ try:
+ total = next(it)
+ except StopIteration:
+ return
+ else:
+ yield total
+
+ for element in it:
+ total = func(total, element)
+ yield total
+
+
+def take(n, iterable):
+ """Return first *n* items of the iterable as a list.
+
+ >>> take(3, range(10))
+ [0, 1, 2]
+ >>> take(5, range(3))
+ [0, 1, 2]
+
+ Effectively a short replacement for ``next`` based iterator consumption
+ when you want more than one item, but less than the whole iterator.
+
+ """
+ return list(islice(iterable, n))
+
+
+def tabulate(function, start=0):
+ """Return an iterator over the results of ``func(start)``,
+ ``func(start + 1)``, ``func(start + 2)``...
+
+ *func* should be a function that accepts one integer argument.
+
+ If *start* is not specified it defaults to 0. It will be incremented each
+ time the iterator is advanced.
+
+ >>> square = lambda x: x ** 2
+ >>> iterator = tabulate(square, -3)
+ >>> take(4, iterator)
+ [9, 4, 1, 0]
+
+ """
+ return map(function, count(start))
+
+
+def tail(n, iterable):
+ """Return an iterator over the last *n* items of *iterable*.
+
+ >>> t = tail(3, 'ABCDEFG')
+ >>> list(t)
+ ['E', 'F', 'G']
+
+ """
+ return iter(deque(iterable, maxlen=n))
+
+
+def consume(iterator, n=None):
+ """Advance *iterable* by *n* steps. If *n* is ``None``, consume it
+ entirely.
+
+ Efficiently exhausts an iterator without returning values. Defaults to
+ consuming the whole iterator, but an optional second argument may be
+ provided to limit consumption.
+
+ >>> i = (x for x in range(10))
+ >>> next(i)
+ 0
+ >>> consume(i, 3)
+ >>> next(i)
+ 4
+ >>> consume(i)
+ >>> next(i)
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ StopIteration
+
+ If the iterator has fewer items remaining than the provided limit, the
+ whole iterator will be consumed.
+
+ >>> i = (x for x in range(3))
+ >>> consume(i, 5)
+ >>> next(i)
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ StopIteration
+
+ """
+ # Use functions that consume iterators at C speed.
+ if n is None:
+ # feed the entire iterator into a zero-length deque
+ deque(iterator, maxlen=0)
+ else:
+ # advance to the empty slice starting at position n
+ next(islice(iterator, n, n), None)
+
+
+def nth(iterable, n, default=None):
+ """Returns the nth item or a default value.
+
+ >>> l = range(10)
+ >>> nth(l, 3)
+ 3
+ >>> nth(l, 20, "zebra")
+ 'zebra'
+
+ """
+ return next(islice(iterable, n, None), default)
+
+
+def all_equal(iterable):
+ """
+ Returns ``True`` if all the elements are equal to each other.
+
+ >>> all_equal('aaaa')
+ True
+ >>> all_equal('aaab')
+ False
+
+ """
+ g = groupby(iterable)
+ return next(g, True) and not next(g, False)
+
+
+def quantify(iterable, pred=bool):
+ """Return the how many times the predicate is true.
+
+ >>> quantify([True, False, True])
+ 2
+
+ """
+ return sum(map(pred, iterable))
+
+
+def padnone(iterable):
+ """Returns the sequence of elements and then returns ``None`` indefinitely.
+
+ >>> take(5, padnone(range(3)))
+ [0, 1, 2, None, None]
+
+ Useful for emulating the behavior of the built-in :func:`map` function.
+
+ See also :func:`padded`.
+
+ """
+ return chain(iterable, repeat(None))
+
+
+def ncycles(iterable, n):
+ """Returns the sequence elements *n* times
+
+ >>> list(ncycles(["a", "b"], 3))
+ ['a', 'b', 'a', 'b', 'a', 'b']
+
+ """
+ return chain.from_iterable(repeat(tuple(iterable), n))
+
+
+def dotproduct(vec1, vec2):
+ """Returns the dot product of the two iterables.
+
+ >>> dotproduct([10, 10], [20, 20])
+ 400
+
+ """
+ return sum(map(operator.mul, vec1, vec2))
+
+
+def flatten(listOfLists):
+ """Return an iterator flattening one level of nesting in a list of lists.
+
+ >>> list(flatten([[0, 1], [2, 3]]))
+ [0, 1, 2, 3]
+
+ See also :func:`collapse`, which can flatten multiple levels of nesting.
+
+ """
+ return chain.from_iterable(listOfLists)
+
+
+def repeatfunc(func, times=None, *args):
+ """Call *func* with *args* repeatedly, returning an iterable over the
+ results.
+
+ If *times* is specified, the iterable will terminate after that many
+ repetitions:
+
+ >>> from operator import add
+ >>> times = 4
+ >>> args = 3, 5
+ >>> list(repeatfunc(add, times, *args))
+ [8, 8, 8, 8]
+
+ If *times* is ``None`` the iterable will not terminate:
+
+ >>> from random import randrange
+ >>> times = None
+ >>> args = 1, 11
+ >>> take(6, repeatfunc(randrange, times, *args)) # doctest:+SKIP
+ [2, 4, 8, 1, 8, 4]
+
+ """
+ if times is None:
+ return starmap(func, repeat(args))
+ return starmap(func, repeat(args, times))
+
+
+def pairwise(iterable):
+ """Returns an iterator of paired items, overlapping, from the original
+
+ >>> take(4, pairwise(count()))
+ [(0, 1), (1, 2), (2, 3), (3, 4)]
+
+ """
+ a, b = tee(iterable)
+ next(b, None)
+ return zip(a, b)
+
+
+def grouper(n, iterable, fillvalue=None):
+ """Collect data into fixed-length chunks or blocks.
+
+ >>> list(grouper(3, 'ABCDEFG', 'x'))
+ [('A', 'B', 'C'), ('D', 'E', 'F'), ('G', 'x', 'x')]
+
+ """
+ args = [iter(iterable)] * n
+ return zip_longest(fillvalue=fillvalue, *args)
+
+
+def roundrobin(*iterables):
+ """Yields an item from each iterable, alternating between them.
+
+ >>> list(roundrobin('ABC', 'D', 'EF'))
+ ['A', 'D', 'E', 'B', 'F', 'C']
+
+ This function produces the same output as :func:`interleave_longest`, but
+ may perform better for some inputs (in particular when the number of
+ iterables is small).
+
+ """
+ # Recipe credited to George Sakkis
+ pending = len(iterables)
+ if PY2:
+ nexts = cycle(iter(it).next for it in iterables)
+ else:
+ nexts = cycle(iter(it).__next__ for it in iterables)
+ while pending:
+ try:
+ for next in nexts:
+ yield next()
+ except StopIteration:
+ pending -= 1
+ nexts = cycle(islice(nexts, pending))
+
+
+def partition(pred, iterable):
+ """
+ Returns a 2-tuple of iterables derived from the input iterable.
+ The first yields the items that have ``pred(item) == False``.
+ The second yields the items that have ``pred(item) == True``.
+
+ >>> is_odd = lambda x: x % 2 != 0
+ >>> iterable = range(10)
+ >>> even_items, odd_items = partition(is_odd, iterable)
+ >>> list(even_items), list(odd_items)
+ ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9])
+
+ """
+ # partition(is_odd, range(10)) --> 0 2 4 6 8 and 1 3 5 7 9
+ t1, t2 = tee(iterable)
+ return filterfalse(pred, t1), filter(pred, t2)
+
+
+def powerset(iterable):
+ """Yields all possible subsets of the iterable.
+
+ >>> list(powerset([1,2,3]))
+ [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)]
+
+ """
+ s = list(iterable)
+ return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1))
+
+
+def unique_everseen(iterable, key=None):
+ """
+ Yield unique elements, preserving order.
+
+ >>> list(unique_everseen('AAAABBBCCDAABBB'))
+ ['A', 'B', 'C', 'D']
+ >>> list(unique_everseen('ABBCcAD', str.lower))
+ ['A', 'B', 'C', 'D']
+
+ Sequences with a mix of hashable and unhashable items can be used.
+ The function will be slower (i.e., `O(n^2)`) for unhashable items.
+
+ """
+ seenset = set()
+ seenset_add = seenset.add
+ seenlist = []
+ seenlist_add = seenlist.append
+ if key is None:
+ for element in iterable:
+ try:
+ if element not in seenset:
+ seenset_add(element)
+ yield element
+ except TypeError:
+ if element not in seenlist:
+ seenlist_add(element)
+ yield element
+ else:
+ for element in iterable:
+ k = key(element)
+ try:
+ if k not in seenset:
+ seenset_add(k)
+ yield element
+ except TypeError:
+ if k not in seenlist:
+ seenlist_add(k)
+ yield element
+
+
+def unique_justseen(iterable, key=None):
+ """Yields elements in order, ignoring serial duplicates
+
+ >>> list(unique_justseen('AAAABBBCCDAABBB'))
+ ['A', 'B', 'C', 'D', 'A', 'B']
+ >>> list(unique_justseen('ABBCcAD', str.lower))
+ ['A', 'B', 'C', 'A', 'D']
+
+ """
+ return map(next, map(operator.itemgetter(1), groupby(iterable, key)))
+
+
+def iter_except(func, exception, first=None):
+ """Yields results from a function repeatedly until an exception is raised.
+
+ Converts a call-until-exception interface to an iterator interface.
+ Like ``iter(func, sentinel)``, but uses an exception instead of a sentinel
+ to end the loop.
+
+ >>> l = [0, 1, 2]
+ >>> list(iter_except(l.pop, IndexError))
+ [2, 1, 0]
+
+ """
+ try:
+ if first is not None:
+ yield first()
+ while 1:
+ yield func()
+ except exception:
+ pass
+
+
+def first_true(iterable, default=False, pred=None):
+ """
+ Returns the first true value in the iterable.
+
+ If no true value is found, returns *default*
+
+ If *pred* is not None, returns the first item for which
+ ``pred(item) == True`` .
+
+ >>> first_true(range(10))
+ 1
+ >>> first_true(range(10), pred=lambda x: x > 5)
+ 6
+ >>> first_true(range(10), default='missing', pred=lambda x: x > 9)
+ 'missing'
+
+ """
+ return next(filter(pred, iterable), default)
+
+
+def random_product(*args, **kwds):
+ """Draw an item at random from each of the input iterables.
+
+ >>> random_product('abc', range(4), 'XYZ') # doctest:+SKIP
+ ('c', 3, 'Z')
+
+ If *repeat* is provided as a keyword argument, that many items will be
+ drawn from each iterable.
+
+ >>> random_product('abcd', range(4), repeat=2) # doctest:+SKIP
+ ('a', 2, 'd', 3)
+
+ This equivalent to taking a random selection from
+ ``itertools.product(*args, **kwarg)``.
+
+ """
+ pools = [tuple(pool) for pool in args] * kwds.get('repeat', 1)
+ return tuple(choice(pool) for pool in pools)
+
+
+def random_permutation(iterable, r=None):
+ """Return a random *r* length permutation of the elements in *iterable*.
+
+ If *r* is not specified or is ``None``, then *r* defaults to the length of
+ *iterable*.
+
+ >>> random_permutation(range(5)) # doctest:+SKIP
+ (3, 4, 0, 1, 2)
+
+ This equivalent to taking a random selection from
+ ``itertools.permutations(iterable, r)``.
+
+ """
+ pool = tuple(iterable)
+ r = len(pool) if r is None else r
+ return tuple(sample(pool, r))
+
+
+def random_combination(iterable, r):
+ """Return a random *r* length subsequence of the elements in *iterable*.
+
+ >>> random_combination(range(5), 3) # doctest:+SKIP
+ (2, 3, 4)
+
+ This equivalent to taking a random selection from
+ ``itertools.combinations(iterable, r)``.
+
+ """
+ pool = tuple(iterable)
+ n = len(pool)
+ indices = sorted(sample(range(n), r))
+ return tuple(pool[i] for i in indices)
+
+
+def random_combination_with_replacement(iterable, r):
+ """Return a random *r* length subsequence of elements in *iterable*,
+ allowing individual elements to be repeated.
+
+ >>> random_combination_with_replacement(range(3), 5) # doctest:+SKIP
+ (0, 0, 1, 2, 2)
+
+ This equivalent to taking a random selection from
+ ``itertools.combinations_with_replacement(iterable, r)``.
+
+ """
+ pool = tuple(iterable)
+ n = len(pool)
+ indices = sorted(randrange(n) for i in range(r))
+ return tuple(pool[i] for i in indices)
+
+
+def nth_combination(iterable, r, index):
+ """Equivalent to ``list(combinations(iterable, r))[index]``.
+
+ The subsequences of *iterable* that are of length *r* can be ordered
+ lexicographically. :func:`nth_combination` computes the subsequence at
+ sort position *index* directly, without computing the previous
+ subsequences.
+
+ """
+ pool = tuple(iterable)
+ n = len(pool)
+ if (r < 0) or (r > n):
+ raise ValueError
+
+ c = 1
+ k = min(r, n - r)
+ for i in range(1, k + 1):
+ c = c * (n - k + i) // i
+
+ if index < 0:
+ index += c
+
+ if (index < 0) or (index >= c):
+ raise IndexError
+
+ result = []
+ while r:
+ c, n, r = c * r // n, n - 1, r - 1
+ while index >= c:
+ index -= c
+ c, n = c * (n - r) // n, n - 1
+ result.append(pool[-1 - n])
+
+ return tuple(result)
+
+
+def prepend(value, iterator):
+ """Yield *value*, followed by the elements in *iterator*.
+
+ >>> value = '0'
+ >>> iterator = ['1', '2', '3']
+ >>> list(prepend(value, iterator))
+ ['0', '1', '2', '3']
+
+ To prepend multiple values, see :func:`itertools.chain`.
+
+ """
+ return chain([value], iterator)
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/tests/__init__.py b/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/tests/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/tests/test_more.py b/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/tests/test_more.py
new file mode 100644
index 0000000000..2023ba6a4c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/tests/test_more.py
@@ -0,0 +1,1848 @@
+from __future__ import division, print_function, unicode_literals
+
+from decimal import Decimal
+from doctest import DocTestSuite
+from fractions import Fraction
+from functools import partial, reduce
+from heapq import merge
+from io import StringIO
+from itertools import (
+ chain,
+ count,
+ groupby,
+ islice,
+ permutations,
+ product,
+ repeat,
+)
+from operator import add, mul, itemgetter
+from unittest import TestCase
+
+from six.moves import filter, map, range, zip
+
+import more_itertools as mi
+
+
+def load_tests(loader, tests, ignore):
+ # Add the doctests
+ tests.addTests(DocTestSuite('more_itertools.more'))
+ return tests
+
+
+class CollateTests(TestCase):
+ """Unit tests for ``collate()``"""
+ # Also accidentally tests peekable, though that could use its own tests
+
+ def test_default(self):
+ """Test with the default `key` function."""
+ iterables = [range(4), range(7), range(3, 6)]
+ self.assertEqual(
+ sorted(reduce(list.__add__, [list(it) for it in iterables])),
+ list(mi.collate(*iterables))
+ )
+
+ def test_key(self):
+ """Test using a custom `key` function."""
+ iterables = [range(5, 0, -1), range(4, 0, -1)]
+ actual = sorted(
+ reduce(list.__add__, [list(it) for it in iterables]), reverse=True
+ )
+ expected = list(mi.collate(*iterables, key=lambda x: -x))
+ self.assertEqual(actual, expected)
+
+ def test_empty(self):
+ """Be nice if passed an empty list of iterables."""
+ self.assertEqual([], list(mi.collate()))
+
+ def test_one(self):
+ """Work when only 1 iterable is passed."""
+ self.assertEqual([0, 1], list(mi.collate(range(2))))
+
+ def test_reverse(self):
+ """Test the `reverse` kwarg."""
+ iterables = [range(4, 0, -1), range(7, 0, -1), range(3, 6, -1)]
+
+ actual = sorted(
+ reduce(list.__add__, [list(it) for it in iterables]), reverse=True
+ )
+ expected = list(mi.collate(*iterables, reverse=True))
+ self.assertEqual(actual, expected)
+
+ def test_alias(self):
+ self.assertNotEqual(merge.__doc__, mi.collate.__doc__)
+ self.assertNotEqual(partial.__doc__, mi.collate.__doc__)
+
+
+class ChunkedTests(TestCase):
+ """Tests for ``chunked()``"""
+
+ def test_even(self):
+ """Test when ``n`` divides evenly into the length of the iterable."""
+ self.assertEqual(
+ list(mi.chunked('ABCDEF', 3)), [['A', 'B', 'C'], ['D', 'E', 'F']]
+ )
+
+ def test_odd(self):
+ """Test when ``n`` does not divide evenly into the length of the
+ iterable.
+
+ """
+ self.assertEqual(
+ list(mi.chunked('ABCDE', 3)), [['A', 'B', 'C'], ['D', 'E']]
+ )
+
+
+class FirstTests(TestCase):
+ """Tests for ``first()``"""
+
+ def test_many(self):
+ """Test that it works on many-item iterables."""
+ # Also try it on a generator expression to make sure it works on
+ # whatever those return, across Python versions.
+ self.assertEqual(mi.first(x for x in range(4)), 0)
+
+ def test_one(self):
+ """Test that it doesn't raise StopIteration prematurely."""
+ self.assertEqual(mi.first([3]), 3)
+
+ def test_empty_stop_iteration(self):
+ """It should raise StopIteration for empty iterables."""
+ self.assertRaises(ValueError, lambda: mi.first([]))
+
+ def test_default(self):
+ """It should return the provided default arg for empty iterables."""
+ self.assertEqual(mi.first([], 'boo'), 'boo')
+
+
+class PeekableTests(TestCase):
+ """Tests for ``peekable()`` behavor not incidentally covered by testing
+ ``collate()``
+
+ """
+ def test_peek_default(self):
+ """Make sure passing a default into ``peek()`` works."""
+ p = mi.peekable([])
+ self.assertEqual(p.peek(7), 7)
+
+ def test_truthiness(self):
+ """Make sure a ``peekable`` tests true iff there are items remaining in
+ the iterable.
+
+ """
+ p = mi.peekable([])
+ self.assertFalse(p)
+
+ p = mi.peekable(range(3))
+ self.assertTrue(p)
+
+ def test_simple_peeking(self):
+ """Make sure ``next`` and ``peek`` advance and don't advance the
+ iterator, respectively.
+
+ """
+ p = mi.peekable(range(10))
+ self.assertEqual(next(p), 0)
+ self.assertEqual(p.peek(), 1)
+ self.assertEqual(next(p), 1)
+
+ def test_indexing(self):
+ """
+ Indexing into the peekable shouldn't advance the iterator.
+ """
+ p = mi.peekable('abcdefghijkl')
+
+ # The 0th index is what ``next()`` will return
+ self.assertEqual(p[0], 'a')
+ self.assertEqual(next(p), 'a')
+
+ # Indexing further into the peekable shouldn't advance the itertor
+ self.assertEqual(p[2], 'd')
+ self.assertEqual(next(p), 'b')
+
+ # The 0th index moves up with the iterator; the last index follows
+ self.assertEqual(p[0], 'c')
+ self.assertEqual(p[9], 'l')
+
+ self.assertEqual(next(p), 'c')
+ self.assertEqual(p[8], 'l')
+
+ # Negative indexing should work too
+ self.assertEqual(p[-2], 'k')
+ self.assertEqual(p[-9], 'd')
+ self.assertRaises(IndexError, lambda: p[-10])
+
+ def test_slicing(self):
+ """Slicing the peekable shouldn't advance the iterator."""
+ seq = list('abcdefghijkl')
+ p = mi.peekable(seq)
+
+ # Slicing the peekable should just be like slicing a re-iterable
+ self.assertEqual(p[1:4], seq[1:4])
+
+ # Advancing the iterator moves the slices up also
+ self.assertEqual(next(p), 'a')
+ self.assertEqual(p[1:4], seq[1:][1:4])
+
+ # Implicit starts and stop should work
+ self.assertEqual(p[:5], seq[1:][:5])
+ self.assertEqual(p[:], seq[1:][:])
+
+ # Indexing past the end should work
+ self.assertEqual(p[:100], seq[1:][:100])
+
+ # Steps should work, including negative
+ self.assertEqual(p[::2], seq[1:][::2])
+ self.assertEqual(p[::-1], seq[1:][::-1])
+
+ def test_slicing_reset(self):
+ """Test slicing on a fresh iterable each time"""
+ iterable = ['0', '1', '2', '3', '4', '5']
+ indexes = list(range(-4, len(iterable) + 4)) + [None]
+ steps = [1, 2, 3, 4, -1, -2, -3, 4]
+ for slice_args in product(indexes, indexes, steps):
+ it = iter(iterable)
+ p = mi.peekable(it)
+ next(p)
+ index = slice(*slice_args)
+ actual = p[index]
+ expected = iterable[1:][index]
+ self.assertEqual(actual, expected, slice_args)
+
+ def test_slicing_error(self):
+ iterable = '01234567'
+ p = mi.peekable(iter(iterable))
+
+ # Prime the cache
+ p.peek()
+ old_cache = list(p._cache)
+
+ # Illegal slice
+ with self.assertRaises(ValueError):
+ p[1:-1:0]
+
+ # Neither the cache nor the iteration should be affected
+ self.assertEqual(old_cache, list(p._cache))
+ self.assertEqual(list(p), list(iterable))
+
+ def test_passthrough(self):
+ """Iterating a peekable without using ``peek()`` or ``prepend()``
+ should just give the underlying iterable's elements (a trivial test but
+ useful to set a baseline in case something goes wrong)"""
+ expected = [1, 2, 3, 4, 5]
+ actual = list(mi.peekable(expected))
+ self.assertEqual(actual, expected)
+
+ # prepend() behavior tests
+
+ def test_prepend(self):
+ """Tests intersperesed ``prepend()`` and ``next()`` calls"""
+ it = mi.peekable(range(2))
+ actual = []
+
+ # Test prepend() before next()
+ it.prepend(10)
+ actual += [next(it), next(it)]
+
+ # Test prepend() between next()s
+ it.prepend(11)
+ actual += [next(it), next(it)]
+
+ # Test prepend() after source iterable is consumed
+ it.prepend(12)
+ actual += [next(it)]
+
+ expected = [10, 0, 11, 1, 12]
+ self.assertEqual(actual, expected)
+
+ def test_multi_prepend(self):
+ """Tests prepending multiple items and getting them in proper order"""
+ it = mi.peekable(range(5))
+ actual = [next(it), next(it)]
+ it.prepend(10, 11, 12)
+ it.prepend(20, 21)
+ actual += list(it)
+ expected = [0, 1, 20, 21, 10, 11, 12, 2, 3, 4]
+ self.assertEqual(actual, expected)
+
+ def test_empty(self):
+ """Tests prepending in front of an empty iterable"""
+ it = mi.peekable([])
+ it.prepend(10)
+ actual = list(it)
+ expected = [10]
+ self.assertEqual(actual, expected)
+
+ def test_prepend_truthiness(self):
+ """Tests that ``__bool__()`` or ``__nonzero__()`` works properly
+ with ``prepend()``"""
+ it = mi.peekable(range(5))
+ self.assertTrue(it)
+ actual = list(it)
+ self.assertFalse(it)
+ it.prepend(10)
+ self.assertTrue(it)
+ actual += [next(it)]
+ self.assertFalse(it)
+ expected = [0, 1, 2, 3, 4, 10]
+ self.assertEqual(actual, expected)
+
+ def test_multi_prepend_peek(self):
+ """Tests prepending multiple elements and getting them in reverse order
+ while peeking"""
+ it = mi.peekable(range(5))
+ actual = [next(it), next(it)]
+ self.assertEqual(it.peek(), 2)
+ it.prepend(10, 11, 12)
+ self.assertEqual(it.peek(), 10)
+ it.prepend(20, 21)
+ self.assertEqual(it.peek(), 20)
+ actual += list(it)
+ self.assertFalse(it)
+ expected = [0, 1, 20, 21, 10, 11, 12, 2, 3, 4]
+ self.assertEqual(actual, expected)
+
+ def test_prepend_after_stop(self):
+ """Test resuming iteration after a previous exhaustion"""
+ it = mi.peekable(range(3))
+ self.assertEqual(list(it), [0, 1, 2])
+ self.assertRaises(StopIteration, lambda: next(it))
+ it.prepend(10)
+ self.assertEqual(next(it), 10)
+ self.assertRaises(StopIteration, lambda: next(it))
+
+ def test_prepend_slicing(self):
+ """Tests interaction between prepending and slicing"""
+ seq = list(range(20))
+ p = mi.peekable(seq)
+
+ p.prepend(30, 40, 50)
+ pseq = [30, 40, 50] + seq # pseq for prepended_seq
+
+ # adapt the specific tests from test_slicing
+ self.assertEqual(p[0], 30)
+ self.assertEqual(p[1:8], pseq[1:8])
+ self.assertEqual(p[1:], pseq[1:])
+ self.assertEqual(p[:5], pseq[:5])
+ self.assertEqual(p[:], pseq[:])
+ self.assertEqual(p[:100], pseq[:100])
+ self.assertEqual(p[::2], pseq[::2])
+ self.assertEqual(p[::-1], pseq[::-1])
+
+ def test_prepend_indexing(self):
+ """Tests interaction between prepending and indexing"""
+ seq = list(range(20))
+ p = mi.peekable(seq)
+
+ p.prepend(30, 40, 50)
+
+ self.assertEqual(p[0], 30)
+ self.assertEqual(next(p), 30)
+ self.assertEqual(p[2], 0)
+ self.assertEqual(next(p), 40)
+ self.assertEqual(p[0], 50)
+ self.assertEqual(p[9], 8)
+ self.assertEqual(next(p), 50)
+ self.assertEqual(p[8], 8)
+ self.assertEqual(p[-2], 18)
+ self.assertEqual(p[-9], 11)
+ self.assertRaises(IndexError, lambda: p[-21])
+
+ def test_prepend_iterable(self):
+ """Tests prepending from an iterable"""
+ it = mi.peekable(range(5))
+ # Don't directly use the range() object to avoid any range-specific
+ # optimizations
+ it.prepend(*(x for x in range(5)))
+ actual = list(it)
+ expected = list(chain(range(5), range(5)))
+ self.assertEqual(actual, expected)
+
+ def test_prepend_many(self):
+ """Tests that prepending a huge number of elements works"""
+ it = mi.peekable(range(5))
+ # Don't directly use the range() object to avoid any range-specific
+ # optimizations
+ it.prepend(*(x for x in range(20000)))
+ actual = list(it)
+ expected = list(chain(range(20000), range(5)))
+ self.assertEqual(actual, expected)
+
+ def test_prepend_reversed(self):
+ """Tests prepending from a reversed iterable"""
+ it = mi.peekable(range(3))
+ it.prepend(*reversed((10, 11, 12)))
+ actual = list(it)
+ expected = [12, 11, 10, 0, 1, 2]
+ self.assertEqual(actual, expected)
+
+
+class ConsumerTests(TestCase):
+ """Tests for ``consumer()``"""
+
+ def test_consumer(self):
+ @mi.consumer
+ def eater():
+ while True:
+ x = yield # noqa
+
+ e = eater()
+ e.send('hi') # without @consumer, would raise TypeError
+
+
+class DistinctPermutationsTests(TestCase):
+ def test_distinct_permutations(self):
+ """Make sure the output for ``distinct_permutations()`` is the same as
+ set(permutations(it)).
+
+ """
+ iterable = ['z', 'a', 'a', 'q', 'q', 'q', 'y']
+ test_output = sorted(mi.distinct_permutations(iterable))
+ ref_output = sorted(set(permutations(iterable)))
+ self.assertEqual(test_output, ref_output)
+
+ def test_other_iterables(self):
+ """Make sure ``distinct_permutations()`` accepts a different type of
+ iterables.
+
+ """
+ # a generator
+ iterable = (c for c in ['z', 'a', 'a', 'q', 'q', 'q', 'y'])
+ test_output = sorted(mi.distinct_permutations(iterable))
+ # "reload" it
+ iterable = (c for c in ['z', 'a', 'a', 'q', 'q', 'q', 'y'])
+ ref_output = sorted(set(permutations(iterable)))
+ self.assertEqual(test_output, ref_output)
+
+ # an iterator
+ iterable = iter(['z', 'a', 'a', 'q', 'q', 'q', 'y'])
+ test_output = sorted(mi.distinct_permutations(iterable))
+ # "reload" it
+ iterable = iter(['z', 'a', 'a', 'q', 'q', 'q', 'y'])
+ ref_output = sorted(set(permutations(iterable)))
+ self.assertEqual(test_output, ref_output)
+
+
+class IlenTests(TestCase):
+ def test_ilen(self):
+ """Sanity-checks for ``ilen()``."""
+ # Non-empty
+ self.assertEqual(
+ mi.ilen(filter(lambda x: x % 10 == 0, range(101))), 11
+ )
+
+ # Empty
+ self.assertEqual(mi.ilen((x for x in range(0))), 0)
+
+ # Iterable with __len__
+ self.assertEqual(mi.ilen(list(range(6))), 6)
+
+
+class WithIterTests(TestCase):
+ def test_with_iter(self):
+ s = StringIO('One fish\nTwo fish')
+ initial_words = [line.split()[0] for line in mi.with_iter(s)]
+
+ # Iterable's items should be faithfully represented
+ self.assertEqual(initial_words, ['One', 'Two'])
+ # The file object should be closed
+ self.assertEqual(s.closed, True)
+
+
+class OneTests(TestCase):
+ def test_basic(self):
+ it = iter(['item'])
+ self.assertEqual(mi.one(it), 'item')
+
+ def test_too_short(self):
+ it = iter([])
+ self.assertRaises(ValueError, lambda: mi.one(it))
+ self.assertRaises(IndexError, lambda: mi.one(it, too_short=IndexError))
+
+ def test_too_long(self):
+ it = count()
+ self.assertRaises(ValueError, lambda: mi.one(it)) # burn 0 and 1
+ self.assertEqual(next(it), 2)
+ self.assertRaises(
+ OverflowError, lambda: mi.one(it, too_long=OverflowError)
+ )
+
+
+class IntersperseTest(TestCase):
+ """ Tests for intersperse() """
+
+ def test_even(self):
+ iterable = (x for x in '01')
+ self.assertEqual(
+ list(mi.intersperse(None, iterable)), ['0', None, '1']
+ )
+
+ def test_odd(self):
+ iterable = (x for x in '012')
+ self.assertEqual(
+ list(mi.intersperse(None, iterable)), ['0', None, '1', None, '2']
+ )
+
+ def test_nested(self):
+ element = ('a', 'b')
+ iterable = (x for x in '012')
+ actual = list(mi.intersperse(element, iterable))
+ expected = ['0', ('a', 'b'), '1', ('a', 'b'), '2']
+ self.assertEqual(actual, expected)
+
+ def test_not_iterable(self):
+ self.assertRaises(TypeError, lambda: mi.intersperse('x', 1))
+
+ def test_n(self):
+ for n, element, expected in [
+ (1, '_', ['0', '_', '1', '_', '2', '_', '3', '_', '4', '_', '5']),
+ (2, '_', ['0', '1', '_', '2', '3', '_', '4', '5']),
+ (3, '_', ['0', '1', '2', '_', '3', '4', '5']),
+ (4, '_', ['0', '1', '2', '3', '_', '4', '5']),
+ (5, '_', ['0', '1', '2', '3', '4', '_', '5']),
+ (6, '_', ['0', '1', '2', '3', '4', '5']),
+ (7, '_', ['0', '1', '2', '3', '4', '5']),
+ (3, ['a', 'b'], ['0', '1', '2', ['a', 'b'], '3', '4', '5']),
+ ]:
+ iterable = (x for x in '012345')
+ actual = list(mi.intersperse(element, iterable, n=n))
+ self.assertEqual(actual, expected)
+
+ def test_n_zero(self):
+ self.assertRaises(
+ ValueError, lambda: list(mi.intersperse('x', '012', n=0))
+ )
+
+
+class UniqueToEachTests(TestCase):
+ """Tests for ``unique_to_each()``"""
+
+ def test_all_unique(self):
+ """When all the input iterables are unique the output should match
+ the input."""
+ iterables = [[1, 2], [3, 4, 5], [6, 7, 8]]
+ self.assertEqual(mi.unique_to_each(*iterables), iterables)
+
+ def test_duplicates(self):
+ """When there are duplicates in any of the input iterables that aren't
+ in the rest, those duplicates should be emitted."""
+ iterables = ["mississippi", "missouri"]
+ self.assertEqual(
+ mi.unique_to_each(*iterables), [['p', 'p'], ['o', 'u', 'r']]
+ )
+
+ def test_mixed(self):
+ """When the input iterables contain different types the function should
+ still behave properly"""
+ iterables = ['x', (i for i in range(3)), [1, 2, 3], tuple()]
+ self.assertEqual(mi.unique_to_each(*iterables), [['x'], [0], [3], []])
+
+
+class WindowedTests(TestCase):
+ """Tests for ``windowed()``"""
+
+ def test_basic(self):
+ actual = list(mi.windowed([1, 2, 3, 4, 5], 3))
+ expected = [(1, 2, 3), (2, 3, 4), (3, 4, 5)]
+ self.assertEqual(actual, expected)
+
+ def test_large_size(self):
+ """
+ When the window size is larger than the iterable, and no fill value is
+ given,``None`` should be filled in.
+ """
+ actual = list(mi.windowed([1, 2, 3, 4, 5], 6))
+ expected = [(1, 2, 3, 4, 5, None)]
+ self.assertEqual(actual, expected)
+
+ def test_fillvalue(self):
+ """
+ When sizes don't match evenly, the given fill value should be used.
+ """
+ iterable = [1, 2, 3, 4, 5]
+
+ for n, kwargs, expected in [
+ (6, {}, [(1, 2, 3, 4, 5, '!')]), # n > len(iterable)
+ (3, {'step': 3}, [(1, 2, 3), (4, 5, '!')]), # using ``step``
+ ]:
+ actual = list(mi.windowed(iterable, n, fillvalue='!', **kwargs))
+ self.assertEqual(actual, expected)
+
+ def test_zero(self):
+ """When the window size is zero, an empty tuple should be emitted."""
+ actual = list(mi.windowed([1, 2, 3, 4, 5], 0))
+ expected = [tuple()]
+ self.assertEqual(actual, expected)
+
+ def test_negative(self):
+ """When the window size is negative, ValueError should be raised."""
+ with self.assertRaises(ValueError):
+ list(mi.windowed([1, 2, 3, 4, 5], -1))
+
+ def test_step(self):
+ """The window should advance by the number of steps provided"""
+ iterable = [1, 2, 3, 4, 5, 6, 7]
+ for n, step, expected in [
+ (3, 2, [(1, 2, 3), (3, 4, 5), (5, 6, 7)]), # n > step
+ (3, 3, [(1, 2, 3), (4, 5, 6), (7, None, None)]), # n == step
+ (3, 4, [(1, 2, 3), (5, 6, 7)]), # line up nicely
+ (3, 5, [(1, 2, 3), (6, 7, None)]), # off by one
+ (3, 6, [(1, 2, 3), (7, None, None)]), # off by two
+ (3, 7, [(1, 2, 3)]), # step past the end
+ (7, 8, [(1, 2, 3, 4, 5, 6, 7)]), # step > len(iterable)
+ ]:
+ actual = list(mi.windowed(iterable, n, step=step))
+ self.assertEqual(actual, expected)
+
+ # Step must be greater than or equal to 1
+ with self.assertRaises(ValueError):
+ list(mi.windowed(iterable, 3, step=0))
+
+
+class BucketTests(TestCase):
+ """Tests for ``bucket()``"""
+
+ def test_basic(self):
+ iterable = [10, 20, 30, 11, 21, 31, 12, 22, 23, 33]
+ D = mi.bucket(iterable, key=lambda x: 10 * (x // 10))
+
+ # In-order access
+ self.assertEqual(list(D[10]), [10, 11, 12])
+
+ # Out of order access
+ self.assertEqual(list(D[30]), [30, 31, 33])
+ self.assertEqual(list(D[20]), [20, 21, 22, 23])
+
+ self.assertEqual(list(D[40]), []) # Nothing in here!
+
+ def test_in(self):
+ iterable = [10, 20, 30, 11, 21, 31, 12, 22, 23, 33]
+ D = mi.bucket(iterable, key=lambda x: 10 * (x // 10))
+
+ self.assertTrue(10 in D)
+ self.assertFalse(40 in D)
+ self.assertTrue(20 in D)
+ self.assertFalse(21 in D)
+
+ # Checking in-ness shouldn't advance the iterator
+ self.assertEqual(next(D[10]), 10)
+
+ def test_validator(self):
+ iterable = count(0)
+ key = lambda x: int(str(x)[0]) # First digit of each number
+ validator = lambda x: 0 < x < 10 # No leading zeros
+ D = mi.bucket(iterable, key, validator=validator)
+ self.assertEqual(mi.take(3, D[1]), [1, 10, 11])
+ self.assertNotIn(0, D) # Non-valid entries don't return True
+ self.assertNotIn(0, D._cache) # Don't store non-valid entries
+ self.assertEqual(list(D[0]), [])
+
+
+class SpyTests(TestCase):
+ """Tests for ``spy()``"""
+
+ def test_basic(self):
+ original_iterable = iter('abcdefg')
+ head, new_iterable = mi.spy(original_iterable)
+ self.assertEqual(head, ['a'])
+ self.assertEqual(
+ list(new_iterable), ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+ )
+
+ def test_unpacking(self):
+ original_iterable = iter('abcdefg')
+ (first, second, third), new_iterable = mi.spy(original_iterable, 3)
+ self.assertEqual(first, 'a')
+ self.assertEqual(second, 'b')
+ self.assertEqual(third, 'c')
+ self.assertEqual(
+ list(new_iterable), ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+ )
+
+ def test_too_many(self):
+ original_iterable = iter('abc')
+ head, new_iterable = mi.spy(original_iterable, 4)
+ self.assertEqual(head, ['a', 'b', 'c'])
+ self.assertEqual(list(new_iterable), ['a', 'b', 'c'])
+
+ def test_zero(self):
+ original_iterable = iter('abc')
+ head, new_iterable = mi.spy(original_iterable, 0)
+ self.assertEqual(head, [])
+ self.assertEqual(list(new_iterable), ['a', 'b', 'c'])
+
+
+class InterleaveTests(TestCase):
+ def test_even(self):
+ actual = list(mi.interleave([1, 4, 7], [2, 5, 8], [3, 6, 9]))
+ expected = [1, 2, 3, 4, 5, 6, 7, 8, 9]
+ self.assertEqual(actual, expected)
+
+ def test_short(self):
+ actual = list(mi.interleave([1, 4], [2, 5, 7], [3, 6, 8]))
+ expected = [1, 2, 3, 4, 5, 6]
+ self.assertEqual(actual, expected)
+
+ def test_mixed_types(self):
+ it_list = ['a', 'b', 'c', 'd']
+ it_str = '12345'
+ it_inf = count()
+ actual = list(mi.interleave(it_list, it_str, it_inf))
+ expected = ['a', '1', 0, 'b', '2', 1, 'c', '3', 2, 'd', '4', 3]
+ self.assertEqual(actual, expected)
+
+
+class InterleaveLongestTests(TestCase):
+ def test_even(self):
+ actual = list(mi.interleave_longest([1, 4, 7], [2, 5, 8], [3, 6, 9]))
+ expected = [1, 2, 3, 4, 5, 6, 7, 8, 9]
+ self.assertEqual(actual, expected)
+
+ def test_short(self):
+ actual = list(mi.interleave_longest([1, 4], [2, 5, 7], [3, 6, 8]))
+ expected = [1, 2, 3, 4, 5, 6, 7, 8]
+ self.assertEqual(actual, expected)
+
+ def test_mixed_types(self):
+ it_list = ['a', 'b', 'c', 'd']
+ it_str = '12345'
+ it_gen = (x for x in range(3))
+ actual = list(mi.interleave_longest(it_list, it_str, it_gen))
+ expected = ['a', '1', 0, 'b', '2', 1, 'c', '3', 2, 'd', '4', '5']
+ self.assertEqual(actual, expected)
+
+
+class TestCollapse(TestCase):
+ """Tests for ``collapse()``"""
+
+ def test_collapse(self):
+ l = [[1], 2, [[3], 4], [[[5]]]]
+ self.assertEqual(list(mi.collapse(l)), [1, 2, 3, 4, 5])
+
+ def test_collapse_to_string(self):
+ l = [["s1"], "s2", [["s3"], "s4"], [[["s5"]]]]
+ self.assertEqual(list(mi.collapse(l)), ["s1", "s2", "s3", "s4", "s5"])
+
+ def test_collapse_flatten(self):
+ l = [[1], [2], [[3], 4], [[[5]]]]
+ self.assertEqual(list(mi.collapse(l, levels=1)), list(mi.flatten(l)))
+
+ def test_collapse_to_level(self):
+ l = [[1], 2, [[3], 4], [[[5]]]]
+ self.assertEqual(list(mi.collapse(l, levels=2)), [1, 2, 3, 4, [5]])
+ self.assertEqual(
+ list(mi.collapse(mi.collapse(l, levels=1), levels=1)),
+ list(mi.collapse(l, levels=2))
+ )
+
+ def test_collapse_to_list(self):
+ l = (1, [2], (3, [4, (5,)], 'ab'))
+ actual = list(mi.collapse(l, base_type=list))
+ expected = [1, [2], 3, [4, (5,)], 'ab']
+ self.assertEqual(actual, expected)
+
+
+class SideEffectTests(TestCase):
+ """Tests for ``side_effect()``"""
+
+ def test_individual(self):
+ # The function increments the counter for each call
+ counter = [0]
+
+ def func(arg):
+ counter[0] += 1
+
+ result = list(mi.side_effect(func, range(10)))
+ self.assertEqual(result, list(range(10)))
+ self.assertEqual(counter[0], 10)
+
+ def test_chunked(self):
+ # The function increments the counter for each call
+ counter = [0]
+
+ def func(arg):
+ counter[0] += 1
+
+ result = list(mi.side_effect(func, range(10), 2))
+ self.assertEqual(result, list(range(10)))
+ self.assertEqual(counter[0], 5)
+
+ def test_before_after(self):
+ f = StringIO()
+ collector = []
+
+ def func(item):
+ print(item, file=f)
+ collector.append(f.getvalue())
+
+ def it():
+ yield u'a'
+ yield u'b'
+ raise RuntimeError('kaboom')
+
+ before = lambda: print('HEADER', file=f)
+ after = f.close
+
+ try:
+ mi.consume(mi.side_effect(func, it(), before=before, after=after))
+ except RuntimeError:
+ pass
+
+ # The iterable should have been written to the file
+ self.assertEqual(collector, [u'HEADER\na\n', u'HEADER\na\nb\n'])
+
+ # The file should be closed even though something bad happened
+ self.assertTrue(f.closed)
+
+ def test_before_fails(self):
+ f = StringIO()
+ func = lambda x: print(x, file=f)
+
+ def before():
+ raise RuntimeError('ouch')
+
+ try:
+ mi.consume(
+ mi.side_effect(func, u'abc', before=before, after=f.close)
+ )
+ except RuntimeError:
+ pass
+
+ # The file should be closed even though something bad happened in the
+ # before function
+ self.assertTrue(f.closed)
+
+
+class SlicedTests(TestCase):
+ """Tests for ``sliced()``"""
+
+ def test_even(self):
+ """Test when the length of the sequence is divisible by *n*"""
+ seq = 'ABCDEFGHI'
+ self.assertEqual(list(mi.sliced(seq, 3)), ['ABC', 'DEF', 'GHI'])
+
+ def test_odd(self):
+ """Test when the length of the sequence is not divisible by *n*"""
+ seq = 'ABCDEFGHI'
+ self.assertEqual(list(mi.sliced(seq, 4)), ['ABCD', 'EFGH', 'I'])
+
+ def test_not_sliceable(self):
+ seq = (x for x in 'ABCDEFGHI')
+
+ with self.assertRaises(TypeError):
+ list(mi.sliced(seq, 3))
+
+
+class SplitAtTests(TestCase):
+ """Tests for ``split()``"""
+
+ def comp_with_str_split(self, str_to_split, delim):
+ pred = lambda c: c == delim
+ actual = list(map(''.join, mi.split_at(str_to_split, pred)))
+ expected = str_to_split.split(delim)
+ self.assertEqual(actual, expected)
+
+ def test_seperators(self):
+ test_strs = ['', 'abcba', 'aaabbbcccddd', 'e']
+ for s, delim in product(test_strs, 'abcd'):
+ self.comp_with_str_split(s, delim)
+
+
+class SplitBeforeTest(TestCase):
+ """Tests for ``split_before()``"""
+
+ def test_starts_with_sep(self):
+ actual = list(mi.split_before('xooxoo', lambda c: c == 'x'))
+ expected = [['x', 'o', 'o'], ['x', 'o', 'o']]
+ self.assertEqual(actual, expected)
+
+ def test_ends_with_sep(self):
+ actual = list(mi.split_before('ooxoox', lambda c: c == 'x'))
+ expected = [['o', 'o'], ['x', 'o', 'o'], ['x']]
+ self.assertEqual(actual, expected)
+
+ def test_no_sep(self):
+ actual = list(mi.split_before('ooo', lambda c: c == 'x'))
+ expected = [['o', 'o', 'o']]
+ self.assertEqual(actual, expected)
+
+
+class SplitAfterTest(TestCase):
+ """Tests for ``split_after()``"""
+
+ def test_starts_with_sep(self):
+ actual = list(mi.split_after('xooxoo', lambda c: c == 'x'))
+ expected = [['x'], ['o', 'o', 'x'], ['o', 'o']]
+ self.assertEqual(actual, expected)
+
+ def test_ends_with_sep(self):
+ actual = list(mi.split_after('ooxoox', lambda c: c == 'x'))
+ expected = [['o', 'o', 'x'], ['o', 'o', 'x']]
+ self.assertEqual(actual, expected)
+
+ def test_no_sep(self):
+ actual = list(mi.split_after('ooo', lambda c: c == 'x'))
+ expected = [['o', 'o', 'o']]
+ self.assertEqual(actual, expected)
+
+
+class PaddedTest(TestCase):
+ """Tests for ``padded()``"""
+
+ def test_no_n(self):
+ seq = [1, 2, 3]
+
+ # No fillvalue
+ self.assertEqual(mi.take(5, mi.padded(seq)), [1, 2, 3, None, None])
+
+ # With fillvalue
+ self.assertEqual(
+ mi.take(5, mi.padded(seq, fillvalue='')), [1, 2, 3, '', '']
+ )
+
+ def test_invalid_n(self):
+ self.assertRaises(ValueError, lambda: list(mi.padded([1, 2, 3], n=-1)))
+ self.assertRaises(ValueError, lambda: list(mi.padded([1, 2, 3], n=0)))
+
+ def test_valid_n(self):
+ seq = [1, 2, 3, 4, 5]
+
+ # No need for padding: len(seq) <= n
+ self.assertEqual(list(mi.padded(seq, n=4)), [1, 2, 3, 4, 5])
+ self.assertEqual(list(mi.padded(seq, n=5)), [1, 2, 3, 4, 5])
+
+ # No fillvalue
+ self.assertEqual(
+ list(mi.padded(seq, n=7)), [1, 2, 3, 4, 5, None, None]
+ )
+
+ # With fillvalue
+ self.assertEqual(
+ list(mi.padded(seq, fillvalue='', n=7)), [1, 2, 3, 4, 5, '', '']
+ )
+
+ def test_next_multiple(self):
+ seq = [1, 2, 3, 4, 5, 6]
+
+ # No need for padding: len(seq) % n == 0
+ self.assertEqual(
+ list(mi.padded(seq, n=3, next_multiple=True)), [1, 2, 3, 4, 5, 6]
+ )
+
+ # Padding needed: len(seq) < n
+ self.assertEqual(
+ list(mi.padded(seq, n=8, next_multiple=True)),
+ [1, 2, 3, 4, 5, 6, None, None]
+ )
+
+ # No padding needed: len(seq) == n
+ self.assertEqual(
+ list(mi.padded(seq, n=6, next_multiple=True)), [1, 2, 3, 4, 5, 6]
+ )
+
+ # Padding needed: len(seq) > n
+ self.assertEqual(
+ list(mi.padded(seq, n=4, next_multiple=True)),
+ [1, 2, 3, 4, 5, 6, None, None]
+ )
+
+ # With fillvalue
+ self.assertEqual(
+ list(mi.padded(seq, fillvalue='', n=4, next_multiple=True)),
+ [1, 2, 3, 4, 5, 6, '', '']
+ )
+
+
+class DistributeTest(TestCase):
+ """Tests for distribute()"""
+
+ def test_invalid_n(self):
+ self.assertRaises(ValueError, lambda: mi.distribute(-1, [1, 2, 3]))
+ self.assertRaises(ValueError, lambda: mi.distribute(0, [1, 2, 3]))
+
+ def test_basic(self):
+ iterable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+
+ for n, expected in [
+ (1, [iterable]),
+ (2, [[1, 3, 5, 7, 9], [2, 4, 6, 8, 10]]),
+ (3, [[1, 4, 7, 10], [2, 5, 8], [3, 6, 9]]),
+ (10, [[n] for n in range(1, 10 + 1)]),
+ ]:
+ self.assertEqual(
+ [list(x) for x in mi.distribute(n, iterable)], expected
+ )
+
+ def test_large_n(self):
+ iterable = [1, 2, 3, 4]
+ self.assertEqual(
+ [list(x) for x in mi.distribute(6, iterable)],
+ [[1], [2], [3], [4], [], []]
+ )
+
+
+class StaggerTest(TestCase):
+ """Tests for ``stagger()``"""
+
+ def test_default(self):
+ iterable = [0, 1, 2, 3]
+ actual = list(mi.stagger(iterable))
+ expected = [(None, 0, 1), (0, 1, 2), (1, 2, 3)]
+ self.assertEqual(actual, expected)
+
+ def test_offsets(self):
+ iterable = [0, 1, 2, 3]
+ for offsets, expected in [
+ ((-2, 0, 2), [('', 0, 2), ('', 1, 3)]),
+ ((-2, -1), [('', ''), ('', 0), (0, 1), (1, 2), (2, 3)]),
+ ((1, 2), [(1, 2), (2, 3)]),
+ ]:
+ all_groups = mi.stagger(iterable, offsets=offsets, fillvalue='')
+ self.assertEqual(list(all_groups), expected)
+
+ def test_longest(self):
+ iterable = [0, 1, 2, 3]
+ for offsets, expected in [
+ (
+ (-1, 0, 1),
+ [('', 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, ''), (3, '', '')]
+ ),
+ ((-2, -1), [('', ''), ('', 0), (0, 1), (1, 2), (2, 3), (3, '')]),
+ ((1, 2), [(1, 2), (2, 3), (3, '')]),
+ ]:
+ all_groups = mi.stagger(
+ iterable, offsets=offsets, fillvalue='', longest=True
+ )
+ self.assertEqual(list(all_groups), expected)
+
+
+class ZipOffsetTest(TestCase):
+ """Tests for ``zip_offset()``"""
+
+ def test_shortest(self):
+ a_1 = [0, 1, 2, 3]
+ a_2 = [0, 1, 2, 3, 4, 5]
+ a_3 = [0, 1, 2, 3, 4, 5, 6, 7]
+ actual = list(
+ mi.zip_offset(a_1, a_2, a_3, offsets=(-1, 0, 1), fillvalue='')
+ )
+ expected = [('', 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, 4), (3, 4, 5)]
+ self.assertEqual(actual, expected)
+
+ def test_longest(self):
+ a_1 = [0, 1, 2, 3]
+ a_2 = [0, 1, 2, 3, 4, 5]
+ a_3 = [0, 1, 2, 3, 4, 5, 6, 7]
+ actual = list(
+ mi.zip_offset(a_1, a_2, a_3, offsets=(-1, 0, 1), longest=True)
+ )
+ expected = [
+ (None, 0, 1),
+ (0, 1, 2),
+ (1, 2, 3),
+ (2, 3, 4),
+ (3, 4, 5),
+ (None, 5, 6),
+ (None, None, 7),
+ ]
+ self.assertEqual(actual, expected)
+
+ def test_mismatch(self):
+ iterables = [0, 1, 2], [2, 3, 4]
+ offsets = (-1, 0, 1)
+ self.assertRaises(
+ ValueError,
+ lambda: list(mi.zip_offset(*iterables, offsets=offsets))
+ )
+
+
+class SortTogetherTest(TestCase):
+ """Tests for sort_together()"""
+
+ def test_key_list(self):
+ """tests `key_list` including default, iterables include duplicates"""
+ iterables = [
+ ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'],
+ ['May', 'Aug.', 'May', 'June', 'July', 'July'],
+ [97, 20, 100, 70, 100, 20]
+ ]
+
+ self.assertEqual(
+ mi.sort_together(iterables),
+ [
+ ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'),
+ ('June', 'July', 'July', 'May', 'Aug.', 'May'),
+ (70, 100, 20, 97, 20, 100)
+ ]
+ )
+
+ self.assertEqual(
+ mi.sort_together(iterables, key_list=(0, 1)),
+ [
+ ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'),
+ ('July', 'July', 'June', 'Aug.', 'May', 'May'),
+ (100, 20, 70, 20, 97, 100)
+ ]
+ )
+
+ self.assertEqual(
+ mi.sort_together(iterables, key_list=(0, 1, 2)),
+ [
+ ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'),
+ ('July', 'July', 'June', 'Aug.', 'May', 'May'),
+ (20, 100, 70, 20, 97, 100)
+ ]
+ )
+
+ self.assertEqual(
+ mi.sort_together(iterables, key_list=(2,)),
+ [
+ ('GA', 'CT', 'CT', 'GA', 'GA', 'CT'),
+ ('Aug.', 'July', 'June', 'May', 'May', 'July'),
+ (20, 20, 70, 97, 100, 100)
+ ]
+ )
+
+ def test_invalid_key_list(self):
+ """tests `key_list` for indexes not available in `iterables`"""
+ iterables = [
+ ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'],
+ ['May', 'Aug.', 'May', 'June', 'July', 'July'],
+ [97, 20, 100, 70, 100, 20]
+ ]
+
+ self.assertRaises(
+ IndexError, lambda: mi.sort_together(iterables, key_list=(5,))
+ )
+
+ def test_reverse(self):
+ """tests `reverse` to ensure a reverse sort for `key_list` iterables"""
+ iterables = [
+ ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'],
+ ['May', 'Aug.', 'May', 'June', 'July', 'July'],
+ [97, 20, 100, 70, 100, 20]
+ ]
+
+ self.assertEqual(
+ mi.sort_together(iterables, key_list=(0, 1, 2), reverse=True),
+ [('GA', 'GA', 'GA', 'CT', 'CT', 'CT'),
+ ('May', 'May', 'Aug.', 'June', 'July', 'July'),
+ (100, 97, 20, 70, 100, 20)]
+ )
+
+ def test_uneven_iterables(self):
+ """tests trimming of iterables to the shortest length before sorting"""
+ iterables = [['GA', 'GA', 'GA', 'CT', 'CT', 'CT', 'MA'],
+ ['May', 'Aug.', 'May', 'June', 'July', 'July'],
+ [97, 20, 100, 70, 100, 20, 0]]
+
+ self.assertEqual(
+ mi.sort_together(iterables),
+ [
+ ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'),
+ ('June', 'July', 'July', 'May', 'Aug.', 'May'),
+ (70, 100, 20, 97, 20, 100)
+ ]
+ )
+
+
+class DivideTest(TestCase):
+ """Tests for divide()"""
+
+ def test_invalid_n(self):
+ self.assertRaises(ValueError, lambda: mi.divide(-1, [1, 2, 3]))
+ self.assertRaises(ValueError, lambda: mi.divide(0, [1, 2, 3]))
+
+ def test_basic(self):
+ iterable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+
+ for n, expected in [
+ (1, [iterable]),
+ (2, [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]),
+ (3, [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]]),
+ (10, [[n] for n in range(1, 10 + 1)]),
+ ]:
+ self.assertEqual(
+ [list(x) for x in mi.divide(n, iterable)], expected
+ )
+
+ def test_large_n(self):
+ iterable = [1, 2, 3, 4]
+ self.assertEqual(
+ [list(x) for x in mi.divide(6, iterable)],
+ [[1], [2], [3], [4], [], []]
+ )
+
+
+class TestAlwaysIterable(TestCase):
+ """Tests for always_iterable()"""
+ def test_single(self):
+ self.assertEqual(list(mi.always_iterable(1)), [1])
+
+ def test_strings(self):
+ for obj in ['foo', b'bar', u'baz']:
+ actual = list(mi.always_iterable(obj))
+ expected = [obj]
+ self.assertEqual(actual, expected)
+
+ def test_base_type(self):
+ dict_obj = {'a': 1, 'b': 2}
+ str_obj = '123'
+
+ # Default: dicts are iterable like they normally are
+ default_actual = list(mi.always_iterable(dict_obj))
+ default_expected = list(dict_obj)
+ self.assertEqual(default_actual, default_expected)
+
+ # Unitary types set: dicts are not iterable
+ custom_actual = list(mi.always_iterable(dict_obj, base_type=dict))
+ custom_expected = [dict_obj]
+ self.assertEqual(custom_actual, custom_expected)
+
+ # With unitary types set, strings are iterable
+ str_actual = list(mi.always_iterable(str_obj, base_type=None))
+ str_expected = list(str_obj)
+ self.assertEqual(str_actual, str_expected)
+
+ def test_iterables(self):
+ self.assertEqual(list(mi.always_iterable([0, 1])), [0, 1])
+ self.assertEqual(
+ list(mi.always_iterable([0, 1], base_type=list)), [[0, 1]]
+ )
+ self.assertEqual(
+ list(mi.always_iterable(iter('foo'))), ['f', 'o', 'o']
+ )
+ self.assertEqual(list(mi.always_iterable([])), [])
+
+ def test_none(self):
+ self.assertEqual(list(mi.always_iterable(None)), [])
+
+ def test_generator(self):
+ def _gen():
+ yield 0
+ yield 1
+
+ self.assertEqual(list(mi.always_iterable(_gen())), [0, 1])
+
+
+class AdjacentTests(TestCase):
+ def test_typical(self):
+ actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10)))
+ expected = [(True, 0), (True, 1), (False, 2), (False, 3), (True, 4),
+ (True, 5), (True, 6), (False, 7), (False, 8), (False, 9)]
+ self.assertEqual(actual, expected)
+
+ def test_empty_iterable(self):
+ actual = list(mi.adjacent(lambda x: x % 5 == 0, []))
+ expected = []
+ self.assertEqual(actual, expected)
+
+ def test_length_one(self):
+ actual = list(mi.adjacent(lambda x: x % 5 == 0, [0]))
+ expected = [(True, 0)]
+ self.assertEqual(actual, expected)
+
+ actual = list(mi.adjacent(lambda x: x % 5 == 0, [1]))
+ expected = [(False, 1)]
+ self.assertEqual(actual, expected)
+
+ def test_consecutive_true(self):
+ """Test that when the predicate matches multiple consecutive elements
+ it doesn't repeat elements in the output"""
+ actual = list(mi.adjacent(lambda x: x % 5 < 2, range(10)))
+ expected = [(True, 0), (True, 1), (True, 2), (False, 3), (True, 4),
+ (True, 5), (True, 6), (True, 7), (False, 8), (False, 9)]
+ self.assertEqual(actual, expected)
+
+ def test_distance(self):
+ actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10), distance=2))
+ expected = [(True, 0), (True, 1), (True, 2), (True, 3), (True, 4),
+ (True, 5), (True, 6), (True, 7), (False, 8), (False, 9)]
+ self.assertEqual(actual, expected)
+
+ actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10), distance=3))
+ expected = [(True, 0), (True, 1), (True, 2), (True, 3), (True, 4),
+ (True, 5), (True, 6), (True, 7), (True, 8), (False, 9)]
+ self.assertEqual(actual, expected)
+
+ def test_large_distance(self):
+ """Test distance larger than the length of the iterable"""
+ iterable = range(10)
+ actual = list(mi.adjacent(lambda x: x % 5 == 4, iterable, distance=20))
+ expected = list(zip(repeat(True), iterable))
+ self.assertEqual(actual, expected)
+
+ actual = list(mi.adjacent(lambda x: False, iterable, distance=20))
+ expected = list(zip(repeat(False), iterable))
+ self.assertEqual(actual, expected)
+
+ def test_zero_distance(self):
+ """Test that adjacent() reduces to zip+map when distance is 0"""
+ iterable = range(1000)
+ predicate = lambda x: x % 4 == 2
+ actual = mi.adjacent(predicate, iterable, 0)
+ expected = zip(map(predicate, iterable), iterable)
+ self.assertTrue(all(a == e for a, e in zip(actual, expected)))
+
+ def test_negative_distance(self):
+ """Test that adjacent() raises an error with negative distance"""
+ pred = lambda x: x
+ self.assertRaises(
+ ValueError, lambda: mi.adjacent(pred, range(1000), -1)
+ )
+ self.assertRaises(
+ ValueError, lambda: mi.adjacent(pred, range(10), -10)
+ )
+
+ def test_grouping(self):
+ """Test interaction of adjacent() with groupby_transform()"""
+ iterable = mi.adjacent(lambda x: x % 5 == 0, range(10))
+ grouper = mi.groupby_transform(iterable, itemgetter(0), itemgetter(1))
+ actual = [(k, list(g)) for k, g in grouper]
+ expected = [
+ (True, [0, 1]),
+ (False, [2, 3]),
+ (True, [4, 5, 6]),
+ (False, [7, 8, 9]),
+ ]
+ self.assertEqual(actual, expected)
+
+ def test_call_once(self):
+ """Test that the predicate is only called once per item."""
+ already_seen = set()
+ iterable = range(10)
+
+ def predicate(item):
+ self.assertNotIn(item, already_seen)
+ already_seen.add(item)
+ return True
+
+ actual = list(mi.adjacent(predicate, iterable))
+ expected = [(True, x) for x in iterable]
+ self.assertEqual(actual, expected)
+
+
+class GroupByTransformTests(TestCase):
+ def assertAllGroupsEqual(self, groupby1, groupby2):
+ """Compare two groupby objects for equality, both keys and groups."""
+ for a, b in zip(groupby1, groupby2):
+ key1, group1 = a
+ key2, group2 = b
+ self.assertEqual(key1, key2)
+ self.assertListEqual(list(group1), list(group2))
+ self.assertRaises(StopIteration, lambda: next(groupby1))
+ self.assertRaises(StopIteration, lambda: next(groupby2))
+
+ def test_default_funcs(self):
+ """Test that groupby_transform() with default args mimics groupby()"""
+ iterable = [(x // 5, x) for x in range(1000)]
+ actual = mi.groupby_transform(iterable)
+ expected = groupby(iterable)
+ self.assertAllGroupsEqual(actual, expected)
+
+ def test_valuefunc(self):
+ iterable = [(int(x / 5), int(x / 3), x) for x in range(10)]
+
+ # Test the standard usage of grouping one iterable using another's keys
+ grouper = mi.groupby_transform(
+ iterable, keyfunc=itemgetter(0), valuefunc=itemgetter(-1)
+ )
+ actual = [(k, list(g)) for k, g in grouper]
+ expected = [(0, [0, 1, 2, 3, 4]), (1, [5, 6, 7, 8, 9])]
+ self.assertEqual(actual, expected)
+
+ grouper = mi.groupby_transform(
+ iterable, keyfunc=itemgetter(1), valuefunc=itemgetter(-1)
+ )
+ actual = [(k, list(g)) for k, g in grouper]
+ expected = [(0, [0, 1, 2]), (1, [3, 4, 5]), (2, [6, 7, 8]), (3, [9])]
+ self.assertEqual(actual, expected)
+
+ # and now for something a little different
+ d = dict(zip(range(10), 'abcdefghij'))
+ grouper = mi.groupby_transform(
+ range(10), keyfunc=lambda x: x // 5, valuefunc=d.get
+ )
+ actual = [(k, ''.join(g)) for k, g in grouper]
+ expected = [(0, 'abcde'), (1, 'fghij')]
+ self.assertEqual(actual, expected)
+
+ def test_no_valuefunc(self):
+ iterable = range(1000)
+
+ def key(x):
+ return x // 5
+
+ actual = mi.groupby_transform(iterable, key, valuefunc=None)
+ expected = groupby(iterable, key)
+ self.assertAllGroupsEqual(actual, expected)
+
+ actual = mi.groupby_transform(iterable, key) # default valuefunc
+ expected = groupby(iterable, key)
+ self.assertAllGroupsEqual(actual, expected)
+
+
+class NumericRangeTests(TestCase):
+ def test_basic(self):
+ for args, expected in [
+ ((4,), [0, 1, 2, 3]),
+ ((4.0,), [0.0, 1.0, 2.0, 3.0]),
+ ((1.0, 4), [1.0, 2.0, 3.0]),
+ ((1, 4.0), [1, 2, 3]),
+ ((1.0, 5), [1.0, 2.0, 3.0, 4.0]),
+ ((0, 20, 5), [0, 5, 10, 15]),
+ ((0, 20, 5.0), [0.0, 5.0, 10.0, 15.0]),
+ ((0, 10, 3), [0, 3, 6, 9]),
+ ((0, 10, 3.0), [0.0, 3.0, 6.0, 9.0]),
+ ((0, -5, -1), [0, -1, -2, -3, -4]),
+ ((0.0, -5, -1), [0.0, -1.0, -2.0, -3.0, -4.0]),
+ ((1, 2, Fraction(1, 2)), [Fraction(1, 1), Fraction(3, 2)]),
+ ((0,), []),
+ ((0.0,), []),
+ ((1, 0), []),
+ ((1.0, 0.0), []),
+ ((Fraction(2, 1),), [Fraction(0, 1), Fraction(1, 1)]),
+ ((Decimal('2.0'),), [Decimal('0.0'), Decimal('1.0')]),
+ ]:
+ actual = list(mi.numeric_range(*args))
+ self.assertEqual(actual, expected)
+ self.assertTrue(
+ all(type(a) == type(e) for a, e in zip(actual, expected))
+ )
+
+ def test_arg_count(self):
+ self.assertRaises(TypeError, lambda: list(mi.numeric_range()))
+ self.assertRaises(
+ TypeError, lambda: list(mi.numeric_range(0, 1, 2, 3))
+ )
+
+ def test_zero_step(self):
+ self.assertRaises(
+ ValueError, lambda: list(mi.numeric_range(1, 2, 0))
+ )
+
+
+class CountCycleTests(TestCase):
+ def test_basic(self):
+ expected = [
+ (0, 'a'), (0, 'b'), (0, 'c'),
+ (1, 'a'), (1, 'b'), (1, 'c'),
+ (2, 'a'), (2, 'b'), (2, 'c'),
+ ]
+ for actual in [
+ mi.take(9, mi.count_cycle('abc')), # n=None
+ list(mi.count_cycle('abc', 3)), # n=3
+ ]:
+ self.assertEqual(actual, expected)
+
+ def test_empty(self):
+ self.assertEqual(list(mi.count_cycle('')), [])
+ self.assertEqual(list(mi.count_cycle('', 2)), [])
+
+ def test_negative(self):
+ self.assertEqual(list(mi.count_cycle('abc', -3)), [])
+
+
+class LocateTests(TestCase):
+ def test_default_pred(self):
+ iterable = [0, 1, 1, 0, 1, 0, 0]
+ actual = list(mi.locate(iterable))
+ expected = [1, 2, 4]
+ self.assertEqual(actual, expected)
+
+ def test_no_matches(self):
+ iterable = [0, 0, 0]
+ actual = list(mi.locate(iterable))
+ expected = []
+ self.assertEqual(actual, expected)
+
+ def test_custom_pred(self):
+ iterable = ['0', 1, 1, '0', 1, '0', '0']
+ pred = lambda x: x == '0'
+ actual = list(mi.locate(iterable, pred))
+ expected = [0, 3, 5, 6]
+ self.assertEqual(actual, expected)
+
+
+class StripFunctionTests(TestCase):
+ def test_hashable(self):
+ iterable = list('www.example.com')
+ pred = lambda x: x in set('cmowz.')
+
+ self.assertEqual(list(mi.lstrip(iterable, pred)), list('example.com'))
+ self.assertEqual(list(mi.rstrip(iterable, pred)), list('www.example'))
+ self.assertEqual(list(mi.strip(iterable, pred)), list('example'))
+
+ def test_not_hashable(self):
+ iterable = [
+ list('http://'), list('www'), list('.example'), list('.com')
+ ]
+ pred = lambda x: x in [list('http://'), list('www'), list('.com')]
+
+ self.assertEqual(list(mi.lstrip(iterable, pred)), iterable[2:])
+ self.assertEqual(list(mi.rstrip(iterable, pred)), iterable[:3])
+ self.assertEqual(list(mi.strip(iterable, pred)), iterable[2: 3])
+
+ def test_math(self):
+ iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2]
+ pred = lambda x: x <= 2
+
+ self.assertEqual(list(mi.lstrip(iterable, pred)), iterable[3:])
+ self.assertEqual(list(mi.rstrip(iterable, pred)), iterable[:-3])
+ self.assertEqual(list(mi.strip(iterable, pred)), iterable[3:-3])
+
+
+class IsliceExtendedTests(TestCase):
+ def test_all(self):
+ iterable = ['0', '1', '2', '3', '4', '5']
+ indexes = list(range(-4, len(iterable) + 4)) + [None]
+ steps = [1, 2, 3, 4, -1, -2, -3, 4]
+ for slice_args in product(indexes, indexes, steps):
+ try:
+ actual = list(mi.islice_extended(iterable, *slice_args))
+ except Exception as e:
+ self.fail((slice_args, e))
+
+ expected = iterable[slice(*slice_args)]
+ self.assertEqual(actual, expected, slice_args)
+
+ def test_zero_step(self):
+ with self.assertRaises(ValueError):
+ list(mi.islice_extended([1, 2, 3], 0, 1, 0))
+
+
+class ConsecutiveGroupsTest(TestCase):
+ def test_numbers(self):
+ iterable = [-10, -8, -7, -6, 1, 2, 4, 5, -1, 7]
+ actual = [list(g) for g in mi.consecutive_groups(iterable)]
+ expected = [[-10], [-8, -7, -6], [1, 2], [4, 5], [-1], [7]]
+ self.assertEqual(actual, expected)
+
+ def test_custom_ordering(self):
+ iterable = ['1', '10', '11', '20', '21', '22', '30', '31']
+ ordering = lambda x: int(x)
+ actual = [list(g) for g in mi.consecutive_groups(iterable, ordering)]
+ expected = [['1'], ['10', '11'], ['20', '21', '22'], ['30', '31']]
+ self.assertEqual(actual, expected)
+
+ def test_exotic_ordering(self):
+ iterable = [
+ ('a', 'b', 'c', 'd'),
+ ('a', 'c', 'b', 'd'),
+ ('a', 'c', 'd', 'b'),
+ ('a', 'd', 'b', 'c'),
+ ('d', 'b', 'c', 'a'),
+ ('d', 'c', 'a', 'b'),
+ ]
+ ordering = list(permutations('abcd')).index
+ actual = [list(g) for g in mi.consecutive_groups(iterable, ordering)]
+ expected = [
+ [('a', 'b', 'c', 'd')],
+ [('a', 'c', 'b', 'd'), ('a', 'c', 'd', 'b'), ('a', 'd', 'b', 'c')],
+ [('d', 'b', 'c', 'a'), ('d', 'c', 'a', 'b')],
+ ]
+ self.assertEqual(actual, expected)
+
+
+class DifferenceTest(TestCase):
+ def test_normal(self):
+ iterable = [10, 20, 30, 40, 50]
+ actual = list(mi.difference(iterable))
+ expected = [10, 10, 10, 10, 10]
+ self.assertEqual(actual, expected)
+
+ def test_custom(self):
+ iterable = [10, 20, 30, 40, 50]
+ actual = list(mi.difference(iterable, add))
+ expected = [10, 30, 50, 70, 90]
+ self.assertEqual(actual, expected)
+
+ def test_roundtrip(self):
+ original = list(range(100))
+ accumulated = mi.accumulate(original)
+ actual = list(mi.difference(accumulated))
+ self.assertEqual(actual, original)
+
+ def test_one(self):
+ self.assertEqual(list(mi.difference([0])), [0])
+
+ def test_empty(self):
+ self.assertEqual(list(mi.difference([])), [])
+
+
+class SeekableTest(TestCase):
+ def test_exhaustion_reset(self):
+ iterable = [str(n) for n in range(10)]
+
+ s = mi.seekable(iterable)
+ self.assertEqual(list(s), iterable) # Normal iteration
+ self.assertEqual(list(s), []) # Iterable is exhausted
+
+ s.seek(0)
+ self.assertEqual(list(s), iterable) # Back in action
+
+ def test_partial_reset(self):
+ iterable = [str(n) for n in range(10)]
+
+ s = mi.seekable(iterable)
+ self.assertEqual(mi.take(5, s), iterable[:5]) # Normal iteration
+
+ s.seek(1)
+ self.assertEqual(list(s), iterable[1:]) # Get the rest of the iterable
+
+ def test_forward(self):
+ iterable = [str(n) for n in range(10)]
+
+ s = mi.seekable(iterable)
+ self.assertEqual(mi.take(1, s), iterable[:1]) # Normal iteration
+
+ s.seek(3) # Skip over index 2
+ self.assertEqual(list(s), iterable[3:]) # Result is similar to slicing
+
+ s.seek(0) # Back to 0
+ self.assertEqual(list(s), iterable) # No difference in result
+
+ def test_past_end(self):
+ iterable = [str(n) for n in range(10)]
+
+ s = mi.seekable(iterable)
+ self.assertEqual(mi.take(1, s), iterable[:1]) # Normal iteration
+
+ s.seek(20)
+ self.assertEqual(list(s), []) # Iterable is exhausted
+
+ s.seek(0) # Back to 0
+ self.assertEqual(list(s), iterable) # No difference in result
+
+ def test_elements(self):
+ iterable = map(str, count())
+
+ s = mi.seekable(iterable)
+ mi.take(10, s)
+
+ elements = s.elements()
+ self.assertEqual(
+ [elements[i] for i in range(10)], [str(n) for n in range(10)]
+ )
+ self.assertEqual(len(elements), 10)
+
+ mi.take(10, s)
+ self.assertEqual(list(elements), [str(n) for n in range(20)])
+
+
+class SequenceViewTests(TestCase):
+ def test_init(self):
+ view = mi.SequenceView((1, 2, 3))
+ self.assertEqual(repr(view), "SequenceView((1, 2, 3))")
+ self.assertRaises(TypeError, lambda: mi.SequenceView({}))
+
+ def test_update(self):
+ seq = [1, 2, 3]
+ view = mi.SequenceView(seq)
+ self.assertEqual(len(view), 3)
+ self.assertEqual(repr(view), "SequenceView([1, 2, 3])")
+
+ seq.pop()
+ self.assertEqual(len(view), 2)
+ self.assertEqual(repr(view), "SequenceView([1, 2])")
+
+ def test_indexing(self):
+ seq = ('a', 'b', 'c', 'd', 'e', 'f')
+ view = mi.SequenceView(seq)
+ for i in range(-len(seq), len(seq)):
+ self.assertEqual(view[i], seq[i])
+
+ def test_slicing(self):
+ seq = ('a', 'b', 'c', 'd', 'e', 'f')
+ view = mi.SequenceView(seq)
+ n = len(seq)
+ indexes = list(range(-n - 1, n + 1)) + [None]
+ steps = list(range(-n, n + 1))
+ steps.remove(0)
+ for slice_args in product(indexes, indexes, steps):
+ i = slice(*slice_args)
+ self.assertEqual(view[i], seq[i])
+
+ def test_abc_methods(self):
+ # collections.Sequence should provide all of this functionality
+ seq = ('a', 'b', 'c', 'd', 'e', 'f', 'f')
+ view = mi.SequenceView(seq)
+
+ # __contains__
+ self.assertIn('b', view)
+ self.assertNotIn('g', view)
+
+ # __iter__
+ self.assertEqual(list(iter(view)), list(seq))
+
+ # __reversed__
+ self.assertEqual(list(reversed(view)), list(reversed(seq)))
+
+ # index
+ self.assertEqual(view.index('b'), 1)
+
+ # count
+ self.assertEqual(seq.count('f'), 2)
+
+
+class RunLengthTest(TestCase):
+ def test_encode(self):
+ iterable = (int(str(n)[0]) for n in count(800))
+ actual = mi.take(4, mi.run_length.encode(iterable))
+ expected = [(8, 100), (9, 100), (1, 1000), (2, 1000)]
+ self.assertEqual(actual, expected)
+
+ def test_decode(self):
+ iterable = [('d', 4), ('c', 3), ('b', 2), ('a', 1)]
+ actual = ''.join(mi.run_length.decode(iterable))
+ expected = 'ddddcccbba'
+ self.assertEqual(actual, expected)
+
+
+class ExactlyNTests(TestCase):
+ """Tests for ``exactly_n()``"""
+
+ def test_true(self):
+ """Iterable has ``n`` ``True`` elements"""
+ self.assertTrue(mi.exactly_n([True, False, True], 2))
+ self.assertTrue(mi.exactly_n([1, 1, 1, 0], 3))
+ self.assertTrue(mi.exactly_n([False, False], 0))
+ self.assertTrue(mi.exactly_n(range(100), 10, lambda x: x < 10))
+
+ def test_false(self):
+ """Iterable does not have ``n`` ``True`` elements"""
+ self.assertFalse(mi.exactly_n([True, False, False], 2))
+ self.assertFalse(mi.exactly_n([True, True, False], 1))
+ self.assertFalse(mi.exactly_n([False], 1))
+ self.assertFalse(mi.exactly_n([True], -1))
+ self.assertFalse(mi.exactly_n(repeat(True), 100))
+
+ def test_empty(self):
+ """Return ``True`` if the iterable is empty and ``n`` is 0"""
+ self.assertTrue(mi.exactly_n([], 0))
+ self.assertFalse(mi.exactly_n([], 1))
+
+
+class AlwaysReversibleTests(TestCase):
+ """Tests for ``always_reversible()``"""
+
+ def test_regular_reversed(self):
+ self.assertEqual(list(reversed(range(10))),
+ list(mi.always_reversible(range(10))))
+ self.assertEqual(list(reversed([1, 2, 3])),
+ list(mi.always_reversible([1, 2, 3])))
+ self.assertEqual(reversed([1, 2, 3]).__class__,
+ mi.always_reversible([1, 2, 3]).__class__)
+
+ def test_nonseq_reversed(self):
+ # Create a non-reversible generator from a sequence
+ with self.assertRaises(TypeError):
+ reversed(x for x in range(10))
+
+ self.assertEqual(list(reversed(range(10))),
+ list(mi.always_reversible(x for x in range(10))))
+ self.assertEqual(list(reversed([1, 2, 3])),
+ list(mi.always_reversible(x for x in [1, 2, 3])))
+ self.assertNotEqual(reversed((1, 2)).__class__,
+ mi.always_reversible(x for x in (1, 2)).__class__)
+
+
+class CircularShiftsTests(TestCase):
+ def test_empty(self):
+ # empty iterable -> empty list
+ self.assertEqual(list(mi.circular_shifts([])), [])
+
+ def test_simple_circular_shifts(self):
+ # test the a simple iterator case
+ self.assertEqual(
+ mi.circular_shifts(range(4)),
+ [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)]
+ )
+
+ def test_duplicates(self):
+ # test non-distinct entries
+ self.assertEqual(
+ mi.circular_shifts([0, 1, 0, 1]),
+ [(0, 1, 0, 1), (1, 0, 1, 0), (0, 1, 0, 1), (1, 0, 1, 0)]
+ )
+
+
+class MakeDecoratorTests(TestCase):
+ def test_basic(self):
+ slicer = mi.make_decorator(islice)
+
+ @slicer(1, 10, 2)
+ def user_function(arg_1, arg_2, kwarg_1=None):
+ self.assertEqual(arg_1, 'arg_1')
+ self.assertEqual(arg_2, 'arg_2')
+ self.assertEqual(kwarg_1, 'kwarg_1')
+ return map(str, count())
+
+ it = user_function('arg_1', 'arg_2', kwarg_1='kwarg_1')
+ actual = list(it)
+ expected = ['1', '3', '5', '7', '9']
+ self.assertEqual(actual, expected)
+
+ def test_result_index(self):
+ def stringify(*args, **kwargs):
+ self.assertEqual(args[0], 'arg_0')
+ iterable = args[1]
+ self.assertEqual(args[2], 'arg_2')
+ self.assertEqual(kwargs['kwarg_1'], 'kwarg_1')
+ return map(str, iterable)
+
+ stringifier = mi.make_decorator(stringify, result_index=1)
+
+ @stringifier('arg_0', 'arg_2', kwarg_1='kwarg_1')
+ def user_function(n):
+ return count(n)
+
+ it = user_function(1)
+ actual = mi.take(5, it)
+ expected = ['1', '2', '3', '4', '5']
+ self.assertEqual(actual, expected)
+
+ def test_wrap_class(self):
+ seeker = mi.make_decorator(mi.seekable)
+
+ @seeker()
+ def user_function(n):
+ return map(str, range(n))
+
+ it = user_function(5)
+ self.assertEqual(list(it), ['0', '1', '2', '3', '4'])
+
+ it.seek(0)
+ self.assertEqual(list(it), ['0', '1', '2', '3', '4'])
+
+
+class MapReduceTests(TestCase):
+ def test_default(self):
+ iterable = (str(x) for x in range(5))
+ keyfunc = lambda x: int(x) // 2
+ actual = sorted(mi.map_reduce(iterable, keyfunc).items())
+ expected = [(0, ['0', '1']), (1, ['2', '3']), (2, ['4'])]
+ self.assertEqual(actual, expected)
+
+ def test_valuefunc(self):
+ iterable = (str(x) for x in range(5))
+ keyfunc = lambda x: int(x) // 2
+ valuefunc = int
+ actual = sorted(mi.map_reduce(iterable, keyfunc, valuefunc).items())
+ expected = [(0, [0, 1]), (1, [2, 3]), (2, [4])]
+ self.assertEqual(actual, expected)
+
+ def test_reducefunc(self):
+ iterable = (str(x) for x in range(5))
+ keyfunc = lambda x: int(x) // 2
+ valuefunc = int
+ reducefunc = lambda value_list: reduce(mul, value_list, 1)
+ actual = sorted(
+ mi.map_reduce(iterable, keyfunc, valuefunc, reducefunc).items()
+ )
+ expected = [(0, 0), (1, 6), (2, 4)]
+ self.assertEqual(actual, expected)
+
+ def test_ret(self):
+ d = mi.map_reduce([1, 0, 2, 0, 1, 0], bool)
+ self.assertEqual(d, {False: [0, 0, 0], True: [1, 2, 1]})
+ self.assertRaises(KeyError, lambda: d[None].append(1))
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/tests/test_recipes.py b/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/tests/test_recipes.py
new file mode 100644
index 0000000000..81721fdf9f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/more_itertools/tests/test_recipes.py
@@ -0,0 +1,607 @@
+from doctest import DocTestSuite
+from unittest import TestCase
+
+from itertools import combinations
+from six.moves import range
+
+import more_itertools as mi
+
+
+def load_tests(loader, tests, ignore):
+ # Add the doctests
+ tests.addTests(DocTestSuite('more_itertools.recipes'))
+ return tests
+
+
+class AccumulateTests(TestCase):
+ """Tests for ``accumulate()``"""
+
+ def test_empty(self):
+ """Test that an empty input returns an empty output"""
+ self.assertEqual(list(mi.accumulate([])), [])
+
+ def test_default(self):
+ """Test accumulate with the default function (addition)"""
+ self.assertEqual(list(mi.accumulate([1, 2, 3])), [1, 3, 6])
+
+ def test_bogus_function(self):
+ """Test accumulate with an invalid function"""
+ with self.assertRaises(TypeError):
+ list(mi.accumulate([1, 2, 3], func=lambda x: x))
+
+ def test_custom_function(self):
+ """Test accumulate with a custom function"""
+ self.assertEqual(
+ list(mi.accumulate((1, 2, 3, 2, 1), func=max)), [1, 2, 3, 3, 3]
+ )
+
+
+class TakeTests(TestCase):
+ """Tests for ``take()``"""
+
+ def test_simple_take(self):
+ """Test basic usage"""
+ t = mi.take(5, range(10))
+ self.assertEqual(t, [0, 1, 2, 3, 4])
+
+ def test_null_take(self):
+ """Check the null case"""
+ t = mi.take(0, range(10))
+ self.assertEqual(t, [])
+
+ def test_negative_take(self):
+ """Make sure taking negative items results in a ValueError"""
+ self.assertRaises(ValueError, lambda: mi.take(-3, range(10)))
+
+ def test_take_too_much(self):
+ """Taking more than an iterator has remaining should return what the
+ iterator has remaining.
+
+ """
+ t = mi.take(10, range(5))
+ self.assertEqual(t, [0, 1, 2, 3, 4])
+
+
+class TabulateTests(TestCase):
+ """Tests for ``tabulate()``"""
+
+ def test_simple_tabulate(self):
+ """Test the happy path"""
+ t = mi.tabulate(lambda x: x)
+ f = tuple([next(t) for _ in range(3)])
+ self.assertEqual(f, (0, 1, 2))
+
+ def test_count(self):
+ """Ensure tabulate accepts specific count"""
+ t = mi.tabulate(lambda x: 2 * x, -1)
+ f = (next(t), next(t), next(t))
+ self.assertEqual(f, (-2, 0, 2))
+
+
+class TailTests(TestCase):
+ """Tests for ``tail()``"""
+
+ def test_greater(self):
+ """Length of iterable is greather than requested tail"""
+ self.assertEqual(list(mi.tail(3, 'ABCDEFG')), ['E', 'F', 'G'])
+
+ def test_equal(self):
+ """Length of iterable is equal to the requested tail"""
+ self.assertEqual(
+ list(mi.tail(7, 'ABCDEFG')), ['A', 'B', 'C', 'D', 'E', 'F', 'G']
+ )
+
+ def test_less(self):
+ """Length of iterable is less than requested tail"""
+ self.assertEqual(
+ list(mi.tail(8, 'ABCDEFG')), ['A', 'B', 'C', 'D', 'E', 'F', 'G']
+ )
+
+
+class ConsumeTests(TestCase):
+ """Tests for ``consume()``"""
+
+ def test_sanity(self):
+ """Test basic functionality"""
+ r = (x for x in range(10))
+ mi.consume(r, 3)
+ self.assertEqual(3, next(r))
+
+ def test_null_consume(self):
+ """Check the null case"""
+ r = (x for x in range(10))
+ mi.consume(r, 0)
+ self.assertEqual(0, next(r))
+
+ def test_negative_consume(self):
+ """Check that negative consumsion throws an error"""
+ r = (x for x in range(10))
+ self.assertRaises(ValueError, lambda: mi.consume(r, -1))
+
+ def test_total_consume(self):
+ """Check that iterator is totally consumed by default"""
+ r = (x for x in range(10))
+ mi.consume(r)
+ self.assertRaises(StopIteration, lambda: next(r))
+
+
+class NthTests(TestCase):
+ """Tests for ``nth()``"""
+
+ def test_basic(self):
+ """Make sure the nth item is returned"""
+ l = range(10)
+ for i, v in enumerate(l):
+ self.assertEqual(mi.nth(l, i), v)
+
+ def test_default(self):
+ """Ensure a default value is returned when nth item not found"""
+ l = range(3)
+ self.assertEqual(mi.nth(l, 100, "zebra"), "zebra")
+
+ def test_negative_item_raises(self):
+ """Ensure asking for a negative item raises an exception"""
+ self.assertRaises(ValueError, lambda: mi.nth(range(10), -3))
+
+
+class AllEqualTests(TestCase):
+ """Tests for ``all_equal()``"""
+
+ def test_true(self):
+ """Everything is equal"""
+ self.assertTrue(mi.all_equal('aaaaaa'))
+ self.assertTrue(mi.all_equal([0, 0, 0, 0]))
+
+ def test_false(self):
+ """Not everything is equal"""
+ self.assertFalse(mi.all_equal('aaaaab'))
+ self.assertFalse(mi.all_equal([0, 0, 0, 1]))
+
+ def test_tricky(self):
+ """Not everything is identical, but everything is equal"""
+ items = [1, complex(1, 0), 1.0]
+ self.assertTrue(mi.all_equal(items))
+
+ def test_empty(self):
+ """Return True if the iterable is empty"""
+ self.assertTrue(mi.all_equal(''))
+ self.assertTrue(mi.all_equal([]))
+
+ def test_one(self):
+ """Return True if the iterable is singular"""
+ self.assertTrue(mi.all_equal('0'))
+ self.assertTrue(mi.all_equal([0]))
+
+
+class QuantifyTests(TestCase):
+ """Tests for ``quantify()``"""
+
+ def test_happy_path(self):
+ """Make sure True count is returned"""
+ q = [True, False, True]
+ self.assertEqual(mi.quantify(q), 2)
+
+ def test_custom_predicate(self):
+ """Ensure non-default predicates return as expected"""
+ q = range(10)
+ self.assertEqual(mi.quantify(q, lambda x: x % 2 == 0), 5)
+
+
+class PadnoneTests(TestCase):
+ """Tests for ``padnone()``"""
+
+ def test_happy_path(self):
+ """wrapper iterator should return None indefinitely"""
+ r = range(2)
+ p = mi.padnone(r)
+ self.assertEqual([0, 1, None, None], [next(p) for _ in range(4)])
+
+
+class NcyclesTests(TestCase):
+ """Tests for ``nyclces()``"""
+
+ def test_happy_path(self):
+ """cycle a sequence three times"""
+ r = ["a", "b", "c"]
+ n = mi.ncycles(r, 3)
+ self.assertEqual(
+ ["a", "b", "c", "a", "b", "c", "a", "b", "c"],
+ list(n)
+ )
+
+ def test_null_case(self):
+ """asking for 0 cycles should return an empty iterator"""
+ n = mi.ncycles(range(100), 0)
+ self.assertRaises(StopIteration, lambda: next(n))
+
+ def test_pathalogical_case(self):
+ """asking for negative cycles should return an empty iterator"""
+ n = mi.ncycles(range(100), -10)
+ self.assertRaises(StopIteration, lambda: next(n))
+
+
+class DotproductTests(TestCase):
+ """Tests for ``dotproduct()``'"""
+
+ def test_happy_path(self):
+ """simple dotproduct example"""
+ self.assertEqual(400, mi.dotproduct([10, 10], [20, 20]))
+
+
+class FlattenTests(TestCase):
+ """Tests for ``flatten()``"""
+
+ def test_basic_usage(self):
+ """ensure list of lists is flattened one level"""
+ f = [[0, 1, 2], [3, 4, 5]]
+ self.assertEqual(list(range(6)), list(mi.flatten(f)))
+
+ def test_single_level(self):
+ """ensure list of lists is flattened only one level"""
+ f = [[0, [1, 2]], [[3, 4], 5]]
+ self.assertEqual([0, [1, 2], [3, 4], 5], list(mi.flatten(f)))
+
+
+class RepeatfuncTests(TestCase):
+ """Tests for ``repeatfunc()``"""
+
+ def test_simple_repeat(self):
+ """test simple repeated functions"""
+ r = mi.repeatfunc(lambda: 5)
+ self.assertEqual([5, 5, 5, 5, 5], [next(r) for _ in range(5)])
+
+ def test_finite_repeat(self):
+ """ensure limited repeat when times is provided"""
+ r = mi.repeatfunc(lambda: 5, times=5)
+ self.assertEqual([5, 5, 5, 5, 5], list(r))
+
+ def test_added_arguments(self):
+ """ensure arguments are applied to the function"""
+ r = mi.repeatfunc(lambda x: x, 2, 3)
+ self.assertEqual([3, 3], list(r))
+
+ def test_null_times(self):
+ """repeat 0 should return an empty iterator"""
+ r = mi.repeatfunc(range, 0, 3)
+ self.assertRaises(StopIteration, lambda: next(r))
+
+
+class PairwiseTests(TestCase):
+ """Tests for ``pairwise()``"""
+
+ def test_base_case(self):
+ """ensure an iterable will return pairwise"""
+ p = mi.pairwise([1, 2, 3])
+ self.assertEqual([(1, 2), (2, 3)], list(p))
+
+ def test_short_case(self):
+ """ensure an empty iterator if there's not enough values to pair"""
+ p = mi.pairwise("a")
+ self.assertRaises(StopIteration, lambda: next(p))
+
+
+class GrouperTests(TestCase):
+ """Tests for ``grouper()``"""
+
+ def test_even(self):
+ """Test when group size divides evenly into the length of
+ the iterable.
+
+ """
+ self.assertEqual(
+ list(mi.grouper(3, 'ABCDEF')), [('A', 'B', 'C'), ('D', 'E', 'F')]
+ )
+
+ def test_odd(self):
+ """Test when group size does not divide evenly into the length of the
+ iterable.
+
+ """
+ self.assertEqual(
+ list(mi.grouper(3, 'ABCDE')), [('A', 'B', 'C'), ('D', 'E', None)]
+ )
+
+ def test_fill_value(self):
+ """Test that the fill value is used to pad the final group"""
+ self.assertEqual(
+ list(mi.grouper(3, 'ABCDE', 'x')),
+ [('A', 'B', 'C'), ('D', 'E', 'x')]
+ )
+
+
+class RoundrobinTests(TestCase):
+ """Tests for ``roundrobin()``"""
+
+ def test_even_groups(self):
+ """Ensure ordered output from evenly populated iterables"""
+ self.assertEqual(
+ list(mi.roundrobin('ABC', [1, 2, 3], range(3))),
+ ['A', 1, 0, 'B', 2, 1, 'C', 3, 2]
+ )
+
+ def test_uneven_groups(self):
+ """Ensure ordered output from unevenly populated iterables"""
+ self.assertEqual(
+ list(mi.roundrobin('ABCD', [1, 2], range(0))),
+ ['A', 1, 'B', 2, 'C', 'D']
+ )
+
+
+class PartitionTests(TestCase):
+ """Tests for ``partition()``"""
+
+ def test_bool(self):
+ """Test when pred() returns a boolean"""
+ lesser, greater = mi.partition(lambda x: x > 5, range(10))
+ self.assertEqual(list(lesser), [0, 1, 2, 3, 4, 5])
+ self.assertEqual(list(greater), [6, 7, 8, 9])
+
+ def test_arbitrary(self):
+ """Test when pred() returns an integer"""
+ divisibles, remainders = mi.partition(lambda x: x % 3, range(10))
+ self.assertEqual(list(divisibles), [0, 3, 6, 9])
+ self.assertEqual(list(remainders), [1, 2, 4, 5, 7, 8])
+
+
+class PowersetTests(TestCase):
+ """Tests for ``powerset()``"""
+
+ def test_combinatorics(self):
+ """Ensure a proper enumeration"""
+ p = mi.powerset([1, 2, 3])
+ self.assertEqual(
+ list(p),
+ [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)]
+ )
+
+
+class UniqueEverseenTests(TestCase):
+ """Tests for ``unique_everseen()``"""
+
+ def test_everseen(self):
+ """ensure duplicate elements are ignored"""
+ u = mi.unique_everseen('AAAABBBBCCDAABBB')
+ self.assertEqual(
+ ['A', 'B', 'C', 'D'],
+ list(u)
+ )
+
+ def test_custom_key(self):
+ """ensure the custom key comparison works"""
+ u = mi.unique_everseen('aAbACCc', key=str.lower)
+ self.assertEqual(list('abC'), list(u))
+
+ def test_unhashable(self):
+ """ensure things work for unhashable items"""
+ iterable = ['a', [1, 2, 3], [1, 2, 3], 'a']
+ u = mi.unique_everseen(iterable)
+ self.assertEqual(list(u), ['a', [1, 2, 3]])
+
+ def test_unhashable_key(self):
+ """ensure things work for unhashable items with a custom key"""
+ iterable = ['a', [1, 2, 3], [1, 2, 3], 'a']
+ u = mi.unique_everseen(iterable, key=lambda x: x)
+ self.assertEqual(list(u), ['a', [1, 2, 3]])
+
+
+class UniqueJustseenTests(TestCase):
+ """Tests for ``unique_justseen()``"""
+
+ def test_justseen(self):
+ """ensure only last item is remembered"""
+ u = mi.unique_justseen('AAAABBBCCDABB')
+ self.assertEqual(list('ABCDAB'), list(u))
+
+ def test_custom_key(self):
+ """ensure the custom key comparison works"""
+ u = mi.unique_justseen('AABCcAD', str.lower)
+ self.assertEqual(list('ABCAD'), list(u))
+
+
+class IterExceptTests(TestCase):
+ """Tests for ``iter_except()``"""
+
+ def test_exact_exception(self):
+ """ensure the exact specified exception is caught"""
+ l = [1, 2, 3]
+ i = mi.iter_except(l.pop, IndexError)
+ self.assertEqual(list(i), [3, 2, 1])
+
+ def test_generic_exception(self):
+ """ensure the generic exception can be caught"""
+ l = [1, 2]
+ i = mi.iter_except(l.pop, Exception)
+ self.assertEqual(list(i), [2, 1])
+
+ def test_uncaught_exception_is_raised(self):
+ """ensure a non-specified exception is raised"""
+ l = [1, 2, 3]
+ i = mi.iter_except(l.pop, KeyError)
+ self.assertRaises(IndexError, lambda: list(i))
+
+ def test_first(self):
+ """ensure first is run before the function"""
+ l = [1, 2, 3]
+ f = lambda: 25
+ i = mi.iter_except(l.pop, IndexError, f)
+ self.assertEqual(list(i), [25, 3, 2, 1])
+
+
+class FirstTrueTests(TestCase):
+ """Tests for ``first_true()``"""
+
+ def test_something_true(self):
+ """Test with no keywords"""
+ self.assertEqual(mi.first_true(range(10)), 1)
+
+ def test_nothing_true(self):
+ """Test default return value."""
+ self.assertEqual(mi.first_true([0, 0, 0]), False)
+
+ def test_default(self):
+ """Test with a default keyword"""
+ self.assertEqual(mi.first_true([0, 0, 0], default='!'), '!')
+
+ def test_pred(self):
+ """Test with a custom predicate"""
+ self.assertEqual(
+ mi.first_true([2, 4, 6], pred=lambda x: x % 3 == 0), 6
+ )
+
+
+class RandomProductTests(TestCase):
+ """Tests for ``random_product()``
+
+ Since random.choice() has different results with the same seed across
+ python versions 2.x and 3.x, these tests use highly probably events to
+ create predictable outcomes across platforms.
+ """
+
+ def test_simple_lists(self):
+ """Ensure that one item is chosen from each list in each pair.
+ Also ensure that each item from each list eventually appears in
+ the chosen combinations.
+
+ Odds are roughly 1 in 7.1 * 10e16 that one item from either list will
+ not be chosen after 100 samplings of one item from each list. Just to
+ be safe, better use a known random seed, too.
+
+ """
+ nums = [1, 2, 3]
+ lets = ['a', 'b', 'c']
+ n, m = zip(*[mi.random_product(nums, lets) for _ in range(100)])
+ n, m = set(n), set(m)
+ self.assertEqual(n, set(nums))
+ self.assertEqual(m, set(lets))
+ self.assertEqual(len(n), len(nums))
+ self.assertEqual(len(m), len(lets))
+
+ def test_list_with_repeat(self):
+ """ensure multiple items are chosen, and that they appear to be chosen
+ from one list then the next, in proper order.
+
+ """
+ nums = [1, 2, 3]
+ lets = ['a', 'b', 'c']
+ r = list(mi.random_product(nums, lets, repeat=100))
+ self.assertEqual(2 * 100, len(r))
+ n, m = set(r[::2]), set(r[1::2])
+ self.assertEqual(n, set(nums))
+ self.assertEqual(m, set(lets))
+ self.assertEqual(len(n), len(nums))
+ self.assertEqual(len(m), len(lets))
+
+
+class RandomPermutationTests(TestCase):
+ """Tests for ``random_permutation()``"""
+
+ def test_full_permutation(self):
+ """ensure every item from the iterable is returned in a new ordering
+
+ 15 elements have a 1 in 1.3 * 10e12 of appearing in sorted order, so
+ we fix a seed value just to be sure.
+
+ """
+ i = range(15)
+ r = mi.random_permutation(i)
+ self.assertEqual(set(i), set(r))
+ if i == r:
+ raise AssertionError("Values were not permuted")
+
+ def test_partial_permutation(self):
+ """ensure all returned items are from the iterable, that the returned
+ permutation is of the desired length, and that all items eventually
+ get returned.
+
+ Sampling 100 permutations of length 5 from a set of 15 leaves a
+ (2/3)^100 chance that an item will not be chosen. Multiplied by 15
+ items, there is a 1 in 2.6e16 chance that at least 1 item will not
+ show up in the resulting output. Using a random seed will fix that.
+
+ """
+ items = range(15)
+ item_set = set(items)
+ all_items = set()
+ for _ in range(100):
+ permutation = mi.random_permutation(items, 5)
+ self.assertEqual(len(permutation), 5)
+ permutation_set = set(permutation)
+ self.assertLessEqual(permutation_set, item_set)
+ all_items |= permutation_set
+ self.assertEqual(all_items, item_set)
+
+
+class RandomCombinationTests(TestCase):
+ """Tests for ``random_combination()``"""
+
+ def test_psuedorandomness(self):
+ """ensure different subsets of the iterable get returned over many
+ samplings of random combinations"""
+ items = range(15)
+ all_items = set()
+ for _ in range(50):
+ combination = mi.random_combination(items, 5)
+ all_items |= set(combination)
+ self.assertEqual(all_items, set(items))
+
+ def test_no_replacement(self):
+ """ensure that elements are sampled without replacement"""
+ items = range(15)
+ for _ in range(50):
+ combination = mi.random_combination(items, len(items))
+ self.assertEqual(len(combination), len(set(combination)))
+ self.assertRaises(
+ ValueError, lambda: mi.random_combination(items, len(items) + 1)
+ )
+
+
+class RandomCombinationWithReplacementTests(TestCase):
+ """Tests for ``random_combination_with_replacement()``"""
+
+ def test_replacement(self):
+ """ensure that elements are sampled with replacement"""
+ items = range(5)
+ combo = mi.random_combination_with_replacement(items, len(items) * 2)
+ self.assertEqual(2 * len(items), len(combo))
+ if len(set(combo)) == len(combo):
+ raise AssertionError("Combination contained no duplicates")
+
+ def test_pseudorandomness(self):
+ """ensure different subsets of the iterable get returned over many
+ samplings of random combinations"""
+ items = range(15)
+ all_items = set()
+ for _ in range(50):
+ combination = mi.random_combination_with_replacement(items, 5)
+ all_items |= set(combination)
+ self.assertEqual(all_items, set(items))
+
+
+class NthCombinationTests(TestCase):
+ def test_basic(self):
+ iterable = 'abcdefg'
+ r = 4
+ for index, expected in enumerate(combinations(iterable, r)):
+ actual = mi.nth_combination(iterable, r, index)
+ self.assertEqual(actual, expected)
+
+ def test_long(self):
+ actual = mi.nth_combination(range(180), 4, 2000000)
+ expected = (2, 12, 35, 126)
+ self.assertEqual(actual, expected)
+
+
+class PrependTests(TestCase):
+ def test_basic(self):
+ value = 'a'
+ iterator = iter('bcdefg')
+ actual = list(mi.prepend(value, iterator))
+ expected = list('abcdefg')
+ self.assertEqual(actual, expected)
+
+ def test_multiple(self):
+ value = 'ab'
+ iterator = iter('cdefg')
+ actual = tuple(mi.prepend(value, iterator))
+ expected = ('ab',) + tuple('cdefg')
+ self.assertEqual(actual, expected)
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/setup.cfg b/testing/web-platform/tests/tools/third_party/more-itertools/setup.cfg
new file mode 100644
index 0000000000..7c0e37ba6c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/setup.cfg
@@ -0,0 +1,3 @@
+[flake8]
+exclude = ./docs/conf.py, .eggs/
+ignore = E731, E741, F999
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/setup.py b/testing/web-platform/tests/tools/third_party/more-itertools/setup.py
new file mode 100644
index 0000000000..484e4d06f7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/setup.py
@@ -0,0 +1,59 @@
+# Hack to prevent stupid error on exit of `python setup.py test`. (See
+# http://www.eby-sarna.com/pipermail/peak/2010-May/003357.html.)
+try:
+ import multiprocessing # noqa
+except ImportError:
+ pass
+from re import sub
+
+from setuptools import setup, find_packages
+
+
+def get_long_description():
+ # Fix display issues on PyPI caused by RST markup
+ readme = open('README.rst').read()
+
+ version_lines = []
+ with open('docs/versions.rst') as infile:
+ next(infile)
+ for line in infile:
+ line = line.rstrip().replace('.. automodule:: more_itertools', '')
+ version_lines.append(line)
+ version_history = '\n'.join(version_lines)
+ version_history = sub(r':func:`([a-zA-Z0-9._]+)`', r'\1', version_history)
+
+ ret = readme + '\n\n' + version_history
+ return ret
+
+
+setup(
+ name='more-itertools',
+ version='4.2.0',
+ description='More routines for operating on iterables, beyond itertools',
+ long_description=get_long_description(),
+ author='Erik Rose',
+ author_email='erikrose@grinchcentral.com',
+ license='MIT',
+ packages=find_packages(exclude=['ez_setup']),
+ install_requires=['six>=1.0.0,<2.0.0'],
+ test_suite='more_itertools.tests',
+ url='https://github.com/erikrose/more-itertools',
+ include_package_data=True,
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ 'Natural Language :: English',
+ 'License :: OSI Approved :: MIT License',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.2',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Topic :: Software Development :: Libraries'],
+ keywords=['itertools', 'iterator', 'iteration', 'filter', 'peek',
+ 'peekable', 'collate', 'chunk', 'chunked'],
+)
diff --git a/testing/web-platform/tests/tools/third_party/more-itertools/tox.ini b/testing/web-platform/tests/tools/third_party/more-itertools/tox.ini
new file mode 100644
index 0000000000..70c68c058d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/more-itertools/tox.ini
@@ -0,0 +1,5 @@
+[tox]
+envlist = py27, py34, py35, py36, py37
+
+[testenv]
+commands = {envbindir}/python -m unittest discover -v
diff --git a/testing/web-platform/tests/tools/third_party/packaging/.coveragerc b/testing/web-platform/tests/tools/third_party/packaging/.coveragerc
new file mode 100644
index 0000000000..da205e5a14
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/.coveragerc
@@ -0,0 +1,9 @@
+[run]
+branch = True
+omit = packaging/_compat.py
+
+[report]
+exclude_lines =
+ pragma: no cover
+ @abc.abstractmethod
+ @abc.abstractproperty
diff --git a/testing/web-platform/tests/tools/third_party/packaging/.flake8 b/testing/web-platform/tests/tools/third_party/packaging/.flake8
new file mode 100644
index 0000000000..b5a35be92a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/.flake8
@@ -0,0 +1,3 @@
+[flake8]
+max-line-length = 88
+ignore = E203,W503,W504
diff --git a/testing/web-platform/tests/tools/third_party/packaging/.github/workflows/docs.yml b/testing/web-platform/tests/tools/third_party/packaging/.github/workflows/docs.yml
new file mode 100644
index 0000000000..2c15738779
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/.github/workflows/docs.yml
@@ -0,0 +1,30 @@
+name: Documentation
+
+on:
+ pull_request:
+ paths:
+ - 'docs/**'
+ push:
+ paths:
+ - 'docs/**'
+
+jobs:
+ docs:
+ name: nox -s docs
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v1
+
+ - uses: actions/setup-python@v2
+ name: Install Python
+ with:
+ python-version: '3.9'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install --upgrade nox
+
+ - name: Build documentation
+ run: python -m nox -s docs
diff --git a/testing/web-platform/tests/tools/third_party/packaging/.github/workflows/lint.yml b/testing/web-platform/tests/tools/third_party/packaging/.github/workflows/lint.yml
new file mode 100644
index 0000000000..f37e63463c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/.github/workflows/lint.yml
@@ -0,0 +1,59 @@
+name: Linting
+
+on:
+ pull_request:
+ paths:
+ - "**.py"
+ push:
+ paths:
+ - "**.py"
+
+jobs:
+ lint:
+ name: nox -s lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v1
+
+ - uses: actions/setup-python@v2
+ name: Install Python
+ with:
+ python-version: "3.9"
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install --upgrade nox
+
+ - name: Run `nox -s lint`
+ run: python -m nox -s lint
+
+ build:
+ name: Build sdist and wheel
+ runs-on: ubuntu-latest
+ # Linting verifies that the project is in an acceptable state to create files
+ # for releasing.
+ # And this action should be run whenever a release is ready to go public as
+ # the version number will be changed by editing __about__.py.
+ needs: lint
+
+ steps:
+ - uses: actions/checkout@v1
+
+ - uses: actions/setup-python@v2
+ name: Install Python
+ with:
+ python-version: "3.9"
+
+ - name: Install dependencies
+ run: python -m pip install --upgrade build
+
+ - name: Build
+ run: pyproject-build
+
+ - name: Archive files
+ uses: actions/upload-artifact@v1
+ with:
+ name: dist
+ path: dist
diff --git a/testing/web-platform/tests/tools/third_party/packaging/.github/workflows/test.yml b/testing/web-platform/tests/tools/third_party/packaging/.github/workflows/test.yml
new file mode 100644
index 0000000000..97c0c25d3c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/.github/workflows/test.yml
@@ -0,0 +1,56 @@
+name: Test
+
+on:
+ pull_request:
+ paths:
+ - ".github/workflows/test.yml"
+ - "**.py"
+ push:
+ paths:
+ - ".github/workflows/test.yml"
+ - "**.py"
+
+jobs:
+ test:
+ name: ${{ matrix.os }} / ${{ matrix.python_version }}
+ runs-on: ${{ matrix.os }}-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [Ubuntu, Windows, macOS]
+ python_version:
+ ["3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.7"]
+
+ steps:
+ - uses: actions/checkout@v1
+
+ - uses: actions/setup-python@v2
+ name: Install Python ${{ matrix.python_version }}
+ with:
+ python-version: ${{ matrix.python_version }}
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install nox
+ shell: bash
+
+ - name: Run nox
+ run: |
+ python -m nox --error-on-missing-interpreters -s tests-${{ matrix.python_version }}
+ shell: bash
+ if: ${{ ! startsWith( matrix.python_version, 'pypy' ) }}
+
+ # Binary is named 'pypy', but setup-python specifies it as 'pypy2'.
+ - name: Run nox for pypy2
+ run: |
+ python -m nox --error-on-missing-interpreters -s tests-pypy
+ shell: bash
+ if: matrix.python_version == 'pypy2'
+
+ # Binary is named 'pypy3', but setup-python specifies it as 'pypy-3.7'.
+ - name: Run nox for pypy3
+ run: |
+ python -m nox --error-on-missing-interpreters -s tests-pypy3
+ shell: bash
+ if: matrix.python_version == 'pypy-3.7'
diff --git a/testing/web-platform/tests/tools/third_party/packaging/.gitignore b/testing/web-platform/tests/tools/third_party/packaging/.gitignore
new file mode 100644
index 0000000000..05e554a64c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/.gitignore
@@ -0,0 +1,18 @@
+*.egg-info/
+*.egg
+*.py[co]
+
+.[nt]ox/
+.cache/
+.coverage
+.idea
+.venv*
+.vscode/
+
+.mypy_cache/
+.pytest_cache/
+__pycache__/
+_build/
+build/
+dist/
+htmlcov/
diff --git a/testing/web-platform/tests/tools/third_party/packaging/.pre-commit-config.yaml b/testing/web-platform/tests/tools/third_party/packaging/.pre-commit-config.yaml
new file mode 100644
index 0000000000..49ae0d4e78
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/.pre-commit-config.yaml
@@ -0,0 +1,39 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v3.4.0
+ hooks:
+ - id: check-toml
+ - id: check-yaml
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v0.812
+ hooks:
+ - id: mypy
+ exclude: '^(docs|tasks|tests)|setup\.py'
+ args: []
+
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v2.29.0
+ hooks:
+ - id: pyupgrade
+ args: [--py36-plus]
+
+ - repo: https://github.com/psf/black
+ rev: 20.8b1
+ hooks:
+ - id: black
+
+ - repo: https://github.com/PyCQA/isort
+ rev: 5.8.0
+ hooks:
+ - id: isort
+
+ - repo: https://gitlab.com/PyCQA/flake8
+ rev: "3.9.0"
+ hooks:
+ - id: flake8
+ additional_dependencies: ["pep8-naming"]
+ # Ignore all format-related checks as Black takes care of those.
+ args: ["--ignore", "E2,W5", "--select", "E,W,F,N"]
diff --git a/testing/web-platform/tests/tools/third_party/packaging/.readthedocs.yml b/testing/web-platform/tests/tools/third_party/packaging/.readthedocs.yml
new file mode 100644
index 0000000000..d8ac216687
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/.readthedocs.yml
@@ -0,0 +1,15 @@
+version: 2
+
+build:
+ image: latest
+
+formats: [pdf]
+sphinx:
+ configuration: docs/conf.py
+
+python:
+ version: 3.8
+ install:
+ - requirements: docs/requirements.txt
+ - method: pip
+ path: .
diff --git a/testing/web-platform/tests/tools/third_party/packaging/CHANGELOG.rst b/testing/web-platform/tests/tools/third_party/packaging/CHANGELOG.rst
new file mode 100644
index 0000000000..f23c30314a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/CHANGELOG.rst
@@ -0,0 +1,347 @@
+Changelog
+---------
+
+21.3 - 2021-11-17
+~~~~~~~~~~~~~~~~~
+
+* Add a ``pp3-none-any`` tag (:issue:`311`)
+* Replace the blank pyparsing 3 exclusion with a 3.0.5 exclusion (:issue:`481`, :issue:`486`)
+* Fix a spelling mistake (:issue:`479`)
+
+21.2 - 2021-10-29
+~~~~~~~~~~~~~~~~~
+
+* Update documentation entry for 21.1.
+
+21.1 - 2021-10-29
+~~~~~~~~~~~~~~~~~
+
+* Update pin to pyparsing to exclude 3.0.0.
+
+21.0 - 2021-07-03
+~~~~~~~~~~~~~~~~~
+
+* PEP 656: musllinux support (:issue:`411`)
+* Drop support for Python 2.7, Python 3.4 and Python 3.5.
+* Replace distutils usage with sysconfig (:issue:`396`)
+* Add support for zip files in ``parse_sdist_filename`` (:issue:`429`)
+* Use cached ``_hash`` attribute to short-circuit tag equality comparisons (:issue:`417`)
+* Specify the default value for the ``specifier`` argument to ``SpecifierSet`` (:issue:`437`)
+* Proper keyword-only "warn" argument in packaging.tags (:issue:`403`)
+* Correctly remove prerelease suffixes from ~= check (:issue:`366`)
+* Fix type hints for ``Version.post`` and ``Version.dev`` (:issue:`393`)
+* Use typing alias ``UnparsedVersion`` (:issue:`398`)
+* Improve type inference for ``packaging.specifiers.filter()`` (:issue:`430`)
+* Tighten the return type of ``canonicalize_version()`` (:issue:`402`)
+
+20.9 - 2021-01-29
+~~~~~~~~~~~~~~~~~
+
+* Run `isort <https://pypi.org/project/isort/>`_ over the code base (:issue:`377`)
+* Add support for the ``macosx_10_*_universal2`` platform tags (:issue:`379`)
+* Introduce ``packaging.utils.parse_wheel_filename()`` and ``parse_sdist_filename()``
+ (:issue:`387` and :issue:`389`)
+
+20.8 - 2020-12-11
+~~~~~~~~~~~~~~~~~
+
+* Revert back to setuptools for compatibility purposes for some Linux distros (:issue:`363`)
+* Do not insert an underscore in wheel tags when the interpreter version number
+ is more than 2 digits (:issue:`372`)
+
+20.7 - 2020-11-28
+~~~~~~~~~~~~~~~~~
+
+No unreleased changes.
+
+20.6 - 2020-11-28
+~~~~~~~~~~~~~~~~~
+
+.. note:: This release was subsequently yanked, and these changes were included in 20.7.
+
+* Fix flit configuration, to include LICENSE files (:issue:`357`)
+* Make `intel` a recognized CPU architecture for the `universal` macOS platform tag (:issue:`361`)
+* Add some missing type hints to `packaging.requirements` (issue:`350`)
+
+20.5 - 2020-11-27
+~~~~~~~~~~~~~~~~~
+
+* Officially support Python 3.9 (:issue:`343`)
+* Deprecate the ``LegacyVersion`` and ``LegacySpecifier`` classes (:issue:`321`)
+* Handle ``OSError`` on non-dynamic executables when attempting to resolve
+ the glibc version string.
+
+20.4 - 2020-05-19
+~~~~~~~~~~~~~~~~~
+
+* Canonicalize version before comparing specifiers. (:issue:`282`)
+* Change type hint for ``canonicalize_name`` to return
+ ``packaging.utils.NormalizedName``.
+ This enables the use of static typing tools (like mypy) to detect mixing of
+ normalized and un-normalized names.
+
+20.3 - 2020-03-05
+~~~~~~~~~~~~~~~~~
+
+* Fix changelog for 20.2.
+
+20.2 - 2020-03-05
+~~~~~~~~~~~~~~~~~
+
+* Fix a bug that caused a 32-bit OS that runs on a 64-bit ARM CPU (e.g. ARM-v8,
+ aarch64), to report the wrong bitness.
+
+20.1 - 2020-01-24
+~~~~~~~~~~~~~~~~~~~
+
+* Fix a bug caused by reuse of an exhausted iterator. (:issue:`257`)
+
+20.0 - 2020-01-06
+~~~~~~~~~~~~~~~~~
+
+* Add type hints (:issue:`191`)
+
+* Add proper trove classifiers for PyPy support (:issue:`198`)
+
+* Scale back depending on ``ctypes`` for manylinux support detection (:issue:`171`)
+
+* Use ``sys.implementation.name`` where appropriate for ``packaging.tags`` (:issue:`193`)
+
+* Expand upon the API provided by ``packaging.tags``: ``interpreter_name()``, ``mac_platforms()``, ``compatible_tags()``, ``cpython_tags()``, ``generic_tags()`` (:issue:`187`)
+
+* Officially support Python 3.8 (:issue:`232`)
+
+* Add ``major``, ``minor``, and ``micro`` aliases to ``packaging.version.Version`` (:issue:`226`)
+
+* Properly mark ``packaging`` has being fully typed by adding a `py.typed` file (:issue:`226`)
+
+19.2 - 2019-09-18
+~~~~~~~~~~~~~~~~~
+
+* Remove dependency on ``attrs`` (:issue:`178`, :issue:`179`)
+
+* Use appropriate fallbacks for CPython ABI tag (:issue:`181`, :issue:`185`)
+
+* Add manylinux2014 support (:issue:`186`)
+
+* Improve ABI detection (:issue:`181`)
+
+* Properly handle debug wheels for Python 3.8 (:issue:`172`)
+
+* Improve detection of debug builds on Windows (:issue:`194`)
+
+19.1 - 2019-07-30
+~~~~~~~~~~~~~~~~~
+
+* Add the ``packaging.tags`` module. (:issue:`156`)
+
+* Correctly handle two-digit versions in ``python_version`` (:issue:`119`)
+
+
+19.0 - 2019-01-20
+~~~~~~~~~~~~~~~~~
+
+* Fix string representation of PEP 508 direct URL requirements with markers.
+
+* Better handling of file URLs
+
+ This allows for using ``file:///absolute/path``, which was previously
+ prevented due to the missing ``netloc``.
+
+ This allows for all file URLs that ``urlunparse`` turns back into the
+ original URL to be valid.
+
+
+18.0 - 2018-09-26
+~~~~~~~~~~~~~~~~~
+
+* Improve error messages when invalid requirements are given. (:issue:`129`)
+
+
+17.1 - 2017-02-28
+~~~~~~~~~~~~~~~~~
+
+* Fix ``utils.canonicalize_version`` when supplying non PEP 440 versions.
+
+
+17.0 - 2017-02-28
+~~~~~~~~~~~~~~~~~
+
+* Drop support for python 2.6, 3.2, and 3.3.
+
+* Define minimal pyparsing version to 2.0.2 (:issue:`91`).
+
+* Add ``epoch``, ``release``, ``pre``, ``dev``, and ``post`` attributes to
+ ``Version`` and ``LegacyVersion`` (:issue:`34`).
+
+* Add ``Version().is_devrelease`` and ``LegacyVersion().is_devrelease`` to
+ make it easy to determine if a release is a development release.
+
+* Add ``utils.canonicalize_version`` to canonicalize version strings or
+ ``Version`` instances (:issue:`121`).
+
+
+16.8 - 2016-10-29
+~~~~~~~~~~~~~~~~~
+
+* Fix markers that utilize ``in`` so that they render correctly.
+
+* Fix an erroneous test on Python RC releases.
+
+
+16.7 - 2016-04-23
+~~~~~~~~~~~~~~~~~
+
+* Add support for the deprecated ``python_implementation`` marker which was
+ an undocumented setuptools marker in addition to the newer markers.
+
+
+16.6 - 2016-03-29
+~~~~~~~~~~~~~~~~~
+
+* Add support for the deprecated, PEP 345 environment markers in addition to
+ the newer markers.
+
+
+16.5 - 2016-02-26
+~~~~~~~~~~~~~~~~~
+
+* Fix a regression in parsing requirements with whitespaces between the comma
+ separators.
+
+
+16.4 - 2016-02-22
+~~~~~~~~~~~~~~~~~
+
+* Fix a regression in parsing requirements like ``foo (==4)``.
+
+
+16.3 - 2016-02-21
+~~~~~~~~~~~~~~~~~
+
+* Fix a bug where ``packaging.requirements:Requirement`` was overly strict when
+ matching legacy requirements.
+
+
+16.2 - 2016-02-09
+~~~~~~~~~~~~~~~~~
+
+* Add a function that implements the name canonicalization from PEP 503.
+
+
+16.1 - 2016-02-07
+~~~~~~~~~~~~~~~~~
+
+* Implement requirement specifiers from PEP 508.
+
+
+16.0 - 2016-01-19
+~~~~~~~~~~~~~~~~~
+
+* Relicense so that packaging is available under *either* the Apache License,
+ Version 2.0 or a 2 Clause BSD license.
+
+* Support installation of packaging when only distutils is available.
+
+* Fix ``==`` comparison when there is a prefix and a local version in play.
+ (:issue:`41`).
+
+* Implement environment markers from PEP 508.
+
+
+15.3 - 2015-08-01
+~~~~~~~~~~~~~~~~~
+
+* Normalize post-release spellings for rev/r prefixes. :issue:`35`
+
+
+15.2 - 2015-05-13
+~~~~~~~~~~~~~~~~~
+
+* Fix an error where the arbitrary specifier (``===``) was not correctly
+ allowing pre-releases when it was being used.
+
+* Expose the specifier and version parts through properties on the
+ ``Specifier`` classes.
+
+* Allow iterating over the ``SpecifierSet`` to get access to all of the
+ ``Specifier`` instances.
+
+* Allow testing if a version is contained within a specifier via the ``in``
+ operator.
+
+
+15.1 - 2015-04-13
+~~~~~~~~~~~~~~~~~
+
+* Fix a logic error that was causing inconsistent answers about whether or not
+ a pre-release was contained within a ``SpecifierSet`` or not.
+
+
+15.0 - 2015-01-02
+~~~~~~~~~~~~~~~~~
+
+* Add ``Version().is_postrelease`` and ``LegacyVersion().is_postrelease`` to
+ make it easy to determine if a release is a post release.
+
+* Add ``Version().base_version`` and ``LegacyVersion().base_version`` to make
+ it easy to get the public version without any pre or post release markers.
+
+* Support the update to PEP 440 which removed the implied ``!=V.*`` when using
+ either ``>V`` or ``<V`` and which instead special cased the handling of
+ pre-releases, post-releases, and local versions when using ``>V`` or ``<V``.
+
+
+14.5 - 2014-12-17
+~~~~~~~~~~~~~~~~~
+
+* Normalize release candidates as ``rc`` instead of ``c``.
+
+* Expose the ``VERSION_PATTERN`` constant, a regular expression matching
+ a valid version.
+
+
+14.4 - 2014-12-15
+~~~~~~~~~~~~~~~~~
+
+* Ensure that versions are normalized before comparison when used in a
+ specifier with a less than (``<``) or greater than (``>``) operator.
+
+
+14.3 - 2014-11-19
+~~~~~~~~~~~~~~~~~
+
+* **BACKWARDS INCOMPATIBLE** Refactor specifier support so that it can sanely
+ handle legacy specifiers as well as PEP 440 specifiers.
+
+* **BACKWARDS INCOMPATIBLE** Move the specifier support out of
+ ``packaging.version`` into ``packaging.specifiers``.
+
+
+14.2 - 2014-09-10
+~~~~~~~~~~~~~~~~~
+
+* Add prerelease support to ``Specifier``.
+* Remove the ability to do ``item in Specifier()`` and replace it with
+ ``Specifier().contains(item)`` in order to allow flags that signal if a
+ prerelease should be accepted or not.
+* Add a method ``Specifier().filter()`` which will take an iterable and returns
+ an iterable with items that do not match the specifier filtered out.
+
+
+14.1 - 2014-09-08
+~~~~~~~~~~~~~~~~~
+
+* Allow ``LegacyVersion`` and ``Version`` to be sorted together.
+* Add ``packaging.version.parse()`` to enable easily parsing a version string
+ as either a ``Version`` or a ``LegacyVersion`` depending on it's PEP 440
+ validity.
+
+
+14.0 - 2014-09-05
+~~~~~~~~~~~~~~~~~
+
+* Initial release.
+
+
+.. _`master`: https://github.com/pypa/packaging/
diff --git a/testing/web-platform/tests/tools/third_party/packaging/CONTRIBUTING.rst b/testing/web-platform/tests/tools/third_party/packaging/CONTRIBUTING.rst
new file mode 100644
index 0000000000..d9d70ec047
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/CONTRIBUTING.rst
@@ -0,0 +1,23 @@
+Contributing to packaging
+=========================
+
+As an open source project, packaging welcomes contributions of many forms.
+
+Examples of contributions include:
+
+* Code patches
+* Documentation improvements
+* Bug reports and patch reviews
+
+Extensive contribution guidelines are available in the repository at
+``docs/development/index.rst``, or online at:
+
+https://packaging.pypa.io/en/latest/development/
+
+Security issues
+---------------
+
+To report a security issue, please follow the special `security reporting
+guidelines`_, do not report them in the public issue tracker.
+
+.. _`security reporting guidelines`: https://packaging.pypa.io/en/latest/security/
diff --git a/testing/web-platform/tests/tools/third_party/packaging/LICENSE b/testing/web-platform/tests/tools/third_party/packaging/LICENSE
new file mode 100644
index 0000000000..6f62d44e4e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/LICENSE
@@ -0,0 +1,3 @@
+This software is made available under the terms of *either* of the licenses
+found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made
+under the terms of *both* these licenses.
diff --git a/testing/web-platform/tests/tools/third_party/packaging/LICENSE.APACHE b/testing/web-platform/tests/tools/third_party/packaging/LICENSE.APACHE
new file mode 100644
index 0000000000..f433b1a53f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/LICENSE.APACHE
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/testing/web-platform/tests/tools/third_party/packaging/LICENSE.BSD b/testing/web-platform/tests/tools/third_party/packaging/LICENSE.BSD
new file mode 100644
index 0000000000..42ce7b75c9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/LICENSE.BSD
@@ -0,0 +1,23 @@
+Copyright (c) Donald Stufft and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/testing/web-platform/tests/tools/third_party/packaging/MANIFEST.in b/testing/web-platform/tests/tools/third_party/packaging/MANIFEST.in
new file mode 100644
index 0000000000..a078133d35
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/MANIFEST.in
@@ -0,0 +1,24 @@
+include CHANGELOG.rst CONTRIBUTING.rst README.rst
+include LICENSE LICENSE.APACHE LICENSE.BSD
+
+include .coveragerc
+include .flake8
+include .pre-commit-config.yaml
+include mypy.ini
+
+recursive-include docs *
+recursive-include tests *.py
+recursive-include tests/manylinux hello-world-*
+recursive-include tests/musllinux glibc-*
+recursive-include tests/musllinux musl-*
+
+exclude noxfile.py
+exclude .readthedocs.yml
+exclude .travis.yml
+exclude dev-requirements.txt
+exclude tests/manylinux/build-hello-world.sh
+exclude tests/musllinux/build.sh
+exclude tests/hello-world.c
+
+prune docs/_build
+prune tasks
diff --git a/testing/web-platform/tests/tools/third_party/packaging/README.rst b/testing/web-platform/tests/tools/third_party/packaging/README.rst
new file mode 100644
index 0000000000..e8bebe74dc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/README.rst
@@ -0,0 +1,73 @@
+packaging
+=========
+
+.. start-intro
+
+Reusable core utilities for various Python Packaging
+`interoperability specifications <https://packaging.python.org/specifications/>`_.
+
+This library provides utilities that implement the interoperability
+specifications which have clearly one correct behaviour (eg: :pep:`440`)
+or benefit greatly from having a single shared implementation (eg: :pep:`425`).
+
+.. end-intro
+
+The ``packaging`` project includes the following: version handling, specifiers,
+markers, requirements, tags, utilities.
+
+Documentation
+-------------
+
+The `documentation`_ provides information and the API for the following:
+
+- Version Handling
+- Specifiers
+- Markers
+- Requirements
+- Tags
+- Utilities
+
+Installation
+------------
+
+Use ``pip`` to install these utilities::
+
+ pip install packaging
+
+Discussion
+----------
+
+If you run into bugs, you can file them in our `issue tracker`_.
+
+You can also join ``#pypa`` on Freenode to ask questions or get involved.
+
+
+.. _`documentation`: https://packaging.pypa.io/
+.. _`issue tracker`: https://github.com/pypa/packaging/issues
+
+
+Code of Conduct
+---------------
+
+Everyone interacting in the packaging project's codebases, issue trackers, chat
+rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
+
+.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
+
+Contributing
+------------
+
+The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as
+well as how to report a potential security issue. The documentation for this
+project also covers information about `project development`_ and `security`_.
+
+.. _`project development`: https://packaging.pypa.io/en/latest/development/
+.. _`security`: https://packaging.pypa.io/en/latest/security/
+
+Project History
+---------------
+
+Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for
+recent changes and project history.
+
+.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/Makefile b/testing/web-platform/tests/tools/third_party/packaging/docs/Makefile
new file mode 100644
index 0000000000..9d683b4024
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/Makefile
@@ -0,0 +1,153 @@
+# 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) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+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 " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @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)/*
+
+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/packaging.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/packaging.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/packaging"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/packaging"
+ @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."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+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/testing/web-platform/tests/tools/third_party/packaging/docs/_static/.empty b/testing/web-platform/tests/tools/third_party/packaging/docs/_static/.empty
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/_static/.empty
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/changelog.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/changelog.rst
new file mode 100644
index 0000000000..565b0521d0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/changelog.rst
@@ -0,0 +1 @@
+.. include:: ../CHANGELOG.rst
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/conf.py b/testing/web-platform/tests/tools/third_party/packaging/docs/conf.py
new file mode 100644
index 0000000000..edd8dd5cc7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/conf.py
@@ -0,0 +1,111 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import os
+import sys
+
+# 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 ----------------------------------------------------
+
+# 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.extlinks",
+ "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 = ".rst"
+
+# The master toctree document.
+master_doc = "index"
+
+# General information about the project.
+project = "Packaging"
+
+# 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.
+#
+
+base_dir = os.path.join(os.path.dirname(__file__), os.pardir)
+about = {}
+with open(os.path.join(base_dir, "packaging", "__about__.py")) as f:
+ exec(f.read(), about)
+
+version = release = about["__version__"]
+copyright = about["__copyright__"]
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ["_build"]
+
+extlinks = {
+ "issue": ("https://github.com/pypa/packaging/issues/%s", "#"),
+ "pull": ("https://github.com/pypa/packaging/pull/%s", "PR #"),
+}
+# -- Options for HTML output --------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+
+html_theme = "furo"
+html_title = "packaging"
+
+# 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"]
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = "packagingdoc"
+
+
+# -- Options for LaTeX output -------------------------------------------------
+
+latex_elements = {}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual])
+latex_documents = [
+ ("index", "packaging.tex", "Packaging Documentation", "Donald Stufft", "manual")
+]
+
+# -- Options for manual page output -------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [("index", "packaging", "Packaging Documentation", ["Donald Stufft"], 1)]
+
+# -- Options for Texinfo output -----------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (
+ "index",
+ "packaging",
+ "Packaging Documentation",
+ "Donald Stufft",
+ "packaging",
+ "Core utilities for Python packages",
+ "Miscellaneous",
+ )
+]
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {"https://docs.python.org/": None}
+
+epub_theme = "epub"
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/development/getting-started.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/development/getting-started.rst
new file mode 100644
index 0000000000..8bd42ac089
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/development/getting-started.rst
@@ -0,0 +1,77 @@
+Getting started
+===============
+
+Working on packaging requires the installation of a small number of
+development dependencies. To see what dependencies are required to
+run the tests manually, please look at the ``noxfile.py`` file.
+
+Running tests
+~~~~~~~~~~~~~
+
+The packaging unit tests are found in the ``tests/`` directory and are
+designed to be run using `pytest`_. `pytest`_ will discover the tests
+automatically, so all you have to do is:
+
+.. code-block:: console
+
+ $ python -m pytest
+ ...
+ 29204 passed, 4 skipped, 1 xfailed in 83.98 seconds
+
+This runs the tests with the default Python interpreter. This also allows
+you to run select tests instead of the entire test suite.
+
+You can also verify that the tests pass on other supported Python interpreters.
+For this we use `nox`_, which will automatically create a `virtualenv`_ for
+each supported Python version and run the tests. For example:
+
+.. code-block:: console
+
+ $ nox -s tests
+ ...
+ nox > Ran multiple sessions:
+ nox > * tests-3.6: success
+ nox > * tests-3.7: success
+ nox > * tests-3.8: success
+ nox > * tests-3.9: success
+ nox > * tests-pypy3: skipped
+
+You may not have all the required Python versions installed, in which case you
+will see one or more ``InterpreterNotFound`` errors.
+
+Running linters
+~~~~~~~~~~~~~~~
+
+If you wish to run the linting rules, you may use `pre-commit`_ or run
+``nox -s lint``.
+
+.. code-block:: console
+
+ $ nox -s lint
+ ...
+ nox > Session lint was successful.
+
+Building documentation
+~~~~~~~~~~~~~~~~~~~~~~
+
+packaging documentation is stored in the ``docs/`` directory. It is
+written in `reStructured Text`_ and rendered using `Sphinx`_.
+
+Use `nox`_ to build the documentation. For example:
+
+.. code-block:: console
+
+ $ nox -s docs
+ ...
+ nox > Session docs was successful.
+
+The HTML documentation index can now be found at
+``docs/_build/html/index.html``.
+
+.. _`pytest`: https://pypi.org/project/pytest/
+.. _`nox`: https://pypi.org/project/nox/
+.. _`virtualenv`: https://pypi.org/project/virtualenv/
+.. _`pip`: https://pypi.org/project/pip/
+.. _`sphinx`: https://pypi.org/project/Sphinx/
+.. _`reStructured Text`: http://sphinx-doc.org/rest.html
+.. _`pre-commit`: https://pre-commit.com
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/development/index.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/development/index.rst
new file mode 100644
index 0000000000..c0aea8acb3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/development/index.rst
@@ -0,0 +1,19 @@
+Development
+===========
+
+As an open source project, packaging welcomes contributions of all
+forms. The sections below will help you get started.
+
+File bugs and feature requests on our issue tracker on `GitHub`_. If it is a
+bug check out `what to put in your bug report`_.
+
+.. toctree::
+ :maxdepth: 2
+
+ getting-started
+ submitting-patches
+ reviewing-patches
+ release-process
+
+.. _`GitHub`: https://github.com/pypa/packaging
+.. _`what to put in your bug report`: http://www.contribution-guide.org/#what-to-put-in-your-bug-report
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/development/release-process.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/development/release-process.rst
new file mode 100644
index 0000000000..84e5bec868
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/development/release-process.rst
@@ -0,0 +1,25 @@
+Release Process
+===============
+
+#. Checkout the current ``main`` branch.
+#. Install the latest ``nox``::
+
+ $ pip install nox
+
+#. Run the release automation with the required version number (YY.N)::
+
+ $ nox -s release -- YY.N
+
+ You will need the password for your GPG key as well as an API token for PyPI.
+
+#. Add a `release on GitHub <https://github.com/pypa/packaging/releases>`__.
+
+#. Notify the other project owners of the release.
+
+.. note::
+
+ Access needed for making the release are:
+
+ - PyPI maintainer (or owner) access to ``packaging``
+ - push directly to the ``main`` branch on the source repository
+ - push tags directly to the source repository
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/development/reviewing-patches.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/development/reviewing-patches.rst
new file mode 100644
index 0000000000..c476c7512d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/development/reviewing-patches.rst
@@ -0,0 +1,37 @@
+Reviewing and merging patches
+=============================
+
+Everyone is encouraged to review open pull requests. We only ask that you try
+and think carefully, ask questions and are `excellent to one another`_. Code
+review is our opportunity to share knowledge, design ideas and make friends.
+
+When reviewing a patch try to keep each of these concepts in mind:
+
+Architecture
+------------
+
+* Is the proposed change being made in the correct place?
+
+Intent
+------
+
+* What is the change being proposed?
+* Do we want this feature or is the bug they're fixing really a bug?
+
+Implementation
+--------------
+
+* Does the change do what the author claims?
+* Are there sufficient tests?
+* Has it been documented?
+* Will this change introduce new bugs?
+
+Grammar and style
+-----------------
+
+These are small things that are not caught by the automated style checkers.
+
+* Does a variable need a better name?
+* Should this be a keyword argument?
+
+.. _`excellent to one another`: https://speakerdeck.com/ohrite/better-code-review
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/development/submitting-patches.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/development/submitting-patches.rst
new file mode 100644
index 0000000000..fbdb5a4deb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/development/submitting-patches.rst
@@ -0,0 +1,74 @@
+Submitting patches
+==================
+
+* Always make a new branch for your work.
+* Patches should be small to facilitate easier review. `Studies have shown`_
+ that review quality falls off as patch size grows. Sometimes this will result
+ in many small PRs to land a single large feature.
+* Larger changes should be discussed in a ticket before submission.
+* New features and significant bug fixes should be documented in the
+ :doc:`/changelog`.
+* You must have legal permission to distribute any code you contribute and it
+ must be available under both the BSD and Apache Software License Version 2.0
+ licenses.
+
+If you believe you've identified a security issue in packaging, please
+follow the directions on the :doc:`security page </security>`.
+
+Code
+----
+
+This project's source is auto-formatted with |black|. You can check if your
+code meets our requirements by running our linters against it with ``nox -s
+lint`` or ``pre-commit run --all-files``.
+
+`Write comments as complete sentences.`_
+
+Every code file must start with the boilerplate licensing notice:
+
+.. code-block:: python
+
+ # This file is dual licensed under the terms of the Apache License, Version
+ # 2.0, and the BSD License. See the LICENSE file in the root of this repository
+ # for complete details.
+
+Tests
+-----
+
+All code changes must be accompanied by unit tests with 100% code coverage (as
+measured by the combined metrics across our build matrix).
+
+
+Documentation
+-------------
+
+All features should be documented with prose in the ``docs`` section.
+
+When referring to a hypothetical individual (such as "a person receiving an
+encrypted message") use gender neutral pronouns (they/them/their).
+
+Docstrings are typically only used when writing abstract classes, but should
+be written like this if required:
+
+.. code-block:: python
+
+ def some_function(some_arg):
+ """
+ Does some things.
+
+ :param some_arg: Some argument.
+ """
+
+So, specifically:
+
+* Always use three double quotes.
+* Put the three double quotes on their own line.
+* No blank line at the end.
+* Use Sphinx parameter/attribute documentation `syntax`_.
+
+
+.. |black| replace:: ``black``
+.. _black: https://pypi.org/project/black/
+.. _`Write comments as complete sentences.`: https://nedbatchelder.com/blog/201401/comments_should_be_sentences.html
+.. _`syntax`: http://sphinx-doc.org/domains.html#info-field-lists
+.. _`Studies have shown`: http://www.ibm.com/developerworks/rational/library/11-proven-practices-for-peer-review/
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/index.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/index.rst
new file mode 100644
index 0000000000..aafdae83c4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/index.rst
@@ -0,0 +1,38 @@
+Welcome to packaging
+====================
+
+.. include:: ../README.rst
+ :start-after: start-intro
+ :end-before: end-intro
+
+
+Installation
+------------
+
+You can install packaging with ``pip``:
+
+.. code-block:: console
+
+ $ pip install packaging
+
+
+.. toctree::
+ :maxdepth: 1
+ :caption: API Documentation
+ :hidden:
+
+ version
+ specifiers
+ markers
+ requirements
+ tags
+ utils
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Project
+ :hidden:
+
+ development/index
+ security
+ changelog
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/markers.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/markers.rst
new file mode 100644
index 0000000000..ad25361647
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/markers.rst
@@ -0,0 +1,93 @@
+Markers
+=======
+
+.. currentmodule:: packaging.markers
+
+One extra requirement of dealing with dependencies is the ability to specify
+if it is required depending on the operating system or Python version in use.
+`PEP 508`_ defines the scheme which has been implemented by this module.
+
+Usage
+-----
+
+.. doctest::
+
+ >>> from packaging.markers import Marker, UndefinedEnvironmentName
+ >>> marker = Marker("python_version>'2'")
+ >>> marker
+ <Marker('python_version > "2"')>
+ >>> # We can evaluate the marker to see if it is satisfied
+ >>> marker.evaluate()
+ True
+ >>> # We can also override the environment
+ >>> env = {'python_version': '1.5.4'}
+ >>> marker.evaluate(environment=env)
+ False
+ >>> # Multiple markers can be ANDed
+ >>> and_marker = Marker("os_name=='a' and os_name=='b'")
+ >>> and_marker
+ <Marker('os_name == "a" and os_name == "b"')>
+ >>> # Multiple markers can be ORed
+ >>> or_marker = Marker("os_name=='a' or os_name=='b'")
+ >>> or_marker
+ <Marker('os_name == "a" or os_name == "b"')>
+ >>> # Markers can be also used with extras, to pull in dependencies if
+ >>> # a certain extra is being installed
+ >>> extra = Marker('extra == "bar"')
+ >>> # Evaluating an extra marker with no environment is an error
+ >>> try:
+ ... extra.evaluate()
+ ... except UndefinedEnvironmentName:
+ ... pass
+ >>> extra_environment = {'extra': ''}
+ >>> extra.evaluate(environment=extra_environment)
+ False
+ >>> extra_environment['extra'] = 'bar'
+ >>> extra.evaluate(environment=extra_environment)
+ True
+
+
+Reference
+---------
+
+.. class:: Marker(markers)
+
+ This class abstracts handling markers for dependencies of a project. It can
+ be passed a single marker or multiple markers that are ANDed or ORed
+ together. Each marker will be parsed according to PEP 508.
+
+ :param str markers: The string representation of a marker or markers.
+ :raises InvalidMarker: If the given ``markers`` are not parseable, then
+ this exception will be raised.
+
+ .. method:: evaluate(environment=None)
+
+ Evaluate the marker given the context of the current Python process.
+
+ :param dict environment: A dictionary containing keys and values to
+ override the detected environment.
+ :raises: UndefinedComparison: If the marker uses a PEP 440 comparison on
+ strings which are not valid PEP 440 versions.
+ :raises: UndefinedEnvironmentName: If the marker accesses a value that
+ isn't present inside of the environment
+ dictionary.
+
+.. exception:: InvalidMarker
+
+ Raised when attempting to create a :class:`Marker` with a string that
+ does not conform to PEP 508.
+
+
+.. exception:: UndefinedComparison
+
+ Raised when attempting to evaluate a :class:`Marker` with a PEP 440
+ comparison operator against values that are not valid PEP 440 versions.
+
+
+.. exception:: UndefinedEnvironmentName
+
+ Raised when attempting to evaluate a :class:`Marker` with a value that is
+ missing from the evaluation environment.
+
+
+.. _`PEP 508`: https://www.python.org/dev/peps/pep-0508/
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/requirements.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/requirements.rst
new file mode 100644
index 0000000000..e7c5a85a5e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/requirements.rst
@@ -0,0 +1,89 @@
+Requirements
+============
+
+.. currentmodule:: packaging.requirements
+
+Parse a given requirements line for specifying dependencies of a Python
+project, using `PEP 508`_ which defines the scheme that has been implemented
+by this module.
+
+Usage
+-----
+
+.. doctest::
+
+ >>> from packaging.requirements import Requirement
+ >>> simple_req = Requirement("name")
+ >>> simple_req
+ <Requirement('name')>
+ >>> simple_req.name
+ 'name'
+ >>> simple_req.url is None
+ True
+ >>> simple_req.extras
+ set()
+ >>> simple_req.specifier
+ <SpecifierSet('')>
+ >>> simple_req.marker is None
+ True
+ >>> # Requirements can be specified with extras, specifiers and markers
+ >>> req = Requirement('name[foo]>=2,<3; python_version>"2.0"')
+ >>> req.name
+ 'name'
+ >>> req.extras
+ {'foo'}
+ >>> req.specifier
+ <SpecifierSet('<3,>=2')>
+ >>> req.marker
+ <Marker('python_version > "2.0"')>
+ >>> # Requirements can also be specified with a URL, but may not specify
+ >>> # a version.
+ >>> url_req = Requirement('name @ https://github.com/pypa ;os_name=="a"')
+ >>> url_req.name
+ 'name'
+ >>> url_req.url
+ 'https://github.com/pypa'
+ >>> url_req.extras
+ set()
+ >>> url_req.marker
+ <Marker('os_name == "a"')>
+
+
+Reference
+---------
+
+.. class:: Requirement(requirement)
+
+ This class abstracts handling the details of a requirement for a project.
+ Each requirement will be parsed according to PEP 508.
+
+ :param str requirement: The string representation of a requirement.
+ :raises InvalidRequirement: If the given ``requirement`` is not parseable,
+ then this exception will be raised.
+
+ .. attribute:: name
+
+ The name of the requirement.
+
+ .. attribute:: url
+
+ The URL, if any where to download the requirement from. Can be None.
+
+ .. attribute:: extras
+
+ A set of extras that the requirement specifies.
+
+ .. attribute:: specifier
+
+ A :class:`~.SpecifierSet` of the version specified by the requirement.
+
+ .. attribute:: marker
+
+ A :class:`~.Marker` of the marker for the requirement. Can be None.
+
+.. exception:: InvalidRequirement
+
+ Raised when attempting to create a :class:`Requirement` with a string that
+ does not conform to PEP 508.
+
+.. _`PEP 508`: https://www.python.org/dev/peps/pep-0508/
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/requirements.txt b/testing/web-platform/tests/tools/third_party/packaging/docs/requirements.txt
new file mode 100644
index 0000000000..a95ae18b4f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/requirements.txt
@@ -0,0 +1 @@
+furo
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/security.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/security.rst
new file mode 100644
index 0000000000..f7fdb00029
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/security.rst
@@ -0,0 +1,18 @@
+Security
+========
+
+We take the security of packaging seriously. If you believe you've identified a
+security issue in it, DO NOT report the issue in any public forum, including
+(but not limited to):
+
+- GitHub issue tracker
+- Official or unofficial chat channels
+- Official or unofficial mailing lists
+
+Please report your issue to ``security@python.org``. Messages may be optionally
+encrypted with GPG using key fingerprints available at the `Python Security
+page <https://www.python.org/news/security/>`_.
+
+Once you've submitted an issue via email, you should receive an acknowledgment
+within 48 hours, and depending on the action to be taken, you may receive
+further follow-up emails.
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/specifiers.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/specifiers.rst
new file mode 100644
index 0000000000..83299a8a70
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/specifiers.rst
@@ -0,0 +1,222 @@
+Specifiers
+==========
+
+.. currentmodule:: packaging.specifiers
+
+A core requirement of dealing with dependencies is the ability to specify what
+versions of a dependency are acceptable for you. `PEP 440`_ defines the
+standard specifier scheme which has been implemented by this module.
+
+Usage
+-----
+
+.. doctest::
+
+ >>> from packaging.specifiers import SpecifierSet
+ >>> from packaging.version import Version
+ >>> spec1 = SpecifierSet("~=1.0")
+ >>> spec1
+ <SpecifierSet('~=1.0')>
+ >>> spec2 = SpecifierSet(">=1.0")
+ >>> spec2
+ <SpecifierSet('>=1.0')>
+ >>> # We can combine specifiers
+ >>> combined_spec = spec1 & spec2
+ >>> combined_spec
+ <SpecifierSet('>=1.0,~=1.0')>
+ >>> # We can also implicitly combine a string specifier
+ >>> combined_spec &= "!=1.1"
+ >>> combined_spec
+ <SpecifierSet('!=1.1,>=1.0,~=1.0')>
+ >>> # Create a few versions to check for contains.
+ >>> v1 = Version("1.0a5")
+ >>> v2 = Version("1.0")
+ >>> # We can check a version object to see if it falls within a specifier
+ >>> v1 in combined_spec
+ False
+ >>> v2 in combined_spec
+ True
+ >>> # We can even do the same with a string based version
+ >>> "1.4" in combined_spec
+ True
+ >>> # Finally we can filter a list of versions to get only those which are
+ >>> # contained within our specifier.
+ >>> list(combined_spec.filter([v1, v2, "1.4"]))
+ [<Version('1.0')>, '1.4']
+
+
+Reference
+---------
+
+.. class:: SpecifierSet(specifiers="", prereleases=None)
+
+ This class abstracts handling specifying the dependencies of a project. It
+ can be passed a single specifier (``>=3.0``), a comma-separated list of
+ specifiers (``>=3.0,!=3.1``), or no specifier at all. Each individual
+ specifier will be attempted to be parsed as a PEP 440 specifier
+ (:class:`Specifier`) or as a legacy, setuptools style specifier
+ (deprecated :class:`LegacySpecifier`). You may combine
+ :class:`SpecifierSet` instances using the ``&`` operator
+ (``SpecifierSet(">2") & SpecifierSet("<4")``).
+
+ Both the membership tests and the combination support using raw strings
+ in place of already instantiated objects.
+
+ :param str specifiers: The string representation of a specifier or a
+ comma-separated list of specifiers which will
+ be parsed and normalized before use.
+ :param bool prereleases: This tells the SpecifierSet if it should accept
+ prerelease versions if applicable or not. The
+ default of ``None`` will autodetect it from the
+ given specifiers.
+ :raises InvalidSpecifier: If the given ``specifiers`` are not parseable
+ than this exception will be raised.
+
+ .. attribute:: prereleases
+
+ A boolean value indicating whether this :class:`SpecifierSet`
+ represents a specifier that includes a pre-release versions. This can be
+ set to either ``True`` or ``False`` to explicitly enable or disable
+ prereleases or it can be set to ``None`` (the default) to enable
+ autodetection.
+
+ .. method:: __contains__(version)
+
+ This is the more Pythonic version of :meth:`contains()`, but does
+ not allow you to override the ``prereleases`` argument. If you
+ need that, use :meth:`contains()`.
+
+ See :meth:`contains()`.
+
+ .. method:: contains(version, prereleases=None)
+
+ Determines if ``version``, which can be either a version string, a
+ :class:`Version`, or a deprecated :class:`LegacyVersion` object, is
+ contained within this set of specifiers.
+
+ This will either match or not match prereleases based on the
+ ``prereleases`` parameter. When ``prereleases`` is set to ``None``
+ (the default) it will use the ``Specifier().prereleases`` attribute to
+ determine if to allow them. Otherwise it will use the boolean value of
+ the passed in value to determine if to allow them or not.
+
+ .. method:: __len__()
+
+ Returns the number of specifiers in this specifier set.
+
+ .. method:: __iter__()
+
+ Returns an iterator over all the underlying :class:`Specifier` (or
+ deprecated :class:`LegacySpecifier`) instances in this specifier set.
+
+ .. method:: filter(iterable, prereleases=None)
+
+ Takes an iterable that can contain version strings, :class:`~.Version`,
+ and deprecated :class:`~.LegacyVersion` instances and will then filter
+ it, returning an iterable that contains only items which match the
+ rules of this specifier object.
+
+ This method is smarter than just
+ ``filter(Specifier().contains, [...])`` because it implements the rule
+ from PEP 440 where a prerelease item SHOULD be accepted if no other
+ versions match the given specifier.
+
+ The ``prereleases`` parameter functions similarly to that of the same
+ parameter in ``contains``. If the value is ``None`` (the default) then
+ it will intelligently decide if to allow prereleases based on the
+ specifier, the ``Specifier().prereleases`` value, and the PEP 440
+ rules. Otherwise it will act as a boolean which will enable or disable
+ all prerelease versions from being included.
+
+
+.. class:: Specifier(specifier, prereleases=None)
+
+ This class abstracts the handling of a single `PEP 440`_ compatible
+ specifier. It is generally not required to instantiate this manually,
+ preferring instead to work with :class:`SpecifierSet`.
+
+ :param str specifier: The string representation of a specifier which will
+ be parsed and normalized before use.
+ :param bool prereleases: This tells the specifier if it should accept
+ prerelease versions if applicable or not. The
+ default of ``None`` will autodetect it from the
+ given specifiers.
+ :raises InvalidSpecifier: If the ``specifier`` does not conform to PEP 440
+ in any way then this exception will be raised.
+
+ .. attribute:: operator
+
+ The string value of the operator part of this specifier.
+
+ .. attribute:: version
+
+ The string version of the version part of this specifier.
+
+ .. attribute:: prereleases
+
+ See :attr:`SpecifierSet.prereleases`.
+
+ .. method:: __contains__(version)
+
+ See :meth:`SpecifierSet.__contains__()`.
+
+ .. method:: contains(version, prereleases=None)
+
+ See :meth:`SpecifierSet.contains()`.
+
+ .. method:: filter(iterable, prereleases=None)
+
+ See :meth:`SpecifierSet.filter()`.
+
+
+.. class:: LegacySpecifier(specifier, prereleases=None)
+
+ .. deprecated:: 20.5
+
+ Use :class:`Specifier` instead.
+
+ This class abstracts the handling of a single legacy, setuptools style
+ specifier. It is generally not required to instantiate this manually,
+ preferring instead to work with :class:`SpecifierSet`.
+
+ :param str specifier: The string representation of a specifier which will
+ be parsed and normalized before use.
+ :param bool prereleases: This tells the specifier if it should accept
+ prerelease versions if applicable or not. The
+ default of ``None`` will autodetect it from the
+ given specifiers.
+ :raises InvalidSpecifier: If the ``specifier`` is not parseable then this
+ will be raised.
+
+ .. attribute:: operator
+
+ The string value of the operator part of this specifier.
+
+ .. attribute:: version
+
+ The string version of the version part of this specifier.
+
+ .. attribute:: prereleases
+
+ See :attr:`SpecifierSet.prereleases`.
+
+ .. method:: __contains__(version)
+
+ See :meth:`SpecifierSet.__contains__()`.
+
+ .. method:: contains(version, prereleases=None)
+
+ See :meth:`SpecifierSet.contains()`.
+
+ .. method:: filter(iterable, prereleases=None)
+
+ See :meth:`SpecifierSet.filter()`.
+
+
+.. exception:: InvalidSpecifier
+
+ Raised when attempting to create a :class:`Specifier` with a specifier
+ string that does not conform to `PEP 440`_.
+
+
+.. _`PEP 440`: https://www.python.org/dev/peps/pep-0440/
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/tags.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/tags.rst
new file mode 100644
index 0000000000..ecd613b5aa
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/tags.rst
@@ -0,0 +1,225 @@
+Tags
+====
+
+.. currentmodule:: packaging.tags
+
+Wheels encode the Python interpreter, ABI, and platform that they support in
+their filenames using `platform compatibility tags`_. This module provides
+support for both parsing these tags as well as discovering what tags the
+running Python interpreter supports.
+
+Usage
+-----
+
+.. doctest::
+
+ >>> from packaging.tags import Tag, sys_tags
+ >>> import sys
+ >>> looking_for = Tag("py{major}".format(major=sys.version_info.major), "none", "any")
+ >>> supported_tags = list(sys_tags())
+ >>> looking_for in supported_tags
+ True
+ >>> really_old = Tag("py1", "none", "any")
+ >>> wheels = {really_old, looking_for}
+ >>> best_wheel = None
+ >>> for supported_tag in supported_tags:
+ ... for wheel_tag in wheels:
+ ... if supported_tag == wheel_tag:
+ ... best_wheel = wheel_tag
+ ... break
+ >>> best_wheel == looking_for
+ True
+
+Reference
+---------
+
+High Level Interface
+''''''''''''''''''''
+
+The following functions are the main interface to the library, and are typically the only
+items that applications should need to reference, in order to parse and check tags.
+
+.. class:: Tag(interpreter, abi, platform)
+
+ A representation of the tag triple for a wheel. Instances are considered
+ immutable and thus are hashable. Equality checking is also supported.
+
+ :param str interpreter: The interpreter name, e.g. ``"py"``
+ (see :attr:`INTERPRETER_SHORT_NAMES` for mapping
+ well-known interpreter names to their short names).
+ :param str abi: The ABI that a wheel supports, e.g. ``"cp37m"``.
+ :param str platform: The OS/platform the wheel supports,
+ e.g. ``"win_amd64"``.
+
+ .. attribute:: interpreter
+
+ The interpreter name.
+
+ .. attribute:: abi
+
+ The supported ABI.
+
+ .. attribute:: platform
+
+ The OS/platform.
+
+
+.. function:: parse_tag(tag)
+
+ Parses the provided ``tag`` into a set of :class:`Tag` instances.
+
+ Returning a set is required due to the possibility that the tag is a
+ `compressed tag set`_, e.g. ``"py2.py3-none-any"`` which supports both
+ Python 2 and Python 3.
+
+ :param str tag: The tag to parse, e.g. ``"py3-none-any"``.
+
+
+.. function:: sys_tags(*, warn=False)
+
+ Yields the tags that the running interpreter supports.
+
+ The iterable is ordered so that the best-matching tag is first in the
+ sequence. The exact preferential order to tags is interpreter-specific, but
+ in general the tag importance is in the order of:
+
+ 1. Interpreter
+ 2. Platform
+ 3. ABI
+
+ This order is due to the fact that an ABI is inherently tied to the
+ platform, but platform-specific code is not necessarily tied to the ABI. The
+ interpreter is the most important tag as it dictates basic support for any
+ wheel.
+
+ The function returns an iterable in order to allow for the possible
+ short-circuiting of tag generation if the entire sequence is not necessary
+ and tag calculation happens to be expensive.
+
+ :param bool warn: Whether warnings should be logged. Defaults to ``False``.
+
+
+Low Level Interface
+'''''''''''''''''''
+
+The following functions are low-level implementation details. They should typically not
+be needed in application code, unless the application has specialised requirements (for
+example, constructing sets of supported tags for environments other than the running
+interpreter).
+
+These functions capture the precise details of which environments support which tags. That
+information is not defined in the compatibility tag standards but is noted as being up
+to the implementation to provide.
+
+
+.. attribute:: INTERPRETER_SHORT_NAMES
+
+ A dictionary mapping interpreter names to their `abbreviation codes`_
+ (e.g. ``"cpython"`` is ``"cp"``). All interpreter names are lower-case.
+
+
+.. function:: interpreter_name()
+
+ Returns the running interpreter's name.
+
+ This typically acts as the prefix to the :attr:`~Tag.interpreter` tag.
+
+
+.. function:: interpreter_version(*, warn=False)
+
+ Returns the running interpreter's version.
+
+ This typically acts as the suffix to the :attr:`~Tag.interpreter` tag.
+
+
+.. function:: mac_platforms(version=None, arch=None)
+
+ Yields the :attr:`~Tag.platform` tags for macOS.
+
+ :param tuple version: A two-item tuple presenting the version of macOS.
+ Defaults to the current system's version.
+ :param str arch: The CPU architecture. Defaults to the architecture of the
+ current system, e.g. ``"x86_64"``.
+
+ .. note::
+ Equivalent support for the other major platforms is purposefully not
+ provided:
+
+ - On Windows, platform compatibility is statically specified
+ - On Linux, code must be run on the system itself to determine
+ compatibility
+
+
+.. function:: platform_tags(version=None, arch=None)
+
+ Yields the :attr:`~Tag.platform` tags for the running interpreter.
+
+
+.. function:: compatible_tags(python_version=None, interpreter=None, platforms=None)
+
+ Yields the tags for an interpreter compatible with the Python version
+ specified by ``python_version``.
+
+ The specific tags generated are:
+
+ - ``py*-none-<platform>``
+ - ``<interpreter>-none-any`` if ``interpreter`` is provided
+ - ``py*-none-any``
+
+ :param Sequence python_version: A one- or two-item sequence representing the
+ compatible version of Python. Defaults to
+ ``sys.version_info[:2]``.
+ :param str interpreter: The name of the interpreter (if known), e.g.
+ ``"cp38"``. Defaults to the current interpreter.
+ :param Iterable platforms: Iterable of compatible platforms. Defaults to the
+ platforms compatible with the current system.
+
+.. function:: cpython_tags(python_version=None, abis=None, platforms=None, *, warn=False)
+
+ Yields the tags for the CPython interpreter.
+
+ The specific tags generated are:
+
+ - ``cp<python_version>-<abi>-<platform>``
+ - ``cp<python_version>-abi3-<platform>``
+ - ``cp<python_version>-none-<platform>``
+ - ``cp<older version>-abi3-<platform>`` where "older version" is all older
+ minor versions down to Python 3.2 (when ``abi3`` was introduced)
+
+ If ``python_version`` only provides a major-only version then only
+ user-provided ABIs via ``abis`` and the ``none`` ABI will be used.
+
+ :param Sequence python_version: A one- or two-item sequence representing the
+ targeted Python version. Defaults to
+ ``sys.version_info[:2]``.
+ :param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs
+ compatible with the current system.
+ :param Iterable platforms: Iterable of compatible platforms. Defaults to the
+ platforms compatible with the current system.
+ :param bool warn: Whether warnings should be logged. Defaults to ``False``.
+
+.. function:: generic_tags(interpreter=None, abis=None, platforms=None, *, warn=False)
+
+ Yields the tags for an interpreter which requires no specialization.
+
+ This function should be used if one of the other interpreter-specific
+ functions provided by this module is not appropriate (i.e. not calculating
+ tags for a CPython interpreter).
+
+ The specific tags generated are:
+
+ - ``<interpreter>-<abi>-<platform>``
+
+ The ``"none"`` ABI will be added if it was not explicitly provided.
+
+ :param str interpreter: The name of the interpreter. Defaults to being
+ calculated.
+ :param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs
+ compatible with the current system.
+ :param Iterable platforms: Iterable of compatible platforms. Defaults to the
+ platforms compatible with the current system.
+ :param bool warn: Whether warnings should be logged. Defaults to ``False``.
+
+.. _`abbreviation codes`: https://www.python.org/dev/peps/pep-0425/#python-tag
+.. _`compressed tag set`: https://www.python.org/dev/peps/pep-0425/#compressed-tag-sets
+.. _`platform compatibility tags`: https://packaging.python.org/specifications/platform-compatibility-tags/
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/utils.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/utils.rst
new file mode 100644
index 0000000000..8fbb0250b7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/utils.rst
@@ -0,0 +1,92 @@
+Utilities
+=========
+
+.. currentmodule:: packaging.utils
+
+
+A set of small, helper utilities for dealing with Python packages.
+
+
+Reference
+---------
+
+.. function:: canonicalize_name(name)
+
+ This function takes a valid Python package name, and returns the normalized
+ form of it.
+
+ :param str name: The name to normalize.
+
+ .. doctest::
+
+ >>> from packaging.utils import canonicalize_name
+ >>> canonicalize_name("Django")
+ 'django'
+ >>> canonicalize_name("oslo.concurrency")
+ 'oslo-concurrency'
+ >>> canonicalize_name("requests")
+ 'requests'
+
+.. function:: canonicalize_version(version)
+
+ This function takes a string representing a package version (or a
+ :class:`~packaging.version.Version` instance), and returns the
+ normalized form of it.
+
+ :param str version: The version to normalize.
+
+ .. doctest::
+
+ >>> from packaging.utils import canonicalize_version
+ >>> canonicalize_version('1.4.0.0.0')
+ '1.4'
+
+.. function:: parse_wheel_filename(filename)
+
+ This function takes the filename of a wheel file, and parses it,
+ returning a tuple of name, version, build number, and tags.
+
+ The name part of the tuple is normalized. The version portion is an
+ instance of :class:`~packaging.version.Version`. The build number
+ is ``()`` if there is no build number in the wheel filename,
+ otherwise a two-item tuple of an integer for the leading digits and
+ a string for the rest of the build number. The tags portion is an
+ instance of :class:`~packaging.tags.Tag`.
+
+ :param str filename: The name of the wheel file.
+
+ .. doctest::
+
+ >>> from packaging.utils import parse_wheel_filename
+ >>> from packaging.tags import Tag
+ >>> from packaging.version import Version
+ >>> name, ver, build, tags = parse_wheel_filename("foo-1.0-py3-none-any.whl")
+ >>> name
+ 'foo'
+ >>> ver == Version('1.0')
+ True
+ >>> tags == {Tag("py3", "none", "any")}
+ True
+ >>> not build
+ True
+
+.. function:: parse_sdist_filename(filename)
+
+ This function takes the filename of a sdist file (as specified
+ in the `Source distribution format`_ documentation), and parses
+ it, returning a tuple of the normalized name and version as
+ represented by an instance of :class:`~packaging.version.Version`.
+
+ :param str filename: The name of the sdist file.
+
+ .. doctest::
+
+ >>> from packaging.utils import parse_sdist_filename
+ >>> from packaging.version import Version
+ >>> name, ver = parse_sdist_filename("foo-1.0.tar.gz")
+ >>> name
+ 'foo'
+ >>> ver == Version('1.0')
+ True
+
+.. _Source distribution format: https://packaging.python.org/specifications/source-distribution-format/#source-distribution-file-name
diff --git a/testing/web-platform/tests/tools/third_party/packaging/docs/version.rst b/testing/web-platform/tests/tools/third_party/packaging/docs/version.rst
new file mode 100644
index 0000000000..a43cf7868e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/docs/version.rst
@@ -0,0 +1,292 @@
+Version Handling
+================
+
+.. currentmodule:: packaging.version
+
+A core requirement of dealing with packages is the ability to work with
+versions. `PEP 440`_ defines the standard version scheme for Python packages
+which has been implemented by this module.
+
+Usage
+-----
+
+.. doctest::
+
+ >>> from packaging.version import Version, parse
+ >>> v1 = parse("1.0a5")
+ >>> v2 = Version("1.0")
+ >>> v1
+ <Version('1.0a5')>
+ >>> v2
+ <Version('1.0')>
+ >>> v1 < v2
+ True
+ >>> v1.epoch
+ 0
+ >>> v1.release
+ (1, 0)
+ >>> v1.pre
+ ('a', 5)
+ >>> v1.is_prerelease
+ True
+ >>> v2.is_prerelease
+ False
+ >>> Version("french toast")
+ Traceback (most recent call last):
+ ...
+ InvalidVersion: Invalid version: 'french toast'
+ >>> Version("1.0").post
+ >>> Version("1.0").is_postrelease
+ False
+ >>> Version("1.0.post0").post
+ 0
+ >>> Version("1.0.post0").is_postrelease
+ True
+
+
+Reference
+---------
+
+.. function:: parse(version)
+
+ This function takes a version string and will parse it as a
+ :class:`Version` if the version is a valid PEP 440 version, otherwise it
+ will parse it as a deprecated :class:`LegacyVersion`.
+
+
+.. class:: Version(version)
+
+ This class abstracts handling of a project's versions. It implements the
+ scheme defined in `PEP 440`_. A :class:`Version` instance is comparison
+ aware and can be compared and sorted using the standard Python interfaces.
+
+ :param str version: The string representation of a version which will be
+ parsed and normalized before use.
+ :raises InvalidVersion: If the ``version`` does not conform to PEP 440 in
+ any way then this exception will be raised.
+
+ .. attribute:: public
+
+ A string representing the public version portion of this ``Version()``.
+
+ .. attribute:: base_version
+
+ A string representing the base version of this :class:`Version`
+ instance. The base version is the public version of the project without
+ any pre or post release markers.
+
+ .. attribute:: epoch
+
+ An integer giving the version epoch of this :class:`Version` instance
+
+ .. attribute:: release
+
+ A tuple of integers giving the components of the release segment of
+ this :class:`Version` instance; that is, the ``1.2.3`` part of the
+ version number, including trailing zeroes but not including the epoch
+ or any prerelease/development/postrelease suffixes
+
+ .. attribute:: major
+
+ An integer representing the first item of :attr:`release` or ``0`` if unavailable.
+
+ .. attribute:: minor
+
+ An integer representing the second item of :attr:`release` or ``0`` if unavailable.
+
+ .. attribute:: micro
+
+ An integer representing the third item of :attr:`release` or ``0`` if unavailable.
+
+ .. attribute:: local
+
+ A string representing the local version portion of this ``Version()``
+ if it has one, or ``None`` otherwise.
+
+ .. attribute:: pre
+
+ If this :class:`Version` instance represents a prerelease, this
+ attribute will be a pair of the prerelease phase (the string ``"a"``,
+ ``"b"``, or ``"rc"``) and the prerelease number (an integer). If this
+ instance is not a prerelease, the attribute will be `None`.
+
+ .. attribute:: is_prerelease
+
+ A boolean value indicating whether this :class:`Version` instance
+ represents a prerelease and/or development release.
+
+ .. attribute:: dev
+
+ If this :class:`Version` instance represents a development release,
+ this attribute will be the development release number (an integer);
+ otherwise, it will be `None`.
+
+ .. attribute:: is_devrelease
+
+ A boolean value indicating whether this :class:`Version` instance
+ represents a development release.
+
+ .. attribute:: post
+
+ If this :class:`Version` instance represents a postrelease, this
+ attribute will be the postrelease number (an integer); otherwise, it
+ will be `None`.
+
+ .. attribute:: is_postrelease
+
+ A boolean value indicating whether this :class:`Version` instance
+ represents a post-release.
+
+
+.. class:: LegacyVersion(version)
+
+ .. deprecated:: 20.5
+
+ Use :class:`Version` instead.
+
+ This class abstracts handling of a project's versions if they are not
+ compatible with the scheme defined in `PEP 440`_. It implements a similar
+ interface to that of :class:`Version`.
+
+ This class implements the previous de facto sorting algorithm used by
+ setuptools, however it will always sort as less than a :class:`Version`
+ instance.
+
+ :param str version: The string representation of a version which will be
+ used as is.
+
+ .. note::
+
+ :class:`LegacyVersion` instances are always ordered lower than :class:`Version` instances.
+
+ >>> from packaging.version import Version, LegacyVersion
+ >>> v1 = Version("1.0")
+ >>> v2 = LegacyVersion("1.0")
+ >>> v1 > v2
+ True
+ >>> v3 = LegacyVersion("1.3")
+ >>> v1 > v3
+ True
+
+ Also note that some strings are still valid PEP 440 strings (:class:`Version`), even if they look very similar to
+ other versions that are not (:class:`LegacyVersion`). Examples include versions with `Pre-release spelling`_ and
+ `Post-release spelling`_.
+
+ >>> from packaging.version import parse
+ >>> v1 = parse('0.9.8a')
+ >>> v2 = parse('0.9.8beta')
+ >>> v3 = parse('0.9.8r')
+ >>> v4 = parse('0.9.8rev')
+ >>> v5 = parse('0.9.8t')
+ >>> v1
+ <Version('0.9.8a0')>
+ >>> v1.is_prerelease
+ True
+ >>> v2
+ <Version('0.9.8b0')>
+ >>> v2.is_prerelease
+ True
+ >>> v3
+ <Version('0.9.8.post0')>
+ >>> v3.is_postrelease
+ True
+ >>> v4
+ <Version('0.9.8.post0')>
+ >>> v4.is_postrelease
+ True
+ >>> v5
+ <LegacyVersion('0.9.8t')>
+ >>> v5.is_prerelease
+ False
+ >>> v5.is_postrelease
+ False
+
+ .. attribute:: public
+
+ A string representing the public version portion of this
+ :class:`LegacyVersion`. This will always be the entire version string.
+
+ .. attribute:: base_version
+
+ A string representing the base version portion of this
+ :class:`LegacyVersion` instance. This will always be the entire version
+ string.
+
+ .. attribute:: epoch
+
+ This will always be ``-1`` since without `PEP 440`_ we do not have the
+ concept of version epochs. The value reflects the fact that
+ :class:`LegacyVersion` instances always compare less than
+ :class:`Version` instances.
+
+ .. attribute:: release
+
+ This will always be ``None`` since without `PEP 440`_ we do not have
+ the concept of a release segment or its components. It exists
+ primarily to allow a :class:`LegacyVersion` to be used as a stand in
+ for a :class:`Version`.
+
+ .. attribute:: local
+
+ This will always be ``None`` since without `PEP 440`_ we do not have
+ the concept of a local version. It exists primarily to allow a
+ :class:`LegacyVersion` to be used as a stand in for a :class:`Version`.
+
+ .. attribute:: pre
+
+ This will always be ``None`` since without `PEP 440`_ we do not have
+ the concept of a prerelease. It exists primarily to allow a
+ :class:`LegacyVersion` to be used as a stand in for a :class:`Version`.
+
+ .. attribute:: is_prerelease
+
+ A boolean value indicating whether this :class:`LegacyVersion`
+ represents a prerelease and/or development release. Since without
+ `PEP 440`_ there is no concept of pre or dev releases this will
+ always be `False` and exists for compatibility with :class:`Version`.
+
+ .. attribute:: dev
+
+ This will always be ``None`` since without `PEP 440`_ we do not have
+ the concept of a development release. It exists primarily to allow a
+ :class:`LegacyVersion` to be used as a stand in for a :class:`Version`.
+
+ .. attribute:: is_devrelease
+
+ A boolean value indicating whether this :class:`LegacyVersion`
+ represents a development release. Since without `PEP 440`_ there is
+ no concept of dev releases this will always be `False` and exists for
+ compatibility with :class:`Version`.
+
+ .. attribute:: post
+
+ This will always be ``None`` since without `PEP 440`_ we do not have
+ the concept of a postrelease. It exists primarily to allow a
+ :class:`LegacyVersion` to be used as a stand in for a :class:`Version`.
+
+ .. attribute:: is_postrelease
+
+ A boolean value indicating whether this :class:`LegacyVersion`
+ represents a post-release. Since without `PEP 440`_ there is no concept
+ of post-releases this will always be ``False`` and exists for
+ compatibility with :class:`Version`.
+
+
+.. exception:: InvalidVersion
+
+ Raised when attempting to create a :class:`Version` with a version string
+ that does not conform to `PEP 440`_.
+
+
+.. data:: VERSION_PATTERN
+
+ A string containing the regular expression used to match a valid version.
+ The pattern is not anchored at either end, and is intended for embedding
+ in larger expressions (for example, matching a version number as part of
+ a file name). The regular expression should be compiled with the
+ ``re.VERBOSE`` and ``re.IGNORECASE`` flags set.
+
+
+.. _PEP 440: https://www.python.org/dev/peps/pep-0440/
+.. _Pre-release spelling : https://www.python.org/dev/peps/pep-0440/#pre-release-spelling
+.. _Post-release spelling : https://www.python.org/dev/peps/pep-0440/#post-release-spelling
diff --git a/testing/web-platform/tests/tools/third_party/packaging/mypy.ini b/testing/web-platform/tests/tools/third_party/packaging/mypy.ini
new file mode 100644
index 0000000000..d88ab8f164
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/mypy.ini
@@ -0,0 +1,17 @@
+[mypy]
+ignore_missing_imports = True
+
+# The following are the flags enabled by --strict
+warn_unused_configs = True
+disallow_subclassing_any = True
+disallow_any_generics = True
+disallow_untyped_calls = True
+disallow_untyped_defs = True
+disallow_incomplete_defs = True
+check_untyped_defs = True
+disallow_untyped_decorators = True
+no_implicit_optional = True
+warn_redundant_casts = True
+warn_unused_ignores = True
+warn_return_any = True
+no_implicit_reexport = True
diff --git a/testing/web-platform/tests/tools/third_party/packaging/noxfile.py b/testing/web-platform/tests/tools/third_party/packaging/noxfile.py
new file mode 100644
index 0000000000..10564101ef
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/noxfile.py
@@ -0,0 +1,321 @@
+# mypy: disallow-untyped-defs=False, disallow-untyped-calls=False
+
+import contextlib
+import datetime
+import difflib
+import glob
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+import textwrap
+import time
+import webbrowser
+from pathlib import Path
+
+import nox
+
+nox.options.sessions = ["lint"]
+nox.options.reuse_existing_virtualenvs = True
+
+
+@nox.session(python=["3.6", "3.7", "3.8", "3.9", "3.10", "pypy3"])
+def tests(session):
+ def coverage(*args):
+ session.run("python", "-m", "coverage", *args)
+
+ # Once coverage 5 is used then `.coverage` can move into `pyproject.toml`.
+ session.install("coverage<5.0.0", "pretend", "pytest>=6.2.0", "pip>=9.0.2")
+ session.install(".")
+
+ if "pypy" not in session.python:
+ coverage(
+ "run",
+ "--source",
+ "packaging/",
+ "-m",
+ "pytest",
+ "--strict-markers",
+ *session.posargs,
+ )
+ coverage("report", "-m", "--fail-under", "100")
+ else:
+ # Don't do coverage tracking for PyPy, since it's SLOW.
+ session.run(
+ "python",
+ "-m",
+ "pytest",
+ "--capture=no",
+ "--strict-markers",
+ *session.posargs,
+ )
+
+
+@nox.session(python="3.9")
+def lint(session):
+ # Run the linters (via pre-commit)
+ session.install("pre-commit")
+ session.run("pre-commit", "run", "--all-files")
+
+ # Check the distribution
+ session.install("build", "twine")
+ session.run("pyproject-build")
+ session.run("twine", "check", *glob.glob("dist/*"))
+
+
+@nox.session(python="3.9")
+def docs(session):
+ shutil.rmtree("docs/_build", ignore_errors=True)
+ session.install("furo")
+ session.install("-e", ".")
+
+ variants = [
+ # (builder, dest)
+ ("html", "html"),
+ ("latex", "latex"),
+ ("doctest", "html"),
+ ]
+
+ for builder, dest in variants:
+ session.run(
+ "sphinx-build",
+ "-W",
+ "-b",
+ builder,
+ "-d",
+ "docs/_build/doctrees/" + dest,
+ "docs", # source directory
+ "docs/_build/" + dest, # output directory
+ )
+
+
+@nox.session
+def release(session):
+ package_name = "packaging"
+ version_file = Path(f"{package_name}/__about__.py")
+ changelog_file = Path("CHANGELOG.rst")
+
+ try:
+ release_version = _get_version_from_arguments(session.posargs)
+ except ValueError as e:
+ session.error(f"Invalid arguments: {e}")
+ return
+
+ # Check state of working directory and git.
+ _check_working_directory_state(session)
+ _check_git_state(session, release_version)
+
+ # Prepare for release.
+ _changelog_update_unreleased_title(release_version, file=changelog_file)
+ session.run("git", "add", str(changelog_file), external=True)
+ _bump(session, version=release_version, file=version_file, kind="release")
+
+ # Tag the release commit.
+ # fmt: off
+ session.run(
+ "git", "tag",
+ "-s", release_version,
+ "-m", f"Release {release_version}",
+ external=True,
+ )
+ # fmt: on
+
+ # Prepare for development.
+ _changelog_add_unreleased_title(file=changelog_file)
+ session.run("git", "add", str(changelog_file), external=True)
+
+ major, minor = map(int, release_version.split("."))
+ next_version = f"{major}.{minor + 1}.dev0"
+ _bump(session, version=next_version, file=version_file, kind="development")
+
+ # Checkout the git tag.
+ session.run("git", "checkout", "-q", release_version, external=True)
+
+ session.install("build", "twine")
+
+ # Build the distribution.
+ session.run("python", "-m", "build")
+
+ # Check what files are in dist/ for upload.
+ files = sorted(glob.glob("dist/*"))
+ expected = [
+ f"dist/{package_name}-{release_version}-py3-none-any.whl",
+ f"dist/{package_name}-{release_version}.tar.gz",
+ ]
+ if files != expected:
+ diff_generator = difflib.context_diff(
+ expected, files, fromfile="expected", tofile="got", lineterm=""
+ )
+ diff = "\n".join(diff_generator)
+ session.error(f"Got the wrong files:\n{diff}")
+
+ # Get back out into main.
+ session.run("git", "checkout", "-q", "main", external=True)
+
+ # Check and upload distribution files.
+ session.run("twine", "check", *files)
+
+ # Push the commits and tag.
+ # NOTE: The following fails if pushing to the branch is not allowed. This can
+ # happen on GitHub, if the main branch is protected, there are required
+ # CI checks and "Include administrators" is enabled on the protection.
+ session.run("git", "push", "upstream", "main", release_version, external=True)
+
+ # Upload the distribution.
+ session.run("twine", "upload", *files)
+
+ # Open up the GitHub release page.
+ webbrowser.open("https://github.com/pypa/packaging/releases")
+
+
+# -----------------------------------------------------------------------------
+# Helpers
+# -----------------------------------------------------------------------------
+def _get_version_from_arguments(arguments):
+ """Checks the arguments passed to `nox -s release`.
+
+ Only 1 argument that looks like a version? Return the argument.
+ Otherwise, raise a ValueError describing what's wrong.
+ """
+ if len(arguments) != 1:
+ raise ValueError("Expected exactly 1 argument")
+
+ version = arguments[0]
+ parts = version.split(".")
+
+ if len(parts) != 2:
+ # Not of the form: YY.N
+ raise ValueError("not of the form: YY.N")
+
+ if not all(part.isdigit() for part in parts):
+ # Not all segments are integers.
+ raise ValueError("non-integer segments")
+
+ # All is good.
+ return version
+
+
+def _check_working_directory_state(session):
+ """Check state of the working directory, prior to making the release."""
+ should_not_exist = ["build/", "dist/"]
+
+ bad_existing_paths = list(filter(os.path.exists, should_not_exist))
+ if bad_existing_paths:
+ session.error(f"Remove {', '.join(bad_existing_paths)} and try again")
+
+
+def _check_git_state(session, version_tag):
+ """Check state of the git repository, prior to making the release."""
+ # Ensure the upstream remote pushes to the correct URL.
+ allowed_upstreams = [
+ "git@github.com:pypa/packaging.git",
+ "https://github.com/pypa/packaging.git",
+ ]
+ result = subprocess.run(
+ ["git", "remote", "get-url", "--push", "upstream"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ if result.stdout.rstrip() not in allowed_upstreams:
+ session.error(f"git remote `upstream` is not one of {allowed_upstreams}")
+ # Ensure we're on main branch for cutting a release.
+ result = subprocess.run(
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ if result.stdout != "main\n":
+ session.error(f"Not on main branch: {result.stdout!r}")
+
+ # Ensure there are no uncommitted changes.
+ result = subprocess.run(
+ ["git", "status", "--porcelain"], capture_output=True, encoding="utf-8"
+ )
+ if result.stdout:
+ print(result.stdout, end="", file=sys.stderr)
+ session.error("The working tree has uncommitted changes")
+
+ # Ensure this tag doesn't exist already.
+ result = subprocess.run(
+ ["git", "rev-parse", version_tag], capture_output=True, encoding="utf-8"
+ )
+ if not result.returncode:
+ session.error(f"Tag already exists! {version_tag} -- {result.stdout!r}")
+
+ # Back up the current git reference, in a tag that's easy to clean up.
+ _release_backup_tag = "auto/release-start-" + str(int(time.time()))
+ session.run("git", "tag", _release_backup_tag, external=True)
+
+
+def _bump(session, *, version, file, kind):
+ session.log(f"Bump version to {version!r}")
+ contents = file.read_text()
+ new_contents = re.sub(
+ '__version__ = "(.+)"', f'__version__ = "{version}"', contents
+ )
+ file.write_text(new_contents)
+
+ session.log("git commit")
+ subprocess.run(["git", "add", str(file)])
+ subprocess.run(["git", "commit", "-m", f"Bump for {kind}"])
+
+
+@contextlib.contextmanager
+def _replace_file(original_path):
+ # Create a temporary file.
+ fh, replacement_path = tempfile.mkstemp()
+
+ try:
+ with os.fdopen(fh, "w") as replacement:
+ with open(original_path) as original:
+ yield original, replacement
+ except Exception:
+ raise
+ else:
+ shutil.copymode(original_path, replacement_path)
+ os.remove(original_path)
+ shutil.move(replacement_path, original_path)
+
+
+def _changelog_update_unreleased_title(version, *, file):
+ """Update an "*unreleased*" heading to "{version} - {date}" """
+ yyyy_mm_dd = datetime.datetime.today().strftime("%Y-%m-%d")
+ title = f"{version} - {yyyy_mm_dd}"
+
+ with _replace_file(file) as (original, replacement):
+ for line in original:
+ if line == "*unreleased*\n":
+ replacement.write(f"{title}\n")
+ replacement.write(len(title) * "~" + "\n")
+ # Skip processing the next line (the heading underline for *unreleased*)
+ # since we already wrote the heading underline.
+ next(original)
+ else:
+ replacement.write(line)
+
+
+def _changelog_add_unreleased_title(*, file):
+ with _replace_file(file) as (original, replacement):
+ # Duplicate first 3 lines from the original file.
+ for _ in range(3):
+ line = next(original)
+ replacement.write(line)
+
+ # Write the heading.
+ replacement.write(
+ textwrap.dedent(
+ """\
+ *unreleased*
+ ~~~~~~~~~~~~
+
+ No unreleased changes.
+
+ """
+ )
+ )
+
+ # Duplicate all the remaining lines.
+ for line in original:
+ replacement.write(line)
diff --git a/testing/web-platform/tests/tools/third_party/packaging/packaging/__about__.py b/testing/web-platform/tests/tools/third_party/packaging/packaging/__about__.py
new file mode 100644
index 0000000000..3551bc2d29
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/packaging/__about__.py
@@ -0,0 +1,26 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+__all__ = [
+ "__title__",
+ "__summary__",
+ "__uri__",
+ "__version__",
+ "__author__",
+ "__email__",
+ "__license__",
+ "__copyright__",
+]
+
+__title__ = "packaging"
+__summary__ = "Core utilities for Python packages"
+__uri__ = "https://github.com/pypa/packaging"
+
+__version__ = "21.3"
+
+__author__ = "Donald Stufft and individual contributors"
+__email__ = "donald@stufft.io"
+
+__license__ = "BSD-2-Clause or Apache-2.0"
+__copyright__ = "2014-2019 %s" % __author__
diff --git a/testing/web-platform/tests/tools/third_party/packaging/packaging/__init__.py b/testing/web-platform/tests/tools/third_party/packaging/packaging/__init__.py
new file mode 100644
index 0000000000..3c50c5dcfe
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/packaging/__init__.py
@@ -0,0 +1,25 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+from .__about__ import (
+ __author__,
+ __copyright__,
+ __email__,
+ __license__,
+ __summary__,
+ __title__,
+ __uri__,
+ __version__,
+)
+
+__all__ = [
+ "__title__",
+ "__summary__",
+ "__uri__",
+ "__version__",
+ "__author__",
+ "__email__",
+ "__license__",
+ "__copyright__",
+]
diff --git a/testing/web-platform/tests/tools/third_party/packaging/packaging/_manylinux.py b/testing/web-platform/tests/tools/third_party/packaging/packaging/_manylinux.py
new file mode 100644
index 0000000000..4c379aa6f6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/packaging/_manylinux.py
@@ -0,0 +1,301 @@
+import collections
+import functools
+import os
+import re
+import struct
+import sys
+import warnings
+from typing import IO, Dict, Iterator, NamedTuple, Optional, Tuple
+
+
+# Python does not provide platform information at sufficient granularity to
+# identify the architecture of the running executable in some cases, so we
+# determine it dynamically by reading the information from the running
+# process. This only applies on Linux, which uses the ELF format.
+class _ELFFileHeader:
+ # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
+ class _InvalidELFFileHeader(ValueError):
+ """
+ An invalid ELF file header was found.
+ """
+
+ ELF_MAGIC_NUMBER = 0x7F454C46
+ ELFCLASS32 = 1
+ ELFCLASS64 = 2
+ ELFDATA2LSB = 1
+ ELFDATA2MSB = 2
+ EM_386 = 3
+ EM_S390 = 22
+ EM_ARM = 40
+ EM_X86_64 = 62
+ EF_ARM_ABIMASK = 0xFF000000
+ EF_ARM_ABI_VER5 = 0x05000000
+ EF_ARM_ABI_FLOAT_HARD = 0x00000400
+
+ def __init__(self, file: IO[bytes]) -> None:
+ def unpack(fmt: str) -> int:
+ try:
+ data = file.read(struct.calcsize(fmt))
+ result: Tuple[int, ...] = struct.unpack(fmt, data)
+ except struct.error:
+ raise _ELFFileHeader._InvalidELFFileHeader()
+ return result[0]
+
+ self.e_ident_magic = unpack(">I")
+ if self.e_ident_magic != self.ELF_MAGIC_NUMBER:
+ raise _ELFFileHeader._InvalidELFFileHeader()
+ self.e_ident_class = unpack("B")
+ if self.e_ident_class not in {self.ELFCLASS32, self.ELFCLASS64}:
+ raise _ELFFileHeader._InvalidELFFileHeader()
+ self.e_ident_data = unpack("B")
+ if self.e_ident_data not in {self.ELFDATA2LSB, self.ELFDATA2MSB}:
+ raise _ELFFileHeader._InvalidELFFileHeader()
+ self.e_ident_version = unpack("B")
+ self.e_ident_osabi = unpack("B")
+ self.e_ident_abiversion = unpack("B")
+ self.e_ident_pad = file.read(7)
+ format_h = "<H" if self.e_ident_data == self.ELFDATA2LSB else ">H"
+ format_i = "<I" if self.e_ident_data == self.ELFDATA2LSB else ">I"
+ format_q = "<Q" if self.e_ident_data == self.ELFDATA2LSB else ">Q"
+ format_p = format_i if self.e_ident_class == self.ELFCLASS32 else format_q
+ self.e_type = unpack(format_h)
+ self.e_machine = unpack(format_h)
+ self.e_version = unpack(format_i)
+ self.e_entry = unpack(format_p)
+ self.e_phoff = unpack(format_p)
+ self.e_shoff = unpack(format_p)
+ self.e_flags = unpack(format_i)
+ self.e_ehsize = unpack(format_h)
+ self.e_phentsize = unpack(format_h)
+ self.e_phnum = unpack(format_h)
+ self.e_shentsize = unpack(format_h)
+ self.e_shnum = unpack(format_h)
+ self.e_shstrndx = unpack(format_h)
+
+
+def _get_elf_header() -> Optional[_ELFFileHeader]:
+ try:
+ with open(sys.executable, "rb") as f:
+ elf_header = _ELFFileHeader(f)
+ except (OSError, TypeError, _ELFFileHeader._InvalidELFFileHeader):
+ return None
+ return elf_header
+
+
+def _is_linux_armhf() -> bool:
+ # hard-float ABI can be detected from the ELF header of the running
+ # process
+ # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf
+ elf_header = _get_elf_header()
+ if elf_header is None:
+ return False
+ result = elf_header.e_ident_class == elf_header.ELFCLASS32
+ result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB
+ result &= elf_header.e_machine == elf_header.EM_ARM
+ result &= (
+ elf_header.e_flags & elf_header.EF_ARM_ABIMASK
+ ) == elf_header.EF_ARM_ABI_VER5
+ result &= (
+ elf_header.e_flags & elf_header.EF_ARM_ABI_FLOAT_HARD
+ ) == elf_header.EF_ARM_ABI_FLOAT_HARD
+ return result
+
+
+def _is_linux_i686() -> bool:
+ elf_header = _get_elf_header()
+ if elf_header is None:
+ return False
+ result = elf_header.e_ident_class == elf_header.ELFCLASS32
+ result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB
+ result &= elf_header.e_machine == elf_header.EM_386
+ return result
+
+
+def _have_compatible_abi(arch: str) -> bool:
+ if arch == "armv7l":
+ return _is_linux_armhf()
+ if arch == "i686":
+ return _is_linux_i686()
+ return arch in {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x"}
+
+
+# If glibc ever changes its major version, we need to know what the last
+# minor version was, so we can build the complete list of all versions.
+# For now, guess what the highest minor version might be, assume it will
+# be 50 for testing. Once this actually happens, update the dictionary
+# with the actual value.
+_LAST_GLIBC_MINOR: Dict[int, int] = collections.defaultdict(lambda: 50)
+
+
+class _GLibCVersion(NamedTuple):
+ major: int
+ minor: int
+
+
+def _glibc_version_string_confstr() -> Optional[str]:
+ """
+ Primary implementation of glibc_version_string using os.confstr.
+ """
+ # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely
+ # to be broken or missing. This strategy is used in the standard library
+ # platform module.
+ # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183
+ try:
+ # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17".
+ version_string = os.confstr("CS_GNU_LIBC_VERSION")
+ assert version_string is not None
+ _, version = version_string.split()
+ except (AssertionError, AttributeError, OSError, ValueError):
+ # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...
+ return None
+ return version
+
+
+def _glibc_version_string_ctypes() -> Optional[str]:
+ """
+ Fallback implementation of glibc_version_string using ctypes.
+ """
+ try:
+ import ctypes
+ except ImportError:
+ return None
+
+ # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
+ # manpage says, "If filename is NULL, then the returned handle is for the
+ # main program". This way we can let the linker do the work to figure out
+ # which libc our process is actually using.
+ #
+ # We must also handle the special case where the executable is not a
+ # dynamically linked executable. This can occur when using musl libc,
+ # for example. In this situation, dlopen() will error, leading to an
+ # OSError. Interestingly, at least in the case of musl, there is no
+ # errno set on the OSError. The single string argument used to construct
+ # OSError comes from libc itself and is therefore not portable to
+ # hard code here. In any case, failure to call dlopen() means we
+ # can proceed, so we bail on our attempt.
+ try:
+ process_namespace = ctypes.CDLL(None)
+ except OSError:
+ return None
+
+ try:
+ gnu_get_libc_version = process_namespace.gnu_get_libc_version
+ except AttributeError:
+ # Symbol doesn't exist -> therefore, we are not linked to
+ # glibc.
+ return None
+
+ # Call gnu_get_libc_version, which returns a string like "2.5"
+ gnu_get_libc_version.restype = ctypes.c_char_p
+ version_str: str = gnu_get_libc_version()
+ # py2 / py3 compatibility:
+ if not isinstance(version_str, str):
+ version_str = version_str.decode("ascii")
+
+ return version_str
+
+
+def _glibc_version_string() -> Optional[str]:
+ """Returns glibc version string, or None if not using glibc."""
+ return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
+
+
+def _parse_glibc_version(version_str: str) -> Tuple[int, int]:
+ """Parse glibc version.
+
+ We use a regexp instead of str.split because we want to discard any
+ random junk that might come after the minor version -- this might happen
+ in patched/forked versions of glibc (e.g. Linaro's version of glibc
+ uses version strings like "2.20-2014.11"). See gh-3588.
+ """
+ m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str)
+ if not m:
+ warnings.warn(
+ "Expected glibc version with 2 components major.minor,"
+ " got: %s" % version_str,
+ RuntimeWarning,
+ )
+ return -1, -1
+ return int(m.group("major")), int(m.group("minor"))
+
+
+@functools.lru_cache()
+def _get_glibc_version() -> Tuple[int, int]:
+ version_str = _glibc_version_string()
+ if version_str is None:
+ return (-1, -1)
+ return _parse_glibc_version(version_str)
+
+
+# From PEP 513, PEP 600
+def _is_compatible(name: str, arch: str, version: _GLibCVersion) -> bool:
+ sys_glibc = _get_glibc_version()
+ if sys_glibc < version:
+ return False
+ # Check for presence of _manylinux module.
+ try:
+ import _manylinux # noqa
+ except ImportError:
+ return True
+ if hasattr(_manylinux, "manylinux_compatible"):
+ result = _manylinux.manylinux_compatible(version[0], version[1], arch)
+ if result is not None:
+ return bool(result)
+ return True
+ if version == _GLibCVersion(2, 5):
+ if hasattr(_manylinux, "manylinux1_compatible"):
+ return bool(_manylinux.manylinux1_compatible)
+ if version == _GLibCVersion(2, 12):
+ if hasattr(_manylinux, "manylinux2010_compatible"):
+ return bool(_manylinux.manylinux2010_compatible)
+ if version == _GLibCVersion(2, 17):
+ if hasattr(_manylinux, "manylinux2014_compatible"):
+ return bool(_manylinux.manylinux2014_compatible)
+ return True
+
+
+_LEGACY_MANYLINUX_MAP = {
+ # CentOS 7 w/ glibc 2.17 (PEP 599)
+ (2, 17): "manylinux2014",
+ # CentOS 6 w/ glibc 2.12 (PEP 571)
+ (2, 12): "manylinux2010",
+ # CentOS 5 w/ glibc 2.5 (PEP 513)
+ (2, 5): "manylinux1",
+}
+
+
+def platform_tags(linux: str, arch: str) -> Iterator[str]:
+ if not _have_compatible_abi(arch):
+ return
+ # Oldest glibc to be supported regardless of architecture is (2, 17).
+ too_old_glibc2 = _GLibCVersion(2, 16)
+ if arch in {"x86_64", "i686"}:
+ # On x86/i686 also oldest glibc to be supported is (2, 5).
+ too_old_glibc2 = _GLibCVersion(2, 4)
+ current_glibc = _GLibCVersion(*_get_glibc_version())
+ glibc_max_list = [current_glibc]
+ # We can assume compatibility across glibc major versions.
+ # https://sourceware.org/bugzilla/show_bug.cgi?id=24636
+ #
+ # Build a list of maximum glibc versions so that we can
+ # output the canonical list of all glibc from current_glibc
+ # down to too_old_glibc2, including all intermediary versions.
+ for glibc_major in range(current_glibc.major - 1, 1, -1):
+ glibc_minor = _LAST_GLIBC_MINOR[glibc_major]
+ glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor))
+ for glibc_max in glibc_max_list:
+ if glibc_max.major == too_old_glibc2.major:
+ min_minor = too_old_glibc2.minor
+ else:
+ # For other glibc major versions oldest supported is (x, 0).
+ min_minor = -1
+ for glibc_minor in range(glibc_max.minor, min_minor, -1):
+ glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
+ tag = "manylinux_{}_{}".format(*glibc_version)
+ if _is_compatible(tag, arch, glibc_version):
+ yield linux.replace("linux", tag)
+ # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
+ if glibc_version in _LEGACY_MANYLINUX_MAP:
+ legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
+ if _is_compatible(legacy_tag, arch, glibc_version):
+ yield linux.replace("linux", legacy_tag)
diff --git a/testing/web-platform/tests/tools/third_party/packaging/packaging/_musllinux.py b/testing/web-platform/tests/tools/third_party/packaging/packaging/_musllinux.py
new file mode 100644
index 0000000000..8ac3059ba3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/packaging/_musllinux.py
@@ -0,0 +1,136 @@
+"""PEP 656 support.
+
+This module implements logic to detect if the currently running Python is
+linked against musl, and what musl version is used.
+"""
+
+import contextlib
+import functools
+import operator
+import os
+import re
+import struct
+import subprocess
+import sys
+from typing import IO, Iterator, NamedTuple, Optional, Tuple
+
+
+def _read_unpacked(f: IO[bytes], fmt: str) -> Tuple[int, ...]:
+ return struct.unpack(fmt, f.read(struct.calcsize(fmt)))
+
+
+def _parse_ld_musl_from_elf(f: IO[bytes]) -> Optional[str]:
+ """Detect musl libc location by parsing the Python executable.
+
+ Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca
+ ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
+ """
+ f.seek(0)
+ try:
+ ident = _read_unpacked(f, "16B")
+ except struct.error:
+ return None
+ if ident[:4] != tuple(b"\x7fELF"): # Invalid magic, not ELF.
+ return None
+ f.seek(struct.calcsize("HHI"), 1) # Skip file type, machine, and version.
+
+ try:
+ # e_fmt: Format for program header.
+ # p_fmt: Format for section header.
+ # p_idx: Indexes to find p_type, p_offset, and p_filesz.
+ e_fmt, p_fmt, p_idx = {
+ 1: ("IIIIHHH", "IIIIIIII", (0, 1, 4)), # 32-bit.
+ 2: ("QQQIHHH", "IIQQQQQQ", (0, 2, 5)), # 64-bit.
+ }[ident[4]]
+ except KeyError:
+ return None
+ else:
+ p_get = operator.itemgetter(*p_idx)
+
+ # Find the interpreter section and return its content.
+ try:
+ _, e_phoff, _, _, _, e_phentsize, e_phnum = _read_unpacked(f, e_fmt)
+ except struct.error:
+ return None
+ for i in range(e_phnum + 1):
+ f.seek(e_phoff + e_phentsize * i)
+ try:
+ p_type, p_offset, p_filesz = p_get(_read_unpacked(f, p_fmt))
+ except struct.error:
+ return None
+ if p_type != 3: # Not PT_INTERP.
+ continue
+ f.seek(p_offset)
+ interpreter = os.fsdecode(f.read(p_filesz)).strip("\0")
+ if "musl" not in interpreter:
+ return None
+ return interpreter
+ return None
+
+
+class _MuslVersion(NamedTuple):
+ major: int
+ minor: int
+
+
+def _parse_musl_version(output: str) -> Optional[_MuslVersion]:
+ lines = [n for n in (n.strip() for n in output.splitlines()) if n]
+ if len(lines) < 2 or lines[0][:4] != "musl":
+ return None
+ m = re.match(r"Version (\d+)\.(\d+)", lines[1])
+ if not m:
+ return None
+ return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2)))
+
+
+@functools.lru_cache()
+def _get_musl_version(executable: str) -> Optional[_MuslVersion]:
+ """Detect currently-running musl runtime version.
+
+ This is done by checking the specified executable's dynamic linking
+ information, and invoking the loader to parse its output for a version
+ string. If the loader is musl, the output would be something like::
+
+ musl libc (x86_64)
+ Version 1.2.2
+ Dynamic Program Loader
+ """
+ with contextlib.ExitStack() as stack:
+ try:
+ f = stack.enter_context(open(executable, "rb"))
+ except OSError:
+ return None
+ ld = _parse_ld_musl_from_elf(f)
+ if not ld:
+ return None
+ proc = subprocess.run([ld], stderr=subprocess.PIPE, universal_newlines=True)
+ return _parse_musl_version(proc.stderr)
+
+
+def platform_tags(arch: str) -> Iterator[str]:
+ """Generate musllinux tags compatible to the current platform.
+
+ :param arch: Should be the part of platform tag after the ``linux_``
+ prefix, e.g. ``x86_64``. The ``linux_`` prefix is assumed as a
+ prerequisite for the current platform to be musllinux-compatible.
+
+ :returns: An iterator of compatible musllinux tags.
+ """
+ sys_musl = _get_musl_version(sys.executable)
+ if sys_musl is None: # Python not dynamically linked against musl.
+ return
+ for minor in range(sys_musl.minor, -1, -1):
+ yield f"musllinux_{sys_musl.major}_{minor}_{arch}"
+
+
+if __name__ == "__main__": # pragma: no cover
+ import sysconfig
+
+ plat = sysconfig.get_platform()
+ assert plat.startswith("linux-"), "not linux"
+
+ print("plat:", plat)
+ print("musl:", _get_musl_version(sys.executable))
+ print("tags:", end=" ")
+ for t in platform_tags(re.sub(r"[.-]", "_", plat.split("-", 1)[-1])):
+ print(t, end="\n ")
diff --git a/testing/web-platform/tests/tools/third_party/packaging/packaging/_structures.py b/testing/web-platform/tests/tools/third_party/packaging/packaging/_structures.py
new file mode 100644
index 0000000000..90a6465f96
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/packaging/_structures.py
@@ -0,0 +1,61 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+
+class InfinityType:
+ def __repr__(self) -> str:
+ return "Infinity"
+
+ def __hash__(self) -> int:
+ return hash(repr(self))
+
+ def __lt__(self, other: object) -> bool:
+ return False
+
+ def __le__(self, other: object) -> bool:
+ return False
+
+ def __eq__(self, other: object) -> bool:
+ return isinstance(other, self.__class__)
+
+ def __gt__(self, other: object) -> bool:
+ return True
+
+ def __ge__(self, other: object) -> bool:
+ return True
+
+ def __neg__(self: object) -> "NegativeInfinityType":
+ return NegativeInfinity
+
+
+Infinity = InfinityType()
+
+
+class NegativeInfinityType:
+ def __repr__(self) -> str:
+ return "-Infinity"
+
+ def __hash__(self) -> int:
+ return hash(repr(self))
+
+ def __lt__(self, other: object) -> bool:
+ return True
+
+ def __le__(self, other: object) -> bool:
+ return True
+
+ def __eq__(self, other: object) -> bool:
+ return isinstance(other, self.__class__)
+
+ def __gt__(self, other: object) -> bool:
+ return False
+
+ def __ge__(self, other: object) -> bool:
+ return False
+
+ def __neg__(self: object) -> InfinityType:
+ return Infinity
+
+
+NegativeInfinity = NegativeInfinityType()
diff --git a/testing/web-platform/tests/tools/third_party/packaging/packaging/markers.py b/testing/web-platform/tests/tools/third_party/packaging/packaging/markers.py
new file mode 100644
index 0000000000..cb640e8f9b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/packaging/markers.py
@@ -0,0 +1,304 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import operator
+import os
+import platform
+import sys
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+
+from pyparsing import ( # noqa: N817
+ Forward,
+ Group,
+ Literal as L,
+ ParseException,
+ ParseResults,
+ QuotedString,
+ ZeroOrMore,
+ stringEnd,
+ stringStart,
+)
+
+from .specifiers import InvalidSpecifier, Specifier
+
+__all__ = [
+ "InvalidMarker",
+ "UndefinedComparison",
+ "UndefinedEnvironmentName",
+ "Marker",
+ "default_environment",
+]
+
+Operator = Callable[[str, str], bool]
+
+
+class InvalidMarker(ValueError):
+ """
+ An invalid marker was found, users should refer to PEP 508.
+ """
+
+
+class UndefinedComparison(ValueError):
+ """
+ An invalid operation was attempted on a value that doesn't support it.
+ """
+
+
+class UndefinedEnvironmentName(ValueError):
+ """
+ A name was attempted to be used that does not exist inside of the
+ environment.
+ """
+
+
+class Node:
+ def __init__(self, value: Any) -> None:
+ self.value = value
+
+ def __str__(self) -> str:
+ return str(self.value)
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__.__name__}('{self}')>"
+
+ def serialize(self) -> str:
+ raise NotImplementedError
+
+
+class Variable(Node):
+ def serialize(self) -> str:
+ return str(self)
+
+
+class Value(Node):
+ def serialize(self) -> str:
+ return f'"{self}"'
+
+
+class Op(Node):
+ def serialize(self) -> str:
+ return str(self)
+
+
+VARIABLE = (
+ L("implementation_version")
+ | L("platform_python_implementation")
+ | L("implementation_name")
+ | L("python_full_version")
+ | L("platform_release")
+ | L("platform_version")
+ | L("platform_machine")
+ | L("platform_system")
+ | L("python_version")
+ | L("sys_platform")
+ | L("os_name")
+ | L("os.name") # PEP-345
+ | L("sys.platform") # PEP-345
+ | L("platform.version") # PEP-345
+ | L("platform.machine") # PEP-345
+ | L("platform.python_implementation") # PEP-345
+ | L("python_implementation") # undocumented setuptools legacy
+ | L("extra") # PEP-508
+)
+ALIASES = {
+ "os.name": "os_name",
+ "sys.platform": "sys_platform",
+ "platform.version": "platform_version",
+ "platform.machine": "platform_machine",
+ "platform.python_implementation": "platform_python_implementation",
+ "python_implementation": "platform_python_implementation",
+}
+VARIABLE.setParseAction(lambda s, l, t: Variable(ALIASES.get(t[0], t[0])))
+
+VERSION_CMP = (
+ L("===") | L("==") | L(">=") | L("<=") | L("!=") | L("~=") | L(">") | L("<")
+)
+
+MARKER_OP = VERSION_CMP | L("not in") | L("in")
+MARKER_OP.setParseAction(lambda s, l, t: Op(t[0]))
+
+MARKER_VALUE = QuotedString("'") | QuotedString('"')
+MARKER_VALUE.setParseAction(lambda s, l, t: Value(t[0]))
+
+BOOLOP = L("and") | L("or")
+
+MARKER_VAR = VARIABLE | MARKER_VALUE
+
+MARKER_ITEM = Group(MARKER_VAR + MARKER_OP + MARKER_VAR)
+MARKER_ITEM.setParseAction(lambda s, l, t: tuple(t[0]))
+
+LPAREN = L("(").suppress()
+RPAREN = L(")").suppress()
+
+MARKER_EXPR = Forward()
+MARKER_ATOM = MARKER_ITEM | Group(LPAREN + MARKER_EXPR + RPAREN)
+MARKER_EXPR << MARKER_ATOM + ZeroOrMore(BOOLOP + MARKER_EXPR)
+
+MARKER = stringStart + MARKER_EXPR + stringEnd
+
+
+def _coerce_parse_result(results: Union[ParseResults, List[Any]]) -> List[Any]:
+ if isinstance(results, ParseResults):
+ return [_coerce_parse_result(i) for i in results]
+ else:
+ return results
+
+
+def _format_marker(
+ marker: Union[List[str], Tuple[Node, ...], str], first: Optional[bool] = True
+) -> str:
+
+ assert isinstance(marker, (list, tuple, str))
+
+ # Sometimes we have a structure like [[...]] which is a single item list
+ # where the single item is itself it's own list. In that case we want skip
+ # the rest of this function so that we don't get extraneous () on the
+ # outside.
+ if (
+ isinstance(marker, list)
+ and len(marker) == 1
+ and isinstance(marker[0], (list, tuple))
+ ):
+ return _format_marker(marker[0])
+
+ if isinstance(marker, list):
+ inner = (_format_marker(m, first=False) for m in marker)
+ if first:
+ return " ".join(inner)
+ else:
+ return "(" + " ".join(inner) + ")"
+ elif isinstance(marker, tuple):
+ return " ".join([m.serialize() for m in marker])
+ else:
+ return marker
+
+
+_operators: Dict[str, Operator] = {
+ "in": lambda lhs, rhs: lhs in rhs,
+ "not in": lambda lhs, rhs: lhs not in rhs,
+ "<": operator.lt,
+ "<=": operator.le,
+ "==": operator.eq,
+ "!=": operator.ne,
+ ">=": operator.ge,
+ ">": operator.gt,
+}
+
+
+def _eval_op(lhs: str, op: Op, rhs: str) -> bool:
+ try:
+ spec = Specifier("".join([op.serialize(), rhs]))
+ except InvalidSpecifier:
+ pass
+ else:
+ return spec.contains(lhs)
+
+ oper: Optional[Operator] = _operators.get(op.serialize())
+ if oper is None:
+ raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
+
+ return oper(lhs, rhs)
+
+
+class Undefined:
+ pass
+
+
+_undefined = Undefined()
+
+
+def _get_env(environment: Dict[str, str], name: str) -> str:
+ value: Union[str, Undefined] = environment.get(name, _undefined)
+
+ if isinstance(value, Undefined):
+ raise UndefinedEnvironmentName(
+ f"{name!r} does not exist in evaluation environment."
+ )
+
+ return value
+
+
+def _evaluate_markers(markers: List[Any], environment: Dict[str, str]) -> bool:
+ groups: List[List[bool]] = [[]]
+
+ for marker in markers:
+ assert isinstance(marker, (list, tuple, str))
+
+ if isinstance(marker, list):
+ groups[-1].append(_evaluate_markers(marker, environment))
+ elif isinstance(marker, tuple):
+ lhs, op, rhs = marker
+
+ if isinstance(lhs, Variable):
+ lhs_value = _get_env(environment, lhs.value)
+ rhs_value = rhs.value
+ else:
+ lhs_value = lhs.value
+ rhs_value = _get_env(environment, rhs.value)
+
+ groups[-1].append(_eval_op(lhs_value, op, rhs_value))
+ else:
+ assert marker in ["and", "or"]
+ if marker == "or":
+ groups.append([])
+
+ return any(all(item) for item in groups)
+
+
+def format_full_version(info: "sys._version_info") -> str:
+ version = "{0.major}.{0.minor}.{0.micro}".format(info)
+ kind = info.releaselevel
+ if kind != "final":
+ version += kind[0] + str(info.serial)
+ return version
+
+
+def default_environment() -> Dict[str, str]:
+ iver = format_full_version(sys.implementation.version)
+ implementation_name = sys.implementation.name
+ return {
+ "implementation_name": implementation_name,
+ "implementation_version": iver,
+ "os_name": os.name,
+ "platform_machine": platform.machine(),
+ "platform_release": platform.release(),
+ "platform_system": platform.system(),
+ "platform_version": platform.version(),
+ "python_full_version": platform.python_version(),
+ "platform_python_implementation": platform.python_implementation(),
+ "python_version": ".".join(platform.python_version_tuple()[:2]),
+ "sys_platform": sys.platform,
+ }
+
+
+class Marker:
+ def __init__(self, marker: str) -> None:
+ try:
+ self._markers = _coerce_parse_result(MARKER.parseString(marker))
+ except ParseException as e:
+ raise InvalidMarker(
+ f"Invalid marker: {marker!r}, parse error at "
+ f"{marker[e.loc : e.loc + 8]!r}"
+ )
+
+ def __str__(self) -> str:
+ return _format_marker(self._markers)
+
+ def __repr__(self) -> str:
+ return f"<Marker('{self}')>"
+
+ def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool:
+ """Evaluate a marker.
+
+ Return the boolean from evaluating the given marker against the
+ environment. environment is an optional argument to override all or
+ part of the determined environment.
+
+ The environment is determined from the current Python process.
+ """
+ current_environment = default_environment()
+ if environment is not None:
+ current_environment.update(environment)
+
+ return _evaluate_markers(self._markers, current_environment)
diff --git a/testing/web-platform/tests/tools/third_party/packaging/packaging/py.typed b/testing/web-platform/tests/tools/third_party/packaging/packaging/py.typed
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/packaging/py.typed
diff --git a/testing/web-platform/tests/tools/third_party/packaging/packaging/requirements.py b/testing/web-platform/tests/tools/third_party/packaging/packaging/requirements.py
new file mode 100644
index 0000000000..53f9a3aa42
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/packaging/requirements.py
@@ -0,0 +1,146 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import re
+import string
+import urllib.parse
+from typing import List, Optional as TOptional, Set
+
+from pyparsing import ( # noqa
+ Combine,
+ Literal as L,
+ Optional,
+ ParseException,
+ Regex,
+ Word,
+ ZeroOrMore,
+ originalTextFor,
+ stringEnd,
+ stringStart,
+)
+
+from .markers import MARKER_EXPR, Marker
+from .specifiers import LegacySpecifier, Specifier, SpecifierSet
+
+
+class InvalidRequirement(ValueError):
+ """
+ An invalid requirement was found, users should refer to PEP 508.
+ """
+
+
+ALPHANUM = Word(string.ascii_letters + string.digits)
+
+LBRACKET = L("[").suppress()
+RBRACKET = L("]").suppress()
+LPAREN = L("(").suppress()
+RPAREN = L(")").suppress()
+COMMA = L(",").suppress()
+SEMICOLON = L(";").suppress()
+AT = L("@").suppress()
+
+PUNCTUATION = Word("-_.")
+IDENTIFIER_END = ALPHANUM | (ZeroOrMore(PUNCTUATION) + ALPHANUM)
+IDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END))
+
+NAME = IDENTIFIER("name")
+EXTRA = IDENTIFIER
+
+URI = Regex(r"[^ ]+")("url")
+URL = AT + URI
+
+EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA)
+EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras")
+
+VERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE)
+VERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE)
+
+VERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY
+VERSION_MANY = Combine(
+ VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), joinString=",", adjacent=False
+)("_raw_spec")
+_VERSION_SPEC = Optional((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY)
+_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or "")
+
+VERSION_SPEC = originalTextFor(_VERSION_SPEC)("specifier")
+VERSION_SPEC.setParseAction(lambda s, l, t: t[1])
+
+MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker")
+MARKER_EXPR.setParseAction(
+ lambda s, l, t: Marker(s[t._original_start : t._original_end])
+)
+MARKER_SEPARATOR = SEMICOLON
+MARKER = MARKER_SEPARATOR + MARKER_EXPR
+
+VERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER)
+URL_AND_MARKER = URL + Optional(MARKER)
+
+NAMED_REQUIREMENT = NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER)
+
+REQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd
+# pyparsing isn't thread safe during initialization, so we do it eagerly, see
+# issue #104
+REQUIREMENT.parseString("x[]")
+
+
+class Requirement:
+ """Parse a requirement.
+
+ Parse a given requirement string into its parts, such as name, specifier,
+ URL, and extras. Raises InvalidRequirement on a badly-formed requirement
+ string.
+ """
+
+ # TODO: Can we test whether something is contained within a requirement?
+ # If so how do we do that? Do we need to test against the _name_ of
+ # the thing as well as the version? What about the markers?
+ # TODO: Can we normalize the name and extra name?
+
+ def __init__(self, requirement_string: str) -> None:
+ try:
+ req = REQUIREMENT.parseString(requirement_string)
+ except ParseException as e:
+ raise InvalidRequirement(
+ f'Parse error at "{ requirement_string[e.loc : e.loc + 8]!r}": {e.msg}'
+ )
+
+ self.name: str = req.name
+ if req.url:
+ parsed_url = urllib.parse.urlparse(req.url)
+ if parsed_url.scheme == "file":
+ if urllib.parse.urlunparse(parsed_url) != req.url:
+ raise InvalidRequirement("Invalid URL given")
+ elif not (parsed_url.scheme and parsed_url.netloc) or (
+ not parsed_url.scheme and not parsed_url.netloc
+ ):
+ raise InvalidRequirement(f"Invalid URL: {req.url}")
+ self.url: TOptional[str] = req.url
+ else:
+ self.url = None
+ self.extras: Set[str] = set(req.extras.asList() if req.extras else [])
+ self.specifier: SpecifierSet = SpecifierSet(req.specifier)
+ self.marker: TOptional[Marker] = req.marker if req.marker else None
+
+ def __str__(self) -> str:
+ parts: List[str] = [self.name]
+
+ if self.extras:
+ formatted_extras = ",".join(sorted(self.extras))
+ parts.append(f"[{formatted_extras}]")
+
+ if self.specifier:
+ parts.append(str(self.specifier))
+
+ if self.url:
+ parts.append(f"@ {self.url}")
+ if self.marker:
+ parts.append(" ")
+
+ if self.marker:
+ parts.append(f"; {self.marker}")
+
+ return "".join(parts)
+
+ def __repr__(self) -> str:
+ return f"<Requirement('{self}')>"
diff --git a/testing/web-platform/tests/tools/third_party/packaging/packaging/specifiers.py b/testing/web-platform/tests/tools/third_party/packaging/packaging/specifiers.py
new file mode 100644
index 0000000000..0e218a6f9f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/packaging/specifiers.py
@@ -0,0 +1,802 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import abc
+import functools
+import itertools
+import re
+import warnings
+from typing import (
+ Callable,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Pattern,
+ Set,
+ Tuple,
+ TypeVar,
+ Union,
+)
+
+from .utils import canonicalize_version
+from .version import LegacyVersion, Version, parse
+
+ParsedVersion = Union[Version, LegacyVersion]
+UnparsedVersion = Union[Version, LegacyVersion, str]
+VersionTypeVar = TypeVar("VersionTypeVar", bound=UnparsedVersion)
+CallableOperator = Callable[[ParsedVersion, str], bool]
+
+
+class InvalidSpecifier(ValueError):
+ """
+ An invalid specifier was found, users should refer to PEP 440.
+ """
+
+
+class BaseSpecifier(metaclass=abc.ABCMeta):
+ @abc.abstractmethod
+ def __str__(self) -> str:
+ """
+ Returns the str representation of this Specifier like object. This
+ should be representative of the Specifier itself.
+ """
+
+ @abc.abstractmethod
+ def __hash__(self) -> int:
+ """
+ Returns a hash value for this Specifier like object.
+ """
+
+ @abc.abstractmethod
+ def __eq__(self, other: object) -> bool:
+ """
+ Returns a boolean representing whether or not the two Specifier like
+ objects are equal.
+ """
+
+ @abc.abstractproperty
+ def prereleases(self) -> Optional[bool]:
+ """
+ Returns whether or not pre-releases as a whole are allowed by this
+ specifier.
+ """
+
+ @prereleases.setter
+ def prereleases(self, value: bool) -> None:
+ """
+ Sets whether or not pre-releases as a whole are allowed by this
+ specifier.
+ """
+
+ @abc.abstractmethod
+ def contains(self, item: str, prereleases: Optional[bool] = None) -> bool:
+ """
+ Determines if the given item is contained within this specifier.
+ """
+
+ @abc.abstractmethod
+ def filter(
+ self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None
+ ) -> Iterable[VersionTypeVar]:
+ """
+ Takes an iterable of items and filters them so that only items which
+ are contained within this specifier are allowed in it.
+ """
+
+
+class _IndividualSpecifier(BaseSpecifier):
+
+ _operators: Dict[str, str] = {}
+ _regex: Pattern[str]
+
+ def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:
+ match = self._regex.search(spec)
+ if not match:
+ raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
+
+ self._spec: Tuple[str, str] = (
+ match.group("operator").strip(),
+ match.group("version").strip(),
+ )
+
+ # Store whether or not this Specifier should accept prereleases
+ self._prereleases = prereleases
+
+ def __repr__(self) -> str:
+ pre = (
+ f", prereleases={self.prereleases!r}"
+ if self._prereleases is not None
+ else ""
+ )
+
+ return f"<{self.__class__.__name__}({str(self)!r}{pre})>"
+
+ def __str__(self) -> str:
+ return "{}{}".format(*self._spec)
+
+ @property
+ def _canonical_spec(self) -> Tuple[str, str]:
+ return self._spec[0], canonicalize_version(self._spec[1])
+
+ def __hash__(self) -> int:
+ return hash(self._canonical_spec)
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, str):
+ try:
+ other = self.__class__(str(other))
+ except InvalidSpecifier:
+ return NotImplemented
+ elif not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self._canonical_spec == other._canonical_spec
+
+ def _get_operator(self, op: str) -> CallableOperator:
+ operator_callable: CallableOperator = getattr(
+ self, f"_compare_{self._operators[op]}"
+ )
+ return operator_callable
+
+ def _coerce_version(self, version: UnparsedVersion) -> ParsedVersion:
+ if not isinstance(version, (LegacyVersion, Version)):
+ version = parse(version)
+ return version
+
+ @property
+ def operator(self) -> str:
+ return self._spec[0]
+
+ @property
+ def version(self) -> str:
+ return self._spec[1]
+
+ @property
+ def prereleases(self) -> Optional[bool]:
+ return self._prereleases
+
+ @prereleases.setter
+ def prereleases(self, value: bool) -> None:
+ self._prereleases = value
+
+ def __contains__(self, item: str) -> bool:
+ return self.contains(item)
+
+ def contains(
+ self, item: UnparsedVersion, prereleases: Optional[bool] = None
+ ) -> bool:
+
+ # Determine if prereleases are to be allowed or not.
+ if prereleases is None:
+ prereleases = self.prereleases
+
+ # Normalize item to a Version or LegacyVersion, this allows us to have
+ # a shortcut for ``"2.0" in Specifier(">=2")
+ normalized_item = self._coerce_version(item)
+
+ # Determine if we should be supporting prereleases in this specifier
+ # or not, if we do not support prereleases than we can short circuit
+ # logic if this version is a prereleases.
+ if normalized_item.is_prerelease and not prereleases:
+ return False
+
+ # Actually do the comparison to determine if this item is contained
+ # within this Specifier or not.
+ operator_callable: CallableOperator = self._get_operator(self.operator)
+ return operator_callable(normalized_item, self.version)
+
+ def filter(
+ self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None
+ ) -> Iterable[VersionTypeVar]:
+
+ yielded = False
+ found_prereleases = []
+
+ kw = {"prereleases": prereleases if prereleases is not None else True}
+
+ # Attempt to iterate over all the values in the iterable and if any of
+ # them match, yield them.
+ for version in iterable:
+ parsed_version = self._coerce_version(version)
+
+ if self.contains(parsed_version, **kw):
+ # If our version is a prerelease, and we were not set to allow
+ # prereleases, then we'll store it for later in case nothing
+ # else matches this specifier.
+ if parsed_version.is_prerelease and not (
+ prereleases or self.prereleases
+ ):
+ found_prereleases.append(version)
+ # Either this is not a prerelease, or we should have been
+ # accepting prereleases from the beginning.
+ else:
+ yielded = True
+ yield version
+
+ # Now that we've iterated over everything, determine if we've yielded
+ # any values, and if we have not and we have any prereleases stored up
+ # then we will go ahead and yield the prereleases.
+ if not yielded and found_prereleases:
+ for version in found_prereleases:
+ yield version
+
+
+class LegacySpecifier(_IndividualSpecifier):
+
+ _regex_str = r"""
+ (?P<operator>(==|!=|<=|>=|<|>))
+ \s*
+ (?P<version>
+ [^,;\s)]* # Since this is a "legacy" specifier, and the version
+ # string can be just about anything, we match everything
+ # except for whitespace, a semi-colon for marker support,
+ # a closing paren since versions can be enclosed in
+ # them, and a comma since it's a version separator.
+ )
+ """
+
+ _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE)
+
+ _operators = {
+ "==": "equal",
+ "!=": "not_equal",
+ "<=": "less_than_equal",
+ ">=": "greater_than_equal",
+ "<": "less_than",
+ ">": "greater_than",
+ }
+
+ def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:
+ super().__init__(spec, prereleases)
+
+ warnings.warn(
+ "Creating a LegacyVersion has been deprecated and will be "
+ "removed in the next major release",
+ DeprecationWarning,
+ )
+
+ def _coerce_version(self, version: UnparsedVersion) -> LegacyVersion:
+ if not isinstance(version, LegacyVersion):
+ version = LegacyVersion(str(version))
+ return version
+
+ def _compare_equal(self, prospective: LegacyVersion, spec: str) -> bool:
+ return prospective == self._coerce_version(spec)
+
+ def _compare_not_equal(self, prospective: LegacyVersion, spec: str) -> bool:
+ return prospective != self._coerce_version(spec)
+
+ def _compare_less_than_equal(self, prospective: LegacyVersion, spec: str) -> bool:
+ return prospective <= self._coerce_version(spec)
+
+ def _compare_greater_than_equal(
+ self, prospective: LegacyVersion, spec: str
+ ) -> bool:
+ return prospective >= self._coerce_version(spec)
+
+ def _compare_less_than(self, prospective: LegacyVersion, spec: str) -> bool:
+ return prospective < self._coerce_version(spec)
+
+ def _compare_greater_than(self, prospective: LegacyVersion, spec: str) -> bool:
+ return prospective > self._coerce_version(spec)
+
+
+def _require_version_compare(
+ fn: Callable[["Specifier", ParsedVersion, str], bool]
+) -> Callable[["Specifier", ParsedVersion, str], bool]:
+ @functools.wraps(fn)
+ def wrapped(self: "Specifier", prospective: ParsedVersion, spec: str) -> bool:
+ if not isinstance(prospective, Version):
+ return False
+ return fn(self, prospective, spec)
+
+ return wrapped
+
+
+class Specifier(_IndividualSpecifier):
+
+ _regex_str = r"""
+ (?P<operator>(~=|==|!=|<=|>=|<|>|===))
+ (?P<version>
+ (?:
+ # The identity operators allow for an escape hatch that will
+ # do an exact string match of the version you wish to install.
+ # This will not be parsed by PEP 440 and we cannot determine
+ # any semantic meaning from it. This operator is discouraged
+ # but included entirely as an escape hatch.
+ (?<====) # Only match for the identity operator
+ \s*
+ [^\s]* # We just match everything, except for whitespace
+ # since we are only testing for strict identity.
+ )
+ |
+ (?:
+ # The (non)equality operators allow for wild card and local
+ # versions to be specified so we have to define these two
+ # operators separately to enable that.
+ (?<===|!=) # Only match for equals and not equals
+
+ \s*
+ v?
+ (?:[0-9]+!)? # epoch
+ [0-9]+(?:\.[0-9]+)* # release
+ (?: # pre release
+ [-_\.]?
+ (a|b|c|rc|alpha|beta|pre|preview)
+ [-_\.]?
+ [0-9]*
+ )?
+ (?: # post release
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
+ )?
+
+ # You cannot use a wild card and a dev or local version
+ # together so group them with a | and make them optional.
+ (?:
+ (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
+ (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local
+ |
+ \.\* # Wild card syntax of .*
+ )?
+ )
+ |
+ (?:
+ # The compatible operator requires at least two digits in the
+ # release segment.
+ (?<=~=) # Only match for the compatible operator
+
+ \s*
+ v?
+ (?:[0-9]+!)? # epoch
+ [0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *)
+ (?: # pre release
+ [-_\.]?
+ (a|b|c|rc|alpha|beta|pre|preview)
+ [-_\.]?
+ [0-9]*
+ )?
+ (?: # post release
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
+ )?
+ (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
+ )
+ |
+ (?:
+ # All other operators only allow a sub set of what the
+ # (non)equality operators do. Specifically they do not allow
+ # local versions to be specified nor do they allow the prefix
+ # matching wild cards.
+ (?<!==|!=|~=) # We have special cases for these
+ # operators so we want to make sure they
+ # don't match here.
+
+ \s*
+ v?
+ (?:[0-9]+!)? # epoch
+ [0-9]+(?:\.[0-9]+)* # release
+ (?: # pre release
+ [-_\.]?
+ (a|b|c|rc|alpha|beta|pre|preview)
+ [-_\.]?
+ [0-9]*
+ )?
+ (?: # post release
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
+ )?
+ (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
+ )
+ )
+ """
+
+ _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE)
+
+ _operators = {
+ "~=": "compatible",
+ "==": "equal",
+ "!=": "not_equal",
+ "<=": "less_than_equal",
+ ">=": "greater_than_equal",
+ "<": "less_than",
+ ">": "greater_than",
+ "===": "arbitrary",
+ }
+
+ @_require_version_compare
+ def _compare_compatible(self, prospective: ParsedVersion, spec: str) -> bool:
+
+ # Compatible releases have an equivalent combination of >= and ==. That
+ # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to
+ # implement this in terms of the other specifiers instead of
+ # implementing it ourselves. The only thing we need to do is construct
+ # the other specifiers.
+
+ # We want everything but the last item in the version, but we want to
+ # ignore suffix segments.
+ prefix = ".".join(
+ list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1]
+ )
+
+ # Add the prefix notation to the end of our string
+ prefix += ".*"
+
+ return self._get_operator(">=")(prospective, spec) and self._get_operator("==")(
+ prospective, prefix
+ )
+
+ @_require_version_compare
+ def _compare_equal(self, prospective: ParsedVersion, spec: str) -> bool:
+
+ # We need special logic to handle prefix matching
+ if spec.endswith(".*"):
+ # In the case of prefix matching we want to ignore local segment.
+ prospective = Version(prospective.public)
+ # Split the spec out by dots, and pretend that there is an implicit
+ # dot in between a release segment and a pre-release segment.
+ split_spec = _version_split(spec[:-2]) # Remove the trailing .*
+
+ # Split the prospective version out by dots, and pretend that there
+ # is an implicit dot in between a release segment and a pre-release
+ # segment.
+ split_prospective = _version_split(str(prospective))
+
+ # Shorten the prospective version to be the same length as the spec
+ # so that we can determine if the specifier is a prefix of the
+ # prospective version or not.
+ shortened_prospective = split_prospective[: len(split_spec)]
+
+ # Pad out our two sides with zeros so that they both equal the same
+ # length.
+ padded_spec, padded_prospective = _pad_version(
+ split_spec, shortened_prospective
+ )
+
+ return padded_prospective == padded_spec
+ else:
+ # Convert our spec string into a Version
+ spec_version = Version(spec)
+
+ # If the specifier does not have a local segment, then we want to
+ # act as if the prospective version also does not have a local
+ # segment.
+ if not spec_version.local:
+ prospective = Version(prospective.public)
+
+ return prospective == spec_version
+
+ @_require_version_compare
+ def _compare_not_equal(self, prospective: ParsedVersion, spec: str) -> bool:
+ return not self._compare_equal(prospective, spec)
+
+ @_require_version_compare
+ def _compare_less_than_equal(self, prospective: ParsedVersion, spec: str) -> bool:
+
+ # NB: Local version identifiers are NOT permitted in the version
+ # specifier, so local version labels can be universally removed from
+ # the prospective version.
+ return Version(prospective.public) <= Version(spec)
+
+ @_require_version_compare
+ def _compare_greater_than_equal(
+ self, prospective: ParsedVersion, spec: str
+ ) -> bool:
+
+ # NB: Local version identifiers are NOT permitted in the version
+ # specifier, so local version labels can be universally removed from
+ # the prospective version.
+ return Version(prospective.public) >= Version(spec)
+
+ @_require_version_compare
+ def _compare_less_than(self, prospective: ParsedVersion, spec_str: str) -> bool:
+
+ # Convert our spec to a Version instance, since we'll want to work with
+ # it as a version.
+ spec = Version(spec_str)
+
+ # Check to see if the prospective version is less than the spec
+ # version. If it's not we can short circuit and just return False now
+ # instead of doing extra unneeded work.
+ if not prospective < spec:
+ return False
+
+ # This special case is here so that, unless the specifier itself
+ # includes is a pre-release version, that we do not accept pre-release
+ # versions for the version mentioned in the specifier (e.g. <3.1 should
+ # not match 3.1.dev0, but should match 3.0.dev0).
+ if not spec.is_prerelease and prospective.is_prerelease:
+ if Version(prospective.base_version) == Version(spec.base_version):
+ return False
+
+ # If we've gotten to here, it means that prospective version is both
+ # less than the spec version *and* it's not a pre-release of the same
+ # version in the spec.
+ return True
+
+ @_require_version_compare
+ def _compare_greater_than(self, prospective: ParsedVersion, spec_str: str) -> bool:
+
+ # Convert our spec to a Version instance, since we'll want to work with
+ # it as a version.
+ spec = Version(spec_str)
+
+ # Check to see if the prospective version is greater than the spec
+ # version. If it's not we can short circuit and just return False now
+ # instead of doing extra unneeded work.
+ if not prospective > spec:
+ return False
+
+ # This special case is here so that, unless the specifier itself
+ # includes is a post-release version, that we do not accept
+ # post-release versions for the version mentioned in the specifier
+ # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0).
+ if not spec.is_postrelease and prospective.is_postrelease:
+ if Version(prospective.base_version) == Version(spec.base_version):
+ return False
+
+ # Ensure that we do not allow a local version of the version mentioned
+ # in the specifier, which is technically greater than, to match.
+ if prospective.local is not None:
+ if Version(prospective.base_version) == Version(spec.base_version):
+ return False
+
+ # If we've gotten to here, it means that prospective version is both
+ # greater than the spec version *and* it's not a pre-release of the
+ # same version in the spec.
+ return True
+
+ def _compare_arbitrary(self, prospective: Version, spec: str) -> bool:
+ return str(prospective).lower() == str(spec).lower()
+
+ @property
+ def prereleases(self) -> bool:
+
+ # If there is an explicit prereleases set for this, then we'll just
+ # blindly use that.
+ if self._prereleases is not None:
+ return self._prereleases
+
+ # Look at all of our specifiers and determine if they are inclusive
+ # operators, and if they are if they are including an explicit
+ # prerelease.
+ operator, version = self._spec
+ if operator in ["==", ">=", "<=", "~=", "==="]:
+ # The == specifier can include a trailing .*, if it does we
+ # want to remove before parsing.
+ if operator == "==" and version.endswith(".*"):
+ version = version[:-2]
+
+ # Parse the version, and if it is a pre-release than this
+ # specifier allows pre-releases.
+ if parse(version).is_prerelease:
+ return True
+
+ return False
+
+ @prereleases.setter
+ def prereleases(self, value: bool) -> None:
+ self._prereleases = value
+
+
+_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
+
+
+def _version_split(version: str) -> List[str]:
+ result: List[str] = []
+ for item in version.split("."):
+ match = _prefix_regex.search(item)
+ if match:
+ result.extend(match.groups())
+ else:
+ result.append(item)
+ return result
+
+
+def _is_not_suffix(segment: str) -> bool:
+ return not any(
+ segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post")
+ )
+
+
+def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str]]:
+ left_split, right_split = [], []
+
+ # Get the release segment of our versions
+ left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left)))
+ right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right)))
+
+ # Get the rest of our versions
+ left_split.append(left[len(left_split[0]) :])
+ right_split.append(right[len(right_split[0]) :])
+
+ # Insert our padding
+ left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0])))
+ right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0])))
+
+ return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split)))
+
+
+class SpecifierSet(BaseSpecifier):
+ def __init__(
+ self, specifiers: str = "", prereleases: Optional[bool] = None
+ ) -> None:
+
+ # Split on , to break each individual specifier into it's own item, and
+ # strip each item to remove leading/trailing whitespace.
+ split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
+
+ # Parsed each individual specifier, attempting first to make it a
+ # Specifier and falling back to a LegacySpecifier.
+ parsed: Set[_IndividualSpecifier] = set()
+ for specifier in split_specifiers:
+ try:
+ parsed.add(Specifier(specifier))
+ except InvalidSpecifier:
+ parsed.add(LegacySpecifier(specifier))
+
+ # Turn our parsed specifiers into a frozen set and save them for later.
+ self._specs = frozenset(parsed)
+
+ # Store our prereleases value so we can use it later to determine if
+ # we accept prereleases or not.
+ self._prereleases = prereleases
+
+ def __repr__(self) -> str:
+ pre = (
+ f", prereleases={self.prereleases!r}"
+ if self._prereleases is not None
+ else ""
+ )
+
+ return f"<SpecifierSet({str(self)!r}{pre})>"
+
+ def __str__(self) -> str:
+ return ",".join(sorted(str(s) for s in self._specs))
+
+ def __hash__(self) -> int:
+ return hash(self._specs)
+
+ def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet":
+ if isinstance(other, str):
+ other = SpecifierSet(other)
+ elif not isinstance(other, SpecifierSet):
+ return NotImplemented
+
+ specifier = SpecifierSet()
+ specifier._specs = frozenset(self._specs | other._specs)
+
+ if self._prereleases is None and other._prereleases is not None:
+ specifier._prereleases = other._prereleases
+ elif self._prereleases is not None and other._prereleases is None:
+ specifier._prereleases = self._prereleases
+ elif self._prereleases == other._prereleases:
+ specifier._prereleases = self._prereleases
+ else:
+ raise ValueError(
+ "Cannot combine SpecifierSets with True and False prerelease "
+ "overrides."
+ )
+
+ return specifier
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, (str, _IndividualSpecifier)):
+ other = SpecifierSet(str(other))
+ elif not isinstance(other, SpecifierSet):
+ return NotImplemented
+
+ return self._specs == other._specs
+
+ def __len__(self) -> int:
+ return len(self._specs)
+
+ def __iter__(self) -> Iterator[_IndividualSpecifier]:
+ return iter(self._specs)
+
+ @property
+ def prereleases(self) -> Optional[bool]:
+
+ # If we have been given an explicit prerelease modifier, then we'll
+ # pass that through here.
+ if self._prereleases is not None:
+ return self._prereleases
+
+ # If we don't have any specifiers, and we don't have a forced value,
+ # then we'll just return None since we don't know if this should have
+ # pre-releases or not.
+ if not self._specs:
+ return None
+
+ # Otherwise we'll see if any of the given specifiers accept
+ # prereleases, if any of them do we'll return True, otherwise False.
+ return any(s.prereleases for s in self._specs)
+
+ @prereleases.setter
+ def prereleases(self, value: bool) -> None:
+ self._prereleases = value
+
+ def __contains__(self, item: UnparsedVersion) -> bool:
+ return self.contains(item)
+
+ def contains(
+ self, item: UnparsedVersion, prereleases: Optional[bool] = None
+ ) -> bool:
+
+ # Ensure that our item is a Version or LegacyVersion instance.
+ if not isinstance(item, (LegacyVersion, Version)):
+ item = parse(item)
+
+ # Determine if we're forcing a prerelease or not, if we're not forcing
+ # one for this particular filter call, then we'll use whatever the
+ # SpecifierSet thinks for whether or not we should support prereleases.
+ if prereleases is None:
+ prereleases = self.prereleases
+
+ # We can determine if we're going to allow pre-releases by looking to
+ # see if any of the underlying items supports them. If none of them do
+ # and this item is a pre-release then we do not allow it and we can
+ # short circuit that here.
+ # Note: This means that 1.0.dev1 would not be contained in something
+ # like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0
+ if not prereleases and item.is_prerelease:
+ return False
+
+ # We simply dispatch to the underlying specs here to make sure that the
+ # given version is contained within all of them.
+ # Note: This use of all() here means that an empty set of specifiers
+ # will always return True, this is an explicit design decision.
+ return all(s.contains(item, prereleases=prereleases) for s in self._specs)
+
+ def filter(
+ self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None
+ ) -> Iterable[VersionTypeVar]:
+
+ # Determine if we're forcing a prerelease or not, if we're not forcing
+ # one for this particular filter call, then we'll use whatever the
+ # SpecifierSet thinks for whether or not we should support prereleases.
+ if prereleases is None:
+ prereleases = self.prereleases
+
+ # If we have any specifiers, then we want to wrap our iterable in the
+ # filter method for each one, this will act as a logical AND amongst
+ # each specifier.
+ if self._specs:
+ for spec in self._specs:
+ iterable = spec.filter(iterable, prereleases=bool(prereleases))
+ return iterable
+ # If we do not have any specifiers, then we need to have a rough filter
+ # which will filter out any pre-releases, unless there are no final
+ # releases, and which will filter out LegacyVersion in general.
+ else:
+ filtered: List[VersionTypeVar] = []
+ found_prereleases: List[VersionTypeVar] = []
+
+ item: UnparsedVersion
+ parsed_version: Union[Version, LegacyVersion]
+
+ for item in iterable:
+ # Ensure that we some kind of Version class for this item.
+ if not isinstance(item, (LegacyVersion, Version)):
+ parsed_version = parse(item)
+ else:
+ parsed_version = item
+
+ # Filter out any item which is parsed as a LegacyVersion
+ if isinstance(parsed_version, LegacyVersion):
+ continue
+
+ # Store any item which is a pre-release for later unless we've
+ # already found a final version or we are accepting prereleases
+ if parsed_version.is_prerelease and not prereleases:
+ if not filtered:
+ found_prereleases.append(item)
+ else:
+ filtered.append(item)
+
+ # If we've found no items except for pre-releases, then we'll go
+ # ahead and use the pre-releases
+ if not filtered and found_prereleases and prereleases is None:
+ return found_prereleases
+
+ return filtered
diff --git a/testing/web-platform/tests/tools/third_party/packaging/packaging/tags.py b/testing/web-platform/tests/tools/third_party/packaging/packaging/tags.py
new file mode 100644
index 0000000000..9a3d25a71c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/packaging/tags.py
@@ -0,0 +1,487 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import logging
+import platform
+import sys
+import sysconfig
+from importlib.machinery import EXTENSION_SUFFIXES
+from typing import (
+ Dict,
+ FrozenSet,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Sequence,
+ Tuple,
+ Union,
+ cast,
+)
+
+from . import _manylinux, _musllinux
+
+logger = logging.getLogger(__name__)
+
+PythonVersion = Sequence[int]
+MacVersion = Tuple[int, int]
+
+INTERPRETER_SHORT_NAMES: Dict[str, str] = {
+ "python": "py", # Generic.
+ "cpython": "cp",
+ "pypy": "pp",
+ "ironpython": "ip",
+ "jython": "jy",
+}
+
+
+_32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32
+
+
+class Tag:
+ """
+ A representation of the tag triple for a wheel.
+
+ Instances are considered immutable and thus are hashable. Equality checking
+ is also supported.
+ """
+
+ __slots__ = ["_interpreter", "_abi", "_platform", "_hash"]
+
+ def __init__(self, interpreter: str, abi: str, platform: str) -> None:
+ self._interpreter = interpreter.lower()
+ self._abi = abi.lower()
+ self._platform = platform.lower()
+ # The __hash__ of every single element in a Set[Tag] will be evaluated each time
+ # that a set calls its `.disjoint()` method, which may be called hundreds of
+ # times when scanning a page of links for packages with tags matching that
+ # Set[Tag]. Pre-computing the value here produces significant speedups for
+ # downstream consumers.
+ self._hash = hash((self._interpreter, self._abi, self._platform))
+
+ @property
+ def interpreter(self) -> str:
+ return self._interpreter
+
+ @property
+ def abi(self) -> str:
+ return self._abi
+
+ @property
+ def platform(self) -> str:
+ return self._platform
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Tag):
+ return NotImplemented
+
+ return (
+ (self._hash == other._hash) # Short-circuit ASAP for perf reasons.
+ and (self._platform == other._platform)
+ and (self._abi == other._abi)
+ and (self._interpreter == other._interpreter)
+ )
+
+ def __hash__(self) -> int:
+ return self._hash
+
+ def __str__(self) -> str:
+ return f"{self._interpreter}-{self._abi}-{self._platform}"
+
+ def __repr__(self) -> str:
+ return f"<{self} @ {id(self)}>"
+
+
+def parse_tag(tag: str) -> FrozenSet[Tag]:
+ """
+ Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances.
+
+ Returning a set is required due to the possibility that the tag is a
+ compressed tag set.
+ """
+ tags = set()
+ interpreters, abis, platforms = tag.split("-")
+ for interpreter in interpreters.split("."):
+ for abi in abis.split("."):
+ for platform_ in platforms.split("."):
+ tags.add(Tag(interpreter, abi, platform_))
+ return frozenset(tags)
+
+
+def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]:
+ value = sysconfig.get_config_var(name)
+ if value is None and warn:
+ logger.debug(
+ "Config variable '%s' is unset, Python ABI tag may be incorrect", name
+ )
+ return value
+
+
+def _normalize_string(string: str) -> str:
+ return string.replace(".", "_").replace("-", "_")
+
+
+def _abi3_applies(python_version: PythonVersion) -> bool:
+ """
+ Determine if the Python version supports abi3.
+
+ PEP 384 was first implemented in Python 3.2.
+ """
+ return len(python_version) > 1 and tuple(python_version) >= (3, 2)
+
+
+def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]:
+ py_version = tuple(py_version) # To allow for version comparison.
+ abis = []
+ version = _version_nodot(py_version[:2])
+ debug = pymalloc = ucs4 = ""
+ with_debug = _get_config_var("Py_DEBUG", warn)
+ has_refcount = hasattr(sys, "gettotalrefcount")
+ # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled
+ # extension modules is the best option.
+ # https://github.com/pypa/pip/issues/3383#issuecomment-173267692
+ has_ext = "_d.pyd" in EXTENSION_SUFFIXES
+ if with_debug or (with_debug is None and (has_refcount or has_ext)):
+ debug = "d"
+ if py_version < (3, 8):
+ with_pymalloc = _get_config_var("WITH_PYMALLOC", warn)
+ if with_pymalloc or with_pymalloc is None:
+ pymalloc = "m"
+ if py_version < (3, 3):
+ unicode_size = _get_config_var("Py_UNICODE_SIZE", warn)
+ if unicode_size == 4 or (
+ unicode_size is None and sys.maxunicode == 0x10FFFF
+ ):
+ ucs4 = "u"
+ elif debug:
+ # Debug builds can also load "normal" extension modules.
+ # We can also assume no UCS-4 or pymalloc requirement.
+ abis.append(f"cp{version}")
+ abis.insert(
+ 0,
+ "cp{version}{debug}{pymalloc}{ucs4}".format(
+ version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4
+ ),
+ )
+ return abis
+
+
+def cpython_tags(
+ python_version: Optional[PythonVersion] = None,
+ abis: Optional[Iterable[str]] = None,
+ platforms: Optional[Iterable[str]] = None,
+ *,
+ warn: bool = False,
+) -> Iterator[Tag]:
+ """
+ Yields the tags for a CPython interpreter.
+
+ The tags consist of:
+ - cp<python_version>-<abi>-<platform>
+ - cp<python_version>-abi3-<platform>
+ - cp<python_version>-none-<platform>
+ - cp<less than python_version>-abi3-<platform> # Older Python versions down to 3.2.
+
+ If python_version only specifies a major version then user-provided ABIs and
+ the 'none' ABItag will be used.
+
+ If 'abi3' or 'none' are specified in 'abis' then they will be yielded at
+ their normal position and not at the beginning.
+ """
+ if not python_version:
+ python_version = sys.version_info[:2]
+
+ interpreter = f"cp{_version_nodot(python_version[:2])}"
+
+ if abis is None:
+ if len(python_version) > 1:
+ abis = _cpython_abis(python_version, warn)
+ else:
+ abis = []
+ abis = list(abis)
+ # 'abi3' and 'none' are explicitly handled later.
+ for explicit_abi in ("abi3", "none"):
+ try:
+ abis.remove(explicit_abi)
+ except ValueError:
+ pass
+
+ platforms = list(platforms or platform_tags())
+ for abi in abis:
+ for platform_ in platforms:
+ yield Tag(interpreter, abi, platform_)
+ if _abi3_applies(python_version):
+ yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms)
+ yield from (Tag(interpreter, "none", platform_) for platform_ in platforms)
+
+ if _abi3_applies(python_version):
+ for minor_version in range(python_version[1] - 1, 1, -1):
+ for platform_ in platforms:
+ interpreter = "cp{version}".format(
+ version=_version_nodot((python_version[0], minor_version))
+ )
+ yield Tag(interpreter, "abi3", platform_)
+
+
+def _generic_abi() -> Iterator[str]:
+ abi = sysconfig.get_config_var("SOABI")
+ if abi:
+ yield _normalize_string(abi)
+
+
+def generic_tags(
+ interpreter: Optional[str] = None,
+ abis: Optional[Iterable[str]] = None,
+ platforms: Optional[Iterable[str]] = None,
+ *,
+ warn: bool = False,
+) -> Iterator[Tag]:
+ """
+ Yields the tags for a generic interpreter.
+
+ The tags consist of:
+ - <interpreter>-<abi>-<platform>
+
+ The "none" ABI will be added if it was not explicitly provided.
+ """
+ if not interpreter:
+ interp_name = interpreter_name()
+ interp_version = interpreter_version(warn=warn)
+ interpreter = "".join([interp_name, interp_version])
+ if abis is None:
+ abis = _generic_abi()
+ platforms = list(platforms or platform_tags())
+ abis = list(abis)
+ if "none" not in abis:
+ abis.append("none")
+ for abi in abis:
+ for platform_ in platforms:
+ yield Tag(interpreter, abi, platform_)
+
+
+def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]:
+ """
+ Yields Python versions in descending order.
+
+ After the latest version, the major-only version will be yielded, and then
+ all previous versions of that major version.
+ """
+ if len(py_version) > 1:
+ yield f"py{_version_nodot(py_version[:2])}"
+ yield f"py{py_version[0]}"
+ if len(py_version) > 1:
+ for minor in range(py_version[1] - 1, -1, -1):
+ yield f"py{_version_nodot((py_version[0], minor))}"
+
+
+def compatible_tags(
+ python_version: Optional[PythonVersion] = None,
+ interpreter: Optional[str] = None,
+ platforms: Optional[Iterable[str]] = None,
+) -> Iterator[Tag]:
+ """
+ Yields the sequence of tags that are compatible with a specific version of Python.
+
+ The tags consist of:
+ - py*-none-<platform>
+ - <interpreter>-none-any # ... if `interpreter` is provided.
+ - py*-none-any
+ """
+ if not python_version:
+ python_version = sys.version_info[:2]
+ platforms = list(platforms or platform_tags())
+ for version in _py_interpreter_range(python_version):
+ for platform_ in platforms:
+ yield Tag(version, "none", platform_)
+ if interpreter:
+ yield Tag(interpreter, "none", "any")
+ for version in _py_interpreter_range(python_version):
+ yield Tag(version, "none", "any")
+
+
+def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str:
+ if not is_32bit:
+ return arch
+
+ if arch.startswith("ppc"):
+ return "ppc"
+
+ return "i386"
+
+
+def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]:
+ formats = [cpu_arch]
+ if cpu_arch == "x86_64":
+ if version < (10, 4):
+ return []
+ formats.extend(["intel", "fat64", "fat32"])
+
+ elif cpu_arch == "i386":
+ if version < (10, 4):
+ return []
+ formats.extend(["intel", "fat32", "fat"])
+
+ elif cpu_arch == "ppc64":
+ # TODO: Need to care about 32-bit PPC for ppc64 through 10.2?
+ if version > (10, 5) or version < (10, 4):
+ return []
+ formats.append("fat64")
+
+ elif cpu_arch == "ppc":
+ if version > (10, 6):
+ return []
+ formats.extend(["fat32", "fat"])
+
+ if cpu_arch in {"arm64", "x86_64"}:
+ formats.append("universal2")
+
+ if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}:
+ formats.append("universal")
+
+ return formats
+
+
+def mac_platforms(
+ version: Optional[MacVersion] = None, arch: Optional[str] = None
+) -> Iterator[str]:
+ """
+ Yields the platform tags for a macOS system.
+
+ The `version` parameter is a two-item tuple specifying the macOS version to
+ generate platform tags for. The `arch` parameter is the CPU architecture to
+ generate platform tags for. Both parameters default to the appropriate value
+ for the current system.
+ """
+ version_str, _, cpu_arch = platform.mac_ver()
+ if version is None:
+ version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
+ else:
+ version = version
+ if arch is None:
+ arch = _mac_arch(cpu_arch)
+ else:
+ arch = arch
+
+ if (10, 0) <= version and version < (11, 0):
+ # Prior to Mac OS 11, each yearly release of Mac OS bumped the
+ # "minor" version number. The major version was always 10.
+ for minor_version in range(version[1], -1, -1):
+ compat_version = 10, minor_version
+ binary_formats = _mac_binary_formats(compat_version, arch)
+ for binary_format in binary_formats:
+ yield "macosx_{major}_{minor}_{binary_format}".format(
+ major=10, minor=minor_version, binary_format=binary_format
+ )
+
+ if version >= (11, 0):
+ # Starting with Mac OS 11, each yearly release bumps the major version
+ # number. The minor versions are now the midyear updates.
+ for major_version in range(version[0], 10, -1):
+ compat_version = major_version, 0
+ binary_formats = _mac_binary_formats(compat_version, arch)
+ for binary_format in binary_formats:
+ yield "macosx_{major}_{minor}_{binary_format}".format(
+ major=major_version, minor=0, binary_format=binary_format
+ )
+
+ if version >= (11, 0):
+ # Mac OS 11 on x86_64 is compatible with binaries from previous releases.
+ # Arm64 support was introduced in 11.0, so no Arm binaries from previous
+ # releases exist.
+ #
+ # However, the "universal2" binary format can have a
+ # macOS version earlier than 11.0 when the x86_64 part of the binary supports
+ # that version of macOS.
+ if arch == "x86_64":
+ for minor_version in range(16, 3, -1):
+ compat_version = 10, minor_version
+ binary_formats = _mac_binary_formats(compat_version, arch)
+ for binary_format in binary_formats:
+ yield "macosx_{major}_{minor}_{binary_format}".format(
+ major=compat_version[0],
+ minor=compat_version[1],
+ binary_format=binary_format,
+ )
+ else:
+ for minor_version in range(16, 3, -1):
+ compat_version = 10, minor_version
+ binary_format = "universal2"
+ yield "macosx_{major}_{minor}_{binary_format}".format(
+ major=compat_version[0],
+ minor=compat_version[1],
+ binary_format=binary_format,
+ )
+
+
+def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
+ linux = _normalize_string(sysconfig.get_platform())
+ if is_32bit:
+ if linux == "linux_x86_64":
+ linux = "linux_i686"
+ elif linux == "linux_aarch64":
+ linux = "linux_armv7l"
+ _, arch = linux.split("_", 1)
+ yield from _manylinux.platform_tags(linux, arch)
+ yield from _musllinux.platform_tags(arch)
+ yield linux
+
+
+def _generic_platforms() -> Iterator[str]:
+ yield _normalize_string(sysconfig.get_platform())
+
+
+def platform_tags() -> Iterator[str]:
+ """
+ Provides the platform tags for this installation.
+ """
+ if platform.system() == "Darwin":
+ return mac_platforms()
+ elif platform.system() == "Linux":
+ return _linux_platforms()
+ else:
+ return _generic_platforms()
+
+
+def interpreter_name() -> str:
+ """
+ Returns the name of the running interpreter.
+ """
+ name = sys.implementation.name
+ return INTERPRETER_SHORT_NAMES.get(name) or name
+
+
+def interpreter_version(*, warn: bool = False) -> str:
+ """
+ Returns the version of the running interpreter.
+ """
+ version = _get_config_var("py_version_nodot", warn=warn)
+ if version:
+ version = str(version)
+ else:
+ version = _version_nodot(sys.version_info[:2])
+ return version
+
+
+def _version_nodot(version: PythonVersion) -> str:
+ return "".join(map(str, version))
+
+
+def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
+ """
+ Returns the sequence of tag triples for the running interpreter.
+
+ The order of the sequence corresponds to priority order for the
+ interpreter, from most to least important.
+ """
+
+ interp_name = interpreter_name()
+ if interp_name == "cp":
+ yield from cpython_tags(warn=warn)
+ else:
+ yield from generic_tags()
+
+ if interp_name == "pp":
+ yield from compatible_tags(interpreter="pp3")
+ else:
+ yield from compatible_tags()
diff --git a/testing/web-platform/tests/tools/third_party/packaging/packaging/utils.py b/testing/web-platform/tests/tools/third_party/packaging/packaging/utils.py
new file mode 100644
index 0000000000..bab11b80c6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/packaging/utils.py
@@ -0,0 +1,136 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import re
+from typing import FrozenSet, NewType, Tuple, Union, cast
+
+from .tags import Tag, parse_tag
+from .version import InvalidVersion, Version
+
+BuildTag = Union[Tuple[()], Tuple[int, str]]
+NormalizedName = NewType("NormalizedName", str)
+
+
+class InvalidWheelFilename(ValueError):
+ """
+ An invalid wheel filename was found, users should refer to PEP 427.
+ """
+
+
+class InvalidSdistFilename(ValueError):
+ """
+ An invalid sdist filename was found, users should refer to the packaging user guide.
+ """
+
+
+_canonicalize_regex = re.compile(r"[-_.]+")
+# PEP 427: The build number must start with a digit.
+_build_tag_regex = re.compile(r"(\d+)(.*)")
+
+
+def canonicalize_name(name: str) -> NormalizedName:
+ # This is taken from PEP 503.
+ value = _canonicalize_regex.sub("-", name).lower()
+ return cast(NormalizedName, value)
+
+
+def canonicalize_version(version: Union[Version, str]) -> str:
+ """
+ This is very similar to Version.__str__, but has one subtle difference
+ with the way it handles the release segment.
+ """
+ if isinstance(version, str):
+ try:
+ parsed = Version(version)
+ except InvalidVersion:
+ # Legacy versions cannot be normalized
+ return version
+ else:
+ parsed = version
+
+ parts = []
+
+ # Epoch
+ if parsed.epoch != 0:
+ parts.append(f"{parsed.epoch}!")
+
+ # Release segment
+ # NB: This strips trailing '.0's to normalize
+ parts.append(re.sub(r"(\.0)+$", "", ".".join(str(x) for x in parsed.release)))
+
+ # Pre-release
+ if parsed.pre is not None:
+ parts.append("".join(str(x) for x in parsed.pre))
+
+ # Post-release
+ if parsed.post is not None:
+ parts.append(f".post{parsed.post}")
+
+ # Development release
+ if parsed.dev is not None:
+ parts.append(f".dev{parsed.dev}")
+
+ # Local version segment
+ if parsed.local is not None:
+ parts.append(f"+{parsed.local}")
+
+ return "".join(parts)
+
+
+def parse_wheel_filename(
+ filename: str,
+) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]:
+ if not filename.endswith(".whl"):
+ raise InvalidWheelFilename(
+ f"Invalid wheel filename (extension must be '.whl'): {filename}"
+ )
+
+ filename = filename[:-4]
+ dashes = filename.count("-")
+ if dashes not in (4, 5):
+ raise InvalidWheelFilename(
+ f"Invalid wheel filename (wrong number of parts): {filename}"
+ )
+
+ parts = filename.split("-", dashes - 2)
+ name_part = parts[0]
+ # See PEP 427 for the rules on escaping the project name
+ if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None:
+ raise InvalidWheelFilename(f"Invalid project name: {filename}")
+ name = canonicalize_name(name_part)
+ version = Version(parts[1])
+ if dashes == 5:
+ build_part = parts[2]
+ build_match = _build_tag_regex.match(build_part)
+ if build_match is None:
+ raise InvalidWheelFilename(
+ f"Invalid build number: {build_part} in '{filename}'"
+ )
+ build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2)))
+ else:
+ build = ()
+ tags = parse_tag(parts[-1])
+ return (name, version, build, tags)
+
+
+def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]:
+ if filename.endswith(".tar.gz"):
+ file_stem = filename[: -len(".tar.gz")]
+ elif filename.endswith(".zip"):
+ file_stem = filename[: -len(".zip")]
+ else:
+ raise InvalidSdistFilename(
+ f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):"
+ f" {filename}"
+ )
+
+ # We are requiring a PEP 440 version, which cannot contain dashes,
+ # so we split on the last dash.
+ name_part, sep, version_part = file_stem.rpartition("-")
+ if not sep:
+ raise InvalidSdistFilename(f"Invalid sdist filename: {filename}")
+
+ name = canonicalize_name(name_part)
+ version = Version(version_part)
+ return (name, version)
diff --git a/testing/web-platform/tests/tools/third_party/packaging/packaging/version.py b/testing/web-platform/tests/tools/third_party/packaging/packaging/version.py
new file mode 100644
index 0000000000..de9a09a4ed
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/packaging/version.py
@@ -0,0 +1,504 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import collections
+import itertools
+import re
+import warnings
+from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union
+
+from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
+
+__all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"]
+
+InfiniteTypes = Union[InfinityType, NegativeInfinityType]
+PrePostDevType = Union[InfiniteTypes, Tuple[str, int]]
+SubLocalType = Union[InfiniteTypes, int, str]
+LocalType = Union[
+ NegativeInfinityType,
+ Tuple[
+ Union[
+ SubLocalType,
+ Tuple[SubLocalType, str],
+ Tuple[NegativeInfinityType, SubLocalType],
+ ],
+ ...,
+ ],
+]
+CmpKey = Tuple[
+ int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType
+]
+LegacyCmpKey = Tuple[int, Tuple[str, ...]]
+VersionComparisonMethod = Callable[
+ [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool
+]
+
+_Version = collections.namedtuple(
+ "_Version", ["epoch", "release", "dev", "pre", "post", "local"]
+)
+
+
+def parse(version: str) -> Union["LegacyVersion", "Version"]:
+ """
+ Parse the given version string and return either a :class:`Version` object
+ or a :class:`LegacyVersion` object depending on if the given version is
+ a valid PEP 440 version or a legacy version.
+ """
+ try:
+ return Version(version)
+ except InvalidVersion:
+ return LegacyVersion(version)
+
+
+class InvalidVersion(ValueError):
+ """
+ An invalid version was found, users should refer to PEP 440.
+ """
+
+
+class _BaseVersion:
+ _key: Union[CmpKey, LegacyCmpKey]
+
+ def __hash__(self) -> int:
+ return hash(self._key)
+
+ # Please keep the duplicated `isinstance` check
+ # in the six comparisons hereunder
+ # unless you find a way to avoid adding overhead function calls.
+ def __lt__(self, other: "_BaseVersion") -> bool:
+ if not isinstance(other, _BaseVersion):
+ return NotImplemented
+
+ return self._key < other._key
+
+ def __le__(self, other: "_BaseVersion") -> bool:
+ if not isinstance(other, _BaseVersion):
+ return NotImplemented
+
+ return self._key <= other._key
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, _BaseVersion):
+ return NotImplemented
+
+ return self._key == other._key
+
+ def __ge__(self, other: "_BaseVersion") -> bool:
+ if not isinstance(other, _BaseVersion):
+ return NotImplemented
+
+ return self._key >= other._key
+
+ def __gt__(self, other: "_BaseVersion") -> bool:
+ if not isinstance(other, _BaseVersion):
+ return NotImplemented
+
+ return self._key > other._key
+
+ def __ne__(self, other: object) -> bool:
+ if not isinstance(other, _BaseVersion):
+ return NotImplemented
+
+ return self._key != other._key
+
+
+class LegacyVersion(_BaseVersion):
+ def __init__(self, version: str) -> None:
+ self._version = str(version)
+ self._key = _legacy_cmpkey(self._version)
+
+ warnings.warn(
+ "Creating a LegacyVersion has been deprecated and will be "
+ "removed in the next major release",
+ DeprecationWarning,
+ )
+
+ def __str__(self) -> str:
+ return self._version
+
+ def __repr__(self) -> str:
+ return f"<LegacyVersion('{self}')>"
+
+ @property
+ def public(self) -> str:
+ return self._version
+
+ @property
+ def base_version(self) -> str:
+ return self._version
+
+ @property
+ def epoch(self) -> int:
+ return -1
+
+ @property
+ def release(self) -> None:
+ return None
+
+ @property
+ def pre(self) -> None:
+ return None
+
+ @property
+ def post(self) -> None:
+ return None
+
+ @property
+ def dev(self) -> None:
+ return None
+
+ @property
+ def local(self) -> None:
+ return None
+
+ @property
+ def is_prerelease(self) -> bool:
+ return False
+
+ @property
+ def is_postrelease(self) -> bool:
+ return False
+
+ @property
+ def is_devrelease(self) -> bool:
+ return False
+
+
+_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE)
+
+_legacy_version_replacement_map = {
+ "pre": "c",
+ "preview": "c",
+ "-": "final-",
+ "rc": "c",
+ "dev": "@",
+}
+
+
+def _parse_version_parts(s: str) -> Iterator[str]:
+ for part in _legacy_version_component_re.split(s):
+ part = _legacy_version_replacement_map.get(part, part)
+
+ if not part or part == ".":
+ continue
+
+ if part[:1] in "0123456789":
+ # pad for numeric comparison
+ yield part.zfill(8)
+ else:
+ yield "*" + part
+
+ # ensure that alpha/beta/candidate are before final
+ yield "*final"
+
+
+def _legacy_cmpkey(version: str) -> LegacyCmpKey:
+
+ # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch
+ # greater than or equal to 0. This will effectively put the LegacyVersion,
+ # which uses the defacto standard originally implemented by setuptools,
+ # as before all PEP 440 versions.
+ epoch = -1
+
+ # This scheme is taken from pkg_resources.parse_version setuptools prior to
+ # it's adoption of the packaging library.
+ parts: List[str] = []
+ for part in _parse_version_parts(version.lower()):
+ if part.startswith("*"):
+ # remove "-" before a prerelease tag
+ if part < "*final":
+ while parts and parts[-1] == "*final-":
+ parts.pop()
+
+ # remove trailing zeros from each series of numeric parts
+ while parts and parts[-1] == "00000000":
+ parts.pop()
+
+ parts.append(part)
+
+ return epoch, tuple(parts)
+
+
+# Deliberately not anchored to the start and end of the string, to make it
+# easier for 3rd party code to reuse
+VERSION_PATTERN = r"""
+ v?
+ (?:
+ (?:(?P<epoch>[0-9]+)!)? # epoch
+ (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
+ (?P<pre> # pre-release
+ [-_\.]?
+ (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
+ [-_\.]?
+ (?P<pre_n>[0-9]+)?
+ )?
+ (?P<post> # post release
+ (?:-(?P<post_n1>[0-9]+))
+ |
+ (?:
+ [-_\.]?
+ (?P<post_l>post|rev|r)
+ [-_\.]?
+ (?P<post_n2>[0-9]+)?
+ )
+ )?
+ (?P<dev> # dev release
+ [-_\.]?
+ (?P<dev_l>dev)
+ [-_\.]?
+ (?P<dev_n>[0-9]+)?
+ )?
+ )
+ (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
+"""
+
+
+class Version(_BaseVersion):
+
+ _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
+
+ def __init__(self, version: str) -> None:
+
+ # Validate the version and parse it into pieces
+ match = self._regex.search(version)
+ if not match:
+ raise InvalidVersion(f"Invalid version: '{version}'")
+
+ # Store the parsed out pieces of the version
+ self._version = _Version(
+ epoch=int(match.group("epoch")) if match.group("epoch") else 0,
+ release=tuple(int(i) for i in match.group("release").split(".")),
+ pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
+ post=_parse_letter_version(
+ match.group("post_l"), match.group("post_n1") or match.group("post_n2")
+ ),
+ dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
+ local=_parse_local_version(match.group("local")),
+ )
+
+ # Generate a key which will be used for sorting
+ self._key = _cmpkey(
+ self._version.epoch,
+ self._version.release,
+ self._version.pre,
+ self._version.post,
+ self._version.dev,
+ self._version.local,
+ )
+
+ def __repr__(self) -> str:
+ return f"<Version('{self}')>"
+
+ def __str__(self) -> str:
+ parts = []
+
+ # Epoch
+ if self.epoch != 0:
+ parts.append(f"{self.epoch}!")
+
+ # Release segment
+ parts.append(".".join(str(x) for x in self.release))
+
+ # Pre-release
+ if self.pre is not None:
+ parts.append("".join(str(x) for x in self.pre))
+
+ # Post-release
+ if self.post is not None:
+ parts.append(f".post{self.post}")
+
+ # Development release
+ if self.dev is not None:
+ parts.append(f".dev{self.dev}")
+
+ # Local version segment
+ if self.local is not None:
+ parts.append(f"+{self.local}")
+
+ return "".join(parts)
+
+ @property
+ def epoch(self) -> int:
+ _epoch: int = self._version.epoch
+ return _epoch
+
+ @property
+ def release(self) -> Tuple[int, ...]:
+ _release: Tuple[int, ...] = self._version.release
+ return _release
+
+ @property
+ def pre(self) -> Optional[Tuple[str, int]]:
+ _pre: Optional[Tuple[str, int]] = self._version.pre
+ return _pre
+
+ @property
+ def post(self) -> Optional[int]:
+ return self._version.post[1] if self._version.post else None
+
+ @property
+ def dev(self) -> Optional[int]:
+ return self._version.dev[1] if self._version.dev else None
+
+ @property
+ def local(self) -> Optional[str]:
+ if self._version.local:
+ return ".".join(str(x) for x in self._version.local)
+ else:
+ return None
+
+ @property
+ def public(self) -> str:
+ return str(self).split("+", 1)[0]
+
+ @property
+ def base_version(self) -> str:
+ parts = []
+
+ # Epoch
+ if self.epoch != 0:
+ parts.append(f"{self.epoch}!")
+
+ # Release segment
+ parts.append(".".join(str(x) for x in self.release))
+
+ return "".join(parts)
+
+ @property
+ def is_prerelease(self) -> bool:
+ return self.dev is not None or self.pre is not None
+
+ @property
+ def is_postrelease(self) -> bool:
+ return self.post is not None
+
+ @property
+ def is_devrelease(self) -> bool:
+ return self.dev is not None
+
+ @property
+ def major(self) -> int:
+ return self.release[0] if len(self.release) >= 1 else 0
+
+ @property
+ def minor(self) -> int:
+ return self.release[1] if len(self.release) >= 2 else 0
+
+ @property
+ def micro(self) -> int:
+ return self.release[2] if len(self.release) >= 3 else 0
+
+
+def _parse_letter_version(
+ letter: str, number: Union[str, bytes, SupportsInt]
+) -> Optional[Tuple[str, int]]:
+
+ if letter:
+ # We consider there to be an implicit 0 in a pre-release if there is
+ # not a numeral associated with it.
+ if number is None:
+ number = 0
+
+ # We normalize any letters to their lower case form
+ letter = letter.lower()
+
+ # We consider some words to be alternate spellings of other words and
+ # in those cases we want to normalize the spellings to our preferred
+ # spelling.
+ if letter == "alpha":
+ letter = "a"
+ elif letter == "beta":
+ letter = "b"
+ elif letter in ["c", "pre", "preview"]:
+ letter = "rc"
+ elif letter in ["rev", "r"]:
+ letter = "post"
+
+ return letter, int(number)
+ if not letter and number:
+ # We assume if we are given a number, but we are not given a letter
+ # then this is using the implicit post release syntax (e.g. 1.0-1)
+ letter = "post"
+
+ return letter, int(number)
+
+ return None
+
+
+_local_version_separators = re.compile(r"[\._-]")
+
+
+def _parse_local_version(local: str) -> Optional[LocalType]:
+ """
+ Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
+ """
+ if local is not None:
+ return tuple(
+ part.lower() if not part.isdigit() else int(part)
+ for part in _local_version_separators.split(local)
+ )
+ return None
+
+
+def _cmpkey(
+ epoch: int,
+ release: Tuple[int, ...],
+ pre: Optional[Tuple[str, int]],
+ post: Optional[Tuple[str, int]],
+ dev: Optional[Tuple[str, int]],
+ local: Optional[Tuple[SubLocalType]],
+) -> CmpKey:
+
+ # When we compare a release version, we want to compare it with all of the
+ # trailing zeros removed. So we'll use a reverse the list, drop all the now
+ # leading zeros until we come to something non zero, then take the rest
+ # re-reverse it back into the correct order and make it a tuple and use
+ # that for our sorting key.
+ _release = tuple(
+ reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
+ )
+
+ # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
+ # We'll do this by abusing the pre segment, but we _only_ want to do this
+ # if there is not a pre or a post segment. If we have one of those then
+ # the normal sorting rules will handle this case correctly.
+ if pre is None and post is None and dev is not None:
+ _pre: PrePostDevType = NegativeInfinity
+ # Versions without a pre-release (except as noted above) should sort after
+ # those with one.
+ elif pre is None:
+ _pre = Infinity
+ else:
+ _pre = pre
+
+ # Versions without a post segment should sort before those with one.
+ if post is None:
+ _post: PrePostDevType = NegativeInfinity
+
+ else:
+ _post = post
+
+ # Versions without a development segment should sort after those with one.
+ if dev is None:
+ _dev: PrePostDevType = Infinity
+
+ else:
+ _dev = dev
+
+ if local is None:
+ # Versions without a local segment should sort before those with one.
+ _local: LocalType = NegativeInfinity
+ else:
+ # Versions with a local segment need that segment parsed to implement
+ # the sorting rules in PEP440.
+ # - Alpha numeric segments sort before numeric segments
+ # - Alpha numeric segments sort lexicographically
+ # - Numeric segments sort numerically
+ # - Shorter versions sort before longer versions when the prefixes
+ # match exactly
+ _local = tuple(
+ (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
+ )
+
+ return epoch, _release, _pre, _post, _dev, _local
diff --git a/testing/web-platform/tests/tools/third_party/packaging/pyproject.toml b/testing/web-platform/tests/tools/third_party/packaging/pyproject.toml
new file mode 100644
index 0000000000..cb37b725dc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ['setuptools >= 40.8.0', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/testing/web-platform/tests/tools/third_party/packaging/setup.cfg b/testing/web-platform/tests/tools/third_party/packaging/setup.cfg
new file mode 100644
index 0000000000..c97a4e4409
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/setup.cfg
@@ -0,0 +1,3 @@
+[isort]
+profile = black
+combine_as_imports = true
diff --git a/testing/web-platform/tests/tools/third_party/packaging/setup.py b/testing/web-platform/tests/tools/third_party/packaging/setup.py
new file mode 100644
index 0000000000..ba1023f58a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/setup.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import os
+import re
+
+# While I generally consider it an antipattern to try and support both
+# setuptools and distutils with a single setup.py, in this specific instance
+# where packaging is a dependency of setuptools, it can create a circular
+# dependency when projects attempt to unbundle stuff from setuptools and pip.
+# Though we don't really support that, it makes things easier if we do this and
+# should hopefully cause less issues for end users.
+try:
+ from setuptools import setup
+except ImportError:
+ from distutils.core import setup
+
+
+base_dir = os.path.dirname(__file__)
+
+about = {}
+with open(os.path.join(base_dir, "packaging", "__about__.py")) as f:
+ exec(f.read(), about)
+
+with open(os.path.join(base_dir, "README.rst")) as f:
+ long_description = f.read()
+
+with open(os.path.join(base_dir, "CHANGELOG.rst")) as f:
+ # Remove :issue:`ddd` tags that breaks the description rendering
+ changelog = re.sub(
+ r":issue:`(\d+)`",
+ r"`#\1 <https://github.com/pypa/packaging/issues/\1>`__",
+ f.read(),
+ )
+ long_description = "\n".join([long_description, changelog])
+
+
+setup(
+ name=about["__title__"],
+ version=about["__version__"],
+ description=about["__summary__"],
+ long_description=long_description,
+ long_description_content_type="text/x-rst",
+ license=about["__license__"],
+ url=about["__uri__"],
+ author=about["__author__"],
+ author_email=about["__email__"],
+ python_requires=">=3.6",
+ install_requires=["pyparsing>=2.0.2,!=3.0.5"], # 2.0.2 + needed to avoid issue #91
+ classifiers=[
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "License :: OSI Approved :: BSD License",
+ "Programming Language :: Python",
+ "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 :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ ],
+ packages=["packaging"],
+ package_data={"packaging": ["py.typed"]},
+)
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tasks/__init__.py b/testing/web-platform/tests/tools/third_party/packaging/tasks/__init__.py
new file mode 100644
index 0000000000..883da23c96
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tasks/__init__.py
@@ -0,0 +1,9 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import invoke
+
+from . import check
+
+ns = invoke.Collection(check)
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tasks/check.py b/testing/web-platform/tests/tools/third_party/packaging/tasks/check.py
new file mode 100644
index 0000000000..b0896e1f76
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tasks/check.py
@@ -0,0 +1,141 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import itertools
+import json
+import os.path
+import xmlrpc.client
+
+import invoke
+import pkg_resources
+import progress.bar
+
+from packaging.version import Version
+
+from .paths import CACHE
+
+
+def _parse_version(value):
+ try:
+ return Version(value)
+ except ValueError:
+ return None
+
+
+@invoke.task
+def pep440(cached=False):
+ cache_path = os.path.join(CACHE, "pep440.json")
+
+ # If we were given --cached, then we want to attempt to use cached data if
+ # possible
+ if cached:
+ try:
+ with open(cache_path) as fp:
+ data = json.load(fp)
+ except Exception:
+ data = None
+ else:
+ data = None
+
+ # If we don't have data, then let's go fetch it from PyPI
+ if data is None:
+ bar = progress.bar.ShadyBar("Fetching Versions")
+ client = xmlrpc.client.Server("https://pypi.python.org/pypi")
+
+ data = {
+ project: client.package_releases(project, True)
+ for project in bar.iter(client.list_packages())
+ }
+
+ os.makedirs(os.path.dirname(cache_path), exist_ok=True)
+ with open(cache_path, "w") as fp:
+ json.dump(data, fp)
+
+ # Get a list of all of the version numbers on PyPI
+ all_versions = list(itertools.chain.from_iterable(data.values()))
+
+ # Determine the total number of versions which are compatible with the
+ # current routine
+ parsed_versions = [
+ _parse_version(v) for v in all_versions if _parse_version(v) is not None
+ ]
+
+ # Determine a list of projects that sort exactly the same between
+ # pkg_resources and PEP 440
+ compatible_sorting = [
+ project
+ for project, versions in data.items()
+ if (
+ sorted(versions, key=pkg_resources.parse_version)
+ == sorted((x for x in versions if _parse_version(x)), key=Version)
+ )
+ ]
+
+ # Determine a list of projects that sort exactly the same between
+ # pkg_resources and PEP 440 when invalid versions are filtered out
+ filtered_compatible_sorting = [
+ project
+ for project, versions in (
+ (p, [v for v in vs if _parse_version(v) is not None])
+ for p, vs in data.items()
+ )
+ if (
+ sorted(versions, key=pkg_resources.parse_version)
+ == sorted(versions, key=Version)
+ )
+ ]
+
+ # Determine a list of projects which do not have any versions that are
+ # valid with PEP 440 and which have any versions registered
+ only_invalid_versions = [
+ project
+ for project, versions in data.items()
+ if (versions and not [v for v in versions if _parse_version(v) is not None])
+ ]
+
+ # Determine a list of projects which have matching latest versions between
+ # pkg_resources and PEP 440
+ differing_latest_versions = [
+ project
+ for project, versions in data.items()
+ if (
+ sorted(versions, key=pkg_resources.parse_version)[-1:]
+ != sorted((x for x in versions if _parse_version(x)), key=Version)[-1:]
+ )
+ ]
+
+ # Print out our findings
+ print(
+ "Total Version Compatibility: {}/{} ({:.2%})".format(
+ len(parsed_versions),
+ len(all_versions),
+ len(parsed_versions) / len(all_versions),
+ )
+ )
+ print(
+ "Total Sorting Compatibility (Unfiltered): {}/{} ({:.2%})".format(
+ len(compatible_sorting), len(data), len(compatible_sorting) / len(data)
+ )
+ )
+ print(
+ "Total Sorting Compatibility (Filtered): {}/{} ({:.2%})".format(
+ len(filtered_compatible_sorting),
+ len(data),
+ len(filtered_compatible_sorting) / len(data),
+ )
+ )
+ print(
+ "Projects with No Compatible Versions: {}/{} ({:.2%})".format(
+ len(only_invalid_versions),
+ len(data),
+ len(only_invalid_versions) / len(data),
+ )
+ )
+ print(
+ "Projects with Differing Latest Version: {}/{} ({:.2%})".format(
+ len(differing_latest_versions),
+ len(data),
+ len(differing_latest_versions) / len(data),
+ )
+ )
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tasks/paths.py b/testing/web-platform/tests/tools/third_party/packaging/tasks/paths.py
new file mode 100644
index 0000000000..0888fb6967
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tasks/paths.py
@@ -0,0 +1,9 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import os.path
+
+PROJECT = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
+
+CACHE = os.path.join(PROJECT, ".cache")
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tasks/requirements.txt b/testing/web-platform/tests/tools/third_party/packaging/tasks/requirements.txt
new file mode 100644
index 0000000000..5677c0e89b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tasks/requirements.txt
@@ -0,0 +1,3 @@
+# The requirements required to invoke the tasks
+invoke
+progress
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/__init__.py b/testing/web-platform/tests/tools/third_party/packaging/tests/__init__.py
new file mode 100644
index 0000000000..b509336233
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/__init__.py
@@ -0,0 +1,3 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/hello-world.c b/testing/web-platform/tests/tools/third_party/packaging/tests/hello-world.c
new file mode 100644
index 0000000000..5e591c3ec5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/hello-world.c
@@ -0,0 +1,7 @@
+#include <stdio.h>
+
+int main(int argc, char* argv[])
+{
+ printf("Hello world");
+ return 0;
+}
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/build.sh b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/build.sh
new file mode 100755
index 0000000000..5071561560
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/build.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+set -x
+set -e
+
+if [ $# -eq 0 ]; then
+ docker run --rm -v $(pwd):/home/hello-world arm32v5/debian /home/hello-world/manylinux/build.sh incontainer 52
+ docker run --rm -v $(pwd):/home/hello-world arm32v7/debian /home/hello-world/manylinux/build.sh incontainer 52
+ docker run --rm -v $(pwd):/home/hello-world i386/debian /home/hello-world/manylinux/build.sh incontainer 52
+ docker run --rm -v $(pwd):/home/hello-world s390x/debian /home/hello-world/manylinux/build.sh incontainer 64
+ docker run --rm -v $(pwd):/home/hello-world debian /home/hello-world/manylinux/build.sh incontainer 64
+ docker run --rm -v $(pwd):/home/hello-world debian /home/hello-world/manylinux/build.sh x32 52
+ cp -f manylinux/hello-world-x86_64-i386 manylinux/hello-world-invalid-magic
+ printf "\x00" | dd of=manylinux/hello-world-invalid-magic bs=1 seek=0x00 count=1 conv=notrunc
+ cp -f manylinux/hello-world-x86_64-i386 manylinux/hello-world-invalid-class
+ printf "\x00" | dd of=manylinux/hello-world-invalid-class bs=1 seek=0x04 count=1 conv=notrunc
+ cp -f manylinux/hello-world-x86_64-i386 manylinux/hello-world-invalid-data
+ printf "\x00" | dd of=manylinux/hello-world-invalid-data bs=1 seek=0x05 count=1 conv=notrunc
+ head -c 40 manylinux/hello-world-x86_64-i386 > manylinux/hello-world-too-short
+ exit 0
+fi
+
+export DEBIAN_FRONTEND=noninteractive
+cd /home/hello-world/
+apt-get update
+apt-get install -y --no-install-recommends gcc libc6-dev
+if [ "$1" == "incontainer" ]; then
+ ARCH=$(dpkg --print-architecture)
+ CFLAGS=""
+else
+ ARCH=$1
+ dpkg --add-architecture ${ARCH}
+ apt-get install -y --no-install-recommends gcc-multilib libc6-dev-${ARCH}
+ CFLAGS="-mx32"
+fi
+NAME=hello-world-$(uname -m)-${ARCH}
+gcc -Os -s ${CFLAGS} -o ${NAME}-full hello-world.c
+head -c $2 ${NAME}-full > ${NAME}
+rm -f ${NAME}-full
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-armv7l-armel b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-armv7l-armel
new file mode 100755
index 0000000000..1dfd23fa3c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-armv7l-armel
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-armv7l-armhf b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-armv7l-armhf
new file mode 100755
index 0000000000..965ab3003a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-armv7l-armhf
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-invalid-class b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-invalid-class
new file mode 100755
index 0000000000..5e9899fc07
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-invalid-class
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-invalid-data b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-invalid-data
new file mode 100755
index 0000000000..2659b8ee25
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-invalid-data
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-invalid-magic b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-invalid-magic
new file mode 100755
index 0000000000..46066ad2de
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-invalid-magic
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-s390x-s390x b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-s390x-s390x
new file mode 100644
index 0000000000..c4e9578889
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-s390x-s390x
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-too-short b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-too-short
new file mode 100644
index 0000000000..4e5c0396b9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-too-short
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-x86_64-amd64 b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-x86_64-amd64
new file mode 100644
index 0000000000..c7f5b0b5e5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-x86_64-amd64
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-x86_64-i386 b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-x86_64-i386
new file mode 100755
index 0000000000..ff1d540a30
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-x86_64-i386
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-x86_64-x32 b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-x86_64-x32
new file mode 100755
index 0000000000..daf85d3473
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/manylinux/hello-world-x86_64-x32
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/musllinux/build.sh b/testing/web-platform/tests/tools/third_party/packaging/tests/musllinux/build.sh
new file mode 100644
index 0000000000..acd2b94c7f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/musllinux/build.sh
@@ -0,0 +1,61 @@
+# Build helper binaries for musllinux tests.
+# Usages:
+# build.sh # Build everything.
+# build.sh $DISTRO $ARCH # Build one executable in $ARCH using $DISTRO.
+#
+# Either invocation ultimately runs this script in a Docker container with
+# `build.sh glibc|musl $ARCH` to actually build the executable.
+
+set -euo pipefail
+set -x
+
+UBUNTU_VERSION='focal'
+ALPINE_VERSION='v3.13'
+
+build_one_in_ubuntu () {
+ $1 "multiarch/ubuntu-core:${2}-${UBUNTU_VERSION}" \
+ bash "/home/hello-world/musllinux/build.sh" glibc "glibc-${2}"
+}
+
+build_one_in_alpine () {
+ $1 "multiarch/alpine:${2}-${ALPINE_VERSION}" \
+ sh "/home/hello-world/musllinux/build.sh" musl "musl-${2}"
+}
+
+build_in_container () {
+ local SOURCE="$(dirname $(dirname $(realpath ${BASH_SOURCE[0]})))"
+ DOCKER="docker run --rm -v ${SOURCE}:/home/hello-world"
+
+ if [[ $# -ne 0 ]]; then
+ "build_one_in_${1}" "$DOCKER" "$2"
+ return
+ fi
+
+ build_one_in_alpine "$DOCKER" x86_64
+ build_one_in_alpine "$DOCKER" i386
+ build_one_in_alpine "$DOCKER" aarch64
+ build_one_in_ubuntu "$DOCKER" x86_64
+}
+
+if [[ $# -eq 0 ]]; then
+ build_in_container
+ exit 0
+elif [[ "$1" == "glibc" ]]; then
+ DEBIAN_FRONTEND=noninteractive apt-get update -qq \
+ && apt-get install -qqy --no-install-recommends gcc libc6-dev
+elif [[ "$1" == "musl" ]]; then
+ apk add -q build-base
+else
+ build_in_container "$@"
+ exit 0
+fi
+
+build () {
+ local CFLAGS=""
+ local OUT="/home/hello-world/musllinux/${2}"
+ gcc -Os ${CFLAGS} -o "${OUT}-full" "/home/hello-world/hello-world.c"
+ head -c1024 "${OUT}-full" > "$OUT"
+ rm -f "${OUT}-full"
+}
+
+build "$@"
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/test_manylinux.py b/testing/web-platform/tests/tools/third_party/packaging/tests/test_manylinux.py
new file mode 100644
index 0000000000..a04db15960
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/test_manylinux.py
@@ -0,0 +1,253 @@
+try:
+ import ctypes
+except ImportError:
+ ctypes = None
+import os
+import platform
+import sys
+import types
+import warnings
+
+import pretend
+import pytest
+
+from packaging import _manylinux
+from packaging._manylinux import (
+ _ELFFileHeader,
+ _get_elf_header,
+ _get_glibc_version,
+ _glibc_version_string,
+ _glibc_version_string_confstr,
+ _glibc_version_string_ctypes,
+ _is_compatible,
+ _is_linux_armhf,
+ _is_linux_i686,
+ _parse_glibc_version,
+)
+
+
+@pytest.fixture(autouse=True)
+def clear_lru_cache():
+ yield
+ _get_glibc_version.cache_clear()
+
+
+@pytest.fixture
+def manylinux_module(monkeypatch):
+ monkeypatch.setattr(_manylinux, "_get_glibc_version", lambda *args: (2, 20))
+ module_name = "_manylinux"
+ module = types.ModuleType(module_name)
+ monkeypatch.setitem(sys.modules, module_name, module)
+ return module
+
+
+@pytest.mark.parametrize("tf", (True, False))
+@pytest.mark.parametrize(
+ "attribute,glibc", (("1", (2, 5)), ("2010", (2, 12)), ("2014", (2, 17)))
+)
+def test_module_declaration(monkeypatch, manylinux_module, attribute, glibc, tf):
+ manylinux = f"manylinux{attribute}_compatible"
+ monkeypatch.setattr(manylinux_module, manylinux, tf, raising=False)
+ res = _is_compatible(manylinux, "x86_64", glibc)
+ assert tf is res
+
+
+@pytest.mark.parametrize(
+ "attribute,glibc", (("1", (2, 5)), ("2010", (2, 12)), ("2014", (2, 17)))
+)
+def test_module_declaration_missing_attribute(
+ monkeypatch, manylinux_module, attribute, glibc
+):
+ manylinux = f"manylinux{attribute}_compatible"
+ monkeypatch.delattr(manylinux_module, manylinux, raising=False)
+ assert _is_compatible(manylinux, "x86_64", glibc)
+
+
+@pytest.mark.parametrize(
+ "version,compatible", (((2, 0), True), ((2, 5), True), ((2, 10), False))
+)
+def test_is_manylinux_compatible_glibc_support(version, compatible, monkeypatch):
+ monkeypatch.setitem(sys.modules, "_manylinux", None)
+ monkeypatch.setattr(_manylinux, "_get_glibc_version", lambda: (2, 5))
+ assert bool(_is_compatible("manylinux1", "any", version)) == compatible
+
+
+@pytest.mark.parametrize("version_str", ["glibc-2.4.5", "2"])
+def test_check_glibc_version_warning(version_str):
+ with warnings.catch_warnings(record=True) as w:
+ _parse_glibc_version(version_str)
+ assert len(w) == 1
+ assert issubclass(w[0].category, RuntimeWarning)
+
+
+@pytest.mark.skipif(not ctypes, reason="requires ctypes")
+@pytest.mark.parametrize(
+ "version_str,expected",
+ [
+ # Be very explicit about bytes and Unicode for Python 2 testing.
+ (b"2.4", "2.4"),
+ ("2.4", "2.4"),
+ ],
+)
+def test_glibc_version_string(version_str, expected, monkeypatch):
+ class LibcVersion:
+ def __init__(self, version_str):
+ self.version_str = version_str
+
+ def __call__(self):
+ return version_str
+
+ class ProcessNamespace:
+ def __init__(self, libc_version):
+ self.gnu_get_libc_version = libc_version
+
+ process_namespace = ProcessNamespace(LibcVersion(version_str))
+ monkeypatch.setattr(ctypes, "CDLL", lambda _: process_namespace)
+ monkeypatch.setattr(_manylinux, "_glibc_version_string_confstr", lambda: False)
+
+ assert _glibc_version_string() == expected
+
+ del process_namespace.gnu_get_libc_version
+ assert _glibc_version_string() is None
+
+
+def test_glibc_version_string_confstr(monkeypatch):
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False)
+ assert _glibc_version_string_confstr() == "2.20"
+
+
+def test_glibc_version_string_fail(monkeypatch):
+ monkeypatch.setattr(os, "confstr", lambda x: None, raising=False)
+ monkeypatch.setitem(sys.modules, "ctypes", None)
+ assert _glibc_version_string() is None
+ assert _get_glibc_version() == (-1, -1)
+
+
+@pytest.mark.parametrize(
+ "failure",
+ [pretend.raiser(ValueError), pretend.raiser(OSError), lambda x: "XXX"],
+)
+def test_glibc_version_string_confstr_fail(monkeypatch, failure):
+ monkeypatch.setattr(os, "confstr", failure, raising=False)
+ assert _glibc_version_string_confstr() is None
+
+
+def test_glibc_version_string_confstr_missing(monkeypatch):
+ monkeypatch.delattr(os, "confstr", raising=False)
+ assert _glibc_version_string_confstr() is None
+
+
+def test_glibc_version_string_ctypes_missing(monkeypatch):
+ monkeypatch.setitem(sys.modules, "ctypes", None)
+ assert _glibc_version_string_ctypes() is None
+
+
+def test_glibc_version_string_ctypes_raise_oserror(monkeypatch):
+ def patched_cdll(name):
+ raise OSError("Dynamic loading not supported")
+
+ monkeypatch.setattr(ctypes, "CDLL", patched_cdll)
+ assert _glibc_version_string_ctypes() is None
+
+
+@pytest.mark.skipif(platform.system() != "Linux", reason="requires Linux")
+def test_is_manylinux_compatible_old():
+ # Assuming no one is running this test with a version of glibc released in
+ # 1997.
+ assert _is_compatible("any", "any", (2, 0))
+
+
+def test_is_manylinux_compatible(monkeypatch):
+ monkeypatch.setattr(_manylinux, "_glibc_version_string", lambda: "2.4")
+ assert _is_compatible("", "any", (2, 4))
+
+
+def test_glibc_version_string_none(monkeypatch):
+ monkeypatch.setattr(_manylinux, "_glibc_version_string", lambda: None)
+ assert not _is_compatible("any", "any", (2, 4))
+
+
+def test_is_linux_armhf_not_elf(monkeypatch):
+ monkeypatch.setattr(_manylinux, "_get_elf_header", lambda: None)
+ assert not _is_linux_armhf()
+
+
+def test_is_linux_i686_not_elf(monkeypatch):
+ monkeypatch.setattr(_manylinux, "_get_elf_header", lambda: None)
+ assert not _is_linux_i686()
+
+
+@pytest.mark.parametrize(
+ "machine, abi, elf_class, elf_data, elf_machine",
+ [
+ (
+ "x86_64",
+ "x32",
+ _ELFFileHeader.ELFCLASS32,
+ _ELFFileHeader.ELFDATA2LSB,
+ _ELFFileHeader.EM_X86_64,
+ ),
+ (
+ "x86_64",
+ "i386",
+ _ELFFileHeader.ELFCLASS32,
+ _ELFFileHeader.ELFDATA2LSB,
+ _ELFFileHeader.EM_386,
+ ),
+ (
+ "x86_64",
+ "amd64",
+ _ELFFileHeader.ELFCLASS64,
+ _ELFFileHeader.ELFDATA2LSB,
+ _ELFFileHeader.EM_X86_64,
+ ),
+ (
+ "armv7l",
+ "armel",
+ _ELFFileHeader.ELFCLASS32,
+ _ELFFileHeader.ELFDATA2LSB,
+ _ELFFileHeader.EM_ARM,
+ ),
+ (
+ "armv7l",
+ "armhf",
+ _ELFFileHeader.ELFCLASS32,
+ _ELFFileHeader.ELFDATA2LSB,
+ _ELFFileHeader.EM_ARM,
+ ),
+ (
+ "s390x",
+ "s390x",
+ _ELFFileHeader.ELFCLASS64,
+ _ELFFileHeader.ELFDATA2MSB,
+ _ELFFileHeader.EM_S390,
+ ),
+ ],
+)
+def test_get_elf_header(monkeypatch, machine, abi, elf_class, elf_data, elf_machine):
+ path = os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ f"hello-world-{machine}-{abi}",
+ )
+ monkeypatch.setattr(sys, "executable", path)
+ elf_header = _get_elf_header()
+ assert elf_header.e_ident_class == elf_class
+ assert elf_header.e_ident_data == elf_data
+ assert elf_header.e_machine == elf_machine
+
+
+@pytest.mark.parametrize(
+ "content", [None, "invalid-magic", "invalid-class", "invalid-data", "too-short"]
+)
+def test_get_elf_header_bad_executable(monkeypatch, content):
+ if content:
+ path = os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ f"hello-world-{content}",
+ )
+ else:
+ path = None
+ monkeypatch.setattr(sys, "executable", path)
+ assert _get_elf_header() is None
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/test_markers.py b/testing/web-platform/tests/tools/third_party/packaging/tests/test_markers.py
new file mode 100644
index 0000000000..c2640afeb6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/test_markers.py
@@ -0,0 +1,310 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import collections
+import itertools
+import os
+import platform
+import sys
+
+import pytest
+
+from packaging.markers import (
+ InvalidMarker,
+ Marker,
+ Node,
+ UndefinedComparison,
+ UndefinedEnvironmentName,
+ default_environment,
+ format_full_version,
+)
+
+VARIABLES = [
+ "extra",
+ "implementation_name",
+ "implementation_version",
+ "os_name",
+ "platform_machine",
+ "platform_release",
+ "platform_system",
+ "platform_version",
+ "python_full_version",
+ "python_version",
+ "platform_python_implementation",
+ "sys_platform",
+]
+
+PEP_345_VARIABLES = [
+ "os.name",
+ "sys.platform",
+ "platform.version",
+ "platform.machine",
+ "platform.python_implementation",
+]
+
+SETUPTOOLS_VARIABLES = ["python_implementation"]
+
+OPERATORS = ["===", "==", ">=", "<=", "!=", "~=", ">", "<", "in", "not in"]
+
+VALUES = [
+ "1.0",
+ "5.6a0",
+ "dog",
+ "freebsd",
+ "literally any string can go here",
+ "things @#4 dsfd (((",
+]
+
+
+class TestNode:
+ @pytest.mark.parametrize("value", ["one", "two", None, 3, 5, []])
+ def test_accepts_value(self, value):
+ assert Node(value).value == value
+
+ @pytest.mark.parametrize("value", ["one", "two", None, 3, 5, []])
+ def test_str(self, value):
+ assert str(Node(value)) == str(value)
+
+ @pytest.mark.parametrize("value", ["one", "two", None, 3, 5, []])
+ def test_repr(self, value):
+ assert repr(Node(value)) == f"<Node({str(value)!r})>"
+
+ def test_base_class(self):
+ with pytest.raises(NotImplementedError):
+ Node("cover all the code").serialize()
+
+
+class TestOperatorEvaluation:
+ def test_prefers_pep440(self):
+ assert Marker('"2.7.9" < "foo"').evaluate(dict(foo="2.7.10"))
+
+ def test_falls_back_to_python(self):
+ assert Marker('"b" > "a"').evaluate(dict(a="a"))
+
+ def test_fails_when_undefined(self):
+ with pytest.raises(UndefinedComparison):
+ Marker("'2.7.0' ~= os_name").evaluate()
+
+
+FakeVersionInfo = collections.namedtuple(
+ "FakeVersionInfo", ["major", "minor", "micro", "releaselevel", "serial"]
+)
+
+
+class TestDefaultEnvironment:
+ def test_matches_expected(self):
+ environment = default_environment()
+
+ iver = "{0.major}.{0.minor}.{0.micro}".format(sys.implementation.version)
+ if sys.implementation.version.releaselevel != "final":
+ iver = "{0}{1[0]}{2}".format(
+ iver,
+ sys.implementation.version.releaselevel,
+ sys.implementation.version.serial,
+ )
+
+ assert environment == {
+ "implementation_name": sys.implementation.name,
+ "implementation_version": iver,
+ "os_name": os.name,
+ "platform_machine": platform.machine(),
+ "platform_release": platform.release(),
+ "platform_system": platform.system(),
+ "platform_version": platform.version(),
+ "python_full_version": platform.python_version(),
+ "platform_python_implementation": platform.python_implementation(),
+ "python_version": ".".join(platform.python_version_tuple()[:2]),
+ "sys_platform": sys.platform,
+ }
+
+ def test_multidigit_minor_version(self, monkeypatch):
+ version_info = (3, 10, 0, "final", 0)
+ monkeypatch.setattr(sys, "version_info", version_info, raising=False)
+
+ monkeypatch.setattr(platform, "python_version", lambda: "3.10.0", raising=False)
+ monkeypatch.setattr(
+ platform, "python_version_tuple", lambda: ("3", "10", "0"), raising=False
+ )
+
+ environment = default_environment()
+ assert environment["python_version"] == "3.10"
+
+ def tests_when_releaselevel_final(self):
+ v = FakeVersionInfo(3, 4, 2, "final", 0)
+ assert format_full_version(v) == "3.4.2"
+
+ def tests_when_releaselevel_not_final(self):
+ v = FakeVersionInfo(3, 4, 2, "beta", 4)
+ assert format_full_version(v) == "3.4.2b4"
+
+
+class TestMarker:
+ @pytest.mark.parametrize(
+ "marker_string",
+ [
+ "{} {} {!r}".format(*i)
+ for i in itertools.product(VARIABLES, OPERATORS, VALUES)
+ ]
+ + [
+ "{2!r} {1} {0}".format(*i)
+ for i in itertools.product(VARIABLES, OPERATORS, VALUES)
+ ],
+ )
+ def test_parses_valid(self, marker_string):
+ Marker(marker_string)
+
+ @pytest.mark.parametrize(
+ "marker_string",
+ [
+ "this_isnt_a_real_variable >= '1.0'",
+ "python_version",
+ "(python_version)",
+ "python_version >= 1.0 and (python_version)",
+ ],
+ )
+ def test_parses_invalid(self, marker_string):
+ with pytest.raises(InvalidMarker):
+ Marker(marker_string)
+
+ @pytest.mark.parametrize(
+ ("marker_string", "expected"),
+ [
+ # Test the different quoting rules
+ ("python_version == '2.7'", 'python_version == "2.7"'),
+ ('python_version == "2.7"', 'python_version == "2.7"'),
+ # Test and/or expressions
+ (
+ 'python_version == "2.7" and os_name == "linux"',
+ 'python_version == "2.7" and os_name == "linux"',
+ ),
+ (
+ 'python_version == "2.7" or os_name == "linux"',
+ 'python_version == "2.7" or os_name == "linux"',
+ ),
+ (
+ 'python_version == "2.7" and os_name == "linux" or '
+ 'sys_platform == "win32"',
+ 'python_version == "2.7" and os_name == "linux" or '
+ 'sys_platform == "win32"',
+ ),
+ # Test nested expressions and grouping with ()
+ ('(python_version == "2.7")', 'python_version == "2.7"'),
+ (
+ '(python_version == "2.7" and sys_platform == "win32")',
+ 'python_version == "2.7" and sys_platform == "win32"',
+ ),
+ (
+ 'python_version == "2.7" and (sys_platform == "win32" or '
+ 'sys_platform == "linux")',
+ 'python_version == "2.7" and (sys_platform == "win32" or '
+ 'sys_platform == "linux")',
+ ),
+ ],
+ )
+ def test_str_and_repr(self, marker_string, expected):
+ m = Marker(marker_string)
+ assert str(m) == expected
+ assert repr(m) == f"<Marker({str(m)!r})>"
+
+ def test_extra_with_no_extra_in_environment(self):
+ # We can't evaluate an extra if no extra is passed into the environment
+ m = Marker("extra == 'security'")
+ with pytest.raises(UndefinedEnvironmentName):
+ m.evaluate()
+
+ @pytest.mark.parametrize(
+ ("marker_string", "environment", "expected"),
+ [
+ (f"os_name == '{os.name}'", None, True),
+ ("os_name == 'foo'", {"os_name": "foo"}, True),
+ ("os_name == 'foo'", {"os_name": "bar"}, False),
+ ("'2.7' in python_version", {"python_version": "2.7.5"}, True),
+ ("'2.7' not in python_version", {"python_version": "2.7"}, False),
+ (
+ "os_name == 'foo' and python_version ~= '2.7.0'",
+ {"os_name": "foo", "python_version": "2.7.6"},
+ True,
+ ),
+ (
+ "python_version ~= '2.7.0' and (os_name == 'foo' or "
+ "os_name == 'bar')",
+ {"os_name": "foo", "python_version": "2.7.4"},
+ True,
+ ),
+ (
+ "python_version ~= '2.7.0' and (os_name == 'foo' or "
+ "os_name == 'bar')",
+ {"os_name": "bar", "python_version": "2.7.4"},
+ True,
+ ),
+ (
+ "python_version ~= '2.7.0' and (os_name == 'foo' or "
+ "os_name == 'bar')",
+ {"os_name": "other", "python_version": "2.7.4"},
+ False,
+ ),
+ ("extra == 'security'", {"extra": "quux"}, False),
+ ("extra == 'security'", {"extra": "security"}, True),
+ ],
+ )
+ def test_evaluates(self, marker_string, environment, expected):
+ args = [] if environment is None else [environment]
+ assert Marker(marker_string).evaluate(*args) == expected
+
+ @pytest.mark.parametrize(
+ "marker_string",
+ [
+ "{} {} {!r}".format(*i)
+ for i in itertools.product(PEP_345_VARIABLES, OPERATORS, VALUES)
+ ]
+ + [
+ "{2!r} {1} {0}".format(*i)
+ for i in itertools.product(PEP_345_VARIABLES, OPERATORS, VALUES)
+ ],
+ )
+ def test_parses_pep345_valid(self, marker_string):
+ Marker(marker_string)
+
+ @pytest.mark.parametrize(
+ ("marker_string", "environment", "expected"),
+ [
+ (f"os.name == '{os.name}'", None, True),
+ ("sys.platform == 'win32'", {"sys_platform": "linux2"}, False),
+ ("platform.version in 'Ubuntu'", {"platform_version": "#39"}, False),
+ ("platform.machine=='x86_64'", {"platform_machine": "x86_64"}, True),
+ (
+ "platform.python_implementation=='Jython'",
+ {"platform_python_implementation": "CPython"},
+ False,
+ ),
+ (
+ "python_version == '2.5' and platform.python_implementation"
+ "!= 'Jython'",
+ {"python_version": "2.7"},
+ False,
+ ),
+ ],
+ )
+ def test_evaluate_pep345_markers(self, marker_string, environment, expected):
+ args = [] if environment is None else [environment]
+ assert Marker(marker_string).evaluate(*args) == expected
+
+ @pytest.mark.parametrize(
+ "marker_string",
+ [
+ "{} {} {!r}".format(*i)
+ for i in itertools.product(SETUPTOOLS_VARIABLES, OPERATORS, VALUES)
+ ]
+ + [
+ "{2!r} {1} {0}".format(*i)
+ for i in itertools.product(SETUPTOOLS_VARIABLES, OPERATORS, VALUES)
+ ],
+ )
+ def test_parses_setuptools_legacy_valid(self, marker_string):
+ Marker(marker_string)
+
+ def test_evaluate_setuptools_legacy_markers(self):
+ marker_string = "python_implementation=='Jython'"
+ args = [{"platform_python_implementation": "CPython"}]
+ assert Marker(marker_string).evaluate(*args) is False
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/test_musllinux.py b/testing/web-platform/tests/tools/third_party/packaging/tests/test_musllinux.py
new file mode 100644
index 0000000000..d2c87ca159
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/test_musllinux.py
@@ -0,0 +1,146 @@
+import collections
+import io
+import pathlib
+import struct
+import subprocess
+
+import pretend
+import pytest
+
+from packaging import _musllinux
+from packaging._musllinux import (
+ _get_musl_version,
+ _MuslVersion,
+ _parse_ld_musl_from_elf,
+ _parse_musl_version,
+)
+
+MUSL_AMD64 = "musl libc (x86_64)\nVersion 1.2.2\n"
+MUSL_I386 = "musl libc (i386)\nVersion 1.2.1\n"
+MUSL_AARCH64 = "musl libc (aarch64)\nVersion 1.1.24\n"
+MUSL_INVALID = "musl libc (invalid)\n"
+MUSL_UNKNOWN = "musl libc (unknown)\nVersion unknown\n"
+
+MUSL_DIR = pathlib.Path(__file__).with_name("musllinux").resolve()
+
+BIN_GLIBC_X86_64 = MUSL_DIR.joinpath("glibc-x86_64")
+BIN_MUSL_X86_64 = MUSL_DIR.joinpath("musl-x86_64")
+BIN_MUSL_I386 = MUSL_DIR.joinpath("musl-i386")
+BIN_MUSL_AARCH64 = MUSL_DIR.joinpath("musl-aarch64")
+
+LD_MUSL_X86_64 = "/lib/ld-musl-x86_64.so.1"
+LD_MUSL_I386 = "/lib/ld-musl-i386.so.1"
+LD_MUSL_AARCH64 = "/lib/ld-musl-aarch64.so.1"
+
+
+@pytest.fixture(autouse=True)
+def clear_lru_cache():
+ yield
+ _get_musl_version.cache_clear()
+
+
+@pytest.mark.parametrize(
+ "output, version",
+ [
+ (MUSL_AMD64, _MuslVersion(1, 2)),
+ (MUSL_I386, _MuslVersion(1, 2)),
+ (MUSL_AARCH64, _MuslVersion(1, 1)),
+ (MUSL_INVALID, None),
+ (MUSL_UNKNOWN, None),
+ ],
+ ids=["amd64-1.2.2", "i386-1.2.1", "aarch64-1.1.24", "invalid", "unknown"],
+)
+def test_parse_musl_version(output, version):
+ assert _parse_musl_version(output) == version
+
+
+@pytest.mark.parametrize(
+ "executable, location",
+ [
+ (BIN_GLIBC_X86_64, None),
+ (BIN_MUSL_X86_64, LD_MUSL_X86_64),
+ (BIN_MUSL_I386, LD_MUSL_I386),
+ (BIN_MUSL_AARCH64, LD_MUSL_AARCH64),
+ ],
+ ids=["glibc", "x86_64", "i386", "aarch64"],
+)
+def test_parse_ld_musl_from_elf(executable, location):
+ with executable.open("rb") as f:
+ assert _parse_ld_musl_from_elf(f) == location
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ # Too short for magic.
+ b"\0",
+ # Enough for magic, but not ELF.
+ b"#!/bin/bash" + b"\0" * 16,
+ # ELF, but unknown byte declaration.
+ b"\x7fELF\3" + b"\0" * 16,
+ ],
+ ids=["no-magic", "wrong-magic", "unknown-format"],
+)
+def test_parse_ld_musl_from_elf_invalid(data):
+ assert _parse_ld_musl_from_elf(io.BytesIO(data)) is None
+
+
+@pytest.mark.parametrize(
+ "head",
+ [
+ 25, # Enough for magic, but not the section definitions.
+ 58, # Enough for section definitions, but not the actual sections.
+ ],
+)
+def test_parse_ld_musl_from_elf_invalid_section(head):
+ data = BIN_MUSL_X86_64.read_bytes()[:head]
+ assert _parse_ld_musl_from_elf(io.BytesIO(data)) is None
+
+
+def test_parse_ld_musl_from_elf_no_interpreter_section():
+ with BIN_MUSL_X86_64.open("rb") as f:
+ data = f.read()
+
+ # Change all sections to *not* PT_INTERP.
+ unpacked = struct.unpack("16BHHIQQQIHHH", data[:58])
+ *_, e_phoff, _, _, _, e_phentsize, e_phnum = unpacked
+ for i in range(e_phnum + 1):
+ sb = e_phoff + e_phentsize * i
+ se = sb + 56
+ section = struct.unpack("IIQQQQQQ", data[sb:se])
+ data = data[:sb] + struct.pack("IIQQQQQQ", 0, *section[1:]) + data[se:]
+
+ assert _parse_ld_musl_from_elf(io.BytesIO(data)) is None
+
+
+@pytest.mark.parametrize(
+ "executable, output, version, ld_musl",
+ [
+ (MUSL_DIR.joinpath("does-not-exist"), "error", None, None),
+ (BIN_GLIBC_X86_64, "error", None, None),
+ (BIN_MUSL_X86_64, MUSL_AMD64, _MuslVersion(1, 2), LD_MUSL_X86_64),
+ (BIN_MUSL_I386, MUSL_I386, _MuslVersion(1, 2), LD_MUSL_I386),
+ (BIN_MUSL_AARCH64, MUSL_AARCH64, _MuslVersion(1, 1), LD_MUSL_AARCH64),
+ ],
+ ids=["does-not-exist", "glibc", "x86_64", "i386", "aarch64"],
+)
+def test_get_musl_version(monkeypatch, executable, output, version, ld_musl):
+ def mock_run(*args, **kwargs):
+ return collections.namedtuple("Proc", "stderr")(output)
+
+ run_recorder = pretend.call_recorder(mock_run)
+ monkeypatch.setattr(_musllinux.subprocess, "run", run_recorder)
+
+ assert _get_musl_version(str(executable)) == version
+
+ if ld_musl is not None:
+ expected_calls = [
+ pretend.call(
+ [ld_musl],
+ stderr=subprocess.PIPE,
+ universal_newlines=True,
+ )
+ ]
+ else:
+ expected_calls = []
+ assert run_recorder.calls == expected_calls
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/test_requirements.py b/testing/web-platform/tests/tools/third_party/packaging/tests/test_requirements.py
new file mode 100644
index 0000000000..f2c209c45c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/test_requirements.py
@@ -0,0 +1,197 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import pytest
+
+from packaging.markers import Marker
+from packaging.requirements import URL, URL_AND_MARKER, InvalidRequirement, Requirement
+from packaging.specifiers import SpecifierSet
+
+
+class TestRequirements:
+ def test_string_specifier_marker(self):
+ requirement = 'name[bar]>=3; python_version == "2.7"'
+ req = Requirement(requirement)
+ assert str(req) == requirement
+
+ def test_string_url(self):
+ requirement = "name@ http://foo.com"
+ req = Requirement(requirement)
+ assert str(req) == requirement
+
+ def test_string_url_with_marker(self):
+ requirement = 'name@ http://foo.com ; extra == "feature"'
+ req = Requirement(requirement)
+ assert str(req) == requirement
+
+ def test_repr(self):
+ req = Requirement("name")
+ assert repr(req) == "<Requirement('name')>"
+
+ def _assert_requirement(
+ self, req, name, url=None, extras=[], specifier="", marker=None
+ ):
+ assert req.name == name
+ assert req.url == url
+ assert sorted(req.extras) == sorted(extras)
+ assert str(req.specifier) == specifier
+ if marker:
+ assert str(req.marker) == marker
+ else:
+ assert req.marker is None
+
+ def test_simple_names(self):
+ for name in ("A", "aa", "name"):
+ req = Requirement(name)
+ self._assert_requirement(req, name)
+
+ def test_name_with_other_characters(self):
+ name = "foo-bar.quux_baz"
+ req = Requirement(name)
+ self._assert_requirement(req, name)
+
+ def test_invalid_name(self):
+ with pytest.raises(InvalidRequirement):
+ Requirement("foo!")
+
+ def test_name_with_version(self):
+ req = Requirement("name>=3")
+ self._assert_requirement(req, "name", specifier=">=3")
+
+ def test_with_legacy_version(self):
+ req = Requirement("name==1.0.org1")
+ self._assert_requirement(req, "name", specifier="==1.0.org1")
+
+ def test_with_legacy_version_and_marker(self):
+ req = Requirement("name>=1.x.y;python_version=='2.6'")
+ self._assert_requirement(
+ req, "name", specifier=">=1.x.y", marker='python_version == "2.6"'
+ )
+
+ def test_version_with_parens_and_whitespace(self):
+ req = Requirement("name (==4)")
+ self._assert_requirement(req, "name", specifier="==4")
+
+ def test_name_with_multiple_versions(self):
+ req = Requirement("name>=3,<2")
+ self._assert_requirement(req, "name", specifier="<2,>=3")
+
+ def test_name_with_multiple_versions_and_whitespace(self):
+ req = Requirement("name >=2, <3")
+ self._assert_requirement(req, "name", specifier="<3,>=2")
+
+ def test_extras(self):
+ req = Requirement("foobar [quux,bar]")
+ self._assert_requirement(req, "foobar", extras=["bar", "quux"])
+
+ def test_empty_extras(self):
+ req = Requirement("foo[]")
+ self._assert_requirement(req, "foo")
+
+ def test_url(self):
+ url_section = "@ http://example.com"
+ parsed = URL.parseString(url_section)
+ assert parsed.url == "http://example.com"
+
+ def test_url_and_marker(self):
+ instring = "@ http://example.com ; os_name=='a'"
+ parsed = URL_AND_MARKER.parseString(instring)
+ assert parsed.url == "http://example.com"
+ assert str(parsed.marker) == 'os_name == "a"'
+
+ def test_invalid_url(self):
+ with pytest.raises(InvalidRequirement) as e:
+ Requirement("name @ gopher:/foo/com")
+ assert "Invalid URL: " in str(e.value)
+ assert "gopher:/foo/com" in str(e.value)
+
+ def test_file_url(self):
+ req = Requirement("name @ file:///absolute/path")
+ self._assert_requirement(req, "name", "file:///absolute/path")
+ req = Requirement("name @ file://.")
+ self._assert_requirement(req, "name", "file://.")
+
+ def test_invalid_file_urls(self):
+ with pytest.raises(InvalidRequirement):
+ Requirement("name @ file:.")
+ with pytest.raises(InvalidRequirement):
+ Requirement("name @ file:/.")
+
+ def test_extras_and_url_and_marker(self):
+ req = Requirement("name [fred,bar] @ http://foo.com ; python_version=='2.7'")
+ self._assert_requirement(
+ req,
+ "name",
+ extras=["bar", "fred"],
+ url="http://foo.com",
+ marker='python_version == "2.7"',
+ )
+
+ def test_complex_url_and_marker(self):
+ url = "https://example.com/name;v=1.1/?query=foo&bar=baz#blah"
+ req = Requirement("foo @ %s ; python_version=='3.4'" % url)
+ self._assert_requirement(req, "foo", url=url, marker='python_version == "3.4"')
+
+ def test_multiple_markers(self):
+ req = Requirement(
+ "name[quux, strange];python_version<'2.7' and " "platform_version=='2'"
+ )
+ marker = 'python_version < "2.7" and platform_version == "2"'
+ self._assert_requirement(req, "name", extras=["strange", "quux"], marker=marker)
+
+ def test_multiple_comparison_markers(self):
+ req = Requirement("name; os_name=='a' and os_name=='b' or os_name=='c'")
+ marker = 'os_name == "a" and os_name == "b" or os_name == "c"'
+ self._assert_requirement(req, "name", marker=marker)
+
+ def test_invalid_marker(self):
+ with pytest.raises(InvalidRequirement):
+ Requirement("name; foobar=='x'")
+
+ def test_types(self):
+ req = Requirement("foobar[quux]<2,>=3; os_name=='a'")
+ assert isinstance(req.name, str)
+ assert isinstance(req.extras, set)
+ assert req.url is None
+ assert isinstance(req.specifier, SpecifierSet)
+ assert isinstance(req.marker, Marker)
+
+ def test_types_with_nothing(self):
+ req = Requirement("foobar")
+ assert isinstance(req.name, str)
+ assert isinstance(req.extras, set)
+ assert req.url is None
+ assert isinstance(req.specifier, SpecifierSet)
+ assert req.marker is None
+
+ def test_types_with_url(self):
+ req = Requirement("foobar @ http://foo.com")
+ assert isinstance(req.name, str)
+ assert isinstance(req.extras, set)
+ assert isinstance(req.url, str)
+ assert isinstance(req.specifier, SpecifierSet)
+ assert req.marker is None
+
+ def test_sys_platform_linux_equal(self):
+ req = Requirement('something>=1.2.3; sys_platform == "foo"')
+
+ assert req.name == "something"
+ assert req.marker is not None
+ assert req.marker.evaluate(dict(sys_platform="foo")) is True
+ assert req.marker.evaluate(dict(sys_platform="bar")) is False
+
+ def test_sys_platform_linux_in(self):
+ req = Requirement("aviato>=1.2.3; 'f' in sys_platform")
+
+ assert req.name == "aviato"
+ assert req.marker is not None
+ assert req.marker.evaluate(dict(sys_platform="foo")) is True
+ assert req.marker.evaluate(dict(sys_platform="bar")) is False
+
+ def test_parseexception_error_msg(self):
+ with pytest.raises(InvalidRequirement) as e:
+ Requirement("toto 42")
+ assert "Expected stringEnd" in str(e.value) or (
+ "Expected string_end" in str(e.value) # pyparsing>=3.0.0
+ )
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/test_specifiers.py b/testing/web-platform/tests/tools/third_party/packaging/tests/test_specifiers.py
new file mode 100644
index 0000000000..ca21fa1de0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/test_specifiers.py
@@ -0,0 +1,998 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import itertools
+import operator
+import warnings
+
+import pytest
+
+from packaging.specifiers import (
+ InvalidSpecifier,
+ LegacySpecifier,
+ Specifier,
+ SpecifierSet,
+)
+from packaging.version import LegacyVersion, Version, parse
+
+from .test_version import LEGACY_VERSIONS, VERSIONS
+
+LEGACY_SPECIFIERS = [
+ "==2.1.0.3",
+ "!=2.2.0.5",
+ "<=5",
+ ">=7.9a1",
+ "<1.0.dev1",
+ ">2.0.post1",
+]
+
+SPECIFIERS = [
+ "~=2.0",
+ "==2.1.*",
+ "==2.1.0.3",
+ "!=2.2.*",
+ "!=2.2.0.5",
+ "<=5",
+ ">=7.9a1",
+ "<1.0.dev1",
+ ">2.0.post1",
+ "===lolwat",
+]
+
+
+class TestSpecifier:
+ @pytest.mark.parametrize("specifier", SPECIFIERS)
+ def test_specifiers_valid(self, specifier):
+ Specifier(specifier)
+
+ @pytest.mark.parametrize(
+ "specifier",
+ [
+ # Operator-less specifier
+ "2.0",
+ # Invalid operator
+ "=>2.0",
+ # Version-less specifier
+ "==",
+ # Local segment on operators which don't support them
+ "~=1.0+5",
+ ">=1.0+deadbeef",
+ "<=1.0+abc123",
+ ">1.0+watwat",
+ "<1.0+1.0",
+ # Prefix matching on operators which don't support them
+ "~=1.0.*",
+ ">=1.0.*",
+ "<=1.0.*",
+ ">1.0.*",
+ "<1.0.*",
+ # Combination of local and prefix matching on operators which do
+ # support one or the other
+ "==1.0.*+5",
+ "!=1.0.*+deadbeef",
+ # Prefix matching cannot be used inside of a local version
+ "==1.0+5.*",
+ "!=1.0+deadbeef.*",
+ # Prefix matching must appear at the end
+ "==1.0.*.5",
+ # Compatible operator requires 2 digits in the release operator
+ "~=1",
+ # Cannot use a prefix matching after a .devN version
+ "==1.0.dev1.*",
+ "!=1.0.dev1.*",
+ ],
+ )
+ def test_specifiers_invalid(self, specifier):
+ with pytest.raises(InvalidSpecifier):
+ Specifier(specifier)
+
+ @pytest.mark.parametrize(
+ "version",
+ [
+ # Various development release incarnations
+ "1.0dev",
+ "1.0.dev",
+ "1.0dev1",
+ "1.0dev",
+ "1.0-dev",
+ "1.0-dev1",
+ "1.0DEV",
+ "1.0.DEV",
+ "1.0DEV1",
+ "1.0DEV",
+ "1.0.DEV1",
+ "1.0-DEV",
+ "1.0-DEV1",
+ # Various alpha incarnations
+ "1.0a",
+ "1.0.a",
+ "1.0.a1",
+ "1.0-a",
+ "1.0-a1",
+ "1.0alpha",
+ "1.0.alpha",
+ "1.0.alpha1",
+ "1.0-alpha",
+ "1.0-alpha1",
+ "1.0A",
+ "1.0.A",
+ "1.0.A1",
+ "1.0-A",
+ "1.0-A1",
+ "1.0ALPHA",
+ "1.0.ALPHA",
+ "1.0.ALPHA1",
+ "1.0-ALPHA",
+ "1.0-ALPHA1",
+ # Various beta incarnations
+ "1.0b",
+ "1.0.b",
+ "1.0.b1",
+ "1.0-b",
+ "1.0-b1",
+ "1.0beta",
+ "1.0.beta",
+ "1.0.beta1",
+ "1.0-beta",
+ "1.0-beta1",
+ "1.0B",
+ "1.0.B",
+ "1.0.B1",
+ "1.0-B",
+ "1.0-B1",
+ "1.0BETA",
+ "1.0.BETA",
+ "1.0.BETA1",
+ "1.0-BETA",
+ "1.0-BETA1",
+ # Various release candidate incarnations
+ "1.0c",
+ "1.0.c",
+ "1.0.c1",
+ "1.0-c",
+ "1.0-c1",
+ "1.0rc",
+ "1.0.rc",
+ "1.0.rc1",
+ "1.0-rc",
+ "1.0-rc1",
+ "1.0C",
+ "1.0.C",
+ "1.0.C1",
+ "1.0-C",
+ "1.0-C1",
+ "1.0RC",
+ "1.0.RC",
+ "1.0.RC1",
+ "1.0-RC",
+ "1.0-RC1",
+ # Various post release incarnations
+ "1.0post",
+ "1.0.post",
+ "1.0post1",
+ "1.0post",
+ "1.0-post",
+ "1.0-post1",
+ "1.0POST",
+ "1.0.POST",
+ "1.0POST1",
+ "1.0POST",
+ "1.0.POST1",
+ "1.0-POST",
+ "1.0-POST1",
+ "1.0-5",
+ # Local version case insensitivity
+ "1.0+AbC"
+ # Integer Normalization
+ "1.01",
+ "1.0a05",
+ "1.0b07",
+ "1.0c056",
+ "1.0rc09",
+ "1.0.post000",
+ "1.1.dev09000",
+ "00!1.2",
+ "0100!0.0",
+ # Various other normalizations
+ "v1.0",
+ " \r \f \v v1.0\t\n",
+ ],
+ )
+ def test_specifiers_normalized(self, version):
+ if "+" not in version:
+ ops = ["~=", "==", "!=", "<=", ">=", "<", ">"]
+ else:
+ ops = ["==", "!="]
+
+ for op in ops:
+ Specifier(op + version)
+
+ @pytest.mark.parametrize(
+ ("specifier", "expected"),
+ [
+ # Single item specifiers should just be reflexive
+ ("!=2.0", "!=2.0"),
+ ("<2.0", "<2.0"),
+ ("<=2.0", "<=2.0"),
+ ("==2.0", "==2.0"),
+ (">2.0", ">2.0"),
+ (">=2.0", ">=2.0"),
+ ("~=2.0", "~=2.0"),
+ # Spaces should be removed
+ ("< 2", "<2"),
+ ],
+ )
+ def test_specifiers_str_and_repr(self, specifier, expected):
+ spec = Specifier(specifier)
+
+ assert str(spec) == expected
+ assert repr(spec) == f"<Specifier({expected!r})>"
+
+ @pytest.mark.parametrize("specifier", SPECIFIERS)
+ def test_specifiers_hash(self, specifier):
+ assert hash(Specifier(specifier)) == hash(Specifier(specifier))
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ itertools.chain(
+ *
+ # Verify that the equal (==) operator works correctly
+ [[(x, x, operator.eq) for x in SPECIFIERS]]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [
+ [(x, y, operator.ne) for j, y in enumerate(SPECIFIERS) if i != j]
+ for i, x in enumerate(SPECIFIERS)
+ ]
+ ),
+ )
+ def test_comparison_true(self, left, right, op):
+ assert op(Specifier(left), Specifier(right))
+ assert op(left, Specifier(right))
+ assert op(Specifier(left), right)
+
+ @pytest.mark.parametrize(("left", "right"), [("==2.8.0", "==2.8")])
+ def test_comparison_canonicalizes(self, left, right):
+ assert Specifier(left) == Specifier(right)
+ assert left == Specifier(right)
+ assert Specifier(left) == right
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ itertools.chain(
+ *
+ # Verify that the equal (==) operator works correctly
+ [[(x, x, operator.ne) for x in SPECIFIERS]]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [
+ [(x, y, operator.eq) for j, y in enumerate(SPECIFIERS) if i != j]
+ for i, x in enumerate(SPECIFIERS)
+ ]
+ ),
+ )
+ def test_comparison_false(self, left, right, op):
+ assert not op(Specifier(left), Specifier(right))
+ assert not op(left, Specifier(right))
+ assert not op(Specifier(left), right)
+
+ def test_comparison_non_specifier(self):
+ assert Specifier("==1.0") != 12
+ assert not Specifier("==1.0") == 12
+ assert Specifier("==1.0") != "12"
+ assert not Specifier("==1.0") == "12"
+
+ @pytest.mark.parametrize(
+ ("version", "spec", "expected"),
+ [
+ (v, s, True)
+ for v, s in [
+ # Test the equality operation
+ ("2.0", "==2"),
+ ("2.0", "==2.0"),
+ ("2.0", "==2.0.0"),
+ ("2.0+deadbeef", "==2"),
+ ("2.0+deadbeef", "==2.0"),
+ ("2.0+deadbeef", "==2.0.0"),
+ ("2.0+deadbeef", "==2+deadbeef"),
+ ("2.0+deadbeef", "==2.0+deadbeef"),
+ ("2.0+deadbeef", "==2.0.0+deadbeef"),
+ ("2.0+deadbeef.0", "==2.0.0+deadbeef.00"),
+ # Test the equality operation with a prefix
+ ("2.dev1", "==2.*"),
+ ("2a1", "==2.*"),
+ ("2a1.post1", "==2.*"),
+ ("2b1", "==2.*"),
+ ("2b1.dev1", "==2.*"),
+ ("2c1", "==2.*"),
+ ("2c1.post1.dev1", "==2.*"),
+ ("2rc1", "==2.*"),
+ ("2", "==2.*"),
+ ("2.0", "==2.*"),
+ ("2.0.0", "==2.*"),
+ ("2.0.post1", "==2.0.post1.*"),
+ ("2.0.post1.dev1", "==2.0.post1.*"),
+ ("2.1+local.version", "==2.1.*"),
+ # Test the in-equality operation
+ ("2.1", "!=2"),
+ ("2.1", "!=2.0"),
+ ("2.0.1", "!=2"),
+ ("2.0.1", "!=2.0"),
+ ("2.0.1", "!=2.0.0"),
+ ("2.0", "!=2.0+deadbeef"),
+ # Test the in-equality operation with a prefix
+ ("2.0", "!=3.*"),
+ ("2.1", "!=2.0.*"),
+ # Test the greater than equal operation
+ ("2.0", ">=2"),
+ ("2.0", ">=2.0"),
+ ("2.0", ">=2.0.0"),
+ ("2.0.post1", ">=2"),
+ ("2.0.post1.dev1", ">=2"),
+ ("3", ">=2"),
+ # Test the less than equal operation
+ ("2.0", "<=2"),
+ ("2.0", "<=2.0"),
+ ("2.0", "<=2.0.0"),
+ ("2.0.dev1", "<=2"),
+ ("2.0a1", "<=2"),
+ ("2.0a1.dev1", "<=2"),
+ ("2.0b1", "<=2"),
+ ("2.0b1.post1", "<=2"),
+ ("2.0c1", "<=2"),
+ ("2.0c1.post1.dev1", "<=2"),
+ ("2.0rc1", "<=2"),
+ ("1", "<=2"),
+ # Test the greater than operation
+ ("3", ">2"),
+ ("2.1", ">2.0"),
+ ("2.0.1", ">2"),
+ ("2.1.post1", ">2"),
+ ("2.1+local.version", ">2"),
+ # Test the less than operation
+ ("1", "<2"),
+ ("2.0", "<2.1"),
+ ("2.0.dev0", "<2.1"),
+ # Test the compatibility operation
+ ("1", "~=1.0"),
+ ("1.0.1", "~=1.0"),
+ ("1.1", "~=1.0"),
+ ("1.9999999", "~=1.0"),
+ ("1.1", "~=1.0a1"),
+ # Test that epochs are handled sanely
+ ("2!1.0", "~=2!1.0"),
+ ("2!1.0", "==2!1.*"),
+ ("2!1.0", "==2!1.0"),
+ ("2!1.0", "!=1.0"),
+ ("1.0", "!=2!1.0"),
+ ("1.0", "<=2!0.1"),
+ ("2!1.0", ">=2.0"),
+ ("1.0", "<2!0.1"),
+ ("2!1.0", ">2.0"),
+ # Test some normalization rules
+ ("2.0.5", ">2.0dev"),
+ ]
+ ]
+ + [
+ (v, s, False)
+ for v, s in [
+ # Test the equality operation
+ ("2.1", "==2"),
+ ("2.1", "==2.0"),
+ ("2.1", "==2.0.0"),
+ ("2.0", "==2.0+deadbeef"),
+ # Test the equality operation with a prefix
+ ("2.0", "==3.*"),
+ ("2.1", "==2.0.*"),
+ # Test the in-equality operation
+ ("2.0", "!=2"),
+ ("2.0", "!=2.0"),
+ ("2.0", "!=2.0.0"),
+ ("2.0+deadbeef", "!=2"),
+ ("2.0+deadbeef", "!=2.0"),
+ ("2.0+deadbeef", "!=2.0.0"),
+ ("2.0+deadbeef", "!=2+deadbeef"),
+ ("2.0+deadbeef", "!=2.0+deadbeef"),
+ ("2.0+deadbeef", "!=2.0.0+deadbeef"),
+ ("2.0+deadbeef.0", "!=2.0.0+deadbeef.00"),
+ # Test the in-equality operation with a prefix
+ ("2.dev1", "!=2.*"),
+ ("2a1", "!=2.*"),
+ ("2a1.post1", "!=2.*"),
+ ("2b1", "!=2.*"),
+ ("2b1.dev1", "!=2.*"),
+ ("2c1", "!=2.*"),
+ ("2c1.post1.dev1", "!=2.*"),
+ ("2rc1", "!=2.*"),
+ ("2", "!=2.*"),
+ ("2.0", "!=2.*"),
+ ("2.0.0", "!=2.*"),
+ ("2.0.post1", "!=2.0.post1.*"),
+ ("2.0.post1.dev1", "!=2.0.post1.*"),
+ # Test the greater than equal operation
+ ("2.0.dev1", ">=2"),
+ ("2.0a1", ">=2"),
+ ("2.0a1.dev1", ">=2"),
+ ("2.0b1", ">=2"),
+ ("2.0b1.post1", ">=2"),
+ ("2.0c1", ">=2"),
+ ("2.0c1.post1.dev1", ">=2"),
+ ("2.0rc1", ">=2"),
+ ("1", ">=2"),
+ # Test the less than equal operation
+ ("2.0.post1", "<=2"),
+ ("2.0.post1.dev1", "<=2"),
+ ("3", "<=2"),
+ # Test the greater than operation
+ ("1", ">2"),
+ ("2.0.dev1", ">2"),
+ ("2.0a1", ">2"),
+ ("2.0a1.post1", ">2"),
+ ("2.0b1", ">2"),
+ ("2.0b1.dev1", ">2"),
+ ("2.0c1", ">2"),
+ ("2.0c1.post1.dev1", ">2"),
+ ("2.0rc1", ">2"),
+ ("2.0", ">2"),
+ ("2.0.post1", ">2"),
+ ("2.0.post1.dev1", ">2"),
+ ("2.0+local.version", ">2"),
+ # Test the less than operation
+ ("2.0.dev1", "<2"),
+ ("2.0a1", "<2"),
+ ("2.0a1.post1", "<2"),
+ ("2.0b1", "<2"),
+ ("2.0b2.dev1", "<2"),
+ ("2.0c1", "<2"),
+ ("2.0c1.post1.dev1", "<2"),
+ ("2.0rc1", "<2"),
+ ("2.0", "<2"),
+ ("2.post1", "<2"),
+ ("2.post1.dev1", "<2"),
+ ("3", "<2"),
+ # Test the compatibility operation
+ ("2.0", "~=1.0"),
+ ("1.1.0", "~=1.0.0"),
+ ("1.1.post1", "~=1.0.0"),
+ # Test that epochs are handled sanely
+ ("1.0", "~=2!1.0"),
+ ("2!1.0", "~=1.0"),
+ ("2!1.0", "==1.0"),
+ ("1.0", "==2!1.0"),
+ ("2!1.0", "==1.*"),
+ ("1.0", "==2!1.*"),
+ ("2!1.0", "!=2!1.0"),
+ ]
+ ],
+ )
+ def test_specifiers(self, version, spec, expected):
+ spec = Specifier(spec, prereleases=True)
+
+ if expected:
+ # Test that the plain string form works
+ assert version in spec
+ assert spec.contains(version)
+
+ # Test that the version instance form works
+ assert Version(version) in spec
+ assert spec.contains(Version(version))
+ else:
+ # Test that the plain string form works
+ assert version not in spec
+ assert not spec.contains(version)
+
+ # Test that the version instance form works
+ assert Version(version) not in spec
+ assert not spec.contains(Version(version))
+
+ @pytest.mark.parametrize(
+ ("version", "spec", "expected"),
+ [
+ # Test identity comparison by itself
+ ("lolwat", "===lolwat", True),
+ ("Lolwat", "===lolwat", True),
+ ("1.0", "===1.0", True),
+ ("nope", "===lolwat", False),
+ ("1.0.0", "===1.0", False),
+ ("1.0.dev0", "===1.0.dev0", True),
+ ],
+ )
+ def test_specifiers_identity(self, version, spec, expected):
+ spec = Specifier(spec)
+
+ if expected:
+ # Identity comparisons only support the plain string form
+ assert version in spec
+ else:
+ # Identity comparisons only support the plain string form
+ assert version not in spec
+
+ @pytest.mark.parametrize(
+ ("specifier", "expected"),
+ [
+ ("==1.0", False),
+ (">=1.0", False),
+ ("<=1.0", False),
+ ("~=1.0", False),
+ ("<1.0", False),
+ (">1.0", False),
+ ("<1.0.dev1", False),
+ (">1.0.dev1", False),
+ ("==1.0.*", False),
+ ("==1.0.dev1", True),
+ (">=1.0.dev1", True),
+ ("<=1.0.dev1", True),
+ ("~=1.0.dev1", True),
+ ],
+ )
+ def test_specifier_prereleases_detection(self, specifier, expected):
+ assert Specifier(specifier).prereleases == expected
+
+ @pytest.mark.parametrize(
+ ("specifier", "version", "expected"),
+ [
+ (">=1.0", "2.0.dev1", False),
+ (">=2.0.dev1", "2.0a1", True),
+ ("==2.0.*", "2.0a1.dev1", False),
+ ("==2.0a1.*", "2.0a1.dev1", True),
+ ("<=2.0", "1.0.dev1", False),
+ ("<=2.0.dev1", "1.0a1", True),
+ ],
+ )
+ def test_specifiers_prereleases(self, specifier, version, expected):
+ spec = Specifier(specifier)
+
+ if expected:
+ assert version in spec
+ spec.prereleases = False
+ assert version not in spec
+ else:
+ assert version not in spec
+ spec.prereleases = True
+ assert version in spec
+
+ @pytest.mark.parametrize(
+ ("specifier", "prereleases", "input", "expected"),
+ [
+ (">=1.0", None, ["2.0a1"], ["2.0a1"]),
+ (">=1.0.dev1", None, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
+ (">=1.0.dev1", False, ["1.0", "2.0a1"], ["1.0"]),
+ ],
+ )
+ def test_specifier_filter(self, specifier, prereleases, input, expected):
+ spec = Specifier(specifier)
+
+ kwargs = {"prereleases": prereleases} if prereleases is not None else {}
+
+ assert list(spec.filter(input, **kwargs)) == expected
+
+ @pytest.mark.xfail
+ def test_specifier_explicit_legacy(self):
+ assert Specifier("==1.0").contains(LegacyVersion("1.0"))
+
+ @pytest.mark.parametrize(
+ ("spec", "op"),
+ [
+ ("~=2.0", "~="),
+ ("==2.1.*", "=="),
+ ("==2.1.0.3", "=="),
+ ("!=2.2.*", "!="),
+ ("!=2.2.0.5", "!="),
+ ("<=5", "<="),
+ (">=7.9a1", ">="),
+ ("<1.0.dev1", "<"),
+ (">2.0.post1", ">"),
+ ("===lolwat", "==="),
+ ],
+ )
+ def test_specifier_operator_property(self, spec, op):
+ assert Specifier(spec).operator == op
+
+ @pytest.mark.parametrize(
+ ("spec", "version"),
+ [
+ ("~=2.0", "2.0"),
+ ("==2.1.*", "2.1.*"),
+ ("==2.1.0.3", "2.1.0.3"),
+ ("!=2.2.*", "2.2.*"),
+ ("!=2.2.0.5", "2.2.0.5"),
+ ("<=5", "5"),
+ (">=7.9a1", "7.9a1"),
+ ("<1.0.dev1", "1.0.dev1"),
+ (">2.0.post1", "2.0.post1"),
+ ("===lolwat", "lolwat"),
+ ],
+ )
+ def test_specifier_version_property(self, spec, version):
+ assert Specifier(spec).version == version
+
+ @pytest.mark.parametrize(
+ ("spec", "expected_length"),
+ [("", 0), ("==2.0", 1), (">=2.0", 1), (">=2.0,<3", 2), (">=2.0,<3,==2.4", 3)],
+ )
+ def test_length(self, spec, expected_length):
+ spec = SpecifierSet(spec)
+ assert len(spec) == expected_length
+
+ @pytest.mark.parametrize(
+ ("spec", "expected_items"),
+ [
+ ("", []),
+ ("==2.0", ["==2.0"]),
+ (">=2.0", [">=2.0"]),
+ (">=2.0,<3", [">=2.0", "<3"]),
+ (">=2.0,<3,==2.4", [">=2.0", "<3", "==2.4"]),
+ ],
+ )
+ def test_iteration(self, spec, expected_items):
+ spec = SpecifierSet(spec)
+ items = {str(item) for item in spec}
+ assert items == set(expected_items)
+
+
+class TestLegacySpecifier:
+ def test_legacy_specifier_is_deprecated(self):
+ with warnings.catch_warnings(record=True) as w:
+ LegacySpecifier(">=some-legacy-version")
+ assert len(w) == 1
+ assert issubclass(w[0].category, DeprecationWarning)
+
+ @pytest.mark.parametrize(
+ ("version", "spec", "expected"),
+ [
+ (v, s, True)
+ for v, s in [
+ # Test the equality operation
+ ("2.0", "==2"),
+ ("2.0", "==2.0"),
+ ("2.0", "==2.0.0"),
+ # Test the in-equality operation
+ ("2.1", "!=2"),
+ ("2.1", "!=2.0"),
+ ("2.0.1", "!=2"),
+ ("2.0.1", "!=2.0"),
+ ("2.0.1", "!=2.0.0"),
+ # Test the greater than equal operation
+ ("2.0", ">=2"),
+ ("2.0", ">=2.0"),
+ ("2.0", ">=2.0.0"),
+ ("2.0.post1", ">=2"),
+ ("2.0.post1.dev1", ">=2"),
+ ("3", ">=2"),
+ # Test the less than equal operation
+ ("2.0", "<=2"),
+ ("2.0", "<=2.0"),
+ ("2.0", "<=2.0.0"),
+ ("2.0.dev1", "<=2"),
+ ("2.0a1", "<=2"),
+ ("2.0a1.dev1", "<=2"),
+ ("2.0b1", "<=2"),
+ ("2.0b1.post1", "<=2"),
+ ("2.0c1", "<=2"),
+ ("2.0c1.post1.dev1", "<=2"),
+ ("2.0rc1", "<=2"),
+ ("1", "<=2"),
+ # Test the greater than operation
+ ("3", ">2"),
+ ("2.1", ">2.0"),
+ # Test the less than operation
+ ("1", "<2"),
+ ("2.0", "<2.1"),
+ ]
+ ]
+ + [
+ (v, s, False)
+ for v, s in [
+ # Test the equality operation
+ ("2.1", "==2"),
+ ("2.1", "==2.0"),
+ ("2.1", "==2.0.0"),
+ # Test the in-equality operation
+ ("2.0", "!=2"),
+ ("2.0", "!=2.0"),
+ ("2.0", "!=2.0.0"),
+ # Test the greater than equal operation
+ ("2.0.dev1", ">=2"),
+ ("2.0a1", ">=2"),
+ ("2.0a1.dev1", ">=2"),
+ ("2.0b1", ">=2"),
+ ("2.0b1.post1", ">=2"),
+ ("2.0c1", ">=2"),
+ ("2.0c1.post1.dev1", ">=2"),
+ ("2.0rc1", ">=2"),
+ ("1", ">=2"),
+ # Test the less than equal operation
+ ("2.0.post1", "<=2"),
+ ("2.0.post1.dev1", "<=2"),
+ ("3", "<=2"),
+ # Test the greater than operation
+ ("1", ">2"),
+ ("2.0.dev1", ">2"),
+ ("2.0a1", ">2"),
+ ("2.0a1.post1", ">2"),
+ ("2.0b1", ">2"),
+ ("2.0b1.dev1", ">2"),
+ ("2.0c1", ">2"),
+ ("2.0c1.post1.dev1", ">2"),
+ ("2.0rc1", ">2"),
+ ("2.0", ">2"),
+ # Test the less than operation
+ ("3", "<2"),
+ ]
+ ],
+ )
+ def test_specifiers(self, version, spec, expected):
+ spec = LegacySpecifier(spec, prereleases=True)
+
+ if expected:
+ # Test that the plain string form works
+ assert version in spec
+ assert spec.contains(version)
+
+ # Test that the version instance form works
+ assert LegacyVersion(version) in spec
+ assert spec.contains(LegacyVersion(version))
+ else:
+ # Test that the plain string form works
+ assert version not in spec
+ assert not spec.contains(version)
+
+ # Test that the version instance form works
+ assert LegacyVersion(version) not in spec
+ assert not spec.contains(LegacyVersion(version))
+
+ def test_specifier_explicit_prereleases(self):
+ spec = LegacySpecifier(">=1.0")
+ assert not spec.prereleases
+ spec.prereleases = True
+ assert spec.prereleases
+
+ spec = LegacySpecifier(">=1.0", prereleases=False)
+ assert not spec.prereleases
+ spec.prereleases = True
+ assert spec.prereleases
+
+ spec = LegacySpecifier(">=1.0", prereleases=True)
+ assert spec.prereleases
+ spec.prereleases = False
+ assert not spec.prereleases
+
+ spec = LegacySpecifier(">=1.0", prereleases=True)
+ assert spec.prereleases
+ spec.prereleases = None
+ assert not spec.prereleases
+
+
+class TestSpecifierSet:
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_empty_specifier(self, version):
+ spec = SpecifierSet(prereleases=True)
+
+ assert version in spec
+ assert spec.contains(version)
+ assert parse(version) in spec
+ assert spec.contains(parse(version))
+
+ def test_specifier_prereleases_explicit(self):
+ spec = SpecifierSet()
+ assert not spec.prereleases
+ assert "1.0.dev1" not in spec
+ assert not spec.contains("1.0.dev1")
+ spec.prereleases = True
+ assert spec.prereleases
+ assert "1.0.dev1" in spec
+ assert spec.contains("1.0.dev1")
+
+ spec = SpecifierSet(prereleases=True)
+ assert spec.prereleases
+ assert "1.0.dev1" in spec
+ assert spec.contains("1.0.dev1")
+ spec.prereleases = False
+ assert not spec.prereleases
+ assert "1.0.dev1" not in spec
+ assert not spec.contains("1.0.dev1")
+
+ spec = SpecifierSet(prereleases=True)
+ assert spec.prereleases
+ assert "1.0.dev1" in spec
+ assert spec.contains("1.0.dev1")
+ spec.prereleases = None
+ assert not spec.prereleases
+ assert "1.0.dev1" not in spec
+ assert not spec.contains("1.0.dev1")
+
+ def test_specifier_contains_prereleases(self):
+ spec = SpecifierSet()
+ assert spec.prereleases is None
+ assert not spec.contains("1.0.dev1")
+ assert spec.contains("1.0.dev1", prereleases=True)
+
+ spec = SpecifierSet(prereleases=True)
+ assert spec.prereleases
+ assert spec.contains("1.0.dev1")
+ assert not spec.contains("1.0.dev1", prereleases=False)
+
+ @pytest.mark.parametrize(
+ ("specifier", "specifier_prereleases", "prereleases", "input", "expected"),
+ [
+ # General test of the filter method
+ ("", None, None, ["1.0", "2.0a1"], ["1.0"]),
+ (">=1.0.dev1", None, None, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
+ ("", None, None, ["1.0a1"], ["1.0a1"]),
+ ("", None, None, ["1.0", Version("2.0")], ["1.0", Version("2.0")]),
+ ("", None, None, ["2.0dog", "1.0"], ["1.0"]),
+ # Test overriding with the prereleases parameter on filter
+ ("", None, False, ["1.0a1"], []),
+ (">=1.0.dev1", None, False, ["1.0", "2.0a1"], ["1.0"]),
+ ("", None, True, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
+ # Test overriding with the overall specifier
+ ("", True, None, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
+ ("", False, None, ["1.0", "2.0a1"], ["1.0"]),
+ (">=1.0.dev1", True, None, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
+ (">=1.0.dev1", False, None, ["1.0", "2.0a1"], ["1.0"]),
+ ("", True, None, ["1.0a1"], ["1.0a1"]),
+ ("", False, None, ["1.0a1"], []),
+ ],
+ )
+ def test_specifier_filter(
+ self, specifier_prereleases, specifier, prereleases, input, expected
+ ):
+ if specifier_prereleases is None:
+ spec = SpecifierSet(specifier)
+ else:
+ spec = SpecifierSet(specifier, prereleases=specifier_prereleases)
+
+ kwargs = {"prereleases": prereleases} if prereleases is not None else {}
+
+ assert list(spec.filter(input, **kwargs)) == expected
+
+ def test_legacy_specifiers_combined(self):
+ spec = SpecifierSet("<3,>1-1-1")
+ assert "2.0" in spec
+
+ @pytest.mark.parametrize(
+ ("specifier", "expected"),
+ [
+ # Single item specifiers should just be reflexive
+ ("!=2.0", "!=2.0"),
+ ("<2.0", "<2.0"),
+ ("<=2.0", "<=2.0"),
+ ("==2.0", "==2.0"),
+ (">2.0", ">2.0"),
+ (">=2.0", ">=2.0"),
+ ("~=2.0", "~=2.0"),
+ # Spaces should be removed
+ ("< 2", "<2"),
+ # Multiple item specifiers should work
+ ("!=2.0,>1.0", "!=2.0,>1.0"),
+ ("!=2.0 ,>1.0", "!=2.0,>1.0"),
+ ],
+ )
+ def test_specifiers_str_and_repr(self, specifier, expected):
+ spec = SpecifierSet(specifier)
+
+ assert str(spec) == expected
+ assert repr(spec) == f"<SpecifierSet({expected!r})>"
+
+ @pytest.mark.parametrize("specifier", SPECIFIERS + LEGACY_SPECIFIERS)
+ def test_specifiers_hash(self, specifier):
+ assert hash(SpecifierSet(specifier)) == hash(SpecifierSet(specifier))
+
+ @pytest.mark.parametrize(
+ ("left", "right", "expected"), [(">2.0", "<5.0", ">2.0,<5.0")]
+ )
+ def test_specifiers_combine(self, left, right, expected):
+ result = SpecifierSet(left) & SpecifierSet(right)
+ assert result == SpecifierSet(expected)
+
+ result = SpecifierSet(left) & right
+ assert result == SpecifierSet(expected)
+
+ result = SpecifierSet(left, prereleases=True) & SpecifierSet(right)
+ assert result == SpecifierSet(expected)
+ assert result.prereleases
+
+ result = SpecifierSet(left, prereleases=False) & SpecifierSet(right)
+ assert result == SpecifierSet(expected)
+ assert not result.prereleases
+
+ result = SpecifierSet(left) & SpecifierSet(right, prereleases=True)
+ assert result == SpecifierSet(expected)
+ assert result.prereleases
+
+ result = SpecifierSet(left) & SpecifierSet(right, prereleases=False)
+ assert result == SpecifierSet(expected)
+ assert not result.prereleases
+
+ result = SpecifierSet(left, prereleases=True) & SpecifierSet(
+ right, prereleases=True
+ )
+ assert result == SpecifierSet(expected)
+ assert result.prereleases
+
+ result = SpecifierSet(left, prereleases=False) & SpecifierSet(
+ right, prereleases=False
+ )
+ assert result == SpecifierSet(expected)
+ assert not result.prereleases
+
+ with pytest.raises(ValueError):
+ result = SpecifierSet(left, prereleases=True) & SpecifierSet(
+ right, prereleases=False
+ )
+
+ with pytest.raises(ValueError):
+ result = SpecifierSet(left, prereleases=False) & SpecifierSet(
+ right, prereleases=True
+ )
+
+ def test_specifiers_combine_not_implemented(self):
+ with pytest.raises(TypeError):
+ SpecifierSet() & 12
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ itertools.chain(
+ *
+ # Verify that the equal (==) operator works correctly
+ [[(x, x, operator.eq) for x in SPECIFIERS]]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [
+ [(x, y, operator.ne) for j, y in enumerate(SPECIFIERS) if i != j]
+ for i, x in enumerate(SPECIFIERS)
+ ]
+ ),
+ )
+ def test_comparison_true(self, left, right, op):
+ assert op(SpecifierSet(left), SpecifierSet(right))
+ assert op(SpecifierSet(left), Specifier(right))
+ assert op(Specifier(left), SpecifierSet(right))
+ assert op(left, SpecifierSet(right))
+ assert op(SpecifierSet(left), right)
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ itertools.chain(
+ *
+ # Verify that the equal (==) operator works correctly
+ [[(x, x, operator.ne) for x in SPECIFIERS]]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [
+ [(x, y, operator.eq) for j, y in enumerate(SPECIFIERS) if i != j]
+ for i, x in enumerate(SPECIFIERS)
+ ]
+ ),
+ )
+ def test_comparison_false(self, left, right, op):
+ assert not op(SpecifierSet(left), SpecifierSet(right))
+ assert not op(SpecifierSet(left), Specifier(right))
+ assert not op(Specifier(left), SpecifierSet(right))
+ assert not op(left, SpecifierSet(right))
+ assert not op(SpecifierSet(left), right)
+
+ @pytest.mark.parametrize(("left", "right"), [("==2.8.0", "==2.8")])
+ def test_comparison_canonicalizes(self, left, right):
+ assert SpecifierSet(left) == SpecifierSet(right)
+ assert left == SpecifierSet(right)
+ assert SpecifierSet(left) == right
+
+ def test_comparison_non_specifier(self):
+ assert SpecifierSet("==1.0") != 12
+ assert not SpecifierSet("==1.0") == 12
+
+ @pytest.mark.parametrize(
+ ("version", "specifier", "expected"),
+ [
+ ("1.0.0+local", "==1.0.0", True),
+ ("1.0.0+local", "!=1.0.0", False),
+ ("1.0.0+local", "<=1.0.0", True),
+ ("1.0.0+local", ">=1.0.0", True),
+ ("1.0.0+local", "<1.0.0", False),
+ ("1.0.0+local", ">1.0.0", False),
+ ],
+ )
+ def test_comparison_ignores_local(self, version, specifier, expected):
+ assert (Version(version) in SpecifierSet(specifier)) == expected
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/test_structures.py b/testing/web-platform/tests/tools/third_party/packaging/tests/test_structures.py
new file mode 100644
index 0000000000..f8115e5742
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/test_structures.py
@@ -0,0 +1,59 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import pytest
+
+from packaging._structures import Infinity, NegativeInfinity
+
+
+def test_infinity_repr():
+ repr(Infinity) == "Infinity"
+
+
+def test_negative_infinity_repr():
+ repr(NegativeInfinity) == "-Infinity"
+
+
+def test_infinity_hash():
+ assert hash(Infinity) == hash(Infinity)
+
+
+def test_negative_infinity_hash():
+ assert hash(NegativeInfinity) == hash(NegativeInfinity)
+
+
+@pytest.mark.parametrize("left", [1, "a", ("b", 4)])
+def test_infinity_comparison(left):
+ assert left < Infinity
+ assert left <= Infinity
+ assert not left == Infinity
+ assert left != Infinity
+ assert not left > Infinity
+ assert not left >= Infinity
+
+
+@pytest.mark.parametrize("left", [1, "a", ("b", 4)])
+def test_negative_infinity_lesser(left):
+ assert not left < NegativeInfinity
+ assert not left <= NegativeInfinity
+ assert not left == NegativeInfinity
+ assert left != NegativeInfinity
+ assert left > NegativeInfinity
+ assert left >= NegativeInfinity
+
+
+def test_infinity_equal():
+ assert Infinity == Infinity
+
+
+def test_negative_infinity_equal():
+ assert NegativeInfinity == NegativeInfinity
+
+
+def test_negate_infinity():
+ assert isinstance(-Infinity, NegativeInfinity.__class__)
+
+
+def test_negate_negative_infinity():
+ assert isinstance(-NegativeInfinity, Infinity.__class__)
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/test_tags.py b/testing/web-platform/tests/tools/third_party/packaging/tests/test_tags.py
new file mode 100644
index 0000000000..446dee4ef7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/test_tags.py
@@ -0,0 +1,1191 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+
+import collections.abc
+
+try:
+ import ctypes
+except ImportError:
+ ctypes = None
+import os
+import pathlib
+import platform
+import sys
+import sysconfig
+import types
+
+import pretend
+import pytest
+
+from packaging import tags
+from packaging._musllinux import _MuslVersion
+
+
+@pytest.fixture
+def example_tag():
+ return tags.Tag("py3", "none", "any")
+
+
+@pytest.fixture
+def manylinux_module(monkeypatch):
+ monkeypatch.setattr(tags._manylinux, "_get_glibc_version", lambda *args: (2, 20))
+ module_name = "_manylinux"
+ module = types.ModuleType(module_name)
+ monkeypatch.setitem(sys.modules, module_name, module)
+ return module
+
+
+@pytest.fixture
+def mock_interpreter_name(monkeypatch):
+ def mock(name):
+ name = name.lower()
+ if sys.implementation.name != name:
+ monkeypatch.setattr(sys.implementation, "name", name)
+ return True
+ return False
+
+ return mock
+
+
+class TestTag:
+ def test_lowercasing(self):
+ tag = tags.Tag("PY3", "None", "ANY")
+ assert tag.interpreter == "py3"
+ assert tag.abi == "none"
+ assert tag.platform == "any"
+
+ def test_equality(self):
+ args = "py3", "none", "any"
+ assert tags.Tag(*args) == tags.Tag(*args)
+
+ def test_equality_fails_with_non_tag(self):
+ assert not tags.Tag("py3", "none", "any") == "non-tag"
+
+ def test_hashing(self, example_tag):
+ tags = {example_tag} # Should not raise TypeError.
+ assert example_tag in tags
+
+ def test_hash_equality(self, example_tag):
+ equal_tag = tags.Tag("py3", "none", "any")
+ assert example_tag == equal_tag # Sanity check.
+ assert example_tag.__hash__() == equal_tag.__hash__()
+
+ def test_str(self, example_tag):
+ assert str(example_tag) == "py3-none-any"
+
+ def test_repr(self, example_tag):
+ assert repr(example_tag) == "<py3-none-any @ {tag_id}>".format(
+ tag_id=id(example_tag)
+ )
+
+ def test_attribute_access(self, example_tag):
+ assert example_tag.interpreter == "py3"
+ assert example_tag.abi == "none"
+ assert example_tag.platform == "any"
+
+
+class TestParseTag:
+ def test_simple(self, example_tag):
+ parsed_tags = tags.parse_tag(str(example_tag))
+ assert parsed_tags == {example_tag}
+
+ def test_multi_interpreter(self, example_tag):
+ expected = {example_tag, tags.Tag("py2", "none", "any")}
+ given = tags.parse_tag("py2.py3-none-any")
+ assert given == expected
+
+ def test_multi_platform(self):
+ expected = {
+ tags.Tag("cp37", "cp37m", platform)
+ for platform in (
+ "macosx_10_6_intel",
+ "macosx_10_9_intel",
+ "macosx_10_9_x86_64",
+ "macosx_10_10_intel",
+ "macosx_10_10_x86_64",
+ )
+ }
+ given = tags.parse_tag(
+ "cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64."
+ "macosx_10_10_intel.macosx_10_10_x86_64"
+ )
+ assert given == expected
+
+
+class TestInterpreterName:
+ def test_sys_implementation_name(self, monkeypatch):
+ class MockImplementation:
+ pass
+
+ mock_implementation = MockImplementation()
+ mock_implementation.name = "sillywalk"
+ monkeypatch.setattr(sys, "implementation", mock_implementation, raising=False)
+ assert tags.interpreter_name() == "sillywalk"
+
+ def test_interpreter_short_names(self, mock_interpreter_name, monkeypatch):
+ mock_interpreter_name("cpython")
+ assert tags.interpreter_name() == "cp"
+
+
+class TestInterpreterVersion:
+ def test_warn(self, monkeypatch):
+ class MockConfigVar:
+ def __init__(self, return_):
+ self.warn = None
+ self._return = return_
+
+ def __call__(self, name, warn):
+ self.warn = warn
+ return self._return
+
+ mock_config_var = MockConfigVar("38")
+ monkeypatch.setattr(tags, "_get_config_var", mock_config_var)
+ tags.interpreter_version(warn=True)
+ assert mock_config_var.warn
+
+ def test_python_version_nodot(self, monkeypatch):
+ monkeypatch.setattr(tags, "_get_config_var", lambda var, warn: "NN")
+ assert tags.interpreter_version() == "NN"
+
+ @pytest.mark.parametrize(
+ "version_info,version_str",
+ [
+ ((1, 2, 3), "12"),
+ ((1, 12, 3), "112"),
+ ((11, 2, 3), "112"),
+ ((11, 12, 3), "1112"),
+ ((1, 2, 13), "12"),
+ ],
+ )
+ def test_sys_version_info(self, version_info, version_str, monkeypatch):
+ monkeypatch.setattr(tags, "_get_config_var", lambda *args, **kwargs: None)
+ monkeypatch.setattr(sys, "version_info", version_info)
+ assert tags.interpreter_version() == version_str
+
+
+class TestMacOSPlatforms:
+ @pytest.mark.parametrize(
+ "arch, is_32bit, expected",
+ [
+ ("i386", True, "i386"),
+ ("ppc", True, "ppc"),
+ ("x86_64", False, "x86_64"),
+ ("x86_64", True, "i386"),
+ ("ppc64", False, "ppc64"),
+ ("ppc64", True, "ppc"),
+ ],
+ )
+ def test_architectures(self, arch, is_32bit, expected):
+ assert tags._mac_arch(arch, is_32bit=is_32bit) == expected
+
+ @pytest.mark.parametrize(
+ "version,arch,expected",
+ [
+ (
+ (10, 15),
+ "x86_64",
+ ["x86_64", "intel", "fat64", "fat32", "universal2", "universal"],
+ ),
+ (
+ (10, 4),
+ "x86_64",
+ ["x86_64", "intel", "fat64", "fat32", "universal2", "universal"],
+ ),
+ ((10, 3), "x86_64", []),
+ ((10, 15), "i386", ["i386", "intel", "fat32", "fat", "universal"]),
+ ((10, 4), "i386", ["i386", "intel", "fat32", "fat", "universal"]),
+ ((10, 3), "intel", ["intel", "universal"]),
+ ((10, 5), "intel", ["intel", "universal"]),
+ ((10, 15), "intel", ["intel", "universal"]),
+ ((10, 3), "i386", []),
+ ((10, 15), "ppc64", []),
+ ((10, 6), "ppc64", []),
+ ((10, 5), "ppc64", ["ppc64", "fat64", "universal"]),
+ ((10, 3), "ppc64", []),
+ ((10, 15), "ppc", []),
+ ((10, 7), "ppc", []),
+ ((10, 6), "ppc", ["ppc", "fat32", "fat", "universal"]),
+ ((10, 0), "ppc", ["ppc", "fat32", "fat", "universal"]),
+ ((11, 0), "riscv", ["riscv"]),
+ (
+ (11, 0),
+ "x86_64",
+ ["x86_64", "intel", "fat64", "fat32", "universal2", "universal"],
+ ),
+ ((11, 0), "arm64", ["arm64", "universal2"]),
+ ((11, 1), "arm64", ["arm64", "universal2"]),
+ ((12, 0), "arm64", ["arm64", "universal2"]),
+ ],
+ )
+ def test_binary_formats(self, version, arch, expected):
+ assert tags._mac_binary_formats(version, arch) == expected
+
+ def test_version_detection(self, monkeypatch):
+ if platform.system() != "Darwin":
+ monkeypatch.setattr(
+ platform, "mac_ver", lambda: ("10.14", ("", "", ""), "x86_64")
+ )
+ version = platform.mac_ver()[0].split(".")
+ major = version[0]
+ minor = version[1] if major == "10" else "0"
+ expected = f"macosx_{major}_{minor}"
+
+ platforms = list(tags.mac_platforms(arch="x86_64"))
+ print(platforms, expected)
+ assert platforms[0].startswith(expected)
+
+ @pytest.mark.parametrize("arch", ["x86_64", "i386"])
+ def test_arch_detection(self, arch, monkeypatch):
+ if platform.system() != "Darwin" or platform.mac_ver()[2] != arch:
+ monkeypatch.setattr(
+ platform, "mac_ver", lambda: ("10.14", ("", "", ""), arch)
+ )
+ monkeypatch.setattr(tags, "_mac_arch", lambda *args: arch)
+ assert next(tags.mac_platforms((10, 14))).endswith(arch)
+
+ def test_mac_platforms(self):
+ platforms = list(tags.mac_platforms((10, 5), "x86_64"))
+ assert platforms == [
+ "macosx_10_5_x86_64",
+ "macosx_10_5_intel",
+ "macosx_10_5_fat64",
+ "macosx_10_5_fat32",
+ "macosx_10_5_universal2",
+ "macosx_10_5_universal",
+ "macosx_10_4_x86_64",
+ "macosx_10_4_intel",
+ "macosx_10_4_fat64",
+ "macosx_10_4_fat32",
+ "macosx_10_4_universal2",
+ "macosx_10_4_universal",
+ ]
+
+ assert len(list(tags.mac_platforms((10, 17), "x86_64"))) == 14 * 6
+
+ assert not list(tags.mac_platforms((10, 0), "x86_64"))
+
+ @pytest.mark.parametrize("major,minor", [(11, 0), (11, 3), (12, 0), (12, 3)])
+ def test_macos_11(self, major, minor):
+ platforms = list(tags.mac_platforms((major, minor), "x86_64"))
+ assert "macosx_11_0_arm64" not in platforms
+ assert "macosx_11_0_x86_64" in platforms
+ assert "macosx_11_3_x86_64" not in platforms
+ assert "macosx_11_0_universal" in platforms
+ assert "macosx_11_0_universal2" in platforms
+ # Mac OS "10.16" is the version number that binaries compiled against an old
+ # (pre 11.0) SDK will see. It can also be enabled explicitly for a process
+ # with the environment variable SYSTEM_VERSION_COMPAT=1.
+ assert "macosx_10_16_x86_64" in platforms
+ assert "macosx_10_15_x86_64" in platforms
+ assert "macosx_10_15_universal2" in platforms
+ assert "macosx_10_4_x86_64" in platforms
+ assert "macosx_10_3_x86_64" not in platforms
+ if major >= 12:
+ assert "macosx_12_0_x86_64" in platforms
+ assert "macosx_12_0_universal" in platforms
+ assert "macosx_12_0_universal2" in platforms
+
+ platforms = list(tags.mac_platforms((major, minor), "arm64"))
+ assert "macosx_11_0_arm64" in platforms
+ assert "macosx_11_3_arm64" not in platforms
+ assert "macosx_11_0_universal" not in platforms
+ assert "macosx_11_0_universal2" in platforms
+ assert "macosx_10_15_universal2" in platforms
+ assert "macosx_10_15_x86_64" not in platforms
+ assert "macosx_10_4_x86_64" not in platforms
+ assert "macosx_10_3_x86_64" not in platforms
+ if major >= 12:
+ assert "macosx_12_0_arm64" in platforms
+ assert "macosx_12_0_universal2" in platforms
+
+
+class TestManylinuxPlatform:
+ def teardown_method(self):
+ # Clear the version cache
+ tags._manylinux._get_glibc_version.cache_clear()
+
+ def test_get_config_var_does_not_log(self, monkeypatch):
+ debug = pretend.call_recorder(lambda *a: None)
+ monkeypatch.setattr(tags.logger, "debug", debug)
+ tags._get_config_var("missing")
+ assert debug.calls == []
+
+ def test_get_config_var_does_log(self, monkeypatch):
+ debug = pretend.call_recorder(lambda *a: None)
+ monkeypatch.setattr(tags.logger, "debug", debug)
+ tags._get_config_var("missing", warn=True)
+ assert debug.calls == [
+ pretend.call(
+ "Config variable '%s' is unset, Python ABI tag may be incorrect",
+ "missing",
+ )
+ ]
+
+ @pytest.mark.parametrize(
+ "arch,is_32bit,expected",
+ [
+ ("linux-x86_64", False, "linux_x86_64"),
+ ("linux-x86_64", True, "linux_i686"),
+ ("linux-aarch64", False, "linux_aarch64"),
+ ("linux-aarch64", True, "linux_armv7l"),
+ ],
+ )
+ def test_linux_platforms_32_64bit_on_64bit_os(
+ self, arch, is_32bit, expected, monkeypatch
+ ):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: arch)
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False)
+ monkeypatch.setattr(tags._manylinux, "_is_compatible", lambda *args: False)
+ linux_platform = list(tags._linux_platforms(is_32bit=is_32bit))[-1]
+ assert linux_platform == expected
+
+ def test_linux_platforms_manylinux_unsupported(self, monkeypatch):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_x86_64")
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False)
+ monkeypatch.setattr(tags._manylinux, "_is_compatible", lambda *args: False)
+ linux_platform = list(tags._linux_platforms(is_32bit=False))
+ assert linux_platform == ["linux_x86_64"]
+
+ def test_linux_platforms_manylinux1(self, monkeypatch):
+ monkeypatch.setattr(
+ tags._manylinux, "_is_compatible", lambda name, *args: name == "manylinux1"
+ )
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_x86_64")
+ monkeypatch.setattr(platform, "machine", lambda: "x86_64")
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False)
+ platforms = list(tags._linux_platforms(is_32bit=False))
+ arch = platform.machine()
+ assert platforms == ["manylinux1_" + arch, "linux_" + arch]
+
+ def test_linux_platforms_manylinux2010(self, monkeypatch):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_x86_64")
+ monkeypatch.setattr(platform, "machine", lambda: "x86_64")
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.12", raising=False)
+ platforms = list(tags._linux_platforms(is_32bit=False))
+ arch = platform.machine()
+ expected = [
+ "manylinux_2_12_" + arch,
+ "manylinux2010_" + arch,
+ "manylinux_2_11_" + arch,
+ "manylinux_2_10_" + arch,
+ "manylinux_2_9_" + arch,
+ "manylinux_2_8_" + arch,
+ "manylinux_2_7_" + arch,
+ "manylinux_2_6_" + arch,
+ "manylinux_2_5_" + arch,
+ "manylinux1_" + arch,
+ "linux_" + arch,
+ ]
+ assert platforms == expected
+
+ def test_linux_platforms_manylinux2014(self, monkeypatch):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_x86_64")
+ monkeypatch.setattr(platform, "machine", lambda: "x86_64")
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.17", raising=False)
+ platforms = list(tags._linux_platforms(is_32bit=False))
+ arch = platform.machine()
+ expected = [
+ "manylinux_2_17_" + arch,
+ "manylinux2014_" + arch,
+ "manylinux_2_16_" + arch,
+ "manylinux_2_15_" + arch,
+ "manylinux_2_14_" + arch,
+ "manylinux_2_13_" + arch,
+ "manylinux_2_12_" + arch,
+ "manylinux2010_" + arch,
+ "manylinux_2_11_" + arch,
+ "manylinux_2_10_" + arch,
+ "manylinux_2_9_" + arch,
+ "manylinux_2_8_" + arch,
+ "manylinux_2_7_" + arch,
+ "manylinux_2_6_" + arch,
+ "manylinux_2_5_" + arch,
+ "manylinux1_" + arch,
+ "linux_" + arch,
+ ]
+ assert platforms == expected
+
+ def test_linux_platforms_manylinux2014_armhf_abi(self, monkeypatch):
+ monkeypatch.setattr(tags._manylinux, "_glibc_version_string", lambda: "2.30")
+ monkeypatch.setattr(
+ tags._manylinux,
+ "_is_compatible",
+ lambda name, *args: name == "manylinux2014",
+ )
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_armv7l")
+ monkeypatch.setattr(
+ sys,
+ "executable",
+ os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ "hello-world-armv7l-armhf",
+ ),
+ )
+ platforms = list(tags._linux_platforms(is_32bit=True))
+ expected = ["manylinux2014_armv7l", "linux_armv7l"]
+ assert platforms == expected
+
+ def test_linux_platforms_manylinux2014_i386_abi(self, monkeypatch):
+ monkeypatch.setattr(tags._manylinux, "_glibc_version_string", lambda: "2.17")
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_x86_64")
+ monkeypatch.setattr(
+ sys,
+ "executable",
+ os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ "hello-world-x86_64-i386",
+ ),
+ )
+ platforms = list(tags._linux_platforms(is_32bit=True))
+ expected = [
+ "manylinux_2_17_i686",
+ "manylinux2014_i686",
+ "manylinux_2_16_i686",
+ "manylinux_2_15_i686",
+ "manylinux_2_14_i686",
+ "manylinux_2_13_i686",
+ "manylinux_2_12_i686",
+ "manylinux2010_i686",
+ "manylinux_2_11_i686",
+ "manylinux_2_10_i686",
+ "manylinux_2_9_i686",
+ "manylinux_2_8_i686",
+ "manylinux_2_7_i686",
+ "manylinux_2_6_i686",
+ "manylinux_2_5_i686",
+ "manylinux1_i686",
+ "linux_i686",
+ ]
+ assert platforms == expected
+
+ def test_linux_platforms_manylinux_glibc3(self, monkeypatch):
+ # test for a future glic 3.x version
+ monkeypatch.setattr(tags._manylinux, "_glibc_version_string", lambda: "3.2")
+ monkeypatch.setattr(tags._manylinux, "_is_compatible", lambda name, *args: True)
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_aarch64")
+ monkeypatch.setattr(
+ sys,
+ "executable",
+ os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ "hello-world-aarch64",
+ ),
+ )
+ platforms = list(tags._linux_platforms(is_32bit=False))
+ expected = (
+ ["manylinux_3_2_aarch64", "manylinux_3_1_aarch64", "manylinux_3_0_aarch64"]
+ + [f"manylinux_2_{i}_aarch64" for i in range(50, 16, -1)]
+ + ["manylinux2014_aarch64", "linux_aarch64"]
+ )
+ assert platforms == expected
+
+ @pytest.mark.parametrize(
+ "native_arch, cross32_arch, musl_version",
+ [
+ ("aarch64", "armv7l", _MuslVersion(1, 1)),
+ ("i386", "i386", _MuslVersion(1, 2)),
+ ("x86_64", "i686", _MuslVersion(1, 2)),
+ ],
+ )
+ @pytest.mark.parametrize("cross32", [True, False], ids=["cross", "native"])
+ def test_linux_platforms_musllinux(
+ self, monkeypatch, native_arch, cross32_arch, musl_version, cross32
+ ):
+ fake_executable = str(
+ pathlib.Path(__file__)
+ .parent.joinpath("musllinux", f"musl-{native_arch}")
+ .resolve()
+ )
+ monkeypatch.setattr(tags._musllinux.sys, "executable", fake_executable)
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: f"linux_{native_arch}")
+ monkeypatch.setattr(tags._manylinux, "platform_tags", lambda *_: ())
+
+ recorder = pretend.call_recorder(lambda _: musl_version)
+ monkeypatch.setattr(tags._musllinux, "_get_musl_version", recorder)
+
+ platforms = list(tags._linux_platforms(is_32bit=cross32))
+ target_arch = cross32_arch if cross32 else native_arch
+ expected = [
+ f"musllinux_{musl_version[0]}_{minor}_{target_arch}"
+ for minor in range(musl_version[1], -1, -1)
+ ] + [f"linux_{target_arch}"]
+ assert platforms == expected
+
+ assert recorder.calls == [pretend.call(fake_executable)]
+
+ def test_linux_platforms_manylinux2014_armv6l(self, monkeypatch):
+ monkeypatch.setattr(
+ tags._manylinux, "_is_compatible", lambda name, _: name == "manylinux2014"
+ )
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_armv6l")
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False)
+ platforms = list(tags._linux_platforms(is_32bit=True))
+ expected = ["linux_armv6l"]
+ assert platforms == expected
+
+ @pytest.mark.parametrize(
+ "machine, abi, alt_machine",
+ [("x86_64", "x32", "i686"), ("armv7l", "armel", "armv7l")],
+ )
+ def test_linux_platforms_not_manylinux_abi(
+ self, monkeypatch, machine, abi, alt_machine
+ ):
+ monkeypatch.setattr(tags._manylinux, "_is_compatible", lambda name, _: False)
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: f"linux_{machine}")
+ monkeypatch.setattr(
+ sys,
+ "executable",
+ os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ f"hello-world-{machine}-{abi}",
+ ),
+ )
+ platforms = list(tags._linux_platforms(is_32bit=True))
+ expected = [f"linux_{alt_machine}"]
+ assert platforms == expected
+
+
+@pytest.mark.parametrize(
+ "platform_name,dispatch_func",
+ [
+ ("Darwin", "mac_platforms"),
+ ("Linux", "_linux_platforms"),
+ ("Generic", "_generic_platforms"),
+ ],
+)
+def test_platform_tags(platform_name, dispatch_func, monkeypatch):
+ expected = ["sillywalk"]
+ monkeypatch.setattr(platform, "system", lambda: platform_name)
+ monkeypatch.setattr(tags, dispatch_func, lambda: expected)
+ assert tags.platform_tags() == expected
+
+
+class TestCPythonABI:
+ @pytest.mark.parametrize(
+ "py_debug,gettotalrefcount,result",
+ [(1, False, True), (0, False, False), (None, True, True)],
+ )
+ def test_debug(self, py_debug, gettotalrefcount, result, monkeypatch):
+ config = {"Py_DEBUG": py_debug, "WITH_PYMALLOC": 0, "Py_UNICODE_SIZE": 2}
+ monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__)
+ if gettotalrefcount:
+ monkeypatch.setattr(sys, "gettotalrefcount", 1, raising=False)
+ expected = ["cp37d" if result else "cp37"]
+ assert tags._cpython_abis((3, 7)) == expected
+
+ def test_debug_file_extension(self, monkeypatch):
+ config = {"Py_DEBUG": None}
+ monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__)
+ monkeypatch.delattr(sys, "gettotalrefcount", raising=False)
+ monkeypatch.setattr(tags, "EXTENSION_SUFFIXES", {"_d.pyd"})
+ assert tags._cpython_abis((3, 8)) == ["cp38d", "cp38"]
+
+ @pytest.mark.parametrize(
+ "debug,expected", [(True, ["cp38d", "cp38"]), (False, ["cp38"])]
+ )
+ def test__debug_cp38(self, debug, expected, monkeypatch):
+ config = {"Py_DEBUG": debug}
+ monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__)
+ assert tags._cpython_abis((3, 8)) == expected
+
+ @pytest.mark.parametrize(
+ "pymalloc,version,result",
+ [
+ (1, (3, 7), True),
+ (0, (3, 7), False),
+ (None, (3, 7), True),
+ (1, (3, 8), False),
+ ],
+ )
+ def test_pymalloc(self, pymalloc, version, result, monkeypatch):
+ config = {"Py_DEBUG": 0, "WITH_PYMALLOC": pymalloc, "Py_UNICODE_SIZE": 2}
+ monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__)
+ base_abi = f"cp{version[0]}{version[1]}"
+ expected = [base_abi + "m" if result else base_abi]
+ assert tags._cpython_abis(version) == expected
+
+ @pytest.mark.parametrize(
+ "unicode_size,maxunicode,version,result",
+ [
+ (4, 0x10FFFF, (3, 2), True),
+ (2, 0xFFFF, (3, 2), False),
+ (None, 0x10FFFF, (3, 2), True),
+ (None, 0xFFFF, (3, 2), False),
+ (4, 0x10FFFF, (3, 3), False),
+ ],
+ )
+ def test_wide_unicode(self, unicode_size, maxunicode, version, result, monkeypatch):
+ config = {"Py_DEBUG": 0, "WITH_PYMALLOC": 0, "Py_UNICODE_SIZE": unicode_size}
+ monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__)
+ monkeypatch.setattr(sys, "maxunicode", maxunicode)
+ base_abi = "cp" + tags._version_nodot(version)
+ expected = [base_abi + "u" if result else base_abi]
+ assert tags._cpython_abis(version) == expected
+
+
+class TestCPythonTags:
+ def test_iterator_returned(self):
+ result_iterator = tags.cpython_tags(
+ (3, 8), ["cp38d", "cp38"], ["plat1", "plat2"]
+ )
+ assert isinstance(result_iterator, collections.abc.Iterator)
+
+ def test_all_args(self):
+ result_iterator = tags.cpython_tags(
+ (3, 11), ["cp311d", "cp311"], ["plat1", "plat2"]
+ )
+ result = list(result_iterator)
+ assert result == [
+ tags.Tag("cp311", "cp311d", "plat1"),
+ tags.Tag("cp311", "cp311d", "plat2"),
+ tags.Tag("cp311", "cp311", "plat1"),
+ tags.Tag("cp311", "cp311", "plat2"),
+ tags.Tag("cp311", "abi3", "plat1"),
+ tags.Tag("cp311", "abi3", "plat2"),
+ tags.Tag("cp311", "none", "plat1"),
+ tags.Tag("cp311", "none", "plat2"),
+ tags.Tag("cp310", "abi3", "plat1"),
+ tags.Tag("cp310", "abi3", "plat2"),
+ tags.Tag("cp39", "abi3", "plat1"),
+ tags.Tag("cp39", "abi3", "plat2"),
+ tags.Tag("cp38", "abi3", "plat1"),
+ tags.Tag("cp38", "abi3", "plat2"),
+ tags.Tag("cp37", "abi3", "plat1"),
+ tags.Tag("cp37", "abi3", "plat2"),
+ tags.Tag("cp36", "abi3", "plat1"),
+ tags.Tag("cp36", "abi3", "plat2"),
+ tags.Tag("cp35", "abi3", "plat1"),
+ tags.Tag("cp35", "abi3", "plat2"),
+ tags.Tag("cp34", "abi3", "plat1"),
+ tags.Tag("cp34", "abi3", "plat2"),
+ tags.Tag("cp33", "abi3", "plat1"),
+ tags.Tag("cp33", "abi3", "plat2"),
+ tags.Tag("cp32", "abi3", "plat1"),
+ tags.Tag("cp32", "abi3", "plat2"),
+ ]
+ result_iterator = tags.cpython_tags(
+ (3, 8), ["cp38d", "cp38"], ["plat1", "plat2"]
+ )
+ result = list(result_iterator)
+ assert result == [
+ tags.Tag("cp38", "cp38d", "plat1"),
+ tags.Tag("cp38", "cp38d", "plat2"),
+ tags.Tag("cp38", "cp38", "plat1"),
+ tags.Tag("cp38", "cp38", "plat2"),
+ tags.Tag("cp38", "abi3", "plat1"),
+ tags.Tag("cp38", "abi3", "plat2"),
+ tags.Tag("cp38", "none", "plat1"),
+ tags.Tag("cp38", "none", "plat2"),
+ tags.Tag("cp37", "abi3", "plat1"),
+ tags.Tag("cp37", "abi3", "plat2"),
+ tags.Tag("cp36", "abi3", "plat1"),
+ tags.Tag("cp36", "abi3", "plat2"),
+ tags.Tag("cp35", "abi3", "plat1"),
+ tags.Tag("cp35", "abi3", "plat2"),
+ tags.Tag("cp34", "abi3", "plat1"),
+ tags.Tag("cp34", "abi3", "plat2"),
+ tags.Tag("cp33", "abi3", "plat1"),
+ tags.Tag("cp33", "abi3", "plat2"),
+ tags.Tag("cp32", "abi3", "plat1"),
+ tags.Tag("cp32", "abi3", "plat2"),
+ ]
+
+ result = list(tags.cpython_tags((3, 3), ["cp33m"], ["plat1", "plat2"]))
+ assert result == [
+ tags.Tag("cp33", "cp33m", "plat1"),
+ tags.Tag("cp33", "cp33m", "plat2"),
+ tags.Tag("cp33", "abi3", "plat1"),
+ tags.Tag("cp33", "abi3", "plat2"),
+ tags.Tag("cp33", "none", "plat1"),
+ tags.Tag("cp33", "none", "plat2"),
+ tags.Tag("cp32", "abi3", "plat1"),
+ tags.Tag("cp32", "abi3", "plat2"),
+ ]
+
+ def test_python_version_defaults(self):
+ tag = next(tags.cpython_tags(abis=["abi3"], platforms=["any"]))
+ interpreter = "cp" + tags._version_nodot(sys.version_info[:2])
+ assert interpreter == tag.interpreter
+
+ def test_abi_defaults(self, monkeypatch):
+ monkeypatch.setattr(tags, "_cpython_abis", lambda _1, _2: ["cp38"])
+ result = list(tags.cpython_tags((3, 8), platforms=["any"]))
+ assert tags.Tag("cp38", "cp38", "any") in result
+ assert tags.Tag("cp38", "abi3", "any") in result
+ assert tags.Tag("cp38", "none", "any") in result
+
+ def test_abi_defaults_needs_underscore(self, monkeypatch):
+ monkeypatch.setattr(tags, "_cpython_abis", lambda _1, _2: ["cp311"])
+ result = list(tags.cpython_tags((3, 11), platforms=["any"]))
+ assert tags.Tag("cp311", "cp311", "any") in result
+ assert tags.Tag("cp311", "abi3", "any") in result
+ assert tags.Tag("cp311", "none", "any") in result
+
+ def test_platforms_defaults(self, monkeypatch):
+ monkeypatch.setattr(tags, "platform_tags", lambda: ["plat1"])
+ result = list(tags.cpython_tags((3, 8), abis=["whatever"]))
+ assert tags.Tag("cp38", "whatever", "plat1") in result
+
+ def test_platforms_defaults_needs_underscore(self, monkeypatch):
+ monkeypatch.setattr(tags, "platform_tags", lambda: ["plat1"])
+ result = list(tags.cpython_tags((3, 11), abis=["whatever"]))
+ assert tags.Tag("cp311", "whatever", "plat1") in result
+
+ def test_major_only_python_version(self):
+ result = list(tags.cpython_tags((3,), ["abi"], ["plat"]))
+ assert result == [
+ tags.Tag("cp3", "abi", "plat"),
+ tags.Tag("cp3", "none", "plat"),
+ ]
+
+ def test_major_only_python_version_with_default_abis(self):
+ result = list(tags.cpython_tags((3,), platforms=["plat"]))
+ assert result == [tags.Tag("cp3", "none", "plat")]
+
+ @pytest.mark.parametrize("abis", [[], ["abi3"], ["none"]])
+ def test_skip_redundant_abis(self, abis):
+ results = list(tags.cpython_tags((3, 0), abis=abis, platforms=["any"]))
+ assert results == [tags.Tag("cp30", "none", "any")]
+
+ def test_abi3_python33(self):
+ results = list(tags.cpython_tags((3, 3), abis=["cp33"], platforms=["plat"]))
+ assert results == [
+ tags.Tag("cp33", "cp33", "plat"),
+ tags.Tag("cp33", "abi3", "plat"),
+ tags.Tag("cp33", "none", "plat"),
+ tags.Tag("cp32", "abi3", "plat"),
+ ]
+
+ def test_no_excess_abi3_python32(self):
+ results = list(tags.cpython_tags((3, 2), abis=["cp32"], platforms=["plat"]))
+ assert results == [
+ tags.Tag("cp32", "cp32", "plat"),
+ tags.Tag("cp32", "abi3", "plat"),
+ tags.Tag("cp32", "none", "plat"),
+ ]
+
+ def test_no_abi3_python31(self):
+ results = list(tags.cpython_tags((3, 1), abis=["cp31"], platforms=["plat"]))
+ assert results == [
+ tags.Tag("cp31", "cp31", "plat"),
+ tags.Tag("cp31", "none", "plat"),
+ ]
+
+ def test_no_abi3_python27(self):
+ results = list(tags.cpython_tags((2, 7), abis=["cp27"], platforms=["plat"]))
+ assert results == [
+ tags.Tag("cp27", "cp27", "plat"),
+ tags.Tag("cp27", "none", "plat"),
+ ]
+
+
+class TestGenericTags:
+ @pytest.mark.skipif(
+ not sysconfig.get_config_var("SOABI"), reason="SOABI not defined"
+ )
+ def test__generic_abi_soabi_provided(self):
+ abi = sysconfig.get_config_var("SOABI").replace(".", "_").replace("-", "_")
+ assert [abi] == list(tags._generic_abi())
+
+ def test__generic_abi(self, monkeypatch):
+ monkeypatch.setattr(
+ sysconfig, "get_config_var", lambda key: "cpython-37m-darwin"
+ )
+ assert list(tags._generic_abi()) == ["cpython_37m_darwin"]
+
+ def test__generic_abi_no_soabi(self, monkeypatch):
+ monkeypatch.setattr(sysconfig, "get_config_var", lambda key: None)
+ assert not list(tags._generic_abi())
+
+ def test_generic_platforms(self):
+ platform = sysconfig.get_platform().replace("-", "_")
+ platform = platform.replace(".", "_")
+ assert list(tags._generic_platforms()) == [platform]
+
+ def test_iterator_returned(self):
+ result_iterator = tags.generic_tags("sillywalk33", ["abi"], ["plat1", "plat2"])
+ assert isinstance(result_iterator, collections.abc.Iterator)
+
+ def test_all_args(self):
+ result_iterator = tags.generic_tags("sillywalk33", ["abi"], ["plat1", "plat2"])
+ result = list(result_iterator)
+ assert result == [
+ tags.Tag("sillywalk33", "abi", "plat1"),
+ tags.Tag("sillywalk33", "abi", "plat2"),
+ tags.Tag("sillywalk33", "none", "plat1"),
+ tags.Tag("sillywalk33", "none", "plat2"),
+ ]
+
+ @pytest.mark.parametrize("abi", [[], ["none"]])
+ def test_abi_unspecified(self, abi):
+ no_abi = list(tags.generic_tags("sillywalk34", abi, ["plat1", "plat2"]))
+ assert no_abi == [
+ tags.Tag("sillywalk34", "none", "plat1"),
+ tags.Tag("sillywalk34", "none", "plat2"),
+ ]
+
+ def test_interpreter_default(self, monkeypatch):
+ monkeypatch.setattr(tags, "interpreter_name", lambda: "sillywalk")
+ monkeypatch.setattr(tags, "interpreter_version", lambda warn: "NN")
+ result = list(tags.generic_tags(abis=["none"], platforms=["any"]))
+ assert result == [tags.Tag("sillywalkNN", "none", "any")]
+
+ def test_abis_default(self, monkeypatch):
+ monkeypatch.setattr(tags, "_generic_abi", lambda: iter(["abi"]))
+ result = list(tags.generic_tags(interpreter="sillywalk", platforms=["any"]))
+ assert result == [
+ tags.Tag("sillywalk", "abi", "any"),
+ tags.Tag("sillywalk", "none", "any"),
+ ]
+
+ def test_platforms_default(self, monkeypatch):
+ monkeypatch.setattr(tags, "platform_tags", lambda: ["plat"])
+ result = list(tags.generic_tags(interpreter="sillywalk", abis=["none"]))
+ assert result == [tags.Tag("sillywalk", "none", "plat")]
+
+
+class TestCompatibleTags:
+ def test_all_args(self):
+ result = list(tags.compatible_tags((3, 3), "cp33", ["plat1", "plat2"]))
+ assert result == [
+ tags.Tag("py33", "none", "plat1"),
+ tags.Tag("py33", "none", "plat2"),
+ tags.Tag("py3", "none", "plat1"),
+ tags.Tag("py3", "none", "plat2"),
+ tags.Tag("py32", "none", "plat1"),
+ tags.Tag("py32", "none", "plat2"),
+ tags.Tag("py31", "none", "plat1"),
+ tags.Tag("py31", "none", "plat2"),
+ tags.Tag("py30", "none", "plat1"),
+ tags.Tag("py30", "none", "plat2"),
+ tags.Tag("cp33", "none", "any"),
+ tags.Tag("py33", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ tags.Tag("py32", "none", "any"),
+ tags.Tag("py31", "none", "any"),
+ tags.Tag("py30", "none", "any"),
+ ]
+
+ def test_all_args_needs_underscore(self):
+ result = list(tags.compatible_tags((3, 11), "cp311", ["plat1", "plat2"]))
+ assert result == [
+ tags.Tag("py311", "none", "plat1"),
+ tags.Tag("py311", "none", "plat2"),
+ tags.Tag("py3", "none", "plat1"),
+ tags.Tag("py3", "none", "plat2"),
+ tags.Tag("py310", "none", "plat1"),
+ tags.Tag("py310", "none", "plat2"),
+ tags.Tag("py39", "none", "plat1"),
+ tags.Tag("py39", "none", "plat2"),
+ tags.Tag("py38", "none", "plat1"),
+ tags.Tag("py38", "none", "plat2"),
+ tags.Tag("py37", "none", "plat1"),
+ tags.Tag("py37", "none", "plat2"),
+ tags.Tag("py36", "none", "plat1"),
+ tags.Tag("py36", "none", "plat2"),
+ tags.Tag("py35", "none", "plat1"),
+ tags.Tag("py35", "none", "plat2"),
+ tags.Tag("py34", "none", "plat1"),
+ tags.Tag("py34", "none", "plat2"),
+ tags.Tag("py33", "none", "plat1"),
+ tags.Tag("py33", "none", "plat2"),
+ tags.Tag("py32", "none", "plat1"),
+ tags.Tag("py32", "none", "plat2"),
+ tags.Tag("py31", "none", "plat1"),
+ tags.Tag("py31", "none", "plat2"),
+ tags.Tag("py30", "none", "plat1"),
+ tags.Tag("py30", "none", "plat2"),
+ tags.Tag("cp311", "none", "any"),
+ tags.Tag("py311", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ tags.Tag("py310", "none", "any"),
+ tags.Tag("py39", "none", "any"),
+ tags.Tag("py38", "none", "any"),
+ tags.Tag("py37", "none", "any"),
+ tags.Tag("py36", "none", "any"),
+ tags.Tag("py35", "none", "any"),
+ tags.Tag("py34", "none", "any"),
+ tags.Tag("py33", "none", "any"),
+ tags.Tag("py32", "none", "any"),
+ tags.Tag("py31", "none", "any"),
+ tags.Tag("py30", "none", "any"),
+ ]
+
+ def test_major_only_python_version(self):
+ result = list(tags.compatible_tags((3,), "cp33", ["plat"]))
+ assert result == [
+ tags.Tag("py3", "none", "plat"),
+ tags.Tag("cp33", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ ]
+
+ def test_default_python_version(self, monkeypatch):
+ monkeypatch.setattr(sys, "version_info", (3, 1))
+ result = list(tags.compatible_tags(interpreter="cp31", platforms=["plat"]))
+ assert result == [
+ tags.Tag("py31", "none", "plat"),
+ tags.Tag("py3", "none", "plat"),
+ tags.Tag("py30", "none", "plat"),
+ tags.Tag("cp31", "none", "any"),
+ tags.Tag("py31", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ tags.Tag("py30", "none", "any"),
+ ]
+
+ def test_default_python_version_needs_underscore(self, monkeypatch):
+ monkeypatch.setattr(sys, "version_info", (3, 11))
+ result = list(tags.compatible_tags(interpreter="cp311", platforms=["plat"]))
+ assert result == [
+ tags.Tag("py311", "none", "plat"),
+ tags.Tag("py3", "none", "plat"),
+ tags.Tag("py310", "none", "plat"),
+ tags.Tag("py39", "none", "plat"),
+ tags.Tag("py38", "none", "plat"),
+ tags.Tag("py37", "none", "plat"),
+ tags.Tag("py36", "none", "plat"),
+ tags.Tag("py35", "none", "plat"),
+ tags.Tag("py34", "none", "plat"),
+ tags.Tag("py33", "none", "plat"),
+ tags.Tag("py32", "none", "plat"),
+ tags.Tag("py31", "none", "plat"),
+ tags.Tag("py30", "none", "plat"),
+ tags.Tag("cp311", "none", "any"),
+ tags.Tag("py311", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ tags.Tag("py310", "none", "any"),
+ tags.Tag("py39", "none", "any"),
+ tags.Tag("py38", "none", "any"),
+ tags.Tag("py37", "none", "any"),
+ tags.Tag("py36", "none", "any"),
+ tags.Tag("py35", "none", "any"),
+ tags.Tag("py34", "none", "any"),
+ tags.Tag("py33", "none", "any"),
+ tags.Tag("py32", "none", "any"),
+ tags.Tag("py31", "none", "any"),
+ tags.Tag("py30", "none", "any"),
+ ]
+
+ def test_default_interpreter(self):
+ result = list(tags.compatible_tags((3, 1), platforms=["plat"]))
+ assert result == [
+ tags.Tag("py31", "none", "plat"),
+ tags.Tag("py3", "none", "plat"),
+ tags.Tag("py30", "none", "plat"),
+ tags.Tag("py31", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ tags.Tag("py30", "none", "any"),
+ ]
+
+ def test_default_platforms(self, monkeypatch):
+ monkeypatch.setattr(tags, "platform_tags", lambda: iter(["plat", "plat2"]))
+ result = list(tags.compatible_tags((3, 1), "cp31"))
+ assert result == [
+ tags.Tag("py31", "none", "plat"),
+ tags.Tag("py31", "none", "plat2"),
+ tags.Tag("py3", "none", "plat"),
+ tags.Tag("py3", "none", "plat2"),
+ tags.Tag("py30", "none", "plat"),
+ tags.Tag("py30", "none", "plat2"),
+ tags.Tag("cp31", "none", "any"),
+ tags.Tag("py31", "none", "any"),
+ tags.Tag("py3", "none", "any"),
+ tags.Tag("py30", "none", "any"),
+ ]
+
+
+class TestSysTags:
+ def teardown_method(self):
+ # Clear the version cache
+ tags._glibc_version = []
+
+ @pytest.mark.parametrize(
+ "name,expected",
+ [("CPython", "cp"), ("PyPy", "pp"), ("Jython", "jy"), ("IronPython", "ip")],
+ )
+ def test_interpreter_name(self, name, expected, mock_interpreter_name):
+ mock_interpreter_name(name)
+ assert tags.interpreter_name() == expected
+
+ def test_iterator(self):
+ assert isinstance(tags.sys_tags(), collections.abc.Iterator)
+
+ def test_mac_cpython(self, mock_interpreter_name, monkeypatch):
+ if mock_interpreter_name("CPython"):
+ monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"])
+ if platform.system() != "Darwin":
+ monkeypatch.setattr(platform, "system", lambda: "Darwin")
+ monkeypatch.setattr(tags, "mac_platforms", lambda: ["macosx_10_5_x86_64"])
+ abis = tags._cpython_abis(sys.version_info[:2])
+ platforms = list(tags.mac_platforms())
+ result = list(tags.sys_tags())
+ assert len(abis) == 1
+ assert result[0] == tags.Tag(
+ "cp" + tags._version_nodot(sys.version_info[:2]), abis[0], platforms[0]
+ )
+ assert result[-1] == tags.Tag(
+ "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any"
+ )
+
+ def test_windows_cpython(self, mock_interpreter_name, monkeypatch):
+ if mock_interpreter_name("CPython"):
+ monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"])
+ if platform.system() != "Windows":
+ monkeypatch.setattr(platform, "system", lambda: "Windows")
+ monkeypatch.setattr(tags, "_generic_platforms", lambda: ["win_amd64"])
+ abis = list(tags._cpython_abis(sys.version_info[:2]))
+ platforms = list(tags._generic_platforms())
+ result = list(tags.sys_tags())
+ interpreter = "cp" + tags._version_nodot(sys.version_info[:2])
+ assert len(abis) == 1
+ expected = tags.Tag(interpreter, abis[0], platforms[0])
+ assert result[0] == expected
+ expected = tags.Tag(
+ "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any"
+ )
+ assert result[-1] == expected
+
+ def test_linux_cpython(self, mock_interpreter_name, monkeypatch):
+ if mock_interpreter_name("CPython"):
+ monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"])
+ if platform.system() != "Linux":
+ monkeypatch.setattr(platform, "system", lambda: "Linux")
+ monkeypatch.setattr(tags, "_linux_platforms", lambda: ["linux_x86_64"])
+ abis = list(tags._cpython_abis(sys.version_info[:2]))
+ platforms = list(tags._linux_platforms())
+ result = list(tags.sys_tags())
+ expected_interpreter = "cp" + tags._version_nodot(sys.version_info[:2])
+ assert len(abis) == 1
+ assert result[0] == tags.Tag(expected_interpreter, abis[0], platforms[0])
+ expected = tags.Tag(
+ "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any"
+ )
+ assert result[-1] == expected
+
+ def test_generic(self, monkeypatch):
+ monkeypatch.setattr(platform, "system", lambda: "Generic")
+ monkeypatch.setattr(tags, "interpreter_name", lambda: "generic")
+
+ result = list(tags.sys_tags())
+ expected = tags.Tag(
+ "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any"
+ )
+ assert result[-1] == expected
+
+ def test_linux_platforms_manylinux2014_armv6l(self, monkeypatch, manylinux_module):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_armv6l")
+ monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False)
+ platforms = list(tags._linux_platforms(is_32bit=True))
+ expected = ["linux_armv6l"]
+ assert platforms == expected
+
+ def test_skip_manylinux_2014(self, monkeypatch, manylinux_module):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_ppc64")
+ monkeypatch.setattr(tags._manylinux, "_get_glibc_version", lambda: (2, 20))
+ monkeypatch.setattr(
+ manylinux_module, "manylinux2014_compatible", False, raising=False
+ )
+ expected = [
+ "manylinux_2_20_ppc64",
+ "manylinux_2_19_ppc64",
+ "manylinux_2_18_ppc64",
+ # "manylinux2014_ppc64", # this one is skipped
+ # "manylinux_2_17_ppc64", # this one is also skipped
+ "linux_ppc64",
+ ]
+ platforms = list(tags._linux_platforms())
+ assert platforms == expected
+
+ @pytest.mark.parametrize(
+ "machine, abi, alt_machine",
+ [("x86_64", "x32", "i686"), ("armv7l", "armel", "armv7l")],
+ )
+ def test_linux_platforms_not_manylinux_abi(
+ self, monkeypatch, manylinux_module, machine, abi, alt_machine
+ ):
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: f"linux_{machine}")
+ monkeypatch.setattr(
+ sys,
+ "executable",
+ os.path.join(
+ os.path.dirname(__file__),
+ "manylinux",
+ f"hello-world-{machine}-{abi}",
+ ),
+ )
+ platforms = list(tags._linux_platforms(is_32bit=True))
+ expected = [f"linux_{alt_machine}"]
+ assert platforms == expected
+
+ @pytest.mark.parametrize(
+ "machine, major, minor, tf", [("x86_64", 2, 20, False), ("s390x", 2, 22, True)]
+ )
+ def test_linux_use_manylinux_compatible(
+ self, monkeypatch, manylinux_module, machine, major, minor, tf
+ ):
+ def manylinux_compatible(tag_major, tag_minor, tag_arch):
+ if tag_major == 2 and tag_minor == 22:
+ return tag_arch == "s390x"
+ return False
+
+ monkeypatch.setattr(
+ tags._manylinux,
+ "_get_glibc_version",
+ lambda: (major, minor),
+ )
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: f"linux_{machine}")
+ monkeypatch.setattr(
+ manylinux_module,
+ "manylinux_compatible",
+ manylinux_compatible,
+ raising=False,
+ )
+ platforms = list(tags._linux_platforms(is_32bit=False))
+ if tf:
+ expected = [f"manylinux_2_22_{machine}"]
+ else:
+ expected = []
+ expected.append(f"linux_{machine}")
+ assert platforms == expected
+
+ def test_linux_use_manylinux_compatible_none(self, monkeypatch, manylinux_module):
+ def manylinux_compatible(tag_major, tag_minor, tag_arch):
+ if tag_major == 2 and tag_minor < 25:
+ return False
+ return None
+
+ monkeypatch.setattr(tags._manylinux, "_get_glibc_version", lambda: (2, 30))
+ monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_x86_64")
+ monkeypatch.setattr(
+ manylinux_module,
+ "manylinux_compatible",
+ manylinux_compatible,
+ raising=False,
+ )
+ platforms = list(tags._linux_platforms(is_32bit=False))
+ expected = [
+ "manylinux_2_30_x86_64",
+ "manylinux_2_29_x86_64",
+ "manylinux_2_28_x86_64",
+ "manylinux_2_27_x86_64",
+ "manylinux_2_26_x86_64",
+ "manylinux_2_25_x86_64",
+ "linux_x86_64",
+ ]
+ assert platforms == expected
+
+ def test_pypy_first_none_any_tag(self, monkeypatch):
+ # When building the complete list of pypy tags, make sure the first
+ # <interpreter>-none-any one is pp3-none-any
+ monkeypatch.setattr(tags, "interpreter_name", lambda: "pp")
+
+ for tag in tags.sys_tags():
+ if tag.abi == "none" and tag.platform == "any":
+ break
+
+ assert tag == tags.Tag("pp3", "none", "any")
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/test_utils.py b/testing/web-platform/tests/tools/third_party/packaging/tests/test_utils.py
new file mode 100644
index 0000000000..be52d67048
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/test_utils.py
@@ -0,0 +1,124 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import pytest
+
+from packaging.tags import Tag
+from packaging.utils import (
+ InvalidSdistFilename,
+ InvalidWheelFilename,
+ canonicalize_name,
+ canonicalize_version,
+ parse_sdist_filename,
+ parse_wheel_filename,
+)
+from packaging.version import Version
+
+
+@pytest.mark.parametrize(
+ ("name", "expected"),
+ [
+ ("foo", "foo"),
+ ("Foo", "foo"),
+ ("fOo", "foo"),
+ ("foo.bar", "foo-bar"),
+ ("Foo.Bar", "foo-bar"),
+ ("Foo.....Bar", "foo-bar"),
+ ("foo_bar", "foo-bar"),
+ ("foo___bar", "foo-bar"),
+ ("foo-bar", "foo-bar"),
+ ("foo----bar", "foo-bar"),
+ ],
+)
+def test_canonicalize_name(name, expected):
+ assert canonicalize_name(name) == expected
+
+
+@pytest.mark.parametrize(
+ ("version", "expected"),
+ [
+ (Version("1.4.0"), "1.4"),
+ ("1.4.0", "1.4"),
+ ("1.40.0", "1.40"),
+ ("1.4.0.0.00.000.0000", "1.4"),
+ ("1.0", "1"),
+ ("1.0+abc", "1+abc"),
+ ("1.0.dev0", "1.dev0"),
+ ("1.0.post0", "1.post0"),
+ ("1.0a0", "1a0"),
+ ("1.0rc0", "1rc0"),
+ ("100!0.0", "100!0"),
+ ("1.0.1-test7", "1.0.1-test7"), # LegacyVersion is unchanged
+ ],
+)
+def test_canonicalize_version(version, expected):
+ assert canonicalize_version(version) == expected
+
+
+@pytest.mark.parametrize(
+ ("filename", "name", "version", "build", "tags"),
+ [
+ (
+ "foo-1.0-py3-none-any.whl",
+ "foo",
+ Version("1.0"),
+ (),
+ {Tag("py3", "none", "any")},
+ ),
+ (
+ "some_PACKAGE-1.0-py3-none-any.whl",
+ "some-package",
+ Version("1.0"),
+ (),
+ {Tag("py3", "none", "any")},
+ ),
+ (
+ "foo-1.0-1000-py3-none-any.whl",
+ "foo",
+ Version("1.0"),
+ (1000, ""),
+ {Tag("py3", "none", "any")},
+ ),
+ (
+ "foo-1.0-1000abc-py3-none-any.whl",
+ "foo",
+ Version("1.0"),
+ (1000, "abc"),
+ {Tag("py3", "none", "any")},
+ ),
+ ],
+)
+def test_parse_wheel_filename(filename, name, version, build, tags):
+ assert parse_wheel_filename(filename) == (name, version, build, tags)
+
+
+@pytest.mark.parametrize(
+ ("filename"),
+ [
+ ("foo-1.0.whl"), # Missing tags
+ ("foo-1.0-py3-none-any.wheel"), # Incorrect file extension (`.wheel`)
+ ("foo__bar-1.0-py3-none-any.whl"), # Invalid name (`__`)
+ ("foo#bar-1.0-py3-none-any.whl"), # Invalid name (`#`)
+ # Build number doesn't start with a digit (`abc`)
+ ("foo-1.0-abc-py3-none-any.whl"),
+ ("foo-1.0-200-py3-none-any-junk.whl"), # Too many dashes (`-junk`)
+ ],
+)
+def test_parse_wheel_invalid_filename(filename):
+ with pytest.raises(InvalidWheelFilename):
+ parse_wheel_filename(filename)
+
+
+@pytest.mark.parametrize(
+ ("filename", "name", "version"),
+ [("foo-1.0.tar.gz", "foo", Version("1.0")), ("foo-1.0.zip", "foo", Version("1.0"))],
+)
+def test_parse_sdist_filename(filename, name, version):
+ assert parse_sdist_filename(filename) == (name, version)
+
+
+@pytest.mark.parametrize(("filename"), [("foo-1.0.xz"), ("foo1.0.tar.gz")])
+def test_parse_sdist_invalid_filename(filename):
+ with pytest.raises(InvalidSdistFilename):
+ parse_sdist_filename(filename)
diff --git a/testing/web-platform/tests/tools/third_party/packaging/tests/test_version.py b/testing/web-platform/tests/tools/third_party/packaging/tests/test_version.py
new file mode 100644
index 0000000000..5f2251e11e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/packaging/tests/test_version.py
@@ -0,0 +1,904 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+import itertools
+import operator
+import warnings
+
+import pretend
+import pytest
+
+from packaging.version import InvalidVersion, LegacyVersion, Version, parse
+
+
+@pytest.mark.parametrize(
+ ("version", "klass"), [("1.0", Version), ("1-1-1", LegacyVersion)]
+)
+def test_parse(version, klass):
+ assert isinstance(parse(version), klass)
+
+
+# This list must be in the correct sorting order
+VERSIONS = [
+ # Implicit epoch of 0
+ "1.0.dev456",
+ "1.0a1",
+ "1.0a2.dev456",
+ "1.0a12.dev456",
+ "1.0a12",
+ "1.0b1.dev456",
+ "1.0b2",
+ "1.0b2.post345.dev456",
+ "1.0b2.post345",
+ "1.0b2-346",
+ "1.0c1.dev456",
+ "1.0c1",
+ "1.0rc2",
+ "1.0c3",
+ "1.0",
+ "1.0.post456.dev34",
+ "1.0.post456",
+ "1.1.dev1",
+ "1.2+123abc",
+ "1.2+123abc456",
+ "1.2+abc",
+ "1.2+abc123",
+ "1.2+abc123def",
+ "1.2+1234.abc",
+ "1.2+123456",
+ "1.2.r32+123456",
+ "1.2.rev33+123456",
+ # Explicit epoch of 1
+ "1!1.0.dev456",
+ "1!1.0a1",
+ "1!1.0a2.dev456",
+ "1!1.0a12.dev456",
+ "1!1.0a12",
+ "1!1.0b1.dev456",
+ "1!1.0b2",
+ "1!1.0b2.post345.dev456",
+ "1!1.0b2.post345",
+ "1!1.0b2-346",
+ "1!1.0c1.dev456",
+ "1!1.0c1",
+ "1!1.0rc2",
+ "1!1.0c3",
+ "1!1.0",
+ "1!1.0.post456.dev34",
+ "1!1.0.post456",
+ "1!1.1.dev1",
+ "1!1.2+123abc",
+ "1!1.2+123abc456",
+ "1!1.2+abc",
+ "1!1.2+abc123",
+ "1!1.2+abc123def",
+ "1!1.2+1234.abc",
+ "1!1.2+123456",
+ "1!1.2.r32+123456",
+ "1!1.2.rev33+123456",
+]
+
+
+class TestVersion:
+ @pytest.mark.parametrize("version", VERSIONS)
+ def test_valid_versions(self, version):
+ Version(version)
+
+ @pytest.mark.parametrize(
+ "version",
+ [
+ # Non sensical versions should be invalid
+ "french toast",
+ # Versions with invalid local versions
+ "1.0+a+",
+ "1.0++",
+ "1.0+_foobar",
+ "1.0+foo&asd",
+ "1.0+1+1",
+ ],
+ )
+ def test_invalid_versions(self, version):
+ with pytest.raises(InvalidVersion):
+ Version(version)
+
+ @pytest.mark.parametrize(
+ ("version", "normalized"),
+ [
+ # Various development release incarnations
+ ("1.0dev", "1.0.dev0"),
+ ("1.0.dev", "1.0.dev0"),
+ ("1.0dev1", "1.0.dev1"),
+ ("1.0dev", "1.0.dev0"),
+ ("1.0-dev", "1.0.dev0"),
+ ("1.0-dev1", "1.0.dev1"),
+ ("1.0DEV", "1.0.dev0"),
+ ("1.0.DEV", "1.0.dev0"),
+ ("1.0DEV1", "1.0.dev1"),
+ ("1.0DEV", "1.0.dev0"),
+ ("1.0.DEV1", "1.0.dev1"),
+ ("1.0-DEV", "1.0.dev0"),
+ ("1.0-DEV1", "1.0.dev1"),
+ # Various alpha incarnations
+ ("1.0a", "1.0a0"),
+ ("1.0.a", "1.0a0"),
+ ("1.0.a1", "1.0a1"),
+ ("1.0-a", "1.0a0"),
+ ("1.0-a1", "1.0a1"),
+ ("1.0alpha", "1.0a0"),
+ ("1.0.alpha", "1.0a0"),
+ ("1.0.alpha1", "1.0a1"),
+ ("1.0-alpha", "1.0a0"),
+ ("1.0-alpha1", "1.0a1"),
+ ("1.0A", "1.0a0"),
+ ("1.0.A", "1.0a0"),
+ ("1.0.A1", "1.0a1"),
+ ("1.0-A", "1.0a0"),
+ ("1.0-A1", "1.0a1"),
+ ("1.0ALPHA", "1.0a0"),
+ ("1.0.ALPHA", "1.0a0"),
+ ("1.0.ALPHA1", "1.0a1"),
+ ("1.0-ALPHA", "1.0a0"),
+ ("1.0-ALPHA1", "1.0a1"),
+ # Various beta incarnations
+ ("1.0b", "1.0b0"),
+ ("1.0.b", "1.0b0"),
+ ("1.0.b1", "1.0b1"),
+ ("1.0-b", "1.0b0"),
+ ("1.0-b1", "1.0b1"),
+ ("1.0beta", "1.0b0"),
+ ("1.0.beta", "1.0b0"),
+ ("1.0.beta1", "1.0b1"),
+ ("1.0-beta", "1.0b0"),
+ ("1.0-beta1", "1.0b1"),
+ ("1.0B", "1.0b0"),
+ ("1.0.B", "1.0b0"),
+ ("1.0.B1", "1.0b1"),
+ ("1.0-B", "1.0b0"),
+ ("1.0-B1", "1.0b1"),
+ ("1.0BETA", "1.0b0"),
+ ("1.0.BETA", "1.0b0"),
+ ("1.0.BETA1", "1.0b1"),
+ ("1.0-BETA", "1.0b0"),
+ ("1.0-BETA1", "1.0b1"),
+ # Various release candidate incarnations
+ ("1.0c", "1.0rc0"),
+ ("1.0.c", "1.0rc0"),
+ ("1.0.c1", "1.0rc1"),
+ ("1.0-c", "1.0rc0"),
+ ("1.0-c1", "1.0rc1"),
+ ("1.0rc", "1.0rc0"),
+ ("1.0.rc", "1.0rc0"),
+ ("1.0.rc1", "1.0rc1"),
+ ("1.0-rc", "1.0rc0"),
+ ("1.0-rc1", "1.0rc1"),
+ ("1.0C", "1.0rc0"),
+ ("1.0.C", "1.0rc0"),
+ ("1.0.C1", "1.0rc1"),
+ ("1.0-C", "1.0rc0"),
+ ("1.0-C1", "1.0rc1"),
+ ("1.0RC", "1.0rc0"),
+ ("1.0.RC", "1.0rc0"),
+ ("1.0.RC1", "1.0rc1"),
+ ("1.0-RC", "1.0rc0"),
+ ("1.0-RC1", "1.0rc1"),
+ # Various post release incarnations
+ ("1.0post", "1.0.post0"),
+ ("1.0.post", "1.0.post0"),
+ ("1.0post1", "1.0.post1"),
+ ("1.0post", "1.0.post0"),
+ ("1.0-post", "1.0.post0"),
+ ("1.0-post1", "1.0.post1"),
+ ("1.0POST", "1.0.post0"),
+ ("1.0.POST", "1.0.post0"),
+ ("1.0POST1", "1.0.post1"),
+ ("1.0POST", "1.0.post0"),
+ ("1.0r", "1.0.post0"),
+ ("1.0rev", "1.0.post0"),
+ ("1.0.POST1", "1.0.post1"),
+ ("1.0.r1", "1.0.post1"),
+ ("1.0.rev1", "1.0.post1"),
+ ("1.0-POST", "1.0.post0"),
+ ("1.0-POST1", "1.0.post1"),
+ ("1.0-5", "1.0.post5"),
+ ("1.0-r5", "1.0.post5"),
+ ("1.0-rev5", "1.0.post5"),
+ # Local version case insensitivity
+ ("1.0+AbC", "1.0+abc"),
+ # Integer Normalization
+ ("1.01", "1.1"),
+ ("1.0a05", "1.0a5"),
+ ("1.0b07", "1.0b7"),
+ ("1.0c056", "1.0rc56"),
+ ("1.0rc09", "1.0rc9"),
+ ("1.0.post000", "1.0.post0"),
+ ("1.1.dev09000", "1.1.dev9000"),
+ ("00!1.2", "1.2"),
+ ("0100!0.0", "100!0.0"),
+ # Various other normalizations
+ ("v1.0", "1.0"),
+ (" v1.0\t\n", "1.0"),
+ ],
+ )
+ def test_normalized_versions(self, version, normalized):
+ assert str(Version(version)) == normalized
+
+ @pytest.mark.parametrize(
+ ("version", "expected"),
+ [
+ ("1.0.dev456", "1.0.dev456"),
+ ("1.0a1", "1.0a1"),
+ ("1.0a2.dev456", "1.0a2.dev456"),
+ ("1.0a12.dev456", "1.0a12.dev456"),
+ ("1.0a12", "1.0a12"),
+ ("1.0b1.dev456", "1.0b1.dev456"),
+ ("1.0b2", "1.0b2"),
+ ("1.0b2.post345.dev456", "1.0b2.post345.dev456"),
+ ("1.0b2.post345", "1.0b2.post345"),
+ ("1.0rc1.dev456", "1.0rc1.dev456"),
+ ("1.0rc1", "1.0rc1"),
+ ("1.0", "1.0"),
+ ("1.0.post456.dev34", "1.0.post456.dev34"),
+ ("1.0.post456", "1.0.post456"),
+ ("1.0.1", "1.0.1"),
+ ("0!1.0.2", "1.0.2"),
+ ("1.0.3+7", "1.0.3+7"),
+ ("0!1.0.4+8.0", "1.0.4+8.0"),
+ ("1.0.5+9.5", "1.0.5+9.5"),
+ ("1.2+1234.abc", "1.2+1234.abc"),
+ ("1.2+123456", "1.2+123456"),
+ ("1.2+123abc", "1.2+123abc"),
+ ("1.2+123abc456", "1.2+123abc456"),
+ ("1.2+abc", "1.2+abc"),
+ ("1.2+abc123", "1.2+abc123"),
+ ("1.2+abc123def", "1.2+abc123def"),
+ ("1.1.dev1", "1.1.dev1"),
+ ("7!1.0.dev456", "7!1.0.dev456"),
+ ("7!1.0a1", "7!1.0a1"),
+ ("7!1.0a2.dev456", "7!1.0a2.dev456"),
+ ("7!1.0a12.dev456", "7!1.0a12.dev456"),
+ ("7!1.0a12", "7!1.0a12"),
+ ("7!1.0b1.dev456", "7!1.0b1.dev456"),
+ ("7!1.0b2", "7!1.0b2"),
+ ("7!1.0b2.post345.dev456", "7!1.0b2.post345.dev456"),
+ ("7!1.0b2.post345", "7!1.0b2.post345"),
+ ("7!1.0rc1.dev456", "7!1.0rc1.dev456"),
+ ("7!1.0rc1", "7!1.0rc1"),
+ ("7!1.0", "7!1.0"),
+ ("7!1.0.post456.dev34", "7!1.0.post456.dev34"),
+ ("7!1.0.post456", "7!1.0.post456"),
+ ("7!1.0.1", "7!1.0.1"),
+ ("7!1.0.2", "7!1.0.2"),
+ ("7!1.0.3+7", "7!1.0.3+7"),
+ ("7!1.0.4+8.0", "7!1.0.4+8.0"),
+ ("7!1.0.5+9.5", "7!1.0.5+9.5"),
+ ("7!1.1.dev1", "7!1.1.dev1"),
+ ],
+ )
+ def test_version_str_repr(self, version, expected):
+ assert str(Version(version)) == expected
+ assert repr(Version(version)) == f"<Version({expected!r})>"
+
+ def test_version_rc_and_c_equals(self):
+ assert Version("1.0rc1") == Version("1.0c1")
+
+ @pytest.mark.parametrize("version", VERSIONS)
+ def test_version_hash(self, version):
+ assert hash(Version(version)) == hash(Version(version))
+
+ @pytest.mark.parametrize(
+ ("version", "public"),
+ [
+ ("1.0", "1.0"),
+ ("1.0.dev0", "1.0.dev0"),
+ ("1.0.dev6", "1.0.dev6"),
+ ("1.0a1", "1.0a1"),
+ ("1.0a1.post5", "1.0a1.post5"),
+ ("1.0a1.post5.dev6", "1.0a1.post5.dev6"),
+ ("1.0rc4", "1.0rc4"),
+ ("1.0.post5", "1.0.post5"),
+ ("1!1.0", "1!1.0"),
+ ("1!1.0.dev6", "1!1.0.dev6"),
+ ("1!1.0a1", "1!1.0a1"),
+ ("1!1.0a1.post5", "1!1.0a1.post5"),
+ ("1!1.0a1.post5.dev6", "1!1.0a1.post5.dev6"),
+ ("1!1.0rc4", "1!1.0rc4"),
+ ("1!1.0.post5", "1!1.0.post5"),
+ ("1.0+deadbeef", "1.0"),
+ ("1.0.dev6+deadbeef", "1.0.dev6"),
+ ("1.0a1+deadbeef", "1.0a1"),
+ ("1.0a1.post5+deadbeef", "1.0a1.post5"),
+ ("1.0a1.post5.dev6+deadbeef", "1.0a1.post5.dev6"),
+ ("1.0rc4+deadbeef", "1.0rc4"),
+ ("1.0.post5+deadbeef", "1.0.post5"),
+ ("1!1.0+deadbeef", "1!1.0"),
+ ("1!1.0.dev6+deadbeef", "1!1.0.dev6"),
+ ("1!1.0a1+deadbeef", "1!1.0a1"),
+ ("1!1.0a1.post5+deadbeef", "1!1.0a1.post5"),
+ ("1!1.0a1.post5.dev6+deadbeef", "1!1.0a1.post5.dev6"),
+ ("1!1.0rc4+deadbeef", "1!1.0rc4"),
+ ("1!1.0.post5+deadbeef", "1!1.0.post5"),
+ ],
+ )
+ def test_version_public(self, version, public):
+ assert Version(version).public == public
+
+ @pytest.mark.parametrize(
+ ("version", "base_version"),
+ [
+ ("1.0", "1.0"),
+ ("1.0.dev0", "1.0"),
+ ("1.0.dev6", "1.0"),
+ ("1.0a1", "1.0"),
+ ("1.0a1.post5", "1.0"),
+ ("1.0a1.post5.dev6", "1.0"),
+ ("1.0rc4", "1.0"),
+ ("1.0.post5", "1.0"),
+ ("1!1.0", "1!1.0"),
+ ("1!1.0.dev6", "1!1.0"),
+ ("1!1.0a1", "1!1.0"),
+ ("1!1.0a1.post5", "1!1.0"),
+ ("1!1.0a1.post5.dev6", "1!1.0"),
+ ("1!1.0rc4", "1!1.0"),
+ ("1!1.0.post5", "1!1.0"),
+ ("1.0+deadbeef", "1.0"),
+ ("1.0.dev6+deadbeef", "1.0"),
+ ("1.0a1+deadbeef", "1.0"),
+ ("1.0a1.post5+deadbeef", "1.0"),
+ ("1.0a1.post5.dev6+deadbeef", "1.0"),
+ ("1.0rc4+deadbeef", "1.0"),
+ ("1.0.post5+deadbeef", "1.0"),
+ ("1!1.0+deadbeef", "1!1.0"),
+ ("1!1.0.dev6+deadbeef", "1!1.0"),
+ ("1!1.0a1+deadbeef", "1!1.0"),
+ ("1!1.0a1.post5+deadbeef", "1!1.0"),
+ ("1!1.0a1.post5.dev6+deadbeef", "1!1.0"),
+ ("1!1.0rc4+deadbeef", "1!1.0"),
+ ("1!1.0.post5+deadbeef", "1!1.0"),
+ ],
+ )
+ def test_version_base_version(self, version, base_version):
+ assert Version(version).base_version == base_version
+
+ @pytest.mark.parametrize(
+ ("version", "epoch"),
+ [
+ ("1.0", 0),
+ ("1.0.dev0", 0),
+ ("1.0.dev6", 0),
+ ("1.0a1", 0),
+ ("1.0a1.post5", 0),
+ ("1.0a1.post5.dev6", 0),
+ ("1.0rc4", 0),
+ ("1.0.post5", 0),
+ ("1!1.0", 1),
+ ("1!1.0.dev6", 1),
+ ("1!1.0a1", 1),
+ ("1!1.0a1.post5", 1),
+ ("1!1.0a1.post5.dev6", 1),
+ ("1!1.0rc4", 1),
+ ("1!1.0.post5", 1),
+ ("1.0+deadbeef", 0),
+ ("1.0.dev6+deadbeef", 0),
+ ("1.0a1+deadbeef", 0),
+ ("1.0a1.post5+deadbeef", 0),
+ ("1.0a1.post5.dev6+deadbeef", 0),
+ ("1.0rc4+deadbeef", 0),
+ ("1.0.post5+deadbeef", 0),
+ ("1!1.0+deadbeef", 1),
+ ("1!1.0.dev6+deadbeef", 1),
+ ("1!1.0a1+deadbeef", 1),
+ ("1!1.0a1.post5+deadbeef", 1),
+ ("1!1.0a1.post5.dev6+deadbeef", 1),
+ ("1!1.0rc4+deadbeef", 1),
+ ("1!1.0.post5+deadbeef", 1),
+ ],
+ )
+ def test_version_epoch(self, version, epoch):
+ assert Version(version).epoch == epoch
+
+ @pytest.mark.parametrize(
+ ("version", "release"),
+ [
+ ("1.0", (1, 0)),
+ ("1.0.dev0", (1, 0)),
+ ("1.0.dev6", (1, 0)),
+ ("1.0a1", (1, 0)),
+ ("1.0a1.post5", (1, 0)),
+ ("1.0a1.post5.dev6", (1, 0)),
+ ("1.0rc4", (1, 0)),
+ ("1.0.post5", (1, 0)),
+ ("1!1.0", (1, 0)),
+ ("1!1.0.dev6", (1, 0)),
+ ("1!1.0a1", (1, 0)),
+ ("1!1.0a1.post5", (1, 0)),
+ ("1!1.0a1.post5.dev6", (1, 0)),
+ ("1!1.0rc4", (1, 0)),
+ ("1!1.0.post5", (1, 0)),
+ ("1.0+deadbeef", (1, 0)),
+ ("1.0.dev6+deadbeef", (1, 0)),
+ ("1.0a1+deadbeef", (1, 0)),
+ ("1.0a1.post5+deadbeef", (1, 0)),
+ ("1.0a1.post5.dev6+deadbeef", (1, 0)),
+ ("1.0rc4+deadbeef", (1, 0)),
+ ("1.0.post5+deadbeef", (1, 0)),
+ ("1!1.0+deadbeef", (1, 0)),
+ ("1!1.0.dev6+deadbeef", (1, 0)),
+ ("1!1.0a1+deadbeef", (1, 0)),
+ ("1!1.0a1.post5+deadbeef", (1, 0)),
+ ("1!1.0a1.post5.dev6+deadbeef", (1, 0)),
+ ("1!1.0rc4+deadbeef", (1, 0)),
+ ("1!1.0.post5+deadbeef", (1, 0)),
+ ],
+ )
+ def test_version_release(self, version, release):
+ assert Version(version).release == release
+
+ @pytest.mark.parametrize(
+ ("version", "local"),
+ [
+ ("1.0", None),
+ ("1.0.dev0", None),
+ ("1.0.dev6", None),
+ ("1.0a1", None),
+ ("1.0a1.post5", None),
+ ("1.0a1.post5.dev6", None),
+ ("1.0rc4", None),
+ ("1.0.post5", None),
+ ("1!1.0", None),
+ ("1!1.0.dev6", None),
+ ("1!1.0a1", None),
+ ("1!1.0a1.post5", None),
+ ("1!1.0a1.post5.dev6", None),
+ ("1!1.0rc4", None),
+ ("1!1.0.post5", None),
+ ("1.0+deadbeef", "deadbeef"),
+ ("1.0.dev6+deadbeef", "deadbeef"),
+ ("1.0a1+deadbeef", "deadbeef"),
+ ("1.0a1.post5+deadbeef", "deadbeef"),
+ ("1.0a1.post5.dev6+deadbeef", "deadbeef"),
+ ("1.0rc4+deadbeef", "deadbeef"),
+ ("1.0.post5+deadbeef", "deadbeef"),
+ ("1!1.0+deadbeef", "deadbeef"),
+ ("1!1.0.dev6+deadbeef", "deadbeef"),
+ ("1!1.0a1+deadbeef", "deadbeef"),
+ ("1!1.0a1.post5+deadbeef", "deadbeef"),
+ ("1!1.0a1.post5.dev6+deadbeef", "deadbeef"),
+ ("1!1.0rc4+deadbeef", "deadbeef"),
+ ("1!1.0.post5+deadbeef", "deadbeef"),
+ ],
+ )
+ def test_version_local(self, version, local):
+ assert Version(version).local == local
+
+ @pytest.mark.parametrize(
+ ("version", "pre"),
+ [
+ ("1.0", None),
+ ("1.0.dev0", None),
+ ("1.0.dev6", None),
+ ("1.0a1", ("a", 1)),
+ ("1.0a1.post5", ("a", 1)),
+ ("1.0a1.post5.dev6", ("a", 1)),
+ ("1.0rc4", ("rc", 4)),
+ ("1.0.post5", None),
+ ("1!1.0", None),
+ ("1!1.0.dev6", None),
+ ("1!1.0a1", ("a", 1)),
+ ("1!1.0a1.post5", ("a", 1)),
+ ("1!1.0a1.post5.dev6", ("a", 1)),
+ ("1!1.0rc4", ("rc", 4)),
+ ("1!1.0.post5", None),
+ ("1.0+deadbeef", None),
+ ("1.0.dev6+deadbeef", None),
+ ("1.0a1+deadbeef", ("a", 1)),
+ ("1.0a1.post5+deadbeef", ("a", 1)),
+ ("1.0a1.post5.dev6+deadbeef", ("a", 1)),
+ ("1.0rc4+deadbeef", ("rc", 4)),
+ ("1.0.post5+deadbeef", None),
+ ("1!1.0+deadbeef", None),
+ ("1!1.0.dev6+deadbeef", None),
+ ("1!1.0a1+deadbeef", ("a", 1)),
+ ("1!1.0a1.post5+deadbeef", ("a", 1)),
+ ("1!1.0a1.post5.dev6+deadbeef", ("a", 1)),
+ ("1!1.0rc4+deadbeef", ("rc", 4)),
+ ("1!1.0.post5+deadbeef", None),
+ ],
+ )
+ def test_version_pre(self, version, pre):
+ assert Version(version).pre == pre
+
+ @pytest.mark.parametrize(
+ ("version", "expected"),
+ [
+ ("1.0.dev0", True),
+ ("1.0.dev1", True),
+ ("1.0a1.dev1", True),
+ ("1.0b1.dev1", True),
+ ("1.0c1.dev1", True),
+ ("1.0rc1.dev1", True),
+ ("1.0a1", True),
+ ("1.0b1", True),
+ ("1.0c1", True),
+ ("1.0rc1", True),
+ ("1.0a1.post1.dev1", True),
+ ("1.0b1.post1.dev1", True),
+ ("1.0c1.post1.dev1", True),
+ ("1.0rc1.post1.dev1", True),
+ ("1.0a1.post1", True),
+ ("1.0b1.post1", True),
+ ("1.0c1.post1", True),
+ ("1.0rc1.post1", True),
+ ("1.0", False),
+ ("1.0+dev", False),
+ ("1.0.post1", False),
+ ("1.0.post1+dev", False),
+ ],
+ )
+ def test_version_is_prerelease(self, version, expected):
+ assert Version(version).is_prerelease is expected
+
+ @pytest.mark.parametrize(
+ ("version", "dev"),
+ [
+ ("1.0", None),
+ ("1.0.dev0", 0),
+ ("1.0.dev6", 6),
+ ("1.0a1", None),
+ ("1.0a1.post5", None),
+ ("1.0a1.post5.dev6", 6),
+ ("1.0rc4", None),
+ ("1.0.post5", None),
+ ("1!1.0", None),
+ ("1!1.0.dev6", 6),
+ ("1!1.0a1", None),
+ ("1!1.0a1.post5", None),
+ ("1!1.0a1.post5.dev6", 6),
+ ("1!1.0rc4", None),
+ ("1!1.0.post5", None),
+ ("1.0+deadbeef", None),
+ ("1.0.dev6+deadbeef", 6),
+ ("1.0a1+deadbeef", None),
+ ("1.0a1.post5+deadbeef", None),
+ ("1.0a1.post5.dev6+deadbeef", 6),
+ ("1.0rc4+deadbeef", None),
+ ("1.0.post5+deadbeef", None),
+ ("1!1.0+deadbeef", None),
+ ("1!1.0.dev6+deadbeef", 6),
+ ("1!1.0a1+deadbeef", None),
+ ("1!1.0a1.post5+deadbeef", None),
+ ("1!1.0a1.post5.dev6+deadbeef", 6),
+ ("1!1.0rc4+deadbeef", None),
+ ("1!1.0.post5+deadbeef", None),
+ ],
+ )
+ def test_version_dev(self, version, dev):
+ assert Version(version).dev == dev
+
+ @pytest.mark.parametrize(
+ ("version", "expected"),
+ [
+ ("1.0", False),
+ ("1.0.dev0", True),
+ ("1.0.dev6", True),
+ ("1.0a1", False),
+ ("1.0a1.post5", False),
+ ("1.0a1.post5.dev6", True),
+ ("1.0rc4", False),
+ ("1.0.post5", False),
+ ("1!1.0", False),
+ ("1!1.0.dev6", True),
+ ("1!1.0a1", False),
+ ("1!1.0a1.post5", False),
+ ("1!1.0a1.post5.dev6", True),
+ ("1!1.0rc4", False),
+ ("1!1.0.post5", False),
+ ("1.0+deadbeef", False),
+ ("1.0.dev6+deadbeef", True),
+ ("1.0a1+deadbeef", False),
+ ("1.0a1.post5+deadbeef", False),
+ ("1.0a1.post5.dev6+deadbeef", True),
+ ("1.0rc4+deadbeef", False),
+ ("1.0.post5+deadbeef", False),
+ ("1!1.0+deadbeef", False),
+ ("1!1.0.dev6+deadbeef", True),
+ ("1!1.0a1+deadbeef", False),
+ ("1!1.0a1.post5+deadbeef", False),
+ ("1!1.0a1.post5.dev6+deadbeef", True),
+ ("1!1.0rc4+deadbeef", False),
+ ("1!1.0.post5+deadbeef", False),
+ ],
+ )
+ def test_version_is_devrelease(self, version, expected):
+ assert Version(version).is_devrelease is expected
+
+ @pytest.mark.parametrize(
+ ("version", "post"),
+ [
+ ("1.0", None),
+ ("1.0.dev0", None),
+ ("1.0.dev6", None),
+ ("1.0a1", None),
+ ("1.0a1.post5", 5),
+ ("1.0a1.post5.dev6", 5),
+ ("1.0rc4", None),
+ ("1.0.post5", 5),
+ ("1!1.0", None),
+ ("1!1.0.dev6", None),
+ ("1!1.0a1", None),
+ ("1!1.0a1.post5", 5),
+ ("1!1.0a1.post5.dev6", 5),
+ ("1!1.0rc4", None),
+ ("1!1.0.post5", 5),
+ ("1.0+deadbeef", None),
+ ("1.0.dev6+deadbeef", None),
+ ("1.0a1+deadbeef", None),
+ ("1.0a1.post5+deadbeef", 5),
+ ("1.0a1.post5.dev6+deadbeef", 5),
+ ("1.0rc4+deadbeef", None),
+ ("1.0.post5+deadbeef", 5),
+ ("1!1.0+deadbeef", None),
+ ("1!1.0.dev6+deadbeef", None),
+ ("1!1.0a1+deadbeef", None),
+ ("1!1.0a1.post5+deadbeef", 5),
+ ("1!1.0a1.post5.dev6+deadbeef", 5),
+ ("1!1.0rc4+deadbeef", None),
+ ("1!1.0.post5+deadbeef", 5),
+ ],
+ )
+ def test_version_post(self, version, post):
+ assert Version(version).post == post
+
+ @pytest.mark.parametrize(
+ ("version", "expected"),
+ [
+ ("1.0.dev1", False),
+ ("1.0", False),
+ ("1.0+foo", False),
+ ("1.0.post1.dev1", True),
+ ("1.0.post1", True),
+ ],
+ )
+ def test_version_is_postrelease(self, version, expected):
+ assert Version(version).is_postrelease is expected
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ # Below we'll generate every possible combination of VERSIONS that
+ # should be True for the given operator
+ itertools.chain(
+ *
+ # Verify that the less than (<) operator works correctly
+ [
+ [(x, y, operator.lt) for y in VERSIONS[i + 1 :]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the less than equal (<=) operator works correctly
+ [
+ [(x, y, operator.le) for y in VERSIONS[i:]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the equal (==) operator works correctly
+ [[(x, x, operator.eq) for x in VERSIONS]]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [
+ [(x, y, operator.ne) for j, y in enumerate(VERSIONS) if i != j]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the greater than equal (>=) operator works correctly
+ [
+ [(x, y, operator.ge) for y in VERSIONS[: i + 1]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the greater than (>) operator works correctly
+ [
+ [(x, y, operator.gt) for y in VERSIONS[:i]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ ),
+ )
+ def test_comparison_true(self, left, right, op):
+ assert op(Version(left), Version(right))
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ # Below we'll generate every possible combination of VERSIONS that
+ # should be False for the given operator
+ itertools.chain(
+ *
+ # Verify that the less than (<) operator works correctly
+ [
+ [(x, y, operator.lt) for y in VERSIONS[: i + 1]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the less than equal (<=) operator works correctly
+ [
+ [(x, y, operator.le) for y in VERSIONS[:i]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the equal (==) operator works correctly
+ [
+ [(x, y, operator.eq) for j, y in enumerate(VERSIONS) if i != j]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [[(x, x, operator.ne) for x in VERSIONS]]
+ +
+ # Verify that the greater than equal (>=) operator works correctly
+ [
+ [(x, y, operator.ge) for y in VERSIONS[i + 1 :]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ +
+ # Verify that the greater than (>) operator works correctly
+ [
+ [(x, y, operator.gt) for y in VERSIONS[i:]]
+ for i, x in enumerate(VERSIONS)
+ ]
+ ),
+ )
+ def test_comparison_false(self, left, right, op):
+ assert not op(Version(left), Version(right))
+
+ @pytest.mark.parametrize("op", ["lt", "le", "eq", "ge", "gt", "ne"])
+ def test_dunder_op_returns_notimplemented(self, op):
+ method = getattr(Version, f"__{op}__")
+ assert method(Version("1"), 1) is NotImplemented
+
+ @pytest.mark.parametrize(("op", "expected"), [("eq", False), ("ne", True)])
+ def test_compare_other(self, op, expected):
+ other = pretend.stub(**{f"__{op}__": lambda other: NotImplemented})
+
+ assert getattr(operator, op)(Version("1"), other) is expected
+
+ def test_compare_legacyversion_version(self):
+ result = sorted([Version("0"), LegacyVersion("1")])
+ assert result == [LegacyVersion("1"), Version("0")]
+
+ def test_major_version(self):
+ assert Version("2.1.0").major == 2
+
+ def test_minor_version(self):
+ assert Version("2.1.0").minor == 1
+ assert Version("2").minor == 0
+
+ def test_micro_version(self):
+ assert Version("2.1.3").micro == 3
+ assert Version("2.1").micro == 0
+ assert Version("2").micro == 0
+
+
+LEGACY_VERSIONS = ["foobar", "a cat is fine too", "lolwut", "1-0", "2.0-a1"]
+
+
+class TestLegacyVersion:
+ def test_legacy_version_is_deprecated(self):
+ with warnings.catch_warnings(record=True) as w:
+ LegacyVersion("some-legacy-version")
+ assert len(w) == 1
+ assert issubclass(w[0].category, DeprecationWarning)
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_valid_legacy_versions(self, version):
+ LegacyVersion(version)
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_str_repr(self, version):
+ assert str(LegacyVersion(version)) == version
+ assert repr(LegacyVersion(version)) == "<LegacyVersion({})>".format(
+ repr(version)
+ )
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_hash(self, version):
+ assert hash(LegacyVersion(version)) == hash(LegacyVersion(version))
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_public(self, version):
+ assert LegacyVersion(version).public == version
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_base_version(self, version):
+ assert LegacyVersion(version).base_version == version
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_epoch(self, version):
+ assert LegacyVersion(version).epoch == -1
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_release(self, version):
+ assert LegacyVersion(version).release is None
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_local(self, version):
+ assert LegacyVersion(version).local is None
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_pre(self, version):
+ assert LegacyVersion(version).pre is None
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_is_prerelease(self, version):
+ assert not LegacyVersion(version).is_prerelease
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_dev(self, version):
+ assert LegacyVersion(version).dev is None
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_is_devrelease(self, version):
+ assert not LegacyVersion(version).is_devrelease
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_post(self, version):
+ assert LegacyVersion(version).post is None
+
+ @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS)
+ def test_legacy_version_is_postrelease(self, version):
+ assert not LegacyVersion(version).is_postrelease
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ # Below we'll generate every possible combination of
+ # VERSIONS + LEGACY_VERSIONS that should be True for the given operator
+ itertools.chain(
+ *
+ # Verify that the equal (==) operator works correctly
+ [[(x, x, operator.eq) for x in VERSIONS + LEGACY_VERSIONS]]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [
+ [
+ (x, y, operator.ne)
+ for j, y in enumerate(VERSIONS + LEGACY_VERSIONS)
+ if i != j
+ ]
+ for i, x in enumerate(VERSIONS + LEGACY_VERSIONS)
+ ]
+ ),
+ )
+ def test_comparison_true(self, left, right, op):
+ assert op(LegacyVersion(left), LegacyVersion(right))
+
+ @pytest.mark.parametrize(
+ ("left", "right", "op"),
+ # Below we'll generate every possible combination of
+ # VERSIONS + LEGACY_VERSIONS that should be False for the given
+ # operator
+ itertools.chain(
+ *
+ # Verify that the equal (==) operator works correctly
+ [
+ [
+ (x, y, operator.eq)
+ for j, y in enumerate(VERSIONS + LEGACY_VERSIONS)
+ if i != j
+ ]
+ for i, x in enumerate(VERSIONS + LEGACY_VERSIONS)
+ ]
+ +
+ # Verify that the not equal (!=) operator works correctly
+ [[(x, x, operator.ne) for x in VERSIONS + LEGACY_VERSIONS]]
+ ),
+ )
+ def test_comparison_false(self, left, right, op):
+ assert not op(LegacyVersion(left), LegacyVersion(right))
+
+ @pytest.mark.parametrize("op", ["lt", "le", "eq", "ge", "gt", "ne"])
+ def test_dunder_op_returns_notimplemented(self, op):
+ method = getattr(LegacyVersion, f"__{op}__")
+ assert method(LegacyVersion("1"), 1) is NotImplemented
+
+ @pytest.mark.parametrize(("op", "expected"), [("eq", False), ("ne", True)])
+ def test_compare_other(self, op, expected):
+ other = pretend.stub(**{f"__{op}__": lambda other: NotImplemented})
+
+ assert getattr(operator, op)(LegacyVersion("1"), other) is expected
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/.gitignore b/testing/web-platform/tests/tools/third_party/pathlib2/.gitignore
new file mode 100644
index 0000000000..db4561eaa1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/.gitignore
@@ -0,0 +1,54 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.cache
+nosetests.xml
+coverage.xml
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/.travis.yml b/testing/web-platform/tests/tools/third_party/pathlib2/.travis.yml
new file mode 100644
index 0000000000..3166741220
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/.travis.yml
@@ -0,0 +1,47 @@
+language: python
+sudo: false
+matrix:
+ include:
+ - python: "3.7"
+ dist: xenial
+ env: LC_ALL="en_US.utf-8"
+ - python: "3.7"
+ dist: xenial
+ env: LC_ALL="en_US.ascii"
+ - python: "3.6"
+ dist: xenial
+ - python: "3.5"
+ dist: xenial
+ - python: "3.4"
+ dist: xenial
+ - python: "2.7"
+ dist: xenial
+ - python: "2.6"
+ dist: trusty
+ - python: "pypy"
+ dist: xenial
+ - python: "pypy3"
+ dist: xenial
+ allow_failures:
+ # pypy occasionally has some bugs
+ - python: "pypy"
+ - python: "pypy3"
+branches:
+ only:
+ - develop
+install:
+ - "python -c \"import sys; print(sys.getfilesystemencoding())\""
+ - "pip install --upgrade setuptools"
+ - "pip install --upgrade pytest"
+ - "pip install pytest-cov"
+ - "pip install -r requirements.txt"
+ - "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi"
+ - "if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then pip install check-manifest flake8; fi"
+ - "pip install ."
+script:
+ - "if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then check-manifest; fi"
+ - "if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then flake8; fi"
+ - cd tests
+ - "pytest --cov-report=xml --cov=pathlib2 ."
+after_success:
+ - bash <(curl -s https://codecov.io/bash)
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/CHANGELOG.rst b/testing/web-platform/tests/tools/third_party/pathlib2/CHANGELOG.rst
new file mode 100644
index 0000000000..867c1b38e9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/CHANGELOG.rst
@@ -0,0 +1,163 @@
+History
+-------
+
+Version 2.3.5
+^^^^^^^^^^^^^
+
+- Fall back to ascii when getfilesystemencoding returns None (see
+ issue #59).
+
+Version 2.3.4
+^^^^^^^^^^^^^
+
+- Do not raise windows error when calling resolve on a non-existing
+ path in Python 2.7, to match behaviour on Python 3.x (see issue #54).
+
+- Use the new collections.abc when possible (see issue #53).
+
+- Sync with upstream pathlib (see issues #47 and #51).
+
+Version 2.3.3
+^^^^^^^^^^^^^
+
+- Bring back old deprecated dependency syntax to ensure compatibility
+ with older systems (see issue #46).
+
+- Drop Python 3.3 support, as scandir no longer supports it.
+
+- Add Python 3.7 support.
+
+Version 2.3.2
+^^^^^^^^^^^^^
+
+- Hotfix for broken setup.py.
+
+Version 2.3.1
+^^^^^^^^^^^^^
+
+- Fix tests for systems where filesystem encoding only supports ascii
+ (reported by yurivict, fixed with help of honnibal, see issue #30).
+
+- Use modern setuptools syntax for specifying conditional scandir
+ dependency (see issue #31).
+
+- Remove legacy use of support module from old pathlib module (see
+ issue #39). This fixes the tests for Python 3.6.
+
+- Drop the "from __future__ import unicode_literals" and -Qnew tests
+ as it introduced subtle bugs in the tests, and maintaining separate
+ test modules for these legacy features seems not worth the effort.
+
+- Drop Python 3.2 support, as scandir no longer supports it.
+
+Version 2.3.0
+^^^^^^^^^^^^^
+
+- Sync with upstream pathlib from CPython 3.6.1 (7d1017d).
+
+Version 2.2.1
+^^^^^^^^^^^^^
+
+- Fix conditional scandir dependency in wheel (reported by AvdN, see
+ issue #20 and pull request #21).
+
+Version 2.2.0
+^^^^^^^^^^^^^
+
+- Sync with upstream pathlib from CPython 3.5.2 and 3.6.0: fix various
+ exceptions, empty glob pattern, scandir, __fspath__.
+
+- Support unicode strings to be used to construct paths in Python 2
+ (reported by native-api, see issue #13 and pull request #15).
+
+Version 2.1.0
+^^^^^^^^^^^^^
+
+- Sync with upstream pathlib from CPython 3.5.0: gethomedir, home,
+ expanduser.
+
+Version 2.0.1
+^^^^^^^^^^^^^
+
+- Fix TypeError exceptions in write_bytes and write_text (contributed
+ by Emanuele Gaifas, see pull request #2).
+
+Version 2.0
+^^^^^^^^^^^
+
+- Sync with upstream pathlib from CPython: read_text, write_text,
+ read_bytes, write_bytes, __enter__, __exit__, samefile.
+- Use travis and appveyor for continuous integration.
+- Fixed some bugs in test code.
+
+Version 1.0.1
+^^^^^^^^^^^^^
+
+- Pull request #4: Python 2.6 compatibility by eevee.
+
+Version 1.0
+^^^^^^^^^^^
+
+This version brings ``pathlib`` up to date with the official Python 3.4
+release, and also fixes a couple of 2.7-specific issues.
+
+- Python issue #20765: Add missing documentation for PurePath.with_name()
+ and PurePath.with_suffix().
+- Fix test_mkdir_parents when the working directory has additional bits
+ set (such as the setgid or sticky bits).
+- Python issue #20111: pathlib.Path.with_suffix() now sanity checks the
+ given suffix.
+- Python issue #19918: Fix PurePath.relative_to() under Windows.
+- Python issue #19921: When Path.mkdir() is called with parents=True, any
+ missing parent is created with the default permissions, ignoring the mode
+ argument (mimicking the POSIX "mkdir -p" command).
+- Python issue #19887: Improve the Path.resolve() algorithm to support
+ certain symlink chains.
+- Make pathlib usable under Python 2.7 with unicode pathnames (only pure
+ ASCII, though).
+- Issue #21: fix TypeError under Python 2.7 when using new division.
+- Add tox support for easier testing.
+
+Version 0.97
+^^^^^^^^^^^^
+
+This version brings ``pathlib`` up to date with the final API specified
+in :pep:`428`. The changes are too long to list here, it is recommended
+to read the `documentation <https://pathlib.readthedocs.org/>`_.
+
+.. warning::
+ The API in this version is partially incompatible with pathlib 0.8 and
+ earlier. Be sure to check your code for possible breakage!
+
+Version 0.8
+^^^^^^^^^^^
+
+- Add PurePath.name and PurePath.anchor.
+- Add Path.owner and Path.group.
+- Add Path.replace().
+- Add Path.as_uri().
+- Issue #10: when creating a file with Path.open(), don't set the executable
+ bit.
+- Issue #11: fix comparisons with non-Path objects.
+
+Version 0.7
+^^^^^^^^^^^
+
+- Add '**' (recursive) patterns to Path.glob().
+- Fix openat() support after the API refactoring in Python 3.3 beta1.
+- Add a *target_is_directory* argument to Path.symlink_to()
+
+Version 0.6
+^^^^^^^^^^^
+
+- Add Path.is_file() and Path.is_symlink()
+- Add Path.glob() and Path.rglob()
+- Add PurePath.match()
+
+Version 0.5
+^^^^^^^^^^^
+
+- Add Path.mkdir().
+- Add Python 2.7 compatibility by Michele Lacchia.
+- Make parent() raise ValueError when the level is greater than the path
+ length.
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/LICENSE.rst b/testing/web-platform/tests/tools/third_party/pathlib2/LICENSE.rst
new file mode 100644
index 0000000000..1715d3d7a2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/LICENSE.rst
@@ -0,0 +1,23 @@
+The MIT License (MIT)
+
+Copyright (c) 2014-2017 Matthias C. M. Troffaes
+Copyright (c) 2012-2014 Antoine Pitrou and 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
+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/testing/web-platform/tests/tools/third_party/pathlib2/MANIFEST.in b/testing/web-platform/tests/tools/third_party/pathlib2/MANIFEST.in
new file mode 100644
index 0000000000..2f03369dc6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/MANIFEST.in
@@ -0,0 +1,10 @@
+include *.py
+recursive-include pathlib2 *.py
+recursive-include tests *.py
+include *.rst
+include VERSION
+include requirements.txt
+exclude .travis.yml
+exclude appveyor.yml
+exclude codecov.yml
+prune appveyor
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/README.rst b/testing/web-platform/tests/tools/third_party/pathlib2/README.rst
new file mode 100644
index 0000000000..6378f284dd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/README.rst
@@ -0,0 +1,66 @@
+pathlib2
+========
+
+|appveyor| |travis| |codecov|
+
+Fork of pathlib aiming to support the full stdlib Python API.
+
+The `old pathlib <https://bitbucket.org/pitrou/pathlib>`_
+module on bitbucket is in bugfix-only mode.
+The goal of pathlib2 is to provide a backport of
+`standard pathlib <http://docs.python.org/dev/library/pathlib.html>`_
+module which tracks the standard library module,
+so all the newest features of the standard pathlib can be
+used also on older Python versions.
+
+Download
+--------
+
+Standalone releases are available on PyPI:
+http://pypi.python.org/pypi/pathlib2/
+
+Development
+-----------
+
+The main development takes place in the Python standard library: see
+the `Python developer's guide <http://docs.python.org/devguide/>`_.
+In particular, new features should be submitted to the
+`Python bug tracker <http://bugs.python.org/>`_.
+
+Issues that occur in this backport, but that do not occur not in the
+standard Python pathlib module can be submitted on
+the `pathlib2 bug tracker <https://github.com/mcmtroffaes/pathlib2/issues>`_.
+
+Documentation
+-------------
+
+Refer to the
+`standard pathlib <http://docs.python.org/dev/library/pathlib.html>`_
+documentation.
+
+Known Issues
+------------
+
+For historic reasons, pathlib2 still uses bytes to represent file paths internally.
+Unfortunately, on Windows with Python 2.7, the file system encoder (``mcbs``)
+has only poor support for non-ascii characters,
+and can silently replace non-ascii characters without warning.
+For example, ``u'тест'.encode(sys.getfilesystemencoding())`` results in ``????``
+which is obviously completely useless.
+
+Therefore, on Windows with Python 2.7, until this problem is fixed upstream,
+unfortunately you cannot rely on pathlib2 to support the full unicode range for filenames.
+See `issue #56 <https://github.com/mcmtroffaes/pathlib2/issues/56>`_ for more details.
+
+.. |travis| image:: https://travis-ci.org/mcmtroffaes/pathlib2.png?branch=develop
+ :target: https://travis-ci.org/mcmtroffaes/pathlib2
+ :alt: travis-ci
+
+.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/baddx3rpet2wyi2c?svg=true
+ :target: https://ci.appveyor.com/project/mcmtroffaes/pathlib2
+ :alt: appveyor
+
+.. |codecov| image:: https://codecov.io/gh/mcmtroffaes/pathlib2/branch/develop/graph/badge.svg
+ :target: https://codecov.io/gh/mcmtroffaes/pathlib2
+ :alt: codecov
+
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/VERSION b/testing/web-platform/tests/tools/third_party/pathlib2/VERSION
new file mode 100644
index 0000000000..cc6c9a491e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/VERSION
@@ -0,0 +1 @@
+2.3.5
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/appveyor.yml b/testing/web-platform/tests/tools/third_party/pathlib2/appveyor.yml
new file mode 100644
index 0000000000..aae7f25f34
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/appveyor.yml
@@ -0,0 +1,30 @@
+environment:
+
+ matrix:
+ - PYTHON: "C:\\Python27"
+
+ - PYTHON: "C:\\Python34"
+
+ - PYTHON: "C:\\Python35"
+
+ - PYTHON: "C:\\Python36"
+
+ - PYTHON: "C:\\Python37"
+
+init:
+ - "%PYTHON%/python --version"
+
+install:
+ - "powershell appveyor\\install.ps1"
+
+build: off
+
+test_script:
+ - cd tests
+ - "%PYTHON%/Scripts/py.test --cov-report=xml --cov=pathlib2 ."
+
+after_test:
+ - ps: |
+ $env:PATH = 'C:\msys64\usr\bin;' + $env:PATH
+ Invoke-WebRequest -Uri 'https://codecov.io/bash' -OutFile codecov.sh
+ bash codecov.sh -f "coverage.xml"
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/appveyor/install.ps1 b/testing/web-platform/tests/tools/third_party/pathlib2/appveyor/install.ps1
new file mode 100644
index 0000000000..ebfbb8db96
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/appveyor/install.ps1
@@ -0,0 +1,44 @@
+# Sample script to install Python and pip under Windows
+# Authors: Olivier Grisel and Kyle Kastner
+# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/
+
+$GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py"
+$GET_PIP_PATH = "C:\get-pip.py"
+
+
+function InstallPip ($python_home) {
+ $pip_path = $python_home + "/Scripts/pip.exe"
+ $python_path = $python_home + "/python.exe"
+ if (-not(Test-Path $pip_path)) {
+ Write-Host "Installing pip..."
+ $webclient = New-Object System.Net.WebClient
+ $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH)
+ Write-Host "Executing:" $python_path $GET_PIP_PATH
+ Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru
+ } else {
+ Write-Host "Upgrading pip..."
+ & $python_path -m pip install --upgrade pip
+ }
+ Write-Host "Upgrading setuptools..."
+ & $python_path -m pip install --upgrade setuptools
+}
+
+function InstallPackage ($python_home, $pkg) {
+ $pip_path = $python_home + "/Scripts/pip.exe"
+ & $pip_path install $pkg
+}
+
+function InstallRequirements ($python_home, $reqs) {
+ $pip_path = $python_home + "/Scripts/pip.exe"
+ & $pip_path install -r $reqs
+}
+
+function main () {
+ InstallPip $env:PYTHON
+ InstallRequirements $env:PYTHON -r requirements.txt
+ InstallPackage $env:PYTHON pytest-cov
+ InstallPackage $env:PYTHON unittest2
+ InstallPackage $env:PYTHON .
+}
+
+main
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/codecov.yml b/testing/web-platform/tests/tools/third_party/pathlib2/codecov.yml
new file mode 100644
index 0000000000..db2472009c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/codecov.yml
@@ -0,0 +1 @@
+comment: off
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/pathlib2/__init__.py b/testing/web-platform/tests/tools/third_party/pathlib2/pathlib2/__init__.py
new file mode 100644
index 0000000000..d5a47a66c6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/pathlib2/__init__.py
@@ -0,0 +1,1809 @@
+# Copyright (c) 2014-2017 Matthias C. M. Troffaes
+# Copyright (c) 2012-2014 Antoine Pitrou and contributors
+# Distributed under the terms of the MIT License.
+
+import ctypes
+import fnmatch
+import functools
+import io
+import ntpath
+import os
+import posixpath
+import re
+import six
+import sys
+
+from errno import EINVAL, ENOENT, ENOTDIR, EBADF
+from errno import EEXIST, EPERM, EACCES
+from operator import attrgetter
+from stat import (
+ S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO)
+
+try:
+ from collections.abc import Sequence
+except ImportError:
+ from collections import Sequence
+
+try:
+ from urllib import quote as urlquote_from_bytes
+except ImportError:
+ from urllib.parse import quote_from_bytes as urlquote_from_bytes
+
+
+try:
+ intern = intern
+except NameError:
+ intern = sys.intern
+
+supports_symlinks = True
+if os.name == 'nt':
+ import nt
+ if sys.getwindowsversion()[:2] >= (6, 0) and sys.version_info >= (3, 2):
+ from nt import _getfinalpathname
+ else:
+ supports_symlinks = False
+ _getfinalpathname = None
+else:
+ nt = None
+
+try:
+ from os import scandir as os_scandir
+except ImportError:
+ from scandir import scandir as os_scandir
+
+__all__ = [
+ "PurePath", "PurePosixPath", "PureWindowsPath",
+ "Path", "PosixPath", "WindowsPath",
+ ]
+
+#
+# Internals
+#
+
+# EBADF - guard agains macOS `stat` throwing EBADF
+_IGNORED_ERROS = (ENOENT, ENOTDIR, EBADF)
+
+_IGNORED_WINERRORS = (
+ 21, # ERROR_NOT_READY - drive exists but is not accessible
+)
+
+
+def _ignore_error(exception):
+ return (getattr(exception, 'errno', None) in _IGNORED_ERROS or
+ getattr(exception, 'winerror', None) in _IGNORED_WINERRORS)
+
+
+def _py2_fsencode(parts):
+ # py2 => minimal unicode support
+ assert six.PY2
+ return [part.encode('ascii') if isinstance(part, six.text_type)
+ else part for part in parts]
+
+
+def _try_except_fileexistserror(try_func, except_func, else_func=None):
+ if sys.version_info >= (3, 3):
+ try:
+ try_func()
+ except FileExistsError as exc:
+ except_func(exc)
+ else:
+ if else_func is not None:
+ else_func()
+ else:
+ try:
+ try_func()
+ except EnvironmentError as exc:
+ if exc.errno != EEXIST:
+ raise
+ else:
+ except_func(exc)
+ else:
+ if else_func is not None:
+ else_func()
+
+
+def _try_except_filenotfounderror(try_func, except_func):
+ if sys.version_info >= (3, 3):
+ try:
+ try_func()
+ except FileNotFoundError as exc:
+ except_func(exc)
+ elif os.name != 'nt':
+ try:
+ try_func()
+ except EnvironmentError as exc:
+ if exc.errno != ENOENT:
+ raise
+ else:
+ except_func(exc)
+ else:
+ try:
+ try_func()
+ except WindowsError as exc:
+ # errno contains winerror
+ # 2 = file not found
+ # 3 = path not found
+ if exc.errno not in (2, 3):
+ raise
+ else:
+ except_func(exc)
+ except EnvironmentError as exc:
+ if exc.errno != ENOENT:
+ raise
+ else:
+ except_func(exc)
+
+
+def _try_except_permissionerror_iter(try_iter, except_iter):
+ if sys.version_info >= (3, 3):
+ try:
+ for x in try_iter():
+ yield x
+ except PermissionError as exc:
+ for x in except_iter(exc):
+ yield x
+ else:
+ try:
+ for x in try_iter():
+ yield x
+ except EnvironmentError as exc:
+ if exc.errno not in (EPERM, EACCES):
+ raise
+ else:
+ for x in except_iter(exc):
+ yield x
+
+
+def _win32_get_unique_path_id(path):
+ # get file information, needed for samefile on older Python versions
+ # see http://timgolden.me.uk/python/win32_how_do_i/
+ # see_if_two_files_are_the_same_file.html
+ from ctypes import POINTER, Structure, WinError
+ from ctypes.wintypes import DWORD, HANDLE, BOOL
+
+ class FILETIME(Structure):
+ _fields_ = [("datetime_lo", DWORD),
+ ("datetime_hi", DWORD),
+ ]
+
+ class BY_HANDLE_FILE_INFORMATION(Structure):
+ _fields_ = [("attributes", DWORD),
+ ("created_at", FILETIME),
+ ("accessed_at", FILETIME),
+ ("written_at", FILETIME),
+ ("volume", DWORD),
+ ("file_hi", DWORD),
+ ("file_lo", DWORD),
+ ("n_links", DWORD),
+ ("index_hi", DWORD),
+ ("index_lo", DWORD),
+ ]
+
+ CreateFile = ctypes.windll.kernel32.CreateFileW
+ CreateFile.argtypes = [ctypes.c_wchar_p, DWORD, DWORD, ctypes.c_void_p,
+ DWORD, DWORD, HANDLE]
+ CreateFile.restype = HANDLE
+ GetFileInformationByHandle = (
+ ctypes.windll.kernel32.GetFileInformationByHandle)
+ GetFileInformationByHandle.argtypes = [
+ HANDLE, POINTER(BY_HANDLE_FILE_INFORMATION)]
+ GetFileInformationByHandle.restype = BOOL
+ CloseHandle = ctypes.windll.kernel32.CloseHandle
+ CloseHandle.argtypes = [HANDLE]
+ CloseHandle.restype = BOOL
+ GENERIC_READ = 0x80000000
+ FILE_SHARE_READ = 0x00000001
+ FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
+ OPEN_EXISTING = 3
+ if os.path.isdir(path):
+ flags = FILE_FLAG_BACKUP_SEMANTICS
+ else:
+ flags = 0
+ hfile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ,
+ None, OPEN_EXISTING, flags, None)
+ if hfile == 0xffffffff:
+ if sys.version_info >= (3, 3):
+ raise FileNotFoundError(path)
+ else:
+ exc = OSError("file not found: path")
+ exc.errno = ENOENT
+ raise exc
+ info = BY_HANDLE_FILE_INFORMATION()
+ success = GetFileInformationByHandle(hfile, info)
+ CloseHandle(hfile)
+ if success == 0:
+ raise WinError()
+ return info.volume, info.index_hi, info.index_lo
+
+
+def _is_wildcard_pattern(pat):
+ # Whether this pattern needs actual matching using fnmatch, or can
+ # be looked up directly as a file.
+ return "*" in pat or "?" in pat or "[" in pat
+
+
+class _Flavour(object):
+
+ """A flavour implements a particular (platform-specific) set of path
+ semantics."""
+
+ def __init__(self):
+ self.join = self.sep.join
+
+ def parse_parts(self, parts):
+ if six.PY2:
+ parts = _py2_fsencode(parts)
+ parsed = []
+ sep = self.sep
+ altsep = self.altsep
+ drv = root = ''
+ it = reversed(parts)
+ for part in it:
+ if not part:
+ continue
+ if altsep:
+ part = part.replace(altsep, sep)
+ drv, root, rel = self.splitroot(part)
+ if sep in rel:
+ for x in reversed(rel.split(sep)):
+ if x and x != '.':
+ parsed.append(intern(x))
+ else:
+ if rel and rel != '.':
+ parsed.append(intern(rel))
+ if drv or root:
+ if not drv:
+ # If no drive is present, try to find one in the previous
+ # parts. This makes the result of parsing e.g.
+ # ("C:", "/", "a") reasonably intuitive.
+ for part in it:
+ if not part:
+ continue
+ if altsep:
+ part = part.replace(altsep, sep)
+ drv = self.splitroot(part)[0]
+ if drv:
+ break
+ break
+ if drv or root:
+ parsed.append(drv + root)
+ parsed.reverse()
+ return drv, root, parsed
+
+ def join_parsed_parts(self, drv, root, parts, drv2, root2, parts2):
+ """
+ Join the two paths represented by the respective
+ (drive, root, parts) tuples. Return a new (drive, root, parts) tuple.
+ """
+ if root2:
+ if not drv2 and drv:
+ return drv, root2, [drv + root2] + parts2[1:]
+ elif drv2:
+ if drv2 == drv or self.casefold(drv2) == self.casefold(drv):
+ # Same drive => second path is relative to the first
+ return drv, root, parts + parts2[1:]
+ else:
+ # Second path is non-anchored (common case)
+ return drv, root, parts + parts2
+ return drv2, root2, parts2
+
+
+class _WindowsFlavour(_Flavour):
+ # Reference for Windows paths can be found at
+ # http://msdn.microsoft.com/en-us/library/aa365247%28v=vs.85%29.aspx
+
+ sep = '\\'
+ altsep = '/'
+ has_drv = True
+ pathmod = ntpath
+
+ is_supported = (os.name == 'nt')
+
+ drive_letters = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
+ ext_namespace_prefix = '\\\\?\\'
+
+ reserved_names = (
+ set(['CON', 'PRN', 'AUX', 'NUL']) |
+ set(['COM%d' % i for i in range(1, 10)]) |
+ set(['LPT%d' % i for i in range(1, 10)])
+ )
+
+ # Interesting findings about extended paths:
+ # - '\\?\c:\a', '//?/c:\a' and '//?/c:/a' are all supported
+ # but '\\?\c:/a' is not
+ # - extended paths are always absolute; "relative" extended paths will
+ # fail.
+
+ def splitroot(self, part, sep=sep):
+ first = part[0:1]
+ second = part[1:2]
+ if (second == sep and first == sep):
+ # XXX extended paths should also disable the collapsing of "."
+ # components (according to MSDN docs).
+ prefix, part = self._split_extended_path(part)
+ first = part[0:1]
+ second = part[1:2]
+ else:
+ prefix = ''
+ third = part[2:3]
+ if (second == sep and first == sep and third != sep):
+ # is a UNC path:
+ # vvvvvvvvvvvvvvvvvvvvv root
+ # \\machine\mountpoint\directory\etc\...
+ # directory ^^^^^^^^^^^^^^
+ index = part.find(sep, 2)
+ if index != -1:
+ index2 = part.find(sep, index + 1)
+ # a UNC path can't have two slashes in a row
+ # (after the initial two)
+ if index2 != index + 1:
+ if index2 == -1:
+ index2 = len(part)
+ if prefix:
+ return prefix + part[1:index2], sep, part[index2 + 1:]
+ else:
+ return part[:index2], sep, part[index2 + 1:]
+ drv = root = ''
+ if second == ':' and first in self.drive_letters:
+ drv = part[:2]
+ part = part[2:]
+ first = third
+ if first == sep:
+ root = first
+ part = part.lstrip(sep)
+ return prefix + drv, root, part
+
+ def casefold(self, s):
+ return s.lower()
+
+ def casefold_parts(self, parts):
+ return [p.lower() for p in parts]
+
+ def resolve(self, path, strict=False):
+ s = str(path)
+ if not s:
+ return os.getcwd()
+ previous_s = None
+ if _getfinalpathname is not None:
+ if strict:
+ return self._ext_to_normal(_getfinalpathname(s))
+ else:
+ # End of the path after the first one not found
+ tail_parts = []
+
+ def _try_func():
+ result[0] = self._ext_to_normal(_getfinalpathname(s))
+ # if there was no exception, set flag to 0
+ result[1] = 0
+
+ def _exc_func(exc):
+ pass
+
+ while True:
+ result = [None, 1]
+ _try_except_filenotfounderror(_try_func, _exc_func)
+ if result[1] == 1: # file not found exception raised
+ previous_s = s
+ s, tail = os.path.split(s)
+ tail_parts.append(tail)
+ if previous_s == s:
+ return path
+ else:
+ s = result[0]
+ return os.path.join(s, *reversed(tail_parts))
+ # Means fallback on absolute
+ return None
+
+ def _split_extended_path(self, s, ext_prefix=ext_namespace_prefix):
+ prefix = ''
+ if s.startswith(ext_prefix):
+ prefix = s[:4]
+ s = s[4:]
+ if s.startswith('UNC\\'):
+ prefix += s[:3]
+ s = '\\' + s[3:]
+ return prefix, s
+
+ def _ext_to_normal(self, s):
+ # Turn back an extended path into a normal DOS-like path
+ return self._split_extended_path(s)[1]
+
+ def is_reserved(self, parts):
+ # NOTE: the rules for reserved names seem somewhat complicated
+ # (e.g. r"..\NUL" is reserved but not r"foo\NUL").
+ # We err on the side of caution and return True for paths which are
+ # not considered reserved by Windows.
+ if not parts:
+ return False
+ if parts[0].startswith('\\\\'):
+ # UNC paths are never reserved
+ return False
+ return parts[-1].partition('.')[0].upper() in self.reserved_names
+
+ def make_uri(self, path):
+ # Under Windows, file URIs use the UTF-8 encoding.
+ drive = path.drive
+ if len(drive) == 2 and drive[1] == ':':
+ # It's a path on a local drive => 'file:///c:/a/b'
+ rest = path.as_posix()[2:].lstrip('/')
+ return 'file:///%s/%s' % (
+ drive, urlquote_from_bytes(rest.encode('utf-8')))
+ else:
+ # It's a path on a network drive => 'file://host/share/a/b'
+ return 'file:' + urlquote_from_bytes(
+ path.as_posix().encode('utf-8'))
+
+ def gethomedir(self, username):
+ if 'HOME' in os.environ:
+ userhome = os.environ['HOME']
+ elif 'USERPROFILE' in os.environ:
+ userhome = os.environ['USERPROFILE']
+ elif 'HOMEPATH' in os.environ:
+ try:
+ drv = os.environ['HOMEDRIVE']
+ except KeyError:
+ drv = ''
+ userhome = drv + os.environ['HOMEPATH']
+ else:
+ raise RuntimeError("Can't determine home directory")
+
+ if username:
+ # Try to guess user home directory. By default all users
+ # directories are located in the same place and are named by
+ # corresponding usernames. If current user home directory points
+ # to nonstandard place, this guess is likely wrong.
+ if os.environ['USERNAME'] != username:
+ drv, root, parts = self.parse_parts((userhome,))
+ if parts[-1] != os.environ['USERNAME']:
+ raise RuntimeError("Can't determine home directory "
+ "for %r" % username)
+ parts[-1] = username
+ if drv or root:
+ userhome = drv + root + self.join(parts[1:])
+ else:
+ userhome = self.join(parts)
+ return userhome
+
+
+class _PosixFlavour(_Flavour):
+ sep = '/'
+ altsep = ''
+ has_drv = False
+ pathmod = posixpath
+
+ is_supported = (os.name != 'nt')
+
+ def splitroot(self, part, sep=sep):
+ if part and part[0] == sep:
+ stripped_part = part.lstrip(sep)
+ # According to POSIX path resolution:
+ # http://pubs.opengroup.org/onlinepubs/009695399/basedefs/
+ # xbd_chap04.html#tag_04_11
+ # "A pathname that begins with two successive slashes may be
+ # interpreted in an implementation-defined manner, although more
+ # than two leading slashes shall be treated as a single slash".
+ if len(part) - len(stripped_part) == 2:
+ return '', sep * 2, stripped_part
+ else:
+ return '', sep, stripped_part
+ else:
+ return '', '', part
+
+ def casefold(self, s):
+ return s
+
+ def casefold_parts(self, parts):
+ return parts
+
+ def resolve(self, path, strict=False):
+ sep = self.sep
+ accessor = path._accessor
+ seen = {}
+
+ def _resolve(path, rest):
+ if rest.startswith(sep):
+ path = ''
+
+ for name in rest.split(sep):
+ if not name or name == '.':
+ # current dir
+ continue
+ if name == '..':
+ # parent dir
+ path, _, _ = path.rpartition(sep)
+ continue
+ newpath = path + sep + name
+ if newpath in seen:
+ # Already seen this path
+ path = seen[newpath]
+ if path is not None:
+ # use cached value
+ continue
+ # The symlink is not resolved, so we must have a symlink
+ # loop.
+ raise RuntimeError("Symlink loop from %r" % newpath)
+ # Resolve the symbolic link
+ try:
+ target = accessor.readlink(newpath)
+ except OSError as e:
+ if e.errno != EINVAL and strict:
+ raise
+ # Not a symlink, or non-strict mode. We just leave the path
+ # untouched.
+ path = newpath
+ else:
+ seen[newpath] = None # not resolved symlink
+ path = _resolve(path, target)
+ seen[newpath] = path # resolved symlink
+
+ return path
+ # NOTE: according to POSIX, getcwd() cannot contain path components
+ # which are symlinks.
+ base = '' if path.is_absolute() else os.getcwd()
+ return _resolve(base, str(path)) or sep
+
+ def is_reserved(self, parts):
+ return False
+
+ def make_uri(self, path):
+ # We represent the path using the local filesystem encoding,
+ # for portability to other applications.
+ bpath = bytes(path)
+ return 'file://' + urlquote_from_bytes(bpath)
+
+ def gethomedir(self, username):
+ if not username:
+ try:
+ return os.environ['HOME']
+ except KeyError:
+ import pwd
+ return pwd.getpwuid(os.getuid()).pw_dir
+ else:
+ import pwd
+ try:
+ return pwd.getpwnam(username).pw_dir
+ except KeyError:
+ raise RuntimeError("Can't determine home directory "
+ "for %r" % username)
+
+
+_windows_flavour = _WindowsFlavour()
+_posix_flavour = _PosixFlavour()
+
+
+class _Accessor:
+
+ """An accessor implements a particular (system-specific or not) way of
+ accessing paths on the filesystem."""
+
+
+class _NormalAccessor(_Accessor):
+
+ def _wrap_strfunc(strfunc):
+ @functools.wraps(strfunc)
+ def wrapped(pathobj, *args):
+ return strfunc(str(pathobj), *args)
+ return staticmethod(wrapped)
+
+ def _wrap_binary_strfunc(strfunc):
+ @functools.wraps(strfunc)
+ def wrapped(pathobjA, pathobjB, *args):
+ return strfunc(str(pathobjA), str(pathobjB), *args)
+ return staticmethod(wrapped)
+
+ stat = _wrap_strfunc(os.stat)
+
+ lstat = _wrap_strfunc(os.lstat)
+
+ open = _wrap_strfunc(os.open)
+
+ listdir = _wrap_strfunc(os.listdir)
+
+ scandir = _wrap_strfunc(os_scandir)
+
+ chmod = _wrap_strfunc(os.chmod)
+
+ if hasattr(os, "lchmod"):
+ lchmod = _wrap_strfunc(os.lchmod)
+ else:
+ def lchmod(self, pathobj, mode):
+ raise NotImplementedError("lchmod() not available on this system")
+
+ mkdir = _wrap_strfunc(os.mkdir)
+
+ unlink = _wrap_strfunc(os.unlink)
+
+ rmdir = _wrap_strfunc(os.rmdir)
+
+ rename = _wrap_binary_strfunc(os.rename)
+
+ if sys.version_info >= (3, 3):
+ replace = _wrap_binary_strfunc(os.replace)
+
+ if nt:
+ if supports_symlinks:
+ symlink = _wrap_binary_strfunc(os.symlink)
+ else:
+ def symlink(a, b, target_is_directory):
+ raise NotImplementedError(
+ "symlink() not available on this system")
+ else:
+ # Under POSIX, os.symlink() takes two args
+ @staticmethod
+ def symlink(a, b, target_is_directory):
+ return os.symlink(str(a), str(b))
+
+ utime = _wrap_strfunc(os.utime)
+
+ # Helper for resolve()
+ def readlink(self, path):
+ return os.readlink(path)
+
+
+_normal_accessor = _NormalAccessor()
+
+
+#
+# Globbing helpers
+#
+
+def _make_selector(pattern_parts):
+ pat = pattern_parts[0]
+ child_parts = pattern_parts[1:]
+ if pat == '**':
+ cls = _RecursiveWildcardSelector
+ elif '**' in pat:
+ raise ValueError(
+ "Invalid pattern: '**' can only be an entire path component")
+ elif _is_wildcard_pattern(pat):
+ cls = _WildcardSelector
+ else:
+ cls = _PreciseSelector
+ return cls(pat, child_parts)
+
+
+if hasattr(functools, "lru_cache"):
+ _make_selector = functools.lru_cache()(_make_selector)
+
+
+class _Selector:
+
+ """A selector matches a specific glob pattern part against the children
+ of a given path."""
+
+ def __init__(self, child_parts):
+ self.child_parts = child_parts
+ if child_parts:
+ self.successor = _make_selector(child_parts)
+ self.dironly = True
+ else:
+ self.successor = _TerminatingSelector()
+ self.dironly = False
+
+ def select_from(self, parent_path):
+ """Iterate over all child paths of `parent_path` matched by this
+ selector. This can contain parent_path itself."""
+ path_cls = type(parent_path)
+ is_dir = path_cls.is_dir
+ exists = path_cls.exists
+ scandir = parent_path._accessor.scandir
+ if not is_dir(parent_path):
+ return iter([])
+ return self._select_from(parent_path, is_dir, exists, scandir)
+
+
+class _TerminatingSelector:
+
+ def _select_from(self, parent_path, is_dir, exists, scandir):
+ yield parent_path
+
+
+class _PreciseSelector(_Selector):
+
+ def __init__(self, name, child_parts):
+ self.name = name
+ _Selector.__init__(self, child_parts)
+
+ def _select_from(self, parent_path, is_dir, exists, scandir):
+ def try_iter():
+ path = parent_path._make_child_relpath(self.name)
+ if (is_dir if self.dironly else exists)(path):
+ for p in self.successor._select_from(
+ path, is_dir, exists, scandir):
+ yield p
+
+ def except_iter(exc):
+ return
+ yield
+
+ for x in _try_except_permissionerror_iter(try_iter, except_iter):
+ yield x
+
+
+class _WildcardSelector(_Selector):
+
+ def __init__(self, pat, child_parts):
+ self.pat = re.compile(fnmatch.translate(pat))
+ _Selector.__init__(self, child_parts)
+
+ def _select_from(self, parent_path, is_dir, exists, scandir):
+ def try_iter():
+ cf = parent_path._flavour.casefold
+ entries = list(scandir(parent_path))
+ for entry in entries:
+ if not self.dironly or entry.is_dir():
+ name = entry.name
+ casefolded = cf(name)
+ if self.pat.match(casefolded):
+ path = parent_path._make_child_relpath(name)
+ for p in self.successor._select_from(
+ path, is_dir, exists, scandir):
+ yield p
+
+ def except_iter(exc):
+ return
+ yield
+
+ for x in _try_except_permissionerror_iter(try_iter, except_iter):
+ yield x
+
+
+class _RecursiveWildcardSelector(_Selector):
+
+ def __init__(self, pat, child_parts):
+ _Selector.__init__(self, child_parts)
+
+ def _iterate_directories(self, parent_path, is_dir, scandir):
+ yield parent_path
+
+ def try_iter():
+ entries = list(scandir(parent_path))
+ for entry in entries:
+ entry_is_dir = False
+ try:
+ entry_is_dir = entry.is_dir()
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ if entry_is_dir and not entry.is_symlink():
+ path = parent_path._make_child_relpath(entry.name)
+ for p in self._iterate_directories(path, is_dir, scandir):
+ yield p
+
+ def except_iter(exc):
+ return
+ yield
+
+ for x in _try_except_permissionerror_iter(try_iter, except_iter):
+ yield x
+
+ def _select_from(self, parent_path, is_dir, exists, scandir):
+ def try_iter():
+ yielded = set()
+ try:
+ successor_select = self.successor._select_from
+ for starting_point in self._iterate_directories(
+ parent_path, is_dir, scandir):
+ for p in successor_select(
+ starting_point, is_dir, exists, scandir):
+ if p not in yielded:
+ yield p
+ yielded.add(p)
+ finally:
+ yielded.clear()
+
+ def except_iter(exc):
+ return
+ yield
+
+ for x in _try_except_permissionerror_iter(try_iter, except_iter):
+ yield x
+
+
+#
+# Public API
+#
+
+class _PathParents(Sequence):
+
+ """This object provides sequence-like access to the logical ancestors
+ of a path. Don't try to construct it yourself."""
+ __slots__ = ('_pathcls', '_drv', '_root', '_parts')
+
+ def __init__(self, path):
+ # We don't store the instance to avoid reference cycles
+ self._pathcls = type(path)
+ self._drv = path._drv
+ self._root = path._root
+ self._parts = path._parts
+
+ def __len__(self):
+ if self._drv or self._root:
+ return len(self._parts) - 1
+ else:
+ return len(self._parts)
+
+ def __getitem__(self, idx):
+ if idx < 0 or idx >= len(self):
+ raise IndexError(idx)
+ return self._pathcls._from_parsed_parts(self._drv, self._root,
+ self._parts[:-idx - 1])
+
+ def __repr__(self):
+ return "<{0}.parents>".format(self._pathcls.__name__)
+
+
+class PurePath(object):
+
+ """PurePath represents a filesystem path and offers operations which
+ don't imply any actual filesystem I/O. Depending on your system,
+ instantiating a PurePath will return either a PurePosixPath or a
+ PureWindowsPath object. You can also instantiate either of these classes
+ directly, regardless of your system.
+ """
+ __slots__ = (
+ '_drv', '_root', '_parts',
+ '_str', '_hash', '_pparts', '_cached_cparts',
+ )
+
+ def __new__(cls, *args):
+ """Construct a PurePath from one or several strings and or existing
+ PurePath objects. The strings and path objects are combined so as
+ to yield a canonicalized path, which is incorporated into the
+ new PurePath object.
+ """
+ if cls is PurePath:
+ cls = PureWindowsPath if os.name == 'nt' else PurePosixPath
+ return cls._from_parts(args)
+
+ def __reduce__(self):
+ # Using the parts tuple helps share interned path parts
+ # when pickling related paths.
+ return (self.__class__, tuple(self._parts))
+
+ @classmethod
+ def _parse_args(cls, args):
+ # This is useful when you don't want to create an instance, just
+ # canonicalize some constructor arguments.
+ parts = []
+ for a in args:
+ if isinstance(a, PurePath):
+ parts += a._parts
+ else:
+ if sys.version_info >= (3, 6):
+ a = os.fspath(a)
+ else:
+ # duck typing for older Python versions
+ if hasattr(a, "__fspath__"):
+ a = a.__fspath__()
+ if isinstance(a, str):
+ # Force-cast str subclasses to str (issue #21127)
+ parts.append(str(a))
+ # also handle unicode for PY2 (six.text_type = unicode)
+ elif six.PY2 and isinstance(a, six.text_type):
+ # cast to str using filesystem encoding
+ # note: in rare circumstances, on Python < 3.2,
+ # getfilesystemencoding can return None, in that
+ # case fall back to ascii
+ parts.append(a.encode(
+ sys.getfilesystemencoding() or "ascii"))
+ else:
+ raise TypeError(
+ "argument should be a str object or an os.PathLike "
+ "object returning str, not %r"
+ % type(a))
+ return cls._flavour.parse_parts(parts)
+
+ @classmethod
+ def _from_parts(cls, args, init=True):
+ # We need to call _parse_args on the instance, so as to get the
+ # right flavour.
+ self = object.__new__(cls)
+ drv, root, parts = self._parse_args(args)
+ self._drv = drv
+ self._root = root
+ self._parts = parts
+ if init:
+ self._init()
+ return self
+
+ @classmethod
+ def _from_parsed_parts(cls, drv, root, parts, init=True):
+ self = object.__new__(cls)
+ self._drv = drv
+ self._root = root
+ self._parts = parts
+ if init:
+ self._init()
+ return self
+
+ @classmethod
+ def _format_parsed_parts(cls, drv, root, parts):
+ if drv or root:
+ return drv + root + cls._flavour.join(parts[1:])
+ else:
+ return cls._flavour.join(parts)
+
+ def _init(self):
+ # Overridden in concrete Path
+ pass
+
+ def _make_child(self, args):
+ drv, root, parts = self._parse_args(args)
+ drv, root, parts = self._flavour.join_parsed_parts(
+ self._drv, self._root, self._parts, drv, root, parts)
+ return self._from_parsed_parts(drv, root, parts)
+
+ def __str__(self):
+ """Return the string representation of the path, suitable for
+ passing to system calls."""
+ try:
+ return self._str
+ except AttributeError:
+ self._str = self._format_parsed_parts(self._drv, self._root,
+ self._parts) or '.'
+ return self._str
+
+ def __fspath__(self):
+ return str(self)
+
+ def as_posix(self):
+ """Return the string representation of the path with forward (/)
+ slashes."""
+ f = self._flavour
+ return str(self).replace(f.sep, '/')
+
+ def __bytes__(self):
+ """Return the bytes representation of the path. This is only
+ recommended to use under Unix."""
+ if sys.version_info < (3, 2):
+ raise NotImplementedError("needs Python 3.2 or later")
+ return os.fsencode(str(self))
+
+ def __repr__(self):
+ return "{0}({1!r})".format(self.__class__.__name__, self.as_posix())
+
+ def as_uri(self):
+ """Return the path as a 'file' URI."""
+ if not self.is_absolute():
+ raise ValueError("relative path can't be expressed as a file URI")
+ return self._flavour.make_uri(self)
+
+ @property
+ def _cparts(self):
+ # Cached casefolded parts, for hashing and comparison
+ try:
+ return self._cached_cparts
+ except AttributeError:
+ self._cached_cparts = self._flavour.casefold_parts(self._parts)
+ return self._cached_cparts
+
+ def __eq__(self, other):
+ if not isinstance(other, PurePath):
+ return NotImplemented
+ return (
+ self._cparts == other._cparts
+ and self._flavour is other._flavour)
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __hash__(self):
+ try:
+ return self._hash
+ except AttributeError:
+ self._hash = hash(tuple(self._cparts))
+ return self._hash
+
+ def __lt__(self, other):
+ if (not isinstance(other, PurePath)
+ or self._flavour is not other._flavour):
+ return NotImplemented
+ return self._cparts < other._cparts
+
+ def __le__(self, other):
+ if (not isinstance(other, PurePath)
+ or self._flavour is not other._flavour):
+ return NotImplemented
+ return self._cparts <= other._cparts
+
+ def __gt__(self, other):
+ if (not isinstance(other, PurePath)
+ or self._flavour is not other._flavour):
+ return NotImplemented
+ return self._cparts > other._cparts
+
+ def __ge__(self, other):
+ if (not isinstance(other, PurePath)
+ or self._flavour is not other._flavour):
+ return NotImplemented
+ return self._cparts >= other._cparts
+
+ drive = property(attrgetter('_drv'),
+ doc="""The drive prefix (letter or UNC path), if any.""")
+
+ root = property(attrgetter('_root'),
+ doc="""The root of the path, if any.""")
+
+ @property
+ def anchor(self):
+ """The concatenation of the drive and root, or ''."""
+ anchor = self._drv + self._root
+ return anchor
+
+ @property
+ def name(self):
+ """The final path component, if any."""
+ parts = self._parts
+ if len(parts) == (1 if (self._drv or self._root) else 0):
+ return ''
+ return parts[-1]
+
+ @property
+ def suffix(self):
+ """The final component's last suffix, if any."""
+ name = self.name
+ i = name.rfind('.')
+ if 0 < i < len(name) - 1:
+ return name[i:]
+ else:
+ return ''
+
+ @property
+ def suffixes(self):
+ """A list of the final component's suffixes, if any."""
+ name = self.name
+ if name.endswith('.'):
+ return []
+ name = name.lstrip('.')
+ return ['.' + suffix for suffix in name.split('.')[1:]]
+
+ @property
+ def stem(self):
+ """The final path component, minus its last suffix."""
+ name = self.name
+ i = name.rfind('.')
+ if 0 < i < len(name) - 1:
+ return name[:i]
+ else:
+ return name
+
+ def with_name(self, name):
+ """Return a new path with the file name changed."""
+ if not self.name:
+ raise ValueError("%r has an empty name" % (self,))
+ drv, root, parts = self._flavour.parse_parts((name,))
+ if (not name or name[-1] in [self._flavour.sep, self._flavour.altsep]
+ or drv or root or len(parts) != 1):
+ raise ValueError("Invalid name %r" % (name))
+ return self._from_parsed_parts(self._drv, self._root,
+ self._parts[:-1] + [name])
+
+ def with_suffix(self, suffix):
+ """Return a new path with the file suffix changed. If the path
+ has no suffix, add given suffix. If the given suffix is an empty
+ string, remove the suffix from the path.
+ """
+ # XXX if suffix is None, should the current suffix be removed?
+ f = self._flavour
+ if f.sep in suffix or f.altsep and f.altsep in suffix:
+ raise ValueError("Invalid suffix %r" % (suffix))
+ if suffix and not suffix.startswith('.') or suffix == '.':
+ raise ValueError("Invalid suffix %r" % (suffix))
+ name = self.name
+ if not name:
+ raise ValueError("%r has an empty name" % (self,))
+ old_suffix = self.suffix
+ if not old_suffix:
+ name = name + suffix
+ else:
+ name = name[:-len(old_suffix)] + suffix
+ return self._from_parsed_parts(self._drv, self._root,
+ self._parts[:-1] + [name])
+
+ def relative_to(self, *other):
+ """Return the relative path to another path identified by the passed
+ arguments. If the operation is not possible (because this is not
+ a subpath of the other path), raise ValueError.
+ """
+ # For the purpose of this method, drive and root are considered
+ # separate parts, i.e.:
+ # Path('c:/').relative_to('c:') gives Path('/')
+ # Path('c:/').relative_to('/') raise ValueError
+ if not other:
+ raise TypeError("need at least one argument")
+ parts = self._parts
+ drv = self._drv
+ root = self._root
+ if root:
+ abs_parts = [drv, root] + parts[1:]
+ else:
+ abs_parts = parts
+ to_drv, to_root, to_parts = self._parse_args(other)
+ if to_root:
+ to_abs_parts = [to_drv, to_root] + to_parts[1:]
+ else:
+ to_abs_parts = to_parts
+ n = len(to_abs_parts)
+ cf = self._flavour.casefold_parts
+ if (root or drv) if n == 0 else cf(abs_parts[:n]) != cf(to_abs_parts):
+ formatted = self._format_parsed_parts(to_drv, to_root, to_parts)
+ raise ValueError("{0!r} does not start with {1!r}"
+ .format(str(self), str(formatted)))
+ return self._from_parsed_parts('', root if n == 1 else '',
+ abs_parts[n:])
+
+ @property
+ def parts(self):
+ """An object providing sequence-like access to the
+ components in the filesystem path."""
+ # We cache the tuple to avoid building a new one each time .parts
+ # is accessed. XXX is this necessary?
+ try:
+ return self._pparts
+ except AttributeError:
+ self._pparts = tuple(self._parts)
+ return self._pparts
+
+ def joinpath(self, *args):
+ """Combine this path with one or several arguments, and return a
+ new path representing either a subpath (if all arguments are relative
+ paths) or a totally different path (if one of the arguments is
+ anchored).
+ """
+ return self._make_child(args)
+
+ def __truediv__(self, key):
+ return self._make_child((key,))
+
+ def __rtruediv__(self, key):
+ return self._from_parts([key] + self._parts)
+
+ if six.PY2:
+ __div__ = __truediv__
+ __rdiv__ = __rtruediv__
+
+ @property
+ def parent(self):
+ """The logical parent of the path."""
+ drv = self._drv
+ root = self._root
+ parts = self._parts
+ if len(parts) == 1 and (drv or root):
+ return self
+ return self._from_parsed_parts(drv, root, parts[:-1])
+
+ @property
+ def parents(self):
+ """A sequence of this path's logical parents."""
+ return _PathParents(self)
+
+ def is_absolute(self):
+ """True if the path is absolute (has both a root and, if applicable,
+ a drive)."""
+ if not self._root:
+ return False
+ return not self._flavour.has_drv or bool(self._drv)
+
+ def is_reserved(self):
+ """Return True if the path contains one of the special names reserved
+ by the system, if any."""
+ return self._flavour.is_reserved(self._parts)
+
+ def match(self, path_pattern):
+ """
+ Return True if this path matches the given pattern.
+ """
+ cf = self._flavour.casefold
+ path_pattern = cf(path_pattern)
+ drv, root, pat_parts = self._flavour.parse_parts((path_pattern,))
+ if not pat_parts:
+ raise ValueError("empty pattern")
+ if drv and drv != cf(self._drv):
+ return False
+ if root and root != cf(self._root):
+ return False
+ parts = self._cparts
+ if drv or root:
+ if len(pat_parts) != len(parts):
+ return False
+ pat_parts = pat_parts[1:]
+ elif len(pat_parts) > len(parts):
+ return False
+ for part, pat in zip(reversed(parts), reversed(pat_parts)):
+ if not fnmatch.fnmatchcase(part, pat):
+ return False
+ return True
+
+
+# Can't subclass os.PathLike from PurePath and keep the constructor
+# optimizations in PurePath._parse_args().
+if sys.version_info >= (3, 6):
+ os.PathLike.register(PurePath)
+
+
+class PurePosixPath(PurePath):
+ _flavour = _posix_flavour
+ __slots__ = ()
+
+
+class PureWindowsPath(PurePath):
+ """PurePath subclass for Windows systems.
+
+ On a Windows system, instantiating a PurePath should return this object.
+ However, you can also instantiate it directly on any system.
+ """
+ _flavour = _windows_flavour
+ __slots__ = ()
+
+
+# Filesystem-accessing classes
+
+
+class Path(PurePath):
+ """PurePath subclass that can make system calls.
+
+ Path represents a filesystem path but unlike PurePath, also offers
+ methods to do system calls on path objects. Depending on your system,
+ instantiating a Path will return either a PosixPath or a WindowsPath
+ object. You can also instantiate a PosixPath or WindowsPath directly,
+ but cannot instantiate a WindowsPath on a POSIX system or vice versa.
+ """
+ __slots__ = (
+ '_accessor',
+ '_closed',
+ )
+
+ def __new__(cls, *args, **kwargs):
+ if cls is Path:
+ cls = WindowsPath if os.name == 'nt' else PosixPath
+ self = cls._from_parts(args, init=False)
+ if not self._flavour.is_supported:
+ raise NotImplementedError("cannot instantiate %r on your system"
+ % (cls.__name__,))
+ self._init()
+ return self
+
+ def _init(self,
+ # Private non-constructor arguments
+ template=None,
+ ):
+ self._closed = False
+ if template is not None:
+ self._accessor = template._accessor
+ else:
+ self._accessor = _normal_accessor
+
+ def _make_child_relpath(self, part):
+ # This is an optimization used for dir walking. `part` must be
+ # a single part relative to this path.
+ parts = self._parts + [part]
+ return self._from_parsed_parts(self._drv, self._root, parts)
+
+ def __enter__(self):
+ if self._closed:
+ self._raise_closed()
+ return self
+
+ def __exit__(self, t, v, tb):
+ self._closed = True
+
+ def _raise_closed(self):
+ raise ValueError("I/O operation on closed path")
+
+ def _opener(self, name, flags, mode=0o666):
+ # A stub for the opener argument to built-in open()
+ return self._accessor.open(self, flags, mode)
+
+ def _raw_open(self, flags, mode=0o777):
+ """
+ Open the file pointed by this path and return a file descriptor,
+ as os.open() does.
+ """
+ if self._closed:
+ self._raise_closed()
+ return self._accessor.open(self, flags, mode)
+
+ # Public API
+
+ @classmethod
+ def cwd(cls):
+ """Return a new path pointing to the current working directory
+ (as returned by os.getcwd()).
+ """
+ return cls(os.getcwd())
+
+ @classmethod
+ def home(cls):
+ """Return a new path pointing to the user's home directory (as
+ returned by os.path.expanduser('~')).
+ """
+ return cls(cls()._flavour.gethomedir(None))
+
+ def samefile(self, other_path):
+ """Return whether other_path is the same or not as this file
+ (as returned by os.path.samefile()).
+ """
+ if hasattr(os.path, "samestat"):
+ st = self.stat()
+ try:
+ other_st = other_path.stat()
+ except AttributeError:
+ other_st = os.stat(other_path)
+ return os.path.samestat(st, other_st)
+ else:
+ filename1 = six.text_type(self)
+ filename2 = six.text_type(other_path)
+ st1 = _win32_get_unique_path_id(filename1)
+ st2 = _win32_get_unique_path_id(filename2)
+ return st1 == st2
+
+ def iterdir(self):
+ """Iterate over the files in this directory. Does not yield any
+ result for the special paths '.' and '..'.
+ """
+ if self._closed:
+ self._raise_closed()
+ for name in self._accessor.listdir(self):
+ if name in ('.', '..'):
+ # Yielding a path object for these makes little sense
+ continue
+ yield self._make_child_relpath(name)
+ if self._closed:
+ self._raise_closed()
+
+ def glob(self, pattern):
+ """Iterate over this subtree and yield all existing files (of any
+ kind, including directories) matching the given relative pattern.
+ """
+ if not pattern:
+ raise ValueError("Unacceptable pattern: {0!r}".format(pattern))
+ pattern = self._flavour.casefold(pattern)
+ drv, root, pattern_parts = self._flavour.parse_parts((pattern,))
+ if drv or root:
+ raise NotImplementedError("Non-relative patterns are unsupported")
+ selector = _make_selector(tuple(pattern_parts))
+ for p in selector.select_from(self):
+ yield p
+
+ def rglob(self, pattern):
+ """Recursively yield all existing files (of any kind, including
+ directories) matching the given relative pattern, anywhere in
+ this subtree.
+ """
+ pattern = self._flavour.casefold(pattern)
+ drv, root, pattern_parts = self._flavour.parse_parts((pattern,))
+ if drv or root:
+ raise NotImplementedError("Non-relative patterns are unsupported")
+ selector = _make_selector(("**",) + tuple(pattern_parts))
+ for p in selector.select_from(self):
+ yield p
+
+ def absolute(self):
+ """Return an absolute version of this path. This function works
+ even if the path doesn't point to anything.
+
+ No normalization is done, i.e. all '.' and '..' will be kept along.
+ Use resolve() to get the canonical path to a file.
+ """
+ # XXX untested yet!
+ if self._closed:
+ self._raise_closed()
+ if self.is_absolute():
+ return self
+ # FIXME this must defer to the specific flavour (and, under Windows,
+ # use nt._getfullpathname())
+ obj = self._from_parts([os.getcwd()] + self._parts, init=False)
+ obj._init(template=self)
+ return obj
+
+ def resolve(self, strict=False):
+ """
+ Make the path absolute, resolving all symlinks on the way and also
+ normalizing it (for example turning slashes into backslashes under
+ Windows).
+ """
+ if self._closed:
+ self._raise_closed()
+ s = self._flavour.resolve(self, strict=strict)
+ if s is None:
+ # No symlink resolution => for consistency, raise an error if
+ # the path is forbidden
+ # but not raise error if file does not exist (see issue #54).
+
+ def _try_func():
+ self.stat()
+
+ def _exc_func(exc):
+ pass
+
+ _try_except_filenotfounderror(_try_func, _exc_func)
+ s = str(self.absolute())
+ else:
+ # ensure s is a string (normpath requires this on older python)
+ s = str(s)
+ # Now we have no symlinks in the path, it's safe to normalize it.
+ normed = self._flavour.pathmod.normpath(s)
+ obj = self._from_parts((normed,), init=False)
+ obj._init(template=self)
+ return obj
+
+ def stat(self):
+ """
+ Return the result of the stat() system call on this path, like
+ os.stat() does.
+ """
+ return self._accessor.stat(self)
+
+ def owner(self):
+ """
+ Return the login name of the file owner.
+ """
+ import pwd
+ return pwd.getpwuid(self.stat().st_uid).pw_name
+
+ def group(self):
+ """
+ Return the group name of the file gid.
+ """
+ import grp
+ return grp.getgrgid(self.stat().st_gid).gr_name
+
+ def open(self, mode='r', buffering=-1, encoding=None,
+ errors=None, newline=None):
+ """
+ Open the file pointed by this path and return a file object, as
+ the built-in open() function does.
+ """
+ if self._closed:
+ self._raise_closed()
+ if sys.version_info >= (3, 3):
+ return io.open(
+ str(self), mode, buffering, encoding, errors, newline,
+ opener=self._opener)
+ else:
+ return io.open(str(self), mode, buffering,
+ encoding, errors, newline)
+
+ def read_bytes(self):
+ """
+ Open the file in bytes mode, read it, and close the file.
+ """
+ with self.open(mode='rb') as f:
+ return f.read()
+
+ def read_text(self, encoding=None, errors=None):
+ """
+ Open the file in text mode, read it, and close the file.
+ """
+ with self.open(mode='r', encoding=encoding, errors=errors) as f:
+ return f.read()
+
+ def write_bytes(self, data):
+ """
+ Open the file in bytes mode, write to it, and close the file.
+ """
+ if not isinstance(data, six.binary_type):
+ raise TypeError(
+ 'data must be %s, not %s' %
+ (six.binary_type.__name__, data.__class__.__name__))
+ with self.open(mode='wb') as f:
+ return f.write(data)
+
+ def write_text(self, data, encoding=None, errors=None):
+ """
+ Open the file in text mode, write to it, and close the file.
+ """
+ if not isinstance(data, six.text_type):
+ raise TypeError(
+ 'data must be %s, not %s' %
+ (six.text_type.__name__, data.__class__.__name__))
+ with self.open(mode='w', encoding=encoding, errors=errors) as f:
+ return f.write(data)
+
+ def touch(self, mode=0o666, exist_ok=True):
+ """
+ Create this file with the given access mode, if it doesn't exist.
+ """
+ if self._closed:
+ self._raise_closed()
+ if exist_ok:
+ # First try to bump modification time
+ # Implementation note: GNU touch uses the UTIME_NOW option of
+ # the utimensat() / futimens() functions.
+ try:
+ self._accessor.utime(self, None)
+ except OSError:
+ # Avoid exception chaining
+ pass
+ else:
+ return
+ flags = os.O_CREAT | os.O_WRONLY
+ if not exist_ok:
+ flags |= os.O_EXCL
+ fd = self._raw_open(flags, mode)
+ os.close(fd)
+
+ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
+ """
+ Create a new directory at this given path.
+ """
+ if self._closed:
+ self._raise_closed()
+
+ def _try_func():
+ self._accessor.mkdir(self, mode)
+
+ def _exc_func(exc):
+ if not parents or self.parent == self:
+ raise exc
+ self.parent.mkdir(parents=True, exist_ok=True)
+ self.mkdir(mode, parents=False, exist_ok=exist_ok)
+
+ try:
+ _try_except_filenotfounderror(_try_func, _exc_func)
+ except OSError:
+ # Cannot rely on checking for EEXIST, since the operating system
+ # could give priority to other errors like EACCES or EROFS
+ if not exist_ok or not self.is_dir():
+ raise
+
+ def chmod(self, mode):
+ """
+ Change the permissions of the path, like os.chmod().
+ """
+ if self._closed:
+ self._raise_closed()
+ self._accessor.chmod(self, mode)
+
+ def lchmod(self, mode):
+ """
+ Like chmod(), except if the path points to a symlink, the symlink's
+ permissions are changed, rather than its target's.
+ """
+ if self._closed:
+ self._raise_closed()
+ self._accessor.lchmod(self, mode)
+
+ def unlink(self):
+ """
+ Remove this file or link.
+ If the path is a directory, use rmdir() instead.
+ """
+ if self._closed:
+ self._raise_closed()
+ self._accessor.unlink(self)
+
+ def rmdir(self):
+ """
+ Remove this directory. The directory must be empty.
+ """
+ if self._closed:
+ self._raise_closed()
+ self._accessor.rmdir(self)
+
+ def lstat(self):
+ """
+ Like stat(), except if the path points to a symlink, the symlink's
+ status information is returned, rather than its target's.
+ """
+ if self._closed:
+ self._raise_closed()
+ return self._accessor.lstat(self)
+
+ def rename(self, target):
+ """
+ Rename this path to the given path.
+ """
+ if self._closed:
+ self._raise_closed()
+ self._accessor.rename(self, target)
+
+ def replace(self, target):
+ """
+ Rename this path to the given path, clobbering the existing
+ destination if it exists.
+ """
+ if sys.version_info < (3, 3):
+ raise NotImplementedError("replace() is only available "
+ "with Python 3.3 and later")
+ if self._closed:
+ self._raise_closed()
+ self._accessor.replace(self, target)
+
+ def symlink_to(self, target, target_is_directory=False):
+ """
+ Make this path a symlink pointing to the given path.
+ Note the order of arguments (self, target) is the reverse of
+ os.symlink's.
+ """
+ if self._closed:
+ self._raise_closed()
+ self._accessor.symlink(target, self, target_is_directory)
+
+ # Convenience functions for querying the stat results
+
+ def exists(self):
+ """
+ Whether this path exists.
+ """
+ try:
+ self.stat()
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+ return True
+
+ def is_dir(self):
+ """
+ Whether this path is a directory.
+ """
+ try:
+ return S_ISDIR(self.stat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist or is a broken symlink
+ # (see https://bitbucket.org/pitrou/pathlib/issue/12/)
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+
+ def is_file(self):
+ """
+ Whether this path is a regular file (also True for symlinks pointing
+ to regular files).
+ """
+ try:
+ return S_ISREG(self.stat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist or is a broken symlink
+ # (see https://bitbucket.org/pitrou/pathlib/issue/12/)
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+
+ def is_mount(self):
+ """
+ Check if this path is a POSIX mount point
+ """
+ # Need to exist and be a dir
+ if not self.exists() or not self.is_dir():
+ return False
+
+ parent = Path(self.parent)
+ try:
+ parent_dev = parent.stat().st_dev
+ except OSError:
+ return False
+
+ dev = self.stat().st_dev
+ if dev != parent_dev:
+ return True
+ ino = self.stat().st_ino
+ parent_ino = parent.stat().st_ino
+ return ino == parent_ino
+
+ def is_symlink(self):
+ """
+ Whether this path is a symbolic link.
+ """
+ try:
+ return S_ISLNK(self.lstat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+
+ def is_block_device(self):
+ """
+ Whether this path is a block device.
+ """
+ try:
+ return S_ISBLK(self.stat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist or is a broken symlink
+ # (see https://bitbucket.org/pitrou/pathlib/issue/12/)
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+
+ def is_char_device(self):
+ """
+ Whether this path is a character device.
+ """
+ try:
+ return S_ISCHR(self.stat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist or is a broken symlink
+ # (see https://bitbucket.org/pitrou/pathlib/issue/12/)
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+
+ def is_fifo(self):
+ """
+ Whether this path is a FIFO.
+ """
+ try:
+ return S_ISFIFO(self.stat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist or is a broken symlink
+ # (see https://bitbucket.org/pitrou/pathlib/issue/12/)
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+
+ def is_socket(self):
+ """
+ Whether this path is a socket.
+ """
+ try:
+ return S_ISSOCK(self.stat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist or is a broken symlink
+ # (see https://bitbucket.org/pitrou/pathlib/issue/12/)
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+
+ def expanduser(self):
+ """ Return a new path with expanded ~ and ~user constructs
+ (as returned by os.path.expanduser)
+ """
+ if (not (self._drv or self._root)
+ and self._parts and self._parts[0][:1] == '~'):
+ homedir = self._flavour.gethomedir(self._parts[0][1:])
+ return self._from_parts([homedir] + self._parts[1:])
+
+ return self
+
+
+class PosixPath(Path, PurePosixPath):
+ """Path subclass for non-Windows systems.
+
+ On a POSIX system, instantiating a Path should return this object.
+ """
+ __slots__ = ()
+
+
+class WindowsPath(Path, PureWindowsPath):
+ """Path subclass for Windows systems.
+
+ On a Windows system, instantiating a Path should return this object.
+ """
+ __slots__ = ()
+
+ def owner(self):
+ raise NotImplementedError("Path.owner() is unsupported on this system")
+
+ def group(self):
+ raise NotImplementedError("Path.group() is unsupported on this system")
+
+ def is_mount(self):
+ raise NotImplementedError(
+ "Path.is_mount() is unsupported on this system")
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/requirements.txt b/testing/web-platform/tests/tools/third_party/pathlib2/requirements.txt
new file mode 100644
index 0000000000..9d43212790
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/requirements.txt
@@ -0,0 +1,3 @@
+six
+scandir; python_version < '3.5'
+mock; python_version < '3.3'
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/setup.cfg b/testing/web-platform/tests/tools/third_party/pathlib2/setup.cfg
new file mode 100644
index 0000000000..32afc861d3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/setup.cfg
@@ -0,0 +1,8 @@
+[nosetests]
+with-coverage=1
+cover-package=pathlib2
+cover-branches=1
+cover-html=1
+
+[wheel]
+universal = 1
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/setup.py b/testing/web-platform/tests/tools/third_party/pathlib2/setup.py
new file mode 100644
index 0000000000..cbb6aaa0df
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/setup.py
@@ -0,0 +1,48 @@
+# Copyright (c) 2014-2017 Matthias C. M. Troffaes
+# Copyright (c) 2012-2014 Antoine Pitrou and contributors
+# Distributed under the terms of the MIT License.
+
+import io
+from setuptools import setup, find_packages
+
+
+def readfile(filename):
+ with io.open(filename, encoding="utf-8") as stream:
+ return stream.read().split("\n")
+
+
+readme = readfile("README.rst")[5:] # skip title and badges
+version = readfile("VERSION")[0].strip()
+
+setup(
+ name='pathlib2',
+ version=version,
+ packages=find_packages(),
+ license='MIT',
+ description='Object-oriented filesystem paths',
+ long_description="\n".join(readme[2:]),
+ author='Matthias C. M. Troffaes',
+ author_email='matthias.troffaes@gmail.com',
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Topic :: Software Development :: Libraries',
+ 'Topic :: System :: Filesystems',
+ ],
+ url='https://github.com/mcmtroffaes/pathlib2',
+ install_requires=['six'],
+ extras_require={
+ ':python_version<"3.5"': ['scandir'],
+ },
+)
diff --git a/testing/web-platform/tests/tools/third_party/pathlib2/tests/test_pathlib2.py b/testing/web-platform/tests/tools/third_party/pathlib2/tests/test_pathlib2.py
new file mode 100644
index 0000000000..65a5281a9d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pathlib2/tests/test_pathlib2.py
@@ -0,0 +1,2406 @@
+# Copyright (c) 2014-2017 Matthias C. M. Troffaes
+# Copyright (c) 2012-2014 Antoine Pitrou and contributors
+# Distributed under the terms of the MIT License.
+
+
+import io
+import os
+import errno
+import pathlib2 as pathlib
+import pickle
+import six
+import socket
+import stat
+import sys
+import tempfile
+
+if sys.version_info >= (3, 3):
+ import collections.abc as collections_abc
+else:
+ import collections as collections_abc
+
+if sys.version_info < (2, 7):
+ try:
+ import unittest2 as unittest
+ except ImportError:
+ raise ImportError("unittest2 is required for tests on pre-2.7")
+else:
+ import unittest
+
+if sys.version_info < (3, 3):
+ try:
+ import mock
+ except ImportError:
+ raise ImportError("mock is required for tests on pre-3.3")
+else:
+ from unittest import mock
+
+# assertRaisesRegex is missing prior to Python 3.2
+if sys.version_info < (3, 2):
+ unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
+
+try:
+ from test import support
+except ImportError:
+ from test import test_support as support
+
+android_not_root = getattr(support, "android_not_root", False)
+
+TESTFN = support.TESTFN
+
+# work around broken support.rmtree on Python 3.3 on Windows
+if (os.name == 'nt'
+ and sys.version_info >= (3, 0) and sys.version_info < (3, 4)):
+ import shutil
+ support.rmtree = shutil.rmtree
+
+try:
+ import grp
+ import pwd
+except ImportError:
+ grp = pwd = None
+
+# support.can_symlink is missing prior to Python 3
+if six.PY2:
+
+ def support_can_symlink():
+ return pathlib.supports_symlinks
+
+ support_skip_unless_symlink = unittest.skipIf(
+ not pathlib.supports_symlinks,
+ "symlinks not supported on this platform")
+else:
+ support_can_symlink = support.can_symlink
+ support_skip_unless_symlink = support.skip_unless_symlink
+
+
+# Backported from 3.4
+def fs_is_case_insensitive(directory):
+ """Detects if the file system for the specified directory is
+ case-insensitive.
+ """
+ base_fp, base_path = tempfile.mkstemp(dir=directory)
+ case_path = base_path.upper()
+ if case_path == base_path:
+ case_path = base_path.lower()
+ try:
+ return os.path.samefile(base_path, case_path)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+ return False
+ finally:
+ os.unlink(base_path)
+
+
+support.fs_is_case_insensitive = fs_is_case_insensitive
+
+
+class _BaseFlavourTest(object):
+
+ def _check_parse_parts(self, arg, expected):
+ f = self.flavour.parse_parts
+ sep = self.flavour.sep
+ altsep = self.flavour.altsep
+ actual = f([x.replace('/', sep) for x in arg])
+ self.assertEqual(actual, expected)
+ if altsep:
+ actual = f([x.replace('/', altsep) for x in arg])
+ self.assertEqual(actual, expected)
+ drv, root, parts = actual
+ # neither bytes (py3) nor unicode (py2)
+ self.assertIsInstance(drv, str)
+ self.assertIsInstance(root, str)
+ for p in parts:
+ self.assertIsInstance(p, str)
+
+ def test_parse_parts_common(self):
+ check = self._check_parse_parts
+ sep = self.flavour.sep
+ # Unanchored parts
+ check([], ('', '', []))
+ check(['a'], ('', '', ['a']))
+ check(['a/'], ('', '', ['a']))
+ check(['a', 'b'], ('', '', ['a', 'b']))
+ # Expansion
+ check(['a/b'], ('', '', ['a', 'b']))
+ check(['a/b/'], ('', '', ['a', 'b']))
+ check(['a', 'b/c', 'd'], ('', '', ['a', 'b', 'c', 'd']))
+ # Collapsing and stripping excess slashes
+ check(['a', 'b//c', 'd'], ('', '', ['a', 'b', 'c', 'd']))
+ check(['a', 'b/c/', 'd'], ('', '', ['a', 'b', 'c', 'd']))
+ # Eliminating standalone dots
+ check(['.'], ('', '', []))
+ check(['.', '.', 'b'], ('', '', ['b']))
+ check(['a', '.', 'b'], ('', '', ['a', 'b']))
+ check(['a', '.', '.'], ('', '', ['a']))
+ # The first part is anchored
+ check(['/a/b'], ('', sep, [sep, 'a', 'b']))
+ check(['/a', 'b'], ('', sep, [sep, 'a', 'b']))
+ check(['/a/', 'b'], ('', sep, [sep, 'a', 'b']))
+ # Ignoring parts before an anchored part
+ check(['a', '/b', 'c'], ('', sep, [sep, 'b', 'c']))
+ check(['a', '/b', '/c'], ('', sep, [sep, 'c']))
+
+
+class PosixFlavourTest(_BaseFlavourTest, unittest.TestCase):
+ flavour = pathlib._posix_flavour
+
+ def test_parse_parts(self):
+ check = self._check_parse_parts
+ # Collapsing of excess leading slashes, except for the double-slash
+ # special case.
+ check(['//a', 'b'], ('', '//', ['//', 'a', 'b']))
+ check(['///a', 'b'], ('', '/', ['/', 'a', 'b']))
+ check(['////a', 'b'], ('', '/', ['/', 'a', 'b']))
+ # Paths which look like NT paths aren't treated specially
+ check(['c:a'], ('', '', ['c:a']))
+ check(['c:\\a'], ('', '', ['c:\\a']))
+ check(['\\a'], ('', '', ['\\a']))
+
+ def test_splitroot(self):
+ f = self.flavour.splitroot
+ self.assertEqual(f(''), ('', '', ''))
+ self.assertEqual(f('a'), ('', '', 'a'))
+ self.assertEqual(f('a/b'), ('', '', 'a/b'))
+ self.assertEqual(f('a/b/'), ('', '', 'a/b/'))
+ self.assertEqual(f('/a'), ('', '/', 'a'))
+ self.assertEqual(f('/a/b'), ('', '/', 'a/b'))
+ self.assertEqual(f('/a/b/'), ('', '/', 'a/b/'))
+ # The root is collapsed when there are redundant slashes
+ # except when there are exactly two leading slashes, which
+ # is a special case in POSIX.
+ self.assertEqual(f('//a'), ('', '//', 'a'))
+ self.assertEqual(f('///a'), ('', '/', 'a'))
+ self.assertEqual(f('///a/b'), ('', '/', 'a/b'))
+ # Paths which look like NT paths aren't treated specially
+ self.assertEqual(f('c:/a/b'), ('', '', 'c:/a/b'))
+ self.assertEqual(f('\\/a/b'), ('', '', '\\/a/b'))
+ self.assertEqual(f('\\a\\b'), ('', '', '\\a\\b'))
+
+
+class NTFlavourTest(_BaseFlavourTest, unittest.TestCase):
+ flavour = pathlib._windows_flavour
+
+ def test_parse_parts(self):
+ check = self._check_parse_parts
+ # First part is anchored
+ check(['c:'], ('c:', '', ['c:']))
+ check(['c:/'], ('c:', '\\', ['c:\\']))
+ check(['/'], ('', '\\', ['\\']))
+ check(['c:a'], ('c:', '', ['c:', 'a']))
+ check(['c:/a'], ('c:', '\\', ['c:\\', 'a']))
+ check(['/a'], ('', '\\', ['\\', 'a']))
+ # UNC paths
+ check(['//a/b'], ('\\\\a\\b', '\\', ['\\\\a\\b\\']))
+ check(['//a/b/'], ('\\\\a\\b', '\\', ['\\\\a\\b\\']))
+ check(['//a/b/c'], ('\\\\a\\b', '\\', ['\\\\a\\b\\', 'c']))
+ # Second part is anchored, so that the first part is ignored
+ check(['a', 'Z:b', 'c'], ('Z:', '', ['Z:', 'b', 'c']))
+ check(['a', 'Z:/b', 'c'], ('Z:', '\\', ['Z:\\', 'b', 'c']))
+ # UNC paths
+ check(['a', '//b/c', 'd'], ('\\\\b\\c', '\\', ['\\\\b\\c\\', 'd']))
+ # Collapsing and stripping excess slashes
+ check(['a', 'Z://b//c/', 'd/'], ('Z:', '\\', ['Z:\\', 'b', 'c', 'd']))
+ # UNC paths
+ check(['a', '//b/c//', 'd'], ('\\\\b\\c', '\\', ['\\\\b\\c\\', 'd']))
+ # Extended paths
+ check(['//?/c:/'], ('\\\\?\\c:', '\\', ['\\\\?\\c:\\']))
+ check(['//?/c:/a'], ('\\\\?\\c:', '\\', ['\\\\?\\c:\\', 'a']))
+ check(['//?/c:/a', '/b'], ('\\\\?\\c:', '\\', ['\\\\?\\c:\\', 'b']))
+ # Extended UNC paths (format is "\\?\UNC\server\share")
+ check(['//?/UNC/b/c'],
+ ('\\\\?\\UNC\\b\\c', '\\', ['\\\\?\\UNC\\b\\c\\']))
+ check(['//?/UNC/b/c/d'],
+ ('\\\\?\\UNC\\b\\c', '\\', ['\\\\?\\UNC\\b\\c\\', 'd']))
+ # Second part has a root but not drive
+ check(['a', '/b', 'c'], ('', '\\', ['\\', 'b', 'c']))
+ check(['Z:/a', '/b', 'c'], ('Z:', '\\', ['Z:\\', 'b', 'c']))
+ check(['//?/Z:/a', '/b', 'c'],
+ ('\\\\?\\Z:', '\\', ['\\\\?\\Z:\\', 'b', 'c']))
+
+ def test_splitroot(self):
+ f = self.flavour.splitroot
+ self.assertEqual(f(''), ('', '', ''))
+ self.assertEqual(f('a'), ('', '', 'a'))
+ self.assertEqual(f('a\\b'), ('', '', 'a\\b'))
+ self.assertEqual(f('\\a'), ('', '\\', 'a'))
+ self.assertEqual(f('\\a\\b'), ('', '\\', 'a\\b'))
+ self.assertEqual(f('c:a\\b'), ('c:', '', 'a\\b'))
+ self.assertEqual(f('c:\\a\\b'), ('c:', '\\', 'a\\b'))
+ # Redundant slashes in the root are collapsed
+ self.assertEqual(f('\\\\a'), ('', '\\', 'a'))
+ self.assertEqual(f('\\\\\\a/b'), ('', '\\', 'a/b'))
+ self.assertEqual(f('c:\\\\a'), ('c:', '\\', 'a'))
+ self.assertEqual(f('c:\\\\\\a/b'), ('c:', '\\', 'a/b'))
+ # Valid UNC paths
+ self.assertEqual(f('\\\\a\\b'), ('\\\\a\\b', '\\', ''))
+ self.assertEqual(f('\\\\a\\b\\'), ('\\\\a\\b', '\\', ''))
+ self.assertEqual(f('\\\\a\\b\\c\\d'), ('\\\\a\\b', '\\', 'c\\d'))
+ # These are non-UNC paths (according to ntpath.py and test_ntpath)
+ # However, command.com says such paths are invalid, so it's
+ # difficult to know what the right semantics are
+ self.assertEqual(f('\\\\\\a\\b'), ('', '\\', 'a\\b'))
+ self.assertEqual(f('\\\\a'), ('', '\\', 'a'))
+
+
+#
+# Tests for the pure classes
+#
+
+with_fsencode = unittest.skipIf(
+ sys.version_info < (3, 2),
+ 'os.fsencode has been introduced in version 3.2')
+
+
+class _BasePurePathTest(object):
+
+ # keys are canonical paths, values are list of tuples of arguments
+ # supposed to produce equal paths
+ equivalences = {
+ 'a/b': [
+ ('a', 'b'), ('a/', 'b'), ('a', 'b/'), ('a/', 'b/'),
+ ('a/b/',), ('a//b',), ('a//b//',),
+ # empty components get removed
+ ('', 'a', 'b'), ('a', '', 'b'), ('a', 'b', ''),
+ ],
+ '/b/c/d': [
+ ('a', '/b/c', 'd'), ('a', '///b//c', 'd/'),
+ ('/a', '/b/c', 'd'),
+ # empty components get removed
+ ('/', 'b', '', 'c/d'), ('/', '', 'b/c/d'), ('', '/b/c/d'),
+ ],
+ }
+
+ def setUp(self):
+ p = self.cls('a')
+ self.flavour = p._flavour
+ self.sep = self.flavour.sep
+ self.altsep = self.flavour.altsep
+
+ def test_constructor_common(self):
+ P = self.cls
+ p = P('a')
+ self.assertIsInstance(p, P)
+
+ class PathLike:
+ def __fspath__(self):
+ return "a/b/c"
+
+ P('a', 'b', 'c')
+ P('/a', 'b', 'c')
+ P('a/b/c')
+ P('/a/b/c')
+ P(PathLike())
+ self.assertEqual(P(P('a')), P('a'))
+ self.assertEqual(P(P('a'), 'b'), P('a/b'))
+ self.assertEqual(P(P('a'), P('b')), P('a/b'))
+ self.assertEqual(P(P('a'), P('b'), P('c')), P(PathLike()))
+
+ def _check_str_subclass(self, *args):
+ # Issue #21127: it should be possible to construct a PurePath object
+ # from a str subclass instance, and it then gets converted to
+ # a pure str object.
+ class StrSubclass(str):
+ pass
+ P = self.cls
+ p = P(*(StrSubclass(x) for x in args))
+ self.assertEqual(p, P(*args))
+ for part in p.parts:
+ self.assertIs(type(part), str)
+
+ def test_str_subclass_common(self):
+ self._check_str_subclass('')
+ self._check_str_subclass('.')
+ self._check_str_subclass('a')
+ self._check_str_subclass('a/b.txt')
+ self._check_str_subclass('/a/b.txt')
+
+ def test_join_common(self):
+ P = self.cls
+ p = P('a/b')
+ pp = p.joinpath('c')
+ self.assertEqual(pp, P('a/b/c'))
+ self.assertIs(type(pp), type(p))
+ pp = p.joinpath('c', 'd')
+ self.assertEqual(pp, P('a/b/c/d'))
+ pp = p.joinpath(P('c'))
+ self.assertEqual(pp, P('a/b/c'))
+ pp = p.joinpath('/c')
+ self.assertEqual(pp, P('/c'))
+
+ def test_div_common(self):
+ # Basically the same as joinpath()
+ P = self.cls
+ p = P('a/b')
+ pp = p / 'c'
+ self.assertEqual(pp, P('a/b/c'))
+ self.assertIs(type(pp), type(p))
+ pp = p / 'c/d'
+ self.assertEqual(pp, P('a/b/c/d'))
+ pp = p / 'c' / 'd'
+ self.assertEqual(pp, P('a/b/c/d'))
+ pp = 'c' / p / 'd'
+ self.assertEqual(pp, P('c/a/b/d'))
+ pp = p / P('c')
+ self.assertEqual(pp, P('a/b/c'))
+ pp = p / '/c'
+ self.assertEqual(pp, P('/c'))
+
+ def _check_str(self, expected, args):
+ p = self.cls(*args)
+ self.assertEqual(str(p), expected.replace('/', self.sep))
+
+ def test_str_common(self):
+ # Canonicalized paths roundtrip
+ for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'):
+ self._check_str(pathstr, (pathstr,))
+ # Special case for the empty path
+ self._check_str('.', ('',))
+ # Other tests for str() are in test_equivalences()
+
+ def test_as_posix_common(self):
+ P = self.cls
+ for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'):
+ self.assertEqual(P(pathstr).as_posix(), pathstr)
+ # Other tests for as_posix() are in test_equivalences()
+
+ @with_fsencode
+ def test_as_bytes_common(self):
+ sep = os.fsencode(self.sep)
+ P = self.cls
+ self.assertEqual(bytes(P('a/b')), b'a' + sep + b'b')
+
+ def test_as_uri_common(self):
+ P = self.cls
+ with self.assertRaises(ValueError):
+ P('a').as_uri()
+ with self.assertRaises(ValueError):
+ P().as_uri()
+
+ def test_repr_common(self):
+ for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'):
+ p = self.cls(pathstr)
+ clsname = p.__class__.__name__
+ r = repr(p)
+ # The repr() is in the form ClassName("forward-slashes path")
+ self.assertTrue(r.startswith(clsname + '('), r)
+ self.assertTrue(r.endswith(')'), r)
+ inner = r[len(clsname) + 1: -1]
+ self.assertEqual(eval(inner), p.as_posix())
+ # The repr() roundtrips
+ q = eval(r, pathlib.__dict__)
+ self.assertIs(q.__class__, p.__class__)
+ self.assertEqual(q, p)
+ self.assertEqual(repr(q), r)
+
+ def test_eq_common(self):
+ P = self.cls
+ self.assertEqual(P('a/b'), P('a/b'))
+ self.assertEqual(P('a/b'), P('a', 'b'))
+ self.assertNotEqual(P('a/b'), P('a'))
+ self.assertNotEqual(P('a/b'), P('/a/b'))
+ self.assertNotEqual(P('a/b'), P())
+ self.assertNotEqual(P('/a/b'), P('/'))
+ self.assertNotEqual(P(), P('/'))
+ self.assertNotEqual(P(), "")
+ self.assertNotEqual(P(), {})
+ self.assertNotEqual(P(), int)
+
+ def test_match_common(self):
+ P = self.cls
+ self.assertRaises(ValueError, P('a').match, '')
+ self.assertRaises(ValueError, P('a').match, '.')
+ # Simple relative pattern
+ self.assertTrue(P('b.py').match('b.py'))
+ self.assertTrue(P('a/b.py').match('b.py'))
+ self.assertTrue(P('/a/b.py').match('b.py'))
+ self.assertFalse(P('a.py').match('b.py'))
+ self.assertFalse(P('b/py').match('b.py'))
+ self.assertFalse(P('/a.py').match('b.py'))
+ self.assertFalse(P('b.py/c').match('b.py'))
+ # Wilcard relative pattern
+ self.assertTrue(P('b.py').match('*.py'))
+ self.assertTrue(P('a/b.py').match('*.py'))
+ self.assertTrue(P('/a/b.py').match('*.py'))
+ self.assertFalse(P('b.pyc').match('*.py'))
+ self.assertFalse(P('b./py').match('*.py'))
+ self.assertFalse(P('b.py/c').match('*.py'))
+ # Multi-part relative pattern
+ self.assertTrue(P('ab/c.py').match('a*/*.py'))
+ self.assertTrue(P('/d/ab/c.py').match('a*/*.py'))
+ self.assertFalse(P('a.py').match('a*/*.py'))
+ self.assertFalse(P('/dab/c.py').match('a*/*.py'))
+ self.assertFalse(P('ab/c.py/d').match('a*/*.py'))
+ # Absolute pattern
+ self.assertTrue(P('/b.py').match('/*.py'))
+ self.assertFalse(P('b.py').match('/*.py'))
+ self.assertFalse(P('a/b.py').match('/*.py'))
+ self.assertFalse(P('/a/b.py').match('/*.py'))
+ # Multi-part absolute pattern
+ self.assertTrue(P('/a/b.py').match('/a/*.py'))
+ self.assertFalse(P('/ab.py').match('/a/*.py'))
+ self.assertFalse(P('/a/b/c.py').match('/a/*.py'))
+
+ def test_ordering_common(self):
+ # Ordering is tuple-alike
+ def assertLess(a, b):
+ self.assertLess(a, b)
+ self.assertGreater(b, a)
+ P = self.cls
+ a = P('a')
+ b = P('a/b')
+ c = P('abc')
+ d = P('b')
+ assertLess(a, b)
+ assertLess(a, c)
+ assertLess(a, d)
+ assertLess(b, c)
+ assertLess(c, d)
+ P = self.cls
+ a = P('/a')
+ b = P('/a/b')
+ c = P('/abc')
+ d = P('/b')
+ assertLess(a, b)
+ assertLess(a, c)
+ assertLess(a, d)
+ assertLess(b, c)
+ assertLess(c, d)
+ if sys.version_info > (3,):
+ with self.assertRaises(TypeError):
+ P() < {}
+ else:
+ P() < {}
+
+ def test_parts_common(self):
+ # `parts` returns a tuple
+ sep = self.sep
+ P = self.cls
+ p = P('a/b')
+ parts = p.parts
+ self.assertEqual(parts, ('a', 'b'))
+ # The object gets reused
+ self.assertIs(parts, p.parts)
+ # When the path is absolute, the anchor is a separate part
+ p = P('/a/b')
+ parts = p.parts
+ self.assertEqual(parts, (sep, 'a', 'b'))
+
+ def test_fspath_common(self):
+ P = self.cls
+ p = P('a/b')
+ self._check_str(p.__fspath__(), ('a/b',))
+ if sys.version_info >= (3, 6):
+ self._check_str(os.fspath(p), ('a/b',))
+
+ def test_equivalences(self):
+ for k, tuples in self.equivalences.items():
+ canon = k.replace('/', self.sep)
+ posix = k.replace(self.sep, '/')
+ if canon != posix:
+ tuples = tuples + [
+ tuple(part.replace('/', self.sep) for part in t)
+ for t in tuples
+ ]
+ tuples.append((posix, ))
+ pcanon = self.cls(canon)
+ for t in tuples:
+ p = self.cls(*t)
+ self.assertEqual(p, pcanon, "failed with args {0}".format(t))
+ self.assertEqual(hash(p), hash(pcanon))
+ self.assertEqual(str(p), canon)
+ self.assertEqual(p.as_posix(), posix)
+
+ def test_parent_common(self):
+ # Relative
+ P = self.cls
+ p = P('a/b/c')
+ self.assertEqual(p.parent, P('a/b'))
+ self.assertEqual(p.parent.parent, P('a'))
+ self.assertEqual(p.parent.parent.parent, P())
+ self.assertEqual(p.parent.parent.parent.parent, P())
+ # Anchored
+ p = P('/a/b/c')
+ self.assertEqual(p.parent, P('/a/b'))
+ self.assertEqual(p.parent.parent, P('/a'))
+ self.assertEqual(p.parent.parent.parent, P('/'))
+ self.assertEqual(p.parent.parent.parent.parent, P('/'))
+
+ def test_parents_common(self):
+ # Relative
+ P = self.cls
+ p = P('a/b/c')
+ par = p.parents
+ self.assertEqual(len(par), 3)
+ self.assertEqual(par[0], P('a/b'))
+ self.assertEqual(par[1], P('a'))
+ self.assertEqual(par[2], P('.'))
+ self.assertEqual(list(par), [P('a/b'), P('a'), P('.')])
+ with self.assertRaises(IndexError):
+ par[-1]
+ with self.assertRaises(IndexError):
+ par[3]
+ with self.assertRaises(TypeError):
+ par[0] = p
+ # Anchored
+ p = P('/a/b/c')
+ par = p.parents
+ self.assertEqual(len(par), 3)
+ self.assertEqual(par[0], P('/a/b'))
+ self.assertEqual(par[1], P('/a'))
+ self.assertEqual(par[2], P('/'))
+ self.assertEqual(list(par), [P('/a/b'), P('/a'), P('/')])
+ with self.assertRaises(IndexError):
+ par[3]
+
+ def test_drive_common(self):
+ P = self.cls
+ self.assertEqual(P('a/b').drive, '')
+ self.assertEqual(P('/a/b').drive, '')
+ self.assertEqual(P('').drive, '')
+
+ def test_root_common(self):
+ P = self.cls
+ sep = self.sep
+ self.assertEqual(P('').root, '')
+ self.assertEqual(P('a/b').root, '')
+ self.assertEqual(P('/').root, sep)
+ self.assertEqual(P('/a/b').root, sep)
+
+ def test_anchor_common(self):
+ P = self.cls
+ sep = self.sep
+ self.assertEqual(P('').anchor, '')
+ self.assertEqual(P('a/b').anchor, '')
+ self.assertEqual(P('/').anchor, sep)
+ self.assertEqual(P('/a/b').anchor, sep)
+
+ def test_name_common(self):
+ P = self.cls
+ self.assertEqual(P('').name, '')
+ self.assertEqual(P('.').name, '')
+ self.assertEqual(P('/').name, '')
+ self.assertEqual(P('a/b').name, 'b')
+ self.assertEqual(P('/a/b').name, 'b')
+ self.assertEqual(P('/a/b/.').name, 'b')
+ self.assertEqual(P('a/b.py').name, 'b.py')
+ self.assertEqual(P('/a/b.py').name, 'b.py')
+
+ def test_suffix_common(self):
+ P = self.cls
+ self.assertEqual(P('').suffix, '')
+ self.assertEqual(P('.').suffix, '')
+ self.assertEqual(P('..').suffix, '')
+ self.assertEqual(P('/').suffix, '')
+ self.assertEqual(P('a/b').suffix, '')
+ self.assertEqual(P('/a/b').suffix, '')
+ self.assertEqual(P('/a/b/.').suffix, '')
+ self.assertEqual(P('a/b.py').suffix, '.py')
+ self.assertEqual(P('/a/b.py').suffix, '.py')
+ self.assertEqual(P('a/.hgrc').suffix, '')
+ self.assertEqual(P('/a/.hgrc').suffix, '')
+ self.assertEqual(P('a/.hg.rc').suffix, '.rc')
+ self.assertEqual(P('/a/.hg.rc').suffix, '.rc')
+ self.assertEqual(P('a/b.tar.gz').suffix, '.gz')
+ self.assertEqual(P('/a/b.tar.gz').suffix, '.gz')
+ self.assertEqual(P('a/Some name. Ending with a dot.').suffix, '')
+ self.assertEqual(P('/a/Some name. Ending with a dot.').suffix, '')
+
+ def test_suffixes_common(self):
+ P = self.cls
+ self.assertEqual(P('').suffixes, [])
+ self.assertEqual(P('.').suffixes, [])
+ self.assertEqual(P('/').suffixes, [])
+ self.assertEqual(P('a/b').suffixes, [])
+ self.assertEqual(P('/a/b').suffixes, [])
+ self.assertEqual(P('/a/b/.').suffixes, [])
+ self.assertEqual(P('a/b.py').suffixes, ['.py'])
+ self.assertEqual(P('/a/b.py').suffixes, ['.py'])
+ self.assertEqual(P('a/.hgrc').suffixes, [])
+ self.assertEqual(P('/a/.hgrc').suffixes, [])
+ self.assertEqual(P('a/.hg.rc').suffixes, ['.rc'])
+ self.assertEqual(P('/a/.hg.rc').suffixes, ['.rc'])
+ self.assertEqual(P('a/b.tar.gz').suffixes, ['.tar', '.gz'])
+ self.assertEqual(P('/a/b.tar.gz').suffixes, ['.tar', '.gz'])
+ self.assertEqual(P('a/Some name. Ending with a dot.').suffixes, [])
+ self.assertEqual(P('/a/Some name. Ending with a dot.').suffixes, [])
+
+ def test_stem_common(self):
+ P = self.cls
+ self.assertEqual(P('').stem, '')
+ self.assertEqual(P('.').stem, '')
+ self.assertEqual(P('..').stem, '..')
+ self.assertEqual(P('/').stem, '')
+ self.assertEqual(P('a/b').stem, 'b')
+ self.assertEqual(P('a/b.py').stem, 'b')
+ self.assertEqual(P('a/.hgrc').stem, '.hgrc')
+ self.assertEqual(P('a/.hg.rc').stem, '.hg')
+ self.assertEqual(P('a/b.tar.gz').stem, 'b.tar')
+ self.assertEqual(P('a/Some name. Ending with a dot.').stem,
+ 'Some name. Ending with a dot.')
+
+ def test_with_name_common(self):
+ P = self.cls
+ self.assertEqual(P('a/b').with_name('d.xml'), P('a/d.xml'))
+ self.assertEqual(P('/a/b').with_name('d.xml'), P('/a/d.xml'))
+ self.assertEqual(P('a/b.py').with_name('d.xml'), P('a/d.xml'))
+ self.assertEqual(P('/a/b.py').with_name('d.xml'), P('/a/d.xml'))
+ self.assertEqual(P('a/Dot ending.').with_name('d.xml'), P('a/d.xml'))
+ self.assertEqual(P('/a/Dot ending.').with_name('d.xml'), P('/a/d.xml'))
+ self.assertRaises(ValueError, P('').with_name, 'd.xml')
+ self.assertRaises(ValueError, P('.').with_name, 'd.xml')
+ self.assertRaises(ValueError, P('/').with_name, 'd.xml')
+ self.assertRaises(ValueError, P('a/b').with_name, '')
+ self.assertRaises(ValueError, P('a/b').with_name, '/c')
+ self.assertRaises(ValueError, P('a/b').with_name, 'c/')
+ self.assertRaises(ValueError, P('a/b').with_name, 'c/d')
+
+ def test_with_suffix_common(self):
+ P = self.cls
+ self.assertEqual(P('a/b').with_suffix('.gz'), P('a/b.gz'))
+ self.assertEqual(P('/a/b').with_suffix('.gz'), P('/a/b.gz'))
+ self.assertEqual(P('a/b.py').with_suffix('.gz'), P('a/b.gz'))
+ self.assertEqual(P('/a/b.py').with_suffix('.gz'), P('/a/b.gz'))
+ # Stripping suffix
+ self.assertEqual(P('a/b.py').with_suffix(''), P('a/b'))
+ self.assertEqual(P('/a/b').with_suffix(''), P('/a/b'))
+ # Path doesn't have a "filename" component
+ self.assertRaises(ValueError, P('').with_suffix, '.gz')
+ self.assertRaises(ValueError, P('.').with_suffix, '.gz')
+ self.assertRaises(ValueError, P('/').with_suffix, '.gz')
+ # Invalid suffix
+ self.assertRaises(ValueError, P('a/b').with_suffix, 'gz')
+ self.assertRaises(ValueError, P('a/b').with_suffix, '/')
+ self.assertRaises(ValueError, P('a/b').with_suffix, '.')
+ self.assertRaises(ValueError, P('a/b').with_suffix, '/.gz')
+ self.assertRaises(ValueError, P('a/b').with_suffix, 'c/d')
+ self.assertRaises(ValueError, P('a/b').with_suffix, '.c/.d')
+ self.assertRaises(ValueError, P('a/b').with_suffix, './.d')
+ self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.')
+
+ def test_relative_to_common(self):
+ P = self.cls
+ p = P('a/b')
+ self.assertRaises(TypeError, p.relative_to)
+ if six.PY3:
+ self.assertRaises(TypeError, p.relative_to, b'a')
+ self.assertEqual(p.relative_to(P()), P('a/b'))
+ self.assertEqual(p.relative_to(''), P('a/b'))
+ self.assertEqual(p.relative_to(P('a')), P('b'))
+ self.assertEqual(p.relative_to('a'), P('b'))
+ self.assertEqual(p.relative_to('a/'), P('b'))
+ self.assertEqual(p.relative_to(P('a/b')), P())
+ self.assertEqual(p.relative_to('a/b'), P())
+ # With several args
+ self.assertEqual(p.relative_to('a', 'b'), P())
+ # Unrelated paths
+ self.assertRaises(ValueError, p.relative_to, P('c'))
+ self.assertRaises(ValueError, p.relative_to, P('a/b/c'))
+ self.assertRaises(ValueError, p.relative_to, P('a/c'))
+ self.assertRaises(ValueError, p.relative_to, P('/a'))
+ p = P('/a/b')
+ self.assertEqual(p.relative_to(P('/')), P('a/b'))
+ self.assertEqual(p.relative_to('/'), P('a/b'))
+ self.assertEqual(p.relative_to(P('/a')), P('b'))
+ self.assertEqual(p.relative_to('/a'), P('b'))
+ self.assertEqual(p.relative_to('/a/'), P('b'))
+ self.assertEqual(p.relative_to(P('/a/b')), P())
+ self.assertEqual(p.relative_to('/a/b'), P())
+ # Unrelated paths
+ self.assertRaises(ValueError, p.relative_to, P('/c'))
+ self.assertRaises(ValueError, p.relative_to, P('/a/b/c'))
+ self.assertRaises(ValueError, p.relative_to, P('/a/c'))
+ self.assertRaises(ValueError, p.relative_to, P())
+ self.assertRaises(ValueError, p.relative_to, '')
+ self.assertRaises(ValueError, p.relative_to, P('a'))
+
+ def test_pickling_common(self):
+ P = self.cls
+ p = P('/a/b')
+ for proto in range(0, pickle.HIGHEST_PROTOCOL + 1):
+ dumped = pickle.dumps(p, proto)
+ pp = pickle.loads(dumped)
+ self.assertIs(pp.__class__, p.__class__)
+ self.assertEqual(pp, p)
+ self.assertEqual(hash(pp), hash(p))
+ self.assertEqual(str(pp), str(p))
+
+ # note: this is a new test not part of upstream
+ # test that unicode works on Python 2
+ @unittest.skipIf(
+ six.unichr(0x0100).encode(
+ sys.getfilesystemencoding(), "replace") == b"?",
+ "file system encoding only supports ascii")
+ def test_unicode(self):
+ self.cls(six.unichr(0x0100))
+
+
+class PurePosixPathTest(_BasePurePathTest, unittest.TestCase):
+ cls = pathlib.PurePosixPath
+
+ def test_root(self):
+ P = self.cls
+ self.assertEqual(P('/a/b').root, '/')
+ self.assertEqual(P('///a/b').root, '/')
+ # POSIX special case for two leading slashes
+ self.assertEqual(P('//a/b').root, '//')
+
+ def test_eq(self):
+ P = self.cls
+ self.assertNotEqual(P('a/b'), P('A/b'))
+ self.assertEqual(P('/a'), P('///a'))
+ self.assertNotEqual(P('/a'), P('//a'))
+
+ def test_as_uri(self):
+ P = self.cls
+ self.assertEqual(P('/').as_uri(), 'file:///')
+ self.assertEqual(P('/a/b.c').as_uri(), 'file:///a/b.c')
+ self.assertEqual(P('/a/b%#c').as_uri(), 'file:///a/b%25%23c')
+
+ @with_fsencode
+ def test_as_uri_non_ascii(self):
+ from urllib.parse import quote_from_bytes
+ P = self.cls
+ try:
+ os.fsencode('\xe9')
+ except UnicodeEncodeError:
+ self.skipTest("\\xe9 cannot be encoded to the filesystem encoding")
+ self.assertEqual(P('/a/b\xe9').as_uri(),
+ 'file:///a/b' + quote_from_bytes(os.fsencode('\xe9')))
+
+ def test_match(self):
+ P = self.cls
+ self.assertFalse(P('A.py').match('a.PY'))
+
+ def test_is_absolute(self):
+ P = self.cls
+ self.assertFalse(P().is_absolute())
+ self.assertFalse(P('a').is_absolute())
+ self.assertFalse(P('a/b/').is_absolute())
+ self.assertTrue(P('/').is_absolute())
+ self.assertTrue(P('/a').is_absolute())
+ self.assertTrue(P('/a/b/').is_absolute())
+ self.assertTrue(P('//a').is_absolute())
+ self.assertTrue(P('//a/b').is_absolute())
+
+ def test_is_reserved(self):
+ P = self.cls
+ self.assertIs(False, P('').is_reserved())
+ self.assertIs(False, P('/').is_reserved())
+ self.assertIs(False, P('/foo/bar').is_reserved())
+ self.assertIs(False, P('/dev/con/PRN/NUL').is_reserved())
+
+ def test_join(self):
+ P = self.cls
+ p = P('//a')
+ pp = p.joinpath('b')
+ self.assertEqual(pp, P('//a/b'))
+ pp = P('/a').joinpath('//c')
+ self.assertEqual(pp, P('//c'))
+ pp = P('//a').joinpath('/c')
+ self.assertEqual(pp, P('/c'))
+
+ def test_div(self):
+ # Basically the same as joinpath()
+ P = self.cls
+ p = P('//a')
+ pp = p / 'b'
+ self.assertEqual(pp, P('//a/b'))
+ pp = P('/a') / '//c'
+ self.assertEqual(pp, P('//c'))
+ pp = P('//a') / '/c'
+ self.assertEqual(pp, P('/c'))
+
+
+class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase):
+ cls = pathlib.PureWindowsPath
+
+ equivalences = _BasePurePathTest.equivalences.copy()
+ equivalences.update({
+ 'c:a': [('c:', 'a'), ('c:', 'a/'), ('/', 'c:', 'a')],
+ 'c:/a': [
+ ('c:/', 'a'), ('c:', '/', 'a'), ('c:', '/a'),
+ ('/z', 'c:/', 'a'), ('//x/y', 'c:/', 'a'),
+ ],
+ '//a/b/': [('//a/b',)],
+ '//a/b/c': [
+ ('//a/b', 'c'), ('//a/b/', 'c'),
+ ],
+ })
+
+ def test_str(self):
+ p = self.cls('a/b/c')
+ self.assertEqual(str(p), 'a\\b\\c')
+ p = self.cls('c:/a/b/c')
+ self.assertEqual(str(p), 'c:\\a\\b\\c')
+ p = self.cls('//a/b')
+ self.assertEqual(str(p), '\\\\a\\b\\')
+ p = self.cls('//a/b/c')
+ self.assertEqual(str(p), '\\\\a\\b\\c')
+ p = self.cls('//a/b/c/d')
+ self.assertEqual(str(p), '\\\\a\\b\\c\\d')
+
+ def test_str_subclass(self):
+ self._check_str_subclass('c:')
+ self._check_str_subclass('c:a')
+ self._check_str_subclass('c:a\\b.txt')
+ self._check_str_subclass('c:\\')
+ self._check_str_subclass('c:\\a')
+ self._check_str_subclass('c:\\a\\b.txt')
+ self._check_str_subclass('\\\\some\\share')
+ self._check_str_subclass('\\\\some\\share\\a')
+ self._check_str_subclass('\\\\some\\share\\a\\b.txt')
+
+ def test_eq(self):
+ P = self.cls
+ self.assertEqual(P('c:a/b'), P('c:a/b'))
+ self.assertEqual(P('c:a/b'), P('c:', 'a', 'b'))
+ self.assertNotEqual(P('c:a/b'), P('d:a/b'))
+ self.assertNotEqual(P('c:a/b'), P('c:/a/b'))
+ self.assertNotEqual(P('/a/b'), P('c:/a/b'))
+ # Case-insensitivity
+ self.assertEqual(P('a/B'), P('A/b'))
+ self.assertEqual(P('C:a/B'), P('c:A/b'))
+ self.assertEqual(P('//Some/SHARE/a/B'), P('//somE/share/A/b'))
+
+ @with_fsencode
+ def test_as_uri(self):
+ P = self.cls
+ with self.assertRaises(ValueError):
+ P('/a/b').as_uri()
+ with self.assertRaises(ValueError):
+ P('c:a/b').as_uri()
+ self.assertEqual(P('c:/').as_uri(), 'file:///c:/')
+ self.assertEqual(P('c:/a/b.c').as_uri(), 'file:///c:/a/b.c')
+ self.assertEqual(P('c:/a/b%#c').as_uri(), 'file:///c:/a/b%25%23c')
+ self.assertEqual(P('c:/a/b\xe9').as_uri(), 'file:///c:/a/b%C3%A9')
+ self.assertEqual(P('//some/share/').as_uri(), 'file://some/share/')
+ self.assertEqual(P('//some/share/a/b.c').as_uri(),
+ 'file://some/share/a/b.c')
+ self.assertEqual(P('//some/share/a/b%#c\xe9').as_uri(),
+ 'file://some/share/a/b%25%23c%C3%A9')
+
+ def test_match_common(self):
+ P = self.cls
+ # Absolute patterns
+ self.assertTrue(P('c:/b.py').match('/*.py'))
+ self.assertTrue(P('c:/b.py').match('c:*.py'))
+ self.assertTrue(P('c:/b.py').match('c:/*.py'))
+ self.assertFalse(P('d:/b.py').match('c:/*.py')) # wrong drive
+ self.assertFalse(P('b.py').match('/*.py'))
+ self.assertFalse(P('b.py').match('c:*.py'))
+ self.assertFalse(P('b.py').match('c:/*.py'))
+ self.assertFalse(P('c:b.py').match('/*.py'))
+ self.assertFalse(P('c:b.py').match('c:/*.py'))
+ self.assertFalse(P('/b.py').match('c:*.py'))
+ self.assertFalse(P('/b.py').match('c:/*.py'))
+ # UNC patterns
+ self.assertTrue(P('//some/share/a.py').match('/*.py'))
+ self.assertTrue(P('//some/share/a.py').match('//some/share/*.py'))
+ self.assertFalse(P('//other/share/a.py').match('//some/share/*.py'))
+ self.assertFalse(P('//some/share/a/b.py').match('//some/share/*.py'))
+ # Case-insensitivity
+ self.assertTrue(P('B.py').match('b.PY'))
+ self.assertTrue(P('c:/a/B.Py').match('C:/A/*.pY'))
+ self.assertTrue(P('//Some/Share/B.Py').match('//somE/sharE/*.pY'))
+
+ def test_ordering_common(self):
+ # Case-insensitivity
+ def assertOrderedEqual(a, b):
+ self.assertLessEqual(a, b)
+ self.assertGreaterEqual(b, a)
+ P = self.cls
+ p = P('c:A/b')
+ q = P('C:a/B')
+ assertOrderedEqual(p, q)
+ self.assertFalse(p < q)
+ self.assertFalse(p > q)
+ p = P('//some/Share/A/b')
+ q = P('//Some/SHARE/a/B')
+ assertOrderedEqual(p, q)
+ self.assertFalse(p < q)
+ self.assertFalse(p > q)
+
+ def test_parts(self):
+ P = self.cls
+ p = P('c:a/b')
+ parts = p.parts
+ self.assertEqual(parts, ('c:', 'a', 'b'))
+ p = P('c:/a/b')
+ parts = p.parts
+ self.assertEqual(parts, ('c:\\', 'a', 'b'))
+ p = P('//a/b/c/d')
+ parts = p.parts
+ self.assertEqual(parts, ('\\\\a\\b\\', 'c', 'd'))
+
+ def test_parent(self):
+ # Anchored
+ P = self.cls
+ p = P('z:a/b/c')
+ self.assertEqual(p.parent, P('z:a/b'))
+ self.assertEqual(p.parent.parent, P('z:a'))
+ self.assertEqual(p.parent.parent.parent, P('z:'))
+ self.assertEqual(p.parent.parent.parent.parent, P('z:'))
+ p = P('z:/a/b/c')
+ self.assertEqual(p.parent, P('z:/a/b'))
+ self.assertEqual(p.parent.parent, P('z:/a'))
+ self.assertEqual(p.parent.parent.parent, P('z:/'))
+ self.assertEqual(p.parent.parent.parent.parent, P('z:/'))
+ p = P('//a/b/c/d')
+ self.assertEqual(p.parent, P('//a/b/c'))
+ self.assertEqual(p.parent.parent, P('//a/b'))
+ self.assertEqual(p.parent.parent.parent, P('//a/b'))
+
+ def test_parents(self):
+ # Anchored
+ P = self.cls
+ p = P('z:a/b/')
+ par = p.parents
+ self.assertEqual(len(par), 2)
+ self.assertEqual(par[0], P('z:a'))
+ self.assertEqual(par[1], P('z:'))
+ self.assertEqual(list(par), [P('z:a'), P('z:')])
+ with self.assertRaises(IndexError):
+ par[2]
+ p = P('z:/a/b/')
+ par = p.parents
+ self.assertEqual(len(par), 2)
+ self.assertEqual(par[0], P('z:/a'))
+ self.assertEqual(par[1], P('z:/'))
+ self.assertEqual(list(par), [P('z:/a'), P('z:/')])
+ with self.assertRaises(IndexError):
+ par[2]
+ p = P('//a/b/c/d')
+ par = p.parents
+ self.assertEqual(len(par), 2)
+ self.assertEqual(par[0], P('//a/b/c'))
+ self.assertEqual(par[1], P('//a/b'))
+ self.assertEqual(list(par), [P('//a/b/c'), P('//a/b')])
+ with self.assertRaises(IndexError):
+ par[2]
+
+ def test_drive(self):
+ P = self.cls
+ self.assertEqual(P('c:').drive, 'c:')
+ self.assertEqual(P('c:a/b').drive, 'c:')
+ self.assertEqual(P('c:/').drive, 'c:')
+ self.assertEqual(P('c:/a/b/').drive, 'c:')
+ self.assertEqual(P('//a/b').drive, '\\\\a\\b')
+ self.assertEqual(P('//a/b/').drive, '\\\\a\\b')
+ self.assertEqual(P('//a/b/c/d').drive, '\\\\a\\b')
+
+ def test_root(self):
+ P = self.cls
+ self.assertEqual(P('c:').root, '')
+ self.assertEqual(P('c:a/b').root, '')
+ self.assertEqual(P('c:/').root, '\\')
+ self.assertEqual(P('c:/a/b/').root, '\\')
+ self.assertEqual(P('//a/b').root, '\\')
+ self.assertEqual(P('//a/b/').root, '\\')
+ self.assertEqual(P('//a/b/c/d').root, '\\')
+
+ def test_anchor(self):
+ P = self.cls
+ self.assertEqual(P('c:').anchor, 'c:')
+ self.assertEqual(P('c:a/b').anchor, 'c:')
+ self.assertEqual(P('c:/').anchor, 'c:\\')
+ self.assertEqual(P('c:/a/b/').anchor, 'c:\\')
+ self.assertEqual(P('//a/b').anchor, '\\\\a\\b\\')
+ self.assertEqual(P('//a/b/').anchor, '\\\\a\\b\\')
+ self.assertEqual(P('//a/b/c/d').anchor, '\\\\a\\b\\')
+
+ def test_name(self):
+ P = self.cls
+ self.assertEqual(P('c:').name, '')
+ self.assertEqual(P('c:/').name, '')
+ self.assertEqual(P('c:a/b').name, 'b')
+ self.assertEqual(P('c:/a/b').name, 'b')
+ self.assertEqual(P('c:a/b.py').name, 'b.py')
+ self.assertEqual(P('c:/a/b.py').name, 'b.py')
+ self.assertEqual(P('//My.py/Share.php').name, '')
+ self.assertEqual(P('//My.py/Share.php/a/b').name, 'b')
+
+ def test_suffix(self):
+ P = self.cls
+ self.assertEqual(P('c:').suffix, '')
+ self.assertEqual(P('c:/').suffix, '')
+ self.assertEqual(P('c:a/b').suffix, '')
+ self.assertEqual(P('c:/a/b').suffix, '')
+ self.assertEqual(P('c:a/b.py').suffix, '.py')
+ self.assertEqual(P('c:/a/b.py').suffix, '.py')
+ self.assertEqual(P('c:a/.hgrc').suffix, '')
+ self.assertEqual(P('c:/a/.hgrc').suffix, '')
+ self.assertEqual(P('c:a/.hg.rc').suffix, '.rc')
+ self.assertEqual(P('c:/a/.hg.rc').suffix, '.rc')
+ self.assertEqual(P('c:a/b.tar.gz').suffix, '.gz')
+ self.assertEqual(P('c:/a/b.tar.gz').suffix, '.gz')
+ self.assertEqual(P('c:a/Some name. Ending with a dot.').suffix, '')
+ self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffix, '')
+ self.assertEqual(P('//My.py/Share.php').suffix, '')
+ self.assertEqual(P('//My.py/Share.php/a/b').suffix, '')
+
+ def test_suffixes(self):
+ P = self.cls
+ self.assertEqual(P('c:').suffixes, [])
+ self.assertEqual(P('c:/').suffixes, [])
+ self.assertEqual(P('c:a/b').suffixes, [])
+ self.assertEqual(P('c:/a/b').suffixes, [])
+ self.assertEqual(P('c:a/b.py').suffixes, ['.py'])
+ self.assertEqual(P('c:/a/b.py').suffixes, ['.py'])
+ self.assertEqual(P('c:a/.hgrc').suffixes, [])
+ self.assertEqual(P('c:/a/.hgrc').suffixes, [])
+ self.assertEqual(P('c:a/.hg.rc').suffixes, ['.rc'])
+ self.assertEqual(P('c:/a/.hg.rc').suffixes, ['.rc'])
+ self.assertEqual(P('c:a/b.tar.gz').suffixes, ['.tar', '.gz'])
+ self.assertEqual(P('c:/a/b.tar.gz').suffixes, ['.tar', '.gz'])
+ self.assertEqual(P('//My.py/Share.php').suffixes, [])
+ self.assertEqual(P('//My.py/Share.php/a/b').suffixes, [])
+ self.assertEqual(P('c:a/Some name. Ending with a dot.').suffixes, [])
+ self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffixes, [])
+
+ def test_stem(self):
+ P = self.cls
+ self.assertEqual(P('c:').stem, '')
+ self.assertEqual(P('c:.').stem, '')
+ self.assertEqual(P('c:..').stem, '..')
+ self.assertEqual(P('c:/').stem, '')
+ self.assertEqual(P('c:a/b').stem, 'b')
+ self.assertEqual(P('c:a/b.py').stem, 'b')
+ self.assertEqual(P('c:a/.hgrc').stem, '.hgrc')
+ self.assertEqual(P('c:a/.hg.rc').stem, '.hg')
+ self.assertEqual(P('c:a/b.tar.gz').stem, 'b.tar')
+ self.assertEqual(P('c:a/Some name. Ending with a dot.').stem,
+ 'Some name. Ending with a dot.')
+
+ def test_with_name(self):
+ P = self.cls
+ self.assertEqual(P('c:a/b').with_name('d.xml'), P('c:a/d.xml'))
+ self.assertEqual(P('c:/a/b').with_name('d.xml'), P('c:/a/d.xml'))
+ self.assertEqual(
+ P('c:a/Dot ending.').with_name('d.xml'), P('c:a/d.xml'))
+ self.assertEqual(
+ P('c:/a/Dot ending.').with_name('d.xml'), P('c:/a/d.xml'))
+ self.assertRaises(ValueError, P('c:').with_name, 'd.xml')
+ self.assertRaises(ValueError, P('c:/').with_name, 'd.xml')
+ self.assertRaises(ValueError, P('//My/Share').with_name, 'd.xml')
+ self.assertRaises(ValueError, P('c:a/b').with_name, 'd:')
+ self.assertRaises(ValueError, P('c:a/b').with_name, 'd:e')
+ self.assertRaises(ValueError, P('c:a/b').with_name, 'd:/e')
+ self.assertRaises(ValueError, P('c:a/b').with_name, '//My/Share')
+
+ def test_with_suffix(self):
+ P = self.cls
+ self.assertEqual(P('c:a/b').with_suffix('.gz'), P('c:a/b.gz'))
+ self.assertEqual(P('c:/a/b').with_suffix('.gz'), P('c:/a/b.gz'))
+ self.assertEqual(P('c:a/b.py').with_suffix('.gz'), P('c:a/b.gz'))
+ self.assertEqual(P('c:/a/b.py').with_suffix('.gz'), P('c:/a/b.gz'))
+ # Path doesn't have a "filename" component
+ self.assertRaises(ValueError, P('').with_suffix, '.gz')
+ self.assertRaises(ValueError, P('.').with_suffix, '.gz')
+ self.assertRaises(ValueError, P('/').with_suffix, '.gz')
+ self.assertRaises(ValueError, P('//My/Share').with_suffix, '.gz')
+ # Invalid suffix
+ self.assertRaises(ValueError, P('c:a/b').with_suffix, 'gz')
+ self.assertRaises(ValueError, P('c:a/b').with_suffix, '/')
+ self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\')
+ self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:')
+ self.assertRaises(ValueError, P('c:a/b').with_suffix, '/.gz')
+ self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\.gz')
+ self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:.gz')
+ self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c/d')
+ self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c\\d')
+ self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c/d')
+ self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c\\d')
+
+ def test_relative_to(self):
+ P = self.cls
+ p = P('C:Foo/Bar')
+ self.assertEqual(p.relative_to(P('c:')), P('Foo/Bar'))
+ self.assertEqual(p.relative_to('c:'), P('Foo/Bar'))
+ self.assertEqual(p.relative_to(P('c:foO')), P('Bar'))
+ self.assertEqual(p.relative_to('c:foO'), P('Bar'))
+ self.assertEqual(p.relative_to('c:foO/'), P('Bar'))
+ self.assertEqual(p.relative_to(P('c:foO/baR')), P())
+ self.assertEqual(p.relative_to('c:foO/baR'), P())
+ # Unrelated paths
+ self.assertRaises(ValueError, p.relative_to, P())
+ self.assertRaises(ValueError, p.relative_to, '')
+ self.assertRaises(ValueError, p.relative_to, P('d:'))
+ self.assertRaises(ValueError, p.relative_to, P('/'))
+ self.assertRaises(ValueError, p.relative_to, P('Foo'))
+ self.assertRaises(ValueError, p.relative_to, P('/Foo'))
+ self.assertRaises(ValueError, p.relative_to, P('C:/Foo'))
+ self.assertRaises(ValueError, p.relative_to, P('C:Foo/Bar/Baz'))
+ self.assertRaises(ValueError, p.relative_to, P('C:Foo/Baz'))
+ p = P('C:/Foo/Bar')
+ self.assertEqual(p.relative_to(P('c:')), P('/Foo/Bar'))
+ self.assertEqual(p.relative_to('c:'), P('/Foo/Bar'))
+ self.assertEqual(str(p.relative_to(P('c:'))), '\\Foo\\Bar')
+ self.assertEqual(str(p.relative_to('c:')), '\\Foo\\Bar')
+ self.assertEqual(p.relative_to(P('c:/')), P('Foo/Bar'))
+ self.assertEqual(p.relative_to('c:/'), P('Foo/Bar'))
+ self.assertEqual(p.relative_to(P('c:/foO')), P('Bar'))
+ self.assertEqual(p.relative_to('c:/foO'), P('Bar'))
+ self.assertEqual(p.relative_to('c:/foO/'), P('Bar'))
+ self.assertEqual(p.relative_to(P('c:/foO/baR')), P())
+ self.assertEqual(p.relative_to('c:/foO/baR'), P())
+ # Unrelated paths
+ self.assertRaises(ValueError, p.relative_to, P('C:/Baz'))
+ self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Bar/Baz'))
+ self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Baz'))
+ self.assertRaises(ValueError, p.relative_to, P('C:Foo'))
+ self.assertRaises(ValueError, p.relative_to, P('d:'))
+ self.assertRaises(ValueError, p.relative_to, P('d:/'))
+ self.assertRaises(ValueError, p.relative_to, P('/'))
+ self.assertRaises(ValueError, p.relative_to, P('/Foo'))
+ self.assertRaises(ValueError, p.relative_to, P('//C/Foo'))
+ # UNC paths
+ p = P('//Server/Share/Foo/Bar')
+ self.assertEqual(p.relative_to(P('//sErver/sHare')), P('Foo/Bar'))
+ self.assertEqual(p.relative_to('//sErver/sHare'), P('Foo/Bar'))
+ self.assertEqual(p.relative_to('//sErver/sHare/'), P('Foo/Bar'))
+ self.assertEqual(p.relative_to(P('//sErver/sHare/Foo')), P('Bar'))
+ self.assertEqual(p.relative_to('//sErver/sHare/Foo'), P('Bar'))
+ self.assertEqual(p.relative_to('//sErver/sHare/Foo/'), P('Bar'))
+ self.assertEqual(p.relative_to(P('//sErver/sHare/Foo/Bar')), P())
+ self.assertEqual(p.relative_to('//sErver/sHare/Foo/Bar'), P())
+ # Unrelated paths
+ self.assertRaises(ValueError, p.relative_to, P('/Server/Share/Foo'))
+ self.assertRaises(ValueError, p.relative_to, P('c:/Server/Share/Foo'))
+ self.assertRaises(ValueError, p.relative_to, P('//z/Share/Foo'))
+ self.assertRaises(ValueError, p.relative_to, P('//Server/z/Foo'))
+
+ def test_is_absolute(self):
+ P = self.cls
+ # Under NT, only paths with both a drive and a root are absolute
+ self.assertFalse(P().is_absolute())
+ self.assertFalse(P('a').is_absolute())
+ self.assertFalse(P('a/b/').is_absolute())
+ self.assertFalse(P('/').is_absolute())
+ self.assertFalse(P('/a').is_absolute())
+ self.assertFalse(P('/a/b/').is_absolute())
+ self.assertFalse(P('c:').is_absolute())
+ self.assertFalse(P('c:a').is_absolute())
+ self.assertFalse(P('c:a/b/').is_absolute())
+ self.assertTrue(P('c:/').is_absolute())
+ self.assertTrue(P('c:/a').is_absolute())
+ self.assertTrue(P('c:/a/b/').is_absolute())
+ # UNC paths are absolute by definition
+ self.assertTrue(P('//a/b').is_absolute())
+ self.assertTrue(P('//a/b/').is_absolute())
+ self.assertTrue(P('//a/b/c').is_absolute())
+ self.assertTrue(P('//a/b/c/d').is_absolute())
+
+ def test_join(self):
+ P = self.cls
+ p = P('C:/a/b')
+ pp = p.joinpath('x/y')
+ self.assertEqual(pp, P('C:/a/b/x/y'))
+ pp = p.joinpath('/x/y')
+ self.assertEqual(pp, P('C:/x/y'))
+ # Joining with a different drive => the first path is ignored, even
+ # if the second path is relative.
+ pp = p.joinpath('D:x/y')
+ self.assertEqual(pp, P('D:x/y'))
+ pp = p.joinpath('D:/x/y')
+ self.assertEqual(pp, P('D:/x/y'))
+ pp = p.joinpath('//host/share/x/y')
+ self.assertEqual(pp, P('//host/share/x/y'))
+ # Joining with the same drive => the first path is appended to if
+ # the second path is relative.
+ pp = p.joinpath('c:x/y')
+ self.assertEqual(pp, P('C:/a/b/x/y'))
+ pp = p.joinpath('c:/x/y')
+ self.assertEqual(pp, P('C:/x/y'))
+
+ def test_div(self):
+ # Basically the same as joinpath()
+ P = self.cls
+ p = P('C:/a/b')
+ self.assertEqual(p / 'x/y', P('C:/a/b/x/y'))
+ self.assertEqual(p / 'x' / 'y', P('C:/a/b/x/y'))
+ self.assertEqual(p / '/x/y', P('C:/x/y'))
+ self.assertEqual(p / '/x' / 'y', P('C:/x/y'))
+ # Joining with a different drive => the first path is ignored, even
+ # if the second path is relative.
+ self.assertEqual(p / 'D:x/y', P('D:x/y'))
+ self.assertEqual(p / 'D:' / 'x/y', P('D:x/y'))
+ self.assertEqual(p / 'D:/x/y', P('D:/x/y'))
+ self.assertEqual(p / 'D:' / '/x/y', P('D:/x/y'))
+ self.assertEqual(p / '//host/share/x/y', P('//host/share/x/y'))
+ # Joining with the same drive => the first path is appended to if
+ # the second path is relative.
+ self.assertEqual(p / 'c:x/y', P('C:/a/b/x/y'))
+ self.assertEqual(p / 'c:/x/y', P('C:/x/y'))
+
+ def test_is_reserved(self):
+ P = self.cls
+ self.assertIs(False, P('').is_reserved())
+ self.assertIs(False, P('/').is_reserved())
+ self.assertIs(False, P('/foo/bar').is_reserved())
+ self.assertIs(True, P('con').is_reserved())
+ self.assertIs(True, P('NUL').is_reserved())
+ self.assertIs(True, P('NUL.txt').is_reserved())
+ self.assertIs(True, P('com1').is_reserved())
+ self.assertIs(True, P('com9.bar').is_reserved())
+ self.assertIs(False, P('bar.com9').is_reserved())
+ self.assertIs(True, P('lpt1').is_reserved())
+ self.assertIs(True, P('lpt9.bar').is_reserved())
+ self.assertIs(False, P('bar.lpt9').is_reserved())
+ # Only the last component matters
+ self.assertIs(False, P('c:/NUL/con/baz').is_reserved())
+ # UNC paths are never reserved
+ self.assertIs(False, P('//my/share/nul/con/aux').is_reserved())
+
+
+class PurePathTest(_BasePurePathTest, unittest.TestCase):
+ cls = pathlib.PurePath
+
+ def test_concrete_class(self):
+ p = self.cls('a')
+ self.assertIs(
+ type(p),
+ pathlib.PureWindowsPath
+ if os.name == 'nt' else pathlib.PurePosixPath)
+
+ def test_different_flavours_unequal(self):
+ p = pathlib.PurePosixPath('a')
+ q = pathlib.PureWindowsPath('a')
+ self.assertNotEqual(p, q)
+
+ @unittest.skipIf(sys.version_info < (3, 0),
+ 'Most types are orderable in Python 2')
+ def test_different_flavours_unordered(self):
+ p = pathlib.PurePosixPath('a')
+ q = pathlib.PureWindowsPath('a')
+ with self.assertRaises(TypeError):
+ p < q
+ with self.assertRaises(TypeError):
+ p <= q
+ with self.assertRaises(TypeError):
+ p > q
+ with self.assertRaises(TypeError):
+ p >= q
+
+
+#
+# Tests for the concrete classes
+#
+
+# Make sure any symbolic links in the base test path are resolved
+BASE = os.path.realpath(TESTFN)
+
+
+def join(*x):
+ return os.path.join(BASE, *x)
+
+
+def rel_join(*x):
+ return os.path.join(TESTFN, *x)
+
+
+only_nt = unittest.skipIf(os.name != 'nt',
+ 'test requires a Windows-compatible system')
+only_posix = unittest.skipIf(os.name == 'nt',
+ 'test requires a POSIX-compatible system')
+
+
+@only_posix
+class PosixPathAsPureTest(PurePosixPathTest):
+ cls = pathlib.PosixPath
+
+
+@only_nt
+class WindowsPathAsPureTest(PureWindowsPathTest):
+ cls = pathlib.WindowsPath
+
+ def test_owner(self):
+ P = self.cls
+ with self.assertRaises(NotImplementedError):
+ P('c:/').owner()
+
+ def test_group(self):
+ P = self.cls
+ with self.assertRaises(NotImplementedError):
+ P('c:/').group()
+
+
+class _BasePathTest(object):
+ """Tests for the FS-accessing functionalities of the Path classes."""
+
+ # (BASE)
+ # |
+ # |-- brokenLink -> non-existing
+ # |-- dirA
+ # | `-- linkC -> ../dirB
+ # |-- dirB
+ # | |-- fileB
+ # | `-- linkD -> ../dirB
+ # |-- dirC
+ # | |-- dirD
+ # | | `-- fileD
+ # | `-- fileC
+ # |-- dirE # No permissions
+ # |-- fileA
+ # |-- linkA -> fileA
+ # `-- linkB -> dirB
+ #
+
+ def setUp(self):
+ def cleanup():
+ os.chmod(join('dirE'), 0o777)
+ support.rmtree(BASE)
+ self.addCleanup(cleanup)
+ os.mkdir(BASE)
+ os.mkdir(join('dirA'))
+ os.mkdir(join('dirB'))
+ os.mkdir(join('dirC'))
+ os.mkdir(join('dirC', 'dirD'))
+ os.mkdir(join('dirE'))
+ with open(join('fileA'), 'wb') as f:
+ f.write(b"this is file A\n")
+ with open(join('dirB', 'fileB'), 'wb') as f:
+ f.write(b"this is file B\n")
+ with open(join('dirC', 'fileC'), 'wb') as f:
+ f.write(b"this is file C\n")
+ with open(join('dirC', 'dirD', 'fileD'), 'wb') as f:
+ f.write(b"this is file D\n")
+ os.chmod(join('dirE'), 0)
+ if support_can_symlink():
+ # Relative symlinks
+ os.symlink('fileA', join('linkA'))
+ os.symlink('non-existing', join('brokenLink'))
+ self.dirlink('dirB', join('linkB'))
+ self.dirlink(os.path.join('..', 'dirB'), join('dirA', 'linkC'))
+ # This one goes upwards, creating a loop
+ self.dirlink(os.path.join('..', 'dirB'), join('dirB', 'linkD'))
+
+ if os.name == 'nt':
+ # Workaround for http://bugs.python.org/issue13772
+ def dirlink(self, src, dest):
+ os.symlink(src, dest, target_is_directory=True)
+ else:
+ def dirlink(self, src, dest):
+ os.symlink(src, dest)
+
+ def assertSame(self, path_a, path_b):
+ self.assertTrue(os.path.samefile(str(path_a), str(path_b)),
+ "%r and %r don't point to the same file" %
+ (path_a, path_b))
+
+ def assertFileNotFound(self, func, *args, **kwargs):
+ if sys.version_info >= (3, 3):
+ with self.assertRaises(FileNotFoundError) as cm:
+ func(*args, **kwargs)
+ else:
+ with self.assertRaises(OSError) as cm:
+ # Python 2.6 kludge for http://bugs.python.org/issue7853
+ try:
+ func(*args, **kwargs)
+ except: # noqa: E722
+ raise
+ self.assertEqual(cm.exception.errno, errno.ENOENT)
+
+ def assertFileExists(self, func, *args, **kwargs):
+ if sys.version_info >= (3, 3):
+ with self.assertRaises(FileExistsError) as cm:
+ func(*args, **kwargs)
+ else:
+ with self.assertRaises(OSError) as cm:
+ # Python 2.6 kludge for http://bugs.python.org/issue7853
+ try:
+ func(*args, **kwargs)
+ except: # noqa: E722
+ raise
+ self.assertEqual(cm.exception.errno, errno.EEXIST)
+
+ def _test_cwd(self, p):
+ q = self.cls(os.getcwd())
+ self.assertEqual(p, q)
+ self.assertEqual(str(p), str(q))
+ self.assertIs(type(p), type(q))
+ self.assertTrue(p.is_absolute())
+
+ def test_cwd(self):
+ p = self.cls.cwd()
+ self._test_cwd(p)
+
+ def _test_home(self, p):
+ q = self.cls(os.path.expanduser('~'))
+ self.assertEqual(p, q)
+ self.assertEqual(str(p), str(q))
+ self.assertIs(type(p), type(q))
+ self.assertTrue(p.is_absolute())
+
+ def test_home(self):
+ p = self.cls.home()
+ self._test_home(p)
+
+ def test_samefile(self):
+ fileA_path = os.path.join(BASE, 'fileA')
+ fileB_path = os.path.join(BASE, 'dirB', 'fileB')
+ p = self.cls(fileA_path)
+ pp = self.cls(fileA_path)
+ q = self.cls(fileB_path)
+ self.assertTrue(p.samefile(fileA_path))
+ self.assertTrue(p.samefile(pp))
+ self.assertFalse(p.samefile(fileB_path))
+ self.assertFalse(p.samefile(q))
+ # Test the non-existent file case
+ non_existent = os.path.join(BASE, 'foo')
+ r = self.cls(non_existent)
+ self.assertFileNotFound(p.samefile, r)
+ self.assertFileNotFound(p.samefile, non_existent)
+ self.assertFileNotFound(r.samefile, p)
+ self.assertFileNotFound(r.samefile, non_existent)
+ self.assertFileNotFound(r.samefile, r)
+ self.assertFileNotFound(r.samefile, non_existent)
+
+ def test_empty_path(self):
+ # The empty path points to '.'
+ p = self.cls('')
+ self.assertEqual(p.stat(), os.stat('.'))
+
+ def test_expanduser_common(self):
+ P = self.cls
+ p = P('~')
+ self.assertEqual(p.expanduser(), P(os.path.expanduser('~')))
+ p = P('foo')
+ self.assertEqual(p.expanduser(), p)
+ p = P('/~')
+ self.assertEqual(p.expanduser(), p)
+ p = P('../~')
+ self.assertEqual(p.expanduser(), p)
+ p = P(P('').absolute().anchor) / '~'
+ self.assertEqual(p.expanduser(), p)
+
+ def test_exists(self):
+ P = self.cls
+ p = P(BASE)
+ self.assertIs(True, p.exists())
+ self.assertIs(True, (p / 'dirA').exists())
+ self.assertIs(True, (p / 'fileA').exists())
+ self.assertIs(False, (p / 'fileA' / 'bah').exists())
+ if support_can_symlink():
+ self.assertIs(True, (p / 'linkA').exists())
+ self.assertIs(True, (p / 'linkB').exists())
+ self.assertIs(True, (p / 'linkB' / 'fileB').exists())
+ self.assertIs(False, (p / 'linkA' / 'bah').exists())
+ self.assertIs(False, (p / 'foo').exists())
+ self.assertIs(False, P('/xyzzy').exists())
+
+ def test_open_common(self):
+ p = self.cls(BASE)
+ with (p / 'fileA').open('r') as f:
+ self.assertIsInstance(f, io.TextIOBase)
+ self.assertEqual(f.read(), "this is file A\n")
+ with (p / 'fileA').open('rb') as f:
+ self.assertIsInstance(f, io.BufferedIOBase)
+ self.assertEqual(f.read().strip(), b"this is file A")
+ with (p / 'fileA').open('rb', buffering=0) as f:
+ self.assertIsInstance(f, io.RawIOBase)
+ self.assertEqual(f.read().strip(), b"this is file A")
+
+ def test_read_write_bytes(self):
+ p = self.cls(BASE)
+ (p / 'fileA').write_bytes(b'abcdefg')
+ self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg')
+ # check that trying to write str does not truncate the file
+ with self.assertRaises(TypeError) as cm:
+ (p / 'fileA').write_bytes(six.u('somestr'))
+ self.assertTrue(str(cm.exception).startswith('data must be'))
+ self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg')
+
+ def test_read_write_text(self):
+ p = self.cls(BASE)
+ (p / 'fileA').write_text(six.u('\u00e4bcdefg'), encoding='latin-1')
+ self.assertEqual((p / 'fileA').read_text(
+ encoding='utf-8', errors='ignore'), six.u('bcdefg'))
+ # check that trying to write bytes does not truncate the file
+ with self.assertRaises(TypeError) as cm:
+ (p / 'fileA').write_text(b'somebytes')
+ self.assertTrue(str(cm.exception).startswith('data must be'))
+ self.assertEqual((p / 'fileA').read_text(encoding='latin-1'),
+ six.u('\u00e4bcdefg'))
+
+ def test_iterdir(self):
+ P = self.cls
+ p = P(BASE)
+ it = p.iterdir()
+ paths = set(it)
+ expected = ['dirA', 'dirB', 'dirC', 'dirE', 'fileA']
+ if support_can_symlink():
+ expected += ['linkA', 'linkB', 'brokenLink']
+ self.assertEqual(paths, set(P(BASE, q) for q in expected))
+
+ @support_skip_unless_symlink
+ def test_iterdir_symlink(self):
+ # __iter__ on a symlink to a directory
+ P = self.cls
+ p = P(BASE, 'linkB')
+ paths = set(p.iterdir())
+ expected = set(P(BASE, 'linkB', q) for q in ['fileB', 'linkD'])
+ self.assertEqual(paths, expected)
+
+ def test_iterdir_nodir(self):
+ # __iter__ on something that is not a directory
+ p = self.cls(BASE, 'fileA')
+ with self.assertRaises(OSError) as cm:
+ # Python 2.6 kludge for http://bugs.python.org/issue7853
+ try:
+ next(p.iterdir())
+ except: # noqa: E722s
+ raise
+ # ENOENT or EINVAL under Windows, ENOTDIR otherwise
+ # (see issue #12802)
+ self.assertIn(cm.exception.errno, (errno.ENOTDIR,
+ errno.ENOENT, errno.EINVAL))
+
+ def test_glob_common(self):
+ def _check(glob, expected):
+ self.assertEqual(set(glob), set(P(BASE, q) for q in expected))
+ P = self.cls
+ p = P(BASE)
+ it = p.glob("fileA")
+ self.assertIsInstance(it, collections_abc.Iterator)
+ _check(it, ["fileA"])
+ _check(p.glob("fileB"), [])
+ _check(p.glob("dir*/file*"), ["dirB/fileB", "dirC/fileC"])
+ if not support_can_symlink():
+ _check(p.glob("*A"), ['dirA', 'fileA'])
+ else:
+ _check(p.glob("*A"), ['dirA', 'fileA', 'linkA'])
+ if not support_can_symlink():
+ _check(p.glob("*B/*"), ['dirB/fileB'])
+ else:
+ _check(p.glob("*B/*"), ['dirB/fileB', 'dirB/linkD',
+ 'linkB/fileB', 'linkB/linkD'])
+ if not support_can_symlink():
+ _check(p.glob("*/fileB"), ['dirB/fileB'])
+ else:
+ _check(p.glob("*/fileB"), ['dirB/fileB', 'linkB/fileB'])
+
+ def test_rglob_common(self):
+ def _check(glob, expected):
+ self.assertEqual(set(glob), set(P(BASE, q) for q in expected))
+ P = self.cls
+ p = P(BASE)
+ it = p.rglob("fileA")
+ self.assertIsInstance(it, collections_abc.Iterator)
+ _check(it, ["fileA"])
+ _check(p.rglob("fileB"), ["dirB/fileB"])
+ _check(p.rglob("*/fileA"), [])
+ if not support_can_symlink():
+ _check(p.rglob("*/fileB"), ["dirB/fileB"])
+ else:
+ _check(p.rglob("*/fileB"), ["dirB/fileB", "dirB/linkD/fileB",
+ "linkB/fileB", "dirA/linkC/fileB"])
+ _check(p.rglob("file*"), ["fileA", "dirB/fileB",
+ "dirC/fileC", "dirC/dirD/fileD"])
+ p = P(BASE, "dirC")
+ _check(p.rglob("file*"), ["dirC/fileC", "dirC/dirD/fileD"])
+ _check(p.rglob("*/*"), ["dirC/dirD/fileD"])
+
+ @support_skip_unless_symlink
+ def test_rglob_symlink_loop(self):
+ # Don't get fooled by symlink loops (Issue #26012)
+ P = self.cls
+ p = P(BASE)
+ given = set(p.rglob('*'))
+ expect = set([
+ 'brokenLink',
+ 'dirA', 'dirA/linkC',
+ 'dirB', 'dirB/fileB', 'dirB/linkD',
+ 'dirC', 'dirC/dirD', 'dirC/dirD/fileD', 'dirC/fileC',
+ 'dirE',
+ 'fileA',
+ 'linkA',
+ 'linkB',
+ ])
+ self.assertEqual(given, set([p / x for x in expect]))
+
+ def test_glob_dotdot(self):
+ # ".." is not special in globs
+ P = self.cls
+ p = P(BASE)
+ self.assertEqual(set(p.glob("..")), set([P(BASE, "..")]))
+ self.assertEqual(set(p.glob("dirA/../file*")),
+ set([P(BASE, "dirA/../fileA")]))
+ self.assertEqual(set(p.glob("../xyzzy")), set())
+
+ def _check_resolve(self, p, expected, strict=True):
+ q = p.resolve(strict)
+ self.assertEqual(q, expected)
+
+ # this can be used to check both relative and absolute resolutions
+ _check_resolve_relative = _check_resolve_absolute = _check_resolve
+
+ @support_skip_unless_symlink
+ def test_resolve_common(self):
+ P = self.cls
+ p = P(BASE, 'foo')
+ with self.assertRaises(OSError) as cm:
+ p.resolve(strict=True)
+ self.assertEqual(cm.exception.errno, errno.ENOENT)
+ # Non-strict
+ self.assertEqual(str(p.resolve(strict=False)),
+ os.path.join(BASE, 'foo'))
+ p = P(BASE, 'foo', 'in', 'spam')
+ self.assertEqual(str(p.resolve(strict=False)),
+ os.path.join(BASE, 'foo', 'in', 'spam'))
+ p = P(BASE, '..', 'foo', 'in', 'spam')
+ self.assertEqual(str(p.resolve(strict=False)),
+ os.path.abspath(os.path.join('foo', 'in', 'spam')))
+ # These are all relative symlinks
+ p = P(BASE, 'dirB', 'fileB')
+ self._check_resolve_relative(p, p)
+ p = P(BASE, 'linkA')
+ self._check_resolve_relative(p, P(BASE, 'fileA'))
+ p = P(BASE, 'dirA', 'linkC', 'fileB')
+ self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB'))
+ p = P(BASE, 'dirB', 'linkD', 'fileB')
+ self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB'))
+ # Non-strict
+ p = P(BASE, 'dirA', 'linkC', 'fileB', 'foo', 'in', 'spam')
+ self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB', 'foo', 'in',
+ 'spam'), False)
+ p = P(BASE, 'dirA', 'linkC', '..', 'foo', 'in', 'spam')
+ if os.name == 'nt':
+ # In Windows, if linkY points to dirB, 'dirA\linkY\..'
+ # resolves to 'dirA' without resolving linkY first.
+ self._check_resolve_relative(p, P(BASE, 'dirA', 'foo', 'in',
+ 'spam'), False)
+ else:
+ # In Posix, if linkY points to dirB, 'dirA/linkY/..'
+ # resolves to 'dirB/..' first before resolving to parent of dirB.
+ self._check_resolve_relative(
+ p, P(BASE, 'foo', 'in', 'spam'), False)
+ # Now create absolute symlinks
+ d = tempfile.mkdtemp(suffix='-dirD')
+ self.addCleanup(support.rmtree, d)
+ os.symlink(os.path.join(d), join('dirA', 'linkX'))
+ os.symlink(join('dirB'), os.path.join(d, 'linkY'))
+ p = P(BASE, 'dirA', 'linkX', 'linkY', 'fileB')
+ self._check_resolve_absolute(p, P(BASE, 'dirB', 'fileB'))
+ # Non-strict
+ p = P(BASE, 'dirA', 'linkX', 'linkY', 'foo', 'in', 'spam')
+ self._check_resolve_relative(p, P(BASE, 'dirB', 'foo', 'in', 'spam'),
+ False)
+ p = P(BASE, 'dirA', 'linkX', 'linkY', '..', 'foo', 'in', 'spam')
+ if os.name == 'nt':
+ # In Windows, if linkY points to dirB, 'dirA\linkY\..'
+ # resolves to 'dirA' without resolving linkY first.
+ self._check_resolve_relative(p, P(d, 'foo', 'in', 'spam'), False)
+ else:
+ # In Posix, if linkY points to dirB, 'dirA/linkY/..'
+ # resolves to 'dirB/..' first before resolving to parent of dirB.
+ self._check_resolve_relative(
+ p, P(BASE, 'foo', 'in', 'spam'), False)
+
+ @support_skip_unless_symlink
+ def test_resolve_dot(self):
+ # See https://bitbucket.org/pitrou/pathlib/issue/9/
+ # pathresolve-fails-on-complex-symlinks
+ p = self.cls(BASE)
+ self.dirlink('.', join('0'))
+ self.dirlink(os.path.join('0', '0'), join('1'))
+ self.dirlink(os.path.join('1', '1'), join('2'))
+ q = p / '2'
+ self.assertEqual(q.resolve(strict=True), p)
+ r = q / '3' / '4'
+ self.assertFileNotFound(r.resolve, strict=True)
+ # Non-strict
+ self.assertEqual(r.resolve(strict=False), p / '3' / '4')
+
+ def test_with(self):
+ p = self.cls(BASE)
+ it = p.iterdir()
+ it2 = p.iterdir()
+ next(it2)
+ with p:
+ pass
+ # I/O operation on closed path
+ self.assertRaises(ValueError, next, it)
+ self.assertRaises(ValueError, next, it2)
+ self.assertRaises(ValueError, p.open)
+ self.assertRaises(ValueError, p.resolve)
+ self.assertRaises(ValueError, p.absolute)
+ self.assertRaises(ValueError, p.__enter__)
+
+ def test_chmod(self):
+ p = self.cls(BASE) / 'fileA'
+ mode = p.stat().st_mode
+ # Clear writable bit
+ new_mode = mode & ~0o222
+ p.chmod(new_mode)
+ self.assertEqual(p.stat().st_mode, new_mode)
+ # Set writable bit
+ new_mode = mode | 0o222
+ p.chmod(new_mode)
+ self.assertEqual(p.stat().st_mode, new_mode)
+
+ # XXX also need a test for lchmod
+
+ def test_stat(self):
+ p = self.cls(BASE) / 'fileA'
+ st = p.stat()
+ self.assertEqual(p.stat(), st)
+ # Change file mode by flipping write bit
+ p.chmod(st.st_mode ^ 0o222)
+ self.addCleanup(p.chmod, st.st_mode)
+ self.assertNotEqual(p.stat(), st)
+
+ @support_skip_unless_symlink
+ def test_lstat(self):
+ p = self.cls(BASE) / 'linkA'
+ st = p.stat()
+ self.assertNotEqual(st, p.lstat())
+
+ def test_lstat_nosymlink(self):
+ p = self.cls(BASE) / 'fileA'
+ st = p.stat()
+ self.assertEqual(st, p.lstat())
+
+ @unittest.skipUnless(pwd, "the pwd module is needed for this test")
+ def test_owner(self):
+ p = self.cls(BASE) / 'fileA'
+ uid = p.stat().st_uid
+ try:
+ name = pwd.getpwuid(uid).pw_name
+ except KeyError:
+ self.skipTest(
+ "user %d doesn't have an entry in the system database" % uid)
+ self.assertEqual(name, p.owner())
+
+ @unittest.skipUnless(grp, "the grp module is needed for this test")
+ def test_group(self):
+ p = self.cls(BASE) / 'fileA'
+ gid = p.stat().st_gid
+ try:
+ name = grp.getgrgid(gid).gr_name
+ except KeyError:
+ self.skipTest(
+ "group %d doesn't have an entry in the system database" % gid)
+ self.assertEqual(name, p.group())
+
+ def test_unlink(self):
+ p = self.cls(BASE) / 'fileA'
+ p.unlink()
+ self.assertFileNotFound(p.stat)
+ self.assertFileNotFound(p.unlink)
+
+ def test_rmdir(self):
+ p = self.cls(BASE) / 'dirA'
+ for q in p.iterdir():
+ q.unlink()
+ p.rmdir()
+ self.assertFileNotFound(p.stat)
+ self.assertFileNotFound(p.unlink)
+
+ def test_rename(self):
+ P = self.cls(BASE)
+ p = P / 'fileA'
+ size = p.stat().st_size
+ # Renaming to another path
+ q = P / 'dirA' / 'fileAA'
+ p.rename(q)
+ self.assertEqual(q.stat().st_size, size)
+ self.assertFileNotFound(p.stat)
+ # Renaming to a str of a relative path
+ r = rel_join('fileAAA')
+ q.rename(r)
+ self.assertEqual(os.stat(r).st_size, size)
+ self.assertFileNotFound(q.stat)
+
+ def test_replace(self):
+ P = self.cls(BASE)
+ p = P / 'fileA'
+ if sys.version_info < (3, 3):
+ self.assertRaises(NotImplementedError, p.replace, p)
+ return
+ size = p.stat().st_size
+ # Replacing a non-existing path
+ q = P / 'dirA' / 'fileAA'
+ p.replace(q)
+ self.assertEqual(q.stat().st_size, size)
+ self.assertFileNotFound(p.stat)
+ # Replacing another (existing) path
+ r = rel_join('dirB', 'fileB')
+ q.replace(r)
+ self.assertEqual(os.stat(r).st_size, size)
+ self.assertFileNotFound(q.stat)
+
+ def test_touch_common(self):
+ P = self.cls(BASE)
+ p = P / 'newfileA'
+ self.assertFalse(p.exists())
+ p.touch()
+ self.assertTrue(p.exists())
+ # Rewind the mtime sufficiently far in the past to work around
+ # filesystem-specific timestamp granularity.
+ old_mtime = p.stat().st_mtime - 10
+ os.utime(str(p), (old_mtime, old_mtime))
+ # The file mtime should be refreshed by calling touch() again
+ p.touch()
+ self.assertGreaterEqual(p.stat().st_mtime, old_mtime)
+ # Now with exist_ok=False
+ p = P / 'newfileB'
+ self.assertFalse(p.exists())
+ p.touch(mode=0o700, exist_ok=False)
+ self.assertTrue(p.exists())
+ self.assertRaises(OSError, p.touch, exist_ok=False)
+
+ def test_touch_nochange(self):
+ P = self.cls(BASE)
+ p = P / 'fileA'
+ p.touch()
+ with p.open('rb') as f:
+ self.assertEqual(f.read().strip(), b"this is file A")
+
+ def test_mkdir(self):
+ P = self.cls(BASE)
+ p = P / 'newdirA'
+ self.assertFalse(p.exists())
+ p.mkdir()
+ self.assertTrue(p.exists())
+ self.assertTrue(p.is_dir())
+ with self.assertRaises(OSError) as cm:
+ # Python 2.6 kludge for http://bugs.python.org/issue7853
+ try:
+ p.mkdir()
+ except: # noqa: E722
+ raise
+ self.assertEqual(cm.exception.errno, errno.EEXIST)
+
+ def test_mkdir_parents(self):
+ # Creating a chain of directories
+ p = self.cls(BASE, 'newdirB', 'newdirC')
+ self.assertFalse(p.exists())
+ with self.assertRaises(OSError) as cm:
+ p.mkdir()
+ self.assertEqual(cm.exception.errno, errno.ENOENT)
+ p.mkdir(parents=True)
+ self.assertTrue(p.exists())
+ self.assertTrue(p.is_dir())
+ with self.assertRaises(OSError) as cm:
+ p.mkdir(parents=True)
+ self.assertEqual(cm.exception.errno, errno.EEXIST)
+ # test `mode` arg
+ mode = stat.S_IMODE(p.stat().st_mode) # default mode
+ p = self.cls(BASE, 'newdirD', 'newdirE')
+ p.mkdir(0o555, parents=True)
+ self.assertTrue(p.exists())
+ self.assertTrue(p.is_dir())
+ if os.name != 'nt':
+ # the directory's permissions follow the mode argument
+ self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o7555 & mode)
+ # the parent's permissions follow the default process settings
+ self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), mode)
+
+ def test_mkdir_exist_ok(self):
+ p = self.cls(BASE, 'dirB')
+ st_ctime_first = p.stat().st_ctime
+ self.assertTrue(p.exists())
+ self.assertTrue(p.is_dir())
+ self.assertFileExists(p.mkdir)
+ p.mkdir(exist_ok=True)
+ self.assertTrue(p.exists())
+ self.assertEqual(p.stat().st_ctime, st_ctime_first)
+
+ def test_mkdir_exist_ok_with_parent(self):
+ p = self.cls(BASE, 'dirC')
+ self.assertTrue(p.exists())
+ self.assertFileExists(p.mkdir)
+ p = p / 'newdirC'
+ p.mkdir(parents=True)
+ st_ctime_first = p.stat().st_ctime
+ self.assertTrue(p.exists())
+ self.assertFileExists(p.mkdir, parents=True)
+ p.mkdir(parents=True, exist_ok=True)
+ self.assertTrue(p.exists())
+ self.assertEqual(p.stat().st_ctime, st_ctime_first)
+
+ def test_mkdir_exist_ok_root(self):
+ # Issue #25803: A drive root could raise PermissionError on Windows
+ self.cls('/').resolve().mkdir(exist_ok=True)
+ self.cls('/').resolve().mkdir(parents=True, exist_ok=True)
+
+ @only_nt # XXX: not sure how to test this on POSIX
+ def test_mkdir_with_unknown_drive(self):
+ for d in 'ZYXWVUTSRQPONMLKJIHGFEDCBA':
+ p = self.cls(d + ':\\')
+ if not p.is_dir():
+ break
+ else:
+ self.skipTest("cannot find a drive that doesn't exist")
+ with self.assertRaises(OSError):
+ (p / 'child' / 'path').mkdir(parents=True)
+
+ def test_mkdir_with_child_file(self):
+ p = self.cls(BASE, 'dirB', 'fileB')
+ self.assertTrue(p.exists())
+ # An exception is raised when the last path component is an existing
+ # regular file, regardless of whether exist_ok is true or not.
+ self.assertFileExists(p.mkdir, parents=True)
+ self.assertFileExists(p.mkdir, parents=True, exist_ok=True)
+
+ def test_mkdir_no_parents_file(self):
+ p = self.cls(BASE, 'fileA')
+ self.assertTrue(p.exists())
+ # An exception is raised when the last path component is an existing
+ # regular file, regardless of whether exist_ok is true or not.
+ self.assertFileExists(p.mkdir)
+ self.assertFileExists(p.mkdir, exist_ok=True)
+
+ def test_mkdir_concurrent_parent_creation(self):
+ for pattern_num in range(32):
+ p = self.cls(BASE, 'dirCPC%d' % pattern_num)
+ self.assertFalse(p.exists())
+
+ def my_mkdir(path, mode=0o777):
+ path = str(path)
+ # Emulate another process that would create the directory
+ # just before we try to create it ourselves. We do it
+ # in all possible pattern combinations, assuming that this
+ # function is called at most 5 times (dirCPC/dir1/dir2,
+ # dirCPC/dir1, dirCPC, dirCPC/dir1, dirCPC/dir1/dir2).
+ if pattern.pop():
+ os.mkdir(path, mode) # from another process
+ concurrently_created.add(path)
+ os.mkdir(path, mode) # our real call
+
+ pattern = [bool(pattern_num & (1 << n)) for n in range(5)]
+ concurrently_created = set()
+ p12 = p / 'dir1' / 'dir2'
+
+ def _try_func():
+ with mock.patch("pathlib2._normal_accessor.mkdir", my_mkdir):
+ p12.mkdir(parents=True, exist_ok=False)
+
+ def _exc_func(exc):
+ self.assertIn(str(p12), concurrently_created)
+
+ def _else_func():
+ self.assertNotIn(str(p12), concurrently_created)
+
+ pathlib._try_except_fileexistserror(
+ _try_func, _exc_func, _else_func)
+ self.assertTrue(p.exists())
+
+ @support_skip_unless_symlink
+ def test_symlink_to(self):
+ P = self.cls(BASE)
+ target = P / 'fileA'
+ # Symlinking a path target
+ link = P / 'dirA' / 'linkAA'
+ link.symlink_to(target)
+ self.assertEqual(link.stat(), target.stat())
+ self.assertNotEqual(link.lstat(), target.stat())
+ # Symlinking a str target
+ link = P / 'dirA' / 'linkAAA'
+ link.symlink_to(str(target))
+ self.assertEqual(link.stat(), target.stat())
+ self.assertNotEqual(link.lstat(), target.stat())
+ self.assertFalse(link.is_dir())
+ # Symlinking to a directory
+ target = P / 'dirB'
+ link = P / 'dirA' / 'linkAAAA'
+ link.symlink_to(target, target_is_directory=True)
+ self.assertEqual(link.stat(), target.stat())
+ self.assertNotEqual(link.lstat(), target.stat())
+ self.assertTrue(link.is_dir())
+ self.assertTrue(list(link.iterdir()))
+
+ def test_is_dir(self):
+ P = self.cls(BASE)
+ self.assertTrue((P / 'dirA').is_dir())
+ self.assertFalse((P / 'fileA').is_dir())
+ self.assertFalse((P / 'non-existing').is_dir())
+ self.assertFalse((P / 'fileA' / 'bah').is_dir())
+ if support_can_symlink():
+ self.assertFalse((P / 'linkA').is_dir())
+ self.assertTrue((P / 'linkB').is_dir())
+ self.assertFalse((P / 'brokenLink').is_dir())
+
+ def test_is_file(self):
+ P = self.cls(BASE)
+ self.assertTrue((P / 'fileA').is_file())
+ self.assertFalse((P / 'dirA').is_file())
+ self.assertFalse((P / 'non-existing').is_file())
+ self.assertFalse((P / 'fileA' / 'bah').is_file())
+ if support_can_symlink():
+ self.assertTrue((P / 'linkA').is_file())
+ self.assertFalse((P / 'linkB').is_file())
+ self.assertFalse((P / 'brokenLink').is_file())
+
+ def test_is_symlink(self):
+ P = self.cls(BASE)
+ self.assertFalse((P / 'fileA').is_symlink())
+ self.assertFalse((P / 'dirA').is_symlink())
+ self.assertFalse((P / 'non-existing').is_symlink())
+ self.assertFalse((P / 'fileA' / 'bah').is_symlink())
+ if support_can_symlink():
+ self.assertTrue((P / 'linkA').is_symlink())
+ self.assertTrue((P / 'linkB').is_symlink())
+ self.assertTrue((P / 'brokenLink').is_symlink())
+
+ def test_is_fifo_false(self):
+ P = self.cls(BASE)
+ self.assertFalse((P / 'fileA').is_fifo())
+ self.assertFalse((P / 'dirA').is_fifo())
+ self.assertFalse((P / 'non-existing').is_fifo())
+ self.assertFalse((P / 'fileA' / 'bah').is_fifo())
+
+ @unittest.skipUnless(hasattr(os, "mkfifo"), "os.mkfifo() required")
+ @unittest.skipIf(android_not_root, "mkfifo not allowed, non root user")
+ def test_is_fifo_true(self):
+ P = self.cls(BASE, 'myfifo')
+ os.mkfifo(str(P))
+ self.assertTrue(P.is_fifo())
+ self.assertFalse(P.is_socket())
+ self.assertFalse(P.is_file())
+
+ def test_is_socket_false(self):
+ P = self.cls(BASE)
+ self.assertFalse((P / 'fileA').is_socket())
+ self.assertFalse((P / 'dirA').is_socket())
+ self.assertFalse((P / 'non-existing').is_socket())
+ self.assertFalse((P / 'fileA' / 'bah').is_socket())
+
+ @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "Unix sockets required")
+ def test_is_socket_true(self):
+ P = self.cls(BASE, 'mysock')
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.addCleanup(sock.close)
+ try:
+ sock.bind(str(P))
+ except OSError as e:
+ if "AF_UNIX path too long" in str(e):
+ self.skipTest("cannot bind Unix socket: " + str(e))
+ self.assertTrue(P.is_socket())
+ self.assertFalse(P.is_fifo())
+ self.assertFalse(P.is_file())
+
+ def test_is_block_device_false(self):
+ P = self.cls(BASE)
+ self.assertFalse((P / 'fileA').is_block_device())
+ self.assertFalse((P / 'dirA').is_block_device())
+ self.assertFalse((P / 'non-existing').is_block_device())
+ self.assertFalse((P / 'fileA' / 'bah').is_block_device())
+
+ def test_is_char_device_false(self):
+ P = self.cls(BASE)
+ self.assertFalse((P / 'fileA').is_char_device())
+ self.assertFalse((P / 'dirA').is_char_device())
+ self.assertFalse((P / 'non-existing').is_char_device())
+ self.assertFalse((P / 'fileA' / 'bah').is_char_device())
+
+ @only_posix
+ def test_is_char_device_true(self):
+ # Under Unix, /dev/null should generally be a char device
+ P = self.cls('/dev/null')
+ if not P.exists():
+ self.skipTest("/dev/null required")
+ self.assertTrue(P.is_char_device())
+ self.assertFalse(P.is_block_device())
+ self.assertFalse(P.is_file())
+
+ def test_pickling_common(self):
+ p = self.cls(BASE, 'fileA')
+ for proto in range(0, pickle.HIGHEST_PROTOCOL + 1):
+ dumped = pickle.dumps(p, proto)
+ pp = pickle.loads(dumped)
+ self.assertEqual(pp.stat(), p.stat())
+
+ def test_parts_interning(self):
+ P = self.cls
+ p = P('/usr/bin/foo')
+ q = P('/usr/local/bin')
+ # 'usr'
+ self.assertIs(p.parts[1], q.parts[1])
+ # 'bin'
+ self.assertIs(p.parts[2], q.parts[3])
+
+ def _check_complex_symlinks(self, link0_target):
+ # Test solving a non-looping chain of symlinks (issue #19887)
+ P = self.cls(BASE)
+ self.dirlink(os.path.join('link0', 'link0'), join('link1'))
+ self.dirlink(os.path.join('link1', 'link1'), join('link2'))
+ self.dirlink(os.path.join('link2', 'link2'), join('link3'))
+ self.dirlink(link0_target, join('link0'))
+
+ # Resolve absolute paths
+ p = (P / 'link0').resolve()
+ self.assertEqual(p, P)
+ self.assertEqual(str(p), BASE)
+ p = (P / 'link1').resolve()
+ self.assertEqual(p, P)
+ self.assertEqual(str(p), BASE)
+ p = (P / 'link2').resolve()
+ self.assertEqual(p, P)
+ self.assertEqual(str(p), BASE)
+ p = (P / 'link3').resolve()
+ self.assertEqual(p, P)
+ self.assertEqual(str(p), BASE)
+
+ # Resolve relative paths
+ old_path = os.getcwd()
+ os.chdir(BASE)
+ try:
+ p = self.cls('link0').resolve()
+ self.assertEqual(p, P)
+ self.assertEqual(str(p), BASE)
+ p = self.cls('link1').resolve()
+ self.assertEqual(p, P)
+ self.assertEqual(str(p), BASE)
+ p = self.cls('link2').resolve()
+ self.assertEqual(p, P)
+ self.assertEqual(str(p), BASE)
+ p = self.cls('link3').resolve()
+ self.assertEqual(p, P)
+ self.assertEqual(str(p), BASE)
+ finally:
+ os.chdir(old_path)
+
+ @support_skip_unless_symlink
+ def test_complex_symlinks_absolute(self):
+ self._check_complex_symlinks(BASE)
+
+ @support_skip_unless_symlink
+ def test_complex_symlinks_relative(self):
+ self._check_complex_symlinks('.')
+
+ @support_skip_unless_symlink
+ def test_complex_symlinks_relative_dot_dot(self):
+ self._check_complex_symlinks(os.path.join('dirA', '..'))
+
+
+class PathTest(_BasePathTest, unittest.TestCase):
+ cls = pathlib.Path
+
+ def test_concrete_class(self):
+ p = self.cls('a')
+ self.assertIs(
+ type(p),
+ pathlib.WindowsPath if os.name == 'nt' else pathlib.PosixPath)
+
+ def test_unsupported_flavour(self):
+ if os.name == 'nt':
+ self.assertRaises(NotImplementedError, pathlib.PosixPath)
+ else:
+ self.assertRaises(NotImplementedError, pathlib.WindowsPath)
+
+ def test_glob_empty_pattern(self):
+ p = self.cls()
+ with self.assertRaisesRegex(ValueError, 'Unacceptable pattern'):
+ list(p.glob(''))
+
+
+@only_posix
+class PosixPathTest(_BasePathTest, unittest.TestCase):
+ cls = pathlib.PosixPath
+
+ def _check_symlink_loop(self, *args):
+ path = self.cls(*args)
+ with self.assertRaises(RuntimeError):
+ print(path.resolve(strict=True))
+
+ def _check_symlink_loop_nonstrict(self, *args):
+ path = self.cls(*args)
+ with self.assertRaises(RuntimeError):
+ print(path.resolve(strict=False))
+
+ def test_open_mode(self):
+ old_mask = os.umask(0)
+ self.addCleanup(os.umask, old_mask)
+ p = self.cls(BASE)
+ with (p / 'new_file').open('wb'):
+ pass
+ st = os.stat(join('new_file'))
+ self.assertEqual(stat.S_IMODE(st.st_mode), 0o666)
+ os.umask(0o022)
+ with (p / 'other_new_file').open('wb'):
+ pass
+ st = os.stat(join('other_new_file'))
+ self.assertEqual(stat.S_IMODE(st.st_mode), 0o644)
+
+ def test_touch_mode(self):
+ old_mask = os.umask(0)
+ self.addCleanup(os.umask, old_mask)
+ p = self.cls(BASE)
+ (p / 'new_file').touch()
+ st = os.stat(join('new_file'))
+ self.assertEqual(stat.S_IMODE(st.st_mode), 0o666)
+ os.umask(0o022)
+ (p / 'other_new_file').touch()
+ st = os.stat(join('other_new_file'))
+ self.assertEqual(stat.S_IMODE(st.st_mode), 0o644)
+ (p / 'masked_new_file').touch(mode=0o750)
+ st = os.stat(join('masked_new_file'))
+ self.assertEqual(stat.S_IMODE(st.st_mode), 0o750)
+
+ @support_skip_unless_symlink
+ def test_resolve_loop(self):
+ # Loops with relative symlinks
+ os.symlink('linkX/inside', join('linkX'))
+ self._check_symlink_loop(BASE, 'linkX')
+ os.symlink('linkY', join('linkY'))
+ self._check_symlink_loop(BASE, 'linkY')
+ os.symlink('linkZ/../linkZ', join('linkZ'))
+ self._check_symlink_loop(BASE, 'linkZ')
+ # Non-strict
+ self._check_symlink_loop_nonstrict(BASE, 'linkZ', 'foo')
+ # Loops with absolute symlinks
+ os.symlink(join('linkU/inside'), join('linkU'))
+ self._check_symlink_loop(BASE, 'linkU')
+ os.symlink(join('linkV'), join('linkV'))
+ self._check_symlink_loop(BASE, 'linkV')
+ os.symlink(join('linkW/../linkW'), join('linkW'))
+ self._check_symlink_loop(BASE, 'linkW')
+ # Non-strict
+ self._check_symlink_loop_nonstrict(BASE, 'linkW', 'foo')
+
+ def test_glob(self):
+ P = self.cls
+ p = P(BASE)
+ given = set(p.glob("FILEa"))
+ expect = set() if not support.fs_is_case_insensitive(BASE) else given
+ self.assertEqual(given, expect)
+ self.assertEqual(set(p.glob("FILEa*")), set())
+
+ def test_rglob(self):
+ P = self.cls
+ p = P(BASE, "dirC")
+ given = set(p.rglob("FILEd"))
+ expect = set() if not support.fs_is_case_insensitive(BASE) else given
+ self.assertEqual(given, expect)
+ self.assertEqual(set(p.rglob("FILEd*")), set())
+
+ @unittest.skipUnless(hasattr(pwd, 'getpwall'),
+ 'pwd module does not expose getpwall()')
+ def test_expanduser(self):
+ P = self.cls
+ support.import_module('pwd')
+ import pwd
+ pwdent = pwd.getpwuid(os.getuid())
+ username = pwdent.pw_name
+ userhome = pwdent.pw_dir.rstrip('/') or '/'
+ # find arbitrary different user (if exists)
+ for pwdent in pwd.getpwall():
+ othername = pwdent.pw_name
+ otherhome = pwdent.pw_dir.rstrip('/')
+ if othername != username and otherhome:
+ break
+
+ p1 = P('~/Documents')
+ p2 = P('~' + username + '/Documents')
+ p3 = P('~' + othername + '/Documents')
+ p4 = P('../~' + username + '/Documents')
+ p5 = P('/~' + username + '/Documents')
+ p6 = P('')
+ p7 = P('~fakeuser/Documents')
+
+ with support.EnvironmentVarGuard() as env:
+ env.unset('HOME')
+
+ self.assertEqual(p1.expanduser(), P(userhome) / 'Documents')
+ self.assertEqual(p2.expanduser(), P(userhome) / 'Documents')
+ self.assertEqual(p3.expanduser(), P(otherhome) / 'Documents')
+ self.assertEqual(p4.expanduser(), p4)
+ self.assertEqual(p5.expanduser(), p5)
+ self.assertEqual(p6.expanduser(), p6)
+ self.assertRaises(RuntimeError, p7.expanduser)
+
+ env.set('HOME', '/tmp')
+ self.assertEqual(p1.expanduser(), P('/tmp/Documents'))
+ self.assertEqual(p2.expanduser(), P(userhome) / 'Documents')
+ self.assertEqual(p3.expanduser(), P(otherhome) / 'Documents')
+ self.assertEqual(p4.expanduser(), p4)
+ self.assertEqual(p5.expanduser(), p5)
+ self.assertEqual(p6.expanduser(), p6)
+ self.assertRaises(RuntimeError, p7.expanduser)
+
+
+@only_nt
+class WindowsPathTest(_BasePathTest, unittest.TestCase):
+ cls = pathlib.WindowsPath
+
+ def test_glob(self):
+ P = self.cls
+ p = P(BASE)
+ self.assertEqual(set(p.glob("FILEa")), set([P(BASE, "fileA")]))
+
+ def test_rglob(self):
+ P = self.cls
+ p = P(BASE, "dirC")
+ self.assertEqual(set(p.rglob("FILEd")),
+ set([P(BASE, "dirC/dirD/fileD")]))
+
+ def test_expanduser(self):
+ P = self.cls
+ with support.EnvironmentVarGuard() as env:
+ env.unset('HOME')
+ env.unset('USERPROFILE')
+ env.unset('HOMEPATH')
+ env.unset('HOMEDRIVE')
+ env.set('USERNAME', 'alice')
+
+ # test that the path returns unchanged
+ p1 = P('~/My Documents')
+ p2 = P('~alice/My Documents')
+ p3 = P('~bob/My Documents')
+ p4 = P('/~/My Documents')
+ p5 = P('d:~/My Documents')
+ p6 = P('')
+ self.assertRaises(RuntimeError, p1.expanduser)
+ self.assertRaises(RuntimeError, p2.expanduser)
+ self.assertRaises(RuntimeError, p3.expanduser)
+ self.assertEqual(p4.expanduser(), p4)
+ self.assertEqual(p5.expanduser(), p5)
+ self.assertEqual(p6.expanduser(), p6)
+
+ def check():
+ env.unset('USERNAME')
+ self.assertEqual(p1.expanduser(),
+ P('C:/Users/alice/My Documents'))
+ self.assertRaises(KeyError, p2.expanduser)
+ env.set('USERNAME', 'alice')
+ self.assertEqual(p2.expanduser(),
+ P('C:/Users/alice/My Documents'))
+ self.assertEqual(p3.expanduser(),
+ P('C:/Users/bob/My Documents'))
+ self.assertEqual(p4.expanduser(), p4)
+ self.assertEqual(p5.expanduser(), p5)
+ self.assertEqual(p6.expanduser(), p6)
+
+ # test the first lookup key in the env vars
+ env.set('HOME', 'C:\\Users\\alice')
+ check()
+
+ # test that HOMEPATH is available instead
+ env.unset('HOME')
+ env.set('HOMEPATH', 'C:\\Users\\alice')
+ check()
+
+ env.set('HOMEDRIVE', 'C:\\')
+ env.set('HOMEPATH', 'Users\\alice')
+ check()
+
+ env.unset('HOMEDRIVE')
+ env.unset('HOMEPATH')
+ env.set('USERPROFILE', 'C:\\Users\\alice')
+ check()
+
+
+# extra test to ensure coverage of issue #54
+def test_resolve_extra():
+ pathlib.Path("~/does_not_exist").resolve()
+
+
+def main():
+ unittest.main(__name__)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/testing/web-platform/tests/tools/third_party/pdf_js/LICENSE b/testing/web-platform/tests/tools/third_party/pdf_js/LICENSE
new file mode 100644
index 0000000000..f433b1a53f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pdf_js/LICENSE
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/testing/web-platform/tests/tools/third_party/pdf_js/pdf.js b/testing/web-platform/tests/tools/third_party/pdf_js/pdf.js
new file mode 100644
index 0000000000..dcefe070cc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pdf_js/pdf.js
@@ -0,0 +1,24624 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * Javascript code in this page
+ *
+ * Copyright 2019 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @licend The above is the entire license notice for the
+ * Javascript code in this page
+ */
+
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define("pdfjs-dist/build/pdf", [], factory);
+ else if(typeof exports === 'object')
+ exports["pdfjs-dist/build/pdf"] = factory();
+ else
+ root["pdfjs-dist/build/pdf"] = root.pdfjsLib = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __w_pdfjs_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __w_pdfjs_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __w_pdfjs_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __w_pdfjs_require__.c = installedModules;
+/******/
+/******/ // define getter function for harmony exports
+/******/ __w_pdfjs_require__.d = function(exports, name, getter) {
+/******/ if(!__w_pdfjs_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
+/******/ }
+/******/ };
+/******/
+/******/ // define __esModule on exports
+/******/ __w_pdfjs_require__.r = function(exports) {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/
+/******/ // create a fake namespace object
+/******/ // mode & 1: value is a module id, require it
+/******/ // mode & 2: merge all properties of value into the ns
+/******/ // mode & 4: return value when already ns object
+/******/ // mode & 8|1: behave like require
+/******/ __w_pdfjs_require__.t = function(value, mode) {
+/******/ if(mode & 1) value = __w_pdfjs_require__(value);
+/******/ if(mode & 8) return value;
+/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ var ns = Object.create(null);
+/******/ __w_pdfjs_require__.r(ns);
+/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __w_pdfjs_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ return ns;
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __w_pdfjs_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __w_pdfjs_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __w_pdfjs_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __w_pdfjs_require__.p = "";
+/******/
+/******/
+/******/ // Load entry module and return exports
+/******/ return __w_pdfjs_require__(__w_pdfjs_require__.s = 0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var pdfjsVersion = '2.2.228';
+var pdfjsBuild = 'd7afb74a';
+
+var pdfjsSharedUtil = __w_pdfjs_require__(1);
+
+var pdfjsDisplayAPI = __w_pdfjs_require__(147);
+
+var pdfjsDisplayTextLayer = __w_pdfjs_require__(162);
+
+var pdfjsDisplayAnnotationLayer = __w_pdfjs_require__(163);
+
+var pdfjsDisplayDisplayUtils = __w_pdfjs_require__(151);
+
+var pdfjsDisplaySVG = __w_pdfjs_require__(164);
+
+var pdfjsDisplayWorkerOptions = __w_pdfjs_require__(156);
+
+var pdfjsDisplayAPICompatibility = __w_pdfjs_require__(153);
+
+{
+ var isNodeJS = __w_pdfjs_require__(4);
+
+ if (isNodeJS()) {
+ var PDFNodeStream = __w_pdfjs_require__(165).PDFNodeStream;
+
+ pdfjsDisplayAPI.setPDFNetworkStreamFactory(function (params) {
+ return new PDFNodeStream(params);
+ });
+ } else {
+ var PDFNetworkStream = __w_pdfjs_require__(168).PDFNetworkStream;
+
+ var PDFFetchStream;
+
+ if (pdfjsDisplayDisplayUtils.isFetchSupported()) {
+ PDFFetchStream = __w_pdfjs_require__(169).PDFFetchStream;
+ }
+
+ pdfjsDisplayAPI.setPDFNetworkStreamFactory(function (params) {
+ if (PDFFetchStream && pdfjsDisplayDisplayUtils.isValidFetchUrl(params.url)) {
+ return new PDFFetchStream(params);
+ }
+
+ return new PDFNetworkStream(params);
+ });
+ }
+}
+exports.build = pdfjsDisplayAPI.build;
+exports.version = pdfjsDisplayAPI.version;
+exports.getDocument = pdfjsDisplayAPI.getDocument;
+exports.LoopbackPort = pdfjsDisplayAPI.LoopbackPort;
+exports.PDFDataRangeTransport = pdfjsDisplayAPI.PDFDataRangeTransport;
+exports.PDFWorker = pdfjsDisplayAPI.PDFWorker;
+exports.renderTextLayer = pdfjsDisplayTextLayer.renderTextLayer;
+exports.AnnotationLayer = pdfjsDisplayAnnotationLayer.AnnotationLayer;
+exports.createPromiseCapability = pdfjsSharedUtil.createPromiseCapability;
+exports.PasswordResponses = pdfjsSharedUtil.PasswordResponses;
+exports.InvalidPDFException = pdfjsSharedUtil.InvalidPDFException;
+exports.MissingPDFException = pdfjsSharedUtil.MissingPDFException;
+exports.SVGGraphics = pdfjsDisplaySVG.SVGGraphics;
+exports.NativeImageDecoding = pdfjsSharedUtil.NativeImageDecoding;
+exports.CMapCompressionType = pdfjsSharedUtil.CMapCompressionType;
+exports.PermissionFlag = pdfjsSharedUtil.PermissionFlag;
+exports.UnexpectedResponseException = pdfjsSharedUtil.UnexpectedResponseException;
+exports.OPS = pdfjsSharedUtil.OPS;
+exports.VerbosityLevel = pdfjsSharedUtil.VerbosityLevel;
+exports.UNSUPPORTED_FEATURES = pdfjsSharedUtil.UNSUPPORTED_FEATURES;
+exports.createValidAbsoluteUrl = pdfjsSharedUtil.createValidAbsoluteUrl;
+exports.createObjectURL = pdfjsSharedUtil.createObjectURL;
+exports.removeNullCharacters = pdfjsSharedUtil.removeNullCharacters;
+exports.shadow = pdfjsSharedUtil.shadow;
+exports.Util = pdfjsSharedUtil.Util;
+exports.ReadableStream = pdfjsSharedUtil.ReadableStream;
+exports.URL = pdfjsSharedUtil.URL;
+exports.RenderingCancelledException = pdfjsDisplayDisplayUtils.RenderingCancelledException;
+exports.getFilenameFromUrl = pdfjsDisplayDisplayUtils.getFilenameFromUrl;
+exports.LinkTarget = pdfjsDisplayDisplayUtils.LinkTarget;
+exports.addLinkAttributes = pdfjsDisplayDisplayUtils.addLinkAttributes;
+exports.loadScript = pdfjsDisplayDisplayUtils.loadScript;
+exports.PDFDateString = pdfjsDisplayDisplayUtils.PDFDateString;
+exports.GlobalWorkerOptions = pdfjsDisplayWorkerOptions.GlobalWorkerOptions;
+exports.apiCompatibilityParams = pdfjsDisplayAPICompatibility.apiCompatibilityParams;
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.arrayByteLength = arrayByteLength;
+exports.arraysToBytes = arraysToBytes;
+exports.assert = assert;
+exports.bytesToString = bytesToString;
+exports.createPromiseCapability = createPromiseCapability;
+exports.getVerbosityLevel = getVerbosityLevel;
+exports.info = info;
+exports.isArrayBuffer = isArrayBuffer;
+exports.isArrayEqual = isArrayEqual;
+exports.isBool = isBool;
+exports.isEmptyObj = isEmptyObj;
+exports.isNum = isNum;
+exports.isString = isString;
+exports.isSpace = isSpace;
+exports.isSameOrigin = isSameOrigin;
+exports.createValidAbsoluteUrl = createValidAbsoluteUrl;
+exports.isLittleEndian = isLittleEndian;
+exports.isEvalSupported = isEvalSupported;
+exports.log2 = log2;
+exports.readInt8 = readInt8;
+exports.readUint16 = readUint16;
+exports.readUint32 = readUint32;
+exports.removeNullCharacters = removeNullCharacters;
+exports.setVerbosityLevel = setVerbosityLevel;
+exports.shadow = shadow;
+exports.string32 = string32;
+exports.stringToBytes = stringToBytes;
+exports.stringToPDFString = stringToPDFString;
+exports.stringToUTF8String = stringToUTF8String;
+exports.utf8StringToString = utf8StringToString;
+exports.warn = warn;
+exports.unreachable = unreachable;
+Object.defineProperty(exports, "ReadableStream", {
+ enumerable: true,
+ get: function get() {
+ return _streams_polyfill.ReadableStream;
+ }
+});
+Object.defineProperty(exports, "URL", {
+ enumerable: true,
+ get: function get() {
+ return _url_polyfill.URL;
+ }
+});
+exports.createObjectURL = exports.FormatError = exports.Util = exports.UnknownErrorException = exports.UnexpectedResponseException = exports.TextRenderingMode = exports.StreamType = exports.PermissionFlag = exports.PasswordResponses = exports.PasswordException = exports.NativeImageDecoding = exports.MissingPDFException = exports.InvalidPDFException = exports.AbortException = exports.CMapCompressionType = exports.ImageKind = exports.FontType = exports.AnnotationType = exports.AnnotationFlag = exports.AnnotationFieldFlag = exports.AnnotationBorderStyleType = exports.UNSUPPORTED_FEATURES = exports.VerbosityLevel = exports.OPS = exports.IDENTITY_MATRIX = exports.FONT_IDENTITY_MATRIX = void 0;
+
+__w_pdfjs_require__(2);
+
+var _streams_polyfill = __w_pdfjs_require__(143);
+
+var _url_polyfill = __w_pdfjs_require__(145);
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
+exports.IDENTITY_MATRIX = IDENTITY_MATRIX;
+var FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
+exports.FONT_IDENTITY_MATRIX = FONT_IDENTITY_MATRIX;
+var NativeImageDecoding = {
+ NONE: 'none',
+ DECODE: 'decode',
+ DISPLAY: 'display'
+};
+exports.NativeImageDecoding = NativeImageDecoding;
+var PermissionFlag = {
+ PRINT: 0x04,
+ MODIFY_CONTENTS: 0x08,
+ COPY: 0x10,
+ MODIFY_ANNOTATIONS: 0x20,
+ FILL_INTERACTIVE_FORMS: 0x100,
+ COPY_FOR_ACCESSIBILITY: 0x200,
+ ASSEMBLE: 0x400,
+ PRINT_HIGH_QUALITY: 0x800
+};
+exports.PermissionFlag = PermissionFlag;
+var TextRenderingMode = {
+ FILL: 0,
+ STROKE: 1,
+ FILL_STROKE: 2,
+ INVISIBLE: 3,
+ FILL_ADD_TO_PATH: 4,
+ STROKE_ADD_TO_PATH: 5,
+ FILL_STROKE_ADD_TO_PATH: 6,
+ ADD_TO_PATH: 7,
+ FILL_STROKE_MASK: 3,
+ ADD_TO_PATH_FLAG: 4
+};
+exports.TextRenderingMode = TextRenderingMode;
+var ImageKind = {
+ GRAYSCALE_1BPP: 1,
+ RGB_24BPP: 2,
+ RGBA_32BPP: 3
+};
+exports.ImageKind = ImageKind;
+var AnnotationType = {
+ TEXT: 1,
+ LINK: 2,
+ FREETEXT: 3,
+ LINE: 4,
+ SQUARE: 5,
+ CIRCLE: 6,
+ POLYGON: 7,
+ POLYLINE: 8,
+ HIGHLIGHT: 9,
+ UNDERLINE: 10,
+ SQUIGGLY: 11,
+ STRIKEOUT: 12,
+ STAMP: 13,
+ CARET: 14,
+ INK: 15,
+ POPUP: 16,
+ FILEATTACHMENT: 17,
+ SOUND: 18,
+ MOVIE: 19,
+ WIDGET: 20,
+ SCREEN: 21,
+ PRINTERMARK: 22,
+ TRAPNET: 23,
+ WATERMARK: 24,
+ THREED: 25,
+ REDACT: 26
+};
+exports.AnnotationType = AnnotationType;
+var AnnotationFlag = {
+ INVISIBLE: 0x01,
+ HIDDEN: 0x02,
+ PRINT: 0x04,
+ NOZOOM: 0x08,
+ NOROTATE: 0x10,
+ NOVIEW: 0x20,
+ READONLY: 0x40,
+ LOCKED: 0x80,
+ TOGGLENOVIEW: 0x100,
+ LOCKEDCONTENTS: 0x200
+};
+exports.AnnotationFlag = AnnotationFlag;
+var AnnotationFieldFlag = {
+ READONLY: 0x0000001,
+ REQUIRED: 0x0000002,
+ NOEXPORT: 0x0000004,
+ MULTILINE: 0x0001000,
+ PASSWORD: 0x0002000,
+ NOTOGGLETOOFF: 0x0004000,
+ RADIO: 0x0008000,
+ PUSHBUTTON: 0x0010000,
+ COMBO: 0x0020000,
+ EDIT: 0x0040000,
+ SORT: 0x0080000,
+ FILESELECT: 0x0100000,
+ MULTISELECT: 0x0200000,
+ DONOTSPELLCHECK: 0x0400000,
+ DONOTSCROLL: 0x0800000,
+ COMB: 0x1000000,
+ RICHTEXT: 0x2000000,
+ RADIOSINUNISON: 0x2000000,
+ COMMITONSELCHANGE: 0x4000000
+};
+exports.AnnotationFieldFlag = AnnotationFieldFlag;
+var AnnotationBorderStyleType = {
+ SOLID: 1,
+ DASHED: 2,
+ BEVELED: 3,
+ INSET: 4,
+ UNDERLINE: 5
+};
+exports.AnnotationBorderStyleType = AnnotationBorderStyleType;
+var StreamType = {
+ UNKNOWN: 0,
+ FLATE: 1,
+ LZW: 2,
+ DCT: 3,
+ JPX: 4,
+ JBIG: 5,
+ A85: 6,
+ AHX: 7,
+ CCF: 8,
+ RL: 9
+};
+exports.StreamType = StreamType;
+var FontType = {
+ UNKNOWN: 0,
+ TYPE1: 1,
+ TYPE1C: 2,
+ CIDFONTTYPE0: 3,
+ CIDFONTTYPE0C: 4,
+ TRUETYPE: 5,
+ CIDFONTTYPE2: 6,
+ TYPE3: 7,
+ OPENTYPE: 8,
+ TYPE0: 9,
+ MMTYPE1: 10
+};
+exports.FontType = FontType;
+var VerbosityLevel = {
+ ERRORS: 0,
+ WARNINGS: 1,
+ INFOS: 5
+};
+exports.VerbosityLevel = VerbosityLevel;
+var CMapCompressionType = {
+ NONE: 0,
+ BINARY: 1,
+ STREAM: 2
+};
+exports.CMapCompressionType = CMapCompressionType;
+var OPS = {
+ dependency: 1,
+ setLineWidth: 2,
+ setLineCap: 3,
+ setLineJoin: 4,
+ setMiterLimit: 5,
+ setDash: 6,
+ setRenderingIntent: 7,
+ setFlatness: 8,
+ setGState: 9,
+ save: 10,
+ restore: 11,
+ transform: 12,
+ moveTo: 13,
+ lineTo: 14,
+ curveTo: 15,
+ curveTo2: 16,
+ curveTo3: 17,
+ closePath: 18,
+ rectangle: 19,
+ stroke: 20,
+ closeStroke: 21,
+ fill: 22,
+ eoFill: 23,
+ fillStroke: 24,
+ eoFillStroke: 25,
+ closeFillStroke: 26,
+ closeEOFillStroke: 27,
+ endPath: 28,
+ clip: 29,
+ eoClip: 30,
+ beginText: 31,
+ endText: 32,
+ setCharSpacing: 33,
+ setWordSpacing: 34,
+ setHScale: 35,
+ setLeading: 36,
+ setFont: 37,
+ setTextRenderingMode: 38,
+ setTextRise: 39,
+ moveText: 40,
+ setLeadingMoveText: 41,
+ setTextMatrix: 42,
+ nextLine: 43,
+ showText: 44,
+ showSpacedText: 45,
+ nextLineShowText: 46,
+ nextLineSetSpacingShowText: 47,
+ setCharWidth: 48,
+ setCharWidthAndBounds: 49,
+ setStrokeColorSpace: 50,
+ setFillColorSpace: 51,
+ setStrokeColor: 52,
+ setStrokeColorN: 53,
+ setFillColor: 54,
+ setFillColorN: 55,
+ setStrokeGray: 56,
+ setFillGray: 57,
+ setStrokeRGBColor: 58,
+ setFillRGBColor: 59,
+ setStrokeCMYKColor: 60,
+ setFillCMYKColor: 61,
+ shadingFill: 62,
+ beginInlineImage: 63,
+ beginImageData: 64,
+ endInlineImage: 65,
+ paintXObject: 66,
+ markPoint: 67,
+ markPointProps: 68,
+ beginMarkedContent: 69,
+ beginMarkedContentProps: 70,
+ endMarkedContent: 71,
+ beginCompat: 72,
+ endCompat: 73,
+ paintFormXObjectBegin: 74,
+ paintFormXObjectEnd: 75,
+ beginGroup: 76,
+ endGroup: 77,
+ beginAnnotations: 78,
+ endAnnotations: 79,
+ beginAnnotation: 80,
+ endAnnotation: 81,
+ paintJpegXObject: 82,
+ paintImageMaskXObject: 83,
+ paintImageMaskXObjectGroup: 84,
+ paintImageXObject: 85,
+ paintInlineImageXObject: 86,
+ paintInlineImageXObjectGroup: 87,
+ paintImageXObjectRepeat: 88,
+ paintImageMaskXObjectRepeat: 89,
+ paintSolidColorImageMask: 90,
+ constructPath: 91
+};
+exports.OPS = OPS;
+var UNSUPPORTED_FEATURES = {
+ unknown: 'unknown',
+ forms: 'forms',
+ javaScript: 'javaScript',
+ smask: 'smask',
+ shadingPattern: 'shadingPattern',
+ font: 'font'
+};
+exports.UNSUPPORTED_FEATURES = UNSUPPORTED_FEATURES;
+var PasswordResponses = {
+ NEED_PASSWORD: 1,
+ INCORRECT_PASSWORD: 2
+};
+exports.PasswordResponses = PasswordResponses;
+var verbosity = VerbosityLevel.WARNINGS;
+
+function setVerbosityLevel(level) {
+ if (Number.isInteger(level)) {
+ verbosity = level;
+ }
+}
+
+function getVerbosityLevel() {
+ return verbosity;
+}
+
+function info(msg) {
+ if (verbosity >= VerbosityLevel.INFOS) {
+ console.log('Info: ' + msg);
+ }
+}
+
+function warn(msg) {
+ if (verbosity >= VerbosityLevel.WARNINGS) {
+ console.log('Warning: ' + msg);
+ }
+}
+
+function unreachable(msg) {
+ throw new Error(msg);
+}
+
+function assert(cond, msg) {
+ if (!cond) {
+ unreachable(msg);
+ }
+}
+
+function isSameOrigin(baseUrl, otherUrl) {
+ try {
+ var base = new _url_polyfill.URL(baseUrl);
+
+ if (!base.origin || base.origin === 'null') {
+ return false;
+ }
+ } catch (e) {
+ return false;
+ }
+
+ var other = new _url_polyfill.URL(otherUrl, base);
+ return base.origin === other.origin;
+}
+
+function _isValidProtocol(url) {
+ if (!url) {
+ return false;
+ }
+
+ switch (url.protocol) {
+ case 'http:':
+ case 'https:':
+ case 'ftp:':
+ case 'mailto:':
+ case 'tel:':
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+function createValidAbsoluteUrl(url, baseUrl) {
+ if (!url) {
+ return null;
+ }
+
+ try {
+ var absoluteUrl = baseUrl ? new _url_polyfill.URL(url, baseUrl) : new _url_polyfill.URL(url);
+
+ if (_isValidProtocol(absoluteUrl)) {
+ return absoluteUrl;
+ }
+ } catch (ex) {}
+
+ return null;
+}
+
+function shadow(obj, prop, value) {
+ Object.defineProperty(obj, prop, {
+ value: value,
+ enumerable: true,
+ configurable: true,
+ writable: false
+ });
+ return value;
+}
+
+var PasswordException = function PasswordExceptionClosure() {
+ function PasswordException(msg, code) {
+ this.name = 'PasswordException';
+ this.message = msg;
+ this.code = code;
+ }
+
+ PasswordException.prototype = new Error();
+ PasswordException.constructor = PasswordException;
+ return PasswordException;
+}();
+
+exports.PasswordException = PasswordException;
+
+var UnknownErrorException = function UnknownErrorExceptionClosure() {
+ function UnknownErrorException(msg, details) {
+ this.name = 'UnknownErrorException';
+ this.message = msg;
+ this.details = details;
+ }
+
+ UnknownErrorException.prototype = new Error();
+ UnknownErrorException.constructor = UnknownErrorException;
+ return UnknownErrorException;
+}();
+
+exports.UnknownErrorException = UnknownErrorException;
+
+var InvalidPDFException = function InvalidPDFExceptionClosure() {
+ function InvalidPDFException(msg) {
+ this.name = 'InvalidPDFException';
+ this.message = msg;
+ }
+
+ InvalidPDFException.prototype = new Error();
+ InvalidPDFException.constructor = InvalidPDFException;
+ return InvalidPDFException;
+}();
+
+exports.InvalidPDFException = InvalidPDFException;
+
+var MissingPDFException = function MissingPDFExceptionClosure() {
+ function MissingPDFException(msg) {
+ this.name = 'MissingPDFException';
+ this.message = msg;
+ }
+
+ MissingPDFException.prototype = new Error();
+ MissingPDFException.constructor = MissingPDFException;
+ return MissingPDFException;
+}();
+
+exports.MissingPDFException = MissingPDFException;
+
+var UnexpectedResponseException = function UnexpectedResponseExceptionClosure() {
+ function UnexpectedResponseException(msg, status) {
+ this.name = 'UnexpectedResponseException';
+ this.message = msg;
+ this.status = status;
+ }
+
+ UnexpectedResponseException.prototype = new Error();
+ UnexpectedResponseException.constructor = UnexpectedResponseException;
+ return UnexpectedResponseException;
+}();
+
+exports.UnexpectedResponseException = UnexpectedResponseException;
+
+var FormatError = function FormatErrorClosure() {
+ function FormatError(msg) {
+ this.message = msg;
+ }
+
+ FormatError.prototype = new Error();
+ FormatError.prototype.name = 'FormatError';
+ FormatError.constructor = FormatError;
+ return FormatError;
+}();
+
+exports.FormatError = FormatError;
+
+var AbortException = function AbortExceptionClosure() {
+ function AbortException(msg) {
+ this.name = 'AbortException';
+ this.message = msg;
+ }
+
+ AbortException.prototype = new Error();
+ AbortException.constructor = AbortException;
+ return AbortException;
+}();
+
+exports.AbortException = AbortException;
+var NullCharactersRegExp = /\x00/g;
+
+function removeNullCharacters(str) {
+ if (typeof str !== 'string') {
+ warn('The argument for removeNullCharacters must be a string.');
+ return str;
+ }
+
+ return str.replace(NullCharactersRegExp, '');
+}
+
+function bytesToString(bytes) {
+ assert(bytes !== null && _typeof(bytes) === 'object' && bytes.length !== undefined, 'Invalid argument for bytesToString');
+ var length = bytes.length;
+ var MAX_ARGUMENT_COUNT = 8192;
+
+ if (length < MAX_ARGUMENT_COUNT) {
+ return String.fromCharCode.apply(null, bytes);
+ }
+
+ var strBuf = [];
+
+ for (var i = 0; i < length; i += MAX_ARGUMENT_COUNT) {
+ var chunkEnd = Math.min(i + MAX_ARGUMENT_COUNT, length);
+ var chunk = bytes.subarray(i, chunkEnd);
+ strBuf.push(String.fromCharCode.apply(null, chunk));
+ }
+
+ return strBuf.join('');
+}
+
+function stringToBytes(str) {
+ assert(typeof str === 'string', 'Invalid argument for stringToBytes');
+ var length = str.length;
+ var bytes = new Uint8Array(length);
+
+ for (var i = 0; i < length; ++i) {
+ bytes[i] = str.charCodeAt(i) & 0xFF;
+ }
+
+ return bytes;
+}
+
+function arrayByteLength(arr) {
+ if (arr.length !== undefined) {
+ return arr.length;
+ }
+
+ assert(arr.byteLength !== undefined);
+ return arr.byteLength;
+}
+
+function arraysToBytes(arr) {
+ if (arr.length === 1 && arr[0] instanceof Uint8Array) {
+ return arr[0];
+ }
+
+ var resultLength = 0;
+ var i,
+ ii = arr.length;
+ var item, itemLength;
+
+ for (i = 0; i < ii; i++) {
+ item = arr[i];
+ itemLength = arrayByteLength(item);
+ resultLength += itemLength;
+ }
+
+ var pos = 0;
+ var data = new Uint8Array(resultLength);
+
+ for (i = 0; i < ii; i++) {
+ item = arr[i];
+
+ if (!(item instanceof Uint8Array)) {
+ if (typeof item === 'string') {
+ item = stringToBytes(item);
+ } else {
+ item = new Uint8Array(item);
+ }
+ }
+
+ itemLength = item.byteLength;
+ data.set(item, pos);
+ pos += itemLength;
+ }
+
+ return data;
+}
+
+function string32(value) {
+ return String.fromCharCode(value >> 24 & 0xff, value >> 16 & 0xff, value >> 8 & 0xff, value & 0xff);
+}
+
+function log2(x) {
+ if (x <= 0) {
+ return 0;
+ }
+
+ return Math.ceil(Math.log2(x));
+}
+
+function readInt8(data, start) {
+ return data[start] << 24 >> 24;
+}
+
+function readUint16(data, offset) {
+ return data[offset] << 8 | data[offset + 1];
+}
+
+function readUint32(data, offset) {
+ return (data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]) >>> 0;
+}
+
+function isLittleEndian() {
+ var buffer8 = new Uint8Array(4);
+ buffer8[0] = 1;
+ var view32 = new Uint32Array(buffer8.buffer, 0, 1);
+ return view32[0] === 1;
+}
+
+function isEvalSupported() {
+ try {
+ new Function('');
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+var Util = function UtilClosure() {
+ function Util() {}
+
+ var rgbBuf = ['rgb(', 0, ',', 0, ',', 0, ')'];
+
+ Util.makeCssRgb = function Util_makeCssRgb(r, g, b) {
+ rgbBuf[1] = r;
+ rgbBuf[3] = g;
+ rgbBuf[5] = b;
+ return rgbBuf.join('');
+ };
+
+ Util.transform = function Util_transform(m1, m2) {
+ return [m1[0] * m2[0] + m1[2] * m2[1], m1[1] * m2[0] + m1[3] * m2[1], m1[0] * m2[2] + m1[2] * m2[3], m1[1] * m2[2] + m1[3] * m2[3], m1[0] * m2[4] + m1[2] * m2[5] + m1[4], m1[1] * m2[4] + m1[3] * m2[5] + m1[5]];
+ };
+
+ Util.applyTransform = function Util_applyTransform(p, m) {
+ var xt = p[0] * m[0] + p[1] * m[2] + m[4];
+ var yt = p[0] * m[1] + p[1] * m[3] + m[5];
+ return [xt, yt];
+ };
+
+ Util.applyInverseTransform = function Util_applyInverseTransform(p, m) {
+ var d = m[0] * m[3] - m[1] * m[2];
+ var xt = (p[0] * m[3] - p[1] * m[2] + m[2] * m[5] - m[4] * m[3]) / d;
+ var yt = (-p[0] * m[1] + p[1] * m[0] + m[4] * m[1] - m[5] * m[0]) / d;
+ return [xt, yt];
+ };
+
+ Util.getAxialAlignedBoundingBox = function Util_getAxialAlignedBoundingBox(r, m) {
+ var p1 = Util.applyTransform(r, m);
+ var p2 = Util.applyTransform(r.slice(2, 4), m);
+ var p3 = Util.applyTransform([r[0], r[3]], m);
+ var p4 = Util.applyTransform([r[2], r[1]], m);
+ return [Math.min(p1[0], p2[0], p3[0], p4[0]), Math.min(p1[1], p2[1], p3[1], p4[1]), Math.max(p1[0], p2[0], p3[0], p4[0]), Math.max(p1[1], p2[1], p3[1], p4[1])];
+ };
+
+ Util.inverseTransform = function Util_inverseTransform(m) {
+ var d = m[0] * m[3] - m[1] * m[2];
+ return [m[3] / d, -m[1] / d, -m[2] / d, m[0] / d, (m[2] * m[5] - m[4] * m[3]) / d, (m[4] * m[1] - m[5] * m[0]) / d];
+ };
+
+ Util.apply3dTransform = function Util_apply3dTransform(m, v) {
+ return [m[0] * v[0] + m[1] * v[1] + m[2] * v[2], m[3] * v[0] + m[4] * v[1] + m[5] * v[2], m[6] * v[0] + m[7] * v[1] + m[8] * v[2]];
+ };
+
+ Util.singularValueDecompose2dScale = function Util_singularValueDecompose2dScale(m) {
+ var transpose = [m[0], m[2], m[1], m[3]];
+ var a = m[0] * transpose[0] + m[1] * transpose[2];
+ var b = m[0] * transpose[1] + m[1] * transpose[3];
+ var c = m[2] * transpose[0] + m[3] * transpose[2];
+ var d = m[2] * transpose[1] + m[3] * transpose[3];
+ var first = (a + d) / 2;
+ var second = Math.sqrt((a + d) * (a + d) - 4 * (a * d - c * b)) / 2;
+ var sx = first + second || 1;
+ var sy = first - second || 1;
+ return [Math.sqrt(sx), Math.sqrt(sy)];
+ };
+
+ Util.normalizeRect = function Util_normalizeRect(rect) {
+ var r = rect.slice(0);
+
+ if (rect[0] > rect[2]) {
+ r[0] = rect[2];
+ r[2] = rect[0];
+ }
+
+ if (rect[1] > rect[3]) {
+ r[1] = rect[3];
+ r[3] = rect[1];
+ }
+
+ return r;
+ };
+
+ Util.intersect = function Util_intersect(rect1, rect2) {
+ function compare(a, b) {
+ return a - b;
+ }
+
+ var orderedX = [rect1[0], rect1[2], rect2[0], rect2[2]].sort(compare),
+ orderedY = [rect1[1], rect1[3], rect2[1], rect2[3]].sort(compare),
+ result = [];
+ rect1 = Util.normalizeRect(rect1);
+ rect2 = Util.normalizeRect(rect2);
+
+ if (orderedX[0] === rect1[0] && orderedX[1] === rect2[0] || orderedX[0] === rect2[0] && orderedX[1] === rect1[0]) {
+ result[0] = orderedX[1];
+ result[2] = orderedX[2];
+ } else {
+ return false;
+ }
+
+ if (orderedY[0] === rect1[1] && orderedY[1] === rect2[1] || orderedY[0] === rect2[1] && orderedY[1] === rect1[1]) {
+ result[1] = orderedY[1];
+ result[3] = orderedY[2];
+ } else {
+ return false;
+ }
+
+ return result;
+ };
+
+ return Util;
+}();
+
+exports.Util = Util;
+var PDFStringTranslateTable = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2D8, 0x2C7, 0x2C6, 0x2D9, 0x2DD, 0x2DB, 0x2DA, 0x2DC, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2022, 0x2020, 0x2021, 0x2026, 0x2014, 0x2013, 0x192, 0x2044, 0x2039, 0x203A, 0x2212, 0x2030, 0x201E, 0x201C, 0x201D, 0x2018, 0x2019, 0x201A, 0x2122, 0xFB01, 0xFB02, 0x141, 0x152, 0x160, 0x178, 0x17D, 0x131, 0x142, 0x153, 0x161, 0x17E, 0, 0x20AC];
+
+function stringToPDFString(str) {
+ var i,
+ n = str.length,
+ strBuf = [];
+
+ if (str[0] === '\xFE' && str[1] === '\xFF') {
+ for (i = 2; i < n; i += 2) {
+ strBuf.push(String.fromCharCode(str.charCodeAt(i) << 8 | str.charCodeAt(i + 1)));
+ }
+ } else {
+ for (i = 0; i < n; ++i) {
+ var code = PDFStringTranslateTable[str.charCodeAt(i)];
+ strBuf.push(code ? String.fromCharCode(code) : str.charAt(i));
+ }
+ }
+
+ return strBuf.join('');
+}
+
+function stringToUTF8String(str) {
+ return decodeURIComponent(escape(str));
+}
+
+function utf8StringToString(str) {
+ return unescape(encodeURIComponent(str));
+}
+
+function isEmptyObj(obj) {
+ for (var key in obj) {
+ return false;
+ }
+
+ return true;
+}
+
+function isBool(v) {
+ return typeof v === 'boolean';
+}
+
+function isNum(v) {
+ return typeof v === 'number';
+}
+
+function isString(v) {
+ return typeof v === 'string';
+}
+
+function isArrayBuffer(v) {
+ return _typeof(v) === 'object' && v !== null && v.byteLength !== undefined;
+}
+
+function isArrayEqual(arr1, arr2) {
+ if (arr1.length !== arr2.length) {
+ return false;
+ }
+
+ return arr1.every(function (element, index) {
+ return element === arr2[index];
+ });
+}
+
+function isSpace(ch) {
+ return ch === 0x20 || ch === 0x09 || ch === 0x0D || ch === 0x0A;
+}
+
+function createPromiseCapability() {
+ var capability = Object.create(null);
+ var isSettled = false;
+ Object.defineProperty(capability, 'settled', {
+ get: function get() {
+ return isSettled;
+ }
+ });
+ capability.promise = new Promise(function (resolve, reject) {
+ capability.resolve = function (data) {
+ isSettled = true;
+ resolve(data);
+ };
+
+ capability.reject = function (reason) {
+ isSettled = true;
+ reject(reason);
+ };
+ });
+ return capability;
+}
+
+var createObjectURL = function createObjectURLClosure() {
+ var digits = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+ return function createObjectURL(data, contentType) {
+ var forceDataSchema = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+
+ if (!forceDataSchema && _url_polyfill.URL.createObjectURL) {
+ var blob = new Blob([data], {
+ type: contentType
+ });
+ return _url_polyfill.URL.createObjectURL(blob);
+ }
+
+ var buffer = 'data:' + contentType + ';base64,';
+
+ for (var i = 0, ii = data.length; i < ii; i += 3) {
+ var b1 = data[i] & 0xFF;
+ var b2 = data[i + 1] & 0xFF;
+ var b3 = data[i + 2] & 0xFF;
+ var d1 = b1 >> 2,
+ d2 = (b1 & 3) << 4 | b2 >> 4;
+ var d3 = i + 1 < ii ? (b2 & 0xF) << 2 | b3 >> 6 : 64;
+ var d4 = i + 2 < ii ? b3 & 0x3F : 64;
+ buffer += digits[d1] + digits[d2] + digits[d3] + digits[d4];
+ }
+
+ return buffer;
+ };
+}();
+
+exports.createObjectURL = createObjectURL;
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var globalScope = __w_pdfjs_require__(3);
+
+if (!globalScope._pdfjsCompatibilityChecked) {
+ globalScope._pdfjsCompatibilityChecked = true;
+
+ var isNodeJS = __w_pdfjs_require__(4);
+
+ var hasDOM = (typeof window === "undefined" ? "undefined" : _typeof(window)) === 'object' && (typeof document === "undefined" ? "undefined" : _typeof(document)) === 'object';
+
+ (function checkNodeBtoa() {
+ if (globalScope.btoa || !isNodeJS()) {
+ return;
+ }
+
+ globalScope.btoa = function (chars) {
+ return Buffer.from(chars, 'binary').toString('base64');
+ };
+ })();
+
+ (function checkNodeAtob() {
+ if (globalScope.atob || !isNodeJS()) {
+ return;
+ }
+
+ globalScope.atob = function (input) {
+ return Buffer.from(input, 'base64').toString('binary');
+ };
+ })();
+
+ (function checkChildNodeRemove() {
+ if (!hasDOM) {
+ return;
+ }
+
+ if (typeof Element.prototype.remove !== 'undefined') {
+ return;
+ }
+
+ Element.prototype.remove = function () {
+ if (this.parentNode) {
+ this.parentNode.removeChild(this);
+ }
+ };
+ })();
+
+ (function checkDOMTokenListAddRemove() {
+ if (!hasDOM || isNodeJS()) {
+ return;
+ }
+
+ var div = document.createElement('div');
+ div.classList.add('testOne', 'testTwo');
+
+ if (div.classList.contains('testOne') === true && div.classList.contains('testTwo') === true) {
+ return;
+ }
+
+ var OriginalDOMTokenListAdd = DOMTokenList.prototype.add;
+ var OriginalDOMTokenListRemove = DOMTokenList.prototype.remove;
+
+ DOMTokenList.prototype.add = function () {
+ for (var _len = arguments.length, tokens = new Array(_len), _key = 0; _key < _len; _key++) {
+ tokens[_key] = arguments[_key];
+ }
+
+ for (var _i = 0, _tokens = tokens; _i < _tokens.length; _i++) {
+ var token = _tokens[_i];
+ OriginalDOMTokenListAdd.call(this, token);
+ }
+ };
+
+ DOMTokenList.prototype.remove = function () {
+ for (var _len2 = arguments.length, tokens = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
+ tokens[_key2] = arguments[_key2];
+ }
+
+ for (var _i2 = 0, _tokens2 = tokens; _i2 < _tokens2.length; _i2++) {
+ var token = _tokens2[_i2];
+ OriginalDOMTokenListRemove.call(this, token);
+ }
+ };
+ })();
+
+ (function checkDOMTokenListToggle() {
+ if (!hasDOM || isNodeJS()) {
+ return;
+ }
+
+ var div = document.createElement('div');
+
+ if (div.classList.toggle('test', 0) === false) {
+ return;
+ }
+
+ DOMTokenList.prototype.toggle = function (token) {
+ var force = arguments.length > 1 ? !!arguments[1] : !this.contains(token);
+ return this[force ? 'add' : 'remove'](token), force;
+ };
+ })();
+
+ (function checkStringStartsWith() {
+ if (String.prototype.startsWith) {
+ return;
+ }
+
+ __w_pdfjs_require__(5);
+ })();
+
+ (function checkStringEndsWith() {
+ if (String.prototype.endsWith) {
+ return;
+ }
+
+ __w_pdfjs_require__(36);
+ })();
+
+ (function checkStringIncludes() {
+ if (String.prototype.includes) {
+ return;
+ }
+
+ __w_pdfjs_require__(38);
+ })();
+
+ (function checkArrayIncludes() {
+ if (Array.prototype.includes) {
+ return;
+ }
+
+ __w_pdfjs_require__(40);
+ })();
+
+ (function checkArrayFrom() {
+ if (Array.from) {
+ return;
+ }
+
+ __w_pdfjs_require__(47);
+ })();
+
+ (function checkObjectAssign() {
+ if (Object.assign) {
+ return;
+ }
+
+ __w_pdfjs_require__(70);
+ })();
+
+ (function checkMathLog2() {
+ if (Math.log2) {
+ return;
+ }
+
+ Math.log2 = __w_pdfjs_require__(75);
+ })();
+
+ (function checkNumberIsNaN() {
+ if (Number.isNaN) {
+ return;
+ }
+
+ Number.isNaN = __w_pdfjs_require__(77);
+ })();
+
+ (function checkNumberIsInteger() {
+ if (Number.isInteger) {
+ return;
+ }
+
+ Number.isInteger = __w_pdfjs_require__(79);
+ })();
+
+ (function checkPromise() {
+ if (globalScope.Promise && globalScope.Promise.prototype && globalScope.Promise.prototype["finally"]) {
+ return;
+ }
+
+ globalScope.Promise = __w_pdfjs_require__(82);
+ })();
+
+ (function checkWeakMap() {
+ if (globalScope.WeakMap) {
+ return;
+ }
+
+ globalScope.WeakMap = __w_pdfjs_require__(102);
+ })();
+
+ (function checkWeakSet() {
+ if (globalScope.WeakSet) {
+ return;
+ }
+
+ globalScope.WeakSet = __w_pdfjs_require__(119);
+ })();
+
+ (function checkStringCodePointAt() {
+ if (String.codePointAt) {
+ return;
+ }
+
+ String.codePointAt = __w_pdfjs_require__(123);
+ })();
+
+ (function checkStringFromCodePoint() {
+ if (String.fromCodePoint) {
+ return;
+ }
+
+ String.fromCodePoint = __w_pdfjs_require__(125);
+ })();
+
+ (function checkSymbol() {
+ if (globalScope.Symbol) {
+ return;
+ }
+
+ __w_pdfjs_require__(127);
+ })();
+
+ (function checkStringPadStart() {
+ if (String.prototype.padStart) {
+ return;
+ }
+
+ __w_pdfjs_require__(134);
+ })();
+
+ (function checkStringPadEnd() {
+ if (String.prototype.padEnd) {
+ return;
+ }
+
+ __w_pdfjs_require__(138);
+ })();
+
+ (function checkObjectValues() {
+ if (Object.values) {
+ return;
+ }
+
+ Object.values = __w_pdfjs_require__(140);
+ })();
+}
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = typeof window !== 'undefined' && window.Math === Math ? window : typeof global !== 'undefined' && global.Math === Math ? global : typeof self !== 'undefined' && self.Math === Math ? self : {};
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+module.exports = function isNodeJS() {
+ return (typeof process === "undefined" ? "undefined" : _typeof(process)) === 'object' && process + '' === '[object process]' && !process.versions['nw'] && !process.versions['electron'];
+};
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(6);
+
+module.exports = __w_pdfjs_require__(9).String.startsWith;
+
+/***/ }),
+/* 6 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+var toLength = __w_pdfjs_require__(28);
+
+var context = __w_pdfjs_require__(30);
+
+var STARTS_WITH = 'startsWith';
+var $startsWith = ''[STARTS_WITH];
+$export($export.P + $export.F * __w_pdfjs_require__(35)(STARTS_WITH), 'String', {
+ startsWith: function startsWith(searchString) {
+ var that = context(this, searchString, STARTS_WITH);
+ var index = toLength(Math.min(arguments.length > 1 ? arguments[1] : undefined, that.length));
+ var search = String(searchString);
+ return $startsWith ? $startsWith.call(that, search, index) : that.slice(index, index + search.length) === search;
+ }
+});
+
+/***/ }),
+/* 7 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(8);
+
+var core = __w_pdfjs_require__(9);
+
+var hide = __w_pdfjs_require__(10);
+
+var redefine = __w_pdfjs_require__(20);
+
+var ctx = __w_pdfjs_require__(26);
+
+var PROTOTYPE = 'prototype';
+
+var $export = function $export(type, name, source) {
+ var IS_FORCED = type & $export.F;
+ var IS_GLOBAL = type & $export.G;
+ var IS_STATIC = type & $export.S;
+ var IS_PROTO = type & $export.P;
+ var IS_BIND = type & $export.B;
+ var target = IS_GLOBAL ? global : IS_STATIC ? global[name] || (global[name] = {}) : (global[name] || {})[PROTOTYPE];
+ var exports = IS_GLOBAL ? core : core[name] || (core[name] = {});
+ var expProto = exports[PROTOTYPE] || (exports[PROTOTYPE] = {});
+ var key, own, out, exp;
+ if (IS_GLOBAL) source = name;
+
+ for (key in source) {
+ own = !IS_FORCED && target && target[key] !== undefined;
+ out = (own ? target : source)[key];
+ exp = IS_BIND && own ? ctx(out, global) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out;
+ if (target) redefine(target, key, out, type & $export.U);
+ if (exports[key] != out) hide(exports, key, exp);
+ if (IS_PROTO && expProto[key] != out) expProto[key] = out;
+ }
+};
+
+global.core = core;
+$export.F = 1;
+$export.G = 2;
+$export.S = 4;
+$export.P = 8;
+$export.B = 16;
+$export.W = 32;
+$export.U = 64;
+$export.R = 128;
+module.exports = $export;
+
+/***/ }),
+/* 8 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = module.exports = typeof window != 'undefined' && window.Math == Math ? window : typeof self != 'undefined' && self.Math == Math ? self : Function('return this')();
+if (typeof __g == 'number') __g = global;
+
+/***/ }),
+/* 9 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var core = module.exports = {
+ version: '2.6.9'
+};
+if (typeof __e == 'number') __e = core;
+
+/***/ }),
+/* 10 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var dP = __w_pdfjs_require__(11);
+
+var createDesc = __w_pdfjs_require__(19);
+
+module.exports = __w_pdfjs_require__(15) ? function (object, key, value) {
+ return dP.f(object, key, createDesc(1, value));
+} : function (object, key, value) {
+ object[key] = value;
+ return object;
+};
+
+/***/ }),
+/* 11 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var anObject = __w_pdfjs_require__(12);
+
+var IE8_DOM_DEFINE = __w_pdfjs_require__(14);
+
+var toPrimitive = __w_pdfjs_require__(18);
+
+var dP = Object.defineProperty;
+exports.f = __w_pdfjs_require__(15) ? Object.defineProperty : function defineProperty(O, P, Attributes) {
+ anObject(O);
+ P = toPrimitive(P, true);
+ anObject(Attributes);
+ if (IE8_DOM_DEFINE) try {
+ return dP(O, P, Attributes);
+ } catch (e) {}
+ if ('get' in Attributes || 'set' in Attributes) throw TypeError('Accessors not supported!');
+ if ('value' in Attributes) O[P] = Attributes.value;
+ return O;
+};
+
+/***/ }),
+/* 12 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(13);
+
+module.exports = function (it) {
+ if (!isObject(it)) throw TypeError(it + ' is not an object!');
+ return it;
+};
+
+/***/ }),
+/* 13 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+module.exports = function (it) {
+ return _typeof(it) === 'object' ? it !== null : typeof it === 'function';
+};
+
+/***/ }),
+/* 14 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = !__w_pdfjs_require__(15) && !__w_pdfjs_require__(16)(function () {
+ return Object.defineProperty(__w_pdfjs_require__(17)('div'), 'a', {
+ get: function get() {
+ return 7;
+ }
+ }).a != 7;
+});
+
+/***/ }),
+/* 15 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = !__w_pdfjs_require__(16)(function () {
+ return Object.defineProperty({}, 'a', {
+ get: function get() {
+ return 7;
+ }
+ }).a != 7;
+});
+
+/***/ }),
+/* 16 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (exec) {
+ try {
+ return !!exec();
+ } catch (e) {
+ return true;
+ }
+};
+
+/***/ }),
+/* 17 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(13);
+
+var document = __w_pdfjs_require__(8).document;
+
+var is = isObject(document) && isObject(document.createElement);
+
+module.exports = function (it) {
+ return is ? document.createElement(it) : {};
+};
+
+/***/ }),
+/* 18 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(13);
+
+module.exports = function (it, S) {
+ if (!isObject(it)) return it;
+ var fn, val;
+ if (S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it))) return val;
+ if (typeof (fn = it.valueOf) == 'function' && !isObject(val = fn.call(it))) return val;
+ if (!S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it))) return val;
+ throw TypeError("Can't convert object to primitive value");
+};
+
+/***/ }),
+/* 19 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (bitmap, value) {
+ return {
+ enumerable: !(bitmap & 1),
+ configurable: !(bitmap & 2),
+ writable: !(bitmap & 4),
+ value: value
+ };
+};
+
+/***/ }),
+/* 20 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(8);
+
+var hide = __w_pdfjs_require__(10);
+
+var has = __w_pdfjs_require__(21);
+
+var SRC = __w_pdfjs_require__(22)('src');
+
+var $toString = __w_pdfjs_require__(23);
+
+var TO_STRING = 'toString';
+var TPL = ('' + $toString).split(TO_STRING);
+
+__w_pdfjs_require__(9).inspectSource = function (it) {
+ return $toString.call(it);
+};
+
+(module.exports = function (O, key, val, safe) {
+ var isFunction = typeof val == 'function';
+ if (isFunction) has(val, 'name') || hide(val, 'name', key);
+ if (O[key] === val) return;
+ if (isFunction) has(val, SRC) || hide(val, SRC, O[key] ? '' + O[key] : TPL.join(String(key)));
+
+ if (O === global) {
+ O[key] = val;
+ } else if (!safe) {
+ delete O[key];
+ hide(O, key, val);
+ } else if (O[key]) {
+ O[key] = val;
+ } else {
+ hide(O, key, val);
+ }
+})(Function.prototype, TO_STRING, function toString() {
+ return typeof this == 'function' && this[SRC] || $toString.call(this);
+});
+
+/***/ }),
+/* 21 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var hasOwnProperty = {}.hasOwnProperty;
+
+module.exports = function (it, key) {
+ return hasOwnProperty.call(it, key);
+};
+
+/***/ }),
+/* 22 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var id = 0;
+var px = Math.random();
+
+module.exports = function (key) {
+ return 'Symbol('.concat(key === undefined ? '' : key, ')_', (++id + px).toString(36));
+};
+
+/***/ }),
+/* 23 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = __w_pdfjs_require__(24)('native-function-to-string', Function.toString);
+
+/***/ }),
+/* 24 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var core = __w_pdfjs_require__(9);
+
+var global = __w_pdfjs_require__(8);
+
+var SHARED = '__core-js_shared__';
+var store = global[SHARED] || (global[SHARED] = {});
+(module.exports = function (key, value) {
+ return store[key] || (store[key] = value !== undefined ? value : {});
+})('versions', []).push({
+ version: core.version,
+ mode: __w_pdfjs_require__(25) ? 'pure' : 'global',
+ copyright: '© 2019 Denis Pushkarev (zloirock.ru)'
+});
+
+/***/ }),
+/* 25 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = false;
+
+/***/ }),
+/* 26 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var aFunction = __w_pdfjs_require__(27);
+
+module.exports = function (fn, that, length) {
+ aFunction(fn);
+ if (that === undefined) return fn;
+
+ switch (length) {
+ case 1:
+ return function (a) {
+ return fn.call(that, a);
+ };
+
+ case 2:
+ return function (a, b) {
+ return fn.call(that, a, b);
+ };
+
+ case 3:
+ return function (a, b, c) {
+ return fn.call(that, a, b, c);
+ };
+ }
+
+ return function () {
+ return fn.apply(that, arguments);
+ };
+};
+
+/***/ }),
+/* 27 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (it) {
+ if (typeof it != 'function') throw TypeError(it + ' is not a function!');
+ return it;
+};
+
+/***/ }),
+/* 28 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toInteger = __w_pdfjs_require__(29);
+
+var min = Math.min;
+
+module.exports = function (it) {
+ return it > 0 ? min(toInteger(it), 0x1fffffffffffff) : 0;
+};
+
+/***/ }),
+/* 29 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var ceil = Math.ceil;
+var floor = Math.floor;
+
+module.exports = function (it) {
+ return isNaN(it = +it) ? 0 : (it > 0 ? floor : ceil)(it);
+};
+
+/***/ }),
+/* 30 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isRegExp = __w_pdfjs_require__(31);
+
+var defined = __w_pdfjs_require__(34);
+
+module.exports = function (that, searchString, NAME) {
+ if (isRegExp(searchString)) throw TypeError('String#' + NAME + " doesn't accept regex!");
+ return String(defined(that));
+};
+
+/***/ }),
+/* 31 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(13);
+
+var cof = __w_pdfjs_require__(32);
+
+var MATCH = __w_pdfjs_require__(33)('match');
+
+module.exports = function (it) {
+ var isRegExp;
+ return isObject(it) && ((isRegExp = it[MATCH]) !== undefined ? !!isRegExp : cof(it) == 'RegExp');
+};
+
+/***/ }),
+/* 32 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toString = {}.toString;
+
+module.exports = function (it) {
+ return toString.call(it).slice(8, -1);
+};
+
+/***/ }),
+/* 33 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var store = __w_pdfjs_require__(24)('wks');
+
+var uid = __w_pdfjs_require__(22);
+
+var _Symbol = __w_pdfjs_require__(8).Symbol;
+
+var USE_SYMBOL = typeof _Symbol == 'function';
+
+var $exports = module.exports = function (name) {
+ return store[name] || (store[name] = USE_SYMBOL && _Symbol[name] || (USE_SYMBOL ? _Symbol : uid)('Symbol.' + name));
+};
+
+$exports.store = store;
+
+/***/ }),
+/* 34 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (it) {
+ if (it == undefined) throw TypeError("Can't call method on " + it);
+ return it;
+};
+
+/***/ }),
+/* 35 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var MATCH = __w_pdfjs_require__(33)('match');
+
+module.exports = function (KEY) {
+ var re = /./;
+
+ try {
+ '/./'[KEY](re);
+ } catch (e) {
+ try {
+ re[MATCH] = false;
+ return !'/./'[KEY](re);
+ } catch (f) {}
+ }
+
+ return true;
+};
+
+/***/ }),
+/* 36 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(37);
+
+module.exports = __w_pdfjs_require__(9).String.endsWith;
+
+/***/ }),
+/* 37 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+var toLength = __w_pdfjs_require__(28);
+
+var context = __w_pdfjs_require__(30);
+
+var ENDS_WITH = 'endsWith';
+var $endsWith = ''[ENDS_WITH];
+$export($export.P + $export.F * __w_pdfjs_require__(35)(ENDS_WITH), 'String', {
+ endsWith: function endsWith(searchString) {
+ var that = context(this, searchString, ENDS_WITH);
+ var endPosition = arguments.length > 1 ? arguments[1] : undefined;
+ var len = toLength(that.length);
+ var end = endPosition === undefined ? len : Math.min(toLength(endPosition), len);
+ var search = String(searchString);
+ return $endsWith ? $endsWith.call(that, search, end) : that.slice(end - search.length, end) === search;
+ }
+});
+
+/***/ }),
+/* 38 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(39);
+
+module.exports = __w_pdfjs_require__(9).String.includes;
+
+/***/ }),
+/* 39 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+var context = __w_pdfjs_require__(30);
+
+var INCLUDES = 'includes';
+$export($export.P + $export.F * __w_pdfjs_require__(35)(INCLUDES), 'String', {
+ includes: function includes(searchString) {
+ return !!~context(this, searchString, INCLUDES).indexOf(searchString, arguments.length > 1 ? arguments[1] : undefined);
+ }
+});
+
+/***/ }),
+/* 40 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(41);
+
+module.exports = __w_pdfjs_require__(9).Array.includes;
+
+/***/ }),
+/* 41 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+var $includes = __w_pdfjs_require__(42)(true);
+
+$export($export.P, 'Array', {
+ includes: function includes(el) {
+ return $includes(this, el, arguments.length > 1 ? arguments[1] : undefined);
+ }
+});
+
+__w_pdfjs_require__(46)('includes');
+
+/***/ }),
+/* 42 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toIObject = __w_pdfjs_require__(43);
+
+var toLength = __w_pdfjs_require__(28);
+
+var toAbsoluteIndex = __w_pdfjs_require__(45);
+
+module.exports = function (IS_INCLUDES) {
+ return function ($this, el, fromIndex) {
+ var O = toIObject($this);
+ var length = toLength(O.length);
+ var index = toAbsoluteIndex(fromIndex, length);
+ var value;
+ if (IS_INCLUDES && el != el) while (length > index) {
+ value = O[index++];
+ if (value != value) return true;
+ } else for (; length > index; index++) {
+ if (IS_INCLUDES || index in O) {
+ if (O[index] === el) return IS_INCLUDES || index || 0;
+ }
+ }
+ return !IS_INCLUDES && -1;
+ };
+};
+
+/***/ }),
+/* 43 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var IObject = __w_pdfjs_require__(44);
+
+var defined = __w_pdfjs_require__(34);
+
+module.exports = function (it) {
+ return IObject(defined(it));
+};
+
+/***/ }),
+/* 44 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var cof = __w_pdfjs_require__(32);
+
+module.exports = Object('z').propertyIsEnumerable(0) ? Object : function (it) {
+ return cof(it) == 'String' ? it.split('') : Object(it);
+};
+
+/***/ }),
+/* 45 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toInteger = __w_pdfjs_require__(29);
+
+var max = Math.max;
+var min = Math.min;
+
+module.exports = function (index, length) {
+ index = toInteger(index);
+ return index < 0 ? max(index + length, 0) : min(index, length);
+};
+
+/***/ }),
+/* 46 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var UNSCOPABLES = __w_pdfjs_require__(33)('unscopables');
+
+var ArrayProto = Array.prototype;
+if (ArrayProto[UNSCOPABLES] == undefined) __w_pdfjs_require__(10)(ArrayProto, UNSCOPABLES, {});
+
+module.exports = function (key) {
+ ArrayProto[UNSCOPABLES][key] = true;
+};
+
+/***/ }),
+/* 47 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(48);
+
+__w_pdfjs_require__(63);
+
+module.exports = __w_pdfjs_require__(9).Array.from;
+
+/***/ }),
+/* 48 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $at = __w_pdfjs_require__(49)(true);
+
+__w_pdfjs_require__(50)(String, 'String', function (iterated) {
+ this._t = String(iterated);
+ this._i = 0;
+}, function () {
+ var O = this._t;
+ var index = this._i;
+ var point;
+ if (index >= O.length) return {
+ value: undefined,
+ done: true
+ };
+ point = $at(O, index);
+ this._i += point.length;
+ return {
+ value: point,
+ done: false
+ };
+});
+
+/***/ }),
+/* 49 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toInteger = __w_pdfjs_require__(29);
+
+var defined = __w_pdfjs_require__(34);
+
+module.exports = function (TO_STRING) {
+ return function (that, pos) {
+ var s = String(defined(that));
+ var i = toInteger(pos);
+ var l = s.length;
+ var a, b;
+ if (i < 0 || i >= l) return TO_STRING ? '' : undefined;
+ a = s.charCodeAt(i);
+ return a < 0xd800 || a > 0xdbff || i + 1 === l || (b = s.charCodeAt(i + 1)) < 0xdc00 || b > 0xdfff ? TO_STRING ? s.charAt(i) : a : TO_STRING ? s.slice(i, i + 2) : (a - 0xd800 << 10) + (b - 0xdc00) + 0x10000;
+ };
+};
+
+/***/ }),
+/* 50 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var LIBRARY = __w_pdfjs_require__(25);
+
+var $export = __w_pdfjs_require__(7);
+
+var redefine = __w_pdfjs_require__(20);
+
+var hide = __w_pdfjs_require__(10);
+
+var Iterators = __w_pdfjs_require__(51);
+
+var $iterCreate = __w_pdfjs_require__(52);
+
+var setToStringTag = __w_pdfjs_require__(60);
+
+var getPrototypeOf = __w_pdfjs_require__(61);
+
+var ITERATOR = __w_pdfjs_require__(33)('iterator');
+
+var BUGGY = !([].keys && 'next' in [].keys());
+var FF_ITERATOR = '@@iterator';
+var KEYS = 'keys';
+var VALUES = 'values';
+
+var returnThis = function returnThis() {
+ return this;
+};
+
+module.exports = function (Base, NAME, Constructor, next, DEFAULT, IS_SET, FORCED) {
+ $iterCreate(Constructor, NAME, next);
+
+ var getMethod = function getMethod(kind) {
+ if (!BUGGY && kind in proto) return proto[kind];
+
+ switch (kind) {
+ case KEYS:
+ return function keys() {
+ return new Constructor(this, kind);
+ };
+
+ case VALUES:
+ return function values() {
+ return new Constructor(this, kind);
+ };
+ }
+
+ return function entries() {
+ return new Constructor(this, kind);
+ };
+ };
+
+ var TAG = NAME + ' Iterator';
+ var DEF_VALUES = DEFAULT == VALUES;
+ var VALUES_BUG = false;
+ var proto = Base.prototype;
+ var $native = proto[ITERATOR] || proto[FF_ITERATOR] || DEFAULT && proto[DEFAULT];
+ var $default = $native || getMethod(DEFAULT);
+ var $entries = DEFAULT ? !DEF_VALUES ? $default : getMethod('entries') : undefined;
+ var $anyNative = NAME == 'Array' ? proto.entries || $native : $native;
+ var methods, key, IteratorPrototype;
+
+ if ($anyNative) {
+ IteratorPrototype = getPrototypeOf($anyNative.call(new Base()));
+
+ if (IteratorPrototype !== Object.prototype && IteratorPrototype.next) {
+ setToStringTag(IteratorPrototype, TAG, true);
+ if (!LIBRARY && typeof IteratorPrototype[ITERATOR] != 'function') hide(IteratorPrototype, ITERATOR, returnThis);
+ }
+ }
+
+ if (DEF_VALUES && $native && $native.name !== VALUES) {
+ VALUES_BUG = true;
+
+ $default = function values() {
+ return $native.call(this);
+ };
+ }
+
+ if ((!LIBRARY || FORCED) && (BUGGY || VALUES_BUG || !proto[ITERATOR])) {
+ hide(proto, ITERATOR, $default);
+ }
+
+ Iterators[NAME] = $default;
+ Iterators[TAG] = returnThis;
+
+ if (DEFAULT) {
+ methods = {
+ values: DEF_VALUES ? $default : getMethod(VALUES),
+ keys: IS_SET ? $default : getMethod(KEYS),
+ entries: $entries
+ };
+ if (FORCED) for (key in methods) {
+ if (!(key in proto)) redefine(proto, key, methods[key]);
+ } else $export($export.P + $export.F * (BUGGY || VALUES_BUG), NAME, methods);
+ }
+
+ return methods;
+};
+
+/***/ }),
+/* 51 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = {};
+
+/***/ }),
+/* 52 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var create = __w_pdfjs_require__(53);
+
+var descriptor = __w_pdfjs_require__(19);
+
+var setToStringTag = __w_pdfjs_require__(60);
+
+var IteratorPrototype = {};
+
+__w_pdfjs_require__(10)(IteratorPrototype, __w_pdfjs_require__(33)('iterator'), function () {
+ return this;
+});
+
+module.exports = function (Constructor, NAME, next) {
+ Constructor.prototype = create(IteratorPrototype, {
+ next: descriptor(1, next)
+ });
+ setToStringTag(Constructor, NAME + ' Iterator');
+};
+
+/***/ }),
+/* 53 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var anObject = __w_pdfjs_require__(12);
+
+var dPs = __w_pdfjs_require__(54);
+
+var enumBugKeys = __w_pdfjs_require__(58);
+
+var IE_PROTO = __w_pdfjs_require__(57)('IE_PROTO');
+
+var Empty = function Empty() {};
+
+var PROTOTYPE = 'prototype';
+
+var _createDict = function createDict() {
+ var iframe = __w_pdfjs_require__(17)('iframe');
+
+ var i = enumBugKeys.length;
+ var lt = '<';
+ var gt = '>';
+ var iframeDocument;
+ iframe.style.display = 'none';
+
+ __w_pdfjs_require__(59).appendChild(iframe);
+
+ iframe.src = 'javascript:';
+ iframeDocument = iframe.contentWindow.document;
+ iframeDocument.open();
+ iframeDocument.write(lt + 'script' + gt + 'document.F=Object' + lt + '/script' + gt);
+ iframeDocument.close();
+ _createDict = iframeDocument.F;
+
+ while (i--) {
+ delete _createDict[PROTOTYPE][enumBugKeys[i]];
+ }
+
+ return _createDict();
+};
+
+module.exports = Object.create || function create(O, Properties) {
+ var result;
+
+ if (O !== null) {
+ Empty[PROTOTYPE] = anObject(O);
+ result = new Empty();
+ Empty[PROTOTYPE] = null;
+ result[IE_PROTO] = O;
+ } else result = _createDict();
+
+ return Properties === undefined ? result : dPs(result, Properties);
+};
+
+/***/ }),
+/* 54 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var dP = __w_pdfjs_require__(11);
+
+var anObject = __w_pdfjs_require__(12);
+
+var getKeys = __w_pdfjs_require__(55);
+
+module.exports = __w_pdfjs_require__(15) ? Object.defineProperties : function defineProperties(O, Properties) {
+ anObject(O);
+ var keys = getKeys(Properties);
+ var length = keys.length;
+ var i = 0;
+ var P;
+
+ while (length > i) {
+ dP.f(O, P = keys[i++], Properties[P]);
+ }
+
+ return O;
+};
+
+/***/ }),
+/* 55 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $keys = __w_pdfjs_require__(56);
+
+var enumBugKeys = __w_pdfjs_require__(58);
+
+module.exports = Object.keys || function keys(O) {
+ return $keys(O, enumBugKeys);
+};
+
+/***/ }),
+/* 56 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var has = __w_pdfjs_require__(21);
+
+var toIObject = __w_pdfjs_require__(43);
+
+var arrayIndexOf = __w_pdfjs_require__(42)(false);
+
+var IE_PROTO = __w_pdfjs_require__(57)('IE_PROTO');
+
+module.exports = function (object, names) {
+ var O = toIObject(object);
+ var i = 0;
+ var result = [];
+ var key;
+
+ for (key in O) {
+ if (key != IE_PROTO) has(O, key) && result.push(key);
+ }
+
+ while (names.length > i) {
+ if (has(O, key = names[i++])) {
+ ~arrayIndexOf(result, key) || result.push(key);
+ }
+ }
+
+ return result;
+};
+
+/***/ }),
+/* 57 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var shared = __w_pdfjs_require__(24)('keys');
+
+var uid = __w_pdfjs_require__(22);
+
+module.exports = function (key) {
+ return shared[key] || (shared[key] = uid(key));
+};
+
+/***/ }),
+/* 58 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = 'constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf'.split(',');
+
+/***/ }),
+/* 59 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var document = __w_pdfjs_require__(8).document;
+
+module.exports = document && document.documentElement;
+
+/***/ }),
+/* 60 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var def = __w_pdfjs_require__(11).f;
+
+var has = __w_pdfjs_require__(21);
+
+var TAG = __w_pdfjs_require__(33)('toStringTag');
+
+module.exports = function (it, tag, stat) {
+ if (it && !has(it = stat ? it : it.prototype, TAG)) def(it, TAG, {
+ configurable: true,
+ value: tag
+ });
+};
+
+/***/ }),
+/* 61 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var has = __w_pdfjs_require__(21);
+
+var toObject = __w_pdfjs_require__(62);
+
+var IE_PROTO = __w_pdfjs_require__(57)('IE_PROTO');
+
+var ObjectProto = Object.prototype;
+
+module.exports = Object.getPrototypeOf || function (O) {
+ O = toObject(O);
+ if (has(O, IE_PROTO)) return O[IE_PROTO];
+
+ if (typeof O.constructor == 'function' && O instanceof O.constructor) {
+ return O.constructor.prototype;
+ }
+
+ return O instanceof Object ? ObjectProto : null;
+};
+
+/***/ }),
+/* 62 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var defined = __w_pdfjs_require__(34);
+
+module.exports = function (it) {
+ return Object(defined(it));
+};
+
+/***/ }),
+/* 63 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var ctx = __w_pdfjs_require__(26);
+
+var $export = __w_pdfjs_require__(7);
+
+var toObject = __w_pdfjs_require__(62);
+
+var call = __w_pdfjs_require__(64);
+
+var isArrayIter = __w_pdfjs_require__(65);
+
+var toLength = __w_pdfjs_require__(28);
+
+var createProperty = __w_pdfjs_require__(66);
+
+var getIterFn = __w_pdfjs_require__(67);
+
+$export($export.S + $export.F * !__w_pdfjs_require__(69)(function (iter) {
+ Array.from(iter);
+}), 'Array', {
+ from: function from(arrayLike) {
+ var O = toObject(arrayLike);
+ var C = typeof this == 'function' ? this : Array;
+ var aLen = arguments.length;
+ var mapfn = aLen > 1 ? arguments[1] : undefined;
+ var mapping = mapfn !== undefined;
+ var index = 0;
+ var iterFn = getIterFn(O);
+ var length, result, step, iterator;
+ if (mapping) mapfn = ctx(mapfn, aLen > 2 ? arguments[2] : undefined, 2);
+
+ if (iterFn != undefined && !(C == Array && isArrayIter(iterFn))) {
+ for (iterator = iterFn.call(O), result = new C(); !(step = iterator.next()).done; index++) {
+ createProperty(result, index, mapping ? call(iterator, mapfn, [step.value, index], true) : step.value);
+ }
+ } else {
+ length = toLength(O.length);
+
+ for (result = new C(length); length > index; index++) {
+ createProperty(result, index, mapping ? mapfn(O[index], index) : O[index]);
+ }
+ }
+
+ result.length = index;
+ return result;
+ }
+});
+
+/***/ }),
+/* 64 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var anObject = __w_pdfjs_require__(12);
+
+module.exports = function (iterator, fn, value, entries) {
+ try {
+ return entries ? fn(anObject(value)[0], value[1]) : fn(value);
+ } catch (e) {
+ var ret = iterator['return'];
+ if (ret !== undefined) anObject(ret.call(iterator));
+ throw e;
+ }
+};
+
+/***/ }),
+/* 65 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var Iterators = __w_pdfjs_require__(51);
+
+var ITERATOR = __w_pdfjs_require__(33)('iterator');
+
+var ArrayProto = Array.prototype;
+
+module.exports = function (it) {
+ return it !== undefined && (Iterators.Array === it || ArrayProto[ITERATOR] === it);
+};
+
+/***/ }),
+/* 66 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $defineProperty = __w_pdfjs_require__(11);
+
+var createDesc = __w_pdfjs_require__(19);
+
+module.exports = function (object, index, value) {
+ if (index in object) $defineProperty.f(object, index, createDesc(0, value));else object[index] = value;
+};
+
+/***/ }),
+/* 67 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var classof = __w_pdfjs_require__(68);
+
+var ITERATOR = __w_pdfjs_require__(33)('iterator');
+
+var Iterators = __w_pdfjs_require__(51);
+
+module.exports = __w_pdfjs_require__(9).getIteratorMethod = function (it) {
+ if (it != undefined) return it[ITERATOR] || it['@@iterator'] || Iterators[classof(it)];
+};
+
+/***/ }),
+/* 68 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var cof = __w_pdfjs_require__(32);
+
+var TAG = __w_pdfjs_require__(33)('toStringTag');
+
+var ARG = cof(function () {
+ return arguments;
+}()) == 'Arguments';
+
+var tryGet = function tryGet(it, key) {
+ try {
+ return it[key];
+ } catch (e) {}
+};
+
+module.exports = function (it) {
+ var O, T, B;
+ return it === undefined ? 'Undefined' : it === null ? 'Null' : typeof (T = tryGet(O = Object(it), TAG)) == 'string' ? T : ARG ? cof(O) : (B = cof(O)) == 'Object' && typeof O.callee == 'function' ? 'Arguments' : B;
+};
+
+/***/ }),
+/* 69 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var ITERATOR = __w_pdfjs_require__(33)('iterator');
+
+var SAFE_CLOSING = false;
+
+try {
+ var riter = [7][ITERATOR]();
+
+ riter['return'] = function () {
+ SAFE_CLOSING = true;
+ };
+
+ Array.from(riter, function () {
+ throw 2;
+ });
+} catch (e) {}
+
+module.exports = function (exec, skipClosing) {
+ if (!skipClosing && !SAFE_CLOSING) return false;
+ var safe = false;
+
+ try {
+ var arr = [7];
+ var iter = arr[ITERATOR]();
+
+ iter.next = function () {
+ return {
+ done: safe = true
+ };
+ };
+
+ arr[ITERATOR] = function () {
+ return iter;
+ };
+
+ exec(arr);
+ } catch (e) {}
+
+ return safe;
+};
+
+/***/ }),
+/* 70 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(71);
+
+module.exports = __w_pdfjs_require__(9).Object.assign;
+
+/***/ }),
+/* 71 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+$export($export.S + $export.F, 'Object', {
+ assign: __w_pdfjs_require__(72)
+});
+
+/***/ }),
+/* 72 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var DESCRIPTORS = __w_pdfjs_require__(15);
+
+var getKeys = __w_pdfjs_require__(55);
+
+var gOPS = __w_pdfjs_require__(73);
+
+var pIE = __w_pdfjs_require__(74);
+
+var toObject = __w_pdfjs_require__(62);
+
+var IObject = __w_pdfjs_require__(44);
+
+var $assign = Object.assign;
+module.exports = !$assign || __w_pdfjs_require__(16)(function () {
+ var A = {};
+ var B = {};
+ var S = Symbol();
+ var K = 'abcdefghijklmnopqrst';
+ A[S] = 7;
+ K.split('').forEach(function (k) {
+ B[k] = k;
+ });
+ return $assign({}, A)[S] != 7 || Object.keys($assign({}, B)).join('') != K;
+}) ? function assign(target, source) {
+ var T = toObject(target);
+ var aLen = arguments.length;
+ var index = 1;
+ var getSymbols = gOPS.f;
+ var isEnum = pIE.f;
+
+ while (aLen > index) {
+ var S = IObject(arguments[index++]);
+ var keys = getSymbols ? getKeys(S).concat(getSymbols(S)) : getKeys(S);
+ var length = keys.length;
+ var j = 0;
+ var key;
+
+ while (length > j) {
+ key = keys[j++];
+ if (!DESCRIPTORS || isEnum.call(S, key)) T[key] = S[key];
+ }
+ }
+
+ return T;
+} : $assign;
+
+/***/ }),
+/* 73 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+exports.f = Object.getOwnPropertySymbols;
+
+/***/ }),
+/* 74 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+exports.f = {}.propertyIsEnumerable;
+
+/***/ }),
+/* 75 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(76);
+
+module.exports = __w_pdfjs_require__(9).Math.log2;
+
+/***/ }),
+/* 76 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+$export($export.S, 'Math', {
+ log2: function log2(x) {
+ return Math.log(x) / Math.LN2;
+ }
+});
+
+/***/ }),
+/* 77 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(78);
+
+module.exports = __w_pdfjs_require__(9).Number.isNaN;
+
+/***/ }),
+/* 78 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+$export($export.S, 'Number', {
+ isNaN: function isNaN(number) {
+ return number != number;
+ }
+});
+
+/***/ }),
+/* 79 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(80);
+
+module.exports = __w_pdfjs_require__(9).Number.isInteger;
+
+/***/ }),
+/* 80 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+$export($export.S, 'Number', {
+ isInteger: __w_pdfjs_require__(81)
+});
+
+/***/ }),
+/* 81 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(13);
+
+var floor = Math.floor;
+
+module.exports = function isInteger(it) {
+ return !isObject(it) && isFinite(it) && floor(it) === it;
+};
+
+/***/ }),
+/* 82 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(83);
+
+__w_pdfjs_require__(48);
+
+__w_pdfjs_require__(84);
+
+__w_pdfjs_require__(87);
+
+__w_pdfjs_require__(100);
+
+__w_pdfjs_require__(101);
+
+module.exports = __w_pdfjs_require__(9).Promise;
+
+/***/ }),
+/* 83 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var classof = __w_pdfjs_require__(68);
+
+var test = {};
+test[__w_pdfjs_require__(33)('toStringTag')] = 'z';
+
+if (test + '' != '[object z]') {
+ __w_pdfjs_require__(20)(Object.prototype, 'toString', function toString() {
+ return '[object ' + classof(this) + ']';
+ }, true);
+}
+
+/***/ }),
+/* 84 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $iterators = __w_pdfjs_require__(85);
+
+var getKeys = __w_pdfjs_require__(55);
+
+var redefine = __w_pdfjs_require__(20);
+
+var global = __w_pdfjs_require__(8);
+
+var hide = __w_pdfjs_require__(10);
+
+var Iterators = __w_pdfjs_require__(51);
+
+var wks = __w_pdfjs_require__(33);
+
+var ITERATOR = wks('iterator');
+var TO_STRING_TAG = wks('toStringTag');
+var ArrayValues = Iterators.Array;
+var DOMIterables = {
+ CSSRuleList: true,
+ CSSStyleDeclaration: false,
+ CSSValueList: false,
+ ClientRectList: false,
+ DOMRectList: false,
+ DOMStringList: false,
+ DOMTokenList: true,
+ DataTransferItemList: false,
+ FileList: false,
+ HTMLAllCollection: false,
+ HTMLCollection: false,
+ HTMLFormElement: false,
+ HTMLSelectElement: false,
+ MediaList: true,
+ MimeTypeArray: false,
+ NamedNodeMap: false,
+ NodeList: true,
+ PaintRequestList: false,
+ Plugin: false,
+ PluginArray: false,
+ SVGLengthList: false,
+ SVGNumberList: false,
+ SVGPathSegList: false,
+ SVGPointList: false,
+ SVGStringList: false,
+ SVGTransformList: false,
+ SourceBufferList: false,
+ StyleSheetList: true,
+ TextTrackCueList: false,
+ TextTrackList: false,
+ TouchList: false
+};
+
+for (var collections = getKeys(DOMIterables), i = 0; i < collections.length; i++) {
+ var NAME = collections[i];
+ var explicit = DOMIterables[NAME];
+ var Collection = global[NAME];
+ var proto = Collection && Collection.prototype;
+ var key;
+
+ if (proto) {
+ if (!proto[ITERATOR]) hide(proto, ITERATOR, ArrayValues);
+ if (!proto[TO_STRING_TAG]) hide(proto, TO_STRING_TAG, NAME);
+ Iterators[NAME] = ArrayValues;
+ if (explicit) for (key in $iterators) {
+ if (!proto[key]) redefine(proto, key, $iterators[key], true);
+ }
+ }
+}
+
+/***/ }),
+/* 85 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var addToUnscopables = __w_pdfjs_require__(46);
+
+var step = __w_pdfjs_require__(86);
+
+var Iterators = __w_pdfjs_require__(51);
+
+var toIObject = __w_pdfjs_require__(43);
+
+module.exports = __w_pdfjs_require__(50)(Array, 'Array', function (iterated, kind) {
+ this._t = toIObject(iterated);
+ this._i = 0;
+ this._k = kind;
+}, function () {
+ var O = this._t;
+ var kind = this._k;
+ var index = this._i++;
+
+ if (!O || index >= O.length) {
+ this._t = undefined;
+ return step(1);
+ }
+
+ if (kind == 'keys') return step(0, index);
+ if (kind == 'values') return step(0, O[index]);
+ return step(0, [index, O[index]]);
+}, 'values');
+Iterators.Arguments = Iterators.Array;
+addToUnscopables('keys');
+addToUnscopables('values');
+addToUnscopables('entries');
+
+/***/ }),
+/* 86 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (done, value) {
+ return {
+ value: value,
+ done: !!done
+ };
+};
+
+/***/ }),
+/* 87 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var LIBRARY = __w_pdfjs_require__(25);
+
+var global = __w_pdfjs_require__(8);
+
+var ctx = __w_pdfjs_require__(26);
+
+var classof = __w_pdfjs_require__(68);
+
+var $export = __w_pdfjs_require__(7);
+
+var isObject = __w_pdfjs_require__(13);
+
+var aFunction = __w_pdfjs_require__(27);
+
+var anInstance = __w_pdfjs_require__(88);
+
+var forOf = __w_pdfjs_require__(89);
+
+var speciesConstructor = __w_pdfjs_require__(90);
+
+var task = __w_pdfjs_require__(91).set;
+
+var microtask = __w_pdfjs_require__(93)();
+
+var newPromiseCapabilityModule = __w_pdfjs_require__(94);
+
+var perform = __w_pdfjs_require__(95);
+
+var userAgent = __w_pdfjs_require__(96);
+
+var promiseResolve = __w_pdfjs_require__(97);
+
+var PROMISE = 'Promise';
+var TypeError = global.TypeError;
+var process = global.process;
+var versions = process && process.versions;
+var v8 = versions && versions.v8 || '';
+var $Promise = global[PROMISE];
+var isNode = classof(process) == 'process';
+
+var empty = function empty() {};
+
+var Internal, newGenericPromiseCapability, OwnPromiseCapability, Wrapper;
+var newPromiseCapability = newGenericPromiseCapability = newPromiseCapabilityModule.f;
+var USE_NATIVE = !!function () {
+ try {
+ var promise = $Promise.resolve(1);
+
+ var FakePromise = (promise.constructor = {})[__w_pdfjs_require__(33)('species')] = function (exec) {
+ exec(empty, empty);
+ };
+
+ return (isNode || typeof PromiseRejectionEvent == 'function') && promise.then(empty) instanceof FakePromise && v8.indexOf('6.6') !== 0 && userAgent.indexOf('Chrome/66') === -1;
+ } catch (e) {}
+}();
+
+var isThenable = function isThenable(it) {
+ var then;
+ return isObject(it) && typeof (then = it.then) == 'function' ? then : false;
+};
+
+var notify = function notify(promise, isReject) {
+ if (promise._n) return;
+ promise._n = true;
+ var chain = promise._c;
+ microtask(function () {
+ var value = promise._v;
+ var ok = promise._s == 1;
+ var i = 0;
+
+ var run = function run(reaction) {
+ var handler = ok ? reaction.ok : reaction.fail;
+ var resolve = reaction.resolve;
+ var reject = reaction.reject;
+ var domain = reaction.domain;
+ var result, then, exited;
+
+ try {
+ if (handler) {
+ if (!ok) {
+ if (promise._h == 2) onHandleUnhandled(promise);
+ promise._h = 1;
+ }
+
+ if (handler === true) result = value;else {
+ if (domain) domain.enter();
+ result = handler(value);
+
+ if (domain) {
+ domain.exit();
+ exited = true;
+ }
+ }
+
+ if (result === reaction.promise) {
+ reject(TypeError('Promise-chain cycle'));
+ } else if (then = isThenable(result)) {
+ then.call(result, resolve, reject);
+ } else resolve(result);
+ } else reject(value);
+ } catch (e) {
+ if (domain && !exited) domain.exit();
+ reject(e);
+ }
+ };
+
+ while (chain.length > i) {
+ run(chain[i++]);
+ }
+
+ promise._c = [];
+ promise._n = false;
+ if (isReject && !promise._h) onUnhandled(promise);
+ });
+};
+
+var onUnhandled = function onUnhandled(promise) {
+ task.call(global, function () {
+ var value = promise._v;
+ var unhandled = isUnhandled(promise);
+ var result, handler, console;
+
+ if (unhandled) {
+ result = perform(function () {
+ if (isNode) {
+ process.emit('unhandledRejection', value, promise);
+ } else if (handler = global.onunhandledrejection) {
+ handler({
+ promise: promise,
+ reason: value
+ });
+ } else if ((console = global.console) && console.error) {
+ console.error('Unhandled promise rejection', value);
+ }
+ });
+ promise._h = isNode || isUnhandled(promise) ? 2 : 1;
+ }
+
+ promise._a = undefined;
+ if (unhandled && result.e) throw result.v;
+ });
+};
+
+var isUnhandled = function isUnhandled(promise) {
+ return promise._h !== 1 && (promise._a || promise._c).length === 0;
+};
+
+var onHandleUnhandled = function onHandleUnhandled(promise) {
+ task.call(global, function () {
+ var handler;
+
+ if (isNode) {
+ process.emit('rejectionHandled', promise);
+ } else if (handler = global.onrejectionhandled) {
+ handler({
+ promise: promise,
+ reason: promise._v
+ });
+ }
+ });
+};
+
+var $reject = function $reject(value) {
+ var promise = this;
+ if (promise._d) return;
+ promise._d = true;
+ promise = promise._w || promise;
+ promise._v = value;
+ promise._s = 2;
+ if (!promise._a) promise._a = promise._c.slice();
+ notify(promise, true);
+};
+
+var $resolve = function $resolve(value) {
+ var promise = this;
+ var then;
+ if (promise._d) return;
+ promise._d = true;
+ promise = promise._w || promise;
+
+ try {
+ if (promise === value) throw TypeError("Promise can't be resolved itself");
+
+ if (then = isThenable(value)) {
+ microtask(function () {
+ var wrapper = {
+ _w: promise,
+ _d: false
+ };
+
+ try {
+ then.call(value, ctx($resolve, wrapper, 1), ctx($reject, wrapper, 1));
+ } catch (e) {
+ $reject.call(wrapper, e);
+ }
+ });
+ } else {
+ promise._v = value;
+ promise._s = 1;
+ notify(promise, false);
+ }
+ } catch (e) {
+ $reject.call({
+ _w: promise,
+ _d: false
+ }, e);
+ }
+};
+
+if (!USE_NATIVE) {
+ $Promise = function Promise(executor) {
+ anInstance(this, $Promise, PROMISE, '_h');
+ aFunction(executor);
+ Internal.call(this);
+
+ try {
+ executor(ctx($resolve, this, 1), ctx($reject, this, 1));
+ } catch (err) {
+ $reject.call(this, err);
+ }
+ };
+
+ Internal = function Promise(executor) {
+ this._c = [];
+ this._a = undefined;
+ this._s = 0;
+ this._d = false;
+ this._v = undefined;
+ this._h = 0;
+ this._n = false;
+ };
+
+ Internal.prototype = __w_pdfjs_require__(98)($Promise.prototype, {
+ then: function then(onFulfilled, onRejected) {
+ var reaction = newPromiseCapability(speciesConstructor(this, $Promise));
+ reaction.ok = typeof onFulfilled == 'function' ? onFulfilled : true;
+ reaction.fail = typeof onRejected == 'function' && onRejected;
+ reaction.domain = isNode ? process.domain : undefined;
+
+ this._c.push(reaction);
+
+ if (this._a) this._a.push(reaction);
+ if (this._s) notify(this, false);
+ return reaction.promise;
+ },
+ 'catch': function _catch(onRejected) {
+ return this.then(undefined, onRejected);
+ }
+ });
+
+ OwnPromiseCapability = function OwnPromiseCapability() {
+ var promise = new Internal();
+ this.promise = promise;
+ this.resolve = ctx($resolve, promise, 1);
+ this.reject = ctx($reject, promise, 1);
+ };
+
+ newPromiseCapabilityModule.f = newPromiseCapability = function newPromiseCapability(C) {
+ return C === $Promise || C === Wrapper ? new OwnPromiseCapability(C) : newGenericPromiseCapability(C);
+ };
+}
+
+$export($export.G + $export.W + $export.F * !USE_NATIVE, {
+ Promise: $Promise
+});
+
+__w_pdfjs_require__(60)($Promise, PROMISE);
+
+__w_pdfjs_require__(99)(PROMISE);
+
+Wrapper = __w_pdfjs_require__(9)[PROMISE];
+$export($export.S + $export.F * !USE_NATIVE, PROMISE, {
+ reject: function reject(r) {
+ var capability = newPromiseCapability(this);
+ var $$reject = capability.reject;
+ $$reject(r);
+ return capability.promise;
+ }
+});
+$export($export.S + $export.F * (LIBRARY || !USE_NATIVE), PROMISE, {
+ resolve: function resolve(x) {
+ return promiseResolve(LIBRARY && this === Wrapper ? $Promise : this, x);
+ }
+});
+$export($export.S + $export.F * !(USE_NATIVE && __w_pdfjs_require__(69)(function (iter) {
+ $Promise.all(iter)['catch'](empty);
+})), PROMISE, {
+ all: function all(iterable) {
+ var C = this;
+ var capability = newPromiseCapability(C);
+ var resolve = capability.resolve;
+ var reject = capability.reject;
+ var result = perform(function () {
+ var values = [];
+ var index = 0;
+ var remaining = 1;
+ forOf(iterable, false, function (promise) {
+ var $index = index++;
+ var alreadyCalled = false;
+ values.push(undefined);
+ remaining++;
+ C.resolve(promise).then(function (value) {
+ if (alreadyCalled) return;
+ alreadyCalled = true;
+ values[$index] = value;
+ --remaining || resolve(values);
+ }, reject);
+ });
+ --remaining || resolve(values);
+ });
+ if (result.e) reject(result.v);
+ return capability.promise;
+ },
+ race: function race(iterable) {
+ var C = this;
+ var capability = newPromiseCapability(C);
+ var reject = capability.reject;
+ var result = perform(function () {
+ forOf(iterable, false, function (promise) {
+ C.resolve(promise).then(capability.resolve, reject);
+ });
+ });
+ if (result.e) reject(result.v);
+ return capability.promise;
+ }
+});
+
+/***/ }),
+/* 88 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (it, Constructor, name, forbiddenField) {
+ if (!(it instanceof Constructor) || forbiddenField !== undefined && forbiddenField in it) {
+ throw TypeError(name + ': incorrect invocation!');
+ }
+
+ return it;
+};
+
+/***/ }),
+/* 89 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var ctx = __w_pdfjs_require__(26);
+
+var call = __w_pdfjs_require__(64);
+
+var isArrayIter = __w_pdfjs_require__(65);
+
+var anObject = __w_pdfjs_require__(12);
+
+var toLength = __w_pdfjs_require__(28);
+
+var getIterFn = __w_pdfjs_require__(67);
+
+var BREAK = {};
+var RETURN = {};
+
+var _exports = module.exports = function (iterable, entries, fn, that, ITERATOR) {
+ var iterFn = ITERATOR ? function () {
+ return iterable;
+ } : getIterFn(iterable);
+ var f = ctx(fn, that, entries ? 2 : 1);
+ var index = 0;
+ var length, step, iterator, result;
+ if (typeof iterFn != 'function') throw TypeError(iterable + ' is not iterable!');
+ if (isArrayIter(iterFn)) for (length = toLength(iterable.length); length > index; index++) {
+ result = entries ? f(anObject(step = iterable[index])[0], step[1]) : f(iterable[index]);
+ if (result === BREAK || result === RETURN) return result;
+ } else for (iterator = iterFn.call(iterable); !(step = iterator.next()).done;) {
+ result = call(iterator, f, step.value, entries);
+ if (result === BREAK || result === RETURN) return result;
+ }
+};
+
+_exports.BREAK = BREAK;
+_exports.RETURN = RETURN;
+
+/***/ }),
+/* 90 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var anObject = __w_pdfjs_require__(12);
+
+var aFunction = __w_pdfjs_require__(27);
+
+var SPECIES = __w_pdfjs_require__(33)('species');
+
+module.exports = function (O, D) {
+ var C = anObject(O).constructor;
+ var S;
+ return C === undefined || (S = anObject(C)[SPECIES]) == undefined ? D : aFunction(S);
+};
+
+/***/ }),
+/* 91 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var ctx = __w_pdfjs_require__(26);
+
+var invoke = __w_pdfjs_require__(92);
+
+var html = __w_pdfjs_require__(59);
+
+var cel = __w_pdfjs_require__(17);
+
+var global = __w_pdfjs_require__(8);
+
+var process = global.process;
+var setTask = global.setImmediate;
+var clearTask = global.clearImmediate;
+var MessageChannel = global.MessageChannel;
+var Dispatch = global.Dispatch;
+var counter = 0;
+var queue = {};
+var ONREADYSTATECHANGE = 'onreadystatechange';
+var defer, channel, port;
+
+var run = function run() {
+ var id = +this;
+
+ if (queue.hasOwnProperty(id)) {
+ var fn = queue[id];
+ delete queue[id];
+ fn();
+ }
+};
+
+var listener = function listener(event) {
+ run.call(event.data);
+};
+
+if (!setTask || !clearTask) {
+ setTask = function setImmediate(fn) {
+ var args = [];
+ var i = 1;
+
+ while (arguments.length > i) {
+ args.push(arguments[i++]);
+ }
+
+ queue[++counter] = function () {
+ invoke(typeof fn == 'function' ? fn : Function(fn), args);
+ };
+
+ defer(counter);
+ return counter;
+ };
+
+ clearTask = function clearImmediate(id) {
+ delete queue[id];
+ };
+
+ if (__w_pdfjs_require__(32)(process) == 'process') {
+ defer = function defer(id) {
+ process.nextTick(ctx(run, id, 1));
+ };
+ } else if (Dispatch && Dispatch.now) {
+ defer = function defer(id) {
+ Dispatch.now(ctx(run, id, 1));
+ };
+ } else if (MessageChannel) {
+ channel = new MessageChannel();
+ port = channel.port2;
+ channel.port1.onmessage = listener;
+ defer = ctx(port.postMessage, port, 1);
+ } else if (global.addEventListener && typeof postMessage == 'function' && !global.importScripts) {
+ defer = function defer(id) {
+ global.postMessage(id + '', '*');
+ };
+
+ global.addEventListener('message', listener, false);
+ } else if (ONREADYSTATECHANGE in cel('script')) {
+ defer = function defer(id) {
+ html.appendChild(cel('script'))[ONREADYSTATECHANGE] = function () {
+ html.removeChild(this);
+ run.call(id);
+ };
+ };
+ } else {
+ defer = function defer(id) {
+ setTimeout(ctx(run, id, 1), 0);
+ };
+ }
+}
+
+module.exports = {
+ set: setTask,
+ clear: clearTask
+};
+
+/***/ }),
+/* 92 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (fn, args, that) {
+ var un = that === undefined;
+
+ switch (args.length) {
+ case 0:
+ return un ? fn() : fn.call(that);
+
+ case 1:
+ return un ? fn(args[0]) : fn.call(that, args[0]);
+
+ case 2:
+ return un ? fn(args[0], args[1]) : fn.call(that, args[0], args[1]);
+
+ case 3:
+ return un ? fn(args[0], args[1], args[2]) : fn.call(that, args[0], args[1], args[2]);
+
+ case 4:
+ return un ? fn(args[0], args[1], args[2], args[3]) : fn.call(that, args[0], args[1], args[2], args[3]);
+ }
+
+ return fn.apply(that, args);
+};
+
+/***/ }),
+/* 93 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(8);
+
+var macrotask = __w_pdfjs_require__(91).set;
+
+var Observer = global.MutationObserver || global.WebKitMutationObserver;
+var process = global.process;
+var Promise = global.Promise;
+var isNode = __w_pdfjs_require__(32)(process) == 'process';
+
+module.exports = function () {
+ var head, last, notify;
+
+ var flush = function flush() {
+ var parent, fn;
+ if (isNode && (parent = process.domain)) parent.exit();
+
+ while (head) {
+ fn = head.fn;
+ head = head.next;
+
+ try {
+ fn();
+ } catch (e) {
+ if (head) notify();else last = undefined;
+ throw e;
+ }
+ }
+
+ last = undefined;
+ if (parent) parent.enter();
+ };
+
+ if (isNode) {
+ notify = function notify() {
+ process.nextTick(flush);
+ };
+ } else if (Observer && !(global.navigator && global.navigator.standalone)) {
+ var toggle = true;
+ var node = document.createTextNode('');
+ new Observer(flush).observe(node, {
+ characterData: true
+ });
+
+ notify = function notify() {
+ node.data = toggle = !toggle;
+ };
+ } else if (Promise && Promise.resolve) {
+ var promise = Promise.resolve(undefined);
+
+ notify = function notify() {
+ promise.then(flush);
+ };
+ } else {
+ notify = function notify() {
+ macrotask.call(global, flush);
+ };
+ }
+
+ return function (fn) {
+ var task = {
+ fn: fn,
+ next: undefined
+ };
+ if (last) last.next = task;
+
+ if (!head) {
+ head = task;
+ notify();
+ }
+
+ last = task;
+ };
+};
+
+/***/ }),
+/* 94 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var aFunction = __w_pdfjs_require__(27);
+
+function PromiseCapability(C) {
+ var resolve, reject;
+ this.promise = new C(function ($$resolve, $$reject) {
+ if (resolve !== undefined || reject !== undefined) throw TypeError('Bad Promise constructor');
+ resolve = $$resolve;
+ reject = $$reject;
+ });
+ this.resolve = aFunction(resolve);
+ this.reject = aFunction(reject);
+}
+
+module.exports.f = function (C) {
+ return new PromiseCapability(C);
+};
+
+/***/ }),
+/* 95 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (exec) {
+ try {
+ return {
+ e: false,
+ v: exec()
+ };
+ } catch (e) {
+ return {
+ e: true,
+ v: e
+ };
+ }
+};
+
+/***/ }),
+/* 96 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(8);
+
+var navigator = global.navigator;
+module.exports = navigator && navigator.userAgent || '';
+
+/***/ }),
+/* 97 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var anObject = __w_pdfjs_require__(12);
+
+var isObject = __w_pdfjs_require__(13);
+
+var newPromiseCapability = __w_pdfjs_require__(94);
+
+module.exports = function (C, x) {
+ anObject(C);
+ if (isObject(x) && x.constructor === C) return x;
+ var promiseCapability = newPromiseCapability.f(C);
+ var resolve = promiseCapability.resolve;
+ resolve(x);
+ return promiseCapability.promise;
+};
+
+/***/ }),
+/* 98 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var redefine = __w_pdfjs_require__(20);
+
+module.exports = function (target, src, safe) {
+ for (var key in src) {
+ redefine(target, key, src[key], safe);
+ }
+
+ return target;
+};
+
+/***/ }),
+/* 99 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(8);
+
+var dP = __w_pdfjs_require__(11);
+
+var DESCRIPTORS = __w_pdfjs_require__(15);
+
+var SPECIES = __w_pdfjs_require__(33)('species');
+
+module.exports = function (KEY) {
+ var C = global[KEY];
+ if (DESCRIPTORS && C && !C[SPECIES]) dP.f(C, SPECIES, {
+ configurable: true,
+ get: function get() {
+ return this;
+ }
+ });
+};
+
+/***/ }),
+/* 100 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+var core = __w_pdfjs_require__(9);
+
+var global = __w_pdfjs_require__(8);
+
+var speciesConstructor = __w_pdfjs_require__(90);
+
+var promiseResolve = __w_pdfjs_require__(97);
+
+$export($export.P + $export.R, 'Promise', {
+ 'finally': function _finally(onFinally) {
+ var C = speciesConstructor(this, core.Promise || global.Promise);
+ var isFunction = typeof onFinally == 'function';
+ return this.then(isFunction ? function (x) {
+ return promiseResolve(C, onFinally()).then(function () {
+ return x;
+ });
+ } : onFinally, isFunction ? function (e) {
+ return promiseResolve(C, onFinally()).then(function () {
+ throw e;
+ });
+ } : onFinally);
+ }
+});
+
+/***/ }),
+/* 101 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+var newPromiseCapability = __w_pdfjs_require__(94);
+
+var perform = __w_pdfjs_require__(95);
+
+$export($export.S, 'Promise', {
+ 'try': function _try(callbackfn) {
+ var promiseCapability = newPromiseCapability.f(this);
+ var result = perform(callbackfn);
+ (result.e ? promiseCapability.reject : promiseCapability.resolve)(result.v);
+ return promiseCapability.promise;
+ }
+});
+
+/***/ }),
+/* 102 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(83);
+
+__w_pdfjs_require__(84);
+
+__w_pdfjs_require__(103);
+
+__w_pdfjs_require__(115);
+
+__w_pdfjs_require__(117);
+
+module.exports = __w_pdfjs_require__(9).WeakMap;
+
+/***/ }),
+/* 103 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(8);
+
+var each = __w_pdfjs_require__(104)(0);
+
+var redefine = __w_pdfjs_require__(20);
+
+var meta = __w_pdfjs_require__(108);
+
+var assign = __w_pdfjs_require__(72);
+
+var weak = __w_pdfjs_require__(109);
+
+var isObject = __w_pdfjs_require__(13);
+
+var validate = __w_pdfjs_require__(110);
+
+var NATIVE_WEAK_MAP = __w_pdfjs_require__(110);
+
+var IS_IE11 = !global.ActiveXObject && 'ActiveXObject' in global;
+var WEAK_MAP = 'WeakMap';
+var getWeak = meta.getWeak;
+var isExtensible = Object.isExtensible;
+var uncaughtFrozenStore = weak.ufstore;
+var InternalMap;
+
+var wrapper = function wrapper(get) {
+ return function WeakMap() {
+ return get(this, arguments.length > 0 ? arguments[0] : undefined);
+ };
+};
+
+var methods = {
+ get: function get(key) {
+ if (isObject(key)) {
+ var data = getWeak(key);
+ if (data === true) return uncaughtFrozenStore(validate(this, WEAK_MAP)).get(key);
+ return data ? data[this._i] : undefined;
+ }
+ },
+ set: function set(key, value) {
+ return weak.def(validate(this, WEAK_MAP), key, value);
+ }
+};
+
+var $WeakMap = module.exports = __w_pdfjs_require__(111)(WEAK_MAP, wrapper, methods, weak, true, true);
+
+if (NATIVE_WEAK_MAP && IS_IE11) {
+ InternalMap = weak.getConstructor(wrapper, WEAK_MAP);
+ assign(InternalMap.prototype, methods);
+ meta.NEED = true;
+ each(['delete', 'has', 'get', 'set'], function (key) {
+ var proto = $WeakMap.prototype;
+ var method = proto[key];
+ redefine(proto, key, function (a, b) {
+ if (isObject(a) && !isExtensible(a)) {
+ if (!this._f) this._f = new InternalMap();
+
+ var result = this._f[key](a, b);
+
+ return key == 'set' ? this : result;
+ }
+
+ return method.call(this, a, b);
+ });
+ });
+}
+
+/***/ }),
+/* 104 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var ctx = __w_pdfjs_require__(26);
+
+var IObject = __w_pdfjs_require__(44);
+
+var toObject = __w_pdfjs_require__(62);
+
+var toLength = __w_pdfjs_require__(28);
+
+var asc = __w_pdfjs_require__(105);
+
+module.exports = function (TYPE, $create) {
+ var IS_MAP = TYPE == 1;
+ var IS_FILTER = TYPE == 2;
+ var IS_SOME = TYPE == 3;
+ var IS_EVERY = TYPE == 4;
+ var IS_FIND_INDEX = TYPE == 6;
+ var NO_HOLES = TYPE == 5 || IS_FIND_INDEX;
+ var create = $create || asc;
+ return function ($this, callbackfn, that) {
+ var O = toObject($this);
+ var self = IObject(O);
+ var f = ctx(callbackfn, that, 3);
+ var length = toLength(self.length);
+ var index = 0;
+ var result = IS_MAP ? create($this, length) : IS_FILTER ? create($this, 0) : undefined;
+ var val, res;
+
+ for (; length > index; index++) {
+ if (NO_HOLES || index in self) {
+ val = self[index];
+ res = f(val, index, O);
+
+ if (TYPE) {
+ if (IS_MAP) result[index] = res;else if (res) switch (TYPE) {
+ case 3:
+ return true;
+
+ case 5:
+ return val;
+
+ case 6:
+ return index;
+
+ case 2:
+ result.push(val);
+ } else if (IS_EVERY) return false;
+ }
+ }
+ }
+
+ return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : result;
+ };
+};
+
+/***/ }),
+/* 105 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var speciesConstructor = __w_pdfjs_require__(106);
+
+module.exports = function (original, length) {
+ return new (speciesConstructor(original))(length);
+};
+
+/***/ }),
+/* 106 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(13);
+
+var isArray = __w_pdfjs_require__(107);
+
+var SPECIES = __w_pdfjs_require__(33)('species');
+
+module.exports = function (original) {
+ var C;
+
+ if (isArray(original)) {
+ C = original.constructor;
+ if (typeof C == 'function' && (C === Array || isArray(C.prototype))) C = undefined;
+
+ if (isObject(C)) {
+ C = C[SPECIES];
+ if (C === null) C = undefined;
+ }
+ }
+
+ return C === undefined ? Array : C;
+};
+
+/***/ }),
+/* 107 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var cof = __w_pdfjs_require__(32);
+
+module.exports = Array.isArray || function isArray(arg) {
+ return cof(arg) == 'Array';
+};
+
+/***/ }),
+/* 108 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var META = __w_pdfjs_require__(22)('meta');
+
+var isObject = __w_pdfjs_require__(13);
+
+var has = __w_pdfjs_require__(21);
+
+var setDesc = __w_pdfjs_require__(11).f;
+
+var id = 0;
+
+var isExtensible = Object.isExtensible || function () {
+ return true;
+};
+
+var FREEZE = !__w_pdfjs_require__(16)(function () {
+ return isExtensible(Object.preventExtensions({}));
+});
+
+var setMeta = function setMeta(it) {
+ setDesc(it, META, {
+ value: {
+ i: 'O' + ++id,
+ w: {}
+ }
+ });
+};
+
+var fastKey = function fastKey(it, create) {
+ if (!isObject(it)) return _typeof(it) == 'symbol' ? it : (typeof it == 'string' ? 'S' : 'P') + it;
+
+ if (!has(it, META)) {
+ if (!isExtensible(it)) return 'F';
+ if (!create) return 'E';
+ setMeta(it);
+ }
+
+ return it[META].i;
+};
+
+var getWeak = function getWeak(it, create) {
+ if (!has(it, META)) {
+ if (!isExtensible(it)) return true;
+ if (!create) return false;
+ setMeta(it);
+ }
+
+ return it[META].w;
+};
+
+var onFreeze = function onFreeze(it) {
+ if (FREEZE && meta.NEED && isExtensible(it) && !has(it, META)) setMeta(it);
+ return it;
+};
+
+var meta = module.exports = {
+ KEY: META,
+ NEED: false,
+ fastKey: fastKey,
+ getWeak: getWeak,
+ onFreeze: onFreeze
+};
+
+/***/ }),
+/* 109 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var redefineAll = __w_pdfjs_require__(98);
+
+var getWeak = __w_pdfjs_require__(108).getWeak;
+
+var anObject = __w_pdfjs_require__(12);
+
+var isObject = __w_pdfjs_require__(13);
+
+var anInstance = __w_pdfjs_require__(88);
+
+var forOf = __w_pdfjs_require__(89);
+
+var createArrayMethod = __w_pdfjs_require__(104);
+
+var $has = __w_pdfjs_require__(21);
+
+var validate = __w_pdfjs_require__(110);
+
+var arrayFind = createArrayMethod(5);
+var arrayFindIndex = createArrayMethod(6);
+var id = 0;
+
+var uncaughtFrozenStore = function uncaughtFrozenStore(that) {
+ return that._l || (that._l = new UncaughtFrozenStore());
+};
+
+var UncaughtFrozenStore = function UncaughtFrozenStore() {
+ this.a = [];
+};
+
+var findUncaughtFrozen = function findUncaughtFrozen(store, key) {
+ return arrayFind(store.a, function (it) {
+ return it[0] === key;
+ });
+};
+
+UncaughtFrozenStore.prototype = {
+ get: function get(key) {
+ var entry = findUncaughtFrozen(this, key);
+ if (entry) return entry[1];
+ },
+ has: function has(key) {
+ return !!findUncaughtFrozen(this, key);
+ },
+ set: function set(key, value) {
+ var entry = findUncaughtFrozen(this, key);
+ if (entry) entry[1] = value;else this.a.push([key, value]);
+ },
+ 'delete': function _delete(key) {
+ var index = arrayFindIndex(this.a, function (it) {
+ return it[0] === key;
+ });
+ if (~index) this.a.splice(index, 1);
+ return !!~index;
+ }
+};
+module.exports = {
+ getConstructor: function getConstructor(wrapper, NAME, IS_MAP, ADDER) {
+ var C = wrapper(function (that, iterable) {
+ anInstance(that, C, NAME, '_i');
+ that._t = NAME;
+ that._i = id++;
+ that._l = undefined;
+ if (iterable != undefined) forOf(iterable, IS_MAP, that[ADDER], that);
+ });
+ redefineAll(C.prototype, {
+ 'delete': function _delete(key) {
+ if (!isObject(key)) return false;
+ var data = getWeak(key);
+ if (data === true) return uncaughtFrozenStore(validate(this, NAME))['delete'](key);
+ return data && $has(data, this._i) && delete data[this._i];
+ },
+ has: function has(key) {
+ if (!isObject(key)) return false;
+ var data = getWeak(key);
+ if (data === true) return uncaughtFrozenStore(validate(this, NAME)).has(key);
+ return data && $has(data, this._i);
+ }
+ });
+ return C;
+ },
+ def: function def(that, key, value) {
+ var data = getWeak(anObject(key), true);
+ if (data === true) uncaughtFrozenStore(that).set(key, value);else data[that._i] = value;
+ return that;
+ },
+ ufstore: uncaughtFrozenStore
+};
+
+/***/ }),
+/* 110 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(13);
+
+module.exports = function (it, TYPE) {
+ if (!isObject(it) || it._t !== TYPE) throw TypeError('Incompatible receiver, ' + TYPE + ' required!');
+ return it;
+};
+
+/***/ }),
+/* 111 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(8);
+
+var $export = __w_pdfjs_require__(7);
+
+var redefine = __w_pdfjs_require__(20);
+
+var redefineAll = __w_pdfjs_require__(98);
+
+var meta = __w_pdfjs_require__(108);
+
+var forOf = __w_pdfjs_require__(89);
+
+var anInstance = __w_pdfjs_require__(88);
+
+var isObject = __w_pdfjs_require__(13);
+
+var fails = __w_pdfjs_require__(16);
+
+var $iterDetect = __w_pdfjs_require__(69);
+
+var setToStringTag = __w_pdfjs_require__(60);
+
+var inheritIfRequired = __w_pdfjs_require__(112);
+
+module.exports = function (NAME, wrapper, methods, common, IS_MAP, IS_WEAK) {
+ var Base = global[NAME];
+ var C = Base;
+ var ADDER = IS_MAP ? 'set' : 'add';
+ var proto = C && C.prototype;
+ var O = {};
+
+ var fixMethod = function fixMethod(KEY) {
+ var fn = proto[KEY];
+ redefine(proto, KEY, KEY == 'delete' ? function (a) {
+ return IS_WEAK && !isObject(a) ? false : fn.call(this, a === 0 ? 0 : a);
+ } : KEY == 'has' ? function has(a) {
+ return IS_WEAK && !isObject(a) ? false : fn.call(this, a === 0 ? 0 : a);
+ } : KEY == 'get' ? function get(a) {
+ return IS_WEAK && !isObject(a) ? undefined : fn.call(this, a === 0 ? 0 : a);
+ } : KEY == 'add' ? function add(a) {
+ fn.call(this, a === 0 ? 0 : a);
+ return this;
+ } : function set(a, b) {
+ fn.call(this, a === 0 ? 0 : a, b);
+ return this;
+ });
+ };
+
+ if (typeof C != 'function' || !(IS_WEAK || proto.forEach && !fails(function () {
+ new C().entries().next();
+ }))) {
+ C = common.getConstructor(wrapper, NAME, IS_MAP, ADDER);
+ redefineAll(C.prototype, methods);
+ meta.NEED = true;
+ } else {
+ var instance = new C();
+ var HASNT_CHAINING = instance[ADDER](IS_WEAK ? {} : -0, 1) != instance;
+ var THROWS_ON_PRIMITIVES = fails(function () {
+ instance.has(1);
+ });
+ var ACCEPT_ITERABLES = $iterDetect(function (iter) {
+ new C(iter);
+ });
+ var BUGGY_ZERO = !IS_WEAK && fails(function () {
+ var $instance = new C();
+ var index = 5;
+
+ while (index--) {
+ $instance[ADDER](index, index);
+ }
+
+ return !$instance.has(-0);
+ });
+
+ if (!ACCEPT_ITERABLES) {
+ C = wrapper(function (target, iterable) {
+ anInstance(target, C, NAME);
+ var that = inheritIfRequired(new Base(), target, C);
+ if (iterable != undefined) forOf(iterable, IS_MAP, that[ADDER], that);
+ return that;
+ });
+ C.prototype = proto;
+ proto.constructor = C;
+ }
+
+ if (THROWS_ON_PRIMITIVES || BUGGY_ZERO) {
+ fixMethod('delete');
+ fixMethod('has');
+ IS_MAP && fixMethod('get');
+ }
+
+ if (BUGGY_ZERO || HASNT_CHAINING) fixMethod(ADDER);
+ if (IS_WEAK && proto.clear) delete proto.clear;
+ }
+
+ setToStringTag(C, NAME);
+ O[NAME] = C;
+ $export($export.G + $export.W + $export.F * (C != Base), O);
+ if (!IS_WEAK) common.setStrong(C, NAME, IS_MAP);
+ return C;
+};
+
+/***/ }),
+/* 112 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(13);
+
+var setPrototypeOf = __w_pdfjs_require__(113).set;
+
+module.exports = function (that, target, C) {
+ var S = target.constructor;
+ var P;
+
+ if (S !== C && typeof S == 'function' && (P = S.prototype) !== C.prototype && isObject(P) && setPrototypeOf) {
+ setPrototypeOf(that, P);
+ }
+
+ return that;
+};
+
+/***/ }),
+/* 113 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(13);
+
+var anObject = __w_pdfjs_require__(12);
+
+var check = function check(O, proto) {
+ anObject(O);
+ if (!isObject(proto) && proto !== null) throw TypeError(proto + ": can't set as prototype!");
+};
+
+module.exports = {
+ set: Object.setPrototypeOf || ('__proto__' in {} ? function (test, buggy, set) {
+ try {
+ set = __w_pdfjs_require__(26)(Function.call, __w_pdfjs_require__(114).f(Object.prototype, '__proto__').set, 2);
+ set(test, []);
+ buggy = !(test instanceof Array);
+ } catch (e) {
+ buggy = true;
+ }
+
+ return function setPrototypeOf(O, proto) {
+ check(O, proto);
+ if (buggy) O.__proto__ = proto;else set(O, proto);
+ return O;
+ };
+ }({}, false) : undefined),
+ check: check
+};
+
+/***/ }),
+/* 114 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var pIE = __w_pdfjs_require__(74);
+
+var createDesc = __w_pdfjs_require__(19);
+
+var toIObject = __w_pdfjs_require__(43);
+
+var toPrimitive = __w_pdfjs_require__(18);
+
+var has = __w_pdfjs_require__(21);
+
+var IE8_DOM_DEFINE = __w_pdfjs_require__(14);
+
+var gOPD = Object.getOwnPropertyDescriptor;
+exports.f = __w_pdfjs_require__(15) ? gOPD : function getOwnPropertyDescriptor(O, P) {
+ O = toIObject(O);
+ P = toPrimitive(P, true);
+ if (IE8_DOM_DEFINE) try {
+ return gOPD(O, P);
+ } catch (e) {}
+ if (has(O, P)) return createDesc(!pIE.f.call(O, P), O[P]);
+};
+
+/***/ }),
+/* 115 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(116)('WeakMap');
+
+/***/ }),
+/* 116 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+module.exports = function (COLLECTION) {
+ $export($export.S, COLLECTION, {
+ of: function of() {
+ var length = arguments.length;
+ var A = new Array(length);
+
+ while (length--) {
+ A[length] = arguments[length];
+ }
+
+ return new this(A);
+ }
+ });
+};
+
+/***/ }),
+/* 117 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(118)('WeakMap');
+
+/***/ }),
+/* 118 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+var aFunction = __w_pdfjs_require__(27);
+
+var ctx = __w_pdfjs_require__(26);
+
+var forOf = __w_pdfjs_require__(89);
+
+module.exports = function (COLLECTION) {
+ $export($export.S, COLLECTION, {
+ from: function from(source) {
+ var mapFn = arguments[1];
+ var mapping, A, n, cb;
+ aFunction(this);
+ mapping = mapFn !== undefined;
+ if (mapping) aFunction(mapFn);
+ if (source == undefined) return new this();
+ A = [];
+
+ if (mapping) {
+ n = 0;
+ cb = ctx(mapFn, arguments[2], 2);
+ forOf(source, false, function (nextItem) {
+ A.push(cb(nextItem, n++));
+ });
+ } else {
+ forOf(source, false, A.push, A);
+ }
+
+ return new this(A);
+ }
+ });
+};
+
+/***/ }),
+/* 119 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(83);
+
+__w_pdfjs_require__(84);
+
+__w_pdfjs_require__(120);
+
+__w_pdfjs_require__(121);
+
+__w_pdfjs_require__(122);
+
+module.exports = __w_pdfjs_require__(9).WeakSet;
+
+/***/ }),
+/* 120 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var weak = __w_pdfjs_require__(109);
+
+var validate = __w_pdfjs_require__(110);
+
+var WEAK_SET = 'WeakSet';
+
+__w_pdfjs_require__(111)(WEAK_SET, function (get) {
+ return function WeakSet() {
+ return get(this, arguments.length > 0 ? arguments[0] : undefined);
+ };
+}, {
+ add: function add(value) {
+ return weak.def(validate(this, WEAK_SET), value, true);
+ }
+}, weak, false, true);
+
+/***/ }),
+/* 121 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(116)('WeakSet');
+
+/***/ }),
+/* 122 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(118)('WeakSet');
+
+/***/ }),
+/* 123 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(124);
+
+module.exports = __w_pdfjs_require__(9).String.codePointAt;
+
+/***/ }),
+/* 124 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+var $at = __w_pdfjs_require__(49)(false);
+
+$export($export.P, 'String', {
+ codePointAt: function codePointAt(pos) {
+ return $at(this, pos);
+ }
+});
+
+/***/ }),
+/* 125 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(126);
+
+module.exports = __w_pdfjs_require__(9).String.fromCodePoint;
+
+/***/ }),
+/* 126 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+var toAbsoluteIndex = __w_pdfjs_require__(45);
+
+var fromCharCode = String.fromCharCode;
+var $fromCodePoint = String.fromCodePoint;
+$export($export.S + $export.F * (!!$fromCodePoint && $fromCodePoint.length != 1), 'String', {
+ fromCodePoint: function fromCodePoint(x) {
+ var res = [];
+ var aLen = arguments.length;
+ var i = 0;
+ var code;
+
+ while (aLen > i) {
+ code = +arguments[i++];
+ if (toAbsoluteIndex(code, 0x10ffff) !== code) throw RangeError(code + ' is not a valid code point');
+ res.push(code < 0x10000 ? fromCharCode(code) : fromCharCode(((code -= 0x10000) >> 10) + 0xd800, code % 0x400 + 0xdc00));
+ }
+
+ return res.join('');
+ }
+});
+
+/***/ }),
+/* 127 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(128);
+
+__w_pdfjs_require__(83);
+
+module.exports = __w_pdfjs_require__(9).Symbol;
+
+/***/ }),
+/* 128 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var global = __w_pdfjs_require__(8);
+
+var has = __w_pdfjs_require__(21);
+
+var DESCRIPTORS = __w_pdfjs_require__(15);
+
+var $export = __w_pdfjs_require__(7);
+
+var redefine = __w_pdfjs_require__(20);
+
+var META = __w_pdfjs_require__(108).KEY;
+
+var $fails = __w_pdfjs_require__(16);
+
+var shared = __w_pdfjs_require__(24);
+
+var setToStringTag = __w_pdfjs_require__(60);
+
+var uid = __w_pdfjs_require__(22);
+
+var wks = __w_pdfjs_require__(33);
+
+var wksExt = __w_pdfjs_require__(129);
+
+var wksDefine = __w_pdfjs_require__(130);
+
+var enumKeys = __w_pdfjs_require__(131);
+
+var isArray = __w_pdfjs_require__(107);
+
+var anObject = __w_pdfjs_require__(12);
+
+var isObject = __w_pdfjs_require__(13);
+
+var toObject = __w_pdfjs_require__(62);
+
+var toIObject = __w_pdfjs_require__(43);
+
+var toPrimitive = __w_pdfjs_require__(18);
+
+var createDesc = __w_pdfjs_require__(19);
+
+var _create = __w_pdfjs_require__(53);
+
+var gOPNExt = __w_pdfjs_require__(132);
+
+var $GOPD = __w_pdfjs_require__(114);
+
+var $GOPS = __w_pdfjs_require__(73);
+
+var $DP = __w_pdfjs_require__(11);
+
+var $keys = __w_pdfjs_require__(55);
+
+var gOPD = $GOPD.f;
+var dP = $DP.f;
+var gOPN = gOPNExt.f;
+var $Symbol = global.Symbol;
+var $JSON = global.JSON;
+
+var _stringify = $JSON && $JSON.stringify;
+
+var PROTOTYPE = 'prototype';
+var HIDDEN = wks('_hidden');
+var TO_PRIMITIVE = wks('toPrimitive');
+var isEnum = {}.propertyIsEnumerable;
+var SymbolRegistry = shared('symbol-registry');
+var AllSymbols = shared('symbols');
+var OPSymbols = shared('op-symbols');
+var ObjectProto = Object[PROTOTYPE];
+var USE_NATIVE = typeof $Symbol == 'function' && !!$GOPS.f;
+var QObject = global.QObject;
+var setter = !QObject || !QObject[PROTOTYPE] || !QObject[PROTOTYPE].findChild;
+var setSymbolDesc = DESCRIPTORS && $fails(function () {
+ return _create(dP({}, 'a', {
+ get: function get() {
+ return dP(this, 'a', {
+ value: 7
+ }).a;
+ }
+ })).a != 7;
+}) ? function (it, key, D) {
+ var protoDesc = gOPD(ObjectProto, key);
+ if (protoDesc) delete ObjectProto[key];
+ dP(it, key, D);
+ if (protoDesc && it !== ObjectProto) dP(ObjectProto, key, protoDesc);
+} : dP;
+
+var wrap = function wrap(tag) {
+ var sym = AllSymbols[tag] = _create($Symbol[PROTOTYPE]);
+
+ sym._k = tag;
+ return sym;
+};
+
+var isSymbol = USE_NATIVE && _typeof($Symbol.iterator) == 'symbol' ? function (it) {
+ return _typeof(it) == 'symbol';
+} : function (it) {
+ return it instanceof $Symbol;
+};
+
+var $defineProperty = function defineProperty(it, key, D) {
+ if (it === ObjectProto) $defineProperty(OPSymbols, key, D);
+ anObject(it);
+ key = toPrimitive(key, true);
+ anObject(D);
+
+ if (has(AllSymbols, key)) {
+ if (!D.enumerable) {
+ if (!has(it, HIDDEN)) dP(it, HIDDEN, createDesc(1, {}));
+ it[HIDDEN][key] = true;
+ } else {
+ if (has(it, HIDDEN) && it[HIDDEN][key]) it[HIDDEN][key] = false;
+ D = _create(D, {
+ enumerable: createDesc(0, false)
+ });
+ }
+
+ return setSymbolDesc(it, key, D);
+ }
+
+ return dP(it, key, D);
+};
+
+var $defineProperties = function defineProperties(it, P) {
+ anObject(it);
+ var keys = enumKeys(P = toIObject(P));
+ var i = 0;
+ var l = keys.length;
+ var key;
+
+ while (l > i) {
+ $defineProperty(it, key = keys[i++], P[key]);
+ }
+
+ return it;
+};
+
+var $create = function create(it, P) {
+ return P === undefined ? _create(it) : $defineProperties(_create(it), P);
+};
+
+var $propertyIsEnumerable = function propertyIsEnumerable(key) {
+ var E = isEnum.call(this, key = toPrimitive(key, true));
+ if (this === ObjectProto && has(AllSymbols, key) && !has(OPSymbols, key)) return false;
+ return E || !has(this, key) || !has(AllSymbols, key) || has(this, HIDDEN) && this[HIDDEN][key] ? E : true;
+};
+
+var $getOwnPropertyDescriptor = function getOwnPropertyDescriptor(it, key) {
+ it = toIObject(it);
+ key = toPrimitive(key, true);
+ if (it === ObjectProto && has(AllSymbols, key) && !has(OPSymbols, key)) return;
+ var D = gOPD(it, key);
+ if (D && has(AllSymbols, key) && !(has(it, HIDDEN) && it[HIDDEN][key])) D.enumerable = true;
+ return D;
+};
+
+var $getOwnPropertyNames = function getOwnPropertyNames(it) {
+ var names = gOPN(toIObject(it));
+ var result = [];
+ var i = 0;
+ var key;
+
+ while (names.length > i) {
+ if (!has(AllSymbols, key = names[i++]) && key != HIDDEN && key != META) result.push(key);
+ }
+
+ return result;
+};
+
+var $getOwnPropertySymbols = function getOwnPropertySymbols(it) {
+ var IS_OP = it === ObjectProto;
+ var names = gOPN(IS_OP ? OPSymbols : toIObject(it));
+ var result = [];
+ var i = 0;
+ var key;
+
+ while (names.length > i) {
+ if (has(AllSymbols, key = names[i++]) && (IS_OP ? has(ObjectProto, key) : true)) result.push(AllSymbols[key]);
+ }
+
+ return result;
+};
+
+if (!USE_NATIVE) {
+ $Symbol = function _Symbol() {
+ if (this instanceof $Symbol) throw TypeError('Symbol is not a constructor!');
+ var tag = uid(arguments.length > 0 ? arguments[0] : undefined);
+
+ var $set = function $set(value) {
+ if (this === ObjectProto) $set.call(OPSymbols, value);
+ if (has(this, HIDDEN) && has(this[HIDDEN], tag)) this[HIDDEN][tag] = false;
+ setSymbolDesc(this, tag, createDesc(1, value));
+ };
+
+ if (DESCRIPTORS && setter) setSymbolDesc(ObjectProto, tag, {
+ configurable: true,
+ set: $set
+ });
+ return wrap(tag);
+ };
+
+ redefine($Symbol[PROTOTYPE], 'toString', function toString() {
+ return this._k;
+ });
+ $GOPD.f = $getOwnPropertyDescriptor;
+ $DP.f = $defineProperty;
+ __w_pdfjs_require__(133).f = gOPNExt.f = $getOwnPropertyNames;
+ __w_pdfjs_require__(74).f = $propertyIsEnumerable;
+ $GOPS.f = $getOwnPropertySymbols;
+
+ if (DESCRIPTORS && !__w_pdfjs_require__(25)) {
+ redefine(ObjectProto, 'propertyIsEnumerable', $propertyIsEnumerable, true);
+ }
+
+ wksExt.f = function (name) {
+ return wrap(wks(name));
+ };
+}
+
+$export($export.G + $export.W + $export.F * !USE_NATIVE, {
+ Symbol: $Symbol
+});
+
+for (var es6Symbols = 'hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables'.split(','), j = 0; es6Symbols.length > j;) {
+ wks(es6Symbols[j++]);
+}
+
+for (var wellKnownSymbols = $keys(wks.store), k = 0; wellKnownSymbols.length > k;) {
+ wksDefine(wellKnownSymbols[k++]);
+}
+
+$export($export.S + $export.F * !USE_NATIVE, 'Symbol', {
+ 'for': function _for(key) {
+ return has(SymbolRegistry, key += '') ? SymbolRegistry[key] : SymbolRegistry[key] = $Symbol(key);
+ },
+ keyFor: function keyFor(sym) {
+ if (!isSymbol(sym)) throw TypeError(sym + ' is not a symbol!');
+
+ for (var key in SymbolRegistry) {
+ if (SymbolRegistry[key] === sym) return key;
+ }
+ },
+ useSetter: function useSetter() {
+ setter = true;
+ },
+ useSimple: function useSimple() {
+ setter = false;
+ }
+});
+$export($export.S + $export.F * !USE_NATIVE, 'Object', {
+ create: $create,
+ defineProperty: $defineProperty,
+ defineProperties: $defineProperties,
+ getOwnPropertyDescriptor: $getOwnPropertyDescriptor,
+ getOwnPropertyNames: $getOwnPropertyNames,
+ getOwnPropertySymbols: $getOwnPropertySymbols
+});
+var FAILS_ON_PRIMITIVES = $fails(function () {
+ $GOPS.f(1);
+});
+$export($export.S + $export.F * FAILS_ON_PRIMITIVES, 'Object', {
+ getOwnPropertySymbols: function getOwnPropertySymbols(it) {
+ return $GOPS.f(toObject(it));
+ }
+});
+$JSON && $export($export.S + $export.F * (!USE_NATIVE || $fails(function () {
+ var S = $Symbol();
+ return _stringify([S]) != '[null]' || _stringify({
+ a: S
+ }) != '{}' || _stringify(Object(S)) != '{}';
+})), 'JSON', {
+ stringify: function stringify(it) {
+ var args = [it];
+ var i = 1;
+ var replacer, $replacer;
+
+ while (arguments.length > i) {
+ args.push(arguments[i++]);
+ }
+
+ $replacer = replacer = args[1];
+ if (!isObject(replacer) && it === undefined || isSymbol(it)) return;
+ if (!isArray(replacer)) replacer = function replacer(key, value) {
+ if (typeof $replacer == 'function') value = $replacer.call(this, key, value);
+ if (!isSymbol(value)) return value;
+ };
+ args[1] = replacer;
+ return _stringify.apply($JSON, args);
+ }
+});
+$Symbol[PROTOTYPE][TO_PRIMITIVE] || __w_pdfjs_require__(10)($Symbol[PROTOTYPE], TO_PRIMITIVE, $Symbol[PROTOTYPE].valueOf);
+setToStringTag($Symbol, 'Symbol');
+setToStringTag(Math, 'Math', true);
+setToStringTag(global.JSON, 'JSON', true);
+
+/***/ }),
+/* 129 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+exports.f = __w_pdfjs_require__(33);
+
+/***/ }),
+/* 130 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(8);
+
+var core = __w_pdfjs_require__(9);
+
+var LIBRARY = __w_pdfjs_require__(25);
+
+var wksExt = __w_pdfjs_require__(129);
+
+var defineProperty = __w_pdfjs_require__(11).f;
+
+module.exports = function (name) {
+ var $Symbol = core.Symbol || (core.Symbol = LIBRARY ? {} : global.Symbol || {});
+ if (name.charAt(0) != '_' && !(name in $Symbol)) defineProperty($Symbol, name, {
+ value: wksExt.f(name)
+ });
+};
+
+/***/ }),
+/* 131 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var getKeys = __w_pdfjs_require__(55);
+
+var gOPS = __w_pdfjs_require__(73);
+
+var pIE = __w_pdfjs_require__(74);
+
+module.exports = function (it) {
+ var result = getKeys(it);
+ var getSymbols = gOPS.f;
+
+ if (getSymbols) {
+ var symbols = getSymbols(it);
+ var isEnum = pIE.f;
+ var i = 0;
+ var key;
+
+ while (symbols.length > i) {
+ if (isEnum.call(it, key = symbols[i++])) result.push(key);
+ }
+ }
+
+ return result;
+};
+
+/***/ }),
+/* 132 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var toIObject = __w_pdfjs_require__(43);
+
+var gOPN = __w_pdfjs_require__(133).f;
+
+var toString = {}.toString;
+var windowNames = (typeof window === "undefined" ? "undefined" : _typeof(window)) == 'object' && window && Object.getOwnPropertyNames ? Object.getOwnPropertyNames(window) : [];
+
+var getWindowNames = function getWindowNames(it) {
+ try {
+ return gOPN(it);
+ } catch (e) {
+ return windowNames.slice();
+ }
+};
+
+module.exports.f = function getOwnPropertyNames(it) {
+ return windowNames && toString.call(it) == '[object Window]' ? getWindowNames(it) : gOPN(toIObject(it));
+};
+
+/***/ }),
+/* 133 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $keys = __w_pdfjs_require__(56);
+
+var hiddenKeys = __w_pdfjs_require__(58).concat('length', 'prototype');
+
+exports.f = Object.getOwnPropertyNames || function getOwnPropertyNames(O) {
+ return $keys(O, hiddenKeys);
+};
+
+/***/ }),
+/* 134 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(135);
+
+module.exports = __w_pdfjs_require__(9).String.padStart;
+
+/***/ }),
+/* 135 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+var $pad = __w_pdfjs_require__(136);
+
+var userAgent = __w_pdfjs_require__(96);
+
+var WEBKIT_BUG = /Version\/10\.\d+(\.\d+)?( Mobile\/\w+)? Safari\//.test(userAgent);
+$export($export.P + $export.F * WEBKIT_BUG, 'String', {
+ padStart: function padStart(maxLength) {
+ return $pad(this, maxLength, arguments.length > 1 ? arguments[1] : undefined, true);
+ }
+});
+
+/***/ }),
+/* 136 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toLength = __w_pdfjs_require__(28);
+
+var repeat = __w_pdfjs_require__(137);
+
+var defined = __w_pdfjs_require__(34);
+
+module.exports = function (that, maxLength, fillString, left) {
+ var S = String(defined(that));
+ var stringLength = S.length;
+ var fillStr = fillString === undefined ? ' ' : String(fillString);
+ var intMaxLength = toLength(maxLength);
+ if (intMaxLength <= stringLength || fillStr == '') return S;
+ var fillLen = intMaxLength - stringLength;
+ var stringFiller = repeat.call(fillStr, Math.ceil(fillLen / fillStr.length));
+ if (stringFiller.length > fillLen) stringFiller = stringFiller.slice(0, fillLen);
+ return left ? stringFiller + S : S + stringFiller;
+};
+
+/***/ }),
+/* 137 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toInteger = __w_pdfjs_require__(29);
+
+var defined = __w_pdfjs_require__(34);
+
+module.exports = function repeat(count) {
+ var str = String(defined(this));
+ var res = '';
+ var n = toInteger(count);
+ if (n < 0 || n == Infinity) throw RangeError("Count can't be negative");
+
+ for (; n > 0; (n >>>= 1) && (str += str)) {
+ if (n & 1) res += str;
+ }
+
+ return res;
+};
+
+/***/ }),
+/* 138 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(139);
+
+module.exports = __w_pdfjs_require__(9).String.padEnd;
+
+/***/ }),
+/* 139 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+var $pad = __w_pdfjs_require__(136);
+
+var userAgent = __w_pdfjs_require__(96);
+
+var WEBKIT_BUG = /Version\/10\.\d+(\.\d+)?( Mobile\/\w+)? Safari\//.test(userAgent);
+$export($export.P + $export.F * WEBKIT_BUG, 'String', {
+ padEnd: function padEnd(maxLength) {
+ return $pad(this, maxLength, arguments.length > 1 ? arguments[1] : undefined, false);
+ }
+});
+
+/***/ }),
+/* 140 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(141);
+
+module.exports = __w_pdfjs_require__(9).Object.values;
+
+/***/ }),
+/* 141 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(7);
+
+var $values = __w_pdfjs_require__(142)(false);
+
+$export($export.S, 'Object', {
+ values: function values(it) {
+ return $values(it);
+ }
+});
+
+/***/ }),
+/* 142 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var DESCRIPTORS = __w_pdfjs_require__(15);
+
+var getKeys = __w_pdfjs_require__(55);
+
+var toIObject = __w_pdfjs_require__(43);
+
+var isEnum = __w_pdfjs_require__(74).f;
+
+module.exports = function (isEntries) {
+ return function (it) {
+ var O = toIObject(it);
+ var keys = getKeys(O);
+ var length = keys.length;
+ var i = 0;
+ var result = [];
+ var key;
+
+ while (length > i) {
+ key = keys[i++];
+
+ if (!DESCRIPTORS || isEnum.call(O, key)) {
+ result.push(isEntries ? [key, O[key]] : O[key]);
+ }
+ }
+
+ return result;
+ };
+};
+
+/***/ }),
+/* 143 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+{
+ var isReadableStreamSupported = false;
+
+ if (typeof ReadableStream !== 'undefined') {
+ try {
+ new ReadableStream({
+ start: function start(controller) {
+ controller.close();
+ }
+ });
+ isReadableStreamSupported = true;
+ } catch (e) {}
+ }
+
+ if (isReadableStreamSupported) {
+ exports.ReadableStream = ReadableStream;
+ } else {
+ exports.ReadableStream = __w_pdfjs_require__(144).ReadableStream;
+ }
+}
+
+/***/ }),
+/* 144 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof2(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof2 = function _typeof2(obj) { return typeof obj; }; } else { _typeof2 = function _typeof2(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof2(obj); }
+
+(function (e, a) {
+ for (var i in a) {
+ e[i] = a[i];
+ }
+})(exports, function (modules) {
+ var installedModules = {};
+
+ function __w_pdfjs_require__(moduleId) {
+ if (installedModules[moduleId]) return installedModules[moduleId].exports;
+ var module = installedModules[moduleId] = {
+ i: moduleId,
+ l: false,
+ exports: {}
+ };
+ modules[moduleId].call(module.exports, module, module.exports, __w_pdfjs_require__);
+ module.l = true;
+ return module.exports;
+ }
+
+ __w_pdfjs_require__.m = modules;
+ __w_pdfjs_require__.c = installedModules;
+
+ __w_pdfjs_require__.i = function (value) {
+ return value;
+ };
+
+ __w_pdfjs_require__.d = function (exports, name, getter) {
+ if (!__w_pdfjs_require__.o(exports, name)) {
+ Object.defineProperty(exports, name, {
+ configurable: false,
+ enumerable: true,
+ get: getter
+ });
+ }
+ };
+
+ __w_pdfjs_require__.n = function (module) {
+ var getter = module && module.__esModule ? function getDefault() {
+ return module['default'];
+ } : function getModuleExports() {
+ return module;
+ };
+
+ __w_pdfjs_require__.d(getter, 'a', getter);
+
+ return getter;
+ };
+
+ __w_pdfjs_require__.o = function (object, property) {
+ return Object.prototype.hasOwnProperty.call(object, property);
+ };
+
+ __w_pdfjs_require__.p = "";
+ return __w_pdfjs_require__(__w_pdfjs_require__.s = 7);
+}([function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ var _typeof = typeof Symbol === "function" && _typeof2(Symbol.iterator) === "symbol" ? function (obj) {
+ return _typeof2(obj);
+ } : function (obj) {
+ return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : _typeof2(obj);
+ };
+
+ var _require = __w_pdfjs_require__(1),
+ assert = _require.assert;
+
+ function IsPropertyKey(argument) {
+ return typeof argument === 'string' || (typeof argument === 'undefined' ? 'undefined' : _typeof(argument)) === 'symbol';
+ }
+
+ exports.typeIsObject = function (x) {
+ return (typeof x === 'undefined' ? 'undefined' : _typeof(x)) === 'object' && x !== null || typeof x === 'function';
+ };
+
+ exports.createDataProperty = function (o, p, v) {
+ assert(exports.typeIsObject(o));
+ Object.defineProperty(o, p, {
+ value: v,
+ writable: true,
+ enumerable: true,
+ configurable: true
+ });
+ };
+
+ exports.createArrayFromList = function (elements) {
+ return elements.slice();
+ };
+
+ exports.ArrayBufferCopy = function (dest, destOffset, src, srcOffset, n) {
+ new Uint8Array(dest).set(new Uint8Array(src, srcOffset, n), destOffset);
+ };
+
+ exports.CreateIterResultObject = function (value, done) {
+ assert(typeof done === 'boolean');
+ var obj = {};
+ Object.defineProperty(obj, 'value', {
+ value: value,
+ enumerable: true,
+ writable: true,
+ configurable: true
+ });
+ Object.defineProperty(obj, 'done', {
+ value: done,
+ enumerable: true,
+ writable: true,
+ configurable: true
+ });
+ return obj;
+ };
+
+ exports.IsFiniteNonNegativeNumber = function (v) {
+ if (Number.isNaN(v)) {
+ return false;
+ }
+
+ if (v === Infinity) {
+ return false;
+ }
+
+ if (v < 0) {
+ return false;
+ }
+
+ return true;
+ };
+
+ function Call(F, V, args) {
+ if (typeof F !== 'function') {
+ throw new TypeError('Argument is not a function');
+ }
+
+ return Function.prototype.apply.call(F, V, args);
+ }
+
+ exports.InvokeOrNoop = function (O, P, args) {
+ assert(O !== undefined);
+ assert(IsPropertyKey(P));
+ assert(Array.isArray(args));
+ var method = O[P];
+
+ if (method === undefined) {
+ return undefined;
+ }
+
+ return Call(method, O, args);
+ };
+
+ exports.PromiseInvokeOrNoop = function (O, P, args) {
+ assert(O !== undefined);
+ assert(IsPropertyKey(P));
+ assert(Array.isArray(args));
+
+ try {
+ return Promise.resolve(exports.InvokeOrNoop(O, P, args));
+ } catch (returnValueE) {
+ return Promise.reject(returnValueE);
+ }
+ };
+
+ exports.PromiseInvokeOrPerformFallback = function (O, P, args, F, argsF) {
+ assert(O !== undefined);
+ assert(IsPropertyKey(P));
+ assert(Array.isArray(args));
+ assert(Array.isArray(argsF));
+ var method = void 0;
+
+ try {
+ method = O[P];
+ } catch (methodE) {
+ return Promise.reject(methodE);
+ }
+
+ if (method === undefined) {
+ return F.apply(null, argsF);
+ }
+
+ try {
+ return Promise.resolve(Call(method, O, args));
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ };
+
+ exports.TransferArrayBuffer = function (O) {
+ return O.slice();
+ };
+
+ exports.ValidateAndNormalizeHighWaterMark = function (highWaterMark) {
+ highWaterMark = Number(highWaterMark);
+
+ if (Number.isNaN(highWaterMark) || highWaterMark < 0) {
+ throw new RangeError('highWaterMark property of a queuing strategy must be non-negative and non-NaN');
+ }
+
+ return highWaterMark;
+ };
+
+ exports.ValidateAndNormalizeQueuingStrategy = function (size, highWaterMark) {
+ if (size !== undefined && typeof size !== 'function') {
+ throw new TypeError('size property of a queuing strategy must be a function');
+ }
+
+ highWaterMark = exports.ValidateAndNormalizeHighWaterMark(highWaterMark);
+ return {
+ size: size,
+ highWaterMark: highWaterMark
+ };
+ };
+}, function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ function rethrowAssertionErrorRejection(e) {
+ if (e && e.constructor === AssertionError) {
+ setTimeout(function () {
+ throw e;
+ }, 0);
+ }
+ }
+
+ function AssertionError(message) {
+ this.name = 'AssertionError';
+ this.message = message || '';
+ this.stack = new Error().stack;
+ }
+
+ AssertionError.prototype = Object.create(Error.prototype);
+ AssertionError.prototype.constructor = AssertionError;
+
+ function assert(value, message) {
+ if (!value) {
+ throw new AssertionError(message);
+ }
+ }
+
+ module.exports = {
+ rethrowAssertionErrorRejection: rethrowAssertionErrorRejection,
+ AssertionError: AssertionError,
+ assert: assert
+ };
+}, function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ var _createClass = function () {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ return function (Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+ }();
+
+ function _classCallCheck(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ }
+
+ var _require = __w_pdfjs_require__(0),
+ InvokeOrNoop = _require.InvokeOrNoop,
+ PromiseInvokeOrNoop = _require.PromiseInvokeOrNoop,
+ ValidateAndNormalizeQueuingStrategy = _require.ValidateAndNormalizeQueuingStrategy,
+ typeIsObject = _require.typeIsObject;
+
+ var _require2 = __w_pdfjs_require__(1),
+ assert = _require2.assert,
+ rethrowAssertionErrorRejection = _require2.rethrowAssertionErrorRejection;
+
+ var _require3 = __w_pdfjs_require__(3),
+ DequeueValue = _require3.DequeueValue,
+ EnqueueValueWithSize = _require3.EnqueueValueWithSize,
+ PeekQueueValue = _require3.PeekQueueValue,
+ ResetQueue = _require3.ResetQueue;
+
+ var WritableStream = function () {
+ function WritableStream() {
+ var underlyingSink = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
+ size = _ref.size,
+ _ref$highWaterMark = _ref.highWaterMark,
+ highWaterMark = _ref$highWaterMark === undefined ? 1 : _ref$highWaterMark;
+
+ _classCallCheck(this, WritableStream);
+
+ this._state = 'writable';
+ this._storedError = undefined;
+ this._writer = undefined;
+ this._writableStreamController = undefined;
+ this._writeRequests = [];
+ this._inFlightWriteRequest = undefined;
+ this._closeRequest = undefined;
+ this._inFlightCloseRequest = undefined;
+ this._pendingAbortRequest = undefined;
+ this._backpressure = false;
+ var type = underlyingSink.type;
+
+ if (type !== undefined) {
+ throw new RangeError('Invalid type is specified');
+ }
+
+ this._writableStreamController = new WritableStreamDefaultController(this, underlyingSink, size, highWaterMark);
+
+ this._writableStreamController.__startSteps();
+ }
+
+ _createClass(WritableStream, [{
+ key: 'abort',
+ value: function abort(reason) {
+ if (IsWritableStream(this) === false) {
+ return Promise.reject(streamBrandCheckException('abort'));
+ }
+
+ if (IsWritableStreamLocked(this) === true) {
+ return Promise.reject(new TypeError('Cannot abort a stream that already has a writer'));
+ }
+
+ return WritableStreamAbort(this, reason);
+ }
+ }, {
+ key: 'getWriter',
+ value: function getWriter() {
+ if (IsWritableStream(this) === false) {
+ throw streamBrandCheckException('getWriter');
+ }
+
+ return AcquireWritableStreamDefaultWriter(this);
+ }
+ }, {
+ key: 'locked',
+ get: function get() {
+ if (IsWritableStream(this) === false) {
+ throw streamBrandCheckException('locked');
+ }
+
+ return IsWritableStreamLocked(this);
+ }
+ }]);
+
+ return WritableStream;
+ }();
+
+ module.exports = {
+ AcquireWritableStreamDefaultWriter: AcquireWritableStreamDefaultWriter,
+ IsWritableStream: IsWritableStream,
+ IsWritableStreamLocked: IsWritableStreamLocked,
+ WritableStream: WritableStream,
+ WritableStreamAbort: WritableStreamAbort,
+ WritableStreamDefaultControllerError: WritableStreamDefaultControllerError,
+ WritableStreamDefaultWriterCloseWithErrorPropagation: WritableStreamDefaultWriterCloseWithErrorPropagation,
+ WritableStreamDefaultWriterRelease: WritableStreamDefaultWriterRelease,
+ WritableStreamDefaultWriterWrite: WritableStreamDefaultWriterWrite,
+ WritableStreamCloseQueuedOrInFlight: WritableStreamCloseQueuedOrInFlight
+ };
+
+ function AcquireWritableStreamDefaultWriter(stream) {
+ return new WritableStreamDefaultWriter(stream);
+ }
+
+ function IsWritableStream(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_writableStreamController')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function IsWritableStreamLocked(stream) {
+ assert(IsWritableStream(stream) === true, 'IsWritableStreamLocked should only be used on known writable streams');
+
+ if (stream._writer === undefined) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function WritableStreamAbort(stream, reason) {
+ var state = stream._state;
+
+ if (state === 'closed') {
+ return Promise.resolve(undefined);
+ }
+
+ if (state === 'errored') {
+ return Promise.reject(stream._storedError);
+ }
+
+ var error = new TypeError('Requested to abort');
+
+ if (stream._pendingAbortRequest !== undefined) {
+ return Promise.reject(error);
+ }
+
+ assert(state === 'writable' || state === 'erroring', 'state must be writable or erroring');
+ var wasAlreadyErroring = false;
+
+ if (state === 'erroring') {
+ wasAlreadyErroring = true;
+ reason = undefined;
+ }
+
+ var promise = new Promise(function (resolve, reject) {
+ stream._pendingAbortRequest = {
+ _resolve: resolve,
+ _reject: reject,
+ _reason: reason,
+ _wasAlreadyErroring: wasAlreadyErroring
+ };
+ });
+
+ if (wasAlreadyErroring === false) {
+ WritableStreamStartErroring(stream, error);
+ }
+
+ return promise;
+ }
+
+ function WritableStreamAddWriteRequest(stream) {
+ assert(IsWritableStreamLocked(stream) === true);
+ assert(stream._state === 'writable');
+ var promise = new Promise(function (resolve, reject) {
+ var writeRequest = {
+ _resolve: resolve,
+ _reject: reject
+ };
+
+ stream._writeRequests.push(writeRequest);
+ });
+ return promise;
+ }
+
+ function WritableStreamDealWithRejection(stream, error) {
+ var state = stream._state;
+
+ if (state === 'writable') {
+ WritableStreamStartErroring(stream, error);
+ return;
+ }
+
+ assert(state === 'erroring');
+ WritableStreamFinishErroring(stream);
+ }
+
+ function WritableStreamStartErroring(stream, reason) {
+ assert(stream._storedError === undefined, 'stream._storedError === undefined');
+ assert(stream._state === 'writable', 'state must be writable');
+ var controller = stream._writableStreamController;
+ assert(controller !== undefined, 'controller must not be undefined');
+ stream._state = 'erroring';
+ stream._storedError = reason;
+ var writer = stream._writer;
+
+ if (writer !== undefined) {
+ WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason);
+ }
+
+ if (WritableStreamHasOperationMarkedInFlight(stream) === false && controller._started === true) {
+ WritableStreamFinishErroring(stream);
+ }
+ }
+
+ function WritableStreamFinishErroring(stream) {
+ assert(stream._state === 'erroring', 'stream._state === erroring');
+ assert(WritableStreamHasOperationMarkedInFlight(stream) === false, 'WritableStreamHasOperationMarkedInFlight(stream) === false');
+ stream._state = 'errored';
+
+ stream._writableStreamController.__errorSteps();
+
+ var storedError = stream._storedError;
+
+ for (var i = 0; i < stream._writeRequests.length; i++) {
+ var writeRequest = stream._writeRequests[i];
+
+ writeRequest._reject(storedError);
+ }
+
+ stream._writeRequests = [];
+
+ if (stream._pendingAbortRequest === undefined) {
+ WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream);
+ return;
+ }
+
+ var abortRequest = stream._pendingAbortRequest;
+ stream._pendingAbortRequest = undefined;
+
+ if (abortRequest._wasAlreadyErroring === true) {
+ abortRequest._reject(storedError);
+
+ WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream);
+ return;
+ }
+
+ var promise = stream._writableStreamController.__abortSteps(abortRequest._reason);
+
+ promise.then(function () {
+ abortRequest._resolve();
+
+ WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream);
+ }, function (reason) {
+ abortRequest._reject(reason);
+
+ WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream);
+ });
+ }
+
+ function WritableStreamFinishInFlightWrite(stream) {
+ assert(stream._inFlightWriteRequest !== undefined);
+
+ stream._inFlightWriteRequest._resolve(undefined);
+
+ stream._inFlightWriteRequest = undefined;
+ }
+
+ function WritableStreamFinishInFlightWriteWithError(stream, error) {
+ assert(stream._inFlightWriteRequest !== undefined);
+
+ stream._inFlightWriteRequest._reject(error);
+
+ stream._inFlightWriteRequest = undefined;
+ assert(stream._state === 'writable' || stream._state === 'erroring');
+ WritableStreamDealWithRejection(stream, error);
+ }
+
+ function WritableStreamFinishInFlightClose(stream) {
+ assert(stream._inFlightCloseRequest !== undefined);
+
+ stream._inFlightCloseRequest._resolve(undefined);
+
+ stream._inFlightCloseRequest = undefined;
+ var state = stream._state;
+ assert(state === 'writable' || state === 'erroring');
+
+ if (state === 'erroring') {
+ stream._storedError = undefined;
+
+ if (stream._pendingAbortRequest !== undefined) {
+ stream._pendingAbortRequest._resolve();
+
+ stream._pendingAbortRequest = undefined;
+ }
+ }
+
+ stream._state = 'closed';
+ var writer = stream._writer;
+
+ if (writer !== undefined) {
+ defaultWriterClosedPromiseResolve(writer);
+ }
+
+ assert(stream._pendingAbortRequest === undefined, 'stream._pendingAbortRequest === undefined');
+ assert(stream._storedError === undefined, 'stream._storedError === undefined');
+ }
+
+ function WritableStreamFinishInFlightCloseWithError(stream, error) {
+ assert(stream._inFlightCloseRequest !== undefined);
+
+ stream._inFlightCloseRequest._reject(error);
+
+ stream._inFlightCloseRequest = undefined;
+ assert(stream._state === 'writable' || stream._state === 'erroring');
+
+ if (stream._pendingAbortRequest !== undefined) {
+ stream._pendingAbortRequest._reject(error);
+
+ stream._pendingAbortRequest = undefined;
+ }
+
+ WritableStreamDealWithRejection(stream, error);
+ }
+
+ function WritableStreamCloseQueuedOrInFlight(stream) {
+ if (stream._closeRequest === undefined && stream._inFlightCloseRequest === undefined) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function WritableStreamHasOperationMarkedInFlight(stream) {
+ if (stream._inFlightWriteRequest === undefined && stream._inFlightCloseRequest === undefined) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function WritableStreamMarkCloseRequestInFlight(stream) {
+ assert(stream._inFlightCloseRequest === undefined);
+ assert(stream._closeRequest !== undefined);
+ stream._inFlightCloseRequest = stream._closeRequest;
+ stream._closeRequest = undefined;
+ }
+
+ function WritableStreamMarkFirstWriteRequestInFlight(stream) {
+ assert(stream._inFlightWriteRequest === undefined, 'there must be no pending write request');
+ assert(stream._writeRequests.length !== 0, 'writeRequests must not be empty');
+ stream._inFlightWriteRequest = stream._writeRequests.shift();
+ }
+
+ function WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream) {
+ assert(stream._state === 'errored', '_stream_.[[state]] is `"errored"`');
+
+ if (stream._closeRequest !== undefined) {
+ assert(stream._inFlightCloseRequest === undefined);
+
+ stream._closeRequest._reject(stream._storedError);
+
+ stream._closeRequest = undefined;
+ }
+
+ var writer = stream._writer;
+
+ if (writer !== undefined) {
+ defaultWriterClosedPromiseReject(writer, stream._storedError);
+
+ writer._closedPromise["catch"](function () {});
+ }
+ }
+
+ function WritableStreamUpdateBackpressure(stream, backpressure) {
+ assert(stream._state === 'writable');
+ assert(WritableStreamCloseQueuedOrInFlight(stream) === false);
+ var writer = stream._writer;
+
+ if (writer !== undefined && backpressure !== stream._backpressure) {
+ if (backpressure === true) {
+ defaultWriterReadyPromiseReset(writer);
+ } else {
+ assert(backpressure === false);
+ defaultWriterReadyPromiseResolve(writer);
+ }
+ }
+
+ stream._backpressure = backpressure;
+ }
+
+ var WritableStreamDefaultWriter = function () {
+ function WritableStreamDefaultWriter(stream) {
+ _classCallCheck(this, WritableStreamDefaultWriter);
+
+ if (IsWritableStream(stream) === false) {
+ throw new TypeError('WritableStreamDefaultWriter can only be constructed with a WritableStream instance');
+ }
+
+ if (IsWritableStreamLocked(stream) === true) {
+ throw new TypeError('This stream has already been locked for exclusive writing by another writer');
+ }
+
+ this._ownerWritableStream = stream;
+ stream._writer = this;
+ var state = stream._state;
+
+ if (state === 'writable') {
+ if (WritableStreamCloseQueuedOrInFlight(stream) === false && stream._backpressure === true) {
+ defaultWriterReadyPromiseInitialize(this);
+ } else {
+ defaultWriterReadyPromiseInitializeAsResolved(this);
+ }
+
+ defaultWriterClosedPromiseInitialize(this);
+ } else if (state === 'erroring') {
+ defaultWriterReadyPromiseInitializeAsRejected(this, stream._storedError);
+
+ this._readyPromise["catch"](function () {});
+
+ defaultWriterClosedPromiseInitialize(this);
+ } else if (state === 'closed') {
+ defaultWriterReadyPromiseInitializeAsResolved(this);
+ defaultWriterClosedPromiseInitializeAsResolved(this);
+ } else {
+ assert(state === 'errored', 'state must be errored');
+ var storedError = stream._storedError;
+ defaultWriterReadyPromiseInitializeAsRejected(this, storedError);
+
+ this._readyPromise["catch"](function () {});
+
+ defaultWriterClosedPromiseInitializeAsRejected(this, storedError);
+
+ this._closedPromise["catch"](function () {});
+ }
+ }
+
+ _createClass(WritableStreamDefaultWriter, [{
+ key: 'abort',
+ value: function abort(reason) {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ return Promise.reject(defaultWriterBrandCheckException('abort'));
+ }
+
+ if (this._ownerWritableStream === undefined) {
+ return Promise.reject(defaultWriterLockException('abort'));
+ }
+
+ return WritableStreamDefaultWriterAbort(this, reason);
+ }
+ }, {
+ key: 'close',
+ value: function close() {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ return Promise.reject(defaultWriterBrandCheckException('close'));
+ }
+
+ var stream = this._ownerWritableStream;
+
+ if (stream === undefined) {
+ return Promise.reject(defaultWriterLockException('close'));
+ }
+
+ if (WritableStreamCloseQueuedOrInFlight(stream) === true) {
+ return Promise.reject(new TypeError('cannot close an already-closing stream'));
+ }
+
+ return WritableStreamDefaultWriterClose(this);
+ }
+ }, {
+ key: 'releaseLock',
+ value: function releaseLock() {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ throw defaultWriterBrandCheckException('releaseLock');
+ }
+
+ var stream = this._ownerWritableStream;
+
+ if (stream === undefined) {
+ return;
+ }
+
+ assert(stream._writer !== undefined);
+ WritableStreamDefaultWriterRelease(this);
+ }
+ }, {
+ key: 'write',
+ value: function write(chunk) {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ return Promise.reject(defaultWriterBrandCheckException('write'));
+ }
+
+ if (this._ownerWritableStream === undefined) {
+ return Promise.reject(defaultWriterLockException('write to'));
+ }
+
+ return WritableStreamDefaultWriterWrite(this, chunk);
+ }
+ }, {
+ key: 'closed',
+ get: function get() {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ return Promise.reject(defaultWriterBrandCheckException('closed'));
+ }
+
+ return this._closedPromise;
+ }
+ }, {
+ key: 'desiredSize',
+ get: function get() {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ throw defaultWriterBrandCheckException('desiredSize');
+ }
+
+ if (this._ownerWritableStream === undefined) {
+ throw defaultWriterLockException('desiredSize');
+ }
+
+ return WritableStreamDefaultWriterGetDesiredSize(this);
+ }
+ }, {
+ key: 'ready',
+ get: function get() {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ return Promise.reject(defaultWriterBrandCheckException('ready'));
+ }
+
+ return this._readyPromise;
+ }
+ }]);
+
+ return WritableStreamDefaultWriter;
+ }();
+
+ function IsWritableStreamDefaultWriter(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_ownerWritableStream')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function WritableStreamDefaultWriterAbort(writer, reason) {
+ var stream = writer._ownerWritableStream;
+ assert(stream !== undefined);
+ return WritableStreamAbort(stream, reason);
+ }
+
+ function WritableStreamDefaultWriterClose(writer) {
+ var stream = writer._ownerWritableStream;
+ assert(stream !== undefined);
+ var state = stream._state;
+
+ if (state === 'closed' || state === 'errored') {
+ return Promise.reject(new TypeError('The stream (in ' + state + ' state) is not in the writable state and cannot be closed'));
+ }
+
+ assert(state === 'writable' || state === 'erroring');
+ assert(WritableStreamCloseQueuedOrInFlight(stream) === false);
+ var promise = new Promise(function (resolve, reject) {
+ var closeRequest = {
+ _resolve: resolve,
+ _reject: reject
+ };
+ stream._closeRequest = closeRequest;
+ });
+
+ if (stream._backpressure === true && state === 'writable') {
+ defaultWriterReadyPromiseResolve(writer);
+ }
+
+ WritableStreamDefaultControllerClose(stream._writableStreamController);
+ return promise;
+ }
+
+ function WritableStreamDefaultWriterCloseWithErrorPropagation(writer) {
+ var stream = writer._ownerWritableStream;
+ assert(stream !== undefined);
+ var state = stream._state;
+
+ if (WritableStreamCloseQueuedOrInFlight(stream) === true || state === 'closed') {
+ return Promise.resolve();
+ }
+
+ if (state === 'errored') {
+ return Promise.reject(stream._storedError);
+ }
+
+ assert(state === 'writable' || state === 'erroring');
+ return WritableStreamDefaultWriterClose(writer);
+ }
+
+ function WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, error) {
+ if (writer._closedPromiseState === 'pending') {
+ defaultWriterClosedPromiseReject(writer, error);
+ } else {
+ defaultWriterClosedPromiseResetToRejected(writer, error);
+ }
+
+ writer._closedPromise["catch"](function () {});
+ }
+
+ function WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, error) {
+ if (writer._readyPromiseState === 'pending') {
+ defaultWriterReadyPromiseReject(writer, error);
+ } else {
+ defaultWriterReadyPromiseResetToRejected(writer, error);
+ }
+
+ writer._readyPromise["catch"](function () {});
+ }
+
+ function WritableStreamDefaultWriterGetDesiredSize(writer) {
+ var stream = writer._ownerWritableStream;
+ var state = stream._state;
+
+ if (state === 'errored' || state === 'erroring') {
+ return null;
+ }
+
+ if (state === 'closed') {
+ return 0;
+ }
+
+ return WritableStreamDefaultControllerGetDesiredSize(stream._writableStreamController);
+ }
+
+ function WritableStreamDefaultWriterRelease(writer) {
+ var stream = writer._ownerWritableStream;
+ assert(stream !== undefined);
+ assert(stream._writer === writer);
+ var releasedError = new TypeError('Writer was released and can no longer be used to monitor the stream\'s closedness');
+ WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, releasedError);
+ WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, releasedError);
+ stream._writer = undefined;
+ writer._ownerWritableStream = undefined;
+ }
+
+ function WritableStreamDefaultWriterWrite(writer, chunk) {
+ var stream = writer._ownerWritableStream;
+ assert(stream !== undefined);
+ var controller = stream._writableStreamController;
+ var chunkSize = WritableStreamDefaultControllerGetChunkSize(controller, chunk);
+
+ if (stream !== writer._ownerWritableStream) {
+ return Promise.reject(defaultWriterLockException('write to'));
+ }
+
+ var state = stream._state;
+
+ if (state === 'errored') {
+ return Promise.reject(stream._storedError);
+ }
+
+ if (WritableStreamCloseQueuedOrInFlight(stream) === true || state === 'closed') {
+ return Promise.reject(new TypeError('The stream is closing or closed and cannot be written to'));
+ }
+
+ if (state === 'erroring') {
+ return Promise.reject(stream._storedError);
+ }
+
+ assert(state === 'writable');
+ var promise = WritableStreamAddWriteRequest(stream);
+ WritableStreamDefaultControllerWrite(controller, chunk, chunkSize);
+ return promise;
+ }
+
+ var WritableStreamDefaultController = function () {
+ function WritableStreamDefaultController(stream, underlyingSink, size, highWaterMark) {
+ _classCallCheck(this, WritableStreamDefaultController);
+
+ if (IsWritableStream(stream) === false) {
+ throw new TypeError('WritableStreamDefaultController can only be constructed with a WritableStream instance');
+ }
+
+ if (stream._writableStreamController !== undefined) {
+ throw new TypeError('WritableStreamDefaultController instances can only be created by the WritableStream constructor');
+ }
+
+ this._controlledWritableStream = stream;
+ this._underlyingSink = underlyingSink;
+ this._queue = undefined;
+ this._queueTotalSize = undefined;
+ ResetQueue(this);
+ this._started = false;
+ var normalizedStrategy = ValidateAndNormalizeQueuingStrategy(size, highWaterMark);
+ this._strategySize = normalizedStrategy.size;
+ this._strategyHWM = normalizedStrategy.highWaterMark;
+ var backpressure = WritableStreamDefaultControllerGetBackpressure(this);
+ WritableStreamUpdateBackpressure(stream, backpressure);
+ }
+
+ _createClass(WritableStreamDefaultController, [{
+ key: 'error',
+ value: function error(e) {
+ if (IsWritableStreamDefaultController(this) === false) {
+ throw new TypeError('WritableStreamDefaultController.prototype.error can only be used on a WritableStreamDefaultController');
+ }
+
+ var state = this._controlledWritableStream._state;
+
+ if (state !== 'writable') {
+ return;
+ }
+
+ WritableStreamDefaultControllerError(this, e);
+ }
+ }, {
+ key: '__abortSteps',
+ value: function __abortSteps(reason) {
+ return PromiseInvokeOrNoop(this._underlyingSink, 'abort', [reason]);
+ }
+ }, {
+ key: '__errorSteps',
+ value: function __errorSteps() {
+ ResetQueue(this);
+ }
+ }, {
+ key: '__startSteps',
+ value: function __startSteps() {
+ var _this = this;
+
+ var startResult = InvokeOrNoop(this._underlyingSink, 'start', [this]);
+ var stream = this._controlledWritableStream;
+ Promise.resolve(startResult).then(function () {
+ assert(stream._state === 'writable' || stream._state === 'erroring');
+ _this._started = true;
+ WritableStreamDefaultControllerAdvanceQueueIfNeeded(_this);
+ }, function (r) {
+ assert(stream._state === 'writable' || stream._state === 'erroring');
+ _this._started = true;
+ WritableStreamDealWithRejection(stream, r);
+ })["catch"](rethrowAssertionErrorRejection);
+ }
+ }]);
+
+ return WritableStreamDefaultController;
+ }();
+
+ function WritableStreamDefaultControllerClose(controller) {
+ EnqueueValueWithSize(controller, 'close', 0);
+ WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller);
+ }
+
+ function WritableStreamDefaultControllerGetChunkSize(controller, chunk) {
+ var strategySize = controller._strategySize;
+
+ if (strategySize === undefined) {
+ return 1;
+ }
+
+ try {
+ return strategySize(chunk);
+ } catch (chunkSizeE) {
+ WritableStreamDefaultControllerErrorIfNeeded(controller, chunkSizeE);
+ return 1;
+ }
+ }
+
+ function WritableStreamDefaultControllerGetDesiredSize(controller) {
+ return controller._strategyHWM - controller._queueTotalSize;
+ }
+
+ function WritableStreamDefaultControllerWrite(controller, chunk, chunkSize) {
+ var writeRecord = {
+ chunk: chunk
+ };
+
+ try {
+ EnqueueValueWithSize(controller, writeRecord, chunkSize);
+ } catch (enqueueE) {
+ WritableStreamDefaultControllerErrorIfNeeded(controller, enqueueE);
+ return;
+ }
+
+ var stream = controller._controlledWritableStream;
+
+ if (WritableStreamCloseQueuedOrInFlight(stream) === false && stream._state === 'writable') {
+ var backpressure = WritableStreamDefaultControllerGetBackpressure(controller);
+ WritableStreamUpdateBackpressure(stream, backpressure);
+ }
+
+ WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller);
+ }
+
+ function IsWritableStreamDefaultController(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_underlyingSink')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller) {
+ var stream = controller._controlledWritableStream;
+
+ if (controller._started === false) {
+ return;
+ }
+
+ if (stream._inFlightWriteRequest !== undefined) {
+ return;
+ }
+
+ var state = stream._state;
+
+ if (state === 'closed' || state === 'errored') {
+ return;
+ }
+
+ if (state === 'erroring') {
+ WritableStreamFinishErroring(stream);
+ return;
+ }
+
+ if (controller._queue.length === 0) {
+ return;
+ }
+
+ var writeRecord = PeekQueueValue(controller);
+
+ if (writeRecord === 'close') {
+ WritableStreamDefaultControllerProcessClose(controller);
+ } else {
+ WritableStreamDefaultControllerProcessWrite(controller, writeRecord.chunk);
+ }
+ }
+
+ function WritableStreamDefaultControllerErrorIfNeeded(controller, error) {
+ if (controller._controlledWritableStream._state === 'writable') {
+ WritableStreamDefaultControllerError(controller, error);
+ }
+ }
+
+ function WritableStreamDefaultControllerProcessClose(controller) {
+ var stream = controller._controlledWritableStream;
+ WritableStreamMarkCloseRequestInFlight(stream);
+ DequeueValue(controller);
+ assert(controller._queue.length === 0, 'queue must be empty once the final write record is dequeued');
+ var sinkClosePromise = PromiseInvokeOrNoop(controller._underlyingSink, 'close', []);
+ sinkClosePromise.then(function () {
+ WritableStreamFinishInFlightClose(stream);
+ }, function (reason) {
+ WritableStreamFinishInFlightCloseWithError(stream, reason);
+ })["catch"](rethrowAssertionErrorRejection);
+ }
+
+ function WritableStreamDefaultControllerProcessWrite(controller, chunk) {
+ var stream = controller._controlledWritableStream;
+ WritableStreamMarkFirstWriteRequestInFlight(stream);
+ var sinkWritePromise = PromiseInvokeOrNoop(controller._underlyingSink, 'write', [chunk, controller]);
+ sinkWritePromise.then(function () {
+ WritableStreamFinishInFlightWrite(stream);
+ var state = stream._state;
+ assert(state === 'writable' || state === 'erroring');
+ DequeueValue(controller);
+
+ if (WritableStreamCloseQueuedOrInFlight(stream) === false && state === 'writable') {
+ var backpressure = WritableStreamDefaultControllerGetBackpressure(controller);
+ WritableStreamUpdateBackpressure(stream, backpressure);
+ }
+
+ WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller);
+ }, function (reason) {
+ WritableStreamFinishInFlightWriteWithError(stream, reason);
+ })["catch"](rethrowAssertionErrorRejection);
+ }
+
+ function WritableStreamDefaultControllerGetBackpressure(controller) {
+ var desiredSize = WritableStreamDefaultControllerGetDesiredSize(controller);
+ return desiredSize <= 0;
+ }
+
+ function WritableStreamDefaultControllerError(controller, error) {
+ var stream = controller._controlledWritableStream;
+ assert(stream._state === 'writable');
+ WritableStreamStartErroring(stream, error);
+ }
+
+ function streamBrandCheckException(name) {
+ return new TypeError('WritableStream.prototype.' + name + ' can only be used on a WritableStream');
+ }
+
+ function defaultWriterBrandCheckException(name) {
+ return new TypeError('WritableStreamDefaultWriter.prototype.' + name + ' can only be used on a WritableStreamDefaultWriter');
+ }
+
+ function defaultWriterLockException(name) {
+ return new TypeError('Cannot ' + name + ' a stream using a released writer');
+ }
+
+ function defaultWriterClosedPromiseInitialize(writer) {
+ writer._closedPromise = new Promise(function (resolve, reject) {
+ writer._closedPromise_resolve = resolve;
+ writer._closedPromise_reject = reject;
+ writer._closedPromiseState = 'pending';
+ });
+ }
+
+ function defaultWriterClosedPromiseInitializeAsRejected(writer, reason) {
+ writer._closedPromise = Promise.reject(reason);
+ writer._closedPromise_resolve = undefined;
+ writer._closedPromise_reject = undefined;
+ writer._closedPromiseState = 'rejected';
+ }
+
+ function defaultWriterClosedPromiseInitializeAsResolved(writer) {
+ writer._closedPromise = Promise.resolve(undefined);
+ writer._closedPromise_resolve = undefined;
+ writer._closedPromise_reject = undefined;
+ writer._closedPromiseState = 'resolved';
+ }
+
+ function defaultWriterClosedPromiseReject(writer, reason) {
+ assert(writer._closedPromise_resolve !== undefined, 'writer._closedPromise_resolve !== undefined');
+ assert(writer._closedPromise_reject !== undefined, 'writer._closedPromise_reject !== undefined');
+ assert(writer._closedPromiseState === 'pending', 'writer._closedPromiseState is pending');
+
+ writer._closedPromise_reject(reason);
+
+ writer._closedPromise_resolve = undefined;
+ writer._closedPromise_reject = undefined;
+ writer._closedPromiseState = 'rejected';
+ }
+
+ function defaultWriterClosedPromiseResetToRejected(writer, reason) {
+ assert(writer._closedPromise_resolve === undefined, 'writer._closedPromise_resolve === undefined');
+ assert(writer._closedPromise_reject === undefined, 'writer._closedPromise_reject === undefined');
+ assert(writer._closedPromiseState !== 'pending', 'writer._closedPromiseState is not pending');
+ writer._closedPromise = Promise.reject(reason);
+ writer._closedPromiseState = 'rejected';
+ }
+
+ function defaultWriterClosedPromiseResolve(writer) {
+ assert(writer._closedPromise_resolve !== undefined, 'writer._closedPromise_resolve !== undefined');
+ assert(writer._closedPromise_reject !== undefined, 'writer._closedPromise_reject !== undefined');
+ assert(writer._closedPromiseState === 'pending', 'writer._closedPromiseState is pending');
+
+ writer._closedPromise_resolve(undefined);
+
+ writer._closedPromise_resolve = undefined;
+ writer._closedPromise_reject = undefined;
+ writer._closedPromiseState = 'resolved';
+ }
+
+ function defaultWriterReadyPromiseInitialize(writer) {
+ writer._readyPromise = new Promise(function (resolve, reject) {
+ writer._readyPromise_resolve = resolve;
+ writer._readyPromise_reject = reject;
+ });
+ writer._readyPromiseState = 'pending';
+ }
+
+ function defaultWriterReadyPromiseInitializeAsRejected(writer, reason) {
+ writer._readyPromise = Promise.reject(reason);
+ writer._readyPromise_resolve = undefined;
+ writer._readyPromise_reject = undefined;
+ writer._readyPromiseState = 'rejected';
+ }
+
+ function defaultWriterReadyPromiseInitializeAsResolved(writer) {
+ writer._readyPromise = Promise.resolve(undefined);
+ writer._readyPromise_resolve = undefined;
+ writer._readyPromise_reject = undefined;
+ writer._readyPromiseState = 'fulfilled';
+ }
+
+ function defaultWriterReadyPromiseReject(writer, reason) {
+ assert(writer._readyPromise_resolve !== undefined, 'writer._readyPromise_resolve !== undefined');
+ assert(writer._readyPromise_reject !== undefined, 'writer._readyPromise_reject !== undefined');
+
+ writer._readyPromise_reject(reason);
+
+ writer._readyPromise_resolve = undefined;
+ writer._readyPromise_reject = undefined;
+ writer._readyPromiseState = 'rejected';
+ }
+
+ function defaultWriterReadyPromiseReset(writer) {
+ assert(writer._readyPromise_resolve === undefined, 'writer._readyPromise_resolve === undefined');
+ assert(writer._readyPromise_reject === undefined, 'writer._readyPromise_reject === undefined');
+ writer._readyPromise = new Promise(function (resolve, reject) {
+ writer._readyPromise_resolve = resolve;
+ writer._readyPromise_reject = reject;
+ });
+ writer._readyPromiseState = 'pending';
+ }
+
+ function defaultWriterReadyPromiseResetToRejected(writer, reason) {
+ assert(writer._readyPromise_resolve === undefined, 'writer._readyPromise_resolve === undefined');
+ assert(writer._readyPromise_reject === undefined, 'writer._readyPromise_reject === undefined');
+ writer._readyPromise = Promise.reject(reason);
+ writer._readyPromiseState = 'rejected';
+ }
+
+ function defaultWriterReadyPromiseResolve(writer) {
+ assert(writer._readyPromise_resolve !== undefined, 'writer._readyPromise_resolve !== undefined');
+ assert(writer._readyPromise_reject !== undefined, 'writer._readyPromise_reject !== undefined');
+
+ writer._readyPromise_resolve(undefined);
+
+ writer._readyPromise_resolve = undefined;
+ writer._readyPromise_reject = undefined;
+ writer._readyPromiseState = 'fulfilled';
+ }
+}, function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ var _require = __w_pdfjs_require__(0),
+ IsFiniteNonNegativeNumber = _require.IsFiniteNonNegativeNumber;
+
+ var _require2 = __w_pdfjs_require__(1),
+ assert = _require2.assert;
+
+ exports.DequeueValue = function (container) {
+ assert('_queue' in container && '_queueTotalSize' in container, 'Spec-level failure: DequeueValue should only be used on containers with [[queue]] and [[queueTotalSize]].');
+ assert(container._queue.length > 0, 'Spec-level failure: should never dequeue from an empty queue.');
+
+ var pair = container._queue.shift();
+
+ container._queueTotalSize -= pair.size;
+
+ if (container._queueTotalSize < 0) {
+ container._queueTotalSize = 0;
+ }
+
+ return pair.value;
+ };
+
+ exports.EnqueueValueWithSize = function (container, value, size) {
+ assert('_queue' in container && '_queueTotalSize' in container, 'Spec-level failure: EnqueueValueWithSize should only be used on containers with [[queue]] and ' + '[[queueTotalSize]].');
+ size = Number(size);
+
+ if (!IsFiniteNonNegativeNumber(size)) {
+ throw new RangeError('Size must be a finite, non-NaN, non-negative number.');
+ }
+
+ container._queue.push({
+ value: value,
+ size: size
+ });
+
+ container._queueTotalSize += size;
+ };
+
+ exports.PeekQueueValue = function (container) {
+ assert('_queue' in container && '_queueTotalSize' in container, 'Spec-level failure: PeekQueueValue should only be used on containers with [[queue]] and [[queueTotalSize]].');
+ assert(container._queue.length > 0, 'Spec-level failure: should never peek at an empty queue.');
+ var pair = container._queue[0];
+ return pair.value;
+ };
+
+ exports.ResetQueue = function (container) {
+ assert('_queue' in container && '_queueTotalSize' in container, 'Spec-level failure: ResetQueue should only be used on containers with [[queue]] and [[queueTotalSize]].');
+ container._queue = [];
+ container._queueTotalSize = 0;
+ };
+}, function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ var _createClass = function () {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ return function (Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+ }();
+
+ function _classCallCheck(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ }
+
+ var _require = __w_pdfjs_require__(0),
+ ArrayBufferCopy = _require.ArrayBufferCopy,
+ CreateIterResultObject = _require.CreateIterResultObject,
+ IsFiniteNonNegativeNumber = _require.IsFiniteNonNegativeNumber,
+ InvokeOrNoop = _require.InvokeOrNoop,
+ PromiseInvokeOrNoop = _require.PromiseInvokeOrNoop,
+ TransferArrayBuffer = _require.TransferArrayBuffer,
+ ValidateAndNormalizeQueuingStrategy = _require.ValidateAndNormalizeQueuingStrategy,
+ ValidateAndNormalizeHighWaterMark = _require.ValidateAndNormalizeHighWaterMark;
+
+ var _require2 = __w_pdfjs_require__(0),
+ createArrayFromList = _require2.createArrayFromList,
+ createDataProperty = _require2.createDataProperty,
+ typeIsObject = _require2.typeIsObject;
+
+ var _require3 = __w_pdfjs_require__(1),
+ assert = _require3.assert,
+ rethrowAssertionErrorRejection = _require3.rethrowAssertionErrorRejection;
+
+ var _require4 = __w_pdfjs_require__(3),
+ DequeueValue = _require4.DequeueValue,
+ EnqueueValueWithSize = _require4.EnqueueValueWithSize,
+ ResetQueue = _require4.ResetQueue;
+
+ var _require5 = __w_pdfjs_require__(2),
+ AcquireWritableStreamDefaultWriter = _require5.AcquireWritableStreamDefaultWriter,
+ IsWritableStream = _require5.IsWritableStream,
+ IsWritableStreamLocked = _require5.IsWritableStreamLocked,
+ WritableStreamAbort = _require5.WritableStreamAbort,
+ WritableStreamDefaultWriterCloseWithErrorPropagation = _require5.WritableStreamDefaultWriterCloseWithErrorPropagation,
+ WritableStreamDefaultWriterRelease = _require5.WritableStreamDefaultWriterRelease,
+ WritableStreamDefaultWriterWrite = _require5.WritableStreamDefaultWriterWrite,
+ WritableStreamCloseQueuedOrInFlight = _require5.WritableStreamCloseQueuedOrInFlight;
+
+ var ReadableStream = function () {
+ function ReadableStream() {
+ var underlyingSource = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
+ size = _ref.size,
+ highWaterMark = _ref.highWaterMark;
+
+ _classCallCheck(this, ReadableStream);
+
+ this._state = 'readable';
+ this._reader = undefined;
+ this._storedError = undefined;
+ this._disturbed = false;
+ this._readableStreamController = undefined;
+ var type = underlyingSource.type;
+ var typeString = String(type);
+
+ if (typeString === 'bytes') {
+ if (highWaterMark === undefined) {
+ highWaterMark = 0;
+ }
+
+ this._readableStreamController = new ReadableByteStreamController(this, underlyingSource, highWaterMark);
+ } else if (type === undefined) {
+ if (highWaterMark === undefined) {
+ highWaterMark = 1;
+ }
+
+ this._readableStreamController = new ReadableStreamDefaultController(this, underlyingSource, size, highWaterMark);
+ } else {
+ throw new RangeError('Invalid type is specified');
+ }
+ }
+
+ _createClass(ReadableStream, [{
+ key: 'cancel',
+ value: function cancel(reason) {
+ if (IsReadableStream(this) === false) {
+ return Promise.reject(streamBrandCheckException('cancel'));
+ }
+
+ if (IsReadableStreamLocked(this) === true) {
+ return Promise.reject(new TypeError('Cannot cancel a stream that already has a reader'));
+ }
+
+ return ReadableStreamCancel(this, reason);
+ }
+ }, {
+ key: 'getReader',
+ value: function getReader() {
+ var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
+ mode = _ref2.mode;
+
+ if (IsReadableStream(this) === false) {
+ throw streamBrandCheckException('getReader');
+ }
+
+ if (mode === undefined) {
+ return AcquireReadableStreamDefaultReader(this);
+ }
+
+ mode = String(mode);
+
+ if (mode === 'byob') {
+ return AcquireReadableStreamBYOBReader(this);
+ }
+
+ throw new RangeError('Invalid mode is specified');
+ }
+ }, {
+ key: 'pipeThrough',
+ value: function pipeThrough(_ref3, options) {
+ var writable = _ref3.writable,
+ readable = _ref3.readable;
+ var promise = this.pipeTo(writable, options);
+ ifIsObjectAndHasAPromiseIsHandledInternalSlotSetPromiseIsHandledToTrue(promise);
+ return readable;
+ }
+ }, {
+ key: 'pipeTo',
+ value: function pipeTo(dest) {
+ var _this = this;
+
+ var _ref4 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
+ preventClose = _ref4.preventClose,
+ preventAbort = _ref4.preventAbort,
+ preventCancel = _ref4.preventCancel;
+
+ if (IsReadableStream(this) === false) {
+ return Promise.reject(streamBrandCheckException('pipeTo'));
+ }
+
+ if (IsWritableStream(dest) === false) {
+ return Promise.reject(new TypeError('ReadableStream.prototype.pipeTo\'s first argument must be a WritableStream'));
+ }
+
+ preventClose = Boolean(preventClose);
+ preventAbort = Boolean(preventAbort);
+ preventCancel = Boolean(preventCancel);
+
+ if (IsReadableStreamLocked(this) === true) {
+ return Promise.reject(new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream'));
+ }
+
+ if (IsWritableStreamLocked(dest) === true) {
+ return Promise.reject(new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream'));
+ }
+
+ var reader = AcquireReadableStreamDefaultReader(this);
+ var writer = AcquireWritableStreamDefaultWriter(dest);
+ var shuttingDown = false;
+ var currentWrite = Promise.resolve();
+ return new Promise(function (resolve, reject) {
+ function pipeLoop() {
+ currentWrite = Promise.resolve();
+
+ if (shuttingDown === true) {
+ return Promise.resolve();
+ }
+
+ return writer._readyPromise.then(function () {
+ return ReadableStreamDefaultReaderRead(reader).then(function (_ref5) {
+ var value = _ref5.value,
+ done = _ref5.done;
+
+ if (done === true) {
+ return;
+ }
+
+ currentWrite = WritableStreamDefaultWriterWrite(writer, value)["catch"](function () {});
+ });
+ }).then(pipeLoop);
+ }
+
+ isOrBecomesErrored(_this, reader._closedPromise, function (storedError) {
+ if (preventAbort === false) {
+ shutdownWithAction(function () {
+ return WritableStreamAbort(dest, storedError);
+ }, true, storedError);
+ } else {
+ shutdown(true, storedError);
+ }
+ });
+ isOrBecomesErrored(dest, writer._closedPromise, function (storedError) {
+ if (preventCancel === false) {
+ shutdownWithAction(function () {
+ return ReadableStreamCancel(_this, storedError);
+ }, true, storedError);
+ } else {
+ shutdown(true, storedError);
+ }
+ });
+ isOrBecomesClosed(_this, reader._closedPromise, function () {
+ if (preventClose === false) {
+ shutdownWithAction(function () {
+ return WritableStreamDefaultWriterCloseWithErrorPropagation(writer);
+ });
+ } else {
+ shutdown();
+ }
+ });
+
+ if (WritableStreamCloseQueuedOrInFlight(dest) === true || dest._state === 'closed') {
+ var destClosed = new TypeError('the destination writable stream closed before all data could be piped to it');
+
+ if (preventCancel === false) {
+ shutdownWithAction(function () {
+ return ReadableStreamCancel(_this, destClosed);
+ }, true, destClosed);
+ } else {
+ shutdown(true, destClosed);
+ }
+ }
+
+ pipeLoop()["catch"](function (err) {
+ currentWrite = Promise.resolve();
+ rethrowAssertionErrorRejection(err);
+ });
+
+ function waitForWritesToFinish() {
+ var oldCurrentWrite = currentWrite;
+ return currentWrite.then(function () {
+ return oldCurrentWrite !== currentWrite ? waitForWritesToFinish() : undefined;
+ });
+ }
+
+ function isOrBecomesErrored(stream, promise, action) {
+ if (stream._state === 'errored') {
+ action(stream._storedError);
+ } else {
+ promise["catch"](action)["catch"](rethrowAssertionErrorRejection);
+ }
+ }
+
+ function isOrBecomesClosed(stream, promise, action) {
+ if (stream._state === 'closed') {
+ action();
+ } else {
+ promise.then(action)["catch"](rethrowAssertionErrorRejection);
+ }
+ }
+
+ function shutdownWithAction(action, originalIsError, originalError) {
+ if (shuttingDown === true) {
+ return;
+ }
+
+ shuttingDown = true;
+
+ if (dest._state === 'writable' && WritableStreamCloseQueuedOrInFlight(dest) === false) {
+ waitForWritesToFinish().then(doTheRest);
+ } else {
+ doTheRest();
+ }
+
+ function doTheRest() {
+ action().then(function () {
+ return finalize(originalIsError, originalError);
+ }, function (newError) {
+ return finalize(true, newError);
+ })["catch"](rethrowAssertionErrorRejection);
+ }
+ }
+
+ function shutdown(isError, error) {
+ if (shuttingDown === true) {
+ return;
+ }
+
+ shuttingDown = true;
+
+ if (dest._state === 'writable' && WritableStreamCloseQueuedOrInFlight(dest) === false) {
+ waitForWritesToFinish().then(function () {
+ return finalize(isError, error);
+ })["catch"](rethrowAssertionErrorRejection);
+ } else {
+ finalize(isError, error);
+ }
+ }
+
+ function finalize(isError, error) {
+ WritableStreamDefaultWriterRelease(writer);
+ ReadableStreamReaderGenericRelease(reader);
+
+ if (isError) {
+ reject(error);
+ } else {
+ resolve(undefined);
+ }
+ }
+ });
+ }
+ }, {
+ key: 'tee',
+ value: function tee() {
+ if (IsReadableStream(this) === false) {
+ throw streamBrandCheckException('tee');
+ }
+
+ var branches = ReadableStreamTee(this, false);
+ return createArrayFromList(branches);
+ }
+ }, {
+ key: 'locked',
+ get: function get() {
+ if (IsReadableStream(this) === false) {
+ throw streamBrandCheckException('locked');
+ }
+
+ return IsReadableStreamLocked(this);
+ }
+ }]);
+
+ return ReadableStream;
+ }();
+
+ module.exports = {
+ ReadableStream: ReadableStream,
+ IsReadableStreamDisturbed: IsReadableStreamDisturbed,
+ ReadableStreamDefaultControllerClose: ReadableStreamDefaultControllerClose,
+ ReadableStreamDefaultControllerEnqueue: ReadableStreamDefaultControllerEnqueue,
+ ReadableStreamDefaultControllerError: ReadableStreamDefaultControllerError,
+ ReadableStreamDefaultControllerGetDesiredSize: ReadableStreamDefaultControllerGetDesiredSize
+ };
+
+ function AcquireReadableStreamBYOBReader(stream) {
+ return new ReadableStreamBYOBReader(stream);
+ }
+
+ function AcquireReadableStreamDefaultReader(stream) {
+ return new ReadableStreamDefaultReader(stream);
+ }
+
+ function IsReadableStream(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_readableStreamController')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function IsReadableStreamDisturbed(stream) {
+ assert(IsReadableStream(stream) === true, 'IsReadableStreamDisturbed should only be used on known readable streams');
+ return stream._disturbed;
+ }
+
+ function IsReadableStreamLocked(stream) {
+ assert(IsReadableStream(stream) === true, 'IsReadableStreamLocked should only be used on known readable streams');
+
+ if (stream._reader === undefined) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function ReadableStreamTee(stream, cloneForBranch2) {
+ assert(IsReadableStream(stream) === true);
+ assert(typeof cloneForBranch2 === 'boolean');
+ var reader = AcquireReadableStreamDefaultReader(stream);
+ var teeState = {
+ closedOrErrored: false,
+ canceled1: false,
+ canceled2: false,
+ reason1: undefined,
+ reason2: undefined
+ };
+ teeState.promise = new Promise(function (resolve) {
+ teeState._resolve = resolve;
+ });
+ var pull = create_ReadableStreamTeePullFunction();
+ pull._reader = reader;
+ pull._teeState = teeState;
+ pull._cloneForBranch2 = cloneForBranch2;
+ var cancel1 = create_ReadableStreamTeeBranch1CancelFunction();
+ cancel1._stream = stream;
+ cancel1._teeState = teeState;
+ var cancel2 = create_ReadableStreamTeeBranch2CancelFunction();
+ cancel2._stream = stream;
+ cancel2._teeState = teeState;
+ var underlyingSource1 = Object.create(Object.prototype);
+ createDataProperty(underlyingSource1, 'pull', pull);
+ createDataProperty(underlyingSource1, 'cancel', cancel1);
+ var branch1Stream = new ReadableStream(underlyingSource1);
+ var underlyingSource2 = Object.create(Object.prototype);
+ createDataProperty(underlyingSource2, 'pull', pull);
+ createDataProperty(underlyingSource2, 'cancel', cancel2);
+ var branch2Stream = new ReadableStream(underlyingSource2);
+ pull._branch1 = branch1Stream._readableStreamController;
+ pull._branch2 = branch2Stream._readableStreamController;
+
+ reader._closedPromise["catch"](function (r) {
+ if (teeState.closedOrErrored === true) {
+ return;
+ }
+
+ ReadableStreamDefaultControllerError(pull._branch1, r);
+ ReadableStreamDefaultControllerError(pull._branch2, r);
+ teeState.closedOrErrored = true;
+ });
+
+ return [branch1Stream, branch2Stream];
+ }
+
+ function create_ReadableStreamTeePullFunction() {
+ function f() {
+ var reader = f._reader,
+ branch1 = f._branch1,
+ branch2 = f._branch2,
+ teeState = f._teeState;
+ return ReadableStreamDefaultReaderRead(reader).then(function (result) {
+ assert(typeIsObject(result));
+ var value = result.value;
+ var done = result.done;
+ assert(typeof done === 'boolean');
+
+ if (done === true && teeState.closedOrErrored === false) {
+ if (teeState.canceled1 === false) {
+ ReadableStreamDefaultControllerClose(branch1);
+ }
+
+ if (teeState.canceled2 === false) {
+ ReadableStreamDefaultControllerClose(branch2);
+ }
+
+ teeState.closedOrErrored = true;
+ }
+
+ if (teeState.closedOrErrored === true) {
+ return;
+ }
+
+ var value1 = value;
+ var value2 = value;
+
+ if (teeState.canceled1 === false) {
+ ReadableStreamDefaultControllerEnqueue(branch1, value1);
+ }
+
+ if (teeState.canceled2 === false) {
+ ReadableStreamDefaultControllerEnqueue(branch2, value2);
+ }
+ });
+ }
+
+ return f;
+ }
+
+ function create_ReadableStreamTeeBranch1CancelFunction() {
+ function f(reason) {
+ var stream = f._stream,
+ teeState = f._teeState;
+ teeState.canceled1 = true;
+ teeState.reason1 = reason;
+
+ if (teeState.canceled2 === true) {
+ var compositeReason = createArrayFromList([teeState.reason1, teeState.reason2]);
+ var cancelResult = ReadableStreamCancel(stream, compositeReason);
+
+ teeState._resolve(cancelResult);
+ }
+
+ return teeState.promise;
+ }
+
+ return f;
+ }
+
+ function create_ReadableStreamTeeBranch2CancelFunction() {
+ function f(reason) {
+ var stream = f._stream,
+ teeState = f._teeState;
+ teeState.canceled2 = true;
+ teeState.reason2 = reason;
+
+ if (teeState.canceled1 === true) {
+ var compositeReason = createArrayFromList([teeState.reason1, teeState.reason2]);
+ var cancelResult = ReadableStreamCancel(stream, compositeReason);
+
+ teeState._resolve(cancelResult);
+ }
+
+ return teeState.promise;
+ }
+
+ return f;
+ }
+
+ function ReadableStreamAddReadIntoRequest(stream) {
+ assert(IsReadableStreamBYOBReader(stream._reader) === true);
+ assert(stream._state === 'readable' || stream._state === 'closed');
+ var promise = new Promise(function (resolve, reject) {
+ var readIntoRequest = {
+ _resolve: resolve,
+ _reject: reject
+ };
+
+ stream._reader._readIntoRequests.push(readIntoRequest);
+ });
+ return promise;
+ }
+
+ function ReadableStreamAddReadRequest(stream) {
+ assert(IsReadableStreamDefaultReader(stream._reader) === true);
+ assert(stream._state === 'readable');
+ var promise = new Promise(function (resolve, reject) {
+ var readRequest = {
+ _resolve: resolve,
+ _reject: reject
+ };
+
+ stream._reader._readRequests.push(readRequest);
+ });
+ return promise;
+ }
+
+ function ReadableStreamCancel(stream, reason) {
+ stream._disturbed = true;
+
+ if (stream._state === 'closed') {
+ return Promise.resolve(undefined);
+ }
+
+ if (stream._state === 'errored') {
+ return Promise.reject(stream._storedError);
+ }
+
+ ReadableStreamClose(stream);
+
+ var sourceCancelPromise = stream._readableStreamController.__cancelSteps(reason);
+
+ return sourceCancelPromise.then(function () {
+ return undefined;
+ });
+ }
+
+ function ReadableStreamClose(stream) {
+ assert(stream._state === 'readable');
+ stream._state = 'closed';
+ var reader = stream._reader;
+
+ if (reader === undefined) {
+ return undefined;
+ }
+
+ if (IsReadableStreamDefaultReader(reader) === true) {
+ for (var i = 0; i < reader._readRequests.length; i++) {
+ var _resolve = reader._readRequests[i]._resolve;
+
+ _resolve(CreateIterResultObject(undefined, true));
+ }
+
+ reader._readRequests = [];
+ }
+
+ defaultReaderClosedPromiseResolve(reader);
+ return undefined;
+ }
+
+ function ReadableStreamError(stream, e) {
+ assert(IsReadableStream(stream) === true, 'stream must be ReadableStream');
+ assert(stream._state === 'readable', 'state must be readable');
+ stream._state = 'errored';
+ stream._storedError = e;
+ var reader = stream._reader;
+
+ if (reader === undefined) {
+ return undefined;
+ }
+
+ if (IsReadableStreamDefaultReader(reader) === true) {
+ for (var i = 0; i < reader._readRequests.length; i++) {
+ var readRequest = reader._readRequests[i];
+
+ readRequest._reject(e);
+ }
+
+ reader._readRequests = [];
+ } else {
+ assert(IsReadableStreamBYOBReader(reader), 'reader must be ReadableStreamBYOBReader');
+
+ for (var _i = 0; _i < reader._readIntoRequests.length; _i++) {
+ var readIntoRequest = reader._readIntoRequests[_i];
+
+ readIntoRequest._reject(e);
+ }
+
+ reader._readIntoRequests = [];
+ }
+
+ defaultReaderClosedPromiseReject(reader, e);
+
+ reader._closedPromise["catch"](function () {});
+ }
+
+ function ReadableStreamFulfillReadIntoRequest(stream, chunk, done) {
+ var reader = stream._reader;
+ assert(reader._readIntoRequests.length > 0);
+
+ var readIntoRequest = reader._readIntoRequests.shift();
+
+ readIntoRequest._resolve(CreateIterResultObject(chunk, done));
+ }
+
+ function ReadableStreamFulfillReadRequest(stream, chunk, done) {
+ var reader = stream._reader;
+ assert(reader._readRequests.length > 0);
+
+ var readRequest = reader._readRequests.shift();
+
+ readRequest._resolve(CreateIterResultObject(chunk, done));
+ }
+
+ function ReadableStreamGetNumReadIntoRequests(stream) {
+ return stream._reader._readIntoRequests.length;
+ }
+
+ function ReadableStreamGetNumReadRequests(stream) {
+ return stream._reader._readRequests.length;
+ }
+
+ function ReadableStreamHasBYOBReader(stream) {
+ var reader = stream._reader;
+
+ if (reader === undefined) {
+ return false;
+ }
+
+ if (IsReadableStreamBYOBReader(reader) === false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function ReadableStreamHasDefaultReader(stream) {
+ var reader = stream._reader;
+
+ if (reader === undefined) {
+ return false;
+ }
+
+ if (IsReadableStreamDefaultReader(reader) === false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ var ReadableStreamDefaultReader = function () {
+ function ReadableStreamDefaultReader(stream) {
+ _classCallCheck(this, ReadableStreamDefaultReader);
+
+ if (IsReadableStream(stream) === false) {
+ throw new TypeError('ReadableStreamDefaultReader can only be constructed with a ReadableStream instance');
+ }
+
+ if (IsReadableStreamLocked(stream) === true) {
+ throw new TypeError('This stream has already been locked for exclusive reading by another reader');
+ }
+
+ ReadableStreamReaderGenericInitialize(this, stream);
+ this._readRequests = [];
+ }
+
+ _createClass(ReadableStreamDefaultReader, [{
+ key: 'cancel',
+ value: function cancel(reason) {
+ if (IsReadableStreamDefaultReader(this) === false) {
+ return Promise.reject(defaultReaderBrandCheckException('cancel'));
+ }
+
+ if (this._ownerReadableStream === undefined) {
+ return Promise.reject(readerLockException('cancel'));
+ }
+
+ return ReadableStreamReaderGenericCancel(this, reason);
+ }
+ }, {
+ key: 'read',
+ value: function read() {
+ if (IsReadableStreamDefaultReader(this) === false) {
+ return Promise.reject(defaultReaderBrandCheckException('read'));
+ }
+
+ if (this._ownerReadableStream === undefined) {
+ return Promise.reject(readerLockException('read from'));
+ }
+
+ return ReadableStreamDefaultReaderRead(this);
+ }
+ }, {
+ key: 'releaseLock',
+ value: function releaseLock() {
+ if (IsReadableStreamDefaultReader(this) === false) {
+ throw defaultReaderBrandCheckException('releaseLock');
+ }
+
+ if (this._ownerReadableStream === undefined) {
+ return;
+ }
+
+ if (this._readRequests.length > 0) {
+ throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled');
+ }
+
+ ReadableStreamReaderGenericRelease(this);
+ }
+ }, {
+ key: 'closed',
+ get: function get() {
+ if (IsReadableStreamDefaultReader(this) === false) {
+ return Promise.reject(defaultReaderBrandCheckException('closed'));
+ }
+
+ return this._closedPromise;
+ }
+ }]);
+
+ return ReadableStreamDefaultReader;
+ }();
+
+ var ReadableStreamBYOBReader = function () {
+ function ReadableStreamBYOBReader(stream) {
+ _classCallCheck(this, ReadableStreamBYOBReader);
+
+ if (!IsReadableStream(stream)) {
+ throw new TypeError('ReadableStreamBYOBReader can only be constructed with a ReadableStream instance given a ' + 'byte source');
+ }
+
+ if (IsReadableByteStreamController(stream._readableStreamController) === false) {
+ throw new TypeError('Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte ' + 'source');
+ }
+
+ if (IsReadableStreamLocked(stream)) {
+ throw new TypeError('This stream has already been locked for exclusive reading by another reader');
+ }
+
+ ReadableStreamReaderGenericInitialize(this, stream);
+ this._readIntoRequests = [];
+ }
+
+ _createClass(ReadableStreamBYOBReader, [{
+ key: 'cancel',
+ value: function cancel(reason) {
+ if (!IsReadableStreamBYOBReader(this)) {
+ return Promise.reject(byobReaderBrandCheckException('cancel'));
+ }
+
+ if (this._ownerReadableStream === undefined) {
+ return Promise.reject(readerLockException('cancel'));
+ }
+
+ return ReadableStreamReaderGenericCancel(this, reason);
+ }
+ }, {
+ key: 'read',
+ value: function read(view) {
+ if (!IsReadableStreamBYOBReader(this)) {
+ return Promise.reject(byobReaderBrandCheckException('read'));
+ }
+
+ if (this._ownerReadableStream === undefined) {
+ return Promise.reject(readerLockException('read from'));
+ }
+
+ if (!ArrayBuffer.isView(view)) {
+ return Promise.reject(new TypeError('view must be an array buffer view'));
+ }
+
+ if (view.byteLength === 0) {
+ return Promise.reject(new TypeError('view must have non-zero byteLength'));
+ }
+
+ return ReadableStreamBYOBReaderRead(this, view);
+ }
+ }, {
+ key: 'releaseLock',
+ value: function releaseLock() {
+ if (!IsReadableStreamBYOBReader(this)) {
+ throw byobReaderBrandCheckException('releaseLock');
+ }
+
+ if (this._ownerReadableStream === undefined) {
+ return;
+ }
+
+ if (this._readIntoRequests.length > 0) {
+ throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled');
+ }
+
+ ReadableStreamReaderGenericRelease(this);
+ }
+ }, {
+ key: 'closed',
+ get: function get() {
+ if (!IsReadableStreamBYOBReader(this)) {
+ return Promise.reject(byobReaderBrandCheckException('closed'));
+ }
+
+ return this._closedPromise;
+ }
+ }]);
+
+ return ReadableStreamBYOBReader;
+ }();
+
+ function IsReadableStreamBYOBReader(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_readIntoRequests')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function IsReadableStreamDefaultReader(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_readRequests')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function ReadableStreamReaderGenericInitialize(reader, stream) {
+ reader._ownerReadableStream = stream;
+ stream._reader = reader;
+
+ if (stream._state === 'readable') {
+ defaultReaderClosedPromiseInitialize(reader);
+ } else if (stream._state === 'closed') {
+ defaultReaderClosedPromiseInitializeAsResolved(reader);
+ } else {
+ assert(stream._state === 'errored', 'state must be errored');
+ defaultReaderClosedPromiseInitializeAsRejected(reader, stream._storedError);
+
+ reader._closedPromise["catch"](function () {});
+ }
+ }
+
+ function ReadableStreamReaderGenericCancel(reader, reason) {
+ var stream = reader._ownerReadableStream;
+ assert(stream !== undefined);
+ return ReadableStreamCancel(stream, reason);
+ }
+
+ function ReadableStreamReaderGenericRelease(reader) {
+ assert(reader._ownerReadableStream !== undefined);
+ assert(reader._ownerReadableStream._reader === reader);
+
+ if (reader._ownerReadableStream._state === 'readable') {
+ defaultReaderClosedPromiseReject(reader, new TypeError('Reader was released and can no longer be used to monitor the stream\'s closedness'));
+ } else {
+ defaultReaderClosedPromiseResetToRejected(reader, new TypeError('Reader was released and can no longer be used to monitor the stream\'s closedness'));
+ }
+
+ reader._closedPromise["catch"](function () {});
+
+ reader._ownerReadableStream._reader = undefined;
+ reader._ownerReadableStream = undefined;
+ }
+
+ function ReadableStreamBYOBReaderRead(reader, view) {
+ var stream = reader._ownerReadableStream;
+ assert(stream !== undefined);
+ stream._disturbed = true;
+
+ if (stream._state === 'errored') {
+ return Promise.reject(stream._storedError);
+ }
+
+ return ReadableByteStreamControllerPullInto(stream._readableStreamController, view);
+ }
+
+ function ReadableStreamDefaultReaderRead(reader) {
+ var stream = reader._ownerReadableStream;
+ assert(stream !== undefined);
+ stream._disturbed = true;
+
+ if (stream._state === 'closed') {
+ return Promise.resolve(CreateIterResultObject(undefined, true));
+ }
+
+ if (stream._state === 'errored') {
+ return Promise.reject(stream._storedError);
+ }
+
+ assert(stream._state === 'readable');
+ return stream._readableStreamController.__pullSteps();
+ }
+
+ var ReadableStreamDefaultController = function () {
+ function ReadableStreamDefaultController(stream, underlyingSource, size, highWaterMark) {
+ _classCallCheck(this, ReadableStreamDefaultController);
+
+ if (IsReadableStream(stream) === false) {
+ throw new TypeError('ReadableStreamDefaultController can only be constructed with a ReadableStream instance');
+ }
+
+ if (stream._readableStreamController !== undefined) {
+ throw new TypeError('ReadableStreamDefaultController instances can only be created by the ReadableStream constructor');
+ }
+
+ this._controlledReadableStream = stream;
+ this._underlyingSource = underlyingSource;
+ this._queue = undefined;
+ this._queueTotalSize = undefined;
+ ResetQueue(this);
+ this._started = false;
+ this._closeRequested = false;
+ this._pullAgain = false;
+ this._pulling = false;
+ var normalizedStrategy = ValidateAndNormalizeQueuingStrategy(size, highWaterMark);
+ this._strategySize = normalizedStrategy.size;
+ this._strategyHWM = normalizedStrategy.highWaterMark;
+ var controller = this;
+ var startResult = InvokeOrNoop(underlyingSource, 'start', [this]);
+ Promise.resolve(startResult).then(function () {
+ controller._started = true;
+ assert(controller._pulling === false);
+ assert(controller._pullAgain === false);
+ ReadableStreamDefaultControllerCallPullIfNeeded(controller);
+ }, function (r) {
+ ReadableStreamDefaultControllerErrorIfNeeded(controller, r);
+ })["catch"](rethrowAssertionErrorRejection);
+ }
+
+ _createClass(ReadableStreamDefaultController, [{
+ key: 'close',
+ value: function close() {
+ if (IsReadableStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('close');
+ }
+
+ if (this._closeRequested === true) {
+ throw new TypeError('The stream has already been closed; do not close it again!');
+ }
+
+ var state = this._controlledReadableStream._state;
+
+ if (state !== 'readable') {
+ throw new TypeError('The stream (in ' + state + ' state) is not in the readable state and cannot be closed');
+ }
+
+ ReadableStreamDefaultControllerClose(this);
+ }
+ }, {
+ key: 'enqueue',
+ value: function enqueue(chunk) {
+ if (IsReadableStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('enqueue');
+ }
+
+ if (this._closeRequested === true) {
+ throw new TypeError('stream is closed or draining');
+ }
+
+ var state = this._controlledReadableStream._state;
+
+ if (state !== 'readable') {
+ throw new TypeError('The stream (in ' + state + ' state) is not in the readable state and cannot be enqueued to');
+ }
+
+ return ReadableStreamDefaultControllerEnqueue(this, chunk);
+ }
+ }, {
+ key: 'error',
+ value: function error(e) {
+ if (IsReadableStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('error');
+ }
+
+ var stream = this._controlledReadableStream;
+
+ if (stream._state !== 'readable') {
+ throw new TypeError('The stream is ' + stream._state + ' and so cannot be errored');
+ }
+
+ ReadableStreamDefaultControllerError(this, e);
+ }
+ }, {
+ key: '__cancelSteps',
+ value: function __cancelSteps(reason) {
+ ResetQueue(this);
+ return PromiseInvokeOrNoop(this._underlyingSource, 'cancel', [reason]);
+ }
+ }, {
+ key: '__pullSteps',
+ value: function __pullSteps() {
+ var stream = this._controlledReadableStream;
+
+ if (this._queue.length > 0) {
+ var chunk = DequeueValue(this);
+
+ if (this._closeRequested === true && this._queue.length === 0) {
+ ReadableStreamClose(stream);
+ } else {
+ ReadableStreamDefaultControllerCallPullIfNeeded(this);
+ }
+
+ return Promise.resolve(CreateIterResultObject(chunk, false));
+ }
+
+ var pendingPromise = ReadableStreamAddReadRequest(stream);
+ ReadableStreamDefaultControllerCallPullIfNeeded(this);
+ return pendingPromise;
+ }
+ }, {
+ key: 'desiredSize',
+ get: function get() {
+ if (IsReadableStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('desiredSize');
+ }
+
+ return ReadableStreamDefaultControllerGetDesiredSize(this);
+ }
+ }]);
+
+ return ReadableStreamDefaultController;
+ }();
+
+ function IsReadableStreamDefaultController(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_underlyingSource')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function ReadableStreamDefaultControllerCallPullIfNeeded(controller) {
+ var shouldPull = ReadableStreamDefaultControllerShouldCallPull(controller);
+
+ if (shouldPull === false) {
+ return undefined;
+ }
+
+ if (controller._pulling === true) {
+ controller._pullAgain = true;
+ return undefined;
+ }
+
+ assert(controller._pullAgain === false);
+ controller._pulling = true;
+ var pullPromise = PromiseInvokeOrNoop(controller._underlyingSource, 'pull', [controller]);
+ pullPromise.then(function () {
+ controller._pulling = false;
+
+ if (controller._pullAgain === true) {
+ controller._pullAgain = false;
+ return ReadableStreamDefaultControllerCallPullIfNeeded(controller);
+ }
+
+ return undefined;
+ }, function (e) {
+ ReadableStreamDefaultControllerErrorIfNeeded(controller, e);
+ })["catch"](rethrowAssertionErrorRejection);
+ return undefined;
+ }
+
+ function ReadableStreamDefaultControllerShouldCallPull(controller) {
+ var stream = controller._controlledReadableStream;
+
+ if (stream._state === 'closed' || stream._state === 'errored') {
+ return false;
+ }
+
+ if (controller._closeRequested === true) {
+ return false;
+ }
+
+ if (controller._started === false) {
+ return false;
+ }
+
+ if (IsReadableStreamLocked(stream) === true && ReadableStreamGetNumReadRequests(stream) > 0) {
+ return true;
+ }
+
+ var desiredSize = ReadableStreamDefaultControllerGetDesiredSize(controller);
+
+ if (desiredSize > 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function ReadableStreamDefaultControllerClose(controller) {
+ var stream = controller._controlledReadableStream;
+ assert(controller._closeRequested === false);
+ assert(stream._state === 'readable');
+ controller._closeRequested = true;
+
+ if (controller._queue.length === 0) {
+ ReadableStreamClose(stream);
+ }
+ }
+
+ function ReadableStreamDefaultControllerEnqueue(controller, chunk) {
+ var stream = controller._controlledReadableStream;
+ assert(controller._closeRequested === false);
+ assert(stream._state === 'readable');
+
+ if (IsReadableStreamLocked(stream) === true && ReadableStreamGetNumReadRequests(stream) > 0) {
+ ReadableStreamFulfillReadRequest(stream, chunk, false);
+ } else {
+ var chunkSize = 1;
+
+ if (controller._strategySize !== undefined) {
+ var strategySize = controller._strategySize;
+
+ try {
+ chunkSize = strategySize(chunk);
+ } catch (chunkSizeE) {
+ ReadableStreamDefaultControllerErrorIfNeeded(controller, chunkSizeE);
+ throw chunkSizeE;
+ }
+ }
+
+ try {
+ EnqueueValueWithSize(controller, chunk, chunkSize);
+ } catch (enqueueE) {
+ ReadableStreamDefaultControllerErrorIfNeeded(controller, enqueueE);
+ throw enqueueE;
+ }
+ }
+
+ ReadableStreamDefaultControllerCallPullIfNeeded(controller);
+ return undefined;
+ }
+
+ function ReadableStreamDefaultControllerError(controller, e) {
+ var stream = controller._controlledReadableStream;
+ assert(stream._state === 'readable');
+ ResetQueue(controller);
+ ReadableStreamError(stream, e);
+ }
+
+ function ReadableStreamDefaultControllerErrorIfNeeded(controller, e) {
+ if (controller._controlledReadableStream._state === 'readable') {
+ ReadableStreamDefaultControllerError(controller, e);
+ }
+ }
+
+ function ReadableStreamDefaultControllerGetDesiredSize(controller) {
+ var stream = controller._controlledReadableStream;
+ var state = stream._state;
+
+ if (state === 'errored') {
+ return null;
+ }
+
+ if (state === 'closed') {
+ return 0;
+ }
+
+ return controller._strategyHWM - controller._queueTotalSize;
+ }
+
+ var ReadableStreamBYOBRequest = function () {
+ function ReadableStreamBYOBRequest(controller, view) {
+ _classCallCheck(this, ReadableStreamBYOBRequest);
+
+ this._associatedReadableByteStreamController = controller;
+ this._view = view;
+ }
+
+ _createClass(ReadableStreamBYOBRequest, [{
+ key: 'respond',
+ value: function respond(bytesWritten) {
+ if (IsReadableStreamBYOBRequest(this) === false) {
+ throw byobRequestBrandCheckException('respond');
+ }
+
+ if (this._associatedReadableByteStreamController === undefined) {
+ throw new TypeError('This BYOB request has been invalidated');
+ }
+
+ ReadableByteStreamControllerRespond(this._associatedReadableByteStreamController, bytesWritten);
+ }
+ }, {
+ key: 'respondWithNewView',
+ value: function respondWithNewView(view) {
+ if (IsReadableStreamBYOBRequest(this) === false) {
+ throw byobRequestBrandCheckException('respond');
+ }
+
+ if (this._associatedReadableByteStreamController === undefined) {
+ throw new TypeError('This BYOB request has been invalidated');
+ }
+
+ if (!ArrayBuffer.isView(view)) {
+ throw new TypeError('You can only respond with array buffer views');
+ }
+
+ ReadableByteStreamControllerRespondWithNewView(this._associatedReadableByteStreamController, view);
+ }
+ }, {
+ key: 'view',
+ get: function get() {
+ return this._view;
+ }
+ }]);
+
+ return ReadableStreamBYOBRequest;
+ }();
+
+ var ReadableByteStreamController = function () {
+ function ReadableByteStreamController(stream, underlyingByteSource, highWaterMark) {
+ _classCallCheck(this, ReadableByteStreamController);
+
+ if (IsReadableStream(stream) === false) {
+ throw new TypeError('ReadableByteStreamController can only be constructed with a ReadableStream instance given ' + 'a byte source');
+ }
+
+ if (stream._readableStreamController !== undefined) {
+ throw new TypeError('ReadableByteStreamController instances can only be created by the ReadableStream constructor given a byte ' + 'source');
+ }
+
+ this._controlledReadableStream = stream;
+ this._underlyingByteSource = underlyingByteSource;
+ this._pullAgain = false;
+ this._pulling = false;
+ ReadableByteStreamControllerClearPendingPullIntos(this);
+ this._queue = this._queueTotalSize = undefined;
+ ResetQueue(this);
+ this._closeRequested = false;
+ this._started = false;
+ this._strategyHWM = ValidateAndNormalizeHighWaterMark(highWaterMark);
+ var autoAllocateChunkSize = underlyingByteSource.autoAllocateChunkSize;
+
+ if (autoAllocateChunkSize !== undefined) {
+ if (Number.isInteger(autoAllocateChunkSize) === false || autoAllocateChunkSize <= 0) {
+ throw new RangeError('autoAllocateChunkSize must be a positive integer');
+ }
+ }
+
+ this._autoAllocateChunkSize = autoAllocateChunkSize;
+ this._pendingPullIntos = [];
+ var controller = this;
+ var startResult = InvokeOrNoop(underlyingByteSource, 'start', [this]);
+ Promise.resolve(startResult).then(function () {
+ controller._started = true;
+ assert(controller._pulling === false);
+ assert(controller._pullAgain === false);
+ ReadableByteStreamControllerCallPullIfNeeded(controller);
+ }, function (r) {
+ if (stream._state === 'readable') {
+ ReadableByteStreamControllerError(controller, r);
+ }
+ })["catch"](rethrowAssertionErrorRejection);
+ }
+
+ _createClass(ReadableByteStreamController, [{
+ key: 'close',
+ value: function close() {
+ if (IsReadableByteStreamController(this) === false) {
+ throw byteStreamControllerBrandCheckException('close');
+ }
+
+ if (this._closeRequested === true) {
+ throw new TypeError('The stream has already been closed; do not close it again!');
+ }
+
+ var state = this._controlledReadableStream._state;
+
+ if (state !== 'readable') {
+ throw new TypeError('The stream (in ' + state + ' state) is not in the readable state and cannot be closed');
+ }
+
+ ReadableByteStreamControllerClose(this);
+ }
+ }, {
+ key: 'enqueue',
+ value: function enqueue(chunk) {
+ if (IsReadableByteStreamController(this) === false) {
+ throw byteStreamControllerBrandCheckException('enqueue');
+ }
+
+ if (this._closeRequested === true) {
+ throw new TypeError('stream is closed or draining');
+ }
+
+ var state = this._controlledReadableStream._state;
+
+ if (state !== 'readable') {
+ throw new TypeError('The stream (in ' + state + ' state) is not in the readable state and cannot be enqueued to');
+ }
+
+ if (!ArrayBuffer.isView(chunk)) {
+ throw new TypeError('You can only enqueue array buffer views when using a ReadableByteStreamController');
+ }
+
+ ReadableByteStreamControllerEnqueue(this, chunk);
+ }
+ }, {
+ key: 'error',
+ value: function error(e) {
+ if (IsReadableByteStreamController(this) === false) {
+ throw byteStreamControllerBrandCheckException('error');
+ }
+
+ var stream = this._controlledReadableStream;
+
+ if (stream._state !== 'readable') {
+ throw new TypeError('The stream is ' + stream._state + ' and so cannot be errored');
+ }
+
+ ReadableByteStreamControllerError(this, e);
+ }
+ }, {
+ key: '__cancelSteps',
+ value: function __cancelSteps(reason) {
+ if (this._pendingPullIntos.length > 0) {
+ var firstDescriptor = this._pendingPullIntos[0];
+ firstDescriptor.bytesFilled = 0;
+ }
+
+ ResetQueue(this);
+ return PromiseInvokeOrNoop(this._underlyingByteSource, 'cancel', [reason]);
+ }
+ }, {
+ key: '__pullSteps',
+ value: function __pullSteps() {
+ var stream = this._controlledReadableStream;
+ assert(ReadableStreamHasDefaultReader(stream) === true);
+
+ if (this._queueTotalSize > 0) {
+ assert(ReadableStreamGetNumReadRequests(stream) === 0);
+
+ var entry = this._queue.shift();
+
+ this._queueTotalSize -= entry.byteLength;
+ ReadableByteStreamControllerHandleQueueDrain(this);
+ var view = void 0;
+
+ try {
+ view = new Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength);
+ } catch (viewE) {
+ return Promise.reject(viewE);
+ }
+
+ return Promise.resolve(CreateIterResultObject(view, false));
+ }
+
+ var autoAllocateChunkSize = this._autoAllocateChunkSize;
+
+ if (autoAllocateChunkSize !== undefined) {
+ var buffer = void 0;
+
+ try {
+ buffer = new ArrayBuffer(autoAllocateChunkSize);
+ } catch (bufferE) {
+ return Promise.reject(bufferE);
+ }
+
+ var pullIntoDescriptor = {
+ buffer: buffer,
+ byteOffset: 0,
+ byteLength: autoAllocateChunkSize,
+ bytesFilled: 0,
+ elementSize: 1,
+ ctor: Uint8Array,
+ readerType: 'default'
+ };
+
+ this._pendingPullIntos.push(pullIntoDescriptor);
+ }
+
+ var promise = ReadableStreamAddReadRequest(stream);
+ ReadableByteStreamControllerCallPullIfNeeded(this);
+ return promise;
+ }
+ }, {
+ key: 'byobRequest',
+ get: function get() {
+ if (IsReadableByteStreamController(this) === false) {
+ throw byteStreamControllerBrandCheckException('byobRequest');
+ }
+
+ if (this._byobRequest === undefined && this._pendingPullIntos.length > 0) {
+ var firstDescriptor = this._pendingPullIntos[0];
+ var view = new Uint8Array(firstDescriptor.buffer, firstDescriptor.byteOffset + firstDescriptor.bytesFilled, firstDescriptor.byteLength - firstDescriptor.bytesFilled);
+ this._byobRequest = new ReadableStreamBYOBRequest(this, view);
+ }
+
+ return this._byobRequest;
+ }
+ }, {
+ key: 'desiredSize',
+ get: function get() {
+ if (IsReadableByteStreamController(this) === false) {
+ throw byteStreamControllerBrandCheckException('desiredSize');
+ }
+
+ return ReadableByteStreamControllerGetDesiredSize(this);
+ }
+ }]);
+
+ return ReadableByteStreamController;
+ }();
+
+ function IsReadableByteStreamController(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_underlyingByteSource')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function IsReadableStreamBYOBRequest(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_associatedReadableByteStreamController')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function ReadableByteStreamControllerCallPullIfNeeded(controller) {
+ var shouldPull = ReadableByteStreamControllerShouldCallPull(controller);
+
+ if (shouldPull === false) {
+ return undefined;
+ }
+
+ if (controller._pulling === true) {
+ controller._pullAgain = true;
+ return undefined;
+ }
+
+ assert(controller._pullAgain === false);
+ controller._pulling = true;
+ var pullPromise = PromiseInvokeOrNoop(controller._underlyingByteSource, 'pull', [controller]);
+ pullPromise.then(function () {
+ controller._pulling = false;
+
+ if (controller._pullAgain === true) {
+ controller._pullAgain = false;
+ ReadableByteStreamControllerCallPullIfNeeded(controller);
+ }
+ }, function (e) {
+ if (controller._controlledReadableStream._state === 'readable') {
+ ReadableByteStreamControllerError(controller, e);
+ }
+ })["catch"](rethrowAssertionErrorRejection);
+ return undefined;
+ }
+
+ function ReadableByteStreamControllerClearPendingPullIntos(controller) {
+ ReadableByteStreamControllerInvalidateBYOBRequest(controller);
+ controller._pendingPullIntos = [];
+ }
+
+ function ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor) {
+ assert(stream._state !== 'errored', 'state must not be errored');
+ var done = false;
+
+ if (stream._state === 'closed') {
+ assert(pullIntoDescriptor.bytesFilled === 0);
+ done = true;
+ }
+
+ var filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor);
+
+ if (pullIntoDescriptor.readerType === 'default') {
+ ReadableStreamFulfillReadRequest(stream, filledView, done);
+ } else {
+ assert(pullIntoDescriptor.readerType === 'byob');
+ ReadableStreamFulfillReadIntoRequest(stream, filledView, done);
+ }
+ }
+
+ function ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor) {
+ var bytesFilled = pullIntoDescriptor.bytesFilled;
+ var elementSize = pullIntoDescriptor.elementSize;
+ assert(bytesFilled <= pullIntoDescriptor.byteLength);
+ assert(bytesFilled % elementSize === 0);
+ return new pullIntoDescriptor.ctor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, bytesFilled / elementSize);
+ }
+
+ function ReadableByteStreamControllerEnqueueChunkToQueue(controller, buffer, byteOffset, byteLength) {
+ controller._queue.push({
+ buffer: buffer,
+ byteOffset: byteOffset,
+ byteLength: byteLength
+ });
+
+ controller._queueTotalSize += byteLength;
+ }
+
+ function ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) {
+ var elementSize = pullIntoDescriptor.elementSize;
+ var currentAlignedBytes = pullIntoDescriptor.bytesFilled - pullIntoDescriptor.bytesFilled % elementSize;
+ var maxBytesToCopy = Math.min(controller._queueTotalSize, pullIntoDescriptor.byteLength - pullIntoDescriptor.bytesFilled);
+ var maxBytesFilled = pullIntoDescriptor.bytesFilled + maxBytesToCopy;
+ var maxAlignedBytes = maxBytesFilled - maxBytesFilled % elementSize;
+ var totalBytesToCopyRemaining = maxBytesToCopy;
+ var ready = false;
+
+ if (maxAlignedBytes > currentAlignedBytes) {
+ totalBytesToCopyRemaining = maxAlignedBytes - pullIntoDescriptor.bytesFilled;
+ ready = true;
+ }
+
+ var queue = controller._queue;
+
+ while (totalBytesToCopyRemaining > 0) {
+ var headOfQueue = queue[0];
+ var bytesToCopy = Math.min(totalBytesToCopyRemaining, headOfQueue.byteLength);
+ var destStart = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled;
+ ArrayBufferCopy(pullIntoDescriptor.buffer, destStart, headOfQueue.buffer, headOfQueue.byteOffset, bytesToCopy);
+
+ if (headOfQueue.byteLength === bytesToCopy) {
+ queue.shift();
+ } else {
+ headOfQueue.byteOffset += bytesToCopy;
+ headOfQueue.byteLength -= bytesToCopy;
+ }
+
+ controller._queueTotalSize -= bytesToCopy;
+ ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesToCopy, pullIntoDescriptor);
+ totalBytesToCopyRemaining -= bytesToCopy;
+ }
+
+ if (ready === false) {
+ assert(controller._queueTotalSize === 0, 'queue must be empty');
+ assert(pullIntoDescriptor.bytesFilled > 0);
+ assert(pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize);
+ }
+
+ return ready;
+ }
+
+ function ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, size, pullIntoDescriptor) {
+ assert(controller._pendingPullIntos.length === 0 || controller._pendingPullIntos[0] === pullIntoDescriptor);
+ ReadableByteStreamControllerInvalidateBYOBRequest(controller);
+ pullIntoDescriptor.bytesFilled += size;
+ }
+
+ function ReadableByteStreamControllerHandleQueueDrain(controller) {
+ assert(controller._controlledReadableStream._state === 'readable');
+
+ if (controller._queueTotalSize === 0 && controller._closeRequested === true) {
+ ReadableStreamClose(controller._controlledReadableStream);
+ } else {
+ ReadableByteStreamControllerCallPullIfNeeded(controller);
+ }
+ }
+
+ function ReadableByteStreamControllerInvalidateBYOBRequest(controller) {
+ if (controller._byobRequest === undefined) {
+ return;
+ }
+
+ controller._byobRequest._associatedReadableByteStreamController = undefined;
+ controller._byobRequest._view = undefined;
+ controller._byobRequest = undefined;
+ }
+
+ function ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller) {
+ assert(controller._closeRequested === false);
+
+ while (controller._pendingPullIntos.length > 0) {
+ if (controller._queueTotalSize === 0) {
+ return;
+ }
+
+ var pullIntoDescriptor = controller._pendingPullIntos[0];
+
+ if (ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) === true) {
+ ReadableByteStreamControllerShiftPendingPullInto(controller);
+ ReadableByteStreamControllerCommitPullIntoDescriptor(controller._controlledReadableStream, pullIntoDescriptor);
+ }
+ }
+ }
+
+ function ReadableByteStreamControllerPullInto(controller, view) {
+ var stream = controller._controlledReadableStream;
+ var elementSize = 1;
+
+ if (view.constructor !== DataView) {
+ elementSize = view.constructor.BYTES_PER_ELEMENT;
+ }
+
+ var ctor = view.constructor;
+ var pullIntoDescriptor = {
+ buffer: view.buffer,
+ byteOffset: view.byteOffset,
+ byteLength: view.byteLength,
+ bytesFilled: 0,
+ elementSize: elementSize,
+ ctor: ctor,
+ readerType: 'byob'
+ };
+
+ if (controller._pendingPullIntos.length > 0) {
+ pullIntoDescriptor.buffer = TransferArrayBuffer(pullIntoDescriptor.buffer);
+
+ controller._pendingPullIntos.push(pullIntoDescriptor);
+
+ return ReadableStreamAddReadIntoRequest(stream);
+ }
+
+ if (stream._state === 'closed') {
+ var emptyView = new view.constructor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, 0);
+ return Promise.resolve(CreateIterResultObject(emptyView, true));
+ }
+
+ if (controller._queueTotalSize > 0) {
+ if (ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) === true) {
+ var filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor);
+ ReadableByteStreamControllerHandleQueueDrain(controller);
+ return Promise.resolve(CreateIterResultObject(filledView, false));
+ }
+
+ if (controller._closeRequested === true) {
+ var e = new TypeError('Insufficient bytes to fill elements in the given buffer');
+ ReadableByteStreamControllerError(controller, e);
+ return Promise.reject(e);
+ }
+ }
+
+ pullIntoDescriptor.buffer = TransferArrayBuffer(pullIntoDescriptor.buffer);
+
+ controller._pendingPullIntos.push(pullIntoDescriptor);
+
+ var promise = ReadableStreamAddReadIntoRequest(stream);
+ ReadableByteStreamControllerCallPullIfNeeded(controller);
+ return promise;
+ }
+
+ function ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor) {
+ firstDescriptor.buffer = TransferArrayBuffer(firstDescriptor.buffer);
+ assert(firstDescriptor.bytesFilled === 0, 'bytesFilled must be 0');
+ var stream = controller._controlledReadableStream;
+
+ if (ReadableStreamHasBYOBReader(stream) === true) {
+ while (ReadableStreamGetNumReadIntoRequests(stream) > 0) {
+ var pullIntoDescriptor = ReadableByteStreamControllerShiftPendingPullInto(controller);
+ ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor);
+ }
+ }
+ }
+
+ function ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, pullIntoDescriptor) {
+ if (pullIntoDescriptor.bytesFilled + bytesWritten > pullIntoDescriptor.byteLength) {
+ throw new RangeError('bytesWritten out of range');
+ }
+
+ ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesWritten, pullIntoDescriptor);
+
+ if (pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize) {
+ return;
+ }
+
+ ReadableByteStreamControllerShiftPendingPullInto(controller);
+ var remainderSize = pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize;
+
+ if (remainderSize > 0) {
+ var end = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled;
+ var remainder = pullIntoDescriptor.buffer.slice(end - remainderSize, end);
+ ReadableByteStreamControllerEnqueueChunkToQueue(controller, remainder, 0, remainder.byteLength);
+ }
+
+ pullIntoDescriptor.buffer = TransferArrayBuffer(pullIntoDescriptor.buffer);
+ pullIntoDescriptor.bytesFilled -= remainderSize;
+ ReadableByteStreamControllerCommitPullIntoDescriptor(controller._controlledReadableStream, pullIntoDescriptor);
+ ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);
+ }
+
+ function ReadableByteStreamControllerRespondInternal(controller, bytesWritten) {
+ var firstDescriptor = controller._pendingPullIntos[0];
+ var stream = controller._controlledReadableStream;
+
+ if (stream._state === 'closed') {
+ if (bytesWritten !== 0) {
+ throw new TypeError('bytesWritten must be 0 when calling respond() on a closed stream');
+ }
+
+ ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor);
+ } else {
+ assert(stream._state === 'readable');
+ ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, firstDescriptor);
+ }
+ }
+
+ function ReadableByteStreamControllerShiftPendingPullInto(controller) {
+ var descriptor = controller._pendingPullIntos.shift();
+
+ ReadableByteStreamControllerInvalidateBYOBRequest(controller);
+ return descriptor;
+ }
+
+ function ReadableByteStreamControllerShouldCallPull(controller) {
+ var stream = controller._controlledReadableStream;
+
+ if (stream._state !== 'readable') {
+ return false;
+ }
+
+ if (controller._closeRequested === true) {
+ return false;
+ }
+
+ if (controller._started === false) {
+ return false;
+ }
+
+ if (ReadableStreamHasDefaultReader(stream) === true && ReadableStreamGetNumReadRequests(stream) > 0) {
+ return true;
+ }
+
+ if (ReadableStreamHasBYOBReader(stream) === true && ReadableStreamGetNumReadIntoRequests(stream) > 0) {
+ return true;
+ }
+
+ if (ReadableByteStreamControllerGetDesiredSize(controller) > 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function ReadableByteStreamControllerClose(controller) {
+ var stream = controller._controlledReadableStream;
+ assert(controller._closeRequested === false);
+ assert(stream._state === 'readable');
+
+ if (controller._queueTotalSize > 0) {
+ controller._closeRequested = true;
+ return;
+ }
+
+ if (controller._pendingPullIntos.length > 0) {
+ var firstPendingPullInto = controller._pendingPullIntos[0];
+
+ if (firstPendingPullInto.bytesFilled > 0) {
+ var e = new TypeError('Insufficient bytes to fill elements in the given buffer');
+ ReadableByteStreamControllerError(controller, e);
+ throw e;
+ }
+ }
+
+ ReadableStreamClose(stream);
+ }
+
+ function ReadableByteStreamControllerEnqueue(controller, chunk) {
+ var stream = controller._controlledReadableStream;
+ assert(controller._closeRequested === false);
+ assert(stream._state === 'readable');
+ var buffer = chunk.buffer;
+ var byteOffset = chunk.byteOffset;
+ var byteLength = chunk.byteLength;
+ var transferredBuffer = TransferArrayBuffer(buffer);
+
+ if (ReadableStreamHasDefaultReader(stream) === true) {
+ if (ReadableStreamGetNumReadRequests(stream) === 0) {
+ ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength);
+ } else {
+ assert(controller._queue.length === 0);
+ var transferredView = new Uint8Array(transferredBuffer, byteOffset, byteLength);
+ ReadableStreamFulfillReadRequest(stream, transferredView, false);
+ }
+ } else if (ReadableStreamHasBYOBReader(stream) === true) {
+ ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength);
+ ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);
+ } else {
+ assert(IsReadableStreamLocked(stream) === false, 'stream must not be locked');
+ ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength);
+ }
+ }
+
+ function ReadableByteStreamControllerError(controller, e) {
+ var stream = controller._controlledReadableStream;
+ assert(stream._state === 'readable');
+ ReadableByteStreamControllerClearPendingPullIntos(controller);
+ ResetQueue(controller);
+ ReadableStreamError(stream, e);
+ }
+
+ function ReadableByteStreamControllerGetDesiredSize(controller) {
+ var stream = controller._controlledReadableStream;
+ var state = stream._state;
+
+ if (state === 'errored') {
+ return null;
+ }
+
+ if (state === 'closed') {
+ return 0;
+ }
+
+ return controller._strategyHWM - controller._queueTotalSize;
+ }
+
+ function ReadableByteStreamControllerRespond(controller, bytesWritten) {
+ bytesWritten = Number(bytesWritten);
+
+ if (IsFiniteNonNegativeNumber(bytesWritten) === false) {
+ throw new RangeError('bytesWritten must be a finite');
+ }
+
+ assert(controller._pendingPullIntos.length > 0);
+ ReadableByteStreamControllerRespondInternal(controller, bytesWritten);
+ }
+
+ function ReadableByteStreamControllerRespondWithNewView(controller, view) {
+ assert(controller._pendingPullIntos.length > 0);
+ var firstDescriptor = controller._pendingPullIntos[0];
+
+ if (firstDescriptor.byteOffset + firstDescriptor.bytesFilled !== view.byteOffset) {
+ throw new RangeError('The region specified by view does not match byobRequest');
+ }
+
+ if (firstDescriptor.byteLength !== view.byteLength) {
+ throw new RangeError('The buffer of view has different capacity than byobRequest');
+ }
+
+ firstDescriptor.buffer = view.buffer;
+ ReadableByteStreamControllerRespondInternal(controller, view.byteLength);
+ }
+
+ function streamBrandCheckException(name) {
+ return new TypeError('ReadableStream.prototype.' + name + ' can only be used on a ReadableStream');
+ }
+
+ function readerLockException(name) {
+ return new TypeError('Cannot ' + name + ' a stream using a released reader');
+ }
+
+ function defaultReaderBrandCheckException(name) {
+ return new TypeError('ReadableStreamDefaultReader.prototype.' + name + ' can only be used on a ReadableStreamDefaultReader');
+ }
+
+ function defaultReaderClosedPromiseInitialize(reader) {
+ reader._closedPromise = new Promise(function (resolve, reject) {
+ reader._closedPromise_resolve = resolve;
+ reader._closedPromise_reject = reject;
+ });
+ }
+
+ function defaultReaderClosedPromiseInitializeAsRejected(reader, reason) {
+ reader._closedPromise = Promise.reject(reason);
+ reader._closedPromise_resolve = undefined;
+ reader._closedPromise_reject = undefined;
+ }
+
+ function defaultReaderClosedPromiseInitializeAsResolved(reader) {
+ reader._closedPromise = Promise.resolve(undefined);
+ reader._closedPromise_resolve = undefined;
+ reader._closedPromise_reject = undefined;
+ }
+
+ function defaultReaderClosedPromiseReject(reader, reason) {
+ assert(reader._closedPromise_resolve !== undefined);
+ assert(reader._closedPromise_reject !== undefined);
+
+ reader._closedPromise_reject(reason);
+
+ reader._closedPromise_resolve = undefined;
+ reader._closedPromise_reject = undefined;
+ }
+
+ function defaultReaderClosedPromiseResetToRejected(reader, reason) {
+ assert(reader._closedPromise_resolve === undefined);
+ assert(reader._closedPromise_reject === undefined);
+ reader._closedPromise = Promise.reject(reason);
+ }
+
+ function defaultReaderClosedPromiseResolve(reader) {
+ assert(reader._closedPromise_resolve !== undefined);
+ assert(reader._closedPromise_reject !== undefined);
+
+ reader._closedPromise_resolve(undefined);
+
+ reader._closedPromise_resolve = undefined;
+ reader._closedPromise_reject = undefined;
+ }
+
+ function byobReaderBrandCheckException(name) {
+ return new TypeError('ReadableStreamBYOBReader.prototype.' + name + ' can only be used on a ReadableStreamBYOBReader');
+ }
+
+ function defaultControllerBrandCheckException(name) {
+ return new TypeError('ReadableStreamDefaultController.prototype.' + name + ' can only be used on a ReadableStreamDefaultController');
+ }
+
+ function byobRequestBrandCheckException(name) {
+ return new TypeError('ReadableStreamBYOBRequest.prototype.' + name + ' can only be used on a ReadableStreamBYOBRequest');
+ }
+
+ function byteStreamControllerBrandCheckException(name) {
+ return new TypeError('ReadableByteStreamController.prototype.' + name + ' can only be used on a ReadableByteStreamController');
+ }
+
+ function ifIsObjectAndHasAPromiseIsHandledInternalSlotSetPromiseIsHandledToTrue(promise) {
+ try {
+ Promise.prototype.then.call(promise, undefined, function () {});
+ } catch (e) {}
+ }
+}, function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ var transformStream = __w_pdfjs_require__(6);
+
+ var readableStream = __w_pdfjs_require__(4);
+
+ var writableStream = __w_pdfjs_require__(2);
+
+ exports.TransformStream = transformStream.TransformStream;
+ exports.ReadableStream = readableStream.ReadableStream;
+ exports.IsReadableStreamDisturbed = readableStream.IsReadableStreamDisturbed;
+ exports.ReadableStreamDefaultControllerClose = readableStream.ReadableStreamDefaultControllerClose;
+ exports.ReadableStreamDefaultControllerEnqueue = readableStream.ReadableStreamDefaultControllerEnqueue;
+ exports.ReadableStreamDefaultControllerError = readableStream.ReadableStreamDefaultControllerError;
+ exports.ReadableStreamDefaultControllerGetDesiredSize = readableStream.ReadableStreamDefaultControllerGetDesiredSize;
+ exports.AcquireWritableStreamDefaultWriter = writableStream.AcquireWritableStreamDefaultWriter;
+ exports.IsWritableStream = writableStream.IsWritableStream;
+ exports.IsWritableStreamLocked = writableStream.IsWritableStreamLocked;
+ exports.WritableStream = writableStream.WritableStream;
+ exports.WritableStreamAbort = writableStream.WritableStreamAbort;
+ exports.WritableStreamDefaultControllerError = writableStream.WritableStreamDefaultControllerError;
+ exports.WritableStreamDefaultWriterCloseWithErrorPropagation = writableStream.WritableStreamDefaultWriterCloseWithErrorPropagation;
+ exports.WritableStreamDefaultWriterRelease = writableStream.WritableStreamDefaultWriterRelease;
+ exports.WritableStreamDefaultWriterWrite = writableStream.WritableStreamDefaultWriterWrite;
+}, function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ var _createClass = function () {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ return function (Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+ }();
+
+ function _classCallCheck(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ }
+
+ var _require = __w_pdfjs_require__(1),
+ assert = _require.assert;
+
+ var _require2 = __w_pdfjs_require__(0),
+ InvokeOrNoop = _require2.InvokeOrNoop,
+ PromiseInvokeOrPerformFallback = _require2.PromiseInvokeOrPerformFallback,
+ PromiseInvokeOrNoop = _require2.PromiseInvokeOrNoop,
+ typeIsObject = _require2.typeIsObject;
+
+ var _require3 = __w_pdfjs_require__(4),
+ ReadableStream = _require3.ReadableStream,
+ ReadableStreamDefaultControllerClose = _require3.ReadableStreamDefaultControllerClose,
+ ReadableStreamDefaultControllerEnqueue = _require3.ReadableStreamDefaultControllerEnqueue,
+ ReadableStreamDefaultControllerError = _require3.ReadableStreamDefaultControllerError,
+ ReadableStreamDefaultControllerGetDesiredSize = _require3.ReadableStreamDefaultControllerGetDesiredSize;
+
+ var _require4 = __w_pdfjs_require__(2),
+ WritableStream = _require4.WritableStream,
+ WritableStreamDefaultControllerError = _require4.WritableStreamDefaultControllerError;
+
+ function TransformStreamCloseReadable(transformStream) {
+ if (transformStream._errored === true) {
+ throw new TypeError('TransformStream is already errored');
+ }
+
+ if (transformStream._readableClosed === true) {
+ throw new TypeError('Readable side is already closed');
+ }
+
+ TransformStreamCloseReadableInternal(transformStream);
+ }
+
+ function TransformStreamEnqueueToReadable(transformStream, chunk) {
+ if (transformStream._errored === true) {
+ throw new TypeError('TransformStream is already errored');
+ }
+
+ if (transformStream._readableClosed === true) {
+ throw new TypeError('Readable side is already closed');
+ }
+
+ var controller = transformStream._readableController;
+
+ try {
+ ReadableStreamDefaultControllerEnqueue(controller, chunk);
+ } catch (e) {
+ transformStream._readableClosed = true;
+ TransformStreamErrorIfNeeded(transformStream, e);
+ throw transformStream._storedError;
+ }
+
+ var desiredSize = ReadableStreamDefaultControllerGetDesiredSize(controller);
+ var maybeBackpressure = desiredSize <= 0;
+
+ if (maybeBackpressure === true && transformStream._backpressure === false) {
+ TransformStreamSetBackpressure(transformStream, true);
+ }
+ }
+
+ function TransformStreamError(transformStream, e) {
+ if (transformStream._errored === true) {
+ throw new TypeError('TransformStream is already errored');
+ }
+
+ TransformStreamErrorInternal(transformStream, e);
+ }
+
+ function TransformStreamCloseReadableInternal(transformStream) {
+ assert(transformStream._errored === false);
+ assert(transformStream._readableClosed === false);
+
+ try {
+ ReadableStreamDefaultControllerClose(transformStream._readableController);
+ } catch (e) {
+ assert(false);
+ }
+
+ transformStream._readableClosed = true;
+ }
+
+ function TransformStreamErrorIfNeeded(transformStream, e) {
+ if (transformStream._errored === false) {
+ TransformStreamErrorInternal(transformStream, e);
+ }
+ }
+
+ function TransformStreamErrorInternal(transformStream, e) {
+ assert(transformStream._errored === false);
+ transformStream._errored = true;
+ transformStream._storedError = e;
+
+ if (transformStream._writableDone === false) {
+ WritableStreamDefaultControllerError(transformStream._writableController, e);
+ }
+
+ if (transformStream._readableClosed === false) {
+ ReadableStreamDefaultControllerError(transformStream._readableController, e);
+ }
+ }
+
+ function TransformStreamReadableReadyPromise(transformStream) {
+ assert(transformStream._backpressureChangePromise !== undefined, '_backpressureChangePromise should have been initialized');
+
+ if (transformStream._backpressure === false) {
+ return Promise.resolve();
+ }
+
+ assert(transformStream._backpressure === true, '_backpressure should have been initialized');
+ return transformStream._backpressureChangePromise;
+ }
+
+ function TransformStreamSetBackpressure(transformStream, backpressure) {
+ assert(transformStream._backpressure !== backpressure, 'TransformStreamSetBackpressure() should be called only when backpressure is changed');
+
+ if (transformStream._backpressureChangePromise !== undefined) {
+ transformStream._backpressureChangePromise_resolve(backpressure);
+ }
+
+ transformStream._backpressureChangePromise = new Promise(function (resolve) {
+ transformStream._backpressureChangePromise_resolve = resolve;
+ });
+
+ transformStream._backpressureChangePromise.then(function (resolution) {
+ assert(resolution !== backpressure, '_backpressureChangePromise should be fulfilled only when backpressure is changed');
+ });
+
+ transformStream._backpressure = backpressure;
+ }
+
+ function TransformStreamDefaultTransform(chunk, transformStreamController) {
+ var transformStream = transformStreamController._controlledTransformStream;
+ TransformStreamEnqueueToReadable(transformStream, chunk);
+ return Promise.resolve();
+ }
+
+ function TransformStreamTransform(transformStream, chunk) {
+ assert(transformStream._errored === false);
+ assert(transformStream._transforming === false);
+ assert(transformStream._backpressure === false);
+ transformStream._transforming = true;
+ var transformer = transformStream._transformer;
+ var controller = transformStream._transformStreamController;
+ var transformPromise = PromiseInvokeOrPerformFallback(transformer, 'transform', [chunk, controller], TransformStreamDefaultTransform, [chunk, controller]);
+ return transformPromise.then(function () {
+ transformStream._transforming = false;
+ return TransformStreamReadableReadyPromise(transformStream);
+ }, function (e) {
+ TransformStreamErrorIfNeeded(transformStream, e);
+ return Promise.reject(e);
+ });
+ }
+
+ function IsTransformStreamDefaultController(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_controlledTransformStream')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function IsTransformStream(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_transformStreamController')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ var TransformStreamSink = function () {
+ function TransformStreamSink(transformStream, startPromise) {
+ _classCallCheck(this, TransformStreamSink);
+
+ this._transformStream = transformStream;
+ this._startPromise = startPromise;
+ }
+
+ _createClass(TransformStreamSink, [{
+ key: 'start',
+ value: function start(c) {
+ var transformStream = this._transformStream;
+ transformStream._writableController = c;
+ return this._startPromise.then(function () {
+ return TransformStreamReadableReadyPromise(transformStream);
+ });
+ }
+ }, {
+ key: 'write',
+ value: function write(chunk) {
+ var transformStream = this._transformStream;
+ return TransformStreamTransform(transformStream, chunk);
+ }
+ }, {
+ key: 'abort',
+ value: function abort() {
+ var transformStream = this._transformStream;
+ transformStream._writableDone = true;
+ TransformStreamErrorInternal(transformStream, new TypeError('Writable side aborted'));
+ }
+ }, {
+ key: 'close',
+ value: function close() {
+ var transformStream = this._transformStream;
+ assert(transformStream._transforming === false);
+ transformStream._writableDone = true;
+ var flushPromise = PromiseInvokeOrNoop(transformStream._transformer, 'flush', [transformStream._transformStreamController]);
+ return flushPromise.then(function () {
+ if (transformStream._errored === true) {
+ return Promise.reject(transformStream._storedError);
+ }
+
+ if (transformStream._readableClosed === false) {
+ TransformStreamCloseReadableInternal(transformStream);
+ }
+
+ return Promise.resolve();
+ })["catch"](function (r) {
+ TransformStreamErrorIfNeeded(transformStream, r);
+ return Promise.reject(transformStream._storedError);
+ });
+ }
+ }]);
+
+ return TransformStreamSink;
+ }();
+
+ var TransformStreamSource = function () {
+ function TransformStreamSource(transformStream, startPromise) {
+ _classCallCheck(this, TransformStreamSource);
+
+ this._transformStream = transformStream;
+ this._startPromise = startPromise;
+ }
+
+ _createClass(TransformStreamSource, [{
+ key: 'start',
+ value: function start(c) {
+ var transformStream = this._transformStream;
+ transformStream._readableController = c;
+ return this._startPromise.then(function () {
+ assert(transformStream._backpressureChangePromise !== undefined, '_backpressureChangePromise should have been initialized');
+
+ if (transformStream._backpressure === true) {
+ return Promise.resolve();
+ }
+
+ assert(transformStream._backpressure === false, '_backpressure should have been initialized');
+ return transformStream._backpressureChangePromise;
+ });
+ }
+ }, {
+ key: 'pull',
+ value: function pull() {
+ var transformStream = this._transformStream;
+ assert(transformStream._backpressure === true, 'pull() should be never called while _backpressure is false');
+ assert(transformStream._backpressureChangePromise !== undefined, '_backpressureChangePromise should have been initialized');
+ TransformStreamSetBackpressure(transformStream, false);
+ return transformStream._backpressureChangePromise;
+ }
+ }, {
+ key: 'cancel',
+ value: function cancel() {
+ var transformStream = this._transformStream;
+ transformStream._readableClosed = true;
+ TransformStreamErrorInternal(transformStream, new TypeError('Readable side canceled'));
+ }
+ }]);
+
+ return TransformStreamSource;
+ }();
+
+ var TransformStreamDefaultController = function () {
+ function TransformStreamDefaultController(transformStream) {
+ _classCallCheck(this, TransformStreamDefaultController);
+
+ if (IsTransformStream(transformStream) === false) {
+ throw new TypeError('TransformStreamDefaultController can only be ' + 'constructed with a TransformStream instance');
+ }
+
+ if (transformStream._transformStreamController !== undefined) {
+ throw new TypeError('TransformStreamDefaultController instances can ' + 'only be created by the TransformStream constructor');
+ }
+
+ this._controlledTransformStream = transformStream;
+ }
+
+ _createClass(TransformStreamDefaultController, [{
+ key: 'enqueue',
+ value: function enqueue(chunk) {
+ if (IsTransformStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('enqueue');
+ }
+
+ TransformStreamEnqueueToReadable(this._controlledTransformStream, chunk);
+ }
+ }, {
+ key: 'close',
+ value: function close() {
+ if (IsTransformStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('close');
+ }
+
+ TransformStreamCloseReadable(this._controlledTransformStream);
+ }
+ }, {
+ key: 'error',
+ value: function error(reason) {
+ if (IsTransformStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('error');
+ }
+
+ TransformStreamError(this._controlledTransformStream, reason);
+ }
+ }, {
+ key: 'desiredSize',
+ get: function get() {
+ if (IsTransformStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('desiredSize');
+ }
+
+ var transformStream = this._controlledTransformStream;
+ var readableController = transformStream._readableController;
+ return ReadableStreamDefaultControllerGetDesiredSize(readableController);
+ }
+ }]);
+
+ return TransformStreamDefaultController;
+ }();
+
+ var TransformStream = function () {
+ function TransformStream() {
+ var transformer = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ _classCallCheck(this, TransformStream);
+
+ this._transformer = transformer;
+ var readableStrategy = transformer.readableStrategy,
+ writableStrategy = transformer.writableStrategy;
+ this._transforming = false;
+ this._errored = false;
+ this._storedError = undefined;
+ this._writableController = undefined;
+ this._readableController = undefined;
+ this._transformStreamController = undefined;
+ this._writableDone = false;
+ this._readableClosed = false;
+ this._backpressure = undefined;
+ this._backpressureChangePromise = undefined;
+ this._backpressureChangePromise_resolve = undefined;
+ this._transformStreamController = new TransformStreamDefaultController(this);
+ var startPromise_resolve = void 0;
+ var startPromise = new Promise(function (resolve) {
+ startPromise_resolve = resolve;
+ });
+ var source = new TransformStreamSource(this, startPromise);
+ this._readable = new ReadableStream(source, readableStrategy);
+ var sink = new TransformStreamSink(this, startPromise);
+ this._writable = new WritableStream(sink, writableStrategy);
+ assert(this._writableController !== undefined);
+ assert(this._readableController !== undefined);
+ var desiredSize = ReadableStreamDefaultControllerGetDesiredSize(this._readableController);
+ TransformStreamSetBackpressure(this, desiredSize <= 0);
+ var transformStream = this;
+ var startResult = InvokeOrNoop(transformer, 'start', [transformStream._transformStreamController]);
+ startPromise_resolve(startResult);
+ startPromise["catch"](function (e) {
+ if (transformStream._errored === false) {
+ transformStream._errored = true;
+ transformStream._storedError = e;
+ }
+ });
+ }
+
+ _createClass(TransformStream, [{
+ key: 'readable',
+ get: function get() {
+ if (IsTransformStream(this) === false) {
+ throw streamBrandCheckException('readable');
+ }
+
+ return this._readable;
+ }
+ }, {
+ key: 'writable',
+ get: function get() {
+ if (IsTransformStream(this) === false) {
+ throw streamBrandCheckException('writable');
+ }
+
+ return this._writable;
+ }
+ }]);
+
+ return TransformStream;
+ }();
+
+ module.exports = {
+ TransformStream: TransformStream
+ };
+
+ function defaultControllerBrandCheckException(name) {
+ return new TypeError('TransformStreamDefaultController.prototype.' + name + ' can only be used on a TransformStreamDefaultController');
+ }
+
+ function streamBrandCheckException(name) {
+ return new TypeError('TransformStream.prototype.' + name + ' can only be used on a TransformStream');
+ }
+}, function (module, exports, __w_pdfjs_require__) {
+ module.exports = __w_pdfjs_require__(5);
+}]));
+
+/***/ }),
+/* 145 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+{
+ var isURLSupported = false;
+
+ try {
+ if (typeof URL === 'function' && _typeof(URL.prototype) === 'object' && 'origin' in URL.prototype) {
+ var u = new URL('b', 'http://a');
+ u.pathname = 'c%20d';
+ isURLSupported = u.href === 'http://a/c%20d';
+ }
+ } catch (ex) {}
+
+ if (isURLSupported) {
+ exports.URL = URL;
+ } else {
+ var PolyfillURL = __w_pdfjs_require__(146).URL;
+
+ var OriginalURL = __w_pdfjs_require__(3).URL;
+
+ if (OriginalURL) {
+ PolyfillURL.createObjectURL = function (blob) {
+ return OriginalURL.createObjectURL.apply(OriginalURL, arguments);
+ };
+
+ PolyfillURL.revokeObjectURL = function (url) {
+ OriginalURL.revokeObjectURL(url);
+ };
+ }
+
+ exports.URL = PolyfillURL;
+ }
+}
+
+/***/ }),
+/* 146 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+(function URLConstructorClosure() {
+ 'use strict';
+
+ var relative = Object.create(null);
+ relative['ftp'] = 21;
+ relative['file'] = 0;
+ relative['gopher'] = 70;
+ relative['http'] = 80;
+ relative['https'] = 443;
+ relative['ws'] = 80;
+ relative['wss'] = 443;
+ var relativePathDotMapping = Object.create(null);
+ relativePathDotMapping['%2e'] = '.';
+ relativePathDotMapping['.%2e'] = '..';
+ relativePathDotMapping['%2e.'] = '..';
+ relativePathDotMapping['%2e%2e'] = '..';
+
+ function isRelativeScheme(scheme) {
+ return relative[scheme] !== undefined;
+ }
+
+ function invalid() {
+ clear.call(this);
+ this._isInvalid = true;
+ }
+
+ function IDNAToASCII(h) {
+ if (h === '') {
+ invalid.call(this);
+ }
+
+ return h.toLowerCase();
+ }
+
+ function percentEscape(c) {
+ var unicode = c.charCodeAt(0);
+
+ if (unicode > 0x20 && unicode < 0x7F && [0x22, 0x23, 0x3C, 0x3E, 0x3F, 0x60].indexOf(unicode) === -1) {
+ return c;
+ }
+
+ return encodeURIComponent(c);
+ }
+
+ function percentEscapeQuery(c) {
+ var unicode = c.charCodeAt(0);
+
+ if (unicode > 0x20 && unicode < 0x7F && [0x22, 0x23, 0x3C, 0x3E, 0x60].indexOf(unicode) === -1) {
+ return c;
+ }
+
+ return encodeURIComponent(c);
+ }
+
+ var EOF,
+ ALPHA = /[a-zA-Z]/,
+ ALPHANUMERIC = /[a-zA-Z0-9\+\-\.]/;
+
+ function parse(input, stateOverride, base) {
+ function err(message) {
+ errors.push(message);
+ }
+
+ var state = stateOverride || 'scheme start',
+ cursor = 0,
+ buffer = '',
+ seenAt = false,
+ seenBracket = false,
+ errors = [];
+
+ loop: while ((input[cursor - 1] !== EOF || cursor === 0) && !this._isInvalid) {
+ var c = input[cursor];
+
+ switch (state) {
+ case 'scheme start':
+ if (c && ALPHA.test(c)) {
+ buffer += c.toLowerCase();
+ state = 'scheme';
+ } else if (!stateOverride) {
+ buffer = '';
+ state = 'no scheme';
+ continue;
+ } else {
+ err('Invalid scheme.');
+ break loop;
+ }
+
+ break;
+
+ case 'scheme':
+ if (c && ALPHANUMERIC.test(c)) {
+ buffer += c.toLowerCase();
+ } else if (c === ':') {
+ this._scheme = buffer;
+ buffer = '';
+
+ if (stateOverride) {
+ break loop;
+ }
+
+ if (isRelativeScheme(this._scheme)) {
+ this._isRelative = true;
+ }
+
+ if (this._scheme === 'file') {
+ state = 'relative';
+ } else if (this._isRelative && base && base._scheme === this._scheme) {
+ state = 'relative or authority';
+ } else if (this._isRelative) {
+ state = 'authority first slash';
+ } else {
+ state = 'scheme data';
+ }
+ } else if (!stateOverride) {
+ buffer = '';
+ cursor = 0;
+ state = 'no scheme';
+ continue;
+ } else if (c === EOF) {
+ break loop;
+ } else {
+ err('Code point not allowed in scheme: ' + c);
+ break loop;
+ }
+
+ break;
+
+ case 'scheme data':
+ if (c === '?') {
+ this._query = '?';
+ state = 'query';
+ } else if (c === '#') {
+ this._fragment = '#';
+ state = 'fragment';
+ } else {
+ if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') {
+ this._schemeData += percentEscape(c);
+ }
+ }
+
+ break;
+
+ case 'no scheme':
+ if (!base || !isRelativeScheme(base._scheme)) {
+ err('Missing scheme.');
+ invalid.call(this);
+ } else {
+ state = 'relative';
+ continue;
+ }
+
+ break;
+
+ case 'relative or authority':
+ if (c === '/' && input[cursor + 1] === '/') {
+ state = 'authority ignore slashes';
+ } else {
+ err('Expected /, got: ' + c);
+ state = 'relative';
+ continue;
+ }
+
+ break;
+
+ case 'relative':
+ this._isRelative = true;
+
+ if (this._scheme !== 'file') {
+ this._scheme = base._scheme;
+ }
+
+ if (c === EOF) {
+ this._host = base._host;
+ this._port = base._port;
+ this._path = base._path.slice();
+ this._query = base._query;
+ this._username = base._username;
+ this._password = base._password;
+ break loop;
+ } else if (c === '/' || c === '\\') {
+ if (c === '\\') {
+ err('\\ is an invalid code point.');
+ }
+
+ state = 'relative slash';
+ } else if (c === '?') {
+ this._host = base._host;
+ this._port = base._port;
+ this._path = base._path.slice();
+ this._query = '?';
+ this._username = base._username;
+ this._password = base._password;
+ state = 'query';
+ } else if (c === '#') {
+ this._host = base._host;
+ this._port = base._port;
+ this._path = base._path.slice();
+ this._query = base._query;
+ this._fragment = '#';
+ this._username = base._username;
+ this._password = base._password;
+ state = 'fragment';
+ } else {
+ var nextC = input[cursor + 1];
+ var nextNextC = input[cursor + 2];
+
+ if (this._scheme !== 'file' || !ALPHA.test(c) || nextC !== ':' && nextC !== '|' || nextNextC !== EOF && nextNextC !== '/' && nextNextC !== '\\' && nextNextC !== '?' && nextNextC !== '#') {
+ this._host = base._host;
+ this._port = base._port;
+ this._username = base._username;
+ this._password = base._password;
+ this._path = base._path.slice();
+
+ this._path.pop();
+ }
+
+ state = 'relative path';
+ continue;
+ }
+
+ break;
+
+ case 'relative slash':
+ if (c === '/' || c === '\\') {
+ if (c === '\\') {
+ err('\\ is an invalid code point.');
+ }
+
+ if (this._scheme === 'file') {
+ state = 'file host';
+ } else {
+ state = 'authority ignore slashes';
+ }
+ } else {
+ if (this._scheme !== 'file') {
+ this._host = base._host;
+ this._port = base._port;
+ this._username = base._username;
+ this._password = base._password;
+ }
+
+ state = 'relative path';
+ continue;
+ }
+
+ break;
+
+ case 'authority first slash':
+ if (c === '/') {
+ state = 'authority second slash';
+ } else {
+ err('Expected \'/\', got: ' + c);
+ state = 'authority ignore slashes';
+ continue;
+ }
+
+ break;
+
+ case 'authority second slash':
+ state = 'authority ignore slashes';
+
+ if (c !== '/') {
+ err('Expected \'/\', got: ' + c);
+ continue;
+ }
+
+ break;
+
+ case 'authority ignore slashes':
+ if (c !== '/' && c !== '\\') {
+ state = 'authority';
+ continue;
+ } else {
+ err('Expected authority, got: ' + c);
+ }
+
+ break;
+
+ case 'authority':
+ if (c === '@') {
+ if (seenAt) {
+ err('@ already seen.');
+ buffer += '%40';
+ }
+
+ seenAt = true;
+
+ for (var i = 0; i < buffer.length; i++) {
+ var cp = buffer[i];
+
+ if (cp === '\t' || cp === '\n' || cp === '\r') {
+ err('Invalid whitespace in authority.');
+ continue;
+ }
+
+ if (cp === ':' && this._password === null) {
+ this._password = '';
+ continue;
+ }
+
+ var tempC = percentEscape(cp);
+
+ if (this._password !== null) {
+ this._password += tempC;
+ } else {
+ this._username += tempC;
+ }
+ }
+
+ buffer = '';
+ } else if (c === EOF || c === '/' || c === '\\' || c === '?' || c === '#') {
+ cursor -= buffer.length;
+ buffer = '';
+ state = 'host';
+ continue;
+ } else {
+ buffer += c;
+ }
+
+ break;
+
+ case 'file host':
+ if (c === EOF || c === '/' || c === '\\' || c === '?' || c === '#') {
+ if (buffer.length === 2 && ALPHA.test(buffer[0]) && (buffer[1] === ':' || buffer[1] === '|')) {
+ state = 'relative path';
+ } else if (buffer.length === 0) {
+ state = 'relative path start';
+ } else {
+ this._host = IDNAToASCII.call(this, buffer);
+ buffer = '';
+ state = 'relative path start';
+ }
+
+ continue;
+ } else if (c === '\t' || c === '\n' || c === '\r') {
+ err('Invalid whitespace in file host.');
+ } else {
+ buffer += c;
+ }
+
+ break;
+
+ case 'host':
+ case 'hostname':
+ if (c === ':' && !seenBracket) {
+ this._host = IDNAToASCII.call(this, buffer);
+ buffer = '';
+ state = 'port';
+
+ if (stateOverride === 'hostname') {
+ break loop;
+ }
+ } else if (c === EOF || c === '/' || c === '\\' || c === '?' || c === '#') {
+ this._host = IDNAToASCII.call(this, buffer);
+ buffer = '';
+ state = 'relative path start';
+
+ if (stateOverride) {
+ break loop;
+ }
+
+ continue;
+ } else if (c !== '\t' && c !== '\n' && c !== '\r') {
+ if (c === '[') {
+ seenBracket = true;
+ } else if (c === ']') {
+ seenBracket = false;
+ }
+
+ buffer += c;
+ } else {
+ err('Invalid code point in host/hostname: ' + c);
+ }
+
+ break;
+
+ case 'port':
+ if (/[0-9]/.test(c)) {
+ buffer += c;
+ } else if (c === EOF || c === '/' || c === '\\' || c === '?' || c === '#' || stateOverride) {
+ if (buffer !== '') {
+ var temp = parseInt(buffer, 10);
+
+ if (temp !== relative[this._scheme]) {
+ this._port = temp + '';
+ }
+
+ buffer = '';
+ }
+
+ if (stateOverride) {
+ break loop;
+ }
+
+ state = 'relative path start';
+ continue;
+ } else if (c === '\t' || c === '\n' || c === '\r') {
+ err('Invalid code point in port: ' + c);
+ } else {
+ invalid.call(this);
+ }
+
+ break;
+
+ case 'relative path start':
+ if (c === '\\') {
+ err('\'\\\' not allowed in path.');
+ }
+
+ state = 'relative path';
+
+ if (c !== '/' && c !== '\\') {
+ continue;
+ }
+
+ break;
+
+ case 'relative path':
+ if (c === EOF || c === '/' || c === '\\' || !stateOverride && (c === '?' || c === '#')) {
+ if (c === '\\') {
+ err('\\ not allowed in relative path.');
+ }
+
+ var tmp;
+
+ if (tmp = relativePathDotMapping[buffer.toLowerCase()]) {
+ buffer = tmp;
+ }
+
+ if (buffer === '..') {
+ this._path.pop();
+
+ if (c !== '/' && c !== '\\') {
+ this._path.push('');
+ }
+ } else if (buffer === '.' && c !== '/' && c !== '\\') {
+ this._path.push('');
+ } else if (buffer !== '.') {
+ if (this._scheme === 'file' && this._path.length === 0 && buffer.length === 2 && ALPHA.test(buffer[0]) && buffer[1] === '|') {
+ buffer = buffer[0] + ':';
+ }
+
+ this._path.push(buffer);
+ }
+
+ buffer = '';
+
+ if (c === '?') {
+ this._query = '?';
+ state = 'query';
+ } else if (c === '#') {
+ this._fragment = '#';
+ state = 'fragment';
+ }
+ } else if (c !== '\t' && c !== '\n' && c !== '\r') {
+ buffer += percentEscape(c);
+ }
+
+ break;
+
+ case 'query':
+ if (!stateOverride && c === '#') {
+ this._fragment = '#';
+ state = 'fragment';
+ } else if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') {
+ this._query += percentEscapeQuery(c);
+ }
+
+ break;
+
+ case 'fragment':
+ if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') {
+ this._fragment += c;
+ }
+
+ break;
+ }
+
+ cursor++;
+ }
+ }
+
+ function clear() {
+ this._scheme = '';
+ this._schemeData = '';
+ this._username = '';
+ this._password = null;
+ this._host = '';
+ this._port = '';
+ this._path = [];
+ this._query = '';
+ this._fragment = '';
+ this._isInvalid = false;
+ this._isRelative = false;
+ }
+
+ function JURL(url, base) {
+ if (base !== undefined && !(base instanceof JURL)) {
+ base = new JURL(String(base));
+ }
+
+ this._url = url;
+ clear.call(this);
+ var input = url.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g, '');
+ parse.call(this, input, null, base);
+ }
+
+ JURL.prototype = {
+ toString: function toString() {
+ return this.href;
+ },
+
+ get href() {
+ if (this._isInvalid) {
+ return this._url;
+ }
+
+ var authority = '';
+
+ if (this._username !== '' || this._password !== null) {
+ authority = this._username + (this._password !== null ? ':' + this._password : '') + '@';
+ }
+
+ return this.protocol + (this._isRelative ? '//' + authority + this.host : '') + this.pathname + this._query + this._fragment;
+ },
+
+ set href(value) {
+ clear.call(this);
+ parse.call(this, value);
+ },
+
+ get protocol() {
+ return this._scheme + ':';
+ },
+
+ set protocol(value) {
+ if (this._isInvalid) {
+ return;
+ }
+
+ parse.call(this, value + ':', 'scheme start');
+ },
+
+ get host() {
+ return this._isInvalid ? '' : this._port ? this._host + ':' + this._port : this._host;
+ },
+
+ set host(value) {
+ if (this._isInvalid || !this._isRelative) {
+ return;
+ }
+
+ parse.call(this, value, 'host');
+ },
+
+ get hostname() {
+ return this._host;
+ },
+
+ set hostname(value) {
+ if (this._isInvalid || !this._isRelative) {
+ return;
+ }
+
+ parse.call(this, value, 'hostname');
+ },
+
+ get port() {
+ return this._port;
+ },
+
+ set port(value) {
+ if (this._isInvalid || !this._isRelative) {
+ return;
+ }
+
+ parse.call(this, value, 'port');
+ },
+
+ get pathname() {
+ return this._isInvalid ? '' : this._isRelative ? '/' + this._path.join('/') : this._schemeData;
+ },
+
+ set pathname(value) {
+ if (this._isInvalid || !this._isRelative) {
+ return;
+ }
+
+ this._path = [];
+ parse.call(this, value, 'relative path start');
+ },
+
+ get search() {
+ return this._isInvalid || !this._query || this._query === '?' ? '' : this._query;
+ },
+
+ set search(value) {
+ if (this._isInvalid || !this._isRelative) {
+ return;
+ }
+
+ this._query = '?';
+
+ if (value[0] === '?') {
+ value = value.slice(1);
+ }
+
+ parse.call(this, value, 'query');
+ },
+
+ get hash() {
+ return this._isInvalid || !this._fragment || this._fragment === '#' ? '' : this._fragment;
+ },
+
+ set hash(value) {
+ if (this._isInvalid) {
+ return;
+ }
+
+ this._fragment = '#';
+
+ if (value[0] === '#') {
+ value = value.slice(1);
+ }
+
+ parse.call(this, value, 'fragment');
+ },
+
+ get origin() {
+ var host;
+
+ if (this._isInvalid || !this._scheme) {
+ return '';
+ }
+
+ switch (this._scheme) {
+ case 'data':
+ case 'file':
+ case 'javascript':
+ case 'mailto':
+ return 'null';
+
+ case 'blob':
+ try {
+ return new JURL(this._schemeData).origin || 'null';
+ } catch (_) {}
+
+ return 'null';
+ }
+
+ host = this.host;
+
+ if (!host) {
+ return '';
+ }
+
+ return this._scheme + '://' + host;
+ }
+
+ };
+ exports.URL = JURL;
+})();
+
+/***/ }),
+/* 147 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.getDocument = getDocument;
+exports.setPDFNetworkStreamFactory = setPDFNetworkStreamFactory;
+exports.build = exports.version = exports.PDFPageProxy = exports.PDFDocumentProxy = exports.PDFWorker = exports.PDFDataRangeTransport = exports.LoopbackPort = void 0;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(148));
+
+var _util = __w_pdfjs_require__(1);
+
+var _display_utils = __w_pdfjs_require__(151);
+
+var _font_loader = __w_pdfjs_require__(152);
+
+var _api_compatibility = __w_pdfjs_require__(153);
+
+var _canvas = __w_pdfjs_require__(154);
+
+var _global_scope = _interopRequireDefault(__w_pdfjs_require__(3));
+
+var _worker_options = __w_pdfjs_require__(156);
+
+var _message_handler = __w_pdfjs_require__(157);
+
+var _metadata = __w_pdfjs_require__(158);
+
+var _transport_stream = __w_pdfjs_require__(160);
+
+var _webgl = __w_pdfjs_require__(161);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
+
+function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
+
+function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
+
+function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
+
+function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
+
+function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
+
+function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
+
+function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var DEFAULT_RANGE_CHUNK_SIZE = 65536;
+var isWorkerDisabled = false;
+var fallbackWorkerSrc;
+var fakeWorkerFilesLoader = null;
+{
+ var useRequireEnsure = false;
+
+ if (typeof window === 'undefined') {
+ isWorkerDisabled = true;
+
+ if (typeof require.ensure === 'undefined') {
+ require.ensure = require('node-ensure');
+ }
+
+ useRequireEnsure = true;
+ } else if (typeof require !== 'undefined' && typeof require.ensure === 'function') {
+ useRequireEnsure = true;
+ }
+
+ if (typeof requirejs !== 'undefined' && requirejs.toUrl) {
+ fallbackWorkerSrc = requirejs.toUrl('pdfjs-dist/build/pdf.worker.js');
+ }
+
+ var dynamicLoaderSupported = typeof requirejs !== 'undefined' && requirejs.load;
+ fakeWorkerFilesLoader = useRequireEnsure ? function () {
+ return new Promise(function (resolve, reject) {
+ require.ensure([], function () {
+ try {
+ var worker;
+ worker = require('./pdf.worker.js');
+ resolve(worker.WorkerMessageHandler);
+ } catch (ex) {
+ reject(ex);
+ }
+ }, reject, 'pdfjsWorker');
+ });
+ } : dynamicLoaderSupported ? function () {
+ return new Promise(function (resolve, reject) {
+ requirejs(['pdfjs-dist/build/pdf.worker'], function (worker) {
+ try {
+ resolve(worker.WorkerMessageHandler);
+ } catch (ex) {
+ reject(ex);
+ }
+ }, reject);
+ });
+ } : null;
+
+ if (!fallbackWorkerSrc && (typeof document === "undefined" ? "undefined" : _typeof(document)) === 'object' && 'currentScript' in document) {
+ var pdfjsFilePath = document.currentScript && document.currentScript.src;
+
+ if (pdfjsFilePath) {
+ fallbackWorkerSrc = pdfjsFilePath.replace(/(\.(?:min\.)?js)(\?.*)?$/i, '.worker$1$2');
+ }
+ }
+}
+var createPDFNetworkStream;
+
+function setPDFNetworkStreamFactory(pdfNetworkStreamFactory) {
+ createPDFNetworkStream = pdfNetworkStreamFactory;
+}
+
+function getDocument(src) {
+ var task = new PDFDocumentLoadingTask();
+ var source;
+
+ if (typeof src === 'string') {
+ source = {
+ url: src
+ };
+ } else if ((0, _util.isArrayBuffer)(src)) {
+ source = {
+ data: src
+ };
+ } else if (src instanceof PDFDataRangeTransport) {
+ source = {
+ range: src
+ };
+ } else {
+ if (_typeof(src) !== 'object') {
+ throw new Error('Invalid parameter in getDocument, ' + 'need either Uint8Array, string or a parameter object');
+ }
+
+ if (!src.url && !src.data && !src.range) {
+ throw new Error('Invalid parameter object: need either .data, .range or .url');
+ }
+
+ source = src;
+ }
+
+ var params = Object.create(null);
+ var rangeTransport = null,
+ worker = null;
+
+ for (var key in source) {
+ if (key === 'url' && typeof window !== 'undefined') {
+ params[key] = new _util.URL(source[key], window.location).href;
+ continue;
+ } else if (key === 'range') {
+ rangeTransport = source[key];
+ continue;
+ } else if (key === 'worker') {
+ worker = source[key];
+ continue;
+ } else if (key === 'data' && !(source[key] instanceof Uint8Array)) {
+ var pdfBytes = source[key];
+
+ if (typeof pdfBytes === 'string') {
+ params[key] = (0, _util.stringToBytes)(pdfBytes);
+ } else if (_typeof(pdfBytes) === 'object' && pdfBytes !== null && !isNaN(pdfBytes.length)) {
+ params[key] = new Uint8Array(pdfBytes);
+ } else if ((0, _util.isArrayBuffer)(pdfBytes)) {
+ params[key] = new Uint8Array(pdfBytes);
+ } else {
+ throw new Error('Invalid PDF binary data: either typed array, ' + 'string or array-like object is expected in the ' + 'data property.');
+ }
+
+ continue;
+ }
+
+ params[key] = source[key];
+ }
+
+ params.rangeChunkSize = params.rangeChunkSize || DEFAULT_RANGE_CHUNK_SIZE;
+ params.CMapReaderFactory = params.CMapReaderFactory || _display_utils.DOMCMapReaderFactory;
+ params.ignoreErrors = params.stopAtErrors !== true;
+ params.pdfBug = params.pdfBug === true;
+ var NativeImageDecoderValues = Object.values(_util.NativeImageDecoding);
+
+ if (params.nativeImageDecoderSupport === undefined || !NativeImageDecoderValues.includes(params.nativeImageDecoderSupport)) {
+ params.nativeImageDecoderSupport = _api_compatibility.apiCompatibilityParams.nativeImageDecoderSupport || _util.NativeImageDecoding.DECODE;
+ }
+
+ if (!Number.isInteger(params.maxImageSize)) {
+ params.maxImageSize = -1;
+ }
+
+ if (typeof params.isEvalSupported !== 'boolean') {
+ params.isEvalSupported = true;
+ }
+
+ if (typeof params.disableFontFace !== 'boolean') {
+ params.disableFontFace = _api_compatibility.apiCompatibilityParams.disableFontFace || false;
+ }
+
+ if (typeof params.disableRange !== 'boolean') {
+ params.disableRange = false;
+ }
+
+ if (typeof params.disableStream !== 'boolean') {
+ params.disableStream = false;
+ }
+
+ if (typeof params.disableAutoFetch !== 'boolean') {
+ params.disableAutoFetch = false;
+ }
+
+ if (typeof params.disableCreateObjectURL !== 'boolean') {
+ params.disableCreateObjectURL = _api_compatibility.apiCompatibilityParams.disableCreateObjectURL || false;
+ }
+
+ (0, _util.setVerbosityLevel)(params.verbosity);
+
+ if (!worker) {
+ var workerParams = {
+ postMessageTransfers: params.postMessageTransfers,
+ verbosity: params.verbosity,
+ port: _worker_options.GlobalWorkerOptions.workerPort
+ };
+ worker = workerParams.port ? PDFWorker.fromPort(workerParams) : new PDFWorker(workerParams);
+ task._worker = worker;
+ }
+
+ var docId = task.docId;
+ worker.promise.then(function () {
+ if (task.destroyed) {
+ throw new Error('Loading aborted');
+ }
+
+ return _fetchDocument(worker, params, rangeTransport, docId).then(function (workerId) {
+ if (task.destroyed) {
+ throw new Error('Loading aborted');
+ }
+
+ var networkStream;
+
+ if (rangeTransport) {
+ networkStream = new _transport_stream.PDFDataTransportStream({
+ length: params.length,
+ initialData: params.initialData,
+ progressiveDone: params.progressiveDone,
+ disableRange: params.disableRange,
+ disableStream: params.disableStream
+ }, rangeTransport);
+ } else if (!params.data) {
+ networkStream = createPDFNetworkStream({
+ url: params.url,
+ length: params.length,
+ httpHeaders: params.httpHeaders,
+ withCredentials: params.withCredentials,
+ rangeChunkSize: params.rangeChunkSize,
+ disableRange: params.disableRange,
+ disableStream: params.disableStream
+ });
+ }
+
+ var messageHandler = new _message_handler.MessageHandler(docId, workerId, worker.port);
+ messageHandler.postMessageTransfers = worker.postMessageTransfers;
+ var transport = new WorkerTransport(messageHandler, task, networkStream, params);
+ task._transport = transport;
+ messageHandler.send('Ready', null);
+ });
+ })["catch"](task._capability.reject);
+ return task;
+}
+
+function _fetchDocument(worker, source, pdfDataRangeTransport, docId) {
+ if (worker.destroyed) {
+ return Promise.reject(new Error('Worker was destroyed'));
+ }
+
+ if (pdfDataRangeTransport) {
+ source.length = pdfDataRangeTransport.length;
+ source.initialData = pdfDataRangeTransport.initialData;
+ source.progressiveDone = pdfDataRangeTransport.progressiveDone;
+ }
+
+ return worker.messageHandler.sendWithPromise('GetDocRequest', {
+ docId: docId,
+ apiVersion: '2.2.228',
+ source: {
+ data: source.data,
+ url: source.url,
+ password: source.password,
+ disableAutoFetch: source.disableAutoFetch,
+ rangeChunkSize: source.rangeChunkSize,
+ length: source.length
+ },
+ maxImageSize: source.maxImageSize,
+ disableFontFace: source.disableFontFace,
+ disableCreateObjectURL: source.disableCreateObjectURL,
+ postMessageTransfers: worker.postMessageTransfers,
+ docBaseUrl: source.docBaseUrl,
+ nativeImageDecoderSupport: source.nativeImageDecoderSupport,
+ ignoreErrors: source.ignoreErrors,
+ isEvalSupported: source.isEvalSupported
+ }).then(function (workerId) {
+ if (worker.destroyed) {
+ throw new Error('Worker was destroyed');
+ }
+
+ return workerId;
+ });
+}
+
+var PDFDocumentLoadingTask = function PDFDocumentLoadingTaskClosure() {
+ var nextDocumentId = 0;
+
+ var PDFDocumentLoadingTask =
+ /*#__PURE__*/
+ function () {
+ function PDFDocumentLoadingTask() {
+ _classCallCheck(this, PDFDocumentLoadingTask);
+
+ this._capability = (0, _util.createPromiseCapability)();
+ this._transport = null;
+ this._worker = null;
+ this.docId = 'd' + nextDocumentId++;
+ this.destroyed = false;
+ this.onPassword = null;
+ this.onProgress = null;
+ this.onUnsupportedFeature = null;
+ }
+
+ _createClass(PDFDocumentLoadingTask, [{
+ key: "destroy",
+ value: function destroy() {
+ var _this = this;
+
+ this.destroyed = true;
+ var transportDestroyed = !this._transport ? Promise.resolve() : this._transport.destroy();
+ return transportDestroyed.then(function () {
+ _this._transport = null;
+
+ if (_this._worker) {
+ _this._worker.destroy();
+
+ _this._worker = null;
+ }
+ });
+ }
+ }, {
+ key: "then",
+ value: function then(onFulfilled, onRejected) {
+ (0, _display_utils.deprecated)('PDFDocumentLoadingTask.then method, ' + 'use the `promise` getter instead.');
+ return this.promise.then.apply(this.promise, arguments);
+ }
+ }, {
+ key: "promise",
+ get: function get() {
+ return this._capability.promise;
+ }
+ }]);
+
+ return PDFDocumentLoadingTask;
+ }();
+
+ return PDFDocumentLoadingTask;
+}();
+
+var PDFDataRangeTransport =
+/*#__PURE__*/
+function () {
+ function PDFDataRangeTransport(length, initialData) {
+ var progressiveDone = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+
+ _classCallCheck(this, PDFDataRangeTransport);
+
+ this.length = length;
+ this.initialData = initialData;
+ this.progressiveDone = progressiveDone;
+ this._rangeListeners = [];
+ this._progressListeners = [];
+ this._progressiveReadListeners = [];
+ this._progressiveDoneListeners = [];
+ this._readyCapability = (0, _util.createPromiseCapability)();
+ }
+
+ _createClass(PDFDataRangeTransport, [{
+ key: "addRangeListener",
+ value: function addRangeListener(listener) {
+ this._rangeListeners.push(listener);
+ }
+ }, {
+ key: "addProgressListener",
+ value: function addProgressListener(listener) {
+ this._progressListeners.push(listener);
+ }
+ }, {
+ key: "addProgressiveReadListener",
+ value: function addProgressiveReadListener(listener) {
+ this._progressiveReadListeners.push(listener);
+ }
+ }, {
+ key: "addProgressiveDoneListener",
+ value: function addProgressiveDoneListener(listener) {
+ this._progressiveDoneListeners.push(listener);
+ }
+ }, {
+ key: "onDataRange",
+ value: function onDataRange(begin, chunk) {
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = this._rangeListeners[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var listener = _step.value;
+ listener(begin, chunk);
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator["return"] != null) {
+ _iterator["return"]();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+ }
+ }, {
+ key: "onDataProgress",
+ value: function onDataProgress(loaded, total) {
+ var _this2 = this;
+
+ this._readyCapability.promise.then(function () {
+ var _iteratorNormalCompletion2 = true;
+ var _didIteratorError2 = false;
+ var _iteratorError2 = undefined;
+
+ try {
+ for (var _iterator2 = _this2._progressListeners[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
+ var listener = _step2.value;
+ listener(loaded, total);
+ }
+ } catch (err) {
+ _didIteratorError2 = true;
+ _iteratorError2 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) {
+ _iterator2["return"]();
+ }
+ } finally {
+ if (_didIteratorError2) {
+ throw _iteratorError2;
+ }
+ }
+ }
+ });
+ }
+ }, {
+ key: "onDataProgressiveRead",
+ value: function onDataProgressiveRead(chunk) {
+ var _this3 = this;
+
+ this._readyCapability.promise.then(function () {
+ var _iteratorNormalCompletion3 = true;
+ var _didIteratorError3 = false;
+ var _iteratorError3 = undefined;
+
+ try {
+ for (var _iterator3 = _this3._progressiveReadListeners[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
+ var listener = _step3.value;
+ listener(chunk);
+ }
+ } catch (err) {
+ _didIteratorError3 = true;
+ _iteratorError3 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion3 && _iterator3["return"] != null) {
+ _iterator3["return"]();
+ }
+ } finally {
+ if (_didIteratorError3) {
+ throw _iteratorError3;
+ }
+ }
+ }
+ });
+ }
+ }, {
+ key: "onDataProgressiveDone",
+ value: function onDataProgressiveDone() {
+ var _this4 = this;
+
+ this._readyCapability.promise.then(function () {
+ var _iteratorNormalCompletion4 = true;
+ var _didIteratorError4 = false;
+ var _iteratorError4 = undefined;
+
+ try {
+ for (var _iterator4 = _this4._progressiveDoneListeners[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
+ var listener = _step4.value;
+ listener();
+ }
+ } catch (err) {
+ _didIteratorError4 = true;
+ _iteratorError4 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion4 && _iterator4["return"] != null) {
+ _iterator4["return"]();
+ }
+ } finally {
+ if (_didIteratorError4) {
+ throw _iteratorError4;
+ }
+ }
+ }
+ });
+ }
+ }, {
+ key: "transportReady",
+ value: function transportReady() {
+ this._readyCapability.resolve();
+ }
+ }, {
+ key: "requestDataRange",
+ value: function requestDataRange(begin, end) {
+ (0, _util.unreachable)('Abstract method PDFDataRangeTransport.requestDataRange');
+ }
+ }, {
+ key: "abort",
+ value: function abort() {}
+ }]);
+
+ return PDFDataRangeTransport;
+}();
+
+exports.PDFDataRangeTransport = PDFDataRangeTransport;
+
+var PDFDocumentProxy =
+/*#__PURE__*/
+function () {
+ function PDFDocumentProxy(pdfInfo, transport) {
+ _classCallCheck(this, PDFDocumentProxy);
+
+ this._pdfInfo = pdfInfo;
+ this._transport = transport;
+ }
+
+ _createClass(PDFDocumentProxy, [{
+ key: "getPage",
+ value: function getPage(pageNumber) {
+ return this._transport.getPage(pageNumber);
+ }
+ }, {
+ key: "getPageIndex",
+ value: function getPageIndex(ref) {
+ return this._transport.getPageIndex(ref);
+ }
+ }, {
+ key: "getDestinations",
+ value: function getDestinations() {
+ return this._transport.getDestinations();
+ }
+ }, {
+ key: "getDestination",
+ value: function getDestination(id) {
+ return this._transport.getDestination(id);
+ }
+ }, {
+ key: "getPageLabels",
+ value: function getPageLabels() {
+ return this._transport.getPageLabels();
+ }
+ }, {
+ key: "getPageLayout",
+ value: function getPageLayout() {
+ return this._transport.getPageLayout();
+ }
+ }, {
+ key: "getPageMode",
+ value: function getPageMode() {
+ return this._transport.getPageMode();
+ }
+ }, {
+ key: "getViewerPreferences",
+ value: function getViewerPreferences() {
+ return this._transport.getViewerPreferences();
+ }
+ }, {
+ key: "getOpenActionDestination",
+ value: function getOpenActionDestination() {
+ return this._transport.getOpenActionDestination();
+ }
+ }, {
+ key: "getAttachments",
+ value: function getAttachments() {
+ return this._transport.getAttachments();
+ }
+ }, {
+ key: "getJavaScript",
+ value: function getJavaScript() {
+ return this._transport.getJavaScript();
+ }
+ }, {
+ key: "getOutline",
+ value: function getOutline() {
+ return this._transport.getOutline();
+ }
+ }, {
+ key: "getPermissions",
+ value: function getPermissions() {
+ return this._transport.getPermissions();
+ }
+ }, {
+ key: "getMetadata",
+ value: function getMetadata() {
+ return this._transport.getMetadata();
+ }
+ }, {
+ key: "getData",
+ value: function getData() {
+ return this._transport.getData();
+ }
+ }, {
+ key: "getDownloadInfo",
+ value: function getDownloadInfo() {
+ return this._transport.downloadInfoCapability.promise;
+ }
+ }, {
+ key: "getStats",
+ value: function getStats() {
+ return this._transport.getStats();
+ }
+ }, {
+ key: "cleanup",
+ value: function cleanup() {
+ this._transport.startCleanup();
+ }
+ }, {
+ key: "destroy",
+ value: function destroy() {
+ return this.loadingTask.destroy();
+ }
+ }, {
+ key: "numPages",
+ get: function get() {
+ return this._pdfInfo.numPages;
+ }
+ }, {
+ key: "fingerprint",
+ get: function get() {
+ return this._pdfInfo.fingerprint;
+ }
+ }, {
+ key: "loadingParams",
+ get: function get() {
+ return this._transport.loadingParams;
+ }
+ }, {
+ key: "loadingTask",
+ get: function get() {
+ return this._transport.loadingTask;
+ }
+ }]);
+
+ return PDFDocumentProxy;
+}();
+
+exports.PDFDocumentProxy = PDFDocumentProxy;
+
+var PDFPageProxy =
+/*#__PURE__*/
+function () {
+ function PDFPageProxy(pageIndex, pageInfo, transport) {
+ var pdfBug = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
+
+ _classCallCheck(this, PDFPageProxy);
+
+ this.pageIndex = pageIndex;
+ this._pageInfo = pageInfo;
+ this._transport = transport;
+ this._stats = pdfBug ? new _display_utils.StatTimer() : _display_utils.DummyStatTimer;
+ this._pdfBug = pdfBug;
+ this.commonObjs = transport.commonObjs;
+ this.objs = new PDFObjects();
+ this.cleanupAfterRender = false;
+ this.pendingCleanup = false;
+ this.intentStates = Object.create(null);
+ this.destroyed = false;
+ }
+
+ _createClass(PDFPageProxy, [{
+ key: "getViewport",
+ value: function getViewport() {
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
+ scale = _ref.scale,
+ _ref$rotation = _ref.rotation,
+ rotation = _ref$rotation === void 0 ? this.rotate : _ref$rotation,
+ _ref$dontFlip = _ref.dontFlip,
+ dontFlip = _ref$dontFlip === void 0 ? false : _ref$dontFlip;
+
+ if (arguments.length > 1 || typeof arguments[0] === 'number') {
+ (0, _display_utils.deprecated)('getViewport is called with obsolete arguments.');
+ scale = arguments[0];
+ rotation = typeof arguments[1] === 'number' ? arguments[1] : this.rotate;
+ dontFlip = typeof arguments[2] === 'boolean' ? arguments[2] : false;
+ }
+
+ return new _display_utils.PageViewport({
+ viewBox: this.view,
+ scale: scale,
+ rotation: rotation,
+ dontFlip: dontFlip
+ });
+ }
+ }, {
+ key: "getAnnotations",
+ value: function getAnnotations() {
+ var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
+ _ref2$intent = _ref2.intent,
+ intent = _ref2$intent === void 0 ? null : _ref2$intent;
+
+ if (!this.annotationsPromise || this.annotationsIntent !== intent) {
+ this.annotationsPromise = this._transport.getAnnotations(this.pageIndex, intent);
+ this.annotationsIntent = intent;
+ }
+
+ return this.annotationsPromise;
+ }
+ }, {
+ key: "render",
+ value: function render(_ref3) {
+ var _this5 = this;
+
+ var canvasContext = _ref3.canvasContext,
+ viewport = _ref3.viewport,
+ _ref3$intent = _ref3.intent,
+ intent = _ref3$intent === void 0 ? 'display' : _ref3$intent,
+ _ref3$enableWebGL = _ref3.enableWebGL,
+ enableWebGL = _ref3$enableWebGL === void 0 ? false : _ref3$enableWebGL,
+ _ref3$renderInteracti = _ref3.renderInteractiveForms,
+ renderInteractiveForms = _ref3$renderInteracti === void 0 ? false : _ref3$renderInteracti,
+ _ref3$transform = _ref3.transform,
+ transform = _ref3$transform === void 0 ? null : _ref3$transform,
+ _ref3$imageLayer = _ref3.imageLayer,
+ imageLayer = _ref3$imageLayer === void 0 ? null : _ref3$imageLayer,
+ _ref3$canvasFactory = _ref3.canvasFactory,
+ canvasFactory = _ref3$canvasFactory === void 0 ? null : _ref3$canvasFactory,
+ _ref3$background = _ref3.background,
+ background = _ref3$background === void 0 ? null : _ref3$background;
+ var stats = this._stats;
+ stats.time('Overall');
+ this.pendingCleanup = false;
+ var renderingIntent = intent === 'print' ? 'print' : 'display';
+ var canvasFactoryInstance = canvasFactory || new _display_utils.DOMCanvasFactory();
+ var webGLContext = new _webgl.WebGLContext({
+ enable: enableWebGL
+ });
+
+ if (!this.intentStates[renderingIntent]) {
+ this.intentStates[renderingIntent] = Object.create(null);
+ }
+
+ var intentState = this.intentStates[renderingIntent];
+
+ if (!intentState.displayReadyCapability) {
+ intentState.receivingOperatorList = true;
+ intentState.displayReadyCapability = (0, _util.createPromiseCapability)();
+ intentState.operatorList = {
+ fnArray: [],
+ argsArray: [],
+ lastChunk: false
+ };
+ stats.time('Page Request');
+
+ this._transport.messageHandler.send('RenderPageRequest', {
+ pageIndex: this.pageNumber - 1,
+ intent: renderingIntent,
+ renderInteractiveForms: renderInteractiveForms === true
+ });
+ }
+
+ var complete = function complete(error) {
+ var i = intentState.renderTasks.indexOf(internalRenderTask);
+
+ if (i >= 0) {
+ intentState.renderTasks.splice(i, 1);
+ }
+
+ if (_this5.cleanupAfterRender || renderingIntent === 'print') {
+ _this5.pendingCleanup = true;
+ }
+
+ _this5._tryCleanup();
+
+ if (error) {
+ internalRenderTask.capability.reject(error);
+ } else {
+ internalRenderTask.capability.resolve();
+ }
+
+ stats.timeEnd('Rendering');
+ stats.timeEnd('Overall');
+ };
+
+ var internalRenderTask = new InternalRenderTask({
+ callback: complete,
+ params: {
+ canvasContext: canvasContext,
+ viewport: viewport,
+ transform: transform,
+ imageLayer: imageLayer,
+ background: background
+ },
+ objs: this.objs,
+ commonObjs: this.commonObjs,
+ operatorList: intentState.operatorList,
+ pageNumber: this.pageNumber,
+ canvasFactory: canvasFactoryInstance,
+ webGLContext: webGLContext,
+ useRequestAnimationFrame: renderingIntent !== 'print',
+ pdfBug: this._pdfBug
+ });
+
+ if (!intentState.renderTasks) {
+ intentState.renderTasks = [];
+ }
+
+ intentState.renderTasks.push(internalRenderTask);
+ var renderTask = internalRenderTask.task;
+ intentState.displayReadyCapability.promise.then(function (transparency) {
+ if (_this5.pendingCleanup) {
+ complete();
+ return;
+ }
+
+ stats.time('Rendering');
+ internalRenderTask.initializeGraphics(transparency);
+ internalRenderTask.operatorListChanged();
+ })["catch"](complete);
+ return renderTask;
+ }
+ }, {
+ key: "getOperatorList",
+ value: function getOperatorList() {
+ function operatorListChanged() {
+ if (intentState.operatorList.lastChunk) {
+ intentState.opListReadCapability.resolve(intentState.operatorList);
+ var i = intentState.renderTasks.indexOf(opListTask);
+
+ if (i >= 0) {
+ intentState.renderTasks.splice(i, 1);
+ }
+ }
+ }
+
+ var renderingIntent = 'oplist';
+
+ if (!this.intentStates[renderingIntent]) {
+ this.intentStates[renderingIntent] = Object.create(null);
+ }
+
+ var intentState = this.intentStates[renderingIntent];
+ var opListTask;
+
+ if (!intentState.opListReadCapability) {
+ opListTask = {};
+ opListTask.operatorListChanged = operatorListChanged;
+ intentState.receivingOperatorList = true;
+ intentState.opListReadCapability = (0, _util.createPromiseCapability)();
+ intentState.renderTasks = [];
+ intentState.renderTasks.push(opListTask);
+ intentState.operatorList = {
+ fnArray: [],
+ argsArray: [],
+ lastChunk: false
+ };
+
+ this._stats.time('Page Request');
+
+ this._transport.messageHandler.send('RenderPageRequest', {
+ pageIndex: this.pageIndex,
+ intent: renderingIntent
+ });
+ }
+
+ return intentState.opListReadCapability.promise;
+ }
+ }, {
+ key: "streamTextContent",
+ value: function streamTextContent() {
+ var _ref4 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
+ _ref4$normalizeWhites = _ref4.normalizeWhitespace,
+ normalizeWhitespace = _ref4$normalizeWhites === void 0 ? false : _ref4$normalizeWhites,
+ _ref4$disableCombineT = _ref4.disableCombineTextItems,
+ disableCombineTextItems = _ref4$disableCombineT === void 0 ? false : _ref4$disableCombineT;
+
+ var TEXT_CONTENT_CHUNK_SIZE = 100;
+ return this._transport.messageHandler.sendWithStream('GetTextContent', {
+ pageIndex: this.pageNumber - 1,
+ normalizeWhitespace: normalizeWhitespace === true,
+ combineTextItems: disableCombineTextItems !== true
+ }, {
+ highWaterMark: TEXT_CONTENT_CHUNK_SIZE,
+ size: function size(textContent) {
+ return textContent.items.length;
+ }
+ });
+ }
+ }, {
+ key: "getTextContent",
+ value: function getTextContent() {
+ var params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+ var readableStream = this.streamTextContent(params);
+ return new Promise(function (resolve, reject) {
+ function pump() {
+ reader.read().then(function (_ref5) {
+ var _textContent$items;
+
+ var value = _ref5.value,
+ done = _ref5.done;
+
+ if (done) {
+ resolve(textContent);
+ return;
+ }
+
+ Object.assign(textContent.styles, value.styles);
+
+ (_textContent$items = textContent.items).push.apply(_textContent$items, _toConsumableArray(value.items));
+
+ pump();
+ }, reject);
+ }
+
+ var reader = readableStream.getReader();
+ var textContent = {
+ items: [],
+ styles: Object.create(null)
+ };
+ pump();
+ });
+ }
+ }, {
+ key: "_destroy",
+ value: function _destroy() {
+ this.destroyed = true;
+ this._transport.pageCache[this.pageIndex] = null;
+ var waitOn = [];
+ Object.keys(this.intentStates).forEach(function (intent) {
+ if (intent === 'oplist') {
+ return;
+ }
+
+ var intentState = this.intentStates[intent];
+ intentState.renderTasks.forEach(function (renderTask) {
+ var renderCompleted = renderTask.capability.promise["catch"](function () {});
+ waitOn.push(renderCompleted);
+ renderTask.cancel();
+ });
+ }, this);
+ this.objs.clear();
+ this.annotationsPromise = null;
+ this.pendingCleanup = false;
+ return Promise.all(waitOn);
+ }
+ }, {
+ key: "cleanup",
+ value: function cleanup() {
+ var resetStats = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ this.pendingCleanup = true;
+
+ this._tryCleanup(resetStats);
+ }
+ }, {
+ key: "_tryCleanup",
+ value: function _tryCleanup() {
+ var resetStats = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+
+ if (!this.pendingCleanup || Object.keys(this.intentStates).some(function (intent) {
+ var intentState = this.intentStates[intent];
+ return intentState.renderTasks.length !== 0 || intentState.receivingOperatorList;
+ }, this)) {
+ return;
+ }
+
+ Object.keys(this.intentStates).forEach(function (intent) {
+ delete this.intentStates[intent];
+ }, this);
+ this.objs.clear();
+ this.annotationsPromise = null;
+
+ if (resetStats && this._stats instanceof _display_utils.StatTimer) {
+ this._stats = new _display_utils.StatTimer();
+ }
+
+ this.pendingCleanup = false;
+ }
+ }, {
+ key: "_startRenderPage",
+ value: function _startRenderPage(transparency, intent) {
+ var intentState = this.intentStates[intent];
+
+ if (intentState.displayReadyCapability) {
+ intentState.displayReadyCapability.resolve(transparency);
+ }
+ }
+ }, {
+ key: "_renderPageChunk",
+ value: function _renderPageChunk(operatorListChunk, intent) {
+ var intentState = this.intentStates[intent];
+
+ for (var i = 0, ii = operatorListChunk.length; i < ii; i++) {
+ intentState.operatorList.fnArray.push(operatorListChunk.fnArray[i]);
+ intentState.operatorList.argsArray.push(operatorListChunk.argsArray[i]);
+ }
+
+ intentState.operatorList.lastChunk = operatorListChunk.lastChunk;
+
+ for (var _i = 0; _i < intentState.renderTasks.length; _i++) {
+ intentState.renderTasks[_i].operatorListChanged();
+ }
+
+ if (operatorListChunk.lastChunk) {
+ intentState.receivingOperatorList = false;
+
+ this._tryCleanup();
+ }
+ }
+ }, {
+ key: "pageNumber",
+ get: function get() {
+ return this.pageIndex + 1;
+ }
+ }, {
+ key: "rotate",
+ get: function get() {
+ return this._pageInfo.rotate;
+ }
+ }, {
+ key: "ref",
+ get: function get() {
+ return this._pageInfo.ref;
+ }
+ }, {
+ key: "userUnit",
+ get: function get() {
+ return this._pageInfo.userUnit;
+ }
+ }, {
+ key: "view",
+ get: function get() {
+ return this._pageInfo.view;
+ }
+ }, {
+ key: "stats",
+ get: function get() {
+ return this._stats instanceof _display_utils.StatTimer ? this._stats : null;
+ }
+ }]);
+
+ return PDFPageProxy;
+}();
+
+exports.PDFPageProxy = PDFPageProxy;
+
+var LoopbackPort =
+/*#__PURE__*/
+function () {
+ function LoopbackPort() {
+ var defer = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
+
+ _classCallCheck(this, LoopbackPort);
+
+ this._listeners = [];
+ this._defer = defer;
+ this._deferred = Promise.resolve(undefined);
+ }
+
+ _createClass(LoopbackPort, [{
+ key: "postMessage",
+ value: function postMessage(obj, transfers) {
+ var _this6 = this;
+
+ function cloneValue(value) {
+ if (_typeof(value) !== 'object' || value === null) {
+ return value;
+ }
+
+ if (cloned.has(value)) {
+ return cloned.get(value);
+ }
+
+ var buffer, result;
+
+ if ((buffer = value.buffer) && (0, _util.isArrayBuffer)(buffer)) {
+ var transferable = transfers && transfers.includes(buffer);
+
+ if (value === buffer) {
+ result = value;
+ } else if (transferable) {
+ result = new value.constructor(buffer, value.byteOffset, value.byteLength);
+ } else {
+ result = new value.constructor(value);
+ }
+
+ cloned.set(value, result);
+ return result;
+ }
+
+ result = Array.isArray(value) ? [] : {};
+ cloned.set(value, result);
+
+ for (var i in value) {
+ var desc = void 0,
+ p = value;
+
+ while (!(desc = Object.getOwnPropertyDescriptor(p, i))) {
+ p = Object.getPrototypeOf(p);
+ }
+
+ if (typeof desc.value === 'undefined' || typeof desc.value === 'function') {
+ continue;
+ }
+
+ result[i] = cloneValue(desc.value);
+ }
+
+ return result;
+ }
+
+ if (!this._defer) {
+ this._listeners.forEach(function (listener) {
+ listener.call(this, {
+ data: obj
+ });
+ }, this);
+
+ return;
+ }
+
+ var cloned = new WeakMap();
+ var e = {
+ data: cloneValue(obj)
+ };
+
+ this._deferred.then(function () {
+ _this6._listeners.forEach(function (listener) {
+ listener.call(this, e);
+ }, _this6);
+ });
+ }
+ }, {
+ key: "addEventListener",
+ value: function addEventListener(name, listener) {
+ this._listeners.push(listener);
+ }
+ }, {
+ key: "removeEventListener",
+ value: function removeEventListener(name, listener) {
+ var i = this._listeners.indexOf(listener);
+
+ this._listeners.splice(i, 1);
+ }
+ }, {
+ key: "terminate",
+ value: function terminate() {
+ this._listeners.length = 0;
+ }
+ }]);
+
+ return LoopbackPort;
+}();
+
+exports.LoopbackPort = LoopbackPort;
+
+var PDFWorker = function PDFWorkerClosure() {
+ var pdfWorkerPorts = new WeakMap();
+ var nextFakeWorkerId = 0;
+ var fakeWorkerFilesLoadedCapability;
+
+ function _getWorkerSrc() {
+ if (_worker_options.GlobalWorkerOptions.workerSrc) {
+ return _worker_options.GlobalWorkerOptions.workerSrc;
+ }
+
+ if (typeof fallbackWorkerSrc !== 'undefined') {
+ return fallbackWorkerSrc;
+ }
+
+ throw new Error('No "GlobalWorkerOptions.workerSrc" specified.');
+ }
+
+ function getMainThreadWorkerMessageHandler() {
+ try {
+ if (typeof window !== 'undefined') {
+ return window.pdfjsWorker && window.pdfjsWorker.WorkerMessageHandler;
+ }
+ } catch (ex) {}
+
+ return null;
+ }
+
+ function setupFakeWorkerGlobal() {
+ if (fakeWorkerFilesLoadedCapability) {
+ return fakeWorkerFilesLoadedCapability.promise;
+ }
+
+ fakeWorkerFilesLoadedCapability = (0, _util.createPromiseCapability)();
+ var mainWorkerMessageHandler = getMainThreadWorkerMessageHandler();
+
+ if (mainWorkerMessageHandler) {
+ fakeWorkerFilesLoadedCapability.resolve(mainWorkerMessageHandler);
+ return fakeWorkerFilesLoadedCapability.promise;
+ }
+
+ var loader = fakeWorkerFilesLoader || function () {
+ return (0, _display_utils.loadScript)(_getWorkerSrc()).then(function () {
+ return window.pdfjsWorker.WorkerMessageHandler;
+ });
+ };
+
+ loader().then(fakeWorkerFilesLoadedCapability.resolve, fakeWorkerFilesLoadedCapability.reject);
+ return fakeWorkerFilesLoadedCapability.promise;
+ }
+
+ function createCDNWrapper(url) {
+ var wrapper = 'importScripts(\'' + url + '\');';
+ return _util.URL.createObjectURL(new Blob([wrapper]));
+ }
+
+ var PDFWorker =
+ /*#__PURE__*/
+ function () {
+ function PDFWorker() {
+ var _ref6 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
+ _ref6$name = _ref6.name,
+ name = _ref6$name === void 0 ? null : _ref6$name,
+ _ref6$port = _ref6.port,
+ port = _ref6$port === void 0 ? null : _ref6$port,
+ _ref6$postMessageTran = _ref6.postMessageTransfers,
+ postMessageTransfers = _ref6$postMessageTran === void 0 ? true : _ref6$postMessageTran,
+ _ref6$verbosity = _ref6.verbosity,
+ verbosity = _ref6$verbosity === void 0 ? (0, _util.getVerbosityLevel)() : _ref6$verbosity;
+
+ _classCallCheck(this, PDFWorker);
+
+ if (port && pdfWorkerPorts.has(port)) {
+ throw new Error('Cannot use more than one PDFWorker per port');
+ }
+
+ this.name = name;
+ this.destroyed = false;
+ this.postMessageTransfers = postMessageTransfers !== false;
+ this.verbosity = verbosity;
+ this._readyCapability = (0, _util.createPromiseCapability)();
+ this._port = null;
+ this._webWorker = null;
+ this._messageHandler = null;
+
+ if (port) {
+ pdfWorkerPorts.set(port, this);
+
+ this._initializeFromPort(port);
+
+ return;
+ }
+
+ this._initialize();
+ }
+
+ _createClass(PDFWorker, [{
+ key: "_initializeFromPort",
+ value: function _initializeFromPort(port) {
+ this._port = port;
+ this._messageHandler = new _message_handler.MessageHandler('main', 'worker', port);
+
+ this._messageHandler.on('ready', function () {});
+
+ this._readyCapability.resolve();
+ }
+ }, {
+ key: "_initialize",
+ value: function _initialize() {
+ var _this7 = this;
+
+ if (typeof Worker !== 'undefined' && !isWorkerDisabled && !getMainThreadWorkerMessageHandler()) {
+ var workerSrc = _getWorkerSrc();
+
+ try {
+ if (!(0, _util.isSameOrigin)(window.location.href, workerSrc)) {
+ workerSrc = createCDNWrapper(new _util.URL(workerSrc, window.location).href);
+ }
+
+ var worker = new Worker(workerSrc);
+ var messageHandler = new _message_handler.MessageHandler('main', 'worker', worker);
+
+ var terminateEarly = function terminateEarly() {
+ worker.removeEventListener('error', onWorkerError);
+ messageHandler.destroy();
+ worker.terminate();
+
+ if (_this7.destroyed) {
+ _this7._readyCapability.reject(new Error('Worker was destroyed'));
+ } else {
+ _this7._setupFakeWorker();
+ }
+ };
+
+ var onWorkerError = function onWorkerError() {
+ if (!_this7._webWorker) {
+ terminateEarly();
+ }
+ };
+
+ worker.addEventListener('error', onWorkerError);
+ messageHandler.on('test', function (data) {
+ worker.removeEventListener('error', onWorkerError);
+
+ if (_this7.destroyed) {
+ terminateEarly();
+ return;
+ }
+
+ if (data && data.supportTypedArray) {
+ _this7._messageHandler = messageHandler;
+ _this7._port = worker;
+ _this7._webWorker = worker;
+
+ if (!data.supportTransfers) {
+ _this7.postMessageTransfers = false;
+ }
+
+ _this7._readyCapability.resolve();
+
+ messageHandler.send('configure', {
+ verbosity: _this7.verbosity
+ });
+ } else {
+ _this7._setupFakeWorker();
+
+ messageHandler.destroy();
+ worker.terminate();
+ }
+ });
+ messageHandler.on('ready', function (data) {
+ worker.removeEventListener('error', onWorkerError);
+
+ if (_this7.destroyed) {
+ terminateEarly();
+ return;
+ }
+
+ try {
+ sendTest();
+ } catch (e) {
+ _this7._setupFakeWorker();
+ }
+ });
+
+ var sendTest = function sendTest() {
+ var testObj = new Uint8Array([_this7.postMessageTransfers ? 255 : 0]);
+
+ try {
+ messageHandler.send('test', testObj, [testObj.buffer]);
+ } catch (ex) {
+ (0, _util.info)('Cannot use postMessage transfers');
+ testObj[0] = 0;
+ messageHandler.send('test', testObj);
+ }
+ };
+
+ sendTest();
+ return;
+ } catch (e) {
+ (0, _util.info)('The worker has been disabled.');
+ }
+ }
+
+ this._setupFakeWorker();
+ }
+ }, {
+ key: "_setupFakeWorker",
+ value: function _setupFakeWorker() {
+ var _this8 = this;
+
+ if (!isWorkerDisabled) {
+ (0, _util.warn)('Setting up fake worker.');
+ isWorkerDisabled = true;
+ }
+
+ setupFakeWorkerGlobal().then(function (WorkerMessageHandler) {
+ if (_this8.destroyed) {
+ _this8._readyCapability.reject(new Error('Worker was destroyed'));
+
+ return;
+ }
+
+ var port = new LoopbackPort();
+ _this8._port = port;
+ var id = 'fake' + nextFakeWorkerId++;
+ var workerHandler = new _message_handler.MessageHandler(id + '_worker', id, port);
+ WorkerMessageHandler.setup(workerHandler, port);
+ var messageHandler = new _message_handler.MessageHandler(id, id + '_worker', port);
+ _this8._messageHandler = messageHandler;
+
+ _this8._readyCapability.resolve();
+ })["catch"](function (reason) {
+ _this8._readyCapability.reject(new Error("Setting up fake worker failed: \"".concat(reason.message, "\".")));
+ });
+ }
+ }, {
+ key: "destroy",
+ value: function destroy() {
+ this.destroyed = true;
+
+ if (this._webWorker) {
+ this._webWorker.terminate();
+
+ this._webWorker = null;
+ }
+
+ pdfWorkerPorts["delete"](this._port);
+ this._port = null;
+
+ if (this._messageHandler) {
+ this._messageHandler.destroy();
+
+ this._messageHandler = null;
+ }
+ }
+ }, {
+ key: "promise",
+ get: function get() {
+ return this._readyCapability.promise;
+ }
+ }, {
+ key: "port",
+ get: function get() {
+ return this._port;
+ }
+ }, {
+ key: "messageHandler",
+ get: function get() {
+ return this._messageHandler;
+ }
+ }], [{
+ key: "fromPort",
+ value: function fromPort(params) {
+ if (!params || !params.port) {
+ throw new Error('PDFWorker.fromPort - invalid method signature.');
+ }
+
+ if (pdfWorkerPorts.has(params.port)) {
+ return pdfWorkerPorts.get(params.port);
+ }
+
+ return new PDFWorker(params);
+ }
+ }, {
+ key: "getWorkerSrc",
+ value: function getWorkerSrc() {
+ return _getWorkerSrc();
+ }
+ }]);
+
+ return PDFWorker;
+ }();
+
+ return PDFWorker;
+}();
+
+exports.PDFWorker = PDFWorker;
+
+var WorkerTransport =
+/*#__PURE__*/
+function () {
+ function WorkerTransport(messageHandler, loadingTask, networkStream, params) {
+ _classCallCheck(this, WorkerTransport);
+
+ this.messageHandler = messageHandler;
+ this.loadingTask = loadingTask;
+ this.commonObjs = new PDFObjects();
+ this.fontLoader = new _font_loader.FontLoader({
+ docId: loadingTask.docId,
+ onUnsupportedFeature: this._onUnsupportedFeature.bind(this)
+ });
+ this._params = params;
+ this.CMapReaderFactory = new params.CMapReaderFactory({
+ baseUrl: params.cMapUrl,
+ isCompressed: params.cMapPacked
+ });
+ this.destroyed = false;
+ this.destroyCapability = null;
+ this._passwordCapability = null;
+ this._networkStream = networkStream;
+ this._fullReader = null;
+ this._lastProgress = null;
+ this.pageCache = [];
+ this.pagePromises = [];
+ this.downloadInfoCapability = (0, _util.createPromiseCapability)();
+ this.setupMessageHandler();
+ }
+
+ _createClass(WorkerTransport, [{
+ key: "destroy",
+ value: function destroy() {
+ var _this9 = this;
+
+ if (this.destroyCapability) {
+ return this.destroyCapability.promise;
+ }
+
+ this.destroyed = true;
+ this.destroyCapability = (0, _util.createPromiseCapability)();
+
+ if (this._passwordCapability) {
+ this._passwordCapability.reject(new Error('Worker was destroyed during onPassword callback'));
+ }
+
+ var waitOn = [];
+ this.pageCache.forEach(function (page) {
+ if (page) {
+ waitOn.push(page._destroy());
+ }
+ });
+ this.pageCache.length = 0;
+ this.pagePromises.length = 0;
+ var terminated = this.messageHandler.sendWithPromise('Terminate', null);
+ waitOn.push(terminated);
+ Promise.all(waitOn).then(function () {
+ _this9.fontLoader.clear();
+
+ if (_this9._networkStream) {
+ _this9._networkStream.cancelAllRequests();
+ }
+
+ if (_this9.messageHandler) {
+ _this9.messageHandler.destroy();
+
+ _this9.messageHandler = null;
+ }
+
+ _this9.destroyCapability.resolve();
+ }, this.destroyCapability.reject);
+ return this.destroyCapability.promise;
+ }
+ }, {
+ key: "setupMessageHandler",
+ value: function setupMessageHandler() {
+ var messageHandler = this.messageHandler,
+ loadingTask = this.loadingTask;
+ messageHandler.on('GetReader', function (data, sink) {
+ var _this10 = this;
+
+ (0, _util.assert)(this._networkStream);
+ this._fullReader = this._networkStream.getFullReader();
+
+ this._fullReader.onProgress = function (evt) {
+ _this10._lastProgress = {
+ loaded: evt.loaded,
+ total: evt.total
+ };
+ };
+
+ sink.onPull = function () {
+ _this10._fullReader.read().then(function (_ref7) {
+ var value = _ref7.value,
+ done = _ref7.done;
+
+ if (done) {
+ sink.close();
+ return;
+ }
+
+ (0, _util.assert)((0, _util.isArrayBuffer)(value));
+ sink.enqueue(new Uint8Array(value), 1, [value]);
+ })["catch"](function (reason) {
+ sink.error(reason);
+ });
+ };
+
+ sink.onCancel = function (reason) {
+ _this10._fullReader.cancel(reason);
+ };
+ }, this);
+ messageHandler.on('ReaderHeadersReady', function (data) {
+ var _this11 = this;
+
+ var headersCapability = (0, _util.createPromiseCapability)();
+ var fullReader = this._fullReader;
+ fullReader.headersReady.then(function () {
+ if (!fullReader.isStreamingSupported || !fullReader.isRangeSupported) {
+ if (_this11._lastProgress && loadingTask.onProgress) {
+ loadingTask.onProgress(_this11._lastProgress);
+ }
+
+ fullReader.onProgress = function (evt) {
+ if (loadingTask.onProgress) {
+ loadingTask.onProgress({
+ loaded: evt.loaded,
+ total: evt.total
+ });
+ }
+ };
+ }
+
+ headersCapability.resolve({
+ isStreamingSupported: fullReader.isStreamingSupported,
+ isRangeSupported: fullReader.isRangeSupported,
+ contentLength: fullReader.contentLength
+ });
+ }, headersCapability.reject);
+ return headersCapability.promise;
+ }, this);
+ messageHandler.on('GetRangeReader', function (data, sink) {
+ (0, _util.assert)(this._networkStream);
+
+ var rangeReader = this._networkStream.getRangeReader(data.begin, data.end);
+
+ if (!rangeReader) {
+ sink.close();
+ return;
+ }
+
+ sink.onPull = function () {
+ rangeReader.read().then(function (_ref8) {
+ var value = _ref8.value,
+ done = _ref8.done;
+
+ if (done) {
+ sink.close();
+ return;
+ }
+
+ (0, _util.assert)((0, _util.isArrayBuffer)(value));
+ sink.enqueue(new Uint8Array(value), 1, [value]);
+ })["catch"](function (reason) {
+ sink.error(reason);
+ });
+ };
+
+ sink.onCancel = function (reason) {
+ rangeReader.cancel(reason);
+ };
+ }, this);
+ messageHandler.on('GetDoc', function (_ref9) {
+ var pdfInfo = _ref9.pdfInfo;
+ this._numPages = pdfInfo.numPages;
+
+ loadingTask._capability.resolve(new PDFDocumentProxy(pdfInfo, this));
+ }, this);
+ messageHandler.on('PasswordRequest', function (exception) {
+ var _this12 = this;
+
+ this._passwordCapability = (0, _util.createPromiseCapability)();
+
+ if (loadingTask.onPassword) {
+ var updatePassword = function updatePassword(password) {
+ _this12._passwordCapability.resolve({
+ password: password
+ });
+ };
+
+ try {
+ loadingTask.onPassword(updatePassword, exception.code);
+ } catch (ex) {
+ this._passwordCapability.reject(ex);
+ }
+ } else {
+ this._passwordCapability.reject(new _util.PasswordException(exception.message, exception.code));
+ }
+
+ return this._passwordCapability.promise;
+ }, this);
+ messageHandler.on('PasswordException', function (exception) {
+ loadingTask._capability.reject(new _util.PasswordException(exception.message, exception.code));
+ }, this);
+ messageHandler.on('InvalidPDF', function (exception) {
+ loadingTask._capability.reject(new _util.InvalidPDFException(exception.message));
+ }, this);
+ messageHandler.on('MissingPDF', function (exception) {
+ loadingTask._capability.reject(new _util.MissingPDFException(exception.message));
+ }, this);
+ messageHandler.on('UnexpectedResponse', function (exception) {
+ loadingTask._capability.reject(new _util.UnexpectedResponseException(exception.message, exception.status));
+ }, this);
+ messageHandler.on('UnknownError', function (exception) {
+ loadingTask._capability.reject(new _util.UnknownErrorException(exception.message, exception.details));
+ }, this);
+ messageHandler.on('DataLoaded', function (data) {
+ if (loadingTask.onProgress) {
+ loadingTask.onProgress({
+ loaded: data.length,
+ total: data.length
+ });
+ }
+
+ this.downloadInfoCapability.resolve(data);
+ }, this);
+ messageHandler.on('StartRenderPage', function (data) {
+ if (this.destroyed) {
+ return;
+ }
+
+ var page = this.pageCache[data.pageIndex];
+
+ page._stats.timeEnd('Page Request');
+
+ page._startRenderPage(data.transparency, data.intent);
+ }, this);
+ messageHandler.on('RenderPageChunk', function (data) {
+ if (this.destroyed) {
+ return;
+ }
+
+ var page = this.pageCache[data.pageIndex];
+
+ page._renderPageChunk(data.operatorList, data.intent);
+ }, this);
+ messageHandler.on('commonobj', function (data) {
+ var _this13 = this;
+
+ if (this.destroyed) {
+ return;
+ }
+
+ var _data = _slicedToArray(data, 3),
+ id = _data[0],
+ type = _data[1],
+ exportedData = _data[2];
+
+ if (this.commonObjs.has(id)) {
+ return;
+ }
+
+ switch (type) {
+ case 'Font':
+ var params = this._params;
+
+ if ('error' in exportedData) {
+ var exportedError = exportedData.error;
+ (0, _util.warn)("Error during font loading: ".concat(exportedError));
+ this.commonObjs.resolve(id, exportedError);
+ break;
+ }
+
+ var fontRegistry = null;
+
+ if (params.pdfBug && _global_scope["default"].FontInspector && _global_scope["default"].FontInspector.enabled) {
+ fontRegistry = {
+ registerFont: function registerFont(font, url) {
+ _global_scope["default"]['FontInspector'].fontAdded(font, url);
+ }
+ };
+ }
+
+ var font = new _font_loader.FontFaceObject(exportedData, {
+ isEvalSupported: params.isEvalSupported,
+ disableFontFace: params.disableFontFace,
+ ignoreErrors: params.ignoreErrors,
+ onUnsupportedFeature: this._onUnsupportedFeature.bind(this),
+ fontRegistry: fontRegistry
+ });
+ this.fontLoader.bind(font).then(function () {
+ _this13.commonObjs.resolve(id, font);
+ }, function (reason) {
+ messageHandler.sendWithPromise('FontFallback', {
+ id: id
+ })["finally"](function () {
+ _this13.commonObjs.resolve(id, font);
+ });
+ });
+ break;
+
+ case 'FontPath':
+ case 'FontType3Res':
+ this.commonObjs.resolve(id, exportedData);
+ break;
+
+ default:
+ throw new Error("Got unknown common object type ".concat(type));
+ }
+ }, this);
+ messageHandler.on('obj', function (data) {
+ if (this.destroyed) {
+ return undefined;
+ }
+
+ var _data2 = _slicedToArray(data, 4),
+ id = _data2[0],
+ pageIndex = _data2[1],
+ type = _data2[2],
+ imageData = _data2[3];
+
+ var pageProxy = this.pageCache[pageIndex];
+
+ if (pageProxy.objs.has(id)) {
+ return undefined;
+ }
+
+ switch (type) {
+ case 'JpegStream':
+ return new Promise(function (resolve, reject) {
+ var img = new Image();
+
+ img.onload = function () {
+ resolve(img);
+ };
+
+ img.onerror = function () {
+ reject(new Error('Error during JPEG image loading'));
+ (0, _display_utils.releaseImageResources)(img);
+ };
+
+ img.src = imageData;
+ }).then(function (img) {
+ pageProxy.objs.resolve(id, img);
+ });
+
+ case 'Image':
+ pageProxy.objs.resolve(id, imageData);
+ var MAX_IMAGE_SIZE_TO_STORE = 8000000;
+
+ if (imageData && 'data' in imageData && imageData.data.length > MAX_IMAGE_SIZE_TO_STORE) {
+ pageProxy.cleanupAfterRender = true;
+ }
+
+ break;
+
+ default:
+ throw new Error("Got unknown object type ".concat(type));
+ }
+
+ return undefined;
+ }, this);
+ messageHandler.on('DocProgress', function (data) {
+ if (this.destroyed) {
+ return;
+ }
+
+ if (loadingTask.onProgress) {
+ loadingTask.onProgress({
+ loaded: data.loaded,
+ total: data.total
+ });
+ }
+ }, this);
+ messageHandler.on('PageError', function (data) {
+ if (this.destroyed) {
+ return;
+ }
+
+ var page = this.pageCache[data.pageIndex];
+ var intentState = page.intentStates[data.intent];
+
+ if (intentState.displayReadyCapability) {
+ intentState.displayReadyCapability.reject(new Error(data.error));
+ } else {
+ throw new Error(data.error);
+ }
+
+ if (intentState.operatorList) {
+ intentState.operatorList.lastChunk = true;
+
+ for (var i = 0; i < intentState.renderTasks.length; i++) {
+ intentState.renderTasks[i].operatorListChanged();
+ }
+ }
+ }, this);
+ messageHandler.on('UnsupportedFeature', this._onUnsupportedFeature, this);
+ messageHandler.on('JpegDecode', function (data) {
+ if (this.destroyed) {
+ return Promise.reject(new Error('Worker was destroyed'));
+ }
+
+ if (typeof document === 'undefined') {
+ return Promise.reject(new Error('"document" is not defined.'));
+ }
+
+ var _data3 = _slicedToArray(data, 2),
+ imageUrl = _data3[0],
+ components = _data3[1];
+
+ if (components !== 3 && components !== 1) {
+ return Promise.reject(new Error('Only 3 components or 1 component can be returned'));
+ }
+
+ return new Promise(function (resolve, reject) {
+ var img = new Image();
+
+ img.onload = function () {
+ var width = img.width,
+ height = img.height;
+ var size = width * height;
+ var rgbaLength = size * 4;
+ var buf = new Uint8ClampedArray(size * components);
+ var tmpCanvas = document.createElement('canvas');
+ tmpCanvas.width = width;
+ tmpCanvas.height = height;
+ var tmpCtx = tmpCanvas.getContext('2d');
+ tmpCtx.drawImage(img, 0, 0);
+ var data = tmpCtx.getImageData(0, 0, width, height).data;
+
+ if (components === 3) {
+ for (var i = 0, j = 0; i < rgbaLength; i += 4, j += 3) {
+ buf[j] = data[i];
+ buf[j + 1] = data[i + 1];
+ buf[j + 2] = data[i + 2];
+ }
+ } else if (components === 1) {
+ for (var _i2 = 0, _j = 0; _i2 < rgbaLength; _i2 += 4, _j++) {
+ buf[_j] = data[_i2];
+ }
+ }
+
+ resolve({
+ data: buf,
+ width: width,
+ height: height
+ });
+ (0, _display_utils.releaseImageResources)(img);
+ tmpCanvas.width = 0;
+ tmpCanvas.height = 0;
+ tmpCanvas = null;
+ tmpCtx = null;
+ };
+
+ img.onerror = function () {
+ reject(new Error('JpegDecode failed to load image'));
+ (0, _display_utils.releaseImageResources)(img);
+ };
+
+ img.src = imageUrl;
+ });
+ }, this);
+ messageHandler.on('FetchBuiltInCMap', function (data) {
+ if (this.destroyed) {
+ return Promise.reject(new Error('Worker was destroyed'));
+ }
+
+ return this.CMapReaderFactory.fetch({
+ name: data.name
+ });
+ }, this);
+ }
+ }, {
+ key: "_onUnsupportedFeature",
+ value: function _onUnsupportedFeature(_ref10) {
+ var featureId = _ref10.featureId;
+
+ if (this.destroyed) {
+ return;
+ }
+
+ if (this.loadingTask.onUnsupportedFeature) {
+ this.loadingTask.onUnsupportedFeature(featureId);
+ }
+ }
+ }, {
+ key: "getData",
+ value: function getData() {
+ return this.messageHandler.sendWithPromise('GetData', null);
+ }
+ }, {
+ key: "getPage",
+ value: function getPage(pageNumber) {
+ var _this14 = this;
+
+ if (!Number.isInteger(pageNumber) || pageNumber <= 0 || pageNumber > this._numPages) {
+ return Promise.reject(new Error('Invalid page request'));
+ }
+
+ var pageIndex = pageNumber - 1;
+
+ if (pageIndex in this.pagePromises) {
+ return this.pagePromises[pageIndex];
+ }
+
+ var promise = this.messageHandler.sendWithPromise('GetPage', {
+ pageIndex: pageIndex
+ }).then(function (pageInfo) {
+ if (_this14.destroyed) {
+ throw new Error('Transport destroyed');
+ }
+
+ var page = new PDFPageProxy(pageIndex, pageInfo, _this14, _this14._params.pdfBug);
+ _this14.pageCache[pageIndex] = page;
+ return page;
+ });
+ this.pagePromises[pageIndex] = promise;
+ return promise;
+ }
+ }, {
+ key: "getPageIndex",
+ value: function getPageIndex(ref) {
+ return this.messageHandler.sendWithPromise('GetPageIndex', {
+ ref: ref
+ })["catch"](function (reason) {
+ return Promise.reject(new Error(reason));
+ });
+ }
+ }, {
+ key: "getAnnotations",
+ value: function getAnnotations(pageIndex, intent) {
+ return this.messageHandler.sendWithPromise('GetAnnotations', {
+ pageIndex: pageIndex,
+ intent: intent
+ });
+ }
+ }, {
+ key: "getDestinations",
+ value: function getDestinations() {
+ return this.messageHandler.sendWithPromise('GetDestinations', null);
+ }
+ }, {
+ key: "getDestination",
+ value: function getDestination(id) {
+ if (typeof id !== 'string') {
+ return Promise.reject(new Error('Invalid destination request.'));
+ }
+
+ return this.messageHandler.sendWithPromise('GetDestination', {
+ id: id
+ });
+ }
+ }, {
+ key: "getPageLabels",
+ value: function getPageLabels() {
+ return this.messageHandler.sendWithPromise('GetPageLabels', null);
+ }
+ }, {
+ key: "getPageLayout",
+ value: function getPageLayout() {
+ return this.messageHandler.sendWithPromise('GetPageLayout', null);
+ }
+ }, {
+ key: "getPageMode",
+ value: function getPageMode() {
+ return this.messageHandler.sendWithPromise('GetPageMode', null);
+ }
+ }, {
+ key: "getViewerPreferences",
+ value: function getViewerPreferences() {
+ return this.messageHandler.sendWithPromise('GetViewerPreferences', null);
+ }
+ }, {
+ key: "getOpenActionDestination",
+ value: function getOpenActionDestination() {
+ return this.messageHandler.sendWithPromise('GetOpenActionDestination', null);
+ }
+ }, {
+ key: "getAttachments",
+ value: function getAttachments() {
+ return this.messageHandler.sendWithPromise('GetAttachments', null);
+ }
+ }, {
+ key: "getJavaScript",
+ value: function getJavaScript() {
+ return this.messageHandler.sendWithPromise('GetJavaScript', null);
+ }
+ }, {
+ key: "getOutline",
+ value: function getOutline() {
+ return this.messageHandler.sendWithPromise('GetOutline', null);
+ }
+ }, {
+ key: "getPermissions",
+ value: function getPermissions() {
+ return this.messageHandler.sendWithPromise('GetPermissions', null);
+ }
+ }, {
+ key: "getMetadata",
+ value: function getMetadata() {
+ var _this15 = this;
+
+ return this.messageHandler.sendWithPromise('GetMetadata', null).then(function (results) {
+ return {
+ info: results[0],
+ metadata: results[1] ? new _metadata.Metadata(results[1]) : null,
+ contentDispositionFilename: _this15._fullReader ? _this15._fullReader.filename : null
+ };
+ });
+ }
+ }, {
+ key: "getStats",
+ value: function getStats() {
+ return this.messageHandler.sendWithPromise('GetStats', null);
+ }
+ }, {
+ key: "startCleanup",
+ value: function startCleanup() {
+ var _this16 = this;
+
+ this.messageHandler.sendWithPromise('Cleanup', null).then(function () {
+ for (var i = 0, ii = _this16.pageCache.length; i < ii; i++) {
+ var page = _this16.pageCache[i];
+
+ if (page) {
+ page.cleanup();
+ }
+ }
+
+ _this16.commonObjs.clear();
+
+ _this16.fontLoader.clear();
+ });
+ }
+ }, {
+ key: "loadingParams",
+ get: function get() {
+ var params = this._params;
+ return (0, _util.shadow)(this, 'loadingParams', {
+ disableAutoFetch: params.disableAutoFetch,
+ disableCreateObjectURL: params.disableCreateObjectURL,
+ disableFontFace: params.disableFontFace,
+ nativeImageDecoderSupport: params.nativeImageDecoderSupport
+ });
+ }
+ }]);
+
+ return WorkerTransport;
+}();
+
+var PDFObjects =
+/*#__PURE__*/
+function () {
+ function PDFObjects() {
+ _classCallCheck(this, PDFObjects);
+
+ this._objs = Object.create(null);
+ }
+
+ _createClass(PDFObjects, [{
+ key: "_ensureObj",
+ value: function _ensureObj(objId) {
+ if (this._objs[objId]) {
+ return this._objs[objId];
+ }
+
+ return this._objs[objId] = {
+ capability: (0, _util.createPromiseCapability)(),
+ data: null,
+ resolved: false
+ };
+ }
+ }, {
+ key: "get",
+ value: function get(objId) {
+ var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
+
+ if (callback) {
+ this._ensureObj(objId).capability.promise.then(callback);
+
+ return null;
+ }
+
+ var obj = this._objs[objId];
+
+ if (!obj || !obj.resolved) {
+ throw new Error("Requesting object that isn't resolved yet ".concat(objId, "."));
+ }
+
+ return obj.data;
+ }
+ }, {
+ key: "has",
+ value: function has(objId) {
+ var obj = this._objs[objId];
+ return obj ? obj.resolved : false;
+ }
+ }, {
+ key: "resolve",
+ value: function resolve(objId, data) {
+ var obj = this._ensureObj(objId);
+
+ obj.resolved = true;
+ obj.data = data;
+ obj.capability.resolve(data);
+ }
+ }, {
+ key: "clear",
+ value: function clear() {
+ for (var objId in this._objs) {
+ var data = this._objs[objId].data;
+
+ if (typeof Image !== 'undefined' && data instanceof Image) {
+ (0, _display_utils.releaseImageResources)(data);
+ }
+ }
+
+ this._objs = Object.create(null);
+ }
+ }]);
+
+ return PDFObjects;
+}();
+
+var RenderTask =
+/*#__PURE__*/
+function () {
+ function RenderTask(internalRenderTask) {
+ _classCallCheck(this, RenderTask);
+
+ this._internalRenderTask = internalRenderTask;
+ this.onContinue = null;
+ }
+
+ _createClass(RenderTask, [{
+ key: "cancel",
+ value: function cancel() {
+ this._internalRenderTask.cancel();
+ }
+ }, {
+ key: "then",
+ value: function then(onFulfilled, onRejected) {
+ (0, _display_utils.deprecated)('RenderTask.then method, use the `promise` getter instead.');
+ return this.promise.then.apply(this.promise, arguments);
+ }
+ }, {
+ key: "promise",
+ get: function get() {
+ return this._internalRenderTask.capability.promise;
+ }
+ }]);
+
+ return RenderTask;
+}();
+
+var InternalRenderTask = function InternalRenderTaskClosure() {
+ var canvasInRendering = new WeakSet();
+
+ var InternalRenderTask =
+ /*#__PURE__*/
+ function () {
+ function InternalRenderTask(_ref11) {
+ var callback = _ref11.callback,
+ params = _ref11.params,
+ objs = _ref11.objs,
+ commonObjs = _ref11.commonObjs,
+ operatorList = _ref11.operatorList,
+ pageNumber = _ref11.pageNumber,
+ canvasFactory = _ref11.canvasFactory,
+ webGLContext = _ref11.webGLContext,
+ _ref11$useRequestAnim = _ref11.useRequestAnimationFrame,
+ useRequestAnimationFrame = _ref11$useRequestAnim === void 0 ? false : _ref11$useRequestAnim,
+ _ref11$pdfBug = _ref11.pdfBug,
+ pdfBug = _ref11$pdfBug === void 0 ? false : _ref11$pdfBug;
+
+ _classCallCheck(this, InternalRenderTask);
+
+ this.callback = callback;
+ this.params = params;
+ this.objs = objs;
+ this.commonObjs = commonObjs;
+ this.operatorListIdx = null;
+ this.operatorList = operatorList;
+ this.pageNumber = pageNumber;
+ this.canvasFactory = canvasFactory;
+ this.webGLContext = webGLContext;
+ this._pdfBug = pdfBug;
+ this.running = false;
+ this.graphicsReadyCallback = null;
+ this.graphicsReady = false;
+ this._useRequestAnimationFrame = useRequestAnimationFrame === true && typeof window !== 'undefined';
+ this.cancelled = false;
+ this.capability = (0, _util.createPromiseCapability)();
+ this.task = new RenderTask(this);
+ this._continueBound = this._continue.bind(this);
+ this._scheduleNextBound = this._scheduleNext.bind(this);
+ this._nextBound = this._next.bind(this);
+ this._canvas = params.canvasContext.canvas;
+ }
+
+ _createClass(InternalRenderTask, [{
+ key: "initializeGraphics",
+ value: function initializeGraphics() {
+ var transparency = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+
+ if (this.cancelled) {
+ return;
+ }
+
+ if (this._canvas) {
+ if (canvasInRendering.has(this._canvas)) {
+ throw new Error('Cannot use the same canvas during multiple render() operations. ' + 'Use different canvas or ensure previous operations were ' + 'cancelled or completed.');
+ }
+
+ canvasInRendering.add(this._canvas);
+ }
+
+ if (this._pdfBug && _global_scope["default"].StepperManager && _global_scope["default"].StepperManager.enabled) {
+ this.stepper = _global_scope["default"].StepperManager.create(this.pageNumber - 1);
+ this.stepper.init(this.operatorList);
+ this.stepper.nextBreakPoint = this.stepper.getNextBreakPoint();
+ }
+
+ var _this$params = this.params,
+ canvasContext = _this$params.canvasContext,
+ viewport = _this$params.viewport,
+ transform = _this$params.transform,
+ imageLayer = _this$params.imageLayer,
+ background = _this$params.background;
+ this.gfx = new _canvas.CanvasGraphics(canvasContext, this.commonObjs, this.objs, this.canvasFactory, this.webGLContext, imageLayer);
+ this.gfx.beginDrawing({
+ transform: transform,
+ viewport: viewport,
+ transparency: transparency,
+ background: background
+ });
+ this.operatorListIdx = 0;
+ this.graphicsReady = true;
+
+ if (this.graphicsReadyCallback) {
+ this.graphicsReadyCallback();
+ }
+ }
+ }, {
+ key: "cancel",
+ value: function cancel() {
+ var error = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
+ this.running = false;
+ this.cancelled = true;
+
+ if (this.gfx) {
+ this.gfx.endDrawing();
+ }
+
+ if (this._canvas) {
+ canvasInRendering["delete"](this._canvas);
+ }
+
+ this.callback(error || new _display_utils.RenderingCancelledException("Rendering cancelled, page ".concat(this.pageNumber), 'canvas'));
+ }
+ }, {
+ key: "operatorListChanged",
+ value: function operatorListChanged() {
+ if (!this.graphicsReady) {
+ if (!this.graphicsReadyCallback) {
+ this.graphicsReadyCallback = this._continueBound;
+ }
+
+ return;
+ }
+
+ if (this.stepper) {
+ this.stepper.updateOperatorList(this.operatorList);
+ }
+
+ if (this.running) {
+ return;
+ }
+
+ this._continue();
+ }
+ }, {
+ key: "_continue",
+ value: function _continue() {
+ this.running = true;
+
+ if (this.cancelled) {
+ return;
+ }
+
+ if (this.task.onContinue) {
+ this.task.onContinue(this._scheduleNextBound);
+ } else {
+ this._scheduleNext();
+ }
+ }
+ }, {
+ key: "_scheduleNext",
+ value: function _scheduleNext() {
+ var _this17 = this;
+
+ if (this._useRequestAnimationFrame) {
+ window.requestAnimationFrame(function () {
+ _this17._nextBound()["catch"](_this17.cancel.bind(_this17));
+ });
+ } else {
+ Promise.resolve().then(this._nextBound)["catch"](this.cancel.bind(this));
+ }
+ }
+ }, {
+ key: "_next",
+ value: function () {
+ var _next2 = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee() {
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ if (!this.cancelled) {
+ _context.next = 2;
+ break;
+ }
+
+ return _context.abrupt("return");
+
+ case 2:
+ this.operatorListIdx = this.gfx.executeOperatorList(this.operatorList, this.operatorListIdx, this._continueBound, this.stepper);
+
+ if (this.operatorListIdx === this.operatorList.argsArray.length) {
+ this.running = false;
+
+ if (this.operatorList.lastChunk) {
+ this.gfx.endDrawing();
+
+ if (this._canvas) {
+ canvasInRendering["delete"](this._canvas);
+ }
+
+ this.callback();
+ }
+ }
+
+ case 4:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee, this);
+ }));
+
+ function _next() {
+ return _next2.apply(this, arguments);
+ }
+
+ return _next;
+ }()
+ }]);
+
+ return InternalRenderTask;
+ }();
+
+ return InternalRenderTask;
+}();
+
+var version = '2.2.228';
+exports.version = version;
+var build = 'd7afb74a';
+exports.build = build;
+
+/***/ }),
+/* 148 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = __w_pdfjs_require__(149);
+
+/***/ }),
+/* 149 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+/* WEBPACK VAR INJECTION */(function(module) {
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var runtime = function (exports) {
+ "use strict";
+
+ var Op = Object.prototype;
+ var hasOwn = Op.hasOwnProperty;
+ var undefined;
+ var $Symbol = typeof Symbol === "function" ? Symbol : {};
+ var iteratorSymbol = $Symbol.iterator || "@@iterator";
+ var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator";
+ var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag";
+
+ function wrap(innerFn, outerFn, self, tryLocsList) {
+ var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator;
+ var generator = Object.create(protoGenerator.prototype);
+ var context = new Context(tryLocsList || []);
+ generator._invoke = makeInvokeMethod(innerFn, self, context);
+ return generator;
+ }
+
+ exports.wrap = wrap;
+
+ function tryCatch(fn, obj, arg) {
+ try {
+ return {
+ type: "normal",
+ arg: fn.call(obj, arg)
+ };
+ } catch (err) {
+ return {
+ type: "throw",
+ arg: err
+ };
+ }
+ }
+
+ var GenStateSuspendedStart = "suspendedStart";
+ var GenStateSuspendedYield = "suspendedYield";
+ var GenStateExecuting = "executing";
+ var GenStateCompleted = "completed";
+ var ContinueSentinel = {};
+
+ function Generator() {}
+
+ function GeneratorFunction() {}
+
+ function GeneratorFunctionPrototype() {}
+
+ var IteratorPrototype = {};
+
+ IteratorPrototype[iteratorSymbol] = function () {
+ return this;
+ };
+
+ var getProto = Object.getPrototypeOf;
+ var NativeIteratorPrototype = getProto && getProto(getProto(values([])));
+
+ if (NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) {
+ IteratorPrototype = NativeIteratorPrototype;
+ }
+
+ var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype);
+ GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;
+ GeneratorFunctionPrototype.constructor = GeneratorFunction;
+ GeneratorFunctionPrototype[toStringTagSymbol] = GeneratorFunction.displayName = "GeneratorFunction";
+
+ function defineIteratorMethods(prototype) {
+ ["next", "throw", "return"].forEach(function (method) {
+ prototype[method] = function (arg) {
+ return this._invoke(method, arg);
+ };
+ });
+ }
+
+ exports.isGeneratorFunction = function (genFun) {
+ var ctor = typeof genFun === "function" && genFun.constructor;
+ return ctor ? ctor === GeneratorFunction || (ctor.displayName || ctor.name) === "GeneratorFunction" : false;
+ };
+
+ exports.mark = function (genFun) {
+ if (Object.setPrototypeOf) {
+ Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);
+ } else {
+ genFun.__proto__ = GeneratorFunctionPrototype;
+
+ if (!(toStringTagSymbol in genFun)) {
+ genFun[toStringTagSymbol] = "GeneratorFunction";
+ }
+ }
+
+ genFun.prototype = Object.create(Gp);
+ return genFun;
+ };
+
+ exports.awrap = function (arg) {
+ return {
+ __await: arg
+ };
+ };
+
+ function AsyncIterator(generator) {
+ function invoke(method, arg, resolve, reject) {
+ var record = tryCatch(generator[method], generator, arg);
+
+ if (record.type === "throw") {
+ reject(record.arg);
+ } else {
+ var result = record.arg;
+ var value = result.value;
+
+ if (value && _typeof(value) === "object" && hasOwn.call(value, "__await")) {
+ return Promise.resolve(value.__await).then(function (value) {
+ invoke("next", value, resolve, reject);
+ }, function (err) {
+ invoke("throw", err, resolve, reject);
+ });
+ }
+
+ return Promise.resolve(value).then(function (unwrapped) {
+ result.value = unwrapped;
+ resolve(result);
+ }, function (error) {
+ return invoke("throw", error, resolve, reject);
+ });
+ }
+ }
+
+ var previousPromise;
+
+ function enqueue(method, arg) {
+ function callInvokeWithMethodAndArg() {
+ return new Promise(function (resolve, reject) {
+ invoke(method, arg, resolve, reject);
+ });
+ }
+
+ return previousPromise = previousPromise ? previousPromise.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg();
+ }
+
+ this._invoke = enqueue;
+ }
+
+ defineIteratorMethods(AsyncIterator.prototype);
+
+ AsyncIterator.prototype[asyncIteratorSymbol] = function () {
+ return this;
+ };
+
+ exports.AsyncIterator = AsyncIterator;
+
+ exports.async = function (innerFn, outerFn, self, tryLocsList) {
+ var iter = new AsyncIterator(wrap(innerFn, outerFn, self, tryLocsList));
+ return exports.isGeneratorFunction(outerFn) ? iter : iter.next().then(function (result) {
+ return result.done ? result.value : iter.next();
+ });
+ };
+
+ function makeInvokeMethod(innerFn, self, context) {
+ var state = GenStateSuspendedStart;
+ return function invoke(method, arg) {
+ if (state === GenStateExecuting) {
+ throw new Error("Generator is already running");
+ }
+
+ if (state === GenStateCompleted) {
+ if (method === "throw") {
+ throw arg;
+ }
+
+ return doneResult();
+ }
+
+ context.method = method;
+ context.arg = arg;
+
+ while (true) {
+ var delegate = context.delegate;
+
+ if (delegate) {
+ var delegateResult = maybeInvokeDelegate(delegate, context);
+
+ if (delegateResult) {
+ if (delegateResult === ContinueSentinel) continue;
+ return delegateResult;
+ }
+ }
+
+ if (context.method === "next") {
+ context.sent = context._sent = context.arg;
+ } else if (context.method === "throw") {
+ if (state === GenStateSuspendedStart) {
+ state = GenStateCompleted;
+ throw context.arg;
+ }
+
+ context.dispatchException(context.arg);
+ } else if (context.method === "return") {
+ context.abrupt("return", context.arg);
+ }
+
+ state = GenStateExecuting;
+ var record = tryCatch(innerFn, self, context);
+
+ if (record.type === "normal") {
+ state = context.done ? GenStateCompleted : GenStateSuspendedYield;
+
+ if (record.arg === ContinueSentinel) {
+ continue;
+ }
+
+ return {
+ value: record.arg,
+ done: context.done
+ };
+ } else if (record.type === "throw") {
+ state = GenStateCompleted;
+ context.method = "throw";
+ context.arg = record.arg;
+ }
+ }
+ };
+ }
+
+ function maybeInvokeDelegate(delegate, context) {
+ var method = delegate.iterator[context.method];
+
+ if (method === undefined) {
+ context.delegate = null;
+
+ if (context.method === "throw") {
+ if (delegate.iterator["return"]) {
+ context.method = "return";
+ context.arg = undefined;
+ maybeInvokeDelegate(delegate, context);
+
+ if (context.method === "throw") {
+ return ContinueSentinel;
+ }
+ }
+
+ context.method = "throw";
+ context.arg = new TypeError("The iterator does not provide a 'throw' method");
+ }
+
+ return ContinueSentinel;
+ }
+
+ var record = tryCatch(method, delegate.iterator, context.arg);
+
+ if (record.type === "throw") {
+ context.method = "throw";
+ context.arg = record.arg;
+ context.delegate = null;
+ return ContinueSentinel;
+ }
+
+ var info = record.arg;
+
+ if (!info) {
+ context.method = "throw";
+ context.arg = new TypeError("iterator result is not an object");
+ context.delegate = null;
+ return ContinueSentinel;
+ }
+
+ if (info.done) {
+ context[delegate.resultName] = info.value;
+ context.next = delegate.nextLoc;
+
+ if (context.method !== "return") {
+ context.method = "next";
+ context.arg = undefined;
+ }
+ } else {
+ return info;
+ }
+
+ context.delegate = null;
+ return ContinueSentinel;
+ }
+
+ defineIteratorMethods(Gp);
+ Gp[toStringTagSymbol] = "Generator";
+
+ Gp[iteratorSymbol] = function () {
+ return this;
+ };
+
+ Gp.toString = function () {
+ return "[object Generator]";
+ };
+
+ function pushTryEntry(locs) {
+ var entry = {
+ tryLoc: locs[0]
+ };
+
+ if (1 in locs) {
+ entry.catchLoc = locs[1];
+ }
+
+ if (2 in locs) {
+ entry.finallyLoc = locs[2];
+ entry.afterLoc = locs[3];
+ }
+
+ this.tryEntries.push(entry);
+ }
+
+ function resetTryEntry(entry) {
+ var record = entry.completion || {};
+ record.type = "normal";
+ delete record.arg;
+ entry.completion = record;
+ }
+
+ function Context(tryLocsList) {
+ this.tryEntries = [{
+ tryLoc: "root"
+ }];
+ tryLocsList.forEach(pushTryEntry, this);
+ this.reset(true);
+ }
+
+ exports.keys = function (object) {
+ var keys = [];
+
+ for (var key in object) {
+ keys.push(key);
+ }
+
+ keys.reverse();
+ return function next() {
+ while (keys.length) {
+ var key = keys.pop();
+
+ if (key in object) {
+ next.value = key;
+ next.done = false;
+ return next;
+ }
+ }
+
+ next.done = true;
+ return next;
+ };
+ };
+
+ function values(iterable) {
+ if (iterable) {
+ var iteratorMethod = iterable[iteratorSymbol];
+
+ if (iteratorMethod) {
+ return iteratorMethod.call(iterable);
+ }
+
+ if (typeof iterable.next === "function") {
+ return iterable;
+ }
+
+ if (!isNaN(iterable.length)) {
+ var i = -1,
+ next = function next() {
+ while (++i < iterable.length) {
+ if (hasOwn.call(iterable, i)) {
+ next.value = iterable[i];
+ next.done = false;
+ return next;
+ }
+ }
+
+ next.value = undefined;
+ next.done = true;
+ return next;
+ };
+
+ return next.next = next;
+ }
+ }
+
+ return {
+ next: doneResult
+ };
+ }
+
+ exports.values = values;
+
+ function doneResult() {
+ return {
+ value: undefined,
+ done: true
+ };
+ }
+
+ Context.prototype = {
+ constructor: Context,
+ reset: function reset(skipTempReset) {
+ this.prev = 0;
+ this.next = 0;
+ this.sent = this._sent = undefined;
+ this.done = false;
+ this.delegate = null;
+ this.method = "next";
+ this.arg = undefined;
+ this.tryEntries.forEach(resetTryEntry);
+
+ if (!skipTempReset) {
+ for (var name in this) {
+ if (name.charAt(0) === "t" && hasOwn.call(this, name) && !isNaN(+name.slice(1))) {
+ this[name] = undefined;
+ }
+ }
+ }
+ },
+ stop: function stop() {
+ this.done = true;
+ var rootEntry = this.tryEntries[0];
+ var rootRecord = rootEntry.completion;
+
+ if (rootRecord.type === "throw") {
+ throw rootRecord.arg;
+ }
+
+ return this.rval;
+ },
+ dispatchException: function dispatchException(exception) {
+ if (this.done) {
+ throw exception;
+ }
+
+ var context = this;
+
+ function handle(loc, caught) {
+ record.type = "throw";
+ record.arg = exception;
+ context.next = loc;
+
+ if (caught) {
+ context.method = "next";
+ context.arg = undefined;
+ }
+
+ return !!caught;
+ }
+
+ for (var i = this.tryEntries.length - 1; i >= 0; --i) {
+ var entry = this.tryEntries[i];
+ var record = entry.completion;
+
+ if (entry.tryLoc === "root") {
+ return handle("end");
+ }
+
+ if (entry.tryLoc <= this.prev) {
+ var hasCatch = hasOwn.call(entry, "catchLoc");
+ var hasFinally = hasOwn.call(entry, "finallyLoc");
+
+ if (hasCatch && hasFinally) {
+ if (this.prev < entry.catchLoc) {
+ return handle(entry.catchLoc, true);
+ } else if (this.prev < entry.finallyLoc) {
+ return handle(entry.finallyLoc);
+ }
+ } else if (hasCatch) {
+ if (this.prev < entry.catchLoc) {
+ return handle(entry.catchLoc, true);
+ }
+ } else if (hasFinally) {
+ if (this.prev < entry.finallyLoc) {
+ return handle(entry.finallyLoc);
+ }
+ } else {
+ throw new Error("try statement without catch or finally");
+ }
+ }
+ }
+ },
+ abrupt: function abrupt(type, arg) {
+ for (var i = this.tryEntries.length - 1; i >= 0; --i) {
+ var entry = this.tryEntries[i];
+
+ if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) {
+ var finallyEntry = entry;
+ break;
+ }
+ }
+
+ if (finallyEntry && (type === "break" || type === "continue") && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc) {
+ finallyEntry = null;
+ }
+
+ var record = finallyEntry ? finallyEntry.completion : {};
+ record.type = type;
+ record.arg = arg;
+
+ if (finallyEntry) {
+ this.method = "next";
+ this.next = finallyEntry.finallyLoc;
+ return ContinueSentinel;
+ }
+
+ return this.complete(record);
+ },
+ complete: function complete(record, afterLoc) {
+ if (record.type === "throw") {
+ throw record.arg;
+ }
+
+ if (record.type === "break" || record.type === "continue") {
+ this.next = record.arg;
+ } else if (record.type === "return") {
+ this.rval = this.arg = record.arg;
+ this.method = "return";
+ this.next = "end";
+ } else if (record.type === "normal" && afterLoc) {
+ this.next = afterLoc;
+ }
+
+ return ContinueSentinel;
+ },
+ finish: function finish(finallyLoc) {
+ for (var i = this.tryEntries.length - 1; i >= 0; --i) {
+ var entry = this.tryEntries[i];
+
+ if (entry.finallyLoc === finallyLoc) {
+ this.complete(entry.completion, entry.afterLoc);
+ resetTryEntry(entry);
+ return ContinueSentinel;
+ }
+ }
+ },
+ "catch": function _catch(tryLoc) {
+ for (var i = this.tryEntries.length - 1; i >= 0; --i) {
+ var entry = this.tryEntries[i];
+
+ if (entry.tryLoc === tryLoc) {
+ var record = entry.completion;
+
+ if (record.type === "throw") {
+ var thrown = record.arg;
+ resetTryEntry(entry);
+ }
+
+ return thrown;
+ }
+ }
+
+ throw new Error("illegal catch attempt");
+ },
+ delegateYield: function delegateYield(iterable, resultName, nextLoc) {
+ this.delegate = {
+ iterator: values(iterable),
+ resultName: resultName,
+ nextLoc: nextLoc
+ };
+
+ if (this.method === "next") {
+ this.arg = undefined;
+ }
+
+ return ContinueSentinel;
+ }
+ };
+ return exports;
+}(( false ? undefined : _typeof(module)) === "object" ? module.exports : {});
+
+try {
+ regeneratorRuntime = runtime;
+} catch (accidentalStrictMode) {
+ Function("r", "regeneratorRuntime = r")(runtime);
+}
+/* WEBPACK VAR INJECTION */}.call(this, __w_pdfjs_require__(150)(module)))
+
+/***/ }),
+/* 150 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (module) {
+ if (!module.webpackPolyfill) {
+ module.deprecate = function () {};
+
+ module.paths = [];
+ if (!module.children) module.children = [];
+ Object.defineProperty(module, "loaded", {
+ enumerable: true,
+ get: function get() {
+ return module.l;
+ }
+ });
+ Object.defineProperty(module, "id", {
+ enumerable: true,
+ get: function get() {
+ return module.i;
+ }
+ });
+ module.webpackPolyfill = 1;
+ }
+
+ return module;
+};
+
+/***/ }),
+/* 151 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.addLinkAttributes = addLinkAttributes;
+exports.getFilenameFromUrl = getFilenameFromUrl;
+exports.isFetchSupported = isFetchSupported;
+exports.isValidFetchUrl = isValidFetchUrl;
+exports.loadScript = loadScript;
+exports.deprecated = deprecated;
+exports.releaseImageResources = releaseImageResources;
+exports.PDFDateString = exports.DummyStatTimer = exports.StatTimer = exports.DOMSVGFactory = exports.DOMCMapReaderFactory = exports.DOMCanvasFactory = exports.DEFAULT_LINK_REL = exports.LinkTarget = exports.RenderingCancelledException = exports.PageViewport = void 0;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(148));
+
+var _util = __w_pdfjs_require__(1);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var DEFAULT_LINK_REL = 'noopener noreferrer nofollow';
+exports.DEFAULT_LINK_REL = DEFAULT_LINK_REL;
+var SVG_NS = 'http://www.w3.org/2000/svg';
+
+var DOMCanvasFactory =
+/*#__PURE__*/
+function () {
+ function DOMCanvasFactory() {
+ _classCallCheck(this, DOMCanvasFactory);
+ }
+
+ _createClass(DOMCanvasFactory, [{
+ key: "create",
+ value: function create(width, height) {
+ if (width <= 0 || height <= 0) {
+ throw new Error('Invalid canvas size');
+ }
+
+ var canvas = document.createElement('canvas');
+ var context = canvas.getContext('2d');
+ canvas.width = width;
+ canvas.height = height;
+ return {
+ canvas: canvas,
+ context: context
+ };
+ }
+ }, {
+ key: "reset",
+ value: function reset(canvasAndContext, width, height) {
+ if (!canvasAndContext.canvas) {
+ throw new Error('Canvas is not specified');
+ }
+
+ if (width <= 0 || height <= 0) {
+ throw new Error('Invalid canvas size');
+ }
+
+ canvasAndContext.canvas.width = width;
+ canvasAndContext.canvas.height = height;
+ }
+ }, {
+ key: "destroy",
+ value: function destroy(canvasAndContext) {
+ if (!canvasAndContext.canvas) {
+ throw new Error('Canvas is not specified');
+ }
+
+ canvasAndContext.canvas.width = 0;
+ canvasAndContext.canvas.height = 0;
+ canvasAndContext.canvas = null;
+ canvasAndContext.context = null;
+ }
+ }]);
+
+ return DOMCanvasFactory;
+}();
+
+exports.DOMCanvasFactory = DOMCanvasFactory;
+
+var DOMCMapReaderFactory =
+/*#__PURE__*/
+function () {
+ function DOMCMapReaderFactory(_ref) {
+ var _ref$baseUrl = _ref.baseUrl,
+ baseUrl = _ref$baseUrl === void 0 ? null : _ref$baseUrl,
+ _ref$isCompressed = _ref.isCompressed,
+ isCompressed = _ref$isCompressed === void 0 ? false : _ref$isCompressed;
+
+ _classCallCheck(this, DOMCMapReaderFactory);
+
+ this.baseUrl = baseUrl;
+ this.isCompressed = isCompressed;
+ }
+
+ _createClass(DOMCMapReaderFactory, [{
+ key: "fetch",
+ value: function (_fetch) {
+ function fetch(_x) {
+ return _fetch.apply(this, arguments);
+ }
+
+ fetch.toString = function () {
+ return _fetch.toString();
+ };
+
+ return fetch;
+ }(
+ /*#__PURE__*/
+ function () {
+ var _ref3 = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee2(_ref2) {
+ var _this = this;
+
+ var name, url, compressionType;
+ return _regenerator["default"].wrap(function _callee2$(_context2) {
+ while (1) {
+ switch (_context2.prev = _context2.next) {
+ case 0:
+ name = _ref2.name;
+
+ if (this.baseUrl) {
+ _context2.next = 3;
+ break;
+ }
+
+ throw new Error('The CMap "baseUrl" parameter must be specified, ensure that ' + 'the "cMapUrl" and "cMapPacked" API parameters are provided.');
+
+ case 3:
+ if (name) {
+ _context2.next = 5;
+ break;
+ }
+
+ throw new Error('CMap name must be specified.');
+
+ case 5:
+ url = this.baseUrl + name + (this.isCompressed ? '.bcmap' : '');
+ compressionType = this.isCompressed ? _util.CMapCompressionType.BINARY : _util.CMapCompressionType.NONE;
+
+ if (!(isFetchSupported() && isValidFetchUrl(url, document.baseURI))) {
+ _context2.next = 9;
+ break;
+ }
+
+ return _context2.abrupt("return", fetch(url).then(
+ /*#__PURE__*/
+ function () {
+ var _ref4 = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee(response) {
+ var cMapData;
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ if (response.ok) {
+ _context.next = 2;
+ break;
+ }
+
+ throw new Error(response.statusText);
+
+ case 2:
+ if (!_this.isCompressed) {
+ _context.next = 10;
+ break;
+ }
+
+ _context.t0 = Uint8Array;
+ _context.next = 6;
+ return response.arrayBuffer();
+
+ case 6:
+ _context.t1 = _context.sent;
+ cMapData = new _context.t0(_context.t1);
+ _context.next = 15;
+ break;
+
+ case 10:
+ _context.t2 = _util.stringToBytes;
+ _context.next = 13;
+ return response.text();
+
+ case 13:
+ _context.t3 = _context.sent;
+ cMapData = (0, _context.t2)(_context.t3);
+
+ case 15:
+ return _context.abrupt("return", {
+ cMapData: cMapData,
+ compressionType: compressionType
+ });
+
+ case 16:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee);
+ }));
+
+ return function (_x3) {
+ return _ref4.apply(this, arguments);
+ };
+ }())["catch"](function (reason) {
+ throw new Error("Unable to load ".concat(_this.isCompressed ? 'binary ' : '') + "CMap at: ".concat(url));
+ }));
+
+ case 9:
+ return _context2.abrupt("return", new Promise(function (resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.open('GET', url, true);
+
+ if (_this.isCompressed) {
+ request.responseType = 'arraybuffer';
+ }
+
+ request.onreadystatechange = function () {
+ if (request.readyState !== XMLHttpRequest.DONE) {
+ return;
+ }
+
+ if (request.status === 200 || request.status === 0) {
+ var cMapData;
+
+ if (_this.isCompressed && request.response) {
+ cMapData = new Uint8Array(request.response);
+ } else if (!_this.isCompressed && request.responseText) {
+ cMapData = (0, _util.stringToBytes)(request.responseText);
+ }
+
+ if (cMapData) {
+ resolve({
+ cMapData: cMapData,
+ compressionType: compressionType
+ });
+ return;
+ }
+ }
+
+ reject(new Error(request.statusText));
+ };
+
+ request.send(null);
+ })["catch"](function (reason) {
+ throw new Error("Unable to load ".concat(_this.isCompressed ? 'binary ' : '') + "CMap at: ".concat(url));
+ }));
+
+ case 10:
+ case "end":
+ return _context2.stop();
+ }
+ }
+ }, _callee2, this);
+ }));
+
+ return function (_x2) {
+ return _ref3.apply(this, arguments);
+ };
+ }())
+ }]);
+
+ return DOMCMapReaderFactory;
+}();
+
+exports.DOMCMapReaderFactory = DOMCMapReaderFactory;
+
+var DOMSVGFactory =
+/*#__PURE__*/
+function () {
+ function DOMSVGFactory() {
+ _classCallCheck(this, DOMSVGFactory);
+ }
+
+ _createClass(DOMSVGFactory, [{
+ key: "create",
+ value: function create(width, height) {
+ (0, _util.assert)(width > 0 && height > 0, 'Invalid SVG dimensions');
+ var svg = document.createElementNS(SVG_NS, 'svg:svg');
+ svg.setAttribute('version', '1.1');
+ svg.setAttribute('width', width + 'px');
+ svg.setAttribute('height', height + 'px');
+ svg.setAttribute('preserveAspectRatio', 'none');
+ svg.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
+ return svg;
+ }
+ }, {
+ key: "createElement",
+ value: function createElement(type) {
+ (0, _util.assert)(typeof type === 'string', 'Invalid SVG element type');
+ return document.createElementNS(SVG_NS, type);
+ }
+ }]);
+
+ return DOMSVGFactory;
+}();
+
+exports.DOMSVGFactory = DOMSVGFactory;
+
+var PageViewport =
+/*#__PURE__*/
+function () {
+ function PageViewport(_ref5) {
+ var viewBox = _ref5.viewBox,
+ scale = _ref5.scale,
+ rotation = _ref5.rotation,
+ _ref5$offsetX = _ref5.offsetX,
+ offsetX = _ref5$offsetX === void 0 ? 0 : _ref5$offsetX,
+ _ref5$offsetY = _ref5.offsetY,
+ offsetY = _ref5$offsetY === void 0 ? 0 : _ref5$offsetY,
+ _ref5$dontFlip = _ref5.dontFlip,
+ dontFlip = _ref5$dontFlip === void 0 ? false : _ref5$dontFlip;
+
+ _classCallCheck(this, PageViewport);
+
+ this.viewBox = viewBox;
+ this.scale = scale;
+ this.rotation = rotation;
+ this.offsetX = offsetX;
+ this.offsetY = offsetY;
+ var centerX = (viewBox[2] + viewBox[0]) / 2;
+ var centerY = (viewBox[3] + viewBox[1]) / 2;
+ var rotateA, rotateB, rotateC, rotateD;
+ rotation = rotation % 360;
+ rotation = rotation < 0 ? rotation + 360 : rotation;
+
+ switch (rotation) {
+ case 180:
+ rotateA = -1;
+ rotateB = 0;
+ rotateC = 0;
+ rotateD = 1;
+ break;
+
+ case 90:
+ rotateA = 0;
+ rotateB = 1;
+ rotateC = 1;
+ rotateD = 0;
+ break;
+
+ case 270:
+ rotateA = 0;
+ rotateB = -1;
+ rotateC = -1;
+ rotateD = 0;
+ break;
+
+ default:
+ rotateA = 1;
+ rotateB = 0;
+ rotateC = 0;
+ rotateD = -1;
+ break;
+ }
+
+ if (dontFlip) {
+ rotateC = -rotateC;
+ rotateD = -rotateD;
+ }
+
+ var offsetCanvasX, offsetCanvasY;
+ var width, height;
+
+ if (rotateA === 0) {
+ offsetCanvasX = Math.abs(centerY - viewBox[1]) * scale + offsetX;
+ offsetCanvasY = Math.abs(centerX - viewBox[0]) * scale + offsetY;
+ width = Math.abs(viewBox[3] - viewBox[1]) * scale;
+ height = Math.abs(viewBox[2] - viewBox[0]) * scale;
+ } else {
+ offsetCanvasX = Math.abs(centerX - viewBox[0]) * scale + offsetX;
+ offsetCanvasY = Math.abs(centerY - viewBox[1]) * scale + offsetY;
+ width = Math.abs(viewBox[2] - viewBox[0]) * scale;
+ height = Math.abs(viewBox[3] - viewBox[1]) * scale;
+ }
+
+ this.transform = [rotateA * scale, rotateB * scale, rotateC * scale, rotateD * scale, offsetCanvasX - rotateA * scale * centerX - rotateC * scale * centerY, offsetCanvasY - rotateB * scale * centerX - rotateD * scale * centerY];
+ this.width = width;
+ this.height = height;
+ }
+
+ _createClass(PageViewport, [{
+ key: "clone",
+ value: function clone() {
+ var _ref6 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
+ _ref6$scale = _ref6.scale,
+ scale = _ref6$scale === void 0 ? this.scale : _ref6$scale,
+ _ref6$rotation = _ref6.rotation,
+ rotation = _ref6$rotation === void 0 ? this.rotation : _ref6$rotation,
+ _ref6$dontFlip = _ref6.dontFlip,
+ dontFlip = _ref6$dontFlip === void 0 ? false : _ref6$dontFlip;
+
+ return new PageViewport({
+ viewBox: this.viewBox.slice(),
+ scale: scale,
+ rotation: rotation,
+ offsetX: this.offsetX,
+ offsetY: this.offsetY,
+ dontFlip: dontFlip
+ });
+ }
+ }, {
+ key: "convertToViewportPoint",
+ value: function convertToViewportPoint(x, y) {
+ return _util.Util.applyTransform([x, y], this.transform);
+ }
+ }, {
+ key: "convertToViewportRectangle",
+ value: function convertToViewportRectangle(rect) {
+ var topLeft = _util.Util.applyTransform([rect[0], rect[1]], this.transform);
+
+ var bottomRight = _util.Util.applyTransform([rect[2], rect[3]], this.transform);
+
+ return [topLeft[0], topLeft[1], bottomRight[0], bottomRight[1]];
+ }
+ }, {
+ key: "convertToPdfPoint",
+ value: function convertToPdfPoint(x, y) {
+ return _util.Util.applyInverseTransform([x, y], this.transform);
+ }
+ }]);
+
+ return PageViewport;
+}();
+
+exports.PageViewport = PageViewport;
+
+var RenderingCancelledException = function RenderingCancelledException() {
+ function RenderingCancelledException(msg, type) {
+ this.message = msg;
+ this.type = type;
+ }
+
+ RenderingCancelledException.prototype = new Error();
+ RenderingCancelledException.prototype.name = 'RenderingCancelledException';
+ RenderingCancelledException.constructor = RenderingCancelledException;
+ return RenderingCancelledException;
+}();
+
+exports.RenderingCancelledException = RenderingCancelledException;
+var LinkTarget = {
+ NONE: 0,
+ SELF: 1,
+ BLANK: 2,
+ PARENT: 3,
+ TOP: 4
+};
+exports.LinkTarget = LinkTarget;
+var LinkTargetStringMap = ['', '_self', '_blank', '_parent', '_top'];
+
+function addLinkAttributes(link) {
+ var _ref7 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
+ url = _ref7.url,
+ target = _ref7.target,
+ rel = _ref7.rel;
+
+ link.href = link.title = url ? (0, _util.removeNullCharacters)(url) : '';
+
+ if (url) {
+ var LinkTargetValues = Object.values(LinkTarget);
+ var targetIndex = LinkTargetValues.includes(target) ? target : LinkTarget.NONE;
+ link.target = LinkTargetStringMap[targetIndex];
+ link.rel = typeof rel === 'string' ? rel : DEFAULT_LINK_REL;
+ }
+}
+
+function getFilenameFromUrl(url) {
+ var anchor = url.indexOf('#');
+ var query = url.indexOf('?');
+ var end = Math.min(anchor > 0 ? anchor : url.length, query > 0 ? query : url.length);
+ return url.substring(url.lastIndexOf('/', end) + 1, end);
+}
+
+var StatTimer =
+/*#__PURE__*/
+function () {
+ function StatTimer() {
+ var enable = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
+
+ _classCallCheck(this, StatTimer);
+
+ this.enabled = !!enable;
+ this.started = Object.create(null);
+ this.times = [];
+ }
+
+ _createClass(StatTimer, [{
+ key: "time",
+ value: function time(name) {
+ if (!this.enabled) {
+ return;
+ }
+
+ if (name in this.started) {
+ (0, _util.warn)('Timer is already running for ' + name);
+ }
+
+ this.started[name] = Date.now();
+ }
+ }, {
+ key: "timeEnd",
+ value: function timeEnd(name) {
+ if (!this.enabled) {
+ return;
+ }
+
+ if (!(name in this.started)) {
+ (0, _util.warn)('Timer has not been started for ' + name);
+ }
+
+ this.times.push({
+ 'name': name,
+ 'start': this.started[name],
+ 'end': Date.now()
+ });
+ delete this.started[name];
+ }
+ }, {
+ key: "toString",
+ value: function toString() {
+ var out = '',
+ longest = 0;
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = this.times[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var time = _step.value;
+ var name = time.name;
+
+ if (name.length > longest) {
+ longest = name.length;
+ }
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator["return"] != null) {
+ _iterator["return"]();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+
+ var _iteratorNormalCompletion2 = true;
+ var _didIteratorError2 = false;
+ var _iteratorError2 = undefined;
+
+ try {
+ for (var _iterator2 = this.times[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
+ var _time = _step2.value;
+ var duration = _time.end - _time.start;
+ out += "".concat(_time.name.padEnd(longest), " ").concat(duration, "ms\n");
+ }
+ } catch (err) {
+ _didIteratorError2 = true;
+ _iteratorError2 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) {
+ _iterator2["return"]();
+ }
+ } finally {
+ if (_didIteratorError2) {
+ throw _iteratorError2;
+ }
+ }
+ }
+
+ return out;
+ }
+ }]);
+
+ return StatTimer;
+}();
+
+exports.StatTimer = StatTimer;
+
+var DummyStatTimer =
+/*#__PURE__*/
+function () {
+ function DummyStatTimer() {
+ _classCallCheck(this, DummyStatTimer);
+
+ (0, _util.unreachable)('Cannot initialize DummyStatTimer.');
+ }
+
+ _createClass(DummyStatTimer, null, [{
+ key: "time",
+ value: function time(name) {}
+ }, {
+ key: "timeEnd",
+ value: function timeEnd(name) {}
+ }, {
+ key: "toString",
+ value: function toString() {
+ return '';
+ }
+ }]);
+
+ return DummyStatTimer;
+}();
+
+exports.DummyStatTimer = DummyStatTimer;
+
+function isFetchSupported() {
+ return typeof fetch !== 'undefined' && typeof Response !== 'undefined' && 'body' in Response.prototype && typeof ReadableStream !== 'undefined';
+}
+
+function isValidFetchUrl(url, baseUrl) {
+ try {
+ var _ref8 = baseUrl ? new _util.URL(url, baseUrl) : new _util.URL(url),
+ protocol = _ref8.protocol;
+
+ return protocol === 'http:' || protocol === 'https:';
+ } catch (ex) {
+ return false;
+ }
+}
+
+function loadScript(src) {
+ return new Promise(function (resolve, reject) {
+ var script = document.createElement('script');
+ script.src = src;
+ script.onload = resolve;
+
+ script.onerror = function () {
+ reject(new Error("Cannot load script at: ".concat(script.src)));
+ };
+
+ (document.head || document.documentElement).appendChild(script);
+ });
+}
+
+function deprecated(details) {
+ console.log('Deprecated API usage: ' + details);
+}
+
+function releaseImageResources(img) {
+ (0, _util.assert)(img instanceof Image, 'Invalid `img` parameter.');
+ var url = img.src;
+
+ if (typeof url === 'string' && url.startsWith('blob:') && _util.URL.revokeObjectURL) {
+ _util.URL.revokeObjectURL(url);
+ }
+
+ img.removeAttribute('src');
+}
+
+var pdfDateStringRegex;
+
+var PDFDateString =
+/*#__PURE__*/
+function () {
+ function PDFDateString() {
+ _classCallCheck(this, PDFDateString);
+ }
+
+ _createClass(PDFDateString, null, [{
+ key: "toDateObject",
+ value: function toDateObject(input) {
+ if (!input || !(0, _util.isString)(input)) {
+ return null;
+ }
+
+ if (!pdfDateStringRegex) {
+ pdfDateStringRegex = new RegExp('^D:' + '(\\d{4})' + '(\\d{2})?' + '(\\d{2})?' + '(\\d{2})?' + '(\\d{2})?' + '(\\d{2})?' + '([Z|+|-])?' + '(\\d{2})?' + '\'?' + '(\\d{2})?' + '\'?');
+ }
+
+ var matches = pdfDateStringRegex.exec(input);
+
+ if (!matches) {
+ return null;
+ }
+
+ var year = parseInt(matches[1], 10);
+ var month = parseInt(matches[2], 10);
+ month = month >= 1 && month <= 12 ? month - 1 : 0;
+ var day = parseInt(matches[3], 10);
+ day = day >= 1 && day <= 31 ? day : 1;
+ var hour = parseInt(matches[4], 10);
+ hour = hour >= 0 && hour <= 23 ? hour : 0;
+ var minute = parseInt(matches[5], 10);
+ minute = minute >= 0 && minute <= 59 ? minute : 0;
+ var second = parseInt(matches[6], 10);
+ second = second >= 0 && second <= 59 ? second : 0;
+ var universalTimeRelation = matches[7] || 'Z';
+ var offsetHour = parseInt(matches[8], 10);
+ offsetHour = offsetHour >= 0 && offsetHour <= 23 ? offsetHour : 0;
+ var offsetMinute = parseInt(matches[9], 10) || 0;
+ offsetMinute = offsetMinute >= 0 && offsetMinute <= 59 ? offsetMinute : 0;
+
+ if (universalTimeRelation === '-') {
+ hour += offsetHour;
+ minute += offsetMinute;
+ } else if (universalTimeRelation === '+') {
+ hour -= offsetHour;
+ minute -= offsetMinute;
+ }
+
+ return new Date(Date.UTC(year, month, day, hour, minute, second));
+ }
+ }]);
+
+ return PDFDateString;
+}();
+
+exports.PDFDateString = PDFDateString;
+
+/***/ }),
+/* 152 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.FontLoader = exports.FontFaceObject = void 0;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(148));
+
+var _util = __w_pdfjs_require__(1);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
+
+function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
+
+function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
+
+function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var BaseFontLoader =
+/*#__PURE__*/
+function () {
+ function BaseFontLoader(_ref) {
+ var docId = _ref.docId,
+ onUnsupportedFeature = _ref.onUnsupportedFeature;
+
+ _classCallCheck(this, BaseFontLoader);
+
+ if (this.constructor === BaseFontLoader) {
+ (0, _util.unreachable)('Cannot initialize BaseFontLoader.');
+ }
+
+ this.docId = docId;
+ this._onUnsupportedFeature = onUnsupportedFeature;
+ this.nativeFontFaces = [];
+ this.styleElement = null;
+ }
+
+ _createClass(BaseFontLoader, [{
+ key: "addNativeFontFace",
+ value: function addNativeFontFace(nativeFontFace) {
+ this.nativeFontFaces.push(nativeFontFace);
+ document.fonts.add(nativeFontFace);
+ }
+ }, {
+ key: "insertRule",
+ value: function insertRule(rule) {
+ var styleElement = this.styleElement;
+
+ if (!styleElement) {
+ styleElement = this.styleElement = document.createElement('style');
+ styleElement.id = "PDFJS_FONT_STYLE_TAG_".concat(this.docId);
+ document.documentElement.getElementsByTagName('head')[0].appendChild(styleElement);
+ }
+
+ var styleSheet = styleElement.sheet;
+ styleSheet.insertRule(rule, styleSheet.cssRules.length);
+ }
+ }, {
+ key: "clear",
+ value: function clear() {
+ this.nativeFontFaces.forEach(function (nativeFontFace) {
+ document.fonts["delete"](nativeFontFace);
+ });
+ this.nativeFontFaces.length = 0;
+
+ if (this.styleElement) {
+ this.styleElement.remove();
+ this.styleElement = null;
+ }
+ }
+ }, {
+ key: "bind",
+ value: function () {
+ var _bind = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee(font) {
+ var _this = this;
+
+ var nativeFontFace, rule;
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ if (!(font.attached || font.missingFile)) {
+ _context.next = 2;
+ break;
+ }
+
+ return _context.abrupt("return", undefined);
+
+ case 2:
+ font.attached = true;
+
+ if (!this.isFontLoadingAPISupported) {
+ _context.next = 19;
+ break;
+ }
+
+ nativeFontFace = font.createNativeFontFace();
+
+ if (!nativeFontFace) {
+ _context.next = 18;
+ break;
+ }
+
+ this.addNativeFontFace(nativeFontFace);
+ _context.prev = 7;
+ _context.next = 10;
+ return nativeFontFace.loaded;
+
+ case 10:
+ _context.next = 18;
+ break;
+
+ case 12:
+ _context.prev = 12;
+ _context.t0 = _context["catch"](7);
+
+ this._onUnsupportedFeature({
+ featureId: _util.UNSUPPORTED_FEATURES.font
+ });
+
+ (0, _util.warn)("Failed to load font '".concat(nativeFontFace.family, "': '").concat(_context.t0, "'."));
+ font.disableFontFace = true;
+ throw _context.t0;
+
+ case 18:
+ return _context.abrupt("return", undefined);
+
+ case 19:
+ rule = font.createFontFaceRule();
+
+ if (!rule) {
+ _context.next = 25;
+ break;
+ }
+
+ this.insertRule(rule);
+
+ if (!this.isSyncFontLoadingSupported) {
+ _context.next = 24;
+ break;
+ }
+
+ return _context.abrupt("return", undefined);
+
+ case 24:
+ return _context.abrupt("return", new Promise(function (resolve) {
+ var request = _this._queueLoadingCallback(resolve);
+
+ _this._prepareFontLoadEvent([rule], [font], request);
+ }));
+
+ case 25:
+ return _context.abrupt("return", undefined);
+
+ case 26:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee, this, [[7, 12]]);
+ }));
+
+ function bind(_x) {
+ return _bind.apply(this, arguments);
+ }
+
+ return bind;
+ }()
+ }, {
+ key: "_queueLoadingCallback",
+ value: function _queueLoadingCallback(callback) {
+ (0, _util.unreachable)('Abstract method `_queueLoadingCallback`.');
+ }
+ }, {
+ key: "_prepareFontLoadEvent",
+ value: function _prepareFontLoadEvent(rules, fontsToLoad, request) {
+ (0, _util.unreachable)('Abstract method `_prepareFontLoadEvent`.');
+ }
+ }, {
+ key: "isFontLoadingAPISupported",
+ get: function get() {
+ (0, _util.unreachable)('Abstract method `isFontLoadingAPISupported`.');
+ }
+ }, {
+ key: "isSyncFontLoadingSupported",
+ get: function get() {
+ (0, _util.unreachable)('Abstract method `isSyncFontLoadingSupported`.');
+ }
+ }, {
+ key: "_loadTestFont",
+ get: function get() {
+ (0, _util.unreachable)('Abstract method `_loadTestFont`.');
+ }
+ }]);
+
+ return BaseFontLoader;
+}();
+
+var FontLoader;
+exports.FontLoader = FontLoader;
+{
+ exports.FontLoader = FontLoader =
+ /*#__PURE__*/
+ function (_BaseFontLoader) {
+ _inherits(GenericFontLoader, _BaseFontLoader);
+
+ function GenericFontLoader(docId) {
+ var _this2;
+
+ _classCallCheck(this, GenericFontLoader);
+
+ _this2 = _possibleConstructorReturn(this, _getPrototypeOf(GenericFontLoader).call(this, docId));
+ _this2.loadingContext = {
+ requests: [],
+ nextRequestId: 0
+ };
+ _this2.loadTestFontId = 0;
+ return _this2;
+ }
+
+ _createClass(GenericFontLoader, [{
+ key: "_queueLoadingCallback",
+ value: function _queueLoadingCallback(callback) {
+ function completeRequest() {
+ (0, _util.assert)(!request.done, 'completeRequest() cannot be called twice.');
+ request.done = true;
+
+ while (context.requests.length > 0 && context.requests[0].done) {
+ var otherRequest = context.requests.shift();
+ setTimeout(otherRequest.callback, 0);
+ }
+ }
+
+ var context = this.loadingContext;
+ var request = {
+ id: "pdfjs-font-loading-".concat(context.nextRequestId++),
+ done: false,
+ complete: completeRequest,
+ callback: callback
+ };
+ context.requests.push(request);
+ return request;
+ }
+ }, {
+ key: "_prepareFontLoadEvent",
+ value: function _prepareFontLoadEvent(rules, fonts, request) {
+ function int32(data, offset) {
+ return data.charCodeAt(offset) << 24 | data.charCodeAt(offset + 1) << 16 | data.charCodeAt(offset + 2) << 8 | data.charCodeAt(offset + 3) & 0xff;
+ }
+
+ function spliceString(s, offset, remove, insert) {
+ var chunk1 = s.substring(0, offset);
+ var chunk2 = s.substring(offset + remove);
+ return chunk1 + insert + chunk2;
+ }
+
+ var i, ii;
+ var canvas = document.createElement('canvas');
+ canvas.width = 1;
+ canvas.height = 1;
+ var ctx = canvas.getContext('2d');
+ var called = 0;
+
+ function isFontReady(name, callback) {
+ called++;
+
+ if (called > 30) {
+ (0, _util.warn)('Load test font never loaded.');
+ callback();
+ return;
+ }
+
+ ctx.font = '30px ' + name;
+ ctx.fillText('.', 0, 20);
+ var imageData = ctx.getImageData(0, 0, 1, 1);
+
+ if (imageData.data[3] > 0) {
+ callback();
+ return;
+ }
+
+ setTimeout(isFontReady.bind(null, name, callback));
+ }
+
+ var loadTestFontId = "lt".concat(Date.now()).concat(this.loadTestFontId++);
+ var data = this._loadTestFont;
+ var COMMENT_OFFSET = 976;
+ data = spliceString(data, COMMENT_OFFSET, loadTestFontId.length, loadTestFontId);
+ var CFF_CHECKSUM_OFFSET = 16;
+ var XXXX_VALUE = 0x58585858;
+ var checksum = int32(data, CFF_CHECKSUM_OFFSET);
+
+ for (i = 0, ii = loadTestFontId.length - 3; i < ii; i += 4) {
+ checksum = checksum - XXXX_VALUE + int32(loadTestFontId, i) | 0;
+ }
+
+ if (i < loadTestFontId.length) {
+ checksum = checksum - XXXX_VALUE + int32(loadTestFontId + 'XXX', i) | 0;
+ }
+
+ data = spliceString(data, CFF_CHECKSUM_OFFSET, 4, (0, _util.string32)(checksum));
+ var url = "url(data:font/opentype;base64,".concat(btoa(data), ");");
+ var rule = "@font-face {font-family:\"".concat(loadTestFontId, "\";src:").concat(url, "}");
+ this.insertRule(rule);
+ var names = [];
+
+ for (i = 0, ii = fonts.length; i < ii; i++) {
+ names.push(fonts[i].loadedName);
+ }
+
+ names.push(loadTestFontId);
+ var div = document.createElement('div');
+ div.setAttribute('style', 'visibility: hidden;' + 'width: 10px; height: 10px;' + 'position: absolute; top: 0px; left: 0px;');
+
+ for (i = 0, ii = names.length; i < ii; ++i) {
+ var span = document.createElement('span');
+ span.textContent = 'Hi';
+ span.style.fontFamily = names[i];
+ div.appendChild(span);
+ }
+
+ document.body.appendChild(div);
+ isFontReady(loadTestFontId, function () {
+ document.body.removeChild(div);
+ request.complete();
+ });
+ }
+ }, {
+ key: "isFontLoadingAPISupported",
+ get: function get() {
+ var supported = typeof document !== 'undefined' && !!document.fonts;
+
+ if (supported && typeof navigator !== 'undefined') {
+ var m = /Mozilla\/5.0.*?rv:(\d+).*? Gecko/.exec(navigator.userAgent);
+
+ if (m && m[1] < 63) {
+ supported = false;
+ }
+ }
+
+ return (0, _util.shadow)(this, 'isFontLoadingAPISupported', supported);
+ }
+ }, {
+ key: "isSyncFontLoadingSupported",
+ get: function get() {
+ var supported = false;
+
+ if (typeof navigator === 'undefined') {
+ supported = true;
+ } else {
+ var m = /Mozilla\/5.0.*?rv:(\d+).*? Gecko/.exec(navigator.userAgent);
+
+ if (m && m[1] >= 14) {
+ supported = true;
+ }
+ }
+
+ return (0, _util.shadow)(this, 'isSyncFontLoadingSupported', supported);
+ }
+ }, {
+ key: "_loadTestFont",
+ get: function get() {
+ var getLoadTestFont = function getLoadTestFont() {
+ return atob('T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQA' + 'FQAABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAA' + 'ALwAAAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgA' + 'AAAGbmFtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1' + 'AAsD6AAAAADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD' + '6AAAAAAD6AABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACM' + 'AooCvAAAAeAAMQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4D' + 'IP84AFoDIQAAAAAAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAA' + 'AAEAAQAAAAEAAAAAAAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUA' + 'AQAAAAEAAAAAAAYAAQAAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgAB' + 'AAMAAQQJAAMAAgABAAMAAQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABY' + 'AAAAAAAAAwAAAAMAAAAcAAEAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAA' + 'AC7////TAAEAAAAAAAABBgAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + 'AAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAA' + 'AAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgcA/gXBIwMAYuL+nz5tQXkD5j3CBLnEQAC' + 'AQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYAAABAQAADwACAQEEE/t3' + 'Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQAAAAAAAABAAAAAMmJbzEAAAAAzgTj' + 'FQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAgABAAAAAAAAAAAD6AAAAAAAAA==');
+ };
+
+ return (0, _util.shadow)(this, '_loadTestFont', getLoadTestFont());
+ }
+ }]);
+
+ return GenericFontLoader;
+ }(BaseFontLoader);
+}
+var IsEvalSupportedCached = {
+ get value() {
+ return (0, _util.shadow)(this, 'value', (0, _util.isEvalSupported)());
+ }
+
+};
+
+var FontFaceObject =
+/*#__PURE__*/
+function () {
+ function FontFaceObject(translatedData, _ref2) {
+ var _ref2$isEvalSupported = _ref2.isEvalSupported,
+ isEvalSupported = _ref2$isEvalSupported === void 0 ? true : _ref2$isEvalSupported,
+ _ref2$disableFontFace = _ref2.disableFontFace,
+ disableFontFace = _ref2$disableFontFace === void 0 ? false : _ref2$disableFontFace,
+ _ref2$ignoreErrors = _ref2.ignoreErrors,
+ ignoreErrors = _ref2$ignoreErrors === void 0 ? false : _ref2$ignoreErrors,
+ _ref2$onUnsupportedFe = _ref2.onUnsupportedFeature,
+ onUnsupportedFeature = _ref2$onUnsupportedFe === void 0 ? null : _ref2$onUnsupportedFe,
+ _ref2$fontRegistry = _ref2.fontRegistry,
+ fontRegistry = _ref2$fontRegistry === void 0 ? null : _ref2$fontRegistry;
+
+ _classCallCheck(this, FontFaceObject);
+
+ this.compiledGlyphs = Object.create(null);
+
+ for (var i in translatedData) {
+ this[i] = translatedData[i];
+ }
+
+ this.isEvalSupported = isEvalSupported !== false;
+ this.disableFontFace = disableFontFace === true;
+ this.ignoreErrors = ignoreErrors === true;
+ this._onUnsupportedFeature = onUnsupportedFeature;
+ this.fontRegistry = fontRegistry;
+ }
+
+ _createClass(FontFaceObject, [{
+ key: "createNativeFontFace",
+ value: function createNativeFontFace() {
+ if (!this.data || this.disableFontFace) {
+ return null;
+ }
+
+ var nativeFontFace = new FontFace(this.loadedName, this.data, {});
+
+ if (this.fontRegistry) {
+ this.fontRegistry.registerFont(this);
+ }
+
+ return nativeFontFace;
+ }
+ }, {
+ key: "createFontFaceRule",
+ value: function createFontFaceRule() {
+ if (!this.data || this.disableFontFace) {
+ return null;
+ }
+
+ var data = (0, _util.bytesToString)(new Uint8Array(this.data));
+ var url = "url(data:".concat(this.mimetype, ";base64,").concat(btoa(data), ");");
+ var rule = "@font-face {font-family:\"".concat(this.loadedName, "\";src:").concat(url, "}");
+
+ if (this.fontRegistry) {
+ this.fontRegistry.registerFont(this, url);
+ }
+
+ return rule;
+ }
+ }, {
+ key: "getPathGenerator",
+ value: function getPathGenerator(objs, character) {
+ if (this.compiledGlyphs[character] !== undefined) {
+ return this.compiledGlyphs[character];
+ }
+
+ var cmds, current;
+
+ try {
+ cmds = objs.get(this.loadedName + '_path_' + character);
+ } catch (ex) {
+ if (!this.ignoreErrors) {
+ throw ex;
+ }
+
+ if (this._onUnsupportedFeature) {
+ this._onUnsupportedFeature({
+ featureId: _util.UNSUPPORTED_FEATURES.font
+ });
+ }
+
+ (0, _util.warn)("getPathGenerator - ignoring character: \"".concat(ex, "\"."));
+ return this.compiledGlyphs[character] = function (c, size) {};
+ }
+
+ if (this.isEvalSupported && IsEvalSupportedCached.value) {
+ var args,
+ js = '';
+
+ for (var i = 0, ii = cmds.length; i < ii; i++) {
+ current = cmds[i];
+
+ if (current.args !== undefined) {
+ args = current.args.join(',');
+ } else {
+ args = '';
+ }
+
+ js += 'c.' + current.cmd + '(' + args + ');\n';
+ }
+
+ return this.compiledGlyphs[character] = new Function('c', 'size', js);
+ }
+
+ return this.compiledGlyphs[character] = function (c, size) {
+ for (var _i = 0, _ii = cmds.length; _i < _ii; _i++) {
+ current = cmds[_i];
+
+ if (current.cmd === 'scale') {
+ current.args = [size, -size];
+ }
+
+ c[current.cmd].apply(c, current.args);
+ }
+ };
+ }
+ }]);
+
+ return FontFaceObject;
+}();
+
+exports.FontFaceObject = FontFaceObject;
+
+/***/ }),
+/* 153 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var compatibilityParams = Object.create(null);
+{
+ var isNodeJS = __w_pdfjs_require__(4);
+
+ var userAgent = typeof navigator !== 'undefined' && navigator.userAgent || '';
+ var isIE = /Trident/.test(userAgent);
+ var isIOSChrome = /CriOS/.test(userAgent);
+
+ (function checkOnBlobSupport() {
+ if (isIE || isIOSChrome) {
+ compatibilityParams.disableCreateObjectURL = true;
+ }
+ })();
+
+ (function checkFontFaceAndImage() {
+ if (isNodeJS()) {
+ compatibilityParams.disableFontFace = true;
+ compatibilityParams.nativeImageDecoderSupport = 'none';
+ }
+ })();
+}
+exports.apiCompatibilityParams = Object.freeze(compatibilityParams);
+
+/***/ }),
+/* 154 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.CanvasGraphics = void 0;
+
+var _util = __w_pdfjs_require__(1);
+
+var _pattern_helper = __w_pdfjs_require__(155);
+
+var MIN_FONT_SIZE = 16;
+var MAX_FONT_SIZE = 100;
+var MAX_GROUP_SIZE = 4096;
+var MIN_WIDTH_FACTOR = 0.65;
+var COMPILE_TYPE3_GLYPHS = true;
+var MAX_SIZE_TO_COMPILE = 1000;
+var FULL_CHUNK_HEIGHT = 16;
+var IsLittleEndianCached = {
+ get value() {
+ return (0, _util.shadow)(IsLittleEndianCached, 'value', (0, _util.isLittleEndian)());
+ }
+
+};
+
+function addContextCurrentTransform(ctx) {
+ if (!ctx.mozCurrentTransform) {
+ ctx._originalSave = ctx.save;
+ ctx._originalRestore = ctx.restore;
+ ctx._originalRotate = ctx.rotate;
+ ctx._originalScale = ctx.scale;
+ ctx._originalTranslate = ctx.translate;
+ ctx._originalTransform = ctx.transform;
+ ctx._originalSetTransform = ctx.setTransform;
+ ctx._transformMatrix = ctx._transformMatrix || [1, 0, 0, 1, 0, 0];
+ ctx._transformStack = [];
+ Object.defineProperty(ctx, 'mozCurrentTransform', {
+ get: function getCurrentTransform() {
+ return this._transformMatrix;
+ }
+ });
+ Object.defineProperty(ctx, 'mozCurrentTransformInverse', {
+ get: function getCurrentTransformInverse() {
+ var m = this._transformMatrix;
+ var a = m[0],
+ b = m[1],
+ c = m[2],
+ d = m[3],
+ e = m[4],
+ f = m[5];
+ var ad_bc = a * d - b * c;
+ var bc_ad = b * c - a * d;
+ return [d / ad_bc, b / bc_ad, c / bc_ad, a / ad_bc, (d * e - c * f) / bc_ad, (b * e - a * f) / ad_bc];
+ }
+ });
+
+ ctx.save = function ctxSave() {
+ var old = this._transformMatrix;
+
+ this._transformStack.push(old);
+
+ this._transformMatrix = old.slice(0, 6);
+
+ this._originalSave();
+ };
+
+ ctx.restore = function ctxRestore() {
+ var prev = this._transformStack.pop();
+
+ if (prev) {
+ this._transformMatrix = prev;
+
+ this._originalRestore();
+ }
+ };
+
+ ctx.translate = function ctxTranslate(x, y) {
+ var m = this._transformMatrix;
+ m[4] = m[0] * x + m[2] * y + m[4];
+ m[5] = m[1] * x + m[3] * y + m[5];
+
+ this._originalTranslate(x, y);
+ };
+
+ ctx.scale = function ctxScale(x, y) {
+ var m = this._transformMatrix;
+ m[0] = m[0] * x;
+ m[1] = m[1] * x;
+ m[2] = m[2] * y;
+ m[3] = m[3] * y;
+
+ this._originalScale(x, y);
+ };
+
+ ctx.transform = function ctxTransform(a, b, c, d, e, f) {
+ var m = this._transformMatrix;
+ this._transformMatrix = [m[0] * a + m[2] * b, m[1] * a + m[3] * b, m[0] * c + m[2] * d, m[1] * c + m[3] * d, m[0] * e + m[2] * f + m[4], m[1] * e + m[3] * f + m[5]];
+
+ ctx._originalTransform(a, b, c, d, e, f);
+ };
+
+ ctx.setTransform = function ctxSetTransform(a, b, c, d, e, f) {
+ this._transformMatrix = [a, b, c, d, e, f];
+
+ ctx._originalSetTransform(a, b, c, d, e, f);
+ };
+
+ ctx.rotate = function ctxRotate(angle) {
+ var cosValue = Math.cos(angle);
+ var sinValue = Math.sin(angle);
+ var m = this._transformMatrix;
+ this._transformMatrix = [m[0] * cosValue + m[2] * sinValue, m[1] * cosValue + m[3] * sinValue, m[0] * -sinValue + m[2] * cosValue, m[1] * -sinValue + m[3] * cosValue, m[4], m[5]];
+
+ this._originalRotate(angle);
+ };
+ }
+}
+
+var CachedCanvases = function CachedCanvasesClosure() {
+ function CachedCanvases(canvasFactory) {
+ this.canvasFactory = canvasFactory;
+ this.cache = Object.create(null);
+ }
+
+ CachedCanvases.prototype = {
+ getCanvas: function CachedCanvases_getCanvas(id, width, height, trackTransform) {
+ var canvasEntry;
+
+ if (this.cache[id] !== undefined) {
+ canvasEntry = this.cache[id];
+ this.canvasFactory.reset(canvasEntry, width, height);
+ canvasEntry.context.setTransform(1, 0, 0, 1, 0, 0);
+ } else {
+ canvasEntry = this.canvasFactory.create(width, height);
+ this.cache[id] = canvasEntry;
+ }
+
+ if (trackTransform) {
+ addContextCurrentTransform(canvasEntry.context);
+ }
+
+ return canvasEntry;
+ },
+ clear: function clear() {
+ for (var id in this.cache) {
+ var canvasEntry = this.cache[id];
+ this.canvasFactory.destroy(canvasEntry);
+ delete this.cache[id];
+ }
+ }
+ };
+ return CachedCanvases;
+}();
+
+function compileType3Glyph(imgData) {
+ var POINT_TO_PROCESS_LIMIT = 1000;
+ var width = imgData.width,
+ height = imgData.height;
+ var i,
+ j,
+ j0,
+ width1 = width + 1;
+ var points = new Uint8Array(width1 * (height + 1));
+ var POINT_TYPES = new Uint8Array([0, 2, 4, 0, 1, 0, 5, 4, 8, 10, 0, 8, 0, 2, 1, 0]);
+ var lineSize = width + 7 & ~7,
+ data0 = imgData.data;
+ var data = new Uint8Array(lineSize * height),
+ pos = 0,
+ ii;
+
+ for (i = 0, ii = data0.length; i < ii; i++) {
+ var mask = 128,
+ elem = data0[i];
+
+ while (mask > 0) {
+ data[pos++] = elem & mask ? 0 : 255;
+ mask >>= 1;
+ }
+ }
+
+ var count = 0;
+ pos = 0;
+
+ if (data[pos] !== 0) {
+ points[0] = 1;
+ ++count;
+ }
+
+ for (j = 1; j < width; j++) {
+ if (data[pos] !== data[pos + 1]) {
+ points[j] = data[pos] ? 2 : 1;
+ ++count;
+ }
+
+ pos++;
+ }
+
+ if (data[pos] !== 0) {
+ points[j] = 2;
+ ++count;
+ }
+
+ for (i = 1; i < height; i++) {
+ pos = i * lineSize;
+ j0 = i * width1;
+
+ if (data[pos - lineSize] !== data[pos]) {
+ points[j0] = data[pos] ? 1 : 8;
+ ++count;
+ }
+
+ var sum = (data[pos] ? 4 : 0) + (data[pos - lineSize] ? 8 : 0);
+
+ for (j = 1; j < width; j++) {
+ sum = (sum >> 2) + (data[pos + 1] ? 4 : 0) + (data[pos - lineSize + 1] ? 8 : 0);
+
+ if (POINT_TYPES[sum]) {
+ points[j0 + j] = POINT_TYPES[sum];
+ ++count;
+ }
+
+ pos++;
+ }
+
+ if (data[pos - lineSize] !== data[pos]) {
+ points[j0 + j] = data[pos] ? 2 : 4;
+ ++count;
+ }
+
+ if (count > POINT_TO_PROCESS_LIMIT) {
+ return null;
+ }
+ }
+
+ pos = lineSize * (height - 1);
+ j0 = i * width1;
+
+ if (data[pos] !== 0) {
+ points[j0] = 8;
+ ++count;
+ }
+
+ for (j = 1; j < width; j++) {
+ if (data[pos] !== data[pos + 1]) {
+ points[j0 + j] = data[pos] ? 4 : 8;
+ ++count;
+ }
+
+ pos++;
+ }
+
+ if (data[pos] !== 0) {
+ points[j0 + j] = 4;
+ ++count;
+ }
+
+ if (count > POINT_TO_PROCESS_LIMIT) {
+ return null;
+ }
+
+ var steps = new Int32Array([0, width1, -1, 0, -width1, 0, 0, 0, 1]);
+ var outlines = [];
+
+ for (i = 0; count && i <= height; i++) {
+ var p = i * width1;
+ var end = p + width;
+
+ while (p < end && !points[p]) {
+ p++;
+ }
+
+ if (p === end) {
+ continue;
+ }
+
+ var coords = [p % width1, i];
+ var type = points[p],
+ p0 = p,
+ pp;
+
+ do {
+ var step = steps[type];
+
+ do {
+ p += step;
+ } while (!points[p]);
+
+ pp = points[p];
+
+ if (pp !== 5 && pp !== 10) {
+ type = pp;
+ points[p] = 0;
+ } else {
+ type = pp & 0x33 * type >> 4;
+ points[p] &= type >> 2 | type << 2;
+ }
+
+ coords.push(p % width1);
+ coords.push(p / width1 | 0);
+
+ if (!points[p]) {
+ --count;
+ }
+ } while (p0 !== p);
+
+ outlines.push(coords);
+ --i;
+ }
+
+ var drawOutline = function drawOutline(c) {
+ c.save();
+ c.scale(1 / width, -1 / height);
+ c.translate(0, -height);
+ c.beginPath();
+
+ for (var i = 0, ii = outlines.length; i < ii; i++) {
+ var o = outlines[i];
+ c.moveTo(o[0], o[1]);
+
+ for (var j = 2, jj = o.length; j < jj; j += 2) {
+ c.lineTo(o[j], o[j + 1]);
+ }
+ }
+
+ c.fill();
+ c.beginPath();
+ c.restore();
+ };
+
+ return drawOutline;
+}
+
+var CanvasExtraState = function CanvasExtraStateClosure() {
+ function CanvasExtraState() {
+ this.alphaIsShape = false;
+ this.fontSize = 0;
+ this.fontSizeScale = 1;
+ this.textMatrix = _util.IDENTITY_MATRIX;
+ this.textMatrixScale = 1;
+ this.fontMatrix = _util.FONT_IDENTITY_MATRIX;
+ this.leading = 0;
+ this.x = 0;
+ this.y = 0;
+ this.lineX = 0;
+ this.lineY = 0;
+ this.charSpacing = 0;
+ this.wordSpacing = 0;
+ this.textHScale = 1;
+ this.textRenderingMode = _util.TextRenderingMode.FILL;
+ this.textRise = 0;
+ this.fillColor = '#000000';
+ this.strokeColor = '#000000';
+ this.patternFill = false;
+ this.fillAlpha = 1;
+ this.strokeAlpha = 1;
+ this.lineWidth = 1;
+ this.activeSMask = null;
+ this.resumeSMaskCtx = null;
+ }
+
+ CanvasExtraState.prototype = {
+ clone: function CanvasExtraState_clone() {
+ return Object.create(this);
+ },
+ setCurrentPoint: function CanvasExtraState_setCurrentPoint(x, y) {
+ this.x = x;
+ this.y = y;
+ }
+ };
+ return CanvasExtraState;
+}();
+
+var CanvasGraphics = function CanvasGraphicsClosure() {
+ var EXECUTION_TIME = 15;
+ var EXECUTION_STEPS = 10;
+
+ function CanvasGraphics(canvasCtx, commonObjs, objs, canvasFactory, webGLContext, imageLayer) {
+ this.ctx = canvasCtx;
+ this.current = new CanvasExtraState();
+ this.stateStack = [];
+ this.pendingClip = null;
+ this.pendingEOFill = false;
+ this.res = null;
+ this.xobjs = null;
+ this.commonObjs = commonObjs;
+ this.objs = objs;
+ this.canvasFactory = canvasFactory;
+ this.webGLContext = webGLContext;
+ this.imageLayer = imageLayer;
+ this.groupStack = [];
+ this.processingType3 = null;
+ this.baseTransform = null;
+ this.baseTransformStack = [];
+ this.groupLevel = 0;
+ this.smaskStack = [];
+ this.smaskCounter = 0;
+ this.tempSMask = null;
+ this.cachedCanvases = new CachedCanvases(this.canvasFactory);
+
+ if (canvasCtx) {
+ addContextCurrentTransform(canvasCtx);
+ }
+
+ this._cachedGetSinglePixelWidth = null;
+ }
+
+ function putBinaryImageData(ctx, imgData) {
+ if (typeof ImageData !== 'undefined' && imgData instanceof ImageData) {
+ ctx.putImageData(imgData, 0, 0);
+ return;
+ }
+
+ var height = imgData.height,
+ width = imgData.width;
+ var partialChunkHeight = height % FULL_CHUNK_HEIGHT;
+ var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT;
+ var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1;
+ var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT);
+ var srcPos = 0,
+ destPos;
+ var src = imgData.data;
+ var dest = chunkImgData.data;
+ var i, j, thisChunkHeight, elemsInThisChunk;
+
+ if (imgData.kind === _util.ImageKind.GRAYSCALE_1BPP) {
+ var srcLength = src.byteLength;
+ var dest32 = new Uint32Array(dest.buffer, 0, dest.byteLength >> 2);
+ var dest32DataLength = dest32.length;
+ var fullSrcDiff = width + 7 >> 3;
+ var white = 0xFFFFFFFF;
+ var black = IsLittleEndianCached.value ? 0xFF000000 : 0x000000FF;
+
+ for (i = 0; i < totalChunks; i++) {
+ thisChunkHeight = i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight;
+ destPos = 0;
+
+ for (j = 0; j < thisChunkHeight; j++) {
+ var srcDiff = srcLength - srcPos;
+ var k = 0;
+ var kEnd = srcDiff > fullSrcDiff ? width : srcDiff * 8 - 7;
+ var kEndUnrolled = kEnd & ~7;
+ var mask = 0;
+ var srcByte = 0;
+
+ for (; k < kEndUnrolled; k += 8) {
+ srcByte = src[srcPos++];
+ dest32[destPos++] = srcByte & 128 ? white : black;
+ dest32[destPos++] = srcByte & 64 ? white : black;
+ dest32[destPos++] = srcByte & 32 ? white : black;
+ dest32[destPos++] = srcByte & 16 ? white : black;
+ dest32[destPos++] = srcByte & 8 ? white : black;
+ dest32[destPos++] = srcByte & 4 ? white : black;
+ dest32[destPos++] = srcByte & 2 ? white : black;
+ dest32[destPos++] = srcByte & 1 ? white : black;
+ }
+
+ for (; k < kEnd; k++) {
+ if (mask === 0) {
+ srcByte = src[srcPos++];
+ mask = 128;
+ }
+
+ dest32[destPos++] = srcByte & mask ? white : black;
+ mask >>= 1;
+ }
+ }
+
+ while (destPos < dest32DataLength) {
+ dest32[destPos++] = 0;
+ }
+
+ ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
+ }
+ } else if (imgData.kind === _util.ImageKind.RGBA_32BPP) {
+ j = 0;
+ elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4;
+
+ for (i = 0; i < fullChunks; i++) {
+ dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
+ srcPos += elemsInThisChunk;
+ ctx.putImageData(chunkImgData, 0, j);
+ j += FULL_CHUNK_HEIGHT;
+ }
+
+ if (i < totalChunks) {
+ elemsInThisChunk = width * partialChunkHeight * 4;
+ dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
+ ctx.putImageData(chunkImgData, 0, j);
+ }
+ } else if (imgData.kind === _util.ImageKind.RGB_24BPP) {
+ thisChunkHeight = FULL_CHUNK_HEIGHT;
+ elemsInThisChunk = width * thisChunkHeight;
+
+ for (i = 0; i < totalChunks; i++) {
+ if (i >= fullChunks) {
+ thisChunkHeight = partialChunkHeight;
+ elemsInThisChunk = width * thisChunkHeight;
+ }
+
+ destPos = 0;
+
+ for (j = elemsInThisChunk; j--;) {
+ dest[destPos++] = src[srcPos++];
+ dest[destPos++] = src[srcPos++];
+ dest[destPos++] = src[srcPos++];
+ dest[destPos++] = 255;
+ }
+
+ ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
+ }
+ } else {
+ throw new Error("bad image kind: ".concat(imgData.kind));
+ }
+ }
+
+ function putBinaryImageMask(ctx, imgData) {
+ var height = imgData.height,
+ width = imgData.width;
+ var partialChunkHeight = height % FULL_CHUNK_HEIGHT;
+ var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT;
+ var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1;
+ var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT);
+ var srcPos = 0;
+ var src = imgData.data;
+ var dest = chunkImgData.data;
+
+ for (var i = 0; i < totalChunks; i++) {
+ var thisChunkHeight = i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight;
+ var destPos = 3;
+
+ for (var j = 0; j < thisChunkHeight; j++) {
+ var mask = 0;
+
+ for (var k = 0; k < width; k++) {
+ if (!mask) {
+ var elem = src[srcPos++];
+ mask = 128;
+ }
+
+ dest[destPos] = elem & mask ? 0 : 255;
+ destPos += 4;
+ mask >>= 1;
+ }
+ }
+
+ ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
+ }
+ }
+
+ function copyCtxState(sourceCtx, destCtx) {
+ var properties = ['strokeStyle', 'fillStyle', 'fillRule', 'globalAlpha', 'lineWidth', 'lineCap', 'lineJoin', 'miterLimit', 'globalCompositeOperation', 'font'];
+
+ for (var i = 0, ii = properties.length; i < ii; i++) {
+ var property = properties[i];
+
+ if (sourceCtx[property] !== undefined) {
+ destCtx[property] = sourceCtx[property];
+ }
+ }
+
+ if (sourceCtx.setLineDash !== undefined) {
+ destCtx.setLineDash(sourceCtx.getLineDash());
+ destCtx.lineDashOffset = sourceCtx.lineDashOffset;
+ }
+ }
+
+ function resetCtxToDefault(ctx) {
+ ctx.strokeStyle = '#000000';
+ ctx.fillStyle = '#000000';
+ ctx.fillRule = 'nonzero';
+ ctx.globalAlpha = 1;
+ ctx.lineWidth = 1;
+ ctx.lineCap = 'butt';
+ ctx.lineJoin = 'miter';
+ ctx.miterLimit = 10;
+ ctx.globalCompositeOperation = 'source-over';
+ ctx.font = '10px sans-serif';
+
+ if (ctx.setLineDash !== undefined) {
+ ctx.setLineDash([]);
+ ctx.lineDashOffset = 0;
+ }
+ }
+
+ function composeSMaskBackdrop(bytes, r0, g0, b0) {
+ var length = bytes.length;
+
+ for (var i = 3; i < length; i += 4) {
+ var alpha = bytes[i];
+
+ if (alpha === 0) {
+ bytes[i - 3] = r0;
+ bytes[i - 2] = g0;
+ bytes[i - 1] = b0;
+ } else if (alpha < 255) {
+ var alpha_ = 255 - alpha;
+ bytes[i - 3] = bytes[i - 3] * alpha + r0 * alpha_ >> 8;
+ bytes[i - 2] = bytes[i - 2] * alpha + g0 * alpha_ >> 8;
+ bytes[i - 1] = bytes[i - 1] * alpha + b0 * alpha_ >> 8;
+ }
+ }
+ }
+
+ function composeSMaskAlpha(maskData, layerData, transferMap) {
+ var length = maskData.length;
+ var scale = 1 / 255;
+
+ for (var i = 3; i < length; i += 4) {
+ var alpha = transferMap ? transferMap[maskData[i]] : maskData[i];
+ layerData[i] = layerData[i] * alpha * scale | 0;
+ }
+ }
+
+ function composeSMaskLuminosity(maskData, layerData, transferMap) {
+ var length = maskData.length;
+
+ for (var i = 3; i < length; i += 4) {
+ var y = maskData[i - 3] * 77 + maskData[i - 2] * 152 + maskData[i - 1] * 28;
+ layerData[i] = transferMap ? layerData[i] * transferMap[y >> 8] >> 8 : layerData[i] * y >> 16;
+ }
+ }
+
+ function genericComposeSMask(maskCtx, layerCtx, width, height, subtype, backdrop, transferMap) {
+ var hasBackdrop = !!backdrop;
+ var r0 = hasBackdrop ? backdrop[0] : 0;
+ var g0 = hasBackdrop ? backdrop[1] : 0;
+ var b0 = hasBackdrop ? backdrop[2] : 0;
+ var composeFn;
+
+ if (subtype === 'Luminosity') {
+ composeFn = composeSMaskLuminosity;
+ } else {
+ composeFn = composeSMaskAlpha;
+ }
+
+ var PIXELS_TO_PROCESS = 1048576;
+ var chunkSize = Math.min(height, Math.ceil(PIXELS_TO_PROCESS / width));
+
+ for (var row = 0; row < height; row += chunkSize) {
+ var chunkHeight = Math.min(chunkSize, height - row);
+ var maskData = maskCtx.getImageData(0, row, width, chunkHeight);
+ var layerData = layerCtx.getImageData(0, row, width, chunkHeight);
+
+ if (hasBackdrop) {
+ composeSMaskBackdrop(maskData.data, r0, g0, b0);
+ }
+
+ composeFn(maskData.data, layerData.data, transferMap);
+ maskCtx.putImageData(layerData, 0, row);
+ }
+ }
+
+ function composeSMask(ctx, smask, layerCtx, webGLContext) {
+ var mask = smask.canvas;
+ var maskCtx = smask.context;
+ ctx.setTransform(smask.scaleX, 0, 0, smask.scaleY, smask.offsetX, smask.offsetY);
+ var backdrop = smask.backdrop || null;
+
+ if (!smask.transferMap && webGLContext.isEnabled) {
+ var composed = webGLContext.composeSMask({
+ layer: layerCtx.canvas,
+ mask: mask,
+ properties: {
+ subtype: smask.subtype,
+ backdrop: backdrop
+ }
+ });
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
+ ctx.drawImage(composed, smask.offsetX, smask.offsetY);
+ return;
+ }
+
+ genericComposeSMask(maskCtx, layerCtx, mask.width, mask.height, smask.subtype, backdrop, smask.transferMap);
+ ctx.drawImage(mask, 0, 0);
+ }
+
+ var LINE_CAP_STYLES = ['butt', 'round', 'square'];
+ var LINE_JOIN_STYLES = ['miter', 'round', 'bevel'];
+ var NORMAL_CLIP = {};
+ var EO_CLIP = {};
+ CanvasGraphics.prototype = {
+ beginDrawing: function beginDrawing(_ref) {
+ var transform = _ref.transform,
+ viewport = _ref.viewport,
+ _ref$transparency = _ref.transparency,
+ transparency = _ref$transparency === void 0 ? false : _ref$transparency,
+ _ref$background = _ref.background,
+ background = _ref$background === void 0 ? null : _ref$background;
+ var width = this.ctx.canvas.width;
+ var height = this.ctx.canvas.height;
+ this.ctx.save();
+ this.ctx.fillStyle = background || 'rgb(255, 255, 255)';
+ this.ctx.fillRect(0, 0, width, height);
+ this.ctx.restore();
+
+ if (transparency) {
+ var transparentCanvas = this.cachedCanvases.getCanvas('transparent', width, height, true);
+ this.compositeCtx = this.ctx;
+ this.transparentCanvas = transparentCanvas.canvas;
+ this.ctx = transparentCanvas.context;
+ this.ctx.save();
+ this.ctx.transform.apply(this.ctx, this.compositeCtx.mozCurrentTransform);
+ }
+
+ this.ctx.save();
+ resetCtxToDefault(this.ctx);
+
+ if (transform) {
+ this.ctx.transform.apply(this.ctx, transform);
+ }
+
+ this.ctx.transform.apply(this.ctx, viewport.transform);
+ this.baseTransform = this.ctx.mozCurrentTransform.slice();
+
+ if (this.imageLayer) {
+ this.imageLayer.beginLayout();
+ }
+ },
+ executeOperatorList: function CanvasGraphics_executeOperatorList(operatorList, executionStartIdx, continueCallback, stepper) {
+ var argsArray = operatorList.argsArray;
+ var fnArray = operatorList.fnArray;
+ var i = executionStartIdx || 0;
+ var argsArrayLen = argsArray.length;
+
+ if (argsArrayLen === i) {
+ return i;
+ }
+
+ var chunkOperations = argsArrayLen - i > EXECUTION_STEPS && typeof continueCallback === 'function';
+ var endTime = chunkOperations ? Date.now() + EXECUTION_TIME : 0;
+ var steps = 0;
+ var commonObjs = this.commonObjs;
+ var objs = this.objs;
+ var fnId;
+
+ while (true) {
+ if (stepper !== undefined && i === stepper.nextBreakPoint) {
+ stepper.breakIt(i, continueCallback);
+ return i;
+ }
+
+ fnId = fnArray[i];
+
+ if (fnId !== _util.OPS.dependency) {
+ this[fnId].apply(this, argsArray[i]);
+ } else {
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = argsArray[i][Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var depObjId = _step.value;
+ var objsPool = depObjId.startsWith('g_') ? commonObjs : objs;
+
+ if (!objsPool.has(depObjId)) {
+ objsPool.get(depObjId, continueCallback);
+ return i;
+ }
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator["return"] != null) {
+ _iterator["return"]();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+ }
+
+ i++;
+
+ if (i === argsArrayLen) {
+ return i;
+ }
+
+ if (chunkOperations && ++steps > EXECUTION_STEPS) {
+ if (Date.now() > endTime) {
+ continueCallback();
+ return i;
+ }
+
+ steps = 0;
+ }
+ }
+ },
+ endDrawing: function CanvasGraphics_endDrawing() {
+ if (this.current.activeSMask !== null) {
+ this.endSMaskGroup();
+ }
+
+ this.ctx.restore();
+
+ if (this.transparentCanvas) {
+ this.ctx = this.compositeCtx;
+ this.ctx.save();
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0);
+ this.ctx.drawImage(this.transparentCanvas, 0, 0);
+ this.ctx.restore();
+ this.transparentCanvas = null;
+ }
+
+ this.cachedCanvases.clear();
+ this.webGLContext.clear();
+
+ if (this.imageLayer) {
+ this.imageLayer.endLayout();
+ }
+ },
+ setLineWidth: function CanvasGraphics_setLineWidth(width) {
+ this.current.lineWidth = width;
+ this.ctx.lineWidth = width;
+ },
+ setLineCap: function CanvasGraphics_setLineCap(style) {
+ this.ctx.lineCap = LINE_CAP_STYLES[style];
+ },
+ setLineJoin: function CanvasGraphics_setLineJoin(style) {
+ this.ctx.lineJoin = LINE_JOIN_STYLES[style];
+ },
+ setMiterLimit: function CanvasGraphics_setMiterLimit(limit) {
+ this.ctx.miterLimit = limit;
+ },
+ setDash: function CanvasGraphics_setDash(dashArray, dashPhase) {
+ var ctx = this.ctx;
+
+ if (ctx.setLineDash !== undefined) {
+ ctx.setLineDash(dashArray);
+ ctx.lineDashOffset = dashPhase;
+ }
+ },
+ setRenderingIntent: function setRenderingIntent(intent) {},
+ setFlatness: function setFlatness(flatness) {},
+ setGState: function CanvasGraphics_setGState(states) {
+ for (var i = 0, ii = states.length; i < ii; i++) {
+ var state = states[i];
+ var key = state[0];
+ var value = state[1];
+
+ switch (key) {
+ case 'LW':
+ this.setLineWidth(value);
+ break;
+
+ case 'LC':
+ this.setLineCap(value);
+ break;
+
+ case 'LJ':
+ this.setLineJoin(value);
+ break;
+
+ case 'ML':
+ this.setMiterLimit(value);
+ break;
+
+ case 'D':
+ this.setDash(value[0], value[1]);
+ break;
+
+ case 'RI':
+ this.setRenderingIntent(value);
+ break;
+
+ case 'FL':
+ this.setFlatness(value);
+ break;
+
+ case 'Font':
+ this.setFont(value[0], value[1]);
+ break;
+
+ case 'CA':
+ this.current.strokeAlpha = state[1];
+ break;
+
+ case 'ca':
+ this.current.fillAlpha = state[1];
+ this.ctx.globalAlpha = state[1];
+ break;
+
+ case 'BM':
+ this.ctx.globalCompositeOperation = value;
+ break;
+
+ case 'SMask':
+ if (this.current.activeSMask) {
+ if (this.stateStack.length > 0 && this.stateStack[this.stateStack.length - 1].activeSMask === this.current.activeSMask) {
+ this.suspendSMaskGroup();
+ } else {
+ this.endSMaskGroup();
+ }
+ }
+
+ this.current.activeSMask = value ? this.tempSMask : null;
+
+ if (this.current.activeSMask) {
+ this.beginSMaskGroup();
+ }
+
+ this.tempSMask = null;
+ break;
+ }
+ }
+ },
+ beginSMaskGroup: function CanvasGraphics_beginSMaskGroup() {
+ var activeSMask = this.current.activeSMask;
+ var drawnWidth = activeSMask.canvas.width;
+ var drawnHeight = activeSMask.canvas.height;
+ var cacheId = 'smaskGroupAt' + this.groupLevel;
+ var scratchCanvas = this.cachedCanvases.getCanvas(cacheId, drawnWidth, drawnHeight, true);
+ var currentCtx = this.ctx;
+ var currentTransform = currentCtx.mozCurrentTransform;
+ this.ctx.save();
+ var groupCtx = scratchCanvas.context;
+ groupCtx.scale(1 / activeSMask.scaleX, 1 / activeSMask.scaleY);
+ groupCtx.translate(-activeSMask.offsetX, -activeSMask.offsetY);
+ groupCtx.transform.apply(groupCtx, currentTransform);
+ activeSMask.startTransformInverse = groupCtx.mozCurrentTransformInverse;
+ copyCtxState(currentCtx, groupCtx);
+ this.ctx = groupCtx;
+ this.setGState([['BM', 'source-over'], ['ca', 1], ['CA', 1]]);
+ this.groupStack.push(currentCtx);
+ this.groupLevel++;
+ },
+ suspendSMaskGroup: function CanvasGraphics_endSMaskGroup() {
+ var groupCtx = this.ctx;
+ this.groupLevel--;
+ this.ctx = this.groupStack.pop();
+ composeSMask(this.ctx, this.current.activeSMask, groupCtx, this.webGLContext);
+ this.ctx.restore();
+ this.ctx.save();
+ copyCtxState(groupCtx, this.ctx);
+ this.current.resumeSMaskCtx = groupCtx;
+
+ var deltaTransform = _util.Util.transform(this.current.activeSMask.startTransformInverse, groupCtx.mozCurrentTransform);
+
+ this.ctx.transform.apply(this.ctx, deltaTransform);
+ groupCtx.save();
+ groupCtx.setTransform(1, 0, 0, 1, 0, 0);
+ groupCtx.clearRect(0, 0, groupCtx.canvas.width, groupCtx.canvas.height);
+ groupCtx.restore();
+ },
+ resumeSMaskGroup: function CanvasGraphics_endSMaskGroup() {
+ var groupCtx = this.current.resumeSMaskCtx;
+ var currentCtx = this.ctx;
+ this.ctx = groupCtx;
+ this.groupStack.push(currentCtx);
+ this.groupLevel++;
+ },
+ endSMaskGroup: function CanvasGraphics_endSMaskGroup() {
+ var groupCtx = this.ctx;
+ this.groupLevel--;
+ this.ctx = this.groupStack.pop();
+ composeSMask(this.ctx, this.current.activeSMask, groupCtx, this.webGLContext);
+ this.ctx.restore();
+ copyCtxState(groupCtx, this.ctx);
+
+ var deltaTransform = _util.Util.transform(this.current.activeSMask.startTransformInverse, groupCtx.mozCurrentTransform);
+
+ this.ctx.transform.apply(this.ctx, deltaTransform);
+ },
+ save: function CanvasGraphics_save() {
+ this.ctx.save();
+ var old = this.current;
+ this.stateStack.push(old);
+ this.current = old.clone();
+ this.current.resumeSMaskCtx = null;
+ },
+ restore: function CanvasGraphics_restore() {
+ if (this.current.resumeSMaskCtx) {
+ this.resumeSMaskGroup();
+ }
+
+ if (this.current.activeSMask !== null && (this.stateStack.length === 0 || this.stateStack[this.stateStack.length - 1].activeSMask !== this.current.activeSMask)) {
+ this.endSMaskGroup();
+ }
+
+ if (this.stateStack.length !== 0) {
+ this.current = this.stateStack.pop();
+ this.ctx.restore();
+ this.pendingClip = null;
+ this._cachedGetSinglePixelWidth = null;
+ }
+ },
+ transform: function CanvasGraphics_transform(a, b, c, d, e, f) {
+ this.ctx.transform(a, b, c, d, e, f);
+ this._cachedGetSinglePixelWidth = null;
+ },
+ constructPath: function CanvasGraphics_constructPath(ops, args) {
+ var ctx = this.ctx;
+ var current = this.current;
+ var x = current.x,
+ y = current.y;
+
+ for (var i = 0, j = 0, ii = ops.length; i < ii; i++) {
+ switch (ops[i] | 0) {
+ case _util.OPS.rectangle:
+ x = args[j++];
+ y = args[j++];
+ var width = args[j++];
+ var height = args[j++];
+
+ if (width === 0) {
+ width = this.getSinglePixelWidth();
+ }
+
+ if (height === 0) {
+ height = this.getSinglePixelWidth();
+ }
+
+ var xw = x + width;
+ var yh = y + height;
+ this.ctx.moveTo(x, y);
+ this.ctx.lineTo(xw, y);
+ this.ctx.lineTo(xw, yh);
+ this.ctx.lineTo(x, yh);
+ this.ctx.lineTo(x, y);
+ this.ctx.closePath();
+ break;
+
+ case _util.OPS.moveTo:
+ x = args[j++];
+ y = args[j++];
+ ctx.moveTo(x, y);
+ break;
+
+ case _util.OPS.lineTo:
+ x = args[j++];
+ y = args[j++];
+ ctx.lineTo(x, y);
+ break;
+
+ case _util.OPS.curveTo:
+ x = args[j + 4];
+ y = args[j + 5];
+ ctx.bezierCurveTo(args[j], args[j + 1], args[j + 2], args[j + 3], x, y);
+ j += 6;
+ break;
+
+ case _util.OPS.curveTo2:
+ ctx.bezierCurveTo(x, y, args[j], args[j + 1], args[j + 2], args[j + 3]);
+ x = args[j + 2];
+ y = args[j + 3];
+ j += 4;
+ break;
+
+ case _util.OPS.curveTo3:
+ x = args[j + 2];
+ y = args[j + 3];
+ ctx.bezierCurveTo(args[j], args[j + 1], x, y, x, y);
+ j += 4;
+ break;
+
+ case _util.OPS.closePath:
+ ctx.closePath();
+ break;
+ }
+ }
+
+ current.setCurrentPoint(x, y);
+ },
+ closePath: function CanvasGraphics_closePath() {
+ this.ctx.closePath();
+ },
+ stroke: function CanvasGraphics_stroke(consumePath) {
+ consumePath = typeof consumePath !== 'undefined' ? consumePath : true;
+ var ctx = this.ctx;
+ var strokeColor = this.current.strokeColor;
+ ctx.lineWidth = Math.max(this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, this.current.lineWidth);
+ ctx.globalAlpha = this.current.strokeAlpha;
+
+ if (strokeColor && strokeColor.hasOwnProperty('type') && strokeColor.type === 'Pattern') {
+ ctx.save();
+ ctx.strokeStyle = strokeColor.getPattern(ctx, this);
+ ctx.stroke();
+ ctx.restore();
+ } else {
+ ctx.stroke();
+ }
+
+ if (consumePath) {
+ this.consumePath();
+ }
+
+ ctx.globalAlpha = this.current.fillAlpha;
+ },
+ closeStroke: function CanvasGraphics_closeStroke() {
+ this.closePath();
+ this.stroke();
+ },
+ fill: function CanvasGraphics_fill(consumePath) {
+ consumePath = typeof consumePath !== 'undefined' ? consumePath : true;
+ var ctx = this.ctx;
+ var fillColor = this.current.fillColor;
+ var isPatternFill = this.current.patternFill;
+ var needRestore = false;
+
+ if (isPatternFill) {
+ ctx.save();
+
+ if (this.baseTransform) {
+ ctx.setTransform.apply(ctx, this.baseTransform);
+ }
+
+ ctx.fillStyle = fillColor.getPattern(ctx, this);
+ needRestore = true;
+ }
+
+ if (this.pendingEOFill) {
+ ctx.fill('evenodd');
+ this.pendingEOFill = false;
+ } else {
+ ctx.fill();
+ }
+
+ if (needRestore) {
+ ctx.restore();
+ }
+
+ if (consumePath) {
+ this.consumePath();
+ }
+ },
+ eoFill: function CanvasGraphics_eoFill() {
+ this.pendingEOFill = true;
+ this.fill();
+ },
+ fillStroke: function CanvasGraphics_fillStroke() {
+ this.fill(false);
+ this.stroke(false);
+ this.consumePath();
+ },
+ eoFillStroke: function CanvasGraphics_eoFillStroke() {
+ this.pendingEOFill = true;
+ this.fillStroke();
+ },
+ closeFillStroke: function CanvasGraphics_closeFillStroke() {
+ this.closePath();
+ this.fillStroke();
+ },
+ closeEOFillStroke: function CanvasGraphics_closeEOFillStroke() {
+ this.pendingEOFill = true;
+ this.closePath();
+ this.fillStroke();
+ },
+ endPath: function CanvasGraphics_endPath() {
+ this.consumePath();
+ },
+ clip: function CanvasGraphics_clip() {
+ this.pendingClip = NORMAL_CLIP;
+ },
+ eoClip: function CanvasGraphics_eoClip() {
+ this.pendingClip = EO_CLIP;
+ },
+ beginText: function CanvasGraphics_beginText() {
+ this.current.textMatrix = _util.IDENTITY_MATRIX;
+ this.current.textMatrixScale = 1;
+ this.current.x = this.current.lineX = 0;
+ this.current.y = this.current.lineY = 0;
+ },
+ endText: function CanvasGraphics_endText() {
+ var paths = this.pendingTextPaths;
+ var ctx = this.ctx;
+
+ if (paths === undefined) {
+ ctx.beginPath();
+ return;
+ }
+
+ ctx.save();
+ ctx.beginPath();
+
+ for (var i = 0; i < paths.length; i++) {
+ var path = paths[i];
+ ctx.setTransform.apply(ctx, path.transform);
+ ctx.translate(path.x, path.y);
+ path.addToPath(ctx, path.fontSize);
+ }
+
+ ctx.restore();
+ ctx.clip();
+ ctx.beginPath();
+ delete this.pendingTextPaths;
+ },
+ setCharSpacing: function CanvasGraphics_setCharSpacing(spacing) {
+ this.current.charSpacing = spacing;
+ },
+ setWordSpacing: function CanvasGraphics_setWordSpacing(spacing) {
+ this.current.wordSpacing = spacing;
+ },
+ setHScale: function CanvasGraphics_setHScale(scale) {
+ this.current.textHScale = scale / 100;
+ },
+ setLeading: function CanvasGraphics_setLeading(leading) {
+ this.current.leading = -leading;
+ },
+ setFont: function CanvasGraphics_setFont(fontRefName, size) {
+ var fontObj = this.commonObjs.get(fontRefName);
+ var current = this.current;
+
+ if (!fontObj) {
+ throw new Error("Can't find font for ".concat(fontRefName));
+ }
+
+ current.fontMatrix = fontObj.fontMatrix ? fontObj.fontMatrix : _util.FONT_IDENTITY_MATRIX;
+
+ if (current.fontMatrix[0] === 0 || current.fontMatrix[3] === 0) {
+ (0, _util.warn)('Invalid font matrix for font ' + fontRefName);
+ }
+
+ if (size < 0) {
+ size = -size;
+ current.fontDirection = -1;
+ } else {
+ current.fontDirection = 1;
+ }
+
+ this.current.font = fontObj;
+ this.current.fontSize = size;
+
+ if (fontObj.isType3Font) {
+ return;
+ }
+
+ var name = fontObj.loadedName || 'sans-serif';
+ var bold = fontObj.black ? '900' : fontObj.bold ? 'bold' : 'normal';
+ var italic = fontObj.italic ? 'italic' : 'normal';
+ var typeface = "\"".concat(name, "\", ").concat(fontObj.fallbackName);
+ var browserFontSize = size < MIN_FONT_SIZE ? MIN_FONT_SIZE : size > MAX_FONT_SIZE ? MAX_FONT_SIZE : size;
+ this.current.fontSizeScale = size / browserFontSize;
+ this.ctx.font = "".concat(italic, " ").concat(bold, " ").concat(browserFontSize, "px ").concat(typeface);
+ },
+ setTextRenderingMode: function CanvasGraphics_setTextRenderingMode(mode) {
+ this.current.textRenderingMode = mode;
+ },
+ setTextRise: function CanvasGraphics_setTextRise(rise) {
+ this.current.textRise = rise;
+ },
+ moveText: function CanvasGraphics_moveText(x, y) {
+ this.current.x = this.current.lineX += x;
+ this.current.y = this.current.lineY += y;
+ },
+ setLeadingMoveText: function CanvasGraphics_setLeadingMoveText(x, y) {
+ this.setLeading(-y);
+ this.moveText(x, y);
+ },
+ setTextMatrix: function CanvasGraphics_setTextMatrix(a, b, c, d, e, f) {
+ this.current.textMatrix = [a, b, c, d, e, f];
+ this.current.textMatrixScale = Math.sqrt(a * a + b * b);
+ this.current.x = this.current.lineX = 0;
+ this.current.y = this.current.lineY = 0;
+ },
+ nextLine: function CanvasGraphics_nextLine() {
+ this.moveText(0, this.current.leading);
+ },
+ paintChar: function paintChar(character, x, y, patternTransform) {
+ var ctx = this.ctx;
+ var current = this.current;
+ var font = current.font;
+ var textRenderingMode = current.textRenderingMode;
+ var fontSize = current.fontSize / current.fontSizeScale;
+ var fillStrokeMode = textRenderingMode & _util.TextRenderingMode.FILL_STROKE_MASK;
+ var isAddToPathSet = !!(textRenderingMode & _util.TextRenderingMode.ADD_TO_PATH_FLAG);
+ var patternFill = current.patternFill && font.data;
+ var addToPath;
+
+ if (font.disableFontFace || isAddToPathSet || patternFill) {
+ addToPath = font.getPathGenerator(this.commonObjs, character);
+ }
+
+ if (font.disableFontFace || patternFill) {
+ ctx.save();
+ ctx.translate(x, y);
+ ctx.beginPath();
+ addToPath(ctx, fontSize);
+
+ if (patternTransform) {
+ ctx.setTransform.apply(ctx, patternTransform);
+ }
+
+ if (fillStrokeMode === _util.TextRenderingMode.FILL || fillStrokeMode === _util.TextRenderingMode.FILL_STROKE) {
+ ctx.fill();
+ }
+
+ if (fillStrokeMode === _util.TextRenderingMode.STROKE || fillStrokeMode === _util.TextRenderingMode.FILL_STROKE) {
+ ctx.stroke();
+ }
+
+ ctx.restore();
+ } else {
+ if (fillStrokeMode === _util.TextRenderingMode.FILL || fillStrokeMode === _util.TextRenderingMode.FILL_STROKE) {
+ ctx.fillText(character, x, y);
+ }
+
+ if (fillStrokeMode === _util.TextRenderingMode.STROKE || fillStrokeMode === _util.TextRenderingMode.FILL_STROKE) {
+ ctx.strokeText(character, x, y);
+ }
+ }
+
+ if (isAddToPathSet) {
+ var paths = this.pendingTextPaths || (this.pendingTextPaths = []);
+ paths.push({
+ transform: ctx.mozCurrentTransform,
+ x: x,
+ y: y,
+ fontSize: fontSize,
+ addToPath: addToPath
+ });
+ }
+ },
+
+ get isFontSubpixelAAEnabled() {
+ var _this$cachedCanvases$ = this.cachedCanvases.getCanvas('isFontSubpixelAAEnabled', 10, 10),
+ ctx = _this$cachedCanvases$.context;
+
+ ctx.scale(1.5, 1);
+ ctx.fillText('I', 0, 10);
+ var data = ctx.getImageData(0, 0, 10, 10).data;
+ var enabled = false;
+
+ for (var i = 3; i < data.length; i += 4) {
+ if (data[i] > 0 && data[i] < 255) {
+ enabled = true;
+ break;
+ }
+ }
+
+ return (0, _util.shadow)(this, 'isFontSubpixelAAEnabled', enabled);
+ },
+
+ showText: function CanvasGraphics_showText(glyphs) {
+ var current = this.current;
+ var font = current.font;
+
+ if (font.isType3Font) {
+ return this.showType3Text(glyphs);
+ }
+
+ var fontSize = current.fontSize;
+
+ if (fontSize === 0) {
+ return undefined;
+ }
+
+ var ctx = this.ctx;
+ var fontSizeScale = current.fontSizeScale;
+ var charSpacing = current.charSpacing;
+ var wordSpacing = current.wordSpacing;
+ var fontDirection = current.fontDirection;
+ var textHScale = current.textHScale * fontDirection;
+ var glyphsLength = glyphs.length;
+ var vertical = font.vertical;
+ var spacingDir = vertical ? 1 : -1;
+ var defaultVMetrics = font.defaultVMetrics;
+ var widthAdvanceScale = fontSize * current.fontMatrix[0];
+ var simpleFillText = current.textRenderingMode === _util.TextRenderingMode.FILL && !font.disableFontFace && !current.patternFill;
+ ctx.save();
+ var patternTransform;
+
+ if (current.patternFill) {
+ ctx.save();
+ var pattern = current.fillColor.getPattern(ctx, this);
+ patternTransform = ctx.mozCurrentTransform;
+ ctx.restore();
+ ctx.fillStyle = pattern;
+ }
+
+ ctx.transform.apply(ctx, current.textMatrix);
+ ctx.translate(current.x, current.y + current.textRise);
+
+ if (fontDirection > 0) {
+ ctx.scale(textHScale, -1);
+ } else {
+ ctx.scale(textHScale, 1);
+ }
+
+ var lineWidth = current.lineWidth;
+ var scale = current.textMatrixScale;
+
+ if (scale === 0 || lineWidth === 0) {
+ var fillStrokeMode = current.textRenderingMode & _util.TextRenderingMode.FILL_STROKE_MASK;
+
+ if (fillStrokeMode === _util.TextRenderingMode.STROKE || fillStrokeMode === _util.TextRenderingMode.FILL_STROKE) {
+ this._cachedGetSinglePixelWidth = null;
+ lineWidth = this.getSinglePixelWidth() * MIN_WIDTH_FACTOR;
+ }
+ } else {
+ lineWidth /= scale;
+ }
+
+ if (fontSizeScale !== 1.0) {
+ ctx.scale(fontSizeScale, fontSizeScale);
+ lineWidth /= fontSizeScale;
+ }
+
+ ctx.lineWidth = lineWidth;
+ var x = 0,
+ i;
+
+ for (i = 0; i < glyphsLength; ++i) {
+ var glyph = glyphs[i];
+
+ if ((0, _util.isNum)(glyph)) {
+ x += spacingDir * glyph * fontSize / 1000;
+ continue;
+ }
+
+ var restoreNeeded = false;
+ var spacing = (glyph.isSpace ? wordSpacing : 0) + charSpacing;
+ var character = glyph.fontChar;
+ var accent = glyph.accent;
+ var scaledX, scaledY, scaledAccentX, scaledAccentY;
+ var width = glyph.width;
+
+ if (vertical) {
+ var vmetric, vx, vy;
+ vmetric = glyph.vmetric || defaultVMetrics;
+ vx = glyph.vmetric ? vmetric[1] : width * 0.5;
+ vx = -vx * widthAdvanceScale;
+ vy = vmetric[2] * widthAdvanceScale;
+ width = vmetric ? -vmetric[0] : width;
+ scaledX = vx / fontSizeScale;
+ scaledY = (x + vy) / fontSizeScale;
+ } else {
+ scaledX = x / fontSizeScale;
+ scaledY = 0;
+ }
+
+ if (font.remeasure && width > 0) {
+ var measuredWidth = ctx.measureText(character).width * 1000 / fontSize * fontSizeScale;
+
+ if (width < measuredWidth && this.isFontSubpixelAAEnabled) {
+ var characterScaleX = width / measuredWidth;
+ restoreNeeded = true;
+ ctx.save();
+ ctx.scale(characterScaleX, 1);
+ scaledX /= characterScaleX;
+ } else if (width !== measuredWidth) {
+ scaledX += (width - measuredWidth) / 2000 * fontSize / fontSizeScale;
+ }
+ }
+
+ if (glyph.isInFont || font.missingFile) {
+ if (simpleFillText && !accent) {
+ ctx.fillText(character, scaledX, scaledY);
+ } else {
+ this.paintChar(character, scaledX, scaledY, patternTransform);
+
+ if (accent) {
+ scaledAccentX = scaledX + accent.offset.x / fontSizeScale;
+ scaledAccentY = scaledY - accent.offset.y / fontSizeScale;
+ this.paintChar(accent.fontChar, scaledAccentX, scaledAccentY, patternTransform);
+ }
+ }
+ }
+
+ var charWidth = width * widthAdvanceScale + spacing * fontDirection;
+ x += charWidth;
+
+ if (restoreNeeded) {
+ ctx.restore();
+ }
+ }
+
+ if (vertical) {
+ current.y -= x * textHScale;
+ } else {
+ current.x += x * textHScale;
+ }
+
+ ctx.restore();
+ },
+ showType3Text: function CanvasGraphics_showType3Text(glyphs) {
+ var ctx = this.ctx;
+ var current = this.current;
+ var font = current.font;
+ var fontSize = current.fontSize;
+ var fontDirection = current.fontDirection;
+ var spacingDir = font.vertical ? 1 : -1;
+ var charSpacing = current.charSpacing;
+ var wordSpacing = current.wordSpacing;
+ var textHScale = current.textHScale * fontDirection;
+ var fontMatrix = current.fontMatrix || _util.FONT_IDENTITY_MATRIX;
+ var glyphsLength = glyphs.length;
+ var isTextInvisible = current.textRenderingMode === _util.TextRenderingMode.INVISIBLE;
+ var i, glyph, width, spacingLength;
+
+ if (isTextInvisible || fontSize === 0) {
+ return;
+ }
+
+ this._cachedGetSinglePixelWidth = null;
+ ctx.save();
+ ctx.transform.apply(ctx, current.textMatrix);
+ ctx.translate(current.x, current.y);
+ ctx.scale(textHScale, fontDirection);
+
+ for (i = 0; i < glyphsLength; ++i) {
+ glyph = glyphs[i];
+
+ if ((0, _util.isNum)(glyph)) {
+ spacingLength = spacingDir * glyph * fontSize / 1000;
+ this.ctx.translate(spacingLength, 0);
+ current.x += spacingLength * textHScale;
+ continue;
+ }
+
+ var spacing = (glyph.isSpace ? wordSpacing : 0) + charSpacing;
+ var operatorList = font.charProcOperatorList[glyph.operatorListId];
+
+ if (!operatorList) {
+ (0, _util.warn)("Type3 character \"".concat(glyph.operatorListId, "\" is not available."));
+ continue;
+ }
+
+ this.processingType3 = glyph;
+ this.save();
+ ctx.scale(fontSize, fontSize);
+ ctx.transform.apply(ctx, fontMatrix);
+ this.executeOperatorList(operatorList);
+ this.restore();
+
+ var transformed = _util.Util.applyTransform([glyph.width, 0], fontMatrix);
+
+ width = transformed[0] * fontSize + spacing;
+ ctx.translate(width, 0);
+ current.x += width * textHScale;
+ }
+
+ ctx.restore();
+ this.processingType3 = null;
+ },
+ setCharWidth: function CanvasGraphics_setCharWidth(xWidth, yWidth) {},
+ setCharWidthAndBounds: function CanvasGraphics_setCharWidthAndBounds(xWidth, yWidth, llx, lly, urx, ury) {
+ this.ctx.rect(llx, lly, urx - llx, ury - lly);
+ this.clip();
+ this.endPath();
+ },
+ getColorN_Pattern: function CanvasGraphics_getColorN_Pattern(IR) {
+ var _this = this;
+
+ var pattern;
+
+ if (IR[0] === 'TilingPattern') {
+ var color = IR[1];
+ var baseTransform = this.baseTransform || this.ctx.mozCurrentTransform.slice();
+ var canvasGraphicsFactory = {
+ createCanvasGraphics: function createCanvasGraphics(ctx) {
+ return new CanvasGraphics(ctx, _this.commonObjs, _this.objs, _this.canvasFactory, _this.webGLContext);
+ }
+ };
+ pattern = new _pattern_helper.TilingPattern(IR, color, this.ctx, canvasGraphicsFactory, baseTransform);
+ } else {
+ pattern = (0, _pattern_helper.getShadingPatternFromIR)(IR);
+ }
+
+ return pattern;
+ },
+ setStrokeColorN: function CanvasGraphics_setStrokeColorN() {
+ this.current.strokeColor = this.getColorN_Pattern(arguments);
+ },
+ setFillColorN: function CanvasGraphics_setFillColorN() {
+ this.current.fillColor = this.getColorN_Pattern(arguments);
+ this.current.patternFill = true;
+ },
+ setStrokeRGBColor: function CanvasGraphics_setStrokeRGBColor(r, g, b) {
+ var color = _util.Util.makeCssRgb(r, g, b);
+
+ this.ctx.strokeStyle = color;
+ this.current.strokeColor = color;
+ },
+ setFillRGBColor: function CanvasGraphics_setFillRGBColor(r, g, b) {
+ var color = _util.Util.makeCssRgb(r, g, b);
+
+ this.ctx.fillStyle = color;
+ this.current.fillColor = color;
+ this.current.patternFill = false;
+ },
+ shadingFill: function CanvasGraphics_shadingFill(patternIR) {
+ var ctx = this.ctx;
+ this.save();
+ var pattern = (0, _pattern_helper.getShadingPatternFromIR)(patternIR);
+ ctx.fillStyle = pattern.getPattern(ctx, this, true);
+ var inv = ctx.mozCurrentTransformInverse;
+
+ if (inv) {
+ var canvas = ctx.canvas;
+ var width = canvas.width;
+ var height = canvas.height;
+
+ var bl = _util.Util.applyTransform([0, 0], inv);
+
+ var br = _util.Util.applyTransform([0, height], inv);
+
+ var ul = _util.Util.applyTransform([width, 0], inv);
+
+ var ur = _util.Util.applyTransform([width, height], inv);
+
+ var x0 = Math.min(bl[0], br[0], ul[0], ur[0]);
+ var y0 = Math.min(bl[1], br[1], ul[1], ur[1]);
+ var x1 = Math.max(bl[0], br[0], ul[0], ur[0]);
+ var y1 = Math.max(bl[1], br[1], ul[1], ur[1]);
+ this.ctx.fillRect(x0, y0, x1 - x0, y1 - y0);
+ } else {
+ this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10);
+ }
+
+ this.restore();
+ },
+ beginInlineImage: function CanvasGraphics_beginInlineImage() {
+ (0, _util.unreachable)('Should not call beginInlineImage');
+ },
+ beginImageData: function CanvasGraphics_beginImageData() {
+ (0, _util.unreachable)('Should not call beginImageData');
+ },
+ paintFormXObjectBegin: function CanvasGraphics_paintFormXObjectBegin(matrix, bbox) {
+ this.save();
+ this.baseTransformStack.push(this.baseTransform);
+
+ if (Array.isArray(matrix) && matrix.length === 6) {
+ this.transform.apply(this, matrix);
+ }
+
+ this.baseTransform = this.ctx.mozCurrentTransform;
+
+ if (bbox) {
+ var width = bbox[2] - bbox[0];
+ var height = bbox[3] - bbox[1];
+ this.ctx.rect(bbox[0], bbox[1], width, height);
+ this.clip();
+ this.endPath();
+ }
+ },
+ paintFormXObjectEnd: function CanvasGraphics_paintFormXObjectEnd() {
+ this.restore();
+ this.baseTransform = this.baseTransformStack.pop();
+ },
+ beginGroup: function CanvasGraphics_beginGroup(group) {
+ this.save();
+ var currentCtx = this.ctx;
+
+ if (!group.isolated) {
+ (0, _util.info)('TODO: Support non-isolated groups.');
+ }
+
+ if (group.knockout) {
+ (0, _util.warn)('Knockout groups not supported.');
+ }
+
+ var currentTransform = currentCtx.mozCurrentTransform;
+
+ if (group.matrix) {
+ currentCtx.transform.apply(currentCtx, group.matrix);
+ }
+
+ if (!group.bbox) {
+ throw new Error('Bounding box is required.');
+ }
+
+ var bounds = _util.Util.getAxialAlignedBoundingBox(group.bbox, currentCtx.mozCurrentTransform);
+
+ var canvasBounds = [0, 0, currentCtx.canvas.width, currentCtx.canvas.height];
+ bounds = _util.Util.intersect(bounds, canvasBounds) || [0, 0, 0, 0];
+ var offsetX = Math.floor(bounds[0]);
+ var offsetY = Math.floor(bounds[1]);
+ var drawnWidth = Math.max(Math.ceil(bounds[2]) - offsetX, 1);
+ var drawnHeight = Math.max(Math.ceil(bounds[3]) - offsetY, 1);
+ var scaleX = 1,
+ scaleY = 1;
+
+ if (drawnWidth > MAX_GROUP_SIZE) {
+ scaleX = drawnWidth / MAX_GROUP_SIZE;
+ drawnWidth = MAX_GROUP_SIZE;
+ }
+
+ if (drawnHeight > MAX_GROUP_SIZE) {
+ scaleY = drawnHeight / MAX_GROUP_SIZE;
+ drawnHeight = MAX_GROUP_SIZE;
+ }
+
+ var cacheId = 'groupAt' + this.groupLevel;
+
+ if (group.smask) {
+ cacheId += '_smask_' + this.smaskCounter++ % 2;
+ }
+
+ var scratchCanvas = this.cachedCanvases.getCanvas(cacheId, drawnWidth, drawnHeight, true);
+ var groupCtx = scratchCanvas.context;
+ groupCtx.scale(1 / scaleX, 1 / scaleY);
+ groupCtx.translate(-offsetX, -offsetY);
+ groupCtx.transform.apply(groupCtx, currentTransform);
+
+ if (group.smask) {
+ this.smaskStack.push({
+ canvas: scratchCanvas.canvas,
+ context: groupCtx,
+ offsetX: offsetX,
+ offsetY: offsetY,
+ scaleX: scaleX,
+ scaleY: scaleY,
+ subtype: group.smask.subtype,
+ backdrop: group.smask.backdrop,
+ transferMap: group.smask.transferMap || null,
+ startTransformInverse: null
+ });
+ } else {
+ currentCtx.setTransform(1, 0, 0, 1, 0, 0);
+ currentCtx.translate(offsetX, offsetY);
+ currentCtx.scale(scaleX, scaleY);
+ }
+
+ copyCtxState(currentCtx, groupCtx);
+ this.ctx = groupCtx;
+ this.setGState([['BM', 'source-over'], ['ca', 1], ['CA', 1]]);
+ this.groupStack.push(currentCtx);
+ this.groupLevel++;
+ this.current.activeSMask = null;
+ },
+ endGroup: function CanvasGraphics_endGroup(group) {
+ this.groupLevel--;
+ var groupCtx = this.ctx;
+ this.ctx = this.groupStack.pop();
+
+ if (this.ctx.imageSmoothingEnabled !== undefined) {
+ this.ctx.imageSmoothingEnabled = false;
+ } else {
+ this.ctx.mozImageSmoothingEnabled = false;
+ }
+
+ if (group.smask) {
+ this.tempSMask = this.smaskStack.pop();
+ } else {
+ this.ctx.drawImage(groupCtx.canvas, 0, 0);
+ }
+
+ this.restore();
+ },
+ beginAnnotations: function CanvasGraphics_beginAnnotations() {
+ this.save();
+
+ if (this.baseTransform) {
+ this.ctx.setTransform.apply(this.ctx, this.baseTransform);
+ }
+ },
+ endAnnotations: function CanvasGraphics_endAnnotations() {
+ this.restore();
+ },
+ beginAnnotation: function CanvasGraphics_beginAnnotation(rect, transform, matrix) {
+ this.save();
+ resetCtxToDefault(this.ctx);
+ this.current = new CanvasExtraState();
+
+ if (Array.isArray(rect) && rect.length === 4) {
+ var width = rect[2] - rect[0];
+ var height = rect[3] - rect[1];
+ this.ctx.rect(rect[0], rect[1], width, height);
+ this.clip();
+ this.endPath();
+ }
+
+ this.transform.apply(this, transform);
+ this.transform.apply(this, matrix);
+ },
+ endAnnotation: function CanvasGraphics_endAnnotation() {
+ this.restore();
+ },
+ paintJpegXObject: function CanvasGraphics_paintJpegXObject(objId, w, h) {
+ var domImage = this.processingType3 ? this.commonObjs.get(objId) : this.objs.get(objId);
+
+ if (!domImage) {
+ (0, _util.warn)('Dependent image isn\'t ready yet');
+ return;
+ }
+
+ this.save();
+ var ctx = this.ctx;
+ ctx.scale(1 / w, -1 / h);
+ ctx.drawImage(domImage, 0, 0, domImage.width, domImage.height, 0, -h, w, h);
+
+ if (this.imageLayer) {
+ var currentTransform = ctx.mozCurrentTransformInverse;
+ var position = this.getCanvasPosition(0, 0);
+ this.imageLayer.appendImage({
+ objId: objId,
+ left: position[0],
+ top: position[1],
+ width: w / currentTransform[0],
+ height: h / currentTransform[3]
+ });
+ }
+
+ this.restore();
+ },
+ paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(img) {
+ var ctx = this.ctx;
+ var width = img.width,
+ height = img.height;
+ var fillColor = this.current.fillColor;
+ var isPatternFill = this.current.patternFill;
+ var glyph = this.processingType3;
+
+ if (COMPILE_TYPE3_GLYPHS && glyph && glyph.compiled === undefined) {
+ if (width <= MAX_SIZE_TO_COMPILE && height <= MAX_SIZE_TO_COMPILE) {
+ glyph.compiled = compileType3Glyph({
+ data: img.data,
+ width: width,
+ height: height
+ });
+ } else {
+ glyph.compiled = null;
+ }
+ }
+
+ if (glyph && glyph.compiled) {
+ glyph.compiled(ctx);
+ return;
+ }
+
+ var maskCanvas = this.cachedCanvases.getCanvas('maskCanvas', width, height);
+ var maskCtx = maskCanvas.context;
+ maskCtx.save();
+ putBinaryImageMask(maskCtx, img);
+ maskCtx.globalCompositeOperation = 'source-in';
+ maskCtx.fillStyle = isPatternFill ? fillColor.getPattern(maskCtx, this) : fillColor;
+ maskCtx.fillRect(0, 0, width, height);
+ maskCtx.restore();
+ this.paintInlineImageXObject(maskCanvas.canvas);
+ },
+ paintImageMaskXObjectRepeat: function CanvasGraphics_paintImageMaskXObjectRepeat(imgData, scaleX, scaleY, positions) {
+ var width = imgData.width;
+ var height = imgData.height;
+ var fillColor = this.current.fillColor;
+ var isPatternFill = this.current.patternFill;
+ var maskCanvas = this.cachedCanvases.getCanvas('maskCanvas', width, height);
+ var maskCtx = maskCanvas.context;
+ maskCtx.save();
+ putBinaryImageMask(maskCtx, imgData);
+ maskCtx.globalCompositeOperation = 'source-in';
+ maskCtx.fillStyle = isPatternFill ? fillColor.getPattern(maskCtx, this) : fillColor;
+ maskCtx.fillRect(0, 0, width, height);
+ maskCtx.restore();
+ var ctx = this.ctx;
+
+ for (var i = 0, ii = positions.length; i < ii; i += 2) {
+ ctx.save();
+ ctx.transform(scaleX, 0, 0, scaleY, positions[i], positions[i + 1]);
+ ctx.scale(1, -1);
+ ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, 0, -1, 1, 1);
+ ctx.restore();
+ }
+ },
+ paintImageMaskXObjectGroup: function CanvasGraphics_paintImageMaskXObjectGroup(images) {
+ var ctx = this.ctx;
+ var fillColor = this.current.fillColor;
+ var isPatternFill = this.current.patternFill;
+
+ for (var i = 0, ii = images.length; i < ii; i++) {
+ var image = images[i];
+ var width = image.width,
+ height = image.height;
+ var maskCanvas = this.cachedCanvases.getCanvas('maskCanvas', width, height);
+ var maskCtx = maskCanvas.context;
+ maskCtx.save();
+ putBinaryImageMask(maskCtx, image);
+ maskCtx.globalCompositeOperation = 'source-in';
+ maskCtx.fillStyle = isPatternFill ? fillColor.getPattern(maskCtx, this) : fillColor;
+ maskCtx.fillRect(0, 0, width, height);
+ maskCtx.restore();
+ ctx.save();
+ ctx.transform.apply(ctx, image.transform);
+ ctx.scale(1, -1);
+ ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, 0, -1, 1, 1);
+ ctx.restore();
+ }
+ },
+ paintImageXObject: function CanvasGraphics_paintImageXObject(objId) {
+ var imgData = this.processingType3 ? this.commonObjs.get(objId) : this.objs.get(objId);
+
+ if (!imgData) {
+ (0, _util.warn)('Dependent image isn\'t ready yet');
+ return;
+ }
+
+ this.paintInlineImageXObject(imgData);
+ },
+ paintImageXObjectRepeat: function CanvasGraphics_paintImageXObjectRepeat(objId, scaleX, scaleY, positions) {
+ var imgData = this.processingType3 ? this.commonObjs.get(objId) : this.objs.get(objId);
+
+ if (!imgData) {
+ (0, _util.warn)('Dependent image isn\'t ready yet');
+ return;
+ }
+
+ var width = imgData.width;
+ var height = imgData.height;
+ var map = [];
+
+ for (var i = 0, ii = positions.length; i < ii; i += 2) {
+ map.push({
+ transform: [scaleX, 0, 0, scaleY, positions[i], positions[i + 1]],
+ x: 0,
+ y: 0,
+ w: width,
+ h: height
+ });
+ }
+
+ this.paintInlineImageXObjectGroup(imgData, map);
+ },
+ paintInlineImageXObject: function CanvasGraphics_paintInlineImageXObject(imgData) {
+ var width = imgData.width;
+ var height = imgData.height;
+ var ctx = this.ctx;
+ this.save();
+ ctx.scale(1 / width, -1 / height);
+ var currentTransform = ctx.mozCurrentTransformInverse;
+ var a = currentTransform[0],
+ b = currentTransform[1];
+ var widthScale = Math.max(Math.sqrt(a * a + b * b), 1);
+ var c = currentTransform[2],
+ d = currentTransform[3];
+ var heightScale = Math.max(Math.sqrt(c * c + d * d), 1);
+ var imgToPaint, tmpCanvas;
+
+ if (typeof HTMLElement === 'function' && imgData instanceof HTMLElement || !imgData.data) {
+ imgToPaint = imgData;
+ } else {
+ tmpCanvas = this.cachedCanvases.getCanvas('inlineImage', width, height);
+ var tmpCtx = tmpCanvas.context;
+ putBinaryImageData(tmpCtx, imgData);
+ imgToPaint = tmpCanvas.canvas;
+ }
+
+ var paintWidth = width,
+ paintHeight = height;
+ var tmpCanvasId = 'prescale1';
+
+ while (widthScale > 2 && paintWidth > 1 || heightScale > 2 && paintHeight > 1) {
+ var newWidth = paintWidth,
+ newHeight = paintHeight;
+
+ if (widthScale > 2 && paintWidth > 1) {
+ newWidth = Math.ceil(paintWidth / 2);
+ widthScale /= paintWidth / newWidth;
+ }
+
+ if (heightScale > 2 && paintHeight > 1) {
+ newHeight = Math.ceil(paintHeight / 2);
+ heightScale /= paintHeight / newHeight;
+ }
+
+ tmpCanvas = this.cachedCanvases.getCanvas(tmpCanvasId, newWidth, newHeight);
+ tmpCtx = tmpCanvas.context;
+ tmpCtx.clearRect(0, 0, newWidth, newHeight);
+ tmpCtx.drawImage(imgToPaint, 0, 0, paintWidth, paintHeight, 0, 0, newWidth, newHeight);
+ imgToPaint = tmpCanvas.canvas;
+ paintWidth = newWidth;
+ paintHeight = newHeight;
+ tmpCanvasId = tmpCanvasId === 'prescale1' ? 'prescale2' : 'prescale1';
+ }
+
+ ctx.drawImage(imgToPaint, 0, 0, paintWidth, paintHeight, 0, -height, width, height);
+
+ if (this.imageLayer) {
+ var position = this.getCanvasPosition(0, -height);
+ this.imageLayer.appendImage({
+ imgData: imgData,
+ left: position[0],
+ top: position[1],
+ width: width / currentTransform[0],
+ height: height / currentTransform[3]
+ });
+ }
+
+ this.restore();
+ },
+ paintInlineImageXObjectGroup: function CanvasGraphics_paintInlineImageXObjectGroup(imgData, map) {
+ var ctx = this.ctx;
+ var w = imgData.width;
+ var h = imgData.height;
+ var tmpCanvas = this.cachedCanvases.getCanvas('inlineImage', w, h);
+ var tmpCtx = tmpCanvas.context;
+ putBinaryImageData(tmpCtx, imgData);
+
+ for (var i = 0, ii = map.length; i < ii; i++) {
+ var entry = map[i];
+ ctx.save();
+ ctx.transform.apply(ctx, entry.transform);
+ ctx.scale(1, -1);
+ ctx.drawImage(tmpCanvas.canvas, entry.x, entry.y, entry.w, entry.h, 0, -1, 1, 1);
+
+ if (this.imageLayer) {
+ var position = this.getCanvasPosition(entry.x, entry.y);
+ this.imageLayer.appendImage({
+ imgData: imgData,
+ left: position[0],
+ top: position[1],
+ width: w,
+ height: h
+ });
+ }
+
+ ctx.restore();
+ }
+ },
+ paintSolidColorImageMask: function CanvasGraphics_paintSolidColorImageMask() {
+ this.ctx.fillRect(0, 0, 1, 1);
+ },
+ paintXObject: function CanvasGraphics_paintXObject() {
+ (0, _util.warn)('Unsupported \'paintXObject\' command.');
+ },
+ markPoint: function CanvasGraphics_markPoint(tag) {},
+ markPointProps: function CanvasGraphics_markPointProps(tag, properties) {},
+ beginMarkedContent: function CanvasGraphics_beginMarkedContent(tag) {},
+ beginMarkedContentProps: function CanvasGraphics_beginMarkedContentProps(tag, properties) {},
+ endMarkedContent: function CanvasGraphics_endMarkedContent() {},
+ beginCompat: function CanvasGraphics_beginCompat() {},
+ endCompat: function CanvasGraphics_endCompat() {},
+ consumePath: function CanvasGraphics_consumePath() {
+ var ctx = this.ctx;
+
+ if (this.pendingClip) {
+ if (this.pendingClip === EO_CLIP) {
+ ctx.clip('evenodd');
+ } else {
+ ctx.clip();
+ }
+
+ this.pendingClip = null;
+ }
+
+ ctx.beginPath();
+ },
+ getSinglePixelWidth: function getSinglePixelWidth(scale) {
+ if (this._cachedGetSinglePixelWidth === null) {
+ var inverse = this.ctx.mozCurrentTransformInverse;
+ this._cachedGetSinglePixelWidth = Math.sqrt(Math.max(inverse[0] * inverse[0] + inverse[1] * inverse[1], inverse[2] * inverse[2] + inverse[3] * inverse[3]));
+ }
+
+ return this._cachedGetSinglePixelWidth;
+ },
+ getCanvasPosition: function CanvasGraphics_getCanvasPosition(x, y) {
+ var transform = this.ctx.mozCurrentTransform;
+ return [transform[0] * x + transform[2] * y + transform[4], transform[1] * x + transform[3] * y + transform[5]];
+ }
+ };
+
+ for (var op in _util.OPS) {
+ CanvasGraphics.prototype[_util.OPS[op]] = CanvasGraphics.prototype[op];
+ }
+
+ return CanvasGraphics;
+}();
+
+exports.CanvasGraphics = CanvasGraphics;
+
+/***/ }),
+/* 155 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.getShadingPatternFromIR = getShadingPatternFromIR;
+exports.TilingPattern = void 0;
+
+var _util = __w_pdfjs_require__(1);
+
+var ShadingIRs = {};
+ShadingIRs.RadialAxial = {
+ fromIR: function RadialAxial_fromIR(raw) {
+ var type = raw[1];
+ var colorStops = raw[2];
+ var p0 = raw[3];
+ var p1 = raw[4];
+ var r0 = raw[5];
+ var r1 = raw[6];
+ return {
+ type: 'Pattern',
+ getPattern: function RadialAxial_getPattern(ctx) {
+ var grad;
+
+ if (type === 'axial') {
+ grad = ctx.createLinearGradient(p0[0], p0[1], p1[0], p1[1]);
+ } else if (type === 'radial') {
+ grad = ctx.createRadialGradient(p0[0], p0[1], r0, p1[0], p1[1], r1);
+ }
+
+ for (var i = 0, ii = colorStops.length; i < ii; ++i) {
+ var c = colorStops[i];
+ grad.addColorStop(c[0], c[1]);
+ }
+
+ return grad;
+ }
+ };
+ }
+};
+
+var createMeshCanvas = function createMeshCanvasClosure() {
+ function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) {
+ var coords = context.coords,
+ colors = context.colors;
+ var bytes = data.data,
+ rowSize = data.width * 4;
+ var tmp;
+
+ if (coords[p1 + 1] > coords[p2 + 1]) {
+ tmp = p1;
+ p1 = p2;
+ p2 = tmp;
+ tmp = c1;
+ c1 = c2;
+ c2 = tmp;
+ }
+
+ if (coords[p2 + 1] > coords[p3 + 1]) {
+ tmp = p2;
+ p2 = p3;
+ p3 = tmp;
+ tmp = c2;
+ c2 = c3;
+ c3 = tmp;
+ }
+
+ if (coords[p1 + 1] > coords[p2 + 1]) {
+ tmp = p1;
+ p1 = p2;
+ p2 = tmp;
+ tmp = c1;
+ c1 = c2;
+ c2 = tmp;
+ }
+
+ var x1 = (coords[p1] + context.offsetX) * context.scaleX;
+ var y1 = (coords[p1 + 1] + context.offsetY) * context.scaleY;
+ var x2 = (coords[p2] + context.offsetX) * context.scaleX;
+ var y2 = (coords[p2 + 1] + context.offsetY) * context.scaleY;
+ var x3 = (coords[p3] + context.offsetX) * context.scaleX;
+ var y3 = (coords[p3 + 1] + context.offsetY) * context.scaleY;
+
+ if (y1 >= y3) {
+ return;
+ }
+
+ var c1r = colors[c1],
+ c1g = colors[c1 + 1],
+ c1b = colors[c1 + 2];
+ var c2r = colors[c2],
+ c2g = colors[c2 + 1],
+ c2b = colors[c2 + 2];
+ var c3r = colors[c3],
+ c3g = colors[c3 + 1],
+ c3b = colors[c3 + 2];
+ var minY = Math.round(y1),
+ maxY = Math.round(y3);
+ var xa, car, cag, cab;
+ var xb, cbr, cbg, cbb;
+ var k;
+
+ for (var y = minY; y <= maxY; y++) {
+ if (y < y2) {
+ k = y < y1 ? 0 : y1 === y2 ? 1 : (y1 - y) / (y1 - y2);
+ xa = x1 - (x1 - x2) * k;
+ car = c1r - (c1r - c2r) * k;
+ cag = c1g - (c1g - c2g) * k;
+ cab = c1b - (c1b - c2b) * k;
+ } else {
+ k = y > y3 ? 1 : y2 === y3 ? 0 : (y2 - y) / (y2 - y3);
+ xa = x2 - (x2 - x3) * k;
+ car = c2r - (c2r - c3r) * k;
+ cag = c2g - (c2g - c3g) * k;
+ cab = c2b - (c2b - c3b) * k;
+ }
+
+ k = y < y1 ? 0 : y > y3 ? 1 : (y1 - y) / (y1 - y3);
+ xb = x1 - (x1 - x3) * k;
+ cbr = c1r - (c1r - c3r) * k;
+ cbg = c1g - (c1g - c3g) * k;
+ cbb = c1b - (c1b - c3b) * k;
+ var x1_ = Math.round(Math.min(xa, xb));
+ var x2_ = Math.round(Math.max(xa, xb));
+ var j = rowSize * y + x1_ * 4;
+
+ for (var x = x1_; x <= x2_; x++) {
+ k = (xa - x) / (xa - xb);
+ k = k < 0 ? 0 : k > 1 ? 1 : k;
+ bytes[j++] = car - (car - cbr) * k | 0;
+ bytes[j++] = cag - (cag - cbg) * k | 0;
+ bytes[j++] = cab - (cab - cbb) * k | 0;
+ bytes[j++] = 255;
+ }
+ }
+ }
+
+ function drawFigure(data, figure, context) {
+ var ps = figure.coords;
+ var cs = figure.colors;
+ var i, ii;
+
+ switch (figure.type) {
+ case 'lattice':
+ var verticesPerRow = figure.verticesPerRow;
+ var rows = Math.floor(ps.length / verticesPerRow) - 1;
+ var cols = verticesPerRow - 1;
+
+ for (i = 0; i < rows; i++) {
+ var q = i * verticesPerRow;
+
+ for (var j = 0; j < cols; j++, q++) {
+ drawTriangle(data, context, ps[q], ps[q + 1], ps[q + verticesPerRow], cs[q], cs[q + 1], cs[q + verticesPerRow]);
+ drawTriangle(data, context, ps[q + verticesPerRow + 1], ps[q + 1], ps[q + verticesPerRow], cs[q + verticesPerRow + 1], cs[q + 1], cs[q + verticesPerRow]);
+ }
+ }
+
+ break;
+
+ case 'triangles':
+ for (i = 0, ii = ps.length; i < ii; i += 3) {
+ drawTriangle(data, context, ps[i], ps[i + 1], ps[i + 2], cs[i], cs[i + 1], cs[i + 2]);
+ }
+
+ break;
+
+ default:
+ throw new Error('illegal figure');
+ }
+ }
+
+ function createMeshCanvas(bounds, combinesScale, coords, colors, figures, backgroundColor, cachedCanvases, webGLContext) {
+ var EXPECTED_SCALE = 1.1;
+ var MAX_PATTERN_SIZE = 3000;
+ var BORDER_SIZE = 2;
+ var offsetX = Math.floor(bounds[0]);
+ var offsetY = Math.floor(bounds[1]);
+ var boundsWidth = Math.ceil(bounds[2]) - offsetX;
+ var boundsHeight = Math.ceil(bounds[3]) - offsetY;
+ var width = Math.min(Math.ceil(Math.abs(boundsWidth * combinesScale[0] * EXPECTED_SCALE)), MAX_PATTERN_SIZE);
+ var height = Math.min(Math.ceil(Math.abs(boundsHeight * combinesScale[1] * EXPECTED_SCALE)), MAX_PATTERN_SIZE);
+ var scaleX = boundsWidth / width;
+ var scaleY = boundsHeight / height;
+ var context = {
+ coords: coords,
+ colors: colors,
+ offsetX: -offsetX,
+ offsetY: -offsetY,
+ scaleX: 1 / scaleX,
+ scaleY: 1 / scaleY
+ };
+ var paddedWidth = width + BORDER_SIZE * 2;
+ var paddedHeight = height + BORDER_SIZE * 2;
+ var canvas, tmpCanvas, i, ii;
+
+ if (webGLContext.isEnabled) {
+ canvas = webGLContext.drawFigures({
+ width: width,
+ height: height,
+ backgroundColor: backgroundColor,
+ figures: figures,
+ context: context
+ });
+ tmpCanvas = cachedCanvases.getCanvas('mesh', paddedWidth, paddedHeight, false);
+ tmpCanvas.context.drawImage(canvas, BORDER_SIZE, BORDER_SIZE);
+ canvas = tmpCanvas.canvas;
+ } else {
+ tmpCanvas = cachedCanvases.getCanvas('mesh', paddedWidth, paddedHeight, false);
+ var tmpCtx = tmpCanvas.context;
+ var data = tmpCtx.createImageData(width, height);
+
+ if (backgroundColor) {
+ var bytes = data.data;
+
+ for (i = 0, ii = bytes.length; i < ii; i += 4) {
+ bytes[i] = backgroundColor[0];
+ bytes[i + 1] = backgroundColor[1];
+ bytes[i + 2] = backgroundColor[2];
+ bytes[i + 3] = 255;
+ }
+ }
+
+ for (i = 0; i < figures.length; i++) {
+ drawFigure(data, figures[i], context);
+ }
+
+ tmpCtx.putImageData(data, BORDER_SIZE, BORDER_SIZE);
+ canvas = tmpCanvas.canvas;
+ }
+
+ return {
+ canvas: canvas,
+ offsetX: offsetX - BORDER_SIZE * scaleX,
+ offsetY: offsetY - BORDER_SIZE * scaleY,
+ scaleX: scaleX,
+ scaleY: scaleY
+ };
+ }
+
+ return createMeshCanvas;
+}();
+
+ShadingIRs.Mesh = {
+ fromIR: function Mesh_fromIR(raw) {
+ var coords = raw[2];
+ var colors = raw[3];
+ var figures = raw[4];
+ var bounds = raw[5];
+ var matrix = raw[6];
+ var background = raw[8];
+ return {
+ type: 'Pattern',
+ getPattern: function Mesh_getPattern(ctx, owner, shadingFill) {
+ var scale;
+
+ if (shadingFill) {
+ scale = _util.Util.singularValueDecompose2dScale(ctx.mozCurrentTransform);
+ } else {
+ scale = _util.Util.singularValueDecompose2dScale(owner.baseTransform);
+
+ if (matrix) {
+ var matrixScale = _util.Util.singularValueDecompose2dScale(matrix);
+
+ scale = [scale[0] * matrixScale[0], scale[1] * matrixScale[1]];
+ }
+ }
+
+ var temporaryPatternCanvas = createMeshCanvas(bounds, scale, coords, colors, figures, shadingFill ? null : background, owner.cachedCanvases, owner.webGLContext);
+
+ if (!shadingFill) {
+ ctx.setTransform.apply(ctx, owner.baseTransform);
+
+ if (matrix) {
+ ctx.transform.apply(ctx, matrix);
+ }
+ }
+
+ ctx.translate(temporaryPatternCanvas.offsetX, temporaryPatternCanvas.offsetY);
+ ctx.scale(temporaryPatternCanvas.scaleX, temporaryPatternCanvas.scaleY);
+ return ctx.createPattern(temporaryPatternCanvas.canvas, 'no-repeat');
+ }
+ };
+ }
+};
+ShadingIRs.Dummy = {
+ fromIR: function Dummy_fromIR() {
+ return {
+ type: 'Pattern',
+ getPattern: function Dummy_fromIR_getPattern() {
+ return 'hotpink';
+ }
+ };
+ }
+};
+
+function getShadingPatternFromIR(raw) {
+ var shadingIR = ShadingIRs[raw[0]];
+
+ if (!shadingIR) {
+ throw new Error("Unknown IR type: ".concat(raw[0]));
+ }
+
+ return shadingIR.fromIR(raw);
+}
+
+var TilingPattern = function TilingPatternClosure() {
+ var PaintType = {
+ COLORED: 1,
+ UNCOLORED: 2
+ };
+ var MAX_PATTERN_SIZE = 3000;
+
+ function TilingPattern(IR, color, ctx, canvasGraphicsFactory, baseTransform) {
+ this.operatorList = IR[2];
+ this.matrix = IR[3] || [1, 0, 0, 1, 0, 0];
+ this.bbox = IR[4];
+ this.xstep = IR[5];
+ this.ystep = IR[6];
+ this.paintType = IR[7];
+ this.tilingType = IR[8];
+ this.color = color;
+ this.canvasGraphicsFactory = canvasGraphicsFactory;
+ this.baseTransform = baseTransform;
+ this.type = 'Pattern';
+ this.ctx = ctx;
+ }
+
+ TilingPattern.prototype = {
+ createPatternCanvas: function TilinPattern_createPatternCanvas(owner) {
+ var operatorList = this.operatorList;
+ var bbox = this.bbox;
+ var xstep = this.xstep;
+ var ystep = this.ystep;
+ var paintType = this.paintType;
+ var tilingType = this.tilingType;
+ var color = this.color;
+ var canvasGraphicsFactory = this.canvasGraphicsFactory;
+ (0, _util.info)('TilingType: ' + tilingType);
+ var x0 = bbox[0],
+ y0 = bbox[1],
+ x1 = bbox[2],
+ y1 = bbox[3];
+
+ var matrixScale = _util.Util.singularValueDecompose2dScale(this.matrix);
+
+ var curMatrixScale = _util.Util.singularValueDecompose2dScale(this.baseTransform);
+
+ var combinedScale = [matrixScale[0] * curMatrixScale[0], matrixScale[1] * curMatrixScale[1]];
+ var dimx = this.getSizeAndScale(xstep, this.ctx.canvas.width, combinedScale[0]);
+ var dimy = this.getSizeAndScale(ystep, this.ctx.canvas.height, combinedScale[1]);
+ var tmpCanvas = owner.cachedCanvases.getCanvas('pattern', dimx.size, dimy.size, true);
+ var tmpCtx = tmpCanvas.context;
+ var graphics = canvasGraphicsFactory.createCanvasGraphics(tmpCtx);
+ graphics.groupLevel = owner.groupLevel;
+ this.setFillAndStrokeStyleToContext(graphics, paintType, color);
+ graphics.transform(dimx.scale, 0, 0, dimy.scale, 0, 0);
+ graphics.transform(1, 0, 0, 1, -x0, -y0);
+ this.clipBbox(graphics, bbox, x0, y0, x1, y1);
+ graphics.executeOperatorList(operatorList);
+ this.ctx.transform(1, 0, 0, 1, x0, y0);
+ this.ctx.scale(1 / dimx.scale, 1 / dimy.scale);
+ return tmpCanvas.canvas;
+ },
+ getSizeAndScale: function TilingPattern_getSizeAndScale(step, realOutputSize, scale) {
+ step = Math.abs(step);
+ var maxSize = Math.max(MAX_PATTERN_SIZE, realOutputSize);
+ var size = Math.ceil(step * scale);
+
+ if (size >= maxSize) {
+ size = maxSize;
+ } else {
+ scale = size / step;
+ }
+
+ return {
+ scale: scale,
+ size: size
+ };
+ },
+ clipBbox: function clipBbox(graphics, bbox, x0, y0, x1, y1) {
+ if (Array.isArray(bbox) && bbox.length === 4) {
+ var bboxWidth = x1 - x0;
+ var bboxHeight = y1 - y0;
+ graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight);
+ graphics.clip();
+ graphics.endPath();
+ }
+ },
+ setFillAndStrokeStyleToContext: function setFillAndStrokeStyleToContext(graphics, paintType, color) {
+ var context = graphics.ctx,
+ current = graphics.current;
+
+ switch (paintType) {
+ case PaintType.COLORED:
+ var ctx = this.ctx;
+ context.fillStyle = ctx.fillStyle;
+ context.strokeStyle = ctx.strokeStyle;
+ current.fillColor = ctx.fillStyle;
+ current.strokeColor = ctx.strokeStyle;
+ break;
+
+ case PaintType.UNCOLORED:
+ var cssColor = _util.Util.makeCssRgb(color[0], color[1], color[2]);
+
+ context.fillStyle = cssColor;
+ context.strokeStyle = cssColor;
+ current.fillColor = cssColor;
+ current.strokeColor = cssColor;
+ break;
+
+ default:
+ throw new _util.FormatError("Unsupported paint type: ".concat(paintType));
+ }
+ },
+ getPattern: function TilingPattern_getPattern(ctx, owner) {
+ ctx = this.ctx;
+ ctx.setTransform.apply(ctx, this.baseTransform);
+ ctx.transform.apply(ctx, this.matrix);
+ var temporaryPatternCanvas = this.createPatternCanvas(owner);
+ return ctx.createPattern(temporaryPatternCanvas, 'repeat');
+ }
+ };
+ return TilingPattern;
+}();
+
+exports.TilingPattern = TilingPattern;
+
+/***/ }),
+/* 156 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.GlobalWorkerOptions = void 0;
+var GlobalWorkerOptions = Object.create(null);
+exports.GlobalWorkerOptions = GlobalWorkerOptions;
+GlobalWorkerOptions.workerPort = GlobalWorkerOptions.workerPort === undefined ? null : GlobalWorkerOptions.workerPort;
+GlobalWorkerOptions.workerSrc = GlobalWorkerOptions.workerSrc === undefined ? '' : GlobalWorkerOptions.workerSrc;
+
+/***/ }),
+/* 157 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.MessageHandler = MessageHandler;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(148));
+
+var _util = __w_pdfjs_require__(1);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+function resolveCall(_x, _x2) {
+ return _resolveCall.apply(this, arguments);
+}
+
+function _resolveCall() {
+ _resolveCall = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee(fn, args) {
+ var thisArg,
+ _args = arguments;
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ thisArg = _args.length > 2 && _args[2] !== undefined ? _args[2] : null;
+
+ if (fn) {
+ _context.next = 3;
+ break;
+ }
+
+ return _context.abrupt("return", undefined);
+
+ case 3:
+ return _context.abrupt("return", fn.apply(thisArg, args));
+
+ case 4:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee);
+ }));
+ return _resolveCall.apply(this, arguments);
+}
+
+function wrapReason(reason) {
+ if (_typeof(reason) !== 'object') {
+ return reason;
+ }
+
+ switch (reason.name) {
+ case 'AbortException':
+ return new _util.AbortException(reason.message);
+
+ case 'MissingPDFException':
+ return new _util.MissingPDFException(reason.message);
+
+ case 'UnexpectedResponseException':
+ return new _util.UnexpectedResponseException(reason.message, reason.status);
+
+ default:
+ return new _util.UnknownErrorException(reason.message, reason.details);
+ }
+}
+
+function makeReasonSerializable(reason) {
+ if (!(reason instanceof Error) || reason instanceof _util.AbortException || reason instanceof _util.MissingPDFException || reason instanceof _util.UnexpectedResponseException || reason instanceof _util.UnknownErrorException) {
+ return reason;
+ }
+
+ return new _util.UnknownErrorException(reason.message, reason.toString());
+}
+
+function resolveOrReject(capability, success, reason) {
+ if (success) {
+ capability.resolve();
+ } else {
+ capability.reject(reason);
+ }
+}
+
+function finalize(promise) {
+ return Promise.resolve(promise)["catch"](function () {});
+}
+
+function MessageHandler(sourceName, targetName, comObj) {
+ var _this = this;
+
+ this.sourceName = sourceName;
+ this.targetName = targetName;
+ this.comObj = comObj;
+ this.callbackId = 1;
+ this.streamId = 1;
+ this.postMessageTransfers = true;
+ this.streamSinks = Object.create(null);
+ this.streamControllers = Object.create(null);
+ var callbacksCapabilities = this.callbacksCapabilities = Object.create(null);
+ var ah = this.actionHandler = Object.create(null);
+
+ this._onComObjOnMessage = function (event) {
+ var data = event.data;
+
+ if (data.targetName !== _this.sourceName) {
+ return;
+ }
+
+ if (data.stream) {
+ _this._processStreamMessage(data);
+ } else if (data.isReply) {
+ var callbackId = data.callbackId;
+
+ if (data.callbackId in callbacksCapabilities) {
+ var callback = callbacksCapabilities[callbackId];
+ delete callbacksCapabilities[callbackId];
+
+ if ('error' in data) {
+ callback.reject(wrapReason(data.error));
+ } else {
+ callback.resolve(data.data);
+ }
+ } else {
+ throw new Error("Cannot resolve callback ".concat(callbackId));
+ }
+ } else if (data.action in ah) {
+ var action = ah[data.action];
+
+ if (data.callbackId) {
+ var _sourceName = _this.sourceName;
+ var _targetName = data.sourceName;
+ Promise.resolve().then(function () {
+ return action[0].call(action[1], data.data);
+ }).then(function (result) {
+ comObj.postMessage({
+ sourceName: _sourceName,
+ targetName: _targetName,
+ isReply: true,
+ callbackId: data.callbackId,
+ data: result
+ });
+ }, function (reason) {
+ comObj.postMessage({
+ sourceName: _sourceName,
+ targetName: _targetName,
+ isReply: true,
+ callbackId: data.callbackId,
+ error: makeReasonSerializable(reason)
+ });
+ });
+ } else if (data.streamId) {
+ _this._createStreamSink(data);
+ } else {
+ action[0].call(action[1], data.data);
+ }
+ } else {
+ throw new Error("Unknown action from worker: ".concat(data.action));
+ }
+ };
+
+ comObj.addEventListener('message', this._onComObjOnMessage);
+}
+
+MessageHandler.prototype = {
+ on: function on(actionName, handler, scope) {
+ var ah = this.actionHandler;
+
+ if (ah[actionName]) {
+ throw new Error("There is already an actionName called \"".concat(actionName, "\""));
+ }
+
+ ah[actionName] = [handler, scope];
+ },
+ send: function send(actionName, data, transfers) {
+ var message = {
+ sourceName: this.sourceName,
+ targetName: this.targetName,
+ action: actionName,
+ data: data
+ };
+ this.postMessage(message, transfers);
+ },
+ sendWithPromise: function sendWithPromise(actionName, data, transfers) {
+ var callbackId = this.callbackId++;
+ var message = {
+ sourceName: this.sourceName,
+ targetName: this.targetName,
+ action: actionName,
+ data: data,
+ callbackId: callbackId
+ };
+ var capability = (0, _util.createPromiseCapability)();
+ this.callbacksCapabilities[callbackId] = capability;
+
+ try {
+ this.postMessage(message, transfers);
+ } catch (e) {
+ capability.reject(e);
+ }
+
+ return capability.promise;
+ },
+ sendWithStream: function sendWithStream(actionName, data, queueingStrategy, transfers) {
+ var _this2 = this;
+
+ var streamId = this.streamId++;
+ var sourceName = this.sourceName;
+ var targetName = this.targetName;
+ return new _util.ReadableStream({
+ start: function start(controller) {
+ var startCapability = (0, _util.createPromiseCapability)();
+ _this2.streamControllers[streamId] = {
+ controller: controller,
+ startCall: startCapability,
+ isClosed: false
+ };
+
+ _this2.postMessage({
+ sourceName: sourceName,
+ targetName: targetName,
+ action: actionName,
+ streamId: streamId,
+ data: data,
+ desiredSize: controller.desiredSize
+ });
+
+ return startCapability.promise;
+ },
+ pull: function pull(controller) {
+ var pullCapability = (0, _util.createPromiseCapability)();
+ _this2.streamControllers[streamId].pullCall = pullCapability;
+
+ _this2.postMessage({
+ sourceName: sourceName,
+ targetName: targetName,
+ stream: 'pull',
+ streamId: streamId,
+ desiredSize: controller.desiredSize
+ });
+
+ return pullCapability.promise;
+ },
+ cancel: function cancel(reason) {
+ var cancelCapability = (0, _util.createPromiseCapability)();
+ _this2.streamControllers[streamId].cancelCall = cancelCapability;
+ _this2.streamControllers[streamId].isClosed = true;
+
+ _this2.postMessage({
+ sourceName: sourceName,
+ targetName: targetName,
+ stream: 'cancel',
+ reason: reason,
+ streamId: streamId
+ });
+
+ return cancelCapability.promise;
+ }
+ }, queueingStrategy);
+ },
+ _createStreamSink: function _createStreamSink(data) {
+ var _this3 = this;
+
+ var self = this;
+ var action = this.actionHandler[data.action];
+ var streamId = data.streamId;
+ var desiredSize = data.desiredSize;
+ var sourceName = this.sourceName;
+ var targetName = data.sourceName;
+ var capability = (0, _util.createPromiseCapability)();
+
+ var sendStreamRequest = function sendStreamRequest(_ref) {
+ var stream = _ref.stream,
+ chunk = _ref.chunk,
+ transfers = _ref.transfers,
+ success = _ref.success,
+ reason = _ref.reason;
+
+ _this3.postMessage({
+ sourceName: sourceName,
+ targetName: targetName,
+ stream: stream,
+ streamId: streamId,
+ chunk: chunk,
+ success: success,
+ reason: reason
+ }, transfers);
+ };
+
+ var streamSink = {
+ enqueue: function enqueue(chunk) {
+ var size = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
+ var transfers = arguments.length > 2 ? arguments[2] : undefined;
+
+ if (this.isCancelled) {
+ return;
+ }
+
+ var lastDesiredSize = this.desiredSize;
+ this.desiredSize -= size;
+
+ if (lastDesiredSize > 0 && this.desiredSize <= 0) {
+ this.sinkCapability = (0, _util.createPromiseCapability)();
+ this.ready = this.sinkCapability.promise;
+ }
+
+ sendStreamRequest({
+ stream: 'enqueue',
+ chunk: chunk,
+ transfers: transfers
+ });
+ },
+ close: function close() {
+ if (this.isCancelled) {
+ return;
+ }
+
+ this.isCancelled = true;
+ sendStreamRequest({
+ stream: 'close'
+ });
+ delete self.streamSinks[streamId];
+ },
+ error: function error(reason) {
+ if (this.isCancelled) {
+ return;
+ }
+
+ this.isCancelled = true;
+ sendStreamRequest({
+ stream: 'error',
+ reason: reason
+ });
+ },
+ sinkCapability: capability,
+ onPull: null,
+ onCancel: null,
+ isCancelled: false,
+ desiredSize: desiredSize,
+ ready: null
+ };
+ streamSink.sinkCapability.resolve();
+ streamSink.ready = streamSink.sinkCapability.promise;
+ this.streamSinks[streamId] = streamSink;
+ resolveCall(action[0], [data.data, streamSink], action[1]).then(function () {
+ sendStreamRequest({
+ stream: 'start_complete',
+ success: true
+ });
+ }, function (reason) {
+ sendStreamRequest({
+ stream: 'start_complete',
+ success: false,
+ reason: reason
+ });
+ });
+ },
+ _processStreamMessage: function _processStreamMessage(data) {
+ var _this4 = this;
+
+ var sourceName = this.sourceName;
+ var targetName = data.sourceName;
+ var streamId = data.streamId;
+
+ var sendStreamResponse = function sendStreamResponse(_ref2) {
+ var stream = _ref2.stream,
+ success = _ref2.success,
+ reason = _ref2.reason;
+
+ _this4.comObj.postMessage({
+ sourceName: sourceName,
+ targetName: targetName,
+ stream: stream,
+ success: success,
+ streamId: streamId,
+ reason: reason
+ });
+ };
+
+ var deleteStreamController = function deleteStreamController() {
+ Promise.all([_this4.streamControllers[data.streamId].startCall, _this4.streamControllers[data.streamId].pullCall, _this4.streamControllers[data.streamId].cancelCall].map(function (capability) {
+ return capability && finalize(capability.promise);
+ })).then(function () {
+ delete _this4.streamControllers[data.streamId];
+ });
+ };
+
+ switch (data.stream) {
+ case 'start_complete':
+ resolveOrReject(this.streamControllers[data.streamId].startCall, data.success, wrapReason(data.reason));
+ break;
+
+ case 'pull_complete':
+ resolveOrReject(this.streamControllers[data.streamId].pullCall, data.success, wrapReason(data.reason));
+ break;
+
+ case 'pull':
+ if (!this.streamSinks[data.streamId]) {
+ sendStreamResponse({
+ stream: 'pull_complete',
+ success: true
+ });
+ break;
+ }
+
+ if (this.streamSinks[data.streamId].desiredSize <= 0 && data.desiredSize > 0) {
+ this.streamSinks[data.streamId].sinkCapability.resolve();
+ }
+
+ this.streamSinks[data.streamId].desiredSize = data.desiredSize;
+ resolveCall(this.streamSinks[data.streamId].onPull).then(function () {
+ sendStreamResponse({
+ stream: 'pull_complete',
+ success: true
+ });
+ }, function (reason) {
+ sendStreamResponse({
+ stream: 'pull_complete',
+ success: false,
+ reason: reason
+ });
+ });
+ break;
+
+ case 'enqueue':
+ (0, _util.assert)(this.streamControllers[data.streamId], 'enqueue should have stream controller');
+
+ if (!this.streamControllers[data.streamId].isClosed) {
+ this.streamControllers[data.streamId].controller.enqueue(data.chunk);
+ }
+
+ break;
+
+ case 'close':
+ (0, _util.assert)(this.streamControllers[data.streamId], 'close should have stream controller');
+
+ if (this.streamControllers[data.streamId].isClosed) {
+ break;
+ }
+
+ this.streamControllers[data.streamId].isClosed = true;
+ this.streamControllers[data.streamId].controller.close();
+ deleteStreamController();
+ break;
+
+ case 'error':
+ (0, _util.assert)(this.streamControllers[data.streamId], 'error should have stream controller');
+ this.streamControllers[data.streamId].controller.error(wrapReason(data.reason));
+ deleteStreamController();
+ break;
+
+ case 'cancel_complete':
+ resolveOrReject(this.streamControllers[data.streamId].cancelCall, data.success, wrapReason(data.reason));
+ deleteStreamController();
+ break;
+
+ case 'cancel':
+ if (!this.streamSinks[data.streamId]) {
+ break;
+ }
+
+ resolveCall(this.streamSinks[data.streamId].onCancel, [wrapReason(data.reason)]).then(function () {
+ sendStreamResponse({
+ stream: 'cancel_complete',
+ success: true
+ });
+ }, function (reason) {
+ sendStreamResponse({
+ stream: 'cancel_complete',
+ success: false,
+ reason: reason
+ });
+ });
+ this.streamSinks[data.streamId].sinkCapability.reject(wrapReason(data.reason));
+ this.streamSinks[data.streamId].isCancelled = true;
+ delete this.streamSinks[data.streamId];
+ break;
+
+ default:
+ throw new Error('Unexpected stream case');
+ }
+ },
+ postMessage: function postMessage(message, transfers) {
+ if (transfers && this.postMessageTransfers) {
+ this.comObj.postMessage(message, transfers);
+ } else {
+ this.comObj.postMessage(message);
+ }
+ },
+ destroy: function destroy() {
+ this.comObj.removeEventListener('message', this._onComObjOnMessage);
+ }
+};
+
+/***/ }),
+/* 158 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.Metadata = void 0;
+
+var _util = __w_pdfjs_require__(1);
+
+var _xml_parser = __w_pdfjs_require__(159);
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var Metadata =
+/*#__PURE__*/
+function () {
+ function Metadata(data) {
+ _classCallCheck(this, Metadata);
+
+ (0, _util.assert)(typeof data === 'string', 'Metadata: input is not a string');
+ data = this._repair(data);
+ var parser = new _xml_parser.SimpleXMLParser();
+ var xmlDocument = parser.parseFromString(data);
+ this._metadata = Object.create(null);
+
+ if (xmlDocument) {
+ this._parse(xmlDocument);
+ }
+ }
+
+ _createClass(Metadata, [{
+ key: "_repair",
+ value: function _repair(data) {
+ return data.replace(/^([^<]+)/, '').replace(/>\\376\\377([^<]+)/g, function (all, codes) {
+ var bytes = codes.replace(/\\([0-3])([0-7])([0-7])/g, function (code, d1, d2, d3) {
+ return String.fromCharCode(d1 * 64 + d2 * 8 + d3 * 1);
+ }).replace(/&(amp|apos|gt|lt|quot);/g, function (str, name) {
+ switch (name) {
+ case 'amp':
+ return '&';
+
+ case 'apos':
+ return '\'';
+
+ case 'gt':
+ return '>';
+
+ case 'lt':
+ return '<';
+
+ case 'quot':
+ return '\"';
+ }
+
+ throw new Error("_repair: ".concat(name, " isn't defined."));
+ });
+ var chars = '';
+
+ for (var i = 0, ii = bytes.length; i < ii; i += 2) {
+ var code = bytes.charCodeAt(i) * 256 + bytes.charCodeAt(i + 1);
+
+ if (code >= 32 && code < 127 && code !== 60 && code !== 62 && code !== 38) {
+ chars += String.fromCharCode(code);
+ } else {
+ chars += '&#x' + (0x10000 + code).toString(16).substring(1) + ';';
+ }
+ }
+
+ return '>' + chars;
+ });
+ }
+ }, {
+ key: "_parse",
+ value: function _parse(xmlDocument) {
+ var rdf = xmlDocument.documentElement;
+
+ if (rdf.nodeName.toLowerCase() !== 'rdf:rdf') {
+ rdf = rdf.firstChild;
+
+ while (rdf && rdf.nodeName.toLowerCase() !== 'rdf:rdf') {
+ rdf = rdf.nextSibling;
+ }
+ }
+
+ var nodeName = rdf ? rdf.nodeName.toLowerCase() : null;
+
+ if (!rdf || nodeName !== 'rdf:rdf' || !rdf.hasChildNodes()) {
+ return;
+ }
+
+ var children = rdf.childNodes;
+
+ for (var i = 0, ii = children.length; i < ii; i++) {
+ var desc = children[i];
+
+ if (desc.nodeName.toLowerCase() !== 'rdf:description') {
+ continue;
+ }
+
+ for (var j = 0, jj = desc.childNodes.length; j < jj; j++) {
+ if (desc.childNodes[j].nodeName.toLowerCase() !== '#text') {
+ var entry = desc.childNodes[j];
+ var name = entry.nodeName.toLowerCase();
+ this._metadata[name] = entry.textContent.trim();
+ }
+ }
+ }
+ }
+ }, {
+ key: "get",
+ value: function get(name) {
+ var data = this._metadata[name];
+ return typeof data !== 'undefined' ? data : null;
+ }
+ }, {
+ key: "getAll",
+ value: function getAll() {
+ return this._metadata;
+ }
+ }, {
+ key: "has",
+ value: function has(name) {
+ return typeof this._metadata[name] !== 'undefined';
+ }
+ }]);
+
+ return Metadata;
+}();
+
+exports.Metadata = Metadata;
+
+/***/ }),
+/* 159 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.SimpleXMLParser = void 0;
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
+
+function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
+
+function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
+
+function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
+
+function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
+
+function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
+
+function _get(target, property, receiver) { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get; } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(receiver); } return desc.value; }; } return _get(target, property, receiver || target); }
+
+function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = _getPrototypeOf(object); if (object === null) break; } return object; }
+
+function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
+
+function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var XMLParserErrorCode = {
+ NoError: 0,
+ EndOfDocument: -1,
+ UnterminatedCdat: -2,
+ UnterminatedXmlDeclaration: -3,
+ UnterminatedDoctypeDeclaration: -4,
+ UnterminatedComment: -5,
+ MalformedElement: -6,
+ OutOfMemory: -7,
+ UnterminatedAttributeValue: -8,
+ UnterminatedElement: -9,
+ ElementNeverBegun: -10
+};
+
+function isWhitespace(s, index) {
+ var ch = s[index];
+ return ch === ' ' || ch === '\n' || ch === '\r' || ch === '\t';
+}
+
+function isWhitespaceString(s) {
+ for (var i = 0, ii = s.length; i < ii; i++) {
+ if (!isWhitespace(s, i)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+var XMLParserBase =
+/*#__PURE__*/
+function () {
+ function XMLParserBase() {
+ _classCallCheck(this, XMLParserBase);
+ }
+
+ _createClass(XMLParserBase, [{
+ key: "_resolveEntities",
+ value: function _resolveEntities(s) {
+ var _this = this;
+
+ return s.replace(/&([^;]+);/g, function (all, entity) {
+ if (entity.substring(0, 2) === '#x') {
+ return String.fromCharCode(parseInt(entity.substring(2), 16));
+ } else if (entity.substring(0, 1) === '#') {
+ return String.fromCharCode(parseInt(entity.substring(1), 10));
+ }
+
+ switch (entity) {
+ case 'lt':
+ return '<';
+
+ case 'gt':
+ return '>';
+
+ case 'amp':
+ return '&';
+
+ case 'quot':
+ return '\"';
+ }
+
+ return _this.onResolveEntity(entity);
+ });
+ }
+ }, {
+ key: "_parseContent",
+ value: function _parseContent(s, start) {
+ var pos = start,
+ name,
+ attributes = [];
+
+ function skipWs() {
+ while (pos < s.length && isWhitespace(s, pos)) {
+ ++pos;
+ }
+ }
+
+ while (pos < s.length && !isWhitespace(s, pos) && s[pos] !== '>' && s[pos] !== '/') {
+ ++pos;
+ }
+
+ name = s.substring(start, pos);
+ skipWs();
+
+ while (pos < s.length && s[pos] !== '>' && s[pos] !== '/' && s[pos] !== '?') {
+ skipWs();
+ var attrName = '',
+ attrValue = '';
+
+ while (pos < s.length && !isWhitespace(s, pos) && s[pos] !== '=') {
+ attrName += s[pos];
+ ++pos;
+ }
+
+ skipWs();
+
+ if (s[pos] !== '=') {
+ return null;
+ }
+
+ ++pos;
+ skipWs();
+ var attrEndChar = s[pos];
+
+ if (attrEndChar !== '\"' && attrEndChar !== '\'') {
+ return null;
+ }
+
+ var attrEndIndex = s.indexOf(attrEndChar, ++pos);
+
+ if (attrEndIndex < 0) {
+ return null;
+ }
+
+ attrValue = s.substring(pos, attrEndIndex);
+ attributes.push({
+ name: attrName,
+ value: this._resolveEntities(attrValue)
+ });
+ pos = attrEndIndex + 1;
+ skipWs();
+ }
+
+ return {
+ name: name,
+ attributes: attributes,
+ parsed: pos - start
+ };
+ }
+ }, {
+ key: "_parseProcessingInstruction",
+ value: function _parseProcessingInstruction(s, start) {
+ var pos = start,
+ name,
+ value;
+
+ function skipWs() {
+ while (pos < s.length && isWhitespace(s, pos)) {
+ ++pos;
+ }
+ }
+
+ while (pos < s.length && !isWhitespace(s, pos) && s[pos] !== '>' && s[pos] !== '/') {
+ ++pos;
+ }
+
+ name = s.substring(start, pos);
+ skipWs();
+ var attrStart = pos;
+
+ while (pos < s.length && (s[pos] !== '?' || s[pos + 1] !== '>')) {
+ ++pos;
+ }
+
+ value = s.substring(attrStart, pos);
+ return {
+ name: name,
+ value: value,
+ parsed: pos - start
+ };
+ }
+ }, {
+ key: "parseXml",
+ value: function parseXml(s) {
+ var i = 0;
+
+ while (i < s.length) {
+ var ch = s[i];
+ var j = i;
+
+ if (ch === '<') {
+ ++j;
+ var ch2 = s[j];
+ var q = void 0;
+
+ switch (ch2) {
+ case '/':
+ ++j;
+ q = s.indexOf('>', j);
+
+ if (q < 0) {
+ this.onError(XMLParserErrorCode.UnterminatedElement);
+ return;
+ }
+
+ this.onEndElement(s.substring(j, q));
+ j = q + 1;
+ break;
+
+ case '?':
+ ++j;
+
+ var pi = this._parseProcessingInstruction(s, j);
+
+ if (s.substring(j + pi.parsed, j + pi.parsed + 2) !== '?>') {
+ this.onError(XMLParserErrorCode.UnterminatedXmlDeclaration);
+ return;
+ }
+
+ this.onPi(pi.name, pi.value);
+ j += pi.parsed + 2;
+ break;
+
+ case '!':
+ if (s.substring(j + 1, j + 3) === '--') {
+ q = s.indexOf('-->', j + 3);
+
+ if (q < 0) {
+ this.onError(XMLParserErrorCode.UnterminatedComment);
+ return;
+ }
+
+ this.onComment(s.substring(j + 3, q));
+ j = q + 3;
+ } else if (s.substring(j + 1, j + 8) === '[CDATA[') {
+ q = s.indexOf(']]>', j + 8);
+
+ if (q < 0) {
+ this.onError(XMLParserErrorCode.UnterminatedCdat);
+ return;
+ }
+
+ this.onCdata(s.substring(j + 8, q));
+ j = q + 3;
+ } else if (s.substring(j + 1, j + 8) === 'DOCTYPE') {
+ var q2 = s.indexOf('[', j + 8);
+ var complexDoctype = false;
+ q = s.indexOf('>', j + 8);
+
+ if (q < 0) {
+ this.onError(XMLParserErrorCode.UnterminatedDoctypeDeclaration);
+ return;
+ }
+
+ if (q2 > 0 && q > q2) {
+ q = s.indexOf(']>', j + 8);
+
+ if (q < 0) {
+ this.onError(XMLParserErrorCode.UnterminatedDoctypeDeclaration);
+ return;
+ }
+
+ complexDoctype = true;
+ }
+
+ var doctypeContent = s.substring(j + 8, q + (complexDoctype ? 1 : 0));
+ this.onDoctype(doctypeContent);
+ j = q + (complexDoctype ? 2 : 1);
+ } else {
+ this.onError(XMLParserErrorCode.MalformedElement);
+ return;
+ }
+
+ break;
+
+ default:
+ var content = this._parseContent(s, j);
+
+ if (content === null) {
+ this.onError(XMLParserErrorCode.MalformedElement);
+ return;
+ }
+
+ var isClosed = false;
+
+ if (s.substring(j + content.parsed, j + content.parsed + 2) === '/>') {
+ isClosed = true;
+ } else if (s.substring(j + content.parsed, j + content.parsed + 1) !== '>') {
+ this.onError(XMLParserErrorCode.UnterminatedElement);
+ return;
+ }
+
+ this.onBeginElement(content.name, content.attributes, isClosed);
+ j += content.parsed + (isClosed ? 2 : 1);
+ break;
+ }
+ } else {
+ while (j < s.length && s[j] !== '<') {
+ j++;
+ }
+
+ var text = s.substring(i, j);
+ this.onText(this._resolveEntities(text));
+ }
+
+ i = j;
+ }
+ }
+ }, {
+ key: "onResolveEntity",
+ value: function onResolveEntity(name) {
+ return "&".concat(name, ";");
+ }
+ }, {
+ key: "onPi",
+ value: function onPi(name, value) {}
+ }, {
+ key: "onComment",
+ value: function onComment(text) {}
+ }, {
+ key: "onCdata",
+ value: function onCdata(text) {}
+ }, {
+ key: "onDoctype",
+ value: function onDoctype(doctypeContent) {}
+ }, {
+ key: "onText",
+ value: function onText(text) {}
+ }, {
+ key: "onBeginElement",
+ value: function onBeginElement(name, attributes, isEmpty) {}
+ }, {
+ key: "onEndElement",
+ value: function onEndElement(name) {}
+ }, {
+ key: "onError",
+ value: function onError(code) {}
+ }]);
+
+ return XMLParserBase;
+}();
+
+var SimpleDOMNode =
+/*#__PURE__*/
+function () {
+ function SimpleDOMNode(nodeName, nodeValue) {
+ _classCallCheck(this, SimpleDOMNode);
+
+ this.nodeName = nodeName;
+ this.nodeValue = nodeValue;
+ Object.defineProperty(this, 'parentNode', {
+ value: null,
+ writable: true
+ });
+ }
+
+ _createClass(SimpleDOMNode, [{
+ key: "hasChildNodes",
+ value: function hasChildNodes() {
+ return this.childNodes && this.childNodes.length > 0;
+ }
+ }, {
+ key: "firstChild",
+ get: function get() {
+ return this.childNodes && this.childNodes[0];
+ }
+ }, {
+ key: "nextSibling",
+ get: function get() {
+ var childNodes = this.parentNode.childNodes;
+
+ if (!childNodes) {
+ return undefined;
+ }
+
+ var index = childNodes.indexOf(this);
+
+ if (index === -1) {
+ return undefined;
+ }
+
+ return childNodes[index + 1];
+ }
+ }, {
+ key: "textContent",
+ get: function get() {
+ if (!this.childNodes) {
+ return this.nodeValue || '';
+ }
+
+ return this.childNodes.map(function (child) {
+ return child.textContent;
+ }).join('');
+ }
+ }]);
+
+ return SimpleDOMNode;
+}();
+
+var SimpleXMLParser =
+/*#__PURE__*/
+function (_XMLParserBase) {
+ _inherits(SimpleXMLParser, _XMLParserBase);
+
+ function SimpleXMLParser() {
+ var _this2;
+
+ _classCallCheck(this, SimpleXMLParser);
+
+ _this2 = _possibleConstructorReturn(this, _getPrototypeOf(SimpleXMLParser).call(this));
+ _this2._currentFragment = null;
+ _this2._stack = null;
+ _this2._errorCode = XMLParserErrorCode.NoError;
+ return _this2;
+ }
+
+ _createClass(SimpleXMLParser, [{
+ key: "parseFromString",
+ value: function parseFromString(data) {
+ this._currentFragment = [];
+ this._stack = [];
+ this._errorCode = XMLParserErrorCode.NoError;
+ this.parseXml(data);
+
+ if (this._errorCode !== XMLParserErrorCode.NoError) {
+ return undefined;
+ }
+
+ var _this$_currentFragmen = _slicedToArray(this._currentFragment, 1),
+ documentElement = _this$_currentFragmen[0];
+
+ if (!documentElement) {
+ return undefined;
+ }
+
+ return {
+ documentElement: documentElement
+ };
+ }
+ }, {
+ key: "onResolveEntity",
+ value: function onResolveEntity(name) {
+ switch (name) {
+ case 'apos':
+ return '\'';
+ }
+
+ return _get(_getPrototypeOf(SimpleXMLParser.prototype), "onResolveEntity", this).call(this, name);
+ }
+ }, {
+ key: "onText",
+ value: function onText(text) {
+ if (isWhitespaceString(text)) {
+ return;
+ }
+
+ var node = new SimpleDOMNode('#text', text);
+
+ this._currentFragment.push(node);
+ }
+ }, {
+ key: "onCdata",
+ value: function onCdata(text) {
+ var node = new SimpleDOMNode('#text', text);
+
+ this._currentFragment.push(node);
+ }
+ }, {
+ key: "onBeginElement",
+ value: function onBeginElement(name, attributes, isEmpty) {
+ var node = new SimpleDOMNode(name);
+ node.childNodes = [];
+
+ this._currentFragment.push(node);
+
+ if (isEmpty) {
+ return;
+ }
+
+ this._stack.push(this._currentFragment);
+
+ this._currentFragment = node.childNodes;
+ }
+ }, {
+ key: "onEndElement",
+ value: function onEndElement(name) {
+ this._currentFragment = this._stack.pop() || [];
+ var lastElement = this._currentFragment[this._currentFragment.length - 1];
+
+ if (!lastElement) {
+ return;
+ }
+
+ for (var i = 0, ii = lastElement.childNodes.length; i < ii; i++) {
+ lastElement.childNodes[i].parentNode = lastElement;
+ }
+ }
+ }, {
+ key: "onError",
+ value: function onError(code) {
+ this._errorCode = code;
+ }
+ }]);
+
+ return SimpleXMLParser;
+}(XMLParserBase);
+
+exports.SimpleXMLParser = SimpleXMLParser;
+
+/***/ }),
+/* 160 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.PDFDataTransportStream = void 0;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(148));
+
+var _util = __w_pdfjs_require__(1);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var PDFDataTransportStream =
+/*#__PURE__*/
+function () {
+ function PDFDataTransportStream(params, pdfDataRangeTransport) {
+ var _this = this;
+
+ _classCallCheck(this, PDFDataTransportStream);
+
+ (0, _util.assert)(pdfDataRangeTransport);
+ this._queuedChunks = [];
+ this._progressiveDone = params.progressiveDone || false;
+ var initialData = params.initialData;
+
+ if (initialData && initialData.length > 0) {
+ var buffer = new Uint8Array(initialData).buffer;
+
+ this._queuedChunks.push(buffer);
+ }
+
+ this._pdfDataRangeTransport = pdfDataRangeTransport;
+ this._isStreamingSupported = !params.disableStream;
+ this._isRangeSupported = !params.disableRange;
+ this._contentLength = params.length;
+ this._fullRequestReader = null;
+ this._rangeReaders = [];
+
+ this._pdfDataRangeTransport.addRangeListener(function (begin, chunk) {
+ _this._onReceiveData({
+ begin: begin,
+ chunk: chunk
+ });
+ });
+
+ this._pdfDataRangeTransport.addProgressListener(function (loaded, total) {
+ _this._onProgress({
+ loaded: loaded,
+ total: total
+ });
+ });
+
+ this._pdfDataRangeTransport.addProgressiveReadListener(function (chunk) {
+ _this._onReceiveData({
+ chunk: chunk
+ });
+ });
+
+ this._pdfDataRangeTransport.addProgressiveDoneListener(function () {
+ _this._onProgressiveDone();
+ });
+
+ this._pdfDataRangeTransport.transportReady();
+ }
+
+ _createClass(PDFDataTransportStream, [{
+ key: "_onReceiveData",
+ value: function _onReceiveData(args) {
+ var buffer = new Uint8Array(args.chunk).buffer;
+
+ if (args.begin === undefined) {
+ if (this._fullRequestReader) {
+ this._fullRequestReader._enqueue(buffer);
+ } else {
+ this._queuedChunks.push(buffer);
+ }
+ } else {
+ var found = this._rangeReaders.some(function (rangeReader) {
+ if (rangeReader._begin !== args.begin) {
+ return false;
+ }
+
+ rangeReader._enqueue(buffer);
+
+ return true;
+ });
+
+ (0, _util.assert)(found);
+ }
+ }
+ }, {
+ key: "_onProgress",
+ value: function _onProgress(evt) {
+ if (evt.total === undefined) {
+ var firstReader = this._rangeReaders[0];
+
+ if (firstReader && firstReader.onProgress) {
+ firstReader.onProgress({
+ loaded: evt.loaded
+ });
+ }
+ } else {
+ var fullReader = this._fullRequestReader;
+
+ if (fullReader && fullReader.onProgress) {
+ fullReader.onProgress({
+ loaded: evt.loaded,
+ total: evt.total
+ });
+ }
+ }
+ }
+ }, {
+ key: "_onProgressiveDone",
+ value: function _onProgressiveDone() {
+ if (this._fullRequestReader) {
+ this._fullRequestReader.progressiveDone();
+ }
+
+ this._progressiveDone = true;
+ }
+ }, {
+ key: "_removeRangeReader",
+ value: function _removeRangeReader(reader) {
+ var i = this._rangeReaders.indexOf(reader);
+
+ if (i >= 0) {
+ this._rangeReaders.splice(i, 1);
+ }
+ }
+ }, {
+ key: "getFullReader",
+ value: function getFullReader() {
+ (0, _util.assert)(!this._fullRequestReader);
+ var queuedChunks = this._queuedChunks;
+ this._queuedChunks = null;
+ return new PDFDataTransportStreamReader(this, queuedChunks, this._progressiveDone);
+ }
+ }, {
+ key: "getRangeReader",
+ value: function getRangeReader(begin, end) {
+ if (end <= this._progressiveDataLength) {
+ return null;
+ }
+
+ var reader = new PDFDataTransportStreamRangeReader(this, begin, end);
+
+ this._pdfDataRangeTransport.requestDataRange(begin, end);
+
+ this._rangeReaders.push(reader);
+
+ return reader;
+ }
+ }, {
+ key: "cancelAllRequests",
+ value: function cancelAllRequests(reason) {
+ if (this._fullRequestReader) {
+ this._fullRequestReader.cancel(reason);
+ }
+
+ var readers = this._rangeReaders.slice(0);
+
+ readers.forEach(function (rangeReader) {
+ rangeReader.cancel(reason);
+ });
+
+ this._pdfDataRangeTransport.abort();
+ }
+ }, {
+ key: "_progressiveDataLength",
+ get: function get() {
+ return this._fullRequestReader ? this._fullRequestReader._loaded : 0;
+ }
+ }]);
+
+ return PDFDataTransportStream;
+}();
+
+exports.PDFDataTransportStream = PDFDataTransportStream;
+
+var PDFDataTransportStreamReader =
+/*#__PURE__*/
+function () {
+ function PDFDataTransportStreamReader(stream, queuedChunks) {
+ var progressiveDone = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+
+ _classCallCheck(this, PDFDataTransportStreamReader);
+
+ this._stream = stream;
+ this._done = progressiveDone || false;
+ this._filename = null;
+ this._queuedChunks = queuedChunks || [];
+ this._loaded = 0;
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = this._queuedChunks[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var chunk = _step.value;
+ this._loaded += chunk.byteLength;
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator["return"] != null) {
+ _iterator["return"]();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+
+ this._requests = [];
+ this._headersReady = Promise.resolve();
+ stream._fullRequestReader = this;
+ this.onProgress = null;
+ }
+
+ _createClass(PDFDataTransportStreamReader, [{
+ key: "_enqueue",
+ value: function _enqueue(chunk) {
+ if (this._done) {
+ return;
+ }
+
+ if (this._requests.length > 0) {
+ var requestCapability = this._requests.shift();
+
+ requestCapability.resolve({
+ value: chunk,
+ done: false
+ });
+ } else {
+ this._queuedChunks.push(chunk);
+ }
+
+ this._loaded += chunk.byteLength;
+ }
+ }, {
+ key: "read",
+ value: function () {
+ var _read = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee() {
+ var chunk, requestCapability;
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ if (!(this._queuedChunks.length > 0)) {
+ _context.next = 3;
+ break;
+ }
+
+ chunk = this._queuedChunks.shift();
+ return _context.abrupt("return", {
+ value: chunk,
+ done: false
+ });
+
+ case 3:
+ if (!this._done) {
+ _context.next = 5;
+ break;
+ }
+
+ return _context.abrupt("return", {
+ value: undefined,
+ done: true
+ });
+
+ case 5:
+ requestCapability = (0, _util.createPromiseCapability)();
+
+ this._requests.push(requestCapability);
+
+ return _context.abrupt("return", requestCapability.promise);
+
+ case 8:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee, this);
+ }));
+
+ function read() {
+ return _read.apply(this, arguments);
+ }
+
+ return read;
+ }()
+ }, {
+ key: "cancel",
+ value: function cancel(reason) {
+ this._done = true;
+
+ this._requests.forEach(function (requestCapability) {
+ requestCapability.resolve({
+ value: undefined,
+ done: true
+ });
+ });
+
+ this._requests = [];
+ }
+ }, {
+ key: "progressiveDone",
+ value: function progressiveDone() {
+ if (this._done) {
+ return;
+ }
+
+ this._done = true;
+ }
+ }, {
+ key: "headersReady",
+ get: function get() {
+ return this._headersReady;
+ }
+ }, {
+ key: "filename",
+ get: function get() {
+ return this._filename;
+ }
+ }, {
+ key: "isRangeSupported",
+ get: function get() {
+ return this._stream._isRangeSupported;
+ }
+ }, {
+ key: "isStreamingSupported",
+ get: function get() {
+ return this._stream._isStreamingSupported;
+ }
+ }, {
+ key: "contentLength",
+ get: function get() {
+ return this._stream._contentLength;
+ }
+ }]);
+
+ return PDFDataTransportStreamReader;
+}();
+
+var PDFDataTransportStreamRangeReader =
+/*#__PURE__*/
+function () {
+ function PDFDataTransportStreamRangeReader(stream, begin, end) {
+ _classCallCheck(this, PDFDataTransportStreamRangeReader);
+
+ this._stream = stream;
+ this._begin = begin;
+ this._end = end;
+ this._queuedChunk = null;
+ this._requests = [];
+ this._done = false;
+ this.onProgress = null;
+ }
+
+ _createClass(PDFDataTransportStreamRangeReader, [{
+ key: "_enqueue",
+ value: function _enqueue(chunk) {
+ if (this._done) {
+ return;
+ }
+
+ if (this._requests.length === 0) {
+ this._queuedChunk = chunk;
+ } else {
+ var requestsCapability = this._requests.shift();
+
+ requestsCapability.resolve({
+ value: chunk,
+ done: false
+ });
+
+ this._requests.forEach(function (requestCapability) {
+ requestCapability.resolve({
+ value: undefined,
+ done: true
+ });
+ });
+
+ this._requests = [];
+ }
+
+ this._done = true;
+
+ this._stream._removeRangeReader(this);
+ }
+ }, {
+ key: "read",
+ value: function () {
+ var _read2 = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee2() {
+ var chunk, requestCapability;
+ return _regenerator["default"].wrap(function _callee2$(_context2) {
+ while (1) {
+ switch (_context2.prev = _context2.next) {
+ case 0:
+ if (!this._queuedChunk) {
+ _context2.next = 4;
+ break;
+ }
+
+ chunk = this._queuedChunk;
+ this._queuedChunk = null;
+ return _context2.abrupt("return", {
+ value: chunk,
+ done: false
+ });
+
+ case 4:
+ if (!this._done) {
+ _context2.next = 6;
+ break;
+ }
+
+ return _context2.abrupt("return", {
+ value: undefined,
+ done: true
+ });
+
+ case 6:
+ requestCapability = (0, _util.createPromiseCapability)();
+
+ this._requests.push(requestCapability);
+
+ return _context2.abrupt("return", requestCapability.promise);
+
+ case 9:
+ case "end":
+ return _context2.stop();
+ }
+ }
+ }, _callee2, this);
+ }));
+
+ function read() {
+ return _read2.apply(this, arguments);
+ }
+
+ return read;
+ }()
+ }, {
+ key: "cancel",
+ value: function cancel(reason) {
+ this._done = true;
+
+ this._requests.forEach(function (requestCapability) {
+ requestCapability.resolve({
+ value: undefined,
+ done: true
+ });
+ });
+
+ this._requests = [];
+
+ this._stream._removeRangeReader(this);
+ }
+ }, {
+ key: "isStreamingSupported",
+ get: function get() {
+ return false;
+ }
+ }]);
+
+ return PDFDataTransportStreamRangeReader;
+}();
+
+/***/ }),
+/* 161 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.WebGLContext = void 0;
+
+var _util = __w_pdfjs_require__(1);
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var WebGLContext =
+/*#__PURE__*/
+function () {
+ function WebGLContext(_ref) {
+ var _ref$enable = _ref.enable,
+ enable = _ref$enable === void 0 ? false : _ref$enable;
+
+ _classCallCheck(this, WebGLContext);
+
+ this._enabled = enable === true;
+ }
+
+ _createClass(WebGLContext, [{
+ key: "composeSMask",
+ value: function composeSMask(_ref2) {
+ var layer = _ref2.layer,
+ mask = _ref2.mask,
+ properties = _ref2.properties;
+ return WebGLUtils.composeSMask(layer, mask, properties);
+ }
+ }, {
+ key: "drawFigures",
+ value: function drawFigures(_ref3) {
+ var width = _ref3.width,
+ height = _ref3.height,
+ backgroundColor = _ref3.backgroundColor,
+ figures = _ref3.figures,
+ context = _ref3.context;
+ return WebGLUtils.drawFigures(width, height, backgroundColor, figures, context);
+ }
+ }, {
+ key: "clear",
+ value: function clear() {
+ WebGLUtils.cleanup();
+ }
+ }, {
+ key: "isEnabled",
+ get: function get() {
+ var enabled = this._enabled;
+
+ if (enabled) {
+ enabled = WebGLUtils.tryInitGL();
+ }
+
+ return (0, _util.shadow)(this, 'isEnabled', enabled);
+ }
+ }]);
+
+ return WebGLContext;
+}();
+
+exports.WebGLContext = WebGLContext;
+
+var WebGLUtils = function WebGLUtilsClosure() {
+ function loadShader(gl, code, shaderType) {
+ var shader = gl.createShader(shaderType);
+ gl.shaderSource(shader, code);
+ gl.compileShader(shader);
+ var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
+
+ if (!compiled) {
+ var errorMsg = gl.getShaderInfoLog(shader);
+ throw new Error('Error during shader compilation: ' + errorMsg);
+ }
+
+ return shader;
+ }
+
+ function createVertexShader(gl, code) {
+ return loadShader(gl, code, gl.VERTEX_SHADER);
+ }
+
+ function createFragmentShader(gl, code) {
+ return loadShader(gl, code, gl.FRAGMENT_SHADER);
+ }
+
+ function createProgram(gl, shaders) {
+ var program = gl.createProgram();
+
+ for (var i = 0, ii = shaders.length; i < ii; ++i) {
+ gl.attachShader(program, shaders[i]);
+ }
+
+ gl.linkProgram(program);
+ var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
+
+ if (!linked) {
+ var errorMsg = gl.getProgramInfoLog(program);
+ throw new Error('Error during program linking: ' + errorMsg);
+ }
+
+ return program;
+ }
+
+ function createTexture(gl, image, textureId) {
+ gl.activeTexture(textureId);
+ var texture = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
+ return texture;
+ }
+
+ var currentGL, currentCanvas;
+
+ function generateGL() {
+ if (currentGL) {
+ return;
+ }
+
+ currentCanvas = document.createElement('canvas');
+ currentGL = currentCanvas.getContext('webgl', {
+ premultipliedalpha: false
+ });
+ }
+
+ var smaskVertexShaderCode = '\
+ attribute vec2 a_position; \
+ attribute vec2 a_texCoord; \
+ \
+ uniform vec2 u_resolution; \
+ \
+ varying vec2 v_texCoord; \
+ \
+ void main() { \
+ vec2 clipSpace = (a_position / u_resolution) * 2.0 - 1.0; \
+ gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); \
+ \
+ v_texCoord = a_texCoord; \
+ } ';
+ var smaskFragmentShaderCode = '\
+ precision mediump float; \
+ \
+ uniform vec4 u_backdrop; \
+ uniform int u_subtype; \
+ uniform sampler2D u_image; \
+ uniform sampler2D u_mask; \
+ \
+ varying vec2 v_texCoord; \
+ \
+ void main() { \
+ vec4 imageColor = texture2D(u_image, v_texCoord); \
+ vec4 maskColor = texture2D(u_mask, v_texCoord); \
+ if (u_backdrop.a > 0.0) { \
+ maskColor.rgb = maskColor.rgb * maskColor.a + \
+ u_backdrop.rgb * (1.0 - maskColor.a); \
+ } \
+ float lum; \
+ if (u_subtype == 0) { \
+ lum = maskColor.a; \
+ } else { \
+ lum = maskColor.r * 0.3 + maskColor.g * 0.59 + \
+ maskColor.b * 0.11; \
+ } \
+ imageColor.a *= lum; \
+ imageColor.rgb *= imageColor.a; \
+ gl_FragColor = imageColor; \
+ } ';
+ var smaskCache = null;
+
+ function initSmaskGL() {
+ var canvas, gl;
+ generateGL();
+ canvas = currentCanvas;
+ currentCanvas = null;
+ gl = currentGL;
+ currentGL = null;
+ var vertexShader = createVertexShader(gl, smaskVertexShaderCode);
+ var fragmentShader = createFragmentShader(gl, smaskFragmentShaderCode);
+ var program = createProgram(gl, [vertexShader, fragmentShader]);
+ gl.useProgram(program);
+ var cache = {};
+ cache.gl = gl;
+ cache.canvas = canvas;
+ cache.resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
+ cache.positionLocation = gl.getAttribLocation(program, 'a_position');
+ cache.backdropLocation = gl.getUniformLocation(program, 'u_backdrop');
+ cache.subtypeLocation = gl.getUniformLocation(program, 'u_subtype');
+ var texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
+ var texLayerLocation = gl.getUniformLocation(program, 'u_image');
+ var texMaskLocation = gl.getUniformLocation(program, 'u_mask');
+ var texCoordBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]), gl.STATIC_DRAW);
+ gl.enableVertexAttribArray(texCoordLocation);
+ gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
+ gl.uniform1i(texLayerLocation, 0);
+ gl.uniform1i(texMaskLocation, 1);
+ smaskCache = cache;
+ }
+
+ function composeSMask(layer, mask, properties) {
+ var width = layer.width,
+ height = layer.height;
+
+ if (!smaskCache) {
+ initSmaskGL();
+ }
+
+ var cache = smaskCache,
+ canvas = cache.canvas,
+ gl = cache.gl;
+ canvas.width = width;
+ canvas.height = height;
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
+ gl.uniform2f(cache.resolutionLocation, width, height);
+
+ if (properties.backdrop) {
+ gl.uniform4f(cache.resolutionLocation, properties.backdrop[0], properties.backdrop[1], properties.backdrop[2], 1);
+ } else {
+ gl.uniform4f(cache.resolutionLocation, 0, 0, 0, 0);
+ }
+
+ gl.uniform1i(cache.subtypeLocation, properties.subtype === 'Luminosity' ? 1 : 0);
+ var texture = createTexture(gl, layer, gl.TEXTURE0);
+ var maskTexture = createTexture(gl, mask, gl.TEXTURE1);
+ var buffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, width, 0, 0, height, 0, height, width, 0, width, height]), gl.STATIC_DRAW);
+ gl.enableVertexAttribArray(cache.positionLocation);
+ gl.vertexAttribPointer(cache.positionLocation, 2, gl.FLOAT, false, 0, 0);
+ gl.clearColor(0, 0, 0, 0);
+ gl.enable(gl.BLEND);
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
+ gl.flush();
+ gl.deleteTexture(texture);
+ gl.deleteTexture(maskTexture);
+ gl.deleteBuffer(buffer);
+ return canvas;
+ }
+
+ var figuresVertexShaderCode = '\
+ attribute vec2 a_position; \
+ attribute vec3 a_color; \
+ \
+ uniform vec2 u_resolution; \
+ uniform vec2 u_scale; \
+ uniform vec2 u_offset; \
+ \
+ varying vec4 v_color; \
+ \
+ void main() { \
+ vec2 position = (a_position + u_offset) * u_scale; \
+ vec2 clipSpace = (position / u_resolution) * 2.0 - 1.0; \
+ gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); \
+ \
+ v_color = vec4(a_color / 255.0, 1.0); \
+ } ';
+ var figuresFragmentShaderCode = '\
+ precision mediump float; \
+ \
+ varying vec4 v_color; \
+ \
+ void main() { \
+ gl_FragColor = v_color; \
+ } ';
+ var figuresCache = null;
+
+ function initFiguresGL() {
+ var canvas, gl;
+ generateGL();
+ canvas = currentCanvas;
+ currentCanvas = null;
+ gl = currentGL;
+ currentGL = null;
+ var vertexShader = createVertexShader(gl, figuresVertexShaderCode);
+ var fragmentShader = createFragmentShader(gl, figuresFragmentShaderCode);
+ var program = createProgram(gl, [vertexShader, fragmentShader]);
+ gl.useProgram(program);
+ var cache = {};
+ cache.gl = gl;
+ cache.canvas = canvas;
+ cache.resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
+ cache.scaleLocation = gl.getUniformLocation(program, 'u_scale');
+ cache.offsetLocation = gl.getUniformLocation(program, 'u_offset');
+ cache.positionLocation = gl.getAttribLocation(program, 'a_position');
+ cache.colorLocation = gl.getAttribLocation(program, 'a_color');
+ figuresCache = cache;
+ }
+
+ function drawFigures(width, height, backgroundColor, figures, context) {
+ if (!figuresCache) {
+ initFiguresGL();
+ }
+
+ var cache = figuresCache,
+ canvas = cache.canvas,
+ gl = cache.gl;
+ canvas.width = width;
+ canvas.height = height;
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
+ gl.uniform2f(cache.resolutionLocation, width, height);
+ var count = 0;
+ var i, ii, rows;
+
+ for (i = 0, ii = figures.length; i < ii; i++) {
+ switch (figures[i].type) {
+ case 'lattice':
+ rows = figures[i].coords.length / figures[i].verticesPerRow | 0;
+ count += (rows - 1) * (figures[i].verticesPerRow - 1) * 6;
+ break;
+
+ case 'triangles':
+ count += figures[i].coords.length;
+ break;
+ }
+ }
+
+ var coords = new Float32Array(count * 2);
+ var colors = new Uint8Array(count * 3);
+ var coordsMap = context.coords,
+ colorsMap = context.colors;
+ var pIndex = 0,
+ cIndex = 0;
+
+ for (i = 0, ii = figures.length; i < ii; i++) {
+ var figure = figures[i],
+ ps = figure.coords,
+ cs = figure.colors;
+
+ switch (figure.type) {
+ case 'lattice':
+ var cols = figure.verticesPerRow;
+ rows = ps.length / cols | 0;
+
+ for (var row = 1; row < rows; row++) {
+ var offset = row * cols + 1;
+
+ for (var col = 1; col < cols; col++, offset++) {
+ coords[pIndex] = coordsMap[ps[offset - cols - 1]];
+ coords[pIndex + 1] = coordsMap[ps[offset - cols - 1] + 1];
+ coords[pIndex + 2] = coordsMap[ps[offset - cols]];
+ coords[pIndex + 3] = coordsMap[ps[offset - cols] + 1];
+ coords[pIndex + 4] = coordsMap[ps[offset - 1]];
+ coords[pIndex + 5] = coordsMap[ps[offset - 1] + 1];
+ colors[cIndex] = colorsMap[cs[offset - cols - 1]];
+ colors[cIndex + 1] = colorsMap[cs[offset - cols - 1] + 1];
+ colors[cIndex + 2] = colorsMap[cs[offset - cols - 1] + 2];
+ colors[cIndex + 3] = colorsMap[cs[offset - cols]];
+ colors[cIndex + 4] = colorsMap[cs[offset - cols] + 1];
+ colors[cIndex + 5] = colorsMap[cs[offset - cols] + 2];
+ colors[cIndex + 6] = colorsMap[cs[offset - 1]];
+ colors[cIndex + 7] = colorsMap[cs[offset - 1] + 1];
+ colors[cIndex + 8] = colorsMap[cs[offset - 1] + 2];
+ coords[pIndex + 6] = coords[pIndex + 2];
+ coords[pIndex + 7] = coords[pIndex + 3];
+ coords[pIndex + 8] = coords[pIndex + 4];
+ coords[pIndex + 9] = coords[pIndex + 5];
+ coords[pIndex + 10] = coordsMap[ps[offset]];
+ coords[pIndex + 11] = coordsMap[ps[offset] + 1];
+ colors[cIndex + 9] = colors[cIndex + 3];
+ colors[cIndex + 10] = colors[cIndex + 4];
+ colors[cIndex + 11] = colors[cIndex + 5];
+ colors[cIndex + 12] = colors[cIndex + 6];
+ colors[cIndex + 13] = colors[cIndex + 7];
+ colors[cIndex + 14] = colors[cIndex + 8];
+ colors[cIndex + 15] = colorsMap[cs[offset]];
+ colors[cIndex + 16] = colorsMap[cs[offset] + 1];
+ colors[cIndex + 17] = colorsMap[cs[offset] + 2];
+ pIndex += 12;
+ cIndex += 18;
+ }
+ }
+
+ break;
+
+ case 'triangles':
+ for (var j = 0, jj = ps.length; j < jj; j++) {
+ coords[pIndex] = coordsMap[ps[j]];
+ coords[pIndex + 1] = coordsMap[ps[j] + 1];
+ colors[cIndex] = colorsMap[cs[j]];
+ colors[cIndex + 1] = colorsMap[cs[j] + 1];
+ colors[cIndex + 2] = colorsMap[cs[j] + 2];
+ pIndex += 2;
+ cIndex += 3;
+ }
+
+ break;
+ }
+ }
+
+ if (backgroundColor) {
+ gl.clearColor(backgroundColor[0] / 255, backgroundColor[1] / 255, backgroundColor[2] / 255, 1.0);
+ } else {
+ gl.clearColor(0, 0, 0, 0);
+ }
+
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ var coordsBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, coordsBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, coords, gl.STATIC_DRAW);
+ gl.enableVertexAttribArray(cache.positionLocation);
+ gl.vertexAttribPointer(cache.positionLocation, 2, gl.FLOAT, false, 0, 0);
+ var colorsBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, colorsBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
+ gl.enableVertexAttribArray(cache.colorLocation);
+ gl.vertexAttribPointer(cache.colorLocation, 3, gl.UNSIGNED_BYTE, false, 0, 0);
+ gl.uniform2f(cache.scaleLocation, context.scaleX, context.scaleY);
+ gl.uniform2f(cache.offsetLocation, context.offsetX, context.offsetY);
+ gl.drawArrays(gl.TRIANGLES, 0, count);
+ gl.flush();
+ gl.deleteBuffer(coordsBuffer);
+ gl.deleteBuffer(colorsBuffer);
+ return canvas;
+ }
+
+ return {
+ tryInitGL: function tryInitGL() {
+ try {
+ generateGL();
+ return !!currentGL;
+ } catch (ex) {}
+
+ return false;
+ },
+ composeSMask: composeSMask,
+ drawFigures: drawFigures,
+ cleanup: function cleanup() {
+ if (smaskCache && smaskCache.canvas) {
+ smaskCache.canvas.width = 0;
+ smaskCache.canvas.height = 0;
+ }
+
+ if (figuresCache && figuresCache.canvas) {
+ figuresCache.canvas.width = 0;
+ figuresCache.canvas.height = 0;
+ }
+
+ smaskCache = null;
+ figuresCache = null;
+ }
+ };
+}();
+
+/***/ }),
+/* 162 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.renderTextLayer = void 0;
+
+var _util = __w_pdfjs_require__(1);
+
+var _global_scope = _interopRequireDefault(__w_pdfjs_require__(3));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+var renderTextLayer = function renderTextLayerClosure() {
+ var MAX_TEXT_DIVS_TO_RENDER = 100000;
+ var NonWhitespaceRegexp = /\S/;
+
+ function isAllWhitespace(str) {
+ return !NonWhitespaceRegexp.test(str);
+ }
+
+ var styleBuf = ['left: ', 0, 'px; top: ', 0, 'px; font-size: ', 0, 'px; font-family: ', '', ';'];
+
+ function appendText(task, geom, styles) {
+ var textDiv = document.createElement('span');
+ var textDivProperties = {
+ style: null,
+ angle: 0,
+ canvasWidth: 0,
+ isWhitespace: false,
+ originalTransform: null,
+ paddingBottom: 0,
+ paddingLeft: 0,
+ paddingRight: 0,
+ paddingTop: 0,
+ scale: 1
+ };
+
+ task._textDivs.push(textDiv);
+
+ if (isAllWhitespace(geom.str)) {
+ textDivProperties.isWhitespace = true;
+
+ task._textDivProperties.set(textDiv, textDivProperties);
+
+ return;
+ }
+
+ var tx = _util.Util.transform(task._viewport.transform, geom.transform);
+
+ var angle = Math.atan2(tx[1], tx[0]);
+ var style = styles[geom.fontName];
+
+ if (style.vertical) {
+ angle += Math.PI / 2;
+ }
+
+ var fontHeight = Math.sqrt(tx[2] * tx[2] + tx[3] * tx[3]);
+ var fontAscent = fontHeight;
+
+ if (style.ascent) {
+ fontAscent = style.ascent * fontAscent;
+ } else if (style.descent) {
+ fontAscent = (1 + style.descent) * fontAscent;
+ }
+
+ var left;
+ var top;
+
+ if (angle === 0) {
+ left = tx[4];
+ top = tx[5] - fontAscent;
+ } else {
+ left = tx[4] + fontAscent * Math.sin(angle);
+ top = tx[5] - fontAscent * Math.cos(angle);
+ }
+
+ styleBuf[1] = left;
+ styleBuf[3] = top;
+ styleBuf[5] = fontHeight;
+ styleBuf[7] = style.fontFamily;
+ textDivProperties.style = styleBuf.join('');
+ textDiv.setAttribute('style', textDivProperties.style);
+ textDiv.textContent = geom.str;
+
+ if (task._fontInspectorEnabled) {
+ textDiv.dataset.fontName = geom.fontName;
+ }
+
+ if (angle !== 0) {
+ textDivProperties.angle = angle * (180 / Math.PI);
+ }
+
+ if (geom.str.length > 1) {
+ if (style.vertical) {
+ textDivProperties.canvasWidth = geom.height * task._viewport.scale;
+ } else {
+ textDivProperties.canvasWidth = geom.width * task._viewport.scale;
+ }
+ }
+
+ task._textDivProperties.set(textDiv, textDivProperties);
+
+ if (task._textContentStream) {
+ task._layoutText(textDiv);
+ }
+
+ if (task._enhanceTextSelection) {
+ var angleCos = 1,
+ angleSin = 0;
+
+ if (angle !== 0) {
+ angleCos = Math.cos(angle);
+ angleSin = Math.sin(angle);
+ }
+
+ var divWidth = (style.vertical ? geom.height : geom.width) * task._viewport.scale;
+ var divHeight = fontHeight;
+ var m, b;
+
+ if (angle !== 0) {
+ m = [angleCos, angleSin, -angleSin, angleCos, left, top];
+ b = _util.Util.getAxialAlignedBoundingBox([0, 0, divWidth, divHeight], m);
+ } else {
+ b = [left, top, left + divWidth, top + divHeight];
+ }
+
+ task._bounds.push({
+ left: b[0],
+ top: b[1],
+ right: b[2],
+ bottom: b[3],
+ div: textDiv,
+ size: [divWidth, divHeight],
+ m: m
+ });
+ }
+ }
+
+ function render(task) {
+ if (task._canceled) {
+ return;
+ }
+
+ var textDivs = task._textDivs;
+ var capability = task._capability;
+ var textDivsLength = textDivs.length;
+
+ if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) {
+ task._renderingDone = true;
+ capability.resolve();
+ return;
+ }
+
+ if (!task._textContentStream) {
+ for (var i = 0; i < textDivsLength; i++) {
+ task._layoutText(textDivs[i]);
+ }
+ }
+
+ task._renderingDone = true;
+ capability.resolve();
+ }
+
+ function expand(task) {
+ var bounds = task._bounds;
+ var viewport = task._viewport;
+ var expanded = expandBounds(viewport.width, viewport.height, bounds);
+
+ for (var i = 0; i < expanded.length; i++) {
+ var div = bounds[i].div;
+
+ var divProperties = task._textDivProperties.get(div);
+
+ if (divProperties.angle === 0) {
+ divProperties.paddingLeft = bounds[i].left - expanded[i].left;
+ divProperties.paddingTop = bounds[i].top - expanded[i].top;
+ divProperties.paddingRight = expanded[i].right - bounds[i].right;
+ divProperties.paddingBottom = expanded[i].bottom - bounds[i].bottom;
+
+ task._textDivProperties.set(div, divProperties);
+
+ continue;
+ }
+
+ var e = expanded[i],
+ b = bounds[i];
+ var m = b.m,
+ c = m[0],
+ s = m[1];
+ var points = [[0, 0], [0, b.size[1]], [b.size[0], 0], b.size];
+ var ts = new Float64Array(64);
+ points.forEach(function (p, i) {
+ var t = _util.Util.applyTransform(p, m);
+
+ ts[i + 0] = c && (e.left - t[0]) / c;
+ ts[i + 4] = s && (e.top - t[1]) / s;
+ ts[i + 8] = c && (e.right - t[0]) / c;
+ ts[i + 12] = s && (e.bottom - t[1]) / s;
+ ts[i + 16] = s && (e.left - t[0]) / -s;
+ ts[i + 20] = c && (e.top - t[1]) / c;
+ ts[i + 24] = s && (e.right - t[0]) / -s;
+ ts[i + 28] = c && (e.bottom - t[1]) / c;
+ ts[i + 32] = c && (e.left - t[0]) / -c;
+ ts[i + 36] = s && (e.top - t[1]) / -s;
+ ts[i + 40] = c && (e.right - t[0]) / -c;
+ ts[i + 44] = s && (e.bottom - t[1]) / -s;
+ ts[i + 48] = s && (e.left - t[0]) / s;
+ ts[i + 52] = c && (e.top - t[1]) / -c;
+ ts[i + 56] = s && (e.right - t[0]) / s;
+ ts[i + 60] = c && (e.bottom - t[1]) / -c;
+ });
+
+ var findPositiveMin = function findPositiveMin(ts, offset, count) {
+ var result = 0;
+
+ for (var i = 0; i < count; i++) {
+ var t = ts[offset++];
+
+ if (t > 0) {
+ result = result ? Math.min(t, result) : t;
+ }
+ }
+
+ return result;
+ };
+
+ var boxScale = 1 + Math.min(Math.abs(c), Math.abs(s));
+ divProperties.paddingLeft = findPositiveMin(ts, 32, 16) / boxScale;
+ divProperties.paddingTop = findPositiveMin(ts, 48, 16) / boxScale;
+ divProperties.paddingRight = findPositiveMin(ts, 0, 16) / boxScale;
+ divProperties.paddingBottom = findPositiveMin(ts, 16, 16) / boxScale;
+
+ task._textDivProperties.set(div, divProperties);
+ }
+ }
+
+ function expandBounds(width, height, boxes) {
+ var bounds = boxes.map(function (box, i) {
+ return {
+ x1: box.left,
+ y1: box.top,
+ x2: box.right,
+ y2: box.bottom,
+ index: i,
+ x1New: undefined,
+ x2New: undefined
+ };
+ });
+ expandBoundsLTR(width, bounds);
+ var expanded = new Array(boxes.length);
+ bounds.forEach(function (b) {
+ var i = b.index;
+ expanded[i] = {
+ left: b.x1New,
+ top: 0,
+ right: b.x2New,
+ bottom: 0
+ };
+ });
+ boxes.map(function (box, i) {
+ var e = expanded[i],
+ b = bounds[i];
+ b.x1 = box.top;
+ b.y1 = width - e.right;
+ b.x2 = box.bottom;
+ b.y2 = width - e.left;
+ b.index = i;
+ b.x1New = undefined;
+ b.x2New = undefined;
+ });
+ expandBoundsLTR(height, bounds);
+ bounds.forEach(function (b) {
+ var i = b.index;
+ expanded[i].top = b.x1New;
+ expanded[i].bottom = b.x2New;
+ });
+ return expanded;
+ }
+
+ function expandBoundsLTR(width, bounds) {
+ bounds.sort(function (a, b) {
+ return a.x1 - b.x1 || a.index - b.index;
+ });
+ var fakeBoundary = {
+ x1: -Infinity,
+ y1: -Infinity,
+ x2: 0,
+ y2: Infinity,
+ index: -1,
+ x1New: 0,
+ x2New: 0
+ };
+ var horizon = [{
+ start: -Infinity,
+ end: Infinity,
+ boundary: fakeBoundary
+ }];
+ bounds.forEach(function (boundary) {
+ var i = 0;
+
+ while (i < horizon.length && horizon[i].end <= boundary.y1) {
+ i++;
+ }
+
+ var j = horizon.length - 1;
+
+ while (j >= 0 && horizon[j].start >= boundary.y2) {
+ j--;
+ }
+
+ var horizonPart, affectedBoundary;
+ var q,
+ k,
+ maxXNew = -Infinity;
+
+ for (q = i; q <= j; q++) {
+ horizonPart = horizon[q];
+ affectedBoundary = horizonPart.boundary;
+ var xNew;
+
+ if (affectedBoundary.x2 > boundary.x1) {
+ xNew = affectedBoundary.index > boundary.index ? affectedBoundary.x1New : boundary.x1;
+ } else if (affectedBoundary.x2New === undefined) {
+ xNew = (affectedBoundary.x2 + boundary.x1) / 2;
+ } else {
+ xNew = affectedBoundary.x2New;
+ }
+
+ if (xNew > maxXNew) {
+ maxXNew = xNew;
+ }
+ }
+
+ boundary.x1New = maxXNew;
+
+ for (q = i; q <= j; q++) {
+ horizonPart = horizon[q];
+ affectedBoundary = horizonPart.boundary;
+
+ if (affectedBoundary.x2New === undefined) {
+ if (affectedBoundary.x2 > boundary.x1) {
+ if (affectedBoundary.index > boundary.index) {
+ affectedBoundary.x2New = affectedBoundary.x2;
+ }
+ } else {
+ affectedBoundary.x2New = maxXNew;
+ }
+ } else if (affectedBoundary.x2New > maxXNew) {
+ affectedBoundary.x2New = Math.max(maxXNew, affectedBoundary.x2);
+ }
+ }
+
+ var changedHorizon = [],
+ lastBoundary = null;
+
+ for (q = i; q <= j; q++) {
+ horizonPart = horizon[q];
+ affectedBoundary = horizonPart.boundary;
+ var useBoundary = affectedBoundary.x2 > boundary.x2 ? affectedBoundary : boundary;
+
+ if (lastBoundary === useBoundary) {
+ changedHorizon[changedHorizon.length - 1].end = horizonPart.end;
+ } else {
+ changedHorizon.push({
+ start: horizonPart.start,
+ end: horizonPart.end,
+ boundary: useBoundary
+ });
+ lastBoundary = useBoundary;
+ }
+ }
+
+ if (horizon[i].start < boundary.y1) {
+ changedHorizon[0].start = boundary.y1;
+ changedHorizon.unshift({
+ start: horizon[i].start,
+ end: boundary.y1,
+ boundary: horizon[i].boundary
+ });
+ }
+
+ if (boundary.y2 < horizon[j].end) {
+ changedHorizon[changedHorizon.length - 1].end = boundary.y2;
+ changedHorizon.push({
+ start: boundary.y2,
+ end: horizon[j].end,
+ boundary: horizon[j].boundary
+ });
+ }
+
+ for (q = i; q <= j; q++) {
+ horizonPart = horizon[q];
+ affectedBoundary = horizonPart.boundary;
+
+ if (affectedBoundary.x2New !== undefined) {
+ continue;
+ }
+
+ var used = false;
+
+ for (k = i - 1; !used && k >= 0 && horizon[k].start >= affectedBoundary.y1; k--) {
+ used = horizon[k].boundary === affectedBoundary;
+ }
+
+ for (k = j + 1; !used && k < horizon.length && horizon[k].end <= affectedBoundary.y2; k++) {
+ used = horizon[k].boundary === affectedBoundary;
+ }
+
+ for (k = 0; !used && k < changedHorizon.length; k++) {
+ used = changedHorizon[k].boundary === affectedBoundary;
+ }
+
+ if (!used) {
+ affectedBoundary.x2New = maxXNew;
+ }
+ }
+
+ Array.prototype.splice.apply(horizon, [i, j - i + 1].concat(changedHorizon));
+ });
+ horizon.forEach(function (horizonPart) {
+ var affectedBoundary = horizonPart.boundary;
+
+ if (affectedBoundary.x2New === undefined) {
+ affectedBoundary.x2New = Math.max(width, affectedBoundary.x2);
+ }
+ });
+ }
+
+ function TextLayerRenderTask(_ref) {
+ var _this = this;
+
+ var textContent = _ref.textContent,
+ textContentStream = _ref.textContentStream,
+ container = _ref.container,
+ viewport = _ref.viewport,
+ textDivs = _ref.textDivs,
+ textContentItemsStr = _ref.textContentItemsStr,
+ enhanceTextSelection = _ref.enhanceTextSelection;
+ this._textContent = textContent;
+ this._textContentStream = textContentStream;
+ this._container = container;
+ this._viewport = viewport;
+ this._textDivs = textDivs || [];
+ this._textContentItemsStr = textContentItemsStr || [];
+ this._enhanceTextSelection = !!enhanceTextSelection;
+ this._fontInspectorEnabled = !!(_global_scope["default"].FontInspector && _global_scope["default"].FontInspector.enabled);
+ this._reader = null;
+ this._layoutTextLastFontSize = null;
+ this._layoutTextLastFontFamily = null;
+ this._layoutTextCtx = null;
+ this._textDivProperties = new WeakMap();
+ this._renderingDone = false;
+ this._canceled = false;
+ this._capability = (0, _util.createPromiseCapability)();
+ this._renderTimer = null;
+ this._bounds = [];
+
+ this._capability.promise["finally"](function () {
+ if (_this._layoutTextCtx) {
+ _this._layoutTextCtx.canvas.width = 0;
+ _this._layoutTextCtx.canvas.height = 0;
+ _this._layoutTextCtx = null;
+ }
+ });
+ }
+
+ TextLayerRenderTask.prototype = {
+ get promise() {
+ return this._capability.promise;
+ },
+
+ cancel: function TextLayer_cancel() {
+ this._canceled = true;
+
+ if (this._reader) {
+ this._reader.cancel(new _util.AbortException('TextLayer task cancelled.'));
+
+ this._reader = null;
+ }
+
+ if (this._renderTimer !== null) {
+ clearTimeout(this._renderTimer);
+ this._renderTimer = null;
+ }
+
+ this._capability.reject(new Error('TextLayer task cancelled.'));
+ },
+ _processItems: function _processItems(items, styleCache) {
+ for (var i = 0, len = items.length; i < len; i++) {
+ this._textContentItemsStr.push(items[i].str);
+
+ appendText(this, items[i], styleCache);
+ }
+ },
+ _layoutText: function _layoutText(textDiv) {
+ var textLayerFrag = this._container;
+
+ var textDivProperties = this._textDivProperties.get(textDiv);
+
+ if (textDivProperties.isWhitespace) {
+ return;
+ }
+
+ var fontSize = textDiv.style.fontSize;
+ var fontFamily = textDiv.style.fontFamily;
+
+ if (fontSize !== this._layoutTextLastFontSize || fontFamily !== this._layoutTextLastFontFamily) {
+ this._layoutTextCtx.font = fontSize + ' ' + fontFamily;
+ this._layoutTextLastFontSize = fontSize;
+ this._layoutTextLastFontFamily = fontFamily;
+ }
+
+ var width = this._layoutTextCtx.measureText(textDiv.textContent).width;
+
+ var transform = '';
+
+ if (textDivProperties.canvasWidth !== 0 && width > 0) {
+ textDivProperties.scale = textDivProperties.canvasWidth / width;
+ transform = "scaleX(".concat(textDivProperties.scale, ")");
+ }
+
+ if (textDivProperties.angle !== 0) {
+ transform = "rotate(".concat(textDivProperties.angle, "deg) ").concat(transform);
+ }
+
+ if (transform.length > 0) {
+ textDivProperties.originalTransform = transform;
+ textDiv.style.transform = transform;
+ }
+
+ this._textDivProperties.set(textDiv, textDivProperties);
+
+ textLayerFrag.appendChild(textDiv);
+ },
+ _render: function TextLayer_render(timeout) {
+ var _this2 = this;
+
+ var capability = (0, _util.createPromiseCapability)();
+ var styleCache = Object.create(null);
+ var canvas = document.createElement('canvas');
+ canvas.mozOpaque = true;
+ this._layoutTextCtx = canvas.getContext('2d', {
+ alpha: false
+ });
+
+ if (this._textContent) {
+ var textItems = this._textContent.items;
+ var textStyles = this._textContent.styles;
+
+ this._processItems(textItems, textStyles);
+
+ capability.resolve();
+ } else if (this._textContentStream) {
+ var pump = function pump() {
+ _this2._reader.read().then(function (_ref2) {
+ var value = _ref2.value,
+ done = _ref2.done;
+
+ if (done) {
+ capability.resolve();
+ return;
+ }
+
+ Object.assign(styleCache, value.styles);
+
+ _this2._processItems(value.items, styleCache);
+
+ pump();
+ }, capability.reject);
+ };
+
+ this._reader = this._textContentStream.getReader();
+ pump();
+ } else {
+ throw new Error('Neither "textContent" nor "textContentStream"' + ' parameters specified.');
+ }
+
+ capability.promise.then(function () {
+ styleCache = null;
+
+ if (!timeout) {
+ render(_this2);
+ } else {
+ _this2._renderTimer = setTimeout(function () {
+ render(_this2);
+ _this2._renderTimer = null;
+ }, timeout);
+ }
+ }, this._capability.reject);
+ },
+ expandTextDivs: function TextLayer_expandTextDivs(expandDivs) {
+ if (!this._enhanceTextSelection || !this._renderingDone) {
+ return;
+ }
+
+ if (this._bounds !== null) {
+ expand(this);
+ this._bounds = null;
+ }
+
+ for (var i = 0, ii = this._textDivs.length; i < ii; i++) {
+ var div = this._textDivs[i];
+
+ var divProperties = this._textDivProperties.get(div);
+
+ if (divProperties.isWhitespace) {
+ continue;
+ }
+
+ if (expandDivs) {
+ var transform = '',
+ padding = '';
+
+ if (divProperties.scale !== 1) {
+ transform = 'scaleX(' + divProperties.scale + ')';
+ }
+
+ if (divProperties.angle !== 0) {
+ transform = 'rotate(' + divProperties.angle + 'deg) ' + transform;
+ }
+
+ if (divProperties.paddingLeft !== 0) {
+ padding += ' padding-left: ' + divProperties.paddingLeft / divProperties.scale + 'px;';
+ transform += ' translateX(' + -divProperties.paddingLeft / divProperties.scale + 'px)';
+ }
+
+ if (divProperties.paddingTop !== 0) {
+ padding += ' padding-top: ' + divProperties.paddingTop + 'px;';
+ transform += ' translateY(' + -divProperties.paddingTop + 'px)';
+ }
+
+ if (divProperties.paddingRight !== 0) {
+ padding += ' padding-right: ' + divProperties.paddingRight / divProperties.scale + 'px;';
+ }
+
+ if (divProperties.paddingBottom !== 0) {
+ padding += ' padding-bottom: ' + divProperties.paddingBottom + 'px;';
+ }
+
+ if (padding !== '') {
+ div.setAttribute('style', divProperties.style + padding);
+ }
+
+ if (transform !== '') {
+ div.style.transform = transform;
+ }
+ } else {
+ div.style.padding = 0;
+ div.style.transform = divProperties.originalTransform || '';
+ }
+ }
+ }
+ };
+
+ function renderTextLayer(renderParameters) {
+ var task = new TextLayerRenderTask({
+ textContent: renderParameters.textContent,
+ textContentStream: renderParameters.textContentStream,
+ container: renderParameters.container,
+ viewport: renderParameters.viewport,
+ textDivs: renderParameters.textDivs,
+ textContentItemsStr: renderParameters.textContentItemsStr,
+ enhanceTextSelection: renderParameters.enhanceTextSelection
+ });
+
+ task._render(renderParameters.timeout);
+
+ return task;
+ }
+
+ return renderTextLayer;
+}();
+
+exports.renderTextLayer = renderTextLayer;
+
+/***/ }),
+/* 163 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.AnnotationLayer = void 0;
+
+var _display_utils = __w_pdfjs_require__(151);
+
+var _util = __w_pdfjs_require__(1);
+
+function _get(target, property, receiver) { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get; } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(receiver); } return desc.value; }; } return _get(target, property, receiver || target); }
+
+function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = _getPrototypeOf(object); if (object === null) break; } return object; }
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
+
+function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
+
+function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
+
+function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var AnnotationElementFactory =
+/*#__PURE__*/
+function () {
+ function AnnotationElementFactory() {
+ _classCallCheck(this, AnnotationElementFactory);
+ }
+
+ _createClass(AnnotationElementFactory, null, [{
+ key: "create",
+ value: function create(parameters) {
+ var subtype = parameters.data.annotationType;
+
+ switch (subtype) {
+ case _util.AnnotationType.LINK:
+ return new LinkAnnotationElement(parameters);
+
+ case _util.AnnotationType.TEXT:
+ return new TextAnnotationElement(parameters);
+
+ case _util.AnnotationType.WIDGET:
+ var fieldType = parameters.data.fieldType;
+
+ switch (fieldType) {
+ case 'Tx':
+ return new TextWidgetAnnotationElement(parameters);
+
+ case 'Btn':
+ if (parameters.data.radioButton) {
+ return new RadioButtonWidgetAnnotationElement(parameters);
+ } else if (parameters.data.checkBox) {
+ return new CheckboxWidgetAnnotationElement(parameters);
+ }
+
+ return new PushButtonWidgetAnnotationElement(parameters);
+
+ case 'Ch':
+ return new ChoiceWidgetAnnotationElement(parameters);
+ }
+
+ return new WidgetAnnotationElement(parameters);
+
+ case _util.AnnotationType.POPUP:
+ return new PopupAnnotationElement(parameters);
+
+ case _util.AnnotationType.FREETEXT:
+ return new FreeTextAnnotationElement(parameters);
+
+ case _util.AnnotationType.LINE:
+ return new LineAnnotationElement(parameters);
+
+ case _util.AnnotationType.SQUARE:
+ return new SquareAnnotationElement(parameters);
+
+ case _util.AnnotationType.CIRCLE:
+ return new CircleAnnotationElement(parameters);
+
+ case _util.AnnotationType.POLYLINE:
+ return new PolylineAnnotationElement(parameters);
+
+ case _util.AnnotationType.CARET:
+ return new CaretAnnotationElement(parameters);
+
+ case _util.AnnotationType.INK:
+ return new InkAnnotationElement(parameters);
+
+ case _util.AnnotationType.POLYGON:
+ return new PolygonAnnotationElement(parameters);
+
+ case _util.AnnotationType.HIGHLIGHT:
+ return new HighlightAnnotationElement(parameters);
+
+ case _util.AnnotationType.UNDERLINE:
+ return new UnderlineAnnotationElement(parameters);
+
+ case _util.AnnotationType.SQUIGGLY:
+ return new SquigglyAnnotationElement(parameters);
+
+ case _util.AnnotationType.STRIKEOUT:
+ return new StrikeOutAnnotationElement(parameters);
+
+ case _util.AnnotationType.STAMP:
+ return new StampAnnotationElement(parameters);
+
+ case _util.AnnotationType.FILEATTACHMENT:
+ return new FileAttachmentAnnotationElement(parameters);
+
+ default:
+ return new AnnotationElement(parameters);
+ }
+ }
+ }]);
+
+ return AnnotationElementFactory;
+}();
+
+var AnnotationElement =
+/*#__PURE__*/
+function () {
+ function AnnotationElement(parameters) {
+ var isRenderable = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var ignoreBorder = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+
+ _classCallCheck(this, AnnotationElement);
+
+ this.isRenderable = isRenderable;
+ this.data = parameters.data;
+ this.layer = parameters.layer;
+ this.page = parameters.page;
+ this.viewport = parameters.viewport;
+ this.linkService = parameters.linkService;
+ this.downloadManager = parameters.downloadManager;
+ this.imageResourcesPath = parameters.imageResourcesPath;
+ this.renderInteractiveForms = parameters.renderInteractiveForms;
+ this.svgFactory = parameters.svgFactory;
+
+ if (isRenderable) {
+ this.container = this._createContainer(ignoreBorder);
+ }
+ }
+
+ _createClass(AnnotationElement, [{
+ key: "_createContainer",
+ value: function _createContainer() {
+ var ignoreBorder = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ var data = this.data,
+ page = this.page,
+ viewport = this.viewport;
+ var container = document.createElement('section');
+ var width = data.rect[2] - data.rect[0];
+ var height = data.rect[3] - data.rect[1];
+ container.setAttribute('data-annotation-id', data.id);
+
+ var rect = _util.Util.normalizeRect([data.rect[0], page.view[3] - data.rect[1] + page.view[1], data.rect[2], page.view[3] - data.rect[3] + page.view[1]]);
+
+ container.style.transform = 'matrix(' + viewport.transform.join(',') + ')';
+ container.style.transformOrigin = -rect[0] + 'px ' + -rect[1] + 'px';
+
+ if (!ignoreBorder && data.borderStyle.width > 0) {
+ container.style.borderWidth = data.borderStyle.width + 'px';
+
+ if (data.borderStyle.style !== _util.AnnotationBorderStyleType.UNDERLINE) {
+ width = width - 2 * data.borderStyle.width;
+ height = height - 2 * data.borderStyle.width;
+ }
+
+ var horizontalRadius = data.borderStyle.horizontalCornerRadius;
+ var verticalRadius = data.borderStyle.verticalCornerRadius;
+
+ if (horizontalRadius > 0 || verticalRadius > 0) {
+ var radius = horizontalRadius + 'px / ' + verticalRadius + 'px';
+ container.style.borderRadius = radius;
+ }
+
+ switch (data.borderStyle.style) {
+ case _util.AnnotationBorderStyleType.SOLID:
+ container.style.borderStyle = 'solid';
+ break;
+
+ case _util.AnnotationBorderStyleType.DASHED:
+ container.style.borderStyle = 'dashed';
+ break;
+
+ case _util.AnnotationBorderStyleType.BEVELED:
+ (0, _util.warn)('Unimplemented border style: beveled');
+ break;
+
+ case _util.AnnotationBorderStyleType.INSET:
+ (0, _util.warn)('Unimplemented border style: inset');
+ break;
+
+ case _util.AnnotationBorderStyleType.UNDERLINE:
+ container.style.borderBottomStyle = 'solid';
+ break;
+
+ default:
+ break;
+ }
+
+ if (data.color) {
+ container.style.borderColor = _util.Util.makeCssRgb(data.color[0] | 0, data.color[1] | 0, data.color[2] | 0);
+ } else {
+ container.style.borderWidth = 0;
+ }
+ }
+
+ container.style.left = rect[0] + 'px';
+ container.style.top = rect[1] + 'px';
+ container.style.width = width + 'px';
+ container.style.height = height + 'px';
+ return container;
+ }
+ }, {
+ key: "_createPopup",
+ value: function _createPopup(container, trigger, data) {
+ if (!trigger) {
+ trigger = document.createElement('div');
+ trigger.style.height = container.style.height;
+ trigger.style.width = container.style.width;
+ container.appendChild(trigger);
+ }
+
+ var popupElement = new PopupElement({
+ container: container,
+ trigger: trigger,
+ color: data.color,
+ title: data.title,
+ modificationDate: data.modificationDate,
+ contents: data.contents,
+ hideWrapper: true
+ });
+ var popup = popupElement.render();
+ popup.style.left = container.style.width;
+ container.appendChild(popup);
+ }
+ }, {
+ key: "render",
+ value: function render() {
+ (0, _util.unreachable)('Abstract method `AnnotationElement.render` called');
+ }
+ }]);
+
+ return AnnotationElement;
+}();
+
+var LinkAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement) {
+ _inherits(LinkAnnotationElement, _AnnotationElement);
+
+ function LinkAnnotationElement(parameters) {
+ _classCallCheck(this, LinkAnnotationElement);
+
+ var isRenderable = !!(parameters.data.url || parameters.data.dest || parameters.data.action);
+ return _possibleConstructorReturn(this, _getPrototypeOf(LinkAnnotationElement).call(this, parameters, isRenderable));
+ }
+
+ _createClass(LinkAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'linkAnnotation';
+ var data = this.data,
+ linkService = this.linkService;
+ var link = document.createElement('a');
+ (0, _display_utils.addLinkAttributes)(link, {
+ url: data.url,
+ target: data.newWindow ? _display_utils.LinkTarget.BLANK : linkService.externalLinkTarget,
+ rel: linkService.externalLinkRel
+ });
+
+ if (!data.url) {
+ if (data.action) {
+ this._bindNamedAction(link, data.action);
+ } else {
+ this._bindLink(link, data.dest);
+ }
+ }
+
+ this.container.appendChild(link);
+ return this.container;
+ }
+ }, {
+ key: "_bindLink",
+ value: function _bindLink(link, destination) {
+ var _this = this;
+
+ link.href = this.linkService.getDestinationHash(destination);
+
+ link.onclick = function () {
+ if (destination) {
+ _this.linkService.navigateTo(destination);
+ }
+
+ return false;
+ };
+
+ if (destination) {
+ link.className = 'internalLink';
+ }
+ }
+ }, {
+ key: "_bindNamedAction",
+ value: function _bindNamedAction(link, action) {
+ var _this2 = this;
+
+ link.href = this.linkService.getAnchorUrl('');
+
+ link.onclick = function () {
+ _this2.linkService.executeNamedAction(action);
+
+ return false;
+ };
+
+ link.className = 'internalLink';
+ }
+ }]);
+
+ return LinkAnnotationElement;
+}(AnnotationElement);
+
+var TextAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement2) {
+ _inherits(TextAnnotationElement, _AnnotationElement2);
+
+ function TextAnnotationElement(parameters) {
+ _classCallCheck(this, TextAnnotationElement);
+
+ var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents);
+ return _possibleConstructorReturn(this, _getPrototypeOf(TextAnnotationElement).call(this, parameters, isRenderable));
+ }
+
+ _createClass(TextAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'textAnnotation';
+ var image = document.createElement('img');
+ image.style.height = this.container.style.height;
+ image.style.width = this.container.style.width;
+ image.src = this.imageResourcesPath + 'annotation-' + this.data.name.toLowerCase() + '.svg';
+ image.alt = '[{{type}} Annotation]';
+ image.dataset.l10nId = 'text_annotation_type';
+ image.dataset.l10nArgs = JSON.stringify({
+ type: this.data.name
+ });
+
+ if (!this.data.hasPopup) {
+ this._createPopup(this.container, image, this.data);
+ }
+
+ this.container.appendChild(image);
+ return this.container;
+ }
+ }]);
+
+ return TextAnnotationElement;
+}(AnnotationElement);
+
+var WidgetAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement3) {
+ _inherits(WidgetAnnotationElement, _AnnotationElement3);
+
+ function WidgetAnnotationElement() {
+ _classCallCheck(this, WidgetAnnotationElement);
+
+ return _possibleConstructorReturn(this, _getPrototypeOf(WidgetAnnotationElement).apply(this, arguments));
+ }
+
+ _createClass(WidgetAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ return this.container;
+ }
+ }]);
+
+ return WidgetAnnotationElement;
+}(AnnotationElement);
+
+var TextWidgetAnnotationElement =
+/*#__PURE__*/
+function (_WidgetAnnotationElem) {
+ _inherits(TextWidgetAnnotationElement, _WidgetAnnotationElem);
+
+ function TextWidgetAnnotationElement(parameters) {
+ _classCallCheck(this, TextWidgetAnnotationElement);
+
+ var isRenderable = parameters.renderInteractiveForms || !parameters.data.hasAppearance && !!parameters.data.fieldValue;
+ return _possibleConstructorReturn(this, _getPrototypeOf(TextWidgetAnnotationElement).call(this, parameters, isRenderable));
+ }
+
+ _createClass(TextWidgetAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ var TEXT_ALIGNMENT = ['left', 'center', 'right'];
+ this.container.className = 'textWidgetAnnotation';
+ var element = null;
+
+ if (this.renderInteractiveForms) {
+ if (this.data.multiLine) {
+ element = document.createElement('textarea');
+ element.textContent = this.data.fieldValue;
+ } else {
+ element = document.createElement('input');
+ element.type = 'text';
+ element.setAttribute('value', this.data.fieldValue);
+ }
+
+ element.disabled = this.data.readOnly;
+
+ if (this.data.maxLen !== null) {
+ element.maxLength = this.data.maxLen;
+ }
+
+ if (this.data.comb) {
+ var fieldWidth = this.data.rect[2] - this.data.rect[0];
+ var combWidth = fieldWidth / this.data.maxLen;
+ element.classList.add('comb');
+ element.style.letterSpacing = 'calc(' + combWidth + 'px - 1ch)';
+ }
+ } else {
+ element = document.createElement('div');
+ element.textContent = this.data.fieldValue;
+ element.style.verticalAlign = 'middle';
+ element.style.display = 'table-cell';
+ var font = null;
+
+ if (this.data.fontRefName && this.page.commonObjs.has(this.data.fontRefName)) {
+ font = this.page.commonObjs.get(this.data.fontRefName);
+ }
+
+ this._setTextStyle(element, font);
+ }
+
+ if (this.data.textAlignment !== null) {
+ element.style.textAlign = TEXT_ALIGNMENT[this.data.textAlignment];
+ }
+
+ this.container.appendChild(element);
+ return this.container;
+ }
+ }, {
+ key: "_setTextStyle",
+ value: function _setTextStyle(element, font) {
+ var style = element.style;
+ style.fontSize = this.data.fontSize + 'px';
+ style.direction = this.data.fontDirection < 0 ? 'rtl' : 'ltr';
+
+ if (!font) {
+ return;
+ }
+
+ style.fontWeight = font.black ? font.bold ? '900' : 'bold' : font.bold ? 'bold' : 'normal';
+ style.fontStyle = font.italic ? 'italic' : 'normal';
+ var fontFamily = font.loadedName ? '"' + font.loadedName + '", ' : '';
+ var fallbackName = font.fallbackName || 'Helvetica, sans-serif';
+ style.fontFamily = fontFamily + fallbackName;
+ }
+ }]);
+
+ return TextWidgetAnnotationElement;
+}(WidgetAnnotationElement);
+
+var CheckboxWidgetAnnotationElement =
+/*#__PURE__*/
+function (_WidgetAnnotationElem2) {
+ _inherits(CheckboxWidgetAnnotationElement, _WidgetAnnotationElem2);
+
+ function CheckboxWidgetAnnotationElement(parameters) {
+ _classCallCheck(this, CheckboxWidgetAnnotationElement);
+
+ return _possibleConstructorReturn(this, _getPrototypeOf(CheckboxWidgetAnnotationElement).call(this, parameters, parameters.renderInteractiveForms));
+ }
+
+ _createClass(CheckboxWidgetAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'buttonWidgetAnnotation checkBox';
+ var element = document.createElement('input');
+ element.disabled = this.data.readOnly;
+ element.type = 'checkbox';
+
+ if (this.data.fieldValue && this.data.fieldValue !== 'Off') {
+ element.setAttribute('checked', true);
+ }
+
+ this.container.appendChild(element);
+ return this.container;
+ }
+ }]);
+
+ return CheckboxWidgetAnnotationElement;
+}(WidgetAnnotationElement);
+
+var RadioButtonWidgetAnnotationElement =
+/*#__PURE__*/
+function (_WidgetAnnotationElem3) {
+ _inherits(RadioButtonWidgetAnnotationElement, _WidgetAnnotationElem3);
+
+ function RadioButtonWidgetAnnotationElement(parameters) {
+ _classCallCheck(this, RadioButtonWidgetAnnotationElement);
+
+ return _possibleConstructorReturn(this, _getPrototypeOf(RadioButtonWidgetAnnotationElement).call(this, parameters, parameters.renderInteractiveForms));
+ }
+
+ _createClass(RadioButtonWidgetAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'buttonWidgetAnnotation radioButton';
+ var element = document.createElement('input');
+ element.disabled = this.data.readOnly;
+ element.type = 'radio';
+ element.name = this.data.fieldName;
+
+ if (this.data.fieldValue === this.data.buttonValue) {
+ element.setAttribute('checked', true);
+ }
+
+ this.container.appendChild(element);
+ return this.container;
+ }
+ }]);
+
+ return RadioButtonWidgetAnnotationElement;
+}(WidgetAnnotationElement);
+
+var PushButtonWidgetAnnotationElement =
+/*#__PURE__*/
+function (_LinkAnnotationElemen) {
+ _inherits(PushButtonWidgetAnnotationElement, _LinkAnnotationElemen);
+
+ function PushButtonWidgetAnnotationElement() {
+ _classCallCheck(this, PushButtonWidgetAnnotationElement);
+
+ return _possibleConstructorReturn(this, _getPrototypeOf(PushButtonWidgetAnnotationElement).apply(this, arguments));
+ }
+
+ _createClass(PushButtonWidgetAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ var container = _get(_getPrototypeOf(PushButtonWidgetAnnotationElement.prototype), "render", this).call(this);
+
+ container.className = 'buttonWidgetAnnotation pushButton';
+ return container;
+ }
+ }]);
+
+ return PushButtonWidgetAnnotationElement;
+}(LinkAnnotationElement);
+
+var ChoiceWidgetAnnotationElement =
+/*#__PURE__*/
+function (_WidgetAnnotationElem4) {
+ _inherits(ChoiceWidgetAnnotationElement, _WidgetAnnotationElem4);
+
+ function ChoiceWidgetAnnotationElement(parameters) {
+ _classCallCheck(this, ChoiceWidgetAnnotationElement);
+
+ return _possibleConstructorReturn(this, _getPrototypeOf(ChoiceWidgetAnnotationElement).call(this, parameters, parameters.renderInteractiveForms));
+ }
+
+ _createClass(ChoiceWidgetAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'choiceWidgetAnnotation';
+ var selectElement = document.createElement('select');
+ selectElement.disabled = this.data.readOnly;
+
+ if (!this.data.combo) {
+ selectElement.size = this.data.options.length;
+
+ if (this.data.multiSelect) {
+ selectElement.multiple = true;
+ }
+ }
+
+ for (var i = 0, ii = this.data.options.length; i < ii; i++) {
+ var option = this.data.options[i];
+ var optionElement = document.createElement('option');
+ optionElement.textContent = option.displayValue;
+ optionElement.value = option.exportValue;
+
+ if (this.data.fieldValue.includes(option.displayValue)) {
+ optionElement.setAttribute('selected', true);
+ }
+
+ selectElement.appendChild(optionElement);
+ }
+
+ this.container.appendChild(selectElement);
+ return this.container;
+ }
+ }]);
+
+ return ChoiceWidgetAnnotationElement;
+}(WidgetAnnotationElement);
+
+var PopupAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement4) {
+ _inherits(PopupAnnotationElement, _AnnotationElement4);
+
+ function PopupAnnotationElement(parameters) {
+ _classCallCheck(this, PopupAnnotationElement);
+
+ var isRenderable = !!(parameters.data.title || parameters.data.contents);
+ return _possibleConstructorReturn(this, _getPrototypeOf(PopupAnnotationElement).call(this, parameters, isRenderable));
+ }
+
+ _createClass(PopupAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ var IGNORE_TYPES = ['Line', 'Square', 'Circle', 'PolyLine', 'Polygon', 'Ink'];
+ this.container.className = 'popupAnnotation';
+
+ if (IGNORE_TYPES.includes(this.data.parentType)) {
+ return this.container;
+ }
+
+ var selector = '[data-annotation-id="' + this.data.parentId + '"]';
+ var parentElement = this.layer.querySelector(selector);
+
+ if (!parentElement) {
+ return this.container;
+ }
+
+ var popup = new PopupElement({
+ container: this.container,
+ trigger: parentElement,
+ color: this.data.color,
+ title: this.data.title,
+ modificationDate: this.data.modificationDate,
+ contents: this.data.contents
+ });
+ var parentLeft = parseFloat(parentElement.style.left);
+ var parentWidth = parseFloat(parentElement.style.width);
+ this.container.style.transformOrigin = -(parentLeft + parentWidth) + 'px -' + parentElement.style.top;
+ this.container.style.left = parentLeft + parentWidth + 'px';
+ this.container.appendChild(popup.render());
+ return this.container;
+ }
+ }]);
+
+ return PopupAnnotationElement;
+}(AnnotationElement);
+
+var PopupElement =
+/*#__PURE__*/
+function () {
+ function PopupElement(parameters) {
+ _classCallCheck(this, PopupElement);
+
+ this.container = parameters.container;
+ this.trigger = parameters.trigger;
+ this.color = parameters.color;
+ this.title = parameters.title;
+ this.modificationDate = parameters.modificationDate;
+ this.contents = parameters.contents;
+ this.hideWrapper = parameters.hideWrapper || false;
+ this.pinned = false;
+ }
+
+ _createClass(PopupElement, [{
+ key: "render",
+ value: function render() {
+ var BACKGROUND_ENLIGHT = 0.7;
+ var wrapper = document.createElement('div');
+ wrapper.className = 'popupWrapper';
+ this.hideElement = this.hideWrapper ? wrapper : this.container;
+ this.hideElement.setAttribute('hidden', true);
+ var popup = document.createElement('div');
+ popup.className = 'popup';
+ var color = this.color;
+
+ if (color) {
+ var r = BACKGROUND_ENLIGHT * (255 - color[0]) + color[0];
+ var g = BACKGROUND_ENLIGHT * (255 - color[1]) + color[1];
+ var b = BACKGROUND_ENLIGHT * (255 - color[2]) + color[2];
+ popup.style.backgroundColor = _util.Util.makeCssRgb(r | 0, g | 0, b | 0);
+ }
+
+ var title = document.createElement('h1');
+ title.textContent = this.title;
+ popup.appendChild(title);
+
+ var dateObject = _display_utils.PDFDateString.toDateObject(this.modificationDate);
+
+ if (dateObject) {
+ var modificationDate = document.createElement('span');
+ modificationDate.textContent = '{{date}}, {{time}}';
+ modificationDate.dataset.l10nId = 'annotation_date_string';
+ modificationDate.dataset.l10nArgs = JSON.stringify({
+ date: dateObject.toLocaleDateString(),
+ time: dateObject.toLocaleTimeString()
+ });
+ popup.appendChild(modificationDate);
+ }
+
+ var contents = this._formatContents(this.contents);
+
+ popup.appendChild(contents);
+ this.trigger.addEventListener('click', this._toggle.bind(this));
+ this.trigger.addEventListener('mouseover', this._show.bind(this, false));
+ this.trigger.addEventListener('mouseout', this._hide.bind(this, false));
+ popup.addEventListener('click', this._hide.bind(this, true));
+ wrapper.appendChild(popup);
+ return wrapper;
+ }
+ }, {
+ key: "_formatContents",
+ value: function _formatContents(contents) {
+ var p = document.createElement('p');
+ var lines = contents.split(/(?:\r\n?|\n)/);
+
+ for (var i = 0, ii = lines.length; i < ii; ++i) {
+ var line = lines[i];
+ p.appendChild(document.createTextNode(line));
+
+ if (i < ii - 1) {
+ p.appendChild(document.createElement('br'));
+ }
+ }
+
+ return p;
+ }
+ }, {
+ key: "_toggle",
+ value: function _toggle() {
+ if (this.pinned) {
+ this._hide(true);
+ } else {
+ this._show(true);
+ }
+ }
+ }, {
+ key: "_show",
+ value: function _show() {
+ var pin = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+
+ if (pin) {
+ this.pinned = true;
+ }
+
+ if (this.hideElement.hasAttribute('hidden')) {
+ this.hideElement.removeAttribute('hidden');
+ this.container.style.zIndex += 1;
+ }
+ }
+ }, {
+ key: "_hide",
+ value: function _hide() {
+ var unpin = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
+
+ if (unpin) {
+ this.pinned = false;
+ }
+
+ if (!this.hideElement.hasAttribute('hidden') && !this.pinned) {
+ this.hideElement.setAttribute('hidden', true);
+ this.container.style.zIndex -= 1;
+ }
+ }
+ }]);
+
+ return PopupElement;
+}();
+
+var FreeTextAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement5) {
+ _inherits(FreeTextAnnotationElement, _AnnotationElement5);
+
+ function FreeTextAnnotationElement(parameters) {
+ _classCallCheck(this, FreeTextAnnotationElement);
+
+ var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents);
+ return _possibleConstructorReturn(this, _getPrototypeOf(FreeTextAnnotationElement).call(this, parameters, isRenderable, true));
+ }
+
+ _createClass(FreeTextAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'freeTextAnnotation';
+
+ if (!this.data.hasPopup) {
+ this._createPopup(this.container, null, this.data);
+ }
+
+ return this.container;
+ }
+ }]);
+
+ return FreeTextAnnotationElement;
+}(AnnotationElement);
+
+var LineAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement6) {
+ _inherits(LineAnnotationElement, _AnnotationElement6);
+
+ function LineAnnotationElement(parameters) {
+ _classCallCheck(this, LineAnnotationElement);
+
+ var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents);
+ return _possibleConstructorReturn(this, _getPrototypeOf(LineAnnotationElement).call(this, parameters, isRenderable, true));
+ }
+
+ _createClass(LineAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'lineAnnotation';
+ var data = this.data;
+ var width = data.rect[2] - data.rect[0];
+ var height = data.rect[3] - data.rect[1];
+ var svg = this.svgFactory.create(width, height);
+ var line = this.svgFactory.createElement('svg:line');
+ line.setAttribute('x1', data.rect[2] - data.lineCoordinates[0]);
+ line.setAttribute('y1', data.rect[3] - data.lineCoordinates[1]);
+ line.setAttribute('x2', data.rect[2] - data.lineCoordinates[2]);
+ line.setAttribute('y2', data.rect[3] - data.lineCoordinates[3]);
+ line.setAttribute('stroke-width', data.borderStyle.width);
+ line.setAttribute('stroke', 'transparent');
+ svg.appendChild(line);
+ this.container.append(svg);
+
+ this._createPopup(this.container, line, data);
+
+ return this.container;
+ }
+ }]);
+
+ return LineAnnotationElement;
+}(AnnotationElement);
+
+var SquareAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement7) {
+ _inherits(SquareAnnotationElement, _AnnotationElement7);
+
+ function SquareAnnotationElement(parameters) {
+ _classCallCheck(this, SquareAnnotationElement);
+
+ var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents);
+ return _possibleConstructorReturn(this, _getPrototypeOf(SquareAnnotationElement).call(this, parameters, isRenderable, true));
+ }
+
+ _createClass(SquareAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'squareAnnotation';
+ var data = this.data;
+ var width = data.rect[2] - data.rect[0];
+ var height = data.rect[3] - data.rect[1];
+ var svg = this.svgFactory.create(width, height);
+ var borderWidth = data.borderStyle.width;
+ var square = this.svgFactory.createElement('svg:rect');
+ square.setAttribute('x', borderWidth / 2);
+ square.setAttribute('y', borderWidth / 2);
+ square.setAttribute('width', width - borderWidth);
+ square.setAttribute('height', height - borderWidth);
+ square.setAttribute('stroke-width', borderWidth);
+ square.setAttribute('stroke', 'transparent');
+ square.setAttribute('fill', 'none');
+ svg.appendChild(square);
+ this.container.append(svg);
+
+ this._createPopup(this.container, square, data);
+
+ return this.container;
+ }
+ }]);
+
+ return SquareAnnotationElement;
+}(AnnotationElement);
+
+var CircleAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement8) {
+ _inherits(CircleAnnotationElement, _AnnotationElement8);
+
+ function CircleAnnotationElement(parameters) {
+ _classCallCheck(this, CircleAnnotationElement);
+
+ var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents);
+ return _possibleConstructorReturn(this, _getPrototypeOf(CircleAnnotationElement).call(this, parameters, isRenderable, true));
+ }
+
+ _createClass(CircleAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'circleAnnotation';
+ var data = this.data;
+ var width = data.rect[2] - data.rect[0];
+ var height = data.rect[3] - data.rect[1];
+ var svg = this.svgFactory.create(width, height);
+ var borderWidth = data.borderStyle.width;
+ var circle = this.svgFactory.createElement('svg:ellipse');
+ circle.setAttribute('cx', width / 2);
+ circle.setAttribute('cy', height / 2);
+ circle.setAttribute('rx', width / 2 - borderWidth / 2);
+ circle.setAttribute('ry', height / 2 - borderWidth / 2);
+ circle.setAttribute('stroke-width', borderWidth);
+ circle.setAttribute('stroke', 'transparent');
+ circle.setAttribute('fill', 'none');
+ svg.appendChild(circle);
+ this.container.append(svg);
+
+ this._createPopup(this.container, circle, data);
+
+ return this.container;
+ }
+ }]);
+
+ return CircleAnnotationElement;
+}(AnnotationElement);
+
+var PolylineAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement9) {
+ _inherits(PolylineAnnotationElement, _AnnotationElement9);
+
+ function PolylineAnnotationElement(parameters) {
+ var _this3;
+
+ _classCallCheck(this, PolylineAnnotationElement);
+
+ var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents);
+ _this3 = _possibleConstructorReturn(this, _getPrototypeOf(PolylineAnnotationElement).call(this, parameters, isRenderable, true));
+ _this3.containerClassName = 'polylineAnnotation';
+ _this3.svgElementName = 'svg:polyline';
+ return _this3;
+ }
+
+ _createClass(PolylineAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = this.containerClassName;
+ var data = this.data;
+ var width = data.rect[2] - data.rect[0];
+ var height = data.rect[3] - data.rect[1];
+ var svg = this.svgFactory.create(width, height);
+ var vertices = data.vertices;
+ var points = [];
+
+ for (var i = 0, ii = vertices.length; i < ii; i++) {
+ var x = vertices[i].x - data.rect[0];
+ var y = data.rect[3] - vertices[i].y;
+ points.push(x + ',' + y);
+ }
+
+ points = points.join(' ');
+ var borderWidth = data.borderStyle.width;
+ var polyline = this.svgFactory.createElement(this.svgElementName);
+ polyline.setAttribute('points', points);
+ polyline.setAttribute('stroke-width', borderWidth);
+ polyline.setAttribute('stroke', 'transparent');
+ polyline.setAttribute('fill', 'none');
+ svg.appendChild(polyline);
+ this.container.append(svg);
+
+ this._createPopup(this.container, polyline, data);
+
+ return this.container;
+ }
+ }]);
+
+ return PolylineAnnotationElement;
+}(AnnotationElement);
+
+var PolygonAnnotationElement =
+/*#__PURE__*/
+function (_PolylineAnnotationEl) {
+ _inherits(PolygonAnnotationElement, _PolylineAnnotationEl);
+
+ function PolygonAnnotationElement(parameters) {
+ var _this4;
+
+ _classCallCheck(this, PolygonAnnotationElement);
+
+ _this4 = _possibleConstructorReturn(this, _getPrototypeOf(PolygonAnnotationElement).call(this, parameters));
+ _this4.containerClassName = 'polygonAnnotation';
+ _this4.svgElementName = 'svg:polygon';
+ return _this4;
+ }
+
+ return PolygonAnnotationElement;
+}(PolylineAnnotationElement);
+
+var CaretAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement10) {
+ _inherits(CaretAnnotationElement, _AnnotationElement10);
+
+ function CaretAnnotationElement(parameters) {
+ _classCallCheck(this, CaretAnnotationElement);
+
+ var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents);
+ return _possibleConstructorReturn(this, _getPrototypeOf(CaretAnnotationElement).call(this, parameters, isRenderable, true));
+ }
+
+ _createClass(CaretAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'caretAnnotation';
+
+ if (!this.data.hasPopup) {
+ this._createPopup(this.container, null, this.data);
+ }
+
+ return this.container;
+ }
+ }]);
+
+ return CaretAnnotationElement;
+}(AnnotationElement);
+
+var InkAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement11) {
+ _inherits(InkAnnotationElement, _AnnotationElement11);
+
+ function InkAnnotationElement(parameters) {
+ var _this5;
+
+ _classCallCheck(this, InkAnnotationElement);
+
+ var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents);
+ _this5 = _possibleConstructorReturn(this, _getPrototypeOf(InkAnnotationElement).call(this, parameters, isRenderable, true));
+ _this5.containerClassName = 'inkAnnotation';
+ _this5.svgElementName = 'svg:polyline';
+ return _this5;
+ }
+
+ _createClass(InkAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = this.containerClassName;
+ var data = this.data;
+ var width = data.rect[2] - data.rect[0];
+ var height = data.rect[3] - data.rect[1];
+ var svg = this.svgFactory.create(width, height);
+ var inkLists = data.inkLists;
+
+ for (var i = 0, ii = inkLists.length; i < ii; i++) {
+ var inkList = inkLists[i];
+ var points = [];
+
+ for (var j = 0, jj = inkList.length; j < jj; j++) {
+ var x = inkList[j].x - data.rect[0];
+ var y = data.rect[3] - inkList[j].y;
+ points.push(x + ',' + y);
+ }
+
+ points = points.join(' ');
+ var borderWidth = data.borderStyle.width;
+ var polyline = this.svgFactory.createElement(this.svgElementName);
+ polyline.setAttribute('points', points);
+ polyline.setAttribute('stroke-width', borderWidth);
+ polyline.setAttribute('stroke', 'transparent');
+ polyline.setAttribute('fill', 'none');
+
+ this._createPopup(this.container, polyline, data);
+
+ svg.appendChild(polyline);
+ }
+
+ this.container.append(svg);
+ return this.container;
+ }
+ }]);
+
+ return InkAnnotationElement;
+}(AnnotationElement);
+
+var HighlightAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement12) {
+ _inherits(HighlightAnnotationElement, _AnnotationElement12);
+
+ function HighlightAnnotationElement(parameters) {
+ _classCallCheck(this, HighlightAnnotationElement);
+
+ var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents);
+ return _possibleConstructorReturn(this, _getPrototypeOf(HighlightAnnotationElement).call(this, parameters, isRenderable, true));
+ }
+
+ _createClass(HighlightAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'highlightAnnotation';
+
+ if (!this.data.hasPopup) {
+ this._createPopup(this.container, null, this.data);
+ }
+
+ return this.container;
+ }
+ }]);
+
+ return HighlightAnnotationElement;
+}(AnnotationElement);
+
+var UnderlineAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement13) {
+ _inherits(UnderlineAnnotationElement, _AnnotationElement13);
+
+ function UnderlineAnnotationElement(parameters) {
+ _classCallCheck(this, UnderlineAnnotationElement);
+
+ var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents);
+ return _possibleConstructorReturn(this, _getPrototypeOf(UnderlineAnnotationElement).call(this, parameters, isRenderable, true));
+ }
+
+ _createClass(UnderlineAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'underlineAnnotation';
+
+ if (!this.data.hasPopup) {
+ this._createPopup(this.container, null, this.data);
+ }
+
+ return this.container;
+ }
+ }]);
+
+ return UnderlineAnnotationElement;
+}(AnnotationElement);
+
+var SquigglyAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement14) {
+ _inherits(SquigglyAnnotationElement, _AnnotationElement14);
+
+ function SquigglyAnnotationElement(parameters) {
+ _classCallCheck(this, SquigglyAnnotationElement);
+
+ var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents);
+ return _possibleConstructorReturn(this, _getPrototypeOf(SquigglyAnnotationElement).call(this, parameters, isRenderable, true));
+ }
+
+ _createClass(SquigglyAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'squigglyAnnotation';
+
+ if (!this.data.hasPopup) {
+ this._createPopup(this.container, null, this.data);
+ }
+
+ return this.container;
+ }
+ }]);
+
+ return SquigglyAnnotationElement;
+}(AnnotationElement);
+
+var StrikeOutAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement15) {
+ _inherits(StrikeOutAnnotationElement, _AnnotationElement15);
+
+ function StrikeOutAnnotationElement(parameters) {
+ _classCallCheck(this, StrikeOutAnnotationElement);
+
+ var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents);
+ return _possibleConstructorReturn(this, _getPrototypeOf(StrikeOutAnnotationElement).call(this, parameters, isRenderable, true));
+ }
+
+ _createClass(StrikeOutAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'strikeoutAnnotation';
+
+ if (!this.data.hasPopup) {
+ this._createPopup(this.container, null, this.data);
+ }
+
+ return this.container;
+ }
+ }]);
+
+ return StrikeOutAnnotationElement;
+}(AnnotationElement);
+
+var StampAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement16) {
+ _inherits(StampAnnotationElement, _AnnotationElement16);
+
+ function StampAnnotationElement(parameters) {
+ _classCallCheck(this, StampAnnotationElement);
+
+ var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents);
+ return _possibleConstructorReturn(this, _getPrototypeOf(StampAnnotationElement).call(this, parameters, isRenderable, true));
+ }
+
+ _createClass(StampAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'stampAnnotation';
+
+ if (!this.data.hasPopup) {
+ this._createPopup(this.container, null, this.data);
+ }
+
+ return this.container;
+ }
+ }]);
+
+ return StampAnnotationElement;
+}(AnnotationElement);
+
+var FileAttachmentAnnotationElement =
+/*#__PURE__*/
+function (_AnnotationElement17) {
+ _inherits(FileAttachmentAnnotationElement, _AnnotationElement17);
+
+ function FileAttachmentAnnotationElement(parameters) {
+ var _this6;
+
+ _classCallCheck(this, FileAttachmentAnnotationElement);
+
+ _this6 = _possibleConstructorReturn(this, _getPrototypeOf(FileAttachmentAnnotationElement).call(this, parameters, true));
+ var _this6$data$file = _this6.data.file,
+ filename = _this6$data$file.filename,
+ content = _this6$data$file.content;
+ _this6.filename = (0, _display_utils.getFilenameFromUrl)(filename);
+ _this6.content = content;
+
+ if (_this6.linkService.eventBus) {
+ _this6.linkService.eventBus.dispatch('fileattachmentannotation', {
+ source: _assertThisInitialized(_this6),
+ id: (0, _util.stringToPDFString)(filename),
+ filename: filename,
+ content: content
+ });
+ }
+
+ return _this6;
+ }
+
+ _createClass(FileAttachmentAnnotationElement, [{
+ key: "render",
+ value: function render() {
+ this.container.className = 'fileAttachmentAnnotation';
+ var trigger = document.createElement('div');
+ trigger.style.height = this.container.style.height;
+ trigger.style.width = this.container.style.width;
+ trigger.addEventListener('dblclick', this._download.bind(this));
+
+ if (!this.data.hasPopup && (this.data.title || this.data.contents)) {
+ this._createPopup(this.container, trigger, this.data);
+ }
+
+ this.container.appendChild(trigger);
+ return this.container;
+ }
+ }, {
+ key: "_download",
+ value: function _download() {
+ if (!this.downloadManager) {
+ (0, _util.warn)('Download cannot be started due to unavailable download manager');
+ return;
+ }
+
+ this.downloadManager.downloadData(this.content, this.filename, '');
+ }
+ }]);
+
+ return FileAttachmentAnnotationElement;
+}(AnnotationElement);
+
+var AnnotationLayer =
+/*#__PURE__*/
+function () {
+ function AnnotationLayer() {
+ _classCallCheck(this, AnnotationLayer);
+ }
+
+ _createClass(AnnotationLayer, null, [{
+ key: "render",
+ value: function render(parameters) {
+ for (var i = 0, ii = parameters.annotations.length; i < ii; i++) {
+ var data = parameters.annotations[i];
+
+ if (!data) {
+ continue;
+ }
+
+ var element = AnnotationElementFactory.create({
+ data: data,
+ layer: parameters.div,
+ page: parameters.page,
+ viewport: parameters.viewport,
+ linkService: parameters.linkService,
+ downloadManager: parameters.downloadManager,
+ imageResourcesPath: parameters.imageResourcesPath || '',
+ renderInteractiveForms: parameters.renderInteractiveForms || false,
+ svgFactory: new _display_utils.DOMSVGFactory()
+ });
+
+ if (element.isRenderable) {
+ parameters.div.appendChild(element.render());
+ }
+ }
+ }
+ }, {
+ key: "update",
+ value: function update(parameters) {
+ for (var i = 0, ii = parameters.annotations.length; i < ii; i++) {
+ var data = parameters.annotations[i];
+ var element = parameters.div.querySelector('[data-annotation-id="' + data.id + '"]');
+
+ if (element) {
+ element.style.transform = 'matrix(' + parameters.viewport.transform.join(',') + ')';
+ }
+ }
+
+ parameters.div.removeAttribute('hidden');
+ }
+ }]);
+
+ return AnnotationLayer;
+}();
+
+exports.AnnotationLayer = AnnotationLayer;
+
+/***/ }),
+/* 164 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.SVGGraphics = void 0;
+
+var _util = __w_pdfjs_require__(1);
+
+var _display_utils = __w_pdfjs_require__(151);
+
+var _is_node = _interopRequireDefault(__w_pdfjs_require__(4));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
+
+function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
+
+function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
+
+function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }
+
+function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
+
+function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
+
+function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
+
+function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var SVGGraphics = function SVGGraphics() {
+ throw new Error('Not implemented: SVGGraphics');
+};
+
+exports.SVGGraphics = SVGGraphics;
+{
+ var opListToTree = function opListToTree(opList) {
+ var opTree = [];
+ var tmp = [];
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = opList[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var opListElement = _step.value;
+
+ if (opListElement.fn === 'save') {
+ opTree.push({
+ 'fnId': 92,
+ 'fn': 'group',
+ 'items': []
+ });
+ tmp.push(opTree);
+ opTree = opTree[opTree.length - 1].items;
+ continue;
+ }
+
+ if (opListElement.fn === 'restore') {
+ opTree = tmp.pop();
+ } else {
+ opTree.push(opListElement);
+ }
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator["return"] != null) {
+ _iterator["return"]();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+
+ return opTree;
+ };
+
+ var pf = function pf(value) {
+ if (Number.isInteger(value)) {
+ return value.toString();
+ }
+
+ var s = value.toFixed(10);
+ var i = s.length - 1;
+
+ if (s[i] !== '0') {
+ return s;
+ }
+
+ do {
+ i--;
+ } while (s[i] === '0');
+
+ return s.substring(0, s[i] === '.' ? i : i + 1);
+ };
+
+ var pm = function pm(m) {
+ if (m[4] === 0 && m[5] === 0) {
+ if (m[1] === 0 && m[2] === 0) {
+ if (m[0] === 1 && m[3] === 1) {
+ return '';
+ }
+
+ return "scale(".concat(pf(m[0]), " ").concat(pf(m[3]), ")");
+ }
+
+ if (m[0] === m[3] && m[1] === -m[2]) {
+ var a = Math.acos(m[0]) * 180 / Math.PI;
+ return "rotate(".concat(pf(a), ")");
+ }
+ } else {
+ if (m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1) {
+ return "translate(".concat(pf(m[4]), " ").concat(pf(m[5]), ")");
+ }
+ }
+
+ return "matrix(".concat(pf(m[0]), " ").concat(pf(m[1]), " ").concat(pf(m[2]), " ").concat(pf(m[3]), " ").concat(pf(m[4]), " ") + "".concat(pf(m[5]), ")");
+ };
+
+ var SVG_DEFAULTS = {
+ fontStyle: 'normal',
+ fontWeight: 'normal',
+ fillColor: '#000000'
+ };
+ var XML_NS = 'http://www.w3.org/XML/1998/namespace';
+ var XLINK_NS = 'http://www.w3.org/1999/xlink';
+ var LINE_CAP_STYLES = ['butt', 'round', 'square'];
+ var LINE_JOIN_STYLES = ['miter', 'round', 'bevel'];
+
+ var convertImgDataToPng = function () {
+ var PNG_HEADER = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
+ var CHUNK_WRAPPER_SIZE = 12;
+ var crcTable = new Int32Array(256);
+
+ for (var i = 0; i < 256; i++) {
+ var c = i;
+
+ for (var h = 0; h < 8; h++) {
+ if (c & 1) {
+ c = 0xedB88320 ^ c >> 1 & 0x7fffffff;
+ } else {
+ c = c >> 1 & 0x7fffffff;
+ }
+ }
+
+ crcTable[i] = c;
+ }
+
+ function crc32(data, start, end) {
+ var crc = -1;
+
+ for (var _i = start; _i < end; _i++) {
+ var a = (crc ^ data[_i]) & 0xff;
+ var b = crcTable[a];
+ crc = crc >>> 8 ^ b;
+ }
+
+ return crc ^ -1;
+ }
+
+ function writePngChunk(type, body, data, offset) {
+ var p = offset;
+ var len = body.length;
+ data[p] = len >> 24 & 0xff;
+ data[p + 1] = len >> 16 & 0xff;
+ data[p + 2] = len >> 8 & 0xff;
+ data[p + 3] = len & 0xff;
+ p += 4;
+ data[p] = type.charCodeAt(0) & 0xff;
+ data[p + 1] = type.charCodeAt(1) & 0xff;
+ data[p + 2] = type.charCodeAt(2) & 0xff;
+ data[p + 3] = type.charCodeAt(3) & 0xff;
+ p += 4;
+ data.set(body, p);
+ p += body.length;
+ var crc = crc32(data, offset + 4, p);
+ data[p] = crc >> 24 & 0xff;
+ data[p + 1] = crc >> 16 & 0xff;
+ data[p + 2] = crc >> 8 & 0xff;
+ data[p + 3] = crc & 0xff;
+ }
+
+ function adler32(data, start, end) {
+ var a = 1;
+ var b = 0;
+
+ for (var _i2 = start; _i2 < end; ++_i2) {
+ a = (a + (data[_i2] & 0xff)) % 65521;
+ b = (b + a) % 65521;
+ }
+
+ return b << 16 | a;
+ }
+
+ function deflateSync(literals) {
+ if (!(0, _is_node["default"])()) {
+ return deflateSyncUncompressed(literals);
+ }
+
+ try {
+ var input;
+
+ if (parseInt(process.versions.node) >= 8) {
+ input = literals;
+ } else {
+ input = new Buffer(literals);
+ }
+
+ var output = require('zlib').deflateSync(input, {
+ level: 9
+ });
+
+ return output instanceof Uint8Array ? output : new Uint8Array(output);
+ } catch (e) {
+ (0, _util.warn)('Not compressing PNG because zlib.deflateSync is unavailable: ' + e);
+ }
+
+ return deflateSyncUncompressed(literals);
+ }
+
+ function deflateSyncUncompressed(literals) {
+ var len = literals.length;
+ var maxBlockLength = 0xFFFF;
+ var deflateBlocks = Math.ceil(len / maxBlockLength);
+ var idat = new Uint8Array(2 + len + deflateBlocks * 5 + 4);
+ var pi = 0;
+ idat[pi++] = 0x78;
+ idat[pi++] = 0x9c;
+ var pos = 0;
+
+ while (len > maxBlockLength) {
+ idat[pi++] = 0x00;
+ idat[pi++] = 0xff;
+ idat[pi++] = 0xff;
+ idat[pi++] = 0x00;
+ idat[pi++] = 0x00;
+ idat.set(literals.subarray(pos, pos + maxBlockLength), pi);
+ pi += maxBlockLength;
+ pos += maxBlockLength;
+ len -= maxBlockLength;
+ }
+
+ idat[pi++] = 0x01;
+ idat[pi++] = len & 0xff;
+ idat[pi++] = len >> 8 & 0xff;
+ idat[pi++] = ~len & 0xffff & 0xff;
+ idat[pi++] = (~len & 0xffff) >> 8 & 0xff;
+ idat.set(literals.subarray(pos), pi);
+ pi += literals.length - pos;
+ var adler = adler32(literals, 0, literals.length);
+ idat[pi++] = adler >> 24 & 0xff;
+ idat[pi++] = adler >> 16 & 0xff;
+ idat[pi++] = adler >> 8 & 0xff;
+ idat[pi++] = adler & 0xff;
+ return idat;
+ }
+
+ function encode(imgData, kind, forceDataSchema, isMask) {
+ var width = imgData.width;
+ var height = imgData.height;
+ var bitDepth, colorType, lineSize;
+ var bytes = imgData.data;
+
+ switch (kind) {
+ case _util.ImageKind.GRAYSCALE_1BPP:
+ colorType = 0;
+ bitDepth = 1;
+ lineSize = width + 7 >> 3;
+ break;
+
+ case _util.ImageKind.RGB_24BPP:
+ colorType = 2;
+ bitDepth = 8;
+ lineSize = width * 3;
+ break;
+
+ case _util.ImageKind.RGBA_32BPP:
+ colorType = 6;
+ bitDepth = 8;
+ lineSize = width * 4;
+ break;
+
+ default:
+ throw new Error('invalid format');
+ }
+
+ var literals = new Uint8Array((1 + lineSize) * height);
+ var offsetLiterals = 0,
+ offsetBytes = 0;
+
+ for (var y = 0; y < height; ++y) {
+ literals[offsetLiterals++] = 0;
+ literals.set(bytes.subarray(offsetBytes, offsetBytes + lineSize), offsetLiterals);
+ offsetBytes += lineSize;
+ offsetLiterals += lineSize;
+ }
+
+ if (kind === _util.ImageKind.GRAYSCALE_1BPP && isMask) {
+ offsetLiterals = 0;
+
+ for (var _y = 0; _y < height; _y++) {
+ offsetLiterals++;
+
+ for (var _i3 = 0; _i3 < lineSize; _i3++) {
+ literals[offsetLiterals++] ^= 0xFF;
+ }
+ }
+ }
+
+ var ihdr = new Uint8Array([width >> 24 & 0xff, width >> 16 & 0xff, width >> 8 & 0xff, width & 0xff, height >> 24 & 0xff, height >> 16 & 0xff, height >> 8 & 0xff, height & 0xff, bitDepth, colorType, 0x00, 0x00, 0x00]);
+ var idat = deflateSync(literals);
+ var pngLength = PNG_HEADER.length + CHUNK_WRAPPER_SIZE * 3 + ihdr.length + idat.length;
+ var data = new Uint8Array(pngLength);
+ var offset = 0;
+ data.set(PNG_HEADER, offset);
+ offset += PNG_HEADER.length;
+ writePngChunk('IHDR', ihdr, data, offset);
+ offset += CHUNK_WRAPPER_SIZE + ihdr.length;
+ writePngChunk('IDATA', idat, data, offset);
+ offset += CHUNK_WRAPPER_SIZE + idat.length;
+ writePngChunk('IEND', new Uint8Array(0), data, offset);
+ return (0, _util.createObjectURL)(data, 'image/png', forceDataSchema);
+ }
+
+ return function convertImgDataToPng(imgData, forceDataSchema, isMask) {
+ var kind = imgData.kind === undefined ? _util.ImageKind.GRAYSCALE_1BPP : imgData.kind;
+ return encode(imgData, kind, forceDataSchema, isMask);
+ };
+ }();
+
+ var SVGExtraState =
+ /*#__PURE__*/
+ function () {
+ function SVGExtraState() {
+ _classCallCheck(this, SVGExtraState);
+
+ this.fontSizeScale = 1;
+ this.fontWeight = SVG_DEFAULTS.fontWeight;
+ this.fontSize = 0;
+ this.textMatrix = _util.IDENTITY_MATRIX;
+ this.fontMatrix = _util.FONT_IDENTITY_MATRIX;
+ this.leading = 0;
+ this.textRenderingMode = _util.TextRenderingMode.FILL;
+ this.textMatrixScale = 1;
+ this.x = 0;
+ this.y = 0;
+ this.lineX = 0;
+ this.lineY = 0;
+ this.charSpacing = 0;
+ this.wordSpacing = 0;
+ this.textHScale = 1;
+ this.textRise = 0;
+ this.fillColor = SVG_DEFAULTS.fillColor;
+ this.strokeColor = '#000000';
+ this.fillAlpha = 1;
+ this.strokeAlpha = 1;
+ this.lineWidth = 1;
+ this.lineJoin = '';
+ this.lineCap = '';
+ this.miterLimit = 0;
+ this.dashArray = [];
+ this.dashPhase = 0;
+ this.dependencies = [];
+ this.activeClipUrl = null;
+ this.clipGroup = null;
+ this.maskId = '';
+ }
+
+ _createClass(SVGExtraState, [{
+ key: "clone",
+ value: function clone() {
+ return Object.create(this);
+ }
+ }, {
+ key: "setCurrentPoint",
+ value: function setCurrentPoint(x, y) {
+ this.x = x;
+ this.y = y;
+ }
+ }]);
+
+ return SVGExtraState;
+ }();
+
+ var clipCount = 0;
+ var maskCount = 0;
+ var shadingCount = 0;
+
+ exports.SVGGraphics = SVGGraphics =
+ /*#__PURE__*/
+ function () {
+ function SVGGraphics(commonObjs, objs, forceDataSchema) {
+ _classCallCheck(this, SVGGraphics);
+
+ this.svgFactory = new _display_utils.DOMSVGFactory();
+ this.current = new SVGExtraState();
+ this.transformMatrix = _util.IDENTITY_MATRIX;
+ this.transformStack = [];
+ this.extraStack = [];
+ this.commonObjs = commonObjs;
+ this.objs = objs;
+ this.pendingClip = null;
+ this.pendingEOFill = false;
+ this.embedFonts = false;
+ this.embeddedFonts = Object.create(null);
+ this.cssStyle = null;
+ this.forceDataSchema = !!forceDataSchema;
+ this._operatorIdMapping = [];
+
+ for (var op in _util.OPS) {
+ this._operatorIdMapping[_util.OPS[op]] = op;
+ }
+ }
+
+ _createClass(SVGGraphics, [{
+ key: "save",
+ value: function save() {
+ this.transformStack.push(this.transformMatrix);
+ var old = this.current;
+ this.extraStack.push(old);
+ this.current = old.clone();
+ }
+ }, {
+ key: "restore",
+ value: function restore() {
+ this.transformMatrix = this.transformStack.pop();
+ this.current = this.extraStack.pop();
+ this.pendingClip = null;
+ this.tgrp = null;
+ }
+ }, {
+ key: "group",
+ value: function group(items) {
+ this.save();
+ this.executeOpTree(items);
+ this.restore();
+ }
+ }, {
+ key: "loadDependencies",
+ value: function loadDependencies(operatorList) {
+ var _this = this;
+
+ var fnArray = operatorList.fnArray;
+ var argsArray = operatorList.argsArray;
+
+ for (var i = 0, ii = fnArray.length; i < ii; i++) {
+ if (fnArray[i] !== _util.OPS.dependency) {
+ continue;
+ }
+
+ var _iteratorNormalCompletion2 = true;
+ var _didIteratorError2 = false;
+ var _iteratorError2 = undefined;
+
+ try {
+ var _loop = function _loop() {
+ var obj = _step2.value;
+ var objsPool = obj.startsWith('g_') ? _this.commonObjs : _this.objs;
+ var promise = new Promise(function (resolve) {
+ objsPool.get(obj, resolve);
+ });
+
+ _this.current.dependencies.push(promise);
+ };
+
+ for (var _iterator2 = argsArray[i][Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
+ _loop();
+ }
+ } catch (err) {
+ _didIteratorError2 = true;
+ _iteratorError2 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) {
+ _iterator2["return"]();
+ }
+ } finally {
+ if (_didIteratorError2) {
+ throw _iteratorError2;
+ }
+ }
+ }
+ }
+
+ return Promise.all(this.current.dependencies);
+ }
+ }, {
+ key: "transform",
+ value: function transform(a, b, c, d, e, f) {
+ var transformMatrix = [a, b, c, d, e, f];
+ this.transformMatrix = _util.Util.transform(this.transformMatrix, transformMatrix);
+ this.tgrp = null;
+ }
+ }, {
+ key: "getSVG",
+ value: function getSVG(operatorList, viewport) {
+ var _this2 = this;
+
+ this.viewport = viewport;
+
+ var svgElement = this._initialize(viewport);
+
+ return this.loadDependencies(operatorList).then(function () {
+ _this2.transformMatrix = _util.IDENTITY_MATRIX;
+
+ _this2.executeOpTree(_this2.convertOpList(operatorList));
+
+ return svgElement;
+ });
+ }
+ }, {
+ key: "convertOpList",
+ value: function convertOpList(operatorList) {
+ var operatorIdMapping = this._operatorIdMapping;
+ var argsArray = operatorList.argsArray;
+ var fnArray = operatorList.fnArray;
+ var opList = [];
+
+ for (var i = 0, ii = fnArray.length; i < ii; i++) {
+ var fnId = fnArray[i];
+ opList.push({
+ 'fnId': fnId,
+ 'fn': operatorIdMapping[fnId],
+ 'args': argsArray[i]
+ });
+ }
+
+ return opListToTree(opList);
+ }
+ }, {
+ key: "executeOpTree",
+ value: function executeOpTree(opTree) {
+ var _iteratorNormalCompletion3 = true;
+ var _didIteratorError3 = false;
+ var _iteratorError3 = undefined;
+
+ try {
+ for (var _iterator3 = opTree[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
+ var opTreeElement = _step3.value;
+ var fn = opTreeElement.fn;
+ var fnId = opTreeElement.fnId;
+ var args = opTreeElement.args;
+
+ switch (fnId | 0) {
+ case _util.OPS.beginText:
+ this.beginText();
+ break;
+
+ case _util.OPS.dependency:
+ break;
+
+ case _util.OPS.setLeading:
+ this.setLeading(args);
+ break;
+
+ case _util.OPS.setLeadingMoveText:
+ this.setLeadingMoveText(args[0], args[1]);
+ break;
+
+ case _util.OPS.setFont:
+ this.setFont(args);
+ break;
+
+ case _util.OPS.showText:
+ this.showText(args[0]);
+ break;
+
+ case _util.OPS.showSpacedText:
+ this.showText(args[0]);
+ break;
+
+ case _util.OPS.endText:
+ this.endText();
+ break;
+
+ case _util.OPS.moveText:
+ this.moveText(args[0], args[1]);
+ break;
+
+ case _util.OPS.setCharSpacing:
+ this.setCharSpacing(args[0]);
+ break;
+
+ case _util.OPS.setWordSpacing:
+ this.setWordSpacing(args[0]);
+ break;
+
+ case _util.OPS.setHScale:
+ this.setHScale(args[0]);
+ break;
+
+ case _util.OPS.setTextMatrix:
+ this.setTextMatrix(args[0], args[1], args[2], args[3], args[4], args[5]);
+ break;
+
+ case _util.OPS.setTextRise:
+ this.setTextRise(args[0]);
+ break;
+
+ case _util.OPS.setTextRenderingMode:
+ this.setTextRenderingMode(args[0]);
+ break;
+
+ case _util.OPS.setLineWidth:
+ this.setLineWidth(args[0]);
+ break;
+
+ case _util.OPS.setLineJoin:
+ this.setLineJoin(args[0]);
+ break;
+
+ case _util.OPS.setLineCap:
+ this.setLineCap(args[0]);
+ break;
+
+ case _util.OPS.setMiterLimit:
+ this.setMiterLimit(args[0]);
+ break;
+
+ case _util.OPS.setFillRGBColor:
+ this.setFillRGBColor(args[0], args[1], args[2]);
+ break;
+
+ case _util.OPS.setStrokeRGBColor:
+ this.setStrokeRGBColor(args[0], args[1], args[2]);
+ break;
+
+ case _util.OPS.setStrokeColorN:
+ this.setStrokeColorN(args);
+ break;
+
+ case _util.OPS.setFillColorN:
+ this.setFillColorN(args);
+ break;
+
+ case _util.OPS.shadingFill:
+ this.shadingFill(args[0]);
+ break;
+
+ case _util.OPS.setDash:
+ this.setDash(args[0], args[1]);
+ break;
+
+ case _util.OPS.setRenderingIntent:
+ this.setRenderingIntent(args[0]);
+ break;
+
+ case _util.OPS.setFlatness:
+ this.setFlatness(args[0]);
+ break;
+
+ case _util.OPS.setGState:
+ this.setGState(args[0]);
+ break;
+
+ case _util.OPS.fill:
+ this.fill();
+ break;
+
+ case _util.OPS.eoFill:
+ this.eoFill();
+ break;
+
+ case _util.OPS.stroke:
+ this.stroke();
+ break;
+
+ case _util.OPS.fillStroke:
+ this.fillStroke();
+ break;
+
+ case _util.OPS.eoFillStroke:
+ this.eoFillStroke();
+ break;
+
+ case _util.OPS.clip:
+ this.clip('nonzero');
+ break;
+
+ case _util.OPS.eoClip:
+ this.clip('evenodd');
+ break;
+
+ case _util.OPS.paintSolidColorImageMask:
+ this.paintSolidColorImageMask();
+ break;
+
+ case _util.OPS.paintJpegXObject:
+ this.paintJpegXObject(args[0], args[1], args[2]);
+ break;
+
+ case _util.OPS.paintImageXObject:
+ this.paintImageXObject(args[0]);
+ break;
+
+ case _util.OPS.paintInlineImageXObject:
+ this.paintInlineImageXObject(args[0]);
+ break;
+
+ case _util.OPS.paintImageMaskXObject:
+ this.paintImageMaskXObject(args[0]);
+ break;
+
+ case _util.OPS.paintFormXObjectBegin:
+ this.paintFormXObjectBegin(args[0], args[1]);
+ break;
+
+ case _util.OPS.paintFormXObjectEnd:
+ this.paintFormXObjectEnd();
+ break;
+
+ case _util.OPS.closePath:
+ this.closePath();
+ break;
+
+ case _util.OPS.closeStroke:
+ this.closeStroke();
+ break;
+
+ case _util.OPS.closeFillStroke:
+ this.closeFillStroke();
+ break;
+
+ case _util.OPS.closeEOFillStroke:
+ this.closeEOFillStroke();
+ break;
+
+ case _util.OPS.nextLine:
+ this.nextLine();
+ break;
+
+ case _util.OPS.transform:
+ this.transform(args[0], args[1], args[2], args[3], args[4], args[5]);
+ break;
+
+ case _util.OPS.constructPath:
+ this.constructPath(args[0], args[1]);
+ break;
+
+ case _util.OPS.endPath:
+ this.endPath();
+ break;
+
+ case 92:
+ this.group(opTreeElement.items);
+ break;
+
+ default:
+ (0, _util.warn)("Unimplemented operator ".concat(fn));
+ break;
+ }
+ }
+ } catch (err) {
+ _didIteratorError3 = true;
+ _iteratorError3 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion3 && _iterator3["return"] != null) {
+ _iterator3["return"]();
+ }
+ } finally {
+ if (_didIteratorError3) {
+ throw _iteratorError3;
+ }
+ }
+ }
+ }
+ }, {
+ key: "setWordSpacing",
+ value: function setWordSpacing(wordSpacing) {
+ this.current.wordSpacing = wordSpacing;
+ }
+ }, {
+ key: "setCharSpacing",
+ value: function setCharSpacing(charSpacing) {
+ this.current.charSpacing = charSpacing;
+ }
+ }, {
+ key: "nextLine",
+ value: function nextLine() {
+ this.moveText(0, this.current.leading);
+ }
+ }, {
+ key: "setTextMatrix",
+ value: function setTextMatrix(a, b, c, d, e, f) {
+ var current = this.current;
+ current.textMatrix = current.lineMatrix = [a, b, c, d, e, f];
+ current.textMatrixScale = Math.sqrt(a * a + b * b);
+ current.x = current.lineX = 0;
+ current.y = current.lineY = 0;
+ current.xcoords = [];
+ current.tspan = this.svgFactory.createElement('svg:tspan');
+ current.tspan.setAttributeNS(null, 'font-family', current.fontFamily);
+ current.tspan.setAttributeNS(null, 'font-size', "".concat(pf(current.fontSize), "px"));
+ current.tspan.setAttributeNS(null, 'y', pf(-current.y));
+ current.txtElement = this.svgFactory.createElement('svg:text');
+ current.txtElement.appendChild(current.tspan);
+ }
+ }, {
+ key: "beginText",
+ value: function beginText() {
+ var current = this.current;
+ current.x = current.lineX = 0;
+ current.y = current.lineY = 0;
+ current.textMatrix = _util.IDENTITY_MATRIX;
+ current.lineMatrix = _util.IDENTITY_MATRIX;
+ current.textMatrixScale = 1;
+ current.tspan = this.svgFactory.createElement('svg:tspan');
+ current.txtElement = this.svgFactory.createElement('svg:text');
+ current.txtgrp = this.svgFactory.createElement('svg:g');
+ current.xcoords = [];
+ }
+ }, {
+ key: "moveText",
+ value: function moveText(x, y) {
+ var current = this.current;
+ current.x = current.lineX += x;
+ current.y = current.lineY += y;
+ current.xcoords = [];
+ current.tspan = this.svgFactory.createElement('svg:tspan');
+ current.tspan.setAttributeNS(null, 'font-family', current.fontFamily);
+ current.tspan.setAttributeNS(null, 'font-size', "".concat(pf(current.fontSize), "px"));
+ current.tspan.setAttributeNS(null, 'y', pf(-current.y));
+ }
+ }, {
+ key: "showText",
+ value: function showText(glyphs) {
+ var current = this.current;
+ var font = current.font;
+ var fontSize = current.fontSize;
+
+ if (fontSize === 0) {
+ return;
+ }
+
+ var charSpacing = current.charSpacing;
+ var wordSpacing = current.wordSpacing;
+ var fontDirection = current.fontDirection;
+ var textHScale = current.textHScale * fontDirection;
+ var vertical = font.vertical;
+ var widthAdvanceScale = fontSize * current.fontMatrix[0];
+ var x = 0;
+ var _iteratorNormalCompletion4 = true;
+ var _didIteratorError4 = false;
+ var _iteratorError4 = undefined;
+
+ try {
+ for (var _iterator4 = glyphs[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
+ var glyph = _step4.value;
+
+ if (glyph === null) {
+ x += fontDirection * wordSpacing;
+ continue;
+ } else if ((0, _util.isNum)(glyph)) {
+ x += -glyph * fontSize * 0.001;
+ continue;
+ }
+
+ var width = glyph.width;
+ var character = glyph.fontChar;
+ var spacing = (glyph.isSpace ? wordSpacing : 0) + charSpacing;
+ var charWidth = width * widthAdvanceScale + spacing * fontDirection;
+
+ if (!glyph.isInFont && !font.missingFile) {
+ x += charWidth;
+ continue;
+ }
+
+ current.xcoords.push(current.x + x * textHScale);
+ current.tspan.textContent += character;
+ x += charWidth;
+ }
+ } catch (err) {
+ _didIteratorError4 = true;
+ _iteratorError4 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion4 && _iterator4["return"] != null) {
+ _iterator4["return"]();
+ }
+ } finally {
+ if (_didIteratorError4) {
+ throw _iteratorError4;
+ }
+ }
+ }
+
+ if (vertical) {
+ current.y -= x * textHScale;
+ } else {
+ current.x += x * textHScale;
+ }
+
+ current.tspan.setAttributeNS(null, 'x', current.xcoords.map(pf).join(' '));
+ current.tspan.setAttributeNS(null, 'y', pf(-current.y));
+ current.tspan.setAttributeNS(null, 'font-family', current.fontFamily);
+ current.tspan.setAttributeNS(null, 'font-size', "".concat(pf(current.fontSize), "px"));
+
+ if (current.fontStyle !== SVG_DEFAULTS.fontStyle) {
+ current.tspan.setAttributeNS(null, 'font-style', current.fontStyle);
+ }
+
+ if (current.fontWeight !== SVG_DEFAULTS.fontWeight) {
+ current.tspan.setAttributeNS(null, 'font-weight', current.fontWeight);
+ }
+
+ var fillStrokeMode = current.textRenderingMode & _util.TextRenderingMode.FILL_STROKE_MASK;
+
+ if (fillStrokeMode === _util.TextRenderingMode.FILL || fillStrokeMode === _util.TextRenderingMode.FILL_STROKE) {
+ if (current.fillColor !== SVG_DEFAULTS.fillColor) {
+ current.tspan.setAttributeNS(null, 'fill', current.fillColor);
+ }
+
+ if (current.fillAlpha < 1) {
+ current.tspan.setAttributeNS(null, 'fill-opacity', current.fillAlpha);
+ }
+ } else if (current.textRenderingMode === _util.TextRenderingMode.ADD_TO_PATH) {
+ current.tspan.setAttributeNS(null, 'fill', 'transparent');
+ } else {
+ current.tspan.setAttributeNS(null, 'fill', 'none');
+ }
+
+ if (fillStrokeMode === _util.TextRenderingMode.STROKE || fillStrokeMode === _util.TextRenderingMode.FILL_STROKE) {
+ var lineWidthScale = 1 / (current.textMatrixScale || 1);
+
+ this._setStrokeAttributes(current.tspan, lineWidthScale);
+ }
+
+ var textMatrix = current.textMatrix;
+
+ if (current.textRise !== 0) {
+ textMatrix = textMatrix.slice();
+ textMatrix[5] += current.textRise;
+ }
+
+ current.txtElement.setAttributeNS(null, 'transform', "".concat(pm(textMatrix), " scale(1, -1)"));
+ current.txtElement.setAttributeNS(XML_NS, 'xml:space', 'preserve');
+ current.txtElement.appendChild(current.tspan);
+ current.txtgrp.appendChild(current.txtElement);
+
+ this._ensureTransformGroup().appendChild(current.txtElement);
+ }
+ }, {
+ key: "setLeadingMoveText",
+ value: function setLeadingMoveText(x, y) {
+ this.setLeading(-y);
+ this.moveText(x, y);
+ }
+ }, {
+ key: "addFontStyle",
+ value: function addFontStyle(fontObj) {
+ if (!this.cssStyle) {
+ this.cssStyle = this.svgFactory.createElement('svg:style');
+ this.cssStyle.setAttributeNS(null, 'type', 'text/css');
+ this.defs.appendChild(this.cssStyle);
+ }
+
+ var url = (0, _util.createObjectURL)(fontObj.data, fontObj.mimetype, this.forceDataSchema);
+ this.cssStyle.textContent += "@font-face { font-family: \"".concat(fontObj.loadedName, "\";") + " src: url(".concat(url, "); }\n");
+ }
+ }, {
+ key: "setFont",
+ value: function setFont(details) {
+ var current = this.current;
+ var fontObj = this.commonObjs.get(details[0]);
+ var size = details[1];
+ current.font = fontObj;
+
+ if (this.embedFonts && fontObj.data && !this.embeddedFonts[fontObj.loadedName]) {
+ this.addFontStyle(fontObj);
+ this.embeddedFonts[fontObj.loadedName] = fontObj;
+ }
+
+ current.fontMatrix = fontObj.fontMatrix ? fontObj.fontMatrix : _util.FONT_IDENTITY_MATRIX;
+ var bold = fontObj.black ? fontObj.bold ? 'bolder' : 'bold' : fontObj.bold ? 'bold' : 'normal';
+ var italic = fontObj.italic ? 'italic' : 'normal';
+
+ if (size < 0) {
+ size = -size;
+ current.fontDirection = -1;
+ } else {
+ current.fontDirection = 1;
+ }
+
+ current.fontSize = size;
+ current.fontFamily = fontObj.loadedName;
+ current.fontWeight = bold;
+ current.fontStyle = italic;
+ current.tspan = this.svgFactory.createElement('svg:tspan');
+ current.tspan.setAttributeNS(null, 'y', pf(-current.y));
+ current.xcoords = [];
+ }
+ }, {
+ key: "endText",
+ value: function endText() {
+ var current = this.current;
+
+ if (current.textRenderingMode & _util.TextRenderingMode.ADD_TO_PATH_FLAG && current.txtElement && current.txtElement.hasChildNodes()) {
+ current.element = current.txtElement;
+ this.clip('nonzero');
+ this.endPath();
+ }
+ }
+ }, {
+ key: "setLineWidth",
+ value: function setLineWidth(width) {
+ if (width > 0) {
+ this.current.lineWidth = width;
+ }
+ }
+ }, {
+ key: "setLineCap",
+ value: function setLineCap(style) {
+ this.current.lineCap = LINE_CAP_STYLES[style];
+ }
+ }, {
+ key: "setLineJoin",
+ value: function setLineJoin(style) {
+ this.current.lineJoin = LINE_JOIN_STYLES[style];
+ }
+ }, {
+ key: "setMiterLimit",
+ value: function setMiterLimit(limit) {
+ this.current.miterLimit = limit;
+ }
+ }, {
+ key: "setStrokeAlpha",
+ value: function setStrokeAlpha(strokeAlpha) {
+ this.current.strokeAlpha = strokeAlpha;
+ }
+ }, {
+ key: "setStrokeRGBColor",
+ value: function setStrokeRGBColor(r, g, b) {
+ this.current.strokeColor = _util.Util.makeCssRgb(r, g, b);
+ }
+ }, {
+ key: "setFillAlpha",
+ value: function setFillAlpha(fillAlpha) {
+ this.current.fillAlpha = fillAlpha;
+ }
+ }, {
+ key: "setFillRGBColor",
+ value: function setFillRGBColor(r, g, b) {
+ this.current.fillColor = _util.Util.makeCssRgb(r, g, b);
+ this.current.tspan = this.svgFactory.createElement('svg:tspan');
+ this.current.xcoords = [];
+ }
+ }, {
+ key: "setStrokeColorN",
+ value: function setStrokeColorN(args) {
+ this.current.strokeColor = this._makeColorN_Pattern(args);
+ }
+ }, {
+ key: "setFillColorN",
+ value: function setFillColorN(args) {
+ this.current.fillColor = this._makeColorN_Pattern(args);
+ }
+ }, {
+ key: "shadingFill",
+ value: function shadingFill(args) {
+ var width = this.viewport.width;
+ var height = this.viewport.height;
+
+ var inv = _util.Util.inverseTransform(this.transformMatrix);
+
+ var bl = _util.Util.applyTransform([0, 0], inv);
+
+ var br = _util.Util.applyTransform([0, height], inv);
+
+ var ul = _util.Util.applyTransform([width, 0], inv);
+
+ var ur = _util.Util.applyTransform([width, height], inv);
+
+ var x0 = Math.min(bl[0], br[0], ul[0], ur[0]);
+ var y0 = Math.min(bl[1], br[1], ul[1], ur[1]);
+ var x1 = Math.max(bl[0], br[0], ul[0], ur[0]);
+ var y1 = Math.max(bl[1], br[1], ul[1], ur[1]);
+ var rect = this.svgFactory.createElement('svg:rect');
+ rect.setAttributeNS(null, 'x', x0);
+ rect.setAttributeNS(null, 'y', y0);
+ rect.setAttributeNS(null, 'width', x1 - x0);
+ rect.setAttributeNS(null, 'height', y1 - y0);
+ rect.setAttributeNS(null, 'fill', this._makeShadingPattern(args));
+
+ this._ensureTransformGroup().appendChild(rect);
+ }
+ }, {
+ key: "_makeColorN_Pattern",
+ value: function _makeColorN_Pattern(args) {
+ if (args[0] === 'TilingPattern') {
+ return this._makeTilingPattern(args);
+ }
+
+ return this._makeShadingPattern(args);
+ }
+ }, {
+ key: "_makeTilingPattern",
+ value: function _makeTilingPattern(args) {
+ var color = args[1];
+ var operatorList = args[2];
+ var matrix = args[3] || _util.IDENTITY_MATRIX;
+
+ var _args$ = _slicedToArray(args[4], 4),
+ x0 = _args$[0],
+ y0 = _args$[1],
+ x1 = _args$[2],
+ y1 = _args$[3];
+
+ var xstep = args[5];
+ var ystep = args[6];
+ var paintType = args[7];
+ var tilingId = "shading".concat(shadingCount++);
+
+ var _Util$applyTransform = _util.Util.applyTransform([x0, y0], matrix),
+ _Util$applyTransform2 = _slicedToArray(_Util$applyTransform, 2),
+ tx0 = _Util$applyTransform2[0],
+ ty0 = _Util$applyTransform2[1];
+
+ var _Util$applyTransform3 = _util.Util.applyTransform([x1, y1], matrix),
+ _Util$applyTransform4 = _slicedToArray(_Util$applyTransform3, 2),
+ tx1 = _Util$applyTransform4[0],
+ ty1 = _Util$applyTransform4[1];
+
+ var _Util$singularValueDe = _util.Util.singularValueDecompose2dScale(matrix),
+ _Util$singularValueDe2 = _slicedToArray(_Util$singularValueDe, 2),
+ xscale = _Util$singularValueDe2[0],
+ yscale = _Util$singularValueDe2[1];
+
+ var txstep = xstep * xscale;
+ var tystep = ystep * yscale;
+ var tiling = this.svgFactory.createElement('svg:pattern');
+ tiling.setAttributeNS(null, 'id', tilingId);
+ tiling.setAttributeNS(null, 'patternUnits', 'userSpaceOnUse');
+ tiling.setAttributeNS(null, 'width', txstep);
+ tiling.setAttributeNS(null, 'height', tystep);
+ tiling.setAttributeNS(null, 'x', "".concat(tx0));
+ tiling.setAttributeNS(null, 'y', "".concat(ty0));
+ var svg = this.svg;
+ var transformMatrix = this.transformMatrix;
+ var fillColor = this.current.fillColor;
+ var strokeColor = this.current.strokeColor;
+ var bbox = this.svgFactory.create(tx1 - tx0, ty1 - ty0);
+ this.svg = bbox;
+ this.transformMatrix = matrix;
+
+ if (paintType === 2) {
+ var cssColor = _util.Util.makeCssRgb.apply(_util.Util, _toConsumableArray(color));
+
+ this.current.fillColor = cssColor;
+ this.current.strokeColor = cssColor;
+ }
+
+ this.executeOpTree(this.convertOpList(operatorList));
+ this.svg = svg;
+ this.transformMatrix = transformMatrix;
+ this.current.fillColor = fillColor;
+ this.current.strokeColor = strokeColor;
+ tiling.appendChild(bbox.childNodes[0]);
+ this.defs.appendChild(tiling);
+ return "url(#".concat(tilingId, ")");
+ }
+ }, {
+ key: "_makeShadingPattern",
+ value: function _makeShadingPattern(args) {
+ switch (args[0]) {
+ case 'RadialAxial':
+ var shadingId = "shading".concat(shadingCount++);
+ var colorStops = args[2];
+ var gradient;
+
+ switch (args[1]) {
+ case 'axial':
+ var point0 = args[3];
+ var point1 = args[4];
+ gradient = this.svgFactory.createElement('svg:linearGradient');
+ gradient.setAttributeNS(null, 'id', shadingId);
+ gradient.setAttributeNS(null, 'gradientUnits', 'userSpaceOnUse');
+ gradient.setAttributeNS(null, 'x1', point0[0]);
+ gradient.setAttributeNS(null, 'y1', point0[1]);
+ gradient.setAttributeNS(null, 'x2', point1[0]);
+ gradient.setAttributeNS(null, 'y2', point1[1]);
+ break;
+
+ case 'radial':
+ var focalPoint = args[3];
+ var circlePoint = args[4];
+ var focalRadius = args[5];
+ var circleRadius = args[6];
+ gradient = this.svgFactory.createElement('svg:radialGradient');
+ gradient.setAttributeNS(null, 'id', shadingId);
+ gradient.setAttributeNS(null, 'gradientUnits', 'userSpaceOnUse');
+ gradient.setAttributeNS(null, 'cx', circlePoint[0]);
+ gradient.setAttributeNS(null, 'cy', circlePoint[1]);
+ gradient.setAttributeNS(null, 'r', circleRadius);
+ gradient.setAttributeNS(null, 'fx', focalPoint[0]);
+ gradient.setAttributeNS(null, 'fy', focalPoint[1]);
+ gradient.setAttributeNS(null, 'fr', focalRadius);
+ break;
+
+ default:
+ throw new Error("Unknown RadialAxial type: ".concat(args[1]));
+ }
+
+ var _iteratorNormalCompletion5 = true;
+ var _didIteratorError5 = false;
+ var _iteratorError5 = undefined;
+
+ try {
+ for (var _iterator5 = colorStops[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
+ var colorStop = _step5.value;
+ var stop = this.svgFactory.createElement('svg:stop');
+ stop.setAttributeNS(null, 'offset', colorStop[0]);
+ stop.setAttributeNS(null, 'stop-color', colorStop[1]);
+ gradient.appendChild(stop);
+ }
+ } catch (err) {
+ _didIteratorError5 = true;
+ _iteratorError5 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion5 && _iterator5["return"] != null) {
+ _iterator5["return"]();
+ }
+ } finally {
+ if (_didIteratorError5) {
+ throw _iteratorError5;
+ }
+ }
+ }
+
+ this.defs.appendChild(gradient);
+ return "url(#".concat(shadingId, ")");
+
+ case 'Mesh':
+ (0, _util.warn)('Unimplemented pattern Mesh');
+ return null;
+
+ case 'Dummy':
+ return 'hotpink';
+
+ default:
+ throw new Error("Unknown IR type: ".concat(args[0]));
+ }
+ }
+ }, {
+ key: "setDash",
+ value: function setDash(dashArray, dashPhase) {
+ this.current.dashArray = dashArray;
+ this.current.dashPhase = dashPhase;
+ }
+ }, {
+ key: "constructPath",
+ value: function constructPath(ops, args) {
+ var current = this.current;
+ var x = current.x,
+ y = current.y;
+ var d = [];
+ var j = 0;
+ var _iteratorNormalCompletion6 = true;
+ var _didIteratorError6 = false;
+ var _iteratorError6 = undefined;
+
+ try {
+ for (var _iterator6 = ops[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
+ var op = _step6.value;
+
+ switch (op | 0) {
+ case _util.OPS.rectangle:
+ x = args[j++];
+ y = args[j++];
+ var width = args[j++];
+ var height = args[j++];
+ var xw = x + width;
+ var yh = y + height;
+ d.push('M', pf(x), pf(y), 'L', pf(xw), pf(y), 'L', pf(xw), pf(yh), 'L', pf(x), pf(yh), 'Z');
+ break;
+
+ case _util.OPS.moveTo:
+ x = args[j++];
+ y = args[j++];
+ d.push('M', pf(x), pf(y));
+ break;
+
+ case _util.OPS.lineTo:
+ x = args[j++];
+ y = args[j++];
+ d.push('L', pf(x), pf(y));
+ break;
+
+ case _util.OPS.curveTo:
+ x = args[j + 4];
+ y = args[j + 5];
+ d.push('C', pf(args[j]), pf(args[j + 1]), pf(args[j + 2]), pf(args[j + 3]), pf(x), pf(y));
+ j += 6;
+ break;
+
+ case _util.OPS.curveTo2:
+ x = args[j + 2];
+ y = args[j + 3];
+ d.push('C', pf(x), pf(y), pf(args[j]), pf(args[j + 1]), pf(args[j + 2]), pf(args[j + 3]));
+ j += 4;
+ break;
+
+ case _util.OPS.curveTo3:
+ x = args[j + 2];
+ y = args[j + 3];
+ d.push('C', pf(args[j]), pf(args[j + 1]), pf(x), pf(y), pf(x), pf(y));
+ j += 4;
+ break;
+
+ case _util.OPS.closePath:
+ d.push('Z');
+ break;
+ }
+ }
+ } catch (err) {
+ _didIteratorError6 = true;
+ _iteratorError6 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion6 && _iterator6["return"] != null) {
+ _iterator6["return"]();
+ }
+ } finally {
+ if (_didIteratorError6) {
+ throw _iteratorError6;
+ }
+ }
+ }
+
+ d = d.join(' ');
+
+ if (current.path && ops.length > 0 && ops[0] !== _util.OPS.rectangle && ops[0] !== _util.OPS.moveTo) {
+ d = current.path.getAttributeNS(null, 'd') + d;
+ } else {
+ current.path = this.svgFactory.createElement('svg:path');
+
+ this._ensureTransformGroup().appendChild(current.path);
+ }
+
+ current.path.setAttributeNS(null, 'd', d);
+ current.path.setAttributeNS(null, 'fill', 'none');
+ current.element = current.path;
+ current.setCurrentPoint(x, y);
+ }
+ }, {
+ key: "endPath",
+ value: function endPath() {
+ var current = this.current;
+ current.path = null;
+
+ if (!this.pendingClip) {
+ return;
+ }
+
+ if (!current.element) {
+ this.pendingClip = null;
+ return;
+ }
+
+ var clipId = "clippath".concat(clipCount++);
+ var clipPath = this.svgFactory.createElement('svg:clipPath');
+ clipPath.setAttributeNS(null, 'id', clipId);
+ clipPath.setAttributeNS(null, 'transform', pm(this.transformMatrix));
+ var clipElement = current.element.cloneNode(true);
+
+ if (this.pendingClip === 'evenodd') {
+ clipElement.setAttributeNS(null, 'clip-rule', 'evenodd');
+ } else {
+ clipElement.setAttributeNS(null, 'clip-rule', 'nonzero');
+ }
+
+ this.pendingClip = null;
+ clipPath.appendChild(clipElement);
+ this.defs.appendChild(clipPath);
+
+ if (current.activeClipUrl) {
+ current.clipGroup = null;
+ this.extraStack.forEach(function (prev) {
+ prev.clipGroup = null;
+ });
+ clipPath.setAttributeNS(null, 'clip-path', current.activeClipUrl);
+ }
+
+ current.activeClipUrl = "url(#".concat(clipId, ")");
+ this.tgrp = null;
+ }
+ }, {
+ key: "clip",
+ value: function clip(type) {
+ this.pendingClip = type;
+ }
+ }, {
+ key: "closePath",
+ value: function closePath() {
+ var current = this.current;
+
+ if (current.path) {
+ var d = "".concat(current.path.getAttributeNS(null, 'd'), "Z");
+ current.path.setAttributeNS(null, 'd', d);
+ }
+ }
+ }, {
+ key: "setLeading",
+ value: function setLeading(leading) {
+ this.current.leading = -leading;
+ }
+ }, {
+ key: "setTextRise",
+ value: function setTextRise(textRise) {
+ this.current.textRise = textRise;
+ }
+ }, {
+ key: "setTextRenderingMode",
+ value: function setTextRenderingMode(textRenderingMode) {
+ this.current.textRenderingMode = textRenderingMode;
+ }
+ }, {
+ key: "setHScale",
+ value: function setHScale(scale) {
+ this.current.textHScale = scale / 100;
+ }
+ }, {
+ key: "setRenderingIntent",
+ value: function setRenderingIntent(intent) {}
+ }, {
+ key: "setFlatness",
+ value: function setFlatness(flatness) {}
+ }, {
+ key: "setGState",
+ value: function setGState(states) {
+ var _iteratorNormalCompletion7 = true;
+ var _didIteratorError7 = false;
+ var _iteratorError7 = undefined;
+
+ try {
+ for (var _iterator7 = states[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) {
+ var _step7$value = _slicedToArray(_step7.value, 2),
+ key = _step7$value[0],
+ value = _step7$value[1];
+
+ switch (key) {
+ case 'LW':
+ this.setLineWidth(value);
+ break;
+
+ case 'LC':
+ this.setLineCap(value);
+ break;
+
+ case 'LJ':
+ this.setLineJoin(value);
+ break;
+
+ case 'ML':
+ this.setMiterLimit(value);
+ break;
+
+ case 'D':
+ this.setDash(value[0], value[1]);
+ break;
+
+ case 'RI':
+ this.setRenderingIntent(value);
+ break;
+
+ case 'FL':
+ this.setFlatness(value);
+ break;
+
+ case 'Font':
+ this.setFont(value);
+ break;
+
+ case 'CA':
+ this.setStrokeAlpha(value);
+ break;
+
+ case 'ca':
+ this.setFillAlpha(value);
+ break;
+
+ default:
+ (0, _util.warn)("Unimplemented graphic state operator ".concat(key));
+ break;
+ }
+ }
+ } catch (err) {
+ _didIteratorError7 = true;
+ _iteratorError7 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion7 && _iterator7["return"] != null) {
+ _iterator7["return"]();
+ }
+ } finally {
+ if (_didIteratorError7) {
+ throw _iteratorError7;
+ }
+ }
+ }
+ }
+ }, {
+ key: "fill",
+ value: function fill() {
+ var current = this.current;
+
+ if (current.element) {
+ current.element.setAttributeNS(null, 'fill', current.fillColor);
+ current.element.setAttributeNS(null, 'fill-opacity', current.fillAlpha);
+ this.endPath();
+ }
+ }
+ }, {
+ key: "stroke",
+ value: function stroke() {
+ var current = this.current;
+
+ if (current.element) {
+ this._setStrokeAttributes(current.element);
+
+ current.element.setAttributeNS(null, 'fill', 'none');
+ this.endPath();
+ }
+ }
+ }, {
+ key: "_setStrokeAttributes",
+ value: function _setStrokeAttributes(element) {
+ var lineWidthScale = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
+ var current = this.current;
+ var dashArray = current.dashArray;
+
+ if (lineWidthScale !== 1 && dashArray.length > 0) {
+ dashArray = dashArray.map(function (value) {
+ return lineWidthScale * value;
+ });
+ }
+
+ element.setAttributeNS(null, 'stroke', current.strokeColor);
+ element.setAttributeNS(null, 'stroke-opacity', current.strokeAlpha);
+ element.setAttributeNS(null, 'stroke-miterlimit', pf(current.miterLimit));
+ element.setAttributeNS(null, 'stroke-linecap', current.lineCap);
+ element.setAttributeNS(null, 'stroke-linejoin', current.lineJoin);
+ element.setAttributeNS(null, 'stroke-width', pf(lineWidthScale * current.lineWidth) + 'px');
+ element.setAttributeNS(null, 'stroke-dasharray', dashArray.map(pf).join(' '));
+ element.setAttributeNS(null, 'stroke-dashoffset', pf(lineWidthScale * current.dashPhase) + 'px');
+ }
+ }, {
+ key: "eoFill",
+ value: function eoFill() {
+ if (this.current.element) {
+ this.current.element.setAttributeNS(null, 'fill-rule', 'evenodd');
+ }
+
+ this.fill();
+ }
+ }, {
+ key: "fillStroke",
+ value: function fillStroke() {
+ this.stroke();
+ this.fill();
+ }
+ }, {
+ key: "eoFillStroke",
+ value: function eoFillStroke() {
+ if (this.current.element) {
+ this.current.element.setAttributeNS(null, 'fill-rule', 'evenodd');
+ }
+
+ this.fillStroke();
+ }
+ }, {
+ key: "closeStroke",
+ value: function closeStroke() {
+ this.closePath();
+ this.stroke();
+ }
+ }, {
+ key: "closeFillStroke",
+ value: function closeFillStroke() {
+ this.closePath();
+ this.fillStroke();
+ }
+ }, {
+ key: "closeEOFillStroke",
+ value: function closeEOFillStroke() {
+ this.closePath();
+ this.eoFillStroke();
+ }
+ }, {
+ key: "paintSolidColorImageMask",
+ value: function paintSolidColorImageMask() {
+ var rect = this.svgFactory.createElement('svg:rect');
+ rect.setAttributeNS(null, 'x', '0');
+ rect.setAttributeNS(null, 'y', '0');
+ rect.setAttributeNS(null, 'width', '1px');
+ rect.setAttributeNS(null, 'height', '1px');
+ rect.setAttributeNS(null, 'fill', this.current.fillColor);
+
+ this._ensureTransformGroup().appendChild(rect);
+ }
+ }, {
+ key: "paintJpegXObject",
+ value: function paintJpegXObject(objId, w, h) {
+ var imgObj = this.objs.get(objId);
+ var imgEl = this.svgFactory.createElement('svg:image');
+ imgEl.setAttributeNS(XLINK_NS, 'xlink:href', imgObj.src);
+ imgEl.setAttributeNS(null, 'width', pf(w));
+ imgEl.setAttributeNS(null, 'height', pf(h));
+ imgEl.setAttributeNS(null, 'x', '0');
+ imgEl.setAttributeNS(null, 'y', pf(-h));
+ imgEl.setAttributeNS(null, 'transform', "scale(".concat(pf(1 / w), " ").concat(pf(-1 / h), ")"));
+
+ this._ensureTransformGroup().appendChild(imgEl);
+ }
+ }, {
+ key: "paintImageXObject",
+ value: function paintImageXObject(objId) {
+ var imgData = this.objs.get(objId);
+
+ if (!imgData) {
+ (0, _util.warn)("Dependent image with object ID ".concat(objId, " is not ready yet"));
+ return;
+ }
+
+ this.paintInlineImageXObject(imgData);
+ }
+ }, {
+ key: "paintInlineImageXObject",
+ value: function paintInlineImageXObject(imgData, mask) {
+ var width = imgData.width;
+ var height = imgData.height;
+ var imgSrc = convertImgDataToPng(imgData, this.forceDataSchema, !!mask);
+ var cliprect = this.svgFactory.createElement('svg:rect');
+ cliprect.setAttributeNS(null, 'x', '0');
+ cliprect.setAttributeNS(null, 'y', '0');
+ cliprect.setAttributeNS(null, 'width', pf(width));
+ cliprect.setAttributeNS(null, 'height', pf(height));
+ this.current.element = cliprect;
+ this.clip('nonzero');
+ var imgEl = this.svgFactory.createElement('svg:image');
+ imgEl.setAttributeNS(XLINK_NS, 'xlink:href', imgSrc);
+ imgEl.setAttributeNS(null, 'x', '0');
+ imgEl.setAttributeNS(null, 'y', pf(-height));
+ imgEl.setAttributeNS(null, 'width', pf(width) + 'px');
+ imgEl.setAttributeNS(null, 'height', pf(height) + 'px');
+ imgEl.setAttributeNS(null, 'transform', "scale(".concat(pf(1 / width), " ").concat(pf(-1 / height), ")"));
+
+ if (mask) {
+ mask.appendChild(imgEl);
+ } else {
+ this._ensureTransformGroup().appendChild(imgEl);
+ }
+ }
+ }, {
+ key: "paintImageMaskXObject",
+ value: function paintImageMaskXObject(imgData) {
+ var current = this.current;
+ var width = imgData.width;
+ var height = imgData.height;
+ var fillColor = current.fillColor;
+ current.maskId = "mask".concat(maskCount++);
+ var mask = this.svgFactory.createElement('svg:mask');
+ mask.setAttributeNS(null, 'id', current.maskId);
+ var rect = this.svgFactory.createElement('svg:rect');
+ rect.setAttributeNS(null, 'x', '0');
+ rect.setAttributeNS(null, 'y', '0');
+ rect.setAttributeNS(null, 'width', pf(width));
+ rect.setAttributeNS(null, 'height', pf(height));
+ rect.setAttributeNS(null, 'fill', fillColor);
+ rect.setAttributeNS(null, 'mask', "url(#".concat(current.maskId, ")"));
+ this.defs.appendChild(mask);
+
+ this._ensureTransformGroup().appendChild(rect);
+
+ this.paintInlineImageXObject(imgData, mask);
+ }
+ }, {
+ key: "paintFormXObjectBegin",
+ value: function paintFormXObjectBegin(matrix, bbox) {
+ if (Array.isArray(matrix) && matrix.length === 6) {
+ this.transform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
+ }
+
+ if (bbox) {
+ var width = bbox[2] - bbox[0];
+ var height = bbox[3] - bbox[1];
+ var cliprect = this.svgFactory.createElement('svg:rect');
+ cliprect.setAttributeNS(null, 'x', bbox[0]);
+ cliprect.setAttributeNS(null, 'y', bbox[1]);
+ cliprect.setAttributeNS(null, 'width', pf(width));
+ cliprect.setAttributeNS(null, 'height', pf(height));
+ this.current.element = cliprect;
+ this.clip('nonzero');
+ this.endPath();
+ }
+ }
+ }, {
+ key: "paintFormXObjectEnd",
+ value: function paintFormXObjectEnd() {}
+ }, {
+ key: "_initialize",
+ value: function _initialize(viewport) {
+ var svg = this.svgFactory.create(viewport.width, viewport.height);
+ var definitions = this.svgFactory.createElement('svg:defs');
+ svg.appendChild(definitions);
+ this.defs = definitions;
+ var rootGroup = this.svgFactory.createElement('svg:g');
+ rootGroup.setAttributeNS(null, 'transform', pm(viewport.transform));
+ svg.appendChild(rootGroup);
+ this.svg = rootGroup;
+ return svg;
+ }
+ }, {
+ key: "_ensureClipGroup",
+ value: function _ensureClipGroup() {
+ if (!this.current.clipGroup) {
+ var clipGroup = this.svgFactory.createElement('svg:g');
+ clipGroup.setAttributeNS(null, 'clip-path', this.current.activeClipUrl);
+ this.svg.appendChild(clipGroup);
+ this.current.clipGroup = clipGroup;
+ }
+
+ return this.current.clipGroup;
+ }
+ }, {
+ key: "_ensureTransformGroup",
+ value: function _ensureTransformGroup() {
+ if (!this.tgrp) {
+ this.tgrp = this.svgFactory.createElement('svg:g');
+ this.tgrp.setAttributeNS(null, 'transform', pm(this.transformMatrix));
+
+ if (this.current.activeClipUrl) {
+ this._ensureClipGroup().appendChild(this.tgrp);
+ } else {
+ this.svg.appendChild(this.tgrp);
+ }
+ }
+
+ return this.tgrp;
+ }
+ }]);
+
+ return SVGGraphics;
+ }();
+}
+
+/***/ }),
+/* 165 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.PDFNodeStream = void 0;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(148));
+
+var _util = __w_pdfjs_require__(1);
+
+var _network_utils = __w_pdfjs_require__(166);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
+
+function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
+
+function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
+
+function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var fs = require('fs');
+
+var http = require('http');
+
+var https = require('https');
+
+var url = require('url');
+
+var fileUriRegex = /^file:\/\/\/[a-zA-Z]:\//;
+
+function parseUrl(sourceUrl) {
+ var parsedUrl = url.parse(sourceUrl);
+
+ if (parsedUrl.protocol === 'file:' || parsedUrl.host) {
+ return parsedUrl;
+ }
+
+ if (/^[a-z]:[/\\]/i.test(sourceUrl)) {
+ return url.parse("file:///".concat(sourceUrl));
+ }
+
+ if (!parsedUrl.host) {
+ parsedUrl.protocol = 'file:';
+ }
+
+ return parsedUrl;
+}
+
+var PDFNodeStream =
+/*#__PURE__*/
+function () {
+ function PDFNodeStream(source) {
+ _classCallCheck(this, PDFNodeStream);
+
+ this.source = source;
+ this.url = parseUrl(source.url);
+ this.isHttp = this.url.protocol === 'http:' || this.url.protocol === 'https:';
+ this.isFsUrl = this.url.protocol === 'file:';
+ this.httpHeaders = this.isHttp && source.httpHeaders || {};
+ this._fullRequestReader = null;
+ this._rangeRequestReaders = [];
+ }
+
+ _createClass(PDFNodeStream, [{
+ key: "getFullReader",
+ value: function getFullReader() {
+ (0, _util.assert)(!this._fullRequestReader);
+ this._fullRequestReader = this.isFsUrl ? new PDFNodeStreamFsFullReader(this) : new PDFNodeStreamFullReader(this);
+ return this._fullRequestReader;
+ }
+ }, {
+ key: "getRangeReader",
+ value: function getRangeReader(start, end) {
+ if (end <= this._progressiveDataLength) {
+ return null;
+ }
+
+ var rangeReader = this.isFsUrl ? new PDFNodeStreamFsRangeReader(this, start, end) : new PDFNodeStreamRangeReader(this, start, end);
+
+ this._rangeRequestReaders.push(rangeReader);
+
+ return rangeReader;
+ }
+ }, {
+ key: "cancelAllRequests",
+ value: function cancelAllRequests(reason) {
+ if (this._fullRequestReader) {
+ this._fullRequestReader.cancel(reason);
+ }
+
+ var readers = this._rangeRequestReaders.slice(0);
+
+ readers.forEach(function (reader) {
+ reader.cancel(reason);
+ });
+ }
+ }, {
+ key: "_progressiveDataLength",
+ get: function get() {
+ return this._fullRequestReader ? this._fullRequestReader._loaded : 0;
+ }
+ }]);
+
+ return PDFNodeStream;
+}();
+
+exports.PDFNodeStream = PDFNodeStream;
+
+var BaseFullReader =
+/*#__PURE__*/
+function () {
+ function BaseFullReader(stream) {
+ _classCallCheck(this, BaseFullReader);
+
+ this._url = stream.url;
+ this._done = false;
+ this._storedError = null;
+ this.onProgress = null;
+ var source = stream.source;
+ this._contentLength = source.length;
+ this._loaded = 0;
+ this._filename = null;
+ this._disableRange = source.disableRange || false;
+ this._rangeChunkSize = source.rangeChunkSize;
+
+ if (!this._rangeChunkSize && !this._disableRange) {
+ this._disableRange = true;
+ }
+
+ this._isStreamingSupported = !source.disableStream;
+ this._isRangeSupported = !source.disableRange;
+ this._readableStream = null;
+ this._readCapability = (0, _util.createPromiseCapability)();
+ this._headersCapability = (0, _util.createPromiseCapability)();
+ }
+
+ _createClass(BaseFullReader, [{
+ key: "read",
+ value: function () {
+ var _read = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee() {
+ var chunk, buffer;
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ _context.next = 2;
+ return this._readCapability.promise;
+
+ case 2:
+ if (!this._done) {
+ _context.next = 4;
+ break;
+ }
+
+ return _context.abrupt("return", {
+ value: undefined,
+ done: true
+ });
+
+ case 4:
+ if (!this._storedError) {
+ _context.next = 6;
+ break;
+ }
+
+ throw this._storedError;
+
+ case 6:
+ chunk = this._readableStream.read();
+
+ if (!(chunk === null)) {
+ _context.next = 10;
+ break;
+ }
+
+ this._readCapability = (0, _util.createPromiseCapability)();
+ return _context.abrupt("return", this.read());
+
+ case 10:
+ this._loaded += chunk.length;
+
+ if (this.onProgress) {
+ this.onProgress({
+ loaded: this._loaded,
+ total: this._contentLength
+ });
+ }
+
+ buffer = new Uint8Array(chunk).buffer;
+ return _context.abrupt("return", {
+ value: buffer,
+ done: false
+ });
+
+ case 14:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee, this);
+ }));
+
+ function read() {
+ return _read.apply(this, arguments);
+ }
+
+ return read;
+ }()
+ }, {
+ key: "cancel",
+ value: function cancel(reason) {
+ if (!this._readableStream) {
+ this._error(reason);
+
+ return;
+ }
+
+ this._readableStream.destroy(reason);
+ }
+ }, {
+ key: "_error",
+ value: function _error(reason) {
+ this._storedError = reason;
+
+ this._readCapability.resolve();
+ }
+ }, {
+ key: "_setReadableStream",
+ value: function _setReadableStream(readableStream) {
+ var _this = this;
+
+ this._readableStream = readableStream;
+ readableStream.on('readable', function () {
+ _this._readCapability.resolve();
+ });
+ readableStream.on('end', function () {
+ readableStream.destroy();
+ _this._done = true;
+
+ _this._readCapability.resolve();
+ });
+ readableStream.on('error', function (reason) {
+ _this._error(reason);
+ });
+
+ if (!this._isStreamingSupported && this._isRangeSupported) {
+ this._error(new _util.AbortException('streaming is disabled'));
+ }
+
+ if (this._storedError) {
+ this._readableStream.destroy(this._storedError);
+ }
+ }
+ }, {
+ key: "headersReady",
+ get: function get() {
+ return this._headersCapability.promise;
+ }
+ }, {
+ key: "filename",
+ get: function get() {
+ return this._filename;
+ }
+ }, {
+ key: "contentLength",
+ get: function get() {
+ return this._contentLength;
+ }
+ }, {
+ key: "isRangeSupported",
+ get: function get() {
+ return this._isRangeSupported;
+ }
+ }, {
+ key: "isStreamingSupported",
+ get: function get() {
+ return this._isStreamingSupported;
+ }
+ }]);
+
+ return BaseFullReader;
+}();
+
+var BaseRangeReader =
+/*#__PURE__*/
+function () {
+ function BaseRangeReader(stream) {
+ _classCallCheck(this, BaseRangeReader);
+
+ this._url = stream.url;
+ this._done = false;
+ this._storedError = null;
+ this.onProgress = null;
+ this._loaded = 0;
+ this._readableStream = null;
+ this._readCapability = (0, _util.createPromiseCapability)();
+ var source = stream.source;
+ this._isStreamingSupported = !source.disableStream;
+ }
+
+ _createClass(BaseRangeReader, [{
+ key: "read",
+ value: function () {
+ var _read2 = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee2() {
+ var chunk, buffer;
+ return _regenerator["default"].wrap(function _callee2$(_context2) {
+ while (1) {
+ switch (_context2.prev = _context2.next) {
+ case 0:
+ _context2.next = 2;
+ return this._readCapability.promise;
+
+ case 2:
+ if (!this._done) {
+ _context2.next = 4;
+ break;
+ }
+
+ return _context2.abrupt("return", {
+ value: undefined,
+ done: true
+ });
+
+ case 4:
+ if (!this._storedError) {
+ _context2.next = 6;
+ break;
+ }
+
+ throw this._storedError;
+
+ case 6:
+ chunk = this._readableStream.read();
+
+ if (!(chunk === null)) {
+ _context2.next = 10;
+ break;
+ }
+
+ this._readCapability = (0, _util.createPromiseCapability)();
+ return _context2.abrupt("return", this.read());
+
+ case 10:
+ this._loaded += chunk.length;
+
+ if (this.onProgress) {
+ this.onProgress({
+ loaded: this._loaded
+ });
+ }
+
+ buffer = new Uint8Array(chunk).buffer;
+ return _context2.abrupt("return", {
+ value: buffer,
+ done: false
+ });
+
+ case 14:
+ case "end":
+ return _context2.stop();
+ }
+ }
+ }, _callee2, this);
+ }));
+
+ function read() {
+ return _read2.apply(this, arguments);
+ }
+
+ return read;
+ }()
+ }, {
+ key: "cancel",
+ value: function cancel(reason) {
+ if (!this._readableStream) {
+ this._error(reason);
+
+ return;
+ }
+
+ this._readableStream.destroy(reason);
+ }
+ }, {
+ key: "_error",
+ value: function _error(reason) {
+ this._storedError = reason;
+
+ this._readCapability.resolve();
+ }
+ }, {
+ key: "_setReadableStream",
+ value: function _setReadableStream(readableStream) {
+ var _this2 = this;
+
+ this._readableStream = readableStream;
+ readableStream.on('readable', function () {
+ _this2._readCapability.resolve();
+ });
+ readableStream.on('end', function () {
+ readableStream.destroy();
+ _this2._done = true;
+
+ _this2._readCapability.resolve();
+ });
+ readableStream.on('error', function (reason) {
+ _this2._error(reason);
+ });
+
+ if (this._storedError) {
+ this._readableStream.destroy(this._storedError);
+ }
+ }
+ }, {
+ key: "isStreamingSupported",
+ get: function get() {
+ return this._isStreamingSupported;
+ }
+ }]);
+
+ return BaseRangeReader;
+}();
+
+function createRequestOptions(url, headers) {
+ return {
+ protocol: url.protocol,
+ auth: url.auth,
+ host: url.hostname,
+ port: url.port,
+ path: url.path,
+ method: 'GET',
+ headers: headers
+ };
+}
+
+var PDFNodeStreamFullReader =
+/*#__PURE__*/
+function (_BaseFullReader) {
+ _inherits(PDFNodeStreamFullReader, _BaseFullReader);
+
+ function PDFNodeStreamFullReader(stream) {
+ var _this3;
+
+ _classCallCheck(this, PDFNodeStreamFullReader);
+
+ _this3 = _possibleConstructorReturn(this, _getPrototypeOf(PDFNodeStreamFullReader).call(this, stream));
+
+ var handleResponse = function handleResponse(response) {
+ if (response.statusCode === 404) {
+ var error = new _util.MissingPDFException("Missing PDF \"".concat(_this3._url, "\"."));
+ _this3._storedError = error;
+
+ _this3._headersCapability.reject(error);
+
+ return;
+ }
+
+ _this3._headersCapability.resolve();
+
+ _this3._setReadableStream(response);
+
+ var getResponseHeader = function getResponseHeader(name) {
+ return _this3._readableStream.headers[name.toLowerCase()];
+ };
+
+ var _validateRangeRequest = (0, _network_utils.validateRangeRequestCapabilities)({
+ getResponseHeader: getResponseHeader,
+ isHttp: stream.isHttp,
+ rangeChunkSize: _this3._rangeChunkSize,
+ disableRange: _this3._disableRange
+ }),
+ allowRangeRequests = _validateRangeRequest.allowRangeRequests,
+ suggestedLength = _validateRangeRequest.suggestedLength;
+
+ _this3._isRangeSupported = allowRangeRequests;
+ _this3._contentLength = suggestedLength || _this3._contentLength;
+ _this3._filename = (0, _network_utils.extractFilenameFromHeader)(getResponseHeader);
+ };
+
+ _this3._request = null;
+
+ if (_this3._url.protocol === 'http:') {
+ _this3._request = http.request(createRequestOptions(_this3._url, stream.httpHeaders), handleResponse);
+ } else {
+ _this3._request = https.request(createRequestOptions(_this3._url, stream.httpHeaders), handleResponse);
+ }
+
+ _this3._request.on('error', function (reason) {
+ _this3._storedError = reason;
+
+ _this3._headersCapability.reject(reason);
+ });
+
+ _this3._request.end();
+
+ return _this3;
+ }
+
+ return PDFNodeStreamFullReader;
+}(BaseFullReader);
+
+var PDFNodeStreamRangeReader =
+/*#__PURE__*/
+function (_BaseRangeReader) {
+ _inherits(PDFNodeStreamRangeReader, _BaseRangeReader);
+
+ function PDFNodeStreamRangeReader(stream, start, end) {
+ var _this4;
+
+ _classCallCheck(this, PDFNodeStreamRangeReader);
+
+ _this4 = _possibleConstructorReturn(this, _getPrototypeOf(PDFNodeStreamRangeReader).call(this, stream));
+ _this4._httpHeaders = {};
+
+ for (var property in stream.httpHeaders) {
+ var value = stream.httpHeaders[property];
+
+ if (typeof value === 'undefined') {
+ continue;
+ }
+
+ _this4._httpHeaders[property] = value;
+ }
+
+ _this4._httpHeaders['Range'] = "bytes=".concat(start, "-").concat(end - 1);
+
+ var handleResponse = function handleResponse(response) {
+ if (response.statusCode === 404) {
+ var error = new _util.MissingPDFException("Missing PDF \"".concat(_this4._url, "\"."));
+ _this4._storedError = error;
+ return;
+ }
+
+ _this4._setReadableStream(response);
+ };
+
+ _this4._request = null;
+
+ if (_this4._url.protocol === 'http:') {
+ _this4._request = http.request(createRequestOptions(_this4._url, _this4._httpHeaders), handleResponse);
+ } else {
+ _this4._request = https.request(createRequestOptions(_this4._url, _this4._httpHeaders), handleResponse);
+ }
+
+ _this4._request.on('error', function (reason) {
+ _this4._storedError = reason;
+ });
+
+ _this4._request.end();
+
+ return _this4;
+ }
+
+ return PDFNodeStreamRangeReader;
+}(BaseRangeReader);
+
+var PDFNodeStreamFsFullReader =
+/*#__PURE__*/
+function (_BaseFullReader2) {
+ _inherits(PDFNodeStreamFsFullReader, _BaseFullReader2);
+
+ function PDFNodeStreamFsFullReader(stream) {
+ var _this5;
+
+ _classCallCheck(this, PDFNodeStreamFsFullReader);
+
+ _this5 = _possibleConstructorReturn(this, _getPrototypeOf(PDFNodeStreamFsFullReader).call(this, stream));
+ var path = decodeURIComponent(_this5._url.path);
+
+ if (fileUriRegex.test(_this5._url.href)) {
+ path = path.replace(/^\//, '');
+ }
+
+ fs.lstat(path, function (error, stat) {
+ if (error) {
+ if (error.code === 'ENOENT') {
+ error = new _util.MissingPDFException("Missing PDF \"".concat(path, "\"."));
+ }
+
+ _this5._storedError = error;
+
+ _this5._headersCapability.reject(error);
+
+ return;
+ }
+
+ _this5._contentLength = stat.size;
+
+ _this5._setReadableStream(fs.createReadStream(path));
+
+ _this5._headersCapability.resolve();
+ });
+ return _this5;
+ }
+
+ return PDFNodeStreamFsFullReader;
+}(BaseFullReader);
+
+var PDFNodeStreamFsRangeReader =
+/*#__PURE__*/
+function (_BaseRangeReader2) {
+ _inherits(PDFNodeStreamFsRangeReader, _BaseRangeReader2);
+
+ function PDFNodeStreamFsRangeReader(stream, start, end) {
+ var _this6;
+
+ _classCallCheck(this, PDFNodeStreamFsRangeReader);
+
+ _this6 = _possibleConstructorReturn(this, _getPrototypeOf(PDFNodeStreamFsRangeReader).call(this, stream));
+ var path = decodeURIComponent(_this6._url.path);
+
+ if (fileUriRegex.test(_this6._url.href)) {
+ path = path.replace(/^\//, '');
+ }
+
+ _this6._setReadableStream(fs.createReadStream(path, {
+ start: start,
+ end: end - 1
+ }));
+
+ return _this6;
+ }
+
+ return PDFNodeStreamFsRangeReader;
+}(BaseRangeReader);
+
+/***/ }),
+/* 166 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.createResponseStatusError = createResponseStatusError;
+exports.extractFilenameFromHeader = extractFilenameFromHeader;
+exports.validateRangeRequestCapabilities = validateRangeRequestCapabilities;
+exports.validateResponseStatus = validateResponseStatus;
+
+var _util = __w_pdfjs_require__(1);
+
+var _content_disposition = __w_pdfjs_require__(167);
+
+function validateRangeRequestCapabilities(_ref) {
+ var getResponseHeader = _ref.getResponseHeader,
+ isHttp = _ref.isHttp,
+ rangeChunkSize = _ref.rangeChunkSize,
+ disableRange = _ref.disableRange;
+ (0, _util.assert)(rangeChunkSize > 0, 'Range chunk size must be larger than zero');
+ var returnValues = {
+ allowRangeRequests: false,
+ suggestedLength: undefined
+ };
+ var length = parseInt(getResponseHeader('Content-Length'), 10);
+
+ if (!Number.isInteger(length)) {
+ return returnValues;
+ }
+
+ returnValues.suggestedLength = length;
+
+ if (length <= 2 * rangeChunkSize) {
+ return returnValues;
+ }
+
+ if (disableRange || !isHttp) {
+ return returnValues;
+ }
+
+ if (getResponseHeader('Accept-Ranges') !== 'bytes') {
+ return returnValues;
+ }
+
+ var contentEncoding = getResponseHeader('Content-Encoding') || 'identity';
+
+ if (contentEncoding !== 'identity') {
+ return returnValues;
+ }
+
+ returnValues.allowRangeRequests = true;
+ return returnValues;
+}
+
+function extractFilenameFromHeader(getResponseHeader) {
+ var contentDisposition = getResponseHeader('Content-Disposition');
+
+ if (contentDisposition) {
+ var filename = (0, _content_disposition.getFilenameFromContentDispositionHeader)(contentDisposition);
+
+ if (/\.pdf$/i.test(filename)) {
+ return filename;
+ }
+ }
+
+ return null;
+}
+
+function createResponseStatusError(status, url) {
+ if (status === 404 || status === 0 && /^file:/.test(url)) {
+ return new _util.MissingPDFException('Missing PDF "' + url + '".');
+ }
+
+ return new _util.UnexpectedResponseException('Unexpected server response (' + status + ') while retrieving PDF "' + url + '".', status);
+}
+
+function validateResponseStatus(status) {
+ return status === 200 || status === 206;
+}
+
+/***/ }),
+/* 167 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.getFilenameFromContentDispositionHeader = getFilenameFromContentDispositionHeader;
+
+function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
+
+function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
+
+function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
+
+function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
+
+function getFilenameFromContentDispositionHeader(contentDisposition) {
+ var needsEncodingFixup = true;
+ var tmp = toParamRegExp('filename\\*', 'i').exec(contentDisposition);
+
+ if (tmp) {
+ tmp = tmp[1];
+ var filename = rfc2616unquote(tmp);
+ filename = unescape(filename);
+ filename = rfc5987decode(filename);
+ filename = rfc2047decode(filename);
+ return fixupEncoding(filename);
+ }
+
+ tmp = rfc2231getparam(contentDisposition);
+
+ if (tmp) {
+ var _filename = rfc2047decode(tmp);
+
+ return fixupEncoding(_filename);
+ }
+
+ tmp = toParamRegExp('filename', 'i').exec(contentDisposition);
+
+ if (tmp) {
+ tmp = tmp[1];
+
+ var _filename2 = rfc2616unquote(tmp);
+
+ _filename2 = rfc2047decode(_filename2);
+ return fixupEncoding(_filename2);
+ }
+
+ function toParamRegExp(attributePattern, flags) {
+ return new RegExp('(?:^|;)\\s*' + attributePattern + '\\s*=\\s*' + '(' + '[^";\\s][^;\\s]*' + '|' + '"(?:[^"\\\\]|\\\\"?)+"?' + ')', flags);
+ }
+
+ function textdecode(encoding, value) {
+ if (encoding) {
+ if (!/^[\x00-\xFF]+$/.test(value)) {
+ return value;
+ }
+
+ try {
+ var decoder = new TextDecoder(encoding, {
+ fatal: true
+ });
+ var bytes = Array.from(value, function (ch) {
+ return ch.charCodeAt(0) & 0xFF;
+ });
+ value = decoder.decode(new Uint8Array(bytes));
+ needsEncodingFixup = false;
+ } catch (e) {
+ if (/^utf-?8$/i.test(encoding)) {
+ try {
+ value = decodeURIComponent(escape(value));
+ needsEncodingFixup = false;
+ } catch (err) {}
+ }
+ }
+ }
+
+ return value;
+ }
+
+ function fixupEncoding(value) {
+ if (needsEncodingFixup && /[\x80-\xff]/.test(value)) {
+ value = textdecode('utf-8', value);
+
+ if (needsEncodingFixup) {
+ value = textdecode('iso-8859-1', value);
+ }
+ }
+
+ return value;
+ }
+
+ function rfc2231getparam(contentDisposition) {
+ var matches = [],
+ match;
+ var iter = toParamRegExp('filename\\*((?!0\\d)\\d+)(\\*?)', 'ig');
+
+ while ((match = iter.exec(contentDisposition)) !== null) {
+ var _match = match,
+ _match2 = _slicedToArray(_match, 4),
+ n = _match2[1],
+ quot = _match2[2],
+ part = _match2[3];
+
+ n = parseInt(n, 10);
+
+ if (n in matches) {
+ if (n === 0) {
+ break;
+ }
+
+ continue;
+ }
+
+ matches[n] = [quot, part];
+ }
+
+ var parts = [];
+
+ for (var n = 0; n < matches.length; ++n) {
+ if (!(n in matches)) {
+ break;
+ }
+
+ var _matches$n = _slicedToArray(matches[n], 2),
+ quot = _matches$n[0],
+ part = _matches$n[1];
+
+ part = rfc2616unquote(part);
+
+ if (quot) {
+ part = unescape(part);
+
+ if (n === 0) {
+ part = rfc5987decode(part);
+ }
+ }
+
+ parts.push(part);
+ }
+
+ return parts.join('');
+ }
+
+ function rfc2616unquote(value) {
+ if (value.startsWith('"')) {
+ var parts = value.slice(1).split('\\"');
+
+ for (var i = 0; i < parts.length; ++i) {
+ var quotindex = parts[i].indexOf('"');
+
+ if (quotindex !== -1) {
+ parts[i] = parts[i].slice(0, quotindex);
+ parts.length = i + 1;
+ }
+
+ parts[i] = parts[i].replace(/\\(.)/g, '$1');
+ }
+
+ value = parts.join('"');
+ }
+
+ return value;
+ }
+
+ function rfc5987decode(extvalue) {
+ var encodingend = extvalue.indexOf('\'');
+
+ if (encodingend === -1) {
+ return extvalue;
+ }
+
+ var encoding = extvalue.slice(0, encodingend);
+ var langvalue = extvalue.slice(encodingend + 1);
+ var value = langvalue.replace(/^[^']*'/, '');
+ return textdecode(encoding, value);
+ }
+
+ function rfc2047decode(value) {
+ if (!value.startsWith('=?') || /[\x00-\x19\x80-\xff]/.test(value)) {
+ return value;
+ }
+
+ return value.replace(/=\?([\w-]*)\?([QqBb])\?((?:[^?]|\?(?!=))*)\?=/g, function (_, charset, encoding, text) {
+ if (encoding === 'q' || encoding === 'Q') {
+ text = text.replace(/_/g, ' ');
+ text = text.replace(/=([0-9a-fA-F]{2})/g, function (_, hex) {
+ return String.fromCharCode(parseInt(hex, 16));
+ });
+ return textdecode(charset, text);
+ }
+
+ try {
+ text = atob(text);
+ } catch (e) {}
+
+ return textdecode(charset, text);
+ });
+ }
+
+ return '';
+}
+
+/***/ }),
+/* 168 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.PDFNetworkStream = void 0;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(148));
+
+var _util = __w_pdfjs_require__(1);
+
+var _network_utils = __w_pdfjs_require__(166);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+;
+var OK_RESPONSE = 200;
+var PARTIAL_CONTENT_RESPONSE = 206;
+
+function getArrayBuffer(xhr) {
+ var data = xhr.response;
+
+ if (typeof data !== 'string') {
+ return data;
+ }
+
+ var array = (0, _util.stringToBytes)(data);
+ return array.buffer;
+}
+
+var NetworkManager =
+/*#__PURE__*/
+function () {
+ function NetworkManager(url, args) {
+ _classCallCheck(this, NetworkManager);
+
+ this.url = url;
+ args = args || {};
+ this.isHttp = /^https?:/i.test(url);
+ this.httpHeaders = this.isHttp && args.httpHeaders || {};
+ this.withCredentials = args.withCredentials || false;
+
+ this.getXhr = args.getXhr || function NetworkManager_getXhr() {
+ return new XMLHttpRequest();
+ };
+
+ this.currXhrId = 0;
+ this.pendingRequests = Object.create(null);
+ }
+
+ _createClass(NetworkManager, [{
+ key: "requestRange",
+ value: function requestRange(begin, end, listeners) {
+ var args = {
+ begin: begin,
+ end: end
+ };
+
+ for (var prop in listeners) {
+ args[prop] = listeners[prop];
+ }
+
+ return this.request(args);
+ }
+ }, {
+ key: "requestFull",
+ value: function requestFull(listeners) {
+ return this.request(listeners);
+ }
+ }, {
+ key: "request",
+ value: function request(args) {
+ var xhr = this.getXhr();
+ var xhrId = this.currXhrId++;
+ var pendingRequest = this.pendingRequests[xhrId] = {
+ xhr: xhr
+ };
+ xhr.open('GET', this.url);
+ xhr.withCredentials = this.withCredentials;
+
+ for (var property in this.httpHeaders) {
+ var value = this.httpHeaders[property];
+
+ if (typeof value === 'undefined') {
+ continue;
+ }
+
+ xhr.setRequestHeader(property, value);
+ }
+
+ if (this.isHttp && 'begin' in args && 'end' in args) {
+ xhr.setRequestHeader('Range', "bytes=".concat(args.begin, "-").concat(args.end - 1));
+ pendingRequest.expectedStatus = PARTIAL_CONTENT_RESPONSE;
+ } else {
+ pendingRequest.expectedStatus = OK_RESPONSE;
+ }
+
+ xhr.responseType = 'arraybuffer';
+
+ if (args.onError) {
+ xhr.onerror = function (evt) {
+ args.onError(xhr.status);
+ };
+ }
+
+ xhr.onreadystatechange = this.onStateChange.bind(this, xhrId);
+ xhr.onprogress = this.onProgress.bind(this, xhrId);
+ pendingRequest.onHeadersReceived = args.onHeadersReceived;
+ pendingRequest.onDone = args.onDone;
+ pendingRequest.onError = args.onError;
+ pendingRequest.onProgress = args.onProgress;
+ xhr.send(null);
+ return xhrId;
+ }
+ }, {
+ key: "onProgress",
+ value: function onProgress(xhrId, evt) {
+ var pendingRequest = this.pendingRequests[xhrId];
+
+ if (!pendingRequest) {
+ return;
+ }
+
+ if (pendingRequest.onProgress) {
+ pendingRequest.onProgress(evt);
+ }
+ }
+ }, {
+ key: "onStateChange",
+ value: function onStateChange(xhrId, evt) {
+ var pendingRequest = this.pendingRequests[xhrId];
+
+ if (!pendingRequest) {
+ return;
+ }
+
+ var xhr = pendingRequest.xhr;
+
+ if (xhr.readyState >= 2 && pendingRequest.onHeadersReceived) {
+ pendingRequest.onHeadersReceived();
+ delete pendingRequest.onHeadersReceived;
+ }
+
+ if (xhr.readyState !== 4) {
+ return;
+ }
+
+ if (!(xhrId in this.pendingRequests)) {
+ return;
+ }
+
+ delete this.pendingRequests[xhrId];
+
+ if (xhr.status === 0 && this.isHttp) {
+ if (pendingRequest.onError) {
+ pendingRequest.onError(xhr.status);
+ }
+
+ return;
+ }
+
+ var xhrStatus = xhr.status || OK_RESPONSE;
+ var ok_response_on_range_request = xhrStatus === OK_RESPONSE && pendingRequest.expectedStatus === PARTIAL_CONTENT_RESPONSE;
+
+ if (!ok_response_on_range_request && xhrStatus !== pendingRequest.expectedStatus) {
+ if (pendingRequest.onError) {
+ pendingRequest.onError(xhr.status);
+ }
+
+ return;
+ }
+
+ var chunk = getArrayBuffer(xhr);
+
+ if (xhrStatus === PARTIAL_CONTENT_RESPONSE) {
+ var rangeHeader = xhr.getResponseHeader('Content-Range');
+ var matches = /bytes (\d+)-(\d+)\/(\d+)/.exec(rangeHeader);
+ pendingRequest.onDone({
+ begin: parseInt(matches[1], 10),
+ chunk: chunk
+ });
+ } else if (chunk) {
+ pendingRequest.onDone({
+ begin: 0,
+ chunk: chunk
+ });
+ } else if (pendingRequest.onError) {
+ pendingRequest.onError(xhr.status);
+ }
+ }
+ }, {
+ key: "hasPendingRequests",
+ value: function hasPendingRequests() {
+ for (var xhrId in this.pendingRequests) {
+ return true;
+ }
+
+ return false;
+ }
+ }, {
+ key: "getRequestXhr",
+ value: function getRequestXhr(xhrId) {
+ return this.pendingRequests[xhrId].xhr;
+ }
+ }, {
+ key: "isPendingRequest",
+ value: function isPendingRequest(xhrId) {
+ return xhrId in this.pendingRequests;
+ }
+ }, {
+ key: "abortAllRequests",
+ value: function abortAllRequests() {
+ for (var xhrId in this.pendingRequests) {
+ this.abortRequest(xhrId | 0);
+ }
+ }
+ }, {
+ key: "abortRequest",
+ value: function abortRequest(xhrId) {
+ var xhr = this.pendingRequests[xhrId].xhr;
+ delete this.pendingRequests[xhrId];
+ xhr.abort();
+ }
+ }]);
+
+ return NetworkManager;
+}();
+
+var PDFNetworkStream =
+/*#__PURE__*/
+function () {
+ function PDFNetworkStream(source) {
+ _classCallCheck(this, PDFNetworkStream);
+
+ this._source = source;
+ this._manager = new NetworkManager(source.url, {
+ httpHeaders: source.httpHeaders,
+ withCredentials: source.withCredentials
+ });
+ this._rangeChunkSize = source.rangeChunkSize;
+ this._fullRequestReader = null;
+ this._rangeRequestReaders = [];
+ }
+
+ _createClass(PDFNetworkStream, [{
+ key: "_onRangeRequestReaderClosed",
+ value: function _onRangeRequestReaderClosed(reader) {
+ var i = this._rangeRequestReaders.indexOf(reader);
+
+ if (i >= 0) {
+ this._rangeRequestReaders.splice(i, 1);
+ }
+ }
+ }, {
+ key: "getFullReader",
+ value: function getFullReader() {
+ (0, _util.assert)(!this._fullRequestReader);
+ this._fullRequestReader = new PDFNetworkStreamFullRequestReader(this._manager, this._source);
+ return this._fullRequestReader;
+ }
+ }, {
+ key: "getRangeReader",
+ value: function getRangeReader(begin, end) {
+ var reader = new PDFNetworkStreamRangeRequestReader(this._manager, begin, end);
+ reader.onClosed = this._onRangeRequestReaderClosed.bind(this);
+
+ this._rangeRequestReaders.push(reader);
+
+ return reader;
+ }
+ }, {
+ key: "cancelAllRequests",
+ value: function cancelAllRequests(reason) {
+ if (this._fullRequestReader) {
+ this._fullRequestReader.cancel(reason);
+ }
+
+ var readers = this._rangeRequestReaders.slice(0);
+
+ readers.forEach(function (reader) {
+ reader.cancel(reason);
+ });
+ }
+ }]);
+
+ return PDFNetworkStream;
+}();
+
+exports.PDFNetworkStream = PDFNetworkStream;
+
+var PDFNetworkStreamFullRequestReader =
+/*#__PURE__*/
+function () {
+ function PDFNetworkStreamFullRequestReader(manager, source) {
+ _classCallCheck(this, PDFNetworkStreamFullRequestReader);
+
+ this._manager = manager;
+ var args = {
+ onHeadersReceived: this._onHeadersReceived.bind(this),
+ onDone: this._onDone.bind(this),
+ onError: this._onError.bind(this),
+ onProgress: this._onProgress.bind(this)
+ };
+ this._url = source.url;
+ this._fullRequestId = manager.requestFull(args);
+ this._headersReceivedCapability = (0, _util.createPromiseCapability)();
+ this._disableRange = source.disableRange || false;
+ this._contentLength = source.length;
+ this._rangeChunkSize = source.rangeChunkSize;
+
+ if (!this._rangeChunkSize && !this._disableRange) {
+ this._disableRange = true;
+ }
+
+ this._isStreamingSupported = false;
+ this._isRangeSupported = false;
+ this._cachedChunks = [];
+ this._requests = [];
+ this._done = false;
+ this._storedError = undefined;
+ this._filename = null;
+ this.onProgress = null;
+ }
+
+ _createClass(PDFNetworkStreamFullRequestReader, [{
+ key: "_onHeadersReceived",
+ value: function _onHeadersReceived() {
+ var fullRequestXhrId = this._fullRequestId;
+
+ var fullRequestXhr = this._manager.getRequestXhr(fullRequestXhrId);
+
+ var getResponseHeader = function getResponseHeader(name) {
+ return fullRequestXhr.getResponseHeader(name);
+ };
+
+ var _validateRangeRequest = (0, _network_utils.validateRangeRequestCapabilities)({
+ getResponseHeader: getResponseHeader,
+ isHttp: this._manager.isHttp,
+ rangeChunkSize: this._rangeChunkSize,
+ disableRange: this._disableRange
+ }),
+ allowRangeRequests = _validateRangeRequest.allowRangeRequests,
+ suggestedLength = _validateRangeRequest.suggestedLength;
+
+ if (allowRangeRequests) {
+ this._isRangeSupported = true;
+ }
+
+ this._contentLength = suggestedLength || this._contentLength;
+ this._filename = (0, _network_utils.extractFilenameFromHeader)(getResponseHeader);
+
+ if (this._isRangeSupported) {
+ this._manager.abortRequest(fullRequestXhrId);
+ }
+
+ this._headersReceivedCapability.resolve();
+ }
+ }, {
+ key: "_onDone",
+ value: function _onDone(args) {
+ if (args) {
+ if (this._requests.length > 0) {
+ var requestCapability = this._requests.shift();
+
+ requestCapability.resolve({
+ value: args.chunk,
+ done: false
+ });
+ } else {
+ this._cachedChunks.push(args.chunk);
+ }
+ }
+
+ this._done = true;
+
+ if (this._cachedChunks.length > 0) {
+ return;
+ }
+
+ this._requests.forEach(function (requestCapability) {
+ requestCapability.resolve({
+ value: undefined,
+ done: true
+ });
+ });
+
+ this._requests = [];
+ }
+ }, {
+ key: "_onError",
+ value: function _onError(status) {
+ var url = this._url;
+ var exception = (0, _network_utils.createResponseStatusError)(status, url);
+ this._storedError = exception;
+
+ this._headersReceivedCapability.reject(exception);
+
+ this._requests.forEach(function (requestCapability) {
+ requestCapability.reject(exception);
+ });
+
+ this._requests = [];
+ this._cachedChunks = [];
+ }
+ }, {
+ key: "_onProgress",
+ value: function _onProgress(data) {
+ if (this.onProgress) {
+ this.onProgress({
+ loaded: data.loaded,
+ total: data.lengthComputable ? data.total : this._contentLength
+ });
+ }
+ }
+ }, {
+ key: "read",
+ value: function () {
+ var _read = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee() {
+ var chunk, requestCapability;
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ if (!this._storedError) {
+ _context.next = 2;
+ break;
+ }
+
+ throw this._storedError;
+
+ case 2:
+ if (!(this._cachedChunks.length > 0)) {
+ _context.next = 5;
+ break;
+ }
+
+ chunk = this._cachedChunks.shift();
+ return _context.abrupt("return", {
+ value: chunk,
+ done: false
+ });
+
+ case 5:
+ if (!this._done) {
+ _context.next = 7;
+ break;
+ }
+
+ return _context.abrupt("return", {
+ value: undefined,
+ done: true
+ });
+
+ case 7:
+ requestCapability = (0, _util.createPromiseCapability)();
+
+ this._requests.push(requestCapability);
+
+ return _context.abrupt("return", requestCapability.promise);
+
+ case 10:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee, this);
+ }));
+
+ function read() {
+ return _read.apply(this, arguments);
+ }
+
+ return read;
+ }()
+ }, {
+ key: "cancel",
+ value: function cancel(reason) {
+ this._done = true;
+
+ this._headersReceivedCapability.reject(reason);
+
+ this._requests.forEach(function (requestCapability) {
+ requestCapability.resolve({
+ value: undefined,
+ done: true
+ });
+ });
+
+ this._requests = [];
+
+ if (this._manager.isPendingRequest(this._fullRequestId)) {
+ this._manager.abortRequest(this._fullRequestId);
+ }
+
+ this._fullRequestReader = null;
+ }
+ }, {
+ key: "filename",
+ get: function get() {
+ return this._filename;
+ }
+ }, {
+ key: "isRangeSupported",
+ get: function get() {
+ return this._isRangeSupported;
+ }
+ }, {
+ key: "isStreamingSupported",
+ get: function get() {
+ return this._isStreamingSupported;
+ }
+ }, {
+ key: "contentLength",
+ get: function get() {
+ return this._contentLength;
+ }
+ }, {
+ key: "headersReady",
+ get: function get() {
+ return this._headersReceivedCapability.promise;
+ }
+ }]);
+
+ return PDFNetworkStreamFullRequestReader;
+}();
+
+var PDFNetworkStreamRangeRequestReader =
+/*#__PURE__*/
+function () {
+ function PDFNetworkStreamRangeRequestReader(manager, begin, end) {
+ _classCallCheck(this, PDFNetworkStreamRangeRequestReader);
+
+ this._manager = manager;
+ var args = {
+ onDone: this._onDone.bind(this),
+ onProgress: this._onProgress.bind(this)
+ };
+ this._requestId = manager.requestRange(begin, end, args);
+ this._requests = [];
+ this._queuedChunk = null;
+ this._done = false;
+ this.onProgress = null;
+ this.onClosed = null;
+ }
+
+ _createClass(PDFNetworkStreamRangeRequestReader, [{
+ key: "_close",
+ value: function _close() {
+ if (this.onClosed) {
+ this.onClosed(this);
+ }
+ }
+ }, {
+ key: "_onDone",
+ value: function _onDone(data) {
+ var chunk = data.chunk;
+
+ if (this._requests.length > 0) {
+ var requestCapability = this._requests.shift();
+
+ requestCapability.resolve({
+ value: chunk,
+ done: false
+ });
+ } else {
+ this._queuedChunk = chunk;
+ }
+
+ this._done = true;
+
+ this._requests.forEach(function (requestCapability) {
+ requestCapability.resolve({
+ value: undefined,
+ done: true
+ });
+ });
+
+ this._requests = [];
+
+ this._close();
+ }
+ }, {
+ key: "_onProgress",
+ value: function _onProgress(evt) {
+ if (!this.isStreamingSupported && this.onProgress) {
+ this.onProgress({
+ loaded: evt.loaded
+ });
+ }
+ }
+ }, {
+ key: "read",
+ value: function () {
+ var _read2 = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee2() {
+ var chunk, requestCapability;
+ return _regenerator["default"].wrap(function _callee2$(_context2) {
+ while (1) {
+ switch (_context2.prev = _context2.next) {
+ case 0:
+ if (!(this._queuedChunk !== null)) {
+ _context2.next = 4;
+ break;
+ }
+
+ chunk = this._queuedChunk;
+ this._queuedChunk = null;
+ return _context2.abrupt("return", {
+ value: chunk,
+ done: false
+ });
+
+ case 4:
+ if (!this._done) {
+ _context2.next = 6;
+ break;
+ }
+
+ return _context2.abrupt("return", {
+ value: undefined,
+ done: true
+ });
+
+ case 6:
+ requestCapability = (0, _util.createPromiseCapability)();
+
+ this._requests.push(requestCapability);
+
+ return _context2.abrupt("return", requestCapability.promise);
+
+ case 9:
+ case "end":
+ return _context2.stop();
+ }
+ }
+ }, _callee2, this);
+ }));
+
+ function read() {
+ return _read2.apply(this, arguments);
+ }
+
+ return read;
+ }()
+ }, {
+ key: "cancel",
+ value: function cancel(reason) {
+ this._done = true;
+
+ this._requests.forEach(function (requestCapability) {
+ requestCapability.resolve({
+ value: undefined,
+ done: true
+ });
+ });
+
+ this._requests = [];
+
+ if (this._manager.isPendingRequest(this._requestId)) {
+ this._manager.abortRequest(this._requestId);
+ }
+
+ this._close();
+ }
+ }, {
+ key: "isStreamingSupported",
+ get: function get() {
+ return false;
+ }
+ }]);
+
+ return PDFNetworkStreamRangeRequestReader;
+}();
+
+/***/ }),
+/* 169 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.PDFFetchStream = void 0;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(148));
+
+var _util = __w_pdfjs_require__(1);
+
+var _network_utils = __w_pdfjs_require__(166);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+function createFetchOptions(headers, withCredentials, abortController) {
+ return {
+ method: 'GET',
+ headers: headers,
+ signal: abortController && abortController.signal,
+ mode: 'cors',
+ credentials: withCredentials ? 'include' : 'same-origin',
+ redirect: 'follow'
+ };
+}
+
+var PDFFetchStream =
+/*#__PURE__*/
+function () {
+ function PDFFetchStream(source) {
+ _classCallCheck(this, PDFFetchStream);
+
+ this.source = source;
+ this.isHttp = /^https?:/i.test(source.url);
+ this.httpHeaders = this.isHttp && source.httpHeaders || {};
+ this._fullRequestReader = null;
+ this._rangeRequestReaders = [];
+ }
+
+ _createClass(PDFFetchStream, [{
+ key: "getFullReader",
+ value: function getFullReader() {
+ (0, _util.assert)(!this._fullRequestReader);
+ this._fullRequestReader = new PDFFetchStreamReader(this);
+ return this._fullRequestReader;
+ }
+ }, {
+ key: "getRangeReader",
+ value: function getRangeReader(begin, end) {
+ if (end <= this._progressiveDataLength) {
+ return null;
+ }
+
+ var reader = new PDFFetchStreamRangeReader(this, begin, end);
+
+ this._rangeRequestReaders.push(reader);
+
+ return reader;
+ }
+ }, {
+ key: "cancelAllRequests",
+ value: function cancelAllRequests(reason) {
+ if (this._fullRequestReader) {
+ this._fullRequestReader.cancel(reason);
+ }
+
+ var readers = this._rangeRequestReaders.slice(0);
+
+ readers.forEach(function (reader) {
+ reader.cancel(reason);
+ });
+ }
+ }, {
+ key: "_progressiveDataLength",
+ get: function get() {
+ return this._fullRequestReader ? this._fullRequestReader._loaded : 0;
+ }
+ }]);
+
+ return PDFFetchStream;
+}();
+
+exports.PDFFetchStream = PDFFetchStream;
+
+var PDFFetchStreamReader =
+/*#__PURE__*/
+function () {
+ function PDFFetchStreamReader(stream) {
+ var _this = this;
+
+ _classCallCheck(this, PDFFetchStreamReader);
+
+ this._stream = stream;
+ this._reader = null;
+ this._loaded = 0;
+ this._filename = null;
+ var source = stream.source;
+ this._withCredentials = source.withCredentials || false;
+ this._contentLength = source.length;
+ this._headersCapability = (0, _util.createPromiseCapability)();
+ this._disableRange = source.disableRange || false;
+ this._rangeChunkSize = source.rangeChunkSize;
+
+ if (!this._rangeChunkSize && !this._disableRange) {
+ this._disableRange = true;
+ }
+
+ if (typeof AbortController !== 'undefined') {
+ this._abortController = new AbortController();
+ }
+
+ this._isStreamingSupported = !source.disableStream;
+ this._isRangeSupported = !source.disableRange;
+ this._headers = new Headers();
+
+ for (var property in this._stream.httpHeaders) {
+ var value = this._stream.httpHeaders[property];
+
+ if (typeof value === 'undefined') {
+ continue;
+ }
+
+ this._headers.append(property, value);
+ }
+
+ var url = source.url;
+ fetch(url, createFetchOptions(this._headers, this._withCredentials, this._abortController)).then(function (response) {
+ if (!(0, _network_utils.validateResponseStatus)(response.status)) {
+ throw (0, _network_utils.createResponseStatusError)(response.status, url);
+ }
+
+ _this._reader = response.body.getReader();
+
+ _this._headersCapability.resolve();
+
+ var getResponseHeader = function getResponseHeader(name) {
+ return response.headers.get(name);
+ };
+
+ var _validateRangeRequest = (0, _network_utils.validateRangeRequestCapabilities)({
+ getResponseHeader: getResponseHeader,
+ isHttp: _this._stream.isHttp,
+ rangeChunkSize: _this._rangeChunkSize,
+ disableRange: _this._disableRange
+ }),
+ allowRangeRequests = _validateRangeRequest.allowRangeRequests,
+ suggestedLength = _validateRangeRequest.suggestedLength;
+
+ _this._isRangeSupported = allowRangeRequests;
+ _this._contentLength = suggestedLength || _this._contentLength;
+ _this._filename = (0, _network_utils.extractFilenameFromHeader)(getResponseHeader);
+
+ if (!_this._isStreamingSupported && _this._isRangeSupported) {
+ _this.cancel(new _util.AbortException('Streaming is disabled.'));
+ }
+ })["catch"](this._headersCapability.reject);
+ this.onProgress = null;
+ }
+
+ _createClass(PDFFetchStreamReader, [{
+ key: "read",
+ value: function () {
+ var _read = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee() {
+ var _ref, value, done, buffer;
+
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ _context.next = 2;
+ return this._headersCapability.promise;
+
+ case 2:
+ _context.next = 4;
+ return this._reader.read();
+
+ case 4:
+ _ref = _context.sent;
+ value = _ref.value;
+ done = _ref.done;
+
+ if (!done) {
+ _context.next = 9;
+ break;
+ }
+
+ return _context.abrupt("return", {
+ value: value,
+ done: done
+ });
+
+ case 9:
+ this._loaded += value.byteLength;
+
+ if (this.onProgress) {
+ this.onProgress({
+ loaded: this._loaded,
+ total: this._contentLength
+ });
+ }
+
+ buffer = new Uint8Array(value).buffer;
+ return _context.abrupt("return", {
+ value: buffer,
+ done: false
+ });
+
+ case 13:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee, this);
+ }));
+
+ function read() {
+ return _read.apply(this, arguments);
+ }
+
+ return read;
+ }()
+ }, {
+ key: "cancel",
+ value: function cancel(reason) {
+ if (this._reader) {
+ this._reader.cancel(reason);
+ }
+
+ if (this._abortController) {
+ this._abortController.abort();
+ }
+ }
+ }, {
+ key: "headersReady",
+ get: function get() {
+ return this._headersCapability.promise;
+ }
+ }, {
+ key: "filename",
+ get: function get() {
+ return this._filename;
+ }
+ }, {
+ key: "contentLength",
+ get: function get() {
+ return this._contentLength;
+ }
+ }, {
+ key: "isRangeSupported",
+ get: function get() {
+ return this._isRangeSupported;
+ }
+ }, {
+ key: "isStreamingSupported",
+ get: function get() {
+ return this._isStreamingSupported;
+ }
+ }]);
+
+ return PDFFetchStreamReader;
+}();
+
+var PDFFetchStreamRangeReader =
+/*#__PURE__*/
+function () {
+ function PDFFetchStreamRangeReader(stream, begin, end) {
+ var _this2 = this;
+
+ _classCallCheck(this, PDFFetchStreamRangeReader);
+
+ this._stream = stream;
+ this._reader = null;
+ this._loaded = 0;
+ var source = stream.source;
+ this._withCredentials = source.withCredentials || false;
+ this._readCapability = (0, _util.createPromiseCapability)();
+ this._isStreamingSupported = !source.disableStream;
+
+ if (typeof AbortController !== 'undefined') {
+ this._abortController = new AbortController();
+ }
+
+ this._headers = new Headers();
+
+ for (var property in this._stream.httpHeaders) {
+ var value = this._stream.httpHeaders[property];
+
+ if (typeof value === 'undefined') {
+ continue;
+ }
+
+ this._headers.append(property, value);
+ }
+
+ this._headers.append('Range', "bytes=".concat(begin, "-").concat(end - 1));
+
+ var url = source.url;
+ fetch(url, createFetchOptions(this._headers, this._withCredentials, this._abortController)).then(function (response) {
+ if (!(0, _network_utils.validateResponseStatus)(response.status)) {
+ throw (0, _network_utils.createResponseStatusError)(response.status, url);
+ }
+
+ _this2._readCapability.resolve();
+
+ _this2._reader = response.body.getReader();
+ });
+ this.onProgress = null;
+ }
+
+ _createClass(PDFFetchStreamRangeReader, [{
+ key: "read",
+ value: function () {
+ var _read2 = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee2() {
+ var _ref2, value, done, buffer;
+
+ return _regenerator["default"].wrap(function _callee2$(_context2) {
+ while (1) {
+ switch (_context2.prev = _context2.next) {
+ case 0:
+ _context2.next = 2;
+ return this._readCapability.promise;
+
+ case 2:
+ _context2.next = 4;
+ return this._reader.read();
+
+ case 4:
+ _ref2 = _context2.sent;
+ value = _ref2.value;
+ done = _ref2.done;
+
+ if (!done) {
+ _context2.next = 9;
+ break;
+ }
+
+ return _context2.abrupt("return", {
+ value: value,
+ done: done
+ });
+
+ case 9:
+ this._loaded += value.byteLength;
+
+ if (this.onProgress) {
+ this.onProgress({
+ loaded: this._loaded
+ });
+ }
+
+ buffer = new Uint8Array(value).buffer;
+ return _context2.abrupt("return", {
+ value: buffer,
+ done: false
+ });
+
+ case 13:
+ case "end":
+ return _context2.stop();
+ }
+ }
+ }, _callee2, this);
+ }));
+
+ function read() {
+ return _read2.apply(this, arguments);
+ }
+
+ return read;
+ }()
+ }, {
+ key: "cancel",
+ value: function cancel(reason) {
+ if (this._reader) {
+ this._reader.cancel(reason);
+ }
+
+ if (this._abortController) {
+ this._abortController.abort();
+ }
+ }
+ }, {
+ key: "isStreamingSupported",
+ get: function get() {
+ return this._isStreamingSupported;
+ }
+ }]);
+
+ return PDFFetchStreamRangeReader;
+}();
+
+/***/ })
+/******/ ]);
+});
+//# sourceMappingURL=pdf.js.map \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/pdf_js/pdf.worker.js b/testing/web-platform/tests/tools/third_party/pdf_js/pdf.worker.js
new file mode 100644
index 0000000000..211fbbdc4c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pdf_js/pdf.worker.js
@@ -0,0 +1,56199 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * Javascript code in this page
+ *
+ * Copyright 2019 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @licend The above is the entire license notice for the
+ * Javascript code in this page
+ */
+
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define("pdfjs-dist/build/pdf.worker", [], factory);
+ else if(typeof exports === 'object')
+ exports["pdfjs-dist/build/pdf.worker"] = factory();
+ else
+ root["pdfjs-dist/build/pdf.worker"] = root.pdfjsWorker = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __w_pdfjs_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __w_pdfjs_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __w_pdfjs_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __w_pdfjs_require__.c = installedModules;
+/******/
+/******/ // define getter function for harmony exports
+/******/ __w_pdfjs_require__.d = function(exports, name, getter) {
+/******/ if(!__w_pdfjs_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
+/******/ }
+/******/ };
+/******/
+/******/ // define __esModule on exports
+/******/ __w_pdfjs_require__.r = function(exports) {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/
+/******/ // create a fake namespace object
+/******/ // mode & 1: value is a module id, require it
+/******/ // mode & 2: merge all properties of value into the ns
+/******/ // mode & 4: return value when already ns object
+/******/ // mode & 8|1: behave like require
+/******/ __w_pdfjs_require__.t = function(value, mode) {
+/******/ if(mode & 1) value = __w_pdfjs_require__(value);
+/******/ if(mode & 8) return value;
+/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ var ns = Object.create(null);
+/******/ __w_pdfjs_require__.r(ns);
+/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __w_pdfjs_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ return ns;
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __w_pdfjs_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __w_pdfjs_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __w_pdfjs_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __w_pdfjs_require__.p = "";
+/******/
+/******/
+/******/ // Load entry module and return exports
+/******/ return __w_pdfjs_require__(__w_pdfjs_require__.s = 0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var pdfjsVersion = '2.2.228';
+var pdfjsBuild = 'd7afb74a';
+
+var pdfjsCoreWorker = __w_pdfjs_require__(1);
+
+exports.WorkerMessageHandler = pdfjsCoreWorker.WorkerMessageHandler;
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.WorkerMessageHandler = exports.WorkerTask = void 0;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(2));
+
+var _util = __w_pdfjs_require__(5);
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _pdf_manager = __w_pdfjs_require__(152);
+
+var _is_node = _interopRequireDefault(__w_pdfjs_require__(8));
+
+var _message_handler = __w_pdfjs_require__(191);
+
+var _worker_stream = __w_pdfjs_require__(192);
+
+var _core_utils = __w_pdfjs_require__(154);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
+
+function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
+
+function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
+
+function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+var WorkerTask = function WorkerTaskClosure() {
+ function WorkerTask(name) {
+ this.name = name;
+ this.terminated = false;
+ this._capability = (0, _util.createPromiseCapability)();
+ }
+
+ WorkerTask.prototype = {
+ get finished() {
+ return this._capability.promise;
+ },
+
+ finish: function finish() {
+ this._capability.resolve();
+ },
+ terminate: function terminate() {
+ this.terminated = true;
+ },
+ ensureNotTerminated: function ensureNotTerminated() {
+ if (this.terminated) {
+ throw new Error('Worker task was terminated');
+ }
+ }
+ };
+ return WorkerTask;
+}();
+
+exports.WorkerTask = WorkerTask;
+var WorkerMessageHandler = {
+ setup: function setup(handler, port) {
+ var testMessageProcessed = false;
+ handler.on('test', function wphSetupTest(data) {
+ if (testMessageProcessed) {
+ return;
+ }
+
+ testMessageProcessed = true;
+
+ if (!(data instanceof Uint8Array)) {
+ handler.send('test', false);
+ return;
+ }
+
+ var supportTransfers = data[0] === 255;
+ handler.postMessageTransfers = supportTransfers;
+ var xhr = new XMLHttpRequest();
+ var responseExists = 'response' in xhr;
+
+ try {
+ xhr.responseType;
+ } catch (e) {
+ responseExists = false;
+ }
+
+ if (!responseExists) {
+ handler.send('test', false);
+ return;
+ }
+
+ handler.send('test', {
+ supportTypedArray: true,
+ supportTransfers: supportTransfers
+ });
+ });
+ handler.on('configure', function wphConfigure(data) {
+ (0, _util.setVerbosityLevel)(data.verbosity);
+ });
+ handler.on('GetDocRequest', function wphSetupDoc(data) {
+ return WorkerMessageHandler.createDocumentHandler(data, port);
+ });
+ },
+ createDocumentHandler: function createDocumentHandler(docParams, port) {
+ var pdfManager;
+ var terminated = false;
+ var cancelXHRs = null;
+ var WorkerTasks = [];
+ var verbosity = (0, _util.getVerbosityLevel)();
+ var apiVersion = docParams.apiVersion;
+ var workerVersion = '2.2.228';
+
+ if (apiVersion !== workerVersion) {
+ throw new Error("The API version \"".concat(apiVersion, "\" does not match ") + "the Worker version \"".concat(workerVersion, "\"."));
+ }
+
+ var docId = docParams.docId;
+ var docBaseUrl = docParams.docBaseUrl;
+ var workerHandlerName = docParams.docId + '_worker';
+ var handler = new _message_handler.MessageHandler(workerHandlerName, docId, port);
+ handler.postMessageTransfers = docParams.postMessageTransfers;
+
+ function ensureNotTerminated() {
+ if (terminated) {
+ throw new Error('Worker was terminated');
+ }
+ }
+
+ function startWorkerTask(task) {
+ WorkerTasks.push(task);
+ }
+
+ function finishWorkerTask(task) {
+ task.finish();
+ var i = WorkerTasks.indexOf(task);
+ WorkerTasks.splice(i, 1);
+ }
+
+ function loadDocument(_x) {
+ return _loadDocument.apply(this, arguments);
+ }
+
+ function _loadDocument() {
+ _loadDocument = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee(recoveryMode) {
+ var _ref4, _ref5, numPages, fingerprint;
+
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ _context.next = 2;
+ return pdfManager.ensureDoc('checkHeader');
+
+ case 2:
+ _context.next = 4;
+ return pdfManager.ensureDoc('parseStartXRef');
+
+ case 4:
+ _context.next = 6;
+ return pdfManager.ensureDoc('parse', [recoveryMode]);
+
+ case 6:
+ if (recoveryMode) {
+ _context.next = 9;
+ break;
+ }
+
+ _context.next = 9;
+ return pdfManager.ensureDoc('checkFirstPage');
+
+ case 9:
+ _context.next = 11;
+ return Promise.all([pdfManager.ensureDoc('numPages'), pdfManager.ensureDoc('fingerprint')]);
+
+ case 11:
+ _ref4 = _context.sent;
+ _ref5 = _slicedToArray(_ref4, 2);
+ numPages = _ref5[0];
+ fingerprint = _ref5[1];
+ return _context.abrupt("return", {
+ numPages: numPages,
+ fingerprint: fingerprint
+ });
+
+ case 16:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee);
+ }));
+ return _loadDocument.apply(this, arguments);
+ }
+
+ function getPdfManager(data, evaluatorOptions) {
+ var pdfManagerCapability = (0, _util.createPromiseCapability)();
+ var pdfManager;
+ var source = data.source;
+
+ if (source.data) {
+ try {
+ pdfManager = new _pdf_manager.LocalPdfManager(docId, source.data, source.password, evaluatorOptions, docBaseUrl);
+ pdfManagerCapability.resolve(pdfManager);
+ } catch (ex) {
+ pdfManagerCapability.reject(ex);
+ }
+
+ return pdfManagerCapability.promise;
+ }
+
+ var pdfStream,
+ cachedChunks = [];
+
+ try {
+ pdfStream = new _worker_stream.PDFWorkerStream(handler);
+ } catch (ex) {
+ pdfManagerCapability.reject(ex);
+ return pdfManagerCapability.promise;
+ }
+
+ var fullRequest = pdfStream.getFullReader();
+ fullRequest.headersReady.then(function () {
+ if (!fullRequest.isRangeSupported) {
+ return;
+ }
+
+ var disableAutoFetch = source.disableAutoFetch || fullRequest.isStreamingSupported;
+ pdfManager = new _pdf_manager.NetworkPdfManager(docId, pdfStream, {
+ msgHandler: handler,
+ password: source.password,
+ length: fullRequest.contentLength,
+ disableAutoFetch: disableAutoFetch,
+ rangeChunkSize: source.rangeChunkSize
+ }, evaluatorOptions, docBaseUrl);
+
+ for (var i = 0; i < cachedChunks.length; i++) {
+ pdfManager.sendProgressiveData(cachedChunks[i]);
+ }
+
+ cachedChunks = [];
+ pdfManagerCapability.resolve(pdfManager);
+ cancelXHRs = null;
+ })["catch"](function (reason) {
+ pdfManagerCapability.reject(reason);
+ cancelXHRs = null;
+ });
+ var loaded = 0;
+
+ var flushChunks = function flushChunks() {
+ var pdfFile = (0, _util.arraysToBytes)(cachedChunks);
+
+ if (source.length && pdfFile.length !== source.length) {
+ (0, _util.warn)('reported HTTP length is different from actual');
+ }
+
+ try {
+ pdfManager = new _pdf_manager.LocalPdfManager(docId, pdfFile, source.password, evaluatorOptions, docBaseUrl);
+ pdfManagerCapability.resolve(pdfManager);
+ } catch (ex) {
+ pdfManagerCapability.reject(ex);
+ }
+
+ cachedChunks = [];
+ };
+
+ var readPromise = new Promise(function (resolve, reject) {
+ var readChunk = function readChunk(chunk) {
+ try {
+ ensureNotTerminated();
+
+ if (chunk.done) {
+ if (!pdfManager) {
+ flushChunks();
+ }
+
+ cancelXHRs = null;
+ return;
+ }
+
+ var data = chunk.value;
+ loaded += (0, _util.arrayByteLength)(data);
+
+ if (!fullRequest.isStreamingSupported) {
+ handler.send('DocProgress', {
+ loaded: loaded,
+ total: Math.max(loaded, fullRequest.contentLength || 0)
+ });
+ }
+
+ if (pdfManager) {
+ pdfManager.sendProgressiveData(data);
+ } else {
+ cachedChunks.push(data);
+ }
+
+ fullRequest.read().then(readChunk, reject);
+ } catch (e) {
+ reject(e);
+ }
+ };
+
+ fullRequest.read().then(readChunk, reject);
+ });
+ readPromise["catch"](function (e) {
+ pdfManagerCapability.reject(e);
+ cancelXHRs = null;
+ });
+
+ cancelXHRs = function cancelXHRs() {
+ pdfStream.cancelAllRequests('abort');
+ };
+
+ return pdfManagerCapability.promise;
+ }
+
+ function setupDoc(data) {
+ function onSuccess(doc) {
+ ensureNotTerminated();
+ handler.send('GetDoc', {
+ pdfInfo: doc
+ });
+ }
+
+ function onFailure(e) {
+ ensureNotTerminated();
+
+ if (e instanceof _util.PasswordException) {
+ var task = new WorkerTask('PasswordException: response ' + e.code);
+ startWorkerTask(task);
+ handler.sendWithPromise('PasswordRequest', e).then(function (data) {
+ finishWorkerTask(task);
+ pdfManager.updatePassword(data.password);
+ pdfManagerReady();
+ })["catch"](function (boundException) {
+ finishWorkerTask(task);
+ handler.send('PasswordException', boundException);
+ }.bind(null, e));
+ } else if (e instanceof _util.InvalidPDFException) {
+ handler.send('InvalidPDF', e);
+ } else if (e instanceof _util.MissingPDFException) {
+ handler.send('MissingPDF', e);
+ } else if (e instanceof _util.UnexpectedResponseException) {
+ handler.send('UnexpectedResponse', e);
+ } else {
+ handler.send('UnknownError', new _util.UnknownErrorException(e.message, e.toString()));
+ }
+ }
+
+ function pdfManagerReady() {
+ ensureNotTerminated();
+ loadDocument(false).then(onSuccess, function loadFailure(ex) {
+ ensureNotTerminated();
+
+ if (!(ex instanceof _core_utils.XRefParseException)) {
+ onFailure(ex);
+ return;
+ }
+
+ pdfManager.requestLoadedStream();
+ pdfManager.onLoadedStream().then(function () {
+ ensureNotTerminated();
+ loadDocument(true).then(onSuccess, onFailure);
+ });
+ }, onFailure);
+ }
+
+ ensureNotTerminated();
+ var evaluatorOptions = {
+ forceDataSchema: data.disableCreateObjectURL,
+ maxImageSize: data.maxImageSize,
+ disableFontFace: data.disableFontFace,
+ nativeImageDecoderSupport: data.nativeImageDecoderSupport,
+ ignoreErrors: data.ignoreErrors,
+ isEvalSupported: data.isEvalSupported
+ };
+ getPdfManager(data, evaluatorOptions).then(function (newPdfManager) {
+ if (terminated) {
+ newPdfManager.terminate();
+ throw new Error('Worker was terminated');
+ }
+
+ pdfManager = newPdfManager;
+ pdfManager.onLoadedStream().then(function (stream) {
+ handler.send('DataLoaded', {
+ length: stream.bytes.byteLength
+ });
+ });
+ }).then(pdfManagerReady, onFailure);
+ }
+
+ handler.on('GetPage', function wphSetupGetPage(data) {
+ return pdfManager.getPage(data.pageIndex).then(function (page) {
+ return Promise.all([pdfManager.ensure(page, 'rotate'), pdfManager.ensure(page, 'ref'), pdfManager.ensure(page, 'userUnit'), pdfManager.ensure(page, 'view')]).then(function (_ref) {
+ var _ref2 = _slicedToArray(_ref, 4),
+ rotate = _ref2[0],
+ ref = _ref2[1],
+ userUnit = _ref2[2],
+ view = _ref2[3];
+
+ return {
+ rotate: rotate,
+ ref: ref,
+ userUnit: userUnit,
+ view: view
+ };
+ });
+ });
+ });
+ handler.on('GetPageIndex', function wphSetupGetPageIndex(data) {
+ var ref = _primitives.Ref.get(data.ref.num, data.ref.gen);
+
+ var catalog = pdfManager.pdfDocument.catalog;
+ return catalog.getPageIndex(ref);
+ });
+ handler.on('GetDestinations', function wphSetupGetDestinations(data) {
+ return pdfManager.ensureCatalog('destinations');
+ });
+ handler.on('GetDestination', function wphSetupGetDestination(data) {
+ return pdfManager.ensureCatalog('getDestination', [data.id]);
+ });
+ handler.on('GetPageLabels', function wphSetupGetPageLabels(data) {
+ return pdfManager.ensureCatalog('pageLabels');
+ });
+ handler.on('GetPageLayout', function wphSetupGetPageLayout(data) {
+ return pdfManager.ensureCatalog('pageLayout');
+ });
+ handler.on('GetPageMode', function wphSetupGetPageMode(data) {
+ return pdfManager.ensureCatalog('pageMode');
+ });
+ handler.on('GetViewerPreferences', function (data) {
+ return pdfManager.ensureCatalog('viewerPreferences');
+ });
+ handler.on('GetOpenActionDestination', function (data) {
+ return pdfManager.ensureCatalog('openActionDestination');
+ });
+ handler.on('GetAttachments', function wphSetupGetAttachments(data) {
+ return pdfManager.ensureCatalog('attachments');
+ });
+ handler.on('GetJavaScript', function wphSetupGetJavaScript(data) {
+ return pdfManager.ensureCatalog('javaScript');
+ });
+ handler.on('GetOutline', function wphSetupGetOutline(data) {
+ return pdfManager.ensureCatalog('documentOutline');
+ });
+ handler.on('GetPermissions', function (data) {
+ return pdfManager.ensureCatalog('permissions');
+ });
+ handler.on('GetMetadata', function wphSetupGetMetadata(data) {
+ return Promise.all([pdfManager.ensureDoc('documentInfo'), pdfManager.ensureCatalog('metadata')]);
+ });
+ handler.on('GetData', function wphSetupGetData(data) {
+ pdfManager.requestLoadedStream();
+ return pdfManager.onLoadedStream().then(function (stream) {
+ return stream.bytes;
+ });
+ });
+ handler.on('GetStats', function wphSetupGetStats(data) {
+ return pdfManager.pdfDocument.xref.stats;
+ });
+ handler.on('GetAnnotations', function (_ref3) {
+ var pageIndex = _ref3.pageIndex,
+ intent = _ref3.intent;
+ return pdfManager.getPage(pageIndex).then(function (page) {
+ return page.getAnnotationsData(intent);
+ });
+ });
+ handler.on('RenderPageRequest', function wphSetupRenderPage(data) {
+ var pageIndex = data.pageIndex;
+ pdfManager.getPage(pageIndex).then(function (page) {
+ var task = new WorkerTask('RenderPageRequest: page ' + pageIndex);
+ startWorkerTask(task);
+ var start = verbosity >= _util.VerbosityLevel.INFOS ? Date.now() : 0;
+ page.getOperatorList({
+ handler: handler,
+ task: task,
+ intent: data.intent,
+ renderInteractiveForms: data.renderInteractiveForms
+ }).then(function (operatorList) {
+ finishWorkerTask(task);
+
+ if (start) {
+ (0, _util.info)("page=".concat(pageIndex + 1, " - getOperatorList: time=") + "".concat(Date.now() - start, "ms, len=").concat(operatorList.totalLength));
+ }
+ }, function (e) {
+ finishWorkerTask(task);
+
+ if (task.terminated) {
+ return;
+ }
+
+ handler.send('UnsupportedFeature', {
+ featureId: _util.UNSUPPORTED_FEATURES.unknown
+ });
+ var minimumStackMessage = 'worker.js: while trying to getPage() and getOperatorList()';
+ var wrappedException;
+
+ if (typeof e === 'string') {
+ wrappedException = {
+ message: e,
+ stack: minimumStackMessage
+ };
+ } else if (_typeof(e) === 'object') {
+ wrappedException = {
+ message: e.message || e.toString(),
+ stack: e.stack || minimumStackMessage
+ };
+ } else {
+ wrappedException = {
+ message: 'Unknown exception type: ' + _typeof(e),
+ stack: minimumStackMessage
+ };
+ }
+
+ handler.send('PageError', {
+ pageIndex: pageIndex,
+ error: wrappedException,
+ intent: data.intent
+ });
+ });
+ });
+ }, this);
+ handler.on('GetTextContent', function wphExtractText(data, sink) {
+ var pageIndex = data.pageIndex;
+
+ sink.onPull = function (desiredSize) {};
+
+ sink.onCancel = function (reason) {};
+
+ pdfManager.getPage(pageIndex).then(function (page) {
+ var task = new WorkerTask('GetTextContent: page ' + pageIndex);
+ startWorkerTask(task);
+ var start = verbosity >= _util.VerbosityLevel.INFOS ? Date.now() : 0;
+ page.extractTextContent({
+ handler: handler,
+ task: task,
+ sink: sink,
+ normalizeWhitespace: data.normalizeWhitespace,
+ combineTextItems: data.combineTextItems
+ }).then(function () {
+ finishWorkerTask(task);
+
+ if (start) {
+ (0, _util.info)("page=".concat(pageIndex + 1, " - getTextContent: time=") + "".concat(Date.now() - start, "ms"));
+ }
+
+ sink.close();
+ }, function (reason) {
+ finishWorkerTask(task);
+
+ if (task.terminated) {
+ return;
+ }
+
+ sink.error(reason);
+ throw reason;
+ });
+ });
+ });
+ handler.on('FontFallback', function (data) {
+ return pdfManager.fontFallback(data.id, handler);
+ });
+ handler.on('Cleanup', function wphCleanup(data) {
+ return pdfManager.cleanup();
+ });
+ handler.on('Terminate', function wphTerminate(data) {
+ terminated = true;
+
+ if (pdfManager) {
+ pdfManager.terminate();
+ pdfManager = null;
+ }
+
+ if (cancelXHRs) {
+ cancelXHRs();
+ }
+
+ (0, _primitives.clearPrimitiveCaches)();
+ var waitOn = [];
+ WorkerTasks.forEach(function (task) {
+ waitOn.push(task.finished);
+ task.terminate();
+ });
+ return Promise.all(waitOn).then(function () {
+ handler.destroy();
+ handler = null;
+ });
+ });
+ handler.on('Ready', function wphReady(data) {
+ setupDoc(docParams);
+ docParams = null;
+ });
+ return workerHandlerName;
+ },
+ initializeFromPort: function initializeFromPort(port) {
+ var handler = new _message_handler.MessageHandler('worker', 'main', port);
+ WorkerMessageHandler.setup(handler, port);
+ handler.send('ready', null);
+ }
+};
+exports.WorkerMessageHandler = WorkerMessageHandler;
+
+function isMessagePort(maybePort) {
+ return typeof maybePort.postMessage === 'function' && 'onmessage' in maybePort;
+}
+
+if (typeof window === 'undefined' && !(0, _is_node["default"])() && typeof self !== 'undefined' && isMessagePort(self)) {
+ WorkerMessageHandler.initializeFromPort(self);
+}
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = __w_pdfjs_require__(3);
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+/* WEBPACK VAR INJECTION */(function(module) {
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var runtime = function (exports) {
+ "use strict";
+
+ var Op = Object.prototype;
+ var hasOwn = Op.hasOwnProperty;
+ var undefined;
+ var $Symbol = typeof Symbol === "function" ? Symbol : {};
+ var iteratorSymbol = $Symbol.iterator || "@@iterator";
+ var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator";
+ var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag";
+
+ function wrap(innerFn, outerFn, self, tryLocsList) {
+ var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator;
+ var generator = Object.create(protoGenerator.prototype);
+ var context = new Context(tryLocsList || []);
+ generator._invoke = makeInvokeMethod(innerFn, self, context);
+ return generator;
+ }
+
+ exports.wrap = wrap;
+
+ function tryCatch(fn, obj, arg) {
+ try {
+ return {
+ type: "normal",
+ arg: fn.call(obj, arg)
+ };
+ } catch (err) {
+ return {
+ type: "throw",
+ arg: err
+ };
+ }
+ }
+
+ var GenStateSuspendedStart = "suspendedStart";
+ var GenStateSuspendedYield = "suspendedYield";
+ var GenStateExecuting = "executing";
+ var GenStateCompleted = "completed";
+ var ContinueSentinel = {};
+
+ function Generator() {}
+
+ function GeneratorFunction() {}
+
+ function GeneratorFunctionPrototype() {}
+
+ var IteratorPrototype = {};
+
+ IteratorPrototype[iteratorSymbol] = function () {
+ return this;
+ };
+
+ var getProto = Object.getPrototypeOf;
+ var NativeIteratorPrototype = getProto && getProto(getProto(values([])));
+
+ if (NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) {
+ IteratorPrototype = NativeIteratorPrototype;
+ }
+
+ var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype);
+ GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;
+ GeneratorFunctionPrototype.constructor = GeneratorFunction;
+ GeneratorFunctionPrototype[toStringTagSymbol] = GeneratorFunction.displayName = "GeneratorFunction";
+
+ function defineIteratorMethods(prototype) {
+ ["next", "throw", "return"].forEach(function (method) {
+ prototype[method] = function (arg) {
+ return this._invoke(method, arg);
+ };
+ });
+ }
+
+ exports.isGeneratorFunction = function (genFun) {
+ var ctor = typeof genFun === "function" && genFun.constructor;
+ return ctor ? ctor === GeneratorFunction || (ctor.displayName || ctor.name) === "GeneratorFunction" : false;
+ };
+
+ exports.mark = function (genFun) {
+ if (Object.setPrototypeOf) {
+ Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);
+ } else {
+ genFun.__proto__ = GeneratorFunctionPrototype;
+
+ if (!(toStringTagSymbol in genFun)) {
+ genFun[toStringTagSymbol] = "GeneratorFunction";
+ }
+ }
+
+ genFun.prototype = Object.create(Gp);
+ return genFun;
+ };
+
+ exports.awrap = function (arg) {
+ return {
+ __await: arg
+ };
+ };
+
+ function AsyncIterator(generator) {
+ function invoke(method, arg, resolve, reject) {
+ var record = tryCatch(generator[method], generator, arg);
+
+ if (record.type === "throw") {
+ reject(record.arg);
+ } else {
+ var result = record.arg;
+ var value = result.value;
+
+ if (value && _typeof(value) === "object" && hasOwn.call(value, "__await")) {
+ return Promise.resolve(value.__await).then(function (value) {
+ invoke("next", value, resolve, reject);
+ }, function (err) {
+ invoke("throw", err, resolve, reject);
+ });
+ }
+
+ return Promise.resolve(value).then(function (unwrapped) {
+ result.value = unwrapped;
+ resolve(result);
+ }, function (error) {
+ return invoke("throw", error, resolve, reject);
+ });
+ }
+ }
+
+ var previousPromise;
+
+ function enqueue(method, arg) {
+ function callInvokeWithMethodAndArg() {
+ return new Promise(function (resolve, reject) {
+ invoke(method, arg, resolve, reject);
+ });
+ }
+
+ return previousPromise = previousPromise ? previousPromise.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg();
+ }
+
+ this._invoke = enqueue;
+ }
+
+ defineIteratorMethods(AsyncIterator.prototype);
+
+ AsyncIterator.prototype[asyncIteratorSymbol] = function () {
+ return this;
+ };
+
+ exports.AsyncIterator = AsyncIterator;
+
+ exports.async = function (innerFn, outerFn, self, tryLocsList) {
+ var iter = new AsyncIterator(wrap(innerFn, outerFn, self, tryLocsList));
+ return exports.isGeneratorFunction(outerFn) ? iter : iter.next().then(function (result) {
+ return result.done ? result.value : iter.next();
+ });
+ };
+
+ function makeInvokeMethod(innerFn, self, context) {
+ var state = GenStateSuspendedStart;
+ return function invoke(method, arg) {
+ if (state === GenStateExecuting) {
+ throw new Error("Generator is already running");
+ }
+
+ if (state === GenStateCompleted) {
+ if (method === "throw") {
+ throw arg;
+ }
+
+ return doneResult();
+ }
+
+ context.method = method;
+ context.arg = arg;
+
+ while (true) {
+ var delegate = context.delegate;
+
+ if (delegate) {
+ var delegateResult = maybeInvokeDelegate(delegate, context);
+
+ if (delegateResult) {
+ if (delegateResult === ContinueSentinel) continue;
+ return delegateResult;
+ }
+ }
+
+ if (context.method === "next") {
+ context.sent = context._sent = context.arg;
+ } else if (context.method === "throw") {
+ if (state === GenStateSuspendedStart) {
+ state = GenStateCompleted;
+ throw context.arg;
+ }
+
+ context.dispatchException(context.arg);
+ } else if (context.method === "return") {
+ context.abrupt("return", context.arg);
+ }
+
+ state = GenStateExecuting;
+ var record = tryCatch(innerFn, self, context);
+
+ if (record.type === "normal") {
+ state = context.done ? GenStateCompleted : GenStateSuspendedYield;
+
+ if (record.arg === ContinueSentinel) {
+ continue;
+ }
+
+ return {
+ value: record.arg,
+ done: context.done
+ };
+ } else if (record.type === "throw") {
+ state = GenStateCompleted;
+ context.method = "throw";
+ context.arg = record.arg;
+ }
+ }
+ };
+ }
+
+ function maybeInvokeDelegate(delegate, context) {
+ var method = delegate.iterator[context.method];
+
+ if (method === undefined) {
+ context.delegate = null;
+
+ if (context.method === "throw") {
+ if (delegate.iterator["return"]) {
+ context.method = "return";
+ context.arg = undefined;
+ maybeInvokeDelegate(delegate, context);
+
+ if (context.method === "throw") {
+ return ContinueSentinel;
+ }
+ }
+
+ context.method = "throw";
+ context.arg = new TypeError("The iterator does not provide a 'throw' method");
+ }
+
+ return ContinueSentinel;
+ }
+
+ var record = tryCatch(method, delegate.iterator, context.arg);
+
+ if (record.type === "throw") {
+ context.method = "throw";
+ context.arg = record.arg;
+ context.delegate = null;
+ return ContinueSentinel;
+ }
+
+ var info = record.arg;
+
+ if (!info) {
+ context.method = "throw";
+ context.arg = new TypeError("iterator result is not an object");
+ context.delegate = null;
+ return ContinueSentinel;
+ }
+
+ if (info.done) {
+ context[delegate.resultName] = info.value;
+ context.next = delegate.nextLoc;
+
+ if (context.method !== "return") {
+ context.method = "next";
+ context.arg = undefined;
+ }
+ } else {
+ return info;
+ }
+
+ context.delegate = null;
+ return ContinueSentinel;
+ }
+
+ defineIteratorMethods(Gp);
+ Gp[toStringTagSymbol] = "Generator";
+
+ Gp[iteratorSymbol] = function () {
+ return this;
+ };
+
+ Gp.toString = function () {
+ return "[object Generator]";
+ };
+
+ function pushTryEntry(locs) {
+ var entry = {
+ tryLoc: locs[0]
+ };
+
+ if (1 in locs) {
+ entry.catchLoc = locs[1];
+ }
+
+ if (2 in locs) {
+ entry.finallyLoc = locs[2];
+ entry.afterLoc = locs[3];
+ }
+
+ this.tryEntries.push(entry);
+ }
+
+ function resetTryEntry(entry) {
+ var record = entry.completion || {};
+ record.type = "normal";
+ delete record.arg;
+ entry.completion = record;
+ }
+
+ function Context(tryLocsList) {
+ this.tryEntries = [{
+ tryLoc: "root"
+ }];
+ tryLocsList.forEach(pushTryEntry, this);
+ this.reset(true);
+ }
+
+ exports.keys = function (object) {
+ var keys = [];
+
+ for (var key in object) {
+ keys.push(key);
+ }
+
+ keys.reverse();
+ return function next() {
+ while (keys.length) {
+ var key = keys.pop();
+
+ if (key in object) {
+ next.value = key;
+ next.done = false;
+ return next;
+ }
+ }
+
+ next.done = true;
+ return next;
+ };
+ };
+
+ function values(iterable) {
+ if (iterable) {
+ var iteratorMethod = iterable[iteratorSymbol];
+
+ if (iteratorMethod) {
+ return iteratorMethod.call(iterable);
+ }
+
+ if (typeof iterable.next === "function") {
+ return iterable;
+ }
+
+ if (!isNaN(iterable.length)) {
+ var i = -1,
+ next = function next() {
+ while (++i < iterable.length) {
+ if (hasOwn.call(iterable, i)) {
+ next.value = iterable[i];
+ next.done = false;
+ return next;
+ }
+ }
+
+ next.value = undefined;
+ next.done = true;
+ return next;
+ };
+
+ return next.next = next;
+ }
+ }
+
+ return {
+ next: doneResult
+ };
+ }
+
+ exports.values = values;
+
+ function doneResult() {
+ return {
+ value: undefined,
+ done: true
+ };
+ }
+
+ Context.prototype = {
+ constructor: Context,
+ reset: function reset(skipTempReset) {
+ this.prev = 0;
+ this.next = 0;
+ this.sent = this._sent = undefined;
+ this.done = false;
+ this.delegate = null;
+ this.method = "next";
+ this.arg = undefined;
+ this.tryEntries.forEach(resetTryEntry);
+
+ if (!skipTempReset) {
+ for (var name in this) {
+ if (name.charAt(0) === "t" && hasOwn.call(this, name) && !isNaN(+name.slice(1))) {
+ this[name] = undefined;
+ }
+ }
+ }
+ },
+ stop: function stop() {
+ this.done = true;
+ var rootEntry = this.tryEntries[0];
+ var rootRecord = rootEntry.completion;
+
+ if (rootRecord.type === "throw") {
+ throw rootRecord.arg;
+ }
+
+ return this.rval;
+ },
+ dispatchException: function dispatchException(exception) {
+ if (this.done) {
+ throw exception;
+ }
+
+ var context = this;
+
+ function handle(loc, caught) {
+ record.type = "throw";
+ record.arg = exception;
+ context.next = loc;
+
+ if (caught) {
+ context.method = "next";
+ context.arg = undefined;
+ }
+
+ return !!caught;
+ }
+
+ for (var i = this.tryEntries.length - 1; i >= 0; --i) {
+ var entry = this.tryEntries[i];
+ var record = entry.completion;
+
+ if (entry.tryLoc === "root") {
+ return handle("end");
+ }
+
+ if (entry.tryLoc <= this.prev) {
+ var hasCatch = hasOwn.call(entry, "catchLoc");
+ var hasFinally = hasOwn.call(entry, "finallyLoc");
+
+ if (hasCatch && hasFinally) {
+ if (this.prev < entry.catchLoc) {
+ return handle(entry.catchLoc, true);
+ } else if (this.prev < entry.finallyLoc) {
+ return handle(entry.finallyLoc);
+ }
+ } else if (hasCatch) {
+ if (this.prev < entry.catchLoc) {
+ return handle(entry.catchLoc, true);
+ }
+ } else if (hasFinally) {
+ if (this.prev < entry.finallyLoc) {
+ return handle(entry.finallyLoc);
+ }
+ } else {
+ throw new Error("try statement without catch or finally");
+ }
+ }
+ }
+ },
+ abrupt: function abrupt(type, arg) {
+ for (var i = this.tryEntries.length - 1; i >= 0; --i) {
+ var entry = this.tryEntries[i];
+
+ if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) {
+ var finallyEntry = entry;
+ break;
+ }
+ }
+
+ if (finallyEntry && (type === "break" || type === "continue") && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc) {
+ finallyEntry = null;
+ }
+
+ var record = finallyEntry ? finallyEntry.completion : {};
+ record.type = type;
+ record.arg = arg;
+
+ if (finallyEntry) {
+ this.method = "next";
+ this.next = finallyEntry.finallyLoc;
+ return ContinueSentinel;
+ }
+
+ return this.complete(record);
+ },
+ complete: function complete(record, afterLoc) {
+ if (record.type === "throw") {
+ throw record.arg;
+ }
+
+ if (record.type === "break" || record.type === "continue") {
+ this.next = record.arg;
+ } else if (record.type === "return") {
+ this.rval = this.arg = record.arg;
+ this.method = "return";
+ this.next = "end";
+ } else if (record.type === "normal" && afterLoc) {
+ this.next = afterLoc;
+ }
+
+ return ContinueSentinel;
+ },
+ finish: function finish(finallyLoc) {
+ for (var i = this.tryEntries.length - 1; i >= 0; --i) {
+ var entry = this.tryEntries[i];
+
+ if (entry.finallyLoc === finallyLoc) {
+ this.complete(entry.completion, entry.afterLoc);
+ resetTryEntry(entry);
+ return ContinueSentinel;
+ }
+ }
+ },
+ "catch": function _catch(tryLoc) {
+ for (var i = this.tryEntries.length - 1; i >= 0; --i) {
+ var entry = this.tryEntries[i];
+
+ if (entry.tryLoc === tryLoc) {
+ var record = entry.completion;
+
+ if (record.type === "throw") {
+ var thrown = record.arg;
+ resetTryEntry(entry);
+ }
+
+ return thrown;
+ }
+ }
+
+ throw new Error("illegal catch attempt");
+ },
+ delegateYield: function delegateYield(iterable, resultName, nextLoc) {
+ this.delegate = {
+ iterator: values(iterable),
+ resultName: resultName,
+ nextLoc: nextLoc
+ };
+
+ if (this.method === "next") {
+ this.arg = undefined;
+ }
+
+ return ContinueSentinel;
+ }
+ };
+ return exports;
+}(( false ? undefined : _typeof(module)) === "object" ? module.exports : {});
+
+try {
+ regeneratorRuntime = runtime;
+} catch (accidentalStrictMode) {
+ Function("r", "regeneratorRuntime = r")(runtime);
+}
+/* WEBPACK VAR INJECTION */}.call(this, __w_pdfjs_require__(4)(module)))
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (module) {
+ if (!module.webpackPolyfill) {
+ module.deprecate = function () {};
+
+ module.paths = [];
+ if (!module.children) module.children = [];
+ Object.defineProperty(module, "loaded", {
+ enumerable: true,
+ get: function get() {
+ return module.l;
+ }
+ });
+ Object.defineProperty(module, "id", {
+ enumerable: true,
+ get: function get() {
+ return module.i;
+ }
+ });
+ module.webpackPolyfill = 1;
+ }
+
+ return module;
+};
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.arrayByteLength = arrayByteLength;
+exports.arraysToBytes = arraysToBytes;
+exports.assert = assert;
+exports.bytesToString = bytesToString;
+exports.createPromiseCapability = createPromiseCapability;
+exports.getVerbosityLevel = getVerbosityLevel;
+exports.info = info;
+exports.isArrayBuffer = isArrayBuffer;
+exports.isArrayEqual = isArrayEqual;
+exports.isBool = isBool;
+exports.isEmptyObj = isEmptyObj;
+exports.isNum = isNum;
+exports.isString = isString;
+exports.isSpace = isSpace;
+exports.isSameOrigin = isSameOrigin;
+exports.createValidAbsoluteUrl = createValidAbsoluteUrl;
+exports.isLittleEndian = isLittleEndian;
+exports.isEvalSupported = isEvalSupported;
+exports.log2 = log2;
+exports.readInt8 = readInt8;
+exports.readUint16 = readUint16;
+exports.readUint32 = readUint32;
+exports.removeNullCharacters = removeNullCharacters;
+exports.setVerbosityLevel = setVerbosityLevel;
+exports.shadow = shadow;
+exports.string32 = string32;
+exports.stringToBytes = stringToBytes;
+exports.stringToPDFString = stringToPDFString;
+exports.stringToUTF8String = stringToUTF8String;
+exports.utf8StringToString = utf8StringToString;
+exports.warn = warn;
+exports.unreachable = unreachable;
+Object.defineProperty(exports, "ReadableStream", {
+ enumerable: true,
+ get: function get() {
+ return _streams_polyfill.ReadableStream;
+ }
+});
+Object.defineProperty(exports, "URL", {
+ enumerable: true,
+ get: function get() {
+ return _url_polyfill.URL;
+ }
+});
+exports.createObjectURL = exports.FormatError = exports.Util = exports.UnknownErrorException = exports.UnexpectedResponseException = exports.TextRenderingMode = exports.StreamType = exports.PermissionFlag = exports.PasswordResponses = exports.PasswordException = exports.NativeImageDecoding = exports.MissingPDFException = exports.InvalidPDFException = exports.AbortException = exports.CMapCompressionType = exports.ImageKind = exports.FontType = exports.AnnotationType = exports.AnnotationFlag = exports.AnnotationFieldFlag = exports.AnnotationBorderStyleType = exports.UNSUPPORTED_FEATURES = exports.VerbosityLevel = exports.OPS = exports.IDENTITY_MATRIX = exports.FONT_IDENTITY_MATRIX = void 0;
+
+__w_pdfjs_require__(6);
+
+var _streams_polyfill = __w_pdfjs_require__(147);
+
+var _url_polyfill = __w_pdfjs_require__(149);
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
+exports.IDENTITY_MATRIX = IDENTITY_MATRIX;
+var FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
+exports.FONT_IDENTITY_MATRIX = FONT_IDENTITY_MATRIX;
+var NativeImageDecoding = {
+ NONE: 'none',
+ DECODE: 'decode',
+ DISPLAY: 'display'
+};
+exports.NativeImageDecoding = NativeImageDecoding;
+var PermissionFlag = {
+ PRINT: 0x04,
+ MODIFY_CONTENTS: 0x08,
+ COPY: 0x10,
+ MODIFY_ANNOTATIONS: 0x20,
+ FILL_INTERACTIVE_FORMS: 0x100,
+ COPY_FOR_ACCESSIBILITY: 0x200,
+ ASSEMBLE: 0x400,
+ PRINT_HIGH_QUALITY: 0x800
+};
+exports.PermissionFlag = PermissionFlag;
+var TextRenderingMode = {
+ FILL: 0,
+ STROKE: 1,
+ FILL_STROKE: 2,
+ INVISIBLE: 3,
+ FILL_ADD_TO_PATH: 4,
+ STROKE_ADD_TO_PATH: 5,
+ FILL_STROKE_ADD_TO_PATH: 6,
+ ADD_TO_PATH: 7,
+ FILL_STROKE_MASK: 3,
+ ADD_TO_PATH_FLAG: 4
+};
+exports.TextRenderingMode = TextRenderingMode;
+var ImageKind = {
+ GRAYSCALE_1BPP: 1,
+ RGB_24BPP: 2,
+ RGBA_32BPP: 3
+};
+exports.ImageKind = ImageKind;
+var AnnotationType = {
+ TEXT: 1,
+ LINK: 2,
+ FREETEXT: 3,
+ LINE: 4,
+ SQUARE: 5,
+ CIRCLE: 6,
+ POLYGON: 7,
+ POLYLINE: 8,
+ HIGHLIGHT: 9,
+ UNDERLINE: 10,
+ SQUIGGLY: 11,
+ STRIKEOUT: 12,
+ STAMP: 13,
+ CARET: 14,
+ INK: 15,
+ POPUP: 16,
+ FILEATTACHMENT: 17,
+ SOUND: 18,
+ MOVIE: 19,
+ WIDGET: 20,
+ SCREEN: 21,
+ PRINTERMARK: 22,
+ TRAPNET: 23,
+ WATERMARK: 24,
+ THREED: 25,
+ REDACT: 26
+};
+exports.AnnotationType = AnnotationType;
+var AnnotationFlag = {
+ INVISIBLE: 0x01,
+ HIDDEN: 0x02,
+ PRINT: 0x04,
+ NOZOOM: 0x08,
+ NOROTATE: 0x10,
+ NOVIEW: 0x20,
+ READONLY: 0x40,
+ LOCKED: 0x80,
+ TOGGLENOVIEW: 0x100,
+ LOCKEDCONTENTS: 0x200
+};
+exports.AnnotationFlag = AnnotationFlag;
+var AnnotationFieldFlag = {
+ READONLY: 0x0000001,
+ REQUIRED: 0x0000002,
+ NOEXPORT: 0x0000004,
+ MULTILINE: 0x0001000,
+ PASSWORD: 0x0002000,
+ NOTOGGLETOOFF: 0x0004000,
+ RADIO: 0x0008000,
+ PUSHBUTTON: 0x0010000,
+ COMBO: 0x0020000,
+ EDIT: 0x0040000,
+ SORT: 0x0080000,
+ FILESELECT: 0x0100000,
+ MULTISELECT: 0x0200000,
+ DONOTSPELLCHECK: 0x0400000,
+ DONOTSCROLL: 0x0800000,
+ COMB: 0x1000000,
+ RICHTEXT: 0x2000000,
+ RADIOSINUNISON: 0x2000000,
+ COMMITONSELCHANGE: 0x4000000
+};
+exports.AnnotationFieldFlag = AnnotationFieldFlag;
+var AnnotationBorderStyleType = {
+ SOLID: 1,
+ DASHED: 2,
+ BEVELED: 3,
+ INSET: 4,
+ UNDERLINE: 5
+};
+exports.AnnotationBorderStyleType = AnnotationBorderStyleType;
+var StreamType = {
+ UNKNOWN: 0,
+ FLATE: 1,
+ LZW: 2,
+ DCT: 3,
+ JPX: 4,
+ JBIG: 5,
+ A85: 6,
+ AHX: 7,
+ CCF: 8,
+ RL: 9
+};
+exports.StreamType = StreamType;
+var FontType = {
+ UNKNOWN: 0,
+ TYPE1: 1,
+ TYPE1C: 2,
+ CIDFONTTYPE0: 3,
+ CIDFONTTYPE0C: 4,
+ TRUETYPE: 5,
+ CIDFONTTYPE2: 6,
+ TYPE3: 7,
+ OPENTYPE: 8,
+ TYPE0: 9,
+ MMTYPE1: 10
+};
+exports.FontType = FontType;
+var VerbosityLevel = {
+ ERRORS: 0,
+ WARNINGS: 1,
+ INFOS: 5
+};
+exports.VerbosityLevel = VerbosityLevel;
+var CMapCompressionType = {
+ NONE: 0,
+ BINARY: 1,
+ STREAM: 2
+};
+exports.CMapCompressionType = CMapCompressionType;
+var OPS = {
+ dependency: 1,
+ setLineWidth: 2,
+ setLineCap: 3,
+ setLineJoin: 4,
+ setMiterLimit: 5,
+ setDash: 6,
+ setRenderingIntent: 7,
+ setFlatness: 8,
+ setGState: 9,
+ save: 10,
+ restore: 11,
+ transform: 12,
+ moveTo: 13,
+ lineTo: 14,
+ curveTo: 15,
+ curveTo2: 16,
+ curveTo3: 17,
+ closePath: 18,
+ rectangle: 19,
+ stroke: 20,
+ closeStroke: 21,
+ fill: 22,
+ eoFill: 23,
+ fillStroke: 24,
+ eoFillStroke: 25,
+ closeFillStroke: 26,
+ closeEOFillStroke: 27,
+ endPath: 28,
+ clip: 29,
+ eoClip: 30,
+ beginText: 31,
+ endText: 32,
+ setCharSpacing: 33,
+ setWordSpacing: 34,
+ setHScale: 35,
+ setLeading: 36,
+ setFont: 37,
+ setTextRenderingMode: 38,
+ setTextRise: 39,
+ moveText: 40,
+ setLeadingMoveText: 41,
+ setTextMatrix: 42,
+ nextLine: 43,
+ showText: 44,
+ showSpacedText: 45,
+ nextLineShowText: 46,
+ nextLineSetSpacingShowText: 47,
+ setCharWidth: 48,
+ setCharWidthAndBounds: 49,
+ setStrokeColorSpace: 50,
+ setFillColorSpace: 51,
+ setStrokeColor: 52,
+ setStrokeColorN: 53,
+ setFillColor: 54,
+ setFillColorN: 55,
+ setStrokeGray: 56,
+ setFillGray: 57,
+ setStrokeRGBColor: 58,
+ setFillRGBColor: 59,
+ setStrokeCMYKColor: 60,
+ setFillCMYKColor: 61,
+ shadingFill: 62,
+ beginInlineImage: 63,
+ beginImageData: 64,
+ endInlineImage: 65,
+ paintXObject: 66,
+ markPoint: 67,
+ markPointProps: 68,
+ beginMarkedContent: 69,
+ beginMarkedContentProps: 70,
+ endMarkedContent: 71,
+ beginCompat: 72,
+ endCompat: 73,
+ paintFormXObjectBegin: 74,
+ paintFormXObjectEnd: 75,
+ beginGroup: 76,
+ endGroup: 77,
+ beginAnnotations: 78,
+ endAnnotations: 79,
+ beginAnnotation: 80,
+ endAnnotation: 81,
+ paintJpegXObject: 82,
+ paintImageMaskXObject: 83,
+ paintImageMaskXObjectGroup: 84,
+ paintImageXObject: 85,
+ paintInlineImageXObject: 86,
+ paintInlineImageXObjectGroup: 87,
+ paintImageXObjectRepeat: 88,
+ paintImageMaskXObjectRepeat: 89,
+ paintSolidColorImageMask: 90,
+ constructPath: 91
+};
+exports.OPS = OPS;
+var UNSUPPORTED_FEATURES = {
+ unknown: 'unknown',
+ forms: 'forms',
+ javaScript: 'javaScript',
+ smask: 'smask',
+ shadingPattern: 'shadingPattern',
+ font: 'font'
+};
+exports.UNSUPPORTED_FEATURES = UNSUPPORTED_FEATURES;
+var PasswordResponses = {
+ NEED_PASSWORD: 1,
+ INCORRECT_PASSWORD: 2
+};
+exports.PasswordResponses = PasswordResponses;
+var verbosity = VerbosityLevel.WARNINGS;
+
+function setVerbosityLevel(level) {
+ if (Number.isInteger(level)) {
+ verbosity = level;
+ }
+}
+
+function getVerbosityLevel() {
+ return verbosity;
+}
+
+function info(msg) {
+ if (verbosity >= VerbosityLevel.INFOS) {
+ console.log('Info: ' + msg);
+ }
+}
+
+function warn(msg) {
+ if (verbosity >= VerbosityLevel.WARNINGS) {
+ console.log('Warning: ' + msg);
+ }
+}
+
+function unreachable(msg) {
+ throw new Error(msg);
+}
+
+function assert(cond, msg) {
+ if (!cond) {
+ unreachable(msg);
+ }
+}
+
+function isSameOrigin(baseUrl, otherUrl) {
+ try {
+ var base = new _url_polyfill.URL(baseUrl);
+
+ if (!base.origin || base.origin === 'null') {
+ return false;
+ }
+ } catch (e) {
+ return false;
+ }
+
+ var other = new _url_polyfill.URL(otherUrl, base);
+ return base.origin === other.origin;
+}
+
+function _isValidProtocol(url) {
+ if (!url) {
+ return false;
+ }
+
+ switch (url.protocol) {
+ case 'http:':
+ case 'https:':
+ case 'ftp:':
+ case 'mailto:':
+ case 'tel:':
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+function createValidAbsoluteUrl(url, baseUrl) {
+ if (!url) {
+ return null;
+ }
+
+ try {
+ var absoluteUrl = baseUrl ? new _url_polyfill.URL(url, baseUrl) : new _url_polyfill.URL(url);
+
+ if (_isValidProtocol(absoluteUrl)) {
+ return absoluteUrl;
+ }
+ } catch (ex) {}
+
+ return null;
+}
+
+function shadow(obj, prop, value) {
+ Object.defineProperty(obj, prop, {
+ value: value,
+ enumerable: true,
+ configurable: true,
+ writable: false
+ });
+ return value;
+}
+
+var PasswordException = function PasswordExceptionClosure() {
+ function PasswordException(msg, code) {
+ this.name = 'PasswordException';
+ this.message = msg;
+ this.code = code;
+ }
+
+ PasswordException.prototype = new Error();
+ PasswordException.constructor = PasswordException;
+ return PasswordException;
+}();
+
+exports.PasswordException = PasswordException;
+
+var UnknownErrorException = function UnknownErrorExceptionClosure() {
+ function UnknownErrorException(msg, details) {
+ this.name = 'UnknownErrorException';
+ this.message = msg;
+ this.details = details;
+ }
+
+ UnknownErrorException.prototype = new Error();
+ UnknownErrorException.constructor = UnknownErrorException;
+ return UnknownErrorException;
+}();
+
+exports.UnknownErrorException = UnknownErrorException;
+
+var InvalidPDFException = function InvalidPDFExceptionClosure() {
+ function InvalidPDFException(msg) {
+ this.name = 'InvalidPDFException';
+ this.message = msg;
+ }
+
+ InvalidPDFException.prototype = new Error();
+ InvalidPDFException.constructor = InvalidPDFException;
+ return InvalidPDFException;
+}();
+
+exports.InvalidPDFException = InvalidPDFException;
+
+var MissingPDFException = function MissingPDFExceptionClosure() {
+ function MissingPDFException(msg) {
+ this.name = 'MissingPDFException';
+ this.message = msg;
+ }
+
+ MissingPDFException.prototype = new Error();
+ MissingPDFException.constructor = MissingPDFException;
+ return MissingPDFException;
+}();
+
+exports.MissingPDFException = MissingPDFException;
+
+var UnexpectedResponseException = function UnexpectedResponseExceptionClosure() {
+ function UnexpectedResponseException(msg, status) {
+ this.name = 'UnexpectedResponseException';
+ this.message = msg;
+ this.status = status;
+ }
+
+ UnexpectedResponseException.prototype = new Error();
+ UnexpectedResponseException.constructor = UnexpectedResponseException;
+ return UnexpectedResponseException;
+}();
+
+exports.UnexpectedResponseException = UnexpectedResponseException;
+
+var FormatError = function FormatErrorClosure() {
+ function FormatError(msg) {
+ this.message = msg;
+ }
+
+ FormatError.prototype = new Error();
+ FormatError.prototype.name = 'FormatError';
+ FormatError.constructor = FormatError;
+ return FormatError;
+}();
+
+exports.FormatError = FormatError;
+
+var AbortException = function AbortExceptionClosure() {
+ function AbortException(msg) {
+ this.name = 'AbortException';
+ this.message = msg;
+ }
+
+ AbortException.prototype = new Error();
+ AbortException.constructor = AbortException;
+ return AbortException;
+}();
+
+exports.AbortException = AbortException;
+var NullCharactersRegExp = /\x00/g;
+
+function removeNullCharacters(str) {
+ if (typeof str !== 'string') {
+ warn('The argument for removeNullCharacters must be a string.');
+ return str;
+ }
+
+ return str.replace(NullCharactersRegExp, '');
+}
+
+function bytesToString(bytes) {
+ assert(bytes !== null && _typeof(bytes) === 'object' && bytes.length !== undefined, 'Invalid argument for bytesToString');
+ var length = bytes.length;
+ var MAX_ARGUMENT_COUNT = 8192;
+
+ if (length < MAX_ARGUMENT_COUNT) {
+ return String.fromCharCode.apply(null, bytes);
+ }
+
+ var strBuf = [];
+
+ for (var i = 0; i < length; i += MAX_ARGUMENT_COUNT) {
+ var chunkEnd = Math.min(i + MAX_ARGUMENT_COUNT, length);
+ var chunk = bytes.subarray(i, chunkEnd);
+ strBuf.push(String.fromCharCode.apply(null, chunk));
+ }
+
+ return strBuf.join('');
+}
+
+function stringToBytes(str) {
+ assert(typeof str === 'string', 'Invalid argument for stringToBytes');
+ var length = str.length;
+ var bytes = new Uint8Array(length);
+
+ for (var i = 0; i < length; ++i) {
+ bytes[i] = str.charCodeAt(i) & 0xFF;
+ }
+
+ return bytes;
+}
+
+function arrayByteLength(arr) {
+ if (arr.length !== undefined) {
+ return arr.length;
+ }
+
+ assert(arr.byteLength !== undefined);
+ return arr.byteLength;
+}
+
+function arraysToBytes(arr) {
+ if (arr.length === 1 && arr[0] instanceof Uint8Array) {
+ return arr[0];
+ }
+
+ var resultLength = 0;
+ var i,
+ ii = arr.length;
+ var item, itemLength;
+
+ for (i = 0; i < ii; i++) {
+ item = arr[i];
+ itemLength = arrayByteLength(item);
+ resultLength += itemLength;
+ }
+
+ var pos = 0;
+ var data = new Uint8Array(resultLength);
+
+ for (i = 0; i < ii; i++) {
+ item = arr[i];
+
+ if (!(item instanceof Uint8Array)) {
+ if (typeof item === 'string') {
+ item = stringToBytes(item);
+ } else {
+ item = new Uint8Array(item);
+ }
+ }
+
+ itemLength = item.byteLength;
+ data.set(item, pos);
+ pos += itemLength;
+ }
+
+ return data;
+}
+
+function string32(value) {
+ return String.fromCharCode(value >> 24 & 0xff, value >> 16 & 0xff, value >> 8 & 0xff, value & 0xff);
+}
+
+function log2(x) {
+ if (x <= 0) {
+ return 0;
+ }
+
+ return Math.ceil(Math.log2(x));
+}
+
+function readInt8(data, start) {
+ return data[start] << 24 >> 24;
+}
+
+function readUint16(data, offset) {
+ return data[offset] << 8 | data[offset + 1];
+}
+
+function readUint32(data, offset) {
+ return (data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]) >>> 0;
+}
+
+function isLittleEndian() {
+ var buffer8 = new Uint8Array(4);
+ buffer8[0] = 1;
+ var view32 = new Uint32Array(buffer8.buffer, 0, 1);
+ return view32[0] === 1;
+}
+
+function isEvalSupported() {
+ try {
+ new Function('');
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+var Util = function UtilClosure() {
+ function Util() {}
+
+ var rgbBuf = ['rgb(', 0, ',', 0, ',', 0, ')'];
+
+ Util.makeCssRgb = function Util_makeCssRgb(r, g, b) {
+ rgbBuf[1] = r;
+ rgbBuf[3] = g;
+ rgbBuf[5] = b;
+ return rgbBuf.join('');
+ };
+
+ Util.transform = function Util_transform(m1, m2) {
+ return [m1[0] * m2[0] + m1[2] * m2[1], m1[1] * m2[0] + m1[3] * m2[1], m1[0] * m2[2] + m1[2] * m2[3], m1[1] * m2[2] + m1[3] * m2[3], m1[0] * m2[4] + m1[2] * m2[5] + m1[4], m1[1] * m2[4] + m1[3] * m2[5] + m1[5]];
+ };
+
+ Util.applyTransform = function Util_applyTransform(p, m) {
+ var xt = p[0] * m[0] + p[1] * m[2] + m[4];
+ var yt = p[0] * m[1] + p[1] * m[3] + m[5];
+ return [xt, yt];
+ };
+
+ Util.applyInverseTransform = function Util_applyInverseTransform(p, m) {
+ var d = m[0] * m[3] - m[1] * m[2];
+ var xt = (p[0] * m[3] - p[1] * m[2] + m[2] * m[5] - m[4] * m[3]) / d;
+ var yt = (-p[0] * m[1] + p[1] * m[0] + m[4] * m[1] - m[5] * m[0]) / d;
+ return [xt, yt];
+ };
+
+ Util.getAxialAlignedBoundingBox = function Util_getAxialAlignedBoundingBox(r, m) {
+ var p1 = Util.applyTransform(r, m);
+ var p2 = Util.applyTransform(r.slice(2, 4), m);
+ var p3 = Util.applyTransform([r[0], r[3]], m);
+ var p4 = Util.applyTransform([r[2], r[1]], m);
+ return [Math.min(p1[0], p2[0], p3[0], p4[0]), Math.min(p1[1], p2[1], p3[1], p4[1]), Math.max(p1[0], p2[0], p3[0], p4[0]), Math.max(p1[1], p2[1], p3[1], p4[1])];
+ };
+
+ Util.inverseTransform = function Util_inverseTransform(m) {
+ var d = m[0] * m[3] - m[1] * m[2];
+ return [m[3] / d, -m[1] / d, -m[2] / d, m[0] / d, (m[2] * m[5] - m[4] * m[3]) / d, (m[4] * m[1] - m[5] * m[0]) / d];
+ };
+
+ Util.apply3dTransform = function Util_apply3dTransform(m, v) {
+ return [m[0] * v[0] + m[1] * v[1] + m[2] * v[2], m[3] * v[0] + m[4] * v[1] + m[5] * v[2], m[6] * v[0] + m[7] * v[1] + m[8] * v[2]];
+ };
+
+ Util.singularValueDecompose2dScale = function Util_singularValueDecompose2dScale(m) {
+ var transpose = [m[0], m[2], m[1], m[3]];
+ var a = m[0] * transpose[0] + m[1] * transpose[2];
+ var b = m[0] * transpose[1] + m[1] * transpose[3];
+ var c = m[2] * transpose[0] + m[3] * transpose[2];
+ var d = m[2] * transpose[1] + m[3] * transpose[3];
+ var first = (a + d) / 2;
+ var second = Math.sqrt((a + d) * (a + d) - 4 * (a * d - c * b)) / 2;
+ var sx = first + second || 1;
+ var sy = first - second || 1;
+ return [Math.sqrt(sx), Math.sqrt(sy)];
+ };
+
+ Util.normalizeRect = function Util_normalizeRect(rect) {
+ var r = rect.slice(0);
+
+ if (rect[0] > rect[2]) {
+ r[0] = rect[2];
+ r[2] = rect[0];
+ }
+
+ if (rect[1] > rect[3]) {
+ r[1] = rect[3];
+ r[3] = rect[1];
+ }
+
+ return r;
+ };
+
+ Util.intersect = function Util_intersect(rect1, rect2) {
+ function compare(a, b) {
+ return a - b;
+ }
+
+ var orderedX = [rect1[0], rect1[2], rect2[0], rect2[2]].sort(compare),
+ orderedY = [rect1[1], rect1[3], rect2[1], rect2[3]].sort(compare),
+ result = [];
+ rect1 = Util.normalizeRect(rect1);
+ rect2 = Util.normalizeRect(rect2);
+
+ if (orderedX[0] === rect1[0] && orderedX[1] === rect2[0] || orderedX[0] === rect2[0] && orderedX[1] === rect1[0]) {
+ result[0] = orderedX[1];
+ result[2] = orderedX[2];
+ } else {
+ return false;
+ }
+
+ if (orderedY[0] === rect1[1] && orderedY[1] === rect2[1] || orderedY[0] === rect2[1] && orderedY[1] === rect1[1]) {
+ result[1] = orderedY[1];
+ result[3] = orderedY[2];
+ } else {
+ return false;
+ }
+
+ return result;
+ };
+
+ return Util;
+}();
+
+exports.Util = Util;
+var PDFStringTranslateTable = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2D8, 0x2C7, 0x2C6, 0x2D9, 0x2DD, 0x2DB, 0x2DA, 0x2DC, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2022, 0x2020, 0x2021, 0x2026, 0x2014, 0x2013, 0x192, 0x2044, 0x2039, 0x203A, 0x2212, 0x2030, 0x201E, 0x201C, 0x201D, 0x2018, 0x2019, 0x201A, 0x2122, 0xFB01, 0xFB02, 0x141, 0x152, 0x160, 0x178, 0x17D, 0x131, 0x142, 0x153, 0x161, 0x17E, 0, 0x20AC];
+
+function stringToPDFString(str) {
+ var i,
+ n = str.length,
+ strBuf = [];
+
+ if (str[0] === '\xFE' && str[1] === '\xFF') {
+ for (i = 2; i < n; i += 2) {
+ strBuf.push(String.fromCharCode(str.charCodeAt(i) << 8 | str.charCodeAt(i + 1)));
+ }
+ } else {
+ for (i = 0; i < n; ++i) {
+ var code = PDFStringTranslateTable[str.charCodeAt(i)];
+ strBuf.push(code ? String.fromCharCode(code) : str.charAt(i));
+ }
+ }
+
+ return strBuf.join('');
+}
+
+function stringToUTF8String(str) {
+ return decodeURIComponent(escape(str));
+}
+
+function utf8StringToString(str) {
+ return unescape(encodeURIComponent(str));
+}
+
+function isEmptyObj(obj) {
+ for (var key in obj) {
+ return false;
+ }
+
+ return true;
+}
+
+function isBool(v) {
+ return typeof v === 'boolean';
+}
+
+function isNum(v) {
+ return typeof v === 'number';
+}
+
+function isString(v) {
+ return typeof v === 'string';
+}
+
+function isArrayBuffer(v) {
+ return _typeof(v) === 'object' && v !== null && v.byteLength !== undefined;
+}
+
+function isArrayEqual(arr1, arr2) {
+ if (arr1.length !== arr2.length) {
+ return false;
+ }
+
+ return arr1.every(function (element, index) {
+ return element === arr2[index];
+ });
+}
+
+function isSpace(ch) {
+ return ch === 0x20 || ch === 0x09 || ch === 0x0D || ch === 0x0A;
+}
+
+function createPromiseCapability() {
+ var capability = Object.create(null);
+ var isSettled = false;
+ Object.defineProperty(capability, 'settled', {
+ get: function get() {
+ return isSettled;
+ }
+ });
+ capability.promise = new Promise(function (resolve, reject) {
+ capability.resolve = function (data) {
+ isSettled = true;
+ resolve(data);
+ };
+
+ capability.reject = function (reason) {
+ isSettled = true;
+ reject(reason);
+ };
+ });
+ return capability;
+}
+
+var createObjectURL = function createObjectURLClosure() {
+ var digits = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+ return function createObjectURL(data, contentType) {
+ var forceDataSchema = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+
+ if (!forceDataSchema && _url_polyfill.URL.createObjectURL) {
+ var blob = new Blob([data], {
+ type: contentType
+ });
+ return _url_polyfill.URL.createObjectURL(blob);
+ }
+
+ var buffer = 'data:' + contentType + ';base64,';
+
+ for (var i = 0, ii = data.length; i < ii; i += 3) {
+ var b1 = data[i] & 0xFF;
+ var b2 = data[i + 1] & 0xFF;
+ var b3 = data[i + 2] & 0xFF;
+ var d1 = b1 >> 2,
+ d2 = (b1 & 3) << 4 | b2 >> 4;
+ var d3 = i + 1 < ii ? (b2 & 0xF) << 2 | b3 >> 6 : 64;
+ var d4 = i + 2 < ii ? b3 & 0x3F : 64;
+ buffer += digits[d1] + digits[d2] + digits[d3] + digits[d4];
+ }
+
+ return buffer;
+ };
+}();
+
+exports.createObjectURL = createObjectURL;
+
+/***/ }),
+/* 6 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var globalScope = __w_pdfjs_require__(7);
+
+if (!globalScope._pdfjsCompatibilityChecked) {
+ globalScope._pdfjsCompatibilityChecked = true;
+
+ var isNodeJS = __w_pdfjs_require__(8);
+
+ var hasDOM = (typeof window === "undefined" ? "undefined" : _typeof(window)) === 'object' && (typeof document === "undefined" ? "undefined" : _typeof(document)) === 'object';
+
+ (function checkNodeBtoa() {
+ if (globalScope.btoa || !isNodeJS()) {
+ return;
+ }
+
+ globalScope.btoa = function (chars) {
+ return Buffer.from(chars, 'binary').toString('base64');
+ };
+ })();
+
+ (function checkNodeAtob() {
+ if (globalScope.atob || !isNodeJS()) {
+ return;
+ }
+
+ globalScope.atob = function (input) {
+ return Buffer.from(input, 'base64').toString('binary');
+ };
+ })();
+
+ (function checkChildNodeRemove() {
+ if (!hasDOM) {
+ return;
+ }
+
+ if (typeof Element.prototype.remove !== 'undefined') {
+ return;
+ }
+
+ Element.prototype.remove = function () {
+ if (this.parentNode) {
+ this.parentNode.removeChild(this);
+ }
+ };
+ })();
+
+ (function checkDOMTokenListAddRemove() {
+ if (!hasDOM || isNodeJS()) {
+ return;
+ }
+
+ var div = document.createElement('div');
+ div.classList.add('testOne', 'testTwo');
+
+ if (div.classList.contains('testOne') === true && div.classList.contains('testTwo') === true) {
+ return;
+ }
+
+ var OriginalDOMTokenListAdd = DOMTokenList.prototype.add;
+ var OriginalDOMTokenListRemove = DOMTokenList.prototype.remove;
+
+ DOMTokenList.prototype.add = function () {
+ for (var _len = arguments.length, tokens = new Array(_len), _key = 0; _key < _len; _key++) {
+ tokens[_key] = arguments[_key];
+ }
+
+ for (var _i = 0, _tokens = tokens; _i < _tokens.length; _i++) {
+ var token = _tokens[_i];
+ OriginalDOMTokenListAdd.call(this, token);
+ }
+ };
+
+ DOMTokenList.prototype.remove = function () {
+ for (var _len2 = arguments.length, tokens = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
+ tokens[_key2] = arguments[_key2];
+ }
+
+ for (var _i2 = 0, _tokens2 = tokens; _i2 < _tokens2.length; _i2++) {
+ var token = _tokens2[_i2];
+ OriginalDOMTokenListRemove.call(this, token);
+ }
+ };
+ })();
+
+ (function checkDOMTokenListToggle() {
+ if (!hasDOM || isNodeJS()) {
+ return;
+ }
+
+ var div = document.createElement('div');
+
+ if (div.classList.toggle('test', 0) === false) {
+ return;
+ }
+
+ DOMTokenList.prototype.toggle = function (token) {
+ var force = arguments.length > 1 ? !!arguments[1] : !this.contains(token);
+ return this[force ? 'add' : 'remove'](token), force;
+ };
+ })();
+
+ (function checkStringStartsWith() {
+ if (String.prototype.startsWith) {
+ return;
+ }
+
+ __w_pdfjs_require__(9);
+ })();
+
+ (function checkStringEndsWith() {
+ if (String.prototype.endsWith) {
+ return;
+ }
+
+ __w_pdfjs_require__(40);
+ })();
+
+ (function checkStringIncludes() {
+ if (String.prototype.includes) {
+ return;
+ }
+
+ __w_pdfjs_require__(42);
+ })();
+
+ (function checkArrayIncludes() {
+ if (Array.prototype.includes) {
+ return;
+ }
+
+ __w_pdfjs_require__(44);
+ })();
+
+ (function checkArrayFrom() {
+ if (Array.from) {
+ return;
+ }
+
+ __w_pdfjs_require__(51);
+ })();
+
+ (function checkObjectAssign() {
+ if (Object.assign) {
+ return;
+ }
+
+ __w_pdfjs_require__(74);
+ })();
+
+ (function checkMathLog2() {
+ if (Math.log2) {
+ return;
+ }
+
+ Math.log2 = __w_pdfjs_require__(79);
+ })();
+
+ (function checkNumberIsNaN() {
+ if (Number.isNaN) {
+ return;
+ }
+
+ Number.isNaN = __w_pdfjs_require__(81);
+ })();
+
+ (function checkNumberIsInteger() {
+ if (Number.isInteger) {
+ return;
+ }
+
+ Number.isInteger = __w_pdfjs_require__(83);
+ })();
+
+ (function checkPromise() {
+ if (globalScope.Promise && globalScope.Promise.prototype && globalScope.Promise.prototype["finally"]) {
+ return;
+ }
+
+ globalScope.Promise = __w_pdfjs_require__(86);
+ })();
+
+ (function checkWeakMap() {
+ if (globalScope.WeakMap) {
+ return;
+ }
+
+ globalScope.WeakMap = __w_pdfjs_require__(106);
+ })();
+
+ (function checkWeakSet() {
+ if (globalScope.WeakSet) {
+ return;
+ }
+
+ globalScope.WeakSet = __w_pdfjs_require__(123);
+ })();
+
+ (function checkStringCodePointAt() {
+ if (String.codePointAt) {
+ return;
+ }
+
+ String.codePointAt = __w_pdfjs_require__(127);
+ })();
+
+ (function checkStringFromCodePoint() {
+ if (String.fromCodePoint) {
+ return;
+ }
+
+ String.fromCodePoint = __w_pdfjs_require__(129);
+ })();
+
+ (function checkSymbol() {
+ if (globalScope.Symbol) {
+ return;
+ }
+
+ __w_pdfjs_require__(131);
+ })();
+
+ (function checkStringPadStart() {
+ if (String.prototype.padStart) {
+ return;
+ }
+
+ __w_pdfjs_require__(138);
+ })();
+
+ (function checkStringPadEnd() {
+ if (String.prototype.padEnd) {
+ return;
+ }
+
+ __w_pdfjs_require__(142);
+ })();
+
+ (function checkObjectValues() {
+ if (Object.values) {
+ return;
+ }
+
+ Object.values = __w_pdfjs_require__(144);
+ })();
+}
+
+/***/ }),
+/* 7 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = typeof window !== 'undefined' && window.Math === Math ? window : typeof global !== 'undefined' && global.Math === Math ? global : typeof self !== 'undefined' && self.Math === Math ? self : {};
+
+/***/ }),
+/* 8 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+module.exports = function isNodeJS() {
+ return (typeof process === "undefined" ? "undefined" : _typeof(process)) === 'object' && process + '' === '[object process]' && !process.versions['nw'] && !process.versions['electron'];
+};
+
+/***/ }),
+/* 9 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(10);
+
+module.exports = __w_pdfjs_require__(13).String.startsWith;
+
+/***/ }),
+/* 10 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+var toLength = __w_pdfjs_require__(32);
+
+var context = __w_pdfjs_require__(34);
+
+var STARTS_WITH = 'startsWith';
+var $startsWith = ''[STARTS_WITH];
+$export($export.P + $export.F * __w_pdfjs_require__(39)(STARTS_WITH), 'String', {
+ startsWith: function startsWith(searchString) {
+ var that = context(this, searchString, STARTS_WITH);
+ var index = toLength(Math.min(arguments.length > 1 ? arguments[1] : undefined, that.length));
+ var search = String(searchString);
+ return $startsWith ? $startsWith.call(that, search, index) : that.slice(index, index + search.length) === search;
+ }
+});
+
+/***/ }),
+/* 11 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(12);
+
+var core = __w_pdfjs_require__(13);
+
+var hide = __w_pdfjs_require__(14);
+
+var redefine = __w_pdfjs_require__(24);
+
+var ctx = __w_pdfjs_require__(30);
+
+var PROTOTYPE = 'prototype';
+
+var $export = function $export(type, name, source) {
+ var IS_FORCED = type & $export.F;
+ var IS_GLOBAL = type & $export.G;
+ var IS_STATIC = type & $export.S;
+ var IS_PROTO = type & $export.P;
+ var IS_BIND = type & $export.B;
+ var target = IS_GLOBAL ? global : IS_STATIC ? global[name] || (global[name] = {}) : (global[name] || {})[PROTOTYPE];
+ var exports = IS_GLOBAL ? core : core[name] || (core[name] = {});
+ var expProto = exports[PROTOTYPE] || (exports[PROTOTYPE] = {});
+ var key, own, out, exp;
+ if (IS_GLOBAL) source = name;
+
+ for (key in source) {
+ own = !IS_FORCED && target && target[key] !== undefined;
+ out = (own ? target : source)[key];
+ exp = IS_BIND && own ? ctx(out, global) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out;
+ if (target) redefine(target, key, out, type & $export.U);
+ if (exports[key] != out) hide(exports, key, exp);
+ if (IS_PROTO && expProto[key] != out) expProto[key] = out;
+ }
+};
+
+global.core = core;
+$export.F = 1;
+$export.G = 2;
+$export.S = 4;
+$export.P = 8;
+$export.B = 16;
+$export.W = 32;
+$export.U = 64;
+$export.R = 128;
+module.exports = $export;
+
+/***/ }),
+/* 12 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = module.exports = typeof window != 'undefined' && window.Math == Math ? window : typeof self != 'undefined' && self.Math == Math ? self : Function('return this')();
+if (typeof __g == 'number') __g = global;
+
+/***/ }),
+/* 13 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var core = module.exports = {
+ version: '2.6.9'
+};
+if (typeof __e == 'number') __e = core;
+
+/***/ }),
+/* 14 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var dP = __w_pdfjs_require__(15);
+
+var createDesc = __w_pdfjs_require__(23);
+
+module.exports = __w_pdfjs_require__(19) ? function (object, key, value) {
+ return dP.f(object, key, createDesc(1, value));
+} : function (object, key, value) {
+ object[key] = value;
+ return object;
+};
+
+/***/ }),
+/* 15 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var anObject = __w_pdfjs_require__(16);
+
+var IE8_DOM_DEFINE = __w_pdfjs_require__(18);
+
+var toPrimitive = __w_pdfjs_require__(22);
+
+var dP = Object.defineProperty;
+exports.f = __w_pdfjs_require__(19) ? Object.defineProperty : function defineProperty(O, P, Attributes) {
+ anObject(O);
+ P = toPrimitive(P, true);
+ anObject(Attributes);
+ if (IE8_DOM_DEFINE) try {
+ return dP(O, P, Attributes);
+ } catch (e) {}
+ if ('get' in Attributes || 'set' in Attributes) throw TypeError('Accessors not supported!');
+ if ('value' in Attributes) O[P] = Attributes.value;
+ return O;
+};
+
+/***/ }),
+/* 16 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(17);
+
+module.exports = function (it) {
+ if (!isObject(it)) throw TypeError(it + ' is not an object!');
+ return it;
+};
+
+/***/ }),
+/* 17 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+module.exports = function (it) {
+ return _typeof(it) === 'object' ? it !== null : typeof it === 'function';
+};
+
+/***/ }),
+/* 18 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = !__w_pdfjs_require__(19) && !__w_pdfjs_require__(20)(function () {
+ return Object.defineProperty(__w_pdfjs_require__(21)('div'), 'a', {
+ get: function get() {
+ return 7;
+ }
+ }).a != 7;
+});
+
+/***/ }),
+/* 19 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = !__w_pdfjs_require__(20)(function () {
+ return Object.defineProperty({}, 'a', {
+ get: function get() {
+ return 7;
+ }
+ }).a != 7;
+});
+
+/***/ }),
+/* 20 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (exec) {
+ try {
+ return !!exec();
+ } catch (e) {
+ return true;
+ }
+};
+
+/***/ }),
+/* 21 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(17);
+
+var document = __w_pdfjs_require__(12).document;
+
+var is = isObject(document) && isObject(document.createElement);
+
+module.exports = function (it) {
+ return is ? document.createElement(it) : {};
+};
+
+/***/ }),
+/* 22 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(17);
+
+module.exports = function (it, S) {
+ if (!isObject(it)) return it;
+ var fn, val;
+ if (S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it))) return val;
+ if (typeof (fn = it.valueOf) == 'function' && !isObject(val = fn.call(it))) return val;
+ if (!S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it))) return val;
+ throw TypeError("Can't convert object to primitive value");
+};
+
+/***/ }),
+/* 23 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (bitmap, value) {
+ return {
+ enumerable: !(bitmap & 1),
+ configurable: !(bitmap & 2),
+ writable: !(bitmap & 4),
+ value: value
+ };
+};
+
+/***/ }),
+/* 24 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(12);
+
+var hide = __w_pdfjs_require__(14);
+
+var has = __w_pdfjs_require__(25);
+
+var SRC = __w_pdfjs_require__(26)('src');
+
+var $toString = __w_pdfjs_require__(27);
+
+var TO_STRING = 'toString';
+var TPL = ('' + $toString).split(TO_STRING);
+
+__w_pdfjs_require__(13).inspectSource = function (it) {
+ return $toString.call(it);
+};
+
+(module.exports = function (O, key, val, safe) {
+ var isFunction = typeof val == 'function';
+ if (isFunction) has(val, 'name') || hide(val, 'name', key);
+ if (O[key] === val) return;
+ if (isFunction) has(val, SRC) || hide(val, SRC, O[key] ? '' + O[key] : TPL.join(String(key)));
+
+ if (O === global) {
+ O[key] = val;
+ } else if (!safe) {
+ delete O[key];
+ hide(O, key, val);
+ } else if (O[key]) {
+ O[key] = val;
+ } else {
+ hide(O, key, val);
+ }
+})(Function.prototype, TO_STRING, function toString() {
+ return typeof this == 'function' && this[SRC] || $toString.call(this);
+});
+
+/***/ }),
+/* 25 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var hasOwnProperty = {}.hasOwnProperty;
+
+module.exports = function (it, key) {
+ return hasOwnProperty.call(it, key);
+};
+
+/***/ }),
+/* 26 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var id = 0;
+var px = Math.random();
+
+module.exports = function (key) {
+ return 'Symbol('.concat(key === undefined ? '' : key, ')_', (++id + px).toString(36));
+};
+
+/***/ }),
+/* 27 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = __w_pdfjs_require__(28)('native-function-to-string', Function.toString);
+
+/***/ }),
+/* 28 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var core = __w_pdfjs_require__(13);
+
+var global = __w_pdfjs_require__(12);
+
+var SHARED = '__core-js_shared__';
+var store = global[SHARED] || (global[SHARED] = {});
+(module.exports = function (key, value) {
+ return store[key] || (store[key] = value !== undefined ? value : {});
+})('versions', []).push({
+ version: core.version,
+ mode: __w_pdfjs_require__(29) ? 'pure' : 'global',
+ copyright: '© 2019 Denis Pushkarev (zloirock.ru)'
+});
+
+/***/ }),
+/* 29 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = false;
+
+/***/ }),
+/* 30 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var aFunction = __w_pdfjs_require__(31);
+
+module.exports = function (fn, that, length) {
+ aFunction(fn);
+ if (that === undefined) return fn;
+
+ switch (length) {
+ case 1:
+ return function (a) {
+ return fn.call(that, a);
+ };
+
+ case 2:
+ return function (a, b) {
+ return fn.call(that, a, b);
+ };
+
+ case 3:
+ return function (a, b, c) {
+ return fn.call(that, a, b, c);
+ };
+ }
+
+ return function () {
+ return fn.apply(that, arguments);
+ };
+};
+
+/***/ }),
+/* 31 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (it) {
+ if (typeof it != 'function') throw TypeError(it + ' is not a function!');
+ return it;
+};
+
+/***/ }),
+/* 32 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toInteger = __w_pdfjs_require__(33);
+
+var min = Math.min;
+
+module.exports = function (it) {
+ return it > 0 ? min(toInteger(it), 0x1fffffffffffff) : 0;
+};
+
+/***/ }),
+/* 33 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var ceil = Math.ceil;
+var floor = Math.floor;
+
+module.exports = function (it) {
+ return isNaN(it = +it) ? 0 : (it > 0 ? floor : ceil)(it);
+};
+
+/***/ }),
+/* 34 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isRegExp = __w_pdfjs_require__(35);
+
+var defined = __w_pdfjs_require__(38);
+
+module.exports = function (that, searchString, NAME) {
+ if (isRegExp(searchString)) throw TypeError('String#' + NAME + " doesn't accept regex!");
+ return String(defined(that));
+};
+
+/***/ }),
+/* 35 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(17);
+
+var cof = __w_pdfjs_require__(36);
+
+var MATCH = __w_pdfjs_require__(37)('match');
+
+module.exports = function (it) {
+ var isRegExp;
+ return isObject(it) && ((isRegExp = it[MATCH]) !== undefined ? !!isRegExp : cof(it) == 'RegExp');
+};
+
+/***/ }),
+/* 36 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toString = {}.toString;
+
+module.exports = function (it) {
+ return toString.call(it).slice(8, -1);
+};
+
+/***/ }),
+/* 37 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var store = __w_pdfjs_require__(28)('wks');
+
+var uid = __w_pdfjs_require__(26);
+
+var _Symbol = __w_pdfjs_require__(12).Symbol;
+
+var USE_SYMBOL = typeof _Symbol == 'function';
+
+var $exports = module.exports = function (name) {
+ return store[name] || (store[name] = USE_SYMBOL && _Symbol[name] || (USE_SYMBOL ? _Symbol : uid)('Symbol.' + name));
+};
+
+$exports.store = store;
+
+/***/ }),
+/* 38 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (it) {
+ if (it == undefined) throw TypeError("Can't call method on " + it);
+ return it;
+};
+
+/***/ }),
+/* 39 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var MATCH = __w_pdfjs_require__(37)('match');
+
+module.exports = function (KEY) {
+ var re = /./;
+
+ try {
+ '/./'[KEY](re);
+ } catch (e) {
+ try {
+ re[MATCH] = false;
+ return !'/./'[KEY](re);
+ } catch (f) {}
+ }
+
+ return true;
+};
+
+/***/ }),
+/* 40 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(41);
+
+module.exports = __w_pdfjs_require__(13).String.endsWith;
+
+/***/ }),
+/* 41 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+var toLength = __w_pdfjs_require__(32);
+
+var context = __w_pdfjs_require__(34);
+
+var ENDS_WITH = 'endsWith';
+var $endsWith = ''[ENDS_WITH];
+$export($export.P + $export.F * __w_pdfjs_require__(39)(ENDS_WITH), 'String', {
+ endsWith: function endsWith(searchString) {
+ var that = context(this, searchString, ENDS_WITH);
+ var endPosition = arguments.length > 1 ? arguments[1] : undefined;
+ var len = toLength(that.length);
+ var end = endPosition === undefined ? len : Math.min(toLength(endPosition), len);
+ var search = String(searchString);
+ return $endsWith ? $endsWith.call(that, search, end) : that.slice(end - search.length, end) === search;
+ }
+});
+
+/***/ }),
+/* 42 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(43);
+
+module.exports = __w_pdfjs_require__(13).String.includes;
+
+/***/ }),
+/* 43 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+var context = __w_pdfjs_require__(34);
+
+var INCLUDES = 'includes';
+$export($export.P + $export.F * __w_pdfjs_require__(39)(INCLUDES), 'String', {
+ includes: function includes(searchString) {
+ return !!~context(this, searchString, INCLUDES).indexOf(searchString, arguments.length > 1 ? arguments[1] : undefined);
+ }
+});
+
+/***/ }),
+/* 44 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(45);
+
+module.exports = __w_pdfjs_require__(13).Array.includes;
+
+/***/ }),
+/* 45 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+var $includes = __w_pdfjs_require__(46)(true);
+
+$export($export.P, 'Array', {
+ includes: function includes(el) {
+ return $includes(this, el, arguments.length > 1 ? arguments[1] : undefined);
+ }
+});
+
+__w_pdfjs_require__(50)('includes');
+
+/***/ }),
+/* 46 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toIObject = __w_pdfjs_require__(47);
+
+var toLength = __w_pdfjs_require__(32);
+
+var toAbsoluteIndex = __w_pdfjs_require__(49);
+
+module.exports = function (IS_INCLUDES) {
+ return function ($this, el, fromIndex) {
+ var O = toIObject($this);
+ var length = toLength(O.length);
+ var index = toAbsoluteIndex(fromIndex, length);
+ var value;
+ if (IS_INCLUDES && el != el) while (length > index) {
+ value = O[index++];
+ if (value != value) return true;
+ } else for (; length > index; index++) {
+ if (IS_INCLUDES || index in O) {
+ if (O[index] === el) return IS_INCLUDES || index || 0;
+ }
+ }
+ return !IS_INCLUDES && -1;
+ };
+};
+
+/***/ }),
+/* 47 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var IObject = __w_pdfjs_require__(48);
+
+var defined = __w_pdfjs_require__(38);
+
+module.exports = function (it) {
+ return IObject(defined(it));
+};
+
+/***/ }),
+/* 48 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var cof = __w_pdfjs_require__(36);
+
+module.exports = Object('z').propertyIsEnumerable(0) ? Object : function (it) {
+ return cof(it) == 'String' ? it.split('') : Object(it);
+};
+
+/***/ }),
+/* 49 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toInteger = __w_pdfjs_require__(33);
+
+var max = Math.max;
+var min = Math.min;
+
+module.exports = function (index, length) {
+ index = toInteger(index);
+ return index < 0 ? max(index + length, 0) : min(index, length);
+};
+
+/***/ }),
+/* 50 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var UNSCOPABLES = __w_pdfjs_require__(37)('unscopables');
+
+var ArrayProto = Array.prototype;
+if (ArrayProto[UNSCOPABLES] == undefined) __w_pdfjs_require__(14)(ArrayProto, UNSCOPABLES, {});
+
+module.exports = function (key) {
+ ArrayProto[UNSCOPABLES][key] = true;
+};
+
+/***/ }),
+/* 51 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(52);
+
+__w_pdfjs_require__(67);
+
+module.exports = __w_pdfjs_require__(13).Array.from;
+
+/***/ }),
+/* 52 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $at = __w_pdfjs_require__(53)(true);
+
+__w_pdfjs_require__(54)(String, 'String', function (iterated) {
+ this._t = String(iterated);
+ this._i = 0;
+}, function () {
+ var O = this._t;
+ var index = this._i;
+ var point;
+ if (index >= O.length) return {
+ value: undefined,
+ done: true
+ };
+ point = $at(O, index);
+ this._i += point.length;
+ return {
+ value: point,
+ done: false
+ };
+});
+
+/***/ }),
+/* 53 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toInteger = __w_pdfjs_require__(33);
+
+var defined = __w_pdfjs_require__(38);
+
+module.exports = function (TO_STRING) {
+ return function (that, pos) {
+ var s = String(defined(that));
+ var i = toInteger(pos);
+ var l = s.length;
+ var a, b;
+ if (i < 0 || i >= l) return TO_STRING ? '' : undefined;
+ a = s.charCodeAt(i);
+ return a < 0xd800 || a > 0xdbff || i + 1 === l || (b = s.charCodeAt(i + 1)) < 0xdc00 || b > 0xdfff ? TO_STRING ? s.charAt(i) : a : TO_STRING ? s.slice(i, i + 2) : (a - 0xd800 << 10) + (b - 0xdc00) + 0x10000;
+ };
+};
+
+/***/ }),
+/* 54 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var LIBRARY = __w_pdfjs_require__(29);
+
+var $export = __w_pdfjs_require__(11);
+
+var redefine = __w_pdfjs_require__(24);
+
+var hide = __w_pdfjs_require__(14);
+
+var Iterators = __w_pdfjs_require__(55);
+
+var $iterCreate = __w_pdfjs_require__(56);
+
+var setToStringTag = __w_pdfjs_require__(64);
+
+var getPrototypeOf = __w_pdfjs_require__(65);
+
+var ITERATOR = __w_pdfjs_require__(37)('iterator');
+
+var BUGGY = !([].keys && 'next' in [].keys());
+var FF_ITERATOR = '@@iterator';
+var KEYS = 'keys';
+var VALUES = 'values';
+
+var returnThis = function returnThis() {
+ return this;
+};
+
+module.exports = function (Base, NAME, Constructor, next, DEFAULT, IS_SET, FORCED) {
+ $iterCreate(Constructor, NAME, next);
+
+ var getMethod = function getMethod(kind) {
+ if (!BUGGY && kind in proto) return proto[kind];
+
+ switch (kind) {
+ case KEYS:
+ return function keys() {
+ return new Constructor(this, kind);
+ };
+
+ case VALUES:
+ return function values() {
+ return new Constructor(this, kind);
+ };
+ }
+
+ return function entries() {
+ return new Constructor(this, kind);
+ };
+ };
+
+ var TAG = NAME + ' Iterator';
+ var DEF_VALUES = DEFAULT == VALUES;
+ var VALUES_BUG = false;
+ var proto = Base.prototype;
+ var $native = proto[ITERATOR] || proto[FF_ITERATOR] || DEFAULT && proto[DEFAULT];
+ var $default = $native || getMethod(DEFAULT);
+ var $entries = DEFAULT ? !DEF_VALUES ? $default : getMethod('entries') : undefined;
+ var $anyNative = NAME == 'Array' ? proto.entries || $native : $native;
+ var methods, key, IteratorPrototype;
+
+ if ($anyNative) {
+ IteratorPrototype = getPrototypeOf($anyNative.call(new Base()));
+
+ if (IteratorPrototype !== Object.prototype && IteratorPrototype.next) {
+ setToStringTag(IteratorPrototype, TAG, true);
+ if (!LIBRARY && typeof IteratorPrototype[ITERATOR] != 'function') hide(IteratorPrototype, ITERATOR, returnThis);
+ }
+ }
+
+ if (DEF_VALUES && $native && $native.name !== VALUES) {
+ VALUES_BUG = true;
+
+ $default = function values() {
+ return $native.call(this);
+ };
+ }
+
+ if ((!LIBRARY || FORCED) && (BUGGY || VALUES_BUG || !proto[ITERATOR])) {
+ hide(proto, ITERATOR, $default);
+ }
+
+ Iterators[NAME] = $default;
+ Iterators[TAG] = returnThis;
+
+ if (DEFAULT) {
+ methods = {
+ values: DEF_VALUES ? $default : getMethod(VALUES),
+ keys: IS_SET ? $default : getMethod(KEYS),
+ entries: $entries
+ };
+ if (FORCED) for (key in methods) {
+ if (!(key in proto)) redefine(proto, key, methods[key]);
+ } else $export($export.P + $export.F * (BUGGY || VALUES_BUG), NAME, methods);
+ }
+
+ return methods;
+};
+
+/***/ }),
+/* 55 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = {};
+
+/***/ }),
+/* 56 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var create = __w_pdfjs_require__(57);
+
+var descriptor = __w_pdfjs_require__(23);
+
+var setToStringTag = __w_pdfjs_require__(64);
+
+var IteratorPrototype = {};
+
+__w_pdfjs_require__(14)(IteratorPrototype, __w_pdfjs_require__(37)('iterator'), function () {
+ return this;
+});
+
+module.exports = function (Constructor, NAME, next) {
+ Constructor.prototype = create(IteratorPrototype, {
+ next: descriptor(1, next)
+ });
+ setToStringTag(Constructor, NAME + ' Iterator');
+};
+
+/***/ }),
+/* 57 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var anObject = __w_pdfjs_require__(16);
+
+var dPs = __w_pdfjs_require__(58);
+
+var enumBugKeys = __w_pdfjs_require__(62);
+
+var IE_PROTO = __w_pdfjs_require__(61)('IE_PROTO');
+
+var Empty = function Empty() {};
+
+var PROTOTYPE = 'prototype';
+
+var _createDict = function createDict() {
+ var iframe = __w_pdfjs_require__(21)('iframe');
+
+ var i = enumBugKeys.length;
+ var lt = '<';
+ var gt = '>';
+ var iframeDocument;
+ iframe.style.display = 'none';
+
+ __w_pdfjs_require__(63).appendChild(iframe);
+
+ iframe.src = 'javascript:';
+ iframeDocument = iframe.contentWindow.document;
+ iframeDocument.open();
+ iframeDocument.write(lt + 'script' + gt + 'document.F=Object' + lt + '/script' + gt);
+ iframeDocument.close();
+ _createDict = iframeDocument.F;
+
+ while (i--) {
+ delete _createDict[PROTOTYPE][enumBugKeys[i]];
+ }
+
+ return _createDict();
+};
+
+module.exports = Object.create || function create(O, Properties) {
+ var result;
+
+ if (O !== null) {
+ Empty[PROTOTYPE] = anObject(O);
+ result = new Empty();
+ Empty[PROTOTYPE] = null;
+ result[IE_PROTO] = O;
+ } else result = _createDict();
+
+ return Properties === undefined ? result : dPs(result, Properties);
+};
+
+/***/ }),
+/* 58 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var dP = __w_pdfjs_require__(15);
+
+var anObject = __w_pdfjs_require__(16);
+
+var getKeys = __w_pdfjs_require__(59);
+
+module.exports = __w_pdfjs_require__(19) ? Object.defineProperties : function defineProperties(O, Properties) {
+ anObject(O);
+ var keys = getKeys(Properties);
+ var length = keys.length;
+ var i = 0;
+ var P;
+
+ while (length > i) {
+ dP.f(O, P = keys[i++], Properties[P]);
+ }
+
+ return O;
+};
+
+/***/ }),
+/* 59 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $keys = __w_pdfjs_require__(60);
+
+var enumBugKeys = __w_pdfjs_require__(62);
+
+module.exports = Object.keys || function keys(O) {
+ return $keys(O, enumBugKeys);
+};
+
+/***/ }),
+/* 60 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var has = __w_pdfjs_require__(25);
+
+var toIObject = __w_pdfjs_require__(47);
+
+var arrayIndexOf = __w_pdfjs_require__(46)(false);
+
+var IE_PROTO = __w_pdfjs_require__(61)('IE_PROTO');
+
+module.exports = function (object, names) {
+ var O = toIObject(object);
+ var i = 0;
+ var result = [];
+ var key;
+
+ for (key in O) {
+ if (key != IE_PROTO) has(O, key) && result.push(key);
+ }
+
+ while (names.length > i) {
+ if (has(O, key = names[i++])) {
+ ~arrayIndexOf(result, key) || result.push(key);
+ }
+ }
+
+ return result;
+};
+
+/***/ }),
+/* 61 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var shared = __w_pdfjs_require__(28)('keys');
+
+var uid = __w_pdfjs_require__(26);
+
+module.exports = function (key) {
+ return shared[key] || (shared[key] = uid(key));
+};
+
+/***/ }),
+/* 62 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = 'constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf'.split(',');
+
+/***/ }),
+/* 63 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var document = __w_pdfjs_require__(12).document;
+
+module.exports = document && document.documentElement;
+
+/***/ }),
+/* 64 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var def = __w_pdfjs_require__(15).f;
+
+var has = __w_pdfjs_require__(25);
+
+var TAG = __w_pdfjs_require__(37)('toStringTag');
+
+module.exports = function (it, tag, stat) {
+ if (it && !has(it = stat ? it : it.prototype, TAG)) def(it, TAG, {
+ configurable: true,
+ value: tag
+ });
+};
+
+/***/ }),
+/* 65 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var has = __w_pdfjs_require__(25);
+
+var toObject = __w_pdfjs_require__(66);
+
+var IE_PROTO = __w_pdfjs_require__(61)('IE_PROTO');
+
+var ObjectProto = Object.prototype;
+
+module.exports = Object.getPrototypeOf || function (O) {
+ O = toObject(O);
+ if (has(O, IE_PROTO)) return O[IE_PROTO];
+
+ if (typeof O.constructor == 'function' && O instanceof O.constructor) {
+ return O.constructor.prototype;
+ }
+
+ return O instanceof Object ? ObjectProto : null;
+};
+
+/***/ }),
+/* 66 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var defined = __w_pdfjs_require__(38);
+
+module.exports = function (it) {
+ return Object(defined(it));
+};
+
+/***/ }),
+/* 67 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var ctx = __w_pdfjs_require__(30);
+
+var $export = __w_pdfjs_require__(11);
+
+var toObject = __w_pdfjs_require__(66);
+
+var call = __w_pdfjs_require__(68);
+
+var isArrayIter = __w_pdfjs_require__(69);
+
+var toLength = __w_pdfjs_require__(32);
+
+var createProperty = __w_pdfjs_require__(70);
+
+var getIterFn = __w_pdfjs_require__(71);
+
+$export($export.S + $export.F * !__w_pdfjs_require__(73)(function (iter) {
+ Array.from(iter);
+}), 'Array', {
+ from: function from(arrayLike) {
+ var O = toObject(arrayLike);
+ var C = typeof this == 'function' ? this : Array;
+ var aLen = arguments.length;
+ var mapfn = aLen > 1 ? arguments[1] : undefined;
+ var mapping = mapfn !== undefined;
+ var index = 0;
+ var iterFn = getIterFn(O);
+ var length, result, step, iterator;
+ if (mapping) mapfn = ctx(mapfn, aLen > 2 ? arguments[2] : undefined, 2);
+
+ if (iterFn != undefined && !(C == Array && isArrayIter(iterFn))) {
+ for (iterator = iterFn.call(O), result = new C(); !(step = iterator.next()).done; index++) {
+ createProperty(result, index, mapping ? call(iterator, mapfn, [step.value, index], true) : step.value);
+ }
+ } else {
+ length = toLength(O.length);
+
+ for (result = new C(length); length > index; index++) {
+ createProperty(result, index, mapping ? mapfn(O[index], index) : O[index]);
+ }
+ }
+
+ result.length = index;
+ return result;
+ }
+});
+
+/***/ }),
+/* 68 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var anObject = __w_pdfjs_require__(16);
+
+module.exports = function (iterator, fn, value, entries) {
+ try {
+ return entries ? fn(anObject(value)[0], value[1]) : fn(value);
+ } catch (e) {
+ var ret = iterator['return'];
+ if (ret !== undefined) anObject(ret.call(iterator));
+ throw e;
+ }
+};
+
+/***/ }),
+/* 69 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var Iterators = __w_pdfjs_require__(55);
+
+var ITERATOR = __w_pdfjs_require__(37)('iterator');
+
+var ArrayProto = Array.prototype;
+
+module.exports = function (it) {
+ return it !== undefined && (Iterators.Array === it || ArrayProto[ITERATOR] === it);
+};
+
+/***/ }),
+/* 70 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $defineProperty = __w_pdfjs_require__(15);
+
+var createDesc = __w_pdfjs_require__(23);
+
+module.exports = function (object, index, value) {
+ if (index in object) $defineProperty.f(object, index, createDesc(0, value));else object[index] = value;
+};
+
+/***/ }),
+/* 71 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var classof = __w_pdfjs_require__(72);
+
+var ITERATOR = __w_pdfjs_require__(37)('iterator');
+
+var Iterators = __w_pdfjs_require__(55);
+
+module.exports = __w_pdfjs_require__(13).getIteratorMethod = function (it) {
+ if (it != undefined) return it[ITERATOR] || it['@@iterator'] || Iterators[classof(it)];
+};
+
+/***/ }),
+/* 72 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var cof = __w_pdfjs_require__(36);
+
+var TAG = __w_pdfjs_require__(37)('toStringTag');
+
+var ARG = cof(function () {
+ return arguments;
+}()) == 'Arguments';
+
+var tryGet = function tryGet(it, key) {
+ try {
+ return it[key];
+ } catch (e) {}
+};
+
+module.exports = function (it) {
+ var O, T, B;
+ return it === undefined ? 'Undefined' : it === null ? 'Null' : typeof (T = tryGet(O = Object(it), TAG)) == 'string' ? T : ARG ? cof(O) : (B = cof(O)) == 'Object' && typeof O.callee == 'function' ? 'Arguments' : B;
+};
+
+/***/ }),
+/* 73 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var ITERATOR = __w_pdfjs_require__(37)('iterator');
+
+var SAFE_CLOSING = false;
+
+try {
+ var riter = [7][ITERATOR]();
+
+ riter['return'] = function () {
+ SAFE_CLOSING = true;
+ };
+
+ Array.from(riter, function () {
+ throw 2;
+ });
+} catch (e) {}
+
+module.exports = function (exec, skipClosing) {
+ if (!skipClosing && !SAFE_CLOSING) return false;
+ var safe = false;
+
+ try {
+ var arr = [7];
+ var iter = arr[ITERATOR]();
+
+ iter.next = function () {
+ return {
+ done: safe = true
+ };
+ };
+
+ arr[ITERATOR] = function () {
+ return iter;
+ };
+
+ exec(arr);
+ } catch (e) {}
+
+ return safe;
+};
+
+/***/ }),
+/* 74 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(75);
+
+module.exports = __w_pdfjs_require__(13).Object.assign;
+
+/***/ }),
+/* 75 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+$export($export.S + $export.F, 'Object', {
+ assign: __w_pdfjs_require__(76)
+});
+
+/***/ }),
+/* 76 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var DESCRIPTORS = __w_pdfjs_require__(19);
+
+var getKeys = __w_pdfjs_require__(59);
+
+var gOPS = __w_pdfjs_require__(77);
+
+var pIE = __w_pdfjs_require__(78);
+
+var toObject = __w_pdfjs_require__(66);
+
+var IObject = __w_pdfjs_require__(48);
+
+var $assign = Object.assign;
+module.exports = !$assign || __w_pdfjs_require__(20)(function () {
+ var A = {};
+ var B = {};
+ var S = Symbol();
+ var K = 'abcdefghijklmnopqrst';
+ A[S] = 7;
+ K.split('').forEach(function (k) {
+ B[k] = k;
+ });
+ return $assign({}, A)[S] != 7 || Object.keys($assign({}, B)).join('') != K;
+}) ? function assign(target, source) {
+ var T = toObject(target);
+ var aLen = arguments.length;
+ var index = 1;
+ var getSymbols = gOPS.f;
+ var isEnum = pIE.f;
+
+ while (aLen > index) {
+ var S = IObject(arguments[index++]);
+ var keys = getSymbols ? getKeys(S).concat(getSymbols(S)) : getKeys(S);
+ var length = keys.length;
+ var j = 0;
+ var key;
+
+ while (length > j) {
+ key = keys[j++];
+ if (!DESCRIPTORS || isEnum.call(S, key)) T[key] = S[key];
+ }
+ }
+
+ return T;
+} : $assign;
+
+/***/ }),
+/* 77 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+exports.f = Object.getOwnPropertySymbols;
+
+/***/ }),
+/* 78 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+exports.f = {}.propertyIsEnumerable;
+
+/***/ }),
+/* 79 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(80);
+
+module.exports = __w_pdfjs_require__(13).Math.log2;
+
+/***/ }),
+/* 80 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+$export($export.S, 'Math', {
+ log2: function log2(x) {
+ return Math.log(x) / Math.LN2;
+ }
+});
+
+/***/ }),
+/* 81 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(82);
+
+module.exports = __w_pdfjs_require__(13).Number.isNaN;
+
+/***/ }),
+/* 82 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+$export($export.S, 'Number', {
+ isNaN: function isNaN(number) {
+ return number != number;
+ }
+});
+
+/***/ }),
+/* 83 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(84);
+
+module.exports = __w_pdfjs_require__(13).Number.isInteger;
+
+/***/ }),
+/* 84 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+$export($export.S, 'Number', {
+ isInteger: __w_pdfjs_require__(85)
+});
+
+/***/ }),
+/* 85 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(17);
+
+var floor = Math.floor;
+
+module.exports = function isInteger(it) {
+ return !isObject(it) && isFinite(it) && floor(it) === it;
+};
+
+/***/ }),
+/* 86 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(87);
+
+__w_pdfjs_require__(52);
+
+__w_pdfjs_require__(88);
+
+__w_pdfjs_require__(91);
+
+__w_pdfjs_require__(104);
+
+__w_pdfjs_require__(105);
+
+module.exports = __w_pdfjs_require__(13).Promise;
+
+/***/ }),
+/* 87 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var classof = __w_pdfjs_require__(72);
+
+var test = {};
+test[__w_pdfjs_require__(37)('toStringTag')] = 'z';
+
+if (test + '' != '[object z]') {
+ __w_pdfjs_require__(24)(Object.prototype, 'toString', function toString() {
+ return '[object ' + classof(this) + ']';
+ }, true);
+}
+
+/***/ }),
+/* 88 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $iterators = __w_pdfjs_require__(89);
+
+var getKeys = __w_pdfjs_require__(59);
+
+var redefine = __w_pdfjs_require__(24);
+
+var global = __w_pdfjs_require__(12);
+
+var hide = __w_pdfjs_require__(14);
+
+var Iterators = __w_pdfjs_require__(55);
+
+var wks = __w_pdfjs_require__(37);
+
+var ITERATOR = wks('iterator');
+var TO_STRING_TAG = wks('toStringTag');
+var ArrayValues = Iterators.Array;
+var DOMIterables = {
+ CSSRuleList: true,
+ CSSStyleDeclaration: false,
+ CSSValueList: false,
+ ClientRectList: false,
+ DOMRectList: false,
+ DOMStringList: false,
+ DOMTokenList: true,
+ DataTransferItemList: false,
+ FileList: false,
+ HTMLAllCollection: false,
+ HTMLCollection: false,
+ HTMLFormElement: false,
+ HTMLSelectElement: false,
+ MediaList: true,
+ MimeTypeArray: false,
+ NamedNodeMap: false,
+ NodeList: true,
+ PaintRequestList: false,
+ Plugin: false,
+ PluginArray: false,
+ SVGLengthList: false,
+ SVGNumberList: false,
+ SVGPathSegList: false,
+ SVGPointList: false,
+ SVGStringList: false,
+ SVGTransformList: false,
+ SourceBufferList: false,
+ StyleSheetList: true,
+ TextTrackCueList: false,
+ TextTrackList: false,
+ TouchList: false
+};
+
+for (var collections = getKeys(DOMIterables), i = 0; i < collections.length; i++) {
+ var NAME = collections[i];
+ var explicit = DOMIterables[NAME];
+ var Collection = global[NAME];
+ var proto = Collection && Collection.prototype;
+ var key;
+
+ if (proto) {
+ if (!proto[ITERATOR]) hide(proto, ITERATOR, ArrayValues);
+ if (!proto[TO_STRING_TAG]) hide(proto, TO_STRING_TAG, NAME);
+ Iterators[NAME] = ArrayValues;
+ if (explicit) for (key in $iterators) {
+ if (!proto[key]) redefine(proto, key, $iterators[key], true);
+ }
+ }
+}
+
+/***/ }),
+/* 89 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var addToUnscopables = __w_pdfjs_require__(50);
+
+var step = __w_pdfjs_require__(90);
+
+var Iterators = __w_pdfjs_require__(55);
+
+var toIObject = __w_pdfjs_require__(47);
+
+module.exports = __w_pdfjs_require__(54)(Array, 'Array', function (iterated, kind) {
+ this._t = toIObject(iterated);
+ this._i = 0;
+ this._k = kind;
+}, function () {
+ var O = this._t;
+ var kind = this._k;
+ var index = this._i++;
+
+ if (!O || index >= O.length) {
+ this._t = undefined;
+ return step(1);
+ }
+
+ if (kind == 'keys') return step(0, index);
+ if (kind == 'values') return step(0, O[index]);
+ return step(0, [index, O[index]]);
+}, 'values');
+Iterators.Arguments = Iterators.Array;
+addToUnscopables('keys');
+addToUnscopables('values');
+addToUnscopables('entries');
+
+/***/ }),
+/* 90 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (done, value) {
+ return {
+ value: value,
+ done: !!done
+ };
+};
+
+/***/ }),
+/* 91 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var LIBRARY = __w_pdfjs_require__(29);
+
+var global = __w_pdfjs_require__(12);
+
+var ctx = __w_pdfjs_require__(30);
+
+var classof = __w_pdfjs_require__(72);
+
+var $export = __w_pdfjs_require__(11);
+
+var isObject = __w_pdfjs_require__(17);
+
+var aFunction = __w_pdfjs_require__(31);
+
+var anInstance = __w_pdfjs_require__(92);
+
+var forOf = __w_pdfjs_require__(93);
+
+var speciesConstructor = __w_pdfjs_require__(94);
+
+var task = __w_pdfjs_require__(95).set;
+
+var microtask = __w_pdfjs_require__(97)();
+
+var newPromiseCapabilityModule = __w_pdfjs_require__(98);
+
+var perform = __w_pdfjs_require__(99);
+
+var userAgent = __w_pdfjs_require__(100);
+
+var promiseResolve = __w_pdfjs_require__(101);
+
+var PROMISE = 'Promise';
+var TypeError = global.TypeError;
+var process = global.process;
+var versions = process && process.versions;
+var v8 = versions && versions.v8 || '';
+var $Promise = global[PROMISE];
+var isNode = classof(process) == 'process';
+
+var empty = function empty() {};
+
+var Internal, newGenericPromiseCapability, OwnPromiseCapability, Wrapper;
+var newPromiseCapability = newGenericPromiseCapability = newPromiseCapabilityModule.f;
+var USE_NATIVE = !!function () {
+ try {
+ var promise = $Promise.resolve(1);
+
+ var FakePromise = (promise.constructor = {})[__w_pdfjs_require__(37)('species')] = function (exec) {
+ exec(empty, empty);
+ };
+
+ return (isNode || typeof PromiseRejectionEvent == 'function') && promise.then(empty) instanceof FakePromise && v8.indexOf('6.6') !== 0 && userAgent.indexOf('Chrome/66') === -1;
+ } catch (e) {}
+}();
+
+var isThenable = function isThenable(it) {
+ var then;
+ return isObject(it) && typeof (then = it.then) == 'function' ? then : false;
+};
+
+var notify = function notify(promise, isReject) {
+ if (promise._n) return;
+ promise._n = true;
+ var chain = promise._c;
+ microtask(function () {
+ var value = promise._v;
+ var ok = promise._s == 1;
+ var i = 0;
+
+ var run = function run(reaction) {
+ var handler = ok ? reaction.ok : reaction.fail;
+ var resolve = reaction.resolve;
+ var reject = reaction.reject;
+ var domain = reaction.domain;
+ var result, then, exited;
+
+ try {
+ if (handler) {
+ if (!ok) {
+ if (promise._h == 2) onHandleUnhandled(promise);
+ promise._h = 1;
+ }
+
+ if (handler === true) result = value;else {
+ if (domain) domain.enter();
+ result = handler(value);
+
+ if (domain) {
+ domain.exit();
+ exited = true;
+ }
+ }
+
+ if (result === reaction.promise) {
+ reject(TypeError('Promise-chain cycle'));
+ } else if (then = isThenable(result)) {
+ then.call(result, resolve, reject);
+ } else resolve(result);
+ } else reject(value);
+ } catch (e) {
+ if (domain && !exited) domain.exit();
+ reject(e);
+ }
+ };
+
+ while (chain.length > i) {
+ run(chain[i++]);
+ }
+
+ promise._c = [];
+ promise._n = false;
+ if (isReject && !promise._h) onUnhandled(promise);
+ });
+};
+
+var onUnhandled = function onUnhandled(promise) {
+ task.call(global, function () {
+ var value = promise._v;
+ var unhandled = isUnhandled(promise);
+ var result, handler, console;
+
+ if (unhandled) {
+ result = perform(function () {
+ if (isNode) {
+ process.emit('unhandledRejection', value, promise);
+ } else if (handler = global.onunhandledrejection) {
+ handler({
+ promise: promise,
+ reason: value
+ });
+ } else if ((console = global.console) && console.error) {
+ console.error('Unhandled promise rejection', value);
+ }
+ });
+ promise._h = isNode || isUnhandled(promise) ? 2 : 1;
+ }
+
+ promise._a = undefined;
+ if (unhandled && result.e) throw result.v;
+ });
+};
+
+var isUnhandled = function isUnhandled(promise) {
+ return promise._h !== 1 && (promise._a || promise._c).length === 0;
+};
+
+var onHandleUnhandled = function onHandleUnhandled(promise) {
+ task.call(global, function () {
+ var handler;
+
+ if (isNode) {
+ process.emit('rejectionHandled', promise);
+ } else if (handler = global.onrejectionhandled) {
+ handler({
+ promise: promise,
+ reason: promise._v
+ });
+ }
+ });
+};
+
+var $reject = function $reject(value) {
+ var promise = this;
+ if (promise._d) return;
+ promise._d = true;
+ promise = promise._w || promise;
+ promise._v = value;
+ promise._s = 2;
+ if (!promise._a) promise._a = promise._c.slice();
+ notify(promise, true);
+};
+
+var $resolve = function $resolve(value) {
+ var promise = this;
+ var then;
+ if (promise._d) return;
+ promise._d = true;
+ promise = promise._w || promise;
+
+ try {
+ if (promise === value) throw TypeError("Promise can't be resolved itself");
+
+ if (then = isThenable(value)) {
+ microtask(function () {
+ var wrapper = {
+ _w: promise,
+ _d: false
+ };
+
+ try {
+ then.call(value, ctx($resolve, wrapper, 1), ctx($reject, wrapper, 1));
+ } catch (e) {
+ $reject.call(wrapper, e);
+ }
+ });
+ } else {
+ promise._v = value;
+ promise._s = 1;
+ notify(promise, false);
+ }
+ } catch (e) {
+ $reject.call({
+ _w: promise,
+ _d: false
+ }, e);
+ }
+};
+
+if (!USE_NATIVE) {
+ $Promise = function Promise(executor) {
+ anInstance(this, $Promise, PROMISE, '_h');
+ aFunction(executor);
+ Internal.call(this);
+
+ try {
+ executor(ctx($resolve, this, 1), ctx($reject, this, 1));
+ } catch (err) {
+ $reject.call(this, err);
+ }
+ };
+
+ Internal = function Promise(executor) {
+ this._c = [];
+ this._a = undefined;
+ this._s = 0;
+ this._d = false;
+ this._v = undefined;
+ this._h = 0;
+ this._n = false;
+ };
+
+ Internal.prototype = __w_pdfjs_require__(102)($Promise.prototype, {
+ then: function then(onFulfilled, onRejected) {
+ var reaction = newPromiseCapability(speciesConstructor(this, $Promise));
+ reaction.ok = typeof onFulfilled == 'function' ? onFulfilled : true;
+ reaction.fail = typeof onRejected == 'function' && onRejected;
+ reaction.domain = isNode ? process.domain : undefined;
+
+ this._c.push(reaction);
+
+ if (this._a) this._a.push(reaction);
+ if (this._s) notify(this, false);
+ return reaction.promise;
+ },
+ 'catch': function _catch(onRejected) {
+ return this.then(undefined, onRejected);
+ }
+ });
+
+ OwnPromiseCapability = function OwnPromiseCapability() {
+ var promise = new Internal();
+ this.promise = promise;
+ this.resolve = ctx($resolve, promise, 1);
+ this.reject = ctx($reject, promise, 1);
+ };
+
+ newPromiseCapabilityModule.f = newPromiseCapability = function newPromiseCapability(C) {
+ return C === $Promise || C === Wrapper ? new OwnPromiseCapability(C) : newGenericPromiseCapability(C);
+ };
+}
+
+$export($export.G + $export.W + $export.F * !USE_NATIVE, {
+ Promise: $Promise
+});
+
+__w_pdfjs_require__(64)($Promise, PROMISE);
+
+__w_pdfjs_require__(103)(PROMISE);
+
+Wrapper = __w_pdfjs_require__(13)[PROMISE];
+$export($export.S + $export.F * !USE_NATIVE, PROMISE, {
+ reject: function reject(r) {
+ var capability = newPromiseCapability(this);
+ var $$reject = capability.reject;
+ $$reject(r);
+ return capability.promise;
+ }
+});
+$export($export.S + $export.F * (LIBRARY || !USE_NATIVE), PROMISE, {
+ resolve: function resolve(x) {
+ return promiseResolve(LIBRARY && this === Wrapper ? $Promise : this, x);
+ }
+});
+$export($export.S + $export.F * !(USE_NATIVE && __w_pdfjs_require__(73)(function (iter) {
+ $Promise.all(iter)['catch'](empty);
+})), PROMISE, {
+ all: function all(iterable) {
+ var C = this;
+ var capability = newPromiseCapability(C);
+ var resolve = capability.resolve;
+ var reject = capability.reject;
+ var result = perform(function () {
+ var values = [];
+ var index = 0;
+ var remaining = 1;
+ forOf(iterable, false, function (promise) {
+ var $index = index++;
+ var alreadyCalled = false;
+ values.push(undefined);
+ remaining++;
+ C.resolve(promise).then(function (value) {
+ if (alreadyCalled) return;
+ alreadyCalled = true;
+ values[$index] = value;
+ --remaining || resolve(values);
+ }, reject);
+ });
+ --remaining || resolve(values);
+ });
+ if (result.e) reject(result.v);
+ return capability.promise;
+ },
+ race: function race(iterable) {
+ var C = this;
+ var capability = newPromiseCapability(C);
+ var reject = capability.reject;
+ var result = perform(function () {
+ forOf(iterable, false, function (promise) {
+ C.resolve(promise).then(capability.resolve, reject);
+ });
+ });
+ if (result.e) reject(result.v);
+ return capability.promise;
+ }
+});
+
+/***/ }),
+/* 92 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (it, Constructor, name, forbiddenField) {
+ if (!(it instanceof Constructor) || forbiddenField !== undefined && forbiddenField in it) {
+ throw TypeError(name + ': incorrect invocation!');
+ }
+
+ return it;
+};
+
+/***/ }),
+/* 93 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var ctx = __w_pdfjs_require__(30);
+
+var call = __w_pdfjs_require__(68);
+
+var isArrayIter = __w_pdfjs_require__(69);
+
+var anObject = __w_pdfjs_require__(16);
+
+var toLength = __w_pdfjs_require__(32);
+
+var getIterFn = __w_pdfjs_require__(71);
+
+var BREAK = {};
+var RETURN = {};
+
+var _exports = module.exports = function (iterable, entries, fn, that, ITERATOR) {
+ var iterFn = ITERATOR ? function () {
+ return iterable;
+ } : getIterFn(iterable);
+ var f = ctx(fn, that, entries ? 2 : 1);
+ var index = 0;
+ var length, step, iterator, result;
+ if (typeof iterFn != 'function') throw TypeError(iterable + ' is not iterable!');
+ if (isArrayIter(iterFn)) for (length = toLength(iterable.length); length > index; index++) {
+ result = entries ? f(anObject(step = iterable[index])[0], step[1]) : f(iterable[index]);
+ if (result === BREAK || result === RETURN) return result;
+ } else for (iterator = iterFn.call(iterable); !(step = iterator.next()).done;) {
+ result = call(iterator, f, step.value, entries);
+ if (result === BREAK || result === RETURN) return result;
+ }
+};
+
+_exports.BREAK = BREAK;
+_exports.RETURN = RETURN;
+
+/***/ }),
+/* 94 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var anObject = __w_pdfjs_require__(16);
+
+var aFunction = __w_pdfjs_require__(31);
+
+var SPECIES = __w_pdfjs_require__(37)('species');
+
+module.exports = function (O, D) {
+ var C = anObject(O).constructor;
+ var S;
+ return C === undefined || (S = anObject(C)[SPECIES]) == undefined ? D : aFunction(S);
+};
+
+/***/ }),
+/* 95 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var ctx = __w_pdfjs_require__(30);
+
+var invoke = __w_pdfjs_require__(96);
+
+var html = __w_pdfjs_require__(63);
+
+var cel = __w_pdfjs_require__(21);
+
+var global = __w_pdfjs_require__(12);
+
+var process = global.process;
+var setTask = global.setImmediate;
+var clearTask = global.clearImmediate;
+var MessageChannel = global.MessageChannel;
+var Dispatch = global.Dispatch;
+var counter = 0;
+var queue = {};
+var ONREADYSTATECHANGE = 'onreadystatechange';
+var defer, channel, port;
+
+var run = function run() {
+ var id = +this;
+
+ if (queue.hasOwnProperty(id)) {
+ var fn = queue[id];
+ delete queue[id];
+ fn();
+ }
+};
+
+var listener = function listener(event) {
+ run.call(event.data);
+};
+
+if (!setTask || !clearTask) {
+ setTask = function setImmediate(fn) {
+ var args = [];
+ var i = 1;
+
+ while (arguments.length > i) {
+ args.push(arguments[i++]);
+ }
+
+ queue[++counter] = function () {
+ invoke(typeof fn == 'function' ? fn : Function(fn), args);
+ };
+
+ defer(counter);
+ return counter;
+ };
+
+ clearTask = function clearImmediate(id) {
+ delete queue[id];
+ };
+
+ if (__w_pdfjs_require__(36)(process) == 'process') {
+ defer = function defer(id) {
+ process.nextTick(ctx(run, id, 1));
+ };
+ } else if (Dispatch && Dispatch.now) {
+ defer = function defer(id) {
+ Dispatch.now(ctx(run, id, 1));
+ };
+ } else if (MessageChannel) {
+ channel = new MessageChannel();
+ port = channel.port2;
+ channel.port1.onmessage = listener;
+ defer = ctx(port.postMessage, port, 1);
+ } else if (global.addEventListener && typeof postMessage == 'function' && !global.importScripts) {
+ defer = function defer(id) {
+ global.postMessage(id + '', '*');
+ };
+
+ global.addEventListener('message', listener, false);
+ } else if (ONREADYSTATECHANGE in cel('script')) {
+ defer = function defer(id) {
+ html.appendChild(cel('script'))[ONREADYSTATECHANGE] = function () {
+ html.removeChild(this);
+ run.call(id);
+ };
+ };
+ } else {
+ defer = function defer(id) {
+ setTimeout(ctx(run, id, 1), 0);
+ };
+ }
+}
+
+module.exports = {
+ set: setTask,
+ clear: clearTask
+};
+
+/***/ }),
+/* 96 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (fn, args, that) {
+ var un = that === undefined;
+
+ switch (args.length) {
+ case 0:
+ return un ? fn() : fn.call(that);
+
+ case 1:
+ return un ? fn(args[0]) : fn.call(that, args[0]);
+
+ case 2:
+ return un ? fn(args[0], args[1]) : fn.call(that, args[0], args[1]);
+
+ case 3:
+ return un ? fn(args[0], args[1], args[2]) : fn.call(that, args[0], args[1], args[2]);
+
+ case 4:
+ return un ? fn(args[0], args[1], args[2], args[3]) : fn.call(that, args[0], args[1], args[2], args[3]);
+ }
+
+ return fn.apply(that, args);
+};
+
+/***/ }),
+/* 97 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(12);
+
+var macrotask = __w_pdfjs_require__(95).set;
+
+var Observer = global.MutationObserver || global.WebKitMutationObserver;
+var process = global.process;
+var Promise = global.Promise;
+var isNode = __w_pdfjs_require__(36)(process) == 'process';
+
+module.exports = function () {
+ var head, last, notify;
+
+ var flush = function flush() {
+ var parent, fn;
+ if (isNode && (parent = process.domain)) parent.exit();
+
+ while (head) {
+ fn = head.fn;
+ head = head.next;
+
+ try {
+ fn();
+ } catch (e) {
+ if (head) notify();else last = undefined;
+ throw e;
+ }
+ }
+
+ last = undefined;
+ if (parent) parent.enter();
+ };
+
+ if (isNode) {
+ notify = function notify() {
+ process.nextTick(flush);
+ };
+ } else if (Observer && !(global.navigator && global.navigator.standalone)) {
+ var toggle = true;
+ var node = document.createTextNode('');
+ new Observer(flush).observe(node, {
+ characterData: true
+ });
+
+ notify = function notify() {
+ node.data = toggle = !toggle;
+ };
+ } else if (Promise && Promise.resolve) {
+ var promise = Promise.resolve(undefined);
+
+ notify = function notify() {
+ promise.then(flush);
+ };
+ } else {
+ notify = function notify() {
+ macrotask.call(global, flush);
+ };
+ }
+
+ return function (fn) {
+ var task = {
+ fn: fn,
+ next: undefined
+ };
+ if (last) last.next = task;
+
+ if (!head) {
+ head = task;
+ notify();
+ }
+
+ last = task;
+ };
+};
+
+/***/ }),
+/* 98 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var aFunction = __w_pdfjs_require__(31);
+
+function PromiseCapability(C) {
+ var resolve, reject;
+ this.promise = new C(function ($$resolve, $$reject) {
+ if (resolve !== undefined || reject !== undefined) throw TypeError('Bad Promise constructor');
+ resolve = $$resolve;
+ reject = $$reject;
+ });
+ this.resolve = aFunction(resolve);
+ this.reject = aFunction(reject);
+}
+
+module.exports.f = function (C) {
+ return new PromiseCapability(C);
+};
+
+/***/ }),
+/* 99 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+module.exports = function (exec) {
+ try {
+ return {
+ e: false,
+ v: exec()
+ };
+ } catch (e) {
+ return {
+ e: true,
+ v: e
+ };
+ }
+};
+
+/***/ }),
+/* 100 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(12);
+
+var navigator = global.navigator;
+module.exports = navigator && navigator.userAgent || '';
+
+/***/ }),
+/* 101 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var anObject = __w_pdfjs_require__(16);
+
+var isObject = __w_pdfjs_require__(17);
+
+var newPromiseCapability = __w_pdfjs_require__(98);
+
+module.exports = function (C, x) {
+ anObject(C);
+ if (isObject(x) && x.constructor === C) return x;
+ var promiseCapability = newPromiseCapability.f(C);
+ var resolve = promiseCapability.resolve;
+ resolve(x);
+ return promiseCapability.promise;
+};
+
+/***/ }),
+/* 102 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var redefine = __w_pdfjs_require__(24);
+
+module.exports = function (target, src, safe) {
+ for (var key in src) {
+ redefine(target, key, src[key], safe);
+ }
+
+ return target;
+};
+
+/***/ }),
+/* 103 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(12);
+
+var dP = __w_pdfjs_require__(15);
+
+var DESCRIPTORS = __w_pdfjs_require__(19);
+
+var SPECIES = __w_pdfjs_require__(37)('species');
+
+module.exports = function (KEY) {
+ var C = global[KEY];
+ if (DESCRIPTORS && C && !C[SPECIES]) dP.f(C, SPECIES, {
+ configurable: true,
+ get: function get() {
+ return this;
+ }
+ });
+};
+
+/***/ }),
+/* 104 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+var core = __w_pdfjs_require__(13);
+
+var global = __w_pdfjs_require__(12);
+
+var speciesConstructor = __w_pdfjs_require__(94);
+
+var promiseResolve = __w_pdfjs_require__(101);
+
+$export($export.P + $export.R, 'Promise', {
+ 'finally': function _finally(onFinally) {
+ var C = speciesConstructor(this, core.Promise || global.Promise);
+ var isFunction = typeof onFinally == 'function';
+ return this.then(isFunction ? function (x) {
+ return promiseResolve(C, onFinally()).then(function () {
+ return x;
+ });
+ } : onFinally, isFunction ? function (e) {
+ return promiseResolve(C, onFinally()).then(function () {
+ throw e;
+ });
+ } : onFinally);
+ }
+});
+
+/***/ }),
+/* 105 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+var newPromiseCapability = __w_pdfjs_require__(98);
+
+var perform = __w_pdfjs_require__(99);
+
+$export($export.S, 'Promise', {
+ 'try': function _try(callbackfn) {
+ var promiseCapability = newPromiseCapability.f(this);
+ var result = perform(callbackfn);
+ (result.e ? promiseCapability.reject : promiseCapability.resolve)(result.v);
+ return promiseCapability.promise;
+ }
+});
+
+/***/ }),
+/* 106 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(87);
+
+__w_pdfjs_require__(88);
+
+__w_pdfjs_require__(107);
+
+__w_pdfjs_require__(119);
+
+__w_pdfjs_require__(121);
+
+module.exports = __w_pdfjs_require__(13).WeakMap;
+
+/***/ }),
+/* 107 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(12);
+
+var each = __w_pdfjs_require__(108)(0);
+
+var redefine = __w_pdfjs_require__(24);
+
+var meta = __w_pdfjs_require__(112);
+
+var assign = __w_pdfjs_require__(76);
+
+var weak = __w_pdfjs_require__(113);
+
+var isObject = __w_pdfjs_require__(17);
+
+var validate = __w_pdfjs_require__(114);
+
+var NATIVE_WEAK_MAP = __w_pdfjs_require__(114);
+
+var IS_IE11 = !global.ActiveXObject && 'ActiveXObject' in global;
+var WEAK_MAP = 'WeakMap';
+var getWeak = meta.getWeak;
+var isExtensible = Object.isExtensible;
+var uncaughtFrozenStore = weak.ufstore;
+var InternalMap;
+
+var wrapper = function wrapper(get) {
+ return function WeakMap() {
+ return get(this, arguments.length > 0 ? arguments[0] : undefined);
+ };
+};
+
+var methods = {
+ get: function get(key) {
+ if (isObject(key)) {
+ var data = getWeak(key);
+ if (data === true) return uncaughtFrozenStore(validate(this, WEAK_MAP)).get(key);
+ return data ? data[this._i] : undefined;
+ }
+ },
+ set: function set(key, value) {
+ return weak.def(validate(this, WEAK_MAP), key, value);
+ }
+};
+
+var $WeakMap = module.exports = __w_pdfjs_require__(115)(WEAK_MAP, wrapper, methods, weak, true, true);
+
+if (NATIVE_WEAK_MAP && IS_IE11) {
+ InternalMap = weak.getConstructor(wrapper, WEAK_MAP);
+ assign(InternalMap.prototype, methods);
+ meta.NEED = true;
+ each(['delete', 'has', 'get', 'set'], function (key) {
+ var proto = $WeakMap.prototype;
+ var method = proto[key];
+ redefine(proto, key, function (a, b) {
+ if (isObject(a) && !isExtensible(a)) {
+ if (!this._f) this._f = new InternalMap();
+
+ var result = this._f[key](a, b);
+
+ return key == 'set' ? this : result;
+ }
+
+ return method.call(this, a, b);
+ });
+ });
+}
+
+/***/ }),
+/* 108 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var ctx = __w_pdfjs_require__(30);
+
+var IObject = __w_pdfjs_require__(48);
+
+var toObject = __w_pdfjs_require__(66);
+
+var toLength = __w_pdfjs_require__(32);
+
+var asc = __w_pdfjs_require__(109);
+
+module.exports = function (TYPE, $create) {
+ var IS_MAP = TYPE == 1;
+ var IS_FILTER = TYPE == 2;
+ var IS_SOME = TYPE == 3;
+ var IS_EVERY = TYPE == 4;
+ var IS_FIND_INDEX = TYPE == 6;
+ var NO_HOLES = TYPE == 5 || IS_FIND_INDEX;
+ var create = $create || asc;
+ return function ($this, callbackfn, that) {
+ var O = toObject($this);
+ var self = IObject(O);
+ var f = ctx(callbackfn, that, 3);
+ var length = toLength(self.length);
+ var index = 0;
+ var result = IS_MAP ? create($this, length) : IS_FILTER ? create($this, 0) : undefined;
+ var val, res;
+
+ for (; length > index; index++) {
+ if (NO_HOLES || index in self) {
+ val = self[index];
+ res = f(val, index, O);
+
+ if (TYPE) {
+ if (IS_MAP) result[index] = res;else if (res) switch (TYPE) {
+ case 3:
+ return true;
+
+ case 5:
+ return val;
+
+ case 6:
+ return index;
+
+ case 2:
+ result.push(val);
+ } else if (IS_EVERY) return false;
+ }
+ }
+ }
+
+ return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : result;
+ };
+};
+
+/***/ }),
+/* 109 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var speciesConstructor = __w_pdfjs_require__(110);
+
+module.exports = function (original, length) {
+ return new (speciesConstructor(original))(length);
+};
+
+/***/ }),
+/* 110 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(17);
+
+var isArray = __w_pdfjs_require__(111);
+
+var SPECIES = __w_pdfjs_require__(37)('species');
+
+module.exports = function (original) {
+ var C;
+
+ if (isArray(original)) {
+ C = original.constructor;
+ if (typeof C == 'function' && (C === Array || isArray(C.prototype))) C = undefined;
+
+ if (isObject(C)) {
+ C = C[SPECIES];
+ if (C === null) C = undefined;
+ }
+ }
+
+ return C === undefined ? Array : C;
+};
+
+/***/ }),
+/* 111 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var cof = __w_pdfjs_require__(36);
+
+module.exports = Array.isArray || function isArray(arg) {
+ return cof(arg) == 'Array';
+};
+
+/***/ }),
+/* 112 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var META = __w_pdfjs_require__(26)('meta');
+
+var isObject = __w_pdfjs_require__(17);
+
+var has = __w_pdfjs_require__(25);
+
+var setDesc = __w_pdfjs_require__(15).f;
+
+var id = 0;
+
+var isExtensible = Object.isExtensible || function () {
+ return true;
+};
+
+var FREEZE = !__w_pdfjs_require__(20)(function () {
+ return isExtensible(Object.preventExtensions({}));
+});
+
+var setMeta = function setMeta(it) {
+ setDesc(it, META, {
+ value: {
+ i: 'O' + ++id,
+ w: {}
+ }
+ });
+};
+
+var fastKey = function fastKey(it, create) {
+ if (!isObject(it)) return _typeof(it) == 'symbol' ? it : (typeof it == 'string' ? 'S' : 'P') + it;
+
+ if (!has(it, META)) {
+ if (!isExtensible(it)) return 'F';
+ if (!create) return 'E';
+ setMeta(it);
+ }
+
+ return it[META].i;
+};
+
+var getWeak = function getWeak(it, create) {
+ if (!has(it, META)) {
+ if (!isExtensible(it)) return true;
+ if (!create) return false;
+ setMeta(it);
+ }
+
+ return it[META].w;
+};
+
+var onFreeze = function onFreeze(it) {
+ if (FREEZE && meta.NEED && isExtensible(it) && !has(it, META)) setMeta(it);
+ return it;
+};
+
+var meta = module.exports = {
+ KEY: META,
+ NEED: false,
+ fastKey: fastKey,
+ getWeak: getWeak,
+ onFreeze: onFreeze
+};
+
+/***/ }),
+/* 113 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var redefineAll = __w_pdfjs_require__(102);
+
+var getWeak = __w_pdfjs_require__(112).getWeak;
+
+var anObject = __w_pdfjs_require__(16);
+
+var isObject = __w_pdfjs_require__(17);
+
+var anInstance = __w_pdfjs_require__(92);
+
+var forOf = __w_pdfjs_require__(93);
+
+var createArrayMethod = __w_pdfjs_require__(108);
+
+var $has = __w_pdfjs_require__(25);
+
+var validate = __w_pdfjs_require__(114);
+
+var arrayFind = createArrayMethod(5);
+var arrayFindIndex = createArrayMethod(6);
+var id = 0;
+
+var uncaughtFrozenStore = function uncaughtFrozenStore(that) {
+ return that._l || (that._l = new UncaughtFrozenStore());
+};
+
+var UncaughtFrozenStore = function UncaughtFrozenStore() {
+ this.a = [];
+};
+
+var findUncaughtFrozen = function findUncaughtFrozen(store, key) {
+ return arrayFind(store.a, function (it) {
+ return it[0] === key;
+ });
+};
+
+UncaughtFrozenStore.prototype = {
+ get: function get(key) {
+ var entry = findUncaughtFrozen(this, key);
+ if (entry) return entry[1];
+ },
+ has: function has(key) {
+ return !!findUncaughtFrozen(this, key);
+ },
+ set: function set(key, value) {
+ var entry = findUncaughtFrozen(this, key);
+ if (entry) entry[1] = value;else this.a.push([key, value]);
+ },
+ 'delete': function _delete(key) {
+ var index = arrayFindIndex(this.a, function (it) {
+ return it[0] === key;
+ });
+ if (~index) this.a.splice(index, 1);
+ return !!~index;
+ }
+};
+module.exports = {
+ getConstructor: function getConstructor(wrapper, NAME, IS_MAP, ADDER) {
+ var C = wrapper(function (that, iterable) {
+ anInstance(that, C, NAME, '_i');
+ that._t = NAME;
+ that._i = id++;
+ that._l = undefined;
+ if (iterable != undefined) forOf(iterable, IS_MAP, that[ADDER], that);
+ });
+ redefineAll(C.prototype, {
+ 'delete': function _delete(key) {
+ if (!isObject(key)) return false;
+ var data = getWeak(key);
+ if (data === true) return uncaughtFrozenStore(validate(this, NAME))['delete'](key);
+ return data && $has(data, this._i) && delete data[this._i];
+ },
+ has: function has(key) {
+ if (!isObject(key)) return false;
+ var data = getWeak(key);
+ if (data === true) return uncaughtFrozenStore(validate(this, NAME)).has(key);
+ return data && $has(data, this._i);
+ }
+ });
+ return C;
+ },
+ def: function def(that, key, value) {
+ var data = getWeak(anObject(key), true);
+ if (data === true) uncaughtFrozenStore(that).set(key, value);else data[that._i] = value;
+ return that;
+ },
+ ufstore: uncaughtFrozenStore
+};
+
+/***/ }),
+/* 114 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(17);
+
+module.exports = function (it, TYPE) {
+ if (!isObject(it) || it._t !== TYPE) throw TypeError('Incompatible receiver, ' + TYPE + ' required!');
+ return it;
+};
+
+/***/ }),
+/* 115 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(12);
+
+var $export = __w_pdfjs_require__(11);
+
+var redefine = __w_pdfjs_require__(24);
+
+var redefineAll = __w_pdfjs_require__(102);
+
+var meta = __w_pdfjs_require__(112);
+
+var forOf = __w_pdfjs_require__(93);
+
+var anInstance = __w_pdfjs_require__(92);
+
+var isObject = __w_pdfjs_require__(17);
+
+var fails = __w_pdfjs_require__(20);
+
+var $iterDetect = __w_pdfjs_require__(73);
+
+var setToStringTag = __w_pdfjs_require__(64);
+
+var inheritIfRequired = __w_pdfjs_require__(116);
+
+module.exports = function (NAME, wrapper, methods, common, IS_MAP, IS_WEAK) {
+ var Base = global[NAME];
+ var C = Base;
+ var ADDER = IS_MAP ? 'set' : 'add';
+ var proto = C && C.prototype;
+ var O = {};
+
+ var fixMethod = function fixMethod(KEY) {
+ var fn = proto[KEY];
+ redefine(proto, KEY, KEY == 'delete' ? function (a) {
+ return IS_WEAK && !isObject(a) ? false : fn.call(this, a === 0 ? 0 : a);
+ } : KEY == 'has' ? function has(a) {
+ return IS_WEAK && !isObject(a) ? false : fn.call(this, a === 0 ? 0 : a);
+ } : KEY == 'get' ? function get(a) {
+ return IS_WEAK && !isObject(a) ? undefined : fn.call(this, a === 0 ? 0 : a);
+ } : KEY == 'add' ? function add(a) {
+ fn.call(this, a === 0 ? 0 : a);
+ return this;
+ } : function set(a, b) {
+ fn.call(this, a === 0 ? 0 : a, b);
+ return this;
+ });
+ };
+
+ if (typeof C != 'function' || !(IS_WEAK || proto.forEach && !fails(function () {
+ new C().entries().next();
+ }))) {
+ C = common.getConstructor(wrapper, NAME, IS_MAP, ADDER);
+ redefineAll(C.prototype, methods);
+ meta.NEED = true;
+ } else {
+ var instance = new C();
+ var HASNT_CHAINING = instance[ADDER](IS_WEAK ? {} : -0, 1) != instance;
+ var THROWS_ON_PRIMITIVES = fails(function () {
+ instance.has(1);
+ });
+ var ACCEPT_ITERABLES = $iterDetect(function (iter) {
+ new C(iter);
+ });
+ var BUGGY_ZERO = !IS_WEAK && fails(function () {
+ var $instance = new C();
+ var index = 5;
+
+ while (index--) {
+ $instance[ADDER](index, index);
+ }
+
+ return !$instance.has(-0);
+ });
+
+ if (!ACCEPT_ITERABLES) {
+ C = wrapper(function (target, iterable) {
+ anInstance(target, C, NAME);
+ var that = inheritIfRequired(new Base(), target, C);
+ if (iterable != undefined) forOf(iterable, IS_MAP, that[ADDER], that);
+ return that;
+ });
+ C.prototype = proto;
+ proto.constructor = C;
+ }
+
+ if (THROWS_ON_PRIMITIVES || BUGGY_ZERO) {
+ fixMethod('delete');
+ fixMethod('has');
+ IS_MAP && fixMethod('get');
+ }
+
+ if (BUGGY_ZERO || HASNT_CHAINING) fixMethod(ADDER);
+ if (IS_WEAK && proto.clear) delete proto.clear;
+ }
+
+ setToStringTag(C, NAME);
+ O[NAME] = C;
+ $export($export.G + $export.W + $export.F * (C != Base), O);
+ if (!IS_WEAK) common.setStrong(C, NAME, IS_MAP);
+ return C;
+};
+
+/***/ }),
+/* 116 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(17);
+
+var setPrototypeOf = __w_pdfjs_require__(117).set;
+
+module.exports = function (that, target, C) {
+ var S = target.constructor;
+ var P;
+
+ if (S !== C && typeof S == 'function' && (P = S.prototype) !== C.prototype && isObject(P) && setPrototypeOf) {
+ setPrototypeOf(that, P);
+ }
+
+ return that;
+};
+
+/***/ }),
+/* 117 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var isObject = __w_pdfjs_require__(17);
+
+var anObject = __w_pdfjs_require__(16);
+
+var check = function check(O, proto) {
+ anObject(O);
+ if (!isObject(proto) && proto !== null) throw TypeError(proto + ": can't set as prototype!");
+};
+
+module.exports = {
+ set: Object.setPrototypeOf || ('__proto__' in {} ? function (test, buggy, set) {
+ try {
+ set = __w_pdfjs_require__(30)(Function.call, __w_pdfjs_require__(118).f(Object.prototype, '__proto__').set, 2);
+ set(test, []);
+ buggy = !(test instanceof Array);
+ } catch (e) {
+ buggy = true;
+ }
+
+ return function setPrototypeOf(O, proto) {
+ check(O, proto);
+ if (buggy) O.__proto__ = proto;else set(O, proto);
+ return O;
+ };
+ }({}, false) : undefined),
+ check: check
+};
+
+/***/ }),
+/* 118 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var pIE = __w_pdfjs_require__(78);
+
+var createDesc = __w_pdfjs_require__(23);
+
+var toIObject = __w_pdfjs_require__(47);
+
+var toPrimitive = __w_pdfjs_require__(22);
+
+var has = __w_pdfjs_require__(25);
+
+var IE8_DOM_DEFINE = __w_pdfjs_require__(18);
+
+var gOPD = Object.getOwnPropertyDescriptor;
+exports.f = __w_pdfjs_require__(19) ? gOPD : function getOwnPropertyDescriptor(O, P) {
+ O = toIObject(O);
+ P = toPrimitive(P, true);
+ if (IE8_DOM_DEFINE) try {
+ return gOPD(O, P);
+ } catch (e) {}
+ if (has(O, P)) return createDesc(!pIE.f.call(O, P), O[P]);
+};
+
+/***/ }),
+/* 119 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(120)('WeakMap');
+
+/***/ }),
+/* 120 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+module.exports = function (COLLECTION) {
+ $export($export.S, COLLECTION, {
+ of: function of() {
+ var length = arguments.length;
+ var A = new Array(length);
+
+ while (length--) {
+ A[length] = arguments[length];
+ }
+
+ return new this(A);
+ }
+ });
+};
+
+/***/ }),
+/* 121 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(122)('WeakMap');
+
+/***/ }),
+/* 122 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+var aFunction = __w_pdfjs_require__(31);
+
+var ctx = __w_pdfjs_require__(30);
+
+var forOf = __w_pdfjs_require__(93);
+
+module.exports = function (COLLECTION) {
+ $export($export.S, COLLECTION, {
+ from: function from(source) {
+ var mapFn = arguments[1];
+ var mapping, A, n, cb;
+ aFunction(this);
+ mapping = mapFn !== undefined;
+ if (mapping) aFunction(mapFn);
+ if (source == undefined) return new this();
+ A = [];
+
+ if (mapping) {
+ n = 0;
+ cb = ctx(mapFn, arguments[2], 2);
+ forOf(source, false, function (nextItem) {
+ A.push(cb(nextItem, n++));
+ });
+ } else {
+ forOf(source, false, A.push, A);
+ }
+
+ return new this(A);
+ }
+ });
+};
+
+/***/ }),
+/* 123 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(87);
+
+__w_pdfjs_require__(88);
+
+__w_pdfjs_require__(124);
+
+__w_pdfjs_require__(125);
+
+__w_pdfjs_require__(126);
+
+module.exports = __w_pdfjs_require__(13).WeakSet;
+
+/***/ }),
+/* 124 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var weak = __w_pdfjs_require__(113);
+
+var validate = __w_pdfjs_require__(114);
+
+var WEAK_SET = 'WeakSet';
+
+__w_pdfjs_require__(115)(WEAK_SET, function (get) {
+ return function WeakSet() {
+ return get(this, arguments.length > 0 ? arguments[0] : undefined);
+ };
+}, {
+ add: function add(value) {
+ return weak.def(validate(this, WEAK_SET), value, true);
+ }
+}, weak, false, true);
+
+/***/ }),
+/* 125 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(120)('WeakSet');
+
+/***/ }),
+/* 126 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(122)('WeakSet');
+
+/***/ }),
+/* 127 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(128);
+
+module.exports = __w_pdfjs_require__(13).String.codePointAt;
+
+/***/ }),
+/* 128 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+var $at = __w_pdfjs_require__(53)(false);
+
+$export($export.P, 'String', {
+ codePointAt: function codePointAt(pos) {
+ return $at(this, pos);
+ }
+});
+
+/***/ }),
+/* 129 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(130);
+
+module.exports = __w_pdfjs_require__(13).String.fromCodePoint;
+
+/***/ }),
+/* 130 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+var toAbsoluteIndex = __w_pdfjs_require__(49);
+
+var fromCharCode = String.fromCharCode;
+var $fromCodePoint = String.fromCodePoint;
+$export($export.S + $export.F * (!!$fromCodePoint && $fromCodePoint.length != 1), 'String', {
+ fromCodePoint: function fromCodePoint(x) {
+ var res = [];
+ var aLen = arguments.length;
+ var i = 0;
+ var code;
+
+ while (aLen > i) {
+ code = +arguments[i++];
+ if (toAbsoluteIndex(code, 0x10ffff) !== code) throw RangeError(code + ' is not a valid code point');
+ res.push(code < 0x10000 ? fromCharCode(code) : fromCharCode(((code -= 0x10000) >> 10) + 0xd800, code % 0x400 + 0xdc00));
+ }
+
+ return res.join('');
+ }
+});
+
+/***/ }),
+/* 131 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(132);
+
+__w_pdfjs_require__(87);
+
+module.exports = __w_pdfjs_require__(13).Symbol;
+
+/***/ }),
+/* 132 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var global = __w_pdfjs_require__(12);
+
+var has = __w_pdfjs_require__(25);
+
+var DESCRIPTORS = __w_pdfjs_require__(19);
+
+var $export = __w_pdfjs_require__(11);
+
+var redefine = __w_pdfjs_require__(24);
+
+var META = __w_pdfjs_require__(112).KEY;
+
+var $fails = __w_pdfjs_require__(20);
+
+var shared = __w_pdfjs_require__(28);
+
+var setToStringTag = __w_pdfjs_require__(64);
+
+var uid = __w_pdfjs_require__(26);
+
+var wks = __w_pdfjs_require__(37);
+
+var wksExt = __w_pdfjs_require__(133);
+
+var wksDefine = __w_pdfjs_require__(134);
+
+var enumKeys = __w_pdfjs_require__(135);
+
+var isArray = __w_pdfjs_require__(111);
+
+var anObject = __w_pdfjs_require__(16);
+
+var isObject = __w_pdfjs_require__(17);
+
+var toObject = __w_pdfjs_require__(66);
+
+var toIObject = __w_pdfjs_require__(47);
+
+var toPrimitive = __w_pdfjs_require__(22);
+
+var createDesc = __w_pdfjs_require__(23);
+
+var _create = __w_pdfjs_require__(57);
+
+var gOPNExt = __w_pdfjs_require__(136);
+
+var $GOPD = __w_pdfjs_require__(118);
+
+var $GOPS = __w_pdfjs_require__(77);
+
+var $DP = __w_pdfjs_require__(15);
+
+var $keys = __w_pdfjs_require__(59);
+
+var gOPD = $GOPD.f;
+var dP = $DP.f;
+var gOPN = gOPNExt.f;
+var $Symbol = global.Symbol;
+var $JSON = global.JSON;
+
+var _stringify = $JSON && $JSON.stringify;
+
+var PROTOTYPE = 'prototype';
+var HIDDEN = wks('_hidden');
+var TO_PRIMITIVE = wks('toPrimitive');
+var isEnum = {}.propertyIsEnumerable;
+var SymbolRegistry = shared('symbol-registry');
+var AllSymbols = shared('symbols');
+var OPSymbols = shared('op-symbols');
+var ObjectProto = Object[PROTOTYPE];
+var USE_NATIVE = typeof $Symbol == 'function' && !!$GOPS.f;
+var QObject = global.QObject;
+var setter = !QObject || !QObject[PROTOTYPE] || !QObject[PROTOTYPE].findChild;
+var setSymbolDesc = DESCRIPTORS && $fails(function () {
+ return _create(dP({}, 'a', {
+ get: function get() {
+ return dP(this, 'a', {
+ value: 7
+ }).a;
+ }
+ })).a != 7;
+}) ? function (it, key, D) {
+ var protoDesc = gOPD(ObjectProto, key);
+ if (protoDesc) delete ObjectProto[key];
+ dP(it, key, D);
+ if (protoDesc && it !== ObjectProto) dP(ObjectProto, key, protoDesc);
+} : dP;
+
+var wrap = function wrap(tag) {
+ var sym = AllSymbols[tag] = _create($Symbol[PROTOTYPE]);
+
+ sym._k = tag;
+ return sym;
+};
+
+var isSymbol = USE_NATIVE && _typeof($Symbol.iterator) == 'symbol' ? function (it) {
+ return _typeof(it) == 'symbol';
+} : function (it) {
+ return it instanceof $Symbol;
+};
+
+var $defineProperty = function defineProperty(it, key, D) {
+ if (it === ObjectProto) $defineProperty(OPSymbols, key, D);
+ anObject(it);
+ key = toPrimitive(key, true);
+ anObject(D);
+
+ if (has(AllSymbols, key)) {
+ if (!D.enumerable) {
+ if (!has(it, HIDDEN)) dP(it, HIDDEN, createDesc(1, {}));
+ it[HIDDEN][key] = true;
+ } else {
+ if (has(it, HIDDEN) && it[HIDDEN][key]) it[HIDDEN][key] = false;
+ D = _create(D, {
+ enumerable: createDesc(0, false)
+ });
+ }
+
+ return setSymbolDesc(it, key, D);
+ }
+
+ return dP(it, key, D);
+};
+
+var $defineProperties = function defineProperties(it, P) {
+ anObject(it);
+ var keys = enumKeys(P = toIObject(P));
+ var i = 0;
+ var l = keys.length;
+ var key;
+
+ while (l > i) {
+ $defineProperty(it, key = keys[i++], P[key]);
+ }
+
+ return it;
+};
+
+var $create = function create(it, P) {
+ return P === undefined ? _create(it) : $defineProperties(_create(it), P);
+};
+
+var $propertyIsEnumerable = function propertyIsEnumerable(key) {
+ var E = isEnum.call(this, key = toPrimitive(key, true));
+ if (this === ObjectProto && has(AllSymbols, key) && !has(OPSymbols, key)) return false;
+ return E || !has(this, key) || !has(AllSymbols, key) || has(this, HIDDEN) && this[HIDDEN][key] ? E : true;
+};
+
+var $getOwnPropertyDescriptor = function getOwnPropertyDescriptor(it, key) {
+ it = toIObject(it);
+ key = toPrimitive(key, true);
+ if (it === ObjectProto && has(AllSymbols, key) && !has(OPSymbols, key)) return;
+ var D = gOPD(it, key);
+ if (D && has(AllSymbols, key) && !(has(it, HIDDEN) && it[HIDDEN][key])) D.enumerable = true;
+ return D;
+};
+
+var $getOwnPropertyNames = function getOwnPropertyNames(it) {
+ var names = gOPN(toIObject(it));
+ var result = [];
+ var i = 0;
+ var key;
+
+ while (names.length > i) {
+ if (!has(AllSymbols, key = names[i++]) && key != HIDDEN && key != META) result.push(key);
+ }
+
+ return result;
+};
+
+var $getOwnPropertySymbols = function getOwnPropertySymbols(it) {
+ var IS_OP = it === ObjectProto;
+ var names = gOPN(IS_OP ? OPSymbols : toIObject(it));
+ var result = [];
+ var i = 0;
+ var key;
+
+ while (names.length > i) {
+ if (has(AllSymbols, key = names[i++]) && (IS_OP ? has(ObjectProto, key) : true)) result.push(AllSymbols[key]);
+ }
+
+ return result;
+};
+
+if (!USE_NATIVE) {
+ $Symbol = function _Symbol() {
+ if (this instanceof $Symbol) throw TypeError('Symbol is not a constructor!');
+ var tag = uid(arguments.length > 0 ? arguments[0] : undefined);
+
+ var $set = function $set(value) {
+ if (this === ObjectProto) $set.call(OPSymbols, value);
+ if (has(this, HIDDEN) && has(this[HIDDEN], tag)) this[HIDDEN][tag] = false;
+ setSymbolDesc(this, tag, createDesc(1, value));
+ };
+
+ if (DESCRIPTORS && setter) setSymbolDesc(ObjectProto, tag, {
+ configurable: true,
+ set: $set
+ });
+ return wrap(tag);
+ };
+
+ redefine($Symbol[PROTOTYPE], 'toString', function toString() {
+ return this._k;
+ });
+ $GOPD.f = $getOwnPropertyDescriptor;
+ $DP.f = $defineProperty;
+ __w_pdfjs_require__(137).f = gOPNExt.f = $getOwnPropertyNames;
+ __w_pdfjs_require__(78).f = $propertyIsEnumerable;
+ $GOPS.f = $getOwnPropertySymbols;
+
+ if (DESCRIPTORS && !__w_pdfjs_require__(29)) {
+ redefine(ObjectProto, 'propertyIsEnumerable', $propertyIsEnumerable, true);
+ }
+
+ wksExt.f = function (name) {
+ return wrap(wks(name));
+ };
+}
+
+$export($export.G + $export.W + $export.F * !USE_NATIVE, {
+ Symbol: $Symbol
+});
+
+for (var es6Symbols = 'hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables'.split(','), j = 0; es6Symbols.length > j;) {
+ wks(es6Symbols[j++]);
+}
+
+for (var wellKnownSymbols = $keys(wks.store), k = 0; wellKnownSymbols.length > k;) {
+ wksDefine(wellKnownSymbols[k++]);
+}
+
+$export($export.S + $export.F * !USE_NATIVE, 'Symbol', {
+ 'for': function _for(key) {
+ return has(SymbolRegistry, key += '') ? SymbolRegistry[key] : SymbolRegistry[key] = $Symbol(key);
+ },
+ keyFor: function keyFor(sym) {
+ if (!isSymbol(sym)) throw TypeError(sym + ' is not a symbol!');
+
+ for (var key in SymbolRegistry) {
+ if (SymbolRegistry[key] === sym) return key;
+ }
+ },
+ useSetter: function useSetter() {
+ setter = true;
+ },
+ useSimple: function useSimple() {
+ setter = false;
+ }
+});
+$export($export.S + $export.F * !USE_NATIVE, 'Object', {
+ create: $create,
+ defineProperty: $defineProperty,
+ defineProperties: $defineProperties,
+ getOwnPropertyDescriptor: $getOwnPropertyDescriptor,
+ getOwnPropertyNames: $getOwnPropertyNames,
+ getOwnPropertySymbols: $getOwnPropertySymbols
+});
+var FAILS_ON_PRIMITIVES = $fails(function () {
+ $GOPS.f(1);
+});
+$export($export.S + $export.F * FAILS_ON_PRIMITIVES, 'Object', {
+ getOwnPropertySymbols: function getOwnPropertySymbols(it) {
+ return $GOPS.f(toObject(it));
+ }
+});
+$JSON && $export($export.S + $export.F * (!USE_NATIVE || $fails(function () {
+ var S = $Symbol();
+ return _stringify([S]) != '[null]' || _stringify({
+ a: S
+ }) != '{}' || _stringify(Object(S)) != '{}';
+})), 'JSON', {
+ stringify: function stringify(it) {
+ var args = [it];
+ var i = 1;
+ var replacer, $replacer;
+
+ while (arguments.length > i) {
+ args.push(arguments[i++]);
+ }
+
+ $replacer = replacer = args[1];
+ if (!isObject(replacer) && it === undefined || isSymbol(it)) return;
+ if (!isArray(replacer)) replacer = function replacer(key, value) {
+ if (typeof $replacer == 'function') value = $replacer.call(this, key, value);
+ if (!isSymbol(value)) return value;
+ };
+ args[1] = replacer;
+ return _stringify.apply($JSON, args);
+ }
+});
+$Symbol[PROTOTYPE][TO_PRIMITIVE] || __w_pdfjs_require__(14)($Symbol[PROTOTYPE], TO_PRIMITIVE, $Symbol[PROTOTYPE].valueOf);
+setToStringTag($Symbol, 'Symbol');
+setToStringTag(Math, 'Math', true);
+setToStringTag(global.JSON, 'JSON', true);
+
+/***/ }),
+/* 133 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+exports.f = __w_pdfjs_require__(37);
+
+/***/ }),
+/* 134 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var global = __w_pdfjs_require__(12);
+
+var core = __w_pdfjs_require__(13);
+
+var LIBRARY = __w_pdfjs_require__(29);
+
+var wksExt = __w_pdfjs_require__(133);
+
+var defineProperty = __w_pdfjs_require__(15).f;
+
+module.exports = function (name) {
+ var $Symbol = core.Symbol || (core.Symbol = LIBRARY ? {} : global.Symbol || {});
+ if (name.charAt(0) != '_' && !(name in $Symbol)) defineProperty($Symbol, name, {
+ value: wksExt.f(name)
+ });
+};
+
+/***/ }),
+/* 135 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var getKeys = __w_pdfjs_require__(59);
+
+var gOPS = __w_pdfjs_require__(77);
+
+var pIE = __w_pdfjs_require__(78);
+
+module.exports = function (it) {
+ var result = getKeys(it);
+ var getSymbols = gOPS.f;
+
+ if (getSymbols) {
+ var symbols = getSymbols(it);
+ var isEnum = pIE.f;
+ var i = 0;
+ var key;
+
+ while (symbols.length > i) {
+ if (isEnum.call(it, key = symbols[i++])) result.push(key);
+ }
+ }
+
+ return result;
+};
+
+/***/ }),
+/* 136 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var toIObject = __w_pdfjs_require__(47);
+
+var gOPN = __w_pdfjs_require__(137).f;
+
+var toString = {}.toString;
+var windowNames = (typeof window === "undefined" ? "undefined" : _typeof(window)) == 'object' && window && Object.getOwnPropertyNames ? Object.getOwnPropertyNames(window) : [];
+
+var getWindowNames = function getWindowNames(it) {
+ try {
+ return gOPN(it);
+ } catch (e) {
+ return windowNames.slice();
+ }
+};
+
+module.exports.f = function getOwnPropertyNames(it) {
+ return windowNames && toString.call(it) == '[object Window]' ? getWindowNames(it) : gOPN(toIObject(it));
+};
+
+/***/ }),
+/* 137 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $keys = __w_pdfjs_require__(60);
+
+var hiddenKeys = __w_pdfjs_require__(62).concat('length', 'prototype');
+
+exports.f = Object.getOwnPropertyNames || function getOwnPropertyNames(O) {
+ return $keys(O, hiddenKeys);
+};
+
+/***/ }),
+/* 138 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(139);
+
+module.exports = __w_pdfjs_require__(13).String.padStart;
+
+/***/ }),
+/* 139 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+var $pad = __w_pdfjs_require__(140);
+
+var userAgent = __w_pdfjs_require__(100);
+
+var WEBKIT_BUG = /Version\/10\.\d+(\.\d+)?( Mobile\/\w+)? Safari\//.test(userAgent);
+$export($export.P + $export.F * WEBKIT_BUG, 'String', {
+ padStart: function padStart(maxLength) {
+ return $pad(this, maxLength, arguments.length > 1 ? arguments[1] : undefined, true);
+ }
+});
+
+/***/ }),
+/* 140 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toLength = __w_pdfjs_require__(32);
+
+var repeat = __w_pdfjs_require__(141);
+
+var defined = __w_pdfjs_require__(38);
+
+module.exports = function (that, maxLength, fillString, left) {
+ var S = String(defined(that));
+ var stringLength = S.length;
+ var fillStr = fillString === undefined ? ' ' : String(fillString);
+ var intMaxLength = toLength(maxLength);
+ if (intMaxLength <= stringLength || fillStr == '') return S;
+ var fillLen = intMaxLength - stringLength;
+ var stringFiller = repeat.call(fillStr, Math.ceil(fillLen / fillStr.length));
+ if (stringFiller.length > fillLen) stringFiller = stringFiller.slice(0, fillLen);
+ return left ? stringFiller + S : S + stringFiller;
+};
+
+/***/ }),
+/* 141 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var toInteger = __w_pdfjs_require__(33);
+
+var defined = __w_pdfjs_require__(38);
+
+module.exports = function repeat(count) {
+ var str = String(defined(this));
+ var res = '';
+ var n = toInteger(count);
+ if (n < 0 || n == Infinity) throw RangeError("Count can't be negative");
+
+ for (; n > 0; (n >>>= 1) && (str += str)) {
+ if (n & 1) res += str;
+ }
+
+ return res;
+};
+
+/***/ }),
+/* 142 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(143);
+
+module.exports = __w_pdfjs_require__(13).String.padEnd;
+
+/***/ }),
+/* 143 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+var $pad = __w_pdfjs_require__(140);
+
+var userAgent = __w_pdfjs_require__(100);
+
+var WEBKIT_BUG = /Version\/10\.\d+(\.\d+)?( Mobile\/\w+)? Safari\//.test(userAgent);
+$export($export.P + $export.F * WEBKIT_BUG, 'String', {
+ padEnd: function padEnd(maxLength) {
+ return $pad(this, maxLength, arguments.length > 1 ? arguments[1] : undefined, false);
+ }
+});
+
+/***/ }),
+/* 144 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+__w_pdfjs_require__(145);
+
+module.exports = __w_pdfjs_require__(13).Object.values;
+
+/***/ }),
+/* 145 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var $export = __w_pdfjs_require__(11);
+
+var $values = __w_pdfjs_require__(146)(false);
+
+$export($export.S, 'Object', {
+ values: function values(it) {
+ return $values(it);
+ }
+});
+
+/***/ }),
+/* 146 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+var DESCRIPTORS = __w_pdfjs_require__(19);
+
+var getKeys = __w_pdfjs_require__(59);
+
+var toIObject = __w_pdfjs_require__(47);
+
+var isEnum = __w_pdfjs_require__(78).f;
+
+module.exports = function (isEntries) {
+ return function (it) {
+ var O = toIObject(it);
+ var keys = getKeys(O);
+ var length = keys.length;
+ var i = 0;
+ var result = [];
+ var key;
+
+ while (length > i) {
+ key = keys[i++];
+
+ if (!DESCRIPTORS || isEnum.call(O, key)) {
+ result.push(isEntries ? [key, O[key]] : O[key]);
+ }
+ }
+
+ return result;
+ };
+};
+
+/***/ }),
+/* 147 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+{
+ var isReadableStreamSupported = false;
+
+ if (typeof ReadableStream !== 'undefined') {
+ try {
+ new ReadableStream({
+ start: function start(controller) {
+ controller.close();
+ }
+ });
+ isReadableStreamSupported = true;
+ } catch (e) {}
+ }
+
+ if (isReadableStreamSupported) {
+ exports.ReadableStream = ReadableStream;
+ } else {
+ exports.ReadableStream = __w_pdfjs_require__(148).ReadableStream;
+ }
+}
+
+/***/ }),
+/* 148 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof2(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof2 = function _typeof2(obj) { return typeof obj; }; } else { _typeof2 = function _typeof2(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof2(obj); }
+
+(function (e, a) {
+ for (var i in a) {
+ e[i] = a[i];
+ }
+})(exports, function (modules) {
+ var installedModules = {};
+
+ function __w_pdfjs_require__(moduleId) {
+ if (installedModules[moduleId]) return installedModules[moduleId].exports;
+ var module = installedModules[moduleId] = {
+ i: moduleId,
+ l: false,
+ exports: {}
+ };
+ modules[moduleId].call(module.exports, module, module.exports, __w_pdfjs_require__);
+ module.l = true;
+ return module.exports;
+ }
+
+ __w_pdfjs_require__.m = modules;
+ __w_pdfjs_require__.c = installedModules;
+
+ __w_pdfjs_require__.i = function (value) {
+ return value;
+ };
+
+ __w_pdfjs_require__.d = function (exports, name, getter) {
+ if (!__w_pdfjs_require__.o(exports, name)) {
+ Object.defineProperty(exports, name, {
+ configurable: false,
+ enumerable: true,
+ get: getter
+ });
+ }
+ };
+
+ __w_pdfjs_require__.n = function (module) {
+ var getter = module && module.__esModule ? function getDefault() {
+ return module['default'];
+ } : function getModuleExports() {
+ return module;
+ };
+
+ __w_pdfjs_require__.d(getter, 'a', getter);
+
+ return getter;
+ };
+
+ __w_pdfjs_require__.o = function (object, property) {
+ return Object.prototype.hasOwnProperty.call(object, property);
+ };
+
+ __w_pdfjs_require__.p = "";
+ return __w_pdfjs_require__(__w_pdfjs_require__.s = 7);
+}([function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ var _typeof = typeof Symbol === "function" && _typeof2(Symbol.iterator) === "symbol" ? function (obj) {
+ return _typeof2(obj);
+ } : function (obj) {
+ return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : _typeof2(obj);
+ };
+
+ var _require = __w_pdfjs_require__(1),
+ assert = _require.assert;
+
+ function IsPropertyKey(argument) {
+ return typeof argument === 'string' || (typeof argument === 'undefined' ? 'undefined' : _typeof(argument)) === 'symbol';
+ }
+
+ exports.typeIsObject = function (x) {
+ return (typeof x === 'undefined' ? 'undefined' : _typeof(x)) === 'object' && x !== null || typeof x === 'function';
+ };
+
+ exports.createDataProperty = function (o, p, v) {
+ assert(exports.typeIsObject(o));
+ Object.defineProperty(o, p, {
+ value: v,
+ writable: true,
+ enumerable: true,
+ configurable: true
+ });
+ };
+
+ exports.createArrayFromList = function (elements) {
+ return elements.slice();
+ };
+
+ exports.ArrayBufferCopy = function (dest, destOffset, src, srcOffset, n) {
+ new Uint8Array(dest).set(new Uint8Array(src, srcOffset, n), destOffset);
+ };
+
+ exports.CreateIterResultObject = function (value, done) {
+ assert(typeof done === 'boolean');
+ var obj = {};
+ Object.defineProperty(obj, 'value', {
+ value: value,
+ enumerable: true,
+ writable: true,
+ configurable: true
+ });
+ Object.defineProperty(obj, 'done', {
+ value: done,
+ enumerable: true,
+ writable: true,
+ configurable: true
+ });
+ return obj;
+ };
+
+ exports.IsFiniteNonNegativeNumber = function (v) {
+ if (Number.isNaN(v)) {
+ return false;
+ }
+
+ if (v === Infinity) {
+ return false;
+ }
+
+ if (v < 0) {
+ return false;
+ }
+
+ return true;
+ };
+
+ function Call(F, V, args) {
+ if (typeof F !== 'function') {
+ throw new TypeError('Argument is not a function');
+ }
+
+ return Function.prototype.apply.call(F, V, args);
+ }
+
+ exports.InvokeOrNoop = function (O, P, args) {
+ assert(O !== undefined);
+ assert(IsPropertyKey(P));
+ assert(Array.isArray(args));
+ var method = O[P];
+
+ if (method === undefined) {
+ return undefined;
+ }
+
+ return Call(method, O, args);
+ };
+
+ exports.PromiseInvokeOrNoop = function (O, P, args) {
+ assert(O !== undefined);
+ assert(IsPropertyKey(P));
+ assert(Array.isArray(args));
+
+ try {
+ return Promise.resolve(exports.InvokeOrNoop(O, P, args));
+ } catch (returnValueE) {
+ return Promise.reject(returnValueE);
+ }
+ };
+
+ exports.PromiseInvokeOrPerformFallback = function (O, P, args, F, argsF) {
+ assert(O !== undefined);
+ assert(IsPropertyKey(P));
+ assert(Array.isArray(args));
+ assert(Array.isArray(argsF));
+ var method = void 0;
+
+ try {
+ method = O[P];
+ } catch (methodE) {
+ return Promise.reject(methodE);
+ }
+
+ if (method === undefined) {
+ return F.apply(null, argsF);
+ }
+
+ try {
+ return Promise.resolve(Call(method, O, args));
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ };
+
+ exports.TransferArrayBuffer = function (O) {
+ return O.slice();
+ };
+
+ exports.ValidateAndNormalizeHighWaterMark = function (highWaterMark) {
+ highWaterMark = Number(highWaterMark);
+
+ if (Number.isNaN(highWaterMark) || highWaterMark < 0) {
+ throw new RangeError('highWaterMark property of a queuing strategy must be non-negative and non-NaN');
+ }
+
+ return highWaterMark;
+ };
+
+ exports.ValidateAndNormalizeQueuingStrategy = function (size, highWaterMark) {
+ if (size !== undefined && typeof size !== 'function') {
+ throw new TypeError('size property of a queuing strategy must be a function');
+ }
+
+ highWaterMark = exports.ValidateAndNormalizeHighWaterMark(highWaterMark);
+ return {
+ size: size,
+ highWaterMark: highWaterMark
+ };
+ };
+}, function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ function rethrowAssertionErrorRejection(e) {
+ if (e && e.constructor === AssertionError) {
+ setTimeout(function () {
+ throw e;
+ }, 0);
+ }
+ }
+
+ function AssertionError(message) {
+ this.name = 'AssertionError';
+ this.message = message || '';
+ this.stack = new Error().stack;
+ }
+
+ AssertionError.prototype = Object.create(Error.prototype);
+ AssertionError.prototype.constructor = AssertionError;
+
+ function assert(value, message) {
+ if (!value) {
+ throw new AssertionError(message);
+ }
+ }
+
+ module.exports = {
+ rethrowAssertionErrorRejection: rethrowAssertionErrorRejection,
+ AssertionError: AssertionError,
+ assert: assert
+ };
+}, function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ var _createClass = function () {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ return function (Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+ }();
+
+ function _classCallCheck(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ }
+
+ var _require = __w_pdfjs_require__(0),
+ InvokeOrNoop = _require.InvokeOrNoop,
+ PromiseInvokeOrNoop = _require.PromiseInvokeOrNoop,
+ ValidateAndNormalizeQueuingStrategy = _require.ValidateAndNormalizeQueuingStrategy,
+ typeIsObject = _require.typeIsObject;
+
+ var _require2 = __w_pdfjs_require__(1),
+ assert = _require2.assert,
+ rethrowAssertionErrorRejection = _require2.rethrowAssertionErrorRejection;
+
+ var _require3 = __w_pdfjs_require__(3),
+ DequeueValue = _require3.DequeueValue,
+ EnqueueValueWithSize = _require3.EnqueueValueWithSize,
+ PeekQueueValue = _require3.PeekQueueValue,
+ ResetQueue = _require3.ResetQueue;
+
+ var WritableStream = function () {
+ function WritableStream() {
+ var underlyingSink = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
+ size = _ref.size,
+ _ref$highWaterMark = _ref.highWaterMark,
+ highWaterMark = _ref$highWaterMark === undefined ? 1 : _ref$highWaterMark;
+
+ _classCallCheck(this, WritableStream);
+
+ this._state = 'writable';
+ this._storedError = undefined;
+ this._writer = undefined;
+ this._writableStreamController = undefined;
+ this._writeRequests = [];
+ this._inFlightWriteRequest = undefined;
+ this._closeRequest = undefined;
+ this._inFlightCloseRequest = undefined;
+ this._pendingAbortRequest = undefined;
+ this._backpressure = false;
+ var type = underlyingSink.type;
+
+ if (type !== undefined) {
+ throw new RangeError('Invalid type is specified');
+ }
+
+ this._writableStreamController = new WritableStreamDefaultController(this, underlyingSink, size, highWaterMark);
+
+ this._writableStreamController.__startSteps();
+ }
+
+ _createClass(WritableStream, [{
+ key: 'abort',
+ value: function abort(reason) {
+ if (IsWritableStream(this) === false) {
+ return Promise.reject(streamBrandCheckException('abort'));
+ }
+
+ if (IsWritableStreamLocked(this) === true) {
+ return Promise.reject(new TypeError('Cannot abort a stream that already has a writer'));
+ }
+
+ return WritableStreamAbort(this, reason);
+ }
+ }, {
+ key: 'getWriter',
+ value: function getWriter() {
+ if (IsWritableStream(this) === false) {
+ throw streamBrandCheckException('getWriter');
+ }
+
+ return AcquireWritableStreamDefaultWriter(this);
+ }
+ }, {
+ key: 'locked',
+ get: function get() {
+ if (IsWritableStream(this) === false) {
+ throw streamBrandCheckException('locked');
+ }
+
+ return IsWritableStreamLocked(this);
+ }
+ }]);
+
+ return WritableStream;
+ }();
+
+ module.exports = {
+ AcquireWritableStreamDefaultWriter: AcquireWritableStreamDefaultWriter,
+ IsWritableStream: IsWritableStream,
+ IsWritableStreamLocked: IsWritableStreamLocked,
+ WritableStream: WritableStream,
+ WritableStreamAbort: WritableStreamAbort,
+ WritableStreamDefaultControllerError: WritableStreamDefaultControllerError,
+ WritableStreamDefaultWriterCloseWithErrorPropagation: WritableStreamDefaultWriterCloseWithErrorPropagation,
+ WritableStreamDefaultWriterRelease: WritableStreamDefaultWriterRelease,
+ WritableStreamDefaultWriterWrite: WritableStreamDefaultWriterWrite,
+ WritableStreamCloseQueuedOrInFlight: WritableStreamCloseQueuedOrInFlight
+ };
+
+ function AcquireWritableStreamDefaultWriter(stream) {
+ return new WritableStreamDefaultWriter(stream);
+ }
+
+ function IsWritableStream(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_writableStreamController')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function IsWritableStreamLocked(stream) {
+ assert(IsWritableStream(stream) === true, 'IsWritableStreamLocked should only be used on known writable streams');
+
+ if (stream._writer === undefined) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function WritableStreamAbort(stream, reason) {
+ var state = stream._state;
+
+ if (state === 'closed') {
+ return Promise.resolve(undefined);
+ }
+
+ if (state === 'errored') {
+ return Promise.reject(stream._storedError);
+ }
+
+ var error = new TypeError('Requested to abort');
+
+ if (stream._pendingAbortRequest !== undefined) {
+ return Promise.reject(error);
+ }
+
+ assert(state === 'writable' || state === 'erroring', 'state must be writable or erroring');
+ var wasAlreadyErroring = false;
+
+ if (state === 'erroring') {
+ wasAlreadyErroring = true;
+ reason = undefined;
+ }
+
+ var promise = new Promise(function (resolve, reject) {
+ stream._pendingAbortRequest = {
+ _resolve: resolve,
+ _reject: reject,
+ _reason: reason,
+ _wasAlreadyErroring: wasAlreadyErroring
+ };
+ });
+
+ if (wasAlreadyErroring === false) {
+ WritableStreamStartErroring(stream, error);
+ }
+
+ return promise;
+ }
+
+ function WritableStreamAddWriteRequest(stream) {
+ assert(IsWritableStreamLocked(stream) === true);
+ assert(stream._state === 'writable');
+ var promise = new Promise(function (resolve, reject) {
+ var writeRequest = {
+ _resolve: resolve,
+ _reject: reject
+ };
+
+ stream._writeRequests.push(writeRequest);
+ });
+ return promise;
+ }
+
+ function WritableStreamDealWithRejection(stream, error) {
+ var state = stream._state;
+
+ if (state === 'writable') {
+ WritableStreamStartErroring(stream, error);
+ return;
+ }
+
+ assert(state === 'erroring');
+ WritableStreamFinishErroring(stream);
+ }
+
+ function WritableStreamStartErroring(stream, reason) {
+ assert(stream._storedError === undefined, 'stream._storedError === undefined');
+ assert(stream._state === 'writable', 'state must be writable');
+ var controller = stream._writableStreamController;
+ assert(controller !== undefined, 'controller must not be undefined');
+ stream._state = 'erroring';
+ stream._storedError = reason;
+ var writer = stream._writer;
+
+ if (writer !== undefined) {
+ WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason);
+ }
+
+ if (WritableStreamHasOperationMarkedInFlight(stream) === false && controller._started === true) {
+ WritableStreamFinishErroring(stream);
+ }
+ }
+
+ function WritableStreamFinishErroring(stream) {
+ assert(stream._state === 'erroring', 'stream._state === erroring');
+ assert(WritableStreamHasOperationMarkedInFlight(stream) === false, 'WritableStreamHasOperationMarkedInFlight(stream) === false');
+ stream._state = 'errored';
+
+ stream._writableStreamController.__errorSteps();
+
+ var storedError = stream._storedError;
+
+ for (var i = 0; i < stream._writeRequests.length; i++) {
+ var writeRequest = stream._writeRequests[i];
+
+ writeRequest._reject(storedError);
+ }
+
+ stream._writeRequests = [];
+
+ if (stream._pendingAbortRequest === undefined) {
+ WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream);
+ return;
+ }
+
+ var abortRequest = stream._pendingAbortRequest;
+ stream._pendingAbortRequest = undefined;
+
+ if (abortRequest._wasAlreadyErroring === true) {
+ abortRequest._reject(storedError);
+
+ WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream);
+ return;
+ }
+
+ var promise = stream._writableStreamController.__abortSteps(abortRequest._reason);
+
+ promise.then(function () {
+ abortRequest._resolve();
+
+ WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream);
+ }, function (reason) {
+ abortRequest._reject(reason);
+
+ WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream);
+ });
+ }
+
+ function WritableStreamFinishInFlightWrite(stream) {
+ assert(stream._inFlightWriteRequest !== undefined);
+
+ stream._inFlightWriteRequest._resolve(undefined);
+
+ stream._inFlightWriteRequest = undefined;
+ }
+
+ function WritableStreamFinishInFlightWriteWithError(stream, error) {
+ assert(stream._inFlightWriteRequest !== undefined);
+
+ stream._inFlightWriteRequest._reject(error);
+
+ stream._inFlightWriteRequest = undefined;
+ assert(stream._state === 'writable' || stream._state === 'erroring');
+ WritableStreamDealWithRejection(stream, error);
+ }
+
+ function WritableStreamFinishInFlightClose(stream) {
+ assert(stream._inFlightCloseRequest !== undefined);
+
+ stream._inFlightCloseRequest._resolve(undefined);
+
+ stream._inFlightCloseRequest = undefined;
+ var state = stream._state;
+ assert(state === 'writable' || state === 'erroring');
+
+ if (state === 'erroring') {
+ stream._storedError = undefined;
+
+ if (stream._pendingAbortRequest !== undefined) {
+ stream._pendingAbortRequest._resolve();
+
+ stream._pendingAbortRequest = undefined;
+ }
+ }
+
+ stream._state = 'closed';
+ var writer = stream._writer;
+
+ if (writer !== undefined) {
+ defaultWriterClosedPromiseResolve(writer);
+ }
+
+ assert(stream._pendingAbortRequest === undefined, 'stream._pendingAbortRequest === undefined');
+ assert(stream._storedError === undefined, 'stream._storedError === undefined');
+ }
+
+ function WritableStreamFinishInFlightCloseWithError(stream, error) {
+ assert(stream._inFlightCloseRequest !== undefined);
+
+ stream._inFlightCloseRequest._reject(error);
+
+ stream._inFlightCloseRequest = undefined;
+ assert(stream._state === 'writable' || stream._state === 'erroring');
+
+ if (stream._pendingAbortRequest !== undefined) {
+ stream._pendingAbortRequest._reject(error);
+
+ stream._pendingAbortRequest = undefined;
+ }
+
+ WritableStreamDealWithRejection(stream, error);
+ }
+
+ function WritableStreamCloseQueuedOrInFlight(stream) {
+ if (stream._closeRequest === undefined && stream._inFlightCloseRequest === undefined) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function WritableStreamHasOperationMarkedInFlight(stream) {
+ if (stream._inFlightWriteRequest === undefined && stream._inFlightCloseRequest === undefined) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function WritableStreamMarkCloseRequestInFlight(stream) {
+ assert(stream._inFlightCloseRequest === undefined);
+ assert(stream._closeRequest !== undefined);
+ stream._inFlightCloseRequest = stream._closeRequest;
+ stream._closeRequest = undefined;
+ }
+
+ function WritableStreamMarkFirstWriteRequestInFlight(stream) {
+ assert(stream._inFlightWriteRequest === undefined, 'there must be no pending write request');
+ assert(stream._writeRequests.length !== 0, 'writeRequests must not be empty');
+ stream._inFlightWriteRequest = stream._writeRequests.shift();
+ }
+
+ function WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream) {
+ assert(stream._state === 'errored', '_stream_.[[state]] is `"errored"`');
+
+ if (stream._closeRequest !== undefined) {
+ assert(stream._inFlightCloseRequest === undefined);
+
+ stream._closeRequest._reject(stream._storedError);
+
+ stream._closeRequest = undefined;
+ }
+
+ var writer = stream._writer;
+
+ if (writer !== undefined) {
+ defaultWriterClosedPromiseReject(writer, stream._storedError);
+
+ writer._closedPromise["catch"](function () {});
+ }
+ }
+
+ function WritableStreamUpdateBackpressure(stream, backpressure) {
+ assert(stream._state === 'writable');
+ assert(WritableStreamCloseQueuedOrInFlight(stream) === false);
+ var writer = stream._writer;
+
+ if (writer !== undefined && backpressure !== stream._backpressure) {
+ if (backpressure === true) {
+ defaultWriterReadyPromiseReset(writer);
+ } else {
+ assert(backpressure === false);
+ defaultWriterReadyPromiseResolve(writer);
+ }
+ }
+
+ stream._backpressure = backpressure;
+ }
+
+ var WritableStreamDefaultWriter = function () {
+ function WritableStreamDefaultWriter(stream) {
+ _classCallCheck(this, WritableStreamDefaultWriter);
+
+ if (IsWritableStream(stream) === false) {
+ throw new TypeError('WritableStreamDefaultWriter can only be constructed with a WritableStream instance');
+ }
+
+ if (IsWritableStreamLocked(stream) === true) {
+ throw new TypeError('This stream has already been locked for exclusive writing by another writer');
+ }
+
+ this._ownerWritableStream = stream;
+ stream._writer = this;
+ var state = stream._state;
+
+ if (state === 'writable') {
+ if (WritableStreamCloseQueuedOrInFlight(stream) === false && stream._backpressure === true) {
+ defaultWriterReadyPromiseInitialize(this);
+ } else {
+ defaultWriterReadyPromiseInitializeAsResolved(this);
+ }
+
+ defaultWriterClosedPromiseInitialize(this);
+ } else if (state === 'erroring') {
+ defaultWriterReadyPromiseInitializeAsRejected(this, stream._storedError);
+
+ this._readyPromise["catch"](function () {});
+
+ defaultWriterClosedPromiseInitialize(this);
+ } else if (state === 'closed') {
+ defaultWriterReadyPromiseInitializeAsResolved(this);
+ defaultWriterClosedPromiseInitializeAsResolved(this);
+ } else {
+ assert(state === 'errored', 'state must be errored');
+ var storedError = stream._storedError;
+ defaultWriterReadyPromiseInitializeAsRejected(this, storedError);
+
+ this._readyPromise["catch"](function () {});
+
+ defaultWriterClosedPromiseInitializeAsRejected(this, storedError);
+
+ this._closedPromise["catch"](function () {});
+ }
+ }
+
+ _createClass(WritableStreamDefaultWriter, [{
+ key: 'abort',
+ value: function abort(reason) {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ return Promise.reject(defaultWriterBrandCheckException('abort'));
+ }
+
+ if (this._ownerWritableStream === undefined) {
+ return Promise.reject(defaultWriterLockException('abort'));
+ }
+
+ return WritableStreamDefaultWriterAbort(this, reason);
+ }
+ }, {
+ key: 'close',
+ value: function close() {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ return Promise.reject(defaultWriterBrandCheckException('close'));
+ }
+
+ var stream = this._ownerWritableStream;
+
+ if (stream === undefined) {
+ return Promise.reject(defaultWriterLockException('close'));
+ }
+
+ if (WritableStreamCloseQueuedOrInFlight(stream) === true) {
+ return Promise.reject(new TypeError('cannot close an already-closing stream'));
+ }
+
+ return WritableStreamDefaultWriterClose(this);
+ }
+ }, {
+ key: 'releaseLock',
+ value: function releaseLock() {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ throw defaultWriterBrandCheckException('releaseLock');
+ }
+
+ var stream = this._ownerWritableStream;
+
+ if (stream === undefined) {
+ return;
+ }
+
+ assert(stream._writer !== undefined);
+ WritableStreamDefaultWriterRelease(this);
+ }
+ }, {
+ key: 'write',
+ value: function write(chunk) {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ return Promise.reject(defaultWriterBrandCheckException('write'));
+ }
+
+ if (this._ownerWritableStream === undefined) {
+ return Promise.reject(defaultWriterLockException('write to'));
+ }
+
+ return WritableStreamDefaultWriterWrite(this, chunk);
+ }
+ }, {
+ key: 'closed',
+ get: function get() {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ return Promise.reject(defaultWriterBrandCheckException('closed'));
+ }
+
+ return this._closedPromise;
+ }
+ }, {
+ key: 'desiredSize',
+ get: function get() {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ throw defaultWriterBrandCheckException('desiredSize');
+ }
+
+ if (this._ownerWritableStream === undefined) {
+ throw defaultWriterLockException('desiredSize');
+ }
+
+ return WritableStreamDefaultWriterGetDesiredSize(this);
+ }
+ }, {
+ key: 'ready',
+ get: function get() {
+ if (IsWritableStreamDefaultWriter(this) === false) {
+ return Promise.reject(defaultWriterBrandCheckException('ready'));
+ }
+
+ return this._readyPromise;
+ }
+ }]);
+
+ return WritableStreamDefaultWriter;
+ }();
+
+ function IsWritableStreamDefaultWriter(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_ownerWritableStream')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function WritableStreamDefaultWriterAbort(writer, reason) {
+ var stream = writer._ownerWritableStream;
+ assert(stream !== undefined);
+ return WritableStreamAbort(stream, reason);
+ }
+
+ function WritableStreamDefaultWriterClose(writer) {
+ var stream = writer._ownerWritableStream;
+ assert(stream !== undefined);
+ var state = stream._state;
+
+ if (state === 'closed' || state === 'errored') {
+ return Promise.reject(new TypeError('The stream (in ' + state + ' state) is not in the writable state and cannot be closed'));
+ }
+
+ assert(state === 'writable' || state === 'erroring');
+ assert(WritableStreamCloseQueuedOrInFlight(stream) === false);
+ var promise = new Promise(function (resolve, reject) {
+ var closeRequest = {
+ _resolve: resolve,
+ _reject: reject
+ };
+ stream._closeRequest = closeRequest;
+ });
+
+ if (stream._backpressure === true && state === 'writable') {
+ defaultWriterReadyPromiseResolve(writer);
+ }
+
+ WritableStreamDefaultControllerClose(stream._writableStreamController);
+ return promise;
+ }
+
+ function WritableStreamDefaultWriterCloseWithErrorPropagation(writer) {
+ var stream = writer._ownerWritableStream;
+ assert(stream !== undefined);
+ var state = stream._state;
+
+ if (WritableStreamCloseQueuedOrInFlight(stream) === true || state === 'closed') {
+ return Promise.resolve();
+ }
+
+ if (state === 'errored') {
+ return Promise.reject(stream._storedError);
+ }
+
+ assert(state === 'writable' || state === 'erroring');
+ return WritableStreamDefaultWriterClose(writer);
+ }
+
+ function WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, error) {
+ if (writer._closedPromiseState === 'pending') {
+ defaultWriterClosedPromiseReject(writer, error);
+ } else {
+ defaultWriterClosedPromiseResetToRejected(writer, error);
+ }
+
+ writer._closedPromise["catch"](function () {});
+ }
+
+ function WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, error) {
+ if (writer._readyPromiseState === 'pending') {
+ defaultWriterReadyPromiseReject(writer, error);
+ } else {
+ defaultWriterReadyPromiseResetToRejected(writer, error);
+ }
+
+ writer._readyPromise["catch"](function () {});
+ }
+
+ function WritableStreamDefaultWriterGetDesiredSize(writer) {
+ var stream = writer._ownerWritableStream;
+ var state = stream._state;
+
+ if (state === 'errored' || state === 'erroring') {
+ return null;
+ }
+
+ if (state === 'closed') {
+ return 0;
+ }
+
+ return WritableStreamDefaultControllerGetDesiredSize(stream._writableStreamController);
+ }
+
+ function WritableStreamDefaultWriterRelease(writer) {
+ var stream = writer._ownerWritableStream;
+ assert(stream !== undefined);
+ assert(stream._writer === writer);
+ var releasedError = new TypeError('Writer was released and can no longer be used to monitor the stream\'s closedness');
+ WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, releasedError);
+ WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, releasedError);
+ stream._writer = undefined;
+ writer._ownerWritableStream = undefined;
+ }
+
+ function WritableStreamDefaultWriterWrite(writer, chunk) {
+ var stream = writer._ownerWritableStream;
+ assert(stream !== undefined);
+ var controller = stream._writableStreamController;
+ var chunkSize = WritableStreamDefaultControllerGetChunkSize(controller, chunk);
+
+ if (stream !== writer._ownerWritableStream) {
+ return Promise.reject(defaultWriterLockException('write to'));
+ }
+
+ var state = stream._state;
+
+ if (state === 'errored') {
+ return Promise.reject(stream._storedError);
+ }
+
+ if (WritableStreamCloseQueuedOrInFlight(stream) === true || state === 'closed') {
+ return Promise.reject(new TypeError('The stream is closing or closed and cannot be written to'));
+ }
+
+ if (state === 'erroring') {
+ return Promise.reject(stream._storedError);
+ }
+
+ assert(state === 'writable');
+ var promise = WritableStreamAddWriteRequest(stream);
+ WritableStreamDefaultControllerWrite(controller, chunk, chunkSize);
+ return promise;
+ }
+
+ var WritableStreamDefaultController = function () {
+ function WritableStreamDefaultController(stream, underlyingSink, size, highWaterMark) {
+ _classCallCheck(this, WritableStreamDefaultController);
+
+ if (IsWritableStream(stream) === false) {
+ throw new TypeError('WritableStreamDefaultController can only be constructed with a WritableStream instance');
+ }
+
+ if (stream._writableStreamController !== undefined) {
+ throw new TypeError('WritableStreamDefaultController instances can only be created by the WritableStream constructor');
+ }
+
+ this._controlledWritableStream = stream;
+ this._underlyingSink = underlyingSink;
+ this._queue = undefined;
+ this._queueTotalSize = undefined;
+ ResetQueue(this);
+ this._started = false;
+ var normalizedStrategy = ValidateAndNormalizeQueuingStrategy(size, highWaterMark);
+ this._strategySize = normalizedStrategy.size;
+ this._strategyHWM = normalizedStrategy.highWaterMark;
+ var backpressure = WritableStreamDefaultControllerGetBackpressure(this);
+ WritableStreamUpdateBackpressure(stream, backpressure);
+ }
+
+ _createClass(WritableStreamDefaultController, [{
+ key: 'error',
+ value: function error(e) {
+ if (IsWritableStreamDefaultController(this) === false) {
+ throw new TypeError('WritableStreamDefaultController.prototype.error can only be used on a WritableStreamDefaultController');
+ }
+
+ var state = this._controlledWritableStream._state;
+
+ if (state !== 'writable') {
+ return;
+ }
+
+ WritableStreamDefaultControllerError(this, e);
+ }
+ }, {
+ key: '__abortSteps',
+ value: function __abortSteps(reason) {
+ return PromiseInvokeOrNoop(this._underlyingSink, 'abort', [reason]);
+ }
+ }, {
+ key: '__errorSteps',
+ value: function __errorSteps() {
+ ResetQueue(this);
+ }
+ }, {
+ key: '__startSteps',
+ value: function __startSteps() {
+ var _this = this;
+
+ var startResult = InvokeOrNoop(this._underlyingSink, 'start', [this]);
+ var stream = this._controlledWritableStream;
+ Promise.resolve(startResult).then(function () {
+ assert(stream._state === 'writable' || stream._state === 'erroring');
+ _this._started = true;
+ WritableStreamDefaultControllerAdvanceQueueIfNeeded(_this);
+ }, function (r) {
+ assert(stream._state === 'writable' || stream._state === 'erroring');
+ _this._started = true;
+ WritableStreamDealWithRejection(stream, r);
+ })["catch"](rethrowAssertionErrorRejection);
+ }
+ }]);
+
+ return WritableStreamDefaultController;
+ }();
+
+ function WritableStreamDefaultControllerClose(controller) {
+ EnqueueValueWithSize(controller, 'close', 0);
+ WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller);
+ }
+
+ function WritableStreamDefaultControllerGetChunkSize(controller, chunk) {
+ var strategySize = controller._strategySize;
+
+ if (strategySize === undefined) {
+ return 1;
+ }
+
+ try {
+ return strategySize(chunk);
+ } catch (chunkSizeE) {
+ WritableStreamDefaultControllerErrorIfNeeded(controller, chunkSizeE);
+ return 1;
+ }
+ }
+
+ function WritableStreamDefaultControllerGetDesiredSize(controller) {
+ return controller._strategyHWM - controller._queueTotalSize;
+ }
+
+ function WritableStreamDefaultControllerWrite(controller, chunk, chunkSize) {
+ var writeRecord = {
+ chunk: chunk
+ };
+
+ try {
+ EnqueueValueWithSize(controller, writeRecord, chunkSize);
+ } catch (enqueueE) {
+ WritableStreamDefaultControllerErrorIfNeeded(controller, enqueueE);
+ return;
+ }
+
+ var stream = controller._controlledWritableStream;
+
+ if (WritableStreamCloseQueuedOrInFlight(stream) === false && stream._state === 'writable') {
+ var backpressure = WritableStreamDefaultControllerGetBackpressure(controller);
+ WritableStreamUpdateBackpressure(stream, backpressure);
+ }
+
+ WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller);
+ }
+
+ function IsWritableStreamDefaultController(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_underlyingSink')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller) {
+ var stream = controller._controlledWritableStream;
+
+ if (controller._started === false) {
+ return;
+ }
+
+ if (stream._inFlightWriteRequest !== undefined) {
+ return;
+ }
+
+ var state = stream._state;
+
+ if (state === 'closed' || state === 'errored') {
+ return;
+ }
+
+ if (state === 'erroring') {
+ WritableStreamFinishErroring(stream);
+ return;
+ }
+
+ if (controller._queue.length === 0) {
+ return;
+ }
+
+ var writeRecord = PeekQueueValue(controller);
+
+ if (writeRecord === 'close') {
+ WritableStreamDefaultControllerProcessClose(controller);
+ } else {
+ WritableStreamDefaultControllerProcessWrite(controller, writeRecord.chunk);
+ }
+ }
+
+ function WritableStreamDefaultControllerErrorIfNeeded(controller, error) {
+ if (controller._controlledWritableStream._state === 'writable') {
+ WritableStreamDefaultControllerError(controller, error);
+ }
+ }
+
+ function WritableStreamDefaultControllerProcessClose(controller) {
+ var stream = controller._controlledWritableStream;
+ WritableStreamMarkCloseRequestInFlight(stream);
+ DequeueValue(controller);
+ assert(controller._queue.length === 0, 'queue must be empty once the final write record is dequeued');
+ var sinkClosePromise = PromiseInvokeOrNoop(controller._underlyingSink, 'close', []);
+ sinkClosePromise.then(function () {
+ WritableStreamFinishInFlightClose(stream);
+ }, function (reason) {
+ WritableStreamFinishInFlightCloseWithError(stream, reason);
+ })["catch"](rethrowAssertionErrorRejection);
+ }
+
+ function WritableStreamDefaultControllerProcessWrite(controller, chunk) {
+ var stream = controller._controlledWritableStream;
+ WritableStreamMarkFirstWriteRequestInFlight(stream);
+ var sinkWritePromise = PromiseInvokeOrNoop(controller._underlyingSink, 'write', [chunk, controller]);
+ sinkWritePromise.then(function () {
+ WritableStreamFinishInFlightWrite(stream);
+ var state = stream._state;
+ assert(state === 'writable' || state === 'erroring');
+ DequeueValue(controller);
+
+ if (WritableStreamCloseQueuedOrInFlight(stream) === false && state === 'writable') {
+ var backpressure = WritableStreamDefaultControllerGetBackpressure(controller);
+ WritableStreamUpdateBackpressure(stream, backpressure);
+ }
+
+ WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller);
+ }, function (reason) {
+ WritableStreamFinishInFlightWriteWithError(stream, reason);
+ })["catch"](rethrowAssertionErrorRejection);
+ }
+
+ function WritableStreamDefaultControllerGetBackpressure(controller) {
+ var desiredSize = WritableStreamDefaultControllerGetDesiredSize(controller);
+ return desiredSize <= 0;
+ }
+
+ function WritableStreamDefaultControllerError(controller, error) {
+ var stream = controller._controlledWritableStream;
+ assert(stream._state === 'writable');
+ WritableStreamStartErroring(stream, error);
+ }
+
+ function streamBrandCheckException(name) {
+ return new TypeError('WritableStream.prototype.' + name + ' can only be used on a WritableStream');
+ }
+
+ function defaultWriterBrandCheckException(name) {
+ return new TypeError('WritableStreamDefaultWriter.prototype.' + name + ' can only be used on a WritableStreamDefaultWriter');
+ }
+
+ function defaultWriterLockException(name) {
+ return new TypeError('Cannot ' + name + ' a stream using a released writer');
+ }
+
+ function defaultWriterClosedPromiseInitialize(writer) {
+ writer._closedPromise = new Promise(function (resolve, reject) {
+ writer._closedPromise_resolve = resolve;
+ writer._closedPromise_reject = reject;
+ writer._closedPromiseState = 'pending';
+ });
+ }
+
+ function defaultWriterClosedPromiseInitializeAsRejected(writer, reason) {
+ writer._closedPromise = Promise.reject(reason);
+ writer._closedPromise_resolve = undefined;
+ writer._closedPromise_reject = undefined;
+ writer._closedPromiseState = 'rejected';
+ }
+
+ function defaultWriterClosedPromiseInitializeAsResolved(writer) {
+ writer._closedPromise = Promise.resolve(undefined);
+ writer._closedPromise_resolve = undefined;
+ writer._closedPromise_reject = undefined;
+ writer._closedPromiseState = 'resolved';
+ }
+
+ function defaultWriterClosedPromiseReject(writer, reason) {
+ assert(writer._closedPromise_resolve !== undefined, 'writer._closedPromise_resolve !== undefined');
+ assert(writer._closedPromise_reject !== undefined, 'writer._closedPromise_reject !== undefined');
+ assert(writer._closedPromiseState === 'pending', 'writer._closedPromiseState is pending');
+
+ writer._closedPromise_reject(reason);
+
+ writer._closedPromise_resolve = undefined;
+ writer._closedPromise_reject = undefined;
+ writer._closedPromiseState = 'rejected';
+ }
+
+ function defaultWriterClosedPromiseResetToRejected(writer, reason) {
+ assert(writer._closedPromise_resolve === undefined, 'writer._closedPromise_resolve === undefined');
+ assert(writer._closedPromise_reject === undefined, 'writer._closedPromise_reject === undefined');
+ assert(writer._closedPromiseState !== 'pending', 'writer._closedPromiseState is not pending');
+ writer._closedPromise = Promise.reject(reason);
+ writer._closedPromiseState = 'rejected';
+ }
+
+ function defaultWriterClosedPromiseResolve(writer) {
+ assert(writer._closedPromise_resolve !== undefined, 'writer._closedPromise_resolve !== undefined');
+ assert(writer._closedPromise_reject !== undefined, 'writer._closedPromise_reject !== undefined');
+ assert(writer._closedPromiseState === 'pending', 'writer._closedPromiseState is pending');
+
+ writer._closedPromise_resolve(undefined);
+
+ writer._closedPromise_resolve = undefined;
+ writer._closedPromise_reject = undefined;
+ writer._closedPromiseState = 'resolved';
+ }
+
+ function defaultWriterReadyPromiseInitialize(writer) {
+ writer._readyPromise = new Promise(function (resolve, reject) {
+ writer._readyPromise_resolve = resolve;
+ writer._readyPromise_reject = reject;
+ });
+ writer._readyPromiseState = 'pending';
+ }
+
+ function defaultWriterReadyPromiseInitializeAsRejected(writer, reason) {
+ writer._readyPromise = Promise.reject(reason);
+ writer._readyPromise_resolve = undefined;
+ writer._readyPromise_reject = undefined;
+ writer._readyPromiseState = 'rejected';
+ }
+
+ function defaultWriterReadyPromiseInitializeAsResolved(writer) {
+ writer._readyPromise = Promise.resolve(undefined);
+ writer._readyPromise_resolve = undefined;
+ writer._readyPromise_reject = undefined;
+ writer._readyPromiseState = 'fulfilled';
+ }
+
+ function defaultWriterReadyPromiseReject(writer, reason) {
+ assert(writer._readyPromise_resolve !== undefined, 'writer._readyPromise_resolve !== undefined');
+ assert(writer._readyPromise_reject !== undefined, 'writer._readyPromise_reject !== undefined');
+
+ writer._readyPromise_reject(reason);
+
+ writer._readyPromise_resolve = undefined;
+ writer._readyPromise_reject = undefined;
+ writer._readyPromiseState = 'rejected';
+ }
+
+ function defaultWriterReadyPromiseReset(writer) {
+ assert(writer._readyPromise_resolve === undefined, 'writer._readyPromise_resolve === undefined');
+ assert(writer._readyPromise_reject === undefined, 'writer._readyPromise_reject === undefined');
+ writer._readyPromise = new Promise(function (resolve, reject) {
+ writer._readyPromise_resolve = resolve;
+ writer._readyPromise_reject = reject;
+ });
+ writer._readyPromiseState = 'pending';
+ }
+
+ function defaultWriterReadyPromiseResetToRejected(writer, reason) {
+ assert(writer._readyPromise_resolve === undefined, 'writer._readyPromise_resolve === undefined');
+ assert(writer._readyPromise_reject === undefined, 'writer._readyPromise_reject === undefined');
+ writer._readyPromise = Promise.reject(reason);
+ writer._readyPromiseState = 'rejected';
+ }
+
+ function defaultWriterReadyPromiseResolve(writer) {
+ assert(writer._readyPromise_resolve !== undefined, 'writer._readyPromise_resolve !== undefined');
+ assert(writer._readyPromise_reject !== undefined, 'writer._readyPromise_reject !== undefined');
+
+ writer._readyPromise_resolve(undefined);
+
+ writer._readyPromise_resolve = undefined;
+ writer._readyPromise_reject = undefined;
+ writer._readyPromiseState = 'fulfilled';
+ }
+}, function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ var _require = __w_pdfjs_require__(0),
+ IsFiniteNonNegativeNumber = _require.IsFiniteNonNegativeNumber;
+
+ var _require2 = __w_pdfjs_require__(1),
+ assert = _require2.assert;
+
+ exports.DequeueValue = function (container) {
+ assert('_queue' in container && '_queueTotalSize' in container, 'Spec-level failure: DequeueValue should only be used on containers with [[queue]] and [[queueTotalSize]].');
+ assert(container._queue.length > 0, 'Spec-level failure: should never dequeue from an empty queue.');
+
+ var pair = container._queue.shift();
+
+ container._queueTotalSize -= pair.size;
+
+ if (container._queueTotalSize < 0) {
+ container._queueTotalSize = 0;
+ }
+
+ return pair.value;
+ };
+
+ exports.EnqueueValueWithSize = function (container, value, size) {
+ assert('_queue' in container && '_queueTotalSize' in container, 'Spec-level failure: EnqueueValueWithSize should only be used on containers with [[queue]] and ' + '[[queueTotalSize]].');
+ size = Number(size);
+
+ if (!IsFiniteNonNegativeNumber(size)) {
+ throw new RangeError('Size must be a finite, non-NaN, non-negative number.');
+ }
+
+ container._queue.push({
+ value: value,
+ size: size
+ });
+
+ container._queueTotalSize += size;
+ };
+
+ exports.PeekQueueValue = function (container) {
+ assert('_queue' in container && '_queueTotalSize' in container, 'Spec-level failure: PeekQueueValue should only be used on containers with [[queue]] and [[queueTotalSize]].');
+ assert(container._queue.length > 0, 'Spec-level failure: should never peek at an empty queue.');
+ var pair = container._queue[0];
+ return pair.value;
+ };
+
+ exports.ResetQueue = function (container) {
+ assert('_queue' in container && '_queueTotalSize' in container, 'Spec-level failure: ResetQueue should only be used on containers with [[queue]] and [[queueTotalSize]].');
+ container._queue = [];
+ container._queueTotalSize = 0;
+ };
+}, function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ var _createClass = function () {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ return function (Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+ }();
+
+ function _classCallCheck(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ }
+
+ var _require = __w_pdfjs_require__(0),
+ ArrayBufferCopy = _require.ArrayBufferCopy,
+ CreateIterResultObject = _require.CreateIterResultObject,
+ IsFiniteNonNegativeNumber = _require.IsFiniteNonNegativeNumber,
+ InvokeOrNoop = _require.InvokeOrNoop,
+ PromiseInvokeOrNoop = _require.PromiseInvokeOrNoop,
+ TransferArrayBuffer = _require.TransferArrayBuffer,
+ ValidateAndNormalizeQueuingStrategy = _require.ValidateAndNormalizeQueuingStrategy,
+ ValidateAndNormalizeHighWaterMark = _require.ValidateAndNormalizeHighWaterMark;
+
+ var _require2 = __w_pdfjs_require__(0),
+ createArrayFromList = _require2.createArrayFromList,
+ createDataProperty = _require2.createDataProperty,
+ typeIsObject = _require2.typeIsObject;
+
+ var _require3 = __w_pdfjs_require__(1),
+ assert = _require3.assert,
+ rethrowAssertionErrorRejection = _require3.rethrowAssertionErrorRejection;
+
+ var _require4 = __w_pdfjs_require__(3),
+ DequeueValue = _require4.DequeueValue,
+ EnqueueValueWithSize = _require4.EnqueueValueWithSize,
+ ResetQueue = _require4.ResetQueue;
+
+ var _require5 = __w_pdfjs_require__(2),
+ AcquireWritableStreamDefaultWriter = _require5.AcquireWritableStreamDefaultWriter,
+ IsWritableStream = _require5.IsWritableStream,
+ IsWritableStreamLocked = _require5.IsWritableStreamLocked,
+ WritableStreamAbort = _require5.WritableStreamAbort,
+ WritableStreamDefaultWriterCloseWithErrorPropagation = _require5.WritableStreamDefaultWriterCloseWithErrorPropagation,
+ WritableStreamDefaultWriterRelease = _require5.WritableStreamDefaultWriterRelease,
+ WritableStreamDefaultWriterWrite = _require5.WritableStreamDefaultWriterWrite,
+ WritableStreamCloseQueuedOrInFlight = _require5.WritableStreamCloseQueuedOrInFlight;
+
+ var ReadableStream = function () {
+ function ReadableStream() {
+ var underlyingSource = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
+ size = _ref.size,
+ highWaterMark = _ref.highWaterMark;
+
+ _classCallCheck(this, ReadableStream);
+
+ this._state = 'readable';
+ this._reader = undefined;
+ this._storedError = undefined;
+ this._disturbed = false;
+ this._readableStreamController = undefined;
+ var type = underlyingSource.type;
+ var typeString = String(type);
+
+ if (typeString === 'bytes') {
+ if (highWaterMark === undefined) {
+ highWaterMark = 0;
+ }
+
+ this._readableStreamController = new ReadableByteStreamController(this, underlyingSource, highWaterMark);
+ } else if (type === undefined) {
+ if (highWaterMark === undefined) {
+ highWaterMark = 1;
+ }
+
+ this._readableStreamController = new ReadableStreamDefaultController(this, underlyingSource, size, highWaterMark);
+ } else {
+ throw new RangeError('Invalid type is specified');
+ }
+ }
+
+ _createClass(ReadableStream, [{
+ key: 'cancel',
+ value: function cancel(reason) {
+ if (IsReadableStream(this) === false) {
+ return Promise.reject(streamBrandCheckException('cancel'));
+ }
+
+ if (IsReadableStreamLocked(this) === true) {
+ return Promise.reject(new TypeError('Cannot cancel a stream that already has a reader'));
+ }
+
+ return ReadableStreamCancel(this, reason);
+ }
+ }, {
+ key: 'getReader',
+ value: function getReader() {
+ var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
+ mode = _ref2.mode;
+
+ if (IsReadableStream(this) === false) {
+ throw streamBrandCheckException('getReader');
+ }
+
+ if (mode === undefined) {
+ return AcquireReadableStreamDefaultReader(this);
+ }
+
+ mode = String(mode);
+
+ if (mode === 'byob') {
+ return AcquireReadableStreamBYOBReader(this);
+ }
+
+ throw new RangeError('Invalid mode is specified');
+ }
+ }, {
+ key: 'pipeThrough',
+ value: function pipeThrough(_ref3, options) {
+ var writable = _ref3.writable,
+ readable = _ref3.readable;
+ var promise = this.pipeTo(writable, options);
+ ifIsObjectAndHasAPromiseIsHandledInternalSlotSetPromiseIsHandledToTrue(promise);
+ return readable;
+ }
+ }, {
+ key: 'pipeTo',
+ value: function pipeTo(dest) {
+ var _this = this;
+
+ var _ref4 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
+ preventClose = _ref4.preventClose,
+ preventAbort = _ref4.preventAbort,
+ preventCancel = _ref4.preventCancel;
+
+ if (IsReadableStream(this) === false) {
+ return Promise.reject(streamBrandCheckException('pipeTo'));
+ }
+
+ if (IsWritableStream(dest) === false) {
+ return Promise.reject(new TypeError('ReadableStream.prototype.pipeTo\'s first argument must be a WritableStream'));
+ }
+
+ preventClose = Boolean(preventClose);
+ preventAbort = Boolean(preventAbort);
+ preventCancel = Boolean(preventCancel);
+
+ if (IsReadableStreamLocked(this) === true) {
+ return Promise.reject(new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream'));
+ }
+
+ if (IsWritableStreamLocked(dest) === true) {
+ return Promise.reject(new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream'));
+ }
+
+ var reader = AcquireReadableStreamDefaultReader(this);
+ var writer = AcquireWritableStreamDefaultWriter(dest);
+ var shuttingDown = false;
+ var currentWrite = Promise.resolve();
+ return new Promise(function (resolve, reject) {
+ function pipeLoop() {
+ currentWrite = Promise.resolve();
+
+ if (shuttingDown === true) {
+ return Promise.resolve();
+ }
+
+ return writer._readyPromise.then(function () {
+ return ReadableStreamDefaultReaderRead(reader).then(function (_ref5) {
+ var value = _ref5.value,
+ done = _ref5.done;
+
+ if (done === true) {
+ return;
+ }
+
+ currentWrite = WritableStreamDefaultWriterWrite(writer, value)["catch"](function () {});
+ });
+ }).then(pipeLoop);
+ }
+
+ isOrBecomesErrored(_this, reader._closedPromise, function (storedError) {
+ if (preventAbort === false) {
+ shutdownWithAction(function () {
+ return WritableStreamAbort(dest, storedError);
+ }, true, storedError);
+ } else {
+ shutdown(true, storedError);
+ }
+ });
+ isOrBecomesErrored(dest, writer._closedPromise, function (storedError) {
+ if (preventCancel === false) {
+ shutdownWithAction(function () {
+ return ReadableStreamCancel(_this, storedError);
+ }, true, storedError);
+ } else {
+ shutdown(true, storedError);
+ }
+ });
+ isOrBecomesClosed(_this, reader._closedPromise, function () {
+ if (preventClose === false) {
+ shutdownWithAction(function () {
+ return WritableStreamDefaultWriterCloseWithErrorPropagation(writer);
+ });
+ } else {
+ shutdown();
+ }
+ });
+
+ if (WritableStreamCloseQueuedOrInFlight(dest) === true || dest._state === 'closed') {
+ var destClosed = new TypeError('the destination writable stream closed before all data could be piped to it');
+
+ if (preventCancel === false) {
+ shutdownWithAction(function () {
+ return ReadableStreamCancel(_this, destClosed);
+ }, true, destClosed);
+ } else {
+ shutdown(true, destClosed);
+ }
+ }
+
+ pipeLoop()["catch"](function (err) {
+ currentWrite = Promise.resolve();
+ rethrowAssertionErrorRejection(err);
+ });
+
+ function waitForWritesToFinish() {
+ var oldCurrentWrite = currentWrite;
+ return currentWrite.then(function () {
+ return oldCurrentWrite !== currentWrite ? waitForWritesToFinish() : undefined;
+ });
+ }
+
+ function isOrBecomesErrored(stream, promise, action) {
+ if (stream._state === 'errored') {
+ action(stream._storedError);
+ } else {
+ promise["catch"](action)["catch"](rethrowAssertionErrorRejection);
+ }
+ }
+
+ function isOrBecomesClosed(stream, promise, action) {
+ if (stream._state === 'closed') {
+ action();
+ } else {
+ promise.then(action)["catch"](rethrowAssertionErrorRejection);
+ }
+ }
+
+ function shutdownWithAction(action, originalIsError, originalError) {
+ if (shuttingDown === true) {
+ return;
+ }
+
+ shuttingDown = true;
+
+ if (dest._state === 'writable' && WritableStreamCloseQueuedOrInFlight(dest) === false) {
+ waitForWritesToFinish().then(doTheRest);
+ } else {
+ doTheRest();
+ }
+
+ function doTheRest() {
+ action().then(function () {
+ return finalize(originalIsError, originalError);
+ }, function (newError) {
+ return finalize(true, newError);
+ })["catch"](rethrowAssertionErrorRejection);
+ }
+ }
+
+ function shutdown(isError, error) {
+ if (shuttingDown === true) {
+ return;
+ }
+
+ shuttingDown = true;
+
+ if (dest._state === 'writable' && WritableStreamCloseQueuedOrInFlight(dest) === false) {
+ waitForWritesToFinish().then(function () {
+ return finalize(isError, error);
+ })["catch"](rethrowAssertionErrorRejection);
+ } else {
+ finalize(isError, error);
+ }
+ }
+
+ function finalize(isError, error) {
+ WritableStreamDefaultWriterRelease(writer);
+ ReadableStreamReaderGenericRelease(reader);
+
+ if (isError) {
+ reject(error);
+ } else {
+ resolve(undefined);
+ }
+ }
+ });
+ }
+ }, {
+ key: 'tee',
+ value: function tee() {
+ if (IsReadableStream(this) === false) {
+ throw streamBrandCheckException('tee');
+ }
+
+ var branches = ReadableStreamTee(this, false);
+ return createArrayFromList(branches);
+ }
+ }, {
+ key: 'locked',
+ get: function get() {
+ if (IsReadableStream(this) === false) {
+ throw streamBrandCheckException('locked');
+ }
+
+ return IsReadableStreamLocked(this);
+ }
+ }]);
+
+ return ReadableStream;
+ }();
+
+ module.exports = {
+ ReadableStream: ReadableStream,
+ IsReadableStreamDisturbed: IsReadableStreamDisturbed,
+ ReadableStreamDefaultControllerClose: ReadableStreamDefaultControllerClose,
+ ReadableStreamDefaultControllerEnqueue: ReadableStreamDefaultControllerEnqueue,
+ ReadableStreamDefaultControllerError: ReadableStreamDefaultControllerError,
+ ReadableStreamDefaultControllerGetDesiredSize: ReadableStreamDefaultControllerGetDesiredSize
+ };
+
+ function AcquireReadableStreamBYOBReader(stream) {
+ return new ReadableStreamBYOBReader(stream);
+ }
+
+ function AcquireReadableStreamDefaultReader(stream) {
+ return new ReadableStreamDefaultReader(stream);
+ }
+
+ function IsReadableStream(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_readableStreamController')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function IsReadableStreamDisturbed(stream) {
+ assert(IsReadableStream(stream) === true, 'IsReadableStreamDisturbed should only be used on known readable streams');
+ return stream._disturbed;
+ }
+
+ function IsReadableStreamLocked(stream) {
+ assert(IsReadableStream(stream) === true, 'IsReadableStreamLocked should only be used on known readable streams');
+
+ if (stream._reader === undefined) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function ReadableStreamTee(stream, cloneForBranch2) {
+ assert(IsReadableStream(stream) === true);
+ assert(typeof cloneForBranch2 === 'boolean');
+ var reader = AcquireReadableStreamDefaultReader(stream);
+ var teeState = {
+ closedOrErrored: false,
+ canceled1: false,
+ canceled2: false,
+ reason1: undefined,
+ reason2: undefined
+ };
+ teeState.promise = new Promise(function (resolve) {
+ teeState._resolve = resolve;
+ });
+ var pull = create_ReadableStreamTeePullFunction();
+ pull._reader = reader;
+ pull._teeState = teeState;
+ pull._cloneForBranch2 = cloneForBranch2;
+ var cancel1 = create_ReadableStreamTeeBranch1CancelFunction();
+ cancel1._stream = stream;
+ cancel1._teeState = teeState;
+ var cancel2 = create_ReadableStreamTeeBranch2CancelFunction();
+ cancel2._stream = stream;
+ cancel2._teeState = teeState;
+ var underlyingSource1 = Object.create(Object.prototype);
+ createDataProperty(underlyingSource1, 'pull', pull);
+ createDataProperty(underlyingSource1, 'cancel', cancel1);
+ var branch1Stream = new ReadableStream(underlyingSource1);
+ var underlyingSource2 = Object.create(Object.prototype);
+ createDataProperty(underlyingSource2, 'pull', pull);
+ createDataProperty(underlyingSource2, 'cancel', cancel2);
+ var branch2Stream = new ReadableStream(underlyingSource2);
+ pull._branch1 = branch1Stream._readableStreamController;
+ pull._branch2 = branch2Stream._readableStreamController;
+
+ reader._closedPromise["catch"](function (r) {
+ if (teeState.closedOrErrored === true) {
+ return;
+ }
+
+ ReadableStreamDefaultControllerError(pull._branch1, r);
+ ReadableStreamDefaultControllerError(pull._branch2, r);
+ teeState.closedOrErrored = true;
+ });
+
+ return [branch1Stream, branch2Stream];
+ }
+
+ function create_ReadableStreamTeePullFunction() {
+ function f() {
+ var reader = f._reader,
+ branch1 = f._branch1,
+ branch2 = f._branch2,
+ teeState = f._teeState;
+ return ReadableStreamDefaultReaderRead(reader).then(function (result) {
+ assert(typeIsObject(result));
+ var value = result.value;
+ var done = result.done;
+ assert(typeof done === 'boolean');
+
+ if (done === true && teeState.closedOrErrored === false) {
+ if (teeState.canceled1 === false) {
+ ReadableStreamDefaultControllerClose(branch1);
+ }
+
+ if (teeState.canceled2 === false) {
+ ReadableStreamDefaultControllerClose(branch2);
+ }
+
+ teeState.closedOrErrored = true;
+ }
+
+ if (teeState.closedOrErrored === true) {
+ return;
+ }
+
+ var value1 = value;
+ var value2 = value;
+
+ if (teeState.canceled1 === false) {
+ ReadableStreamDefaultControllerEnqueue(branch1, value1);
+ }
+
+ if (teeState.canceled2 === false) {
+ ReadableStreamDefaultControllerEnqueue(branch2, value2);
+ }
+ });
+ }
+
+ return f;
+ }
+
+ function create_ReadableStreamTeeBranch1CancelFunction() {
+ function f(reason) {
+ var stream = f._stream,
+ teeState = f._teeState;
+ teeState.canceled1 = true;
+ teeState.reason1 = reason;
+
+ if (teeState.canceled2 === true) {
+ var compositeReason = createArrayFromList([teeState.reason1, teeState.reason2]);
+ var cancelResult = ReadableStreamCancel(stream, compositeReason);
+
+ teeState._resolve(cancelResult);
+ }
+
+ return teeState.promise;
+ }
+
+ return f;
+ }
+
+ function create_ReadableStreamTeeBranch2CancelFunction() {
+ function f(reason) {
+ var stream = f._stream,
+ teeState = f._teeState;
+ teeState.canceled2 = true;
+ teeState.reason2 = reason;
+
+ if (teeState.canceled1 === true) {
+ var compositeReason = createArrayFromList([teeState.reason1, teeState.reason2]);
+ var cancelResult = ReadableStreamCancel(stream, compositeReason);
+
+ teeState._resolve(cancelResult);
+ }
+
+ return teeState.promise;
+ }
+
+ return f;
+ }
+
+ function ReadableStreamAddReadIntoRequest(stream) {
+ assert(IsReadableStreamBYOBReader(stream._reader) === true);
+ assert(stream._state === 'readable' || stream._state === 'closed');
+ var promise = new Promise(function (resolve, reject) {
+ var readIntoRequest = {
+ _resolve: resolve,
+ _reject: reject
+ };
+
+ stream._reader._readIntoRequests.push(readIntoRequest);
+ });
+ return promise;
+ }
+
+ function ReadableStreamAddReadRequest(stream) {
+ assert(IsReadableStreamDefaultReader(stream._reader) === true);
+ assert(stream._state === 'readable');
+ var promise = new Promise(function (resolve, reject) {
+ var readRequest = {
+ _resolve: resolve,
+ _reject: reject
+ };
+
+ stream._reader._readRequests.push(readRequest);
+ });
+ return promise;
+ }
+
+ function ReadableStreamCancel(stream, reason) {
+ stream._disturbed = true;
+
+ if (stream._state === 'closed') {
+ return Promise.resolve(undefined);
+ }
+
+ if (stream._state === 'errored') {
+ return Promise.reject(stream._storedError);
+ }
+
+ ReadableStreamClose(stream);
+
+ var sourceCancelPromise = stream._readableStreamController.__cancelSteps(reason);
+
+ return sourceCancelPromise.then(function () {
+ return undefined;
+ });
+ }
+
+ function ReadableStreamClose(stream) {
+ assert(stream._state === 'readable');
+ stream._state = 'closed';
+ var reader = stream._reader;
+
+ if (reader === undefined) {
+ return undefined;
+ }
+
+ if (IsReadableStreamDefaultReader(reader) === true) {
+ for (var i = 0; i < reader._readRequests.length; i++) {
+ var _resolve = reader._readRequests[i]._resolve;
+
+ _resolve(CreateIterResultObject(undefined, true));
+ }
+
+ reader._readRequests = [];
+ }
+
+ defaultReaderClosedPromiseResolve(reader);
+ return undefined;
+ }
+
+ function ReadableStreamError(stream, e) {
+ assert(IsReadableStream(stream) === true, 'stream must be ReadableStream');
+ assert(stream._state === 'readable', 'state must be readable');
+ stream._state = 'errored';
+ stream._storedError = e;
+ var reader = stream._reader;
+
+ if (reader === undefined) {
+ return undefined;
+ }
+
+ if (IsReadableStreamDefaultReader(reader) === true) {
+ for (var i = 0; i < reader._readRequests.length; i++) {
+ var readRequest = reader._readRequests[i];
+
+ readRequest._reject(e);
+ }
+
+ reader._readRequests = [];
+ } else {
+ assert(IsReadableStreamBYOBReader(reader), 'reader must be ReadableStreamBYOBReader');
+
+ for (var _i = 0; _i < reader._readIntoRequests.length; _i++) {
+ var readIntoRequest = reader._readIntoRequests[_i];
+
+ readIntoRequest._reject(e);
+ }
+
+ reader._readIntoRequests = [];
+ }
+
+ defaultReaderClosedPromiseReject(reader, e);
+
+ reader._closedPromise["catch"](function () {});
+ }
+
+ function ReadableStreamFulfillReadIntoRequest(stream, chunk, done) {
+ var reader = stream._reader;
+ assert(reader._readIntoRequests.length > 0);
+
+ var readIntoRequest = reader._readIntoRequests.shift();
+
+ readIntoRequest._resolve(CreateIterResultObject(chunk, done));
+ }
+
+ function ReadableStreamFulfillReadRequest(stream, chunk, done) {
+ var reader = stream._reader;
+ assert(reader._readRequests.length > 0);
+
+ var readRequest = reader._readRequests.shift();
+
+ readRequest._resolve(CreateIterResultObject(chunk, done));
+ }
+
+ function ReadableStreamGetNumReadIntoRequests(stream) {
+ return stream._reader._readIntoRequests.length;
+ }
+
+ function ReadableStreamGetNumReadRequests(stream) {
+ return stream._reader._readRequests.length;
+ }
+
+ function ReadableStreamHasBYOBReader(stream) {
+ var reader = stream._reader;
+
+ if (reader === undefined) {
+ return false;
+ }
+
+ if (IsReadableStreamBYOBReader(reader) === false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function ReadableStreamHasDefaultReader(stream) {
+ var reader = stream._reader;
+
+ if (reader === undefined) {
+ return false;
+ }
+
+ if (IsReadableStreamDefaultReader(reader) === false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ var ReadableStreamDefaultReader = function () {
+ function ReadableStreamDefaultReader(stream) {
+ _classCallCheck(this, ReadableStreamDefaultReader);
+
+ if (IsReadableStream(stream) === false) {
+ throw new TypeError('ReadableStreamDefaultReader can only be constructed with a ReadableStream instance');
+ }
+
+ if (IsReadableStreamLocked(stream) === true) {
+ throw new TypeError('This stream has already been locked for exclusive reading by another reader');
+ }
+
+ ReadableStreamReaderGenericInitialize(this, stream);
+ this._readRequests = [];
+ }
+
+ _createClass(ReadableStreamDefaultReader, [{
+ key: 'cancel',
+ value: function cancel(reason) {
+ if (IsReadableStreamDefaultReader(this) === false) {
+ return Promise.reject(defaultReaderBrandCheckException('cancel'));
+ }
+
+ if (this._ownerReadableStream === undefined) {
+ return Promise.reject(readerLockException('cancel'));
+ }
+
+ return ReadableStreamReaderGenericCancel(this, reason);
+ }
+ }, {
+ key: 'read',
+ value: function read() {
+ if (IsReadableStreamDefaultReader(this) === false) {
+ return Promise.reject(defaultReaderBrandCheckException('read'));
+ }
+
+ if (this._ownerReadableStream === undefined) {
+ return Promise.reject(readerLockException('read from'));
+ }
+
+ return ReadableStreamDefaultReaderRead(this);
+ }
+ }, {
+ key: 'releaseLock',
+ value: function releaseLock() {
+ if (IsReadableStreamDefaultReader(this) === false) {
+ throw defaultReaderBrandCheckException('releaseLock');
+ }
+
+ if (this._ownerReadableStream === undefined) {
+ return;
+ }
+
+ if (this._readRequests.length > 0) {
+ throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled');
+ }
+
+ ReadableStreamReaderGenericRelease(this);
+ }
+ }, {
+ key: 'closed',
+ get: function get() {
+ if (IsReadableStreamDefaultReader(this) === false) {
+ return Promise.reject(defaultReaderBrandCheckException('closed'));
+ }
+
+ return this._closedPromise;
+ }
+ }]);
+
+ return ReadableStreamDefaultReader;
+ }();
+
+ var ReadableStreamBYOBReader = function () {
+ function ReadableStreamBYOBReader(stream) {
+ _classCallCheck(this, ReadableStreamBYOBReader);
+
+ if (!IsReadableStream(stream)) {
+ throw new TypeError('ReadableStreamBYOBReader can only be constructed with a ReadableStream instance given a ' + 'byte source');
+ }
+
+ if (IsReadableByteStreamController(stream._readableStreamController) === false) {
+ throw new TypeError('Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte ' + 'source');
+ }
+
+ if (IsReadableStreamLocked(stream)) {
+ throw new TypeError('This stream has already been locked for exclusive reading by another reader');
+ }
+
+ ReadableStreamReaderGenericInitialize(this, stream);
+ this._readIntoRequests = [];
+ }
+
+ _createClass(ReadableStreamBYOBReader, [{
+ key: 'cancel',
+ value: function cancel(reason) {
+ if (!IsReadableStreamBYOBReader(this)) {
+ return Promise.reject(byobReaderBrandCheckException('cancel'));
+ }
+
+ if (this._ownerReadableStream === undefined) {
+ return Promise.reject(readerLockException('cancel'));
+ }
+
+ return ReadableStreamReaderGenericCancel(this, reason);
+ }
+ }, {
+ key: 'read',
+ value: function read(view) {
+ if (!IsReadableStreamBYOBReader(this)) {
+ return Promise.reject(byobReaderBrandCheckException('read'));
+ }
+
+ if (this._ownerReadableStream === undefined) {
+ return Promise.reject(readerLockException('read from'));
+ }
+
+ if (!ArrayBuffer.isView(view)) {
+ return Promise.reject(new TypeError('view must be an array buffer view'));
+ }
+
+ if (view.byteLength === 0) {
+ return Promise.reject(new TypeError('view must have non-zero byteLength'));
+ }
+
+ return ReadableStreamBYOBReaderRead(this, view);
+ }
+ }, {
+ key: 'releaseLock',
+ value: function releaseLock() {
+ if (!IsReadableStreamBYOBReader(this)) {
+ throw byobReaderBrandCheckException('releaseLock');
+ }
+
+ if (this._ownerReadableStream === undefined) {
+ return;
+ }
+
+ if (this._readIntoRequests.length > 0) {
+ throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled');
+ }
+
+ ReadableStreamReaderGenericRelease(this);
+ }
+ }, {
+ key: 'closed',
+ get: function get() {
+ if (!IsReadableStreamBYOBReader(this)) {
+ return Promise.reject(byobReaderBrandCheckException('closed'));
+ }
+
+ return this._closedPromise;
+ }
+ }]);
+
+ return ReadableStreamBYOBReader;
+ }();
+
+ function IsReadableStreamBYOBReader(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_readIntoRequests')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function IsReadableStreamDefaultReader(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_readRequests')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function ReadableStreamReaderGenericInitialize(reader, stream) {
+ reader._ownerReadableStream = stream;
+ stream._reader = reader;
+
+ if (stream._state === 'readable') {
+ defaultReaderClosedPromiseInitialize(reader);
+ } else if (stream._state === 'closed') {
+ defaultReaderClosedPromiseInitializeAsResolved(reader);
+ } else {
+ assert(stream._state === 'errored', 'state must be errored');
+ defaultReaderClosedPromiseInitializeAsRejected(reader, stream._storedError);
+
+ reader._closedPromise["catch"](function () {});
+ }
+ }
+
+ function ReadableStreamReaderGenericCancel(reader, reason) {
+ var stream = reader._ownerReadableStream;
+ assert(stream !== undefined);
+ return ReadableStreamCancel(stream, reason);
+ }
+
+ function ReadableStreamReaderGenericRelease(reader) {
+ assert(reader._ownerReadableStream !== undefined);
+ assert(reader._ownerReadableStream._reader === reader);
+
+ if (reader._ownerReadableStream._state === 'readable') {
+ defaultReaderClosedPromiseReject(reader, new TypeError('Reader was released and can no longer be used to monitor the stream\'s closedness'));
+ } else {
+ defaultReaderClosedPromiseResetToRejected(reader, new TypeError('Reader was released and can no longer be used to monitor the stream\'s closedness'));
+ }
+
+ reader._closedPromise["catch"](function () {});
+
+ reader._ownerReadableStream._reader = undefined;
+ reader._ownerReadableStream = undefined;
+ }
+
+ function ReadableStreamBYOBReaderRead(reader, view) {
+ var stream = reader._ownerReadableStream;
+ assert(stream !== undefined);
+ stream._disturbed = true;
+
+ if (stream._state === 'errored') {
+ return Promise.reject(stream._storedError);
+ }
+
+ return ReadableByteStreamControllerPullInto(stream._readableStreamController, view);
+ }
+
+ function ReadableStreamDefaultReaderRead(reader) {
+ var stream = reader._ownerReadableStream;
+ assert(stream !== undefined);
+ stream._disturbed = true;
+
+ if (stream._state === 'closed') {
+ return Promise.resolve(CreateIterResultObject(undefined, true));
+ }
+
+ if (stream._state === 'errored') {
+ return Promise.reject(stream._storedError);
+ }
+
+ assert(stream._state === 'readable');
+ return stream._readableStreamController.__pullSteps();
+ }
+
+ var ReadableStreamDefaultController = function () {
+ function ReadableStreamDefaultController(stream, underlyingSource, size, highWaterMark) {
+ _classCallCheck(this, ReadableStreamDefaultController);
+
+ if (IsReadableStream(stream) === false) {
+ throw new TypeError('ReadableStreamDefaultController can only be constructed with a ReadableStream instance');
+ }
+
+ if (stream._readableStreamController !== undefined) {
+ throw new TypeError('ReadableStreamDefaultController instances can only be created by the ReadableStream constructor');
+ }
+
+ this._controlledReadableStream = stream;
+ this._underlyingSource = underlyingSource;
+ this._queue = undefined;
+ this._queueTotalSize = undefined;
+ ResetQueue(this);
+ this._started = false;
+ this._closeRequested = false;
+ this._pullAgain = false;
+ this._pulling = false;
+ var normalizedStrategy = ValidateAndNormalizeQueuingStrategy(size, highWaterMark);
+ this._strategySize = normalizedStrategy.size;
+ this._strategyHWM = normalizedStrategy.highWaterMark;
+ var controller = this;
+ var startResult = InvokeOrNoop(underlyingSource, 'start', [this]);
+ Promise.resolve(startResult).then(function () {
+ controller._started = true;
+ assert(controller._pulling === false);
+ assert(controller._pullAgain === false);
+ ReadableStreamDefaultControllerCallPullIfNeeded(controller);
+ }, function (r) {
+ ReadableStreamDefaultControllerErrorIfNeeded(controller, r);
+ })["catch"](rethrowAssertionErrorRejection);
+ }
+
+ _createClass(ReadableStreamDefaultController, [{
+ key: 'close',
+ value: function close() {
+ if (IsReadableStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('close');
+ }
+
+ if (this._closeRequested === true) {
+ throw new TypeError('The stream has already been closed; do not close it again!');
+ }
+
+ var state = this._controlledReadableStream._state;
+
+ if (state !== 'readable') {
+ throw new TypeError('The stream (in ' + state + ' state) is not in the readable state and cannot be closed');
+ }
+
+ ReadableStreamDefaultControllerClose(this);
+ }
+ }, {
+ key: 'enqueue',
+ value: function enqueue(chunk) {
+ if (IsReadableStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('enqueue');
+ }
+
+ if (this._closeRequested === true) {
+ throw new TypeError('stream is closed or draining');
+ }
+
+ var state = this._controlledReadableStream._state;
+
+ if (state !== 'readable') {
+ throw new TypeError('The stream (in ' + state + ' state) is not in the readable state and cannot be enqueued to');
+ }
+
+ return ReadableStreamDefaultControllerEnqueue(this, chunk);
+ }
+ }, {
+ key: 'error',
+ value: function error(e) {
+ if (IsReadableStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('error');
+ }
+
+ var stream = this._controlledReadableStream;
+
+ if (stream._state !== 'readable') {
+ throw new TypeError('The stream is ' + stream._state + ' and so cannot be errored');
+ }
+
+ ReadableStreamDefaultControllerError(this, e);
+ }
+ }, {
+ key: '__cancelSteps',
+ value: function __cancelSteps(reason) {
+ ResetQueue(this);
+ return PromiseInvokeOrNoop(this._underlyingSource, 'cancel', [reason]);
+ }
+ }, {
+ key: '__pullSteps',
+ value: function __pullSteps() {
+ var stream = this._controlledReadableStream;
+
+ if (this._queue.length > 0) {
+ var chunk = DequeueValue(this);
+
+ if (this._closeRequested === true && this._queue.length === 0) {
+ ReadableStreamClose(stream);
+ } else {
+ ReadableStreamDefaultControllerCallPullIfNeeded(this);
+ }
+
+ return Promise.resolve(CreateIterResultObject(chunk, false));
+ }
+
+ var pendingPromise = ReadableStreamAddReadRequest(stream);
+ ReadableStreamDefaultControllerCallPullIfNeeded(this);
+ return pendingPromise;
+ }
+ }, {
+ key: 'desiredSize',
+ get: function get() {
+ if (IsReadableStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('desiredSize');
+ }
+
+ return ReadableStreamDefaultControllerGetDesiredSize(this);
+ }
+ }]);
+
+ return ReadableStreamDefaultController;
+ }();
+
+ function IsReadableStreamDefaultController(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_underlyingSource')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function ReadableStreamDefaultControllerCallPullIfNeeded(controller) {
+ var shouldPull = ReadableStreamDefaultControllerShouldCallPull(controller);
+
+ if (shouldPull === false) {
+ return undefined;
+ }
+
+ if (controller._pulling === true) {
+ controller._pullAgain = true;
+ return undefined;
+ }
+
+ assert(controller._pullAgain === false);
+ controller._pulling = true;
+ var pullPromise = PromiseInvokeOrNoop(controller._underlyingSource, 'pull', [controller]);
+ pullPromise.then(function () {
+ controller._pulling = false;
+
+ if (controller._pullAgain === true) {
+ controller._pullAgain = false;
+ return ReadableStreamDefaultControllerCallPullIfNeeded(controller);
+ }
+
+ return undefined;
+ }, function (e) {
+ ReadableStreamDefaultControllerErrorIfNeeded(controller, e);
+ })["catch"](rethrowAssertionErrorRejection);
+ return undefined;
+ }
+
+ function ReadableStreamDefaultControllerShouldCallPull(controller) {
+ var stream = controller._controlledReadableStream;
+
+ if (stream._state === 'closed' || stream._state === 'errored') {
+ return false;
+ }
+
+ if (controller._closeRequested === true) {
+ return false;
+ }
+
+ if (controller._started === false) {
+ return false;
+ }
+
+ if (IsReadableStreamLocked(stream) === true && ReadableStreamGetNumReadRequests(stream) > 0) {
+ return true;
+ }
+
+ var desiredSize = ReadableStreamDefaultControllerGetDesiredSize(controller);
+
+ if (desiredSize > 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function ReadableStreamDefaultControllerClose(controller) {
+ var stream = controller._controlledReadableStream;
+ assert(controller._closeRequested === false);
+ assert(stream._state === 'readable');
+ controller._closeRequested = true;
+
+ if (controller._queue.length === 0) {
+ ReadableStreamClose(stream);
+ }
+ }
+
+ function ReadableStreamDefaultControllerEnqueue(controller, chunk) {
+ var stream = controller._controlledReadableStream;
+ assert(controller._closeRequested === false);
+ assert(stream._state === 'readable');
+
+ if (IsReadableStreamLocked(stream) === true && ReadableStreamGetNumReadRequests(stream) > 0) {
+ ReadableStreamFulfillReadRequest(stream, chunk, false);
+ } else {
+ var chunkSize = 1;
+
+ if (controller._strategySize !== undefined) {
+ var strategySize = controller._strategySize;
+
+ try {
+ chunkSize = strategySize(chunk);
+ } catch (chunkSizeE) {
+ ReadableStreamDefaultControllerErrorIfNeeded(controller, chunkSizeE);
+ throw chunkSizeE;
+ }
+ }
+
+ try {
+ EnqueueValueWithSize(controller, chunk, chunkSize);
+ } catch (enqueueE) {
+ ReadableStreamDefaultControllerErrorIfNeeded(controller, enqueueE);
+ throw enqueueE;
+ }
+ }
+
+ ReadableStreamDefaultControllerCallPullIfNeeded(controller);
+ return undefined;
+ }
+
+ function ReadableStreamDefaultControllerError(controller, e) {
+ var stream = controller._controlledReadableStream;
+ assert(stream._state === 'readable');
+ ResetQueue(controller);
+ ReadableStreamError(stream, e);
+ }
+
+ function ReadableStreamDefaultControllerErrorIfNeeded(controller, e) {
+ if (controller._controlledReadableStream._state === 'readable') {
+ ReadableStreamDefaultControllerError(controller, e);
+ }
+ }
+
+ function ReadableStreamDefaultControllerGetDesiredSize(controller) {
+ var stream = controller._controlledReadableStream;
+ var state = stream._state;
+
+ if (state === 'errored') {
+ return null;
+ }
+
+ if (state === 'closed') {
+ return 0;
+ }
+
+ return controller._strategyHWM - controller._queueTotalSize;
+ }
+
+ var ReadableStreamBYOBRequest = function () {
+ function ReadableStreamBYOBRequest(controller, view) {
+ _classCallCheck(this, ReadableStreamBYOBRequest);
+
+ this._associatedReadableByteStreamController = controller;
+ this._view = view;
+ }
+
+ _createClass(ReadableStreamBYOBRequest, [{
+ key: 'respond',
+ value: function respond(bytesWritten) {
+ if (IsReadableStreamBYOBRequest(this) === false) {
+ throw byobRequestBrandCheckException('respond');
+ }
+
+ if (this._associatedReadableByteStreamController === undefined) {
+ throw new TypeError('This BYOB request has been invalidated');
+ }
+
+ ReadableByteStreamControllerRespond(this._associatedReadableByteStreamController, bytesWritten);
+ }
+ }, {
+ key: 'respondWithNewView',
+ value: function respondWithNewView(view) {
+ if (IsReadableStreamBYOBRequest(this) === false) {
+ throw byobRequestBrandCheckException('respond');
+ }
+
+ if (this._associatedReadableByteStreamController === undefined) {
+ throw new TypeError('This BYOB request has been invalidated');
+ }
+
+ if (!ArrayBuffer.isView(view)) {
+ throw new TypeError('You can only respond with array buffer views');
+ }
+
+ ReadableByteStreamControllerRespondWithNewView(this._associatedReadableByteStreamController, view);
+ }
+ }, {
+ key: 'view',
+ get: function get() {
+ return this._view;
+ }
+ }]);
+
+ return ReadableStreamBYOBRequest;
+ }();
+
+ var ReadableByteStreamController = function () {
+ function ReadableByteStreamController(stream, underlyingByteSource, highWaterMark) {
+ _classCallCheck(this, ReadableByteStreamController);
+
+ if (IsReadableStream(stream) === false) {
+ throw new TypeError('ReadableByteStreamController can only be constructed with a ReadableStream instance given ' + 'a byte source');
+ }
+
+ if (stream._readableStreamController !== undefined) {
+ throw new TypeError('ReadableByteStreamController instances can only be created by the ReadableStream constructor given a byte ' + 'source');
+ }
+
+ this._controlledReadableStream = stream;
+ this._underlyingByteSource = underlyingByteSource;
+ this._pullAgain = false;
+ this._pulling = false;
+ ReadableByteStreamControllerClearPendingPullIntos(this);
+ this._queue = this._queueTotalSize = undefined;
+ ResetQueue(this);
+ this._closeRequested = false;
+ this._started = false;
+ this._strategyHWM = ValidateAndNormalizeHighWaterMark(highWaterMark);
+ var autoAllocateChunkSize = underlyingByteSource.autoAllocateChunkSize;
+
+ if (autoAllocateChunkSize !== undefined) {
+ if (Number.isInteger(autoAllocateChunkSize) === false || autoAllocateChunkSize <= 0) {
+ throw new RangeError('autoAllocateChunkSize must be a positive integer');
+ }
+ }
+
+ this._autoAllocateChunkSize = autoAllocateChunkSize;
+ this._pendingPullIntos = [];
+ var controller = this;
+ var startResult = InvokeOrNoop(underlyingByteSource, 'start', [this]);
+ Promise.resolve(startResult).then(function () {
+ controller._started = true;
+ assert(controller._pulling === false);
+ assert(controller._pullAgain === false);
+ ReadableByteStreamControllerCallPullIfNeeded(controller);
+ }, function (r) {
+ if (stream._state === 'readable') {
+ ReadableByteStreamControllerError(controller, r);
+ }
+ })["catch"](rethrowAssertionErrorRejection);
+ }
+
+ _createClass(ReadableByteStreamController, [{
+ key: 'close',
+ value: function close() {
+ if (IsReadableByteStreamController(this) === false) {
+ throw byteStreamControllerBrandCheckException('close');
+ }
+
+ if (this._closeRequested === true) {
+ throw new TypeError('The stream has already been closed; do not close it again!');
+ }
+
+ var state = this._controlledReadableStream._state;
+
+ if (state !== 'readable') {
+ throw new TypeError('The stream (in ' + state + ' state) is not in the readable state and cannot be closed');
+ }
+
+ ReadableByteStreamControllerClose(this);
+ }
+ }, {
+ key: 'enqueue',
+ value: function enqueue(chunk) {
+ if (IsReadableByteStreamController(this) === false) {
+ throw byteStreamControllerBrandCheckException('enqueue');
+ }
+
+ if (this._closeRequested === true) {
+ throw new TypeError('stream is closed or draining');
+ }
+
+ var state = this._controlledReadableStream._state;
+
+ if (state !== 'readable') {
+ throw new TypeError('The stream (in ' + state + ' state) is not in the readable state and cannot be enqueued to');
+ }
+
+ if (!ArrayBuffer.isView(chunk)) {
+ throw new TypeError('You can only enqueue array buffer views when using a ReadableByteStreamController');
+ }
+
+ ReadableByteStreamControllerEnqueue(this, chunk);
+ }
+ }, {
+ key: 'error',
+ value: function error(e) {
+ if (IsReadableByteStreamController(this) === false) {
+ throw byteStreamControllerBrandCheckException('error');
+ }
+
+ var stream = this._controlledReadableStream;
+
+ if (stream._state !== 'readable') {
+ throw new TypeError('The stream is ' + stream._state + ' and so cannot be errored');
+ }
+
+ ReadableByteStreamControllerError(this, e);
+ }
+ }, {
+ key: '__cancelSteps',
+ value: function __cancelSteps(reason) {
+ if (this._pendingPullIntos.length > 0) {
+ var firstDescriptor = this._pendingPullIntos[0];
+ firstDescriptor.bytesFilled = 0;
+ }
+
+ ResetQueue(this);
+ return PromiseInvokeOrNoop(this._underlyingByteSource, 'cancel', [reason]);
+ }
+ }, {
+ key: '__pullSteps',
+ value: function __pullSteps() {
+ var stream = this._controlledReadableStream;
+ assert(ReadableStreamHasDefaultReader(stream) === true);
+
+ if (this._queueTotalSize > 0) {
+ assert(ReadableStreamGetNumReadRequests(stream) === 0);
+
+ var entry = this._queue.shift();
+
+ this._queueTotalSize -= entry.byteLength;
+ ReadableByteStreamControllerHandleQueueDrain(this);
+ var view = void 0;
+
+ try {
+ view = new Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength);
+ } catch (viewE) {
+ return Promise.reject(viewE);
+ }
+
+ return Promise.resolve(CreateIterResultObject(view, false));
+ }
+
+ var autoAllocateChunkSize = this._autoAllocateChunkSize;
+
+ if (autoAllocateChunkSize !== undefined) {
+ var buffer = void 0;
+
+ try {
+ buffer = new ArrayBuffer(autoAllocateChunkSize);
+ } catch (bufferE) {
+ return Promise.reject(bufferE);
+ }
+
+ var pullIntoDescriptor = {
+ buffer: buffer,
+ byteOffset: 0,
+ byteLength: autoAllocateChunkSize,
+ bytesFilled: 0,
+ elementSize: 1,
+ ctor: Uint8Array,
+ readerType: 'default'
+ };
+
+ this._pendingPullIntos.push(pullIntoDescriptor);
+ }
+
+ var promise = ReadableStreamAddReadRequest(stream);
+ ReadableByteStreamControllerCallPullIfNeeded(this);
+ return promise;
+ }
+ }, {
+ key: 'byobRequest',
+ get: function get() {
+ if (IsReadableByteStreamController(this) === false) {
+ throw byteStreamControllerBrandCheckException('byobRequest');
+ }
+
+ if (this._byobRequest === undefined && this._pendingPullIntos.length > 0) {
+ var firstDescriptor = this._pendingPullIntos[0];
+ var view = new Uint8Array(firstDescriptor.buffer, firstDescriptor.byteOffset + firstDescriptor.bytesFilled, firstDescriptor.byteLength - firstDescriptor.bytesFilled);
+ this._byobRequest = new ReadableStreamBYOBRequest(this, view);
+ }
+
+ return this._byobRequest;
+ }
+ }, {
+ key: 'desiredSize',
+ get: function get() {
+ if (IsReadableByteStreamController(this) === false) {
+ throw byteStreamControllerBrandCheckException('desiredSize');
+ }
+
+ return ReadableByteStreamControllerGetDesiredSize(this);
+ }
+ }]);
+
+ return ReadableByteStreamController;
+ }();
+
+ function IsReadableByteStreamController(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_underlyingByteSource')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function IsReadableStreamBYOBRequest(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_associatedReadableByteStreamController')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function ReadableByteStreamControllerCallPullIfNeeded(controller) {
+ var shouldPull = ReadableByteStreamControllerShouldCallPull(controller);
+
+ if (shouldPull === false) {
+ return undefined;
+ }
+
+ if (controller._pulling === true) {
+ controller._pullAgain = true;
+ return undefined;
+ }
+
+ assert(controller._pullAgain === false);
+ controller._pulling = true;
+ var pullPromise = PromiseInvokeOrNoop(controller._underlyingByteSource, 'pull', [controller]);
+ pullPromise.then(function () {
+ controller._pulling = false;
+
+ if (controller._pullAgain === true) {
+ controller._pullAgain = false;
+ ReadableByteStreamControllerCallPullIfNeeded(controller);
+ }
+ }, function (e) {
+ if (controller._controlledReadableStream._state === 'readable') {
+ ReadableByteStreamControllerError(controller, e);
+ }
+ })["catch"](rethrowAssertionErrorRejection);
+ return undefined;
+ }
+
+ function ReadableByteStreamControllerClearPendingPullIntos(controller) {
+ ReadableByteStreamControllerInvalidateBYOBRequest(controller);
+ controller._pendingPullIntos = [];
+ }
+
+ function ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor) {
+ assert(stream._state !== 'errored', 'state must not be errored');
+ var done = false;
+
+ if (stream._state === 'closed') {
+ assert(pullIntoDescriptor.bytesFilled === 0);
+ done = true;
+ }
+
+ var filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor);
+
+ if (pullIntoDescriptor.readerType === 'default') {
+ ReadableStreamFulfillReadRequest(stream, filledView, done);
+ } else {
+ assert(pullIntoDescriptor.readerType === 'byob');
+ ReadableStreamFulfillReadIntoRequest(stream, filledView, done);
+ }
+ }
+
+ function ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor) {
+ var bytesFilled = pullIntoDescriptor.bytesFilled;
+ var elementSize = pullIntoDescriptor.elementSize;
+ assert(bytesFilled <= pullIntoDescriptor.byteLength);
+ assert(bytesFilled % elementSize === 0);
+ return new pullIntoDescriptor.ctor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, bytesFilled / elementSize);
+ }
+
+ function ReadableByteStreamControllerEnqueueChunkToQueue(controller, buffer, byteOffset, byteLength) {
+ controller._queue.push({
+ buffer: buffer,
+ byteOffset: byteOffset,
+ byteLength: byteLength
+ });
+
+ controller._queueTotalSize += byteLength;
+ }
+
+ function ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) {
+ var elementSize = pullIntoDescriptor.elementSize;
+ var currentAlignedBytes = pullIntoDescriptor.bytesFilled - pullIntoDescriptor.bytesFilled % elementSize;
+ var maxBytesToCopy = Math.min(controller._queueTotalSize, pullIntoDescriptor.byteLength - pullIntoDescriptor.bytesFilled);
+ var maxBytesFilled = pullIntoDescriptor.bytesFilled + maxBytesToCopy;
+ var maxAlignedBytes = maxBytesFilled - maxBytesFilled % elementSize;
+ var totalBytesToCopyRemaining = maxBytesToCopy;
+ var ready = false;
+
+ if (maxAlignedBytes > currentAlignedBytes) {
+ totalBytesToCopyRemaining = maxAlignedBytes - pullIntoDescriptor.bytesFilled;
+ ready = true;
+ }
+
+ var queue = controller._queue;
+
+ while (totalBytesToCopyRemaining > 0) {
+ var headOfQueue = queue[0];
+ var bytesToCopy = Math.min(totalBytesToCopyRemaining, headOfQueue.byteLength);
+ var destStart = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled;
+ ArrayBufferCopy(pullIntoDescriptor.buffer, destStart, headOfQueue.buffer, headOfQueue.byteOffset, bytesToCopy);
+
+ if (headOfQueue.byteLength === bytesToCopy) {
+ queue.shift();
+ } else {
+ headOfQueue.byteOffset += bytesToCopy;
+ headOfQueue.byteLength -= bytesToCopy;
+ }
+
+ controller._queueTotalSize -= bytesToCopy;
+ ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesToCopy, pullIntoDescriptor);
+ totalBytesToCopyRemaining -= bytesToCopy;
+ }
+
+ if (ready === false) {
+ assert(controller._queueTotalSize === 0, 'queue must be empty');
+ assert(pullIntoDescriptor.bytesFilled > 0);
+ assert(pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize);
+ }
+
+ return ready;
+ }
+
+ function ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, size, pullIntoDescriptor) {
+ assert(controller._pendingPullIntos.length === 0 || controller._pendingPullIntos[0] === pullIntoDescriptor);
+ ReadableByteStreamControllerInvalidateBYOBRequest(controller);
+ pullIntoDescriptor.bytesFilled += size;
+ }
+
+ function ReadableByteStreamControllerHandleQueueDrain(controller) {
+ assert(controller._controlledReadableStream._state === 'readable');
+
+ if (controller._queueTotalSize === 0 && controller._closeRequested === true) {
+ ReadableStreamClose(controller._controlledReadableStream);
+ } else {
+ ReadableByteStreamControllerCallPullIfNeeded(controller);
+ }
+ }
+
+ function ReadableByteStreamControllerInvalidateBYOBRequest(controller) {
+ if (controller._byobRequest === undefined) {
+ return;
+ }
+
+ controller._byobRequest._associatedReadableByteStreamController = undefined;
+ controller._byobRequest._view = undefined;
+ controller._byobRequest = undefined;
+ }
+
+ function ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller) {
+ assert(controller._closeRequested === false);
+
+ while (controller._pendingPullIntos.length > 0) {
+ if (controller._queueTotalSize === 0) {
+ return;
+ }
+
+ var pullIntoDescriptor = controller._pendingPullIntos[0];
+
+ if (ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) === true) {
+ ReadableByteStreamControllerShiftPendingPullInto(controller);
+ ReadableByteStreamControllerCommitPullIntoDescriptor(controller._controlledReadableStream, pullIntoDescriptor);
+ }
+ }
+ }
+
+ function ReadableByteStreamControllerPullInto(controller, view) {
+ var stream = controller._controlledReadableStream;
+ var elementSize = 1;
+
+ if (view.constructor !== DataView) {
+ elementSize = view.constructor.BYTES_PER_ELEMENT;
+ }
+
+ var ctor = view.constructor;
+ var pullIntoDescriptor = {
+ buffer: view.buffer,
+ byteOffset: view.byteOffset,
+ byteLength: view.byteLength,
+ bytesFilled: 0,
+ elementSize: elementSize,
+ ctor: ctor,
+ readerType: 'byob'
+ };
+
+ if (controller._pendingPullIntos.length > 0) {
+ pullIntoDescriptor.buffer = TransferArrayBuffer(pullIntoDescriptor.buffer);
+
+ controller._pendingPullIntos.push(pullIntoDescriptor);
+
+ return ReadableStreamAddReadIntoRequest(stream);
+ }
+
+ if (stream._state === 'closed') {
+ var emptyView = new view.constructor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, 0);
+ return Promise.resolve(CreateIterResultObject(emptyView, true));
+ }
+
+ if (controller._queueTotalSize > 0) {
+ if (ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) === true) {
+ var filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor);
+ ReadableByteStreamControllerHandleQueueDrain(controller);
+ return Promise.resolve(CreateIterResultObject(filledView, false));
+ }
+
+ if (controller._closeRequested === true) {
+ var e = new TypeError('Insufficient bytes to fill elements in the given buffer');
+ ReadableByteStreamControllerError(controller, e);
+ return Promise.reject(e);
+ }
+ }
+
+ pullIntoDescriptor.buffer = TransferArrayBuffer(pullIntoDescriptor.buffer);
+
+ controller._pendingPullIntos.push(pullIntoDescriptor);
+
+ var promise = ReadableStreamAddReadIntoRequest(stream);
+ ReadableByteStreamControllerCallPullIfNeeded(controller);
+ return promise;
+ }
+
+ function ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor) {
+ firstDescriptor.buffer = TransferArrayBuffer(firstDescriptor.buffer);
+ assert(firstDescriptor.bytesFilled === 0, 'bytesFilled must be 0');
+ var stream = controller._controlledReadableStream;
+
+ if (ReadableStreamHasBYOBReader(stream) === true) {
+ while (ReadableStreamGetNumReadIntoRequests(stream) > 0) {
+ var pullIntoDescriptor = ReadableByteStreamControllerShiftPendingPullInto(controller);
+ ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor);
+ }
+ }
+ }
+
+ function ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, pullIntoDescriptor) {
+ if (pullIntoDescriptor.bytesFilled + bytesWritten > pullIntoDescriptor.byteLength) {
+ throw new RangeError('bytesWritten out of range');
+ }
+
+ ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesWritten, pullIntoDescriptor);
+
+ if (pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize) {
+ return;
+ }
+
+ ReadableByteStreamControllerShiftPendingPullInto(controller);
+ var remainderSize = pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize;
+
+ if (remainderSize > 0) {
+ var end = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled;
+ var remainder = pullIntoDescriptor.buffer.slice(end - remainderSize, end);
+ ReadableByteStreamControllerEnqueueChunkToQueue(controller, remainder, 0, remainder.byteLength);
+ }
+
+ pullIntoDescriptor.buffer = TransferArrayBuffer(pullIntoDescriptor.buffer);
+ pullIntoDescriptor.bytesFilled -= remainderSize;
+ ReadableByteStreamControllerCommitPullIntoDescriptor(controller._controlledReadableStream, pullIntoDescriptor);
+ ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);
+ }
+
+ function ReadableByteStreamControllerRespondInternal(controller, bytesWritten) {
+ var firstDescriptor = controller._pendingPullIntos[0];
+ var stream = controller._controlledReadableStream;
+
+ if (stream._state === 'closed') {
+ if (bytesWritten !== 0) {
+ throw new TypeError('bytesWritten must be 0 when calling respond() on a closed stream');
+ }
+
+ ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor);
+ } else {
+ assert(stream._state === 'readable');
+ ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, firstDescriptor);
+ }
+ }
+
+ function ReadableByteStreamControllerShiftPendingPullInto(controller) {
+ var descriptor = controller._pendingPullIntos.shift();
+
+ ReadableByteStreamControllerInvalidateBYOBRequest(controller);
+ return descriptor;
+ }
+
+ function ReadableByteStreamControllerShouldCallPull(controller) {
+ var stream = controller._controlledReadableStream;
+
+ if (stream._state !== 'readable') {
+ return false;
+ }
+
+ if (controller._closeRequested === true) {
+ return false;
+ }
+
+ if (controller._started === false) {
+ return false;
+ }
+
+ if (ReadableStreamHasDefaultReader(stream) === true && ReadableStreamGetNumReadRequests(stream) > 0) {
+ return true;
+ }
+
+ if (ReadableStreamHasBYOBReader(stream) === true && ReadableStreamGetNumReadIntoRequests(stream) > 0) {
+ return true;
+ }
+
+ if (ReadableByteStreamControllerGetDesiredSize(controller) > 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function ReadableByteStreamControllerClose(controller) {
+ var stream = controller._controlledReadableStream;
+ assert(controller._closeRequested === false);
+ assert(stream._state === 'readable');
+
+ if (controller._queueTotalSize > 0) {
+ controller._closeRequested = true;
+ return;
+ }
+
+ if (controller._pendingPullIntos.length > 0) {
+ var firstPendingPullInto = controller._pendingPullIntos[0];
+
+ if (firstPendingPullInto.bytesFilled > 0) {
+ var e = new TypeError('Insufficient bytes to fill elements in the given buffer');
+ ReadableByteStreamControllerError(controller, e);
+ throw e;
+ }
+ }
+
+ ReadableStreamClose(stream);
+ }
+
+ function ReadableByteStreamControllerEnqueue(controller, chunk) {
+ var stream = controller._controlledReadableStream;
+ assert(controller._closeRequested === false);
+ assert(stream._state === 'readable');
+ var buffer = chunk.buffer;
+ var byteOffset = chunk.byteOffset;
+ var byteLength = chunk.byteLength;
+ var transferredBuffer = TransferArrayBuffer(buffer);
+
+ if (ReadableStreamHasDefaultReader(stream) === true) {
+ if (ReadableStreamGetNumReadRequests(stream) === 0) {
+ ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength);
+ } else {
+ assert(controller._queue.length === 0);
+ var transferredView = new Uint8Array(transferredBuffer, byteOffset, byteLength);
+ ReadableStreamFulfillReadRequest(stream, transferredView, false);
+ }
+ } else if (ReadableStreamHasBYOBReader(stream) === true) {
+ ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength);
+ ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);
+ } else {
+ assert(IsReadableStreamLocked(stream) === false, 'stream must not be locked');
+ ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength);
+ }
+ }
+
+ function ReadableByteStreamControllerError(controller, e) {
+ var stream = controller._controlledReadableStream;
+ assert(stream._state === 'readable');
+ ReadableByteStreamControllerClearPendingPullIntos(controller);
+ ResetQueue(controller);
+ ReadableStreamError(stream, e);
+ }
+
+ function ReadableByteStreamControllerGetDesiredSize(controller) {
+ var stream = controller._controlledReadableStream;
+ var state = stream._state;
+
+ if (state === 'errored') {
+ return null;
+ }
+
+ if (state === 'closed') {
+ return 0;
+ }
+
+ return controller._strategyHWM - controller._queueTotalSize;
+ }
+
+ function ReadableByteStreamControllerRespond(controller, bytesWritten) {
+ bytesWritten = Number(bytesWritten);
+
+ if (IsFiniteNonNegativeNumber(bytesWritten) === false) {
+ throw new RangeError('bytesWritten must be a finite');
+ }
+
+ assert(controller._pendingPullIntos.length > 0);
+ ReadableByteStreamControllerRespondInternal(controller, bytesWritten);
+ }
+
+ function ReadableByteStreamControllerRespondWithNewView(controller, view) {
+ assert(controller._pendingPullIntos.length > 0);
+ var firstDescriptor = controller._pendingPullIntos[0];
+
+ if (firstDescriptor.byteOffset + firstDescriptor.bytesFilled !== view.byteOffset) {
+ throw new RangeError('The region specified by view does not match byobRequest');
+ }
+
+ if (firstDescriptor.byteLength !== view.byteLength) {
+ throw new RangeError('The buffer of view has different capacity than byobRequest');
+ }
+
+ firstDescriptor.buffer = view.buffer;
+ ReadableByteStreamControllerRespondInternal(controller, view.byteLength);
+ }
+
+ function streamBrandCheckException(name) {
+ return new TypeError('ReadableStream.prototype.' + name + ' can only be used on a ReadableStream');
+ }
+
+ function readerLockException(name) {
+ return new TypeError('Cannot ' + name + ' a stream using a released reader');
+ }
+
+ function defaultReaderBrandCheckException(name) {
+ return new TypeError('ReadableStreamDefaultReader.prototype.' + name + ' can only be used on a ReadableStreamDefaultReader');
+ }
+
+ function defaultReaderClosedPromiseInitialize(reader) {
+ reader._closedPromise = new Promise(function (resolve, reject) {
+ reader._closedPromise_resolve = resolve;
+ reader._closedPromise_reject = reject;
+ });
+ }
+
+ function defaultReaderClosedPromiseInitializeAsRejected(reader, reason) {
+ reader._closedPromise = Promise.reject(reason);
+ reader._closedPromise_resolve = undefined;
+ reader._closedPromise_reject = undefined;
+ }
+
+ function defaultReaderClosedPromiseInitializeAsResolved(reader) {
+ reader._closedPromise = Promise.resolve(undefined);
+ reader._closedPromise_resolve = undefined;
+ reader._closedPromise_reject = undefined;
+ }
+
+ function defaultReaderClosedPromiseReject(reader, reason) {
+ assert(reader._closedPromise_resolve !== undefined);
+ assert(reader._closedPromise_reject !== undefined);
+
+ reader._closedPromise_reject(reason);
+
+ reader._closedPromise_resolve = undefined;
+ reader._closedPromise_reject = undefined;
+ }
+
+ function defaultReaderClosedPromiseResetToRejected(reader, reason) {
+ assert(reader._closedPromise_resolve === undefined);
+ assert(reader._closedPromise_reject === undefined);
+ reader._closedPromise = Promise.reject(reason);
+ }
+
+ function defaultReaderClosedPromiseResolve(reader) {
+ assert(reader._closedPromise_resolve !== undefined);
+ assert(reader._closedPromise_reject !== undefined);
+
+ reader._closedPromise_resolve(undefined);
+
+ reader._closedPromise_resolve = undefined;
+ reader._closedPromise_reject = undefined;
+ }
+
+ function byobReaderBrandCheckException(name) {
+ return new TypeError('ReadableStreamBYOBReader.prototype.' + name + ' can only be used on a ReadableStreamBYOBReader');
+ }
+
+ function defaultControllerBrandCheckException(name) {
+ return new TypeError('ReadableStreamDefaultController.prototype.' + name + ' can only be used on a ReadableStreamDefaultController');
+ }
+
+ function byobRequestBrandCheckException(name) {
+ return new TypeError('ReadableStreamBYOBRequest.prototype.' + name + ' can only be used on a ReadableStreamBYOBRequest');
+ }
+
+ function byteStreamControllerBrandCheckException(name) {
+ return new TypeError('ReadableByteStreamController.prototype.' + name + ' can only be used on a ReadableByteStreamController');
+ }
+
+ function ifIsObjectAndHasAPromiseIsHandledInternalSlotSetPromiseIsHandledToTrue(promise) {
+ try {
+ Promise.prototype.then.call(promise, undefined, function () {});
+ } catch (e) {}
+ }
+}, function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ var transformStream = __w_pdfjs_require__(6);
+
+ var readableStream = __w_pdfjs_require__(4);
+
+ var writableStream = __w_pdfjs_require__(2);
+
+ exports.TransformStream = transformStream.TransformStream;
+ exports.ReadableStream = readableStream.ReadableStream;
+ exports.IsReadableStreamDisturbed = readableStream.IsReadableStreamDisturbed;
+ exports.ReadableStreamDefaultControllerClose = readableStream.ReadableStreamDefaultControllerClose;
+ exports.ReadableStreamDefaultControllerEnqueue = readableStream.ReadableStreamDefaultControllerEnqueue;
+ exports.ReadableStreamDefaultControllerError = readableStream.ReadableStreamDefaultControllerError;
+ exports.ReadableStreamDefaultControllerGetDesiredSize = readableStream.ReadableStreamDefaultControllerGetDesiredSize;
+ exports.AcquireWritableStreamDefaultWriter = writableStream.AcquireWritableStreamDefaultWriter;
+ exports.IsWritableStream = writableStream.IsWritableStream;
+ exports.IsWritableStreamLocked = writableStream.IsWritableStreamLocked;
+ exports.WritableStream = writableStream.WritableStream;
+ exports.WritableStreamAbort = writableStream.WritableStreamAbort;
+ exports.WritableStreamDefaultControllerError = writableStream.WritableStreamDefaultControllerError;
+ exports.WritableStreamDefaultWriterCloseWithErrorPropagation = writableStream.WritableStreamDefaultWriterCloseWithErrorPropagation;
+ exports.WritableStreamDefaultWriterRelease = writableStream.WritableStreamDefaultWriterRelease;
+ exports.WritableStreamDefaultWriterWrite = writableStream.WritableStreamDefaultWriterWrite;
+}, function (module, exports, __w_pdfjs_require__) {
+ "use strict";
+
+ var _createClass = function () {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ return function (Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+ }();
+
+ function _classCallCheck(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ }
+
+ var _require = __w_pdfjs_require__(1),
+ assert = _require.assert;
+
+ var _require2 = __w_pdfjs_require__(0),
+ InvokeOrNoop = _require2.InvokeOrNoop,
+ PromiseInvokeOrPerformFallback = _require2.PromiseInvokeOrPerformFallback,
+ PromiseInvokeOrNoop = _require2.PromiseInvokeOrNoop,
+ typeIsObject = _require2.typeIsObject;
+
+ var _require3 = __w_pdfjs_require__(4),
+ ReadableStream = _require3.ReadableStream,
+ ReadableStreamDefaultControllerClose = _require3.ReadableStreamDefaultControllerClose,
+ ReadableStreamDefaultControllerEnqueue = _require3.ReadableStreamDefaultControllerEnqueue,
+ ReadableStreamDefaultControllerError = _require3.ReadableStreamDefaultControllerError,
+ ReadableStreamDefaultControllerGetDesiredSize = _require3.ReadableStreamDefaultControllerGetDesiredSize;
+
+ var _require4 = __w_pdfjs_require__(2),
+ WritableStream = _require4.WritableStream,
+ WritableStreamDefaultControllerError = _require4.WritableStreamDefaultControllerError;
+
+ function TransformStreamCloseReadable(transformStream) {
+ if (transformStream._errored === true) {
+ throw new TypeError('TransformStream is already errored');
+ }
+
+ if (transformStream._readableClosed === true) {
+ throw new TypeError('Readable side is already closed');
+ }
+
+ TransformStreamCloseReadableInternal(transformStream);
+ }
+
+ function TransformStreamEnqueueToReadable(transformStream, chunk) {
+ if (transformStream._errored === true) {
+ throw new TypeError('TransformStream is already errored');
+ }
+
+ if (transformStream._readableClosed === true) {
+ throw new TypeError('Readable side is already closed');
+ }
+
+ var controller = transformStream._readableController;
+
+ try {
+ ReadableStreamDefaultControllerEnqueue(controller, chunk);
+ } catch (e) {
+ transformStream._readableClosed = true;
+ TransformStreamErrorIfNeeded(transformStream, e);
+ throw transformStream._storedError;
+ }
+
+ var desiredSize = ReadableStreamDefaultControllerGetDesiredSize(controller);
+ var maybeBackpressure = desiredSize <= 0;
+
+ if (maybeBackpressure === true && transformStream._backpressure === false) {
+ TransformStreamSetBackpressure(transformStream, true);
+ }
+ }
+
+ function TransformStreamError(transformStream, e) {
+ if (transformStream._errored === true) {
+ throw new TypeError('TransformStream is already errored');
+ }
+
+ TransformStreamErrorInternal(transformStream, e);
+ }
+
+ function TransformStreamCloseReadableInternal(transformStream) {
+ assert(transformStream._errored === false);
+ assert(transformStream._readableClosed === false);
+
+ try {
+ ReadableStreamDefaultControllerClose(transformStream._readableController);
+ } catch (e) {
+ assert(false);
+ }
+
+ transformStream._readableClosed = true;
+ }
+
+ function TransformStreamErrorIfNeeded(transformStream, e) {
+ if (transformStream._errored === false) {
+ TransformStreamErrorInternal(transformStream, e);
+ }
+ }
+
+ function TransformStreamErrorInternal(transformStream, e) {
+ assert(transformStream._errored === false);
+ transformStream._errored = true;
+ transformStream._storedError = e;
+
+ if (transformStream._writableDone === false) {
+ WritableStreamDefaultControllerError(transformStream._writableController, e);
+ }
+
+ if (transformStream._readableClosed === false) {
+ ReadableStreamDefaultControllerError(transformStream._readableController, e);
+ }
+ }
+
+ function TransformStreamReadableReadyPromise(transformStream) {
+ assert(transformStream._backpressureChangePromise !== undefined, '_backpressureChangePromise should have been initialized');
+
+ if (transformStream._backpressure === false) {
+ return Promise.resolve();
+ }
+
+ assert(transformStream._backpressure === true, '_backpressure should have been initialized');
+ return transformStream._backpressureChangePromise;
+ }
+
+ function TransformStreamSetBackpressure(transformStream, backpressure) {
+ assert(transformStream._backpressure !== backpressure, 'TransformStreamSetBackpressure() should be called only when backpressure is changed');
+
+ if (transformStream._backpressureChangePromise !== undefined) {
+ transformStream._backpressureChangePromise_resolve(backpressure);
+ }
+
+ transformStream._backpressureChangePromise = new Promise(function (resolve) {
+ transformStream._backpressureChangePromise_resolve = resolve;
+ });
+
+ transformStream._backpressureChangePromise.then(function (resolution) {
+ assert(resolution !== backpressure, '_backpressureChangePromise should be fulfilled only when backpressure is changed');
+ });
+
+ transformStream._backpressure = backpressure;
+ }
+
+ function TransformStreamDefaultTransform(chunk, transformStreamController) {
+ var transformStream = transformStreamController._controlledTransformStream;
+ TransformStreamEnqueueToReadable(transformStream, chunk);
+ return Promise.resolve();
+ }
+
+ function TransformStreamTransform(transformStream, chunk) {
+ assert(transformStream._errored === false);
+ assert(transformStream._transforming === false);
+ assert(transformStream._backpressure === false);
+ transformStream._transforming = true;
+ var transformer = transformStream._transformer;
+ var controller = transformStream._transformStreamController;
+ var transformPromise = PromiseInvokeOrPerformFallback(transformer, 'transform', [chunk, controller], TransformStreamDefaultTransform, [chunk, controller]);
+ return transformPromise.then(function () {
+ transformStream._transforming = false;
+ return TransformStreamReadableReadyPromise(transformStream);
+ }, function (e) {
+ TransformStreamErrorIfNeeded(transformStream, e);
+ return Promise.reject(e);
+ });
+ }
+
+ function IsTransformStreamDefaultController(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_controlledTransformStream')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function IsTransformStream(x) {
+ if (!typeIsObject(x)) {
+ return false;
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(x, '_transformStreamController')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ var TransformStreamSink = function () {
+ function TransformStreamSink(transformStream, startPromise) {
+ _classCallCheck(this, TransformStreamSink);
+
+ this._transformStream = transformStream;
+ this._startPromise = startPromise;
+ }
+
+ _createClass(TransformStreamSink, [{
+ key: 'start',
+ value: function start(c) {
+ var transformStream = this._transformStream;
+ transformStream._writableController = c;
+ return this._startPromise.then(function () {
+ return TransformStreamReadableReadyPromise(transformStream);
+ });
+ }
+ }, {
+ key: 'write',
+ value: function write(chunk) {
+ var transformStream = this._transformStream;
+ return TransformStreamTransform(transformStream, chunk);
+ }
+ }, {
+ key: 'abort',
+ value: function abort() {
+ var transformStream = this._transformStream;
+ transformStream._writableDone = true;
+ TransformStreamErrorInternal(transformStream, new TypeError('Writable side aborted'));
+ }
+ }, {
+ key: 'close',
+ value: function close() {
+ var transformStream = this._transformStream;
+ assert(transformStream._transforming === false);
+ transformStream._writableDone = true;
+ var flushPromise = PromiseInvokeOrNoop(transformStream._transformer, 'flush', [transformStream._transformStreamController]);
+ return flushPromise.then(function () {
+ if (transformStream._errored === true) {
+ return Promise.reject(transformStream._storedError);
+ }
+
+ if (transformStream._readableClosed === false) {
+ TransformStreamCloseReadableInternal(transformStream);
+ }
+
+ return Promise.resolve();
+ })["catch"](function (r) {
+ TransformStreamErrorIfNeeded(transformStream, r);
+ return Promise.reject(transformStream._storedError);
+ });
+ }
+ }]);
+
+ return TransformStreamSink;
+ }();
+
+ var TransformStreamSource = function () {
+ function TransformStreamSource(transformStream, startPromise) {
+ _classCallCheck(this, TransformStreamSource);
+
+ this._transformStream = transformStream;
+ this._startPromise = startPromise;
+ }
+
+ _createClass(TransformStreamSource, [{
+ key: 'start',
+ value: function start(c) {
+ var transformStream = this._transformStream;
+ transformStream._readableController = c;
+ return this._startPromise.then(function () {
+ assert(transformStream._backpressureChangePromise !== undefined, '_backpressureChangePromise should have been initialized');
+
+ if (transformStream._backpressure === true) {
+ return Promise.resolve();
+ }
+
+ assert(transformStream._backpressure === false, '_backpressure should have been initialized');
+ return transformStream._backpressureChangePromise;
+ });
+ }
+ }, {
+ key: 'pull',
+ value: function pull() {
+ var transformStream = this._transformStream;
+ assert(transformStream._backpressure === true, 'pull() should be never called while _backpressure is false');
+ assert(transformStream._backpressureChangePromise !== undefined, '_backpressureChangePromise should have been initialized');
+ TransformStreamSetBackpressure(transformStream, false);
+ return transformStream._backpressureChangePromise;
+ }
+ }, {
+ key: 'cancel',
+ value: function cancel() {
+ var transformStream = this._transformStream;
+ transformStream._readableClosed = true;
+ TransformStreamErrorInternal(transformStream, new TypeError('Readable side canceled'));
+ }
+ }]);
+
+ return TransformStreamSource;
+ }();
+
+ var TransformStreamDefaultController = function () {
+ function TransformStreamDefaultController(transformStream) {
+ _classCallCheck(this, TransformStreamDefaultController);
+
+ if (IsTransformStream(transformStream) === false) {
+ throw new TypeError('TransformStreamDefaultController can only be ' + 'constructed with a TransformStream instance');
+ }
+
+ if (transformStream._transformStreamController !== undefined) {
+ throw new TypeError('TransformStreamDefaultController instances can ' + 'only be created by the TransformStream constructor');
+ }
+
+ this._controlledTransformStream = transformStream;
+ }
+
+ _createClass(TransformStreamDefaultController, [{
+ key: 'enqueue',
+ value: function enqueue(chunk) {
+ if (IsTransformStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('enqueue');
+ }
+
+ TransformStreamEnqueueToReadable(this._controlledTransformStream, chunk);
+ }
+ }, {
+ key: 'close',
+ value: function close() {
+ if (IsTransformStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('close');
+ }
+
+ TransformStreamCloseReadable(this._controlledTransformStream);
+ }
+ }, {
+ key: 'error',
+ value: function error(reason) {
+ if (IsTransformStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('error');
+ }
+
+ TransformStreamError(this._controlledTransformStream, reason);
+ }
+ }, {
+ key: 'desiredSize',
+ get: function get() {
+ if (IsTransformStreamDefaultController(this) === false) {
+ throw defaultControllerBrandCheckException('desiredSize');
+ }
+
+ var transformStream = this._controlledTransformStream;
+ var readableController = transformStream._readableController;
+ return ReadableStreamDefaultControllerGetDesiredSize(readableController);
+ }
+ }]);
+
+ return TransformStreamDefaultController;
+ }();
+
+ var TransformStream = function () {
+ function TransformStream() {
+ var transformer = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ _classCallCheck(this, TransformStream);
+
+ this._transformer = transformer;
+ var readableStrategy = transformer.readableStrategy,
+ writableStrategy = transformer.writableStrategy;
+ this._transforming = false;
+ this._errored = false;
+ this._storedError = undefined;
+ this._writableController = undefined;
+ this._readableController = undefined;
+ this._transformStreamController = undefined;
+ this._writableDone = false;
+ this._readableClosed = false;
+ this._backpressure = undefined;
+ this._backpressureChangePromise = undefined;
+ this._backpressureChangePromise_resolve = undefined;
+ this._transformStreamController = new TransformStreamDefaultController(this);
+ var startPromise_resolve = void 0;
+ var startPromise = new Promise(function (resolve) {
+ startPromise_resolve = resolve;
+ });
+ var source = new TransformStreamSource(this, startPromise);
+ this._readable = new ReadableStream(source, readableStrategy);
+ var sink = new TransformStreamSink(this, startPromise);
+ this._writable = new WritableStream(sink, writableStrategy);
+ assert(this._writableController !== undefined);
+ assert(this._readableController !== undefined);
+ var desiredSize = ReadableStreamDefaultControllerGetDesiredSize(this._readableController);
+ TransformStreamSetBackpressure(this, desiredSize <= 0);
+ var transformStream = this;
+ var startResult = InvokeOrNoop(transformer, 'start', [transformStream._transformStreamController]);
+ startPromise_resolve(startResult);
+ startPromise["catch"](function (e) {
+ if (transformStream._errored === false) {
+ transformStream._errored = true;
+ transformStream._storedError = e;
+ }
+ });
+ }
+
+ _createClass(TransformStream, [{
+ key: 'readable',
+ get: function get() {
+ if (IsTransformStream(this) === false) {
+ throw streamBrandCheckException('readable');
+ }
+
+ return this._readable;
+ }
+ }, {
+ key: 'writable',
+ get: function get() {
+ if (IsTransformStream(this) === false) {
+ throw streamBrandCheckException('writable');
+ }
+
+ return this._writable;
+ }
+ }]);
+
+ return TransformStream;
+ }();
+
+ module.exports = {
+ TransformStream: TransformStream
+ };
+
+ function defaultControllerBrandCheckException(name) {
+ return new TypeError('TransformStreamDefaultController.prototype.' + name + ' can only be used on a TransformStreamDefaultController');
+ }
+
+ function streamBrandCheckException(name) {
+ return new TypeError('TransformStream.prototype.' + name + ' can only be used on a TransformStream');
+ }
+}, function (module, exports, __w_pdfjs_require__) {
+ module.exports = __w_pdfjs_require__(5);
+}]));
+
+/***/ }),
+/* 149 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+{
+ var isURLSupported = false;
+
+ try {
+ if (typeof URL === 'function' && _typeof(URL.prototype) === 'object' && 'origin' in URL.prototype) {
+ var u = new URL('b', 'http://a');
+ u.pathname = 'c%20d';
+ isURLSupported = u.href === 'http://a/c%20d';
+ }
+ } catch (ex) {}
+
+ if (isURLSupported) {
+ exports.URL = URL;
+ } else {
+ var PolyfillURL = __w_pdfjs_require__(150).URL;
+
+ var OriginalURL = __w_pdfjs_require__(7).URL;
+
+ if (OriginalURL) {
+ PolyfillURL.createObjectURL = function (blob) {
+ return OriginalURL.createObjectURL.apply(OriginalURL, arguments);
+ };
+
+ PolyfillURL.revokeObjectURL = function (url) {
+ OriginalURL.revokeObjectURL(url);
+ };
+ }
+
+ exports.URL = PolyfillURL;
+ }
+}
+
+/***/ }),
+/* 150 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+(function URLConstructorClosure() {
+ 'use strict';
+
+ var relative = Object.create(null);
+ relative['ftp'] = 21;
+ relative['file'] = 0;
+ relative['gopher'] = 70;
+ relative['http'] = 80;
+ relative['https'] = 443;
+ relative['ws'] = 80;
+ relative['wss'] = 443;
+ var relativePathDotMapping = Object.create(null);
+ relativePathDotMapping['%2e'] = '.';
+ relativePathDotMapping['.%2e'] = '..';
+ relativePathDotMapping['%2e.'] = '..';
+ relativePathDotMapping['%2e%2e'] = '..';
+
+ function isRelativeScheme(scheme) {
+ return relative[scheme] !== undefined;
+ }
+
+ function invalid() {
+ clear.call(this);
+ this._isInvalid = true;
+ }
+
+ function IDNAToASCII(h) {
+ if (h === '') {
+ invalid.call(this);
+ }
+
+ return h.toLowerCase();
+ }
+
+ function percentEscape(c) {
+ var unicode = c.charCodeAt(0);
+
+ if (unicode > 0x20 && unicode < 0x7F && [0x22, 0x23, 0x3C, 0x3E, 0x3F, 0x60].indexOf(unicode) === -1) {
+ return c;
+ }
+
+ return encodeURIComponent(c);
+ }
+
+ function percentEscapeQuery(c) {
+ var unicode = c.charCodeAt(0);
+
+ if (unicode > 0x20 && unicode < 0x7F && [0x22, 0x23, 0x3C, 0x3E, 0x60].indexOf(unicode) === -1) {
+ return c;
+ }
+
+ return encodeURIComponent(c);
+ }
+
+ var EOF,
+ ALPHA = /[a-zA-Z]/,
+ ALPHANUMERIC = /[a-zA-Z0-9\+\-\.]/;
+
+ function parse(input, stateOverride, base) {
+ function err(message) {
+ errors.push(message);
+ }
+
+ var state = stateOverride || 'scheme start',
+ cursor = 0,
+ buffer = '',
+ seenAt = false,
+ seenBracket = false,
+ errors = [];
+
+ loop: while ((input[cursor - 1] !== EOF || cursor === 0) && !this._isInvalid) {
+ var c = input[cursor];
+
+ switch (state) {
+ case 'scheme start':
+ if (c && ALPHA.test(c)) {
+ buffer += c.toLowerCase();
+ state = 'scheme';
+ } else if (!stateOverride) {
+ buffer = '';
+ state = 'no scheme';
+ continue;
+ } else {
+ err('Invalid scheme.');
+ break loop;
+ }
+
+ break;
+
+ case 'scheme':
+ if (c && ALPHANUMERIC.test(c)) {
+ buffer += c.toLowerCase();
+ } else if (c === ':') {
+ this._scheme = buffer;
+ buffer = '';
+
+ if (stateOverride) {
+ break loop;
+ }
+
+ if (isRelativeScheme(this._scheme)) {
+ this._isRelative = true;
+ }
+
+ if (this._scheme === 'file') {
+ state = 'relative';
+ } else if (this._isRelative && base && base._scheme === this._scheme) {
+ state = 'relative or authority';
+ } else if (this._isRelative) {
+ state = 'authority first slash';
+ } else {
+ state = 'scheme data';
+ }
+ } else if (!stateOverride) {
+ buffer = '';
+ cursor = 0;
+ state = 'no scheme';
+ continue;
+ } else if (c === EOF) {
+ break loop;
+ } else {
+ err('Code point not allowed in scheme: ' + c);
+ break loop;
+ }
+
+ break;
+
+ case 'scheme data':
+ if (c === '?') {
+ this._query = '?';
+ state = 'query';
+ } else if (c === '#') {
+ this._fragment = '#';
+ state = 'fragment';
+ } else {
+ if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') {
+ this._schemeData += percentEscape(c);
+ }
+ }
+
+ break;
+
+ case 'no scheme':
+ if (!base || !isRelativeScheme(base._scheme)) {
+ err('Missing scheme.');
+ invalid.call(this);
+ } else {
+ state = 'relative';
+ continue;
+ }
+
+ break;
+
+ case 'relative or authority':
+ if (c === '/' && input[cursor + 1] === '/') {
+ state = 'authority ignore slashes';
+ } else {
+ err('Expected /, got: ' + c);
+ state = 'relative';
+ continue;
+ }
+
+ break;
+
+ case 'relative':
+ this._isRelative = true;
+
+ if (this._scheme !== 'file') {
+ this._scheme = base._scheme;
+ }
+
+ if (c === EOF) {
+ this._host = base._host;
+ this._port = base._port;
+ this._path = base._path.slice();
+ this._query = base._query;
+ this._username = base._username;
+ this._password = base._password;
+ break loop;
+ } else if (c === '/' || c === '\\') {
+ if (c === '\\') {
+ err('\\ is an invalid code point.');
+ }
+
+ state = 'relative slash';
+ } else if (c === '?') {
+ this._host = base._host;
+ this._port = base._port;
+ this._path = base._path.slice();
+ this._query = '?';
+ this._username = base._username;
+ this._password = base._password;
+ state = 'query';
+ } else if (c === '#') {
+ this._host = base._host;
+ this._port = base._port;
+ this._path = base._path.slice();
+ this._query = base._query;
+ this._fragment = '#';
+ this._username = base._username;
+ this._password = base._password;
+ state = 'fragment';
+ } else {
+ var nextC = input[cursor + 1];
+ var nextNextC = input[cursor + 2];
+
+ if (this._scheme !== 'file' || !ALPHA.test(c) || nextC !== ':' && nextC !== '|' || nextNextC !== EOF && nextNextC !== '/' && nextNextC !== '\\' && nextNextC !== '?' && nextNextC !== '#') {
+ this._host = base._host;
+ this._port = base._port;
+ this._username = base._username;
+ this._password = base._password;
+ this._path = base._path.slice();
+
+ this._path.pop();
+ }
+
+ state = 'relative path';
+ continue;
+ }
+
+ break;
+
+ case 'relative slash':
+ if (c === '/' || c === '\\') {
+ if (c === '\\') {
+ err('\\ is an invalid code point.');
+ }
+
+ if (this._scheme === 'file') {
+ state = 'file host';
+ } else {
+ state = 'authority ignore slashes';
+ }
+ } else {
+ if (this._scheme !== 'file') {
+ this._host = base._host;
+ this._port = base._port;
+ this._username = base._username;
+ this._password = base._password;
+ }
+
+ state = 'relative path';
+ continue;
+ }
+
+ break;
+
+ case 'authority first slash':
+ if (c === '/') {
+ state = 'authority second slash';
+ } else {
+ err('Expected \'/\', got: ' + c);
+ state = 'authority ignore slashes';
+ continue;
+ }
+
+ break;
+
+ case 'authority second slash':
+ state = 'authority ignore slashes';
+
+ if (c !== '/') {
+ err('Expected \'/\', got: ' + c);
+ continue;
+ }
+
+ break;
+
+ case 'authority ignore slashes':
+ if (c !== '/' && c !== '\\') {
+ state = 'authority';
+ continue;
+ } else {
+ err('Expected authority, got: ' + c);
+ }
+
+ break;
+
+ case 'authority':
+ if (c === '@') {
+ if (seenAt) {
+ err('@ already seen.');
+ buffer += '%40';
+ }
+
+ seenAt = true;
+
+ for (var i = 0; i < buffer.length; i++) {
+ var cp = buffer[i];
+
+ if (cp === '\t' || cp === '\n' || cp === '\r') {
+ err('Invalid whitespace in authority.');
+ continue;
+ }
+
+ if (cp === ':' && this._password === null) {
+ this._password = '';
+ continue;
+ }
+
+ var tempC = percentEscape(cp);
+
+ if (this._password !== null) {
+ this._password += tempC;
+ } else {
+ this._username += tempC;
+ }
+ }
+
+ buffer = '';
+ } else if (c === EOF || c === '/' || c === '\\' || c === '?' || c === '#') {
+ cursor -= buffer.length;
+ buffer = '';
+ state = 'host';
+ continue;
+ } else {
+ buffer += c;
+ }
+
+ break;
+
+ case 'file host':
+ if (c === EOF || c === '/' || c === '\\' || c === '?' || c === '#') {
+ if (buffer.length === 2 && ALPHA.test(buffer[0]) && (buffer[1] === ':' || buffer[1] === '|')) {
+ state = 'relative path';
+ } else if (buffer.length === 0) {
+ state = 'relative path start';
+ } else {
+ this._host = IDNAToASCII.call(this, buffer);
+ buffer = '';
+ state = 'relative path start';
+ }
+
+ continue;
+ } else if (c === '\t' || c === '\n' || c === '\r') {
+ err('Invalid whitespace in file host.');
+ } else {
+ buffer += c;
+ }
+
+ break;
+
+ case 'host':
+ case 'hostname':
+ if (c === ':' && !seenBracket) {
+ this._host = IDNAToASCII.call(this, buffer);
+ buffer = '';
+ state = 'port';
+
+ if (stateOverride === 'hostname') {
+ break loop;
+ }
+ } else if (c === EOF || c === '/' || c === '\\' || c === '?' || c === '#') {
+ this._host = IDNAToASCII.call(this, buffer);
+ buffer = '';
+ state = 'relative path start';
+
+ if (stateOverride) {
+ break loop;
+ }
+
+ continue;
+ } else if (c !== '\t' && c !== '\n' && c !== '\r') {
+ if (c === '[') {
+ seenBracket = true;
+ } else if (c === ']') {
+ seenBracket = false;
+ }
+
+ buffer += c;
+ } else {
+ err('Invalid code point in host/hostname: ' + c);
+ }
+
+ break;
+
+ case 'port':
+ if (/[0-9]/.test(c)) {
+ buffer += c;
+ } else if (c === EOF || c === '/' || c === '\\' || c === '?' || c === '#' || stateOverride) {
+ if (buffer !== '') {
+ var temp = parseInt(buffer, 10);
+
+ if (temp !== relative[this._scheme]) {
+ this._port = temp + '';
+ }
+
+ buffer = '';
+ }
+
+ if (stateOverride) {
+ break loop;
+ }
+
+ state = 'relative path start';
+ continue;
+ } else if (c === '\t' || c === '\n' || c === '\r') {
+ err('Invalid code point in port: ' + c);
+ } else {
+ invalid.call(this);
+ }
+
+ break;
+
+ case 'relative path start':
+ if (c === '\\') {
+ err('\'\\\' not allowed in path.');
+ }
+
+ state = 'relative path';
+
+ if (c !== '/' && c !== '\\') {
+ continue;
+ }
+
+ break;
+
+ case 'relative path':
+ if (c === EOF || c === '/' || c === '\\' || !stateOverride && (c === '?' || c === '#')) {
+ if (c === '\\') {
+ err('\\ not allowed in relative path.');
+ }
+
+ var tmp;
+
+ if (tmp = relativePathDotMapping[buffer.toLowerCase()]) {
+ buffer = tmp;
+ }
+
+ if (buffer === '..') {
+ this._path.pop();
+
+ if (c !== '/' && c !== '\\') {
+ this._path.push('');
+ }
+ } else if (buffer === '.' && c !== '/' && c !== '\\') {
+ this._path.push('');
+ } else if (buffer !== '.') {
+ if (this._scheme === 'file' && this._path.length === 0 && buffer.length === 2 && ALPHA.test(buffer[0]) && buffer[1] === '|') {
+ buffer = buffer[0] + ':';
+ }
+
+ this._path.push(buffer);
+ }
+
+ buffer = '';
+
+ if (c === '?') {
+ this._query = '?';
+ state = 'query';
+ } else if (c === '#') {
+ this._fragment = '#';
+ state = 'fragment';
+ }
+ } else if (c !== '\t' && c !== '\n' && c !== '\r') {
+ buffer += percentEscape(c);
+ }
+
+ break;
+
+ case 'query':
+ if (!stateOverride && c === '#') {
+ this._fragment = '#';
+ state = 'fragment';
+ } else if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') {
+ this._query += percentEscapeQuery(c);
+ }
+
+ break;
+
+ case 'fragment':
+ if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') {
+ this._fragment += c;
+ }
+
+ break;
+ }
+
+ cursor++;
+ }
+ }
+
+ function clear() {
+ this._scheme = '';
+ this._schemeData = '';
+ this._username = '';
+ this._password = null;
+ this._host = '';
+ this._port = '';
+ this._path = [];
+ this._query = '';
+ this._fragment = '';
+ this._isInvalid = false;
+ this._isRelative = false;
+ }
+
+ function JURL(url, base) {
+ if (base !== undefined && !(base instanceof JURL)) {
+ base = new JURL(String(base));
+ }
+
+ this._url = url;
+ clear.call(this);
+ var input = url.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g, '');
+ parse.call(this, input, null, base);
+ }
+
+ JURL.prototype = {
+ toString: function toString() {
+ return this.href;
+ },
+
+ get href() {
+ if (this._isInvalid) {
+ return this._url;
+ }
+
+ var authority = '';
+
+ if (this._username !== '' || this._password !== null) {
+ authority = this._username + (this._password !== null ? ':' + this._password : '') + '@';
+ }
+
+ return this.protocol + (this._isRelative ? '//' + authority + this.host : '') + this.pathname + this._query + this._fragment;
+ },
+
+ set href(value) {
+ clear.call(this);
+ parse.call(this, value);
+ },
+
+ get protocol() {
+ return this._scheme + ':';
+ },
+
+ set protocol(value) {
+ if (this._isInvalid) {
+ return;
+ }
+
+ parse.call(this, value + ':', 'scheme start');
+ },
+
+ get host() {
+ return this._isInvalid ? '' : this._port ? this._host + ':' + this._port : this._host;
+ },
+
+ set host(value) {
+ if (this._isInvalid || !this._isRelative) {
+ return;
+ }
+
+ parse.call(this, value, 'host');
+ },
+
+ get hostname() {
+ return this._host;
+ },
+
+ set hostname(value) {
+ if (this._isInvalid || !this._isRelative) {
+ return;
+ }
+
+ parse.call(this, value, 'hostname');
+ },
+
+ get port() {
+ return this._port;
+ },
+
+ set port(value) {
+ if (this._isInvalid || !this._isRelative) {
+ return;
+ }
+
+ parse.call(this, value, 'port');
+ },
+
+ get pathname() {
+ return this._isInvalid ? '' : this._isRelative ? '/' + this._path.join('/') : this._schemeData;
+ },
+
+ set pathname(value) {
+ if (this._isInvalid || !this._isRelative) {
+ return;
+ }
+
+ this._path = [];
+ parse.call(this, value, 'relative path start');
+ },
+
+ get search() {
+ return this._isInvalid || !this._query || this._query === '?' ? '' : this._query;
+ },
+
+ set search(value) {
+ if (this._isInvalid || !this._isRelative) {
+ return;
+ }
+
+ this._query = '?';
+
+ if (value[0] === '?') {
+ value = value.slice(1);
+ }
+
+ parse.call(this, value, 'query');
+ },
+
+ get hash() {
+ return this._isInvalid || !this._fragment || this._fragment === '#' ? '' : this._fragment;
+ },
+
+ set hash(value) {
+ if (this._isInvalid) {
+ return;
+ }
+
+ this._fragment = '#';
+
+ if (value[0] === '#') {
+ value = value.slice(1);
+ }
+
+ parse.call(this, value, 'fragment');
+ },
+
+ get origin() {
+ var host;
+
+ if (this._isInvalid || !this._scheme) {
+ return '';
+ }
+
+ switch (this._scheme) {
+ case 'data':
+ case 'file':
+ case 'javascript':
+ case 'mailto':
+ return 'null';
+
+ case 'blob':
+ try {
+ return new JURL(this._schemeData).origin || 'null';
+ } catch (_) {}
+
+ return 'null';
+ }
+
+ host = this.host;
+
+ if (!host) {
+ return '';
+ }
+
+ return this._scheme + '://' + host;
+ }
+
+ };
+ exports.URL = JURL;
+})();
+
+/***/ }),
+/* 151 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.clearPrimitiveCaches = clearPrimitiveCaches;
+exports.isEOF = isEOF;
+exports.isCmd = isCmd;
+exports.isDict = isDict;
+exports.isName = isName;
+exports.isRef = isRef;
+exports.isRefsEqual = isRefsEqual;
+exports.isStream = isStream;
+exports.RefSetCache = exports.RefSet = exports.Ref = exports.Name = exports.Dict = exports.Cmd = exports.EOF = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var EOF = {};
+exports.EOF = EOF;
+
+var Name = function NameClosure() {
+ var nameCache = Object.create(null);
+
+ function Name(name) {
+ this.name = name;
+ }
+
+ Name.prototype = {};
+
+ Name.get = function Name_get(name) {
+ var nameValue = nameCache[name];
+ return nameValue ? nameValue : nameCache[name] = new Name(name);
+ };
+
+ Name._clearCache = function () {
+ nameCache = Object.create(null);
+ };
+
+ return Name;
+}();
+
+exports.Name = Name;
+
+var Cmd = function CmdClosure() {
+ var cmdCache = Object.create(null);
+
+ function Cmd(cmd) {
+ this.cmd = cmd;
+ }
+
+ Cmd.prototype = {};
+
+ Cmd.get = function Cmd_get(cmd) {
+ var cmdValue = cmdCache[cmd];
+ return cmdValue ? cmdValue : cmdCache[cmd] = new Cmd(cmd);
+ };
+
+ Cmd._clearCache = function () {
+ cmdCache = Object.create(null);
+ };
+
+ return Cmd;
+}();
+
+exports.Cmd = Cmd;
+
+var Dict = function DictClosure() {
+ var nonSerializable = function nonSerializableClosure() {
+ return nonSerializable;
+ };
+
+ function Dict(xref) {
+ this._map = Object.create(null);
+ this.xref = xref;
+ this.objId = null;
+ this.suppressEncryption = false;
+ this.__nonSerializable__ = nonSerializable;
+ }
+
+ Dict.prototype = {
+ assignXref: function Dict_assignXref(newXref) {
+ this.xref = newXref;
+ },
+ get: function Dict_get(key1, key2, key3) {
+ var value;
+ var xref = this.xref,
+ suppressEncryption = this.suppressEncryption;
+
+ if (typeof (value = this._map[key1]) !== 'undefined' || key1 in this._map || typeof key2 === 'undefined') {
+ return xref ? xref.fetchIfRef(value, suppressEncryption) : value;
+ }
+
+ if (typeof (value = this._map[key2]) !== 'undefined' || key2 in this._map || typeof key3 === 'undefined') {
+ return xref ? xref.fetchIfRef(value, suppressEncryption) : value;
+ }
+
+ value = this._map[key3] || null;
+ return xref ? xref.fetchIfRef(value, suppressEncryption) : value;
+ },
+ getAsync: function Dict_getAsync(key1, key2, key3) {
+ var value;
+ var xref = this.xref,
+ suppressEncryption = this.suppressEncryption;
+
+ if (typeof (value = this._map[key1]) !== 'undefined' || key1 in this._map || typeof key2 === 'undefined') {
+ if (xref) {
+ return xref.fetchIfRefAsync(value, suppressEncryption);
+ }
+
+ return Promise.resolve(value);
+ }
+
+ if (typeof (value = this._map[key2]) !== 'undefined' || key2 in this._map || typeof key3 === 'undefined') {
+ if (xref) {
+ return xref.fetchIfRefAsync(value, suppressEncryption);
+ }
+
+ return Promise.resolve(value);
+ }
+
+ value = this._map[key3] || null;
+
+ if (xref) {
+ return xref.fetchIfRefAsync(value, suppressEncryption);
+ }
+
+ return Promise.resolve(value);
+ },
+ getArray: function Dict_getArray(key1, key2, key3) {
+ var value = this.get(key1, key2, key3);
+ var xref = this.xref,
+ suppressEncryption = this.suppressEncryption;
+
+ if (!Array.isArray(value) || !xref) {
+ return value;
+ }
+
+ value = value.slice();
+
+ for (var i = 0, ii = value.length; i < ii; i++) {
+ if (!isRef(value[i])) {
+ continue;
+ }
+
+ value[i] = xref.fetch(value[i], suppressEncryption);
+ }
+
+ return value;
+ },
+ getRaw: function Dict_getRaw(key) {
+ return this._map[key];
+ },
+ getKeys: function Dict_getKeys() {
+ return Object.keys(this._map);
+ },
+ set: function Dict_set(key, value) {
+ this._map[key] = value;
+ },
+ has: function Dict_has(key) {
+ return key in this._map;
+ },
+ forEach: function Dict_forEach(callback) {
+ for (var key in this._map) {
+ callback(key, this.get(key));
+ }
+ }
+ };
+ Dict.empty = new Dict(null);
+
+ Dict.merge = function (xref, dictArray) {
+ var mergedDict = new Dict(xref);
+
+ for (var i = 0, ii = dictArray.length; i < ii; i++) {
+ var dict = dictArray[i];
+
+ if (!isDict(dict)) {
+ continue;
+ }
+
+ for (var keyName in dict._map) {
+ if (mergedDict._map[keyName] !== undefined) {
+ continue;
+ }
+
+ mergedDict._map[keyName] = dict._map[keyName];
+ }
+ }
+
+ return mergedDict;
+ };
+
+ return Dict;
+}();
+
+exports.Dict = Dict;
+
+var Ref = function RefClosure() {
+ var refCache = Object.create(null);
+
+ function Ref(num, gen) {
+ this.num = num;
+ this.gen = gen;
+ }
+
+ Ref.prototype = {
+ toString: function Ref_toString() {
+ if (this.gen === 0) {
+ return "".concat(this.num, "R");
+ }
+
+ return "".concat(this.num, "R").concat(this.gen);
+ }
+ };
+
+ Ref.get = function (num, gen) {
+ var key = gen === 0 ? "".concat(num, "R") : "".concat(num, "R").concat(gen);
+ var refValue = refCache[key];
+ return refValue ? refValue : refCache[key] = new Ref(num, gen);
+ };
+
+ Ref._clearCache = function () {
+ refCache = Object.create(null);
+ };
+
+ return Ref;
+}();
+
+exports.Ref = Ref;
+
+var RefSet = function RefSetClosure() {
+ function RefSet() {
+ this.dict = Object.create(null);
+ }
+
+ RefSet.prototype = {
+ has: function RefSet_has(ref) {
+ return ref.toString() in this.dict;
+ },
+ put: function RefSet_put(ref) {
+ this.dict[ref.toString()] = true;
+ },
+ remove: function RefSet_remove(ref) {
+ delete this.dict[ref.toString()];
+ }
+ };
+ return RefSet;
+}();
+
+exports.RefSet = RefSet;
+
+var RefSetCache = function RefSetCacheClosure() {
+ function RefSetCache() {
+ this.dict = Object.create(null);
+ }
+
+ RefSetCache.prototype = {
+ get: function RefSetCache_get(ref) {
+ return this.dict[ref.toString()];
+ },
+ has: function RefSetCache_has(ref) {
+ return ref.toString() in this.dict;
+ },
+ put: function RefSetCache_put(ref, obj) {
+ this.dict[ref.toString()] = obj;
+ },
+ putAlias: function RefSetCache_putAlias(ref, aliasRef) {
+ this.dict[ref.toString()] = this.get(aliasRef);
+ },
+ forEach: function RefSetCache_forEach(fn, thisArg) {
+ for (var i in this.dict) {
+ fn.call(thisArg, this.dict[i]);
+ }
+ },
+ clear: function RefSetCache_clear() {
+ this.dict = Object.create(null);
+ }
+ };
+ return RefSetCache;
+}();
+
+exports.RefSetCache = RefSetCache;
+
+function isEOF(v) {
+ return v === EOF;
+}
+
+function isName(v, name) {
+ return v instanceof Name && (name === undefined || v.name === name);
+}
+
+function isCmd(v, cmd) {
+ return v instanceof Cmd && (cmd === undefined || v.cmd === cmd);
+}
+
+function isDict(v, type) {
+ return v instanceof Dict && (type === undefined || isName(v.get('Type'), type));
+}
+
+function isRef(v) {
+ return v instanceof Ref;
+}
+
+function isRefsEqual(v1, v2) {
+ return v1.num === v2.num && v1.gen === v2.gen;
+}
+
+function isStream(v) {
+ return _typeof(v) === 'object' && v !== null && v.getBytes !== undefined;
+}
+
+function clearPrimitiveCaches() {
+ Cmd._clearCache();
+
+ Name._clearCache();
+
+ Ref._clearCache();
+}
+
+/***/ }),
+/* 152 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.NetworkPdfManager = exports.LocalPdfManager = void 0;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(2));
+
+var _util = __w_pdfjs_require__(5);
+
+var _chunked_stream = __w_pdfjs_require__(153);
+
+var _core_utils = __w_pdfjs_require__(154);
+
+var _document = __w_pdfjs_require__(155);
+
+var _stream = __w_pdfjs_require__(158);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
+
+function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
+
+function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
+
+function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var BasePdfManager =
+/*#__PURE__*/
+function () {
+ function BasePdfManager() {
+ _classCallCheck(this, BasePdfManager);
+
+ if (this.constructor === BasePdfManager) {
+ (0, _util.unreachable)('Cannot initialize BasePdfManager.');
+ }
+ }
+
+ _createClass(BasePdfManager, [{
+ key: "onLoadedStream",
+ value: function onLoadedStream() {
+ (0, _util.unreachable)('Abstract method `onLoadedStream` called');
+ }
+ }, {
+ key: "ensureDoc",
+ value: function ensureDoc(prop, args) {
+ return this.ensure(this.pdfDocument, prop, args);
+ }
+ }, {
+ key: "ensureXRef",
+ value: function ensureXRef(prop, args) {
+ return this.ensure(this.pdfDocument.xref, prop, args);
+ }
+ }, {
+ key: "ensureCatalog",
+ value: function ensureCatalog(prop, args) {
+ return this.ensure(this.pdfDocument.catalog, prop, args);
+ }
+ }, {
+ key: "getPage",
+ value: function getPage(pageIndex) {
+ return this.pdfDocument.getPage(pageIndex);
+ }
+ }, {
+ key: "fontFallback",
+ value: function fontFallback(id, handler) {
+ return this.pdfDocument.fontFallback(id, handler);
+ }
+ }, {
+ key: "cleanup",
+ value: function cleanup() {
+ return this.pdfDocument.cleanup();
+ }
+ }, {
+ key: "ensure",
+ value: function () {
+ var _ensure = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee(obj, prop, args) {
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ (0, _util.unreachable)('Abstract method `ensure` called');
+
+ case 1:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee);
+ }));
+
+ function ensure(_x, _x2, _x3) {
+ return _ensure.apply(this, arguments);
+ }
+
+ return ensure;
+ }()
+ }, {
+ key: "requestRange",
+ value: function requestRange(begin, end) {
+ (0, _util.unreachable)('Abstract method `requestRange` called');
+ }
+ }, {
+ key: "requestLoadedStream",
+ value: function requestLoadedStream() {
+ (0, _util.unreachable)('Abstract method `requestLoadedStream` called');
+ }
+ }, {
+ key: "sendProgressiveData",
+ value: function sendProgressiveData(chunk) {
+ (0, _util.unreachable)('Abstract method `sendProgressiveData` called');
+ }
+ }, {
+ key: "updatePassword",
+ value: function updatePassword(password) {
+ this._password = password;
+ }
+ }, {
+ key: "terminate",
+ value: function terminate() {
+ (0, _util.unreachable)('Abstract method `terminate` called');
+ }
+ }, {
+ key: "docId",
+ get: function get() {
+ return this._docId;
+ }
+ }, {
+ key: "password",
+ get: function get() {
+ return this._password;
+ }
+ }, {
+ key: "docBaseUrl",
+ get: function get() {
+ var docBaseUrl = null;
+
+ if (this._docBaseUrl) {
+ var absoluteUrl = (0, _util.createValidAbsoluteUrl)(this._docBaseUrl);
+
+ if (absoluteUrl) {
+ docBaseUrl = absoluteUrl.href;
+ } else {
+ (0, _util.warn)("Invalid absolute docBaseUrl: \"".concat(this._docBaseUrl, "\"."));
+ }
+ }
+
+ return (0, _util.shadow)(this, 'docBaseUrl', docBaseUrl);
+ }
+ }]);
+
+ return BasePdfManager;
+}();
+
+var LocalPdfManager =
+/*#__PURE__*/
+function (_BasePdfManager) {
+ _inherits(LocalPdfManager, _BasePdfManager);
+
+ function LocalPdfManager(docId, data, password, evaluatorOptions, docBaseUrl) {
+ var _this;
+
+ _classCallCheck(this, LocalPdfManager);
+
+ _this = _possibleConstructorReturn(this, _getPrototypeOf(LocalPdfManager).call(this));
+ _this._docId = docId;
+ _this._password = password;
+ _this._docBaseUrl = docBaseUrl;
+ _this.evaluatorOptions = evaluatorOptions;
+ var stream = new _stream.Stream(data);
+ _this.pdfDocument = new _document.PDFDocument(_assertThisInitialized(_this), stream);
+ _this._loadedStreamPromise = Promise.resolve(stream);
+ return _this;
+ }
+
+ _createClass(LocalPdfManager, [{
+ key: "ensure",
+ value: function () {
+ var _ensure2 = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee2(obj, prop, args) {
+ var value;
+ return _regenerator["default"].wrap(function _callee2$(_context2) {
+ while (1) {
+ switch (_context2.prev = _context2.next) {
+ case 0:
+ value = obj[prop];
+
+ if (!(typeof value === 'function')) {
+ _context2.next = 3;
+ break;
+ }
+
+ return _context2.abrupt("return", value.apply(obj, args));
+
+ case 3:
+ return _context2.abrupt("return", value);
+
+ case 4:
+ case "end":
+ return _context2.stop();
+ }
+ }
+ }, _callee2);
+ }));
+
+ function ensure(_x4, _x5, _x6) {
+ return _ensure2.apply(this, arguments);
+ }
+
+ return ensure;
+ }()
+ }, {
+ key: "requestRange",
+ value: function requestRange(begin, end) {
+ return Promise.resolve();
+ }
+ }, {
+ key: "requestLoadedStream",
+ value: function requestLoadedStream() {}
+ }, {
+ key: "onLoadedStream",
+ value: function onLoadedStream() {
+ return this._loadedStreamPromise;
+ }
+ }, {
+ key: "terminate",
+ value: function terminate() {}
+ }]);
+
+ return LocalPdfManager;
+}(BasePdfManager);
+
+exports.LocalPdfManager = LocalPdfManager;
+
+var NetworkPdfManager =
+/*#__PURE__*/
+function (_BasePdfManager2) {
+ _inherits(NetworkPdfManager, _BasePdfManager2);
+
+ function NetworkPdfManager(docId, pdfNetworkStream, args, evaluatorOptions, docBaseUrl) {
+ var _this2;
+
+ _classCallCheck(this, NetworkPdfManager);
+
+ _this2 = _possibleConstructorReturn(this, _getPrototypeOf(NetworkPdfManager).call(this));
+ _this2._docId = docId;
+ _this2._password = args.password;
+ _this2._docBaseUrl = docBaseUrl;
+ _this2.msgHandler = args.msgHandler;
+ _this2.evaluatorOptions = evaluatorOptions;
+ _this2.streamManager = new _chunked_stream.ChunkedStreamManager(pdfNetworkStream, {
+ msgHandler: args.msgHandler,
+ length: args.length,
+ disableAutoFetch: args.disableAutoFetch,
+ rangeChunkSize: args.rangeChunkSize
+ });
+ _this2.pdfDocument = new _document.PDFDocument(_assertThisInitialized(_this2), _this2.streamManager.getStream());
+ return _this2;
+ }
+
+ _createClass(NetworkPdfManager, [{
+ key: "ensure",
+ value: function () {
+ var _ensure3 = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee3(obj, prop, args) {
+ var value;
+ return _regenerator["default"].wrap(function _callee3$(_context3) {
+ while (1) {
+ switch (_context3.prev = _context3.next) {
+ case 0:
+ _context3.prev = 0;
+ value = obj[prop];
+
+ if (!(typeof value === 'function')) {
+ _context3.next = 4;
+ break;
+ }
+
+ return _context3.abrupt("return", value.apply(obj, args));
+
+ case 4:
+ return _context3.abrupt("return", value);
+
+ case 7:
+ _context3.prev = 7;
+ _context3.t0 = _context3["catch"](0);
+
+ if (_context3.t0 instanceof _core_utils.MissingDataException) {
+ _context3.next = 11;
+ break;
+ }
+
+ throw _context3.t0;
+
+ case 11:
+ _context3.next = 13;
+ return this.requestRange(_context3.t0.begin, _context3.t0.end);
+
+ case 13:
+ return _context3.abrupt("return", this.ensure(obj, prop, args));
+
+ case 14:
+ case "end":
+ return _context3.stop();
+ }
+ }
+ }, _callee3, this, [[0, 7]]);
+ }));
+
+ function ensure(_x7, _x8, _x9) {
+ return _ensure3.apply(this, arguments);
+ }
+
+ return ensure;
+ }()
+ }, {
+ key: "requestRange",
+ value: function requestRange(begin, end) {
+ return this.streamManager.requestRange(begin, end);
+ }
+ }, {
+ key: "requestLoadedStream",
+ value: function requestLoadedStream() {
+ this.streamManager.requestAllChunks();
+ }
+ }, {
+ key: "sendProgressiveData",
+ value: function sendProgressiveData(chunk) {
+ this.streamManager.onReceiveData({
+ chunk: chunk
+ });
+ }
+ }, {
+ key: "onLoadedStream",
+ value: function onLoadedStream() {
+ return this.streamManager.onLoadedStream();
+ }
+ }, {
+ key: "terminate",
+ value: function terminate() {
+ this.streamManager.abort();
+ }
+ }]);
+
+ return NetworkPdfManager;
+}(BasePdfManager);
+
+exports.NetworkPdfManager = NetworkPdfManager;
+
+/***/ }),
+/* 153 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ChunkedStreamManager = exports.ChunkedStream = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _core_utils = __w_pdfjs_require__(154);
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var ChunkedStream =
+/*#__PURE__*/
+function () {
+ function ChunkedStream(length, chunkSize, manager) {
+ _classCallCheck(this, ChunkedStream);
+
+ this.bytes = new Uint8Array(length);
+ this.start = 0;
+ this.pos = 0;
+ this.end = length;
+ this.chunkSize = chunkSize;
+ this.loadedChunks = [];
+ this.numChunksLoaded = 0;
+ this.numChunks = Math.ceil(length / chunkSize);
+ this.manager = manager;
+ this.progressiveDataLength = 0;
+ this.lastSuccessfulEnsureByteChunk = -1;
+ }
+
+ _createClass(ChunkedStream, [{
+ key: "getMissingChunks",
+ value: function getMissingChunks() {
+ var chunks = [];
+
+ for (var chunk = 0, n = this.numChunks; chunk < n; ++chunk) {
+ if (!this.loadedChunks[chunk]) {
+ chunks.push(chunk);
+ }
+ }
+
+ return chunks;
+ }
+ }, {
+ key: "getBaseStreams",
+ value: function getBaseStreams() {
+ return [this];
+ }
+ }, {
+ key: "allChunksLoaded",
+ value: function allChunksLoaded() {
+ return this.numChunksLoaded === this.numChunks;
+ }
+ }, {
+ key: "onReceiveData",
+ value: function onReceiveData(begin, chunk) {
+ var chunkSize = this.chunkSize;
+
+ if (begin % chunkSize !== 0) {
+ throw new Error("Bad begin offset: ".concat(begin));
+ }
+
+ var end = begin + chunk.byteLength;
+
+ if (end % chunkSize !== 0 && end !== this.bytes.length) {
+ throw new Error("Bad end offset: ".concat(end));
+ }
+
+ this.bytes.set(new Uint8Array(chunk), begin);
+ var beginChunk = Math.floor(begin / chunkSize);
+ var endChunk = Math.floor((end - 1) / chunkSize) + 1;
+
+ for (var curChunk = beginChunk; curChunk < endChunk; ++curChunk) {
+ if (!this.loadedChunks[curChunk]) {
+ this.loadedChunks[curChunk] = true;
+ ++this.numChunksLoaded;
+ }
+ }
+ }
+ }, {
+ key: "onReceiveProgressiveData",
+ value: function onReceiveProgressiveData(data) {
+ var position = this.progressiveDataLength;
+ var beginChunk = Math.floor(position / this.chunkSize);
+ this.bytes.set(new Uint8Array(data), position);
+ position += data.byteLength;
+ this.progressiveDataLength = position;
+ var endChunk = position >= this.end ? this.numChunks : Math.floor(position / this.chunkSize);
+
+ for (var curChunk = beginChunk; curChunk < endChunk; ++curChunk) {
+ if (!this.loadedChunks[curChunk]) {
+ this.loadedChunks[curChunk] = true;
+ ++this.numChunksLoaded;
+ }
+ }
+ }
+ }, {
+ key: "ensureByte",
+ value: function ensureByte(pos) {
+ if (pos < this.progressiveDataLength) {
+ return;
+ }
+
+ var chunk = Math.floor(pos / this.chunkSize);
+
+ if (chunk === this.lastSuccessfulEnsureByteChunk) {
+ return;
+ }
+
+ if (!this.loadedChunks[chunk]) {
+ throw new _core_utils.MissingDataException(pos, pos + 1);
+ }
+
+ this.lastSuccessfulEnsureByteChunk = chunk;
+ }
+ }, {
+ key: "ensureRange",
+ value: function ensureRange(begin, end) {
+ if (begin >= end) {
+ return;
+ }
+
+ if (end <= this.progressiveDataLength) {
+ return;
+ }
+
+ var chunkSize = this.chunkSize;
+ var beginChunk = Math.floor(begin / chunkSize);
+ var endChunk = Math.floor((end - 1) / chunkSize) + 1;
+
+ for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
+ if (!this.loadedChunks[chunk]) {
+ throw new _core_utils.MissingDataException(begin, end);
+ }
+ }
+ }
+ }, {
+ key: "nextEmptyChunk",
+ value: function nextEmptyChunk(beginChunk) {
+ var numChunks = this.numChunks;
+
+ for (var i = 0; i < numChunks; ++i) {
+ var chunk = (beginChunk + i) % numChunks;
+
+ if (!this.loadedChunks[chunk]) {
+ return chunk;
+ }
+ }
+
+ return null;
+ }
+ }, {
+ key: "hasChunk",
+ value: function hasChunk(chunk) {
+ return !!this.loadedChunks[chunk];
+ }
+ }, {
+ key: "getByte",
+ value: function getByte() {
+ var pos = this.pos;
+
+ if (pos >= this.end) {
+ return -1;
+ }
+
+ this.ensureByte(pos);
+ return this.bytes[this.pos++];
+ }
+ }, {
+ key: "getUint16",
+ value: function getUint16() {
+ var b0 = this.getByte();
+ var b1 = this.getByte();
+
+ if (b0 === -1 || b1 === -1) {
+ return -1;
+ }
+
+ return (b0 << 8) + b1;
+ }
+ }, {
+ key: "getInt32",
+ value: function getInt32() {
+ var b0 = this.getByte();
+ var b1 = this.getByte();
+ var b2 = this.getByte();
+ var b3 = this.getByte();
+ return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3;
+ }
+ }, {
+ key: "getBytes",
+ value: function getBytes(length) {
+ var forceClamped = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var bytes = this.bytes;
+ var pos = this.pos;
+ var strEnd = this.end;
+
+ if (!length) {
+ this.ensureRange(pos, strEnd);
+
+ var _subarray = bytes.subarray(pos, strEnd);
+
+ return forceClamped ? new Uint8ClampedArray(_subarray) : _subarray;
+ }
+
+ var end = pos + length;
+
+ if (end > strEnd) {
+ end = strEnd;
+ }
+
+ this.ensureRange(pos, end);
+ this.pos = end;
+ var subarray = bytes.subarray(pos, end);
+ return forceClamped ? new Uint8ClampedArray(subarray) : subarray;
+ }
+ }, {
+ key: "peekByte",
+ value: function peekByte() {
+ var peekedByte = this.getByte();
+ this.pos--;
+ return peekedByte;
+ }
+ }, {
+ key: "peekBytes",
+ value: function peekBytes(length) {
+ var forceClamped = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var bytes = this.getBytes(length, forceClamped);
+ this.pos -= bytes.length;
+ return bytes;
+ }
+ }, {
+ key: "getByteRange",
+ value: function getByteRange(begin, end) {
+ this.ensureRange(begin, end);
+ return this.bytes.subarray(begin, end);
+ }
+ }, {
+ key: "skip",
+ value: function skip(n) {
+ if (!n) {
+ n = 1;
+ }
+
+ this.pos += n;
+ }
+ }, {
+ key: "reset",
+ value: function reset() {
+ this.pos = this.start;
+ }
+ }, {
+ key: "moveStart",
+ value: function moveStart() {
+ this.start = this.pos;
+ }
+ }, {
+ key: "makeSubStream",
+ value: function makeSubStream(start, length, dict) {
+ if (length) {
+ this.ensureRange(start, start + length);
+ } else {
+ this.ensureByte(start);
+ }
+
+ function ChunkedStreamSubstream() {}
+
+ ChunkedStreamSubstream.prototype = Object.create(this);
+
+ ChunkedStreamSubstream.prototype.getMissingChunks = function () {
+ var chunkSize = this.chunkSize;
+ var beginChunk = Math.floor(this.start / chunkSize);
+ var endChunk = Math.floor((this.end - 1) / chunkSize) + 1;
+ var missingChunks = [];
+
+ for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
+ if (!this.loadedChunks[chunk]) {
+ missingChunks.push(chunk);
+ }
+ }
+
+ return missingChunks;
+ };
+
+ var subStream = new ChunkedStreamSubstream();
+ subStream.pos = subStream.start = start;
+ subStream.end = start + length || this.end;
+ subStream.dict = dict;
+ return subStream;
+ }
+ }, {
+ key: "length",
+ get: function get() {
+ return this.end - this.start;
+ }
+ }, {
+ key: "isEmpty",
+ get: function get() {
+ return this.length === 0;
+ }
+ }]);
+
+ return ChunkedStream;
+}();
+
+exports.ChunkedStream = ChunkedStream;
+
+var ChunkedStreamManager =
+/*#__PURE__*/
+function () {
+ function ChunkedStreamManager(pdfNetworkStream, args) {
+ _classCallCheck(this, ChunkedStreamManager);
+
+ this.length = args.length;
+ this.chunkSize = args.rangeChunkSize;
+ this.stream = new ChunkedStream(this.length, this.chunkSize, this);
+ this.pdfNetworkStream = pdfNetworkStream;
+ this.disableAutoFetch = args.disableAutoFetch;
+ this.msgHandler = args.msgHandler;
+ this.currRequestId = 0;
+ this.chunksNeededByRequest = Object.create(null);
+ this.requestsByChunk = Object.create(null);
+ this.promisesByRequest = Object.create(null);
+ this.progressiveDataLength = 0;
+ this.aborted = false;
+ this._loadedStreamCapability = (0, _util.createPromiseCapability)();
+ }
+
+ _createClass(ChunkedStreamManager, [{
+ key: "onLoadedStream",
+ value: function onLoadedStream() {
+ return this._loadedStreamCapability.promise;
+ }
+ }, {
+ key: "sendRequest",
+ value: function sendRequest(begin, end) {
+ var _this = this;
+
+ var rangeReader = this.pdfNetworkStream.getRangeReader(begin, end);
+
+ if (!rangeReader.isStreamingSupported) {
+ rangeReader.onProgress = this.onProgress.bind(this);
+ }
+
+ var chunks = [],
+ loaded = 0;
+ var promise = new Promise(function (resolve, reject) {
+ var readChunk = function readChunk(chunk) {
+ try {
+ if (!chunk.done) {
+ var data = chunk.value;
+ chunks.push(data);
+ loaded += (0, _util.arrayByteLength)(data);
+
+ if (rangeReader.isStreamingSupported) {
+ _this.onProgress({
+ loaded: loaded
+ });
+ }
+
+ rangeReader.read().then(readChunk, reject);
+ return;
+ }
+
+ var chunkData = (0, _util.arraysToBytes)(chunks);
+ chunks = null;
+ resolve(chunkData);
+ } catch (e) {
+ reject(e);
+ }
+ };
+
+ rangeReader.read().then(readChunk, reject);
+ });
+ promise.then(function (data) {
+ if (_this.aborted) {
+ return;
+ }
+
+ _this.onReceiveData({
+ chunk: data,
+ begin: begin
+ });
+ });
+ }
+ }, {
+ key: "requestAllChunks",
+ value: function requestAllChunks() {
+ var missingChunks = this.stream.getMissingChunks();
+
+ this._requestChunks(missingChunks);
+
+ return this._loadedStreamCapability.promise;
+ }
+ }, {
+ key: "_requestChunks",
+ value: function _requestChunks(chunks) {
+ var requestId = this.currRequestId++;
+ var chunksNeeded = Object.create(null);
+ this.chunksNeededByRequest[requestId] = chunksNeeded;
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = chunks[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var _chunk = _step.value;
+
+ if (!this.stream.hasChunk(_chunk)) {
+ chunksNeeded[_chunk] = true;
+ }
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator["return"] != null) {
+ _iterator["return"]();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+
+ if ((0, _util.isEmptyObj)(chunksNeeded)) {
+ return Promise.resolve();
+ }
+
+ var capability = (0, _util.createPromiseCapability)();
+ this.promisesByRequest[requestId] = capability;
+ var chunksToRequest = [];
+
+ for (var chunk in chunksNeeded) {
+ chunk = chunk | 0;
+
+ if (!(chunk in this.requestsByChunk)) {
+ this.requestsByChunk[chunk] = [];
+ chunksToRequest.push(chunk);
+ }
+
+ this.requestsByChunk[chunk].push(requestId);
+ }
+
+ if (!chunksToRequest.length) {
+ return capability.promise;
+ }
+
+ var groupedChunksToRequest = this.groupChunks(chunksToRequest);
+ var _iteratorNormalCompletion2 = true;
+ var _didIteratorError2 = false;
+ var _iteratorError2 = undefined;
+
+ try {
+ for (var _iterator2 = groupedChunksToRequest[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
+ var groupedChunk = _step2.value;
+ var begin = groupedChunk.beginChunk * this.chunkSize;
+ var end = Math.min(groupedChunk.endChunk * this.chunkSize, this.length);
+ this.sendRequest(begin, end);
+ }
+ } catch (err) {
+ _didIteratorError2 = true;
+ _iteratorError2 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) {
+ _iterator2["return"]();
+ }
+ } finally {
+ if (_didIteratorError2) {
+ throw _iteratorError2;
+ }
+ }
+ }
+
+ return capability.promise;
+ }
+ }, {
+ key: "getStream",
+ value: function getStream() {
+ return this.stream;
+ }
+ }, {
+ key: "requestRange",
+ value: function requestRange(begin, end) {
+ end = Math.min(end, this.length);
+ var beginChunk = this.getBeginChunk(begin);
+ var endChunk = this.getEndChunk(end);
+ var chunks = [];
+
+ for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
+ chunks.push(chunk);
+ }
+
+ return this._requestChunks(chunks);
+ }
+ }, {
+ key: "requestRanges",
+ value: function requestRanges() {
+ var ranges = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
+ var chunksToRequest = [];
+ var _iteratorNormalCompletion3 = true;
+ var _didIteratorError3 = false;
+ var _iteratorError3 = undefined;
+
+ try {
+ for (var _iterator3 = ranges[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
+ var range = _step3.value;
+ var beginChunk = this.getBeginChunk(range.begin);
+ var endChunk = this.getEndChunk(range.end);
+
+ for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
+ if (!chunksToRequest.includes(chunk)) {
+ chunksToRequest.push(chunk);
+ }
+ }
+ }
+ } catch (err) {
+ _didIteratorError3 = true;
+ _iteratorError3 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion3 && _iterator3["return"] != null) {
+ _iterator3["return"]();
+ }
+ } finally {
+ if (_didIteratorError3) {
+ throw _iteratorError3;
+ }
+ }
+ }
+
+ chunksToRequest.sort(function (a, b) {
+ return a - b;
+ });
+ return this._requestChunks(chunksToRequest);
+ }
+ }, {
+ key: "groupChunks",
+ value: function groupChunks(chunks) {
+ var groupedChunks = [];
+ var beginChunk = -1;
+ var prevChunk = -1;
+
+ for (var i = 0, ii = chunks.length; i < ii; ++i) {
+ var chunk = chunks[i];
+
+ if (beginChunk < 0) {
+ beginChunk = chunk;
+ }
+
+ if (prevChunk >= 0 && prevChunk + 1 !== chunk) {
+ groupedChunks.push({
+ beginChunk: beginChunk,
+ endChunk: prevChunk + 1
+ });
+ beginChunk = chunk;
+ }
+
+ if (i + 1 === chunks.length) {
+ groupedChunks.push({
+ beginChunk: beginChunk,
+ endChunk: chunk + 1
+ });
+ }
+
+ prevChunk = chunk;
+ }
+
+ return groupedChunks;
+ }
+ }, {
+ key: "onProgress",
+ value: function onProgress(args) {
+ this.msgHandler.send('DocProgress', {
+ loaded: this.stream.numChunksLoaded * this.chunkSize + args.loaded,
+ total: this.length
+ });
+ }
+ }, {
+ key: "onReceiveData",
+ value: function onReceiveData(args) {
+ var chunk = args.chunk;
+ var isProgressive = args.begin === undefined;
+ var begin = isProgressive ? this.progressiveDataLength : args.begin;
+ var end = begin + chunk.byteLength;
+ var beginChunk = Math.floor(begin / this.chunkSize);
+ var endChunk = end < this.length ? Math.floor(end / this.chunkSize) : Math.ceil(end / this.chunkSize);
+
+ if (isProgressive) {
+ this.stream.onReceiveProgressiveData(chunk);
+ this.progressiveDataLength = end;
+ } else {
+ this.stream.onReceiveData(begin, chunk);
+ }
+
+ if (this.stream.allChunksLoaded()) {
+ this._loadedStreamCapability.resolve(this.stream);
+ }
+
+ var loadedRequests = [];
+
+ for (var _chunk2 = beginChunk; _chunk2 < endChunk; ++_chunk2) {
+ var requestIds = this.requestsByChunk[_chunk2] || [];
+ delete this.requestsByChunk[_chunk2];
+ var _iteratorNormalCompletion4 = true;
+ var _didIteratorError4 = false;
+ var _iteratorError4 = undefined;
+
+ try {
+ for (var _iterator4 = requestIds[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
+ var requestId = _step4.value;
+ var chunksNeeded = this.chunksNeededByRequest[requestId];
+
+ if (_chunk2 in chunksNeeded) {
+ delete chunksNeeded[_chunk2];
+ }
+
+ if (!(0, _util.isEmptyObj)(chunksNeeded)) {
+ continue;
+ }
+
+ loadedRequests.push(requestId);
+ }
+ } catch (err) {
+ _didIteratorError4 = true;
+ _iteratorError4 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion4 && _iterator4["return"] != null) {
+ _iterator4["return"]();
+ }
+ } finally {
+ if (_didIteratorError4) {
+ throw _iteratorError4;
+ }
+ }
+ }
+ }
+
+ if (!this.disableAutoFetch && (0, _util.isEmptyObj)(this.requestsByChunk)) {
+ var nextEmptyChunk;
+
+ if (this.stream.numChunksLoaded === 1) {
+ var lastChunk = this.stream.numChunks - 1;
+
+ if (!this.stream.hasChunk(lastChunk)) {
+ nextEmptyChunk = lastChunk;
+ }
+ } else {
+ nextEmptyChunk = this.stream.nextEmptyChunk(endChunk);
+ }
+
+ if (Number.isInteger(nextEmptyChunk)) {
+ this._requestChunks([nextEmptyChunk]);
+ }
+ }
+
+ for (var _i = 0, _loadedRequests = loadedRequests; _i < _loadedRequests.length; _i++) {
+ var _requestId = _loadedRequests[_i];
+ var capability = this.promisesByRequest[_requestId];
+ delete this.promisesByRequest[_requestId];
+ capability.resolve();
+ }
+
+ this.msgHandler.send('DocProgress', {
+ loaded: this.stream.numChunksLoaded * this.chunkSize,
+ total: this.length
+ });
+ }
+ }, {
+ key: "onError",
+ value: function onError(err) {
+ this._loadedStreamCapability.reject(err);
+ }
+ }, {
+ key: "getBeginChunk",
+ value: function getBeginChunk(begin) {
+ return Math.floor(begin / this.chunkSize);
+ }
+ }, {
+ key: "getEndChunk",
+ value: function getEndChunk(end) {
+ return Math.floor((end - 1) / this.chunkSize) + 1;
+ }
+ }, {
+ key: "abort",
+ value: function abort() {
+ this.aborted = true;
+
+ if (this.pdfNetworkStream) {
+ this.pdfNetworkStream.cancelAllRequests('abort');
+ }
+
+ for (var requestId in this.promisesByRequest) {
+ this.promisesByRequest[requestId].reject(new Error('Request was aborted'));
+ }
+ }
+ }]);
+
+ return ChunkedStreamManager;
+}();
+
+exports.ChunkedStreamManager = ChunkedStreamManager;
+
+/***/ }),
+/* 154 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.getLookupTableFactory = getLookupTableFactory;
+exports.getInheritableProperty = getInheritableProperty;
+exports.toRomanNumerals = toRomanNumerals;
+exports.XRefParseException = exports.XRefEntryException = exports.MissingDataException = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+function getLookupTableFactory(initializer) {
+ var lookup;
+ return function () {
+ if (initializer) {
+ lookup = Object.create(null);
+ initializer(lookup);
+ initializer = null;
+ }
+
+ return lookup;
+ };
+}
+
+var MissingDataException = function MissingDataExceptionClosure() {
+ function MissingDataException(begin, end) {
+ this.begin = begin;
+ this.end = end;
+ this.message = "Missing data [".concat(begin, ", ").concat(end, ")");
+ }
+
+ MissingDataException.prototype = new Error();
+ MissingDataException.prototype.name = 'MissingDataException';
+ MissingDataException.constructor = MissingDataException;
+ return MissingDataException;
+}();
+
+exports.MissingDataException = MissingDataException;
+
+var XRefEntryException = function XRefEntryExceptionClosure() {
+ function XRefEntryException(msg) {
+ this.message = msg;
+ }
+
+ XRefEntryException.prototype = new Error();
+ XRefEntryException.prototype.name = 'XRefEntryException';
+ XRefEntryException.constructor = XRefEntryException;
+ return XRefEntryException;
+}();
+
+exports.XRefEntryException = XRefEntryException;
+
+var XRefParseException = function XRefParseExceptionClosure() {
+ function XRefParseException(msg) {
+ this.message = msg;
+ }
+
+ XRefParseException.prototype = new Error();
+ XRefParseException.prototype.name = 'XRefParseException';
+ XRefParseException.constructor = XRefParseException;
+ return XRefParseException;
+}();
+
+exports.XRefParseException = XRefParseException;
+
+function getInheritableProperty(_ref) {
+ var dict = _ref.dict,
+ key = _ref.key,
+ _ref$getArray = _ref.getArray,
+ getArray = _ref$getArray === void 0 ? false : _ref$getArray,
+ _ref$stopWhenFound = _ref.stopWhenFound,
+ stopWhenFound = _ref$stopWhenFound === void 0 ? true : _ref$stopWhenFound;
+ var LOOP_LIMIT = 100;
+ var loopCount = 0;
+ var values;
+
+ while (dict) {
+ var value = getArray ? dict.getArray(key) : dict.get(key);
+
+ if (value !== undefined) {
+ if (stopWhenFound) {
+ return value;
+ }
+
+ if (!values) {
+ values = [];
+ }
+
+ values.push(value);
+ }
+
+ if (++loopCount > LOOP_LIMIT) {
+ (0, _util.warn)("getInheritableProperty: maximum loop count exceeded for \"".concat(key, "\""));
+ break;
+ }
+
+ dict = dict.get('Parent');
+ }
+
+ return values;
+}
+
+var ROMAN_NUMBER_MAP = ['', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'];
+
+function toRomanNumerals(number) {
+ var lowerCase = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ (0, _util.assert)(Number.isInteger(number) && number > 0, 'The number should be a positive integer.');
+ var pos,
+ romanBuf = [];
+
+ while (number >= 1000) {
+ number -= 1000;
+ romanBuf.push('M');
+ }
+
+ pos = number / 100 | 0;
+ number %= 100;
+ romanBuf.push(ROMAN_NUMBER_MAP[pos]);
+ pos = number / 10 | 0;
+ number %= 10;
+ romanBuf.push(ROMAN_NUMBER_MAP[10 + pos]);
+ romanBuf.push(ROMAN_NUMBER_MAP[20 + number]);
+ var romanStr = romanBuf.join('');
+ return lowerCase ? romanStr.toLowerCase() : romanStr;
+}
+
+/***/ }),
+/* 155 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.PDFDocument = exports.Page = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _obj = __w_pdfjs_require__(156);
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _core_utils = __w_pdfjs_require__(154);
+
+var _stream2 = __w_pdfjs_require__(158);
+
+var _annotation = __w_pdfjs_require__(170);
+
+var _crypto = __w_pdfjs_require__(168);
+
+var _parser = __w_pdfjs_require__(157);
+
+var _operator_list = __w_pdfjs_require__(171);
+
+var _evaluator = __w_pdfjs_require__(172);
+
+var _function = __w_pdfjs_require__(186);
+
+function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
+
+function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
+
+function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
+
+function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var DEFAULT_USER_UNIT = 1.0;
+var LETTER_SIZE_MEDIABOX = [0, 0, 612, 792];
+
+function isAnnotationRenderable(annotation, intent) {
+ return intent === 'display' && annotation.viewable || intent === 'print' && annotation.printable;
+}
+
+var Page =
+/*#__PURE__*/
+function () {
+ function Page(_ref) {
+ var pdfManager = _ref.pdfManager,
+ xref = _ref.xref,
+ pageIndex = _ref.pageIndex,
+ pageDict = _ref.pageDict,
+ ref = _ref.ref,
+ fontCache = _ref.fontCache,
+ builtInCMapCache = _ref.builtInCMapCache,
+ pdfFunctionFactory = _ref.pdfFunctionFactory;
+
+ _classCallCheck(this, Page);
+
+ this.pdfManager = pdfManager;
+ this.pageIndex = pageIndex;
+ this.pageDict = pageDict;
+ this.xref = xref;
+ this.ref = ref;
+ this.fontCache = fontCache;
+ this.builtInCMapCache = builtInCMapCache;
+ this.pdfFunctionFactory = pdfFunctionFactory;
+ this.evaluatorOptions = pdfManager.evaluatorOptions;
+ this.resourcesPromise = null;
+ var idCounters = {
+ obj: 0
+ };
+ this.idFactory = {
+ createObjId: function createObjId() {
+ return "p".concat(pageIndex, "_").concat(++idCounters.obj);
+ },
+ getDocId: function getDocId() {
+ return "g_".concat(pdfManager.docId);
+ }
+ };
+ }
+
+ _createClass(Page, [{
+ key: "_getInheritableProperty",
+ value: function _getInheritableProperty(key) {
+ var getArray = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var value = (0, _core_utils.getInheritableProperty)({
+ dict: this.pageDict,
+ key: key,
+ getArray: getArray,
+ stopWhenFound: false
+ });
+
+ if (!Array.isArray(value)) {
+ return value;
+ }
+
+ if (value.length === 1 || !(0, _primitives.isDict)(value[0])) {
+ return value[0];
+ }
+
+ return _primitives.Dict.merge(this.xref, value);
+ }
+ }, {
+ key: "getContentStream",
+ value: function getContentStream() {
+ var content = this.content;
+ var stream;
+
+ if (Array.isArray(content)) {
+ var xref = this.xref;
+ var streams = [];
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = content[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var _stream = _step.value;
+ streams.push(xref.fetchIfRef(_stream));
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator["return"] != null) {
+ _iterator["return"]();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+
+ stream = new _stream2.StreamsSequenceStream(streams);
+ } else if ((0, _primitives.isStream)(content)) {
+ stream = content;
+ } else {
+ stream = new _stream2.NullStream();
+ }
+
+ return stream;
+ }
+ }, {
+ key: "loadResources",
+ value: function loadResources(keys) {
+ var _this = this;
+
+ if (!this.resourcesPromise) {
+ this.resourcesPromise = this.pdfManager.ensure(this, 'resources');
+ }
+
+ return this.resourcesPromise.then(function () {
+ var objectLoader = new _obj.ObjectLoader(_this.resources, keys, _this.xref);
+ return objectLoader.load();
+ });
+ }
+ }, {
+ key: "getOperatorList",
+ value: function getOperatorList(_ref2) {
+ var _this2 = this;
+
+ var handler = _ref2.handler,
+ task = _ref2.task,
+ intent = _ref2.intent,
+ renderInteractiveForms = _ref2.renderInteractiveForms;
+ var contentStreamPromise = this.pdfManager.ensure(this, 'getContentStream');
+ var resourcesPromise = this.loadResources(['ExtGState', 'ColorSpace', 'Pattern', 'Shading', 'XObject', 'Font']);
+ var partialEvaluator = new _evaluator.PartialEvaluator({
+ xref: this.xref,
+ handler: handler,
+ pageIndex: this.pageIndex,
+ idFactory: this.idFactory,
+ fontCache: this.fontCache,
+ builtInCMapCache: this.builtInCMapCache,
+ options: this.evaluatorOptions,
+ pdfFunctionFactory: this.pdfFunctionFactory
+ });
+ var dataPromises = Promise.all([contentStreamPromise, resourcesPromise]);
+ var pageListPromise = dataPromises.then(function (_ref3) {
+ var _ref4 = _slicedToArray(_ref3, 1),
+ contentStream = _ref4[0];
+
+ var opList = new _operator_list.OperatorList(intent, handler, _this2.pageIndex);
+ handler.send('StartRenderPage', {
+ transparency: partialEvaluator.hasBlendModes(_this2.resources),
+ pageIndex: _this2.pageIndex,
+ intent: intent
+ });
+ return partialEvaluator.getOperatorList({
+ stream: contentStream,
+ task: task,
+ resources: _this2.resources,
+ operatorList: opList
+ }).then(function () {
+ return opList;
+ });
+ });
+ return Promise.all([pageListPromise, this._parsedAnnotations]).then(function (_ref5) {
+ var _ref6 = _slicedToArray(_ref5, 2),
+ pageOpList = _ref6[0],
+ annotations = _ref6[1];
+
+ if (annotations.length === 0) {
+ pageOpList.flush(true);
+ return pageOpList;
+ }
+
+ var opListPromises = [];
+ var _iteratorNormalCompletion2 = true;
+ var _didIteratorError2 = false;
+ var _iteratorError2 = undefined;
+
+ try {
+ for (var _iterator2 = annotations[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
+ var annotation = _step2.value;
+
+ if (isAnnotationRenderable(annotation, intent)) {
+ opListPromises.push(annotation.getOperatorList(partialEvaluator, task, renderInteractiveForms));
+ }
+ }
+ } catch (err) {
+ _didIteratorError2 = true;
+ _iteratorError2 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) {
+ _iterator2["return"]();
+ }
+ } finally {
+ if (_didIteratorError2) {
+ throw _iteratorError2;
+ }
+ }
+ }
+
+ return Promise.all(opListPromises).then(function (opLists) {
+ pageOpList.addOp(_util.OPS.beginAnnotations, []);
+ var _iteratorNormalCompletion3 = true;
+ var _didIteratorError3 = false;
+ var _iteratorError3 = undefined;
+
+ try {
+ for (var _iterator3 = opLists[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
+ var opList = _step3.value;
+ pageOpList.addOpList(opList);
+ }
+ } catch (err) {
+ _didIteratorError3 = true;
+ _iteratorError3 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion3 && _iterator3["return"] != null) {
+ _iterator3["return"]();
+ }
+ } finally {
+ if (_didIteratorError3) {
+ throw _iteratorError3;
+ }
+ }
+ }
+
+ pageOpList.addOp(_util.OPS.endAnnotations, []);
+ pageOpList.flush(true);
+ return pageOpList;
+ });
+ });
+ }
+ }, {
+ key: "extractTextContent",
+ value: function extractTextContent(_ref7) {
+ var _this3 = this;
+
+ var handler = _ref7.handler,
+ task = _ref7.task,
+ normalizeWhitespace = _ref7.normalizeWhitespace,
+ sink = _ref7.sink,
+ combineTextItems = _ref7.combineTextItems;
+ var contentStreamPromise = this.pdfManager.ensure(this, 'getContentStream');
+ var resourcesPromise = this.loadResources(['ExtGState', 'XObject', 'Font']);
+ var dataPromises = Promise.all([contentStreamPromise, resourcesPromise]);
+ return dataPromises.then(function (_ref8) {
+ var _ref9 = _slicedToArray(_ref8, 1),
+ contentStream = _ref9[0];
+
+ var partialEvaluator = new _evaluator.PartialEvaluator({
+ xref: _this3.xref,
+ handler: handler,
+ pageIndex: _this3.pageIndex,
+ idFactory: _this3.idFactory,
+ fontCache: _this3.fontCache,
+ builtInCMapCache: _this3.builtInCMapCache,
+ options: _this3.evaluatorOptions,
+ pdfFunctionFactory: _this3.pdfFunctionFactory
+ });
+ return partialEvaluator.getTextContent({
+ stream: contentStream,
+ task: task,
+ resources: _this3.resources,
+ normalizeWhitespace: normalizeWhitespace,
+ combineTextItems: combineTextItems,
+ sink: sink
+ });
+ });
+ }
+ }, {
+ key: "getAnnotationsData",
+ value: function getAnnotationsData(intent) {
+ return this._parsedAnnotations.then(function (annotations) {
+ var annotationsData = [];
+
+ for (var i = 0, ii = annotations.length; i < ii; i++) {
+ if (!intent || isAnnotationRenderable(annotations[i], intent)) {
+ annotationsData.push(annotations[i].data);
+ }
+ }
+
+ return annotationsData;
+ });
+ }
+ }, {
+ key: "content",
+ get: function get() {
+ return this.pageDict.get('Contents');
+ }
+ }, {
+ key: "resources",
+ get: function get() {
+ return (0, _util.shadow)(this, 'resources', this._getInheritableProperty('Resources') || _primitives.Dict.empty);
+ }
+ }, {
+ key: "mediaBox",
+ get: function get() {
+ var mediaBox = this._getInheritableProperty('MediaBox', true);
+
+ if (!Array.isArray(mediaBox) || mediaBox.length !== 4) {
+ return (0, _util.shadow)(this, 'mediaBox', LETTER_SIZE_MEDIABOX);
+ }
+
+ return (0, _util.shadow)(this, 'mediaBox', mediaBox);
+ }
+ }, {
+ key: "cropBox",
+ get: function get() {
+ var cropBox = this._getInheritableProperty('CropBox', true);
+
+ if (!Array.isArray(cropBox) || cropBox.length !== 4) {
+ return (0, _util.shadow)(this, 'cropBox', this.mediaBox);
+ }
+
+ return (0, _util.shadow)(this, 'cropBox', cropBox);
+ }
+ }, {
+ key: "userUnit",
+ get: function get() {
+ var obj = this.pageDict.get('UserUnit');
+
+ if (!(0, _util.isNum)(obj) || obj <= 0) {
+ obj = DEFAULT_USER_UNIT;
+ }
+
+ return (0, _util.shadow)(this, 'userUnit', obj);
+ }
+ }, {
+ key: "view",
+ get: function get() {
+ var mediaBox = this.mediaBox,
+ cropBox = this.cropBox;
+
+ if (mediaBox === cropBox) {
+ return (0, _util.shadow)(this, 'view', mediaBox);
+ }
+
+ var intersection = _util.Util.intersect(cropBox, mediaBox);
+
+ return (0, _util.shadow)(this, 'view', intersection || mediaBox);
+ }
+ }, {
+ key: "rotate",
+ get: function get() {
+ var rotate = this._getInheritableProperty('Rotate') || 0;
+
+ if (rotate % 90 !== 0) {
+ rotate = 0;
+ } else if (rotate >= 360) {
+ rotate = rotate % 360;
+ } else if (rotate < 0) {
+ rotate = (rotate % 360 + 360) % 360;
+ }
+
+ return (0, _util.shadow)(this, 'rotate', rotate);
+ }
+ }, {
+ key: "annotations",
+ get: function get() {
+ return (0, _util.shadow)(this, 'annotations', this._getInheritableProperty('Annots') || []);
+ }
+ }, {
+ key: "_parsedAnnotations",
+ get: function get() {
+ var _this4 = this;
+
+ var parsedAnnotations = this.pdfManager.ensure(this, 'annotations').then(function () {
+ var annotationRefs = _this4.annotations;
+ var annotationPromises = [];
+
+ for (var i = 0, ii = annotationRefs.length; i < ii; i++) {
+ annotationPromises.push(_annotation.AnnotationFactory.create(_this4.xref, annotationRefs[i], _this4.pdfManager, _this4.idFactory));
+ }
+
+ return Promise.all(annotationPromises).then(function (annotations) {
+ return annotations.filter(function isDefined(annotation) {
+ return !!annotation;
+ });
+ }, function (reason) {
+ (0, _util.warn)("_parsedAnnotations: \"".concat(reason, "\"."));
+ return [];
+ });
+ });
+ return (0, _util.shadow)(this, '_parsedAnnotations', parsedAnnotations);
+ }
+ }]);
+
+ return Page;
+}();
+
+exports.Page = Page;
+var FINGERPRINT_FIRST_BYTES = 1024;
+var EMPTY_FINGERPRINT = '\x00\x00\x00\x00\x00\x00\x00' + '\x00\x00\x00\x00\x00\x00\x00\x00\x00';
+
+function find(stream, needle, limit) {
+ var backwards = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
+ (0, _util.assert)(limit > 0, 'The "limit" must be a positive integer.');
+ var str = (0, _util.bytesToString)(stream.peekBytes(limit));
+ var index = backwards ? str.lastIndexOf(needle) : str.indexOf(needle);
+
+ if (index === -1) {
+ return false;
+ }
+
+ stream.pos += index;
+ return true;
+}
+
+var PDFDocument =
+/*#__PURE__*/
+function () {
+ function PDFDocument(pdfManager, arg) {
+ _classCallCheck(this, PDFDocument);
+
+ var stream;
+
+ if ((0, _primitives.isStream)(arg)) {
+ stream = arg;
+ } else if ((0, _util.isArrayBuffer)(arg)) {
+ stream = new _stream2.Stream(arg);
+ } else {
+ throw new Error('PDFDocument: Unknown argument type');
+ }
+
+ if (stream.length <= 0) {
+ throw new Error('PDFDocument: Stream must have data');
+ }
+
+ this.pdfManager = pdfManager;
+ this.stream = stream;
+ this.xref = new _obj.XRef(stream, pdfManager);
+ this.pdfFunctionFactory = new _function.PDFFunctionFactory({
+ xref: this.xref,
+ isEvalSupported: pdfManager.evaluatorOptions.isEvalSupported
+ });
+ this._pagePromises = [];
+ }
+
+ _createClass(PDFDocument, [{
+ key: "parse",
+ value: function parse(recoveryMode) {
+ this.setup(recoveryMode);
+ var version = this.catalog.catDict.get('Version');
+
+ if ((0, _primitives.isName)(version)) {
+ this.pdfFormatVersion = version.name;
+ }
+
+ try {
+ this.acroForm = this.catalog.catDict.get('AcroForm');
+
+ if (this.acroForm) {
+ this.xfa = this.acroForm.get('XFA');
+ var fields = this.acroForm.get('Fields');
+
+ if ((!Array.isArray(fields) || fields.length === 0) && !this.xfa) {
+ this.acroForm = null;
+ }
+ }
+ } catch (ex) {
+ if (ex instanceof _core_utils.MissingDataException) {
+ throw ex;
+ }
+
+ (0, _util.info)('Cannot fetch AcroForm entry; assuming no AcroForms are present');
+ this.acroForm = null;
+ }
+
+ try {
+ var collection = this.catalog.catDict.get('Collection');
+
+ if ((0, _primitives.isDict)(collection) && collection.getKeys().length > 0) {
+ this.collection = collection;
+ }
+ } catch (ex) {
+ if (ex instanceof _core_utils.MissingDataException) {
+ throw ex;
+ }
+
+ (0, _util.info)('Cannot fetch Collection dictionary.');
+ }
+ }
+ }, {
+ key: "checkHeader",
+ value: function checkHeader() {
+ var stream = this.stream;
+ stream.reset();
+
+ if (!find(stream, '%PDF-', 1024)) {
+ return;
+ }
+
+ stream.moveStart();
+ var MAX_PDF_VERSION_LENGTH = 12;
+ var version = '',
+ ch;
+
+ while ((ch = stream.getByte()) > 0x20) {
+ if (version.length >= MAX_PDF_VERSION_LENGTH) {
+ break;
+ }
+
+ version += String.fromCharCode(ch);
+ }
+
+ if (!this.pdfFormatVersion) {
+ this.pdfFormatVersion = version.substring(5);
+ }
+ }
+ }, {
+ key: "parseStartXRef",
+ value: function parseStartXRef() {
+ this.xref.setStartXRef(this.startXRef);
+ }
+ }, {
+ key: "setup",
+ value: function setup(recoveryMode) {
+ this.xref.parse(recoveryMode);
+ this.catalog = new _obj.Catalog(this.pdfManager, this.xref);
+ }
+ }, {
+ key: "_getLinearizationPage",
+ value: function _getLinearizationPage(pageIndex) {
+ var catalog = this.catalog,
+ linearization = this.linearization;
+ (0, _util.assert)(linearization && linearization.pageFirst === pageIndex);
+
+ var ref = _primitives.Ref.get(linearization.objectNumberFirst, 0);
+
+ return this.xref.fetchAsync(ref).then(function (obj) {
+ if ((0, _primitives.isDict)(obj, 'Page') || (0, _primitives.isDict)(obj) && !obj.has('Type') && obj.has('Contents')) {
+ if (ref && !catalog.pageKidsCountCache.has(ref)) {
+ catalog.pageKidsCountCache.put(ref, 1);
+ }
+
+ return [obj, ref];
+ }
+
+ throw new _util.FormatError('The Linearization dictionary doesn\'t point ' + 'to a valid Page dictionary.');
+ })["catch"](function (reason) {
+ (0, _util.info)(reason);
+ return catalog.getPageDict(pageIndex);
+ });
+ }
+ }, {
+ key: "getPage",
+ value: function getPage(pageIndex) {
+ var _this5 = this;
+
+ if (this._pagePromises[pageIndex] !== undefined) {
+ return this._pagePromises[pageIndex];
+ }
+
+ var catalog = this.catalog,
+ linearization = this.linearization;
+ var promise = linearization && linearization.pageFirst === pageIndex ? this._getLinearizationPage(pageIndex) : catalog.getPageDict(pageIndex);
+ return this._pagePromises[pageIndex] = promise.then(function (_ref10) {
+ var _ref11 = _slicedToArray(_ref10, 2),
+ pageDict = _ref11[0],
+ ref = _ref11[1];
+
+ return new Page({
+ pdfManager: _this5.pdfManager,
+ xref: _this5.xref,
+ pageIndex: pageIndex,
+ pageDict: pageDict,
+ ref: ref,
+ fontCache: catalog.fontCache,
+ builtInCMapCache: catalog.builtInCMapCache,
+ pdfFunctionFactory: _this5.pdfFunctionFactory
+ });
+ });
+ }
+ }, {
+ key: "checkFirstPage",
+ value: function checkFirstPage() {
+ var _this6 = this;
+
+ return this.getPage(0)["catch"](function (reason) {
+ if (reason instanceof _core_utils.XRefEntryException) {
+ _this6._pagePromises.length = 0;
+
+ _this6.cleanup();
+
+ throw new _core_utils.XRefParseException();
+ }
+ });
+ }
+ }, {
+ key: "fontFallback",
+ value: function fontFallback(id, handler) {
+ return this.catalog.fontFallback(id, handler);
+ }
+ }, {
+ key: "cleanup",
+ value: function cleanup() {
+ return this.catalog.cleanup();
+ }
+ }, {
+ key: "linearization",
+ get: function get() {
+ var linearization = null;
+
+ try {
+ linearization = _parser.Linearization.create(this.stream);
+ } catch (err) {
+ if (err instanceof _core_utils.MissingDataException) {
+ throw err;
+ }
+
+ (0, _util.info)(err);
+ }
+
+ return (0, _util.shadow)(this, 'linearization', linearization);
+ }
+ }, {
+ key: "startXRef",
+ get: function get() {
+ var stream = this.stream;
+ var startXRef = 0;
+
+ if (this.linearization) {
+ stream.reset();
+
+ if (find(stream, 'endobj', 1024)) {
+ startXRef = stream.pos + 6;
+ }
+ } else {
+ var step = 1024;
+ var startXRefLength = 'startxref'.length;
+ var found = false,
+ pos = stream.end;
+
+ while (!found && pos > 0) {
+ pos -= step - startXRefLength;
+
+ if (pos < 0) {
+ pos = 0;
+ }
+
+ stream.pos = pos;
+ found = find(stream, 'startxref', step, true);
+ }
+
+ if (found) {
+ stream.skip(9);
+ var ch;
+
+ do {
+ ch = stream.getByte();
+ } while ((0, _util.isSpace)(ch));
+
+ var str = '';
+
+ while (ch >= 0x20 && ch <= 0x39) {
+ str += String.fromCharCode(ch);
+ ch = stream.getByte();
+ }
+
+ startXRef = parseInt(str, 10);
+
+ if (isNaN(startXRef)) {
+ startXRef = 0;
+ }
+ }
+ }
+
+ return (0, _util.shadow)(this, 'startXRef', startXRef);
+ }
+ }, {
+ key: "numPages",
+ get: function get() {
+ var linearization = this.linearization;
+ var num = linearization ? linearization.numPages : this.catalog.numPages;
+ return (0, _util.shadow)(this, 'numPages', num);
+ }
+ }, {
+ key: "documentInfo",
+ get: function get() {
+ var DocumentInfoValidators = {
+ Title: _util.isString,
+ Author: _util.isString,
+ Subject: _util.isString,
+ Keywords: _util.isString,
+ Creator: _util.isString,
+ Producer: _util.isString,
+ CreationDate: _util.isString,
+ ModDate: _util.isString,
+ Trapped: _primitives.isName
+ };
+ var docInfo = {
+ PDFFormatVersion: this.pdfFormatVersion,
+ IsLinearized: !!this.linearization,
+ IsAcroFormPresent: !!this.acroForm,
+ IsXFAPresent: !!this.xfa,
+ IsCollectionPresent: !!this.collection
+ };
+ var infoDict;
+
+ try {
+ infoDict = this.xref.trailer.get('Info');
+ } catch (err) {
+ if (err instanceof _core_utils.MissingDataException) {
+ throw err;
+ }
+
+ (0, _util.info)('The document information dictionary is invalid.');
+ }
+
+ if ((0, _primitives.isDict)(infoDict)) {
+ var _iteratorNormalCompletion4 = true;
+ var _didIteratorError4 = false;
+ var _iteratorError4 = undefined;
+
+ try {
+ for (var _iterator4 = infoDict.getKeys()[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
+ var key = _step4.value;
+ var value = infoDict.get(key);
+
+ if (DocumentInfoValidators[key]) {
+ if (DocumentInfoValidators[key](value)) {
+ docInfo[key] = typeof value !== 'string' ? value : (0, _util.stringToPDFString)(value);
+ } else {
+ (0, _util.info)("Bad value in document info for \"".concat(key, "\"."));
+ }
+ } else if (typeof key === 'string') {
+ var customValue = void 0;
+
+ if ((0, _util.isString)(value)) {
+ customValue = (0, _util.stringToPDFString)(value);
+ } else if ((0, _primitives.isName)(value) || (0, _util.isNum)(value) || (0, _util.isBool)(value)) {
+ customValue = value;
+ } else {
+ (0, _util.info)("Unsupported value in document info for (custom) \"".concat(key, "\"."));
+ continue;
+ }
+
+ if (!docInfo['Custom']) {
+ docInfo['Custom'] = Object.create(null);
+ }
+
+ docInfo['Custom'][key] = customValue;
+ }
+ }
+ } catch (err) {
+ _didIteratorError4 = true;
+ _iteratorError4 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion4 && _iterator4["return"] != null) {
+ _iterator4["return"]();
+ }
+ } finally {
+ if (_didIteratorError4) {
+ throw _iteratorError4;
+ }
+ }
+ }
+ }
+
+ return (0, _util.shadow)(this, 'documentInfo', docInfo);
+ }
+ }, {
+ key: "fingerprint",
+ get: function get() {
+ var hash;
+ var idArray = this.xref.trailer.get('ID');
+
+ if (Array.isArray(idArray) && idArray[0] && (0, _util.isString)(idArray[0]) && idArray[0] !== EMPTY_FINGERPRINT) {
+ hash = (0, _util.stringToBytes)(idArray[0]);
+ } else {
+ if (this.stream.ensureRange) {
+ this.stream.ensureRange(0, Math.min(FINGERPRINT_FIRST_BYTES, this.stream.end));
+ }
+
+ hash = (0, _crypto.calculateMD5)(this.stream.bytes.subarray(0, FINGERPRINT_FIRST_BYTES), 0, FINGERPRINT_FIRST_BYTES);
+ }
+
+ var fingerprint = '';
+
+ for (var i = 0, ii = hash.length; i < ii; i++) {
+ var hex = hash[i].toString(16);
+ fingerprint += hex.length === 1 ? '0' + hex : hex;
+ }
+
+ return (0, _util.shadow)(this, 'fingerprint', fingerprint);
+ }
+ }]);
+
+ return PDFDocument;
+}();
+
+exports.PDFDocument = PDFDocument;
+
+/***/ }),
+/* 156 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.FileSpec = exports.XRef = exports.ObjectLoader = exports.Catalog = void 0;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(2));
+
+var _util = __w_pdfjs_require__(5);
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _parser = __w_pdfjs_require__(157);
+
+var _core_utils = __w_pdfjs_require__(154);
+
+var _chunked_stream = __w_pdfjs_require__(153);
+
+var _crypto = __w_pdfjs_require__(168);
+
+var _colorspace = __w_pdfjs_require__(169);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
+
+function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
+
+function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
+
+function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
+
+function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
+
+function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
+
+function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+function fetchDestination(dest) {
+ return (0, _primitives.isDict)(dest) ? dest.get('D') : dest;
+}
+
+var Catalog =
+/*#__PURE__*/
+function () {
+ function Catalog(pdfManager, xref) {
+ _classCallCheck(this, Catalog);
+
+ this.pdfManager = pdfManager;
+ this.xref = xref;
+ this.catDict = xref.getCatalogObj();
+
+ if (!(0, _primitives.isDict)(this.catDict)) {
+ throw new _util.FormatError('Catalog object is not a dictionary.');
+ }
+
+ this.fontCache = new _primitives.RefSetCache();
+ this.builtInCMapCache = new Map();
+ this.pageKidsCountCache = new _primitives.RefSetCache();
+ }
+
+ _createClass(Catalog, [{
+ key: "_readDocumentOutline",
+ value: function _readDocumentOutline() {
+ var obj = this.catDict.get('Outlines');
+
+ if (!(0, _primitives.isDict)(obj)) {
+ return null;
+ }
+
+ obj = obj.getRaw('First');
+
+ if (!(0, _primitives.isRef)(obj)) {
+ return null;
+ }
+
+ var root = {
+ items: []
+ };
+ var queue = [{
+ obj: obj,
+ parent: root
+ }];
+ var processed = new _primitives.RefSet();
+ processed.put(obj);
+ var xref = this.xref,
+ blackColor = new Uint8ClampedArray(3);
+
+ while (queue.length > 0) {
+ var i = queue.shift();
+ var outlineDict = xref.fetchIfRef(i.obj);
+
+ if (outlineDict === null) {
+ continue;
+ }
+
+ if (!outlineDict.has('Title')) {
+ throw new _util.FormatError('Invalid outline item encountered.');
+ }
+
+ var data = {
+ url: null,
+ dest: null
+ };
+ Catalog.parseDestDictionary({
+ destDict: outlineDict,
+ resultObj: data,
+ docBaseUrl: this.pdfManager.docBaseUrl
+ });
+ var title = outlineDict.get('Title');
+ var flags = outlineDict.get('F') || 0;
+ var color = outlineDict.getArray('C');
+ var count = outlineDict.get('Count');
+ var rgbColor = blackColor;
+
+ if (Array.isArray(color) && color.length === 3 && (color[0] !== 0 || color[1] !== 0 || color[2] !== 0)) {
+ rgbColor = _colorspace.ColorSpace.singletons.rgb.getRgb(color, 0);
+ }
+
+ var outlineItem = {
+ dest: data.dest,
+ url: data.url,
+ unsafeUrl: data.unsafeUrl,
+ newWindow: data.newWindow,
+ title: (0, _util.stringToPDFString)(title),
+ color: rgbColor,
+ count: Number.isInteger(count) ? count : undefined,
+ bold: !!(flags & 2),
+ italic: !!(flags & 1),
+ items: []
+ };
+ i.parent.items.push(outlineItem);
+ obj = outlineDict.getRaw('First');
+
+ if ((0, _primitives.isRef)(obj) && !processed.has(obj)) {
+ queue.push({
+ obj: obj,
+ parent: outlineItem
+ });
+ processed.put(obj);
+ }
+
+ obj = outlineDict.getRaw('Next');
+
+ if ((0, _primitives.isRef)(obj) && !processed.has(obj)) {
+ queue.push({
+ obj: obj,
+ parent: i.parent
+ });
+ processed.put(obj);
+ }
+ }
+
+ return root.items.length > 0 ? root.items : null;
+ }
+ }, {
+ key: "_readPermissions",
+ value: function _readPermissions() {
+ var encrypt = this.xref.trailer.get('Encrypt');
+
+ if (!(0, _primitives.isDict)(encrypt)) {
+ return null;
+ }
+
+ var flags = encrypt.get('P');
+
+ if (!(0, _util.isNum)(flags)) {
+ return null;
+ }
+
+ flags += Math.pow(2, 32);
+ var permissions = [];
+
+ for (var key in _util.PermissionFlag) {
+ var value = _util.PermissionFlag[key];
+
+ if (flags & value) {
+ permissions.push(value);
+ }
+ }
+
+ return permissions;
+ }
+ }, {
+ key: "getDestination",
+ value: function getDestination(destinationId) {
+ var obj = this._readDests();
+
+ if (obj instanceof NameTree || obj instanceof _primitives.Dict) {
+ return fetchDestination(obj.get(destinationId) || null);
+ }
+
+ return null;
+ }
+ }, {
+ key: "_readDests",
+ value: function _readDests() {
+ var obj = this.catDict.get('Names');
+
+ if (obj && obj.has('Dests')) {
+ return new NameTree(obj.getRaw('Dests'), this.xref);
+ } else if (this.catDict.has('Dests')) {
+ return this.catDict.get('Dests');
+ }
+
+ return undefined;
+ }
+ }, {
+ key: "_readPageLabels",
+ value: function _readPageLabels() {
+ var obj = this.catDict.getRaw('PageLabels');
+
+ if (!obj) {
+ return null;
+ }
+
+ var pageLabels = new Array(this.numPages);
+ var style = null,
+ prefix = '';
+ var numberTree = new NumberTree(obj, this.xref);
+ var nums = numberTree.getAll();
+ var currentLabel = '',
+ currentIndex = 1;
+
+ for (var i = 0, ii = this.numPages; i < ii; i++) {
+ if (i in nums) {
+ var labelDict = nums[i];
+
+ if (!(0, _primitives.isDict)(labelDict)) {
+ throw new _util.FormatError('PageLabel is not a dictionary.');
+ }
+
+ if (labelDict.has('Type') && !(0, _primitives.isName)(labelDict.get('Type'), 'PageLabel')) {
+ throw new _util.FormatError('Invalid type in PageLabel dictionary.');
+ }
+
+ if (labelDict.has('S')) {
+ var s = labelDict.get('S');
+
+ if (!(0, _primitives.isName)(s)) {
+ throw new _util.FormatError('Invalid style in PageLabel dictionary.');
+ }
+
+ style = s.name;
+ } else {
+ style = null;
+ }
+
+ if (labelDict.has('P')) {
+ var p = labelDict.get('P');
+
+ if (!(0, _util.isString)(p)) {
+ throw new _util.FormatError('Invalid prefix in PageLabel dictionary.');
+ }
+
+ prefix = (0, _util.stringToPDFString)(p);
+ } else {
+ prefix = '';
+ }
+
+ if (labelDict.has('St')) {
+ var st = labelDict.get('St');
+
+ if (!(Number.isInteger(st) && st >= 1)) {
+ throw new _util.FormatError('Invalid start in PageLabel dictionary.');
+ }
+
+ currentIndex = st;
+ } else {
+ currentIndex = 1;
+ }
+ }
+
+ switch (style) {
+ case 'D':
+ currentLabel = currentIndex;
+ break;
+
+ case 'R':
+ case 'r':
+ currentLabel = (0, _core_utils.toRomanNumerals)(currentIndex, style === 'r');
+ break;
+
+ case 'A':
+ case 'a':
+ var LIMIT = 26;
+ var A_UPPER_CASE = 0x41,
+ A_LOWER_CASE = 0x61;
+ var baseCharCode = style === 'a' ? A_LOWER_CASE : A_UPPER_CASE;
+ var letterIndex = currentIndex - 1;
+ var character = String.fromCharCode(baseCharCode + letterIndex % LIMIT);
+ var charBuf = [];
+
+ for (var j = 0, jj = letterIndex / LIMIT | 0; j <= jj; j++) {
+ charBuf.push(character);
+ }
+
+ currentLabel = charBuf.join('');
+ break;
+
+ default:
+ if (style) {
+ throw new _util.FormatError("Invalid style \"".concat(style, "\" in PageLabel dictionary."));
+ }
+
+ currentLabel = '';
+ }
+
+ pageLabels[i] = prefix + currentLabel;
+ currentIndex++;
+ }
+
+ return pageLabels;
+ }
+ }, {
+ key: "fontFallback",
+ value: function fontFallback(id, handler) {
+ var promises = [];
+ this.fontCache.forEach(function (promise) {
+ promises.push(promise);
+ });
+ return Promise.all(promises).then(function (translatedFonts) {
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = translatedFonts[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var translatedFont = _step.value;
+
+ if (translatedFont.loadedName === id) {
+ translatedFont.fallback(handler);
+ return;
+ }
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator["return"] != null) {
+ _iterator["return"]();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+ });
+ }
+ }, {
+ key: "cleanup",
+ value: function cleanup() {
+ var _this = this;
+
+ (0, _primitives.clearPrimitiveCaches)();
+ this.pageKidsCountCache.clear();
+ var promises = [];
+ this.fontCache.forEach(function (promise) {
+ promises.push(promise);
+ });
+ return Promise.all(promises).then(function (translatedFonts) {
+ for (var i = 0, ii = translatedFonts.length; i < ii; i++) {
+ var font = translatedFonts[i].dict;
+ delete font.translated;
+ }
+
+ _this.fontCache.clear();
+
+ _this.builtInCMapCache.clear();
+ });
+ }
+ }, {
+ key: "getPageDict",
+ value: function getPageDict(pageIndex) {
+ var capability = (0, _util.createPromiseCapability)();
+ var nodesToVisit = [this.catDict.getRaw('Pages')];
+ var xref = this.xref,
+ pageKidsCountCache = this.pageKidsCountCache;
+ var count,
+ currentPageIndex = 0;
+
+ function next() {
+ var _loop = function _loop() {
+ var currentNode = nodesToVisit.pop();
+
+ if ((0, _primitives.isRef)(currentNode)) {
+ count = pageKidsCountCache.get(currentNode);
+
+ if (count > 0 && currentPageIndex + count < pageIndex) {
+ currentPageIndex += count;
+ return "continue";
+ }
+
+ xref.fetchAsync(currentNode).then(function (obj) {
+ if ((0, _primitives.isDict)(obj, 'Page') || (0, _primitives.isDict)(obj) && !obj.has('Kids')) {
+ if (pageIndex === currentPageIndex) {
+ if (currentNode && !pageKidsCountCache.has(currentNode)) {
+ pageKidsCountCache.put(currentNode, 1);
+ }
+
+ capability.resolve([obj, currentNode]);
+ } else {
+ currentPageIndex++;
+ next();
+ }
+
+ return;
+ }
+
+ nodesToVisit.push(obj);
+ next();
+ }, capability.reject);
+ return {
+ v: void 0
+ };
+ }
+
+ if (!(0, _primitives.isDict)(currentNode)) {
+ capability.reject(new _util.FormatError('Page dictionary kid reference points to wrong type of object.'));
+ return {
+ v: void 0
+ };
+ }
+
+ count = currentNode.get('Count');
+
+ if (Number.isInteger(count) && count >= 0) {
+ var objId = currentNode.objId;
+
+ if (objId && !pageKidsCountCache.has(objId)) {
+ pageKidsCountCache.put(objId, count);
+ }
+
+ if (currentPageIndex + count <= pageIndex) {
+ currentPageIndex += count;
+ return "continue";
+ }
+ }
+
+ var kids = currentNode.get('Kids');
+
+ if (!Array.isArray(kids)) {
+ if ((0, _primitives.isName)(currentNode.get('Type'), 'Page') || !currentNode.has('Type') && currentNode.has('Contents')) {
+ if (currentPageIndex === pageIndex) {
+ capability.resolve([currentNode, null]);
+ return {
+ v: void 0
+ };
+ }
+
+ currentPageIndex++;
+ return "continue";
+ }
+
+ capability.reject(new _util.FormatError('Page dictionary kids object is not an array.'));
+ return {
+ v: void 0
+ };
+ }
+
+ for (var last = kids.length - 1; last >= 0; last--) {
+ nodesToVisit.push(kids[last]);
+ }
+ };
+
+ while (nodesToVisit.length) {
+ var _ret = _loop();
+
+ switch (_ret) {
+ case "continue":
+ continue;
+
+ default:
+ if (_typeof(_ret) === "object") return _ret.v;
+ }
+ }
+
+ capability.reject(new Error("Page index ".concat(pageIndex, " not found.")));
+ }
+
+ next();
+ return capability.promise;
+ }
+ }, {
+ key: "getPageIndex",
+ value: function getPageIndex(pageRef) {
+ var xref = this.xref;
+
+ function pagesBeforeRef(kidRef) {
+ var total = 0,
+ parentRef;
+ return xref.fetchAsync(kidRef).then(function (node) {
+ if ((0, _primitives.isRefsEqual)(kidRef, pageRef) && !(0, _primitives.isDict)(node, 'Page') && !((0, _primitives.isDict)(node) && !node.has('Type') && node.has('Contents'))) {
+ throw new _util.FormatError('The reference does not point to a /Page dictionary.');
+ }
+
+ if (!node) {
+ return null;
+ }
+
+ if (!(0, _primitives.isDict)(node)) {
+ throw new _util.FormatError('Node must be a dictionary.');
+ }
+
+ parentRef = node.getRaw('Parent');
+ return node.getAsync('Parent');
+ }).then(function (parent) {
+ if (!parent) {
+ return null;
+ }
+
+ if (!(0, _primitives.isDict)(parent)) {
+ throw new _util.FormatError('Parent must be a dictionary.');
+ }
+
+ return parent.getAsync('Kids');
+ }).then(function (kids) {
+ if (!kids) {
+ return null;
+ }
+
+ var kidPromises = [];
+ var found = false;
+
+ for (var i = 0, ii = kids.length; i < ii; i++) {
+ var kid = kids[i];
+
+ if (!(0, _primitives.isRef)(kid)) {
+ throw new _util.FormatError('Kid must be a reference.');
+ }
+
+ if ((0, _primitives.isRefsEqual)(kid, kidRef)) {
+ found = true;
+ break;
+ }
+
+ kidPromises.push(xref.fetchAsync(kid).then(function (kid) {
+ if (!(0, _primitives.isDict)(kid)) {
+ throw new _util.FormatError('Kid node must be a dictionary.');
+ }
+
+ if (kid.has('Count')) {
+ total += kid.get('Count');
+ } else {
+ total++;
+ }
+ }));
+ }
+
+ if (!found) {
+ throw new _util.FormatError('Kid reference not found in parent\'s kids.');
+ }
+
+ return Promise.all(kidPromises).then(function () {
+ return [total, parentRef];
+ });
+ });
+ }
+
+ var total = 0;
+
+ function next(ref) {
+ return pagesBeforeRef(ref).then(function (args) {
+ if (!args) {
+ return total;
+ }
+
+ var _args = _slicedToArray(args, 2),
+ count = _args[0],
+ parentRef = _args[1];
+
+ total += count;
+ return next(parentRef);
+ });
+ }
+
+ return next(pageRef);
+ }
+ }, {
+ key: "metadata",
+ get: function get() {
+ var streamRef = this.catDict.getRaw('Metadata');
+
+ if (!(0, _primitives.isRef)(streamRef)) {
+ return (0, _util.shadow)(this, 'metadata', null);
+ }
+
+ var suppressEncryption = !(this.xref.encrypt && this.xref.encrypt.encryptMetadata);
+ var stream = this.xref.fetch(streamRef, suppressEncryption);
+ var metadata;
+
+ if (stream && (0, _primitives.isDict)(stream.dict)) {
+ var type = stream.dict.get('Type');
+ var subtype = stream.dict.get('Subtype');
+
+ if ((0, _primitives.isName)(type, 'Metadata') && (0, _primitives.isName)(subtype, 'XML')) {
+ try {
+ metadata = (0, _util.stringToUTF8String)((0, _util.bytesToString)(stream.getBytes()));
+ } catch (e) {
+ if (e instanceof _core_utils.MissingDataException) {
+ throw e;
+ }
+
+ (0, _util.info)('Skipping invalid metadata.');
+ }
+ }
+ }
+
+ return (0, _util.shadow)(this, 'metadata', metadata);
+ }
+ }, {
+ key: "toplevelPagesDict",
+ get: function get() {
+ var pagesObj = this.catDict.get('Pages');
+
+ if (!(0, _primitives.isDict)(pagesObj)) {
+ throw new _util.FormatError('Invalid top-level pages dictionary.');
+ }
+
+ return (0, _util.shadow)(this, 'toplevelPagesDict', pagesObj);
+ }
+ }, {
+ key: "documentOutline",
+ get: function get() {
+ var obj = null;
+
+ try {
+ obj = this._readDocumentOutline();
+ } catch (ex) {
+ if (ex instanceof _core_utils.MissingDataException) {
+ throw ex;
+ }
+
+ (0, _util.warn)('Unable to read document outline.');
+ }
+
+ return (0, _util.shadow)(this, 'documentOutline', obj);
+ }
+ }, {
+ key: "permissions",
+ get: function get() {
+ var permissions = null;
+
+ try {
+ permissions = this._readPermissions();
+ } catch (ex) {
+ if (ex instanceof _core_utils.MissingDataException) {
+ throw ex;
+ }
+
+ (0, _util.warn)('Unable to read permissions.');
+ }
+
+ return (0, _util.shadow)(this, 'permissions', permissions);
+ }
+ }, {
+ key: "numPages",
+ get: function get() {
+ var obj = this.toplevelPagesDict.get('Count');
+
+ if (!Number.isInteger(obj)) {
+ throw new _util.FormatError('Page count in top-level pages dictionary is not an integer.');
+ }
+
+ return (0, _util.shadow)(this, 'numPages', obj);
+ }
+ }, {
+ key: "destinations",
+ get: function get() {
+ var obj = this._readDests(),
+ dests = Object.create(null);
+
+ if (obj instanceof NameTree) {
+ var names = obj.getAll();
+
+ for (var name in names) {
+ dests[name] = fetchDestination(names[name]);
+ }
+ } else if (obj instanceof _primitives.Dict) {
+ obj.forEach(function (key, value) {
+ if (value) {
+ dests[key] = fetchDestination(value);
+ }
+ });
+ }
+
+ return (0, _util.shadow)(this, 'destinations', dests);
+ }
+ }, {
+ key: "pageLabels",
+ get: function get() {
+ var obj = null;
+
+ try {
+ obj = this._readPageLabels();
+ } catch (ex) {
+ if (ex instanceof _core_utils.MissingDataException) {
+ throw ex;
+ }
+
+ (0, _util.warn)('Unable to read page labels.');
+ }
+
+ return (0, _util.shadow)(this, 'pageLabels', obj);
+ }
+ }, {
+ key: "pageLayout",
+ get: function get() {
+ var obj = this.catDict.get('PageLayout');
+ var pageLayout = '';
+
+ if ((0, _primitives.isName)(obj)) {
+ switch (obj.name) {
+ case 'SinglePage':
+ case 'OneColumn':
+ case 'TwoColumnLeft':
+ case 'TwoColumnRight':
+ case 'TwoPageLeft':
+ case 'TwoPageRight':
+ pageLayout = obj.name;
+ }
+ }
+
+ return (0, _util.shadow)(this, 'pageLayout', pageLayout);
+ }
+ }, {
+ key: "pageMode",
+ get: function get() {
+ var obj = this.catDict.get('PageMode');
+ var pageMode = 'UseNone';
+
+ if ((0, _primitives.isName)(obj)) {
+ switch (obj.name) {
+ case 'UseNone':
+ case 'UseOutlines':
+ case 'UseThumbs':
+ case 'FullScreen':
+ case 'UseOC':
+ case 'UseAttachments':
+ pageMode = obj.name;
+ }
+ }
+
+ return (0, _util.shadow)(this, 'pageMode', pageMode);
+ }
+ }, {
+ key: "viewerPreferences",
+ get: function get() {
+ var _this2 = this;
+
+ var ViewerPreferencesValidators = {
+ HideToolbar: _util.isBool,
+ HideMenubar: _util.isBool,
+ HideWindowUI: _util.isBool,
+ FitWindow: _util.isBool,
+ CenterWindow: _util.isBool,
+ DisplayDocTitle: _util.isBool,
+ NonFullScreenPageMode: _primitives.isName,
+ Direction: _primitives.isName,
+ ViewArea: _primitives.isName,
+ ViewClip: _primitives.isName,
+ PrintArea: _primitives.isName,
+ PrintClip: _primitives.isName,
+ PrintScaling: _primitives.isName,
+ Duplex: _primitives.isName,
+ PickTrayByPDFSize: _util.isBool,
+ PrintPageRange: Array.isArray,
+ NumCopies: Number.isInteger
+ };
+ var obj = this.catDict.get('ViewerPreferences');
+ var prefs = Object.create(null);
+
+ if ((0, _primitives.isDict)(obj)) {
+ for (var key in ViewerPreferencesValidators) {
+ if (!obj.has(key)) {
+ continue;
+ }
+
+ var value = obj.get(key);
+
+ if (!ViewerPreferencesValidators[key](value)) {
+ (0, _util.info)("Bad value in ViewerPreferences for \"".concat(key, "\"."));
+ continue;
+ }
+
+ var prefValue = void 0;
+
+ switch (key) {
+ case 'NonFullScreenPageMode':
+ switch (value.name) {
+ case 'UseNone':
+ case 'UseOutlines':
+ case 'UseThumbs':
+ case 'UseOC':
+ prefValue = value.name;
+ break;
+
+ default:
+ prefValue = 'UseNone';
+ }
+
+ break;
+
+ case 'Direction':
+ switch (value.name) {
+ case 'L2R':
+ case 'R2L':
+ prefValue = value.name;
+ break;
+
+ default:
+ prefValue = 'L2R';
+ }
+
+ break;
+
+ case 'ViewArea':
+ case 'ViewClip':
+ case 'PrintArea':
+ case 'PrintClip':
+ switch (value.name) {
+ case 'MediaBox':
+ case 'CropBox':
+ case 'BleedBox':
+ case 'TrimBox':
+ case 'ArtBox':
+ prefValue = value.name;
+ break;
+
+ default:
+ prefValue = 'CropBox';
+ }
+
+ break;
+
+ case 'PrintScaling':
+ switch (value.name) {
+ case 'None':
+ case 'AppDefault':
+ prefValue = value.name;
+ break;
+
+ default:
+ prefValue = 'AppDefault';
+ }
+
+ break;
+
+ case 'Duplex':
+ switch (value.name) {
+ case 'Simplex':
+ case 'DuplexFlipShortEdge':
+ case 'DuplexFlipLongEdge':
+ prefValue = value.name;
+ break;
+
+ default:
+ prefValue = 'None';
+ }
+
+ break;
+
+ case 'PrintPageRange':
+ var length = value.length;
+
+ if (length % 2 !== 0) {
+ break;
+ }
+
+ var isValid = value.every(function (page, i, arr) {
+ return Number.isInteger(page) && page > 0 && (i === 0 || page >= arr[i - 1]) && page <= _this2.numPages;
+ });
+
+ if (isValid) {
+ prefValue = value;
+ }
+
+ break;
+
+ case 'NumCopies':
+ if (value > 0) {
+ prefValue = value;
+ }
+
+ break;
+
+ default:
+ (0, _util.assert)(typeof value === 'boolean');
+ prefValue = value;
+ }
+
+ if (prefValue !== undefined) {
+ prefs[key] = prefValue;
+ } else {
+ (0, _util.info)("Bad value in ViewerPreferences for \"".concat(key, "\"."));
+ }
+ }
+ }
+
+ return (0, _util.shadow)(this, 'viewerPreferences', prefs);
+ }
+ }, {
+ key: "openActionDestination",
+ get: function get() {
+ var obj = this.catDict.get('OpenAction');
+ var openActionDest = null;
+
+ if ((0, _primitives.isDict)(obj)) {
+ var destDict = new _primitives.Dict(this.xref);
+ destDict.set('A', obj);
+ var resultObj = {
+ url: null,
+ dest: null
+ };
+ Catalog.parseDestDictionary({
+ destDict: destDict,
+ resultObj: resultObj
+ });
+
+ if (Array.isArray(resultObj.dest)) {
+ openActionDest = resultObj.dest;
+ }
+ } else if (Array.isArray(obj)) {
+ openActionDest = obj;
+ }
+
+ return (0, _util.shadow)(this, 'openActionDestination', openActionDest);
+ }
+ }, {
+ key: "attachments",
+ get: function get() {
+ var obj = this.catDict.get('Names');
+ var attachments = null;
+
+ if (obj && obj.has('EmbeddedFiles')) {
+ var nameTree = new NameTree(obj.getRaw('EmbeddedFiles'), this.xref);
+ var names = nameTree.getAll();
+
+ for (var name in names) {
+ var fs = new FileSpec(names[name], this.xref);
+
+ if (!attachments) {
+ attachments = Object.create(null);
+ }
+
+ attachments[(0, _util.stringToPDFString)(name)] = fs.serializable;
+ }
+ }
+
+ return (0, _util.shadow)(this, 'attachments', attachments);
+ }
+ }, {
+ key: "javaScript",
+ get: function get() {
+ var obj = this.catDict.get('Names');
+ var javaScript = null;
+
+ function appendIfJavaScriptDict(jsDict) {
+ var type = jsDict.get('S');
+
+ if (!(0, _primitives.isName)(type, 'JavaScript')) {
+ return;
+ }
+
+ var js = jsDict.get('JS');
+
+ if ((0, _primitives.isStream)(js)) {
+ js = (0, _util.bytesToString)(js.getBytes());
+ } else if (!(0, _util.isString)(js)) {
+ return;
+ }
+
+ if (!javaScript) {
+ javaScript = [];
+ }
+
+ javaScript.push((0, _util.stringToPDFString)(js));
+ }
+
+ if (obj && obj.has('JavaScript')) {
+ var nameTree = new NameTree(obj.getRaw('JavaScript'), this.xref);
+ var names = nameTree.getAll();
+
+ for (var name in names) {
+ var jsDict = names[name];
+
+ if ((0, _primitives.isDict)(jsDict)) {
+ appendIfJavaScriptDict(jsDict);
+ }
+ }
+ }
+
+ var openActionDict = this.catDict.get('OpenAction');
+
+ if ((0, _primitives.isDict)(openActionDict, 'Action')) {
+ var actionType = openActionDict.get('S');
+
+ if ((0, _primitives.isName)(actionType, 'Named')) {
+ var action = openActionDict.get('N');
+
+ if ((0, _primitives.isName)(action, 'Print')) {
+ if (!javaScript) {
+ javaScript = [];
+ }
+
+ javaScript.push('print({});');
+ }
+ } else {
+ appendIfJavaScriptDict(openActionDict);
+ }
+ }
+
+ return (0, _util.shadow)(this, 'javaScript', javaScript);
+ }
+ }], [{
+ key: "parseDestDictionary",
+ value: function parseDestDictionary(params) {
+ function addDefaultProtocolToUrl(url) {
+ return url.startsWith('www.') ? "http://".concat(url) : url;
+ }
+
+ function tryConvertUrlEncoding(url) {
+ try {
+ return (0, _util.stringToUTF8String)(url);
+ } catch (e) {
+ return url;
+ }
+ }
+
+ var destDict = params.destDict;
+
+ if (!(0, _primitives.isDict)(destDict)) {
+ (0, _util.warn)('parseDestDictionary: `destDict` must be a dictionary.');
+ return;
+ }
+
+ var resultObj = params.resultObj;
+
+ if (_typeof(resultObj) !== 'object') {
+ (0, _util.warn)('parseDestDictionary: `resultObj` must be an object.');
+ return;
+ }
+
+ var docBaseUrl = params.docBaseUrl || null;
+ var action = destDict.get('A'),
+ url,
+ dest;
+
+ if (!(0, _primitives.isDict)(action) && destDict.has('Dest')) {
+ action = destDict.get('Dest');
+ }
+
+ if ((0, _primitives.isDict)(action)) {
+ var actionType = action.get('S');
+
+ if (!(0, _primitives.isName)(actionType)) {
+ (0, _util.warn)('parseDestDictionary: Invalid type in Action dictionary.');
+ return;
+ }
+
+ var actionName = actionType.name;
+
+ switch (actionName) {
+ case 'URI':
+ url = action.get('URI');
+
+ if ((0, _primitives.isName)(url)) {
+ url = '/' + url.name;
+ } else if ((0, _util.isString)(url)) {
+ url = addDefaultProtocolToUrl(url);
+ }
+
+ break;
+
+ case 'GoTo':
+ dest = action.get('D');
+ break;
+
+ case 'Launch':
+ case 'GoToR':
+ var urlDict = action.get('F');
+
+ if ((0, _primitives.isDict)(urlDict)) {
+ url = urlDict.get('F') || null;
+ } else if ((0, _util.isString)(urlDict)) {
+ url = urlDict;
+ }
+
+ var remoteDest = action.get('D');
+
+ if (remoteDest) {
+ if ((0, _primitives.isName)(remoteDest)) {
+ remoteDest = remoteDest.name;
+ }
+
+ if ((0, _util.isString)(url)) {
+ var baseUrl = url.split('#')[0];
+
+ if ((0, _util.isString)(remoteDest)) {
+ url = baseUrl + '#' + remoteDest;
+ } else if (Array.isArray(remoteDest)) {
+ url = baseUrl + '#' + JSON.stringify(remoteDest);
+ }
+ }
+ }
+
+ var newWindow = action.get('NewWindow');
+
+ if ((0, _util.isBool)(newWindow)) {
+ resultObj.newWindow = newWindow;
+ }
+
+ break;
+
+ case 'Named':
+ var namedAction = action.get('N');
+
+ if ((0, _primitives.isName)(namedAction)) {
+ resultObj.action = namedAction.name;
+ }
+
+ break;
+
+ case 'JavaScript':
+ var jsAction = action.get('JS');
+ var js;
+
+ if ((0, _primitives.isStream)(jsAction)) {
+ js = (0, _util.bytesToString)(jsAction.getBytes());
+ } else if ((0, _util.isString)(jsAction)) {
+ js = jsAction;
+ }
+
+ if (js) {
+ var URL_OPEN_METHODS = ['app.launchURL', 'window.open'];
+ var regex = new RegExp('^\\s*(' + URL_OPEN_METHODS.join('|').split('.').join('\\.') + ')\\((?:\'|\")([^\'\"]*)(?:\'|\")(?:,\\s*(\\w+)\\)|\\))', 'i');
+ var jsUrl = regex.exec((0, _util.stringToPDFString)(js));
+
+ if (jsUrl && jsUrl[2]) {
+ url = jsUrl[2];
+
+ if (jsUrl[3] === 'true' && jsUrl[1] === 'app.launchURL') {
+ resultObj.newWindow = true;
+ }
+
+ break;
+ }
+ }
+
+ default:
+ (0, _util.warn)("parseDestDictionary: unsupported action type \"".concat(actionName, "\"."));
+ break;
+ }
+ } else if (destDict.has('Dest')) {
+ dest = destDict.get('Dest');
+ }
+
+ if ((0, _util.isString)(url)) {
+ url = tryConvertUrlEncoding(url);
+ var absoluteUrl = (0, _util.createValidAbsoluteUrl)(url, docBaseUrl);
+
+ if (absoluteUrl) {
+ resultObj.url = absoluteUrl.href;
+ }
+
+ resultObj.unsafeUrl = url;
+ }
+
+ if (dest) {
+ if ((0, _primitives.isName)(dest)) {
+ dest = dest.name;
+ }
+
+ if ((0, _util.isString)(dest) || Array.isArray(dest)) {
+ resultObj.dest = dest;
+ }
+ }
+ }
+ }]);
+
+ return Catalog;
+}();
+
+exports.Catalog = Catalog;
+
+var XRef = function XRefClosure() {
+ function XRef(stream, pdfManager) {
+ this.stream = stream;
+ this.pdfManager = pdfManager;
+ this.entries = [];
+ this.xrefstms = Object.create(null);
+ this.cache = [];
+ this.stats = {
+ streamTypes: [],
+ fontTypes: []
+ };
+ }
+
+ XRef.prototype = {
+ setStartXRef: function XRef_setStartXRef(startXRef) {
+ this.startXRefQueue = [startXRef];
+ },
+ parse: function XRef_parse(recoveryMode) {
+ var trailerDict;
+
+ if (!recoveryMode) {
+ trailerDict = this.readXRef();
+ } else {
+ (0, _util.warn)('Indexing all PDF objects');
+ trailerDict = this.indexObjects();
+ }
+
+ trailerDict.assignXref(this);
+ this.trailer = trailerDict;
+ var encrypt;
+
+ try {
+ encrypt = trailerDict.get('Encrypt');
+ } catch (ex) {
+ if (ex instanceof _core_utils.MissingDataException) {
+ throw ex;
+ }
+
+ (0, _util.warn)("XRef.parse - Invalid \"Encrypt\" reference: \"".concat(ex, "\"."));
+ }
+
+ if ((0, _primitives.isDict)(encrypt)) {
+ var ids = trailerDict.get('ID');
+ var fileId = ids && ids.length ? ids[0] : '';
+ encrypt.suppressEncryption = true;
+ this.encrypt = new _crypto.CipherTransformFactory(encrypt, fileId, this.pdfManager.password);
+ }
+
+ var root;
+
+ try {
+ root = trailerDict.get('Root');
+ } catch (ex) {
+ if (ex instanceof _core_utils.MissingDataException) {
+ throw ex;
+ }
+
+ (0, _util.warn)("XRef.parse - Invalid \"Root\" reference: \"".concat(ex, "\"."));
+ }
+
+ if ((0, _primitives.isDict)(root) && root.has('Pages')) {
+ this.root = root;
+ } else {
+ if (!recoveryMode) {
+ throw new _core_utils.XRefParseException();
+ }
+
+ throw new _util.FormatError('Invalid root reference');
+ }
+ },
+ processXRefTable: function XRef_processXRefTable(parser) {
+ if (!('tableState' in this)) {
+ this.tableState = {
+ entryNum: 0,
+ streamPos: parser.lexer.stream.pos,
+ parserBuf1: parser.buf1,
+ parserBuf2: parser.buf2
+ };
+ }
+
+ var obj = this.readXRefTable(parser);
+
+ if (!(0, _primitives.isCmd)(obj, 'trailer')) {
+ throw new _util.FormatError('Invalid XRef table: could not find trailer dictionary');
+ }
+
+ var dict = parser.getObj();
+
+ if (!(0, _primitives.isDict)(dict) && dict.dict) {
+ dict = dict.dict;
+ }
+
+ if (!(0, _primitives.isDict)(dict)) {
+ throw new _util.FormatError('Invalid XRef table: could not parse trailer dictionary');
+ }
+
+ delete this.tableState;
+ return dict;
+ },
+ readXRefTable: function XRef_readXRefTable(parser) {
+ var stream = parser.lexer.stream;
+ var tableState = this.tableState;
+ stream.pos = tableState.streamPos;
+ parser.buf1 = tableState.parserBuf1;
+ parser.buf2 = tableState.parserBuf2;
+ var obj;
+
+ while (true) {
+ if (!('firstEntryNum' in tableState) || !('entryCount' in tableState)) {
+ if ((0, _primitives.isCmd)(obj = parser.getObj(), 'trailer')) {
+ break;
+ }
+
+ tableState.firstEntryNum = obj;
+ tableState.entryCount = parser.getObj();
+ }
+
+ var first = tableState.firstEntryNum;
+ var count = tableState.entryCount;
+
+ if (!Number.isInteger(first) || !Number.isInteger(count)) {
+ throw new _util.FormatError('Invalid XRef table: wrong types in subsection header');
+ }
+
+ for (var i = tableState.entryNum; i < count; i++) {
+ tableState.streamPos = stream.pos;
+ tableState.entryNum = i;
+ tableState.parserBuf1 = parser.buf1;
+ tableState.parserBuf2 = parser.buf2;
+ var entry = {};
+ entry.offset = parser.getObj();
+ entry.gen = parser.getObj();
+ var type = parser.getObj();
+
+ if (type instanceof _primitives.Cmd) {
+ switch (type.cmd) {
+ case 'f':
+ entry.free = true;
+ break;
+
+ case 'n':
+ entry.uncompressed = true;
+ break;
+ }
+ }
+
+ if (!Number.isInteger(entry.offset) || !Number.isInteger(entry.gen) || !(entry.free || entry.uncompressed)) {
+ throw new _util.FormatError("Invalid entry in XRef subsection: ".concat(first, ", ").concat(count));
+ }
+
+ if (i === 0 && entry.free && first === 1) {
+ first = 0;
+ }
+
+ if (!this.entries[i + first]) {
+ this.entries[i + first] = entry;
+ }
+ }
+
+ tableState.entryNum = 0;
+ tableState.streamPos = stream.pos;
+ tableState.parserBuf1 = parser.buf1;
+ tableState.parserBuf2 = parser.buf2;
+ delete tableState.firstEntryNum;
+ delete tableState.entryCount;
+ }
+
+ if (this.entries[0] && !this.entries[0].free) {
+ throw new _util.FormatError('Invalid XRef table: unexpected first object');
+ }
+
+ return obj;
+ },
+ processXRefStream: function XRef_processXRefStream(stream) {
+ if (!('streamState' in this)) {
+ var streamParameters = stream.dict;
+ var byteWidths = streamParameters.get('W');
+ var range = streamParameters.get('Index');
+
+ if (!range) {
+ range = [0, streamParameters.get('Size')];
+ }
+
+ this.streamState = {
+ entryRanges: range,
+ byteWidths: byteWidths,
+ entryNum: 0,
+ streamPos: stream.pos
+ };
+ }
+
+ this.readXRefStream(stream);
+ delete this.streamState;
+ return stream.dict;
+ },
+ readXRefStream: function XRef_readXRefStream(stream) {
+ var i, j;
+ var streamState = this.streamState;
+ stream.pos = streamState.streamPos;
+ var byteWidths = streamState.byteWidths;
+ var typeFieldWidth = byteWidths[0];
+ var offsetFieldWidth = byteWidths[1];
+ var generationFieldWidth = byteWidths[2];
+ var entryRanges = streamState.entryRanges;
+
+ while (entryRanges.length > 0) {
+ var first = entryRanges[0];
+ var n = entryRanges[1];
+
+ if (!Number.isInteger(first) || !Number.isInteger(n)) {
+ throw new _util.FormatError("Invalid XRef range fields: ".concat(first, ", ").concat(n));
+ }
+
+ if (!Number.isInteger(typeFieldWidth) || !Number.isInteger(offsetFieldWidth) || !Number.isInteger(generationFieldWidth)) {
+ throw new _util.FormatError("Invalid XRef entry fields length: ".concat(first, ", ").concat(n));
+ }
+
+ for (i = streamState.entryNum; i < n; ++i) {
+ streamState.entryNum = i;
+ streamState.streamPos = stream.pos;
+ var type = 0,
+ offset = 0,
+ generation = 0;
+
+ for (j = 0; j < typeFieldWidth; ++j) {
+ type = type << 8 | stream.getByte();
+ }
+
+ if (typeFieldWidth === 0) {
+ type = 1;
+ }
+
+ for (j = 0; j < offsetFieldWidth; ++j) {
+ offset = offset << 8 | stream.getByte();
+ }
+
+ for (j = 0; j < generationFieldWidth; ++j) {
+ generation = generation << 8 | stream.getByte();
+ }
+
+ var entry = {};
+ entry.offset = offset;
+ entry.gen = generation;
+
+ switch (type) {
+ case 0:
+ entry.free = true;
+ break;
+
+ case 1:
+ entry.uncompressed = true;
+ break;
+
+ case 2:
+ break;
+
+ default:
+ throw new _util.FormatError("Invalid XRef entry type: ".concat(type));
+ }
+
+ if (!this.entries[first + i]) {
+ this.entries[first + i] = entry;
+ }
+ }
+
+ streamState.entryNum = 0;
+ streamState.streamPos = stream.pos;
+ entryRanges.splice(0, 2);
+ }
+ },
+ indexObjects: function XRef_indexObjects() {
+ var TAB = 0x9,
+ LF = 0xA,
+ CR = 0xD,
+ SPACE = 0x20;
+ var PERCENT = 0x25,
+ LT = 0x3C;
+
+ function readToken(data, offset) {
+ var token = '',
+ ch = data[offset];
+
+ while (ch !== LF && ch !== CR && ch !== LT) {
+ if (++offset >= data.length) {
+ break;
+ }
+
+ token += String.fromCharCode(ch);
+ ch = data[offset];
+ }
+
+ return token;
+ }
+
+ function skipUntil(data, offset, what) {
+ var length = what.length,
+ dataLength = data.length;
+ var skipped = 0;
+
+ while (offset < dataLength) {
+ var i = 0;
+
+ while (i < length && data[offset + i] === what[i]) {
+ ++i;
+ }
+
+ if (i >= length) {
+ break;
+ }
+
+ offset++;
+ skipped++;
+ }
+
+ return skipped;
+ }
+
+ var objRegExp = /^(\d+)\s+(\d+)\s+obj\b/;
+ var endobjRegExp = /\bendobj[\b\s]$/;
+ var nestedObjRegExp = /\s+(\d+\s+\d+\s+obj[\b\s<])$/;
+ var CHECK_CONTENT_LENGTH = 25;
+ var trailerBytes = new Uint8Array([116, 114, 97, 105, 108, 101, 114]);
+ var startxrefBytes = new Uint8Array([115, 116, 97, 114, 116, 120, 114, 101, 102]);
+ var objBytes = new Uint8Array([111, 98, 106]);
+ var xrefBytes = new Uint8Array([47, 88, 82, 101, 102]);
+ this.entries.length = 0;
+ var stream = this.stream;
+ stream.pos = 0;
+ var buffer = stream.getBytes();
+ var position = stream.start,
+ length = buffer.length;
+ var trailers = [],
+ xrefStms = [];
+
+ while (position < length) {
+ var ch = buffer[position];
+
+ if (ch === TAB || ch === LF || ch === CR || ch === SPACE) {
+ ++position;
+ continue;
+ }
+
+ if (ch === PERCENT) {
+ do {
+ ++position;
+
+ if (position >= length) {
+ break;
+ }
+
+ ch = buffer[position];
+ } while (ch !== LF && ch !== CR);
+
+ continue;
+ }
+
+ var token = readToken(buffer, position);
+ var m;
+
+ if (token.startsWith('xref') && (token.length === 4 || /\s/.test(token[4]))) {
+ position += skipUntil(buffer, position, trailerBytes);
+ trailers.push(position);
+ position += skipUntil(buffer, position, startxrefBytes);
+ } else if (m = objRegExp.exec(token)) {
+ var num = m[1] | 0,
+ gen = m[2] | 0;
+
+ if (typeof this.entries[num] === 'undefined') {
+ this.entries[num] = {
+ offset: position - stream.start,
+ gen: gen,
+ uncompressed: true
+ };
+ }
+
+ var contentLength = void 0,
+ startPos = position + token.length;
+
+ while (startPos < buffer.length) {
+ var endPos = startPos + skipUntil(buffer, startPos, objBytes) + 4;
+ contentLength = endPos - position;
+ var checkPos = Math.max(endPos - CHECK_CONTENT_LENGTH, startPos);
+ var tokenStr = (0, _util.bytesToString)(buffer.subarray(checkPos, endPos));
+
+ if (endobjRegExp.test(tokenStr)) {
+ break;
+ } else {
+ var objToken = nestedObjRegExp.exec(tokenStr);
+
+ if (objToken && objToken[1]) {
+ (0, _util.warn)('indexObjects: Found new "obj" inside of another "obj", ' + 'caused by missing "endobj" -- trying to recover.');
+ contentLength -= objToken[1].length;
+ break;
+ }
+ }
+
+ startPos = endPos;
+ }
+
+ var content = buffer.subarray(position, position + contentLength);
+ var xrefTagOffset = skipUntil(content, 0, xrefBytes);
+
+ if (xrefTagOffset < contentLength && content[xrefTagOffset + 5] < 64) {
+ xrefStms.push(position - stream.start);
+ this.xrefstms[position - stream.start] = 1;
+ }
+
+ position += contentLength;
+ } else if (token.startsWith('trailer') && (token.length === 7 || /\s/.test(token[7]))) {
+ trailers.push(position);
+ position += skipUntil(buffer, position, startxrefBytes);
+ } else {
+ position += token.length + 1;
+ }
+ }
+
+ var i, ii;
+
+ for (i = 0, ii = xrefStms.length; i < ii; ++i) {
+ this.startXRefQueue.push(xrefStms[i]);
+ this.readXRef(true);
+ }
+
+ var trailerDict;
+
+ for (i = 0, ii = trailers.length; i < ii; ++i) {
+ stream.pos = trailers[i];
+ var parser = new _parser.Parser({
+ lexer: new _parser.Lexer(stream),
+ xref: this,
+ allowStreams: true,
+ recoveryMode: true
+ });
+ var obj = parser.getObj();
+
+ if (!(0, _primitives.isCmd)(obj, 'trailer')) {
+ continue;
+ }
+
+ var dict = parser.getObj();
+
+ if (!(0, _primitives.isDict)(dict)) {
+ continue;
+ }
+
+ var rootDict = void 0;
+
+ try {
+ rootDict = dict.get('Root');
+ } catch (ex) {
+ if (ex instanceof _core_utils.MissingDataException) {
+ throw ex;
+ }
+
+ continue;
+ }
+
+ if (!(0, _primitives.isDict)(rootDict) || !rootDict.has('Pages')) {
+ continue;
+ }
+
+ if (dict.has('ID')) {
+ return dict;
+ }
+
+ trailerDict = dict;
+ }
+
+ if (trailerDict) {
+ return trailerDict;
+ }
+
+ throw new _util.InvalidPDFException('Invalid PDF structure');
+ },
+ readXRef: function XRef_readXRef(recoveryMode) {
+ var stream = this.stream;
+ var startXRefParsedCache = Object.create(null);
+
+ try {
+ while (this.startXRefQueue.length) {
+ var startXRef = this.startXRefQueue[0];
+
+ if (startXRefParsedCache[startXRef]) {
+ (0, _util.warn)('readXRef - skipping XRef table since it was already parsed.');
+ this.startXRefQueue.shift();
+ continue;
+ }
+
+ startXRefParsedCache[startXRef] = true;
+ stream.pos = startXRef + stream.start;
+ var parser = new _parser.Parser({
+ lexer: new _parser.Lexer(stream),
+ xref: this,
+ allowStreams: true
+ });
+ var obj = parser.getObj();
+ var dict;
+
+ if ((0, _primitives.isCmd)(obj, 'xref')) {
+ dict = this.processXRefTable(parser);
+
+ if (!this.topDict) {
+ this.topDict = dict;
+ }
+
+ obj = dict.get('XRefStm');
+
+ if (Number.isInteger(obj)) {
+ var pos = obj;
+
+ if (!(pos in this.xrefstms)) {
+ this.xrefstms[pos] = 1;
+ this.startXRefQueue.push(pos);
+ }
+ }
+ } else if (Number.isInteger(obj)) {
+ if (!Number.isInteger(parser.getObj()) || !(0, _primitives.isCmd)(parser.getObj(), 'obj') || !(0, _primitives.isStream)(obj = parser.getObj())) {
+ throw new _util.FormatError('Invalid XRef stream');
+ }
+
+ dict = this.processXRefStream(obj);
+
+ if (!this.topDict) {
+ this.topDict = dict;
+ }
+
+ if (!dict) {
+ throw new _util.FormatError('Failed to read XRef stream');
+ }
+ } else {
+ throw new _util.FormatError('Invalid XRef stream header');
+ }
+
+ obj = dict.get('Prev');
+
+ if (Number.isInteger(obj)) {
+ this.startXRefQueue.push(obj);
+ } else if ((0, _primitives.isRef)(obj)) {
+ this.startXRefQueue.push(obj.num);
+ }
+
+ this.startXRefQueue.shift();
+ }
+
+ return this.topDict;
+ } catch (e) {
+ if (e instanceof _core_utils.MissingDataException) {
+ throw e;
+ }
+
+ (0, _util.info)('(while reading XRef): ' + e);
+ }
+
+ if (recoveryMode) {
+ return undefined;
+ }
+
+ throw new _core_utils.XRefParseException();
+ },
+ getEntry: function XRef_getEntry(i) {
+ var xrefEntry = this.entries[i];
+
+ if (xrefEntry && !xrefEntry.free && xrefEntry.offset) {
+ return xrefEntry;
+ }
+
+ return null;
+ },
+ fetchIfRef: function XRef_fetchIfRef(obj, suppressEncryption) {
+ if (!(0, _primitives.isRef)(obj)) {
+ return obj;
+ }
+
+ return this.fetch(obj, suppressEncryption);
+ },
+ fetch: function XRef_fetch(ref, suppressEncryption) {
+ if (!(0, _primitives.isRef)(ref)) {
+ throw new Error('ref object is not a reference');
+ }
+
+ var num = ref.num;
+
+ if (num in this.cache) {
+ var cacheEntry = this.cache[num];
+
+ if (cacheEntry instanceof _primitives.Dict && !cacheEntry.objId) {
+ cacheEntry.objId = ref.toString();
+ }
+
+ return cacheEntry;
+ }
+
+ var xrefEntry = this.getEntry(num);
+
+ if (xrefEntry === null) {
+ return this.cache[num] = null;
+ }
+
+ if (xrefEntry.uncompressed) {
+ xrefEntry = this.fetchUncompressed(ref, xrefEntry, suppressEncryption);
+ } else {
+ xrefEntry = this.fetchCompressed(ref, xrefEntry, suppressEncryption);
+ }
+
+ if ((0, _primitives.isDict)(xrefEntry)) {
+ xrefEntry.objId = ref.toString();
+ } else if ((0, _primitives.isStream)(xrefEntry)) {
+ xrefEntry.dict.objId = ref.toString();
+ }
+
+ return xrefEntry;
+ },
+ fetchUncompressed: function fetchUncompressed(ref, xrefEntry) {
+ var suppressEncryption = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+ var gen = ref.gen;
+ var num = ref.num;
+
+ if (xrefEntry.gen !== gen) {
+ throw new _core_utils.XRefEntryException("Inconsistent generation in XRef: ".concat(ref));
+ }
+
+ var stream = this.stream.makeSubStream(xrefEntry.offset + this.stream.start);
+ var parser = new _parser.Parser({
+ lexer: new _parser.Lexer(stream),
+ xref: this,
+ allowStreams: true
+ });
+ var obj1 = parser.getObj();
+ var obj2 = parser.getObj();
+ var obj3 = parser.getObj();
+
+ if (!Number.isInteger(obj1)) {
+ obj1 = parseInt(obj1, 10);
+ }
+
+ if (!Number.isInteger(obj2)) {
+ obj2 = parseInt(obj2, 10);
+ }
+
+ if (obj1 !== num || obj2 !== gen || !(obj3 instanceof _primitives.Cmd)) {
+ throw new _core_utils.XRefEntryException("Bad (uncompressed) XRef entry: ".concat(ref));
+ }
+
+ if (obj3.cmd !== 'obj') {
+ if (obj3.cmd.startsWith('obj')) {
+ num = parseInt(obj3.cmd.substring(3), 10);
+
+ if (!Number.isNaN(num)) {
+ return num;
+ }
+ }
+
+ throw new _core_utils.XRefEntryException("Bad (uncompressed) XRef entry: ".concat(ref));
+ }
+
+ if (this.encrypt && !suppressEncryption) {
+ xrefEntry = parser.getObj(this.encrypt.createCipherTransform(num, gen));
+ } else {
+ xrefEntry = parser.getObj();
+ }
+
+ if (!(0, _primitives.isStream)(xrefEntry)) {
+ this.cache[num] = xrefEntry;
+ }
+
+ return xrefEntry;
+ },
+ fetchCompressed: function fetchCompressed(ref, xrefEntry) {
+ var suppressEncryption = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+ var tableOffset = xrefEntry.offset;
+ var stream = this.fetch(_primitives.Ref.get(tableOffset, 0));
+
+ if (!(0, _primitives.isStream)(stream)) {
+ throw new _util.FormatError('bad ObjStm stream');
+ }
+
+ var first = stream.dict.get('First');
+ var n = stream.dict.get('N');
+
+ if (!Number.isInteger(first) || !Number.isInteger(n)) {
+ throw new _util.FormatError('invalid first and n parameters for ObjStm stream');
+ }
+
+ var parser = new _parser.Parser({
+ lexer: new _parser.Lexer(stream),
+ xref: this,
+ allowStreams: true
+ });
+ var i,
+ entries = [],
+ num,
+ nums = [];
+
+ for (i = 0; i < n; ++i) {
+ num = parser.getObj();
+
+ if (!Number.isInteger(num)) {
+ throw new _util.FormatError("invalid object number in the ObjStm stream: ".concat(num));
+ }
+
+ nums.push(num);
+ var offset = parser.getObj();
+
+ if (!Number.isInteger(offset)) {
+ throw new _util.FormatError("invalid object offset in the ObjStm stream: ".concat(offset));
+ }
+ }
+
+ for (i = 0; i < n; ++i) {
+ entries.push(parser.getObj());
+
+ if ((0, _primitives.isCmd)(parser.buf1, 'endobj')) {
+ parser.shift();
+ }
+
+ num = nums[i];
+ var entry = this.entries[num];
+
+ if (entry && entry.offset === tableOffset && entry.gen === i) {
+ this.cache[num] = entries[i];
+ }
+ }
+
+ xrefEntry = entries[xrefEntry.gen];
+
+ if (xrefEntry === undefined) {
+ throw new _core_utils.XRefEntryException("Bad (compressed) XRef entry: ".concat(ref));
+ }
+
+ return xrefEntry;
+ },
+ fetchIfRefAsync: function () {
+ var _fetchIfRefAsync = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee(obj, suppressEncryption) {
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ if ((0, _primitives.isRef)(obj)) {
+ _context.next = 2;
+ break;
+ }
+
+ return _context.abrupt("return", obj);
+
+ case 2:
+ return _context.abrupt("return", this.fetchAsync(obj, suppressEncryption));
+
+ case 3:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee, this);
+ }));
+
+ function fetchIfRefAsync(_x, _x2) {
+ return _fetchIfRefAsync.apply(this, arguments);
+ }
+
+ return fetchIfRefAsync;
+ }(),
+ fetchAsync: function () {
+ var _fetchAsync = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee2(ref, suppressEncryption) {
+ return _regenerator["default"].wrap(function _callee2$(_context2) {
+ while (1) {
+ switch (_context2.prev = _context2.next) {
+ case 0:
+ _context2.prev = 0;
+ return _context2.abrupt("return", this.fetch(ref, suppressEncryption));
+
+ case 4:
+ _context2.prev = 4;
+ _context2.t0 = _context2["catch"](0);
+
+ if (_context2.t0 instanceof _core_utils.MissingDataException) {
+ _context2.next = 8;
+ break;
+ }
+
+ throw _context2.t0;
+
+ case 8:
+ _context2.next = 10;
+ return this.pdfManager.requestRange(_context2.t0.begin, _context2.t0.end);
+
+ case 10:
+ return _context2.abrupt("return", this.fetchAsync(ref, suppressEncryption));
+
+ case 11:
+ case "end":
+ return _context2.stop();
+ }
+ }
+ }, _callee2, this, [[0, 4]]);
+ }));
+
+ function fetchAsync(_x3, _x4) {
+ return _fetchAsync.apply(this, arguments);
+ }
+
+ return fetchAsync;
+ }(),
+ getCatalogObj: function XRef_getCatalogObj() {
+ return this.root;
+ }
+ };
+ return XRef;
+}();
+
+exports.XRef = XRef;
+
+var NameOrNumberTree =
+/*#__PURE__*/
+function () {
+ function NameOrNumberTree(root, xref, type) {
+ _classCallCheck(this, NameOrNumberTree);
+
+ if (this.constructor === NameOrNumberTree) {
+ (0, _util.unreachable)('Cannot initialize NameOrNumberTree.');
+ }
+
+ this.root = root;
+ this.xref = xref;
+ this._type = type;
+ }
+
+ _createClass(NameOrNumberTree, [{
+ key: "getAll",
+ value: function getAll() {
+ var dict = Object.create(null);
+
+ if (!this.root) {
+ return dict;
+ }
+
+ var xref = this.xref;
+ var processed = new _primitives.RefSet();
+ processed.put(this.root);
+ var queue = [this.root];
+
+ while (queue.length > 0) {
+ var obj = xref.fetchIfRef(queue.shift());
+
+ if (!(0, _primitives.isDict)(obj)) {
+ continue;
+ }
+
+ if (obj.has('Kids')) {
+ var kids = obj.get('Kids');
+
+ for (var i = 0, ii = kids.length; i < ii; i++) {
+ var kid = kids[i];
+
+ if (processed.has(kid)) {
+ throw new _util.FormatError("Duplicate entry in \"".concat(this._type, "\" tree."));
+ }
+
+ queue.push(kid);
+ processed.put(kid);
+ }
+
+ continue;
+ }
+
+ var entries = obj.get(this._type);
+
+ if (Array.isArray(entries)) {
+ for (var _i2 = 0, _ii = entries.length; _i2 < _ii; _i2 += 2) {
+ dict[xref.fetchIfRef(entries[_i2])] = xref.fetchIfRef(entries[_i2 + 1]);
+ }
+ }
+ }
+
+ return dict;
+ }
+ }, {
+ key: "get",
+ value: function get(key) {
+ if (!this.root) {
+ return null;
+ }
+
+ var xref = this.xref;
+ var kidsOrEntries = xref.fetchIfRef(this.root);
+ var loopCount = 0;
+ var MAX_LEVELS = 10;
+
+ while (kidsOrEntries.has('Kids')) {
+ if (++loopCount > MAX_LEVELS) {
+ (0, _util.warn)("Search depth limit reached for \"".concat(this._type, "\" tree."));
+ return null;
+ }
+
+ var kids = kidsOrEntries.get('Kids');
+
+ if (!Array.isArray(kids)) {
+ return null;
+ }
+
+ var l = 0,
+ r = kids.length - 1;
+
+ while (l <= r) {
+ var m = l + r >> 1;
+ var kid = xref.fetchIfRef(kids[m]);
+ var limits = kid.get('Limits');
+
+ if (key < xref.fetchIfRef(limits[0])) {
+ r = m - 1;
+ } else if (key > xref.fetchIfRef(limits[1])) {
+ l = m + 1;
+ } else {
+ kidsOrEntries = xref.fetchIfRef(kids[m]);
+ break;
+ }
+ }
+
+ if (l > r) {
+ return null;
+ }
+ }
+
+ var entries = kidsOrEntries.get(this._type);
+
+ if (Array.isArray(entries)) {
+ var _l = 0,
+ _r = entries.length - 2;
+
+ while (_l <= _r) {
+ var tmp = _l + _r >> 1,
+ _m = tmp + (tmp & 1);
+
+ var currentKey = xref.fetchIfRef(entries[_m]);
+
+ if (key < currentKey) {
+ _r = _m - 2;
+ } else if (key > currentKey) {
+ _l = _m + 2;
+ } else {
+ return xref.fetchIfRef(entries[_m + 1]);
+ }
+ }
+
+ (0, _util.info)("Falling back to an exhaustive search, for key \"".concat(key, "\", ") + "in \"".concat(this._type, "\" tree."));
+
+ for (var _m2 = 0, mm = entries.length; _m2 < mm; _m2 += 2) {
+ var _currentKey = xref.fetchIfRef(entries[_m2]);
+
+ if (_currentKey === key) {
+ (0, _util.warn)("The \"".concat(key, "\" key was found at an incorrect, ") + "i.e. out-of-order, position in \"".concat(this._type, "\" tree."));
+ return xref.fetchIfRef(entries[_m2 + 1]);
+ }
+ }
+ }
+
+ return null;
+ }
+ }]);
+
+ return NameOrNumberTree;
+}();
+
+var NameTree =
+/*#__PURE__*/
+function (_NameOrNumberTree) {
+ _inherits(NameTree, _NameOrNumberTree);
+
+ function NameTree(root, xref) {
+ _classCallCheck(this, NameTree);
+
+ return _possibleConstructorReturn(this, _getPrototypeOf(NameTree).call(this, root, xref, 'Names'));
+ }
+
+ return NameTree;
+}(NameOrNumberTree);
+
+var NumberTree =
+/*#__PURE__*/
+function (_NameOrNumberTree2) {
+ _inherits(NumberTree, _NameOrNumberTree2);
+
+ function NumberTree(root, xref) {
+ _classCallCheck(this, NumberTree);
+
+ return _possibleConstructorReturn(this, _getPrototypeOf(NumberTree).call(this, root, xref, 'Nums'));
+ }
+
+ return NumberTree;
+}(NameOrNumberTree);
+
+var FileSpec = function FileSpecClosure() {
+ function FileSpec(root, xref) {
+ if (!root || !(0, _primitives.isDict)(root)) {
+ return;
+ }
+
+ this.xref = xref;
+ this.root = root;
+
+ if (root.has('FS')) {
+ this.fs = root.get('FS');
+ }
+
+ this.description = root.has('Desc') ? (0, _util.stringToPDFString)(root.get('Desc')) : '';
+
+ if (root.has('RF')) {
+ (0, _util.warn)('Related file specifications are not supported');
+ }
+
+ this.contentAvailable = true;
+
+ if (!root.has('EF')) {
+ this.contentAvailable = false;
+ (0, _util.warn)('Non-embedded file specifications are not supported');
+ }
+ }
+
+ function pickPlatformItem(dict) {
+ if (dict.has('UF')) {
+ return dict.get('UF');
+ } else if (dict.has('F')) {
+ return dict.get('F');
+ } else if (dict.has('Unix')) {
+ return dict.get('Unix');
+ } else if (dict.has('Mac')) {
+ return dict.get('Mac');
+ } else if (dict.has('DOS')) {
+ return dict.get('DOS');
+ }
+
+ return null;
+ }
+
+ FileSpec.prototype = {
+ get filename() {
+ if (!this._filename && this.root) {
+ var filename = pickPlatformItem(this.root) || 'unnamed';
+ this._filename = (0, _util.stringToPDFString)(filename).replace(/\\\\/g, '\\').replace(/\\\//g, '/').replace(/\\/g, '/');
+ }
+
+ return this._filename;
+ },
+
+ get content() {
+ if (!this.contentAvailable) {
+ return null;
+ }
+
+ if (!this.contentRef && this.root) {
+ this.contentRef = pickPlatformItem(this.root.get('EF'));
+ }
+
+ var content = null;
+
+ if (this.contentRef) {
+ var xref = this.xref;
+ var fileObj = xref.fetchIfRef(this.contentRef);
+
+ if (fileObj && (0, _primitives.isStream)(fileObj)) {
+ content = fileObj.getBytes();
+ } else {
+ (0, _util.warn)('Embedded file specification points to non-existing/invalid ' + 'content');
+ }
+ } else {
+ (0, _util.warn)('Embedded file specification does not have a content');
+ }
+
+ return content;
+ },
+
+ get serializable() {
+ return {
+ filename: this.filename,
+ content: this.content
+ };
+ }
+
+ };
+ return FileSpec;
+}();
+
+exports.FileSpec = FileSpec;
+
+var ObjectLoader = function () {
+ function mayHaveChildren(value) {
+ return (0, _primitives.isRef)(value) || (0, _primitives.isDict)(value) || Array.isArray(value) || (0, _primitives.isStream)(value);
+ }
+
+ function addChildren(node, nodesToVisit) {
+ if ((0, _primitives.isDict)(node) || (0, _primitives.isStream)(node)) {
+ var dict = (0, _primitives.isDict)(node) ? node : node.dict;
+ var dictKeys = dict.getKeys();
+
+ for (var i = 0, ii = dictKeys.length; i < ii; i++) {
+ var rawValue = dict.getRaw(dictKeys[i]);
+
+ if (mayHaveChildren(rawValue)) {
+ nodesToVisit.push(rawValue);
+ }
+ }
+ } else if (Array.isArray(node)) {
+ for (var _i3 = 0, _ii2 = node.length; _i3 < _ii2; _i3++) {
+ var value = node[_i3];
+
+ if (mayHaveChildren(value)) {
+ nodesToVisit.push(value);
+ }
+ }
+ }
+ }
+
+ function ObjectLoader(dict, keys, xref) {
+ this.dict = dict;
+ this.keys = keys;
+ this.xref = xref;
+ this.refSet = null;
+ this.capability = null;
+ }
+
+ ObjectLoader.prototype = {
+ load: function load() {
+ this.capability = (0, _util.createPromiseCapability)();
+
+ if (!(this.xref.stream instanceof _chunked_stream.ChunkedStream) || this.xref.stream.getMissingChunks().length === 0) {
+ this.capability.resolve();
+ return this.capability.promise;
+ }
+
+ var keys = this.keys,
+ dict = this.dict;
+ this.refSet = new _primitives.RefSet();
+ var nodesToVisit = [];
+
+ for (var i = 0, ii = keys.length; i < ii; i++) {
+ var rawValue = dict.getRaw(keys[i]);
+
+ if (rawValue !== undefined) {
+ nodesToVisit.push(rawValue);
+ }
+ }
+
+ this._walk(nodesToVisit);
+
+ return this.capability.promise;
+ },
+ _walk: function _walk(nodesToVisit) {
+ var _this3 = this;
+
+ var nodesToRevisit = [];
+ var pendingRequests = [];
+
+ while (nodesToVisit.length) {
+ var currentNode = nodesToVisit.pop();
+
+ if ((0, _primitives.isRef)(currentNode)) {
+ if (this.refSet.has(currentNode)) {
+ continue;
+ }
+
+ try {
+ this.refSet.put(currentNode);
+ currentNode = this.xref.fetch(currentNode);
+ } catch (ex) {
+ if (!(ex instanceof _core_utils.MissingDataException)) {
+ throw ex;
+ }
+
+ nodesToRevisit.push(currentNode);
+ pendingRequests.push({
+ begin: ex.begin,
+ end: ex.end
+ });
+ }
+ }
+
+ if (currentNode && currentNode.getBaseStreams) {
+ var baseStreams = currentNode.getBaseStreams();
+ var foundMissingData = false;
+
+ for (var i = 0, ii = baseStreams.length; i < ii; i++) {
+ var stream = baseStreams[i];
+
+ if (stream.getMissingChunks && stream.getMissingChunks().length) {
+ foundMissingData = true;
+ pendingRequests.push({
+ begin: stream.start,
+ end: stream.end
+ });
+ }
+ }
+
+ if (foundMissingData) {
+ nodesToRevisit.push(currentNode);
+ }
+ }
+
+ addChildren(currentNode, nodesToVisit);
+ }
+
+ if (pendingRequests.length) {
+ this.xref.stream.manager.requestRanges(pendingRequests).then(function () {
+ for (var _i4 = 0, _ii3 = nodesToRevisit.length; _i4 < _ii3; _i4++) {
+ var node = nodesToRevisit[_i4];
+
+ if ((0, _primitives.isRef)(node)) {
+ _this3.refSet.remove(node);
+ }
+ }
+
+ _this3._walk(nodesToRevisit);
+ }, this.capability.reject);
+ return;
+ }
+
+ this.refSet = null;
+ this.capability.resolve();
+ }
+ };
+ return ObjectLoader;
+}();
+
+exports.ObjectLoader = ObjectLoader;
+
+/***/ }),
+/* 157 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.Parser = exports.Linearization = exports.Lexer = void 0;
+
+var _stream = __w_pdfjs_require__(158);
+
+var _util = __w_pdfjs_require__(5);
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _ccitt_stream = __w_pdfjs_require__(159);
+
+var _jbig2_stream = __w_pdfjs_require__(161);
+
+var _jpeg_stream = __w_pdfjs_require__(164);
+
+var _jpx_stream = __w_pdfjs_require__(166);
+
+var _core_utils = __w_pdfjs_require__(154);
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var MAX_LENGTH_TO_CACHE = 1000;
+var MAX_ADLER32_LENGTH = 5552;
+
+function computeAdler32(bytes) {
+ var bytesLength = bytes.length;
+ var a = 1,
+ b = 0;
+
+ for (var i = 0; i < bytesLength; ++i) {
+ a += bytes[i] & 0xFF;
+ b += a;
+ }
+
+ return b % 65521 << 16 | a % 65521;
+}
+
+var Parser =
+/*#__PURE__*/
+function () {
+ function Parser(_ref) {
+ var lexer = _ref.lexer,
+ xref = _ref.xref,
+ _ref$allowStreams = _ref.allowStreams,
+ allowStreams = _ref$allowStreams === void 0 ? false : _ref$allowStreams,
+ _ref$recoveryMode = _ref.recoveryMode,
+ recoveryMode = _ref$recoveryMode === void 0 ? false : _ref$recoveryMode;
+
+ _classCallCheck(this, Parser);
+
+ this.lexer = lexer;
+ this.xref = xref;
+ this.allowStreams = allowStreams;
+ this.recoveryMode = recoveryMode;
+ this.imageCache = Object.create(null);
+ this.refill();
+ }
+
+ _createClass(Parser, [{
+ key: "refill",
+ value: function refill() {
+ this.buf1 = this.lexer.getObj();
+ this.buf2 = this.lexer.getObj();
+ }
+ }, {
+ key: "shift",
+ value: function shift() {
+ if ((0, _primitives.isCmd)(this.buf2, 'ID')) {
+ this.buf1 = this.buf2;
+ this.buf2 = null;
+ } else {
+ this.buf1 = this.buf2;
+ this.buf2 = this.lexer.getObj();
+ }
+ }
+ }, {
+ key: "tryShift",
+ value: function tryShift() {
+ try {
+ this.shift();
+ return true;
+ } catch (e) {
+ if (e instanceof _core_utils.MissingDataException) {
+ throw e;
+ }
+
+ return false;
+ }
+ }
+ }, {
+ key: "getObj",
+ value: function getObj(cipherTransform) {
+ var buf1 = this.buf1;
+ this.shift();
+
+ if (buf1 instanceof _primitives.Cmd) {
+ switch (buf1.cmd) {
+ case 'BI':
+ return this.makeInlineImage(cipherTransform);
+
+ case '[':
+ var array = [];
+
+ while (!(0, _primitives.isCmd)(this.buf1, ']') && !(0, _primitives.isEOF)(this.buf1)) {
+ array.push(this.getObj(cipherTransform));
+ }
+
+ if ((0, _primitives.isEOF)(this.buf1)) {
+ if (!this.recoveryMode) {
+ throw new _util.FormatError('End of file inside array');
+ }
+
+ return array;
+ }
+
+ this.shift();
+ return array;
+
+ case '<<':
+ var dict = new _primitives.Dict(this.xref);
+
+ while (!(0, _primitives.isCmd)(this.buf1, '>>') && !(0, _primitives.isEOF)(this.buf1)) {
+ if (!(0, _primitives.isName)(this.buf1)) {
+ (0, _util.info)('Malformed dictionary: key must be a name object');
+ this.shift();
+ continue;
+ }
+
+ var key = this.buf1.name;
+ this.shift();
+
+ if ((0, _primitives.isEOF)(this.buf1)) {
+ break;
+ }
+
+ dict.set(key, this.getObj(cipherTransform));
+ }
+
+ if ((0, _primitives.isEOF)(this.buf1)) {
+ if (!this.recoveryMode) {
+ throw new _util.FormatError('End of file inside dictionary');
+ }
+
+ return dict;
+ }
+
+ if ((0, _primitives.isCmd)(this.buf2, 'stream')) {
+ return this.allowStreams ? this.makeStream(dict, cipherTransform) : dict;
+ }
+
+ this.shift();
+ return dict;
+
+ default:
+ return buf1;
+ }
+ }
+
+ if (Number.isInteger(buf1)) {
+ var num = buf1;
+
+ if (Number.isInteger(this.buf1) && (0, _primitives.isCmd)(this.buf2, 'R')) {
+ var ref = _primitives.Ref.get(num, this.buf1);
+
+ this.shift();
+ this.shift();
+ return ref;
+ }
+
+ return num;
+ }
+
+ if ((0, _util.isString)(buf1)) {
+ var str = buf1;
+
+ if (cipherTransform) {
+ str = cipherTransform.decryptString(str);
+ }
+
+ return str;
+ }
+
+ return buf1;
+ }
+ }, {
+ key: "findDefaultInlineStreamEnd",
+ value: function findDefaultInlineStreamEnd(stream) {
+ var E = 0x45,
+ I = 0x49,
+ SPACE = 0x20,
+ LF = 0xA,
+ CR = 0xD;
+ var n = 10,
+ NUL = 0x0;
+ var startPos = stream.pos,
+ state = 0,
+ ch,
+ maybeEIPos;
+
+ while ((ch = stream.getByte()) !== -1) {
+ if (state === 0) {
+ state = ch === E ? 1 : 0;
+ } else if (state === 1) {
+ state = ch === I ? 2 : 0;
+ } else {
+ (0, _util.assert)(state === 2);
+
+ if (ch === SPACE || ch === LF || ch === CR) {
+ maybeEIPos = stream.pos;
+ var followingBytes = stream.peekBytes(n);
+
+ for (var i = 0, ii = followingBytes.length; i < ii; i++) {
+ ch = followingBytes[i];
+
+ if (ch === NUL && followingBytes[i + 1] !== NUL) {
+ continue;
+ }
+
+ if (ch !== LF && ch !== CR && (ch < SPACE || ch > 0x7F)) {
+ state = 0;
+ break;
+ }
+ }
+
+ if (state === 2) {
+ break;
+ }
+ } else {
+ state = 0;
+ }
+ }
+ }
+
+ if (ch === -1) {
+ (0, _util.warn)('findDefaultInlineStreamEnd: ' + 'Reached the end of the stream without finding a valid EI marker');
+
+ if (maybeEIPos) {
+ (0, _util.warn)('... trying to recover by using the last "EI" occurrence.');
+ stream.skip(-(stream.pos - maybeEIPos));
+ }
+ }
+
+ var endOffset = 4;
+ stream.skip(-endOffset);
+ ch = stream.peekByte();
+ stream.skip(endOffset);
+
+ if (!(0, _util.isSpace)(ch)) {
+ endOffset--;
+ }
+
+ return stream.pos - endOffset - startPos;
+ }
+ }, {
+ key: "findDCTDecodeInlineStreamEnd",
+ value: function findDCTDecodeInlineStreamEnd(stream) {
+ var startPos = stream.pos,
+ foundEOI = false,
+ b,
+ markerLength,
+ length;
+
+ while ((b = stream.getByte()) !== -1) {
+ if (b !== 0xFF) {
+ continue;
+ }
+
+ switch (stream.getByte()) {
+ case 0x00:
+ break;
+
+ case 0xFF:
+ stream.skip(-1);
+ break;
+
+ case 0xD9:
+ foundEOI = true;
+ break;
+
+ case 0xC0:
+ case 0xC1:
+ case 0xC2:
+ case 0xC3:
+ case 0xC5:
+ case 0xC6:
+ case 0xC7:
+ case 0xC9:
+ case 0xCA:
+ case 0xCB:
+ case 0xCD:
+ case 0xCE:
+ case 0xCF:
+ case 0xC4:
+ case 0xCC:
+ case 0xDA:
+ case 0xDB:
+ case 0xDC:
+ case 0xDD:
+ case 0xDE:
+ case 0xDF:
+ case 0xE0:
+ case 0xE1:
+ case 0xE2:
+ case 0xE3:
+ case 0xE4:
+ case 0xE5:
+ case 0xE6:
+ case 0xE7:
+ case 0xE8:
+ case 0xE9:
+ case 0xEA:
+ case 0xEB:
+ case 0xEC:
+ case 0xED:
+ case 0xEE:
+ case 0xEF:
+ case 0xFE:
+ markerLength = stream.getUint16();
+
+ if (markerLength > 2) {
+ stream.skip(markerLength - 2);
+ } else {
+ stream.skip(-2);
+ }
+
+ break;
+ }
+
+ if (foundEOI) {
+ break;
+ }
+ }
+
+ length = stream.pos - startPos;
+
+ if (b === -1) {
+ (0, _util.warn)('Inline DCTDecode image stream: ' + 'EOI marker not found, searching for /EI/ instead.');
+ stream.skip(-length);
+ return this.findDefaultInlineStreamEnd(stream);
+ }
+
+ this.inlineStreamSkipEI(stream);
+ return length;
+ }
+ }, {
+ key: "findASCII85DecodeInlineStreamEnd",
+ value: function findASCII85DecodeInlineStreamEnd(stream) {
+ var TILDE = 0x7E,
+ GT = 0x3E;
+ var startPos = stream.pos,
+ ch,
+ length;
+
+ while ((ch = stream.getByte()) !== -1) {
+ if (ch === TILDE) {
+ ch = stream.peekByte();
+
+ while ((0, _util.isSpace)(ch)) {
+ stream.skip();
+ ch = stream.peekByte();
+ }
+
+ if (ch === GT) {
+ stream.skip();
+ break;
+ }
+ }
+ }
+
+ length = stream.pos - startPos;
+
+ if (ch === -1) {
+ (0, _util.warn)('Inline ASCII85Decode image stream: ' + 'EOD marker not found, searching for /EI/ instead.');
+ stream.skip(-length);
+ return this.findDefaultInlineStreamEnd(stream);
+ }
+
+ this.inlineStreamSkipEI(stream);
+ return length;
+ }
+ }, {
+ key: "findASCIIHexDecodeInlineStreamEnd",
+ value: function findASCIIHexDecodeInlineStreamEnd(stream) {
+ var GT = 0x3E;
+ var startPos = stream.pos,
+ ch,
+ length;
+
+ while ((ch = stream.getByte()) !== -1) {
+ if (ch === GT) {
+ break;
+ }
+ }
+
+ length = stream.pos - startPos;
+
+ if (ch === -1) {
+ (0, _util.warn)('Inline ASCIIHexDecode image stream: ' + 'EOD marker not found, searching for /EI/ instead.');
+ stream.skip(-length);
+ return this.findDefaultInlineStreamEnd(stream);
+ }
+
+ this.inlineStreamSkipEI(stream);
+ return length;
+ }
+ }, {
+ key: "inlineStreamSkipEI",
+ value: function inlineStreamSkipEI(stream) {
+ var E = 0x45,
+ I = 0x49;
+ var state = 0,
+ ch;
+
+ while ((ch = stream.getByte()) !== -1) {
+ if (state === 0) {
+ state = ch === E ? 1 : 0;
+ } else if (state === 1) {
+ state = ch === I ? 2 : 0;
+ } else if (state === 2) {
+ break;
+ }
+ }
+ }
+ }, {
+ key: "makeInlineImage",
+ value: function makeInlineImage(cipherTransform) {
+ var lexer = this.lexer;
+ var stream = lexer.stream;
+ var dict = new _primitives.Dict(this.xref);
+ var dictLength;
+
+ while (!(0, _primitives.isCmd)(this.buf1, 'ID') && !(0, _primitives.isEOF)(this.buf1)) {
+ if (!(0, _primitives.isName)(this.buf1)) {
+ throw new _util.FormatError('Dictionary key must be a name object');
+ }
+
+ var key = this.buf1.name;
+ this.shift();
+
+ if ((0, _primitives.isEOF)(this.buf1)) {
+ break;
+ }
+
+ dict.set(key, this.getObj(cipherTransform));
+ }
+
+ if (lexer.beginInlineImagePos !== -1) {
+ dictLength = stream.pos - lexer.beginInlineImagePos;
+ }
+
+ var filter = dict.get('Filter', 'F');
+ var filterName;
+
+ if ((0, _primitives.isName)(filter)) {
+ filterName = filter.name;
+ } else if (Array.isArray(filter)) {
+ var filterZero = this.xref.fetchIfRef(filter[0]);
+
+ if ((0, _primitives.isName)(filterZero)) {
+ filterName = filterZero.name;
+ }
+ }
+
+ var startPos = stream.pos;
+ var length;
+
+ if (filterName === 'DCTDecode' || filterName === 'DCT') {
+ length = this.findDCTDecodeInlineStreamEnd(stream);
+ } else if (filterName === 'ASCII85Decode' || filterName === 'A85') {
+ length = this.findASCII85DecodeInlineStreamEnd(stream);
+ } else if (filterName === 'ASCIIHexDecode' || filterName === 'AHx') {
+ length = this.findASCIIHexDecodeInlineStreamEnd(stream);
+ } else {
+ length = this.findDefaultInlineStreamEnd(stream);
+ }
+
+ var imageStream = stream.makeSubStream(startPos, length, dict);
+ var cacheKey;
+
+ if (length < MAX_LENGTH_TO_CACHE && dictLength < MAX_ADLER32_LENGTH) {
+ var imageBytes = imageStream.getBytes();
+ imageStream.reset();
+ var initialStreamPos = stream.pos;
+ stream.pos = lexer.beginInlineImagePos;
+ var dictBytes = stream.getBytes(dictLength);
+ stream.pos = initialStreamPos;
+ cacheKey = computeAdler32(imageBytes) + '_' + computeAdler32(dictBytes);
+ var cacheEntry = this.imageCache[cacheKey];
+
+ if (cacheEntry !== undefined) {
+ this.buf2 = _primitives.Cmd.get('EI');
+ this.shift();
+ cacheEntry.reset();
+ return cacheEntry;
+ }
+ }
+
+ if (cipherTransform) {
+ imageStream = cipherTransform.createStream(imageStream, length);
+ }
+
+ imageStream = this.filter(imageStream, dict, length);
+ imageStream.dict = dict;
+
+ if (cacheKey !== undefined) {
+ imageStream.cacheKey = "inline_".concat(length, "_").concat(cacheKey);
+ this.imageCache[cacheKey] = imageStream;
+ }
+
+ this.buf2 = _primitives.Cmd.get('EI');
+ this.shift();
+ return imageStream;
+ }
+ }, {
+ key: "_findStreamLength",
+ value: function _findStreamLength(startPos, signature) {
+ var stream = this.lexer.stream;
+ stream.pos = startPos;
+ var SCAN_BLOCK_LENGTH = 2048;
+ var signatureLength = signature.length;
+
+ while (stream.pos < stream.end) {
+ var scanBytes = stream.peekBytes(SCAN_BLOCK_LENGTH);
+ var scanLength = scanBytes.length - signatureLength;
+
+ if (scanLength <= 0) {
+ break;
+ }
+
+ var pos = 0;
+
+ while (pos < scanLength) {
+ var j = 0;
+
+ while (j < signatureLength && scanBytes[pos + j] === signature[j]) {
+ j++;
+ }
+
+ if (j >= signatureLength) {
+ stream.pos += pos;
+ return stream.pos - startPos;
+ }
+
+ pos++;
+ }
+
+ stream.pos += scanLength;
+ }
+
+ return -1;
+ }
+ }, {
+ key: "makeStream",
+ value: function makeStream(dict, cipherTransform) {
+ var lexer = this.lexer;
+ var stream = lexer.stream;
+ lexer.skipToNextLine();
+ var startPos = stream.pos - 1;
+ var length = dict.get('Length');
+
+ if (!Number.isInteger(length)) {
+ (0, _util.info)("Bad length \"".concat(length, "\" in stream"));
+ length = 0;
+ }
+
+ stream.pos = startPos + length;
+ lexer.nextChar();
+
+ if (this.tryShift() && (0, _primitives.isCmd)(this.buf2, 'endstream')) {
+ this.shift();
+ } else {
+ var ENDSTREAM_SIGNATURE = new Uint8Array([0x65, 0x6E, 0x64, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D]);
+
+ var actualLength = this._findStreamLength(startPos, ENDSTREAM_SIGNATURE);
+
+ if (actualLength < 0) {
+ var MAX_TRUNCATION = 1;
+
+ for (var i = 1; i <= MAX_TRUNCATION; i++) {
+ var end = ENDSTREAM_SIGNATURE.length - i;
+ var TRUNCATED_SIGNATURE = ENDSTREAM_SIGNATURE.slice(0, end);
+
+ var maybeLength = this._findStreamLength(startPos, TRUNCATED_SIGNATURE);
+
+ if (maybeLength >= 0) {
+ var lastByte = stream.peekBytes(end + 1)[end];
+
+ if (!(0, _util.isSpace)(lastByte)) {
+ break;
+ }
+
+ (0, _util.info)("Found \"".concat((0, _util.bytesToString)(TRUNCATED_SIGNATURE), "\" when ") + 'searching for endstream command.');
+ actualLength = maybeLength;
+ break;
+ }
+ }
+
+ if (actualLength < 0) {
+ throw new _util.FormatError('Missing endstream command.');
+ }
+ }
+
+ length = actualLength;
+ lexer.nextChar();
+ this.shift();
+ this.shift();
+ }
+
+ this.shift();
+ stream = stream.makeSubStream(startPos, length, dict);
+
+ if (cipherTransform) {
+ stream = cipherTransform.createStream(stream, length);
+ }
+
+ stream = this.filter(stream, dict, length);
+ stream.dict = dict;
+ return stream;
+ }
+ }, {
+ key: "filter",
+ value: function filter(stream, dict, length) {
+ var filter = dict.get('Filter', 'F');
+ var params = dict.get('DecodeParms', 'DP');
+
+ if ((0, _primitives.isName)(filter)) {
+ if (Array.isArray(params)) {
+ (0, _util.warn)('/DecodeParms should not contain an Array, ' + 'when /Filter contains a Name.');
+ }
+
+ return this.makeFilter(stream, filter.name, length, params);
+ }
+
+ var maybeLength = length;
+
+ if (Array.isArray(filter)) {
+ var filterArray = filter;
+ var paramsArray = params;
+
+ for (var i = 0, ii = filterArray.length; i < ii; ++i) {
+ filter = this.xref.fetchIfRef(filterArray[i]);
+
+ if (!(0, _primitives.isName)(filter)) {
+ throw new _util.FormatError("Bad filter name \"".concat(filter, "\""));
+ }
+
+ params = null;
+
+ if (Array.isArray(paramsArray) && i in paramsArray) {
+ params = this.xref.fetchIfRef(paramsArray[i]);
+ }
+
+ stream = this.makeFilter(stream, filter.name, maybeLength, params);
+ maybeLength = null;
+ }
+ }
+
+ return stream;
+ }
+ }, {
+ key: "makeFilter",
+ value: function makeFilter(stream, name, maybeLength, params) {
+ if (maybeLength === 0) {
+ (0, _util.warn)("Empty \"".concat(name, "\" stream."));
+ return new _stream.NullStream();
+ }
+
+ try {
+ var xrefStreamStats = this.xref.stats.streamTypes;
+
+ if (name === 'FlateDecode' || name === 'Fl') {
+ xrefStreamStats[_util.StreamType.FLATE] = true;
+
+ if (params) {
+ return new _stream.PredictorStream(new _stream.FlateStream(stream, maybeLength), maybeLength, params);
+ }
+
+ return new _stream.FlateStream(stream, maybeLength);
+ }
+
+ if (name === 'LZWDecode' || name === 'LZW') {
+ xrefStreamStats[_util.StreamType.LZW] = true;
+ var earlyChange = 1;
+
+ if (params) {
+ if (params.has('EarlyChange')) {
+ earlyChange = params.get('EarlyChange');
+ }
+
+ return new _stream.PredictorStream(new _stream.LZWStream(stream, maybeLength, earlyChange), maybeLength, params);
+ }
+
+ return new _stream.LZWStream(stream, maybeLength, earlyChange);
+ }
+
+ if (name === 'DCTDecode' || name === 'DCT') {
+ xrefStreamStats[_util.StreamType.DCT] = true;
+ return new _jpeg_stream.JpegStream(stream, maybeLength, stream.dict, params);
+ }
+
+ if (name === 'JPXDecode' || name === 'JPX') {
+ xrefStreamStats[_util.StreamType.JPX] = true;
+ return new _jpx_stream.JpxStream(stream, maybeLength, stream.dict, params);
+ }
+
+ if (name === 'ASCII85Decode' || name === 'A85') {
+ xrefStreamStats[_util.StreamType.A85] = true;
+ return new _stream.Ascii85Stream(stream, maybeLength);
+ }
+
+ if (name === 'ASCIIHexDecode' || name === 'AHx') {
+ xrefStreamStats[_util.StreamType.AHX] = true;
+ return new _stream.AsciiHexStream(stream, maybeLength);
+ }
+
+ if (name === 'CCITTFaxDecode' || name === 'CCF') {
+ xrefStreamStats[_util.StreamType.CCF] = true;
+ return new _ccitt_stream.CCITTFaxStream(stream, maybeLength, params);
+ }
+
+ if (name === 'RunLengthDecode' || name === 'RL') {
+ xrefStreamStats[_util.StreamType.RL] = true;
+ return new _stream.RunLengthStream(stream, maybeLength);
+ }
+
+ if (name === 'JBIG2Decode') {
+ xrefStreamStats[_util.StreamType.JBIG] = true;
+ return new _jbig2_stream.Jbig2Stream(stream, maybeLength, stream.dict, params);
+ }
+
+ (0, _util.warn)("Filter \"".concat(name, "\" is not supported."));
+ return stream;
+ } catch (ex) {
+ if (ex instanceof _core_utils.MissingDataException) {
+ throw ex;
+ }
+
+ (0, _util.warn)("Invalid stream: \"".concat(ex, "\""));
+ return new _stream.NullStream();
+ }
+ }
+ }]);
+
+ return Parser;
+}();
+
+exports.Parser = Parser;
+var specialChars = [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+
+function toHexDigit(ch) {
+ if (ch >= 0x30 && ch <= 0x39) {
+ return ch & 0x0F;
+ }
+
+ if (ch >= 0x41 && ch <= 0x46 || ch >= 0x61 && ch <= 0x66) {
+ return (ch & 0x0F) + 9;
+ }
+
+ return -1;
+}
+
+var Lexer =
+/*#__PURE__*/
+function () {
+ function Lexer(stream) {
+ var knownCommands = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
+
+ _classCallCheck(this, Lexer);
+
+ this.stream = stream;
+ this.nextChar();
+ this.strBuf = [];
+ this.knownCommands = knownCommands;
+ this.beginInlineImagePos = -1;
+ }
+
+ _createClass(Lexer, [{
+ key: "nextChar",
+ value: function nextChar() {
+ return this.currentChar = this.stream.getByte();
+ }
+ }, {
+ key: "peekChar",
+ value: function peekChar() {
+ return this.stream.peekByte();
+ }
+ }, {
+ key: "getNumber",
+ value: function getNumber() {
+ var ch = this.currentChar;
+ var eNotation = false;
+ var divideBy = 0;
+ var sign = 0;
+
+ if (ch === 0x2D) {
+ sign = -1;
+ ch = this.nextChar();
+
+ if (ch === 0x2D) {
+ ch = this.nextChar();
+ }
+ } else if (ch === 0x2B) {
+ sign = 1;
+ ch = this.nextChar();
+ }
+
+ if (ch === 0x0A || ch === 0x0D) {
+ do {
+ ch = this.nextChar();
+ } while (ch === 0x0A || ch === 0x0D);
+ }
+
+ if (ch === 0x2E) {
+ divideBy = 10;
+ ch = this.nextChar();
+ }
+
+ if (ch < 0x30 || ch > 0x39) {
+ if (divideBy === 10 && sign === 0 && ((0, _util.isSpace)(ch) || ch === -1)) {
+ (0, _util.warn)('Lexer.getNumber - treating a single decimal point as zero.');
+ return 0;
+ }
+
+ throw new _util.FormatError("Invalid number: ".concat(String.fromCharCode(ch), " (charCode ").concat(ch, ")"));
+ }
+
+ sign = sign || 1;
+ var baseValue = ch - 0x30;
+ var powerValue = 0;
+ var powerValueSign = 1;
+
+ while ((ch = this.nextChar()) >= 0) {
+ if (0x30 <= ch && ch <= 0x39) {
+ var currentDigit = ch - 0x30;
+
+ if (eNotation) {
+ powerValue = powerValue * 10 + currentDigit;
+ } else {
+ if (divideBy !== 0) {
+ divideBy *= 10;
+ }
+
+ baseValue = baseValue * 10 + currentDigit;
+ }
+ } else if (ch === 0x2E) {
+ if (divideBy === 0) {
+ divideBy = 1;
+ } else {
+ break;
+ }
+ } else if (ch === 0x2D) {
+ (0, _util.warn)('Badly formatted number: minus sign in the middle');
+ } else if (ch === 0x45 || ch === 0x65) {
+ ch = this.peekChar();
+
+ if (ch === 0x2B || ch === 0x2D) {
+ powerValueSign = ch === 0x2D ? -1 : 1;
+ this.nextChar();
+ } else if (ch < 0x30 || ch > 0x39) {
+ break;
+ }
+
+ eNotation = true;
+ } else {
+ break;
+ }
+ }
+
+ if (divideBy !== 0) {
+ baseValue /= divideBy;
+ }
+
+ if (eNotation) {
+ baseValue *= Math.pow(10, powerValueSign * powerValue);
+ }
+
+ return sign * baseValue;
+ }
+ }, {
+ key: "getString",
+ value: function getString() {
+ var numParen = 1;
+ var done = false;
+ var strBuf = this.strBuf;
+ strBuf.length = 0;
+ var ch = this.nextChar();
+
+ while (true) {
+ var charBuffered = false;
+
+ switch (ch | 0) {
+ case -1:
+ (0, _util.warn)('Unterminated string');
+ done = true;
+ break;
+
+ case 0x28:
+ ++numParen;
+ strBuf.push('(');
+ break;
+
+ case 0x29:
+ if (--numParen === 0) {
+ this.nextChar();
+ done = true;
+ } else {
+ strBuf.push(')');
+ }
+
+ break;
+
+ case 0x5C:
+ ch = this.nextChar();
+
+ switch (ch) {
+ case -1:
+ (0, _util.warn)('Unterminated string');
+ done = true;
+ break;
+
+ case 0x6E:
+ strBuf.push('\n');
+ break;
+
+ case 0x72:
+ strBuf.push('\r');
+ break;
+
+ case 0x74:
+ strBuf.push('\t');
+ break;
+
+ case 0x62:
+ strBuf.push('\b');
+ break;
+
+ case 0x66:
+ strBuf.push('\f');
+ break;
+
+ case 0x5C:
+ case 0x28:
+ case 0x29:
+ strBuf.push(String.fromCharCode(ch));
+ break;
+
+ case 0x30:
+ case 0x31:
+ case 0x32:
+ case 0x33:
+ case 0x34:
+ case 0x35:
+ case 0x36:
+ case 0x37:
+ var x = ch & 0x0F;
+ ch = this.nextChar();
+ charBuffered = true;
+
+ if (ch >= 0x30 && ch <= 0x37) {
+ x = (x << 3) + (ch & 0x0F);
+ ch = this.nextChar();
+
+ if (ch >= 0x30 && ch <= 0x37) {
+ charBuffered = false;
+ x = (x << 3) + (ch & 0x0F);
+ }
+ }
+
+ strBuf.push(String.fromCharCode(x));
+ break;
+
+ case 0x0D:
+ if (this.peekChar() === 0x0A) {
+ this.nextChar();
+ }
+
+ break;
+
+ case 0x0A:
+ break;
+
+ default:
+ strBuf.push(String.fromCharCode(ch));
+ break;
+ }
+
+ break;
+
+ default:
+ strBuf.push(String.fromCharCode(ch));
+ break;
+ }
+
+ if (done) {
+ break;
+ }
+
+ if (!charBuffered) {
+ ch = this.nextChar();
+ }
+ }
+
+ return strBuf.join('');
+ }
+ }, {
+ key: "getName",
+ value: function getName() {
+ var ch, previousCh;
+ var strBuf = this.strBuf;
+ strBuf.length = 0;
+
+ while ((ch = this.nextChar()) >= 0 && !specialChars[ch]) {
+ if (ch === 0x23) {
+ ch = this.nextChar();
+
+ if (specialChars[ch]) {
+ (0, _util.warn)('Lexer_getName: ' + 'NUMBER SIGN (#) should be followed by a hexadecimal number.');
+ strBuf.push('#');
+ break;
+ }
+
+ var x = toHexDigit(ch);
+
+ if (x !== -1) {
+ previousCh = ch;
+ ch = this.nextChar();
+ var x2 = toHexDigit(ch);
+
+ if (x2 === -1) {
+ (0, _util.warn)("Lexer_getName: Illegal digit (".concat(String.fromCharCode(ch), ") ") + 'in hexadecimal number.');
+ strBuf.push('#', String.fromCharCode(previousCh));
+
+ if (specialChars[ch]) {
+ break;
+ }
+
+ strBuf.push(String.fromCharCode(ch));
+ continue;
+ }
+
+ strBuf.push(String.fromCharCode(x << 4 | x2));
+ } else {
+ strBuf.push('#', String.fromCharCode(ch));
+ }
+ } else {
+ strBuf.push(String.fromCharCode(ch));
+ }
+ }
+
+ if (strBuf.length > 127) {
+ (0, _util.warn)("Name token is longer than allowed by the spec: ".concat(strBuf.length));
+ }
+
+ return _primitives.Name.get(strBuf.join(''));
+ }
+ }, {
+ key: "getHexString",
+ value: function getHexString() {
+ var strBuf = this.strBuf;
+ strBuf.length = 0;
+ var ch = this.currentChar;
+ var isFirstHex = true;
+ var firstDigit, secondDigit;
+
+ while (true) {
+ if (ch < 0) {
+ (0, _util.warn)('Unterminated hex string');
+ break;
+ } else if (ch === 0x3E) {
+ this.nextChar();
+ break;
+ } else if (specialChars[ch] === 1) {
+ ch = this.nextChar();
+ continue;
+ } else {
+ if (isFirstHex) {
+ firstDigit = toHexDigit(ch);
+
+ if (firstDigit === -1) {
+ (0, _util.warn)("Ignoring invalid character \"".concat(ch, "\" in hex string"));
+ ch = this.nextChar();
+ continue;
+ }
+ } else {
+ secondDigit = toHexDigit(ch);
+
+ if (secondDigit === -1) {
+ (0, _util.warn)("Ignoring invalid character \"".concat(ch, "\" in hex string"));
+ ch = this.nextChar();
+ continue;
+ }
+
+ strBuf.push(String.fromCharCode(firstDigit << 4 | secondDigit));
+ }
+
+ isFirstHex = !isFirstHex;
+ ch = this.nextChar();
+ }
+ }
+
+ return strBuf.join('');
+ }
+ }, {
+ key: "getObj",
+ value: function getObj() {
+ var comment = false;
+ var ch = this.currentChar;
+
+ while (true) {
+ if (ch < 0) {
+ return _primitives.EOF;
+ }
+
+ if (comment) {
+ if (ch === 0x0A || ch === 0x0D) {
+ comment = false;
+ }
+ } else if (ch === 0x25) {
+ comment = true;
+ } else if (specialChars[ch] !== 1) {
+ break;
+ }
+
+ ch = this.nextChar();
+ }
+
+ switch (ch | 0) {
+ case 0x30:
+ case 0x31:
+ case 0x32:
+ case 0x33:
+ case 0x34:
+ case 0x35:
+ case 0x36:
+ case 0x37:
+ case 0x38:
+ case 0x39:
+ case 0x2B:
+ case 0x2D:
+ case 0x2E:
+ return this.getNumber();
+
+ case 0x28:
+ return this.getString();
+
+ case 0x2F:
+ return this.getName();
+
+ case 0x5B:
+ this.nextChar();
+ return _primitives.Cmd.get('[');
+
+ case 0x5D:
+ this.nextChar();
+ return _primitives.Cmd.get(']');
+
+ case 0x3C:
+ ch = this.nextChar();
+
+ if (ch === 0x3C) {
+ this.nextChar();
+ return _primitives.Cmd.get('<<');
+ }
+
+ return this.getHexString();
+
+ case 0x3E:
+ ch = this.nextChar();
+
+ if (ch === 0x3E) {
+ this.nextChar();
+ return _primitives.Cmd.get('>>');
+ }
+
+ return _primitives.Cmd.get('>');
+
+ case 0x7B:
+ this.nextChar();
+ return _primitives.Cmd.get('{');
+
+ case 0x7D:
+ this.nextChar();
+ return _primitives.Cmd.get('}');
+
+ case 0x29:
+ this.nextChar();
+ throw new _util.FormatError("Illegal character: ".concat(ch));
+ }
+
+ var str = String.fromCharCode(ch);
+ var knownCommands = this.knownCommands;
+ var knownCommandFound = knownCommands && knownCommands[str] !== undefined;
+
+ while ((ch = this.nextChar()) >= 0 && !specialChars[ch]) {
+ var possibleCommand = str + String.fromCharCode(ch);
+
+ if (knownCommandFound && knownCommands[possibleCommand] === undefined) {
+ break;
+ }
+
+ if (str.length === 128) {
+ throw new _util.FormatError("Command token too long: ".concat(str.length));
+ }
+
+ str = possibleCommand;
+ knownCommandFound = knownCommands && knownCommands[str] !== undefined;
+ }
+
+ if (str === 'true') {
+ return true;
+ }
+
+ if (str === 'false') {
+ return false;
+ }
+
+ if (str === 'null') {
+ return null;
+ }
+
+ if (str === 'BI') {
+ this.beginInlineImagePos = this.stream.pos;
+ }
+
+ return _primitives.Cmd.get(str);
+ }
+ }, {
+ key: "skipToNextLine",
+ value: function skipToNextLine() {
+ var ch = this.currentChar;
+
+ while (ch >= 0) {
+ if (ch === 0x0D) {
+ ch = this.nextChar();
+
+ if (ch === 0x0A) {
+ this.nextChar();
+ }
+
+ break;
+ } else if (ch === 0x0A) {
+ this.nextChar();
+ break;
+ }
+
+ ch = this.nextChar();
+ }
+ }
+ }]);
+
+ return Lexer;
+}();
+
+exports.Lexer = Lexer;
+
+var Linearization =
+/*#__PURE__*/
+function () {
+ function Linearization() {
+ _classCallCheck(this, Linearization);
+ }
+
+ _createClass(Linearization, null, [{
+ key: "create",
+ value: function create(stream) {
+ function getInt(linDict, name) {
+ var allowZeroValue = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+ var obj = linDict.get(name);
+
+ if (Number.isInteger(obj) && (allowZeroValue ? obj >= 0 : obj > 0)) {
+ return obj;
+ }
+
+ throw new Error("The \"".concat(name, "\" parameter in the linearization ") + 'dictionary is invalid.');
+ }
+
+ function getHints(linDict) {
+ var hints = linDict.get('H');
+ var hintsLength;
+
+ if (Array.isArray(hints) && ((hintsLength = hints.length) === 2 || hintsLength === 4)) {
+ for (var index = 0; index < hintsLength; index++) {
+ var hint = hints[index];
+
+ if (!(Number.isInteger(hint) && hint > 0)) {
+ throw new Error("Hint (".concat(index, ") in the linearization dictionary ") + 'is invalid.');
+ }
+ }
+
+ return hints;
+ }
+
+ throw new Error('Hint array in the linearization dictionary is invalid.');
+ }
+
+ var parser = new Parser({
+ lexer: new Lexer(stream),
+ xref: null
+ });
+ var obj1 = parser.getObj();
+ var obj2 = parser.getObj();
+ var obj3 = parser.getObj();
+ var linDict = parser.getObj();
+ var obj, length;
+
+ if (!(Number.isInteger(obj1) && Number.isInteger(obj2) && (0, _primitives.isCmd)(obj3, 'obj') && (0, _primitives.isDict)(linDict) && (0, _util.isNum)(obj = linDict.get('Linearized')) && obj > 0)) {
+ return null;
+ } else if ((length = getInt(linDict, 'L')) !== stream.length) {
+ throw new Error('The "L" parameter in the linearization dictionary ' + 'does not equal the stream length.');
+ }
+
+ return {
+ length: length,
+ hints: getHints(linDict),
+ objectNumberFirst: getInt(linDict, 'O'),
+ endFirst: getInt(linDict, 'E'),
+ numPages: getInt(linDict, 'N'),
+ mainXRefEntriesOffset: getInt(linDict, 'T'),
+ pageFirst: linDict.has('P') ? getInt(linDict, 'P', true) : 0
+ };
+ }
+ }]);
+
+ return Linearization;
+}();
+
+exports.Linearization = Linearization;
+
+/***/ }),
+/* 158 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.LZWStream = exports.StringStream = exports.StreamsSequenceStream = exports.Stream = exports.RunLengthStream = exports.PredictorStream = exports.NullStream = exports.FlateStream = exports.DecodeStream = exports.DecryptStream = exports.AsciiHexStream = exports.Ascii85Stream = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _primitives = __w_pdfjs_require__(151);
+
+function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
+
+function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
+
+function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
+
+function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }
+
+var Stream = function StreamClosure() {
+ function Stream(arrayBuffer, start, length, dict) {
+ this.bytes = arrayBuffer instanceof Uint8Array ? arrayBuffer : new Uint8Array(arrayBuffer);
+ this.start = start || 0;
+ this.pos = this.start;
+ this.end = start + length || this.bytes.length;
+ this.dict = dict;
+ }
+
+ Stream.prototype = {
+ get length() {
+ return this.end - this.start;
+ },
+
+ get isEmpty() {
+ return this.length === 0;
+ },
+
+ getByte: function Stream_getByte() {
+ if (this.pos >= this.end) {
+ return -1;
+ }
+
+ return this.bytes[this.pos++];
+ },
+ getUint16: function Stream_getUint16() {
+ var b0 = this.getByte();
+ var b1 = this.getByte();
+
+ if (b0 === -1 || b1 === -1) {
+ return -1;
+ }
+
+ return (b0 << 8) + b1;
+ },
+ getInt32: function Stream_getInt32() {
+ var b0 = this.getByte();
+ var b1 = this.getByte();
+ var b2 = this.getByte();
+ var b3 = this.getByte();
+ return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3;
+ },
+ getBytes: function getBytes(length) {
+ var forceClamped = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var bytes = this.bytes;
+ var pos = this.pos;
+ var strEnd = this.end;
+
+ if (!length) {
+ var _subarray = bytes.subarray(pos, strEnd);
+
+ return forceClamped ? new Uint8ClampedArray(_subarray) : _subarray;
+ }
+
+ var end = pos + length;
+
+ if (end > strEnd) {
+ end = strEnd;
+ }
+
+ this.pos = end;
+ var subarray = bytes.subarray(pos, end);
+ return forceClamped ? new Uint8ClampedArray(subarray) : subarray;
+ },
+ peekByte: function Stream_peekByte() {
+ var peekedByte = this.getByte();
+ this.pos--;
+ return peekedByte;
+ },
+ peekBytes: function peekBytes(length) {
+ var forceClamped = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var bytes = this.getBytes(length, forceClamped);
+ this.pos -= bytes.length;
+ return bytes;
+ },
+ skip: function Stream_skip(n) {
+ if (!n) {
+ n = 1;
+ }
+
+ this.pos += n;
+ },
+ reset: function Stream_reset() {
+ this.pos = this.start;
+ },
+ moveStart: function Stream_moveStart() {
+ this.start = this.pos;
+ },
+ makeSubStream: function Stream_makeSubStream(start, length, dict) {
+ return new Stream(this.bytes.buffer, start, length, dict);
+ }
+ };
+ return Stream;
+}();
+
+exports.Stream = Stream;
+
+var StringStream = function StringStreamClosure() {
+ function StringStream(str) {
+ var bytes = (0, _util.stringToBytes)(str);
+ Stream.call(this, bytes);
+ }
+
+ StringStream.prototype = Stream.prototype;
+ return StringStream;
+}();
+
+exports.StringStream = StringStream;
+
+var DecodeStream = function DecodeStreamClosure() {
+ var emptyBuffer = new Uint8Array(0);
+
+ function DecodeStream(maybeMinBufferLength) {
+ this._rawMinBufferLength = maybeMinBufferLength || 0;
+ this.pos = 0;
+ this.bufferLength = 0;
+ this.eof = false;
+ this.buffer = emptyBuffer;
+ this.minBufferLength = 512;
+
+ if (maybeMinBufferLength) {
+ while (this.minBufferLength < maybeMinBufferLength) {
+ this.minBufferLength *= 2;
+ }
+ }
+ }
+
+ DecodeStream.prototype = {
+ get isEmpty() {
+ while (!this.eof && this.bufferLength === 0) {
+ this.readBlock();
+ }
+
+ return this.bufferLength === 0;
+ },
+
+ ensureBuffer: function DecodeStream_ensureBuffer(requested) {
+ var buffer = this.buffer;
+
+ if (requested <= buffer.byteLength) {
+ return buffer;
+ }
+
+ var size = this.minBufferLength;
+
+ while (size < requested) {
+ size *= 2;
+ }
+
+ var buffer2 = new Uint8Array(size);
+ buffer2.set(buffer);
+ return this.buffer = buffer2;
+ },
+ getByte: function DecodeStream_getByte() {
+ var pos = this.pos;
+
+ while (this.bufferLength <= pos) {
+ if (this.eof) {
+ return -1;
+ }
+
+ this.readBlock();
+ }
+
+ return this.buffer[this.pos++];
+ },
+ getUint16: function DecodeStream_getUint16() {
+ var b0 = this.getByte();
+ var b1 = this.getByte();
+
+ if (b0 === -1 || b1 === -1) {
+ return -1;
+ }
+
+ return (b0 << 8) + b1;
+ },
+ getInt32: function DecodeStream_getInt32() {
+ var b0 = this.getByte();
+ var b1 = this.getByte();
+ var b2 = this.getByte();
+ var b3 = this.getByte();
+ return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3;
+ },
+ getBytes: function getBytes(length) {
+ var forceClamped = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var end,
+ pos = this.pos;
+
+ if (length) {
+ this.ensureBuffer(pos + length);
+ end = pos + length;
+
+ while (!this.eof && this.bufferLength < end) {
+ this.readBlock();
+ }
+
+ var bufEnd = this.bufferLength;
+
+ if (end > bufEnd) {
+ end = bufEnd;
+ }
+ } else {
+ while (!this.eof) {
+ this.readBlock();
+ }
+
+ end = this.bufferLength;
+ }
+
+ this.pos = end;
+ var subarray = this.buffer.subarray(pos, end);
+ return forceClamped && !(subarray instanceof Uint8ClampedArray) ? new Uint8ClampedArray(subarray) : subarray;
+ },
+ peekByte: function DecodeStream_peekByte() {
+ var peekedByte = this.getByte();
+ this.pos--;
+ return peekedByte;
+ },
+ peekBytes: function peekBytes(length) {
+ var forceClamped = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var bytes = this.getBytes(length, forceClamped);
+ this.pos -= bytes.length;
+ return bytes;
+ },
+ makeSubStream: function DecodeStream_makeSubStream(start, length, dict) {
+ var end = start + length;
+
+ while (this.bufferLength <= end && !this.eof) {
+ this.readBlock();
+ }
+
+ return new Stream(this.buffer, start, length, dict);
+ },
+ skip: function DecodeStream_skip(n) {
+ if (!n) {
+ n = 1;
+ }
+
+ this.pos += n;
+ },
+ reset: function DecodeStream_reset() {
+ this.pos = 0;
+ },
+ getBaseStreams: function DecodeStream_getBaseStreams() {
+ if (this.str && this.str.getBaseStreams) {
+ return this.str.getBaseStreams();
+ }
+
+ return [];
+ }
+ };
+ return DecodeStream;
+}();
+
+exports.DecodeStream = DecodeStream;
+
+var StreamsSequenceStream = function StreamsSequenceStreamClosure() {
+ function StreamsSequenceStream(streams) {
+ this.streams = streams;
+ var maybeLength = 0;
+
+ for (var i = 0, ii = streams.length; i < ii; i++) {
+ var stream = streams[i];
+
+ if (stream instanceof DecodeStream) {
+ maybeLength += stream._rawMinBufferLength;
+ } else {
+ maybeLength += stream.length;
+ }
+ }
+
+ DecodeStream.call(this, maybeLength);
+ }
+
+ StreamsSequenceStream.prototype = Object.create(DecodeStream.prototype);
+
+ StreamsSequenceStream.prototype.readBlock = function streamSequenceStreamReadBlock() {
+ var streams = this.streams;
+
+ if (streams.length === 0) {
+ this.eof = true;
+ return;
+ }
+
+ var stream = streams.shift();
+ var chunk = stream.getBytes();
+ var bufferLength = this.bufferLength;
+ var newLength = bufferLength + chunk.length;
+ var buffer = this.ensureBuffer(newLength);
+ buffer.set(chunk, bufferLength);
+ this.bufferLength = newLength;
+ };
+
+ StreamsSequenceStream.prototype.getBaseStreams = function StreamsSequenceStream_getBaseStreams() {
+ var baseStreams = [];
+
+ for (var i = 0, ii = this.streams.length; i < ii; i++) {
+ var stream = this.streams[i];
+
+ if (stream.getBaseStreams) {
+ baseStreams.push.apply(baseStreams, _toConsumableArray(stream.getBaseStreams()));
+ }
+ }
+
+ return baseStreams;
+ };
+
+ return StreamsSequenceStream;
+}();
+
+exports.StreamsSequenceStream = StreamsSequenceStream;
+
+var FlateStream = function FlateStreamClosure() {
+ var codeLenCodeMap = new Int32Array([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
+ var lengthDecode = new Int32Array([0x00003, 0x00004, 0x00005, 0x00006, 0x00007, 0x00008, 0x00009, 0x0000a, 0x1000b, 0x1000d, 0x1000f, 0x10011, 0x20013, 0x20017, 0x2001b, 0x2001f, 0x30023, 0x3002b, 0x30033, 0x3003b, 0x40043, 0x40053, 0x40063, 0x40073, 0x50083, 0x500a3, 0x500c3, 0x500e3, 0x00102, 0x00102, 0x00102]);
+ var distDecode = new Int32Array([0x00001, 0x00002, 0x00003, 0x00004, 0x10005, 0x10007, 0x20009, 0x2000d, 0x30011, 0x30019, 0x40021, 0x40031, 0x50041, 0x50061, 0x60081, 0x600c1, 0x70101, 0x70181, 0x80201, 0x80301, 0x90401, 0x90601, 0xa0801, 0xa0c01, 0xb1001, 0xb1801, 0xc2001, 0xc3001, 0xd4001, 0xd6001]);
+ var fixedLitCodeTab = [new Int32Array([0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c0, 0x70108, 0x80060, 0x80020, 0x900a0, 0x80000, 0x80080, 0x80040, 0x900e0, 0x70104, 0x80058, 0x80018, 0x90090, 0x70114, 0x80078, 0x80038, 0x900d0, 0x7010c, 0x80068, 0x80028, 0x900b0, 0x80008, 0x80088, 0x80048, 0x900f0, 0x70102, 0x80054, 0x80014, 0x8011c, 0x70112, 0x80074, 0x80034, 0x900c8, 0x7010a, 0x80064, 0x80024, 0x900a8, 0x80004, 0x80084, 0x80044, 0x900e8, 0x70106, 0x8005c, 0x8001c, 0x90098, 0x70116, 0x8007c, 0x8003c, 0x900d8, 0x7010e, 0x8006c, 0x8002c, 0x900b8, 0x8000c, 0x8008c, 0x8004c, 0x900f8, 0x70101, 0x80052, 0x80012, 0x8011a, 0x70111, 0x80072, 0x80032, 0x900c4, 0x70109, 0x80062, 0x80022, 0x900a4, 0x80002, 0x80082, 0x80042, 0x900e4, 0x70105, 0x8005a, 0x8001a, 0x90094, 0x70115, 0x8007a, 0x8003a, 0x900d4, 0x7010d, 0x8006a, 0x8002a, 0x900b4, 0x8000a, 0x8008a, 0x8004a, 0x900f4, 0x70103, 0x80056, 0x80016, 0x8011e, 0x70113, 0x80076, 0x80036, 0x900cc, 0x7010b, 0x80066, 0x80026, 0x900ac, 0x80006, 0x80086, 0x80046, 0x900ec, 0x70107, 0x8005e, 0x8001e, 0x9009c, 0x70117, 0x8007e, 0x8003e, 0x900dc, 0x7010f, 0x8006e, 0x8002e, 0x900bc, 0x8000e, 0x8008e, 0x8004e, 0x900fc, 0x70100, 0x80051, 0x80011, 0x80119, 0x70110, 0x80071, 0x80031, 0x900c2, 0x70108, 0x80061, 0x80021, 0x900a2, 0x80001, 0x80081, 0x80041, 0x900e2, 0x70104, 0x80059, 0x80019, 0x90092, 0x70114, 0x80079, 0x80039, 0x900d2, 0x7010c, 0x80069, 0x80029, 0x900b2, 0x80009, 0x80089, 0x80049, 0x900f2, 0x70102, 0x80055, 0x80015, 0x8011d, 0x70112, 0x80075, 0x80035, 0x900ca, 0x7010a, 0x80065, 0x80025, 0x900aa, 0x80005, 0x80085, 0x80045, 0x900ea, 0x70106, 0x8005d, 0x8001d, 0x9009a, 0x70116, 0x8007d, 0x8003d, 0x900da, 0x7010e, 0x8006d, 0x8002d, 0x900ba, 0x8000d, 0x8008d, 0x8004d, 0x900fa, 0x70101, 0x80053, 0x80013, 0x8011b, 0x70111, 0x80073, 0x80033, 0x900c6, 0x70109, 0x80063, 0x80023, 0x900a6, 0x80003, 0x80083, 0x80043, 0x900e6, 0x70105, 0x8005b, 0x8001b, 0x90096, 0x70115, 0x8007b, 0x8003b, 0x900d6, 0x7010d, 0x8006b, 0x8002b, 0x900b6, 0x8000b, 0x8008b, 0x8004b, 0x900f6, 0x70103, 0x80057, 0x80017, 0x8011f, 0x70113, 0x80077, 0x80037, 0x900ce, 0x7010b, 0x80067, 0x80027, 0x900ae, 0x80007, 0x80087, 0x80047, 0x900ee, 0x70107, 0x8005f, 0x8001f, 0x9009e, 0x70117, 0x8007f, 0x8003f, 0x900de, 0x7010f, 0x8006f, 0x8002f, 0x900be, 0x8000f, 0x8008f, 0x8004f, 0x900fe, 0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c1, 0x70108, 0x80060, 0x80020, 0x900a1, 0x80000, 0x80080, 0x80040, 0x900e1, 0x70104, 0x80058, 0x80018, 0x90091, 0x70114, 0x80078, 0x80038, 0x900d1, 0x7010c, 0x80068, 0x80028, 0x900b1, 0x80008, 0x80088, 0x80048, 0x900f1, 0x70102, 0x80054, 0x80014, 0x8011c, 0x70112, 0x80074, 0x80034, 0x900c9, 0x7010a, 0x80064, 0x80024, 0x900a9, 0x80004, 0x80084, 0x80044, 0x900e9, 0x70106, 0x8005c, 0x8001c, 0x90099, 0x70116, 0x8007c, 0x8003c, 0x900d9, 0x7010e, 0x8006c, 0x8002c, 0x900b9, 0x8000c, 0x8008c, 0x8004c, 0x900f9, 0x70101, 0x80052, 0x80012, 0x8011a, 0x70111, 0x80072, 0x80032, 0x900c5, 0x70109, 0x80062, 0x80022, 0x900a5, 0x80002, 0x80082, 0x80042, 0x900e5, 0x70105, 0x8005a, 0x8001a, 0x90095, 0x70115, 0x8007a, 0x8003a, 0x900d5, 0x7010d, 0x8006a, 0x8002a, 0x900b5, 0x8000a, 0x8008a, 0x8004a, 0x900f5, 0x70103, 0x80056, 0x80016, 0x8011e, 0x70113, 0x80076, 0x80036, 0x900cd, 0x7010b, 0x80066, 0x80026, 0x900ad, 0x80006, 0x80086, 0x80046, 0x900ed, 0x70107, 0x8005e, 0x8001e, 0x9009d, 0x70117, 0x8007e, 0x8003e, 0x900dd, 0x7010f, 0x8006e, 0x8002e, 0x900bd, 0x8000e, 0x8008e, 0x8004e, 0x900fd, 0x70100, 0x80051, 0x80011, 0x80119, 0x70110, 0x80071, 0x80031, 0x900c3, 0x70108, 0x80061, 0x80021, 0x900a3, 0x80001, 0x80081, 0x80041, 0x900e3, 0x70104, 0x80059, 0x80019, 0x90093, 0x70114, 0x80079, 0x80039, 0x900d3, 0x7010c, 0x80069, 0x80029, 0x900b3, 0x80009, 0x80089, 0x80049, 0x900f3, 0x70102, 0x80055, 0x80015, 0x8011d, 0x70112, 0x80075, 0x80035, 0x900cb, 0x7010a, 0x80065, 0x80025, 0x900ab, 0x80005, 0x80085, 0x80045, 0x900eb, 0x70106, 0x8005d, 0x8001d, 0x9009b, 0x70116, 0x8007d, 0x8003d, 0x900db, 0x7010e, 0x8006d, 0x8002d, 0x900bb, 0x8000d, 0x8008d, 0x8004d, 0x900fb, 0x70101, 0x80053, 0x80013, 0x8011b, 0x70111, 0x80073, 0x80033, 0x900c7, 0x70109, 0x80063, 0x80023, 0x900a7, 0x80003, 0x80083, 0x80043, 0x900e7, 0x70105, 0x8005b, 0x8001b, 0x90097, 0x70115, 0x8007b, 0x8003b, 0x900d7, 0x7010d, 0x8006b, 0x8002b, 0x900b7, 0x8000b, 0x8008b, 0x8004b, 0x900f7, 0x70103, 0x80057, 0x80017, 0x8011f, 0x70113, 0x80077, 0x80037, 0x900cf, 0x7010b, 0x80067, 0x80027, 0x900af, 0x80007, 0x80087, 0x80047, 0x900ef, 0x70107, 0x8005f, 0x8001f, 0x9009f, 0x70117, 0x8007f, 0x8003f, 0x900df, 0x7010f, 0x8006f, 0x8002f, 0x900bf, 0x8000f, 0x8008f, 0x8004f, 0x900ff]), 9];
+ var fixedDistCodeTab = [new Int32Array([0x50000, 0x50010, 0x50008, 0x50018, 0x50004, 0x50014, 0x5000c, 0x5001c, 0x50002, 0x50012, 0x5000a, 0x5001a, 0x50006, 0x50016, 0x5000e, 0x00000, 0x50001, 0x50011, 0x50009, 0x50019, 0x50005, 0x50015, 0x5000d, 0x5001d, 0x50003, 0x50013, 0x5000b, 0x5001b, 0x50007, 0x50017, 0x5000f, 0x00000]), 5];
+
+ function FlateStream(str, maybeLength) {
+ this.str = str;
+ this.dict = str.dict;
+ var cmf = str.getByte();
+ var flg = str.getByte();
+
+ if (cmf === -1 || flg === -1) {
+ throw new _util.FormatError("Invalid header in flate stream: ".concat(cmf, ", ").concat(flg));
+ }
+
+ if ((cmf & 0x0f) !== 0x08) {
+ throw new _util.FormatError("Unknown compression method in flate stream: ".concat(cmf, ", ").concat(flg));
+ }
+
+ if (((cmf << 8) + flg) % 31 !== 0) {
+ throw new _util.FormatError("Bad FCHECK in flate stream: ".concat(cmf, ", ").concat(flg));
+ }
+
+ if (flg & 0x20) {
+ throw new _util.FormatError("FDICT bit set in flate stream: ".concat(cmf, ", ").concat(flg));
+ }
+
+ this.codeSize = 0;
+ this.codeBuf = 0;
+ DecodeStream.call(this, maybeLength);
+ }
+
+ FlateStream.prototype = Object.create(DecodeStream.prototype);
+
+ FlateStream.prototype.getBits = function FlateStream_getBits(bits) {
+ var str = this.str;
+ var codeSize = this.codeSize;
+ var codeBuf = this.codeBuf;
+ var b;
+
+ while (codeSize < bits) {
+ if ((b = str.getByte()) === -1) {
+ throw new _util.FormatError('Bad encoding in flate stream');
+ }
+
+ codeBuf |= b << codeSize;
+ codeSize += 8;
+ }
+
+ b = codeBuf & (1 << bits) - 1;
+ this.codeBuf = codeBuf >> bits;
+ this.codeSize = codeSize -= bits;
+ return b;
+ };
+
+ FlateStream.prototype.getCode = function FlateStream_getCode(table) {
+ var str = this.str;
+ var codes = table[0];
+ var maxLen = table[1];
+ var codeSize = this.codeSize;
+ var codeBuf = this.codeBuf;
+ var b;
+
+ while (codeSize < maxLen) {
+ if ((b = str.getByte()) === -1) {
+ break;
+ }
+
+ codeBuf |= b << codeSize;
+ codeSize += 8;
+ }
+
+ var code = codes[codeBuf & (1 << maxLen) - 1];
+ var codeLen = code >> 16;
+ var codeVal = code & 0xffff;
+
+ if (codeLen < 1 || codeSize < codeLen) {
+ throw new _util.FormatError('Bad encoding in flate stream');
+ }
+
+ this.codeBuf = codeBuf >> codeLen;
+ this.codeSize = codeSize - codeLen;
+ return codeVal;
+ };
+
+ FlateStream.prototype.generateHuffmanTable = function flateStreamGenerateHuffmanTable(lengths) {
+ var n = lengths.length;
+ var maxLen = 0;
+ var i;
+
+ for (i = 0; i < n; ++i) {
+ if (lengths[i] > maxLen) {
+ maxLen = lengths[i];
+ }
+ }
+
+ var size = 1 << maxLen;
+ var codes = new Int32Array(size);
+
+ for (var len = 1, code = 0, skip = 2; len <= maxLen; ++len, code <<= 1, skip <<= 1) {
+ for (var val = 0; val < n; ++val) {
+ if (lengths[val] === len) {
+ var code2 = 0;
+ var t = code;
+
+ for (i = 0; i < len; ++i) {
+ code2 = code2 << 1 | t & 1;
+ t >>= 1;
+ }
+
+ for (i = code2; i < size; i += skip) {
+ codes[i] = len << 16 | val;
+ }
+
+ ++code;
+ }
+ }
+ }
+
+ return [codes, maxLen];
+ };
+
+ FlateStream.prototype.readBlock = function FlateStream_readBlock() {
+ var buffer, len;
+ var str = this.str;
+ var hdr = this.getBits(3);
+
+ if (hdr & 1) {
+ this.eof = true;
+ }
+
+ hdr >>= 1;
+
+ if (hdr === 0) {
+ var b;
+
+ if ((b = str.getByte()) === -1) {
+ throw new _util.FormatError('Bad block header in flate stream');
+ }
+
+ var blockLen = b;
+
+ if ((b = str.getByte()) === -1) {
+ throw new _util.FormatError('Bad block header in flate stream');
+ }
+
+ blockLen |= b << 8;
+
+ if ((b = str.getByte()) === -1) {
+ throw new _util.FormatError('Bad block header in flate stream');
+ }
+
+ var check = b;
+
+ if ((b = str.getByte()) === -1) {
+ throw new _util.FormatError('Bad block header in flate stream');
+ }
+
+ check |= b << 8;
+
+ if (check !== (~blockLen & 0xffff) && (blockLen !== 0 || check !== 0)) {
+ throw new _util.FormatError('Bad uncompressed block length in flate stream');
+ }
+
+ this.codeBuf = 0;
+ this.codeSize = 0;
+ var bufferLength = this.bufferLength;
+ buffer = this.ensureBuffer(bufferLength + blockLen);
+ var end = bufferLength + blockLen;
+ this.bufferLength = end;
+
+ if (blockLen === 0) {
+ if (str.peekByte() === -1) {
+ this.eof = true;
+ }
+ } else {
+ for (var n = bufferLength; n < end; ++n) {
+ if ((b = str.getByte()) === -1) {
+ this.eof = true;
+ break;
+ }
+
+ buffer[n] = b;
+ }
+ }
+
+ return;
+ }
+
+ var litCodeTable;
+ var distCodeTable;
+
+ if (hdr === 1) {
+ litCodeTable = fixedLitCodeTab;
+ distCodeTable = fixedDistCodeTab;
+ } else if (hdr === 2) {
+ var numLitCodes = this.getBits(5) + 257;
+ var numDistCodes = this.getBits(5) + 1;
+ var numCodeLenCodes = this.getBits(4) + 4;
+ var codeLenCodeLengths = new Uint8Array(codeLenCodeMap.length);
+ var i;
+
+ for (i = 0; i < numCodeLenCodes; ++i) {
+ codeLenCodeLengths[codeLenCodeMap[i]] = this.getBits(3);
+ }
+
+ var codeLenCodeTab = this.generateHuffmanTable(codeLenCodeLengths);
+ len = 0;
+ i = 0;
+ var codes = numLitCodes + numDistCodes;
+ var codeLengths = new Uint8Array(codes);
+ var bitsLength, bitsOffset, what;
+
+ while (i < codes) {
+ var code = this.getCode(codeLenCodeTab);
+
+ if (code === 16) {
+ bitsLength = 2;
+ bitsOffset = 3;
+ what = len;
+ } else if (code === 17) {
+ bitsLength = 3;
+ bitsOffset = 3;
+ what = len = 0;
+ } else if (code === 18) {
+ bitsLength = 7;
+ bitsOffset = 11;
+ what = len = 0;
+ } else {
+ codeLengths[i++] = len = code;
+ continue;
+ }
+
+ var repeatLength = this.getBits(bitsLength) + bitsOffset;
+
+ while (repeatLength-- > 0) {
+ codeLengths[i++] = what;
+ }
+ }
+
+ litCodeTable = this.generateHuffmanTable(codeLengths.subarray(0, numLitCodes));
+ distCodeTable = this.generateHuffmanTable(codeLengths.subarray(numLitCodes, codes));
+ } else {
+ throw new _util.FormatError('Unknown block type in flate stream');
+ }
+
+ buffer = this.buffer;
+ var limit = buffer ? buffer.length : 0;
+ var pos = this.bufferLength;
+
+ while (true) {
+ var code1 = this.getCode(litCodeTable);
+
+ if (code1 < 256) {
+ if (pos + 1 >= limit) {
+ buffer = this.ensureBuffer(pos + 1);
+ limit = buffer.length;
+ }
+
+ buffer[pos++] = code1;
+ continue;
+ }
+
+ if (code1 === 256) {
+ this.bufferLength = pos;
+ return;
+ }
+
+ code1 -= 257;
+ code1 = lengthDecode[code1];
+ var code2 = code1 >> 16;
+
+ if (code2 > 0) {
+ code2 = this.getBits(code2);
+ }
+
+ len = (code1 & 0xffff) + code2;
+ code1 = this.getCode(distCodeTable);
+ code1 = distDecode[code1];
+ code2 = code1 >> 16;
+
+ if (code2 > 0) {
+ code2 = this.getBits(code2);
+ }
+
+ var dist = (code1 & 0xffff) + code2;
+
+ if (pos + len >= limit) {
+ buffer = this.ensureBuffer(pos + len);
+ limit = buffer.length;
+ }
+
+ for (var k = 0; k < len; ++k, ++pos) {
+ buffer[pos] = buffer[pos - dist];
+ }
+ }
+ };
+
+ return FlateStream;
+}();
+
+exports.FlateStream = FlateStream;
+
+var PredictorStream = function PredictorStreamClosure() {
+ function PredictorStream(str, maybeLength, params) {
+ if (!(0, _primitives.isDict)(params)) {
+ return str;
+ }
+
+ var predictor = this.predictor = params.get('Predictor') || 1;
+
+ if (predictor <= 1) {
+ return str;
+ }
+
+ if (predictor !== 2 && (predictor < 10 || predictor > 15)) {
+ throw new _util.FormatError("Unsupported predictor: ".concat(predictor));
+ }
+
+ if (predictor === 2) {
+ this.readBlock = this.readBlockTiff;
+ } else {
+ this.readBlock = this.readBlockPng;
+ }
+
+ this.str = str;
+ this.dict = str.dict;
+ var colors = this.colors = params.get('Colors') || 1;
+ var bits = this.bits = params.get('BitsPerComponent') || 8;
+ var columns = this.columns = params.get('Columns') || 1;
+ this.pixBytes = colors * bits + 7 >> 3;
+ this.rowBytes = columns * colors * bits + 7 >> 3;
+ DecodeStream.call(this, maybeLength);
+ return this;
+ }
+
+ PredictorStream.prototype = Object.create(DecodeStream.prototype);
+
+ PredictorStream.prototype.readBlockTiff = function predictorStreamReadBlockTiff() {
+ var rowBytes = this.rowBytes;
+ var bufferLength = this.bufferLength;
+ var buffer = this.ensureBuffer(bufferLength + rowBytes);
+ var bits = this.bits;
+ var colors = this.colors;
+ var rawBytes = this.str.getBytes(rowBytes);
+ this.eof = !rawBytes.length;
+
+ if (this.eof) {
+ return;
+ }
+
+ var inbuf = 0,
+ outbuf = 0;
+ var inbits = 0,
+ outbits = 0;
+ var pos = bufferLength;
+ var i;
+
+ if (bits === 1 && colors === 1) {
+ for (i = 0; i < rowBytes; ++i) {
+ var c = rawBytes[i] ^ inbuf;
+ c ^= c >> 1;
+ c ^= c >> 2;
+ c ^= c >> 4;
+ inbuf = (c & 1) << 7;
+ buffer[pos++] = c;
+ }
+ } else if (bits === 8) {
+ for (i = 0; i < colors; ++i) {
+ buffer[pos++] = rawBytes[i];
+ }
+
+ for (; i < rowBytes; ++i) {
+ buffer[pos] = buffer[pos - colors] + rawBytes[i];
+ pos++;
+ }
+ } else if (bits === 16) {
+ var bytesPerPixel = colors * 2;
+
+ for (i = 0; i < bytesPerPixel; ++i) {
+ buffer[pos++] = rawBytes[i];
+ }
+
+ for (; i < rowBytes; i += 2) {
+ var sum = ((rawBytes[i] & 0xFF) << 8) + (rawBytes[i + 1] & 0xFF) + ((buffer[pos - bytesPerPixel] & 0xFF) << 8) + (buffer[pos - bytesPerPixel + 1] & 0xFF);
+ buffer[pos++] = sum >> 8 & 0xFF;
+ buffer[pos++] = sum & 0xFF;
+ }
+ } else {
+ var compArray = new Uint8Array(colors + 1);
+ var bitMask = (1 << bits) - 1;
+ var j = 0,
+ k = bufferLength;
+ var columns = this.columns;
+
+ for (i = 0; i < columns; ++i) {
+ for (var kk = 0; kk < colors; ++kk) {
+ if (inbits < bits) {
+ inbuf = inbuf << 8 | rawBytes[j++] & 0xFF;
+ inbits += 8;
+ }
+
+ compArray[kk] = compArray[kk] + (inbuf >> inbits - bits) & bitMask;
+ inbits -= bits;
+ outbuf = outbuf << bits | compArray[kk];
+ outbits += bits;
+
+ if (outbits >= 8) {
+ buffer[k++] = outbuf >> outbits - 8 & 0xFF;
+ outbits -= 8;
+ }
+ }
+ }
+
+ if (outbits > 0) {
+ buffer[k++] = (outbuf << 8 - outbits) + (inbuf & (1 << 8 - outbits) - 1);
+ }
+ }
+
+ this.bufferLength += rowBytes;
+ };
+
+ PredictorStream.prototype.readBlockPng = function predictorStreamReadBlockPng() {
+ var rowBytes = this.rowBytes;
+ var pixBytes = this.pixBytes;
+ var predictor = this.str.getByte();
+ var rawBytes = this.str.getBytes(rowBytes);
+ this.eof = !rawBytes.length;
+
+ if (this.eof) {
+ return;
+ }
+
+ var bufferLength = this.bufferLength;
+ var buffer = this.ensureBuffer(bufferLength + rowBytes);
+ var prevRow = buffer.subarray(bufferLength - rowBytes, bufferLength);
+
+ if (prevRow.length === 0) {
+ prevRow = new Uint8Array(rowBytes);
+ }
+
+ var i,
+ j = bufferLength,
+ up,
+ c;
+
+ switch (predictor) {
+ case 0:
+ for (i = 0; i < rowBytes; ++i) {
+ buffer[j++] = rawBytes[i];
+ }
+
+ break;
+
+ case 1:
+ for (i = 0; i < pixBytes; ++i) {
+ buffer[j++] = rawBytes[i];
+ }
+
+ for (; i < rowBytes; ++i) {
+ buffer[j] = buffer[j - pixBytes] + rawBytes[i] & 0xFF;
+ j++;
+ }
+
+ break;
+
+ case 2:
+ for (i = 0; i < rowBytes; ++i) {
+ buffer[j++] = prevRow[i] + rawBytes[i] & 0xFF;
+ }
+
+ break;
+
+ case 3:
+ for (i = 0; i < pixBytes; ++i) {
+ buffer[j++] = (prevRow[i] >> 1) + rawBytes[i];
+ }
+
+ for (; i < rowBytes; ++i) {
+ buffer[j] = (prevRow[i] + buffer[j - pixBytes] >> 1) + rawBytes[i] & 0xFF;
+ j++;
+ }
+
+ break;
+
+ case 4:
+ for (i = 0; i < pixBytes; ++i) {
+ up = prevRow[i];
+ c = rawBytes[i];
+ buffer[j++] = up + c;
+ }
+
+ for (; i < rowBytes; ++i) {
+ up = prevRow[i];
+ var upLeft = prevRow[i - pixBytes];
+ var left = buffer[j - pixBytes];
+ var p = left + up - upLeft;
+ var pa = p - left;
+
+ if (pa < 0) {
+ pa = -pa;
+ }
+
+ var pb = p - up;
+
+ if (pb < 0) {
+ pb = -pb;
+ }
+
+ var pc = p - upLeft;
+
+ if (pc < 0) {
+ pc = -pc;
+ }
+
+ c = rawBytes[i];
+
+ if (pa <= pb && pa <= pc) {
+ buffer[j++] = left + c;
+ } else if (pb <= pc) {
+ buffer[j++] = up + c;
+ } else {
+ buffer[j++] = upLeft + c;
+ }
+ }
+
+ break;
+
+ default:
+ throw new _util.FormatError("Unsupported predictor: ".concat(predictor));
+ }
+
+ this.bufferLength += rowBytes;
+ };
+
+ return PredictorStream;
+}();
+
+exports.PredictorStream = PredictorStream;
+
+var DecryptStream = function DecryptStreamClosure() {
+ function DecryptStream(str, maybeLength, decrypt) {
+ this.str = str;
+ this.dict = str.dict;
+ this.decrypt = decrypt;
+ this.nextChunk = null;
+ this.initialized = false;
+ DecodeStream.call(this, maybeLength);
+ }
+
+ var chunkSize = 512;
+ DecryptStream.prototype = Object.create(DecodeStream.prototype);
+
+ DecryptStream.prototype.readBlock = function DecryptStream_readBlock() {
+ var chunk;
+
+ if (this.initialized) {
+ chunk = this.nextChunk;
+ } else {
+ chunk = this.str.getBytes(chunkSize);
+ this.initialized = true;
+ }
+
+ if (!chunk || chunk.length === 0) {
+ this.eof = true;
+ return;
+ }
+
+ this.nextChunk = this.str.getBytes(chunkSize);
+ var hasMoreData = this.nextChunk && this.nextChunk.length > 0;
+ var decrypt = this.decrypt;
+ chunk = decrypt(chunk, !hasMoreData);
+ var bufferLength = this.bufferLength;
+ var i,
+ n = chunk.length;
+ var buffer = this.ensureBuffer(bufferLength + n);
+
+ for (i = 0; i < n; i++) {
+ buffer[bufferLength++] = chunk[i];
+ }
+
+ this.bufferLength = bufferLength;
+ };
+
+ return DecryptStream;
+}();
+
+exports.DecryptStream = DecryptStream;
+
+var Ascii85Stream = function Ascii85StreamClosure() {
+ function Ascii85Stream(str, maybeLength) {
+ this.str = str;
+ this.dict = str.dict;
+ this.input = new Uint8Array(5);
+
+ if (maybeLength) {
+ maybeLength = 0.8 * maybeLength;
+ }
+
+ DecodeStream.call(this, maybeLength);
+ }
+
+ Ascii85Stream.prototype = Object.create(DecodeStream.prototype);
+
+ Ascii85Stream.prototype.readBlock = function Ascii85Stream_readBlock() {
+ var TILDA_CHAR = 0x7E;
+ var Z_LOWER_CHAR = 0x7A;
+ var EOF = -1;
+ var str = this.str;
+ var c = str.getByte();
+
+ while ((0, _util.isSpace)(c)) {
+ c = str.getByte();
+ }
+
+ if (c === EOF || c === TILDA_CHAR) {
+ this.eof = true;
+ return;
+ }
+
+ var bufferLength = this.bufferLength,
+ buffer;
+ var i;
+
+ if (c === Z_LOWER_CHAR) {
+ buffer = this.ensureBuffer(bufferLength + 4);
+
+ for (i = 0; i < 4; ++i) {
+ buffer[bufferLength + i] = 0;
+ }
+
+ this.bufferLength += 4;
+ } else {
+ var input = this.input;
+ input[0] = c;
+
+ for (i = 1; i < 5; ++i) {
+ c = str.getByte();
+
+ while ((0, _util.isSpace)(c)) {
+ c = str.getByte();
+ }
+
+ input[i] = c;
+
+ if (c === EOF || c === TILDA_CHAR) {
+ break;
+ }
+ }
+
+ buffer = this.ensureBuffer(bufferLength + i - 1);
+ this.bufferLength += i - 1;
+
+ if (i < 5) {
+ for (; i < 5; ++i) {
+ input[i] = 0x21 + 84;
+ }
+
+ this.eof = true;
+ }
+
+ var t = 0;
+
+ for (i = 0; i < 5; ++i) {
+ t = t * 85 + (input[i] - 0x21);
+ }
+
+ for (i = 3; i >= 0; --i) {
+ buffer[bufferLength + i] = t & 0xFF;
+ t >>= 8;
+ }
+ }
+ };
+
+ return Ascii85Stream;
+}();
+
+exports.Ascii85Stream = Ascii85Stream;
+
+var AsciiHexStream = function AsciiHexStreamClosure() {
+ function AsciiHexStream(str, maybeLength) {
+ this.str = str;
+ this.dict = str.dict;
+ this.firstDigit = -1;
+
+ if (maybeLength) {
+ maybeLength = 0.5 * maybeLength;
+ }
+
+ DecodeStream.call(this, maybeLength);
+ }
+
+ AsciiHexStream.prototype = Object.create(DecodeStream.prototype);
+
+ AsciiHexStream.prototype.readBlock = function AsciiHexStream_readBlock() {
+ var UPSTREAM_BLOCK_SIZE = 8000;
+ var bytes = this.str.getBytes(UPSTREAM_BLOCK_SIZE);
+
+ if (!bytes.length) {
+ this.eof = true;
+ return;
+ }
+
+ var maxDecodeLength = bytes.length + 1 >> 1;
+ var buffer = this.ensureBuffer(this.bufferLength + maxDecodeLength);
+ var bufferLength = this.bufferLength;
+ var firstDigit = this.firstDigit;
+
+ for (var i = 0, ii = bytes.length; i < ii; i++) {
+ var ch = bytes[i],
+ digit;
+
+ if (ch >= 0x30 && ch <= 0x39) {
+ digit = ch & 0x0F;
+ } else if (ch >= 0x41 && ch <= 0x46 || ch >= 0x61 && ch <= 0x66) {
+ digit = (ch & 0x0F) + 9;
+ } else if (ch === 0x3E) {
+ this.eof = true;
+ break;
+ } else {
+ continue;
+ }
+
+ if (firstDigit < 0) {
+ firstDigit = digit;
+ } else {
+ buffer[bufferLength++] = firstDigit << 4 | digit;
+ firstDigit = -1;
+ }
+ }
+
+ if (firstDigit >= 0 && this.eof) {
+ buffer[bufferLength++] = firstDigit << 4;
+ firstDigit = -1;
+ }
+
+ this.firstDigit = firstDigit;
+ this.bufferLength = bufferLength;
+ };
+
+ return AsciiHexStream;
+}();
+
+exports.AsciiHexStream = AsciiHexStream;
+
+var RunLengthStream = function RunLengthStreamClosure() {
+ function RunLengthStream(str, maybeLength) {
+ this.str = str;
+ this.dict = str.dict;
+ DecodeStream.call(this, maybeLength);
+ }
+
+ RunLengthStream.prototype = Object.create(DecodeStream.prototype);
+
+ RunLengthStream.prototype.readBlock = function RunLengthStream_readBlock() {
+ var repeatHeader = this.str.getBytes(2);
+
+ if (!repeatHeader || repeatHeader.length < 2 || repeatHeader[0] === 128) {
+ this.eof = true;
+ return;
+ }
+
+ var buffer;
+ var bufferLength = this.bufferLength;
+ var n = repeatHeader[0];
+
+ if (n < 128) {
+ buffer = this.ensureBuffer(bufferLength + n + 1);
+ buffer[bufferLength++] = repeatHeader[1];
+
+ if (n > 0) {
+ var source = this.str.getBytes(n);
+ buffer.set(source, bufferLength);
+ bufferLength += n;
+ }
+ } else {
+ n = 257 - n;
+ var b = repeatHeader[1];
+ buffer = this.ensureBuffer(bufferLength + n + 1);
+
+ for (var i = 0; i < n; i++) {
+ buffer[bufferLength++] = b;
+ }
+ }
+
+ this.bufferLength = bufferLength;
+ };
+
+ return RunLengthStream;
+}();
+
+exports.RunLengthStream = RunLengthStream;
+
+var LZWStream = function LZWStreamClosure() {
+ function LZWStream(str, maybeLength, earlyChange) {
+ this.str = str;
+ this.dict = str.dict;
+ this.cachedData = 0;
+ this.bitsCached = 0;
+ var maxLzwDictionarySize = 4096;
+ var lzwState = {
+ earlyChange: earlyChange,
+ codeLength: 9,
+ nextCode: 258,
+ dictionaryValues: new Uint8Array(maxLzwDictionarySize),
+ dictionaryLengths: new Uint16Array(maxLzwDictionarySize),
+ dictionaryPrevCodes: new Uint16Array(maxLzwDictionarySize),
+ currentSequence: new Uint8Array(maxLzwDictionarySize),
+ currentSequenceLength: 0
+ };
+
+ for (var i = 0; i < 256; ++i) {
+ lzwState.dictionaryValues[i] = i;
+ lzwState.dictionaryLengths[i] = 1;
+ }
+
+ this.lzwState = lzwState;
+ DecodeStream.call(this, maybeLength);
+ }
+
+ LZWStream.prototype = Object.create(DecodeStream.prototype);
+
+ LZWStream.prototype.readBits = function LZWStream_readBits(n) {
+ var bitsCached = this.bitsCached;
+ var cachedData = this.cachedData;
+
+ while (bitsCached < n) {
+ var c = this.str.getByte();
+
+ if (c === -1) {
+ this.eof = true;
+ return null;
+ }
+
+ cachedData = cachedData << 8 | c;
+ bitsCached += 8;
+ }
+
+ this.bitsCached = bitsCached -= n;
+ this.cachedData = cachedData;
+ this.lastCode = null;
+ return cachedData >>> bitsCached & (1 << n) - 1;
+ };
+
+ LZWStream.prototype.readBlock = function LZWStream_readBlock() {
+ var blockSize = 512;
+ var estimatedDecodedSize = blockSize * 2,
+ decodedSizeDelta = blockSize;
+ var i, j, q;
+ var lzwState = this.lzwState;
+
+ if (!lzwState) {
+ return;
+ }
+
+ var earlyChange = lzwState.earlyChange;
+ var nextCode = lzwState.nextCode;
+ var dictionaryValues = lzwState.dictionaryValues;
+ var dictionaryLengths = lzwState.dictionaryLengths;
+ var dictionaryPrevCodes = lzwState.dictionaryPrevCodes;
+ var codeLength = lzwState.codeLength;
+ var prevCode = lzwState.prevCode;
+ var currentSequence = lzwState.currentSequence;
+ var currentSequenceLength = lzwState.currentSequenceLength;
+ var decodedLength = 0;
+ var currentBufferLength = this.bufferLength;
+ var buffer = this.ensureBuffer(this.bufferLength + estimatedDecodedSize);
+
+ for (i = 0; i < blockSize; i++) {
+ var code = this.readBits(codeLength);
+ var hasPrev = currentSequenceLength > 0;
+
+ if (code < 256) {
+ currentSequence[0] = code;
+ currentSequenceLength = 1;
+ } else if (code >= 258) {
+ if (code < nextCode) {
+ currentSequenceLength = dictionaryLengths[code];
+
+ for (j = currentSequenceLength - 1, q = code; j >= 0; j--) {
+ currentSequence[j] = dictionaryValues[q];
+ q = dictionaryPrevCodes[q];
+ }
+ } else {
+ currentSequence[currentSequenceLength++] = currentSequence[0];
+ }
+ } else if (code === 256) {
+ codeLength = 9;
+ nextCode = 258;
+ currentSequenceLength = 0;
+ continue;
+ } else {
+ this.eof = true;
+ delete this.lzwState;
+ break;
+ }
+
+ if (hasPrev) {
+ dictionaryPrevCodes[nextCode] = prevCode;
+ dictionaryLengths[nextCode] = dictionaryLengths[prevCode] + 1;
+ dictionaryValues[nextCode] = currentSequence[0];
+ nextCode++;
+ codeLength = nextCode + earlyChange & nextCode + earlyChange - 1 ? codeLength : Math.min(Math.log(nextCode + earlyChange) / 0.6931471805599453 + 1, 12) | 0;
+ }
+
+ prevCode = code;
+ decodedLength += currentSequenceLength;
+
+ if (estimatedDecodedSize < decodedLength) {
+ do {
+ estimatedDecodedSize += decodedSizeDelta;
+ } while (estimatedDecodedSize < decodedLength);
+
+ buffer = this.ensureBuffer(this.bufferLength + estimatedDecodedSize);
+ }
+
+ for (j = 0; j < currentSequenceLength; j++) {
+ buffer[currentBufferLength++] = currentSequence[j];
+ }
+ }
+
+ lzwState.nextCode = nextCode;
+ lzwState.codeLength = codeLength;
+ lzwState.prevCode = prevCode;
+ lzwState.currentSequenceLength = currentSequenceLength;
+ this.bufferLength = currentBufferLength;
+ };
+
+ return LZWStream;
+}();
+
+exports.LZWStream = LZWStream;
+
+var NullStream = function NullStreamClosure() {
+ function NullStream() {
+ Stream.call(this, new Uint8Array(0));
+ }
+
+ NullStream.prototype = Stream.prototype;
+ return NullStream;
+}();
+
+exports.NullStream = NullStream;
+
+/***/ }),
+/* 159 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.CCITTFaxStream = void 0;
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _ccitt = __w_pdfjs_require__(160);
+
+var _stream = __w_pdfjs_require__(158);
+
+var CCITTFaxStream = function CCITTFaxStreamClosure() {
+ function CCITTFaxStream(str, maybeLength, params) {
+ this.str = str;
+ this.dict = str.dict;
+
+ if (!(0, _primitives.isDict)(params)) {
+ params = _primitives.Dict.empty;
+ }
+
+ var source = {
+ next: function next() {
+ return str.getByte();
+ }
+ };
+ this.ccittFaxDecoder = new _ccitt.CCITTFaxDecoder(source, {
+ K: params.get('K'),
+ EndOfLine: params.get('EndOfLine'),
+ EncodedByteAlign: params.get('EncodedByteAlign'),
+ Columns: params.get('Columns'),
+ Rows: params.get('Rows'),
+ EndOfBlock: params.get('EndOfBlock'),
+ BlackIs1: params.get('BlackIs1')
+ });
+
+ _stream.DecodeStream.call(this, maybeLength);
+ }
+
+ CCITTFaxStream.prototype = Object.create(_stream.DecodeStream.prototype);
+
+ CCITTFaxStream.prototype.readBlock = function () {
+ while (!this.eof) {
+ var c = this.ccittFaxDecoder.readNextChar();
+
+ if (c === -1) {
+ this.eof = true;
+ return;
+ }
+
+ this.ensureBuffer(this.bufferLength + 1);
+ this.buffer[this.bufferLength++] = c;
+ }
+ };
+
+ return CCITTFaxStream;
+}();
+
+exports.CCITTFaxStream = CCITTFaxStream;
+
+/***/ }),
+/* 160 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.CCITTFaxDecoder = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var CCITTFaxDecoder = function CCITTFaxDecoder() {
+ var ccittEOL = -2;
+ var ccittEOF = -1;
+ var twoDimPass = 0;
+ var twoDimHoriz = 1;
+ var twoDimVert0 = 2;
+ var twoDimVertR1 = 3;
+ var twoDimVertL1 = 4;
+ var twoDimVertR2 = 5;
+ var twoDimVertL2 = 6;
+ var twoDimVertR3 = 7;
+ var twoDimVertL3 = 8;
+ var twoDimTable = [[-1, -1], [-1, -1], [7, twoDimVertL3], [7, twoDimVertR3], [6, twoDimVertL2], [6, twoDimVertL2], [6, twoDimVertR2], [6, twoDimVertR2], [4, twoDimPass], [4, twoDimPass], [4, twoDimPass], [4, twoDimPass], [4, twoDimPass], [4, twoDimPass], [4, twoDimPass], [4, twoDimPass], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimHoriz], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertL1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [3, twoDimVertR1], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0], [1, twoDimVert0]];
+ var whiteTable1 = [[-1, -1], [12, ccittEOL], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [11, 1792], [11, 1792], [12, 1984], [12, 2048], [12, 2112], [12, 2176], [12, 2240], [12, 2304], [11, 1856], [11, 1856], [11, 1920], [11, 1920], [12, 2368], [12, 2432], [12, 2496], [12, 2560]];
+ var whiteTable2 = [[-1, -1], [-1, -1], [-1, -1], [-1, -1], [8, 29], [8, 29], [8, 30], [8, 30], [8, 45], [8, 45], [8, 46], [8, 46], [7, 22], [7, 22], [7, 22], [7, 22], [7, 23], [7, 23], [7, 23], [7, 23], [8, 47], [8, 47], [8, 48], [8, 48], [6, 13], [6, 13], [6, 13], [6, 13], [6, 13], [6, 13], [6, 13], [6, 13], [7, 20], [7, 20], [7, 20], [7, 20], [8, 33], [8, 33], [8, 34], [8, 34], [8, 35], [8, 35], [8, 36], [8, 36], [8, 37], [8, 37], [8, 38], [8, 38], [7, 19], [7, 19], [7, 19], [7, 19], [8, 31], [8, 31], [8, 32], [8, 32], [6, 1], [6, 1], [6, 1], [6, 1], [6, 1], [6, 1], [6, 1], [6, 1], [6, 12], [6, 12], [6, 12], [6, 12], [6, 12], [6, 12], [6, 12], [6, 12], [8, 53], [8, 53], [8, 54], [8, 54], [7, 26], [7, 26], [7, 26], [7, 26], [8, 39], [8, 39], [8, 40], [8, 40], [8, 41], [8, 41], [8, 42], [8, 42], [8, 43], [8, 43], [8, 44], [8, 44], [7, 21], [7, 21], [7, 21], [7, 21], [7, 28], [7, 28], [7, 28], [7, 28], [8, 61], [8, 61], [8, 62], [8, 62], [8, 63], [8, 63], [8, 0], [8, 0], [8, 320], [8, 320], [8, 384], [8, 384], [5, 10], [5, 10], [5, 10], [5, 10], [5, 10], [5, 10], [5, 10], [5, 10], [5, 10], [5, 10], [5, 10], [5, 10], [5, 10], [5, 10], [5, 10], [5, 10], [5, 11], [5, 11], [5, 11], [5, 11], [5, 11], [5, 11], [5, 11], [5, 11], [5, 11], [5, 11], [5, 11], [5, 11], [5, 11], [5, 11], [5, 11], [5, 11], [7, 27], [7, 27], [7, 27], [7, 27], [8, 59], [8, 59], [8, 60], [8, 60], [9, 1472], [9, 1536], [9, 1600], [9, 1728], [7, 18], [7, 18], [7, 18], [7, 18], [7, 24], [7, 24], [7, 24], [7, 24], [8, 49], [8, 49], [8, 50], [8, 50], [8, 51], [8, 51], [8, 52], [8, 52], [7, 25], [7, 25], [7, 25], [7, 25], [8, 55], [8, 55], [8, 56], [8, 56], [8, 57], [8, 57], [8, 58], [8, 58], [6, 192], [6, 192], [6, 192], [6, 192], [6, 192], [6, 192], [6, 192], [6, 192], [6, 1664], [6, 1664], [6, 1664], [6, 1664], [6, 1664], [6, 1664], [6, 1664], [6, 1664], [8, 448], [8, 448], [8, 512], [8, 512], [9, 704], [9, 768], [8, 640], [8, 640], [8, 576], [8, 576], [9, 832], [9, 896], [9, 960], [9, 1024], [9, 1088], [9, 1152], [9, 1216], [9, 1280], [9, 1344], [9, 1408], [7, 256], [7, 256], [7, 256], [7, 256], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [4, 3], [5, 128], [5, 128], [5, 128], [5, 128], [5, 128], [5, 128], [5, 128], [5, 128], [5, 128], [5, 128], [5, 128], [5, 128], [5, 128], [5, 128], [5, 128], [5, 128], [5, 8], [5, 8], [5, 8], [5, 8], [5, 8], [5, 8], [5, 8], [5, 8], [5, 8], [5, 8], [5, 8], [5, 8], [5, 8], [5, 8], [5, 8], [5, 8], [5, 9], [5, 9], [5, 9], [5, 9], [5, 9], [5, 9], [5, 9], [5, 9], [5, 9], [5, 9], [5, 9], [5, 9], [5, 9], [5, 9], [5, 9], [5, 9], [6, 16], [6, 16], [6, 16], [6, 16], [6, 16], [6, 16], [6, 16], [6, 16], [6, 17], [6, 17], [6, 17], [6, 17], [6, 17], [6, 17], [6, 17], [6, 17], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 4], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [4, 5], [6, 14], [6, 14], [6, 14], [6, 14], [6, 14], [6, 14], [6, 14], [6, 14], [6, 15], [6, 15], [6, 15], [6, 15], [6, 15], [6, 15], [6, 15], [6, 15], [5, 64], [5, 64], [5, 64], [5, 64], [5, 64], [5, 64], [5, 64], [5, 64], [5, 64], [5, 64], [5, 64], [5, 64], [5, 64], [5, 64], [5, 64], [5, 64], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 6], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7], [4, 7]];
+ var blackTable1 = [[-1, -1], [-1, -1], [12, ccittEOL], [12, ccittEOL], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [11, 1792], [11, 1792], [11, 1792], [11, 1792], [12, 1984], [12, 1984], [12, 2048], [12, 2048], [12, 2112], [12, 2112], [12, 2176], [12, 2176], [12, 2240], [12, 2240], [12, 2304], [12, 2304], [11, 1856], [11, 1856], [11, 1856], [11, 1856], [11, 1920], [11, 1920], [11, 1920], [11, 1920], [12, 2368], [12, 2368], [12, 2432], [12, 2432], [12, 2496], [12, 2496], [12, 2560], [12, 2560], [10, 18], [10, 18], [10, 18], [10, 18], [10, 18], [10, 18], [10, 18], [10, 18], [12, 52], [12, 52], [13, 640], [13, 704], [13, 768], [13, 832], [12, 55], [12, 55], [12, 56], [12, 56], [13, 1280], [13, 1344], [13, 1408], [13, 1472], [12, 59], [12, 59], [12, 60], [12, 60], [13, 1536], [13, 1600], [11, 24], [11, 24], [11, 24], [11, 24], [11, 25], [11, 25], [11, 25], [11, 25], [13, 1664], [13, 1728], [12, 320], [12, 320], [12, 384], [12, 384], [12, 448], [12, 448], [13, 512], [13, 576], [12, 53], [12, 53], [12, 54], [12, 54], [13, 896], [13, 960], [13, 1024], [13, 1088], [13, 1152], [13, 1216], [10, 64], [10, 64], [10, 64], [10, 64], [10, 64], [10, 64], [10, 64], [10, 64]];
+ var blackTable2 = [[8, 13], [8, 13], [8, 13], [8, 13], [8, 13], [8, 13], [8, 13], [8, 13], [8, 13], [8, 13], [8, 13], [8, 13], [8, 13], [8, 13], [8, 13], [8, 13], [11, 23], [11, 23], [12, 50], [12, 51], [12, 44], [12, 45], [12, 46], [12, 47], [12, 57], [12, 58], [12, 61], [12, 256], [10, 16], [10, 16], [10, 16], [10, 16], [10, 17], [10, 17], [10, 17], [10, 17], [12, 48], [12, 49], [12, 62], [12, 63], [12, 30], [12, 31], [12, 32], [12, 33], [12, 40], [12, 41], [11, 22], [11, 22], [8, 14], [8, 14], [8, 14], [8, 14], [8, 14], [8, 14], [8, 14], [8, 14], [8, 14], [8, 14], [8, 14], [8, 14], [8, 14], [8, 14], [8, 14], [8, 14], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 10], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [7, 11], [9, 15], [9, 15], [9, 15], [9, 15], [9, 15], [9, 15], [9, 15], [9, 15], [12, 128], [12, 192], [12, 26], [12, 27], [12, 28], [12, 29], [11, 19], [11, 19], [11, 20], [11, 20], [12, 34], [12, 35], [12, 36], [12, 37], [12, 38], [12, 39], [11, 21], [11, 21], [12, 42], [12, 43], [10, 0], [10, 0], [10, 0], [10, 0], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12], [7, 12]];
+ var blackTable3 = [[-1, -1], [-1, -1], [-1, -1], [-1, -1], [6, 9], [6, 8], [5, 7], [5, 7], [4, 6], [4, 6], [4, 6], [4, 6], [4, 5], [4, 5], [4, 5], [4, 5], [3, 1], [3, 1], [3, 1], [3, 1], [3, 1], [3, 1], [3, 1], [3, 1], [3, 4], [3, 4], [3, 4], [3, 4], [3, 4], [3, 4], [3, 4], [3, 4], [2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2], [2, 2]];
+
+ function CCITTFaxDecoder(source) {
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+ if (!source || typeof source.next !== 'function') {
+ throw new Error('CCITTFaxDecoder - invalid "source" parameter.');
+ }
+
+ this.source = source;
+ this.eof = false;
+ this.encoding = options['K'] || 0;
+ this.eoline = options['EndOfLine'] || false;
+ this.byteAlign = options['EncodedByteAlign'] || false;
+ this.columns = options['Columns'] || 1728;
+ this.rows = options['Rows'] || 0;
+ var eoblock = options['EndOfBlock'];
+
+ if (eoblock === null || eoblock === undefined) {
+ eoblock = true;
+ }
+
+ this.eoblock = eoblock;
+ this.black = options['BlackIs1'] || false;
+ this.codingLine = new Uint32Array(this.columns + 1);
+ this.refLine = new Uint32Array(this.columns + 2);
+ this.codingLine[0] = this.columns;
+ this.codingPos = 0;
+ this.row = 0;
+ this.nextLine2D = this.encoding < 0;
+ this.inputBits = 0;
+ this.inputBuf = 0;
+ this.outputBits = 0;
+ this.rowsDone = false;
+ var code1;
+
+ while ((code1 = this._lookBits(12)) === 0) {
+ this._eatBits(1);
+ }
+
+ if (code1 === 1) {
+ this._eatBits(12);
+ }
+
+ if (this.encoding > 0) {
+ this.nextLine2D = !this._lookBits(1);
+
+ this._eatBits(1);
+ }
+ }
+
+ CCITTFaxDecoder.prototype = {
+ readNextChar: function readNextChar() {
+ if (this.eof) {
+ return -1;
+ }
+
+ var refLine = this.refLine;
+ var codingLine = this.codingLine;
+ var columns = this.columns;
+ var refPos, blackPixels, bits, i;
+
+ if (this.outputBits === 0) {
+ if (this.rowsDone) {
+ this.eof = true;
+ }
+
+ if (this.eof) {
+ return -1;
+ }
+
+ this.err = false;
+ var code1, code2, code3;
+
+ if (this.nextLine2D) {
+ for (i = 0; codingLine[i] < columns; ++i) {
+ refLine[i] = codingLine[i];
+ }
+
+ refLine[i++] = columns;
+ refLine[i] = columns;
+ codingLine[0] = 0;
+ this.codingPos = 0;
+ refPos = 0;
+ blackPixels = 0;
+
+ while (codingLine[this.codingPos] < columns) {
+ code1 = this._getTwoDimCode();
+
+ switch (code1) {
+ case twoDimPass:
+ this._addPixels(refLine[refPos + 1], blackPixels);
+
+ if (refLine[refPos + 1] < columns) {
+ refPos += 2;
+ }
+
+ break;
+
+ case twoDimHoriz:
+ code1 = code2 = 0;
+
+ if (blackPixels) {
+ do {
+ code1 += code3 = this._getBlackCode();
+ } while (code3 >= 64);
+
+ do {
+ code2 += code3 = this._getWhiteCode();
+ } while (code3 >= 64);
+ } else {
+ do {
+ code1 += code3 = this._getWhiteCode();
+ } while (code3 >= 64);
+
+ do {
+ code2 += code3 = this._getBlackCode();
+ } while (code3 >= 64);
+ }
+
+ this._addPixels(codingLine[this.codingPos] + code1, blackPixels);
+
+ if (codingLine[this.codingPos] < columns) {
+ this._addPixels(codingLine[this.codingPos] + code2, blackPixels ^ 1);
+ }
+
+ while (refLine[refPos] <= codingLine[this.codingPos] && refLine[refPos] < columns) {
+ refPos += 2;
+ }
+
+ break;
+
+ case twoDimVertR3:
+ this._addPixels(refLine[refPos] + 3, blackPixels);
+
+ blackPixels ^= 1;
+
+ if (codingLine[this.codingPos] < columns) {
+ ++refPos;
+
+ while (refLine[refPos] <= codingLine[this.codingPos] && refLine[refPos] < columns) {
+ refPos += 2;
+ }
+ }
+
+ break;
+
+ case twoDimVertR2:
+ this._addPixels(refLine[refPos] + 2, blackPixels);
+
+ blackPixels ^= 1;
+
+ if (codingLine[this.codingPos] < columns) {
+ ++refPos;
+
+ while (refLine[refPos] <= codingLine[this.codingPos] && refLine[refPos] < columns) {
+ refPos += 2;
+ }
+ }
+
+ break;
+
+ case twoDimVertR1:
+ this._addPixels(refLine[refPos] + 1, blackPixels);
+
+ blackPixels ^= 1;
+
+ if (codingLine[this.codingPos] < columns) {
+ ++refPos;
+
+ while (refLine[refPos] <= codingLine[this.codingPos] && refLine[refPos] < columns) {
+ refPos += 2;
+ }
+ }
+
+ break;
+
+ case twoDimVert0:
+ this._addPixels(refLine[refPos], blackPixels);
+
+ blackPixels ^= 1;
+
+ if (codingLine[this.codingPos] < columns) {
+ ++refPos;
+
+ while (refLine[refPos] <= codingLine[this.codingPos] && refLine[refPos] < columns) {
+ refPos += 2;
+ }
+ }
+
+ break;
+
+ case twoDimVertL3:
+ this._addPixelsNeg(refLine[refPos] - 3, blackPixels);
+
+ blackPixels ^= 1;
+
+ if (codingLine[this.codingPos] < columns) {
+ if (refPos > 0) {
+ --refPos;
+ } else {
+ ++refPos;
+ }
+
+ while (refLine[refPos] <= codingLine[this.codingPos] && refLine[refPos] < columns) {
+ refPos += 2;
+ }
+ }
+
+ break;
+
+ case twoDimVertL2:
+ this._addPixelsNeg(refLine[refPos] - 2, blackPixels);
+
+ blackPixels ^= 1;
+
+ if (codingLine[this.codingPos] < columns) {
+ if (refPos > 0) {
+ --refPos;
+ } else {
+ ++refPos;
+ }
+
+ while (refLine[refPos] <= codingLine[this.codingPos] && refLine[refPos] < columns) {
+ refPos += 2;
+ }
+ }
+
+ break;
+
+ case twoDimVertL1:
+ this._addPixelsNeg(refLine[refPos] - 1, blackPixels);
+
+ blackPixels ^= 1;
+
+ if (codingLine[this.codingPos] < columns) {
+ if (refPos > 0) {
+ --refPos;
+ } else {
+ ++refPos;
+ }
+
+ while (refLine[refPos] <= codingLine[this.codingPos] && refLine[refPos] < columns) {
+ refPos += 2;
+ }
+ }
+
+ break;
+
+ case ccittEOF:
+ this._addPixels(columns, 0);
+
+ this.eof = true;
+ break;
+
+ default:
+ (0, _util.info)('bad 2d code');
+
+ this._addPixels(columns, 0);
+
+ this.err = true;
+ }
+ }
+ } else {
+ codingLine[0] = 0;
+ this.codingPos = 0;
+ blackPixels = 0;
+
+ while (codingLine[this.codingPos] < columns) {
+ code1 = 0;
+
+ if (blackPixels) {
+ do {
+ code1 += code3 = this._getBlackCode();
+ } while (code3 >= 64);
+ } else {
+ do {
+ code1 += code3 = this._getWhiteCode();
+ } while (code3 >= 64);
+ }
+
+ this._addPixels(codingLine[this.codingPos] + code1, blackPixels);
+
+ blackPixels ^= 1;
+ }
+ }
+
+ var gotEOL = false;
+
+ if (this.byteAlign) {
+ this.inputBits &= ~7;
+ }
+
+ if (!this.eoblock && this.row === this.rows - 1) {
+ this.rowsDone = true;
+ } else {
+ code1 = this._lookBits(12);
+
+ if (this.eoline) {
+ while (code1 !== ccittEOF && code1 !== 1) {
+ this._eatBits(1);
+
+ code1 = this._lookBits(12);
+ }
+ } else {
+ while (code1 === 0) {
+ this._eatBits(1);
+
+ code1 = this._lookBits(12);
+ }
+ }
+
+ if (code1 === 1) {
+ this._eatBits(12);
+
+ gotEOL = true;
+ } else if (code1 === ccittEOF) {
+ this.eof = true;
+ }
+ }
+
+ if (!this.eof && this.encoding > 0 && !this.rowsDone) {
+ this.nextLine2D = !this._lookBits(1);
+
+ this._eatBits(1);
+ }
+
+ if (this.eoblock && gotEOL && this.byteAlign) {
+ code1 = this._lookBits(12);
+
+ if (code1 === 1) {
+ this._eatBits(12);
+
+ if (this.encoding > 0) {
+ this._lookBits(1);
+
+ this._eatBits(1);
+ }
+
+ if (this.encoding >= 0) {
+ for (i = 0; i < 4; ++i) {
+ code1 = this._lookBits(12);
+
+ if (code1 !== 1) {
+ (0, _util.info)('bad rtc code: ' + code1);
+ }
+
+ this._eatBits(12);
+
+ if (this.encoding > 0) {
+ this._lookBits(1);
+
+ this._eatBits(1);
+ }
+ }
+ }
+
+ this.eof = true;
+ }
+ } else if (this.err && this.eoline) {
+ while (true) {
+ code1 = this._lookBits(13);
+
+ if (code1 === ccittEOF) {
+ this.eof = true;
+ return -1;
+ }
+
+ if (code1 >> 1 === 1) {
+ break;
+ }
+
+ this._eatBits(1);
+ }
+
+ this._eatBits(12);
+
+ if (this.encoding > 0) {
+ this._eatBits(1);
+
+ this.nextLine2D = !(code1 & 1);
+ }
+ }
+
+ if (codingLine[0] > 0) {
+ this.outputBits = codingLine[this.codingPos = 0];
+ } else {
+ this.outputBits = codingLine[this.codingPos = 1];
+ }
+
+ this.row++;
+ }
+
+ var c;
+
+ if (this.outputBits >= 8) {
+ c = this.codingPos & 1 ? 0 : 0xFF;
+ this.outputBits -= 8;
+
+ if (this.outputBits === 0 && codingLine[this.codingPos] < columns) {
+ this.codingPos++;
+ this.outputBits = codingLine[this.codingPos] - codingLine[this.codingPos - 1];
+ }
+ } else {
+ bits = 8;
+ c = 0;
+
+ do {
+ if (this.outputBits > bits) {
+ c <<= bits;
+
+ if (!(this.codingPos & 1)) {
+ c |= 0xFF >> 8 - bits;
+ }
+
+ this.outputBits -= bits;
+ bits = 0;
+ } else {
+ c <<= this.outputBits;
+
+ if (!(this.codingPos & 1)) {
+ c |= 0xFF >> 8 - this.outputBits;
+ }
+
+ bits -= this.outputBits;
+ this.outputBits = 0;
+
+ if (codingLine[this.codingPos] < columns) {
+ this.codingPos++;
+ this.outputBits = codingLine[this.codingPos] - codingLine[this.codingPos - 1];
+ } else if (bits > 0) {
+ c <<= bits;
+ bits = 0;
+ }
+ }
+ } while (bits);
+ }
+
+ if (this.black) {
+ c ^= 0xFF;
+ }
+
+ return c;
+ },
+ _addPixels: function _addPixels(a1, blackPixels) {
+ var codingLine = this.codingLine;
+ var codingPos = this.codingPos;
+
+ if (a1 > codingLine[codingPos]) {
+ if (a1 > this.columns) {
+ (0, _util.info)('row is wrong length');
+ this.err = true;
+ a1 = this.columns;
+ }
+
+ if (codingPos & 1 ^ blackPixels) {
+ ++codingPos;
+ }
+
+ codingLine[codingPos] = a1;
+ }
+
+ this.codingPos = codingPos;
+ },
+ _addPixelsNeg: function _addPixelsNeg(a1, blackPixels) {
+ var codingLine = this.codingLine;
+ var codingPos = this.codingPos;
+
+ if (a1 > codingLine[codingPos]) {
+ if (a1 > this.columns) {
+ (0, _util.info)('row is wrong length');
+ this.err = true;
+ a1 = this.columns;
+ }
+
+ if (codingPos & 1 ^ blackPixels) {
+ ++codingPos;
+ }
+
+ codingLine[codingPos] = a1;
+ } else if (a1 < codingLine[codingPos]) {
+ if (a1 < 0) {
+ (0, _util.info)('invalid code');
+ this.err = true;
+ a1 = 0;
+ }
+
+ while (codingPos > 0 && a1 < codingLine[codingPos - 1]) {
+ --codingPos;
+ }
+
+ codingLine[codingPos] = a1;
+ }
+
+ this.codingPos = codingPos;
+ },
+ _findTableCode: function _findTableCode(start, end, table, limit) {
+ var limitValue = limit || 0;
+
+ for (var i = start; i <= end; ++i) {
+ var code = this._lookBits(i);
+
+ if (code === ccittEOF) {
+ return [true, 1, false];
+ }
+
+ if (i < end) {
+ code <<= end - i;
+ }
+
+ if (!limitValue || code >= limitValue) {
+ var p = table[code - limitValue];
+
+ if (p[0] === i) {
+ this._eatBits(i);
+
+ return [true, p[1], true];
+ }
+ }
+ }
+
+ return [false, 0, false];
+ },
+ _getTwoDimCode: function _getTwoDimCode() {
+ var code = 0;
+ var p;
+
+ if (this.eoblock) {
+ code = this._lookBits(7);
+ p = twoDimTable[code];
+
+ if (p && p[0] > 0) {
+ this._eatBits(p[0]);
+
+ return p[1];
+ }
+ } else {
+ var result = this._findTableCode(1, 7, twoDimTable);
+
+ if (result[0] && result[2]) {
+ return result[1];
+ }
+ }
+
+ (0, _util.info)('Bad two dim code');
+ return ccittEOF;
+ },
+ _getWhiteCode: function _getWhiteCode() {
+ var code = 0;
+ var p;
+
+ if (this.eoblock) {
+ code = this._lookBits(12);
+
+ if (code === ccittEOF) {
+ return 1;
+ }
+
+ if (code >> 5 === 0) {
+ p = whiteTable1[code];
+ } else {
+ p = whiteTable2[code >> 3];
+ }
+
+ if (p[0] > 0) {
+ this._eatBits(p[0]);
+
+ return p[1];
+ }
+ } else {
+ var result = this._findTableCode(1, 9, whiteTable2);
+
+ if (result[0]) {
+ return result[1];
+ }
+
+ result = this._findTableCode(11, 12, whiteTable1);
+
+ if (result[0]) {
+ return result[1];
+ }
+ }
+
+ (0, _util.info)('bad white code');
+
+ this._eatBits(1);
+
+ return 1;
+ },
+ _getBlackCode: function _getBlackCode() {
+ var code, p;
+
+ if (this.eoblock) {
+ code = this._lookBits(13);
+
+ if (code === ccittEOF) {
+ return 1;
+ }
+
+ if (code >> 7 === 0) {
+ p = blackTable1[code];
+ } else if (code >> 9 === 0 && code >> 7 !== 0) {
+ p = blackTable2[(code >> 1) - 64];
+ } else {
+ p = blackTable3[code >> 7];
+ }
+
+ if (p[0] > 0) {
+ this._eatBits(p[0]);
+
+ return p[1];
+ }
+ } else {
+ var result = this._findTableCode(2, 6, blackTable3);
+
+ if (result[0]) {
+ return result[1];
+ }
+
+ result = this._findTableCode(7, 12, blackTable2, 64);
+
+ if (result[0]) {
+ return result[1];
+ }
+
+ result = this._findTableCode(10, 13, blackTable1);
+
+ if (result[0]) {
+ return result[1];
+ }
+ }
+
+ (0, _util.info)('bad black code');
+
+ this._eatBits(1);
+
+ return 1;
+ },
+ _lookBits: function _lookBits(n) {
+ var c;
+
+ while (this.inputBits < n) {
+ if ((c = this.source.next()) === -1) {
+ if (this.inputBits === 0) {
+ return ccittEOF;
+ }
+
+ return this.inputBuf << n - this.inputBits & 0xFFFF >> 16 - n;
+ }
+
+ this.inputBuf = this.inputBuf << 8 | c;
+ this.inputBits += 8;
+ }
+
+ return this.inputBuf >> this.inputBits - n & 0xFFFF >> 16 - n;
+ },
+ _eatBits: function _eatBits(n) {
+ if ((this.inputBits -= n) < 0) {
+ this.inputBits = 0;
+ }
+ }
+ };
+ return CCITTFaxDecoder;
+}();
+
+exports.CCITTFaxDecoder = CCITTFaxDecoder;
+
+/***/ }),
+/* 161 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.Jbig2Stream = void 0;
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _stream = __w_pdfjs_require__(158);
+
+var _jbig = __w_pdfjs_require__(162);
+
+var _util = __w_pdfjs_require__(5);
+
+var Jbig2Stream = function Jbig2StreamClosure() {
+ function Jbig2Stream(stream, maybeLength, dict, params) {
+ this.stream = stream;
+ this.maybeLength = maybeLength;
+ this.dict = dict;
+ this.params = params;
+
+ _stream.DecodeStream.call(this, maybeLength);
+ }
+
+ Jbig2Stream.prototype = Object.create(_stream.DecodeStream.prototype);
+ Object.defineProperty(Jbig2Stream.prototype, 'bytes', {
+ get: function get() {
+ return (0, _util.shadow)(this, 'bytes', this.stream.getBytes(this.maybeLength));
+ },
+ configurable: true
+ });
+
+ Jbig2Stream.prototype.ensureBuffer = function (requested) {};
+
+ Jbig2Stream.prototype.readBlock = function () {
+ if (this.eof) {
+ return;
+ }
+
+ var jbig2Image = new _jbig.Jbig2Image();
+ var chunks = [];
+
+ if ((0, _primitives.isDict)(this.params)) {
+ var globalsStream = this.params.get('JBIG2Globals');
+
+ if ((0, _primitives.isStream)(globalsStream)) {
+ var globals = globalsStream.getBytes();
+ chunks.push({
+ data: globals,
+ start: 0,
+ end: globals.length
+ });
+ }
+ }
+
+ chunks.push({
+ data: this.bytes,
+ start: 0,
+ end: this.bytes.length
+ });
+ var data = jbig2Image.parseChunks(chunks);
+ var dataLength = data.length;
+
+ for (var i = 0; i < dataLength; i++) {
+ data[i] ^= 0xFF;
+ }
+
+ this.buffer = data;
+ this.bufferLength = dataLength;
+ this.eof = true;
+ };
+
+ return Jbig2Stream;
+}();
+
+exports.Jbig2Stream = Jbig2Stream;
+
+/***/ }),
+/* 162 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.Jbig2Image = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _arithmetic_decoder = __w_pdfjs_require__(163);
+
+var _ccitt = __w_pdfjs_require__(160);
+
+var Jbig2Error = function Jbig2ErrorClosure() {
+ function Jbig2Error(msg) {
+ this.message = 'JBIG2 error: ' + msg;
+ }
+
+ Jbig2Error.prototype = new Error();
+ Jbig2Error.prototype.name = 'Jbig2Error';
+ Jbig2Error.constructor = Jbig2Error;
+ return Jbig2Error;
+}();
+
+var Jbig2Image = function Jbig2ImageClosure() {
+ function ContextCache() {}
+
+ ContextCache.prototype = {
+ getContexts: function getContexts(id) {
+ if (id in this) {
+ return this[id];
+ }
+
+ return this[id] = new Int8Array(1 << 16);
+ }
+ };
+
+ function DecodingContext(data, start, end) {
+ this.data = data;
+ this.start = start;
+ this.end = end;
+ }
+
+ DecodingContext.prototype = {
+ get decoder() {
+ var decoder = new _arithmetic_decoder.ArithmeticDecoder(this.data, this.start, this.end);
+ return (0, _util.shadow)(this, 'decoder', decoder);
+ },
+
+ get contextCache() {
+ var cache = new ContextCache();
+ return (0, _util.shadow)(this, 'contextCache', cache);
+ }
+
+ };
+
+ function decodeInteger(contextCache, procedure, decoder) {
+ var contexts = contextCache.getContexts(procedure);
+ var prev = 1;
+
+ function readBits(length) {
+ var v = 0;
+
+ for (var i = 0; i < length; i++) {
+ var bit = decoder.readBit(contexts, prev);
+ prev = prev < 256 ? prev << 1 | bit : (prev << 1 | bit) & 511 | 256;
+ v = v << 1 | bit;
+ }
+
+ return v >>> 0;
+ }
+
+ var sign = readBits(1);
+ var value = readBits(1) ? readBits(1) ? readBits(1) ? readBits(1) ? readBits(1) ? readBits(32) + 4436 : readBits(12) + 340 : readBits(8) + 84 : readBits(6) + 20 : readBits(4) + 4 : readBits(2);
+ return sign === 0 ? value : value > 0 ? -value : null;
+ }
+
+ function decodeIAID(contextCache, decoder, codeLength) {
+ var contexts = contextCache.getContexts('IAID');
+ var prev = 1;
+
+ for (var i = 0; i < codeLength; i++) {
+ var bit = decoder.readBit(contexts, prev);
+ prev = prev << 1 | bit;
+ }
+
+ if (codeLength < 31) {
+ return prev & (1 << codeLength) - 1;
+ }
+
+ return prev & 0x7FFFFFFF;
+ }
+
+ var SegmentTypes = ['SymbolDictionary', null, null, null, 'IntermediateTextRegion', null, 'ImmediateTextRegion', 'ImmediateLosslessTextRegion', null, null, null, null, null, null, null, null, 'PatternDictionary', null, null, null, 'IntermediateHalftoneRegion', null, 'ImmediateHalftoneRegion', 'ImmediateLosslessHalftoneRegion', null, null, null, null, null, null, null, null, null, null, null, null, 'IntermediateGenericRegion', null, 'ImmediateGenericRegion', 'ImmediateLosslessGenericRegion', 'IntermediateGenericRefinementRegion', null, 'ImmediateGenericRefinementRegion', 'ImmediateLosslessGenericRefinementRegion', null, null, null, null, 'PageInformation', 'EndOfPage', 'EndOfStripe', 'EndOfFile', 'Profiles', 'Tables', null, null, null, null, null, null, null, null, 'Extension'];
+ var CodingTemplates = [[{
+ x: -1,
+ y: -2
+ }, {
+ x: 0,
+ y: -2
+ }, {
+ x: 1,
+ y: -2
+ }, {
+ x: -2,
+ y: -1
+ }, {
+ x: -1,
+ y: -1
+ }, {
+ x: 0,
+ y: -1
+ }, {
+ x: 1,
+ y: -1
+ }, {
+ x: 2,
+ y: -1
+ }, {
+ x: -4,
+ y: 0
+ }, {
+ x: -3,
+ y: 0
+ }, {
+ x: -2,
+ y: 0
+ }, {
+ x: -1,
+ y: 0
+ }], [{
+ x: -1,
+ y: -2
+ }, {
+ x: 0,
+ y: -2
+ }, {
+ x: 1,
+ y: -2
+ }, {
+ x: 2,
+ y: -2
+ }, {
+ x: -2,
+ y: -1
+ }, {
+ x: -1,
+ y: -1
+ }, {
+ x: 0,
+ y: -1
+ }, {
+ x: 1,
+ y: -1
+ }, {
+ x: 2,
+ y: -1
+ }, {
+ x: -3,
+ y: 0
+ }, {
+ x: -2,
+ y: 0
+ }, {
+ x: -1,
+ y: 0
+ }], [{
+ x: -1,
+ y: -2
+ }, {
+ x: 0,
+ y: -2
+ }, {
+ x: 1,
+ y: -2
+ }, {
+ x: -2,
+ y: -1
+ }, {
+ x: -1,
+ y: -1
+ }, {
+ x: 0,
+ y: -1
+ }, {
+ x: 1,
+ y: -1
+ }, {
+ x: -2,
+ y: 0
+ }, {
+ x: -1,
+ y: 0
+ }], [{
+ x: -3,
+ y: -1
+ }, {
+ x: -2,
+ y: -1
+ }, {
+ x: -1,
+ y: -1
+ }, {
+ x: 0,
+ y: -1
+ }, {
+ x: 1,
+ y: -1
+ }, {
+ x: -4,
+ y: 0
+ }, {
+ x: -3,
+ y: 0
+ }, {
+ x: -2,
+ y: 0
+ }, {
+ x: -1,
+ y: 0
+ }]];
+ var RefinementTemplates = [{
+ coding: [{
+ x: 0,
+ y: -1
+ }, {
+ x: 1,
+ y: -1
+ }, {
+ x: -1,
+ y: 0
+ }],
+ reference: [{
+ x: 0,
+ y: -1
+ }, {
+ x: 1,
+ y: -1
+ }, {
+ x: -1,
+ y: 0
+ }, {
+ x: 0,
+ y: 0
+ }, {
+ x: 1,
+ y: 0
+ }, {
+ x: -1,
+ y: 1
+ }, {
+ x: 0,
+ y: 1
+ }, {
+ x: 1,
+ y: 1
+ }]
+ }, {
+ coding: [{
+ x: -1,
+ y: -1
+ }, {
+ x: 0,
+ y: -1
+ }, {
+ x: 1,
+ y: -1
+ }, {
+ x: -1,
+ y: 0
+ }],
+ reference: [{
+ x: 0,
+ y: -1
+ }, {
+ x: -1,
+ y: 0
+ }, {
+ x: 0,
+ y: 0
+ }, {
+ x: 1,
+ y: 0
+ }, {
+ x: 0,
+ y: 1
+ }, {
+ x: 1,
+ y: 1
+ }]
+ }];
+ var ReusedContexts = [0x9B25, 0x0795, 0x00E5, 0x0195];
+ var RefinementReusedContexts = [0x0020, 0x0008];
+
+ function decodeBitmapTemplate0(width, height, decodingContext) {
+ var decoder = decodingContext.decoder;
+ var contexts = decodingContext.contextCache.getContexts('GB');
+ var contextLabel,
+ i,
+ j,
+ pixel,
+ row,
+ row1,
+ row2,
+ bitmap = [];
+ var OLD_PIXEL_MASK = 0x7BF7;
+
+ for (i = 0; i < height; i++) {
+ row = bitmap[i] = new Uint8Array(width);
+ row1 = i < 1 ? row : bitmap[i - 1];
+ row2 = i < 2 ? row : bitmap[i - 2];
+ contextLabel = row2[0] << 13 | row2[1] << 12 | row2[2] << 11 | row1[0] << 7 | row1[1] << 6 | row1[2] << 5 | row1[3] << 4;
+
+ for (j = 0; j < width; j++) {
+ row[j] = pixel = decoder.readBit(contexts, contextLabel);
+ contextLabel = (contextLabel & OLD_PIXEL_MASK) << 1 | (j + 3 < width ? row2[j + 3] << 11 : 0) | (j + 4 < width ? row1[j + 4] << 4 : 0) | pixel;
+ }
+ }
+
+ return bitmap;
+ }
+
+ function decodeBitmap(mmr, width, height, templateIndex, prediction, skip, at, decodingContext) {
+ if (mmr) {
+ var input = new Reader(decodingContext.data, decodingContext.start, decodingContext.end);
+ return decodeMMRBitmap(input, width, height, false);
+ }
+
+ if (templateIndex === 0 && !skip && !prediction && at.length === 4 && at[0].x === 3 && at[0].y === -1 && at[1].x === -3 && at[1].y === -1 && at[2].x === 2 && at[2].y === -2 && at[3].x === -2 && at[3].y === -2) {
+ return decodeBitmapTemplate0(width, height, decodingContext);
+ }
+
+ var useskip = !!skip;
+ var template = CodingTemplates[templateIndex].concat(at);
+ template.sort(function (a, b) {
+ return a.y - b.y || a.x - b.x;
+ });
+ var templateLength = template.length;
+ var templateX = new Int8Array(templateLength);
+ var templateY = new Int8Array(templateLength);
+ var changingTemplateEntries = [];
+ var reuseMask = 0,
+ minX = 0,
+ maxX = 0,
+ minY = 0;
+ var c, k;
+
+ for (k = 0; k < templateLength; k++) {
+ templateX[k] = template[k].x;
+ templateY[k] = template[k].y;
+ minX = Math.min(minX, template[k].x);
+ maxX = Math.max(maxX, template[k].x);
+ minY = Math.min(minY, template[k].y);
+
+ if (k < templateLength - 1 && template[k].y === template[k + 1].y && template[k].x === template[k + 1].x - 1) {
+ reuseMask |= 1 << templateLength - 1 - k;
+ } else {
+ changingTemplateEntries.push(k);
+ }
+ }
+
+ var changingEntriesLength = changingTemplateEntries.length;
+ var changingTemplateX = new Int8Array(changingEntriesLength);
+ var changingTemplateY = new Int8Array(changingEntriesLength);
+ var changingTemplateBit = new Uint16Array(changingEntriesLength);
+
+ for (c = 0; c < changingEntriesLength; c++) {
+ k = changingTemplateEntries[c];
+ changingTemplateX[c] = template[k].x;
+ changingTemplateY[c] = template[k].y;
+ changingTemplateBit[c] = 1 << templateLength - 1 - k;
+ }
+
+ var sbb_left = -minX;
+ var sbb_top = -minY;
+ var sbb_right = width - maxX;
+ var pseudoPixelContext = ReusedContexts[templateIndex];
+ var row = new Uint8Array(width);
+ var bitmap = [];
+ var decoder = decodingContext.decoder;
+ var contexts = decodingContext.contextCache.getContexts('GB');
+ var ltp = 0,
+ j,
+ i0,
+ j0,
+ contextLabel = 0,
+ bit,
+ shift;
+
+ for (var i = 0; i < height; i++) {
+ if (prediction) {
+ var sltp = decoder.readBit(contexts, pseudoPixelContext);
+ ltp ^= sltp;
+
+ if (ltp) {
+ bitmap.push(row);
+ continue;
+ }
+ }
+
+ row = new Uint8Array(row);
+ bitmap.push(row);
+
+ for (j = 0; j < width; j++) {
+ if (useskip && skip[i][j]) {
+ row[j] = 0;
+ continue;
+ }
+
+ if (j >= sbb_left && j < sbb_right && i >= sbb_top) {
+ contextLabel = contextLabel << 1 & reuseMask;
+
+ for (k = 0; k < changingEntriesLength; k++) {
+ i0 = i + changingTemplateY[k];
+ j0 = j + changingTemplateX[k];
+ bit = bitmap[i0][j0];
+
+ if (bit) {
+ bit = changingTemplateBit[k];
+ contextLabel |= bit;
+ }
+ }
+ } else {
+ contextLabel = 0;
+ shift = templateLength - 1;
+
+ for (k = 0; k < templateLength; k++, shift--) {
+ j0 = j + templateX[k];
+
+ if (j0 >= 0 && j0 < width) {
+ i0 = i + templateY[k];
+
+ if (i0 >= 0) {
+ bit = bitmap[i0][j0];
+
+ if (bit) {
+ contextLabel |= bit << shift;
+ }
+ }
+ }
+ }
+ }
+
+ var pixel = decoder.readBit(contexts, contextLabel);
+ row[j] = pixel;
+ }
+ }
+
+ return bitmap;
+ }
+
+ function decodeRefinement(width, height, templateIndex, referenceBitmap, offsetX, offsetY, prediction, at, decodingContext) {
+ var codingTemplate = RefinementTemplates[templateIndex].coding;
+
+ if (templateIndex === 0) {
+ codingTemplate = codingTemplate.concat([at[0]]);
+ }
+
+ var codingTemplateLength = codingTemplate.length;
+ var codingTemplateX = new Int32Array(codingTemplateLength);
+ var codingTemplateY = new Int32Array(codingTemplateLength);
+ var k;
+
+ for (k = 0; k < codingTemplateLength; k++) {
+ codingTemplateX[k] = codingTemplate[k].x;
+ codingTemplateY[k] = codingTemplate[k].y;
+ }
+
+ var referenceTemplate = RefinementTemplates[templateIndex].reference;
+
+ if (templateIndex === 0) {
+ referenceTemplate = referenceTemplate.concat([at[1]]);
+ }
+
+ var referenceTemplateLength = referenceTemplate.length;
+ var referenceTemplateX = new Int32Array(referenceTemplateLength);
+ var referenceTemplateY = new Int32Array(referenceTemplateLength);
+
+ for (k = 0; k < referenceTemplateLength; k++) {
+ referenceTemplateX[k] = referenceTemplate[k].x;
+ referenceTemplateY[k] = referenceTemplate[k].y;
+ }
+
+ var referenceWidth = referenceBitmap[0].length;
+ var referenceHeight = referenceBitmap.length;
+ var pseudoPixelContext = RefinementReusedContexts[templateIndex];
+ var bitmap = [];
+ var decoder = decodingContext.decoder;
+ var contexts = decodingContext.contextCache.getContexts('GR');
+ var ltp = 0;
+
+ for (var i = 0; i < height; i++) {
+ if (prediction) {
+ var sltp = decoder.readBit(contexts, pseudoPixelContext);
+ ltp ^= sltp;
+
+ if (ltp) {
+ throw new Jbig2Error('prediction is not supported');
+ }
+ }
+
+ var row = new Uint8Array(width);
+ bitmap.push(row);
+
+ for (var j = 0; j < width; j++) {
+ var i0, j0;
+ var contextLabel = 0;
+
+ for (k = 0; k < codingTemplateLength; k++) {
+ i0 = i + codingTemplateY[k];
+ j0 = j + codingTemplateX[k];
+
+ if (i0 < 0 || j0 < 0 || j0 >= width) {
+ contextLabel <<= 1;
+ } else {
+ contextLabel = contextLabel << 1 | bitmap[i0][j0];
+ }
+ }
+
+ for (k = 0; k < referenceTemplateLength; k++) {
+ i0 = i + referenceTemplateY[k] - offsetY;
+ j0 = j + referenceTemplateX[k] - offsetX;
+
+ if (i0 < 0 || i0 >= referenceHeight || j0 < 0 || j0 >= referenceWidth) {
+ contextLabel <<= 1;
+ } else {
+ contextLabel = contextLabel << 1 | referenceBitmap[i0][j0];
+ }
+ }
+
+ var pixel = decoder.readBit(contexts, contextLabel);
+ row[j] = pixel;
+ }
+ }
+
+ return bitmap;
+ }
+
+ function decodeSymbolDictionary(huffman, refinement, symbols, numberOfNewSymbols, numberOfExportedSymbols, huffmanTables, templateIndex, at, refinementTemplateIndex, refinementAt, decodingContext, huffmanInput) {
+ if (huffman && refinement) {
+ throw new Jbig2Error('symbol refinement with Huffman is not supported');
+ }
+
+ var newSymbols = [];
+ var currentHeight = 0;
+ var symbolCodeLength = (0, _util.log2)(symbols.length + numberOfNewSymbols);
+ var decoder = decodingContext.decoder;
+ var contextCache = decodingContext.contextCache;
+ var tableB1, symbolWidths;
+
+ if (huffman) {
+ tableB1 = getStandardTable(1);
+ symbolWidths = [];
+ symbolCodeLength = Math.max(symbolCodeLength, 1);
+ }
+
+ while (newSymbols.length < numberOfNewSymbols) {
+ var deltaHeight = huffman ? huffmanTables.tableDeltaHeight.decode(huffmanInput) : decodeInteger(contextCache, 'IADH', decoder);
+ currentHeight += deltaHeight;
+ var currentWidth = 0,
+ totalWidth = 0;
+ var firstSymbol = huffman ? symbolWidths.length : 0;
+
+ while (true) {
+ var deltaWidth = huffman ? huffmanTables.tableDeltaWidth.decode(huffmanInput) : decodeInteger(contextCache, 'IADW', decoder);
+
+ if (deltaWidth === null) {
+ break;
+ }
+
+ currentWidth += deltaWidth;
+ totalWidth += currentWidth;
+ var bitmap;
+
+ if (refinement) {
+ var numberOfInstances = decodeInteger(contextCache, 'IAAI', decoder);
+
+ if (numberOfInstances > 1) {
+ bitmap = decodeTextRegion(huffman, refinement, currentWidth, currentHeight, 0, numberOfInstances, 1, symbols.concat(newSymbols), symbolCodeLength, 0, 0, 1, 0, huffmanTables, refinementTemplateIndex, refinementAt, decodingContext, 0, huffmanInput);
+ } else {
+ var symbolId = decodeIAID(contextCache, decoder, symbolCodeLength);
+ var rdx = decodeInteger(contextCache, 'IARDX', decoder);
+ var rdy = decodeInteger(contextCache, 'IARDY', decoder);
+ var symbol = symbolId < symbols.length ? symbols[symbolId] : newSymbols[symbolId - symbols.length];
+ bitmap = decodeRefinement(currentWidth, currentHeight, refinementTemplateIndex, symbol, rdx, rdy, false, refinementAt, decodingContext);
+ }
+
+ newSymbols.push(bitmap);
+ } else if (huffman) {
+ symbolWidths.push(currentWidth);
+ } else {
+ bitmap = decodeBitmap(false, currentWidth, currentHeight, templateIndex, false, null, at, decodingContext);
+ newSymbols.push(bitmap);
+ }
+ }
+
+ if (huffman && !refinement) {
+ var bitmapSize = huffmanTables.tableBitmapSize.decode(huffmanInput);
+ huffmanInput.byteAlign();
+ var collectiveBitmap = void 0;
+
+ if (bitmapSize === 0) {
+ collectiveBitmap = readUncompressedBitmap(huffmanInput, totalWidth, currentHeight);
+ } else {
+ var originalEnd = huffmanInput.end;
+ var bitmapEnd = huffmanInput.position + bitmapSize;
+ huffmanInput.end = bitmapEnd;
+ collectiveBitmap = decodeMMRBitmap(huffmanInput, totalWidth, currentHeight, false);
+ huffmanInput.end = originalEnd;
+ huffmanInput.position = bitmapEnd;
+ }
+
+ var numberOfSymbolsDecoded = symbolWidths.length;
+
+ if (firstSymbol === numberOfSymbolsDecoded - 1) {
+ newSymbols.push(collectiveBitmap);
+ } else {
+ var _i = void 0,
+ y = void 0,
+ xMin = 0,
+ xMax = void 0,
+ bitmapWidth = void 0,
+ symbolBitmap = void 0;
+
+ for (_i = firstSymbol; _i < numberOfSymbolsDecoded; _i++) {
+ bitmapWidth = symbolWidths[_i];
+ xMax = xMin + bitmapWidth;
+ symbolBitmap = [];
+
+ for (y = 0; y < currentHeight; y++) {
+ symbolBitmap.push(collectiveBitmap[y].subarray(xMin, xMax));
+ }
+
+ newSymbols.push(symbolBitmap);
+ xMin = xMax;
+ }
+ }
+ }
+ }
+
+ var exportedSymbols = [];
+ var flags = [],
+ currentFlag = false;
+ var totalSymbolsLength = symbols.length + numberOfNewSymbols;
+
+ while (flags.length < totalSymbolsLength) {
+ var runLength = huffman ? tableB1.decode(huffmanInput) : decodeInteger(contextCache, 'IAEX', decoder);
+
+ while (runLength--) {
+ flags.push(currentFlag);
+ }
+
+ currentFlag = !currentFlag;
+ }
+
+ for (var i = 0, ii = symbols.length; i < ii; i++) {
+ if (flags[i]) {
+ exportedSymbols.push(symbols[i]);
+ }
+ }
+
+ for (var j = 0; j < numberOfNewSymbols; i++, j++) {
+ if (flags[i]) {
+ exportedSymbols.push(newSymbols[j]);
+ }
+ }
+
+ return exportedSymbols;
+ }
+
+ function decodeTextRegion(huffman, refinement, width, height, defaultPixelValue, numberOfSymbolInstances, stripSize, inputSymbols, symbolCodeLength, transposed, dsOffset, referenceCorner, combinationOperator, huffmanTables, refinementTemplateIndex, refinementAt, decodingContext, logStripSize, huffmanInput) {
+ if (huffman && refinement) {
+ throw new Jbig2Error('refinement with Huffman is not supported');
+ }
+
+ var bitmap = [];
+ var i, row;
+
+ for (i = 0; i < height; i++) {
+ row = new Uint8Array(width);
+
+ if (defaultPixelValue) {
+ for (var j = 0; j < width; j++) {
+ row[j] = defaultPixelValue;
+ }
+ }
+
+ bitmap.push(row);
+ }
+
+ var decoder = decodingContext.decoder;
+ var contextCache = decodingContext.contextCache;
+ var stripT = huffman ? -huffmanTables.tableDeltaT.decode(huffmanInput) : -decodeInteger(contextCache, 'IADT', decoder);
+ var firstS = 0;
+ i = 0;
+
+ while (i < numberOfSymbolInstances) {
+ var deltaT = huffman ? huffmanTables.tableDeltaT.decode(huffmanInput) : decodeInteger(contextCache, 'IADT', decoder);
+ stripT += deltaT;
+ var deltaFirstS = huffman ? huffmanTables.tableFirstS.decode(huffmanInput) : decodeInteger(contextCache, 'IAFS', decoder);
+ firstS += deltaFirstS;
+ var currentS = firstS;
+
+ do {
+ var currentT = 0;
+
+ if (stripSize > 1) {
+ currentT = huffman ? huffmanInput.readBits(logStripSize) : decodeInteger(contextCache, 'IAIT', decoder);
+ }
+
+ var t = stripSize * stripT + currentT;
+ var symbolId = huffman ? huffmanTables.symbolIDTable.decode(huffmanInput) : decodeIAID(contextCache, decoder, symbolCodeLength);
+ var applyRefinement = refinement && (huffman ? huffmanInput.readBit() : decodeInteger(contextCache, 'IARI', decoder));
+ var symbolBitmap = inputSymbols[symbolId];
+ var symbolWidth = symbolBitmap[0].length;
+ var symbolHeight = symbolBitmap.length;
+
+ if (applyRefinement) {
+ var rdw = decodeInteger(contextCache, 'IARDW', decoder);
+ var rdh = decodeInteger(contextCache, 'IARDH', decoder);
+ var rdx = decodeInteger(contextCache, 'IARDX', decoder);
+ var rdy = decodeInteger(contextCache, 'IARDY', decoder);
+ symbolWidth += rdw;
+ symbolHeight += rdh;
+ symbolBitmap = decodeRefinement(symbolWidth, symbolHeight, refinementTemplateIndex, symbolBitmap, (rdw >> 1) + rdx, (rdh >> 1) + rdy, false, refinementAt, decodingContext);
+ }
+
+ var offsetT = t - (referenceCorner & 1 ? 0 : symbolHeight - 1);
+ var offsetS = currentS - (referenceCorner & 2 ? symbolWidth - 1 : 0);
+ var s2, t2, symbolRow;
+
+ if (transposed) {
+ for (s2 = 0; s2 < symbolHeight; s2++) {
+ row = bitmap[offsetS + s2];
+
+ if (!row) {
+ continue;
+ }
+
+ symbolRow = symbolBitmap[s2];
+ var maxWidth = Math.min(width - offsetT, symbolWidth);
+
+ switch (combinationOperator) {
+ case 0:
+ for (t2 = 0; t2 < maxWidth; t2++) {
+ row[offsetT + t2] |= symbolRow[t2];
+ }
+
+ break;
+
+ case 2:
+ for (t2 = 0; t2 < maxWidth; t2++) {
+ row[offsetT + t2] ^= symbolRow[t2];
+ }
+
+ break;
+
+ default:
+ throw new Jbig2Error("operator ".concat(combinationOperator, " is not supported"));
+ }
+ }
+
+ currentS += symbolHeight - 1;
+ } else {
+ for (t2 = 0; t2 < symbolHeight; t2++) {
+ row = bitmap[offsetT + t2];
+
+ if (!row) {
+ continue;
+ }
+
+ symbolRow = symbolBitmap[t2];
+
+ switch (combinationOperator) {
+ case 0:
+ for (s2 = 0; s2 < symbolWidth; s2++) {
+ row[offsetS + s2] |= symbolRow[s2];
+ }
+
+ break;
+
+ case 2:
+ for (s2 = 0; s2 < symbolWidth; s2++) {
+ row[offsetS + s2] ^= symbolRow[s2];
+ }
+
+ break;
+
+ default:
+ throw new Jbig2Error("operator ".concat(combinationOperator, " is not supported"));
+ }
+ }
+
+ currentS += symbolWidth - 1;
+ }
+
+ i++;
+ var deltaS = huffman ? huffmanTables.tableDeltaS.decode(huffmanInput) : decodeInteger(contextCache, 'IADS', decoder);
+
+ if (deltaS === null) {
+ break;
+ }
+
+ currentS += deltaS + dsOffset;
+ } while (true);
+ }
+
+ return bitmap;
+ }
+
+ function decodePatternDictionary(mmr, patternWidth, patternHeight, maxPatternIndex, template, decodingContext) {
+ var at = [];
+
+ if (!mmr) {
+ at.push({
+ x: -patternWidth,
+ y: 0
+ });
+
+ if (template === 0) {
+ at.push({
+ x: -3,
+ y: -1
+ });
+ at.push({
+ x: 2,
+ y: -2
+ });
+ at.push({
+ x: -2,
+ y: -2
+ });
+ }
+ }
+
+ var collectiveWidth = (maxPatternIndex + 1) * patternWidth;
+ var collectiveBitmap = decodeBitmap(mmr, collectiveWidth, patternHeight, template, false, null, at, decodingContext);
+ var patterns = [],
+ i = 0,
+ patternBitmap,
+ xMin,
+ xMax,
+ y;
+
+ while (i <= maxPatternIndex) {
+ patternBitmap = [];
+ xMin = patternWidth * i;
+ xMax = xMin + patternWidth;
+
+ for (y = 0; y < patternHeight; y++) {
+ patternBitmap.push(collectiveBitmap[y].subarray(xMin, xMax));
+ }
+
+ patterns.push(patternBitmap);
+ i++;
+ }
+
+ return patterns;
+ }
+
+ function decodeHalftoneRegion(mmr, patterns, template, regionWidth, regionHeight, defaultPixelValue, enableSkip, combinationOperator, gridWidth, gridHeight, gridOffsetX, gridOffsetY, gridVectorX, gridVectorY, decodingContext) {
+ var skip = null;
+
+ if (enableSkip) {
+ throw new Jbig2Error('skip is not supported');
+ }
+
+ if (combinationOperator !== 0) {
+ throw new Jbig2Error('operator ' + combinationOperator + ' is not supported in halftone region');
+ }
+
+ var regionBitmap = [];
+ var i, j, row;
+
+ for (i = 0; i < regionHeight; i++) {
+ row = new Uint8Array(regionWidth);
+
+ if (defaultPixelValue) {
+ for (j = 0; j < regionWidth; j++) {
+ row[j] = defaultPixelValue;
+ }
+ }
+
+ regionBitmap.push(row);
+ }
+
+ var numberOfPatterns = patterns.length;
+ var pattern0 = patterns[0];
+ var patternWidth = pattern0[0].length,
+ patternHeight = pattern0.length;
+ var bitsPerValue = (0, _util.log2)(numberOfPatterns);
+ var at = [];
+
+ if (!mmr) {
+ at.push({
+ x: template <= 1 ? 3 : 2,
+ y: -1
+ });
+
+ if (template === 0) {
+ at.push({
+ x: -3,
+ y: -1
+ });
+ at.push({
+ x: 2,
+ y: -2
+ });
+ at.push({
+ x: -2,
+ y: -2
+ });
+ }
+ }
+
+ var grayScaleBitPlanes = [],
+ mmrInput,
+ bitmap;
+
+ if (mmr) {
+ mmrInput = new Reader(decodingContext.data, decodingContext.start, decodingContext.end);
+ }
+
+ for (i = bitsPerValue - 1; i >= 0; i--) {
+ if (mmr) {
+ bitmap = decodeMMRBitmap(mmrInput, gridWidth, gridHeight, true);
+ } else {
+ bitmap = decodeBitmap(false, gridWidth, gridHeight, template, false, skip, at, decodingContext);
+ }
+
+ grayScaleBitPlanes[i] = bitmap;
+ }
+
+ var mg, ng, bit, patternIndex, patternBitmap, x, y, patternRow, regionRow;
+
+ for (mg = 0; mg < gridHeight; mg++) {
+ for (ng = 0; ng < gridWidth; ng++) {
+ bit = 0;
+ patternIndex = 0;
+
+ for (j = bitsPerValue - 1; j >= 0; j--) {
+ bit = grayScaleBitPlanes[j][mg][ng] ^ bit;
+ patternIndex |= bit << j;
+ }
+
+ patternBitmap = patterns[patternIndex];
+ x = gridOffsetX + mg * gridVectorY + ng * gridVectorX >> 8;
+ y = gridOffsetY + mg * gridVectorX - ng * gridVectorY >> 8;
+
+ if (x >= 0 && x + patternWidth <= regionWidth && y >= 0 && y + patternHeight <= regionHeight) {
+ for (i = 0; i < patternHeight; i++) {
+ regionRow = regionBitmap[y + i];
+ patternRow = patternBitmap[i];
+
+ for (j = 0; j < patternWidth; j++) {
+ regionRow[x + j] |= patternRow[j];
+ }
+ }
+ } else {
+ var regionX = void 0,
+ regionY = void 0;
+
+ for (i = 0; i < patternHeight; i++) {
+ regionY = y + i;
+
+ if (regionY < 0 || regionY >= regionHeight) {
+ continue;
+ }
+
+ regionRow = regionBitmap[regionY];
+ patternRow = patternBitmap[i];
+
+ for (j = 0; j < patternWidth; j++) {
+ regionX = x + j;
+
+ if (regionX >= 0 && regionX < regionWidth) {
+ regionRow[regionX] |= patternRow[j];
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return regionBitmap;
+ }
+
+ function readSegmentHeader(data, start) {
+ var segmentHeader = {};
+ segmentHeader.number = (0, _util.readUint32)(data, start);
+ var flags = data[start + 4];
+ var segmentType = flags & 0x3F;
+
+ if (!SegmentTypes[segmentType]) {
+ throw new Jbig2Error('invalid segment type: ' + segmentType);
+ }
+
+ segmentHeader.type = segmentType;
+ segmentHeader.typeName = SegmentTypes[segmentType];
+ segmentHeader.deferredNonRetain = !!(flags & 0x80);
+ var pageAssociationFieldSize = !!(flags & 0x40);
+ var referredFlags = data[start + 5];
+ var referredToCount = referredFlags >> 5 & 7;
+ var retainBits = [referredFlags & 31];
+ var position = start + 6;
+
+ if (referredFlags === 7) {
+ referredToCount = (0, _util.readUint32)(data, position - 1) & 0x1FFFFFFF;
+ position += 3;
+ var bytes = referredToCount + 7 >> 3;
+ retainBits[0] = data[position++];
+
+ while (--bytes > 0) {
+ retainBits.push(data[position++]);
+ }
+ } else if (referredFlags === 5 || referredFlags === 6) {
+ throw new Jbig2Error('invalid referred-to flags');
+ }
+
+ segmentHeader.retainBits = retainBits;
+ var referredToSegmentNumberSize = segmentHeader.number <= 256 ? 1 : segmentHeader.number <= 65536 ? 2 : 4;
+ var referredTo = [];
+ var i, ii;
+
+ for (i = 0; i < referredToCount; i++) {
+ var number = referredToSegmentNumberSize === 1 ? data[position] : referredToSegmentNumberSize === 2 ? (0, _util.readUint16)(data, position) : (0, _util.readUint32)(data, position);
+ referredTo.push(number);
+ position += referredToSegmentNumberSize;
+ }
+
+ segmentHeader.referredTo = referredTo;
+
+ if (!pageAssociationFieldSize) {
+ segmentHeader.pageAssociation = data[position++];
+ } else {
+ segmentHeader.pageAssociation = (0, _util.readUint32)(data, position);
+ position += 4;
+ }
+
+ segmentHeader.length = (0, _util.readUint32)(data, position);
+ position += 4;
+
+ if (segmentHeader.length === 0xFFFFFFFF) {
+ if (segmentType === 38) {
+ var genericRegionInfo = readRegionSegmentInformation(data, position);
+ var genericRegionSegmentFlags = data[position + RegionSegmentInformationFieldLength];
+ var genericRegionMmr = !!(genericRegionSegmentFlags & 1);
+ var searchPatternLength = 6;
+ var searchPattern = new Uint8Array(searchPatternLength);
+
+ if (!genericRegionMmr) {
+ searchPattern[0] = 0xFF;
+ searchPattern[1] = 0xAC;
+ }
+
+ searchPattern[2] = genericRegionInfo.height >>> 24 & 0xFF;
+ searchPattern[3] = genericRegionInfo.height >> 16 & 0xFF;
+ searchPattern[4] = genericRegionInfo.height >> 8 & 0xFF;
+ searchPattern[5] = genericRegionInfo.height & 0xFF;
+
+ for (i = position, ii = data.length; i < ii; i++) {
+ var j = 0;
+
+ while (j < searchPatternLength && searchPattern[j] === data[i + j]) {
+ j++;
+ }
+
+ if (j === searchPatternLength) {
+ segmentHeader.length = i + searchPatternLength;
+ break;
+ }
+ }
+
+ if (segmentHeader.length === 0xFFFFFFFF) {
+ throw new Jbig2Error('segment end was not found');
+ }
+ } else {
+ throw new Jbig2Error('invalid unknown segment length');
+ }
+ }
+
+ segmentHeader.headerEnd = position;
+ return segmentHeader;
+ }
+
+ function readSegments(header, data, start, end) {
+ var segments = [];
+ var position = start;
+
+ while (position < end) {
+ var segmentHeader = readSegmentHeader(data, position);
+ position = segmentHeader.headerEnd;
+ var segment = {
+ header: segmentHeader,
+ data: data
+ };
+
+ if (!header.randomAccess) {
+ segment.start = position;
+ position += segmentHeader.length;
+ segment.end = position;
+ }
+
+ segments.push(segment);
+
+ if (segmentHeader.type === 51) {
+ break;
+ }
+ }
+
+ if (header.randomAccess) {
+ for (var i = 0, ii = segments.length; i < ii; i++) {
+ segments[i].start = position;
+ position += segments[i].header.length;
+ segments[i].end = position;
+ }
+ }
+
+ return segments;
+ }
+
+ function readRegionSegmentInformation(data, start) {
+ return {
+ width: (0, _util.readUint32)(data, start),
+ height: (0, _util.readUint32)(data, start + 4),
+ x: (0, _util.readUint32)(data, start + 8),
+ y: (0, _util.readUint32)(data, start + 12),
+ combinationOperator: data[start + 16] & 7
+ };
+ }
+
+ var RegionSegmentInformationFieldLength = 17;
+
+ function processSegment(segment, visitor) {
+ var header = segment.header;
+ var data = segment.data,
+ position = segment.start,
+ end = segment.end;
+ var args, at, i, atLength;
+
+ switch (header.type) {
+ case 0:
+ var dictionary = {};
+ var dictionaryFlags = (0, _util.readUint16)(data, position);
+ dictionary.huffman = !!(dictionaryFlags & 1);
+ dictionary.refinement = !!(dictionaryFlags & 2);
+ dictionary.huffmanDHSelector = dictionaryFlags >> 2 & 3;
+ dictionary.huffmanDWSelector = dictionaryFlags >> 4 & 3;
+ dictionary.bitmapSizeSelector = dictionaryFlags >> 6 & 1;
+ dictionary.aggregationInstancesSelector = dictionaryFlags >> 7 & 1;
+ dictionary.bitmapCodingContextUsed = !!(dictionaryFlags & 256);
+ dictionary.bitmapCodingContextRetained = !!(dictionaryFlags & 512);
+ dictionary.template = dictionaryFlags >> 10 & 3;
+ dictionary.refinementTemplate = dictionaryFlags >> 12 & 1;
+ position += 2;
+
+ if (!dictionary.huffman) {
+ atLength = dictionary.template === 0 ? 4 : 1;
+ at = [];
+
+ for (i = 0; i < atLength; i++) {
+ at.push({
+ x: (0, _util.readInt8)(data, position),
+ y: (0, _util.readInt8)(data, position + 1)
+ });
+ position += 2;
+ }
+
+ dictionary.at = at;
+ }
+
+ if (dictionary.refinement && !dictionary.refinementTemplate) {
+ at = [];
+
+ for (i = 0; i < 2; i++) {
+ at.push({
+ x: (0, _util.readInt8)(data, position),
+ y: (0, _util.readInt8)(data, position + 1)
+ });
+ position += 2;
+ }
+
+ dictionary.refinementAt = at;
+ }
+
+ dictionary.numberOfExportedSymbols = (0, _util.readUint32)(data, position);
+ position += 4;
+ dictionary.numberOfNewSymbols = (0, _util.readUint32)(data, position);
+ position += 4;
+ args = [dictionary, header.number, header.referredTo, data, position, end];
+ break;
+
+ case 6:
+ case 7:
+ var textRegion = {};
+ textRegion.info = readRegionSegmentInformation(data, position);
+ position += RegionSegmentInformationFieldLength;
+ var textRegionSegmentFlags = (0, _util.readUint16)(data, position);
+ position += 2;
+ textRegion.huffman = !!(textRegionSegmentFlags & 1);
+ textRegion.refinement = !!(textRegionSegmentFlags & 2);
+ textRegion.logStripSize = textRegionSegmentFlags >> 2 & 3;
+ textRegion.stripSize = 1 << textRegion.logStripSize;
+ textRegion.referenceCorner = textRegionSegmentFlags >> 4 & 3;
+ textRegion.transposed = !!(textRegionSegmentFlags & 64);
+ textRegion.combinationOperator = textRegionSegmentFlags >> 7 & 3;
+ textRegion.defaultPixelValue = textRegionSegmentFlags >> 9 & 1;
+ textRegion.dsOffset = textRegionSegmentFlags << 17 >> 27;
+ textRegion.refinementTemplate = textRegionSegmentFlags >> 15 & 1;
+
+ if (textRegion.huffman) {
+ var textRegionHuffmanFlags = (0, _util.readUint16)(data, position);
+ position += 2;
+ textRegion.huffmanFS = textRegionHuffmanFlags & 3;
+ textRegion.huffmanDS = textRegionHuffmanFlags >> 2 & 3;
+ textRegion.huffmanDT = textRegionHuffmanFlags >> 4 & 3;
+ textRegion.huffmanRefinementDW = textRegionHuffmanFlags >> 6 & 3;
+ textRegion.huffmanRefinementDH = textRegionHuffmanFlags >> 8 & 3;
+ textRegion.huffmanRefinementDX = textRegionHuffmanFlags >> 10 & 3;
+ textRegion.huffmanRefinementDY = textRegionHuffmanFlags >> 12 & 3;
+ textRegion.huffmanRefinementSizeSelector = !!(textRegionHuffmanFlags & 0x4000);
+ }
+
+ if (textRegion.refinement && !textRegion.refinementTemplate) {
+ at = [];
+
+ for (i = 0; i < 2; i++) {
+ at.push({
+ x: (0, _util.readInt8)(data, position),
+ y: (0, _util.readInt8)(data, position + 1)
+ });
+ position += 2;
+ }
+
+ textRegion.refinementAt = at;
+ }
+
+ textRegion.numberOfSymbolInstances = (0, _util.readUint32)(data, position);
+ position += 4;
+ args = [textRegion, header.referredTo, data, position, end];
+ break;
+
+ case 16:
+ var patternDictionary = {};
+ var patternDictionaryFlags = data[position++];
+ patternDictionary.mmr = !!(patternDictionaryFlags & 1);
+ patternDictionary.template = patternDictionaryFlags >> 1 & 3;
+ patternDictionary.patternWidth = data[position++];
+ patternDictionary.patternHeight = data[position++];
+ patternDictionary.maxPatternIndex = (0, _util.readUint32)(data, position);
+ position += 4;
+ args = [patternDictionary, header.number, data, position, end];
+ break;
+
+ case 22:
+ case 23:
+ var halftoneRegion = {};
+ halftoneRegion.info = readRegionSegmentInformation(data, position);
+ position += RegionSegmentInformationFieldLength;
+ var halftoneRegionFlags = data[position++];
+ halftoneRegion.mmr = !!(halftoneRegionFlags & 1);
+ halftoneRegion.template = halftoneRegionFlags >> 1 & 3;
+ halftoneRegion.enableSkip = !!(halftoneRegionFlags & 8);
+ halftoneRegion.combinationOperator = halftoneRegionFlags >> 4 & 7;
+ halftoneRegion.defaultPixelValue = halftoneRegionFlags >> 7 & 1;
+ halftoneRegion.gridWidth = (0, _util.readUint32)(data, position);
+ position += 4;
+ halftoneRegion.gridHeight = (0, _util.readUint32)(data, position);
+ position += 4;
+ halftoneRegion.gridOffsetX = (0, _util.readUint32)(data, position) & 0xFFFFFFFF;
+ position += 4;
+ halftoneRegion.gridOffsetY = (0, _util.readUint32)(data, position) & 0xFFFFFFFF;
+ position += 4;
+ halftoneRegion.gridVectorX = (0, _util.readUint16)(data, position);
+ position += 2;
+ halftoneRegion.gridVectorY = (0, _util.readUint16)(data, position);
+ position += 2;
+ args = [halftoneRegion, header.referredTo, data, position, end];
+ break;
+
+ case 38:
+ case 39:
+ var genericRegion = {};
+ genericRegion.info = readRegionSegmentInformation(data, position);
+ position += RegionSegmentInformationFieldLength;
+ var genericRegionSegmentFlags = data[position++];
+ genericRegion.mmr = !!(genericRegionSegmentFlags & 1);
+ genericRegion.template = genericRegionSegmentFlags >> 1 & 3;
+ genericRegion.prediction = !!(genericRegionSegmentFlags & 8);
+
+ if (!genericRegion.mmr) {
+ atLength = genericRegion.template === 0 ? 4 : 1;
+ at = [];
+
+ for (i = 0; i < atLength; i++) {
+ at.push({
+ x: (0, _util.readInt8)(data, position),
+ y: (0, _util.readInt8)(data, position + 1)
+ });
+ position += 2;
+ }
+
+ genericRegion.at = at;
+ }
+
+ args = [genericRegion, data, position, end];
+ break;
+
+ case 48:
+ var pageInfo = {
+ width: (0, _util.readUint32)(data, position),
+ height: (0, _util.readUint32)(data, position + 4),
+ resolutionX: (0, _util.readUint32)(data, position + 8),
+ resolutionY: (0, _util.readUint32)(data, position + 12)
+ };
+
+ if (pageInfo.height === 0xFFFFFFFF) {
+ delete pageInfo.height;
+ }
+
+ var pageSegmentFlags = data[position + 16];
+ (0, _util.readUint16)(data, position + 17);
+ pageInfo.lossless = !!(pageSegmentFlags & 1);
+ pageInfo.refinement = !!(pageSegmentFlags & 2);
+ pageInfo.defaultPixelValue = pageSegmentFlags >> 2 & 1;
+ pageInfo.combinationOperator = pageSegmentFlags >> 3 & 3;
+ pageInfo.requiresBuffer = !!(pageSegmentFlags & 32);
+ pageInfo.combinationOperatorOverride = !!(pageSegmentFlags & 64);
+ args = [pageInfo];
+ break;
+
+ case 49:
+ break;
+
+ case 50:
+ break;
+
+ case 51:
+ break;
+
+ case 53:
+ args = [header.number, data, position, end];
+ break;
+
+ case 62:
+ break;
+
+ default:
+ throw new Jbig2Error("segment type ".concat(header.typeName, "(").concat(header.type, ")") + ' is not implemented');
+ }
+
+ var callbackName = 'on' + header.typeName;
+
+ if (callbackName in visitor) {
+ visitor[callbackName].apply(visitor, args);
+ }
+ }
+
+ function processSegments(segments, visitor) {
+ for (var i = 0, ii = segments.length; i < ii; i++) {
+ processSegment(segments[i], visitor);
+ }
+ }
+
+ function parseJbig2Chunks(chunks) {
+ var visitor = new SimpleSegmentVisitor();
+
+ for (var i = 0, ii = chunks.length; i < ii; i++) {
+ var chunk = chunks[i];
+ var segments = readSegments({}, chunk.data, chunk.start, chunk.end);
+ processSegments(segments, visitor);
+ }
+
+ return visitor.buffer;
+ }
+
+ function parseJbig2(data) {
+ var position = 0,
+ end = data.length;
+
+ if (data[position] !== 0x97 || data[position + 1] !== 0x4A || data[position + 2] !== 0x42 || data[position + 3] !== 0x32 || data[position + 4] !== 0x0D || data[position + 5] !== 0x0A || data[position + 6] !== 0x1A || data[position + 7] !== 0x0A) {
+ throw new Jbig2Error('parseJbig2 - invalid header.');
+ }
+
+ var header = Object.create(null);
+ position += 8;
+ var flags = data[position++];
+ header.randomAccess = !(flags & 1);
+
+ if (!(flags & 2)) {
+ header.numberOfPages = (0, _util.readUint32)(data, position);
+ position += 4;
+ }
+
+ var segments = readSegments(header, data, position, end);
+ var visitor = new SimpleSegmentVisitor();
+ processSegments(segments, visitor);
+ var _visitor$currentPageI = visitor.currentPageInfo,
+ width = _visitor$currentPageI.width,
+ height = _visitor$currentPageI.height;
+ var bitPacked = visitor.buffer;
+ var imgData = new Uint8ClampedArray(width * height);
+ var q = 0,
+ k = 0;
+
+ for (var i = 0; i < height; i++) {
+ var mask = 0,
+ buffer = void 0;
+
+ for (var j = 0; j < width; j++) {
+ if (!mask) {
+ mask = 128;
+ buffer = bitPacked[k++];
+ }
+
+ imgData[q++] = buffer & mask ? 0 : 255;
+ mask >>= 1;
+ }
+ }
+
+ return {
+ imgData: imgData,
+ width: width,
+ height: height
+ };
+ }
+
+ function SimpleSegmentVisitor() {}
+
+ SimpleSegmentVisitor.prototype = {
+ onPageInformation: function SimpleSegmentVisitor_onPageInformation(info) {
+ this.currentPageInfo = info;
+ var rowSize = info.width + 7 >> 3;
+ var buffer = new Uint8ClampedArray(rowSize * info.height);
+
+ if (info.defaultPixelValue) {
+ for (var i = 0, ii = buffer.length; i < ii; i++) {
+ buffer[i] = 0xFF;
+ }
+ }
+
+ this.buffer = buffer;
+ },
+ drawBitmap: function SimpleSegmentVisitor_drawBitmap(regionInfo, bitmap) {
+ var pageInfo = this.currentPageInfo;
+ var width = regionInfo.width,
+ height = regionInfo.height;
+ var rowSize = pageInfo.width + 7 >> 3;
+ var combinationOperator = pageInfo.combinationOperatorOverride ? regionInfo.combinationOperator : pageInfo.combinationOperator;
+ var buffer = this.buffer;
+ var mask0 = 128 >> (regionInfo.x & 7);
+ var offset0 = regionInfo.y * rowSize + (regionInfo.x >> 3);
+ var i, j, mask, offset;
+
+ switch (combinationOperator) {
+ case 0:
+ for (i = 0; i < height; i++) {
+ mask = mask0;
+ offset = offset0;
+
+ for (j = 0; j < width; j++) {
+ if (bitmap[i][j]) {
+ buffer[offset] |= mask;
+ }
+
+ mask >>= 1;
+
+ if (!mask) {
+ mask = 128;
+ offset++;
+ }
+ }
+
+ offset0 += rowSize;
+ }
+
+ break;
+
+ case 2:
+ for (i = 0; i < height; i++) {
+ mask = mask0;
+ offset = offset0;
+
+ for (j = 0; j < width; j++) {
+ if (bitmap[i][j]) {
+ buffer[offset] ^= mask;
+ }
+
+ mask >>= 1;
+
+ if (!mask) {
+ mask = 128;
+ offset++;
+ }
+ }
+
+ offset0 += rowSize;
+ }
+
+ break;
+
+ default:
+ throw new Jbig2Error("operator ".concat(combinationOperator, " is not supported"));
+ }
+ },
+ onImmediateGenericRegion: function SimpleSegmentVisitor_onImmediateGenericRegion(region, data, start, end) {
+ var regionInfo = region.info;
+ var decodingContext = new DecodingContext(data, start, end);
+ var bitmap = decodeBitmap(region.mmr, regionInfo.width, regionInfo.height, region.template, region.prediction, null, region.at, decodingContext);
+ this.drawBitmap(regionInfo, bitmap);
+ },
+ onImmediateLosslessGenericRegion: function SimpleSegmentVisitor_onImmediateLosslessGenericRegion() {
+ this.onImmediateGenericRegion.apply(this, arguments);
+ },
+ onSymbolDictionary: function SimpleSegmentVisitor_onSymbolDictionary(dictionary, currentSegment, referredSegments, data, start, end) {
+ var huffmanTables, huffmanInput;
+
+ if (dictionary.huffman) {
+ huffmanTables = getSymbolDictionaryHuffmanTables(dictionary, referredSegments, this.customTables);
+ huffmanInput = new Reader(data, start, end);
+ }
+
+ var symbols = this.symbols;
+
+ if (!symbols) {
+ this.symbols = symbols = {};
+ }
+
+ var inputSymbols = [];
+
+ for (var i = 0, ii = referredSegments.length; i < ii; i++) {
+ var referredSymbols = symbols[referredSegments[i]];
+
+ if (referredSymbols) {
+ inputSymbols = inputSymbols.concat(referredSymbols);
+ }
+ }
+
+ var decodingContext = new DecodingContext(data, start, end);
+ symbols[currentSegment] = decodeSymbolDictionary(dictionary.huffman, dictionary.refinement, inputSymbols, dictionary.numberOfNewSymbols, dictionary.numberOfExportedSymbols, huffmanTables, dictionary.template, dictionary.at, dictionary.refinementTemplate, dictionary.refinementAt, decodingContext, huffmanInput);
+ },
+ onImmediateTextRegion: function SimpleSegmentVisitor_onImmediateTextRegion(region, referredSegments, data, start, end) {
+ var regionInfo = region.info;
+ var huffmanTables, huffmanInput;
+ var symbols = this.symbols;
+ var inputSymbols = [];
+
+ for (var i = 0, ii = referredSegments.length; i < ii; i++) {
+ var referredSymbols = symbols[referredSegments[i]];
+
+ if (referredSymbols) {
+ inputSymbols = inputSymbols.concat(referredSymbols);
+ }
+ }
+
+ var symbolCodeLength = (0, _util.log2)(inputSymbols.length);
+
+ if (region.huffman) {
+ huffmanInput = new Reader(data, start, end);
+ huffmanTables = getTextRegionHuffmanTables(region, referredSegments, this.customTables, inputSymbols.length, huffmanInput);
+ }
+
+ var decodingContext = new DecodingContext(data, start, end);
+ var bitmap = decodeTextRegion(region.huffman, region.refinement, regionInfo.width, regionInfo.height, region.defaultPixelValue, region.numberOfSymbolInstances, region.stripSize, inputSymbols, symbolCodeLength, region.transposed, region.dsOffset, region.referenceCorner, region.combinationOperator, huffmanTables, region.refinementTemplate, region.refinementAt, decodingContext, region.logStripSize, huffmanInput);
+ this.drawBitmap(regionInfo, bitmap);
+ },
+ onImmediateLosslessTextRegion: function SimpleSegmentVisitor_onImmediateLosslessTextRegion() {
+ this.onImmediateTextRegion.apply(this, arguments);
+ },
+ onPatternDictionary: function onPatternDictionary(dictionary, currentSegment, data, start, end) {
+ var patterns = this.patterns;
+
+ if (!patterns) {
+ this.patterns = patterns = {};
+ }
+
+ var decodingContext = new DecodingContext(data, start, end);
+ patterns[currentSegment] = decodePatternDictionary(dictionary.mmr, dictionary.patternWidth, dictionary.patternHeight, dictionary.maxPatternIndex, dictionary.template, decodingContext);
+ },
+ onImmediateHalftoneRegion: function onImmediateHalftoneRegion(region, referredSegments, data, start, end) {
+ var patterns = this.patterns[referredSegments[0]];
+ var regionInfo = region.info;
+ var decodingContext = new DecodingContext(data, start, end);
+ var bitmap = decodeHalftoneRegion(region.mmr, patterns, region.template, regionInfo.width, regionInfo.height, region.defaultPixelValue, region.enableSkip, region.combinationOperator, region.gridWidth, region.gridHeight, region.gridOffsetX, region.gridOffsetY, region.gridVectorX, region.gridVectorY, decodingContext);
+ this.drawBitmap(regionInfo, bitmap);
+ },
+ onImmediateLosslessHalftoneRegion: function onImmediateLosslessHalftoneRegion() {
+ this.onImmediateHalftoneRegion.apply(this, arguments);
+ },
+ onTables: function onTables(currentSegment, data, start, end) {
+ var customTables = this.customTables;
+
+ if (!customTables) {
+ this.customTables = customTables = {};
+ }
+
+ customTables[currentSegment] = decodeTablesSegment(data, start, end);
+ }
+ };
+
+ function HuffmanLine(lineData) {
+ if (lineData.length === 2) {
+ this.isOOB = true;
+ this.rangeLow = 0;
+ this.prefixLength = lineData[0];
+ this.rangeLength = 0;
+ this.prefixCode = lineData[1];
+ this.isLowerRange = false;
+ } else {
+ this.isOOB = false;
+ this.rangeLow = lineData[0];
+ this.prefixLength = lineData[1];
+ this.rangeLength = lineData[2];
+ this.prefixCode = lineData[3];
+ this.isLowerRange = lineData[4] === 'lower';
+ }
+ }
+
+ function HuffmanTreeNode(line) {
+ this.children = [];
+
+ if (line) {
+ this.isLeaf = true;
+ this.rangeLength = line.rangeLength;
+ this.rangeLow = line.rangeLow;
+ this.isLowerRange = line.isLowerRange;
+ this.isOOB = line.isOOB;
+ } else {
+ this.isLeaf = false;
+ }
+ }
+
+ HuffmanTreeNode.prototype = {
+ buildTree: function buildTree(line, shift) {
+ var bit = line.prefixCode >> shift & 1;
+
+ if (shift <= 0) {
+ this.children[bit] = new HuffmanTreeNode(line);
+ } else {
+ var node = this.children[bit];
+
+ if (!node) {
+ this.children[bit] = node = new HuffmanTreeNode(null);
+ }
+
+ node.buildTree(line, shift - 1);
+ }
+ },
+ decodeNode: function decodeNode(reader) {
+ if (this.isLeaf) {
+ if (this.isOOB) {
+ return null;
+ }
+
+ var htOffset = reader.readBits(this.rangeLength);
+ return this.rangeLow + (this.isLowerRange ? -htOffset : htOffset);
+ }
+
+ var node = this.children[reader.readBit()];
+
+ if (!node) {
+ throw new Jbig2Error('invalid Huffman data');
+ }
+
+ return node.decodeNode(reader);
+ }
+ };
+
+ function HuffmanTable(lines, prefixCodesDone) {
+ if (!prefixCodesDone) {
+ this.assignPrefixCodes(lines);
+ }
+
+ this.rootNode = new HuffmanTreeNode(null);
+ var i,
+ ii = lines.length,
+ line;
+
+ for (i = 0; i < ii; i++) {
+ line = lines[i];
+
+ if (line.prefixLength > 0) {
+ this.rootNode.buildTree(line, line.prefixLength - 1);
+ }
+ }
+ }
+
+ HuffmanTable.prototype = {
+ decode: function decode(reader) {
+ return this.rootNode.decodeNode(reader);
+ },
+ assignPrefixCodes: function assignPrefixCodes(lines) {
+ var linesLength = lines.length,
+ prefixLengthMax = 0,
+ i;
+
+ for (i = 0; i < linesLength; i++) {
+ prefixLengthMax = Math.max(prefixLengthMax, lines[i].prefixLength);
+ }
+
+ var histogram = new Uint32Array(prefixLengthMax + 1);
+
+ for (i = 0; i < linesLength; i++) {
+ histogram[lines[i].prefixLength]++;
+ }
+
+ var currentLength = 1,
+ firstCode = 0,
+ currentCode,
+ currentTemp,
+ line;
+ histogram[0] = 0;
+
+ while (currentLength <= prefixLengthMax) {
+ firstCode = firstCode + histogram[currentLength - 1] << 1;
+ currentCode = firstCode;
+ currentTemp = 0;
+
+ while (currentTemp < linesLength) {
+ line = lines[currentTemp];
+
+ if (line.prefixLength === currentLength) {
+ line.prefixCode = currentCode;
+ currentCode++;
+ }
+
+ currentTemp++;
+ }
+
+ currentLength++;
+ }
+ }
+ };
+
+ function decodeTablesSegment(data, start, end) {
+ var flags = data[start];
+ var lowestValue = (0, _util.readUint32)(data, start + 1) & 0xFFFFFFFF;
+ var highestValue = (0, _util.readUint32)(data, start + 5) & 0xFFFFFFFF;
+ var reader = new Reader(data, start + 9, end);
+ var prefixSizeBits = (flags >> 1 & 7) + 1;
+ var rangeSizeBits = (flags >> 4 & 7) + 1;
+ var lines = [];
+ var prefixLength,
+ rangeLength,
+ currentRangeLow = lowestValue;
+
+ do {
+ prefixLength = reader.readBits(prefixSizeBits);
+ rangeLength = reader.readBits(rangeSizeBits);
+ lines.push(new HuffmanLine([currentRangeLow, prefixLength, rangeLength, 0]));
+ currentRangeLow += 1 << rangeLength;
+ } while (currentRangeLow < highestValue);
+
+ prefixLength = reader.readBits(prefixSizeBits);
+ lines.push(new HuffmanLine([lowestValue - 1, prefixLength, 32, 0, 'lower']));
+ prefixLength = reader.readBits(prefixSizeBits);
+ lines.push(new HuffmanLine([highestValue, prefixLength, 32, 0]));
+
+ if (flags & 1) {
+ prefixLength = reader.readBits(prefixSizeBits);
+ lines.push(new HuffmanLine([prefixLength, 0]));
+ }
+
+ return new HuffmanTable(lines, false);
+ }
+
+ var standardTablesCache = {};
+
+ function getStandardTable(number) {
+ var table = standardTablesCache[number];
+
+ if (table) {
+ return table;
+ }
+
+ var lines;
+
+ switch (number) {
+ case 1:
+ lines = [[0, 1, 4, 0x0], [16, 2, 8, 0x2], [272, 3, 16, 0x6], [65808, 3, 32, 0x7]];
+ break;
+
+ case 2:
+ lines = [[0, 1, 0, 0x0], [1, 2, 0, 0x2], [2, 3, 0, 0x6], [3, 4, 3, 0xE], [11, 5, 6, 0x1E], [75, 6, 32, 0x3E], [6, 0x3F]];
+ break;
+
+ case 3:
+ lines = [[-256, 8, 8, 0xFE], [0, 1, 0, 0x0], [1, 2, 0, 0x2], [2, 3, 0, 0x6], [3, 4, 3, 0xE], [11, 5, 6, 0x1E], [-257, 8, 32, 0xFF, 'lower'], [75, 7, 32, 0x7E], [6, 0x3E]];
+ break;
+
+ case 4:
+ lines = [[1, 1, 0, 0x0], [2, 2, 0, 0x2], [3, 3, 0, 0x6], [4, 4, 3, 0xE], [12, 5, 6, 0x1E], [76, 5, 32, 0x1F]];
+ break;
+
+ case 5:
+ lines = [[-255, 7, 8, 0x7E], [1, 1, 0, 0x0], [2, 2, 0, 0x2], [3, 3, 0, 0x6], [4, 4, 3, 0xE], [12, 5, 6, 0x1E], [-256, 7, 32, 0x7F, 'lower'], [76, 6, 32, 0x3E]];
+ break;
+
+ case 6:
+ lines = [[-2048, 5, 10, 0x1C], [-1024, 4, 9, 0x8], [-512, 4, 8, 0x9], [-256, 4, 7, 0xA], [-128, 5, 6, 0x1D], [-64, 5, 5, 0x1E], [-32, 4, 5, 0xB], [0, 2, 7, 0x0], [128, 3, 7, 0x2], [256, 3, 8, 0x3], [512, 4, 9, 0xC], [1024, 4, 10, 0xD], [-2049, 6, 32, 0x3E, 'lower'], [2048, 6, 32, 0x3F]];
+ break;
+
+ case 7:
+ lines = [[-1024, 4, 9, 0x8], [-512, 3, 8, 0x0], [-256, 4, 7, 0x9], [-128, 5, 6, 0x1A], [-64, 5, 5, 0x1B], [-32, 4, 5, 0xA], [0, 4, 5, 0xB], [32, 5, 5, 0x1C], [64, 5, 6, 0x1D], [128, 4, 7, 0xC], [256, 3, 8, 0x1], [512, 3, 9, 0x2], [1024, 3, 10, 0x3], [-1025, 5, 32, 0x1E, 'lower'], [2048, 5, 32, 0x1F]];
+ break;
+
+ case 8:
+ lines = [[-15, 8, 3, 0xFC], [-7, 9, 1, 0x1FC], [-5, 8, 1, 0xFD], [-3, 9, 0, 0x1FD], [-2, 7, 0, 0x7C], [-1, 4, 0, 0xA], [0, 2, 1, 0x0], [2, 5, 0, 0x1A], [3, 6, 0, 0x3A], [4, 3, 4, 0x4], [20, 6, 1, 0x3B], [22, 4, 4, 0xB], [38, 4, 5, 0xC], [70, 5, 6, 0x1B], [134, 5, 7, 0x1C], [262, 6, 7, 0x3C], [390, 7, 8, 0x7D], [646, 6, 10, 0x3D], [-16, 9, 32, 0x1FE, 'lower'], [1670, 9, 32, 0x1FF], [2, 0x1]];
+ break;
+
+ case 9:
+ lines = [[-31, 8, 4, 0xFC], [-15, 9, 2, 0x1FC], [-11, 8, 2, 0xFD], [-7, 9, 1, 0x1FD], [-5, 7, 1, 0x7C], [-3, 4, 1, 0xA], [-1, 3, 1, 0x2], [1, 3, 1, 0x3], [3, 5, 1, 0x1A], [5, 6, 1, 0x3A], [7, 3, 5, 0x4], [39, 6, 2, 0x3B], [43, 4, 5, 0xB], [75, 4, 6, 0xC], [139, 5, 7, 0x1B], [267, 5, 8, 0x1C], [523, 6, 8, 0x3C], [779, 7, 9, 0x7D], [1291, 6, 11, 0x3D], [-32, 9, 32, 0x1FE, 'lower'], [3339, 9, 32, 0x1FF], [2, 0x0]];
+ break;
+
+ case 10:
+ lines = [[-21, 7, 4, 0x7A], [-5, 8, 0, 0xFC], [-4, 7, 0, 0x7B], [-3, 5, 0, 0x18], [-2, 2, 2, 0x0], [2, 5, 0, 0x19], [3, 6, 0, 0x36], [4, 7, 0, 0x7C], [5, 8, 0, 0xFD], [6, 2, 6, 0x1], [70, 5, 5, 0x1A], [102, 6, 5, 0x37], [134, 6, 6, 0x38], [198, 6, 7, 0x39], [326, 6, 8, 0x3A], [582, 6, 9, 0x3B], [1094, 6, 10, 0x3C], [2118, 7, 11, 0x7D], [-22, 8, 32, 0xFE, 'lower'], [4166, 8, 32, 0xFF], [2, 0x2]];
+ break;
+
+ case 11:
+ lines = [[1, 1, 0, 0x0], [2, 2, 1, 0x2], [4, 4, 0, 0xC], [5, 4, 1, 0xD], [7, 5, 1, 0x1C], [9, 5, 2, 0x1D], [13, 6, 2, 0x3C], [17, 7, 2, 0x7A], [21, 7, 3, 0x7B], [29, 7, 4, 0x7C], [45, 7, 5, 0x7D], [77, 7, 6, 0x7E], [141, 7, 32, 0x7F]];
+ break;
+
+ case 12:
+ lines = [[1, 1, 0, 0x0], [2, 2, 0, 0x2], [3, 3, 1, 0x6], [5, 5, 0, 0x1C], [6, 5, 1, 0x1D], [8, 6, 1, 0x3C], [10, 7, 0, 0x7A], [11, 7, 1, 0x7B], [13, 7, 2, 0x7C], [17, 7, 3, 0x7D], [25, 7, 4, 0x7E], [41, 8, 5, 0xFE], [73, 8, 32, 0xFF]];
+ break;
+
+ case 13:
+ lines = [[1, 1, 0, 0x0], [2, 3, 0, 0x4], [3, 4, 0, 0xC], [4, 5, 0, 0x1C], [5, 4, 1, 0xD], [7, 3, 3, 0x5], [15, 6, 1, 0x3A], [17, 6, 2, 0x3B], [21, 6, 3, 0x3C], [29, 6, 4, 0x3D], [45, 6, 5, 0x3E], [77, 7, 6, 0x7E], [141, 7, 32, 0x7F]];
+ break;
+
+ case 14:
+ lines = [[-2, 3, 0, 0x4], [-1, 3, 0, 0x5], [0, 1, 0, 0x0], [1, 3, 0, 0x6], [2, 3, 0, 0x7]];
+ break;
+
+ case 15:
+ lines = [[-24, 7, 4, 0x7C], [-8, 6, 2, 0x3C], [-4, 5, 1, 0x1C], [-2, 4, 0, 0xC], [-1, 3, 0, 0x4], [0, 1, 0, 0x0], [1, 3, 0, 0x5], [2, 4, 0, 0xD], [3, 5, 1, 0x1D], [5, 6, 2, 0x3D], [9, 7, 4, 0x7D], [-25, 7, 32, 0x7E, 'lower'], [25, 7, 32, 0x7F]];
+ break;
+
+ default:
+ throw new Jbig2Error("standard table B.".concat(number, " does not exist"));
+ }
+
+ var length = lines.length,
+ i;
+
+ for (i = 0; i < length; i++) {
+ lines[i] = new HuffmanLine(lines[i]);
+ }
+
+ table = new HuffmanTable(lines, true);
+ standardTablesCache[number] = table;
+ return table;
+ }
+
+ function Reader(data, start, end) {
+ this.data = data;
+ this.start = start;
+ this.end = end;
+ this.position = start;
+ this.shift = -1;
+ this.currentByte = 0;
+ }
+
+ Reader.prototype = {
+ readBit: function readBit() {
+ if (this.shift < 0) {
+ if (this.position >= this.end) {
+ throw new Jbig2Error('end of data while reading bit');
+ }
+
+ this.currentByte = this.data[this.position++];
+ this.shift = 7;
+ }
+
+ var bit = this.currentByte >> this.shift & 1;
+ this.shift--;
+ return bit;
+ },
+ readBits: function readBits(numBits) {
+ var result = 0,
+ i;
+
+ for (i = numBits - 1; i >= 0; i--) {
+ result |= this.readBit() << i;
+ }
+
+ return result;
+ },
+ byteAlign: function byteAlign() {
+ this.shift = -1;
+ },
+ next: function next() {
+ if (this.position >= this.end) {
+ return -1;
+ }
+
+ return this.data[this.position++];
+ }
+ };
+
+ function getCustomHuffmanTable(index, referredTo, customTables) {
+ var currentIndex = 0,
+ i,
+ ii = referredTo.length,
+ table;
+
+ for (i = 0; i < ii; i++) {
+ table = customTables[referredTo[i]];
+
+ if (table) {
+ if (index === currentIndex) {
+ return table;
+ }
+
+ currentIndex++;
+ }
+ }
+
+ throw new Jbig2Error('can\'t find custom Huffman table');
+ }
+
+ function getTextRegionHuffmanTables(textRegion, referredTo, customTables, numberOfSymbols, reader) {
+ var codes = [],
+ i,
+ codeLength;
+
+ for (i = 0; i <= 34; i++) {
+ codeLength = reader.readBits(4);
+ codes.push(new HuffmanLine([i, codeLength, 0, 0]));
+ }
+
+ var runCodesTable = new HuffmanTable(codes, false);
+ codes.length = 0;
+
+ for (i = 0; i < numberOfSymbols;) {
+ codeLength = runCodesTable.decode(reader);
+
+ if (codeLength >= 32) {
+ var repeatedLength = void 0,
+ numberOfRepeats = void 0,
+ j = void 0;
+
+ switch (codeLength) {
+ case 32:
+ if (i === 0) {
+ throw new Jbig2Error('no previous value in symbol ID table');
+ }
+
+ numberOfRepeats = reader.readBits(2) + 3;
+ repeatedLength = codes[i - 1].prefixLength;
+ break;
+
+ case 33:
+ numberOfRepeats = reader.readBits(3) + 3;
+ repeatedLength = 0;
+ break;
+
+ case 34:
+ numberOfRepeats = reader.readBits(7) + 11;
+ repeatedLength = 0;
+ break;
+
+ default:
+ throw new Jbig2Error('invalid code length in symbol ID table');
+ }
+
+ for (j = 0; j < numberOfRepeats; j++) {
+ codes.push(new HuffmanLine([i, repeatedLength, 0, 0]));
+ i++;
+ }
+ } else {
+ codes.push(new HuffmanLine([i, codeLength, 0, 0]));
+ i++;
+ }
+ }
+
+ reader.byteAlign();
+ var symbolIDTable = new HuffmanTable(codes, false);
+ var customIndex = 0,
+ tableFirstS,
+ tableDeltaS,
+ tableDeltaT;
+
+ switch (textRegion.huffmanFS) {
+ case 0:
+ case 1:
+ tableFirstS = getStandardTable(textRegion.huffmanFS + 6);
+ break;
+
+ case 3:
+ tableFirstS = getCustomHuffmanTable(customIndex, referredTo, customTables);
+ customIndex++;
+ break;
+
+ default:
+ throw new Jbig2Error('invalid Huffman FS selector');
+ }
+
+ switch (textRegion.huffmanDS) {
+ case 0:
+ case 1:
+ case 2:
+ tableDeltaS = getStandardTable(textRegion.huffmanDS + 8);
+ break;
+
+ case 3:
+ tableDeltaS = getCustomHuffmanTable(customIndex, referredTo, customTables);
+ customIndex++;
+ break;
+
+ default:
+ throw new Jbig2Error('invalid Huffman DS selector');
+ }
+
+ switch (textRegion.huffmanDT) {
+ case 0:
+ case 1:
+ case 2:
+ tableDeltaT = getStandardTable(textRegion.huffmanDT + 11);
+ break;
+
+ case 3:
+ tableDeltaT = getCustomHuffmanTable(customIndex, referredTo, customTables);
+ customIndex++;
+ break;
+
+ default:
+ throw new Jbig2Error('invalid Huffman DT selector');
+ }
+
+ if (textRegion.refinement) {
+ throw new Jbig2Error('refinement with Huffman is not supported');
+ }
+
+ return {
+ symbolIDTable: symbolIDTable,
+ tableFirstS: tableFirstS,
+ tableDeltaS: tableDeltaS,
+ tableDeltaT: tableDeltaT
+ };
+ }
+
+ function getSymbolDictionaryHuffmanTables(dictionary, referredTo, customTables) {
+ var customIndex = 0,
+ tableDeltaHeight,
+ tableDeltaWidth;
+
+ switch (dictionary.huffmanDHSelector) {
+ case 0:
+ case 1:
+ tableDeltaHeight = getStandardTable(dictionary.huffmanDHSelector + 4);
+ break;
+
+ case 3:
+ tableDeltaHeight = getCustomHuffmanTable(customIndex, referredTo, customTables);
+ customIndex++;
+ break;
+
+ default:
+ throw new Jbig2Error('invalid Huffman DH selector');
+ }
+
+ switch (dictionary.huffmanDWSelector) {
+ case 0:
+ case 1:
+ tableDeltaWidth = getStandardTable(dictionary.huffmanDWSelector + 2);
+ break;
+
+ case 3:
+ tableDeltaWidth = getCustomHuffmanTable(customIndex, referredTo, customTables);
+ customIndex++;
+ break;
+
+ default:
+ throw new Jbig2Error('invalid Huffman DW selector');
+ }
+
+ var tableBitmapSize, tableAggregateInstances;
+
+ if (dictionary.bitmapSizeSelector) {
+ tableBitmapSize = getCustomHuffmanTable(customIndex, referredTo, customTables);
+ customIndex++;
+ } else {
+ tableBitmapSize = getStandardTable(1);
+ }
+
+ if (dictionary.aggregationInstancesSelector) {
+ tableAggregateInstances = getCustomHuffmanTable(customIndex, referredTo, customTables);
+ } else {
+ tableAggregateInstances = getStandardTable(1);
+ }
+
+ return {
+ tableDeltaHeight: tableDeltaHeight,
+ tableDeltaWidth: tableDeltaWidth,
+ tableBitmapSize: tableBitmapSize,
+ tableAggregateInstances: tableAggregateInstances
+ };
+ }
+
+ function readUncompressedBitmap(reader, width, height) {
+ var bitmap = [],
+ x,
+ y,
+ row;
+
+ for (y = 0; y < height; y++) {
+ row = new Uint8Array(width);
+ bitmap.push(row);
+
+ for (x = 0; x < width; x++) {
+ row[x] = reader.readBit();
+ }
+
+ reader.byteAlign();
+ }
+
+ return bitmap;
+ }
+
+ function decodeMMRBitmap(input, width, height, endOfBlock) {
+ var params = {
+ K: -1,
+ Columns: width,
+ Rows: height,
+ BlackIs1: true,
+ EndOfBlock: endOfBlock
+ };
+ var decoder = new _ccitt.CCITTFaxDecoder(input, params);
+ var bitmap = [],
+ x,
+ y,
+ row,
+ currentByte,
+ shift,
+ eof = false;
+
+ for (y = 0; y < height; y++) {
+ row = new Uint8Array(width);
+ bitmap.push(row);
+ shift = -1;
+
+ for (x = 0; x < width; x++) {
+ if (shift < 0) {
+ currentByte = decoder.readNextChar();
+
+ if (currentByte === -1) {
+ currentByte = 0;
+ eof = true;
+ }
+
+ shift = 7;
+ }
+
+ row[x] = currentByte >> shift & 1;
+ shift--;
+ }
+ }
+
+ if (endOfBlock && !eof) {
+ var lookForEOFLimit = 5;
+
+ for (var i = 0; i < lookForEOFLimit; i++) {
+ if (decoder.readNextChar() === -1) {
+ break;
+ }
+ }
+ }
+
+ return bitmap;
+ }
+
+ function Jbig2Image() {}
+
+ Jbig2Image.prototype = {
+ parseChunks: function parseChunks(chunks) {
+ return parseJbig2Chunks(chunks);
+ },
+ parse: function parse(data) {
+ var _parseJbig = parseJbig2(data),
+ imgData = _parseJbig.imgData,
+ width = _parseJbig.width,
+ height = _parseJbig.height;
+
+ this.width = width;
+ this.height = height;
+ return imgData;
+ }
+ };
+ return Jbig2Image;
+}();
+
+exports.Jbig2Image = Jbig2Image;
+
+/***/ }),
+/* 163 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ArithmeticDecoder = void 0;
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var QeTable = [{
+ qe: 0x5601,
+ nmps: 1,
+ nlps: 1,
+ switchFlag: 1
+}, {
+ qe: 0x3401,
+ nmps: 2,
+ nlps: 6,
+ switchFlag: 0
+}, {
+ qe: 0x1801,
+ nmps: 3,
+ nlps: 9,
+ switchFlag: 0
+}, {
+ qe: 0x0AC1,
+ nmps: 4,
+ nlps: 12,
+ switchFlag: 0
+}, {
+ qe: 0x0521,
+ nmps: 5,
+ nlps: 29,
+ switchFlag: 0
+}, {
+ qe: 0x0221,
+ nmps: 38,
+ nlps: 33,
+ switchFlag: 0
+}, {
+ qe: 0x5601,
+ nmps: 7,
+ nlps: 6,
+ switchFlag: 1
+}, {
+ qe: 0x5401,
+ nmps: 8,
+ nlps: 14,
+ switchFlag: 0
+}, {
+ qe: 0x4801,
+ nmps: 9,
+ nlps: 14,
+ switchFlag: 0
+}, {
+ qe: 0x3801,
+ nmps: 10,
+ nlps: 14,
+ switchFlag: 0
+}, {
+ qe: 0x3001,
+ nmps: 11,
+ nlps: 17,
+ switchFlag: 0
+}, {
+ qe: 0x2401,
+ nmps: 12,
+ nlps: 18,
+ switchFlag: 0
+}, {
+ qe: 0x1C01,
+ nmps: 13,
+ nlps: 20,
+ switchFlag: 0
+}, {
+ qe: 0x1601,
+ nmps: 29,
+ nlps: 21,
+ switchFlag: 0
+}, {
+ qe: 0x5601,
+ nmps: 15,
+ nlps: 14,
+ switchFlag: 1
+}, {
+ qe: 0x5401,
+ nmps: 16,
+ nlps: 14,
+ switchFlag: 0
+}, {
+ qe: 0x5101,
+ nmps: 17,
+ nlps: 15,
+ switchFlag: 0
+}, {
+ qe: 0x4801,
+ nmps: 18,
+ nlps: 16,
+ switchFlag: 0
+}, {
+ qe: 0x3801,
+ nmps: 19,
+ nlps: 17,
+ switchFlag: 0
+}, {
+ qe: 0x3401,
+ nmps: 20,
+ nlps: 18,
+ switchFlag: 0
+}, {
+ qe: 0x3001,
+ nmps: 21,
+ nlps: 19,
+ switchFlag: 0
+}, {
+ qe: 0x2801,
+ nmps: 22,
+ nlps: 19,
+ switchFlag: 0
+}, {
+ qe: 0x2401,
+ nmps: 23,
+ nlps: 20,
+ switchFlag: 0
+}, {
+ qe: 0x2201,
+ nmps: 24,
+ nlps: 21,
+ switchFlag: 0
+}, {
+ qe: 0x1C01,
+ nmps: 25,
+ nlps: 22,
+ switchFlag: 0
+}, {
+ qe: 0x1801,
+ nmps: 26,
+ nlps: 23,
+ switchFlag: 0
+}, {
+ qe: 0x1601,
+ nmps: 27,
+ nlps: 24,
+ switchFlag: 0
+}, {
+ qe: 0x1401,
+ nmps: 28,
+ nlps: 25,
+ switchFlag: 0
+}, {
+ qe: 0x1201,
+ nmps: 29,
+ nlps: 26,
+ switchFlag: 0
+}, {
+ qe: 0x1101,
+ nmps: 30,
+ nlps: 27,
+ switchFlag: 0
+}, {
+ qe: 0x0AC1,
+ nmps: 31,
+ nlps: 28,
+ switchFlag: 0
+}, {
+ qe: 0x09C1,
+ nmps: 32,
+ nlps: 29,
+ switchFlag: 0
+}, {
+ qe: 0x08A1,
+ nmps: 33,
+ nlps: 30,
+ switchFlag: 0
+}, {
+ qe: 0x0521,
+ nmps: 34,
+ nlps: 31,
+ switchFlag: 0
+}, {
+ qe: 0x0441,
+ nmps: 35,
+ nlps: 32,
+ switchFlag: 0
+}, {
+ qe: 0x02A1,
+ nmps: 36,
+ nlps: 33,
+ switchFlag: 0
+}, {
+ qe: 0x0221,
+ nmps: 37,
+ nlps: 34,
+ switchFlag: 0
+}, {
+ qe: 0x0141,
+ nmps: 38,
+ nlps: 35,
+ switchFlag: 0
+}, {
+ qe: 0x0111,
+ nmps: 39,
+ nlps: 36,
+ switchFlag: 0
+}, {
+ qe: 0x0085,
+ nmps: 40,
+ nlps: 37,
+ switchFlag: 0
+}, {
+ qe: 0x0049,
+ nmps: 41,
+ nlps: 38,
+ switchFlag: 0
+}, {
+ qe: 0x0025,
+ nmps: 42,
+ nlps: 39,
+ switchFlag: 0
+}, {
+ qe: 0x0015,
+ nmps: 43,
+ nlps: 40,
+ switchFlag: 0
+}, {
+ qe: 0x0009,
+ nmps: 44,
+ nlps: 41,
+ switchFlag: 0
+}, {
+ qe: 0x0005,
+ nmps: 45,
+ nlps: 42,
+ switchFlag: 0
+}, {
+ qe: 0x0001,
+ nmps: 45,
+ nlps: 43,
+ switchFlag: 0
+}, {
+ qe: 0x5601,
+ nmps: 46,
+ nlps: 46,
+ switchFlag: 0
+}];
+
+var ArithmeticDecoder =
+/*#__PURE__*/
+function () {
+ function ArithmeticDecoder(data, start, end) {
+ _classCallCheck(this, ArithmeticDecoder);
+
+ this.data = data;
+ this.bp = start;
+ this.dataEnd = end;
+ this.chigh = data[start];
+ this.clow = 0;
+ this.byteIn();
+ this.chigh = this.chigh << 7 & 0xFFFF | this.clow >> 9 & 0x7F;
+ this.clow = this.clow << 7 & 0xFFFF;
+ this.ct -= 7;
+ this.a = 0x8000;
+ }
+
+ _createClass(ArithmeticDecoder, [{
+ key: "byteIn",
+ value: function byteIn() {
+ var data = this.data;
+ var bp = this.bp;
+
+ if (data[bp] === 0xFF) {
+ if (data[bp + 1] > 0x8F) {
+ this.clow += 0xFF00;
+ this.ct = 8;
+ } else {
+ bp++;
+ this.clow += data[bp] << 9;
+ this.ct = 7;
+ this.bp = bp;
+ }
+ } else {
+ bp++;
+ this.clow += bp < this.dataEnd ? data[bp] << 8 : 0xFF00;
+ this.ct = 8;
+ this.bp = bp;
+ }
+
+ if (this.clow > 0xFFFF) {
+ this.chigh += this.clow >> 16;
+ this.clow &= 0xFFFF;
+ }
+ }
+ }, {
+ key: "readBit",
+ value: function readBit(contexts, pos) {
+ var cx_index = contexts[pos] >> 1,
+ cx_mps = contexts[pos] & 1;
+ var qeTableIcx = QeTable[cx_index];
+ var qeIcx = qeTableIcx.qe;
+ var d;
+ var a = this.a - qeIcx;
+
+ if (this.chigh < qeIcx) {
+ if (a < qeIcx) {
+ a = qeIcx;
+ d = cx_mps;
+ cx_index = qeTableIcx.nmps;
+ } else {
+ a = qeIcx;
+ d = 1 ^ cx_mps;
+
+ if (qeTableIcx.switchFlag === 1) {
+ cx_mps = d;
+ }
+
+ cx_index = qeTableIcx.nlps;
+ }
+ } else {
+ this.chigh -= qeIcx;
+
+ if ((a & 0x8000) !== 0) {
+ this.a = a;
+ return cx_mps;
+ }
+
+ if (a < qeIcx) {
+ d = 1 ^ cx_mps;
+
+ if (qeTableIcx.switchFlag === 1) {
+ cx_mps = d;
+ }
+
+ cx_index = qeTableIcx.nlps;
+ } else {
+ d = cx_mps;
+ cx_index = qeTableIcx.nmps;
+ }
+ }
+
+ do {
+ if (this.ct === 0) {
+ this.byteIn();
+ }
+
+ a <<= 1;
+ this.chigh = this.chigh << 1 & 0xFFFF | this.clow >> 15 & 1;
+ this.clow = this.clow << 1 & 0xFFFF;
+ this.ct--;
+ } while ((a & 0x8000) === 0);
+
+ this.a = a;
+ contexts[pos] = cx_index << 1 | cx_mps;
+ return d;
+ }
+ }]);
+
+ return ArithmeticDecoder;
+}();
+
+exports.ArithmeticDecoder = ArithmeticDecoder;
+
+/***/ }),
+/* 164 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.JpegStream = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _stream = __w_pdfjs_require__(158);
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _jpg = __w_pdfjs_require__(165);
+
+var JpegStream = function JpegStreamClosure() {
+ function JpegStream(stream, maybeLength, dict, params) {
+ var ch;
+
+ while ((ch = stream.getByte()) !== -1) {
+ if (ch === 0xFF) {
+ stream.skip(-1);
+ break;
+ }
+ }
+
+ this.stream = stream;
+ this.maybeLength = maybeLength;
+ this.dict = dict;
+ this.params = params;
+
+ _stream.DecodeStream.call(this, maybeLength);
+ }
+
+ JpegStream.prototype = Object.create(_stream.DecodeStream.prototype);
+ Object.defineProperty(JpegStream.prototype, 'bytes', {
+ get: function JpegStream_bytes() {
+ return (0, _util.shadow)(this, 'bytes', this.stream.getBytes(this.maybeLength));
+ },
+ configurable: true
+ });
+
+ JpegStream.prototype.ensureBuffer = function (requested) {};
+
+ JpegStream.prototype.readBlock = function () {
+ if (this.eof) {
+ return;
+ }
+
+ var jpegOptions = {
+ decodeTransform: undefined,
+ colorTransform: undefined
+ };
+ var decodeArr = this.dict.getArray('Decode', 'D');
+
+ if (this.forceRGB && Array.isArray(decodeArr)) {
+ var bitsPerComponent = this.dict.get('BitsPerComponent') || 8;
+ var decodeArrLength = decodeArr.length;
+ var transform = new Int32Array(decodeArrLength);
+ var transformNeeded = false;
+ var maxValue = (1 << bitsPerComponent) - 1;
+
+ for (var i = 0; i < decodeArrLength; i += 2) {
+ transform[i] = (decodeArr[i + 1] - decodeArr[i]) * 256 | 0;
+ transform[i + 1] = decodeArr[i] * maxValue | 0;
+
+ if (transform[i] !== 256 || transform[i + 1] !== 0) {
+ transformNeeded = true;
+ }
+ }
+
+ if (transformNeeded) {
+ jpegOptions.decodeTransform = transform;
+ }
+ }
+
+ if ((0, _primitives.isDict)(this.params)) {
+ var colorTransform = this.params.get('ColorTransform');
+
+ if (Number.isInteger(colorTransform)) {
+ jpegOptions.colorTransform = colorTransform;
+ }
+ }
+
+ var jpegImage = new _jpg.JpegImage(jpegOptions);
+ jpegImage.parse(this.bytes);
+ var data = jpegImage.getData({
+ width: this.drawWidth,
+ height: this.drawHeight,
+ forceRGB: this.forceRGB,
+ isSourcePDF: true
+ });
+ this.buffer = data;
+ this.bufferLength = data.length;
+ this.eof = true;
+ };
+
+ JpegStream.prototype.getIR = function () {
+ var forceDataSchema = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ return (0, _util.createObjectURL)(this.bytes, 'image/jpeg', forceDataSchema);
+ };
+
+ return JpegStream;
+}();
+
+exports.JpegStream = JpegStream;
+
+/***/ }),
+/* 165 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.JpegImage = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+var JpegError = function JpegErrorClosure() {
+ function JpegError(msg) {
+ this.message = 'JPEG error: ' + msg;
+ }
+
+ JpegError.prototype = new Error();
+ JpegError.prototype.name = 'JpegError';
+ JpegError.constructor = JpegError;
+ return JpegError;
+}();
+
+var DNLMarkerError = function DNLMarkerErrorClosure() {
+ function DNLMarkerError(message, scanLines) {
+ this.message = message;
+ this.scanLines = scanLines;
+ }
+
+ DNLMarkerError.prototype = new Error();
+ DNLMarkerError.prototype.name = 'DNLMarkerError';
+ DNLMarkerError.constructor = DNLMarkerError;
+ return DNLMarkerError;
+}();
+
+var EOIMarkerError = function EOIMarkerErrorClosure() {
+ function EOIMarkerError(message) {
+ this.message = message;
+ }
+
+ EOIMarkerError.prototype = new Error();
+ EOIMarkerError.prototype.name = 'EOIMarkerError';
+ EOIMarkerError.constructor = EOIMarkerError;
+ return EOIMarkerError;
+}();
+
+var JpegImage = function JpegImageClosure() {
+ var dctZigZag = new Uint8Array([0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63]);
+ var dctCos1 = 4017;
+ var dctSin1 = 799;
+ var dctCos3 = 3406;
+ var dctSin3 = 2276;
+ var dctCos6 = 1567;
+ var dctSin6 = 3784;
+ var dctSqrt2 = 5793;
+ var dctSqrt1d2 = 2896;
+
+ function JpegImage() {
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
+ _ref$decodeTransform = _ref.decodeTransform,
+ decodeTransform = _ref$decodeTransform === void 0 ? null : _ref$decodeTransform,
+ _ref$colorTransform = _ref.colorTransform,
+ colorTransform = _ref$colorTransform === void 0 ? -1 : _ref$colorTransform;
+
+ this._decodeTransform = decodeTransform;
+ this._colorTransform = colorTransform;
+ }
+
+ function buildHuffmanTable(codeLengths, values) {
+ var k = 0,
+ code = [],
+ i,
+ j,
+ length = 16;
+
+ while (length > 0 && !codeLengths[length - 1]) {
+ length--;
+ }
+
+ code.push({
+ children: [],
+ index: 0
+ });
+ var p = code[0],
+ q;
+
+ for (i = 0; i < length; i++) {
+ for (j = 0; j < codeLengths[i]; j++) {
+ p = code.pop();
+ p.children[p.index] = values[k];
+
+ while (p.index > 0) {
+ p = code.pop();
+ }
+
+ p.index++;
+ code.push(p);
+
+ while (code.length <= i) {
+ code.push(q = {
+ children: [],
+ index: 0
+ });
+ p.children[p.index] = q.children;
+ p = q;
+ }
+
+ k++;
+ }
+
+ if (i + 1 < length) {
+ code.push(q = {
+ children: [],
+ index: 0
+ });
+ p.children[p.index] = q.children;
+ p = q;
+ }
+ }
+
+ return code[0].children;
+ }
+
+ function getBlockBufferOffset(component, row, col) {
+ return 64 * ((component.blocksPerLine + 1) * row + col);
+ }
+
+ function decodeScan(data, offset, frame, components, resetInterval, spectralStart, spectralEnd, successivePrev, successive) {
+ var parseDNLMarker = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : false;
+ var mcusPerLine = frame.mcusPerLine;
+ var progressive = frame.progressive;
+ var startOffset = offset,
+ bitsData = 0,
+ bitsCount = 0;
+
+ function readBit() {
+ if (bitsCount > 0) {
+ bitsCount--;
+ return bitsData >> bitsCount & 1;
+ }
+
+ bitsData = data[offset++];
+
+ if (bitsData === 0xFF) {
+ var nextByte = data[offset++];
+
+ if (nextByte) {
+ if (nextByte === 0xDC && parseDNLMarker) {
+ offset += 2;
+ var scanLines = data[offset++] << 8 | data[offset++];
+
+ if (scanLines > 0 && scanLines !== frame.scanLines) {
+ throw new DNLMarkerError('Found DNL marker (0xFFDC) while parsing scan data', scanLines);
+ }
+ } else if (nextByte === 0xD9) {
+ throw new EOIMarkerError('Found EOI marker (0xFFD9) while parsing scan data');
+ }
+
+ throw new JpegError("unexpected marker ".concat((bitsData << 8 | nextByte).toString(16)));
+ }
+ }
+
+ bitsCount = 7;
+ return bitsData >>> 7;
+ }
+
+ function decodeHuffman(tree) {
+ var node = tree;
+
+ while (true) {
+ node = node[readBit()];
+
+ if (typeof node === 'number') {
+ return node;
+ }
+
+ if (_typeof(node) !== 'object') {
+ throw new JpegError('invalid huffman sequence');
+ }
+ }
+ }
+
+ function receive(length) {
+ var n = 0;
+
+ while (length > 0) {
+ n = n << 1 | readBit();
+ length--;
+ }
+
+ return n;
+ }
+
+ function receiveAndExtend(length) {
+ if (length === 1) {
+ return readBit() === 1 ? 1 : -1;
+ }
+
+ var n = receive(length);
+
+ if (n >= 1 << length - 1) {
+ return n;
+ }
+
+ return n + (-1 << length) + 1;
+ }
+
+ function decodeBaseline(component, offset) {
+ var t = decodeHuffman(component.huffmanTableDC);
+ var diff = t === 0 ? 0 : receiveAndExtend(t);
+ component.blockData[offset] = component.pred += diff;
+ var k = 1;
+
+ while (k < 64) {
+ var rs = decodeHuffman(component.huffmanTableAC);
+ var s = rs & 15,
+ r = rs >> 4;
+
+ if (s === 0) {
+ if (r < 15) {
+ break;
+ }
+
+ k += 16;
+ continue;
+ }
+
+ k += r;
+ var z = dctZigZag[k];
+ component.blockData[offset + z] = receiveAndExtend(s);
+ k++;
+ }
+ }
+
+ function decodeDCFirst(component, offset) {
+ var t = decodeHuffman(component.huffmanTableDC);
+ var diff = t === 0 ? 0 : receiveAndExtend(t) << successive;
+ component.blockData[offset] = component.pred += diff;
+ }
+
+ function decodeDCSuccessive(component, offset) {
+ component.blockData[offset] |= readBit() << successive;
+ }
+
+ var eobrun = 0;
+
+ function decodeACFirst(component, offset) {
+ if (eobrun > 0) {
+ eobrun--;
+ return;
+ }
+
+ var k = spectralStart,
+ e = spectralEnd;
+
+ while (k <= e) {
+ var rs = decodeHuffman(component.huffmanTableAC);
+ var s = rs & 15,
+ r = rs >> 4;
+
+ if (s === 0) {
+ if (r < 15) {
+ eobrun = receive(r) + (1 << r) - 1;
+ break;
+ }
+
+ k += 16;
+ continue;
+ }
+
+ k += r;
+ var z = dctZigZag[k];
+ component.blockData[offset + z] = receiveAndExtend(s) * (1 << successive);
+ k++;
+ }
+ }
+
+ var successiveACState = 0,
+ successiveACNextValue;
+
+ function decodeACSuccessive(component, offset) {
+ var k = spectralStart;
+ var e = spectralEnd;
+ var r = 0;
+ var s;
+ var rs;
+
+ while (k <= e) {
+ var offsetZ = offset + dctZigZag[k];
+ var sign = component.blockData[offsetZ] < 0 ? -1 : 1;
+
+ switch (successiveACState) {
+ case 0:
+ rs = decodeHuffman(component.huffmanTableAC);
+ s = rs & 15;
+ r = rs >> 4;
+
+ if (s === 0) {
+ if (r < 15) {
+ eobrun = receive(r) + (1 << r);
+ successiveACState = 4;
+ } else {
+ r = 16;
+ successiveACState = 1;
+ }
+ } else {
+ if (s !== 1) {
+ throw new JpegError('invalid ACn encoding');
+ }
+
+ successiveACNextValue = receiveAndExtend(s);
+ successiveACState = r ? 2 : 3;
+ }
+
+ continue;
+
+ case 1:
+ case 2:
+ if (component.blockData[offsetZ]) {
+ component.blockData[offsetZ] += sign * (readBit() << successive);
+ } else {
+ r--;
+
+ if (r === 0) {
+ successiveACState = successiveACState === 2 ? 3 : 0;
+ }
+ }
+
+ break;
+
+ case 3:
+ if (component.blockData[offsetZ]) {
+ component.blockData[offsetZ] += sign * (readBit() << successive);
+ } else {
+ component.blockData[offsetZ] = successiveACNextValue << successive;
+ successiveACState = 0;
+ }
+
+ break;
+
+ case 4:
+ if (component.blockData[offsetZ]) {
+ component.blockData[offsetZ] += sign * (readBit() << successive);
+ }
+
+ break;
+ }
+
+ k++;
+ }
+
+ if (successiveACState === 4) {
+ eobrun--;
+
+ if (eobrun === 0) {
+ successiveACState = 0;
+ }
+ }
+ }
+
+ function decodeMcu(component, decode, mcu, row, col) {
+ var mcuRow = mcu / mcusPerLine | 0;
+ var mcuCol = mcu % mcusPerLine;
+ var blockRow = mcuRow * component.v + row;
+ var blockCol = mcuCol * component.h + col;
+ var offset = getBlockBufferOffset(component, blockRow, blockCol);
+ decode(component, offset);
+ }
+
+ function decodeBlock(component, decode, mcu) {
+ var blockRow = mcu / component.blocksPerLine | 0;
+ var blockCol = mcu % component.blocksPerLine;
+ var offset = getBlockBufferOffset(component, blockRow, blockCol);
+ decode(component, offset);
+ }
+
+ var componentsLength = components.length;
+ var component, i, j, k, n;
+ var decodeFn;
+
+ if (progressive) {
+ if (spectralStart === 0) {
+ decodeFn = successivePrev === 0 ? decodeDCFirst : decodeDCSuccessive;
+ } else {
+ decodeFn = successivePrev === 0 ? decodeACFirst : decodeACSuccessive;
+ }
+ } else {
+ decodeFn = decodeBaseline;
+ }
+
+ var mcu = 0,
+ fileMarker;
+ var mcuExpected;
+
+ if (componentsLength === 1) {
+ mcuExpected = components[0].blocksPerLine * components[0].blocksPerColumn;
+ } else {
+ mcuExpected = mcusPerLine * frame.mcusPerColumn;
+ }
+
+ var h, v;
+
+ while (mcu < mcuExpected) {
+ var mcuToRead = resetInterval ? Math.min(mcuExpected - mcu, resetInterval) : mcuExpected;
+
+ for (i = 0; i < componentsLength; i++) {
+ components[i].pred = 0;
+ }
+
+ eobrun = 0;
+
+ if (componentsLength === 1) {
+ component = components[0];
+
+ for (n = 0; n < mcuToRead; n++) {
+ decodeBlock(component, decodeFn, mcu);
+ mcu++;
+ }
+ } else {
+ for (n = 0; n < mcuToRead; n++) {
+ for (i = 0; i < componentsLength; i++) {
+ component = components[i];
+ h = component.h;
+ v = component.v;
+
+ for (j = 0; j < v; j++) {
+ for (k = 0; k < h; k++) {
+ decodeMcu(component, decodeFn, mcu, j, k);
+ }
+ }
+ }
+
+ mcu++;
+ }
+ }
+
+ bitsCount = 0;
+ fileMarker = findNextFileMarker(data, offset);
+
+ if (fileMarker && fileMarker.invalid) {
+ (0, _util.warn)('decodeScan - unexpected MCU data, current marker is: ' + fileMarker.invalid);
+ offset = fileMarker.offset;
+ }
+
+ var marker = fileMarker && fileMarker.marker;
+
+ if (!marker || marker <= 0xFF00) {
+ throw new JpegError('marker was not found');
+ }
+
+ if (marker >= 0xFFD0 && marker <= 0xFFD7) {
+ offset += 2;
+ } else {
+ break;
+ }
+ }
+
+ fileMarker = findNextFileMarker(data, offset);
+
+ if (fileMarker && fileMarker.invalid) {
+ (0, _util.warn)('decodeScan - unexpected Scan data, current marker is: ' + fileMarker.invalid);
+ offset = fileMarker.offset;
+ }
+
+ return offset - startOffset;
+ }
+
+ function quantizeAndInverse(component, blockBufferOffset, p) {
+ var qt = component.quantizationTable,
+ blockData = component.blockData;
+ var v0, v1, v2, v3, v4, v5, v6, v7;
+ var p0, p1, p2, p3, p4, p5, p6, p7;
+ var t;
+
+ if (!qt) {
+ throw new JpegError('missing required Quantization Table.');
+ }
+
+ for (var row = 0; row < 64; row += 8) {
+ p0 = blockData[blockBufferOffset + row];
+ p1 = blockData[blockBufferOffset + row + 1];
+ p2 = blockData[blockBufferOffset + row + 2];
+ p3 = blockData[blockBufferOffset + row + 3];
+ p4 = blockData[blockBufferOffset + row + 4];
+ p5 = blockData[blockBufferOffset + row + 5];
+ p6 = blockData[blockBufferOffset + row + 6];
+ p7 = blockData[blockBufferOffset + row + 7];
+ p0 *= qt[row];
+
+ if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) === 0) {
+ t = dctSqrt2 * p0 + 512 >> 10;
+ p[row] = t;
+ p[row + 1] = t;
+ p[row + 2] = t;
+ p[row + 3] = t;
+ p[row + 4] = t;
+ p[row + 5] = t;
+ p[row + 6] = t;
+ p[row + 7] = t;
+ continue;
+ }
+
+ p1 *= qt[row + 1];
+ p2 *= qt[row + 2];
+ p3 *= qt[row + 3];
+ p4 *= qt[row + 4];
+ p5 *= qt[row + 5];
+ p6 *= qt[row + 6];
+ p7 *= qt[row + 7];
+ v0 = dctSqrt2 * p0 + 128 >> 8;
+ v1 = dctSqrt2 * p4 + 128 >> 8;
+ v2 = p2;
+ v3 = p6;
+ v4 = dctSqrt1d2 * (p1 - p7) + 128 >> 8;
+ v7 = dctSqrt1d2 * (p1 + p7) + 128 >> 8;
+ v5 = p3 << 4;
+ v6 = p5 << 4;
+ v0 = v0 + v1 + 1 >> 1;
+ v1 = v0 - v1;
+ t = v2 * dctSin6 + v3 * dctCos6 + 128 >> 8;
+ v2 = v2 * dctCos6 - v3 * dctSin6 + 128 >> 8;
+ v3 = t;
+ v4 = v4 + v6 + 1 >> 1;
+ v6 = v4 - v6;
+ v7 = v7 + v5 + 1 >> 1;
+ v5 = v7 - v5;
+ v0 = v0 + v3 + 1 >> 1;
+ v3 = v0 - v3;
+ v1 = v1 + v2 + 1 >> 1;
+ v2 = v1 - v2;
+ t = v4 * dctSin3 + v7 * dctCos3 + 2048 >> 12;
+ v4 = v4 * dctCos3 - v7 * dctSin3 + 2048 >> 12;
+ v7 = t;
+ t = v5 * dctSin1 + v6 * dctCos1 + 2048 >> 12;
+ v5 = v5 * dctCos1 - v6 * dctSin1 + 2048 >> 12;
+ v6 = t;
+ p[row] = v0 + v7;
+ p[row + 7] = v0 - v7;
+ p[row + 1] = v1 + v6;
+ p[row + 6] = v1 - v6;
+ p[row + 2] = v2 + v5;
+ p[row + 5] = v2 - v5;
+ p[row + 3] = v3 + v4;
+ p[row + 4] = v3 - v4;
+ }
+
+ for (var col = 0; col < 8; ++col) {
+ p0 = p[col];
+ p1 = p[col + 8];
+ p2 = p[col + 16];
+ p3 = p[col + 24];
+ p4 = p[col + 32];
+ p5 = p[col + 40];
+ p6 = p[col + 48];
+ p7 = p[col + 56];
+
+ if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) === 0) {
+ t = dctSqrt2 * p0 + 8192 >> 14;
+ t = t < -2040 ? 0 : t >= 2024 ? 255 : t + 2056 >> 4;
+ blockData[blockBufferOffset + col] = t;
+ blockData[blockBufferOffset + col + 8] = t;
+ blockData[blockBufferOffset + col + 16] = t;
+ blockData[blockBufferOffset + col + 24] = t;
+ blockData[blockBufferOffset + col + 32] = t;
+ blockData[blockBufferOffset + col + 40] = t;
+ blockData[blockBufferOffset + col + 48] = t;
+ blockData[blockBufferOffset + col + 56] = t;
+ continue;
+ }
+
+ v0 = dctSqrt2 * p0 + 2048 >> 12;
+ v1 = dctSqrt2 * p4 + 2048 >> 12;
+ v2 = p2;
+ v3 = p6;
+ v4 = dctSqrt1d2 * (p1 - p7) + 2048 >> 12;
+ v7 = dctSqrt1d2 * (p1 + p7) + 2048 >> 12;
+ v5 = p3;
+ v6 = p5;
+ v0 = (v0 + v1 + 1 >> 1) + 4112;
+ v1 = v0 - v1;
+ t = v2 * dctSin6 + v3 * dctCos6 + 2048 >> 12;
+ v2 = v2 * dctCos6 - v3 * dctSin6 + 2048 >> 12;
+ v3 = t;
+ v4 = v4 + v6 + 1 >> 1;
+ v6 = v4 - v6;
+ v7 = v7 + v5 + 1 >> 1;
+ v5 = v7 - v5;
+ v0 = v0 + v3 + 1 >> 1;
+ v3 = v0 - v3;
+ v1 = v1 + v2 + 1 >> 1;
+ v2 = v1 - v2;
+ t = v4 * dctSin3 + v7 * dctCos3 + 2048 >> 12;
+ v4 = v4 * dctCos3 - v7 * dctSin3 + 2048 >> 12;
+ v7 = t;
+ t = v5 * dctSin1 + v6 * dctCos1 + 2048 >> 12;
+ v5 = v5 * dctCos1 - v6 * dctSin1 + 2048 >> 12;
+ v6 = t;
+ p0 = v0 + v7;
+ p7 = v0 - v7;
+ p1 = v1 + v6;
+ p6 = v1 - v6;
+ p2 = v2 + v5;
+ p5 = v2 - v5;
+ p3 = v3 + v4;
+ p4 = v3 - v4;
+ p0 = p0 < 16 ? 0 : p0 >= 4080 ? 255 : p0 >> 4;
+ p1 = p1 < 16 ? 0 : p1 >= 4080 ? 255 : p1 >> 4;
+ p2 = p2 < 16 ? 0 : p2 >= 4080 ? 255 : p2 >> 4;
+ p3 = p3 < 16 ? 0 : p3 >= 4080 ? 255 : p3 >> 4;
+ p4 = p4 < 16 ? 0 : p4 >= 4080 ? 255 : p4 >> 4;
+ p5 = p5 < 16 ? 0 : p5 >= 4080 ? 255 : p5 >> 4;
+ p6 = p6 < 16 ? 0 : p6 >= 4080 ? 255 : p6 >> 4;
+ p7 = p7 < 16 ? 0 : p7 >= 4080 ? 255 : p7 >> 4;
+ blockData[blockBufferOffset + col] = p0;
+ blockData[blockBufferOffset + col + 8] = p1;
+ blockData[blockBufferOffset + col + 16] = p2;
+ blockData[blockBufferOffset + col + 24] = p3;
+ blockData[blockBufferOffset + col + 32] = p4;
+ blockData[blockBufferOffset + col + 40] = p5;
+ blockData[blockBufferOffset + col + 48] = p6;
+ blockData[blockBufferOffset + col + 56] = p7;
+ }
+ }
+
+ function buildComponentData(frame, component) {
+ var blocksPerLine = component.blocksPerLine;
+ var blocksPerColumn = component.blocksPerColumn;
+ var computationBuffer = new Int16Array(64);
+
+ for (var blockRow = 0; blockRow < blocksPerColumn; blockRow++) {
+ for (var blockCol = 0; blockCol < blocksPerLine; blockCol++) {
+ var offset = getBlockBufferOffset(component, blockRow, blockCol);
+ quantizeAndInverse(component, offset, computationBuffer);
+ }
+ }
+
+ return component.blockData;
+ }
+
+ function findNextFileMarker(data, currentPos) {
+ var startPos = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : currentPos;
+
+ function peekUint16(pos) {
+ return data[pos] << 8 | data[pos + 1];
+ }
+
+ var maxPos = data.length - 1;
+ var newPos = startPos < currentPos ? startPos : currentPos;
+
+ if (currentPos >= maxPos) {
+ return null;
+ }
+
+ var currentMarker = peekUint16(currentPos);
+
+ if (currentMarker >= 0xFFC0 && currentMarker <= 0xFFFE) {
+ return {
+ invalid: null,
+ marker: currentMarker,
+ offset: currentPos
+ };
+ }
+
+ var newMarker = peekUint16(newPos);
+
+ while (!(newMarker >= 0xFFC0 && newMarker <= 0xFFFE)) {
+ if (++newPos >= maxPos) {
+ return null;
+ }
+
+ newMarker = peekUint16(newPos);
+ }
+
+ return {
+ invalid: currentMarker.toString(16),
+ marker: newMarker,
+ offset: newPos
+ };
+ }
+
+ JpegImage.prototype = {
+ parse: function parse(data) {
+ var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
+ _ref2$dnlScanLines = _ref2.dnlScanLines,
+ dnlScanLines = _ref2$dnlScanLines === void 0 ? null : _ref2$dnlScanLines;
+
+ function readUint16() {
+ var value = data[offset] << 8 | data[offset + 1];
+ offset += 2;
+ return value;
+ }
+
+ function readDataBlock() {
+ var length = readUint16();
+ var endOffset = offset + length - 2;
+ var fileMarker = findNextFileMarker(data, endOffset, offset);
+
+ if (fileMarker && fileMarker.invalid) {
+ (0, _util.warn)('readDataBlock - incorrect length, current marker is: ' + fileMarker.invalid);
+ endOffset = fileMarker.offset;
+ }
+
+ var array = data.subarray(offset, endOffset);
+ offset += array.length;
+ return array;
+ }
+
+ function prepareComponents(frame) {
+ var mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / frame.maxH);
+ var mcusPerColumn = Math.ceil(frame.scanLines / 8 / frame.maxV);
+
+ for (var i = 0; i < frame.components.length; i++) {
+ component = frame.components[i];
+ var blocksPerLine = Math.ceil(Math.ceil(frame.samplesPerLine / 8) * component.h / frame.maxH);
+ var blocksPerColumn = Math.ceil(Math.ceil(frame.scanLines / 8) * component.v / frame.maxV);
+ var blocksPerLineForMcu = mcusPerLine * component.h;
+ var blocksPerColumnForMcu = mcusPerColumn * component.v;
+ var blocksBufferSize = 64 * blocksPerColumnForMcu * (blocksPerLineForMcu + 1);
+ component.blockData = new Int16Array(blocksBufferSize);
+ component.blocksPerLine = blocksPerLine;
+ component.blocksPerColumn = blocksPerColumn;
+ }
+
+ frame.mcusPerLine = mcusPerLine;
+ frame.mcusPerColumn = mcusPerColumn;
+ }
+
+ var offset = 0;
+ var jfif = null;
+ var adobe = null;
+ var frame, resetInterval;
+ var numSOSMarkers = 0;
+ var quantizationTables = [];
+ var huffmanTablesAC = [],
+ huffmanTablesDC = [];
+ var fileMarker = readUint16();
+
+ if (fileMarker !== 0xFFD8) {
+ throw new JpegError('SOI not found');
+ }
+
+ fileMarker = readUint16();
+
+ markerLoop: while (fileMarker !== 0xFFD9) {
+ var i, j, l;
+
+ switch (fileMarker) {
+ case 0xFFE0:
+ case 0xFFE1:
+ case 0xFFE2:
+ case 0xFFE3:
+ case 0xFFE4:
+ case 0xFFE5:
+ case 0xFFE6:
+ case 0xFFE7:
+ case 0xFFE8:
+ case 0xFFE9:
+ case 0xFFEA:
+ case 0xFFEB:
+ case 0xFFEC:
+ case 0xFFED:
+ case 0xFFEE:
+ case 0xFFEF:
+ case 0xFFFE:
+ var appData = readDataBlock();
+
+ if (fileMarker === 0xFFE0) {
+ if (appData[0] === 0x4A && appData[1] === 0x46 && appData[2] === 0x49 && appData[3] === 0x46 && appData[4] === 0) {
+ jfif = {
+ version: {
+ major: appData[5],
+ minor: appData[6]
+ },
+ densityUnits: appData[7],
+ xDensity: appData[8] << 8 | appData[9],
+ yDensity: appData[10] << 8 | appData[11],
+ thumbWidth: appData[12],
+ thumbHeight: appData[13],
+ thumbData: appData.subarray(14, 14 + 3 * appData[12] * appData[13])
+ };
+ }
+ }
+
+ if (fileMarker === 0xFFEE) {
+ if (appData[0] === 0x41 && appData[1] === 0x64 && appData[2] === 0x6F && appData[3] === 0x62 && appData[4] === 0x65) {
+ adobe = {
+ version: appData[5] << 8 | appData[6],
+ flags0: appData[7] << 8 | appData[8],
+ flags1: appData[9] << 8 | appData[10],
+ transformCode: appData[11]
+ };
+ }
+ }
+
+ break;
+
+ case 0xFFDB:
+ var quantizationTablesLength = readUint16();
+ var quantizationTablesEnd = quantizationTablesLength + offset - 2;
+ var z;
+
+ while (offset < quantizationTablesEnd) {
+ var quantizationTableSpec = data[offset++];
+ var tableData = new Uint16Array(64);
+
+ if (quantizationTableSpec >> 4 === 0) {
+ for (j = 0; j < 64; j++) {
+ z = dctZigZag[j];
+ tableData[z] = data[offset++];
+ }
+ } else if (quantizationTableSpec >> 4 === 1) {
+ for (j = 0; j < 64; j++) {
+ z = dctZigZag[j];
+ tableData[z] = readUint16();
+ }
+ } else {
+ throw new JpegError('DQT - invalid table spec');
+ }
+
+ quantizationTables[quantizationTableSpec & 15] = tableData;
+ }
+
+ break;
+
+ case 0xFFC0:
+ case 0xFFC1:
+ case 0xFFC2:
+ if (frame) {
+ throw new JpegError('Only single frame JPEGs supported');
+ }
+
+ readUint16();
+ frame = {};
+ frame.extended = fileMarker === 0xFFC1;
+ frame.progressive = fileMarker === 0xFFC2;
+ frame.precision = data[offset++];
+ var sofScanLines = readUint16();
+ frame.scanLines = dnlScanLines || sofScanLines;
+ frame.samplesPerLine = readUint16();
+ frame.components = [];
+ frame.componentIds = {};
+ var componentsCount = data[offset++],
+ componentId;
+ var maxH = 0,
+ maxV = 0;
+
+ for (i = 0; i < componentsCount; i++) {
+ componentId = data[offset];
+ var h = data[offset + 1] >> 4;
+ var v = data[offset + 1] & 15;
+
+ if (maxH < h) {
+ maxH = h;
+ }
+
+ if (maxV < v) {
+ maxV = v;
+ }
+
+ var qId = data[offset + 2];
+ l = frame.components.push({
+ h: h,
+ v: v,
+ quantizationId: qId,
+ quantizationTable: null
+ });
+ frame.componentIds[componentId] = l - 1;
+ offset += 3;
+ }
+
+ frame.maxH = maxH;
+ frame.maxV = maxV;
+ prepareComponents(frame);
+ break;
+
+ case 0xFFC4:
+ var huffmanLength = readUint16();
+
+ for (i = 2; i < huffmanLength;) {
+ var huffmanTableSpec = data[offset++];
+ var codeLengths = new Uint8Array(16);
+ var codeLengthSum = 0;
+
+ for (j = 0; j < 16; j++, offset++) {
+ codeLengthSum += codeLengths[j] = data[offset];
+ }
+
+ var huffmanValues = new Uint8Array(codeLengthSum);
+
+ for (j = 0; j < codeLengthSum; j++, offset++) {
+ huffmanValues[j] = data[offset];
+ }
+
+ i += 17 + codeLengthSum;
+ (huffmanTableSpec >> 4 === 0 ? huffmanTablesDC : huffmanTablesAC)[huffmanTableSpec & 15] = buildHuffmanTable(codeLengths, huffmanValues);
+ }
+
+ break;
+
+ case 0xFFDD:
+ readUint16();
+ resetInterval = readUint16();
+ break;
+
+ case 0xFFDA:
+ var parseDNLMarker = ++numSOSMarkers === 1 && !dnlScanLines;
+ readUint16();
+ var selectorsCount = data[offset++];
+ var components = [],
+ component;
+
+ for (i = 0; i < selectorsCount; i++) {
+ var componentIndex = frame.componentIds[data[offset++]];
+ component = frame.components[componentIndex];
+ var tableSpec = data[offset++];
+ component.huffmanTableDC = huffmanTablesDC[tableSpec >> 4];
+ component.huffmanTableAC = huffmanTablesAC[tableSpec & 15];
+ components.push(component);
+ }
+
+ var spectralStart = data[offset++];
+ var spectralEnd = data[offset++];
+ var successiveApproximation = data[offset++];
+
+ try {
+ var processed = decodeScan(data, offset, frame, components, resetInterval, spectralStart, spectralEnd, successiveApproximation >> 4, successiveApproximation & 15, parseDNLMarker);
+ offset += processed;
+ } catch (ex) {
+ if (ex instanceof DNLMarkerError) {
+ (0, _util.warn)("".concat(ex.message, " -- attempting to re-parse the JPEG image."));
+ return this.parse(data, {
+ dnlScanLines: ex.scanLines
+ });
+ } else if (ex instanceof EOIMarkerError) {
+ (0, _util.warn)("".concat(ex.message, " -- ignoring the rest of the image data."));
+ break markerLoop;
+ }
+
+ throw ex;
+ }
+
+ break;
+
+ case 0xFFDC:
+ offset += 4;
+ break;
+
+ case 0xFFFF:
+ if (data[offset] !== 0xFF) {
+ offset--;
+ }
+
+ break;
+
+ default:
+ if (data[offset - 3] === 0xFF && data[offset - 2] >= 0xC0 && data[offset - 2] <= 0xFE) {
+ offset -= 3;
+ break;
+ }
+
+ var nextFileMarker = findNextFileMarker(data, offset - 2);
+
+ if (nextFileMarker && nextFileMarker.invalid) {
+ (0, _util.warn)('JpegImage.parse - unexpected data, current marker is: ' + nextFileMarker.invalid);
+ offset = nextFileMarker.offset;
+ break;
+ }
+
+ throw new JpegError('unknown marker ' + fileMarker.toString(16));
+ }
+
+ fileMarker = readUint16();
+ }
+
+ this.width = frame.samplesPerLine;
+ this.height = frame.scanLines;
+ this.jfif = jfif;
+ this.adobe = adobe;
+ this.components = [];
+
+ for (i = 0; i < frame.components.length; i++) {
+ component = frame.components[i];
+ var quantizationTable = quantizationTables[component.quantizationId];
+
+ if (quantizationTable) {
+ component.quantizationTable = quantizationTable;
+ }
+
+ this.components.push({
+ output: buildComponentData(frame, component),
+ scaleX: component.h / frame.maxH,
+ scaleY: component.v / frame.maxV,
+ blocksPerLine: component.blocksPerLine,
+ blocksPerColumn: component.blocksPerColumn
+ });
+ }
+
+ this.numComponents = this.components.length;
+ return undefined;
+ },
+ _getLinearizedBlockData: function _getLinearizedBlockData(width, height) {
+ var isSourcePDF = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+ var scaleX = this.width / width,
+ scaleY = this.height / height;
+ var component, componentScaleX, componentScaleY, blocksPerScanline;
+ var x, y, i, j, k;
+ var index;
+ var offset = 0;
+ var output;
+ var numComponents = this.components.length;
+ var dataLength = width * height * numComponents;
+ var data = new Uint8ClampedArray(dataLength);
+ var xScaleBlockOffset = new Uint32Array(width);
+ var mask3LSB = 0xfffffff8;
+
+ for (i = 0; i < numComponents; i++) {
+ component = this.components[i];
+ componentScaleX = component.scaleX * scaleX;
+ componentScaleY = component.scaleY * scaleY;
+ offset = i;
+ output = component.output;
+ blocksPerScanline = component.blocksPerLine + 1 << 3;
+
+ for (x = 0; x < width; x++) {
+ j = 0 | x * componentScaleX;
+ xScaleBlockOffset[x] = (j & mask3LSB) << 3 | j & 7;
+ }
+
+ for (y = 0; y < height; y++) {
+ j = 0 | y * componentScaleY;
+ index = blocksPerScanline * (j & mask3LSB) | (j & 7) << 3;
+
+ for (x = 0; x < width; x++) {
+ data[offset] = output[index + xScaleBlockOffset[x]];
+ offset += numComponents;
+ }
+ }
+ }
+
+ var transform = this._decodeTransform;
+
+ if (!isSourcePDF && numComponents === 4 && !transform) {
+ transform = new Int32Array([-256, 255, -256, 255, -256, 255, -256, 255]);
+ }
+
+ if (transform) {
+ for (i = 0; i < dataLength;) {
+ for (j = 0, k = 0; j < numComponents; j++, i++, k += 2) {
+ data[i] = (data[i] * transform[k] >> 8) + transform[k + 1];
+ }
+ }
+ }
+
+ return data;
+ },
+
+ get _isColorConversionNeeded() {
+ if (this.adobe) {
+ return !!this.adobe.transformCode;
+ }
+
+ if (this.numComponents === 3) {
+ if (this._colorTransform === 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ if (this._colorTransform === 1) {
+ return true;
+ }
+
+ return false;
+ },
+
+ _convertYccToRgb: function convertYccToRgb(data) {
+ var Y, Cb, Cr;
+
+ for (var i = 0, length = data.length; i < length; i += 3) {
+ Y = data[i];
+ Cb = data[i + 1];
+ Cr = data[i + 2];
+ data[i] = Y - 179.456 + 1.402 * Cr;
+ data[i + 1] = Y + 135.459 - 0.344 * Cb - 0.714 * Cr;
+ data[i + 2] = Y - 226.816 + 1.772 * Cb;
+ }
+
+ return data;
+ },
+ _convertYcckToRgb: function convertYcckToRgb(data) {
+ var Y, Cb, Cr, k;
+ var offset = 0;
+
+ for (var i = 0, length = data.length; i < length; i += 4) {
+ Y = data[i];
+ Cb = data[i + 1];
+ Cr = data[i + 2];
+ k = data[i + 3];
+ data[offset++] = -122.67195406894 + Cb * (-6.60635669420364e-5 * Cb + 0.000437130475926232 * Cr - 5.4080610064599e-5 * Y + 0.00048449797120281 * k - 0.154362151871126) + Cr * (-0.000957964378445773 * Cr + 0.000817076911346625 * Y - 0.00477271405408747 * k + 1.53380253221734) + Y * (0.000961250184130688 * Y - 0.00266257332283933 * k + 0.48357088451265) + k * (-0.000336197177618394 * k + 0.484791561490776);
+ data[offset++] = 107.268039397724 + Cb * (2.19927104525741e-5 * Cb - 0.000640992018297945 * Cr + 0.000659397001245577 * Y + 0.000426105652938837 * k - 0.176491792462875) + Cr * (-0.000778269941513683 * Cr + 0.00130872261408275 * Y + 0.000770482631801132 * k - 0.151051492775562) + Y * (0.00126935368114843 * Y - 0.00265090189010898 * k + 0.25802910206845) + k * (-0.000318913117588328 * k - 0.213742400323665);
+ data[offset++] = -20.810012546947 + Cb * (-0.000570115196973677 * Cb - 2.63409051004589e-5 * Cr + 0.0020741088115012 * Y - 0.00288260236853442 * k + 0.814272968359295) + Cr * (-1.53496057440975e-5 * Cr - 0.000132689043961446 * Y + 0.000560833691242812 * k - 0.195152027534049) + Y * (0.00174418132927582 * Y - 0.00255243321439347 * k + 0.116935020465145) + k * (-0.000343531996510555 * k + 0.24165260232407);
+ }
+
+ return data.subarray(0, offset);
+ },
+ _convertYcckToCmyk: function convertYcckToCmyk(data) {
+ var Y, Cb, Cr;
+
+ for (var i = 0, length = data.length; i < length; i += 4) {
+ Y = data[i];
+ Cb = data[i + 1];
+ Cr = data[i + 2];
+ data[i] = 434.456 - Y - 1.402 * Cr;
+ data[i + 1] = 119.541 - Y + 0.344 * Cb + 0.714 * Cr;
+ data[i + 2] = 481.816 - Y - 1.772 * Cb;
+ }
+
+ return data;
+ },
+ _convertCmykToRgb: function convertCmykToRgb(data) {
+ var c, m, y, k;
+ var offset = 0;
+ var scale = 1 / 255;
+
+ for (var i = 0, length = data.length; i < length; i += 4) {
+ c = data[i] * scale;
+ m = data[i + 1] * scale;
+ y = data[i + 2] * scale;
+ k = data[i + 3] * scale;
+ data[offset++] = 255 + c * (-4.387332384609988 * c + 54.48615194189176 * m + 18.82290502165302 * y + 212.25662451639585 * k - 285.2331026137004) + m * (1.7149763477362134 * m - 5.6096736904047315 * y - 17.873870861415444 * k - 5.497006427196366) + y * (-2.5217340131683033 * y - 21.248923337353073 * k + 17.5119270841813) - k * (21.86122147463605 * k + 189.48180835922747);
+ data[offset++] = 255 + c * (8.841041422036149 * c + 60.118027045597366 * m + 6.871425592049007 * y + 31.159100130055922 * k - 79.2970844816548) + m * (-15.310361306967817 * m + 17.575251261109482 * y + 131.35250912493976 * k - 190.9453302588951) + y * (4.444339102852739 * y + 9.8632861493405 * k - 24.86741582555878) - k * (20.737325471181034 * k + 187.80453709719578);
+ data[offset++] = 255 + c * (0.8842522430003296 * c + 8.078677503112928 * m + 30.89978309703729 * y - 0.23883238689178934 * k - 14.183576799673286) + m * (10.49593273432072 * m + 63.02378494754052 * y + 50.606957656360734 * k - 112.23884253719248) + y * (0.03296041114873217 * y + 115.60384449646641 * k - 193.58209356861505) - k * (22.33816807309886 * k + 180.12613974708367);
+ }
+
+ return data.subarray(0, offset);
+ },
+ getData: function getData(_ref3) {
+ var width = _ref3.width,
+ height = _ref3.height,
+ _ref3$forceRGB = _ref3.forceRGB,
+ forceRGB = _ref3$forceRGB === void 0 ? false : _ref3$forceRGB,
+ _ref3$isSourcePDF = _ref3.isSourcePDF,
+ isSourcePDF = _ref3$isSourcePDF === void 0 ? false : _ref3$isSourcePDF;
+
+ if (this.numComponents > 4) {
+ throw new JpegError('Unsupported color mode');
+ }
+
+ var data = this._getLinearizedBlockData(width, height, isSourcePDF);
+
+ if (this.numComponents === 1 && forceRGB) {
+ var dataLength = data.length;
+ var rgbData = new Uint8ClampedArray(dataLength * 3);
+ var offset = 0;
+
+ for (var i = 0; i < dataLength; i++) {
+ var grayColor = data[i];
+ rgbData[offset++] = grayColor;
+ rgbData[offset++] = grayColor;
+ rgbData[offset++] = grayColor;
+ }
+
+ return rgbData;
+ } else if (this.numComponents === 3 && this._isColorConversionNeeded) {
+ return this._convertYccToRgb(data);
+ } else if (this.numComponents === 4) {
+ if (this._isColorConversionNeeded) {
+ if (forceRGB) {
+ return this._convertYcckToRgb(data);
+ }
+
+ return this._convertYcckToCmyk(data);
+ } else if (forceRGB) {
+ return this._convertCmykToRgb(data);
+ }
+ }
+
+ return data;
+ }
+ };
+ return JpegImage;
+}();
+
+exports.JpegImage = JpegImage;
+
+/***/ }),
+/* 166 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.JpxStream = void 0;
+
+var _stream = __w_pdfjs_require__(158);
+
+var _jpx = __w_pdfjs_require__(167);
+
+var _util = __w_pdfjs_require__(5);
+
+var JpxStream = function JpxStreamClosure() {
+ function JpxStream(stream, maybeLength, dict, params) {
+ this.stream = stream;
+ this.maybeLength = maybeLength;
+ this.dict = dict;
+ this.params = params;
+
+ _stream.DecodeStream.call(this, maybeLength);
+ }
+
+ JpxStream.prototype = Object.create(_stream.DecodeStream.prototype);
+ Object.defineProperty(JpxStream.prototype, 'bytes', {
+ get: function JpxStream_bytes() {
+ return (0, _util.shadow)(this, 'bytes', this.stream.getBytes(this.maybeLength));
+ },
+ configurable: true
+ });
+
+ JpxStream.prototype.ensureBuffer = function (requested) {};
+
+ JpxStream.prototype.readBlock = function () {
+ if (this.eof) {
+ return;
+ }
+
+ var jpxImage = new _jpx.JpxImage();
+ jpxImage.parse(this.bytes);
+ var width = jpxImage.width;
+ var height = jpxImage.height;
+ var componentsCount = jpxImage.componentsCount;
+ var tileCount = jpxImage.tiles.length;
+
+ if (tileCount === 1) {
+ this.buffer = jpxImage.tiles[0].items;
+ } else {
+ var data = new Uint8ClampedArray(width * height * componentsCount);
+
+ for (var k = 0; k < tileCount; k++) {
+ var tileComponents = jpxImage.tiles[k];
+ var tileWidth = tileComponents.width;
+ var tileHeight = tileComponents.height;
+ var tileLeft = tileComponents.left;
+ var tileTop = tileComponents.top;
+ var src = tileComponents.items;
+ var srcPosition = 0;
+ var dataPosition = (width * tileTop + tileLeft) * componentsCount;
+ var imgRowSize = width * componentsCount;
+ var tileRowSize = tileWidth * componentsCount;
+
+ for (var j = 0; j < tileHeight; j++) {
+ var rowBytes = src.subarray(srcPosition, srcPosition + tileRowSize);
+ data.set(rowBytes, dataPosition);
+ srcPosition += tileRowSize;
+ dataPosition += imgRowSize;
+ }
+ }
+
+ this.buffer = data;
+ }
+
+ this.bufferLength = this.buffer.length;
+ this.eof = true;
+ };
+
+ return JpxStream;
+}();
+
+exports.JpxStream = JpxStream;
+
+/***/ }),
+/* 167 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.JpxImage = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _arithmetic_decoder = __w_pdfjs_require__(163);
+
+var JpxError = function JpxErrorClosure() {
+ function JpxError(msg) {
+ this.message = 'JPX error: ' + msg;
+ }
+
+ JpxError.prototype = new Error();
+ JpxError.prototype.name = 'JpxError';
+ JpxError.constructor = JpxError;
+ return JpxError;
+}();
+
+var JpxImage = function JpxImageClosure() {
+ var SubbandsGainLog2 = {
+ 'LL': 0,
+ 'LH': 1,
+ 'HL': 1,
+ 'HH': 2
+ };
+
+ function JpxImage() {
+ this.failOnCorruptedImage = false;
+ }
+
+ JpxImage.prototype = {
+ parse: function JpxImage_parse(data) {
+ var head = (0, _util.readUint16)(data, 0);
+
+ if (head === 0xFF4F) {
+ this.parseCodestream(data, 0, data.length);
+ return;
+ }
+
+ var position = 0,
+ length = data.length;
+
+ while (position < length) {
+ var headerSize = 8;
+ var lbox = (0, _util.readUint32)(data, position);
+ var tbox = (0, _util.readUint32)(data, position + 4);
+ position += headerSize;
+
+ if (lbox === 1) {
+ lbox = (0, _util.readUint32)(data, position) * 4294967296 + (0, _util.readUint32)(data, position + 4);
+ position += 8;
+ headerSize += 8;
+ }
+
+ if (lbox === 0) {
+ lbox = length - position + headerSize;
+ }
+
+ if (lbox < headerSize) {
+ throw new JpxError('Invalid box field size');
+ }
+
+ var dataLength = lbox - headerSize;
+ var jumpDataLength = true;
+
+ switch (tbox) {
+ case 0x6A703268:
+ jumpDataLength = false;
+ break;
+
+ case 0x636F6C72:
+ var method = data[position];
+
+ if (method === 1) {
+ var colorspace = (0, _util.readUint32)(data, position + 3);
+
+ switch (colorspace) {
+ case 16:
+ case 17:
+ case 18:
+ break;
+
+ default:
+ (0, _util.warn)('Unknown colorspace ' + colorspace);
+ break;
+ }
+ } else if (method === 2) {
+ (0, _util.info)('ICC profile not supported');
+ }
+
+ break;
+
+ case 0x6A703263:
+ this.parseCodestream(data, position, position + dataLength);
+ break;
+
+ case 0x6A502020:
+ if ((0, _util.readUint32)(data, position) !== 0x0d0a870a) {
+ (0, _util.warn)('Invalid JP2 signature');
+ }
+
+ break;
+
+ case 0x6A501A1A:
+ case 0x66747970:
+ case 0x72726571:
+ case 0x72657320:
+ case 0x69686472:
+ break;
+
+ default:
+ var headerType = String.fromCharCode(tbox >> 24 & 0xFF, tbox >> 16 & 0xFF, tbox >> 8 & 0xFF, tbox & 0xFF);
+ (0, _util.warn)('Unsupported header type ' + tbox + ' (' + headerType + ')');
+ break;
+ }
+
+ if (jumpDataLength) {
+ position += dataLength;
+ }
+ }
+ },
+ parseImageProperties: function JpxImage_parseImageProperties(stream) {
+ var newByte = stream.getByte();
+
+ while (newByte >= 0) {
+ var oldByte = newByte;
+ newByte = stream.getByte();
+ var code = oldByte << 8 | newByte;
+
+ if (code === 0xFF51) {
+ stream.skip(4);
+ var Xsiz = stream.getInt32() >>> 0;
+ var Ysiz = stream.getInt32() >>> 0;
+ var XOsiz = stream.getInt32() >>> 0;
+ var YOsiz = stream.getInt32() >>> 0;
+ stream.skip(16);
+ var Csiz = stream.getUint16();
+ this.width = Xsiz - XOsiz;
+ this.height = Ysiz - YOsiz;
+ this.componentsCount = Csiz;
+ this.bitsPerComponent = 8;
+ return;
+ }
+ }
+
+ throw new JpxError('No size marker found in JPX stream');
+ },
+ parseCodestream: function JpxImage_parseCodestream(data, start, end) {
+ var context = {};
+ var doNotRecover = false;
+
+ try {
+ var position = start;
+
+ while (position + 1 < end) {
+ var code = (0, _util.readUint16)(data, position);
+ position += 2;
+ var length = 0,
+ j,
+ sqcd,
+ spqcds,
+ spqcdSize,
+ scalarExpounded,
+ tile;
+
+ switch (code) {
+ case 0xFF4F:
+ context.mainHeader = true;
+ break;
+
+ case 0xFFD9:
+ break;
+
+ case 0xFF51:
+ length = (0, _util.readUint16)(data, position);
+ var siz = {};
+ siz.Xsiz = (0, _util.readUint32)(data, position + 4);
+ siz.Ysiz = (0, _util.readUint32)(data, position + 8);
+ siz.XOsiz = (0, _util.readUint32)(data, position + 12);
+ siz.YOsiz = (0, _util.readUint32)(data, position + 16);
+ siz.XTsiz = (0, _util.readUint32)(data, position + 20);
+ siz.YTsiz = (0, _util.readUint32)(data, position + 24);
+ siz.XTOsiz = (0, _util.readUint32)(data, position + 28);
+ siz.YTOsiz = (0, _util.readUint32)(data, position + 32);
+ var componentsCount = (0, _util.readUint16)(data, position + 36);
+ siz.Csiz = componentsCount;
+ var components = [];
+ j = position + 38;
+
+ for (var i = 0; i < componentsCount; i++) {
+ var component = {
+ precision: (data[j] & 0x7F) + 1,
+ isSigned: !!(data[j] & 0x80),
+ XRsiz: data[j + 1],
+ YRsiz: data[j + 2]
+ };
+ j += 3;
+ calculateComponentDimensions(component, siz);
+ components.push(component);
+ }
+
+ context.SIZ = siz;
+ context.components = components;
+ calculateTileGrids(context, components);
+ context.QCC = [];
+ context.COC = [];
+ break;
+
+ case 0xFF5C:
+ length = (0, _util.readUint16)(data, position);
+ var qcd = {};
+ j = position + 2;
+ sqcd = data[j++];
+
+ switch (sqcd & 0x1F) {
+ case 0:
+ spqcdSize = 8;
+ scalarExpounded = true;
+ break;
+
+ case 1:
+ spqcdSize = 16;
+ scalarExpounded = false;
+ break;
+
+ case 2:
+ spqcdSize = 16;
+ scalarExpounded = true;
+ break;
+
+ default:
+ throw new Error('Invalid SQcd value ' + sqcd);
+ }
+
+ qcd.noQuantization = spqcdSize === 8;
+ qcd.scalarExpounded = scalarExpounded;
+ qcd.guardBits = sqcd >> 5;
+ spqcds = [];
+
+ while (j < length + position) {
+ var spqcd = {};
+
+ if (spqcdSize === 8) {
+ spqcd.epsilon = data[j++] >> 3;
+ spqcd.mu = 0;
+ } else {
+ spqcd.epsilon = data[j] >> 3;
+ spqcd.mu = (data[j] & 0x7) << 8 | data[j + 1];
+ j += 2;
+ }
+
+ spqcds.push(spqcd);
+ }
+
+ qcd.SPqcds = spqcds;
+
+ if (context.mainHeader) {
+ context.QCD = qcd;
+ } else {
+ context.currentTile.QCD = qcd;
+ context.currentTile.QCC = [];
+ }
+
+ break;
+
+ case 0xFF5D:
+ length = (0, _util.readUint16)(data, position);
+ var qcc = {};
+ j = position + 2;
+ var cqcc;
+
+ if (context.SIZ.Csiz < 257) {
+ cqcc = data[j++];
+ } else {
+ cqcc = (0, _util.readUint16)(data, j);
+ j += 2;
+ }
+
+ sqcd = data[j++];
+
+ switch (sqcd & 0x1F) {
+ case 0:
+ spqcdSize = 8;
+ scalarExpounded = true;
+ break;
+
+ case 1:
+ spqcdSize = 16;
+ scalarExpounded = false;
+ break;
+
+ case 2:
+ spqcdSize = 16;
+ scalarExpounded = true;
+ break;
+
+ default:
+ throw new Error('Invalid SQcd value ' + sqcd);
+ }
+
+ qcc.noQuantization = spqcdSize === 8;
+ qcc.scalarExpounded = scalarExpounded;
+ qcc.guardBits = sqcd >> 5;
+ spqcds = [];
+
+ while (j < length + position) {
+ spqcd = {};
+
+ if (spqcdSize === 8) {
+ spqcd.epsilon = data[j++] >> 3;
+ spqcd.mu = 0;
+ } else {
+ spqcd.epsilon = data[j] >> 3;
+ spqcd.mu = (data[j] & 0x7) << 8 | data[j + 1];
+ j += 2;
+ }
+
+ spqcds.push(spqcd);
+ }
+
+ qcc.SPqcds = spqcds;
+
+ if (context.mainHeader) {
+ context.QCC[cqcc] = qcc;
+ } else {
+ context.currentTile.QCC[cqcc] = qcc;
+ }
+
+ break;
+
+ case 0xFF52:
+ length = (0, _util.readUint16)(data, position);
+ var cod = {};
+ j = position + 2;
+ var scod = data[j++];
+ cod.entropyCoderWithCustomPrecincts = !!(scod & 1);
+ cod.sopMarkerUsed = !!(scod & 2);
+ cod.ephMarkerUsed = !!(scod & 4);
+ cod.progressionOrder = data[j++];
+ cod.layersCount = (0, _util.readUint16)(data, j);
+ j += 2;
+ cod.multipleComponentTransform = data[j++];
+ cod.decompositionLevelsCount = data[j++];
+ cod.xcb = (data[j++] & 0xF) + 2;
+ cod.ycb = (data[j++] & 0xF) + 2;
+ var blockStyle = data[j++];
+ cod.selectiveArithmeticCodingBypass = !!(blockStyle & 1);
+ cod.resetContextProbabilities = !!(blockStyle & 2);
+ cod.terminationOnEachCodingPass = !!(blockStyle & 4);
+ cod.verticallyStripe = !!(blockStyle & 8);
+ cod.predictableTermination = !!(blockStyle & 16);
+ cod.segmentationSymbolUsed = !!(blockStyle & 32);
+ cod.reversibleTransformation = data[j++];
+
+ if (cod.entropyCoderWithCustomPrecincts) {
+ var precinctsSizes = [];
+
+ while (j < length + position) {
+ var precinctsSize = data[j++];
+ precinctsSizes.push({
+ PPx: precinctsSize & 0xF,
+ PPy: precinctsSize >> 4
+ });
+ }
+
+ cod.precinctsSizes = precinctsSizes;
+ }
+
+ var unsupported = [];
+
+ if (cod.selectiveArithmeticCodingBypass) {
+ unsupported.push('selectiveArithmeticCodingBypass');
+ }
+
+ if (cod.resetContextProbabilities) {
+ unsupported.push('resetContextProbabilities');
+ }
+
+ if (cod.terminationOnEachCodingPass) {
+ unsupported.push('terminationOnEachCodingPass');
+ }
+
+ if (cod.verticallyStripe) {
+ unsupported.push('verticallyStripe');
+ }
+
+ if (cod.predictableTermination) {
+ unsupported.push('predictableTermination');
+ }
+
+ if (unsupported.length > 0) {
+ doNotRecover = true;
+ throw new Error('Unsupported COD options (' + unsupported.join(', ') + ')');
+ }
+
+ if (context.mainHeader) {
+ context.COD = cod;
+ } else {
+ context.currentTile.COD = cod;
+ context.currentTile.COC = [];
+ }
+
+ break;
+
+ case 0xFF90:
+ length = (0, _util.readUint16)(data, position);
+ tile = {};
+ tile.index = (0, _util.readUint16)(data, position + 2);
+ tile.length = (0, _util.readUint32)(data, position + 4);
+ tile.dataEnd = tile.length + position - 2;
+ tile.partIndex = data[position + 8];
+ tile.partsCount = data[position + 9];
+ context.mainHeader = false;
+
+ if (tile.partIndex === 0) {
+ tile.COD = context.COD;
+ tile.COC = context.COC.slice(0);
+ tile.QCD = context.QCD;
+ tile.QCC = context.QCC.slice(0);
+ }
+
+ context.currentTile = tile;
+ break;
+
+ case 0xFF93:
+ tile = context.currentTile;
+
+ if (tile.partIndex === 0) {
+ initializeTile(context, tile.index);
+ buildPackets(context);
+ }
+
+ length = tile.dataEnd - position;
+ parseTilePackets(context, data, position, length);
+ break;
+
+ case 0xFF55:
+ case 0xFF57:
+ case 0xFF58:
+ case 0xFF64:
+ length = (0, _util.readUint16)(data, position);
+ break;
+
+ case 0xFF53:
+ throw new Error('Codestream code 0xFF53 (COC) is ' + 'not implemented');
+
+ default:
+ throw new Error('Unknown codestream code: ' + code.toString(16));
+ }
+
+ position += length;
+ }
+ } catch (e) {
+ if (doNotRecover || this.failOnCorruptedImage) {
+ throw new JpxError(e.message);
+ } else {
+ (0, _util.warn)('JPX: Trying to recover from: ' + e.message);
+ }
+ }
+
+ this.tiles = transformComponents(context);
+ this.width = context.SIZ.Xsiz - context.SIZ.XOsiz;
+ this.height = context.SIZ.Ysiz - context.SIZ.YOsiz;
+ this.componentsCount = context.SIZ.Csiz;
+ }
+ };
+
+ function calculateComponentDimensions(component, siz) {
+ component.x0 = Math.ceil(siz.XOsiz / component.XRsiz);
+ component.x1 = Math.ceil(siz.Xsiz / component.XRsiz);
+ component.y0 = Math.ceil(siz.YOsiz / component.YRsiz);
+ component.y1 = Math.ceil(siz.Ysiz / component.YRsiz);
+ component.width = component.x1 - component.x0;
+ component.height = component.y1 - component.y0;
+ }
+
+ function calculateTileGrids(context, components) {
+ var siz = context.SIZ;
+ var tile,
+ tiles = [];
+ var numXtiles = Math.ceil((siz.Xsiz - siz.XTOsiz) / siz.XTsiz);
+ var numYtiles = Math.ceil((siz.Ysiz - siz.YTOsiz) / siz.YTsiz);
+
+ for (var q = 0; q < numYtiles; q++) {
+ for (var p = 0; p < numXtiles; p++) {
+ tile = {};
+ tile.tx0 = Math.max(siz.XTOsiz + p * siz.XTsiz, siz.XOsiz);
+ tile.ty0 = Math.max(siz.YTOsiz + q * siz.YTsiz, siz.YOsiz);
+ tile.tx1 = Math.min(siz.XTOsiz + (p + 1) * siz.XTsiz, siz.Xsiz);
+ tile.ty1 = Math.min(siz.YTOsiz + (q + 1) * siz.YTsiz, siz.Ysiz);
+ tile.width = tile.tx1 - tile.tx0;
+ tile.height = tile.ty1 - tile.ty0;
+ tile.components = [];
+ tiles.push(tile);
+ }
+ }
+
+ context.tiles = tiles;
+ var componentsCount = siz.Csiz;
+
+ for (var i = 0, ii = componentsCount; i < ii; i++) {
+ var component = components[i];
+
+ for (var j = 0, jj = tiles.length; j < jj; j++) {
+ var tileComponent = {};
+ tile = tiles[j];
+ tileComponent.tcx0 = Math.ceil(tile.tx0 / component.XRsiz);
+ tileComponent.tcy0 = Math.ceil(tile.ty0 / component.YRsiz);
+ tileComponent.tcx1 = Math.ceil(tile.tx1 / component.XRsiz);
+ tileComponent.tcy1 = Math.ceil(tile.ty1 / component.YRsiz);
+ tileComponent.width = tileComponent.tcx1 - tileComponent.tcx0;
+ tileComponent.height = tileComponent.tcy1 - tileComponent.tcy0;
+ tile.components[i] = tileComponent;
+ }
+ }
+ }
+
+ function getBlocksDimensions(context, component, r) {
+ var codOrCoc = component.codingStyleParameters;
+ var result = {};
+
+ if (!codOrCoc.entropyCoderWithCustomPrecincts) {
+ result.PPx = 15;
+ result.PPy = 15;
+ } else {
+ result.PPx = codOrCoc.precinctsSizes[r].PPx;
+ result.PPy = codOrCoc.precinctsSizes[r].PPy;
+ }
+
+ result.xcb_ = r > 0 ? Math.min(codOrCoc.xcb, result.PPx - 1) : Math.min(codOrCoc.xcb, result.PPx);
+ result.ycb_ = r > 0 ? Math.min(codOrCoc.ycb, result.PPy - 1) : Math.min(codOrCoc.ycb, result.PPy);
+ return result;
+ }
+
+ function buildPrecincts(context, resolution, dimensions) {
+ var precinctWidth = 1 << dimensions.PPx;
+ var precinctHeight = 1 << dimensions.PPy;
+ var isZeroRes = resolution.resLevel === 0;
+ var precinctWidthInSubband = 1 << dimensions.PPx + (isZeroRes ? 0 : -1);
+ var precinctHeightInSubband = 1 << dimensions.PPy + (isZeroRes ? 0 : -1);
+ var numprecinctswide = resolution.trx1 > resolution.trx0 ? Math.ceil(resolution.trx1 / precinctWidth) - Math.floor(resolution.trx0 / precinctWidth) : 0;
+ var numprecinctshigh = resolution.try1 > resolution.try0 ? Math.ceil(resolution.try1 / precinctHeight) - Math.floor(resolution.try0 / precinctHeight) : 0;
+ var numprecincts = numprecinctswide * numprecinctshigh;
+ resolution.precinctParameters = {
+ precinctWidth: precinctWidth,
+ precinctHeight: precinctHeight,
+ numprecinctswide: numprecinctswide,
+ numprecinctshigh: numprecinctshigh,
+ numprecincts: numprecincts,
+ precinctWidthInSubband: precinctWidthInSubband,
+ precinctHeightInSubband: precinctHeightInSubband
+ };
+ }
+
+ function buildCodeblocks(context, subband, dimensions) {
+ var xcb_ = dimensions.xcb_;
+ var ycb_ = dimensions.ycb_;
+ var codeblockWidth = 1 << xcb_;
+ var codeblockHeight = 1 << ycb_;
+ var cbx0 = subband.tbx0 >> xcb_;
+ var cby0 = subband.tby0 >> ycb_;
+ var cbx1 = subband.tbx1 + codeblockWidth - 1 >> xcb_;
+ var cby1 = subband.tby1 + codeblockHeight - 1 >> ycb_;
+ var precinctParameters = subband.resolution.precinctParameters;
+ var codeblocks = [];
+ var precincts = [];
+ var i, j, codeblock, precinctNumber;
+
+ for (j = cby0; j < cby1; j++) {
+ for (i = cbx0; i < cbx1; i++) {
+ codeblock = {
+ cbx: i,
+ cby: j,
+ tbx0: codeblockWidth * i,
+ tby0: codeblockHeight * j,
+ tbx1: codeblockWidth * (i + 1),
+ tby1: codeblockHeight * (j + 1)
+ };
+ codeblock.tbx0_ = Math.max(subband.tbx0, codeblock.tbx0);
+ codeblock.tby0_ = Math.max(subband.tby0, codeblock.tby0);
+ codeblock.tbx1_ = Math.min(subband.tbx1, codeblock.tbx1);
+ codeblock.tby1_ = Math.min(subband.tby1, codeblock.tby1);
+ var pi = Math.floor((codeblock.tbx0_ - subband.tbx0) / precinctParameters.precinctWidthInSubband);
+ var pj = Math.floor((codeblock.tby0_ - subband.tby0) / precinctParameters.precinctHeightInSubband);
+ precinctNumber = pi + pj * precinctParameters.numprecinctswide;
+ codeblock.precinctNumber = precinctNumber;
+ codeblock.subbandType = subband.type;
+ codeblock.Lblock = 3;
+
+ if (codeblock.tbx1_ <= codeblock.tbx0_ || codeblock.tby1_ <= codeblock.tby0_) {
+ continue;
+ }
+
+ codeblocks.push(codeblock);
+ var precinct = precincts[precinctNumber];
+
+ if (precinct !== undefined) {
+ if (i < precinct.cbxMin) {
+ precinct.cbxMin = i;
+ } else if (i > precinct.cbxMax) {
+ precinct.cbxMax = i;
+ }
+
+ if (j < precinct.cbyMin) {
+ precinct.cbxMin = j;
+ } else if (j > precinct.cbyMax) {
+ precinct.cbyMax = j;
+ }
+ } else {
+ precincts[precinctNumber] = precinct = {
+ cbxMin: i,
+ cbyMin: j,
+ cbxMax: i,
+ cbyMax: j
+ };
+ }
+
+ codeblock.precinct = precinct;
+ }
+ }
+
+ subband.codeblockParameters = {
+ codeblockWidth: xcb_,
+ codeblockHeight: ycb_,
+ numcodeblockwide: cbx1 - cbx0 + 1,
+ numcodeblockhigh: cby1 - cby0 + 1
+ };
+ subband.codeblocks = codeblocks;
+ subband.precincts = precincts;
+ }
+
+ function createPacket(resolution, precinctNumber, layerNumber) {
+ var precinctCodeblocks = [];
+ var subbands = resolution.subbands;
+
+ for (var i = 0, ii = subbands.length; i < ii; i++) {
+ var subband = subbands[i];
+ var codeblocks = subband.codeblocks;
+
+ for (var j = 0, jj = codeblocks.length; j < jj; j++) {
+ var codeblock = codeblocks[j];
+
+ if (codeblock.precinctNumber !== precinctNumber) {
+ continue;
+ }
+
+ precinctCodeblocks.push(codeblock);
+ }
+ }
+
+ return {
+ layerNumber: layerNumber,
+ codeblocks: precinctCodeblocks
+ };
+ }
+
+ function LayerResolutionComponentPositionIterator(context) {
+ var siz = context.SIZ;
+ var tileIndex = context.currentTile.index;
+ var tile = context.tiles[tileIndex];
+ var layersCount = tile.codingStyleDefaultParameters.layersCount;
+ var componentsCount = siz.Csiz;
+ var maxDecompositionLevelsCount = 0;
+
+ for (var q = 0; q < componentsCount; q++) {
+ maxDecompositionLevelsCount = Math.max(maxDecompositionLevelsCount, tile.components[q].codingStyleParameters.decompositionLevelsCount);
+ }
+
+ var l = 0,
+ r = 0,
+ i = 0,
+ k = 0;
+
+ this.nextPacket = function JpxImage_nextPacket() {
+ for (; l < layersCount; l++) {
+ for (; r <= maxDecompositionLevelsCount; r++) {
+ for (; i < componentsCount; i++) {
+ var component = tile.components[i];
+
+ if (r > component.codingStyleParameters.decompositionLevelsCount) {
+ continue;
+ }
+
+ var resolution = component.resolutions[r];
+ var numprecincts = resolution.precinctParameters.numprecincts;
+
+ for (; k < numprecincts;) {
+ var packet = createPacket(resolution, k, l);
+ k++;
+ return packet;
+ }
+
+ k = 0;
+ }
+
+ i = 0;
+ }
+
+ r = 0;
+ }
+
+ throw new JpxError('Out of packets');
+ };
+ }
+
+ function ResolutionLayerComponentPositionIterator(context) {
+ var siz = context.SIZ;
+ var tileIndex = context.currentTile.index;
+ var tile = context.tiles[tileIndex];
+ var layersCount = tile.codingStyleDefaultParameters.layersCount;
+ var componentsCount = siz.Csiz;
+ var maxDecompositionLevelsCount = 0;
+
+ for (var q = 0; q < componentsCount; q++) {
+ maxDecompositionLevelsCount = Math.max(maxDecompositionLevelsCount, tile.components[q].codingStyleParameters.decompositionLevelsCount);
+ }
+
+ var r = 0,
+ l = 0,
+ i = 0,
+ k = 0;
+
+ this.nextPacket = function JpxImage_nextPacket() {
+ for (; r <= maxDecompositionLevelsCount; r++) {
+ for (; l < layersCount; l++) {
+ for (; i < componentsCount; i++) {
+ var component = tile.components[i];
+
+ if (r > component.codingStyleParameters.decompositionLevelsCount) {
+ continue;
+ }
+
+ var resolution = component.resolutions[r];
+ var numprecincts = resolution.precinctParameters.numprecincts;
+
+ for (; k < numprecincts;) {
+ var packet = createPacket(resolution, k, l);
+ k++;
+ return packet;
+ }
+
+ k = 0;
+ }
+
+ i = 0;
+ }
+
+ l = 0;
+ }
+
+ throw new JpxError('Out of packets');
+ };
+ }
+
+ function ResolutionPositionComponentLayerIterator(context) {
+ var siz = context.SIZ;
+ var tileIndex = context.currentTile.index;
+ var tile = context.tiles[tileIndex];
+ var layersCount = tile.codingStyleDefaultParameters.layersCount;
+ var componentsCount = siz.Csiz;
+ var l, r, c, p;
+ var maxDecompositionLevelsCount = 0;
+
+ for (c = 0; c < componentsCount; c++) {
+ var component = tile.components[c];
+ maxDecompositionLevelsCount = Math.max(maxDecompositionLevelsCount, component.codingStyleParameters.decompositionLevelsCount);
+ }
+
+ var maxNumPrecinctsInLevel = new Int32Array(maxDecompositionLevelsCount + 1);
+
+ for (r = 0; r <= maxDecompositionLevelsCount; ++r) {
+ var maxNumPrecincts = 0;
+
+ for (c = 0; c < componentsCount; ++c) {
+ var resolutions = tile.components[c].resolutions;
+
+ if (r < resolutions.length) {
+ maxNumPrecincts = Math.max(maxNumPrecincts, resolutions[r].precinctParameters.numprecincts);
+ }
+ }
+
+ maxNumPrecinctsInLevel[r] = maxNumPrecincts;
+ }
+
+ l = 0;
+ r = 0;
+ c = 0;
+ p = 0;
+
+ this.nextPacket = function JpxImage_nextPacket() {
+ for (; r <= maxDecompositionLevelsCount; r++) {
+ for (; p < maxNumPrecinctsInLevel[r]; p++) {
+ for (; c < componentsCount; c++) {
+ var component = tile.components[c];
+
+ if (r > component.codingStyleParameters.decompositionLevelsCount) {
+ continue;
+ }
+
+ var resolution = component.resolutions[r];
+ var numprecincts = resolution.precinctParameters.numprecincts;
+
+ if (p >= numprecincts) {
+ continue;
+ }
+
+ for (; l < layersCount;) {
+ var packet = createPacket(resolution, p, l);
+ l++;
+ return packet;
+ }
+
+ l = 0;
+ }
+
+ c = 0;
+ }
+
+ p = 0;
+ }
+
+ throw new JpxError('Out of packets');
+ };
+ }
+
+ function PositionComponentResolutionLayerIterator(context) {
+ var siz = context.SIZ;
+ var tileIndex = context.currentTile.index;
+ var tile = context.tiles[tileIndex];
+ var layersCount = tile.codingStyleDefaultParameters.layersCount;
+ var componentsCount = siz.Csiz;
+ var precinctsSizes = getPrecinctSizesInImageScale(tile);
+ var precinctsIterationSizes = precinctsSizes;
+ var l = 0,
+ r = 0,
+ c = 0,
+ px = 0,
+ py = 0;
+
+ this.nextPacket = function JpxImage_nextPacket() {
+ for (; py < precinctsIterationSizes.maxNumHigh; py++) {
+ for (; px < precinctsIterationSizes.maxNumWide; px++) {
+ for (; c < componentsCount; c++) {
+ var component = tile.components[c];
+ var decompositionLevelsCount = component.codingStyleParameters.decompositionLevelsCount;
+
+ for (; r <= decompositionLevelsCount; r++) {
+ var resolution = component.resolutions[r];
+ var sizeInImageScale = precinctsSizes.components[c].resolutions[r];
+ var k = getPrecinctIndexIfExist(px, py, sizeInImageScale, precinctsIterationSizes, resolution);
+
+ if (k === null) {
+ continue;
+ }
+
+ for (; l < layersCount;) {
+ var packet = createPacket(resolution, k, l);
+ l++;
+ return packet;
+ }
+
+ l = 0;
+ }
+
+ r = 0;
+ }
+
+ c = 0;
+ }
+
+ px = 0;
+ }
+
+ throw new JpxError('Out of packets');
+ };
+ }
+
+ function ComponentPositionResolutionLayerIterator(context) {
+ var siz = context.SIZ;
+ var tileIndex = context.currentTile.index;
+ var tile = context.tiles[tileIndex];
+ var layersCount = tile.codingStyleDefaultParameters.layersCount;
+ var componentsCount = siz.Csiz;
+ var precinctsSizes = getPrecinctSizesInImageScale(tile);
+ var l = 0,
+ r = 0,
+ c = 0,
+ px = 0,
+ py = 0;
+
+ this.nextPacket = function JpxImage_nextPacket() {
+ for (; c < componentsCount; ++c) {
+ var component = tile.components[c];
+ var precinctsIterationSizes = precinctsSizes.components[c];
+ var decompositionLevelsCount = component.codingStyleParameters.decompositionLevelsCount;
+
+ for (; py < precinctsIterationSizes.maxNumHigh; py++) {
+ for (; px < precinctsIterationSizes.maxNumWide; px++) {
+ for (; r <= decompositionLevelsCount; r++) {
+ var resolution = component.resolutions[r];
+ var sizeInImageScale = precinctsIterationSizes.resolutions[r];
+ var k = getPrecinctIndexIfExist(px, py, sizeInImageScale, precinctsIterationSizes, resolution);
+
+ if (k === null) {
+ continue;
+ }
+
+ for (; l < layersCount;) {
+ var packet = createPacket(resolution, k, l);
+ l++;
+ return packet;
+ }
+
+ l = 0;
+ }
+
+ r = 0;
+ }
+
+ px = 0;
+ }
+
+ py = 0;
+ }
+
+ throw new JpxError('Out of packets');
+ };
+ }
+
+ function getPrecinctIndexIfExist(pxIndex, pyIndex, sizeInImageScale, precinctIterationSizes, resolution) {
+ var posX = pxIndex * precinctIterationSizes.minWidth;
+ var posY = pyIndex * precinctIterationSizes.minHeight;
+
+ if (posX % sizeInImageScale.width !== 0 || posY % sizeInImageScale.height !== 0) {
+ return null;
+ }
+
+ var startPrecinctRowIndex = posY / sizeInImageScale.width * resolution.precinctParameters.numprecinctswide;
+ return posX / sizeInImageScale.height + startPrecinctRowIndex;
+ }
+
+ function getPrecinctSizesInImageScale(tile) {
+ var componentsCount = tile.components.length;
+ var minWidth = Number.MAX_VALUE;
+ var minHeight = Number.MAX_VALUE;
+ var maxNumWide = 0;
+ var maxNumHigh = 0;
+ var sizePerComponent = new Array(componentsCount);
+
+ for (var c = 0; c < componentsCount; c++) {
+ var component = tile.components[c];
+ var decompositionLevelsCount = component.codingStyleParameters.decompositionLevelsCount;
+ var sizePerResolution = new Array(decompositionLevelsCount + 1);
+ var minWidthCurrentComponent = Number.MAX_VALUE;
+ var minHeightCurrentComponent = Number.MAX_VALUE;
+ var maxNumWideCurrentComponent = 0;
+ var maxNumHighCurrentComponent = 0;
+ var scale = 1;
+
+ for (var r = decompositionLevelsCount; r >= 0; --r) {
+ var resolution = component.resolutions[r];
+ var widthCurrentResolution = scale * resolution.precinctParameters.precinctWidth;
+ var heightCurrentResolution = scale * resolution.precinctParameters.precinctHeight;
+ minWidthCurrentComponent = Math.min(minWidthCurrentComponent, widthCurrentResolution);
+ minHeightCurrentComponent = Math.min(minHeightCurrentComponent, heightCurrentResolution);
+ maxNumWideCurrentComponent = Math.max(maxNumWideCurrentComponent, resolution.precinctParameters.numprecinctswide);
+ maxNumHighCurrentComponent = Math.max(maxNumHighCurrentComponent, resolution.precinctParameters.numprecinctshigh);
+ sizePerResolution[r] = {
+ width: widthCurrentResolution,
+ height: heightCurrentResolution
+ };
+ scale <<= 1;
+ }
+
+ minWidth = Math.min(minWidth, minWidthCurrentComponent);
+ minHeight = Math.min(minHeight, minHeightCurrentComponent);
+ maxNumWide = Math.max(maxNumWide, maxNumWideCurrentComponent);
+ maxNumHigh = Math.max(maxNumHigh, maxNumHighCurrentComponent);
+ sizePerComponent[c] = {
+ resolutions: sizePerResolution,
+ minWidth: minWidthCurrentComponent,
+ minHeight: minHeightCurrentComponent,
+ maxNumWide: maxNumWideCurrentComponent,
+ maxNumHigh: maxNumHighCurrentComponent
+ };
+ }
+
+ return {
+ components: sizePerComponent,
+ minWidth: minWidth,
+ minHeight: minHeight,
+ maxNumWide: maxNumWide,
+ maxNumHigh: maxNumHigh
+ };
+ }
+
+ function buildPackets(context) {
+ var siz = context.SIZ;
+ var tileIndex = context.currentTile.index;
+ var tile = context.tiles[tileIndex];
+ var componentsCount = siz.Csiz;
+
+ for (var c = 0; c < componentsCount; c++) {
+ var component = tile.components[c];
+ var decompositionLevelsCount = component.codingStyleParameters.decompositionLevelsCount;
+ var resolutions = [];
+ var subbands = [];
+
+ for (var r = 0; r <= decompositionLevelsCount; r++) {
+ var blocksDimensions = getBlocksDimensions(context, component, r);
+ var resolution = {};
+ var scale = 1 << decompositionLevelsCount - r;
+ resolution.trx0 = Math.ceil(component.tcx0 / scale);
+ resolution.try0 = Math.ceil(component.tcy0 / scale);
+ resolution.trx1 = Math.ceil(component.tcx1 / scale);
+ resolution.try1 = Math.ceil(component.tcy1 / scale);
+ resolution.resLevel = r;
+ buildPrecincts(context, resolution, blocksDimensions);
+ resolutions.push(resolution);
+ var subband;
+
+ if (r === 0) {
+ subband = {};
+ subband.type = 'LL';
+ subband.tbx0 = Math.ceil(component.tcx0 / scale);
+ subband.tby0 = Math.ceil(component.tcy0 / scale);
+ subband.tbx1 = Math.ceil(component.tcx1 / scale);
+ subband.tby1 = Math.ceil(component.tcy1 / scale);
+ subband.resolution = resolution;
+ buildCodeblocks(context, subband, blocksDimensions);
+ subbands.push(subband);
+ resolution.subbands = [subband];
+ } else {
+ var bscale = 1 << decompositionLevelsCount - r + 1;
+ var resolutionSubbands = [];
+ subband = {};
+ subband.type = 'HL';
+ subband.tbx0 = Math.ceil(component.tcx0 / bscale - 0.5);
+ subband.tby0 = Math.ceil(component.tcy0 / bscale);
+ subband.tbx1 = Math.ceil(component.tcx1 / bscale - 0.5);
+ subband.tby1 = Math.ceil(component.tcy1 / bscale);
+ subband.resolution = resolution;
+ buildCodeblocks(context, subband, blocksDimensions);
+ subbands.push(subband);
+ resolutionSubbands.push(subband);
+ subband = {};
+ subband.type = 'LH';
+ subband.tbx0 = Math.ceil(component.tcx0 / bscale);
+ subband.tby0 = Math.ceil(component.tcy0 / bscale - 0.5);
+ subband.tbx1 = Math.ceil(component.tcx1 / bscale);
+ subband.tby1 = Math.ceil(component.tcy1 / bscale - 0.5);
+ subband.resolution = resolution;
+ buildCodeblocks(context, subband, blocksDimensions);
+ subbands.push(subband);
+ resolutionSubbands.push(subband);
+ subband = {};
+ subband.type = 'HH';
+ subband.tbx0 = Math.ceil(component.tcx0 / bscale - 0.5);
+ subband.tby0 = Math.ceil(component.tcy0 / bscale - 0.5);
+ subband.tbx1 = Math.ceil(component.tcx1 / bscale - 0.5);
+ subband.tby1 = Math.ceil(component.tcy1 / bscale - 0.5);
+ subband.resolution = resolution;
+ buildCodeblocks(context, subband, blocksDimensions);
+ subbands.push(subband);
+ resolutionSubbands.push(subband);
+ resolution.subbands = resolutionSubbands;
+ }
+ }
+
+ component.resolutions = resolutions;
+ component.subbands = subbands;
+ }
+
+ var progressionOrder = tile.codingStyleDefaultParameters.progressionOrder;
+
+ switch (progressionOrder) {
+ case 0:
+ tile.packetsIterator = new LayerResolutionComponentPositionIterator(context);
+ break;
+
+ case 1:
+ tile.packetsIterator = new ResolutionLayerComponentPositionIterator(context);
+ break;
+
+ case 2:
+ tile.packetsIterator = new ResolutionPositionComponentLayerIterator(context);
+ break;
+
+ case 3:
+ tile.packetsIterator = new PositionComponentResolutionLayerIterator(context);
+ break;
+
+ case 4:
+ tile.packetsIterator = new ComponentPositionResolutionLayerIterator(context);
+ break;
+
+ default:
+ throw new JpxError("Unsupported progression order ".concat(progressionOrder));
+ }
+ }
+
+ function parseTilePackets(context, data, offset, dataLength) {
+ var position = 0;
+ var buffer,
+ bufferSize = 0,
+ skipNextBit = false;
+
+ function readBits(count) {
+ while (bufferSize < count) {
+ var b = data[offset + position];
+ position++;
+
+ if (skipNextBit) {
+ buffer = buffer << 7 | b;
+ bufferSize += 7;
+ skipNextBit = false;
+ } else {
+ buffer = buffer << 8 | b;
+ bufferSize += 8;
+ }
+
+ if (b === 0xFF) {
+ skipNextBit = true;
+ }
+ }
+
+ bufferSize -= count;
+ return buffer >>> bufferSize & (1 << count) - 1;
+ }
+
+ function skipMarkerIfEqual(value) {
+ if (data[offset + position - 1] === 0xFF && data[offset + position] === value) {
+ skipBytes(1);
+ return true;
+ } else if (data[offset + position] === 0xFF && data[offset + position + 1] === value) {
+ skipBytes(2);
+ return true;
+ }
+
+ return false;
+ }
+
+ function skipBytes(count) {
+ position += count;
+ }
+
+ function alignToByte() {
+ bufferSize = 0;
+
+ if (skipNextBit) {
+ position++;
+ skipNextBit = false;
+ }
+ }
+
+ function readCodingpasses() {
+ if (readBits(1) === 0) {
+ return 1;
+ }
+
+ if (readBits(1) === 0) {
+ return 2;
+ }
+
+ var value = readBits(2);
+
+ if (value < 3) {
+ return value + 3;
+ }
+
+ value = readBits(5);
+
+ if (value < 31) {
+ return value + 6;
+ }
+
+ value = readBits(7);
+ return value + 37;
+ }
+
+ var tileIndex = context.currentTile.index;
+ var tile = context.tiles[tileIndex];
+ var sopMarkerUsed = context.COD.sopMarkerUsed;
+ var ephMarkerUsed = context.COD.ephMarkerUsed;
+ var packetsIterator = tile.packetsIterator;
+
+ while (position < dataLength) {
+ alignToByte();
+
+ if (sopMarkerUsed && skipMarkerIfEqual(0x91)) {
+ skipBytes(4);
+ }
+
+ var packet = packetsIterator.nextPacket();
+
+ if (!readBits(1)) {
+ continue;
+ }
+
+ var layerNumber = packet.layerNumber;
+ var queue = [],
+ codeblock;
+
+ for (var i = 0, ii = packet.codeblocks.length; i < ii; i++) {
+ codeblock = packet.codeblocks[i];
+ var precinct = codeblock.precinct;
+ var codeblockColumn = codeblock.cbx - precinct.cbxMin;
+ var codeblockRow = codeblock.cby - precinct.cbyMin;
+ var codeblockIncluded = false;
+ var firstTimeInclusion = false;
+ var valueReady;
+
+ if (codeblock['included'] !== undefined) {
+ codeblockIncluded = !!readBits(1);
+ } else {
+ precinct = codeblock.precinct;
+ var inclusionTree, zeroBitPlanesTree;
+
+ if (precinct['inclusionTree'] !== undefined) {
+ inclusionTree = precinct.inclusionTree;
+ } else {
+ var width = precinct.cbxMax - precinct.cbxMin + 1;
+ var height = precinct.cbyMax - precinct.cbyMin + 1;
+ inclusionTree = new InclusionTree(width, height, layerNumber);
+ zeroBitPlanesTree = new TagTree(width, height);
+ precinct.inclusionTree = inclusionTree;
+ precinct.zeroBitPlanesTree = zeroBitPlanesTree;
+ }
+
+ if (inclusionTree.reset(codeblockColumn, codeblockRow, layerNumber)) {
+ while (true) {
+ if (readBits(1)) {
+ valueReady = !inclusionTree.nextLevel();
+
+ if (valueReady) {
+ codeblock.included = true;
+ codeblockIncluded = firstTimeInclusion = true;
+ break;
+ }
+ } else {
+ inclusionTree.incrementValue(layerNumber);
+ break;
+ }
+ }
+ }
+ }
+
+ if (!codeblockIncluded) {
+ continue;
+ }
+
+ if (firstTimeInclusion) {
+ zeroBitPlanesTree = precinct.zeroBitPlanesTree;
+ zeroBitPlanesTree.reset(codeblockColumn, codeblockRow);
+
+ while (true) {
+ if (readBits(1)) {
+ valueReady = !zeroBitPlanesTree.nextLevel();
+
+ if (valueReady) {
+ break;
+ }
+ } else {
+ zeroBitPlanesTree.incrementValue();
+ }
+ }
+
+ codeblock.zeroBitPlanes = zeroBitPlanesTree.value;
+ }
+
+ var codingpasses = readCodingpasses();
+
+ while (readBits(1)) {
+ codeblock.Lblock++;
+ }
+
+ var codingpassesLog2 = (0, _util.log2)(codingpasses);
+ var bits = (codingpasses < 1 << codingpassesLog2 ? codingpassesLog2 - 1 : codingpassesLog2) + codeblock.Lblock;
+ var codedDataLength = readBits(bits);
+ queue.push({
+ codeblock: codeblock,
+ codingpasses: codingpasses,
+ dataLength: codedDataLength
+ });
+ }
+
+ alignToByte();
+
+ if (ephMarkerUsed) {
+ skipMarkerIfEqual(0x92);
+ }
+
+ while (queue.length > 0) {
+ var packetItem = queue.shift();
+ codeblock = packetItem.codeblock;
+
+ if (codeblock['data'] === undefined) {
+ codeblock.data = [];
+ }
+
+ codeblock.data.push({
+ data: data,
+ start: offset + position,
+ end: offset + position + packetItem.dataLength,
+ codingpasses: packetItem.codingpasses
+ });
+ position += packetItem.dataLength;
+ }
+ }
+
+ return position;
+ }
+
+ function copyCoefficients(coefficients, levelWidth, levelHeight, subband, delta, mb, reversible, segmentationSymbolUsed) {
+ var x0 = subband.tbx0;
+ var y0 = subband.tby0;
+ var width = subband.tbx1 - subband.tbx0;
+ var codeblocks = subband.codeblocks;
+ var right = subband.type.charAt(0) === 'H' ? 1 : 0;
+ var bottom = subband.type.charAt(1) === 'H' ? levelWidth : 0;
+
+ for (var i = 0, ii = codeblocks.length; i < ii; ++i) {
+ var codeblock = codeblocks[i];
+ var blockWidth = codeblock.tbx1_ - codeblock.tbx0_;
+ var blockHeight = codeblock.tby1_ - codeblock.tby0_;
+
+ if (blockWidth === 0 || blockHeight === 0) {
+ continue;
+ }
+
+ if (codeblock['data'] === undefined) {
+ continue;
+ }
+
+ var bitModel, currentCodingpassType;
+ bitModel = new BitModel(blockWidth, blockHeight, codeblock.subbandType, codeblock.zeroBitPlanes, mb);
+ currentCodingpassType = 2;
+ var data = codeblock.data,
+ totalLength = 0,
+ codingpasses = 0;
+ var j, jj, dataItem;
+
+ for (j = 0, jj = data.length; j < jj; j++) {
+ dataItem = data[j];
+ totalLength += dataItem.end - dataItem.start;
+ codingpasses += dataItem.codingpasses;
+ }
+
+ var encodedData = new Uint8Array(totalLength);
+ var position = 0;
+
+ for (j = 0, jj = data.length; j < jj; j++) {
+ dataItem = data[j];
+ var chunk = dataItem.data.subarray(dataItem.start, dataItem.end);
+ encodedData.set(chunk, position);
+ position += chunk.length;
+ }
+
+ var decoder = new _arithmetic_decoder.ArithmeticDecoder(encodedData, 0, totalLength);
+ bitModel.setDecoder(decoder);
+
+ for (j = 0; j < codingpasses; j++) {
+ switch (currentCodingpassType) {
+ case 0:
+ bitModel.runSignificancePropagationPass();
+ break;
+
+ case 1:
+ bitModel.runMagnitudeRefinementPass();
+ break;
+
+ case 2:
+ bitModel.runCleanupPass();
+
+ if (segmentationSymbolUsed) {
+ bitModel.checkSegmentationSymbol();
+ }
+
+ break;
+ }
+
+ currentCodingpassType = (currentCodingpassType + 1) % 3;
+ }
+
+ var offset = codeblock.tbx0_ - x0 + (codeblock.tby0_ - y0) * width;
+ var sign = bitModel.coefficentsSign;
+ var magnitude = bitModel.coefficentsMagnitude;
+ var bitsDecoded = bitModel.bitsDecoded;
+ var magnitudeCorrection = reversible ? 0 : 0.5;
+ var k, n, nb;
+ position = 0;
+ var interleave = subband.type !== 'LL';
+
+ for (j = 0; j < blockHeight; j++) {
+ var row = offset / width | 0;
+ var levelOffset = 2 * row * (levelWidth - width) + right + bottom;
+
+ for (k = 0; k < blockWidth; k++) {
+ n = magnitude[position];
+
+ if (n !== 0) {
+ n = (n + magnitudeCorrection) * delta;
+
+ if (sign[position] !== 0) {
+ n = -n;
+ }
+
+ nb = bitsDecoded[position];
+ var pos = interleave ? levelOffset + (offset << 1) : offset;
+
+ if (reversible && nb >= mb) {
+ coefficients[pos] = n;
+ } else {
+ coefficients[pos] = n * (1 << mb - nb);
+ }
+ }
+
+ offset++;
+ position++;
+ }
+
+ offset += width - blockWidth;
+ }
+ }
+ }
+
+ function transformTile(context, tile, c) {
+ var component = tile.components[c];
+ var codingStyleParameters = component.codingStyleParameters;
+ var quantizationParameters = component.quantizationParameters;
+ var decompositionLevelsCount = codingStyleParameters.decompositionLevelsCount;
+ var spqcds = quantizationParameters.SPqcds;
+ var scalarExpounded = quantizationParameters.scalarExpounded;
+ var guardBits = quantizationParameters.guardBits;
+ var segmentationSymbolUsed = codingStyleParameters.segmentationSymbolUsed;
+ var precision = context.components[c].precision;
+ var reversible = codingStyleParameters.reversibleTransformation;
+ var transform = reversible ? new ReversibleTransform() : new IrreversibleTransform();
+ var subbandCoefficients = [];
+ var b = 0;
+
+ for (var i = 0; i <= decompositionLevelsCount; i++) {
+ var resolution = component.resolutions[i];
+ var width = resolution.trx1 - resolution.trx0;
+ var height = resolution.try1 - resolution.try0;
+ var coefficients = new Float32Array(width * height);
+
+ for (var j = 0, jj = resolution.subbands.length; j < jj; j++) {
+ var mu, epsilon;
+
+ if (!scalarExpounded) {
+ mu = spqcds[0].mu;
+ epsilon = spqcds[0].epsilon + (i > 0 ? 1 - i : 0);
+ } else {
+ mu = spqcds[b].mu;
+ epsilon = spqcds[b].epsilon;
+ b++;
+ }
+
+ var subband = resolution.subbands[j];
+ var gainLog2 = SubbandsGainLog2[subband.type];
+ var delta = reversible ? 1 : Math.pow(2, precision + gainLog2 - epsilon) * (1 + mu / 2048);
+ var mb = guardBits + epsilon - 1;
+ copyCoefficients(coefficients, width, height, subband, delta, mb, reversible, segmentationSymbolUsed);
+ }
+
+ subbandCoefficients.push({
+ width: width,
+ height: height,
+ items: coefficients
+ });
+ }
+
+ var result = transform.calculate(subbandCoefficients, component.tcx0, component.tcy0);
+ return {
+ left: component.tcx0,
+ top: component.tcy0,
+ width: result.width,
+ height: result.height,
+ items: result.items
+ };
+ }
+
+ function transformComponents(context) {
+ var siz = context.SIZ;
+ var components = context.components;
+ var componentsCount = siz.Csiz;
+ var resultImages = [];
+
+ for (var i = 0, ii = context.tiles.length; i < ii; i++) {
+ var tile = context.tiles[i];
+ var transformedTiles = [];
+ var c;
+
+ for (c = 0; c < componentsCount; c++) {
+ transformedTiles[c] = transformTile(context, tile, c);
+ }
+
+ var tile0 = transformedTiles[0];
+ var out = new Uint8ClampedArray(tile0.items.length * componentsCount);
+ var result = {
+ left: tile0.left,
+ top: tile0.top,
+ width: tile0.width,
+ height: tile0.height,
+ items: out
+ };
+ var shift, offset;
+ var pos = 0,
+ j,
+ jj,
+ y0,
+ y1,
+ y2;
+
+ if (tile.codingStyleDefaultParameters.multipleComponentTransform) {
+ var fourComponents = componentsCount === 4;
+ var y0items = transformedTiles[0].items;
+ var y1items = transformedTiles[1].items;
+ var y2items = transformedTiles[2].items;
+ var y3items = fourComponents ? transformedTiles[3].items : null;
+ shift = components[0].precision - 8;
+ offset = (128 << shift) + 0.5;
+ var component0 = tile.components[0];
+ var alpha01 = componentsCount - 3;
+ jj = y0items.length;
+
+ if (!component0.codingStyleParameters.reversibleTransformation) {
+ for (j = 0; j < jj; j++, pos += alpha01) {
+ y0 = y0items[j] + offset;
+ y1 = y1items[j];
+ y2 = y2items[j];
+ out[pos++] = y0 + 1.402 * y2 >> shift;
+ out[pos++] = y0 - 0.34413 * y1 - 0.71414 * y2 >> shift;
+ out[pos++] = y0 + 1.772 * y1 >> shift;
+ }
+ } else {
+ for (j = 0; j < jj; j++, pos += alpha01) {
+ y0 = y0items[j] + offset;
+ y1 = y1items[j];
+ y2 = y2items[j];
+ var g = y0 - (y2 + y1 >> 2);
+ out[pos++] = g + y2 >> shift;
+ out[pos++] = g >> shift;
+ out[pos++] = g + y1 >> shift;
+ }
+ }
+
+ if (fourComponents) {
+ for (j = 0, pos = 3; j < jj; j++, pos += 4) {
+ out[pos] = y3items[j] + offset >> shift;
+ }
+ }
+ } else {
+ for (c = 0; c < componentsCount; c++) {
+ var items = transformedTiles[c].items;
+ shift = components[c].precision - 8;
+ offset = (128 << shift) + 0.5;
+
+ for (pos = c, j = 0, jj = items.length; j < jj; j++) {
+ out[pos] = items[j] + offset >> shift;
+ pos += componentsCount;
+ }
+ }
+ }
+
+ resultImages.push(result);
+ }
+
+ return resultImages;
+ }
+
+ function initializeTile(context, tileIndex) {
+ var siz = context.SIZ;
+ var componentsCount = siz.Csiz;
+ var tile = context.tiles[tileIndex];
+
+ for (var c = 0; c < componentsCount; c++) {
+ var component = tile.components[c];
+ var qcdOrQcc = context.currentTile.QCC[c] !== undefined ? context.currentTile.QCC[c] : context.currentTile.QCD;
+ component.quantizationParameters = qcdOrQcc;
+ var codOrCoc = context.currentTile.COC[c] !== undefined ? context.currentTile.COC[c] : context.currentTile.COD;
+ component.codingStyleParameters = codOrCoc;
+ }
+
+ tile.codingStyleDefaultParameters = context.currentTile.COD;
+ }
+
+ var TagTree = function TagTreeClosure() {
+ function TagTree(width, height) {
+ var levelsLength = (0, _util.log2)(Math.max(width, height)) + 1;
+ this.levels = [];
+
+ for (var i = 0; i < levelsLength; i++) {
+ var level = {
+ width: width,
+ height: height,
+ items: []
+ };
+ this.levels.push(level);
+ width = Math.ceil(width / 2);
+ height = Math.ceil(height / 2);
+ }
+ }
+
+ TagTree.prototype = {
+ reset: function TagTree_reset(i, j) {
+ var currentLevel = 0,
+ value = 0,
+ level;
+
+ while (currentLevel < this.levels.length) {
+ level = this.levels[currentLevel];
+ var index = i + j * level.width;
+
+ if (level.items[index] !== undefined) {
+ value = level.items[index];
+ break;
+ }
+
+ level.index = index;
+ i >>= 1;
+ j >>= 1;
+ currentLevel++;
+ }
+
+ currentLevel--;
+ level = this.levels[currentLevel];
+ level.items[level.index] = value;
+ this.currentLevel = currentLevel;
+ delete this.value;
+ },
+ incrementValue: function TagTree_incrementValue() {
+ var level = this.levels[this.currentLevel];
+ level.items[level.index]++;
+ },
+ nextLevel: function TagTree_nextLevel() {
+ var currentLevel = this.currentLevel;
+ var level = this.levels[currentLevel];
+ var value = level.items[level.index];
+ currentLevel--;
+
+ if (currentLevel < 0) {
+ this.value = value;
+ return false;
+ }
+
+ this.currentLevel = currentLevel;
+ level = this.levels[currentLevel];
+ level.items[level.index] = value;
+ return true;
+ }
+ };
+ return TagTree;
+ }();
+
+ var InclusionTree = function InclusionTreeClosure() {
+ function InclusionTree(width, height, defaultValue) {
+ var levelsLength = (0, _util.log2)(Math.max(width, height)) + 1;
+ this.levels = [];
+
+ for (var i = 0; i < levelsLength; i++) {
+ var items = new Uint8Array(width * height);
+
+ for (var j = 0, jj = items.length; j < jj; j++) {
+ items[j] = defaultValue;
+ }
+
+ var level = {
+ width: width,
+ height: height,
+ items: items
+ };
+ this.levels.push(level);
+ width = Math.ceil(width / 2);
+ height = Math.ceil(height / 2);
+ }
+ }
+
+ InclusionTree.prototype = {
+ reset: function InclusionTree_reset(i, j, stopValue) {
+ var currentLevel = 0;
+
+ while (currentLevel < this.levels.length) {
+ var level = this.levels[currentLevel];
+ var index = i + j * level.width;
+ level.index = index;
+ var value = level.items[index];
+
+ if (value === 0xFF) {
+ break;
+ }
+
+ if (value > stopValue) {
+ this.currentLevel = currentLevel;
+ this.propagateValues();
+ return false;
+ }
+
+ i >>= 1;
+ j >>= 1;
+ currentLevel++;
+ }
+
+ this.currentLevel = currentLevel - 1;
+ return true;
+ },
+ incrementValue: function InclusionTree_incrementValue(stopValue) {
+ var level = this.levels[this.currentLevel];
+ level.items[level.index] = stopValue + 1;
+ this.propagateValues();
+ },
+ propagateValues: function InclusionTree_propagateValues() {
+ var levelIndex = this.currentLevel;
+ var level = this.levels[levelIndex];
+ var currentValue = level.items[level.index];
+
+ while (--levelIndex >= 0) {
+ level = this.levels[levelIndex];
+ level.items[level.index] = currentValue;
+ }
+ },
+ nextLevel: function InclusionTree_nextLevel() {
+ var currentLevel = this.currentLevel;
+ var level = this.levels[currentLevel];
+ var value = level.items[level.index];
+ level.items[level.index] = 0xFF;
+ currentLevel--;
+
+ if (currentLevel < 0) {
+ return false;
+ }
+
+ this.currentLevel = currentLevel;
+ level = this.levels[currentLevel];
+ level.items[level.index] = value;
+ return true;
+ }
+ };
+ return InclusionTree;
+ }();
+
+ var BitModel = function BitModelClosure() {
+ var UNIFORM_CONTEXT = 17;
+ var RUNLENGTH_CONTEXT = 18;
+ var LLAndLHContextsLabel = new Uint8Array([0, 5, 8, 0, 3, 7, 8, 0, 4, 7, 8, 0, 0, 0, 0, 0, 1, 6, 8, 0, 3, 7, 8, 0, 4, 7, 8, 0, 0, 0, 0, 0, 2, 6, 8, 0, 3, 7, 8, 0, 4, 7, 8, 0, 0, 0, 0, 0, 2, 6, 8, 0, 3, 7, 8, 0, 4, 7, 8, 0, 0, 0, 0, 0, 2, 6, 8, 0, 3, 7, 8, 0, 4, 7, 8]);
+ var HLContextLabel = new Uint8Array([0, 3, 4, 0, 5, 7, 7, 0, 8, 8, 8, 0, 0, 0, 0, 0, 1, 3, 4, 0, 6, 7, 7, 0, 8, 8, 8, 0, 0, 0, 0, 0, 2, 3, 4, 0, 6, 7, 7, 0, 8, 8, 8, 0, 0, 0, 0, 0, 2, 3, 4, 0, 6, 7, 7, 0, 8, 8, 8, 0, 0, 0, 0, 0, 2, 3, 4, 0, 6, 7, 7, 0, 8, 8, 8]);
+ var HHContextLabel = new Uint8Array([0, 1, 2, 0, 1, 2, 2, 0, 2, 2, 2, 0, 0, 0, 0, 0, 3, 4, 5, 0, 4, 5, 5, 0, 5, 5, 5, 0, 0, 0, 0, 0, 6, 7, 7, 0, 7, 7, 7, 0, 7, 7, 7, 0, 0, 0, 0, 0, 8, 8, 8, 0, 8, 8, 8, 0, 8, 8, 8, 0, 0, 0, 0, 0, 8, 8, 8, 0, 8, 8, 8, 0, 8, 8, 8]);
+
+ function BitModel(width, height, subband, zeroBitPlanes, mb) {
+ this.width = width;
+ this.height = height;
+ this.contextLabelTable = subband === 'HH' ? HHContextLabel : subband === 'HL' ? HLContextLabel : LLAndLHContextsLabel;
+ var coefficientCount = width * height;
+ this.neighborsSignificance = new Uint8Array(coefficientCount);
+ this.coefficentsSign = new Uint8Array(coefficientCount);
+ this.coefficentsMagnitude = mb > 14 ? new Uint32Array(coefficientCount) : mb > 6 ? new Uint16Array(coefficientCount) : new Uint8Array(coefficientCount);
+ this.processingFlags = new Uint8Array(coefficientCount);
+ var bitsDecoded = new Uint8Array(coefficientCount);
+
+ if (zeroBitPlanes !== 0) {
+ for (var i = 0; i < coefficientCount; i++) {
+ bitsDecoded[i] = zeroBitPlanes;
+ }
+ }
+
+ this.bitsDecoded = bitsDecoded;
+ this.reset();
+ }
+
+ BitModel.prototype = {
+ setDecoder: function BitModel_setDecoder(decoder) {
+ this.decoder = decoder;
+ },
+ reset: function BitModel_reset() {
+ this.contexts = new Int8Array(19);
+ this.contexts[0] = 4 << 1 | 0;
+ this.contexts[UNIFORM_CONTEXT] = 46 << 1 | 0;
+ this.contexts[RUNLENGTH_CONTEXT] = 3 << 1 | 0;
+ },
+ setNeighborsSignificance: function BitModel_setNeighborsSignificance(row, column, index) {
+ var neighborsSignificance = this.neighborsSignificance;
+ var width = this.width,
+ height = this.height;
+ var left = column > 0;
+ var right = column + 1 < width;
+ var i;
+
+ if (row > 0) {
+ i = index - width;
+
+ if (left) {
+ neighborsSignificance[i - 1] += 0x10;
+ }
+
+ if (right) {
+ neighborsSignificance[i + 1] += 0x10;
+ }
+
+ neighborsSignificance[i] += 0x04;
+ }
+
+ if (row + 1 < height) {
+ i = index + width;
+
+ if (left) {
+ neighborsSignificance[i - 1] += 0x10;
+ }
+
+ if (right) {
+ neighborsSignificance[i + 1] += 0x10;
+ }
+
+ neighborsSignificance[i] += 0x04;
+ }
+
+ if (left) {
+ neighborsSignificance[index - 1] += 0x01;
+ }
+
+ if (right) {
+ neighborsSignificance[index + 1] += 0x01;
+ }
+
+ neighborsSignificance[index] |= 0x80;
+ },
+ runSignificancePropagationPass: function BitModel_runSignificancePropagationPass() {
+ var decoder = this.decoder;
+ var width = this.width,
+ height = this.height;
+ var coefficentsMagnitude = this.coefficentsMagnitude;
+ var coefficentsSign = this.coefficentsSign;
+ var neighborsSignificance = this.neighborsSignificance;
+ var processingFlags = this.processingFlags;
+ var contexts = this.contexts;
+ var labels = this.contextLabelTable;
+ var bitsDecoded = this.bitsDecoded;
+ var processedInverseMask = ~1;
+ var processedMask = 1;
+ var firstMagnitudeBitMask = 2;
+
+ for (var i0 = 0; i0 < height; i0 += 4) {
+ for (var j = 0; j < width; j++) {
+ var index = i0 * width + j;
+
+ for (var i1 = 0; i1 < 4; i1++, index += width) {
+ var i = i0 + i1;
+
+ if (i >= height) {
+ break;
+ }
+
+ processingFlags[index] &= processedInverseMask;
+
+ if (coefficentsMagnitude[index] || !neighborsSignificance[index]) {
+ continue;
+ }
+
+ var contextLabel = labels[neighborsSignificance[index]];
+ var decision = decoder.readBit(contexts, contextLabel);
+
+ if (decision) {
+ var sign = this.decodeSignBit(i, j, index);
+ coefficentsSign[index] = sign;
+ coefficentsMagnitude[index] = 1;
+ this.setNeighborsSignificance(i, j, index);
+ processingFlags[index] |= firstMagnitudeBitMask;
+ }
+
+ bitsDecoded[index]++;
+ processingFlags[index] |= processedMask;
+ }
+ }
+ }
+ },
+ decodeSignBit: function BitModel_decodeSignBit(row, column, index) {
+ var width = this.width,
+ height = this.height;
+ var coefficentsMagnitude = this.coefficentsMagnitude;
+ var coefficentsSign = this.coefficentsSign;
+ var contribution, sign0, sign1, significance1;
+ var contextLabel, decoded;
+ significance1 = column > 0 && coefficentsMagnitude[index - 1] !== 0;
+
+ if (column + 1 < width && coefficentsMagnitude[index + 1] !== 0) {
+ sign1 = coefficentsSign[index + 1];
+
+ if (significance1) {
+ sign0 = coefficentsSign[index - 1];
+ contribution = 1 - sign1 - sign0;
+ } else {
+ contribution = 1 - sign1 - sign1;
+ }
+ } else if (significance1) {
+ sign0 = coefficentsSign[index - 1];
+ contribution = 1 - sign0 - sign0;
+ } else {
+ contribution = 0;
+ }
+
+ var horizontalContribution = 3 * contribution;
+ significance1 = row > 0 && coefficentsMagnitude[index - width] !== 0;
+
+ if (row + 1 < height && coefficentsMagnitude[index + width] !== 0) {
+ sign1 = coefficentsSign[index + width];
+
+ if (significance1) {
+ sign0 = coefficentsSign[index - width];
+ contribution = 1 - sign1 - sign0 + horizontalContribution;
+ } else {
+ contribution = 1 - sign1 - sign1 + horizontalContribution;
+ }
+ } else if (significance1) {
+ sign0 = coefficentsSign[index - width];
+ contribution = 1 - sign0 - sign0 + horizontalContribution;
+ } else {
+ contribution = horizontalContribution;
+ }
+
+ if (contribution >= 0) {
+ contextLabel = 9 + contribution;
+ decoded = this.decoder.readBit(this.contexts, contextLabel);
+ } else {
+ contextLabel = 9 - contribution;
+ decoded = this.decoder.readBit(this.contexts, contextLabel) ^ 1;
+ }
+
+ return decoded;
+ },
+ runMagnitudeRefinementPass: function BitModel_runMagnitudeRefinementPass() {
+ var decoder = this.decoder;
+ var width = this.width,
+ height = this.height;
+ var coefficentsMagnitude = this.coefficentsMagnitude;
+ var neighborsSignificance = this.neighborsSignificance;
+ var contexts = this.contexts;
+ var bitsDecoded = this.bitsDecoded;
+ var processingFlags = this.processingFlags;
+ var processedMask = 1;
+ var firstMagnitudeBitMask = 2;
+ var length = width * height;
+ var width4 = width * 4;
+
+ for (var index0 = 0, indexNext; index0 < length; index0 = indexNext) {
+ indexNext = Math.min(length, index0 + width4);
+
+ for (var j = 0; j < width; j++) {
+ for (var index = index0 + j; index < indexNext; index += width) {
+ if (!coefficentsMagnitude[index] || (processingFlags[index] & processedMask) !== 0) {
+ continue;
+ }
+
+ var contextLabel = 16;
+
+ if ((processingFlags[index] & firstMagnitudeBitMask) !== 0) {
+ processingFlags[index] ^= firstMagnitudeBitMask;
+ var significance = neighborsSignificance[index] & 127;
+ contextLabel = significance === 0 ? 15 : 14;
+ }
+
+ var bit = decoder.readBit(contexts, contextLabel);
+ coefficentsMagnitude[index] = coefficentsMagnitude[index] << 1 | bit;
+ bitsDecoded[index]++;
+ processingFlags[index] |= processedMask;
+ }
+ }
+ }
+ },
+ runCleanupPass: function BitModel_runCleanupPass() {
+ var decoder = this.decoder;
+ var width = this.width,
+ height = this.height;
+ var neighborsSignificance = this.neighborsSignificance;
+ var coefficentsMagnitude = this.coefficentsMagnitude;
+ var coefficentsSign = this.coefficentsSign;
+ var contexts = this.contexts;
+ var labels = this.contextLabelTable;
+ var bitsDecoded = this.bitsDecoded;
+ var processingFlags = this.processingFlags;
+ var processedMask = 1;
+ var firstMagnitudeBitMask = 2;
+ var oneRowDown = width;
+ var twoRowsDown = width * 2;
+ var threeRowsDown = width * 3;
+ var iNext;
+
+ for (var i0 = 0; i0 < height; i0 = iNext) {
+ iNext = Math.min(i0 + 4, height);
+ var indexBase = i0 * width;
+ var checkAllEmpty = i0 + 3 < height;
+
+ for (var j = 0; j < width; j++) {
+ var index0 = indexBase + j;
+ var allEmpty = checkAllEmpty && processingFlags[index0] === 0 && processingFlags[index0 + oneRowDown] === 0 && processingFlags[index0 + twoRowsDown] === 0 && processingFlags[index0 + threeRowsDown] === 0 && neighborsSignificance[index0] === 0 && neighborsSignificance[index0 + oneRowDown] === 0 && neighborsSignificance[index0 + twoRowsDown] === 0 && neighborsSignificance[index0 + threeRowsDown] === 0;
+ var i1 = 0,
+ index = index0;
+ var i = i0,
+ sign;
+
+ if (allEmpty) {
+ var hasSignificantCoefficent = decoder.readBit(contexts, RUNLENGTH_CONTEXT);
+
+ if (!hasSignificantCoefficent) {
+ bitsDecoded[index0]++;
+ bitsDecoded[index0 + oneRowDown]++;
+ bitsDecoded[index0 + twoRowsDown]++;
+ bitsDecoded[index0 + threeRowsDown]++;
+ continue;
+ }
+
+ i1 = decoder.readBit(contexts, UNIFORM_CONTEXT) << 1 | decoder.readBit(contexts, UNIFORM_CONTEXT);
+
+ if (i1 !== 0) {
+ i = i0 + i1;
+ index += i1 * width;
+ }
+
+ sign = this.decodeSignBit(i, j, index);
+ coefficentsSign[index] = sign;
+ coefficentsMagnitude[index] = 1;
+ this.setNeighborsSignificance(i, j, index);
+ processingFlags[index] |= firstMagnitudeBitMask;
+ index = index0;
+
+ for (var i2 = i0; i2 <= i; i2++, index += width) {
+ bitsDecoded[index]++;
+ }
+
+ i1++;
+ }
+
+ for (i = i0 + i1; i < iNext; i++, index += width) {
+ if (coefficentsMagnitude[index] || (processingFlags[index] & processedMask) !== 0) {
+ continue;
+ }
+
+ var contextLabel = labels[neighborsSignificance[index]];
+ var decision = decoder.readBit(contexts, contextLabel);
+
+ if (decision === 1) {
+ sign = this.decodeSignBit(i, j, index);
+ coefficentsSign[index] = sign;
+ coefficentsMagnitude[index] = 1;
+ this.setNeighborsSignificance(i, j, index);
+ processingFlags[index] |= firstMagnitudeBitMask;
+ }
+
+ bitsDecoded[index]++;
+ }
+ }
+ }
+ },
+ checkSegmentationSymbol: function BitModel_checkSegmentationSymbol() {
+ var decoder = this.decoder;
+ var contexts = this.contexts;
+ var symbol = decoder.readBit(contexts, UNIFORM_CONTEXT) << 3 | decoder.readBit(contexts, UNIFORM_CONTEXT) << 2 | decoder.readBit(contexts, UNIFORM_CONTEXT) << 1 | decoder.readBit(contexts, UNIFORM_CONTEXT);
+
+ if (symbol !== 0xA) {
+ throw new JpxError('Invalid segmentation symbol');
+ }
+ }
+ };
+ return BitModel;
+ }();
+
+ var Transform = function TransformClosure() {
+ function Transform() {}
+
+ Transform.prototype.calculate = function transformCalculate(subbands, u0, v0) {
+ var ll = subbands[0];
+
+ for (var i = 1, ii = subbands.length; i < ii; i++) {
+ ll = this.iterate(ll, subbands[i], u0, v0);
+ }
+
+ return ll;
+ };
+
+ Transform.prototype.extend = function extend(buffer, offset, size) {
+ var i1 = offset - 1,
+ j1 = offset + 1;
+ var i2 = offset + size - 2,
+ j2 = offset + size;
+ buffer[i1--] = buffer[j1++];
+ buffer[j2++] = buffer[i2--];
+ buffer[i1--] = buffer[j1++];
+ buffer[j2++] = buffer[i2--];
+ buffer[i1--] = buffer[j1++];
+ buffer[j2++] = buffer[i2--];
+ buffer[i1] = buffer[j1];
+ buffer[j2] = buffer[i2];
+ };
+
+ Transform.prototype.iterate = function Transform_iterate(ll, hl_lh_hh, u0, v0) {
+ var llWidth = ll.width,
+ llHeight = ll.height,
+ llItems = ll.items;
+ var width = hl_lh_hh.width;
+ var height = hl_lh_hh.height;
+ var items = hl_lh_hh.items;
+ var i, j, k, l, u, v;
+
+ for (k = 0, i = 0; i < llHeight; i++) {
+ l = i * 2 * width;
+
+ for (j = 0; j < llWidth; j++, k++, l += 2) {
+ items[l] = llItems[k];
+ }
+ }
+
+ llItems = ll.items = null;
+ var bufferPadding = 4;
+ var rowBuffer = new Float32Array(width + 2 * bufferPadding);
+
+ if (width === 1) {
+ if ((u0 & 1) !== 0) {
+ for (v = 0, k = 0; v < height; v++, k += width) {
+ items[k] *= 0.5;
+ }
+ }
+ } else {
+ for (v = 0, k = 0; v < height; v++, k += width) {
+ rowBuffer.set(items.subarray(k, k + width), bufferPadding);
+ this.extend(rowBuffer, bufferPadding, width);
+ this.filter(rowBuffer, bufferPadding, width);
+ items.set(rowBuffer.subarray(bufferPadding, bufferPadding + width), k);
+ }
+ }
+
+ var numBuffers = 16;
+ var colBuffers = [];
+
+ for (i = 0; i < numBuffers; i++) {
+ colBuffers.push(new Float32Array(height + 2 * bufferPadding));
+ }
+
+ var b,
+ currentBuffer = 0;
+ ll = bufferPadding + height;
+
+ if (height === 1) {
+ if ((v0 & 1) !== 0) {
+ for (u = 0; u < width; u++) {
+ items[u] *= 0.5;
+ }
+ }
+ } else {
+ for (u = 0; u < width; u++) {
+ if (currentBuffer === 0) {
+ numBuffers = Math.min(width - u, numBuffers);
+
+ for (k = u, l = bufferPadding; l < ll; k += width, l++) {
+ for (b = 0; b < numBuffers; b++) {
+ colBuffers[b][l] = items[k + b];
+ }
+ }
+
+ currentBuffer = numBuffers;
+ }
+
+ currentBuffer--;
+ var buffer = colBuffers[currentBuffer];
+ this.extend(buffer, bufferPadding, height);
+ this.filter(buffer, bufferPadding, height);
+
+ if (currentBuffer === 0) {
+ k = u - numBuffers + 1;
+
+ for (l = bufferPadding; l < ll; k += width, l++) {
+ for (b = 0; b < numBuffers; b++) {
+ items[k + b] = colBuffers[b][l];
+ }
+ }
+ }
+ }
+ }
+
+ return {
+ width: width,
+ height: height,
+ items: items
+ };
+ };
+
+ return Transform;
+ }();
+
+ var IrreversibleTransform = function IrreversibleTransformClosure() {
+ function IrreversibleTransform() {
+ Transform.call(this);
+ }
+
+ IrreversibleTransform.prototype = Object.create(Transform.prototype);
+
+ IrreversibleTransform.prototype.filter = function irreversibleTransformFilter(x, offset, length) {
+ var len = length >> 1;
+ offset = offset | 0;
+ var j, n, current, next;
+ var alpha = -1.586134342059924;
+ var beta = -0.052980118572961;
+ var gamma = 0.882911075530934;
+ var delta = 0.443506852043971;
+ var K = 1.230174104914001;
+ var K_ = 1 / K;
+ j = offset - 3;
+
+ for (n = len + 4; n--; j += 2) {
+ x[j] *= K_;
+ }
+
+ j = offset - 2;
+ current = delta * x[j - 1];
+
+ for (n = len + 3; n--; j += 2) {
+ next = delta * x[j + 1];
+ x[j] = K * x[j] - current - next;
+
+ if (n--) {
+ j += 2;
+ current = delta * x[j + 1];
+ x[j] = K * x[j] - current - next;
+ } else {
+ break;
+ }
+ }
+
+ j = offset - 1;
+ current = gamma * x[j - 1];
+
+ for (n = len + 2; n--; j += 2) {
+ next = gamma * x[j + 1];
+ x[j] -= current + next;
+
+ if (n--) {
+ j += 2;
+ current = gamma * x[j + 1];
+ x[j] -= current + next;
+ } else {
+ break;
+ }
+ }
+
+ j = offset;
+ current = beta * x[j - 1];
+
+ for (n = len + 1; n--; j += 2) {
+ next = beta * x[j + 1];
+ x[j] -= current + next;
+
+ if (n--) {
+ j += 2;
+ current = beta * x[j + 1];
+ x[j] -= current + next;
+ } else {
+ break;
+ }
+ }
+
+ if (len !== 0) {
+ j = offset + 1;
+ current = alpha * x[j - 1];
+
+ for (n = len; n--; j += 2) {
+ next = alpha * x[j + 1];
+ x[j] -= current + next;
+
+ if (n--) {
+ j += 2;
+ current = alpha * x[j + 1];
+ x[j] -= current + next;
+ } else {
+ break;
+ }
+ }
+ }
+ };
+
+ return IrreversibleTransform;
+ }();
+
+ var ReversibleTransform = function ReversibleTransformClosure() {
+ function ReversibleTransform() {
+ Transform.call(this);
+ }
+
+ ReversibleTransform.prototype = Object.create(Transform.prototype);
+
+ ReversibleTransform.prototype.filter = function reversibleTransformFilter(x, offset, length) {
+ var len = length >> 1;
+ offset = offset | 0;
+ var j, n;
+
+ for (j = offset, n = len + 1; n--; j += 2) {
+ x[j] -= x[j - 1] + x[j + 1] + 2 >> 2;
+ }
+
+ for (j = offset + 1, n = len; n--; j += 2) {
+ x[j] += x[j - 1] + x[j + 1] >> 1;
+ }
+ };
+
+ return ReversibleTransform;
+ }();
+
+ return JpxImage;
+}();
+
+exports.JpxImage = JpxImage;
+
+/***/ }),
+/* 168 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.calculateSHA512 = exports.calculateSHA384 = exports.calculateSHA256 = exports.calculateMD5 = exports.PDF20 = exports.PDF17 = exports.CipherTransformFactory = exports.ARCFourCipher = exports.AES256Cipher = exports.AES128Cipher = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _stream = __w_pdfjs_require__(158);
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
+
+function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
+
+function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
+
+function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var ARCFourCipher = function ARCFourCipherClosure() {
+ function ARCFourCipher(key) {
+ this.a = 0;
+ this.b = 0;
+ var s = new Uint8Array(256);
+ var i,
+ j = 0,
+ tmp,
+ keyLength = key.length;
+
+ for (i = 0; i < 256; ++i) {
+ s[i] = i;
+ }
+
+ for (i = 0; i < 256; ++i) {
+ tmp = s[i];
+ j = j + tmp + key[i % keyLength] & 0xFF;
+ s[i] = s[j];
+ s[j] = tmp;
+ }
+
+ this.s = s;
+ }
+
+ ARCFourCipher.prototype = {
+ encryptBlock: function ARCFourCipher_encryptBlock(data) {
+ var i,
+ n = data.length,
+ tmp,
+ tmp2;
+ var a = this.a,
+ b = this.b,
+ s = this.s;
+ var output = new Uint8Array(n);
+
+ for (i = 0; i < n; ++i) {
+ a = a + 1 & 0xFF;
+ tmp = s[a];
+ b = b + tmp & 0xFF;
+ tmp2 = s[b];
+ s[a] = tmp2;
+ s[b] = tmp;
+ output[i] = data[i] ^ s[tmp + tmp2 & 0xFF];
+ }
+
+ this.a = a;
+ this.b = b;
+ return output;
+ }
+ };
+ ARCFourCipher.prototype.decryptBlock = ARCFourCipher.prototype.encryptBlock;
+ return ARCFourCipher;
+}();
+
+exports.ARCFourCipher = ARCFourCipher;
+
+var calculateMD5 = function calculateMD5Closure() {
+ var r = new Uint8Array([7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21]);
+ var k = new Int32Array([-680876936, -389564586, 606105819, -1044525330, -176418897, 1200080426, -1473231341, -45705983, 1770035416, -1958414417, -42063, -1990404162, 1804603682, -40341101, -1502002290, 1236535329, -165796510, -1069501632, 643717713, -373897302, -701558691, 38016083, -660478335, -405537848, 568446438, -1019803690, -187363961, 1163531501, -1444681467, -51403784, 1735328473, -1926607734, -378558, -2022574463, 1839030562, -35309556, -1530992060, 1272893353, -155497632, -1094730640, 681279174, -358537222, -722521979, 76029189, -640364487, -421815835, 530742520, -995338651, -198630844, 1126891415, -1416354905, -57434055, 1700485571, -1894986606, -1051523, -2054922799, 1873313359, -30611744, -1560198380, 1309151649, -145523070, -1120210379, 718787259, -343485551]);
+
+ function hash(data, offset, length) {
+ var h0 = 1732584193,
+ h1 = -271733879,
+ h2 = -1732584194,
+ h3 = 271733878;
+ var paddedLength = length + 72 & ~63;
+ var padded = new Uint8Array(paddedLength);
+ var i, j, n;
+
+ for (i = 0; i < length; ++i) {
+ padded[i] = data[offset++];
+ }
+
+ padded[i++] = 0x80;
+ n = paddedLength - 8;
+
+ while (i < n) {
+ padded[i++] = 0;
+ }
+
+ padded[i++] = length << 3 & 0xFF;
+ padded[i++] = length >> 5 & 0xFF;
+ padded[i++] = length >> 13 & 0xFF;
+ padded[i++] = length >> 21 & 0xFF;
+ padded[i++] = length >>> 29 & 0xFF;
+ padded[i++] = 0;
+ padded[i++] = 0;
+ padded[i++] = 0;
+ var w = new Int32Array(16);
+
+ for (i = 0; i < paddedLength;) {
+ for (j = 0; j < 16; ++j, i += 4) {
+ w[j] = padded[i] | padded[i + 1] << 8 | padded[i + 2] << 16 | padded[i + 3] << 24;
+ }
+
+ var a = h0,
+ b = h1,
+ c = h2,
+ d = h3,
+ f,
+ g;
+
+ for (j = 0; j < 64; ++j) {
+ if (j < 16) {
+ f = b & c | ~b & d;
+ g = j;
+ } else if (j < 32) {
+ f = d & b | ~d & c;
+ g = 5 * j + 1 & 15;
+ } else if (j < 48) {
+ f = b ^ c ^ d;
+ g = 3 * j + 5 & 15;
+ } else {
+ f = c ^ (b | ~d);
+ g = 7 * j & 15;
+ }
+
+ var tmp = d,
+ rotateArg = a + f + k[j] + w[g] | 0,
+ rotate = r[j];
+ d = c;
+ c = b;
+ b = b + (rotateArg << rotate | rotateArg >>> 32 - rotate) | 0;
+ a = tmp;
+ }
+
+ h0 = h0 + a | 0;
+ h1 = h1 + b | 0;
+ h2 = h2 + c | 0;
+ h3 = h3 + d | 0;
+ }
+
+ return new Uint8Array([h0 & 0xFF, h0 >> 8 & 0xFF, h0 >> 16 & 0xFF, h0 >>> 24 & 0xFF, h1 & 0xFF, h1 >> 8 & 0xFF, h1 >> 16 & 0xFF, h1 >>> 24 & 0xFF, h2 & 0xFF, h2 >> 8 & 0xFF, h2 >> 16 & 0xFF, h2 >>> 24 & 0xFF, h3 & 0xFF, h3 >> 8 & 0xFF, h3 >> 16 & 0xFF, h3 >>> 24 & 0xFF]);
+ }
+
+ return hash;
+}();
+
+exports.calculateMD5 = calculateMD5;
+
+var Word64 = function Word64Closure() {
+ function Word64(highInteger, lowInteger) {
+ this.high = highInteger | 0;
+ this.low = lowInteger | 0;
+ }
+
+ Word64.prototype = {
+ and: function Word64_and(word) {
+ this.high &= word.high;
+ this.low &= word.low;
+ },
+ xor: function Word64_xor(word) {
+ this.high ^= word.high;
+ this.low ^= word.low;
+ },
+ or: function Word64_or(word) {
+ this.high |= word.high;
+ this.low |= word.low;
+ },
+ shiftRight: function Word64_shiftRight(places) {
+ if (places >= 32) {
+ this.low = this.high >>> places - 32 | 0;
+ this.high = 0;
+ } else {
+ this.low = this.low >>> places | this.high << 32 - places;
+ this.high = this.high >>> places | 0;
+ }
+ },
+ shiftLeft: function Word64_shiftLeft(places) {
+ if (places >= 32) {
+ this.high = this.low << places - 32;
+ this.low = 0;
+ } else {
+ this.high = this.high << places | this.low >>> 32 - places;
+ this.low = this.low << places;
+ }
+ },
+ rotateRight: function Word64_rotateRight(places) {
+ var low, high;
+
+ if (places & 32) {
+ high = this.low;
+ low = this.high;
+ } else {
+ low = this.low;
+ high = this.high;
+ }
+
+ places &= 31;
+ this.low = low >>> places | high << 32 - places;
+ this.high = high >>> places | low << 32 - places;
+ },
+ not: function Word64_not() {
+ this.high = ~this.high;
+ this.low = ~this.low;
+ },
+ add: function Word64_add(word) {
+ var lowAdd = (this.low >>> 0) + (word.low >>> 0);
+ var highAdd = (this.high >>> 0) + (word.high >>> 0);
+
+ if (lowAdd > 0xFFFFFFFF) {
+ highAdd += 1;
+ }
+
+ this.low = lowAdd | 0;
+ this.high = highAdd | 0;
+ },
+ copyTo: function Word64_copyTo(bytes, offset) {
+ bytes[offset] = this.high >>> 24 & 0xFF;
+ bytes[offset + 1] = this.high >> 16 & 0xFF;
+ bytes[offset + 2] = this.high >> 8 & 0xFF;
+ bytes[offset + 3] = this.high & 0xFF;
+ bytes[offset + 4] = this.low >>> 24 & 0xFF;
+ bytes[offset + 5] = this.low >> 16 & 0xFF;
+ bytes[offset + 6] = this.low >> 8 & 0xFF;
+ bytes[offset + 7] = this.low & 0xFF;
+ },
+ assign: function Word64_assign(word) {
+ this.high = word.high;
+ this.low = word.low;
+ }
+ };
+ return Word64;
+}();
+
+var calculateSHA256 = function calculateSHA256Closure() {
+ function rotr(x, n) {
+ return x >>> n | x << 32 - n;
+ }
+
+ function ch(x, y, z) {
+ return x & y ^ ~x & z;
+ }
+
+ function maj(x, y, z) {
+ return x & y ^ x & z ^ y & z;
+ }
+
+ function sigma(x) {
+ return rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22);
+ }
+
+ function sigmaPrime(x) {
+ return rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25);
+ }
+
+ function littleSigma(x) {
+ return rotr(x, 7) ^ rotr(x, 18) ^ x >>> 3;
+ }
+
+ function littleSigmaPrime(x) {
+ return rotr(x, 17) ^ rotr(x, 19) ^ x >>> 10;
+ }
+
+ var k = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2];
+
+ function hash(data, offset, length) {
+ var h0 = 0x6a09e667,
+ h1 = 0xbb67ae85,
+ h2 = 0x3c6ef372,
+ h3 = 0xa54ff53a,
+ h4 = 0x510e527f,
+ h5 = 0x9b05688c,
+ h6 = 0x1f83d9ab,
+ h7 = 0x5be0cd19;
+ var paddedLength = Math.ceil((length + 9) / 64) * 64;
+ var padded = new Uint8Array(paddedLength);
+ var i, j, n;
+
+ for (i = 0; i < length; ++i) {
+ padded[i] = data[offset++];
+ }
+
+ padded[i++] = 0x80;
+ n = paddedLength - 8;
+
+ while (i < n) {
+ padded[i++] = 0;
+ }
+
+ padded[i++] = 0;
+ padded[i++] = 0;
+ padded[i++] = 0;
+ padded[i++] = length >>> 29 & 0xFF;
+ padded[i++] = length >> 21 & 0xFF;
+ padded[i++] = length >> 13 & 0xFF;
+ padded[i++] = length >> 5 & 0xFF;
+ padded[i++] = length << 3 & 0xFF;
+ var w = new Uint32Array(64);
+
+ for (i = 0; i < paddedLength;) {
+ for (j = 0; j < 16; ++j) {
+ w[j] = padded[i] << 24 | padded[i + 1] << 16 | padded[i + 2] << 8 | padded[i + 3];
+ i += 4;
+ }
+
+ for (j = 16; j < 64; ++j) {
+ w[j] = littleSigmaPrime(w[j - 2]) + w[j - 7] + littleSigma(w[j - 15]) + w[j - 16] | 0;
+ }
+
+ var a = h0,
+ b = h1,
+ c = h2,
+ d = h3,
+ e = h4,
+ f = h5,
+ g = h6,
+ h = h7,
+ t1,
+ t2;
+
+ for (j = 0; j < 64; ++j) {
+ t1 = h + sigmaPrime(e) + ch(e, f, g) + k[j] + w[j];
+ t2 = sigma(a) + maj(a, b, c);
+ h = g;
+ g = f;
+ f = e;
+ e = d + t1 | 0;
+ d = c;
+ c = b;
+ b = a;
+ a = t1 + t2 | 0;
+ }
+
+ h0 = h0 + a | 0;
+ h1 = h1 + b | 0;
+ h2 = h2 + c | 0;
+ h3 = h3 + d | 0;
+ h4 = h4 + e | 0;
+ h5 = h5 + f | 0;
+ h6 = h6 + g | 0;
+ h7 = h7 + h | 0;
+ }
+
+ return new Uint8Array([h0 >> 24 & 0xFF, h0 >> 16 & 0xFF, h0 >> 8 & 0xFF, h0 & 0xFF, h1 >> 24 & 0xFF, h1 >> 16 & 0xFF, h1 >> 8 & 0xFF, h1 & 0xFF, h2 >> 24 & 0xFF, h2 >> 16 & 0xFF, h2 >> 8 & 0xFF, h2 & 0xFF, h3 >> 24 & 0xFF, h3 >> 16 & 0xFF, h3 >> 8 & 0xFF, h3 & 0xFF, h4 >> 24 & 0xFF, h4 >> 16 & 0xFF, h4 >> 8 & 0xFF, h4 & 0xFF, h5 >> 24 & 0xFF, h5 >> 16 & 0xFF, h5 >> 8 & 0xFF, h5 & 0xFF, h6 >> 24 & 0xFF, h6 >> 16 & 0xFF, h6 >> 8 & 0xFF, h6 & 0xFF, h7 >> 24 & 0xFF, h7 >> 16 & 0xFF, h7 >> 8 & 0xFF, h7 & 0xFF]);
+ }
+
+ return hash;
+}();
+
+exports.calculateSHA256 = calculateSHA256;
+
+var calculateSHA512 = function calculateSHA512Closure() {
+ function ch(result, x, y, z, tmp) {
+ result.assign(x);
+ result.and(y);
+ tmp.assign(x);
+ tmp.not();
+ tmp.and(z);
+ result.xor(tmp);
+ }
+
+ function maj(result, x, y, z, tmp) {
+ result.assign(x);
+ result.and(y);
+ tmp.assign(x);
+ tmp.and(z);
+ result.xor(tmp);
+ tmp.assign(y);
+ tmp.and(z);
+ result.xor(tmp);
+ }
+
+ function sigma(result, x, tmp) {
+ result.assign(x);
+ result.rotateRight(28);
+ tmp.assign(x);
+ tmp.rotateRight(34);
+ result.xor(tmp);
+ tmp.assign(x);
+ tmp.rotateRight(39);
+ result.xor(tmp);
+ }
+
+ function sigmaPrime(result, x, tmp) {
+ result.assign(x);
+ result.rotateRight(14);
+ tmp.assign(x);
+ tmp.rotateRight(18);
+ result.xor(tmp);
+ tmp.assign(x);
+ tmp.rotateRight(41);
+ result.xor(tmp);
+ }
+
+ function littleSigma(result, x, tmp) {
+ result.assign(x);
+ result.rotateRight(1);
+ tmp.assign(x);
+ tmp.rotateRight(8);
+ result.xor(tmp);
+ tmp.assign(x);
+ tmp.shiftRight(7);
+ result.xor(tmp);
+ }
+
+ function littleSigmaPrime(result, x, tmp) {
+ result.assign(x);
+ result.rotateRight(19);
+ tmp.assign(x);
+ tmp.rotateRight(61);
+ result.xor(tmp);
+ tmp.assign(x);
+ tmp.shiftRight(6);
+ result.xor(tmp);
+ }
+
+ var k = [new Word64(0x428a2f98, 0xd728ae22), new Word64(0x71374491, 0x23ef65cd), new Word64(0xb5c0fbcf, 0xec4d3b2f), new Word64(0xe9b5dba5, 0x8189dbbc), new Word64(0x3956c25b, 0xf348b538), new Word64(0x59f111f1, 0xb605d019), new Word64(0x923f82a4, 0xaf194f9b), new Word64(0xab1c5ed5, 0xda6d8118), new Word64(0xd807aa98, 0xa3030242), new Word64(0x12835b01, 0x45706fbe), new Word64(0x243185be, 0x4ee4b28c), new Word64(0x550c7dc3, 0xd5ffb4e2), new Word64(0x72be5d74, 0xf27b896f), new Word64(0x80deb1fe, 0x3b1696b1), new Word64(0x9bdc06a7, 0x25c71235), new Word64(0xc19bf174, 0xcf692694), new Word64(0xe49b69c1, 0x9ef14ad2), new Word64(0xefbe4786, 0x384f25e3), new Word64(0x0fc19dc6, 0x8b8cd5b5), new Word64(0x240ca1cc, 0x77ac9c65), new Word64(0x2de92c6f, 0x592b0275), new Word64(0x4a7484aa, 0x6ea6e483), new Word64(0x5cb0a9dc, 0xbd41fbd4), new Word64(0x76f988da, 0x831153b5), new Word64(0x983e5152, 0xee66dfab), new Word64(0xa831c66d, 0x2db43210), new Word64(0xb00327c8, 0x98fb213f), new Word64(0xbf597fc7, 0xbeef0ee4), new Word64(0xc6e00bf3, 0x3da88fc2), new Word64(0xd5a79147, 0x930aa725), new Word64(0x06ca6351, 0xe003826f), new Word64(0x14292967, 0x0a0e6e70), new Word64(0x27b70a85, 0x46d22ffc), new Word64(0x2e1b2138, 0x5c26c926), new Word64(0x4d2c6dfc, 0x5ac42aed), new Word64(0x53380d13, 0x9d95b3df), new Word64(0x650a7354, 0x8baf63de), new Word64(0x766a0abb, 0x3c77b2a8), new Word64(0x81c2c92e, 0x47edaee6), new Word64(0x92722c85, 0x1482353b), new Word64(0xa2bfe8a1, 0x4cf10364), new Word64(0xa81a664b, 0xbc423001), new Word64(0xc24b8b70, 0xd0f89791), new Word64(0xc76c51a3, 0x0654be30), new Word64(0xd192e819, 0xd6ef5218), new Word64(0xd6990624, 0x5565a910), new Word64(0xf40e3585, 0x5771202a), new Word64(0x106aa070, 0x32bbd1b8), new Word64(0x19a4c116, 0xb8d2d0c8), new Word64(0x1e376c08, 0x5141ab53), new Word64(0x2748774c, 0xdf8eeb99), new Word64(0x34b0bcb5, 0xe19b48a8), new Word64(0x391c0cb3, 0xc5c95a63), new Word64(0x4ed8aa4a, 0xe3418acb), new Word64(0x5b9cca4f, 0x7763e373), new Word64(0x682e6ff3, 0xd6b2b8a3), new Word64(0x748f82ee, 0x5defb2fc), new Word64(0x78a5636f, 0x43172f60), new Word64(0x84c87814, 0xa1f0ab72), new Word64(0x8cc70208, 0x1a6439ec), new Word64(0x90befffa, 0x23631e28), new Word64(0xa4506ceb, 0xde82bde9), new Word64(0xbef9a3f7, 0xb2c67915), new Word64(0xc67178f2, 0xe372532b), new Word64(0xca273ece, 0xea26619c), new Word64(0xd186b8c7, 0x21c0c207), new Word64(0xeada7dd6, 0xcde0eb1e), new Word64(0xf57d4f7f, 0xee6ed178), new Word64(0x06f067aa, 0x72176fba), new Word64(0x0a637dc5, 0xa2c898a6), new Word64(0x113f9804, 0xbef90dae), new Word64(0x1b710b35, 0x131c471b), new Word64(0x28db77f5, 0x23047d84), new Word64(0x32caab7b, 0x40c72493), new Word64(0x3c9ebe0a, 0x15c9bebc), new Word64(0x431d67c4, 0x9c100d4c), new Word64(0x4cc5d4be, 0xcb3e42b6), new Word64(0x597f299c, 0xfc657e2a), new Word64(0x5fcb6fab, 0x3ad6faec), new Word64(0x6c44198c, 0x4a475817)];
+
+ function hash(data, offset, length, mode384) {
+ mode384 = !!mode384;
+ var h0, h1, h2, h3, h4, h5, h6, h7;
+
+ if (!mode384) {
+ h0 = new Word64(0x6a09e667, 0xf3bcc908);
+ h1 = new Word64(0xbb67ae85, 0x84caa73b);
+ h2 = new Word64(0x3c6ef372, 0xfe94f82b);
+ h3 = new Word64(0xa54ff53a, 0x5f1d36f1);
+ h4 = new Word64(0x510e527f, 0xade682d1);
+ h5 = new Word64(0x9b05688c, 0x2b3e6c1f);
+ h6 = new Word64(0x1f83d9ab, 0xfb41bd6b);
+ h7 = new Word64(0x5be0cd19, 0x137e2179);
+ } else {
+ h0 = new Word64(0xcbbb9d5d, 0xc1059ed8);
+ h1 = new Word64(0x629a292a, 0x367cd507);
+ h2 = new Word64(0x9159015a, 0x3070dd17);
+ h3 = new Word64(0x152fecd8, 0xf70e5939);
+ h4 = new Word64(0x67332667, 0xffc00b31);
+ h5 = new Word64(0x8eb44a87, 0x68581511);
+ h6 = new Word64(0xdb0c2e0d, 0x64f98fa7);
+ h7 = new Word64(0x47b5481d, 0xbefa4fa4);
+ }
+
+ var paddedLength = Math.ceil((length + 17) / 128) * 128;
+ var padded = new Uint8Array(paddedLength);
+ var i, j, n;
+
+ for (i = 0; i < length; ++i) {
+ padded[i] = data[offset++];
+ }
+
+ padded[i++] = 0x80;
+ n = paddedLength - 16;
+
+ while (i < n) {
+ padded[i++] = 0;
+ }
+
+ padded[i++] = 0;
+ padded[i++] = 0;
+ padded[i++] = 0;
+ padded[i++] = 0;
+ padded[i++] = 0;
+ padded[i++] = 0;
+ padded[i++] = 0;
+ padded[i++] = 0;
+ padded[i++] = 0;
+ padded[i++] = 0;
+ padded[i++] = 0;
+ padded[i++] = length >>> 29 & 0xFF;
+ padded[i++] = length >> 21 & 0xFF;
+ padded[i++] = length >> 13 & 0xFF;
+ padded[i++] = length >> 5 & 0xFF;
+ padded[i++] = length << 3 & 0xFF;
+ var w = new Array(80);
+
+ for (i = 0; i < 80; i++) {
+ w[i] = new Word64(0, 0);
+ }
+
+ var a = new Word64(0, 0),
+ b = new Word64(0, 0),
+ c = new Word64(0, 0);
+ var d = new Word64(0, 0),
+ e = new Word64(0, 0),
+ f = new Word64(0, 0);
+ var g = new Word64(0, 0),
+ h = new Word64(0, 0);
+ var t1 = new Word64(0, 0),
+ t2 = new Word64(0, 0);
+ var tmp1 = new Word64(0, 0),
+ tmp2 = new Word64(0, 0),
+ tmp3;
+
+ for (i = 0; i < paddedLength;) {
+ for (j = 0; j < 16; ++j) {
+ w[j].high = padded[i] << 24 | padded[i + 1] << 16 | padded[i + 2] << 8 | padded[i + 3];
+ w[j].low = padded[i + 4] << 24 | padded[i + 5] << 16 | padded[i + 6] << 8 | padded[i + 7];
+ i += 8;
+ }
+
+ for (j = 16; j < 80; ++j) {
+ tmp3 = w[j];
+ littleSigmaPrime(tmp3, w[j - 2], tmp2);
+ tmp3.add(w[j - 7]);
+ littleSigma(tmp1, w[j - 15], tmp2);
+ tmp3.add(tmp1);
+ tmp3.add(w[j - 16]);
+ }
+
+ a.assign(h0);
+ b.assign(h1);
+ c.assign(h2);
+ d.assign(h3);
+ e.assign(h4);
+ f.assign(h5);
+ g.assign(h6);
+ h.assign(h7);
+
+ for (j = 0; j < 80; ++j) {
+ t1.assign(h);
+ sigmaPrime(tmp1, e, tmp2);
+ t1.add(tmp1);
+ ch(tmp1, e, f, g, tmp2);
+ t1.add(tmp1);
+ t1.add(k[j]);
+ t1.add(w[j]);
+ sigma(t2, a, tmp2);
+ maj(tmp1, a, b, c, tmp2);
+ t2.add(tmp1);
+ tmp3 = h;
+ h = g;
+ g = f;
+ f = e;
+ d.add(t1);
+ e = d;
+ d = c;
+ c = b;
+ b = a;
+ tmp3.assign(t1);
+ tmp3.add(t2);
+ a = tmp3;
+ }
+
+ h0.add(a);
+ h1.add(b);
+ h2.add(c);
+ h3.add(d);
+ h4.add(e);
+ h5.add(f);
+ h6.add(g);
+ h7.add(h);
+ }
+
+ var result;
+
+ if (!mode384) {
+ result = new Uint8Array(64);
+ h0.copyTo(result, 0);
+ h1.copyTo(result, 8);
+ h2.copyTo(result, 16);
+ h3.copyTo(result, 24);
+ h4.copyTo(result, 32);
+ h5.copyTo(result, 40);
+ h6.copyTo(result, 48);
+ h7.copyTo(result, 56);
+ } else {
+ result = new Uint8Array(48);
+ h0.copyTo(result, 0);
+ h1.copyTo(result, 8);
+ h2.copyTo(result, 16);
+ h3.copyTo(result, 24);
+ h4.copyTo(result, 32);
+ h5.copyTo(result, 40);
+ }
+
+ return result;
+ }
+
+ return hash;
+}();
+
+exports.calculateSHA512 = calculateSHA512;
+
+var calculateSHA384 = function calculateSHA384Closure() {
+ function hash(data, offset, length) {
+ return calculateSHA512(data, offset, length, true);
+ }
+
+ return hash;
+}();
+
+exports.calculateSHA384 = calculateSHA384;
+
+var NullCipher = function NullCipherClosure() {
+ function NullCipher() {}
+
+ NullCipher.prototype = {
+ decryptBlock: function NullCipher_decryptBlock(data) {
+ return data;
+ }
+ };
+ return NullCipher;
+}();
+
+var AESBaseCipher =
+/*#__PURE__*/
+function () {
+ function AESBaseCipher() {
+ _classCallCheck(this, AESBaseCipher);
+
+ if (this.constructor === AESBaseCipher) {
+ (0, _util.unreachable)('Cannot initialize AESBaseCipher.');
+ }
+
+ this._s = new Uint8Array([0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16]);
+ this._inv_s = new Uint8Array([0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d]);
+ this._mix = new Uint32Array([0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3]);
+ this._mixCol = new Uint8Array(256);
+
+ for (var i = 0; i < 256; i++) {
+ if (i < 128) {
+ this._mixCol[i] = i << 1;
+ } else {
+ this._mixCol[i] = i << 1 ^ 0x1b;
+ }
+ }
+
+ this.buffer = new Uint8Array(16);
+ this.bufferPosition = 0;
+ }
+
+ _createClass(AESBaseCipher, [{
+ key: "_expandKey",
+ value: function _expandKey(cipherKey) {
+ (0, _util.unreachable)('Cannot call `_expandKey` on the base class');
+ }
+ }, {
+ key: "_decrypt",
+ value: function _decrypt(input, key) {
+ var t, u, v;
+ var state = new Uint8Array(16);
+ state.set(input);
+
+ for (var j = 0, k = this._keySize; j < 16; ++j, ++k) {
+ state[j] ^= key[k];
+ }
+
+ for (var i = this._cyclesOfRepetition - 1; i >= 1; --i) {
+ t = state[13];
+ state[13] = state[9];
+ state[9] = state[5];
+ state[5] = state[1];
+ state[1] = t;
+ t = state[14];
+ u = state[10];
+ state[14] = state[6];
+ state[10] = state[2];
+ state[6] = t;
+ state[2] = u;
+ t = state[15];
+ u = state[11];
+ v = state[7];
+ state[15] = state[3];
+ state[11] = t;
+ state[7] = u;
+ state[3] = v;
+
+ for (var _j = 0; _j < 16; ++_j) {
+ state[_j] = this._inv_s[state[_j]];
+ }
+
+ for (var _j2 = 0, _k = i * 16; _j2 < 16; ++_j2, ++_k) {
+ state[_j2] ^= key[_k];
+ }
+
+ for (var _j3 = 0; _j3 < 16; _j3 += 4) {
+ var s0 = this._mix[state[_j3]];
+ var s1 = this._mix[state[_j3 + 1]];
+ var s2 = this._mix[state[_j3 + 2]];
+ var s3 = this._mix[state[_j3 + 3]];
+ t = s0 ^ s1 >>> 8 ^ s1 << 24 ^ s2 >>> 16 ^ s2 << 16 ^ s3 >>> 24 ^ s3 << 8;
+ state[_j3] = t >>> 24 & 0xFF;
+ state[_j3 + 1] = t >> 16 & 0xFF;
+ state[_j3 + 2] = t >> 8 & 0xFF;
+ state[_j3 + 3] = t & 0xFF;
+ }
+ }
+
+ t = state[13];
+ state[13] = state[9];
+ state[9] = state[5];
+ state[5] = state[1];
+ state[1] = t;
+ t = state[14];
+ u = state[10];
+ state[14] = state[6];
+ state[10] = state[2];
+ state[6] = t;
+ state[2] = u;
+ t = state[15];
+ u = state[11];
+ v = state[7];
+ state[15] = state[3];
+ state[11] = t;
+ state[7] = u;
+ state[3] = v;
+
+ for (var _j4 = 0; _j4 < 16; ++_j4) {
+ state[_j4] = this._inv_s[state[_j4]];
+ state[_j4] ^= key[_j4];
+ }
+
+ return state;
+ }
+ }, {
+ key: "_encrypt",
+ value: function _encrypt(input, key) {
+ var s = this._s;
+ var t, u, v;
+ var state = new Uint8Array(16);
+ state.set(input);
+
+ for (var j = 0; j < 16; ++j) {
+ state[j] ^= key[j];
+ }
+
+ for (var i = 1; i < this._cyclesOfRepetition; i++) {
+ for (var _j5 = 0; _j5 < 16; ++_j5) {
+ state[_j5] = s[state[_j5]];
+ }
+
+ v = state[1];
+ state[1] = state[5];
+ state[5] = state[9];
+ state[9] = state[13];
+ state[13] = v;
+ v = state[2];
+ u = state[6];
+ state[2] = state[10];
+ state[6] = state[14];
+ state[10] = v;
+ state[14] = u;
+ v = state[3];
+ u = state[7];
+ t = state[11];
+ state[3] = state[15];
+ state[7] = v;
+ state[11] = u;
+ state[15] = t;
+
+ for (var _j6 = 0; _j6 < 16; _j6 += 4) {
+ var s0 = state[_j6 + 0];
+ var s1 = state[_j6 + 1];
+ var s2 = state[_j6 + 2];
+ var s3 = state[_j6 + 3];
+ t = s0 ^ s1 ^ s2 ^ s3;
+ state[_j6 + 0] ^= t ^ this._mixCol[s0 ^ s1];
+ state[_j6 + 1] ^= t ^ this._mixCol[s1 ^ s2];
+ state[_j6 + 2] ^= t ^ this._mixCol[s2 ^ s3];
+ state[_j6 + 3] ^= t ^ this._mixCol[s3 ^ s0];
+ }
+
+ for (var _j7 = 0, k = i * 16; _j7 < 16; ++_j7, ++k) {
+ state[_j7] ^= key[k];
+ }
+ }
+
+ for (var _j8 = 0; _j8 < 16; ++_j8) {
+ state[_j8] = s[state[_j8]];
+ }
+
+ v = state[1];
+ state[1] = state[5];
+ state[5] = state[9];
+ state[9] = state[13];
+ state[13] = v;
+ v = state[2];
+ u = state[6];
+ state[2] = state[10];
+ state[6] = state[14];
+ state[10] = v;
+ state[14] = u;
+ v = state[3];
+ u = state[7];
+ t = state[11];
+ state[3] = state[15];
+ state[7] = v;
+ state[11] = u;
+ state[15] = t;
+
+ for (var _j9 = 0, _k2 = this._keySize; _j9 < 16; ++_j9, ++_k2) {
+ state[_j9] ^= key[_k2];
+ }
+
+ return state;
+ }
+ }, {
+ key: "_decryptBlock2",
+ value: function _decryptBlock2(data, finalize) {
+ var sourceLength = data.length;
+ var buffer = this.buffer,
+ bufferLength = this.bufferPosition;
+ var result = [],
+ iv = this.iv;
+
+ for (var i = 0; i < sourceLength; ++i) {
+ buffer[bufferLength] = data[i];
+ ++bufferLength;
+
+ if (bufferLength < 16) {
+ continue;
+ }
+
+ var plain = this._decrypt(buffer, this._key);
+
+ for (var j = 0; j < 16; ++j) {
+ plain[j] ^= iv[j];
+ }
+
+ iv = buffer;
+ result.push(plain);
+ buffer = new Uint8Array(16);
+ bufferLength = 0;
+ }
+
+ this.buffer = buffer;
+ this.bufferLength = bufferLength;
+ this.iv = iv;
+
+ if (result.length === 0) {
+ return new Uint8Array(0);
+ }
+
+ var outputLength = 16 * result.length;
+
+ if (finalize) {
+ var lastBlock = result[result.length - 1];
+ var psLen = lastBlock[15];
+
+ if (psLen <= 16) {
+ for (var _i = 15, ii = 16 - psLen; _i >= ii; --_i) {
+ if (lastBlock[_i] !== psLen) {
+ psLen = 0;
+ break;
+ }
+ }
+
+ outputLength -= psLen;
+ result[result.length - 1] = lastBlock.subarray(0, 16 - psLen);
+ }
+ }
+
+ var output = new Uint8Array(outputLength);
+
+ for (var _i2 = 0, _j10 = 0, _ii = result.length; _i2 < _ii; ++_i2, _j10 += 16) {
+ output.set(result[_i2], _j10);
+ }
+
+ return output;
+ }
+ }, {
+ key: "decryptBlock",
+ value: function decryptBlock(data, finalize) {
+ var iv = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
+ var sourceLength = data.length;
+ var buffer = this.buffer,
+ bufferLength = this.bufferPosition;
+
+ if (iv) {
+ this.iv = iv;
+ } else {
+ for (var i = 0; bufferLength < 16 && i < sourceLength; ++i, ++bufferLength) {
+ buffer[bufferLength] = data[i];
+ }
+
+ if (bufferLength < 16) {
+ this.bufferLength = bufferLength;
+ return new Uint8Array(0);
+ }
+
+ this.iv = buffer;
+ data = data.subarray(16);
+ }
+
+ this.buffer = new Uint8Array(16);
+ this.bufferLength = 0;
+ this.decryptBlock = this._decryptBlock2;
+ return this.decryptBlock(data, finalize);
+ }
+ }, {
+ key: "encrypt",
+ value: function encrypt(data, iv) {
+ var sourceLength = data.length;
+ var buffer = this.buffer,
+ bufferLength = this.bufferPosition;
+ var result = [];
+
+ if (!iv) {
+ iv = new Uint8Array(16);
+ }
+
+ for (var i = 0; i < sourceLength; ++i) {
+ buffer[bufferLength] = data[i];
+ ++bufferLength;
+
+ if (bufferLength < 16) {
+ continue;
+ }
+
+ for (var j = 0; j < 16; ++j) {
+ buffer[j] ^= iv[j];
+ }
+
+ var cipher = this._encrypt(buffer, this._key);
+
+ iv = cipher;
+ result.push(cipher);
+ buffer = new Uint8Array(16);
+ bufferLength = 0;
+ }
+
+ this.buffer = buffer;
+ this.bufferLength = bufferLength;
+ this.iv = iv;
+
+ if (result.length === 0) {
+ return new Uint8Array(0);
+ }
+
+ var outputLength = 16 * result.length;
+ var output = new Uint8Array(outputLength);
+
+ for (var _i3 = 0, _j11 = 0, ii = result.length; _i3 < ii; ++_i3, _j11 += 16) {
+ output.set(result[_i3], _j11);
+ }
+
+ return output;
+ }
+ }]);
+
+ return AESBaseCipher;
+}();
+
+var AES128Cipher =
+/*#__PURE__*/
+function (_AESBaseCipher) {
+ _inherits(AES128Cipher, _AESBaseCipher);
+
+ function AES128Cipher(key) {
+ var _this;
+
+ _classCallCheck(this, AES128Cipher);
+
+ _this = _possibleConstructorReturn(this, _getPrototypeOf(AES128Cipher).call(this));
+ _this._cyclesOfRepetition = 10;
+ _this._keySize = 160;
+ _this._rcon = new Uint8Array([0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d]);
+ _this._key = _this._expandKey(key);
+ return _this;
+ }
+
+ _createClass(AES128Cipher, [{
+ key: "_expandKey",
+ value: function _expandKey(cipherKey) {
+ var b = 176;
+ var s = this._s;
+ var rcon = this._rcon;
+ var result = new Uint8Array(b);
+ result.set(cipherKey);
+
+ for (var j = 16, i = 1; j < b; ++i) {
+ var t1 = result[j - 3];
+ var t2 = result[j - 2];
+ var t3 = result[j - 1];
+ var t4 = result[j - 4];
+ t1 = s[t1];
+ t2 = s[t2];
+ t3 = s[t3];
+ t4 = s[t4];
+ t1 = t1 ^ rcon[i];
+
+ for (var n = 0; n < 4; ++n) {
+ result[j] = t1 ^= result[j - 16];
+ j++;
+ result[j] = t2 ^= result[j - 16];
+ j++;
+ result[j] = t3 ^= result[j - 16];
+ j++;
+ result[j] = t4 ^= result[j - 16];
+ j++;
+ }
+ }
+
+ return result;
+ }
+ }]);
+
+ return AES128Cipher;
+}(AESBaseCipher);
+
+exports.AES128Cipher = AES128Cipher;
+
+var AES256Cipher =
+/*#__PURE__*/
+function (_AESBaseCipher2) {
+ _inherits(AES256Cipher, _AESBaseCipher2);
+
+ function AES256Cipher(key) {
+ var _this2;
+
+ _classCallCheck(this, AES256Cipher);
+
+ _this2 = _possibleConstructorReturn(this, _getPrototypeOf(AES256Cipher).call(this));
+ _this2._cyclesOfRepetition = 14;
+ _this2._keySize = 224;
+ _this2._key = _this2._expandKey(key);
+ return _this2;
+ }
+
+ _createClass(AES256Cipher, [{
+ key: "_expandKey",
+ value: function _expandKey(cipherKey) {
+ var b = 240;
+ var s = this._s;
+ var result = new Uint8Array(b);
+ result.set(cipherKey);
+ var r = 1;
+ var t1, t2, t3, t4;
+
+ for (var j = 32, i = 1; j < b; ++i) {
+ if (j % 32 === 16) {
+ t1 = s[t1];
+ t2 = s[t2];
+ t3 = s[t3];
+ t4 = s[t4];
+ } else if (j % 32 === 0) {
+ t1 = result[j - 3];
+ t2 = result[j - 2];
+ t3 = result[j - 1];
+ t4 = result[j - 4];
+ t1 = s[t1];
+ t2 = s[t2];
+ t3 = s[t3];
+ t4 = s[t4];
+ t1 = t1 ^ r;
+
+ if ((r <<= 1) >= 256) {
+ r = (r ^ 0x1b) & 0xFF;
+ }
+ }
+
+ for (var n = 0; n < 4; ++n) {
+ result[j] = t1 ^= result[j - 32];
+ j++;
+ result[j] = t2 ^= result[j - 32];
+ j++;
+ result[j] = t3 ^= result[j - 32];
+ j++;
+ result[j] = t4 ^= result[j - 32];
+ j++;
+ }
+ }
+
+ return result;
+ }
+ }]);
+
+ return AES256Cipher;
+}(AESBaseCipher);
+
+exports.AES256Cipher = AES256Cipher;
+
+var PDF17 = function PDF17Closure() {
+ function compareByteArrays(array1, array2) {
+ if (array1.length !== array2.length) {
+ return false;
+ }
+
+ for (var i = 0; i < array1.length; i++) {
+ if (array1[i] !== array2[i]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ function PDF17() {}
+
+ PDF17.prototype = {
+ checkOwnerPassword: function PDF17_checkOwnerPassword(password, ownerValidationSalt, userBytes, ownerPassword) {
+ var hashData = new Uint8Array(password.length + 56);
+ hashData.set(password, 0);
+ hashData.set(ownerValidationSalt, password.length);
+ hashData.set(userBytes, password.length + ownerValidationSalt.length);
+ var result = calculateSHA256(hashData, 0, hashData.length);
+ return compareByteArrays(result, ownerPassword);
+ },
+ checkUserPassword: function PDF17_checkUserPassword(password, userValidationSalt, userPassword) {
+ var hashData = new Uint8Array(password.length + 8);
+ hashData.set(password, 0);
+ hashData.set(userValidationSalt, password.length);
+ var result = calculateSHA256(hashData, 0, hashData.length);
+ return compareByteArrays(result, userPassword);
+ },
+ getOwnerKey: function PDF17_getOwnerKey(password, ownerKeySalt, userBytes, ownerEncryption) {
+ var hashData = new Uint8Array(password.length + 56);
+ hashData.set(password, 0);
+ hashData.set(ownerKeySalt, password.length);
+ hashData.set(userBytes, password.length + ownerKeySalt.length);
+ var key = calculateSHA256(hashData, 0, hashData.length);
+ var cipher = new AES256Cipher(key);
+ return cipher.decryptBlock(ownerEncryption, false, new Uint8Array(16));
+ },
+ getUserKey: function PDF17_getUserKey(password, userKeySalt, userEncryption) {
+ var hashData = new Uint8Array(password.length + 8);
+ hashData.set(password, 0);
+ hashData.set(userKeySalt, password.length);
+ var key = calculateSHA256(hashData, 0, hashData.length);
+ var cipher = new AES256Cipher(key);
+ return cipher.decryptBlock(userEncryption, false, new Uint8Array(16));
+ }
+ };
+ return PDF17;
+}();
+
+exports.PDF17 = PDF17;
+
+var PDF20 = function PDF20Closure() {
+ function concatArrays(array1, array2) {
+ var t = new Uint8Array(array1.length + array2.length);
+ t.set(array1, 0);
+ t.set(array2, array1.length);
+ return t;
+ }
+
+ function calculatePDF20Hash(password, input, userBytes) {
+ var k = calculateSHA256(input, 0, input.length).subarray(0, 32);
+ var e = [0];
+ var i = 0;
+
+ while (i < 64 || e[e.length - 1] > i - 32) {
+ var arrayLength = password.length + k.length + userBytes.length;
+ var k1 = new Uint8Array(arrayLength * 64);
+ var array = concatArrays(password, k);
+ array = concatArrays(array, userBytes);
+
+ for (var j = 0, pos = 0; j < 64; j++, pos += arrayLength) {
+ k1.set(array, pos);
+ }
+
+ var cipher = new AES128Cipher(k.subarray(0, 16));
+ e = cipher.encrypt(k1, k.subarray(16, 32));
+ var remainder = 0;
+
+ for (var z = 0; z < 16; z++) {
+ remainder *= 256 % 3;
+ remainder %= 3;
+ remainder += (e[z] >>> 0) % 3;
+ remainder %= 3;
+ }
+
+ if (remainder === 0) {
+ k = calculateSHA256(e, 0, e.length);
+ } else if (remainder === 1) {
+ k = calculateSHA384(e, 0, e.length);
+ } else if (remainder === 2) {
+ k = calculateSHA512(e, 0, e.length);
+ }
+
+ i++;
+ }
+
+ return k.subarray(0, 32);
+ }
+
+ function PDF20() {}
+
+ function compareByteArrays(array1, array2) {
+ if (array1.length !== array2.length) {
+ return false;
+ }
+
+ for (var i = 0; i < array1.length; i++) {
+ if (array1[i] !== array2[i]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ PDF20.prototype = {
+ hash: function PDF20_hash(password, concatBytes, userBytes) {
+ return calculatePDF20Hash(password, concatBytes, userBytes);
+ },
+ checkOwnerPassword: function PDF20_checkOwnerPassword(password, ownerValidationSalt, userBytes, ownerPassword) {
+ var hashData = new Uint8Array(password.length + 56);
+ hashData.set(password, 0);
+ hashData.set(ownerValidationSalt, password.length);
+ hashData.set(userBytes, password.length + ownerValidationSalt.length);
+ var result = calculatePDF20Hash(password, hashData, userBytes);
+ return compareByteArrays(result, ownerPassword);
+ },
+ checkUserPassword: function PDF20_checkUserPassword(password, userValidationSalt, userPassword) {
+ var hashData = new Uint8Array(password.length + 8);
+ hashData.set(password, 0);
+ hashData.set(userValidationSalt, password.length);
+ var result = calculatePDF20Hash(password, hashData, []);
+ return compareByteArrays(result, userPassword);
+ },
+ getOwnerKey: function PDF20_getOwnerKey(password, ownerKeySalt, userBytes, ownerEncryption) {
+ var hashData = new Uint8Array(password.length + 56);
+ hashData.set(password, 0);
+ hashData.set(ownerKeySalt, password.length);
+ hashData.set(userBytes, password.length + ownerKeySalt.length);
+ var key = calculatePDF20Hash(password, hashData, userBytes);
+ var cipher = new AES256Cipher(key);
+ return cipher.decryptBlock(ownerEncryption, false, new Uint8Array(16));
+ },
+ getUserKey: function PDF20_getUserKey(password, userKeySalt, userEncryption) {
+ var hashData = new Uint8Array(password.length + 8);
+ hashData.set(password, 0);
+ hashData.set(userKeySalt, password.length);
+ var key = calculatePDF20Hash(password, hashData, []);
+ var cipher = new AES256Cipher(key);
+ return cipher.decryptBlock(userEncryption, false, new Uint8Array(16));
+ }
+ };
+ return PDF20;
+}();
+
+exports.PDF20 = PDF20;
+
+var CipherTransform = function CipherTransformClosure() {
+ function CipherTransform(stringCipherConstructor, streamCipherConstructor) {
+ this.StringCipherConstructor = stringCipherConstructor;
+ this.StreamCipherConstructor = streamCipherConstructor;
+ }
+
+ CipherTransform.prototype = {
+ createStream: function CipherTransform_createStream(stream, length) {
+ var cipher = new this.StreamCipherConstructor();
+ return new _stream.DecryptStream(stream, length, function cipherTransformDecryptStream(data, finalize) {
+ return cipher.decryptBlock(data, finalize);
+ });
+ },
+ decryptString: function CipherTransform_decryptString(s) {
+ var cipher = new this.StringCipherConstructor();
+ var data = (0, _util.stringToBytes)(s);
+ data = cipher.decryptBlock(data, true);
+ return (0, _util.bytesToString)(data);
+ }
+ };
+ return CipherTransform;
+}();
+
+var CipherTransformFactory = function CipherTransformFactoryClosure() {
+ var defaultPasswordBytes = new Uint8Array([0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A]);
+
+ function createEncryptionKey20(revision, password, ownerPassword, ownerValidationSalt, ownerKeySalt, uBytes, userPassword, userValidationSalt, userKeySalt, ownerEncryption, userEncryption, perms) {
+ if (password) {
+ var passwordLength = Math.min(127, password.length);
+ password = password.subarray(0, passwordLength);
+ } else {
+ password = [];
+ }
+
+ var pdfAlgorithm;
+
+ if (revision === 6) {
+ pdfAlgorithm = new PDF20();
+ } else {
+ pdfAlgorithm = new PDF17();
+ }
+
+ if (pdfAlgorithm.checkUserPassword(password, userValidationSalt, userPassword)) {
+ return pdfAlgorithm.getUserKey(password, userKeySalt, userEncryption);
+ } else if (password.length && pdfAlgorithm.checkOwnerPassword(password, ownerValidationSalt, uBytes, ownerPassword)) {
+ return pdfAlgorithm.getOwnerKey(password, ownerKeySalt, uBytes, ownerEncryption);
+ }
+
+ return null;
+ }
+
+ function prepareKeyData(fileId, password, ownerPassword, userPassword, flags, revision, keyLength, encryptMetadata) {
+ var hashDataSize = 40 + ownerPassword.length + fileId.length;
+ var hashData = new Uint8Array(hashDataSize),
+ i = 0,
+ j,
+ n;
+
+ if (password) {
+ n = Math.min(32, password.length);
+
+ for (; i < n; ++i) {
+ hashData[i] = password[i];
+ }
+ }
+
+ j = 0;
+
+ while (i < 32) {
+ hashData[i++] = defaultPasswordBytes[j++];
+ }
+
+ for (j = 0, n = ownerPassword.length; j < n; ++j) {
+ hashData[i++] = ownerPassword[j];
+ }
+
+ hashData[i++] = flags & 0xFF;
+ hashData[i++] = flags >> 8 & 0xFF;
+ hashData[i++] = flags >> 16 & 0xFF;
+ hashData[i++] = flags >>> 24 & 0xFF;
+
+ for (j = 0, n = fileId.length; j < n; ++j) {
+ hashData[i++] = fileId[j];
+ }
+
+ if (revision >= 4 && !encryptMetadata) {
+ hashData[i++] = 0xFF;
+ hashData[i++] = 0xFF;
+ hashData[i++] = 0xFF;
+ hashData[i++] = 0xFF;
+ }
+
+ var hash = calculateMD5(hashData, 0, i);
+ var keyLengthInBytes = keyLength >> 3;
+
+ if (revision >= 3) {
+ for (j = 0; j < 50; ++j) {
+ hash = calculateMD5(hash, 0, keyLengthInBytes);
+ }
+ }
+
+ var encryptionKey = hash.subarray(0, keyLengthInBytes);
+ var cipher, checkData;
+
+ if (revision >= 3) {
+ for (i = 0; i < 32; ++i) {
+ hashData[i] = defaultPasswordBytes[i];
+ }
+
+ for (j = 0, n = fileId.length; j < n; ++j) {
+ hashData[i++] = fileId[j];
+ }
+
+ cipher = new ARCFourCipher(encryptionKey);
+ checkData = cipher.encryptBlock(calculateMD5(hashData, 0, i));
+ n = encryptionKey.length;
+ var derivedKey = new Uint8Array(n),
+ k;
+
+ for (j = 1; j <= 19; ++j) {
+ for (k = 0; k < n; ++k) {
+ derivedKey[k] = encryptionKey[k] ^ j;
+ }
+
+ cipher = new ARCFourCipher(derivedKey);
+ checkData = cipher.encryptBlock(checkData);
+ }
+
+ for (j = 0, n = checkData.length; j < n; ++j) {
+ if (userPassword[j] !== checkData[j]) {
+ return null;
+ }
+ }
+ } else {
+ cipher = new ARCFourCipher(encryptionKey);
+ checkData = cipher.encryptBlock(defaultPasswordBytes);
+
+ for (j = 0, n = checkData.length; j < n; ++j) {
+ if (userPassword[j] !== checkData[j]) {
+ return null;
+ }
+ }
+ }
+
+ return encryptionKey;
+ }
+
+ function decodeUserPassword(password, ownerPassword, revision, keyLength) {
+ var hashData = new Uint8Array(32),
+ i = 0,
+ j,
+ n;
+ n = Math.min(32, password.length);
+
+ for (; i < n; ++i) {
+ hashData[i] = password[i];
+ }
+
+ j = 0;
+
+ while (i < 32) {
+ hashData[i++] = defaultPasswordBytes[j++];
+ }
+
+ var hash = calculateMD5(hashData, 0, i);
+ var keyLengthInBytes = keyLength >> 3;
+
+ if (revision >= 3) {
+ for (j = 0; j < 50; ++j) {
+ hash = calculateMD5(hash, 0, hash.length);
+ }
+ }
+
+ var cipher, userPassword;
+
+ if (revision >= 3) {
+ userPassword = ownerPassword;
+ var derivedKey = new Uint8Array(keyLengthInBytes),
+ k;
+
+ for (j = 19; j >= 0; j--) {
+ for (k = 0; k < keyLengthInBytes; ++k) {
+ derivedKey[k] = hash[k] ^ j;
+ }
+
+ cipher = new ARCFourCipher(derivedKey);
+ userPassword = cipher.encryptBlock(userPassword);
+ }
+ } else {
+ cipher = new ARCFourCipher(hash.subarray(0, keyLengthInBytes));
+ userPassword = cipher.encryptBlock(ownerPassword);
+ }
+
+ return userPassword;
+ }
+
+ var identityName = _primitives.Name.get('Identity');
+
+ function CipherTransformFactory(dict, fileId, password) {
+ var filter = dict.get('Filter');
+
+ if (!(0, _primitives.isName)(filter, 'Standard')) {
+ throw new _util.FormatError('unknown encryption method');
+ }
+
+ this.dict = dict;
+ var algorithm = dict.get('V');
+
+ if (!Number.isInteger(algorithm) || algorithm !== 1 && algorithm !== 2 && algorithm !== 4 && algorithm !== 5) {
+ throw new _util.FormatError('unsupported encryption algorithm');
+ }
+
+ this.algorithm = algorithm;
+ var keyLength = dict.get('Length');
+
+ if (!keyLength) {
+ if (algorithm <= 3) {
+ keyLength = 40;
+ } else {
+ var cfDict = dict.get('CF');
+ var streamCryptoName = dict.get('StmF');
+
+ if ((0, _primitives.isDict)(cfDict) && (0, _primitives.isName)(streamCryptoName)) {
+ cfDict.suppressEncryption = true;
+ var handlerDict = cfDict.get(streamCryptoName.name);
+ keyLength = handlerDict && handlerDict.get('Length') || 128;
+
+ if (keyLength < 40) {
+ keyLength <<= 3;
+ }
+ }
+ }
+ }
+
+ if (!Number.isInteger(keyLength) || keyLength < 40 || keyLength % 8 !== 0) {
+ throw new _util.FormatError('invalid key length');
+ }
+
+ var ownerPassword = (0, _util.stringToBytes)(dict.get('O')).subarray(0, 32);
+ var userPassword = (0, _util.stringToBytes)(dict.get('U')).subarray(0, 32);
+ var flags = dict.get('P');
+ var revision = dict.get('R');
+ var encryptMetadata = (algorithm === 4 || algorithm === 5) && dict.get('EncryptMetadata') !== false;
+ this.encryptMetadata = encryptMetadata;
+ var fileIdBytes = (0, _util.stringToBytes)(fileId);
+ var passwordBytes;
+
+ if (password) {
+ if (revision === 6) {
+ try {
+ password = (0, _util.utf8StringToString)(password);
+ } catch (ex) {
+ (0, _util.warn)('CipherTransformFactory: ' + 'Unable to convert UTF8 encoded password.');
+ }
+ }
+
+ passwordBytes = (0, _util.stringToBytes)(password);
+ }
+
+ var encryptionKey;
+
+ if (algorithm !== 5) {
+ encryptionKey = prepareKeyData(fileIdBytes, passwordBytes, ownerPassword, userPassword, flags, revision, keyLength, encryptMetadata);
+ } else {
+ var ownerValidationSalt = (0, _util.stringToBytes)(dict.get('O')).subarray(32, 40);
+ var ownerKeySalt = (0, _util.stringToBytes)(dict.get('O')).subarray(40, 48);
+ var uBytes = (0, _util.stringToBytes)(dict.get('U')).subarray(0, 48);
+ var userValidationSalt = (0, _util.stringToBytes)(dict.get('U')).subarray(32, 40);
+ var userKeySalt = (0, _util.stringToBytes)(dict.get('U')).subarray(40, 48);
+ var ownerEncryption = (0, _util.stringToBytes)(dict.get('OE'));
+ var userEncryption = (0, _util.stringToBytes)(dict.get('UE'));
+ var perms = (0, _util.stringToBytes)(dict.get('Perms'));
+ encryptionKey = createEncryptionKey20(revision, passwordBytes, ownerPassword, ownerValidationSalt, ownerKeySalt, uBytes, userPassword, userValidationSalt, userKeySalt, ownerEncryption, userEncryption, perms);
+ }
+
+ if (!encryptionKey && !password) {
+ throw new _util.PasswordException('No password given', _util.PasswordResponses.NEED_PASSWORD);
+ } else if (!encryptionKey && password) {
+ var decodedPassword = decodeUserPassword(passwordBytes, ownerPassword, revision, keyLength);
+ encryptionKey = prepareKeyData(fileIdBytes, decodedPassword, ownerPassword, userPassword, flags, revision, keyLength, encryptMetadata);
+ }
+
+ if (!encryptionKey) {
+ throw new _util.PasswordException('Incorrect Password', _util.PasswordResponses.INCORRECT_PASSWORD);
+ }
+
+ this.encryptionKey = encryptionKey;
+
+ if (algorithm >= 4) {
+ var cf = dict.get('CF');
+
+ if ((0, _primitives.isDict)(cf)) {
+ cf.suppressEncryption = true;
+ }
+
+ this.cf = cf;
+ this.stmf = dict.get('StmF') || identityName;
+ this.strf = dict.get('StrF') || identityName;
+ this.eff = dict.get('EFF') || this.stmf;
+ }
+ }
+
+ function buildObjectKey(num, gen, encryptionKey, isAes) {
+ var key = new Uint8Array(encryptionKey.length + 9),
+ i,
+ n;
+
+ for (i = 0, n = encryptionKey.length; i < n; ++i) {
+ key[i] = encryptionKey[i];
+ }
+
+ key[i++] = num & 0xFF;
+ key[i++] = num >> 8 & 0xFF;
+ key[i++] = num >> 16 & 0xFF;
+ key[i++] = gen & 0xFF;
+ key[i++] = gen >> 8 & 0xFF;
+
+ if (isAes) {
+ key[i++] = 0x73;
+ key[i++] = 0x41;
+ key[i++] = 0x6C;
+ key[i++] = 0x54;
+ }
+
+ var hash = calculateMD5(key, 0, i);
+ return hash.subarray(0, Math.min(encryptionKey.length + 5, 16));
+ }
+
+ function buildCipherConstructor(cf, name, num, gen, key) {
+ if (!(0, _primitives.isName)(name)) {
+ throw new _util.FormatError('Invalid crypt filter name.');
+ }
+
+ var cryptFilter = cf.get(name.name);
+ var cfm;
+
+ if (cryptFilter !== null && cryptFilter !== undefined) {
+ cfm = cryptFilter.get('CFM');
+ }
+
+ if (!cfm || cfm.name === 'None') {
+ return function cipherTransformFactoryBuildCipherConstructorNone() {
+ return new NullCipher();
+ };
+ }
+
+ if (cfm.name === 'V2') {
+ return function cipherTransformFactoryBuildCipherConstructorV2() {
+ return new ARCFourCipher(buildObjectKey(num, gen, key, false));
+ };
+ }
+
+ if (cfm.name === 'AESV2') {
+ return function cipherTransformFactoryBuildCipherConstructorAESV2() {
+ return new AES128Cipher(buildObjectKey(num, gen, key, true));
+ };
+ }
+
+ if (cfm.name === 'AESV3') {
+ return function cipherTransformFactoryBuildCipherConstructorAESV3() {
+ return new AES256Cipher(key);
+ };
+ }
+
+ throw new _util.FormatError('Unknown crypto method');
+ }
+
+ CipherTransformFactory.prototype = {
+ createCipherTransform: function CipherTransformFactory_createCipherTransform(num, gen) {
+ if (this.algorithm === 4 || this.algorithm === 5) {
+ return new CipherTransform(buildCipherConstructor(this.cf, this.stmf, num, gen, this.encryptionKey), buildCipherConstructor(this.cf, this.strf, num, gen, this.encryptionKey));
+ }
+
+ var key = buildObjectKey(num, gen, this.encryptionKey, false);
+
+ var cipherConstructor = function buildCipherCipherConstructor() {
+ return new ARCFourCipher(key);
+ };
+
+ return new CipherTransform(cipherConstructor, cipherConstructor);
+ }
+ };
+ return CipherTransformFactory;
+}();
+
+exports.CipherTransformFactory = CipherTransformFactory;
+
+/***/ }),
+/* 169 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ColorSpace = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _primitives = __w_pdfjs_require__(151);
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
+
+function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
+
+function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
+
+function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+function resizeRgbImage(src, dest, w1, h1, w2, h2, alpha01) {
+ var COMPONENTS = 3;
+ alpha01 = alpha01 !== 1 ? 0 : alpha01;
+ var xRatio = w1 / w2;
+ var yRatio = h1 / h2;
+ var newIndex = 0,
+ oldIndex;
+ var xScaled = new Uint16Array(w2);
+ var w1Scanline = w1 * COMPONENTS;
+
+ for (var i = 0; i < w2; i++) {
+ xScaled[i] = Math.floor(i * xRatio) * COMPONENTS;
+ }
+
+ for (var _i = 0; _i < h2; _i++) {
+ var py = Math.floor(_i * yRatio) * w1Scanline;
+
+ for (var j = 0; j < w2; j++) {
+ oldIndex = py + xScaled[j];
+ dest[newIndex++] = src[oldIndex++];
+ dest[newIndex++] = src[oldIndex++];
+ dest[newIndex++] = src[oldIndex++];
+ newIndex += alpha01;
+ }
+ }
+}
+
+var ColorSpace =
+/*#__PURE__*/
+function () {
+ function ColorSpace(name, numComps) {
+ _classCallCheck(this, ColorSpace);
+
+ if (this.constructor === ColorSpace) {
+ (0, _util.unreachable)('Cannot initialize ColorSpace.');
+ }
+
+ this.name = name;
+ this.numComps = numComps;
+ }
+
+ _createClass(ColorSpace, [{
+ key: "getRgb",
+ value: function getRgb(src, srcOffset) {
+ var rgb = new Uint8ClampedArray(3);
+ this.getRgbItem(src, srcOffset, rgb, 0);
+ return rgb;
+ }
+ }, {
+ key: "getRgbItem",
+ value: function getRgbItem(src, srcOffset, dest, destOffset) {
+ (0, _util.unreachable)('Should not call ColorSpace.getRgbItem');
+ }
+ }, {
+ key: "getRgbBuffer",
+ value: function getRgbBuffer(src, srcOffset, count, dest, destOffset, bits, alpha01) {
+ (0, _util.unreachable)('Should not call ColorSpace.getRgbBuffer');
+ }
+ }, {
+ key: "getOutputLength",
+ value: function getOutputLength(inputLength, alpha01) {
+ (0, _util.unreachable)('Should not call ColorSpace.getOutputLength');
+ }
+ }, {
+ key: "isPassthrough",
+ value: function isPassthrough(bits) {
+ return false;
+ }
+ }, {
+ key: "isDefaultDecode",
+ value: function isDefaultDecode(decodeMap, bpc) {
+ return ColorSpace.isDefaultDecode(decodeMap, this.numComps);
+ }
+ }, {
+ key: "fillRgb",
+ value: function fillRgb(dest, originalWidth, originalHeight, width, height, actualHeight, bpc, comps, alpha01) {
+ var count = originalWidth * originalHeight;
+ var rgbBuf = null;
+ var numComponentColors = 1 << bpc;
+ var needsResizing = originalHeight !== height || originalWidth !== width;
+
+ if (this.isPassthrough(bpc)) {
+ rgbBuf = comps;
+ } else if (this.numComps === 1 && count > numComponentColors && this.name !== 'DeviceGray' && this.name !== 'DeviceRGB') {
+ var allColors = bpc <= 8 ? new Uint8Array(numComponentColors) : new Uint16Array(numComponentColors);
+
+ for (var i = 0; i < numComponentColors; i++) {
+ allColors[i] = i;
+ }
+
+ var colorMap = new Uint8ClampedArray(numComponentColors * 3);
+ this.getRgbBuffer(allColors, 0, numComponentColors, colorMap, 0, bpc, 0);
+
+ if (!needsResizing) {
+ var destPos = 0;
+
+ for (var _i2 = 0; _i2 < count; ++_i2) {
+ var key = comps[_i2] * 3;
+ dest[destPos++] = colorMap[key];
+ dest[destPos++] = colorMap[key + 1];
+ dest[destPos++] = colorMap[key + 2];
+ destPos += alpha01;
+ }
+ } else {
+ rgbBuf = new Uint8Array(count * 3);
+ var rgbPos = 0;
+
+ for (var _i3 = 0; _i3 < count; ++_i3) {
+ var _key = comps[_i3] * 3;
+
+ rgbBuf[rgbPos++] = colorMap[_key];
+ rgbBuf[rgbPos++] = colorMap[_key + 1];
+ rgbBuf[rgbPos++] = colorMap[_key + 2];
+ }
+ }
+ } else {
+ if (!needsResizing) {
+ this.getRgbBuffer(comps, 0, width * actualHeight, dest, 0, bpc, alpha01);
+ } else {
+ rgbBuf = new Uint8ClampedArray(count * 3);
+ this.getRgbBuffer(comps, 0, count, rgbBuf, 0, bpc, 0);
+ }
+ }
+
+ if (rgbBuf) {
+ if (needsResizing) {
+ resizeRgbImage(rgbBuf, dest, originalWidth, originalHeight, width, height, alpha01);
+ } else {
+ var _destPos = 0,
+ _rgbPos = 0;
+
+ for (var _i4 = 0, ii = width * actualHeight; _i4 < ii; _i4++) {
+ dest[_destPos++] = rgbBuf[_rgbPos++];
+ dest[_destPos++] = rgbBuf[_rgbPos++];
+ dest[_destPos++] = rgbBuf[_rgbPos++];
+ _destPos += alpha01;
+ }
+ }
+ }
+ }
+ }, {
+ key: "usesZeroToOneRange",
+ get: function get() {
+ return (0, _util.shadow)(this, 'usesZeroToOneRange', true);
+ }
+ }], [{
+ key: "parse",
+ value: function parse(cs, xref, res, pdfFunctionFactory) {
+ var IR = this.parseToIR(cs, xref, res, pdfFunctionFactory);
+ return this.fromIR(IR);
+ }
+ }, {
+ key: "fromIR",
+ value: function fromIR(IR) {
+ var name = Array.isArray(IR) ? IR[0] : IR;
+ var whitePoint, blackPoint, gamma;
+
+ switch (name) {
+ case 'DeviceGrayCS':
+ return this.singletons.gray;
+
+ case 'DeviceRgbCS':
+ return this.singletons.rgb;
+
+ case 'DeviceCmykCS':
+ return this.singletons.cmyk;
+
+ case 'CalGrayCS':
+ whitePoint = IR[1];
+ blackPoint = IR[2];
+ gamma = IR[3];
+ return new CalGrayCS(whitePoint, blackPoint, gamma);
+
+ case 'CalRGBCS':
+ whitePoint = IR[1];
+ blackPoint = IR[2];
+ gamma = IR[3];
+ var matrix = IR[4];
+ return new CalRGBCS(whitePoint, blackPoint, gamma, matrix);
+
+ case 'PatternCS':
+ var basePatternCS = IR[1];
+
+ if (basePatternCS) {
+ basePatternCS = this.fromIR(basePatternCS);
+ }
+
+ return new PatternCS(basePatternCS);
+
+ case 'IndexedCS':
+ var baseIndexedCS = IR[1];
+ var hiVal = IR[2];
+ var lookup = IR[3];
+ return new IndexedCS(this.fromIR(baseIndexedCS), hiVal, lookup);
+
+ case 'AlternateCS':
+ var numComps = IR[1];
+ var alt = IR[2];
+ var tintFn = IR[3];
+ return new AlternateCS(numComps, this.fromIR(alt), tintFn);
+
+ case 'LabCS':
+ whitePoint = IR[1];
+ blackPoint = IR[2];
+ var range = IR[3];
+ return new LabCS(whitePoint, blackPoint, range);
+
+ default:
+ throw new _util.FormatError("Unknown colorspace name: ".concat(name));
+ }
+ }
+ }, {
+ key: "parseToIR",
+ value: function parseToIR(cs, xref) {
+ var res = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
+ var pdfFunctionFactory = arguments.length > 3 ? arguments[3] : undefined;
+ cs = xref.fetchIfRef(cs);
+
+ if ((0, _primitives.isName)(cs)) {
+ switch (cs.name) {
+ case 'DeviceGray':
+ case 'G':
+ return 'DeviceGrayCS';
+
+ case 'DeviceRGB':
+ case 'RGB':
+ return 'DeviceRgbCS';
+
+ case 'DeviceCMYK':
+ case 'CMYK':
+ return 'DeviceCmykCS';
+
+ case 'Pattern':
+ return ['PatternCS', null];
+
+ default:
+ if ((0, _primitives.isDict)(res)) {
+ var colorSpaces = res.get('ColorSpace');
+
+ if ((0, _primitives.isDict)(colorSpaces)) {
+ var resCS = colorSpaces.get(cs.name);
+
+ if (resCS) {
+ if ((0, _primitives.isName)(resCS)) {
+ return this.parseToIR(resCS, xref, res, pdfFunctionFactory);
+ }
+
+ cs = resCS;
+ break;
+ }
+ }
+ }
+
+ throw new _util.FormatError("unrecognized colorspace ".concat(cs.name));
+ }
+ }
+
+ if (Array.isArray(cs)) {
+ var mode = xref.fetchIfRef(cs[0]).name;
+ var numComps, params, alt, whitePoint, blackPoint, gamma;
+
+ switch (mode) {
+ case 'DeviceGray':
+ case 'G':
+ return 'DeviceGrayCS';
+
+ case 'DeviceRGB':
+ case 'RGB':
+ return 'DeviceRgbCS';
+
+ case 'DeviceCMYK':
+ case 'CMYK':
+ return 'DeviceCmykCS';
+
+ case 'CalGray':
+ params = xref.fetchIfRef(cs[1]);
+ whitePoint = params.getArray('WhitePoint');
+ blackPoint = params.getArray('BlackPoint');
+ gamma = params.get('Gamma');
+ return ['CalGrayCS', whitePoint, blackPoint, gamma];
+
+ case 'CalRGB':
+ params = xref.fetchIfRef(cs[1]);
+ whitePoint = params.getArray('WhitePoint');
+ blackPoint = params.getArray('BlackPoint');
+ gamma = params.getArray('Gamma');
+ var matrix = params.getArray('Matrix');
+ return ['CalRGBCS', whitePoint, blackPoint, gamma, matrix];
+
+ case 'ICCBased':
+ var stream = xref.fetchIfRef(cs[1]);
+ var dict = stream.dict;
+ numComps = dict.get('N');
+ alt = dict.get('Alternate');
+
+ if (alt) {
+ var altIR = this.parseToIR(alt, xref, res, pdfFunctionFactory);
+ var altCS = this.fromIR(altIR, pdfFunctionFactory);
+
+ if (altCS.numComps === numComps) {
+ return altIR;
+ }
+
+ (0, _util.warn)('ICCBased color space: Ignoring incorrect /Alternate entry.');
+ }
+
+ if (numComps === 1) {
+ return 'DeviceGrayCS';
+ } else if (numComps === 3) {
+ return 'DeviceRgbCS';
+ } else if (numComps === 4) {
+ return 'DeviceCmykCS';
+ }
+
+ break;
+
+ case 'Pattern':
+ var basePatternCS = cs[1] || null;
+
+ if (basePatternCS) {
+ basePatternCS = this.parseToIR(basePatternCS, xref, res, pdfFunctionFactory);
+ }
+
+ return ['PatternCS', basePatternCS];
+
+ case 'Indexed':
+ case 'I':
+ var baseIndexedCS = this.parseToIR(cs[1], xref, res, pdfFunctionFactory);
+ var hiVal = xref.fetchIfRef(cs[2]) + 1;
+ var lookup = xref.fetchIfRef(cs[3]);
+
+ if ((0, _primitives.isStream)(lookup)) {
+ lookup = lookup.getBytes();
+ }
+
+ return ['IndexedCS', baseIndexedCS, hiVal, lookup];
+
+ case 'Separation':
+ case 'DeviceN':
+ var name = xref.fetchIfRef(cs[1]);
+ numComps = Array.isArray(name) ? name.length : 1;
+ alt = this.parseToIR(cs[2], xref, res, pdfFunctionFactory);
+ var tintFn = pdfFunctionFactory.create(xref.fetchIfRef(cs[3]));
+ return ['AlternateCS', numComps, alt, tintFn];
+
+ case 'Lab':
+ params = xref.fetchIfRef(cs[1]);
+ whitePoint = params.getArray('WhitePoint');
+ blackPoint = params.getArray('BlackPoint');
+ var range = params.getArray('Range');
+ return ['LabCS', whitePoint, blackPoint, range];
+
+ default:
+ throw new _util.FormatError("unimplemented color space object \"".concat(mode, "\""));
+ }
+ }
+
+ throw new _util.FormatError("unrecognized color space object: \"".concat(cs, "\""));
+ }
+ }, {
+ key: "isDefaultDecode",
+ value: function isDefaultDecode(decode, numComps) {
+ if (!Array.isArray(decode)) {
+ return true;
+ }
+
+ if (numComps * 2 !== decode.length) {
+ (0, _util.warn)('The decode map is not the correct length');
+ return true;
+ }
+
+ for (var i = 0, ii = decode.length; i < ii; i += 2) {
+ if (decode[i] !== 0 || decode[i + 1] !== 1) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }, {
+ key: "singletons",
+ get: function get() {
+ return (0, _util.shadow)(this, 'singletons', {
+ get gray() {
+ return (0, _util.shadow)(this, 'gray', new DeviceGrayCS());
+ },
+
+ get rgb() {
+ return (0, _util.shadow)(this, 'rgb', new DeviceRgbCS());
+ },
+
+ get cmyk() {
+ return (0, _util.shadow)(this, 'cmyk', new DeviceCmykCS());
+ }
+
+ });
+ }
+ }]);
+
+ return ColorSpace;
+}();
+
+exports.ColorSpace = ColorSpace;
+
+var AlternateCS =
+/*#__PURE__*/
+function (_ColorSpace) {
+ _inherits(AlternateCS, _ColorSpace);
+
+ function AlternateCS(numComps, base, tintFn) {
+ var _this;
+
+ _classCallCheck(this, AlternateCS);
+
+ _this = _possibleConstructorReturn(this, _getPrototypeOf(AlternateCS).call(this, 'Alternate', numComps));
+ _this.base = base;
+ _this.tintFn = tintFn;
+ _this.tmpBuf = new Float32Array(base.numComps);
+ return _this;
+ }
+
+ _createClass(AlternateCS, [{
+ key: "getRgbItem",
+ value: function getRgbItem(src, srcOffset, dest, destOffset) {
+ var tmpBuf = this.tmpBuf;
+ this.tintFn(src, srcOffset, tmpBuf, 0);
+ this.base.getRgbItem(tmpBuf, 0, dest, destOffset);
+ }
+ }, {
+ key: "getRgbBuffer",
+ value: function getRgbBuffer(src, srcOffset, count, dest, destOffset, bits, alpha01) {
+ var tintFn = this.tintFn;
+ var base = this.base;
+ var scale = 1 / ((1 << bits) - 1);
+ var baseNumComps = base.numComps;
+ var usesZeroToOneRange = base.usesZeroToOneRange;
+ var isPassthrough = (base.isPassthrough(8) || !usesZeroToOneRange) && alpha01 === 0;
+ var pos = isPassthrough ? destOffset : 0;
+ var baseBuf = isPassthrough ? dest : new Uint8ClampedArray(baseNumComps * count);
+ var numComps = this.numComps;
+ var scaled = new Float32Array(numComps);
+ var tinted = new Float32Array(baseNumComps);
+ var i, j;
+
+ for (i = 0; i < count; i++) {
+ for (j = 0; j < numComps; j++) {
+ scaled[j] = src[srcOffset++] * scale;
+ }
+
+ tintFn(scaled, 0, tinted, 0);
+
+ if (usesZeroToOneRange) {
+ for (j = 0; j < baseNumComps; j++) {
+ baseBuf[pos++] = tinted[j] * 255;
+ }
+ } else {
+ base.getRgbItem(tinted, 0, baseBuf, pos);
+ pos += baseNumComps;
+ }
+ }
+
+ if (!isPassthrough) {
+ base.getRgbBuffer(baseBuf, 0, count, dest, destOffset, 8, alpha01);
+ }
+ }
+ }, {
+ key: "getOutputLength",
+ value: function getOutputLength(inputLength, alpha01) {
+ return this.base.getOutputLength(inputLength * this.base.numComps / this.numComps, alpha01);
+ }
+ }]);
+
+ return AlternateCS;
+}(ColorSpace);
+
+var PatternCS =
+/*#__PURE__*/
+function (_ColorSpace2) {
+ _inherits(PatternCS, _ColorSpace2);
+
+ function PatternCS(baseCS) {
+ var _this2;
+
+ _classCallCheck(this, PatternCS);
+
+ _this2 = _possibleConstructorReturn(this, _getPrototypeOf(PatternCS).call(this, 'Pattern', null));
+ _this2.base = baseCS;
+ return _this2;
+ }
+
+ _createClass(PatternCS, [{
+ key: "isDefaultDecode",
+ value: function isDefaultDecode(decodeMap, bpc) {
+ (0, _util.unreachable)('Should not call PatternCS.isDefaultDecode');
+ }
+ }]);
+
+ return PatternCS;
+}(ColorSpace);
+
+var IndexedCS =
+/*#__PURE__*/
+function (_ColorSpace3) {
+ _inherits(IndexedCS, _ColorSpace3);
+
+ function IndexedCS(base, highVal, lookup) {
+ var _this3;
+
+ _classCallCheck(this, IndexedCS);
+
+ _this3 = _possibleConstructorReturn(this, _getPrototypeOf(IndexedCS).call(this, 'Indexed', 1));
+ _this3.base = base;
+ _this3.highVal = highVal;
+ var baseNumComps = base.numComps;
+ var length = baseNumComps * highVal;
+
+ if ((0, _primitives.isStream)(lookup)) {
+ _this3.lookup = new Uint8Array(length);
+ var bytes = lookup.getBytes(length);
+
+ _this3.lookup.set(bytes);
+ } else if ((0, _util.isString)(lookup)) {
+ _this3.lookup = new Uint8Array(length);
+
+ for (var i = 0; i < length; ++i) {
+ _this3.lookup[i] = lookup.charCodeAt(i);
+ }
+ } else if (lookup instanceof Uint8Array) {
+ _this3.lookup = lookup;
+ } else {
+ throw new _util.FormatError("Unrecognized lookup table: ".concat(lookup));
+ }
+
+ return _this3;
+ }
+
+ _createClass(IndexedCS, [{
+ key: "getRgbItem",
+ value: function getRgbItem(src, srcOffset, dest, destOffset) {
+ var numComps = this.base.numComps;
+ var start = src[srcOffset] * numComps;
+ this.base.getRgbBuffer(this.lookup, start, 1, dest, destOffset, 8, 0);
+ }
+ }, {
+ key: "getRgbBuffer",
+ value: function getRgbBuffer(src, srcOffset, count, dest, destOffset, bits, alpha01) {
+ var base = this.base;
+ var numComps = base.numComps;
+ var outputDelta = base.getOutputLength(numComps, alpha01);
+ var lookup = this.lookup;
+
+ for (var i = 0; i < count; ++i) {
+ var lookupPos = src[srcOffset++] * numComps;
+ base.getRgbBuffer(lookup, lookupPos, 1, dest, destOffset, 8, alpha01);
+ destOffset += outputDelta;
+ }
+ }
+ }, {
+ key: "getOutputLength",
+ value: function getOutputLength(inputLength, alpha01) {
+ return this.base.getOutputLength(inputLength * this.base.numComps, alpha01);
+ }
+ }, {
+ key: "isDefaultDecode",
+ value: function isDefaultDecode(decodeMap, bpc) {
+ if (!Array.isArray(decodeMap)) {
+ return true;
+ }
+
+ if (decodeMap.length !== 2) {
+ (0, _util.warn)('Decode map length is not correct');
+ return true;
+ }
+
+ if (!Number.isInteger(bpc) || bpc < 1) {
+ (0, _util.warn)('Bits per component is not correct');
+ return true;
+ }
+
+ return decodeMap[0] === 0 && decodeMap[1] === (1 << bpc) - 1;
+ }
+ }]);
+
+ return IndexedCS;
+}(ColorSpace);
+
+var DeviceGrayCS =
+/*#__PURE__*/
+function (_ColorSpace4) {
+ _inherits(DeviceGrayCS, _ColorSpace4);
+
+ function DeviceGrayCS() {
+ _classCallCheck(this, DeviceGrayCS);
+
+ return _possibleConstructorReturn(this, _getPrototypeOf(DeviceGrayCS).call(this, 'DeviceGray', 1));
+ }
+
+ _createClass(DeviceGrayCS, [{
+ key: "getRgbItem",
+ value: function getRgbItem(src, srcOffset, dest, destOffset) {
+ var c = src[srcOffset] * 255;
+ dest[destOffset] = dest[destOffset + 1] = dest[destOffset + 2] = c;
+ }
+ }, {
+ key: "getRgbBuffer",
+ value: function getRgbBuffer(src, srcOffset, count, dest, destOffset, bits, alpha01) {
+ var scale = 255 / ((1 << bits) - 1);
+ var j = srcOffset,
+ q = destOffset;
+
+ for (var i = 0; i < count; ++i) {
+ var c = scale * src[j++];
+ dest[q++] = c;
+ dest[q++] = c;
+ dest[q++] = c;
+ q += alpha01;
+ }
+ }
+ }, {
+ key: "getOutputLength",
+ value: function getOutputLength(inputLength, alpha01) {
+ return inputLength * (3 + alpha01);
+ }
+ }]);
+
+ return DeviceGrayCS;
+}(ColorSpace);
+
+var DeviceRgbCS =
+/*#__PURE__*/
+function (_ColorSpace5) {
+ _inherits(DeviceRgbCS, _ColorSpace5);
+
+ function DeviceRgbCS() {
+ _classCallCheck(this, DeviceRgbCS);
+
+ return _possibleConstructorReturn(this, _getPrototypeOf(DeviceRgbCS).call(this, 'DeviceRGB', 3));
+ }
+
+ _createClass(DeviceRgbCS, [{
+ key: "getRgbItem",
+ value: function getRgbItem(src, srcOffset, dest, destOffset) {
+ dest[destOffset] = src[srcOffset] * 255;
+ dest[destOffset + 1] = src[srcOffset + 1] * 255;
+ dest[destOffset + 2] = src[srcOffset + 2] * 255;
+ }
+ }, {
+ key: "getRgbBuffer",
+ value: function getRgbBuffer(src, srcOffset, count, dest, destOffset, bits, alpha01) {
+ if (bits === 8 && alpha01 === 0) {
+ dest.set(src.subarray(srcOffset, srcOffset + count * 3), destOffset);
+ return;
+ }
+
+ var scale = 255 / ((1 << bits) - 1);
+ var j = srcOffset,
+ q = destOffset;
+
+ for (var i = 0; i < count; ++i) {
+ dest[q++] = scale * src[j++];
+ dest[q++] = scale * src[j++];
+ dest[q++] = scale * src[j++];
+ q += alpha01;
+ }
+ }
+ }, {
+ key: "getOutputLength",
+ value: function getOutputLength(inputLength, alpha01) {
+ return inputLength * (3 + alpha01) / 3 | 0;
+ }
+ }, {
+ key: "isPassthrough",
+ value: function isPassthrough(bits) {
+ return bits === 8;
+ }
+ }]);
+
+ return DeviceRgbCS;
+}(ColorSpace);
+
+var DeviceCmykCS = function DeviceCmykCSClosure() {
+ function convertToRgb(src, srcOffset, srcScale, dest, destOffset) {
+ var c = src[srcOffset] * srcScale;
+ var m = src[srcOffset + 1] * srcScale;
+ var y = src[srcOffset + 2] * srcScale;
+ var k = src[srcOffset + 3] * srcScale;
+ dest[destOffset] = 255 + c * (-4.387332384609988 * c + 54.48615194189176 * m + 18.82290502165302 * y + 212.25662451639585 * k + -285.2331026137004) + m * (1.7149763477362134 * m - 5.6096736904047315 * y + -17.873870861415444 * k - 5.497006427196366) + y * (-2.5217340131683033 * y - 21.248923337353073 * k + 17.5119270841813) + k * (-21.86122147463605 * k - 189.48180835922747);
+ dest[destOffset + 1] = 255 + c * (8.841041422036149 * c + 60.118027045597366 * m + 6.871425592049007 * y + 31.159100130055922 * k + -79.2970844816548) + m * (-15.310361306967817 * m + 17.575251261109482 * y + 131.35250912493976 * k - 190.9453302588951) + y * (4.444339102852739 * y + 9.8632861493405 * k - 24.86741582555878) + k * (-20.737325471181034 * k - 187.80453709719578);
+ dest[destOffset + 2] = 255 + c * (0.8842522430003296 * c + 8.078677503112928 * m + 30.89978309703729 * y - 0.23883238689178934 * k + -14.183576799673286) + m * (10.49593273432072 * m + 63.02378494754052 * y + 50.606957656360734 * k - 112.23884253719248) + y * (0.03296041114873217 * y + 115.60384449646641 * k + -193.58209356861505) + k * (-22.33816807309886 * k - 180.12613974708367);
+ }
+
+ var DeviceCmykCS =
+ /*#__PURE__*/
+ function (_ColorSpace6) {
+ _inherits(DeviceCmykCS, _ColorSpace6);
+
+ function DeviceCmykCS() {
+ _classCallCheck(this, DeviceCmykCS);
+
+ return _possibleConstructorReturn(this, _getPrototypeOf(DeviceCmykCS).call(this, 'DeviceCMYK', 4));
+ }
+
+ _createClass(DeviceCmykCS, [{
+ key: "getRgbItem",
+ value: function getRgbItem(src, srcOffset, dest, destOffset) {
+ convertToRgb(src, srcOffset, 1, dest, destOffset);
+ }
+ }, {
+ key: "getRgbBuffer",
+ value: function getRgbBuffer(src, srcOffset, count, dest, destOffset, bits, alpha01) {
+ var scale = 1 / ((1 << bits) - 1);
+
+ for (var i = 0; i < count; i++) {
+ convertToRgb(src, srcOffset, scale, dest, destOffset);
+ srcOffset += 4;
+ destOffset += 3 + alpha01;
+ }
+ }
+ }, {
+ key: "getOutputLength",
+ value: function getOutputLength(inputLength, alpha01) {
+ return inputLength / 4 * (3 + alpha01) | 0;
+ }
+ }]);
+
+ return DeviceCmykCS;
+ }(ColorSpace);
+
+ return DeviceCmykCS;
+}();
+
+var CalGrayCS = function CalGrayCSClosure() {
+ function convertToRgb(cs, src, srcOffset, dest, destOffset, scale) {
+ var A = src[srcOffset] * scale;
+ var AG = Math.pow(A, cs.G);
+ var L = cs.YW * AG;
+ var val = Math.max(295.8 * Math.pow(L, 0.333333333333333333) - 40.8, 0);
+ dest[destOffset] = val;
+ dest[destOffset + 1] = val;
+ dest[destOffset + 2] = val;
+ }
+
+ var CalGrayCS =
+ /*#__PURE__*/
+ function (_ColorSpace7) {
+ _inherits(CalGrayCS, _ColorSpace7);
+
+ function CalGrayCS(whitePoint, blackPoint, gamma) {
+ var _this4;
+
+ _classCallCheck(this, CalGrayCS);
+
+ _this4 = _possibleConstructorReturn(this, _getPrototypeOf(CalGrayCS).call(this, 'CalGray', 1));
+
+ if (!whitePoint) {
+ throw new _util.FormatError('WhitePoint missing - required for color space CalGray');
+ }
+
+ blackPoint = blackPoint || [0, 0, 0];
+ gamma = gamma || 1;
+ _this4.XW = whitePoint[0];
+ _this4.YW = whitePoint[1];
+ _this4.ZW = whitePoint[2];
+ _this4.XB = blackPoint[0];
+ _this4.YB = blackPoint[1];
+ _this4.ZB = blackPoint[2];
+ _this4.G = gamma;
+
+ if (_this4.XW < 0 || _this4.ZW < 0 || _this4.YW !== 1) {
+ throw new _util.FormatError("Invalid WhitePoint components for ".concat(_this4.name) + ', no fallback available');
+ }
+
+ if (_this4.XB < 0 || _this4.YB < 0 || _this4.ZB < 0) {
+ (0, _util.info)("Invalid BlackPoint for ".concat(_this4.name, ", falling back to default."));
+ _this4.XB = _this4.YB = _this4.ZB = 0;
+ }
+
+ if (_this4.XB !== 0 || _this4.YB !== 0 || _this4.ZB !== 0) {
+ (0, _util.warn)("".concat(_this4.name, ", BlackPoint: XB: ").concat(_this4.XB, ", YB: ").concat(_this4.YB, ", ") + "ZB: ".concat(_this4.ZB, ", only default values are supported."));
+ }
+
+ if (_this4.G < 1) {
+ (0, _util.info)("Invalid Gamma: ".concat(_this4.G, " for ").concat(_this4.name, ", ") + 'falling back to default.');
+ _this4.G = 1;
+ }
+
+ return _this4;
+ }
+
+ _createClass(CalGrayCS, [{
+ key: "getRgbItem",
+ value: function getRgbItem(src, srcOffset, dest, destOffset) {
+ convertToRgb(this, src, srcOffset, dest, destOffset, 1);
+ }
+ }, {
+ key: "getRgbBuffer",
+ value: function getRgbBuffer(src, srcOffset, count, dest, destOffset, bits, alpha01) {
+ var scale = 1 / ((1 << bits) - 1);
+
+ for (var i = 0; i < count; ++i) {
+ convertToRgb(this, src, srcOffset, dest, destOffset, scale);
+ srcOffset += 1;
+ destOffset += 3 + alpha01;
+ }
+ }
+ }, {
+ key: "getOutputLength",
+ value: function getOutputLength(inputLength, alpha01) {
+ return inputLength * (3 + alpha01);
+ }
+ }]);
+
+ return CalGrayCS;
+ }(ColorSpace);
+
+ return CalGrayCS;
+}();
+
+var CalRGBCS = function CalRGBCSClosure() {
+ var BRADFORD_SCALE_MATRIX = new Float32Array([0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296]);
+ var BRADFORD_SCALE_INVERSE_MATRIX = new Float32Array([0.9869929, -0.1470543, 0.1599627, 0.4323053, 0.5183603, 0.0492912, -0.0085287, 0.0400428, 0.9684867]);
+ var SRGB_D65_XYZ_TO_RGB_MATRIX = new Float32Array([3.2404542, -1.5371385, -0.4985314, -0.9692660, 1.8760108, 0.0415560, 0.0556434, -0.2040259, 1.0572252]);
+ var FLAT_WHITEPOINT_MATRIX = new Float32Array([1, 1, 1]);
+ var tempNormalizeMatrix = new Float32Array(3);
+ var tempConvertMatrix1 = new Float32Array(3);
+ var tempConvertMatrix2 = new Float32Array(3);
+ var DECODE_L_CONSTANT = Math.pow((8 + 16) / 116, 3) / 8.0;
+
+ function matrixProduct(a, b, result) {
+ result[0] = a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+ result[1] = a[3] * b[0] + a[4] * b[1] + a[5] * b[2];
+ result[2] = a[6] * b[0] + a[7] * b[1] + a[8] * b[2];
+ }
+
+ function convertToFlat(sourceWhitePoint, LMS, result) {
+ result[0] = LMS[0] * 1 / sourceWhitePoint[0];
+ result[1] = LMS[1] * 1 / sourceWhitePoint[1];
+ result[2] = LMS[2] * 1 / sourceWhitePoint[2];
+ }
+
+ function convertToD65(sourceWhitePoint, LMS, result) {
+ var D65X = 0.95047;
+ var D65Y = 1;
+ var D65Z = 1.08883;
+ result[0] = LMS[0] * D65X / sourceWhitePoint[0];
+ result[1] = LMS[1] * D65Y / sourceWhitePoint[1];
+ result[2] = LMS[2] * D65Z / sourceWhitePoint[2];
+ }
+
+ function sRGBTransferFunction(color) {
+ if (color <= 0.0031308) {
+ return adjustToRange(0, 1, 12.92 * color);
+ }
+
+ return adjustToRange(0, 1, (1 + 0.055) * Math.pow(color, 1 / 2.4) - 0.055);
+ }
+
+ function adjustToRange(min, max, value) {
+ return Math.max(min, Math.min(max, value));
+ }
+
+ function decodeL(L) {
+ if (L < 0) {
+ return -decodeL(-L);
+ }
+
+ if (L > 8.0) {
+ return Math.pow((L + 16) / 116, 3);
+ }
+
+ return L * DECODE_L_CONSTANT;
+ }
+
+ function compensateBlackPoint(sourceBlackPoint, XYZ_Flat, result) {
+ if (sourceBlackPoint[0] === 0 && sourceBlackPoint[1] === 0 && sourceBlackPoint[2] === 0) {
+ result[0] = XYZ_Flat[0];
+ result[1] = XYZ_Flat[1];
+ result[2] = XYZ_Flat[2];
+ return;
+ }
+
+ var zeroDecodeL = decodeL(0);
+ var X_DST = zeroDecodeL;
+ var X_SRC = decodeL(sourceBlackPoint[0]);
+ var Y_DST = zeroDecodeL;
+ var Y_SRC = decodeL(sourceBlackPoint[1]);
+ var Z_DST = zeroDecodeL;
+ var Z_SRC = decodeL(sourceBlackPoint[2]);
+ var X_Scale = (1 - X_DST) / (1 - X_SRC);
+ var X_Offset = 1 - X_Scale;
+ var Y_Scale = (1 - Y_DST) / (1 - Y_SRC);
+ var Y_Offset = 1 - Y_Scale;
+ var Z_Scale = (1 - Z_DST) / (1 - Z_SRC);
+ var Z_Offset = 1 - Z_Scale;
+ result[0] = XYZ_Flat[0] * X_Scale + X_Offset;
+ result[1] = XYZ_Flat[1] * Y_Scale + Y_Offset;
+ result[2] = XYZ_Flat[2] * Z_Scale + Z_Offset;
+ }
+
+ function normalizeWhitePointToFlat(sourceWhitePoint, XYZ_In, result) {
+ if (sourceWhitePoint[0] === 1 && sourceWhitePoint[2] === 1) {
+ result[0] = XYZ_In[0];
+ result[1] = XYZ_In[1];
+ result[2] = XYZ_In[2];
+ return;
+ }
+
+ var LMS = result;
+ matrixProduct(BRADFORD_SCALE_MATRIX, XYZ_In, LMS);
+ var LMS_Flat = tempNormalizeMatrix;
+ convertToFlat(sourceWhitePoint, LMS, LMS_Flat);
+ matrixProduct(BRADFORD_SCALE_INVERSE_MATRIX, LMS_Flat, result);
+ }
+
+ function normalizeWhitePointToD65(sourceWhitePoint, XYZ_In, result) {
+ var LMS = result;
+ matrixProduct(BRADFORD_SCALE_MATRIX, XYZ_In, LMS);
+ var LMS_D65 = tempNormalizeMatrix;
+ convertToD65(sourceWhitePoint, LMS, LMS_D65);
+ matrixProduct(BRADFORD_SCALE_INVERSE_MATRIX, LMS_D65, result);
+ }
+
+ function convertToRgb(cs, src, srcOffset, dest, destOffset, scale) {
+ var A = adjustToRange(0, 1, src[srcOffset] * scale);
+ var B = adjustToRange(0, 1, src[srcOffset + 1] * scale);
+ var C = adjustToRange(0, 1, src[srcOffset + 2] * scale);
+ var AGR = Math.pow(A, cs.GR);
+ var BGG = Math.pow(B, cs.GG);
+ var CGB = Math.pow(C, cs.GB);
+ var X = cs.MXA * AGR + cs.MXB * BGG + cs.MXC * CGB;
+ var Y = cs.MYA * AGR + cs.MYB * BGG + cs.MYC * CGB;
+ var Z = cs.MZA * AGR + cs.MZB * BGG + cs.MZC * CGB;
+ var XYZ = tempConvertMatrix1;
+ XYZ[0] = X;
+ XYZ[1] = Y;
+ XYZ[2] = Z;
+ var XYZ_Flat = tempConvertMatrix2;
+ normalizeWhitePointToFlat(cs.whitePoint, XYZ, XYZ_Flat);
+ var XYZ_Black = tempConvertMatrix1;
+ compensateBlackPoint(cs.blackPoint, XYZ_Flat, XYZ_Black);
+ var XYZ_D65 = tempConvertMatrix2;
+ normalizeWhitePointToD65(FLAT_WHITEPOINT_MATRIX, XYZ_Black, XYZ_D65);
+ var SRGB = tempConvertMatrix1;
+ matrixProduct(SRGB_D65_XYZ_TO_RGB_MATRIX, XYZ_D65, SRGB);
+ dest[destOffset] = sRGBTransferFunction(SRGB[0]) * 255;
+ dest[destOffset + 1] = sRGBTransferFunction(SRGB[1]) * 255;
+ dest[destOffset + 2] = sRGBTransferFunction(SRGB[2]) * 255;
+ }
+
+ var CalRGBCS =
+ /*#__PURE__*/
+ function (_ColorSpace8) {
+ _inherits(CalRGBCS, _ColorSpace8);
+
+ function CalRGBCS(whitePoint, blackPoint, gamma, matrix) {
+ var _this5;
+
+ _classCallCheck(this, CalRGBCS);
+
+ _this5 = _possibleConstructorReturn(this, _getPrototypeOf(CalRGBCS).call(this, 'CalRGB', 3));
+
+ if (!whitePoint) {
+ throw new _util.FormatError('WhitePoint missing - required for color space CalRGB');
+ }
+
+ blackPoint = blackPoint || new Float32Array(3);
+ gamma = gamma || new Float32Array([1, 1, 1]);
+ matrix = matrix || new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
+ var XW = whitePoint[0];
+ var YW = whitePoint[1];
+ var ZW = whitePoint[2];
+ _this5.whitePoint = whitePoint;
+ var XB = blackPoint[0];
+ var YB = blackPoint[1];
+ var ZB = blackPoint[2];
+ _this5.blackPoint = blackPoint;
+ _this5.GR = gamma[0];
+ _this5.GG = gamma[1];
+ _this5.GB = gamma[2];
+ _this5.MXA = matrix[0];
+ _this5.MYA = matrix[1];
+ _this5.MZA = matrix[2];
+ _this5.MXB = matrix[3];
+ _this5.MYB = matrix[4];
+ _this5.MZB = matrix[5];
+ _this5.MXC = matrix[6];
+ _this5.MYC = matrix[7];
+ _this5.MZC = matrix[8];
+
+ if (XW < 0 || ZW < 0 || YW !== 1) {
+ throw new _util.FormatError("Invalid WhitePoint components for ".concat(_this5.name) + ', no fallback available');
+ }
+
+ if (XB < 0 || YB < 0 || ZB < 0) {
+ (0, _util.info)("Invalid BlackPoint for ".concat(_this5.name, " [").concat(XB, ", ").concat(YB, ", ").concat(ZB, "], ") + 'falling back to default.');
+ _this5.blackPoint = new Float32Array(3);
+ }
+
+ if (_this5.GR < 0 || _this5.GG < 0 || _this5.GB < 0) {
+ (0, _util.info)("Invalid Gamma [".concat(_this5.GR, ", ").concat(_this5.GG, ", ").concat(_this5.GB, "] for ") + "".concat(_this5.name, ", falling back to default."));
+ _this5.GR = _this5.GG = _this5.GB = 1;
+ }
+
+ return _this5;
+ }
+
+ _createClass(CalRGBCS, [{
+ key: "getRgbItem",
+ value: function getRgbItem(src, srcOffset, dest, destOffset) {
+ convertToRgb(this, src, srcOffset, dest, destOffset, 1);
+ }
+ }, {
+ key: "getRgbBuffer",
+ value: function getRgbBuffer(src, srcOffset, count, dest, destOffset, bits, alpha01) {
+ var scale = 1 / ((1 << bits) - 1);
+
+ for (var i = 0; i < count; ++i) {
+ convertToRgb(this, src, srcOffset, dest, destOffset, scale);
+ srcOffset += 3;
+ destOffset += 3 + alpha01;
+ }
+ }
+ }, {
+ key: "getOutputLength",
+ value: function getOutputLength(inputLength, alpha01) {
+ return inputLength * (3 + alpha01) / 3 | 0;
+ }
+ }]);
+
+ return CalRGBCS;
+ }(ColorSpace);
+
+ return CalRGBCS;
+}();
+
+var LabCS = function LabCSClosure() {
+ function fn_g(x) {
+ var result;
+
+ if (x >= 6 / 29) {
+ result = x * x * x;
+ } else {
+ result = 108 / 841 * (x - 4 / 29);
+ }
+
+ return result;
+ }
+
+ function decode(value, high1, low2, high2) {
+ return low2 + value * (high2 - low2) / high1;
+ }
+
+ function convertToRgb(cs, src, srcOffset, maxVal, dest, destOffset) {
+ var Ls = src[srcOffset];
+ var as = src[srcOffset + 1];
+ var bs = src[srcOffset + 2];
+
+ if (maxVal !== false) {
+ Ls = decode(Ls, maxVal, 0, 100);
+ as = decode(as, maxVal, cs.amin, cs.amax);
+ bs = decode(bs, maxVal, cs.bmin, cs.bmax);
+ }
+
+ as = as > cs.amax ? cs.amax : as < cs.amin ? cs.amin : as;
+ bs = bs > cs.bmax ? cs.bmax : bs < cs.bmin ? cs.bmin : bs;
+ var M = (Ls + 16) / 116;
+ var L = M + as / 500;
+ var N = M - bs / 200;
+ var X = cs.XW * fn_g(L);
+ var Y = cs.YW * fn_g(M);
+ var Z = cs.ZW * fn_g(N);
+ var r, g, b;
+
+ if (cs.ZW < 1) {
+ r = X * 3.1339 + Y * -1.6170 + Z * -0.4906;
+ g = X * -0.9785 + Y * 1.9160 + Z * 0.0333;
+ b = X * 0.0720 + Y * -0.2290 + Z * 1.4057;
+ } else {
+ r = X * 3.2406 + Y * -1.5372 + Z * -0.4986;
+ g = X * -0.9689 + Y * 1.8758 + Z * 0.0415;
+ b = X * 0.0557 + Y * -0.2040 + Z * 1.0570;
+ }
+
+ dest[destOffset] = Math.sqrt(r) * 255;
+ dest[destOffset + 1] = Math.sqrt(g) * 255;
+ dest[destOffset + 2] = Math.sqrt(b) * 255;
+ }
+
+ var LabCS =
+ /*#__PURE__*/
+ function (_ColorSpace9) {
+ _inherits(LabCS, _ColorSpace9);
+
+ function LabCS(whitePoint, blackPoint, range) {
+ var _this6;
+
+ _classCallCheck(this, LabCS);
+
+ _this6 = _possibleConstructorReturn(this, _getPrototypeOf(LabCS).call(this, 'Lab', 3));
+
+ if (!whitePoint) {
+ throw new _util.FormatError('WhitePoint missing - required for color space Lab');
+ }
+
+ blackPoint = blackPoint || [0, 0, 0];
+ range = range || [-100, 100, -100, 100];
+ _this6.XW = whitePoint[0];
+ _this6.YW = whitePoint[1];
+ _this6.ZW = whitePoint[2];
+ _this6.amin = range[0];
+ _this6.amax = range[1];
+ _this6.bmin = range[2];
+ _this6.bmax = range[3];
+ _this6.XB = blackPoint[0];
+ _this6.YB = blackPoint[1];
+ _this6.ZB = blackPoint[2];
+
+ if (_this6.XW < 0 || _this6.ZW < 0 || _this6.YW !== 1) {
+ throw new _util.FormatError('Invalid WhitePoint components, no fallback available');
+ }
+
+ if (_this6.XB < 0 || _this6.YB < 0 || _this6.ZB < 0) {
+ (0, _util.info)('Invalid BlackPoint, falling back to default');
+ _this6.XB = _this6.YB = _this6.ZB = 0;
+ }
+
+ if (_this6.amin > _this6.amax || _this6.bmin > _this6.bmax) {
+ (0, _util.info)('Invalid Range, falling back to defaults');
+ _this6.amin = -100;
+ _this6.amax = 100;
+ _this6.bmin = -100;
+ _this6.bmax = 100;
+ }
+
+ return _this6;
+ }
+
+ _createClass(LabCS, [{
+ key: "getRgbItem",
+ value: function getRgbItem(src, srcOffset, dest, destOffset) {
+ convertToRgb(this, src, srcOffset, false, dest, destOffset);
+ }
+ }, {
+ key: "getRgbBuffer",
+ value: function getRgbBuffer(src, srcOffset, count, dest, destOffset, bits, alpha01) {
+ var maxVal = (1 << bits) - 1;
+
+ for (var i = 0; i < count; i++) {
+ convertToRgb(this, src, srcOffset, maxVal, dest, destOffset);
+ srcOffset += 3;
+ destOffset += 3 + alpha01;
+ }
+ }
+ }, {
+ key: "getOutputLength",
+ value: function getOutputLength(inputLength, alpha01) {
+ return inputLength * (3 + alpha01) / 3 | 0;
+ }
+ }, {
+ key: "isDefaultDecode",
+ value: function isDefaultDecode(decodeMap, bpc) {
+ return true;
+ }
+ }, {
+ key: "usesZeroToOneRange",
+ get: function get() {
+ return (0, _util.shadow)(this, 'usesZeroToOneRange', false);
+ }
+ }]);
+
+ return LabCS;
+ }(ColorSpace);
+
+ return LabCS;
+}();
+
+/***/ }),
+/* 170 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.MarkupAnnotation = exports.AnnotationFactory = exports.AnnotationBorderStyle = exports.Annotation = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _obj = __w_pdfjs_require__(156);
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _colorspace = __w_pdfjs_require__(169);
+
+var _core_utils = __w_pdfjs_require__(154);
+
+var _operator_list = __w_pdfjs_require__(171);
+
+var _stream = __w_pdfjs_require__(158);
+
+function _get(target, property, receiver) { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get; } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(receiver); } return desc.value; }; } return _get(target, property, receiver || target); }
+
+function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = _getPrototypeOf(object); if (object === null) break; } return object; }
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
+
+function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
+
+function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
+
+function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var AnnotationFactory =
+/*#__PURE__*/
+function () {
+ function AnnotationFactory() {
+ _classCallCheck(this, AnnotationFactory);
+ }
+
+ _createClass(AnnotationFactory, null, [{
+ key: "create",
+ value: function create(xref, ref, pdfManager, idFactory) {
+ return pdfManager.ensure(this, '_create', [xref, ref, pdfManager, idFactory]);
+ }
+ }, {
+ key: "_create",
+ value: function _create(xref, ref, pdfManager, idFactory) {
+ var dict = xref.fetchIfRef(ref);
+
+ if (!(0, _primitives.isDict)(dict)) {
+ return undefined;
+ }
+
+ var id = (0, _primitives.isRef)(ref) ? ref.toString() : "annot_".concat(idFactory.createObjId());
+ var subtype = dict.get('Subtype');
+ subtype = (0, _primitives.isName)(subtype) ? subtype.name : null;
+ var parameters = {
+ xref: xref,
+ dict: dict,
+ subtype: subtype,
+ id: id,
+ pdfManager: pdfManager
+ };
+
+ switch (subtype) {
+ case 'Link':
+ return new LinkAnnotation(parameters);
+
+ case 'Text':
+ return new TextAnnotation(parameters);
+
+ case 'Widget':
+ var fieldType = (0, _core_utils.getInheritableProperty)({
+ dict: dict,
+ key: 'FT'
+ });
+ fieldType = (0, _primitives.isName)(fieldType) ? fieldType.name : null;
+
+ switch (fieldType) {
+ case 'Tx':
+ return new TextWidgetAnnotation(parameters);
+
+ case 'Btn':
+ return new ButtonWidgetAnnotation(parameters);
+
+ case 'Ch':
+ return new ChoiceWidgetAnnotation(parameters);
+ }
+
+ (0, _util.warn)('Unimplemented widget field type "' + fieldType + '", ' + 'falling back to base field type.');
+ return new WidgetAnnotation(parameters);
+
+ case 'Popup':
+ return new PopupAnnotation(parameters);
+
+ case 'FreeText':
+ return new FreeTextAnnotation(parameters);
+
+ case 'Line':
+ return new LineAnnotation(parameters);
+
+ case 'Square':
+ return new SquareAnnotation(parameters);
+
+ case 'Circle':
+ return new CircleAnnotation(parameters);
+
+ case 'PolyLine':
+ return new PolylineAnnotation(parameters);
+
+ case 'Polygon':
+ return new PolygonAnnotation(parameters);
+
+ case 'Caret':
+ return new CaretAnnotation(parameters);
+
+ case 'Ink':
+ return new InkAnnotation(parameters);
+
+ case 'Highlight':
+ return new HighlightAnnotation(parameters);
+
+ case 'Underline':
+ return new UnderlineAnnotation(parameters);
+
+ case 'Squiggly':
+ return new SquigglyAnnotation(parameters);
+
+ case 'StrikeOut':
+ return new StrikeOutAnnotation(parameters);
+
+ case 'Stamp':
+ return new StampAnnotation(parameters);
+
+ case 'FileAttachment':
+ return new FileAttachmentAnnotation(parameters);
+
+ default:
+ if (!subtype) {
+ (0, _util.warn)('Annotation is missing the required /Subtype.');
+ } else {
+ (0, _util.warn)('Unimplemented annotation type "' + subtype + '", ' + 'falling back to base annotation.');
+ }
+
+ return new Annotation(parameters);
+ }
+ }
+ }]);
+
+ return AnnotationFactory;
+}();
+
+exports.AnnotationFactory = AnnotationFactory;
+
+function getTransformMatrix(rect, bbox, matrix) {
+ var bounds = _util.Util.getAxialAlignedBoundingBox(bbox, matrix);
+
+ var minX = bounds[0];
+ var minY = bounds[1];
+ var maxX = bounds[2];
+ var maxY = bounds[3];
+
+ if (minX === maxX || minY === maxY) {
+ return [1, 0, 0, 1, rect[0], rect[1]];
+ }
+
+ var xRatio = (rect[2] - rect[0]) / (maxX - minX);
+ var yRatio = (rect[3] - rect[1]) / (maxY - minY);
+ return [xRatio, 0, 0, yRatio, rect[0] - minX * xRatio, rect[1] - minY * yRatio];
+}
+
+var Annotation =
+/*#__PURE__*/
+function () {
+ function Annotation(params) {
+ _classCallCheck(this, Annotation);
+
+ var dict = params.dict;
+ this.setContents(dict.get('Contents'));
+ this.setModificationDate(dict.get('M'));
+ this.setFlags(dict.get('F'));
+ this.setRectangle(dict.getArray('Rect'));
+ this.setColor(dict.getArray('C'));
+ this.setBorderStyle(dict);
+ this.setAppearance(dict);
+ this.data = {
+ annotationFlags: this.flags,
+ borderStyle: this.borderStyle,
+ color: this.color,
+ contents: this.contents,
+ hasAppearance: !!this.appearance,
+ id: params.id,
+ modificationDate: this.modificationDate,
+ rect: this.rectangle,
+ subtype: params.subtype
+ };
+ }
+
+ _createClass(Annotation, [{
+ key: "_hasFlag",
+ value: function _hasFlag(flags, flag) {
+ return !!(flags & flag);
+ }
+ }, {
+ key: "_isViewable",
+ value: function _isViewable(flags) {
+ return !this._hasFlag(flags, _util.AnnotationFlag.INVISIBLE) && !this._hasFlag(flags, _util.AnnotationFlag.HIDDEN) && !this._hasFlag(flags, _util.AnnotationFlag.NOVIEW);
+ }
+ }, {
+ key: "_isPrintable",
+ value: function _isPrintable(flags) {
+ return this._hasFlag(flags, _util.AnnotationFlag.PRINT) && !this._hasFlag(flags, _util.AnnotationFlag.INVISIBLE) && !this._hasFlag(flags, _util.AnnotationFlag.HIDDEN);
+ }
+ }, {
+ key: "setContents",
+ value: function setContents(contents) {
+ this.contents = (0, _util.stringToPDFString)(contents || '');
+ }
+ }, {
+ key: "setModificationDate",
+ value: function setModificationDate(modificationDate) {
+ this.modificationDate = (0, _util.isString)(modificationDate) ? modificationDate : null;
+ }
+ }, {
+ key: "setFlags",
+ value: function setFlags(flags) {
+ this.flags = Number.isInteger(flags) && flags > 0 ? flags : 0;
+ }
+ }, {
+ key: "hasFlag",
+ value: function hasFlag(flag) {
+ return this._hasFlag(this.flags, flag);
+ }
+ }, {
+ key: "setRectangle",
+ value: function setRectangle(rectangle) {
+ if (Array.isArray(rectangle) && rectangle.length === 4) {
+ this.rectangle = _util.Util.normalizeRect(rectangle);
+ } else {
+ this.rectangle = [0, 0, 0, 0];
+ }
+ }
+ }, {
+ key: "setColor",
+ value: function setColor(color) {
+ var rgbColor = new Uint8ClampedArray(3);
+
+ if (!Array.isArray(color)) {
+ this.color = rgbColor;
+ return;
+ }
+
+ switch (color.length) {
+ case 0:
+ this.color = null;
+ break;
+
+ case 1:
+ _colorspace.ColorSpace.singletons.gray.getRgbItem(color, 0, rgbColor, 0);
+
+ this.color = rgbColor;
+ break;
+
+ case 3:
+ _colorspace.ColorSpace.singletons.rgb.getRgbItem(color, 0, rgbColor, 0);
+
+ this.color = rgbColor;
+ break;
+
+ case 4:
+ _colorspace.ColorSpace.singletons.cmyk.getRgbItem(color, 0, rgbColor, 0);
+
+ this.color = rgbColor;
+ break;
+
+ default:
+ this.color = rgbColor;
+ break;
+ }
+ }
+ }, {
+ key: "setBorderStyle",
+ value: function setBorderStyle(borderStyle) {
+ this.borderStyle = new AnnotationBorderStyle();
+
+ if (!(0, _primitives.isDict)(borderStyle)) {
+ return;
+ }
+
+ if (borderStyle.has('BS')) {
+ var dict = borderStyle.get('BS');
+ var dictType = dict.get('Type');
+
+ if (!dictType || (0, _primitives.isName)(dictType, 'Border')) {
+ this.borderStyle.setWidth(dict.get('W'), this.rectangle);
+ this.borderStyle.setStyle(dict.get('S'));
+ this.borderStyle.setDashArray(dict.getArray('D'));
+ }
+ } else if (borderStyle.has('Border')) {
+ var array = borderStyle.getArray('Border');
+
+ if (Array.isArray(array) && array.length >= 3) {
+ this.borderStyle.setHorizontalCornerRadius(array[0]);
+ this.borderStyle.setVerticalCornerRadius(array[1]);
+ this.borderStyle.setWidth(array[2], this.rectangle);
+
+ if (array.length === 4) {
+ this.borderStyle.setDashArray(array[3]);
+ }
+ }
+ } else {
+ this.borderStyle.setWidth(0);
+ }
+ }
+ }, {
+ key: "setAppearance",
+ value: function setAppearance(dict) {
+ this.appearance = null;
+ var appearanceStates = dict.get('AP');
+
+ if (!(0, _primitives.isDict)(appearanceStates)) {
+ return;
+ }
+
+ var normalAppearanceState = appearanceStates.get('N');
+
+ if ((0, _primitives.isStream)(normalAppearanceState)) {
+ this.appearance = normalAppearanceState;
+ return;
+ }
+
+ if (!(0, _primitives.isDict)(normalAppearanceState)) {
+ return;
+ }
+
+ var as = dict.get('AS');
+
+ if (!(0, _primitives.isName)(as) || !normalAppearanceState.has(as.name)) {
+ return;
+ }
+
+ this.appearance = normalAppearanceState.get(as.name);
+ }
+ }, {
+ key: "loadResources",
+ value: function loadResources(keys) {
+ return this.appearance.dict.getAsync('Resources').then(function (resources) {
+ if (!resources) {
+ return undefined;
+ }
+
+ var objectLoader = new _obj.ObjectLoader(resources, keys, resources.xref);
+ return objectLoader.load().then(function () {
+ return resources;
+ });
+ });
+ }
+ }, {
+ key: "getOperatorList",
+ value: function getOperatorList(evaluator, task, renderForms) {
+ var _this = this;
+
+ if (!this.appearance) {
+ return Promise.resolve(new _operator_list.OperatorList());
+ }
+
+ var data = this.data;
+ var appearanceDict = this.appearance.dict;
+ var resourcesPromise = this.loadResources(['ExtGState', 'ColorSpace', 'Pattern', 'Shading', 'XObject', 'Font']);
+ var bbox = appearanceDict.getArray('BBox') || [0, 0, 1, 1];
+ var matrix = appearanceDict.getArray('Matrix') || [1, 0, 0, 1, 0, 0];
+ var transform = getTransformMatrix(data.rect, bbox, matrix);
+ return resourcesPromise.then(function (resources) {
+ var opList = new _operator_list.OperatorList();
+ opList.addOp(_util.OPS.beginAnnotation, [data.rect, transform, matrix]);
+ return evaluator.getOperatorList({
+ stream: _this.appearance,
+ task: task,
+ resources: resources,
+ operatorList: opList
+ }).then(function () {
+ opList.addOp(_util.OPS.endAnnotation, []);
+
+ _this.appearance.reset();
+
+ return opList;
+ });
+ });
+ }
+ }, {
+ key: "viewable",
+ get: function get() {
+ if (this.flags === 0) {
+ return true;
+ }
+
+ return this._isViewable(this.flags);
+ }
+ }, {
+ key: "printable",
+ get: function get() {
+ if (this.flags === 0) {
+ return false;
+ }
+
+ return this._isPrintable(this.flags);
+ }
+ }]);
+
+ return Annotation;
+}();
+
+exports.Annotation = Annotation;
+
+var AnnotationBorderStyle =
+/*#__PURE__*/
+function () {
+ function AnnotationBorderStyle() {
+ _classCallCheck(this, AnnotationBorderStyle);
+
+ this.width = 1;
+ this.style = _util.AnnotationBorderStyleType.SOLID;
+ this.dashArray = [3];
+ this.horizontalCornerRadius = 0;
+ this.verticalCornerRadius = 0;
+ }
+
+ _createClass(AnnotationBorderStyle, [{
+ key: "setWidth",
+ value: function setWidth(width) {
+ var rect = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [0, 0, 0, 0];
+
+ if ((0, _primitives.isName)(width)) {
+ this.width = 0;
+ return;
+ }
+
+ if (Number.isInteger(width)) {
+ if (width > 0) {
+ var maxWidth = (rect[2] - rect[0]) / 2;
+ var maxHeight = (rect[3] - rect[1]) / 2;
+
+ if (maxWidth > 0 && maxHeight > 0 && (width > maxWidth || width > maxHeight)) {
+ (0, _util.warn)("AnnotationBorderStyle.setWidth - ignoring width: ".concat(width));
+ width = 1;
+ }
+ }
+
+ this.width = width;
+ }
+ }
+ }, {
+ key: "setStyle",
+ value: function setStyle(style) {
+ if (!(0, _primitives.isName)(style)) {
+ return;
+ }
+
+ switch (style.name) {
+ case 'S':
+ this.style = _util.AnnotationBorderStyleType.SOLID;
+ break;
+
+ case 'D':
+ this.style = _util.AnnotationBorderStyleType.DASHED;
+ break;
+
+ case 'B':
+ this.style = _util.AnnotationBorderStyleType.BEVELED;
+ break;
+
+ case 'I':
+ this.style = _util.AnnotationBorderStyleType.INSET;
+ break;
+
+ case 'U':
+ this.style = _util.AnnotationBorderStyleType.UNDERLINE;
+ break;
+
+ default:
+ break;
+ }
+ }
+ }, {
+ key: "setDashArray",
+ value: function setDashArray(dashArray) {
+ if (Array.isArray(dashArray) && dashArray.length > 0) {
+ var isValid = true;
+ var allZeros = true;
+
+ for (var i = 0, len = dashArray.length; i < len; i++) {
+ var element = dashArray[i];
+ var validNumber = +element >= 0;
+
+ if (!validNumber) {
+ isValid = false;
+ break;
+ } else if (element > 0) {
+ allZeros = false;
+ }
+ }
+
+ if (isValid && !allZeros) {
+ this.dashArray = dashArray;
+ } else {
+ this.width = 0;
+ }
+ } else if (dashArray) {
+ this.width = 0;
+ }
+ }
+ }, {
+ key: "setHorizontalCornerRadius",
+ value: function setHorizontalCornerRadius(radius) {
+ if (Number.isInteger(radius)) {
+ this.horizontalCornerRadius = radius;
+ }
+ }
+ }, {
+ key: "setVerticalCornerRadius",
+ value: function setVerticalCornerRadius(radius) {
+ if (Number.isInteger(radius)) {
+ this.verticalCornerRadius = radius;
+ }
+ }
+ }]);
+
+ return AnnotationBorderStyle;
+}();
+
+exports.AnnotationBorderStyle = AnnotationBorderStyle;
+
+var MarkupAnnotation =
+/*#__PURE__*/
+function (_Annotation) {
+ _inherits(MarkupAnnotation, _Annotation);
+
+ function MarkupAnnotation(parameters) {
+ var _this2;
+
+ _classCallCheck(this, MarkupAnnotation);
+
+ _this2 = _possibleConstructorReturn(this, _getPrototypeOf(MarkupAnnotation).call(this, parameters));
+ var dict = parameters.dict;
+
+ if (!dict.has('C')) {
+ _this2.data.color = null;
+ }
+
+ _this2.setCreationDate(dict.get('CreationDate'));
+
+ _this2.data.creationDate = _this2.creationDate;
+ _this2.data.hasPopup = dict.has('Popup');
+ _this2.data.title = (0, _util.stringToPDFString)(dict.get('T') || '');
+ return _this2;
+ }
+
+ _createClass(MarkupAnnotation, [{
+ key: "setCreationDate",
+ value: function setCreationDate(creationDate) {
+ this.creationDate = (0, _util.isString)(creationDate) ? creationDate : null;
+ }
+ }]);
+
+ return MarkupAnnotation;
+}(Annotation);
+
+exports.MarkupAnnotation = MarkupAnnotation;
+
+var WidgetAnnotation =
+/*#__PURE__*/
+function (_Annotation2) {
+ _inherits(WidgetAnnotation, _Annotation2);
+
+ function WidgetAnnotation(params) {
+ var _this3;
+
+ _classCallCheck(this, WidgetAnnotation);
+
+ _this3 = _possibleConstructorReturn(this, _getPrototypeOf(WidgetAnnotation).call(this, params));
+ var dict = params.dict;
+ var data = _this3.data;
+ data.annotationType = _util.AnnotationType.WIDGET;
+ data.fieldName = _this3._constructFieldName(dict);
+ data.fieldValue = (0, _core_utils.getInheritableProperty)({
+ dict: dict,
+ key: 'V',
+ getArray: true
+ });
+ data.alternativeText = (0, _util.stringToPDFString)(dict.get('TU') || '');
+ data.defaultAppearance = (0, _core_utils.getInheritableProperty)({
+ dict: dict,
+ key: 'DA'
+ }) || '';
+ var fieldType = (0, _core_utils.getInheritableProperty)({
+ dict: dict,
+ key: 'FT'
+ });
+ data.fieldType = (0, _primitives.isName)(fieldType) ? fieldType.name : null;
+ _this3.fieldResources = (0, _core_utils.getInheritableProperty)({
+ dict: dict,
+ key: 'DR'
+ }) || _primitives.Dict.empty;
+ data.fieldFlags = (0, _core_utils.getInheritableProperty)({
+ dict: dict,
+ key: 'Ff'
+ });
+
+ if (!Number.isInteger(data.fieldFlags) || data.fieldFlags < 0) {
+ data.fieldFlags = 0;
+ }
+
+ data.readOnly = _this3.hasFieldFlag(_util.AnnotationFieldFlag.READONLY);
+
+ if (data.fieldType === 'Sig') {
+ data.fieldValue = null;
+
+ _this3.setFlags(_util.AnnotationFlag.HIDDEN);
+ }
+
+ return _this3;
+ }
+
+ _createClass(WidgetAnnotation, [{
+ key: "_constructFieldName",
+ value: function _constructFieldName(dict) {
+ if (!dict.has('T') && !dict.has('Parent')) {
+ (0, _util.warn)('Unknown field name, falling back to empty field name.');
+ return '';
+ }
+
+ if (!dict.has('Parent')) {
+ return (0, _util.stringToPDFString)(dict.get('T'));
+ }
+
+ var fieldName = [];
+
+ if (dict.has('T')) {
+ fieldName.unshift((0, _util.stringToPDFString)(dict.get('T')));
+ }
+
+ var loopDict = dict;
+
+ while (loopDict.has('Parent')) {
+ loopDict = loopDict.get('Parent');
+
+ if (!(0, _primitives.isDict)(loopDict)) {
+ break;
+ }
+
+ if (loopDict.has('T')) {
+ fieldName.unshift((0, _util.stringToPDFString)(loopDict.get('T')));
+ }
+ }
+
+ return fieldName.join('.');
+ }
+ }, {
+ key: "hasFieldFlag",
+ value: function hasFieldFlag(flag) {
+ return !!(this.data.fieldFlags & flag);
+ }
+ }, {
+ key: "getOperatorList",
+ value: function getOperatorList(evaluator, task, renderForms) {
+ if (renderForms) {
+ return Promise.resolve(new _operator_list.OperatorList());
+ }
+
+ return _get(_getPrototypeOf(WidgetAnnotation.prototype), "getOperatorList", this).call(this, evaluator, task, renderForms);
+ }
+ }]);
+
+ return WidgetAnnotation;
+}(Annotation);
+
+var TextWidgetAnnotation =
+/*#__PURE__*/
+function (_WidgetAnnotation) {
+ _inherits(TextWidgetAnnotation, _WidgetAnnotation);
+
+ function TextWidgetAnnotation(params) {
+ var _this4;
+
+ _classCallCheck(this, TextWidgetAnnotation);
+
+ _this4 = _possibleConstructorReturn(this, _getPrototypeOf(TextWidgetAnnotation).call(this, params));
+ var dict = params.dict;
+ _this4.data.fieldValue = (0, _util.stringToPDFString)(_this4.data.fieldValue || '');
+ var alignment = (0, _core_utils.getInheritableProperty)({
+ dict: dict,
+ key: 'Q'
+ });
+
+ if (!Number.isInteger(alignment) || alignment < 0 || alignment > 2) {
+ alignment = null;
+ }
+
+ _this4.data.textAlignment = alignment;
+ var maximumLength = (0, _core_utils.getInheritableProperty)({
+ dict: dict,
+ key: 'MaxLen'
+ });
+
+ if (!Number.isInteger(maximumLength) || maximumLength < 0) {
+ maximumLength = null;
+ }
+
+ _this4.data.maxLen = maximumLength;
+ _this4.data.multiLine = _this4.hasFieldFlag(_util.AnnotationFieldFlag.MULTILINE);
+ _this4.data.comb = _this4.hasFieldFlag(_util.AnnotationFieldFlag.COMB) && !_this4.hasFieldFlag(_util.AnnotationFieldFlag.MULTILINE) && !_this4.hasFieldFlag(_util.AnnotationFieldFlag.PASSWORD) && !_this4.hasFieldFlag(_util.AnnotationFieldFlag.FILESELECT) && _this4.data.maxLen !== null;
+ return _this4;
+ }
+
+ _createClass(TextWidgetAnnotation, [{
+ key: "getOperatorList",
+ value: function getOperatorList(evaluator, task, renderForms) {
+ if (renderForms || this.appearance) {
+ return _get(_getPrototypeOf(TextWidgetAnnotation.prototype), "getOperatorList", this).call(this, evaluator, task, renderForms);
+ }
+
+ var operatorList = new _operator_list.OperatorList();
+
+ if (!this.data.defaultAppearance) {
+ return Promise.resolve(operatorList);
+ }
+
+ var stream = new _stream.Stream((0, _util.stringToBytes)(this.data.defaultAppearance));
+ return evaluator.getOperatorList({
+ stream: stream,
+ task: task,
+ resources: this.fieldResources,
+ operatorList: operatorList
+ }).then(function () {
+ return operatorList;
+ });
+ }
+ }]);
+
+ return TextWidgetAnnotation;
+}(WidgetAnnotation);
+
+var ButtonWidgetAnnotation =
+/*#__PURE__*/
+function (_WidgetAnnotation2) {
+ _inherits(ButtonWidgetAnnotation, _WidgetAnnotation2);
+
+ function ButtonWidgetAnnotation(params) {
+ var _this5;
+
+ _classCallCheck(this, ButtonWidgetAnnotation);
+
+ _this5 = _possibleConstructorReturn(this, _getPrototypeOf(ButtonWidgetAnnotation).call(this, params));
+ _this5.data.checkBox = !_this5.hasFieldFlag(_util.AnnotationFieldFlag.RADIO) && !_this5.hasFieldFlag(_util.AnnotationFieldFlag.PUSHBUTTON);
+ _this5.data.radioButton = _this5.hasFieldFlag(_util.AnnotationFieldFlag.RADIO) && !_this5.hasFieldFlag(_util.AnnotationFieldFlag.PUSHBUTTON);
+ _this5.data.pushButton = _this5.hasFieldFlag(_util.AnnotationFieldFlag.PUSHBUTTON);
+
+ if (_this5.data.checkBox) {
+ _this5._processCheckBox(params);
+ } else if (_this5.data.radioButton) {
+ _this5._processRadioButton(params);
+ } else if (_this5.data.pushButton) {
+ _this5._processPushButton(params);
+ } else {
+ (0, _util.warn)('Invalid field flags for button widget annotation');
+ }
+
+ return _this5;
+ }
+
+ _createClass(ButtonWidgetAnnotation, [{
+ key: "_processCheckBox",
+ value: function _processCheckBox(params) {
+ if ((0, _primitives.isName)(this.data.fieldValue)) {
+ this.data.fieldValue = this.data.fieldValue.name;
+ }
+
+ var customAppearance = params.dict.get('AP');
+
+ if (!(0, _primitives.isDict)(customAppearance)) {
+ return;
+ }
+
+ var exportValueOptionsDict = customAppearance.get('D');
+
+ if (!(0, _primitives.isDict)(exportValueOptionsDict)) {
+ return;
+ }
+
+ var exportValues = exportValueOptionsDict.getKeys();
+ var hasCorrectOptionCount = exportValues.length === 2;
+
+ if (!hasCorrectOptionCount) {
+ return;
+ }
+
+ this.data.exportValue = exportValues[0] === 'Off' ? exportValues[1] : exportValues[0];
+ }
+ }, {
+ key: "_processRadioButton",
+ value: function _processRadioButton(params) {
+ this.data.fieldValue = this.data.buttonValue = null;
+ var fieldParent = params.dict.get('Parent');
+
+ if ((0, _primitives.isDict)(fieldParent) && fieldParent.has('V')) {
+ var fieldParentValue = fieldParent.get('V');
+
+ if ((0, _primitives.isName)(fieldParentValue)) {
+ this.data.fieldValue = fieldParentValue.name;
+ }
+ }
+
+ var appearanceStates = params.dict.get('AP');
+
+ if (!(0, _primitives.isDict)(appearanceStates)) {
+ return;
+ }
+
+ var normalAppearanceState = appearanceStates.get('N');
+
+ if (!(0, _primitives.isDict)(normalAppearanceState)) {
+ return;
+ }
+
+ var keys = normalAppearanceState.getKeys();
+
+ for (var i = 0, ii = keys.length; i < ii; i++) {
+ if (keys[i] !== 'Off') {
+ this.data.buttonValue = keys[i];
+ break;
+ }
+ }
+ }
+ }, {
+ key: "_processPushButton",
+ value: function _processPushButton(params) {
+ if (!params.dict.has('A')) {
+ (0, _util.warn)('Push buttons without action dictionaries are not supported');
+ return;
+ }
+
+ _obj.Catalog.parseDestDictionary({
+ destDict: params.dict,
+ resultObj: this.data,
+ docBaseUrl: params.pdfManager.docBaseUrl
+ });
+ }
+ }]);
+
+ return ButtonWidgetAnnotation;
+}(WidgetAnnotation);
+
+var ChoiceWidgetAnnotation =
+/*#__PURE__*/
+function (_WidgetAnnotation3) {
+ _inherits(ChoiceWidgetAnnotation, _WidgetAnnotation3);
+
+ function ChoiceWidgetAnnotation(params) {
+ var _this6;
+
+ _classCallCheck(this, ChoiceWidgetAnnotation);
+
+ _this6 = _possibleConstructorReturn(this, _getPrototypeOf(ChoiceWidgetAnnotation).call(this, params));
+ _this6.data.options = [];
+ var options = (0, _core_utils.getInheritableProperty)({
+ dict: params.dict,
+ key: 'Opt'
+ });
+
+ if (Array.isArray(options)) {
+ var xref = params.xref;
+
+ for (var i = 0, ii = options.length; i < ii; i++) {
+ var option = xref.fetchIfRef(options[i]);
+ var isOptionArray = Array.isArray(option);
+ _this6.data.options[i] = {
+ exportValue: isOptionArray ? xref.fetchIfRef(option[0]) : option,
+ displayValue: (0, _util.stringToPDFString)(isOptionArray ? xref.fetchIfRef(option[1]) : option)
+ };
+ }
+ }
+
+ if (!Array.isArray(_this6.data.fieldValue)) {
+ _this6.data.fieldValue = [_this6.data.fieldValue];
+ }
+
+ _this6.data.combo = _this6.hasFieldFlag(_util.AnnotationFieldFlag.COMBO);
+ _this6.data.multiSelect = _this6.hasFieldFlag(_util.AnnotationFieldFlag.MULTISELECT);
+ return _this6;
+ }
+
+ return ChoiceWidgetAnnotation;
+}(WidgetAnnotation);
+
+var TextAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation) {
+ _inherits(TextAnnotation, _MarkupAnnotation);
+
+ function TextAnnotation(parameters) {
+ var _this7;
+
+ _classCallCheck(this, TextAnnotation);
+
+ var DEFAULT_ICON_SIZE = 22;
+ _this7 = _possibleConstructorReturn(this, _getPrototypeOf(TextAnnotation).call(this, parameters));
+ _this7.data.annotationType = _util.AnnotationType.TEXT;
+
+ if (_this7.data.hasAppearance) {
+ _this7.data.name = 'NoIcon';
+ } else {
+ _this7.data.rect[1] = _this7.data.rect[3] - DEFAULT_ICON_SIZE;
+ _this7.data.rect[2] = _this7.data.rect[0] + DEFAULT_ICON_SIZE;
+ _this7.data.name = parameters.dict.has('Name') ? parameters.dict.get('Name').name : 'Note';
+ }
+
+ return _this7;
+ }
+
+ return TextAnnotation;
+}(MarkupAnnotation);
+
+var LinkAnnotation =
+/*#__PURE__*/
+function (_Annotation3) {
+ _inherits(LinkAnnotation, _Annotation3);
+
+ function LinkAnnotation(params) {
+ var _this8;
+
+ _classCallCheck(this, LinkAnnotation);
+
+ _this8 = _possibleConstructorReturn(this, _getPrototypeOf(LinkAnnotation).call(this, params));
+ _this8.data.annotationType = _util.AnnotationType.LINK;
+
+ _obj.Catalog.parseDestDictionary({
+ destDict: params.dict,
+ resultObj: _this8.data,
+ docBaseUrl: params.pdfManager.docBaseUrl
+ });
+
+ return _this8;
+ }
+
+ return LinkAnnotation;
+}(Annotation);
+
+var PopupAnnotation =
+/*#__PURE__*/
+function (_Annotation4) {
+ _inherits(PopupAnnotation, _Annotation4);
+
+ function PopupAnnotation(parameters) {
+ var _this9;
+
+ _classCallCheck(this, PopupAnnotation);
+
+ _this9 = _possibleConstructorReturn(this, _getPrototypeOf(PopupAnnotation).call(this, parameters));
+ _this9.data.annotationType = _util.AnnotationType.POPUP;
+ var dict = parameters.dict;
+ var parentItem = dict.get('Parent');
+
+ if (!parentItem) {
+ (0, _util.warn)('Popup annotation has a missing or invalid parent annotation.');
+ return _possibleConstructorReturn(_this9);
+ }
+
+ var parentSubtype = parentItem.get('Subtype');
+ _this9.data.parentType = (0, _primitives.isName)(parentSubtype) ? parentSubtype.name : null;
+ _this9.data.parentId = dict.getRaw('Parent').toString();
+ _this9.data.title = (0, _util.stringToPDFString)(parentItem.get('T') || '');
+ _this9.data.contents = (0, _util.stringToPDFString)(parentItem.get('Contents') || '');
+
+ if (!parentItem.has('M')) {
+ _this9.data.modificationDate = null;
+ } else {
+ _this9.setModificationDate(parentItem.get('M'));
+
+ _this9.data.modificationDate = _this9.modificationDate;
+ }
+
+ if (!parentItem.has('C')) {
+ _this9.data.color = null;
+ } else {
+ _this9.setColor(parentItem.getArray('C'));
+
+ _this9.data.color = _this9.color;
+ }
+
+ if (!_this9.viewable) {
+ var parentFlags = parentItem.get('F');
+
+ if (_this9._isViewable(parentFlags)) {
+ _this9.setFlags(parentFlags);
+ }
+ }
+
+ return _this9;
+ }
+
+ return PopupAnnotation;
+}(Annotation);
+
+var FreeTextAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation2) {
+ _inherits(FreeTextAnnotation, _MarkupAnnotation2);
+
+ function FreeTextAnnotation(parameters) {
+ var _this10;
+
+ _classCallCheck(this, FreeTextAnnotation);
+
+ _this10 = _possibleConstructorReturn(this, _getPrototypeOf(FreeTextAnnotation).call(this, parameters));
+ _this10.data.annotationType = _util.AnnotationType.FREETEXT;
+ return _this10;
+ }
+
+ return FreeTextAnnotation;
+}(MarkupAnnotation);
+
+var LineAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation3) {
+ _inherits(LineAnnotation, _MarkupAnnotation3);
+
+ function LineAnnotation(parameters) {
+ var _this11;
+
+ _classCallCheck(this, LineAnnotation);
+
+ _this11 = _possibleConstructorReturn(this, _getPrototypeOf(LineAnnotation).call(this, parameters));
+ _this11.data.annotationType = _util.AnnotationType.LINE;
+ var dict = parameters.dict;
+ _this11.data.lineCoordinates = _util.Util.normalizeRect(dict.getArray('L'));
+ return _this11;
+ }
+
+ return LineAnnotation;
+}(MarkupAnnotation);
+
+var SquareAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation4) {
+ _inherits(SquareAnnotation, _MarkupAnnotation4);
+
+ function SquareAnnotation(parameters) {
+ var _this12;
+
+ _classCallCheck(this, SquareAnnotation);
+
+ _this12 = _possibleConstructorReturn(this, _getPrototypeOf(SquareAnnotation).call(this, parameters));
+ _this12.data.annotationType = _util.AnnotationType.SQUARE;
+ return _this12;
+ }
+
+ return SquareAnnotation;
+}(MarkupAnnotation);
+
+var CircleAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation5) {
+ _inherits(CircleAnnotation, _MarkupAnnotation5);
+
+ function CircleAnnotation(parameters) {
+ var _this13;
+
+ _classCallCheck(this, CircleAnnotation);
+
+ _this13 = _possibleConstructorReturn(this, _getPrototypeOf(CircleAnnotation).call(this, parameters));
+ _this13.data.annotationType = _util.AnnotationType.CIRCLE;
+ return _this13;
+ }
+
+ return CircleAnnotation;
+}(MarkupAnnotation);
+
+var PolylineAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation6) {
+ _inherits(PolylineAnnotation, _MarkupAnnotation6);
+
+ function PolylineAnnotation(parameters) {
+ var _this14;
+
+ _classCallCheck(this, PolylineAnnotation);
+
+ _this14 = _possibleConstructorReturn(this, _getPrototypeOf(PolylineAnnotation).call(this, parameters));
+ _this14.data.annotationType = _util.AnnotationType.POLYLINE;
+ var dict = parameters.dict;
+ var rawVertices = dict.getArray('Vertices');
+ _this14.data.vertices = [];
+
+ for (var i = 0, ii = rawVertices.length; i < ii; i += 2) {
+ _this14.data.vertices.push({
+ x: rawVertices[i],
+ y: rawVertices[i + 1]
+ });
+ }
+
+ return _this14;
+ }
+
+ return PolylineAnnotation;
+}(MarkupAnnotation);
+
+var PolygonAnnotation =
+/*#__PURE__*/
+function (_PolylineAnnotation) {
+ _inherits(PolygonAnnotation, _PolylineAnnotation);
+
+ function PolygonAnnotation(parameters) {
+ var _this15;
+
+ _classCallCheck(this, PolygonAnnotation);
+
+ _this15 = _possibleConstructorReturn(this, _getPrototypeOf(PolygonAnnotation).call(this, parameters));
+ _this15.data.annotationType = _util.AnnotationType.POLYGON;
+ return _this15;
+ }
+
+ return PolygonAnnotation;
+}(PolylineAnnotation);
+
+var CaretAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation7) {
+ _inherits(CaretAnnotation, _MarkupAnnotation7);
+
+ function CaretAnnotation(parameters) {
+ var _this16;
+
+ _classCallCheck(this, CaretAnnotation);
+
+ _this16 = _possibleConstructorReturn(this, _getPrototypeOf(CaretAnnotation).call(this, parameters));
+ _this16.data.annotationType = _util.AnnotationType.CARET;
+ return _this16;
+ }
+
+ return CaretAnnotation;
+}(MarkupAnnotation);
+
+var InkAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation8) {
+ _inherits(InkAnnotation, _MarkupAnnotation8);
+
+ function InkAnnotation(parameters) {
+ var _this17;
+
+ _classCallCheck(this, InkAnnotation);
+
+ _this17 = _possibleConstructorReturn(this, _getPrototypeOf(InkAnnotation).call(this, parameters));
+ _this17.data.annotationType = _util.AnnotationType.INK;
+ var dict = parameters.dict;
+ var xref = parameters.xref;
+ var originalInkLists = dict.getArray('InkList');
+ _this17.data.inkLists = [];
+
+ for (var i = 0, ii = originalInkLists.length; i < ii; ++i) {
+ _this17.data.inkLists.push([]);
+
+ for (var j = 0, jj = originalInkLists[i].length; j < jj; j += 2) {
+ _this17.data.inkLists[i].push({
+ x: xref.fetchIfRef(originalInkLists[i][j]),
+ y: xref.fetchIfRef(originalInkLists[i][j + 1])
+ });
+ }
+ }
+
+ return _this17;
+ }
+
+ return InkAnnotation;
+}(MarkupAnnotation);
+
+var HighlightAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation9) {
+ _inherits(HighlightAnnotation, _MarkupAnnotation9);
+
+ function HighlightAnnotation(parameters) {
+ var _this18;
+
+ _classCallCheck(this, HighlightAnnotation);
+
+ _this18 = _possibleConstructorReturn(this, _getPrototypeOf(HighlightAnnotation).call(this, parameters));
+ _this18.data.annotationType = _util.AnnotationType.HIGHLIGHT;
+ return _this18;
+ }
+
+ return HighlightAnnotation;
+}(MarkupAnnotation);
+
+var UnderlineAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation10) {
+ _inherits(UnderlineAnnotation, _MarkupAnnotation10);
+
+ function UnderlineAnnotation(parameters) {
+ var _this19;
+
+ _classCallCheck(this, UnderlineAnnotation);
+
+ _this19 = _possibleConstructorReturn(this, _getPrototypeOf(UnderlineAnnotation).call(this, parameters));
+ _this19.data.annotationType = _util.AnnotationType.UNDERLINE;
+ return _this19;
+ }
+
+ return UnderlineAnnotation;
+}(MarkupAnnotation);
+
+var SquigglyAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation11) {
+ _inherits(SquigglyAnnotation, _MarkupAnnotation11);
+
+ function SquigglyAnnotation(parameters) {
+ var _this20;
+
+ _classCallCheck(this, SquigglyAnnotation);
+
+ _this20 = _possibleConstructorReturn(this, _getPrototypeOf(SquigglyAnnotation).call(this, parameters));
+ _this20.data.annotationType = _util.AnnotationType.SQUIGGLY;
+ return _this20;
+ }
+
+ return SquigglyAnnotation;
+}(MarkupAnnotation);
+
+var StrikeOutAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation12) {
+ _inherits(StrikeOutAnnotation, _MarkupAnnotation12);
+
+ function StrikeOutAnnotation(parameters) {
+ var _this21;
+
+ _classCallCheck(this, StrikeOutAnnotation);
+
+ _this21 = _possibleConstructorReturn(this, _getPrototypeOf(StrikeOutAnnotation).call(this, parameters));
+ _this21.data.annotationType = _util.AnnotationType.STRIKEOUT;
+ return _this21;
+ }
+
+ return StrikeOutAnnotation;
+}(MarkupAnnotation);
+
+var StampAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation13) {
+ _inherits(StampAnnotation, _MarkupAnnotation13);
+
+ function StampAnnotation(parameters) {
+ var _this22;
+
+ _classCallCheck(this, StampAnnotation);
+
+ _this22 = _possibleConstructorReturn(this, _getPrototypeOf(StampAnnotation).call(this, parameters));
+ _this22.data.annotationType = _util.AnnotationType.STAMP;
+ return _this22;
+ }
+
+ return StampAnnotation;
+}(MarkupAnnotation);
+
+var FileAttachmentAnnotation =
+/*#__PURE__*/
+function (_MarkupAnnotation14) {
+ _inherits(FileAttachmentAnnotation, _MarkupAnnotation14);
+
+ function FileAttachmentAnnotation(parameters) {
+ var _this23;
+
+ _classCallCheck(this, FileAttachmentAnnotation);
+
+ _this23 = _possibleConstructorReturn(this, _getPrototypeOf(FileAttachmentAnnotation).call(this, parameters));
+ var file = new _obj.FileSpec(parameters.dict.get('FS'), parameters.xref);
+ _this23.data.annotationType = _util.AnnotationType.FILEATTACHMENT;
+ _this23.data.file = file.serializable;
+ return _this23;
+ }
+
+ return FileAttachmentAnnotation;
+}(MarkupAnnotation);
+
+/***/ }),
+/* 171 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.OperatorList = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var QueueOptimizer = function QueueOptimizerClosure() {
+ function addState(parentState, pattern, checkFn, iterateFn, processFn) {
+ var state = parentState;
+
+ for (var i = 0, ii = pattern.length - 1; i < ii; i++) {
+ var item = pattern[i];
+ state = state[item] || (state[item] = []);
+ }
+
+ state[pattern[pattern.length - 1]] = {
+ checkFn: checkFn,
+ iterateFn: iterateFn,
+ processFn: processFn
+ };
+ }
+
+ function handlePaintSolidColorImageMask(iFirstSave, count, fnArray, argsArray) {
+ var iFirstPIMXO = iFirstSave + 2;
+
+ for (var i = 0; i < count; i++) {
+ var arg = argsArray[iFirstPIMXO + 4 * i];
+ var imageMask = arg.length === 1 && arg[0];
+
+ if (imageMask && imageMask.width === 1 && imageMask.height === 1 && (!imageMask.data.length || imageMask.data.length === 1 && imageMask.data[0] === 0)) {
+ fnArray[iFirstPIMXO + 4 * i] = _util.OPS.paintSolidColorImageMask;
+ continue;
+ }
+
+ break;
+ }
+
+ return count - i;
+ }
+
+ var InitialState = [];
+ addState(InitialState, [_util.OPS.save, _util.OPS.transform, _util.OPS.paintInlineImageXObject, _util.OPS.restore], null, function iterateInlineImageGroup(context, i) {
+ var fnArray = context.fnArray;
+ var iFirstSave = context.iCurr - 3;
+ var pos = (i - iFirstSave) % 4;
+
+ switch (pos) {
+ case 0:
+ return fnArray[i] === _util.OPS.save;
+
+ case 1:
+ return fnArray[i] === _util.OPS.transform;
+
+ case 2:
+ return fnArray[i] === _util.OPS.paintInlineImageXObject;
+
+ case 3:
+ return fnArray[i] === _util.OPS.restore;
+ }
+
+ throw new Error("iterateInlineImageGroup - invalid pos: ".concat(pos));
+ }, function foundInlineImageGroup(context, i) {
+ var MIN_IMAGES_IN_INLINE_IMAGES_BLOCK = 10;
+ var MAX_IMAGES_IN_INLINE_IMAGES_BLOCK = 200;
+ var MAX_WIDTH = 1000;
+ var IMAGE_PADDING = 1;
+ var fnArray = context.fnArray,
+ argsArray = context.argsArray;
+ var curr = context.iCurr;
+ var iFirstSave = curr - 3;
+ var iFirstTransform = curr - 2;
+ var iFirstPIIXO = curr - 1;
+ var count = Math.min(Math.floor((i - iFirstSave) / 4), MAX_IMAGES_IN_INLINE_IMAGES_BLOCK);
+
+ if (count < MIN_IMAGES_IN_INLINE_IMAGES_BLOCK) {
+ return i - (i - iFirstSave) % 4;
+ }
+
+ var maxX = 0;
+ var map = [],
+ maxLineHeight = 0;
+ var currentX = IMAGE_PADDING,
+ currentY = IMAGE_PADDING;
+ var q;
+
+ for (q = 0; q < count; q++) {
+ var transform = argsArray[iFirstTransform + (q << 2)];
+ var img = argsArray[iFirstPIIXO + (q << 2)][0];
+
+ if (currentX + img.width > MAX_WIDTH) {
+ maxX = Math.max(maxX, currentX);
+ currentY += maxLineHeight + 2 * IMAGE_PADDING;
+ currentX = 0;
+ maxLineHeight = 0;
+ }
+
+ map.push({
+ transform: transform,
+ x: currentX,
+ y: currentY,
+ w: img.width,
+ h: img.height
+ });
+ currentX += img.width + 2 * IMAGE_PADDING;
+ maxLineHeight = Math.max(maxLineHeight, img.height);
+ }
+
+ var imgWidth = Math.max(maxX, currentX) + IMAGE_PADDING;
+ var imgHeight = currentY + maxLineHeight + IMAGE_PADDING;
+ var imgData = new Uint8ClampedArray(imgWidth * imgHeight * 4);
+ var imgRowSize = imgWidth << 2;
+
+ for (q = 0; q < count; q++) {
+ var data = argsArray[iFirstPIIXO + (q << 2)][0].data;
+ var rowSize = map[q].w << 2;
+ var dataOffset = 0;
+ var offset = map[q].x + map[q].y * imgWidth << 2;
+ imgData.set(data.subarray(0, rowSize), offset - imgRowSize);
+
+ for (var k = 0, kk = map[q].h; k < kk; k++) {
+ imgData.set(data.subarray(dataOffset, dataOffset + rowSize), offset);
+ dataOffset += rowSize;
+ offset += imgRowSize;
+ }
+
+ imgData.set(data.subarray(dataOffset - rowSize, dataOffset), offset);
+
+ while (offset >= 0) {
+ data[offset - 4] = data[offset];
+ data[offset - 3] = data[offset + 1];
+ data[offset - 2] = data[offset + 2];
+ data[offset - 1] = data[offset + 3];
+ data[offset + rowSize] = data[offset + rowSize - 4];
+ data[offset + rowSize + 1] = data[offset + rowSize - 3];
+ data[offset + rowSize + 2] = data[offset + rowSize - 2];
+ data[offset + rowSize + 3] = data[offset + rowSize - 1];
+ offset -= imgRowSize;
+ }
+ }
+
+ fnArray.splice(iFirstSave, count * 4, _util.OPS.paintInlineImageXObjectGroup);
+ argsArray.splice(iFirstSave, count * 4, [{
+ width: imgWidth,
+ height: imgHeight,
+ kind: _util.ImageKind.RGBA_32BPP,
+ data: imgData
+ }, map]);
+ return iFirstSave + 1;
+ });
+ addState(InitialState, [_util.OPS.save, _util.OPS.transform, _util.OPS.paintImageMaskXObject, _util.OPS.restore], null, function iterateImageMaskGroup(context, i) {
+ var fnArray = context.fnArray;
+ var iFirstSave = context.iCurr - 3;
+ var pos = (i - iFirstSave) % 4;
+
+ switch (pos) {
+ case 0:
+ return fnArray[i] === _util.OPS.save;
+
+ case 1:
+ return fnArray[i] === _util.OPS.transform;
+
+ case 2:
+ return fnArray[i] === _util.OPS.paintImageMaskXObject;
+
+ case 3:
+ return fnArray[i] === _util.OPS.restore;
+ }
+
+ throw new Error("iterateImageMaskGroup - invalid pos: ".concat(pos));
+ }, function foundImageMaskGroup(context, i) {
+ var MIN_IMAGES_IN_MASKS_BLOCK = 10;
+ var MAX_IMAGES_IN_MASKS_BLOCK = 100;
+ var MAX_SAME_IMAGES_IN_MASKS_BLOCK = 1000;
+ var fnArray = context.fnArray,
+ argsArray = context.argsArray;
+ var curr = context.iCurr;
+ var iFirstSave = curr - 3;
+ var iFirstTransform = curr - 2;
+ var iFirstPIMXO = curr - 1;
+ var count = Math.floor((i - iFirstSave) / 4);
+ count = handlePaintSolidColorImageMask(iFirstSave, count, fnArray, argsArray);
+
+ if (count < MIN_IMAGES_IN_MASKS_BLOCK) {
+ return i - (i - iFirstSave) % 4;
+ }
+
+ var q;
+ var isSameImage = false;
+ var iTransform, transformArgs;
+ var firstPIMXOArg0 = argsArray[iFirstPIMXO][0];
+
+ if (argsArray[iFirstTransform][1] === 0 && argsArray[iFirstTransform][2] === 0) {
+ isSameImage = true;
+ var firstTransformArg0 = argsArray[iFirstTransform][0];
+ var firstTransformArg3 = argsArray[iFirstTransform][3];
+ iTransform = iFirstTransform + 4;
+ var iPIMXO = iFirstPIMXO + 4;
+
+ for (q = 1; q < count; q++, iTransform += 4, iPIMXO += 4) {
+ transformArgs = argsArray[iTransform];
+
+ if (argsArray[iPIMXO][0] !== firstPIMXOArg0 || transformArgs[0] !== firstTransformArg0 || transformArgs[1] !== 0 || transformArgs[2] !== 0 || transformArgs[3] !== firstTransformArg3) {
+ if (q < MIN_IMAGES_IN_MASKS_BLOCK) {
+ isSameImage = false;
+ } else {
+ count = q;
+ }
+
+ break;
+ }
+ }
+ }
+
+ if (isSameImage) {
+ count = Math.min(count, MAX_SAME_IMAGES_IN_MASKS_BLOCK);
+ var positions = new Float32Array(count * 2);
+ iTransform = iFirstTransform;
+
+ for (q = 0; q < count; q++, iTransform += 4) {
+ transformArgs = argsArray[iTransform];
+ positions[q << 1] = transformArgs[4];
+ positions[(q << 1) + 1] = transformArgs[5];
+ }
+
+ fnArray.splice(iFirstSave, count * 4, _util.OPS.paintImageMaskXObjectRepeat);
+ argsArray.splice(iFirstSave, count * 4, [firstPIMXOArg0, firstTransformArg0, firstTransformArg3, positions]);
+ } else {
+ count = Math.min(count, MAX_IMAGES_IN_MASKS_BLOCK);
+ var images = [];
+
+ for (q = 0; q < count; q++) {
+ transformArgs = argsArray[iFirstTransform + (q << 2)];
+ var maskParams = argsArray[iFirstPIMXO + (q << 2)][0];
+ images.push({
+ data: maskParams.data,
+ width: maskParams.width,
+ height: maskParams.height,
+ transform: transformArgs
+ });
+ }
+
+ fnArray.splice(iFirstSave, count * 4, _util.OPS.paintImageMaskXObjectGroup);
+ argsArray.splice(iFirstSave, count * 4, [images]);
+ }
+
+ return iFirstSave + 1;
+ });
+ addState(InitialState, [_util.OPS.save, _util.OPS.transform, _util.OPS.paintImageXObject, _util.OPS.restore], function (context) {
+ var argsArray = context.argsArray;
+ var iFirstTransform = context.iCurr - 2;
+ return argsArray[iFirstTransform][1] === 0 && argsArray[iFirstTransform][2] === 0;
+ }, function iterateImageGroup(context, i) {
+ var fnArray = context.fnArray,
+ argsArray = context.argsArray;
+ var iFirstSave = context.iCurr - 3;
+ var pos = (i - iFirstSave) % 4;
+
+ switch (pos) {
+ case 0:
+ return fnArray[i] === _util.OPS.save;
+
+ case 1:
+ if (fnArray[i] !== _util.OPS.transform) {
+ return false;
+ }
+
+ var iFirstTransform = context.iCurr - 2;
+ var firstTransformArg0 = argsArray[iFirstTransform][0];
+ var firstTransformArg3 = argsArray[iFirstTransform][3];
+
+ if (argsArray[i][0] !== firstTransformArg0 || argsArray[i][1] !== 0 || argsArray[i][2] !== 0 || argsArray[i][3] !== firstTransformArg3) {
+ return false;
+ }
+
+ return true;
+
+ case 2:
+ if (fnArray[i] !== _util.OPS.paintImageXObject) {
+ return false;
+ }
+
+ var iFirstPIXO = context.iCurr - 1;
+ var firstPIXOArg0 = argsArray[iFirstPIXO][0];
+
+ if (argsArray[i][0] !== firstPIXOArg0) {
+ return false;
+ }
+
+ return true;
+
+ case 3:
+ return fnArray[i] === _util.OPS.restore;
+ }
+
+ throw new Error("iterateImageGroup - invalid pos: ".concat(pos));
+ }, function (context, i) {
+ var MIN_IMAGES_IN_BLOCK = 3;
+ var MAX_IMAGES_IN_BLOCK = 1000;
+ var fnArray = context.fnArray,
+ argsArray = context.argsArray;
+ var curr = context.iCurr;
+ var iFirstSave = curr - 3;
+ var iFirstTransform = curr - 2;
+ var iFirstPIXO = curr - 1;
+ var firstPIXOArg0 = argsArray[iFirstPIXO][0];
+ var firstTransformArg0 = argsArray[iFirstTransform][0];
+ var firstTransformArg3 = argsArray[iFirstTransform][3];
+ var count = Math.min(Math.floor((i - iFirstSave) / 4), MAX_IMAGES_IN_BLOCK);
+
+ if (count < MIN_IMAGES_IN_BLOCK) {
+ return i - (i - iFirstSave) % 4;
+ }
+
+ var positions = new Float32Array(count * 2);
+ var iTransform = iFirstTransform;
+
+ for (var q = 0; q < count; q++, iTransform += 4) {
+ var transformArgs = argsArray[iTransform];
+ positions[q << 1] = transformArgs[4];
+ positions[(q << 1) + 1] = transformArgs[5];
+ }
+
+ var args = [firstPIXOArg0, firstTransformArg0, firstTransformArg3, positions];
+ fnArray.splice(iFirstSave, count * 4, _util.OPS.paintImageXObjectRepeat);
+ argsArray.splice(iFirstSave, count * 4, args);
+ return iFirstSave + 1;
+ });
+ addState(InitialState, [_util.OPS.beginText, _util.OPS.setFont, _util.OPS.setTextMatrix, _util.OPS.showText, _util.OPS.endText], null, function iterateShowTextGroup(context, i) {
+ var fnArray = context.fnArray,
+ argsArray = context.argsArray;
+ var iFirstSave = context.iCurr - 4;
+ var pos = (i - iFirstSave) % 5;
+
+ switch (pos) {
+ case 0:
+ return fnArray[i] === _util.OPS.beginText;
+
+ case 1:
+ return fnArray[i] === _util.OPS.setFont;
+
+ case 2:
+ return fnArray[i] === _util.OPS.setTextMatrix;
+
+ case 3:
+ if (fnArray[i] !== _util.OPS.showText) {
+ return false;
+ }
+
+ var iFirstSetFont = context.iCurr - 3;
+ var firstSetFontArg0 = argsArray[iFirstSetFont][0];
+ var firstSetFontArg1 = argsArray[iFirstSetFont][1];
+
+ if (argsArray[i][0] !== firstSetFontArg0 || argsArray[i][1] !== firstSetFontArg1) {
+ return false;
+ }
+
+ return true;
+
+ case 4:
+ return fnArray[i] === _util.OPS.endText;
+ }
+
+ throw new Error("iterateShowTextGroup - invalid pos: ".concat(pos));
+ }, function (context, i) {
+ var MIN_CHARS_IN_BLOCK = 3;
+ var MAX_CHARS_IN_BLOCK = 1000;
+ var fnArray = context.fnArray,
+ argsArray = context.argsArray;
+ var curr = context.iCurr;
+ var iFirstBeginText = curr - 4;
+ var iFirstSetFont = curr - 3;
+ var iFirstSetTextMatrix = curr - 2;
+ var iFirstShowText = curr - 1;
+ var iFirstEndText = curr;
+ var firstSetFontArg0 = argsArray[iFirstSetFont][0];
+ var firstSetFontArg1 = argsArray[iFirstSetFont][1];
+ var count = Math.min(Math.floor((i - iFirstBeginText) / 5), MAX_CHARS_IN_BLOCK);
+
+ if (count < MIN_CHARS_IN_BLOCK) {
+ return i - (i - iFirstBeginText) % 5;
+ }
+
+ var iFirst = iFirstBeginText;
+
+ if (iFirstBeginText >= 4 && fnArray[iFirstBeginText - 4] === fnArray[iFirstSetFont] && fnArray[iFirstBeginText - 3] === fnArray[iFirstSetTextMatrix] && fnArray[iFirstBeginText - 2] === fnArray[iFirstShowText] && fnArray[iFirstBeginText - 1] === fnArray[iFirstEndText] && argsArray[iFirstBeginText - 4][0] === firstSetFontArg0 && argsArray[iFirstBeginText - 4][1] === firstSetFontArg1) {
+ count++;
+ iFirst -= 5;
+ }
+
+ var iEndText = iFirst + 4;
+
+ for (var q = 1; q < count; q++) {
+ fnArray.splice(iEndText, 3);
+ argsArray.splice(iEndText, 3);
+ iEndText += 2;
+ }
+
+ return iEndText + 1;
+ });
+
+ function QueueOptimizer(queue) {
+ this.queue = queue;
+ this.state = null;
+ this.context = {
+ iCurr: 0,
+ fnArray: queue.fnArray,
+ argsArray: queue.argsArray
+ };
+ this.match = null;
+ this.lastProcessed = 0;
+ }
+
+ QueueOptimizer.prototype = {
+ _optimize: function _optimize() {
+ var fnArray = this.queue.fnArray;
+ var i = this.lastProcessed,
+ ii = fnArray.length;
+ var state = this.state;
+ var match = this.match;
+
+ if (!state && !match && i + 1 === ii && !InitialState[fnArray[i]]) {
+ this.lastProcessed = ii;
+ return;
+ }
+
+ var context = this.context;
+
+ while (i < ii) {
+ if (match) {
+ var iterate = (0, match.iterateFn)(context, i);
+
+ if (iterate) {
+ i++;
+ continue;
+ }
+
+ i = (0, match.processFn)(context, i + 1);
+ ii = fnArray.length;
+ match = null;
+ state = null;
+
+ if (i >= ii) {
+ break;
+ }
+ }
+
+ state = (state || InitialState)[fnArray[i]];
+
+ if (!state || Array.isArray(state)) {
+ i++;
+ continue;
+ }
+
+ context.iCurr = i;
+ i++;
+
+ if (state.checkFn && !(0, state.checkFn)(context)) {
+ state = null;
+ continue;
+ }
+
+ match = state;
+ state = null;
+ }
+
+ this.state = state;
+ this.match = match;
+ this.lastProcessed = i;
+ },
+ push: function push(fn, args) {
+ this.queue.fnArray.push(fn);
+ this.queue.argsArray.push(args);
+
+ this._optimize();
+ },
+ flush: function flush() {
+ while (this.match) {
+ var length = this.queue.fnArray.length;
+ this.lastProcessed = (0, this.match.processFn)(this.context, length);
+ this.match = null;
+ this.state = null;
+
+ this._optimize();
+ }
+ },
+ reset: function reset() {
+ this.state = null;
+ this.match = null;
+ this.lastProcessed = 0;
+ }
+ };
+ return QueueOptimizer;
+}();
+
+var NullOptimizer = function NullOptimizerClosure() {
+ function NullOptimizer(queue) {
+ this.queue = queue;
+ }
+
+ NullOptimizer.prototype = {
+ push: function push(fn, args) {
+ this.queue.fnArray.push(fn);
+ this.queue.argsArray.push(args);
+ },
+ flush: function flush() {},
+ reset: function reset() {}
+ };
+ return NullOptimizer;
+}();
+
+var OperatorList = function OperatorListClosure() {
+ var CHUNK_SIZE = 1000;
+ var CHUNK_SIZE_ABOUT = CHUNK_SIZE - 5;
+
+ function OperatorList(intent, messageHandler, pageIndex) {
+ this.messageHandler = messageHandler;
+ this.fnArray = [];
+ this.argsArray = [];
+
+ if (messageHandler && intent !== 'oplist') {
+ this.optimizer = new QueueOptimizer(this);
+ } else {
+ this.optimizer = new NullOptimizer(this);
+ }
+
+ this.dependencies = Object.create(null);
+ this._totalLength = 0;
+ this.pageIndex = pageIndex;
+ this.intent = intent;
+ this.weight = 0;
+ }
+
+ OperatorList.prototype = {
+ get length() {
+ return this.argsArray.length;
+ },
+
+ get totalLength() {
+ return this._totalLength + this.length;
+ },
+
+ addOp: function addOp(fn, args) {
+ this.optimizer.push(fn, args);
+ this.weight++;
+
+ if (this.messageHandler) {
+ if (this.weight >= CHUNK_SIZE) {
+ this.flush();
+ } else if (this.weight >= CHUNK_SIZE_ABOUT && (fn === _util.OPS.restore || fn === _util.OPS.endText)) {
+ this.flush();
+ }
+ }
+ },
+ addDependency: function addDependency(dependency) {
+ if (dependency in this.dependencies) {
+ return;
+ }
+
+ this.dependencies[dependency] = true;
+ this.addOp(_util.OPS.dependency, [dependency]);
+ },
+ addDependencies: function addDependencies(dependencies) {
+ for (var key in dependencies) {
+ this.addDependency(key);
+ }
+ },
+ addOpList: function addOpList(opList) {
+ Object.assign(this.dependencies, opList.dependencies);
+
+ for (var i = 0, ii = opList.length; i < ii; i++) {
+ this.addOp(opList.fnArray[i], opList.argsArray[i]);
+ }
+ },
+ getIR: function getIR() {
+ return {
+ fnArray: this.fnArray,
+ argsArray: this.argsArray,
+ length: this.length
+ };
+ },
+
+ get _transfers() {
+ var transfers = [];
+ var fnArray = this.fnArray,
+ argsArray = this.argsArray,
+ length = this.length;
+
+ for (var i = 0; i < length; i++) {
+ switch (fnArray[i]) {
+ case _util.OPS.paintInlineImageXObject:
+ case _util.OPS.paintInlineImageXObjectGroup:
+ case _util.OPS.paintImageMaskXObject:
+ var arg = argsArray[i][0];
+ ;
+
+ if (!arg.cached) {
+ transfers.push(arg.data.buffer);
+ }
+
+ break;
+ }
+ }
+
+ return transfers;
+ },
+
+ flush: function flush() {
+ var lastChunk = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ this.optimizer.flush();
+ var length = this.length;
+ this._totalLength += length;
+ this.messageHandler.send('RenderPageChunk', {
+ operatorList: {
+ fnArray: this.fnArray,
+ argsArray: this.argsArray,
+ lastChunk: lastChunk,
+ length: length
+ },
+ pageIndex: this.pageIndex,
+ intent: this.intent
+ }, this._transfers);
+ this.dependencies = Object.create(null);
+ this.fnArray.length = 0;
+ this.argsArray.length = 0;
+ this.weight = 0;
+ this.optimizer.reset();
+ }
+ };
+ return OperatorList;
+}();
+
+exports.OperatorList = OperatorList;
+
+/***/ }),
+/* 172 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.PartialEvaluator = void 0;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(2));
+
+var _util = __w_pdfjs_require__(5);
+
+var _cmap = __w_pdfjs_require__(173);
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _fonts = __w_pdfjs_require__(174);
+
+var _encodings = __w_pdfjs_require__(177);
+
+var _unicode = __w_pdfjs_require__(180);
+
+var _standard_fonts = __w_pdfjs_require__(179);
+
+var _pattern = __w_pdfjs_require__(183);
+
+var _parser = __w_pdfjs_require__(157);
+
+var _bidi = __w_pdfjs_require__(184);
+
+var _colorspace = __w_pdfjs_require__(169);
+
+var _stream = __w_pdfjs_require__(158);
+
+var _glyphlist = __w_pdfjs_require__(178);
+
+var _core_utils = __w_pdfjs_require__(154);
+
+var _metrics = __w_pdfjs_require__(185);
+
+var _function = __w_pdfjs_require__(186);
+
+var _jpeg_stream = __w_pdfjs_require__(164);
+
+var _murmurhash = __w_pdfjs_require__(188);
+
+var _image_utils = __w_pdfjs_require__(189);
+
+var _operator_list = __w_pdfjs_require__(171);
+
+var _image = __w_pdfjs_require__(190);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+var PartialEvaluator = function PartialEvaluatorClosure() {
+ var DefaultPartialEvaluatorOptions = {
+ forceDataSchema: false,
+ maxImageSize: -1,
+ disableFontFace: false,
+ nativeImageDecoderSupport: _util.NativeImageDecoding.DECODE,
+ ignoreErrors: false,
+ isEvalSupported: true
+ };
+
+ function PartialEvaluator(_ref) {
+ var _this = this;
+
+ var xref = _ref.xref,
+ handler = _ref.handler,
+ pageIndex = _ref.pageIndex,
+ idFactory = _ref.idFactory,
+ fontCache = _ref.fontCache,
+ builtInCMapCache = _ref.builtInCMapCache,
+ _ref$options = _ref.options,
+ options = _ref$options === void 0 ? null : _ref$options,
+ pdfFunctionFactory = _ref.pdfFunctionFactory;
+ this.xref = xref;
+ this.handler = handler;
+ this.pageIndex = pageIndex;
+ this.idFactory = idFactory;
+ this.fontCache = fontCache;
+ this.builtInCMapCache = builtInCMapCache;
+ this.options = options || DefaultPartialEvaluatorOptions;
+ this.pdfFunctionFactory = pdfFunctionFactory;
+ this.parsingType3Font = false;
+
+ this.fetchBuiltInCMap =
+ /*#__PURE__*/
+ function () {
+ var _ref2 = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee(name) {
+ var data;
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ if (!_this.builtInCMapCache.has(name)) {
+ _context.next = 2;
+ break;
+ }
+
+ return _context.abrupt("return", _this.builtInCMapCache.get(name));
+
+ case 2:
+ _context.next = 4;
+ return _this.handler.sendWithPromise('FetchBuiltInCMap', {
+ name: name
+ });
+
+ case 4:
+ data = _context.sent;
+
+ if (data.compressionType !== _util.CMapCompressionType.NONE) {
+ _this.builtInCMapCache.set(name, data);
+ }
+
+ return _context.abrupt("return", data);
+
+ case 7:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee);
+ }));
+
+ return function (_x) {
+ return _ref2.apply(this, arguments);
+ };
+ }();
+ }
+
+ var TIME_SLOT_DURATION_MS = 20;
+ var CHECK_TIME_EVERY = 100;
+
+ function TimeSlotManager() {
+ this.reset();
+ }
+
+ TimeSlotManager.prototype = {
+ check: function TimeSlotManager_check() {
+ if (++this.checked < CHECK_TIME_EVERY) {
+ return false;
+ }
+
+ this.checked = 0;
+ return this.endTime <= Date.now();
+ },
+ reset: function TimeSlotManager_reset() {
+ this.endTime = Date.now() + TIME_SLOT_DURATION_MS;
+ this.checked = 0;
+ }
+ };
+
+ function normalizeBlendMode(value) {
+ if (!(0, _primitives.isName)(value)) {
+ return 'source-over';
+ }
+
+ switch (value.name) {
+ case 'Normal':
+ case 'Compatible':
+ return 'source-over';
+
+ case 'Multiply':
+ return 'multiply';
+
+ case 'Screen':
+ return 'screen';
+
+ case 'Overlay':
+ return 'overlay';
+
+ case 'Darken':
+ return 'darken';
+
+ case 'Lighten':
+ return 'lighten';
+
+ case 'ColorDodge':
+ return 'color-dodge';
+
+ case 'ColorBurn':
+ return 'color-burn';
+
+ case 'HardLight':
+ return 'hard-light';
+
+ case 'SoftLight':
+ return 'soft-light';
+
+ case 'Difference':
+ return 'difference';
+
+ case 'Exclusion':
+ return 'exclusion';
+
+ case 'Hue':
+ return 'hue';
+
+ case 'Saturation':
+ return 'saturation';
+
+ case 'Color':
+ return 'color';
+
+ case 'Luminosity':
+ return 'luminosity';
+ }
+
+ (0, _util.warn)('Unsupported blend mode: ' + value.name);
+ return 'source-over';
+ }
+
+ var deferred = Promise.resolve();
+ var TILING_PATTERN = 1,
+ SHADING_PATTERN = 2;
+ PartialEvaluator.prototype = {
+ clone: function clone() {
+ var newOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : DefaultPartialEvaluatorOptions;
+ var newEvaluator = Object.create(this);
+ newEvaluator.options = newOptions;
+ return newEvaluator;
+ },
+ hasBlendModes: function PartialEvaluator_hasBlendModes(resources) {
+ if (!(0, _primitives.isDict)(resources)) {
+ return false;
+ }
+
+ var processed = Object.create(null);
+
+ if (resources.objId) {
+ processed[resources.objId] = true;
+ }
+
+ var nodes = [resources],
+ xref = this.xref;
+
+ while (nodes.length) {
+ var key, i, ii;
+ var node = nodes.shift();
+ var graphicStates = node.get('ExtGState');
+
+ if ((0, _primitives.isDict)(graphicStates)) {
+ var graphicStatesKeys = graphicStates.getKeys();
+
+ for (i = 0, ii = graphicStatesKeys.length; i < ii; i++) {
+ key = graphicStatesKeys[i];
+ var graphicState = graphicStates.get(key);
+ var bm = graphicState.get('BM');
+
+ if ((0, _primitives.isName)(bm) && bm.name !== 'Normal') {
+ return true;
+ }
+ }
+ }
+
+ var xObjects = node.get('XObject');
+
+ if (!(0, _primitives.isDict)(xObjects)) {
+ continue;
+ }
+
+ var xObjectsKeys = xObjects.getKeys();
+
+ for (i = 0, ii = xObjectsKeys.length; i < ii; i++) {
+ key = xObjectsKeys[i];
+ var xObject = xObjects.getRaw(key);
+
+ if ((0, _primitives.isRef)(xObject)) {
+ if (processed[xObject.toString()]) {
+ continue;
+ }
+
+ xObject = xref.fetch(xObject);
+ }
+
+ if (!(0, _primitives.isStream)(xObject)) {
+ continue;
+ }
+
+ if (xObject.dict.objId) {
+ if (processed[xObject.dict.objId]) {
+ continue;
+ }
+
+ processed[xObject.dict.objId] = true;
+ }
+
+ var xResources = xObject.dict.get('Resources');
+
+ if ((0, _primitives.isDict)(xResources) && (!xResources.objId || !processed[xResources.objId])) {
+ nodes.push(xResources);
+
+ if (xResources.objId) {
+ processed[xResources.objId] = true;
+ }
+ }
+ }
+ }
+
+ return false;
+ },
+ buildFormXObject: function PartialEvaluator_buildFormXObject(resources, xobj, smask, operatorList, task, initialState) {
+ var dict = xobj.dict;
+ var matrix = dict.getArray('Matrix');
+ var bbox = dict.getArray('BBox');
+
+ if (Array.isArray(bbox) && bbox.length === 4) {
+ bbox = _util.Util.normalizeRect(bbox);
+ } else {
+ bbox = null;
+ }
+
+ var group = dict.get('Group');
+
+ if (group) {
+ var groupOptions = {
+ matrix: matrix,
+ bbox: bbox,
+ smask: smask,
+ isolated: false,
+ knockout: false
+ };
+ var groupSubtype = group.get('S');
+ var colorSpace = null;
+
+ if ((0, _primitives.isName)(groupSubtype, 'Transparency')) {
+ groupOptions.isolated = group.get('I') || false;
+ groupOptions.knockout = group.get('K') || false;
+
+ if (group.has('CS')) {
+ colorSpace = _colorspace.ColorSpace.parse(group.get('CS'), this.xref, resources, this.pdfFunctionFactory);
+ }
+ }
+
+ if (smask && smask.backdrop) {
+ colorSpace = colorSpace || _colorspace.ColorSpace.singletons.rgb;
+ smask.backdrop = colorSpace.getRgb(smask.backdrop, 0);
+ }
+
+ operatorList.addOp(_util.OPS.beginGroup, [groupOptions]);
+ }
+
+ operatorList.addOp(_util.OPS.paintFormXObjectBegin, [matrix, bbox]);
+ return this.getOperatorList({
+ stream: xobj,
+ task: task,
+ resources: dict.get('Resources') || resources,
+ operatorList: operatorList,
+ initialState: initialState
+ }).then(function () {
+ operatorList.addOp(_util.OPS.paintFormXObjectEnd, []);
+
+ if (group) {
+ operatorList.addOp(_util.OPS.endGroup, [groupOptions]);
+ }
+ });
+ },
+ buildPaintImageXObject: function () {
+ var _buildPaintImageXObject = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee2(_ref3) {
+ var _this2 = this;
+
+ var resources, image, _ref3$isInline, isInline, operatorList, cacheKey, imageCache, _ref3$forceDisableNat, forceDisableNativeImageDecoder, dict, w, h, maxImageSize, imageMask, imgData, args, width, height, bitStrideLength, imgArray, decode, softMask, mask, SMALL_IMAGE_DIMENSIONS, imageObj, nativeImageDecoderSupport, objId, nativeImageDecoder, imgPromise;
+
+ return _regenerator["default"].wrap(function _callee2$(_context2) {
+ while (1) {
+ switch (_context2.prev = _context2.next) {
+ case 0:
+ resources = _ref3.resources, image = _ref3.image, _ref3$isInline = _ref3.isInline, isInline = _ref3$isInline === void 0 ? false : _ref3$isInline, operatorList = _ref3.operatorList, cacheKey = _ref3.cacheKey, imageCache = _ref3.imageCache, _ref3$forceDisableNat = _ref3.forceDisableNativeImageDecoder, forceDisableNativeImageDecoder = _ref3$forceDisableNat === void 0 ? false : _ref3$forceDisableNat;
+ dict = image.dict;
+ w = dict.get('Width', 'W');
+ h = dict.get('Height', 'H');
+
+ if (!(!(w && (0, _util.isNum)(w)) || !(h && (0, _util.isNum)(h)))) {
+ _context2.next = 7;
+ break;
+ }
+
+ (0, _util.warn)('Image dimensions are missing, or not numbers.');
+ return _context2.abrupt("return", undefined);
+
+ case 7:
+ maxImageSize = this.options.maxImageSize;
+
+ if (!(maxImageSize !== -1 && w * h > maxImageSize)) {
+ _context2.next = 11;
+ break;
+ }
+
+ (0, _util.warn)('Image exceeded maximum allowed size and was removed.');
+ return _context2.abrupt("return", undefined);
+
+ case 11:
+ imageMask = dict.get('ImageMask', 'IM') || false;
+
+ if (!imageMask) {
+ _context2.next = 24;
+ break;
+ }
+
+ width = dict.get('Width', 'W');
+ height = dict.get('Height', 'H');
+ bitStrideLength = width + 7 >> 3;
+ imgArray = image.getBytes(bitStrideLength * height, true);
+ decode = dict.getArray('Decode', 'D');
+ imgData = _image.PDFImage.createMask({
+ imgArray: imgArray,
+ width: width,
+ height: height,
+ imageIsFromDecodeStream: image instanceof _stream.DecodeStream,
+ inverseDecode: !!decode && decode[0] > 0
+ });
+ imgData.cached = !!cacheKey;
+ args = [imgData];
+ operatorList.addOp(_util.OPS.paintImageMaskXObject, args);
+
+ if (cacheKey) {
+ imageCache[cacheKey] = {
+ fn: _util.OPS.paintImageMaskXObject,
+ args: args
+ };
+ }
+
+ return _context2.abrupt("return", undefined);
+
+ case 24:
+ softMask = dict.get('SMask', 'SM') || false;
+ mask = dict.get('Mask') || false;
+ SMALL_IMAGE_DIMENSIONS = 200;
+
+ if (!(isInline && !softMask && !mask && !(image instanceof _jpeg_stream.JpegStream) && w + h < SMALL_IMAGE_DIMENSIONS)) {
+ _context2.next = 32;
+ break;
+ }
+
+ imageObj = new _image.PDFImage({
+ xref: this.xref,
+ res: resources,
+ image: image,
+ isInline: isInline,
+ pdfFunctionFactory: this.pdfFunctionFactory
+ });
+ imgData = imageObj.createImageData(true);
+ operatorList.addOp(_util.OPS.paintInlineImageXObject, [imgData]);
+ return _context2.abrupt("return", undefined);
+
+ case 32:
+ nativeImageDecoderSupport = forceDisableNativeImageDecoder ? _util.NativeImageDecoding.NONE : this.options.nativeImageDecoderSupport;
+ objId = "img_".concat(this.idFactory.createObjId());
+
+ if (this.parsingType3Font) {
+ (0, _util.assert)(nativeImageDecoderSupport === _util.NativeImageDecoding.NONE, 'Type3 image resources should be completely decoded in the worker.');
+ objId = "".concat(this.idFactory.getDocId(), "_type3res_").concat(objId);
+ }
+
+ if (!(nativeImageDecoderSupport !== _util.NativeImageDecoding.NONE && !softMask && !mask && image instanceof _jpeg_stream.JpegStream && _image_utils.NativeImageDecoder.isSupported(image, this.xref, resources, this.pdfFunctionFactory))) {
+ _context2.next = 37;
+ break;
+ }
+
+ return _context2.abrupt("return", this.handler.sendWithPromise('obj', [objId, this.pageIndex, 'JpegStream', image.getIR(this.options.forceDataSchema)]).then(function () {
+ operatorList.addDependency(objId);
+ args = [objId, w, h];
+ operatorList.addOp(_util.OPS.paintJpegXObject, args);
+
+ if (cacheKey) {
+ imageCache[cacheKey] = {
+ fn: _util.OPS.paintJpegXObject,
+ args: args
+ };
+ }
+ }, function (reason) {
+ (0, _util.warn)('Native JPEG decoding failed -- trying to recover: ' + (reason && reason.message));
+ return _this2.buildPaintImageXObject({
+ resources: resources,
+ image: image,
+ isInline: isInline,
+ operatorList: operatorList,
+ cacheKey: cacheKey,
+ imageCache: imageCache,
+ forceDisableNativeImageDecoder: true
+ });
+ }));
+
+ case 37:
+ nativeImageDecoder = null;
+
+ if (nativeImageDecoderSupport === _util.NativeImageDecoding.DECODE && (image instanceof _jpeg_stream.JpegStream || mask instanceof _jpeg_stream.JpegStream || softMask instanceof _jpeg_stream.JpegStream)) {
+ nativeImageDecoder = new _image_utils.NativeImageDecoder({
+ xref: this.xref,
+ resources: resources,
+ handler: this.handler,
+ forceDataSchema: this.options.forceDataSchema,
+ pdfFunctionFactory: this.pdfFunctionFactory
+ });
+ }
+
+ operatorList.addDependency(objId);
+ args = [objId, w, h];
+ imgPromise = _image.PDFImage.buildImage({
+ handler: this.handler,
+ xref: this.xref,
+ res: resources,
+ image: image,
+ isInline: isInline,
+ nativeDecoder: nativeImageDecoder,
+ pdfFunctionFactory: this.pdfFunctionFactory
+ }).then(function (imageObj) {
+ var imgData = imageObj.createImageData(false);
+
+ if (_this2.parsingType3Font) {
+ return _this2.handler.sendWithPromise('commonobj', [objId, 'FontType3Res', imgData], [imgData.data.buffer]);
+ }
+
+ _this2.handler.send('obj', [objId, _this2.pageIndex, 'Image', imgData], [imgData.data.buffer]);
+
+ return undefined;
+ })["catch"](function (reason) {
+ (0, _util.warn)('Unable to decode image: ' + reason);
+
+ if (_this2.parsingType3Font) {
+ return _this2.handler.sendWithPromise('commonobj', [objId, 'FontType3Res', null]);
+ }
+
+ _this2.handler.send('obj', [objId, _this2.pageIndex, 'Image', null]);
+
+ return undefined;
+ });
+
+ if (!this.parsingType3Font) {
+ _context2.next = 45;
+ break;
+ }
+
+ _context2.next = 45;
+ return imgPromise;
+
+ case 45:
+ operatorList.addOp(_util.OPS.paintImageXObject, args);
+
+ if (cacheKey) {
+ imageCache[cacheKey] = {
+ fn: _util.OPS.paintImageXObject,
+ args: args
+ };
+ }
+
+ return _context2.abrupt("return", undefined);
+
+ case 48:
+ case "end":
+ return _context2.stop();
+ }
+ }
+ }, _callee2, this);
+ }));
+
+ function buildPaintImageXObject(_x2) {
+ return _buildPaintImageXObject.apply(this, arguments);
+ }
+
+ return buildPaintImageXObject;
+ }(),
+ handleSMask: function PartialEvaluator_handleSmask(smask, resources, operatorList, task, stateManager) {
+ var smaskContent = smask.get('G');
+ var smaskOptions = {
+ subtype: smask.get('S').name,
+ backdrop: smask.get('BC')
+ };
+ var transferObj = smask.get('TR');
+
+ if ((0, _function.isPDFFunction)(transferObj)) {
+ var transferFn = this.pdfFunctionFactory.create(transferObj);
+ var transferMap = new Uint8Array(256);
+ var tmp = new Float32Array(1);
+
+ for (var i = 0; i < 256; i++) {
+ tmp[0] = i / 255;
+ transferFn(tmp, 0, tmp, 0);
+ transferMap[i] = tmp[0] * 255 | 0;
+ }
+
+ smaskOptions.transferMap = transferMap;
+ }
+
+ return this.buildFormXObject(resources, smaskContent, smaskOptions, operatorList, task, stateManager.state.clone());
+ },
+ handleTilingType: function handleTilingType(fn, args, resources, pattern, patternDict, operatorList, task) {
+ var _this3 = this;
+
+ var tilingOpList = new _operator_list.OperatorList();
+ var resourcesArray = [patternDict.get('Resources'), resources];
+
+ var patternResources = _primitives.Dict.merge(this.xref, resourcesArray);
+
+ return this.getOperatorList({
+ stream: pattern,
+ task: task,
+ resources: patternResources,
+ operatorList: tilingOpList
+ }).then(function () {
+ return (0, _pattern.getTilingPatternIR)({
+ fnArray: tilingOpList.fnArray,
+ argsArray: tilingOpList.argsArray
+ }, patternDict, args);
+ }).then(function (tilingPatternIR) {
+ operatorList.addDependencies(tilingOpList.dependencies);
+ operatorList.addOp(fn, tilingPatternIR);
+ }, function (reason) {
+ if (_this3.options.ignoreErrors) {
+ _this3.handler.send('UnsupportedFeature', {
+ featureId: _util.UNSUPPORTED_FEATURES.unknown
+ });
+
+ (0, _util.warn)("handleTilingType - ignoring pattern: \"".concat(reason, "\"."));
+ return;
+ }
+
+ throw reason;
+ });
+ },
+ handleSetFont: function PartialEvaluator_handleSetFont(resources, fontArgs, fontRef, operatorList, task, state) {
+ var _this4 = this;
+
+ var fontName;
+
+ if (fontArgs) {
+ fontArgs = fontArgs.slice();
+ fontName = fontArgs[0].name;
+ }
+
+ return this.loadFont(fontName, fontRef, resources).then(function (translated) {
+ if (!translated.font.isType3Font) {
+ return translated;
+ }
+
+ return translated.loadType3Data(_this4, resources, operatorList, task).then(function () {
+ return translated;
+ })["catch"](function (reason) {
+ _this4.handler.send('UnsupportedFeature', {
+ featureId: _util.UNSUPPORTED_FEATURES.font
+ });
+
+ return new TranslatedFont('g_font_error', new _fonts.ErrorFont('Type3 font load error: ' + reason), translated.font);
+ });
+ }).then(function (translated) {
+ state.font = translated.font;
+ translated.send(_this4.handler);
+ return translated.loadedName;
+ });
+ },
+ handleText: function handleText(chars, state) {
+ var font = state.font;
+ var glyphs = font.charsToGlyphs(chars);
+
+ if (font.data) {
+ var isAddToPathSet = !!(state.textRenderingMode & _util.TextRenderingMode.ADD_TO_PATH_FLAG);
+
+ if (isAddToPathSet || state.fillColorSpace.name === 'Pattern' || font.disableFontFace || this.options.disableFontFace) {
+ PartialEvaluator.buildFontPaths(font, glyphs, this.handler);
+ }
+ }
+
+ return glyphs;
+ },
+ setGState: function PartialEvaluator_setGState(resources, gState, operatorList, task, stateManager) {
+ var _this5 = this;
+
+ var gStateObj = [];
+ var gStateKeys = gState.getKeys();
+ var promise = Promise.resolve();
+
+ var _loop = function _loop() {
+ var key = gStateKeys[i];
+ var value = gState.get(key);
+
+ switch (key) {
+ case 'Type':
+ break;
+
+ case 'LW':
+ case 'LC':
+ case 'LJ':
+ case 'ML':
+ case 'D':
+ case 'RI':
+ case 'FL':
+ case 'CA':
+ case 'ca':
+ gStateObj.push([key, value]);
+ break;
+
+ case 'Font':
+ promise = promise.then(function () {
+ return _this5.handleSetFont(resources, null, value[0], operatorList, task, stateManager.state).then(function (loadedName) {
+ operatorList.addDependency(loadedName);
+ gStateObj.push([key, [loadedName, value[1]]]);
+ });
+ });
+ break;
+
+ case 'BM':
+ gStateObj.push([key, normalizeBlendMode(value)]);
+ break;
+
+ case 'SMask':
+ if ((0, _primitives.isName)(value, 'None')) {
+ gStateObj.push([key, false]);
+ break;
+ }
+
+ if ((0, _primitives.isDict)(value)) {
+ promise = promise.then(function () {
+ return _this5.handleSMask(value, resources, operatorList, task, stateManager);
+ });
+ gStateObj.push([key, true]);
+ } else {
+ (0, _util.warn)('Unsupported SMask type');
+ }
+
+ break;
+
+ case 'OP':
+ case 'op':
+ case 'OPM':
+ case 'BG':
+ case 'BG2':
+ case 'UCR':
+ case 'UCR2':
+ case 'TR':
+ case 'TR2':
+ case 'HT':
+ case 'SM':
+ case 'SA':
+ case 'AIS':
+ case 'TK':
+ (0, _util.info)('graphic state operator ' + key);
+ break;
+
+ default:
+ (0, _util.info)('Unknown graphic state operator ' + key);
+ break;
+ }
+ };
+
+ for (var i = 0, ii = gStateKeys.length; i < ii; i++) {
+ _loop();
+ }
+
+ return promise.then(function () {
+ if (gStateObj.length > 0) {
+ operatorList.addOp(_util.OPS.setGState, [gStateObj]);
+ }
+ });
+ },
+ loadFont: function PartialEvaluator_loadFont(fontName, font, resources) {
+ var _this6 = this;
+
+ function errorFont() {
+ return Promise.resolve(new TranslatedFont('g_font_error', new _fonts.ErrorFont('Font ' + fontName + ' is not available'), font));
+ }
+
+ var fontRef,
+ xref = this.xref;
+
+ if (font) {
+ if (!(0, _primitives.isRef)(font)) {
+ throw new Error('The "font" object should be a reference.');
+ }
+
+ fontRef = font;
+ } else {
+ var fontRes = resources.get('Font');
+
+ if (fontRes) {
+ fontRef = fontRes.getRaw(fontName);
+ } else {
+ (0, _util.warn)('fontRes not available');
+ return errorFont();
+ }
+ }
+
+ if (!fontRef) {
+ (0, _util.warn)('fontRef not available');
+ return errorFont();
+ }
+
+ if (this.fontCache.has(fontRef)) {
+ return this.fontCache.get(fontRef);
+ }
+
+ font = xref.fetchIfRef(fontRef);
+
+ if (!(0, _primitives.isDict)(font)) {
+ return errorFont();
+ }
+
+ if (font.translated) {
+ return font.translated;
+ }
+
+ var fontCapability = (0, _util.createPromiseCapability)();
+ var preEvaluatedFont = this.preEvaluateFont(font);
+ var descriptor = preEvaluatedFont.descriptor,
+ hash = preEvaluatedFont.hash;
+ var fontRefIsRef = (0, _primitives.isRef)(fontRef),
+ fontID;
+
+ if (fontRefIsRef) {
+ fontID = fontRef.toString();
+ }
+
+ if (hash && (0, _primitives.isDict)(descriptor)) {
+ if (!descriptor.fontAliases) {
+ descriptor.fontAliases = Object.create(null);
+ }
+
+ var fontAliases = descriptor.fontAliases;
+
+ if (fontAliases[hash]) {
+ var aliasFontRef = fontAliases[hash].aliasRef;
+
+ if (fontRefIsRef && aliasFontRef && this.fontCache.has(aliasFontRef)) {
+ this.fontCache.putAlias(fontRef, aliasFontRef);
+ return this.fontCache.get(fontRef);
+ }
+ } else {
+ fontAliases[hash] = {
+ fontID: _fonts.Font.getFontID()
+ };
+ }
+
+ if (fontRefIsRef) {
+ fontAliases[hash].aliasRef = fontRef;
+ }
+
+ fontID = fontAliases[hash].fontID;
+ }
+
+ if (fontRefIsRef) {
+ this.fontCache.put(fontRef, fontCapability.promise);
+ } else {
+ if (!fontID) {
+ fontID = this.idFactory.createObjId();
+ }
+
+ this.fontCache.put("id_".concat(fontID), fontCapability.promise);
+ }
+
+ (0, _util.assert)(fontID, 'The "fontID" must be defined.');
+ font.loadedName = "".concat(this.idFactory.getDocId(), "_f").concat(fontID);
+ font.translated = fontCapability.promise;
+ var translatedPromise;
+
+ try {
+ translatedPromise = this.translateFont(preEvaluatedFont);
+ } catch (e) {
+ translatedPromise = Promise.reject(e);
+ }
+
+ translatedPromise.then(function (translatedFont) {
+ if (translatedFont.fontType !== undefined) {
+ var xrefFontStats = xref.stats.fontTypes;
+ xrefFontStats[translatedFont.fontType] = true;
+ }
+
+ fontCapability.resolve(new TranslatedFont(font.loadedName, translatedFont, font));
+ })["catch"](function (reason) {
+ _this6.handler.send('UnsupportedFeature', {
+ featureId: _util.UNSUPPORTED_FEATURES.font
+ });
+
+ try {
+ var fontFile3 = descriptor && descriptor.get('FontFile3');
+ var subtype = fontFile3 && fontFile3.get('Subtype');
+ var fontType = (0, _fonts.getFontType)(preEvaluatedFont.type, subtype && subtype.name);
+ var xrefFontStats = xref.stats.fontTypes;
+ xrefFontStats[fontType] = true;
+ } catch (ex) {}
+
+ fontCapability.resolve(new TranslatedFont(font.loadedName, new _fonts.ErrorFont(reason instanceof Error ? reason.message : reason), font));
+ });
+ return fontCapability.promise;
+ },
+ buildPath: function buildPath(operatorList, fn, args) {
+ var parsingText = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
+ var lastIndex = operatorList.length - 1;
+
+ if (!args) {
+ args = [];
+ }
+
+ if (lastIndex < 0 || operatorList.fnArray[lastIndex] !== _util.OPS.constructPath) {
+ if (parsingText) {
+ (0, _util.warn)("Encountered path operator \"".concat(fn, "\" inside of a text object."));
+ operatorList.addOp(_util.OPS.save, null);
+ }
+
+ operatorList.addOp(_util.OPS.constructPath, [[fn], args]);
+
+ if (parsingText) {
+ operatorList.addOp(_util.OPS.restore, null);
+ }
+ } else {
+ var opArgs = operatorList.argsArray[lastIndex];
+ opArgs[0].push(fn);
+ Array.prototype.push.apply(opArgs[1], args);
+ }
+ },
+ handleColorN: function () {
+ var _handleColorN = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee3(operatorList, fn, args, cs, patterns, resources, task) {
+ var patternName, pattern, dict, typeNum, color, shading, matrix;
+ return _regenerator["default"].wrap(function _callee3$(_context3) {
+ while (1) {
+ switch (_context3.prev = _context3.next) {
+ case 0:
+ patternName = args[args.length - 1];
+
+ if (!((0, _primitives.isName)(patternName) && (pattern = patterns.get(patternName.name)))) {
+ _context3.next = 16;
+ break;
+ }
+
+ dict = (0, _primitives.isStream)(pattern) ? pattern.dict : pattern;
+ typeNum = dict.get('PatternType');
+
+ if (!(typeNum === TILING_PATTERN)) {
+ _context3.next = 9;
+ break;
+ }
+
+ color = cs.base ? cs.base.getRgb(args, 0) : null;
+ return _context3.abrupt("return", this.handleTilingType(fn, color, resources, pattern, dict, operatorList, task));
+
+ case 9:
+ if (!(typeNum === SHADING_PATTERN)) {
+ _context3.next = 15;
+ break;
+ }
+
+ shading = dict.get('Shading');
+ matrix = dict.getArray('Matrix');
+ pattern = _pattern.Pattern.parseShading(shading, matrix, this.xref, resources, this.handler, this.pdfFunctionFactory);
+ operatorList.addOp(fn, pattern.getIR());
+ return _context3.abrupt("return", undefined);
+
+ case 15:
+ throw new _util.FormatError("Unknown PatternType: ".concat(typeNum));
+
+ case 16:
+ throw new _util.FormatError("Unknown PatternName: ".concat(patternName));
+
+ case 17:
+ case "end":
+ return _context3.stop();
+ }
+ }
+ }, _callee3, this);
+ }));
+
+ function handleColorN(_x3, _x4, _x5, _x6, _x7, _x8, _x9) {
+ return _handleColorN.apply(this, arguments);
+ }
+
+ return handleColorN;
+ }(),
+ getOperatorList: function getOperatorList(_ref4) {
+ var _this7 = this;
+
+ var stream = _ref4.stream,
+ task = _ref4.task,
+ resources = _ref4.resources,
+ operatorList = _ref4.operatorList,
+ _ref4$initialState = _ref4.initialState,
+ initialState = _ref4$initialState === void 0 ? null : _ref4$initialState;
+ resources = resources || _primitives.Dict.empty;
+ initialState = initialState || new EvalState();
+
+ if (!operatorList) {
+ throw new Error('getOperatorList: missing "operatorList" parameter');
+ }
+
+ var self = this;
+ var xref = this.xref;
+ var parsingText = false;
+ var imageCache = Object.create(null);
+
+ var xobjs = resources.get('XObject') || _primitives.Dict.empty;
+
+ var patterns = resources.get('Pattern') || _primitives.Dict.empty;
+
+ var stateManager = new StateManager(initialState);
+ var preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager);
+ var timeSlotManager = new TimeSlotManager();
+
+ function closePendingRestoreOPS(argument) {
+ for (var i = 0, ii = preprocessor.savedStatesDepth; i < ii; i++) {
+ operatorList.addOp(_util.OPS.restore, []);
+ }
+ }
+
+ return new Promise(function promiseBody(resolve, reject) {
+ var next = function next(promise) {
+ promise.then(function () {
+ try {
+ promiseBody(resolve, reject);
+ } catch (ex) {
+ reject(ex);
+ }
+ }, reject);
+ };
+
+ task.ensureNotTerminated();
+ timeSlotManager.reset();
+ var stop,
+ operation = {},
+ i,
+ ii,
+ cs;
+
+ while (!(stop = timeSlotManager.check())) {
+ operation.args = null;
+
+ if (!preprocessor.read(operation)) {
+ break;
+ }
+
+ var args = operation.args;
+ var fn = operation.fn;
+
+ switch (fn | 0) {
+ case _util.OPS.paintXObject:
+ var name = args[0].name;
+
+ if (name && imageCache[name] !== undefined) {
+ operatorList.addOp(imageCache[name].fn, imageCache[name].args);
+ args = null;
+ continue;
+ }
+
+ next(new Promise(function (resolveXObject, rejectXObject) {
+ if (!name) {
+ throw new _util.FormatError('XObject must be referred to by name.');
+ }
+
+ var xobj = xobjs.get(name);
+
+ if (!xobj) {
+ operatorList.addOp(fn, args);
+ resolveXObject();
+ return;
+ }
+
+ if (!(0, _primitives.isStream)(xobj)) {
+ throw new _util.FormatError('XObject should be a stream');
+ }
+
+ var type = xobj.dict.get('Subtype');
+
+ if (!(0, _primitives.isName)(type)) {
+ throw new _util.FormatError('XObject should have a Name subtype');
+ }
+
+ if (type.name === 'Form') {
+ stateManager.save();
+ self.buildFormXObject(resources, xobj, null, operatorList, task, stateManager.state.clone()).then(function () {
+ stateManager.restore();
+ resolveXObject();
+ }, rejectXObject);
+ return;
+ } else if (type.name === 'Image') {
+ self.buildPaintImageXObject({
+ resources: resources,
+ image: xobj,
+ operatorList: operatorList,
+ cacheKey: name,
+ imageCache: imageCache
+ }).then(resolveXObject, rejectXObject);
+ return;
+ } else if (type.name === 'PS') {
+ (0, _util.info)('Ignored XObject subtype PS');
+ } else {
+ throw new _util.FormatError("Unhandled XObject subtype ".concat(type.name));
+ }
+
+ resolveXObject();
+ })["catch"](function (reason) {
+ if (self.options.ignoreErrors) {
+ self.handler.send('UnsupportedFeature', {
+ featureId: _util.UNSUPPORTED_FEATURES.unknown
+ });
+ (0, _util.warn)("getOperatorList - ignoring XObject: \"".concat(reason, "\"."));
+ return;
+ }
+
+ throw reason;
+ }));
+ return;
+
+ case _util.OPS.setFont:
+ var fontSize = args[1];
+ next(self.handleSetFont(resources, args, null, operatorList, task, stateManager.state).then(function (loadedName) {
+ operatorList.addDependency(loadedName);
+ operatorList.addOp(_util.OPS.setFont, [loadedName, fontSize]);
+ }));
+ return;
+
+ case _util.OPS.beginText:
+ parsingText = true;
+ break;
+
+ case _util.OPS.endText:
+ parsingText = false;
+ break;
+
+ case _util.OPS.endInlineImage:
+ var cacheKey = args[0].cacheKey;
+
+ if (cacheKey) {
+ var cacheEntry = imageCache[cacheKey];
+
+ if (cacheEntry !== undefined) {
+ operatorList.addOp(cacheEntry.fn, cacheEntry.args);
+ args = null;
+ continue;
+ }
+ }
+
+ next(self.buildPaintImageXObject({
+ resources: resources,
+ image: args[0],
+ isInline: true,
+ operatorList: operatorList,
+ cacheKey: cacheKey,
+ imageCache: imageCache
+ }));
+ return;
+
+ case _util.OPS.showText:
+ args[0] = self.handleText(args[0], stateManager.state);
+ break;
+
+ case _util.OPS.showSpacedText:
+ var arr = args[0];
+ var combinedGlyphs = [];
+ var arrLength = arr.length;
+ var state = stateManager.state;
+
+ for (i = 0; i < arrLength; ++i) {
+ var arrItem = arr[i];
+
+ if ((0, _util.isString)(arrItem)) {
+ Array.prototype.push.apply(combinedGlyphs, self.handleText(arrItem, state));
+ } else if ((0, _util.isNum)(arrItem)) {
+ combinedGlyphs.push(arrItem);
+ }
+ }
+
+ args[0] = combinedGlyphs;
+ fn = _util.OPS.showText;
+ break;
+
+ case _util.OPS.nextLineShowText:
+ operatorList.addOp(_util.OPS.nextLine);
+ args[0] = self.handleText(args[0], stateManager.state);
+ fn = _util.OPS.showText;
+ break;
+
+ case _util.OPS.nextLineSetSpacingShowText:
+ operatorList.addOp(_util.OPS.nextLine);
+ operatorList.addOp(_util.OPS.setWordSpacing, [args.shift()]);
+ operatorList.addOp(_util.OPS.setCharSpacing, [args.shift()]);
+ args[0] = self.handleText(args[0], stateManager.state);
+ fn = _util.OPS.showText;
+ break;
+
+ case _util.OPS.setTextRenderingMode:
+ stateManager.state.textRenderingMode = args[0];
+ break;
+
+ case _util.OPS.setFillColorSpace:
+ stateManager.state.fillColorSpace = _colorspace.ColorSpace.parse(args[0], xref, resources, self.pdfFunctionFactory);
+ continue;
+
+ case _util.OPS.setStrokeColorSpace:
+ stateManager.state.strokeColorSpace = _colorspace.ColorSpace.parse(args[0], xref, resources, self.pdfFunctionFactory);
+ continue;
+
+ case _util.OPS.setFillColor:
+ cs = stateManager.state.fillColorSpace;
+ args = cs.getRgb(args, 0);
+ fn = _util.OPS.setFillRGBColor;
+ break;
+
+ case _util.OPS.setStrokeColor:
+ cs = stateManager.state.strokeColorSpace;
+ args = cs.getRgb(args, 0);
+ fn = _util.OPS.setStrokeRGBColor;
+ break;
+
+ case _util.OPS.setFillGray:
+ stateManager.state.fillColorSpace = _colorspace.ColorSpace.singletons.gray;
+ args = _colorspace.ColorSpace.singletons.gray.getRgb(args, 0);
+ fn = _util.OPS.setFillRGBColor;
+ break;
+
+ case _util.OPS.setStrokeGray:
+ stateManager.state.strokeColorSpace = _colorspace.ColorSpace.singletons.gray;
+ args = _colorspace.ColorSpace.singletons.gray.getRgb(args, 0);
+ fn = _util.OPS.setStrokeRGBColor;
+ break;
+
+ case _util.OPS.setFillCMYKColor:
+ stateManager.state.fillColorSpace = _colorspace.ColorSpace.singletons.cmyk;
+ args = _colorspace.ColorSpace.singletons.cmyk.getRgb(args, 0);
+ fn = _util.OPS.setFillRGBColor;
+ break;
+
+ case _util.OPS.setStrokeCMYKColor:
+ stateManager.state.strokeColorSpace = _colorspace.ColorSpace.singletons.cmyk;
+ args = _colorspace.ColorSpace.singletons.cmyk.getRgb(args, 0);
+ fn = _util.OPS.setStrokeRGBColor;
+ break;
+
+ case _util.OPS.setFillRGBColor:
+ stateManager.state.fillColorSpace = _colorspace.ColorSpace.singletons.rgb;
+ args = _colorspace.ColorSpace.singletons.rgb.getRgb(args, 0);
+ break;
+
+ case _util.OPS.setStrokeRGBColor:
+ stateManager.state.strokeColorSpace = _colorspace.ColorSpace.singletons.rgb;
+ args = _colorspace.ColorSpace.singletons.rgb.getRgb(args, 0);
+ break;
+
+ case _util.OPS.setFillColorN:
+ cs = stateManager.state.fillColorSpace;
+
+ if (cs.name === 'Pattern') {
+ next(self.handleColorN(operatorList, _util.OPS.setFillColorN, args, cs, patterns, resources, task));
+ return;
+ }
+
+ args = cs.getRgb(args, 0);
+ fn = _util.OPS.setFillRGBColor;
+ break;
+
+ case _util.OPS.setStrokeColorN:
+ cs = stateManager.state.strokeColorSpace;
+
+ if (cs.name === 'Pattern') {
+ next(self.handleColorN(operatorList, _util.OPS.setStrokeColorN, args, cs, patterns, resources, task));
+ return;
+ }
+
+ args = cs.getRgb(args, 0);
+ fn = _util.OPS.setStrokeRGBColor;
+ break;
+
+ case _util.OPS.shadingFill:
+ var shadingRes = resources.get('Shading');
+
+ if (!shadingRes) {
+ throw new _util.FormatError('No shading resource found');
+ }
+
+ var shading = shadingRes.get(args[0].name);
+
+ if (!shading) {
+ throw new _util.FormatError('No shading object found');
+ }
+
+ var shadingFill = _pattern.Pattern.parseShading(shading, null, xref, resources, self.handler, self.pdfFunctionFactory);
+
+ var patternIR = shadingFill.getIR();
+ args = [patternIR];
+ fn = _util.OPS.shadingFill;
+ break;
+
+ case _util.OPS.setGState:
+ var dictName = args[0];
+ var extGState = resources.get('ExtGState');
+
+ if (!(0, _primitives.isDict)(extGState) || !extGState.has(dictName.name)) {
+ break;
+ }
+
+ var gState = extGState.get(dictName.name);
+ next(self.setGState(resources, gState, operatorList, task, stateManager));
+ return;
+
+ case _util.OPS.moveTo:
+ case _util.OPS.lineTo:
+ case _util.OPS.curveTo:
+ case _util.OPS.curveTo2:
+ case _util.OPS.curveTo3:
+ case _util.OPS.closePath:
+ case _util.OPS.rectangle:
+ self.buildPath(operatorList, fn, args, parsingText);
+ continue;
+
+ case _util.OPS.markPoint:
+ case _util.OPS.markPointProps:
+ case _util.OPS.beginMarkedContent:
+ case _util.OPS.beginMarkedContentProps:
+ case _util.OPS.endMarkedContent:
+ case _util.OPS.beginCompat:
+ case _util.OPS.endCompat:
+ continue;
+
+ default:
+ if (args !== null) {
+ for (i = 0, ii = args.length; i < ii; i++) {
+ if (args[i] instanceof _primitives.Dict) {
+ break;
+ }
+ }
+
+ if (i < ii) {
+ (0, _util.warn)('getOperatorList - ignoring operator: ' + fn);
+ continue;
+ }
+ }
+
+ }
+
+ operatorList.addOp(fn, args);
+ }
+
+ if (stop) {
+ next(deferred);
+ return;
+ }
+
+ closePendingRestoreOPS();
+ resolve();
+ })["catch"](function (reason) {
+ if (_this7.options.ignoreErrors) {
+ _this7.handler.send('UnsupportedFeature', {
+ featureId: _util.UNSUPPORTED_FEATURES.unknown
+ });
+
+ (0, _util.warn)("getOperatorList - ignoring errors during \"".concat(task.name, "\" ") + "task: \"".concat(reason, "\"."));
+ closePendingRestoreOPS();
+ return;
+ }
+
+ throw reason;
+ });
+ },
+ getTextContent: function getTextContent(_ref5) {
+ var _this8 = this;
+
+ var stream = _ref5.stream,
+ task = _ref5.task,
+ resources = _ref5.resources,
+ _ref5$stateManager = _ref5.stateManager,
+ stateManager = _ref5$stateManager === void 0 ? null : _ref5$stateManager,
+ _ref5$normalizeWhites = _ref5.normalizeWhitespace,
+ normalizeWhitespace = _ref5$normalizeWhites === void 0 ? false : _ref5$normalizeWhites,
+ _ref5$combineTextItem = _ref5.combineTextItems,
+ combineTextItems = _ref5$combineTextItem === void 0 ? false : _ref5$combineTextItem,
+ sink = _ref5.sink,
+ _ref5$seenStyles = _ref5.seenStyles,
+ seenStyles = _ref5$seenStyles === void 0 ? Object.create(null) : _ref5$seenStyles;
+ resources = resources || _primitives.Dict.empty;
+ stateManager = stateManager || new StateManager(new TextState());
+ var WhitespaceRegexp = /\s/g;
+ var textContent = {
+ items: [],
+ styles: Object.create(null)
+ };
+ var textContentItem = {
+ initialized: false,
+ str: [],
+ width: 0,
+ height: 0,
+ vertical: false,
+ lastAdvanceWidth: 0,
+ lastAdvanceHeight: 0,
+ textAdvanceScale: 0,
+ spaceWidth: 0,
+ fakeSpaceMin: Infinity,
+ fakeMultiSpaceMin: Infinity,
+ fakeMultiSpaceMax: -0,
+ textRunBreakAllowed: false,
+ transform: null,
+ fontName: null
+ };
+ var SPACE_FACTOR = 0.3;
+ var MULTI_SPACE_FACTOR = 1.5;
+ var MULTI_SPACE_FACTOR_MAX = 4;
+ var self = this;
+ var xref = this.xref;
+ var xobjs = null;
+ var skipEmptyXObjs = Object.create(null);
+ var preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager);
+ var textState;
+
+ function ensureTextContentItem() {
+ if (textContentItem.initialized) {
+ return textContentItem;
+ }
+
+ var font = textState.font;
+
+ if (!(font.loadedName in seenStyles)) {
+ seenStyles[font.loadedName] = true;
+ textContent.styles[font.loadedName] = {
+ fontFamily: font.fallbackName,
+ ascent: font.ascent,
+ descent: font.descent,
+ vertical: !!font.vertical
+ };
+ }
+
+ textContentItem.fontName = font.loadedName;
+ var tsm = [textState.fontSize * textState.textHScale, 0, 0, textState.fontSize, 0, textState.textRise];
+
+ if (font.isType3Font && textState.fontSize <= 1 && !(0, _util.isArrayEqual)(textState.fontMatrix, _util.FONT_IDENTITY_MATRIX)) {
+ var glyphHeight = font.bbox[3] - font.bbox[1];
+
+ if (glyphHeight > 0) {
+ tsm[3] *= glyphHeight * textState.fontMatrix[3];
+ }
+ }
+
+ var trm = _util.Util.transform(textState.ctm, _util.Util.transform(textState.textMatrix, tsm));
+
+ textContentItem.transform = trm;
+
+ if (!font.vertical) {
+ textContentItem.width = 0;
+ textContentItem.height = Math.sqrt(trm[2] * trm[2] + trm[3] * trm[3]);
+ textContentItem.vertical = false;
+ } else {
+ textContentItem.width = Math.sqrt(trm[0] * trm[0] + trm[1] * trm[1]);
+ textContentItem.height = 0;
+ textContentItem.vertical = true;
+ }
+
+ var a = textState.textLineMatrix[0];
+ var b = textState.textLineMatrix[1];
+ var scaleLineX = Math.sqrt(a * a + b * b);
+ a = textState.ctm[0];
+ b = textState.ctm[1];
+ var scaleCtmX = Math.sqrt(a * a + b * b);
+ textContentItem.textAdvanceScale = scaleCtmX * scaleLineX;
+ textContentItem.lastAdvanceWidth = 0;
+ textContentItem.lastAdvanceHeight = 0;
+ var spaceWidth = font.spaceWidth / 1000 * textState.fontSize;
+
+ if (spaceWidth) {
+ textContentItem.spaceWidth = spaceWidth;
+ textContentItem.fakeSpaceMin = spaceWidth * SPACE_FACTOR;
+ textContentItem.fakeMultiSpaceMin = spaceWidth * MULTI_SPACE_FACTOR;
+ textContentItem.fakeMultiSpaceMax = spaceWidth * MULTI_SPACE_FACTOR_MAX;
+ textContentItem.textRunBreakAllowed = !font.isMonospace;
+ } else {
+ textContentItem.spaceWidth = 0;
+ textContentItem.fakeSpaceMin = Infinity;
+ textContentItem.fakeMultiSpaceMin = Infinity;
+ textContentItem.fakeMultiSpaceMax = 0;
+ textContentItem.textRunBreakAllowed = false;
+ }
+
+ textContentItem.initialized = true;
+ return textContentItem;
+ }
+
+ function replaceWhitespace(str) {
+ var i = 0,
+ ii = str.length,
+ code;
+
+ while (i < ii && (code = str.charCodeAt(i)) >= 0x20 && code <= 0x7F) {
+ i++;
+ }
+
+ return i < ii ? str.replace(WhitespaceRegexp, ' ') : str;
+ }
+
+ function runBidiTransform(textChunk) {
+ var str = textChunk.str.join('');
+ var bidiResult = (0, _bidi.bidi)(str, -1, textChunk.vertical);
+ return {
+ str: normalizeWhitespace ? replaceWhitespace(bidiResult.str) : bidiResult.str,
+ dir: bidiResult.dir,
+ width: textChunk.width,
+ height: textChunk.height,
+ transform: textChunk.transform,
+ fontName: textChunk.fontName
+ };
+ }
+
+ function handleSetFont(fontName, fontRef) {
+ return self.loadFont(fontName, fontRef, resources).then(function (translated) {
+ textState.font = translated.font;
+ textState.fontMatrix = translated.font.fontMatrix || _util.FONT_IDENTITY_MATRIX;
+ });
+ }
+
+ function buildTextContentItem(chars) {
+ var font = textState.font;
+ var textChunk = ensureTextContentItem();
+ var width = 0;
+ var height = 0;
+ var glyphs = font.charsToGlyphs(chars);
+
+ for (var i = 0; i < glyphs.length; i++) {
+ var glyph = glyphs[i];
+ var glyphWidth = null;
+
+ if (font.vertical && glyph.vmetric) {
+ glyphWidth = glyph.vmetric[0];
+ } else {
+ glyphWidth = glyph.width;
+ }
+
+ var glyphUnicode = glyph.unicode;
+ var NormalizedUnicodes = (0, _unicode.getNormalizedUnicodes)();
+
+ if (NormalizedUnicodes[glyphUnicode] !== undefined) {
+ glyphUnicode = NormalizedUnicodes[glyphUnicode];
+ }
+
+ glyphUnicode = (0, _unicode.reverseIfRtl)(glyphUnicode);
+ var charSpacing = textState.charSpacing;
+
+ if (glyph.isSpace) {
+ var wordSpacing = textState.wordSpacing;
+ charSpacing += wordSpacing;
+
+ if (wordSpacing > 0) {
+ addFakeSpaces(wordSpacing, textChunk.str);
+ }
+ }
+
+ var tx = 0;
+ var ty = 0;
+
+ if (!font.vertical) {
+ var w0 = glyphWidth * textState.fontMatrix[0];
+ tx = (w0 * textState.fontSize + charSpacing) * textState.textHScale;
+ width += tx;
+ } else {
+ var w1 = glyphWidth * textState.fontMatrix[0];
+ ty = w1 * textState.fontSize + charSpacing;
+ height += ty;
+ }
+
+ textState.translateTextMatrix(tx, ty);
+ textChunk.str.push(glyphUnicode);
+ }
+
+ if (!font.vertical) {
+ textChunk.lastAdvanceWidth = width;
+ textChunk.width += width;
+ } else {
+ textChunk.lastAdvanceHeight = height;
+ textChunk.height += Math.abs(height);
+ }
+
+ return textChunk;
+ }
+
+ function addFakeSpaces(width, strBuf) {
+ if (width < textContentItem.fakeSpaceMin) {
+ return;
+ }
+
+ if (width < textContentItem.fakeMultiSpaceMin) {
+ strBuf.push(' ');
+ return;
+ }
+
+ var fakeSpaces = Math.round(width / textContentItem.spaceWidth);
+
+ while (fakeSpaces-- > 0) {
+ strBuf.push(' ');
+ }
+ }
+
+ function flushTextContentItem() {
+ if (!textContentItem.initialized) {
+ return;
+ }
+
+ if (!textContentItem.vertical) {
+ textContentItem.width *= textContentItem.textAdvanceScale;
+ } else {
+ textContentItem.height *= textContentItem.textAdvanceScale;
+ }
+
+ textContent.items.push(runBidiTransform(textContentItem));
+ textContentItem.initialized = false;
+ textContentItem.str.length = 0;
+ }
+
+ function enqueueChunk() {
+ var length = textContent.items.length;
+
+ if (length > 0) {
+ sink.enqueue(textContent, length);
+ textContent.items = [];
+ textContent.styles = Object.create(null);
+ }
+ }
+
+ var timeSlotManager = new TimeSlotManager();
+ return new Promise(function promiseBody(resolve, reject) {
+ var next = function next(promise) {
+ enqueueChunk();
+ Promise.all([promise, sink.ready]).then(function () {
+ try {
+ promiseBody(resolve, reject);
+ } catch (ex) {
+ reject(ex);
+ }
+ }, reject);
+ };
+
+ task.ensureNotTerminated();
+ timeSlotManager.reset();
+ var stop,
+ operation = {},
+ args = [];
+
+ while (!(stop = timeSlotManager.check())) {
+ args.length = 0;
+ operation.args = args;
+
+ if (!preprocessor.read(operation)) {
+ break;
+ }
+
+ textState = stateManager.state;
+ var fn = operation.fn;
+ args = operation.args;
+ var advance, diff;
+
+ switch (fn | 0) {
+ case _util.OPS.setFont:
+ var fontNameArg = args[0].name,
+ fontSizeArg = args[1];
+
+ if (textState.font && fontNameArg === textState.fontName && fontSizeArg === textState.fontSize) {
+ break;
+ }
+
+ flushTextContentItem();
+ textState.fontName = fontNameArg;
+ textState.fontSize = fontSizeArg;
+ next(handleSetFont(fontNameArg, null));
+ return;
+
+ case _util.OPS.setTextRise:
+ flushTextContentItem();
+ textState.textRise = args[0];
+ break;
+
+ case _util.OPS.setHScale:
+ flushTextContentItem();
+ textState.textHScale = args[0] / 100;
+ break;
+
+ case _util.OPS.setLeading:
+ flushTextContentItem();
+ textState.leading = args[0];
+ break;
+
+ case _util.OPS.moveText:
+ var isSameTextLine = !textState.font ? false : (textState.font.vertical ? args[0] : args[1]) === 0;
+ advance = args[0] - args[1];
+
+ if (combineTextItems && isSameTextLine && textContentItem.initialized && advance > 0 && advance <= textContentItem.fakeMultiSpaceMax) {
+ textState.translateTextLineMatrix(args[0], args[1]);
+ textContentItem.width += args[0] - textContentItem.lastAdvanceWidth;
+ textContentItem.height += args[1] - textContentItem.lastAdvanceHeight;
+ diff = args[0] - textContentItem.lastAdvanceWidth - (args[1] - textContentItem.lastAdvanceHeight);
+ addFakeSpaces(diff, textContentItem.str);
+ break;
+ }
+
+ flushTextContentItem();
+ textState.translateTextLineMatrix(args[0], args[1]);
+ textState.textMatrix = textState.textLineMatrix.slice();
+ break;
+
+ case _util.OPS.setLeadingMoveText:
+ flushTextContentItem();
+ textState.leading = -args[1];
+ textState.translateTextLineMatrix(args[0], args[1]);
+ textState.textMatrix = textState.textLineMatrix.slice();
+ break;
+
+ case _util.OPS.nextLine:
+ flushTextContentItem();
+ textState.carriageReturn();
+ break;
+
+ case _util.OPS.setTextMatrix:
+ advance = textState.calcTextLineMatrixAdvance(args[0], args[1], args[2], args[3], args[4], args[5]);
+
+ if (combineTextItems && advance !== null && textContentItem.initialized && advance.value > 0 && advance.value <= textContentItem.fakeMultiSpaceMax) {
+ textState.translateTextLineMatrix(advance.width, advance.height);
+ textContentItem.width += advance.width - textContentItem.lastAdvanceWidth;
+ textContentItem.height += advance.height - textContentItem.lastAdvanceHeight;
+ diff = advance.width - textContentItem.lastAdvanceWidth - (advance.height - textContentItem.lastAdvanceHeight);
+ addFakeSpaces(diff, textContentItem.str);
+ break;
+ }
+
+ flushTextContentItem();
+ textState.setTextMatrix(args[0], args[1], args[2], args[3], args[4], args[5]);
+ textState.setTextLineMatrix(args[0], args[1], args[2], args[3], args[4], args[5]);
+ break;
+
+ case _util.OPS.setCharSpacing:
+ textState.charSpacing = args[0];
+ break;
+
+ case _util.OPS.setWordSpacing:
+ textState.wordSpacing = args[0];
+ break;
+
+ case _util.OPS.beginText:
+ flushTextContentItem();
+ textState.textMatrix = _util.IDENTITY_MATRIX.slice();
+ textState.textLineMatrix = _util.IDENTITY_MATRIX.slice();
+ break;
+
+ case _util.OPS.showSpacedText:
+ var items = args[0];
+ var offset;
+
+ for (var j = 0, jj = items.length; j < jj; j++) {
+ if (typeof items[j] === 'string') {
+ buildTextContentItem(items[j]);
+ } else if ((0, _util.isNum)(items[j])) {
+ ensureTextContentItem();
+ advance = items[j] * textState.fontSize / 1000;
+ var breakTextRun = false;
+
+ if (textState.font.vertical) {
+ offset = advance;
+ textState.translateTextMatrix(0, offset);
+ breakTextRun = textContentItem.textRunBreakAllowed && advance > textContentItem.fakeMultiSpaceMax;
+
+ if (!breakTextRun) {
+ textContentItem.height += offset;
+ }
+ } else {
+ advance = -advance;
+ offset = advance * textState.textHScale;
+ textState.translateTextMatrix(offset, 0);
+ breakTextRun = textContentItem.textRunBreakAllowed && advance > textContentItem.fakeMultiSpaceMax;
+
+ if (!breakTextRun) {
+ textContentItem.width += offset;
+ }
+ }
+
+ if (breakTextRun) {
+ flushTextContentItem();
+ } else if (advance > 0) {
+ addFakeSpaces(advance, textContentItem.str);
+ }
+ }
+ }
+
+ break;
+
+ case _util.OPS.showText:
+ buildTextContentItem(args[0]);
+ break;
+
+ case _util.OPS.nextLineShowText:
+ flushTextContentItem();
+ textState.carriageReturn();
+ buildTextContentItem(args[0]);
+ break;
+
+ case _util.OPS.nextLineSetSpacingShowText:
+ flushTextContentItem();
+ textState.wordSpacing = args[0];
+ textState.charSpacing = args[1];
+ textState.carriageReturn();
+ buildTextContentItem(args[2]);
+ break;
+
+ case _util.OPS.paintXObject:
+ flushTextContentItem();
+
+ if (!xobjs) {
+ xobjs = resources.get('XObject') || _primitives.Dict.empty;
+ }
+
+ var name = args[0].name;
+
+ if (name && skipEmptyXObjs[name] !== undefined) {
+ break;
+ }
+
+ next(new Promise(function (resolveXObject, rejectXObject) {
+ if (!name) {
+ throw new _util.FormatError('XObject must be referred to by name.');
+ }
+
+ var xobj = xobjs.get(name);
+
+ if (!xobj) {
+ resolveXObject();
+ return;
+ }
+
+ if (!(0, _primitives.isStream)(xobj)) {
+ throw new _util.FormatError('XObject should be a stream');
+ }
+
+ var type = xobj.dict.get('Subtype');
+
+ if (!(0, _primitives.isName)(type)) {
+ throw new _util.FormatError('XObject should have a Name subtype');
+ }
+
+ if (type.name !== 'Form') {
+ skipEmptyXObjs[name] = true;
+ resolveXObject();
+ return;
+ }
+
+ var currentState = stateManager.state.clone();
+ var xObjStateManager = new StateManager(currentState);
+ var matrix = xobj.dict.getArray('Matrix');
+
+ if (Array.isArray(matrix) && matrix.length === 6) {
+ xObjStateManager.transform(matrix);
+ }
+
+ enqueueChunk();
+ var sinkWrapper = {
+ enqueueInvoked: false,
+ enqueue: function enqueue(chunk, size) {
+ this.enqueueInvoked = true;
+ sink.enqueue(chunk, size);
+ },
+
+ get desiredSize() {
+ return sink.desiredSize;
+ },
+
+ get ready() {
+ return sink.ready;
+ }
+
+ };
+ self.getTextContent({
+ stream: xobj,
+ task: task,
+ resources: xobj.dict.get('Resources') || resources,
+ stateManager: xObjStateManager,
+ normalizeWhitespace: normalizeWhitespace,
+ combineTextItems: combineTextItems,
+ sink: sinkWrapper,
+ seenStyles: seenStyles
+ }).then(function () {
+ if (!sinkWrapper.enqueueInvoked) {
+ skipEmptyXObjs[name] = true;
+ }
+
+ resolveXObject();
+ }, rejectXObject);
+ })["catch"](function (reason) {
+ if (reason instanceof _util.AbortException) {
+ return;
+ }
+
+ if (self.options.ignoreErrors) {
+ (0, _util.warn)("getTextContent - ignoring XObject: \"".concat(reason, "\"."));
+ return;
+ }
+
+ throw reason;
+ }));
+ return;
+
+ case _util.OPS.setGState:
+ flushTextContentItem();
+ var dictName = args[0];
+ var extGState = resources.get('ExtGState');
+
+ if (!(0, _primitives.isDict)(extGState) || !(0, _primitives.isName)(dictName)) {
+ break;
+ }
+
+ var gState = extGState.get(dictName.name);
+
+ if (!(0, _primitives.isDict)(gState)) {
+ break;
+ }
+
+ var gStateFont = gState.get('Font');
+
+ if (gStateFont) {
+ textState.fontName = null;
+ textState.fontSize = gStateFont[1];
+ next(handleSetFont(null, gStateFont[0]));
+ return;
+ }
+
+ break;
+ }
+
+ if (textContent.items.length >= sink.desiredSize) {
+ stop = true;
+ break;
+ }
+ }
+
+ if (stop) {
+ next(deferred);
+ return;
+ }
+
+ flushTextContentItem();
+ enqueueChunk();
+ resolve();
+ })["catch"](function (reason) {
+ if (reason instanceof _util.AbortException) {
+ return;
+ }
+
+ if (_this8.options.ignoreErrors) {
+ (0, _util.warn)("getTextContent - ignoring errors during \"".concat(task.name, "\" ") + "task: \"".concat(reason, "\"."));
+ flushTextContentItem();
+ enqueueChunk();
+ return;
+ }
+
+ throw reason;
+ });
+ },
+ extractDataStructures: function PartialEvaluator_extractDataStructures(dict, baseDict, properties) {
+ var _this9 = this;
+
+ var xref = this.xref,
+ cidToGidBytes;
+ var toUnicode = dict.get('ToUnicode') || baseDict.get('ToUnicode');
+ var toUnicodePromise = toUnicode ? this.readToUnicode(toUnicode) : Promise.resolve(undefined);
+
+ if (properties.composite) {
+ var cidSystemInfo = dict.get('CIDSystemInfo');
+
+ if ((0, _primitives.isDict)(cidSystemInfo)) {
+ properties.cidSystemInfo = {
+ registry: (0, _util.stringToPDFString)(cidSystemInfo.get('Registry')),
+ ordering: (0, _util.stringToPDFString)(cidSystemInfo.get('Ordering')),
+ supplement: cidSystemInfo.get('Supplement')
+ };
+ }
+
+ var cidToGidMap = dict.get('CIDToGIDMap');
+
+ if ((0, _primitives.isStream)(cidToGidMap)) {
+ cidToGidBytes = cidToGidMap.getBytes();
+ }
+ }
+
+ var differences = [];
+ var baseEncodingName = null;
+ var encoding;
+
+ if (dict.has('Encoding')) {
+ encoding = dict.get('Encoding');
+
+ if ((0, _primitives.isDict)(encoding)) {
+ baseEncodingName = encoding.get('BaseEncoding');
+ baseEncodingName = (0, _primitives.isName)(baseEncodingName) ? baseEncodingName.name : null;
+
+ if (encoding.has('Differences')) {
+ var diffEncoding = encoding.get('Differences');
+ var index = 0;
+
+ for (var j = 0, jj = diffEncoding.length; j < jj; j++) {
+ var data = xref.fetchIfRef(diffEncoding[j]);
+
+ if ((0, _util.isNum)(data)) {
+ index = data;
+ } else if ((0, _primitives.isName)(data)) {
+ differences[index++] = data.name;
+ } else {
+ throw new _util.FormatError("Invalid entry in 'Differences' array: ".concat(data));
+ }
+ }
+ }
+ } else if ((0, _primitives.isName)(encoding)) {
+ baseEncodingName = encoding.name;
+ } else {
+ throw new _util.FormatError('Encoding is not a Name nor a Dict');
+ }
+
+ if (baseEncodingName !== 'MacRomanEncoding' && baseEncodingName !== 'MacExpertEncoding' && baseEncodingName !== 'WinAnsiEncoding') {
+ baseEncodingName = null;
+ }
+ }
+
+ if (baseEncodingName) {
+ properties.defaultEncoding = (0, _encodings.getEncoding)(baseEncodingName).slice();
+ } else {
+ var isSymbolicFont = !!(properties.flags & _fonts.FontFlags.Symbolic);
+ var isNonsymbolicFont = !!(properties.flags & _fonts.FontFlags.Nonsymbolic);
+ encoding = _encodings.StandardEncoding;
+
+ if (properties.type === 'TrueType' && !isNonsymbolicFont) {
+ encoding = _encodings.WinAnsiEncoding;
+ }
+
+ if (isSymbolicFont) {
+ encoding = _encodings.MacRomanEncoding;
+
+ if (!properties.file) {
+ if (/Symbol/i.test(properties.name)) {
+ encoding = _encodings.SymbolSetEncoding;
+ } else if (/Dingbats/i.test(properties.name)) {
+ encoding = _encodings.ZapfDingbatsEncoding;
+ }
+ }
+ }
+
+ properties.defaultEncoding = encoding;
+ }
+
+ properties.differences = differences;
+ properties.baseEncodingName = baseEncodingName;
+ properties.hasEncoding = !!baseEncodingName || differences.length > 0;
+ properties.dict = dict;
+ return toUnicodePromise.then(function (toUnicode) {
+ properties.toUnicode = toUnicode;
+ return _this9.buildToUnicode(properties);
+ }).then(function (toUnicode) {
+ properties.toUnicode = toUnicode;
+
+ if (cidToGidBytes) {
+ properties.cidToGidMap = _this9.readCidToGidMap(cidToGidBytes, toUnicode);
+ }
+
+ return properties;
+ });
+ },
+ _buildSimpleFontToUnicode: function _buildSimpleFontToUnicode(properties) {
+ (0, _util.assert)(!properties.composite, 'Must be a simple font.');
+ var toUnicode = [],
+ charcode,
+ glyphName;
+ var encoding = properties.defaultEncoding.slice();
+ var baseEncodingName = properties.baseEncodingName;
+ var differences = properties.differences;
+
+ for (charcode in differences) {
+ glyphName = differences[charcode];
+
+ if (glyphName === '.notdef') {
+ continue;
+ }
+
+ encoding[charcode] = glyphName;
+ }
+
+ var glyphsUnicodeMap = (0, _glyphlist.getGlyphsUnicode)();
+
+ for (charcode in encoding) {
+ glyphName = encoding[charcode];
+
+ if (glyphName === '') {
+ continue;
+ } else if (glyphsUnicodeMap[glyphName] === undefined) {
+ var code = 0;
+
+ switch (glyphName[0]) {
+ case 'G':
+ if (glyphName.length === 3) {
+ code = parseInt(glyphName.substring(1), 16);
+ }
+
+ break;
+
+ case 'g':
+ if (glyphName.length === 5) {
+ code = parseInt(glyphName.substring(1), 16);
+ }
+
+ break;
+
+ case 'C':
+ case 'c':
+ if (glyphName.length >= 3) {
+ code = +glyphName.substring(1);
+ }
+
+ break;
+
+ default:
+ var unicode = (0, _unicode.getUnicodeForGlyph)(glyphName, glyphsUnicodeMap);
+
+ if (unicode !== -1) {
+ code = unicode;
+ }
+
+ }
+
+ if (code) {
+ if (baseEncodingName && code === +charcode) {
+ var baseEncoding = (0, _encodings.getEncoding)(baseEncodingName);
+
+ if (baseEncoding && (glyphName = baseEncoding[charcode])) {
+ toUnicode[charcode] = String.fromCharCode(glyphsUnicodeMap[glyphName]);
+ continue;
+ }
+ }
+
+ toUnicode[charcode] = String.fromCodePoint(code);
+ }
+
+ continue;
+ }
+
+ toUnicode[charcode] = String.fromCharCode(glyphsUnicodeMap[glyphName]);
+ }
+
+ return new _fonts.ToUnicodeMap(toUnicode);
+ },
+ buildToUnicode: function buildToUnicode(properties) {
+ properties.hasIncludedToUnicodeMap = !!properties.toUnicode && properties.toUnicode.length > 0;
+
+ if (properties.hasIncludedToUnicodeMap) {
+ if (!properties.composite && properties.hasEncoding) {
+ properties.fallbackToUnicode = this._buildSimpleFontToUnicode(properties);
+ }
+
+ return Promise.resolve(properties.toUnicode);
+ }
+
+ if (!properties.composite) {
+ return Promise.resolve(this._buildSimpleFontToUnicode(properties));
+ }
+
+ if (properties.composite && (properties.cMap.builtInCMap && !(properties.cMap instanceof _cmap.IdentityCMap) || properties.cidSystemInfo.registry === 'Adobe' && (properties.cidSystemInfo.ordering === 'GB1' || properties.cidSystemInfo.ordering === 'CNS1' || properties.cidSystemInfo.ordering === 'Japan1' || properties.cidSystemInfo.ordering === 'Korea1'))) {
+ var registry = properties.cidSystemInfo.registry;
+ var ordering = properties.cidSystemInfo.ordering;
+
+ var ucs2CMapName = _primitives.Name.get(registry + '-' + ordering + '-UCS2');
+
+ return _cmap.CMapFactory.create({
+ encoding: ucs2CMapName,
+ fetchBuiltInCMap: this.fetchBuiltInCMap,
+ useCMap: null
+ }).then(function (ucs2CMap) {
+ var cMap = properties.cMap;
+ var toUnicode = [];
+ cMap.forEach(function (charcode, cid) {
+ if (cid > 0xffff) {
+ throw new _util.FormatError('Max size of CID is 65,535');
+ }
+
+ var ucs2 = ucs2CMap.lookup(cid);
+
+ if (ucs2) {
+ toUnicode[charcode] = String.fromCharCode((ucs2.charCodeAt(0) << 8) + ucs2.charCodeAt(1));
+ }
+ });
+ return new _fonts.ToUnicodeMap(toUnicode);
+ });
+ }
+
+ return Promise.resolve(new _fonts.IdentityToUnicodeMap(properties.firstChar, properties.lastChar));
+ },
+ readToUnicode: function PartialEvaluator_readToUnicode(toUnicode) {
+ var cmapObj = toUnicode;
+
+ if ((0, _primitives.isName)(cmapObj)) {
+ return _cmap.CMapFactory.create({
+ encoding: cmapObj,
+ fetchBuiltInCMap: this.fetchBuiltInCMap,
+ useCMap: null
+ }).then(function (cmap) {
+ if (cmap instanceof _cmap.IdentityCMap) {
+ return new _fonts.IdentityToUnicodeMap(0, 0xFFFF);
+ }
+
+ return new _fonts.ToUnicodeMap(cmap.getMap());
+ });
+ } else if ((0, _primitives.isStream)(cmapObj)) {
+ return _cmap.CMapFactory.create({
+ encoding: cmapObj,
+ fetchBuiltInCMap: this.fetchBuiltInCMap,
+ useCMap: null
+ }).then(function (cmap) {
+ if (cmap instanceof _cmap.IdentityCMap) {
+ return new _fonts.IdentityToUnicodeMap(0, 0xFFFF);
+ }
+
+ var map = new Array(cmap.length);
+ cmap.forEach(function (charCode, token) {
+ var str = [];
+
+ for (var k = 0; k < token.length; k += 2) {
+ var w1 = token.charCodeAt(k) << 8 | token.charCodeAt(k + 1);
+
+ if ((w1 & 0xF800) !== 0xD800) {
+ str.push(w1);
+ continue;
+ }
+
+ k += 2;
+ var w2 = token.charCodeAt(k) << 8 | token.charCodeAt(k + 1);
+ str.push(((w1 & 0x3ff) << 10) + (w2 & 0x3ff) + 0x10000);
+ }
+
+ map[charCode] = String.fromCodePoint.apply(String, str);
+ });
+ return new _fonts.ToUnicodeMap(map);
+ });
+ }
+
+ return Promise.resolve(null);
+ },
+ readCidToGidMap: function readCidToGidMap(glyphsData, toUnicode) {
+ var result = [];
+
+ for (var j = 0, jj = glyphsData.length; j < jj; j++) {
+ var glyphID = glyphsData[j++] << 8 | glyphsData[j];
+ var code = j >> 1;
+
+ if (glyphID === 0 && !toUnicode.has(code)) {
+ continue;
+ }
+
+ result[code] = glyphID;
+ }
+
+ return result;
+ },
+ extractWidths: function PartialEvaluator_extractWidths(dict, descriptor, properties) {
+ var xref = this.xref;
+ var glyphsWidths = [];
+ var defaultWidth = 0;
+ var glyphsVMetrics = [];
+ var defaultVMetrics;
+ var i, ii, j, jj, start, code, widths;
+
+ if (properties.composite) {
+ defaultWidth = dict.has('DW') ? dict.get('DW') : 1000;
+ widths = dict.get('W');
+
+ if (widths) {
+ for (i = 0, ii = widths.length; i < ii; i++) {
+ start = xref.fetchIfRef(widths[i++]);
+ code = xref.fetchIfRef(widths[i]);
+
+ if (Array.isArray(code)) {
+ for (j = 0, jj = code.length; j < jj; j++) {
+ glyphsWidths[start++] = xref.fetchIfRef(code[j]);
+ }
+ } else {
+ var width = xref.fetchIfRef(widths[++i]);
+
+ for (j = start; j <= code; j++) {
+ glyphsWidths[j] = width;
+ }
+ }
+ }
+ }
+
+ if (properties.vertical) {
+ var vmetrics = dict.getArray('DW2') || [880, -1000];
+ defaultVMetrics = [vmetrics[1], defaultWidth * 0.5, vmetrics[0]];
+ vmetrics = dict.get('W2');
+
+ if (vmetrics) {
+ for (i = 0, ii = vmetrics.length; i < ii; i++) {
+ start = xref.fetchIfRef(vmetrics[i++]);
+ code = xref.fetchIfRef(vmetrics[i]);
+
+ if (Array.isArray(code)) {
+ for (j = 0, jj = code.length; j < jj; j++) {
+ glyphsVMetrics[start++] = [xref.fetchIfRef(code[j++]), xref.fetchIfRef(code[j++]), xref.fetchIfRef(code[j])];
+ }
+ } else {
+ var vmetric = [xref.fetchIfRef(vmetrics[++i]), xref.fetchIfRef(vmetrics[++i]), xref.fetchIfRef(vmetrics[++i])];
+
+ for (j = start; j <= code; j++) {
+ glyphsVMetrics[j] = vmetric;
+ }
+ }
+ }
+ }
+ }
+ } else {
+ var firstChar = properties.firstChar;
+ widths = dict.get('Widths');
+
+ if (widths) {
+ j = firstChar;
+
+ for (i = 0, ii = widths.length; i < ii; i++) {
+ glyphsWidths[j++] = xref.fetchIfRef(widths[i]);
+ }
+
+ defaultWidth = parseFloat(descriptor.get('MissingWidth')) || 0;
+ } else {
+ var baseFontName = dict.get('BaseFont');
+
+ if ((0, _primitives.isName)(baseFontName)) {
+ var metrics = this.getBaseFontMetrics(baseFontName.name);
+ glyphsWidths = this.buildCharCodeToWidth(metrics.widths, properties);
+ defaultWidth = metrics.defaultWidth;
+ }
+ }
+ }
+
+ var isMonospace = true;
+ var firstWidth = defaultWidth;
+
+ for (var glyph in glyphsWidths) {
+ var glyphWidth = glyphsWidths[glyph];
+
+ if (!glyphWidth) {
+ continue;
+ }
+
+ if (!firstWidth) {
+ firstWidth = glyphWidth;
+ continue;
+ }
+
+ if (firstWidth !== glyphWidth) {
+ isMonospace = false;
+ break;
+ }
+ }
+
+ if (isMonospace) {
+ properties.flags |= _fonts.FontFlags.FixedPitch;
+ }
+
+ properties.defaultWidth = defaultWidth;
+ properties.widths = glyphsWidths;
+ properties.defaultVMetrics = defaultVMetrics;
+ properties.vmetrics = glyphsVMetrics;
+ },
+ isSerifFont: function PartialEvaluator_isSerifFont(baseFontName) {
+ var fontNameWoStyle = baseFontName.split('-')[0];
+ return fontNameWoStyle in (0, _standard_fonts.getSerifFonts)() || fontNameWoStyle.search(/serif/gi) !== -1;
+ },
+ getBaseFontMetrics: function PartialEvaluator_getBaseFontMetrics(name) {
+ var defaultWidth = 0;
+ var widths = [];
+ var monospace = false;
+ var stdFontMap = (0, _standard_fonts.getStdFontMap)();
+ var lookupName = stdFontMap[name] || name;
+ var Metrics = (0, _metrics.getMetrics)();
+
+ if (!(lookupName in Metrics)) {
+ if (this.isSerifFont(name)) {
+ lookupName = 'Times-Roman';
+ } else {
+ lookupName = 'Helvetica';
+ }
+ }
+
+ var glyphWidths = Metrics[lookupName];
+
+ if ((0, _util.isNum)(glyphWidths)) {
+ defaultWidth = glyphWidths;
+ monospace = true;
+ } else {
+ widths = glyphWidths();
+ }
+
+ return {
+ defaultWidth: defaultWidth,
+ monospace: monospace,
+ widths: widths
+ };
+ },
+ buildCharCodeToWidth: function PartialEvaluator_bulildCharCodeToWidth(widthsByGlyphName, properties) {
+ var widths = Object.create(null);
+ var differences = properties.differences;
+ var encoding = properties.defaultEncoding;
+
+ for (var charCode = 0; charCode < 256; charCode++) {
+ if (charCode in differences && widthsByGlyphName[differences[charCode]]) {
+ widths[charCode] = widthsByGlyphName[differences[charCode]];
+ continue;
+ }
+
+ if (charCode in encoding && widthsByGlyphName[encoding[charCode]]) {
+ widths[charCode] = widthsByGlyphName[encoding[charCode]];
+ continue;
+ }
+ }
+
+ return widths;
+ },
+ preEvaluateFont: function PartialEvaluator_preEvaluateFont(dict) {
+ var baseDict = dict;
+ var type = dict.get('Subtype');
+
+ if (!(0, _primitives.isName)(type)) {
+ throw new _util.FormatError('invalid font Subtype');
+ }
+
+ var composite = false;
+ var uint8array;
+
+ if (type.name === 'Type0') {
+ var df = dict.get('DescendantFonts');
+
+ if (!df) {
+ throw new _util.FormatError('Descendant fonts are not specified');
+ }
+
+ dict = Array.isArray(df) ? this.xref.fetchIfRef(df[0]) : df;
+ type = dict.get('Subtype');
+
+ if (!(0, _primitives.isName)(type)) {
+ throw new _util.FormatError('invalid font Subtype');
+ }
+
+ composite = true;
+ }
+
+ var descriptor = dict.get('FontDescriptor');
+
+ if (descriptor) {
+ var hash = new _murmurhash.MurmurHash3_64();
+ var encoding = baseDict.getRaw('Encoding');
+
+ if ((0, _primitives.isName)(encoding)) {
+ hash.update(encoding.name);
+ } else if ((0, _primitives.isRef)(encoding)) {
+ hash.update(encoding.toString());
+ } else if ((0, _primitives.isDict)(encoding)) {
+ var keys = encoding.getKeys();
+
+ for (var i = 0, ii = keys.length; i < ii; i++) {
+ var entry = encoding.getRaw(keys[i]);
+
+ if ((0, _primitives.isName)(entry)) {
+ hash.update(entry.name);
+ } else if ((0, _primitives.isRef)(entry)) {
+ hash.update(entry.toString());
+ } else if (Array.isArray(entry)) {
+ var diffLength = entry.length,
+ diffBuf = new Array(diffLength);
+
+ for (var j = 0; j < diffLength; j++) {
+ var diffEntry = entry[j];
+
+ if ((0, _primitives.isName)(diffEntry)) {
+ diffBuf[j] = diffEntry.name;
+ } else if ((0, _util.isNum)(diffEntry) || (0, _primitives.isRef)(diffEntry)) {
+ diffBuf[j] = diffEntry.toString();
+ }
+ }
+
+ hash.update(diffBuf.join());
+ }
+ }
+ }
+
+ var firstChar = dict.get('FirstChar') || 0;
+ var lastChar = dict.get('LastChar') || (composite ? 0xFFFF : 0xFF);
+ hash.update("".concat(firstChar, "-").concat(lastChar));
+ var toUnicode = dict.get('ToUnicode') || baseDict.get('ToUnicode');
+
+ if ((0, _primitives.isStream)(toUnicode)) {
+ var stream = toUnicode.str || toUnicode;
+ uint8array = stream.buffer ? new Uint8Array(stream.buffer.buffer, 0, stream.bufferLength) : new Uint8Array(stream.bytes.buffer, stream.start, stream.end - stream.start);
+ hash.update(uint8array);
+ } else if ((0, _primitives.isName)(toUnicode)) {
+ hash.update(toUnicode.name);
+ }
+
+ var widths = dict.get('Widths') || baseDict.get('Widths');
+
+ if (widths) {
+ uint8array = new Uint8Array(new Uint32Array(widths).buffer);
+ hash.update(uint8array);
+ }
+ }
+
+ return {
+ descriptor: descriptor,
+ dict: dict,
+ baseDict: baseDict,
+ composite: composite,
+ type: type.name,
+ hash: hash ? hash.hexdigest() : ''
+ };
+ },
+ translateFont: function PartialEvaluator_translateFont(preEvaluatedFont) {
+ var _this10 = this;
+
+ var baseDict = preEvaluatedFont.baseDict;
+ var dict = preEvaluatedFont.dict;
+ var composite = preEvaluatedFont.composite;
+ var descriptor = preEvaluatedFont.descriptor;
+ var type = preEvaluatedFont.type;
+ var maxCharIndex = composite ? 0xFFFF : 0xFF;
+ var properties;
+
+ if (!descriptor) {
+ if (type === 'Type3') {
+ descriptor = new _primitives.Dict(null);
+ descriptor.set('FontName', _primitives.Name.get(type));
+ descriptor.set('FontBBox', dict.getArray('FontBBox'));
+ } else {
+ var baseFontName = dict.get('BaseFont');
+
+ if (!(0, _primitives.isName)(baseFontName)) {
+ throw new _util.FormatError('Base font is not specified');
+ }
+
+ baseFontName = baseFontName.name.replace(/[,_]/g, '-');
+ var metrics = this.getBaseFontMetrics(baseFontName);
+ var fontNameWoStyle = baseFontName.split('-')[0];
+ var flags = (this.isSerifFont(fontNameWoStyle) ? _fonts.FontFlags.Serif : 0) | (metrics.monospace ? _fonts.FontFlags.FixedPitch : 0) | ((0, _standard_fonts.getSymbolsFonts)()[fontNameWoStyle] ? _fonts.FontFlags.Symbolic : _fonts.FontFlags.Nonsymbolic);
+ properties = {
+ type: type,
+ name: baseFontName,
+ widths: metrics.widths,
+ defaultWidth: metrics.defaultWidth,
+ flags: flags,
+ firstChar: 0,
+ lastChar: maxCharIndex
+ };
+ return this.extractDataStructures(dict, dict, properties).then(function (properties) {
+ properties.widths = _this10.buildCharCodeToWidth(metrics.widths, properties);
+ return new _fonts.Font(baseFontName, null, properties);
+ });
+ }
+ }
+
+ var firstChar = dict.get('FirstChar') || 0;
+ var lastChar = dict.get('LastChar') || maxCharIndex;
+ var fontName = descriptor.get('FontName');
+ var baseFont = dict.get('BaseFont');
+
+ if ((0, _util.isString)(fontName)) {
+ fontName = _primitives.Name.get(fontName);
+ }
+
+ if ((0, _util.isString)(baseFont)) {
+ baseFont = _primitives.Name.get(baseFont);
+ }
+
+ if (type !== 'Type3') {
+ var fontNameStr = fontName && fontName.name;
+ var baseFontStr = baseFont && baseFont.name;
+
+ if (fontNameStr !== baseFontStr) {
+ (0, _util.info)("The FontDescriptor's FontName is \"".concat(fontNameStr, "\" but ") + "should be the same as the Font's BaseFont \"".concat(baseFontStr, "\"."));
+
+ if (fontNameStr && baseFontStr && baseFontStr.startsWith(fontNameStr)) {
+ fontName = baseFont;
+ }
+ }
+ }
+
+ fontName = fontName || baseFont;
+
+ if (!(0, _primitives.isName)(fontName)) {
+ throw new _util.FormatError('invalid font name');
+ }
+
+ var fontFile = descriptor.get('FontFile', 'FontFile2', 'FontFile3');
+
+ if (fontFile) {
+ if (fontFile.dict) {
+ var subtype = fontFile.dict.get('Subtype');
+
+ if (subtype) {
+ subtype = subtype.name;
+ }
+
+ var length1 = fontFile.dict.get('Length1');
+ var length2 = fontFile.dict.get('Length2');
+ var length3 = fontFile.dict.get('Length3');
+ }
+ }
+
+ properties = {
+ type: type,
+ name: fontName.name,
+ subtype: subtype,
+ file: fontFile,
+ length1: length1,
+ length2: length2,
+ length3: length3,
+ loadedName: baseDict.loadedName,
+ composite: composite,
+ wideChars: composite,
+ fixedPitch: false,
+ fontMatrix: dict.getArray('FontMatrix') || _util.FONT_IDENTITY_MATRIX,
+ firstChar: firstChar || 0,
+ lastChar: lastChar || maxCharIndex,
+ bbox: descriptor.getArray('FontBBox'),
+ ascent: descriptor.get('Ascent'),
+ descent: descriptor.get('Descent'),
+ xHeight: descriptor.get('XHeight'),
+ capHeight: descriptor.get('CapHeight'),
+ flags: descriptor.get('Flags'),
+ italicAngle: descriptor.get('ItalicAngle'),
+ isType3Font: false
+ };
+ var cMapPromise;
+
+ if (composite) {
+ var cidEncoding = baseDict.get('Encoding');
+
+ if ((0, _primitives.isName)(cidEncoding)) {
+ properties.cidEncoding = cidEncoding.name;
+ }
+
+ cMapPromise = _cmap.CMapFactory.create({
+ encoding: cidEncoding,
+ fetchBuiltInCMap: this.fetchBuiltInCMap,
+ useCMap: null
+ }).then(function (cMap) {
+ properties.cMap = cMap;
+ properties.vertical = properties.cMap.vertical;
+ });
+ } else {
+ cMapPromise = Promise.resolve(undefined);
+ }
+
+ return cMapPromise.then(function () {
+ return _this10.extractDataStructures(dict, baseDict, properties);
+ }).then(function (properties) {
+ _this10.extractWidths(dict, descriptor, properties);
+
+ if (type === 'Type3') {
+ properties.isType3Font = true;
+ }
+
+ return new _fonts.Font(fontName.name, fontFile, properties);
+ });
+ }
+ };
+
+ PartialEvaluator.buildFontPaths = function (font, glyphs, handler) {
+ function buildPath(fontChar) {
+ if (font.renderer.hasBuiltPath(fontChar)) {
+ return;
+ }
+
+ handler.send('commonobj', ["".concat(font.loadedName, "_path_").concat(fontChar), 'FontPath', font.renderer.getPathJs(fontChar)]);
+ }
+
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = glyphs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var glyph = _step.value;
+ buildPath(glyph.fontChar);
+ var accent = glyph.accent;
+
+ if (accent && accent.fontChar) {
+ buildPath(accent.fontChar);
+ }
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator["return"] != null) {
+ _iterator["return"]();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+ };
+
+ return PartialEvaluator;
+}();
+
+exports.PartialEvaluator = PartialEvaluator;
+
+var TranslatedFont = function TranslatedFontClosure() {
+ function TranslatedFont(loadedName, font, dict) {
+ this.loadedName = loadedName;
+ this.font = font;
+ this.dict = dict;
+ this.type3Loaded = null;
+ this.sent = false;
+ }
+
+ TranslatedFont.prototype = {
+ send: function send(handler) {
+ if (this.sent) {
+ return;
+ }
+
+ this.sent = true;
+ handler.send('commonobj', [this.loadedName, 'Font', this.font.exportData()]);
+ },
+ fallback: function fallback(handler) {
+ if (!this.font.data) {
+ return;
+ }
+
+ this.font.disableFontFace = true;
+ var glyphs = this.font.glyphCacheValues;
+ PartialEvaluator.buildFontPaths(this.font, glyphs, handler);
+ },
+ loadType3Data: function loadType3Data(evaluator, resources, parentOperatorList, task) {
+ if (!this.font.isType3Font) {
+ throw new Error('Must be a Type3 font.');
+ }
+
+ if (this.type3Loaded) {
+ return this.type3Loaded;
+ }
+
+ var type3Options = Object.create(evaluator.options);
+ type3Options.ignoreErrors = false;
+ type3Options.nativeImageDecoderSupport = _util.NativeImageDecoding.NONE;
+ var type3Evaluator = evaluator.clone(type3Options);
+ type3Evaluator.parsingType3Font = true;
+ var translatedFont = this.font;
+ var loadCharProcsPromise = Promise.resolve();
+ var charProcs = this.dict.get('CharProcs');
+ var fontResources = this.dict.get('Resources') || resources;
+ var charProcKeys = charProcs.getKeys();
+ var charProcOperatorList = Object.create(null);
+
+ var _loop2 = function _loop2() {
+ var key = charProcKeys[i];
+ loadCharProcsPromise = loadCharProcsPromise.then(function () {
+ var glyphStream = charProcs.get(key);
+ var operatorList = new _operator_list.OperatorList();
+ return type3Evaluator.getOperatorList({
+ stream: glyphStream,
+ task: task,
+ resources: fontResources,
+ operatorList: operatorList
+ }).then(function () {
+ charProcOperatorList[key] = operatorList.getIR();
+ parentOperatorList.addDependencies(operatorList.dependencies);
+ })["catch"](function (reason) {
+ (0, _util.warn)("Type3 font resource \"".concat(key, "\" is not available."));
+ var operatorList = new _operator_list.OperatorList();
+ charProcOperatorList[key] = operatorList.getIR();
+ });
+ });
+ };
+
+ for (var i = 0, n = charProcKeys.length; i < n; ++i) {
+ _loop2();
+ }
+
+ this.type3Loaded = loadCharProcsPromise.then(function () {
+ translatedFont.charProcOperatorList = charProcOperatorList;
+ });
+ return this.type3Loaded;
+ }
+ };
+ return TranslatedFont;
+}();
+
+var StateManager = function StateManagerClosure() {
+ function StateManager(initialState) {
+ this.state = initialState;
+ this.stateStack = [];
+ }
+
+ StateManager.prototype = {
+ save: function save() {
+ var old = this.state;
+ this.stateStack.push(this.state);
+ this.state = old.clone();
+ },
+ restore: function restore() {
+ var prev = this.stateStack.pop();
+
+ if (prev) {
+ this.state = prev;
+ }
+ },
+ transform: function transform(args) {
+ this.state.ctm = _util.Util.transform(this.state.ctm, args);
+ }
+ };
+ return StateManager;
+}();
+
+var TextState = function TextStateClosure() {
+ function TextState() {
+ this.ctm = new Float32Array(_util.IDENTITY_MATRIX);
+ this.fontName = null;
+ this.fontSize = 0;
+ this.font = null;
+ this.fontMatrix = _util.FONT_IDENTITY_MATRIX;
+ this.textMatrix = _util.IDENTITY_MATRIX.slice();
+ this.textLineMatrix = _util.IDENTITY_MATRIX.slice();
+ this.charSpacing = 0;
+ this.wordSpacing = 0;
+ this.leading = 0;
+ this.textHScale = 1;
+ this.textRise = 0;
+ }
+
+ TextState.prototype = {
+ setTextMatrix: function TextState_setTextMatrix(a, b, c, d, e, f) {
+ var m = this.textMatrix;
+ m[0] = a;
+ m[1] = b;
+ m[2] = c;
+ m[3] = d;
+ m[4] = e;
+ m[5] = f;
+ },
+ setTextLineMatrix: function TextState_setTextMatrix(a, b, c, d, e, f) {
+ var m = this.textLineMatrix;
+ m[0] = a;
+ m[1] = b;
+ m[2] = c;
+ m[3] = d;
+ m[4] = e;
+ m[5] = f;
+ },
+ translateTextMatrix: function TextState_translateTextMatrix(x, y) {
+ var m = this.textMatrix;
+ m[4] = m[0] * x + m[2] * y + m[4];
+ m[5] = m[1] * x + m[3] * y + m[5];
+ },
+ translateTextLineMatrix: function TextState_translateTextMatrix(x, y) {
+ var m = this.textLineMatrix;
+ m[4] = m[0] * x + m[2] * y + m[4];
+ m[5] = m[1] * x + m[3] * y + m[5];
+ },
+ calcTextLineMatrixAdvance: function TextState_calcTextLineMatrixAdvance(a, b, c, d, e, f) {
+ var font = this.font;
+
+ if (!font) {
+ return null;
+ }
+
+ var m = this.textLineMatrix;
+
+ if (!(a === m[0] && b === m[1] && c === m[2] && d === m[3])) {
+ return null;
+ }
+
+ var txDiff = e - m[4],
+ tyDiff = f - m[5];
+
+ if (font.vertical && txDiff !== 0 || !font.vertical && tyDiff !== 0) {
+ return null;
+ }
+
+ var tx,
+ ty,
+ denominator = a * d - b * c;
+
+ if (font.vertical) {
+ tx = -tyDiff * c / denominator;
+ ty = tyDiff * a / denominator;
+ } else {
+ tx = txDiff * d / denominator;
+ ty = -txDiff * b / denominator;
+ }
+
+ return {
+ width: tx,
+ height: ty,
+ value: font.vertical ? ty : tx
+ };
+ },
+ calcRenderMatrix: function TextState_calcRendeMatrix(ctm) {
+ var tsm = [this.fontSize * this.textHScale, 0, 0, this.fontSize, 0, this.textRise];
+ return _util.Util.transform(ctm, _util.Util.transform(this.textMatrix, tsm));
+ },
+ carriageReturn: function TextState_carriageReturn() {
+ this.translateTextLineMatrix(0, -this.leading);
+ this.textMatrix = this.textLineMatrix.slice();
+ },
+ clone: function TextState_clone() {
+ var clone = Object.create(this);
+ clone.textMatrix = this.textMatrix.slice();
+ clone.textLineMatrix = this.textLineMatrix.slice();
+ clone.fontMatrix = this.fontMatrix.slice();
+ return clone;
+ }
+ };
+ return TextState;
+}();
+
+var EvalState = function EvalStateClosure() {
+ function EvalState() {
+ this.ctm = new Float32Array(_util.IDENTITY_MATRIX);
+ this.font = null;
+ this.textRenderingMode = _util.TextRenderingMode.FILL;
+ this.fillColorSpace = _colorspace.ColorSpace.singletons.gray;
+ this.strokeColorSpace = _colorspace.ColorSpace.singletons.gray;
+ }
+
+ EvalState.prototype = {
+ clone: function CanvasExtraState_clone() {
+ return Object.create(this);
+ }
+ };
+ return EvalState;
+}();
+
+var EvaluatorPreprocessor = function EvaluatorPreprocessorClosure() {
+ var getOPMap = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['w'] = {
+ id: _util.OPS.setLineWidth,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['J'] = {
+ id: _util.OPS.setLineCap,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['j'] = {
+ id: _util.OPS.setLineJoin,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['M'] = {
+ id: _util.OPS.setMiterLimit,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['d'] = {
+ id: _util.OPS.setDash,
+ numArgs: 2,
+ variableArgs: false
+ };
+ t['ri'] = {
+ id: _util.OPS.setRenderingIntent,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['i'] = {
+ id: _util.OPS.setFlatness,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['gs'] = {
+ id: _util.OPS.setGState,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['q'] = {
+ id: _util.OPS.save,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['Q'] = {
+ id: _util.OPS.restore,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['cm'] = {
+ id: _util.OPS.transform,
+ numArgs: 6,
+ variableArgs: false
+ };
+ t['m'] = {
+ id: _util.OPS.moveTo,
+ numArgs: 2,
+ variableArgs: false
+ };
+ t['l'] = {
+ id: _util.OPS.lineTo,
+ numArgs: 2,
+ variableArgs: false
+ };
+ t['c'] = {
+ id: _util.OPS.curveTo,
+ numArgs: 6,
+ variableArgs: false
+ };
+ t['v'] = {
+ id: _util.OPS.curveTo2,
+ numArgs: 4,
+ variableArgs: false
+ };
+ t['y'] = {
+ id: _util.OPS.curveTo3,
+ numArgs: 4,
+ variableArgs: false
+ };
+ t['h'] = {
+ id: _util.OPS.closePath,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['re'] = {
+ id: _util.OPS.rectangle,
+ numArgs: 4,
+ variableArgs: false
+ };
+ t['S'] = {
+ id: _util.OPS.stroke,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['s'] = {
+ id: _util.OPS.closeStroke,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['f'] = {
+ id: _util.OPS.fill,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['F'] = {
+ id: _util.OPS.fill,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['f*'] = {
+ id: _util.OPS.eoFill,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['B'] = {
+ id: _util.OPS.fillStroke,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['B*'] = {
+ id: _util.OPS.eoFillStroke,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['b'] = {
+ id: _util.OPS.closeFillStroke,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['b*'] = {
+ id: _util.OPS.closeEOFillStroke,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['n'] = {
+ id: _util.OPS.endPath,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['W'] = {
+ id: _util.OPS.clip,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['W*'] = {
+ id: _util.OPS.eoClip,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['BT'] = {
+ id: _util.OPS.beginText,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['ET'] = {
+ id: _util.OPS.endText,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['Tc'] = {
+ id: _util.OPS.setCharSpacing,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['Tw'] = {
+ id: _util.OPS.setWordSpacing,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['Tz'] = {
+ id: _util.OPS.setHScale,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['TL'] = {
+ id: _util.OPS.setLeading,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['Tf'] = {
+ id: _util.OPS.setFont,
+ numArgs: 2,
+ variableArgs: false
+ };
+ t['Tr'] = {
+ id: _util.OPS.setTextRenderingMode,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['Ts'] = {
+ id: _util.OPS.setTextRise,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['Td'] = {
+ id: _util.OPS.moveText,
+ numArgs: 2,
+ variableArgs: false
+ };
+ t['TD'] = {
+ id: _util.OPS.setLeadingMoveText,
+ numArgs: 2,
+ variableArgs: false
+ };
+ t['Tm'] = {
+ id: _util.OPS.setTextMatrix,
+ numArgs: 6,
+ variableArgs: false
+ };
+ t['T*'] = {
+ id: _util.OPS.nextLine,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['Tj'] = {
+ id: _util.OPS.showText,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['TJ'] = {
+ id: _util.OPS.showSpacedText,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['\''] = {
+ id: _util.OPS.nextLineShowText,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['"'] = {
+ id: _util.OPS.nextLineSetSpacingShowText,
+ numArgs: 3,
+ variableArgs: false
+ };
+ t['d0'] = {
+ id: _util.OPS.setCharWidth,
+ numArgs: 2,
+ variableArgs: false
+ };
+ t['d1'] = {
+ id: _util.OPS.setCharWidthAndBounds,
+ numArgs: 6,
+ variableArgs: false
+ };
+ t['CS'] = {
+ id: _util.OPS.setStrokeColorSpace,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['cs'] = {
+ id: _util.OPS.setFillColorSpace,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['SC'] = {
+ id: _util.OPS.setStrokeColor,
+ numArgs: 4,
+ variableArgs: true
+ };
+ t['SCN'] = {
+ id: _util.OPS.setStrokeColorN,
+ numArgs: 33,
+ variableArgs: true
+ };
+ t['sc'] = {
+ id: _util.OPS.setFillColor,
+ numArgs: 4,
+ variableArgs: true
+ };
+ t['scn'] = {
+ id: _util.OPS.setFillColorN,
+ numArgs: 33,
+ variableArgs: true
+ };
+ t['G'] = {
+ id: _util.OPS.setStrokeGray,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['g'] = {
+ id: _util.OPS.setFillGray,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['RG'] = {
+ id: _util.OPS.setStrokeRGBColor,
+ numArgs: 3,
+ variableArgs: false
+ };
+ t['rg'] = {
+ id: _util.OPS.setFillRGBColor,
+ numArgs: 3,
+ variableArgs: false
+ };
+ t['K'] = {
+ id: _util.OPS.setStrokeCMYKColor,
+ numArgs: 4,
+ variableArgs: false
+ };
+ t['k'] = {
+ id: _util.OPS.setFillCMYKColor,
+ numArgs: 4,
+ variableArgs: false
+ };
+ t['sh'] = {
+ id: _util.OPS.shadingFill,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['BI'] = {
+ id: _util.OPS.beginInlineImage,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['ID'] = {
+ id: _util.OPS.beginImageData,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['EI'] = {
+ id: _util.OPS.endInlineImage,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['Do'] = {
+ id: _util.OPS.paintXObject,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['MP'] = {
+ id: _util.OPS.markPoint,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['DP'] = {
+ id: _util.OPS.markPointProps,
+ numArgs: 2,
+ variableArgs: false
+ };
+ t['BMC'] = {
+ id: _util.OPS.beginMarkedContent,
+ numArgs: 1,
+ variableArgs: false
+ };
+ t['BDC'] = {
+ id: _util.OPS.beginMarkedContentProps,
+ numArgs: 2,
+ variableArgs: false
+ };
+ t['EMC'] = {
+ id: _util.OPS.endMarkedContent,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['BX'] = {
+ id: _util.OPS.beginCompat,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['EX'] = {
+ id: _util.OPS.endCompat,
+ numArgs: 0,
+ variableArgs: false
+ };
+ t['BM'] = null;
+ t['BD'] = null;
+ t['true'] = null;
+ t['fa'] = null;
+ t['fal'] = null;
+ t['fals'] = null;
+ t['false'] = null;
+ t['nu'] = null;
+ t['nul'] = null;
+ t['null'] = null;
+ });
+ var MAX_INVALID_PATH_OPS = 20;
+
+ function EvaluatorPreprocessor(stream, xref, stateManager) {
+ this.opMap = getOPMap();
+ this.parser = new _parser.Parser({
+ lexer: new _parser.Lexer(stream, this.opMap),
+ xref: xref
+ });
+ this.stateManager = stateManager;
+ this.nonProcessedArgs = [];
+ this._numInvalidPathOPS = 0;
+ }
+
+ EvaluatorPreprocessor.prototype = {
+ get savedStatesDepth() {
+ return this.stateManager.stateStack.length;
+ },
+
+ read: function EvaluatorPreprocessor_read(operation) {
+ var args = operation.args;
+
+ while (true) {
+ var obj = this.parser.getObj();
+
+ if ((0, _primitives.isCmd)(obj)) {
+ var cmd = obj.cmd;
+ var opSpec = this.opMap[cmd];
+
+ if (!opSpec) {
+ (0, _util.warn)("Unknown command \"".concat(cmd, "\"."));
+ continue;
+ }
+
+ var fn = opSpec.id;
+ var numArgs = opSpec.numArgs;
+ var argsLength = args !== null ? args.length : 0;
+
+ if (!opSpec.variableArgs) {
+ if (argsLength !== numArgs) {
+ var nonProcessedArgs = this.nonProcessedArgs;
+
+ while (argsLength > numArgs) {
+ nonProcessedArgs.push(args.shift());
+ argsLength--;
+ }
+
+ while (argsLength < numArgs && nonProcessedArgs.length !== 0) {
+ if (args === null) {
+ args = [];
+ }
+
+ args.unshift(nonProcessedArgs.pop());
+ argsLength++;
+ }
+ }
+
+ if (argsLength < numArgs) {
+ var partialMsg = "command ".concat(cmd, ": expected ").concat(numArgs, " args, ") + "but received ".concat(argsLength, " args.");
+
+ if (fn >= _util.OPS.moveTo && fn <= _util.OPS.endPath && ++this._numInvalidPathOPS > MAX_INVALID_PATH_OPS) {
+ throw new _util.FormatError("Invalid ".concat(partialMsg));
+ }
+
+ (0, _util.warn)("Skipping ".concat(partialMsg));
+
+ if (args !== null) {
+ args.length = 0;
+ }
+
+ continue;
+ }
+ } else if (argsLength > numArgs) {
+ (0, _util.info)("Command ".concat(cmd, ": expected [0, ").concat(numArgs, "] args, ") + "but received ".concat(argsLength, " args."));
+ }
+
+ this.preprocessCommand(fn, args);
+ operation.fn = fn;
+ operation.args = args;
+ return true;
+ }
+
+ if ((0, _primitives.isEOF)(obj)) {
+ return false;
+ }
+
+ if (obj !== null) {
+ if (args === null) {
+ args = [];
+ }
+
+ args.push(obj);
+
+ if (args.length > 33) {
+ throw new _util.FormatError('Too many arguments');
+ }
+ }
+ }
+ },
+ preprocessCommand: function EvaluatorPreprocessor_preprocessCommand(fn, args) {
+ switch (fn | 0) {
+ case _util.OPS.save:
+ this.stateManager.save();
+ break;
+
+ case _util.OPS.restore:
+ this.stateManager.restore();
+ break;
+
+ case _util.OPS.transform:
+ this.stateManager.transform(args);
+ break;
+ }
+ }
+ };
+ return EvaluatorPreprocessor;
+}();
+
+/***/ }),
+/* 173 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.CMapFactory = exports.IdentityCMap = exports.CMap = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _parser = __w_pdfjs_require__(157);
+
+var _core_utils = __w_pdfjs_require__(154);
+
+var _stream = __w_pdfjs_require__(158);
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
+
+function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
+
+function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
+
+function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var BUILT_IN_CMAPS = ['Adobe-GB1-UCS2', 'Adobe-CNS1-UCS2', 'Adobe-Japan1-UCS2', 'Adobe-Korea1-UCS2', '78-EUC-H', '78-EUC-V', '78-H', '78-RKSJ-H', '78-RKSJ-V', '78-V', '78ms-RKSJ-H', '78ms-RKSJ-V', '83pv-RKSJ-H', '90ms-RKSJ-H', '90ms-RKSJ-V', '90msp-RKSJ-H', '90msp-RKSJ-V', '90pv-RKSJ-H', '90pv-RKSJ-V', 'Add-H', 'Add-RKSJ-H', 'Add-RKSJ-V', 'Add-V', 'Adobe-CNS1-0', 'Adobe-CNS1-1', 'Adobe-CNS1-2', 'Adobe-CNS1-3', 'Adobe-CNS1-4', 'Adobe-CNS1-5', 'Adobe-CNS1-6', 'Adobe-GB1-0', 'Adobe-GB1-1', 'Adobe-GB1-2', 'Adobe-GB1-3', 'Adobe-GB1-4', 'Adobe-GB1-5', 'Adobe-Japan1-0', 'Adobe-Japan1-1', 'Adobe-Japan1-2', 'Adobe-Japan1-3', 'Adobe-Japan1-4', 'Adobe-Japan1-5', 'Adobe-Japan1-6', 'Adobe-Korea1-0', 'Adobe-Korea1-1', 'Adobe-Korea1-2', 'B5-H', 'B5-V', 'B5pc-H', 'B5pc-V', 'CNS-EUC-H', 'CNS-EUC-V', 'CNS1-H', 'CNS1-V', 'CNS2-H', 'CNS2-V', 'ETHK-B5-H', 'ETHK-B5-V', 'ETen-B5-H', 'ETen-B5-V', 'ETenms-B5-H', 'ETenms-B5-V', 'EUC-H', 'EUC-V', 'Ext-H', 'Ext-RKSJ-H', 'Ext-RKSJ-V', 'Ext-V', 'GB-EUC-H', 'GB-EUC-V', 'GB-H', 'GB-V', 'GBK-EUC-H', 'GBK-EUC-V', 'GBK2K-H', 'GBK2K-V', 'GBKp-EUC-H', 'GBKp-EUC-V', 'GBT-EUC-H', 'GBT-EUC-V', 'GBT-H', 'GBT-V', 'GBTpc-EUC-H', 'GBTpc-EUC-V', 'GBpc-EUC-H', 'GBpc-EUC-V', 'H', 'HKdla-B5-H', 'HKdla-B5-V', 'HKdlb-B5-H', 'HKdlb-B5-V', 'HKgccs-B5-H', 'HKgccs-B5-V', 'HKm314-B5-H', 'HKm314-B5-V', 'HKm471-B5-H', 'HKm471-B5-V', 'HKscs-B5-H', 'HKscs-B5-V', 'Hankaku', 'Hiragana', 'KSC-EUC-H', 'KSC-EUC-V', 'KSC-H', 'KSC-Johab-H', 'KSC-Johab-V', 'KSC-V', 'KSCms-UHC-H', 'KSCms-UHC-HW-H', 'KSCms-UHC-HW-V', 'KSCms-UHC-V', 'KSCpc-EUC-H', 'KSCpc-EUC-V', 'Katakana', 'NWP-H', 'NWP-V', 'RKSJ-H', 'RKSJ-V', 'Roman', 'UniCNS-UCS2-H', 'UniCNS-UCS2-V', 'UniCNS-UTF16-H', 'UniCNS-UTF16-V', 'UniCNS-UTF32-H', 'UniCNS-UTF32-V', 'UniCNS-UTF8-H', 'UniCNS-UTF8-V', 'UniGB-UCS2-H', 'UniGB-UCS2-V', 'UniGB-UTF16-H', 'UniGB-UTF16-V', 'UniGB-UTF32-H', 'UniGB-UTF32-V', 'UniGB-UTF8-H', 'UniGB-UTF8-V', 'UniJIS-UCS2-H', 'UniJIS-UCS2-HW-H', 'UniJIS-UCS2-HW-V', 'UniJIS-UCS2-V', 'UniJIS-UTF16-H', 'UniJIS-UTF16-V', 'UniJIS-UTF32-H', 'UniJIS-UTF32-V', 'UniJIS-UTF8-H', 'UniJIS-UTF8-V', 'UniJIS2004-UTF16-H', 'UniJIS2004-UTF16-V', 'UniJIS2004-UTF32-H', 'UniJIS2004-UTF32-V', 'UniJIS2004-UTF8-H', 'UniJIS2004-UTF8-V', 'UniJISPro-UCS2-HW-V', 'UniJISPro-UCS2-V', 'UniJISPro-UTF8-V', 'UniJISX0213-UTF32-H', 'UniJISX0213-UTF32-V', 'UniJISX02132004-UTF32-H', 'UniJISX02132004-UTF32-V', 'UniKS-UCS2-H', 'UniKS-UCS2-V', 'UniKS-UTF16-H', 'UniKS-UTF16-V', 'UniKS-UTF32-H', 'UniKS-UTF32-V', 'UniKS-UTF8-H', 'UniKS-UTF8-V', 'V', 'WP-Symbol'];
+
+var CMap =
+/*#__PURE__*/
+function () {
+ function CMap() {
+ var builtInCMap = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+
+ _classCallCheck(this, CMap);
+
+ this.codespaceRanges = [[], [], [], []];
+ this.numCodespaceRanges = 0;
+ this._map = [];
+ this.name = '';
+ this.vertical = false;
+ this.useCMap = null;
+ this.builtInCMap = builtInCMap;
+ }
+
+ _createClass(CMap, [{
+ key: "addCodespaceRange",
+ value: function addCodespaceRange(n, low, high) {
+ this.codespaceRanges[n - 1].push(low, high);
+ this.numCodespaceRanges++;
+ }
+ }, {
+ key: "mapCidRange",
+ value: function mapCidRange(low, high, dstLow) {
+ while (low <= high) {
+ this._map[low++] = dstLow++;
+ }
+ }
+ }, {
+ key: "mapBfRange",
+ value: function mapBfRange(low, high, dstLow) {
+ var lastByte = dstLow.length - 1;
+
+ while (low <= high) {
+ this._map[low++] = dstLow;
+ dstLow = dstLow.substring(0, lastByte) + String.fromCharCode(dstLow.charCodeAt(lastByte) + 1);
+ }
+ }
+ }, {
+ key: "mapBfRangeToArray",
+ value: function mapBfRangeToArray(low, high, array) {
+ var i = 0,
+ ii = array.length;
+
+ while (low <= high && i < ii) {
+ this._map[low] = array[i++];
+ ++low;
+ }
+ }
+ }, {
+ key: "mapOne",
+ value: function mapOne(src, dst) {
+ this._map[src] = dst;
+ }
+ }, {
+ key: "lookup",
+ value: function lookup(code) {
+ return this._map[code];
+ }
+ }, {
+ key: "contains",
+ value: function contains(code) {
+ return this._map[code] !== undefined;
+ }
+ }, {
+ key: "forEach",
+ value: function forEach(callback) {
+ var map = this._map;
+ var length = map.length;
+
+ if (length <= 0x10000) {
+ for (var i = 0; i < length; i++) {
+ if (map[i] !== undefined) {
+ callback(i, map[i]);
+ }
+ }
+ } else {
+ for (var _i in map) {
+ callback(_i, map[_i]);
+ }
+ }
+ }
+ }, {
+ key: "charCodeOf",
+ value: function charCodeOf(value) {
+ var map = this._map;
+
+ if (map.length <= 0x10000) {
+ return map.indexOf(value);
+ }
+
+ for (var charCode in map) {
+ if (map[charCode] === value) {
+ return charCode | 0;
+ }
+ }
+
+ return -1;
+ }
+ }, {
+ key: "getMap",
+ value: function getMap() {
+ return this._map;
+ }
+ }, {
+ key: "readCharCode",
+ value: function readCharCode(str, offset, out) {
+ var c = 0;
+ var codespaceRanges = this.codespaceRanges;
+
+ for (var n = 0, nn = codespaceRanges.length; n < nn; n++) {
+ c = (c << 8 | str.charCodeAt(offset + n)) >>> 0;
+ var codespaceRange = codespaceRanges[n];
+
+ for (var k = 0, kk = codespaceRange.length; k < kk;) {
+ var low = codespaceRange[k++];
+ var high = codespaceRange[k++];
+
+ if (c >= low && c <= high) {
+ out.charcode = c;
+ out.length = n + 1;
+ return;
+ }
+ }
+ }
+
+ out.charcode = 0;
+ out.length = 1;
+ }
+ }, {
+ key: "length",
+ get: function get() {
+ return this._map.length;
+ }
+ }, {
+ key: "isIdentityCMap",
+ get: function get() {
+ if (!(this.name === 'Identity-H' || this.name === 'Identity-V')) {
+ return false;
+ }
+
+ if (this._map.length !== 0x10000) {
+ return false;
+ }
+
+ for (var i = 0; i < 0x10000; i++) {
+ if (this._map[i] !== i) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }]);
+
+ return CMap;
+}();
+
+exports.CMap = CMap;
+
+var IdentityCMap =
+/*#__PURE__*/
+function (_CMap) {
+ _inherits(IdentityCMap, _CMap);
+
+ function IdentityCMap(vertical, n) {
+ var _this;
+
+ _classCallCheck(this, IdentityCMap);
+
+ _this = _possibleConstructorReturn(this, _getPrototypeOf(IdentityCMap).call(this));
+ _this.vertical = vertical;
+
+ _this.addCodespaceRange(n, 0, 0xffff);
+
+ return _this;
+ }
+
+ _createClass(IdentityCMap, [{
+ key: "mapCidRange",
+ value: function mapCidRange(low, high, dstLow) {
+ (0, _util.unreachable)('should not call mapCidRange');
+ }
+ }, {
+ key: "mapBfRange",
+ value: function mapBfRange(low, high, dstLow) {
+ (0, _util.unreachable)('should not call mapBfRange');
+ }
+ }, {
+ key: "mapBfRangeToArray",
+ value: function mapBfRangeToArray(low, high, array) {
+ (0, _util.unreachable)('should not call mapBfRangeToArray');
+ }
+ }, {
+ key: "mapOne",
+ value: function mapOne(src, dst) {
+ (0, _util.unreachable)('should not call mapCidOne');
+ }
+ }, {
+ key: "lookup",
+ value: function lookup(code) {
+ return Number.isInteger(code) && code <= 0xffff ? code : undefined;
+ }
+ }, {
+ key: "contains",
+ value: function contains(code) {
+ return Number.isInteger(code) && code <= 0xffff;
+ }
+ }, {
+ key: "forEach",
+ value: function forEach(callback) {
+ for (var i = 0; i <= 0xffff; i++) {
+ callback(i, i);
+ }
+ }
+ }, {
+ key: "charCodeOf",
+ value: function charCodeOf(value) {
+ return Number.isInteger(value) && value <= 0xffff ? value : -1;
+ }
+ }, {
+ key: "getMap",
+ value: function getMap() {
+ var map = new Array(0x10000);
+
+ for (var i = 0; i <= 0xffff; i++) {
+ map[i] = i;
+ }
+
+ return map;
+ }
+ }, {
+ key: "length",
+ get: function get() {
+ return 0x10000;
+ }
+ }, {
+ key: "isIdentityCMap",
+ get: function get() {
+ (0, _util.unreachable)('should not access .isIdentityCMap');
+ }
+ }]);
+
+ return IdentityCMap;
+}(CMap);
+
+exports.IdentityCMap = IdentityCMap;
+
+var BinaryCMapReader = function BinaryCMapReaderClosure() {
+ function hexToInt(a, size) {
+ var n = 0;
+
+ for (var i = 0; i <= size; i++) {
+ n = n << 8 | a[i];
+ }
+
+ return n >>> 0;
+ }
+
+ function hexToStr(a, size) {
+ if (size === 1) {
+ return String.fromCharCode(a[0], a[1]);
+ }
+
+ if (size === 3) {
+ return String.fromCharCode(a[0], a[1], a[2], a[3]);
+ }
+
+ return String.fromCharCode.apply(null, a.subarray(0, size + 1));
+ }
+
+ function addHex(a, b, size) {
+ var c = 0;
+
+ for (var i = size; i >= 0; i--) {
+ c += a[i] + b[i];
+ a[i] = c & 255;
+ c >>= 8;
+ }
+ }
+
+ function incHex(a, size) {
+ var c = 1;
+
+ for (var i = size; i >= 0 && c > 0; i--) {
+ c += a[i];
+ a[i] = c & 255;
+ c >>= 8;
+ }
+ }
+
+ var MAX_NUM_SIZE = 16;
+ var MAX_ENCODED_NUM_SIZE = 19;
+
+ function BinaryCMapStream(data) {
+ this.buffer = data;
+ this.pos = 0;
+ this.end = data.length;
+ this.tmpBuf = new Uint8Array(MAX_ENCODED_NUM_SIZE);
+ }
+
+ BinaryCMapStream.prototype = {
+ readByte: function readByte() {
+ if (this.pos >= this.end) {
+ return -1;
+ }
+
+ return this.buffer[this.pos++];
+ },
+ readNumber: function readNumber() {
+ var n = 0;
+ var last;
+
+ do {
+ var b = this.readByte();
+
+ if (b < 0) {
+ throw new _util.FormatError('unexpected EOF in bcmap');
+ }
+
+ last = !(b & 0x80);
+ n = n << 7 | b & 0x7F;
+ } while (!last);
+
+ return n;
+ },
+ readSigned: function readSigned() {
+ var n = this.readNumber();
+ return n & 1 ? ~(n >>> 1) : n >>> 1;
+ },
+ readHex: function readHex(num, size) {
+ num.set(this.buffer.subarray(this.pos, this.pos + size + 1));
+ this.pos += size + 1;
+ },
+ readHexNumber: function readHexNumber(num, size) {
+ var last;
+ var stack = this.tmpBuf,
+ sp = 0;
+
+ do {
+ var b = this.readByte();
+
+ if (b < 0) {
+ throw new _util.FormatError('unexpected EOF in bcmap');
+ }
+
+ last = !(b & 0x80);
+ stack[sp++] = b & 0x7F;
+ } while (!last);
+
+ var i = size,
+ buffer = 0,
+ bufferSize = 0;
+
+ while (i >= 0) {
+ while (bufferSize < 8 && stack.length > 0) {
+ buffer = stack[--sp] << bufferSize | buffer;
+ bufferSize += 7;
+ }
+
+ num[i] = buffer & 255;
+ i--;
+ buffer >>= 8;
+ bufferSize -= 8;
+ }
+ },
+ readHexSigned: function readHexSigned(num, size) {
+ this.readHexNumber(num, size);
+ var sign = num[size] & 1 ? 255 : 0;
+ var c = 0;
+
+ for (var i = 0; i <= size; i++) {
+ c = (c & 1) << 8 | num[i];
+ num[i] = c >> 1 ^ sign;
+ }
+ },
+ readString: function readString() {
+ var len = this.readNumber();
+ var s = '';
+
+ for (var i = 0; i < len; i++) {
+ s += String.fromCharCode(this.readNumber());
+ }
+
+ return s;
+ }
+ };
+
+ function processBinaryCMap(data, cMap, extend) {
+ return new Promise(function (resolve, reject) {
+ var stream = new BinaryCMapStream(data);
+ var header = stream.readByte();
+ cMap.vertical = !!(header & 1);
+ var useCMap = null;
+ var start = new Uint8Array(MAX_NUM_SIZE);
+ var end = new Uint8Array(MAX_NUM_SIZE);
+
+ var _char = new Uint8Array(MAX_NUM_SIZE);
+
+ var charCode = new Uint8Array(MAX_NUM_SIZE);
+ var tmp = new Uint8Array(MAX_NUM_SIZE);
+ var code;
+ var b;
+
+ while ((b = stream.readByte()) >= 0) {
+ var type = b >> 5;
+
+ if (type === 7) {
+ switch (b & 0x1F) {
+ case 0:
+ stream.readString();
+ break;
+
+ case 1:
+ useCMap = stream.readString();
+ break;
+ }
+
+ continue;
+ }
+
+ var sequence = !!(b & 0x10);
+ var dataSize = b & 15;
+
+ if (dataSize + 1 > MAX_NUM_SIZE) {
+ throw new Error('processBinaryCMap: Invalid dataSize.');
+ }
+
+ var ucs2DataSize = 1;
+ var subitemsCount = stream.readNumber();
+ var i;
+
+ switch (type) {
+ case 0:
+ stream.readHex(start, dataSize);
+ stream.readHexNumber(end, dataSize);
+ addHex(end, start, dataSize);
+ cMap.addCodespaceRange(dataSize + 1, hexToInt(start, dataSize), hexToInt(end, dataSize));
+
+ for (i = 1; i < subitemsCount; i++) {
+ incHex(end, dataSize);
+ stream.readHexNumber(start, dataSize);
+ addHex(start, end, dataSize);
+ stream.readHexNumber(end, dataSize);
+ addHex(end, start, dataSize);
+ cMap.addCodespaceRange(dataSize + 1, hexToInt(start, dataSize), hexToInt(end, dataSize));
+ }
+
+ break;
+
+ case 1:
+ stream.readHex(start, dataSize);
+ stream.readHexNumber(end, dataSize);
+ addHex(end, start, dataSize);
+ stream.readNumber();
+
+ for (i = 1; i < subitemsCount; i++) {
+ incHex(end, dataSize);
+ stream.readHexNumber(start, dataSize);
+ addHex(start, end, dataSize);
+ stream.readHexNumber(end, dataSize);
+ addHex(end, start, dataSize);
+ stream.readNumber();
+ }
+
+ break;
+
+ case 2:
+ stream.readHex(_char, dataSize);
+ code = stream.readNumber();
+ cMap.mapOne(hexToInt(_char, dataSize), code);
+
+ for (i = 1; i < subitemsCount; i++) {
+ incHex(_char, dataSize);
+
+ if (!sequence) {
+ stream.readHexNumber(tmp, dataSize);
+ addHex(_char, tmp, dataSize);
+ }
+
+ code = stream.readSigned() + (code + 1);
+ cMap.mapOne(hexToInt(_char, dataSize), code);
+ }
+
+ break;
+
+ case 3:
+ stream.readHex(start, dataSize);
+ stream.readHexNumber(end, dataSize);
+ addHex(end, start, dataSize);
+ code = stream.readNumber();
+ cMap.mapCidRange(hexToInt(start, dataSize), hexToInt(end, dataSize), code);
+
+ for (i = 1; i < subitemsCount; i++) {
+ incHex(end, dataSize);
+
+ if (!sequence) {
+ stream.readHexNumber(start, dataSize);
+ addHex(start, end, dataSize);
+ } else {
+ start.set(end);
+ }
+
+ stream.readHexNumber(end, dataSize);
+ addHex(end, start, dataSize);
+ code = stream.readNumber();
+ cMap.mapCidRange(hexToInt(start, dataSize), hexToInt(end, dataSize), code);
+ }
+
+ break;
+
+ case 4:
+ stream.readHex(_char, ucs2DataSize);
+ stream.readHex(charCode, dataSize);
+ cMap.mapOne(hexToInt(_char, ucs2DataSize), hexToStr(charCode, dataSize));
+
+ for (i = 1; i < subitemsCount; i++) {
+ incHex(_char, ucs2DataSize);
+
+ if (!sequence) {
+ stream.readHexNumber(tmp, ucs2DataSize);
+ addHex(_char, tmp, ucs2DataSize);
+ }
+
+ incHex(charCode, dataSize);
+ stream.readHexSigned(tmp, dataSize);
+ addHex(charCode, tmp, dataSize);
+ cMap.mapOne(hexToInt(_char, ucs2DataSize), hexToStr(charCode, dataSize));
+ }
+
+ break;
+
+ case 5:
+ stream.readHex(start, ucs2DataSize);
+ stream.readHexNumber(end, ucs2DataSize);
+ addHex(end, start, ucs2DataSize);
+ stream.readHex(charCode, dataSize);
+ cMap.mapBfRange(hexToInt(start, ucs2DataSize), hexToInt(end, ucs2DataSize), hexToStr(charCode, dataSize));
+
+ for (i = 1; i < subitemsCount; i++) {
+ incHex(end, ucs2DataSize);
+
+ if (!sequence) {
+ stream.readHexNumber(start, ucs2DataSize);
+ addHex(start, end, ucs2DataSize);
+ } else {
+ start.set(end);
+ }
+
+ stream.readHexNumber(end, ucs2DataSize);
+ addHex(end, start, ucs2DataSize);
+ stream.readHex(charCode, dataSize);
+ cMap.mapBfRange(hexToInt(start, ucs2DataSize), hexToInt(end, ucs2DataSize), hexToStr(charCode, dataSize));
+ }
+
+ break;
+
+ default:
+ reject(new Error('processBinaryCMap: Unknown type: ' + type));
+ return;
+ }
+ }
+
+ if (useCMap) {
+ resolve(extend(useCMap));
+ return;
+ }
+
+ resolve(cMap);
+ });
+ }
+
+ function BinaryCMapReader() {}
+
+ BinaryCMapReader.prototype = {
+ process: processBinaryCMap
+ };
+ return BinaryCMapReader;
+}();
+
+var CMapFactory = function CMapFactoryClosure() {
+ function strToInt(str) {
+ var a = 0;
+
+ for (var i = 0; i < str.length; i++) {
+ a = a << 8 | str.charCodeAt(i);
+ }
+
+ return a >>> 0;
+ }
+
+ function expectString(obj) {
+ if (!(0, _util.isString)(obj)) {
+ throw new _util.FormatError('Malformed CMap: expected string.');
+ }
+ }
+
+ function expectInt(obj) {
+ if (!Number.isInteger(obj)) {
+ throw new _util.FormatError('Malformed CMap: expected int.');
+ }
+ }
+
+ function parseBfChar(cMap, lexer) {
+ while (true) {
+ var obj = lexer.getObj();
+
+ if ((0, _primitives.isEOF)(obj)) {
+ break;
+ }
+
+ if ((0, _primitives.isCmd)(obj, 'endbfchar')) {
+ return;
+ }
+
+ expectString(obj);
+ var src = strToInt(obj);
+ obj = lexer.getObj();
+ expectString(obj);
+ var dst = obj;
+ cMap.mapOne(src, dst);
+ }
+ }
+
+ function parseBfRange(cMap, lexer) {
+ while (true) {
+ var obj = lexer.getObj();
+
+ if ((0, _primitives.isEOF)(obj)) {
+ break;
+ }
+
+ if ((0, _primitives.isCmd)(obj, 'endbfrange')) {
+ return;
+ }
+
+ expectString(obj);
+ var low = strToInt(obj);
+ obj = lexer.getObj();
+ expectString(obj);
+ var high = strToInt(obj);
+ obj = lexer.getObj();
+
+ if (Number.isInteger(obj) || (0, _util.isString)(obj)) {
+ var dstLow = Number.isInteger(obj) ? String.fromCharCode(obj) : obj;
+ cMap.mapBfRange(low, high, dstLow);
+ } else if ((0, _primitives.isCmd)(obj, '[')) {
+ obj = lexer.getObj();
+ var array = [];
+
+ while (!(0, _primitives.isCmd)(obj, ']') && !(0, _primitives.isEOF)(obj)) {
+ array.push(obj);
+ obj = lexer.getObj();
+ }
+
+ cMap.mapBfRangeToArray(low, high, array);
+ } else {
+ break;
+ }
+ }
+
+ throw new _util.FormatError('Invalid bf range.');
+ }
+
+ function parseCidChar(cMap, lexer) {
+ while (true) {
+ var obj = lexer.getObj();
+
+ if ((0, _primitives.isEOF)(obj)) {
+ break;
+ }
+
+ if ((0, _primitives.isCmd)(obj, 'endcidchar')) {
+ return;
+ }
+
+ expectString(obj);
+ var src = strToInt(obj);
+ obj = lexer.getObj();
+ expectInt(obj);
+ var dst = obj;
+ cMap.mapOne(src, dst);
+ }
+ }
+
+ function parseCidRange(cMap, lexer) {
+ while (true) {
+ var obj = lexer.getObj();
+
+ if ((0, _primitives.isEOF)(obj)) {
+ break;
+ }
+
+ if ((0, _primitives.isCmd)(obj, 'endcidrange')) {
+ return;
+ }
+
+ expectString(obj);
+ var low = strToInt(obj);
+ obj = lexer.getObj();
+ expectString(obj);
+ var high = strToInt(obj);
+ obj = lexer.getObj();
+ expectInt(obj);
+ var dstLow = obj;
+ cMap.mapCidRange(low, high, dstLow);
+ }
+ }
+
+ function parseCodespaceRange(cMap, lexer) {
+ while (true) {
+ var obj = lexer.getObj();
+
+ if ((0, _primitives.isEOF)(obj)) {
+ break;
+ }
+
+ if ((0, _primitives.isCmd)(obj, 'endcodespacerange')) {
+ return;
+ }
+
+ if (!(0, _util.isString)(obj)) {
+ break;
+ }
+
+ var low = strToInt(obj);
+ obj = lexer.getObj();
+
+ if (!(0, _util.isString)(obj)) {
+ break;
+ }
+
+ var high = strToInt(obj);
+ cMap.addCodespaceRange(obj.length, low, high);
+ }
+
+ throw new _util.FormatError('Invalid codespace range.');
+ }
+
+ function parseWMode(cMap, lexer) {
+ var obj = lexer.getObj();
+
+ if (Number.isInteger(obj)) {
+ cMap.vertical = !!obj;
+ }
+ }
+
+ function parseCMapName(cMap, lexer) {
+ var obj = lexer.getObj();
+
+ if ((0, _primitives.isName)(obj) && (0, _util.isString)(obj.name)) {
+ cMap.name = obj.name;
+ }
+ }
+
+ function parseCMap(cMap, lexer, fetchBuiltInCMap, useCMap) {
+ var previous;
+ var embeddedUseCMap;
+
+ objLoop: while (true) {
+ try {
+ var obj = lexer.getObj();
+
+ if ((0, _primitives.isEOF)(obj)) {
+ break;
+ } else if ((0, _primitives.isName)(obj)) {
+ if (obj.name === 'WMode') {
+ parseWMode(cMap, lexer);
+ } else if (obj.name === 'CMapName') {
+ parseCMapName(cMap, lexer);
+ }
+
+ previous = obj;
+ } else if ((0, _primitives.isCmd)(obj)) {
+ switch (obj.cmd) {
+ case 'endcmap':
+ break objLoop;
+
+ case 'usecmap':
+ if ((0, _primitives.isName)(previous)) {
+ embeddedUseCMap = previous.name;
+ }
+
+ break;
+
+ case 'begincodespacerange':
+ parseCodespaceRange(cMap, lexer);
+ break;
+
+ case 'beginbfchar':
+ parseBfChar(cMap, lexer);
+ break;
+
+ case 'begincidchar':
+ parseCidChar(cMap, lexer);
+ break;
+
+ case 'beginbfrange':
+ parseBfRange(cMap, lexer);
+ break;
+
+ case 'begincidrange':
+ parseCidRange(cMap, lexer);
+ break;
+ }
+ }
+ } catch (ex) {
+ if (ex instanceof _core_utils.MissingDataException) {
+ throw ex;
+ }
+
+ (0, _util.warn)('Invalid cMap data: ' + ex);
+ continue;
+ }
+ }
+
+ if (!useCMap && embeddedUseCMap) {
+ useCMap = embeddedUseCMap;
+ }
+
+ if (useCMap) {
+ return extendCMap(cMap, fetchBuiltInCMap, useCMap);
+ }
+
+ return Promise.resolve(cMap);
+ }
+
+ function extendCMap(cMap, fetchBuiltInCMap, useCMap) {
+ return createBuiltInCMap(useCMap, fetchBuiltInCMap).then(function (newCMap) {
+ cMap.useCMap = newCMap;
+
+ if (cMap.numCodespaceRanges === 0) {
+ var useCodespaceRanges = cMap.useCMap.codespaceRanges;
+
+ for (var i = 0; i < useCodespaceRanges.length; i++) {
+ cMap.codespaceRanges[i] = useCodespaceRanges[i].slice();
+ }
+
+ cMap.numCodespaceRanges = cMap.useCMap.numCodespaceRanges;
+ }
+
+ cMap.useCMap.forEach(function (key, value) {
+ if (!cMap.contains(key)) {
+ cMap.mapOne(key, cMap.useCMap.lookup(key));
+ }
+ });
+ return cMap;
+ });
+ }
+
+ function createBuiltInCMap(name, fetchBuiltInCMap) {
+ if (name === 'Identity-H') {
+ return Promise.resolve(new IdentityCMap(false, 2));
+ } else if (name === 'Identity-V') {
+ return Promise.resolve(new IdentityCMap(true, 2));
+ }
+
+ if (!BUILT_IN_CMAPS.includes(name)) {
+ return Promise.reject(new Error('Unknown CMap name: ' + name));
+ }
+
+ if (!fetchBuiltInCMap) {
+ return Promise.reject(new Error('Built-in CMap parameters are not provided.'));
+ }
+
+ return fetchBuiltInCMap(name).then(function (data) {
+ var cMapData = data.cMapData,
+ compressionType = data.compressionType;
+ var cMap = new CMap(true);
+
+ if (compressionType === _util.CMapCompressionType.BINARY) {
+ return new BinaryCMapReader().process(cMapData, cMap, function (useCMap) {
+ return extendCMap(cMap, fetchBuiltInCMap, useCMap);
+ });
+ }
+
+ if (compressionType === _util.CMapCompressionType.NONE) {
+ var lexer = new _parser.Lexer(new _stream.Stream(cMapData));
+ return parseCMap(cMap, lexer, fetchBuiltInCMap, null);
+ }
+
+ return Promise.reject(new Error('TODO: Only BINARY/NONE CMap compression is currently supported.'));
+ });
+ }
+
+ return {
+ create: function create(params) {
+ var encoding = params.encoding;
+ var fetchBuiltInCMap = params.fetchBuiltInCMap;
+ var useCMap = params.useCMap;
+
+ if ((0, _primitives.isName)(encoding)) {
+ return createBuiltInCMap(encoding.name, fetchBuiltInCMap);
+ } else if ((0, _primitives.isStream)(encoding)) {
+ var cMap = new CMap();
+ var lexer = new _parser.Lexer(encoding);
+ return parseCMap(cMap, lexer, fetchBuiltInCMap, useCMap).then(function (parsedCMap) {
+ if (parsedCMap.isIdentityCMap) {
+ return createBuiltInCMap(parsedCMap.name, fetchBuiltInCMap);
+ }
+
+ return parsedCMap;
+ });
+ }
+
+ return Promise.reject(new Error('Encoding required.'));
+ }
+ };
+}();
+
+exports.CMapFactory = CMapFactory;
+
+/***/ }),
+/* 174 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.getFontType = getFontType;
+exports.IdentityToUnicodeMap = exports.ToUnicodeMap = exports.FontFlags = exports.Font = exports.ErrorFont = exports.SEAC_ANALYSIS_ENABLED = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _cff_parser = __w_pdfjs_require__(175);
+
+var _glyphlist = __w_pdfjs_require__(178);
+
+var _encodings = __w_pdfjs_require__(177);
+
+var _standard_fonts = __w_pdfjs_require__(179);
+
+var _unicode = __w_pdfjs_require__(180);
+
+var _font_renderer = __w_pdfjs_require__(181);
+
+var _cmap = __w_pdfjs_require__(173);
+
+var _core_utils = __w_pdfjs_require__(154);
+
+var _stream = __w_pdfjs_require__(158);
+
+var _type1_parser = __w_pdfjs_require__(182);
+
+function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
+
+function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
+
+function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
+
+function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
+
+var PRIVATE_USE_AREAS = [[0xE000, 0xF8FF], [0x100000, 0x10FFFD]];
+var PDF_GLYPH_SPACE_UNITS = 1000;
+var SEAC_ANALYSIS_ENABLED = true;
+exports.SEAC_ANALYSIS_ENABLED = SEAC_ANALYSIS_ENABLED;
+var FontFlags = {
+ FixedPitch: 1,
+ Serif: 2,
+ Symbolic: 4,
+ Script: 8,
+ Nonsymbolic: 32,
+ Italic: 64,
+ AllCap: 65536,
+ SmallCap: 131072,
+ ForceBold: 262144
+};
+exports.FontFlags = FontFlags;
+var MacStandardGlyphOrdering = ['.notdef', '.null', 'nonmarkingreturn', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore', 'grave', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', 'Adieresis', 'Aring', 'Ccedilla', 'Eacute', 'Ntilde', 'Odieresis', 'Udieresis', 'aacute', 'agrave', 'acircumflex', 'adieresis', 'atilde', 'aring', 'ccedilla', 'eacute', 'egrave', 'ecircumflex', 'edieresis', 'iacute', 'igrave', 'icircumflex', 'idieresis', 'ntilde', 'oacute', 'ograve', 'ocircumflex', 'odieresis', 'otilde', 'uacute', 'ugrave', 'ucircumflex', 'udieresis', 'dagger', 'degree', 'cent', 'sterling', 'section', 'bullet', 'paragraph', 'germandbls', 'registered', 'copyright', 'trademark', 'acute', 'dieresis', 'notequal', 'AE', 'Oslash', 'infinity', 'plusminus', 'lessequal', 'greaterequal', 'yen', 'mu', 'partialdiff', 'summation', 'product', 'pi', 'integral', 'ordfeminine', 'ordmasculine', 'Omega', 'ae', 'oslash', 'questiondown', 'exclamdown', 'logicalnot', 'radical', 'florin', 'approxequal', 'Delta', 'guillemotleft', 'guillemotright', 'ellipsis', 'nonbreakingspace', 'Agrave', 'Atilde', 'Otilde', 'OE', 'oe', 'endash', 'emdash', 'quotedblleft', 'quotedblright', 'quoteleft', 'quoteright', 'divide', 'lozenge', 'ydieresis', 'Ydieresis', 'fraction', 'currency', 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'daggerdbl', 'periodcentered', 'quotesinglbase', 'quotedblbase', 'perthousand', 'Acircumflex', 'Ecircumflex', 'Aacute', 'Edieresis', 'Egrave', 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Oacute', 'Ocircumflex', 'apple', 'Ograve', 'Uacute', 'Ucircumflex', 'Ugrave', 'dotlessi', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron', 'Lslash', 'lslash', 'Scaron', 'scaron', 'Zcaron', 'zcaron', 'brokenbar', 'Eth', 'eth', 'Yacute', 'yacute', 'Thorn', 'thorn', 'minus', 'multiply', 'onesuperior', 'twosuperior', 'threesuperior', 'onehalf', 'onequarter', 'threequarters', 'franc', 'Gbreve', 'gbreve', 'Idotaccent', 'Scedilla', 'scedilla', 'Cacute', 'cacute', 'Ccaron', 'ccaron', 'dcroat'];
+
+function adjustWidths(properties) {
+ if (!properties.fontMatrix) {
+ return;
+ }
+
+ if (properties.fontMatrix[0] === _util.FONT_IDENTITY_MATRIX[0]) {
+ return;
+ }
+
+ var scale = 0.001 / properties.fontMatrix[0];
+ var glyphsWidths = properties.widths;
+
+ for (var glyph in glyphsWidths) {
+ glyphsWidths[glyph] *= scale;
+ }
+
+ properties.defaultWidth *= scale;
+}
+
+function adjustToUnicode(properties, builtInEncoding) {
+ if (properties.hasIncludedToUnicodeMap) {
+ return;
+ }
+
+ if (properties.hasEncoding) {
+ return;
+ }
+
+ if (builtInEncoding === properties.defaultEncoding) {
+ return;
+ }
+
+ if (properties.toUnicode instanceof IdentityToUnicodeMap) {
+ return;
+ }
+
+ var toUnicode = [],
+ glyphsUnicodeMap = (0, _glyphlist.getGlyphsUnicode)();
+
+ for (var charCode in builtInEncoding) {
+ var glyphName = builtInEncoding[charCode];
+ var unicode = (0, _unicode.getUnicodeForGlyph)(glyphName, glyphsUnicodeMap);
+
+ if (unicode !== -1) {
+ toUnicode[charCode] = String.fromCharCode(unicode);
+ }
+ }
+
+ properties.toUnicode.amend(toUnicode);
+}
+
+function getFontType(type, subtype) {
+ switch (type) {
+ case 'Type1':
+ return subtype === 'Type1C' ? _util.FontType.TYPE1C : _util.FontType.TYPE1;
+
+ case 'CIDFontType0':
+ return subtype === 'CIDFontType0C' ? _util.FontType.CIDFONTTYPE0C : _util.FontType.CIDFONTTYPE0;
+
+ case 'OpenType':
+ return _util.FontType.OPENTYPE;
+
+ case 'TrueType':
+ return _util.FontType.TRUETYPE;
+
+ case 'CIDFontType2':
+ return _util.FontType.CIDFONTTYPE2;
+
+ case 'MMType1':
+ return _util.FontType.MMTYPE1;
+
+ case 'Type0':
+ return _util.FontType.TYPE0;
+
+ default:
+ return _util.FontType.UNKNOWN;
+ }
+}
+
+function recoverGlyphName(name, glyphsUnicodeMap) {
+ if (glyphsUnicodeMap[name] !== undefined) {
+ return name;
+ }
+
+ var unicode = (0, _unicode.getUnicodeForGlyph)(name, glyphsUnicodeMap);
+
+ if (unicode !== -1) {
+ for (var key in glyphsUnicodeMap) {
+ if (glyphsUnicodeMap[key] === unicode) {
+ return key;
+ }
+ }
+ }
+
+ (0, _util.info)('Unable to recover a standard glyph name for: ' + name);
+ return name;
+}
+
+var Glyph = function GlyphClosure() {
+ function Glyph(fontChar, unicode, accent, width, vmetric, operatorListId, isSpace, isInFont) {
+ this.fontChar = fontChar;
+ this.unicode = unicode;
+ this.accent = accent;
+ this.width = width;
+ this.vmetric = vmetric;
+ this.operatorListId = operatorListId;
+ this.isSpace = isSpace;
+ this.isInFont = isInFont;
+ }
+
+ Glyph.prototype.matchesForCache = function (fontChar, unicode, accent, width, vmetric, operatorListId, isSpace, isInFont) {
+ return this.fontChar === fontChar && this.unicode === unicode && this.accent === accent && this.width === width && this.vmetric === vmetric && this.operatorListId === operatorListId && this.isSpace === isSpace && this.isInFont === isInFont;
+ };
+
+ return Glyph;
+}();
+
+var ToUnicodeMap = function ToUnicodeMapClosure() {
+ function ToUnicodeMap() {
+ var cmap = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
+ this._map = cmap;
+ }
+
+ ToUnicodeMap.prototype = {
+ get length() {
+ return this._map.length;
+ },
+
+ forEach: function forEach(callback) {
+ for (var charCode in this._map) {
+ callback(charCode, this._map[charCode].charCodeAt(0));
+ }
+ },
+ has: function has(i) {
+ return this._map[i] !== undefined;
+ },
+ get: function get(i) {
+ return this._map[i];
+ },
+ charCodeOf: function charCodeOf(value) {
+ var map = this._map;
+
+ if (map.length <= 0x10000) {
+ return map.indexOf(value);
+ }
+
+ for (var charCode in map) {
+ if (map[charCode] === value) {
+ return charCode | 0;
+ }
+ }
+
+ return -1;
+ },
+ amend: function amend(map) {
+ for (var charCode in map) {
+ this._map[charCode] = map[charCode];
+ }
+ }
+ };
+ return ToUnicodeMap;
+}();
+
+exports.ToUnicodeMap = ToUnicodeMap;
+
+var IdentityToUnicodeMap = function IdentityToUnicodeMapClosure() {
+ function IdentityToUnicodeMap(firstChar, lastChar) {
+ this.firstChar = firstChar;
+ this.lastChar = lastChar;
+ }
+
+ IdentityToUnicodeMap.prototype = {
+ get length() {
+ return this.lastChar + 1 - this.firstChar;
+ },
+
+ forEach: function forEach(callback) {
+ for (var i = this.firstChar, ii = this.lastChar; i <= ii; i++) {
+ callback(i, i);
+ }
+ },
+ has: function has(i) {
+ return this.firstChar <= i && i <= this.lastChar;
+ },
+ get: function get(i) {
+ if (this.firstChar <= i && i <= this.lastChar) {
+ return String.fromCharCode(i);
+ }
+
+ return undefined;
+ },
+ charCodeOf: function charCodeOf(v) {
+ return Number.isInteger(v) && v >= this.firstChar && v <= this.lastChar ? v : -1;
+ },
+ amend: function amend(map) {
+ (0, _util.unreachable)('Should not call amend()');
+ }
+ };
+ return IdentityToUnicodeMap;
+}();
+
+exports.IdentityToUnicodeMap = IdentityToUnicodeMap;
+
+var OpenTypeFileBuilder = function OpenTypeFileBuilderClosure() {
+ function writeInt16(dest, offset, num) {
+ dest[offset] = num >> 8 & 0xFF;
+ dest[offset + 1] = num & 0xFF;
+ }
+
+ function writeInt32(dest, offset, num) {
+ dest[offset] = num >> 24 & 0xFF;
+ dest[offset + 1] = num >> 16 & 0xFF;
+ dest[offset + 2] = num >> 8 & 0xFF;
+ dest[offset + 3] = num & 0xFF;
+ }
+
+ function writeData(dest, offset, data) {
+ var i, ii;
+
+ if (data instanceof Uint8Array) {
+ dest.set(data, offset);
+ } else if (typeof data === 'string') {
+ for (i = 0, ii = data.length; i < ii; i++) {
+ dest[offset++] = data.charCodeAt(i) & 0xFF;
+ }
+ } else {
+ for (i = 0, ii = data.length; i < ii; i++) {
+ dest[offset++] = data[i] & 0xFF;
+ }
+ }
+ }
+
+ function OpenTypeFileBuilder(sfnt) {
+ this.sfnt = sfnt;
+ this.tables = Object.create(null);
+ }
+
+ OpenTypeFileBuilder.getSearchParams = function OpenTypeFileBuilder_getSearchParams(entriesCount, entrySize) {
+ var maxPower2 = 1,
+ log2 = 0;
+
+ while ((maxPower2 ^ entriesCount) > maxPower2) {
+ maxPower2 <<= 1;
+ log2++;
+ }
+
+ var searchRange = maxPower2 * entrySize;
+ return {
+ range: searchRange,
+ entry: log2,
+ rangeShift: entrySize * entriesCount - searchRange
+ };
+ };
+
+ var OTF_HEADER_SIZE = 12;
+ var OTF_TABLE_ENTRY_SIZE = 16;
+ OpenTypeFileBuilder.prototype = {
+ toArray: function OpenTypeFileBuilder_toArray() {
+ var sfnt = this.sfnt;
+ var tables = this.tables;
+ var tablesNames = Object.keys(tables);
+ tablesNames.sort();
+ var numTables = tablesNames.length;
+ var i, j, jj, table, tableName;
+ var offset = OTF_HEADER_SIZE + numTables * OTF_TABLE_ENTRY_SIZE;
+ var tableOffsets = [offset];
+
+ for (i = 0; i < numTables; i++) {
+ table = tables[tablesNames[i]];
+ var paddedLength = (table.length + 3 & ~3) >>> 0;
+ offset += paddedLength;
+ tableOffsets.push(offset);
+ }
+
+ var file = new Uint8Array(offset);
+
+ for (i = 0; i < numTables; i++) {
+ table = tables[tablesNames[i]];
+ writeData(file, tableOffsets[i], table);
+ }
+
+ if (sfnt === 'true') {
+ sfnt = (0, _util.string32)(0x00010000);
+ }
+
+ file[0] = sfnt.charCodeAt(0) & 0xFF;
+ file[1] = sfnt.charCodeAt(1) & 0xFF;
+ file[2] = sfnt.charCodeAt(2) & 0xFF;
+ file[3] = sfnt.charCodeAt(3) & 0xFF;
+ writeInt16(file, 4, numTables);
+ var searchParams = OpenTypeFileBuilder.getSearchParams(numTables, 16);
+ writeInt16(file, 6, searchParams.range);
+ writeInt16(file, 8, searchParams.entry);
+ writeInt16(file, 10, searchParams.rangeShift);
+ offset = OTF_HEADER_SIZE;
+
+ for (i = 0; i < numTables; i++) {
+ tableName = tablesNames[i];
+ file[offset] = tableName.charCodeAt(0) & 0xFF;
+ file[offset + 1] = tableName.charCodeAt(1) & 0xFF;
+ file[offset + 2] = tableName.charCodeAt(2) & 0xFF;
+ file[offset + 3] = tableName.charCodeAt(3) & 0xFF;
+ var checksum = 0;
+
+ for (j = tableOffsets[i], jj = tableOffsets[i + 1]; j < jj; j += 4) {
+ var quad = (0, _util.readUint32)(file, j);
+ checksum = checksum + quad >>> 0;
+ }
+
+ writeInt32(file, offset + 4, checksum);
+ writeInt32(file, offset + 8, tableOffsets[i]);
+ writeInt32(file, offset + 12, tables[tableName].length);
+ offset += OTF_TABLE_ENTRY_SIZE;
+ }
+
+ return file;
+ },
+ addTable: function OpenTypeFileBuilder_addTable(tag, data) {
+ if (tag in this.tables) {
+ throw new Error('Table ' + tag + ' already exists');
+ }
+
+ this.tables[tag] = data;
+ }
+ };
+ return OpenTypeFileBuilder;
+}();
+
+var Font = function FontClosure() {
+ function Font(name, file, properties) {
+ var charCode;
+ this.name = name;
+ this.loadedName = properties.loadedName;
+ this.isType3Font = properties.isType3Font;
+ this.sizes = [];
+ this.missingFile = false;
+ this.glyphCache = Object.create(null);
+ this.isSerifFont = !!(properties.flags & FontFlags.Serif);
+ this.isSymbolicFont = !!(properties.flags & FontFlags.Symbolic);
+ this.isMonospace = !!(properties.flags & FontFlags.FixedPitch);
+ var type = properties.type;
+ var subtype = properties.subtype;
+ this.type = type;
+ this.subtype = subtype;
+ this.fallbackName = this.isMonospace ? 'monospace' : this.isSerifFont ? 'serif' : 'sans-serif';
+ this.differences = properties.differences;
+ this.widths = properties.widths;
+ this.defaultWidth = properties.defaultWidth;
+ this.composite = properties.composite;
+ this.wideChars = properties.wideChars;
+ this.cMap = properties.cMap;
+ this.ascent = properties.ascent / PDF_GLYPH_SPACE_UNITS;
+ this.descent = properties.descent / PDF_GLYPH_SPACE_UNITS;
+ this.fontMatrix = properties.fontMatrix;
+ this.bbox = properties.bbox;
+ this.defaultEncoding = properties.defaultEncoding;
+ this.toUnicode = properties.toUnicode;
+ this.fallbackToUnicode = properties.fallbackToUnicode || new ToUnicodeMap();
+ this.toFontChar = [];
+
+ if (properties.type === 'Type3') {
+ for (charCode = 0; charCode < 256; charCode++) {
+ this.toFontChar[charCode] = this.differences[charCode] || properties.defaultEncoding[charCode];
+ }
+
+ this.fontType = _util.FontType.TYPE3;
+ return;
+ }
+
+ this.cidEncoding = properties.cidEncoding;
+ this.vertical = properties.vertical;
+
+ if (this.vertical) {
+ this.vmetrics = properties.vmetrics;
+ this.defaultVMetrics = properties.defaultVMetrics;
+ }
+
+ if (!file || file.isEmpty) {
+ if (file) {
+ (0, _util.warn)('Font file is empty in "' + name + '" (' + this.loadedName + ')');
+ }
+
+ this.fallbackToSystemFont();
+ return;
+ }
+
+ var _getFontFileType = getFontFileType(file, properties);
+
+ var _getFontFileType2 = _slicedToArray(_getFontFileType, 2);
+
+ type = _getFontFileType2[0];
+ subtype = _getFontFileType2[1];
+
+ if (type !== this.type || subtype !== this.subtype) {
+ (0, _util.info)('Inconsistent font file Type/SubType, expected: ' + "".concat(this.type, "/").concat(this.subtype, " but found: ").concat(type, "/").concat(subtype, "."));
+ }
+
+ try {
+ var data;
+
+ switch (type) {
+ case 'MMType1':
+ (0, _util.info)('MMType1 font (' + name + '), falling back to Type1.');
+
+ case 'Type1':
+ case 'CIDFontType0':
+ this.mimetype = 'font/opentype';
+ var cff = subtype === 'Type1C' || subtype === 'CIDFontType0C' ? new CFFFont(file, properties) : new Type1Font(name, file, properties);
+ adjustWidths(properties);
+ data = this.convert(name, cff, properties);
+ break;
+
+ case 'OpenType':
+ case 'TrueType':
+ case 'CIDFontType2':
+ this.mimetype = 'font/opentype';
+ data = this.checkAndRepair(name, file, properties);
+
+ if (this.isOpenType) {
+ adjustWidths(properties);
+ type = 'OpenType';
+ }
+
+ break;
+
+ default:
+ throw new _util.FormatError("Font ".concat(type, " is not supported"));
+ }
+ } catch (e) {
+ (0, _util.warn)(e);
+ this.fallbackToSystemFont();
+ return;
+ }
+
+ this.data = data;
+ this.fontType = getFontType(type, subtype);
+ this.fontMatrix = properties.fontMatrix;
+ this.widths = properties.widths;
+ this.defaultWidth = properties.defaultWidth;
+ this.toUnicode = properties.toUnicode;
+ this.encoding = properties.baseEncoding;
+ this.seacMap = properties.seacMap;
+ }
+
+ Font.getFontID = function () {
+ var ID = 1;
+ return function Font_getFontID() {
+ return String(ID++);
+ };
+ }();
+
+ function int16(b0, b1) {
+ return (b0 << 8) + b1;
+ }
+
+ function writeSignedInt16(bytes, index, value) {
+ bytes[index + 1] = value;
+ bytes[index] = value >>> 8;
+ }
+
+ function signedInt16(b0, b1) {
+ var value = (b0 << 8) + b1;
+ return value & 1 << 15 ? value - 0x10000 : value;
+ }
+
+ function int32(b0, b1, b2, b3) {
+ return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3;
+ }
+
+ function string16(value) {
+ return String.fromCharCode(value >> 8 & 0xff, value & 0xff);
+ }
+
+ function safeString16(value) {
+ value = value > 0x7FFF ? 0x7FFF : value < -0x8000 ? -0x8000 : value;
+ return String.fromCharCode(value >> 8 & 0xff, value & 0xff);
+ }
+
+ function isTrueTypeFile(file) {
+ var header = file.peekBytes(4);
+ return (0, _util.readUint32)(header, 0) === 0x00010000 || (0, _util.bytesToString)(header) === 'true';
+ }
+
+ function isTrueTypeCollectionFile(file) {
+ var header = file.peekBytes(4);
+ return (0, _util.bytesToString)(header) === 'ttcf';
+ }
+
+ function isOpenTypeFile(file) {
+ var header = file.peekBytes(4);
+ return (0, _util.bytesToString)(header) === 'OTTO';
+ }
+
+ function isType1File(file) {
+ var header = file.peekBytes(2);
+
+ if (header[0] === 0x25 && header[1] === 0x21) {
+ return true;
+ }
+
+ if (header[0] === 0x80 && header[1] === 0x01) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function isCFFFile(file) {
+ var header = file.peekBytes(4);
+
+ if (header[0] >= 1 && header[3] >= 1 && header[3] <= 4) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function getFontFileType(file, _ref) {
+ var type = _ref.type,
+ subtype = _ref.subtype,
+ composite = _ref.composite;
+ var fileType, fileSubtype;
+
+ if (isTrueTypeFile(file) || isTrueTypeCollectionFile(file)) {
+ if (composite) {
+ fileType = 'CIDFontType2';
+ } else {
+ fileType = 'TrueType';
+ }
+ } else if (isOpenTypeFile(file)) {
+ if (composite) {
+ fileType = 'CIDFontType2';
+ } else {
+ fileType = 'OpenType';
+ }
+ } else if (isType1File(file)) {
+ if (composite) {
+ fileType = 'CIDFontType0';
+ } else {
+ fileType = type === 'MMType1' ? 'MMType1' : 'Type1';
+ }
+ } else if (isCFFFile(file)) {
+ if (composite) {
+ fileType = 'CIDFontType0';
+ fileSubtype = 'CIDFontType0C';
+ } else {
+ fileType = type === 'MMType1' ? 'MMType1' : 'Type1';
+ fileSubtype = 'Type1C';
+ }
+ } else {
+ (0, _util.warn)('getFontFileType: Unable to detect correct font file Type/Subtype.');
+ fileType = type;
+ fileSubtype = subtype;
+ }
+
+ return [fileType, fileSubtype];
+ }
+
+ function buildToFontChar(encoding, glyphsUnicodeMap, differences) {
+ var toFontChar = [],
+ unicode;
+
+ for (var i = 0, ii = encoding.length; i < ii; i++) {
+ unicode = (0, _unicode.getUnicodeForGlyph)(encoding[i], glyphsUnicodeMap);
+
+ if (unicode !== -1) {
+ toFontChar[i] = unicode;
+ }
+ }
+
+ for (var charCode in differences) {
+ unicode = (0, _unicode.getUnicodeForGlyph)(differences[charCode], glyphsUnicodeMap);
+
+ if (unicode !== -1) {
+ toFontChar[+charCode] = unicode;
+ }
+ }
+
+ return toFontChar;
+ }
+
+ function adjustMapping(charCodeToGlyphId, hasGlyph, newGlyphZeroId) {
+ var newMap = Object.create(null);
+ var toFontChar = [];
+ var privateUseAreaIndex = 0;
+ var nextAvailableFontCharCode = PRIVATE_USE_AREAS[privateUseAreaIndex][0];
+ var privateUseOffetEnd = PRIVATE_USE_AREAS[privateUseAreaIndex][1];
+
+ for (var originalCharCode in charCodeToGlyphId) {
+ originalCharCode |= 0;
+ var glyphId = charCodeToGlyphId[originalCharCode];
+
+ if (!hasGlyph(glyphId)) {
+ continue;
+ }
+
+ if (nextAvailableFontCharCode > privateUseOffetEnd) {
+ privateUseAreaIndex++;
+
+ if (privateUseAreaIndex >= PRIVATE_USE_AREAS.length) {
+ (0, _util.warn)('Ran out of space in font private use area.');
+ break;
+ }
+
+ nextAvailableFontCharCode = PRIVATE_USE_AREAS[privateUseAreaIndex][0];
+ privateUseOffetEnd = PRIVATE_USE_AREAS[privateUseAreaIndex][1];
+ }
+
+ var fontCharCode = nextAvailableFontCharCode++;
+
+ if (glyphId === 0) {
+ glyphId = newGlyphZeroId;
+ }
+
+ newMap[fontCharCode] = glyphId;
+ toFontChar[originalCharCode] = fontCharCode;
+ }
+
+ return {
+ toFontChar: toFontChar,
+ charCodeToGlyphId: newMap,
+ nextAvailableFontCharCode: nextAvailableFontCharCode
+ };
+ }
+
+ function getRanges(glyphs, numGlyphs) {
+ var codes = [];
+
+ for (var charCode in glyphs) {
+ if (glyphs[charCode] >= numGlyphs) {
+ continue;
+ }
+
+ codes.push({
+ fontCharCode: charCode | 0,
+ glyphId: glyphs[charCode]
+ });
+ }
+
+ if (codes.length === 0) {
+ codes.push({
+ fontCharCode: 0,
+ glyphId: 0
+ });
+ }
+
+ codes.sort(function fontGetRangesSort(a, b) {
+ return a.fontCharCode - b.fontCharCode;
+ });
+ var ranges = [];
+ var length = codes.length;
+
+ for (var n = 0; n < length;) {
+ var start = codes[n].fontCharCode;
+ var codeIndices = [codes[n].glyphId];
+ ++n;
+ var end = start;
+
+ while (n < length && end + 1 === codes[n].fontCharCode) {
+ codeIndices.push(codes[n].glyphId);
+ ++end;
+ ++n;
+
+ if (end === 0xFFFF) {
+ break;
+ }
+ }
+
+ ranges.push([start, end, codeIndices]);
+ }
+
+ return ranges;
+ }
+
+ function createCmapTable(glyphs, numGlyphs) {
+ var ranges = getRanges(glyphs, numGlyphs);
+ var numTables = ranges[ranges.length - 1][1] > 0xFFFF ? 2 : 1;
+ var cmap = '\x00\x00' + string16(numTables) + '\x00\x03' + '\x00\x01' + (0, _util.string32)(4 + numTables * 8);
+ var i, ii, j, jj;
+
+ for (i = ranges.length - 1; i >= 0; --i) {
+ if (ranges[i][0] <= 0xFFFF) {
+ break;
+ }
+ }
+
+ var bmpLength = i + 1;
+
+ if (ranges[i][0] < 0xFFFF && ranges[i][1] === 0xFFFF) {
+ ranges[i][1] = 0xFFFE;
+ }
+
+ var trailingRangesCount = ranges[i][1] < 0xFFFF ? 1 : 0;
+ var segCount = bmpLength + trailingRangesCount;
+ var searchParams = OpenTypeFileBuilder.getSearchParams(segCount, 2);
+ var startCount = '';
+ var endCount = '';
+ var idDeltas = '';
+ var idRangeOffsets = '';
+ var glyphsIds = '';
+ var bias = 0;
+ var range, start, end, codes;
+
+ for (i = 0, ii = bmpLength; i < ii; i++) {
+ range = ranges[i];
+ start = range[0];
+ end = range[1];
+ startCount += string16(start);
+ endCount += string16(end);
+ codes = range[2];
+ var contiguous = true;
+
+ for (j = 1, jj = codes.length; j < jj; ++j) {
+ if (codes[j] !== codes[j - 1] + 1) {
+ contiguous = false;
+ break;
+ }
+ }
+
+ if (!contiguous) {
+ var offset = (segCount - i) * 2 + bias * 2;
+ bias += end - start + 1;
+ idDeltas += string16(0);
+ idRangeOffsets += string16(offset);
+
+ for (j = 0, jj = codes.length; j < jj; ++j) {
+ glyphsIds += string16(codes[j]);
+ }
+ } else {
+ var startCode = codes[0];
+ idDeltas += string16(startCode - start & 0xFFFF);
+ idRangeOffsets += string16(0);
+ }
+ }
+
+ if (trailingRangesCount > 0) {
+ endCount += '\xFF\xFF';
+ startCount += '\xFF\xFF';
+ idDeltas += '\x00\x01';
+ idRangeOffsets += '\x00\x00';
+ }
+
+ var format314 = '\x00\x00' + string16(2 * segCount) + string16(searchParams.range) + string16(searchParams.entry) + string16(searchParams.rangeShift) + endCount + '\x00\x00' + startCount + idDeltas + idRangeOffsets + glyphsIds;
+ var format31012 = '';
+ var header31012 = '';
+
+ if (numTables > 1) {
+ cmap += '\x00\x03' + '\x00\x0A' + (0, _util.string32)(4 + numTables * 8 + 4 + format314.length);
+ format31012 = '';
+
+ for (i = 0, ii = ranges.length; i < ii; i++) {
+ range = ranges[i];
+ start = range[0];
+ codes = range[2];
+ var code = codes[0];
+
+ for (j = 1, jj = codes.length; j < jj; ++j) {
+ if (codes[j] !== codes[j - 1] + 1) {
+ end = range[0] + j - 1;
+ format31012 += (0, _util.string32)(start) + (0, _util.string32)(end) + (0, _util.string32)(code);
+ start = end + 1;
+ code = codes[j];
+ }
+ }
+
+ format31012 += (0, _util.string32)(start) + (0, _util.string32)(range[1]) + (0, _util.string32)(code);
+ }
+
+ header31012 = '\x00\x0C' + '\x00\x00' + (0, _util.string32)(format31012.length + 16) + '\x00\x00\x00\x00' + (0, _util.string32)(format31012.length / 12);
+ }
+
+ return cmap + '\x00\x04' + string16(format314.length + 4) + format314 + header31012 + format31012;
+ }
+
+ function validateOS2Table(os2) {
+ var stream = new _stream.Stream(os2.data);
+ var version = stream.getUint16();
+ stream.getBytes(60);
+ var selection = stream.getUint16();
+
+ if (version < 4 && selection & 0x0300) {
+ return false;
+ }
+
+ var firstChar = stream.getUint16();
+ var lastChar = stream.getUint16();
+
+ if (firstChar > lastChar) {
+ return false;
+ }
+
+ stream.getBytes(6);
+ var usWinAscent = stream.getUint16();
+
+ if (usWinAscent === 0) {
+ return false;
+ }
+
+ os2.data[8] = os2.data[9] = 0;
+ return true;
+ }
+
+ function createOS2Table(properties, charstrings, override) {
+ override = override || {
+ unitsPerEm: 0,
+ yMax: 0,
+ yMin: 0,
+ ascent: 0,
+ descent: 0
+ };
+ var ulUnicodeRange1 = 0;
+ var ulUnicodeRange2 = 0;
+ var ulUnicodeRange3 = 0;
+ var ulUnicodeRange4 = 0;
+ var firstCharIndex = null;
+ var lastCharIndex = 0;
+
+ if (charstrings) {
+ for (var code in charstrings) {
+ code |= 0;
+
+ if (firstCharIndex > code || !firstCharIndex) {
+ firstCharIndex = code;
+ }
+
+ if (lastCharIndex < code) {
+ lastCharIndex = code;
+ }
+
+ var position = (0, _unicode.getUnicodeRangeFor)(code);
+
+ if (position < 32) {
+ ulUnicodeRange1 |= 1 << position;
+ } else if (position < 64) {
+ ulUnicodeRange2 |= 1 << position - 32;
+ } else if (position < 96) {
+ ulUnicodeRange3 |= 1 << position - 64;
+ } else if (position < 123) {
+ ulUnicodeRange4 |= 1 << position - 96;
+ } else {
+ throw new _util.FormatError('Unicode ranges Bits > 123 are reserved for internal usage');
+ }
+ }
+
+ if (lastCharIndex > 0xFFFF) {
+ lastCharIndex = 0xFFFF;
+ }
+ } else {
+ firstCharIndex = 0;
+ lastCharIndex = 255;
+ }
+
+ var bbox = properties.bbox || [0, 0, 0, 0];
+ var unitsPerEm = override.unitsPerEm || 1 / (properties.fontMatrix || _util.FONT_IDENTITY_MATRIX)[0];
+ var scale = properties.ascentScaled ? 1.0 : unitsPerEm / PDF_GLYPH_SPACE_UNITS;
+ var typoAscent = override.ascent || Math.round(scale * (properties.ascent || bbox[3]));
+ var typoDescent = override.descent || Math.round(scale * (properties.descent || bbox[1]));
+
+ if (typoDescent > 0 && properties.descent > 0 && bbox[1] < 0) {
+ typoDescent = -typoDescent;
+ }
+
+ var winAscent = override.yMax || typoAscent;
+ var winDescent = -override.yMin || -typoDescent;
+ return '\x00\x03' + '\x02\x24' + '\x01\xF4' + '\x00\x05' + '\x00\x00' + '\x02\x8A' + '\x02\xBB' + '\x00\x00' + '\x00\x8C' + '\x02\x8A' + '\x02\xBB' + '\x00\x00' + '\x01\xDF' + '\x00\x31' + '\x01\x02' + '\x00\x00' + '\x00\x00\x06' + String.fromCharCode(properties.fixedPitch ? 0x09 : 0x00) + '\x00\x00\x00\x00\x00\x00' + (0, _util.string32)(ulUnicodeRange1) + (0, _util.string32)(ulUnicodeRange2) + (0, _util.string32)(ulUnicodeRange3) + (0, _util.string32)(ulUnicodeRange4) + '\x2A\x32\x31\x2A' + string16(properties.italicAngle ? 1 : 0) + string16(firstCharIndex || properties.firstChar) + string16(lastCharIndex || properties.lastChar) + string16(typoAscent) + string16(typoDescent) + '\x00\x64' + string16(winAscent) + string16(winDescent) + '\x00\x00\x00\x00' + '\x00\x00\x00\x00' + string16(properties.xHeight) + string16(properties.capHeight) + string16(0) + string16(firstCharIndex || properties.firstChar) + '\x00\x03';
+ }
+
+ function createPostTable(properties) {
+ var angle = Math.floor(properties.italicAngle * Math.pow(2, 16));
+ return '\x00\x03\x00\x00' + (0, _util.string32)(angle) + '\x00\x00' + '\x00\x00' + (0, _util.string32)(properties.fixedPitch) + '\x00\x00\x00\x00' + '\x00\x00\x00\x00' + '\x00\x00\x00\x00' + '\x00\x00\x00\x00';
+ }
+
+ function createNameTable(name, proto) {
+ if (!proto) {
+ proto = [[], []];
+ }
+
+ var strings = [proto[0][0] || 'Original licence', proto[0][1] || name, proto[0][2] || 'Unknown', proto[0][3] || 'uniqueID', proto[0][4] || name, proto[0][5] || 'Version 0.11', proto[0][6] || '', proto[0][7] || 'Unknown', proto[0][8] || 'Unknown', proto[0][9] || 'Unknown'];
+ var stringsUnicode = [];
+ var i, ii, j, jj, str;
+
+ for (i = 0, ii = strings.length; i < ii; i++) {
+ str = proto[1][i] || strings[i];
+ var strBufUnicode = [];
+
+ for (j = 0, jj = str.length; j < jj; j++) {
+ strBufUnicode.push(string16(str.charCodeAt(j)));
+ }
+
+ stringsUnicode.push(strBufUnicode.join(''));
+ }
+
+ var names = [strings, stringsUnicode];
+ var platforms = ['\x00\x01', '\x00\x03'];
+ var encodings = ['\x00\x00', '\x00\x01'];
+ var languages = ['\x00\x00', '\x04\x09'];
+ var namesRecordCount = strings.length * platforms.length;
+ var nameTable = '\x00\x00' + string16(namesRecordCount) + string16(namesRecordCount * 12 + 6);
+ var strOffset = 0;
+
+ for (i = 0, ii = platforms.length; i < ii; i++) {
+ var strs = names[i];
+
+ for (j = 0, jj = strs.length; j < jj; j++) {
+ str = strs[j];
+ var nameRecord = platforms[i] + encodings[i] + languages[i] + string16(j) + string16(str.length) + string16(strOffset);
+ nameTable += nameRecord;
+ strOffset += str.length;
+ }
+ }
+
+ nameTable += strings.join('') + stringsUnicode.join('');
+ return nameTable;
+ }
+
+ Font.prototype = {
+ name: null,
+ font: null,
+ mimetype: null,
+ encoding: null,
+ disableFontFace: false,
+
+ get renderer() {
+ var renderer = _font_renderer.FontRendererFactory.create(this, SEAC_ANALYSIS_ENABLED);
+
+ return (0, _util.shadow)(this, 'renderer', renderer);
+ },
+
+ exportData: function Font_exportData() {
+ var data = {};
+
+ for (var i in this) {
+ if (this.hasOwnProperty(i)) {
+ data[i] = this[i];
+ }
+ }
+
+ return data;
+ },
+ fallbackToSystemFont: function Font_fallbackToSystemFont() {
+ var _this = this;
+
+ this.missingFile = true;
+ var charCode, unicode;
+ var name = this.name;
+ var type = this.type;
+ var subtype = this.subtype;
+ var fontName = name.replace(/[,_]/g, '-');
+ var stdFontMap = (0, _standard_fonts.getStdFontMap)(),
+ nonStdFontMap = (0, _standard_fonts.getNonStdFontMap)();
+ var isStandardFont = !!stdFontMap[fontName] || !!(nonStdFontMap[fontName] && stdFontMap[nonStdFontMap[fontName]]);
+ fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName;
+ this.bold = fontName.search(/bold/gi) !== -1;
+ this.italic = fontName.search(/oblique/gi) !== -1 || fontName.search(/italic/gi) !== -1;
+ this.black = name.search(/Black/g) !== -1;
+ this.remeasure = Object.keys(this.widths).length > 0;
+
+ if (isStandardFont && type === 'CIDFontType2' && this.cidEncoding.startsWith('Identity-')) {
+ var GlyphMapForStandardFonts = (0, _standard_fonts.getGlyphMapForStandardFonts)();
+ var map = [];
+
+ for (charCode in GlyphMapForStandardFonts) {
+ map[+charCode] = GlyphMapForStandardFonts[charCode];
+ }
+
+ if (/Arial-?Black/i.test(name)) {
+ var SupplementalGlyphMapForArialBlack = (0, _standard_fonts.getSupplementalGlyphMapForArialBlack)();
+
+ for (charCode in SupplementalGlyphMapForArialBlack) {
+ map[+charCode] = SupplementalGlyphMapForArialBlack[charCode];
+ }
+ } else if (/Calibri/i.test(name)) {
+ var SupplementalGlyphMapForCalibri = (0, _standard_fonts.getSupplementalGlyphMapForCalibri)();
+
+ for (charCode in SupplementalGlyphMapForCalibri) {
+ map[+charCode] = SupplementalGlyphMapForCalibri[charCode];
+ }
+ }
+
+ var isIdentityUnicode = this.toUnicode instanceof IdentityToUnicodeMap;
+
+ if (!isIdentityUnicode) {
+ this.toUnicode.forEach(function (charCode, unicodeCharCode) {
+ map[+charCode] = unicodeCharCode;
+ });
+ }
+
+ this.toFontChar = map;
+ this.toUnicode = new ToUnicodeMap(map);
+ } else if (/Symbol/i.test(fontName)) {
+ this.toFontChar = buildToFontChar(_encodings.SymbolSetEncoding, (0, _glyphlist.getGlyphsUnicode)(), this.differences);
+ } else if (/Dingbats/i.test(fontName)) {
+ if (/Wingdings/i.test(name)) {
+ (0, _util.warn)('Non-embedded Wingdings font, falling back to ZapfDingbats.');
+ }
+
+ this.toFontChar = buildToFontChar(_encodings.ZapfDingbatsEncoding, (0, _glyphlist.getDingbatsGlyphsUnicode)(), this.differences);
+ } else if (isStandardFont) {
+ this.toFontChar = buildToFontChar(this.defaultEncoding, (0, _glyphlist.getGlyphsUnicode)(), this.differences);
+ } else {
+ var glyphsUnicodeMap = (0, _glyphlist.getGlyphsUnicode)();
+ this.toUnicode.forEach(function (charCode, unicodeCharCode) {
+ if (!_this.composite) {
+ var glyphName = _this.differences[charCode] || _this.defaultEncoding[charCode];
+ unicode = (0, _unicode.getUnicodeForGlyph)(glyphName, glyphsUnicodeMap);
+
+ if (unicode !== -1) {
+ unicodeCharCode = unicode;
+ }
+ }
+
+ _this.toFontChar[charCode] = unicodeCharCode;
+ });
+ }
+
+ this.loadedName = fontName.split('-')[0];
+ this.fontType = getFontType(type, subtype);
+ },
+ checkAndRepair: function Font_checkAndRepair(name, font, properties) {
+ var VALID_TABLES = ['OS/2', 'cmap', 'head', 'hhea', 'hmtx', 'maxp', 'name', 'post', 'loca', 'glyf', 'fpgm', 'prep', 'cvt ', 'CFF '];
+
+ function readTables(file, numTables) {
+ var tables = Object.create(null);
+ tables['OS/2'] = null;
+ tables['cmap'] = null;
+ tables['head'] = null;
+ tables['hhea'] = null;
+ tables['hmtx'] = null;
+ tables['maxp'] = null;
+ tables['name'] = null;
+ tables['post'] = null;
+
+ for (var i = 0; i < numTables; i++) {
+ var table = readTableEntry(font);
+
+ if (!VALID_TABLES.includes(table.tag)) {
+ continue;
+ }
+
+ if (table.length === 0) {
+ continue;
+ }
+
+ tables[table.tag] = table;
+ }
+
+ return tables;
+ }
+
+ function readTableEntry(file) {
+ var tag = (0, _util.bytesToString)(file.getBytes(4));
+ var checksum = file.getInt32() >>> 0;
+ var offset = file.getInt32() >>> 0;
+ var length = file.getInt32() >>> 0;
+ var previousPosition = file.pos;
+ file.pos = file.start ? file.start : 0;
+ file.skip(offset);
+ var data = file.getBytes(length);
+ file.pos = previousPosition;
+
+ if (tag === 'head') {
+ data[8] = data[9] = data[10] = data[11] = 0;
+ data[17] |= 0x20;
+ }
+
+ return {
+ tag: tag,
+ checksum: checksum,
+ length: length,
+ offset: offset,
+ data: data
+ };
+ }
+
+ function readOpenTypeHeader(ttf) {
+ return {
+ version: (0, _util.bytesToString)(ttf.getBytes(4)),
+ numTables: ttf.getUint16(),
+ searchRange: ttf.getUint16(),
+ entrySelector: ttf.getUint16(),
+ rangeShift: ttf.getUint16()
+ };
+ }
+
+ function readTrueTypeCollectionHeader(ttc) {
+ var ttcTag = (0, _util.bytesToString)(ttc.getBytes(4));
+ (0, _util.assert)(ttcTag === 'ttcf', 'Must be a TrueType Collection font.');
+ var majorVersion = ttc.getUint16();
+ var minorVersion = ttc.getUint16();
+ var numFonts = ttc.getInt32() >>> 0;
+ var offsetTable = [];
+
+ for (var i = 0; i < numFonts; i++) {
+ offsetTable.push(ttc.getInt32() >>> 0);
+ }
+
+ var header = {
+ ttcTag: ttcTag,
+ majorVersion: majorVersion,
+ minorVersion: minorVersion,
+ numFonts: numFonts,
+ offsetTable: offsetTable
+ };
+
+ switch (majorVersion) {
+ case 1:
+ return header;
+
+ case 2:
+ header.dsigTag = ttc.getInt32() >>> 0;
+ header.dsigLength = ttc.getInt32() >>> 0;
+ header.dsigOffset = ttc.getInt32() >>> 0;
+ return header;
+ }
+
+ throw new _util.FormatError("Invalid TrueType Collection majorVersion: ".concat(majorVersion, "."));
+ }
+
+ function readTrueTypeCollectionData(ttc, fontName) {
+ var _readTrueTypeCollecti = readTrueTypeCollectionHeader(ttc),
+ numFonts = _readTrueTypeCollecti.numFonts,
+ offsetTable = _readTrueTypeCollecti.offsetTable;
+
+ for (var i = 0; i < numFonts; i++) {
+ ttc.pos = (ttc.start || 0) + offsetTable[i];
+ var potentialHeader = readOpenTypeHeader(ttc);
+ var potentialTables = readTables(ttc, potentialHeader.numTables);
+
+ if (!potentialTables['name']) {
+ throw new _util.FormatError('TrueType Collection font must contain a "name" table.');
+ }
+
+ var nameTable = readNameTable(potentialTables['name']);
+
+ for (var j = 0, jj = nameTable.length; j < jj; j++) {
+ for (var k = 0, kk = nameTable[j].length; k < kk; k++) {
+ var nameEntry = nameTable[j][k];
+
+ if (nameEntry && nameEntry.replace(/\s/g, '') === fontName) {
+ return {
+ header: potentialHeader,
+ tables: potentialTables
+ };
+ }
+ }
+ }
+ }
+
+ throw new _util.FormatError("TrueType Collection does not contain \"".concat(fontName, "\" font."));
+ }
+
+ function readCmapTable(cmap, font, isSymbolicFont, hasEncoding) {
+ if (!cmap) {
+ (0, _util.warn)('No cmap table available.');
+ return {
+ platformId: -1,
+ encodingId: -1,
+ mappings: [],
+ hasShortCmap: false
+ };
+ }
+
+ var segment;
+ var start = (font.start ? font.start : 0) + cmap.offset;
+ font.pos = start;
+ font.getUint16();
+ var numTables = font.getUint16();
+ var potentialTable;
+ var canBreak = false;
+
+ for (var i = 0; i < numTables; i++) {
+ var platformId = font.getUint16();
+ var encodingId = font.getUint16();
+ var offset = font.getInt32() >>> 0;
+ var useTable = false;
+
+ if (potentialTable && potentialTable.platformId === platformId && potentialTable.encodingId === encodingId) {
+ continue;
+ }
+
+ if (platformId === 0 && encodingId === 0) {
+ useTable = true;
+ } else if (platformId === 1 && encodingId === 0) {
+ useTable = true;
+ } else if (platformId === 3 && encodingId === 1 && (hasEncoding || !potentialTable)) {
+ useTable = true;
+
+ if (!isSymbolicFont) {
+ canBreak = true;
+ }
+ } else if (isSymbolicFont && platformId === 3 && encodingId === 0) {
+ useTable = true;
+ canBreak = true;
+ }
+
+ if (useTable) {
+ potentialTable = {
+ platformId: platformId,
+ encodingId: encodingId,
+ offset: offset
+ };
+ }
+
+ if (canBreak) {
+ break;
+ }
+ }
+
+ if (potentialTable) {
+ font.pos = start + potentialTable.offset;
+ }
+
+ if (!potentialTable || font.peekByte() === -1) {
+ (0, _util.warn)('Could not find a preferred cmap table.');
+ return {
+ platformId: -1,
+ encodingId: -1,
+ mappings: [],
+ hasShortCmap: false
+ };
+ }
+
+ var format = font.getUint16();
+ font.getUint16();
+ font.getUint16();
+ var hasShortCmap = false;
+ var mappings = [];
+ var j, glyphId;
+
+ if (format === 0) {
+ for (j = 0; j < 256; j++) {
+ var index = font.getByte();
+
+ if (!index) {
+ continue;
+ }
+
+ mappings.push({
+ charCode: j,
+ glyphId: index
+ });
+ }
+
+ hasShortCmap = true;
+ } else if (format === 4) {
+ var segCount = font.getUint16() >> 1;
+ font.getBytes(6);
+ var segIndex,
+ segments = [];
+
+ for (segIndex = 0; segIndex < segCount; segIndex++) {
+ segments.push({
+ end: font.getUint16()
+ });
+ }
+
+ font.getUint16();
+
+ for (segIndex = 0; segIndex < segCount; segIndex++) {
+ segments[segIndex].start = font.getUint16();
+ }
+
+ for (segIndex = 0; segIndex < segCount; segIndex++) {
+ segments[segIndex].delta = font.getUint16();
+ }
+
+ var offsetsCount = 0;
+
+ for (segIndex = 0; segIndex < segCount; segIndex++) {
+ segment = segments[segIndex];
+ var rangeOffset = font.getUint16();
+
+ if (!rangeOffset) {
+ segment.offsetIndex = -1;
+ continue;
+ }
+
+ var offsetIndex = (rangeOffset >> 1) - (segCount - segIndex);
+ segment.offsetIndex = offsetIndex;
+ offsetsCount = Math.max(offsetsCount, offsetIndex + segment.end - segment.start + 1);
+ }
+
+ var offsets = [];
+
+ for (j = 0; j < offsetsCount; j++) {
+ offsets.push(font.getUint16());
+ }
+
+ for (segIndex = 0; segIndex < segCount; segIndex++) {
+ segment = segments[segIndex];
+ start = segment.start;
+ var end = segment.end;
+ var delta = segment.delta;
+ offsetIndex = segment.offsetIndex;
+
+ for (j = start; j <= end; j++) {
+ if (j === 0xFFFF) {
+ continue;
+ }
+
+ glyphId = offsetIndex < 0 ? j : offsets[offsetIndex + j - start];
+ glyphId = glyphId + delta & 0xFFFF;
+ mappings.push({
+ charCode: j,
+ glyphId: glyphId
+ });
+ }
+ }
+ } else if (format === 6) {
+ var firstCode = font.getUint16();
+ var entryCount = font.getUint16();
+
+ for (j = 0; j < entryCount; j++) {
+ glyphId = font.getUint16();
+ var charCode = firstCode + j;
+ mappings.push({
+ charCode: charCode,
+ glyphId: glyphId
+ });
+ }
+ } else {
+ (0, _util.warn)('cmap table has unsupported format: ' + format);
+ return {
+ platformId: -1,
+ encodingId: -1,
+ mappings: [],
+ hasShortCmap: false
+ };
+ }
+
+ mappings.sort(function (a, b) {
+ return a.charCode - b.charCode;
+ });
+
+ for (i = 1; i < mappings.length; i++) {
+ if (mappings[i - 1].charCode === mappings[i].charCode) {
+ mappings.splice(i, 1);
+ i--;
+ }
+ }
+
+ return {
+ platformId: potentialTable.platformId,
+ encodingId: potentialTable.encodingId,
+ mappings: mappings,
+ hasShortCmap: hasShortCmap
+ };
+ }
+
+ function sanitizeMetrics(font, header, metrics, numGlyphs, dupFirstEntry) {
+ if (!header) {
+ if (metrics) {
+ metrics.data = null;
+ }
+
+ return;
+ }
+
+ font.pos = (font.start ? font.start : 0) + header.offset;
+ font.pos += 4;
+ font.pos += 2;
+ font.pos += 2;
+ font.pos += 2;
+ font.pos += 2;
+ font.pos += 2;
+ font.pos += 2;
+ font.pos += 2;
+ font.pos += 2;
+ font.pos += 2;
+ font.pos += 2;
+ font.pos += 8;
+ font.pos += 2;
+ var numOfMetrics = font.getUint16();
+
+ if (numOfMetrics > numGlyphs) {
+ (0, _util.info)('The numOfMetrics (' + numOfMetrics + ') should not be ' + 'greater than the numGlyphs (' + numGlyphs + ')');
+ numOfMetrics = numGlyphs;
+ header.data[34] = (numOfMetrics & 0xff00) >> 8;
+ header.data[35] = numOfMetrics & 0x00ff;
+ }
+
+ var numOfSidebearings = numGlyphs - numOfMetrics;
+ var numMissing = numOfSidebearings - (metrics.length - numOfMetrics * 4 >> 1);
+
+ if (numMissing > 0) {
+ var entries = new Uint8Array(metrics.length + numMissing * 2);
+ entries.set(metrics.data);
+
+ if (dupFirstEntry) {
+ entries[metrics.length] = metrics.data[2];
+ entries[metrics.length + 1] = metrics.data[3];
+ }
+
+ metrics.data = entries;
+ }
+ }
+
+ function sanitizeGlyph(source, sourceStart, sourceEnd, dest, destStart, hintsValid) {
+ var glyphProfile = {
+ length: 0,
+ sizeOfInstructions: 0
+ };
+
+ if (sourceEnd - sourceStart <= 12) {
+ return glyphProfile;
+ }
+
+ var glyf = source.subarray(sourceStart, sourceEnd);
+ var contoursCount = signedInt16(glyf[0], glyf[1]);
+
+ if (contoursCount < 0) {
+ contoursCount = -1;
+ writeSignedInt16(glyf, 0, contoursCount);
+ dest.set(glyf, destStart);
+ glyphProfile.length = glyf.length;
+ return glyphProfile;
+ }
+
+ var i,
+ j = 10,
+ flagsCount = 0;
+
+ for (i = 0; i < contoursCount; i++) {
+ var endPoint = glyf[j] << 8 | glyf[j + 1];
+ flagsCount = endPoint + 1;
+ j += 2;
+ }
+
+ var instructionsStart = j;
+ var instructionsLength = glyf[j] << 8 | glyf[j + 1];
+ glyphProfile.sizeOfInstructions = instructionsLength;
+ j += 2 + instructionsLength;
+ var instructionsEnd = j;
+ var coordinatesLength = 0;
+
+ for (i = 0; i < flagsCount; i++) {
+ var flag = glyf[j++];
+
+ if (flag & 0xC0) {
+ glyf[j - 1] = flag & 0x3F;
+ }
+
+ var xyLength = (flag & 2 ? 1 : flag & 16 ? 0 : 2) + (flag & 4 ? 1 : flag & 32 ? 0 : 2);
+ coordinatesLength += xyLength;
+
+ if (flag & 8) {
+ var repeat = glyf[j++];
+ i += repeat;
+ coordinatesLength += repeat * xyLength;
+ }
+ }
+
+ if (coordinatesLength === 0) {
+ return glyphProfile;
+ }
+
+ var glyphDataLength = j + coordinatesLength;
+
+ if (glyphDataLength > glyf.length) {
+ return glyphProfile;
+ }
+
+ if (!hintsValid && instructionsLength > 0) {
+ dest.set(glyf.subarray(0, instructionsStart), destStart);
+ dest.set([0, 0], destStart + instructionsStart);
+ dest.set(glyf.subarray(instructionsEnd, glyphDataLength), destStart + instructionsStart + 2);
+ glyphDataLength -= instructionsLength;
+
+ if (glyf.length - glyphDataLength > 3) {
+ glyphDataLength = glyphDataLength + 3 & ~3;
+ }
+
+ glyphProfile.length = glyphDataLength;
+ return glyphProfile;
+ }
+
+ if (glyf.length - glyphDataLength > 3) {
+ glyphDataLength = glyphDataLength + 3 & ~3;
+ dest.set(glyf.subarray(0, glyphDataLength), destStart);
+ glyphProfile.length = glyphDataLength;
+ return glyphProfile;
+ }
+
+ dest.set(glyf, destStart);
+ glyphProfile.length = glyf.length;
+ return glyphProfile;
+ }
+
+ function sanitizeHead(head, numGlyphs, locaLength) {
+ var data = head.data;
+ var version = int32(data[0], data[1], data[2], data[3]);
+
+ if (version >> 16 !== 1) {
+ (0, _util.info)('Attempting to fix invalid version in head table: ' + version);
+ data[0] = 0;
+ data[1] = 1;
+ data[2] = 0;
+ data[3] = 0;
+ }
+
+ var indexToLocFormat = int16(data[50], data[51]);
+
+ if (indexToLocFormat < 0 || indexToLocFormat > 1) {
+ (0, _util.info)('Attempting to fix invalid indexToLocFormat in head table: ' + indexToLocFormat);
+ var numGlyphsPlusOne = numGlyphs + 1;
+
+ if (locaLength === numGlyphsPlusOne << 1) {
+ data[50] = 0;
+ data[51] = 0;
+ } else if (locaLength === numGlyphsPlusOne << 2) {
+ data[50] = 0;
+ data[51] = 1;
+ } else {
+ throw new _util.FormatError('Could not fix indexToLocFormat: ' + indexToLocFormat);
+ }
+ }
+ }
+
+ function sanitizeGlyphLocations(loca, glyf, numGlyphs, isGlyphLocationsLong, hintsValid, dupFirstEntry, maxSizeOfInstructions) {
+ var itemSize, itemDecode, itemEncode;
+
+ if (isGlyphLocationsLong) {
+ itemSize = 4;
+
+ itemDecode = function fontItemDecodeLong(data, offset) {
+ return data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
+ };
+
+ itemEncode = function fontItemEncodeLong(data, offset, value) {
+ data[offset] = value >>> 24 & 0xFF;
+ data[offset + 1] = value >> 16 & 0xFF;
+ data[offset + 2] = value >> 8 & 0xFF;
+ data[offset + 3] = value & 0xFF;
+ };
+ } else {
+ itemSize = 2;
+
+ itemDecode = function fontItemDecode(data, offset) {
+ return data[offset] << 9 | data[offset + 1] << 1;
+ };
+
+ itemEncode = function fontItemEncode(data, offset, value) {
+ data[offset] = value >> 9 & 0xFF;
+ data[offset + 1] = value >> 1 & 0xFF;
+ };
+ }
+
+ var numGlyphsOut = dupFirstEntry ? numGlyphs + 1 : numGlyphs;
+ var locaData = loca.data;
+ var locaDataSize = itemSize * (1 + numGlyphsOut);
+ locaData = new Uint8Array(locaDataSize);
+ locaData.set(loca.data.subarray(0, locaDataSize));
+ loca.data = locaData;
+ var oldGlyfData = glyf.data;
+ var oldGlyfDataLength = oldGlyfData.length;
+ var newGlyfData = new Uint8Array(oldGlyfDataLength);
+ var startOffset = itemDecode(locaData, 0);
+ var writeOffset = 0;
+ var missingGlyphs = Object.create(null);
+ itemEncode(locaData, 0, writeOffset);
+ var i, j;
+
+ for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) {
+ var endOffset = itemDecode(locaData, j);
+
+ if (endOffset === 0) {
+ endOffset = startOffset;
+ }
+
+ if (endOffset > oldGlyfDataLength && (oldGlyfDataLength + 3 & ~3) === endOffset) {
+ endOffset = oldGlyfDataLength;
+ }
+
+ if (endOffset > oldGlyfDataLength) {
+ startOffset = endOffset;
+ }
+
+ var glyphProfile = sanitizeGlyph(oldGlyfData, startOffset, endOffset, newGlyfData, writeOffset, hintsValid);
+ var newLength = glyphProfile.length;
+
+ if (newLength === 0) {
+ missingGlyphs[i] = true;
+ }
+
+ if (glyphProfile.sizeOfInstructions > maxSizeOfInstructions) {
+ maxSizeOfInstructions = glyphProfile.sizeOfInstructions;
+ }
+
+ writeOffset += newLength;
+ itemEncode(locaData, j, writeOffset);
+ startOffset = endOffset;
+ }
+
+ if (writeOffset === 0) {
+ var simpleGlyph = new Uint8Array([0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0]);
+
+ for (i = 0, j = itemSize; i < numGlyphsOut; i++, j += itemSize) {
+ itemEncode(locaData, j, simpleGlyph.length);
+ }
+
+ glyf.data = simpleGlyph;
+ } else if (dupFirstEntry) {
+ var firstEntryLength = itemDecode(locaData, itemSize);
+
+ if (newGlyfData.length > firstEntryLength + writeOffset) {
+ glyf.data = newGlyfData.subarray(0, firstEntryLength + writeOffset);
+ } else {
+ glyf.data = new Uint8Array(firstEntryLength + writeOffset);
+ glyf.data.set(newGlyfData.subarray(0, writeOffset));
+ }
+
+ glyf.data.set(newGlyfData.subarray(0, firstEntryLength), writeOffset);
+ itemEncode(loca.data, locaData.length - itemSize, writeOffset + firstEntryLength);
+ } else {
+ glyf.data = newGlyfData.subarray(0, writeOffset);
+ }
+
+ return {
+ missingGlyphs: missingGlyphs,
+ maxSizeOfInstructions: maxSizeOfInstructions
+ };
+ }
+
+ function readPostScriptTable(post, properties, maxpNumGlyphs) {
+ var start = (font.start ? font.start : 0) + post.offset;
+ font.pos = start;
+ var length = post.length,
+ end = start + length;
+ var version = font.getInt32();
+ font.getBytes(28);
+ var glyphNames;
+ var valid = true;
+ var i;
+
+ switch (version) {
+ case 0x00010000:
+ glyphNames = MacStandardGlyphOrdering;
+ break;
+
+ case 0x00020000:
+ var numGlyphs = font.getUint16();
+
+ if (numGlyphs !== maxpNumGlyphs) {
+ valid = false;
+ break;
+ }
+
+ var glyphNameIndexes = [];
+
+ for (i = 0; i < numGlyphs; ++i) {
+ var index = font.getUint16();
+
+ if (index >= 32768) {
+ valid = false;
+ break;
+ }
+
+ glyphNameIndexes.push(index);
+ }
+
+ if (!valid) {
+ break;
+ }
+
+ var customNames = [];
+ var strBuf = [];
+
+ while (font.pos < end) {
+ var stringLength = font.getByte();
+ strBuf.length = stringLength;
+
+ for (i = 0; i < stringLength; ++i) {
+ strBuf[i] = String.fromCharCode(font.getByte());
+ }
+
+ customNames.push(strBuf.join(''));
+ }
+
+ glyphNames = [];
+
+ for (i = 0; i < numGlyphs; ++i) {
+ var j = glyphNameIndexes[i];
+
+ if (j < 258) {
+ glyphNames.push(MacStandardGlyphOrdering[j]);
+ continue;
+ }
+
+ glyphNames.push(customNames[j - 258]);
+ }
+
+ break;
+
+ case 0x00030000:
+ break;
+
+ default:
+ (0, _util.warn)('Unknown/unsupported post table version ' + version);
+ valid = false;
+
+ if (properties.defaultEncoding) {
+ glyphNames = properties.defaultEncoding;
+ }
+
+ break;
+ }
+
+ properties.glyphNames = glyphNames;
+ return valid;
+ }
+
+ function readNameTable(nameTable) {
+ var start = (font.start ? font.start : 0) + nameTable.offset;
+ font.pos = start;
+ var names = [[], []];
+ var length = nameTable.length,
+ end = start + length;
+ var format = font.getUint16();
+ var FORMAT_0_HEADER_LENGTH = 6;
+
+ if (format !== 0 || length < FORMAT_0_HEADER_LENGTH) {
+ return names;
+ }
+
+ var numRecords = font.getUint16();
+ var stringsStart = font.getUint16();
+ var records = [];
+ var NAME_RECORD_LENGTH = 12;
+ var i, ii;
+
+ for (i = 0; i < numRecords && font.pos + NAME_RECORD_LENGTH <= end; i++) {
+ var r = {
+ platform: font.getUint16(),
+ encoding: font.getUint16(),
+ language: font.getUint16(),
+ name: font.getUint16(),
+ length: font.getUint16(),
+ offset: font.getUint16()
+ };
+
+ if (r.platform === 1 && r.encoding === 0 && r.language === 0 || r.platform === 3 && r.encoding === 1 && r.language === 0x409) {
+ records.push(r);
+ }
+ }
+
+ for (i = 0, ii = records.length; i < ii; i++) {
+ var record = records[i];
+
+ if (record.length <= 0) {
+ continue;
+ }
+
+ var pos = start + stringsStart + record.offset;
+
+ if (pos + record.length > end) {
+ continue;
+ }
+
+ font.pos = pos;
+ var nameIndex = record.name;
+
+ if (record.encoding) {
+ var str = '';
+
+ for (var j = 0, jj = record.length; j < jj; j += 2) {
+ str += String.fromCharCode(font.getUint16());
+ }
+
+ names[1][nameIndex] = str;
+ } else {
+ names[0][nameIndex] = (0, _util.bytesToString)(font.getBytes(record.length));
+ }
+ }
+
+ return names;
+ }
+
+ var TTOpsStackDeltas = [0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -2, -2, 0, 0, -2, -5, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, -1, -1, 1, -1, -999, 0, 1, 0, -1, -2, 0, -1, -2, -1, -1, 0, -1, -1, 0, 0, -999, -999, -1, -1, -1, -1, -2, -999, -2, -2, -999, 0, -2, -2, 0, 0, -2, 0, -2, 0, 0, 0, -2, -1, -1, 1, 1, 0, 0, -1, -1, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, 0, -999, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -999, -999, -999, -999, -999, -1, -1, -2, -2, 0, 0, 0, 0, -1, -1, -999, -2, -2, 0, 0, -1, -2, -2, 0, 0, 0, -1, -1, -1, -2];
+
+ function sanitizeTTProgram(table, ttContext) {
+ var data = table.data;
+ var i = 0,
+ j,
+ n,
+ b,
+ funcId,
+ pc,
+ lastEndf = 0,
+ lastDeff = 0;
+ var stack = [];
+ var callstack = [];
+ var functionsCalled = [];
+ var tooComplexToFollowFunctions = ttContext.tooComplexToFollowFunctions;
+ var inFDEF = false,
+ ifLevel = 0,
+ inELSE = 0;
+
+ for (var ii = data.length; i < ii;) {
+ var op = data[i++];
+
+ if (op === 0x40) {
+ n = data[i++];
+
+ if (inFDEF || inELSE) {
+ i += n;
+ } else {
+ for (j = 0; j < n; j++) {
+ stack.push(data[i++]);
+ }
+ }
+ } else if (op === 0x41) {
+ n = data[i++];
+
+ if (inFDEF || inELSE) {
+ i += n * 2;
+ } else {
+ for (j = 0; j < n; j++) {
+ b = data[i++];
+ stack.push(b << 8 | data[i++]);
+ }
+ }
+ } else if ((op & 0xF8) === 0xB0) {
+ n = op - 0xB0 + 1;
+
+ if (inFDEF || inELSE) {
+ i += n;
+ } else {
+ for (j = 0; j < n; j++) {
+ stack.push(data[i++]);
+ }
+ }
+ } else if ((op & 0xF8) === 0xB8) {
+ n = op - 0xB8 + 1;
+
+ if (inFDEF || inELSE) {
+ i += n * 2;
+ } else {
+ for (j = 0; j < n; j++) {
+ b = data[i++];
+ stack.push(b << 8 | data[i++]);
+ }
+ }
+ } else if (op === 0x2B && !tooComplexToFollowFunctions) {
+ if (!inFDEF && !inELSE) {
+ funcId = stack[stack.length - 1];
+
+ if (isNaN(funcId)) {
+ (0, _util.info)('TT: CALL empty stack (or invalid entry).');
+ } else {
+ ttContext.functionsUsed[funcId] = true;
+
+ if (funcId in ttContext.functionsStackDeltas) {
+ var newStackLength = stack.length + ttContext.functionsStackDeltas[funcId];
+
+ if (newStackLength < 0) {
+ (0, _util.warn)('TT: CALL invalid functions stack delta.');
+ ttContext.hintsValid = false;
+ return;
+ }
+
+ stack.length = newStackLength;
+ } else if (funcId in ttContext.functionsDefined && !functionsCalled.includes(funcId)) {
+ callstack.push({
+ data: data,
+ i: i,
+ stackTop: stack.length - 1
+ });
+ functionsCalled.push(funcId);
+ pc = ttContext.functionsDefined[funcId];
+
+ if (!pc) {
+ (0, _util.warn)('TT: CALL non-existent function');
+ ttContext.hintsValid = false;
+ return;
+ }
+
+ data = pc.data;
+ i = pc.i;
+ }
+ }
+ }
+ } else if (op === 0x2C && !tooComplexToFollowFunctions) {
+ if (inFDEF || inELSE) {
+ (0, _util.warn)('TT: nested FDEFs not allowed');
+ tooComplexToFollowFunctions = true;
+ }
+
+ inFDEF = true;
+ lastDeff = i;
+ funcId = stack.pop();
+ ttContext.functionsDefined[funcId] = {
+ data: data,
+ i: i
+ };
+ } else if (op === 0x2D) {
+ if (inFDEF) {
+ inFDEF = false;
+ lastEndf = i;
+ } else {
+ pc = callstack.pop();
+
+ if (!pc) {
+ (0, _util.warn)('TT: ENDF bad stack');
+ ttContext.hintsValid = false;
+ return;
+ }
+
+ funcId = functionsCalled.pop();
+ data = pc.data;
+ i = pc.i;
+ ttContext.functionsStackDeltas[funcId] = stack.length - pc.stackTop;
+ }
+ } else if (op === 0x89) {
+ if (inFDEF || inELSE) {
+ (0, _util.warn)('TT: nested IDEFs not allowed');
+ tooComplexToFollowFunctions = true;
+ }
+
+ inFDEF = true;
+ lastDeff = i;
+ } else if (op === 0x58) {
+ ++ifLevel;
+ } else if (op === 0x1B) {
+ inELSE = ifLevel;
+ } else if (op === 0x59) {
+ if (inELSE === ifLevel) {
+ inELSE = 0;
+ }
+
+ --ifLevel;
+ } else if (op === 0x1C) {
+ if (!inFDEF && !inELSE) {
+ var offset = stack[stack.length - 1];
+
+ if (offset > 0) {
+ i += offset - 1;
+ }
+ }
+ }
+
+ if (!inFDEF && !inELSE) {
+ var stackDelta = op <= 0x8E ? TTOpsStackDeltas[op] : op >= 0xC0 && op <= 0xDF ? -1 : op >= 0xE0 ? -2 : 0;
+
+ if (op >= 0x71 && op <= 0x75) {
+ n = stack.pop();
+
+ if (!isNaN(n)) {
+ stackDelta = -n * 2;
+ }
+ }
+
+ while (stackDelta < 0 && stack.length > 0) {
+ stack.pop();
+ stackDelta++;
+ }
+
+ while (stackDelta > 0) {
+ stack.push(NaN);
+ stackDelta--;
+ }
+ }
+ }
+
+ ttContext.tooComplexToFollowFunctions = tooComplexToFollowFunctions;
+ var content = [data];
+
+ if (i > data.length) {
+ content.push(new Uint8Array(i - data.length));
+ }
+
+ if (lastDeff > lastEndf) {
+ (0, _util.warn)('TT: complementing a missing function tail');
+ content.push(new Uint8Array([0x22, 0x2D]));
+ }
+
+ foldTTTable(table, content);
+ }
+
+ function checkInvalidFunctions(ttContext, maxFunctionDefs) {
+ if (ttContext.tooComplexToFollowFunctions) {
+ return;
+ }
+
+ if (ttContext.functionsDefined.length > maxFunctionDefs) {
+ (0, _util.warn)('TT: more functions defined than expected');
+ ttContext.hintsValid = false;
+ return;
+ }
+
+ for (var j = 0, jj = ttContext.functionsUsed.length; j < jj; j++) {
+ if (j > maxFunctionDefs) {
+ (0, _util.warn)('TT: invalid function id: ' + j);
+ ttContext.hintsValid = false;
+ return;
+ }
+
+ if (ttContext.functionsUsed[j] && !ttContext.functionsDefined[j]) {
+ (0, _util.warn)('TT: undefined function: ' + j);
+ ttContext.hintsValid = false;
+ return;
+ }
+ }
+ }
+
+ function foldTTTable(table, content) {
+ if (content.length > 1) {
+ var newLength = 0;
+ var j, jj;
+
+ for (j = 0, jj = content.length; j < jj; j++) {
+ newLength += content[j].length;
+ }
+
+ newLength = newLength + 3 & ~3;
+ var result = new Uint8Array(newLength);
+ var pos = 0;
+
+ for (j = 0, jj = content.length; j < jj; j++) {
+ result.set(content[j], pos);
+ pos += content[j].length;
+ }
+
+ table.data = result;
+ table.length = newLength;
+ }
+ }
+
+ function sanitizeTTPrograms(fpgm, prep, cvt, maxFunctionDefs) {
+ var ttContext = {
+ functionsDefined: [],
+ functionsUsed: [],
+ functionsStackDeltas: [],
+ tooComplexToFollowFunctions: false,
+ hintsValid: true
+ };
+
+ if (fpgm) {
+ sanitizeTTProgram(fpgm, ttContext);
+ }
+
+ if (prep) {
+ sanitizeTTProgram(prep, ttContext);
+ }
+
+ if (fpgm) {
+ checkInvalidFunctions(ttContext, maxFunctionDefs);
+ }
+
+ if (cvt && cvt.length & 1) {
+ var cvtData = new Uint8Array(cvt.length + 1);
+ cvtData.set(cvt.data);
+ cvt.data = cvtData;
+ }
+
+ return ttContext.hintsValid;
+ }
+
+ font = new _stream.Stream(new Uint8Array(font.getBytes()));
+ var header, tables;
+
+ if (isTrueTypeCollectionFile(font)) {
+ var ttcData = readTrueTypeCollectionData(font, this.name);
+ header = ttcData.header;
+ tables = ttcData.tables;
+ } else {
+ header = readOpenTypeHeader(font);
+ tables = readTables(font, header.numTables);
+ }
+
+ var cff, cffFile;
+ var isTrueType = !tables['CFF '];
+
+ if (!isTrueType) {
+ var isComposite = properties.composite && ((properties.cidToGidMap || []).length > 0 || !(properties.cMap instanceof _cmap.IdentityCMap));
+
+ if (header.version === 'OTTO' && !isComposite || !tables['head'] || !tables['hhea'] || !tables['maxp'] || !tables['post']) {
+ cffFile = new _stream.Stream(tables['CFF '].data);
+ cff = new CFFFont(cffFile, properties);
+ adjustWidths(properties);
+ return this.convert(name, cff, properties);
+ }
+
+ delete tables['glyf'];
+ delete tables['loca'];
+ delete tables['fpgm'];
+ delete tables['prep'];
+ delete tables['cvt '];
+ this.isOpenType = true;
+ } else {
+ if (!tables['loca']) {
+ throw new _util.FormatError('Required "loca" table is not found');
+ }
+
+ if (!tables['glyf']) {
+ (0, _util.warn)('Required "glyf" table is not found -- trying to recover.');
+ tables['glyf'] = {
+ tag: 'glyf',
+ data: new Uint8Array(0)
+ };
+ }
+
+ this.isOpenType = false;
+ }
+
+ if (!tables['maxp']) {
+ throw new _util.FormatError('Required "maxp" table is not found');
+ }
+
+ font.pos = (font.start || 0) + tables['maxp'].offset;
+ var version = font.getInt32();
+ var numGlyphs = font.getUint16();
+ var numGlyphsOut = numGlyphs + 1;
+ var dupFirstEntry = true;
+
+ if (numGlyphsOut > 0xFFFF) {
+ dupFirstEntry = false;
+ numGlyphsOut = numGlyphs;
+ (0, _util.warn)('Not enough space in glyfs to duplicate first glyph.');
+ }
+
+ var maxFunctionDefs = 0;
+ var maxSizeOfInstructions = 0;
+
+ if (version >= 0x00010000 && tables['maxp'].length >= 22) {
+ font.pos += 8;
+ var maxZones = font.getUint16();
+
+ if (maxZones > 2) {
+ tables['maxp'].data[14] = 0;
+ tables['maxp'].data[15] = 2;
+ }
+
+ font.pos += 4;
+ maxFunctionDefs = font.getUint16();
+ font.pos += 4;
+ maxSizeOfInstructions = font.getUint16();
+ }
+
+ tables['maxp'].data[4] = numGlyphsOut >> 8;
+ tables['maxp'].data[5] = numGlyphsOut & 255;
+ var hintsValid = sanitizeTTPrograms(tables['fpgm'], tables['prep'], tables['cvt '], maxFunctionDefs);
+
+ if (!hintsValid) {
+ delete tables['fpgm'];
+ delete tables['prep'];
+ delete tables['cvt '];
+ }
+
+ sanitizeMetrics(font, tables['hhea'], tables['hmtx'], numGlyphsOut, dupFirstEntry);
+
+ if (!tables['head']) {
+ throw new _util.FormatError('Required "head" table is not found');
+ }
+
+ sanitizeHead(tables['head'], numGlyphs, isTrueType ? tables['loca'].length : 0);
+ var missingGlyphs = Object.create(null);
+
+ if (isTrueType) {
+ var isGlyphLocationsLong = int16(tables['head'].data[50], tables['head'].data[51]);
+ var glyphsInfo = sanitizeGlyphLocations(tables['loca'], tables['glyf'], numGlyphs, isGlyphLocationsLong, hintsValid, dupFirstEntry, maxSizeOfInstructions);
+ missingGlyphs = glyphsInfo.missingGlyphs;
+
+ if (version >= 0x00010000 && tables['maxp'].length >= 22) {
+ tables['maxp'].data[26] = glyphsInfo.maxSizeOfInstructions >> 8;
+ tables['maxp'].data[27] = glyphsInfo.maxSizeOfInstructions & 255;
+ }
+ }
+
+ if (!tables['hhea']) {
+ throw new _util.FormatError('Required "hhea" table is not found');
+ }
+
+ if (tables['hhea'].data[10] === 0 && tables['hhea'].data[11] === 0) {
+ tables['hhea'].data[10] = 0xFF;
+ tables['hhea'].data[11] = 0xFF;
+ }
+
+ var metricsOverride = {
+ unitsPerEm: int16(tables['head'].data[18], tables['head'].data[19]),
+ yMax: int16(tables['head'].data[42], tables['head'].data[43]),
+ yMin: signedInt16(tables['head'].data[38], tables['head'].data[39]),
+ ascent: int16(tables['hhea'].data[4], tables['hhea'].data[5]),
+ descent: signedInt16(tables['hhea'].data[6], tables['hhea'].data[7])
+ };
+ this.ascent = metricsOverride.ascent / metricsOverride.unitsPerEm;
+ this.descent = metricsOverride.descent / metricsOverride.unitsPerEm;
+
+ if (tables['post']) {
+ readPostScriptTable(tables['post'], properties, numGlyphs);
+ }
+
+ tables['post'] = {
+ tag: 'post',
+ data: createPostTable(properties)
+ };
+ var charCodeToGlyphId = [],
+ charCode;
+
+ function hasGlyph(glyphId) {
+ return !missingGlyphs[glyphId];
+ }
+
+ if (properties.composite) {
+ var cidToGidMap = properties.cidToGidMap || [];
+ var isCidToGidMapEmpty = cidToGidMap.length === 0;
+ properties.cMap.forEach(function (charCode, cid) {
+ if (cid > 0xffff) {
+ throw new _util.FormatError('Max size of CID is 65,535');
+ }
+
+ var glyphId = -1;
+
+ if (isCidToGidMapEmpty) {
+ glyphId = cid;
+ } else if (cidToGidMap[cid] !== undefined) {
+ glyphId = cidToGidMap[cid];
+ }
+
+ if (glyphId >= 0 && glyphId < numGlyphs && hasGlyph(glyphId)) {
+ charCodeToGlyphId[charCode] = glyphId;
+ }
+ });
+ } else {
+ var cmapTable = readCmapTable(tables['cmap'], font, this.isSymbolicFont, properties.hasEncoding);
+ var cmapPlatformId = cmapTable.platformId;
+ var cmapEncodingId = cmapTable.encodingId;
+ var cmapMappings = cmapTable.mappings;
+ var cmapMappingsLength = cmapMappings.length;
+
+ if (properties.hasEncoding && (cmapPlatformId === 3 && cmapEncodingId === 1 || cmapPlatformId === 1 && cmapEncodingId === 0) || cmapPlatformId === -1 && cmapEncodingId === -1 && !!(0, _encodings.getEncoding)(properties.baseEncodingName)) {
+ var baseEncoding = [];
+
+ if (properties.baseEncodingName === 'MacRomanEncoding' || properties.baseEncodingName === 'WinAnsiEncoding') {
+ baseEncoding = (0, _encodings.getEncoding)(properties.baseEncodingName);
+ }
+
+ var glyphsUnicodeMap = (0, _glyphlist.getGlyphsUnicode)();
+
+ for (charCode = 0; charCode < 256; charCode++) {
+ var glyphName, standardGlyphName;
+
+ if (this.differences && charCode in this.differences) {
+ glyphName = this.differences[charCode];
+ } else if (charCode in baseEncoding && baseEncoding[charCode] !== '') {
+ glyphName = baseEncoding[charCode];
+ } else {
+ glyphName = _encodings.StandardEncoding[charCode];
+ }
+
+ if (!glyphName) {
+ continue;
+ }
+
+ standardGlyphName = recoverGlyphName(glyphName, glyphsUnicodeMap);
+ var unicodeOrCharCode;
+
+ if (cmapPlatformId === 3 && cmapEncodingId === 1) {
+ unicodeOrCharCode = glyphsUnicodeMap[standardGlyphName];
+ } else if (cmapPlatformId === 1 && cmapEncodingId === 0) {
+ unicodeOrCharCode = _encodings.MacRomanEncoding.indexOf(standardGlyphName);
+ }
+
+ var found = false;
+
+ for (var i = 0; i < cmapMappingsLength; ++i) {
+ if (cmapMappings[i].charCode !== unicodeOrCharCode) {
+ continue;
+ }
+
+ charCodeToGlyphId[charCode] = cmapMappings[i].glyphId;
+ found = true;
+ break;
+ }
+
+ if (!found && properties.glyphNames) {
+ var glyphId = properties.glyphNames.indexOf(glyphName);
+
+ if (glyphId === -1 && standardGlyphName !== glyphName) {
+ glyphId = properties.glyphNames.indexOf(standardGlyphName);
+ }
+
+ if (glyphId > 0 && hasGlyph(glyphId)) {
+ charCodeToGlyphId[charCode] = glyphId;
+ }
+ }
+ }
+ } else if (cmapPlatformId === 0 && cmapEncodingId === 0) {
+ for (var _i2 = 0; _i2 < cmapMappingsLength; ++_i2) {
+ charCodeToGlyphId[cmapMappings[_i2].charCode] = cmapMappings[_i2].glyphId;
+ }
+ } else {
+ for (var _i3 = 0; _i3 < cmapMappingsLength; ++_i3) {
+ charCode = cmapMappings[_i3].charCode;
+
+ if (cmapPlatformId === 3 && charCode >= 0xF000 && charCode <= 0xF0FF) {
+ charCode &= 0xFF;
+ }
+
+ charCodeToGlyphId[charCode] = cmapMappings[_i3].glyphId;
+ }
+ }
+ }
+
+ if (charCodeToGlyphId.length === 0) {
+ charCodeToGlyphId[0] = 0;
+ }
+
+ var glyphZeroId = numGlyphsOut - 1;
+
+ if (!dupFirstEntry) {
+ glyphZeroId = 0;
+ }
+
+ var newMapping = adjustMapping(charCodeToGlyphId, hasGlyph, glyphZeroId);
+ this.toFontChar = newMapping.toFontChar;
+ tables['cmap'] = {
+ tag: 'cmap',
+ data: createCmapTable(newMapping.charCodeToGlyphId, numGlyphsOut)
+ };
+
+ if (!tables['OS/2'] || !validateOS2Table(tables['OS/2'])) {
+ tables['OS/2'] = {
+ tag: 'OS/2',
+ data: createOS2Table(properties, newMapping.charCodeToGlyphId, metricsOverride)
+ };
+ }
+
+ if (!isTrueType) {
+ try {
+ cffFile = new _stream.Stream(tables['CFF '].data);
+ var parser = new _cff_parser.CFFParser(cffFile, properties, SEAC_ANALYSIS_ENABLED);
+ cff = parser.parse();
+ cff.duplicateFirstGlyph();
+ var compiler = new _cff_parser.CFFCompiler(cff);
+ tables['CFF '].data = compiler.compile();
+ } catch (e) {
+ (0, _util.warn)('Failed to compile font ' + properties.loadedName);
+ }
+ }
+
+ if (!tables['name']) {
+ tables['name'] = {
+ tag: 'name',
+ data: createNameTable(this.name)
+ };
+ } else {
+ var namePrototype = readNameTable(tables['name']);
+ tables['name'].data = createNameTable(name, namePrototype);
+ }
+
+ var builder = new OpenTypeFileBuilder(header.version);
+
+ for (var tableTag in tables) {
+ builder.addTable(tableTag, tables[tableTag].data);
+ }
+
+ return builder.toArray();
+ },
+ convert: function Font_convert(fontName, font, properties) {
+ properties.fixedPitch = false;
+
+ if (properties.builtInEncoding) {
+ adjustToUnicode(properties, properties.builtInEncoding);
+ }
+
+ var glyphZeroId = 1;
+
+ if (font instanceof CFFFont) {
+ glyphZeroId = font.numGlyphs - 1;
+ }
+
+ var mapping = font.getGlyphMapping(properties);
+ var newMapping = adjustMapping(mapping, font.hasGlyphId.bind(font), glyphZeroId);
+ this.toFontChar = newMapping.toFontChar;
+ var numGlyphs = font.numGlyphs;
+
+ function getCharCodes(charCodeToGlyphId, glyphId) {
+ var charCodes = null;
+
+ for (var charCode in charCodeToGlyphId) {
+ if (glyphId === charCodeToGlyphId[charCode]) {
+ if (!charCodes) {
+ charCodes = [];
+ }
+
+ charCodes.push(charCode | 0);
+ }
+ }
+
+ return charCodes;
+ }
+
+ function createCharCode(charCodeToGlyphId, glyphId) {
+ for (var charCode in charCodeToGlyphId) {
+ if (glyphId === charCodeToGlyphId[charCode]) {
+ return charCode | 0;
+ }
+ }
+
+ newMapping.charCodeToGlyphId[newMapping.nextAvailableFontCharCode] = glyphId;
+ return newMapping.nextAvailableFontCharCode++;
+ }
+
+ var seacs = font.seacs;
+
+ if (SEAC_ANALYSIS_ENABLED && seacs && seacs.length) {
+ var matrix = properties.fontMatrix || _util.FONT_IDENTITY_MATRIX;
+ var charset = font.getCharset();
+ var seacMap = Object.create(null);
+
+ for (var glyphId in seacs) {
+ glyphId |= 0;
+ var seac = seacs[glyphId];
+ var baseGlyphName = _encodings.StandardEncoding[seac[2]];
+ var accentGlyphName = _encodings.StandardEncoding[seac[3]];
+ var baseGlyphId = charset.indexOf(baseGlyphName);
+ var accentGlyphId = charset.indexOf(accentGlyphName);
+
+ if (baseGlyphId < 0 || accentGlyphId < 0) {
+ continue;
+ }
+
+ var accentOffset = {
+ x: seac[0] * matrix[0] + seac[1] * matrix[2] + matrix[4],
+ y: seac[0] * matrix[1] + seac[1] * matrix[3] + matrix[5]
+ };
+ var charCodes = getCharCodes(mapping, glyphId);
+
+ if (!charCodes) {
+ continue;
+ }
+
+ for (var i = 0, ii = charCodes.length; i < ii; i++) {
+ var charCode = charCodes[i];
+ var charCodeToGlyphId = newMapping.charCodeToGlyphId;
+ var baseFontCharCode = createCharCode(charCodeToGlyphId, baseGlyphId);
+ var accentFontCharCode = createCharCode(charCodeToGlyphId, accentGlyphId);
+ seacMap[charCode] = {
+ baseFontCharCode: baseFontCharCode,
+ accentFontCharCode: accentFontCharCode,
+ accentOffset: accentOffset
+ };
+ }
+ }
+
+ properties.seacMap = seacMap;
+ }
+
+ var unitsPerEm = 1 / (properties.fontMatrix || _util.FONT_IDENTITY_MATRIX)[0];
+ var builder = new OpenTypeFileBuilder('\x4F\x54\x54\x4F');
+ builder.addTable('CFF ', font.data);
+ builder.addTable('OS/2', createOS2Table(properties, newMapping.charCodeToGlyphId));
+ builder.addTable('cmap', createCmapTable(newMapping.charCodeToGlyphId, numGlyphs));
+ builder.addTable('head', '\x00\x01\x00\x00' + '\x00\x00\x10\x00' + '\x00\x00\x00\x00' + '\x5F\x0F\x3C\xF5' + '\x00\x00' + safeString16(unitsPerEm) + '\x00\x00\x00\x00\x9e\x0b\x7e\x27' + '\x00\x00\x00\x00\x9e\x0b\x7e\x27' + '\x00\x00' + safeString16(properties.descent) + '\x0F\xFF' + safeString16(properties.ascent) + string16(properties.italicAngle ? 2 : 0) + '\x00\x11' + '\x00\x00' + '\x00\x00' + '\x00\x00');
+ builder.addTable('hhea', '\x00\x01\x00\x00' + safeString16(properties.ascent) + safeString16(properties.descent) + '\x00\x00' + '\xFF\xFF' + '\x00\x00' + '\x00\x00' + '\x00\x00' + safeString16(properties.capHeight) + safeString16(Math.tan(properties.italicAngle) * properties.xHeight) + '\x00\x00' + '\x00\x00' + '\x00\x00' + '\x00\x00' + '\x00\x00' + '\x00\x00' + string16(numGlyphs));
+ builder.addTable('hmtx', function fontFieldsHmtx() {
+ var charstrings = font.charstrings;
+ var cffWidths = font.cff ? font.cff.widths : null;
+ var hmtx = '\x00\x00\x00\x00';
+
+ for (var i = 1, ii = numGlyphs; i < ii; i++) {
+ var width = 0;
+
+ if (charstrings) {
+ var charstring = charstrings[i - 1];
+ width = 'width' in charstring ? charstring.width : 0;
+ } else if (cffWidths) {
+ width = Math.ceil(cffWidths[i] || 0);
+ }
+
+ hmtx += string16(width) + string16(0);
+ }
+
+ return hmtx;
+ }());
+ builder.addTable('maxp', '\x00\x00\x50\x00' + string16(numGlyphs));
+ builder.addTable('name', createNameTable(fontName));
+ builder.addTable('post', createPostTable(properties));
+ return builder.toArray();
+ },
+
+ get spaceWidth() {
+ if ('_shadowWidth' in this) {
+ return this._shadowWidth;
+ }
+
+ var possibleSpaceReplacements = ['space', 'minus', 'one', 'i', 'I'];
+ var width;
+
+ for (var i = 0, ii = possibleSpaceReplacements.length; i < ii; i++) {
+ var glyphName = possibleSpaceReplacements[i];
+
+ if (glyphName in this.widths) {
+ width = this.widths[glyphName];
+ break;
+ }
+
+ var glyphsUnicodeMap = (0, _glyphlist.getGlyphsUnicode)();
+ var glyphUnicode = glyphsUnicodeMap[glyphName];
+ var charcode = 0;
+
+ if (this.composite) {
+ if (this.cMap.contains(glyphUnicode)) {
+ charcode = this.cMap.lookup(glyphUnicode);
+ }
+ }
+
+ if (!charcode && this.toUnicode) {
+ charcode = this.toUnicode.charCodeOf(glyphUnicode);
+ }
+
+ if (charcode <= 0) {
+ charcode = glyphUnicode;
+ }
+
+ width = this.widths[charcode];
+
+ if (width) {
+ break;
+ }
+ }
+
+ width = width || this.defaultWidth;
+ this._shadowWidth = width;
+ return width;
+ },
+
+ charToGlyph: function Font_charToGlyph(charcode, isSpace) {
+ var fontCharCode, width, operatorListId;
+ var widthCode = charcode;
+
+ if (this.cMap && this.cMap.contains(charcode)) {
+ widthCode = this.cMap.lookup(charcode);
+ }
+
+ width = this.widths[widthCode];
+ width = (0, _util.isNum)(width) ? width : this.defaultWidth;
+ var vmetric = this.vmetrics && this.vmetrics[widthCode];
+ var unicode = this.toUnicode.get(charcode) || this.fallbackToUnicode.get(charcode) || charcode;
+
+ if (typeof unicode === 'number') {
+ unicode = String.fromCharCode(unicode);
+ }
+
+ var isInFont = charcode in this.toFontChar;
+ fontCharCode = this.toFontChar[charcode] || charcode;
+
+ if (this.missingFile) {
+ fontCharCode = (0, _unicode.mapSpecialUnicodeValues)(fontCharCode);
+ }
+
+ if (this.isType3Font) {
+ operatorListId = fontCharCode;
+ }
+
+ var accent = null;
+
+ if (this.seacMap && this.seacMap[charcode]) {
+ isInFont = true;
+ var seac = this.seacMap[charcode];
+ fontCharCode = seac.baseFontCharCode;
+ accent = {
+ fontChar: String.fromCodePoint(seac.accentFontCharCode),
+ offset: seac.accentOffset
+ };
+ }
+
+ var fontChar = typeof fontCharCode === 'number' ? String.fromCodePoint(fontCharCode) : '';
+ var glyph = this.glyphCache[charcode];
+
+ if (!glyph || !glyph.matchesForCache(fontChar, unicode, accent, width, vmetric, operatorListId, isSpace, isInFont)) {
+ glyph = new Glyph(fontChar, unicode, accent, width, vmetric, operatorListId, isSpace, isInFont);
+ this.glyphCache[charcode] = glyph;
+ }
+
+ return glyph;
+ },
+ charsToGlyphs: function Font_charsToGlyphs(chars) {
+ var charsCache = this.charsCache;
+ var glyphs, glyph, charcode;
+
+ if (charsCache) {
+ glyphs = charsCache[chars];
+
+ if (glyphs) {
+ return glyphs;
+ }
+ }
+
+ if (!charsCache) {
+ charsCache = this.charsCache = Object.create(null);
+ }
+
+ glyphs = [];
+ var charsCacheKey = chars;
+ var i = 0,
+ ii;
+
+ if (this.cMap) {
+ var c = Object.create(null);
+
+ while (i < chars.length) {
+ this.cMap.readCharCode(chars, i, c);
+ charcode = c.charcode;
+ var length = c.length;
+ i += length;
+ var isSpace = length === 1 && chars.charCodeAt(i - 1) === 0x20;
+ glyph = this.charToGlyph(charcode, isSpace);
+ glyphs.push(glyph);
+ }
+ } else {
+ for (i = 0, ii = chars.length; i < ii; ++i) {
+ charcode = chars.charCodeAt(i);
+ glyph = this.charToGlyph(charcode, charcode === 0x20);
+ glyphs.push(glyph);
+ }
+ }
+
+ return charsCache[charsCacheKey] = glyphs;
+ },
+
+ get glyphCacheValues() {
+ return Object.values(this.glyphCache);
+ }
+
+ };
+ return Font;
+}();
+
+exports.Font = Font;
+
+var ErrorFont = function ErrorFontClosure() {
+ function ErrorFont(error) {
+ this.error = error;
+ this.loadedName = 'g_font_error';
+ this.missingFile = true;
+ }
+
+ ErrorFont.prototype = {
+ charsToGlyphs: function ErrorFont_charsToGlyphs() {
+ return [];
+ },
+ exportData: function ErrorFont_exportData() {
+ return {
+ error: this.error
+ };
+ }
+ };
+ return ErrorFont;
+}();
+
+exports.ErrorFont = ErrorFont;
+
+function type1FontGlyphMapping(properties, builtInEncoding, glyphNames) {
+ var charCodeToGlyphId = Object.create(null);
+ var glyphId, charCode, baseEncoding;
+ var isSymbolicFont = !!(properties.flags & FontFlags.Symbolic);
+
+ if (properties.baseEncodingName) {
+ baseEncoding = (0, _encodings.getEncoding)(properties.baseEncodingName);
+
+ for (charCode = 0; charCode < baseEncoding.length; charCode++) {
+ glyphId = glyphNames.indexOf(baseEncoding[charCode]);
+
+ if (glyphId >= 0) {
+ charCodeToGlyphId[charCode] = glyphId;
+ } else {
+ charCodeToGlyphId[charCode] = 0;
+ }
+ }
+ } else if (isSymbolicFont) {
+ for (charCode in builtInEncoding) {
+ charCodeToGlyphId[charCode] = builtInEncoding[charCode];
+ }
+ } else {
+ baseEncoding = _encodings.StandardEncoding;
+
+ for (charCode = 0; charCode < baseEncoding.length; charCode++) {
+ glyphId = glyphNames.indexOf(baseEncoding[charCode]);
+
+ if (glyphId >= 0) {
+ charCodeToGlyphId[charCode] = glyphId;
+ } else {
+ charCodeToGlyphId[charCode] = 0;
+ }
+ }
+ }
+
+ var differences = properties.differences,
+ glyphsUnicodeMap;
+
+ if (differences) {
+ for (charCode in differences) {
+ var glyphName = differences[charCode];
+ glyphId = glyphNames.indexOf(glyphName);
+
+ if (glyphId === -1) {
+ if (!glyphsUnicodeMap) {
+ glyphsUnicodeMap = (0, _glyphlist.getGlyphsUnicode)();
+ }
+
+ var standardGlyphName = recoverGlyphName(glyphName, glyphsUnicodeMap);
+
+ if (standardGlyphName !== glyphName) {
+ glyphId = glyphNames.indexOf(standardGlyphName);
+ }
+ }
+
+ if (glyphId >= 0) {
+ charCodeToGlyphId[charCode] = glyphId;
+ } else {
+ charCodeToGlyphId[charCode] = 0;
+ }
+ }
+ }
+
+ return charCodeToGlyphId;
+}
+
+var Type1Font = function Type1FontClosure() {
+ function findBlock(streamBytes, signature, startIndex) {
+ var streamBytesLength = streamBytes.length;
+ var signatureLength = signature.length;
+ var scanLength = streamBytesLength - signatureLength;
+ var i = startIndex,
+ j,
+ found = false;
+
+ while (i < scanLength) {
+ j = 0;
+
+ while (j < signatureLength && streamBytes[i + j] === signature[j]) {
+ j++;
+ }
+
+ if (j >= signatureLength) {
+ i += j;
+
+ while (i < streamBytesLength && (0, _util.isSpace)(streamBytes[i])) {
+ i++;
+ }
+
+ found = true;
+ break;
+ }
+
+ i++;
+ }
+
+ return {
+ found: found,
+ length: i
+ };
+ }
+
+ function getHeaderBlock(stream, suggestedLength) {
+ var EEXEC_SIGNATURE = [0x65, 0x65, 0x78, 0x65, 0x63];
+ var streamStartPos = stream.pos;
+ var headerBytes, headerBytesLength, block;
+
+ try {
+ headerBytes = stream.getBytes(suggestedLength);
+ headerBytesLength = headerBytes.length;
+ } catch (ex) {
+ if (ex instanceof _core_utils.MissingDataException) {
+ throw ex;
+ }
+ }
+
+ if (headerBytesLength === suggestedLength) {
+ block = findBlock(headerBytes, EEXEC_SIGNATURE, suggestedLength - 2 * EEXEC_SIGNATURE.length);
+
+ if (block.found && block.length === suggestedLength) {
+ return {
+ stream: new _stream.Stream(headerBytes),
+ length: suggestedLength
+ };
+ }
+ }
+
+ (0, _util.warn)('Invalid "Length1" property in Type1 font -- trying to recover.');
+ stream.pos = streamStartPos;
+ var SCAN_BLOCK_LENGTH = 2048;
+ var actualLength;
+
+ while (true) {
+ var scanBytes = stream.peekBytes(SCAN_BLOCK_LENGTH);
+ block = findBlock(scanBytes, EEXEC_SIGNATURE, 0);
+
+ if (block.length === 0) {
+ break;
+ }
+
+ stream.pos += block.length;
+
+ if (block.found) {
+ actualLength = stream.pos - streamStartPos;
+ break;
+ }
+ }
+
+ stream.pos = streamStartPos;
+
+ if (actualLength) {
+ return {
+ stream: new _stream.Stream(stream.getBytes(actualLength)),
+ length: actualLength
+ };
+ }
+
+ (0, _util.warn)('Unable to recover "Length1" property in Type1 font -- using as is.');
+ return {
+ stream: new _stream.Stream(stream.getBytes(suggestedLength)),
+ length: suggestedLength
+ };
+ }
+
+ function getEexecBlock(stream, suggestedLength) {
+ var eexecBytes = stream.getBytes();
+ return {
+ stream: new _stream.Stream(eexecBytes),
+ length: eexecBytes.length
+ };
+ }
+
+ function Type1Font(name, file, properties) {
+ var PFB_HEADER_SIZE = 6;
+ var headerBlockLength = properties.length1;
+ var eexecBlockLength = properties.length2;
+ var pfbHeader = file.peekBytes(PFB_HEADER_SIZE);
+ var pfbHeaderPresent = pfbHeader[0] === 0x80 && pfbHeader[1] === 0x01;
+
+ if (pfbHeaderPresent) {
+ file.skip(PFB_HEADER_SIZE);
+ headerBlockLength = pfbHeader[5] << 24 | pfbHeader[4] << 16 | pfbHeader[3] << 8 | pfbHeader[2];
+ }
+
+ var headerBlock = getHeaderBlock(file, headerBlockLength);
+ var headerBlockParser = new _type1_parser.Type1Parser(headerBlock.stream, false, SEAC_ANALYSIS_ENABLED);
+ headerBlockParser.extractFontHeader(properties);
+
+ if (pfbHeaderPresent) {
+ pfbHeader = file.getBytes(PFB_HEADER_SIZE);
+ eexecBlockLength = pfbHeader[5] << 24 | pfbHeader[4] << 16 | pfbHeader[3] << 8 | pfbHeader[2];
+ }
+
+ var eexecBlock = getEexecBlock(file, eexecBlockLength);
+ var eexecBlockParser = new _type1_parser.Type1Parser(eexecBlock.stream, true, SEAC_ANALYSIS_ENABLED);
+ var data = eexecBlockParser.extractFontProgram();
+
+ for (var info in data.properties) {
+ properties[info] = data.properties[info];
+ }
+
+ var charstrings = data.charstrings;
+ var type2Charstrings = this.getType2Charstrings(charstrings);
+ var subrs = this.getType2Subrs(data.subrs);
+ this.charstrings = charstrings;
+ this.data = this.wrap(name, type2Charstrings, this.charstrings, subrs, properties);
+ this.seacs = this.getSeacs(data.charstrings);
+ }
+
+ Type1Font.prototype = {
+ get numGlyphs() {
+ return this.charstrings.length + 1;
+ },
+
+ getCharset: function Type1Font_getCharset() {
+ var charset = ['.notdef'];
+ var charstrings = this.charstrings;
+
+ for (var glyphId = 0; glyphId < charstrings.length; glyphId++) {
+ charset.push(charstrings[glyphId].glyphName);
+ }
+
+ return charset;
+ },
+ getGlyphMapping: function Type1Font_getGlyphMapping(properties) {
+ var charstrings = this.charstrings;
+ var glyphNames = ['.notdef'],
+ glyphId;
+
+ for (glyphId = 0; glyphId < charstrings.length; glyphId++) {
+ glyphNames.push(charstrings[glyphId].glyphName);
+ }
+
+ var encoding = properties.builtInEncoding;
+
+ if (encoding) {
+ var builtInEncoding = Object.create(null);
+
+ for (var charCode in encoding) {
+ glyphId = glyphNames.indexOf(encoding[charCode]);
+
+ if (glyphId >= 0) {
+ builtInEncoding[charCode] = glyphId;
+ }
+ }
+ }
+
+ return type1FontGlyphMapping(properties, builtInEncoding, glyphNames);
+ },
+ hasGlyphId: function Type1Font_hasGlyphID(id) {
+ if (id < 0 || id >= this.numGlyphs) {
+ return false;
+ }
+
+ if (id === 0) {
+ return true;
+ }
+
+ var glyph = this.charstrings[id - 1];
+ return glyph.charstring.length > 0;
+ },
+ getSeacs: function Type1Font_getSeacs(charstrings) {
+ var i, ii;
+ var seacMap = [];
+
+ for (i = 0, ii = charstrings.length; i < ii; i++) {
+ var charstring = charstrings[i];
+
+ if (charstring.seac) {
+ seacMap[i + 1] = charstring.seac;
+ }
+ }
+
+ return seacMap;
+ },
+ getType2Charstrings: function Type1Font_getType2Charstrings(type1Charstrings) {
+ var type2Charstrings = [];
+
+ for (var i = 0, ii = type1Charstrings.length; i < ii; i++) {
+ type2Charstrings.push(type1Charstrings[i].charstring);
+ }
+
+ return type2Charstrings;
+ },
+ getType2Subrs: function Type1Font_getType2Subrs(type1Subrs) {
+ var bias = 0;
+ var count = type1Subrs.length;
+
+ if (count < 1133) {
+ bias = 107;
+ } else if (count < 33769) {
+ bias = 1131;
+ } else {
+ bias = 32768;
+ }
+
+ var type2Subrs = [];
+ var i;
+
+ for (i = 0; i < bias; i++) {
+ type2Subrs.push([0x0B]);
+ }
+
+ for (i = 0; i < count; i++) {
+ type2Subrs.push(type1Subrs[i]);
+ }
+
+ return type2Subrs;
+ },
+ wrap: function Type1Font_wrap(name, glyphs, charstrings, subrs, properties) {
+ var cff = new _cff_parser.CFF();
+ cff.header = new _cff_parser.CFFHeader(1, 0, 4, 4);
+ cff.names = [name];
+ var topDict = new _cff_parser.CFFTopDict();
+ topDict.setByName('version', 391);
+ topDict.setByName('Notice', 392);
+ topDict.setByName('FullName', 393);
+ topDict.setByName('FamilyName', 394);
+ topDict.setByName('Weight', 395);
+ topDict.setByName('Encoding', null);
+ topDict.setByName('FontMatrix', properties.fontMatrix);
+ topDict.setByName('FontBBox', properties.bbox);
+ topDict.setByName('charset', null);
+ topDict.setByName('CharStrings', null);
+ topDict.setByName('Private', null);
+ cff.topDict = topDict;
+ var strings = new _cff_parser.CFFStrings();
+ strings.add('Version 0.11');
+ strings.add('See original notice');
+ strings.add(name);
+ strings.add(name);
+ strings.add('Medium');
+ cff.strings = strings;
+ cff.globalSubrIndex = new _cff_parser.CFFIndex();
+ var count = glyphs.length;
+ var charsetArray = ['.notdef'];
+ var i, ii;
+
+ for (i = 0; i < count; i++) {
+ var glyphName = charstrings[i].glyphName;
+
+ var index = _cff_parser.CFFStandardStrings.indexOf(glyphName);
+
+ if (index === -1) {
+ strings.add(glyphName);
+ }
+
+ charsetArray.push(glyphName);
+ }
+
+ cff.charset = new _cff_parser.CFFCharset(false, 0, charsetArray);
+ var charStringsIndex = new _cff_parser.CFFIndex();
+ charStringsIndex.add([0x8B, 0x0E]);
+
+ for (i = 0; i < count; i++) {
+ charStringsIndex.add(glyphs[i]);
+ }
+
+ cff.charStrings = charStringsIndex;
+ var privateDict = new _cff_parser.CFFPrivateDict();
+ privateDict.setByName('Subrs', null);
+ var fields = ['BlueValues', 'OtherBlues', 'FamilyBlues', 'FamilyOtherBlues', 'StemSnapH', 'StemSnapV', 'BlueShift', 'BlueFuzz', 'BlueScale', 'LanguageGroup', 'ExpansionFactor', 'ForceBold', 'StdHW', 'StdVW'];
+
+ for (i = 0, ii = fields.length; i < ii; i++) {
+ var field = fields[i];
+
+ if (!(field in properties.privateData)) {
+ continue;
+ }
+
+ var value = properties.privateData[field];
+
+ if (Array.isArray(value)) {
+ for (var j = value.length - 1; j > 0; j--) {
+ value[j] -= value[j - 1];
+ }
+ }
+
+ privateDict.setByName(field, value);
+ }
+
+ cff.topDict.privateDict = privateDict;
+ var subrIndex = new _cff_parser.CFFIndex();
+
+ for (i = 0, ii = subrs.length; i < ii; i++) {
+ subrIndex.add(subrs[i]);
+ }
+
+ privateDict.subrsIndex = subrIndex;
+ var compiler = new _cff_parser.CFFCompiler(cff);
+ return compiler.compile();
+ }
+ };
+ return Type1Font;
+}();
+
+var CFFFont = function CFFFontClosure() {
+ function CFFFont(file, properties) {
+ this.properties = properties;
+ var parser = new _cff_parser.CFFParser(file, properties, SEAC_ANALYSIS_ENABLED);
+ this.cff = parser.parse();
+ this.cff.duplicateFirstGlyph();
+ var compiler = new _cff_parser.CFFCompiler(this.cff);
+ this.seacs = this.cff.seacs;
+
+ try {
+ this.data = compiler.compile();
+ } catch (e) {
+ (0, _util.warn)('Failed to compile font ' + properties.loadedName);
+ this.data = file;
+ }
+ }
+
+ CFFFont.prototype = {
+ get numGlyphs() {
+ return this.cff.charStrings.count;
+ },
+
+ getCharset: function CFFFont_getCharset() {
+ return this.cff.charset.charset;
+ },
+ getGlyphMapping: function CFFFont_getGlyphMapping() {
+ var cff = this.cff;
+ var properties = this.properties;
+ var charsets = cff.charset.charset;
+ var charCodeToGlyphId;
+ var glyphId;
+
+ if (properties.composite) {
+ charCodeToGlyphId = Object.create(null);
+ var charCode;
+
+ if (cff.isCIDFont) {
+ for (glyphId = 0; glyphId < charsets.length; glyphId++) {
+ var cid = charsets[glyphId];
+ charCode = properties.cMap.charCodeOf(cid);
+ charCodeToGlyphId[charCode] = glyphId;
+ }
+ } else {
+ for (glyphId = 0; glyphId < cff.charStrings.count; glyphId++) {
+ charCode = properties.cMap.charCodeOf(glyphId);
+ charCodeToGlyphId[charCode] = glyphId;
+ }
+ }
+
+ return charCodeToGlyphId;
+ }
+
+ var encoding = cff.encoding ? cff.encoding.encoding : null;
+ charCodeToGlyphId = type1FontGlyphMapping(properties, encoding, charsets);
+ return charCodeToGlyphId;
+ },
+ hasGlyphId: function CFFFont_hasGlyphID(id) {
+ return this.cff.hasGlyphId(id);
+ }
+ };
+ return CFFFont;
+}();
+
+/***/ }),
+/* 175 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.CFFFDSelect = exports.CFFCompiler = exports.CFFPrivateDict = exports.CFFTopDict = exports.CFFCharset = exports.CFFIndex = exports.CFFStrings = exports.CFFHeader = exports.CFF = exports.CFFParser = exports.CFFStandardStrings = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _charsets = __w_pdfjs_require__(176);
+
+var _encodings = __w_pdfjs_require__(177);
+
+var MAX_SUBR_NESTING = 10;
+var CFFStandardStrings = ['.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quoteright', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore', 'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', 'exclamdown', 'cent', 'sterling', 'fraction', 'yen', 'florin', 'section', 'currency', 'quotesingle', 'quotedblleft', 'guillemotleft', 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'endash', 'dagger', 'daggerdbl', 'periodcentered', 'paragraph', 'bullet', 'quotesinglbase', 'quotedblbase', 'quotedblright', 'guillemotright', 'ellipsis', 'perthousand', 'questiondown', 'grave', 'acute', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'dieresis', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron', 'emdash', 'AE', 'ordfeminine', 'Lslash', 'Oslash', 'OE', 'ordmasculine', 'ae', 'dotlessi', 'lslash', 'oslash', 'oe', 'germandbls', 'onesuperior', 'logicalnot', 'mu', 'trademark', 'Eth', 'onehalf', 'plusminus', 'Thorn', 'onequarter', 'divide', 'brokenbar', 'degree', 'thorn', 'threequarters', 'twosuperior', 'registered', 'minus', 'eth', 'multiply', 'threesuperior', 'copyright', 'Aacute', 'Acircumflex', 'Adieresis', 'Agrave', 'Aring', 'Atilde', 'Ccedilla', 'Eacute', 'Ecircumflex', 'Edieresis', 'Egrave', 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Ntilde', 'Oacute', 'Ocircumflex', 'Odieresis', 'Ograve', 'Otilde', 'Scaron', 'Uacute', 'Ucircumflex', 'Udieresis', 'Ugrave', 'Yacute', 'Ydieresis', 'Zcaron', 'aacute', 'acircumflex', 'adieresis', 'agrave', 'aring', 'atilde', 'ccedilla', 'eacute', 'ecircumflex', 'edieresis', 'egrave', 'iacute', 'icircumflex', 'idieresis', 'igrave', 'ntilde', 'oacute', 'ocircumflex', 'odieresis', 'ograve', 'otilde', 'scaron', 'uacute', 'ucircumflex', 'udieresis', 'ugrave', 'yacute', 'ydieresis', 'zcaron', 'exclamsmall', 'Hungarumlautsmall', 'dollaroldstyle', 'dollarsuperior', 'ampersandsmall', 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', 'onedotenleader', 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'commasuperior', 'threequartersemdash', 'periodsuperior', 'questionsmall', 'asuperior', 'bsuperior', 'centsuperior', 'dsuperior', 'esuperior', 'isuperior', 'lsuperior', 'msuperior', 'nsuperior', 'osuperior', 'rsuperior', 'ssuperior', 'tsuperior', 'ff', 'ffi', 'ffl', 'parenleftinferior', 'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', 'Asmall', 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', 'Osmall', 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall', 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall', 'exclamdownsmall', 'centoldstyle', 'Lslashsmall', 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall', 'Brevesmall', 'Caronsmall', 'Dotaccentsmall', 'Macronsmall', 'figuredash', 'hypheninferior', 'Ogoneksmall', 'Ringsmall', 'Cedillasmall', 'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds', 'zerosuperior', 'foursuperior', 'fivesuperior', 'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior', 'zeroinferior', 'oneinferior', 'twoinferior', 'threeinferior', 'fourinferior', 'fiveinferior', 'sixinferior', 'seveninferior', 'eightinferior', 'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior', 'commainferior', 'Agravesmall', 'Aacutesmall', 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall', 'Aringsmall', 'AEsmall', 'Ccedillasmall', 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', 'Edieresissmall', 'Igravesmall', 'Iacutesmall', 'Icircumflexsmall', 'Idieresissmall', 'Ethsmall', 'Ntildesmall', 'Ogravesmall', 'Oacutesmall', 'Ocircumflexsmall', 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall', 'Uacutesmall', 'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall', 'Thornsmall', 'Ydieresissmall', '001.000', '001.001', '001.002', '001.003', 'Black', 'Bold', 'Book', 'Light', 'Medium', 'Regular', 'Roman', 'Semibold'];
+exports.CFFStandardStrings = CFFStandardStrings;
+var NUM_STANDARD_CFF_STRINGS = 391;
+
+var CFFParser = function CFFParserClosure() {
+ var CharstringValidationData = [null, {
+ id: 'hstem',
+ min: 2,
+ stackClearing: true,
+ stem: true
+ }, null, {
+ id: 'vstem',
+ min: 2,
+ stackClearing: true,
+ stem: true
+ }, {
+ id: 'vmoveto',
+ min: 1,
+ stackClearing: true
+ }, {
+ id: 'rlineto',
+ min: 2,
+ resetStack: true
+ }, {
+ id: 'hlineto',
+ min: 1,
+ resetStack: true
+ }, {
+ id: 'vlineto',
+ min: 1,
+ resetStack: true
+ }, {
+ id: 'rrcurveto',
+ min: 6,
+ resetStack: true
+ }, null, {
+ id: 'callsubr',
+ min: 1,
+ undefStack: true
+ }, {
+ id: 'return',
+ min: 0,
+ undefStack: true
+ }, null, null, {
+ id: 'endchar',
+ min: 0,
+ stackClearing: true
+ }, null, null, null, {
+ id: 'hstemhm',
+ min: 2,
+ stackClearing: true,
+ stem: true
+ }, {
+ id: 'hintmask',
+ min: 0,
+ stackClearing: true
+ }, {
+ id: 'cntrmask',
+ min: 0,
+ stackClearing: true
+ }, {
+ id: 'rmoveto',
+ min: 2,
+ stackClearing: true
+ }, {
+ id: 'hmoveto',
+ min: 1,
+ stackClearing: true
+ }, {
+ id: 'vstemhm',
+ min: 2,
+ stackClearing: true,
+ stem: true
+ }, {
+ id: 'rcurveline',
+ min: 8,
+ resetStack: true
+ }, {
+ id: 'rlinecurve',
+ min: 8,
+ resetStack: true
+ }, {
+ id: 'vvcurveto',
+ min: 4,
+ resetStack: true
+ }, {
+ id: 'hhcurveto',
+ min: 4,
+ resetStack: true
+ }, null, {
+ id: 'callgsubr',
+ min: 1,
+ undefStack: true
+ }, {
+ id: 'vhcurveto',
+ min: 4,
+ resetStack: true
+ }, {
+ id: 'hvcurveto',
+ min: 4,
+ resetStack: true
+ }];
+ var CharstringValidationData12 = [null, null, null, {
+ id: 'and',
+ min: 2,
+ stackDelta: -1
+ }, {
+ id: 'or',
+ min: 2,
+ stackDelta: -1
+ }, {
+ id: 'not',
+ min: 1,
+ stackDelta: 0
+ }, null, null, null, {
+ id: 'abs',
+ min: 1,
+ stackDelta: 0
+ }, {
+ id: 'add',
+ min: 2,
+ stackDelta: -1,
+ stackFn: function stack_div(stack, index) {
+ stack[index - 2] = stack[index - 2] + stack[index - 1];
+ }
+ }, {
+ id: 'sub',
+ min: 2,
+ stackDelta: -1,
+ stackFn: function stack_div(stack, index) {
+ stack[index - 2] = stack[index - 2] - stack[index - 1];
+ }
+ }, {
+ id: 'div',
+ min: 2,
+ stackDelta: -1,
+ stackFn: function stack_div(stack, index) {
+ stack[index - 2] = stack[index - 2] / stack[index - 1];
+ }
+ }, null, {
+ id: 'neg',
+ min: 1,
+ stackDelta: 0,
+ stackFn: function stack_div(stack, index) {
+ stack[index - 1] = -stack[index - 1];
+ }
+ }, {
+ id: 'eq',
+ min: 2,
+ stackDelta: -1
+ }, null, null, {
+ id: 'drop',
+ min: 1,
+ stackDelta: -1
+ }, null, {
+ id: 'put',
+ min: 2,
+ stackDelta: -2
+ }, {
+ id: 'get',
+ min: 1,
+ stackDelta: 0
+ }, {
+ id: 'ifelse',
+ min: 4,
+ stackDelta: -3
+ }, {
+ id: 'random',
+ min: 0,
+ stackDelta: 1
+ }, {
+ id: 'mul',
+ min: 2,
+ stackDelta: -1,
+ stackFn: function stack_div(stack, index) {
+ stack[index - 2] = stack[index - 2] * stack[index - 1];
+ }
+ }, null, {
+ id: 'sqrt',
+ min: 1,
+ stackDelta: 0
+ }, {
+ id: 'dup',
+ min: 1,
+ stackDelta: 1
+ }, {
+ id: 'exch',
+ min: 2,
+ stackDelta: 0
+ }, {
+ id: 'index',
+ min: 2,
+ stackDelta: 0
+ }, {
+ id: 'roll',
+ min: 3,
+ stackDelta: -2
+ }, null, null, null, {
+ id: 'hflex',
+ min: 7,
+ resetStack: true
+ }, {
+ id: 'flex',
+ min: 13,
+ resetStack: true
+ }, {
+ id: 'hflex1',
+ min: 9,
+ resetStack: true
+ }, {
+ id: 'flex1',
+ min: 11,
+ resetStack: true
+ }];
+
+ function CFFParser(file, properties, seacAnalysisEnabled) {
+ this.bytes = file.getBytes();
+ this.properties = properties;
+ this.seacAnalysisEnabled = !!seacAnalysisEnabled;
+ }
+
+ CFFParser.prototype = {
+ parse: function CFFParser_parse() {
+ var properties = this.properties;
+ var cff = new CFF();
+ this.cff = cff;
+ var header = this.parseHeader();
+ var nameIndex = this.parseIndex(header.endPos);
+ var topDictIndex = this.parseIndex(nameIndex.endPos);
+ var stringIndex = this.parseIndex(topDictIndex.endPos);
+ var globalSubrIndex = this.parseIndex(stringIndex.endPos);
+ var topDictParsed = this.parseDict(topDictIndex.obj.get(0));
+ var topDict = this.createDict(CFFTopDict, topDictParsed, cff.strings);
+ cff.header = header.obj;
+ cff.names = this.parseNameIndex(nameIndex.obj);
+ cff.strings = this.parseStringIndex(stringIndex.obj);
+ cff.topDict = topDict;
+ cff.globalSubrIndex = globalSubrIndex.obj;
+ this.parsePrivateDict(cff.topDict);
+ cff.isCIDFont = topDict.hasName('ROS');
+ var charStringOffset = topDict.getByName('CharStrings');
+ var charStringIndex = this.parseIndex(charStringOffset).obj;
+ var fontMatrix = topDict.getByName('FontMatrix');
+
+ if (fontMatrix) {
+ properties.fontMatrix = fontMatrix;
+ }
+
+ var fontBBox = topDict.getByName('FontBBox');
+
+ if (fontBBox) {
+ properties.ascent = Math.max(fontBBox[3], fontBBox[1]);
+ properties.descent = Math.min(fontBBox[1], fontBBox[3]);
+ properties.ascentScaled = true;
+ }
+
+ var charset, encoding;
+
+ if (cff.isCIDFont) {
+ var fdArrayIndex = this.parseIndex(topDict.getByName('FDArray')).obj;
+
+ for (var i = 0, ii = fdArrayIndex.count; i < ii; ++i) {
+ var dictRaw = fdArrayIndex.get(i);
+ var fontDict = this.createDict(CFFTopDict, this.parseDict(dictRaw), cff.strings);
+ this.parsePrivateDict(fontDict);
+ cff.fdArray.push(fontDict);
+ }
+
+ encoding = null;
+ charset = this.parseCharsets(topDict.getByName('charset'), charStringIndex.count, cff.strings, true);
+ cff.fdSelect = this.parseFDSelect(topDict.getByName('FDSelect'), charStringIndex.count);
+ } else {
+ charset = this.parseCharsets(topDict.getByName('charset'), charStringIndex.count, cff.strings, false);
+ encoding = this.parseEncoding(topDict.getByName('Encoding'), properties, cff.strings, charset.charset);
+ }
+
+ cff.charset = charset;
+ cff.encoding = encoding;
+ var charStringsAndSeacs = this.parseCharStrings({
+ charStrings: charStringIndex,
+ localSubrIndex: topDict.privateDict.subrsIndex,
+ globalSubrIndex: globalSubrIndex.obj,
+ fdSelect: cff.fdSelect,
+ fdArray: cff.fdArray,
+ privateDict: topDict.privateDict
+ });
+ cff.charStrings = charStringsAndSeacs.charStrings;
+ cff.seacs = charStringsAndSeacs.seacs;
+ cff.widths = charStringsAndSeacs.widths;
+ return cff;
+ },
+ parseHeader: function CFFParser_parseHeader() {
+ var bytes = this.bytes;
+ var bytesLength = bytes.length;
+ var offset = 0;
+
+ while (offset < bytesLength && bytes[offset] !== 1) {
+ ++offset;
+ }
+
+ if (offset >= bytesLength) {
+ throw new _util.FormatError('Invalid CFF header');
+ }
+
+ if (offset !== 0) {
+ (0, _util.info)('cff data is shifted');
+ bytes = bytes.subarray(offset);
+ this.bytes = bytes;
+ }
+
+ var major = bytes[0];
+ var minor = bytes[1];
+ var hdrSize = bytes[2];
+ var offSize = bytes[3];
+ var header = new CFFHeader(major, minor, hdrSize, offSize);
+ return {
+ obj: header,
+ endPos: hdrSize
+ };
+ },
+ parseDict: function CFFParser_parseDict(dict) {
+ var pos = 0;
+
+ function parseOperand() {
+ var value = dict[pos++];
+
+ if (value === 30) {
+ return parseFloatOperand();
+ } else if (value === 28) {
+ value = dict[pos++];
+ value = (value << 24 | dict[pos++] << 16) >> 16;
+ return value;
+ } else if (value === 29) {
+ value = dict[pos++];
+ value = value << 8 | dict[pos++];
+ value = value << 8 | dict[pos++];
+ value = value << 8 | dict[pos++];
+ return value;
+ } else if (value >= 32 && value <= 246) {
+ return value - 139;
+ } else if (value >= 247 && value <= 250) {
+ return (value - 247) * 256 + dict[pos++] + 108;
+ } else if (value >= 251 && value <= 254) {
+ return -((value - 251) * 256) - dict[pos++] - 108;
+ }
+
+ (0, _util.warn)('CFFParser_parseDict: "' + value + '" is a reserved command.');
+ return NaN;
+ }
+
+ function parseFloatOperand() {
+ var str = '';
+ var eof = 15;
+ var lookup = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', 'E', 'E-', null, '-'];
+ var length = dict.length;
+
+ while (pos < length) {
+ var b = dict[pos++];
+ var b1 = b >> 4;
+ var b2 = b & 15;
+
+ if (b1 === eof) {
+ break;
+ }
+
+ str += lookup[b1];
+
+ if (b2 === eof) {
+ break;
+ }
+
+ str += lookup[b2];
+ }
+
+ return parseFloat(str);
+ }
+
+ var operands = [];
+ var entries = [];
+ pos = 0;
+ var end = dict.length;
+
+ while (pos < end) {
+ var b = dict[pos];
+
+ if (b <= 21) {
+ if (b === 12) {
+ b = b << 8 | dict[++pos];
+ }
+
+ entries.push([b, operands]);
+ operands = [];
+ ++pos;
+ } else {
+ operands.push(parseOperand());
+ }
+ }
+
+ return entries;
+ },
+ parseIndex: function CFFParser_parseIndex(pos) {
+ var cffIndex = new CFFIndex();
+ var bytes = this.bytes;
+ var count = bytes[pos++] << 8 | bytes[pos++];
+ var offsets = [];
+ var end = pos;
+ var i, ii;
+
+ if (count !== 0) {
+ var offsetSize = bytes[pos++];
+ var startPos = pos + (count + 1) * offsetSize - 1;
+
+ for (i = 0, ii = count + 1; i < ii; ++i) {
+ var offset = 0;
+
+ for (var j = 0; j < offsetSize; ++j) {
+ offset <<= 8;
+ offset += bytes[pos++];
+ }
+
+ offsets.push(startPos + offset);
+ }
+
+ end = offsets[count];
+ }
+
+ for (i = 0, ii = offsets.length - 1; i < ii; ++i) {
+ var offsetStart = offsets[i];
+ var offsetEnd = offsets[i + 1];
+ cffIndex.add(bytes.subarray(offsetStart, offsetEnd));
+ }
+
+ return {
+ obj: cffIndex,
+ endPos: end
+ };
+ },
+ parseNameIndex: function CFFParser_parseNameIndex(index) {
+ var names = [];
+
+ for (var i = 0, ii = index.count; i < ii; ++i) {
+ var name = index.get(i);
+ names.push((0, _util.bytesToString)(name));
+ }
+
+ return names;
+ },
+ parseStringIndex: function CFFParser_parseStringIndex(index) {
+ var strings = new CFFStrings();
+
+ for (var i = 0, ii = index.count; i < ii; ++i) {
+ var data = index.get(i);
+ strings.add((0, _util.bytesToString)(data));
+ }
+
+ return strings;
+ },
+ createDict: function CFFParser_createDict(Type, dict, strings) {
+ var cffDict = new Type(strings);
+
+ for (var i = 0, ii = dict.length; i < ii; ++i) {
+ var pair = dict[i];
+ var key = pair[0];
+ var value = pair[1];
+ cffDict.setByKey(key, value);
+ }
+
+ return cffDict;
+ },
+ parseCharString: function CFFParser_parseCharString(state, data, localSubrIndex, globalSubrIndex) {
+ if (!data || state.callDepth > MAX_SUBR_NESTING) {
+ return false;
+ }
+
+ var stackSize = state.stackSize;
+ var stack = state.stack;
+ var length = data.length;
+
+ for (var j = 0; j < length;) {
+ var value = data[j++];
+ var validationCommand = null;
+
+ if (value === 12) {
+ var q = data[j++];
+
+ if (q === 0) {
+ data[j - 2] = 139;
+ data[j - 1] = 22;
+ stackSize = 0;
+ } else {
+ validationCommand = CharstringValidationData12[q];
+ }
+ } else if (value === 28) {
+ stack[stackSize] = (data[j] << 24 | data[j + 1] << 16) >> 16;
+ j += 2;
+ stackSize++;
+ } else if (value === 14) {
+ if (stackSize >= 4) {
+ stackSize -= 4;
+
+ if (this.seacAnalysisEnabled) {
+ state.seac = stack.slice(stackSize, stackSize + 4);
+ return false;
+ }
+ }
+
+ validationCommand = CharstringValidationData[value];
+ } else if (value >= 32 && value <= 246) {
+ stack[stackSize] = value - 139;
+ stackSize++;
+ } else if (value >= 247 && value <= 254) {
+ stack[stackSize] = value < 251 ? (value - 247 << 8) + data[j] + 108 : -(value - 251 << 8) - data[j] - 108;
+ j++;
+ stackSize++;
+ } else if (value === 255) {
+ stack[stackSize] = (data[j] << 24 | data[j + 1] << 16 | data[j + 2] << 8 | data[j + 3]) / 65536;
+ j += 4;
+ stackSize++;
+ } else if (value === 19 || value === 20) {
+ state.hints += stackSize >> 1;
+ j += state.hints + 7 >> 3;
+ stackSize %= 2;
+ validationCommand = CharstringValidationData[value];
+ } else if (value === 10 || value === 29) {
+ var subrsIndex;
+
+ if (value === 10) {
+ subrsIndex = localSubrIndex;
+ } else {
+ subrsIndex = globalSubrIndex;
+ }
+
+ if (!subrsIndex) {
+ validationCommand = CharstringValidationData[value];
+ (0, _util.warn)('Missing subrsIndex for ' + validationCommand.id);
+ return false;
+ }
+
+ var bias = 32768;
+
+ if (subrsIndex.count < 1240) {
+ bias = 107;
+ } else if (subrsIndex.count < 33900) {
+ bias = 1131;
+ }
+
+ var subrNumber = stack[--stackSize] + bias;
+
+ if (subrNumber < 0 || subrNumber >= subrsIndex.count || isNaN(subrNumber)) {
+ validationCommand = CharstringValidationData[value];
+ (0, _util.warn)('Out of bounds subrIndex for ' + validationCommand.id);
+ return false;
+ }
+
+ state.stackSize = stackSize;
+ state.callDepth++;
+ var valid = this.parseCharString(state, subrsIndex.get(subrNumber), localSubrIndex, globalSubrIndex);
+
+ if (!valid) {
+ return false;
+ }
+
+ state.callDepth--;
+ stackSize = state.stackSize;
+ continue;
+ } else if (value === 11) {
+ state.stackSize = stackSize;
+ return true;
+ } else {
+ validationCommand = CharstringValidationData[value];
+ }
+
+ if (validationCommand) {
+ if (validationCommand.stem) {
+ state.hints += stackSize >> 1;
+
+ if (value === 3 || value === 23) {
+ state.hasVStems = true;
+ } else if (state.hasVStems && (value === 1 || value === 18)) {
+ (0, _util.warn)('CFF stem hints are in wrong order');
+ data[j - 1] = value === 1 ? 3 : 23;
+ }
+ }
+
+ if ('min' in validationCommand) {
+ if (!state.undefStack && stackSize < validationCommand.min) {
+ (0, _util.warn)('Not enough parameters for ' + validationCommand.id + '; actual: ' + stackSize + ', expected: ' + validationCommand.min);
+ return false;
+ }
+ }
+
+ if (state.firstStackClearing && validationCommand.stackClearing) {
+ state.firstStackClearing = false;
+ stackSize -= validationCommand.min;
+
+ if (stackSize >= 2 && validationCommand.stem) {
+ stackSize %= 2;
+ } else if (stackSize > 1) {
+ (0, _util.warn)('Found too many parameters for stack-clearing command');
+ }
+
+ if (stackSize > 0 && stack[stackSize - 1] >= 0) {
+ state.width = stack[stackSize - 1];
+ }
+ }
+
+ if ('stackDelta' in validationCommand) {
+ if ('stackFn' in validationCommand) {
+ validationCommand.stackFn(stack, stackSize);
+ }
+
+ stackSize += validationCommand.stackDelta;
+ } else if (validationCommand.stackClearing) {
+ stackSize = 0;
+ } else if (validationCommand.resetStack) {
+ stackSize = 0;
+ state.undefStack = false;
+ } else if (validationCommand.undefStack) {
+ stackSize = 0;
+ state.undefStack = true;
+ state.firstStackClearing = false;
+ }
+ }
+ }
+
+ state.stackSize = stackSize;
+ return true;
+ },
+ parseCharStrings: function parseCharStrings(_ref) {
+ var charStrings = _ref.charStrings,
+ localSubrIndex = _ref.localSubrIndex,
+ globalSubrIndex = _ref.globalSubrIndex,
+ fdSelect = _ref.fdSelect,
+ fdArray = _ref.fdArray,
+ privateDict = _ref.privateDict;
+ var seacs = [];
+ var widths = [];
+ var count = charStrings.count;
+
+ for (var i = 0; i < count; i++) {
+ var charstring = charStrings.get(i);
+ var state = {
+ callDepth: 0,
+ stackSize: 0,
+ stack: [],
+ undefStack: true,
+ hints: 0,
+ firstStackClearing: true,
+ seac: null,
+ width: null,
+ hasVStems: false
+ };
+ var valid = true;
+ var localSubrToUse = null;
+ var privateDictToUse = privateDict;
+
+ if (fdSelect && fdArray.length) {
+ var fdIndex = fdSelect.getFDIndex(i);
+
+ if (fdIndex === -1) {
+ (0, _util.warn)('Glyph index is not in fd select.');
+ valid = false;
+ }
+
+ if (fdIndex >= fdArray.length) {
+ (0, _util.warn)('Invalid fd index for glyph index.');
+ valid = false;
+ }
+
+ if (valid) {
+ privateDictToUse = fdArray[fdIndex].privateDict;
+ localSubrToUse = privateDictToUse.subrsIndex;
+ }
+ } else if (localSubrIndex) {
+ localSubrToUse = localSubrIndex;
+ }
+
+ if (valid) {
+ valid = this.parseCharString(state, charstring, localSubrToUse, globalSubrIndex);
+ }
+
+ if (state.width !== null) {
+ var nominalWidth = privateDictToUse.getByName('nominalWidthX');
+ widths[i] = nominalWidth + state.width;
+ } else {
+ var defaultWidth = privateDictToUse.getByName('defaultWidthX');
+ widths[i] = defaultWidth;
+ }
+
+ if (state.seac !== null) {
+ seacs[i] = state.seac;
+ }
+
+ if (!valid) {
+ charStrings.set(i, new Uint8Array([14]));
+ }
+ }
+
+ return {
+ charStrings: charStrings,
+ seacs: seacs,
+ widths: widths
+ };
+ },
+ emptyPrivateDictionary: function CFFParser_emptyPrivateDictionary(parentDict) {
+ var privateDict = this.createDict(CFFPrivateDict, [], parentDict.strings);
+ parentDict.setByKey(18, [0, 0]);
+ parentDict.privateDict = privateDict;
+ },
+ parsePrivateDict: function CFFParser_parsePrivateDict(parentDict) {
+ if (!parentDict.hasName('Private')) {
+ this.emptyPrivateDictionary(parentDict);
+ return;
+ }
+
+ var privateOffset = parentDict.getByName('Private');
+
+ if (!Array.isArray(privateOffset) || privateOffset.length !== 2) {
+ parentDict.removeByName('Private');
+ return;
+ }
+
+ var size = privateOffset[0];
+ var offset = privateOffset[1];
+
+ if (size === 0 || offset >= this.bytes.length) {
+ this.emptyPrivateDictionary(parentDict);
+ return;
+ }
+
+ var privateDictEnd = offset + size;
+ var dictData = this.bytes.subarray(offset, privateDictEnd);
+ var dict = this.parseDict(dictData);
+ var privateDict = this.createDict(CFFPrivateDict, dict, parentDict.strings);
+ parentDict.privateDict = privateDict;
+
+ if (!privateDict.getByName('Subrs')) {
+ return;
+ }
+
+ var subrsOffset = privateDict.getByName('Subrs');
+ var relativeOffset = offset + subrsOffset;
+
+ if (subrsOffset === 0 || relativeOffset >= this.bytes.length) {
+ this.emptyPrivateDictionary(parentDict);
+ return;
+ }
+
+ var subrsIndex = this.parseIndex(relativeOffset);
+ privateDict.subrsIndex = subrsIndex.obj;
+ },
+ parseCharsets: function CFFParser_parseCharsets(pos, length, strings, cid) {
+ if (pos === 0) {
+ return new CFFCharset(true, CFFCharsetPredefinedTypes.ISO_ADOBE, _charsets.ISOAdobeCharset);
+ } else if (pos === 1) {
+ return new CFFCharset(true, CFFCharsetPredefinedTypes.EXPERT, _charsets.ExpertCharset);
+ } else if (pos === 2) {
+ return new CFFCharset(true, CFFCharsetPredefinedTypes.EXPERT_SUBSET, _charsets.ExpertSubsetCharset);
+ }
+
+ var bytes = this.bytes;
+ var start = pos;
+ var format = bytes[pos++];
+ var charset = ['.notdef'];
+ var id, count, i;
+ length -= 1;
+
+ switch (format) {
+ case 0:
+ for (i = 0; i < length; i++) {
+ id = bytes[pos++] << 8 | bytes[pos++];
+ charset.push(cid ? id : strings.get(id));
+ }
+
+ break;
+
+ case 1:
+ while (charset.length <= length) {
+ id = bytes[pos++] << 8 | bytes[pos++];
+ count = bytes[pos++];
+
+ for (i = 0; i <= count; i++) {
+ charset.push(cid ? id++ : strings.get(id++));
+ }
+ }
+
+ break;
+
+ case 2:
+ while (charset.length <= length) {
+ id = bytes[pos++] << 8 | bytes[pos++];
+ count = bytes[pos++] << 8 | bytes[pos++];
+
+ for (i = 0; i <= count; i++) {
+ charset.push(cid ? id++ : strings.get(id++));
+ }
+ }
+
+ break;
+
+ default:
+ throw new _util.FormatError('Unknown charset format');
+ }
+
+ var end = pos;
+ var raw = bytes.subarray(start, end);
+ return new CFFCharset(false, format, charset, raw);
+ },
+ parseEncoding: function CFFParser_parseEncoding(pos, properties, strings, charset) {
+ var encoding = Object.create(null);
+ var bytes = this.bytes;
+ var predefined = false;
+ var format, i, ii;
+ var raw = null;
+
+ function readSupplement() {
+ var supplementsCount = bytes[pos++];
+
+ for (i = 0; i < supplementsCount; i++) {
+ var code = bytes[pos++];
+ var sid = (bytes[pos++] << 8) + (bytes[pos++] & 0xff);
+ encoding[code] = charset.indexOf(strings.get(sid));
+ }
+ }
+
+ if (pos === 0 || pos === 1) {
+ predefined = true;
+ format = pos;
+ var baseEncoding = pos ? _encodings.ExpertEncoding : _encodings.StandardEncoding;
+
+ for (i = 0, ii = charset.length; i < ii; i++) {
+ var index = baseEncoding.indexOf(charset[i]);
+
+ if (index !== -1) {
+ encoding[index] = i;
+ }
+ }
+ } else {
+ var dataStart = pos;
+ format = bytes[pos++];
+
+ switch (format & 0x7f) {
+ case 0:
+ var glyphsCount = bytes[pos++];
+
+ for (i = 1; i <= glyphsCount; i++) {
+ encoding[bytes[pos++]] = i;
+ }
+
+ break;
+
+ case 1:
+ var rangesCount = bytes[pos++];
+ var gid = 1;
+
+ for (i = 0; i < rangesCount; i++) {
+ var start = bytes[pos++];
+ var left = bytes[pos++];
+
+ for (var j = start; j <= start + left; j++) {
+ encoding[j] = gid++;
+ }
+ }
+
+ break;
+
+ default:
+ throw new _util.FormatError("Unknown encoding format: ".concat(format, " in CFF"));
+ }
+
+ var dataEnd = pos;
+
+ if (format & 0x80) {
+ bytes[dataStart] &= 0x7f;
+ readSupplement();
+ }
+
+ raw = bytes.subarray(dataStart, dataEnd);
+ }
+
+ format = format & 0x7f;
+ return new CFFEncoding(predefined, format, encoding, raw);
+ },
+ parseFDSelect: function CFFParser_parseFDSelect(pos, length) {
+ var bytes = this.bytes;
+ var format = bytes[pos++];
+ var fdSelect = [];
+ var i;
+
+ switch (format) {
+ case 0:
+ for (i = 0; i < length; ++i) {
+ var id = bytes[pos++];
+ fdSelect.push(id);
+ }
+
+ break;
+
+ case 3:
+ var rangesCount = bytes[pos++] << 8 | bytes[pos++];
+
+ for (i = 0; i < rangesCount; ++i) {
+ var first = bytes[pos++] << 8 | bytes[pos++];
+
+ if (i === 0 && first !== 0) {
+ (0, _util.warn)('parseFDSelect: The first range must have a first GID of 0' + ' -- trying to recover.');
+ first = 0;
+ }
+
+ var fdIndex = bytes[pos++];
+ var next = bytes[pos] << 8 | bytes[pos + 1];
+
+ for (var j = first; j < next; ++j) {
+ fdSelect.push(fdIndex);
+ }
+ }
+
+ pos += 2;
+ break;
+
+ default:
+ throw new _util.FormatError("parseFDSelect: Unknown format \"".concat(format, "\"."));
+ }
+
+ if (fdSelect.length !== length) {
+ throw new _util.FormatError('parseFDSelect: Invalid font data.');
+ }
+
+ return new CFFFDSelect(format, fdSelect);
+ }
+ };
+ return CFFParser;
+}();
+
+exports.CFFParser = CFFParser;
+
+var CFF = function CFFClosure() {
+ function CFF() {
+ this.header = null;
+ this.names = [];
+ this.topDict = null;
+ this.strings = new CFFStrings();
+ this.globalSubrIndex = null;
+ this.encoding = null;
+ this.charset = null;
+ this.charStrings = null;
+ this.fdArray = [];
+ this.fdSelect = null;
+ this.isCIDFont = false;
+ }
+
+ CFF.prototype = {
+ duplicateFirstGlyph: function CFF_duplicateFirstGlyph() {
+ if (this.charStrings.count >= 65535) {
+ (0, _util.warn)('Not enough space in charstrings to duplicate first glyph.');
+ return;
+ }
+
+ var glyphZero = this.charStrings.get(0);
+ this.charStrings.add(glyphZero);
+
+ if (this.isCIDFont) {
+ this.fdSelect.fdSelect.push(this.fdSelect.fdSelect[0]);
+ }
+ },
+ hasGlyphId: function CFF_hasGlyphID(id) {
+ if (id < 0 || id >= this.charStrings.count) {
+ return false;
+ }
+
+ var glyph = this.charStrings.get(id);
+ return glyph.length > 0;
+ }
+ };
+ return CFF;
+}();
+
+exports.CFF = CFF;
+
+var CFFHeader = function CFFHeaderClosure() {
+ function CFFHeader(major, minor, hdrSize, offSize) {
+ this.major = major;
+ this.minor = minor;
+ this.hdrSize = hdrSize;
+ this.offSize = offSize;
+ }
+
+ return CFFHeader;
+}();
+
+exports.CFFHeader = CFFHeader;
+
+var CFFStrings = function CFFStringsClosure() {
+ function CFFStrings() {
+ this.strings = [];
+ }
+
+ CFFStrings.prototype = {
+ get: function CFFStrings_get(index) {
+ if (index >= 0 && index <= NUM_STANDARD_CFF_STRINGS - 1) {
+ return CFFStandardStrings[index];
+ }
+
+ if (index - NUM_STANDARD_CFF_STRINGS <= this.strings.length) {
+ return this.strings[index - NUM_STANDARD_CFF_STRINGS];
+ }
+
+ return CFFStandardStrings[0];
+ },
+ getSID: function CFFStrings_getSID(str) {
+ var index = CFFStandardStrings.indexOf(str);
+
+ if (index !== -1) {
+ return index;
+ }
+
+ index = this.strings.indexOf(str);
+
+ if (index !== -1) {
+ return index + NUM_STANDARD_CFF_STRINGS;
+ }
+
+ return -1;
+ },
+ add: function CFFStrings_add(value) {
+ this.strings.push(value);
+ },
+
+ get count() {
+ return this.strings.length;
+ }
+
+ };
+ return CFFStrings;
+}();
+
+exports.CFFStrings = CFFStrings;
+
+var CFFIndex = function CFFIndexClosure() {
+ function CFFIndex() {
+ this.objects = [];
+ this.length = 0;
+ }
+
+ CFFIndex.prototype = {
+ add: function CFFIndex_add(data) {
+ this.length += data.length;
+ this.objects.push(data);
+ },
+ set: function CFFIndex_set(index, data) {
+ this.length += data.length - this.objects[index].length;
+ this.objects[index] = data;
+ },
+ get: function CFFIndex_get(index) {
+ return this.objects[index];
+ },
+
+ get count() {
+ return this.objects.length;
+ }
+
+ };
+ return CFFIndex;
+}();
+
+exports.CFFIndex = CFFIndex;
+
+var CFFDict = function CFFDictClosure() {
+ function CFFDict(tables, strings) {
+ this.keyToNameMap = tables.keyToNameMap;
+ this.nameToKeyMap = tables.nameToKeyMap;
+ this.defaults = tables.defaults;
+ this.types = tables.types;
+ this.opcodes = tables.opcodes;
+ this.order = tables.order;
+ this.strings = strings;
+ this.values = Object.create(null);
+ }
+
+ CFFDict.prototype = {
+ setByKey: function CFFDict_setByKey(key, value) {
+ if (!(key in this.keyToNameMap)) {
+ return false;
+ }
+
+ var valueLength = value.length;
+
+ if (valueLength === 0) {
+ return true;
+ }
+
+ for (var i = 0; i < valueLength; i++) {
+ if (isNaN(value[i])) {
+ (0, _util.warn)('Invalid CFFDict value: "' + value + '" for key "' + key + '".');
+ return true;
+ }
+ }
+
+ var type = this.types[key];
+
+ if (type === 'num' || type === 'sid' || type === 'offset') {
+ value = value[0];
+ }
+
+ this.values[key] = value;
+ return true;
+ },
+ setByName: function CFFDict_setByName(name, value) {
+ if (!(name in this.nameToKeyMap)) {
+ throw new _util.FormatError("Invalid dictionary name \"".concat(name, "\""));
+ }
+
+ this.values[this.nameToKeyMap[name]] = value;
+ },
+ hasName: function CFFDict_hasName(name) {
+ return this.nameToKeyMap[name] in this.values;
+ },
+ getByName: function CFFDict_getByName(name) {
+ if (!(name in this.nameToKeyMap)) {
+ throw new _util.FormatError("Invalid dictionary name ".concat(name, "\""));
+ }
+
+ var key = this.nameToKeyMap[name];
+
+ if (!(key in this.values)) {
+ return this.defaults[key];
+ }
+
+ return this.values[key];
+ },
+ removeByName: function CFFDict_removeByName(name) {
+ delete this.values[this.nameToKeyMap[name]];
+ }
+ };
+
+ CFFDict.createTables = function CFFDict_createTables(layout) {
+ var tables = {
+ keyToNameMap: {},
+ nameToKeyMap: {},
+ defaults: {},
+ types: {},
+ opcodes: {},
+ order: []
+ };
+
+ for (var i = 0, ii = layout.length; i < ii; ++i) {
+ var entry = layout[i];
+ var key = Array.isArray(entry[0]) ? (entry[0][0] << 8) + entry[0][1] : entry[0];
+ tables.keyToNameMap[key] = entry[1];
+ tables.nameToKeyMap[entry[1]] = key;
+ tables.types[key] = entry[2];
+ tables.defaults[key] = entry[3];
+ tables.opcodes[key] = Array.isArray(entry[0]) ? entry[0] : [entry[0]];
+ tables.order.push(key);
+ }
+
+ return tables;
+ };
+
+ return CFFDict;
+}();
+
+var CFFTopDict = function CFFTopDictClosure() {
+ var layout = [[[12, 30], 'ROS', ['sid', 'sid', 'num'], null], [[12, 20], 'SyntheticBase', 'num', null], [0, 'version', 'sid', null], [1, 'Notice', 'sid', null], [[12, 0], 'Copyright', 'sid', null], [2, 'FullName', 'sid', null], [3, 'FamilyName', 'sid', null], [4, 'Weight', 'sid', null], [[12, 1], 'isFixedPitch', 'num', 0], [[12, 2], 'ItalicAngle', 'num', 0], [[12, 3], 'UnderlinePosition', 'num', -100], [[12, 4], 'UnderlineThickness', 'num', 50], [[12, 5], 'PaintType', 'num', 0], [[12, 6], 'CharstringType', 'num', 2], [[12, 7], 'FontMatrix', ['num', 'num', 'num', 'num', 'num', 'num'], [0.001, 0, 0, 0.001, 0, 0]], [13, 'UniqueID', 'num', null], [5, 'FontBBox', ['num', 'num', 'num', 'num'], [0, 0, 0, 0]], [[12, 8], 'StrokeWidth', 'num', 0], [14, 'XUID', 'array', null], [15, 'charset', 'offset', 0], [16, 'Encoding', 'offset', 0], [17, 'CharStrings', 'offset', 0], [18, 'Private', ['offset', 'offset'], null], [[12, 21], 'PostScript', 'sid', null], [[12, 22], 'BaseFontName', 'sid', null], [[12, 23], 'BaseFontBlend', 'delta', null], [[12, 31], 'CIDFontVersion', 'num', 0], [[12, 32], 'CIDFontRevision', 'num', 0], [[12, 33], 'CIDFontType', 'num', 0], [[12, 34], 'CIDCount', 'num', 8720], [[12, 35], 'UIDBase', 'num', null], [[12, 37], 'FDSelect', 'offset', null], [[12, 36], 'FDArray', 'offset', null], [[12, 38], 'FontName', 'sid', null]];
+ var tables = null;
+
+ function CFFTopDict(strings) {
+ if (tables === null) {
+ tables = CFFDict.createTables(layout);
+ }
+
+ CFFDict.call(this, tables, strings);
+ this.privateDict = null;
+ }
+
+ CFFTopDict.prototype = Object.create(CFFDict.prototype);
+ return CFFTopDict;
+}();
+
+exports.CFFTopDict = CFFTopDict;
+
+var CFFPrivateDict = function CFFPrivateDictClosure() {
+ var layout = [[6, 'BlueValues', 'delta', null], [7, 'OtherBlues', 'delta', null], [8, 'FamilyBlues', 'delta', null], [9, 'FamilyOtherBlues', 'delta', null], [[12, 9], 'BlueScale', 'num', 0.039625], [[12, 10], 'BlueShift', 'num', 7], [[12, 11], 'BlueFuzz', 'num', 1], [10, 'StdHW', 'num', null], [11, 'StdVW', 'num', null], [[12, 12], 'StemSnapH', 'delta', null], [[12, 13], 'StemSnapV', 'delta', null], [[12, 14], 'ForceBold', 'num', 0], [[12, 17], 'LanguageGroup', 'num', 0], [[12, 18], 'ExpansionFactor', 'num', 0.06], [[12, 19], 'initialRandomSeed', 'num', 0], [20, 'defaultWidthX', 'num', 0], [21, 'nominalWidthX', 'num', 0], [19, 'Subrs', 'offset', null]];
+ var tables = null;
+
+ function CFFPrivateDict(strings) {
+ if (tables === null) {
+ tables = CFFDict.createTables(layout);
+ }
+
+ CFFDict.call(this, tables, strings);
+ this.subrsIndex = null;
+ }
+
+ CFFPrivateDict.prototype = Object.create(CFFDict.prototype);
+ return CFFPrivateDict;
+}();
+
+exports.CFFPrivateDict = CFFPrivateDict;
+var CFFCharsetPredefinedTypes = {
+ ISO_ADOBE: 0,
+ EXPERT: 1,
+ EXPERT_SUBSET: 2
+};
+
+var CFFCharset = function CFFCharsetClosure() {
+ function CFFCharset(predefined, format, charset, raw) {
+ this.predefined = predefined;
+ this.format = format;
+ this.charset = charset;
+ this.raw = raw;
+ }
+
+ return CFFCharset;
+}();
+
+exports.CFFCharset = CFFCharset;
+
+var CFFEncoding = function CFFEncodingClosure() {
+ function CFFEncoding(predefined, format, encoding, raw) {
+ this.predefined = predefined;
+ this.format = format;
+ this.encoding = encoding;
+ this.raw = raw;
+ }
+
+ return CFFEncoding;
+}();
+
+var CFFFDSelect = function CFFFDSelectClosure() {
+ function CFFFDSelect(format, fdSelect) {
+ this.format = format;
+ this.fdSelect = fdSelect;
+ }
+
+ CFFFDSelect.prototype = {
+ getFDIndex: function CFFFDSelect_get(glyphIndex) {
+ if (glyphIndex < 0 || glyphIndex >= this.fdSelect.length) {
+ return -1;
+ }
+
+ return this.fdSelect[glyphIndex];
+ }
+ };
+ return CFFFDSelect;
+}();
+
+exports.CFFFDSelect = CFFFDSelect;
+
+var CFFOffsetTracker = function CFFOffsetTrackerClosure() {
+ function CFFOffsetTracker() {
+ this.offsets = Object.create(null);
+ }
+
+ CFFOffsetTracker.prototype = {
+ isTracking: function CFFOffsetTracker_isTracking(key) {
+ return key in this.offsets;
+ },
+ track: function CFFOffsetTracker_track(key, location) {
+ if (key in this.offsets) {
+ throw new _util.FormatError("Already tracking location of ".concat(key));
+ }
+
+ this.offsets[key] = location;
+ },
+ offset: function CFFOffsetTracker_offset(value) {
+ for (var key in this.offsets) {
+ this.offsets[key] += value;
+ }
+ },
+ setEntryLocation: function CFFOffsetTracker_setEntryLocation(key, values, output) {
+ if (!(key in this.offsets)) {
+ throw new _util.FormatError("Not tracking location of ".concat(key));
+ }
+
+ var data = output.data;
+ var dataOffset = this.offsets[key];
+ var size = 5;
+
+ for (var i = 0, ii = values.length; i < ii; ++i) {
+ var offset0 = i * size + dataOffset;
+ var offset1 = offset0 + 1;
+ var offset2 = offset0 + 2;
+ var offset3 = offset0 + 3;
+ var offset4 = offset0 + 4;
+
+ if (data[offset0] !== 0x1d || data[offset1] !== 0 || data[offset2] !== 0 || data[offset3] !== 0 || data[offset4] !== 0) {
+ throw new _util.FormatError('writing to an offset that is not empty');
+ }
+
+ var value = values[i];
+ data[offset0] = 0x1d;
+ data[offset1] = value >> 24 & 0xFF;
+ data[offset2] = value >> 16 & 0xFF;
+ data[offset3] = value >> 8 & 0xFF;
+ data[offset4] = value & 0xFF;
+ }
+ }
+ };
+ return CFFOffsetTracker;
+}();
+
+var CFFCompiler = function CFFCompilerClosure() {
+ function CFFCompiler(cff) {
+ this.cff = cff;
+ }
+
+ CFFCompiler.prototype = {
+ compile: function CFFCompiler_compile() {
+ var cff = this.cff;
+ var output = {
+ data: [],
+ length: 0,
+ add: function CFFCompiler_add(data) {
+ this.data = this.data.concat(data);
+ this.length = this.data.length;
+ }
+ };
+ var header = this.compileHeader(cff.header);
+ output.add(header);
+ var nameIndex = this.compileNameIndex(cff.names);
+ output.add(nameIndex);
+
+ if (cff.isCIDFont) {
+ if (cff.topDict.hasName('FontMatrix')) {
+ var base = cff.topDict.getByName('FontMatrix');
+ cff.topDict.removeByName('FontMatrix');
+
+ for (var i = 0, ii = cff.fdArray.length; i < ii; i++) {
+ var subDict = cff.fdArray[i];
+ var matrix = base.slice(0);
+
+ if (subDict.hasName('FontMatrix')) {
+ matrix = _util.Util.transform(matrix, subDict.getByName('FontMatrix'));
+ }
+
+ subDict.setByName('FontMatrix', matrix);
+ }
+ }
+ }
+
+ cff.topDict.setByName('charset', 0);
+ var compiled = this.compileTopDicts([cff.topDict], output.length, cff.isCIDFont);
+ output.add(compiled.output);
+ var topDictTracker = compiled.trackers[0];
+ var stringIndex = this.compileStringIndex(cff.strings.strings);
+ output.add(stringIndex);
+ var globalSubrIndex = this.compileIndex(cff.globalSubrIndex);
+ output.add(globalSubrIndex);
+
+ if (cff.encoding && cff.topDict.hasName('Encoding')) {
+ if (cff.encoding.predefined) {
+ topDictTracker.setEntryLocation('Encoding', [cff.encoding.format], output);
+ } else {
+ var encoding = this.compileEncoding(cff.encoding);
+ topDictTracker.setEntryLocation('Encoding', [output.length], output);
+ output.add(encoding);
+ }
+ }
+
+ var charset = this.compileCharset(cff.charset, cff.charStrings.count, cff.strings, cff.isCIDFont);
+ topDictTracker.setEntryLocation('charset', [output.length], output);
+ output.add(charset);
+ var charStrings = this.compileCharStrings(cff.charStrings);
+ topDictTracker.setEntryLocation('CharStrings', [output.length], output);
+ output.add(charStrings);
+
+ if (cff.isCIDFont) {
+ topDictTracker.setEntryLocation('FDSelect', [output.length], output);
+ var fdSelect = this.compileFDSelect(cff.fdSelect);
+ output.add(fdSelect);
+ compiled = this.compileTopDicts(cff.fdArray, output.length, true);
+ topDictTracker.setEntryLocation('FDArray', [output.length], output);
+ output.add(compiled.output);
+ var fontDictTrackers = compiled.trackers;
+ this.compilePrivateDicts(cff.fdArray, fontDictTrackers, output);
+ }
+
+ this.compilePrivateDicts([cff.topDict], [topDictTracker], output);
+ output.add([0]);
+ return output.data;
+ },
+ encodeNumber: function CFFCompiler_encodeNumber(value) {
+ if (parseFloat(value) === parseInt(value, 10) && !isNaN(value)) {
+ return this.encodeInteger(value);
+ }
+
+ return this.encodeFloat(value);
+ },
+ encodeFloat: function CFFCompiler_encodeFloat(num) {
+ var value = num.toString();
+ var m = /\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/.exec(value);
+
+ if (m) {
+ var epsilon = parseFloat('1e' + ((m[2] ? +m[2] : 0) + m[1].length));
+ value = (Math.round(num * epsilon) / epsilon).toString();
+ }
+
+ var nibbles = '';
+ var i, ii;
+
+ for (i = 0, ii = value.length; i < ii; ++i) {
+ var a = value[i];
+
+ if (a === 'e') {
+ nibbles += value[++i] === '-' ? 'c' : 'b';
+ } else if (a === '.') {
+ nibbles += 'a';
+ } else if (a === '-') {
+ nibbles += 'e';
+ } else {
+ nibbles += a;
+ }
+ }
+
+ nibbles += nibbles.length & 1 ? 'f' : 'ff';
+ var out = [30];
+
+ for (i = 0, ii = nibbles.length; i < ii; i += 2) {
+ out.push(parseInt(nibbles.substring(i, i + 2), 16));
+ }
+
+ return out;
+ },
+ encodeInteger: function CFFCompiler_encodeInteger(value) {
+ var code;
+
+ if (value >= -107 && value <= 107) {
+ code = [value + 139];
+ } else if (value >= 108 && value <= 1131) {
+ value = value - 108;
+ code = [(value >> 8) + 247, value & 0xFF];
+ } else if (value >= -1131 && value <= -108) {
+ value = -value - 108;
+ code = [(value >> 8) + 251, value & 0xFF];
+ } else if (value >= -32768 && value <= 32767) {
+ code = [0x1c, value >> 8 & 0xFF, value & 0xFF];
+ } else {
+ code = [0x1d, value >> 24 & 0xFF, value >> 16 & 0xFF, value >> 8 & 0xFF, value & 0xFF];
+ }
+
+ return code;
+ },
+ compileHeader: function CFFCompiler_compileHeader(header) {
+ return [header.major, header.minor, header.hdrSize, header.offSize];
+ },
+ compileNameIndex: function CFFCompiler_compileNameIndex(names) {
+ var nameIndex = new CFFIndex();
+
+ for (var i = 0, ii = names.length; i < ii; ++i) {
+ var name = names[i];
+ var length = Math.min(name.length, 127);
+ var sanitizedName = new Array(length);
+
+ for (var j = 0; j < length; j++) {
+ var _char = name[j];
+
+ if (_char < '!' || _char > '~' || _char === '[' || _char === ']' || _char === '(' || _char === ')' || _char === '{' || _char === '}' || _char === '<' || _char === '>' || _char === '/' || _char === '%') {
+ _char = '_';
+ }
+
+ sanitizedName[j] = _char;
+ }
+
+ sanitizedName = sanitizedName.join('');
+
+ if (sanitizedName === '') {
+ sanitizedName = 'Bad_Font_Name';
+ }
+
+ nameIndex.add((0, _util.stringToBytes)(sanitizedName));
+ }
+
+ return this.compileIndex(nameIndex);
+ },
+ compileTopDicts: function CFFCompiler_compileTopDicts(dicts, length, removeCidKeys) {
+ var fontDictTrackers = [];
+ var fdArrayIndex = new CFFIndex();
+
+ for (var i = 0, ii = dicts.length; i < ii; ++i) {
+ var fontDict = dicts[i];
+
+ if (removeCidKeys) {
+ fontDict.removeByName('CIDFontVersion');
+ fontDict.removeByName('CIDFontRevision');
+ fontDict.removeByName('CIDFontType');
+ fontDict.removeByName('CIDCount');
+ fontDict.removeByName('UIDBase');
+ }
+
+ var fontDictTracker = new CFFOffsetTracker();
+ var fontDictData = this.compileDict(fontDict, fontDictTracker);
+ fontDictTrackers.push(fontDictTracker);
+ fdArrayIndex.add(fontDictData);
+ fontDictTracker.offset(length);
+ }
+
+ fdArrayIndex = this.compileIndex(fdArrayIndex, fontDictTrackers);
+ return {
+ trackers: fontDictTrackers,
+ output: fdArrayIndex
+ };
+ },
+ compilePrivateDicts: function CFFCompiler_compilePrivateDicts(dicts, trackers, output) {
+ for (var i = 0, ii = dicts.length; i < ii; ++i) {
+ var fontDict = dicts[i];
+ var privateDict = fontDict.privateDict;
+
+ if (!privateDict || !fontDict.hasName('Private')) {
+ throw new _util.FormatError('There must be a private dictionary.');
+ }
+
+ var privateDictTracker = new CFFOffsetTracker();
+ var privateDictData = this.compileDict(privateDict, privateDictTracker);
+ var outputLength = output.length;
+ privateDictTracker.offset(outputLength);
+
+ if (!privateDictData.length) {
+ outputLength = 0;
+ }
+
+ trackers[i].setEntryLocation('Private', [privateDictData.length, outputLength], output);
+ output.add(privateDictData);
+
+ if (privateDict.subrsIndex && privateDict.hasName('Subrs')) {
+ var subrs = this.compileIndex(privateDict.subrsIndex);
+ privateDictTracker.setEntryLocation('Subrs', [privateDictData.length], output);
+ output.add(subrs);
+ }
+ }
+ },
+ compileDict: function CFFCompiler_compileDict(dict, offsetTracker) {
+ var out = [];
+ var order = dict.order;
+
+ for (var i = 0; i < order.length; ++i) {
+ var key = order[i];
+
+ if (!(key in dict.values)) {
+ continue;
+ }
+
+ var values = dict.values[key];
+ var types = dict.types[key];
+
+ if (!Array.isArray(types)) {
+ types = [types];
+ }
+
+ if (!Array.isArray(values)) {
+ values = [values];
+ }
+
+ if (values.length === 0) {
+ continue;
+ }
+
+ for (var j = 0, jj = types.length; j < jj; ++j) {
+ var type = types[j];
+ var value = values[j];
+
+ switch (type) {
+ case 'num':
+ case 'sid':
+ out = out.concat(this.encodeNumber(value));
+ break;
+
+ case 'offset':
+ var name = dict.keyToNameMap[key];
+
+ if (!offsetTracker.isTracking(name)) {
+ offsetTracker.track(name, out.length);
+ }
+
+ out = out.concat([0x1d, 0, 0, 0, 0]);
+ break;
+
+ case 'array':
+ case 'delta':
+ out = out.concat(this.encodeNumber(value));
+
+ for (var k = 1, kk = values.length; k < kk; ++k) {
+ out = out.concat(this.encodeNumber(values[k]));
+ }
+
+ break;
+
+ default:
+ throw new _util.FormatError("Unknown data type of ".concat(type));
+ }
+ }
+
+ out = out.concat(dict.opcodes[key]);
+ }
+
+ return out;
+ },
+ compileStringIndex: function CFFCompiler_compileStringIndex(strings) {
+ var stringIndex = new CFFIndex();
+
+ for (var i = 0, ii = strings.length; i < ii; ++i) {
+ stringIndex.add((0, _util.stringToBytes)(strings[i]));
+ }
+
+ return this.compileIndex(stringIndex);
+ },
+ compileGlobalSubrIndex: function CFFCompiler_compileGlobalSubrIndex() {
+ var globalSubrIndex = this.cff.globalSubrIndex;
+ this.out.writeByteArray(this.compileIndex(globalSubrIndex));
+ },
+ compileCharStrings: function CFFCompiler_compileCharStrings(charStrings) {
+ var charStringsIndex = new CFFIndex();
+
+ for (var i = 0; i < charStrings.count; i++) {
+ var glyph = charStrings.get(i);
+
+ if (glyph.length === 0) {
+ charStringsIndex.add(new Uint8Array([0x8B, 0x0E]));
+ continue;
+ }
+
+ charStringsIndex.add(glyph);
+ }
+
+ return this.compileIndex(charStringsIndex);
+ },
+ compileCharset: function CFFCompiler_compileCharset(charset, numGlyphs, strings, isCIDFont) {
+ var out;
+ var numGlyphsLessNotDef = numGlyphs - 1;
+
+ if (isCIDFont) {
+ out = new Uint8Array([2, 0, 0, numGlyphsLessNotDef >> 8 & 0xFF, numGlyphsLessNotDef & 0xFF]);
+ } else {
+ var length = 1 + numGlyphsLessNotDef * 2;
+ out = new Uint8Array(length);
+ out[0] = 0;
+ var charsetIndex = 0;
+ var numCharsets = charset.charset.length;
+ var warned = false;
+
+ for (var i = 1; i < out.length; i += 2) {
+ var sid = 0;
+
+ if (charsetIndex < numCharsets) {
+ var name = charset.charset[charsetIndex++];
+ sid = strings.getSID(name);
+
+ if (sid === -1) {
+ sid = 0;
+
+ if (!warned) {
+ warned = true;
+ (0, _util.warn)("Couldn't find ".concat(name, " in CFF strings"));
+ }
+ }
+ }
+
+ out[i] = sid >> 8 & 0xFF;
+ out[i + 1] = sid & 0xFF;
+ }
+ }
+
+ return this.compileTypedArray(out);
+ },
+ compileEncoding: function CFFCompiler_compileEncoding(encoding) {
+ return this.compileTypedArray(encoding.raw);
+ },
+ compileFDSelect: function CFFCompiler_compileFDSelect(fdSelect) {
+ var format = fdSelect.format;
+ var out, i;
+
+ switch (format) {
+ case 0:
+ out = new Uint8Array(1 + fdSelect.fdSelect.length);
+ out[0] = format;
+
+ for (i = 0; i < fdSelect.fdSelect.length; i++) {
+ out[i + 1] = fdSelect.fdSelect[i];
+ }
+
+ break;
+
+ case 3:
+ var start = 0;
+ var lastFD = fdSelect.fdSelect[0];
+ var ranges = [format, 0, 0, start >> 8 & 0xFF, start & 0xFF, lastFD];
+
+ for (i = 1; i < fdSelect.fdSelect.length; i++) {
+ var currentFD = fdSelect.fdSelect[i];
+
+ if (currentFD !== lastFD) {
+ ranges.push(i >> 8 & 0xFF, i & 0xFF, currentFD);
+ lastFD = currentFD;
+ }
+ }
+
+ var numRanges = (ranges.length - 3) / 3;
+ ranges[1] = numRanges >> 8 & 0xFF;
+ ranges[2] = numRanges & 0xFF;
+ ranges.push(i >> 8 & 0xFF, i & 0xFF);
+ out = new Uint8Array(ranges);
+ break;
+ }
+
+ return this.compileTypedArray(out);
+ },
+ compileTypedArray: function CFFCompiler_compileTypedArray(data) {
+ var out = [];
+
+ for (var i = 0, ii = data.length; i < ii; ++i) {
+ out[i] = data[i];
+ }
+
+ return out;
+ },
+ compileIndex: function CFFCompiler_compileIndex(index, trackers) {
+ trackers = trackers || [];
+ var objects = index.objects;
+ var count = objects.length;
+
+ if (count === 0) {
+ return [0, 0, 0];
+ }
+
+ var data = [count >> 8 & 0xFF, count & 0xff];
+ var lastOffset = 1,
+ i;
+
+ for (i = 0; i < count; ++i) {
+ lastOffset += objects[i].length;
+ }
+
+ var offsetSize;
+
+ if (lastOffset < 0x100) {
+ offsetSize = 1;
+ } else if (lastOffset < 0x10000) {
+ offsetSize = 2;
+ } else if (lastOffset < 0x1000000) {
+ offsetSize = 3;
+ } else {
+ offsetSize = 4;
+ }
+
+ data.push(offsetSize);
+ var relativeOffset = 1;
+
+ for (i = 0; i < count + 1; i++) {
+ if (offsetSize === 1) {
+ data.push(relativeOffset & 0xFF);
+ } else if (offsetSize === 2) {
+ data.push(relativeOffset >> 8 & 0xFF, relativeOffset & 0xFF);
+ } else if (offsetSize === 3) {
+ data.push(relativeOffset >> 16 & 0xFF, relativeOffset >> 8 & 0xFF, relativeOffset & 0xFF);
+ } else {
+ data.push(relativeOffset >>> 24 & 0xFF, relativeOffset >> 16 & 0xFF, relativeOffset >> 8 & 0xFF, relativeOffset & 0xFF);
+ }
+
+ if (objects[i]) {
+ relativeOffset += objects[i].length;
+ }
+ }
+
+ for (i = 0; i < count; i++) {
+ if (trackers[i]) {
+ trackers[i].offset(data.length);
+ }
+
+ for (var j = 0, jj = objects[i].length; j < jj; j++) {
+ data.push(objects[i][j]);
+ }
+ }
+
+ return data;
+ }
+ };
+ return CFFCompiler;
+}();
+
+exports.CFFCompiler = CFFCompiler;
+
+/***/ }),
+/* 176 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ExpertSubsetCharset = exports.ExpertCharset = exports.ISOAdobeCharset = void 0;
+var ISOAdobeCharset = ['.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quoteright', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore', 'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', 'exclamdown', 'cent', 'sterling', 'fraction', 'yen', 'florin', 'section', 'currency', 'quotesingle', 'quotedblleft', 'guillemotleft', 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'endash', 'dagger', 'daggerdbl', 'periodcentered', 'paragraph', 'bullet', 'quotesinglbase', 'quotedblbase', 'quotedblright', 'guillemotright', 'ellipsis', 'perthousand', 'questiondown', 'grave', 'acute', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'dieresis', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron', 'emdash', 'AE', 'ordfeminine', 'Lslash', 'Oslash', 'OE', 'ordmasculine', 'ae', 'dotlessi', 'lslash', 'oslash', 'oe', 'germandbls', 'onesuperior', 'logicalnot', 'mu', 'trademark', 'Eth', 'onehalf', 'plusminus', 'Thorn', 'onequarter', 'divide', 'brokenbar', 'degree', 'thorn', 'threequarters', 'twosuperior', 'registered', 'minus', 'eth', 'multiply', 'threesuperior', 'copyright', 'Aacute', 'Acircumflex', 'Adieresis', 'Agrave', 'Aring', 'Atilde', 'Ccedilla', 'Eacute', 'Ecircumflex', 'Edieresis', 'Egrave', 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Ntilde', 'Oacute', 'Ocircumflex', 'Odieresis', 'Ograve', 'Otilde', 'Scaron', 'Uacute', 'Ucircumflex', 'Udieresis', 'Ugrave', 'Yacute', 'Ydieresis', 'Zcaron', 'aacute', 'acircumflex', 'adieresis', 'agrave', 'aring', 'atilde', 'ccedilla', 'eacute', 'ecircumflex', 'edieresis', 'egrave', 'iacute', 'icircumflex', 'idieresis', 'igrave', 'ntilde', 'oacute', 'ocircumflex', 'odieresis', 'ograve', 'otilde', 'scaron', 'uacute', 'ucircumflex', 'udieresis', 'ugrave', 'yacute', 'ydieresis', 'zcaron'];
+exports.ISOAdobeCharset = ISOAdobeCharset;
+var ExpertCharset = ['.notdef', 'space', 'exclamsmall', 'Hungarumlautsmall', 'dollaroldstyle', 'dollarsuperior', 'ampersandsmall', 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', 'onedotenleader', 'comma', 'hyphen', 'period', 'fraction', 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'colon', 'semicolon', 'commasuperior', 'threequartersemdash', 'periodsuperior', 'questionsmall', 'asuperior', 'bsuperior', 'centsuperior', 'dsuperior', 'esuperior', 'isuperior', 'lsuperior', 'msuperior', 'nsuperior', 'osuperior', 'rsuperior', 'ssuperior', 'tsuperior', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'parenleftinferior', 'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', 'Asmall', 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', 'Osmall', 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall', 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall', 'exclamdownsmall', 'centoldstyle', 'Lslashsmall', 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall', 'Brevesmall', 'Caronsmall', 'Dotaccentsmall', 'Macronsmall', 'figuredash', 'hypheninferior', 'Ogoneksmall', 'Ringsmall', 'Cedillasmall', 'onequarter', 'onehalf', 'threequarters', 'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds', 'zerosuperior', 'onesuperior', 'twosuperior', 'threesuperior', 'foursuperior', 'fivesuperior', 'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior', 'zeroinferior', 'oneinferior', 'twoinferior', 'threeinferior', 'fourinferior', 'fiveinferior', 'sixinferior', 'seveninferior', 'eightinferior', 'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior', 'commainferior', 'Agravesmall', 'Aacutesmall', 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall', 'Aringsmall', 'AEsmall', 'Ccedillasmall', 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', 'Edieresissmall', 'Igravesmall', 'Iacutesmall', 'Icircumflexsmall', 'Idieresissmall', 'Ethsmall', 'Ntildesmall', 'Ogravesmall', 'Oacutesmall', 'Ocircumflexsmall', 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall', 'Uacutesmall', 'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall', 'Thornsmall', 'Ydieresissmall'];
+exports.ExpertCharset = ExpertCharset;
+var ExpertSubsetCharset = ['.notdef', 'space', 'dollaroldstyle', 'dollarsuperior', 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', 'onedotenleader', 'comma', 'hyphen', 'period', 'fraction', 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'colon', 'semicolon', 'commasuperior', 'threequartersemdash', 'periodsuperior', 'asuperior', 'bsuperior', 'centsuperior', 'dsuperior', 'esuperior', 'isuperior', 'lsuperior', 'msuperior', 'nsuperior', 'osuperior', 'rsuperior', 'ssuperior', 'tsuperior', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'parenleftinferior', 'parenrightinferior', 'hyphensuperior', 'colonmonetary', 'onefitted', 'rupiah', 'centoldstyle', 'figuredash', 'hypheninferior', 'onequarter', 'onehalf', 'threequarters', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds', 'zerosuperior', 'onesuperior', 'twosuperior', 'threesuperior', 'foursuperior', 'fivesuperior', 'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior', 'zeroinferior', 'oneinferior', 'twoinferior', 'threeinferior', 'fourinferior', 'fiveinferior', 'sixinferior', 'seveninferior', 'eightinferior', 'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior', 'commainferior'];
+exports.ExpertSubsetCharset = ExpertSubsetCharset;
+
+/***/ }),
+/* 177 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.getEncoding = getEncoding;
+exports.ExpertEncoding = exports.ZapfDingbatsEncoding = exports.SymbolSetEncoding = exports.MacRomanEncoding = exports.StandardEncoding = exports.WinAnsiEncoding = void 0;
+var ExpertEncoding = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'space', 'exclamsmall', 'Hungarumlautsmall', '', 'dollaroldstyle', 'dollarsuperior', 'ampersandsmall', 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', 'onedotenleader', 'comma', 'hyphen', 'period', 'fraction', 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'colon', 'semicolon', 'commasuperior', 'threequartersemdash', 'periodsuperior', 'questionsmall', '', 'asuperior', 'bsuperior', 'centsuperior', 'dsuperior', 'esuperior', '', '', '', 'isuperior', '', '', 'lsuperior', 'msuperior', 'nsuperior', 'osuperior', '', '', 'rsuperior', 'ssuperior', 'tsuperior', '', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'parenleftinferior', '', 'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', 'Asmall', 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', 'Osmall', 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall', 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'exclamdownsmall', 'centoldstyle', 'Lslashsmall', '', '', 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall', 'Brevesmall', 'Caronsmall', '', 'Dotaccentsmall', '', '', 'Macronsmall', '', '', 'figuredash', 'hypheninferior', '', '', 'Ogoneksmall', 'Ringsmall', 'Cedillasmall', '', '', '', 'onequarter', 'onehalf', 'threequarters', 'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds', '', '', 'zerosuperior', 'onesuperior', 'twosuperior', 'threesuperior', 'foursuperior', 'fivesuperior', 'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior', 'zeroinferior', 'oneinferior', 'twoinferior', 'threeinferior', 'fourinferior', 'fiveinferior', 'sixinferior', 'seveninferior', 'eightinferior', 'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior', 'commainferior', 'Agravesmall', 'Aacutesmall', 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall', 'Aringsmall', 'AEsmall', 'Ccedillasmall', 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', 'Edieresissmall', 'Igravesmall', 'Iacutesmall', 'Icircumflexsmall', 'Idieresissmall', 'Ethsmall', 'Ntildesmall', 'Ogravesmall', 'Oacutesmall', 'Ocircumflexsmall', 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall', 'Uacutesmall', 'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall', 'Thornsmall', 'Ydieresissmall'];
+exports.ExpertEncoding = ExpertEncoding;
+var MacExpertEncoding = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'space', 'exclamsmall', 'Hungarumlautsmall', 'centoldstyle', 'dollaroldstyle', 'dollarsuperior', 'ampersandsmall', 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', 'onedotenleader', 'comma', 'hyphen', 'period', 'fraction', 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'colon', 'semicolon', '', 'threequartersemdash', '', 'questionsmall', '', '', '', '', 'Ethsmall', '', '', 'onequarter', 'onehalf', 'threequarters', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds', '', '', '', '', '', '', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'parenleftinferior', '', 'parenrightinferior', 'Circumflexsmall', 'hypheninferior', 'Gravesmall', 'Asmall', 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', 'Osmall', 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall', 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall', '', '', 'asuperior', 'centsuperior', '', '', '', '', 'Aacutesmall', 'Agravesmall', 'Acircumflexsmall', 'Adieresissmall', 'Atildesmall', 'Aringsmall', 'Ccedillasmall', 'Eacutesmall', 'Egravesmall', 'Ecircumflexsmall', 'Edieresissmall', 'Iacutesmall', 'Igravesmall', 'Icircumflexsmall', 'Idieresissmall', 'Ntildesmall', 'Oacutesmall', 'Ogravesmall', 'Ocircumflexsmall', 'Odieresissmall', 'Otildesmall', 'Uacutesmall', 'Ugravesmall', 'Ucircumflexsmall', 'Udieresissmall', '', 'eightsuperior', 'fourinferior', 'threeinferior', 'sixinferior', 'eightinferior', 'seveninferior', 'Scaronsmall', '', 'centinferior', 'twoinferior', '', 'Dieresissmall', '', 'Caronsmall', 'osuperior', 'fiveinferior', '', 'commainferior', 'periodinferior', 'Yacutesmall', '', 'dollarinferior', '', '', 'Thornsmall', '', 'nineinferior', 'zeroinferior', 'Zcaronsmall', 'AEsmall', 'Oslashsmall', 'questiondownsmall', 'oneinferior', 'Lslashsmall', '', '', '', '', '', '', 'Cedillasmall', '', '', '', '', '', 'OEsmall', 'figuredash', 'hyphensuperior', '', '', '', '', 'exclamdownsmall', '', 'Ydieresissmall', '', 'onesuperior', 'twosuperior', 'threesuperior', 'foursuperior', 'fivesuperior', 'sixsuperior', 'sevensuperior', 'ninesuperior', 'zerosuperior', '', 'esuperior', 'rsuperior', 'tsuperior', '', '', 'isuperior', 'ssuperior', 'dsuperior', '', '', '', '', '', 'lsuperior', 'Ogoneksmall', 'Brevesmall', 'Macronsmall', 'bsuperior', 'nsuperior', 'msuperior', 'commasuperior', 'periodsuperior', 'Dotaccentsmall', 'Ringsmall', '', '', '', ''];
+var MacRomanEncoding = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore', 'grave', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', '', 'Adieresis', 'Aring', 'Ccedilla', 'Eacute', 'Ntilde', 'Odieresis', 'Udieresis', 'aacute', 'agrave', 'acircumflex', 'adieresis', 'atilde', 'aring', 'ccedilla', 'eacute', 'egrave', 'ecircumflex', 'edieresis', 'iacute', 'igrave', 'icircumflex', 'idieresis', 'ntilde', 'oacute', 'ograve', 'ocircumflex', 'odieresis', 'otilde', 'uacute', 'ugrave', 'ucircumflex', 'udieresis', 'dagger', 'degree', 'cent', 'sterling', 'section', 'bullet', 'paragraph', 'germandbls', 'registered', 'copyright', 'trademark', 'acute', 'dieresis', 'notequal', 'AE', 'Oslash', 'infinity', 'plusminus', 'lessequal', 'greaterequal', 'yen', 'mu', 'partialdiff', 'summation', 'product', 'pi', 'integral', 'ordfeminine', 'ordmasculine', 'Omega', 'ae', 'oslash', 'questiondown', 'exclamdown', 'logicalnot', 'radical', 'florin', 'approxequal', 'Delta', 'guillemotleft', 'guillemotright', 'ellipsis', 'space', 'Agrave', 'Atilde', 'Otilde', 'OE', 'oe', 'endash', 'emdash', 'quotedblleft', 'quotedblright', 'quoteleft', 'quoteright', 'divide', 'lozenge', 'ydieresis', 'Ydieresis', 'fraction', 'currency', 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'daggerdbl', 'periodcentered', 'quotesinglbase', 'quotedblbase', 'perthousand', 'Acircumflex', 'Ecircumflex', 'Aacute', 'Edieresis', 'Egrave', 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Oacute', 'Ocircumflex', 'apple', 'Ograve', 'Uacute', 'Ucircumflex', 'Ugrave', 'dotlessi', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron'];
+exports.MacRomanEncoding = MacRomanEncoding;
+var StandardEncoding = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quoteright', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore', 'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'exclamdown', 'cent', 'sterling', 'fraction', 'yen', 'florin', 'section', 'currency', 'quotesingle', 'quotedblleft', 'guillemotleft', 'guilsinglleft', 'guilsinglright', 'fi', 'fl', '', 'endash', 'dagger', 'daggerdbl', 'periodcentered', '', 'paragraph', 'bullet', 'quotesinglbase', 'quotedblbase', 'quotedblright', 'guillemotright', 'ellipsis', 'perthousand', '', 'questiondown', '', 'grave', 'acute', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'dieresis', '', 'ring', 'cedilla', '', 'hungarumlaut', 'ogonek', 'caron', 'emdash', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'AE', '', 'ordfeminine', '', '', '', '', 'Lslash', 'Oslash', 'OE', 'ordmasculine', '', '', '', '', '', 'ae', '', '', '', 'dotlessi', '', '', 'lslash', 'oslash', 'oe', 'germandbls', '', '', '', ''];
+exports.StandardEncoding = StandardEncoding;
+var WinAnsiEncoding = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore', 'grave', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', 'bullet', 'Euro', 'bullet', 'quotesinglbase', 'florin', 'quotedblbase', 'ellipsis', 'dagger', 'daggerdbl', 'circumflex', 'perthousand', 'Scaron', 'guilsinglleft', 'OE', 'bullet', 'Zcaron', 'bullet', 'bullet', 'quoteleft', 'quoteright', 'quotedblleft', 'quotedblright', 'bullet', 'endash', 'emdash', 'tilde', 'trademark', 'scaron', 'guilsinglright', 'oe', 'bullet', 'zcaron', 'Ydieresis', 'space', 'exclamdown', 'cent', 'sterling', 'currency', 'yen', 'brokenbar', 'section', 'dieresis', 'copyright', 'ordfeminine', 'guillemotleft', 'logicalnot', 'hyphen', 'registered', 'macron', 'degree', 'plusminus', 'twosuperior', 'threesuperior', 'acute', 'mu', 'paragraph', 'periodcentered', 'cedilla', 'onesuperior', 'ordmasculine', 'guillemotright', 'onequarter', 'onehalf', 'threequarters', 'questiondown', 'Agrave', 'Aacute', 'Acircumflex', 'Atilde', 'Adieresis', 'Aring', 'AE', 'Ccedilla', 'Egrave', 'Eacute', 'Ecircumflex', 'Edieresis', 'Igrave', 'Iacute', 'Icircumflex', 'Idieresis', 'Eth', 'Ntilde', 'Ograve', 'Oacute', 'Ocircumflex', 'Otilde', 'Odieresis', 'multiply', 'Oslash', 'Ugrave', 'Uacute', 'Ucircumflex', 'Udieresis', 'Yacute', 'Thorn', 'germandbls', 'agrave', 'aacute', 'acircumflex', 'atilde', 'adieresis', 'aring', 'ae', 'ccedilla', 'egrave', 'eacute', 'ecircumflex', 'edieresis', 'igrave', 'iacute', 'icircumflex', 'idieresis', 'eth', 'ntilde', 'ograve', 'oacute', 'ocircumflex', 'otilde', 'odieresis', 'divide', 'oslash', 'ugrave', 'uacute', 'ucircumflex', 'udieresis', 'yacute', 'thorn', 'ydieresis'];
+exports.WinAnsiEncoding = WinAnsiEncoding;
+var SymbolSetEncoding = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'space', 'exclam', 'universal', 'numbersign', 'existential', 'percent', 'ampersand', 'suchthat', 'parenleft', 'parenright', 'asteriskmath', 'plus', 'comma', 'minus', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', 'question', 'congruent', 'Alpha', 'Beta', 'Chi', 'Delta', 'Epsilon', 'Phi', 'Gamma', 'Eta', 'Iota', 'theta1', 'Kappa', 'Lambda', 'Mu', 'Nu', 'Omicron', 'Pi', 'Theta', 'Rho', 'Sigma', 'Tau', 'Upsilon', 'sigma1', 'Omega', 'Xi', 'Psi', 'Zeta', 'bracketleft', 'therefore', 'bracketright', 'perpendicular', 'underscore', 'radicalex', 'alpha', 'beta', 'chi', 'delta', 'epsilon', 'phi', 'gamma', 'eta', 'iota', 'phi1', 'kappa', 'lambda', 'mu', 'nu', 'omicron', 'pi', 'theta', 'rho', 'sigma', 'tau', 'upsilon', 'omega1', 'omega', 'xi', 'psi', 'zeta', 'braceleft', 'bar', 'braceright', 'similar', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'Euro', 'Upsilon1', 'minute', 'lessequal', 'fraction', 'infinity', 'florin', 'club', 'diamond', 'heart', 'spade', 'arrowboth', 'arrowleft', 'arrowup', 'arrowright', 'arrowdown', 'degree', 'plusminus', 'second', 'greaterequal', 'multiply', 'proportional', 'partialdiff', 'bullet', 'divide', 'notequal', 'equivalence', 'approxequal', 'ellipsis', 'arrowvertex', 'arrowhorizex', 'carriagereturn', 'aleph', 'Ifraktur', 'Rfraktur', 'weierstrass', 'circlemultiply', 'circleplus', 'emptyset', 'intersection', 'union', 'propersuperset', 'reflexsuperset', 'notsubset', 'propersubset', 'reflexsubset', 'element', 'notelement', 'angle', 'gradient', 'registerserif', 'copyrightserif', 'trademarkserif', 'product', 'radical', 'dotmath', 'logicalnot', 'logicaland', 'logicalor', 'arrowdblboth', 'arrowdblleft', 'arrowdblup', 'arrowdblright', 'arrowdbldown', 'lozenge', 'angleleft', 'registersans', 'copyrightsans', 'trademarksans', 'summation', 'parenlefttp', 'parenleftex', 'parenleftbt', 'bracketlefttp', 'bracketleftex', 'bracketleftbt', 'bracelefttp', 'braceleftmid', 'braceleftbt', 'braceex', '', 'angleright', 'integral', 'integraltp', 'integralex', 'integralbt', 'parenrighttp', 'parenrightex', 'parenrightbt', 'bracketrighttp', 'bracketrightex', 'bracketrightbt', 'bracerighttp', 'bracerightmid', 'bracerightbt', ''];
+exports.SymbolSetEncoding = SymbolSetEncoding;
+var ZapfDingbatsEncoding = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'space', 'a1', 'a2', 'a202', 'a3', 'a4', 'a5', 'a119', 'a118', 'a117', 'a11', 'a12', 'a13', 'a14', 'a15', 'a16', 'a105', 'a17', 'a18', 'a19', 'a20', 'a21', 'a22', 'a23', 'a24', 'a25', 'a26', 'a27', 'a28', 'a6', 'a7', 'a8', 'a9', 'a10', 'a29', 'a30', 'a31', 'a32', 'a33', 'a34', 'a35', 'a36', 'a37', 'a38', 'a39', 'a40', 'a41', 'a42', 'a43', 'a44', 'a45', 'a46', 'a47', 'a48', 'a49', 'a50', 'a51', 'a52', 'a53', 'a54', 'a55', 'a56', 'a57', 'a58', 'a59', 'a60', 'a61', 'a62', 'a63', 'a64', 'a65', 'a66', 'a67', 'a68', 'a69', 'a70', 'a71', 'a72', 'a73', 'a74', 'a203', 'a75', 'a204', 'a76', 'a77', 'a78', 'a79', 'a81', 'a82', 'a83', 'a84', 'a97', 'a98', 'a99', 'a100', '', 'a89', 'a90', 'a93', 'a94', 'a91', 'a92', 'a205', 'a85', 'a206', 'a86', 'a87', 'a88', 'a95', 'a96', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'a101', 'a102', 'a103', 'a104', 'a106', 'a107', 'a108', 'a112', 'a111', 'a110', 'a109', 'a120', 'a121', 'a122', 'a123', 'a124', 'a125', 'a126', 'a127', 'a128', 'a129', 'a130', 'a131', 'a132', 'a133', 'a134', 'a135', 'a136', 'a137', 'a138', 'a139', 'a140', 'a141', 'a142', 'a143', 'a144', 'a145', 'a146', 'a147', 'a148', 'a149', 'a150', 'a151', 'a152', 'a153', 'a154', 'a155', 'a156', 'a157', 'a158', 'a159', 'a160', 'a161', 'a163', 'a164', 'a196', 'a165', 'a192', 'a166', 'a167', 'a168', 'a169', 'a170', 'a171', 'a172', 'a173', 'a162', 'a174', 'a175', 'a176', 'a177', 'a178', 'a179', 'a193', 'a180', 'a199', 'a181', 'a200', 'a182', '', 'a201', 'a183', 'a184', 'a197', 'a185', 'a194', 'a198', 'a186', 'a195', 'a187', 'a188', 'a189', 'a190', 'a191', ''];
+exports.ZapfDingbatsEncoding = ZapfDingbatsEncoding;
+
+function getEncoding(encodingName) {
+ switch (encodingName) {
+ case 'WinAnsiEncoding':
+ return WinAnsiEncoding;
+
+ case 'StandardEncoding':
+ return StandardEncoding;
+
+ case 'MacRomanEncoding':
+ return MacRomanEncoding;
+
+ case 'SymbolSetEncoding':
+ return SymbolSetEncoding;
+
+ case 'ZapfDingbatsEncoding':
+ return ZapfDingbatsEncoding;
+
+ case 'ExpertEncoding':
+ return ExpertEncoding;
+
+ case 'MacExpertEncoding':
+ return MacExpertEncoding;
+
+ default:
+ return null;
+ }
+}
+
+/***/ }),
+/* 178 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+var getLookupTableFactory = __w_pdfjs_require__(154).getLookupTableFactory;
+var getGlyphsUnicode = getLookupTableFactory(function (t) {
+ t['A'] = 0x0041;
+ t['AE'] = 0x00C6;
+ t['AEacute'] = 0x01FC;
+ t['AEmacron'] = 0x01E2;
+ t['AEsmall'] = 0xF7E6;
+ t['Aacute'] = 0x00C1;
+ t['Aacutesmall'] = 0xF7E1;
+ t['Abreve'] = 0x0102;
+ t['Abreveacute'] = 0x1EAE;
+ t['Abrevecyrillic'] = 0x04D0;
+ t['Abrevedotbelow'] = 0x1EB6;
+ t['Abrevegrave'] = 0x1EB0;
+ t['Abrevehookabove'] = 0x1EB2;
+ t['Abrevetilde'] = 0x1EB4;
+ t['Acaron'] = 0x01CD;
+ t['Acircle'] = 0x24B6;
+ t['Acircumflex'] = 0x00C2;
+ t['Acircumflexacute'] = 0x1EA4;
+ t['Acircumflexdotbelow'] = 0x1EAC;
+ t['Acircumflexgrave'] = 0x1EA6;
+ t['Acircumflexhookabove'] = 0x1EA8;
+ t['Acircumflexsmall'] = 0xF7E2;
+ t['Acircumflextilde'] = 0x1EAA;
+ t['Acute'] = 0xF6C9;
+ t['Acutesmall'] = 0xF7B4;
+ t['Acyrillic'] = 0x0410;
+ t['Adblgrave'] = 0x0200;
+ t['Adieresis'] = 0x00C4;
+ t['Adieresiscyrillic'] = 0x04D2;
+ t['Adieresismacron'] = 0x01DE;
+ t['Adieresissmall'] = 0xF7E4;
+ t['Adotbelow'] = 0x1EA0;
+ t['Adotmacron'] = 0x01E0;
+ t['Agrave'] = 0x00C0;
+ t['Agravesmall'] = 0xF7E0;
+ t['Ahookabove'] = 0x1EA2;
+ t['Aiecyrillic'] = 0x04D4;
+ t['Ainvertedbreve'] = 0x0202;
+ t['Alpha'] = 0x0391;
+ t['Alphatonos'] = 0x0386;
+ t['Amacron'] = 0x0100;
+ t['Amonospace'] = 0xFF21;
+ t['Aogonek'] = 0x0104;
+ t['Aring'] = 0x00C5;
+ t['Aringacute'] = 0x01FA;
+ t['Aringbelow'] = 0x1E00;
+ t['Aringsmall'] = 0xF7E5;
+ t['Asmall'] = 0xF761;
+ t['Atilde'] = 0x00C3;
+ t['Atildesmall'] = 0xF7E3;
+ t['Aybarmenian'] = 0x0531;
+ t['B'] = 0x0042;
+ t['Bcircle'] = 0x24B7;
+ t['Bdotaccent'] = 0x1E02;
+ t['Bdotbelow'] = 0x1E04;
+ t['Becyrillic'] = 0x0411;
+ t['Benarmenian'] = 0x0532;
+ t['Beta'] = 0x0392;
+ t['Bhook'] = 0x0181;
+ t['Blinebelow'] = 0x1E06;
+ t['Bmonospace'] = 0xFF22;
+ t['Brevesmall'] = 0xF6F4;
+ t['Bsmall'] = 0xF762;
+ t['Btopbar'] = 0x0182;
+ t['C'] = 0x0043;
+ t['Caarmenian'] = 0x053E;
+ t['Cacute'] = 0x0106;
+ t['Caron'] = 0xF6CA;
+ t['Caronsmall'] = 0xF6F5;
+ t['Ccaron'] = 0x010C;
+ t['Ccedilla'] = 0x00C7;
+ t['Ccedillaacute'] = 0x1E08;
+ t['Ccedillasmall'] = 0xF7E7;
+ t['Ccircle'] = 0x24B8;
+ t['Ccircumflex'] = 0x0108;
+ t['Cdot'] = 0x010A;
+ t['Cdotaccent'] = 0x010A;
+ t['Cedillasmall'] = 0xF7B8;
+ t['Chaarmenian'] = 0x0549;
+ t['Cheabkhasiancyrillic'] = 0x04BC;
+ t['Checyrillic'] = 0x0427;
+ t['Chedescenderabkhasiancyrillic'] = 0x04BE;
+ t['Chedescendercyrillic'] = 0x04B6;
+ t['Chedieresiscyrillic'] = 0x04F4;
+ t['Cheharmenian'] = 0x0543;
+ t['Chekhakassiancyrillic'] = 0x04CB;
+ t['Cheverticalstrokecyrillic'] = 0x04B8;
+ t['Chi'] = 0x03A7;
+ t['Chook'] = 0x0187;
+ t['Circumflexsmall'] = 0xF6F6;
+ t['Cmonospace'] = 0xFF23;
+ t['Coarmenian'] = 0x0551;
+ t['Csmall'] = 0xF763;
+ t['D'] = 0x0044;
+ t['DZ'] = 0x01F1;
+ t['DZcaron'] = 0x01C4;
+ t['Daarmenian'] = 0x0534;
+ t['Dafrican'] = 0x0189;
+ t['Dcaron'] = 0x010E;
+ t['Dcedilla'] = 0x1E10;
+ t['Dcircle'] = 0x24B9;
+ t['Dcircumflexbelow'] = 0x1E12;
+ t['Dcroat'] = 0x0110;
+ t['Ddotaccent'] = 0x1E0A;
+ t['Ddotbelow'] = 0x1E0C;
+ t['Decyrillic'] = 0x0414;
+ t['Deicoptic'] = 0x03EE;
+ t['Delta'] = 0x2206;
+ t['Deltagreek'] = 0x0394;
+ t['Dhook'] = 0x018A;
+ t['Dieresis'] = 0xF6CB;
+ t['DieresisAcute'] = 0xF6CC;
+ t['DieresisGrave'] = 0xF6CD;
+ t['Dieresissmall'] = 0xF7A8;
+ t['Digammagreek'] = 0x03DC;
+ t['Djecyrillic'] = 0x0402;
+ t['Dlinebelow'] = 0x1E0E;
+ t['Dmonospace'] = 0xFF24;
+ t['Dotaccentsmall'] = 0xF6F7;
+ t['Dslash'] = 0x0110;
+ t['Dsmall'] = 0xF764;
+ t['Dtopbar'] = 0x018B;
+ t['Dz'] = 0x01F2;
+ t['Dzcaron'] = 0x01C5;
+ t['Dzeabkhasiancyrillic'] = 0x04E0;
+ t['Dzecyrillic'] = 0x0405;
+ t['Dzhecyrillic'] = 0x040F;
+ t['E'] = 0x0045;
+ t['Eacute'] = 0x00C9;
+ t['Eacutesmall'] = 0xF7E9;
+ t['Ebreve'] = 0x0114;
+ t['Ecaron'] = 0x011A;
+ t['Ecedillabreve'] = 0x1E1C;
+ t['Echarmenian'] = 0x0535;
+ t['Ecircle'] = 0x24BA;
+ t['Ecircumflex'] = 0x00CA;
+ t['Ecircumflexacute'] = 0x1EBE;
+ t['Ecircumflexbelow'] = 0x1E18;
+ t['Ecircumflexdotbelow'] = 0x1EC6;
+ t['Ecircumflexgrave'] = 0x1EC0;
+ t['Ecircumflexhookabove'] = 0x1EC2;
+ t['Ecircumflexsmall'] = 0xF7EA;
+ t['Ecircumflextilde'] = 0x1EC4;
+ t['Ecyrillic'] = 0x0404;
+ t['Edblgrave'] = 0x0204;
+ t['Edieresis'] = 0x00CB;
+ t['Edieresissmall'] = 0xF7EB;
+ t['Edot'] = 0x0116;
+ t['Edotaccent'] = 0x0116;
+ t['Edotbelow'] = 0x1EB8;
+ t['Efcyrillic'] = 0x0424;
+ t['Egrave'] = 0x00C8;
+ t['Egravesmall'] = 0xF7E8;
+ t['Eharmenian'] = 0x0537;
+ t['Ehookabove'] = 0x1EBA;
+ t['Eightroman'] = 0x2167;
+ t['Einvertedbreve'] = 0x0206;
+ t['Eiotifiedcyrillic'] = 0x0464;
+ t['Elcyrillic'] = 0x041B;
+ t['Elevenroman'] = 0x216A;
+ t['Emacron'] = 0x0112;
+ t['Emacronacute'] = 0x1E16;
+ t['Emacrongrave'] = 0x1E14;
+ t['Emcyrillic'] = 0x041C;
+ t['Emonospace'] = 0xFF25;
+ t['Encyrillic'] = 0x041D;
+ t['Endescendercyrillic'] = 0x04A2;
+ t['Eng'] = 0x014A;
+ t['Enghecyrillic'] = 0x04A4;
+ t['Enhookcyrillic'] = 0x04C7;
+ t['Eogonek'] = 0x0118;
+ t['Eopen'] = 0x0190;
+ t['Epsilon'] = 0x0395;
+ t['Epsilontonos'] = 0x0388;
+ t['Ercyrillic'] = 0x0420;
+ t['Ereversed'] = 0x018E;
+ t['Ereversedcyrillic'] = 0x042D;
+ t['Escyrillic'] = 0x0421;
+ t['Esdescendercyrillic'] = 0x04AA;
+ t['Esh'] = 0x01A9;
+ t['Esmall'] = 0xF765;
+ t['Eta'] = 0x0397;
+ t['Etarmenian'] = 0x0538;
+ t['Etatonos'] = 0x0389;
+ t['Eth'] = 0x00D0;
+ t['Ethsmall'] = 0xF7F0;
+ t['Etilde'] = 0x1EBC;
+ t['Etildebelow'] = 0x1E1A;
+ t['Euro'] = 0x20AC;
+ t['Ezh'] = 0x01B7;
+ t['Ezhcaron'] = 0x01EE;
+ t['Ezhreversed'] = 0x01B8;
+ t['F'] = 0x0046;
+ t['Fcircle'] = 0x24BB;
+ t['Fdotaccent'] = 0x1E1E;
+ t['Feharmenian'] = 0x0556;
+ t['Feicoptic'] = 0x03E4;
+ t['Fhook'] = 0x0191;
+ t['Fitacyrillic'] = 0x0472;
+ t['Fiveroman'] = 0x2164;
+ t['Fmonospace'] = 0xFF26;
+ t['Fourroman'] = 0x2163;
+ t['Fsmall'] = 0xF766;
+ t['G'] = 0x0047;
+ t['GBsquare'] = 0x3387;
+ t['Gacute'] = 0x01F4;
+ t['Gamma'] = 0x0393;
+ t['Gammaafrican'] = 0x0194;
+ t['Gangiacoptic'] = 0x03EA;
+ t['Gbreve'] = 0x011E;
+ t['Gcaron'] = 0x01E6;
+ t['Gcedilla'] = 0x0122;
+ t['Gcircle'] = 0x24BC;
+ t['Gcircumflex'] = 0x011C;
+ t['Gcommaaccent'] = 0x0122;
+ t['Gdot'] = 0x0120;
+ t['Gdotaccent'] = 0x0120;
+ t['Gecyrillic'] = 0x0413;
+ t['Ghadarmenian'] = 0x0542;
+ t['Ghemiddlehookcyrillic'] = 0x0494;
+ t['Ghestrokecyrillic'] = 0x0492;
+ t['Gheupturncyrillic'] = 0x0490;
+ t['Ghook'] = 0x0193;
+ t['Gimarmenian'] = 0x0533;
+ t['Gjecyrillic'] = 0x0403;
+ t['Gmacron'] = 0x1E20;
+ t['Gmonospace'] = 0xFF27;
+ t['Grave'] = 0xF6CE;
+ t['Gravesmall'] = 0xF760;
+ t['Gsmall'] = 0xF767;
+ t['Gsmallhook'] = 0x029B;
+ t['Gstroke'] = 0x01E4;
+ t['H'] = 0x0048;
+ t['H18533'] = 0x25CF;
+ t['H18543'] = 0x25AA;
+ t['H18551'] = 0x25AB;
+ t['H22073'] = 0x25A1;
+ t['HPsquare'] = 0x33CB;
+ t['Haabkhasiancyrillic'] = 0x04A8;
+ t['Hadescendercyrillic'] = 0x04B2;
+ t['Hardsigncyrillic'] = 0x042A;
+ t['Hbar'] = 0x0126;
+ t['Hbrevebelow'] = 0x1E2A;
+ t['Hcedilla'] = 0x1E28;
+ t['Hcircle'] = 0x24BD;
+ t['Hcircumflex'] = 0x0124;
+ t['Hdieresis'] = 0x1E26;
+ t['Hdotaccent'] = 0x1E22;
+ t['Hdotbelow'] = 0x1E24;
+ t['Hmonospace'] = 0xFF28;
+ t['Hoarmenian'] = 0x0540;
+ t['Horicoptic'] = 0x03E8;
+ t['Hsmall'] = 0xF768;
+ t['Hungarumlaut'] = 0xF6CF;
+ t['Hungarumlautsmall'] = 0xF6F8;
+ t['Hzsquare'] = 0x3390;
+ t['I'] = 0x0049;
+ t['IAcyrillic'] = 0x042F;
+ t['IJ'] = 0x0132;
+ t['IUcyrillic'] = 0x042E;
+ t['Iacute'] = 0x00CD;
+ t['Iacutesmall'] = 0xF7ED;
+ t['Ibreve'] = 0x012C;
+ t['Icaron'] = 0x01CF;
+ t['Icircle'] = 0x24BE;
+ t['Icircumflex'] = 0x00CE;
+ t['Icircumflexsmall'] = 0xF7EE;
+ t['Icyrillic'] = 0x0406;
+ t['Idblgrave'] = 0x0208;
+ t['Idieresis'] = 0x00CF;
+ t['Idieresisacute'] = 0x1E2E;
+ t['Idieresiscyrillic'] = 0x04E4;
+ t['Idieresissmall'] = 0xF7EF;
+ t['Idot'] = 0x0130;
+ t['Idotaccent'] = 0x0130;
+ t['Idotbelow'] = 0x1ECA;
+ t['Iebrevecyrillic'] = 0x04D6;
+ t['Iecyrillic'] = 0x0415;
+ t['Ifraktur'] = 0x2111;
+ t['Igrave'] = 0x00CC;
+ t['Igravesmall'] = 0xF7EC;
+ t['Ihookabove'] = 0x1EC8;
+ t['Iicyrillic'] = 0x0418;
+ t['Iinvertedbreve'] = 0x020A;
+ t['Iishortcyrillic'] = 0x0419;
+ t['Imacron'] = 0x012A;
+ t['Imacroncyrillic'] = 0x04E2;
+ t['Imonospace'] = 0xFF29;
+ t['Iniarmenian'] = 0x053B;
+ t['Iocyrillic'] = 0x0401;
+ t['Iogonek'] = 0x012E;
+ t['Iota'] = 0x0399;
+ t['Iotaafrican'] = 0x0196;
+ t['Iotadieresis'] = 0x03AA;
+ t['Iotatonos'] = 0x038A;
+ t['Ismall'] = 0xF769;
+ t['Istroke'] = 0x0197;
+ t['Itilde'] = 0x0128;
+ t['Itildebelow'] = 0x1E2C;
+ t['Izhitsacyrillic'] = 0x0474;
+ t['Izhitsadblgravecyrillic'] = 0x0476;
+ t['J'] = 0x004A;
+ t['Jaarmenian'] = 0x0541;
+ t['Jcircle'] = 0x24BF;
+ t['Jcircumflex'] = 0x0134;
+ t['Jecyrillic'] = 0x0408;
+ t['Jheharmenian'] = 0x054B;
+ t['Jmonospace'] = 0xFF2A;
+ t['Jsmall'] = 0xF76A;
+ t['K'] = 0x004B;
+ t['KBsquare'] = 0x3385;
+ t['KKsquare'] = 0x33CD;
+ t['Kabashkircyrillic'] = 0x04A0;
+ t['Kacute'] = 0x1E30;
+ t['Kacyrillic'] = 0x041A;
+ t['Kadescendercyrillic'] = 0x049A;
+ t['Kahookcyrillic'] = 0x04C3;
+ t['Kappa'] = 0x039A;
+ t['Kastrokecyrillic'] = 0x049E;
+ t['Kaverticalstrokecyrillic'] = 0x049C;
+ t['Kcaron'] = 0x01E8;
+ t['Kcedilla'] = 0x0136;
+ t['Kcircle'] = 0x24C0;
+ t['Kcommaaccent'] = 0x0136;
+ t['Kdotbelow'] = 0x1E32;
+ t['Keharmenian'] = 0x0554;
+ t['Kenarmenian'] = 0x053F;
+ t['Khacyrillic'] = 0x0425;
+ t['Kheicoptic'] = 0x03E6;
+ t['Khook'] = 0x0198;
+ t['Kjecyrillic'] = 0x040C;
+ t['Klinebelow'] = 0x1E34;
+ t['Kmonospace'] = 0xFF2B;
+ t['Koppacyrillic'] = 0x0480;
+ t['Koppagreek'] = 0x03DE;
+ t['Ksicyrillic'] = 0x046E;
+ t['Ksmall'] = 0xF76B;
+ t['L'] = 0x004C;
+ t['LJ'] = 0x01C7;
+ t['LL'] = 0xF6BF;
+ t['Lacute'] = 0x0139;
+ t['Lambda'] = 0x039B;
+ t['Lcaron'] = 0x013D;
+ t['Lcedilla'] = 0x013B;
+ t['Lcircle'] = 0x24C1;
+ t['Lcircumflexbelow'] = 0x1E3C;
+ t['Lcommaaccent'] = 0x013B;
+ t['Ldot'] = 0x013F;
+ t['Ldotaccent'] = 0x013F;
+ t['Ldotbelow'] = 0x1E36;
+ t['Ldotbelowmacron'] = 0x1E38;
+ t['Liwnarmenian'] = 0x053C;
+ t['Lj'] = 0x01C8;
+ t['Ljecyrillic'] = 0x0409;
+ t['Llinebelow'] = 0x1E3A;
+ t['Lmonospace'] = 0xFF2C;
+ t['Lslash'] = 0x0141;
+ t['Lslashsmall'] = 0xF6F9;
+ t['Lsmall'] = 0xF76C;
+ t['M'] = 0x004D;
+ t['MBsquare'] = 0x3386;
+ t['Macron'] = 0xF6D0;
+ t['Macronsmall'] = 0xF7AF;
+ t['Macute'] = 0x1E3E;
+ t['Mcircle'] = 0x24C2;
+ t['Mdotaccent'] = 0x1E40;
+ t['Mdotbelow'] = 0x1E42;
+ t['Menarmenian'] = 0x0544;
+ t['Mmonospace'] = 0xFF2D;
+ t['Msmall'] = 0xF76D;
+ t['Mturned'] = 0x019C;
+ t['Mu'] = 0x039C;
+ t['N'] = 0x004E;
+ t['NJ'] = 0x01CA;
+ t['Nacute'] = 0x0143;
+ t['Ncaron'] = 0x0147;
+ t['Ncedilla'] = 0x0145;
+ t['Ncircle'] = 0x24C3;
+ t['Ncircumflexbelow'] = 0x1E4A;
+ t['Ncommaaccent'] = 0x0145;
+ t['Ndotaccent'] = 0x1E44;
+ t['Ndotbelow'] = 0x1E46;
+ t['Nhookleft'] = 0x019D;
+ t['Nineroman'] = 0x2168;
+ t['Nj'] = 0x01CB;
+ t['Njecyrillic'] = 0x040A;
+ t['Nlinebelow'] = 0x1E48;
+ t['Nmonospace'] = 0xFF2E;
+ t['Nowarmenian'] = 0x0546;
+ t['Nsmall'] = 0xF76E;
+ t['Ntilde'] = 0x00D1;
+ t['Ntildesmall'] = 0xF7F1;
+ t['Nu'] = 0x039D;
+ t['O'] = 0x004F;
+ t['OE'] = 0x0152;
+ t['OEsmall'] = 0xF6FA;
+ t['Oacute'] = 0x00D3;
+ t['Oacutesmall'] = 0xF7F3;
+ t['Obarredcyrillic'] = 0x04E8;
+ t['Obarreddieresiscyrillic'] = 0x04EA;
+ t['Obreve'] = 0x014E;
+ t['Ocaron'] = 0x01D1;
+ t['Ocenteredtilde'] = 0x019F;
+ t['Ocircle'] = 0x24C4;
+ t['Ocircumflex'] = 0x00D4;
+ t['Ocircumflexacute'] = 0x1ED0;
+ t['Ocircumflexdotbelow'] = 0x1ED8;
+ t['Ocircumflexgrave'] = 0x1ED2;
+ t['Ocircumflexhookabove'] = 0x1ED4;
+ t['Ocircumflexsmall'] = 0xF7F4;
+ t['Ocircumflextilde'] = 0x1ED6;
+ t['Ocyrillic'] = 0x041E;
+ t['Odblacute'] = 0x0150;
+ t['Odblgrave'] = 0x020C;
+ t['Odieresis'] = 0x00D6;
+ t['Odieresiscyrillic'] = 0x04E6;
+ t['Odieresissmall'] = 0xF7F6;
+ t['Odotbelow'] = 0x1ECC;
+ t['Ogoneksmall'] = 0xF6FB;
+ t['Ograve'] = 0x00D2;
+ t['Ogravesmall'] = 0xF7F2;
+ t['Oharmenian'] = 0x0555;
+ t['Ohm'] = 0x2126;
+ t['Ohookabove'] = 0x1ECE;
+ t['Ohorn'] = 0x01A0;
+ t['Ohornacute'] = 0x1EDA;
+ t['Ohorndotbelow'] = 0x1EE2;
+ t['Ohorngrave'] = 0x1EDC;
+ t['Ohornhookabove'] = 0x1EDE;
+ t['Ohorntilde'] = 0x1EE0;
+ t['Ohungarumlaut'] = 0x0150;
+ t['Oi'] = 0x01A2;
+ t['Oinvertedbreve'] = 0x020E;
+ t['Omacron'] = 0x014C;
+ t['Omacronacute'] = 0x1E52;
+ t['Omacrongrave'] = 0x1E50;
+ t['Omega'] = 0x2126;
+ t['Omegacyrillic'] = 0x0460;
+ t['Omegagreek'] = 0x03A9;
+ t['Omegaroundcyrillic'] = 0x047A;
+ t['Omegatitlocyrillic'] = 0x047C;
+ t['Omegatonos'] = 0x038F;
+ t['Omicron'] = 0x039F;
+ t['Omicrontonos'] = 0x038C;
+ t['Omonospace'] = 0xFF2F;
+ t['Oneroman'] = 0x2160;
+ t['Oogonek'] = 0x01EA;
+ t['Oogonekmacron'] = 0x01EC;
+ t['Oopen'] = 0x0186;
+ t['Oslash'] = 0x00D8;
+ t['Oslashacute'] = 0x01FE;
+ t['Oslashsmall'] = 0xF7F8;
+ t['Osmall'] = 0xF76F;
+ t['Ostrokeacute'] = 0x01FE;
+ t['Otcyrillic'] = 0x047E;
+ t['Otilde'] = 0x00D5;
+ t['Otildeacute'] = 0x1E4C;
+ t['Otildedieresis'] = 0x1E4E;
+ t['Otildesmall'] = 0xF7F5;
+ t['P'] = 0x0050;
+ t['Pacute'] = 0x1E54;
+ t['Pcircle'] = 0x24C5;
+ t['Pdotaccent'] = 0x1E56;
+ t['Pecyrillic'] = 0x041F;
+ t['Peharmenian'] = 0x054A;
+ t['Pemiddlehookcyrillic'] = 0x04A6;
+ t['Phi'] = 0x03A6;
+ t['Phook'] = 0x01A4;
+ t['Pi'] = 0x03A0;
+ t['Piwrarmenian'] = 0x0553;
+ t['Pmonospace'] = 0xFF30;
+ t['Psi'] = 0x03A8;
+ t['Psicyrillic'] = 0x0470;
+ t['Psmall'] = 0xF770;
+ t['Q'] = 0x0051;
+ t['Qcircle'] = 0x24C6;
+ t['Qmonospace'] = 0xFF31;
+ t['Qsmall'] = 0xF771;
+ t['R'] = 0x0052;
+ t['Raarmenian'] = 0x054C;
+ t['Racute'] = 0x0154;
+ t['Rcaron'] = 0x0158;
+ t['Rcedilla'] = 0x0156;
+ t['Rcircle'] = 0x24C7;
+ t['Rcommaaccent'] = 0x0156;
+ t['Rdblgrave'] = 0x0210;
+ t['Rdotaccent'] = 0x1E58;
+ t['Rdotbelow'] = 0x1E5A;
+ t['Rdotbelowmacron'] = 0x1E5C;
+ t['Reharmenian'] = 0x0550;
+ t['Rfraktur'] = 0x211C;
+ t['Rho'] = 0x03A1;
+ t['Ringsmall'] = 0xF6FC;
+ t['Rinvertedbreve'] = 0x0212;
+ t['Rlinebelow'] = 0x1E5E;
+ t['Rmonospace'] = 0xFF32;
+ t['Rsmall'] = 0xF772;
+ t['Rsmallinverted'] = 0x0281;
+ t['Rsmallinvertedsuperior'] = 0x02B6;
+ t['S'] = 0x0053;
+ t['SF010000'] = 0x250C;
+ t['SF020000'] = 0x2514;
+ t['SF030000'] = 0x2510;
+ t['SF040000'] = 0x2518;
+ t['SF050000'] = 0x253C;
+ t['SF060000'] = 0x252C;
+ t['SF070000'] = 0x2534;
+ t['SF080000'] = 0x251C;
+ t['SF090000'] = 0x2524;
+ t['SF100000'] = 0x2500;
+ t['SF110000'] = 0x2502;
+ t['SF190000'] = 0x2561;
+ t['SF200000'] = 0x2562;
+ t['SF210000'] = 0x2556;
+ t['SF220000'] = 0x2555;
+ t['SF230000'] = 0x2563;
+ t['SF240000'] = 0x2551;
+ t['SF250000'] = 0x2557;
+ t['SF260000'] = 0x255D;
+ t['SF270000'] = 0x255C;
+ t['SF280000'] = 0x255B;
+ t['SF360000'] = 0x255E;
+ t['SF370000'] = 0x255F;
+ t['SF380000'] = 0x255A;
+ t['SF390000'] = 0x2554;
+ t['SF400000'] = 0x2569;
+ t['SF410000'] = 0x2566;
+ t['SF420000'] = 0x2560;
+ t['SF430000'] = 0x2550;
+ t['SF440000'] = 0x256C;
+ t['SF450000'] = 0x2567;
+ t['SF460000'] = 0x2568;
+ t['SF470000'] = 0x2564;
+ t['SF480000'] = 0x2565;
+ t['SF490000'] = 0x2559;
+ t['SF500000'] = 0x2558;
+ t['SF510000'] = 0x2552;
+ t['SF520000'] = 0x2553;
+ t['SF530000'] = 0x256B;
+ t['SF540000'] = 0x256A;
+ t['Sacute'] = 0x015A;
+ t['Sacutedotaccent'] = 0x1E64;
+ t['Sampigreek'] = 0x03E0;
+ t['Scaron'] = 0x0160;
+ t['Scarondotaccent'] = 0x1E66;
+ t['Scaronsmall'] = 0xF6FD;
+ t['Scedilla'] = 0x015E;
+ t['Schwa'] = 0x018F;
+ t['Schwacyrillic'] = 0x04D8;
+ t['Schwadieresiscyrillic'] = 0x04DA;
+ t['Scircle'] = 0x24C8;
+ t['Scircumflex'] = 0x015C;
+ t['Scommaaccent'] = 0x0218;
+ t['Sdotaccent'] = 0x1E60;
+ t['Sdotbelow'] = 0x1E62;
+ t['Sdotbelowdotaccent'] = 0x1E68;
+ t['Seharmenian'] = 0x054D;
+ t['Sevenroman'] = 0x2166;
+ t['Shaarmenian'] = 0x0547;
+ t['Shacyrillic'] = 0x0428;
+ t['Shchacyrillic'] = 0x0429;
+ t['Sheicoptic'] = 0x03E2;
+ t['Shhacyrillic'] = 0x04BA;
+ t['Shimacoptic'] = 0x03EC;
+ t['Sigma'] = 0x03A3;
+ t['Sixroman'] = 0x2165;
+ t['Smonospace'] = 0xFF33;
+ t['Softsigncyrillic'] = 0x042C;
+ t['Ssmall'] = 0xF773;
+ t['Stigmagreek'] = 0x03DA;
+ t['T'] = 0x0054;
+ t['Tau'] = 0x03A4;
+ t['Tbar'] = 0x0166;
+ t['Tcaron'] = 0x0164;
+ t['Tcedilla'] = 0x0162;
+ t['Tcircle'] = 0x24C9;
+ t['Tcircumflexbelow'] = 0x1E70;
+ t['Tcommaaccent'] = 0x0162;
+ t['Tdotaccent'] = 0x1E6A;
+ t['Tdotbelow'] = 0x1E6C;
+ t['Tecyrillic'] = 0x0422;
+ t['Tedescendercyrillic'] = 0x04AC;
+ t['Tenroman'] = 0x2169;
+ t['Tetsecyrillic'] = 0x04B4;
+ t['Theta'] = 0x0398;
+ t['Thook'] = 0x01AC;
+ t['Thorn'] = 0x00DE;
+ t['Thornsmall'] = 0xF7FE;
+ t['Threeroman'] = 0x2162;
+ t['Tildesmall'] = 0xF6FE;
+ t['Tiwnarmenian'] = 0x054F;
+ t['Tlinebelow'] = 0x1E6E;
+ t['Tmonospace'] = 0xFF34;
+ t['Toarmenian'] = 0x0539;
+ t['Tonefive'] = 0x01BC;
+ t['Tonesix'] = 0x0184;
+ t['Tonetwo'] = 0x01A7;
+ t['Tretroflexhook'] = 0x01AE;
+ t['Tsecyrillic'] = 0x0426;
+ t['Tshecyrillic'] = 0x040B;
+ t['Tsmall'] = 0xF774;
+ t['Twelveroman'] = 0x216B;
+ t['Tworoman'] = 0x2161;
+ t['U'] = 0x0055;
+ t['Uacute'] = 0x00DA;
+ t['Uacutesmall'] = 0xF7FA;
+ t['Ubreve'] = 0x016C;
+ t['Ucaron'] = 0x01D3;
+ t['Ucircle'] = 0x24CA;
+ t['Ucircumflex'] = 0x00DB;
+ t['Ucircumflexbelow'] = 0x1E76;
+ t['Ucircumflexsmall'] = 0xF7FB;
+ t['Ucyrillic'] = 0x0423;
+ t['Udblacute'] = 0x0170;
+ t['Udblgrave'] = 0x0214;
+ t['Udieresis'] = 0x00DC;
+ t['Udieresisacute'] = 0x01D7;
+ t['Udieresisbelow'] = 0x1E72;
+ t['Udieresiscaron'] = 0x01D9;
+ t['Udieresiscyrillic'] = 0x04F0;
+ t['Udieresisgrave'] = 0x01DB;
+ t['Udieresismacron'] = 0x01D5;
+ t['Udieresissmall'] = 0xF7FC;
+ t['Udotbelow'] = 0x1EE4;
+ t['Ugrave'] = 0x00D9;
+ t['Ugravesmall'] = 0xF7F9;
+ t['Uhookabove'] = 0x1EE6;
+ t['Uhorn'] = 0x01AF;
+ t['Uhornacute'] = 0x1EE8;
+ t['Uhorndotbelow'] = 0x1EF0;
+ t['Uhorngrave'] = 0x1EEA;
+ t['Uhornhookabove'] = 0x1EEC;
+ t['Uhorntilde'] = 0x1EEE;
+ t['Uhungarumlaut'] = 0x0170;
+ t['Uhungarumlautcyrillic'] = 0x04F2;
+ t['Uinvertedbreve'] = 0x0216;
+ t['Ukcyrillic'] = 0x0478;
+ t['Umacron'] = 0x016A;
+ t['Umacroncyrillic'] = 0x04EE;
+ t['Umacrondieresis'] = 0x1E7A;
+ t['Umonospace'] = 0xFF35;
+ t['Uogonek'] = 0x0172;
+ t['Upsilon'] = 0x03A5;
+ t['Upsilon1'] = 0x03D2;
+ t['Upsilonacutehooksymbolgreek'] = 0x03D3;
+ t['Upsilonafrican'] = 0x01B1;
+ t['Upsilondieresis'] = 0x03AB;
+ t['Upsilondieresishooksymbolgreek'] = 0x03D4;
+ t['Upsilonhooksymbol'] = 0x03D2;
+ t['Upsilontonos'] = 0x038E;
+ t['Uring'] = 0x016E;
+ t['Ushortcyrillic'] = 0x040E;
+ t['Usmall'] = 0xF775;
+ t['Ustraightcyrillic'] = 0x04AE;
+ t['Ustraightstrokecyrillic'] = 0x04B0;
+ t['Utilde'] = 0x0168;
+ t['Utildeacute'] = 0x1E78;
+ t['Utildebelow'] = 0x1E74;
+ t['V'] = 0x0056;
+ t['Vcircle'] = 0x24CB;
+ t['Vdotbelow'] = 0x1E7E;
+ t['Vecyrillic'] = 0x0412;
+ t['Vewarmenian'] = 0x054E;
+ t['Vhook'] = 0x01B2;
+ t['Vmonospace'] = 0xFF36;
+ t['Voarmenian'] = 0x0548;
+ t['Vsmall'] = 0xF776;
+ t['Vtilde'] = 0x1E7C;
+ t['W'] = 0x0057;
+ t['Wacute'] = 0x1E82;
+ t['Wcircle'] = 0x24CC;
+ t['Wcircumflex'] = 0x0174;
+ t['Wdieresis'] = 0x1E84;
+ t['Wdotaccent'] = 0x1E86;
+ t['Wdotbelow'] = 0x1E88;
+ t['Wgrave'] = 0x1E80;
+ t['Wmonospace'] = 0xFF37;
+ t['Wsmall'] = 0xF777;
+ t['X'] = 0x0058;
+ t['Xcircle'] = 0x24CD;
+ t['Xdieresis'] = 0x1E8C;
+ t['Xdotaccent'] = 0x1E8A;
+ t['Xeharmenian'] = 0x053D;
+ t['Xi'] = 0x039E;
+ t['Xmonospace'] = 0xFF38;
+ t['Xsmall'] = 0xF778;
+ t['Y'] = 0x0059;
+ t['Yacute'] = 0x00DD;
+ t['Yacutesmall'] = 0xF7FD;
+ t['Yatcyrillic'] = 0x0462;
+ t['Ycircle'] = 0x24CE;
+ t['Ycircumflex'] = 0x0176;
+ t['Ydieresis'] = 0x0178;
+ t['Ydieresissmall'] = 0xF7FF;
+ t['Ydotaccent'] = 0x1E8E;
+ t['Ydotbelow'] = 0x1EF4;
+ t['Yericyrillic'] = 0x042B;
+ t['Yerudieresiscyrillic'] = 0x04F8;
+ t['Ygrave'] = 0x1EF2;
+ t['Yhook'] = 0x01B3;
+ t['Yhookabove'] = 0x1EF6;
+ t['Yiarmenian'] = 0x0545;
+ t['Yicyrillic'] = 0x0407;
+ t['Yiwnarmenian'] = 0x0552;
+ t['Ymonospace'] = 0xFF39;
+ t['Ysmall'] = 0xF779;
+ t['Ytilde'] = 0x1EF8;
+ t['Yusbigcyrillic'] = 0x046A;
+ t['Yusbigiotifiedcyrillic'] = 0x046C;
+ t['Yuslittlecyrillic'] = 0x0466;
+ t['Yuslittleiotifiedcyrillic'] = 0x0468;
+ t['Z'] = 0x005A;
+ t['Zaarmenian'] = 0x0536;
+ t['Zacute'] = 0x0179;
+ t['Zcaron'] = 0x017D;
+ t['Zcaronsmall'] = 0xF6FF;
+ t['Zcircle'] = 0x24CF;
+ t['Zcircumflex'] = 0x1E90;
+ t['Zdot'] = 0x017B;
+ t['Zdotaccent'] = 0x017B;
+ t['Zdotbelow'] = 0x1E92;
+ t['Zecyrillic'] = 0x0417;
+ t['Zedescendercyrillic'] = 0x0498;
+ t['Zedieresiscyrillic'] = 0x04DE;
+ t['Zeta'] = 0x0396;
+ t['Zhearmenian'] = 0x053A;
+ t['Zhebrevecyrillic'] = 0x04C1;
+ t['Zhecyrillic'] = 0x0416;
+ t['Zhedescendercyrillic'] = 0x0496;
+ t['Zhedieresiscyrillic'] = 0x04DC;
+ t['Zlinebelow'] = 0x1E94;
+ t['Zmonospace'] = 0xFF3A;
+ t['Zsmall'] = 0xF77A;
+ t['Zstroke'] = 0x01B5;
+ t['a'] = 0x0061;
+ t['aabengali'] = 0x0986;
+ t['aacute'] = 0x00E1;
+ t['aadeva'] = 0x0906;
+ t['aagujarati'] = 0x0A86;
+ t['aagurmukhi'] = 0x0A06;
+ t['aamatragurmukhi'] = 0x0A3E;
+ t['aarusquare'] = 0x3303;
+ t['aavowelsignbengali'] = 0x09BE;
+ t['aavowelsigndeva'] = 0x093E;
+ t['aavowelsigngujarati'] = 0x0ABE;
+ t['abbreviationmarkarmenian'] = 0x055F;
+ t['abbreviationsigndeva'] = 0x0970;
+ t['abengali'] = 0x0985;
+ t['abopomofo'] = 0x311A;
+ t['abreve'] = 0x0103;
+ t['abreveacute'] = 0x1EAF;
+ t['abrevecyrillic'] = 0x04D1;
+ t['abrevedotbelow'] = 0x1EB7;
+ t['abrevegrave'] = 0x1EB1;
+ t['abrevehookabove'] = 0x1EB3;
+ t['abrevetilde'] = 0x1EB5;
+ t['acaron'] = 0x01CE;
+ t['acircle'] = 0x24D0;
+ t['acircumflex'] = 0x00E2;
+ t['acircumflexacute'] = 0x1EA5;
+ t['acircumflexdotbelow'] = 0x1EAD;
+ t['acircumflexgrave'] = 0x1EA7;
+ t['acircumflexhookabove'] = 0x1EA9;
+ t['acircumflextilde'] = 0x1EAB;
+ t['acute'] = 0x00B4;
+ t['acutebelowcmb'] = 0x0317;
+ t['acutecmb'] = 0x0301;
+ t['acutecomb'] = 0x0301;
+ t['acutedeva'] = 0x0954;
+ t['acutelowmod'] = 0x02CF;
+ t['acutetonecmb'] = 0x0341;
+ t['acyrillic'] = 0x0430;
+ t['adblgrave'] = 0x0201;
+ t['addakgurmukhi'] = 0x0A71;
+ t['adeva'] = 0x0905;
+ t['adieresis'] = 0x00E4;
+ t['adieresiscyrillic'] = 0x04D3;
+ t['adieresismacron'] = 0x01DF;
+ t['adotbelow'] = 0x1EA1;
+ t['adotmacron'] = 0x01E1;
+ t['ae'] = 0x00E6;
+ t['aeacute'] = 0x01FD;
+ t['aekorean'] = 0x3150;
+ t['aemacron'] = 0x01E3;
+ t['afii00208'] = 0x2015;
+ t['afii08941'] = 0x20A4;
+ t['afii10017'] = 0x0410;
+ t['afii10018'] = 0x0411;
+ t['afii10019'] = 0x0412;
+ t['afii10020'] = 0x0413;
+ t['afii10021'] = 0x0414;
+ t['afii10022'] = 0x0415;
+ t['afii10023'] = 0x0401;
+ t['afii10024'] = 0x0416;
+ t['afii10025'] = 0x0417;
+ t['afii10026'] = 0x0418;
+ t['afii10027'] = 0x0419;
+ t['afii10028'] = 0x041A;
+ t['afii10029'] = 0x041B;
+ t['afii10030'] = 0x041C;
+ t['afii10031'] = 0x041D;
+ t['afii10032'] = 0x041E;
+ t['afii10033'] = 0x041F;
+ t['afii10034'] = 0x0420;
+ t['afii10035'] = 0x0421;
+ t['afii10036'] = 0x0422;
+ t['afii10037'] = 0x0423;
+ t['afii10038'] = 0x0424;
+ t['afii10039'] = 0x0425;
+ t['afii10040'] = 0x0426;
+ t['afii10041'] = 0x0427;
+ t['afii10042'] = 0x0428;
+ t['afii10043'] = 0x0429;
+ t['afii10044'] = 0x042A;
+ t['afii10045'] = 0x042B;
+ t['afii10046'] = 0x042C;
+ t['afii10047'] = 0x042D;
+ t['afii10048'] = 0x042E;
+ t['afii10049'] = 0x042F;
+ t['afii10050'] = 0x0490;
+ t['afii10051'] = 0x0402;
+ t['afii10052'] = 0x0403;
+ t['afii10053'] = 0x0404;
+ t['afii10054'] = 0x0405;
+ t['afii10055'] = 0x0406;
+ t['afii10056'] = 0x0407;
+ t['afii10057'] = 0x0408;
+ t['afii10058'] = 0x0409;
+ t['afii10059'] = 0x040A;
+ t['afii10060'] = 0x040B;
+ t['afii10061'] = 0x040C;
+ t['afii10062'] = 0x040E;
+ t['afii10063'] = 0xF6C4;
+ t['afii10064'] = 0xF6C5;
+ t['afii10065'] = 0x0430;
+ t['afii10066'] = 0x0431;
+ t['afii10067'] = 0x0432;
+ t['afii10068'] = 0x0433;
+ t['afii10069'] = 0x0434;
+ t['afii10070'] = 0x0435;
+ t['afii10071'] = 0x0451;
+ t['afii10072'] = 0x0436;
+ t['afii10073'] = 0x0437;
+ t['afii10074'] = 0x0438;
+ t['afii10075'] = 0x0439;
+ t['afii10076'] = 0x043A;
+ t['afii10077'] = 0x043B;
+ t['afii10078'] = 0x043C;
+ t['afii10079'] = 0x043D;
+ t['afii10080'] = 0x043E;
+ t['afii10081'] = 0x043F;
+ t['afii10082'] = 0x0440;
+ t['afii10083'] = 0x0441;
+ t['afii10084'] = 0x0442;
+ t['afii10085'] = 0x0443;
+ t['afii10086'] = 0x0444;
+ t['afii10087'] = 0x0445;
+ t['afii10088'] = 0x0446;
+ t['afii10089'] = 0x0447;
+ t['afii10090'] = 0x0448;
+ t['afii10091'] = 0x0449;
+ t['afii10092'] = 0x044A;
+ t['afii10093'] = 0x044B;
+ t['afii10094'] = 0x044C;
+ t['afii10095'] = 0x044D;
+ t['afii10096'] = 0x044E;
+ t['afii10097'] = 0x044F;
+ t['afii10098'] = 0x0491;
+ t['afii10099'] = 0x0452;
+ t['afii10100'] = 0x0453;
+ t['afii10101'] = 0x0454;
+ t['afii10102'] = 0x0455;
+ t['afii10103'] = 0x0456;
+ t['afii10104'] = 0x0457;
+ t['afii10105'] = 0x0458;
+ t['afii10106'] = 0x0459;
+ t['afii10107'] = 0x045A;
+ t['afii10108'] = 0x045B;
+ t['afii10109'] = 0x045C;
+ t['afii10110'] = 0x045E;
+ t['afii10145'] = 0x040F;
+ t['afii10146'] = 0x0462;
+ t['afii10147'] = 0x0472;
+ t['afii10148'] = 0x0474;
+ t['afii10192'] = 0xF6C6;
+ t['afii10193'] = 0x045F;
+ t['afii10194'] = 0x0463;
+ t['afii10195'] = 0x0473;
+ t['afii10196'] = 0x0475;
+ t['afii10831'] = 0xF6C7;
+ t['afii10832'] = 0xF6C8;
+ t['afii10846'] = 0x04D9;
+ t['afii299'] = 0x200E;
+ t['afii300'] = 0x200F;
+ t['afii301'] = 0x200D;
+ t['afii57381'] = 0x066A;
+ t['afii57388'] = 0x060C;
+ t['afii57392'] = 0x0660;
+ t['afii57393'] = 0x0661;
+ t['afii57394'] = 0x0662;
+ t['afii57395'] = 0x0663;
+ t['afii57396'] = 0x0664;
+ t['afii57397'] = 0x0665;
+ t['afii57398'] = 0x0666;
+ t['afii57399'] = 0x0667;
+ t['afii57400'] = 0x0668;
+ t['afii57401'] = 0x0669;
+ t['afii57403'] = 0x061B;
+ t['afii57407'] = 0x061F;
+ t['afii57409'] = 0x0621;
+ t['afii57410'] = 0x0622;
+ t['afii57411'] = 0x0623;
+ t['afii57412'] = 0x0624;
+ t['afii57413'] = 0x0625;
+ t['afii57414'] = 0x0626;
+ t['afii57415'] = 0x0627;
+ t['afii57416'] = 0x0628;
+ t['afii57417'] = 0x0629;
+ t['afii57418'] = 0x062A;
+ t['afii57419'] = 0x062B;
+ t['afii57420'] = 0x062C;
+ t['afii57421'] = 0x062D;
+ t['afii57422'] = 0x062E;
+ t['afii57423'] = 0x062F;
+ t['afii57424'] = 0x0630;
+ t['afii57425'] = 0x0631;
+ t['afii57426'] = 0x0632;
+ t['afii57427'] = 0x0633;
+ t['afii57428'] = 0x0634;
+ t['afii57429'] = 0x0635;
+ t['afii57430'] = 0x0636;
+ t['afii57431'] = 0x0637;
+ t['afii57432'] = 0x0638;
+ t['afii57433'] = 0x0639;
+ t['afii57434'] = 0x063A;
+ t['afii57440'] = 0x0640;
+ t['afii57441'] = 0x0641;
+ t['afii57442'] = 0x0642;
+ t['afii57443'] = 0x0643;
+ t['afii57444'] = 0x0644;
+ t['afii57445'] = 0x0645;
+ t['afii57446'] = 0x0646;
+ t['afii57448'] = 0x0648;
+ t['afii57449'] = 0x0649;
+ t['afii57450'] = 0x064A;
+ t['afii57451'] = 0x064B;
+ t['afii57452'] = 0x064C;
+ t['afii57453'] = 0x064D;
+ t['afii57454'] = 0x064E;
+ t['afii57455'] = 0x064F;
+ t['afii57456'] = 0x0650;
+ t['afii57457'] = 0x0651;
+ t['afii57458'] = 0x0652;
+ t['afii57470'] = 0x0647;
+ t['afii57505'] = 0x06A4;
+ t['afii57506'] = 0x067E;
+ t['afii57507'] = 0x0686;
+ t['afii57508'] = 0x0698;
+ t['afii57509'] = 0x06AF;
+ t['afii57511'] = 0x0679;
+ t['afii57512'] = 0x0688;
+ t['afii57513'] = 0x0691;
+ t['afii57514'] = 0x06BA;
+ t['afii57519'] = 0x06D2;
+ t['afii57534'] = 0x06D5;
+ t['afii57636'] = 0x20AA;
+ t['afii57645'] = 0x05BE;
+ t['afii57658'] = 0x05C3;
+ t['afii57664'] = 0x05D0;
+ t['afii57665'] = 0x05D1;
+ t['afii57666'] = 0x05D2;
+ t['afii57667'] = 0x05D3;
+ t['afii57668'] = 0x05D4;
+ t['afii57669'] = 0x05D5;
+ t['afii57670'] = 0x05D6;
+ t['afii57671'] = 0x05D7;
+ t['afii57672'] = 0x05D8;
+ t['afii57673'] = 0x05D9;
+ t['afii57674'] = 0x05DA;
+ t['afii57675'] = 0x05DB;
+ t['afii57676'] = 0x05DC;
+ t['afii57677'] = 0x05DD;
+ t['afii57678'] = 0x05DE;
+ t['afii57679'] = 0x05DF;
+ t['afii57680'] = 0x05E0;
+ t['afii57681'] = 0x05E1;
+ t['afii57682'] = 0x05E2;
+ t['afii57683'] = 0x05E3;
+ t['afii57684'] = 0x05E4;
+ t['afii57685'] = 0x05E5;
+ t['afii57686'] = 0x05E6;
+ t['afii57687'] = 0x05E7;
+ t['afii57688'] = 0x05E8;
+ t['afii57689'] = 0x05E9;
+ t['afii57690'] = 0x05EA;
+ t['afii57694'] = 0xFB2A;
+ t['afii57695'] = 0xFB2B;
+ t['afii57700'] = 0xFB4B;
+ t['afii57705'] = 0xFB1F;
+ t['afii57716'] = 0x05F0;
+ t['afii57717'] = 0x05F1;
+ t['afii57718'] = 0x05F2;
+ t['afii57723'] = 0xFB35;
+ t['afii57793'] = 0x05B4;
+ t['afii57794'] = 0x05B5;
+ t['afii57795'] = 0x05B6;
+ t['afii57796'] = 0x05BB;
+ t['afii57797'] = 0x05B8;
+ t['afii57798'] = 0x05B7;
+ t['afii57799'] = 0x05B0;
+ t['afii57800'] = 0x05B2;
+ t['afii57801'] = 0x05B1;
+ t['afii57802'] = 0x05B3;
+ t['afii57803'] = 0x05C2;
+ t['afii57804'] = 0x05C1;
+ t['afii57806'] = 0x05B9;
+ t['afii57807'] = 0x05BC;
+ t['afii57839'] = 0x05BD;
+ t['afii57841'] = 0x05BF;
+ t['afii57842'] = 0x05C0;
+ t['afii57929'] = 0x02BC;
+ t['afii61248'] = 0x2105;
+ t['afii61289'] = 0x2113;
+ t['afii61352'] = 0x2116;
+ t['afii61573'] = 0x202C;
+ t['afii61574'] = 0x202D;
+ t['afii61575'] = 0x202E;
+ t['afii61664'] = 0x200C;
+ t['afii63167'] = 0x066D;
+ t['afii64937'] = 0x02BD;
+ t['agrave'] = 0x00E0;
+ t['agujarati'] = 0x0A85;
+ t['agurmukhi'] = 0x0A05;
+ t['ahiragana'] = 0x3042;
+ t['ahookabove'] = 0x1EA3;
+ t['aibengali'] = 0x0990;
+ t['aibopomofo'] = 0x311E;
+ t['aideva'] = 0x0910;
+ t['aiecyrillic'] = 0x04D5;
+ t['aigujarati'] = 0x0A90;
+ t['aigurmukhi'] = 0x0A10;
+ t['aimatragurmukhi'] = 0x0A48;
+ t['ainarabic'] = 0x0639;
+ t['ainfinalarabic'] = 0xFECA;
+ t['aininitialarabic'] = 0xFECB;
+ t['ainmedialarabic'] = 0xFECC;
+ t['ainvertedbreve'] = 0x0203;
+ t['aivowelsignbengali'] = 0x09C8;
+ t['aivowelsigndeva'] = 0x0948;
+ t['aivowelsigngujarati'] = 0x0AC8;
+ t['akatakana'] = 0x30A2;
+ t['akatakanahalfwidth'] = 0xFF71;
+ t['akorean'] = 0x314F;
+ t['alef'] = 0x05D0;
+ t['alefarabic'] = 0x0627;
+ t['alefdageshhebrew'] = 0xFB30;
+ t['aleffinalarabic'] = 0xFE8E;
+ t['alefhamzaabovearabic'] = 0x0623;
+ t['alefhamzaabovefinalarabic'] = 0xFE84;
+ t['alefhamzabelowarabic'] = 0x0625;
+ t['alefhamzabelowfinalarabic'] = 0xFE88;
+ t['alefhebrew'] = 0x05D0;
+ t['aleflamedhebrew'] = 0xFB4F;
+ t['alefmaddaabovearabic'] = 0x0622;
+ t['alefmaddaabovefinalarabic'] = 0xFE82;
+ t['alefmaksuraarabic'] = 0x0649;
+ t['alefmaksurafinalarabic'] = 0xFEF0;
+ t['alefmaksurainitialarabic'] = 0xFEF3;
+ t['alefmaksuramedialarabic'] = 0xFEF4;
+ t['alefpatahhebrew'] = 0xFB2E;
+ t['alefqamatshebrew'] = 0xFB2F;
+ t['aleph'] = 0x2135;
+ t['allequal'] = 0x224C;
+ t['alpha'] = 0x03B1;
+ t['alphatonos'] = 0x03AC;
+ t['amacron'] = 0x0101;
+ t['amonospace'] = 0xFF41;
+ t['ampersand'] = 0x0026;
+ t['ampersandmonospace'] = 0xFF06;
+ t['ampersandsmall'] = 0xF726;
+ t['amsquare'] = 0x33C2;
+ t['anbopomofo'] = 0x3122;
+ t['angbopomofo'] = 0x3124;
+ t['angbracketleft'] = 0x3008;
+ t['angbracketright'] = 0x3009;
+ t['angkhankhuthai'] = 0x0E5A;
+ t['angle'] = 0x2220;
+ t['anglebracketleft'] = 0x3008;
+ t['anglebracketleftvertical'] = 0xFE3F;
+ t['anglebracketright'] = 0x3009;
+ t['anglebracketrightvertical'] = 0xFE40;
+ t['angleleft'] = 0x2329;
+ t['angleright'] = 0x232A;
+ t['angstrom'] = 0x212B;
+ t['anoteleia'] = 0x0387;
+ t['anudattadeva'] = 0x0952;
+ t['anusvarabengali'] = 0x0982;
+ t['anusvaradeva'] = 0x0902;
+ t['anusvaragujarati'] = 0x0A82;
+ t['aogonek'] = 0x0105;
+ t['apaatosquare'] = 0x3300;
+ t['aparen'] = 0x249C;
+ t['apostrophearmenian'] = 0x055A;
+ t['apostrophemod'] = 0x02BC;
+ t['apple'] = 0xF8FF;
+ t['approaches'] = 0x2250;
+ t['approxequal'] = 0x2248;
+ t['approxequalorimage'] = 0x2252;
+ t['approximatelyequal'] = 0x2245;
+ t['araeaekorean'] = 0x318E;
+ t['araeakorean'] = 0x318D;
+ t['arc'] = 0x2312;
+ t['arighthalfring'] = 0x1E9A;
+ t['aring'] = 0x00E5;
+ t['aringacute'] = 0x01FB;
+ t['aringbelow'] = 0x1E01;
+ t['arrowboth'] = 0x2194;
+ t['arrowdashdown'] = 0x21E3;
+ t['arrowdashleft'] = 0x21E0;
+ t['arrowdashright'] = 0x21E2;
+ t['arrowdashup'] = 0x21E1;
+ t['arrowdblboth'] = 0x21D4;
+ t['arrowdbldown'] = 0x21D3;
+ t['arrowdblleft'] = 0x21D0;
+ t['arrowdblright'] = 0x21D2;
+ t['arrowdblup'] = 0x21D1;
+ t['arrowdown'] = 0x2193;
+ t['arrowdownleft'] = 0x2199;
+ t['arrowdownright'] = 0x2198;
+ t['arrowdownwhite'] = 0x21E9;
+ t['arrowheaddownmod'] = 0x02C5;
+ t['arrowheadleftmod'] = 0x02C2;
+ t['arrowheadrightmod'] = 0x02C3;
+ t['arrowheadupmod'] = 0x02C4;
+ t['arrowhorizex'] = 0xF8E7;
+ t['arrowleft'] = 0x2190;
+ t['arrowleftdbl'] = 0x21D0;
+ t['arrowleftdblstroke'] = 0x21CD;
+ t['arrowleftoverright'] = 0x21C6;
+ t['arrowleftwhite'] = 0x21E6;
+ t['arrowright'] = 0x2192;
+ t['arrowrightdblstroke'] = 0x21CF;
+ t['arrowrightheavy'] = 0x279E;
+ t['arrowrightoverleft'] = 0x21C4;
+ t['arrowrightwhite'] = 0x21E8;
+ t['arrowtableft'] = 0x21E4;
+ t['arrowtabright'] = 0x21E5;
+ t['arrowup'] = 0x2191;
+ t['arrowupdn'] = 0x2195;
+ t['arrowupdnbse'] = 0x21A8;
+ t['arrowupdownbase'] = 0x21A8;
+ t['arrowupleft'] = 0x2196;
+ t['arrowupleftofdown'] = 0x21C5;
+ t['arrowupright'] = 0x2197;
+ t['arrowupwhite'] = 0x21E7;
+ t['arrowvertex'] = 0xF8E6;
+ t['asciicircum'] = 0x005E;
+ t['asciicircummonospace'] = 0xFF3E;
+ t['asciitilde'] = 0x007E;
+ t['asciitildemonospace'] = 0xFF5E;
+ t['ascript'] = 0x0251;
+ t['ascriptturned'] = 0x0252;
+ t['asmallhiragana'] = 0x3041;
+ t['asmallkatakana'] = 0x30A1;
+ t['asmallkatakanahalfwidth'] = 0xFF67;
+ t['asterisk'] = 0x002A;
+ t['asteriskaltonearabic'] = 0x066D;
+ t['asteriskarabic'] = 0x066D;
+ t['asteriskmath'] = 0x2217;
+ t['asteriskmonospace'] = 0xFF0A;
+ t['asterisksmall'] = 0xFE61;
+ t['asterism'] = 0x2042;
+ t['asuperior'] = 0xF6E9;
+ t['asymptoticallyequal'] = 0x2243;
+ t['at'] = 0x0040;
+ t['atilde'] = 0x00E3;
+ t['atmonospace'] = 0xFF20;
+ t['atsmall'] = 0xFE6B;
+ t['aturned'] = 0x0250;
+ t['aubengali'] = 0x0994;
+ t['aubopomofo'] = 0x3120;
+ t['audeva'] = 0x0914;
+ t['augujarati'] = 0x0A94;
+ t['augurmukhi'] = 0x0A14;
+ t['aulengthmarkbengali'] = 0x09D7;
+ t['aumatragurmukhi'] = 0x0A4C;
+ t['auvowelsignbengali'] = 0x09CC;
+ t['auvowelsigndeva'] = 0x094C;
+ t['auvowelsigngujarati'] = 0x0ACC;
+ t['avagrahadeva'] = 0x093D;
+ t['aybarmenian'] = 0x0561;
+ t['ayin'] = 0x05E2;
+ t['ayinaltonehebrew'] = 0xFB20;
+ t['ayinhebrew'] = 0x05E2;
+ t['b'] = 0x0062;
+ t['babengali'] = 0x09AC;
+ t['backslash'] = 0x005C;
+ t['backslashmonospace'] = 0xFF3C;
+ t['badeva'] = 0x092C;
+ t['bagujarati'] = 0x0AAC;
+ t['bagurmukhi'] = 0x0A2C;
+ t['bahiragana'] = 0x3070;
+ t['bahtthai'] = 0x0E3F;
+ t['bakatakana'] = 0x30D0;
+ t['bar'] = 0x007C;
+ t['barmonospace'] = 0xFF5C;
+ t['bbopomofo'] = 0x3105;
+ t['bcircle'] = 0x24D1;
+ t['bdotaccent'] = 0x1E03;
+ t['bdotbelow'] = 0x1E05;
+ t['beamedsixteenthnotes'] = 0x266C;
+ t['because'] = 0x2235;
+ t['becyrillic'] = 0x0431;
+ t['beharabic'] = 0x0628;
+ t['behfinalarabic'] = 0xFE90;
+ t['behinitialarabic'] = 0xFE91;
+ t['behiragana'] = 0x3079;
+ t['behmedialarabic'] = 0xFE92;
+ t['behmeeminitialarabic'] = 0xFC9F;
+ t['behmeemisolatedarabic'] = 0xFC08;
+ t['behnoonfinalarabic'] = 0xFC6D;
+ t['bekatakana'] = 0x30D9;
+ t['benarmenian'] = 0x0562;
+ t['bet'] = 0x05D1;
+ t['beta'] = 0x03B2;
+ t['betasymbolgreek'] = 0x03D0;
+ t['betdagesh'] = 0xFB31;
+ t['betdageshhebrew'] = 0xFB31;
+ t['bethebrew'] = 0x05D1;
+ t['betrafehebrew'] = 0xFB4C;
+ t['bhabengali'] = 0x09AD;
+ t['bhadeva'] = 0x092D;
+ t['bhagujarati'] = 0x0AAD;
+ t['bhagurmukhi'] = 0x0A2D;
+ t['bhook'] = 0x0253;
+ t['bihiragana'] = 0x3073;
+ t['bikatakana'] = 0x30D3;
+ t['bilabialclick'] = 0x0298;
+ t['bindigurmukhi'] = 0x0A02;
+ t['birusquare'] = 0x3331;
+ t['blackcircle'] = 0x25CF;
+ t['blackdiamond'] = 0x25C6;
+ t['blackdownpointingtriangle'] = 0x25BC;
+ t['blackleftpointingpointer'] = 0x25C4;
+ t['blackleftpointingtriangle'] = 0x25C0;
+ t['blacklenticularbracketleft'] = 0x3010;
+ t['blacklenticularbracketleftvertical'] = 0xFE3B;
+ t['blacklenticularbracketright'] = 0x3011;
+ t['blacklenticularbracketrightvertical'] = 0xFE3C;
+ t['blacklowerlefttriangle'] = 0x25E3;
+ t['blacklowerrighttriangle'] = 0x25E2;
+ t['blackrectangle'] = 0x25AC;
+ t['blackrightpointingpointer'] = 0x25BA;
+ t['blackrightpointingtriangle'] = 0x25B6;
+ t['blacksmallsquare'] = 0x25AA;
+ t['blacksmilingface'] = 0x263B;
+ t['blacksquare'] = 0x25A0;
+ t['blackstar'] = 0x2605;
+ t['blackupperlefttriangle'] = 0x25E4;
+ t['blackupperrighttriangle'] = 0x25E5;
+ t['blackuppointingsmalltriangle'] = 0x25B4;
+ t['blackuppointingtriangle'] = 0x25B2;
+ t['blank'] = 0x2423;
+ t['blinebelow'] = 0x1E07;
+ t['block'] = 0x2588;
+ t['bmonospace'] = 0xFF42;
+ t['bobaimaithai'] = 0x0E1A;
+ t['bohiragana'] = 0x307C;
+ t['bokatakana'] = 0x30DC;
+ t['bparen'] = 0x249D;
+ t['bqsquare'] = 0x33C3;
+ t['braceex'] = 0xF8F4;
+ t['braceleft'] = 0x007B;
+ t['braceleftbt'] = 0xF8F3;
+ t['braceleftmid'] = 0xF8F2;
+ t['braceleftmonospace'] = 0xFF5B;
+ t['braceleftsmall'] = 0xFE5B;
+ t['bracelefttp'] = 0xF8F1;
+ t['braceleftvertical'] = 0xFE37;
+ t['braceright'] = 0x007D;
+ t['bracerightbt'] = 0xF8FE;
+ t['bracerightmid'] = 0xF8FD;
+ t['bracerightmonospace'] = 0xFF5D;
+ t['bracerightsmall'] = 0xFE5C;
+ t['bracerighttp'] = 0xF8FC;
+ t['bracerightvertical'] = 0xFE38;
+ t['bracketleft'] = 0x005B;
+ t['bracketleftbt'] = 0xF8F0;
+ t['bracketleftex'] = 0xF8EF;
+ t['bracketleftmonospace'] = 0xFF3B;
+ t['bracketlefttp'] = 0xF8EE;
+ t['bracketright'] = 0x005D;
+ t['bracketrightbt'] = 0xF8FB;
+ t['bracketrightex'] = 0xF8FA;
+ t['bracketrightmonospace'] = 0xFF3D;
+ t['bracketrighttp'] = 0xF8F9;
+ t['breve'] = 0x02D8;
+ t['brevebelowcmb'] = 0x032E;
+ t['brevecmb'] = 0x0306;
+ t['breveinvertedbelowcmb'] = 0x032F;
+ t['breveinvertedcmb'] = 0x0311;
+ t['breveinverteddoublecmb'] = 0x0361;
+ t['bridgebelowcmb'] = 0x032A;
+ t['bridgeinvertedbelowcmb'] = 0x033A;
+ t['brokenbar'] = 0x00A6;
+ t['bstroke'] = 0x0180;
+ t['bsuperior'] = 0xF6EA;
+ t['btopbar'] = 0x0183;
+ t['buhiragana'] = 0x3076;
+ t['bukatakana'] = 0x30D6;
+ t['bullet'] = 0x2022;
+ t['bulletinverse'] = 0x25D8;
+ t['bulletoperator'] = 0x2219;
+ t['bullseye'] = 0x25CE;
+ t['c'] = 0x0063;
+ t['caarmenian'] = 0x056E;
+ t['cabengali'] = 0x099A;
+ t['cacute'] = 0x0107;
+ t['cadeva'] = 0x091A;
+ t['cagujarati'] = 0x0A9A;
+ t['cagurmukhi'] = 0x0A1A;
+ t['calsquare'] = 0x3388;
+ t['candrabindubengali'] = 0x0981;
+ t['candrabinducmb'] = 0x0310;
+ t['candrabindudeva'] = 0x0901;
+ t['candrabindugujarati'] = 0x0A81;
+ t['capslock'] = 0x21EA;
+ t['careof'] = 0x2105;
+ t['caron'] = 0x02C7;
+ t['caronbelowcmb'] = 0x032C;
+ t['caroncmb'] = 0x030C;
+ t['carriagereturn'] = 0x21B5;
+ t['cbopomofo'] = 0x3118;
+ t['ccaron'] = 0x010D;
+ t['ccedilla'] = 0x00E7;
+ t['ccedillaacute'] = 0x1E09;
+ t['ccircle'] = 0x24D2;
+ t['ccircumflex'] = 0x0109;
+ t['ccurl'] = 0x0255;
+ t['cdot'] = 0x010B;
+ t['cdotaccent'] = 0x010B;
+ t['cdsquare'] = 0x33C5;
+ t['cedilla'] = 0x00B8;
+ t['cedillacmb'] = 0x0327;
+ t['cent'] = 0x00A2;
+ t['centigrade'] = 0x2103;
+ t['centinferior'] = 0xF6DF;
+ t['centmonospace'] = 0xFFE0;
+ t['centoldstyle'] = 0xF7A2;
+ t['centsuperior'] = 0xF6E0;
+ t['chaarmenian'] = 0x0579;
+ t['chabengali'] = 0x099B;
+ t['chadeva'] = 0x091B;
+ t['chagujarati'] = 0x0A9B;
+ t['chagurmukhi'] = 0x0A1B;
+ t['chbopomofo'] = 0x3114;
+ t['cheabkhasiancyrillic'] = 0x04BD;
+ t['checkmark'] = 0x2713;
+ t['checyrillic'] = 0x0447;
+ t['chedescenderabkhasiancyrillic'] = 0x04BF;
+ t['chedescendercyrillic'] = 0x04B7;
+ t['chedieresiscyrillic'] = 0x04F5;
+ t['cheharmenian'] = 0x0573;
+ t['chekhakassiancyrillic'] = 0x04CC;
+ t['cheverticalstrokecyrillic'] = 0x04B9;
+ t['chi'] = 0x03C7;
+ t['chieuchacirclekorean'] = 0x3277;
+ t['chieuchaparenkorean'] = 0x3217;
+ t['chieuchcirclekorean'] = 0x3269;
+ t['chieuchkorean'] = 0x314A;
+ t['chieuchparenkorean'] = 0x3209;
+ t['chochangthai'] = 0x0E0A;
+ t['chochanthai'] = 0x0E08;
+ t['chochingthai'] = 0x0E09;
+ t['chochoethai'] = 0x0E0C;
+ t['chook'] = 0x0188;
+ t['cieucacirclekorean'] = 0x3276;
+ t['cieucaparenkorean'] = 0x3216;
+ t['cieuccirclekorean'] = 0x3268;
+ t['cieuckorean'] = 0x3148;
+ t['cieucparenkorean'] = 0x3208;
+ t['cieucuparenkorean'] = 0x321C;
+ t['circle'] = 0x25CB;
+ t['circlecopyrt'] = 0x00A9;
+ t['circlemultiply'] = 0x2297;
+ t['circleot'] = 0x2299;
+ t['circleplus'] = 0x2295;
+ t['circlepostalmark'] = 0x3036;
+ t['circlewithlefthalfblack'] = 0x25D0;
+ t['circlewithrighthalfblack'] = 0x25D1;
+ t['circumflex'] = 0x02C6;
+ t['circumflexbelowcmb'] = 0x032D;
+ t['circumflexcmb'] = 0x0302;
+ t['clear'] = 0x2327;
+ t['clickalveolar'] = 0x01C2;
+ t['clickdental'] = 0x01C0;
+ t['clicklateral'] = 0x01C1;
+ t['clickretroflex'] = 0x01C3;
+ t['club'] = 0x2663;
+ t['clubsuitblack'] = 0x2663;
+ t['clubsuitwhite'] = 0x2667;
+ t['cmcubedsquare'] = 0x33A4;
+ t['cmonospace'] = 0xFF43;
+ t['cmsquaredsquare'] = 0x33A0;
+ t['coarmenian'] = 0x0581;
+ t['colon'] = 0x003A;
+ t['colonmonetary'] = 0x20A1;
+ t['colonmonospace'] = 0xFF1A;
+ t['colonsign'] = 0x20A1;
+ t['colonsmall'] = 0xFE55;
+ t['colontriangularhalfmod'] = 0x02D1;
+ t['colontriangularmod'] = 0x02D0;
+ t['comma'] = 0x002C;
+ t['commaabovecmb'] = 0x0313;
+ t['commaaboverightcmb'] = 0x0315;
+ t['commaaccent'] = 0xF6C3;
+ t['commaarabic'] = 0x060C;
+ t['commaarmenian'] = 0x055D;
+ t['commainferior'] = 0xF6E1;
+ t['commamonospace'] = 0xFF0C;
+ t['commareversedabovecmb'] = 0x0314;
+ t['commareversedmod'] = 0x02BD;
+ t['commasmall'] = 0xFE50;
+ t['commasuperior'] = 0xF6E2;
+ t['commaturnedabovecmb'] = 0x0312;
+ t['commaturnedmod'] = 0x02BB;
+ t['compass'] = 0x263C;
+ t['congruent'] = 0x2245;
+ t['contourintegral'] = 0x222E;
+ t['control'] = 0x2303;
+ t['controlACK'] = 0x0006;
+ t['controlBEL'] = 0x0007;
+ t['controlBS'] = 0x0008;
+ t['controlCAN'] = 0x0018;
+ t['controlCR'] = 0x000D;
+ t['controlDC1'] = 0x0011;
+ t['controlDC2'] = 0x0012;
+ t['controlDC3'] = 0x0013;
+ t['controlDC4'] = 0x0014;
+ t['controlDEL'] = 0x007F;
+ t['controlDLE'] = 0x0010;
+ t['controlEM'] = 0x0019;
+ t['controlENQ'] = 0x0005;
+ t['controlEOT'] = 0x0004;
+ t['controlESC'] = 0x001B;
+ t['controlETB'] = 0x0017;
+ t['controlETX'] = 0x0003;
+ t['controlFF'] = 0x000C;
+ t['controlFS'] = 0x001C;
+ t['controlGS'] = 0x001D;
+ t['controlHT'] = 0x0009;
+ t['controlLF'] = 0x000A;
+ t['controlNAK'] = 0x0015;
+ t['controlNULL'] = 0x0000;
+ t['controlRS'] = 0x001E;
+ t['controlSI'] = 0x000F;
+ t['controlSO'] = 0x000E;
+ t['controlSOT'] = 0x0002;
+ t['controlSTX'] = 0x0001;
+ t['controlSUB'] = 0x001A;
+ t['controlSYN'] = 0x0016;
+ t['controlUS'] = 0x001F;
+ t['controlVT'] = 0x000B;
+ t['copyright'] = 0x00A9;
+ t['copyrightsans'] = 0xF8E9;
+ t['copyrightserif'] = 0xF6D9;
+ t['cornerbracketleft'] = 0x300C;
+ t['cornerbracketlefthalfwidth'] = 0xFF62;
+ t['cornerbracketleftvertical'] = 0xFE41;
+ t['cornerbracketright'] = 0x300D;
+ t['cornerbracketrighthalfwidth'] = 0xFF63;
+ t['cornerbracketrightvertical'] = 0xFE42;
+ t['corporationsquare'] = 0x337F;
+ t['cosquare'] = 0x33C7;
+ t['coverkgsquare'] = 0x33C6;
+ t['cparen'] = 0x249E;
+ t['cruzeiro'] = 0x20A2;
+ t['cstretched'] = 0x0297;
+ t['curlyand'] = 0x22CF;
+ t['curlyor'] = 0x22CE;
+ t['currency'] = 0x00A4;
+ t['cyrBreve'] = 0xF6D1;
+ t['cyrFlex'] = 0xF6D2;
+ t['cyrbreve'] = 0xF6D4;
+ t['cyrflex'] = 0xF6D5;
+ t['d'] = 0x0064;
+ t['daarmenian'] = 0x0564;
+ t['dabengali'] = 0x09A6;
+ t['dadarabic'] = 0x0636;
+ t['dadeva'] = 0x0926;
+ t['dadfinalarabic'] = 0xFEBE;
+ t['dadinitialarabic'] = 0xFEBF;
+ t['dadmedialarabic'] = 0xFEC0;
+ t['dagesh'] = 0x05BC;
+ t['dageshhebrew'] = 0x05BC;
+ t['dagger'] = 0x2020;
+ t['daggerdbl'] = 0x2021;
+ t['dagujarati'] = 0x0AA6;
+ t['dagurmukhi'] = 0x0A26;
+ t['dahiragana'] = 0x3060;
+ t['dakatakana'] = 0x30C0;
+ t['dalarabic'] = 0x062F;
+ t['dalet'] = 0x05D3;
+ t['daletdagesh'] = 0xFB33;
+ t['daletdageshhebrew'] = 0xFB33;
+ t['dalethebrew'] = 0x05D3;
+ t['dalfinalarabic'] = 0xFEAA;
+ t['dammaarabic'] = 0x064F;
+ t['dammalowarabic'] = 0x064F;
+ t['dammatanaltonearabic'] = 0x064C;
+ t['dammatanarabic'] = 0x064C;
+ t['danda'] = 0x0964;
+ t['dargahebrew'] = 0x05A7;
+ t['dargalefthebrew'] = 0x05A7;
+ t['dasiapneumatacyrilliccmb'] = 0x0485;
+ t['dblGrave'] = 0xF6D3;
+ t['dblanglebracketleft'] = 0x300A;
+ t['dblanglebracketleftvertical'] = 0xFE3D;
+ t['dblanglebracketright'] = 0x300B;
+ t['dblanglebracketrightvertical'] = 0xFE3E;
+ t['dblarchinvertedbelowcmb'] = 0x032B;
+ t['dblarrowleft'] = 0x21D4;
+ t['dblarrowright'] = 0x21D2;
+ t['dbldanda'] = 0x0965;
+ t['dblgrave'] = 0xF6D6;
+ t['dblgravecmb'] = 0x030F;
+ t['dblintegral'] = 0x222C;
+ t['dbllowline'] = 0x2017;
+ t['dbllowlinecmb'] = 0x0333;
+ t['dbloverlinecmb'] = 0x033F;
+ t['dblprimemod'] = 0x02BA;
+ t['dblverticalbar'] = 0x2016;
+ t['dblverticallineabovecmb'] = 0x030E;
+ t['dbopomofo'] = 0x3109;
+ t['dbsquare'] = 0x33C8;
+ t['dcaron'] = 0x010F;
+ t['dcedilla'] = 0x1E11;
+ t['dcircle'] = 0x24D3;
+ t['dcircumflexbelow'] = 0x1E13;
+ t['dcroat'] = 0x0111;
+ t['ddabengali'] = 0x09A1;
+ t['ddadeva'] = 0x0921;
+ t['ddagujarati'] = 0x0AA1;
+ t['ddagurmukhi'] = 0x0A21;
+ t['ddalarabic'] = 0x0688;
+ t['ddalfinalarabic'] = 0xFB89;
+ t['dddhadeva'] = 0x095C;
+ t['ddhabengali'] = 0x09A2;
+ t['ddhadeva'] = 0x0922;
+ t['ddhagujarati'] = 0x0AA2;
+ t['ddhagurmukhi'] = 0x0A22;
+ t['ddotaccent'] = 0x1E0B;
+ t['ddotbelow'] = 0x1E0D;
+ t['decimalseparatorarabic'] = 0x066B;
+ t['decimalseparatorpersian'] = 0x066B;
+ t['decyrillic'] = 0x0434;
+ t['degree'] = 0x00B0;
+ t['dehihebrew'] = 0x05AD;
+ t['dehiragana'] = 0x3067;
+ t['deicoptic'] = 0x03EF;
+ t['dekatakana'] = 0x30C7;
+ t['deleteleft'] = 0x232B;
+ t['deleteright'] = 0x2326;
+ t['delta'] = 0x03B4;
+ t['deltaturned'] = 0x018D;
+ t['denominatorminusonenumeratorbengali'] = 0x09F8;
+ t['dezh'] = 0x02A4;
+ t['dhabengali'] = 0x09A7;
+ t['dhadeva'] = 0x0927;
+ t['dhagujarati'] = 0x0AA7;
+ t['dhagurmukhi'] = 0x0A27;
+ t['dhook'] = 0x0257;
+ t['dialytikatonos'] = 0x0385;
+ t['dialytikatonoscmb'] = 0x0344;
+ t['diamond'] = 0x2666;
+ t['diamondsuitwhite'] = 0x2662;
+ t['dieresis'] = 0x00A8;
+ t['dieresisacute'] = 0xF6D7;
+ t['dieresisbelowcmb'] = 0x0324;
+ t['dieresiscmb'] = 0x0308;
+ t['dieresisgrave'] = 0xF6D8;
+ t['dieresistonos'] = 0x0385;
+ t['dihiragana'] = 0x3062;
+ t['dikatakana'] = 0x30C2;
+ t['dittomark'] = 0x3003;
+ t['divide'] = 0x00F7;
+ t['divides'] = 0x2223;
+ t['divisionslash'] = 0x2215;
+ t['djecyrillic'] = 0x0452;
+ t['dkshade'] = 0x2593;
+ t['dlinebelow'] = 0x1E0F;
+ t['dlsquare'] = 0x3397;
+ t['dmacron'] = 0x0111;
+ t['dmonospace'] = 0xFF44;
+ t['dnblock'] = 0x2584;
+ t['dochadathai'] = 0x0E0E;
+ t['dodekthai'] = 0x0E14;
+ t['dohiragana'] = 0x3069;
+ t['dokatakana'] = 0x30C9;
+ t['dollar'] = 0x0024;
+ t['dollarinferior'] = 0xF6E3;
+ t['dollarmonospace'] = 0xFF04;
+ t['dollaroldstyle'] = 0xF724;
+ t['dollarsmall'] = 0xFE69;
+ t['dollarsuperior'] = 0xF6E4;
+ t['dong'] = 0x20AB;
+ t['dorusquare'] = 0x3326;
+ t['dotaccent'] = 0x02D9;
+ t['dotaccentcmb'] = 0x0307;
+ t['dotbelowcmb'] = 0x0323;
+ t['dotbelowcomb'] = 0x0323;
+ t['dotkatakana'] = 0x30FB;
+ t['dotlessi'] = 0x0131;
+ t['dotlessj'] = 0xF6BE;
+ t['dotlessjstrokehook'] = 0x0284;
+ t['dotmath'] = 0x22C5;
+ t['dottedcircle'] = 0x25CC;
+ t['doubleyodpatah'] = 0xFB1F;
+ t['doubleyodpatahhebrew'] = 0xFB1F;
+ t['downtackbelowcmb'] = 0x031E;
+ t['downtackmod'] = 0x02D5;
+ t['dparen'] = 0x249F;
+ t['dsuperior'] = 0xF6EB;
+ t['dtail'] = 0x0256;
+ t['dtopbar'] = 0x018C;
+ t['duhiragana'] = 0x3065;
+ t['dukatakana'] = 0x30C5;
+ t['dz'] = 0x01F3;
+ t['dzaltone'] = 0x02A3;
+ t['dzcaron'] = 0x01C6;
+ t['dzcurl'] = 0x02A5;
+ t['dzeabkhasiancyrillic'] = 0x04E1;
+ t['dzecyrillic'] = 0x0455;
+ t['dzhecyrillic'] = 0x045F;
+ t['e'] = 0x0065;
+ t['eacute'] = 0x00E9;
+ t['earth'] = 0x2641;
+ t['ebengali'] = 0x098F;
+ t['ebopomofo'] = 0x311C;
+ t['ebreve'] = 0x0115;
+ t['ecandradeva'] = 0x090D;
+ t['ecandragujarati'] = 0x0A8D;
+ t['ecandravowelsigndeva'] = 0x0945;
+ t['ecandravowelsigngujarati'] = 0x0AC5;
+ t['ecaron'] = 0x011B;
+ t['ecedillabreve'] = 0x1E1D;
+ t['echarmenian'] = 0x0565;
+ t['echyiwnarmenian'] = 0x0587;
+ t['ecircle'] = 0x24D4;
+ t['ecircumflex'] = 0x00EA;
+ t['ecircumflexacute'] = 0x1EBF;
+ t['ecircumflexbelow'] = 0x1E19;
+ t['ecircumflexdotbelow'] = 0x1EC7;
+ t['ecircumflexgrave'] = 0x1EC1;
+ t['ecircumflexhookabove'] = 0x1EC3;
+ t['ecircumflextilde'] = 0x1EC5;
+ t['ecyrillic'] = 0x0454;
+ t['edblgrave'] = 0x0205;
+ t['edeva'] = 0x090F;
+ t['edieresis'] = 0x00EB;
+ t['edot'] = 0x0117;
+ t['edotaccent'] = 0x0117;
+ t['edotbelow'] = 0x1EB9;
+ t['eegurmukhi'] = 0x0A0F;
+ t['eematragurmukhi'] = 0x0A47;
+ t['efcyrillic'] = 0x0444;
+ t['egrave'] = 0x00E8;
+ t['egujarati'] = 0x0A8F;
+ t['eharmenian'] = 0x0567;
+ t['ehbopomofo'] = 0x311D;
+ t['ehiragana'] = 0x3048;
+ t['ehookabove'] = 0x1EBB;
+ t['eibopomofo'] = 0x311F;
+ t['eight'] = 0x0038;
+ t['eightarabic'] = 0x0668;
+ t['eightbengali'] = 0x09EE;
+ t['eightcircle'] = 0x2467;
+ t['eightcircleinversesansserif'] = 0x2791;
+ t['eightdeva'] = 0x096E;
+ t['eighteencircle'] = 0x2471;
+ t['eighteenparen'] = 0x2485;
+ t['eighteenperiod'] = 0x2499;
+ t['eightgujarati'] = 0x0AEE;
+ t['eightgurmukhi'] = 0x0A6E;
+ t['eighthackarabic'] = 0x0668;
+ t['eighthangzhou'] = 0x3028;
+ t['eighthnotebeamed'] = 0x266B;
+ t['eightideographicparen'] = 0x3227;
+ t['eightinferior'] = 0x2088;
+ t['eightmonospace'] = 0xFF18;
+ t['eightoldstyle'] = 0xF738;
+ t['eightparen'] = 0x247B;
+ t['eightperiod'] = 0x248F;
+ t['eightpersian'] = 0x06F8;
+ t['eightroman'] = 0x2177;
+ t['eightsuperior'] = 0x2078;
+ t['eightthai'] = 0x0E58;
+ t['einvertedbreve'] = 0x0207;
+ t['eiotifiedcyrillic'] = 0x0465;
+ t['ekatakana'] = 0x30A8;
+ t['ekatakanahalfwidth'] = 0xFF74;
+ t['ekonkargurmukhi'] = 0x0A74;
+ t['ekorean'] = 0x3154;
+ t['elcyrillic'] = 0x043B;
+ t['element'] = 0x2208;
+ t['elevencircle'] = 0x246A;
+ t['elevenparen'] = 0x247E;
+ t['elevenperiod'] = 0x2492;
+ t['elevenroman'] = 0x217A;
+ t['ellipsis'] = 0x2026;
+ t['ellipsisvertical'] = 0x22EE;
+ t['emacron'] = 0x0113;
+ t['emacronacute'] = 0x1E17;
+ t['emacrongrave'] = 0x1E15;
+ t['emcyrillic'] = 0x043C;
+ t['emdash'] = 0x2014;
+ t['emdashvertical'] = 0xFE31;
+ t['emonospace'] = 0xFF45;
+ t['emphasismarkarmenian'] = 0x055B;
+ t['emptyset'] = 0x2205;
+ t['enbopomofo'] = 0x3123;
+ t['encyrillic'] = 0x043D;
+ t['endash'] = 0x2013;
+ t['endashvertical'] = 0xFE32;
+ t['endescendercyrillic'] = 0x04A3;
+ t['eng'] = 0x014B;
+ t['engbopomofo'] = 0x3125;
+ t['enghecyrillic'] = 0x04A5;
+ t['enhookcyrillic'] = 0x04C8;
+ t['enspace'] = 0x2002;
+ t['eogonek'] = 0x0119;
+ t['eokorean'] = 0x3153;
+ t['eopen'] = 0x025B;
+ t['eopenclosed'] = 0x029A;
+ t['eopenreversed'] = 0x025C;
+ t['eopenreversedclosed'] = 0x025E;
+ t['eopenreversedhook'] = 0x025D;
+ t['eparen'] = 0x24A0;
+ t['epsilon'] = 0x03B5;
+ t['epsilontonos'] = 0x03AD;
+ t['equal'] = 0x003D;
+ t['equalmonospace'] = 0xFF1D;
+ t['equalsmall'] = 0xFE66;
+ t['equalsuperior'] = 0x207C;
+ t['equivalence'] = 0x2261;
+ t['erbopomofo'] = 0x3126;
+ t['ercyrillic'] = 0x0440;
+ t['ereversed'] = 0x0258;
+ t['ereversedcyrillic'] = 0x044D;
+ t['escyrillic'] = 0x0441;
+ t['esdescendercyrillic'] = 0x04AB;
+ t['esh'] = 0x0283;
+ t['eshcurl'] = 0x0286;
+ t['eshortdeva'] = 0x090E;
+ t['eshortvowelsigndeva'] = 0x0946;
+ t['eshreversedloop'] = 0x01AA;
+ t['eshsquatreversed'] = 0x0285;
+ t['esmallhiragana'] = 0x3047;
+ t['esmallkatakana'] = 0x30A7;
+ t['esmallkatakanahalfwidth'] = 0xFF6A;
+ t['estimated'] = 0x212E;
+ t['esuperior'] = 0xF6EC;
+ t['eta'] = 0x03B7;
+ t['etarmenian'] = 0x0568;
+ t['etatonos'] = 0x03AE;
+ t['eth'] = 0x00F0;
+ t['etilde'] = 0x1EBD;
+ t['etildebelow'] = 0x1E1B;
+ t['etnahtafoukhhebrew'] = 0x0591;
+ t['etnahtafoukhlefthebrew'] = 0x0591;
+ t['etnahtahebrew'] = 0x0591;
+ t['etnahtalefthebrew'] = 0x0591;
+ t['eturned'] = 0x01DD;
+ t['eukorean'] = 0x3161;
+ t['euro'] = 0x20AC;
+ t['evowelsignbengali'] = 0x09C7;
+ t['evowelsigndeva'] = 0x0947;
+ t['evowelsigngujarati'] = 0x0AC7;
+ t['exclam'] = 0x0021;
+ t['exclamarmenian'] = 0x055C;
+ t['exclamdbl'] = 0x203C;
+ t['exclamdown'] = 0x00A1;
+ t['exclamdownsmall'] = 0xF7A1;
+ t['exclammonospace'] = 0xFF01;
+ t['exclamsmall'] = 0xF721;
+ t['existential'] = 0x2203;
+ t['ezh'] = 0x0292;
+ t['ezhcaron'] = 0x01EF;
+ t['ezhcurl'] = 0x0293;
+ t['ezhreversed'] = 0x01B9;
+ t['ezhtail'] = 0x01BA;
+ t['f'] = 0x0066;
+ t['fadeva'] = 0x095E;
+ t['fagurmukhi'] = 0x0A5E;
+ t['fahrenheit'] = 0x2109;
+ t['fathaarabic'] = 0x064E;
+ t['fathalowarabic'] = 0x064E;
+ t['fathatanarabic'] = 0x064B;
+ t['fbopomofo'] = 0x3108;
+ t['fcircle'] = 0x24D5;
+ t['fdotaccent'] = 0x1E1F;
+ t['feharabic'] = 0x0641;
+ t['feharmenian'] = 0x0586;
+ t['fehfinalarabic'] = 0xFED2;
+ t['fehinitialarabic'] = 0xFED3;
+ t['fehmedialarabic'] = 0xFED4;
+ t['feicoptic'] = 0x03E5;
+ t['female'] = 0x2640;
+ t['ff'] = 0xFB00;
+ t['ffi'] = 0xFB03;
+ t['ffl'] = 0xFB04;
+ t['fi'] = 0xFB01;
+ t['fifteencircle'] = 0x246E;
+ t['fifteenparen'] = 0x2482;
+ t['fifteenperiod'] = 0x2496;
+ t['figuredash'] = 0x2012;
+ t['filledbox'] = 0x25A0;
+ t['filledrect'] = 0x25AC;
+ t['finalkaf'] = 0x05DA;
+ t['finalkafdagesh'] = 0xFB3A;
+ t['finalkafdageshhebrew'] = 0xFB3A;
+ t['finalkafhebrew'] = 0x05DA;
+ t['finalmem'] = 0x05DD;
+ t['finalmemhebrew'] = 0x05DD;
+ t['finalnun'] = 0x05DF;
+ t['finalnunhebrew'] = 0x05DF;
+ t['finalpe'] = 0x05E3;
+ t['finalpehebrew'] = 0x05E3;
+ t['finaltsadi'] = 0x05E5;
+ t['finaltsadihebrew'] = 0x05E5;
+ t['firsttonechinese'] = 0x02C9;
+ t['fisheye'] = 0x25C9;
+ t['fitacyrillic'] = 0x0473;
+ t['five'] = 0x0035;
+ t['fivearabic'] = 0x0665;
+ t['fivebengali'] = 0x09EB;
+ t['fivecircle'] = 0x2464;
+ t['fivecircleinversesansserif'] = 0x278E;
+ t['fivedeva'] = 0x096B;
+ t['fiveeighths'] = 0x215D;
+ t['fivegujarati'] = 0x0AEB;
+ t['fivegurmukhi'] = 0x0A6B;
+ t['fivehackarabic'] = 0x0665;
+ t['fivehangzhou'] = 0x3025;
+ t['fiveideographicparen'] = 0x3224;
+ t['fiveinferior'] = 0x2085;
+ t['fivemonospace'] = 0xFF15;
+ t['fiveoldstyle'] = 0xF735;
+ t['fiveparen'] = 0x2478;
+ t['fiveperiod'] = 0x248C;
+ t['fivepersian'] = 0x06F5;
+ t['fiveroman'] = 0x2174;
+ t['fivesuperior'] = 0x2075;
+ t['fivethai'] = 0x0E55;
+ t['fl'] = 0xFB02;
+ t['florin'] = 0x0192;
+ t['fmonospace'] = 0xFF46;
+ t['fmsquare'] = 0x3399;
+ t['fofanthai'] = 0x0E1F;
+ t['fofathai'] = 0x0E1D;
+ t['fongmanthai'] = 0x0E4F;
+ t['forall'] = 0x2200;
+ t['four'] = 0x0034;
+ t['fourarabic'] = 0x0664;
+ t['fourbengali'] = 0x09EA;
+ t['fourcircle'] = 0x2463;
+ t['fourcircleinversesansserif'] = 0x278D;
+ t['fourdeva'] = 0x096A;
+ t['fourgujarati'] = 0x0AEA;
+ t['fourgurmukhi'] = 0x0A6A;
+ t['fourhackarabic'] = 0x0664;
+ t['fourhangzhou'] = 0x3024;
+ t['fourideographicparen'] = 0x3223;
+ t['fourinferior'] = 0x2084;
+ t['fourmonospace'] = 0xFF14;
+ t['fournumeratorbengali'] = 0x09F7;
+ t['fouroldstyle'] = 0xF734;
+ t['fourparen'] = 0x2477;
+ t['fourperiod'] = 0x248B;
+ t['fourpersian'] = 0x06F4;
+ t['fourroman'] = 0x2173;
+ t['foursuperior'] = 0x2074;
+ t['fourteencircle'] = 0x246D;
+ t['fourteenparen'] = 0x2481;
+ t['fourteenperiod'] = 0x2495;
+ t['fourthai'] = 0x0E54;
+ t['fourthtonechinese'] = 0x02CB;
+ t['fparen'] = 0x24A1;
+ t['fraction'] = 0x2044;
+ t['franc'] = 0x20A3;
+ t['g'] = 0x0067;
+ t['gabengali'] = 0x0997;
+ t['gacute'] = 0x01F5;
+ t['gadeva'] = 0x0917;
+ t['gafarabic'] = 0x06AF;
+ t['gaffinalarabic'] = 0xFB93;
+ t['gafinitialarabic'] = 0xFB94;
+ t['gafmedialarabic'] = 0xFB95;
+ t['gagujarati'] = 0x0A97;
+ t['gagurmukhi'] = 0x0A17;
+ t['gahiragana'] = 0x304C;
+ t['gakatakana'] = 0x30AC;
+ t['gamma'] = 0x03B3;
+ t['gammalatinsmall'] = 0x0263;
+ t['gammasuperior'] = 0x02E0;
+ t['gangiacoptic'] = 0x03EB;
+ t['gbopomofo'] = 0x310D;
+ t['gbreve'] = 0x011F;
+ t['gcaron'] = 0x01E7;
+ t['gcedilla'] = 0x0123;
+ t['gcircle'] = 0x24D6;
+ t['gcircumflex'] = 0x011D;
+ t['gcommaaccent'] = 0x0123;
+ t['gdot'] = 0x0121;
+ t['gdotaccent'] = 0x0121;
+ t['gecyrillic'] = 0x0433;
+ t['gehiragana'] = 0x3052;
+ t['gekatakana'] = 0x30B2;
+ t['geometricallyequal'] = 0x2251;
+ t['gereshaccenthebrew'] = 0x059C;
+ t['gereshhebrew'] = 0x05F3;
+ t['gereshmuqdamhebrew'] = 0x059D;
+ t['germandbls'] = 0x00DF;
+ t['gershayimaccenthebrew'] = 0x059E;
+ t['gershayimhebrew'] = 0x05F4;
+ t['getamark'] = 0x3013;
+ t['ghabengali'] = 0x0998;
+ t['ghadarmenian'] = 0x0572;
+ t['ghadeva'] = 0x0918;
+ t['ghagujarati'] = 0x0A98;
+ t['ghagurmukhi'] = 0x0A18;
+ t['ghainarabic'] = 0x063A;
+ t['ghainfinalarabic'] = 0xFECE;
+ t['ghaininitialarabic'] = 0xFECF;
+ t['ghainmedialarabic'] = 0xFED0;
+ t['ghemiddlehookcyrillic'] = 0x0495;
+ t['ghestrokecyrillic'] = 0x0493;
+ t['gheupturncyrillic'] = 0x0491;
+ t['ghhadeva'] = 0x095A;
+ t['ghhagurmukhi'] = 0x0A5A;
+ t['ghook'] = 0x0260;
+ t['ghzsquare'] = 0x3393;
+ t['gihiragana'] = 0x304E;
+ t['gikatakana'] = 0x30AE;
+ t['gimarmenian'] = 0x0563;
+ t['gimel'] = 0x05D2;
+ t['gimeldagesh'] = 0xFB32;
+ t['gimeldageshhebrew'] = 0xFB32;
+ t['gimelhebrew'] = 0x05D2;
+ t['gjecyrillic'] = 0x0453;
+ t['glottalinvertedstroke'] = 0x01BE;
+ t['glottalstop'] = 0x0294;
+ t['glottalstopinverted'] = 0x0296;
+ t['glottalstopmod'] = 0x02C0;
+ t['glottalstopreversed'] = 0x0295;
+ t['glottalstopreversedmod'] = 0x02C1;
+ t['glottalstopreversedsuperior'] = 0x02E4;
+ t['glottalstopstroke'] = 0x02A1;
+ t['glottalstopstrokereversed'] = 0x02A2;
+ t['gmacron'] = 0x1E21;
+ t['gmonospace'] = 0xFF47;
+ t['gohiragana'] = 0x3054;
+ t['gokatakana'] = 0x30B4;
+ t['gparen'] = 0x24A2;
+ t['gpasquare'] = 0x33AC;
+ t['gradient'] = 0x2207;
+ t['grave'] = 0x0060;
+ t['gravebelowcmb'] = 0x0316;
+ t['gravecmb'] = 0x0300;
+ t['gravecomb'] = 0x0300;
+ t['gravedeva'] = 0x0953;
+ t['gravelowmod'] = 0x02CE;
+ t['gravemonospace'] = 0xFF40;
+ t['gravetonecmb'] = 0x0340;
+ t['greater'] = 0x003E;
+ t['greaterequal'] = 0x2265;
+ t['greaterequalorless'] = 0x22DB;
+ t['greatermonospace'] = 0xFF1E;
+ t['greaterorequivalent'] = 0x2273;
+ t['greaterorless'] = 0x2277;
+ t['greateroverequal'] = 0x2267;
+ t['greatersmall'] = 0xFE65;
+ t['gscript'] = 0x0261;
+ t['gstroke'] = 0x01E5;
+ t['guhiragana'] = 0x3050;
+ t['guillemotleft'] = 0x00AB;
+ t['guillemotright'] = 0x00BB;
+ t['guilsinglleft'] = 0x2039;
+ t['guilsinglright'] = 0x203A;
+ t['gukatakana'] = 0x30B0;
+ t['guramusquare'] = 0x3318;
+ t['gysquare'] = 0x33C9;
+ t['h'] = 0x0068;
+ t['haabkhasiancyrillic'] = 0x04A9;
+ t['haaltonearabic'] = 0x06C1;
+ t['habengali'] = 0x09B9;
+ t['hadescendercyrillic'] = 0x04B3;
+ t['hadeva'] = 0x0939;
+ t['hagujarati'] = 0x0AB9;
+ t['hagurmukhi'] = 0x0A39;
+ t['haharabic'] = 0x062D;
+ t['hahfinalarabic'] = 0xFEA2;
+ t['hahinitialarabic'] = 0xFEA3;
+ t['hahiragana'] = 0x306F;
+ t['hahmedialarabic'] = 0xFEA4;
+ t['haitusquare'] = 0x332A;
+ t['hakatakana'] = 0x30CF;
+ t['hakatakanahalfwidth'] = 0xFF8A;
+ t['halantgurmukhi'] = 0x0A4D;
+ t['hamzaarabic'] = 0x0621;
+ t['hamzalowarabic'] = 0x0621;
+ t['hangulfiller'] = 0x3164;
+ t['hardsigncyrillic'] = 0x044A;
+ t['harpoonleftbarbup'] = 0x21BC;
+ t['harpoonrightbarbup'] = 0x21C0;
+ t['hasquare'] = 0x33CA;
+ t['hatafpatah'] = 0x05B2;
+ t['hatafpatah16'] = 0x05B2;
+ t['hatafpatah23'] = 0x05B2;
+ t['hatafpatah2f'] = 0x05B2;
+ t['hatafpatahhebrew'] = 0x05B2;
+ t['hatafpatahnarrowhebrew'] = 0x05B2;
+ t['hatafpatahquarterhebrew'] = 0x05B2;
+ t['hatafpatahwidehebrew'] = 0x05B2;
+ t['hatafqamats'] = 0x05B3;
+ t['hatafqamats1b'] = 0x05B3;
+ t['hatafqamats28'] = 0x05B3;
+ t['hatafqamats34'] = 0x05B3;
+ t['hatafqamatshebrew'] = 0x05B3;
+ t['hatafqamatsnarrowhebrew'] = 0x05B3;
+ t['hatafqamatsquarterhebrew'] = 0x05B3;
+ t['hatafqamatswidehebrew'] = 0x05B3;
+ t['hatafsegol'] = 0x05B1;
+ t['hatafsegol17'] = 0x05B1;
+ t['hatafsegol24'] = 0x05B1;
+ t['hatafsegol30'] = 0x05B1;
+ t['hatafsegolhebrew'] = 0x05B1;
+ t['hatafsegolnarrowhebrew'] = 0x05B1;
+ t['hatafsegolquarterhebrew'] = 0x05B1;
+ t['hatafsegolwidehebrew'] = 0x05B1;
+ t['hbar'] = 0x0127;
+ t['hbopomofo'] = 0x310F;
+ t['hbrevebelow'] = 0x1E2B;
+ t['hcedilla'] = 0x1E29;
+ t['hcircle'] = 0x24D7;
+ t['hcircumflex'] = 0x0125;
+ t['hdieresis'] = 0x1E27;
+ t['hdotaccent'] = 0x1E23;
+ t['hdotbelow'] = 0x1E25;
+ t['he'] = 0x05D4;
+ t['heart'] = 0x2665;
+ t['heartsuitblack'] = 0x2665;
+ t['heartsuitwhite'] = 0x2661;
+ t['hedagesh'] = 0xFB34;
+ t['hedageshhebrew'] = 0xFB34;
+ t['hehaltonearabic'] = 0x06C1;
+ t['heharabic'] = 0x0647;
+ t['hehebrew'] = 0x05D4;
+ t['hehfinalaltonearabic'] = 0xFBA7;
+ t['hehfinalalttwoarabic'] = 0xFEEA;
+ t['hehfinalarabic'] = 0xFEEA;
+ t['hehhamzaabovefinalarabic'] = 0xFBA5;
+ t['hehhamzaaboveisolatedarabic'] = 0xFBA4;
+ t['hehinitialaltonearabic'] = 0xFBA8;
+ t['hehinitialarabic'] = 0xFEEB;
+ t['hehiragana'] = 0x3078;
+ t['hehmedialaltonearabic'] = 0xFBA9;
+ t['hehmedialarabic'] = 0xFEEC;
+ t['heiseierasquare'] = 0x337B;
+ t['hekatakana'] = 0x30D8;
+ t['hekatakanahalfwidth'] = 0xFF8D;
+ t['hekutaarusquare'] = 0x3336;
+ t['henghook'] = 0x0267;
+ t['herutusquare'] = 0x3339;
+ t['het'] = 0x05D7;
+ t['hethebrew'] = 0x05D7;
+ t['hhook'] = 0x0266;
+ t['hhooksuperior'] = 0x02B1;
+ t['hieuhacirclekorean'] = 0x327B;
+ t['hieuhaparenkorean'] = 0x321B;
+ t['hieuhcirclekorean'] = 0x326D;
+ t['hieuhkorean'] = 0x314E;
+ t['hieuhparenkorean'] = 0x320D;
+ t['hihiragana'] = 0x3072;
+ t['hikatakana'] = 0x30D2;
+ t['hikatakanahalfwidth'] = 0xFF8B;
+ t['hiriq'] = 0x05B4;
+ t['hiriq14'] = 0x05B4;
+ t['hiriq21'] = 0x05B4;
+ t['hiriq2d'] = 0x05B4;
+ t['hiriqhebrew'] = 0x05B4;
+ t['hiriqnarrowhebrew'] = 0x05B4;
+ t['hiriqquarterhebrew'] = 0x05B4;
+ t['hiriqwidehebrew'] = 0x05B4;
+ t['hlinebelow'] = 0x1E96;
+ t['hmonospace'] = 0xFF48;
+ t['hoarmenian'] = 0x0570;
+ t['hohipthai'] = 0x0E2B;
+ t['hohiragana'] = 0x307B;
+ t['hokatakana'] = 0x30DB;
+ t['hokatakanahalfwidth'] = 0xFF8E;
+ t['holam'] = 0x05B9;
+ t['holam19'] = 0x05B9;
+ t['holam26'] = 0x05B9;
+ t['holam32'] = 0x05B9;
+ t['holamhebrew'] = 0x05B9;
+ t['holamnarrowhebrew'] = 0x05B9;
+ t['holamquarterhebrew'] = 0x05B9;
+ t['holamwidehebrew'] = 0x05B9;
+ t['honokhukthai'] = 0x0E2E;
+ t['hookabovecomb'] = 0x0309;
+ t['hookcmb'] = 0x0309;
+ t['hookpalatalizedbelowcmb'] = 0x0321;
+ t['hookretroflexbelowcmb'] = 0x0322;
+ t['hoonsquare'] = 0x3342;
+ t['horicoptic'] = 0x03E9;
+ t['horizontalbar'] = 0x2015;
+ t['horncmb'] = 0x031B;
+ t['hotsprings'] = 0x2668;
+ t['house'] = 0x2302;
+ t['hparen'] = 0x24A3;
+ t['hsuperior'] = 0x02B0;
+ t['hturned'] = 0x0265;
+ t['huhiragana'] = 0x3075;
+ t['huiitosquare'] = 0x3333;
+ t['hukatakana'] = 0x30D5;
+ t['hukatakanahalfwidth'] = 0xFF8C;
+ t['hungarumlaut'] = 0x02DD;
+ t['hungarumlautcmb'] = 0x030B;
+ t['hv'] = 0x0195;
+ t['hyphen'] = 0x002D;
+ t['hypheninferior'] = 0xF6E5;
+ t['hyphenmonospace'] = 0xFF0D;
+ t['hyphensmall'] = 0xFE63;
+ t['hyphensuperior'] = 0xF6E6;
+ t['hyphentwo'] = 0x2010;
+ t['i'] = 0x0069;
+ t['iacute'] = 0x00ED;
+ t['iacyrillic'] = 0x044F;
+ t['ibengali'] = 0x0987;
+ t['ibopomofo'] = 0x3127;
+ t['ibreve'] = 0x012D;
+ t['icaron'] = 0x01D0;
+ t['icircle'] = 0x24D8;
+ t['icircumflex'] = 0x00EE;
+ t['icyrillic'] = 0x0456;
+ t['idblgrave'] = 0x0209;
+ t['ideographearthcircle'] = 0x328F;
+ t['ideographfirecircle'] = 0x328B;
+ t['ideographicallianceparen'] = 0x323F;
+ t['ideographiccallparen'] = 0x323A;
+ t['ideographiccentrecircle'] = 0x32A5;
+ t['ideographicclose'] = 0x3006;
+ t['ideographiccomma'] = 0x3001;
+ t['ideographiccommaleft'] = 0xFF64;
+ t['ideographiccongratulationparen'] = 0x3237;
+ t['ideographiccorrectcircle'] = 0x32A3;
+ t['ideographicearthparen'] = 0x322F;
+ t['ideographicenterpriseparen'] = 0x323D;
+ t['ideographicexcellentcircle'] = 0x329D;
+ t['ideographicfestivalparen'] = 0x3240;
+ t['ideographicfinancialcircle'] = 0x3296;
+ t['ideographicfinancialparen'] = 0x3236;
+ t['ideographicfireparen'] = 0x322B;
+ t['ideographichaveparen'] = 0x3232;
+ t['ideographichighcircle'] = 0x32A4;
+ t['ideographiciterationmark'] = 0x3005;
+ t['ideographiclaborcircle'] = 0x3298;
+ t['ideographiclaborparen'] = 0x3238;
+ t['ideographicleftcircle'] = 0x32A7;
+ t['ideographiclowcircle'] = 0x32A6;
+ t['ideographicmedicinecircle'] = 0x32A9;
+ t['ideographicmetalparen'] = 0x322E;
+ t['ideographicmoonparen'] = 0x322A;
+ t['ideographicnameparen'] = 0x3234;
+ t['ideographicperiod'] = 0x3002;
+ t['ideographicprintcircle'] = 0x329E;
+ t['ideographicreachparen'] = 0x3243;
+ t['ideographicrepresentparen'] = 0x3239;
+ t['ideographicresourceparen'] = 0x323E;
+ t['ideographicrightcircle'] = 0x32A8;
+ t['ideographicsecretcircle'] = 0x3299;
+ t['ideographicselfparen'] = 0x3242;
+ t['ideographicsocietyparen'] = 0x3233;
+ t['ideographicspace'] = 0x3000;
+ t['ideographicspecialparen'] = 0x3235;
+ t['ideographicstockparen'] = 0x3231;
+ t['ideographicstudyparen'] = 0x323B;
+ t['ideographicsunparen'] = 0x3230;
+ t['ideographicsuperviseparen'] = 0x323C;
+ t['ideographicwaterparen'] = 0x322C;
+ t['ideographicwoodparen'] = 0x322D;
+ t['ideographiczero'] = 0x3007;
+ t['ideographmetalcircle'] = 0x328E;
+ t['ideographmooncircle'] = 0x328A;
+ t['ideographnamecircle'] = 0x3294;
+ t['ideographsuncircle'] = 0x3290;
+ t['ideographwatercircle'] = 0x328C;
+ t['ideographwoodcircle'] = 0x328D;
+ t['ideva'] = 0x0907;
+ t['idieresis'] = 0x00EF;
+ t['idieresisacute'] = 0x1E2F;
+ t['idieresiscyrillic'] = 0x04E5;
+ t['idotbelow'] = 0x1ECB;
+ t['iebrevecyrillic'] = 0x04D7;
+ t['iecyrillic'] = 0x0435;
+ t['ieungacirclekorean'] = 0x3275;
+ t['ieungaparenkorean'] = 0x3215;
+ t['ieungcirclekorean'] = 0x3267;
+ t['ieungkorean'] = 0x3147;
+ t['ieungparenkorean'] = 0x3207;
+ t['igrave'] = 0x00EC;
+ t['igujarati'] = 0x0A87;
+ t['igurmukhi'] = 0x0A07;
+ t['ihiragana'] = 0x3044;
+ t['ihookabove'] = 0x1EC9;
+ t['iibengali'] = 0x0988;
+ t['iicyrillic'] = 0x0438;
+ t['iideva'] = 0x0908;
+ t['iigujarati'] = 0x0A88;
+ t['iigurmukhi'] = 0x0A08;
+ t['iimatragurmukhi'] = 0x0A40;
+ t['iinvertedbreve'] = 0x020B;
+ t['iishortcyrillic'] = 0x0439;
+ t['iivowelsignbengali'] = 0x09C0;
+ t['iivowelsigndeva'] = 0x0940;
+ t['iivowelsigngujarati'] = 0x0AC0;
+ t['ij'] = 0x0133;
+ t['ikatakana'] = 0x30A4;
+ t['ikatakanahalfwidth'] = 0xFF72;
+ t['ikorean'] = 0x3163;
+ t['ilde'] = 0x02DC;
+ t['iluyhebrew'] = 0x05AC;
+ t['imacron'] = 0x012B;
+ t['imacroncyrillic'] = 0x04E3;
+ t['imageorapproximatelyequal'] = 0x2253;
+ t['imatragurmukhi'] = 0x0A3F;
+ t['imonospace'] = 0xFF49;
+ t['increment'] = 0x2206;
+ t['infinity'] = 0x221E;
+ t['iniarmenian'] = 0x056B;
+ t['integral'] = 0x222B;
+ t['integralbottom'] = 0x2321;
+ t['integralbt'] = 0x2321;
+ t['integralex'] = 0xF8F5;
+ t['integraltop'] = 0x2320;
+ t['integraltp'] = 0x2320;
+ t['intersection'] = 0x2229;
+ t['intisquare'] = 0x3305;
+ t['invbullet'] = 0x25D8;
+ t['invcircle'] = 0x25D9;
+ t['invsmileface'] = 0x263B;
+ t['iocyrillic'] = 0x0451;
+ t['iogonek'] = 0x012F;
+ t['iota'] = 0x03B9;
+ t['iotadieresis'] = 0x03CA;
+ t['iotadieresistonos'] = 0x0390;
+ t['iotalatin'] = 0x0269;
+ t['iotatonos'] = 0x03AF;
+ t['iparen'] = 0x24A4;
+ t['irigurmukhi'] = 0x0A72;
+ t['ismallhiragana'] = 0x3043;
+ t['ismallkatakana'] = 0x30A3;
+ t['ismallkatakanahalfwidth'] = 0xFF68;
+ t['issharbengali'] = 0x09FA;
+ t['istroke'] = 0x0268;
+ t['isuperior'] = 0xF6ED;
+ t['iterationhiragana'] = 0x309D;
+ t['iterationkatakana'] = 0x30FD;
+ t['itilde'] = 0x0129;
+ t['itildebelow'] = 0x1E2D;
+ t['iubopomofo'] = 0x3129;
+ t['iucyrillic'] = 0x044E;
+ t['ivowelsignbengali'] = 0x09BF;
+ t['ivowelsigndeva'] = 0x093F;
+ t['ivowelsigngujarati'] = 0x0ABF;
+ t['izhitsacyrillic'] = 0x0475;
+ t['izhitsadblgravecyrillic'] = 0x0477;
+ t['j'] = 0x006A;
+ t['jaarmenian'] = 0x0571;
+ t['jabengali'] = 0x099C;
+ t['jadeva'] = 0x091C;
+ t['jagujarati'] = 0x0A9C;
+ t['jagurmukhi'] = 0x0A1C;
+ t['jbopomofo'] = 0x3110;
+ t['jcaron'] = 0x01F0;
+ t['jcircle'] = 0x24D9;
+ t['jcircumflex'] = 0x0135;
+ t['jcrossedtail'] = 0x029D;
+ t['jdotlessstroke'] = 0x025F;
+ t['jecyrillic'] = 0x0458;
+ t['jeemarabic'] = 0x062C;
+ t['jeemfinalarabic'] = 0xFE9E;
+ t['jeeminitialarabic'] = 0xFE9F;
+ t['jeemmedialarabic'] = 0xFEA0;
+ t['jeharabic'] = 0x0698;
+ t['jehfinalarabic'] = 0xFB8B;
+ t['jhabengali'] = 0x099D;
+ t['jhadeva'] = 0x091D;
+ t['jhagujarati'] = 0x0A9D;
+ t['jhagurmukhi'] = 0x0A1D;
+ t['jheharmenian'] = 0x057B;
+ t['jis'] = 0x3004;
+ t['jmonospace'] = 0xFF4A;
+ t['jparen'] = 0x24A5;
+ t['jsuperior'] = 0x02B2;
+ t['k'] = 0x006B;
+ t['kabashkircyrillic'] = 0x04A1;
+ t['kabengali'] = 0x0995;
+ t['kacute'] = 0x1E31;
+ t['kacyrillic'] = 0x043A;
+ t['kadescendercyrillic'] = 0x049B;
+ t['kadeva'] = 0x0915;
+ t['kaf'] = 0x05DB;
+ t['kafarabic'] = 0x0643;
+ t['kafdagesh'] = 0xFB3B;
+ t['kafdageshhebrew'] = 0xFB3B;
+ t['kaffinalarabic'] = 0xFEDA;
+ t['kafhebrew'] = 0x05DB;
+ t['kafinitialarabic'] = 0xFEDB;
+ t['kafmedialarabic'] = 0xFEDC;
+ t['kafrafehebrew'] = 0xFB4D;
+ t['kagujarati'] = 0x0A95;
+ t['kagurmukhi'] = 0x0A15;
+ t['kahiragana'] = 0x304B;
+ t['kahookcyrillic'] = 0x04C4;
+ t['kakatakana'] = 0x30AB;
+ t['kakatakanahalfwidth'] = 0xFF76;
+ t['kappa'] = 0x03BA;
+ t['kappasymbolgreek'] = 0x03F0;
+ t['kapyeounmieumkorean'] = 0x3171;
+ t['kapyeounphieuphkorean'] = 0x3184;
+ t['kapyeounpieupkorean'] = 0x3178;
+ t['kapyeounssangpieupkorean'] = 0x3179;
+ t['karoriisquare'] = 0x330D;
+ t['kashidaautoarabic'] = 0x0640;
+ t['kashidaautonosidebearingarabic'] = 0x0640;
+ t['kasmallkatakana'] = 0x30F5;
+ t['kasquare'] = 0x3384;
+ t['kasraarabic'] = 0x0650;
+ t['kasratanarabic'] = 0x064D;
+ t['kastrokecyrillic'] = 0x049F;
+ t['katahiraprolongmarkhalfwidth'] = 0xFF70;
+ t['kaverticalstrokecyrillic'] = 0x049D;
+ t['kbopomofo'] = 0x310E;
+ t['kcalsquare'] = 0x3389;
+ t['kcaron'] = 0x01E9;
+ t['kcedilla'] = 0x0137;
+ t['kcircle'] = 0x24DA;
+ t['kcommaaccent'] = 0x0137;
+ t['kdotbelow'] = 0x1E33;
+ t['keharmenian'] = 0x0584;
+ t['kehiragana'] = 0x3051;
+ t['kekatakana'] = 0x30B1;
+ t['kekatakanahalfwidth'] = 0xFF79;
+ t['kenarmenian'] = 0x056F;
+ t['kesmallkatakana'] = 0x30F6;
+ t['kgreenlandic'] = 0x0138;
+ t['khabengali'] = 0x0996;
+ t['khacyrillic'] = 0x0445;
+ t['khadeva'] = 0x0916;
+ t['khagujarati'] = 0x0A96;
+ t['khagurmukhi'] = 0x0A16;
+ t['khaharabic'] = 0x062E;
+ t['khahfinalarabic'] = 0xFEA6;
+ t['khahinitialarabic'] = 0xFEA7;
+ t['khahmedialarabic'] = 0xFEA8;
+ t['kheicoptic'] = 0x03E7;
+ t['khhadeva'] = 0x0959;
+ t['khhagurmukhi'] = 0x0A59;
+ t['khieukhacirclekorean'] = 0x3278;
+ t['khieukhaparenkorean'] = 0x3218;
+ t['khieukhcirclekorean'] = 0x326A;
+ t['khieukhkorean'] = 0x314B;
+ t['khieukhparenkorean'] = 0x320A;
+ t['khokhaithai'] = 0x0E02;
+ t['khokhonthai'] = 0x0E05;
+ t['khokhuatthai'] = 0x0E03;
+ t['khokhwaithai'] = 0x0E04;
+ t['khomutthai'] = 0x0E5B;
+ t['khook'] = 0x0199;
+ t['khorakhangthai'] = 0x0E06;
+ t['khzsquare'] = 0x3391;
+ t['kihiragana'] = 0x304D;
+ t['kikatakana'] = 0x30AD;
+ t['kikatakanahalfwidth'] = 0xFF77;
+ t['kiroguramusquare'] = 0x3315;
+ t['kiromeetorusquare'] = 0x3316;
+ t['kirosquare'] = 0x3314;
+ t['kiyeokacirclekorean'] = 0x326E;
+ t['kiyeokaparenkorean'] = 0x320E;
+ t['kiyeokcirclekorean'] = 0x3260;
+ t['kiyeokkorean'] = 0x3131;
+ t['kiyeokparenkorean'] = 0x3200;
+ t['kiyeoksioskorean'] = 0x3133;
+ t['kjecyrillic'] = 0x045C;
+ t['klinebelow'] = 0x1E35;
+ t['klsquare'] = 0x3398;
+ t['kmcubedsquare'] = 0x33A6;
+ t['kmonospace'] = 0xFF4B;
+ t['kmsquaredsquare'] = 0x33A2;
+ t['kohiragana'] = 0x3053;
+ t['kohmsquare'] = 0x33C0;
+ t['kokaithai'] = 0x0E01;
+ t['kokatakana'] = 0x30B3;
+ t['kokatakanahalfwidth'] = 0xFF7A;
+ t['kooposquare'] = 0x331E;
+ t['koppacyrillic'] = 0x0481;
+ t['koreanstandardsymbol'] = 0x327F;
+ t['koroniscmb'] = 0x0343;
+ t['kparen'] = 0x24A6;
+ t['kpasquare'] = 0x33AA;
+ t['ksicyrillic'] = 0x046F;
+ t['ktsquare'] = 0x33CF;
+ t['kturned'] = 0x029E;
+ t['kuhiragana'] = 0x304F;
+ t['kukatakana'] = 0x30AF;
+ t['kukatakanahalfwidth'] = 0xFF78;
+ t['kvsquare'] = 0x33B8;
+ t['kwsquare'] = 0x33BE;
+ t['l'] = 0x006C;
+ t['labengali'] = 0x09B2;
+ t['lacute'] = 0x013A;
+ t['ladeva'] = 0x0932;
+ t['lagujarati'] = 0x0AB2;
+ t['lagurmukhi'] = 0x0A32;
+ t['lakkhangyaothai'] = 0x0E45;
+ t['lamaleffinalarabic'] = 0xFEFC;
+ t['lamalefhamzaabovefinalarabic'] = 0xFEF8;
+ t['lamalefhamzaaboveisolatedarabic'] = 0xFEF7;
+ t['lamalefhamzabelowfinalarabic'] = 0xFEFA;
+ t['lamalefhamzabelowisolatedarabic'] = 0xFEF9;
+ t['lamalefisolatedarabic'] = 0xFEFB;
+ t['lamalefmaddaabovefinalarabic'] = 0xFEF6;
+ t['lamalefmaddaaboveisolatedarabic'] = 0xFEF5;
+ t['lamarabic'] = 0x0644;
+ t['lambda'] = 0x03BB;
+ t['lambdastroke'] = 0x019B;
+ t['lamed'] = 0x05DC;
+ t['lameddagesh'] = 0xFB3C;
+ t['lameddageshhebrew'] = 0xFB3C;
+ t['lamedhebrew'] = 0x05DC;
+ t['lamfinalarabic'] = 0xFEDE;
+ t['lamhahinitialarabic'] = 0xFCCA;
+ t['laminitialarabic'] = 0xFEDF;
+ t['lamjeeminitialarabic'] = 0xFCC9;
+ t['lamkhahinitialarabic'] = 0xFCCB;
+ t['lamlamhehisolatedarabic'] = 0xFDF2;
+ t['lammedialarabic'] = 0xFEE0;
+ t['lammeemhahinitialarabic'] = 0xFD88;
+ t['lammeeminitialarabic'] = 0xFCCC;
+ t['largecircle'] = 0x25EF;
+ t['lbar'] = 0x019A;
+ t['lbelt'] = 0x026C;
+ t['lbopomofo'] = 0x310C;
+ t['lcaron'] = 0x013E;
+ t['lcedilla'] = 0x013C;
+ t['lcircle'] = 0x24DB;
+ t['lcircumflexbelow'] = 0x1E3D;
+ t['lcommaaccent'] = 0x013C;
+ t['ldot'] = 0x0140;
+ t['ldotaccent'] = 0x0140;
+ t['ldotbelow'] = 0x1E37;
+ t['ldotbelowmacron'] = 0x1E39;
+ t['leftangleabovecmb'] = 0x031A;
+ t['lefttackbelowcmb'] = 0x0318;
+ t['less'] = 0x003C;
+ t['lessequal'] = 0x2264;
+ t['lessequalorgreater'] = 0x22DA;
+ t['lessmonospace'] = 0xFF1C;
+ t['lessorequivalent'] = 0x2272;
+ t['lessorgreater'] = 0x2276;
+ t['lessoverequal'] = 0x2266;
+ t['lesssmall'] = 0xFE64;
+ t['lezh'] = 0x026E;
+ t['lfblock'] = 0x258C;
+ t['lhookretroflex'] = 0x026D;
+ t['lira'] = 0x20A4;
+ t['liwnarmenian'] = 0x056C;
+ t['lj'] = 0x01C9;
+ t['ljecyrillic'] = 0x0459;
+ t['ll'] = 0xF6C0;
+ t['lladeva'] = 0x0933;
+ t['llagujarati'] = 0x0AB3;
+ t['llinebelow'] = 0x1E3B;
+ t['llladeva'] = 0x0934;
+ t['llvocalicbengali'] = 0x09E1;
+ t['llvocalicdeva'] = 0x0961;
+ t['llvocalicvowelsignbengali'] = 0x09E3;
+ t['llvocalicvowelsigndeva'] = 0x0963;
+ t['lmiddletilde'] = 0x026B;
+ t['lmonospace'] = 0xFF4C;
+ t['lmsquare'] = 0x33D0;
+ t['lochulathai'] = 0x0E2C;
+ t['logicaland'] = 0x2227;
+ t['logicalnot'] = 0x00AC;
+ t['logicalnotreversed'] = 0x2310;
+ t['logicalor'] = 0x2228;
+ t['lolingthai'] = 0x0E25;
+ t['longs'] = 0x017F;
+ t['lowlinecenterline'] = 0xFE4E;
+ t['lowlinecmb'] = 0x0332;
+ t['lowlinedashed'] = 0xFE4D;
+ t['lozenge'] = 0x25CA;
+ t['lparen'] = 0x24A7;
+ t['lslash'] = 0x0142;
+ t['lsquare'] = 0x2113;
+ t['lsuperior'] = 0xF6EE;
+ t['ltshade'] = 0x2591;
+ t['luthai'] = 0x0E26;
+ t['lvocalicbengali'] = 0x098C;
+ t['lvocalicdeva'] = 0x090C;
+ t['lvocalicvowelsignbengali'] = 0x09E2;
+ t['lvocalicvowelsigndeva'] = 0x0962;
+ t['lxsquare'] = 0x33D3;
+ t['m'] = 0x006D;
+ t['mabengali'] = 0x09AE;
+ t['macron'] = 0x00AF;
+ t['macronbelowcmb'] = 0x0331;
+ t['macroncmb'] = 0x0304;
+ t['macronlowmod'] = 0x02CD;
+ t['macronmonospace'] = 0xFFE3;
+ t['macute'] = 0x1E3F;
+ t['madeva'] = 0x092E;
+ t['magujarati'] = 0x0AAE;
+ t['magurmukhi'] = 0x0A2E;
+ t['mahapakhhebrew'] = 0x05A4;
+ t['mahapakhlefthebrew'] = 0x05A4;
+ t['mahiragana'] = 0x307E;
+ t['maichattawalowleftthai'] = 0xF895;
+ t['maichattawalowrightthai'] = 0xF894;
+ t['maichattawathai'] = 0x0E4B;
+ t['maichattawaupperleftthai'] = 0xF893;
+ t['maieklowleftthai'] = 0xF88C;
+ t['maieklowrightthai'] = 0xF88B;
+ t['maiekthai'] = 0x0E48;
+ t['maiekupperleftthai'] = 0xF88A;
+ t['maihanakatleftthai'] = 0xF884;
+ t['maihanakatthai'] = 0x0E31;
+ t['maitaikhuleftthai'] = 0xF889;
+ t['maitaikhuthai'] = 0x0E47;
+ t['maitholowleftthai'] = 0xF88F;
+ t['maitholowrightthai'] = 0xF88E;
+ t['maithothai'] = 0x0E49;
+ t['maithoupperleftthai'] = 0xF88D;
+ t['maitrilowleftthai'] = 0xF892;
+ t['maitrilowrightthai'] = 0xF891;
+ t['maitrithai'] = 0x0E4A;
+ t['maitriupperleftthai'] = 0xF890;
+ t['maiyamokthai'] = 0x0E46;
+ t['makatakana'] = 0x30DE;
+ t['makatakanahalfwidth'] = 0xFF8F;
+ t['male'] = 0x2642;
+ t['mansyonsquare'] = 0x3347;
+ t['maqafhebrew'] = 0x05BE;
+ t['mars'] = 0x2642;
+ t['masoracirclehebrew'] = 0x05AF;
+ t['masquare'] = 0x3383;
+ t['mbopomofo'] = 0x3107;
+ t['mbsquare'] = 0x33D4;
+ t['mcircle'] = 0x24DC;
+ t['mcubedsquare'] = 0x33A5;
+ t['mdotaccent'] = 0x1E41;
+ t['mdotbelow'] = 0x1E43;
+ t['meemarabic'] = 0x0645;
+ t['meemfinalarabic'] = 0xFEE2;
+ t['meeminitialarabic'] = 0xFEE3;
+ t['meemmedialarabic'] = 0xFEE4;
+ t['meemmeeminitialarabic'] = 0xFCD1;
+ t['meemmeemisolatedarabic'] = 0xFC48;
+ t['meetorusquare'] = 0x334D;
+ t['mehiragana'] = 0x3081;
+ t['meizierasquare'] = 0x337E;
+ t['mekatakana'] = 0x30E1;
+ t['mekatakanahalfwidth'] = 0xFF92;
+ t['mem'] = 0x05DE;
+ t['memdagesh'] = 0xFB3E;
+ t['memdageshhebrew'] = 0xFB3E;
+ t['memhebrew'] = 0x05DE;
+ t['menarmenian'] = 0x0574;
+ t['merkhahebrew'] = 0x05A5;
+ t['merkhakefulahebrew'] = 0x05A6;
+ t['merkhakefulalefthebrew'] = 0x05A6;
+ t['merkhalefthebrew'] = 0x05A5;
+ t['mhook'] = 0x0271;
+ t['mhzsquare'] = 0x3392;
+ t['middledotkatakanahalfwidth'] = 0xFF65;
+ t['middot'] = 0x00B7;
+ t['mieumacirclekorean'] = 0x3272;
+ t['mieumaparenkorean'] = 0x3212;
+ t['mieumcirclekorean'] = 0x3264;
+ t['mieumkorean'] = 0x3141;
+ t['mieumpansioskorean'] = 0x3170;
+ t['mieumparenkorean'] = 0x3204;
+ t['mieumpieupkorean'] = 0x316E;
+ t['mieumsioskorean'] = 0x316F;
+ t['mihiragana'] = 0x307F;
+ t['mikatakana'] = 0x30DF;
+ t['mikatakanahalfwidth'] = 0xFF90;
+ t['minus'] = 0x2212;
+ t['minusbelowcmb'] = 0x0320;
+ t['minuscircle'] = 0x2296;
+ t['minusmod'] = 0x02D7;
+ t['minusplus'] = 0x2213;
+ t['minute'] = 0x2032;
+ t['miribaarusquare'] = 0x334A;
+ t['mirisquare'] = 0x3349;
+ t['mlonglegturned'] = 0x0270;
+ t['mlsquare'] = 0x3396;
+ t['mmcubedsquare'] = 0x33A3;
+ t['mmonospace'] = 0xFF4D;
+ t['mmsquaredsquare'] = 0x339F;
+ t['mohiragana'] = 0x3082;
+ t['mohmsquare'] = 0x33C1;
+ t['mokatakana'] = 0x30E2;
+ t['mokatakanahalfwidth'] = 0xFF93;
+ t['molsquare'] = 0x33D6;
+ t['momathai'] = 0x0E21;
+ t['moverssquare'] = 0x33A7;
+ t['moverssquaredsquare'] = 0x33A8;
+ t['mparen'] = 0x24A8;
+ t['mpasquare'] = 0x33AB;
+ t['mssquare'] = 0x33B3;
+ t['msuperior'] = 0xF6EF;
+ t['mturned'] = 0x026F;
+ t['mu'] = 0x00B5;
+ t['mu1'] = 0x00B5;
+ t['muasquare'] = 0x3382;
+ t['muchgreater'] = 0x226B;
+ t['muchless'] = 0x226A;
+ t['mufsquare'] = 0x338C;
+ t['mugreek'] = 0x03BC;
+ t['mugsquare'] = 0x338D;
+ t['muhiragana'] = 0x3080;
+ t['mukatakana'] = 0x30E0;
+ t['mukatakanahalfwidth'] = 0xFF91;
+ t['mulsquare'] = 0x3395;
+ t['multiply'] = 0x00D7;
+ t['mumsquare'] = 0x339B;
+ t['munahhebrew'] = 0x05A3;
+ t['munahlefthebrew'] = 0x05A3;
+ t['musicalnote'] = 0x266A;
+ t['musicalnotedbl'] = 0x266B;
+ t['musicflatsign'] = 0x266D;
+ t['musicsharpsign'] = 0x266F;
+ t['mussquare'] = 0x33B2;
+ t['muvsquare'] = 0x33B6;
+ t['muwsquare'] = 0x33BC;
+ t['mvmegasquare'] = 0x33B9;
+ t['mvsquare'] = 0x33B7;
+ t['mwmegasquare'] = 0x33BF;
+ t['mwsquare'] = 0x33BD;
+ t['n'] = 0x006E;
+ t['nabengali'] = 0x09A8;
+ t['nabla'] = 0x2207;
+ t['nacute'] = 0x0144;
+ t['nadeva'] = 0x0928;
+ t['nagujarati'] = 0x0AA8;
+ t['nagurmukhi'] = 0x0A28;
+ t['nahiragana'] = 0x306A;
+ t['nakatakana'] = 0x30CA;
+ t['nakatakanahalfwidth'] = 0xFF85;
+ t['napostrophe'] = 0x0149;
+ t['nasquare'] = 0x3381;
+ t['nbopomofo'] = 0x310B;
+ t['nbspace'] = 0x00A0;
+ t['ncaron'] = 0x0148;
+ t['ncedilla'] = 0x0146;
+ t['ncircle'] = 0x24DD;
+ t['ncircumflexbelow'] = 0x1E4B;
+ t['ncommaaccent'] = 0x0146;
+ t['ndotaccent'] = 0x1E45;
+ t['ndotbelow'] = 0x1E47;
+ t['nehiragana'] = 0x306D;
+ t['nekatakana'] = 0x30CD;
+ t['nekatakanahalfwidth'] = 0xFF88;
+ t['newsheqelsign'] = 0x20AA;
+ t['nfsquare'] = 0x338B;
+ t['ngabengali'] = 0x0999;
+ t['ngadeva'] = 0x0919;
+ t['ngagujarati'] = 0x0A99;
+ t['ngagurmukhi'] = 0x0A19;
+ t['ngonguthai'] = 0x0E07;
+ t['nhiragana'] = 0x3093;
+ t['nhookleft'] = 0x0272;
+ t['nhookretroflex'] = 0x0273;
+ t['nieunacirclekorean'] = 0x326F;
+ t['nieunaparenkorean'] = 0x320F;
+ t['nieuncieuckorean'] = 0x3135;
+ t['nieuncirclekorean'] = 0x3261;
+ t['nieunhieuhkorean'] = 0x3136;
+ t['nieunkorean'] = 0x3134;
+ t['nieunpansioskorean'] = 0x3168;
+ t['nieunparenkorean'] = 0x3201;
+ t['nieunsioskorean'] = 0x3167;
+ t['nieuntikeutkorean'] = 0x3166;
+ t['nihiragana'] = 0x306B;
+ t['nikatakana'] = 0x30CB;
+ t['nikatakanahalfwidth'] = 0xFF86;
+ t['nikhahitleftthai'] = 0xF899;
+ t['nikhahitthai'] = 0x0E4D;
+ t['nine'] = 0x0039;
+ t['ninearabic'] = 0x0669;
+ t['ninebengali'] = 0x09EF;
+ t['ninecircle'] = 0x2468;
+ t['ninecircleinversesansserif'] = 0x2792;
+ t['ninedeva'] = 0x096F;
+ t['ninegujarati'] = 0x0AEF;
+ t['ninegurmukhi'] = 0x0A6F;
+ t['ninehackarabic'] = 0x0669;
+ t['ninehangzhou'] = 0x3029;
+ t['nineideographicparen'] = 0x3228;
+ t['nineinferior'] = 0x2089;
+ t['ninemonospace'] = 0xFF19;
+ t['nineoldstyle'] = 0xF739;
+ t['nineparen'] = 0x247C;
+ t['nineperiod'] = 0x2490;
+ t['ninepersian'] = 0x06F9;
+ t['nineroman'] = 0x2178;
+ t['ninesuperior'] = 0x2079;
+ t['nineteencircle'] = 0x2472;
+ t['nineteenparen'] = 0x2486;
+ t['nineteenperiod'] = 0x249A;
+ t['ninethai'] = 0x0E59;
+ t['nj'] = 0x01CC;
+ t['njecyrillic'] = 0x045A;
+ t['nkatakana'] = 0x30F3;
+ t['nkatakanahalfwidth'] = 0xFF9D;
+ t['nlegrightlong'] = 0x019E;
+ t['nlinebelow'] = 0x1E49;
+ t['nmonospace'] = 0xFF4E;
+ t['nmsquare'] = 0x339A;
+ t['nnabengali'] = 0x09A3;
+ t['nnadeva'] = 0x0923;
+ t['nnagujarati'] = 0x0AA3;
+ t['nnagurmukhi'] = 0x0A23;
+ t['nnnadeva'] = 0x0929;
+ t['nohiragana'] = 0x306E;
+ t['nokatakana'] = 0x30CE;
+ t['nokatakanahalfwidth'] = 0xFF89;
+ t['nonbreakingspace'] = 0x00A0;
+ t['nonenthai'] = 0x0E13;
+ t['nonuthai'] = 0x0E19;
+ t['noonarabic'] = 0x0646;
+ t['noonfinalarabic'] = 0xFEE6;
+ t['noonghunnaarabic'] = 0x06BA;
+ t['noonghunnafinalarabic'] = 0xFB9F;
+ t['nooninitialarabic'] = 0xFEE7;
+ t['noonjeeminitialarabic'] = 0xFCD2;
+ t['noonjeemisolatedarabic'] = 0xFC4B;
+ t['noonmedialarabic'] = 0xFEE8;
+ t['noonmeeminitialarabic'] = 0xFCD5;
+ t['noonmeemisolatedarabic'] = 0xFC4E;
+ t['noonnoonfinalarabic'] = 0xFC8D;
+ t['notcontains'] = 0x220C;
+ t['notelement'] = 0x2209;
+ t['notelementof'] = 0x2209;
+ t['notequal'] = 0x2260;
+ t['notgreater'] = 0x226F;
+ t['notgreaternorequal'] = 0x2271;
+ t['notgreaternorless'] = 0x2279;
+ t['notidentical'] = 0x2262;
+ t['notless'] = 0x226E;
+ t['notlessnorequal'] = 0x2270;
+ t['notparallel'] = 0x2226;
+ t['notprecedes'] = 0x2280;
+ t['notsubset'] = 0x2284;
+ t['notsucceeds'] = 0x2281;
+ t['notsuperset'] = 0x2285;
+ t['nowarmenian'] = 0x0576;
+ t['nparen'] = 0x24A9;
+ t['nssquare'] = 0x33B1;
+ t['nsuperior'] = 0x207F;
+ t['ntilde'] = 0x00F1;
+ t['nu'] = 0x03BD;
+ t['nuhiragana'] = 0x306C;
+ t['nukatakana'] = 0x30CC;
+ t['nukatakanahalfwidth'] = 0xFF87;
+ t['nuktabengali'] = 0x09BC;
+ t['nuktadeva'] = 0x093C;
+ t['nuktagujarati'] = 0x0ABC;
+ t['nuktagurmukhi'] = 0x0A3C;
+ t['numbersign'] = 0x0023;
+ t['numbersignmonospace'] = 0xFF03;
+ t['numbersignsmall'] = 0xFE5F;
+ t['numeralsigngreek'] = 0x0374;
+ t['numeralsignlowergreek'] = 0x0375;
+ t['numero'] = 0x2116;
+ t['nun'] = 0x05E0;
+ t['nundagesh'] = 0xFB40;
+ t['nundageshhebrew'] = 0xFB40;
+ t['nunhebrew'] = 0x05E0;
+ t['nvsquare'] = 0x33B5;
+ t['nwsquare'] = 0x33BB;
+ t['nyabengali'] = 0x099E;
+ t['nyadeva'] = 0x091E;
+ t['nyagujarati'] = 0x0A9E;
+ t['nyagurmukhi'] = 0x0A1E;
+ t['o'] = 0x006F;
+ t['oacute'] = 0x00F3;
+ t['oangthai'] = 0x0E2D;
+ t['obarred'] = 0x0275;
+ t['obarredcyrillic'] = 0x04E9;
+ t['obarreddieresiscyrillic'] = 0x04EB;
+ t['obengali'] = 0x0993;
+ t['obopomofo'] = 0x311B;
+ t['obreve'] = 0x014F;
+ t['ocandradeva'] = 0x0911;
+ t['ocandragujarati'] = 0x0A91;
+ t['ocandravowelsigndeva'] = 0x0949;
+ t['ocandravowelsigngujarati'] = 0x0AC9;
+ t['ocaron'] = 0x01D2;
+ t['ocircle'] = 0x24DE;
+ t['ocircumflex'] = 0x00F4;
+ t['ocircumflexacute'] = 0x1ED1;
+ t['ocircumflexdotbelow'] = 0x1ED9;
+ t['ocircumflexgrave'] = 0x1ED3;
+ t['ocircumflexhookabove'] = 0x1ED5;
+ t['ocircumflextilde'] = 0x1ED7;
+ t['ocyrillic'] = 0x043E;
+ t['odblacute'] = 0x0151;
+ t['odblgrave'] = 0x020D;
+ t['odeva'] = 0x0913;
+ t['odieresis'] = 0x00F6;
+ t['odieresiscyrillic'] = 0x04E7;
+ t['odotbelow'] = 0x1ECD;
+ t['oe'] = 0x0153;
+ t['oekorean'] = 0x315A;
+ t['ogonek'] = 0x02DB;
+ t['ogonekcmb'] = 0x0328;
+ t['ograve'] = 0x00F2;
+ t['ogujarati'] = 0x0A93;
+ t['oharmenian'] = 0x0585;
+ t['ohiragana'] = 0x304A;
+ t['ohookabove'] = 0x1ECF;
+ t['ohorn'] = 0x01A1;
+ t['ohornacute'] = 0x1EDB;
+ t['ohorndotbelow'] = 0x1EE3;
+ t['ohorngrave'] = 0x1EDD;
+ t['ohornhookabove'] = 0x1EDF;
+ t['ohorntilde'] = 0x1EE1;
+ t['ohungarumlaut'] = 0x0151;
+ t['oi'] = 0x01A3;
+ t['oinvertedbreve'] = 0x020F;
+ t['okatakana'] = 0x30AA;
+ t['okatakanahalfwidth'] = 0xFF75;
+ t['okorean'] = 0x3157;
+ t['olehebrew'] = 0x05AB;
+ t['omacron'] = 0x014D;
+ t['omacronacute'] = 0x1E53;
+ t['omacrongrave'] = 0x1E51;
+ t['omdeva'] = 0x0950;
+ t['omega'] = 0x03C9;
+ t['omega1'] = 0x03D6;
+ t['omegacyrillic'] = 0x0461;
+ t['omegalatinclosed'] = 0x0277;
+ t['omegaroundcyrillic'] = 0x047B;
+ t['omegatitlocyrillic'] = 0x047D;
+ t['omegatonos'] = 0x03CE;
+ t['omgujarati'] = 0x0AD0;
+ t['omicron'] = 0x03BF;
+ t['omicrontonos'] = 0x03CC;
+ t['omonospace'] = 0xFF4F;
+ t['one'] = 0x0031;
+ t['onearabic'] = 0x0661;
+ t['onebengali'] = 0x09E7;
+ t['onecircle'] = 0x2460;
+ t['onecircleinversesansserif'] = 0x278A;
+ t['onedeva'] = 0x0967;
+ t['onedotenleader'] = 0x2024;
+ t['oneeighth'] = 0x215B;
+ t['onefitted'] = 0xF6DC;
+ t['onegujarati'] = 0x0AE7;
+ t['onegurmukhi'] = 0x0A67;
+ t['onehackarabic'] = 0x0661;
+ t['onehalf'] = 0x00BD;
+ t['onehangzhou'] = 0x3021;
+ t['oneideographicparen'] = 0x3220;
+ t['oneinferior'] = 0x2081;
+ t['onemonospace'] = 0xFF11;
+ t['onenumeratorbengali'] = 0x09F4;
+ t['oneoldstyle'] = 0xF731;
+ t['oneparen'] = 0x2474;
+ t['oneperiod'] = 0x2488;
+ t['onepersian'] = 0x06F1;
+ t['onequarter'] = 0x00BC;
+ t['oneroman'] = 0x2170;
+ t['onesuperior'] = 0x00B9;
+ t['onethai'] = 0x0E51;
+ t['onethird'] = 0x2153;
+ t['oogonek'] = 0x01EB;
+ t['oogonekmacron'] = 0x01ED;
+ t['oogurmukhi'] = 0x0A13;
+ t['oomatragurmukhi'] = 0x0A4B;
+ t['oopen'] = 0x0254;
+ t['oparen'] = 0x24AA;
+ t['openbullet'] = 0x25E6;
+ t['option'] = 0x2325;
+ t['ordfeminine'] = 0x00AA;
+ t['ordmasculine'] = 0x00BA;
+ t['orthogonal'] = 0x221F;
+ t['oshortdeva'] = 0x0912;
+ t['oshortvowelsigndeva'] = 0x094A;
+ t['oslash'] = 0x00F8;
+ t['oslashacute'] = 0x01FF;
+ t['osmallhiragana'] = 0x3049;
+ t['osmallkatakana'] = 0x30A9;
+ t['osmallkatakanahalfwidth'] = 0xFF6B;
+ t['ostrokeacute'] = 0x01FF;
+ t['osuperior'] = 0xF6F0;
+ t['otcyrillic'] = 0x047F;
+ t['otilde'] = 0x00F5;
+ t['otildeacute'] = 0x1E4D;
+ t['otildedieresis'] = 0x1E4F;
+ t['oubopomofo'] = 0x3121;
+ t['overline'] = 0x203E;
+ t['overlinecenterline'] = 0xFE4A;
+ t['overlinecmb'] = 0x0305;
+ t['overlinedashed'] = 0xFE49;
+ t['overlinedblwavy'] = 0xFE4C;
+ t['overlinewavy'] = 0xFE4B;
+ t['overscore'] = 0x00AF;
+ t['ovowelsignbengali'] = 0x09CB;
+ t['ovowelsigndeva'] = 0x094B;
+ t['ovowelsigngujarati'] = 0x0ACB;
+ t['p'] = 0x0070;
+ t['paampssquare'] = 0x3380;
+ t['paasentosquare'] = 0x332B;
+ t['pabengali'] = 0x09AA;
+ t['pacute'] = 0x1E55;
+ t['padeva'] = 0x092A;
+ t['pagedown'] = 0x21DF;
+ t['pageup'] = 0x21DE;
+ t['pagujarati'] = 0x0AAA;
+ t['pagurmukhi'] = 0x0A2A;
+ t['pahiragana'] = 0x3071;
+ t['paiyannoithai'] = 0x0E2F;
+ t['pakatakana'] = 0x30D1;
+ t['palatalizationcyrilliccmb'] = 0x0484;
+ t['palochkacyrillic'] = 0x04C0;
+ t['pansioskorean'] = 0x317F;
+ t['paragraph'] = 0x00B6;
+ t['parallel'] = 0x2225;
+ t['parenleft'] = 0x0028;
+ t['parenleftaltonearabic'] = 0xFD3E;
+ t['parenleftbt'] = 0xF8ED;
+ t['parenleftex'] = 0xF8EC;
+ t['parenleftinferior'] = 0x208D;
+ t['parenleftmonospace'] = 0xFF08;
+ t['parenleftsmall'] = 0xFE59;
+ t['parenleftsuperior'] = 0x207D;
+ t['parenlefttp'] = 0xF8EB;
+ t['parenleftvertical'] = 0xFE35;
+ t['parenright'] = 0x0029;
+ t['parenrightaltonearabic'] = 0xFD3F;
+ t['parenrightbt'] = 0xF8F8;
+ t['parenrightex'] = 0xF8F7;
+ t['parenrightinferior'] = 0x208E;
+ t['parenrightmonospace'] = 0xFF09;
+ t['parenrightsmall'] = 0xFE5A;
+ t['parenrightsuperior'] = 0x207E;
+ t['parenrighttp'] = 0xF8F6;
+ t['parenrightvertical'] = 0xFE36;
+ t['partialdiff'] = 0x2202;
+ t['paseqhebrew'] = 0x05C0;
+ t['pashtahebrew'] = 0x0599;
+ t['pasquare'] = 0x33A9;
+ t['patah'] = 0x05B7;
+ t['patah11'] = 0x05B7;
+ t['patah1d'] = 0x05B7;
+ t['patah2a'] = 0x05B7;
+ t['patahhebrew'] = 0x05B7;
+ t['patahnarrowhebrew'] = 0x05B7;
+ t['patahquarterhebrew'] = 0x05B7;
+ t['patahwidehebrew'] = 0x05B7;
+ t['pazerhebrew'] = 0x05A1;
+ t['pbopomofo'] = 0x3106;
+ t['pcircle'] = 0x24DF;
+ t['pdotaccent'] = 0x1E57;
+ t['pe'] = 0x05E4;
+ t['pecyrillic'] = 0x043F;
+ t['pedagesh'] = 0xFB44;
+ t['pedageshhebrew'] = 0xFB44;
+ t['peezisquare'] = 0x333B;
+ t['pefinaldageshhebrew'] = 0xFB43;
+ t['peharabic'] = 0x067E;
+ t['peharmenian'] = 0x057A;
+ t['pehebrew'] = 0x05E4;
+ t['pehfinalarabic'] = 0xFB57;
+ t['pehinitialarabic'] = 0xFB58;
+ t['pehiragana'] = 0x307A;
+ t['pehmedialarabic'] = 0xFB59;
+ t['pekatakana'] = 0x30DA;
+ t['pemiddlehookcyrillic'] = 0x04A7;
+ t['perafehebrew'] = 0xFB4E;
+ t['percent'] = 0x0025;
+ t['percentarabic'] = 0x066A;
+ t['percentmonospace'] = 0xFF05;
+ t['percentsmall'] = 0xFE6A;
+ t['period'] = 0x002E;
+ t['periodarmenian'] = 0x0589;
+ t['periodcentered'] = 0x00B7;
+ t['periodhalfwidth'] = 0xFF61;
+ t['periodinferior'] = 0xF6E7;
+ t['periodmonospace'] = 0xFF0E;
+ t['periodsmall'] = 0xFE52;
+ t['periodsuperior'] = 0xF6E8;
+ t['perispomenigreekcmb'] = 0x0342;
+ t['perpendicular'] = 0x22A5;
+ t['perthousand'] = 0x2030;
+ t['peseta'] = 0x20A7;
+ t['pfsquare'] = 0x338A;
+ t['phabengali'] = 0x09AB;
+ t['phadeva'] = 0x092B;
+ t['phagujarati'] = 0x0AAB;
+ t['phagurmukhi'] = 0x0A2B;
+ t['phi'] = 0x03C6;
+ t['phi1'] = 0x03D5;
+ t['phieuphacirclekorean'] = 0x327A;
+ t['phieuphaparenkorean'] = 0x321A;
+ t['phieuphcirclekorean'] = 0x326C;
+ t['phieuphkorean'] = 0x314D;
+ t['phieuphparenkorean'] = 0x320C;
+ t['philatin'] = 0x0278;
+ t['phinthuthai'] = 0x0E3A;
+ t['phisymbolgreek'] = 0x03D5;
+ t['phook'] = 0x01A5;
+ t['phophanthai'] = 0x0E1E;
+ t['phophungthai'] = 0x0E1C;
+ t['phosamphaothai'] = 0x0E20;
+ t['pi'] = 0x03C0;
+ t['pieupacirclekorean'] = 0x3273;
+ t['pieupaparenkorean'] = 0x3213;
+ t['pieupcieuckorean'] = 0x3176;
+ t['pieupcirclekorean'] = 0x3265;
+ t['pieupkiyeokkorean'] = 0x3172;
+ t['pieupkorean'] = 0x3142;
+ t['pieupparenkorean'] = 0x3205;
+ t['pieupsioskiyeokkorean'] = 0x3174;
+ t['pieupsioskorean'] = 0x3144;
+ t['pieupsiostikeutkorean'] = 0x3175;
+ t['pieupthieuthkorean'] = 0x3177;
+ t['pieuptikeutkorean'] = 0x3173;
+ t['pihiragana'] = 0x3074;
+ t['pikatakana'] = 0x30D4;
+ t['pisymbolgreek'] = 0x03D6;
+ t['piwrarmenian'] = 0x0583;
+ t['plus'] = 0x002B;
+ t['plusbelowcmb'] = 0x031F;
+ t['pluscircle'] = 0x2295;
+ t['plusminus'] = 0x00B1;
+ t['plusmod'] = 0x02D6;
+ t['plusmonospace'] = 0xFF0B;
+ t['plussmall'] = 0xFE62;
+ t['plussuperior'] = 0x207A;
+ t['pmonospace'] = 0xFF50;
+ t['pmsquare'] = 0x33D8;
+ t['pohiragana'] = 0x307D;
+ t['pointingindexdownwhite'] = 0x261F;
+ t['pointingindexleftwhite'] = 0x261C;
+ t['pointingindexrightwhite'] = 0x261E;
+ t['pointingindexupwhite'] = 0x261D;
+ t['pokatakana'] = 0x30DD;
+ t['poplathai'] = 0x0E1B;
+ t['postalmark'] = 0x3012;
+ t['postalmarkface'] = 0x3020;
+ t['pparen'] = 0x24AB;
+ t['precedes'] = 0x227A;
+ t['prescription'] = 0x211E;
+ t['primemod'] = 0x02B9;
+ t['primereversed'] = 0x2035;
+ t['product'] = 0x220F;
+ t['projective'] = 0x2305;
+ t['prolongedkana'] = 0x30FC;
+ t['propellor'] = 0x2318;
+ t['propersubset'] = 0x2282;
+ t['propersuperset'] = 0x2283;
+ t['proportion'] = 0x2237;
+ t['proportional'] = 0x221D;
+ t['psi'] = 0x03C8;
+ t['psicyrillic'] = 0x0471;
+ t['psilipneumatacyrilliccmb'] = 0x0486;
+ t['pssquare'] = 0x33B0;
+ t['puhiragana'] = 0x3077;
+ t['pukatakana'] = 0x30D7;
+ t['pvsquare'] = 0x33B4;
+ t['pwsquare'] = 0x33BA;
+ t['q'] = 0x0071;
+ t['qadeva'] = 0x0958;
+ t['qadmahebrew'] = 0x05A8;
+ t['qafarabic'] = 0x0642;
+ t['qaffinalarabic'] = 0xFED6;
+ t['qafinitialarabic'] = 0xFED7;
+ t['qafmedialarabic'] = 0xFED8;
+ t['qamats'] = 0x05B8;
+ t['qamats10'] = 0x05B8;
+ t['qamats1a'] = 0x05B8;
+ t['qamats1c'] = 0x05B8;
+ t['qamats27'] = 0x05B8;
+ t['qamats29'] = 0x05B8;
+ t['qamats33'] = 0x05B8;
+ t['qamatsde'] = 0x05B8;
+ t['qamatshebrew'] = 0x05B8;
+ t['qamatsnarrowhebrew'] = 0x05B8;
+ t['qamatsqatanhebrew'] = 0x05B8;
+ t['qamatsqatannarrowhebrew'] = 0x05B8;
+ t['qamatsqatanquarterhebrew'] = 0x05B8;
+ t['qamatsqatanwidehebrew'] = 0x05B8;
+ t['qamatsquarterhebrew'] = 0x05B8;
+ t['qamatswidehebrew'] = 0x05B8;
+ t['qarneyparahebrew'] = 0x059F;
+ t['qbopomofo'] = 0x3111;
+ t['qcircle'] = 0x24E0;
+ t['qhook'] = 0x02A0;
+ t['qmonospace'] = 0xFF51;
+ t['qof'] = 0x05E7;
+ t['qofdagesh'] = 0xFB47;
+ t['qofdageshhebrew'] = 0xFB47;
+ t['qofhebrew'] = 0x05E7;
+ t['qparen'] = 0x24AC;
+ t['quarternote'] = 0x2669;
+ t['qubuts'] = 0x05BB;
+ t['qubuts18'] = 0x05BB;
+ t['qubuts25'] = 0x05BB;
+ t['qubuts31'] = 0x05BB;
+ t['qubutshebrew'] = 0x05BB;
+ t['qubutsnarrowhebrew'] = 0x05BB;
+ t['qubutsquarterhebrew'] = 0x05BB;
+ t['qubutswidehebrew'] = 0x05BB;
+ t['question'] = 0x003F;
+ t['questionarabic'] = 0x061F;
+ t['questionarmenian'] = 0x055E;
+ t['questiondown'] = 0x00BF;
+ t['questiondownsmall'] = 0xF7BF;
+ t['questiongreek'] = 0x037E;
+ t['questionmonospace'] = 0xFF1F;
+ t['questionsmall'] = 0xF73F;
+ t['quotedbl'] = 0x0022;
+ t['quotedblbase'] = 0x201E;
+ t['quotedblleft'] = 0x201C;
+ t['quotedblmonospace'] = 0xFF02;
+ t['quotedblprime'] = 0x301E;
+ t['quotedblprimereversed'] = 0x301D;
+ t['quotedblright'] = 0x201D;
+ t['quoteleft'] = 0x2018;
+ t['quoteleftreversed'] = 0x201B;
+ t['quotereversed'] = 0x201B;
+ t['quoteright'] = 0x2019;
+ t['quoterightn'] = 0x0149;
+ t['quotesinglbase'] = 0x201A;
+ t['quotesingle'] = 0x0027;
+ t['quotesinglemonospace'] = 0xFF07;
+ t['r'] = 0x0072;
+ t['raarmenian'] = 0x057C;
+ t['rabengali'] = 0x09B0;
+ t['racute'] = 0x0155;
+ t['radeva'] = 0x0930;
+ t['radical'] = 0x221A;
+ t['radicalex'] = 0xF8E5;
+ t['radoverssquare'] = 0x33AE;
+ t['radoverssquaredsquare'] = 0x33AF;
+ t['radsquare'] = 0x33AD;
+ t['rafe'] = 0x05BF;
+ t['rafehebrew'] = 0x05BF;
+ t['ragujarati'] = 0x0AB0;
+ t['ragurmukhi'] = 0x0A30;
+ t['rahiragana'] = 0x3089;
+ t['rakatakana'] = 0x30E9;
+ t['rakatakanahalfwidth'] = 0xFF97;
+ t['ralowerdiagonalbengali'] = 0x09F1;
+ t['ramiddlediagonalbengali'] = 0x09F0;
+ t['ramshorn'] = 0x0264;
+ t['ratio'] = 0x2236;
+ t['rbopomofo'] = 0x3116;
+ t['rcaron'] = 0x0159;
+ t['rcedilla'] = 0x0157;
+ t['rcircle'] = 0x24E1;
+ t['rcommaaccent'] = 0x0157;
+ t['rdblgrave'] = 0x0211;
+ t['rdotaccent'] = 0x1E59;
+ t['rdotbelow'] = 0x1E5B;
+ t['rdotbelowmacron'] = 0x1E5D;
+ t['referencemark'] = 0x203B;
+ t['reflexsubset'] = 0x2286;
+ t['reflexsuperset'] = 0x2287;
+ t['registered'] = 0x00AE;
+ t['registersans'] = 0xF8E8;
+ t['registerserif'] = 0xF6DA;
+ t['reharabic'] = 0x0631;
+ t['reharmenian'] = 0x0580;
+ t['rehfinalarabic'] = 0xFEAE;
+ t['rehiragana'] = 0x308C;
+ t['rekatakana'] = 0x30EC;
+ t['rekatakanahalfwidth'] = 0xFF9A;
+ t['resh'] = 0x05E8;
+ t['reshdageshhebrew'] = 0xFB48;
+ t['reshhebrew'] = 0x05E8;
+ t['reversedtilde'] = 0x223D;
+ t['reviahebrew'] = 0x0597;
+ t['reviamugrashhebrew'] = 0x0597;
+ t['revlogicalnot'] = 0x2310;
+ t['rfishhook'] = 0x027E;
+ t['rfishhookreversed'] = 0x027F;
+ t['rhabengali'] = 0x09DD;
+ t['rhadeva'] = 0x095D;
+ t['rho'] = 0x03C1;
+ t['rhook'] = 0x027D;
+ t['rhookturned'] = 0x027B;
+ t['rhookturnedsuperior'] = 0x02B5;
+ t['rhosymbolgreek'] = 0x03F1;
+ t['rhotichookmod'] = 0x02DE;
+ t['rieulacirclekorean'] = 0x3271;
+ t['rieulaparenkorean'] = 0x3211;
+ t['rieulcirclekorean'] = 0x3263;
+ t['rieulhieuhkorean'] = 0x3140;
+ t['rieulkiyeokkorean'] = 0x313A;
+ t['rieulkiyeoksioskorean'] = 0x3169;
+ t['rieulkorean'] = 0x3139;
+ t['rieulmieumkorean'] = 0x313B;
+ t['rieulpansioskorean'] = 0x316C;
+ t['rieulparenkorean'] = 0x3203;
+ t['rieulphieuphkorean'] = 0x313F;
+ t['rieulpieupkorean'] = 0x313C;
+ t['rieulpieupsioskorean'] = 0x316B;
+ t['rieulsioskorean'] = 0x313D;
+ t['rieulthieuthkorean'] = 0x313E;
+ t['rieultikeutkorean'] = 0x316A;
+ t['rieulyeorinhieuhkorean'] = 0x316D;
+ t['rightangle'] = 0x221F;
+ t['righttackbelowcmb'] = 0x0319;
+ t['righttriangle'] = 0x22BF;
+ t['rihiragana'] = 0x308A;
+ t['rikatakana'] = 0x30EA;
+ t['rikatakanahalfwidth'] = 0xFF98;
+ t['ring'] = 0x02DA;
+ t['ringbelowcmb'] = 0x0325;
+ t['ringcmb'] = 0x030A;
+ t['ringhalfleft'] = 0x02BF;
+ t['ringhalfleftarmenian'] = 0x0559;
+ t['ringhalfleftbelowcmb'] = 0x031C;
+ t['ringhalfleftcentered'] = 0x02D3;
+ t['ringhalfright'] = 0x02BE;
+ t['ringhalfrightbelowcmb'] = 0x0339;
+ t['ringhalfrightcentered'] = 0x02D2;
+ t['rinvertedbreve'] = 0x0213;
+ t['rittorusquare'] = 0x3351;
+ t['rlinebelow'] = 0x1E5F;
+ t['rlongleg'] = 0x027C;
+ t['rlonglegturned'] = 0x027A;
+ t['rmonospace'] = 0xFF52;
+ t['rohiragana'] = 0x308D;
+ t['rokatakana'] = 0x30ED;
+ t['rokatakanahalfwidth'] = 0xFF9B;
+ t['roruathai'] = 0x0E23;
+ t['rparen'] = 0x24AD;
+ t['rrabengali'] = 0x09DC;
+ t['rradeva'] = 0x0931;
+ t['rragurmukhi'] = 0x0A5C;
+ t['rreharabic'] = 0x0691;
+ t['rrehfinalarabic'] = 0xFB8D;
+ t['rrvocalicbengali'] = 0x09E0;
+ t['rrvocalicdeva'] = 0x0960;
+ t['rrvocalicgujarati'] = 0x0AE0;
+ t['rrvocalicvowelsignbengali'] = 0x09C4;
+ t['rrvocalicvowelsigndeva'] = 0x0944;
+ t['rrvocalicvowelsigngujarati'] = 0x0AC4;
+ t['rsuperior'] = 0xF6F1;
+ t['rtblock'] = 0x2590;
+ t['rturned'] = 0x0279;
+ t['rturnedsuperior'] = 0x02B4;
+ t['ruhiragana'] = 0x308B;
+ t['rukatakana'] = 0x30EB;
+ t['rukatakanahalfwidth'] = 0xFF99;
+ t['rupeemarkbengali'] = 0x09F2;
+ t['rupeesignbengali'] = 0x09F3;
+ t['rupiah'] = 0xF6DD;
+ t['ruthai'] = 0x0E24;
+ t['rvocalicbengali'] = 0x098B;
+ t['rvocalicdeva'] = 0x090B;
+ t['rvocalicgujarati'] = 0x0A8B;
+ t['rvocalicvowelsignbengali'] = 0x09C3;
+ t['rvocalicvowelsigndeva'] = 0x0943;
+ t['rvocalicvowelsigngujarati'] = 0x0AC3;
+ t['s'] = 0x0073;
+ t['sabengali'] = 0x09B8;
+ t['sacute'] = 0x015B;
+ t['sacutedotaccent'] = 0x1E65;
+ t['sadarabic'] = 0x0635;
+ t['sadeva'] = 0x0938;
+ t['sadfinalarabic'] = 0xFEBA;
+ t['sadinitialarabic'] = 0xFEBB;
+ t['sadmedialarabic'] = 0xFEBC;
+ t['sagujarati'] = 0x0AB8;
+ t['sagurmukhi'] = 0x0A38;
+ t['sahiragana'] = 0x3055;
+ t['sakatakana'] = 0x30B5;
+ t['sakatakanahalfwidth'] = 0xFF7B;
+ t['sallallahoualayhewasallamarabic'] = 0xFDFA;
+ t['samekh'] = 0x05E1;
+ t['samekhdagesh'] = 0xFB41;
+ t['samekhdageshhebrew'] = 0xFB41;
+ t['samekhhebrew'] = 0x05E1;
+ t['saraaathai'] = 0x0E32;
+ t['saraaethai'] = 0x0E41;
+ t['saraaimaimalaithai'] = 0x0E44;
+ t['saraaimaimuanthai'] = 0x0E43;
+ t['saraamthai'] = 0x0E33;
+ t['saraathai'] = 0x0E30;
+ t['saraethai'] = 0x0E40;
+ t['saraiileftthai'] = 0xF886;
+ t['saraiithai'] = 0x0E35;
+ t['saraileftthai'] = 0xF885;
+ t['saraithai'] = 0x0E34;
+ t['saraothai'] = 0x0E42;
+ t['saraueeleftthai'] = 0xF888;
+ t['saraueethai'] = 0x0E37;
+ t['saraueleftthai'] = 0xF887;
+ t['sarauethai'] = 0x0E36;
+ t['sarauthai'] = 0x0E38;
+ t['sarauuthai'] = 0x0E39;
+ t['sbopomofo'] = 0x3119;
+ t['scaron'] = 0x0161;
+ t['scarondotaccent'] = 0x1E67;
+ t['scedilla'] = 0x015F;
+ t['schwa'] = 0x0259;
+ t['schwacyrillic'] = 0x04D9;
+ t['schwadieresiscyrillic'] = 0x04DB;
+ t['schwahook'] = 0x025A;
+ t['scircle'] = 0x24E2;
+ t['scircumflex'] = 0x015D;
+ t['scommaaccent'] = 0x0219;
+ t['sdotaccent'] = 0x1E61;
+ t['sdotbelow'] = 0x1E63;
+ t['sdotbelowdotaccent'] = 0x1E69;
+ t['seagullbelowcmb'] = 0x033C;
+ t['second'] = 0x2033;
+ t['secondtonechinese'] = 0x02CA;
+ t['section'] = 0x00A7;
+ t['seenarabic'] = 0x0633;
+ t['seenfinalarabic'] = 0xFEB2;
+ t['seeninitialarabic'] = 0xFEB3;
+ t['seenmedialarabic'] = 0xFEB4;
+ t['segol'] = 0x05B6;
+ t['segol13'] = 0x05B6;
+ t['segol1f'] = 0x05B6;
+ t['segol2c'] = 0x05B6;
+ t['segolhebrew'] = 0x05B6;
+ t['segolnarrowhebrew'] = 0x05B6;
+ t['segolquarterhebrew'] = 0x05B6;
+ t['segoltahebrew'] = 0x0592;
+ t['segolwidehebrew'] = 0x05B6;
+ t['seharmenian'] = 0x057D;
+ t['sehiragana'] = 0x305B;
+ t['sekatakana'] = 0x30BB;
+ t['sekatakanahalfwidth'] = 0xFF7E;
+ t['semicolon'] = 0x003B;
+ t['semicolonarabic'] = 0x061B;
+ t['semicolonmonospace'] = 0xFF1B;
+ t['semicolonsmall'] = 0xFE54;
+ t['semivoicedmarkkana'] = 0x309C;
+ t['semivoicedmarkkanahalfwidth'] = 0xFF9F;
+ t['sentisquare'] = 0x3322;
+ t['sentosquare'] = 0x3323;
+ t['seven'] = 0x0037;
+ t['sevenarabic'] = 0x0667;
+ t['sevenbengali'] = 0x09ED;
+ t['sevencircle'] = 0x2466;
+ t['sevencircleinversesansserif'] = 0x2790;
+ t['sevendeva'] = 0x096D;
+ t['seveneighths'] = 0x215E;
+ t['sevengujarati'] = 0x0AED;
+ t['sevengurmukhi'] = 0x0A6D;
+ t['sevenhackarabic'] = 0x0667;
+ t['sevenhangzhou'] = 0x3027;
+ t['sevenideographicparen'] = 0x3226;
+ t['seveninferior'] = 0x2087;
+ t['sevenmonospace'] = 0xFF17;
+ t['sevenoldstyle'] = 0xF737;
+ t['sevenparen'] = 0x247A;
+ t['sevenperiod'] = 0x248E;
+ t['sevenpersian'] = 0x06F7;
+ t['sevenroman'] = 0x2176;
+ t['sevensuperior'] = 0x2077;
+ t['seventeencircle'] = 0x2470;
+ t['seventeenparen'] = 0x2484;
+ t['seventeenperiod'] = 0x2498;
+ t['seventhai'] = 0x0E57;
+ t['sfthyphen'] = 0x00AD;
+ t['shaarmenian'] = 0x0577;
+ t['shabengali'] = 0x09B6;
+ t['shacyrillic'] = 0x0448;
+ t['shaddaarabic'] = 0x0651;
+ t['shaddadammaarabic'] = 0xFC61;
+ t['shaddadammatanarabic'] = 0xFC5E;
+ t['shaddafathaarabic'] = 0xFC60;
+ t['shaddakasraarabic'] = 0xFC62;
+ t['shaddakasratanarabic'] = 0xFC5F;
+ t['shade'] = 0x2592;
+ t['shadedark'] = 0x2593;
+ t['shadelight'] = 0x2591;
+ t['shademedium'] = 0x2592;
+ t['shadeva'] = 0x0936;
+ t['shagujarati'] = 0x0AB6;
+ t['shagurmukhi'] = 0x0A36;
+ t['shalshelethebrew'] = 0x0593;
+ t['shbopomofo'] = 0x3115;
+ t['shchacyrillic'] = 0x0449;
+ t['sheenarabic'] = 0x0634;
+ t['sheenfinalarabic'] = 0xFEB6;
+ t['sheeninitialarabic'] = 0xFEB7;
+ t['sheenmedialarabic'] = 0xFEB8;
+ t['sheicoptic'] = 0x03E3;
+ t['sheqel'] = 0x20AA;
+ t['sheqelhebrew'] = 0x20AA;
+ t['sheva'] = 0x05B0;
+ t['sheva115'] = 0x05B0;
+ t['sheva15'] = 0x05B0;
+ t['sheva22'] = 0x05B0;
+ t['sheva2e'] = 0x05B0;
+ t['shevahebrew'] = 0x05B0;
+ t['shevanarrowhebrew'] = 0x05B0;
+ t['shevaquarterhebrew'] = 0x05B0;
+ t['shevawidehebrew'] = 0x05B0;
+ t['shhacyrillic'] = 0x04BB;
+ t['shimacoptic'] = 0x03ED;
+ t['shin'] = 0x05E9;
+ t['shindagesh'] = 0xFB49;
+ t['shindageshhebrew'] = 0xFB49;
+ t['shindageshshindot'] = 0xFB2C;
+ t['shindageshshindothebrew'] = 0xFB2C;
+ t['shindageshsindot'] = 0xFB2D;
+ t['shindageshsindothebrew'] = 0xFB2D;
+ t['shindothebrew'] = 0x05C1;
+ t['shinhebrew'] = 0x05E9;
+ t['shinshindot'] = 0xFB2A;
+ t['shinshindothebrew'] = 0xFB2A;
+ t['shinsindot'] = 0xFB2B;
+ t['shinsindothebrew'] = 0xFB2B;
+ t['shook'] = 0x0282;
+ t['sigma'] = 0x03C3;
+ t['sigma1'] = 0x03C2;
+ t['sigmafinal'] = 0x03C2;
+ t['sigmalunatesymbolgreek'] = 0x03F2;
+ t['sihiragana'] = 0x3057;
+ t['sikatakana'] = 0x30B7;
+ t['sikatakanahalfwidth'] = 0xFF7C;
+ t['siluqhebrew'] = 0x05BD;
+ t['siluqlefthebrew'] = 0x05BD;
+ t['similar'] = 0x223C;
+ t['sindothebrew'] = 0x05C2;
+ t['siosacirclekorean'] = 0x3274;
+ t['siosaparenkorean'] = 0x3214;
+ t['sioscieuckorean'] = 0x317E;
+ t['sioscirclekorean'] = 0x3266;
+ t['sioskiyeokkorean'] = 0x317A;
+ t['sioskorean'] = 0x3145;
+ t['siosnieunkorean'] = 0x317B;
+ t['siosparenkorean'] = 0x3206;
+ t['siospieupkorean'] = 0x317D;
+ t['siostikeutkorean'] = 0x317C;
+ t['six'] = 0x0036;
+ t['sixarabic'] = 0x0666;
+ t['sixbengali'] = 0x09EC;
+ t['sixcircle'] = 0x2465;
+ t['sixcircleinversesansserif'] = 0x278F;
+ t['sixdeva'] = 0x096C;
+ t['sixgujarati'] = 0x0AEC;
+ t['sixgurmukhi'] = 0x0A6C;
+ t['sixhackarabic'] = 0x0666;
+ t['sixhangzhou'] = 0x3026;
+ t['sixideographicparen'] = 0x3225;
+ t['sixinferior'] = 0x2086;
+ t['sixmonospace'] = 0xFF16;
+ t['sixoldstyle'] = 0xF736;
+ t['sixparen'] = 0x2479;
+ t['sixperiod'] = 0x248D;
+ t['sixpersian'] = 0x06F6;
+ t['sixroman'] = 0x2175;
+ t['sixsuperior'] = 0x2076;
+ t['sixteencircle'] = 0x246F;
+ t['sixteencurrencydenominatorbengali'] = 0x09F9;
+ t['sixteenparen'] = 0x2483;
+ t['sixteenperiod'] = 0x2497;
+ t['sixthai'] = 0x0E56;
+ t['slash'] = 0x002F;
+ t['slashmonospace'] = 0xFF0F;
+ t['slong'] = 0x017F;
+ t['slongdotaccent'] = 0x1E9B;
+ t['smileface'] = 0x263A;
+ t['smonospace'] = 0xFF53;
+ t['sofpasuqhebrew'] = 0x05C3;
+ t['softhyphen'] = 0x00AD;
+ t['softsigncyrillic'] = 0x044C;
+ t['sohiragana'] = 0x305D;
+ t['sokatakana'] = 0x30BD;
+ t['sokatakanahalfwidth'] = 0xFF7F;
+ t['soliduslongoverlaycmb'] = 0x0338;
+ t['solidusshortoverlaycmb'] = 0x0337;
+ t['sorusithai'] = 0x0E29;
+ t['sosalathai'] = 0x0E28;
+ t['sosothai'] = 0x0E0B;
+ t['sosuathai'] = 0x0E2A;
+ t['space'] = 0x0020;
+ t['spacehackarabic'] = 0x0020;
+ t['spade'] = 0x2660;
+ t['spadesuitblack'] = 0x2660;
+ t['spadesuitwhite'] = 0x2664;
+ t['sparen'] = 0x24AE;
+ t['squarebelowcmb'] = 0x033B;
+ t['squarecc'] = 0x33C4;
+ t['squarecm'] = 0x339D;
+ t['squarediagonalcrosshatchfill'] = 0x25A9;
+ t['squarehorizontalfill'] = 0x25A4;
+ t['squarekg'] = 0x338F;
+ t['squarekm'] = 0x339E;
+ t['squarekmcapital'] = 0x33CE;
+ t['squareln'] = 0x33D1;
+ t['squarelog'] = 0x33D2;
+ t['squaremg'] = 0x338E;
+ t['squaremil'] = 0x33D5;
+ t['squaremm'] = 0x339C;
+ t['squaremsquared'] = 0x33A1;
+ t['squareorthogonalcrosshatchfill'] = 0x25A6;
+ t['squareupperlefttolowerrightfill'] = 0x25A7;
+ t['squareupperrighttolowerleftfill'] = 0x25A8;
+ t['squareverticalfill'] = 0x25A5;
+ t['squarewhitewithsmallblack'] = 0x25A3;
+ t['srsquare'] = 0x33DB;
+ t['ssabengali'] = 0x09B7;
+ t['ssadeva'] = 0x0937;
+ t['ssagujarati'] = 0x0AB7;
+ t['ssangcieuckorean'] = 0x3149;
+ t['ssanghieuhkorean'] = 0x3185;
+ t['ssangieungkorean'] = 0x3180;
+ t['ssangkiyeokkorean'] = 0x3132;
+ t['ssangnieunkorean'] = 0x3165;
+ t['ssangpieupkorean'] = 0x3143;
+ t['ssangsioskorean'] = 0x3146;
+ t['ssangtikeutkorean'] = 0x3138;
+ t['ssuperior'] = 0xF6F2;
+ t['sterling'] = 0x00A3;
+ t['sterlingmonospace'] = 0xFFE1;
+ t['strokelongoverlaycmb'] = 0x0336;
+ t['strokeshortoverlaycmb'] = 0x0335;
+ t['subset'] = 0x2282;
+ t['subsetnotequal'] = 0x228A;
+ t['subsetorequal'] = 0x2286;
+ t['succeeds'] = 0x227B;
+ t['suchthat'] = 0x220B;
+ t['suhiragana'] = 0x3059;
+ t['sukatakana'] = 0x30B9;
+ t['sukatakanahalfwidth'] = 0xFF7D;
+ t['sukunarabic'] = 0x0652;
+ t['summation'] = 0x2211;
+ t['sun'] = 0x263C;
+ t['superset'] = 0x2283;
+ t['supersetnotequal'] = 0x228B;
+ t['supersetorequal'] = 0x2287;
+ t['svsquare'] = 0x33DC;
+ t['syouwaerasquare'] = 0x337C;
+ t['t'] = 0x0074;
+ t['tabengali'] = 0x09A4;
+ t['tackdown'] = 0x22A4;
+ t['tackleft'] = 0x22A3;
+ t['tadeva'] = 0x0924;
+ t['tagujarati'] = 0x0AA4;
+ t['tagurmukhi'] = 0x0A24;
+ t['taharabic'] = 0x0637;
+ t['tahfinalarabic'] = 0xFEC2;
+ t['tahinitialarabic'] = 0xFEC3;
+ t['tahiragana'] = 0x305F;
+ t['tahmedialarabic'] = 0xFEC4;
+ t['taisyouerasquare'] = 0x337D;
+ t['takatakana'] = 0x30BF;
+ t['takatakanahalfwidth'] = 0xFF80;
+ t['tatweelarabic'] = 0x0640;
+ t['tau'] = 0x03C4;
+ t['tav'] = 0x05EA;
+ t['tavdages'] = 0xFB4A;
+ t['tavdagesh'] = 0xFB4A;
+ t['tavdageshhebrew'] = 0xFB4A;
+ t['tavhebrew'] = 0x05EA;
+ t['tbar'] = 0x0167;
+ t['tbopomofo'] = 0x310A;
+ t['tcaron'] = 0x0165;
+ t['tccurl'] = 0x02A8;
+ t['tcedilla'] = 0x0163;
+ t['tcheharabic'] = 0x0686;
+ t['tchehfinalarabic'] = 0xFB7B;
+ t['tchehinitialarabic'] = 0xFB7C;
+ t['tchehmedialarabic'] = 0xFB7D;
+ t['tcircle'] = 0x24E3;
+ t['tcircumflexbelow'] = 0x1E71;
+ t['tcommaaccent'] = 0x0163;
+ t['tdieresis'] = 0x1E97;
+ t['tdotaccent'] = 0x1E6B;
+ t['tdotbelow'] = 0x1E6D;
+ t['tecyrillic'] = 0x0442;
+ t['tedescendercyrillic'] = 0x04AD;
+ t['teharabic'] = 0x062A;
+ t['tehfinalarabic'] = 0xFE96;
+ t['tehhahinitialarabic'] = 0xFCA2;
+ t['tehhahisolatedarabic'] = 0xFC0C;
+ t['tehinitialarabic'] = 0xFE97;
+ t['tehiragana'] = 0x3066;
+ t['tehjeeminitialarabic'] = 0xFCA1;
+ t['tehjeemisolatedarabic'] = 0xFC0B;
+ t['tehmarbutaarabic'] = 0x0629;
+ t['tehmarbutafinalarabic'] = 0xFE94;
+ t['tehmedialarabic'] = 0xFE98;
+ t['tehmeeminitialarabic'] = 0xFCA4;
+ t['tehmeemisolatedarabic'] = 0xFC0E;
+ t['tehnoonfinalarabic'] = 0xFC73;
+ t['tekatakana'] = 0x30C6;
+ t['tekatakanahalfwidth'] = 0xFF83;
+ t['telephone'] = 0x2121;
+ t['telephoneblack'] = 0x260E;
+ t['telishagedolahebrew'] = 0x05A0;
+ t['telishaqetanahebrew'] = 0x05A9;
+ t['tencircle'] = 0x2469;
+ t['tenideographicparen'] = 0x3229;
+ t['tenparen'] = 0x247D;
+ t['tenperiod'] = 0x2491;
+ t['tenroman'] = 0x2179;
+ t['tesh'] = 0x02A7;
+ t['tet'] = 0x05D8;
+ t['tetdagesh'] = 0xFB38;
+ t['tetdageshhebrew'] = 0xFB38;
+ t['tethebrew'] = 0x05D8;
+ t['tetsecyrillic'] = 0x04B5;
+ t['tevirhebrew'] = 0x059B;
+ t['tevirlefthebrew'] = 0x059B;
+ t['thabengali'] = 0x09A5;
+ t['thadeva'] = 0x0925;
+ t['thagujarati'] = 0x0AA5;
+ t['thagurmukhi'] = 0x0A25;
+ t['thalarabic'] = 0x0630;
+ t['thalfinalarabic'] = 0xFEAC;
+ t['thanthakhatlowleftthai'] = 0xF898;
+ t['thanthakhatlowrightthai'] = 0xF897;
+ t['thanthakhatthai'] = 0x0E4C;
+ t['thanthakhatupperleftthai'] = 0xF896;
+ t['theharabic'] = 0x062B;
+ t['thehfinalarabic'] = 0xFE9A;
+ t['thehinitialarabic'] = 0xFE9B;
+ t['thehmedialarabic'] = 0xFE9C;
+ t['thereexists'] = 0x2203;
+ t['therefore'] = 0x2234;
+ t['theta'] = 0x03B8;
+ t['theta1'] = 0x03D1;
+ t['thetasymbolgreek'] = 0x03D1;
+ t['thieuthacirclekorean'] = 0x3279;
+ t['thieuthaparenkorean'] = 0x3219;
+ t['thieuthcirclekorean'] = 0x326B;
+ t['thieuthkorean'] = 0x314C;
+ t['thieuthparenkorean'] = 0x320B;
+ t['thirteencircle'] = 0x246C;
+ t['thirteenparen'] = 0x2480;
+ t['thirteenperiod'] = 0x2494;
+ t['thonangmonthothai'] = 0x0E11;
+ t['thook'] = 0x01AD;
+ t['thophuthaothai'] = 0x0E12;
+ t['thorn'] = 0x00FE;
+ t['thothahanthai'] = 0x0E17;
+ t['thothanthai'] = 0x0E10;
+ t['thothongthai'] = 0x0E18;
+ t['thothungthai'] = 0x0E16;
+ t['thousandcyrillic'] = 0x0482;
+ t['thousandsseparatorarabic'] = 0x066C;
+ t['thousandsseparatorpersian'] = 0x066C;
+ t['three'] = 0x0033;
+ t['threearabic'] = 0x0663;
+ t['threebengali'] = 0x09E9;
+ t['threecircle'] = 0x2462;
+ t['threecircleinversesansserif'] = 0x278C;
+ t['threedeva'] = 0x0969;
+ t['threeeighths'] = 0x215C;
+ t['threegujarati'] = 0x0AE9;
+ t['threegurmukhi'] = 0x0A69;
+ t['threehackarabic'] = 0x0663;
+ t['threehangzhou'] = 0x3023;
+ t['threeideographicparen'] = 0x3222;
+ t['threeinferior'] = 0x2083;
+ t['threemonospace'] = 0xFF13;
+ t['threenumeratorbengali'] = 0x09F6;
+ t['threeoldstyle'] = 0xF733;
+ t['threeparen'] = 0x2476;
+ t['threeperiod'] = 0x248A;
+ t['threepersian'] = 0x06F3;
+ t['threequarters'] = 0x00BE;
+ t['threequartersemdash'] = 0xF6DE;
+ t['threeroman'] = 0x2172;
+ t['threesuperior'] = 0x00B3;
+ t['threethai'] = 0x0E53;
+ t['thzsquare'] = 0x3394;
+ t['tihiragana'] = 0x3061;
+ t['tikatakana'] = 0x30C1;
+ t['tikatakanahalfwidth'] = 0xFF81;
+ t['tikeutacirclekorean'] = 0x3270;
+ t['tikeutaparenkorean'] = 0x3210;
+ t['tikeutcirclekorean'] = 0x3262;
+ t['tikeutkorean'] = 0x3137;
+ t['tikeutparenkorean'] = 0x3202;
+ t['tilde'] = 0x02DC;
+ t['tildebelowcmb'] = 0x0330;
+ t['tildecmb'] = 0x0303;
+ t['tildecomb'] = 0x0303;
+ t['tildedoublecmb'] = 0x0360;
+ t['tildeoperator'] = 0x223C;
+ t['tildeoverlaycmb'] = 0x0334;
+ t['tildeverticalcmb'] = 0x033E;
+ t['timescircle'] = 0x2297;
+ t['tipehahebrew'] = 0x0596;
+ t['tipehalefthebrew'] = 0x0596;
+ t['tippigurmukhi'] = 0x0A70;
+ t['titlocyrilliccmb'] = 0x0483;
+ t['tiwnarmenian'] = 0x057F;
+ t['tlinebelow'] = 0x1E6F;
+ t['tmonospace'] = 0xFF54;
+ t['toarmenian'] = 0x0569;
+ t['tohiragana'] = 0x3068;
+ t['tokatakana'] = 0x30C8;
+ t['tokatakanahalfwidth'] = 0xFF84;
+ t['tonebarextrahighmod'] = 0x02E5;
+ t['tonebarextralowmod'] = 0x02E9;
+ t['tonebarhighmod'] = 0x02E6;
+ t['tonebarlowmod'] = 0x02E8;
+ t['tonebarmidmod'] = 0x02E7;
+ t['tonefive'] = 0x01BD;
+ t['tonesix'] = 0x0185;
+ t['tonetwo'] = 0x01A8;
+ t['tonos'] = 0x0384;
+ t['tonsquare'] = 0x3327;
+ t['topatakthai'] = 0x0E0F;
+ t['tortoiseshellbracketleft'] = 0x3014;
+ t['tortoiseshellbracketleftsmall'] = 0xFE5D;
+ t['tortoiseshellbracketleftvertical'] = 0xFE39;
+ t['tortoiseshellbracketright'] = 0x3015;
+ t['tortoiseshellbracketrightsmall'] = 0xFE5E;
+ t['tortoiseshellbracketrightvertical'] = 0xFE3A;
+ t['totaothai'] = 0x0E15;
+ t['tpalatalhook'] = 0x01AB;
+ t['tparen'] = 0x24AF;
+ t['trademark'] = 0x2122;
+ t['trademarksans'] = 0xF8EA;
+ t['trademarkserif'] = 0xF6DB;
+ t['tretroflexhook'] = 0x0288;
+ t['triagdn'] = 0x25BC;
+ t['triaglf'] = 0x25C4;
+ t['triagrt'] = 0x25BA;
+ t['triagup'] = 0x25B2;
+ t['ts'] = 0x02A6;
+ t['tsadi'] = 0x05E6;
+ t['tsadidagesh'] = 0xFB46;
+ t['tsadidageshhebrew'] = 0xFB46;
+ t['tsadihebrew'] = 0x05E6;
+ t['tsecyrillic'] = 0x0446;
+ t['tsere'] = 0x05B5;
+ t['tsere12'] = 0x05B5;
+ t['tsere1e'] = 0x05B5;
+ t['tsere2b'] = 0x05B5;
+ t['tserehebrew'] = 0x05B5;
+ t['tserenarrowhebrew'] = 0x05B5;
+ t['tserequarterhebrew'] = 0x05B5;
+ t['tserewidehebrew'] = 0x05B5;
+ t['tshecyrillic'] = 0x045B;
+ t['tsuperior'] = 0xF6F3;
+ t['ttabengali'] = 0x099F;
+ t['ttadeva'] = 0x091F;
+ t['ttagujarati'] = 0x0A9F;
+ t['ttagurmukhi'] = 0x0A1F;
+ t['tteharabic'] = 0x0679;
+ t['ttehfinalarabic'] = 0xFB67;
+ t['ttehinitialarabic'] = 0xFB68;
+ t['ttehmedialarabic'] = 0xFB69;
+ t['tthabengali'] = 0x09A0;
+ t['tthadeva'] = 0x0920;
+ t['tthagujarati'] = 0x0AA0;
+ t['tthagurmukhi'] = 0x0A20;
+ t['tturned'] = 0x0287;
+ t['tuhiragana'] = 0x3064;
+ t['tukatakana'] = 0x30C4;
+ t['tukatakanahalfwidth'] = 0xFF82;
+ t['tusmallhiragana'] = 0x3063;
+ t['tusmallkatakana'] = 0x30C3;
+ t['tusmallkatakanahalfwidth'] = 0xFF6F;
+ t['twelvecircle'] = 0x246B;
+ t['twelveparen'] = 0x247F;
+ t['twelveperiod'] = 0x2493;
+ t['twelveroman'] = 0x217B;
+ t['twentycircle'] = 0x2473;
+ t['twentyhangzhou'] = 0x5344;
+ t['twentyparen'] = 0x2487;
+ t['twentyperiod'] = 0x249B;
+ t['two'] = 0x0032;
+ t['twoarabic'] = 0x0662;
+ t['twobengali'] = 0x09E8;
+ t['twocircle'] = 0x2461;
+ t['twocircleinversesansserif'] = 0x278B;
+ t['twodeva'] = 0x0968;
+ t['twodotenleader'] = 0x2025;
+ t['twodotleader'] = 0x2025;
+ t['twodotleadervertical'] = 0xFE30;
+ t['twogujarati'] = 0x0AE8;
+ t['twogurmukhi'] = 0x0A68;
+ t['twohackarabic'] = 0x0662;
+ t['twohangzhou'] = 0x3022;
+ t['twoideographicparen'] = 0x3221;
+ t['twoinferior'] = 0x2082;
+ t['twomonospace'] = 0xFF12;
+ t['twonumeratorbengali'] = 0x09F5;
+ t['twooldstyle'] = 0xF732;
+ t['twoparen'] = 0x2475;
+ t['twoperiod'] = 0x2489;
+ t['twopersian'] = 0x06F2;
+ t['tworoman'] = 0x2171;
+ t['twostroke'] = 0x01BB;
+ t['twosuperior'] = 0x00B2;
+ t['twothai'] = 0x0E52;
+ t['twothirds'] = 0x2154;
+ t['u'] = 0x0075;
+ t['uacute'] = 0x00FA;
+ t['ubar'] = 0x0289;
+ t['ubengali'] = 0x0989;
+ t['ubopomofo'] = 0x3128;
+ t['ubreve'] = 0x016D;
+ t['ucaron'] = 0x01D4;
+ t['ucircle'] = 0x24E4;
+ t['ucircumflex'] = 0x00FB;
+ t['ucircumflexbelow'] = 0x1E77;
+ t['ucyrillic'] = 0x0443;
+ t['udattadeva'] = 0x0951;
+ t['udblacute'] = 0x0171;
+ t['udblgrave'] = 0x0215;
+ t['udeva'] = 0x0909;
+ t['udieresis'] = 0x00FC;
+ t['udieresisacute'] = 0x01D8;
+ t['udieresisbelow'] = 0x1E73;
+ t['udieresiscaron'] = 0x01DA;
+ t['udieresiscyrillic'] = 0x04F1;
+ t['udieresisgrave'] = 0x01DC;
+ t['udieresismacron'] = 0x01D6;
+ t['udotbelow'] = 0x1EE5;
+ t['ugrave'] = 0x00F9;
+ t['ugujarati'] = 0x0A89;
+ t['ugurmukhi'] = 0x0A09;
+ t['uhiragana'] = 0x3046;
+ t['uhookabove'] = 0x1EE7;
+ t['uhorn'] = 0x01B0;
+ t['uhornacute'] = 0x1EE9;
+ t['uhorndotbelow'] = 0x1EF1;
+ t['uhorngrave'] = 0x1EEB;
+ t['uhornhookabove'] = 0x1EED;
+ t['uhorntilde'] = 0x1EEF;
+ t['uhungarumlaut'] = 0x0171;
+ t['uhungarumlautcyrillic'] = 0x04F3;
+ t['uinvertedbreve'] = 0x0217;
+ t['ukatakana'] = 0x30A6;
+ t['ukatakanahalfwidth'] = 0xFF73;
+ t['ukcyrillic'] = 0x0479;
+ t['ukorean'] = 0x315C;
+ t['umacron'] = 0x016B;
+ t['umacroncyrillic'] = 0x04EF;
+ t['umacrondieresis'] = 0x1E7B;
+ t['umatragurmukhi'] = 0x0A41;
+ t['umonospace'] = 0xFF55;
+ t['underscore'] = 0x005F;
+ t['underscoredbl'] = 0x2017;
+ t['underscoremonospace'] = 0xFF3F;
+ t['underscorevertical'] = 0xFE33;
+ t['underscorewavy'] = 0xFE4F;
+ t['union'] = 0x222A;
+ t['universal'] = 0x2200;
+ t['uogonek'] = 0x0173;
+ t['uparen'] = 0x24B0;
+ t['upblock'] = 0x2580;
+ t['upperdothebrew'] = 0x05C4;
+ t['upsilon'] = 0x03C5;
+ t['upsilondieresis'] = 0x03CB;
+ t['upsilondieresistonos'] = 0x03B0;
+ t['upsilonlatin'] = 0x028A;
+ t['upsilontonos'] = 0x03CD;
+ t['uptackbelowcmb'] = 0x031D;
+ t['uptackmod'] = 0x02D4;
+ t['uragurmukhi'] = 0x0A73;
+ t['uring'] = 0x016F;
+ t['ushortcyrillic'] = 0x045E;
+ t['usmallhiragana'] = 0x3045;
+ t['usmallkatakana'] = 0x30A5;
+ t['usmallkatakanahalfwidth'] = 0xFF69;
+ t['ustraightcyrillic'] = 0x04AF;
+ t['ustraightstrokecyrillic'] = 0x04B1;
+ t['utilde'] = 0x0169;
+ t['utildeacute'] = 0x1E79;
+ t['utildebelow'] = 0x1E75;
+ t['uubengali'] = 0x098A;
+ t['uudeva'] = 0x090A;
+ t['uugujarati'] = 0x0A8A;
+ t['uugurmukhi'] = 0x0A0A;
+ t['uumatragurmukhi'] = 0x0A42;
+ t['uuvowelsignbengali'] = 0x09C2;
+ t['uuvowelsigndeva'] = 0x0942;
+ t['uuvowelsigngujarati'] = 0x0AC2;
+ t['uvowelsignbengali'] = 0x09C1;
+ t['uvowelsigndeva'] = 0x0941;
+ t['uvowelsigngujarati'] = 0x0AC1;
+ t['v'] = 0x0076;
+ t['vadeva'] = 0x0935;
+ t['vagujarati'] = 0x0AB5;
+ t['vagurmukhi'] = 0x0A35;
+ t['vakatakana'] = 0x30F7;
+ t['vav'] = 0x05D5;
+ t['vavdagesh'] = 0xFB35;
+ t['vavdagesh65'] = 0xFB35;
+ t['vavdageshhebrew'] = 0xFB35;
+ t['vavhebrew'] = 0x05D5;
+ t['vavholam'] = 0xFB4B;
+ t['vavholamhebrew'] = 0xFB4B;
+ t['vavvavhebrew'] = 0x05F0;
+ t['vavyodhebrew'] = 0x05F1;
+ t['vcircle'] = 0x24E5;
+ t['vdotbelow'] = 0x1E7F;
+ t['vecyrillic'] = 0x0432;
+ t['veharabic'] = 0x06A4;
+ t['vehfinalarabic'] = 0xFB6B;
+ t['vehinitialarabic'] = 0xFB6C;
+ t['vehmedialarabic'] = 0xFB6D;
+ t['vekatakana'] = 0x30F9;
+ t['venus'] = 0x2640;
+ t['verticalbar'] = 0x007C;
+ t['verticallineabovecmb'] = 0x030D;
+ t['verticallinebelowcmb'] = 0x0329;
+ t['verticallinelowmod'] = 0x02CC;
+ t['verticallinemod'] = 0x02C8;
+ t['vewarmenian'] = 0x057E;
+ t['vhook'] = 0x028B;
+ t['vikatakana'] = 0x30F8;
+ t['viramabengali'] = 0x09CD;
+ t['viramadeva'] = 0x094D;
+ t['viramagujarati'] = 0x0ACD;
+ t['visargabengali'] = 0x0983;
+ t['visargadeva'] = 0x0903;
+ t['visargagujarati'] = 0x0A83;
+ t['vmonospace'] = 0xFF56;
+ t['voarmenian'] = 0x0578;
+ t['voicediterationhiragana'] = 0x309E;
+ t['voicediterationkatakana'] = 0x30FE;
+ t['voicedmarkkana'] = 0x309B;
+ t['voicedmarkkanahalfwidth'] = 0xFF9E;
+ t['vokatakana'] = 0x30FA;
+ t['vparen'] = 0x24B1;
+ t['vtilde'] = 0x1E7D;
+ t['vturned'] = 0x028C;
+ t['vuhiragana'] = 0x3094;
+ t['vukatakana'] = 0x30F4;
+ t['w'] = 0x0077;
+ t['wacute'] = 0x1E83;
+ t['waekorean'] = 0x3159;
+ t['wahiragana'] = 0x308F;
+ t['wakatakana'] = 0x30EF;
+ t['wakatakanahalfwidth'] = 0xFF9C;
+ t['wakorean'] = 0x3158;
+ t['wasmallhiragana'] = 0x308E;
+ t['wasmallkatakana'] = 0x30EE;
+ t['wattosquare'] = 0x3357;
+ t['wavedash'] = 0x301C;
+ t['wavyunderscorevertical'] = 0xFE34;
+ t['wawarabic'] = 0x0648;
+ t['wawfinalarabic'] = 0xFEEE;
+ t['wawhamzaabovearabic'] = 0x0624;
+ t['wawhamzaabovefinalarabic'] = 0xFE86;
+ t['wbsquare'] = 0x33DD;
+ t['wcircle'] = 0x24E6;
+ t['wcircumflex'] = 0x0175;
+ t['wdieresis'] = 0x1E85;
+ t['wdotaccent'] = 0x1E87;
+ t['wdotbelow'] = 0x1E89;
+ t['wehiragana'] = 0x3091;
+ t['weierstrass'] = 0x2118;
+ t['wekatakana'] = 0x30F1;
+ t['wekorean'] = 0x315E;
+ t['weokorean'] = 0x315D;
+ t['wgrave'] = 0x1E81;
+ t['whitebullet'] = 0x25E6;
+ t['whitecircle'] = 0x25CB;
+ t['whitecircleinverse'] = 0x25D9;
+ t['whitecornerbracketleft'] = 0x300E;
+ t['whitecornerbracketleftvertical'] = 0xFE43;
+ t['whitecornerbracketright'] = 0x300F;
+ t['whitecornerbracketrightvertical'] = 0xFE44;
+ t['whitediamond'] = 0x25C7;
+ t['whitediamondcontainingblacksmalldiamond'] = 0x25C8;
+ t['whitedownpointingsmalltriangle'] = 0x25BF;
+ t['whitedownpointingtriangle'] = 0x25BD;
+ t['whiteleftpointingsmalltriangle'] = 0x25C3;
+ t['whiteleftpointingtriangle'] = 0x25C1;
+ t['whitelenticularbracketleft'] = 0x3016;
+ t['whitelenticularbracketright'] = 0x3017;
+ t['whiterightpointingsmalltriangle'] = 0x25B9;
+ t['whiterightpointingtriangle'] = 0x25B7;
+ t['whitesmallsquare'] = 0x25AB;
+ t['whitesmilingface'] = 0x263A;
+ t['whitesquare'] = 0x25A1;
+ t['whitestar'] = 0x2606;
+ t['whitetelephone'] = 0x260F;
+ t['whitetortoiseshellbracketleft'] = 0x3018;
+ t['whitetortoiseshellbracketright'] = 0x3019;
+ t['whiteuppointingsmalltriangle'] = 0x25B5;
+ t['whiteuppointingtriangle'] = 0x25B3;
+ t['wihiragana'] = 0x3090;
+ t['wikatakana'] = 0x30F0;
+ t['wikorean'] = 0x315F;
+ t['wmonospace'] = 0xFF57;
+ t['wohiragana'] = 0x3092;
+ t['wokatakana'] = 0x30F2;
+ t['wokatakanahalfwidth'] = 0xFF66;
+ t['won'] = 0x20A9;
+ t['wonmonospace'] = 0xFFE6;
+ t['wowaenthai'] = 0x0E27;
+ t['wparen'] = 0x24B2;
+ t['wring'] = 0x1E98;
+ t['wsuperior'] = 0x02B7;
+ t['wturned'] = 0x028D;
+ t['wynn'] = 0x01BF;
+ t['x'] = 0x0078;
+ t['xabovecmb'] = 0x033D;
+ t['xbopomofo'] = 0x3112;
+ t['xcircle'] = 0x24E7;
+ t['xdieresis'] = 0x1E8D;
+ t['xdotaccent'] = 0x1E8B;
+ t['xeharmenian'] = 0x056D;
+ t['xi'] = 0x03BE;
+ t['xmonospace'] = 0xFF58;
+ t['xparen'] = 0x24B3;
+ t['xsuperior'] = 0x02E3;
+ t['y'] = 0x0079;
+ t['yaadosquare'] = 0x334E;
+ t['yabengali'] = 0x09AF;
+ t['yacute'] = 0x00FD;
+ t['yadeva'] = 0x092F;
+ t['yaekorean'] = 0x3152;
+ t['yagujarati'] = 0x0AAF;
+ t['yagurmukhi'] = 0x0A2F;
+ t['yahiragana'] = 0x3084;
+ t['yakatakana'] = 0x30E4;
+ t['yakatakanahalfwidth'] = 0xFF94;
+ t['yakorean'] = 0x3151;
+ t['yamakkanthai'] = 0x0E4E;
+ t['yasmallhiragana'] = 0x3083;
+ t['yasmallkatakana'] = 0x30E3;
+ t['yasmallkatakanahalfwidth'] = 0xFF6C;
+ t['yatcyrillic'] = 0x0463;
+ t['ycircle'] = 0x24E8;
+ t['ycircumflex'] = 0x0177;
+ t['ydieresis'] = 0x00FF;
+ t['ydotaccent'] = 0x1E8F;
+ t['ydotbelow'] = 0x1EF5;
+ t['yeharabic'] = 0x064A;
+ t['yehbarreearabic'] = 0x06D2;
+ t['yehbarreefinalarabic'] = 0xFBAF;
+ t['yehfinalarabic'] = 0xFEF2;
+ t['yehhamzaabovearabic'] = 0x0626;
+ t['yehhamzaabovefinalarabic'] = 0xFE8A;
+ t['yehhamzaaboveinitialarabic'] = 0xFE8B;
+ t['yehhamzaabovemedialarabic'] = 0xFE8C;
+ t['yehinitialarabic'] = 0xFEF3;
+ t['yehmedialarabic'] = 0xFEF4;
+ t['yehmeeminitialarabic'] = 0xFCDD;
+ t['yehmeemisolatedarabic'] = 0xFC58;
+ t['yehnoonfinalarabic'] = 0xFC94;
+ t['yehthreedotsbelowarabic'] = 0x06D1;
+ t['yekorean'] = 0x3156;
+ t['yen'] = 0x00A5;
+ t['yenmonospace'] = 0xFFE5;
+ t['yeokorean'] = 0x3155;
+ t['yeorinhieuhkorean'] = 0x3186;
+ t['yerahbenyomohebrew'] = 0x05AA;
+ t['yerahbenyomolefthebrew'] = 0x05AA;
+ t['yericyrillic'] = 0x044B;
+ t['yerudieresiscyrillic'] = 0x04F9;
+ t['yesieungkorean'] = 0x3181;
+ t['yesieungpansioskorean'] = 0x3183;
+ t['yesieungsioskorean'] = 0x3182;
+ t['yetivhebrew'] = 0x059A;
+ t['ygrave'] = 0x1EF3;
+ t['yhook'] = 0x01B4;
+ t['yhookabove'] = 0x1EF7;
+ t['yiarmenian'] = 0x0575;
+ t['yicyrillic'] = 0x0457;
+ t['yikorean'] = 0x3162;
+ t['yinyang'] = 0x262F;
+ t['yiwnarmenian'] = 0x0582;
+ t['ymonospace'] = 0xFF59;
+ t['yod'] = 0x05D9;
+ t['yoddagesh'] = 0xFB39;
+ t['yoddageshhebrew'] = 0xFB39;
+ t['yodhebrew'] = 0x05D9;
+ t['yodyodhebrew'] = 0x05F2;
+ t['yodyodpatahhebrew'] = 0xFB1F;
+ t['yohiragana'] = 0x3088;
+ t['yoikorean'] = 0x3189;
+ t['yokatakana'] = 0x30E8;
+ t['yokatakanahalfwidth'] = 0xFF96;
+ t['yokorean'] = 0x315B;
+ t['yosmallhiragana'] = 0x3087;
+ t['yosmallkatakana'] = 0x30E7;
+ t['yosmallkatakanahalfwidth'] = 0xFF6E;
+ t['yotgreek'] = 0x03F3;
+ t['yoyaekorean'] = 0x3188;
+ t['yoyakorean'] = 0x3187;
+ t['yoyakthai'] = 0x0E22;
+ t['yoyingthai'] = 0x0E0D;
+ t['yparen'] = 0x24B4;
+ t['ypogegrammeni'] = 0x037A;
+ t['ypogegrammenigreekcmb'] = 0x0345;
+ t['yr'] = 0x01A6;
+ t['yring'] = 0x1E99;
+ t['ysuperior'] = 0x02B8;
+ t['ytilde'] = 0x1EF9;
+ t['yturned'] = 0x028E;
+ t['yuhiragana'] = 0x3086;
+ t['yuikorean'] = 0x318C;
+ t['yukatakana'] = 0x30E6;
+ t['yukatakanahalfwidth'] = 0xFF95;
+ t['yukorean'] = 0x3160;
+ t['yusbigcyrillic'] = 0x046B;
+ t['yusbigiotifiedcyrillic'] = 0x046D;
+ t['yuslittlecyrillic'] = 0x0467;
+ t['yuslittleiotifiedcyrillic'] = 0x0469;
+ t['yusmallhiragana'] = 0x3085;
+ t['yusmallkatakana'] = 0x30E5;
+ t['yusmallkatakanahalfwidth'] = 0xFF6D;
+ t['yuyekorean'] = 0x318B;
+ t['yuyeokorean'] = 0x318A;
+ t['yyabengali'] = 0x09DF;
+ t['yyadeva'] = 0x095F;
+ t['z'] = 0x007A;
+ t['zaarmenian'] = 0x0566;
+ t['zacute'] = 0x017A;
+ t['zadeva'] = 0x095B;
+ t['zagurmukhi'] = 0x0A5B;
+ t['zaharabic'] = 0x0638;
+ t['zahfinalarabic'] = 0xFEC6;
+ t['zahinitialarabic'] = 0xFEC7;
+ t['zahiragana'] = 0x3056;
+ t['zahmedialarabic'] = 0xFEC8;
+ t['zainarabic'] = 0x0632;
+ t['zainfinalarabic'] = 0xFEB0;
+ t['zakatakana'] = 0x30B6;
+ t['zaqefgadolhebrew'] = 0x0595;
+ t['zaqefqatanhebrew'] = 0x0594;
+ t['zarqahebrew'] = 0x0598;
+ t['zayin'] = 0x05D6;
+ t['zayindagesh'] = 0xFB36;
+ t['zayindageshhebrew'] = 0xFB36;
+ t['zayinhebrew'] = 0x05D6;
+ t['zbopomofo'] = 0x3117;
+ t['zcaron'] = 0x017E;
+ t['zcircle'] = 0x24E9;
+ t['zcircumflex'] = 0x1E91;
+ t['zcurl'] = 0x0291;
+ t['zdot'] = 0x017C;
+ t['zdotaccent'] = 0x017C;
+ t['zdotbelow'] = 0x1E93;
+ t['zecyrillic'] = 0x0437;
+ t['zedescendercyrillic'] = 0x0499;
+ t['zedieresiscyrillic'] = 0x04DF;
+ t['zehiragana'] = 0x305C;
+ t['zekatakana'] = 0x30BC;
+ t['zero'] = 0x0030;
+ t['zeroarabic'] = 0x0660;
+ t['zerobengali'] = 0x09E6;
+ t['zerodeva'] = 0x0966;
+ t['zerogujarati'] = 0x0AE6;
+ t['zerogurmukhi'] = 0x0A66;
+ t['zerohackarabic'] = 0x0660;
+ t['zeroinferior'] = 0x2080;
+ t['zeromonospace'] = 0xFF10;
+ t['zerooldstyle'] = 0xF730;
+ t['zeropersian'] = 0x06F0;
+ t['zerosuperior'] = 0x2070;
+ t['zerothai'] = 0x0E50;
+ t['zerowidthjoiner'] = 0xFEFF;
+ t['zerowidthnonjoiner'] = 0x200C;
+ t['zerowidthspace'] = 0x200B;
+ t['zeta'] = 0x03B6;
+ t['zhbopomofo'] = 0x3113;
+ t['zhearmenian'] = 0x056A;
+ t['zhebrevecyrillic'] = 0x04C2;
+ t['zhecyrillic'] = 0x0436;
+ t['zhedescendercyrillic'] = 0x0497;
+ t['zhedieresiscyrillic'] = 0x04DD;
+ t['zihiragana'] = 0x3058;
+ t['zikatakana'] = 0x30B8;
+ t['zinorhebrew'] = 0x05AE;
+ t['zlinebelow'] = 0x1E95;
+ t['zmonospace'] = 0xFF5A;
+ t['zohiragana'] = 0x305E;
+ t['zokatakana'] = 0x30BE;
+ t['zparen'] = 0x24B5;
+ t['zretroflexhook'] = 0x0290;
+ t['zstroke'] = 0x01B6;
+ t['zuhiragana'] = 0x305A;
+ t['zukatakana'] = 0x30BA;
+ t['.notdef'] = 0x0000;
+ t['angbracketleftbig'] = 0x2329;
+ t['angbracketleftBig'] = 0x2329;
+ t['angbracketleftbigg'] = 0x2329;
+ t['angbracketleftBigg'] = 0x2329;
+ t['angbracketrightBig'] = 0x232A;
+ t['angbracketrightbig'] = 0x232A;
+ t['angbracketrightBigg'] = 0x232A;
+ t['angbracketrightbigg'] = 0x232A;
+ t['arrowhookleft'] = 0x21AA;
+ t['arrowhookright'] = 0x21A9;
+ t['arrowlefttophalf'] = 0x21BC;
+ t['arrowleftbothalf'] = 0x21BD;
+ t['arrownortheast'] = 0x2197;
+ t['arrownorthwest'] = 0x2196;
+ t['arrowrighttophalf'] = 0x21C0;
+ t['arrowrightbothalf'] = 0x21C1;
+ t['arrowsoutheast'] = 0x2198;
+ t['arrowsouthwest'] = 0x2199;
+ t['backslashbig'] = 0x2216;
+ t['backslashBig'] = 0x2216;
+ t['backslashBigg'] = 0x2216;
+ t['backslashbigg'] = 0x2216;
+ t['bardbl'] = 0x2016;
+ t['bracehtipdownleft'] = 0xFE37;
+ t['bracehtipdownright'] = 0xFE37;
+ t['bracehtipupleft'] = 0xFE38;
+ t['bracehtipupright'] = 0xFE38;
+ t['braceleftBig'] = 0x007B;
+ t['braceleftbig'] = 0x007B;
+ t['braceleftbigg'] = 0x007B;
+ t['braceleftBigg'] = 0x007B;
+ t['bracerightBig'] = 0x007D;
+ t['bracerightbig'] = 0x007D;
+ t['bracerightbigg'] = 0x007D;
+ t['bracerightBigg'] = 0x007D;
+ t['bracketleftbig'] = 0x005B;
+ t['bracketleftBig'] = 0x005B;
+ t['bracketleftbigg'] = 0x005B;
+ t['bracketleftBigg'] = 0x005B;
+ t['bracketrightBig'] = 0x005D;
+ t['bracketrightbig'] = 0x005D;
+ t['bracketrightbigg'] = 0x005D;
+ t['bracketrightBigg'] = 0x005D;
+ t['ceilingleftbig'] = 0x2308;
+ t['ceilingleftBig'] = 0x2308;
+ t['ceilingleftBigg'] = 0x2308;
+ t['ceilingleftbigg'] = 0x2308;
+ t['ceilingrightbig'] = 0x2309;
+ t['ceilingrightBig'] = 0x2309;
+ t['ceilingrightbigg'] = 0x2309;
+ t['ceilingrightBigg'] = 0x2309;
+ t['circledotdisplay'] = 0x2299;
+ t['circledottext'] = 0x2299;
+ t['circlemultiplydisplay'] = 0x2297;
+ t['circlemultiplytext'] = 0x2297;
+ t['circleplusdisplay'] = 0x2295;
+ t['circleplustext'] = 0x2295;
+ t['contintegraldisplay'] = 0x222E;
+ t['contintegraltext'] = 0x222E;
+ t['coproductdisplay'] = 0x2210;
+ t['coproducttext'] = 0x2210;
+ t['floorleftBig'] = 0x230A;
+ t['floorleftbig'] = 0x230A;
+ t['floorleftbigg'] = 0x230A;
+ t['floorleftBigg'] = 0x230A;
+ t['floorrightbig'] = 0x230B;
+ t['floorrightBig'] = 0x230B;
+ t['floorrightBigg'] = 0x230B;
+ t['floorrightbigg'] = 0x230B;
+ t['hatwide'] = 0x0302;
+ t['hatwider'] = 0x0302;
+ t['hatwidest'] = 0x0302;
+ t['intercal'] = 0x1D40;
+ t['integraldisplay'] = 0x222B;
+ t['integraltext'] = 0x222B;
+ t['intersectiondisplay'] = 0x22C2;
+ t['intersectiontext'] = 0x22C2;
+ t['logicalanddisplay'] = 0x2227;
+ t['logicalandtext'] = 0x2227;
+ t['logicalordisplay'] = 0x2228;
+ t['logicalortext'] = 0x2228;
+ t['parenleftBig'] = 0x0028;
+ t['parenleftbig'] = 0x0028;
+ t['parenleftBigg'] = 0x0028;
+ t['parenleftbigg'] = 0x0028;
+ t['parenrightBig'] = 0x0029;
+ t['parenrightbig'] = 0x0029;
+ t['parenrightBigg'] = 0x0029;
+ t['parenrightbigg'] = 0x0029;
+ t['prime'] = 0x2032;
+ t['productdisplay'] = 0x220F;
+ t['producttext'] = 0x220F;
+ t['radicalbig'] = 0x221A;
+ t['radicalBig'] = 0x221A;
+ t['radicalBigg'] = 0x221A;
+ t['radicalbigg'] = 0x221A;
+ t['radicalbt'] = 0x221A;
+ t['radicaltp'] = 0x221A;
+ t['radicalvertex'] = 0x221A;
+ t['slashbig'] = 0x002F;
+ t['slashBig'] = 0x002F;
+ t['slashBigg'] = 0x002F;
+ t['slashbigg'] = 0x002F;
+ t['summationdisplay'] = 0x2211;
+ t['summationtext'] = 0x2211;
+ t['tildewide'] = 0x02DC;
+ t['tildewider'] = 0x02DC;
+ t['tildewidest'] = 0x02DC;
+ t['uniondisplay'] = 0x22C3;
+ t['unionmultidisplay'] = 0x228E;
+ t['unionmultitext'] = 0x228E;
+ t['unionsqdisplay'] = 0x2294;
+ t['unionsqtext'] = 0x2294;
+ t['uniontext'] = 0x22C3;
+ t['vextenddouble'] = 0x2225;
+ t['vextendsingle'] = 0x2223;
+});
+var getDingbatsGlyphsUnicode = getLookupTableFactory(function (t) {
+ t['space'] = 0x0020;
+ t['a1'] = 0x2701;
+ t['a2'] = 0x2702;
+ t['a202'] = 0x2703;
+ t['a3'] = 0x2704;
+ t['a4'] = 0x260E;
+ t['a5'] = 0x2706;
+ t['a119'] = 0x2707;
+ t['a118'] = 0x2708;
+ t['a117'] = 0x2709;
+ t['a11'] = 0x261B;
+ t['a12'] = 0x261E;
+ t['a13'] = 0x270C;
+ t['a14'] = 0x270D;
+ t['a15'] = 0x270E;
+ t['a16'] = 0x270F;
+ t['a105'] = 0x2710;
+ t['a17'] = 0x2711;
+ t['a18'] = 0x2712;
+ t['a19'] = 0x2713;
+ t['a20'] = 0x2714;
+ t['a21'] = 0x2715;
+ t['a22'] = 0x2716;
+ t['a23'] = 0x2717;
+ t['a24'] = 0x2718;
+ t['a25'] = 0x2719;
+ t['a26'] = 0x271A;
+ t['a27'] = 0x271B;
+ t['a28'] = 0x271C;
+ t['a6'] = 0x271D;
+ t['a7'] = 0x271E;
+ t['a8'] = 0x271F;
+ t['a9'] = 0x2720;
+ t['a10'] = 0x2721;
+ t['a29'] = 0x2722;
+ t['a30'] = 0x2723;
+ t['a31'] = 0x2724;
+ t['a32'] = 0x2725;
+ t['a33'] = 0x2726;
+ t['a34'] = 0x2727;
+ t['a35'] = 0x2605;
+ t['a36'] = 0x2729;
+ t['a37'] = 0x272A;
+ t['a38'] = 0x272B;
+ t['a39'] = 0x272C;
+ t['a40'] = 0x272D;
+ t['a41'] = 0x272E;
+ t['a42'] = 0x272F;
+ t['a43'] = 0x2730;
+ t['a44'] = 0x2731;
+ t['a45'] = 0x2732;
+ t['a46'] = 0x2733;
+ t['a47'] = 0x2734;
+ t['a48'] = 0x2735;
+ t['a49'] = 0x2736;
+ t['a50'] = 0x2737;
+ t['a51'] = 0x2738;
+ t['a52'] = 0x2739;
+ t['a53'] = 0x273A;
+ t['a54'] = 0x273B;
+ t['a55'] = 0x273C;
+ t['a56'] = 0x273D;
+ t['a57'] = 0x273E;
+ t['a58'] = 0x273F;
+ t['a59'] = 0x2740;
+ t['a60'] = 0x2741;
+ t['a61'] = 0x2742;
+ t['a62'] = 0x2743;
+ t['a63'] = 0x2744;
+ t['a64'] = 0x2745;
+ t['a65'] = 0x2746;
+ t['a66'] = 0x2747;
+ t['a67'] = 0x2748;
+ t['a68'] = 0x2749;
+ t['a69'] = 0x274A;
+ t['a70'] = 0x274B;
+ t['a71'] = 0x25CF;
+ t['a72'] = 0x274D;
+ t['a73'] = 0x25A0;
+ t['a74'] = 0x274F;
+ t['a203'] = 0x2750;
+ t['a75'] = 0x2751;
+ t['a204'] = 0x2752;
+ t['a76'] = 0x25B2;
+ t['a77'] = 0x25BC;
+ t['a78'] = 0x25C6;
+ t['a79'] = 0x2756;
+ t['a81'] = 0x25D7;
+ t['a82'] = 0x2758;
+ t['a83'] = 0x2759;
+ t['a84'] = 0x275A;
+ t['a97'] = 0x275B;
+ t['a98'] = 0x275C;
+ t['a99'] = 0x275D;
+ t['a100'] = 0x275E;
+ t['a101'] = 0x2761;
+ t['a102'] = 0x2762;
+ t['a103'] = 0x2763;
+ t['a104'] = 0x2764;
+ t['a106'] = 0x2765;
+ t['a107'] = 0x2766;
+ t['a108'] = 0x2767;
+ t['a112'] = 0x2663;
+ t['a111'] = 0x2666;
+ t['a110'] = 0x2665;
+ t['a109'] = 0x2660;
+ t['a120'] = 0x2460;
+ t['a121'] = 0x2461;
+ t['a122'] = 0x2462;
+ t['a123'] = 0x2463;
+ t['a124'] = 0x2464;
+ t['a125'] = 0x2465;
+ t['a126'] = 0x2466;
+ t['a127'] = 0x2467;
+ t['a128'] = 0x2468;
+ t['a129'] = 0x2469;
+ t['a130'] = 0x2776;
+ t['a131'] = 0x2777;
+ t['a132'] = 0x2778;
+ t['a133'] = 0x2779;
+ t['a134'] = 0x277A;
+ t['a135'] = 0x277B;
+ t['a136'] = 0x277C;
+ t['a137'] = 0x277D;
+ t['a138'] = 0x277E;
+ t['a139'] = 0x277F;
+ t['a140'] = 0x2780;
+ t['a141'] = 0x2781;
+ t['a142'] = 0x2782;
+ t['a143'] = 0x2783;
+ t['a144'] = 0x2784;
+ t['a145'] = 0x2785;
+ t['a146'] = 0x2786;
+ t['a147'] = 0x2787;
+ t['a148'] = 0x2788;
+ t['a149'] = 0x2789;
+ t['a150'] = 0x278A;
+ t['a151'] = 0x278B;
+ t['a152'] = 0x278C;
+ t['a153'] = 0x278D;
+ t['a154'] = 0x278E;
+ t['a155'] = 0x278F;
+ t['a156'] = 0x2790;
+ t['a157'] = 0x2791;
+ t['a158'] = 0x2792;
+ t['a159'] = 0x2793;
+ t['a160'] = 0x2794;
+ t['a161'] = 0x2192;
+ t['a163'] = 0x2194;
+ t['a164'] = 0x2195;
+ t['a196'] = 0x2798;
+ t['a165'] = 0x2799;
+ t['a192'] = 0x279A;
+ t['a166'] = 0x279B;
+ t['a167'] = 0x279C;
+ t['a168'] = 0x279D;
+ t['a169'] = 0x279E;
+ t['a170'] = 0x279F;
+ t['a171'] = 0x27A0;
+ t['a172'] = 0x27A1;
+ t['a173'] = 0x27A2;
+ t['a162'] = 0x27A3;
+ t['a174'] = 0x27A4;
+ t['a175'] = 0x27A5;
+ t['a176'] = 0x27A6;
+ t['a177'] = 0x27A7;
+ t['a178'] = 0x27A8;
+ t['a179'] = 0x27A9;
+ t['a193'] = 0x27AA;
+ t['a180'] = 0x27AB;
+ t['a199'] = 0x27AC;
+ t['a181'] = 0x27AD;
+ t['a200'] = 0x27AE;
+ t['a182'] = 0x27AF;
+ t['a201'] = 0x27B1;
+ t['a183'] = 0x27B2;
+ t['a184'] = 0x27B3;
+ t['a197'] = 0x27B4;
+ t['a185'] = 0x27B5;
+ t['a194'] = 0x27B6;
+ t['a198'] = 0x27B7;
+ t['a186'] = 0x27B8;
+ t['a195'] = 0x27B9;
+ t['a187'] = 0x27BA;
+ t['a188'] = 0x27BB;
+ t['a189'] = 0x27BC;
+ t['a190'] = 0x27BD;
+ t['a191'] = 0x27BE;
+ t['a89'] = 0x2768;
+ t['a90'] = 0x2769;
+ t['a93'] = 0x276A;
+ t['a94'] = 0x276B;
+ t['a91'] = 0x276C;
+ t['a92'] = 0x276D;
+ t['a205'] = 0x276E;
+ t['a85'] = 0x276F;
+ t['a206'] = 0x2770;
+ t['a86'] = 0x2771;
+ t['a87'] = 0x2772;
+ t['a88'] = 0x2773;
+ t['a95'] = 0x2774;
+ t['a96'] = 0x2775;
+ t['.notdef'] = 0x0000;
+});
+exports.getGlyphsUnicode = getGlyphsUnicode;
+exports.getDingbatsGlyphsUnicode = getDingbatsGlyphsUnicode;
+
+/***/ }),
+/* 179 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.getSupplementalGlyphMapForCalibri = exports.getSupplementalGlyphMapForArialBlack = exports.getGlyphMapForStandardFonts = exports.getSymbolsFonts = exports.getSerifFonts = exports.getNonStdFontMap = exports.getStdFontMap = void 0;
+
+var _core_utils = __w_pdfjs_require__(154);
+
+var getStdFontMap = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['ArialNarrow'] = 'Helvetica';
+ t['ArialNarrow-Bold'] = 'Helvetica-Bold';
+ t['ArialNarrow-BoldItalic'] = 'Helvetica-BoldOblique';
+ t['ArialNarrow-Italic'] = 'Helvetica-Oblique';
+ t['ArialBlack'] = 'Helvetica';
+ t['ArialBlack-Bold'] = 'Helvetica-Bold';
+ t['ArialBlack-BoldItalic'] = 'Helvetica-BoldOblique';
+ t['ArialBlack-Italic'] = 'Helvetica-Oblique';
+ t['Arial-Black'] = 'Helvetica';
+ t['Arial-Black-Bold'] = 'Helvetica-Bold';
+ t['Arial-Black-BoldItalic'] = 'Helvetica-BoldOblique';
+ t['Arial-Black-Italic'] = 'Helvetica-Oblique';
+ t['Arial'] = 'Helvetica';
+ t['Arial-Bold'] = 'Helvetica-Bold';
+ t['Arial-BoldItalic'] = 'Helvetica-BoldOblique';
+ t['Arial-Italic'] = 'Helvetica-Oblique';
+ t['Arial-BoldItalicMT'] = 'Helvetica-BoldOblique';
+ t['Arial-BoldMT'] = 'Helvetica-Bold';
+ t['Arial-ItalicMT'] = 'Helvetica-Oblique';
+ t['ArialMT'] = 'Helvetica';
+ t['Courier-Bold'] = 'Courier-Bold';
+ t['Courier-BoldItalic'] = 'Courier-BoldOblique';
+ t['Courier-Italic'] = 'Courier-Oblique';
+ t['CourierNew'] = 'Courier';
+ t['CourierNew-Bold'] = 'Courier-Bold';
+ t['CourierNew-BoldItalic'] = 'Courier-BoldOblique';
+ t['CourierNew-Italic'] = 'Courier-Oblique';
+ t['CourierNewPS-BoldItalicMT'] = 'Courier-BoldOblique';
+ t['CourierNewPS-BoldMT'] = 'Courier-Bold';
+ t['CourierNewPS-ItalicMT'] = 'Courier-Oblique';
+ t['CourierNewPSMT'] = 'Courier';
+ t['Helvetica'] = 'Helvetica';
+ t['Helvetica-Bold'] = 'Helvetica-Bold';
+ t['Helvetica-BoldItalic'] = 'Helvetica-BoldOblique';
+ t['Helvetica-BoldOblique'] = 'Helvetica-BoldOblique';
+ t['Helvetica-Italic'] = 'Helvetica-Oblique';
+ t['Helvetica-Oblique'] = 'Helvetica-Oblique';
+ t['SegoeUISymbol'] = 'Helvetica';
+ t['Symbol-Bold'] = 'Symbol';
+ t['Symbol-BoldItalic'] = 'Symbol';
+ t['Symbol-Italic'] = 'Symbol';
+ t['TimesNewRoman'] = 'Times-Roman';
+ t['TimesNewRoman-Bold'] = 'Times-Bold';
+ t['TimesNewRoman-BoldItalic'] = 'Times-BoldItalic';
+ t['TimesNewRoman-Italic'] = 'Times-Italic';
+ t['TimesNewRomanPS'] = 'Times-Roman';
+ t['TimesNewRomanPS-Bold'] = 'Times-Bold';
+ t['TimesNewRomanPS-BoldItalic'] = 'Times-BoldItalic';
+ t['TimesNewRomanPS-BoldItalicMT'] = 'Times-BoldItalic';
+ t['TimesNewRomanPS-BoldMT'] = 'Times-Bold';
+ t['TimesNewRomanPS-Italic'] = 'Times-Italic';
+ t['TimesNewRomanPS-ItalicMT'] = 'Times-Italic';
+ t['TimesNewRomanPSMT'] = 'Times-Roman';
+ t['TimesNewRomanPSMT-Bold'] = 'Times-Bold';
+ t['TimesNewRomanPSMT-BoldItalic'] = 'Times-BoldItalic';
+ t['TimesNewRomanPSMT-Italic'] = 'Times-Italic';
+});
+exports.getStdFontMap = getStdFontMap;
+var getNonStdFontMap = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['Calibri'] = 'Helvetica';
+ t['Calibri-Bold'] = 'Helvetica-Bold';
+ t['Calibri-BoldItalic'] = 'Helvetica-BoldOblique';
+ t['Calibri-Italic'] = 'Helvetica-Oblique';
+ t['CenturyGothic'] = 'Helvetica';
+ t['CenturyGothic-Bold'] = 'Helvetica-Bold';
+ t['CenturyGothic-BoldItalic'] = 'Helvetica-BoldOblique';
+ t['CenturyGothic-Italic'] = 'Helvetica-Oblique';
+ t['ComicSansMS'] = 'Comic Sans MS';
+ t['ComicSansMS-Bold'] = 'Comic Sans MS-Bold';
+ t['ComicSansMS-BoldItalic'] = 'Comic Sans MS-BoldItalic';
+ t['ComicSansMS-Italic'] = 'Comic Sans MS-Italic';
+ t['LucidaConsole'] = 'Courier';
+ t['LucidaConsole-Bold'] = 'Courier-Bold';
+ t['LucidaConsole-BoldItalic'] = 'Courier-BoldOblique';
+ t['LucidaConsole-Italic'] = 'Courier-Oblique';
+ t['LucidaSans-Demi'] = 'Helvetica-Bold';
+ t['MS-Gothic'] = 'MS Gothic';
+ t['MS-Gothic-Bold'] = 'MS Gothic-Bold';
+ t['MS-Gothic-BoldItalic'] = 'MS Gothic-BoldItalic';
+ t['MS-Gothic-Italic'] = 'MS Gothic-Italic';
+ t['MS-Mincho'] = 'MS Mincho';
+ t['MS-Mincho-Bold'] = 'MS Mincho-Bold';
+ t['MS-Mincho-BoldItalic'] = 'MS Mincho-BoldItalic';
+ t['MS-Mincho-Italic'] = 'MS Mincho-Italic';
+ t['MS-PGothic'] = 'MS PGothic';
+ t['MS-PGothic-Bold'] = 'MS PGothic-Bold';
+ t['MS-PGothic-BoldItalic'] = 'MS PGothic-BoldItalic';
+ t['MS-PGothic-Italic'] = 'MS PGothic-Italic';
+ t['MS-PMincho'] = 'MS PMincho';
+ t['MS-PMincho-Bold'] = 'MS PMincho-Bold';
+ t['MS-PMincho-BoldItalic'] = 'MS PMincho-BoldItalic';
+ t['MS-PMincho-Italic'] = 'MS PMincho-Italic';
+ t['NuptialScript'] = 'Times-Italic';
+ t['Wingdings'] = 'ZapfDingbats';
+});
+exports.getNonStdFontMap = getNonStdFontMap;
+var getSerifFonts = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['Adobe Jenson'] = true;
+ t['Adobe Text'] = true;
+ t['Albertus'] = true;
+ t['Aldus'] = true;
+ t['Alexandria'] = true;
+ t['Algerian'] = true;
+ t['American Typewriter'] = true;
+ t['Antiqua'] = true;
+ t['Apex'] = true;
+ t['Arno'] = true;
+ t['Aster'] = true;
+ t['Aurora'] = true;
+ t['Baskerville'] = true;
+ t['Bell'] = true;
+ t['Bembo'] = true;
+ t['Bembo Schoolbook'] = true;
+ t['Benguiat'] = true;
+ t['Berkeley Old Style'] = true;
+ t['Bernhard Modern'] = true;
+ t['Berthold City'] = true;
+ t['Bodoni'] = true;
+ t['Bauer Bodoni'] = true;
+ t['Book Antiqua'] = true;
+ t['Bookman'] = true;
+ t['Bordeaux Roman'] = true;
+ t['Californian FB'] = true;
+ t['Calisto'] = true;
+ t['Calvert'] = true;
+ t['Capitals'] = true;
+ t['Cambria'] = true;
+ t['Cartier'] = true;
+ t['Caslon'] = true;
+ t['Catull'] = true;
+ t['Centaur'] = true;
+ t['Century Old Style'] = true;
+ t['Century Schoolbook'] = true;
+ t['Chaparral'] = true;
+ t['Charis SIL'] = true;
+ t['Cheltenham'] = true;
+ t['Cholla Slab'] = true;
+ t['Clarendon'] = true;
+ t['Clearface'] = true;
+ t['Cochin'] = true;
+ t['Colonna'] = true;
+ t['Computer Modern'] = true;
+ t['Concrete Roman'] = true;
+ t['Constantia'] = true;
+ t['Cooper Black'] = true;
+ t['Corona'] = true;
+ t['Ecotype'] = true;
+ t['Egyptienne'] = true;
+ t['Elephant'] = true;
+ t['Excelsior'] = true;
+ t['Fairfield'] = true;
+ t['FF Scala'] = true;
+ t['Folkard'] = true;
+ t['Footlight'] = true;
+ t['FreeSerif'] = true;
+ t['Friz Quadrata'] = true;
+ t['Garamond'] = true;
+ t['Gentium'] = true;
+ t['Georgia'] = true;
+ t['Gloucester'] = true;
+ t['Goudy Old Style'] = true;
+ t['Goudy Schoolbook'] = true;
+ t['Goudy Pro Font'] = true;
+ t['Granjon'] = true;
+ t['Guardian Egyptian'] = true;
+ t['Heather'] = true;
+ t['Hercules'] = true;
+ t['High Tower Text'] = true;
+ t['Hiroshige'] = true;
+ t['Hoefler Text'] = true;
+ t['Humana Serif'] = true;
+ t['Imprint'] = true;
+ t['Ionic No. 5'] = true;
+ t['Janson'] = true;
+ t['Joanna'] = true;
+ t['Korinna'] = true;
+ t['Lexicon'] = true;
+ t['Liberation Serif'] = true;
+ t['Linux Libertine'] = true;
+ t['Literaturnaya'] = true;
+ t['Lucida'] = true;
+ t['Lucida Bright'] = true;
+ t['Melior'] = true;
+ t['Memphis'] = true;
+ t['Miller'] = true;
+ t['Minion'] = true;
+ t['Modern'] = true;
+ t['Mona Lisa'] = true;
+ t['Mrs Eaves'] = true;
+ t['MS Serif'] = true;
+ t['Museo Slab'] = true;
+ t['New York'] = true;
+ t['Nimbus Roman'] = true;
+ t['NPS Rawlinson Roadway'] = true;
+ t['NuptialScript'] = true;
+ t['Palatino'] = true;
+ t['Perpetua'] = true;
+ t['Plantin'] = true;
+ t['Plantin Schoolbook'] = true;
+ t['Playbill'] = true;
+ t['Poor Richard'] = true;
+ t['Rawlinson Roadway'] = true;
+ t['Renault'] = true;
+ t['Requiem'] = true;
+ t['Rockwell'] = true;
+ t['Roman'] = true;
+ t['Rotis Serif'] = true;
+ t['Sabon'] = true;
+ t['Scala'] = true;
+ t['Seagull'] = true;
+ t['Sistina'] = true;
+ t['Souvenir'] = true;
+ t['STIX'] = true;
+ t['Stone Informal'] = true;
+ t['Stone Serif'] = true;
+ t['Sylfaen'] = true;
+ t['Times'] = true;
+ t['Trajan'] = true;
+ t['Trinité'] = true;
+ t['Trump Mediaeval'] = true;
+ t['Utopia'] = true;
+ t['Vale Type'] = true;
+ t['Bitstream Vera'] = true;
+ t['Vera Serif'] = true;
+ t['Versailles'] = true;
+ t['Wanted'] = true;
+ t['Weiss'] = true;
+ t['Wide Latin'] = true;
+ t['Windsor'] = true;
+ t['XITS'] = true;
+});
+exports.getSerifFonts = getSerifFonts;
+var getSymbolsFonts = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['Dingbats'] = true;
+ t['Symbol'] = true;
+ t['ZapfDingbats'] = true;
+});
+exports.getSymbolsFonts = getSymbolsFonts;
+var getGlyphMapForStandardFonts = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t[2] = 10;
+ t[3] = 32;
+ t[4] = 33;
+ t[5] = 34;
+ t[6] = 35;
+ t[7] = 36;
+ t[8] = 37;
+ t[9] = 38;
+ t[10] = 39;
+ t[11] = 40;
+ t[12] = 41;
+ t[13] = 42;
+ t[14] = 43;
+ t[15] = 44;
+ t[16] = 45;
+ t[17] = 46;
+ t[18] = 47;
+ t[19] = 48;
+ t[20] = 49;
+ t[21] = 50;
+ t[22] = 51;
+ t[23] = 52;
+ t[24] = 53;
+ t[25] = 54;
+ t[26] = 55;
+ t[27] = 56;
+ t[28] = 57;
+ t[29] = 58;
+ t[30] = 894;
+ t[31] = 60;
+ t[32] = 61;
+ t[33] = 62;
+ t[34] = 63;
+ t[35] = 64;
+ t[36] = 65;
+ t[37] = 66;
+ t[38] = 67;
+ t[39] = 68;
+ t[40] = 69;
+ t[41] = 70;
+ t[42] = 71;
+ t[43] = 72;
+ t[44] = 73;
+ t[45] = 74;
+ t[46] = 75;
+ t[47] = 76;
+ t[48] = 77;
+ t[49] = 78;
+ t[50] = 79;
+ t[51] = 80;
+ t[52] = 81;
+ t[53] = 82;
+ t[54] = 83;
+ t[55] = 84;
+ t[56] = 85;
+ t[57] = 86;
+ t[58] = 87;
+ t[59] = 88;
+ t[60] = 89;
+ t[61] = 90;
+ t[62] = 91;
+ t[63] = 92;
+ t[64] = 93;
+ t[65] = 94;
+ t[66] = 95;
+ t[67] = 96;
+ t[68] = 97;
+ t[69] = 98;
+ t[70] = 99;
+ t[71] = 100;
+ t[72] = 101;
+ t[73] = 102;
+ t[74] = 103;
+ t[75] = 104;
+ t[76] = 105;
+ t[77] = 106;
+ t[78] = 107;
+ t[79] = 108;
+ t[80] = 109;
+ t[81] = 110;
+ t[82] = 111;
+ t[83] = 112;
+ t[84] = 113;
+ t[85] = 114;
+ t[86] = 115;
+ t[87] = 116;
+ t[88] = 117;
+ t[89] = 118;
+ t[90] = 119;
+ t[91] = 120;
+ t[92] = 121;
+ t[93] = 122;
+ t[94] = 123;
+ t[95] = 124;
+ t[96] = 125;
+ t[97] = 126;
+ t[98] = 196;
+ t[99] = 197;
+ t[100] = 199;
+ t[101] = 201;
+ t[102] = 209;
+ t[103] = 214;
+ t[104] = 220;
+ t[105] = 225;
+ t[106] = 224;
+ t[107] = 226;
+ t[108] = 228;
+ t[109] = 227;
+ t[110] = 229;
+ t[111] = 231;
+ t[112] = 233;
+ t[113] = 232;
+ t[114] = 234;
+ t[115] = 235;
+ t[116] = 237;
+ t[117] = 236;
+ t[118] = 238;
+ t[119] = 239;
+ t[120] = 241;
+ t[121] = 243;
+ t[122] = 242;
+ t[123] = 244;
+ t[124] = 246;
+ t[125] = 245;
+ t[126] = 250;
+ t[127] = 249;
+ t[128] = 251;
+ t[129] = 252;
+ t[130] = 8224;
+ t[131] = 176;
+ t[132] = 162;
+ t[133] = 163;
+ t[134] = 167;
+ t[135] = 8226;
+ t[136] = 182;
+ t[137] = 223;
+ t[138] = 174;
+ t[139] = 169;
+ t[140] = 8482;
+ t[141] = 180;
+ t[142] = 168;
+ t[143] = 8800;
+ t[144] = 198;
+ t[145] = 216;
+ t[146] = 8734;
+ t[147] = 177;
+ t[148] = 8804;
+ t[149] = 8805;
+ t[150] = 165;
+ t[151] = 181;
+ t[152] = 8706;
+ t[153] = 8721;
+ t[154] = 8719;
+ t[156] = 8747;
+ t[157] = 170;
+ t[158] = 186;
+ t[159] = 8486;
+ t[160] = 230;
+ t[161] = 248;
+ t[162] = 191;
+ t[163] = 161;
+ t[164] = 172;
+ t[165] = 8730;
+ t[166] = 402;
+ t[167] = 8776;
+ t[168] = 8710;
+ t[169] = 171;
+ t[170] = 187;
+ t[171] = 8230;
+ t[210] = 218;
+ t[223] = 711;
+ t[224] = 321;
+ t[225] = 322;
+ t[227] = 353;
+ t[229] = 382;
+ t[234] = 253;
+ t[252] = 263;
+ t[253] = 268;
+ t[254] = 269;
+ t[258] = 258;
+ t[260] = 260;
+ t[261] = 261;
+ t[265] = 280;
+ t[266] = 281;
+ t[268] = 283;
+ t[269] = 313;
+ t[275] = 323;
+ t[276] = 324;
+ t[278] = 328;
+ t[284] = 345;
+ t[285] = 346;
+ t[286] = 347;
+ t[292] = 367;
+ t[295] = 377;
+ t[296] = 378;
+ t[298] = 380;
+ t[305] = 963;
+ t[306] = 964;
+ t[307] = 966;
+ t[308] = 8215;
+ t[309] = 8252;
+ t[310] = 8319;
+ t[311] = 8359;
+ t[312] = 8592;
+ t[313] = 8593;
+ t[337] = 9552;
+ t[493] = 1039;
+ t[494] = 1040;
+ t[705] = 1524;
+ t[706] = 8362;
+ t[710] = 64288;
+ t[711] = 64298;
+ t[759] = 1617;
+ t[761] = 1776;
+ t[763] = 1778;
+ t[775] = 1652;
+ t[777] = 1764;
+ t[778] = 1780;
+ t[779] = 1781;
+ t[780] = 1782;
+ t[782] = 771;
+ t[783] = 64726;
+ t[786] = 8363;
+ t[788] = 8532;
+ t[790] = 768;
+ t[791] = 769;
+ t[792] = 768;
+ t[795] = 803;
+ t[797] = 64336;
+ t[798] = 64337;
+ t[799] = 64342;
+ t[800] = 64343;
+ t[801] = 64344;
+ t[802] = 64345;
+ t[803] = 64362;
+ t[804] = 64363;
+ t[805] = 64364;
+ t[2424] = 7821;
+ t[2425] = 7822;
+ t[2426] = 7823;
+ t[2427] = 7824;
+ t[2428] = 7825;
+ t[2429] = 7826;
+ t[2430] = 7827;
+ t[2433] = 7682;
+ t[2678] = 8045;
+ t[2679] = 8046;
+ t[2830] = 1552;
+ t[2838] = 686;
+ t[2840] = 751;
+ t[2842] = 753;
+ t[2843] = 754;
+ t[2844] = 755;
+ t[2846] = 757;
+ t[2856] = 767;
+ t[2857] = 848;
+ t[2858] = 849;
+ t[2862] = 853;
+ t[2863] = 854;
+ t[2864] = 855;
+ t[2865] = 861;
+ t[2866] = 862;
+ t[2906] = 7460;
+ t[2908] = 7462;
+ t[2909] = 7463;
+ t[2910] = 7464;
+ t[2912] = 7466;
+ t[2913] = 7467;
+ t[2914] = 7468;
+ t[2916] = 7470;
+ t[2917] = 7471;
+ t[2918] = 7472;
+ t[2920] = 7474;
+ t[2921] = 7475;
+ t[2922] = 7476;
+ t[2924] = 7478;
+ t[2925] = 7479;
+ t[2926] = 7480;
+ t[2928] = 7482;
+ t[2929] = 7483;
+ t[2930] = 7484;
+ t[2932] = 7486;
+ t[2933] = 7487;
+ t[2934] = 7488;
+ t[2936] = 7490;
+ t[2937] = 7491;
+ t[2938] = 7492;
+ t[2940] = 7494;
+ t[2941] = 7495;
+ t[2942] = 7496;
+ t[2944] = 7498;
+ t[2946] = 7500;
+ t[2948] = 7502;
+ t[2950] = 7504;
+ t[2951] = 7505;
+ t[2952] = 7506;
+ t[2954] = 7508;
+ t[2955] = 7509;
+ t[2956] = 7510;
+ t[2958] = 7512;
+ t[2959] = 7513;
+ t[2960] = 7514;
+ t[2962] = 7516;
+ t[2963] = 7517;
+ t[2964] = 7518;
+ t[2966] = 7520;
+ t[2967] = 7521;
+ t[2968] = 7522;
+ t[2970] = 7524;
+ t[2971] = 7525;
+ t[2972] = 7526;
+ t[2974] = 7528;
+ t[2975] = 7529;
+ t[2976] = 7530;
+ t[2978] = 1537;
+ t[2979] = 1538;
+ t[2980] = 1539;
+ t[2982] = 1549;
+ t[2983] = 1551;
+ t[2984] = 1552;
+ t[2986] = 1554;
+ t[2987] = 1555;
+ t[2988] = 1556;
+ t[2990] = 1623;
+ t[2991] = 1624;
+ t[2995] = 1775;
+ t[2999] = 1791;
+ t[3002] = 64290;
+ t[3003] = 64291;
+ t[3004] = 64292;
+ t[3006] = 64294;
+ t[3007] = 64295;
+ t[3008] = 64296;
+ t[3011] = 1900;
+ t[3014] = 8223;
+ t[3015] = 8244;
+ t[3017] = 7532;
+ t[3018] = 7533;
+ t[3019] = 7534;
+ t[3075] = 7590;
+ t[3076] = 7591;
+ t[3079] = 7594;
+ t[3080] = 7595;
+ t[3083] = 7598;
+ t[3084] = 7599;
+ t[3087] = 7602;
+ t[3088] = 7603;
+ t[3091] = 7606;
+ t[3092] = 7607;
+ t[3095] = 7610;
+ t[3096] = 7611;
+ t[3099] = 7614;
+ t[3100] = 7615;
+ t[3103] = 7618;
+ t[3104] = 7619;
+ t[3107] = 8337;
+ t[3108] = 8338;
+ t[3116] = 1884;
+ t[3119] = 1885;
+ t[3120] = 1885;
+ t[3123] = 1886;
+ t[3124] = 1886;
+ t[3127] = 1887;
+ t[3128] = 1887;
+ t[3131] = 1888;
+ t[3132] = 1888;
+ t[3135] = 1889;
+ t[3136] = 1889;
+ t[3139] = 1890;
+ t[3140] = 1890;
+ t[3143] = 1891;
+ t[3144] = 1891;
+ t[3147] = 1892;
+ t[3148] = 1892;
+ t[3153] = 580;
+ t[3154] = 581;
+ t[3157] = 584;
+ t[3158] = 585;
+ t[3161] = 588;
+ t[3162] = 589;
+ t[3165] = 891;
+ t[3166] = 892;
+ t[3169] = 1274;
+ t[3170] = 1275;
+ t[3173] = 1278;
+ t[3174] = 1279;
+ t[3181] = 7622;
+ t[3182] = 7623;
+ t[3282] = 11799;
+ t[3316] = 578;
+ t[3379] = 42785;
+ t[3393] = 1159;
+ t[3416] = 8377;
+});
+exports.getGlyphMapForStandardFonts = getGlyphMapForStandardFonts;
+var getSupplementalGlyphMapForArialBlack = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t[227] = 322;
+ t[264] = 261;
+ t[291] = 346;
+});
+exports.getSupplementalGlyphMapForArialBlack = getSupplementalGlyphMapForArialBlack;
+var getSupplementalGlyphMapForCalibri = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t[1] = 32;
+ t[4] = 65;
+ t[17] = 66;
+ t[18] = 67;
+ t[24] = 68;
+ t[28] = 69;
+ t[38] = 70;
+ t[39] = 71;
+ t[44] = 72;
+ t[47] = 73;
+ t[58] = 74;
+ t[60] = 75;
+ t[62] = 76;
+ t[68] = 77;
+ t[69] = 78;
+ t[75] = 79;
+ t[87] = 80;
+ t[89] = 81;
+ t[90] = 82;
+ t[94] = 83;
+ t[100] = 84;
+ t[104] = 85;
+ t[115] = 86;
+ t[116] = 87;
+ t[121] = 88;
+ t[122] = 89;
+ t[127] = 90;
+ t[258] = 97;
+ t[268] = 261;
+ t[271] = 98;
+ t[272] = 99;
+ t[273] = 263;
+ t[282] = 100;
+ t[286] = 101;
+ t[295] = 281;
+ t[296] = 102;
+ t[336] = 103;
+ t[346] = 104;
+ t[349] = 105;
+ t[361] = 106;
+ t[364] = 107;
+ t[367] = 108;
+ t[371] = 322;
+ t[373] = 109;
+ t[374] = 110;
+ t[381] = 111;
+ t[383] = 243;
+ t[393] = 112;
+ t[395] = 113;
+ t[396] = 114;
+ t[400] = 115;
+ t[401] = 347;
+ t[410] = 116;
+ t[437] = 117;
+ t[448] = 118;
+ t[449] = 119;
+ t[454] = 120;
+ t[455] = 121;
+ t[460] = 122;
+ t[463] = 380;
+ t[853] = 44;
+ t[855] = 58;
+ t[856] = 46;
+ t[876] = 47;
+ t[878] = 45;
+ t[882] = 45;
+ t[894] = 40;
+ t[895] = 41;
+ t[896] = 91;
+ t[897] = 93;
+ t[923] = 64;
+ t[1004] = 48;
+ t[1005] = 49;
+ t[1006] = 50;
+ t[1007] = 51;
+ t[1008] = 52;
+ t[1009] = 53;
+ t[1010] = 54;
+ t[1011] = 55;
+ t[1012] = 56;
+ t[1013] = 57;
+ t[1081] = 37;
+ t[1085] = 43;
+ t[1086] = 45;
+});
+exports.getSupplementalGlyphMapForCalibri = getSupplementalGlyphMapForCalibri;
+
+/***/ }),
+/* 180 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+var getLookupTableFactory = __w_pdfjs_require__(154).getLookupTableFactory;
+var getSpecialPUASymbols = getLookupTableFactory(function (t) {
+ t[63721] = 0x00A9;
+ t[63193] = 0x00A9;
+ t[63720] = 0x00AE;
+ t[63194] = 0x00AE;
+ t[63722] = 0x2122;
+ t[63195] = 0x2122;
+ t[63729] = 0x23A7;
+ t[63730] = 0x23A8;
+ t[63731] = 0x23A9;
+ t[63740] = 0x23AB;
+ t[63741] = 0x23AC;
+ t[63742] = 0x23AD;
+ t[63726] = 0x23A1;
+ t[63727] = 0x23A2;
+ t[63728] = 0x23A3;
+ t[63737] = 0x23A4;
+ t[63738] = 0x23A5;
+ t[63739] = 0x23A6;
+ t[63723] = 0x239B;
+ t[63724] = 0x239C;
+ t[63725] = 0x239D;
+ t[63734] = 0x239E;
+ t[63735] = 0x239F;
+ t[63736] = 0x23A0;
+});
+function mapSpecialUnicodeValues(code) {
+ if (code >= 0xFFF0 && code <= 0xFFFF) {
+ return 0;
+ } else if (code >= 0xF600 && code <= 0xF8FF) {
+ return getSpecialPUASymbols()[code] || code;
+ } else if (code === 0x00AD) {
+ return 0x002D;
+ }
+ return code;
+}
+function getUnicodeForGlyph(name, glyphsUnicodeMap) {
+ var unicode = glyphsUnicodeMap[name];
+ if (unicode !== undefined) {
+ return unicode;
+ }
+ if (!name) {
+ return -1;
+ }
+ if (name[0] === 'u') {
+ var nameLen = name.length, hexStr;
+ if (nameLen === 7 && name[1] === 'n' && name[2] === 'i') {
+ hexStr = name.substring(3);
+ } else if (nameLen >= 5 && nameLen <= 7) {
+ hexStr = name.substring(1);
+ } else {
+ return -1;
+ }
+ if (hexStr === hexStr.toUpperCase()) {
+ unicode = parseInt(hexStr, 16);
+ if (unicode >= 0) {
+ return unicode;
+ }
+ }
+ }
+ return -1;
+}
+var UnicodeRanges = [
+ {
+ 'begin': 0x0000,
+ 'end': 0x007F
+ },
+ {
+ 'begin': 0x0080,
+ 'end': 0x00FF
+ },
+ {
+ 'begin': 0x0100,
+ 'end': 0x017F
+ },
+ {
+ 'begin': 0x0180,
+ 'end': 0x024F
+ },
+ {
+ 'begin': 0x0250,
+ 'end': 0x02AF
+ },
+ {
+ 'begin': 0x02B0,
+ 'end': 0x02FF
+ },
+ {
+ 'begin': 0x0300,
+ 'end': 0x036F
+ },
+ {
+ 'begin': 0x0370,
+ 'end': 0x03FF
+ },
+ {
+ 'begin': 0x2C80,
+ 'end': 0x2CFF
+ },
+ {
+ 'begin': 0x0400,
+ 'end': 0x04FF
+ },
+ {
+ 'begin': 0x0530,
+ 'end': 0x058F
+ },
+ {
+ 'begin': 0x0590,
+ 'end': 0x05FF
+ },
+ {
+ 'begin': 0xA500,
+ 'end': 0xA63F
+ },
+ {
+ 'begin': 0x0600,
+ 'end': 0x06FF
+ },
+ {
+ 'begin': 0x07C0,
+ 'end': 0x07FF
+ },
+ {
+ 'begin': 0x0900,
+ 'end': 0x097F
+ },
+ {
+ 'begin': 0x0980,
+ 'end': 0x09FF
+ },
+ {
+ 'begin': 0x0A00,
+ 'end': 0x0A7F
+ },
+ {
+ 'begin': 0x0A80,
+ 'end': 0x0AFF
+ },
+ {
+ 'begin': 0x0B00,
+ 'end': 0x0B7F
+ },
+ {
+ 'begin': 0x0B80,
+ 'end': 0x0BFF
+ },
+ {
+ 'begin': 0x0C00,
+ 'end': 0x0C7F
+ },
+ {
+ 'begin': 0x0C80,
+ 'end': 0x0CFF
+ },
+ {
+ 'begin': 0x0D00,
+ 'end': 0x0D7F
+ },
+ {
+ 'begin': 0x0E00,
+ 'end': 0x0E7F
+ },
+ {
+ 'begin': 0x0E80,
+ 'end': 0x0EFF
+ },
+ {
+ 'begin': 0x10A0,
+ 'end': 0x10FF
+ },
+ {
+ 'begin': 0x1B00,
+ 'end': 0x1B7F
+ },
+ {
+ 'begin': 0x1100,
+ 'end': 0x11FF
+ },
+ {
+ 'begin': 0x1E00,
+ 'end': 0x1EFF
+ },
+ {
+ 'begin': 0x1F00,
+ 'end': 0x1FFF
+ },
+ {
+ 'begin': 0x2000,
+ 'end': 0x206F
+ },
+ {
+ 'begin': 0x2070,
+ 'end': 0x209F
+ },
+ {
+ 'begin': 0x20A0,
+ 'end': 0x20CF
+ },
+ {
+ 'begin': 0x20D0,
+ 'end': 0x20FF
+ },
+ {
+ 'begin': 0x2100,
+ 'end': 0x214F
+ },
+ {
+ 'begin': 0x2150,
+ 'end': 0x218F
+ },
+ {
+ 'begin': 0x2190,
+ 'end': 0x21FF
+ },
+ {
+ 'begin': 0x2200,
+ 'end': 0x22FF
+ },
+ {
+ 'begin': 0x2300,
+ 'end': 0x23FF
+ },
+ {
+ 'begin': 0x2400,
+ 'end': 0x243F
+ },
+ {
+ 'begin': 0x2440,
+ 'end': 0x245F
+ },
+ {
+ 'begin': 0x2460,
+ 'end': 0x24FF
+ },
+ {
+ 'begin': 0x2500,
+ 'end': 0x257F
+ },
+ {
+ 'begin': 0x2580,
+ 'end': 0x259F
+ },
+ {
+ 'begin': 0x25A0,
+ 'end': 0x25FF
+ },
+ {
+ 'begin': 0x2600,
+ 'end': 0x26FF
+ },
+ {
+ 'begin': 0x2700,
+ 'end': 0x27BF
+ },
+ {
+ 'begin': 0x3000,
+ 'end': 0x303F
+ },
+ {
+ 'begin': 0x3040,
+ 'end': 0x309F
+ },
+ {
+ 'begin': 0x30A0,
+ 'end': 0x30FF
+ },
+ {
+ 'begin': 0x3100,
+ 'end': 0x312F
+ },
+ {
+ 'begin': 0x3130,
+ 'end': 0x318F
+ },
+ {
+ 'begin': 0xA840,
+ 'end': 0xA87F
+ },
+ {
+ 'begin': 0x3200,
+ 'end': 0x32FF
+ },
+ {
+ 'begin': 0x3300,
+ 'end': 0x33FF
+ },
+ {
+ 'begin': 0xAC00,
+ 'end': 0xD7AF
+ },
+ {
+ 'begin': 0xD800,
+ 'end': 0xDFFF
+ },
+ {
+ 'begin': 0x10900,
+ 'end': 0x1091F
+ },
+ {
+ 'begin': 0x4E00,
+ 'end': 0x9FFF
+ },
+ {
+ 'begin': 0xE000,
+ 'end': 0xF8FF
+ },
+ {
+ 'begin': 0x31C0,
+ 'end': 0x31EF
+ },
+ {
+ 'begin': 0xFB00,
+ 'end': 0xFB4F
+ },
+ {
+ 'begin': 0xFB50,
+ 'end': 0xFDFF
+ },
+ {
+ 'begin': 0xFE20,
+ 'end': 0xFE2F
+ },
+ {
+ 'begin': 0xFE10,
+ 'end': 0xFE1F
+ },
+ {
+ 'begin': 0xFE50,
+ 'end': 0xFE6F
+ },
+ {
+ 'begin': 0xFE70,
+ 'end': 0xFEFF
+ },
+ {
+ 'begin': 0xFF00,
+ 'end': 0xFFEF
+ },
+ {
+ 'begin': 0xFFF0,
+ 'end': 0xFFFF
+ },
+ {
+ 'begin': 0x0F00,
+ 'end': 0x0FFF
+ },
+ {
+ 'begin': 0x0700,
+ 'end': 0x074F
+ },
+ {
+ 'begin': 0x0780,
+ 'end': 0x07BF
+ },
+ {
+ 'begin': 0x0D80,
+ 'end': 0x0DFF
+ },
+ {
+ 'begin': 0x1000,
+ 'end': 0x109F
+ },
+ {
+ 'begin': 0x1200,
+ 'end': 0x137F
+ },
+ {
+ 'begin': 0x13A0,
+ 'end': 0x13FF
+ },
+ {
+ 'begin': 0x1400,
+ 'end': 0x167F
+ },
+ {
+ 'begin': 0x1680,
+ 'end': 0x169F
+ },
+ {
+ 'begin': 0x16A0,
+ 'end': 0x16FF
+ },
+ {
+ 'begin': 0x1780,
+ 'end': 0x17FF
+ },
+ {
+ 'begin': 0x1800,
+ 'end': 0x18AF
+ },
+ {
+ 'begin': 0x2800,
+ 'end': 0x28FF
+ },
+ {
+ 'begin': 0xA000,
+ 'end': 0xA48F
+ },
+ {
+ 'begin': 0x1700,
+ 'end': 0x171F
+ },
+ {
+ 'begin': 0x10300,
+ 'end': 0x1032F
+ },
+ {
+ 'begin': 0x10330,
+ 'end': 0x1034F
+ },
+ {
+ 'begin': 0x10400,
+ 'end': 0x1044F
+ },
+ {
+ 'begin': 0x1D000,
+ 'end': 0x1D0FF
+ },
+ {
+ 'begin': 0x1D400,
+ 'end': 0x1D7FF
+ },
+ {
+ 'begin': 0xFF000,
+ 'end': 0xFFFFD
+ },
+ {
+ 'begin': 0xFE00,
+ 'end': 0xFE0F
+ },
+ {
+ 'begin': 0xE0000,
+ 'end': 0xE007F
+ },
+ {
+ 'begin': 0x1900,
+ 'end': 0x194F
+ },
+ {
+ 'begin': 0x1950,
+ 'end': 0x197F
+ },
+ {
+ 'begin': 0x1980,
+ 'end': 0x19DF
+ },
+ {
+ 'begin': 0x1A00,
+ 'end': 0x1A1F
+ },
+ {
+ 'begin': 0x2C00,
+ 'end': 0x2C5F
+ },
+ {
+ 'begin': 0x2D30,
+ 'end': 0x2D7F
+ },
+ {
+ 'begin': 0x4DC0,
+ 'end': 0x4DFF
+ },
+ {
+ 'begin': 0xA800,
+ 'end': 0xA82F
+ },
+ {
+ 'begin': 0x10000,
+ 'end': 0x1007F
+ },
+ {
+ 'begin': 0x10140,
+ 'end': 0x1018F
+ },
+ {
+ 'begin': 0x10380,
+ 'end': 0x1039F
+ },
+ {
+ 'begin': 0x103A0,
+ 'end': 0x103DF
+ },
+ {
+ 'begin': 0x10450,
+ 'end': 0x1047F
+ },
+ {
+ 'begin': 0x10480,
+ 'end': 0x104AF
+ },
+ {
+ 'begin': 0x10800,
+ 'end': 0x1083F
+ },
+ {
+ 'begin': 0x10A00,
+ 'end': 0x10A5F
+ },
+ {
+ 'begin': 0x1D300,
+ 'end': 0x1D35F
+ },
+ {
+ 'begin': 0x12000,
+ 'end': 0x123FF
+ },
+ {
+ 'begin': 0x1D360,
+ 'end': 0x1D37F
+ },
+ {
+ 'begin': 0x1B80,
+ 'end': 0x1BBF
+ },
+ {
+ 'begin': 0x1C00,
+ 'end': 0x1C4F
+ },
+ {
+ 'begin': 0x1C50,
+ 'end': 0x1C7F
+ },
+ {
+ 'begin': 0xA880,
+ 'end': 0xA8DF
+ },
+ {
+ 'begin': 0xA900,
+ 'end': 0xA92F
+ },
+ {
+ 'begin': 0xA930,
+ 'end': 0xA95F
+ },
+ {
+ 'begin': 0xAA00,
+ 'end': 0xAA5F
+ },
+ {
+ 'begin': 0x10190,
+ 'end': 0x101CF
+ },
+ {
+ 'begin': 0x101D0,
+ 'end': 0x101FF
+ },
+ {
+ 'begin': 0x102A0,
+ 'end': 0x102DF
+ },
+ {
+ 'begin': 0x1F030,
+ 'end': 0x1F09F
+ }
+];
+function getUnicodeRangeFor(value) {
+ for (var i = 0, ii = UnicodeRanges.length; i < ii; i++) {
+ var range = UnicodeRanges[i];
+ if (value >= range.begin && value < range.end) {
+ return i;
+ }
+ }
+ return -1;
+}
+function isRTLRangeFor(value) {
+ var range = UnicodeRanges[13];
+ if (value >= range.begin && value < range.end) {
+ return true;
+ }
+ range = UnicodeRanges[11];
+ if (value >= range.begin && value < range.end) {
+ return true;
+ }
+ return false;
+}
+var getNormalizedUnicodes = getLookupTableFactory(function (t) {
+ t['\u00A8'] = '\u0020\u0308';
+ t['\u00AF'] = '\u0020\u0304';
+ t['\u00B4'] = '\u0020\u0301';
+ t['\u00B5'] = '\u03BC';
+ t['\u00B8'] = '\u0020\u0327';
+ t['\u0132'] = '\u0049\u004A';
+ t['\u0133'] = '\u0069\u006A';
+ t['\u013F'] = '\u004C\u00B7';
+ t['\u0140'] = '\u006C\u00B7';
+ t['\u0149'] = '\u02BC\u006E';
+ t['\u017F'] = '\u0073';
+ t['\u01C4'] = '\u0044\u017D';
+ t['\u01C5'] = '\u0044\u017E';
+ t['\u01C6'] = '\u0064\u017E';
+ t['\u01C7'] = '\u004C\u004A';
+ t['\u01C8'] = '\u004C\u006A';
+ t['\u01C9'] = '\u006C\u006A';
+ t['\u01CA'] = '\u004E\u004A';
+ t['\u01CB'] = '\u004E\u006A';
+ t['\u01CC'] = '\u006E\u006A';
+ t['\u01F1'] = '\u0044\u005A';
+ t['\u01F2'] = '\u0044\u007A';
+ t['\u01F3'] = '\u0064\u007A';
+ t['\u02D8'] = '\u0020\u0306';
+ t['\u02D9'] = '\u0020\u0307';
+ t['\u02DA'] = '\u0020\u030A';
+ t['\u02DB'] = '\u0020\u0328';
+ t['\u02DC'] = '\u0020\u0303';
+ t['\u02DD'] = '\u0020\u030B';
+ t['\u037A'] = '\u0020\u0345';
+ t['\u0384'] = '\u0020\u0301';
+ t['\u03D0'] = '\u03B2';
+ t['\u03D1'] = '\u03B8';
+ t['\u03D2'] = '\u03A5';
+ t['\u03D5'] = '\u03C6';
+ t['\u03D6'] = '\u03C0';
+ t['\u03F0'] = '\u03BA';
+ t['\u03F1'] = '\u03C1';
+ t['\u03F2'] = '\u03C2';
+ t['\u03F4'] = '\u0398';
+ t['\u03F5'] = '\u03B5';
+ t['\u03F9'] = '\u03A3';
+ t['\u0587'] = '\u0565\u0582';
+ t['\u0675'] = '\u0627\u0674';
+ t['\u0676'] = '\u0648\u0674';
+ t['\u0677'] = '\u06C7\u0674';
+ t['\u0678'] = '\u064A\u0674';
+ t['\u0E33'] = '\u0E4D\u0E32';
+ t['\u0EB3'] = '\u0ECD\u0EB2';
+ t['\u0EDC'] = '\u0EAB\u0E99';
+ t['\u0EDD'] = '\u0EAB\u0EA1';
+ t['\u0F77'] = '\u0FB2\u0F81';
+ t['\u0F79'] = '\u0FB3\u0F81';
+ t['\u1E9A'] = '\u0061\u02BE';
+ t['\u1FBD'] = '\u0020\u0313';
+ t['\u1FBF'] = '\u0020\u0313';
+ t['\u1FC0'] = '\u0020\u0342';
+ t['\u1FFE'] = '\u0020\u0314';
+ t['\u2002'] = '\u0020';
+ t['\u2003'] = '\u0020';
+ t['\u2004'] = '\u0020';
+ t['\u2005'] = '\u0020';
+ t['\u2006'] = '\u0020';
+ t['\u2008'] = '\u0020';
+ t['\u2009'] = '\u0020';
+ t['\u200A'] = '\u0020';
+ t['\u2017'] = '\u0020\u0333';
+ t['\u2024'] = '\u002E';
+ t['\u2025'] = '\u002E\u002E';
+ t['\u2026'] = '\u002E\u002E\u002E';
+ t['\u2033'] = '\u2032\u2032';
+ t['\u2034'] = '\u2032\u2032\u2032';
+ t['\u2036'] = '\u2035\u2035';
+ t['\u2037'] = '\u2035\u2035\u2035';
+ t['\u203C'] = '\u0021\u0021';
+ t['\u203E'] = '\u0020\u0305';
+ t['\u2047'] = '\u003F\u003F';
+ t['\u2048'] = '\u003F\u0021';
+ t['\u2049'] = '\u0021\u003F';
+ t['\u2057'] = '\u2032\u2032\u2032\u2032';
+ t['\u205F'] = '\u0020';
+ t['\u20A8'] = '\u0052\u0073';
+ t['\u2100'] = '\u0061\u002F\u0063';
+ t['\u2101'] = '\u0061\u002F\u0073';
+ t['\u2103'] = '\u00B0\u0043';
+ t['\u2105'] = '\u0063\u002F\u006F';
+ t['\u2106'] = '\u0063\u002F\u0075';
+ t['\u2107'] = '\u0190';
+ t['\u2109'] = '\u00B0\u0046';
+ t['\u2116'] = '\u004E\u006F';
+ t['\u2121'] = '\u0054\u0045\u004C';
+ t['\u2135'] = '\u05D0';
+ t['\u2136'] = '\u05D1';
+ t['\u2137'] = '\u05D2';
+ t['\u2138'] = '\u05D3';
+ t['\u213B'] = '\u0046\u0041\u0058';
+ t['\u2160'] = '\u0049';
+ t['\u2161'] = '\u0049\u0049';
+ t['\u2162'] = '\u0049\u0049\u0049';
+ t['\u2163'] = '\u0049\u0056';
+ t['\u2164'] = '\u0056';
+ t['\u2165'] = '\u0056\u0049';
+ t['\u2166'] = '\u0056\u0049\u0049';
+ t['\u2167'] = '\u0056\u0049\u0049\u0049';
+ t['\u2168'] = '\u0049\u0058';
+ t['\u2169'] = '\u0058';
+ t['\u216A'] = '\u0058\u0049';
+ t['\u216B'] = '\u0058\u0049\u0049';
+ t['\u216C'] = '\u004C';
+ t['\u216D'] = '\u0043';
+ t['\u216E'] = '\u0044';
+ t['\u216F'] = '\u004D';
+ t['\u2170'] = '\u0069';
+ t['\u2171'] = '\u0069\u0069';
+ t['\u2172'] = '\u0069\u0069\u0069';
+ t['\u2173'] = '\u0069\u0076';
+ t['\u2174'] = '\u0076';
+ t['\u2175'] = '\u0076\u0069';
+ t['\u2176'] = '\u0076\u0069\u0069';
+ t['\u2177'] = '\u0076\u0069\u0069\u0069';
+ t['\u2178'] = '\u0069\u0078';
+ t['\u2179'] = '\u0078';
+ t['\u217A'] = '\u0078\u0069';
+ t['\u217B'] = '\u0078\u0069\u0069';
+ t['\u217C'] = '\u006C';
+ t['\u217D'] = '\u0063';
+ t['\u217E'] = '\u0064';
+ t['\u217F'] = '\u006D';
+ t['\u222C'] = '\u222B\u222B';
+ t['\u222D'] = '\u222B\u222B\u222B';
+ t['\u222F'] = '\u222E\u222E';
+ t['\u2230'] = '\u222E\u222E\u222E';
+ t['\u2474'] = '\u0028\u0031\u0029';
+ t['\u2475'] = '\u0028\u0032\u0029';
+ t['\u2476'] = '\u0028\u0033\u0029';
+ t['\u2477'] = '\u0028\u0034\u0029';
+ t['\u2478'] = '\u0028\u0035\u0029';
+ t['\u2479'] = '\u0028\u0036\u0029';
+ t['\u247A'] = '\u0028\u0037\u0029';
+ t['\u247B'] = '\u0028\u0038\u0029';
+ t['\u247C'] = '\u0028\u0039\u0029';
+ t['\u247D'] = '\u0028\u0031\u0030\u0029';
+ t['\u247E'] = '\u0028\u0031\u0031\u0029';
+ t['\u247F'] = '\u0028\u0031\u0032\u0029';
+ t['\u2480'] = '\u0028\u0031\u0033\u0029';
+ t['\u2481'] = '\u0028\u0031\u0034\u0029';
+ t['\u2482'] = '\u0028\u0031\u0035\u0029';
+ t['\u2483'] = '\u0028\u0031\u0036\u0029';
+ t['\u2484'] = '\u0028\u0031\u0037\u0029';
+ t['\u2485'] = '\u0028\u0031\u0038\u0029';
+ t['\u2486'] = '\u0028\u0031\u0039\u0029';
+ t['\u2487'] = '\u0028\u0032\u0030\u0029';
+ t['\u2488'] = '\u0031\u002E';
+ t['\u2489'] = '\u0032\u002E';
+ t['\u248A'] = '\u0033\u002E';
+ t['\u248B'] = '\u0034\u002E';
+ t['\u248C'] = '\u0035\u002E';
+ t['\u248D'] = '\u0036\u002E';
+ t['\u248E'] = '\u0037\u002E';
+ t['\u248F'] = '\u0038\u002E';
+ t['\u2490'] = '\u0039\u002E';
+ t['\u2491'] = '\u0031\u0030\u002E';
+ t['\u2492'] = '\u0031\u0031\u002E';
+ t['\u2493'] = '\u0031\u0032\u002E';
+ t['\u2494'] = '\u0031\u0033\u002E';
+ t['\u2495'] = '\u0031\u0034\u002E';
+ t['\u2496'] = '\u0031\u0035\u002E';
+ t['\u2497'] = '\u0031\u0036\u002E';
+ t['\u2498'] = '\u0031\u0037\u002E';
+ t['\u2499'] = '\u0031\u0038\u002E';
+ t['\u249A'] = '\u0031\u0039\u002E';
+ t['\u249B'] = '\u0032\u0030\u002E';
+ t['\u249C'] = '\u0028\u0061\u0029';
+ t['\u249D'] = '\u0028\u0062\u0029';
+ t['\u249E'] = '\u0028\u0063\u0029';
+ t['\u249F'] = '\u0028\u0064\u0029';
+ t['\u24A0'] = '\u0028\u0065\u0029';
+ t['\u24A1'] = '\u0028\u0066\u0029';
+ t['\u24A2'] = '\u0028\u0067\u0029';
+ t['\u24A3'] = '\u0028\u0068\u0029';
+ t['\u24A4'] = '\u0028\u0069\u0029';
+ t['\u24A5'] = '\u0028\u006A\u0029';
+ t['\u24A6'] = '\u0028\u006B\u0029';
+ t['\u24A7'] = '\u0028\u006C\u0029';
+ t['\u24A8'] = '\u0028\u006D\u0029';
+ t['\u24A9'] = '\u0028\u006E\u0029';
+ t['\u24AA'] = '\u0028\u006F\u0029';
+ t['\u24AB'] = '\u0028\u0070\u0029';
+ t['\u24AC'] = '\u0028\u0071\u0029';
+ t['\u24AD'] = '\u0028\u0072\u0029';
+ t['\u24AE'] = '\u0028\u0073\u0029';
+ t['\u24AF'] = '\u0028\u0074\u0029';
+ t['\u24B0'] = '\u0028\u0075\u0029';
+ t['\u24B1'] = '\u0028\u0076\u0029';
+ t['\u24B2'] = '\u0028\u0077\u0029';
+ t['\u24B3'] = '\u0028\u0078\u0029';
+ t['\u24B4'] = '\u0028\u0079\u0029';
+ t['\u24B5'] = '\u0028\u007A\u0029';
+ t['\u2A0C'] = '\u222B\u222B\u222B\u222B';
+ t['\u2A74'] = '\u003A\u003A\u003D';
+ t['\u2A75'] = '\u003D\u003D';
+ t['\u2A76'] = '\u003D\u003D\u003D';
+ t['\u2E9F'] = '\u6BCD';
+ t['\u2EF3'] = '\u9F9F';
+ t['\u2F00'] = '\u4E00';
+ t['\u2F01'] = '\u4E28';
+ t['\u2F02'] = '\u4E36';
+ t['\u2F03'] = '\u4E3F';
+ t['\u2F04'] = '\u4E59';
+ t['\u2F05'] = '\u4E85';
+ t['\u2F06'] = '\u4E8C';
+ t['\u2F07'] = '\u4EA0';
+ t['\u2F08'] = '\u4EBA';
+ t['\u2F09'] = '\u513F';
+ t['\u2F0A'] = '\u5165';
+ t['\u2F0B'] = '\u516B';
+ t['\u2F0C'] = '\u5182';
+ t['\u2F0D'] = '\u5196';
+ t['\u2F0E'] = '\u51AB';
+ t['\u2F0F'] = '\u51E0';
+ t['\u2F10'] = '\u51F5';
+ t['\u2F11'] = '\u5200';
+ t['\u2F12'] = '\u529B';
+ t['\u2F13'] = '\u52F9';
+ t['\u2F14'] = '\u5315';
+ t['\u2F15'] = '\u531A';
+ t['\u2F16'] = '\u5338';
+ t['\u2F17'] = '\u5341';
+ t['\u2F18'] = '\u535C';
+ t['\u2F19'] = '\u5369';
+ t['\u2F1A'] = '\u5382';
+ t['\u2F1B'] = '\u53B6';
+ t['\u2F1C'] = '\u53C8';
+ t['\u2F1D'] = '\u53E3';
+ t['\u2F1E'] = '\u56D7';
+ t['\u2F1F'] = '\u571F';
+ t['\u2F20'] = '\u58EB';
+ t['\u2F21'] = '\u5902';
+ t['\u2F22'] = '\u590A';
+ t['\u2F23'] = '\u5915';
+ t['\u2F24'] = '\u5927';
+ t['\u2F25'] = '\u5973';
+ t['\u2F26'] = '\u5B50';
+ t['\u2F27'] = '\u5B80';
+ t['\u2F28'] = '\u5BF8';
+ t['\u2F29'] = '\u5C0F';
+ t['\u2F2A'] = '\u5C22';
+ t['\u2F2B'] = '\u5C38';
+ t['\u2F2C'] = '\u5C6E';
+ t['\u2F2D'] = '\u5C71';
+ t['\u2F2E'] = '\u5DDB';
+ t['\u2F2F'] = '\u5DE5';
+ t['\u2F30'] = '\u5DF1';
+ t['\u2F31'] = '\u5DFE';
+ t['\u2F32'] = '\u5E72';
+ t['\u2F33'] = '\u5E7A';
+ t['\u2F34'] = '\u5E7F';
+ t['\u2F35'] = '\u5EF4';
+ t['\u2F36'] = '\u5EFE';
+ t['\u2F37'] = '\u5F0B';
+ t['\u2F38'] = '\u5F13';
+ t['\u2F39'] = '\u5F50';
+ t['\u2F3A'] = '\u5F61';
+ t['\u2F3B'] = '\u5F73';
+ t['\u2F3C'] = '\u5FC3';
+ t['\u2F3D'] = '\u6208';
+ t['\u2F3E'] = '\u6236';
+ t['\u2F3F'] = '\u624B';
+ t['\u2F40'] = '\u652F';
+ t['\u2F41'] = '\u6534';
+ t['\u2F42'] = '\u6587';
+ t['\u2F43'] = '\u6597';
+ t['\u2F44'] = '\u65A4';
+ t['\u2F45'] = '\u65B9';
+ t['\u2F46'] = '\u65E0';
+ t['\u2F47'] = '\u65E5';
+ t['\u2F48'] = '\u66F0';
+ t['\u2F49'] = '\u6708';
+ t['\u2F4A'] = '\u6728';
+ t['\u2F4B'] = '\u6B20';
+ t['\u2F4C'] = '\u6B62';
+ t['\u2F4D'] = '\u6B79';
+ t['\u2F4E'] = '\u6BB3';
+ t['\u2F4F'] = '\u6BCB';
+ t['\u2F50'] = '\u6BD4';
+ t['\u2F51'] = '\u6BDB';
+ t['\u2F52'] = '\u6C0F';
+ t['\u2F53'] = '\u6C14';
+ t['\u2F54'] = '\u6C34';
+ t['\u2F55'] = '\u706B';
+ t['\u2F56'] = '\u722A';
+ t['\u2F57'] = '\u7236';
+ t['\u2F58'] = '\u723B';
+ t['\u2F59'] = '\u723F';
+ t['\u2F5A'] = '\u7247';
+ t['\u2F5B'] = '\u7259';
+ t['\u2F5C'] = '\u725B';
+ t['\u2F5D'] = '\u72AC';
+ t['\u2F5E'] = '\u7384';
+ t['\u2F5F'] = '\u7389';
+ t['\u2F60'] = '\u74DC';
+ t['\u2F61'] = '\u74E6';
+ t['\u2F62'] = '\u7518';
+ t['\u2F63'] = '\u751F';
+ t['\u2F64'] = '\u7528';
+ t['\u2F65'] = '\u7530';
+ t['\u2F66'] = '\u758B';
+ t['\u2F67'] = '\u7592';
+ t['\u2F68'] = '\u7676';
+ t['\u2F69'] = '\u767D';
+ t['\u2F6A'] = '\u76AE';
+ t['\u2F6B'] = '\u76BF';
+ t['\u2F6C'] = '\u76EE';
+ t['\u2F6D'] = '\u77DB';
+ t['\u2F6E'] = '\u77E2';
+ t['\u2F6F'] = '\u77F3';
+ t['\u2F70'] = '\u793A';
+ t['\u2F71'] = '\u79B8';
+ t['\u2F72'] = '\u79BE';
+ t['\u2F73'] = '\u7A74';
+ t['\u2F74'] = '\u7ACB';
+ t['\u2F75'] = '\u7AF9';
+ t['\u2F76'] = '\u7C73';
+ t['\u2F77'] = '\u7CF8';
+ t['\u2F78'] = '\u7F36';
+ t['\u2F79'] = '\u7F51';
+ t['\u2F7A'] = '\u7F8A';
+ t['\u2F7B'] = '\u7FBD';
+ t['\u2F7C'] = '\u8001';
+ t['\u2F7D'] = '\u800C';
+ t['\u2F7E'] = '\u8012';
+ t['\u2F7F'] = '\u8033';
+ t['\u2F80'] = '\u807F';
+ t['\u2F81'] = '\u8089';
+ t['\u2F82'] = '\u81E3';
+ t['\u2F83'] = '\u81EA';
+ t['\u2F84'] = '\u81F3';
+ t['\u2F85'] = '\u81FC';
+ t['\u2F86'] = '\u820C';
+ t['\u2F87'] = '\u821B';
+ t['\u2F88'] = '\u821F';
+ t['\u2F89'] = '\u826E';
+ t['\u2F8A'] = '\u8272';
+ t['\u2F8B'] = '\u8278';
+ t['\u2F8C'] = '\u864D';
+ t['\u2F8D'] = '\u866B';
+ t['\u2F8E'] = '\u8840';
+ t['\u2F8F'] = '\u884C';
+ t['\u2F90'] = '\u8863';
+ t['\u2F91'] = '\u897E';
+ t['\u2F92'] = '\u898B';
+ t['\u2F93'] = '\u89D2';
+ t['\u2F94'] = '\u8A00';
+ t['\u2F95'] = '\u8C37';
+ t['\u2F96'] = '\u8C46';
+ t['\u2F97'] = '\u8C55';
+ t['\u2F98'] = '\u8C78';
+ t['\u2F99'] = '\u8C9D';
+ t['\u2F9A'] = '\u8D64';
+ t['\u2F9B'] = '\u8D70';
+ t['\u2F9C'] = '\u8DB3';
+ t['\u2F9D'] = '\u8EAB';
+ t['\u2F9E'] = '\u8ECA';
+ t['\u2F9F'] = '\u8F9B';
+ t['\u2FA0'] = '\u8FB0';
+ t['\u2FA1'] = '\u8FB5';
+ t['\u2FA2'] = '\u9091';
+ t['\u2FA3'] = '\u9149';
+ t['\u2FA4'] = '\u91C6';
+ t['\u2FA5'] = '\u91CC';
+ t['\u2FA6'] = '\u91D1';
+ t['\u2FA7'] = '\u9577';
+ t['\u2FA8'] = '\u9580';
+ t['\u2FA9'] = '\u961C';
+ t['\u2FAA'] = '\u96B6';
+ t['\u2FAB'] = '\u96B9';
+ t['\u2FAC'] = '\u96E8';
+ t['\u2FAD'] = '\u9751';
+ t['\u2FAE'] = '\u975E';
+ t['\u2FAF'] = '\u9762';
+ t['\u2FB0'] = '\u9769';
+ t['\u2FB1'] = '\u97CB';
+ t['\u2FB2'] = '\u97ED';
+ t['\u2FB3'] = '\u97F3';
+ t['\u2FB4'] = '\u9801';
+ t['\u2FB5'] = '\u98A8';
+ t['\u2FB6'] = '\u98DB';
+ t['\u2FB7'] = '\u98DF';
+ t['\u2FB8'] = '\u9996';
+ t['\u2FB9'] = '\u9999';
+ t['\u2FBA'] = '\u99AC';
+ t['\u2FBB'] = '\u9AA8';
+ t['\u2FBC'] = '\u9AD8';
+ t['\u2FBD'] = '\u9ADF';
+ t['\u2FBE'] = '\u9B25';
+ t['\u2FBF'] = '\u9B2F';
+ t['\u2FC0'] = '\u9B32';
+ t['\u2FC1'] = '\u9B3C';
+ t['\u2FC2'] = '\u9B5A';
+ t['\u2FC3'] = '\u9CE5';
+ t['\u2FC4'] = '\u9E75';
+ t['\u2FC5'] = '\u9E7F';
+ t['\u2FC6'] = '\u9EA5';
+ t['\u2FC7'] = '\u9EBB';
+ t['\u2FC8'] = '\u9EC3';
+ t['\u2FC9'] = '\u9ECD';
+ t['\u2FCA'] = '\u9ED1';
+ t['\u2FCB'] = '\u9EF9';
+ t['\u2FCC'] = '\u9EFD';
+ t['\u2FCD'] = '\u9F0E';
+ t['\u2FCE'] = '\u9F13';
+ t['\u2FCF'] = '\u9F20';
+ t['\u2FD0'] = '\u9F3B';
+ t['\u2FD1'] = '\u9F4A';
+ t['\u2FD2'] = '\u9F52';
+ t['\u2FD3'] = '\u9F8D';
+ t['\u2FD4'] = '\u9F9C';
+ t['\u2FD5'] = '\u9FA0';
+ t['\u3036'] = '\u3012';
+ t['\u3038'] = '\u5341';
+ t['\u3039'] = '\u5344';
+ t['\u303A'] = '\u5345';
+ t['\u309B'] = '\u0020\u3099';
+ t['\u309C'] = '\u0020\u309A';
+ t['\u3131'] = '\u1100';
+ t['\u3132'] = '\u1101';
+ t['\u3133'] = '\u11AA';
+ t['\u3134'] = '\u1102';
+ t['\u3135'] = '\u11AC';
+ t['\u3136'] = '\u11AD';
+ t['\u3137'] = '\u1103';
+ t['\u3138'] = '\u1104';
+ t['\u3139'] = '\u1105';
+ t['\u313A'] = '\u11B0';
+ t['\u313B'] = '\u11B1';
+ t['\u313C'] = '\u11B2';
+ t['\u313D'] = '\u11B3';
+ t['\u313E'] = '\u11B4';
+ t['\u313F'] = '\u11B5';
+ t['\u3140'] = '\u111A';
+ t['\u3141'] = '\u1106';
+ t['\u3142'] = '\u1107';
+ t['\u3143'] = '\u1108';
+ t['\u3144'] = '\u1121';
+ t['\u3145'] = '\u1109';
+ t['\u3146'] = '\u110A';
+ t['\u3147'] = '\u110B';
+ t['\u3148'] = '\u110C';
+ t['\u3149'] = '\u110D';
+ t['\u314A'] = '\u110E';
+ t['\u314B'] = '\u110F';
+ t['\u314C'] = '\u1110';
+ t['\u314D'] = '\u1111';
+ t['\u314E'] = '\u1112';
+ t['\u314F'] = '\u1161';
+ t['\u3150'] = '\u1162';
+ t['\u3151'] = '\u1163';
+ t['\u3152'] = '\u1164';
+ t['\u3153'] = '\u1165';
+ t['\u3154'] = '\u1166';
+ t['\u3155'] = '\u1167';
+ t['\u3156'] = '\u1168';
+ t['\u3157'] = '\u1169';
+ t['\u3158'] = '\u116A';
+ t['\u3159'] = '\u116B';
+ t['\u315A'] = '\u116C';
+ t['\u315B'] = '\u116D';
+ t['\u315C'] = '\u116E';
+ t['\u315D'] = '\u116F';
+ t['\u315E'] = '\u1170';
+ t['\u315F'] = '\u1171';
+ t['\u3160'] = '\u1172';
+ t['\u3161'] = '\u1173';
+ t['\u3162'] = '\u1174';
+ t['\u3163'] = '\u1175';
+ t['\u3164'] = '\u1160';
+ t['\u3165'] = '\u1114';
+ t['\u3166'] = '\u1115';
+ t['\u3167'] = '\u11C7';
+ t['\u3168'] = '\u11C8';
+ t['\u3169'] = '\u11CC';
+ t['\u316A'] = '\u11CE';
+ t['\u316B'] = '\u11D3';
+ t['\u316C'] = '\u11D7';
+ t['\u316D'] = '\u11D9';
+ t['\u316E'] = '\u111C';
+ t['\u316F'] = '\u11DD';
+ t['\u3170'] = '\u11DF';
+ t['\u3171'] = '\u111D';
+ t['\u3172'] = '\u111E';
+ t['\u3173'] = '\u1120';
+ t['\u3174'] = '\u1122';
+ t['\u3175'] = '\u1123';
+ t['\u3176'] = '\u1127';
+ t['\u3177'] = '\u1129';
+ t['\u3178'] = '\u112B';
+ t['\u3179'] = '\u112C';
+ t['\u317A'] = '\u112D';
+ t['\u317B'] = '\u112E';
+ t['\u317C'] = '\u112F';
+ t['\u317D'] = '\u1132';
+ t['\u317E'] = '\u1136';
+ t['\u317F'] = '\u1140';
+ t['\u3180'] = '\u1147';
+ t['\u3181'] = '\u114C';
+ t['\u3182'] = '\u11F1';
+ t['\u3183'] = '\u11F2';
+ t['\u3184'] = '\u1157';
+ t['\u3185'] = '\u1158';
+ t['\u3186'] = '\u1159';
+ t['\u3187'] = '\u1184';
+ t['\u3188'] = '\u1185';
+ t['\u3189'] = '\u1188';
+ t['\u318A'] = '\u1191';
+ t['\u318B'] = '\u1192';
+ t['\u318C'] = '\u1194';
+ t['\u318D'] = '\u119E';
+ t['\u318E'] = '\u11A1';
+ t['\u3200'] = '\u0028\u1100\u0029';
+ t['\u3201'] = '\u0028\u1102\u0029';
+ t['\u3202'] = '\u0028\u1103\u0029';
+ t['\u3203'] = '\u0028\u1105\u0029';
+ t['\u3204'] = '\u0028\u1106\u0029';
+ t['\u3205'] = '\u0028\u1107\u0029';
+ t['\u3206'] = '\u0028\u1109\u0029';
+ t['\u3207'] = '\u0028\u110B\u0029';
+ t['\u3208'] = '\u0028\u110C\u0029';
+ t['\u3209'] = '\u0028\u110E\u0029';
+ t['\u320A'] = '\u0028\u110F\u0029';
+ t['\u320B'] = '\u0028\u1110\u0029';
+ t['\u320C'] = '\u0028\u1111\u0029';
+ t['\u320D'] = '\u0028\u1112\u0029';
+ t['\u320E'] = '\u0028\u1100\u1161\u0029';
+ t['\u320F'] = '\u0028\u1102\u1161\u0029';
+ t['\u3210'] = '\u0028\u1103\u1161\u0029';
+ t['\u3211'] = '\u0028\u1105\u1161\u0029';
+ t['\u3212'] = '\u0028\u1106\u1161\u0029';
+ t['\u3213'] = '\u0028\u1107\u1161\u0029';
+ t['\u3214'] = '\u0028\u1109\u1161\u0029';
+ t['\u3215'] = '\u0028\u110B\u1161\u0029';
+ t['\u3216'] = '\u0028\u110C\u1161\u0029';
+ t['\u3217'] = '\u0028\u110E\u1161\u0029';
+ t['\u3218'] = '\u0028\u110F\u1161\u0029';
+ t['\u3219'] = '\u0028\u1110\u1161\u0029';
+ t['\u321A'] = '\u0028\u1111\u1161\u0029';
+ t['\u321B'] = '\u0028\u1112\u1161\u0029';
+ t['\u321C'] = '\u0028\u110C\u116E\u0029';
+ t['\u321D'] = '\u0028\u110B\u1169\u110C\u1165\u11AB\u0029';
+ t['\u321E'] = '\u0028\u110B\u1169\u1112\u116E\u0029';
+ t['\u3220'] = '\u0028\u4E00\u0029';
+ t['\u3221'] = '\u0028\u4E8C\u0029';
+ t['\u3222'] = '\u0028\u4E09\u0029';
+ t['\u3223'] = '\u0028\u56DB\u0029';
+ t['\u3224'] = '\u0028\u4E94\u0029';
+ t['\u3225'] = '\u0028\u516D\u0029';
+ t['\u3226'] = '\u0028\u4E03\u0029';
+ t['\u3227'] = '\u0028\u516B\u0029';
+ t['\u3228'] = '\u0028\u4E5D\u0029';
+ t['\u3229'] = '\u0028\u5341\u0029';
+ t['\u322A'] = '\u0028\u6708\u0029';
+ t['\u322B'] = '\u0028\u706B\u0029';
+ t['\u322C'] = '\u0028\u6C34\u0029';
+ t['\u322D'] = '\u0028\u6728\u0029';
+ t['\u322E'] = '\u0028\u91D1\u0029';
+ t['\u322F'] = '\u0028\u571F\u0029';
+ t['\u3230'] = '\u0028\u65E5\u0029';
+ t['\u3231'] = '\u0028\u682A\u0029';
+ t['\u3232'] = '\u0028\u6709\u0029';
+ t['\u3233'] = '\u0028\u793E\u0029';
+ t['\u3234'] = '\u0028\u540D\u0029';
+ t['\u3235'] = '\u0028\u7279\u0029';
+ t['\u3236'] = '\u0028\u8CA1\u0029';
+ t['\u3237'] = '\u0028\u795D\u0029';
+ t['\u3238'] = '\u0028\u52B4\u0029';
+ t['\u3239'] = '\u0028\u4EE3\u0029';
+ t['\u323A'] = '\u0028\u547C\u0029';
+ t['\u323B'] = '\u0028\u5B66\u0029';
+ t['\u323C'] = '\u0028\u76E3\u0029';
+ t['\u323D'] = '\u0028\u4F01\u0029';
+ t['\u323E'] = '\u0028\u8CC7\u0029';
+ t['\u323F'] = '\u0028\u5354\u0029';
+ t['\u3240'] = '\u0028\u796D\u0029';
+ t['\u3241'] = '\u0028\u4F11\u0029';
+ t['\u3242'] = '\u0028\u81EA\u0029';
+ t['\u3243'] = '\u0028\u81F3\u0029';
+ t['\u32C0'] = '\u0031\u6708';
+ t['\u32C1'] = '\u0032\u6708';
+ t['\u32C2'] = '\u0033\u6708';
+ t['\u32C3'] = '\u0034\u6708';
+ t['\u32C4'] = '\u0035\u6708';
+ t['\u32C5'] = '\u0036\u6708';
+ t['\u32C6'] = '\u0037\u6708';
+ t['\u32C7'] = '\u0038\u6708';
+ t['\u32C8'] = '\u0039\u6708';
+ t['\u32C9'] = '\u0031\u0030\u6708';
+ t['\u32CA'] = '\u0031\u0031\u6708';
+ t['\u32CB'] = '\u0031\u0032\u6708';
+ t['\u3358'] = '\u0030\u70B9';
+ t['\u3359'] = '\u0031\u70B9';
+ t['\u335A'] = '\u0032\u70B9';
+ t['\u335B'] = '\u0033\u70B9';
+ t['\u335C'] = '\u0034\u70B9';
+ t['\u335D'] = '\u0035\u70B9';
+ t['\u335E'] = '\u0036\u70B9';
+ t['\u335F'] = '\u0037\u70B9';
+ t['\u3360'] = '\u0038\u70B9';
+ t['\u3361'] = '\u0039\u70B9';
+ t['\u3362'] = '\u0031\u0030\u70B9';
+ t['\u3363'] = '\u0031\u0031\u70B9';
+ t['\u3364'] = '\u0031\u0032\u70B9';
+ t['\u3365'] = '\u0031\u0033\u70B9';
+ t['\u3366'] = '\u0031\u0034\u70B9';
+ t['\u3367'] = '\u0031\u0035\u70B9';
+ t['\u3368'] = '\u0031\u0036\u70B9';
+ t['\u3369'] = '\u0031\u0037\u70B9';
+ t['\u336A'] = '\u0031\u0038\u70B9';
+ t['\u336B'] = '\u0031\u0039\u70B9';
+ t['\u336C'] = '\u0032\u0030\u70B9';
+ t['\u336D'] = '\u0032\u0031\u70B9';
+ t['\u336E'] = '\u0032\u0032\u70B9';
+ t['\u336F'] = '\u0032\u0033\u70B9';
+ t['\u3370'] = '\u0032\u0034\u70B9';
+ t['\u33E0'] = '\u0031\u65E5';
+ t['\u33E1'] = '\u0032\u65E5';
+ t['\u33E2'] = '\u0033\u65E5';
+ t['\u33E3'] = '\u0034\u65E5';
+ t['\u33E4'] = '\u0035\u65E5';
+ t['\u33E5'] = '\u0036\u65E5';
+ t['\u33E6'] = '\u0037\u65E5';
+ t['\u33E7'] = '\u0038\u65E5';
+ t['\u33E8'] = '\u0039\u65E5';
+ t['\u33E9'] = '\u0031\u0030\u65E5';
+ t['\u33EA'] = '\u0031\u0031\u65E5';
+ t['\u33EB'] = '\u0031\u0032\u65E5';
+ t['\u33EC'] = '\u0031\u0033\u65E5';
+ t['\u33ED'] = '\u0031\u0034\u65E5';
+ t['\u33EE'] = '\u0031\u0035\u65E5';
+ t['\u33EF'] = '\u0031\u0036\u65E5';
+ t['\u33F0'] = '\u0031\u0037\u65E5';
+ t['\u33F1'] = '\u0031\u0038\u65E5';
+ t['\u33F2'] = '\u0031\u0039\u65E5';
+ t['\u33F3'] = '\u0032\u0030\u65E5';
+ t['\u33F4'] = '\u0032\u0031\u65E5';
+ t['\u33F5'] = '\u0032\u0032\u65E5';
+ t['\u33F6'] = '\u0032\u0033\u65E5';
+ t['\u33F7'] = '\u0032\u0034\u65E5';
+ t['\u33F8'] = '\u0032\u0035\u65E5';
+ t['\u33F9'] = '\u0032\u0036\u65E5';
+ t['\u33FA'] = '\u0032\u0037\u65E5';
+ t['\u33FB'] = '\u0032\u0038\u65E5';
+ t['\u33FC'] = '\u0032\u0039\u65E5';
+ t['\u33FD'] = '\u0033\u0030\u65E5';
+ t['\u33FE'] = '\u0033\u0031\u65E5';
+ t['\uFB00'] = '\u0066\u0066';
+ t['\uFB01'] = '\u0066\u0069';
+ t['\uFB02'] = '\u0066\u006C';
+ t['\uFB03'] = '\u0066\u0066\u0069';
+ t['\uFB04'] = '\u0066\u0066\u006C';
+ t['\uFB05'] = '\u017F\u0074';
+ t['\uFB06'] = '\u0073\u0074';
+ t['\uFB13'] = '\u0574\u0576';
+ t['\uFB14'] = '\u0574\u0565';
+ t['\uFB15'] = '\u0574\u056B';
+ t['\uFB16'] = '\u057E\u0576';
+ t['\uFB17'] = '\u0574\u056D';
+ t['\uFB4F'] = '\u05D0\u05DC';
+ t['\uFB50'] = '\u0671';
+ t['\uFB51'] = '\u0671';
+ t['\uFB52'] = '\u067B';
+ t['\uFB53'] = '\u067B';
+ t['\uFB54'] = '\u067B';
+ t['\uFB55'] = '\u067B';
+ t['\uFB56'] = '\u067E';
+ t['\uFB57'] = '\u067E';
+ t['\uFB58'] = '\u067E';
+ t['\uFB59'] = '\u067E';
+ t['\uFB5A'] = '\u0680';
+ t['\uFB5B'] = '\u0680';
+ t['\uFB5C'] = '\u0680';
+ t['\uFB5D'] = '\u0680';
+ t['\uFB5E'] = '\u067A';
+ t['\uFB5F'] = '\u067A';
+ t['\uFB60'] = '\u067A';
+ t['\uFB61'] = '\u067A';
+ t['\uFB62'] = '\u067F';
+ t['\uFB63'] = '\u067F';
+ t['\uFB64'] = '\u067F';
+ t['\uFB65'] = '\u067F';
+ t['\uFB66'] = '\u0679';
+ t['\uFB67'] = '\u0679';
+ t['\uFB68'] = '\u0679';
+ t['\uFB69'] = '\u0679';
+ t['\uFB6A'] = '\u06A4';
+ t['\uFB6B'] = '\u06A4';
+ t['\uFB6C'] = '\u06A4';
+ t['\uFB6D'] = '\u06A4';
+ t['\uFB6E'] = '\u06A6';
+ t['\uFB6F'] = '\u06A6';
+ t['\uFB70'] = '\u06A6';
+ t['\uFB71'] = '\u06A6';
+ t['\uFB72'] = '\u0684';
+ t['\uFB73'] = '\u0684';
+ t['\uFB74'] = '\u0684';
+ t['\uFB75'] = '\u0684';
+ t['\uFB76'] = '\u0683';
+ t['\uFB77'] = '\u0683';
+ t['\uFB78'] = '\u0683';
+ t['\uFB79'] = '\u0683';
+ t['\uFB7A'] = '\u0686';
+ t['\uFB7B'] = '\u0686';
+ t['\uFB7C'] = '\u0686';
+ t['\uFB7D'] = '\u0686';
+ t['\uFB7E'] = '\u0687';
+ t['\uFB7F'] = '\u0687';
+ t['\uFB80'] = '\u0687';
+ t['\uFB81'] = '\u0687';
+ t['\uFB82'] = '\u068D';
+ t['\uFB83'] = '\u068D';
+ t['\uFB84'] = '\u068C';
+ t['\uFB85'] = '\u068C';
+ t['\uFB86'] = '\u068E';
+ t['\uFB87'] = '\u068E';
+ t['\uFB88'] = '\u0688';
+ t['\uFB89'] = '\u0688';
+ t['\uFB8A'] = '\u0698';
+ t['\uFB8B'] = '\u0698';
+ t['\uFB8C'] = '\u0691';
+ t['\uFB8D'] = '\u0691';
+ t['\uFB8E'] = '\u06A9';
+ t['\uFB8F'] = '\u06A9';
+ t['\uFB90'] = '\u06A9';
+ t['\uFB91'] = '\u06A9';
+ t['\uFB92'] = '\u06AF';
+ t['\uFB93'] = '\u06AF';
+ t['\uFB94'] = '\u06AF';
+ t['\uFB95'] = '\u06AF';
+ t['\uFB96'] = '\u06B3';
+ t['\uFB97'] = '\u06B3';
+ t['\uFB98'] = '\u06B3';
+ t['\uFB99'] = '\u06B3';
+ t['\uFB9A'] = '\u06B1';
+ t['\uFB9B'] = '\u06B1';
+ t['\uFB9C'] = '\u06B1';
+ t['\uFB9D'] = '\u06B1';
+ t['\uFB9E'] = '\u06BA';
+ t['\uFB9F'] = '\u06BA';
+ t['\uFBA0'] = '\u06BB';
+ t['\uFBA1'] = '\u06BB';
+ t['\uFBA2'] = '\u06BB';
+ t['\uFBA3'] = '\u06BB';
+ t['\uFBA4'] = '\u06C0';
+ t['\uFBA5'] = '\u06C0';
+ t['\uFBA6'] = '\u06C1';
+ t['\uFBA7'] = '\u06C1';
+ t['\uFBA8'] = '\u06C1';
+ t['\uFBA9'] = '\u06C1';
+ t['\uFBAA'] = '\u06BE';
+ t['\uFBAB'] = '\u06BE';
+ t['\uFBAC'] = '\u06BE';
+ t['\uFBAD'] = '\u06BE';
+ t['\uFBAE'] = '\u06D2';
+ t['\uFBAF'] = '\u06D2';
+ t['\uFBB0'] = '\u06D3';
+ t['\uFBB1'] = '\u06D3';
+ t['\uFBD3'] = '\u06AD';
+ t['\uFBD4'] = '\u06AD';
+ t['\uFBD5'] = '\u06AD';
+ t['\uFBD6'] = '\u06AD';
+ t['\uFBD7'] = '\u06C7';
+ t['\uFBD8'] = '\u06C7';
+ t['\uFBD9'] = '\u06C6';
+ t['\uFBDA'] = '\u06C6';
+ t['\uFBDB'] = '\u06C8';
+ t['\uFBDC'] = '\u06C8';
+ t['\uFBDD'] = '\u0677';
+ t['\uFBDE'] = '\u06CB';
+ t['\uFBDF'] = '\u06CB';
+ t['\uFBE0'] = '\u06C5';
+ t['\uFBE1'] = '\u06C5';
+ t['\uFBE2'] = '\u06C9';
+ t['\uFBE3'] = '\u06C9';
+ t['\uFBE4'] = '\u06D0';
+ t['\uFBE5'] = '\u06D0';
+ t['\uFBE6'] = '\u06D0';
+ t['\uFBE7'] = '\u06D0';
+ t['\uFBE8'] = '\u0649';
+ t['\uFBE9'] = '\u0649';
+ t['\uFBEA'] = '\u0626\u0627';
+ t['\uFBEB'] = '\u0626\u0627';
+ t['\uFBEC'] = '\u0626\u06D5';
+ t['\uFBED'] = '\u0626\u06D5';
+ t['\uFBEE'] = '\u0626\u0648';
+ t['\uFBEF'] = '\u0626\u0648';
+ t['\uFBF0'] = '\u0626\u06C7';
+ t['\uFBF1'] = '\u0626\u06C7';
+ t['\uFBF2'] = '\u0626\u06C6';
+ t['\uFBF3'] = '\u0626\u06C6';
+ t['\uFBF4'] = '\u0626\u06C8';
+ t['\uFBF5'] = '\u0626\u06C8';
+ t['\uFBF6'] = '\u0626\u06D0';
+ t['\uFBF7'] = '\u0626\u06D0';
+ t['\uFBF8'] = '\u0626\u06D0';
+ t['\uFBF9'] = '\u0626\u0649';
+ t['\uFBFA'] = '\u0626\u0649';
+ t['\uFBFB'] = '\u0626\u0649';
+ t['\uFBFC'] = '\u06CC';
+ t['\uFBFD'] = '\u06CC';
+ t['\uFBFE'] = '\u06CC';
+ t['\uFBFF'] = '\u06CC';
+ t['\uFC00'] = '\u0626\u062C';
+ t['\uFC01'] = '\u0626\u062D';
+ t['\uFC02'] = '\u0626\u0645';
+ t['\uFC03'] = '\u0626\u0649';
+ t['\uFC04'] = '\u0626\u064A';
+ t['\uFC05'] = '\u0628\u062C';
+ t['\uFC06'] = '\u0628\u062D';
+ t['\uFC07'] = '\u0628\u062E';
+ t['\uFC08'] = '\u0628\u0645';
+ t['\uFC09'] = '\u0628\u0649';
+ t['\uFC0A'] = '\u0628\u064A';
+ t['\uFC0B'] = '\u062A\u062C';
+ t['\uFC0C'] = '\u062A\u062D';
+ t['\uFC0D'] = '\u062A\u062E';
+ t['\uFC0E'] = '\u062A\u0645';
+ t['\uFC0F'] = '\u062A\u0649';
+ t['\uFC10'] = '\u062A\u064A';
+ t['\uFC11'] = '\u062B\u062C';
+ t['\uFC12'] = '\u062B\u0645';
+ t['\uFC13'] = '\u062B\u0649';
+ t['\uFC14'] = '\u062B\u064A';
+ t['\uFC15'] = '\u062C\u062D';
+ t['\uFC16'] = '\u062C\u0645';
+ t['\uFC17'] = '\u062D\u062C';
+ t['\uFC18'] = '\u062D\u0645';
+ t['\uFC19'] = '\u062E\u062C';
+ t['\uFC1A'] = '\u062E\u062D';
+ t['\uFC1B'] = '\u062E\u0645';
+ t['\uFC1C'] = '\u0633\u062C';
+ t['\uFC1D'] = '\u0633\u062D';
+ t['\uFC1E'] = '\u0633\u062E';
+ t['\uFC1F'] = '\u0633\u0645';
+ t['\uFC20'] = '\u0635\u062D';
+ t['\uFC21'] = '\u0635\u0645';
+ t['\uFC22'] = '\u0636\u062C';
+ t['\uFC23'] = '\u0636\u062D';
+ t['\uFC24'] = '\u0636\u062E';
+ t['\uFC25'] = '\u0636\u0645';
+ t['\uFC26'] = '\u0637\u062D';
+ t['\uFC27'] = '\u0637\u0645';
+ t['\uFC28'] = '\u0638\u0645';
+ t['\uFC29'] = '\u0639\u062C';
+ t['\uFC2A'] = '\u0639\u0645';
+ t['\uFC2B'] = '\u063A\u062C';
+ t['\uFC2C'] = '\u063A\u0645';
+ t['\uFC2D'] = '\u0641\u062C';
+ t['\uFC2E'] = '\u0641\u062D';
+ t['\uFC2F'] = '\u0641\u062E';
+ t['\uFC30'] = '\u0641\u0645';
+ t['\uFC31'] = '\u0641\u0649';
+ t['\uFC32'] = '\u0641\u064A';
+ t['\uFC33'] = '\u0642\u062D';
+ t['\uFC34'] = '\u0642\u0645';
+ t['\uFC35'] = '\u0642\u0649';
+ t['\uFC36'] = '\u0642\u064A';
+ t['\uFC37'] = '\u0643\u0627';
+ t['\uFC38'] = '\u0643\u062C';
+ t['\uFC39'] = '\u0643\u062D';
+ t['\uFC3A'] = '\u0643\u062E';
+ t['\uFC3B'] = '\u0643\u0644';
+ t['\uFC3C'] = '\u0643\u0645';
+ t['\uFC3D'] = '\u0643\u0649';
+ t['\uFC3E'] = '\u0643\u064A';
+ t['\uFC3F'] = '\u0644\u062C';
+ t['\uFC40'] = '\u0644\u062D';
+ t['\uFC41'] = '\u0644\u062E';
+ t['\uFC42'] = '\u0644\u0645';
+ t['\uFC43'] = '\u0644\u0649';
+ t['\uFC44'] = '\u0644\u064A';
+ t['\uFC45'] = '\u0645\u062C';
+ t['\uFC46'] = '\u0645\u062D';
+ t['\uFC47'] = '\u0645\u062E';
+ t['\uFC48'] = '\u0645\u0645';
+ t['\uFC49'] = '\u0645\u0649';
+ t['\uFC4A'] = '\u0645\u064A';
+ t['\uFC4B'] = '\u0646\u062C';
+ t['\uFC4C'] = '\u0646\u062D';
+ t['\uFC4D'] = '\u0646\u062E';
+ t['\uFC4E'] = '\u0646\u0645';
+ t['\uFC4F'] = '\u0646\u0649';
+ t['\uFC50'] = '\u0646\u064A';
+ t['\uFC51'] = '\u0647\u062C';
+ t['\uFC52'] = '\u0647\u0645';
+ t['\uFC53'] = '\u0647\u0649';
+ t['\uFC54'] = '\u0647\u064A';
+ t['\uFC55'] = '\u064A\u062C';
+ t['\uFC56'] = '\u064A\u062D';
+ t['\uFC57'] = '\u064A\u062E';
+ t['\uFC58'] = '\u064A\u0645';
+ t['\uFC59'] = '\u064A\u0649';
+ t['\uFC5A'] = '\u064A\u064A';
+ t['\uFC5B'] = '\u0630\u0670';
+ t['\uFC5C'] = '\u0631\u0670';
+ t['\uFC5D'] = '\u0649\u0670';
+ t['\uFC5E'] = '\u0020\u064C\u0651';
+ t['\uFC5F'] = '\u0020\u064D\u0651';
+ t['\uFC60'] = '\u0020\u064E\u0651';
+ t['\uFC61'] = '\u0020\u064F\u0651';
+ t['\uFC62'] = '\u0020\u0650\u0651';
+ t['\uFC63'] = '\u0020\u0651\u0670';
+ t['\uFC64'] = '\u0626\u0631';
+ t['\uFC65'] = '\u0626\u0632';
+ t['\uFC66'] = '\u0626\u0645';
+ t['\uFC67'] = '\u0626\u0646';
+ t['\uFC68'] = '\u0626\u0649';
+ t['\uFC69'] = '\u0626\u064A';
+ t['\uFC6A'] = '\u0628\u0631';
+ t['\uFC6B'] = '\u0628\u0632';
+ t['\uFC6C'] = '\u0628\u0645';
+ t['\uFC6D'] = '\u0628\u0646';
+ t['\uFC6E'] = '\u0628\u0649';
+ t['\uFC6F'] = '\u0628\u064A';
+ t['\uFC70'] = '\u062A\u0631';
+ t['\uFC71'] = '\u062A\u0632';
+ t['\uFC72'] = '\u062A\u0645';
+ t['\uFC73'] = '\u062A\u0646';
+ t['\uFC74'] = '\u062A\u0649';
+ t['\uFC75'] = '\u062A\u064A';
+ t['\uFC76'] = '\u062B\u0631';
+ t['\uFC77'] = '\u062B\u0632';
+ t['\uFC78'] = '\u062B\u0645';
+ t['\uFC79'] = '\u062B\u0646';
+ t['\uFC7A'] = '\u062B\u0649';
+ t['\uFC7B'] = '\u062B\u064A';
+ t['\uFC7C'] = '\u0641\u0649';
+ t['\uFC7D'] = '\u0641\u064A';
+ t['\uFC7E'] = '\u0642\u0649';
+ t['\uFC7F'] = '\u0642\u064A';
+ t['\uFC80'] = '\u0643\u0627';
+ t['\uFC81'] = '\u0643\u0644';
+ t['\uFC82'] = '\u0643\u0645';
+ t['\uFC83'] = '\u0643\u0649';
+ t['\uFC84'] = '\u0643\u064A';
+ t['\uFC85'] = '\u0644\u0645';
+ t['\uFC86'] = '\u0644\u0649';
+ t['\uFC87'] = '\u0644\u064A';
+ t['\uFC88'] = '\u0645\u0627';
+ t['\uFC89'] = '\u0645\u0645';
+ t['\uFC8A'] = '\u0646\u0631';
+ t['\uFC8B'] = '\u0646\u0632';
+ t['\uFC8C'] = '\u0646\u0645';
+ t['\uFC8D'] = '\u0646\u0646';
+ t['\uFC8E'] = '\u0646\u0649';
+ t['\uFC8F'] = '\u0646\u064A';
+ t['\uFC90'] = '\u0649\u0670';
+ t['\uFC91'] = '\u064A\u0631';
+ t['\uFC92'] = '\u064A\u0632';
+ t['\uFC93'] = '\u064A\u0645';
+ t['\uFC94'] = '\u064A\u0646';
+ t['\uFC95'] = '\u064A\u0649';
+ t['\uFC96'] = '\u064A\u064A';
+ t['\uFC97'] = '\u0626\u062C';
+ t['\uFC98'] = '\u0626\u062D';
+ t['\uFC99'] = '\u0626\u062E';
+ t['\uFC9A'] = '\u0626\u0645';
+ t['\uFC9B'] = '\u0626\u0647';
+ t['\uFC9C'] = '\u0628\u062C';
+ t['\uFC9D'] = '\u0628\u062D';
+ t['\uFC9E'] = '\u0628\u062E';
+ t['\uFC9F'] = '\u0628\u0645';
+ t['\uFCA0'] = '\u0628\u0647';
+ t['\uFCA1'] = '\u062A\u062C';
+ t['\uFCA2'] = '\u062A\u062D';
+ t['\uFCA3'] = '\u062A\u062E';
+ t['\uFCA4'] = '\u062A\u0645';
+ t['\uFCA5'] = '\u062A\u0647';
+ t['\uFCA6'] = '\u062B\u0645';
+ t['\uFCA7'] = '\u062C\u062D';
+ t['\uFCA8'] = '\u062C\u0645';
+ t['\uFCA9'] = '\u062D\u062C';
+ t['\uFCAA'] = '\u062D\u0645';
+ t['\uFCAB'] = '\u062E\u062C';
+ t['\uFCAC'] = '\u062E\u0645';
+ t['\uFCAD'] = '\u0633\u062C';
+ t['\uFCAE'] = '\u0633\u062D';
+ t['\uFCAF'] = '\u0633\u062E';
+ t['\uFCB0'] = '\u0633\u0645';
+ t['\uFCB1'] = '\u0635\u062D';
+ t['\uFCB2'] = '\u0635\u062E';
+ t['\uFCB3'] = '\u0635\u0645';
+ t['\uFCB4'] = '\u0636\u062C';
+ t['\uFCB5'] = '\u0636\u062D';
+ t['\uFCB6'] = '\u0636\u062E';
+ t['\uFCB7'] = '\u0636\u0645';
+ t['\uFCB8'] = '\u0637\u062D';
+ t['\uFCB9'] = '\u0638\u0645';
+ t['\uFCBA'] = '\u0639\u062C';
+ t['\uFCBB'] = '\u0639\u0645';
+ t['\uFCBC'] = '\u063A\u062C';
+ t['\uFCBD'] = '\u063A\u0645';
+ t['\uFCBE'] = '\u0641\u062C';
+ t['\uFCBF'] = '\u0641\u062D';
+ t['\uFCC0'] = '\u0641\u062E';
+ t['\uFCC1'] = '\u0641\u0645';
+ t['\uFCC2'] = '\u0642\u062D';
+ t['\uFCC3'] = '\u0642\u0645';
+ t['\uFCC4'] = '\u0643\u062C';
+ t['\uFCC5'] = '\u0643\u062D';
+ t['\uFCC6'] = '\u0643\u062E';
+ t['\uFCC7'] = '\u0643\u0644';
+ t['\uFCC8'] = '\u0643\u0645';
+ t['\uFCC9'] = '\u0644\u062C';
+ t['\uFCCA'] = '\u0644\u062D';
+ t['\uFCCB'] = '\u0644\u062E';
+ t['\uFCCC'] = '\u0644\u0645';
+ t['\uFCCD'] = '\u0644\u0647';
+ t['\uFCCE'] = '\u0645\u062C';
+ t['\uFCCF'] = '\u0645\u062D';
+ t['\uFCD0'] = '\u0645\u062E';
+ t['\uFCD1'] = '\u0645\u0645';
+ t['\uFCD2'] = '\u0646\u062C';
+ t['\uFCD3'] = '\u0646\u062D';
+ t['\uFCD4'] = '\u0646\u062E';
+ t['\uFCD5'] = '\u0646\u0645';
+ t['\uFCD6'] = '\u0646\u0647';
+ t['\uFCD7'] = '\u0647\u062C';
+ t['\uFCD8'] = '\u0647\u0645';
+ t['\uFCD9'] = '\u0647\u0670';
+ t['\uFCDA'] = '\u064A\u062C';
+ t['\uFCDB'] = '\u064A\u062D';
+ t['\uFCDC'] = '\u064A\u062E';
+ t['\uFCDD'] = '\u064A\u0645';
+ t['\uFCDE'] = '\u064A\u0647';
+ t['\uFCDF'] = '\u0626\u0645';
+ t['\uFCE0'] = '\u0626\u0647';
+ t['\uFCE1'] = '\u0628\u0645';
+ t['\uFCE2'] = '\u0628\u0647';
+ t['\uFCE3'] = '\u062A\u0645';
+ t['\uFCE4'] = '\u062A\u0647';
+ t['\uFCE5'] = '\u062B\u0645';
+ t['\uFCE6'] = '\u062B\u0647';
+ t['\uFCE7'] = '\u0633\u0645';
+ t['\uFCE8'] = '\u0633\u0647';
+ t['\uFCE9'] = '\u0634\u0645';
+ t['\uFCEA'] = '\u0634\u0647';
+ t['\uFCEB'] = '\u0643\u0644';
+ t['\uFCEC'] = '\u0643\u0645';
+ t['\uFCED'] = '\u0644\u0645';
+ t['\uFCEE'] = '\u0646\u0645';
+ t['\uFCEF'] = '\u0646\u0647';
+ t['\uFCF0'] = '\u064A\u0645';
+ t['\uFCF1'] = '\u064A\u0647';
+ t['\uFCF2'] = '\u0640\u064E\u0651';
+ t['\uFCF3'] = '\u0640\u064F\u0651';
+ t['\uFCF4'] = '\u0640\u0650\u0651';
+ t['\uFCF5'] = '\u0637\u0649';
+ t['\uFCF6'] = '\u0637\u064A';
+ t['\uFCF7'] = '\u0639\u0649';
+ t['\uFCF8'] = '\u0639\u064A';
+ t['\uFCF9'] = '\u063A\u0649';
+ t['\uFCFA'] = '\u063A\u064A';
+ t['\uFCFB'] = '\u0633\u0649';
+ t['\uFCFC'] = '\u0633\u064A';
+ t['\uFCFD'] = '\u0634\u0649';
+ t['\uFCFE'] = '\u0634\u064A';
+ t['\uFCFF'] = '\u062D\u0649';
+ t['\uFD00'] = '\u062D\u064A';
+ t['\uFD01'] = '\u062C\u0649';
+ t['\uFD02'] = '\u062C\u064A';
+ t['\uFD03'] = '\u062E\u0649';
+ t['\uFD04'] = '\u062E\u064A';
+ t['\uFD05'] = '\u0635\u0649';
+ t['\uFD06'] = '\u0635\u064A';
+ t['\uFD07'] = '\u0636\u0649';
+ t['\uFD08'] = '\u0636\u064A';
+ t['\uFD09'] = '\u0634\u062C';
+ t['\uFD0A'] = '\u0634\u062D';
+ t['\uFD0B'] = '\u0634\u062E';
+ t['\uFD0C'] = '\u0634\u0645';
+ t['\uFD0D'] = '\u0634\u0631';
+ t['\uFD0E'] = '\u0633\u0631';
+ t['\uFD0F'] = '\u0635\u0631';
+ t['\uFD10'] = '\u0636\u0631';
+ t['\uFD11'] = '\u0637\u0649';
+ t['\uFD12'] = '\u0637\u064A';
+ t['\uFD13'] = '\u0639\u0649';
+ t['\uFD14'] = '\u0639\u064A';
+ t['\uFD15'] = '\u063A\u0649';
+ t['\uFD16'] = '\u063A\u064A';
+ t['\uFD17'] = '\u0633\u0649';
+ t['\uFD18'] = '\u0633\u064A';
+ t['\uFD19'] = '\u0634\u0649';
+ t['\uFD1A'] = '\u0634\u064A';
+ t['\uFD1B'] = '\u062D\u0649';
+ t['\uFD1C'] = '\u062D\u064A';
+ t['\uFD1D'] = '\u062C\u0649';
+ t['\uFD1E'] = '\u062C\u064A';
+ t['\uFD1F'] = '\u062E\u0649';
+ t['\uFD20'] = '\u062E\u064A';
+ t['\uFD21'] = '\u0635\u0649';
+ t['\uFD22'] = '\u0635\u064A';
+ t['\uFD23'] = '\u0636\u0649';
+ t['\uFD24'] = '\u0636\u064A';
+ t['\uFD25'] = '\u0634\u062C';
+ t['\uFD26'] = '\u0634\u062D';
+ t['\uFD27'] = '\u0634\u062E';
+ t['\uFD28'] = '\u0634\u0645';
+ t['\uFD29'] = '\u0634\u0631';
+ t['\uFD2A'] = '\u0633\u0631';
+ t['\uFD2B'] = '\u0635\u0631';
+ t['\uFD2C'] = '\u0636\u0631';
+ t['\uFD2D'] = '\u0634\u062C';
+ t['\uFD2E'] = '\u0634\u062D';
+ t['\uFD2F'] = '\u0634\u062E';
+ t['\uFD30'] = '\u0634\u0645';
+ t['\uFD31'] = '\u0633\u0647';
+ t['\uFD32'] = '\u0634\u0647';
+ t['\uFD33'] = '\u0637\u0645';
+ t['\uFD34'] = '\u0633\u062C';
+ t['\uFD35'] = '\u0633\u062D';
+ t['\uFD36'] = '\u0633\u062E';
+ t['\uFD37'] = '\u0634\u062C';
+ t['\uFD38'] = '\u0634\u062D';
+ t['\uFD39'] = '\u0634\u062E';
+ t['\uFD3A'] = '\u0637\u0645';
+ t['\uFD3B'] = '\u0638\u0645';
+ t['\uFD3C'] = '\u0627\u064B';
+ t['\uFD3D'] = '\u0627\u064B';
+ t['\uFD50'] = '\u062A\u062C\u0645';
+ t['\uFD51'] = '\u062A\u062D\u062C';
+ t['\uFD52'] = '\u062A\u062D\u062C';
+ t['\uFD53'] = '\u062A\u062D\u0645';
+ t['\uFD54'] = '\u062A\u062E\u0645';
+ t['\uFD55'] = '\u062A\u0645\u062C';
+ t['\uFD56'] = '\u062A\u0645\u062D';
+ t['\uFD57'] = '\u062A\u0645\u062E';
+ t['\uFD58'] = '\u062C\u0645\u062D';
+ t['\uFD59'] = '\u062C\u0645\u062D';
+ t['\uFD5A'] = '\u062D\u0645\u064A';
+ t['\uFD5B'] = '\u062D\u0645\u0649';
+ t['\uFD5C'] = '\u0633\u062D\u062C';
+ t['\uFD5D'] = '\u0633\u062C\u062D';
+ t['\uFD5E'] = '\u0633\u062C\u0649';
+ t['\uFD5F'] = '\u0633\u0645\u062D';
+ t['\uFD60'] = '\u0633\u0645\u062D';
+ t['\uFD61'] = '\u0633\u0645\u062C';
+ t['\uFD62'] = '\u0633\u0645\u0645';
+ t['\uFD63'] = '\u0633\u0645\u0645';
+ t['\uFD64'] = '\u0635\u062D\u062D';
+ t['\uFD65'] = '\u0635\u062D\u062D';
+ t['\uFD66'] = '\u0635\u0645\u0645';
+ t['\uFD67'] = '\u0634\u062D\u0645';
+ t['\uFD68'] = '\u0634\u062D\u0645';
+ t['\uFD69'] = '\u0634\u062C\u064A';
+ t['\uFD6A'] = '\u0634\u0645\u062E';
+ t['\uFD6B'] = '\u0634\u0645\u062E';
+ t['\uFD6C'] = '\u0634\u0645\u0645';
+ t['\uFD6D'] = '\u0634\u0645\u0645';
+ t['\uFD6E'] = '\u0636\u062D\u0649';
+ t['\uFD6F'] = '\u0636\u062E\u0645';
+ t['\uFD70'] = '\u0636\u062E\u0645';
+ t['\uFD71'] = '\u0637\u0645\u062D';
+ t['\uFD72'] = '\u0637\u0645\u062D';
+ t['\uFD73'] = '\u0637\u0645\u0645';
+ t['\uFD74'] = '\u0637\u0645\u064A';
+ t['\uFD75'] = '\u0639\u062C\u0645';
+ t['\uFD76'] = '\u0639\u0645\u0645';
+ t['\uFD77'] = '\u0639\u0645\u0645';
+ t['\uFD78'] = '\u0639\u0645\u0649';
+ t['\uFD79'] = '\u063A\u0645\u0645';
+ t['\uFD7A'] = '\u063A\u0645\u064A';
+ t['\uFD7B'] = '\u063A\u0645\u0649';
+ t['\uFD7C'] = '\u0641\u062E\u0645';
+ t['\uFD7D'] = '\u0641\u062E\u0645';
+ t['\uFD7E'] = '\u0642\u0645\u062D';
+ t['\uFD7F'] = '\u0642\u0645\u0645';
+ t['\uFD80'] = '\u0644\u062D\u0645';
+ t['\uFD81'] = '\u0644\u062D\u064A';
+ t['\uFD82'] = '\u0644\u062D\u0649';
+ t['\uFD83'] = '\u0644\u062C\u062C';
+ t['\uFD84'] = '\u0644\u062C\u062C';
+ t['\uFD85'] = '\u0644\u062E\u0645';
+ t['\uFD86'] = '\u0644\u062E\u0645';
+ t['\uFD87'] = '\u0644\u0645\u062D';
+ t['\uFD88'] = '\u0644\u0645\u062D';
+ t['\uFD89'] = '\u0645\u062D\u062C';
+ t['\uFD8A'] = '\u0645\u062D\u0645';
+ t['\uFD8B'] = '\u0645\u062D\u064A';
+ t['\uFD8C'] = '\u0645\u062C\u062D';
+ t['\uFD8D'] = '\u0645\u062C\u0645';
+ t['\uFD8E'] = '\u0645\u062E\u062C';
+ t['\uFD8F'] = '\u0645\u062E\u0645';
+ t['\uFD92'] = '\u0645\u062C\u062E';
+ t['\uFD93'] = '\u0647\u0645\u062C';
+ t['\uFD94'] = '\u0647\u0645\u0645';
+ t['\uFD95'] = '\u0646\u062D\u0645';
+ t['\uFD96'] = '\u0646\u062D\u0649';
+ t['\uFD97'] = '\u0646\u062C\u0645';
+ t['\uFD98'] = '\u0646\u062C\u0645';
+ t['\uFD99'] = '\u0646\u062C\u0649';
+ t['\uFD9A'] = '\u0646\u0645\u064A';
+ t['\uFD9B'] = '\u0646\u0645\u0649';
+ t['\uFD9C'] = '\u064A\u0645\u0645';
+ t['\uFD9D'] = '\u064A\u0645\u0645';
+ t['\uFD9E'] = '\u0628\u062E\u064A';
+ t['\uFD9F'] = '\u062A\u062C\u064A';
+ t['\uFDA0'] = '\u062A\u062C\u0649';
+ t['\uFDA1'] = '\u062A\u062E\u064A';
+ t['\uFDA2'] = '\u062A\u062E\u0649';
+ t['\uFDA3'] = '\u062A\u0645\u064A';
+ t['\uFDA4'] = '\u062A\u0645\u0649';
+ t['\uFDA5'] = '\u062C\u0645\u064A';
+ t['\uFDA6'] = '\u062C\u062D\u0649';
+ t['\uFDA7'] = '\u062C\u0645\u0649';
+ t['\uFDA8'] = '\u0633\u062E\u0649';
+ t['\uFDA9'] = '\u0635\u062D\u064A';
+ t['\uFDAA'] = '\u0634\u062D\u064A';
+ t['\uFDAB'] = '\u0636\u062D\u064A';
+ t['\uFDAC'] = '\u0644\u062C\u064A';
+ t['\uFDAD'] = '\u0644\u0645\u064A';
+ t['\uFDAE'] = '\u064A\u062D\u064A';
+ t['\uFDAF'] = '\u064A\u062C\u064A';
+ t['\uFDB0'] = '\u064A\u0645\u064A';
+ t['\uFDB1'] = '\u0645\u0645\u064A';
+ t['\uFDB2'] = '\u0642\u0645\u064A';
+ t['\uFDB3'] = '\u0646\u062D\u064A';
+ t['\uFDB4'] = '\u0642\u0645\u062D';
+ t['\uFDB5'] = '\u0644\u062D\u0645';
+ t['\uFDB6'] = '\u0639\u0645\u064A';
+ t['\uFDB7'] = '\u0643\u0645\u064A';
+ t['\uFDB8'] = '\u0646\u062C\u062D';
+ t['\uFDB9'] = '\u0645\u062E\u064A';
+ t['\uFDBA'] = '\u0644\u062C\u0645';
+ t['\uFDBB'] = '\u0643\u0645\u0645';
+ t['\uFDBC'] = '\u0644\u062C\u0645';
+ t['\uFDBD'] = '\u0646\u062C\u062D';
+ t['\uFDBE'] = '\u062C\u062D\u064A';
+ t['\uFDBF'] = '\u062D\u062C\u064A';
+ t['\uFDC0'] = '\u0645\u062C\u064A';
+ t['\uFDC1'] = '\u0641\u0645\u064A';
+ t['\uFDC2'] = '\u0628\u062D\u064A';
+ t['\uFDC3'] = '\u0643\u0645\u0645';
+ t['\uFDC4'] = '\u0639\u062C\u0645';
+ t['\uFDC5'] = '\u0635\u0645\u0645';
+ t['\uFDC6'] = '\u0633\u062E\u064A';
+ t['\uFDC7'] = '\u0646\u062C\u064A';
+ t['\uFE49'] = '\u203E';
+ t['\uFE4A'] = '\u203E';
+ t['\uFE4B'] = '\u203E';
+ t['\uFE4C'] = '\u203E';
+ t['\uFE4D'] = '\u005F';
+ t['\uFE4E'] = '\u005F';
+ t['\uFE4F'] = '\u005F';
+ t['\uFE80'] = '\u0621';
+ t['\uFE81'] = '\u0622';
+ t['\uFE82'] = '\u0622';
+ t['\uFE83'] = '\u0623';
+ t['\uFE84'] = '\u0623';
+ t['\uFE85'] = '\u0624';
+ t['\uFE86'] = '\u0624';
+ t['\uFE87'] = '\u0625';
+ t['\uFE88'] = '\u0625';
+ t['\uFE89'] = '\u0626';
+ t['\uFE8A'] = '\u0626';
+ t['\uFE8B'] = '\u0626';
+ t['\uFE8C'] = '\u0626';
+ t['\uFE8D'] = '\u0627';
+ t['\uFE8E'] = '\u0627';
+ t['\uFE8F'] = '\u0628';
+ t['\uFE90'] = '\u0628';
+ t['\uFE91'] = '\u0628';
+ t['\uFE92'] = '\u0628';
+ t['\uFE93'] = '\u0629';
+ t['\uFE94'] = '\u0629';
+ t['\uFE95'] = '\u062A';
+ t['\uFE96'] = '\u062A';
+ t['\uFE97'] = '\u062A';
+ t['\uFE98'] = '\u062A';
+ t['\uFE99'] = '\u062B';
+ t['\uFE9A'] = '\u062B';
+ t['\uFE9B'] = '\u062B';
+ t['\uFE9C'] = '\u062B';
+ t['\uFE9D'] = '\u062C';
+ t['\uFE9E'] = '\u062C';
+ t['\uFE9F'] = '\u062C';
+ t['\uFEA0'] = '\u062C';
+ t['\uFEA1'] = '\u062D';
+ t['\uFEA2'] = '\u062D';
+ t['\uFEA3'] = '\u062D';
+ t['\uFEA4'] = '\u062D';
+ t['\uFEA5'] = '\u062E';
+ t['\uFEA6'] = '\u062E';
+ t['\uFEA7'] = '\u062E';
+ t['\uFEA8'] = '\u062E';
+ t['\uFEA9'] = '\u062F';
+ t['\uFEAA'] = '\u062F';
+ t['\uFEAB'] = '\u0630';
+ t['\uFEAC'] = '\u0630';
+ t['\uFEAD'] = '\u0631';
+ t['\uFEAE'] = '\u0631';
+ t['\uFEAF'] = '\u0632';
+ t['\uFEB0'] = '\u0632';
+ t['\uFEB1'] = '\u0633';
+ t['\uFEB2'] = '\u0633';
+ t['\uFEB3'] = '\u0633';
+ t['\uFEB4'] = '\u0633';
+ t['\uFEB5'] = '\u0634';
+ t['\uFEB6'] = '\u0634';
+ t['\uFEB7'] = '\u0634';
+ t['\uFEB8'] = '\u0634';
+ t['\uFEB9'] = '\u0635';
+ t['\uFEBA'] = '\u0635';
+ t['\uFEBB'] = '\u0635';
+ t['\uFEBC'] = '\u0635';
+ t['\uFEBD'] = '\u0636';
+ t['\uFEBE'] = '\u0636';
+ t['\uFEBF'] = '\u0636';
+ t['\uFEC0'] = '\u0636';
+ t['\uFEC1'] = '\u0637';
+ t['\uFEC2'] = '\u0637';
+ t['\uFEC3'] = '\u0637';
+ t['\uFEC4'] = '\u0637';
+ t['\uFEC5'] = '\u0638';
+ t['\uFEC6'] = '\u0638';
+ t['\uFEC7'] = '\u0638';
+ t['\uFEC8'] = '\u0638';
+ t['\uFEC9'] = '\u0639';
+ t['\uFECA'] = '\u0639';
+ t['\uFECB'] = '\u0639';
+ t['\uFECC'] = '\u0639';
+ t['\uFECD'] = '\u063A';
+ t['\uFECE'] = '\u063A';
+ t['\uFECF'] = '\u063A';
+ t['\uFED0'] = '\u063A';
+ t['\uFED1'] = '\u0641';
+ t['\uFED2'] = '\u0641';
+ t['\uFED3'] = '\u0641';
+ t['\uFED4'] = '\u0641';
+ t['\uFED5'] = '\u0642';
+ t['\uFED6'] = '\u0642';
+ t['\uFED7'] = '\u0642';
+ t['\uFED8'] = '\u0642';
+ t['\uFED9'] = '\u0643';
+ t['\uFEDA'] = '\u0643';
+ t['\uFEDB'] = '\u0643';
+ t['\uFEDC'] = '\u0643';
+ t['\uFEDD'] = '\u0644';
+ t['\uFEDE'] = '\u0644';
+ t['\uFEDF'] = '\u0644';
+ t['\uFEE0'] = '\u0644';
+ t['\uFEE1'] = '\u0645';
+ t['\uFEE2'] = '\u0645';
+ t['\uFEE3'] = '\u0645';
+ t['\uFEE4'] = '\u0645';
+ t['\uFEE5'] = '\u0646';
+ t['\uFEE6'] = '\u0646';
+ t['\uFEE7'] = '\u0646';
+ t['\uFEE8'] = '\u0646';
+ t['\uFEE9'] = '\u0647';
+ t['\uFEEA'] = '\u0647';
+ t['\uFEEB'] = '\u0647';
+ t['\uFEEC'] = '\u0647';
+ t['\uFEED'] = '\u0648';
+ t['\uFEEE'] = '\u0648';
+ t['\uFEEF'] = '\u0649';
+ t['\uFEF0'] = '\u0649';
+ t['\uFEF1'] = '\u064A';
+ t['\uFEF2'] = '\u064A';
+ t['\uFEF3'] = '\u064A';
+ t['\uFEF4'] = '\u064A';
+ t['\uFEF5'] = '\u0644\u0622';
+ t['\uFEF6'] = '\u0644\u0622';
+ t['\uFEF7'] = '\u0644\u0623';
+ t['\uFEF8'] = '\u0644\u0623';
+ t['\uFEF9'] = '\u0644\u0625';
+ t['\uFEFA'] = '\u0644\u0625';
+ t['\uFEFB'] = '\u0644\u0627';
+ t['\uFEFC'] = '\u0644\u0627';
+});
+function reverseIfRtl(chars) {
+ var charsLength = chars.length;
+ if (charsLength <= 1 || !isRTLRangeFor(chars.charCodeAt(0))) {
+ return chars;
+ }
+ var s = '';
+ for (var ii = charsLength - 1; ii >= 0; ii--) {
+ s += chars[ii];
+ }
+ return s;
+}
+exports.mapSpecialUnicodeValues = mapSpecialUnicodeValues;
+exports.reverseIfRtl = reverseIfRtl;
+exports.getUnicodeRangeFor = getUnicodeRangeFor;
+exports.getNormalizedUnicodes = getNormalizedUnicodes;
+exports.getUnicodeForGlyph = getUnicodeForGlyph;
+
+/***/ }),
+/* 181 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.FontRendererFactory = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _cff_parser = __w_pdfjs_require__(175);
+
+var _glyphlist = __w_pdfjs_require__(178);
+
+var _encodings = __w_pdfjs_require__(177);
+
+var _stream = __w_pdfjs_require__(158);
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
+
+function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
+
+function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
+
+function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var FontRendererFactory = function FontRendererFactoryClosure() {
+ function getLong(data, offset) {
+ return data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
+ }
+
+ function getUshort(data, offset) {
+ return data[offset] << 8 | data[offset + 1];
+ }
+
+ function parseCmap(data, start, end) {
+ var offset = getUshort(data, start + 2) === 1 ? getLong(data, start + 8) : getLong(data, start + 16);
+ var format = getUshort(data, start + offset);
+ var ranges, p, i;
+
+ if (format === 4) {
+ getUshort(data, start + offset + 2);
+ var segCount = getUshort(data, start + offset + 6) >> 1;
+ p = start + offset + 14;
+ ranges = [];
+
+ for (i = 0; i < segCount; i++, p += 2) {
+ ranges[i] = {
+ end: getUshort(data, p)
+ };
+ }
+
+ p += 2;
+
+ for (i = 0; i < segCount; i++, p += 2) {
+ ranges[i].start = getUshort(data, p);
+ }
+
+ for (i = 0; i < segCount; i++, p += 2) {
+ ranges[i].idDelta = getUshort(data, p);
+ }
+
+ for (i = 0; i < segCount; i++, p += 2) {
+ var idOffset = getUshort(data, p);
+
+ if (idOffset === 0) {
+ continue;
+ }
+
+ ranges[i].ids = [];
+
+ for (var j = 0, jj = ranges[i].end - ranges[i].start + 1; j < jj; j++) {
+ ranges[i].ids[j] = getUshort(data, p + idOffset);
+ idOffset += 2;
+ }
+ }
+
+ return ranges;
+ } else if (format === 12) {
+ getLong(data, start + offset + 4);
+ var groups = getLong(data, start + offset + 12);
+ p = start + offset + 16;
+ ranges = [];
+
+ for (i = 0; i < groups; i++) {
+ ranges.push({
+ start: getLong(data, p),
+ end: getLong(data, p + 4),
+ idDelta: getLong(data, p + 8) - getLong(data, p)
+ });
+ p += 12;
+ }
+
+ return ranges;
+ }
+
+ throw new _util.FormatError("unsupported cmap: ".concat(format));
+ }
+
+ function parseCff(data, start, end, seacAnalysisEnabled) {
+ var properties = {};
+ var parser = new _cff_parser.CFFParser(new _stream.Stream(data, start, end - start), properties, seacAnalysisEnabled);
+ var cff = parser.parse();
+ return {
+ glyphs: cff.charStrings.objects,
+ subrs: cff.topDict.privateDict && cff.topDict.privateDict.subrsIndex && cff.topDict.privateDict.subrsIndex.objects,
+ gsubrs: cff.globalSubrIndex && cff.globalSubrIndex.objects,
+ isCFFCIDFont: cff.isCIDFont,
+ fdSelect: cff.fdSelect,
+ fdArray: cff.fdArray
+ };
+ }
+
+ function parseGlyfTable(glyf, loca, isGlyphLocationsLong) {
+ var itemSize, itemDecode;
+
+ if (isGlyphLocationsLong) {
+ itemSize = 4;
+
+ itemDecode = function fontItemDecodeLong(data, offset) {
+ return data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
+ };
+ } else {
+ itemSize = 2;
+
+ itemDecode = function fontItemDecode(data, offset) {
+ return data[offset] << 9 | data[offset + 1] << 1;
+ };
+ }
+
+ var glyphs = [];
+ var startOffset = itemDecode(loca, 0);
+
+ for (var j = itemSize; j < loca.length; j += itemSize) {
+ var endOffset = itemDecode(loca, j);
+ glyphs.push(glyf.subarray(startOffset, endOffset));
+ startOffset = endOffset;
+ }
+
+ return glyphs;
+ }
+
+ function lookupCmap(ranges, unicode) {
+ var code = unicode.codePointAt(0),
+ gid = 0;
+ var l = 0,
+ r = ranges.length - 1;
+
+ while (l < r) {
+ var c = l + r + 1 >> 1;
+
+ if (code < ranges[c].start) {
+ r = c - 1;
+ } else {
+ l = c;
+ }
+ }
+
+ if (ranges[l].start <= code && code <= ranges[l].end) {
+ gid = ranges[l].idDelta + (ranges[l].ids ? ranges[l].ids[code - ranges[l].start] : code) & 0xFFFF;
+ }
+
+ return {
+ charCode: code,
+ glyphId: gid
+ };
+ }
+
+ function compileGlyf(code, cmds, font) {
+ function moveTo(x, y) {
+ cmds.push({
+ cmd: 'moveTo',
+ args: [x, y]
+ });
+ }
+
+ function lineTo(x, y) {
+ cmds.push({
+ cmd: 'lineTo',
+ args: [x, y]
+ });
+ }
+
+ function quadraticCurveTo(xa, ya, x, y) {
+ cmds.push({
+ cmd: 'quadraticCurveTo',
+ args: [xa, ya, x, y]
+ });
+ }
+
+ var i = 0;
+ var numberOfContours = (code[i] << 24 | code[i + 1] << 16) >> 16;
+ var flags;
+ var x = 0,
+ y = 0;
+ i += 10;
+
+ if (numberOfContours < 0) {
+ do {
+ flags = code[i] << 8 | code[i + 1];
+ var glyphIndex = code[i + 2] << 8 | code[i + 3];
+ i += 4;
+ var arg1, arg2;
+
+ if (flags & 0x01) {
+ arg1 = (code[i] << 24 | code[i + 1] << 16) >> 16;
+ arg2 = (code[i + 2] << 24 | code[i + 3] << 16) >> 16;
+ i += 4;
+ } else {
+ arg1 = code[i++];
+ arg2 = code[i++];
+ }
+
+ if (flags & 0x02) {
+ x = arg1;
+ y = arg2;
+ } else {
+ x = 0;
+ y = 0;
+ }
+
+ var scaleX = 1,
+ scaleY = 1,
+ scale01 = 0,
+ scale10 = 0;
+
+ if (flags & 0x08) {
+ scaleX = scaleY = (code[i] << 24 | code[i + 1] << 16) / 1073741824;
+ i += 2;
+ } else if (flags & 0x40) {
+ scaleX = (code[i] << 24 | code[i + 1] << 16) / 1073741824;
+ scaleY = (code[i + 2] << 24 | code[i + 3] << 16) / 1073741824;
+ i += 4;
+ } else if (flags & 0x80) {
+ scaleX = (code[i] << 24 | code[i + 1] << 16) / 1073741824;
+ scale01 = (code[i + 2] << 24 | code[i + 3] << 16) / 1073741824;
+ scale10 = (code[i + 4] << 24 | code[i + 5] << 16) / 1073741824;
+ scaleY = (code[i + 6] << 24 | code[i + 7] << 16) / 1073741824;
+ i += 8;
+ }
+
+ var subglyph = font.glyphs[glyphIndex];
+
+ if (subglyph) {
+ cmds.push({
+ cmd: 'save'
+ });
+ cmds.push({
+ cmd: 'transform',
+ args: [scaleX, scale01, scale10, scaleY, x, y]
+ });
+ compileGlyf(subglyph, cmds, font);
+ cmds.push({
+ cmd: 'restore'
+ });
+ }
+ } while (flags & 0x20);
+ } else {
+ var endPtsOfContours = [];
+ var j, jj;
+
+ for (j = 0; j < numberOfContours; j++) {
+ endPtsOfContours.push(code[i] << 8 | code[i + 1]);
+ i += 2;
+ }
+
+ var instructionLength = code[i] << 8 | code[i + 1];
+ i += 2 + instructionLength;
+ var numberOfPoints = endPtsOfContours[endPtsOfContours.length - 1] + 1;
+ var points = [];
+
+ while (points.length < numberOfPoints) {
+ flags = code[i++];
+ var repeat = 1;
+
+ if (flags & 0x08) {
+ repeat += code[i++];
+ }
+
+ while (repeat-- > 0) {
+ points.push({
+ flags: flags
+ });
+ }
+ }
+
+ for (j = 0; j < numberOfPoints; j++) {
+ switch (points[j].flags & 0x12) {
+ case 0x00:
+ x += (code[i] << 24 | code[i + 1] << 16) >> 16;
+ i += 2;
+ break;
+
+ case 0x02:
+ x -= code[i++];
+ break;
+
+ case 0x12:
+ x += code[i++];
+ break;
+ }
+
+ points[j].x = x;
+ }
+
+ for (j = 0; j < numberOfPoints; j++) {
+ switch (points[j].flags & 0x24) {
+ case 0x00:
+ y += (code[i] << 24 | code[i + 1] << 16) >> 16;
+ i += 2;
+ break;
+
+ case 0x04:
+ y -= code[i++];
+ break;
+
+ case 0x24:
+ y += code[i++];
+ break;
+ }
+
+ points[j].y = y;
+ }
+
+ var startPoint = 0;
+
+ for (i = 0; i < numberOfContours; i++) {
+ var endPoint = endPtsOfContours[i];
+ var contour = points.slice(startPoint, endPoint + 1);
+
+ if (contour[0].flags & 1) {
+ contour.push(contour[0]);
+ } else if (contour[contour.length - 1].flags & 1) {
+ contour.unshift(contour[contour.length - 1]);
+ } else {
+ var p = {
+ flags: 1,
+ x: (contour[0].x + contour[contour.length - 1].x) / 2,
+ y: (contour[0].y + contour[contour.length - 1].y) / 2
+ };
+ contour.unshift(p);
+ contour.push(p);
+ }
+
+ moveTo(contour[0].x, contour[0].y);
+
+ for (j = 1, jj = contour.length; j < jj; j++) {
+ if (contour[j].flags & 1) {
+ lineTo(contour[j].x, contour[j].y);
+ } else if (contour[j + 1].flags & 1) {
+ quadraticCurveTo(contour[j].x, contour[j].y, contour[j + 1].x, contour[j + 1].y);
+ j++;
+ } else {
+ quadraticCurveTo(contour[j].x, contour[j].y, (contour[j].x + contour[j + 1].x) / 2, (contour[j].y + contour[j + 1].y) / 2);
+ }
+ }
+
+ startPoint = endPoint + 1;
+ }
+ }
+ }
+
+ function compileCharString(code, cmds, font, glyphId) {
+ var stack = [];
+ var x = 0,
+ y = 0;
+ var stems = 0;
+
+ function moveTo(x, y) {
+ cmds.push({
+ cmd: 'moveTo',
+ args: [x, y]
+ });
+ }
+
+ function lineTo(x, y) {
+ cmds.push({
+ cmd: 'lineTo',
+ args: [x, y]
+ });
+ }
+
+ function bezierCurveTo(x1, y1, x2, y2, x, y) {
+ cmds.push({
+ cmd: 'bezierCurveTo',
+ args: [x1, y1, x2, y2, x, y]
+ });
+ }
+
+ function parse(code) {
+ var i = 0;
+
+ while (i < code.length) {
+ var stackClean = false;
+ var v = code[i++];
+ var xa, xb, ya, yb, y1, y2, y3, n, subrCode;
+
+ switch (v) {
+ case 1:
+ stems += stack.length >> 1;
+ stackClean = true;
+ break;
+
+ case 3:
+ stems += stack.length >> 1;
+ stackClean = true;
+ break;
+
+ case 4:
+ y += stack.pop();
+ moveTo(x, y);
+ stackClean = true;
+ break;
+
+ case 5:
+ while (stack.length > 0) {
+ x += stack.shift();
+ y += stack.shift();
+ lineTo(x, y);
+ }
+
+ break;
+
+ case 6:
+ while (stack.length > 0) {
+ x += stack.shift();
+ lineTo(x, y);
+
+ if (stack.length === 0) {
+ break;
+ }
+
+ y += stack.shift();
+ lineTo(x, y);
+ }
+
+ break;
+
+ case 7:
+ while (stack.length > 0) {
+ y += stack.shift();
+ lineTo(x, y);
+
+ if (stack.length === 0) {
+ break;
+ }
+
+ x += stack.shift();
+ lineTo(x, y);
+ }
+
+ break;
+
+ case 8:
+ while (stack.length > 0) {
+ xa = x + stack.shift();
+ ya = y + stack.shift();
+ xb = xa + stack.shift();
+ yb = ya + stack.shift();
+ x = xb + stack.shift();
+ y = yb + stack.shift();
+ bezierCurveTo(xa, ya, xb, yb, x, y);
+ }
+
+ break;
+
+ case 10:
+ n = stack.pop();
+ subrCode = null;
+
+ if (font.isCFFCIDFont) {
+ var fdIndex = font.fdSelect.getFDIndex(glyphId);
+
+ if (fdIndex >= 0 && fdIndex < font.fdArray.length) {
+ var fontDict = font.fdArray[fdIndex],
+ subrs = void 0;
+
+ if (fontDict.privateDict && fontDict.privateDict.subrsIndex) {
+ subrs = fontDict.privateDict.subrsIndex.objects;
+ }
+
+ if (subrs) {
+ var numSubrs = subrs.length;
+ n += numSubrs < 1240 ? 107 : numSubrs < 33900 ? 1131 : 32768;
+ subrCode = subrs[n];
+ }
+ } else {
+ (0, _util.warn)('Invalid fd index for glyph index.');
+ }
+ } else {
+ subrCode = font.subrs[n + font.subrsBias];
+ }
+
+ if (subrCode) {
+ parse(subrCode);
+ }
+
+ break;
+
+ case 11:
+ return;
+
+ case 12:
+ v = code[i++];
+
+ switch (v) {
+ case 34:
+ xa = x + stack.shift();
+ xb = xa + stack.shift();
+ y1 = y + stack.shift();
+ x = xb + stack.shift();
+ bezierCurveTo(xa, y, xb, y1, x, y1);
+ xa = x + stack.shift();
+ xb = xa + stack.shift();
+ x = xb + stack.shift();
+ bezierCurveTo(xa, y1, xb, y, x, y);
+ break;
+
+ case 35:
+ xa = x + stack.shift();
+ ya = y + stack.shift();
+ xb = xa + stack.shift();
+ yb = ya + stack.shift();
+ x = xb + stack.shift();
+ y = yb + stack.shift();
+ bezierCurveTo(xa, ya, xb, yb, x, y);
+ xa = x + stack.shift();
+ ya = y + stack.shift();
+ xb = xa + stack.shift();
+ yb = ya + stack.shift();
+ x = xb + stack.shift();
+ y = yb + stack.shift();
+ bezierCurveTo(xa, ya, xb, yb, x, y);
+ stack.pop();
+ break;
+
+ case 36:
+ xa = x + stack.shift();
+ y1 = y + stack.shift();
+ xb = xa + stack.shift();
+ y2 = y1 + stack.shift();
+ x = xb + stack.shift();
+ bezierCurveTo(xa, y1, xb, y2, x, y2);
+ xa = x + stack.shift();
+ xb = xa + stack.shift();
+ y3 = y2 + stack.shift();
+ x = xb + stack.shift();
+ bezierCurveTo(xa, y2, xb, y3, x, y);
+ break;
+
+ case 37:
+ var x0 = x,
+ y0 = y;
+ xa = x + stack.shift();
+ ya = y + stack.shift();
+ xb = xa + stack.shift();
+ yb = ya + stack.shift();
+ x = xb + stack.shift();
+ y = yb + stack.shift();
+ bezierCurveTo(xa, ya, xb, yb, x, y);
+ xa = x + stack.shift();
+ ya = y + stack.shift();
+ xb = xa + stack.shift();
+ yb = ya + stack.shift();
+ x = xb;
+ y = yb;
+
+ if (Math.abs(x - x0) > Math.abs(y - y0)) {
+ x += stack.shift();
+ } else {
+ y += stack.shift();
+ }
+
+ bezierCurveTo(xa, ya, xb, yb, x, y);
+ break;
+
+ default:
+ throw new _util.FormatError("unknown operator: 12 ".concat(v));
+ }
+
+ break;
+
+ case 14:
+ if (stack.length >= 4) {
+ var achar = stack.pop();
+ var bchar = stack.pop();
+ y = stack.pop();
+ x = stack.pop();
+ cmds.push({
+ cmd: 'save'
+ });
+ cmds.push({
+ cmd: 'translate',
+ args: [x, y]
+ });
+ var cmap = lookupCmap(font.cmap, String.fromCharCode(font.glyphNameMap[_encodings.StandardEncoding[achar]]));
+ compileCharString(font.glyphs[cmap.glyphId], cmds, font, cmap.glyphId);
+ cmds.push({
+ cmd: 'restore'
+ });
+ cmap = lookupCmap(font.cmap, String.fromCharCode(font.glyphNameMap[_encodings.StandardEncoding[bchar]]));
+ compileCharString(font.glyphs[cmap.glyphId], cmds, font, cmap.glyphId);
+ }
+
+ return;
+
+ case 18:
+ stems += stack.length >> 1;
+ stackClean = true;
+ break;
+
+ case 19:
+ stems += stack.length >> 1;
+ i += stems + 7 >> 3;
+ stackClean = true;
+ break;
+
+ case 20:
+ stems += stack.length >> 1;
+ i += stems + 7 >> 3;
+ stackClean = true;
+ break;
+
+ case 21:
+ y += stack.pop();
+ x += stack.pop();
+ moveTo(x, y);
+ stackClean = true;
+ break;
+
+ case 22:
+ x += stack.pop();
+ moveTo(x, y);
+ stackClean = true;
+ break;
+
+ case 23:
+ stems += stack.length >> 1;
+ stackClean = true;
+ break;
+
+ case 24:
+ while (stack.length > 2) {
+ xa = x + stack.shift();
+ ya = y + stack.shift();
+ xb = xa + stack.shift();
+ yb = ya + stack.shift();
+ x = xb + stack.shift();
+ y = yb + stack.shift();
+ bezierCurveTo(xa, ya, xb, yb, x, y);
+ }
+
+ x += stack.shift();
+ y += stack.shift();
+ lineTo(x, y);
+ break;
+
+ case 25:
+ while (stack.length > 6) {
+ x += stack.shift();
+ y += stack.shift();
+ lineTo(x, y);
+ }
+
+ xa = x + stack.shift();
+ ya = y + stack.shift();
+ xb = xa + stack.shift();
+ yb = ya + stack.shift();
+ x = xb + stack.shift();
+ y = yb + stack.shift();
+ bezierCurveTo(xa, ya, xb, yb, x, y);
+ break;
+
+ case 26:
+ if (stack.length % 2) {
+ x += stack.shift();
+ }
+
+ while (stack.length > 0) {
+ xa = x;
+ ya = y + stack.shift();
+ xb = xa + stack.shift();
+ yb = ya + stack.shift();
+ x = xb;
+ y = yb + stack.shift();
+ bezierCurveTo(xa, ya, xb, yb, x, y);
+ }
+
+ break;
+
+ case 27:
+ if (stack.length % 2) {
+ y += stack.shift();
+ }
+
+ while (stack.length > 0) {
+ xa = x + stack.shift();
+ ya = y;
+ xb = xa + stack.shift();
+ yb = ya + stack.shift();
+ x = xb + stack.shift();
+ y = yb;
+ bezierCurveTo(xa, ya, xb, yb, x, y);
+ }
+
+ break;
+
+ case 28:
+ stack.push((code[i] << 24 | code[i + 1] << 16) >> 16);
+ i += 2;
+ break;
+
+ case 29:
+ n = stack.pop() + font.gsubrsBias;
+ subrCode = font.gsubrs[n];
+
+ if (subrCode) {
+ parse(subrCode);
+ }
+
+ break;
+
+ case 30:
+ while (stack.length > 0) {
+ xa = x;
+ ya = y + stack.shift();
+ xb = xa + stack.shift();
+ yb = ya + stack.shift();
+ x = xb + stack.shift();
+ y = yb + (stack.length === 1 ? stack.shift() : 0);
+ bezierCurveTo(xa, ya, xb, yb, x, y);
+
+ if (stack.length === 0) {
+ break;
+ }
+
+ xa = x + stack.shift();
+ ya = y;
+ xb = xa + stack.shift();
+ yb = ya + stack.shift();
+ y = yb + stack.shift();
+ x = xb + (stack.length === 1 ? stack.shift() : 0);
+ bezierCurveTo(xa, ya, xb, yb, x, y);
+ }
+
+ break;
+
+ case 31:
+ while (stack.length > 0) {
+ xa = x + stack.shift();
+ ya = y;
+ xb = xa + stack.shift();
+ yb = ya + stack.shift();
+ y = yb + stack.shift();
+ x = xb + (stack.length === 1 ? stack.shift() : 0);
+ bezierCurveTo(xa, ya, xb, yb, x, y);
+
+ if (stack.length === 0) {
+ break;
+ }
+
+ xa = x;
+ ya = y + stack.shift();
+ xb = xa + stack.shift();
+ yb = ya + stack.shift();
+ x = xb + stack.shift();
+ y = yb + (stack.length === 1 ? stack.shift() : 0);
+ bezierCurveTo(xa, ya, xb, yb, x, y);
+ }
+
+ break;
+
+ default:
+ if (v < 32) {
+ throw new _util.FormatError("unknown operator: ".concat(v));
+ }
+
+ if (v < 247) {
+ stack.push(v - 139);
+ } else if (v < 251) {
+ stack.push((v - 247) * 256 + code[i++] + 108);
+ } else if (v < 255) {
+ stack.push(-(v - 251) * 256 - code[i++] - 108);
+ } else {
+ stack.push((code[i] << 24 | code[i + 1] << 16 | code[i + 2] << 8 | code[i + 3]) / 65536);
+ i += 4;
+ }
+
+ break;
+ }
+
+ if (stackClean) {
+ stack.length = 0;
+ }
+ }
+ }
+
+ parse(code);
+ }
+
+ var NOOP = [];
+
+ var CompiledFont =
+ /*#__PURE__*/
+ function () {
+ function CompiledFont(fontMatrix) {
+ _classCallCheck(this, CompiledFont);
+
+ if (this.constructor === CompiledFont) {
+ (0, _util.unreachable)('Cannot initialize CompiledFont.');
+ }
+
+ this.fontMatrix = fontMatrix;
+ this.compiledGlyphs = Object.create(null);
+ this.compiledCharCodeToGlyphId = Object.create(null);
+ }
+
+ _createClass(CompiledFont, [{
+ key: "getPathJs",
+ value: function getPathJs(unicode) {
+ var cmap = lookupCmap(this.cmap, unicode);
+ var fn = this.compiledGlyphs[cmap.glyphId];
+
+ if (!fn) {
+ fn = this.compileGlyph(this.glyphs[cmap.glyphId], cmap.glyphId);
+ this.compiledGlyphs[cmap.glyphId] = fn;
+ }
+
+ if (this.compiledCharCodeToGlyphId[cmap.charCode] === undefined) {
+ this.compiledCharCodeToGlyphId[cmap.charCode] = cmap.glyphId;
+ }
+
+ return fn;
+ }
+ }, {
+ key: "compileGlyph",
+ value: function compileGlyph(code, glyphId) {
+ if (!code || code.length === 0 || code[0] === 14) {
+ return NOOP;
+ }
+
+ var fontMatrix = this.fontMatrix;
+
+ if (this.isCFFCIDFont) {
+ var fdIndex = this.fdSelect.getFDIndex(glyphId);
+
+ if (fdIndex >= 0 && fdIndex < this.fdArray.length) {
+ var fontDict = this.fdArray[fdIndex];
+ fontMatrix = fontDict.getByName('FontMatrix') || _util.FONT_IDENTITY_MATRIX;
+ } else {
+ (0, _util.warn)('Invalid fd index for glyph index.');
+ }
+ }
+
+ var cmds = [];
+ cmds.push({
+ cmd: 'save'
+ });
+ cmds.push({
+ cmd: 'transform',
+ args: fontMatrix.slice()
+ });
+ cmds.push({
+ cmd: 'scale',
+ args: ['size', '-size']
+ });
+ this.compileGlyphImpl(code, cmds, glyphId);
+ cmds.push({
+ cmd: 'restore'
+ });
+ return cmds;
+ }
+ }, {
+ key: "compileGlyphImpl",
+ value: function compileGlyphImpl() {
+ (0, _util.unreachable)('Children classes should implement this.');
+ }
+ }, {
+ key: "hasBuiltPath",
+ value: function hasBuiltPath(unicode) {
+ var cmap = lookupCmap(this.cmap, unicode);
+ return this.compiledGlyphs[cmap.glyphId] !== undefined && this.compiledCharCodeToGlyphId[cmap.charCode] !== undefined;
+ }
+ }]);
+
+ return CompiledFont;
+ }();
+
+ var TrueTypeCompiled =
+ /*#__PURE__*/
+ function (_CompiledFont) {
+ _inherits(TrueTypeCompiled, _CompiledFont);
+
+ function TrueTypeCompiled(glyphs, cmap, fontMatrix) {
+ var _this;
+
+ _classCallCheck(this, TrueTypeCompiled);
+
+ _this = _possibleConstructorReturn(this, _getPrototypeOf(TrueTypeCompiled).call(this, fontMatrix || [0.000488, 0, 0, 0.000488, 0, 0]));
+ _this.glyphs = glyphs;
+ _this.cmap = cmap;
+ return _this;
+ }
+
+ _createClass(TrueTypeCompiled, [{
+ key: "compileGlyphImpl",
+ value: function compileGlyphImpl(code, cmds) {
+ compileGlyf(code, cmds, this);
+ }
+ }]);
+
+ return TrueTypeCompiled;
+ }(CompiledFont);
+
+ var Type2Compiled =
+ /*#__PURE__*/
+ function (_CompiledFont2) {
+ _inherits(Type2Compiled, _CompiledFont2);
+
+ function Type2Compiled(cffInfo, cmap, fontMatrix, glyphNameMap) {
+ var _this2;
+
+ _classCallCheck(this, Type2Compiled);
+
+ _this2 = _possibleConstructorReturn(this, _getPrototypeOf(Type2Compiled).call(this, fontMatrix || [0.001, 0, 0, 0.001, 0, 0]));
+ _this2.glyphs = cffInfo.glyphs;
+ _this2.gsubrs = cffInfo.gsubrs || [];
+ _this2.subrs = cffInfo.subrs || [];
+ _this2.cmap = cmap;
+ _this2.glyphNameMap = glyphNameMap || (0, _glyphlist.getGlyphsUnicode)();
+ _this2.gsubrsBias = _this2.gsubrs.length < 1240 ? 107 : _this2.gsubrs.length < 33900 ? 1131 : 32768;
+ _this2.subrsBias = _this2.subrs.length < 1240 ? 107 : _this2.subrs.length < 33900 ? 1131 : 32768;
+ _this2.isCFFCIDFont = cffInfo.isCFFCIDFont;
+ _this2.fdSelect = cffInfo.fdSelect;
+ _this2.fdArray = cffInfo.fdArray;
+ return _this2;
+ }
+
+ _createClass(Type2Compiled, [{
+ key: "compileGlyphImpl",
+ value: function compileGlyphImpl(code, cmds, glyphId) {
+ compileCharString(code, cmds, this, glyphId);
+ }
+ }]);
+
+ return Type2Compiled;
+ }(CompiledFont);
+
+ return {
+ create: function FontRendererFactory_create(font, seacAnalysisEnabled) {
+ var data = new Uint8Array(font.data);
+ var cmap, glyf, loca, cff, indexToLocFormat, unitsPerEm;
+ var numTables = getUshort(data, 4);
+
+ for (var i = 0, p = 12; i < numTables; i++, p += 16) {
+ var tag = (0, _util.bytesToString)(data.subarray(p, p + 4));
+ var offset = getLong(data, p + 8);
+ var length = getLong(data, p + 12);
+
+ switch (tag) {
+ case 'cmap':
+ cmap = parseCmap(data, offset, offset + length);
+ break;
+
+ case 'glyf':
+ glyf = data.subarray(offset, offset + length);
+ break;
+
+ case 'loca':
+ loca = data.subarray(offset, offset + length);
+ break;
+
+ case 'head':
+ unitsPerEm = getUshort(data, offset + 18);
+ indexToLocFormat = getUshort(data, offset + 50);
+ break;
+
+ case 'CFF ':
+ cff = parseCff(data, offset, offset + length, seacAnalysisEnabled);
+ break;
+ }
+ }
+
+ if (glyf) {
+ var fontMatrix = !unitsPerEm ? font.fontMatrix : [1 / unitsPerEm, 0, 0, 1 / unitsPerEm, 0, 0];
+ return new TrueTypeCompiled(parseGlyfTable(glyf, loca, indexToLocFormat), cmap, fontMatrix);
+ }
+
+ return new Type2Compiled(cff, cmap, font.fontMatrix, font.glyphNameMap);
+ }
+ };
+}();
+
+exports.FontRendererFactory = FontRendererFactory;
+
+/***/ }),
+/* 182 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.Type1Parser = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _encodings = __w_pdfjs_require__(177);
+
+var _stream = __w_pdfjs_require__(158);
+
+var HINTING_ENABLED = false;
+
+var Type1CharString = function Type1CharStringClosure() {
+ var COMMAND_MAP = {
+ 'hstem': [1],
+ 'vstem': [3],
+ 'vmoveto': [4],
+ 'rlineto': [5],
+ 'hlineto': [6],
+ 'vlineto': [7],
+ 'rrcurveto': [8],
+ 'callsubr': [10],
+ 'flex': [12, 35],
+ 'drop': [12, 18],
+ 'endchar': [14],
+ 'rmoveto': [21],
+ 'hmoveto': [22],
+ 'vhcurveto': [30],
+ 'hvcurveto': [31]
+ };
+
+ function Type1CharString() {
+ this.width = 0;
+ this.lsb = 0;
+ this.flexing = false;
+ this.output = [];
+ this.stack = [];
+ }
+
+ Type1CharString.prototype = {
+ convert: function Type1CharString_convert(encoded, subrs, seacAnalysisEnabled) {
+ var count = encoded.length;
+ var error = false;
+ var wx, sbx, subrNumber;
+
+ for (var i = 0; i < count; i++) {
+ var value = encoded[i];
+
+ if (value < 32) {
+ if (value === 12) {
+ value = (value << 8) + encoded[++i];
+ }
+
+ switch (value) {
+ case 1:
+ if (!HINTING_ENABLED) {
+ this.stack = [];
+ break;
+ }
+
+ error = this.executeCommand(2, COMMAND_MAP.hstem);
+ break;
+
+ case 3:
+ if (!HINTING_ENABLED) {
+ this.stack = [];
+ break;
+ }
+
+ error = this.executeCommand(2, COMMAND_MAP.vstem);
+ break;
+
+ case 4:
+ if (this.flexing) {
+ if (this.stack.length < 1) {
+ error = true;
+ break;
+ }
+
+ var dy = this.stack.pop();
+ this.stack.push(0, dy);
+ break;
+ }
+
+ error = this.executeCommand(1, COMMAND_MAP.vmoveto);
+ break;
+
+ case 5:
+ error = this.executeCommand(2, COMMAND_MAP.rlineto);
+ break;
+
+ case 6:
+ error = this.executeCommand(1, COMMAND_MAP.hlineto);
+ break;
+
+ case 7:
+ error = this.executeCommand(1, COMMAND_MAP.vlineto);
+ break;
+
+ case 8:
+ error = this.executeCommand(6, COMMAND_MAP.rrcurveto);
+ break;
+
+ case 9:
+ this.stack = [];
+ break;
+
+ case 10:
+ if (this.stack.length < 1) {
+ error = true;
+ break;
+ }
+
+ subrNumber = this.stack.pop();
+
+ if (!subrs[subrNumber]) {
+ error = true;
+ break;
+ }
+
+ error = this.convert(subrs[subrNumber], subrs, seacAnalysisEnabled);
+ break;
+
+ case 11:
+ return error;
+
+ case 13:
+ if (this.stack.length < 2) {
+ error = true;
+ break;
+ }
+
+ wx = this.stack.pop();
+ sbx = this.stack.pop();
+ this.lsb = sbx;
+ this.width = wx;
+ this.stack.push(wx, sbx);
+ error = this.executeCommand(2, COMMAND_MAP.hmoveto);
+ break;
+
+ case 14:
+ this.output.push(COMMAND_MAP.endchar[0]);
+ break;
+
+ case 21:
+ if (this.flexing) {
+ break;
+ }
+
+ error = this.executeCommand(2, COMMAND_MAP.rmoveto);
+ break;
+
+ case 22:
+ if (this.flexing) {
+ this.stack.push(0);
+ break;
+ }
+
+ error = this.executeCommand(1, COMMAND_MAP.hmoveto);
+ break;
+
+ case 30:
+ error = this.executeCommand(4, COMMAND_MAP.vhcurveto);
+ break;
+
+ case 31:
+ error = this.executeCommand(4, COMMAND_MAP.hvcurveto);
+ break;
+
+ case (12 << 8) + 0:
+ this.stack = [];
+ break;
+
+ case (12 << 8) + 1:
+ if (!HINTING_ENABLED) {
+ this.stack = [];
+ break;
+ }
+
+ error = this.executeCommand(2, COMMAND_MAP.vstem);
+ break;
+
+ case (12 << 8) + 2:
+ if (!HINTING_ENABLED) {
+ this.stack = [];
+ break;
+ }
+
+ error = this.executeCommand(2, COMMAND_MAP.hstem);
+ break;
+
+ case (12 << 8) + 6:
+ if (seacAnalysisEnabled) {
+ this.seac = this.stack.splice(-4, 4);
+ error = this.executeCommand(0, COMMAND_MAP.endchar);
+ } else {
+ error = this.executeCommand(4, COMMAND_MAP.endchar);
+ }
+
+ break;
+
+ case (12 << 8) + 7:
+ if (this.stack.length < 4) {
+ error = true;
+ break;
+ }
+
+ this.stack.pop();
+ wx = this.stack.pop();
+ var sby = this.stack.pop();
+ sbx = this.stack.pop();
+ this.lsb = sbx;
+ this.width = wx;
+ this.stack.push(wx, sbx, sby);
+ error = this.executeCommand(3, COMMAND_MAP.rmoveto);
+ break;
+
+ case (12 << 8) + 12:
+ if (this.stack.length < 2) {
+ error = true;
+ break;
+ }
+
+ var num2 = this.stack.pop();
+ var num1 = this.stack.pop();
+ this.stack.push(num1 / num2);
+ break;
+
+ case (12 << 8) + 16:
+ if (this.stack.length < 2) {
+ error = true;
+ break;
+ }
+
+ subrNumber = this.stack.pop();
+ var numArgs = this.stack.pop();
+
+ if (subrNumber === 0 && numArgs === 3) {
+ var flexArgs = this.stack.splice(this.stack.length - 17, 17);
+ this.stack.push(flexArgs[2] + flexArgs[0], flexArgs[3] + flexArgs[1], flexArgs[4], flexArgs[5], flexArgs[6], flexArgs[7], flexArgs[8], flexArgs[9], flexArgs[10], flexArgs[11], flexArgs[12], flexArgs[13], flexArgs[14]);
+ error = this.executeCommand(13, COMMAND_MAP.flex, true);
+ this.flexing = false;
+ this.stack.push(flexArgs[15], flexArgs[16]);
+ } else if (subrNumber === 1 && numArgs === 0) {
+ this.flexing = true;
+ }
+
+ break;
+
+ case (12 << 8) + 17:
+ break;
+
+ case (12 << 8) + 33:
+ this.stack = [];
+ break;
+
+ default:
+ (0, _util.warn)('Unknown type 1 charstring command of "' + value + '"');
+ break;
+ }
+
+ if (error) {
+ break;
+ }
+
+ continue;
+ } else if (value <= 246) {
+ value = value - 139;
+ } else if (value <= 250) {
+ value = (value - 247) * 256 + encoded[++i] + 108;
+ } else if (value <= 254) {
+ value = -((value - 251) * 256) - encoded[++i] - 108;
+ } else {
+ value = (encoded[++i] & 0xff) << 24 | (encoded[++i] & 0xff) << 16 | (encoded[++i] & 0xff) << 8 | (encoded[++i] & 0xff) << 0;
+ }
+
+ this.stack.push(value);
+ }
+
+ return error;
+ },
+ executeCommand: function executeCommand(howManyArgs, command, keepStack) {
+ var stackLength = this.stack.length;
+
+ if (howManyArgs > stackLength) {
+ return true;
+ }
+
+ var start = stackLength - howManyArgs;
+
+ for (var i = start; i < stackLength; i++) {
+ var value = this.stack[i];
+
+ if (Number.isInteger(value)) {
+ this.output.push(28, value >> 8 & 0xff, value & 0xff);
+ } else {
+ value = 65536 * value | 0;
+ this.output.push(255, value >> 24 & 0xFF, value >> 16 & 0xFF, value >> 8 & 0xFF, value & 0xFF);
+ }
+ }
+
+ this.output.push.apply(this.output, command);
+
+ if (keepStack) {
+ this.stack.splice(start, howManyArgs);
+ } else {
+ this.stack.length = 0;
+ }
+
+ return false;
+ }
+ };
+ return Type1CharString;
+}();
+
+var Type1Parser = function Type1ParserClosure() {
+ var EEXEC_ENCRYPT_KEY = 55665;
+ var CHAR_STRS_ENCRYPT_KEY = 4330;
+
+ function isHexDigit(code) {
+ return code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
+ }
+
+ function decrypt(data, key, discardNumber) {
+ if (discardNumber >= data.length) {
+ return new Uint8Array(0);
+ }
+
+ var r = key | 0,
+ c1 = 52845,
+ c2 = 22719,
+ i,
+ j;
+
+ for (i = 0; i < discardNumber; i++) {
+ r = (data[i] + r) * c1 + c2 & (1 << 16) - 1;
+ }
+
+ var count = data.length - discardNumber;
+ var decrypted = new Uint8Array(count);
+
+ for (i = discardNumber, j = 0; j < count; i++, j++) {
+ var value = data[i];
+ decrypted[j] = value ^ r >> 8;
+ r = (value + r) * c1 + c2 & (1 << 16) - 1;
+ }
+
+ return decrypted;
+ }
+
+ function decryptAscii(data, key, discardNumber) {
+ var r = key | 0,
+ c1 = 52845,
+ c2 = 22719;
+ var count = data.length,
+ maybeLength = count >>> 1;
+ var decrypted = new Uint8Array(maybeLength);
+ var i, j;
+
+ for (i = 0, j = 0; i < count; i++) {
+ var digit1 = data[i];
+
+ if (!isHexDigit(digit1)) {
+ continue;
+ }
+
+ i++;
+ var digit2;
+
+ while (i < count && !isHexDigit(digit2 = data[i])) {
+ i++;
+ }
+
+ if (i < count) {
+ var value = parseInt(String.fromCharCode(digit1, digit2), 16);
+ decrypted[j++] = value ^ r >> 8;
+ r = (value + r) * c1 + c2 & (1 << 16) - 1;
+ }
+ }
+
+ return Array.prototype.slice.call(decrypted, discardNumber, j);
+ }
+
+ function isSpecial(c) {
+ return c === 0x2F || c === 0x5B || c === 0x5D || c === 0x7B || c === 0x7D || c === 0x28 || c === 0x29;
+ }
+
+ function Type1Parser(stream, encrypted, seacAnalysisEnabled) {
+ if (encrypted) {
+ var data = stream.getBytes();
+ var isBinary = !(isHexDigit(data[0]) && isHexDigit(data[1]) && isHexDigit(data[2]) && isHexDigit(data[3]));
+ stream = new _stream.Stream(isBinary ? decrypt(data, EEXEC_ENCRYPT_KEY, 4) : decryptAscii(data, EEXEC_ENCRYPT_KEY, 4));
+ }
+
+ this.seacAnalysisEnabled = !!seacAnalysisEnabled;
+ this.stream = stream;
+ this.nextChar();
+ }
+
+ Type1Parser.prototype = {
+ readNumberArray: function Type1Parser_readNumberArray() {
+ this.getToken();
+ var array = [];
+
+ while (true) {
+ var token = this.getToken();
+
+ if (token === null || token === ']' || token === '}') {
+ break;
+ }
+
+ array.push(parseFloat(token || 0));
+ }
+
+ return array;
+ },
+ readNumber: function Type1Parser_readNumber() {
+ var token = this.getToken();
+ return parseFloat(token || 0);
+ },
+ readInt: function Type1Parser_readInt() {
+ var token = this.getToken();
+ return parseInt(token || 0, 10) | 0;
+ },
+ readBoolean: function Type1Parser_readBoolean() {
+ var token = this.getToken();
+ return token === 'true' ? 1 : 0;
+ },
+ nextChar: function Type1_nextChar() {
+ return this.currentChar = this.stream.getByte();
+ },
+ getToken: function Type1Parser_getToken() {
+ var comment = false;
+ var ch = this.currentChar;
+
+ while (true) {
+ if (ch === -1) {
+ return null;
+ }
+
+ if (comment) {
+ if (ch === 0x0A || ch === 0x0D) {
+ comment = false;
+ }
+ } else if (ch === 0x25) {
+ comment = true;
+ } else if (!(0, _util.isSpace)(ch)) {
+ break;
+ }
+
+ ch = this.nextChar();
+ }
+
+ if (isSpecial(ch)) {
+ this.nextChar();
+ return String.fromCharCode(ch);
+ }
+
+ var token = '';
+
+ do {
+ token += String.fromCharCode(ch);
+ ch = this.nextChar();
+ } while (ch >= 0 && !(0, _util.isSpace)(ch) && !isSpecial(ch));
+
+ return token;
+ },
+ readCharStrings: function Type1Parser_readCharStrings(bytes, lenIV) {
+ if (lenIV === -1) {
+ return bytes;
+ }
+
+ return decrypt(bytes, CHAR_STRS_ENCRYPT_KEY, lenIV);
+ },
+ extractFontProgram: function Type1Parser_extractFontProgram() {
+ var stream = this.stream;
+ var subrs = [],
+ charstrings = [];
+ var privateData = Object.create(null);
+ privateData['lenIV'] = 4;
+ var program = {
+ subrs: [],
+ charstrings: [],
+ properties: {
+ 'privateData': privateData
+ }
+ };
+ var token, length, data, lenIV, encoded;
+
+ while ((token = this.getToken()) !== null) {
+ if (token !== '/') {
+ continue;
+ }
+
+ token = this.getToken();
+
+ switch (token) {
+ case 'CharStrings':
+ this.getToken();
+ this.getToken();
+ this.getToken();
+ this.getToken();
+
+ while (true) {
+ token = this.getToken();
+
+ if (token === null || token === 'end') {
+ break;
+ }
+
+ if (token !== '/') {
+ continue;
+ }
+
+ var glyph = this.getToken();
+ length = this.readInt();
+ this.getToken();
+ data = length > 0 ? stream.getBytes(length) : new Uint8Array(0);
+ lenIV = program.properties.privateData['lenIV'];
+ encoded = this.readCharStrings(data, lenIV);
+ this.nextChar();
+ token = this.getToken();
+
+ if (token === 'noaccess') {
+ this.getToken();
+ }
+
+ charstrings.push({
+ glyph: glyph,
+ encoded: encoded
+ });
+ }
+
+ break;
+
+ case 'Subrs':
+ this.readInt();
+ this.getToken();
+
+ while (this.getToken() === 'dup') {
+ var index = this.readInt();
+ length = this.readInt();
+ this.getToken();
+ data = length > 0 ? stream.getBytes(length) : new Uint8Array(0);
+ lenIV = program.properties.privateData['lenIV'];
+ encoded = this.readCharStrings(data, lenIV);
+ this.nextChar();
+ token = this.getToken();
+
+ if (token === 'noaccess') {
+ this.getToken();
+ }
+
+ subrs[index] = encoded;
+ }
+
+ break;
+
+ case 'BlueValues':
+ case 'OtherBlues':
+ case 'FamilyBlues':
+ case 'FamilyOtherBlues':
+ var blueArray = this.readNumberArray();
+
+ if (blueArray.length > 0 && blueArray.length % 2 === 0 && HINTING_ENABLED) {
+ program.properties.privateData[token] = blueArray;
+ }
+
+ break;
+
+ case 'StemSnapH':
+ case 'StemSnapV':
+ program.properties.privateData[token] = this.readNumberArray();
+ break;
+
+ case 'StdHW':
+ case 'StdVW':
+ program.properties.privateData[token] = this.readNumberArray()[0];
+ break;
+
+ case 'BlueShift':
+ case 'lenIV':
+ case 'BlueFuzz':
+ case 'BlueScale':
+ case 'LanguageGroup':
+ case 'ExpansionFactor':
+ program.properties.privateData[token] = this.readNumber();
+ break;
+
+ case 'ForceBold':
+ program.properties.privateData[token] = this.readBoolean();
+ break;
+ }
+ }
+
+ for (var i = 0; i < charstrings.length; i++) {
+ glyph = charstrings[i].glyph;
+ encoded = charstrings[i].encoded;
+ var charString = new Type1CharString();
+ var error = charString.convert(encoded, subrs, this.seacAnalysisEnabled);
+ var output = charString.output;
+
+ if (error) {
+ output = [14];
+ }
+
+ program.charstrings.push({
+ glyphName: glyph,
+ charstring: output,
+ width: charString.width,
+ lsb: charString.lsb,
+ seac: charString.seac
+ });
+ }
+
+ return program;
+ },
+ extractFontHeader: function Type1Parser_extractFontHeader(properties) {
+ var token;
+
+ while ((token = this.getToken()) !== null) {
+ if (token !== '/') {
+ continue;
+ }
+
+ token = this.getToken();
+
+ switch (token) {
+ case 'FontMatrix':
+ var matrix = this.readNumberArray();
+ properties.fontMatrix = matrix;
+ break;
+
+ case 'Encoding':
+ var encodingArg = this.getToken();
+ var encoding;
+
+ if (!/^\d+$/.test(encodingArg)) {
+ encoding = (0, _encodings.getEncoding)(encodingArg);
+ } else {
+ encoding = [];
+ var size = parseInt(encodingArg, 10) | 0;
+ this.getToken();
+
+ for (var j = 0; j < size; j++) {
+ token = this.getToken();
+
+ while (token !== 'dup' && token !== 'def') {
+ token = this.getToken();
+
+ if (token === null) {
+ return;
+ }
+ }
+
+ if (token === 'def') {
+ break;
+ }
+
+ var index = this.readInt();
+ this.getToken();
+ var glyph = this.getToken();
+ encoding[index] = glyph;
+ this.getToken();
+ }
+ }
+
+ properties.builtInEncoding = encoding;
+ break;
+
+ case 'FontBBox':
+ var fontBBox = this.readNumberArray();
+ properties.ascent = Math.max(fontBBox[3], fontBBox[1]);
+ properties.descent = Math.min(fontBBox[1], fontBBox[3]);
+ properties.ascentScaled = true;
+ break;
+ }
+ }
+ }
+ };
+ return Type1Parser;
+}();
+
+exports.Type1Parser = Type1Parser;
+
+/***/ }),
+/* 183 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.getTilingPatternIR = getTilingPatternIR;
+exports.Pattern = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _colorspace = __w_pdfjs_require__(169);
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _core_utils = __w_pdfjs_require__(154);
+
+var ShadingType = {
+ FUNCTION_BASED: 1,
+ AXIAL: 2,
+ RADIAL: 3,
+ FREE_FORM_MESH: 4,
+ LATTICE_FORM_MESH: 5,
+ COONS_PATCH_MESH: 6,
+ TENSOR_PATCH_MESH: 7
+};
+
+var Pattern = function PatternClosure() {
+ function Pattern() {
+ (0, _util.unreachable)('should not call Pattern constructor');
+ }
+
+ Pattern.prototype = {
+ getPattern: function Pattern_getPattern(ctx) {
+ (0, _util.unreachable)("Should not call Pattern.getStyle: ".concat(ctx));
+ }
+ };
+
+ Pattern.parseShading = function (shading, matrix, xref, res, handler, pdfFunctionFactory) {
+ var dict = (0, _primitives.isStream)(shading) ? shading.dict : shading;
+ var type = dict.get('ShadingType');
+
+ try {
+ switch (type) {
+ case ShadingType.AXIAL:
+ case ShadingType.RADIAL:
+ return new Shadings.RadialAxial(dict, matrix, xref, res, pdfFunctionFactory);
+
+ case ShadingType.FREE_FORM_MESH:
+ case ShadingType.LATTICE_FORM_MESH:
+ case ShadingType.COONS_PATCH_MESH:
+ case ShadingType.TENSOR_PATCH_MESH:
+ return new Shadings.Mesh(shading, matrix, xref, res, pdfFunctionFactory);
+
+ default:
+ throw new _util.FormatError('Unsupported ShadingType: ' + type);
+ }
+ } catch (ex) {
+ if (ex instanceof _core_utils.MissingDataException) {
+ throw ex;
+ }
+
+ handler.send('UnsupportedFeature', {
+ featureId: _util.UNSUPPORTED_FEATURES.shadingPattern
+ });
+ (0, _util.warn)(ex);
+ return new Shadings.Dummy();
+ }
+ };
+
+ return Pattern;
+}();
+
+exports.Pattern = Pattern;
+var Shadings = {};
+Shadings.SMALL_NUMBER = 1e-6;
+
+Shadings.RadialAxial = function RadialAxialClosure() {
+ function RadialAxial(dict, matrix, xref, res, pdfFunctionFactory) {
+ this.matrix = matrix;
+ this.coordsArr = dict.getArray('Coords');
+ this.shadingType = dict.get('ShadingType');
+ this.type = 'Pattern';
+ var cs = dict.get('ColorSpace', 'CS');
+ cs = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+ this.cs = cs;
+ var t0 = 0.0,
+ t1 = 1.0;
+
+ if (dict.has('Domain')) {
+ var domainArr = dict.getArray('Domain');
+ t0 = domainArr[0];
+ t1 = domainArr[1];
+ }
+
+ var extendStart = false,
+ extendEnd = false;
+
+ if (dict.has('Extend')) {
+ var extendArr = dict.getArray('Extend');
+ extendStart = extendArr[0];
+ extendEnd = extendArr[1];
+ }
+
+ if (this.shadingType === ShadingType.RADIAL && (!extendStart || !extendEnd)) {
+ var x1 = this.coordsArr[0];
+ var y1 = this.coordsArr[1];
+ var r1 = this.coordsArr[2];
+ var x2 = this.coordsArr[3];
+ var y2 = this.coordsArr[4];
+ var r2 = this.coordsArr[5];
+ var distance = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
+
+ if (r1 <= r2 + distance && r2 <= r1 + distance) {
+ (0, _util.warn)('Unsupported radial gradient.');
+ }
+ }
+
+ this.extendStart = extendStart;
+ this.extendEnd = extendEnd;
+ var fnObj = dict.get('Function');
+ var fn = pdfFunctionFactory.createFromArray(fnObj);
+ var diff = t1 - t0;
+ var step = diff / 10;
+ var colorStops = this.colorStops = [];
+
+ if (t0 >= t1 || step <= 0) {
+ (0, _util.info)('Bad shading domain.');
+ return;
+ }
+
+ var color = new Float32Array(cs.numComps),
+ ratio = new Float32Array(1);
+ var rgbColor;
+
+ for (var i = t0; i <= t1; i += step) {
+ ratio[0] = i;
+ fn(ratio, 0, color, 0);
+ rgbColor = cs.getRgb(color, 0);
+
+ var cssColor = _util.Util.makeCssRgb(rgbColor[0], rgbColor[1], rgbColor[2]);
+
+ colorStops.push([(i - t0) / diff, cssColor]);
+ }
+
+ var background = 'transparent';
+
+ if (dict.has('Background')) {
+ rgbColor = cs.getRgb(dict.get('Background'), 0);
+ background = _util.Util.makeCssRgb(rgbColor[0], rgbColor[1], rgbColor[2]);
+ }
+
+ if (!extendStart) {
+ colorStops.unshift([0, background]);
+ colorStops[1][0] += Shadings.SMALL_NUMBER;
+ }
+
+ if (!extendEnd) {
+ colorStops[colorStops.length - 1][0] -= Shadings.SMALL_NUMBER;
+ colorStops.push([1, background]);
+ }
+
+ this.colorStops = colorStops;
+ }
+
+ RadialAxial.prototype = {
+ getIR: function RadialAxial_getIR() {
+ var coordsArr = this.coordsArr;
+ var shadingType = this.shadingType;
+ var type, p0, p1, r0, r1;
+
+ if (shadingType === ShadingType.AXIAL) {
+ p0 = [coordsArr[0], coordsArr[1]];
+ p1 = [coordsArr[2], coordsArr[3]];
+ r0 = null;
+ r1 = null;
+ type = 'axial';
+ } else if (shadingType === ShadingType.RADIAL) {
+ p0 = [coordsArr[0], coordsArr[1]];
+ p1 = [coordsArr[3], coordsArr[4]];
+ r0 = coordsArr[2];
+ r1 = coordsArr[5];
+ type = 'radial';
+ } else {
+ (0, _util.unreachable)("getPattern type unknown: ".concat(shadingType));
+ }
+
+ var matrix = this.matrix;
+
+ if (matrix) {
+ p0 = _util.Util.applyTransform(p0, matrix);
+ p1 = _util.Util.applyTransform(p1, matrix);
+
+ if (shadingType === ShadingType.RADIAL) {
+ var scale = _util.Util.singularValueDecompose2dScale(matrix);
+
+ r0 *= scale[0];
+ r1 *= scale[1];
+ }
+ }
+
+ return ['RadialAxial', type, this.colorStops, p0, p1, r0, r1];
+ }
+ };
+ return RadialAxial;
+}();
+
+Shadings.Mesh = function MeshClosure() {
+ function MeshStreamReader(stream, context) {
+ this.stream = stream;
+ this.context = context;
+ this.buffer = 0;
+ this.bufferLength = 0;
+ var numComps = context.numComps;
+ this.tmpCompsBuf = new Float32Array(numComps);
+ var csNumComps = context.colorSpace.numComps;
+ this.tmpCsCompsBuf = context.colorFn ? new Float32Array(csNumComps) : this.tmpCompsBuf;
+ }
+
+ MeshStreamReader.prototype = {
+ get hasData() {
+ if (this.stream.end) {
+ return this.stream.pos < this.stream.end;
+ }
+
+ if (this.bufferLength > 0) {
+ return true;
+ }
+
+ var nextByte = this.stream.getByte();
+
+ if (nextByte < 0) {
+ return false;
+ }
+
+ this.buffer = nextByte;
+ this.bufferLength = 8;
+ return true;
+ },
+
+ readBits: function MeshStreamReader_readBits(n) {
+ var buffer = this.buffer;
+ var bufferLength = this.bufferLength;
+
+ if (n === 32) {
+ if (bufferLength === 0) {
+ return (this.stream.getByte() << 24 | this.stream.getByte() << 16 | this.stream.getByte() << 8 | this.stream.getByte()) >>> 0;
+ }
+
+ buffer = buffer << 24 | this.stream.getByte() << 16 | this.stream.getByte() << 8 | this.stream.getByte();
+ var nextByte = this.stream.getByte();
+ this.buffer = nextByte & (1 << bufferLength) - 1;
+ return (buffer << 8 - bufferLength | (nextByte & 0xFF) >> bufferLength) >>> 0;
+ }
+
+ if (n === 8 && bufferLength === 0) {
+ return this.stream.getByte();
+ }
+
+ while (bufferLength < n) {
+ buffer = buffer << 8 | this.stream.getByte();
+ bufferLength += 8;
+ }
+
+ bufferLength -= n;
+ this.bufferLength = bufferLength;
+ this.buffer = buffer & (1 << bufferLength) - 1;
+ return buffer >> bufferLength;
+ },
+ align: function MeshStreamReader_align() {
+ this.buffer = 0;
+ this.bufferLength = 0;
+ },
+ readFlag: function MeshStreamReader_readFlag() {
+ return this.readBits(this.context.bitsPerFlag);
+ },
+ readCoordinate: function MeshStreamReader_readCoordinate() {
+ var bitsPerCoordinate = this.context.bitsPerCoordinate;
+ var xi = this.readBits(bitsPerCoordinate);
+ var yi = this.readBits(bitsPerCoordinate);
+ var decode = this.context.decode;
+ var scale = bitsPerCoordinate < 32 ? 1 / ((1 << bitsPerCoordinate) - 1) : 2.3283064365386963e-10;
+ return [xi * scale * (decode[1] - decode[0]) + decode[0], yi * scale * (decode[3] - decode[2]) + decode[2]];
+ },
+ readComponents: function MeshStreamReader_readComponents() {
+ var numComps = this.context.numComps;
+ var bitsPerComponent = this.context.bitsPerComponent;
+ var scale = bitsPerComponent < 32 ? 1 / ((1 << bitsPerComponent) - 1) : 2.3283064365386963e-10;
+ var decode = this.context.decode;
+ var components = this.tmpCompsBuf;
+
+ for (var i = 0, j = 4; i < numComps; i++, j += 2) {
+ var ci = this.readBits(bitsPerComponent);
+ components[i] = ci * scale * (decode[j + 1] - decode[j]) + decode[j];
+ }
+
+ var color = this.tmpCsCompsBuf;
+
+ if (this.context.colorFn) {
+ this.context.colorFn(components, 0, color, 0);
+ }
+
+ return this.context.colorSpace.getRgb(color, 0);
+ }
+ };
+
+ function decodeType4Shading(mesh, reader) {
+ var coords = mesh.coords;
+ var colors = mesh.colors;
+ var operators = [];
+ var ps = [];
+ var verticesLeft = 0;
+
+ while (reader.hasData) {
+ var f = reader.readFlag();
+ var coord = reader.readCoordinate();
+ var color = reader.readComponents();
+
+ if (verticesLeft === 0) {
+ if (!(0 <= f && f <= 2)) {
+ throw new _util.FormatError('Unknown type4 flag');
+ }
+
+ switch (f) {
+ case 0:
+ verticesLeft = 3;
+ break;
+
+ case 1:
+ ps.push(ps[ps.length - 2], ps[ps.length - 1]);
+ verticesLeft = 1;
+ break;
+
+ case 2:
+ ps.push(ps[ps.length - 3], ps[ps.length - 1]);
+ verticesLeft = 1;
+ break;
+ }
+
+ operators.push(f);
+ }
+
+ ps.push(coords.length);
+ coords.push(coord);
+ colors.push(color);
+ verticesLeft--;
+ reader.align();
+ }
+
+ mesh.figures.push({
+ type: 'triangles',
+ coords: new Int32Array(ps),
+ colors: new Int32Array(ps)
+ });
+ }
+
+ function decodeType5Shading(mesh, reader, verticesPerRow) {
+ var coords = mesh.coords;
+ var colors = mesh.colors;
+ var ps = [];
+
+ while (reader.hasData) {
+ var coord = reader.readCoordinate();
+ var color = reader.readComponents();
+ ps.push(coords.length);
+ coords.push(coord);
+ colors.push(color);
+ }
+
+ mesh.figures.push({
+ type: 'lattice',
+ coords: new Int32Array(ps),
+ colors: new Int32Array(ps),
+ verticesPerRow: verticesPerRow
+ });
+ }
+
+ var MIN_SPLIT_PATCH_CHUNKS_AMOUNT = 3;
+ var MAX_SPLIT_PATCH_CHUNKS_AMOUNT = 20;
+ var TRIANGLE_DENSITY = 20;
+
+ var getB = function getBClosure() {
+ function buildB(count) {
+ var lut = [];
+
+ for (var i = 0; i <= count; i++) {
+ var t = i / count,
+ t_ = 1 - t;
+ lut.push(new Float32Array([t_ * t_ * t_, 3 * t * t_ * t_, 3 * t * t * t_, t * t * t]));
+ }
+
+ return lut;
+ }
+
+ var cache = [];
+ return function getB(count) {
+ if (!cache[count]) {
+ cache[count] = buildB(count);
+ }
+
+ return cache[count];
+ };
+ }();
+
+ function buildFigureFromPatch(mesh, index) {
+ var figure = mesh.figures[index];
+ (0, _util.assert)(figure.type === 'patch', 'Unexpected patch mesh figure');
+ var coords = mesh.coords,
+ colors = mesh.colors;
+ var pi = figure.coords;
+ var ci = figure.colors;
+ var figureMinX = Math.min(coords[pi[0]][0], coords[pi[3]][0], coords[pi[12]][0], coords[pi[15]][0]);
+ var figureMinY = Math.min(coords[pi[0]][1], coords[pi[3]][1], coords[pi[12]][1], coords[pi[15]][1]);
+ var figureMaxX = Math.max(coords[pi[0]][0], coords[pi[3]][0], coords[pi[12]][0], coords[pi[15]][0]);
+ var figureMaxY = Math.max(coords[pi[0]][1], coords[pi[3]][1], coords[pi[12]][1], coords[pi[15]][1]);
+ var splitXBy = Math.ceil((figureMaxX - figureMinX) * TRIANGLE_DENSITY / (mesh.bounds[2] - mesh.bounds[0]));
+ splitXBy = Math.max(MIN_SPLIT_PATCH_CHUNKS_AMOUNT, Math.min(MAX_SPLIT_PATCH_CHUNKS_AMOUNT, splitXBy));
+ var splitYBy = Math.ceil((figureMaxY - figureMinY) * TRIANGLE_DENSITY / (mesh.bounds[3] - mesh.bounds[1]));
+ splitYBy = Math.max(MIN_SPLIT_PATCH_CHUNKS_AMOUNT, Math.min(MAX_SPLIT_PATCH_CHUNKS_AMOUNT, splitYBy));
+ var verticesPerRow = splitXBy + 1;
+ var figureCoords = new Int32Array((splitYBy + 1) * verticesPerRow);
+ var figureColors = new Int32Array((splitYBy + 1) * verticesPerRow);
+ var k = 0;
+ var cl = new Uint8Array(3),
+ cr = new Uint8Array(3);
+ var c0 = colors[ci[0]],
+ c1 = colors[ci[1]],
+ c2 = colors[ci[2]],
+ c3 = colors[ci[3]];
+ var bRow = getB(splitYBy),
+ bCol = getB(splitXBy);
+
+ for (var row = 0; row <= splitYBy; row++) {
+ cl[0] = (c0[0] * (splitYBy - row) + c2[0] * row) / splitYBy | 0;
+ cl[1] = (c0[1] * (splitYBy - row) + c2[1] * row) / splitYBy | 0;
+ cl[2] = (c0[2] * (splitYBy - row) + c2[2] * row) / splitYBy | 0;
+ cr[0] = (c1[0] * (splitYBy - row) + c3[0] * row) / splitYBy | 0;
+ cr[1] = (c1[1] * (splitYBy - row) + c3[1] * row) / splitYBy | 0;
+ cr[2] = (c1[2] * (splitYBy - row) + c3[2] * row) / splitYBy | 0;
+
+ for (var col = 0; col <= splitXBy; col++, k++) {
+ if ((row === 0 || row === splitYBy) && (col === 0 || col === splitXBy)) {
+ continue;
+ }
+
+ var x = 0,
+ y = 0;
+ var q = 0;
+
+ for (var i = 0; i <= 3; i++) {
+ for (var j = 0; j <= 3; j++, q++) {
+ var m = bRow[row][i] * bCol[col][j];
+ x += coords[pi[q]][0] * m;
+ y += coords[pi[q]][1] * m;
+ }
+ }
+
+ figureCoords[k] = coords.length;
+ coords.push([x, y]);
+ figureColors[k] = colors.length;
+ var newColor = new Uint8Array(3);
+ newColor[0] = (cl[0] * (splitXBy - col) + cr[0] * col) / splitXBy | 0;
+ newColor[1] = (cl[1] * (splitXBy - col) + cr[1] * col) / splitXBy | 0;
+ newColor[2] = (cl[2] * (splitXBy - col) + cr[2] * col) / splitXBy | 0;
+ colors.push(newColor);
+ }
+ }
+
+ figureCoords[0] = pi[0];
+ figureColors[0] = ci[0];
+ figureCoords[splitXBy] = pi[3];
+ figureColors[splitXBy] = ci[1];
+ figureCoords[verticesPerRow * splitYBy] = pi[12];
+ figureColors[verticesPerRow * splitYBy] = ci[2];
+ figureCoords[verticesPerRow * splitYBy + splitXBy] = pi[15];
+ figureColors[verticesPerRow * splitYBy + splitXBy] = ci[3];
+ mesh.figures[index] = {
+ type: 'lattice',
+ coords: figureCoords,
+ colors: figureColors,
+ verticesPerRow: verticesPerRow
+ };
+ }
+
+ function decodeType6Shading(mesh, reader) {
+ var coords = mesh.coords;
+ var colors = mesh.colors;
+ var ps = new Int32Array(16);
+ var cs = new Int32Array(4);
+
+ while (reader.hasData) {
+ var f = reader.readFlag();
+
+ if (!(0 <= f && f <= 3)) {
+ throw new _util.FormatError('Unknown type6 flag');
+ }
+
+ var i, ii;
+ var pi = coords.length;
+
+ for (i = 0, ii = f !== 0 ? 8 : 12; i < ii; i++) {
+ coords.push(reader.readCoordinate());
+ }
+
+ var ci = colors.length;
+
+ for (i = 0, ii = f !== 0 ? 2 : 4; i < ii; i++) {
+ colors.push(reader.readComponents());
+ }
+
+ var tmp1, tmp2, tmp3, tmp4;
+
+ switch (f) {
+ case 0:
+ ps[12] = pi + 3;
+ ps[13] = pi + 4;
+ ps[14] = pi + 5;
+ ps[15] = pi + 6;
+ ps[8] = pi + 2;
+ ps[11] = pi + 7;
+ ps[4] = pi + 1;
+ ps[7] = pi + 8;
+ ps[0] = pi;
+ ps[1] = pi + 11;
+ ps[2] = pi + 10;
+ ps[3] = pi + 9;
+ cs[2] = ci + 1;
+ cs[3] = ci + 2;
+ cs[0] = ci;
+ cs[1] = ci + 3;
+ break;
+
+ case 1:
+ tmp1 = ps[12];
+ tmp2 = ps[13];
+ tmp3 = ps[14];
+ tmp4 = ps[15];
+ ps[12] = tmp4;
+ ps[13] = pi + 0;
+ ps[14] = pi + 1;
+ ps[15] = pi + 2;
+ ps[8] = tmp3;
+ ps[11] = pi + 3;
+ ps[4] = tmp2;
+ ps[7] = pi + 4;
+ ps[0] = tmp1;
+ ps[1] = pi + 7;
+ ps[2] = pi + 6;
+ ps[3] = pi + 5;
+ tmp1 = cs[2];
+ tmp2 = cs[3];
+ cs[2] = tmp2;
+ cs[3] = ci;
+ cs[0] = tmp1;
+ cs[1] = ci + 1;
+ break;
+
+ case 2:
+ tmp1 = ps[15];
+ tmp2 = ps[11];
+ ps[12] = ps[3];
+ ps[13] = pi + 0;
+ ps[14] = pi + 1;
+ ps[15] = pi + 2;
+ ps[8] = ps[7];
+ ps[11] = pi + 3;
+ ps[4] = tmp2;
+ ps[7] = pi + 4;
+ ps[0] = tmp1;
+ ps[1] = pi + 7;
+ ps[2] = pi + 6;
+ ps[3] = pi + 5;
+ tmp1 = cs[3];
+ cs[2] = cs[1];
+ cs[3] = ci;
+ cs[0] = tmp1;
+ cs[1] = ci + 1;
+ break;
+
+ case 3:
+ ps[12] = ps[0];
+ ps[13] = pi + 0;
+ ps[14] = pi + 1;
+ ps[15] = pi + 2;
+ ps[8] = ps[1];
+ ps[11] = pi + 3;
+ ps[4] = ps[2];
+ ps[7] = pi + 4;
+ ps[0] = ps[3];
+ ps[1] = pi + 7;
+ ps[2] = pi + 6;
+ ps[3] = pi + 5;
+ cs[2] = cs[0];
+ cs[3] = ci;
+ cs[0] = cs[1];
+ cs[1] = ci + 1;
+ break;
+ }
+
+ ps[5] = coords.length;
+ coords.push([(-4 * coords[ps[0]][0] - coords[ps[15]][0] + 6 * (coords[ps[4]][0] + coords[ps[1]][0]) - 2 * (coords[ps[12]][0] + coords[ps[3]][0]) + 3 * (coords[ps[13]][0] + coords[ps[7]][0])) / 9, (-4 * coords[ps[0]][1] - coords[ps[15]][1] + 6 * (coords[ps[4]][1] + coords[ps[1]][1]) - 2 * (coords[ps[12]][1] + coords[ps[3]][1]) + 3 * (coords[ps[13]][1] + coords[ps[7]][1])) / 9]);
+ ps[6] = coords.length;
+ coords.push([(-4 * coords[ps[3]][0] - coords[ps[12]][0] + 6 * (coords[ps[2]][0] + coords[ps[7]][0]) - 2 * (coords[ps[0]][0] + coords[ps[15]][0]) + 3 * (coords[ps[4]][0] + coords[ps[14]][0])) / 9, (-4 * coords[ps[3]][1] - coords[ps[12]][1] + 6 * (coords[ps[2]][1] + coords[ps[7]][1]) - 2 * (coords[ps[0]][1] + coords[ps[15]][1]) + 3 * (coords[ps[4]][1] + coords[ps[14]][1])) / 9]);
+ ps[9] = coords.length;
+ coords.push([(-4 * coords[ps[12]][0] - coords[ps[3]][0] + 6 * (coords[ps[8]][0] + coords[ps[13]][0]) - 2 * (coords[ps[0]][0] + coords[ps[15]][0]) + 3 * (coords[ps[11]][0] + coords[ps[1]][0])) / 9, (-4 * coords[ps[12]][1] - coords[ps[3]][1] + 6 * (coords[ps[8]][1] + coords[ps[13]][1]) - 2 * (coords[ps[0]][1] + coords[ps[15]][1]) + 3 * (coords[ps[11]][1] + coords[ps[1]][1])) / 9]);
+ ps[10] = coords.length;
+ coords.push([(-4 * coords[ps[15]][0] - coords[ps[0]][0] + 6 * (coords[ps[11]][0] + coords[ps[14]][0]) - 2 * (coords[ps[12]][0] + coords[ps[3]][0]) + 3 * (coords[ps[2]][0] + coords[ps[8]][0])) / 9, (-4 * coords[ps[15]][1] - coords[ps[0]][1] + 6 * (coords[ps[11]][1] + coords[ps[14]][1]) - 2 * (coords[ps[12]][1] + coords[ps[3]][1]) + 3 * (coords[ps[2]][1] + coords[ps[8]][1])) / 9]);
+ mesh.figures.push({
+ type: 'patch',
+ coords: new Int32Array(ps),
+ colors: new Int32Array(cs)
+ });
+ }
+ }
+
+ function decodeType7Shading(mesh, reader) {
+ var coords = mesh.coords;
+ var colors = mesh.colors;
+ var ps = new Int32Array(16);
+ var cs = new Int32Array(4);
+
+ while (reader.hasData) {
+ var f = reader.readFlag();
+
+ if (!(0 <= f && f <= 3)) {
+ throw new _util.FormatError('Unknown type7 flag');
+ }
+
+ var i, ii;
+ var pi = coords.length;
+
+ for (i = 0, ii = f !== 0 ? 12 : 16; i < ii; i++) {
+ coords.push(reader.readCoordinate());
+ }
+
+ var ci = colors.length;
+
+ for (i = 0, ii = f !== 0 ? 2 : 4; i < ii; i++) {
+ colors.push(reader.readComponents());
+ }
+
+ var tmp1, tmp2, tmp3, tmp4;
+
+ switch (f) {
+ case 0:
+ ps[12] = pi + 3;
+ ps[13] = pi + 4;
+ ps[14] = pi + 5;
+ ps[15] = pi + 6;
+ ps[8] = pi + 2;
+ ps[9] = pi + 13;
+ ps[10] = pi + 14;
+ ps[11] = pi + 7;
+ ps[4] = pi + 1;
+ ps[5] = pi + 12;
+ ps[6] = pi + 15;
+ ps[7] = pi + 8;
+ ps[0] = pi;
+ ps[1] = pi + 11;
+ ps[2] = pi + 10;
+ ps[3] = pi + 9;
+ cs[2] = ci + 1;
+ cs[3] = ci + 2;
+ cs[0] = ci;
+ cs[1] = ci + 3;
+ break;
+
+ case 1:
+ tmp1 = ps[12];
+ tmp2 = ps[13];
+ tmp3 = ps[14];
+ tmp4 = ps[15];
+ ps[12] = tmp4;
+ ps[13] = pi + 0;
+ ps[14] = pi + 1;
+ ps[15] = pi + 2;
+ ps[8] = tmp3;
+ ps[9] = pi + 9;
+ ps[10] = pi + 10;
+ ps[11] = pi + 3;
+ ps[4] = tmp2;
+ ps[5] = pi + 8;
+ ps[6] = pi + 11;
+ ps[7] = pi + 4;
+ ps[0] = tmp1;
+ ps[1] = pi + 7;
+ ps[2] = pi + 6;
+ ps[3] = pi + 5;
+ tmp1 = cs[2];
+ tmp2 = cs[3];
+ cs[2] = tmp2;
+ cs[3] = ci;
+ cs[0] = tmp1;
+ cs[1] = ci + 1;
+ break;
+
+ case 2:
+ tmp1 = ps[15];
+ tmp2 = ps[11];
+ ps[12] = ps[3];
+ ps[13] = pi + 0;
+ ps[14] = pi + 1;
+ ps[15] = pi + 2;
+ ps[8] = ps[7];
+ ps[9] = pi + 9;
+ ps[10] = pi + 10;
+ ps[11] = pi + 3;
+ ps[4] = tmp2;
+ ps[5] = pi + 8;
+ ps[6] = pi + 11;
+ ps[7] = pi + 4;
+ ps[0] = tmp1;
+ ps[1] = pi + 7;
+ ps[2] = pi + 6;
+ ps[3] = pi + 5;
+ tmp1 = cs[3];
+ cs[2] = cs[1];
+ cs[3] = ci;
+ cs[0] = tmp1;
+ cs[1] = ci + 1;
+ break;
+
+ case 3:
+ ps[12] = ps[0];
+ ps[13] = pi + 0;
+ ps[14] = pi + 1;
+ ps[15] = pi + 2;
+ ps[8] = ps[1];
+ ps[9] = pi + 9;
+ ps[10] = pi + 10;
+ ps[11] = pi + 3;
+ ps[4] = ps[2];
+ ps[5] = pi + 8;
+ ps[6] = pi + 11;
+ ps[7] = pi + 4;
+ ps[0] = ps[3];
+ ps[1] = pi + 7;
+ ps[2] = pi + 6;
+ ps[3] = pi + 5;
+ cs[2] = cs[0];
+ cs[3] = ci;
+ cs[0] = cs[1];
+ cs[1] = ci + 1;
+ break;
+ }
+
+ mesh.figures.push({
+ type: 'patch',
+ coords: new Int32Array(ps),
+ colors: new Int32Array(cs)
+ });
+ }
+ }
+
+ function updateBounds(mesh) {
+ var minX = mesh.coords[0][0],
+ minY = mesh.coords[0][1],
+ maxX = minX,
+ maxY = minY;
+
+ for (var i = 1, ii = mesh.coords.length; i < ii; i++) {
+ var x = mesh.coords[i][0],
+ y = mesh.coords[i][1];
+ minX = minX > x ? x : minX;
+ minY = minY > y ? y : minY;
+ maxX = maxX < x ? x : maxX;
+ maxY = maxY < y ? y : maxY;
+ }
+
+ mesh.bounds = [minX, minY, maxX, maxY];
+ }
+
+ function packData(mesh) {
+ var i, ii, j, jj;
+ var coords = mesh.coords;
+ var coordsPacked = new Float32Array(coords.length * 2);
+
+ for (i = 0, j = 0, ii = coords.length; i < ii; i++) {
+ var xy = coords[i];
+ coordsPacked[j++] = xy[0];
+ coordsPacked[j++] = xy[1];
+ }
+
+ mesh.coords = coordsPacked;
+ var colors = mesh.colors;
+ var colorsPacked = new Uint8Array(colors.length * 3);
+
+ for (i = 0, j = 0, ii = colors.length; i < ii; i++) {
+ var c = colors[i];
+ colorsPacked[j++] = c[0];
+ colorsPacked[j++] = c[1];
+ colorsPacked[j++] = c[2];
+ }
+
+ mesh.colors = colorsPacked;
+ var figures = mesh.figures;
+
+ for (i = 0, ii = figures.length; i < ii; i++) {
+ var figure = figures[i],
+ ps = figure.coords,
+ cs = figure.colors;
+
+ for (j = 0, jj = ps.length; j < jj; j++) {
+ ps[j] *= 2;
+ cs[j] *= 3;
+ }
+ }
+ }
+
+ function Mesh(stream, matrix, xref, res, pdfFunctionFactory) {
+ if (!(0, _primitives.isStream)(stream)) {
+ throw new _util.FormatError('Mesh data is not a stream');
+ }
+
+ var dict = stream.dict;
+ this.matrix = matrix;
+ this.shadingType = dict.get('ShadingType');
+ this.type = 'Pattern';
+ this.bbox = dict.getArray('BBox');
+ var cs = dict.get('ColorSpace', 'CS');
+ cs = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+ this.cs = cs;
+ this.background = dict.has('Background') ? cs.getRgb(dict.get('Background'), 0) : null;
+ var fnObj = dict.get('Function');
+ var fn = fnObj ? pdfFunctionFactory.createFromArray(fnObj) : null;
+ this.coords = [];
+ this.colors = [];
+ this.figures = [];
+ var decodeContext = {
+ bitsPerCoordinate: dict.get('BitsPerCoordinate'),
+ bitsPerComponent: dict.get('BitsPerComponent'),
+ bitsPerFlag: dict.get('BitsPerFlag'),
+ decode: dict.getArray('Decode'),
+ colorFn: fn,
+ colorSpace: cs,
+ numComps: fn ? 1 : cs.numComps
+ };
+ var reader = new MeshStreamReader(stream, decodeContext);
+ var patchMesh = false;
+
+ switch (this.shadingType) {
+ case ShadingType.FREE_FORM_MESH:
+ decodeType4Shading(this, reader);
+ break;
+
+ case ShadingType.LATTICE_FORM_MESH:
+ var verticesPerRow = dict.get('VerticesPerRow') | 0;
+
+ if (verticesPerRow < 2) {
+ throw new _util.FormatError('Invalid VerticesPerRow');
+ }
+
+ decodeType5Shading(this, reader, verticesPerRow);
+ break;
+
+ case ShadingType.COONS_PATCH_MESH:
+ decodeType6Shading(this, reader);
+ patchMesh = true;
+ break;
+
+ case ShadingType.TENSOR_PATCH_MESH:
+ decodeType7Shading(this, reader);
+ patchMesh = true;
+ break;
+
+ default:
+ (0, _util.unreachable)('Unsupported mesh type.');
+ break;
+ }
+
+ if (patchMesh) {
+ updateBounds(this);
+
+ for (var i = 0, ii = this.figures.length; i < ii; i++) {
+ buildFigureFromPatch(this, i);
+ }
+ }
+
+ updateBounds(this);
+ packData(this);
+ }
+
+ Mesh.prototype = {
+ getIR: function Mesh_getIR() {
+ return ['Mesh', this.shadingType, this.coords, this.colors, this.figures, this.bounds, this.matrix, this.bbox, this.background];
+ }
+ };
+ return Mesh;
+}();
+
+Shadings.Dummy = function DummyClosure() {
+ function Dummy() {
+ this.type = 'Pattern';
+ }
+
+ Dummy.prototype = {
+ getIR: function Dummy_getIR() {
+ return ['Dummy'];
+ }
+ };
+ return Dummy;
+}();
+
+function getTilingPatternIR(operatorList, dict, args) {
+ var matrix = dict.getArray('Matrix');
+
+ var bbox = _util.Util.normalizeRect(dict.getArray('BBox'));
+
+ var xstep = dict.get('XStep');
+ var ystep = dict.get('YStep');
+ var paintType = dict.get('PaintType');
+ var tilingType = dict.get('TilingType');
+
+ if (bbox[2] - bbox[0] === 0 || bbox[3] - bbox[1] === 0) {
+ throw new _util.FormatError("Invalid getTilingPatternIR /BBox array: [".concat(bbox, "]."));
+ }
+
+ return ['TilingPattern', args, operatorList, matrix, bbox, xstep, ystep, paintType, tilingType];
+}
+
+/***/ }),
+/* 184 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.bidi = bidi;
+
+var _util = __w_pdfjs_require__(5);
+
+var baseTypes = ['BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'S', 'B', 'S', 'WS', 'B', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'B', 'B', 'B', 'S', 'WS', 'ON', 'ON', 'ET', 'ET', 'ET', 'ON', 'ON', 'ON', 'ON', 'ON', 'ES', 'CS', 'ES', 'CS', 'CS', 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'CS', 'ON', 'ON', 'ON', 'ON', 'ON', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'ON', 'ON', 'ON', 'ON', 'ON', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'ON', 'ON', 'ON', 'ON', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'B', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'CS', 'ON', 'ET', 'ET', 'ET', 'ET', 'ON', 'ON', 'ON', 'ON', 'L', 'ON', 'ON', 'BN', 'ON', 'ON', 'ET', 'ET', 'EN', 'EN', 'ON', 'L', 'ON', 'ON', 'ON', 'EN', 'L', 'ON', 'ON', 'ON', 'ON', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L'];
+var arabicTypes = ['AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'ON', 'ON', 'AL', 'ET', 'ET', 'AL', 'CS', 'AL', 'ON', 'ON', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'AL', 'AL', '', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'ET', 'AN', 'AN', 'AL', 'AL', 'AL', 'NSM', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'AN', 'ON', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'AL', 'AL', 'NSM', 'NSM', 'ON', 'NSM', 'NSM', 'NSM', 'NSM', 'AL', 'AL', 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL'];
+
+function isOdd(i) {
+ return (i & 1) !== 0;
+}
+
+function isEven(i) {
+ return (i & 1) === 0;
+}
+
+function findUnequal(arr, start, value) {
+ for (var j = start, jj = arr.length; j < jj; ++j) {
+ if (arr[j] !== value) {
+ return j;
+ }
+ }
+
+ return j;
+}
+
+function setValues(arr, start, end, value) {
+ for (var j = start; j < end; ++j) {
+ arr[j] = value;
+ }
+}
+
+function reverseValues(arr, start, end) {
+ for (var i = start, j = end - 1; i < j; ++i, --j) {
+ var temp = arr[i];
+ arr[i] = arr[j];
+ arr[j] = temp;
+ }
+}
+
+function createBidiText(str, isLTR, vertical) {
+ return {
+ str: str,
+ dir: vertical ? 'ttb' : isLTR ? 'ltr' : 'rtl'
+ };
+}
+
+var chars = [];
+var types = [];
+
+function bidi(str, startLevel, vertical) {
+ var isLTR = true;
+ var strLength = str.length;
+
+ if (strLength === 0 || vertical) {
+ return createBidiText(str, isLTR, vertical);
+ }
+
+ chars.length = strLength;
+ types.length = strLength;
+ var numBidi = 0;
+ var i, ii;
+
+ for (i = 0; i < strLength; ++i) {
+ chars[i] = str.charAt(i);
+ var charCode = str.charCodeAt(i);
+ var charType = 'L';
+
+ if (charCode <= 0x00ff) {
+ charType = baseTypes[charCode];
+ } else if (0x0590 <= charCode && charCode <= 0x05f4) {
+ charType = 'R';
+ } else if (0x0600 <= charCode && charCode <= 0x06ff) {
+ charType = arabicTypes[charCode & 0xff];
+
+ if (!charType) {
+ (0, _util.warn)('Bidi: invalid Unicode character ' + charCode.toString(16));
+ }
+ } else if (0x0700 <= charCode && charCode <= 0x08AC) {
+ charType = 'AL';
+ }
+
+ if (charType === 'R' || charType === 'AL' || charType === 'AN') {
+ numBidi++;
+ }
+
+ types[i] = charType;
+ }
+
+ if (numBidi === 0) {
+ isLTR = true;
+ return createBidiText(str, isLTR);
+ }
+
+ if (startLevel === -1) {
+ if (numBidi / strLength < 0.3) {
+ isLTR = true;
+ startLevel = 0;
+ } else {
+ isLTR = false;
+ startLevel = 1;
+ }
+ }
+
+ var levels = [];
+
+ for (i = 0; i < strLength; ++i) {
+ levels[i] = startLevel;
+ }
+
+ var e = isOdd(startLevel) ? 'R' : 'L';
+ var sor = e;
+ var eor = sor;
+ var lastType = sor;
+
+ for (i = 0; i < strLength; ++i) {
+ if (types[i] === 'NSM') {
+ types[i] = lastType;
+ } else {
+ lastType = types[i];
+ }
+ }
+
+ lastType = sor;
+ var t;
+
+ for (i = 0; i < strLength; ++i) {
+ t = types[i];
+
+ if (t === 'EN') {
+ types[i] = lastType === 'AL' ? 'AN' : 'EN';
+ } else if (t === 'R' || t === 'L' || t === 'AL') {
+ lastType = t;
+ }
+ }
+
+ for (i = 0; i < strLength; ++i) {
+ t = types[i];
+
+ if (t === 'AL') {
+ types[i] = 'R';
+ }
+ }
+
+ for (i = 1; i < strLength - 1; ++i) {
+ if (types[i] === 'ES' && types[i - 1] === 'EN' && types[i + 1] === 'EN') {
+ types[i] = 'EN';
+ }
+
+ if (types[i] === 'CS' && (types[i - 1] === 'EN' || types[i - 1] === 'AN') && types[i + 1] === types[i - 1]) {
+ types[i] = types[i - 1];
+ }
+ }
+
+ for (i = 0; i < strLength; ++i) {
+ if (types[i] === 'EN') {
+ var j;
+
+ for (j = i - 1; j >= 0; --j) {
+ if (types[j] !== 'ET') {
+ break;
+ }
+
+ types[j] = 'EN';
+ }
+
+ for (j = i + 1; j < strLength; ++j) {
+ if (types[j] !== 'ET') {
+ break;
+ }
+
+ types[j] = 'EN';
+ }
+ }
+ }
+
+ for (i = 0; i < strLength; ++i) {
+ t = types[i];
+
+ if (t === 'WS' || t === 'ES' || t === 'ET' || t === 'CS') {
+ types[i] = 'ON';
+ }
+ }
+
+ lastType = sor;
+
+ for (i = 0; i < strLength; ++i) {
+ t = types[i];
+
+ if (t === 'EN') {
+ types[i] = lastType === 'L' ? 'L' : 'EN';
+ } else if (t === 'R' || t === 'L') {
+ lastType = t;
+ }
+ }
+
+ for (i = 0; i < strLength; ++i) {
+ if (types[i] === 'ON') {
+ var end = findUnequal(types, i + 1, 'ON');
+ var before = sor;
+
+ if (i > 0) {
+ before = types[i - 1];
+ }
+
+ var after = eor;
+
+ if (end + 1 < strLength) {
+ after = types[end + 1];
+ }
+
+ if (before !== 'L') {
+ before = 'R';
+ }
+
+ if (after !== 'L') {
+ after = 'R';
+ }
+
+ if (before === after) {
+ setValues(types, i, end, before);
+ }
+
+ i = end - 1;
+ }
+ }
+
+ for (i = 0; i < strLength; ++i) {
+ if (types[i] === 'ON') {
+ types[i] = e;
+ }
+ }
+
+ for (i = 0; i < strLength; ++i) {
+ t = types[i];
+
+ if (isEven(levels[i])) {
+ if (t === 'R') {
+ levels[i] += 1;
+ } else if (t === 'AN' || t === 'EN') {
+ levels[i] += 2;
+ }
+ } else {
+ if (t === 'L' || t === 'AN' || t === 'EN') {
+ levels[i] += 1;
+ }
+ }
+ }
+
+ var highestLevel = -1;
+ var lowestOddLevel = 99;
+ var level;
+
+ for (i = 0, ii = levels.length; i < ii; ++i) {
+ level = levels[i];
+
+ if (highestLevel < level) {
+ highestLevel = level;
+ }
+
+ if (lowestOddLevel > level && isOdd(level)) {
+ lowestOddLevel = level;
+ }
+ }
+
+ for (level = highestLevel; level >= lowestOddLevel; --level) {
+ var start = -1;
+
+ for (i = 0, ii = levels.length; i < ii; ++i) {
+ if (levels[i] < level) {
+ if (start >= 0) {
+ reverseValues(chars, start, i);
+ start = -1;
+ }
+ } else if (start < 0) {
+ start = i;
+ }
+ }
+
+ if (start >= 0) {
+ reverseValues(chars, start, levels.length);
+ }
+ }
+
+ for (i = 0, ii = chars.length; i < ii; ++i) {
+ var ch = chars[i];
+
+ if (ch === '<' || ch === '>') {
+ chars[i] = '';
+ }
+ }
+
+ return createBidiText(chars.join(''), isLTR);
+}
+
+/***/ }),
+/* 185 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.getMetrics = void 0;
+
+var _core_utils = __w_pdfjs_require__(154);
+
+var getMetrics = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['Courier'] = 600;
+ t['Courier-Bold'] = 600;
+ t['Courier-BoldOblique'] = 600;
+ t['Courier-Oblique'] = 600;
+ t['Helvetica'] = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['space'] = 278;
+ t['exclam'] = 278;
+ t['quotedbl'] = 355;
+ t['numbersign'] = 556;
+ t['dollar'] = 556;
+ t['percent'] = 889;
+ t['ampersand'] = 667;
+ t['quoteright'] = 222;
+ t['parenleft'] = 333;
+ t['parenright'] = 333;
+ t['asterisk'] = 389;
+ t['plus'] = 584;
+ t['comma'] = 278;
+ t['hyphen'] = 333;
+ t['period'] = 278;
+ t['slash'] = 278;
+ t['zero'] = 556;
+ t['one'] = 556;
+ t['two'] = 556;
+ t['three'] = 556;
+ t['four'] = 556;
+ t['five'] = 556;
+ t['six'] = 556;
+ t['seven'] = 556;
+ t['eight'] = 556;
+ t['nine'] = 556;
+ t['colon'] = 278;
+ t['semicolon'] = 278;
+ t['less'] = 584;
+ t['equal'] = 584;
+ t['greater'] = 584;
+ t['question'] = 556;
+ t['at'] = 1015;
+ t['A'] = 667;
+ t['B'] = 667;
+ t['C'] = 722;
+ t['D'] = 722;
+ t['E'] = 667;
+ t['F'] = 611;
+ t['G'] = 778;
+ t['H'] = 722;
+ t['I'] = 278;
+ t['J'] = 500;
+ t['K'] = 667;
+ t['L'] = 556;
+ t['M'] = 833;
+ t['N'] = 722;
+ t['O'] = 778;
+ t['P'] = 667;
+ t['Q'] = 778;
+ t['R'] = 722;
+ t['S'] = 667;
+ t['T'] = 611;
+ t['U'] = 722;
+ t['V'] = 667;
+ t['W'] = 944;
+ t['X'] = 667;
+ t['Y'] = 667;
+ t['Z'] = 611;
+ t['bracketleft'] = 278;
+ t['backslash'] = 278;
+ t['bracketright'] = 278;
+ t['asciicircum'] = 469;
+ t['underscore'] = 556;
+ t['quoteleft'] = 222;
+ t['a'] = 556;
+ t['b'] = 556;
+ t['c'] = 500;
+ t['d'] = 556;
+ t['e'] = 556;
+ t['f'] = 278;
+ t['g'] = 556;
+ t['h'] = 556;
+ t['i'] = 222;
+ t['j'] = 222;
+ t['k'] = 500;
+ t['l'] = 222;
+ t['m'] = 833;
+ t['n'] = 556;
+ t['o'] = 556;
+ t['p'] = 556;
+ t['q'] = 556;
+ t['r'] = 333;
+ t['s'] = 500;
+ t['t'] = 278;
+ t['u'] = 556;
+ t['v'] = 500;
+ t['w'] = 722;
+ t['x'] = 500;
+ t['y'] = 500;
+ t['z'] = 500;
+ t['braceleft'] = 334;
+ t['bar'] = 260;
+ t['braceright'] = 334;
+ t['asciitilde'] = 584;
+ t['exclamdown'] = 333;
+ t['cent'] = 556;
+ t['sterling'] = 556;
+ t['fraction'] = 167;
+ t['yen'] = 556;
+ t['florin'] = 556;
+ t['section'] = 556;
+ t['currency'] = 556;
+ t['quotesingle'] = 191;
+ t['quotedblleft'] = 333;
+ t['guillemotleft'] = 556;
+ t['guilsinglleft'] = 333;
+ t['guilsinglright'] = 333;
+ t['fi'] = 500;
+ t['fl'] = 500;
+ t['endash'] = 556;
+ t['dagger'] = 556;
+ t['daggerdbl'] = 556;
+ t['periodcentered'] = 278;
+ t['paragraph'] = 537;
+ t['bullet'] = 350;
+ t['quotesinglbase'] = 222;
+ t['quotedblbase'] = 333;
+ t['quotedblright'] = 333;
+ t['guillemotright'] = 556;
+ t['ellipsis'] = 1000;
+ t['perthousand'] = 1000;
+ t['questiondown'] = 611;
+ t['grave'] = 333;
+ t['acute'] = 333;
+ t['circumflex'] = 333;
+ t['tilde'] = 333;
+ t['macron'] = 333;
+ t['breve'] = 333;
+ t['dotaccent'] = 333;
+ t['dieresis'] = 333;
+ t['ring'] = 333;
+ t['cedilla'] = 333;
+ t['hungarumlaut'] = 333;
+ t['ogonek'] = 333;
+ t['caron'] = 333;
+ t['emdash'] = 1000;
+ t['AE'] = 1000;
+ t['ordfeminine'] = 370;
+ t['Lslash'] = 556;
+ t['Oslash'] = 778;
+ t['OE'] = 1000;
+ t['ordmasculine'] = 365;
+ t['ae'] = 889;
+ t['dotlessi'] = 278;
+ t['lslash'] = 222;
+ t['oslash'] = 611;
+ t['oe'] = 944;
+ t['germandbls'] = 611;
+ t['Idieresis'] = 278;
+ t['eacute'] = 556;
+ t['abreve'] = 556;
+ t['uhungarumlaut'] = 556;
+ t['ecaron'] = 556;
+ t['Ydieresis'] = 667;
+ t['divide'] = 584;
+ t['Yacute'] = 667;
+ t['Acircumflex'] = 667;
+ t['aacute'] = 556;
+ t['Ucircumflex'] = 722;
+ t['yacute'] = 500;
+ t['scommaaccent'] = 500;
+ t['ecircumflex'] = 556;
+ t['Uring'] = 722;
+ t['Udieresis'] = 722;
+ t['aogonek'] = 556;
+ t['Uacute'] = 722;
+ t['uogonek'] = 556;
+ t['Edieresis'] = 667;
+ t['Dcroat'] = 722;
+ t['commaaccent'] = 250;
+ t['copyright'] = 737;
+ t['Emacron'] = 667;
+ t['ccaron'] = 500;
+ t['aring'] = 556;
+ t['Ncommaaccent'] = 722;
+ t['lacute'] = 222;
+ t['agrave'] = 556;
+ t['Tcommaaccent'] = 611;
+ t['Cacute'] = 722;
+ t['atilde'] = 556;
+ t['Edotaccent'] = 667;
+ t['scaron'] = 500;
+ t['scedilla'] = 500;
+ t['iacute'] = 278;
+ t['lozenge'] = 471;
+ t['Rcaron'] = 722;
+ t['Gcommaaccent'] = 778;
+ t['ucircumflex'] = 556;
+ t['acircumflex'] = 556;
+ t['Amacron'] = 667;
+ t['rcaron'] = 333;
+ t['ccedilla'] = 500;
+ t['Zdotaccent'] = 611;
+ t['Thorn'] = 667;
+ t['Omacron'] = 778;
+ t['Racute'] = 722;
+ t['Sacute'] = 667;
+ t['dcaron'] = 643;
+ t['Umacron'] = 722;
+ t['uring'] = 556;
+ t['threesuperior'] = 333;
+ t['Ograve'] = 778;
+ t['Agrave'] = 667;
+ t['Abreve'] = 667;
+ t['multiply'] = 584;
+ t['uacute'] = 556;
+ t['Tcaron'] = 611;
+ t['partialdiff'] = 476;
+ t['ydieresis'] = 500;
+ t['Nacute'] = 722;
+ t['icircumflex'] = 278;
+ t['Ecircumflex'] = 667;
+ t['adieresis'] = 556;
+ t['edieresis'] = 556;
+ t['cacute'] = 500;
+ t['nacute'] = 556;
+ t['umacron'] = 556;
+ t['Ncaron'] = 722;
+ t['Iacute'] = 278;
+ t['plusminus'] = 584;
+ t['brokenbar'] = 260;
+ t['registered'] = 737;
+ t['Gbreve'] = 778;
+ t['Idotaccent'] = 278;
+ t['summation'] = 600;
+ t['Egrave'] = 667;
+ t['racute'] = 333;
+ t['omacron'] = 556;
+ t['Zacute'] = 611;
+ t['Zcaron'] = 611;
+ t['greaterequal'] = 549;
+ t['Eth'] = 722;
+ t['Ccedilla'] = 722;
+ t['lcommaaccent'] = 222;
+ t['tcaron'] = 317;
+ t['eogonek'] = 556;
+ t['Uogonek'] = 722;
+ t['Aacute'] = 667;
+ t['Adieresis'] = 667;
+ t['egrave'] = 556;
+ t['zacute'] = 500;
+ t['iogonek'] = 222;
+ t['Oacute'] = 778;
+ t['oacute'] = 556;
+ t['amacron'] = 556;
+ t['sacute'] = 500;
+ t['idieresis'] = 278;
+ t['Ocircumflex'] = 778;
+ t['Ugrave'] = 722;
+ t['Delta'] = 612;
+ t['thorn'] = 556;
+ t['twosuperior'] = 333;
+ t['Odieresis'] = 778;
+ t['mu'] = 556;
+ t['igrave'] = 278;
+ t['ohungarumlaut'] = 556;
+ t['Eogonek'] = 667;
+ t['dcroat'] = 556;
+ t['threequarters'] = 834;
+ t['Scedilla'] = 667;
+ t['lcaron'] = 299;
+ t['Kcommaaccent'] = 667;
+ t['Lacute'] = 556;
+ t['trademark'] = 1000;
+ t['edotaccent'] = 556;
+ t['Igrave'] = 278;
+ t['Imacron'] = 278;
+ t['Lcaron'] = 556;
+ t['onehalf'] = 834;
+ t['lessequal'] = 549;
+ t['ocircumflex'] = 556;
+ t['ntilde'] = 556;
+ t['Uhungarumlaut'] = 722;
+ t['Eacute'] = 667;
+ t['emacron'] = 556;
+ t['gbreve'] = 556;
+ t['onequarter'] = 834;
+ t['Scaron'] = 667;
+ t['Scommaaccent'] = 667;
+ t['Ohungarumlaut'] = 778;
+ t['degree'] = 400;
+ t['ograve'] = 556;
+ t['Ccaron'] = 722;
+ t['ugrave'] = 556;
+ t['radical'] = 453;
+ t['Dcaron'] = 722;
+ t['rcommaaccent'] = 333;
+ t['Ntilde'] = 722;
+ t['otilde'] = 556;
+ t['Rcommaaccent'] = 722;
+ t['Lcommaaccent'] = 556;
+ t['Atilde'] = 667;
+ t['Aogonek'] = 667;
+ t['Aring'] = 667;
+ t['Otilde'] = 778;
+ t['zdotaccent'] = 500;
+ t['Ecaron'] = 667;
+ t['Iogonek'] = 278;
+ t['kcommaaccent'] = 500;
+ t['minus'] = 584;
+ t['Icircumflex'] = 278;
+ t['ncaron'] = 556;
+ t['tcommaaccent'] = 278;
+ t['logicalnot'] = 584;
+ t['odieresis'] = 556;
+ t['udieresis'] = 556;
+ t['notequal'] = 549;
+ t['gcommaaccent'] = 556;
+ t['eth'] = 556;
+ t['zcaron'] = 500;
+ t['ncommaaccent'] = 556;
+ t['onesuperior'] = 333;
+ t['imacron'] = 278;
+ t['Euro'] = 556;
+ });
+ t['Helvetica-Bold'] = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['space'] = 278;
+ t['exclam'] = 333;
+ t['quotedbl'] = 474;
+ t['numbersign'] = 556;
+ t['dollar'] = 556;
+ t['percent'] = 889;
+ t['ampersand'] = 722;
+ t['quoteright'] = 278;
+ t['parenleft'] = 333;
+ t['parenright'] = 333;
+ t['asterisk'] = 389;
+ t['plus'] = 584;
+ t['comma'] = 278;
+ t['hyphen'] = 333;
+ t['period'] = 278;
+ t['slash'] = 278;
+ t['zero'] = 556;
+ t['one'] = 556;
+ t['two'] = 556;
+ t['three'] = 556;
+ t['four'] = 556;
+ t['five'] = 556;
+ t['six'] = 556;
+ t['seven'] = 556;
+ t['eight'] = 556;
+ t['nine'] = 556;
+ t['colon'] = 333;
+ t['semicolon'] = 333;
+ t['less'] = 584;
+ t['equal'] = 584;
+ t['greater'] = 584;
+ t['question'] = 611;
+ t['at'] = 975;
+ t['A'] = 722;
+ t['B'] = 722;
+ t['C'] = 722;
+ t['D'] = 722;
+ t['E'] = 667;
+ t['F'] = 611;
+ t['G'] = 778;
+ t['H'] = 722;
+ t['I'] = 278;
+ t['J'] = 556;
+ t['K'] = 722;
+ t['L'] = 611;
+ t['M'] = 833;
+ t['N'] = 722;
+ t['O'] = 778;
+ t['P'] = 667;
+ t['Q'] = 778;
+ t['R'] = 722;
+ t['S'] = 667;
+ t['T'] = 611;
+ t['U'] = 722;
+ t['V'] = 667;
+ t['W'] = 944;
+ t['X'] = 667;
+ t['Y'] = 667;
+ t['Z'] = 611;
+ t['bracketleft'] = 333;
+ t['backslash'] = 278;
+ t['bracketright'] = 333;
+ t['asciicircum'] = 584;
+ t['underscore'] = 556;
+ t['quoteleft'] = 278;
+ t['a'] = 556;
+ t['b'] = 611;
+ t['c'] = 556;
+ t['d'] = 611;
+ t['e'] = 556;
+ t['f'] = 333;
+ t['g'] = 611;
+ t['h'] = 611;
+ t['i'] = 278;
+ t['j'] = 278;
+ t['k'] = 556;
+ t['l'] = 278;
+ t['m'] = 889;
+ t['n'] = 611;
+ t['o'] = 611;
+ t['p'] = 611;
+ t['q'] = 611;
+ t['r'] = 389;
+ t['s'] = 556;
+ t['t'] = 333;
+ t['u'] = 611;
+ t['v'] = 556;
+ t['w'] = 778;
+ t['x'] = 556;
+ t['y'] = 556;
+ t['z'] = 500;
+ t['braceleft'] = 389;
+ t['bar'] = 280;
+ t['braceright'] = 389;
+ t['asciitilde'] = 584;
+ t['exclamdown'] = 333;
+ t['cent'] = 556;
+ t['sterling'] = 556;
+ t['fraction'] = 167;
+ t['yen'] = 556;
+ t['florin'] = 556;
+ t['section'] = 556;
+ t['currency'] = 556;
+ t['quotesingle'] = 238;
+ t['quotedblleft'] = 500;
+ t['guillemotleft'] = 556;
+ t['guilsinglleft'] = 333;
+ t['guilsinglright'] = 333;
+ t['fi'] = 611;
+ t['fl'] = 611;
+ t['endash'] = 556;
+ t['dagger'] = 556;
+ t['daggerdbl'] = 556;
+ t['periodcentered'] = 278;
+ t['paragraph'] = 556;
+ t['bullet'] = 350;
+ t['quotesinglbase'] = 278;
+ t['quotedblbase'] = 500;
+ t['quotedblright'] = 500;
+ t['guillemotright'] = 556;
+ t['ellipsis'] = 1000;
+ t['perthousand'] = 1000;
+ t['questiondown'] = 611;
+ t['grave'] = 333;
+ t['acute'] = 333;
+ t['circumflex'] = 333;
+ t['tilde'] = 333;
+ t['macron'] = 333;
+ t['breve'] = 333;
+ t['dotaccent'] = 333;
+ t['dieresis'] = 333;
+ t['ring'] = 333;
+ t['cedilla'] = 333;
+ t['hungarumlaut'] = 333;
+ t['ogonek'] = 333;
+ t['caron'] = 333;
+ t['emdash'] = 1000;
+ t['AE'] = 1000;
+ t['ordfeminine'] = 370;
+ t['Lslash'] = 611;
+ t['Oslash'] = 778;
+ t['OE'] = 1000;
+ t['ordmasculine'] = 365;
+ t['ae'] = 889;
+ t['dotlessi'] = 278;
+ t['lslash'] = 278;
+ t['oslash'] = 611;
+ t['oe'] = 944;
+ t['germandbls'] = 611;
+ t['Idieresis'] = 278;
+ t['eacute'] = 556;
+ t['abreve'] = 556;
+ t['uhungarumlaut'] = 611;
+ t['ecaron'] = 556;
+ t['Ydieresis'] = 667;
+ t['divide'] = 584;
+ t['Yacute'] = 667;
+ t['Acircumflex'] = 722;
+ t['aacute'] = 556;
+ t['Ucircumflex'] = 722;
+ t['yacute'] = 556;
+ t['scommaaccent'] = 556;
+ t['ecircumflex'] = 556;
+ t['Uring'] = 722;
+ t['Udieresis'] = 722;
+ t['aogonek'] = 556;
+ t['Uacute'] = 722;
+ t['uogonek'] = 611;
+ t['Edieresis'] = 667;
+ t['Dcroat'] = 722;
+ t['commaaccent'] = 250;
+ t['copyright'] = 737;
+ t['Emacron'] = 667;
+ t['ccaron'] = 556;
+ t['aring'] = 556;
+ t['Ncommaaccent'] = 722;
+ t['lacute'] = 278;
+ t['agrave'] = 556;
+ t['Tcommaaccent'] = 611;
+ t['Cacute'] = 722;
+ t['atilde'] = 556;
+ t['Edotaccent'] = 667;
+ t['scaron'] = 556;
+ t['scedilla'] = 556;
+ t['iacute'] = 278;
+ t['lozenge'] = 494;
+ t['Rcaron'] = 722;
+ t['Gcommaaccent'] = 778;
+ t['ucircumflex'] = 611;
+ t['acircumflex'] = 556;
+ t['Amacron'] = 722;
+ t['rcaron'] = 389;
+ t['ccedilla'] = 556;
+ t['Zdotaccent'] = 611;
+ t['Thorn'] = 667;
+ t['Omacron'] = 778;
+ t['Racute'] = 722;
+ t['Sacute'] = 667;
+ t['dcaron'] = 743;
+ t['Umacron'] = 722;
+ t['uring'] = 611;
+ t['threesuperior'] = 333;
+ t['Ograve'] = 778;
+ t['Agrave'] = 722;
+ t['Abreve'] = 722;
+ t['multiply'] = 584;
+ t['uacute'] = 611;
+ t['Tcaron'] = 611;
+ t['partialdiff'] = 494;
+ t['ydieresis'] = 556;
+ t['Nacute'] = 722;
+ t['icircumflex'] = 278;
+ t['Ecircumflex'] = 667;
+ t['adieresis'] = 556;
+ t['edieresis'] = 556;
+ t['cacute'] = 556;
+ t['nacute'] = 611;
+ t['umacron'] = 611;
+ t['Ncaron'] = 722;
+ t['Iacute'] = 278;
+ t['plusminus'] = 584;
+ t['brokenbar'] = 280;
+ t['registered'] = 737;
+ t['Gbreve'] = 778;
+ t['Idotaccent'] = 278;
+ t['summation'] = 600;
+ t['Egrave'] = 667;
+ t['racute'] = 389;
+ t['omacron'] = 611;
+ t['Zacute'] = 611;
+ t['Zcaron'] = 611;
+ t['greaterequal'] = 549;
+ t['Eth'] = 722;
+ t['Ccedilla'] = 722;
+ t['lcommaaccent'] = 278;
+ t['tcaron'] = 389;
+ t['eogonek'] = 556;
+ t['Uogonek'] = 722;
+ t['Aacute'] = 722;
+ t['Adieresis'] = 722;
+ t['egrave'] = 556;
+ t['zacute'] = 500;
+ t['iogonek'] = 278;
+ t['Oacute'] = 778;
+ t['oacute'] = 611;
+ t['amacron'] = 556;
+ t['sacute'] = 556;
+ t['idieresis'] = 278;
+ t['Ocircumflex'] = 778;
+ t['Ugrave'] = 722;
+ t['Delta'] = 612;
+ t['thorn'] = 611;
+ t['twosuperior'] = 333;
+ t['Odieresis'] = 778;
+ t['mu'] = 611;
+ t['igrave'] = 278;
+ t['ohungarumlaut'] = 611;
+ t['Eogonek'] = 667;
+ t['dcroat'] = 611;
+ t['threequarters'] = 834;
+ t['Scedilla'] = 667;
+ t['lcaron'] = 400;
+ t['Kcommaaccent'] = 722;
+ t['Lacute'] = 611;
+ t['trademark'] = 1000;
+ t['edotaccent'] = 556;
+ t['Igrave'] = 278;
+ t['Imacron'] = 278;
+ t['Lcaron'] = 611;
+ t['onehalf'] = 834;
+ t['lessequal'] = 549;
+ t['ocircumflex'] = 611;
+ t['ntilde'] = 611;
+ t['Uhungarumlaut'] = 722;
+ t['Eacute'] = 667;
+ t['emacron'] = 556;
+ t['gbreve'] = 611;
+ t['onequarter'] = 834;
+ t['Scaron'] = 667;
+ t['Scommaaccent'] = 667;
+ t['Ohungarumlaut'] = 778;
+ t['degree'] = 400;
+ t['ograve'] = 611;
+ t['Ccaron'] = 722;
+ t['ugrave'] = 611;
+ t['radical'] = 549;
+ t['Dcaron'] = 722;
+ t['rcommaaccent'] = 389;
+ t['Ntilde'] = 722;
+ t['otilde'] = 611;
+ t['Rcommaaccent'] = 722;
+ t['Lcommaaccent'] = 611;
+ t['Atilde'] = 722;
+ t['Aogonek'] = 722;
+ t['Aring'] = 722;
+ t['Otilde'] = 778;
+ t['zdotaccent'] = 500;
+ t['Ecaron'] = 667;
+ t['Iogonek'] = 278;
+ t['kcommaaccent'] = 556;
+ t['minus'] = 584;
+ t['Icircumflex'] = 278;
+ t['ncaron'] = 611;
+ t['tcommaaccent'] = 333;
+ t['logicalnot'] = 584;
+ t['odieresis'] = 611;
+ t['udieresis'] = 611;
+ t['notequal'] = 549;
+ t['gcommaaccent'] = 611;
+ t['eth'] = 611;
+ t['zcaron'] = 500;
+ t['ncommaaccent'] = 611;
+ t['onesuperior'] = 333;
+ t['imacron'] = 278;
+ t['Euro'] = 556;
+ });
+ t['Helvetica-BoldOblique'] = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['space'] = 278;
+ t['exclam'] = 333;
+ t['quotedbl'] = 474;
+ t['numbersign'] = 556;
+ t['dollar'] = 556;
+ t['percent'] = 889;
+ t['ampersand'] = 722;
+ t['quoteright'] = 278;
+ t['parenleft'] = 333;
+ t['parenright'] = 333;
+ t['asterisk'] = 389;
+ t['plus'] = 584;
+ t['comma'] = 278;
+ t['hyphen'] = 333;
+ t['period'] = 278;
+ t['slash'] = 278;
+ t['zero'] = 556;
+ t['one'] = 556;
+ t['two'] = 556;
+ t['three'] = 556;
+ t['four'] = 556;
+ t['five'] = 556;
+ t['six'] = 556;
+ t['seven'] = 556;
+ t['eight'] = 556;
+ t['nine'] = 556;
+ t['colon'] = 333;
+ t['semicolon'] = 333;
+ t['less'] = 584;
+ t['equal'] = 584;
+ t['greater'] = 584;
+ t['question'] = 611;
+ t['at'] = 975;
+ t['A'] = 722;
+ t['B'] = 722;
+ t['C'] = 722;
+ t['D'] = 722;
+ t['E'] = 667;
+ t['F'] = 611;
+ t['G'] = 778;
+ t['H'] = 722;
+ t['I'] = 278;
+ t['J'] = 556;
+ t['K'] = 722;
+ t['L'] = 611;
+ t['M'] = 833;
+ t['N'] = 722;
+ t['O'] = 778;
+ t['P'] = 667;
+ t['Q'] = 778;
+ t['R'] = 722;
+ t['S'] = 667;
+ t['T'] = 611;
+ t['U'] = 722;
+ t['V'] = 667;
+ t['W'] = 944;
+ t['X'] = 667;
+ t['Y'] = 667;
+ t['Z'] = 611;
+ t['bracketleft'] = 333;
+ t['backslash'] = 278;
+ t['bracketright'] = 333;
+ t['asciicircum'] = 584;
+ t['underscore'] = 556;
+ t['quoteleft'] = 278;
+ t['a'] = 556;
+ t['b'] = 611;
+ t['c'] = 556;
+ t['d'] = 611;
+ t['e'] = 556;
+ t['f'] = 333;
+ t['g'] = 611;
+ t['h'] = 611;
+ t['i'] = 278;
+ t['j'] = 278;
+ t['k'] = 556;
+ t['l'] = 278;
+ t['m'] = 889;
+ t['n'] = 611;
+ t['o'] = 611;
+ t['p'] = 611;
+ t['q'] = 611;
+ t['r'] = 389;
+ t['s'] = 556;
+ t['t'] = 333;
+ t['u'] = 611;
+ t['v'] = 556;
+ t['w'] = 778;
+ t['x'] = 556;
+ t['y'] = 556;
+ t['z'] = 500;
+ t['braceleft'] = 389;
+ t['bar'] = 280;
+ t['braceright'] = 389;
+ t['asciitilde'] = 584;
+ t['exclamdown'] = 333;
+ t['cent'] = 556;
+ t['sterling'] = 556;
+ t['fraction'] = 167;
+ t['yen'] = 556;
+ t['florin'] = 556;
+ t['section'] = 556;
+ t['currency'] = 556;
+ t['quotesingle'] = 238;
+ t['quotedblleft'] = 500;
+ t['guillemotleft'] = 556;
+ t['guilsinglleft'] = 333;
+ t['guilsinglright'] = 333;
+ t['fi'] = 611;
+ t['fl'] = 611;
+ t['endash'] = 556;
+ t['dagger'] = 556;
+ t['daggerdbl'] = 556;
+ t['periodcentered'] = 278;
+ t['paragraph'] = 556;
+ t['bullet'] = 350;
+ t['quotesinglbase'] = 278;
+ t['quotedblbase'] = 500;
+ t['quotedblright'] = 500;
+ t['guillemotright'] = 556;
+ t['ellipsis'] = 1000;
+ t['perthousand'] = 1000;
+ t['questiondown'] = 611;
+ t['grave'] = 333;
+ t['acute'] = 333;
+ t['circumflex'] = 333;
+ t['tilde'] = 333;
+ t['macron'] = 333;
+ t['breve'] = 333;
+ t['dotaccent'] = 333;
+ t['dieresis'] = 333;
+ t['ring'] = 333;
+ t['cedilla'] = 333;
+ t['hungarumlaut'] = 333;
+ t['ogonek'] = 333;
+ t['caron'] = 333;
+ t['emdash'] = 1000;
+ t['AE'] = 1000;
+ t['ordfeminine'] = 370;
+ t['Lslash'] = 611;
+ t['Oslash'] = 778;
+ t['OE'] = 1000;
+ t['ordmasculine'] = 365;
+ t['ae'] = 889;
+ t['dotlessi'] = 278;
+ t['lslash'] = 278;
+ t['oslash'] = 611;
+ t['oe'] = 944;
+ t['germandbls'] = 611;
+ t['Idieresis'] = 278;
+ t['eacute'] = 556;
+ t['abreve'] = 556;
+ t['uhungarumlaut'] = 611;
+ t['ecaron'] = 556;
+ t['Ydieresis'] = 667;
+ t['divide'] = 584;
+ t['Yacute'] = 667;
+ t['Acircumflex'] = 722;
+ t['aacute'] = 556;
+ t['Ucircumflex'] = 722;
+ t['yacute'] = 556;
+ t['scommaaccent'] = 556;
+ t['ecircumflex'] = 556;
+ t['Uring'] = 722;
+ t['Udieresis'] = 722;
+ t['aogonek'] = 556;
+ t['Uacute'] = 722;
+ t['uogonek'] = 611;
+ t['Edieresis'] = 667;
+ t['Dcroat'] = 722;
+ t['commaaccent'] = 250;
+ t['copyright'] = 737;
+ t['Emacron'] = 667;
+ t['ccaron'] = 556;
+ t['aring'] = 556;
+ t['Ncommaaccent'] = 722;
+ t['lacute'] = 278;
+ t['agrave'] = 556;
+ t['Tcommaaccent'] = 611;
+ t['Cacute'] = 722;
+ t['atilde'] = 556;
+ t['Edotaccent'] = 667;
+ t['scaron'] = 556;
+ t['scedilla'] = 556;
+ t['iacute'] = 278;
+ t['lozenge'] = 494;
+ t['Rcaron'] = 722;
+ t['Gcommaaccent'] = 778;
+ t['ucircumflex'] = 611;
+ t['acircumflex'] = 556;
+ t['Amacron'] = 722;
+ t['rcaron'] = 389;
+ t['ccedilla'] = 556;
+ t['Zdotaccent'] = 611;
+ t['Thorn'] = 667;
+ t['Omacron'] = 778;
+ t['Racute'] = 722;
+ t['Sacute'] = 667;
+ t['dcaron'] = 743;
+ t['Umacron'] = 722;
+ t['uring'] = 611;
+ t['threesuperior'] = 333;
+ t['Ograve'] = 778;
+ t['Agrave'] = 722;
+ t['Abreve'] = 722;
+ t['multiply'] = 584;
+ t['uacute'] = 611;
+ t['Tcaron'] = 611;
+ t['partialdiff'] = 494;
+ t['ydieresis'] = 556;
+ t['Nacute'] = 722;
+ t['icircumflex'] = 278;
+ t['Ecircumflex'] = 667;
+ t['adieresis'] = 556;
+ t['edieresis'] = 556;
+ t['cacute'] = 556;
+ t['nacute'] = 611;
+ t['umacron'] = 611;
+ t['Ncaron'] = 722;
+ t['Iacute'] = 278;
+ t['plusminus'] = 584;
+ t['brokenbar'] = 280;
+ t['registered'] = 737;
+ t['Gbreve'] = 778;
+ t['Idotaccent'] = 278;
+ t['summation'] = 600;
+ t['Egrave'] = 667;
+ t['racute'] = 389;
+ t['omacron'] = 611;
+ t['Zacute'] = 611;
+ t['Zcaron'] = 611;
+ t['greaterequal'] = 549;
+ t['Eth'] = 722;
+ t['Ccedilla'] = 722;
+ t['lcommaaccent'] = 278;
+ t['tcaron'] = 389;
+ t['eogonek'] = 556;
+ t['Uogonek'] = 722;
+ t['Aacute'] = 722;
+ t['Adieresis'] = 722;
+ t['egrave'] = 556;
+ t['zacute'] = 500;
+ t['iogonek'] = 278;
+ t['Oacute'] = 778;
+ t['oacute'] = 611;
+ t['amacron'] = 556;
+ t['sacute'] = 556;
+ t['idieresis'] = 278;
+ t['Ocircumflex'] = 778;
+ t['Ugrave'] = 722;
+ t['Delta'] = 612;
+ t['thorn'] = 611;
+ t['twosuperior'] = 333;
+ t['Odieresis'] = 778;
+ t['mu'] = 611;
+ t['igrave'] = 278;
+ t['ohungarumlaut'] = 611;
+ t['Eogonek'] = 667;
+ t['dcroat'] = 611;
+ t['threequarters'] = 834;
+ t['Scedilla'] = 667;
+ t['lcaron'] = 400;
+ t['Kcommaaccent'] = 722;
+ t['Lacute'] = 611;
+ t['trademark'] = 1000;
+ t['edotaccent'] = 556;
+ t['Igrave'] = 278;
+ t['Imacron'] = 278;
+ t['Lcaron'] = 611;
+ t['onehalf'] = 834;
+ t['lessequal'] = 549;
+ t['ocircumflex'] = 611;
+ t['ntilde'] = 611;
+ t['Uhungarumlaut'] = 722;
+ t['Eacute'] = 667;
+ t['emacron'] = 556;
+ t['gbreve'] = 611;
+ t['onequarter'] = 834;
+ t['Scaron'] = 667;
+ t['Scommaaccent'] = 667;
+ t['Ohungarumlaut'] = 778;
+ t['degree'] = 400;
+ t['ograve'] = 611;
+ t['Ccaron'] = 722;
+ t['ugrave'] = 611;
+ t['radical'] = 549;
+ t['Dcaron'] = 722;
+ t['rcommaaccent'] = 389;
+ t['Ntilde'] = 722;
+ t['otilde'] = 611;
+ t['Rcommaaccent'] = 722;
+ t['Lcommaaccent'] = 611;
+ t['Atilde'] = 722;
+ t['Aogonek'] = 722;
+ t['Aring'] = 722;
+ t['Otilde'] = 778;
+ t['zdotaccent'] = 500;
+ t['Ecaron'] = 667;
+ t['Iogonek'] = 278;
+ t['kcommaaccent'] = 556;
+ t['minus'] = 584;
+ t['Icircumflex'] = 278;
+ t['ncaron'] = 611;
+ t['tcommaaccent'] = 333;
+ t['logicalnot'] = 584;
+ t['odieresis'] = 611;
+ t['udieresis'] = 611;
+ t['notequal'] = 549;
+ t['gcommaaccent'] = 611;
+ t['eth'] = 611;
+ t['zcaron'] = 500;
+ t['ncommaaccent'] = 611;
+ t['onesuperior'] = 333;
+ t['imacron'] = 278;
+ t['Euro'] = 556;
+ });
+ t['Helvetica-Oblique'] = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['space'] = 278;
+ t['exclam'] = 278;
+ t['quotedbl'] = 355;
+ t['numbersign'] = 556;
+ t['dollar'] = 556;
+ t['percent'] = 889;
+ t['ampersand'] = 667;
+ t['quoteright'] = 222;
+ t['parenleft'] = 333;
+ t['parenright'] = 333;
+ t['asterisk'] = 389;
+ t['plus'] = 584;
+ t['comma'] = 278;
+ t['hyphen'] = 333;
+ t['period'] = 278;
+ t['slash'] = 278;
+ t['zero'] = 556;
+ t['one'] = 556;
+ t['two'] = 556;
+ t['three'] = 556;
+ t['four'] = 556;
+ t['five'] = 556;
+ t['six'] = 556;
+ t['seven'] = 556;
+ t['eight'] = 556;
+ t['nine'] = 556;
+ t['colon'] = 278;
+ t['semicolon'] = 278;
+ t['less'] = 584;
+ t['equal'] = 584;
+ t['greater'] = 584;
+ t['question'] = 556;
+ t['at'] = 1015;
+ t['A'] = 667;
+ t['B'] = 667;
+ t['C'] = 722;
+ t['D'] = 722;
+ t['E'] = 667;
+ t['F'] = 611;
+ t['G'] = 778;
+ t['H'] = 722;
+ t['I'] = 278;
+ t['J'] = 500;
+ t['K'] = 667;
+ t['L'] = 556;
+ t['M'] = 833;
+ t['N'] = 722;
+ t['O'] = 778;
+ t['P'] = 667;
+ t['Q'] = 778;
+ t['R'] = 722;
+ t['S'] = 667;
+ t['T'] = 611;
+ t['U'] = 722;
+ t['V'] = 667;
+ t['W'] = 944;
+ t['X'] = 667;
+ t['Y'] = 667;
+ t['Z'] = 611;
+ t['bracketleft'] = 278;
+ t['backslash'] = 278;
+ t['bracketright'] = 278;
+ t['asciicircum'] = 469;
+ t['underscore'] = 556;
+ t['quoteleft'] = 222;
+ t['a'] = 556;
+ t['b'] = 556;
+ t['c'] = 500;
+ t['d'] = 556;
+ t['e'] = 556;
+ t['f'] = 278;
+ t['g'] = 556;
+ t['h'] = 556;
+ t['i'] = 222;
+ t['j'] = 222;
+ t['k'] = 500;
+ t['l'] = 222;
+ t['m'] = 833;
+ t['n'] = 556;
+ t['o'] = 556;
+ t['p'] = 556;
+ t['q'] = 556;
+ t['r'] = 333;
+ t['s'] = 500;
+ t['t'] = 278;
+ t['u'] = 556;
+ t['v'] = 500;
+ t['w'] = 722;
+ t['x'] = 500;
+ t['y'] = 500;
+ t['z'] = 500;
+ t['braceleft'] = 334;
+ t['bar'] = 260;
+ t['braceright'] = 334;
+ t['asciitilde'] = 584;
+ t['exclamdown'] = 333;
+ t['cent'] = 556;
+ t['sterling'] = 556;
+ t['fraction'] = 167;
+ t['yen'] = 556;
+ t['florin'] = 556;
+ t['section'] = 556;
+ t['currency'] = 556;
+ t['quotesingle'] = 191;
+ t['quotedblleft'] = 333;
+ t['guillemotleft'] = 556;
+ t['guilsinglleft'] = 333;
+ t['guilsinglright'] = 333;
+ t['fi'] = 500;
+ t['fl'] = 500;
+ t['endash'] = 556;
+ t['dagger'] = 556;
+ t['daggerdbl'] = 556;
+ t['periodcentered'] = 278;
+ t['paragraph'] = 537;
+ t['bullet'] = 350;
+ t['quotesinglbase'] = 222;
+ t['quotedblbase'] = 333;
+ t['quotedblright'] = 333;
+ t['guillemotright'] = 556;
+ t['ellipsis'] = 1000;
+ t['perthousand'] = 1000;
+ t['questiondown'] = 611;
+ t['grave'] = 333;
+ t['acute'] = 333;
+ t['circumflex'] = 333;
+ t['tilde'] = 333;
+ t['macron'] = 333;
+ t['breve'] = 333;
+ t['dotaccent'] = 333;
+ t['dieresis'] = 333;
+ t['ring'] = 333;
+ t['cedilla'] = 333;
+ t['hungarumlaut'] = 333;
+ t['ogonek'] = 333;
+ t['caron'] = 333;
+ t['emdash'] = 1000;
+ t['AE'] = 1000;
+ t['ordfeminine'] = 370;
+ t['Lslash'] = 556;
+ t['Oslash'] = 778;
+ t['OE'] = 1000;
+ t['ordmasculine'] = 365;
+ t['ae'] = 889;
+ t['dotlessi'] = 278;
+ t['lslash'] = 222;
+ t['oslash'] = 611;
+ t['oe'] = 944;
+ t['germandbls'] = 611;
+ t['Idieresis'] = 278;
+ t['eacute'] = 556;
+ t['abreve'] = 556;
+ t['uhungarumlaut'] = 556;
+ t['ecaron'] = 556;
+ t['Ydieresis'] = 667;
+ t['divide'] = 584;
+ t['Yacute'] = 667;
+ t['Acircumflex'] = 667;
+ t['aacute'] = 556;
+ t['Ucircumflex'] = 722;
+ t['yacute'] = 500;
+ t['scommaaccent'] = 500;
+ t['ecircumflex'] = 556;
+ t['Uring'] = 722;
+ t['Udieresis'] = 722;
+ t['aogonek'] = 556;
+ t['Uacute'] = 722;
+ t['uogonek'] = 556;
+ t['Edieresis'] = 667;
+ t['Dcroat'] = 722;
+ t['commaaccent'] = 250;
+ t['copyright'] = 737;
+ t['Emacron'] = 667;
+ t['ccaron'] = 500;
+ t['aring'] = 556;
+ t['Ncommaaccent'] = 722;
+ t['lacute'] = 222;
+ t['agrave'] = 556;
+ t['Tcommaaccent'] = 611;
+ t['Cacute'] = 722;
+ t['atilde'] = 556;
+ t['Edotaccent'] = 667;
+ t['scaron'] = 500;
+ t['scedilla'] = 500;
+ t['iacute'] = 278;
+ t['lozenge'] = 471;
+ t['Rcaron'] = 722;
+ t['Gcommaaccent'] = 778;
+ t['ucircumflex'] = 556;
+ t['acircumflex'] = 556;
+ t['Amacron'] = 667;
+ t['rcaron'] = 333;
+ t['ccedilla'] = 500;
+ t['Zdotaccent'] = 611;
+ t['Thorn'] = 667;
+ t['Omacron'] = 778;
+ t['Racute'] = 722;
+ t['Sacute'] = 667;
+ t['dcaron'] = 643;
+ t['Umacron'] = 722;
+ t['uring'] = 556;
+ t['threesuperior'] = 333;
+ t['Ograve'] = 778;
+ t['Agrave'] = 667;
+ t['Abreve'] = 667;
+ t['multiply'] = 584;
+ t['uacute'] = 556;
+ t['Tcaron'] = 611;
+ t['partialdiff'] = 476;
+ t['ydieresis'] = 500;
+ t['Nacute'] = 722;
+ t['icircumflex'] = 278;
+ t['Ecircumflex'] = 667;
+ t['adieresis'] = 556;
+ t['edieresis'] = 556;
+ t['cacute'] = 500;
+ t['nacute'] = 556;
+ t['umacron'] = 556;
+ t['Ncaron'] = 722;
+ t['Iacute'] = 278;
+ t['plusminus'] = 584;
+ t['brokenbar'] = 260;
+ t['registered'] = 737;
+ t['Gbreve'] = 778;
+ t['Idotaccent'] = 278;
+ t['summation'] = 600;
+ t['Egrave'] = 667;
+ t['racute'] = 333;
+ t['omacron'] = 556;
+ t['Zacute'] = 611;
+ t['Zcaron'] = 611;
+ t['greaterequal'] = 549;
+ t['Eth'] = 722;
+ t['Ccedilla'] = 722;
+ t['lcommaaccent'] = 222;
+ t['tcaron'] = 317;
+ t['eogonek'] = 556;
+ t['Uogonek'] = 722;
+ t['Aacute'] = 667;
+ t['Adieresis'] = 667;
+ t['egrave'] = 556;
+ t['zacute'] = 500;
+ t['iogonek'] = 222;
+ t['Oacute'] = 778;
+ t['oacute'] = 556;
+ t['amacron'] = 556;
+ t['sacute'] = 500;
+ t['idieresis'] = 278;
+ t['Ocircumflex'] = 778;
+ t['Ugrave'] = 722;
+ t['Delta'] = 612;
+ t['thorn'] = 556;
+ t['twosuperior'] = 333;
+ t['Odieresis'] = 778;
+ t['mu'] = 556;
+ t['igrave'] = 278;
+ t['ohungarumlaut'] = 556;
+ t['Eogonek'] = 667;
+ t['dcroat'] = 556;
+ t['threequarters'] = 834;
+ t['Scedilla'] = 667;
+ t['lcaron'] = 299;
+ t['Kcommaaccent'] = 667;
+ t['Lacute'] = 556;
+ t['trademark'] = 1000;
+ t['edotaccent'] = 556;
+ t['Igrave'] = 278;
+ t['Imacron'] = 278;
+ t['Lcaron'] = 556;
+ t['onehalf'] = 834;
+ t['lessequal'] = 549;
+ t['ocircumflex'] = 556;
+ t['ntilde'] = 556;
+ t['Uhungarumlaut'] = 722;
+ t['Eacute'] = 667;
+ t['emacron'] = 556;
+ t['gbreve'] = 556;
+ t['onequarter'] = 834;
+ t['Scaron'] = 667;
+ t['Scommaaccent'] = 667;
+ t['Ohungarumlaut'] = 778;
+ t['degree'] = 400;
+ t['ograve'] = 556;
+ t['Ccaron'] = 722;
+ t['ugrave'] = 556;
+ t['radical'] = 453;
+ t['Dcaron'] = 722;
+ t['rcommaaccent'] = 333;
+ t['Ntilde'] = 722;
+ t['otilde'] = 556;
+ t['Rcommaaccent'] = 722;
+ t['Lcommaaccent'] = 556;
+ t['Atilde'] = 667;
+ t['Aogonek'] = 667;
+ t['Aring'] = 667;
+ t['Otilde'] = 778;
+ t['zdotaccent'] = 500;
+ t['Ecaron'] = 667;
+ t['Iogonek'] = 278;
+ t['kcommaaccent'] = 500;
+ t['minus'] = 584;
+ t['Icircumflex'] = 278;
+ t['ncaron'] = 556;
+ t['tcommaaccent'] = 278;
+ t['logicalnot'] = 584;
+ t['odieresis'] = 556;
+ t['udieresis'] = 556;
+ t['notequal'] = 549;
+ t['gcommaaccent'] = 556;
+ t['eth'] = 556;
+ t['zcaron'] = 500;
+ t['ncommaaccent'] = 556;
+ t['onesuperior'] = 333;
+ t['imacron'] = 278;
+ t['Euro'] = 556;
+ });
+ t['Symbol'] = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['space'] = 250;
+ t['exclam'] = 333;
+ t['universal'] = 713;
+ t['numbersign'] = 500;
+ t['existential'] = 549;
+ t['percent'] = 833;
+ t['ampersand'] = 778;
+ t['suchthat'] = 439;
+ t['parenleft'] = 333;
+ t['parenright'] = 333;
+ t['asteriskmath'] = 500;
+ t['plus'] = 549;
+ t['comma'] = 250;
+ t['minus'] = 549;
+ t['period'] = 250;
+ t['slash'] = 278;
+ t['zero'] = 500;
+ t['one'] = 500;
+ t['two'] = 500;
+ t['three'] = 500;
+ t['four'] = 500;
+ t['five'] = 500;
+ t['six'] = 500;
+ t['seven'] = 500;
+ t['eight'] = 500;
+ t['nine'] = 500;
+ t['colon'] = 278;
+ t['semicolon'] = 278;
+ t['less'] = 549;
+ t['equal'] = 549;
+ t['greater'] = 549;
+ t['question'] = 444;
+ t['congruent'] = 549;
+ t['Alpha'] = 722;
+ t['Beta'] = 667;
+ t['Chi'] = 722;
+ t['Delta'] = 612;
+ t['Epsilon'] = 611;
+ t['Phi'] = 763;
+ t['Gamma'] = 603;
+ t['Eta'] = 722;
+ t['Iota'] = 333;
+ t['theta1'] = 631;
+ t['Kappa'] = 722;
+ t['Lambda'] = 686;
+ t['Mu'] = 889;
+ t['Nu'] = 722;
+ t['Omicron'] = 722;
+ t['Pi'] = 768;
+ t['Theta'] = 741;
+ t['Rho'] = 556;
+ t['Sigma'] = 592;
+ t['Tau'] = 611;
+ t['Upsilon'] = 690;
+ t['sigma1'] = 439;
+ t['Omega'] = 768;
+ t['Xi'] = 645;
+ t['Psi'] = 795;
+ t['Zeta'] = 611;
+ t['bracketleft'] = 333;
+ t['therefore'] = 863;
+ t['bracketright'] = 333;
+ t['perpendicular'] = 658;
+ t['underscore'] = 500;
+ t['radicalex'] = 500;
+ t['alpha'] = 631;
+ t['beta'] = 549;
+ t['chi'] = 549;
+ t['delta'] = 494;
+ t['epsilon'] = 439;
+ t['phi'] = 521;
+ t['gamma'] = 411;
+ t['eta'] = 603;
+ t['iota'] = 329;
+ t['phi1'] = 603;
+ t['kappa'] = 549;
+ t['lambda'] = 549;
+ t['mu'] = 576;
+ t['nu'] = 521;
+ t['omicron'] = 549;
+ t['pi'] = 549;
+ t['theta'] = 521;
+ t['rho'] = 549;
+ t['sigma'] = 603;
+ t['tau'] = 439;
+ t['upsilon'] = 576;
+ t['omega1'] = 713;
+ t['omega'] = 686;
+ t['xi'] = 493;
+ t['psi'] = 686;
+ t['zeta'] = 494;
+ t['braceleft'] = 480;
+ t['bar'] = 200;
+ t['braceright'] = 480;
+ t['similar'] = 549;
+ t['Euro'] = 750;
+ t['Upsilon1'] = 620;
+ t['minute'] = 247;
+ t['lessequal'] = 549;
+ t['fraction'] = 167;
+ t['infinity'] = 713;
+ t['florin'] = 500;
+ t['club'] = 753;
+ t['diamond'] = 753;
+ t['heart'] = 753;
+ t['spade'] = 753;
+ t['arrowboth'] = 1042;
+ t['arrowleft'] = 987;
+ t['arrowup'] = 603;
+ t['arrowright'] = 987;
+ t['arrowdown'] = 603;
+ t['degree'] = 400;
+ t['plusminus'] = 549;
+ t['second'] = 411;
+ t['greaterequal'] = 549;
+ t['multiply'] = 549;
+ t['proportional'] = 713;
+ t['partialdiff'] = 494;
+ t['bullet'] = 460;
+ t['divide'] = 549;
+ t['notequal'] = 549;
+ t['equivalence'] = 549;
+ t['approxequal'] = 549;
+ t['ellipsis'] = 1000;
+ t['arrowvertex'] = 603;
+ t['arrowhorizex'] = 1000;
+ t['carriagereturn'] = 658;
+ t['aleph'] = 823;
+ t['Ifraktur'] = 686;
+ t['Rfraktur'] = 795;
+ t['weierstrass'] = 987;
+ t['circlemultiply'] = 768;
+ t['circleplus'] = 768;
+ t['emptyset'] = 823;
+ t['intersection'] = 768;
+ t['union'] = 768;
+ t['propersuperset'] = 713;
+ t['reflexsuperset'] = 713;
+ t['notsubset'] = 713;
+ t['propersubset'] = 713;
+ t['reflexsubset'] = 713;
+ t['element'] = 713;
+ t['notelement'] = 713;
+ t['angle'] = 768;
+ t['gradient'] = 713;
+ t['registerserif'] = 790;
+ t['copyrightserif'] = 790;
+ t['trademarkserif'] = 890;
+ t['product'] = 823;
+ t['radical'] = 549;
+ t['dotmath'] = 250;
+ t['logicalnot'] = 713;
+ t['logicaland'] = 603;
+ t['logicalor'] = 603;
+ t['arrowdblboth'] = 1042;
+ t['arrowdblleft'] = 987;
+ t['arrowdblup'] = 603;
+ t['arrowdblright'] = 987;
+ t['arrowdbldown'] = 603;
+ t['lozenge'] = 494;
+ t['angleleft'] = 329;
+ t['registersans'] = 790;
+ t['copyrightsans'] = 790;
+ t['trademarksans'] = 786;
+ t['summation'] = 713;
+ t['parenlefttp'] = 384;
+ t['parenleftex'] = 384;
+ t['parenleftbt'] = 384;
+ t['bracketlefttp'] = 384;
+ t['bracketleftex'] = 384;
+ t['bracketleftbt'] = 384;
+ t['bracelefttp'] = 494;
+ t['braceleftmid'] = 494;
+ t['braceleftbt'] = 494;
+ t['braceex'] = 494;
+ t['angleright'] = 329;
+ t['integral'] = 274;
+ t['integraltp'] = 686;
+ t['integralex'] = 686;
+ t['integralbt'] = 686;
+ t['parenrighttp'] = 384;
+ t['parenrightex'] = 384;
+ t['parenrightbt'] = 384;
+ t['bracketrighttp'] = 384;
+ t['bracketrightex'] = 384;
+ t['bracketrightbt'] = 384;
+ t['bracerighttp'] = 494;
+ t['bracerightmid'] = 494;
+ t['bracerightbt'] = 494;
+ t['apple'] = 790;
+ });
+ t['Times-Roman'] = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['space'] = 250;
+ t['exclam'] = 333;
+ t['quotedbl'] = 408;
+ t['numbersign'] = 500;
+ t['dollar'] = 500;
+ t['percent'] = 833;
+ t['ampersand'] = 778;
+ t['quoteright'] = 333;
+ t['parenleft'] = 333;
+ t['parenright'] = 333;
+ t['asterisk'] = 500;
+ t['plus'] = 564;
+ t['comma'] = 250;
+ t['hyphen'] = 333;
+ t['period'] = 250;
+ t['slash'] = 278;
+ t['zero'] = 500;
+ t['one'] = 500;
+ t['two'] = 500;
+ t['three'] = 500;
+ t['four'] = 500;
+ t['five'] = 500;
+ t['six'] = 500;
+ t['seven'] = 500;
+ t['eight'] = 500;
+ t['nine'] = 500;
+ t['colon'] = 278;
+ t['semicolon'] = 278;
+ t['less'] = 564;
+ t['equal'] = 564;
+ t['greater'] = 564;
+ t['question'] = 444;
+ t['at'] = 921;
+ t['A'] = 722;
+ t['B'] = 667;
+ t['C'] = 667;
+ t['D'] = 722;
+ t['E'] = 611;
+ t['F'] = 556;
+ t['G'] = 722;
+ t['H'] = 722;
+ t['I'] = 333;
+ t['J'] = 389;
+ t['K'] = 722;
+ t['L'] = 611;
+ t['M'] = 889;
+ t['N'] = 722;
+ t['O'] = 722;
+ t['P'] = 556;
+ t['Q'] = 722;
+ t['R'] = 667;
+ t['S'] = 556;
+ t['T'] = 611;
+ t['U'] = 722;
+ t['V'] = 722;
+ t['W'] = 944;
+ t['X'] = 722;
+ t['Y'] = 722;
+ t['Z'] = 611;
+ t['bracketleft'] = 333;
+ t['backslash'] = 278;
+ t['bracketright'] = 333;
+ t['asciicircum'] = 469;
+ t['underscore'] = 500;
+ t['quoteleft'] = 333;
+ t['a'] = 444;
+ t['b'] = 500;
+ t['c'] = 444;
+ t['d'] = 500;
+ t['e'] = 444;
+ t['f'] = 333;
+ t['g'] = 500;
+ t['h'] = 500;
+ t['i'] = 278;
+ t['j'] = 278;
+ t['k'] = 500;
+ t['l'] = 278;
+ t['m'] = 778;
+ t['n'] = 500;
+ t['o'] = 500;
+ t['p'] = 500;
+ t['q'] = 500;
+ t['r'] = 333;
+ t['s'] = 389;
+ t['t'] = 278;
+ t['u'] = 500;
+ t['v'] = 500;
+ t['w'] = 722;
+ t['x'] = 500;
+ t['y'] = 500;
+ t['z'] = 444;
+ t['braceleft'] = 480;
+ t['bar'] = 200;
+ t['braceright'] = 480;
+ t['asciitilde'] = 541;
+ t['exclamdown'] = 333;
+ t['cent'] = 500;
+ t['sterling'] = 500;
+ t['fraction'] = 167;
+ t['yen'] = 500;
+ t['florin'] = 500;
+ t['section'] = 500;
+ t['currency'] = 500;
+ t['quotesingle'] = 180;
+ t['quotedblleft'] = 444;
+ t['guillemotleft'] = 500;
+ t['guilsinglleft'] = 333;
+ t['guilsinglright'] = 333;
+ t['fi'] = 556;
+ t['fl'] = 556;
+ t['endash'] = 500;
+ t['dagger'] = 500;
+ t['daggerdbl'] = 500;
+ t['periodcentered'] = 250;
+ t['paragraph'] = 453;
+ t['bullet'] = 350;
+ t['quotesinglbase'] = 333;
+ t['quotedblbase'] = 444;
+ t['quotedblright'] = 444;
+ t['guillemotright'] = 500;
+ t['ellipsis'] = 1000;
+ t['perthousand'] = 1000;
+ t['questiondown'] = 444;
+ t['grave'] = 333;
+ t['acute'] = 333;
+ t['circumflex'] = 333;
+ t['tilde'] = 333;
+ t['macron'] = 333;
+ t['breve'] = 333;
+ t['dotaccent'] = 333;
+ t['dieresis'] = 333;
+ t['ring'] = 333;
+ t['cedilla'] = 333;
+ t['hungarumlaut'] = 333;
+ t['ogonek'] = 333;
+ t['caron'] = 333;
+ t['emdash'] = 1000;
+ t['AE'] = 889;
+ t['ordfeminine'] = 276;
+ t['Lslash'] = 611;
+ t['Oslash'] = 722;
+ t['OE'] = 889;
+ t['ordmasculine'] = 310;
+ t['ae'] = 667;
+ t['dotlessi'] = 278;
+ t['lslash'] = 278;
+ t['oslash'] = 500;
+ t['oe'] = 722;
+ t['germandbls'] = 500;
+ t['Idieresis'] = 333;
+ t['eacute'] = 444;
+ t['abreve'] = 444;
+ t['uhungarumlaut'] = 500;
+ t['ecaron'] = 444;
+ t['Ydieresis'] = 722;
+ t['divide'] = 564;
+ t['Yacute'] = 722;
+ t['Acircumflex'] = 722;
+ t['aacute'] = 444;
+ t['Ucircumflex'] = 722;
+ t['yacute'] = 500;
+ t['scommaaccent'] = 389;
+ t['ecircumflex'] = 444;
+ t['Uring'] = 722;
+ t['Udieresis'] = 722;
+ t['aogonek'] = 444;
+ t['Uacute'] = 722;
+ t['uogonek'] = 500;
+ t['Edieresis'] = 611;
+ t['Dcroat'] = 722;
+ t['commaaccent'] = 250;
+ t['copyright'] = 760;
+ t['Emacron'] = 611;
+ t['ccaron'] = 444;
+ t['aring'] = 444;
+ t['Ncommaaccent'] = 722;
+ t['lacute'] = 278;
+ t['agrave'] = 444;
+ t['Tcommaaccent'] = 611;
+ t['Cacute'] = 667;
+ t['atilde'] = 444;
+ t['Edotaccent'] = 611;
+ t['scaron'] = 389;
+ t['scedilla'] = 389;
+ t['iacute'] = 278;
+ t['lozenge'] = 471;
+ t['Rcaron'] = 667;
+ t['Gcommaaccent'] = 722;
+ t['ucircumflex'] = 500;
+ t['acircumflex'] = 444;
+ t['Amacron'] = 722;
+ t['rcaron'] = 333;
+ t['ccedilla'] = 444;
+ t['Zdotaccent'] = 611;
+ t['Thorn'] = 556;
+ t['Omacron'] = 722;
+ t['Racute'] = 667;
+ t['Sacute'] = 556;
+ t['dcaron'] = 588;
+ t['Umacron'] = 722;
+ t['uring'] = 500;
+ t['threesuperior'] = 300;
+ t['Ograve'] = 722;
+ t['Agrave'] = 722;
+ t['Abreve'] = 722;
+ t['multiply'] = 564;
+ t['uacute'] = 500;
+ t['Tcaron'] = 611;
+ t['partialdiff'] = 476;
+ t['ydieresis'] = 500;
+ t['Nacute'] = 722;
+ t['icircumflex'] = 278;
+ t['Ecircumflex'] = 611;
+ t['adieresis'] = 444;
+ t['edieresis'] = 444;
+ t['cacute'] = 444;
+ t['nacute'] = 500;
+ t['umacron'] = 500;
+ t['Ncaron'] = 722;
+ t['Iacute'] = 333;
+ t['plusminus'] = 564;
+ t['brokenbar'] = 200;
+ t['registered'] = 760;
+ t['Gbreve'] = 722;
+ t['Idotaccent'] = 333;
+ t['summation'] = 600;
+ t['Egrave'] = 611;
+ t['racute'] = 333;
+ t['omacron'] = 500;
+ t['Zacute'] = 611;
+ t['Zcaron'] = 611;
+ t['greaterequal'] = 549;
+ t['Eth'] = 722;
+ t['Ccedilla'] = 667;
+ t['lcommaaccent'] = 278;
+ t['tcaron'] = 326;
+ t['eogonek'] = 444;
+ t['Uogonek'] = 722;
+ t['Aacute'] = 722;
+ t['Adieresis'] = 722;
+ t['egrave'] = 444;
+ t['zacute'] = 444;
+ t['iogonek'] = 278;
+ t['Oacute'] = 722;
+ t['oacute'] = 500;
+ t['amacron'] = 444;
+ t['sacute'] = 389;
+ t['idieresis'] = 278;
+ t['Ocircumflex'] = 722;
+ t['Ugrave'] = 722;
+ t['Delta'] = 612;
+ t['thorn'] = 500;
+ t['twosuperior'] = 300;
+ t['Odieresis'] = 722;
+ t['mu'] = 500;
+ t['igrave'] = 278;
+ t['ohungarumlaut'] = 500;
+ t['Eogonek'] = 611;
+ t['dcroat'] = 500;
+ t['threequarters'] = 750;
+ t['Scedilla'] = 556;
+ t['lcaron'] = 344;
+ t['Kcommaaccent'] = 722;
+ t['Lacute'] = 611;
+ t['trademark'] = 980;
+ t['edotaccent'] = 444;
+ t['Igrave'] = 333;
+ t['Imacron'] = 333;
+ t['Lcaron'] = 611;
+ t['onehalf'] = 750;
+ t['lessequal'] = 549;
+ t['ocircumflex'] = 500;
+ t['ntilde'] = 500;
+ t['Uhungarumlaut'] = 722;
+ t['Eacute'] = 611;
+ t['emacron'] = 444;
+ t['gbreve'] = 500;
+ t['onequarter'] = 750;
+ t['Scaron'] = 556;
+ t['Scommaaccent'] = 556;
+ t['Ohungarumlaut'] = 722;
+ t['degree'] = 400;
+ t['ograve'] = 500;
+ t['Ccaron'] = 667;
+ t['ugrave'] = 500;
+ t['radical'] = 453;
+ t['Dcaron'] = 722;
+ t['rcommaaccent'] = 333;
+ t['Ntilde'] = 722;
+ t['otilde'] = 500;
+ t['Rcommaaccent'] = 667;
+ t['Lcommaaccent'] = 611;
+ t['Atilde'] = 722;
+ t['Aogonek'] = 722;
+ t['Aring'] = 722;
+ t['Otilde'] = 722;
+ t['zdotaccent'] = 444;
+ t['Ecaron'] = 611;
+ t['Iogonek'] = 333;
+ t['kcommaaccent'] = 500;
+ t['minus'] = 564;
+ t['Icircumflex'] = 333;
+ t['ncaron'] = 500;
+ t['tcommaaccent'] = 278;
+ t['logicalnot'] = 564;
+ t['odieresis'] = 500;
+ t['udieresis'] = 500;
+ t['notequal'] = 549;
+ t['gcommaaccent'] = 500;
+ t['eth'] = 500;
+ t['zcaron'] = 444;
+ t['ncommaaccent'] = 500;
+ t['onesuperior'] = 300;
+ t['imacron'] = 278;
+ t['Euro'] = 500;
+ });
+ t['Times-Bold'] = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['space'] = 250;
+ t['exclam'] = 333;
+ t['quotedbl'] = 555;
+ t['numbersign'] = 500;
+ t['dollar'] = 500;
+ t['percent'] = 1000;
+ t['ampersand'] = 833;
+ t['quoteright'] = 333;
+ t['parenleft'] = 333;
+ t['parenright'] = 333;
+ t['asterisk'] = 500;
+ t['plus'] = 570;
+ t['comma'] = 250;
+ t['hyphen'] = 333;
+ t['period'] = 250;
+ t['slash'] = 278;
+ t['zero'] = 500;
+ t['one'] = 500;
+ t['two'] = 500;
+ t['three'] = 500;
+ t['four'] = 500;
+ t['five'] = 500;
+ t['six'] = 500;
+ t['seven'] = 500;
+ t['eight'] = 500;
+ t['nine'] = 500;
+ t['colon'] = 333;
+ t['semicolon'] = 333;
+ t['less'] = 570;
+ t['equal'] = 570;
+ t['greater'] = 570;
+ t['question'] = 500;
+ t['at'] = 930;
+ t['A'] = 722;
+ t['B'] = 667;
+ t['C'] = 722;
+ t['D'] = 722;
+ t['E'] = 667;
+ t['F'] = 611;
+ t['G'] = 778;
+ t['H'] = 778;
+ t['I'] = 389;
+ t['J'] = 500;
+ t['K'] = 778;
+ t['L'] = 667;
+ t['M'] = 944;
+ t['N'] = 722;
+ t['O'] = 778;
+ t['P'] = 611;
+ t['Q'] = 778;
+ t['R'] = 722;
+ t['S'] = 556;
+ t['T'] = 667;
+ t['U'] = 722;
+ t['V'] = 722;
+ t['W'] = 1000;
+ t['X'] = 722;
+ t['Y'] = 722;
+ t['Z'] = 667;
+ t['bracketleft'] = 333;
+ t['backslash'] = 278;
+ t['bracketright'] = 333;
+ t['asciicircum'] = 581;
+ t['underscore'] = 500;
+ t['quoteleft'] = 333;
+ t['a'] = 500;
+ t['b'] = 556;
+ t['c'] = 444;
+ t['d'] = 556;
+ t['e'] = 444;
+ t['f'] = 333;
+ t['g'] = 500;
+ t['h'] = 556;
+ t['i'] = 278;
+ t['j'] = 333;
+ t['k'] = 556;
+ t['l'] = 278;
+ t['m'] = 833;
+ t['n'] = 556;
+ t['o'] = 500;
+ t['p'] = 556;
+ t['q'] = 556;
+ t['r'] = 444;
+ t['s'] = 389;
+ t['t'] = 333;
+ t['u'] = 556;
+ t['v'] = 500;
+ t['w'] = 722;
+ t['x'] = 500;
+ t['y'] = 500;
+ t['z'] = 444;
+ t['braceleft'] = 394;
+ t['bar'] = 220;
+ t['braceright'] = 394;
+ t['asciitilde'] = 520;
+ t['exclamdown'] = 333;
+ t['cent'] = 500;
+ t['sterling'] = 500;
+ t['fraction'] = 167;
+ t['yen'] = 500;
+ t['florin'] = 500;
+ t['section'] = 500;
+ t['currency'] = 500;
+ t['quotesingle'] = 278;
+ t['quotedblleft'] = 500;
+ t['guillemotleft'] = 500;
+ t['guilsinglleft'] = 333;
+ t['guilsinglright'] = 333;
+ t['fi'] = 556;
+ t['fl'] = 556;
+ t['endash'] = 500;
+ t['dagger'] = 500;
+ t['daggerdbl'] = 500;
+ t['periodcentered'] = 250;
+ t['paragraph'] = 540;
+ t['bullet'] = 350;
+ t['quotesinglbase'] = 333;
+ t['quotedblbase'] = 500;
+ t['quotedblright'] = 500;
+ t['guillemotright'] = 500;
+ t['ellipsis'] = 1000;
+ t['perthousand'] = 1000;
+ t['questiondown'] = 500;
+ t['grave'] = 333;
+ t['acute'] = 333;
+ t['circumflex'] = 333;
+ t['tilde'] = 333;
+ t['macron'] = 333;
+ t['breve'] = 333;
+ t['dotaccent'] = 333;
+ t['dieresis'] = 333;
+ t['ring'] = 333;
+ t['cedilla'] = 333;
+ t['hungarumlaut'] = 333;
+ t['ogonek'] = 333;
+ t['caron'] = 333;
+ t['emdash'] = 1000;
+ t['AE'] = 1000;
+ t['ordfeminine'] = 300;
+ t['Lslash'] = 667;
+ t['Oslash'] = 778;
+ t['OE'] = 1000;
+ t['ordmasculine'] = 330;
+ t['ae'] = 722;
+ t['dotlessi'] = 278;
+ t['lslash'] = 278;
+ t['oslash'] = 500;
+ t['oe'] = 722;
+ t['germandbls'] = 556;
+ t['Idieresis'] = 389;
+ t['eacute'] = 444;
+ t['abreve'] = 500;
+ t['uhungarumlaut'] = 556;
+ t['ecaron'] = 444;
+ t['Ydieresis'] = 722;
+ t['divide'] = 570;
+ t['Yacute'] = 722;
+ t['Acircumflex'] = 722;
+ t['aacute'] = 500;
+ t['Ucircumflex'] = 722;
+ t['yacute'] = 500;
+ t['scommaaccent'] = 389;
+ t['ecircumflex'] = 444;
+ t['Uring'] = 722;
+ t['Udieresis'] = 722;
+ t['aogonek'] = 500;
+ t['Uacute'] = 722;
+ t['uogonek'] = 556;
+ t['Edieresis'] = 667;
+ t['Dcroat'] = 722;
+ t['commaaccent'] = 250;
+ t['copyright'] = 747;
+ t['Emacron'] = 667;
+ t['ccaron'] = 444;
+ t['aring'] = 500;
+ t['Ncommaaccent'] = 722;
+ t['lacute'] = 278;
+ t['agrave'] = 500;
+ t['Tcommaaccent'] = 667;
+ t['Cacute'] = 722;
+ t['atilde'] = 500;
+ t['Edotaccent'] = 667;
+ t['scaron'] = 389;
+ t['scedilla'] = 389;
+ t['iacute'] = 278;
+ t['lozenge'] = 494;
+ t['Rcaron'] = 722;
+ t['Gcommaaccent'] = 778;
+ t['ucircumflex'] = 556;
+ t['acircumflex'] = 500;
+ t['Amacron'] = 722;
+ t['rcaron'] = 444;
+ t['ccedilla'] = 444;
+ t['Zdotaccent'] = 667;
+ t['Thorn'] = 611;
+ t['Omacron'] = 778;
+ t['Racute'] = 722;
+ t['Sacute'] = 556;
+ t['dcaron'] = 672;
+ t['Umacron'] = 722;
+ t['uring'] = 556;
+ t['threesuperior'] = 300;
+ t['Ograve'] = 778;
+ t['Agrave'] = 722;
+ t['Abreve'] = 722;
+ t['multiply'] = 570;
+ t['uacute'] = 556;
+ t['Tcaron'] = 667;
+ t['partialdiff'] = 494;
+ t['ydieresis'] = 500;
+ t['Nacute'] = 722;
+ t['icircumflex'] = 278;
+ t['Ecircumflex'] = 667;
+ t['adieresis'] = 500;
+ t['edieresis'] = 444;
+ t['cacute'] = 444;
+ t['nacute'] = 556;
+ t['umacron'] = 556;
+ t['Ncaron'] = 722;
+ t['Iacute'] = 389;
+ t['plusminus'] = 570;
+ t['brokenbar'] = 220;
+ t['registered'] = 747;
+ t['Gbreve'] = 778;
+ t['Idotaccent'] = 389;
+ t['summation'] = 600;
+ t['Egrave'] = 667;
+ t['racute'] = 444;
+ t['omacron'] = 500;
+ t['Zacute'] = 667;
+ t['Zcaron'] = 667;
+ t['greaterequal'] = 549;
+ t['Eth'] = 722;
+ t['Ccedilla'] = 722;
+ t['lcommaaccent'] = 278;
+ t['tcaron'] = 416;
+ t['eogonek'] = 444;
+ t['Uogonek'] = 722;
+ t['Aacute'] = 722;
+ t['Adieresis'] = 722;
+ t['egrave'] = 444;
+ t['zacute'] = 444;
+ t['iogonek'] = 278;
+ t['Oacute'] = 778;
+ t['oacute'] = 500;
+ t['amacron'] = 500;
+ t['sacute'] = 389;
+ t['idieresis'] = 278;
+ t['Ocircumflex'] = 778;
+ t['Ugrave'] = 722;
+ t['Delta'] = 612;
+ t['thorn'] = 556;
+ t['twosuperior'] = 300;
+ t['Odieresis'] = 778;
+ t['mu'] = 556;
+ t['igrave'] = 278;
+ t['ohungarumlaut'] = 500;
+ t['Eogonek'] = 667;
+ t['dcroat'] = 556;
+ t['threequarters'] = 750;
+ t['Scedilla'] = 556;
+ t['lcaron'] = 394;
+ t['Kcommaaccent'] = 778;
+ t['Lacute'] = 667;
+ t['trademark'] = 1000;
+ t['edotaccent'] = 444;
+ t['Igrave'] = 389;
+ t['Imacron'] = 389;
+ t['Lcaron'] = 667;
+ t['onehalf'] = 750;
+ t['lessequal'] = 549;
+ t['ocircumflex'] = 500;
+ t['ntilde'] = 556;
+ t['Uhungarumlaut'] = 722;
+ t['Eacute'] = 667;
+ t['emacron'] = 444;
+ t['gbreve'] = 500;
+ t['onequarter'] = 750;
+ t['Scaron'] = 556;
+ t['Scommaaccent'] = 556;
+ t['Ohungarumlaut'] = 778;
+ t['degree'] = 400;
+ t['ograve'] = 500;
+ t['Ccaron'] = 722;
+ t['ugrave'] = 556;
+ t['radical'] = 549;
+ t['Dcaron'] = 722;
+ t['rcommaaccent'] = 444;
+ t['Ntilde'] = 722;
+ t['otilde'] = 500;
+ t['Rcommaaccent'] = 722;
+ t['Lcommaaccent'] = 667;
+ t['Atilde'] = 722;
+ t['Aogonek'] = 722;
+ t['Aring'] = 722;
+ t['Otilde'] = 778;
+ t['zdotaccent'] = 444;
+ t['Ecaron'] = 667;
+ t['Iogonek'] = 389;
+ t['kcommaaccent'] = 556;
+ t['minus'] = 570;
+ t['Icircumflex'] = 389;
+ t['ncaron'] = 556;
+ t['tcommaaccent'] = 333;
+ t['logicalnot'] = 570;
+ t['odieresis'] = 500;
+ t['udieresis'] = 556;
+ t['notequal'] = 549;
+ t['gcommaaccent'] = 500;
+ t['eth'] = 500;
+ t['zcaron'] = 444;
+ t['ncommaaccent'] = 556;
+ t['onesuperior'] = 300;
+ t['imacron'] = 278;
+ t['Euro'] = 500;
+ });
+ t['Times-BoldItalic'] = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['space'] = 250;
+ t['exclam'] = 389;
+ t['quotedbl'] = 555;
+ t['numbersign'] = 500;
+ t['dollar'] = 500;
+ t['percent'] = 833;
+ t['ampersand'] = 778;
+ t['quoteright'] = 333;
+ t['parenleft'] = 333;
+ t['parenright'] = 333;
+ t['asterisk'] = 500;
+ t['plus'] = 570;
+ t['comma'] = 250;
+ t['hyphen'] = 333;
+ t['period'] = 250;
+ t['slash'] = 278;
+ t['zero'] = 500;
+ t['one'] = 500;
+ t['two'] = 500;
+ t['three'] = 500;
+ t['four'] = 500;
+ t['five'] = 500;
+ t['six'] = 500;
+ t['seven'] = 500;
+ t['eight'] = 500;
+ t['nine'] = 500;
+ t['colon'] = 333;
+ t['semicolon'] = 333;
+ t['less'] = 570;
+ t['equal'] = 570;
+ t['greater'] = 570;
+ t['question'] = 500;
+ t['at'] = 832;
+ t['A'] = 667;
+ t['B'] = 667;
+ t['C'] = 667;
+ t['D'] = 722;
+ t['E'] = 667;
+ t['F'] = 667;
+ t['G'] = 722;
+ t['H'] = 778;
+ t['I'] = 389;
+ t['J'] = 500;
+ t['K'] = 667;
+ t['L'] = 611;
+ t['M'] = 889;
+ t['N'] = 722;
+ t['O'] = 722;
+ t['P'] = 611;
+ t['Q'] = 722;
+ t['R'] = 667;
+ t['S'] = 556;
+ t['T'] = 611;
+ t['U'] = 722;
+ t['V'] = 667;
+ t['W'] = 889;
+ t['X'] = 667;
+ t['Y'] = 611;
+ t['Z'] = 611;
+ t['bracketleft'] = 333;
+ t['backslash'] = 278;
+ t['bracketright'] = 333;
+ t['asciicircum'] = 570;
+ t['underscore'] = 500;
+ t['quoteleft'] = 333;
+ t['a'] = 500;
+ t['b'] = 500;
+ t['c'] = 444;
+ t['d'] = 500;
+ t['e'] = 444;
+ t['f'] = 333;
+ t['g'] = 500;
+ t['h'] = 556;
+ t['i'] = 278;
+ t['j'] = 278;
+ t['k'] = 500;
+ t['l'] = 278;
+ t['m'] = 778;
+ t['n'] = 556;
+ t['o'] = 500;
+ t['p'] = 500;
+ t['q'] = 500;
+ t['r'] = 389;
+ t['s'] = 389;
+ t['t'] = 278;
+ t['u'] = 556;
+ t['v'] = 444;
+ t['w'] = 667;
+ t['x'] = 500;
+ t['y'] = 444;
+ t['z'] = 389;
+ t['braceleft'] = 348;
+ t['bar'] = 220;
+ t['braceright'] = 348;
+ t['asciitilde'] = 570;
+ t['exclamdown'] = 389;
+ t['cent'] = 500;
+ t['sterling'] = 500;
+ t['fraction'] = 167;
+ t['yen'] = 500;
+ t['florin'] = 500;
+ t['section'] = 500;
+ t['currency'] = 500;
+ t['quotesingle'] = 278;
+ t['quotedblleft'] = 500;
+ t['guillemotleft'] = 500;
+ t['guilsinglleft'] = 333;
+ t['guilsinglright'] = 333;
+ t['fi'] = 556;
+ t['fl'] = 556;
+ t['endash'] = 500;
+ t['dagger'] = 500;
+ t['daggerdbl'] = 500;
+ t['periodcentered'] = 250;
+ t['paragraph'] = 500;
+ t['bullet'] = 350;
+ t['quotesinglbase'] = 333;
+ t['quotedblbase'] = 500;
+ t['quotedblright'] = 500;
+ t['guillemotright'] = 500;
+ t['ellipsis'] = 1000;
+ t['perthousand'] = 1000;
+ t['questiondown'] = 500;
+ t['grave'] = 333;
+ t['acute'] = 333;
+ t['circumflex'] = 333;
+ t['tilde'] = 333;
+ t['macron'] = 333;
+ t['breve'] = 333;
+ t['dotaccent'] = 333;
+ t['dieresis'] = 333;
+ t['ring'] = 333;
+ t['cedilla'] = 333;
+ t['hungarumlaut'] = 333;
+ t['ogonek'] = 333;
+ t['caron'] = 333;
+ t['emdash'] = 1000;
+ t['AE'] = 944;
+ t['ordfeminine'] = 266;
+ t['Lslash'] = 611;
+ t['Oslash'] = 722;
+ t['OE'] = 944;
+ t['ordmasculine'] = 300;
+ t['ae'] = 722;
+ t['dotlessi'] = 278;
+ t['lslash'] = 278;
+ t['oslash'] = 500;
+ t['oe'] = 722;
+ t['germandbls'] = 500;
+ t['Idieresis'] = 389;
+ t['eacute'] = 444;
+ t['abreve'] = 500;
+ t['uhungarumlaut'] = 556;
+ t['ecaron'] = 444;
+ t['Ydieresis'] = 611;
+ t['divide'] = 570;
+ t['Yacute'] = 611;
+ t['Acircumflex'] = 667;
+ t['aacute'] = 500;
+ t['Ucircumflex'] = 722;
+ t['yacute'] = 444;
+ t['scommaaccent'] = 389;
+ t['ecircumflex'] = 444;
+ t['Uring'] = 722;
+ t['Udieresis'] = 722;
+ t['aogonek'] = 500;
+ t['Uacute'] = 722;
+ t['uogonek'] = 556;
+ t['Edieresis'] = 667;
+ t['Dcroat'] = 722;
+ t['commaaccent'] = 250;
+ t['copyright'] = 747;
+ t['Emacron'] = 667;
+ t['ccaron'] = 444;
+ t['aring'] = 500;
+ t['Ncommaaccent'] = 722;
+ t['lacute'] = 278;
+ t['agrave'] = 500;
+ t['Tcommaaccent'] = 611;
+ t['Cacute'] = 667;
+ t['atilde'] = 500;
+ t['Edotaccent'] = 667;
+ t['scaron'] = 389;
+ t['scedilla'] = 389;
+ t['iacute'] = 278;
+ t['lozenge'] = 494;
+ t['Rcaron'] = 667;
+ t['Gcommaaccent'] = 722;
+ t['ucircumflex'] = 556;
+ t['acircumflex'] = 500;
+ t['Amacron'] = 667;
+ t['rcaron'] = 389;
+ t['ccedilla'] = 444;
+ t['Zdotaccent'] = 611;
+ t['Thorn'] = 611;
+ t['Omacron'] = 722;
+ t['Racute'] = 667;
+ t['Sacute'] = 556;
+ t['dcaron'] = 608;
+ t['Umacron'] = 722;
+ t['uring'] = 556;
+ t['threesuperior'] = 300;
+ t['Ograve'] = 722;
+ t['Agrave'] = 667;
+ t['Abreve'] = 667;
+ t['multiply'] = 570;
+ t['uacute'] = 556;
+ t['Tcaron'] = 611;
+ t['partialdiff'] = 494;
+ t['ydieresis'] = 444;
+ t['Nacute'] = 722;
+ t['icircumflex'] = 278;
+ t['Ecircumflex'] = 667;
+ t['adieresis'] = 500;
+ t['edieresis'] = 444;
+ t['cacute'] = 444;
+ t['nacute'] = 556;
+ t['umacron'] = 556;
+ t['Ncaron'] = 722;
+ t['Iacute'] = 389;
+ t['plusminus'] = 570;
+ t['brokenbar'] = 220;
+ t['registered'] = 747;
+ t['Gbreve'] = 722;
+ t['Idotaccent'] = 389;
+ t['summation'] = 600;
+ t['Egrave'] = 667;
+ t['racute'] = 389;
+ t['omacron'] = 500;
+ t['Zacute'] = 611;
+ t['Zcaron'] = 611;
+ t['greaterequal'] = 549;
+ t['Eth'] = 722;
+ t['Ccedilla'] = 667;
+ t['lcommaaccent'] = 278;
+ t['tcaron'] = 366;
+ t['eogonek'] = 444;
+ t['Uogonek'] = 722;
+ t['Aacute'] = 667;
+ t['Adieresis'] = 667;
+ t['egrave'] = 444;
+ t['zacute'] = 389;
+ t['iogonek'] = 278;
+ t['Oacute'] = 722;
+ t['oacute'] = 500;
+ t['amacron'] = 500;
+ t['sacute'] = 389;
+ t['idieresis'] = 278;
+ t['Ocircumflex'] = 722;
+ t['Ugrave'] = 722;
+ t['Delta'] = 612;
+ t['thorn'] = 500;
+ t['twosuperior'] = 300;
+ t['Odieresis'] = 722;
+ t['mu'] = 576;
+ t['igrave'] = 278;
+ t['ohungarumlaut'] = 500;
+ t['Eogonek'] = 667;
+ t['dcroat'] = 500;
+ t['threequarters'] = 750;
+ t['Scedilla'] = 556;
+ t['lcaron'] = 382;
+ t['Kcommaaccent'] = 667;
+ t['Lacute'] = 611;
+ t['trademark'] = 1000;
+ t['edotaccent'] = 444;
+ t['Igrave'] = 389;
+ t['Imacron'] = 389;
+ t['Lcaron'] = 611;
+ t['onehalf'] = 750;
+ t['lessequal'] = 549;
+ t['ocircumflex'] = 500;
+ t['ntilde'] = 556;
+ t['Uhungarumlaut'] = 722;
+ t['Eacute'] = 667;
+ t['emacron'] = 444;
+ t['gbreve'] = 500;
+ t['onequarter'] = 750;
+ t['Scaron'] = 556;
+ t['Scommaaccent'] = 556;
+ t['Ohungarumlaut'] = 722;
+ t['degree'] = 400;
+ t['ograve'] = 500;
+ t['Ccaron'] = 667;
+ t['ugrave'] = 556;
+ t['radical'] = 549;
+ t['Dcaron'] = 722;
+ t['rcommaaccent'] = 389;
+ t['Ntilde'] = 722;
+ t['otilde'] = 500;
+ t['Rcommaaccent'] = 667;
+ t['Lcommaaccent'] = 611;
+ t['Atilde'] = 667;
+ t['Aogonek'] = 667;
+ t['Aring'] = 667;
+ t['Otilde'] = 722;
+ t['zdotaccent'] = 389;
+ t['Ecaron'] = 667;
+ t['Iogonek'] = 389;
+ t['kcommaaccent'] = 500;
+ t['minus'] = 606;
+ t['Icircumflex'] = 389;
+ t['ncaron'] = 556;
+ t['tcommaaccent'] = 278;
+ t['logicalnot'] = 606;
+ t['odieresis'] = 500;
+ t['udieresis'] = 556;
+ t['notequal'] = 549;
+ t['gcommaaccent'] = 500;
+ t['eth'] = 500;
+ t['zcaron'] = 389;
+ t['ncommaaccent'] = 556;
+ t['onesuperior'] = 300;
+ t['imacron'] = 278;
+ t['Euro'] = 500;
+ });
+ t['Times-Italic'] = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['space'] = 250;
+ t['exclam'] = 333;
+ t['quotedbl'] = 420;
+ t['numbersign'] = 500;
+ t['dollar'] = 500;
+ t['percent'] = 833;
+ t['ampersand'] = 778;
+ t['quoteright'] = 333;
+ t['parenleft'] = 333;
+ t['parenright'] = 333;
+ t['asterisk'] = 500;
+ t['plus'] = 675;
+ t['comma'] = 250;
+ t['hyphen'] = 333;
+ t['period'] = 250;
+ t['slash'] = 278;
+ t['zero'] = 500;
+ t['one'] = 500;
+ t['two'] = 500;
+ t['three'] = 500;
+ t['four'] = 500;
+ t['five'] = 500;
+ t['six'] = 500;
+ t['seven'] = 500;
+ t['eight'] = 500;
+ t['nine'] = 500;
+ t['colon'] = 333;
+ t['semicolon'] = 333;
+ t['less'] = 675;
+ t['equal'] = 675;
+ t['greater'] = 675;
+ t['question'] = 500;
+ t['at'] = 920;
+ t['A'] = 611;
+ t['B'] = 611;
+ t['C'] = 667;
+ t['D'] = 722;
+ t['E'] = 611;
+ t['F'] = 611;
+ t['G'] = 722;
+ t['H'] = 722;
+ t['I'] = 333;
+ t['J'] = 444;
+ t['K'] = 667;
+ t['L'] = 556;
+ t['M'] = 833;
+ t['N'] = 667;
+ t['O'] = 722;
+ t['P'] = 611;
+ t['Q'] = 722;
+ t['R'] = 611;
+ t['S'] = 500;
+ t['T'] = 556;
+ t['U'] = 722;
+ t['V'] = 611;
+ t['W'] = 833;
+ t['X'] = 611;
+ t['Y'] = 556;
+ t['Z'] = 556;
+ t['bracketleft'] = 389;
+ t['backslash'] = 278;
+ t['bracketright'] = 389;
+ t['asciicircum'] = 422;
+ t['underscore'] = 500;
+ t['quoteleft'] = 333;
+ t['a'] = 500;
+ t['b'] = 500;
+ t['c'] = 444;
+ t['d'] = 500;
+ t['e'] = 444;
+ t['f'] = 278;
+ t['g'] = 500;
+ t['h'] = 500;
+ t['i'] = 278;
+ t['j'] = 278;
+ t['k'] = 444;
+ t['l'] = 278;
+ t['m'] = 722;
+ t['n'] = 500;
+ t['o'] = 500;
+ t['p'] = 500;
+ t['q'] = 500;
+ t['r'] = 389;
+ t['s'] = 389;
+ t['t'] = 278;
+ t['u'] = 500;
+ t['v'] = 444;
+ t['w'] = 667;
+ t['x'] = 444;
+ t['y'] = 444;
+ t['z'] = 389;
+ t['braceleft'] = 400;
+ t['bar'] = 275;
+ t['braceright'] = 400;
+ t['asciitilde'] = 541;
+ t['exclamdown'] = 389;
+ t['cent'] = 500;
+ t['sterling'] = 500;
+ t['fraction'] = 167;
+ t['yen'] = 500;
+ t['florin'] = 500;
+ t['section'] = 500;
+ t['currency'] = 500;
+ t['quotesingle'] = 214;
+ t['quotedblleft'] = 556;
+ t['guillemotleft'] = 500;
+ t['guilsinglleft'] = 333;
+ t['guilsinglright'] = 333;
+ t['fi'] = 500;
+ t['fl'] = 500;
+ t['endash'] = 500;
+ t['dagger'] = 500;
+ t['daggerdbl'] = 500;
+ t['periodcentered'] = 250;
+ t['paragraph'] = 523;
+ t['bullet'] = 350;
+ t['quotesinglbase'] = 333;
+ t['quotedblbase'] = 556;
+ t['quotedblright'] = 556;
+ t['guillemotright'] = 500;
+ t['ellipsis'] = 889;
+ t['perthousand'] = 1000;
+ t['questiondown'] = 500;
+ t['grave'] = 333;
+ t['acute'] = 333;
+ t['circumflex'] = 333;
+ t['tilde'] = 333;
+ t['macron'] = 333;
+ t['breve'] = 333;
+ t['dotaccent'] = 333;
+ t['dieresis'] = 333;
+ t['ring'] = 333;
+ t['cedilla'] = 333;
+ t['hungarumlaut'] = 333;
+ t['ogonek'] = 333;
+ t['caron'] = 333;
+ t['emdash'] = 889;
+ t['AE'] = 889;
+ t['ordfeminine'] = 276;
+ t['Lslash'] = 556;
+ t['Oslash'] = 722;
+ t['OE'] = 944;
+ t['ordmasculine'] = 310;
+ t['ae'] = 667;
+ t['dotlessi'] = 278;
+ t['lslash'] = 278;
+ t['oslash'] = 500;
+ t['oe'] = 667;
+ t['germandbls'] = 500;
+ t['Idieresis'] = 333;
+ t['eacute'] = 444;
+ t['abreve'] = 500;
+ t['uhungarumlaut'] = 500;
+ t['ecaron'] = 444;
+ t['Ydieresis'] = 556;
+ t['divide'] = 675;
+ t['Yacute'] = 556;
+ t['Acircumflex'] = 611;
+ t['aacute'] = 500;
+ t['Ucircumflex'] = 722;
+ t['yacute'] = 444;
+ t['scommaaccent'] = 389;
+ t['ecircumflex'] = 444;
+ t['Uring'] = 722;
+ t['Udieresis'] = 722;
+ t['aogonek'] = 500;
+ t['Uacute'] = 722;
+ t['uogonek'] = 500;
+ t['Edieresis'] = 611;
+ t['Dcroat'] = 722;
+ t['commaaccent'] = 250;
+ t['copyright'] = 760;
+ t['Emacron'] = 611;
+ t['ccaron'] = 444;
+ t['aring'] = 500;
+ t['Ncommaaccent'] = 667;
+ t['lacute'] = 278;
+ t['agrave'] = 500;
+ t['Tcommaaccent'] = 556;
+ t['Cacute'] = 667;
+ t['atilde'] = 500;
+ t['Edotaccent'] = 611;
+ t['scaron'] = 389;
+ t['scedilla'] = 389;
+ t['iacute'] = 278;
+ t['lozenge'] = 471;
+ t['Rcaron'] = 611;
+ t['Gcommaaccent'] = 722;
+ t['ucircumflex'] = 500;
+ t['acircumflex'] = 500;
+ t['Amacron'] = 611;
+ t['rcaron'] = 389;
+ t['ccedilla'] = 444;
+ t['Zdotaccent'] = 556;
+ t['Thorn'] = 611;
+ t['Omacron'] = 722;
+ t['Racute'] = 611;
+ t['Sacute'] = 500;
+ t['dcaron'] = 544;
+ t['Umacron'] = 722;
+ t['uring'] = 500;
+ t['threesuperior'] = 300;
+ t['Ograve'] = 722;
+ t['Agrave'] = 611;
+ t['Abreve'] = 611;
+ t['multiply'] = 675;
+ t['uacute'] = 500;
+ t['Tcaron'] = 556;
+ t['partialdiff'] = 476;
+ t['ydieresis'] = 444;
+ t['Nacute'] = 667;
+ t['icircumflex'] = 278;
+ t['Ecircumflex'] = 611;
+ t['adieresis'] = 500;
+ t['edieresis'] = 444;
+ t['cacute'] = 444;
+ t['nacute'] = 500;
+ t['umacron'] = 500;
+ t['Ncaron'] = 667;
+ t['Iacute'] = 333;
+ t['plusminus'] = 675;
+ t['brokenbar'] = 275;
+ t['registered'] = 760;
+ t['Gbreve'] = 722;
+ t['Idotaccent'] = 333;
+ t['summation'] = 600;
+ t['Egrave'] = 611;
+ t['racute'] = 389;
+ t['omacron'] = 500;
+ t['Zacute'] = 556;
+ t['Zcaron'] = 556;
+ t['greaterequal'] = 549;
+ t['Eth'] = 722;
+ t['Ccedilla'] = 667;
+ t['lcommaaccent'] = 278;
+ t['tcaron'] = 300;
+ t['eogonek'] = 444;
+ t['Uogonek'] = 722;
+ t['Aacute'] = 611;
+ t['Adieresis'] = 611;
+ t['egrave'] = 444;
+ t['zacute'] = 389;
+ t['iogonek'] = 278;
+ t['Oacute'] = 722;
+ t['oacute'] = 500;
+ t['amacron'] = 500;
+ t['sacute'] = 389;
+ t['idieresis'] = 278;
+ t['Ocircumflex'] = 722;
+ t['Ugrave'] = 722;
+ t['Delta'] = 612;
+ t['thorn'] = 500;
+ t['twosuperior'] = 300;
+ t['Odieresis'] = 722;
+ t['mu'] = 500;
+ t['igrave'] = 278;
+ t['ohungarumlaut'] = 500;
+ t['Eogonek'] = 611;
+ t['dcroat'] = 500;
+ t['threequarters'] = 750;
+ t['Scedilla'] = 500;
+ t['lcaron'] = 300;
+ t['Kcommaaccent'] = 667;
+ t['Lacute'] = 556;
+ t['trademark'] = 980;
+ t['edotaccent'] = 444;
+ t['Igrave'] = 333;
+ t['Imacron'] = 333;
+ t['Lcaron'] = 611;
+ t['onehalf'] = 750;
+ t['lessequal'] = 549;
+ t['ocircumflex'] = 500;
+ t['ntilde'] = 500;
+ t['Uhungarumlaut'] = 722;
+ t['Eacute'] = 611;
+ t['emacron'] = 444;
+ t['gbreve'] = 500;
+ t['onequarter'] = 750;
+ t['Scaron'] = 500;
+ t['Scommaaccent'] = 500;
+ t['Ohungarumlaut'] = 722;
+ t['degree'] = 400;
+ t['ograve'] = 500;
+ t['Ccaron'] = 667;
+ t['ugrave'] = 500;
+ t['radical'] = 453;
+ t['Dcaron'] = 722;
+ t['rcommaaccent'] = 389;
+ t['Ntilde'] = 667;
+ t['otilde'] = 500;
+ t['Rcommaaccent'] = 611;
+ t['Lcommaaccent'] = 556;
+ t['Atilde'] = 611;
+ t['Aogonek'] = 611;
+ t['Aring'] = 611;
+ t['Otilde'] = 722;
+ t['zdotaccent'] = 389;
+ t['Ecaron'] = 611;
+ t['Iogonek'] = 333;
+ t['kcommaaccent'] = 444;
+ t['minus'] = 675;
+ t['Icircumflex'] = 333;
+ t['ncaron'] = 500;
+ t['tcommaaccent'] = 278;
+ t['logicalnot'] = 675;
+ t['odieresis'] = 500;
+ t['udieresis'] = 500;
+ t['notequal'] = 549;
+ t['gcommaaccent'] = 500;
+ t['eth'] = 500;
+ t['zcaron'] = 389;
+ t['ncommaaccent'] = 500;
+ t['onesuperior'] = 300;
+ t['imacron'] = 278;
+ t['Euro'] = 500;
+ });
+ t['ZapfDingbats'] = (0, _core_utils.getLookupTableFactory)(function (t) {
+ t['space'] = 278;
+ t['a1'] = 974;
+ t['a2'] = 961;
+ t['a202'] = 974;
+ t['a3'] = 980;
+ t['a4'] = 719;
+ t['a5'] = 789;
+ t['a119'] = 790;
+ t['a118'] = 791;
+ t['a117'] = 690;
+ t['a11'] = 960;
+ t['a12'] = 939;
+ t['a13'] = 549;
+ t['a14'] = 855;
+ t['a15'] = 911;
+ t['a16'] = 933;
+ t['a105'] = 911;
+ t['a17'] = 945;
+ t['a18'] = 974;
+ t['a19'] = 755;
+ t['a20'] = 846;
+ t['a21'] = 762;
+ t['a22'] = 761;
+ t['a23'] = 571;
+ t['a24'] = 677;
+ t['a25'] = 763;
+ t['a26'] = 760;
+ t['a27'] = 759;
+ t['a28'] = 754;
+ t['a6'] = 494;
+ t['a7'] = 552;
+ t['a8'] = 537;
+ t['a9'] = 577;
+ t['a10'] = 692;
+ t['a29'] = 786;
+ t['a30'] = 788;
+ t['a31'] = 788;
+ t['a32'] = 790;
+ t['a33'] = 793;
+ t['a34'] = 794;
+ t['a35'] = 816;
+ t['a36'] = 823;
+ t['a37'] = 789;
+ t['a38'] = 841;
+ t['a39'] = 823;
+ t['a40'] = 833;
+ t['a41'] = 816;
+ t['a42'] = 831;
+ t['a43'] = 923;
+ t['a44'] = 744;
+ t['a45'] = 723;
+ t['a46'] = 749;
+ t['a47'] = 790;
+ t['a48'] = 792;
+ t['a49'] = 695;
+ t['a50'] = 776;
+ t['a51'] = 768;
+ t['a52'] = 792;
+ t['a53'] = 759;
+ t['a54'] = 707;
+ t['a55'] = 708;
+ t['a56'] = 682;
+ t['a57'] = 701;
+ t['a58'] = 826;
+ t['a59'] = 815;
+ t['a60'] = 789;
+ t['a61'] = 789;
+ t['a62'] = 707;
+ t['a63'] = 687;
+ t['a64'] = 696;
+ t['a65'] = 689;
+ t['a66'] = 786;
+ t['a67'] = 787;
+ t['a68'] = 713;
+ t['a69'] = 791;
+ t['a70'] = 785;
+ t['a71'] = 791;
+ t['a72'] = 873;
+ t['a73'] = 761;
+ t['a74'] = 762;
+ t['a203'] = 762;
+ t['a75'] = 759;
+ t['a204'] = 759;
+ t['a76'] = 892;
+ t['a77'] = 892;
+ t['a78'] = 788;
+ t['a79'] = 784;
+ t['a81'] = 438;
+ t['a82'] = 138;
+ t['a83'] = 277;
+ t['a84'] = 415;
+ t['a97'] = 392;
+ t['a98'] = 392;
+ t['a99'] = 668;
+ t['a100'] = 668;
+ t['a89'] = 390;
+ t['a90'] = 390;
+ t['a93'] = 317;
+ t['a94'] = 317;
+ t['a91'] = 276;
+ t['a92'] = 276;
+ t['a205'] = 509;
+ t['a85'] = 509;
+ t['a206'] = 410;
+ t['a86'] = 410;
+ t['a87'] = 234;
+ t['a88'] = 234;
+ t['a95'] = 334;
+ t['a96'] = 334;
+ t['a101'] = 732;
+ t['a102'] = 544;
+ t['a103'] = 544;
+ t['a104'] = 910;
+ t['a106'] = 667;
+ t['a107'] = 760;
+ t['a108'] = 760;
+ t['a112'] = 776;
+ t['a111'] = 595;
+ t['a110'] = 694;
+ t['a109'] = 626;
+ t['a120'] = 788;
+ t['a121'] = 788;
+ t['a122'] = 788;
+ t['a123'] = 788;
+ t['a124'] = 788;
+ t['a125'] = 788;
+ t['a126'] = 788;
+ t['a127'] = 788;
+ t['a128'] = 788;
+ t['a129'] = 788;
+ t['a130'] = 788;
+ t['a131'] = 788;
+ t['a132'] = 788;
+ t['a133'] = 788;
+ t['a134'] = 788;
+ t['a135'] = 788;
+ t['a136'] = 788;
+ t['a137'] = 788;
+ t['a138'] = 788;
+ t['a139'] = 788;
+ t['a140'] = 788;
+ t['a141'] = 788;
+ t['a142'] = 788;
+ t['a143'] = 788;
+ t['a144'] = 788;
+ t['a145'] = 788;
+ t['a146'] = 788;
+ t['a147'] = 788;
+ t['a148'] = 788;
+ t['a149'] = 788;
+ t['a150'] = 788;
+ t['a151'] = 788;
+ t['a152'] = 788;
+ t['a153'] = 788;
+ t['a154'] = 788;
+ t['a155'] = 788;
+ t['a156'] = 788;
+ t['a157'] = 788;
+ t['a158'] = 788;
+ t['a159'] = 788;
+ t['a160'] = 894;
+ t['a161'] = 838;
+ t['a163'] = 1016;
+ t['a164'] = 458;
+ t['a196'] = 748;
+ t['a165'] = 924;
+ t['a192'] = 748;
+ t['a166'] = 918;
+ t['a167'] = 927;
+ t['a168'] = 928;
+ t['a169'] = 928;
+ t['a170'] = 834;
+ t['a171'] = 873;
+ t['a172'] = 828;
+ t['a173'] = 924;
+ t['a162'] = 924;
+ t['a174'] = 917;
+ t['a175'] = 930;
+ t['a176'] = 931;
+ t['a177'] = 463;
+ t['a178'] = 883;
+ t['a179'] = 836;
+ t['a193'] = 836;
+ t['a180'] = 867;
+ t['a199'] = 867;
+ t['a181'] = 696;
+ t['a200'] = 696;
+ t['a182'] = 874;
+ t['a201'] = 874;
+ t['a183'] = 760;
+ t['a184'] = 946;
+ t['a197'] = 771;
+ t['a185'] = 865;
+ t['a194'] = 771;
+ t['a198'] = 888;
+ t['a186'] = 967;
+ t['a195'] = 888;
+ t['a187'] = 831;
+ t['a188'] = 873;
+ t['a189'] = 927;
+ t['a190'] = 970;
+ t['a191'] = 918;
+ });
+});
+exports.getMetrics = getMetrics;
+
+/***/ }),
+/* 186 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.isPDFFunction = isPDFFunction;
+exports.PostScriptCompiler = exports.PostScriptEvaluator = exports.PDFFunctionFactory = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _ps_parser = __w_pdfjs_require__(187);
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var IsEvalSupportedCached = {
+ get value() {
+ return (0, _util.shadow)(this, 'value', (0, _util.isEvalSupported)());
+ }
+
+};
+
+var PDFFunctionFactory =
+/*#__PURE__*/
+function () {
+ function PDFFunctionFactory(_ref) {
+ var xref = _ref.xref,
+ _ref$isEvalSupported = _ref.isEvalSupported,
+ isEvalSupported = _ref$isEvalSupported === void 0 ? true : _ref$isEvalSupported;
+
+ _classCallCheck(this, PDFFunctionFactory);
+
+ this.xref = xref;
+ this.isEvalSupported = isEvalSupported !== false;
+ }
+
+ _createClass(PDFFunctionFactory, [{
+ key: "create",
+ value: function create(fn) {
+ return PDFFunction.parse({
+ xref: this.xref,
+ isEvalSupported: this.isEvalSupported,
+ fn: fn
+ });
+ }
+ }, {
+ key: "createFromArray",
+ value: function createFromArray(fnObj) {
+ return PDFFunction.parseArray({
+ xref: this.xref,
+ isEvalSupported: this.isEvalSupported,
+ fnObj: fnObj
+ });
+ }
+ }]);
+
+ return PDFFunctionFactory;
+}();
+
+exports.PDFFunctionFactory = PDFFunctionFactory;
+
+function toNumberArray(arr) {
+ if (!Array.isArray(arr)) {
+ return null;
+ }
+
+ var length = arr.length;
+
+ for (var i = 0; i < length; i++) {
+ if (typeof arr[i] !== 'number') {
+ var result = new Array(length);
+
+ for (var _i = 0; _i < length; _i++) {
+ result[_i] = +arr[_i];
+ }
+
+ return result;
+ }
+ }
+
+ return arr;
+}
+
+var PDFFunction = function PDFFunctionClosure() {
+ var CONSTRUCT_SAMPLED = 0;
+ var CONSTRUCT_INTERPOLATED = 2;
+ var CONSTRUCT_STICHED = 3;
+ var CONSTRUCT_POSTSCRIPT = 4;
+ return {
+ getSampleArray: function getSampleArray(size, outputSize, bps, stream) {
+ var i, ii;
+ var length = 1;
+
+ for (i = 0, ii = size.length; i < ii; i++) {
+ length *= size[i];
+ }
+
+ length *= outputSize;
+ var array = new Array(length);
+ var codeSize = 0;
+ var codeBuf = 0;
+ var sampleMul = 1.0 / (Math.pow(2.0, bps) - 1);
+ var strBytes = stream.getBytes((length * bps + 7) / 8);
+ var strIdx = 0;
+
+ for (i = 0; i < length; i++) {
+ while (codeSize < bps) {
+ codeBuf <<= 8;
+ codeBuf |= strBytes[strIdx++];
+ codeSize += 8;
+ }
+
+ codeSize -= bps;
+ array[i] = (codeBuf >> codeSize) * sampleMul;
+ codeBuf &= (1 << codeSize) - 1;
+ }
+
+ return array;
+ },
+ getIR: function getIR(_ref2) {
+ var xref = _ref2.xref,
+ isEvalSupported = _ref2.isEvalSupported,
+ fn = _ref2.fn;
+ var dict = fn.dict;
+
+ if (!dict) {
+ dict = fn;
+ }
+
+ var types = [this.constructSampled, null, this.constructInterpolated, this.constructStiched, this.constructPostScript];
+ var typeNum = dict.get('FunctionType');
+ var typeFn = types[typeNum];
+
+ if (!typeFn) {
+ throw new _util.FormatError('Unknown type of function');
+ }
+
+ return typeFn.call(this, {
+ xref: xref,
+ isEvalSupported: isEvalSupported,
+ fn: fn,
+ dict: dict
+ });
+ },
+ fromIR: function fromIR(_ref3) {
+ var xref = _ref3.xref,
+ isEvalSupported = _ref3.isEvalSupported,
+ IR = _ref3.IR;
+ var type = IR[0];
+
+ switch (type) {
+ case CONSTRUCT_SAMPLED:
+ return this.constructSampledFromIR({
+ xref: xref,
+ isEvalSupported: isEvalSupported,
+ IR: IR
+ });
+
+ case CONSTRUCT_INTERPOLATED:
+ return this.constructInterpolatedFromIR({
+ xref: xref,
+ isEvalSupported: isEvalSupported,
+ IR: IR
+ });
+
+ case CONSTRUCT_STICHED:
+ return this.constructStichedFromIR({
+ xref: xref,
+ isEvalSupported: isEvalSupported,
+ IR: IR
+ });
+
+ default:
+ return this.constructPostScriptFromIR({
+ xref: xref,
+ isEvalSupported: isEvalSupported,
+ IR: IR
+ });
+ }
+ },
+ parse: function parse(_ref4) {
+ var xref = _ref4.xref,
+ isEvalSupported = _ref4.isEvalSupported,
+ fn = _ref4.fn;
+ var IR = this.getIR({
+ xref: xref,
+ isEvalSupported: isEvalSupported,
+ fn: fn
+ });
+ return this.fromIR({
+ xref: xref,
+ isEvalSupported: isEvalSupported,
+ IR: IR
+ });
+ },
+ parseArray: function parseArray(_ref5) {
+ var xref = _ref5.xref,
+ isEvalSupported = _ref5.isEvalSupported,
+ fnObj = _ref5.fnObj;
+
+ if (!Array.isArray(fnObj)) {
+ return this.parse({
+ xref: xref,
+ isEvalSupported: isEvalSupported,
+ fn: fnObj
+ });
+ }
+
+ var fnArray = [];
+
+ for (var j = 0, jj = fnObj.length; j < jj; j++) {
+ fnArray.push(this.parse({
+ xref: xref,
+ isEvalSupported: isEvalSupported,
+ fn: xref.fetchIfRef(fnObj[j])
+ }));
+ }
+
+ return function (src, srcOffset, dest, destOffset) {
+ for (var i = 0, ii = fnArray.length; i < ii; i++) {
+ fnArray[i](src, srcOffset, dest, destOffset + i);
+ }
+ };
+ },
+ constructSampled: function constructSampled(_ref6) {
+ var xref = _ref6.xref,
+ isEvalSupported = _ref6.isEvalSupported,
+ fn = _ref6.fn,
+ dict = _ref6.dict;
+
+ function toMultiArray(arr) {
+ var inputLength = arr.length;
+ var out = [];
+ var index = 0;
+
+ for (var i = 0; i < inputLength; i += 2) {
+ out[index] = [arr[i], arr[i + 1]];
+ ++index;
+ }
+
+ return out;
+ }
+
+ var domain = toNumberArray(dict.getArray('Domain'));
+ var range = toNumberArray(dict.getArray('Range'));
+
+ if (!domain || !range) {
+ throw new _util.FormatError('No domain or range');
+ }
+
+ var inputSize = domain.length / 2;
+ var outputSize = range.length / 2;
+ domain = toMultiArray(domain);
+ range = toMultiArray(range);
+ var size = toNumberArray(dict.getArray('Size'));
+ var bps = dict.get('BitsPerSample');
+ var order = dict.get('Order') || 1;
+
+ if (order !== 1) {
+ (0, _util.info)('No support for cubic spline interpolation: ' + order);
+ }
+
+ var encode = toNumberArray(dict.getArray('Encode'));
+
+ if (!encode) {
+ encode = [];
+
+ for (var i = 0; i < inputSize; ++i) {
+ encode.push([0, size[i] - 1]);
+ }
+ } else {
+ encode = toMultiArray(encode);
+ }
+
+ var decode = toNumberArray(dict.getArray('Decode'));
+
+ if (!decode) {
+ decode = range;
+ } else {
+ decode = toMultiArray(decode);
+ }
+
+ var samples = this.getSampleArray(size, outputSize, bps, fn);
+ return [CONSTRUCT_SAMPLED, inputSize, domain, encode, decode, samples, size, outputSize, Math.pow(2, bps) - 1, range];
+ },
+ constructSampledFromIR: function constructSampledFromIR(_ref7) {
+ var xref = _ref7.xref,
+ isEvalSupported = _ref7.isEvalSupported,
+ IR = _ref7.IR;
+
+ function interpolate(x, xmin, xmax, ymin, ymax) {
+ return ymin + (x - xmin) * ((ymax - ymin) / (xmax - xmin));
+ }
+
+ return function constructSampledFromIRResult(src, srcOffset, dest, destOffset) {
+ var m = IR[1];
+ var domain = IR[2];
+ var encode = IR[3];
+ var decode = IR[4];
+ var samples = IR[5];
+ var size = IR[6];
+ var n = IR[7];
+ var range = IR[9];
+ var cubeVertices = 1 << m;
+ var cubeN = new Float64Array(cubeVertices);
+ var cubeVertex = new Uint32Array(cubeVertices);
+ var i, j;
+
+ for (j = 0; j < cubeVertices; j++) {
+ cubeN[j] = 1;
+ }
+
+ var k = n,
+ pos = 1;
+
+ for (i = 0; i < m; ++i) {
+ var domain_2i = domain[i][0];
+ var domain_2i_1 = domain[i][1];
+ var xi = Math.min(Math.max(src[srcOffset + i], domain_2i), domain_2i_1);
+ var e = interpolate(xi, domain_2i, domain_2i_1, encode[i][0], encode[i][1]);
+ var size_i = size[i];
+ e = Math.min(Math.max(e, 0), size_i - 1);
+ var e0 = e < size_i - 1 ? Math.floor(e) : e - 1;
+ var n0 = e0 + 1 - e;
+ var n1 = e - e0;
+ var offset0 = e0 * k;
+ var offset1 = offset0 + k;
+
+ for (j = 0; j < cubeVertices; j++) {
+ if (j & pos) {
+ cubeN[j] *= n1;
+ cubeVertex[j] += offset1;
+ } else {
+ cubeN[j] *= n0;
+ cubeVertex[j] += offset0;
+ }
+ }
+
+ k *= size_i;
+ pos <<= 1;
+ }
+
+ for (j = 0; j < n; ++j) {
+ var rj = 0;
+
+ for (i = 0; i < cubeVertices; i++) {
+ rj += samples[cubeVertex[i] + j] * cubeN[i];
+ }
+
+ rj = interpolate(rj, 0, 1, decode[j][0], decode[j][1]);
+ dest[destOffset + j] = Math.min(Math.max(rj, range[j][0]), range[j][1]);
+ }
+ };
+ },
+ constructInterpolated: function constructInterpolated(_ref8) {
+ var xref = _ref8.xref,
+ isEvalSupported = _ref8.isEvalSupported,
+ fn = _ref8.fn,
+ dict = _ref8.dict;
+ var c0 = toNumberArray(dict.getArray('C0')) || [0];
+ var c1 = toNumberArray(dict.getArray('C1')) || [1];
+ var n = dict.get('N');
+ var length = c0.length;
+ var diff = [];
+
+ for (var i = 0; i < length; ++i) {
+ diff.push(c1[i] - c0[i]);
+ }
+
+ return [CONSTRUCT_INTERPOLATED, c0, diff, n];
+ },
+ constructInterpolatedFromIR: function constructInterpolatedFromIR(_ref9) {
+ var xref = _ref9.xref,
+ isEvalSupported = _ref9.isEvalSupported,
+ IR = _ref9.IR;
+ var c0 = IR[1];
+ var diff = IR[2];
+ var n = IR[3];
+ var length = diff.length;
+ return function constructInterpolatedFromIRResult(src, srcOffset, dest, destOffset) {
+ var x = n === 1 ? src[srcOffset] : Math.pow(src[srcOffset], n);
+
+ for (var j = 0; j < length; ++j) {
+ dest[destOffset + j] = c0[j] + x * diff[j];
+ }
+ };
+ },
+ constructStiched: function constructStiched(_ref10) {
+ var xref = _ref10.xref,
+ isEvalSupported = _ref10.isEvalSupported,
+ fn = _ref10.fn,
+ dict = _ref10.dict;
+ var domain = toNumberArray(dict.getArray('Domain'));
+
+ if (!domain) {
+ throw new _util.FormatError('No domain');
+ }
+
+ var inputSize = domain.length / 2;
+
+ if (inputSize !== 1) {
+ throw new _util.FormatError('Bad domain for stiched function');
+ }
+
+ var fnRefs = dict.get('Functions');
+ var fns = [];
+
+ for (var i = 0, ii = fnRefs.length; i < ii; ++i) {
+ fns.push(this.parse({
+ xref: xref,
+ isEvalSupported: isEvalSupported,
+ fn: xref.fetchIfRef(fnRefs[i])
+ }));
+ }
+
+ var bounds = toNumberArray(dict.getArray('Bounds'));
+ var encode = toNumberArray(dict.getArray('Encode'));
+ return [CONSTRUCT_STICHED, domain, bounds, encode, fns];
+ },
+ constructStichedFromIR: function constructStichedFromIR(_ref11) {
+ var xref = _ref11.xref,
+ isEvalSupported = _ref11.isEvalSupported,
+ IR = _ref11.IR;
+ var domain = IR[1];
+ var bounds = IR[2];
+ var encode = IR[3];
+ var fns = IR[4];
+ var tmpBuf = new Float32Array(1);
+ return function constructStichedFromIRResult(src, srcOffset, dest, destOffset) {
+ var clip = function constructStichedFromIRClip(v, min, max) {
+ if (v > max) {
+ v = max;
+ } else if (v < min) {
+ v = min;
+ }
+
+ return v;
+ };
+
+ var v = clip(src[srcOffset], domain[0], domain[1]);
+
+ for (var i = 0, ii = bounds.length; i < ii; ++i) {
+ if (v < bounds[i]) {
+ break;
+ }
+ }
+
+ var dmin = domain[0];
+
+ if (i > 0) {
+ dmin = bounds[i - 1];
+ }
+
+ var dmax = domain[1];
+
+ if (i < bounds.length) {
+ dmax = bounds[i];
+ }
+
+ var rmin = encode[2 * i];
+ var rmax = encode[2 * i + 1];
+ tmpBuf[0] = dmin === dmax ? rmin : rmin + (v - dmin) * (rmax - rmin) / (dmax - dmin);
+ fns[i](tmpBuf, 0, dest, destOffset);
+ };
+ },
+ constructPostScript: function constructPostScript(_ref12) {
+ var xref = _ref12.xref,
+ isEvalSupported = _ref12.isEvalSupported,
+ fn = _ref12.fn,
+ dict = _ref12.dict;
+ var domain = toNumberArray(dict.getArray('Domain'));
+ var range = toNumberArray(dict.getArray('Range'));
+
+ if (!domain) {
+ throw new _util.FormatError('No domain.');
+ }
+
+ if (!range) {
+ throw new _util.FormatError('No range.');
+ }
+
+ var lexer = new _ps_parser.PostScriptLexer(fn);
+ var parser = new _ps_parser.PostScriptParser(lexer);
+ var code = parser.parse();
+ return [CONSTRUCT_POSTSCRIPT, domain, range, code];
+ },
+ constructPostScriptFromIR: function constructPostScriptFromIR(_ref13) {
+ var xref = _ref13.xref,
+ isEvalSupported = _ref13.isEvalSupported,
+ IR = _ref13.IR;
+ var domain = IR[1];
+ var range = IR[2];
+ var code = IR[3];
+
+ if (isEvalSupported && IsEvalSupportedCached.value) {
+ var compiled = new PostScriptCompiler().compile(code, domain, range);
+
+ if (compiled) {
+ return new Function('src', 'srcOffset', 'dest', 'destOffset', compiled);
+ }
+ }
+
+ (0, _util.info)('Unable to compile PS function');
+ var numOutputs = range.length >> 1;
+ var numInputs = domain.length >> 1;
+ var evaluator = new PostScriptEvaluator(code);
+ var cache = Object.create(null);
+ var MAX_CACHE_SIZE = 2048 * 4;
+ var cache_available = MAX_CACHE_SIZE;
+ var tmpBuf = new Float32Array(numInputs);
+ return function constructPostScriptFromIRResult(src, srcOffset, dest, destOffset) {
+ var i, value;
+ var key = '';
+ var input = tmpBuf;
+
+ for (i = 0; i < numInputs; i++) {
+ value = src[srcOffset + i];
+ input[i] = value;
+ key += value + '_';
+ }
+
+ var cachedValue = cache[key];
+
+ if (cachedValue !== undefined) {
+ dest.set(cachedValue, destOffset);
+ return;
+ }
+
+ var output = new Float32Array(numOutputs);
+ var stack = evaluator.execute(input);
+ var stackIndex = stack.length - numOutputs;
+
+ for (i = 0; i < numOutputs; i++) {
+ value = stack[stackIndex + i];
+ var bound = range[i * 2];
+
+ if (value < bound) {
+ value = bound;
+ } else {
+ bound = range[i * 2 + 1];
+
+ if (value > bound) {
+ value = bound;
+ }
+ }
+
+ output[i] = value;
+ }
+
+ if (cache_available > 0) {
+ cache_available--;
+ cache[key] = output;
+ }
+
+ dest.set(output, destOffset);
+ };
+ }
+ };
+}();
+
+function isPDFFunction(v) {
+ var fnDict;
+
+ if (_typeof(v) !== 'object') {
+ return false;
+ } else if ((0, _primitives.isDict)(v)) {
+ fnDict = v;
+ } else if ((0, _primitives.isStream)(v)) {
+ fnDict = v.dict;
+ } else {
+ return false;
+ }
+
+ return fnDict.has('FunctionType');
+}
+
+var PostScriptStack = function PostScriptStackClosure() {
+ var MAX_STACK_SIZE = 100;
+
+ function PostScriptStack(initialStack) {
+ this.stack = !initialStack ? [] : Array.prototype.slice.call(initialStack, 0);
+ }
+
+ PostScriptStack.prototype = {
+ push: function PostScriptStack_push(value) {
+ if (this.stack.length >= MAX_STACK_SIZE) {
+ throw new Error('PostScript function stack overflow.');
+ }
+
+ this.stack.push(value);
+ },
+ pop: function PostScriptStack_pop() {
+ if (this.stack.length <= 0) {
+ throw new Error('PostScript function stack underflow.');
+ }
+
+ return this.stack.pop();
+ },
+ copy: function PostScriptStack_copy(n) {
+ if (this.stack.length + n >= MAX_STACK_SIZE) {
+ throw new Error('PostScript function stack overflow.');
+ }
+
+ var stack = this.stack;
+
+ for (var i = stack.length - n, j = n - 1; j >= 0; j--, i++) {
+ stack.push(stack[i]);
+ }
+ },
+ index: function PostScriptStack_index(n) {
+ this.push(this.stack[this.stack.length - n - 1]);
+ },
+ roll: function PostScriptStack_roll(n, p) {
+ var stack = this.stack;
+ var l = stack.length - n;
+ var r = stack.length - 1,
+ c = l + (p - Math.floor(p / n) * n),
+ i,
+ j,
+ t;
+
+ for (i = l, j = r; i < j; i++, j--) {
+ t = stack[i];
+ stack[i] = stack[j];
+ stack[j] = t;
+ }
+
+ for (i = l, j = c - 1; i < j; i++, j--) {
+ t = stack[i];
+ stack[i] = stack[j];
+ stack[j] = t;
+ }
+
+ for (i = c, j = r; i < j; i++, j--) {
+ t = stack[i];
+ stack[i] = stack[j];
+ stack[j] = t;
+ }
+ }
+ };
+ return PostScriptStack;
+}();
+
+var PostScriptEvaluator = function PostScriptEvaluatorClosure() {
+ function PostScriptEvaluator(operators) {
+ this.operators = operators;
+ }
+
+ PostScriptEvaluator.prototype = {
+ execute: function PostScriptEvaluator_execute(initialStack) {
+ var stack = new PostScriptStack(initialStack);
+ var counter = 0;
+ var operators = this.operators;
+ var length = operators.length;
+ var operator, a, b;
+
+ while (counter < length) {
+ operator = operators[counter++];
+
+ if (typeof operator === 'number') {
+ stack.push(operator);
+ continue;
+ }
+
+ switch (operator) {
+ case 'jz':
+ b = stack.pop();
+ a = stack.pop();
+
+ if (!a) {
+ counter = b;
+ }
+
+ break;
+
+ case 'j':
+ a = stack.pop();
+ counter = a;
+ break;
+
+ case 'abs':
+ a = stack.pop();
+ stack.push(Math.abs(a));
+ break;
+
+ case 'add':
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(a + b);
+ break;
+
+ case 'and':
+ b = stack.pop();
+ a = stack.pop();
+
+ if ((0, _util.isBool)(a) && (0, _util.isBool)(b)) {
+ stack.push(a && b);
+ } else {
+ stack.push(a & b);
+ }
+
+ break;
+
+ case 'atan':
+ a = stack.pop();
+ stack.push(Math.atan(a));
+ break;
+
+ case 'bitshift':
+ b = stack.pop();
+ a = stack.pop();
+
+ if (a > 0) {
+ stack.push(a << b);
+ } else {
+ stack.push(a >> b);
+ }
+
+ break;
+
+ case 'ceiling':
+ a = stack.pop();
+ stack.push(Math.ceil(a));
+ break;
+
+ case 'copy':
+ a = stack.pop();
+ stack.copy(a);
+ break;
+
+ case 'cos':
+ a = stack.pop();
+ stack.push(Math.cos(a));
+ break;
+
+ case 'cvi':
+ a = stack.pop() | 0;
+ stack.push(a);
+ break;
+
+ case 'cvr':
+ break;
+
+ case 'div':
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(a / b);
+ break;
+
+ case 'dup':
+ stack.copy(1);
+ break;
+
+ case 'eq':
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(a === b);
+ break;
+
+ case 'exch':
+ stack.roll(2, 1);
+ break;
+
+ case 'exp':
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(Math.pow(a, b));
+ break;
+
+ case 'false':
+ stack.push(false);
+ break;
+
+ case 'floor':
+ a = stack.pop();
+ stack.push(Math.floor(a));
+ break;
+
+ case 'ge':
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(a >= b);
+ break;
+
+ case 'gt':
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(a > b);
+ break;
+
+ case 'idiv':
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(a / b | 0);
+ break;
+
+ case 'index':
+ a = stack.pop();
+ stack.index(a);
+ break;
+
+ case 'le':
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(a <= b);
+ break;
+
+ case 'ln':
+ a = stack.pop();
+ stack.push(Math.log(a));
+ break;
+
+ case 'log':
+ a = stack.pop();
+ stack.push(Math.log(a) / Math.LN10);
+ break;
+
+ case 'lt':
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(a < b);
+ break;
+
+ case 'mod':
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(a % b);
+ break;
+
+ case 'mul':
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(a * b);
+ break;
+
+ case 'ne':
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(a !== b);
+ break;
+
+ case 'neg':
+ a = stack.pop();
+ stack.push(-a);
+ break;
+
+ case 'not':
+ a = stack.pop();
+
+ if ((0, _util.isBool)(a)) {
+ stack.push(!a);
+ } else {
+ stack.push(~a);
+ }
+
+ break;
+
+ case 'or':
+ b = stack.pop();
+ a = stack.pop();
+
+ if ((0, _util.isBool)(a) && (0, _util.isBool)(b)) {
+ stack.push(a || b);
+ } else {
+ stack.push(a | b);
+ }
+
+ break;
+
+ case 'pop':
+ stack.pop();
+ break;
+
+ case 'roll':
+ b = stack.pop();
+ a = stack.pop();
+ stack.roll(a, b);
+ break;
+
+ case 'round':
+ a = stack.pop();
+ stack.push(Math.round(a));
+ break;
+
+ case 'sin':
+ a = stack.pop();
+ stack.push(Math.sin(a));
+ break;
+
+ case 'sqrt':
+ a = stack.pop();
+ stack.push(Math.sqrt(a));
+ break;
+
+ case 'sub':
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(a - b);
+ break;
+
+ case 'true':
+ stack.push(true);
+ break;
+
+ case 'truncate':
+ a = stack.pop();
+ a = a < 0 ? Math.ceil(a) : Math.floor(a);
+ stack.push(a);
+ break;
+
+ case 'xor':
+ b = stack.pop();
+ a = stack.pop();
+
+ if ((0, _util.isBool)(a) && (0, _util.isBool)(b)) {
+ stack.push(a !== b);
+ } else {
+ stack.push(a ^ b);
+ }
+
+ break;
+
+ default:
+ throw new _util.FormatError("Unknown operator ".concat(operator));
+ }
+ }
+
+ return stack.stack;
+ }
+ };
+ return PostScriptEvaluator;
+}();
+
+exports.PostScriptEvaluator = PostScriptEvaluator;
+
+var PostScriptCompiler = function PostScriptCompilerClosure() {
+ function AstNode(type) {
+ this.type = type;
+ }
+
+ AstNode.prototype.visit = function (visitor) {
+ (0, _util.unreachable)('abstract method');
+ };
+
+ function AstArgument(index, min, max) {
+ AstNode.call(this, 'args');
+ this.index = index;
+ this.min = min;
+ this.max = max;
+ }
+
+ AstArgument.prototype = Object.create(AstNode.prototype);
+
+ AstArgument.prototype.visit = function (visitor) {
+ visitor.visitArgument(this);
+ };
+
+ function AstLiteral(number) {
+ AstNode.call(this, 'literal');
+ this.number = number;
+ this.min = number;
+ this.max = number;
+ }
+
+ AstLiteral.prototype = Object.create(AstNode.prototype);
+
+ AstLiteral.prototype.visit = function (visitor) {
+ visitor.visitLiteral(this);
+ };
+
+ function AstBinaryOperation(op, arg1, arg2, min, max) {
+ AstNode.call(this, 'binary');
+ this.op = op;
+ this.arg1 = arg1;
+ this.arg2 = arg2;
+ this.min = min;
+ this.max = max;
+ }
+
+ AstBinaryOperation.prototype = Object.create(AstNode.prototype);
+
+ AstBinaryOperation.prototype.visit = function (visitor) {
+ visitor.visitBinaryOperation(this);
+ };
+
+ function AstMin(arg, max) {
+ AstNode.call(this, 'max');
+ this.arg = arg;
+ this.min = arg.min;
+ this.max = max;
+ }
+
+ AstMin.prototype = Object.create(AstNode.prototype);
+
+ AstMin.prototype.visit = function (visitor) {
+ visitor.visitMin(this);
+ };
+
+ function AstVariable(index, min, max) {
+ AstNode.call(this, 'var');
+ this.index = index;
+ this.min = min;
+ this.max = max;
+ }
+
+ AstVariable.prototype = Object.create(AstNode.prototype);
+
+ AstVariable.prototype.visit = function (visitor) {
+ visitor.visitVariable(this);
+ };
+
+ function AstVariableDefinition(variable, arg) {
+ AstNode.call(this, 'definition');
+ this.variable = variable;
+ this.arg = arg;
+ }
+
+ AstVariableDefinition.prototype = Object.create(AstNode.prototype);
+
+ AstVariableDefinition.prototype.visit = function (visitor) {
+ visitor.visitVariableDefinition(this);
+ };
+
+ function ExpressionBuilderVisitor() {
+ this.parts = [];
+ }
+
+ ExpressionBuilderVisitor.prototype = {
+ visitArgument: function visitArgument(arg) {
+ this.parts.push('Math.max(', arg.min, ', Math.min(', arg.max, ', src[srcOffset + ', arg.index, ']))');
+ },
+ visitVariable: function visitVariable(variable) {
+ this.parts.push('v', variable.index);
+ },
+ visitLiteral: function visitLiteral(literal) {
+ this.parts.push(literal.number);
+ },
+ visitBinaryOperation: function visitBinaryOperation(operation) {
+ this.parts.push('(');
+ operation.arg1.visit(this);
+ this.parts.push(' ', operation.op, ' ');
+ operation.arg2.visit(this);
+ this.parts.push(')');
+ },
+ visitVariableDefinition: function visitVariableDefinition(definition) {
+ this.parts.push('var ');
+ definition.variable.visit(this);
+ this.parts.push(' = ');
+ definition.arg.visit(this);
+ this.parts.push(';');
+ },
+ visitMin: function visitMin(max) {
+ this.parts.push('Math.min(');
+ max.arg.visit(this);
+ this.parts.push(', ', max.max, ')');
+ },
+ toString: function toString() {
+ return this.parts.join('');
+ }
+ };
+
+ function buildAddOperation(num1, num2) {
+ if (num2.type === 'literal' && num2.number === 0) {
+ return num1;
+ }
+
+ if (num1.type === 'literal' && num1.number === 0) {
+ return num2;
+ }
+
+ if (num2.type === 'literal' && num1.type === 'literal') {
+ return new AstLiteral(num1.number + num2.number);
+ }
+
+ return new AstBinaryOperation('+', num1, num2, num1.min + num2.min, num1.max + num2.max);
+ }
+
+ function buildMulOperation(num1, num2) {
+ if (num2.type === 'literal') {
+ if (num2.number === 0) {
+ return new AstLiteral(0);
+ } else if (num2.number === 1) {
+ return num1;
+ } else if (num1.type === 'literal') {
+ return new AstLiteral(num1.number * num2.number);
+ }
+ }
+
+ if (num1.type === 'literal') {
+ if (num1.number === 0) {
+ return new AstLiteral(0);
+ } else if (num1.number === 1) {
+ return num2;
+ }
+ }
+
+ var min = Math.min(num1.min * num2.min, num1.min * num2.max, num1.max * num2.min, num1.max * num2.max);
+ var max = Math.max(num1.min * num2.min, num1.min * num2.max, num1.max * num2.min, num1.max * num2.max);
+ return new AstBinaryOperation('*', num1, num2, min, max);
+ }
+
+ function buildSubOperation(num1, num2) {
+ if (num2.type === 'literal') {
+ if (num2.number === 0) {
+ return num1;
+ } else if (num1.type === 'literal') {
+ return new AstLiteral(num1.number - num2.number);
+ }
+ }
+
+ if (num2.type === 'binary' && num2.op === '-' && num1.type === 'literal' && num1.number === 1 && num2.arg1.type === 'literal' && num2.arg1.number === 1) {
+ return num2.arg2;
+ }
+
+ return new AstBinaryOperation('-', num1, num2, num1.min - num2.max, num1.max - num2.min);
+ }
+
+ function buildMinOperation(num1, max) {
+ if (num1.min >= max) {
+ return new AstLiteral(max);
+ } else if (num1.max <= max) {
+ return num1;
+ }
+
+ return new AstMin(num1, max);
+ }
+
+ function PostScriptCompiler() {}
+
+ PostScriptCompiler.prototype = {
+ compile: function PostScriptCompiler_compile(code, domain, range) {
+ var stack = [];
+ var i, ii;
+ var instructions = [];
+ var inputSize = domain.length >> 1,
+ outputSize = range.length >> 1;
+ var lastRegister = 0;
+ var n, j;
+ var num1, num2, ast1, ast2, tmpVar, item;
+
+ for (i = 0; i < inputSize; i++) {
+ stack.push(new AstArgument(i, domain[i * 2], domain[i * 2 + 1]));
+ }
+
+ for (i = 0, ii = code.length; i < ii; i++) {
+ item = code[i];
+
+ if (typeof item === 'number') {
+ stack.push(new AstLiteral(item));
+ continue;
+ }
+
+ switch (item) {
+ case 'add':
+ if (stack.length < 2) {
+ return null;
+ }
+
+ num2 = stack.pop();
+ num1 = stack.pop();
+ stack.push(buildAddOperation(num1, num2));
+ break;
+
+ case 'cvr':
+ if (stack.length < 1) {
+ return null;
+ }
+
+ break;
+
+ case 'mul':
+ if (stack.length < 2) {
+ return null;
+ }
+
+ num2 = stack.pop();
+ num1 = stack.pop();
+ stack.push(buildMulOperation(num1, num2));
+ break;
+
+ case 'sub':
+ if (stack.length < 2) {
+ return null;
+ }
+
+ num2 = stack.pop();
+ num1 = stack.pop();
+ stack.push(buildSubOperation(num1, num2));
+ break;
+
+ case 'exch':
+ if (stack.length < 2) {
+ return null;
+ }
+
+ ast1 = stack.pop();
+ ast2 = stack.pop();
+ stack.push(ast1, ast2);
+ break;
+
+ case 'pop':
+ if (stack.length < 1) {
+ return null;
+ }
+
+ stack.pop();
+ break;
+
+ case 'index':
+ if (stack.length < 1) {
+ return null;
+ }
+
+ num1 = stack.pop();
+
+ if (num1.type !== 'literal') {
+ return null;
+ }
+
+ n = num1.number;
+
+ if (n < 0 || !Number.isInteger(n) || stack.length < n) {
+ return null;
+ }
+
+ ast1 = stack[stack.length - n - 1];
+
+ if (ast1.type === 'literal' || ast1.type === 'var') {
+ stack.push(ast1);
+ break;
+ }
+
+ tmpVar = new AstVariable(lastRegister++, ast1.min, ast1.max);
+ stack[stack.length - n - 1] = tmpVar;
+ stack.push(tmpVar);
+ instructions.push(new AstVariableDefinition(tmpVar, ast1));
+ break;
+
+ case 'dup':
+ if (stack.length < 1) {
+ return null;
+ }
+
+ if (typeof code[i + 1] === 'number' && code[i + 2] === 'gt' && code[i + 3] === i + 7 && code[i + 4] === 'jz' && code[i + 5] === 'pop' && code[i + 6] === code[i + 1]) {
+ num1 = stack.pop();
+ stack.push(buildMinOperation(num1, code[i + 1]));
+ i += 6;
+ break;
+ }
+
+ ast1 = stack[stack.length - 1];
+
+ if (ast1.type === 'literal' || ast1.type === 'var') {
+ stack.push(ast1);
+ break;
+ }
+
+ tmpVar = new AstVariable(lastRegister++, ast1.min, ast1.max);
+ stack[stack.length - 1] = tmpVar;
+ stack.push(tmpVar);
+ instructions.push(new AstVariableDefinition(tmpVar, ast1));
+ break;
+
+ case 'roll':
+ if (stack.length < 2) {
+ return null;
+ }
+
+ num2 = stack.pop();
+ num1 = stack.pop();
+
+ if (num2.type !== 'literal' || num1.type !== 'literal') {
+ return null;
+ }
+
+ j = num2.number;
+ n = num1.number;
+
+ if (n <= 0 || !Number.isInteger(n) || !Number.isInteger(j) || stack.length < n) {
+ return null;
+ }
+
+ j = (j % n + n) % n;
+
+ if (j === 0) {
+ break;
+ }
+
+ Array.prototype.push.apply(stack, stack.splice(stack.length - n, n - j));
+ break;
+
+ default:
+ return null;
+ }
+ }
+
+ if (stack.length !== outputSize) {
+ return null;
+ }
+
+ var result = [];
+ instructions.forEach(function (instruction) {
+ var statementBuilder = new ExpressionBuilderVisitor();
+ instruction.visit(statementBuilder);
+ result.push(statementBuilder.toString());
+ });
+ stack.forEach(function (expr, i) {
+ var statementBuilder = new ExpressionBuilderVisitor();
+ expr.visit(statementBuilder);
+ var min = range[i * 2],
+ max = range[i * 2 + 1];
+ var out = [statementBuilder.toString()];
+
+ if (min > expr.min) {
+ out.unshift('Math.max(', min, ', ');
+ out.push(')');
+ }
+
+ if (max < expr.max) {
+ out.unshift('Math.min(', max, ', ');
+ out.push(')');
+ }
+
+ out.unshift('dest[destOffset + ', i, '] = ');
+ out.push(';');
+ result.push(out.join(''));
+ });
+ return result.join('\n');
+ }
+ };
+ return PostScriptCompiler;
+}();
+
+exports.PostScriptCompiler = PostScriptCompiler;
+
+/***/ }),
+/* 187 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.PostScriptParser = exports.PostScriptLexer = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _primitives = __w_pdfjs_require__(151);
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var PostScriptParser =
+/*#__PURE__*/
+function () {
+ function PostScriptParser(lexer) {
+ _classCallCheck(this, PostScriptParser);
+
+ this.lexer = lexer;
+ this.operators = [];
+ this.token = null;
+ this.prev = null;
+ }
+
+ _createClass(PostScriptParser, [{
+ key: "nextToken",
+ value: function nextToken() {
+ this.prev = this.token;
+ this.token = this.lexer.getToken();
+ }
+ }, {
+ key: "accept",
+ value: function accept(type) {
+ if (this.token.type === type) {
+ this.nextToken();
+ return true;
+ }
+
+ return false;
+ }
+ }, {
+ key: "expect",
+ value: function expect(type) {
+ if (this.accept(type)) {
+ return true;
+ }
+
+ throw new _util.FormatError("Unexpected symbol: found ".concat(this.token.type, " expected ").concat(type, "."));
+ }
+ }, {
+ key: "parse",
+ value: function parse() {
+ this.nextToken();
+ this.expect(PostScriptTokenTypes.LBRACE);
+ this.parseBlock();
+ this.expect(PostScriptTokenTypes.RBRACE);
+ return this.operators;
+ }
+ }, {
+ key: "parseBlock",
+ value: function parseBlock() {
+ while (true) {
+ if (this.accept(PostScriptTokenTypes.NUMBER)) {
+ this.operators.push(this.prev.value);
+ } else if (this.accept(PostScriptTokenTypes.OPERATOR)) {
+ this.operators.push(this.prev.value);
+ } else if (this.accept(PostScriptTokenTypes.LBRACE)) {
+ this.parseCondition();
+ } else {
+ return;
+ }
+ }
+ }
+ }, {
+ key: "parseCondition",
+ value: function parseCondition() {
+ var conditionLocation = this.operators.length;
+ this.operators.push(null, null);
+ this.parseBlock();
+ this.expect(PostScriptTokenTypes.RBRACE);
+
+ if (this.accept(PostScriptTokenTypes.IF)) {
+ this.operators[conditionLocation] = this.operators.length;
+ this.operators[conditionLocation + 1] = 'jz';
+ } else if (this.accept(PostScriptTokenTypes.LBRACE)) {
+ var jumpLocation = this.operators.length;
+ this.operators.push(null, null);
+ var endOfTrue = this.operators.length;
+ this.parseBlock();
+ this.expect(PostScriptTokenTypes.RBRACE);
+ this.expect(PostScriptTokenTypes.IFELSE);
+ this.operators[jumpLocation] = this.operators.length;
+ this.operators[jumpLocation + 1] = 'j';
+ this.operators[conditionLocation] = endOfTrue;
+ this.operators[conditionLocation + 1] = 'jz';
+ } else {
+ throw new _util.FormatError('PS Function: error parsing conditional.');
+ }
+ }
+ }]);
+
+ return PostScriptParser;
+}();
+
+exports.PostScriptParser = PostScriptParser;
+var PostScriptTokenTypes = {
+ LBRACE: 0,
+ RBRACE: 1,
+ NUMBER: 2,
+ OPERATOR: 3,
+ IF: 4,
+ IFELSE: 5
+};
+
+var PostScriptToken = function PostScriptTokenClosure() {
+ var opCache = Object.create(null);
+
+ var PostScriptToken =
+ /*#__PURE__*/
+ function () {
+ function PostScriptToken(type, value) {
+ _classCallCheck(this, PostScriptToken);
+
+ this.type = type;
+ this.value = value;
+ }
+
+ _createClass(PostScriptToken, null, [{
+ key: "getOperator",
+ value: function getOperator(op) {
+ var opValue = opCache[op];
+
+ if (opValue) {
+ return opValue;
+ }
+
+ return opCache[op] = new PostScriptToken(PostScriptTokenTypes.OPERATOR, op);
+ }
+ }, {
+ key: "LBRACE",
+ get: function get() {
+ return (0, _util.shadow)(this, 'LBRACE', new PostScriptToken(PostScriptTokenTypes.LBRACE, '{'));
+ }
+ }, {
+ key: "RBRACE",
+ get: function get() {
+ return (0, _util.shadow)(this, 'RBRACE', new PostScriptToken(PostScriptTokenTypes.RBRACE, '}'));
+ }
+ }, {
+ key: "IF",
+ get: function get() {
+ return (0, _util.shadow)(this, 'IF', new PostScriptToken(PostScriptTokenTypes.IF, 'IF'));
+ }
+ }, {
+ key: "IFELSE",
+ get: function get() {
+ return (0, _util.shadow)(this, 'IFELSE', new PostScriptToken(PostScriptTokenTypes.IFELSE, 'IFELSE'));
+ }
+ }]);
+
+ return PostScriptToken;
+ }();
+
+ return PostScriptToken;
+}();
+
+var PostScriptLexer =
+/*#__PURE__*/
+function () {
+ function PostScriptLexer(stream) {
+ _classCallCheck(this, PostScriptLexer);
+
+ this.stream = stream;
+ this.nextChar();
+ this.strBuf = [];
+ }
+
+ _createClass(PostScriptLexer, [{
+ key: "nextChar",
+ value: function nextChar() {
+ return this.currentChar = this.stream.getByte();
+ }
+ }, {
+ key: "getToken",
+ value: function getToken() {
+ var comment = false;
+ var ch = this.currentChar;
+
+ while (true) {
+ if (ch < 0) {
+ return _primitives.EOF;
+ }
+
+ if (comment) {
+ if (ch === 0x0A || ch === 0x0D) {
+ comment = false;
+ }
+ } else if (ch === 0x25) {
+ comment = true;
+ } else if (!(0, _util.isSpace)(ch)) {
+ break;
+ }
+
+ ch = this.nextChar();
+ }
+
+ switch (ch | 0) {
+ case 0x30:
+ case 0x31:
+ case 0x32:
+ case 0x33:
+ case 0x34:
+ case 0x35:
+ case 0x36:
+ case 0x37:
+ case 0x38:
+ case 0x39:
+ case 0x2B:
+ case 0x2D:
+ case 0x2E:
+ return new PostScriptToken(PostScriptTokenTypes.NUMBER, this.getNumber());
+
+ case 0x7B:
+ this.nextChar();
+ return PostScriptToken.LBRACE;
+
+ case 0x7D:
+ this.nextChar();
+ return PostScriptToken.RBRACE;
+ }
+
+ var strBuf = this.strBuf;
+ strBuf.length = 0;
+ strBuf[0] = String.fromCharCode(ch);
+
+ while ((ch = this.nextChar()) >= 0 && (ch >= 0x41 && ch <= 0x5A || ch >= 0x61 && ch <= 0x7A)) {
+ strBuf.push(String.fromCharCode(ch));
+ }
+
+ var str = strBuf.join('');
+
+ switch (str.toLowerCase()) {
+ case 'if':
+ return PostScriptToken.IF;
+
+ case 'ifelse':
+ return PostScriptToken.IFELSE;
+
+ default:
+ return PostScriptToken.getOperator(str);
+ }
+ }
+ }, {
+ key: "getNumber",
+ value: function getNumber() {
+ var ch = this.currentChar;
+ var strBuf = this.strBuf;
+ strBuf.length = 0;
+ strBuf[0] = String.fromCharCode(ch);
+
+ while ((ch = this.nextChar()) >= 0) {
+ if (ch >= 0x30 && ch <= 0x39 || ch === 0x2D || ch === 0x2E) {
+ strBuf.push(String.fromCharCode(ch));
+ } else {
+ break;
+ }
+ }
+
+ var value = parseFloat(strBuf.join(''));
+
+ if (isNaN(value)) {
+ throw new _util.FormatError("Invalid floating point number: ".concat(value));
+ }
+
+ return value;
+ }
+ }]);
+
+ return PostScriptLexer;
+}();
+
+exports.PostScriptLexer = PostScriptLexer;
+
+/***/ }),
+/* 188 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.MurmurHash3_64 = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var SEED = 0xc3d2e1f0;
+var MASK_HIGH = 0xffff0000;
+var MASK_LOW = 0xffff;
+
+var MurmurHash3_64 =
+/*#__PURE__*/
+function () {
+ function MurmurHash3_64(seed) {
+ _classCallCheck(this, MurmurHash3_64);
+
+ this.h1 = seed ? seed & 0xffffffff : SEED;
+ this.h2 = seed ? seed & 0xffffffff : SEED;
+ }
+
+ _createClass(MurmurHash3_64, [{
+ key: "update",
+ value: function update(input) {
+ var data, length;
+
+ if ((0, _util.isString)(input)) {
+ data = new Uint8Array(input.length * 2);
+ length = 0;
+
+ for (var i = 0, ii = input.length; i < ii; i++) {
+ var code = input.charCodeAt(i);
+
+ if (code <= 0xff) {
+ data[length++] = code;
+ } else {
+ data[length++] = code >>> 8;
+ data[length++] = code & 0xff;
+ }
+ }
+ } else if ((0, _util.isArrayBuffer)(input)) {
+ data = input;
+ length = data.byteLength;
+ } else {
+ throw new Error('Wrong data format in MurmurHash3_64_update. ' + 'Input must be a string or array.');
+ }
+
+ var blockCounts = length >> 2;
+ var tailLength = length - blockCounts * 4;
+ var dataUint32 = new Uint32Array(data.buffer, 0, blockCounts);
+ var k1 = 0,
+ k2 = 0;
+ var h1 = this.h1,
+ h2 = this.h2;
+ var C1 = 0xcc9e2d51,
+ C2 = 0x1b873593;
+ var C1_LOW = C1 & MASK_LOW,
+ C2_LOW = C2 & MASK_LOW;
+
+ for (var _i = 0; _i < blockCounts; _i++) {
+ if (_i & 1) {
+ k1 = dataUint32[_i];
+ k1 = k1 * C1 & MASK_HIGH | k1 * C1_LOW & MASK_LOW;
+ k1 = k1 << 15 | k1 >>> 17;
+ k1 = k1 * C2 & MASK_HIGH | k1 * C2_LOW & MASK_LOW;
+ h1 ^= k1;
+ h1 = h1 << 13 | h1 >>> 19;
+ h1 = h1 * 5 + 0xe6546b64;
+ } else {
+ k2 = dataUint32[_i];
+ k2 = k2 * C1 & MASK_HIGH | k2 * C1_LOW & MASK_LOW;
+ k2 = k2 << 15 | k2 >>> 17;
+ k2 = k2 * C2 & MASK_HIGH | k2 * C2_LOW & MASK_LOW;
+ h2 ^= k2;
+ h2 = h2 << 13 | h2 >>> 19;
+ h2 = h2 * 5 + 0xe6546b64;
+ }
+ }
+
+ k1 = 0;
+
+ switch (tailLength) {
+ case 3:
+ k1 ^= data[blockCounts * 4 + 2] << 16;
+
+ case 2:
+ k1 ^= data[blockCounts * 4 + 1] << 8;
+
+ case 1:
+ k1 ^= data[blockCounts * 4];
+ k1 = k1 * C1 & MASK_HIGH | k1 * C1_LOW & MASK_LOW;
+ k1 = k1 << 15 | k1 >>> 17;
+ k1 = k1 * C2 & MASK_HIGH | k1 * C2_LOW & MASK_LOW;
+
+ if (blockCounts & 1) {
+ h1 ^= k1;
+ } else {
+ h2 ^= k1;
+ }
+
+ }
+
+ this.h1 = h1;
+ this.h2 = h2;
+ }
+ }, {
+ key: "hexdigest",
+ value: function hexdigest() {
+ var h1 = this.h1,
+ h2 = this.h2;
+ h1 ^= h2 >>> 1;
+ h1 = h1 * 0xed558ccd & MASK_HIGH | h1 * 0x8ccd & MASK_LOW;
+ h2 = h2 * 0xff51afd7 & MASK_HIGH | ((h2 << 16 | h1 >>> 16) * 0xafd7ed55 & MASK_HIGH) >>> 16;
+ h1 ^= h2 >>> 1;
+ h1 = h1 * 0x1a85ec53 & MASK_HIGH | h1 * 0xec53 & MASK_LOW;
+ h2 = h2 * 0xc4ceb9fe & MASK_HIGH | ((h2 << 16 | h1 >>> 16) * 0xb9fe1a85 & MASK_HIGH) >>> 16;
+ h1 ^= h2 >>> 1;
+ var hex1 = (h1 >>> 0).toString(16),
+ hex2 = (h2 >>> 0).toString(16);
+ return hex1.padStart(8, '0') + hex2.padStart(8, '0');
+ }
+ }]);
+
+ return MurmurHash3_64;
+}();
+
+exports.MurmurHash3_64 = MurmurHash3_64;
+
+/***/ }),
+/* 189 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.NativeImageDecoder = void 0;
+
+var _colorspace = __w_pdfjs_require__(169);
+
+var _jpeg_stream = __w_pdfjs_require__(164);
+
+var _stream = __w_pdfjs_require__(158);
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var NativeImageDecoder =
+/*#__PURE__*/
+function () {
+ function NativeImageDecoder(_ref) {
+ var xref = _ref.xref,
+ resources = _ref.resources,
+ handler = _ref.handler,
+ _ref$forceDataSchema = _ref.forceDataSchema,
+ forceDataSchema = _ref$forceDataSchema === void 0 ? false : _ref$forceDataSchema,
+ pdfFunctionFactory = _ref.pdfFunctionFactory;
+
+ _classCallCheck(this, NativeImageDecoder);
+
+ this.xref = xref;
+ this.resources = resources;
+ this.handler = handler;
+ this.forceDataSchema = forceDataSchema;
+ this.pdfFunctionFactory = pdfFunctionFactory;
+ }
+
+ _createClass(NativeImageDecoder, [{
+ key: "canDecode",
+ value: function canDecode(image) {
+ return image instanceof _jpeg_stream.JpegStream && NativeImageDecoder.isDecodable(image, this.xref, this.resources, this.pdfFunctionFactory);
+ }
+ }, {
+ key: "decode",
+ value: function decode(image) {
+ var dict = image.dict;
+ var colorSpace = dict.get('ColorSpace', 'CS');
+ colorSpace = _colorspace.ColorSpace.parse(colorSpace, this.xref, this.resources, this.pdfFunctionFactory);
+ return this.handler.sendWithPromise('JpegDecode', [image.getIR(this.forceDataSchema), colorSpace.numComps]).then(function (_ref2) {
+ var data = _ref2.data,
+ width = _ref2.width,
+ height = _ref2.height;
+ return new _stream.Stream(data, 0, data.length, dict);
+ });
+ }
+ }], [{
+ key: "isSupported",
+ value: function isSupported(image, xref, res, pdfFunctionFactory) {
+ var dict = image.dict;
+
+ if (dict.has('DecodeParms') || dict.has('DP')) {
+ return false;
+ }
+
+ var cs = _colorspace.ColorSpace.parse(dict.get('ColorSpace', 'CS'), xref, res, pdfFunctionFactory);
+
+ return (cs.name === 'DeviceGray' || cs.name === 'DeviceRGB') && cs.isDefaultDecode(dict.getArray('Decode', 'D'));
+ }
+ }, {
+ key: "isDecodable",
+ value: function isDecodable(image, xref, res, pdfFunctionFactory) {
+ var dict = image.dict;
+
+ if (dict.has('DecodeParms') || dict.has('DP')) {
+ return false;
+ }
+
+ var cs = _colorspace.ColorSpace.parse(dict.get('ColorSpace', 'CS'), xref, res, pdfFunctionFactory);
+
+ var bpc = dict.get('BitsPerComponent', 'BPC') || 1;
+ return (cs.numComps === 1 || cs.numComps === 3) && cs.isDefaultDecode(dict.getArray('Decode', 'D'), bpc);
+ }
+ }]);
+
+ return NativeImageDecoder;
+}();
+
+exports.NativeImageDecoder = NativeImageDecoder;
+
+/***/ }),
+/* 190 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.PDFImage = void 0;
+
+var _util = __w_pdfjs_require__(5);
+
+var _primitives = __w_pdfjs_require__(151);
+
+var _colorspace = __w_pdfjs_require__(169);
+
+var _stream = __w_pdfjs_require__(158);
+
+var _jpeg_stream = __w_pdfjs_require__(164);
+
+var _jpx = __w_pdfjs_require__(167);
+
+function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
+
+function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
+
+function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
+
+function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
+
+var PDFImage = function PDFImageClosure() {
+ function handleImageData(image, nativeDecoder) {
+ if (nativeDecoder && nativeDecoder.canDecode(image)) {
+ return nativeDecoder.decode(image)["catch"](function (reason) {
+ (0, _util.warn)('Native image decoding failed -- trying to recover: ' + (reason && reason.message));
+ return image;
+ });
+ }
+
+ return Promise.resolve(image);
+ }
+
+ function decodeAndClamp(value, addend, coefficient, max) {
+ value = addend + value * coefficient;
+ return value < 0 ? 0 : value > max ? max : value;
+ }
+
+ function resizeImageMask(src, bpc, w1, h1, w2, h2) {
+ var length = w2 * h2;
+ var dest = bpc <= 8 ? new Uint8Array(length) : bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length);
+ var xRatio = w1 / w2;
+ var yRatio = h1 / h2;
+ var i,
+ j,
+ py,
+ newIndex = 0,
+ oldIndex;
+ var xScaled = new Uint16Array(w2);
+ var w1Scanline = w1;
+
+ for (i = 0; i < w2; i++) {
+ xScaled[i] = Math.floor(i * xRatio);
+ }
+
+ for (i = 0; i < h2; i++) {
+ py = Math.floor(i * yRatio) * w1Scanline;
+
+ for (j = 0; j < w2; j++) {
+ oldIndex = py + xScaled[j];
+ dest[newIndex++] = src[oldIndex];
+ }
+ }
+
+ return dest;
+ }
+
+ function PDFImage(_ref) {
+ var xref = _ref.xref,
+ res = _ref.res,
+ image = _ref.image,
+ _ref$isInline = _ref.isInline,
+ isInline = _ref$isInline === void 0 ? false : _ref$isInline,
+ _ref$smask = _ref.smask,
+ smask = _ref$smask === void 0 ? null : _ref$smask,
+ _ref$mask = _ref.mask,
+ mask = _ref$mask === void 0 ? null : _ref$mask,
+ _ref$isMask = _ref.isMask,
+ isMask = _ref$isMask === void 0 ? false : _ref$isMask,
+ pdfFunctionFactory = _ref.pdfFunctionFactory;
+ this.image = image;
+ var dict = image.dict;
+ var filter = dict.get('Filter');
+
+ if ((0, _primitives.isName)(filter)) {
+ switch (filter.name) {
+ case 'JPXDecode':
+ var jpxImage = new _jpx.JpxImage();
+ jpxImage.parseImageProperties(image.stream);
+ image.stream.reset();
+ image.width = jpxImage.width;
+ image.height = jpxImage.height;
+ image.bitsPerComponent = jpxImage.bitsPerComponent;
+ image.numComps = jpxImage.componentsCount;
+ break;
+
+ case 'JBIG2Decode':
+ image.bitsPerComponent = 1;
+ image.numComps = 1;
+ break;
+ }
+ }
+
+ var width = dict.get('Width', 'W');
+ var height = dict.get('Height', 'H');
+
+ if (Number.isInteger(image.width) && image.width > 0 && Number.isInteger(image.height) && image.height > 0 && (image.width !== width || image.height !== height)) {
+ (0, _util.warn)('PDFImage - using the Width/Height of the image data, ' + 'rather than the image dictionary.');
+ width = image.width;
+ height = image.height;
+ }
+
+ if (width < 1 || height < 1) {
+ throw new _util.FormatError("Invalid image width: ".concat(width, " or ") + "height: ".concat(height));
+ }
+
+ this.width = width;
+ this.height = height;
+ this.interpolate = dict.get('Interpolate', 'I') || false;
+ this.imageMask = dict.get('ImageMask', 'IM') || false;
+ this.matte = dict.get('Matte') || false;
+ var bitsPerComponent = image.bitsPerComponent;
+
+ if (!bitsPerComponent) {
+ bitsPerComponent = dict.get('BitsPerComponent', 'BPC');
+
+ if (!bitsPerComponent) {
+ if (this.imageMask) {
+ bitsPerComponent = 1;
+ } else {
+ throw new _util.FormatError("Bits per component missing in image: ".concat(this.imageMask));
+ }
+ }
+ }
+
+ this.bpc = bitsPerComponent;
+
+ if (!this.imageMask) {
+ var colorSpace = dict.get('ColorSpace', 'CS');
+
+ if (!colorSpace) {
+ (0, _util.info)('JPX images (which do not require color spaces)');
+
+ switch (image.numComps) {
+ case 1:
+ colorSpace = _primitives.Name.get('DeviceGray');
+ break;
+
+ case 3:
+ colorSpace = _primitives.Name.get('DeviceRGB');
+ break;
+
+ case 4:
+ colorSpace = _primitives.Name.get('DeviceCMYK');
+ break;
+
+ default:
+ throw new Error("JPX images with ".concat(image.numComps, " ") + 'color components not supported.');
+ }
+ }
+
+ var resources = isInline ? res : null;
+ this.colorSpace = _colorspace.ColorSpace.parse(colorSpace, xref, resources, pdfFunctionFactory);
+ this.numComps = this.colorSpace.numComps;
+ }
+
+ this.decode = dict.getArray('Decode', 'D');
+ this.needsDecode = false;
+
+ if (this.decode && (this.colorSpace && !this.colorSpace.isDefaultDecode(this.decode, bitsPerComponent) || isMask && !_colorspace.ColorSpace.isDefaultDecode(this.decode, 1))) {
+ this.needsDecode = true;
+ var max = (1 << bitsPerComponent) - 1;
+ this.decodeCoefficients = [];
+ this.decodeAddends = [];
+ var isIndexed = this.colorSpace && this.colorSpace.name === 'Indexed';
+
+ for (var i = 0, j = 0; i < this.decode.length; i += 2, ++j) {
+ var dmin = this.decode[i];
+ var dmax = this.decode[i + 1];
+ this.decodeCoefficients[j] = isIndexed ? (dmax - dmin) / max : dmax - dmin;
+ this.decodeAddends[j] = isIndexed ? dmin : max * dmin;
+ }
+ }
+
+ if (smask) {
+ this.smask = new PDFImage({
+ xref: xref,
+ res: res,
+ image: smask,
+ isInline: isInline,
+ pdfFunctionFactory: pdfFunctionFactory
+ });
+ } else if (mask) {
+ if ((0, _primitives.isStream)(mask)) {
+ var maskDict = mask.dict,
+ imageMask = maskDict.get('ImageMask', 'IM');
+
+ if (!imageMask) {
+ (0, _util.warn)('Ignoring /Mask in image without /ImageMask.');
+ } else {
+ this.mask = new PDFImage({
+ xref: xref,
+ res: res,
+ image: mask,
+ isInline: isInline,
+ isMask: true,
+ pdfFunctionFactory: pdfFunctionFactory
+ });
+ }
+ } else {
+ this.mask = mask;
+ }
+ }
+ }
+
+ PDFImage.buildImage = function (_ref2) {
+ var handler = _ref2.handler,
+ xref = _ref2.xref,
+ res = _ref2.res,
+ image = _ref2.image,
+ _ref2$isInline = _ref2.isInline,
+ isInline = _ref2$isInline === void 0 ? false : _ref2$isInline,
+ _ref2$nativeDecoder = _ref2.nativeDecoder,
+ nativeDecoder = _ref2$nativeDecoder === void 0 ? null : _ref2$nativeDecoder,
+ pdfFunctionFactory = _ref2.pdfFunctionFactory;
+ var imagePromise = handleImageData(image, nativeDecoder);
+ var smaskPromise;
+ var maskPromise;
+ var smask = image.dict.get('SMask');
+ var mask = image.dict.get('Mask');
+
+ if (smask) {
+ smaskPromise = handleImageData(smask, nativeDecoder);
+ maskPromise = Promise.resolve(null);
+ } else {
+ smaskPromise = Promise.resolve(null);
+
+ if (mask) {
+ if ((0, _primitives.isStream)(mask)) {
+ maskPromise = handleImageData(mask, nativeDecoder);
+ } else if (Array.isArray(mask)) {
+ maskPromise = Promise.resolve(mask);
+ } else {
+ (0, _util.warn)('Unsupported mask format.');
+ maskPromise = Promise.resolve(null);
+ }
+ } else {
+ maskPromise = Promise.resolve(null);
+ }
+ }
+
+ return Promise.all([imagePromise, smaskPromise, maskPromise]).then(function (_ref3) {
+ var _ref4 = _slicedToArray(_ref3, 3),
+ imageData = _ref4[0],
+ smaskData = _ref4[1],
+ maskData = _ref4[2];
+
+ return new PDFImage({
+ xref: xref,
+ res: res,
+ image: imageData,
+ isInline: isInline,
+ smask: smaskData,
+ mask: maskData,
+ pdfFunctionFactory: pdfFunctionFactory
+ });
+ });
+ };
+
+ PDFImage.createMask = function (_ref5) {
+ var imgArray = _ref5.imgArray,
+ width = _ref5.width,
+ height = _ref5.height,
+ imageIsFromDecodeStream = _ref5.imageIsFromDecodeStream,
+ inverseDecode = _ref5.inverseDecode;
+ var computedLength = (width + 7 >> 3) * height;
+ var actualLength = imgArray.byteLength;
+ var haveFullData = computedLength === actualLength;
+ var data, i;
+
+ if (imageIsFromDecodeStream && (!inverseDecode || haveFullData)) {
+ data = imgArray;
+ } else if (!inverseDecode) {
+ data = new Uint8ClampedArray(actualLength);
+ data.set(imgArray);
+ } else {
+ data = new Uint8ClampedArray(computedLength);
+ data.set(imgArray);
+
+ for (i = actualLength; i < computedLength; i++) {
+ data[i] = 0xff;
+ }
+ }
+
+ if (inverseDecode) {
+ for (i = 0; i < actualLength; i++) {
+ data[i] ^= 0xFF;
+ }
+ }
+
+ return {
+ data: data,
+ width: width,
+ height: height
+ };
+ };
+
+ PDFImage.prototype = {
+ get drawWidth() {
+ return Math.max(this.width, this.smask && this.smask.width || 0, this.mask && this.mask.width || 0);
+ },
+
+ get drawHeight() {
+ return Math.max(this.height, this.smask && this.smask.height || 0, this.mask && this.mask.height || 0);
+ },
+
+ decodeBuffer: function decodeBuffer(buffer) {
+ var bpc = this.bpc;
+ var numComps = this.numComps;
+ var decodeAddends = this.decodeAddends;
+ var decodeCoefficients = this.decodeCoefficients;
+ var max = (1 << bpc) - 1;
+ var i, ii;
+
+ if (bpc === 1) {
+ for (i = 0, ii = buffer.length; i < ii; i++) {
+ buffer[i] = +!buffer[i];
+ }
+
+ return;
+ }
+
+ var index = 0;
+
+ for (i = 0, ii = this.width * this.height; i < ii; i++) {
+ for (var j = 0; j < numComps; j++) {
+ buffer[index] = decodeAndClamp(buffer[index], decodeAddends[j], decodeCoefficients[j], max);
+ index++;
+ }
+ }
+ },
+ getComponents: function getComponents(buffer) {
+ var bpc = this.bpc;
+
+ if (bpc === 8) {
+ return buffer;
+ }
+
+ var width = this.width;
+ var height = this.height;
+ var numComps = this.numComps;
+ var length = width * height * numComps;
+ var bufferPos = 0;
+ var output = bpc <= 8 ? new Uint8Array(length) : bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length);
+ var rowComps = width * numComps;
+ var max = (1 << bpc) - 1;
+ var i = 0,
+ ii,
+ buf;
+
+ if (bpc === 1) {
+ var mask, loop1End, loop2End;
+
+ for (var j = 0; j < height; j++) {
+ loop1End = i + (rowComps & ~7);
+ loop2End = i + rowComps;
+
+ while (i < loop1End) {
+ buf = buffer[bufferPos++];
+ output[i] = buf >> 7 & 1;
+ output[i + 1] = buf >> 6 & 1;
+ output[i + 2] = buf >> 5 & 1;
+ output[i + 3] = buf >> 4 & 1;
+ output[i + 4] = buf >> 3 & 1;
+ output[i + 5] = buf >> 2 & 1;
+ output[i + 6] = buf >> 1 & 1;
+ output[i + 7] = buf & 1;
+ i += 8;
+ }
+
+ if (i < loop2End) {
+ buf = buffer[bufferPos++];
+ mask = 128;
+
+ while (i < loop2End) {
+ output[i++] = +!!(buf & mask);
+ mask >>= 1;
+ }
+ }
+ }
+ } else {
+ var bits = 0;
+ buf = 0;
+
+ for (i = 0, ii = length; i < ii; ++i) {
+ if (i % rowComps === 0) {
+ buf = 0;
+ bits = 0;
+ }
+
+ while (bits < bpc) {
+ buf = buf << 8 | buffer[bufferPos++];
+ bits += 8;
+ }
+
+ var remainingBits = bits - bpc;
+ var value = buf >> remainingBits;
+ output[i] = value < 0 ? 0 : value > max ? max : value;
+ buf = buf & (1 << remainingBits) - 1;
+ bits = remainingBits;
+ }
+ }
+
+ return output;
+ },
+ fillOpacity: function fillOpacity(rgbaBuf, width, height, actualHeight, image) {
+ var smask = this.smask;
+ var mask = this.mask;
+ var alphaBuf, sw, sh, i, ii, j;
+
+ if (smask) {
+ sw = smask.width;
+ sh = smask.height;
+ alphaBuf = new Uint8ClampedArray(sw * sh);
+ smask.fillGrayBuffer(alphaBuf);
+
+ if (sw !== width || sh !== height) {
+ alphaBuf = resizeImageMask(alphaBuf, smask.bpc, sw, sh, width, height);
+ }
+ } else if (mask) {
+ if (mask instanceof PDFImage) {
+ sw = mask.width;
+ sh = mask.height;
+ alphaBuf = new Uint8ClampedArray(sw * sh);
+ mask.numComps = 1;
+ mask.fillGrayBuffer(alphaBuf);
+
+ for (i = 0, ii = sw * sh; i < ii; ++i) {
+ alphaBuf[i] = 255 - alphaBuf[i];
+ }
+
+ if (sw !== width || sh !== height) {
+ alphaBuf = resizeImageMask(alphaBuf, mask.bpc, sw, sh, width, height);
+ }
+ } else if (Array.isArray(mask)) {
+ alphaBuf = new Uint8ClampedArray(width * height);
+ var numComps = this.numComps;
+
+ for (i = 0, ii = width * height; i < ii; ++i) {
+ var opacity = 0;
+ var imageOffset = i * numComps;
+
+ for (j = 0; j < numComps; ++j) {
+ var color = image[imageOffset + j];
+ var maskOffset = j * 2;
+
+ if (color < mask[maskOffset] || color > mask[maskOffset + 1]) {
+ opacity = 255;
+ break;
+ }
+ }
+
+ alphaBuf[i] = opacity;
+ }
+ } else {
+ throw new _util.FormatError('Unknown mask format.');
+ }
+ }
+
+ if (alphaBuf) {
+ for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
+ rgbaBuf[j] = alphaBuf[i];
+ }
+ } else {
+ for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
+ rgbaBuf[j] = 255;
+ }
+ }
+ },
+ undoPreblend: function undoPreblend(buffer, width, height) {
+ var matte = this.smask && this.smask.matte;
+
+ if (!matte) {
+ return;
+ }
+
+ var matteRgb = this.colorSpace.getRgb(matte, 0);
+ var matteR = matteRgb[0];
+ var matteG = matteRgb[1];
+ var matteB = matteRgb[2];
+ var length = width * height * 4;
+
+ for (var i = 0; i < length; i += 4) {
+ var alpha = buffer[i + 3];
+
+ if (alpha === 0) {
+ buffer[i] = 255;
+ buffer[i + 1] = 255;
+ buffer[i + 2] = 255;
+ continue;
+ }
+
+ var k = 255 / alpha;
+ buffer[i] = (buffer[i] - matteR) * k + matteR;
+ buffer[i + 1] = (buffer[i + 1] - matteG) * k + matteG;
+ buffer[i + 2] = (buffer[i + 2] - matteB) * k + matteB;
+ }
+ },
+ createImageData: function createImageData() {
+ var forceRGBA = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ var drawWidth = this.drawWidth;
+ var drawHeight = this.drawHeight;
+ var imgData = {
+ width: drawWidth,
+ height: drawHeight,
+ kind: 0,
+ data: null
+ };
+ var numComps = this.numComps;
+ var originalWidth = this.width;
+ var originalHeight = this.height;
+ var bpc = this.bpc;
+ var rowBytes = originalWidth * numComps * bpc + 7 >> 3;
+ var imgArray;
+
+ if (!forceRGBA) {
+ var kind;
+
+ if (this.colorSpace.name === 'DeviceGray' && bpc === 1) {
+ kind = _util.ImageKind.GRAYSCALE_1BPP;
+ } else if (this.colorSpace.name === 'DeviceRGB' && bpc === 8 && !this.needsDecode) {
+ kind = _util.ImageKind.RGB_24BPP;
+ }
+
+ if (kind && !this.smask && !this.mask && drawWidth === originalWidth && drawHeight === originalHeight) {
+ imgData.kind = kind;
+ imgArray = this.getImageBytes(originalHeight * rowBytes);
+
+ if (this.image instanceof _stream.DecodeStream) {
+ imgData.data = imgArray;
+ } else {
+ var newArray = new Uint8ClampedArray(imgArray.length);
+ newArray.set(imgArray);
+ imgData.data = newArray;
+ }
+
+ if (this.needsDecode) {
+ (0, _util.assert)(kind === _util.ImageKind.GRAYSCALE_1BPP, 'PDFImage.createImageData: The image must be grayscale.');
+ var buffer = imgData.data;
+
+ for (var i = 0, ii = buffer.length; i < ii; i++) {
+ buffer[i] ^= 0xff;
+ }
+ }
+
+ return imgData;
+ }
+
+ if (this.image instanceof _jpeg_stream.JpegStream && !this.smask && !this.mask) {
+ var imageLength = originalHeight * rowBytes;
+
+ switch (this.colorSpace.name) {
+ case 'DeviceGray':
+ imageLength *= 3;
+
+ case 'DeviceRGB':
+ case 'DeviceCMYK':
+ imgData.kind = _util.ImageKind.RGB_24BPP;
+ imgData.data = this.getImageBytes(imageLength, drawWidth, drawHeight, true);
+ return imgData;
+ }
+ }
+ }
+
+ imgArray = this.getImageBytes(originalHeight * rowBytes);
+ var actualHeight = 0 | imgArray.length / rowBytes * drawHeight / originalHeight;
+ var comps = this.getComponents(imgArray);
+ var alpha01, maybeUndoPreblend;
+
+ if (!forceRGBA && !this.smask && !this.mask) {
+ imgData.kind = _util.ImageKind.RGB_24BPP;
+ imgData.data = new Uint8ClampedArray(drawWidth * drawHeight * 3);
+ alpha01 = 0;
+ maybeUndoPreblend = false;
+ } else {
+ imgData.kind = _util.ImageKind.RGBA_32BPP;
+ imgData.data = new Uint8ClampedArray(drawWidth * drawHeight * 4);
+ alpha01 = 1;
+ maybeUndoPreblend = true;
+ this.fillOpacity(imgData.data, drawWidth, drawHeight, actualHeight, comps);
+ }
+
+ if (this.needsDecode) {
+ this.decodeBuffer(comps);
+ }
+
+ this.colorSpace.fillRgb(imgData.data, originalWidth, originalHeight, drawWidth, drawHeight, actualHeight, bpc, comps, alpha01);
+
+ if (maybeUndoPreblend) {
+ this.undoPreblend(imgData.data, drawWidth, actualHeight);
+ }
+
+ return imgData;
+ },
+ fillGrayBuffer: function fillGrayBuffer(buffer) {
+ var numComps = this.numComps;
+
+ if (numComps !== 1) {
+ throw new _util.FormatError("Reading gray scale from a color image: ".concat(numComps));
+ }
+
+ var width = this.width;
+ var height = this.height;
+ var bpc = this.bpc;
+ var rowBytes = width * numComps * bpc + 7 >> 3;
+ var imgArray = this.getImageBytes(height * rowBytes);
+ var comps = this.getComponents(imgArray);
+ var i, length;
+
+ if (bpc === 1) {
+ length = width * height;
+
+ if (this.needsDecode) {
+ for (i = 0; i < length; ++i) {
+ buffer[i] = comps[i] - 1 & 255;
+ }
+ } else {
+ for (i = 0; i < length; ++i) {
+ buffer[i] = -comps[i] & 255;
+ }
+ }
+
+ return;
+ }
+
+ if (this.needsDecode) {
+ this.decodeBuffer(comps);
+ }
+
+ length = width * height;
+ var scale = 255 / ((1 << bpc) - 1);
+
+ for (i = 0; i < length; ++i) {
+ buffer[i] = scale * comps[i];
+ }
+ },
+ getImageBytes: function getImageBytes(length, drawWidth, drawHeight) {
+ var forceRGB = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
+ this.image.reset();
+ this.image.drawWidth = drawWidth || this.width;
+ this.image.drawHeight = drawHeight || this.height;
+ this.image.forceRGB = !!forceRGB;
+ return this.image.getBytes(length, true);
+ }
+ };
+ return PDFImage;
+}();
+
+exports.PDFImage = PDFImage;
+
+/***/ }),
+/* 191 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.MessageHandler = MessageHandler;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(2));
+
+var _util = __w_pdfjs_require__(5);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+function resolveCall(_x, _x2) {
+ return _resolveCall.apply(this, arguments);
+}
+
+function _resolveCall() {
+ _resolveCall = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee(fn, args) {
+ var thisArg,
+ _args = arguments;
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ thisArg = _args.length > 2 && _args[2] !== undefined ? _args[2] : null;
+
+ if (fn) {
+ _context.next = 3;
+ break;
+ }
+
+ return _context.abrupt("return", undefined);
+
+ case 3:
+ return _context.abrupt("return", fn.apply(thisArg, args));
+
+ case 4:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee);
+ }));
+ return _resolveCall.apply(this, arguments);
+}
+
+function wrapReason(reason) {
+ if (_typeof(reason) !== 'object') {
+ return reason;
+ }
+
+ switch (reason.name) {
+ case 'AbortException':
+ return new _util.AbortException(reason.message);
+
+ case 'MissingPDFException':
+ return new _util.MissingPDFException(reason.message);
+
+ case 'UnexpectedResponseException':
+ return new _util.UnexpectedResponseException(reason.message, reason.status);
+
+ default:
+ return new _util.UnknownErrorException(reason.message, reason.details);
+ }
+}
+
+function makeReasonSerializable(reason) {
+ if (!(reason instanceof Error) || reason instanceof _util.AbortException || reason instanceof _util.MissingPDFException || reason instanceof _util.UnexpectedResponseException || reason instanceof _util.UnknownErrorException) {
+ return reason;
+ }
+
+ return new _util.UnknownErrorException(reason.message, reason.toString());
+}
+
+function resolveOrReject(capability, success, reason) {
+ if (success) {
+ capability.resolve();
+ } else {
+ capability.reject(reason);
+ }
+}
+
+function finalize(promise) {
+ return Promise.resolve(promise)["catch"](function () {});
+}
+
+function MessageHandler(sourceName, targetName, comObj) {
+ var _this = this;
+
+ this.sourceName = sourceName;
+ this.targetName = targetName;
+ this.comObj = comObj;
+ this.callbackId = 1;
+ this.streamId = 1;
+ this.postMessageTransfers = true;
+ this.streamSinks = Object.create(null);
+ this.streamControllers = Object.create(null);
+ var callbacksCapabilities = this.callbacksCapabilities = Object.create(null);
+ var ah = this.actionHandler = Object.create(null);
+
+ this._onComObjOnMessage = function (event) {
+ var data = event.data;
+
+ if (data.targetName !== _this.sourceName) {
+ return;
+ }
+
+ if (data.stream) {
+ _this._processStreamMessage(data);
+ } else if (data.isReply) {
+ var callbackId = data.callbackId;
+
+ if (data.callbackId in callbacksCapabilities) {
+ var callback = callbacksCapabilities[callbackId];
+ delete callbacksCapabilities[callbackId];
+
+ if ('error' in data) {
+ callback.reject(wrapReason(data.error));
+ } else {
+ callback.resolve(data.data);
+ }
+ } else {
+ throw new Error("Cannot resolve callback ".concat(callbackId));
+ }
+ } else if (data.action in ah) {
+ var action = ah[data.action];
+
+ if (data.callbackId) {
+ var _sourceName = _this.sourceName;
+ var _targetName = data.sourceName;
+ Promise.resolve().then(function () {
+ return action[0].call(action[1], data.data);
+ }).then(function (result) {
+ comObj.postMessage({
+ sourceName: _sourceName,
+ targetName: _targetName,
+ isReply: true,
+ callbackId: data.callbackId,
+ data: result
+ });
+ }, function (reason) {
+ comObj.postMessage({
+ sourceName: _sourceName,
+ targetName: _targetName,
+ isReply: true,
+ callbackId: data.callbackId,
+ error: makeReasonSerializable(reason)
+ });
+ });
+ } else if (data.streamId) {
+ _this._createStreamSink(data);
+ } else {
+ action[0].call(action[1], data.data);
+ }
+ } else {
+ throw new Error("Unknown action from worker: ".concat(data.action));
+ }
+ };
+
+ comObj.addEventListener('message', this._onComObjOnMessage);
+}
+
+MessageHandler.prototype = {
+ on: function on(actionName, handler, scope) {
+ var ah = this.actionHandler;
+
+ if (ah[actionName]) {
+ throw new Error("There is already an actionName called \"".concat(actionName, "\""));
+ }
+
+ ah[actionName] = [handler, scope];
+ },
+ send: function send(actionName, data, transfers) {
+ var message = {
+ sourceName: this.sourceName,
+ targetName: this.targetName,
+ action: actionName,
+ data: data
+ };
+ this.postMessage(message, transfers);
+ },
+ sendWithPromise: function sendWithPromise(actionName, data, transfers) {
+ var callbackId = this.callbackId++;
+ var message = {
+ sourceName: this.sourceName,
+ targetName: this.targetName,
+ action: actionName,
+ data: data,
+ callbackId: callbackId
+ };
+ var capability = (0, _util.createPromiseCapability)();
+ this.callbacksCapabilities[callbackId] = capability;
+
+ try {
+ this.postMessage(message, transfers);
+ } catch (e) {
+ capability.reject(e);
+ }
+
+ return capability.promise;
+ },
+ sendWithStream: function sendWithStream(actionName, data, queueingStrategy, transfers) {
+ var _this2 = this;
+
+ var streamId = this.streamId++;
+ var sourceName = this.sourceName;
+ var targetName = this.targetName;
+ return new _util.ReadableStream({
+ start: function start(controller) {
+ var startCapability = (0, _util.createPromiseCapability)();
+ _this2.streamControllers[streamId] = {
+ controller: controller,
+ startCall: startCapability,
+ isClosed: false
+ };
+
+ _this2.postMessage({
+ sourceName: sourceName,
+ targetName: targetName,
+ action: actionName,
+ streamId: streamId,
+ data: data,
+ desiredSize: controller.desiredSize
+ });
+
+ return startCapability.promise;
+ },
+ pull: function pull(controller) {
+ var pullCapability = (0, _util.createPromiseCapability)();
+ _this2.streamControllers[streamId].pullCall = pullCapability;
+
+ _this2.postMessage({
+ sourceName: sourceName,
+ targetName: targetName,
+ stream: 'pull',
+ streamId: streamId,
+ desiredSize: controller.desiredSize
+ });
+
+ return pullCapability.promise;
+ },
+ cancel: function cancel(reason) {
+ var cancelCapability = (0, _util.createPromiseCapability)();
+ _this2.streamControllers[streamId].cancelCall = cancelCapability;
+ _this2.streamControllers[streamId].isClosed = true;
+
+ _this2.postMessage({
+ sourceName: sourceName,
+ targetName: targetName,
+ stream: 'cancel',
+ reason: reason,
+ streamId: streamId
+ });
+
+ return cancelCapability.promise;
+ }
+ }, queueingStrategy);
+ },
+ _createStreamSink: function _createStreamSink(data) {
+ var _this3 = this;
+
+ var self = this;
+ var action = this.actionHandler[data.action];
+ var streamId = data.streamId;
+ var desiredSize = data.desiredSize;
+ var sourceName = this.sourceName;
+ var targetName = data.sourceName;
+ var capability = (0, _util.createPromiseCapability)();
+
+ var sendStreamRequest = function sendStreamRequest(_ref) {
+ var stream = _ref.stream,
+ chunk = _ref.chunk,
+ transfers = _ref.transfers,
+ success = _ref.success,
+ reason = _ref.reason;
+
+ _this3.postMessage({
+ sourceName: sourceName,
+ targetName: targetName,
+ stream: stream,
+ streamId: streamId,
+ chunk: chunk,
+ success: success,
+ reason: reason
+ }, transfers);
+ };
+
+ var streamSink = {
+ enqueue: function enqueue(chunk) {
+ var size = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
+ var transfers = arguments.length > 2 ? arguments[2] : undefined;
+
+ if (this.isCancelled) {
+ return;
+ }
+
+ var lastDesiredSize = this.desiredSize;
+ this.desiredSize -= size;
+
+ if (lastDesiredSize > 0 && this.desiredSize <= 0) {
+ this.sinkCapability = (0, _util.createPromiseCapability)();
+ this.ready = this.sinkCapability.promise;
+ }
+
+ sendStreamRequest({
+ stream: 'enqueue',
+ chunk: chunk,
+ transfers: transfers
+ });
+ },
+ close: function close() {
+ if (this.isCancelled) {
+ return;
+ }
+
+ this.isCancelled = true;
+ sendStreamRequest({
+ stream: 'close'
+ });
+ delete self.streamSinks[streamId];
+ },
+ error: function error(reason) {
+ if (this.isCancelled) {
+ return;
+ }
+
+ this.isCancelled = true;
+ sendStreamRequest({
+ stream: 'error',
+ reason: reason
+ });
+ },
+ sinkCapability: capability,
+ onPull: null,
+ onCancel: null,
+ isCancelled: false,
+ desiredSize: desiredSize,
+ ready: null
+ };
+ streamSink.sinkCapability.resolve();
+ streamSink.ready = streamSink.sinkCapability.promise;
+ this.streamSinks[streamId] = streamSink;
+ resolveCall(action[0], [data.data, streamSink], action[1]).then(function () {
+ sendStreamRequest({
+ stream: 'start_complete',
+ success: true
+ });
+ }, function (reason) {
+ sendStreamRequest({
+ stream: 'start_complete',
+ success: false,
+ reason: reason
+ });
+ });
+ },
+ _processStreamMessage: function _processStreamMessage(data) {
+ var _this4 = this;
+
+ var sourceName = this.sourceName;
+ var targetName = data.sourceName;
+ var streamId = data.streamId;
+
+ var sendStreamResponse = function sendStreamResponse(_ref2) {
+ var stream = _ref2.stream,
+ success = _ref2.success,
+ reason = _ref2.reason;
+
+ _this4.comObj.postMessage({
+ sourceName: sourceName,
+ targetName: targetName,
+ stream: stream,
+ success: success,
+ streamId: streamId,
+ reason: reason
+ });
+ };
+
+ var deleteStreamController = function deleteStreamController() {
+ Promise.all([_this4.streamControllers[data.streamId].startCall, _this4.streamControllers[data.streamId].pullCall, _this4.streamControllers[data.streamId].cancelCall].map(function (capability) {
+ return capability && finalize(capability.promise);
+ })).then(function () {
+ delete _this4.streamControllers[data.streamId];
+ });
+ };
+
+ switch (data.stream) {
+ case 'start_complete':
+ resolveOrReject(this.streamControllers[data.streamId].startCall, data.success, wrapReason(data.reason));
+ break;
+
+ case 'pull_complete':
+ resolveOrReject(this.streamControllers[data.streamId].pullCall, data.success, wrapReason(data.reason));
+ break;
+
+ case 'pull':
+ if (!this.streamSinks[data.streamId]) {
+ sendStreamResponse({
+ stream: 'pull_complete',
+ success: true
+ });
+ break;
+ }
+
+ if (this.streamSinks[data.streamId].desiredSize <= 0 && data.desiredSize > 0) {
+ this.streamSinks[data.streamId].sinkCapability.resolve();
+ }
+
+ this.streamSinks[data.streamId].desiredSize = data.desiredSize;
+ resolveCall(this.streamSinks[data.streamId].onPull).then(function () {
+ sendStreamResponse({
+ stream: 'pull_complete',
+ success: true
+ });
+ }, function (reason) {
+ sendStreamResponse({
+ stream: 'pull_complete',
+ success: false,
+ reason: reason
+ });
+ });
+ break;
+
+ case 'enqueue':
+ (0, _util.assert)(this.streamControllers[data.streamId], 'enqueue should have stream controller');
+
+ if (!this.streamControllers[data.streamId].isClosed) {
+ this.streamControllers[data.streamId].controller.enqueue(data.chunk);
+ }
+
+ break;
+
+ case 'close':
+ (0, _util.assert)(this.streamControllers[data.streamId], 'close should have stream controller');
+
+ if (this.streamControllers[data.streamId].isClosed) {
+ break;
+ }
+
+ this.streamControllers[data.streamId].isClosed = true;
+ this.streamControllers[data.streamId].controller.close();
+ deleteStreamController();
+ break;
+
+ case 'error':
+ (0, _util.assert)(this.streamControllers[data.streamId], 'error should have stream controller');
+ this.streamControllers[data.streamId].controller.error(wrapReason(data.reason));
+ deleteStreamController();
+ break;
+
+ case 'cancel_complete':
+ resolveOrReject(this.streamControllers[data.streamId].cancelCall, data.success, wrapReason(data.reason));
+ deleteStreamController();
+ break;
+
+ case 'cancel':
+ if (!this.streamSinks[data.streamId]) {
+ break;
+ }
+
+ resolveCall(this.streamSinks[data.streamId].onCancel, [wrapReason(data.reason)]).then(function () {
+ sendStreamResponse({
+ stream: 'cancel_complete',
+ success: true
+ });
+ }, function (reason) {
+ sendStreamResponse({
+ stream: 'cancel_complete',
+ success: false,
+ reason: reason
+ });
+ });
+ this.streamSinks[data.streamId].sinkCapability.reject(wrapReason(data.reason));
+ this.streamSinks[data.streamId].isCancelled = true;
+ delete this.streamSinks[data.streamId];
+ break;
+
+ default:
+ throw new Error('Unexpected stream case');
+ }
+ },
+ postMessage: function postMessage(message, transfers) {
+ if (transfers && this.postMessageTransfers) {
+ this.comObj.postMessage(message, transfers);
+ } else {
+ this.comObj.postMessage(message);
+ }
+ },
+ destroy: function destroy() {
+ this.comObj.removeEventListener('message', this._onComObjOnMessage);
+ }
+};
+
+/***/ }),
+/* 192 */
+/***/ (function(module, exports, __w_pdfjs_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.PDFWorkerStream = void 0;
+
+var _regenerator = _interopRequireDefault(__w_pdfjs_require__(2));
+
+var _util = __w_pdfjs_require__(5);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
+
+function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+var PDFWorkerStream =
+/*#__PURE__*/
+function () {
+ function PDFWorkerStream(msgHandler) {
+ _classCallCheck(this, PDFWorkerStream);
+
+ this._msgHandler = msgHandler;
+ this._contentLength = null;
+ this._fullRequestReader = null;
+ this._rangeRequestReaders = [];
+ }
+
+ _createClass(PDFWorkerStream, [{
+ key: "getFullReader",
+ value: function getFullReader() {
+ (0, _util.assert)(!this._fullRequestReader);
+ this._fullRequestReader = new PDFWorkerStreamReader(this._msgHandler);
+ return this._fullRequestReader;
+ }
+ }, {
+ key: "getRangeReader",
+ value: function getRangeReader(begin, end) {
+ var reader = new PDFWorkerStreamRangeReader(begin, end, this._msgHandler);
+
+ this._rangeRequestReaders.push(reader);
+
+ return reader;
+ }
+ }, {
+ key: "cancelAllRequests",
+ value: function cancelAllRequests(reason) {
+ if (this._fullRequestReader) {
+ this._fullRequestReader.cancel(reason);
+ }
+
+ var readers = this._rangeRequestReaders.slice(0);
+
+ readers.forEach(function (reader) {
+ reader.cancel(reason);
+ });
+ }
+ }]);
+
+ return PDFWorkerStream;
+}();
+
+exports.PDFWorkerStream = PDFWorkerStream;
+
+var PDFWorkerStreamReader =
+/*#__PURE__*/
+function () {
+ function PDFWorkerStreamReader(msgHandler) {
+ var _this = this;
+
+ _classCallCheck(this, PDFWorkerStreamReader);
+
+ this._msgHandler = msgHandler;
+ this.onProgress = null;
+ this._contentLength = null;
+ this._isRangeSupported = false;
+ this._isStreamingSupported = false;
+
+ var readableStream = this._msgHandler.sendWithStream('GetReader');
+
+ this._reader = readableStream.getReader();
+ this._headersReady = this._msgHandler.sendWithPromise('ReaderHeadersReady').then(function (data) {
+ _this._isStreamingSupported = data.isStreamingSupported;
+ _this._isRangeSupported = data.isRangeSupported;
+ _this._contentLength = data.contentLength;
+ });
+ }
+
+ _createClass(PDFWorkerStreamReader, [{
+ key: "read",
+ value: function () {
+ var _read = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee() {
+ var _ref, value, done;
+
+ return _regenerator["default"].wrap(function _callee$(_context) {
+ while (1) {
+ switch (_context.prev = _context.next) {
+ case 0:
+ _context.next = 2;
+ return this._reader.read();
+
+ case 2:
+ _ref = _context.sent;
+ value = _ref.value;
+ done = _ref.done;
+
+ if (!done) {
+ _context.next = 7;
+ break;
+ }
+
+ return _context.abrupt("return", {
+ value: undefined,
+ done: true
+ });
+
+ case 7:
+ return _context.abrupt("return", {
+ value: value.buffer,
+ done: false
+ });
+
+ case 8:
+ case "end":
+ return _context.stop();
+ }
+ }
+ }, _callee, this);
+ }));
+
+ function read() {
+ return _read.apply(this, arguments);
+ }
+
+ return read;
+ }()
+ }, {
+ key: "cancel",
+ value: function cancel(reason) {
+ this._reader.cancel(reason);
+ }
+ }, {
+ key: "headersReady",
+ get: function get() {
+ return this._headersReady;
+ }
+ }, {
+ key: "contentLength",
+ get: function get() {
+ return this._contentLength;
+ }
+ }, {
+ key: "isStreamingSupported",
+ get: function get() {
+ return this._isStreamingSupported;
+ }
+ }, {
+ key: "isRangeSupported",
+ get: function get() {
+ return this._isRangeSupported;
+ }
+ }]);
+
+ return PDFWorkerStreamReader;
+}();
+
+var PDFWorkerStreamRangeReader =
+/*#__PURE__*/
+function () {
+ function PDFWorkerStreamRangeReader(begin, end, msgHandler) {
+ _classCallCheck(this, PDFWorkerStreamRangeReader);
+
+ this._msgHandler = msgHandler;
+ this.onProgress = null;
+
+ var readableStream = this._msgHandler.sendWithStream('GetRangeReader', {
+ begin: begin,
+ end: end
+ });
+
+ this._reader = readableStream.getReader();
+ }
+
+ _createClass(PDFWorkerStreamRangeReader, [{
+ key: "read",
+ value: function () {
+ var _read2 = _asyncToGenerator(
+ /*#__PURE__*/
+ _regenerator["default"].mark(function _callee2() {
+ var _ref2, value, done;
+
+ return _regenerator["default"].wrap(function _callee2$(_context2) {
+ while (1) {
+ switch (_context2.prev = _context2.next) {
+ case 0:
+ _context2.next = 2;
+ return this._reader.read();
+
+ case 2:
+ _ref2 = _context2.sent;
+ value = _ref2.value;
+ done = _ref2.done;
+
+ if (!done) {
+ _context2.next = 7;
+ break;
+ }
+
+ return _context2.abrupt("return", {
+ value: undefined,
+ done: true
+ });
+
+ case 7:
+ return _context2.abrupt("return", {
+ value: value.buffer,
+ done: false
+ });
+
+ case 8:
+ case "end":
+ return _context2.stop();
+ }
+ }
+ }, _callee2, this);
+ }));
+
+ function read() {
+ return _read2.apply(this, arguments);
+ }
+
+ return read;
+ }()
+ }, {
+ key: "cancel",
+ value: function cancel(reason) {
+ this._reader.cancel(reason);
+ }
+ }, {
+ key: "isStreamingSupported",
+ get: function get() {
+ return false;
+ }
+ }]);
+
+ return PDFWorkerStreamRangeReader;
+}();
+
+/***/ })
+/******/ ]);
+});
+//# sourceMappingURL=pdf.worker.js.map \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/.coveragerc b/testing/web-platform/tests/tools/third_party/pluggy/.coveragerc
new file mode 100644
index 0000000000..1b1de1cd24
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/.coveragerc
@@ -0,0 +1,14 @@
+[run]
+include =
+ pluggy/*
+ testing/*
+ */lib/python*/site-packages/pluggy/*
+ */pypy*/site-packages/pluggy/*
+ *\Lib\site-packages\pluggy\*
+branch = 1
+
+[paths]
+source = pluggy/
+ */lib/python*/site-packages/pluggy/
+ */pypy*/site-packages/pluggy/
+ *\Lib\site-packages\pluggy\
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/.github/workflows/main.yml b/testing/web-platform/tests/tools/third_party/pluggy/.github/workflows/main.yml
new file mode 100644
index 0000000000..e1022ca96d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/.github/workflows/main.yml
@@ -0,0 +1,148 @@
+name: main
+
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - "*"
+
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ name: [
+ "windows-py36",
+ "windows-py39",
+ "windows-pypy3",
+
+ "ubuntu-py36",
+ "ubuntu-py36-pytestmain",
+ "ubuntu-py37",
+ "ubuntu-py38",
+ "ubuntu-py39",
+ "ubuntu-pypy3",
+ "ubuntu-benchmark",
+
+ "linting",
+ "docs",
+ ]
+
+ include:
+ - name: "windows-py36"
+ python: "3.6"
+ os: windows-latest
+ tox_env: "py36"
+ - name: "windows-py39"
+ python: "3.9"
+ os: windows-latest
+ tox_env: "py39"
+ - name: "windows-pypy3"
+ python: "pypy3"
+ 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-py39"
+ python: "3.9"
+ os: ubuntu-latest
+ tox_env: "py39"
+ use_coverage: true
+ - name: "ubuntu-pypy3"
+ python: "pypy3"
+ os: ubuntu-latest
+ tox_env: "pypy3"
+ use_coverage: true
+ - name: "ubuntu-benchmark"
+ 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
+ with:
+ fetch-depth: 0
+
+ - 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 setuptools
+ python -m 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: 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 }}
+
+ 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
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-python@v2
+ with:
+ python-version: "3.8"
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install --upgrade wheel setuptools setuptools_scm
+
+ - name: Build package
+ run: python setup.py sdist bdist_wheel
+
+ - name: Publish package
+ uses: pypa/gh-action-pypi-publish@v1.4.1
+ with:
+ user: __token__
+ password: ${{ secrets.pypi_token }}
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/.gitignore b/testing/web-platform/tests/tools/third_party/pluggy/.gitignore
new file mode 100644
index 0000000000..4580536c7a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/.gitignore
@@ -0,0 +1,64 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+*.swp
+
+# generated by setuptools_scm
+src/pluggy/_version.py
+
+# generated by pip
+pip-wheel-metadata/
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/.pre-commit-config.yaml b/testing/web-platform/tests/tools/third_party/pluggy/.pre-commit-config.yaml
new file mode 100644
index 0000000000..d919ffeb2f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/.pre-commit-config.yaml
@@ -0,0 +1,34 @@
+repos:
+- repo: https://github.com/ambv/black
+ rev: 21.7b0
+ 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]
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v2.1.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: flake8
+- repo: local
+ hooks:
+ - id: rst
+ name: rst
+ entry: rst-lint --encoding utf-8
+ files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$
+ language: python
+ additional_dependencies: [pygments, restructuredtext_lint]
+- repo: https://github.com/pre-commit/pygrep-hooks
+ rev: v1.9.0
+ hooks:
+ - id: rst-backticks
+- repo: https://github.com/asottile/pyupgrade
+ rev: v2.23.3
+ hooks:
+ - id: pyupgrade
+ args: [--py36-plus]
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/CHANGELOG.rst b/testing/web-platform/tests/tools/third_party/pluggy/CHANGELOG.rst
new file mode 100644
index 0000000000..13a388c435
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/CHANGELOG.rst
@@ -0,0 +1,409 @@
+=========
+Changelog
+=========
+
+.. towncrier release notes start
+
+pluggy 1.0.0 (2021-08-25)
+=========================
+
+Deprecations and Removals
+-------------------------
+
+- `#116 <https://github.com/pytest-dev/pluggy/issues/116>`_: Remove deprecated ``implprefix`` support.
+ Decorate hook implementations using an instance of HookimplMarker instead.
+ The deprecation was announced in release ``0.7.0``.
+
+
+- `#120 <https://github.com/pytest-dev/pluggy/issues/120>`_: Remove the deprecated ``proc`` argument to ``call_historic``.
+ Use ``result_callback`` instead, which has the same behavior.
+ The deprecation was announced in release ``0.7.0``.
+
+
+- `#265 <https://github.com/pytest-dev/pluggy/issues/265>`_: Remove the ``_Result.result`` property. Use ``_Result.get_result()`` instead.
+ Note that unlike ``result``, ``get_result()`` raises the exception if the hook raised.
+ The deprecation was announced in release ``0.6.0``.
+
+
+- `#267 <https://github.com/pytest-dev/pluggy/issues/267>`_: Remove official support for Python 3.4.
+
+
+- `#272 <https://github.com/pytest-dev/pluggy/issues/272>`_: Dropped support for Python 2.
+ Continue to use pluggy 0.13.x for Python 2 support.
+
+
+- `#308 <https://github.com/pytest-dev/pluggy/issues/308>`_: Remove official support for Python 3.5.
+
+
+- `#313 <https://github.com/pytest-dev/pluggy/issues/313>`_: The internal ``pluggy.callers``, ``pluggy.manager`` and ``pluggy.hooks`` are now explicitly marked private by a ``_`` prefix (e.g. ``pluggy._callers``).
+ Only API exported by the top-level ``pluggy`` module is considered public.
+
+
+- `#59 <https://github.com/pytest-dev/pluggy/issues/59>`_: Remove legacy ``__multicall__`` recursive hook calling system.
+ The deprecation was announced in release ``0.5.0``.
+
+
+
+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
+ now raised.
+
+ Previously this problem would cause an error only later, when calling the hook.
+
+ In the unlikely case that you have a hookwrapper that *returns* a generator
+ instead of yielding directly, for example:
+
+ .. code-block:: python
+
+ def my_hook_real_implementation(arg):
+ print("before")
+ yield
+ print("after")
+
+
+ @hookimpl(hookwrapper=True)
+ def my_hook(arg):
+ return my_hook_implementation(arg)
+
+ change it to use ``yield from`` instead:
+
+ .. code-block:: python
+
+ @hookimpl(hookwrapper=True)
+ def my_hook(arg):
+ yield from my_hook_implementation(arg)
+
+
+- `#309 <https://github.com/pytest-dev/pluggy/issues/309>`_: Add official support for Python 3.9.
+
+
+pluggy 0.13.1 (2019-11-21)
+==========================
+
+Trivial/Internal Changes
+------------------------
+
+- `#236 <https://github.com/pytest-dev/pluggy/pull/236>`_: Improved documentation, especially with regard to references.
+
+
+pluggy 0.13.0 (2019-09-10)
+==========================
+
+Trivial/Internal Changes
+------------------------
+
+- `#222 <https://github.com/pytest-dev/pluggy/issues/222>`_: Replace ``importlib_metadata`` backport with ``importlib.metadata`` from the
+ standard library on Python 3.8+.
+
+
+pluggy 0.12.0 (2019-05-27)
+==========================
+
+Features
+--------
+
+- `#215 <https://github.com/pytest-dev/pluggy/issues/215>`_: Switch from ``pkg_resources`` to ``importlib-metadata`` for entrypoint detection for improved performance and import time. This time with ``.egg`` support.
+
+
+pluggy 0.11.0 (2019-05-07)
+==========================
+
+Bug Fixes
+---------
+
+- `#205 <https://github.com/pytest-dev/pluggy/issues/205>`_: Revert changes made in 0.10.0 release breaking ``.egg`` installs.
+
+
+pluggy 0.10.0 (2019-05-07)
+==========================
+
+Features
+--------
+
+- `#199 <https://github.com/pytest-dev/pluggy/issues/199>`_: Switch from ``pkg_resources`` to ``importlib-metadata`` for entrypoint detection for improved performance and import time.
+
+
+pluggy 0.9.0 (2019-02-21)
+=========================
+
+Features
+--------
+
+- `#189 <https://github.com/pytest-dev/pluggy/issues/189>`_: ``PluginManager.load_setuptools_entrypoints`` now accepts a ``name`` parameter that when given will
+ load only entry points with that name.
+
+ ``PluginManager.load_setuptools_entrypoints`` also now returns the number of plugins loaded by the
+ call, as opposed to the number of all plugins loaded by all calls to this method.
+
+
+
+Bug Fixes
+---------
+
+- `#187 <https://github.com/pytest-dev/pluggy/issues/187>`_: Fix internal ``varnames`` function for PyPy3.
+
+
+pluggy 0.8.1 (2018-11-09)
+=========================
+
+Trivial/Internal Changes
+------------------------
+
+- `#166 <https://github.com/pytest-dev/pluggy/issues/166>`_: Add ``stacklevel=2`` to implprefix warning so that the reported location of warning is the caller of PluginManager.
+
+
+pluggy 0.8.0 (2018-10-15)
+=========================
+
+Features
+--------
+
+- `#177 <https://github.com/pytest-dev/pluggy/issues/177>`_: Add ``get_hookimpls()`` method to hook callers.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- `#165 <https://github.com/pytest-dev/pluggy/issues/165>`_: Add changelog in long package description and documentation.
+
+
+- `#172 <https://github.com/pytest-dev/pluggy/issues/172>`_: Add a test exemplifying the opt-in nature of spec defined args.
+
+
+- `#57 <https://github.com/pytest-dev/pluggy/issues/57>`_: Encapsulate hook specifications in a type for easier introspection.
+
+
+pluggy 0.7.1 (2018-07-28)
+=========================
+
+Deprecations and Removals
+-------------------------
+
+- `#116 <https://github.com/pytest-dev/pluggy/issues/116>`_: Deprecate the ``implprefix`` kwarg to ``PluginManager`` and instead
+ expect users to start using explicit ``HookimplMarker`` everywhere.
+
+
+
+Features
+--------
+
+- `#122 <https://github.com/pytest-dev/pluggy/issues/122>`_: Add ``.plugin`` member to ``PluginValidationError`` to access failing plugin during post-mortem.
+
+
+- `#138 <https://github.com/pytest-dev/pluggy/issues/138>`_: Add per implementation warnings support for hookspecs allowing for both
+ deprecation and future warnings of legacy and (future) experimental hooks
+ respectively.
+
+
+
+Bug Fixes
+---------
+
+- `#110 <https://github.com/pytest-dev/pluggy/issues/110>`_: Fix a bug where ``_HookCaller.call_historic()`` would call the ``proc``
+ arg even when the default is ``None`` resulting in a ``TypeError``.
+
+- `#160 <https://github.com/pytest-dev/pluggy/issues/160>`_: Fix problem when handling ``VersionConflict`` errors when loading setuptools plugins.
+
+
+
+Improved Documentation
+----------------------
+
+- `#123 <https://github.com/pytest-dev/pluggy/issues/123>`_: Document how exceptions are handled and how the hook call loop
+ terminates immediately on the first error which is then delivered
+ to any surrounding wrappers.
+
+
+- `#136 <https://github.com/pytest-dev/pluggy/issues/136>`_: Docs rework including a much better introduction and comprehensive example
+ set for new users. A big thanks goes out to @obestwalter for the great work!
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- `#117 <https://github.com/pytest-dev/pluggy/issues/117>`_: Break up the main monolithic package modules into separate modules by concern
+
+
+- `#131 <https://github.com/pytest-dev/pluggy/issues/131>`_: Automate ``setuptools`` wheels building and PyPi upload using TravisCI.
+
+
+- `#153 <https://github.com/pytest-dev/pluggy/issues/153>`_: Reorganize tests more appropriately by modules relating to each
+ internal component/feature. This is in an effort to avoid (future)
+ duplication and better separation of concerns in the test set.
+
+
+- `#156 <https://github.com/pytest-dev/pluggy/issues/156>`_: Add ``HookImpl.__repr__()`` for better debugging.
+
+
+- `#66 <https://github.com/pytest-dev/pluggy/issues/66>`_: Start using ``towncrier`` and a custom ``tox`` environment to prepare releases!
+
+
+pluggy 0.7.0 (Unreleased)
+=========================
+
+* `#160 <https://github.com/pytest-dev/pluggy/issues/160>`_: We discovered a deployment issue so this version was never released to PyPI, only the tag exists.
+
+pluggy 0.6.0 (2017-11-24)
+=========================
+
+- Add CI testing for the features, release, and master
+ branches of ``pytest`` (PR `#79`_).
+- Document public API for ``_Result`` objects passed to wrappers
+ (PR `#85`_).
+- Document and test hook LIFO ordering (PR `#85`_).
+- Turn warnings into errors in test suite (PR `#89`_).
+- Deprecate ``_Result.result`` (PR `#88`_).
+- Convert ``_Multicall`` to a simple function distinguishing it from
+ the legacy version (PR `#90`_).
+- Resolve E741 errors (PR `#96`_).
+- Test and bug fix for unmarked hook collection (PRs `#97`_ and
+ `#102`_).
+- Drop support for EOL Python 2.6 and 3.3 (PR `#103`_).
+- Fix ``inspect`` based arg introspection on py3.6 (PR `#94`_).
+
+.. _#79: https://github.com/pytest-dev/pluggy/pull/79
+.. _#85: https://github.com/pytest-dev/pluggy/pull/85
+.. _#88: https://github.com/pytest-dev/pluggy/pull/88
+.. _#89: https://github.com/pytest-dev/pluggy/pull/89
+.. _#90: https://github.com/pytest-dev/pluggy/pull/90
+.. _#94: https://github.com/pytest-dev/pluggy/pull/94
+.. _#96: https://github.com/pytest-dev/pluggy/pull/96
+.. _#97: https://github.com/pytest-dev/pluggy/pull/97
+.. _#102: https://github.com/pytest-dev/pluggy/pull/102
+.. _#103: https://github.com/pytest-dev/pluggy/pull/103
+
+
+pluggy 0.5.2 (2017-09-06)
+=========================
+
+- fix bug where ``firstresult`` wrappers were being sent an incorrectly configured
+ ``_Result`` (a list was set instead of a single value). Add tests to check for
+ this as well as ``_Result.force_result()`` behaviour. Thanks to `@tgoodlet`_
+ for the PR `#72`_.
+
+- fix incorrect ``getattr`` of ``DeprecationWarning`` from the ``warnings``
+ module. Thanks to `@nicoddemus`_ for the PR `#77`_.
+
+- hide ``pytest`` tracebacks in certain core routines. Thanks to
+ `@nicoddemus`_ for the PR `#80`_.
+
+.. _#72: https://github.com/pytest-dev/pluggy/pull/72
+.. _#77: https://github.com/pytest-dev/pluggy/pull/77
+.. _#80: https://github.com/pytest-dev/pluggy/pull/80
+
+
+pluggy 0.5.1 (2017-08-29)
+=========================
+
+- fix a bug and add tests for case where ``firstresult`` hooks return
+ ``None`` results. Thanks to `@RonnyPfannschmidt`_ and `@tgoodlet`_
+ for the issue (`#68`_) and PR (`#69`_) respectively.
+
+.. _#69: https://github.com/pytest-dev/pluggy/pull/69
+.. _#68: https://github.com/pytest-dev/pluggy/issues/68
+
+
+pluggy 0.5.0 (2017-08-28)
+=========================
+
+- fix bug where callbacks for historic hooks would not be called for
+ already registered plugins. Thanks `@vodik`_ for the PR
+ and `@hpk42`_ for further fixes.
+
+- fix `#17`_ by considering only actual functions for hooks
+ this removes the ability to register arbitrary callable objects
+ which at first glance is a reasonable simplification,
+ thanks `@RonnyPfannschmidt`_ for report and pr.
+
+- fix `#19`_: allow registering hookspecs from instances. The PR from
+ `@tgoodlet`_ also modernized the varnames implementation.
+
+- resolve `#32`_: split up the test set into multiple modules.
+ Thanks to `@RonnyPfannschmidt`_ for the PR and `@tgoodlet`_ for
+ the initial request.
+
+- resolve `#14`_: add full sphinx docs. Thanks to `@tgoodlet`_ for
+ PR `#39`_.
+
+- add hook call mismatch warnings. Thanks to `@tgoodlet`_ for the
+ PR `#42`_.
+
+- resolve `#44`_: move to new-style classes. Thanks to `@MichalTHEDUDE`_
+ for PR `#46`_.
+
+- add baseline benchmarking/speed tests using ``pytest-benchmark``
+ in PR `#54`_. Thanks to `@tgoodlet`_.
+
+- update the README to showcase the API. Thanks to `@tgoodlet`_ for the
+ issue and PR `#55`_.
+
+- deprecate ``__multicall__`` and add a faster call loop implementation.
+ Thanks to `@tgoodlet`_ for PR `#58`_.
+
+- raise a comprehensible error when a ``hookimpl`` is called with positional
+ args. Thanks to `@RonnyPfannschmidt`_ for the issue and `@tgoodlet`_ for
+ PR `#60`_.
+
+- fix the ``firstresult`` test making it more complete
+ and remove a duplicate of that test. Thanks to `@tgoodlet`_
+ for PR `#62`_.
+
+.. _#62: https://github.com/pytest-dev/pluggy/pull/62
+.. _#60: https://github.com/pytest-dev/pluggy/pull/60
+.. _#58: https://github.com/pytest-dev/pluggy/pull/58
+.. _#55: https://github.com/pytest-dev/pluggy/pull/55
+.. _#54: https://github.com/pytest-dev/pluggy/pull/54
+.. _#46: https://github.com/pytest-dev/pluggy/pull/46
+.. _#44: https://github.com/pytest-dev/pluggy/issues/44
+.. _#42: https://github.com/pytest-dev/pluggy/pull/42
+.. _#39: https://github.com/pytest-dev/pluggy/pull/39
+.. _#32: https://github.com/pytest-dev/pluggy/pull/32
+.. _#19: https://github.com/pytest-dev/pluggy/issues/19
+.. _#17: https://github.com/pytest-dev/pluggy/issues/17
+.. _#14: https://github.com/pytest-dev/pluggy/issues/14
+
+
+pluggy 0.4.0 (2016-09-25)
+=========================
+
+- add ``has_plugin(name)`` method to pluginmanager. thanks `@nicoddemus`_.
+
+- fix `#11`_: make plugin parsing more resilient against exceptions
+ from ``__getattr__`` functions. Thanks `@nicoddemus`_.
+
+- fix issue `#4`_: specific ``HookCallError`` exception for when a hook call
+ provides not enough arguments.
+
+- better error message when loading setuptools entrypoints fails
+ due to a ``VersionConflict``. Thanks `@blueyed`_.
+
+.. _#11: https://github.com/pytest-dev/pluggy/issues/11
+.. _#4: https://github.com/pytest-dev/pluggy/issues/4
+
+
+pluggy 0.3.1 (2015-09-17)
+=========================
+
+- avoid using deprecated-in-python3.5 getargspec method. Thanks
+ `@mdboom`_.
+
+
+pluggy 0.3.0 (2015-05-07)
+=========================
+
+initial release
+
+.. contributors
+.. _@hpk42: https://github.com/hpk42
+.. _@tgoodlet: https://github.com/goodboy
+.. _@MichalTHEDUDE: https://github.com/MichalTHEDUDE
+.. _@vodik: https://github.com/vodik
+.. _@RonnyPfannschmidt: https://github.com/RonnyPfannschmidt
+.. _@blueyed: https://github.com/blueyed
+.. _@nicoddemus: https://github.com/nicoddemus
+.. _@mdboom: https://github.com/mdboom
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/LICENSE b/testing/web-platform/tests/tools/third_party/pluggy/LICENSE
new file mode 100644
index 0000000000..85f4dd63d2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42)
+
+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/testing/web-platform/tests/tools/third_party/pluggy/MANIFEST.in b/testing/web-platform/tests/tools/third_party/pluggy/MANIFEST.in
new file mode 100644
index 0000000000..0cf8f3e088
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/MANIFEST.in
@@ -0,0 +1,7 @@
+include CHANGELOG
+include README.rst
+include setup.py
+include tox.ini
+include LICENSE
+graft testing
+recursive-exclude * *.pyc *.pyo
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/README.rst b/testing/web-platform/tests/tools/third_party/pluggy/README.rst
new file mode 100644
index 0000000000..3496617e1e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/README.rst
@@ -0,0 +1,101 @@
+====================================================
+pluggy - A minimalist production ready plugin system
+====================================================
+
+|pypi| |conda-forge| |versions| |github-actions| |gitter| |black| |codecov|
+
+This is the core framework used by the `pytest`_, `tox`_, and `devpi`_ projects.
+
+Please `read the docs`_ to learn more!
+
+A definitive example
+====================
+.. code-block:: python
+
+ import pluggy
+
+ hookspec = pluggy.HookspecMarker("myproject")
+ hookimpl = pluggy.HookimplMarker("myproject")
+
+
+ class MySpec:
+ """A hook specification namespace."""
+
+ @hookspec
+ def myhook(self, arg1, arg2):
+ """My special little hook that you can customize."""
+
+
+ class Plugin_1:
+ """A hook implementation namespace."""
+
+ @hookimpl
+ def myhook(self, arg1, arg2):
+ print("inside Plugin_1.myhook()")
+ return arg1 + arg2
+
+
+ class Plugin_2:
+ """A 2nd hook implementation namespace."""
+
+ @hookimpl
+ def myhook(self, arg1, arg2):
+ print("inside Plugin_2.myhook()")
+ return arg1 - arg2
+
+
+ # create a manager and add the spec
+ pm = pluggy.PluginManager("myproject")
+ pm.add_hookspecs(MySpec)
+
+ # register plugins
+ pm.register(Plugin_1())
+ pm.register(Plugin_2())
+
+ # call our ``myhook`` hook
+ results = pm.hook.myhook(arg1=1, arg2=2)
+ print(results)
+
+
+Running this directly gets us::
+
+ $ python docs/examples/toy-example.py
+ inside Plugin_2.myhook()
+ inside Plugin_1.myhook()
+ [-1, 3]
+
+
+.. badges
+
+.. |pypi| image:: https://img.shields.io/pypi/v/pluggy.svg
+ :target: https://pypi.org/pypi/pluggy
+
+.. |versions| image:: https://img.shields.io/pypi/pyversions/pluggy.svg
+ :target: https://pypi.org/pypi/pluggy
+
+.. |github-actions| image:: https://github.com/pytest-dev/pluggy/workflows/main/badge.svg
+ :target: https://github.com/pytest-dev/pluggy/actions
+
+.. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/pluggy.svg
+ :target: https://anaconda.org/conda-forge/pytest
+
+.. |gitter| image:: https://badges.gitter.im/pytest-dev/pluggy.svg
+ :alt: Join the chat at https://gitter.im/pytest-dev/pluggy
+ :target: https://gitter.im/pytest-dev/pluggy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
+
+.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/ambv/black
+
+.. |codecov| image:: https://codecov.io/gh/pytest-dev/pluggy/branch/master/graph/badge.svg
+ :target: https://codecov.io/gh/pytest-dev/pluggy
+ :alt: Code coverage Status
+
+.. links
+.. _pytest:
+ http://pytest.org
+.. _tox:
+ https://tox.readthedocs.org
+.. _devpi:
+ http://doc.devpi.net
+.. _read the docs:
+ https://pluggy.readthedocs.io/en/latest/
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/RELEASING.rst b/testing/web-platform/tests/tools/third_party/pluggy/RELEASING.rst
new file mode 100644
index 0000000000..ee0d1331e0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/RELEASING.rst
@@ -0,0 +1,23 @@
+Release Procedure
+-----------------
+
+#. From a clean work tree, execute::
+
+ tox -e release -- VERSION
+
+ This will create the branch ready to be pushed.
+
+#. Open a PR targeting ``main``.
+
+#. All tests must pass and the PR must be approved by at least another maintainer.
+
+#. Publish to PyPI by pushing a tag::
+
+ git tag X.Y.Z release-X.Y.Z
+ git push git@github.com:pytest-dev/pluggy.git X.Y.Z
+
+ The tag will trigger a new build, which will deploy to PyPI.
+
+#. Make sure it is `available on PyPI <https://pypi.org/project/pluggy>`_.
+
+#. Merge the PR into ``main``, either manually or using GitHub's web interface.
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/changelog/README.rst b/testing/web-platform/tests/tools/third_party/pluggy/changelog/README.rst
new file mode 100644
index 0000000000..47e21fb33f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/changelog/README.rst
@@ -0,0 +1,32 @@
+This directory contains "newsfragments" which are short files that contain a small **ReST**-formatted
+text that will be added to the next ``CHANGELOG``.
+
+The ``CHANGELOG`` will be read by users, so this description should be aimed to pytest users
+instead of describing internal changes which are only relevant to the developers.
+
+Make sure to use full sentences with correct case and punctuation, for example::
+
+ Fix issue with non-ascii messages from the ``warnings`` module.
+
+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.
+* ``bugfix``: fixes a reported bug.
+* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
+* ``removal``: feature deprecation or removal.
+* ``vendor``: changes in packages vendored in pytest.
+* ``trivial``: fixing a small typo or internal change that might be noteworthy.
+
+So for example: ``123.feature.rst``, ``456.bugfix.rst``.
+
+If your PR fixes an issue, use that number here. If there is no issue,
+then after you submit the PR and get the PR number you can add a
+changelog using that instead.
+
+If you are not sure what issue type to use, don't hesitate to ask in your PR.
+
+``towncrier`` preserves multiple paragraphs and formatting (code blocks, lists, and so on), but for entries
+other than ``features`` it is usually better to stick to a single paragraph to keep it concise. You can install
+``towncrier`` and then run ``towncrier --draft``
+if you want to get a preview of how your change will look in the final release notes.
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/changelog/_template.rst b/testing/web-platform/tests/tools/third_party/pluggy/changelog/_template.rst
new file mode 100644
index 0000000000..974e5c1b2d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/changelog/_template.rst
@@ -0,0 +1,40 @@
+{% for section in sections %}
+{% set underline = "-" %}
+{% if section %}
+{{section}}
+{{ underline * section|length }}{% set underline = "~" %}
+
+{% 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]|dictsort(by='value') %}
+{% set issue_joiner = joiner(', ') %}
+- {% for value in values|sort %}{{ issue_joiner() }}`{{ value }} <https://github.com/pytest-dev/pluggy/issues/{{ value[1:] }}>`_{% endfor %}: {{ text }}
+
+
+{% endfor %}
+{% else %}
+- {{ sections[section][category]['']|sort|join(', ') }}
+
+
+{% endif %}
+{% if sections[section][category]|length == 0 %}
+
+No significant changes.
+
+
+{% else %}
+{% endif %}
+{% endfor %}
+{% else %}
+
+No significant changes.
+
+
+{% endif %}
+{% endfor %}
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/codecov.yml b/testing/web-platform/tests/tools/third_party/pluggy/codecov.yml
new file mode 100644
index 0000000000..a0a308588e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/codecov.yml
@@ -0,0 +1,7 @@
+coverage:
+ status:
+ project: true
+ patch: true
+ changes: true
+
+comment: off
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/_static/img/plug.png b/testing/web-platform/tests/tools/third_party/pluggy/docs/_static/img/plug.png
new file mode 100644
index 0000000000..3339f8a608
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/_static/img/plug.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/api_reference.rst b/testing/web-platform/tests/tools/third_party/pluggy/docs/api_reference.rst
new file mode 100644
index 0000000000..d9552d4485
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/api_reference.rst
@@ -0,0 +1,19 @@
+:orphan:
+
+Api Reference
+=============
+
+.. automodule:: pluggy
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: pluggy._callers._Result
+.. automethod:: pluggy._callers._Result.get_result
+.. automethod:: pluggy._callers._Result.force_result
+
+.. autoclass:: pluggy._hooks._HookCaller
+.. automethod:: pluggy._hooks._HookCaller.call_extra
+.. automethod:: pluggy._hooks._HookCaller.call_historic
+
+.. autoclass:: pluggy._hooks._HookRelay
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/changelog.rst b/testing/web-platform/tests/tools/third_party/pluggy/docs/changelog.rst
new file mode 100644
index 0000000000..565b0521d0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/changelog.rst
@@ -0,0 +1 @@
+.. include:: ../CHANGELOG.rst
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/conf.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/conf.py
new file mode 100644
index 0000000000..f8e70c88bf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/conf.py
@@ -0,0 +1,87 @@
+import sys
+
+if sys.version_info >= (3, 8):
+ from importlib import metadata
+else:
+ import importlib_metadata as metadata
+
+
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.doctest",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.coverage",
+ "sphinx.ext.viewcode",
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+source_suffix = ".rst"
+
+# The master toctree document.
+master_doc = "index"
+
+# General information about the project.
+
+project = "pluggy"
+copyright = "2016, Holger Krekel"
+author = "Holger Krekel"
+
+release = metadata.version(project)
+# The short X.Y version.
+version = ".".join(release.split(".")[:2])
+
+
+language = None
+
+pygments_style = "sphinx"
+# html_logo = "_static/img/plug.png"
+html_theme = "alabaster"
+html_theme_options = {
+ "logo": "img/plug.png",
+ "description": "The pytest plugin system",
+ "github_user": "pytest-dev",
+ "github_repo": "pluggy",
+ "github_button": "true",
+ "github_banner": "true",
+ "github_type": "star",
+ "badge_branch": "master",
+ "page_width": "1080px",
+ "fixed_sidebar": "false",
+}
+html_sidebars = {
+ "**": ["about.html", "localtoc.html", "relations.html", "searchbox.html"]
+}
+html_static_path = ["_static"]
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [(master_doc, "pluggy", "pluggy Documentation", [author], 1)]
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (
+ master_doc,
+ "pluggy",
+ "pluggy Documentation",
+ author,
+ "pluggy",
+ "One line description of project.",
+ "Miscellaneous",
+ )
+]
+
+# Example configuration for intersphinx: refer to the Python standard library.
+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),
+ "devpi": ("https://devpi.net/docs/devpi/devpi/stable/+doc/", None),
+}
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/eggsample_spam.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/eggsample_spam.py
new file mode 100644
index 0000000000..500d885d55
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/eggsample_spam.py
@@ -0,0 +1,22 @@
+import eggsample
+
+
+@eggsample.hookimpl
+def eggsample_add_ingredients(ingredients):
+ """Here the caller expects us to return a list."""
+ if "egg" in ingredients:
+ spam = ["lovely spam", "wonderous spam"]
+ else:
+ spam = ["splendiferous spam", "magnificent spam"]
+ return spam
+
+
+@eggsample.hookimpl
+def eggsample_prep_condiments(condiments):
+ """Here the caller passes a mutable object, so we mess with it directly."""
+ try:
+ del condiments["steak sauce"]
+ except KeyError:
+ pass
+ condiments["spam sauce"] = 42
+ return "Now this is what I call a condiments tray!"
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/setup.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/setup.py
new file mode 100644
index 0000000000..f81a8eb403
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/setup.py
@@ -0,0 +1,8 @@
+from setuptools import setup
+
+setup(
+ name="eggsample-spam",
+ install_requires="eggsample",
+ entry_points={"eggsample": ["spam = eggsample_spam"]},
+ py_modules=["eggsample_spam"],
+)
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/__init__.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/__init__.py
new file mode 100644
index 0000000000..4dc4b36dec
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/__init__.py
@@ -0,0 +1,4 @@
+import pluggy
+
+hookimpl = pluggy.HookimplMarker("eggsample")
+"""Marker to be imported and used in plugins (and for own implementations)"""
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/hookspecs.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/hookspecs.py
new file mode 100644
index 0000000000..48866b2491
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/hookspecs.py
@@ -0,0 +1,21 @@
+import pluggy
+
+hookspec = pluggy.HookspecMarker("eggsample")
+
+
+@hookspec
+def eggsample_add_ingredients(ingredients: tuple):
+ """Have a look at the ingredients and offer your own.
+
+ :param ingredients: the ingredients, don't touch them!
+ :return: a list of ingredients
+ """
+
+
+@hookspec
+def eggsample_prep_condiments(condiments: dict):
+ """Reorganize the condiments tray to your heart's content.
+
+ :param condiments: some sauces and stuff
+ :return: a witty comment about your activity
+ """
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/host.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/host.py
new file mode 100644
index 0000000000..ac1d33b453
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/host.py
@@ -0,0 +1,57 @@
+import itertools
+import random
+
+import pluggy
+
+from eggsample import hookspecs, lib
+
+condiments_tray = {"pickled walnuts": 13, "steak sauce": 4, "mushy peas": 2}
+
+
+def main():
+ pm = get_plugin_manager()
+ cook = EggsellentCook(pm.hook)
+ cook.add_ingredients()
+ cook.prepare_the_food()
+ cook.serve_the_food()
+
+
+def get_plugin_manager():
+ pm = pluggy.PluginManager("eggsample")
+ pm.add_hookspecs(hookspecs)
+ pm.load_setuptools_entrypoints("eggsample")
+ pm.register(lib)
+ return pm
+
+
+class EggsellentCook:
+ FAVORITE_INGREDIENTS = ("egg", "egg", "egg")
+
+ def __init__(self, hook):
+ self.hook = hook
+ self.ingredients = None
+
+ def add_ingredients(self):
+ results = self.hook.eggsample_add_ingredients(
+ ingredients=self.FAVORITE_INGREDIENTS
+ )
+ my_ingredients = list(self.FAVORITE_INGREDIENTS)
+ # Each hook returns a list - so we chain this list of lists
+ other_ingredients = list(itertools.chain(*results))
+ self.ingredients = my_ingredients + other_ingredients
+
+ def prepare_the_food(self):
+ random.shuffle(self.ingredients)
+
+ def serve_the_food(self):
+ condiment_comments = self.hook.eggsample_prep_condiments(
+ condiments=condiments_tray
+ )
+ print(f"Your food. Enjoy some {', '.join(self.ingredients)}")
+ print(f"Some condiments? We have {', '.join(condiments_tray.keys())}")
+ if any(condiment_comments):
+ print("\n".join(condiment_comments))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/lib.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/lib.py
new file mode 100644
index 0000000000..62cea7458e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/lib.py
@@ -0,0 +1,14 @@
+import eggsample
+
+
+@eggsample.hookimpl
+def eggsample_add_ingredients():
+ spices = ["salt", "pepper"]
+ you_can_never_have_enough_eggs = ["egg", "egg"]
+ ingredients = spices + you_can_never_have_enough_eggs
+ return ingredients
+
+
+@eggsample.hookimpl
+def eggsample_prep_condiments(condiments):
+ condiments["mint sauce"] = 1
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/setup.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/setup.py
new file mode 100644
index 0000000000..8b3facb3b6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/setup.py
@@ -0,0 +1,8 @@
+from setuptools import setup, find_packages
+
+setup(
+ name="eggsample",
+ install_requires="pluggy>=0.3,<1.0",
+ entry_points={"console_scripts": ["eggsample=eggsample.host:main"]},
+ packages=find_packages(),
+)
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/toy-example.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/toy-example.py
new file mode 100644
index 0000000000..6d2086f9ba
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/toy-example.py
@@ -0,0 +1,41 @@
+import pluggy
+
+hookspec = pluggy.HookspecMarker("myproject")
+hookimpl = pluggy.HookimplMarker("myproject")
+
+
+class MySpec:
+ """A hook specification namespace."""
+
+ @hookspec
+ def myhook(self, arg1, arg2):
+ """My special little hook that you can customize."""
+
+
+class Plugin_1:
+ """A hook implementation namespace."""
+
+ @hookimpl
+ def myhook(self, arg1, arg2):
+ print("inside Plugin_1.myhook()")
+ return arg1 + arg2
+
+
+class Plugin_2:
+ """A 2nd hook implementation namespace."""
+
+ @hookimpl
+ def myhook(self, arg1, arg2):
+ print("inside Plugin_2.myhook()")
+ return arg1 - arg2
+
+
+# create a manager and add the spec
+pm = pluggy.PluginManager("myproject")
+pm.add_hookspecs(MySpec)
+# register plugins
+pm.register(Plugin_1())
+pm.register(Plugin_2())
+# call our `myhook` hook
+results = pm.hook.myhook(arg1=1, arg2=2)
+print(results)
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/index.rst b/testing/web-platform/tests/tools/third_party/pluggy/docs/index.rst
new file mode 100644
index 0000000000..eab08fcbbd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/index.rst
@@ -0,0 +1,957 @@
+``pluggy``
+==========
+**The pytest plugin system**
+
+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
+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.
+
+It gives users the ability to extend or modify the behaviour of a
+``host program`` by installing a ``plugin`` for that program.
+The plugin code will run as part of normal program execution, changing or
+enhancing certain aspects of it.
+
+In essence, ``pluggy`` enables function `hooking`_ so you can build
+"pluggable" systems.
+
+Why is it useful?
+*****************
+There are some established mechanisms for modifying the behavior of other
+programs/libraries in Python like
+`method overriding <https://en.wikipedia.org/wiki/Method_overriding>`_
+(e.g. Jinja2) or
+`monkey patching <https://en.wikipedia.org/wiki/Monkey_patch>`_ (e.g. gevent
+or for :std:doc:`writing tests <pytest:how-to/monkeypatch>`).
+These strategies become problematic though when several parties want to
+participate in the modification of the same program. Therefore ``pluggy``
+does not rely on these mechanisms to enable a more structured approach and
+avoid unnecessary exposure of state and behaviour. This leads to a more
+`loosely coupled <https://en.wikipedia.org/wiki/Loose_coupling>`_ relationship
+between ``host`` and ``plugins``.
+
+The ``pluggy`` approach puts the burden on the designer of the
+``host program`` to think carefully about which objects are really
+needed in a hook implementation. This gives ``plugin`` creators a clear
+framework for how to extend the ``host`` via a well defined set of functions
+and objects to work with.
+
+How does it work?
+*****************
+Let us start with a short overview of what is involved:
+
+* ``host`` or ``host program``: the program offering extensibility
+ by specifying ``hook functions`` and invoking their implementation(s) as
+ part of program execution
+* ``plugin``: the program implementing (a subset of) the specified hooks and
+ participating in program execution when the implementations are invoked
+ by the ``host``
+* ``pluggy``: connects ``host`` and ``plugins`` by using ...
+
+ - the hook :ref:`specifications <specs>` defining call signatures
+ provided by the ``host`` (a.k.a ``hookspecs`` - see :ref:`marking_hooks`)
+ - the hook :ref:`implementations <impls>` provided by registered
+ ``plugins`` (a.k.a ``hookimpl`` - see `callbacks`_)
+ - the hook :ref:`caller <calling>` - a call loop triggered at appropriate
+ program positions in the ``host`` invoking the implementations and
+ collecting the results
+
+ ... where for each registered hook *specification*, a hook *call* will
+ invoke up to ``N`` registered hook *implementations*.
+* ``user``: the person who installed the ``host program`` and wants to
+ extend its functionality with ``plugins``. In the simplest case they install
+ the ``plugin`` in the same environment as the ``host`` and the magic will
+ happen when the ``host program`` is run the next time. Depending on
+ the ``plugin``, there might be other things they need to do. For example,
+ they might have to call the host with an additional commandline parameter
+ to the host that the ``plugin`` added.
+
+A toy example
+-------------
+Let us demonstrate the core functionality in one module and show how you can
+start experimenting with pluggy functionality.
+
+.. literalinclude:: examples/toy-example.py
+
+Running this directly gets us::
+
+ $ python docs/examples/toy-example.py
+
+ inside Plugin_2.myhook()
+ inside Plugin_1.myhook()
+ [-1, 3]
+
+A complete example
+------------------
+Now let us demonstrate how this plays together in a vaguely real world scenario.
+
+Let's assume our ``host program`` is called **eggsample** where some eggs will
+be prepared and served with a tray containing condiments. As everybody knows:
+the more cooks are involved the better the food, so let us make the process
+pluggable and write a plugin that improves the meal with some spam and replaces
+the steak sauce (nobody likes that anyway) with spam sauce (it's a thing - trust me).
+
+.. note::
+
+ **naming markers**: ``HookSpecMarker`` and ``HookImplMarker`` must be
+ initialized with the name of the ``host`` project (the ``name``
+ parameter in ``setup()``) - so **eggsample** in our case.
+
+ **naming plugin projects**: they should be named in the form of
+ ``<host>-<plugin>`` (e.g. ``pytest-xdist``), therefore we call our
+ plugin *eggsample-spam*.
+
+The host
+^^^^^^^^
+``eggsample/eggsample/__init__.py``
+
+.. literalinclude:: examples/eggsample/eggsample/__init__.py
+
+``eggsample/eggsample/hookspecs.py``
+
+.. literalinclude:: examples/eggsample/eggsample/hookspecs.py
+
+``eggsample/eggsample/lib.py``
+
+.. literalinclude:: examples/eggsample/eggsample/lib.py
+
+``eggsample/eggsample/host.py``
+
+.. literalinclude:: examples/eggsample/eggsample/host.py
+
+``eggsample/setup.py``
+
+.. literalinclude:: examples/eggsample/setup.py
+
+Let's get cooking - we install the host and see what a program run looks like::
+
+ $ pip install --editable pluggy/docs/examples/eggsample
+ $ eggsample
+
+ Your food. Enjoy some egg, egg, salt, egg, egg, pepper, egg
+ Some condiments? We have pickled walnuts, steak sauce, mushy peas, mint sauce
+
+The plugin
+^^^^^^^^^^
+``eggsample-spam/eggsample_spam.py``
+
+.. literalinclude:: examples/eggsample-spam/eggsample_spam.py
+
+``eggsample-spam/setup.py``
+
+.. literalinclude:: examples/eggsample-spam/setup.py
+
+Let's get cooking with more cooks - we install the plugin and and see what
+we get::
+
+ $ pip install --editable pluggy/docs/examples/eggsample-spam
+ $ eggsample
+
+ Your food. Enjoy some egg, lovely spam, salt, egg, egg, egg, wonderous spam, egg, pepper
+ Some condiments? We have pickled walnuts, mushy peas, mint sauce, spam sauce
+ Now this is what I call a condiments tray!
+
+More real world examples
+------------------------
+To see how ``pluggy`` is used in the real world, have a look at these projects
+documentation and source code:
+
+* :ref:`pytest <pytest:writing-plugins>`
+* :std:doc:`tox <tox:plugins>`
+* :std:doc:`devpi <devpi:devguide/index>`
+
+For more details and advanced usage please read on.
+
+.. _define:
+
+Define and collect hooks
+************************
+A *plugin* is a :ref:`namespace <python:tut-scopes>` type (currently one of a
+``class`` or module) which defines a set of *hook* functions.
+
+As mentioned in :ref:`manage`, all *plugins* which specify *hooks*
+are managed by an instance of a :py:class:`pluggy.PluginManager` which
+defines the primary ``pluggy`` API.
+
+In order for a :py:class:`~pluggy.PluginManager` to detect functions in a namespace
+intended to be *hooks*, they must be decorated using special ``pluggy`` *marks*.
+
+.. _marking_hooks:
+
+Marking hooks
+-------------
+The :py:class:`~pluggy.HookspecMarker` and :py:class:`~pluggy.HookimplMarker`
+decorators are used to *mark* functions for detection by a
+:py:class:`~pluggy.PluginManager`:
+
+.. code-block:: python
+
+ from pluggy import HookspecMarker, HookimplMarker
+
+ hookspec = HookspecMarker("project_name")
+ hookimpl = HookimplMarker("project_name")
+
+
+Each decorator type takes a single ``project_name`` string as its
+lone argument the value of which is used to mark hooks for detection by
+a similarly configured :py:class:`~pluggy.PluginManager` instance.
+
+That is, a *mark* type called with ``project_name`` returns an object which
+can be used to decorate functions which will then be detected by a
+:py:class:`~pluggy.PluginManager` which was instantiated with the same
+``project_name`` value.
+
+Furthermore, each *hookimpl* or *hookspec* decorator can configure the
+underlying call-time behavior of each *hook* object by providing special
+*options* passed as keyword arguments.
+
+
+.. note::
+ The following sections correspond to similar documentation in
+ ``pytest`` for :ref:`pytest:writinghooks` and can be used as
+ a supplementary resource.
+
+.. _impls:
+
+Implementations
+---------------
+A hook *implementation* (*hookimpl*) is just a (callback) function
+which has been appropriately marked.
+
+*hookimpls* are loaded from a plugin using the
+:py:meth:`~pluggy.PluginManager.register()` method:
+
+.. code-block:: python
+
+ import sys
+ from pluggy import PluginManager, HookimplMarker
+
+ hookimpl = HookimplMarker("myproject")
+
+
+ @hookimpl
+ def setup_project(config, args):
+ """This hook is used to process the initial config
+ and possibly input arguments.
+ """
+ if args:
+ config.process_args(args)
+
+ return config
+
+
+ pm = PluginManager("myproject")
+
+ # load all hookimpls from the local module's namespace
+ plugin_name = pm.register(sys.modules[__name__])
+
+.. _optionalhook:
+
+Optional validation
+^^^^^^^^^^^^^^^^^^^
+Normally each *hookimpl* should be validated against a corresponding
+hook :ref:`specification <specs>`. If you want to make an exception
+then the *hookimpl* should be marked with the ``"optionalhook"`` option:
+
+.. code-block:: python
+
+ @hookimpl(optionalhook=True)
+ def setup_project(config, args):
+ """This hook is used to process the initial config
+ and possibly input arguments.
+ """
+ if args:
+ config.process_args(args)
+
+ return config
+
+.. _specname:
+
+Hookspec name matching
+^^^^^^^^^^^^^^^^^^^^^^
+
+During plugin :ref:`registration <registration>`, pluggy attempts to match each
+hook implementation declared by the *plugin* to a hook
+:ref:`specification <specs>` in the *host* program with the **same name** as
+the function being decorated by ``@hookimpl`` (e.g. ``setup_project`` in the
+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:*
+
+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:
+
+.. code-block:: python
+
+ @hookimpl(specname="setup_project")
+ def any_plugin_function(config, args):
+ """This hook is used to process the initial config
+ and possibly input arguments.
+ """
+ if args:
+ config.process_args(args)
+
+ return config
+
+Call time order
+^^^^^^^^^^^^^^^
+By default hooks are :ref:`called <calling>` in LIFO registered order, however,
+a *hookimpl* can influence its call-time invocation position using special
+attributes. If marked with a ``"tryfirst"`` or ``"trylast"`` option it
+will be executed *first* or *last* respectively in the hook call loop:
+
+.. code-block:: python
+
+ import sys
+ from pluggy import PluginManager, HookimplMarker
+
+ hookimpl = HookimplMarker("myproject")
+
+
+ @hookimpl(trylast=True)
+ def setup_project(config, args):
+ """Default implementation."""
+ if args:
+ config.process_args(args)
+
+ return config
+
+
+ class SomeOtherPlugin:
+ """Some other plugin defining the same hook."""
+
+ @hookimpl(tryfirst=True)
+ def setup_project(self, config, args):
+ """Report what args were passed before calling
+ downstream hooks.
+ """
+ if args:
+ print("Got args: {}".format(args))
+
+ return config
+
+
+ pm = PluginManager("myproject")
+
+ # load from the local module's namespace
+ pm.register(sys.modules[__name__])
+ # load a plugin defined on a class
+ pm.register(SomeOtherPlugin())
+
+For another example see the :ref:`pytest:plugin-hookorder` section of the
+``pytest`` docs.
+
+.. note::
+ ``tryfirst`` and ``trylast`` hooks are still invoked in LIFO order within
+ each category.
+
+
+.. _hookwrappers:
+
+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:
+
+
+.. code-block:: python
+
+ @hookimpl(hookwrapper=True)
+ def setup_project(config, args):
+ """Wrap calls to ``setup_project()`` implementations which
+ should return json encoded config options.
+ """
+ if config.debug:
+ print("Pre-hook config is {}".format(config.tojson()))
+
+ # get initial default config
+ defaults = config.tojson()
+
+ # all corresponding hookimpls are invoked here
+ outcome = yield
+
+ for item in outcome.get_result():
+ print("JSON config override is {}".format(item))
+
+ if config.debug:
+ print("Post-hook config is {}".format(config.tojson()))
+
+ 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.
+
+.. note::
+ Hook wrappers can **not** return results (as per generator function
+ semantics); they can only modify them using the ``_Result`` API.
+
+Also see the :ref:`pytest:hookwrapper` section in the ``pytest`` docs.
+
+.. _specs:
+
+Specifications
+--------------
+A hook *specification* (*hookspec*) is a definition used to validate each
+*hookimpl* ensuring that an extension writer has correctly defined their
+callback function *implementation* .
+
+*hookspecs* are defined using similarly marked functions however only the
+function *signature* (its name and names of all its arguments) is analyzed
+and stored. As such, often you will see a *hookspec* defined with only
+a docstring in its body.
+
+*hookspecs* are loaded using the
+:py:meth:`~pluggy.PluginManager.add_hookspecs()` method and normally
+should be added before registering corresponding *hookimpls*:
+
+.. code-block:: python
+
+ import sys
+ from pluggy import PluginManager, HookspecMarker
+
+ hookspec = HookspecMarker("myproject")
+
+
+ @hookspec
+ def setup_project(config, args):
+ """This hook is used to process the initial config and input
+ arguments.
+ """
+
+
+ pm = PluginManager("myproject")
+
+ # load from the local module's namespace
+ pm.add_hookspecs(sys.modules[__name__])
+
+
+Registering a *hookimpl* which does not meet the constraints of its
+corresponding *hookspec* will result in an error.
+
+A *hookspec* can also be added **after** some *hookimpls* have been
+registered however this is not normally recommended as it results in
+delayed hook validation.
+
+.. note::
+ The term *hookspec* can sometimes refer to the plugin-namespace
+ which defines ``hookspec`` decorated functions as in the case of
+ ``pytest``'s `hookspec module`_
+
+.. _enforcing:
+
+Enforcing spec validation
+^^^^^^^^^^^^^^^^^^^^^^^^^
+By default there is no strict requirement that each *hookimpl* has
+a corresponding *hookspec*. However, if you'd like you enforce this
+behavior you can run a check with the
+:py:meth:`~pluggy.PluginManager.check_pending()` method. If you'd like
+to enforce requisite *hookspecs* but with certain exceptions for some hooks
+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.
+This allows for extending hook arguments (and thus semantics) without
+breaking existing *hookimpls*.
+
+In other words this is ok:
+
+.. code-block:: python
+
+ @hookspec
+ def myhook(config, args):
+ pass
+
+
+ @hookimpl
+ def myhook(args):
+ print(args)
+
+
+whereas this is not:
+
+.. code-block:: python
+
+ @hookspec
+ def myhook(config, args):
+ pass
+
+
+ @hookimpl
+ def myhook(config, args, extra_arg):
+ print(args)
+
+.. note::
+ The one exception to this rule (that a *hookspec* must have as least as
+ many arguments as its *hookimpls*) is the conventional :ref:`self <python:tut-remarks>` arg; this
+ is always ignored when *hookimpls* are defined as :ref:`methods <python:tut-methodobjects>`.
+
+.. _firstresult:
+
+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``.
+
+.. code-block:: python
+
+ @hookspec(firstresult=True)
+ def myhook(config, args):
+ pass
+
+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.
+
+Also see the :ref:`pytest:firstresult` section in the ``pytest`` docs.
+
+.. _historic:
+
+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**
+having been registered:
+
+.. code-block:: python
+
+ @hookspec(historic=True)
+ def myhook(config, args):
+ pass
+
+The implication is that late registered *hookimpls* will be called back
+immediately at register time and **can not** return a result to the caller.
+
+This turns out to be particularly useful when dealing with lazy or
+dynamically loaded plugins.
+
+For more info see :ref:`call_historic`.
+
+
+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.
+
+
+.. code-block:: python
+
+ @hookspec(
+ warn_on_impl=DeprecationWarning("oldhook is deprecated and will be removed soon")
+ )
+ def oldhook():
+ pass
+
+.. _manage:
+
+The Plugin registry
+*******************
+``pluggy`` manages plugins using instances of the
+:py:class:`pluggy.PluginManager`.
+
+A :py:class:`~pluggy.PluginManager` is instantiated with a single
+``str`` argument, the ``project_name``:
+
+.. code-block:: python
+
+ import pluggy
+
+ pm = pluggy.PluginManager("my_project_name")
+
+
+The ``project_name`` value is used when a :py:class:`~pluggy.PluginManager`
+scans for *hook* functions :ref:`defined on a plugin <define>`.
+This allows for multiple plugin managers from multiple projects
+to define hooks alongside each other.
+
+.. _registration:
+
+Registration
+------------
+Each :py:class:`~pluggy.PluginManager` maintains a *plugin* registry where each *plugin*
+contains a set of *hookimpl* definitions. Loading *hookimpl* and *hookspec*
+definitions to populate the registry is described in detail in the section on
+:ref:`define`.
+
+In summary, you pass a plugin namespace object to the
+:py:meth:`~pluggy.PluginManager.register()` and
+:py:meth:`~pluggy.PluginManager.add_hookspecs()` methods to collect
+hook *implementations* and *specifications* from *plugin* namespaces respectively.
+
+You can unregister any *plugin*'s hooks using
+:py:meth:`~pluggy.PluginManager.unregister()` and check if a plugin is
+registered by passing its name to the
+:py:meth:`~pluggy.PluginManager.is_registered()` method.
+
+Loading ``setuptools`` entry points
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+You can automatically load plugins registered through
+:ref:`setuptools entry points <setuptools:entry_points>`
+with the :py:meth:`~pluggy.PluginManager.load_setuptools_entrypoints()`
+method.
+
+An example use of this is the :ref:`pytest entry point <pytest:pip-installable plugins>`.
+
+
+Blocking
+--------
+You can block any plugin from being registered using
+:py:meth:`~pluggy.PluginManager.set_blocked()` and check if a given
+*plugin* is blocked by name using :py:meth:`~pluggy.PluginManager.is_blocked()`.
+
+
+Inspection
+----------
+You can use a variety of methods to inspect both the registry
+and particular plugins in it:
+
+- :py:meth:`~pluggy.PluginManager.list_name_plugin()` -
+ return a list of name-plugin pairs
+- :py:meth:`~pluggy.PluginManager.get_plugins()` - retrieve all plugins
+- :py:meth:`~pluggy.PluginManager.get_canonical_name()`- get a *plugin*'s
+ canonical name (the name it was registered with)
+- :py:meth:`~pluggy.PluginManager.get_plugin()` - retrieve a plugin by its
+ canonical name
+
+
+Parsing mark options
+^^^^^^^^^^^^^^^^^^^^
+You can retrieve the *options* applied to a particular
+*hookspec* or *hookimpl* as per :ref:`marking_hooks` using the
+:py:meth:`~pluggy.PluginManager.parse_hookspec_opts()` and
+:py:meth:`~pluggy.PluginManager.parse_hookimpl_opts()` respectively.
+
+
+.. _calling:
+
+Calling hooks
+*************
+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
+``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.
+
+More practically you call a *hook* like so:
+
+.. code-block:: python
+
+ import sys
+ import pluggy
+ import mypluginspec
+ import myplugin
+ from configuration import config
+
+ pm = pluggy.PluginManager("myproject")
+ pm.add_hookspecs(mypluginspec)
+ pm.register(myplugin)
+
+ # 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!
+
+Hook implementations are called in LIFO registered order: *the last
+registered plugin's hooks are called first*. As an example, the below
+assertion should not error:
+
+.. code-block:: python
+
+ from pluggy import PluginManager, HookimplMarker
+
+ hookimpl = HookimplMarker("myproject")
+
+
+ class Plugin1:
+ @hookimpl
+ def myhook(self, args):
+ """Default implementation."""
+ return 1
+
+
+ class Plugin2:
+ @hookimpl
+ def myhook(self, args):
+ """Default implementation."""
+ return 2
+
+
+ class Plugin3:
+ @hookimpl
+ def myhook(self, args):
+ """Default implementation."""
+ return 3
+
+
+ pm = PluginManager("myproject")
+ pm.register(Plugin1())
+ pm.register(Plugin2())
+ pm.register(Plugin3())
+
+ assert pm.hook.myhook(args=()) == [3, 2, 1]
+
+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
+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
+its :ref:`first result only <firstresult>` in which case only the first
+single value (which is not ``None``) will be returned.
+
+.. _call_historic:
+
+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:
+
+.. code-block:: python
+
+ from pluggy import PluginManager, HookimplMarker
+
+ hookimpl = HookimplMarker("myproject")
+
+
+ class Plugin1:
+ @hookimpl
+ def myhook(self, args):
+ return 1
+
+
+ class Plugin2:
+ @hookimpl
+ def myhook(self, args):
+ raise RuntimeError
+
+
+ class Plugin3:
+ @hookimpl
+ def myhook(self, args):
+ return 3
+
+
+ @hookimpl(hookwrapper=True)
+ def myhook(self, args):
+ outcome = yield
+
+ try:
+ outcome.get_result()
+ except RuntimeError:
+ # log the error details
+ print(outcome.excinfo)
+
+
+ pm = PluginManager("myproject")
+
+ # register plugins
+ pm.register(Plugin1())
+ pm.register(Plugin2())
+ pm.register(Plugin3())
+
+ # register wrapper
+ pm.register(sys.modules[__name__])
+
+ # this raises RuntimeError due to Plugin2
+ pm.hook.myhook(args=())
+
+Historic calls
+--------------
+A *historic call* allows for all newly registered functions to receive all hook
+calls that happened before their registration. The implication is that this is
+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:
+
+.. code-block:: python
+
+ def callback(result):
+ print("historic call result is {result}".format(result=result))
+
+
+ # call with history; no results returned
+ pm.hook.myhook.call_historic(
+ kwargs={"config": config, "args": sys.argv}, result_callback=callback
+ )
+
+ # ... more of our program ...
+
+ # late loading of some plugin
+ import mylateplugin
+
+ # 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)
+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
+is registered.
+
+.. note::
+ *historic* calls are incompatible with :ref:`firstresult` marked
+ hooks since only the first registered plugin's hook(s) would
+ ever be called.
+
+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.
+
+
+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
+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.
+
+Built-in tracing
+****************
+``pluggy`` comes with some batteries included hook tracing for your
+debugging needs.
+
+
+Call tracing
+------------
+To enable tracing use the
+:py:meth:`pluggy.PluginManager.enable_tracing()` method which returns an
+undo function to disable the behaviour.
+
+.. code-block:: python
+
+ pm = PluginManager("myproject")
+ # magic line to set a writer function
+ pm.trace.root.setwriter(print)
+ undo = pm.enable_tracing()
+
+
+Call monitoring
+---------------
+Instead of using the built-in tracing mechanism you can also add your
+own ``before`` and ``after`` monitoring functions using
+:py:class:`pluggy.PluginManager.add_hookcall_monitoring()`.
+
+The expected signature and default implementations for these functions is:
+
+.. code-block:: python
+
+ def before(hook_name, methods, kwargs):
+ pass
+
+
+ def after(outcome, hook_name, methods, kwargs):
+ pass
+
+Public API
+**********
+Please see the :doc:`api_reference`.
+
+Development
+***********
+Great care must taken when hacking on ``pluggy`` since multiple mature
+projects rely on it. Our Github integrated CI process runs the full
+`tox test suite`_ on each commit so be sure your changes can run on
+all required `Python interpreters`_ and ``pytest`` versions.
+
+For development, we suggest to create a virtual environment and install ``pluggy`` in
+editable mode and ``dev`` dependencies::
+
+ $ python3 -m venv .env
+ $ source .env/bin/activate
+ $ pip install -e .[dev]
+
+To make sure you follow the code style used in the project, install pre-commit_ which
+will run style checks before each commit::
+
+ $ pre-commit install
+
+
+Release Policy
+**************
+Pluggy uses `Semantic Versioning`_. Breaking changes are only foreseen for
+Major releases (incremented X in "X.Y.Z"). If you want to use ``pluggy``
+in your project you should thus use a dependency restriction like
+``"pluggy>=0.1.0,<1.0"`` to avoid surprises.
+
+
+Table of contents
+*****************
+
+.. toctree::
+ :maxdepth: 2
+
+ api_reference
+ changelog
+
+
+
+.. hyperlinks
+.. _hookspec module:
+ https://docs.pytest.org/en/latest/_modules/_pytest/hookspec.html
+.. _request-response pattern:
+ https://en.wikipedia.org/wiki/Request%E2%80%93response
+.. _publish-subscribe:
+ https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern
+.. _hooking:
+ https://en.wikipedia.org/wiki/Hooking
+.. _callbacks:
+ https://en.wikipedia.org/wiki/Callback_(computer_programming)
+.. _tox test suite:
+ https://github.com/pytest-dev/pluggy/blob/master/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/
+.. _pre-commit:
+ https://pre-commit.com/
+
+
+.. Indices and tables
+.. ==================
+.. * :ref:`genindex`
+.. * :ref:`modindex`
+.. * :ref:`search`
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/pyproject.toml b/testing/web-platform/tests/tools/third_party/pluggy/pyproject.toml
new file mode 100644
index 0000000000..15eba26898
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/pyproject.toml
@@ -0,0 +1,47 @@
+[build-system]
+requires = [
+ "setuptools",
+ "setuptools-scm",
+ "wheel",
+]
+
+[tool.setuptools_scm]
+write_to = "src/pluggy/_version.py"
+
+[tool.towncrier]
+package = "pluggy"
+package_dir = "src/pluggy"
+filename = "CHANGELOG.rst"
+directory = "changelog/"
+title_format = "pluggy {version} ({project_date})"
+template = "changelog/_template.rst"
+
+ [[tool.towncrier.type]]
+ directory = "removal"
+ name = "Deprecations and Removals"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "feature"
+ name = "Features"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "bugfix"
+ name = "Bug Fixes"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "vendor"
+ name = "Vendored Libraries"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "doc"
+ name = "Improved Documentation"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "trivial"
+ name = "Trivial/Internal Changes"
+ showcontent = true
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/scripts/release.py b/testing/web-platform/tests/tools/third_party/pluggy/scripts/release.py
new file mode 100644
index 0000000000..e09b8c77b1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/scripts/release.py
@@ -0,0 +1,69 @@
+"""
+Release script.
+"""
+import argparse
+import sys
+from subprocess import check_call
+
+from colorama import init, Fore
+from git import Repo, Remote
+
+
+def create_branch(version):
+ """Create a fresh branch from upstream/main"""
+ repo = Repo.init(".")
+ if repo.is_dirty(untracked_files=True):
+ raise RuntimeError("Repository is dirty, please commit/stash your changes.")
+
+ branch_name = f"release-{version}"
+ print(f"{Fore.CYAN}Create {branch_name} branch from upstream main")
+ upstream = get_upstream(repo)
+ upstream.fetch()
+ release_branch = repo.create_head(branch_name, upstream.refs.main, force=True)
+ release_branch.checkout()
+ return repo
+
+
+def get_upstream(repo: Repo) -> Remote:
+ """Find upstream repository for pluggy on the remotes"""
+ for remote in repo.remotes:
+ for url in remote.urls:
+ if url.endswith(("pytest-dev/pluggy.git", "pytest-dev/pluggy")):
+ return remote
+ raise RuntimeError("could not find pytest-dev/pluggy remote")
+
+
+def pre_release(version):
+ """Generates new docs, release announcements and creates a local tag."""
+ create_branch(version)
+ changelog(version, write_out=True)
+
+ check_call(["git", "commit", "-a", "-m", f"Preparing release {version}"])
+
+ print()
+ print(f"{Fore.GREEN}Please push your branch to your fork and open a PR.")
+
+
+def changelog(version, write_out=False):
+ if write_out:
+ addopts = []
+ else:
+ addopts = ["--draft"]
+ print(f"{Fore.CYAN}Generating CHANGELOG")
+ check_call(["towncrier", "--yes", "--version", version] + addopts)
+
+
+def main():
+ init(autoreset=True)
+ parser = argparse.ArgumentParser()
+ parser.add_argument("version", help="Release version")
+ options = parser.parse_args()
+ try:
+ pre_release(options.version)
+ except RuntimeError as e:
+ print(f"{Fore.RED}ERROR: {e}")
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/scripts/upload-coverage.sh b/testing/web-platform/tests/tools/third_party/pluggy/scripts/upload-coverage.sh
new file mode 100755
index 0000000000..ad3dd48281
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/scripts/upload-coverage.sh
@@ -0,0 +1,16 @@
+#!/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/testing/web-platform/tests/tools/third_party/pluggy/setup.cfg b/testing/web-platform/tests/tools/third_party/pluggy/setup.cfg
new file mode 100644
index 0000000000..7040bcb83b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/setup.cfg
@@ -0,0 +1,52 @@
+[bdist_wheel]
+universal=1
+
+[metadata]
+name = pluggy
+description = plugin and hook calling mechanisms for python
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+license = MIT
+platforms = unix, linux, osx, win32
+author = Holger Krekel
+author_email = holger@merlinux.eu
+url = https://github.com/pytest-dev/pluggy
+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 :: Implementation :: CPython
+ 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
+
+[options]
+packages =
+ pluggy
+install_requires =
+ importlib-metadata>=0.12;python_version<"3.8"
+python_requires = >=3.6
+package_dir =
+ =src
+setup_requires =
+ setuptools-scm
+[options.extras_require]
+dev =
+ pre-commit
+ tox
+testing =
+ pytest
+ pytest-benchmark
+
+[devpi:upload]
+formats=sdist.tgz,bdist_wheel
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/setup.py b/testing/web-platform/tests/tools/third_party/pluggy/setup.py
new file mode 100644
index 0000000000..ed442375f7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/setup.py
@@ -0,0 +1,5 @@
+from setuptools import setup
+
+
+if __name__ == "__main__":
+ setup(use_scm_version={"write_to": "src/pluggy/_version.py"})
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/__init__.py b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/__init__.py
new file mode 100644
index 0000000000..979028f759
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/__init__.py
@@ -0,0 +1,18 @@
+try:
+ from ._version import version as __version__
+except ImportError:
+ # broken installation, we don't even try
+ # unknown only works because we do poor mans version compare
+ __version__ = "unknown"
+
+__all__ = [
+ "PluginManager",
+ "PluginValidationError",
+ "HookCallError",
+ "HookspecMarker",
+ "HookimplMarker",
+]
+
+from ._manager import PluginManager, PluginValidationError
+from ._callers import HookCallError
+from ._hooks import HookspecMarker, HookimplMarker
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_callers.py b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_callers.py
new file mode 100644
index 0000000000..7a16f3bdd4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_callers.py
@@ -0,0 +1,60 @@
+"""
+Call loop machinery
+"""
+import sys
+
+from ._result import HookCallError, _Result, _raise_wrapfail
+
+
+def _multicall(hook_name, hook_impls, caller_kwargs, firstresult):
+ """Execute a call into multiple python functions/methods and return the
+ result(s).
+
+ ``caller_kwargs`` comes from _HookCaller.__call__().
+ """
+ __tracebackhide__ = True
+ results = []
+ excinfo = None
+ try: # run impl and wrapper setup functions in a loop
+ teardowns = []
+ try:
+ for hook_impl in reversed(hook_impls):
+ try:
+ args = [caller_kwargs[argname] for argname in hook_impl.argnames]
+ except KeyError:
+ for argname in hook_impl.argnames:
+ if argname not in caller_kwargs:
+ raise HookCallError(
+ f"hook call must provide argument {argname!r}"
+ )
+
+ if hook_impl.hookwrapper:
+ try:
+ gen = hook_impl.function(*args)
+ next(gen) # first yield
+ teardowns.append(gen)
+ except StopIteration:
+ _raise_wrapfail(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()
+ finally:
+ if firstresult: # first result hooks return a single value
+ outcome = _Result(results[0] if results else None, excinfo)
+ else:
+ outcome = _Result(results, excinfo)
+
+ # run all wrapper post-yield blocks
+ for gen in reversed(teardowns):
+ try:
+ gen.send(outcome)
+ _raise_wrapfail(gen, "has second yield")
+ except StopIteration:
+ pass
+
+ return outcome.get_result()
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_hooks.py b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_hooks.py
new file mode 100644
index 0000000000..1e5fbb7595
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_hooks.py
@@ -0,0 +1,325 @@
+"""
+Internal hook annotation, representation and calling machinery.
+"""
+import inspect
+import sys
+import warnings
+
+
+class HookspecMarker:
+ """Decorator helper class 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.
+ """
+
+ def __init__(self, project_name):
+ self.project_name = project_name
+
+ 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.
+
+ 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.
+
+ If ``historic`` is ``True`` calls to a hook will be memorized and replayed
+ on later registered plugins.
+
+ """
+
+ def setattr_hookspec_opts(func):
+ 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,
+ ),
+ )
+ return func
+
+ if function is not None:
+ return setattr_hookspec_opts(function)
+ else:
+ return setattr_hookspec_opts
+
+
+class HookimplMarker:
+ """Decorator helper class 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.
+ """
+
+ def __init__(self, project_name):
+ self.project_name = project_name
+
+ 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`.
+ 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.
+
+ """
+
+ def setattr_hookimpl_opts(func):
+ setattr(
+ func,
+ self.project_name + "_impl",
+ dict(
+ hookwrapper=hookwrapper,
+ optionalhook=optionalhook,
+ tryfirst=tryfirst,
+ trylast=trylast,
+ specname=specname,
+ ),
+ )
+ return func
+
+ if function is None:
+ return setattr_hookimpl_opts
+ else:
+ return setattr_hookimpl_opts(function)
+
+
+def normalize_hookimpl_opts(opts):
+ opts.setdefault("tryfirst", False)
+ opts.setdefault("trylast", False)
+ opts.setdefault("hookwrapper", False)
+ opts.setdefault("optionalhook", False)
+ opts.setdefault("specname", None)
+
+
+_PYPY = hasattr(sys, "pypy_version_info")
+
+
+def varnames(func):
+ """Return tuple of positional and keywrord argument names for a function,
+ method, class or callable.
+
+ In case of a class, its ``__init__`` method is considered.
+ For methods the ``self`` parameter is not included.
+ """
+ if inspect.isclass(func):
+ try:
+ func = func.__init__
+ except AttributeError:
+ return (), ()
+ elif not inspect.isroutine(func): # callable object?
+ try:
+ func = getattr(func, "__call__", func)
+ except Exception:
+ return (), ()
+
+ try: # func MUST be a function or method here or we won't parse any args
+ spec = inspect.getfullargspec(func)
+ except TypeError:
+ return (), ()
+
+ args, defaults = tuple(spec.args), spec.defaults
+ if defaults:
+ index = -len(defaults)
+ args, kwargs = args[:index], tuple(args[index:])
+ else:
+ kwargs = ()
+
+ # 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 args:
+ if inspect.ismethod(func) or (
+ "." in getattr(func, "__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.
+
+ """
+
+
+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 specmodule_or_class is not None:
+ assert spec_opts is not None
+ self.set_specification(specmodule_or_class, spec_opts)
+
+ def has_spec(self):
+ return self.spec is not None
+
+ def set_specification(self, specmodule_or_class, spec_opts):
+ assert not self.has_spec()
+ self.spec = HookSpec(specmodule_or_class, self.name, spec_opts)
+ if spec_opts.get("historic"):
+ self._call_history = []
+
+ def is_historic(self):
+ 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 get_hookimpls(self):
+ # Order is important for _hookexec
+ return self._nonwrappers + self._wrappers
+
+ def _add_hookimpl(self, hookimpl):
+ """Add an implementation to the callback chain."""
+ if hookimpl.hookwrapper:
+ methods = self._wrappers
+ else:
+ methods = self._nonwrappers
+
+ if hookimpl.trylast:
+ methods.insert(0, hookimpl)
+ elif hookimpl.tryfirst:
+ methods.append(hookimpl)
+ else:
+ # find last non-tryfirst method
+ i = len(methods) - 1
+ while i >= 0 and methods[i].tryfirst:
+ i -= 1
+ methods.insert(i + 1, hookimpl)
+
+ def __repr__(self):
+ return f"<_HookCaller {self.name!r}>"
+
+ def __call__(self, *args, **kwargs):
+ if args:
+ raise TypeError("hook calling supports only keyword arguments")
+ assert not self.is_historic()
+
+ # 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())
+ warnings.warn(
+ "Argument(s) {} which are declared in the hookspec "
+ "can not be found in this hook call".format(notincall),
+ stacklevel=2,
+ )
+ break
+
+ firstresult = self.spec.opts.get("firstresult")
+ else:
+ firstresult = False
+
+ return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
+
+ def call_historic(self, result_callback=None, kwargs=None):
+ """Call the hook with given ``kwargs`` for all registered plugins and
+ for all plugins which will be registered afterwards.
+
+ If ``result_callback`` is not ``None`` it will be called for for each
+ non-``None`` result obtained from a hook implementation.
+ """
+ self._call_history.append((kwargs or {}, 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)
+ if result_callback is None:
+ return
+ for x in res or []:
+ result_callback(x)
+
+ def call_extra(self, methods, kwargs):
+ """Call the hook with some additional temporarily participating
+ methods using the specified ``kwargs`` as call parameters."""
+ old = list(self._nonwrappers), list(self._wrappers)
+ 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
+
+ def _maybe_apply_history(self, method):
+ """Apply call history to a new hookimpl if it is marked as historic."""
+ if self.is_historic():
+ for kwargs, result_callback in self._call_history:
+ res = self._hookexec(self.name, [method], kwargs, False)
+ if res and result_callback is not None:
+ 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)
+
+ def __repr__(self):
+ return f"<HookImpl plugin_name={self.plugin_name!r}, plugin={self.plugin!r}>"
+
+
+class HookSpec:
+ def __init__(self, namespace, name, opts):
+ self.namespace = namespace
+ self.function = function = getattr(namespace, name)
+ self.name = name
+ self.argnames, self.kwargnames = varnames(function)
+ self.opts = opts
+ self.warn_on_impl = opts.get("warn_on_impl")
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_manager.py b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_manager.py
new file mode 100644
index 0000000000..65f4e50842
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_manager.py
@@ -0,0 +1,373 @@
+import inspect
+import sys
+import warnings
+
+from . import _tracing
+from ._callers import _Result, _multicall
+from ._hooks import HookImpl, _HookRelay, _HookCaller, normalize_hookimpl_opts
+
+if sys.version_info >= (3, 8):
+ from importlib import metadata as importlib_metadata
+else:
+ import importlib_metadata
+
+
+def _warn_for_function(warning, function):
+ warnings.warn_explicit(
+ warning,
+ type(warning),
+ lineno=function.__code__.co_firstlineno,
+ filename=function.__code__.co_filename,
+ )
+
+
+class PluginValidationError(Exception):
+ """plugin failed validation.
+
+ :param object plugin: the plugin which failed validation,
+ may be a module or an arbitrary object.
+ """
+
+ def __init__(self, plugin, message):
+ self.plugin = plugin
+ super(Exception, self).__init__(message)
+
+
+class DistFacade:
+ """Emulate a pkg_resources Distribution"""
+
+ def __init__(self, dist):
+ self._dist = dist
+
+ @property
+ def project_name(self):
+ return self.metadata["name"]
+
+ def __getattr__(self, attr, default=None):
+ return getattr(self._dist, attr, default)
+
+ def __dir__(self):
+ 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.
+
+ 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.
+
+ For debugging purposes you can call :py:meth:`.PluginManager.enable_tracing`
+ which will subsequently send debug information to the trace helper.
+ """
+
+ 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()
+ self._inner_hookexec = _multicall
+
+ def _hookexec(self, hook_name, methods, kwargs, firstresult):
+ # 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."""
+ plugin_name = name or self.get_canonical_name(plugin)
+
+ if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers:
+ if self._name2plugin.get(plugin_name, -1) is None:
+ return # blocked plugin, return None to indicate no registration
+ raise ValueError(
+ "Plugin already registered: %s=%s\n%s"
+ % (plugin_name, plugin, self._name2plugin)
+ )
+
+ # XXX if an error happens we should make sure no state has been
+ # changed at point of return
+ 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)
+ hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
+ name = hookimpl_opts.get("specname") or name
+ hook = getattr(self.hook, name, None)
+ if hook is None:
+ 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)
+ if not inspect.isroutine(method):
+ return
+ try:
+ res = getattr(method, self.project_name + "_impl", None)
+ except Exception:
+ res = {}
+ if res is not None and not isinstance(res, dict):
+ # false positive
+ res = None
+ return res
+
+ def unregister(self, plugin=None, name=None):
+ """unregister a plugin object and all its contained hook implementations
+ from internal data structures."""
+ if name is None:
+ assert plugin is not None, "one of name or plugin needs to be specified"
+ name = self.get_name(plugin)
+
+ if plugin is None:
+ plugin = self.get_plugin(name)
+
+ # if self._name2plugin[name] == None registration was blocked: ignore
+ if self._name2plugin.get(name):
+ 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."""
+ self.unregister(name=name)
+ self._name2plugin[name] = None
+
+ def is_blocked(self, name):
+ """return ``True`` if 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."""
+ 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)
+ if hc is None:
+ hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts)
+ setattr(self.hook, name, hc)
+ else:
+ # 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)
+ names.append(name)
+
+ if not names:
+ raise ValueError(
+ f"did not find any {self.project_name!r} hooks in {module_or_class!r}"
+ )
+
+ def parse_hookspec_opts(self, module_or_class, name):
+ 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."""
+ return self._name2plugin.get(name)
+
+ def has_plugin(self, name):
+ """Return ``True`` if 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."""
+ for name, val in self._name2plugin.items():
+ if plugin == val:
+ return name
+
+ def _verify_hook(self, hook, hookimpl):
+ if hook.is_historic() and hookimpl.hookwrapper:
+ raise PluginValidationError(
+ hookimpl.plugin,
+ "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper"
+ % (hookimpl.plugin_name, hook.name),
+ )
+
+ if hook.spec.warn_on_impl:
+ _warn_for_function(hook.spec.warn_on_impl, hookimpl.function)
+
+ # positional arg checking
+ notinspec = set(hookimpl.argnames) - set(hook.spec.argnames)
+ if notinspec:
+ raise PluginValidationError(
+ hookimpl.plugin,
+ "Plugin %r for hook %r\nhookimpl definition: %s\n"
+ "Argument(s) %s are declared in the hookimpl but "
+ "can not be found in the hookspec"
+ % (
+ hookimpl.plugin_name,
+ hook.name,
+ _formatdef(hookimpl.function),
+ notinspec,
+ ),
+ )
+
+ if 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"
+ % (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`."""
+ for name in self.hook.__dict__:
+ if name[0] != "_":
+ hook = getattr(self.hook, name)
+ if not hook.has_spec():
+ for hookimpl in hook.get_hookimpls():
+ if not hookimpl.optionalhook:
+ raise PluginValidationError(
+ hookimpl.plugin,
+ "unknown hook %r in plugin %r"
+ % (name, hookimpl.plugin),
+ )
+
+ def load_setuptools_entrypoints(self, group, name=None):
+ """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.
+ """
+ count = 0
+ for dist in list(importlib_metadata.distributions()):
+ for ep in dist.entry_points:
+ if (
+ ep.group != group
+ or (name is not None and ep.name != name)
+ # already registered
+ or self.get_plugin(ep.name)
+ or self.is_blocked(ep.name)
+ ):
+ continue
+ plugin = ep.load()
+ self.register(plugin, name=ep.name)
+ self._plugin_distinfo.append((plugin, DistFacade(dist)))
+ count += 1
+ return count
+
+ def list_plugin_distinfo(self):
+ """return list of distinfo/plugin tuples for all setuptools registered
+ plugins."""
+ return list(self._plugin_distinfo)
+
+ def list_name_plugin(self):
+ """return list of name/plugin pairs."""
+ return list(self._name2plugin.items())
+
+ def get_hookcallers(self, plugin):
+ """get all hook callers for the specified plugin."""
+ return self._plugin2hookcallers.get(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.
+
+ ``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
+ 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)
+ )
+ after(outcome, hook_name, hook_impls, kwargs)
+ return outcome.get_result()
+
+ self._inner_hookexec = traced_hookexec
+
+ def undo():
+ self._inner_hookexec = oldcall
+
+ return undo
+
+ def enable_tracing(self):
+ """enable tracing of hook calls and return an undo function."""
+ hooktrace = self.trace.root.get("hook")
+
+ def before(hook_name, methods, kwargs):
+ hooktrace.root.indent += 1
+ hooktrace(hook_name, kwargs)
+
+ def after(outcome, hook_name, methods, kwargs):
+ if outcome.excinfo 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)]
+ 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 orig
+
+
+def _formatdef(func):
+ return f"{func.__name__}{inspect.signature(func)}"
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_result.py b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_result.py
new file mode 100644
index 0000000000..4c1f7f1f3c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_result.py
@@ -0,0 +1,60 @@
+"""
+Hook wrapper "result" utilities.
+"""
+import sys
+
+
+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)
+ )
+
+
+class HookCallError(Exception):
+ """Hook was called wrongly."""
+
+
+class _Result:
+ def __init__(self, result, excinfo):
+ self._result = result
+ self._excinfo = excinfo
+
+ @property
+ def excinfo(self):
+ return self._excinfo
+
+ @classmethod
+ def from_call(cls, func):
+ __tracebackhide__ = True
+ result = excinfo = None
+ try:
+ result = func()
+ except BaseException:
+ excinfo = sys.exc_info()
+
+ return cls(result, excinfo)
+
+ def force_result(self, result):
+ """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
+ found during invocation will be deleted.
+ """
+ self._result = result
+ self._excinfo = None
+
+ def get_result(self):
+ """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.
+ """
+ __tracebackhide__ = True
+ if self._excinfo is None:
+ return self._result
+ else:
+ ex = self._excinfo
+ raise ex[1].with_traceback(ex[2])
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_tracing.py b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_tracing.py
new file mode 100644
index 0000000000..82c016271e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_tracing.py
@@ -0,0 +1,62 @@
+"""
+Tracing utils
+"""
+
+
+class TagTracer:
+ def __init__(self):
+ self._tags2proc = {}
+ self._writer = None
+ self.indent = 0
+
+ def get(self, name):
+ return TagTracerSub(self, (name,))
+
+ def _format_message(self, tags, args):
+ if isinstance(args[-1], dict):
+ extra = args[-1]
+ args = args[:-1]
+ else:
+ extra = {}
+
+ content = " ".join(map(str, args))
+ indent = " " * self.indent
+
+ lines = ["{}{} [{}]\n".format(indent, content, ":".join(tags))]
+
+ for name, value in extra.items():
+ lines.append(f"{indent} {name}: {value}\n")
+
+ return "".join(lines)
+
+ def _processmessage(self, tags, args):
+ if self._writer is not None and args:
+ self._writer(self._format_message(tags, args))
+ try:
+ processor = self._tags2proc[tags]
+ except KeyError:
+ pass
+ else:
+ processor(tags, args)
+
+ def setwriter(self, writer):
+ self._writer = writer
+
+ def setprocessor(self, tags, processor):
+ if isinstance(tags, str):
+ tags = tuple(tags.split(":"))
+ else:
+ assert isinstance(tags, tuple)
+ self._tags2proc[tags] = processor
+
+
+class TagTracerSub:
+ def __init__(self, root, tags):
+ self.root = root
+ self.tags = tags
+
+ def __call__(self, *args):
+ self.root._processmessage(self.tags, args)
+
+ def get(self, name):
+ return self.__class__(self.root, self.tags + (name,))
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/benchmark.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/benchmark.py
new file mode 100644
index 0000000000..b0d4b9536a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/benchmark.py
@@ -0,0 +1,102 @@
+"""
+Benchmarking and performance tests.
+"""
+import pytest
+from pluggy import HookspecMarker, HookimplMarker, PluginManager
+from pluggy._hooks import HookImpl
+from pluggy._callers import _multicall
+
+
+hookspec = HookspecMarker("example")
+hookimpl = HookimplMarker("example")
+
+
+@hookimpl
+def hook(arg1, arg2, arg3):
+ return arg1, arg2, arg3
+
+
+@hookimpl(hookwrapper=True)
+def wrapper(arg1, arg2, arg3):
+ yield
+
+
+@pytest.fixture(params=[10, 100], ids="hooks={}".format)
+def hooks(request):
+ return [hook for i in range(request.param)]
+
+
+@pytest.fixture(params=[10, 100], ids="wrappers={}".format)
+def wrappers(request):
+ return [wrapper for i in range(request.param)]
+
+
+def test_hook_and_wrappers_speed(benchmark, hooks, wrappers):
+ def setup():
+ hook_name = "foo"
+ hook_impls = []
+ for method in hooks + wrappers:
+ f = HookImpl(None, "<temp>", method, method.example_impl)
+ hook_impls.append(f)
+ caller_kwargs = {"arg1": 1, "arg2": 2, "arg3": 3}
+ firstresult = False
+ return (hook_name, hook_impls, caller_kwargs, firstresult), {}
+
+ benchmark.pedantic(_multicall, setup=setup)
+
+
+@pytest.mark.parametrize(
+ ("plugins, wrappers, nesting"),
+ [
+ (1, 1, 0),
+ (1, 1, 1),
+ (1, 1, 5),
+ (1, 5, 1),
+ (1, 5, 5),
+ (5, 1, 1),
+ (5, 1, 5),
+ (5, 5, 1),
+ (5, 5, 5),
+ (20, 20, 0),
+ (100, 100, 0),
+ ],
+)
+def test_call_hook(benchmark, plugins, wrappers, nesting):
+ pm = PluginManager("example")
+
+ class HookSpec:
+ @hookspec
+ def fun(self, hooks, nesting: int):
+ yield
+
+ class Plugin:
+ def __init__(self, num):
+ self.num = num
+
+ def __repr__(self):
+ return f"<Plugin {self.num}>"
+
+ @hookimpl
+ def fun(self, hooks, nesting: int):
+ if nesting:
+ hooks.fun(hooks=hooks, nesting=nesting - 1)
+
+ class PluginWrap:
+ def __init__(self, num):
+ self.num = num
+
+ def __repr__(self):
+ return f"<PluginWrap {self.num}>"
+
+ @hookimpl(hookwrapper=True)
+ def fun(self):
+ yield
+
+ pm.add_hookspecs(HookSpec)
+
+ for i in range(plugins):
+ pm.register(Plugin(i), name=f"plug_{i}")
+ for i in range(wrappers):
+ pm.register(PluginWrap(i), name=f"wrap_plug_{i}")
+
+ benchmark(pm.hook.fun, hooks=pm.hook, nesting=nesting)
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/conftest.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/conftest.py
new file mode 100644
index 0000000000..1fd4ecd5bd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/conftest.py
@@ -0,0 +1,26 @@
+import pytest
+
+
+@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
+
+ hookspec = HookspecMarker("example")
+
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ return arg + 1
+
+ pm.add_hookspecs(request.param(Hooks))
+ return pm
+
+
+@pytest.fixture
+def pm():
+ from pluggy import PluginManager
+
+ return PluginManager("example")
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_details.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_details.py
new file mode 100644
index 0000000000..0ceb3b3eb1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_details.py
@@ -0,0 +1,135 @@
+import warnings
+import pytest
+from pluggy import PluginManager, HookimplMarker, HookspecMarker
+
+hookspec = HookspecMarker("example")
+hookimpl = HookimplMarker("example")
+
+
+def test_parse_hookimpl_override():
+ 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 = {}
+ return opts
+
+ class Plugin:
+ def x1meth(self):
+ pass
+
+ @hookimpl(hookwrapper=True, tryfirst=True)
+ def x1meth2(self):
+ yield # pragma: no cover
+
+ class Spec:
+ @hookspec
+ def x1meth(self):
+ pass
+
+ @hookspec
+ def x1meth2(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):
+ warning = DeprecationWarning("foo is deprecated")
+
+ class Spec:
+ @hookspec(warn_on_impl=warning)
+ def foo(self):
+ pass
+
+ class Plugin:
+ @hookimpl
+ def foo(self):
+ pass
+
+ pm = PluginManager(hookspec.project_name)
+ pm.add_hookspecs(Spec)
+
+ with pytest.warns(DeprecationWarning) as records:
+ pm.register(Plugin())
+ (record,) = records
+ assert record.message is warning
+ assert record.filename == Plugin.foo.__code__.co_filename
+ assert record.lineno == Plugin.foo.__code__.co_firstlineno
+
+
+def test_plugin_getattr_raises_errors():
+ """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")
+
+ class Module:
+ pass
+
+ module = Module()
+ module.x = DontTouchMe()
+
+ pm = PluginManager(hookspec.project_name)
+ # register() would raise an error
+ pm.register(module, "donttouch")
+ 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.
+ """
+
+ class Spec:
+ @hookspec
+ def myhook(self, arg1, arg2):
+ pass
+
+ class Plugin:
+ @hookimpl
+ def myhook(self, arg1):
+ pass
+
+ pm = PluginManager(hookspec.project_name)
+ pm.register(Plugin())
+ pm.add_hookspecs(Spec())
+
+ with warnings.catch_warnings(record=True) as warns:
+ warnings.simplefilter("always")
+
+ # calling should trigger a warning
+ pm.hook.myhook(arg1=1)
+
+ assert len(warns) == 1
+ warning = warns[-1]
+ assert issubclass(warning.category, Warning)
+ assert "Argument(s) ('arg2',)" in str(warning.message)
+
+
+def test_repr():
+ class Plugin:
+ @hookimpl
+ def myhook(self):
+ raise NotImplementedError()
+
+ pm = PluginManager(hookspec.project_name)
+
+ plugin = Plugin()
+ pname = pm.register(plugin)
+ assert repr(pm.hook.myhook._nonwrappers[0]) == (
+ f"<HookImpl plugin_name={pname!r}, plugin={plugin!r}>"
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_helpers.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_helpers.py
new file mode 100644
index 0000000000..465858c499
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_helpers.py
@@ -0,0 +1,84 @@
+from pluggy._hooks import varnames
+from pluggy._manager import _formatdef
+
+
+def test_varnames():
+ def f(x):
+ i = 3 # noqa
+
+ class A:
+ def f(self, y):
+ pass
+
+ class B:
+ def __call__(self, z):
+ pass
+
+ assert varnames(f) == (("x",), ())
+ assert varnames(A().f) == (("y",), ())
+ assert varnames(B()) == (("z",), ())
+
+
+def test_varnames_default():
+ def f(x, y=3):
+ pass
+
+ assert varnames(f) == (("x",), ("y",))
+
+
+def test_varnames_class():
+ class C:
+ def __init__(self, x):
+ pass
+
+ class D:
+ pass
+
+ class E:
+ def __init__(self, x):
+ pass
+
+ class F:
+ pass
+
+ assert varnames(C) == (("x",), ())
+ assert varnames(D) == ((), ())
+ assert varnames(E) == (("x",), ())
+ assert varnames(F) == ((), ())
+
+
+def test_varnames_keyword_only():
+ def f1(x, *, y):
+ pass
+
+ def f2(x, *, y=3):
+ pass
+
+ def f3(x=1, *, y=3):
+ pass
+
+ assert varnames(f1) == (("x",), ())
+ assert varnames(f2) == (("x",), ())
+ assert varnames(f3) == ((), ("x",))
+
+
+def test_formatdef():
+ def function1():
+ pass
+
+ assert _formatdef(function1) == "function1()"
+
+ def function2(arg1):
+ pass
+
+ assert _formatdef(function2) == "function2(arg1)"
+
+ def function3(arg1, arg2="qwe"):
+ pass
+
+ assert _formatdef(function3) == "function3(arg1, arg2='qwe')"
+
+ def function4(arg1, *args, **kwargs):
+ pass
+
+ assert _formatdef(function4) == "function4(arg1, *args, **kwargs)"
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_hookcaller.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_hookcaller.py
new file mode 100644
index 0000000000..9eeaef8666
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_hookcaller.py
@@ -0,0 +1,272 @@
+import pytest
+
+from pluggy import HookimplMarker, HookspecMarker, PluginValidationError
+from pluggy._hooks import HookImpl
+
+hookspec = HookspecMarker("example")
+hookimpl = HookimplMarker("example")
+
+
+@pytest.fixture
+def hc(pm):
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ 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))
+ return func
+
+ return wrap
+
+ return addmeth
+
+
+def funcs(hookmethods):
+ return [hookmethod.function for hookmethod in hookmethods]
+
+
+def test_adding_nonwrappers(hc, addmeth):
+ @addmeth()
+ def he_method1():
+ pass
+
+ @addmeth()
+ def he_method2():
+ pass
+
+ @addmeth()
+ def he_method3():
+ pass
+
+ assert funcs(hc._nonwrappers) == [he_method1, he_method2, he_method3]
+
+
+def test_adding_nonwrappers_trylast(hc, addmeth):
+ @addmeth()
+ def he_method1_middle():
+ pass
+
+ @addmeth(trylast=True)
+ def he_method1():
+ pass
+
+ @addmeth()
+ def he_method1_b():
+ pass
+
+ assert funcs(hc._nonwrappers) == [he_method1, he_method1_middle, he_method1_b]
+
+
+def test_adding_nonwrappers_trylast3(hc, addmeth):
+ @addmeth()
+ def he_method1_a():
+ pass
+
+ @addmeth(trylast=True)
+ def he_method1_b():
+ pass
+
+ @addmeth()
+ def he_method1_c():
+ pass
+
+ @addmeth(trylast=True)
+ def he_method1_d():
+ pass
+
+ assert funcs(hc._nonwrappers) == [
+ he_method1_d,
+ he_method1_b,
+ he_method1_a,
+ he_method1_c,
+ ]
+
+
+def test_adding_nonwrappers_trylast2(hc, addmeth):
+ @addmeth()
+ def he_method1_middle():
+ pass
+
+ @addmeth()
+ def he_method1_b():
+ pass
+
+ @addmeth(trylast=True)
+ def he_method1():
+ pass
+
+ assert funcs(hc._nonwrappers) == [he_method1, he_method1_middle, he_method1_b]
+
+
+def test_adding_nonwrappers_tryfirst(hc, addmeth):
+ @addmeth(tryfirst=True)
+ def he_method1():
+ pass
+
+ @addmeth()
+ def he_method1_middle():
+ pass
+
+ @addmeth()
+ def he_method1_b():
+ pass
+
+ assert funcs(hc._nonwrappers) == [he_method1_middle, he_method1_b, he_method1]
+
+
+def test_adding_wrappers_ordering(hc, addmeth):
+ @addmeth(hookwrapper=True)
+ def he_method1():
+ pass
+
+ @addmeth()
+ def he_method1_middle():
+ pass
+
+ @addmeth(hookwrapper=True)
+ def he_method3():
+ pass
+
+ assert funcs(hc._nonwrappers) == [he_method1_middle]
+ assert funcs(hc._wrappers) == [he_method1, he_method3]
+
+
+def test_adding_wrappers_ordering_tryfirst(hc, addmeth):
+ @addmeth(hookwrapper=True, tryfirst=True)
+ def he_method1():
+ pass
+
+ @addmeth(hookwrapper=True)
+ def he_method2():
+ pass
+
+ assert hc._nonwrappers == []
+ assert funcs(hc._wrappers) == [he_method2, he_method1]
+
+
+def test_hookspec(pm):
+ class HookSpec:
+ @hookspec()
+ def he_myhook1(arg1):
+ pass
+
+ @hookspec(firstresult=True)
+ def he_myhook2(arg1):
+ pass
+
+ @hookspec(firstresult=False)
+ def he_myhook3(arg1):
+ pass
+
+ pm.add_hookspecs(HookSpec)
+ assert not pm.hook.he_myhook1.spec.opts["firstresult"]
+ assert pm.hook.he_myhook2.spec.opts["firstresult"]
+ 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):
+ pass
+
+ if val:
+ assert he_myhook1.example_impl.get(name)
+ else:
+ assert not hasattr(he_myhook1, name)
+
+
+def test_hookrelay_registry(pm):
+ """Verify hook caller instances are registered by name onto the relay
+ and can be likewise unregistered."""
+
+ class Api:
+ @hookspec
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+ hook = pm.hook
+ assert hasattr(hook, "hello")
+ assert repr(hook.hello).find("hello") != -1
+
+ class Plugin:
+ @hookimpl
+ def hello(self, arg):
+ return arg + 1
+
+ plugin = Plugin()
+ pm.register(plugin)
+ out = hook.hello(arg=3)
+ assert out == [4]
+ assert not hasattr(hook, "world")
+ pm.unregister(plugin)
+ assert hook.hello(arg=3) == []
+
+
+def test_hookrelay_registration_by_specname(pm):
+ """Verify hook caller instances may also be registered by specifying a
+ specname option to the hookimpl"""
+
+ class Api:
+ @hookspec
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+ hook = pm.hook
+ assert hasattr(hook, "hello")
+ assert len(pm.hook.hello.get_hookimpls()) == 0
+
+ class Plugin:
+ @hookimpl(specname="hello")
+ def foo(self, arg):
+ return arg + 1
+
+ plugin = Plugin()
+ pm.register(plugin)
+ out = hook.hello(arg=3)
+ assert out == [4]
+
+
+def test_hookrelay_registration_by_specname_raises(pm):
+ """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):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+
+ # make sure a bad signature still raises an error when using specname
+ class Plugin:
+ @hookimpl(specname="hello")
+ def foo(self, arg, too, many, args):
+ return arg + 1
+
+ with pytest.raises(PluginValidationError):
+ pm.register(Plugin())
+
+ # make sure check_pending still fails if specname doesn't have a
+ # corresponding spec. EVEN if the function name matches one.
+ class Plugin2:
+ @hookimpl(specname="bar")
+ def hello(self, arg):
+ return arg + 1
+
+ pm.register(Plugin2())
+ with pytest.raises(PluginValidationError):
+ pm.check_pending()
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_invocations.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_invocations.py
new file mode 100644
index 0000000000..323b9b21e8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_invocations.py
@@ -0,0 +1,215 @@
+import pytest
+from pluggy import PluginValidationError, HookimplMarker, HookspecMarker
+
+
+hookspec = HookspecMarker("example")
+hookimpl = HookimplMarker("example")
+
+
+def test_argmismatch(pm):
+ class Api:
+ @hookspec
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+
+ class Plugin:
+ @hookimpl
+ def hello(self, argwrong):
+ pass
+
+ with pytest.raises(PluginValidationError) as exc:
+ pm.register(Plugin())
+
+ assert "argwrong" in str(exc.value)
+
+
+def test_only_kwargs(pm):
+ class Api:
+ @hookspec
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+ with pytest.raises(TypeError) as exc:
+ pm.hook.hello(3)
+
+ comprehensible = "hook calling supports only keyword arguments"
+ assert comprehensible in str(exc.value)
+
+
+def test_opt_in_args(pm):
+ """Verfiy that two hookimpls with mutex args can serve
+ under the same spec.
+ """
+
+ class Api:
+ @hookspec
+ def hello(self, arg1, arg2, common_arg):
+ "api hook 1"
+
+ class Plugin1:
+ @hookimpl
+ def hello(self, arg1, common_arg):
+ return arg1 + common_arg
+
+ class Plugin2:
+ @hookimpl
+ def hello(self, arg2, common_arg):
+ return arg2 + common_arg
+
+ pm.add_hookspecs(Api)
+ pm.register(Plugin1())
+ pm.register(Plugin2())
+
+ results = pm.hook.hello(arg1=1, arg2=2, common_arg=0)
+ assert results == [2, 1]
+
+
+def test_call_order(pm):
+ class Api:
+ @hookspec
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+
+ class Plugin1:
+ @hookimpl
+ def hello(self, arg):
+ return 1
+
+ class Plugin2:
+ @hookimpl
+ def hello(self, arg):
+ return 2
+
+ class Plugin3:
+ @hookimpl
+ def hello(self, arg):
+ return 3
+
+ class Plugin4:
+ @hookimpl(hookwrapper=True)
+ def hello(self, arg):
+ assert arg == 0
+ outcome = yield
+ assert outcome.get_result() == [3, 2, 1]
+
+ pm.register(Plugin1())
+ pm.register(Plugin2())
+ pm.register(Plugin3())
+ pm.register(Plugin4()) # hookwrapper should get same list result
+ res = pm.hook.hello(arg=0)
+ assert res == [3, 2, 1]
+
+
+def test_firstresult_definition(pm):
+ 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
+ def hello(self, arg):
+ return arg - 1
+
+ class Plugin3:
+ @hookimpl
+ def hello(self, arg):
+ return None
+
+ class Plugin4:
+ @hookimpl(hookwrapper=True)
+ def hello(self, arg):
+ assert arg == 3
+ outcome = yield
+ assert outcome.get_result() == 2
+
+ 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
+ res = pm.hook.hello(arg=3)
+ assert res == 2
+
+
+def test_firstresult_force_result(pm):
+ """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(hookwrapper=True)
+ def hello(self, arg):
+ assert arg == 3
+ outcome = yield
+ assert outcome.get_result() == 4
+ outcome.force_result(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):
+ """If None results are returned by underlying implementations ensure
+ the multi-call loop returns a None value.
+ """
+
+ class Api:
+ @hookspec(firstresult=True)
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+
+ class Plugin1:
+ @hookimpl
+ def hello(self, arg):
+ return None
+
+ pm.register(Plugin1())
+ res = pm.hook.hello(arg=3)
+ assert res is None
+
+
+def test_firstresult_no_plugin(pm):
+ """If no implementations/plugins have been registered for a firstresult
+ hook the multi-call loop should return a None value.
+ """
+
+ class Api:
+ @hookspec(firstresult=True)
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+ res = pm.hook.hello(arg=3)
+ assert res is None
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_multicall.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_multicall.py
new file mode 100644
index 0000000000..8ffb452f69
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_multicall.py
@@ -0,0 +1,147 @@
+import pytest
+from pluggy import HookCallError, HookspecMarker, HookimplMarker
+from pluggy._hooks import HookImpl
+from pluggy._callers import _multicall
+
+
+hookspec = HookspecMarker("example")
+hookimpl = HookimplMarker("example")
+
+
+def MC(methods, kwargs, firstresult=False):
+ caller = _multicall
+ hookfuncs = []
+ for method in methods:
+ f = HookImpl(None, "<temp>", method, method.example_impl)
+ hookfuncs.append(f)
+ return caller("foo", hookfuncs, kwargs, firstresult)
+
+
+def test_keyword_args():
+ @hookimpl
+ def f(x):
+ return x + 1
+
+ class A:
+ @hookimpl
+ def f(self, x, y):
+ return x + y
+
+ reslist = MC([f, A().f], dict(x=23, y=24))
+ assert reslist == [24 + 23, 24]
+
+
+def test_keyword_args_with_defaultargs():
+ @hookimpl
+ def f(x, z=1):
+ return x + z
+
+ reslist = MC([f], dict(x=23, y=24))
+ assert reslist == [24]
+
+
+def test_tags_call_error():
+ @hookimpl
+ def f(x):
+ return x
+
+ with pytest.raises(HookCallError):
+ MC([f], {})
+
+
+def test_call_none_is_no_result():
+ @hookimpl
+ def m1():
+ return 1
+
+ @hookimpl
+ def m2():
+ return None
+
+ res = MC([m1, m2], {}, firstresult=True)
+ assert res == 1
+ res = MC([m1, m2], {}, {})
+ assert res == [1]
+
+
+def test_hookwrapper():
+ out = []
+
+ @hookimpl(hookwrapper=True)
+ def m1():
+ out.append("m1 init")
+ yield None
+ out.append("m1 finish")
+
+ @hookimpl
+ def m2():
+ out.append("m2")
+ return 2
+
+ res = MC([m2, m1], {})
+ assert res == [2]
+ assert out == ["m1 init", "m2", "m1 finish"]
+ out[:] = []
+ res = MC([m2, m1], {}, firstresult=True)
+ assert res == 2
+ assert out == ["m1 init", "m2", "m1 finish"]
+
+
+def test_hookwrapper_order():
+ out = []
+
+ @hookimpl(hookwrapper=True)
+ def m1():
+ out.append("m1 init")
+ yield 1
+ out.append("m1 finish")
+
+ @hookimpl(hookwrapper=True)
+ def m2():
+ out.append("m2 init")
+ yield 2
+ out.append("m2 finish")
+
+ res = MC([m2, m1], {})
+ assert res == []
+ assert out == ["m1 init", "m2 init", "m2 finish", "m1 finish"]
+
+
+def test_hookwrapper_not_yield():
+ @hookimpl(hookwrapper=True)
+ def m1():
+ pass
+
+ with pytest.raises(TypeError):
+ MC([m1], {})
+
+
+def test_hookwrapper_too_many_yield():
+ @hookimpl(hookwrapper=True)
+ def m1():
+ yield 1
+ yield 2
+
+ with pytest.raises(RuntimeError) as ex:
+ MC([m1], {})
+ assert "m1" in str(ex.value)
+ assert (__file__ + ":") in str(ex.value)
+
+
+@pytest.mark.parametrize("exc", [ValueError, SystemExit])
+def test_hookwrapper_exception(exc):
+ out = []
+
+ @hookimpl(hookwrapper=True)
+ def m1():
+ out.append("m1 init")
+ yield None
+ out.append("m1 finish")
+
+ @hookimpl
+ def m2():
+ raise exc
+
+ with pytest.raises(exc):
+ MC([m2, m1], {})
+ assert out == ["m1 init", "m1 finish"]
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_pluginmanager.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_pluginmanager.py
new file mode 100644
index 0000000000..304a007a58
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_pluginmanager.py
@@ -0,0 +1,544 @@
+"""
+``PluginManager`` unit and public API testing.
+"""
+import pytest
+
+from pluggy import (
+ PluginValidationError,
+ HookCallError,
+ HookimplMarker,
+ HookspecMarker,
+)
+from pluggy._manager import importlib_metadata
+
+
+hookspec = HookspecMarker("example")
+hookimpl = HookimplMarker("example")
+
+
+def test_plugin_double_register(pm):
+ """Registering the same plugin more then once isn't allowed"""
+ pm.register(42, name="abc")
+ with pytest.raises(ValueError):
+ pm.register(42, name="abc")
+ with pytest.raises(ValueError):
+ pm.register(42, name="def")
+
+
+def test_pm(pm):
+ """Basic registration with objects"""
+
+ class A:
+ pass
+
+ a1, a2 = A(), A()
+ pm.register(a1)
+ assert pm.is_registered(a1)
+ pm.register(a2, "hello")
+ assert pm.is_registered(a2)
+ out = pm.get_plugins()
+ assert a1 in out
+ assert a2 in out
+ assert pm.get_plugin("hello") == a2
+ assert pm.unregister(a1) == a1
+ assert not pm.is_registered(a1)
+
+ out = pm.list_name_plugin()
+ assert len(out) == 1
+ assert out == [("hello", a2)]
+
+
+def test_has_plugin(pm):
+ class A:
+ pass
+
+ a1 = A()
+ pm.register(a1, "hello")
+ assert pm.is_registered(a1)
+ assert pm.has_plugin("hello")
+
+
+def test_register_dynamic_attr(he_pm):
+ class A:
+ def __getattr__(self, name):
+ if name[0] != "_":
+ return 42
+ raise AttributeError()
+
+ a = A()
+ he_pm.register(a)
+ assert not he_pm.get_hookcallers(a)
+
+
+def test_pm_name(pm):
+ class A:
+ pass
+
+ a1 = A()
+ name = pm.register(a1, name="hello")
+ assert name == "hello"
+ pm.unregister(a1)
+ assert pm.get_plugin(a1) 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 not pm.is_registered(a1)
+ assert not pm.get_plugins()
+
+
+def test_set_blocked(pm):
+ class A:
+ pass
+
+ a1 = A()
+ name = pm.register(a1)
+ assert pm.is_registered(a1)
+ assert not pm.is_blocked(name)
+ pm.set_blocked(name)
+ assert pm.is_blocked(name)
+ assert not pm.is_registered(a1)
+
+ pm.set_blocked("somename")
+ assert pm.is_blocked("somename")
+ assert not pm.register(A(), "somename")
+ pm.unregister(name="somename")
+ assert pm.is_blocked("somename")
+
+
+def test_register_mismatch_method(he_pm):
+ class hello:
+ @hookimpl
+ def he_method_notexists(self):
+ pass
+
+ plugin = hello()
+
+ he_pm.register(plugin)
+ with pytest.raises(PluginValidationError) as excinfo:
+ he_pm.check_pending()
+ assert excinfo.value.plugin is plugin
+
+
+def test_register_mismatch_arg(he_pm):
+ class hello:
+ @hookimpl
+ def he_method1(self, qlwkje):
+ pass
+
+ plugin = hello()
+
+ with pytest.raises(PluginValidationError) as excinfo:
+ he_pm.register(plugin)
+ assert excinfo.value.plugin is plugin
+
+
+def test_register_hookwrapper_not_a_generator_function(he_pm):
+ class hello:
+ @hookimpl(hookwrapper=True)
+ def he_method1(self):
+ pass # pragma: no cover
+
+ plugin = hello()
+
+ with pytest.raises(PluginValidationError, match="generator function") as excinfo:
+ he_pm.register(plugin)
+ assert excinfo.value.plugin is plugin
+
+
+def test_register(pm):
+ class MyPlugin:
+ pass
+
+ my = MyPlugin()
+ pm.register(my)
+ assert my in pm.get_plugins()
+ my2 = MyPlugin()
+ pm.register(my2)
+ assert {my, my2}.issubset(pm.get_plugins())
+
+ 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()
+
+
+def test_register_unknown_hooks(pm):
+ class Plugin1:
+ @hookimpl
+ def he_method1(self, arg):
+ return arg + 1
+
+ pname = pm.register(Plugin1())
+
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ pass
+
+ 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
+
+
+def test_register_historic(pm):
+ class Hooks:
+ @hookspec(historic=True)
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ pm.hook.he_method1.call_historic(kwargs=dict(arg=1))
+ out = []
+
+ class Plugin:
+ @hookimpl
+ def he_method1(self, arg):
+ out.append(arg)
+
+ pm.register(Plugin())
+ assert out == [1]
+
+ class Plugin2:
+ @hookimpl
+ def he_method1(self, arg):
+ out.append(arg * 10)
+
+ pm.register(Plugin2())
+ assert out == [1, 10]
+ pm.hook.he_method1.call_historic(kwargs=dict(arg=12))
+ assert out == [1, 10, 120, 12]
+
+
+@pytest.mark.parametrize("result_callback", [True, False])
+def test_with_result_memorized(pm, result_callback):
+ """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:
+
+ def callback(res):
+ out.append(res)
+
+ else:
+ callback = None
+
+ class Hooks:
+ @hookspec(historic=True)
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ class Plugin1:
+ @hookimpl
+ def he_method1(self, arg):
+ return arg * 10
+
+ pm.register(Plugin1())
+
+ he_method1 = pm.hook.he_method1
+ he_method1.call_historic(result_callback=callback, kwargs=dict(arg=1))
+
+ class Plugin2:
+ @hookimpl
+ def he_method1(self, arg):
+ return arg * 10
+
+ pm.register(Plugin2())
+ if result_callback:
+ assert out == [10, 10]
+ else:
+ assert out == []
+
+
+def test_with_callbacks_immediately_executed(pm):
+ class Hooks:
+ @hookspec(historic=True)
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ class Plugin1:
+ @hookimpl
+ def he_method1(self, arg):
+ return arg * 10
+
+ class Plugin2:
+ @hookimpl
+ def he_method1(self, arg):
+ return arg * 20
+
+ class Plugin3:
+ @hookimpl
+ def he_method1(self, arg):
+ return arg * 30
+
+ out = []
+ pm.register(Plugin1())
+ pm.register(Plugin2())
+
+ he_method1 = pm.hook.he_method1
+ he_method1.call_historic(lambda res: out.append(res), dict(arg=1))
+ assert out == [20, 10]
+ pm.register(Plugin3())
+ assert out == [20, 10, 30]
+
+
+def test_register_historic_incompat_hookwrapper(pm):
+ class Hooks:
+ @hookspec(historic=True)
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ out = []
+
+ class Plugin:
+ @hookimpl(hookwrapper=True)
+ def he_method1(self, arg):
+ out.append(arg)
+
+ with pytest.raises(PluginValidationError):
+ pm.register(Plugin())
+
+
+def test_call_extra(pm):
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ def he_method1(arg):
+ return arg * 10
+
+ out = pm.hook.he_method1.call_extra([he_method1], dict(arg=1))
+ assert out == [10]
+
+
+def test_call_with_too_few_args(pm):
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ class Plugin1:
+ @hookimpl
+ def he_method1(self, arg):
+ 0 / 0
+
+ pm.register(Plugin1())
+ with pytest.raises(HookCallError):
+ with pytest.warns(UserWarning):
+ pm.hook.he_method1()
+
+
+def test_subset_hook_caller(pm):
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ out = []
+
+ class Plugin1:
+ @hookimpl
+ def he_method1(self, arg):
+ out.append(arg)
+
+ class Plugin2:
+ @hookimpl
+ def he_method1(self, arg):
+ out.append(arg * 10)
+
+ class PluginNo:
+ pass
+
+ plugin1, plugin2, plugin3 = Plugin1(), Plugin2(), PluginNo()
+ pm.register(plugin1)
+ pm.register(plugin2)
+ pm.register(plugin3)
+ pm.hook.he_method1(arg=1)
+ assert out == [10, 1]
+ out[:] = []
+
+ hc = pm.subset_hook_caller("he_method1", [plugin1])
+ hc(arg=2)
+ assert out == [20]
+ out[:] = []
+
+ hc = pm.subset_hook_caller("he_method1", [plugin2])
+ hc(arg=2)
+ assert out == [2]
+ out[:] = []
+
+ pm.unregister(plugin1)
+ hc(arg=2)
+ assert out == []
+ out[:] = []
+
+ pm.hook.he_method1(arg=1)
+ assert out == [10]
+
+
+def test_get_hookimpls(pm):
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+ assert pm.hook.he_method1.get_hookimpls() == []
+
+ class Plugin1:
+ @hookimpl
+ def he_method1(self, arg):
+ pass
+
+ class Plugin2:
+ @hookimpl
+ def he_method1(self, arg):
+ pass
+
+ class PluginNo:
+ pass
+
+ plugin1, plugin2, plugin3 = Plugin1(), Plugin2(), PluginNo()
+ pm.register(plugin1)
+ pm.register(plugin2)
+ pm.register(plugin3)
+
+ hookimpls = pm.hook.he_method1.get_hookimpls()
+ hook_plugins = [item.plugin for item in hookimpls]
+ assert hook_plugins == [plugin1, plugin2]
+
+
+def test_add_hookspecs_nohooks(pm):
+ with pytest.raises(ValueError):
+ pm.add_hookspecs(10)
+
+
+def test_load_setuptools_instantiation(monkeypatch, pm):
+ class EntryPoint:
+ name = "myname"
+ group = "hello"
+ value = "myname:foo"
+
+ def load(self):
+ class PseudoPlugin:
+ x = 42
+
+ return PseudoPlugin()
+
+ class Distribution:
+ entry_points = (EntryPoint(),)
+
+ dist = Distribution()
+
+ def my_distributions():
+ return (dist,)
+
+ monkeypatch.setattr(importlib_metadata, "distributions", my_distributions)
+ num = pm.load_setuptools_entrypoints("hello")
+ assert num == 1
+ plugin = pm.get_plugin("myname")
+ 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
+ num = pm.load_setuptools_entrypoints("hello")
+ assert num == 0 # no plugin loaded by this call
+
+
+def test_add_tracefuncs(he_pm):
+ out = []
+
+ class api1:
+ @hookimpl
+ def he_method1(self):
+ out.append("he_method1-api1")
+
+ class api2:
+ @hookimpl
+ def he_method1(self):
+ out.append("he_method1-api2")
+
+ he_pm.register(api1())
+ he_pm.register(api2())
+
+ def before(hook_name, hook_impls, kwargs):
+ out.append((hook_name, list(hook_impls), kwargs))
+
+ def after(outcome, hook_name, hook_impls, kwargs):
+ out.append((outcome, hook_name, list(hook_impls), kwargs))
+
+ undo = he_pm.add_hookcall_monitoring(before, after)
+
+ he_pm.hook.he_method1(arg=1)
+ assert len(out) == 4
+ assert out[0][0] == "he_method1"
+ assert len(out[0][1]) == 2
+ assert isinstance(out[0][2], dict)
+ assert out[1] == "he_method1-api2"
+ assert out[2] == "he_method1-api1"
+ assert len(out[3]) == 4
+ assert out[3][1] == out[0][0]
+
+ undo()
+ he_pm.hook.he_method1(arg=1)
+ assert len(out) == 4 + 2
+
+
+def test_hook_tracing(he_pm):
+ saveindent = []
+
+ class api1:
+ @hookimpl
+ def he_method1(self):
+ saveindent.append(he_pm.trace.root.indent)
+
+ class api2:
+ @hookimpl
+ def he_method1(self):
+ saveindent.append(he_pm.trace.root.indent)
+ raise ValueError()
+
+ he_pm.register(api1())
+ out = []
+ he_pm.trace.root.setwriter(out.append)
+ undo = he_pm.enable_tracing()
+ try:
+ indent = he_pm.trace.root.indent
+ he_pm.hook.he_method1(arg=1)
+ assert indent == he_pm.trace.root.indent
+ assert len(out) == 2
+ assert "he_method1" in out[0]
+ assert "finish" in out[1]
+
+ out[:] = []
+ he_pm.register(api2())
+
+ with pytest.raises(ValueError):
+ he_pm.hook.he_method1(arg=1)
+ assert he_pm.trace.root.indent == indent
+ assert saveindent[0] > indent
+ finally:
+ undo()
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_tracer.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_tracer.py
new file mode 100644
index 0000000000..992ec67914
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_tracer.py
@@ -0,0 +1,78 @@
+from pluggy._tracing import TagTracer
+
+import pytest
+
+
+@pytest.fixture
+def rootlogger():
+ return TagTracer()
+
+
+def test_simple(rootlogger):
+ log = rootlogger.get("pytest")
+ log("hello")
+ out = []
+ rootlogger.setwriter(out.append)
+ log("world")
+ assert len(out) == 1
+ assert out[0] == "world [pytest]\n"
+ sublog = log.get("collection")
+ sublog("hello")
+ assert out[1] == "hello [pytest:collection]\n"
+
+
+def test_indent(rootlogger):
+ log = rootlogger.get("1")
+ out = []
+ log.root.setwriter(lambda arg: out.append(arg))
+ log("hello")
+ log.root.indent += 1
+ log("line1")
+ log("line2")
+ log.root.indent += 1
+ log("line3")
+ log("line4")
+ log.root.indent -= 1
+ log("line5")
+ log.root.indent -= 1
+ log("last")
+ assert len(out) == 7
+ names = [x[: x.rfind(" [")] for x in out]
+ assert names == [
+ "hello",
+ " line1",
+ " line2",
+ " line3",
+ " line4",
+ " line5",
+ "last",
+ ]
+
+
+def test_readable_output_dictargs(rootlogger):
+
+ out = rootlogger._format_message(["test"], [1])
+ assert out == "1 [test]\n"
+
+ out2 = rootlogger._format_message(["test"], ["test", {"a": 1}])
+ assert out2 == "test [test]\n a: 1\n"
+
+
+def test_setprocessor(rootlogger):
+ log = rootlogger.get("1")
+ log2 = log.get("2")
+ assert log2.tags == tuple("12")
+ out = []
+ rootlogger.setprocessor(tuple("12"), lambda *args: out.append(args))
+ log("not seen")
+ log2("seen")
+ assert len(out) == 1
+ tags, args = out[0]
+ assert "1" in tags
+ assert "2" in tags
+ assert args == ("seen",)
+ l2 = []
+ rootlogger.setprocessor("1:2", lambda *args: l2.append(args))
+ log2("seen")
+ tags, args = l2[0]
+ assert args == ("seen",)
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/tox.ini b/testing/web-platform/tests/tools/third_party/pluggy/tox.ini
new file mode 100644
index 0000000000..97b3eb7792
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/tox.ini
@@ -0,0 +1,57 @@
+[tox]
+envlist=linting,docs,py{36,37,38,39,py3},py{36,37}-pytest{main}
+
+[testenv]
+commands=
+ {env:_PLUGGY_TOX_CMD:pytest} {posargs}
+ coverage: coverage report -m
+ coverage: coverage xml
+setenv=
+ _PYTEST_SETUP_SKIP_PLUGGY_DEP=1
+ coverage: _PLUGGY_TOX_CMD=coverage run -m pytest
+extras=testing
+deps=
+ coverage: coverage
+ pytestmain: git+https://github.com/pytest-dev/pytest.git@main
+
+[testenv:benchmark]
+commands=pytest {posargs:testing/benchmark.py}
+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
+commands =
+ sphinx-build -W -b html {toxinidir}/docs {toxinidir}/build/html-docs
+
+[pytest]
+minversion=2.0
+testpaths = testing
+#--pyargs --doctest-modules --ignore=.tox
+addopts=-r a
+filterwarnings =
+ error
+
+[flake8]
+max-line-length=99
+
+[testenv:release]
+decription = do a release, required posarg of the version number
+basepython = python3
+skipsdist = True
+usedevelop = True
+passenv = *
+deps =
+ colorama
+ gitpython
+ towncrier
+commands = python scripts/release.py {posargs}
diff --git a/testing/web-platform/tests/tools/third_party/py/.flake8 b/testing/web-platform/tests/tools/third_party/py/.flake8
new file mode 100644
index 0000000000..f9c71a7fbc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/.flake8
@@ -0,0 +1,4 @@
+[flake8]
+max-line-length = 120
+per-file-ignores =
+ **/*.pyi:E252,E301,E302,E305,E501,E701,E704,F401,F811,F821
diff --git a/testing/web-platform/tests/tools/third_party/py/.github/workflows/main.yml b/testing/web-platform/tests/tools/third_party/py/.github/workflows/main.yml
new file mode 100644
index 0000000000..564aa0c531
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/.github/workflows/main.yml
@@ -0,0 +1,66 @@
+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/testing/web-platform/tests/tools/third_party/py/.gitignore b/testing/web-platform/tests/tools/third_party/py/.gitignore
new file mode 100644
index 0000000000..fa936f1596
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/.gitignore
@@ -0,0 +1,15 @@
+
+.cache/
+.tox/
+__pycache__/
+.mypy_cache/
+
+*.pyc
+*.pyo
+
+*.egg-info
+.eggs/
+
+dist/*
+/py/_version.py
+.pytest_cache/
diff --git a/testing/web-platform/tests/tools/third_party/py/AUTHORS b/testing/web-platform/tests/tools/third_party/py/AUTHORS
new file mode 100644
index 0000000000..9c5dda9ceb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/AUTHORS
@@ -0,0 +1,25 @@
+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/testing/web-platform/tests/tools/third_party/py/CHANGELOG.rst b/testing/web-platform/tests/tools/third_party/py/CHANGELOG.rst
new file mode 100644
index 0000000000..47c6fdb7a1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/CHANGELOG.rst
@@ -0,0 +1,1236 @@
+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/testing/web-platform/tests/tools/third_party/py/LICENSE b/testing/web-platform/tests/tools/third_party/py/LICENSE
new file mode 100644
index 0000000000..31ecdfb1db
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/LICENSE
@@ -0,0 +1,19 @@
+
+ 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/testing/web-platform/tests/tools/third_party/py/MANIFEST.in b/testing/web-platform/tests/tools/third_party/py/MANIFEST.in
new file mode 100644
index 0000000000..6d255b1a9e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/MANIFEST.in
@@ -0,0 +1,11 @@
+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/testing/web-platform/tests/tools/third_party/py/README.rst b/testing/web-platform/tests/tools/third_party/py/README.rst
new file mode 100644
index 0000000000..80800b2b7a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/README.rst
@@ -0,0 +1,31 @@
+.. 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/testing/web-platform/tests/tools/third_party/py/RELEASING.rst b/testing/web-platform/tests/tools/third_party/py/RELEASING.rst
new file mode 100644
index 0000000000..fb588e3ab7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/RELEASING.rst
@@ -0,0 +1,17 @@
+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/testing/web-platform/tests/tools/third_party/py/bench/localpath.py b/testing/web-platform/tests/tools/third_party/py/bench/localpath.py
new file mode 100644
index 0000000000..aad44f2e66
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/bench/localpath.py
@@ -0,0 +1,73 @@
+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/testing/web-platform/tests/tools/third_party/py/codecov.yml b/testing/web-platform/tests/tools/third_party/py/codecov.yml
new file mode 100644
index 0000000000..a0a308588e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/codecov.yml
@@ -0,0 +1,7 @@
+coverage:
+ status:
+ project: true
+ patch: true
+ changes: true
+
+comment: off
diff --git a/testing/web-platform/tests/tools/third_party/py/conftest.py b/testing/web-platform/tests/tools/third_party/py/conftest.py
new file mode 100644
index 0000000000..5bff3fe022
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/conftest.py
@@ -0,0 +1,60 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/Makefile b/testing/web-platform/tests/tools/third_party/py/doc/Makefile
new file mode 100644
index 0000000000..0a0e89e01f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/Makefile
@@ -0,0 +1,133 @@
+# 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/testing/web-platform/tests/tools/third_party/py/doc/_templates/layout.html b/testing/web-platform/tests/tools/third_party/py/doc/_templates/layout.html
new file mode 100644
index 0000000000..683863aa46
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/_templates/layout.html
@@ -0,0 +1,18 @@
+{% 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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-0.9.0.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-0.9.0.txt
new file mode 100644
index 0000000000..0710931354
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-0.9.0.txt
@@ -0,0 +1,7 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-0.9.2.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-0.9.2.txt
new file mode 100644
index 0000000000..8340dc4455
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-0.9.2.txt
@@ -0,0 +1,27 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.0.0.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.0.0.txt
new file mode 100644
index 0000000000..aef25ec239
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.0.0.txt
@@ -0,0 +1,63 @@
+
+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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.0.1.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.0.1.txt
new file mode 100644
index 0000000000..0c9f8760bd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.0.1.txt
@@ -0,0 +1,48 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.0.2.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.0.2.txt
new file mode 100644
index 0000000000..2354619535
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.0.2.txt
@@ -0,0 +1,5 @@
+1.0.2: packaging fixes
+-----------------------------------------------------------------------
+
+this release is purely a release for fixing packaging issues.
+
diff --git a/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.1.0.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.1.0.txt
new file mode 100644
index 0000000000..0441c3215e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.1.0.txt
@@ -0,0 +1,115 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.1.1.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.1.1.txt
new file mode 100644
index 0000000000..83e6a1fd8d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.1.1.txt
@@ -0,0 +1,48 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.2.0.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.2.0.txt
new file mode 100644
index 0000000000..4f6a561447
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.2.0.txt
@@ -0,0 +1,116 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.2.1.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.2.1.txt
new file mode 100644
index 0000000000..5bf8ba22dc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.2.1.txt
@@ -0,0 +1,66 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.0.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.0.txt
new file mode 100644
index 0000000000..cf97db0367
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.0.txt
@@ -0,0 +1,580 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.1.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.1.txt
new file mode 100644
index 0000000000..471de408a1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.1.txt
@@ -0,0 +1,104 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.2.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.2.txt
new file mode 100644
index 0000000000..599dfbed75
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.2.txt
@@ -0,0 +1,720 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.3.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.3.txt
new file mode 100644
index 0000000000..c62cb85905
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.3.txt
@@ -0,0 +1,26 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.4.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.4.txt
new file mode 100644
index 0000000000..c156c8bdb3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.3.4.txt
@@ -0,0 +1,22 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.4.0.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.4.0.txt
new file mode 100644
index 0000000000..1c9fa75604
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.4.0.txt
@@ -0,0 +1,47 @@
+
+.. _`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/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.4.1.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.4.1.txt
new file mode 100644
index 0000000000..6ed72aa418
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/release-1.4.1.txt
@@ -0,0 +1,47 @@
+
+.. _`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/testing/web-platform/tests/tools/third_party/py/doc/announce/releases.txt b/testing/web-platform/tests/tools/third_party/py/doc/announce/releases.txt
new file mode 100644
index 0000000000..309c29bac5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/announce/releases.txt
@@ -0,0 +1,16 @@
+=============
+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/testing/web-platform/tests/tools/third_party/py/doc/changelog.txt b/testing/web-platform/tests/tools/third_party/py/doc/changelog.txt
new file mode 100644
index 0000000000..0c9d0928e7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/changelog.txt
@@ -0,0 +1,3 @@
+.. _`changelog`:
+
+.. include:: ../CHANGELOG.rst
diff --git a/testing/web-platform/tests/tools/third_party/py/doc/code.txt b/testing/web-platform/tests/tools/third_party/py/doc/code.txt
new file mode 100644
index 0000000000..bdd8691da0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/code.txt
@@ -0,0 +1,150 @@
+================================================================================
+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/testing/web-platform/tests/tools/third_party/py/doc/conf.py b/testing/web-platform/tests/tools/third_party/py/doc/conf.py
new file mode 100644
index 0000000000..de4cbf8a46
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/conf.py
@@ -0,0 +1,263 @@
+# -*- 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/testing/web-platform/tests/tools/third_party/py/doc/download.html b/testing/web-platform/tests/tools/third_party/py/doc/download.html
new file mode 100644
index 0000000000..5f4c466402
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/download.html
@@ -0,0 +1,18 @@
+<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/testing/web-platform/tests/tools/third_party/py/doc/example/genhtml.py b/testing/web-platform/tests/tools/third_party/py/doc/example/genhtml.py
new file mode 100644
index 0000000000..7a6d493497
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/example/genhtml.py
@@ -0,0 +1,13 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/example/genhtmlcss.py b/testing/web-platform/tests/tools/third_party/py/doc/example/genhtmlcss.py
new file mode 100644
index 0000000000..facca77b78
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/example/genhtmlcss.py
@@ -0,0 +1,23 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/example/genxml.py b/testing/web-platform/tests/tools/third_party/py/doc/example/genxml.py
new file mode 100644
index 0000000000..444a4ca52c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/example/genxml.py
@@ -0,0 +1,17 @@
+
+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/testing/web-platform/tests/tools/third_party/py/doc/faq.txt b/testing/web-platform/tests/tools/third_party/py/doc/faq.txt
new file mode 100644
index 0000000000..6d374e1db9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/faq.txt
@@ -0,0 +1,170 @@
+==================================
+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/testing/web-platform/tests/tools/third_party/py/doc/img/pylib.png b/testing/web-platform/tests/tools/third_party/py/doc/img/pylib.png
new file mode 100644
index 0000000000..2e10d43886
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/img/pylib.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/py/doc/index.txt b/testing/web-platform/tests/tools/third_party/py/doc/index.txt
new file mode 100644
index 0000000000..c700b17e98
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/index.txt
@@ -0,0 +1,39 @@
+.. 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/testing/web-platform/tests/tools/third_party/py/doc/install.txt b/testing/web-platform/tests/tools/third_party/py/doc/install.txt
new file mode 100644
index 0000000000..93c79e3b2d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/install.txt
@@ -0,0 +1,91 @@
+
+.. _`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/testing/web-platform/tests/tools/third_party/py/doc/io.txt b/testing/web-platform/tests/tools/third_party/py/doc/io.txt
new file mode 100644
index 0000000000..c11308a6d2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/io.txt
@@ -0,0 +1,59 @@
+=======
+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/testing/web-platform/tests/tools/third_party/py/doc/links.inc b/testing/web-platform/tests/tools/third_party/py/doc/links.inc
new file mode 100644
index 0000000000..b61d01c696
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/links.inc
@@ -0,0 +1,15 @@
+
+.. _`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/testing/web-platform/tests/tools/third_party/py/doc/log.txt b/testing/web-platform/tests/tools/third_party/py/doc/log.txt
new file mode 100644
index 0000000000..ca60fcac25
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/log.txt
@@ -0,0 +1,208 @@
+.. 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/testing/web-platform/tests/tools/third_party/py/doc/misc.txt b/testing/web-platform/tests/tools/third_party/py/doc/misc.txt
new file mode 100644
index 0000000000..4b45348275
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/misc.txt
@@ -0,0 +1,93 @@
+====================================
+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/testing/web-platform/tests/tools/third_party/py/doc/path.txt b/testing/web-platform/tests/tools/third_party/py/doc/path.txt
new file mode 100644
index 0000000000..8f506d4923
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/path.txt
@@ -0,0 +1,264 @@
+=======
+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/testing/web-platform/tests/tools/third_party/py/doc/style.css b/testing/web-platform/tests/tools/third_party/py/doc/style.css
new file mode 100644
index 0000000000..95e3ef07b2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/style.css
@@ -0,0 +1,1044 @@
+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/testing/web-platform/tests/tools/third_party/py/doc/xml.txt b/testing/web-platform/tests/tools/third_party/py/doc/xml.txt
new file mode 100644
index 0000000000..1022de6e91
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/doc/xml.txt
@@ -0,0 +1,164 @@
+====================================================
+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/testing/web-platform/tests/tools/third_party/py/py/__init__.py b/testing/web-platform/tests/tools/third_party/py/py/__init__.py
new file mode 100644
index 0000000000..b892ce1a2a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/__init__.py
@@ -0,0 +1,156 @@
+"""
+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/testing/web-platform/tests/tools/third_party/py/py/__init__.pyi b/testing/web-platform/tests/tools/third_party/py/py/__init__.pyi
new file mode 100644
index 0000000000..96859e310f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/__init__.pyi
@@ -0,0 +1,20 @@
+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/testing/web-platform/tests/tools/third_party/py/py/__metainfo.py b/testing/web-platform/tests/tools/third_party/py/py/__metainfo.py
new file mode 100644
index 0000000000..12581eb7af
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/__metainfo.py
@@ -0,0 +1,2 @@
+import py
+pydir = py.path.local(py.__file__).dirpath()
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_builtin.py b/testing/web-platform/tests/tools/third_party/py/py/_builtin.py
new file mode 100644
index 0000000000..ddc89fc7be
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_builtin.py
@@ -0,0 +1,149 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_code/__init__.py b/testing/web-platform/tests/tools/third_party/py/py/_code/__init__.py
new file mode 100644
index 0000000000..f15acf8513
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_code/__init__.py
@@ -0,0 +1 @@
+""" python inspection/code generation API """
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_code/_assertionnew.py b/testing/web-platform/tests/tools/third_party/py/py/_code/_assertionnew.py
new file mode 100644
index 0000000000..d03f29d870
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_code/_assertionnew.py
@@ -0,0 +1,322 @@
+"""
+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/testing/web-platform/tests/tools/third_party/py/py/_code/_assertionold.py b/testing/web-platform/tests/tools/third_party/py/py/_code/_assertionold.py
new file mode 100644
index 0000000000..1bb70a875d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_code/_assertionold.py
@@ -0,0 +1,556 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_code/_py2traceback.py b/testing/web-platform/tests/tools/third_party/py/py/_code/_py2traceback.py
new file mode 100644
index 0000000000..d65e27cb73
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_code/_py2traceback.py
@@ -0,0 +1,79 @@
+# 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/testing/web-platform/tests/tools/third_party/py/py/_code/assertion.py b/testing/web-platform/tests/tools/third_party/py/py/_code/assertion.py
new file mode 100644
index 0000000000..ff1643799c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_code/assertion.py
@@ -0,0 +1,90 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_code/code.py b/testing/web-platform/tests/tools/third_party/py/py/_code/code.py
new file mode 100644
index 0000000000..dad796283f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_code/code.py
@@ -0,0 +1,796 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_code/source.py b/testing/web-platform/tests/tools/third_party/py/py/_code/source.py
new file mode 100644
index 0000000000..7fc7b23a96
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_code/source.py
@@ -0,0 +1,410 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_error.py b/testing/web-platform/tests/tools/third_party/py/py/_error.py
new file mode 100644
index 0000000000..a6375de9fa
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_error.py
@@ -0,0 +1,91 @@
+"""
+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/testing/web-platform/tests/tools/third_party/py/py/_io/__init__.py b/testing/web-platform/tests/tools/third_party/py/py/_io/__init__.py
new file mode 100644
index 0000000000..835f01f3ab
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_io/__init__.py
@@ -0,0 +1 @@
+""" input/output helping """
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_io/capture.py b/testing/web-platform/tests/tools/third_party/py/py/_io/capture.py
new file mode 100644
index 0000000000..cacf2fa71a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_io/capture.py
@@ -0,0 +1,371 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_io/saferepr.py b/testing/web-platform/tests/tools/third_party/py/py/_io/saferepr.py
new file mode 100644
index 0000000000..8518290efd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_io/saferepr.py
@@ -0,0 +1,71 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_io/terminalwriter.py b/testing/web-platform/tests/tools/third_party/py/py/_io/terminalwriter.py
new file mode 100644
index 0000000000..442ca2395e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_io/terminalwriter.py
@@ -0,0 +1,423 @@
+"""
+
+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/testing/web-platform/tests/tools/third_party/py/py/_log/__init__.py b/testing/web-platform/tests/tools/third_party/py/py/_log/__init__.py
new file mode 100644
index 0000000000..fad62e960d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_log/__init__.py
@@ -0,0 +1,2 @@
+""" logging API ('producers' and 'consumers' connected via keywords) """
+
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_log/log.py b/testing/web-platform/tests/tools/third_party/py/py/_log/log.py
new file mode 100644
index 0000000000..56969bcb58
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_log/log.py
@@ -0,0 +1,206 @@
+"""
+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/testing/web-platform/tests/tools/third_party/py/py/_log/warning.py b/testing/web-platform/tests/tools/third_party/py/py/_log/warning.py
new file mode 100644
index 0000000000..6ef20d98a2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_log/warning.py
@@ -0,0 +1,79 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_path/__init__.py b/testing/web-platform/tests/tools/third_party/py/py/_path/__init__.py
new file mode 100644
index 0000000000..51f3246f80
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_path/__init__.py
@@ -0,0 +1 @@
+""" unified file system api """
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_path/cacheutil.py b/testing/web-platform/tests/tools/third_party/py/py/_path/cacheutil.py
new file mode 100644
index 0000000000..9922504750
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_path/cacheutil.py
@@ -0,0 +1,114 @@
+"""
+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/testing/web-platform/tests/tools/third_party/py/py/_path/common.py b/testing/web-platform/tests/tools/third_party/py/py/_path/common.py
new file mode 100644
index 0000000000..2364e5fef5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_path/common.py
@@ -0,0 +1,459 @@
+"""
+"""
+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/testing/web-platform/tests/tools/third_party/py/py/_path/local.py b/testing/web-platform/tests/tools/third_party/py/py/_path/local.py
new file mode 100644
index 0000000000..1385a03987
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_path/local.py
@@ -0,0 +1,1030 @@
+"""
+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/testing/web-platform/tests/tools/third_party/py/py/_path/svnurl.py b/testing/web-platform/tests/tools/third_party/py/py/_path/svnurl.py
new file mode 100644
index 0000000000..6589a71d09
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_path/svnurl.py
@@ -0,0 +1,380 @@
+"""
+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/testing/web-platform/tests/tools/third_party/py/py/_path/svnwc.py b/testing/web-platform/tests/tools/third_party/py/py/_path/svnwc.py
new file mode 100644
index 0000000000..b5b9d8d544
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_path/svnwc.py
@@ -0,0 +1,1240 @@
+"""
+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/testing/web-platform/tests/tools/third_party/py/py/_process/__init__.py b/testing/web-platform/tests/tools/third_party/py/py/_process/__init__.py
new file mode 100644
index 0000000000..86c714ad1a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_process/__init__.py
@@ -0,0 +1 @@
+""" high-level sub-process handling """
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_process/cmdexec.py b/testing/web-platform/tests/tools/third_party/py/py/_process/cmdexec.py
new file mode 100644
index 0000000000..f83a249402
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_process/cmdexec.py
@@ -0,0 +1,49 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_process/forkedfunc.py b/testing/web-platform/tests/tools/third_party/py/py/_process/forkedfunc.py
new file mode 100644
index 0000000000..1c28530688
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_process/forkedfunc.py
@@ -0,0 +1,120 @@
+
+"""
+ 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/testing/web-platform/tests/tools/third_party/py/py/_process/killproc.py b/testing/web-platform/tests/tools/third_party/py/py/_process/killproc.py
new file mode 100644
index 0000000000..18e8310b5f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_process/killproc.py
@@ -0,0 +1,23 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_std.py b/testing/web-platform/tests/tools/third_party/py/py/_std.py
new file mode 100644
index 0000000000..66adb7b023
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_std.py
@@ -0,0 +1,27 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/__init__.py b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/INSTALLER b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/INSTALLER
new file mode 100644
index 0000000000..a1b589e38a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/LICENSE b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/LICENSE
new file mode 100644
index 0000000000..ff33b8f7ca
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/LICENSE
@@ -0,0 +1,18 @@
+
+ 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/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/METADATA b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/METADATA
new file mode 100644
index 0000000000..7eea770a02
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/METADATA
@@ -0,0 +1,125 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/RECORD b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/RECORD
new file mode 100644
index 0000000000..357b8b9c72
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/RECORD
@@ -0,0 +1,11 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/REQUESTED b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/REQUESTED
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/REQUESTED
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/WHEEL b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/WHEEL
new file mode 100644
index 0000000000..b733a60d37
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/WHEEL
@@ -0,0 +1,6 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/top_level.txt b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/top_level.txt
new file mode 100644
index 0000000000..e2221c8f9e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/top_level.txt
@@ -0,0 +1 @@
+apipkg
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg/__init__.py b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg/__init__.py
new file mode 100644
index 0000000000..350d8c4b07
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg/__init__.py
@@ -0,0 +1,217 @@
+"""
+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/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg/version.py b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg/version.py
new file mode 100644
index 0000000000..c5b4e0e79f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/apipkg/version.py
@@ -0,0 +1,5 @@
+# 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/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/INSTALLER b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/INSTALLER
new file mode 100644
index 0000000000..a1b589e38a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/LICENSE b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/LICENSE
new file mode 100644
index 0000000000..31ecdfb1db
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/LICENSE
@@ -0,0 +1,19 @@
+
+ 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/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/METADATA b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/METADATA
new file mode 100644
index 0000000000..c078a7532f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/METADATA
@@ -0,0 +1,78 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/RECORD b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/RECORD
new file mode 100644
index 0000000000..168233330b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/RECORD
@@ -0,0 +1,11 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/REQUESTED b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/REQUESTED
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/REQUESTED
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/WHEEL b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/WHEEL
new file mode 100644
index 0000000000..6d38aa0601
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/WHEEL
@@ -0,0 +1,6 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/top_level.txt b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/top_level.txt
new file mode 100644
index 0000000000..9dda53692d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/top_level.txt
@@ -0,0 +1 @@
+iniconfig
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.py b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.py
new file mode 100644
index 0000000000..6ad9eaf868
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.py
@@ -0,0 +1,165 @@
+""" 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/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.pyi b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.pyi
new file mode 100644
index 0000000000..b6284bec3f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.pyi
@@ -0,0 +1,31 @@
+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/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig/py.typed b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig/py.typed
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_vendored_packages/iniconfig/py.typed
diff --git a/testing/web-platform/tests/tools/third_party/py/py/_xmlgen.py b/testing/web-platform/tests/tools/third_party/py/py/_xmlgen.py
new file mode 100644
index 0000000000..1c83545884
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/_xmlgen.py
@@ -0,0 +1,255 @@
+"""
+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('&quot;'), u('<') : u('&lt;'), u('>') : u('&gt;'),
+ u('&') : u('&amp;'), u("'") : u('&apos;'),
+ }
+ 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/testing/web-platform/tests/tools/third_party/py/py/error.pyi b/testing/web-platform/tests/tools/third_party/py/py/error.pyi
new file mode 100644
index 0000000000..034eba609f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/error.pyi
@@ -0,0 +1,129 @@
+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/testing/web-platform/tests/tools/third_party/py/py/iniconfig.pyi b/testing/web-platform/tests/tools/third_party/py/py/iniconfig.pyi
new file mode 100644
index 0000000000..b6284bec3f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/iniconfig.pyi
@@ -0,0 +1,31 @@
+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/testing/web-platform/tests/tools/third_party/py/py/io.pyi b/testing/web-platform/tests/tools/third_party/py/py/io.pyi
new file mode 100644
index 0000000000..d377e2405d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/io.pyi
@@ -0,0 +1,130 @@
+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/testing/web-platform/tests/tools/third_party/py/py/path.pyi b/testing/web-platform/tests/tools/third_party/py/py/path.pyi
new file mode 100644
index 0000000000..1ddab9601e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/path.pyi
@@ -0,0 +1,197 @@
+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/testing/web-platform/tests/tools/third_party/py/py/py.typed b/testing/web-platform/tests/tools/third_party/py/py/py.typed
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/py.typed
diff --git a/testing/web-platform/tests/tools/third_party/py/py/test.py b/testing/web-platform/tests/tools/third_party/py/py/test.py
new file mode 100644
index 0000000000..aa5beb1789
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/test.py
@@ -0,0 +1,10 @@
+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/testing/web-platform/tests/tools/third_party/py/py/xml.pyi b/testing/web-platform/tests/tools/third_party/py/py/xml.pyi
new file mode 100644
index 0000000000..9c44480a5f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/py/xml.pyi
@@ -0,0 +1,25 @@
+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/testing/web-platform/tests/tools/third_party/py/pyproject.toml b/testing/web-platform/tests/tools/third_party/py/pyproject.toml
new file mode 100644
index 0000000000..e386ea0b27
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/pyproject.toml
@@ -0,0 +1,6 @@
+[build-system]
+requires = [
+ "setuptools",
+ "setuptools_scm[toml]",
+]
+build-backend = "setuptools.build_meta"
diff --git a/testing/web-platform/tests/tools/third_party/py/setup.cfg b/testing/web-platform/tests/tools/third_party/py/setup.cfg
new file mode 100644
index 0000000000..5f25c2febf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/setup.cfg
@@ -0,0 +1,8 @@
+[wheel]
+universal = 1
+
+[metadata]
+license_file = LICENSE
+
+[devpi:upload]
+formats=sdist.tgz,bdist_wheel
diff --git a/testing/web-platform/tests/tools/third_party/py/setup.py b/testing/web-platform/tests/tools/third_party/py/setup.py
new file mode 100644
index 0000000000..5948ef0047
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/setup.py
@@ -0,0 +1,48 @@
+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/testing/web-platform/tests/tools/third_party/py/tasks/__init__.py b/testing/web-platform/tests/tools/third_party/py/tasks/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/tasks/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/py/tasks/vendoring.py b/testing/web-platform/tests/tools/third_party/py/tasks/vendoring.py
new file mode 100644
index 0000000000..3c7d6015cf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/tasks/vendoring.py
@@ -0,0 +1,41 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/code/test_assertion.py b/testing/web-platform/tests/tools/third_party/py/testing/code/test_assertion.py
new file mode 100644
index 0000000000..e2a7f90399
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/code/test_assertion.py
@@ -0,0 +1,305 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/code/test_code.py b/testing/web-platform/tests/tools/third_party/py/testing/code/test_code.py
new file mode 100644
index 0000000000..28ec628b00
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/code/test_code.py
@@ -0,0 +1,159 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/code/test_excinfo.py b/testing/web-platform/tests/tools/third_party/py/testing/code/test_excinfo.py
new file mode 100644
index 0000000000..c148ab8cfb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/code/test_excinfo.py
@@ -0,0 +1,956 @@
+# -*- 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/testing/web-platform/tests/tools/third_party/py/testing/code/test_source.py b/testing/web-platform/tests/tools/third_party/py/testing/code/test_source.py
new file mode 100644
index 0000000000..ca9a42275c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/code/test_source.py
@@ -0,0 +1,656 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/conftest.py b/testing/web-platform/tests/tools/third_party/py/testing/conftest.py
new file mode 100644
index 0000000000..0f956b3dd2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/conftest.py
@@ -0,0 +1,3 @@
+
+pytest_plugins = "pytester",
+
diff --git a/testing/web-platform/tests/tools/third_party/py/testing/io_/__init__.py b/testing/web-platform/tests/tools/third_party/py/testing/io_/__init__.py
new file mode 100644
index 0000000000..792d600548
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/io_/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/testing/web-platform/tests/tools/third_party/py/testing/io_/test_capture.py b/testing/web-platform/tests/tools/third_party/py/testing/io_/test_capture.py
new file mode 100644
index 0000000000..b5fedd0abc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/io_/test_capture.py
@@ -0,0 +1,501 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/io_/test_saferepr.py b/testing/web-platform/tests/tools/third_party/py/testing/io_/test_saferepr.py
new file mode 100644
index 0000000000..97be1416fe
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/io_/test_saferepr.py
@@ -0,0 +1,75 @@
+# -*- 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/testing/web-platform/tests/tools/third_party/py/testing/io_/test_terminalwriter.py b/testing/web-platform/tests/tools/third_party/py/testing/io_/test_terminalwriter.py
new file mode 100644
index 0000000000..44b4f1ddee
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/io_/test_terminalwriter.py
@@ -0,0 +1,341 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/io_/test_terminalwriter_linewidth.py b/testing/web-platform/tests/tools/third_party/py/testing/io_/test_terminalwriter_linewidth.py
new file mode 100644
index 0000000000..e6d84fbf7a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/io_/test_terminalwriter_linewidth.py
@@ -0,0 +1,56 @@
+# 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/testing/web-platform/tests/tools/third_party/py/testing/log/__init__.py b/testing/web-platform/tests/tools/third_party/py/testing/log/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/log/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/py/testing/log/test_log.py b/testing/web-platform/tests/tools/third_party/py/testing/log/test_log.py
new file mode 100644
index 0000000000..5c706d9b6a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/log/test_log.py
@@ -0,0 +1,191 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/log/test_warning.py b/testing/web-platform/tests/tools/third_party/py/testing/log/test_warning.py
new file mode 100644
index 0000000000..36efec913a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/log/test_warning.py
@@ -0,0 +1,85 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/path/common.py b/testing/web-platform/tests/tools/third_party/py/testing/path/common.py
new file mode 100644
index 0000000000..d69a1c39d0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/path/common.py
@@ -0,0 +1,492 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/path/conftest.py b/testing/web-platform/tests/tools/third_party/py/testing/path/conftest.py
new file mode 100644
index 0000000000..84fb5c8269
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/path/conftest.py
@@ -0,0 +1,80 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/path/repotest.dump b/testing/web-platform/tests/tools/third_party/py/testing/path/repotest.dump
new file mode 100644
index 0000000000..c7819cad7a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/path/repotest.dump
@@ -0,0 +1,228 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/path/svntestbase.py b/testing/web-platform/tests/tools/third_party/py/testing/path/svntestbase.py
new file mode 100644
index 0000000000..8d94a9ca64
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/path/svntestbase.py
@@ -0,0 +1,31 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/path/test_cacheutil.py b/testing/web-platform/tests/tools/third_party/py/testing/path/test_cacheutil.py
new file mode 100644
index 0000000000..c9fc07463a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/path/test_cacheutil.py
@@ -0,0 +1,89 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/path/test_local.py b/testing/web-platform/tests/tools/third_party/py/testing/path/test_local.py
new file mode 100644
index 0000000000..1b9a7923f6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/path/test_local.py
@@ -0,0 +1,1078 @@
+# -*- 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/testing/web-platform/tests/tools/third_party/py/testing/path/test_svnauth.py b/testing/web-platform/tests/tools/third_party/py/testing/path/test_svnauth.py
new file mode 100644
index 0000000000..654f033224
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/path/test_svnauth.py
@@ -0,0 +1,460 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/path/test_svnurl.py b/testing/web-platform/tests/tools/third_party/py/testing/path/test_svnurl.py
new file mode 100644
index 0000000000..15fbea5047
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/path/test_svnurl.py
@@ -0,0 +1,95 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/path/test_svnwc.py b/testing/web-platform/tests/tools/third_party/py/testing/path/test_svnwc.py
new file mode 100644
index 0000000000..c643d9983f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/path/test_svnwc.py
@@ -0,0 +1,557 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/process/__init__.py b/testing/web-platform/tests/tools/third_party/py/testing/process/__init__.py
new file mode 100644
index 0000000000..792d600548
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/process/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/testing/web-platform/tests/tools/third_party/py/testing/process/test_cmdexec.py b/testing/web-platform/tests/tools/third_party/py/testing/process/test_cmdexec.py
new file mode 100644
index 0000000000..98463d906d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/process/test_cmdexec.py
@@ -0,0 +1,41 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/process/test_forkedfunc.py b/testing/web-platform/tests/tools/third_party/py/testing/process/test_forkedfunc.py
new file mode 100644
index 0000000000..ae0d9ab7e6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/process/test_forkedfunc.py
@@ -0,0 +1,173 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/process/test_killproc.py b/testing/web-platform/tests/tools/third_party/py/testing/process/test_killproc.py
new file mode 100644
index 0000000000..b0d6e2f515
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/process/test_killproc.py
@@ -0,0 +1,18 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/root/__init__.py b/testing/web-platform/tests/tools/third_party/py/testing/root/__init__.py
new file mode 100644
index 0000000000..792d600548
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/root/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/testing/web-platform/tests/tools/third_party/py/testing/root/test_builtin.py b/testing/web-platform/tests/tools/third_party/py/testing/root/test_builtin.py
new file mode 100644
index 0000000000..287c60d552
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/root/test_builtin.py
@@ -0,0 +1,156 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/root/test_error.py b/testing/web-platform/tests/tools/third_party/py/testing/root/test_error.py
new file mode 100644
index 0000000000..7bfbef3bd4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/root/test_error.py
@@ -0,0 +1,76 @@
+
+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/testing/web-platform/tests/tools/third_party/py/testing/root/test_py_imports.py b/testing/web-platform/tests/tools/third_party/py/testing/root/test_py_imports.py
new file mode 100644
index 0000000000..31fe6ead81
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/root/test_py_imports.py
@@ -0,0 +1,71 @@
+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/testing/web-platform/tests/tools/third_party/py/testing/root/test_std.py b/testing/web-platform/tests/tools/third_party/py/testing/root/test_std.py
new file mode 100644
index 0000000000..143556a055
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/root/test_std.py
@@ -0,0 +1,13 @@
+
+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/testing/web-platform/tests/tools/third_party/py/testing/root/test_xmlgen.py b/testing/web-platform/tests/tools/third_party/py/testing/root/test_xmlgen.py
new file mode 100644
index 0000000000..fc0e82665f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/testing/root/test_xmlgen.py
@@ -0,0 +1,146 @@
+
+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 &amp; 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 &amp; 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/testing/web-platform/tests/tools/third_party/py/tox.ini b/testing/web-platform/tests/tools/third_party/py/tox.ini
new file mode 100644
index 0000000000..f3203507fd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/py/tox.ini
@@ -0,0 +1,44 @@
+[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/testing/web-platform/tests/tools/third_party/pytest-asyncio/CHANGELOG.rst b/testing/web-platform/tests/tools/third_party/pytest-asyncio/CHANGELOG.rst
new file mode 100644
index 0000000000..8de226c4f9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/CHANGELOG.rst
@@ -0,0 +1,169 @@
+=========
+Changelog
+=========
+
+0.19.0 (22-07-13)
+=================
+- BREAKING: The default ``asyncio_mode`` is now *strict*. `#293 <https://github.com/pytest-dev/pytest-asyncio/issues/293>`_
+- Removes `setup.py` since all relevant configuration is present `setup.cfg`. Users requiring an editable installation of pytest-asyncio need to use pip v21.1 or newer. `#283 <https://github.com/pytest-dev/pytest-asyncio/issues/283>`_
+- Declare support for Python 3.11.
+
+0.18.3 (22-03-25)
+=================
+- Adds `pytest-trio <https://pypi.org/project/pytest-trio/>`_ to the test dependencies
+- Fixes a bug that caused pytest-asyncio to try to set up async pytest_trio fixtures in strict mode. `#298 <https://github.com/pytest-dev/pytest-asyncio/issues/298>`_
+
+0.18.2 (22-03-03)
+=================
+- Fix asyncio auto mode not marking static methods. `#295 <https://github.com/pytest-dev/pytest-asyncio/issues/295>`_
+- Fix a compatibility issue with Hypothesis 6.39.0. `#302 <https://github.com/pytest-dev/pytest-asyncio/issues/302>`_
+
+0.18.1 (22-02-10)
+=================
+- Fixes a regression that prevented async fixtures from working in synchronous tests. `#286 <https://github.com/pytest-dev/pytest-asyncio/issues/286>`_
+
+0.18.0 (22-02-07)
+=================
+
+- Raise a warning if @pytest.mark.asyncio is applied to non-async function. `#275 <https://github.com/pytest-dev/pytest-asyncio/issues/275>`_
+- Support parametrized ``event_loop`` fixture. `#278 <https://github.com/pytest-dev/pytest-asyncio/issues/278>`_
+
+0.17.2 (22-01-17)
+=================
+
+- Require ``typing-extensions`` on Python<3.8 only. `#269 <https://github.com/pytest-dev/pytest-asyncio/issues/269>`_
+- Fix a regression in tests collection introduced by 0.17.1, the plugin works fine with non-python tests again. `#267 <https://github.com/pytest-dev/pytest-asyncio/issues/267>`_
+
+
+0.17.1 (22-01-16)
+=================
+- Fixes a bug that prevents async Hypothesis tests from working without explicit ``asyncio`` marker when ``--asyncio-mode=auto`` is set. `#258 <https://github.com/pytest-dev/pytest-asyncio/issues/258>`_
+- Fixed a bug that closes the default event loop if the loop doesn't exist `#257 <https://github.com/pytest-dev/pytest-asyncio/issues/257>`_
+- Added type annotations. `#198 <https://github.com/pytest-dev/pytest-asyncio/issues/198>`_
+- Show asyncio mode in pytest report headers. `#266 <https://github.com/pytest-dev/pytest-asyncio/issues/266>`_
+- Relax ``asyncio_mode`` type definition; it allows to support pytest 6.1+. `#262 <https://github.com/pytest-dev/pytest-asyncio/issues/262>`_
+
+0.17.0 (22-01-13)
+=================
+- `pytest-asyncio` no longer alters existing event loop policies. `#168 <https://github.com/pytest-dev/pytest-asyncio/issues/168>`_, `#188 <https://github.com/pytest-dev/pytest-asyncio/issues/168>`_
+- Drop support for Python 3.6
+- Fixed an issue when pytest-asyncio was used in combination with `flaky` or inherited asynchronous Hypothesis tests. `#178 <https://github.com/pytest-dev/pytest-asyncio/issues/178>`_ `#231 <https://github.com/pytest-dev/pytest-asyncio/issues/231>`_
+- Added `flaky <https://pypi.org/project/flaky/>`_ to test dependencies
+- Added ``unused_udp_port`` and ``unused_udp_port_factory`` fixtures (similar to ``unused_tcp_port`` and ``unused_tcp_port_factory`` counterparts. `#99 <https://github.com/pytest-dev/pytest-asyncio/issues/99>`_
+- Added the plugin modes: *strict*, *auto*, and *legacy*. See `documentation <https://github.com/pytest-dev/pytest-asyncio#modes>`_ for details. `#125 <https://github.com/pytest-dev/pytest-asyncio/issues/125>`_
+- Correctly process ``KeyboardInterrupt`` during async fixture setup phase `#219 <https://github.com/pytest-dev/pytest-asyncio/issues/219>`_
+
+0.16.0 (2021-10-16)
+===================
+- Add support for Python 3.10
+
+0.15.1 (2021-04-22)
+===================
+- Hotfix for errors while closing event loops while replacing them.
+ `#209 <https://github.com/pytest-dev/pytest-asyncio/issues/209>`_
+ `#210 <https://github.com/pytest-dev/pytest-asyncio/issues/210>`_
+
+0.15.0 (2021-04-19)
+===================
+- Add support for Python 3.9
+- Abandon support for Python 3.5. If you still require support for Python 3.5, please use pytest-asyncio v0.14 or earlier.
+- Set ``unused_tcp_port_factory`` fixture scope to 'session'.
+ `#163 <https://github.com/pytest-dev/pytest-asyncio/pull/163>`_
+- Properly close event loops when replacing them.
+ `#208 <https://github.com/pytest-dev/pytest-asyncio/issues/208>`_
+
+0.14.0 (2020-06-24)
+===================
+- Fix `#162 <https://github.com/pytest-dev/pytest-asyncio/issues/162>`_, and ``event_loop`` fixture behavior now is coherent on all scopes.
+ `#164 <https://github.com/pytest-dev/pytest-asyncio/pull/164>`_
+
+0.12.0 (2020-05-04)
+===================
+- Run the event loop fixture as soon as possible. This helps with fixtures that have an implicit dependency on the event loop.
+ `#156 <https://github.com/pytest-dev/pytest-asyncio/pull/156>`_
+
+0.11.0 (2020-04-20)
+===================
+- Test on 3.8, drop 3.3 and 3.4. Stick to 0.10 for these versions.
+ `#152 <https://github.com/pytest-dev/pytest-asyncio/pull/152>`_
+- Use the new Pytest 5.4.0 Function API. We therefore depend on pytest >= 5.4.0.
+ `#142 <https://github.com/pytest-dev/pytest-asyncio/pull/142>`_
+- Better ``pytest.skip`` support.
+ `#126 <https://github.com/pytest-dev/pytest-asyncio/pull/126>`_
+
+0.10.0 (2019-01-08)
+====================
+- ``pytest-asyncio`` integrates with `Hypothesis <https://hypothesis.readthedocs.io>`_
+ to support ``@given`` on async test functions using ``asyncio``.
+ `#102 <https://github.com/pytest-dev/pytest-asyncio/pull/102>`_
+- Pytest 4.1 support.
+ `#105 <https://github.com/pytest-dev/pytest-asyncio/pull/105>`_
+
+0.9.0 (2018-07-28)
+==================
+- Python 3.7 support.
+- Remove ``event_loop_process_pool`` fixture and
+ ``pytest.mark.asyncio_process_pool`` marker (see
+ https://bugs.python.org/issue34075 for deprecation and removal details)
+
+0.8.0 (2017-09-23)
+==================
+- Improve integration with other packages (like aiohttp) with more careful event loop handling.
+ `#64 <https://github.com/pytest-dev/pytest-asyncio/pull/64>`_
+
+0.7.0 (2017-09-08)
+==================
+- Python versions pre-3.6 can use the async_generator library for async fixtures.
+ `#62 <https://github.com/pytest-dev/pytest-asyncio/pull/62>`
+
+0.6.0 (2017-05-28)
+==================
+- Support for Python versions pre-3.5 has been dropped.
+- ``pytestmark`` now works on both module and class level.
+- The ``forbid_global_loop`` parameter has been removed.
+- Support for async and async gen fixtures has been added.
+ `#45 <https://github.com/pytest-dev/pytest-asyncio/pull/45>`_
+- The deprecation warning regarding ``asyncio.async()`` has been fixed.
+ `#51 <https://github.com/pytest-dev/pytest-asyncio/pull/51>`_
+
+0.5.0 (2016-09-07)
+==================
+- Introduced a changelog.
+ `#31 <https://github.com/pytest-dev/pytest-asyncio/issues/31>`_
+- The ``event_loop`` fixture is again responsible for closing itself.
+ This makes the fixture slightly harder to correctly override, but enables
+ other fixtures to depend on it correctly.
+ `#30 <https://github.com/pytest-dev/pytest-asyncio/issues/30>`_
+- Deal with the event loop policy by wrapping a special pytest hook,
+ ``pytest_fixture_setup``. This allows setting the policy before fixtures
+ dependent on the ``event_loop`` fixture run, thus allowing them to take
+ advantage of the ``forbid_global_loop`` parameter. As a consequence of this,
+ we now depend on pytest 3.0.
+ `#29 <https://github.com/pytest-dev/pytest-asyncio/issues/29>`_
+
+0.4.1 (2016-06-01)
+==================
+- Fix a bug preventing the propagation of exceptions from the plugin.
+ `#25 <https://github.com/pytest-dev/pytest-asyncio/issues/25>`_
+
+0.4.0 (2016-05-30)
+==================
+- Make ``event_loop`` fixtures simpler to override by closing them in the
+ plugin, instead of directly in the fixture.
+ `#21 <https://github.com/pytest-dev/pytest-asyncio/pull/21>`_
+- Introduce the ``forbid_global_loop`` parameter.
+ `#21 <https://github.com/pytest-dev/pytest-asyncio/pull/21>`_
+
+0.3.0 (2015-12-19)
+==================
+- Support for Python 3.5 ``async``/``await`` syntax.
+ `#17 <https://github.com/pytest-dev/pytest-asyncio/pull/17>`_
+
+0.2.0 (2015-08-01)
+==================
+- ``unused_tcp_port_factory`` fixture.
+ `#10 <https://github.com/pytest-dev/pytest-asyncio/issues/10>`_
+
+0.1.1 (2015-04-23)
+==================
+Initial release.
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/LICENSE b/testing/web-platform/tests/tools/third_party/pytest-asyncio/LICENSE
new file mode 100644
index 0000000000..5c304d1a4a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/LICENSE
@@ -0,0 +1,201 @@
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/MANIFEST.in b/testing/web-platform/tests/tools/third_party/pytest-asyncio/MANIFEST.in
new file mode 100644
index 0000000000..fdf813e915
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/MANIFEST.in
@@ -0,0 +1,5 @@
+include CHANGELOG.rst
+
+recursive-exclude .github *
+exclude .gitignore
+exclude .pre-commit-config.yaml
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/Makefile b/testing/web-platform/tests/tools/third_party/pytest-asyncio/Makefile
new file mode 100644
index 0000000000..2b0216f99e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/Makefile
@@ -0,0 +1,39 @@
+.PHONY: clean clean-build clean-pyc clean-test lint test
+
+clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
+
+clean-build: ## remove build artifacts
+ rm -fr build/
+ rm -fr dist/
+ rm -fr .eggs/
+ find . -name '*.egg-info' -exec rm -fr {} +
+ find . -name '*.egg' -exec rm -f {} +
+
+clean-pyc: ## remove Python file artifacts
+ find . -name '*.pyc' -exec rm -f {} +
+ find . -name '*.pyo' -exec rm -f {} +
+ find . -name '*~' -exec rm -f {} +
+ find . -name '__pycache__' -exec rm -fr {} +
+
+clean-test: ## remove test and coverage artifacts
+ rm -fr .tox/
+ rm -f .coverage
+ rm -fr htmlcov/
+
+lint:
+# CI env-var is set by GitHub actions
+ifdef CI
+ python -m pre_commit run --all-files --show-diff-on-failure
+else
+ python -m pre_commit run --all-files
+endif
+ python -m mypy pytest_asyncio --show-error-codes
+
+test:
+ coverage run -m pytest tests
+ coverage xml
+ coverage report
+
+install:
+ pip install -U pre-commit
+ pre-commit install
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/PKG-INFO b/testing/web-platform/tests/tools/third_party/pytest-asyncio/PKG-INFO
new file mode 100644
index 0000000000..19acaa4d51
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/PKG-INFO
@@ -0,0 +1,285 @@
+Metadata-Version: 2.1
+Name: pytest-asyncio
+Version: 0.19.0
+Summary: Pytest support for asyncio
+Home-page: https://github.com/pytest-dev/pytest-asyncio
+Author: Tin Tvrtković <tinchester@gmail.com>
+Author-email: tinchester@gmail.com
+License: Apache 2.0
+Project-URL: GitHub, https://github.com/pytest-dev/pytest-asyncio
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Topic :: Software Development :: Testing
+Classifier: Framework :: AsyncIO
+Classifier: Framework :: Pytest
+Classifier: Typing :: Typed
+Requires-Python: >=3.7
+Description-Content-Type: text/x-rst
+Provides-Extra: testing
+License-File: LICENSE
+
+pytest-asyncio: pytest support for asyncio
+==========================================
+
+.. image:: https://img.shields.io/pypi/v/pytest-asyncio.svg
+ :target: https://pypi.python.org/pypi/pytest-asyncio
+.. image:: https://github.com/pytest-dev/pytest-asyncio/workflows/CI/badge.svg
+ :target: https://github.com/pytest-dev/pytest-asyncio/actions?workflow=CI
+.. image:: https://codecov.io/gh/pytest-dev/pytest-asyncio/branch/master/graph/badge.svg
+ :target: https://codecov.io/gh/pytest-dev/pytest-asyncio
+.. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio.svg
+ :target: https://github.com/pytest-dev/pytest-asyncio
+ :alt: Supported Python versions
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/ambv/black
+
+pytest-asyncio is an Apache2 licensed library, written in Python, for testing
+asyncio code with pytest.
+
+asyncio code is usually written in the form of coroutines, which makes it
+slightly more difficult to test using normal testing tools. pytest-asyncio
+provides useful fixtures and markers to make testing easier.
+
+.. code-block:: python
+
+ @pytest.mark.asyncio
+ async def test_some_asyncio_code():
+ res = await library.do_something()
+ assert b"expected result" == res
+
+pytest-asyncio has been strongly influenced by pytest-tornado_.
+
+.. _pytest-tornado: https://github.com/eugeniy/pytest-tornado
+
+Features
+--------
+
+- fixtures for creating and injecting versions of the asyncio event loop
+- fixtures for injecting unused tcp/udp ports
+- pytest markers for treating tests as asyncio coroutines
+- easy testing with non-default event loops
+- support for `async def` fixtures and async generator fixtures
+- support *auto* mode to handle all async fixtures and tests automatically by asyncio;
+ provide *strict* mode if a test suite should work with different async frameworks
+ simultaneously, e.g. ``asyncio`` and ``trio``.
+
+Installation
+------------
+
+To install pytest-asyncio, simply:
+
+.. code-block:: bash
+
+ $ pip install pytest-asyncio
+
+This is enough for pytest to pick up pytest-asyncio.
+
+Modes
+-----
+
+Starting from ``pytest-asyncio>=0.17``, three modes are provided: *auto*, *strict* and
+*legacy*. Starting from ``pytest-asyncio>=0.19`` the *strict* mode is the default.
+
+The mode can be set by ``asyncio_mode`` configuration option in `configuration file
+<https://docs.pytest.org/en/latest/reference/customize.html>`_:
+
+.. code-block:: ini
+
+ # pytest.ini
+ [pytest]
+ asyncio_mode = auto
+
+The value can be overridden by command-line option for ``pytest`` invocation:
+
+.. code-block:: bash
+
+ $ pytest tests --asyncio-mode=strict
+
+Auto mode
+~~~~~~~~~
+
+When the mode is auto, all discovered *async* tests are considered *asyncio-driven* even
+if they have no ``@pytest.mark.asyncio`` marker.
+
+All async fixtures are considered *asyncio-driven* as well, even if they are decorated
+with a regular ``@pytest.fixture`` decorator instead of dedicated
+``@pytest_asyncio.fixture`` counterpart.
+
+*asyncio-driven* means that tests and fixtures are executed by ``pytest-asyncio``
+plugin.
+
+This mode requires the simplest tests and fixtures configuration and is
+recommended for default usage *unless* the same project and its test suite should
+execute tests from different async frameworks, e.g. ``asyncio`` and ``trio``. In this
+case, auto-handling can break tests designed for other framework; please use *strict*
+mode instead.
+
+Strict mode
+~~~~~~~~~~~
+
+Strict mode enforces ``@pytest.mark.asyncio`` and ``@pytest_asyncio.fixture`` usage.
+Without these markers, tests and fixtures are not considered as *asyncio-driven*, other
+pytest plugin can handle them.
+
+Please use this mode if multiple async frameworks should be combined in the same test
+suite.
+
+This mode is used by default for the sake of project inter-compatibility.
+
+
+Legacy mode
+~~~~~~~~~~~
+
+This mode follows rules used by ``pytest-asyncio<0.17``: tests are not auto-marked but
+fixtures are.
+
+Deprecation warnings are emitted with suggestion to either switching to ``auto`` mode
+or using ``strict`` mode with ``@pytest_asyncio.fixture`` decorators.
+
+The default was changed to ``strict`` in ``pytest-asyncio>=0.19``.
+
+
+Fixtures
+--------
+
+``event_loop``
+~~~~~~~~~~~~~~
+Creates a new asyncio event loop based on the current event loop policy. The new loop
+is available as the return value of this fixture or via `asyncio.get_running_loop <https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_running_loop>`__.
+The event loop is closed when the fixture scope ends. The fixture scope defaults
+to ``function`` scope.
+
+Note that just using the ``event_loop`` fixture won't make your test function
+a coroutine. You'll need to interact with the event loop directly, using methods
+like ``event_loop.run_until_complete``. See the ``pytest.mark.asyncio`` marker
+for treating test functions like coroutines.
+
+.. code-block:: python
+
+ def test_http_client(event_loop):
+ url = "http://httpbin.org/get"
+ resp = event_loop.run_until_complete(http_client(url))
+ assert b"HTTP/1.1 200 OK" in resp
+
+The ``event_loop`` fixture can be overridden in any of the standard pytest locations,
+e.g. directly in the test file, or in ``conftest.py``. This allows redefining the
+fixture scope, for example:
+
+.. code-block:: python
+
+ @pytest.fixture(scope="session")
+ def event_loop():
+ policy = asyncio.get_event_loop_policy()
+ loop = policy.new_event_loop()
+ yield loop
+ loop.close()
+
+If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture.
+
+If the ``pytest.mark.asyncio`` marker is applied to a test function, the ``event_loop``
+fixture will be requested automatically by the test function.
+
+``unused_tcp_port``
+~~~~~~~~~~~~~~~~~~~
+Finds and yields a single unused TCP port on the localhost interface. Useful for
+binding temporary test servers.
+
+``unused_tcp_port_factory``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+A callable which returns a different unused TCP port each invocation. Useful
+when several unused TCP ports are required in a test.
+
+.. code-block:: python
+
+ def a_test(unused_tcp_port_factory):
+ port1, port2 = unused_tcp_port_factory(), unused_tcp_port_factory()
+ ...
+
+``unused_udp_port`` and ``unused_udp_port_factory``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Work just like their TCP counterparts but return unused UDP ports.
+
+
+Async fixtures
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be decorated with ``@pytest_asyncio.fixture``.
+
+.. code-block:: python3
+
+ import pytest_asyncio
+
+
+ @pytest_asyncio.fixture
+ async def async_gen_fixture():
+ await asyncio.sleep(0.1)
+ yield "a value"
+
+
+ @pytest_asyncio.fixture(scope="module")
+ async def async_fixture():
+ return await asyncio.sleep(0.1)
+
+All scopes are supported, but if you use a non-function scope you will need
+to redefine the ``event_loop`` fixture to have the same or broader scope.
+Async fixtures need the event loop, and so must have the same or narrower scope
+than the ``event_loop`` fixture.
+
+*auto* and *legacy* mode automatically converts async fixtures declared with the
+standard ``@pytest.fixture`` decorator to *asyncio-driven* versions.
+
+
+Markers
+-------
+
+``pytest.mark.asyncio``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Mark your test coroutine with this marker and pytest will execute it as an
+asyncio task using the event loop provided by the ``event_loop`` fixture. See
+the introductory section for an example.
+
+The event loop used can be overridden by overriding the ``event_loop`` fixture
+(see above).
+
+In order to make your test code a little more concise, the pytest |pytestmark|_
+feature can be used to mark entire modules or classes with this marker.
+Only test coroutines will be affected (by default, coroutines prefixed by
+``test_``), so, for example, fixtures are safe to define.
+
+.. code-block:: python
+
+ import asyncio
+
+ import pytest
+
+ # All test coroutines will be treated as marked.
+ pytestmark = pytest.mark.asyncio
+
+
+ async def test_example(event_loop):
+ """No marker!"""
+ await asyncio.sleep(0, loop=event_loop)
+
+In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted, the marker is added
+automatically to *async* test functions.
+
+
+.. |pytestmark| replace:: ``pytestmark``
+.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules
+
+Note about unittest
+-------------------
+
+Test classes subclassing the standard `unittest <https://docs.python.org/3/library/unittest.html>`__ library are not supported, users
+are recommended to use `unittest.IsolatedAsyncioTestCase <https://docs.python.org/3/library/unittest.html#unittest.IsolatedAsyncioTestCase>`__
+or an async framework such as `asynctest <https://asynctest.readthedocs.io/en/latest>`__.
+
+Contributing
+------------
+Contributions are very welcome. Tests can be run with ``tox``, please ensure
+the coverage at least stays the same before you submit a pull request.
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/README.rst b/testing/web-platform/tests/tools/third_party/pytest-asyncio/README.rst
new file mode 100644
index 0000000000..1fc5ef4738
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/README.rst
@@ -0,0 +1,259 @@
+pytest-asyncio: pytest support for asyncio
+==========================================
+
+.. image:: https://img.shields.io/pypi/v/pytest-asyncio.svg
+ :target: https://pypi.python.org/pypi/pytest-asyncio
+.. image:: https://github.com/pytest-dev/pytest-asyncio/workflows/CI/badge.svg
+ :target: https://github.com/pytest-dev/pytest-asyncio/actions?workflow=CI
+.. image:: https://codecov.io/gh/pytest-dev/pytest-asyncio/branch/master/graph/badge.svg
+ :target: https://codecov.io/gh/pytest-dev/pytest-asyncio
+.. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio.svg
+ :target: https://github.com/pytest-dev/pytest-asyncio
+ :alt: Supported Python versions
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/ambv/black
+
+pytest-asyncio is an Apache2 licensed library, written in Python, for testing
+asyncio code with pytest.
+
+asyncio code is usually written in the form of coroutines, which makes it
+slightly more difficult to test using normal testing tools. pytest-asyncio
+provides useful fixtures and markers to make testing easier.
+
+.. code-block:: python
+
+ @pytest.mark.asyncio
+ async def test_some_asyncio_code():
+ res = await library.do_something()
+ assert b"expected result" == res
+
+pytest-asyncio has been strongly influenced by pytest-tornado_.
+
+.. _pytest-tornado: https://github.com/eugeniy/pytest-tornado
+
+Features
+--------
+
+- fixtures for creating and injecting versions of the asyncio event loop
+- fixtures for injecting unused tcp/udp ports
+- pytest markers for treating tests as asyncio coroutines
+- easy testing with non-default event loops
+- support for `async def` fixtures and async generator fixtures
+- support *auto* mode to handle all async fixtures and tests automatically by asyncio;
+ provide *strict* mode if a test suite should work with different async frameworks
+ simultaneously, e.g. ``asyncio`` and ``trio``.
+
+Installation
+------------
+
+To install pytest-asyncio, simply:
+
+.. code-block:: bash
+
+ $ pip install pytest-asyncio
+
+This is enough for pytest to pick up pytest-asyncio.
+
+Modes
+-----
+
+Starting from ``pytest-asyncio>=0.17``, three modes are provided: *auto*, *strict* and
+*legacy*. Starting from ``pytest-asyncio>=0.19`` the *strict* mode is the default.
+
+The mode can be set by ``asyncio_mode`` configuration option in `configuration file
+<https://docs.pytest.org/en/latest/reference/customize.html>`_:
+
+.. code-block:: ini
+
+ # pytest.ini
+ [pytest]
+ asyncio_mode = auto
+
+The value can be overridden by command-line option for ``pytest`` invocation:
+
+.. code-block:: bash
+
+ $ pytest tests --asyncio-mode=strict
+
+Auto mode
+~~~~~~~~~
+
+When the mode is auto, all discovered *async* tests are considered *asyncio-driven* even
+if they have no ``@pytest.mark.asyncio`` marker.
+
+All async fixtures are considered *asyncio-driven* as well, even if they are decorated
+with a regular ``@pytest.fixture`` decorator instead of dedicated
+``@pytest_asyncio.fixture`` counterpart.
+
+*asyncio-driven* means that tests and fixtures are executed by ``pytest-asyncio``
+plugin.
+
+This mode requires the simplest tests and fixtures configuration and is
+recommended for default usage *unless* the same project and its test suite should
+execute tests from different async frameworks, e.g. ``asyncio`` and ``trio``. In this
+case, auto-handling can break tests designed for other framework; please use *strict*
+mode instead.
+
+Strict mode
+~~~~~~~~~~~
+
+Strict mode enforces ``@pytest.mark.asyncio`` and ``@pytest_asyncio.fixture`` usage.
+Without these markers, tests and fixtures are not considered as *asyncio-driven*, other
+pytest plugin can handle them.
+
+Please use this mode if multiple async frameworks should be combined in the same test
+suite.
+
+This mode is used by default for the sake of project inter-compatibility.
+
+
+Legacy mode
+~~~~~~~~~~~
+
+This mode follows rules used by ``pytest-asyncio<0.17``: tests are not auto-marked but
+fixtures are.
+
+Deprecation warnings are emitted with suggestion to either switching to ``auto`` mode
+or using ``strict`` mode with ``@pytest_asyncio.fixture`` decorators.
+
+The default was changed to ``strict`` in ``pytest-asyncio>=0.19``.
+
+
+Fixtures
+--------
+
+``event_loop``
+~~~~~~~~~~~~~~
+Creates a new asyncio event loop based on the current event loop policy. The new loop
+is available as the return value of this fixture or via `asyncio.get_running_loop <https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_running_loop>`__.
+The event loop is closed when the fixture scope ends. The fixture scope defaults
+to ``function`` scope.
+
+Note that just using the ``event_loop`` fixture won't make your test function
+a coroutine. You'll need to interact with the event loop directly, using methods
+like ``event_loop.run_until_complete``. See the ``pytest.mark.asyncio`` marker
+for treating test functions like coroutines.
+
+.. code-block:: python
+
+ def test_http_client(event_loop):
+ url = "http://httpbin.org/get"
+ resp = event_loop.run_until_complete(http_client(url))
+ assert b"HTTP/1.1 200 OK" in resp
+
+The ``event_loop`` fixture can be overridden in any of the standard pytest locations,
+e.g. directly in the test file, or in ``conftest.py``. This allows redefining the
+fixture scope, for example:
+
+.. code-block:: python
+
+ @pytest.fixture(scope="session")
+ def event_loop():
+ policy = asyncio.get_event_loop_policy()
+ loop = policy.new_event_loop()
+ yield loop
+ loop.close()
+
+If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture.
+
+If the ``pytest.mark.asyncio`` marker is applied to a test function, the ``event_loop``
+fixture will be requested automatically by the test function.
+
+``unused_tcp_port``
+~~~~~~~~~~~~~~~~~~~
+Finds and yields a single unused TCP port on the localhost interface. Useful for
+binding temporary test servers.
+
+``unused_tcp_port_factory``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+A callable which returns a different unused TCP port each invocation. Useful
+when several unused TCP ports are required in a test.
+
+.. code-block:: python
+
+ def a_test(unused_tcp_port_factory):
+ port1, port2 = unused_tcp_port_factory(), unused_tcp_port_factory()
+ ...
+
+``unused_udp_port`` and ``unused_udp_port_factory``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Work just like their TCP counterparts but return unused UDP ports.
+
+
+Async fixtures
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be decorated with ``@pytest_asyncio.fixture``.
+
+.. code-block:: python3
+
+ import pytest_asyncio
+
+
+ @pytest_asyncio.fixture
+ async def async_gen_fixture():
+ await asyncio.sleep(0.1)
+ yield "a value"
+
+
+ @pytest_asyncio.fixture(scope="module")
+ async def async_fixture():
+ return await asyncio.sleep(0.1)
+
+All scopes are supported, but if you use a non-function scope you will need
+to redefine the ``event_loop`` fixture to have the same or broader scope.
+Async fixtures need the event loop, and so must have the same or narrower scope
+than the ``event_loop`` fixture.
+
+*auto* and *legacy* mode automatically converts async fixtures declared with the
+standard ``@pytest.fixture`` decorator to *asyncio-driven* versions.
+
+
+Markers
+-------
+
+``pytest.mark.asyncio``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Mark your test coroutine with this marker and pytest will execute it as an
+asyncio task using the event loop provided by the ``event_loop`` fixture. See
+the introductory section for an example.
+
+The event loop used can be overridden by overriding the ``event_loop`` fixture
+(see above).
+
+In order to make your test code a little more concise, the pytest |pytestmark|_
+feature can be used to mark entire modules or classes with this marker.
+Only test coroutines will be affected (by default, coroutines prefixed by
+``test_``), so, for example, fixtures are safe to define.
+
+.. code-block:: python
+
+ import asyncio
+
+ import pytest
+
+ # All test coroutines will be treated as marked.
+ pytestmark = pytest.mark.asyncio
+
+
+ async def test_example(event_loop):
+ """No marker!"""
+ await asyncio.sleep(0, loop=event_loop)
+
+In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted, the marker is added
+automatically to *async* test functions.
+
+
+.. |pytestmark| replace:: ``pytestmark``
+.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules
+
+Note about unittest
+-------------------
+
+Test classes subclassing the standard `unittest <https://docs.python.org/3/library/unittest.html>`__ library are not supported, users
+are recommended to use `unittest.IsolatedAsyncioTestCase <https://docs.python.org/3/library/unittest.html#unittest.IsolatedAsyncioTestCase>`__
+or an async framework such as `asynctest <https://asynctest.readthedocs.io/en/latest>`__.
+
+Contributing
+------------
+Contributions are very welcome. Tests can be run with ``tox``, please ensure
+the coverage at least stays the same before you submit a pull request.
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/default/constraints.txt b/testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/default/constraints.txt
new file mode 100644
index 0000000000..cd99339f5e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/default/constraints.txt
@@ -0,0 +1,24 @@
+async-generator==1.10
+attrs==21.4.0
+coverage==6.4.1
+flaky==3.7.0
+hypothesis==6.48.3
+idna==3.3
+importlib-metadata==4.12.0
+iniconfig==1.1.1
+mypy==0.961
+mypy-extensions==0.4.3
+outcome==1.2.0
+packaging==21.3
+pluggy==1.0.0
+py==1.11.0
+pyparsing==3.0.9
+pytest==7.1.2
+pytest-trio==0.7.0
+sniffio==1.2.0
+sortedcontainers==2.4.0
+tomli==2.0.1
+trio==0.21.0
+typed-ast==1.5.4
+typing_extensions==4.3.0
+zipp==3.8.0
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/default/requirements.txt b/testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/default/requirements.txt
new file mode 100644
index 0000000000..01b2484e6b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/default/requirements.txt
@@ -0,0 +1,4 @@
+# Always adjust install_requires in setup.cfg and pytest-min-requirements.txt
+# when changing runtime dependencies
+pytest >= 6.1.0
+typing-extensions >= 3.7.2; python_version < "3.8"
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/pytest-min/constraints.txt b/testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/pytest-min/constraints.txt
new file mode 100644
index 0000000000..33f7948f4c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/pytest-min/constraints.txt
@@ -0,0 +1,22 @@
+async-generator==1.10
+attrs==21.4.0
+coverage==6.3.2
+flaky==3.7.0
+hypothesis==6.43.3
+idna==3.3
+iniconfig==1.1.1
+mypy==0.942
+mypy-extensions==0.4.3
+outcome==1.1.0
+packaging==21.3
+pluggy==0.13.1
+py==1.11.0
+pyparsing==3.0.8
+pytest==6.1.0
+pytest-trio==0.7.0
+sniffio==1.2.0
+sortedcontainers==2.4.0
+toml==0.10.2
+tomli==2.0.1
+trio==0.20.0
+typing_extensions==4.2.0
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/pytest-min/requirements.txt b/testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/pytest-min/requirements.txt
new file mode 100644
index 0000000000..4fc6ef2fa3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/dependencies/pytest-min/requirements.txt
@@ -0,0 +1,4 @@
+# Always adjust install_requires in setup.cfg and requirements.txt
+# when changing minimum version dependencies
+pytest == 6.1.0
+typing-extensions >= 3.7.2; python_version < "3.8"
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/pyproject.toml b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pyproject.toml
new file mode 100644
index 0000000000..81540a53a6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pyproject.toml
@@ -0,0 +1,10 @@
+[build-system]
+requires = [
+ "setuptools>=51.0",
+ "wheel>=0.36",
+ "setuptools_scm[toml]>=6.2"
+]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
+write_to = "pytest_asyncio/_version.py"
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/PKG-INFO b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/PKG-INFO
new file mode 100644
index 0000000000..19acaa4d51
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/PKG-INFO
@@ -0,0 +1,285 @@
+Metadata-Version: 2.1
+Name: pytest-asyncio
+Version: 0.19.0
+Summary: Pytest support for asyncio
+Home-page: https://github.com/pytest-dev/pytest-asyncio
+Author: Tin Tvrtković <tinchester@gmail.com>
+Author-email: tinchester@gmail.com
+License: Apache 2.0
+Project-URL: GitHub, https://github.com/pytest-dev/pytest-asyncio
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Topic :: Software Development :: Testing
+Classifier: Framework :: AsyncIO
+Classifier: Framework :: Pytest
+Classifier: Typing :: Typed
+Requires-Python: >=3.7
+Description-Content-Type: text/x-rst
+Provides-Extra: testing
+License-File: LICENSE
+
+pytest-asyncio: pytest support for asyncio
+==========================================
+
+.. image:: https://img.shields.io/pypi/v/pytest-asyncio.svg
+ :target: https://pypi.python.org/pypi/pytest-asyncio
+.. image:: https://github.com/pytest-dev/pytest-asyncio/workflows/CI/badge.svg
+ :target: https://github.com/pytest-dev/pytest-asyncio/actions?workflow=CI
+.. image:: https://codecov.io/gh/pytest-dev/pytest-asyncio/branch/master/graph/badge.svg
+ :target: https://codecov.io/gh/pytest-dev/pytest-asyncio
+.. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio.svg
+ :target: https://github.com/pytest-dev/pytest-asyncio
+ :alt: Supported Python versions
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/ambv/black
+
+pytest-asyncio is an Apache2 licensed library, written in Python, for testing
+asyncio code with pytest.
+
+asyncio code is usually written in the form of coroutines, which makes it
+slightly more difficult to test using normal testing tools. pytest-asyncio
+provides useful fixtures and markers to make testing easier.
+
+.. code-block:: python
+
+ @pytest.mark.asyncio
+ async def test_some_asyncio_code():
+ res = await library.do_something()
+ assert b"expected result" == res
+
+pytest-asyncio has been strongly influenced by pytest-tornado_.
+
+.. _pytest-tornado: https://github.com/eugeniy/pytest-tornado
+
+Features
+--------
+
+- fixtures for creating and injecting versions of the asyncio event loop
+- fixtures for injecting unused tcp/udp ports
+- pytest markers for treating tests as asyncio coroutines
+- easy testing with non-default event loops
+- support for `async def` fixtures and async generator fixtures
+- support *auto* mode to handle all async fixtures and tests automatically by asyncio;
+ provide *strict* mode if a test suite should work with different async frameworks
+ simultaneously, e.g. ``asyncio`` and ``trio``.
+
+Installation
+------------
+
+To install pytest-asyncio, simply:
+
+.. code-block:: bash
+
+ $ pip install pytest-asyncio
+
+This is enough for pytest to pick up pytest-asyncio.
+
+Modes
+-----
+
+Starting from ``pytest-asyncio>=0.17``, three modes are provided: *auto*, *strict* and
+*legacy*. Starting from ``pytest-asyncio>=0.19`` the *strict* mode is the default.
+
+The mode can be set by ``asyncio_mode`` configuration option in `configuration file
+<https://docs.pytest.org/en/latest/reference/customize.html>`_:
+
+.. code-block:: ini
+
+ # pytest.ini
+ [pytest]
+ asyncio_mode = auto
+
+The value can be overridden by command-line option for ``pytest`` invocation:
+
+.. code-block:: bash
+
+ $ pytest tests --asyncio-mode=strict
+
+Auto mode
+~~~~~~~~~
+
+When the mode is auto, all discovered *async* tests are considered *asyncio-driven* even
+if they have no ``@pytest.mark.asyncio`` marker.
+
+All async fixtures are considered *asyncio-driven* as well, even if they are decorated
+with a regular ``@pytest.fixture`` decorator instead of dedicated
+``@pytest_asyncio.fixture`` counterpart.
+
+*asyncio-driven* means that tests and fixtures are executed by ``pytest-asyncio``
+plugin.
+
+This mode requires the simplest tests and fixtures configuration and is
+recommended for default usage *unless* the same project and its test suite should
+execute tests from different async frameworks, e.g. ``asyncio`` and ``trio``. In this
+case, auto-handling can break tests designed for other framework; please use *strict*
+mode instead.
+
+Strict mode
+~~~~~~~~~~~
+
+Strict mode enforces ``@pytest.mark.asyncio`` and ``@pytest_asyncio.fixture`` usage.
+Without these markers, tests and fixtures are not considered as *asyncio-driven*, other
+pytest plugin can handle them.
+
+Please use this mode if multiple async frameworks should be combined in the same test
+suite.
+
+This mode is used by default for the sake of project inter-compatibility.
+
+
+Legacy mode
+~~~~~~~~~~~
+
+This mode follows rules used by ``pytest-asyncio<0.17``: tests are not auto-marked but
+fixtures are.
+
+Deprecation warnings are emitted with suggestion to either switching to ``auto`` mode
+or using ``strict`` mode with ``@pytest_asyncio.fixture`` decorators.
+
+The default was changed to ``strict`` in ``pytest-asyncio>=0.19``.
+
+
+Fixtures
+--------
+
+``event_loop``
+~~~~~~~~~~~~~~
+Creates a new asyncio event loop based on the current event loop policy. The new loop
+is available as the return value of this fixture or via `asyncio.get_running_loop <https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_running_loop>`__.
+The event loop is closed when the fixture scope ends. The fixture scope defaults
+to ``function`` scope.
+
+Note that just using the ``event_loop`` fixture won't make your test function
+a coroutine. You'll need to interact with the event loop directly, using methods
+like ``event_loop.run_until_complete``. See the ``pytest.mark.asyncio`` marker
+for treating test functions like coroutines.
+
+.. code-block:: python
+
+ def test_http_client(event_loop):
+ url = "http://httpbin.org/get"
+ resp = event_loop.run_until_complete(http_client(url))
+ assert b"HTTP/1.1 200 OK" in resp
+
+The ``event_loop`` fixture can be overridden in any of the standard pytest locations,
+e.g. directly in the test file, or in ``conftest.py``. This allows redefining the
+fixture scope, for example:
+
+.. code-block:: python
+
+ @pytest.fixture(scope="session")
+ def event_loop():
+ policy = asyncio.get_event_loop_policy()
+ loop = policy.new_event_loop()
+ yield loop
+ loop.close()
+
+If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture.
+
+If the ``pytest.mark.asyncio`` marker is applied to a test function, the ``event_loop``
+fixture will be requested automatically by the test function.
+
+``unused_tcp_port``
+~~~~~~~~~~~~~~~~~~~
+Finds and yields a single unused TCP port on the localhost interface. Useful for
+binding temporary test servers.
+
+``unused_tcp_port_factory``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+A callable which returns a different unused TCP port each invocation. Useful
+when several unused TCP ports are required in a test.
+
+.. code-block:: python
+
+ def a_test(unused_tcp_port_factory):
+ port1, port2 = unused_tcp_port_factory(), unused_tcp_port_factory()
+ ...
+
+``unused_udp_port`` and ``unused_udp_port_factory``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Work just like their TCP counterparts but return unused UDP ports.
+
+
+Async fixtures
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be decorated with ``@pytest_asyncio.fixture``.
+
+.. code-block:: python3
+
+ import pytest_asyncio
+
+
+ @pytest_asyncio.fixture
+ async def async_gen_fixture():
+ await asyncio.sleep(0.1)
+ yield "a value"
+
+
+ @pytest_asyncio.fixture(scope="module")
+ async def async_fixture():
+ return await asyncio.sleep(0.1)
+
+All scopes are supported, but if you use a non-function scope you will need
+to redefine the ``event_loop`` fixture to have the same or broader scope.
+Async fixtures need the event loop, and so must have the same or narrower scope
+than the ``event_loop`` fixture.
+
+*auto* and *legacy* mode automatically converts async fixtures declared with the
+standard ``@pytest.fixture`` decorator to *asyncio-driven* versions.
+
+
+Markers
+-------
+
+``pytest.mark.asyncio``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Mark your test coroutine with this marker and pytest will execute it as an
+asyncio task using the event loop provided by the ``event_loop`` fixture. See
+the introductory section for an example.
+
+The event loop used can be overridden by overriding the ``event_loop`` fixture
+(see above).
+
+In order to make your test code a little more concise, the pytest |pytestmark|_
+feature can be used to mark entire modules or classes with this marker.
+Only test coroutines will be affected (by default, coroutines prefixed by
+``test_``), so, for example, fixtures are safe to define.
+
+.. code-block:: python
+
+ import asyncio
+
+ import pytest
+
+ # All test coroutines will be treated as marked.
+ pytestmark = pytest.mark.asyncio
+
+
+ async def test_example(event_loop):
+ """No marker!"""
+ await asyncio.sleep(0, loop=event_loop)
+
+In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted, the marker is added
+automatically to *async* test functions.
+
+
+.. |pytestmark| replace:: ``pytestmark``
+.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules
+
+Note about unittest
+-------------------
+
+Test classes subclassing the standard `unittest <https://docs.python.org/3/library/unittest.html>`__ library are not supported, users
+are recommended to use `unittest.IsolatedAsyncioTestCase <https://docs.python.org/3/library/unittest.html#unittest.IsolatedAsyncioTestCase>`__
+or an async framework such as `asynctest <https://asynctest.readthedocs.io/en/latest>`__.
+
+Contributing
+------------
+Contributions are very welcome. Tests can be run with ``tox``, please ensure
+the coverage at least stays the same before you submit a pull request.
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/SOURCES.txt b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/SOURCES.txt
new file mode 100644
index 0000000000..a016c1cae0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/SOURCES.txt
@@ -0,0 +1,51 @@
+CHANGELOG.rst
+LICENSE
+MANIFEST.in
+Makefile
+README.rst
+pyproject.toml
+setup.cfg
+tox.ini
+dependencies/default/constraints.txt
+dependencies/default/requirements.txt
+dependencies/pytest-min/constraints.txt
+dependencies/pytest-min/requirements.txt
+pytest_asyncio/__init__.py
+pytest_asyncio/_version.py
+pytest_asyncio/plugin.py
+pytest_asyncio/py.typed
+pytest_asyncio.egg-info/PKG-INFO
+pytest_asyncio.egg-info/SOURCES.txt
+pytest_asyncio.egg-info/dependency_links.txt
+pytest_asyncio.egg-info/entry_points.txt
+pytest_asyncio.egg-info/requires.txt
+pytest_asyncio.egg-info/top_level.txt
+tests/conftest.py
+tests/test_asyncio_fixture.py
+tests/test_dependent_fixtures.py
+tests/test_event_loop_scope.py
+tests/test_flaky_integration.py
+tests/test_simple.py
+tests/test_subprocess.py
+tests/async_fixtures/__init__.py
+tests/async_fixtures/test_async_fixtures.py
+tests/async_fixtures/test_async_fixtures_scope.py
+tests/async_fixtures/test_async_fixtures_with_finalizer.py
+tests/async_fixtures/test_async_gen_fixtures.py
+tests/async_fixtures/test_nested.py
+tests/async_fixtures/test_parametrized_loop.py
+tests/hypothesis/test_base.py
+tests/hypothesis/test_inherited_test.py
+tests/loop_fixture_scope/conftest.py
+tests/loop_fixture_scope/test_loop_fixture_scope.py
+tests/markers/test_class_marker.py
+tests/markers/test_module_marker.py
+tests/modes/test_auto_mode.py
+tests/modes/test_legacy_mode.py
+tests/modes/test_strict_mode.py
+tests/multiloop/conftest.py
+tests/multiloop/test_alternative_loops.py
+tests/respect_event_loop_policy/conftest.py
+tests/respect_event_loop_policy/test_respects_event_loop_policy.py
+tests/trio/test_fixtures.py
+tools/get-version.py \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/dependency_links.txt b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/dependency_links.txt
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/entry_points.txt b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/entry_points.txt
new file mode 100644
index 0000000000..88db714dad
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/entry_points.txt
@@ -0,0 +1,2 @@
+[pytest11]
+asyncio = pytest_asyncio.plugin
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/requires.txt b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/requires.txt
new file mode 100644
index 0000000000..1e3119384d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/requires.txt
@@ -0,0 +1,11 @@
+pytest>=6.1.0
+
+[:python_version < "3.8"]
+typing-extensions>=3.7.2
+
+[testing]
+coverage>=6.2
+hypothesis>=5.7.1
+flaky>=3.5.0
+mypy>=0.931
+pytest-trio>=0.7.0
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/top_level.txt b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/top_level.txt
new file mode 100644
index 0000000000..08d05d1ecf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio.egg-info/top_level.txt
@@ -0,0 +1 @@
+pytest_asyncio
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/__init__.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/__init__.py
new file mode 100644
index 0000000000..1bc2811d93
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/__init__.py
@@ -0,0 +1,5 @@
+"""The main point for importing pytest-asyncio items."""
+from ._version import version as __version__ # noqa
+from .plugin import fixture
+
+__all__ = ("fixture",)
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/_version.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/_version.py
new file mode 100644
index 0000000000..76aa7a209a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/_version.py
@@ -0,0 +1,5 @@
+# coding: utf-8
+# file generated by setuptools_scm
+# don't change, don't track in version control
+__version__ = version = '0.19.0'
+__version_tuple__ = version_tuple = (0, 19, 0)
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/plugin.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/plugin.py
new file mode 100644
index 0000000000..dd6a782b4d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/plugin.py
@@ -0,0 +1,546 @@
+"""pytest-asyncio implementation."""
+import asyncio
+import contextlib
+import enum
+import functools
+import inspect
+import socket
+import sys
+import warnings
+from typing import (
+ Any,
+ AsyncIterator,
+ Awaitable,
+ Callable,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Set,
+ TypeVar,
+ Union,
+ cast,
+ overload,
+)
+
+import pytest
+
+if sys.version_info >= (3, 8):
+ from typing import Literal
+else:
+ from typing_extensions import Literal
+
+_R = TypeVar("_R")
+
+_ScopeName = Literal["session", "package", "module", "class", "function"]
+_T = TypeVar("_T")
+
+SimpleFixtureFunction = TypeVar(
+ "SimpleFixtureFunction", bound=Callable[..., Awaitable[_R]]
+)
+FactoryFixtureFunction = TypeVar(
+ "FactoryFixtureFunction", bound=Callable[..., AsyncIterator[_R]]
+)
+FixtureFunction = Union[SimpleFixtureFunction, FactoryFixtureFunction]
+FixtureFunctionMarker = Callable[[FixtureFunction], FixtureFunction]
+
+Config = Any # pytest < 7.0
+PytestPluginManager = Any # pytest < 7.0
+FixtureDef = Any # pytest < 7.0
+Parser = Any # pytest < 7.0
+SubRequest = Any # pytest < 7.0
+
+
+class Mode(str, enum.Enum):
+ AUTO = "auto"
+ STRICT = "strict"
+ LEGACY = "legacy"
+
+
+LEGACY_MODE = DeprecationWarning(
+ "The 'asyncio_mode' default value will change to 'strict' in future, "
+ "please explicitly use 'asyncio_mode=strict' or 'asyncio_mode=auto' "
+ "in pytest configuration file."
+)
+
+LEGACY_ASYNCIO_FIXTURE = (
+ "'@pytest.fixture' is applied to {name} "
+ "in 'legacy' mode, "
+ "please replace it with '@pytest_asyncio.fixture' as a preparation "
+ "for switching to 'strict' mode (or use 'auto' mode to seamlessly handle "
+ "all these fixtures as asyncio-driven)."
+)
+
+
+ASYNCIO_MODE_HELP = """\
+'auto' - for automatically handling all async functions by the plugin
+'strict' - for autoprocessing disabling (useful if different async frameworks \
+should be tested together, e.g. \
+both pytest-asyncio and pytest-trio are used in the same project)
+'legacy' - for keeping compatibility with pytest-asyncio<0.17: \
+auto-handling is disabled but pytest_asyncio.fixture usage is not enforced
+"""
+
+
+def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None:
+ group = parser.getgroup("asyncio")
+ group.addoption(
+ "--asyncio-mode",
+ dest="asyncio_mode",
+ default=None,
+ metavar="MODE",
+ help=ASYNCIO_MODE_HELP,
+ )
+ parser.addini(
+ "asyncio_mode",
+ help="default value for --asyncio-mode",
+ default="strict",
+ )
+
+
+@overload
+def fixture(
+ fixture_function: FixtureFunction,
+ *,
+ scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
+ params: Optional[Iterable[object]] = ...,
+ autouse: bool = ...,
+ ids: Optional[
+ Union[
+ Iterable[Union[None, str, float, int, bool]],
+ Callable[[Any], Optional[object]],
+ ]
+ ] = ...,
+ name: Optional[str] = ...,
+) -> FixtureFunction:
+ ...
+
+
+@overload
+def fixture(
+ fixture_function: None = ...,
+ *,
+ scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
+ params: Optional[Iterable[object]] = ...,
+ autouse: bool = ...,
+ ids: Optional[
+ Union[
+ Iterable[Union[None, str, float, int, bool]],
+ Callable[[Any], Optional[object]],
+ ]
+ ] = ...,
+ name: Optional[str] = None,
+) -> FixtureFunctionMarker:
+ ...
+
+
+def fixture(
+ fixture_function: Optional[FixtureFunction] = None, **kwargs: Any
+) -> Union[FixtureFunction, FixtureFunctionMarker]:
+ if fixture_function is not None:
+ _set_explicit_asyncio_mark(fixture_function)
+ return pytest.fixture(fixture_function, **kwargs)
+
+ else:
+
+ @functools.wraps(fixture)
+ def inner(fixture_function: FixtureFunction) -> FixtureFunction:
+ return fixture(fixture_function, **kwargs)
+
+ return inner
+
+
+def _has_explicit_asyncio_mark(obj: Any) -> bool:
+ obj = getattr(obj, "__func__", obj) # instance method maybe?
+ return getattr(obj, "_force_asyncio_fixture", False)
+
+
+def _set_explicit_asyncio_mark(obj: Any) -> None:
+ if hasattr(obj, "__func__"):
+ # instance method, check the function object
+ obj = obj.__func__
+ obj._force_asyncio_fixture = True
+
+
+def _is_coroutine(obj: Any) -> bool:
+ """Check to see if an object is really an asyncio coroutine."""
+ return asyncio.iscoroutinefunction(obj)
+
+
+def _is_coroutine_or_asyncgen(obj: Any) -> bool:
+ return _is_coroutine(obj) or inspect.isasyncgenfunction(obj)
+
+
+def _get_asyncio_mode(config: Config) -> Mode:
+ val = config.getoption("asyncio_mode")
+ if val is None:
+ val = config.getini("asyncio_mode")
+ return Mode(val)
+
+
+def pytest_configure(config: Config) -> None:
+ """Inject documentation."""
+ config.addinivalue_line(
+ "markers",
+ "asyncio: "
+ "mark the test as a coroutine, it will be "
+ "run using an asyncio event loop",
+ )
+ if _get_asyncio_mode(config) == Mode.LEGACY:
+ config.issue_config_time_warning(LEGACY_MODE, stacklevel=2)
+
+
+@pytest.mark.tryfirst
+def pytest_report_header(config: Config) -> List[str]:
+ """Add asyncio config to pytest header."""
+ mode = _get_asyncio_mode(config)
+ return [f"asyncio: mode={mode}"]
+
+
+def _preprocess_async_fixtures(config: Config, holder: Set[FixtureDef]) -> None:
+ asyncio_mode = _get_asyncio_mode(config)
+ fixturemanager = config.pluginmanager.get_plugin("funcmanage")
+ for fixtures in fixturemanager._arg2fixturedefs.values():
+ for fixturedef in fixtures:
+ if fixturedef is holder:
+ continue
+ func = fixturedef.func
+ if not _is_coroutine_or_asyncgen(func):
+ # Nothing to do with a regular fixture function
+ continue
+ if not _has_explicit_asyncio_mark(func):
+ if asyncio_mode == Mode.STRICT:
+ # Ignore async fixtures without explicit asyncio mark in strict mode
+ # This applies to pytest_trio fixtures, for example
+ continue
+ elif asyncio_mode == Mode.AUTO:
+ # Enforce asyncio mode if 'auto'
+ _set_explicit_asyncio_mark(func)
+ elif asyncio_mode == Mode.LEGACY:
+ _set_explicit_asyncio_mark(func)
+ try:
+ code = func.__code__
+ except AttributeError:
+ code = func.__func__.__code__
+ name = (
+ f"<fixture {func.__qualname__}, file={code.co_filename}, "
+ f"line={code.co_firstlineno}>"
+ )
+ warnings.warn(
+ LEGACY_ASYNCIO_FIXTURE.format(name=name),
+ DeprecationWarning,
+ )
+
+ to_add = []
+ for name in ("request", "event_loop"):
+ if name not in fixturedef.argnames:
+ to_add.append(name)
+
+ if to_add:
+ fixturedef.argnames += tuple(to_add)
+
+ if inspect.isasyncgenfunction(func):
+ fixturedef.func = _wrap_asyncgen(func)
+ elif inspect.iscoroutinefunction(func):
+ fixturedef.func = _wrap_async(func)
+
+ assert _has_explicit_asyncio_mark(fixturedef.func)
+ holder.add(fixturedef)
+
+
+def _add_kwargs(
+ func: Callable[..., Any],
+ kwargs: Dict[str, Any],
+ event_loop: asyncio.AbstractEventLoop,
+ request: SubRequest,
+) -> Dict[str, Any]:
+ sig = inspect.signature(func)
+ ret = kwargs.copy()
+ if "request" in sig.parameters:
+ ret["request"] = request
+ if "event_loop" in sig.parameters:
+ ret["event_loop"] = event_loop
+ return ret
+
+
+def _wrap_asyncgen(func: Callable[..., AsyncIterator[_R]]) -> Callable[..., _R]:
+ @functools.wraps(func)
+ def _asyncgen_fixture_wrapper(
+ event_loop: asyncio.AbstractEventLoop, request: SubRequest, **kwargs: Any
+ ) -> _R:
+ gen_obj = func(**_add_kwargs(func, kwargs, event_loop, request))
+
+ async def setup() -> _R:
+ res = await gen_obj.__anext__()
+ return res
+
+ def finalizer() -> None:
+ """Yield again, to finalize."""
+
+ async def async_finalizer() -> None:
+ try:
+ await gen_obj.__anext__()
+ except StopAsyncIteration:
+ pass
+ else:
+ msg = "Async generator fixture didn't stop."
+ msg += "Yield only once."
+ raise ValueError(msg)
+
+ event_loop.run_until_complete(async_finalizer())
+
+ result = event_loop.run_until_complete(setup())
+ request.addfinalizer(finalizer)
+ return result
+
+ return _asyncgen_fixture_wrapper
+
+
+def _wrap_async(func: Callable[..., Awaitable[_R]]) -> Callable[..., _R]:
+ @functools.wraps(func)
+ def _async_fixture_wrapper(
+ event_loop: asyncio.AbstractEventLoop, request: SubRequest, **kwargs: Any
+ ) -> _R:
+ async def setup() -> _R:
+ res = await func(**_add_kwargs(func, kwargs, event_loop, request))
+ return res
+
+ return event_loop.run_until_complete(setup())
+
+ return _async_fixture_wrapper
+
+
+_HOLDER: Set[FixtureDef] = set()
+
+
+@pytest.mark.tryfirst
+def pytest_pycollect_makeitem(
+ collector: Union[pytest.Module, pytest.Class], name: str, obj: object
+) -> Union[
+ None, pytest.Item, pytest.Collector, List[Union[pytest.Item, pytest.Collector]]
+]:
+ """A pytest hook to collect asyncio coroutines."""
+ if not collector.funcnamefilter(name):
+ return None
+ _preprocess_async_fixtures(collector.config, _HOLDER)
+ if isinstance(obj, staticmethod):
+ # staticmethods need to be unwrapped.
+ obj = obj.__func__
+ if (
+ _is_coroutine(obj)
+ or _is_hypothesis_test(obj)
+ and _hypothesis_test_wraps_coroutine(obj)
+ ):
+ item = pytest.Function.from_parent(collector, name=name)
+ marker = item.get_closest_marker("asyncio")
+ if marker is not None:
+ return list(collector._genfunctions(name, obj))
+ else:
+ if _get_asyncio_mode(item.config) == Mode.AUTO:
+ # implicitly add asyncio marker if asyncio mode is on
+ ret = list(collector._genfunctions(name, obj))
+ for elem in ret:
+ elem.add_marker("asyncio")
+ return ret # type: ignore[return-value]
+ return None
+
+
+def _hypothesis_test_wraps_coroutine(function: Any) -> bool:
+ return _is_coroutine(function.hypothesis.inner_test)
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_fixture_post_finalizer(fixturedef: FixtureDef, request: SubRequest) -> None:
+ """Called after fixture teardown"""
+ if fixturedef.argname == "event_loop":
+ policy = asyncio.get_event_loop_policy()
+ try:
+ loop = policy.get_event_loop()
+ except RuntimeError:
+ loop = None
+ if loop is not None:
+ # Clean up existing loop to avoid ResourceWarnings
+ loop.close()
+ new_loop = policy.new_event_loop() # Replace existing event loop
+ # Ensure subsequent calls to get_event_loop() succeed
+ policy.set_event_loop(new_loop)
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_fixture_setup(
+ fixturedef: FixtureDef, request: SubRequest
+) -> Optional[object]:
+ """Adjust the event loop policy when an event loop is produced."""
+ if fixturedef.argname == "event_loop":
+ outcome = yield
+ loop = outcome.get_result()
+ policy = asyncio.get_event_loop_policy()
+ try:
+ old_loop = policy.get_event_loop()
+ if old_loop is not loop:
+ old_loop.close()
+ except RuntimeError:
+ # Swallow this, since it's probably bad event loop hygiene.
+ pass
+ policy.set_event_loop(loop)
+ return
+
+ yield
+
+
+@pytest.hookimpl(tryfirst=True, hookwrapper=True)
+def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]:
+ """
+ Pytest hook called before a test case is run.
+
+ Wraps marked tests in a synchronous function
+ where the wrapped test coroutine is executed in an event loop.
+ """
+ marker = pyfuncitem.get_closest_marker("asyncio")
+ if marker is not None:
+ funcargs: Dict[str, object] = pyfuncitem.funcargs # type: ignore[name-defined]
+ loop = cast(asyncio.AbstractEventLoop, funcargs["event_loop"])
+ if _is_hypothesis_test(pyfuncitem.obj):
+ pyfuncitem.obj.hypothesis.inner_test = wrap_in_sync(
+ pyfuncitem,
+ pyfuncitem.obj.hypothesis.inner_test,
+ _loop=loop,
+ )
+ else:
+ pyfuncitem.obj = wrap_in_sync(
+ pyfuncitem,
+ pyfuncitem.obj,
+ _loop=loop,
+ )
+ yield
+
+
+def _is_hypothesis_test(function: Any) -> bool:
+ return getattr(function, "is_hypothesis_test", False)
+
+
+def wrap_in_sync(
+ pyfuncitem: pytest.Function,
+ func: Callable[..., Awaitable[Any]],
+ _loop: asyncio.AbstractEventLoop,
+):
+ """Return a sync wrapper around an async function executing it in the
+ current event loop."""
+
+ # if the function is already wrapped, we rewrap using the original one
+ # not using __wrapped__ because the original function may already be
+ # a wrapped one
+ raw_func = getattr(func, "_raw_test_func", None)
+ if raw_func is not None:
+ func = raw_func
+
+ @functools.wraps(func)
+ def inner(*args, **kwargs):
+ coro = func(*args, **kwargs)
+ if not inspect.isawaitable(coro):
+ pyfuncitem.warn(
+ pytest.PytestWarning(
+ f"The test {pyfuncitem} is marked with '@pytest.mark.asyncio' "
+ "but it is not an async function. "
+ "Please remove asyncio marker. "
+ "If the test is not marked explicitly, "
+ "check for global markers applied via 'pytestmark'."
+ )
+ )
+ return
+ task = asyncio.ensure_future(coro, loop=_loop)
+ try:
+ _loop.run_until_complete(task)
+ except BaseException:
+ # run_until_complete doesn't get the result from exceptions
+ # that are not subclasses of `Exception`. Consume all
+ # exceptions to prevent asyncio's warning from logging.
+ if task.done() and not task.cancelled():
+ task.exception()
+ raise
+
+ inner._raw_test_func = func # type: ignore[attr-defined]
+ return inner
+
+
+def pytest_runtest_setup(item: pytest.Item) -> None:
+ marker = item.get_closest_marker("asyncio")
+ if marker is None:
+ return
+ fixturenames = item.fixturenames # type: ignore[attr-defined]
+ # inject an event loop fixture for all async tests
+ if "event_loop" in fixturenames:
+ fixturenames.remove("event_loop")
+ fixturenames.insert(0, "event_loop")
+ obj = getattr(item, "obj", None)
+ if not getattr(obj, "hypothesis", False) and getattr(
+ obj, "is_hypothesis_test", False
+ ):
+ pytest.fail(
+ "test function `%r` is using Hypothesis, but pytest-asyncio "
+ "only works with Hypothesis 3.64.0 or later." % item
+ )
+
+
+@pytest.fixture
+def event_loop(request: "pytest.FixtureRequest") -> Iterator[asyncio.AbstractEventLoop]:
+ """Create an instance of the default event loop for each test case."""
+ loop = asyncio.get_event_loop_policy().new_event_loop()
+ yield loop
+ loop.close()
+
+
+def _unused_port(socket_type: int) -> int:
+ """Find an unused localhost port from 1024-65535 and return it."""
+ with contextlib.closing(socket.socket(type=socket_type)) as sock:
+ sock.bind(("127.0.0.1", 0))
+ return sock.getsockname()[1]
+
+
+@pytest.fixture
+def unused_tcp_port() -> int:
+ return _unused_port(socket.SOCK_STREAM)
+
+
+@pytest.fixture
+def unused_udp_port() -> int:
+ return _unused_port(socket.SOCK_DGRAM)
+
+
+@pytest.fixture(scope="session")
+def unused_tcp_port_factory() -> Callable[[], int]:
+ """A factory function, producing different unused TCP ports."""
+ produced = set()
+
+ def factory():
+ """Return an unused port."""
+ port = _unused_port(socket.SOCK_STREAM)
+
+ while port in produced:
+ port = _unused_port(socket.SOCK_STREAM)
+
+ produced.add(port)
+
+ return port
+
+ return factory
+
+
+@pytest.fixture(scope="session")
+def unused_udp_port_factory() -> Callable[[], int]:
+ """A factory function, producing different unused UDP ports."""
+ produced = set()
+
+ def factory():
+ """Return an unused port."""
+ port = _unused_port(socket.SOCK_DGRAM)
+
+ while port in produced:
+ port = _unused_port(socket.SOCK_DGRAM)
+
+ produced.add(port)
+
+ return port
+
+ return factory
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/py.typed b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/py.typed
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/pytest_asyncio/py.typed
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/setup.cfg b/testing/web-platform/tests/tools/third_party/pytest-asyncio/setup.cfg
new file mode 100644
index 0000000000..85e3fdb323
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/setup.cfg
@@ -0,0 +1,73 @@
+[metadata]
+name = pytest-asyncio
+version = attr: pytest_asyncio.__version__
+url = https://github.com/pytest-dev/pytest-asyncio
+project_urls =
+ GitHub = https://github.com/pytest-dev/pytest-asyncio
+description = Pytest support for asyncio
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+author = Tin Tvrtković <tinchester@gmail.com>
+author_email = tinchester@gmail.com
+license = Apache 2.0
+license_file = LICENSE
+classifiers =
+ Development Status :: 4 - Beta
+
+ Intended Audience :: Developers
+
+ License :: OSI Approved :: Apache Software 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
+
+ Topic :: Software Development :: Testing
+
+ Framework :: AsyncIO
+ Framework :: Pytest
+ Typing :: Typed
+
+[options]
+python_requires = >=3.7
+packages = find:
+include_package_data = True
+install_requires =
+ pytest >= 6.1.0
+ typing-extensions >= 3.7.2; python_version < "3.8"
+
+[options.extras_require]
+testing =
+ coverage >= 6.2
+ hypothesis >= 5.7.1
+ flaky >= 3.5.0
+ mypy >= 0.931
+ pytest-trio >= 0.7.0
+
+[options.entry_points]
+pytest11 =
+ asyncio = pytest_asyncio.plugin
+
+[coverage:run]
+source = pytest_asyncio
+branch = true
+
+[coverage:report]
+show_missing = true
+
+[tool:pytest]
+addopts = -rsx --tb=short
+testpaths = tests
+asyncio_mode = auto
+junit_family = xunit2
+filterwarnings = error
+
+[flake8]
+max-line-length = 88
+
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/__init__.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_fixtures.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_fixtures.py
new file mode 100644
index 0000000000..7ddd04ab86
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_fixtures.py
@@ -0,0 +1,25 @@
+import asyncio
+import unittest.mock
+
+import pytest
+
+START = object()
+END = object()
+RETVAL = object()
+
+
+@pytest.fixture
+def mock():
+ return unittest.mock.Mock(return_value=RETVAL)
+
+
+@pytest.fixture
+async def async_fixture(mock):
+ return await asyncio.sleep(0.1, result=mock(START))
+
+
+@pytest.mark.asyncio
+async def test_async_fixture(async_fixture, mock):
+ assert mock.call_count == 1
+ assert mock.call_args_list[-1] == unittest.mock.call(START)
+ assert async_fixture is RETVAL
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_fixtures_scope.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_fixtures_scope.py
new file mode 100644
index 0000000000..b150f8a8e5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_fixtures_scope.py
@@ -0,0 +1,25 @@
+"""
+We support module-scoped async fixtures, but only if the event loop is
+module-scoped too.
+"""
+import asyncio
+
+import pytest
+
+
+@pytest.fixture(scope="module")
+def event_loop():
+ """A module-scoped event loop."""
+ return asyncio.new_event_loop()
+
+
+@pytest.fixture(scope="module")
+async def async_fixture():
+ await asyncio.sleep(0.1)
+ return 1
+
+
+@pytest.mark.asyncio
+async def test_async_fixture_scope(async_fixture):
+ assert async_fixture == 1
+ await asyncio.sleep(0.1)
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_fixtures_with_finalizer.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_fixtures_with_finalizer.py
new file mode 100644
index 0000000000..2e72d5de04
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_fixtures_with_finalizer.py
@@ -0,0 +1,59 @@
+import asyncio
+import functools
+
+import pytest
+
+
+@pytest.mark.asyncio
+async def test_module_with_event_loop_finalizer(port_with_event_loop_finalizer):
+ await asyncio.sleep(0.01)
+ assert port_with_event_loop_finalizer
+
+
+@pytest.mark.asyncio
+async def test_module_with_get_event_loop_finalizer(port_with_get_event_loop_finalizer):
+ await asyncio.sleep(0.01)
+ assert port_with_get_event_loop_finalizer
+
+
+@pytest.fixture(scope="module")
+def event_loop():
+ """Change event_loop fixture to module level."""
+ policy = asyncio.get_event_loop_policy()
+ loop = policy.new_event_loop()
+ yield loop
+ loop.close()
+
+
+@pytest.fixture(scope="module")
+async def port_with_event_loop_finalizer(request, event_loop):
+ def port_finalizer(finalizer):
+ async def port_afinalizer():
+ # await task using loop provided by event_loop fixture
+ # RuntimeError is raised if task is created on a different loop
+ await finalizer
+
+ event_loop.run_until_complete(port_afinalizer())
+
+ worker = asyncio.ensure_future(asyncio.sleep(0.2))
+ request.addfinalizer(functools.partial(port_finalizer, worker))
+ return True
+
+
+@pytest.fixture(scope="module")
+async def port_with_get_event_loop_finalizer(request, event_loop):
+ def port_finalizer(finalizer):
+ async def port_afinalizer():
+ # await task using current loop retrieved from the event loop policy
+ # RuntimeError is raised if task is created on a different loop.
+ # This can happen when pytest_fixture_setup
+ # does not set up the loop correctly,
+ # for example when policy.set_event_loop() is called with a wrong argument
+ await finalizer
+
+ current_loop = asyncio.get_event_loop_policy().get_event_loop()
+ current_loop.run_until_complete(port_afinalizer())
+
+ worker = asyncio.ensure_future(asyncio.sleep(0.2))
+ request.addfinalizer(functools.partial(port_finalizer, worker))
+ return True
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_gen_fixtures.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_gen_fixtures.py
new file mode 100644
index 0000000000..0bea745868
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_async_gen_fixtures.py
@@ -0,0 +1,38 @@
+import unittest.mock
+
+import pytest
+
+START = object()
+END = object()
+RETVAL = object()
+
+
+@pytest.fixture(scope="module")
+def mock():
+ return unittest.mock.Mock(return_value=RETVAL)
+
+
+@pytest.fixture
+async def async_gen_fixture(mock):
+ try:
+ yield mock(START)
+ except Exception as e:
+ mock(e)
+ else:
+ mock(END)
+
+
+@pytest.mark.asyncio
+async def test_async_gen_fixture(async_gen_fixture, mock):
+ assert mock.called
+ assert mock.call_args_list[-1] == unittest.mock.call(START)
+ assert async_gen_fixture is RETVAL
+
+
+@pytest.mark.asyncio
+async def test_async_gen_fixture_finalized(mock):
+ try:
+ assert mock.called
+ assert mock.call_args_list[-1] == unittest.mock.call(END)
+ finally:
+ mock.reset_mock()
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_nested.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_nested.py
new file mode 100644
index 0000000000..e81e782452
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_nested.py
@@ -0,0 +1,26 @@
+import asyncio
+
+import pytest
+
+
+@pytest.fixture()
+async def async_inner_fixture():
+ await asyncio.sleep(0.01)
+ print("inner start")
+ yield True
+ print("inner stop")
+
+
+@pytest.fixture()
+async def async_fixture_outer(async_inner_fixture, event_loop):
+ await asyncio.sleep(0.01)
+ print("outer start")
+ assert async_inner_fixture is True
+ yield True
+ print("outer stop")
+
+
+@pytest.mark.asyncio
+async def test_async_fixture(async_fixture_outer):
+ assert async_fixture_outer is True
+ print("test_async_fixture")
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_parametrized_loop.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_parametrized_loop.py
new file mode 100644
index 0000000000..2fb8befa7f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/async_fixtures/test_parametrized_loop.py
@@ -0,0 +1,31 @@
+import asyncio
+
+import pytest
+
+TESTS_COUNT = 0
+
+
+def teardown_module():
+ # parametrized 2 * 2 times: 2 for 'event_loop' and 2 for 'fix'
+ assert TESTS_COUNT == 4
+
+
+@pytest.fixture(scope="module", params=[1, 2])
+def event_loop(request):
+ request.param
+ loop = asyncio.new_event_loop()
+ yield loop
+ loop.close()
+
+
+@pytest.fixture(params=["a", "b"])
+async def fix(request):
+ await asyncio.sleep(0)
+ return request.param
+
+
+@pytest.mark.asyncio
+async def test_parametrized_loop(fix):
+ await asyncio.sleep(0)
+ global TESTS_COUNT
+ TESTS_COUNT += 1
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/conftest.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/conftest.py
new file mode 100644
index 0000000000..4aa8c89aa7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/conftest.py
@@ -0,0 +1,32 @@
+import asyncio
+
+import pytest
+
+pytest_plugins = "pytester"
+
+
+@pytest.fixture
+def dependent_fixture(event_loop):
+ """A fixture dependent on the event_loop fixture, doing some cleanup."""
+ counter = 0
+
+ async def just_a_sleep():
+ """Just sleep a little while."""
+ nonlocal event_loop
+ await asyncio.sleep(0.1)
+ nonlocal counter
+ counter += 1
+
+ event_loop.run_until_complete(just_a_sleep())
+ yield
+ event_loop.run_until_complete(just_a_sleep())
+
+ assert counter == 2
+
+
+@pytest.fixture(scope="session", name="factory_involving_factories")
+def factory_involving_factories_fixture(unused_tcp_port_factory):
+ def factory():
+ return unused_tcp_port_factory()
+
+ return factory
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/hypothesis/test_base.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/hypothesis/test_base.py
new file mode 100644
index 0000000000..e6da342732
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/hypothesis/test_base.py
@@ -0,0 +1,88 @@
+"""Tests for the Hypothesis integration, which wraps async functions in a
+sync shim for Hypothesis.
+"""
+import asyncio
+from textwrap import dedent
+
+import pytest
+from hypothesis import given, strategies as st
+
+
+@pytest.fixture(scope="module")
+def event_loop():
+ loop = asyncio.get_event_loop_policy().new_event_loop()
+ yield loop
+ loop.close()
+
+
+@given(st.integers())
+@pytest.mark.asyncio
+async def test_mark_inner(n):
+ assert isinstance(n, int)
+
+
+@pytest.mark.asyncio
+@given(st.integers())
+async def test_mark_outer(n):
+ assert isinstance(n, int)
+
+
+@pytest.mark.parametrize("y", [1, 2])
+@given(x=st.none())
+@pytest.mark.asyncio
+async def test_mark_and_parametrize(x, y):
+ assert x is None
+ assert y in (1, 2)
+
+
+@given(st.integers())
+@pytest.mark.asyncio
+async def test_can_use_fixture_provided_event_loop(event_loop, n):
+ semaphore = asyncio.Semaphore(value=0)
+ event_loop.call_soon(semaphore.release)
+ await semaphore.acquire()
+
+
+def test_async_auto_marked(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+ from hypothesis import given
+ import hypothesis.strategies as st
+
+ pytest_plugins = 'pytest_asyncio'
+
+ @given(n=st.integers())
+ async def test_hypothesis(n: int):
+ assert isinstance(n, int)
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(passed=1)
+
+
+def test_sync_not_auto_marked(testdir):
+ """Assert that synchronous Hypothesis functions are not marked with asyncio"""
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+ from hypothesis import given
+ import hypothesis.strategies as st
+
+ pytest_plugins = 'pytest_asyncio'
+
+ @given(n=st.integers())
+ def test_hypothesis(request, n: int):
+ markers = [marker.name for marker in request.node.own_markers]
+ assert "asyncio" not in markers
+ assert isinstance(n, int)
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(passed=1)
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/hypothesis/test_inherited_test.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/hypothesis/test_inherited_test.py
new file mode 100644
index 0000000000..a77622648f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/hypothesis/test_inherited_test.py
@@ -0,0 +1,20 @@
+import hypothesis.strategies as st
+import pytest
+from hypothesis import given
+
+
+class BaseClass:
+ @pytest.mark.asyncio
+ @given(value=st.integers())
+ async def test_hypothesis(self, value: int) -> None:
+ pass
+
+
+class TestOne(BaseClass):
+ """During the first execution the Hypothesis test
+ is wrapped in a synchronous function."""
+
+
+class TestTwo(BaseClass):
+ """Execute the test a second time to ensure that
+ the test receives a fresh event loop."""
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/loop_fixture_scope/conftest.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/loop_fixture_scope/conftest.py
new file mode 100644
index 0000000000..223160c2b6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/loop_fixture_scope/conftest.py
@@ -0,0 +1,17 @@
+import asyncio
+
+import pytest
+
+
+class CustomSelectorLoop(asyncio.SelectorEventLoop):
+ """A subclass with no overrides, just to test for presence."""
+
+
+loop = CustomSelectorLoop()
+
+
+@pytest.fixture(scope="module")
+def event_loop():
+ """Create an instance of the default event loop for each test case."""
+ yield loop
+ loop.close()
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/loop_fixture_scope/test_loop_fixture_scope.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/loop_fixture_scope/test_loop_fixture_scope.py
new file mode 100644
index 0000000000..679ab48f09
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/loop_fixture_scope/test_loop_fixture_scope.py
@@ -0,0 +1,16 @@
+"""Unit tests for overriding the event loop with a larger scoped one."""
+import asyncio
+
+import pytest
+
+
+@pytest.mark.asyncio
+async def test_for_custom_loop():
+ """This test should be executed using the custom loop."""
+ await asyncio.sleep(0.01)
+ assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop"
+
+
+@pytest.mark.asyncio
+async def test_dependent_fixture(dependent_fixture):
+ await asyncio.sleep(0.1)
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/markers/test_class_marker.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/markers/test_class_marker.py
new file mode 100644
index 0000000000..d46c3af74f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/markers/test_class_marker.py
@@ -0,0 +1,25 @@
+"""Test if pytestmark works when defined on a class."""
+import asyncio
+
+import pytest
+
+
+class TestPyTestMark:
+ pytestmark = pytest.mark.asyncio
+
+ async def test_is_asyncio(self, event_loop, sample_fixture):
+ assert asyncio.get_event_loop()
+ counter = 1
+
+ async def inc():
+ nonlocal counter
+ counter += 1
+ await asyncio.sleep(0)
+
+ await asyncio.ensure_future(inc())
+ assert counter == 2
+
+
+@pytest.fixture
+def sample_fixture():
+ return None
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/markers/test_module_marker.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/markers/test_module_marker.py
new file mode 100644
index 0000000000..2f69dbc933
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/markers/test_module_marker.py
@@ -0,0 +1,39 @@
+"""Test if pytestmark works when defined in a module."""
+import asyncio
+
+import pytest
+
+pytestmark = pytest.mark.asyncio
+
+
+class TestPyTestMark:
+ async def test_is_asyncio(self, event_loop, sample_fixture):
+ assert asyncio.get_event_loop()
+
+ counter = 1
+
+ async def inc():
+ nonlocal counter
+ counter += 1
+ await asyncio.sleep(0)
+
+ await asyncio.ensure_future(inc())
+ assert counter == 2
+
+
+async def test_is_asyncio(event_loop, sample_fixture):
+ assert asyncio.get_event_loop()
+ counter = 1
+
+ async def inc():
+ nonlocal counter
+ counter += 1
+ await asyncio.sleep(0)
+
+ await asyncio.ensure_future(inc())
+ assert counter == 2
+
+
+@pytest.fixture
+def sample_fixture():
+ return None
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/modes/test_auto_mode.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/modes/test_auto_mode.py
new file mode 100644
index 0000000000..fc4d2df076
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/modes/test_auto_mode.py
@@ -0,0 +1,139 @@
+from textwrap import dedent
+
+
+def test_auto_mode_cmdline(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = 'pytest_asyncio'
+
+ async def test_a():
+ await asyncio.sleep(0)
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(passed=1)
+
+
+def test_auto_mode_cfg(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = 'pytest_asyncio'
+
+ async def test_a():
+ await asyncio.sleep(0)
+ """
+ )
+ )
+ testdir.makefile(".ini", pytest="[pytest]\nasyncio_mode = auto\n")
+ result = testdir.runpytest()
+ result.assert_outcomes(passed=1)
+
+
+def test_auto_mode_async_fixture(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = 'pytest_asyncio'
+
+ @pytest.fixture
+ async def fixture_a():
+ await asyncio.sleep(0)
+ return 1
+
+ async def test_a(fixture_a):
+ await asyncio.sleep(0)
+ assert fixture_a == 1
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(passed=1)
+
+
+def test_auto_mode_method_fixture(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = 'pytest_asyncio'
+
+
+ class TestA:
+
+ @pytest.fixture
+ async def fixture_a(self):
+ await asyncio.sleep(0)
+ return 1
+
+ async def test_a(self, fixture_a):
+ await asyncio.sleep(0)
+ assert fixture_a == 1
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(passed=1)
+
+
+def test_auto_mode_static_method(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+
+ pytest_plugins = 'pytest_asyncio'
+
+
+ class TestA:
+
+ @staticmethod
+ async def test_a():
+ await asyncio.sleep(0)
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(passed=1)
+
+
+def test_auto_mode_static_method_fixture(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = 'pytest_asyncio'
+
+
+ class TestA:
+
+ @staticmethod
+ @pytest.fixture
+ async def fixture_a():
+ await asyncio.sleep(0)
+ return 1
+
+ @staticmethod
+ async def test_a(fixture_a):
+ await asyncio.sleep(0)
+ assert fixture_a == 1
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(passed=1)
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/modes/test_legacy_mode.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/modes/test_legacy_mode.py
new file mode 100644
index 0000000000..12d4afe18d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/modes/test_legacy_mode.py
@@ -0,0 +1,112 @@
+from textwrap import dedent
+
+LEGACY_MODE = (
+ "The 'asyncio_mode' default value will change to 'strict' in future, "
+ "please explicitly use 'asyncio_mode=strict' or 'asyncio_mode=auto' "
+ "in pytest configuration file."
+)
+
+LEGACY_ASYNCIO_FIXTURE = (
+ "'@pytest.fixture' is applied to {name} "
+ "in 'legacy' mode, "
+ "please replace it with '@pytest_asyncio.fixture' as a preparation "
+ "for switching to 'strict' mode (or use 'auto' mode to seamlessly handle "
+ "all these fixtures as asyncio-driven)."
+).format(name="*")
+
+
+def test_warning_for_legacy_mode_cmdline(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = 'pytest_asyncio'
+
+ @pytest.mark.asyncio
+ async def test_a():
+ await asyncio.sleep(0)
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=legacy")
+ assert result.parseoutcomes()["warnings"] == 1
+ result.stdout.fnmatch_lines(["*" + LEGACY_MODE + "*"])
+
+
+def test_warning_for_legacy_mode_cfg(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = 'pytest_asyncio'
+
+ @pytest.mark.asyncio
+ async def test_a():
+ await asyncio.sleep(0)
+ """
+ )
+ )
+ testdir.makefile(".ini", pytest="[pytest]\nasyncio_mode = legacy\n")
+ result = testdir.runpytest()
+ assert result.parseoutcomes()["warnings"] == 1
+ result.stdout.fnmatch_lines(["*" + LEGACY_MODE + "*"])
+ result.stdout.no_fnmatch_line("*" + LEGACY_ASYNCIO_FIXTURE + "*")
+
+
+def test_warning_for_legacy_fixture(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = 'pytest_asyncio'
+
+ @pytest.fixture
+ async def fixture_a():
+ await asyncio.sleep(0)
+ return 1
+
+ @pytest.mark.asyncio
+ async def test_a(fixture_a):
+ await asyncio.sleep(0)
+ assert fixture_a == 1
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=legacy")
+ assert result.parseoutcomes()["warnings"] == 2
+ result.stdout.fnmatch_lines(["*" + LEGACY_ASYNCIO_FIXTURE + "*"])
+
+
+def test_warning_for_legacy_method_fixture(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = 'pytest_asyncio'
+
+
+ class TestA:
+
+ @pytest.fixture
+ async def fixture_a(self):
+ await asyncio.sleep(0)
+ return 1
+
+ @pytest.mark.asyncio
+ async def test_a(self, fixture_a):
+ await asyncio.sleep(0)
+ assert fixture_a == 1
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=legacy")
+ assert result.parseoutcomes()["warnings"] == 2
+ result.stdout.fnmatch_lines(["*" + LEGACY_ASYNCIO_FIXTURE + "*"])
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/modes/test_strict_mode.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/modes/test_strict_mode.py
new file mode 100644
index 0000000000..3b6487c72b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/modes/test_strict_mode.py
@@ -0,0 +1,68 @@
+from textwrap import dedent
+
+
+def test_strict_mode_cmdline(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = 'pytest_asyncio'
+
+ @pytest.mark.asyncio
+ async def test_a():
+ await asyncio.sleep(0)
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+
+
+def test_strict_mode_cfg(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+
+ pytest_plugins = 'pytest_asyncio'
+
+ @pytest.mark.asyncio
+ async def test_a():
+ await asyncio.sleep(0)
+ """
+ )
+ )
+ testdir.makefile(".ini", pytest="[pytest]\nasyncio_mode = strict\n")
+ result = testdir.runpytest()
+ result.assert_outcomes(passed=1)
+
+
+def test_strict_mode_method_fixture(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import pytest
+ import pytest_asyncio
+
+ pytest_plugins = 'pytest_asyncio'
+
+ class TestA:
+
+ @pytest_asyncio.fixture
+ async def fixture_a(self):
+ await asyncio.sleep(0)
+ return 1
+
+ @pytest.mark.asyncio
+ async def test_a(self, fixture_a):
+ await asyncio.sleep(0)
+ assert fixture_a == 1
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=auto")
+ result.assert_outcomes(passed=1)
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/multiloop/conftest.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/multiloop/conftest.py
new file mode 100644
index 0000000000..ebcb627a6d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/multiloop/conftest.py
@@ -0,0 +1,15 @@
+import asyncio
+
+import pytest
+
+
+class CustomSelectorLoop(asyncio.SelectorEventLoop):
+ """A subclass with no overrides, just to test for presence."""
+
+
+@pytest.fixture
+def event_loop():
+ """Create an instance of the default event loop for each test case."""
+ loop = CustomSelectorLoop()
+ yield loop
+ loop.close()
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/multiloop/test_alternative_loops.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/multiloop/test_alternative_loops.py
new file mode 100644
index 0000000000..5f66c96795
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/multiloop/test_alternative_loops.py
@@ -0,0 +1,16 @@
+"""Unit tests for overriding the event loop."""
+import asyncio
+
+import pytest
+
+
+@pytest.mark.asyncio
+async def test_for_custom_loop():
+ """This test should be executed using the custom loop."""
+ await asyncio.sleep(0.01)
+ assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop"
+
+
+@pytest.mark.asyncio
+async def test_dependent_fixture(dependent_fixture):
+ await asyncio.sleep(0.1)
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/respect_event_loop_policy/conftest.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/respect_event_loop_policy/conftest.py
new file mode 100644
index 0000000000..2c5cef24ff
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/respect_event_loop_policy/conftest.py
@@ -0,0 +1,16 @@
+"""Defines and sets a custom event loop policy"""
+import asyncio
+from asyncio import DefaultEventLoopPolicy, SelectorEventLoop
+
+
+class TestEventLoop(SelectorEventLoop):
+ pass
+
+
+class TestEventLoopPolicy(DefaultEventLoopPolicy):
+ def new_event_loop(self):
+ return TestEventLoop()
+
+
+# This statement represents a code which sets a custom event loop policy
+asyncio.set_event_loop_policy(TestEventLoopPolicy())
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/respect_event_loop_policy/test_respects_event_loop_policy.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/respect_event_loop_policy/test_respects_event_loop_policy.py
new file mode 100644
index 0000000000..610b33889e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/respect_event_loop_policy/test_respects_event_loop_policy.py
@@ -0,0 +1,17 @@
+"""Tests that any externally provided event loop policy remains unaltered."""
+import asyncio
+
+import pytest
+
+
+@pytest.mark.asyncio
+async def test_uses_loop_provided_by_custom_policy():
+ """Asserts that test cases use the event loop
+ provided by the custom event loop policy"""
+ assert type(asyncio.get_event_loop()).__name__ == "TestEventLoop"
+
+
+@pytest.mark.asyncio
+async def test_custom_policy_is_not_overwritten():
+ """Asserts that any custom event loop policy stays the same across test cases"""
+ assert type(asyncio.get_event_loop()).__name__ == "TestEventLoop"
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_asyncio_fixture.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_asyncio_fixture.py
new file mode 100644
index 0000000000..3a28cebb63
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_asyncio_fixture.py
@@ -0,0 +1,64 @@
+import asyncio
+from textwrap import dedent
+
+import pytest
+
+import pytest_asyncio
+
+
+@pytest_asyncio.fixture
+async def fixture_bare():
+ await asyncio.sleep(0)
+ return 1
+
+
+@pytest.mark.asyncio
+async def test_bare_fixture(fixture_bare):
+ await asyncio.sleep(0)
+ assert fixture_bare == 1
+
+
+@pytest_asyncio.fixture(name="new_fixture_name")
+async def fixture_with_name(request):
+ await asyncio.sleep(0)
+ return request.fixturename
+
+
+@pytest.mark.asyncio
+async def test_fixture_with_name(new_fixture_name):
+ await asyncio.sleep(0)
+ assert new_fixture_name == "new_fixture_name"
+
+
+@pytest_asyncio.fixture(params=[2, 4])
+async def fixture_with_params(request):
+ await asyncio.sleep(0)
+ return request.param
+
+
+@pytest.mark.asyncio
+async def test_fixture_with_params(fixture_with_params):
+ await asyncio.sleep(0)
+ assert fixture_with_params % 2 == 0
+
+
+@pytest.mark.parametrize("mode", ("auto", "strict", "legacy"))
+def test_sync_function_uses_async_fixture(testdir, mode):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import pytest_asyncio
+
+ pytest_plugins = 'pytest_asyncio'
+
+ @pytest_asyncio.fixture
+ async def always_true():
+ return True
+
+ def test_sync_function_uses_async_fixture(always_true):
+ assert always_true is True
+ """
+ )
+ )
+ result = testdir.runpytest(f"--asyncio-mode={mode}")
+ result.assert_outcomes(passed=1)
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_dependent_fixtures.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_dependent_fixtures.py
new file mode 100644
index 0000000000..dc70fe9cd3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_dependent_fixtures.py
@@ -0,0 +1,14 @@
+import asyncio
+
+import pytest
+
+
+@pytest.mark.asyncio
+async def test_dependent_fixture(dependent_fixture):
+ """Test a dependent fixture."""
+ await asyncio.sleep(0.1)
+
+
+@pytest.mark.asyncio
+async def test_factory_involving_factories(factory_involving_factories):
+ factory_involving_factories()
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_event_loop_scope.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_event_loop_scope.py
new file mode 100644
index 0000000000..21fd641515
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_event_loop_scope.py
@@ -0,0 +1,37 @@
+"""Test the event loop fixture provides a separate loop for each test.
+
+These tests need to be run together.
+"""
+import asyncio
+
+import pytest
+
+loop: asyncio.AbstractEventLoop
+
+
+def test_1():
+ global loop
+ # The main thread should have a default event loop.
+ loop = asyncio.get_event_loop_policy().get_event_loop()
+
+
+@pytest.mark.asyncio
+async def test_2():
+ global loop
+ running_loop = asyncio.get_event_loop_policy().get_event_loop()
+ # Make sure this test case received a different loop
+ assert running_loop is not loop
+ loop = running_loop # Store the loop reference for later
+
+
+def test_3():
+ global loop
+ current_loop = asyncio.get_event_loop_policy().get_event_loop()
+ # Now the event loop from test_2 should have been cleaned up
+ assert loop is not current_loop
+
+
+def test_4(event_loop):
+ # If a test sets the loop to None -- pytest_fixture_post_finalizer()
+ # still should work
+ asyncio.get_event_loop_policy().set_event_loop(None)
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_flaky_integration.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_flaky_integration.py
new file mode 100644
index 0000000000..54c9d2eaeb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_flaky_integration.py
@@ -0,0 +1,43 @@
+"""Tests for the Flaky integration, which retries failed tests.
+"""
+from textwrap import dedent
+
+
+def test_auto_mode_cmdline(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import asyncio
+ import flaky
+ import pytest
+
+ _threshold = -1
+
+ @flaky.flaky(3, 2)
+ @pytest.mark.asyncio
+ async def test_asyncio_flaky_thing_that_fails_then_succeeds():
+ global _threshold
+ await asyncio.sleep(0.1)
+ _threshold += 1
+ assert _threshold != 1
+ """
+ )
+ )
+ # runpytest_subprocess() is required to don't pollute the output
+ # with flaky restart information
+ result = testdir.runpytest_subprocess("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
+ result.stdout.fnmatch_lines(
+ [
+ "===Flaky Test Report===",
+ "test_asyncio_flaky_thing_that_fails_then_succeeds passed 1 "
+ "out of the required 2 times. Running test again until it passes 2 times.",
+ "test_asyncio_flaky_thing_that_fails_then_succeeds failed "
+ "(1 runs remaining out of 3).",
+ " <class 'AssertionError'>",
+ " assert 1 != 1",
+ "test_asyncio_flaky_thing_that_fails_then_succeeds passed 2 "
+ "out of the required 2 times. Success!",
+ "===End Flaky Test Report===",
+ ]
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_simple.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_simple.py
new file mode 100644
index 0000000000..dc68d61ec2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_simple.py
@@ -0,0 +1,275 @@
+"""Quick'n'dirty unit tests for provided fixtures and markers."""
+import asyncio
+from textwrap import dedent
+
+import pytest
+
+import pytest_asyncio.plugin
+
+
+async def async_coro():
+ await asyncio.sleep(0)
+ return "ok"
+
+
+def test_event_loop_fixture(event_loop):
+ """Test the injection of the event_loop fixture."""
+ assert event_loop
+ ret = event_loop.run_until_complete(async_coro())
+ assert ret == "ok"
+
+
+@pytest.mark.asyncio
+async def test_asyncio_marker():
+ """Test the asyncio pytest marker."""
+ await asyncio.sleep(0)
+
+
+@pytest.mark.xfail(reason="need a failure", strict=True)
+@pytest.mark.asyncio
+async def test_asyncio_marker_fail():
+ raise AssertionError
+
+
+@pytest.mark.asyncio
+async def test_asyncio_marker_with_default_param(a_param=None):
+ """Test the asyncio pytest marker."""
+ await asyncio.sleep(0)
+
+
+@pytest.mark.asyncio
+async def test_unused_port_fixture(unused_tcp_port, event_loop):
+ """Test the unused TCP port fixture."""
+
+ async def closer(_, writer):
+ writer.close()
+
+ server1 = await asyncio.start_server(closer, host="localhost", port=unused_tcp_port)
+
+ with pytest.raises(IOError):
+ await asyncio.start_server(closer, host="localhost", port=unused_tcp_port)
+
+ server1.close()
+ await server1.wait_closed()
+
+
+@pytest.mark.asyncio
+async def test_unused_udp_port_fixture(unused_udp_port, event_loop):
+ """Test the unused TCP port fixture."""
+
+ class Closer:
+ def connection_made(self, transport):
+ pass
+
+ def connection_lost(self, *arg, **kwd):
+ pass
+
+ transport1, _ = await event_loop.create_datagram_endpoint(
+ Closer,
+ local_addr=("127.0.0.1", unused_udp_port),
+ reuse_port=False,
+ )
+
+ with pytest.raises(IOError):
+ await event_loop.create_datagram_endpoint(
+ Closer,
+ local_addr=("127.0.0.1", unused_udp_port),
+ reuse_port=False,
+ )
+
+ transport1.abort()
+
+
+@pytest.mark.asyncio
+async def test_unused_port_factory_fixture(unused_tcp_port_factory, event_loop):
+ """Test the unused TCP port factory fixture."""
+
+ async def closer(_, writer):
+ writer.close()
+
+ port1, port2, port3 = (
+ unused_tcp_port_factory(),
+ unused_tcp_port_factory(),
+ unused_tcp_port_factory(),
+ )
+
+ server1 = await asyncio.start_server(closer, host="localhost", port=port1)
+ server2 = await asyncio.start_server(closer, host="localhost", port=port2)
+ server3 = await asyncio.start_server(closer, host="localhost", port=port3)
+
+ for port in port1, port2, port3:
+ with pytest.raises(IOError):
+ await asyncio.start_server(closer, host="localhost", port=port)
+
+ server1.close()
+ await server1.wait_closed()
+ server2.close()
+ await server2.wait_closed()
+ server3.close()
+ await server3.wait_closed()
+
+
+@pytest.mark.asyncio
+async def test_unused_udp_port_factory_fixture(unused_udp_port_factory, event_loop):
+ """Test the unused UDP port factory fixture."""
+
+ class Closer:
+ def connection_made(self, transport):
+ pass
+
+ def connection_lost(self, *arg, **kwd):
+ pass
+
+ port1, port2, port3 = (
+ unused_udp_port_factory(),
+ unused_udp_port_factory(),
+ unused_udp_port_factory(),
+ )
+
+ transport1, _ = await event_loop.create_datagram_endpoint(
+ Closer,
+ local_addr=("127.0.0.1", port1),
+ reuse_port=False,
+ )
+ transport2, _ = await event_loop.create_datagram_endpoint(
+ Closer,
+ local_addr=("127.0.0.1", port2),
+ reuse_port=False,
+ )
+ transport3, _ = await event_loop.create_datagram_endpoint(
+ Closer,
+ local_addr=("127.0.0.1", port3),
+ reuse_port=False,
+ )
+
+ for port in port1, port2, port3:
+ with pytest.raises(IOError):
+ await event_loop.create_datagram_endpoint(
+ Closer,
+ local_addr=("127.0.0.1", port),
+ reuse_port=False,
+ )
+
+ transport1.abort()
+ transport2.abort()
+ transport3.abort()
+
+
+def test_unused_port_factory_duplicate(unused_tcp_port_factory, monkeypatch):
+ """Test correct avoidance of duplicate ports."""
+ counter = 0
+
+ def mock_unused_tcp_port(_ignored):
+ """Force some duplicate ports."""
+ nonlocal counter
+ counter += 1
+ if counter < 5:
+ return 10000
+ else:
+ return 10000 + counter
+
+ monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_tcp_port)
+
+ assert unused_tcp_port_factory() == 10000
+ assert unused_tcp_port_factory() > 10000
+
+
+def test_unused_udp_port_factory_duplicate(unused_udp_port_factory, monkeypatch):
+ """Test correct avoidance of duplicate UDP ports."""
+ counter = 0
+
+ def mock_unused_udp_port(_ignored):
+ """Force some duplicate ports."""
+ nonlocal counter
+ counter += 1
+ if counter < 5:
+ return 10000
+ else:
+ return 10000 + counter
+
+ monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_udp_port)
+
+ assert unused_udp_port_factory() == 10000
+ assert unused_udp_port_factory() > 10000
+
+
+class TestMarkerInClassBasedTests:
+ """Test that asyncio marked functions work for methods of test classes."""
+
+ @pytest.mark.asyncio
+ async def test_asyncio_marker_with_explicit_loop_fixture(self, event_loop):
+ """Test the "asyncio" marker works on a method in
+ a class-based test with explicit loop fixture."""
+ ret = await async_coro()
+ assert ret == "ok"
+
+ @pytest.mark.asyncio
+ async def test_asyncio_marker_with_implicit_loop_fixture(self):
+ """Test the "asyncio" marker works on a method in
+ a class-based test with implicit loop fixture."""
+ ret = await async_coro()
+ assert ret == "ok"
+
+
+class TestEventLoopStartedBeforeFixtures:
+ @pytest.fixture
+ async def loop(self):
+ return asyncio.get_event_loop()
+
+ @staticmethod
+ def foo():
+ return 1
+
+ @pytest.mark.asyncio
+ async def test_no_event_loop(self, loop):
+ assert await loop.run_in_executor(None, self.foo) == 1
+
+ @pytest.mark.asyncio
+ async def test_event_loop_after_fixture(self, loop, event_loop):
+ assert await loop.run_in_executor(None, self.foo) == 1
+
+ @pytest.mark.asyncio
+ async def test_event_loop_before_fixture(self, event_loop, loop):
+ assert await loop.run_in_executor(None, self.foo) == 1
+
+
+@pytest.mark.asyncio
+async def test_no_warning_on_skip():
+ pytest.skip("Test a skip error inside asyncio")
+
+
+def test_async_close_loop(event_loop):
+ event_loop.close()
+ return "ok"
+
+
+def test_warn_asyncio_marker_for_regular_func(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import pytest
+
+ pytest_plugins = 'pytest_asyncio'
+
+ @pytest.mark.asyncio
+ def test_a():
+ pass
+ """
+ )
+ )
+ testdir.makefile(
+ ".ini",
+ pytest=dedent(
+ """\
+ [pytest]
+ asyncio_mode = strict
+ filterwarnings =
+ default
+ """
+ ),
+ )
+ result = testdir.runpytest()
+ result.assert_outcomes(passed=1)
+ result.stdout.fnmatch_lines(
+ ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"]
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_subprocess.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_subprocess.py
new file mode 100644
index 0000000000..79c5109dab
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/test_subprocess.py
@@ -0,0 +1,36 @@
+"""Tests for using subprocesses in tests."""
+import asyncio.subprocess
+import sys
+
+import pytest
+
+if sys.platform == "win32":
+ # The default asyncio event loop implementation on Windows does not
+ # support subprocesses. Subprocesses are available for Windows if a
+ # ProactorEventLoop is used.
+ @pytest.yield_fixture()
+ def event_loop():
+ loop = asyncio.ProactorEventLoop()
+ yield loop
+ loop.close()
+
+
+@pytest.mark.skipif(
+ sys.version_info < (3, 8),
+ reason="""
+ When run with Python 3.7 asyncio.subprocess.create_subprocess_exec seems to be
+ affected by an issue that prevents correct cleanup. Tests using pytest-trio
+ will report that signal handling is already performed by another library and
+ fail. [1] This is possibly a bug in CPython 3.7, so we ignore this test for
+ that Python version.
+
+ [1] https://github.com/python-trio/pytest-trio/issues/126
+ """,
+)
+@pytest.mark.asyncio
+async def test_subprocess(event_loop):
+ """Starting a subprocess should be possible."""
+ proc = await asyncio.subprocess.create_subprocess_exec(
+ sys.executable, "--version", stdout=asyncio.subprocess.PIPE
+ )
+ await proc.communicate()
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/trio/test_fixtures.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/trio/test_fixtures.py
new file mode 100644
index 0000000000..42b28437fd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tests/trio/test_fixtures.py
@@ -0,0 +1,25 @@
+from textwrap import dedent
+
+
+def test_strict_mode_ignores_trio_fixtures(testdir):
+ testdir.makepyfile(
+ dedent(
+ """\
+ import pytest
+ import pytest_asyncio
+ import pytest_trio
+
+ pytest_plugins = ["pytest_asyncio", "pytest_trio"]
+
+ @pytest_trio.trio_fixture
+ async def any_fixture():
+ return True
+
+ @pytest.mark.trio
+ async def test_anything(any_fixture):
+ pass
+ """
+ )
+ )
+ result = testdir.runpytest("--asyncio-mode=strict")
+ result.assert_outcomes(passed=1)
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tools/get-version.py b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tools/get-version.py
new file mode 100644
index 0000000000..e988a32cb9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tools/get-version.py
@@ -0,0 +1,17 @@
+import json
+import sys
+from importlib import metadata
+
+from packaging.version import parse as parse_version
+
+
+def main():
+ version_string = metadata.version("pytest-asyncio")
+ version = parse_version(version_string)
+ print(f"::set-output name=version::{version}")
+ prerelease = json.dumps(version.is_prerelease)
+ print(f"::set-output name=prerelease::{prerelease}")
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/testing/web-platform/tests/tools/third_party/pytest-asyncio/tox.ini b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tox.ini
new file mode 100644
index 0000000000..1d8994ae4c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest-asyncio/tox.ini
@@ -0,0 +1,56 @@
+[tox]
+minversion = 3.14.0
+envlist = py37, py38, py39, py310, py311, lint, version-info, pytest-min
+isolated_build = true
+passenv =
+ CI
+
+[testenv]
+extras = testing
+deps =
+ --requirement dependencies/default/requirements.txt
+ --constraint dependencies/default/constraints.txt
+commands = make test
+allowlist_externals =
+ make
+
+[testenv:pytest-min]
+extras = testing
+deps =
+ --requirement dependencies/pytest-min/requirements.txt
+ --constraint dependencies/pytest-min/constraints.txt
+commands = make test
+allowlist_externals =
+ make
+
+[testenv:lint]
+basepython = python3.10
+extras = testing
+deps =
+ pre-commit == 2.16.0
+commands =
+ make lint
+allowlist_externals =
+ make
+
+[testenv:coverage-report]
+deps = coverage
+skip_install = true
+commands =
+ coverage combine
+ coverage report
+
+[testenv:version-info]
+deps =
+ packaging == 21.3
+commands =
+ python ./tools/get-version.py
+
+[gh-actions]
+python =
+ 3.7: py37, pytest-min
+ 3.8: py38
+ 3.9: py39
+ 3.10: py310
+ 3.11-dev: py311
+ pypy3: pypy3
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.coveragerc b/testing/web-platform/tests/tools/third_party/pytest/.coveragerc
new file mode 100644
index 0000000000..a335557d4f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.coveragerc
@@ -0,0 +1,31 @@
+[run]
+include =
+ src/*
+ testing/*
+ */lib/python*/site-packages/_pytest/*
+ */lib/python*/site-packages/pytest.py
+ */pypy*/site-packages/_pytest/*
+ */pypy*/site-packages/pytest.py
+ *\Lib\site-packages\_pytest\*
+ *\Lib\site-packages\pytest.py
+parallel = 1
+branch = 1
+
+[paths]
+source = src/
+ */lib/python*/site-packages/
+ */pypy*/site-packages/
+ *\Lib\site-packages\
+
+[report]
+skip_covered = True
+show_missing = True
+exclude_lines =
+ \#\s*pragma: no cover
+ ^\s*raise NotImplementedError\b
+ ^\s*return NotImplemented\b
+ ^\s*assert False(,|$)
+ ^\s*assert_never\(
+
+ ^\s*if TYPE_CHECKING:
+ ^\s*@overload( |$)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.gitblameignore b/testing/web-platform/tests/tools/third_party/pytest/.gitblameignore
new file mode 100644
index 0000000000..0cb298b024
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.gitblameignore
@@ -0,0 +1,28 @@
+# 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/testing/web-platform/tests/tools/third_party/pytest/.github/FUNDING.yml b/testing/web-platform/tests/tools/third_party/pytest/.github/FUNDING.yml
new file mode 100644
index 0000000000..5f2d1cf09c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.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/pytest
+open_collective: pytest
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.github/ISSUE_TEMPLATE/1_bug_report.md b/testing/web-platform/tests/tools/third_party/pytest/.github/ISSUE_TEMPLATE/1_bug_report.md
new file mode 100644
index 0000000000..0fc3e06cd2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.github/ISSUE_TEMPLATE/1_bug_report.md
@@ -0,0 +1,16 @@
+---
+name: 🐛 Bug Report
+about: Report errors and problems
+
+---
+
+<!--
+Thanks for submitting an issue!
+
+Quick check-list while reporting bugs:
+-->
+
+- [ ] a detailed description of the bug or problem you are having
+- [ ] output of `pip list` from the virtual environment you are using
+- [ ] pytest and operating system versions
+- [ ] minimal example if possible
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.github/ISSUE_TEMPLATE/2_feature_request.md b/testing/web-platform/tests/tools/third_party/pytest/.github/ISSUE_TEMPLATE/2_feature_request.md
new file mode 100644
index 0000000000..01fe96295e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.github/ISSUE_TEMPLATE/2_feature_request.md
@@ -0,0 +1,25 @@
+---
+name: 🚀 Feature Request
+about: Ideas for new features and improvements
+
+---
+
+<!--
+Thanks for suggesting a feature!
+
+Quick check-list while suggesting features:
+-->
+
+#### What's the problem this feature will solve?
+<!-- What are you trying to do, that you are unable to achieve with pytest as it currently stands? -->
+
+#### Describe the solution you'd like
+<!-- A clear and concise description of what you want to happen. -->
+
+<!-- Provide examples of real-world use cases that this would enable and how it solves the problem described above. -->
+
+#### Alternative Solutions
+<!-- Have you tried to workaround the problem using a pytest plugin or other tools? Or a different approach to solving this issue? Please elaborate here. -->
+
+#### Additional context
+<!-- Add any other context, links, etc. about the feature here. -->
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.github/ISSUE_TEMPLATE/config.yml b/testing/web-platform/tests/tools/third_party/pytest/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..742d2e4d66
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: ❓ Support Question
+ url: https://github.com/pytest-dev/pytest/discussions
+ about: Use GitHub's new Discussions feature for questions
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.github/PULL_REQUEST_TEMPLATE.md b/testing/web-platform/tests/tools/third_party/pytest/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000..5e7282bfd7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,26 @@
+<!--
+Thanks for submitting a PR, your contribution is really appreciated!
+
+Here is a quick checklist that should be present in PRs.
+
+- [ ] Include documentation when adding new features.
+- [ ] Include new tests or update existing tests when applicable.
+- [X] Allow maintainers to push and squash when merging my commits. Please uncheck this if you prefer to squash the commits yourself.
+
+If this change fixes an issue, please:
+
+- [ ] Add text like ``closes #XYZW`` to the PR description and/or commits (where ``XYZW`` is the issue number). See the [github docs](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for more information.
+
+Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please:
+
+- [ ] Create a new changelog file in the `changelog` folder, with a name like `<ISSUE NUMBER>.<TYPE>.rst`. See [changelog/README.rst](https://github.com/pytest-dev/pytest/blob/main/changelog/README.rst) for details.
+
+ Write sentences in the **past or present tense**, examples:
+
+ * *Improved verbose diff output with sequences.*
+ * *Terminal summary statistics now use multiple colors.*
+
+ Also make sure to end the sentence with a `.`.
+
+- [ ] Add yourself to `AUTHORS` in alphabetical order.
+-->
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.github/config.yml b/testing/web-platform/tests/tools/third_party/pytest/.github/config.yml
new file mode 100644
index 0000000000..86a8a97e78
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.github/config.yml
@@ -0,0 +1,2 @@
+rtd:
+ project: pytest
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.github/dependabot.yml b/testing/web-platform/tests/tools/third_party/pytest/.github/dependabot.yml
new file mode 100644
index 0000000000..507789bf5a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.github/dependabot.yml
@@ -0,0 +1,11 @@
+version: 2
+updates:
+- package-ecosystem: pip
+ directory: "/testing/plugins_integration"
+ schedule:
+ interval: weekly
+ time: "03:00"
+ open-pull-requests-limit: 10
+ allow:
+ - dependency-type: direct
+ - dependency-type: indirect
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.github/labels.toml b/testing/web-platform/tests/tools/third_party/pytest/.github/labels.toml
new file mode 100644
index 0000000000..aef1e913af
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.github/labels.toml
@@ -0,0 +1,149 @@
+["os: cygwin"]
+color = "006b75"
+description = "cygwin platform-specific problem"
+name = "os: cygwin"
+
+["os: linux"]
+color = "1d76db"
+description = "linux platform-specific problem"
+name = "os: linux"
+
+["os: mac"]
+color = "bfdadc"
+description = "mac platform-specific problem"
+name = "os: mac"
+
+["os: windows"]
+color = "fbca04"
+description = "windows platform-specific problem"
+name = "os: windows"
+
+["plugin: argcomplete"]
+color = "d4c5f9"
+description = "related to the argcomplete builtin plugin"
+name = "plugin: argcomplete"
+
+["plugin: cache"]
+color = "c7def8"
+description = "related to the cache builtin plugin"
+name = "plugin: cache"
+
+["plugin: capture"]
+color = "1d76db"
+description = "related to the capture builtin plugin"
+name = "plugin: capture"
+
+["plugin: debugging"]
+color = "dd52a8"
+description = "related to the debugging builtin plugin"
+name = "plugin: debugging"
+
+["plugin: doctests"]
+color = "fad8c7"
+description = "related to the doctests builtin plugin"
+name = "plugin: doctests"
+
+["plugin: junitxml"]
+color = "c5def5"
+description = "related to the junitxml builtin plugin"
+name = "plugin: junitxml"
+
+["plugin: logging"]
+color = "ff5432"
+description = "related to the logging builtin plugin"
+name = "plugin: logging"
+
+["plugin: monkeypatch"]
+color = "0e8a16"
+description = "related to the monkeypatch builtin plugin"
+name = "plugin: monkeypatch"
+
+["plugin: nose"]
+color = "bfdadc"
+description = "related to the nose integration builtin plugin"
+name = "plugin: nose"
+
+["plugin: pastebin"]
+color = "bfd4f2"
+description = "related to the pastebin builtin plugin"
+name = "plugin: pastebin"
+
+["plugin: pytester"]
+color = "c5def5"
+description = "related to the pytester builtin plugin"
+name = "plugin: pytester"
+
+["plugin: tmpdir"]
+color = "bfd4f2"
+description = "related to the tmpdir builtin plugin"
+name = "plugin: tmpdir"
+
+["plugin: unittest"]
+color = "006b75"
+description = "related to the unittest integration builtin plugin"
+name = "plugin: unittest"
+
+["plugin: warnings"]
+color = "fef2c0"
+description = "related to the warnings builtin plugin"
+name = "plugin: warnings"
+
+["plugin: xdist"]
+color = "5319e7"
+description = "related to the xdist external plugin"
+name = "plugin: xdist"
+
+["status: critical"]
+color = "e11d21"
+description = "grave problem or usability issue that affects lots of users"
+name = "status: critical"
+
+["status: easy"]
+color = "bfe5bf"
+description = "easy issue that is friendly to new contributor"
+name = "status: easy"
+
+["status: help wanted"]
+color = "159818"
+description = "developers would like help from experts on this topic"
+name = "status: help wanted"
+
+["status: needs information"]
+color = "5319e7"
+description = "reporter needs to provide more information; can be closed after 2 or more weeks of inactivity"
+name = "status: needs information"
+
+["topic: collection"]
+color = "006b75"
+description = "related to the collection phase"
+name = "topic: collection"
+
+["topic: config"]
+color = "006b75"
+description = "related to config handling, argument parsing and config file"
+name = "topic: config"
+
+["topic: fixtures"]
+color = "5319e7"
+description = "anything involving fixtures directly or indirectly"
+name = "topic: fixtures"
+
+["topic: marks"]
+color = "b60205"
+description = "related to marks, either the general marks or builtin"
+name = "topic: marks"
+
+["topic: parametrize"]
+color = "fbca04"
+description = "related to @pytest.mark.parametrize"
+name = "topic: parametrize"
+
+["topic: reporting"]
+color = "fef2c0"
+description = "related to terminal output and user-facing messages and errors"
+name = "topic: reporting"
+
+["topic: rewrite"]
+color = "0e8a16"
+description = "related to the assertion rewrite mechanism"
+name = "topic: rewrite"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.github/workflows/main.yml b/testing/web-platform/tests/tools/third_party/pytest/.github/workflows/main.yml
new file mode 100644
index 0000000000..42759ce853
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.github/workflows/main.yml
@@ -0,0 +1,231 @@
+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/testing/web-platform/tests/tools/third_party/pytest/.github/workflows/prepare-release-pr.yml b/testing/web-platform/tests/tools/third_party/pytest/.github/workflows/prepare-release-pr.yml
new file mode 100644
index 0000000000..429834b3f2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.github/workflows/prepare-release-pr.yml
@@ -0,0 +1,52 @@
+name: prepare release pr
+
+on:
+ workflow_dispatch:
+ inputs:
+ branch:
+ description: 'Branch to base the release from'
+ required: true
+ default: ''
+ major:
+ description: 'Major release? (yes/no)'
+ required: true
+ default: 'no'
+ prerelease:
+ description: 'Prerelease (ex: rc1). Leave empty if not a pre-release.'
+ required: false
+ default: ''
+
+# Set permissions at the job level.
+permissions: {}
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: "3.8"
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install --upgrade setuptools tox
+
+ - name: Prepare release PR (minor/patch release)
+ if: github.event.inputs.major == 'no'
+ run: |
+ tox -e prepare-release-pr -- ${{ github.event.inputs.branch }} ${{ github.token }} --prerelease='${{ github.event.inputs.prerelease }}'
+
+ - name: Prepare release PR (major release)
+ if: github.event.inputs.major == 'yes'
+ run: |
+ tox -e prepare-release-pr -- ${{ github.event.inputs.branch }} ${{ github.token }} --major --prerelease='${{ github.event.inputs.prerelease }}'
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.github/workflows/update-plugin-list.yml b/testing/web-platform/tests/tools/third_party/pytest/.github/workflows/update-plugin-list.yml
new file mode 100644
index 0000000000..193469072f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.github/workflows/update-plugin-list.yml
@@ -0,0 +1,49 @@
+name: Update Plugin List
+
+on:
+ schedule:
+ # At 00:00 on Sunday.
+ # https://crontab.guru
+ - cron: '0 0 * * 0'
+ workflow_dispatch:
+
+# Set permissions at the job level.
+permissions: {}
+
+jobs:
+ createPullRequest:
+ if: github.repository_owner == 'pytest-dev'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install packaging requests tabulate[widechars] tqdm
+
+ - name: Update Plugin List
+ run: python scripts/update-plugin-list.py
+
+ - name: Create Pull Request
+ uses: peter-evans/create-pull-request@2455e1596942c2902952003bbb574afbbe2ab2e6
+ with:
+ commit-message: '[automated] Update plugin list'
+ author: 'pytest bot <pytestbot@users.noreply.github.com>'
+ branch: update-plugin-list/patch
+ delete-branch: true
+ branch-suffix: short-commit-hash
+ title: '[automated] Update plugin list'
+ body: '[automated] Update plugin list'
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.gitignore b/testing/web-platform/tests/tools/third_party/pytest/.gitignore
new file mode 100644
index 0000000000..935da3b9a2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.gitignore
@@ -0,0 +1,58 @@
+# Automatically generated by `hgimportsvn`
+.svn
+.hgsvn
+
+# Ignore local virtualenvs
+lib/
+bin/
+include/
+.Python/
+
+# These lines are suggested according to the svn:ignore property
+# Feel free to enable them by uncommenting them
+*.pyc
+*.pyo
+*.swp
+*.class
+*.orig
+*~
+.hypothesis/
+
+# autogenerated
+src/_pytest/_version.py
+# setuptools
+.eggs/
+
+doc/*/_build
+doc/*/.doctrees
+doc/*/_changelog_towncrier_draft.rst
+build/
+dist/
+*.egg-info
+htmlcov/
+issue/
+env/
+.env/
+.venv/
+/pythonenv*/
+3rdparty/
+.tox
+.cache
+.pytest_cache
+.mypy_cache
+.coverage
+.coverage.*
+coverage.xml
+.ropeproject
+.idea
+.hypothesis
+.pydevproject
+.project
+.settings
+.vscode
+
+# generated by pip
+pip-wheel-metadata/
+
+# pytest debug logs generated via --debug
+pytestdebug.log
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.pre-commit-config.yaml b/testing/web-platform/tests/tools/third_party/pytest/.pre-commit-config.yaml
new file mode 100644
index 0000000000..20cede3b7b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.pre-commit-config.yaml
@@ -0,0 +1,99 @@
+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/pre-commit/pre-commit-hooks
+ rev: v4.0.1
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: fix-encoding-pragma
+ args: [--remove]
+ - id: check-yaml
+ - 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
+ 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]
+- repo: https://github.com/pre-commit/pygrep-hooks
+ rev: v1.9.0
+ hooks:
+ - id: python-use-type-annotations
+- repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v0.910-1
+ hooks:
+ - id: mypy
+ files: ^(src/|testing/)
+ args: []
+ additional_dependencies:
+ - iniconfig>=1.1.0
+ - py>=1.8.2
+ - attrs>=19.2.0
+ - packaging
+ - tomli
+ - types-atomicwrites
+ - types-pkg_resources
+- repo: local
+ hooks:
+ - id: rst
+ name: rst
+ entry: rst-lint --encoding utf-8
+ files: ^(RELEASING.rst|README.rst|TIDELIFT.rst)$
+ language: python
+ additional_dependencies: [pygments, restructuredtext_lint]
+ - id: changelogs-rst
+ name: changelog filenames
+ language: fail
+ entry: 'changelog files must be named ####.(breaking|bugfix|deprecation|doc|feature|improvement|trivial|vendor).rst'
+ exclude: changelog/(\d+\.(breaking|bugfix|deprecation|doc|feature|improvement|trivial|vendor).rst|README.rst|_template.rst)
+ files: ^changelog/
+ - id: py-deprecated
+ name: py library is deprecated
+ language: pygrep
+ entry: >
+ (?x)\bpy\.(
+ _code\.|
+ builtin\.|
+ code\.|
+ io\.|
+ path\.local\.sysfind|
+ process\.|
+ std\.|
+ error\.|
+ xml\.
+ )
+ types: [python]
+ - id: py-path-deprecated
+ name: py.path usage is deprecated
+ exclude: docs|src/_pytest/deprecated.py|testing/deprecated_test.py
+ language: pygrep
+ entry: \bpy\.path\.local
+ types: [python]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/.readthedocs.yml b/testing/web-platform/tests/tools/third_party/pytest/.readthedocs.yml
new file mode 100644
index 0000000000..bc44d38b4c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/.readthedocs.yml
@@ -0,0 +1,19 @@
+version: 2
+
+python:
+ install:
+ - requirements: doc/en/requirements.txt
+ - method: pip
+ path: .
+
+build:
+ os: ubuntu-20.04
+ tools:
+ python: "3.9"
+ apt_packages:
+ - inkscape
+
+formats:
+ - epub
+ - pdf
+ - htmlzip
diff --git a/testing/web-platform/tests/tools/third_party/pytest/AUTHORS b/testing/web-platform/tests/tools/third_party/pytest/AUTHORS
new file mode 100644
index 0000000000..9413f9c2e7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/AUTHORS
@@ -0,0 +1,356 @@
+Holger Krekel, holger at merlinux eu
+merlinux GmbH, Germany, office at merlinux eu
+
+Contributors include::
+
+Aaron Coleman
+Abdeali JK
+Abdelrahman Elbehery
+Abhijeet Kasurde
+Adam Johnson
+Adam Uhlir
+Ahn Ki-Wook
+Akiomi Kamakura
+Alan Velasco
+Alexander Johnson
+Alexander King
+Alexei Kozlenok
+Allan Feldman
+Aly Sivji
+Amir Elkess
+Anatoly Bubenkoff
+Anders Hovmöller
+Andras Mitzki
+Andras Tim
+Andrea Cimatoribus
+Andreas Motl
+Andreas Zeidler
+Andrew Shapton
+Andrey Paramonov
+Andrzej Klajnert
+Andrzej Ostrowski
+Andy Freeland
+Anthon van der Neut
+Anthony Shaw
+Anthony Sottile
+Anton Grinevich
+Anton Lodder
+Antony Lee
+Arel Cordero
+Arias Emmanuel
+Ariel Pillemer
+Armin Rigo
+Aron Coyle
+Aron Curzon
+Aviral Verma
+Aviv Palivoda
+Barney Gale
+Ben Gartner
+Ben Webb
+Benjamin Peterson
+Bernard Pratz
+Bob Ippolito
+Brian Dorsey
+Brian Maissy
+Brian Okken
+Brianna Laugher
+Bruno Oliveira
+Cal Leeming
+Carl Friedrich Bolz
+Carlos Jenkins
+Ceridwen
+Charles Cloud
+Charles Machalow
+Charnjit SiNGH (CCSJ)
+Chris Lamb
+Chris NeJame
+Chris Rose
+Christian Boelsen
+Christian Fetzer
+Christian Neumüller
+Christian Theunert
+Christian Tismer
+Christine Mecklenborg
+Christoph Buelter
+Christopher Dignam
+Christopher Gilling
+Claire Cecil
+Claudio Madotto
+CrazyMerlyn
+Cristian Vera
+Cyrus Maden
+Damian Skrzypczak
+Daniel Grana
+Daniel Hahler
+Daniel Nuri
+Daniel Wandschneider
+Daniele Procida
+Danielle Jenkins
+Daniil Galiev
+Dave Hunt
+David Díaz-Barquero
+David Mohr
+David Paul Röthlisberger
+David Szotten
+David Vierra
+Daw-Ran Liou
+Debi Mishra
+Denis Kirisov
+Denivy Braiam Rück
+Dhiren Serai
+Diego Russo
+Dmitry Dygalo
+Dmitry Pribysh
+Dominic Mortlock
+Duncan Betts
+Edison Gustavo Muenz
+Edoardo Batini
+Edson Tadeu M. Manoel
+Eduardo Schettino
+Eli Boyarski
+Elizaveta Shashkova
+Éloi Rivard
+Endre Galaczi
+Eric Hunsberger
+Eric Liu
+Eric Siegerman
+Erik Aronesty
+Erik M. Bray
+Evan Kepner
+Fabien Zarifian
+Fabio Zadrozny
+Felix Nieuwenhuizen
+Feng Ma
+Florian Bruhin
+Florian Dahlitz
+Floris Bruynooghe
+Gabriel Reis
+Garvit Shubham
+Gene Wood
+George Kussumoto
+Georgy Dyuldin
+Gergely Kalmár
+Gleb Nikonorov
+Graeme Smecher
+Graham Horler
+Greg Price
+Gregory Lee
+Grig Gheorghiu
+Grigorii Eremeev (budulianin)
+Guido Wesdorp
+Guoqiang Zhang
+Harald Armin Massa
+Harshna
+Henk-Jaap Wagenaar
+Holger Kohr
+Hugo van Kemenade
+Hui Wang (coldnight)
+Ian Bicking
+Ian Lesperance
+Ilya Konstantinov
+Ionuț Turturică
+Iwan Briquemont
+Jaap Broekhuizen
+Jakob van Santen
+Jakub Mitoraj
+James Bourbeau
+Jan Balster
+Janne Vanhala
+Jason R. Coombs
+Javier Domingo Cansino
+Javier Romero
+Jeff Rackauckas
+Jeff Widman
+Jenni Rinker
+John Eddie Ayson
+John Towler
+Jon Sonesen
+Jonas Obrist
+Jordan Guymon
+Jordan Moldow
+Jordan Speicher
+Joseph Hunkeler
+Josh Karpel
+Joshua Bronson
+Jurko Gospodnetić
+Justyna Janczyszyn
+Justice Ndou
+Kale Kundert
+Kamran Ahmad
+Karl O. Pinc
+Karthikeyan Singaravelan
+Katarzyna Jachim
+Katarzyna Król
+Katerina Koukiou
+Keri Volans
+Kevin Cox
+Kevin J. Foley
+Kian-Meng Ang
+Kodi B. Arfer
+Kostis Anagnostopoulos
+Kristoffer Nordström
+Kyle Altendorf
+Lawrence Mitchell
+Lee Kamentsky
+Lev Maximov
+Lewis Cowles
+Llandy Riveron Del Risco
+Loic Esteve
+Lukas Bednar
+Luke Murphy
+Maciek Fijalkowski
+Maho
+Maik Figura
+Mandeep Bhutani
+Manuel Krebber
+Marc Schlaich
+Marcelo Duarte Trevisani
+Marcin Bachry
+Marco Gorelli
+Mark Abramowitz
+Mark Dickinson
+Markus Unterwaditzer
+Martijn Faassen
+Martin Altmayer
+Martin K. Scherer
+Martin Prusse
+Mathieu Clabaut
+Matt Bachmann
+Matt Duck
+Matt Williams
+Matthias Hafner
+Maxim Filipenko
+Maximilian Cosmo Sitter
+mbyt
+Mickey Pashov
+Michael Aquilina
+Michael Birtwell
+Michael Droettboom
+Michael Goerz
+Michael Krebs
+Michael Seifert
+Michal Wajszczuk
+Michał Zięba
+Mihai Capotă
+Mike Hoyle (hoylemd)
+Mike Lundy
+Miro Hrončok
+Nathaniel Compton
+Nathaniel Waisbrot
+Ned Batchelder
+Neven Mundar
+Nicholas Devenish
+Nicholas Murphy
+Niclas Olofsson
+Nicolas Delaby
+Nikolay Kondratyev
+Olga Matoula
+Oleg Pidsadnyi
+Oleg Sushchenko
+Oliver Bestwalter
+Omar Kohl
+Omer Hadari
+Ondřej Súkup
+Oscar Benjamin
+Parth Patel
+Patrick Hayes
+Pauli Virtanen
+Pavel Karateev
+Paweł Adamczak
+Pedro Algarvio
+Petter Strandmark
+Philipp Loose
+Pieter Mulder
+Piotr Banaszkiewicz
+Piotr Helm
+Prakhar Gurunani
+Prashant Anand
+Prashant Sharma
+Pulkit Goyal
+Punyashloka Biswal
+Quentin Pradet
+Ralf Schmitt
+Ram Rachum
+Ralph Giles
+Ran Benita
+Raphael Castaneda
+Raphael Pierzina
+Raquel Alegre
+Ravi Chandra
+Robert Holt
+Roberto Polli
+Roland Puntaier
+Romain Dorgueil
+Roman Bolshakov
+Ronny Pfannschmidt
+Ross Lawley
+Ruaridh Williamson
+Russel Winder
+Ryan Wooden
+Saiprasad Kale
+Samuel Dion-Girardeau
+Samuel Searles-Bryant
+Samuele Pedroni
+Sanket Duthade
+Sankt Petersbug
+Segev Finer
+Serhii Mozghovyi
+Seth Junot
+Shantanu Jain
+Shubham Adep
+Simon Gomizelj
+Simon Kerr
+Skylar Downes
+Srinivas Reddy Thatiparthy
+Stefan Farmbauer
+Stefan Scherfke
+Stefan Zimmermann
+Stefano Taschini
+Steffen Allner
+Stephan Obermann
+Sven-Hendrik Haase
+Sylvain Marié
+Tadek Teleżyński
+Takafumi Arakaki
+Taneli Hukkinen
+Tanvi Mehta
+Tarcisio Fischer
+Tareq Alayan
+Ted Xiao
+Terje Runde
+Thomas Grainger
+Thomas Hisch
+Tim Hoffmann
+Tim Strazny
+Tom Dalton
+Tom Viner
+Tomáš Gavenčiak
+Tomer Keren
+Tor Colvin
+Trevor Bekolay
+Tyler Goodlet
+Tzu-ping Chung
+Vasily Kuznetsov
+Victor Maryama
+Victor Uriarte
+Vidar T. Fauske
+Virgil Dupras
+Vitaly Lashmanov
+Vlad Dragos
+Vlad Radziuk
+Vladyslav Rachek
+Volodymyr Piskun
+Wei Lin
+Wil Cooley
+William Lee
+Wim Glenn
+Wouter van Ackooy
+Xixi Zhao
+Xuan Luong
+Xuecong Liao
+Yoav Caspi
+Yuval Shimon
+Zac Hatfield-Dodds
+Zachary Kneupper
+Zoltán Máté
+Zsolt Cserna
diff --git a/testing/web-platform/tests/tools/third_party/pytest/CHANGELOG.rst b/testing/web-platform/tests/tools/third_party/pytest/CHANGELOG.rst
new file mode 100644
index 0000000000..481f277813
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/CHANGELOG.rst
@@ -0,0 +1,7 @@
+=========
+Changelog
+=========
+
+The pytest CHANGELOG is located `here <https://docs.pytest.org/en/stable/changelog.html>`__.
+
+The source document can be found at: https://github.com/pytest-dev/pytest/blob/main/doc/en/changelog.rst
diff --git a/testing/web-platform/tests/tools/third_party/pytest/CITATION b/testing/web-platform/tests/tools/third_party/pytest/CITATION
new file mode 100644
index 0000000000..d4e9d8ec7a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/CITATION
@@ -0,0 +1,16 @@
+NOTE: Change "x.y" by the version you use. If you are unsure about which version
+you are using run: `pip show pytest`.
+
+Text:
+
+[pytest] pytest x.y, 2004
+Krekel et al., https://github.com/pytest-dev/pytest
+
+BibTeX:
+
+@misc{pytestx.y,
+ title = {pytest x.y},
+ author = {Krekel, Holger and Oliveira, Bruno and Pfannschmidt, Ronny and Bruynooghe, Floris and Laugher, Brianna and Bruhin, Florian},
+ year = {2004},
+ url = {https://github.com/pytest-dev/pytest},
+}
diff --git a/testing/web-platform/tests/tools/third_party/pytest/CODE_OF_CONDUCT.md b/testing/web-platform/tests/tools/third_party/pytest/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..f0ca304be4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/CODE_OF_CONDUCT.md
@@ -0,0 +1,83 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at coc@pytest.org. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+The coc@pytest.org address is routed to the following people who can also be
+contacted individually:
+
+- Brianna Laugher ([@pfctdayelise](https://github.com/pfctdayelise)): brianna@laugher.id.au
+- Bruno Oliveira ([@nicoddemus](https://github.com/nicoddemus)): nicoddemus@gmail.com
+- Florian Bruhin ([@the-compiler](https://github.com/the-compiler)): pytest@the-compiler.org
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/testing/web-platform/tests/tools/third_party/pytest/CONTRIBUTING.rst b/testing/web-platform/tests/tools/third_party/pytest/CONTRIBUTING.rst
new file mode 100644
index 0000000000..24bca723c8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/CONTRIBUTING.rst
@@ -0,0 +1,481 @@
+============================
+Contribution getting started
+============================
+
+Contributions are highly welcomed and appreciated. Every little bit of help counts,
+so do not hesitate!
+
+.. contents::
+ :depth: 2
+ :backlinks: none
+
+
+.. _submitfeedback:
+
+Feature requests and feedback
+-----------------------------
+
+Do you like pytest? Share some love on Twitter or in your blog posts!
+
+We'd also like to hear about your propositions and suggestions. Feel free to
+`submit them as issues <https://github.com/pytest-dev/pytest/issues>`_ and:
+
+* Explain in detail how they should work.
+* Keep the scope as narrow as possible. This will make it easier to implement.
+
+
+.. _reportbugs:
+
+Report bugs
+-----------
+
+Report bugs for pytest in the `issue tracker <https://github.com/pytest-dev/pytest/issues>`_.
+
+If you are reporting a bug, please include:
+
+* Your operating system name and version.
+* Any details about your local setup that might be helpful in troubleshooting,
+ specifically the Python interpreter version, installed libraries, and pytest
+ version.
+* Detailed steps to reproduce the bug.
+
+If you can write a demonstration test that currently fails but should pass
+(xfail), that is a very useful commit to make as well, even if you cannot
+fix the bug itself.
+
+
+.. _fixbugs:
+
+Fix bugs
+--------
+
+Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
+
+: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.
+
+Don't forget to check the issue trackers of your favourite plugins, too!
+
+.. _writeplugins:
+
+Implement features
+------------------
+
+Look through the `GitHub issues for enhancements <https://github.com/pytest-dev/pytest/labels/type:%20enhancement>`_.
+
+:ref:`Talk <contact>` to developers to find out how you can implement specific
+features.
+
+Write documentation
+-------------------
+
+Pytest could always use more documentation. What exactly is needed?
+
+* More complementary documentation. Have you perhaps found something unclear?
+* Documentation translations. We currently have only English.
+* Docstrings. There can never be too many of them.
+* Blog posts, articles and such -- they're all very appreciated.
+
+You can also edit documentation files directly in the GitHub web interface,
+without using a local copy. This can be convenient for small fixes.
+
+.. note::
+ Build the documentation locally with the following command:
+
+ .. code:: bash
+
+ $ tox -e docs
+
+ The built documentation should be available in ``doc/en/_build/html``,
+ where 'en' refers to the documentation language.
+
+Pytest has an API reference which in large part is
+`generated automatically <https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html>`_
+from the docstrings of the documented items. Pytest uses the
+`Sphinx docstring format <https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html>`_.
+For example:
+
+.. code-block:: python
+
+ def my_function(arg: ArgType) -> Foo:
+ """Do important stuff.
+
+ More detailed info here, in separate paragraphs from the subject line.
+ Use proper sentences -- start sentences with capital letters and end
+ with periods.
+
+ Can include annotated documentation:
+
+ :param short_arg: An argument which determines stuff.
+ :param long_arg:
+ A long explanation which spans multiple lines, overflows
+ like this.
+ :returns: The result.
+ :raises ValueError:
+ Detailed information when this can happen.
+
+ .. versionadded:: 6.0
+
+ Including types into the annotations above is not necessary when
+ type-hinting is being used (as in this example).
+ """
+
+
+.. _submitplugin:
+
+Submitting Plugins to pytest-dev
+--------------------------------
+
+Pytest development of the core, some plugins and support code happens
+in repositories living under the ``pytest-dev`` organisations:
+
+- `pytest-dev on GitHub <https://github.com/pytest-dev>`_
+
+All pytest-dev Contributors team members have write access to all contained
+repositories. Pytest core and plugins are generally developed
+using `pull requests`_ to respective repositories.
+
+The objectives of the ``pytest-dev`` organisation are:
+
+* Having a central location for popular pytest plugins
+* Sharing some of the maintenance responsibility (in case a maintainer no
+ longer wishes to maintain a plugin)
+
+You can submit your plugin by subscribing to the `pytest-dev mail list
+<https://mail.python.org/mailman/listinfo/pytest-dev>`_ and writing a
+mail pointing to your existing pytest plugin repository which must have
+the following:
+
+- PyPI presence with packaging metadata that contains a ``pytest-``
+ prefixed name, version number, authors, short and long description.
+
+- a `tox configuration <https://tox.readthedocs.io/en/latest/config.html#configuration-discovery>`_
+ for running tests using `tox <https://tox.readthedocs.io>`_.
+
+- a ``README`` describing how to use the plugin and on which
+ platforms it runs.
+
+- a ``LICENSE`` file containing the licensing information, with
+ matching info in its packaging metadata.
+
+- an issue tracker for bug reports and enhancement requests.
+
+- a `changelog <https://keepachangelog.com/>`_.
+
+If no contributor strongly objects and two agree, the repository can then be
+transferred to the ``pytest-dev`` organisation.
+
+Here's a rundown of how a repository transfer usually proceeds
+(using a repository named ``joedoe/pytest-xyz`` as example):
+
+* ``joedoe`` transfers repository ownership to ``pytest-dev`` administrator ``calvin``.
+* ``calvin`` creates ``pytest-xyz-admin`` and ``pytest-xyz-developers`` teams, inviting ``joedoe`` to both as **maintainer**.
+* ``calvin`` transfers repository to ``pytest-dev`` and configures team access:
+
+ - ``pytest-xyz-admin`` **admin** access;
+ - ``pytest-xyz-developers`` **write** access;
+
+The ``pytest-dev/Contributors`` team has write access to all projects, and
+every project administrator is in it. We recommend that each plugin has at least three
+people who have the right to release to PyPI.
+
+Repository owners can rest assured that no ``pytest-dev`` administrator will ever make
+releases of your repository or take ownership in any way, except in rare cases
+where someone becomes unresponsive after months of contact attempts.
+As stated, the objective is to share maintenance and avoid "plugin-abandon".
+
+
+.. _`pull requests`:
+.. _pull-requests:
+
+Preparing Pull Requests
+-----------------------
+
+Short version
+~~~~~~~~~~~~~
+
+#. Fork the repository.
+#. 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.
+#. Tests are run using ``tox``::
+
+ tox -e linting,py37
+
+ The test environments above are usually enough to cover most cases locally.
+
+#. Write a ``changelog`` entry: ``changelog/2574.bugfix.rst``, use issue id number
+ and one of ``feature``, ``improvement``, ``bugfix``, ``doc``, ``deprecation``,
+ ``breaking``, ``vendor`` or ``trivial`` for the issue type.
+
+
+#. Unless your change is a trivial or a documentation fix (e.g., a typo or reword of a small section) please
+ add yourself to the ``AUTHORS`` file, in alphabetical order.
+
+
+Long version
+~~~~~~~~~~~~
+
+What is a "pull request"? It informs the project's core developers about the
+changes you want to review and merge. Pull requests are stored on
+`GitHub servers <https://github.com/pytest-dev/pytest/pulls>`_.
+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/>`_.
+
+Here is a simple overview, with pytest-specific bits:
+
+#. Fork the
+ `pytest GitHub repository <https://github.com/pytest-dev/pytest>`__. It's
+ fine to use ``pytest`` as your fork repository name because it will live
+ under your user.
+
+#. Clone your fork locally using `git <https://git-scm.com/>`_ and create a branch::
+
+ $ git clone git@github.com:YOUR_GITHUB_USERNAME/pytest.git
+ $ cd pytest
+ # now, create your own branch off "main":
+
+ $ git checkout -b your-bugfix-branch-name main
+
+ Given we have "major.minor.micro" version numbers, bug fixes will usually
+ be released in micro releases whereas features will be released in
+ minor releases and incompatible changes in major releases.
+
+ If you need some help with Git, follow this quick start
+ guide: https://git.wiki.kernel.org/index.php/QuickStart
+
+#. Install `pre-commit <https://pre-commit.com>`_ and its hook on the pytest repo::
+
+ $ pip install --user pre-commit
+ $ pre-commit install
+
+ Afterwards ``pre-commit`` will run whenever you commit.
+
+ https://pre-commit.com/ is a framework for managing and maintaining multi-language pre-commit hooks
+ to ensure code-style and code formatting is consistent.
+
+#. Install tox
+
+ Tox is used to run all the tests and will automatically setup virtualenvs
+ to run the tests in.
+ (will implicitly use https://virtualenv.pypa.io/en/latest/)::
+
+ $ pip install tox
+
+#. Run all the tests
+
+ You need to have Python 3.7 available in your system. Now
+ running tests is as simple as issuing this command::
+
+ $ tox -e linting,py37
+
+ This command will run tests via the "tox" tool against Python 3.7
+ 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 pass different options to ``tox``. For example, to run tests on Python 3.7 and pass options to pytest
+ (e.g. enter pdb on failure) to pytest you can do::
+
+ $ tox -e py37 -- --pdb
+
+ Or to only run tests in a particular test module on Python 3.7::
+
+ $ tox -e py37 -- 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::
+
+ $ python3 -m venv .venv
+ $ source .venv/bin/activate # Linux
+ $ .venv/Scripts/activate.bat # Windows
+ $ pip install -e ".[testing]"
+
+ Afterwards, you can edit the files and run pytest normally::
+
+ $ pytest testing/test_config.py
+
+#. Create a new changelog entry in ``changelog``. The file should be named ``<issueid>.<type>.rst``,
+ where *issueid* is the number of the issue related to the change and *type* is one of
+ ``feature``, ``improvement``, ``bugfix``, ``doc``, ``deprecation``, ``breaking``, ``vendor``
+ or ``trivial``. You may skip creating the changelog entry if the change doesn't affect the
+ documented behaviour of pytest.
+
+#. Add yourself to ``AUTHORS`` file if not there yet, in alphabetical order.
+
+#. Commit and push once your tests pass and you are happy with your change(s)::
+
+ $ git commit -a -m "<commit message>"
+ $ git push -u
+
+#. Finally, submit a pull request through the GitHub website using this data::
+
+ head-fork: YOUR_GITHUB_USERNAME/pytest
+ compare: your-branch-name
+
+ base-fork: pytest-dev/pytest
+ base: main
+
+
+Writing Tests
+~~~~~~~~~~~~~
+
+Writing tests for plugins or for pytest itself is often done using the `pytester fixture <https://docs.pytest.org/en/stable/reference/reference.html#pytester>`_, as a "black-box" test.
+
+For example, to ensure a simple test passes you can write:
+
+.. code-block:: python
+
+ def test_true_assertion(pytester):
+ pytester.makepyfile(
+ """
+ def test_foo():
+ assert True
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(failed=0, passed=1)
+
+
+Alternatively, it is possible to make checks based on the actual output of the termal using
+*glob-like* expressions:
+
+.. code-block:: python
+
+ def test_true_assertion(pytester):
+ pytester.makepyfile(
+ """
+ def test_foo():
+ assert False
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*assert False*", "*1 failed*"])
+
+When choosing a file where to write a new test, take a look at the existing files and see if there's
+one file which looks like a good fit. For example, a regression test about a bug in the ``--lf`` option
+should go into ``test_cacheprovider.py``, given that this option is implemented in ``cacheprovider.py``.
+If in doubt, go ahead and open a PR with your best guess and we can discuss this over the code.
+
+Joining the Development Team
+----------------------------
+
+Anyone who has successfully seen through a pull request which did not
+require any extra work from the development team to merge will
+themselves gain commit access if they so wish (if we forget to ask please send a friendly
+reminder). This does not mean there is any change in your contribution workflow:
+everyone goes through the same pull-request-and-review process and
+no-one merges their own pull requests unless already approved. It does however mean you can
+participate in the development process more fully since you can merge
+pull requests from other contributors yourself after having reviewed
+them.
+
+
+Backporting bug fixes for the next patch release
+------------------------------------------------
+
+Pytest makes 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.
+
+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
+ request, as described above. An exception to this is if the bug fix is not
+ applicable to ``main`` anymore.
+
+#. ``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:
+
+ nicoddemus merged commit 0f8b462 into pytest-dev:main
+
+#. ``git cherry-pick -x -m1 REVISION`` # use the revision you found above (``0f8b462``).
+
+#. Open a PR targeting ``1.2.x``:
+
+ * Prefix the message with ``[1.2.x]``.
+ * Delete the PR body, it usually contains a duplicate commit message.
+
+
+Who does the backporting
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+As mentioned above, bugs should first be fixed on ``main`` (except in rare occasions
+that a bug only happens in a previous release). So, who should do the backport procedure described
+above?
+
+1. If the bug was fixed by a core developer, it is the main responsibility of that core developer
+ to do the backport.
+2. However, often the merge is done by another maintainer, in which case it is nice of them to
+ do the backport procedure if they have the time.
+3. For bugs submitted by non-maintainers, it is expected that a core developer will to do
+ the backport, normally the one that merged the PR on ``main``.
+4. If a non-maintainers notices a bug which is fixed on ``main`` but has not been backported
+ (due to maintainers forgetting to apply the *needs backport* label, or just plain missing it),
+ they are also welcome to open a PR with the backport. The procedure is simple and really
+ helps with the maintenance of the project.
+
+All the above are not rules, but merely some guidelines/suggestions on what we should expect
+about backports.
+
+Handling stale issues/PRs
+-------------------------
+
+Stale issues/PRs are those where pytest contributors have asked for questions/changes
+and the authors didn't get around to answer/implement them yet after a somewhat long time, or
+the discussion simply died because people seemed to lose interest.
+
+There are many reasons why people don't answer questions or implement requested changes:
+they might get busy, lose interest, or just forget about it,
+but the fact is that this is very common in open source software.
+
+The pytest team really appreciates every issue and pull request, but being a high-volume project
+with many issues and pull requests being submitted daily, we try to reduce the number of stale
+issues and PRs by regularly closing them. When an issue/pull request is closed in this manner,
+it is by no means a dismissal of the topic being tackled by the issue/pull request, but it
+is just a way for us to clear up the queue and make the maintainers' work more manageable. Submitters
+can always reopen the issue/pull request in their own time later if it makes sense.
+
+When to close
+~~~~~~~~~~~~~
+
+Here are a few general rules the maintainers use deciding when to close issues/PRs because
+of lack of inactivity:
+
+* Issues labeled ``question`` or ``needs information``: closed after 14 days inactive.
+* Issues labeled ``proposal``: closed after six months inactive.
+* Pull requests: after one month, consider pinging the author, update linked issue, or consider closing. For pull requests which are nearly finished, the team should consider finishing it up and merging it.
+
+The above are **not hard rules**, but merely **guidelines**, and can be (and often are!) reviewed on a case-by-case basis.
+
+Closing pull requests
+~~~~~~~~~~~~~~~~~~~~~
+
+When closing a Pull Request, it needs to be acknowledging the time, effort, and interest demonstrated by the person which submitted it. As mentioned previously, it is not the intent of the team to dismiss a stalled pull request entirely but to merely to clear up our queue, so a message like the one below is warranted when closing a pull request that went stale:
+
+ Hi <contributor>,
+
+ First of all, we would like to thank you for your time and effort on working on this, the pytest team deeply appreciates it.
+
+ We noticed it has been awhile since you have updated this PR, however. pytest is a high activity project, with many issues/PRs being opened daily, so it is hard for us maintainers to track which PRs are ready for merging, for review, or need more attention.
+
+ So for those reasons we, think it is best to close the PR for now, but with the only intention to clean up our queue, it is by no means a rejection of your changes. We still encourage you to re-open this PR (it is just a click of a button away) when you are ready to get back to it.
+
+ Again we appreciate your time for working on this, and hope you might get back to this at a later time!
+
+ <bye>
+
+Closing Issues
+--------------
+
+When a pull request is submitted to fix an issue, add text like ``closes #XYZW`` to the PR description and/or commits (where ``XYZW`` is the issue number). See the `GitHub docs <https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword>`_ for more information.
+
+When an issue is due to user error (e.g. misunderstanding of a functionality), please politely explain to the user why the issue raised is really a non-issue and ask them to close the issue if they have no further questions. If the original requestor is unresponsive, the issue will be handled as described in the section `Handling stale issues/PRs`_ above.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/LICENSE b/testing/web-platform/tests/tools/third_party/pytest/LICENSE
new file mode 100644
index 0000000000..c3f1657fce
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2004 Holger Krekel and others
+
+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/testing/web-platform/tests/tools/third_party/pytest/OPENCOLLECTIVE.rst b/testing/web-platform/tests/tools/third_party/pytest/OPENCOLLECTIVE.rst
new file mode 100644
index 0000000000..8c1c90281e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/OPENCOLLECTIVE.rst
@@ -0,0 +1,44 @@
+==============
+OpenCollective
+==============
+
+pytest has a collective setup at `OpenCollective`_. This document describes how the core team manages
+OpenCollective-related activities.
+
+What is it
+==========
+
+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.
+
+Funds
+=====
+
+The OpenCollective funds donated to pytest will be used to fund overall maintenance,
+local sprints, merchandising (stickers to distribute in conferences for example), and future
+gatherings of pytest developers (sprints).
+
+`Core contributors`_ which are contributing on a continuous basis are free to submit invoices
+to bill maintenance hours using the platform. How much each contributor should request is still an
+open question, but we should use common sense and trust in the contributors, most of which know
+themselves in-person. A good rule of thumb is to bill the same amount as monthly payments
+contributors which participate in the `Tidelift`_ subscription. If in doubt, just ask.
+
+Admins
+======
+
+A few people have admin access to the OpenCollective dashboard to make changes. Those people
+are part of the `@pytest-dev/opencollective-admins`_ team.
+
+`Core contributors`_ interested in helping out with OpenCollective maintenance are welcome! We don't
+expect much work here other than the occasional approval of expenses from other core contributors.
+Just drop a line to one of the `@pytest-dev/opencollective-admins`_ or use the mailing list.
+
+
+.. _`OpenCollective`: https://opencollective.com/pytest
+.. _`Tidelift`: https://tidelift.com
+.. _`core contributors`: https://github.com/orgs/pytest-dev/teams/core/members
+.. _`@pytest-dev/opencollective-admins`: https://github.com/orgs/pytest-dev/teams/opencollective-admins/members
diff --git a/testing/web-platform/tests/tools/third_party/pytest/README.rst b/testing/web-platform/tests/tools/third_party/pytest/README.rst
new file mode 100644
index 0000000000..1473376517
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/README.rst
@@ -0,0 +1,167 @@
+.. image:: https://github.com/pytest-dev/pytest/raw/main/doc/en/img/pytest_logo_curves.svg
+ :target: https://docs.pytest.org/en/stable/
+ :align: center
+ :height: 200
+ :alt: pytest
+
+
+------
+
+.. image:: https://img.shields.io/pypi/v/pytest.svg
+ :target: https://pypi.org/project/pytest/
+
+.. image:: https://img.shields.io/conda/vn/conda-forge/pytest.svg
+ :target: https://anaconda.org/conda-forge/pytest
+
+.. image:: https://img.shields.io/pypi/pyversions/pytest.svg
+ :target: https://pypi.org/project/pytest/
+
+.. image:: https://codecov.io/gh/pytest-dev/pytest/branch/main/graph/badge.svg
+ :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://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
+
+.. image:: https://readthedocs.org/projects/pytest/badge/?version=latest
+ :target: https://pytest.readthedocs.io/en/latest/?badge=latest
+ :alt: Documentation Status
+
+.. image:: https://img.shields.io/badge/Discord-pytest--dev-blue
+ :target: https://discord.com/invite/pytest-dev
+ :alt: Discord
+
+.. image:: https://img.shields.io/badge/Libera%20chat-%23pytest-orange
+ :target: https://web.libera.chat/#pytest
+ :alt: Libera chat
+
+
+The ``pytest`` framework makes it easy to write small tests, yet
+scales to support complex functional testing for applications and libraries.
+
+An example of a simple test:
+
+.. code-block:: python
+
+ # content of test_sample.py
+ def inc(x):
+ return x + 1
+
+
+ def test_answer():
+ assert inc(3) == 5
+
+
+To execute it::
+
+ $ pytest
+ ============================= test session starts =============================
+ collected 1 items
+
+ test_sample.py F
+
+ ================================== FAILURES ===================================
+ _________________________________ test_answer _________________________________
+
+ def test_answer():
+ > assert inc(3) == 5
+ E assert 4 == 5
+ E + where 4 = inc(3)
+
+ test_sample.py:5: AssertionError
+ ========================== 1 failed in 0.04 seconds ===========================
+
+
+Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See `getting-started <https://docs.pytest.org/en/stable/getting-started.html#our-first-test-run>`_ for more examples.
+
+
+Features
+--------
+
+- Detailed info on failing `assert statements <https://docs.pytest.org/en/stable/how-to/assert.html>`_ (no need to remember ``self.assert*`` names)
+
+- `Auto-discovery
+ <https://docs.pytest.org/en/stable/explanation/goodpractices.html#python-test-discovery>`_
+ of test modules and functions
+
+- `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
+
+- Python 3.6+ and PyPy3
+
+- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
+
+
+Documentation
+-------------
+
+For full documentation, including installation, tutorials and PDF documents, please see https://docs.pytest.org/en/stable/.
+
+
+Bugs/Requests
+-------------
+
+Please use the `GitHub issue tracker <https://github.com/pytest-dev/pytest/issues>`_ to submit bugs or request features.
+
+
+Changelog
+---------
+
+Consult the `Changelog <https://docs.pytest.org/en/stable/changelog.html>`__ page for fixes and enhancements of each version.
+
+
+Support pytest
+--------------
+
+`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.
+
+See more details in the `pytest collective`_.
+
+.. _Open Collective: https://opencollective.com
+.. _pytest collective: https://opencollective.com/pytest
+
+
+pytest for enterprise
+---------------------
+
+Available as part of the Tidelift Subscription.
+
+The maintainers of pytest and thousands of other packages are working with Tidelift to deliver commercial support and
+maintenance for the open source dependencies you use to build your applications.
+Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use.
+
+`Learn more. <https://tidelift.com/subscription/pkg/pypi-pytest?utm_source=pypi-pytest&utm_medium=referral&utm_campaign=enterprise&utm_term=repo>`_
+
+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/testing/web-platform/tests/tools/third_party/pytest/RELEASING.rst b/testing/web-platform/tests/tools/third_party/pytest/RELEASING.rst
new file mode 100644
index 0000000000..25ce90d0f6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/RELEASING.rst
@@ -0,0 +1,173 @@
+Release Procedure
+-----------------
+
+Our current policy for releasing is to aim for a bug-fix release every few weeks and a minor release every 2-3 months. The idea
+is to get fixes and new features out instead of trying to cram a ton of features into a release and by consequence
+taking a lot of time to make a new one.
+
+The git commands assume the following remotes are setup:
+
+* ``origin``: your own fork of the repository.
+* ``upstream``: the ``pytest-dev/pytest`` official repository.
+
+Preparing: Automatic Method
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We have developed an automated workflow for releases, that uses GitHub workflows and is triggered
+by `manually running <https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow>`__
+the `prepare-release-pr workflow <https://github.com/pytest-dev/pytest/actions/workflows/prepare-release-pr.yml>`__
+on GitHub Actions.
+
+The automation will decide the new version number based on the following criteria:
+
+- If the "major release" input is set to "yes", release a new major release
+ (e.g. 7.0.0 -> 8.0.0)
+- If there are any ``.feature.rst`` or ``.breaking.rst`` files in the
+ ``changelog`` directory, release a new minor release (e.g. 7.0.0 -> 7.1.0)
+- Otherwise, release a bugfix release (e.g. 7.0.0 -> 7.0.1)
+- If the "prerelease" input is set, append the string to the version number
+ (e.g. 7.0.0 -> 8.0.0rc1), if "major" is set, and "prerelease" is set to `rc1`)
+
+Bug-fix and minor releases
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Bug-fix and minor releases are always done from a maintenance branch. First,
+consider double-checking the ``changelog`` directory to see if there are any
+breaking changes or new features.
+
+For a new minor release, first create a new maintenance branch from ``main``::
+
+ git fetch --all
+ git branch 7.1.x upstream/main
+ git push upstream 7.1.x
+
+Then, trigger the workflow with the following inputs:
+
+- branch: **7.1.x**
+- major release: **no**
+- prerelease: empty
+
+Or via the commandline using `GitHub's cli <https://github.com/cli/cli>`__::
+
+ gh workflow run prepare-release-pr.yml -f branch=7.1.x -f major=no -f prerelease=
+
+Where ``7.1.x`` is the maintenance branch for the ``7.1`` series. The automated
+workflow will publish a PR for a branch ``release-7.1.0``.
+
+Similarly, for a bug-fix release, use the existing maintenance branch and
+trigger the workflow with e.g. ``branch: 7.0.x`` to get a new ``release-7.0.1``
+PR.
+
+Major releases
+^^^^^^^^^^^^^^
+
+1. Create a new maintenance branch from ``main``::
+
+ git fetch --all
+ git branch 8.0.x upstream/main
+ git push upstream 8.0.x
+
+2. Trigger the workflow with the following inputs:
+
+ - branch: **8.0.x**
+ - major release: **yes**
+ - prerelease: empty
+
+Or via the commandline::
+
+ gh workflow run prepare-release-pr.yml -f branch=8.0.x -f major=yes -f prerelease=
+
+The automated workflow will publish a PR for a branch ``release-8.0.0``.
+
+At this point on, this follows the same workflow as other maintenance branches: bug-fixes are merged
+into ``main`` and ported back to the maintenance branch, even for release candidates.
+
+Release candidates
+^^^^^^^^^^^^^^^^^^
+
+To release a release candidate, set the "prerelease" input to the version number
+suffix to use. To release a ``8.0.0rc1``, proceed like under "major releases", but set:
+
+- branch: 8.0.x
+- major release: yes
+- prerelease: **rc1**
+
+Or via the commandline::
+
+ gh workflow run prepare-release-pr.yml -f branch=8.0.x -f major=yes -f prerelease=rc1
+
+The automated workflow will publish a PR for a branch ``release-8.0.0rc1``.
+
+**A note about release candidates**
+
+During release candidates we can merge small improvements into
+the maintenance branch before releasing the final major version, however we must take care
+to avoid introducing big changes at this stage.
+
+Preparing: Manual Method
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+**Important**: pytest releases must be prepared on **Linux** because the docs and examples expect
+to be executed on that platform.
+
+To release a version ``MAJOR.MINOR.PATCH``, follow these steps:
+
+#. For major and minor releases, create a new branch ``MAJOR.MINOR.x`` from
+ ``upstream/main`` and push it to ``upstream``.
+
+#. Create a branch ``release-MAJOR.MINOR.PATCH`` from the ``MAJOR.MINOR.x`` branch.
+
+ Ensure your are updated and in a clean working tree.
+
+#. Using ``tox``, generate docs, changelog, announcements::
+
+ $ tox -e release -- MAJOR.MINOR.PATCH
+
+ This will generate a commit with all the changes ready for pushing.
+
+#. Open a PR for the ``release-MAJOR.MINOR.PATCH`` branch targeting ``MAJOR.MINOR.x``.
+
+
+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::
+
+ 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
+
+ Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
+
+#. Merge the PR.
+
+#. Cherry-pick the CHANGELOG / announce files to the ``main`` branch::
+
+ git fetch --all --prune
+ 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
+ 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
+
+#. Send an email announcement with the contents from::
+
+ doc/en/announce/release-<VERSION>.rst
+
+ To the following mailing lists:
+
+ * pytest-dev@python.org (all releases)
+ * python-announce-list@python.org (all releases)
+ * testing-in-python@lists.idyll.org (only major/minor releases)
+
+ And announce it on `Twitter <https://twitter.com/>`_ with the ``#pytest`` hashtag.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/TIDELIFT.rst b/testing/web-platform/tests/tools/third_party/pytest/TIDELIFT.rst
new file mode 100644
index 0000000000..2fe25841c3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/TIDELIFT.rst
@@ -0,0 +1,60 @@
+========
+Tidelift
+========
+
+pytest 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:
+
+* `@asottile`_
+* `@nicoddemus`_
+* `@The-Compiler`_
+
+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
+
+.. _`@asottile`: https://github.com/asottile
+.. _`@nicoddemus`: https://github.com/nicoddemus
+.. _`@The-Compiler`: https://github.com/The-Compiler
diff --git a/testing/web-platform/tests/tools/third_party/pytest/bench/bench.py b/testing/web-platform/tests/tools/third_party/pytest/bench/bench.py
new file mode 100644
index 0000000000..c40fc8636c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/bench/bench.py
@@ -0,0 +1,13 @@
+import sys
+
+if __name__ == "__main__":
+ import cProfile
+ import pytest # NOQA
+ import pstats
+
+ script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
+ cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
+ p = pstats.Stats("prof")
+ p.strip_dirs()
+ p.sort_stats("cumulative")
+ print(p.print_stats(500))
diff --git a/testing/web-platform/tests/tools/third_party/pytest/bench/bench_argcomplete.py b/testing/web-platform/tests/tools/third_party/pytest/bench/bench_argcomplete.py
new file mode 100644
index 0000000000..335733df72
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/bench/bench_argcomplete.py
@@ -0,0 +1,19 @@
+# 10000 iterations, just for relative comparison
+# 2.7.5 3.3.2
+# FilesCompleter 75.1109 69.2116
+# FastFilesCompleter 0.7383 1.0760
+import timeit
+
+imports = [
+ "from argcomplete.completers import FilesCompleter as completer",
+ "from _pytest._argcomplete import FastFilesCompleter as completer",
+]
+
+count = 1000 # only a few seconds
+setup = "%s\nfc = completer()"
+run = 'fc("/d")'
+
+
+if __name__ == "__main__":
+ print(timeit.timeit(run, setup=setup % imports[0], number=count))
+ print(timeit.timeit(run, setup=setup % imports[1], number=count))
diff --git a/testing/web-platform/tests/tools/third_party/pytest/bench/empty.py b/testing/web-platform/tests/tools/third_party/pytest/bench/empty.py
new file mode 100644
index 0000000000..4e7371b6f8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/bench/empty.py
@@ -0,0 +1,2 @@
+for i in range(1000):
+ exec("def test_func_%d(): pass" % i)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/bench/manyparam.py b/testing/web-platform/tests/tools/third_party/pytest/bench/manyparam.py
new file mode 100644
index 0000000000..1226c73bd9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/bench/manyparam.py
@@ -0,0 +1,14 @@
+import pytest
+
+
+@pytest.fixture(scope="module", params=range(966))
+def foo(request):
+ return request.param
+
+
+def test_it(foo):
+ pass
+
+
+def test_it2(foo):
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/bench/skip.py b/testing/web-platform/tests/tools/third_party/pytest/bench/skip.py
new file mode 100644
index 0000000000..f0c9d1ddbe
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/bench/skip.py
@@ -0,0 +1,9 @@
+import pytest
+
+SKIP = True
+
+
+@pytest.mark.parametrize("x", range(5000))
+def test_foo(x):
+ if SKIP:
+ pytest.skip("heh")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/bench/unit_test.py b/testing/web-platform/tests/tools/third_party/pytest/bench/unit_test.py
new file mode 100644
index 0000000000..ad52069dbf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/bench/unit_test.py
@@ -0,0 +1,13 @@
+from unittest import TestCase # noqa: F401
+
+for i in range(15000):
+ exec(
+ f"""
+class Test{i}(TestCase):
+ @classmethod
+ def setUpClass(cls): pass
+ def test_1(self): pass
+ def test_2(self): pass
+ def test_3(self): pass
+"""
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/bench/xunit.py b/testing/web-platform/tests/tools/third_party/pytest/bench/xunit.py
new file mode 100644
index 0000000000..3a77dcdce4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/bench/xunit.py
@@ -0,0 +1,11 @@
+for i in range(5000):
+ exec(
+ f"""
+class Test{i}:
+ @classmethod
+ def setup_class(cls): pass
+ def test_1(self): pass
+ def test_2(self): pass
+ def test_3(self): pass
+"""
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/changelog/README.rst b/testing/web-platform/tests/tools/third_party/pytest/changelog/README.rst
new file mode 100644
index 0000000000..6d026f57ef
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/changelog/README.rst
@@ -0,0 +1,37 @@
+This directory contains "newsfragments" which are short files that contain a small **ReST**-formatted
+text that will be added to the next ``CHANGELOG``.
+
+The ``CHANGELOG`` will be read by **users**, so this description should be aimed to pytest users
+instead of describing internal changes which are only relevant to the developers.
+
+Make sure to use full sentences in the **past or present tense** and use punctuation, examples::
+
+ Improved verbose diff output with sequences.
+
+ Terminal summary statistics now use multiple colors.
+
+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).
+* ``bugfix``: fixes a bug.
+* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
+* ``deprecation``: feature deprecation.
+* ``breaking``: a change which may break existing suites, such as feature removal or behavior change.
+* ``vendor``: changes in packages vendored in pytest.
+* ``trivial``: fixing a small typo or internal change that might be noteworthy.
+
+So for example: ``123.feature.rst``, ``456.bugfix.rst``.
+
+If your PR fixes an issue, use that number here. If there is no issue,
+then after you submit the PR and get the PR number you can add a
+changelog using that instead.
+
+If you are not sure what issue type to use, don't hesitate to ask in your PR.
+
+``towncrier`` preserves multiple paragraphs and formatting (code blocks, lists, and so on), but for entries
+other than ``features`` it is usually better to stick to a single paragraph to keep it concise.
+
+You can also run ``tox -e docs`` to build the documentation
+with the draft changelog (``doc/en/_build/html/changelog.html``) if you want to get a preview of how your change will look in the final release notes.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/changelog/_template.rst b/testing/web-platform/tests/tools/third_party/pytest/changelog/_template.rst
new file mode 100644
index 0000000000..5de4ae97ea
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/changelog/_template.rst
@@ -0,0 +1,40 @@
+{% for section in sections %}
+{% set underline = "-" %}
+{% if section %}
+{{section}}
+{{ underline * section|length }}{% set underline = "~" %}
+
+{% 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]|dictsort(by='value') %}
+{% set issue_joiner = joiner(', ') %}
+- {% for value in values|sort %}{{ issue_joiner() }}`{{ value }} <https://github.com/pytest-dev/pytest/issues/{{ value[1:] }}>`_{% endfor %}: {{ text }}
+
+
+{% endfor %}
+{% else %}
+- {{ sections[section][category]['']|sort|join(', ') }}
+
+
+{% endif %}
+{% if sections[section][category]|length == 0 %}
+
+No significant changes.
+
+
+{% else %}
+{% endif %}
+{% endfor %}
+{% else %}
+
+No significant changes.
+
+
+{% endif %}
+{% endfor %}
diff --git a/testing/web-platform/tests/tools/third_party/pytest/codecov.yml b/testing/web-platform/tests/tools/third_party/pytest/codecov.yml
new file mode 100644
index 0000000000..f1cc869733
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/codecov.yml
@@ -0,0 +1,6 @@
+# reference: https://docs.codecov.io/docs/codecovyml-reference
+coverage:
+ status:
+ patch: true
+ project: false
+comment: false
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/Makefile b/testing/web-platform/tests/tools/third_party/pytest/doc/en/Makefile
new file mode 100644
index 0000000000..f2db689121
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/Makefile
@@ -0,0 +1,43 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+
+REGENDOC_ARGS := \
+ --normalize "/[ \t]+\n/\n/" \
+ --normalize "~\$$REGENDOC_TMPDIR~/home/sweet/project~" \
+ --normalize "~/path/to/example~/home/sweet/project~" \
+ --normalize "/in \d.\d\ds/in 0.12s/" \
+ --normalize "@/tmp/pytest-of-.*/pytest-\d+@PYTEST_TMPDIR@" \
+ --normalize "@pytest-(\d+)\\.[^ ,]+@pytest-\1.x.y@" \
+ --normalize "@py-(\d+)\\.[^ ,]+@py-\1.x.y@" \
+ --normalize "@pluggy-(\d+)\\.[.\d,]+@pluggy-\1.x.y@" \
+ --normalize "@hypothesis-(\d+)\\.[.\d,]+@hypothesis-\1.x.y@" \
+ --normalize "@Python (\d+)\\.[^ ,]+@Python \1.x.y@"
+
+regen: REGENDOC_FILES:=*.rst */*.rst
+regen:
+# need to reset cachedir to the non-tox default
+ PYTHONDONTWRITEBYTECODE=1 \
+ PYTEST_ADDOPTS="-pno:hypothesis -p no:hypothesispytest -Wignore::pytest.PytestUnknownMarkWarning -o cache_dir=.pytest_cache" \
+ COLUMNS=76 \
+ regendoc --update ${REGENDOC_FILES} ${REGENDOC_ARGS}
+
+.PHONY: regen
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/globaltoc.html b/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/globaltoc.html
new file mode 100644
index 0000000000..7c595e7ebf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/globaltoc.html
@@ -0,0 +1,34 @@
+<h3>Contents</h3>
+
+<ul>
+ <li><a href="{{ pathto('index') }}">Home</a></li>
+
+ <li><a href="{{ pathto('getting-started') }}">Get started</a></li>
+ <li><a href="{{ pathto('how-to/index') }}">How-to guides</a></li>
+ <li><a href="{{ pathto('reference/index') }}">Reference guides</a></li>
+ <li><a href="{{ pathto('explanation/index') }}">Explanation</a></li>
+ <li><a href="{{ pathto('contents') }}">Complete table of contents</a></li>
+ <li><a href="{{ pathto('example/index') }}">Library of examples</a></li>
+</ul>
+
+<h3>About the project</h3>
+
+<ul>
+ <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>
+ <li><a href="{{ pathto('contact') }}">Contact Channels</a></li>
+</ul>
+
+{%- if display_toc %}
+ <hr>
+ {{ toc }}
+{%- endif %}
+
+<hr>
+<a href="{{ pathto('genindex') }}">Index</a>
+<hr>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/layout.html b/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/layout.html
new file mode 100644
index 0000000000..f7096eaaa5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/layout.html
@@ -0,0 +1,52 @@
+{#
+
+ Copied from:
+
+ https://raw.githubusercontent.com/pallets/pallets-sphinx-themes/b0c6c41849b4e15cbf62cc1d95c05ef2b3e155c8/src/pallets_sphinx_themes/themes/pocoo/layout.html
+
+ And removed the warning version (see #7331).
+
+#}
+
+{% extends "basic/layout.html" %}
+
+{% set metatags %}
+ {{- metatags }}
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+{%- endset %}
+
+{% block extrahead %}
+ {%- if page_canonical_url %}
+ <link rel="canonical" href="{{ page_canonical_url }}">
+ {%- endif %}
+ <script>DOCUMENTATION_OPTIONS.URL_ROOT = '{{ url_root }}';</script>
+ {{ super() }}
+{%- endblock %}
+
+{% block sidebarlogo %}
+ {% if pagename != "index" or theme_index_sidebar_logo %}
+ {{ super() }}
+ {% endif %}
+{% endblock %}
+
+{% block relbar2 %}{% endblock %}
+
+{% block sidebar2 %}
+ <span id="sidebar-top"></span>
+ {{- super() }}
+{%- endblock %}
+
+{% block footer %}
+ {{ super() }}
+ {%- if READTHEDOCS and not readthedocs_docsearch %}
+ <script>
+ if (typeof READTHEDOCS_DATA !== 'undefined') {
+ if (!READTHEDOCS_DATA.features) {
+ READTHEDOCS_DATA.features = {};
+ }
+ READTHEDOCS_DATA.features.docsearch_disabled = true;
+ }
+ </script>
+ {%- endif %}
+ {{ js_tag("_static/version_warning_offset.js") }}
+{% endblock %}
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/links.html b/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/links.html
new file mode 100644
index 0000000000..c253ecabfd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/links.html
@@ -0,0 +1,7 @@
+<h3>Useful Links</h3>
+<ul>
+ <li><a href="https://pypi.org/project/pytest/">pytest @ PyPI</a></li>
+ <li><a href="https://github.com/pytest-dev/pytest/">pytest @ GitHub</a></li>
+ <li><a href="https://github.com/pytest-dev/pytest/issues">Issue Tracker</a></li>
+ <li><a href="https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf">PDF Documentation</a>
+</ul>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/relations.html b/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/relations.html
new file mode 100644
index 0000000000..3bbcde85bb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/relations.html
@@ -0,0 +1,19 @@
+<h3>Related Topics</h3>
+<ul>
+ <li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul>
+ {%- for parent in parents %}
+ <li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul>
+ {%- endfor %}
+ {%- if prev %}
+ <li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter')
+ }}">{{ prev.title }}</a></li>
+ {%- endif %}
+ {%- if next %}
+ <li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter')
+ }}">{{ next.title }}</a></li>
+ {%- endif %}
+ {%- for parent in parents %}
+ </ul></li>
+ {%- endfor %}
+ </ul></li>
+</ul>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/sidebarintro.html b/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/sidebarintro.html
new file mode 100644
index 0000000000..ae860c172f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/sidebarintro.html
@@ -0,0 +1,5 @@
+<h3>About pytest</h3>
+<p>
+ pytest is a mature full-featured Python testing tool that helps
+ you write better programs.
+</p>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/slim_searchbox.html b/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/slim_searchbox.html
new file mode 100644
index 0000000000..e98ad4ed90
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/_templates/slim_searchbox.html
@@ -0,0 +1,15 @@
+{#
+ basic/searchbox.html with heading removed.
+#}
+{%- if pagename != "search" and builder != "singlehtml" %}
+<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="submit" value="{{ _('Go') }}" />
+ </form>
+ </div>
+</div>
+<script type="text/javascript">$('#searchbox').show(0);</script>
+{%- endif %}
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/adopt.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/adopt.rst
new file mode 100644
index 0000000000..13d82bf011
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/adopt.rst
@@ -0,0 +1,78 @@
+:orphan:
+
+.. warnings about this file not being included in any toctree will be suppressed by :orphan:
+
+
+April 2015 is "adopt pytest month"
+=============================================
+
+Are you an enthusiastic pytest user, the local testing guru in your workplace? Or are you considering using pytest for your open source project, but not sure how to get started? Then you may be interested in "adopt pytest month"!
+
+We will pair experienced pytest users with open source projects, for a month's effort of getting new development teams started with pytest.
+
+In 2015 we are trying this for the first time. In February and March 2015 we will gather volunteers on both sides, in April we will do the work, and in May we will evaluate how it went. This effort is being coordinated by Brianna Laugher. If you have any questions or comments, you can raise them on the `@pytestdotorg twitter account <https://twitter.com/pytestdotorg>`_\, the :issue:`issue tracker <676>` or the `pytest-dev mailing list`_.
+
+
+.. _`pytest-dev mailing list`: https://mail.python.org/mailman/listinfo/pytest-dev
+
+
+The ideal pytest helper
+-----------------------------------------
+
+ - will be able to commit 2-4 hours a week to working with their particular project (this might involve joining their mailing list, installing the software and exploring any existing tests, offering advice, writing some example tests)
+ - feels confident in using pytest (e.g. has explored command line options, knows how to write parametrized tests, has an idea about conftest contents)
+ - does not need to be an expert in every aspect!
+
+Pytest helpers, sign up here! (preferably in February, hard deadline 22 March)
+
+
+
+The ideal partner project
+-----------------------------------------
+
+ - is open source, and predominantly written in Python
+ - has an automated/documented install process for developers
+ - has more than one core developer
+ - has at least one official release (e.g. is available on pypi)
+ - has the support of the core development team, in trying out pytest adoption
+ - has no tests... or 100% test coverage... or somewhere in between!
+
+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?
+
+Progressive success might look like:
+
+ - tests can be run (by pytest) without errors (there may be failures)
+ - tests can be run (by pytest) without failures
+ - test runner is integrated into CI server
+ - existing tests are rewritten to take advantage of pytest features - this can happen in several iterations, for example:
+ - changing to native assert_ statements (pycmd_ has a script to help with that, ``pyconvert_unittest.py``)
+ - changing `setUp/tearDown methods`_ to fixtures_
+ - adding markers_
+ - other changes to reduce boilerplate
+ - assess needs for future tests to be written, e.g. new fixtures, distributed_ testing tweaks
+
+"Success" should also include that the development team feels comfortable with their knowledge of how to use pytest. In fact this is probably more important than anything else. So spending a lot of time on communication, giving examples, etc will probably be important - both in running the tests, and in writing them.
+
+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
+.. _`setUp/tearDown methods`: xunit_setup.html
+.. _fixtures: fixture.html
+.. _markers: mark.html
+.. _distributed: xdist.html
+
+
+Other ways to help
+-----------------------------------------
+
+Promote! Do your favourite open source Python projects use pytest? If not, why not tell them about this page?
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/index.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/index.rst
new file mode 100644
index 0000000000..9505b0b9e4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/index.rst
@@ -0,0 +1,154 @@
+
+Release announcements
+===========================================
+
+.. toctree::
+ :maxdepth: 2
+
+
+ release-7.0.1
+ release-7.0.0
+ release-7.0.0rc1
+ release-6.2.5
+ release-6.2.4
+ release-6.2.3
+ release-6.2.2
+ release-6.2.1
+ release-6.2.0
+ release-6.1.2
+ release-6.1.1
+ release-6.1.0
+ release-6.0.2
+ release-6.0.1
+ release-6.0.0
+ release-6.0.0rc1
+ release-5.4.3
+ release-5.4.2
+ release-5.4.1
+ release-5.4.0
+ release-5.3.5
+ release-5.3.4
+ release-5.3.3
+ release-5.3.2
+ release-5.3.1
+ release-5.3.0
+ release-5.2.4
+ release-5.2.3
+ release-5.2.2
+ release-5.2.1
+ release-5.2.0
+ release-5.1.3
+ release-5.1.2
+ release-5.1.1
+ release-5.1.0
+ release-5.0.1
+ release-5.0.0
+ release-4.6.9
+ release-4.6.8
+ release-4.6.7
+ release-4.6.6
+ release-4.6.5
+ release-4.6.4
+ release-4.6.3
+ release-4.6.2
+ release-4.6.1
+ release-4.6.0
+ release-4.5.0
+ release-4.4.2
+ release-4.4.1
+ release-4.4.0
+ release-4.3.1
+ release-4.3.0
+ release-4.2.1
+ release-4.2.0
+ release-4.1.1
+ release-4.1.0
+ release-4.0.2
+ release-4.0.1
+ release-4.0.0
+ release-3.10.1
+ release-3.10.0
+ release-3.9.3
+ release-3.9.2
+ release-3.9.1
+ release-3.9.0
+ release-3.8.2
+ release-3.8.1
+ release-3.8.0
+ release-3.7.4
+ release-3.7.3
+ release-3.7.2
+ release-3.7.1
+ release-3.7.0
+ release-3.6.4
+ release-3.6.3
+ release-3.6.2
+ release-3.6.1
+ release-3.6.0
+ release-3.5.1
+ release-3.5.0
+ release-3.4.2
+ release-3.4.1
+ release-3.4.0
+ release-3.3.2
+ release-3.3.1
+ release-3.3.0
+ release-3.2.5
+ release-3.2.4
+ release-3.2.3
+ release-3.2.2
+ release-3.2.1
+ release-3.2.0
+ release-3.1.3
+ release-3.1.2
+ release-3.1.1
+ release-3.1.0
+ release-3.0.7
+ release-3.0.6
+ release-3.0.5
+ release-3.0.4
+ release-3.0.3
+ release-3.0.2
+ release-3.0.1
+ release-3.0.0
+ sprint2016
+ release-2.9.2
+ release-2.9.1
+ release-2.9.0
+ release-2.8.7
+ release-2.8.6
+ release-2.8.5
+ release-2.8.4
+ release-2.8.3
+ release-2.8.2
+ release-2.7.2
+ release-2.7.1
+ release-2.7.0
+ release-2.6.3
+ release-2.6.2
+ release-2.6.1
+ release-2.6.0
+ release-2.5.2
+ release-2.5.1
+ release-2.5.0
+ release-2.4.2
+ release-2.4.1
+ release-2.4.0
+ release-2.3.5
+ release-2.3.4
+ release-2.3.3
+ release-2.3.2
+ release-2.3.1
+ release-2.3.0
+ release-2.2.4
+ release-2.2.2
+ release-2.2.1
+ release-2.2.0
+ release-2.1.3
+ release-2.1.2
+ release-2.1.1
+ release-2.1.0
+ release-2.0.3
+ release-2.0.2
+ release-2.0.1
+ release-2.0.0
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.0.rst
new file mode 100644
index 0000000000..ecb1a1db98
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.0.rst
@@ -0,0 +1,129 @@
+py.test 2.0.0: asserts++, unittest++, reporting++, config++, docs++
+===========================================================================
+
+Welcome to pytest-2.0.0, a major new release of "py.test", the rapid
+easy Python testing tool. There are many new features and enhancements,
+see below for summary and detailed lists. A lot of long-deprecated code
+has been removed, resulting in a much smaller and cleaner
+implementation. See the new docs with examples here:
+
+ http://pytest.org/en/stable/index.html
+
+A note on packaging: pytest used to part of the "py" distribution up
+until version py-1.3.4 but this has changed now: pytest-2.0.0 only
+contains py.test related code and is expected to be backward-compatible
+to existing test code. If you want to install pytest, just type one of::
+
+ pip install -U pytest
+ easy_install -U pytest
+
+Many thanks to all issue reporters and people asking questions or
+complaining. Particular thanks to Floris Bruynooghe and Ronny Pfannschmidt
+for their great coding contributions and many others for feedback and help.
+
+best,
+holger krekel
+
+
+New Features
+-----------------------
+
+- new invocations through Python interpreter and from Python::
+
+ python -m pytest # on all pythons >= 2.5
+
+ or from a python program::
+
+ import pytest ; pytest.main(arglist, pluginlist)
+
+ see http://pytest.org/en/stable/how-to/usage.html for details.
+
+- new and better reporting information in assert expressions
+ if comparing lists, sequences or strings.
+
+ see http://pytest.org/en/stable/how-to/assert.html#newreport
+
+- new configuration through ini-files (setup.cfg or tox.ini recognized),
+ for example::
+
+ [pytest]
+ norecursedirs = .hg data* # don't ever recurse in such dirs
+ addopts = -x --pyargs # add these command line options by default
+
+ see http://pytest.org/en/stable/reference/customize.html
+
+- improved standard unittest support. In general py.test should now
+ better be able to run custom unittest.TestCases like twisted trial
+ or Django based TestCases. Also you can now run the tests of an
+ installed 'unittest' package with py.test::
+
+ py.test --pyargs unittest
+
+- new "-q" option which decreases verbosity and prints a more
+ nose/unittest-style "dot" output.
+
+- many many more detailed improvements details
+
+Fixes
+-----------------------
+
+- fix issue126 - introduce py.test.set_trace() to trace execution via
+ PDB during the running of tests even if capturing is ongoing.
+- fix issue124 - make reporting more resilient against tests opening
+ files on filedescriptor 1 (stdout).
+- fix issue109 - sibling conftest.py files will not be loaded.
+ (and Directory collectors cannot be customized anymore from a Directory's
+ conftest.py - this needs to happen at least one level up).
+- fix issue88 (finding custom test nodes from command line arg)
+- fix issue93 stdout/stderr is captured while importing conftest.py
+- fix bug: unittest collected functions now also can have "pytestmark"
+ applied at class/module level
+
+Important Notes
+--------------------
+
+* The usual way in pre-2.0 times to use py.test in python code was
+ to import "py" and then e.g. use "py.test.raises" for the helper.
+ This remains valid and is not planned to be deprecated. However,
+ in most examples and internal code you'll find "import pytest"
+ and "pytest.raises" used as the recommended default way.
+
+* pytest now first performs collection of the complete test suite
+ before running any test. This changes for example the semantics of when
+ pytest_collectstart/pytest_collectreport are called. Some plugins may
+ need upgrading.
+
+* The pytest package consists of a 400 LOC core.py and about 20 builtin plugins,
+ summing up to roughly 5000 LOCs, including docstrings. To be fair, it also
+ uses generic code from the "pylib", and the new "py" package to help
+ with filesystem and introspection/code manipulation.
+
+(Incompatible) Removals
+-----------------------------
+
+- py.test.config is now only available if you are in a test run.
+
+- the following (mostly already deprecated) functionality was removed:
+
+ - removed support for Module/Class/... collection node definitions
+ 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
+ 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.
+
+- py.test.collect.Directory does not exist anymore and it
+ is not possible to provide an own "Directory" object.
+ If you have used this and don't know what to do, get
+ in contact. We'll figure something out.
+
+ Note that pytest_collect_directory() is still called but
+ any return value will be ignored. This allows to keep
+ old code working that performed for example "py.test.skip()"
+ in collect() to prevent recursion into directory trees
+ if a certain dependency or command line option is missing.
+
+
+see :ref:`changelog` for more detailed changes.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.1.rst
new file mode 100644
index 0000000000..4ff3e9f550
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.1.rst
@@ -0,0 +1,67 @@
+py.test 2.0.1: bug fixes
+===========================================================================
+
+Welcome to pytest-2.0.1, a maintenance and bug fix release of pytest,
+a mature testing tool for Python, supporting CPython 2.4-3.2, Jython
+and latest PyPy interpreters. See extensive docs with tested examples here:
+
+ http://pytest.org/
+
+If you want to install or upgrade pytest, just type one of::
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+Many thanks to all issue reporters and people asking questions or
+complaining. Particular thanks to Floris Bruynooghe and Ronny Pfannschmidt
+for their great coding contributions and many others for feedback and help.
+
+best,
+holger krekel
+
+Changes between 2.0.0 and 2.0.1
+----------------------------------------------
+
+- refine and unify initial capturing so that it works nicely
+ even if the logging module is used on an early-loaded conftest.py
+ file or plugin.
+- fix issue12 - show plugin versions with "--version" and
+ "--traceconfig" and also document how to add extra information
+ to reporting test header
+- fix issue17 (import-* reporting issue on python3) by
+ requiring py>1.4.0 (1.4.1 is going to include it)
+- fix issue10 (numpy arrays truth checking) by refining
+ assertion interpretation in py lib
+- fix issue15: make nose compatibility tests compatible
+ with python3 (now that nose-1.0 supports python3)
+- remove somewhat surprising "same-conftest" detection because
+ it ignores conftest.py when they appear in several subdirs.
+- improve assertions ("not in"), thanks Floris Bruynooghe
+- improve behaviour/warnings when running on top of "python -OO"
+ (assertions and docstrings are turned off, leading to potential
+ false positives)
+- introduce a pytest_cmdline_processargs(args) hook
+ to allow dynamic computation of command line arguments.
+ This fixes a regression because py.test prior to 2.0
+ allowed to set command line options from conftest.py
+ files which so far pytest-2.0 only allowed from ini-files now.
+- fix issue7: assert failures in doctest modules.
+ unexpected failures in doctests will not generally
+ show nicer, i.e. within the doctest failing context.
+- fix issue9: setup/teardown functions for an xfail-marked
+ test will report as xfail if they fail but report as normally
+ passing (not xpassing) if they succeed. This only is true
+ for "direct" setup/teardown invocations because teardown_class/
+ teardown_module cannot closely relate to a single test.
+- fix issue14: no logging errors at process exit
+- refinements to "collecting" output on non-ttys
+- refine internal plugin registration and --traceconfig output
+- introduce a mechanism to prevent/unregister plugins from the
+ command line, see http://pytest.org/en/stable/how-to/plugins.html#cmdunregister
+- activate resultlog plugin by default
+- fix regression wrt yielded tests which due to the
+ 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
+ parametrization remains the "pytest_generate_tests"
+ mechanism, see the docs.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.2.rst
new file mode 100644
index 0000000000..f1f44f34f4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.2.rst
@@ -0,0 +1,73 @@
+py.test 2.0.2: bug fixes, improved xfail/skip expressions, speed ups
+===========================================================================
+
+Welcome to pytest-2.0.2, a maintenance and bug fix release of pytest,
+a mature testing tool for Python, supporting CPython 2.4-3.2, Jython
+and latest PyPy interpreters. See the extensive docs with tested examples here:
+
+ http://pytest.org/
+
+If you want to install or upgrade pytest, just type one of::
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+Many thanks to all issue reporters and people asking questions
+or complaining, particularly Jurko for his insistence,
+Laura, Victor and Brianna for helping with improving
+and Ronny for his general advise.
+
+best,
+holger krekel
+
+Changes between 2.0.1 and 2.0.2
+----------------------------------------------
+
+- tackle issue32 - speed up test runs of very quick test functions
+ by reducing the relative overhead
+
+- fix issue30 - extended xfail/skipif handling and improved reporting.
+ If you have a syntax error in your skip/xfail
+ expressions you now get nice error reports.
+
+ Also you can now access module globals from xfail/skipif
+ expressions so that this for example works now::
+
+ import pytest
+ import mymodule
+ @pytest.mark.skipif("mymodule.__version__[0] == "1")
+ def test_function():
+ pass
+
+ This will not run the test function if the module's version string
+ does not start with a "1". Note that specifying a string instead
+ of a boolean expressions allows py.test to report meaningful information
+ when summarizing a test run as to what conditions lead to skipping
+ (or xfail-ing) tests.
+
+- fix issue28 - setup_method and pytest_generate_tests work together
+ The setup_method fixture method now gets called also for
+ test function invocations generated from the pytest_generate_tests
+ hook.
+
+- fix issue27 - collectonly and keyword-selection (-k) now work together
+ Also, if you do "py.test --collectonly -q" you now get a flat list
+ of test ids that you can use to paste to the py.test commandline
+ in order to execute a particular test.
+
+- fix issue25 avoid reported problems with --pdb and python3.2/encodings output
+
+- fix issue23 - tmpdir argument now works on Python3.2 and WindowsXP
+ Starting with Python3.2 os.symlink may be supported. By requiring
+ a newer py lib version the py.path.local() implementation acknowledges
+ this.
+
+- fixed typos in the docs (thanks Victor Garcia, Brianna Laugher) and particular
+ thanks to Laura Creighton who also reviewed parts of the documentation.
+
+- fix slightly wrong output of verbose progress reporting for classes
+ (thanks Amaury)
+
+- more precise (avoiding of) deprecation warnings for node.Class|Function accesses
+
+- avoid std unittest assertion helper code in tracebacks (thanks Ronny)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.3.rst
new file mode 100644
index 0000000000..81d01eb99f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.0.3.rst
@@ -0,0 +1,39 @@
+py.test 2.0.3: bug fixes and speed ups
+===========================================================================
+
+Welcome to pytest-2.0.3, a maintenance and bug fix release of pytest,
+a mature testing tool for Python, supporting CPython 2.4-3.2, Jython
+and latest PyPy interpreters. See the extensive docs with tested examples here:
+
+ http://pytest.org/
+
+If you want to install or upgrade pytest, just type one of::
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+There also is a bugfix release 1.6 of pytest-xdist, the plugin
+that enables seamless distributed and "looponfail" testing for Python.
+
+best,
+holger krekel
+
+Changes between 2.0.2 and 2.0.3
+----------------------------------------------
+
+- fix issue38: nicer tracebacks on calls to hooks, particularly early
+ configure/sessionstart ones
+
+- fix missing skip reason/meta information in junitxml files, reported
+ via http://lists.idyll.org/pipermail/testing-in-python/2011-March/003928.html
+
+- fix issue34: avoid collection failure with "test" prefixed classes
+ deriving from object.
+
+- don't require zlib (and other libs) for genscript plugin without
+ --genscript actually being used.
+
+- speed up skips (by not doing a full traceback representation
+ internally)
+
+- fix issue37: avoid invalid characters in junitxml's output
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.0.rst
new file mode 100644
index 0000000000..78247247e2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.0.rst
@@ -0,0 +1,47 @@
+py.test 2.1.0: perfected assertions and bug fixes
+===========================================================================
+
+Welcome to the release of pytest-2.1, a mature testing tool for Python,
+supporting CPython 2.4-3.2, Jython and latest PyPy interpreters. See
+the improved extensive docs (now also as PDF!) with tested examples here:
+
+ http://pytest.org/
+
+The single biggest news about this release are **perfected assertions**
+courtesy of Benjamin Peterson. You can now safely use ``assert``
+statements in test modules without having to worry about side effects
+or python optimization ("-OO") options. This is achieved by rewriting
+assert statements in test modules upon import, using a PEP302 hook.
+See https://docs.pytest.org/en/stable/how-to/assert.html for
+detailed information. The work has been partly sponsored by my company,
+merlinux GmbH.
+
+For further details on bug fixes and smaller enhancements see below.
+
+If you want to install or upgrade pytest, just type one of::
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+best,
+holger krekel / https://merlinux.eu/
+
+Changes between 2.0.3 and 2.1.0
+----------------------------------------------
+
+- fix issue53 call nosestyle setup functions with correct ordering
+- fix issue58 and issue59: new assertion code fixes
+- merge Benjamin's assertionrewrite branch: now assertions
+ for test modules on python 2.6 and above are done by rewriting
+ the AST and saving the pyc file before the test module is imported.
+ see doc/assert.txt for more info.
+- fix issue43: improve doctests with better traceback reporting on
+ 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 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
+- report KeyboardInterrupt even if interrupted during session startup
+- fix issue 35 - provide PDF doc version and download link from index page
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.1.rst
new file mode 100644
index 0000000000..369428ed2e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.1.rst
@@ -0,0 +1,36 @@
+py.test 2.1.1: assertion fixes and improved junitxml output
+===========================================================================
+
+pytest-2.1.1 is a backward compatible maintenance release of the
+popular py.test testing tool. See extensive docs with examples here:
+
+ http://pytest.org/
+
+Most bug fixes address remaining issues with the perfected assertions
+introduced with 2.1.0 - many thanks to the bug reporters and to Benjamin
+Peterson for helping to fix them. Also, junitxml output now produces
+system-out/err tags which lead to better displays of tracebacks with Jenkins.
+
+Also a quick note to package maintainers and others interested: there now
+is a "pytest" man page which can be generated with "make man" in doc/.
+
+If you want to install or upgrade pytest, just type one of::
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+best,
+holger krekel / https://merlinux.eu/
+
+Changes between 2.1.0 and 2.1.1
+----------------------------------------------
+
+- fix issue64 / pytest.set_trace now works within pytest_generate_tests hooks
+- fix issue60 / fix error conditions involving the creation of __pycache__
+- fix issue63 / assertion rewriting on inserts involving strings containing '%'
+- fix assertion rewriting on calls with a ** arg
+- don't cache rewritten modules if bytecode generation is disabled
+- fix assertion rewriting in read-only directories
+- fix issue59: provide system-out/err tags for junitxml output
+- fix issue61: assertion rewriting on boolean operations with 3 or more operands
+- you can now build a man page with "cd doc ; make man"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.2.rst
new file mode 100644
index 0000000000..a3c0c1a38a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.2.rst
@@ -0,0 +1,32 @@
+py.test 2.1.2: bug fixes and fixes for jython
+===========================================================================
+
+pytest-2.1.2 is a minor backward compatible maintenance release of the
+popular py.test testing tool. pytest is commonly used for unit,
+functional- and integration testing. See extensive docs with examples
+here:
+
+ http://pytest.org/
+
+Most bug fixes address remaining issues with the perfected assertions
+introduced in the 2.1 series - many thanks to the bug reporters and to Benjamin
+Peterson for helping to fix them. pytest should also work better with
+Jython-2.5.1 (and Jython trunk).
+
+If you want to install or upgrade pytest, just type one of::
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+best,
+holger krekel / https://merlinux.eu/
+
+Changes between 2.1.1 and 2.1.2
+----------------------------------------
+
+- fix assertion rewriting on files with windows newlines on some Python versions
+- refine test discovery by package/module name (--pyargs), thanks Florian Mayer
+- fix issue69 / assertion rewriting fixed on some boolean operations
+- fix issue68 / packages now work with assertion rewriting
+- fix issue66: use different assertion rewriting caches when the -O option is passed
+- don't try assertion rewriting on Jython, use reinterp
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.3.rst
new file mode 100644
index 0000000000..a43bc058c1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.1.3.rst
@@ -0,0 +1,32 @@
+py.test 2.1.3: just some more fixes
+===========================================================================
+
+pytest-2.1.3 is a minor backward compatible maintenance release of the
+popular py.test testing tool. It is commonly used for unit, functional-
+and integration testing. See extensive docs with examples here:
+
+ http://pytest.org/
+
+The release contains another fix to the perfected assertions introduced
+with the 2.1 series as well as the new possibility to customize reporting
+for assertion expressions on a per-directory level.
+
+If you want to install or upgrade pytest, just type one of::
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+Thanks to the bug reporters and to Ronny Pfannschmidt, Benjamin Peterson
+and Floris Bruynooghe who implemented the fixes.
+
+best,
+holger krekel
+
+Changes between 2.1.2 and 2.1.3
+----------------------------------------
+
+- fix issue79: assertion rewriting failed on some comparisons in boolops,
+- correctly handle zero length arguments (a la pytest '')
+- fix issue67 / junitxml now contains correct test durations
+- fix issue75 / skipping test failure on jython
+- fix issue77 / Allow assertrepr_compare hook to apply to a subset of tests
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.0.rst
new file mode 100644
index 0000000000..7a32dca173
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.0.rst
@@ -0,0 +1,95 @@
+py.test 2.2.0: test marking++, parametrization++ and duration profiling
+===========================================================================
+
+pytest-2.2.0 is a test-suite compatible release of the popular
+py.test testing tool. Plugins might need upgrades. It comes
+with these improvements:
+
+* easier and more powerful parametrization of tests:
+
+ - new @pytest.mark.parametrize decorator to run tests with different arguments
+ - new metafunc.parametrize() API for parametrizing arguments independently
+ - see examples at http://pytest.org/en/stable/example/how-to/parametrize.html
+ - NOTE that parametrize() related APIs are still a bit experimental
+ and might change in future releases.
+
+* improved handling of test markers and refined marking mechanism:
+
+ - "-m markexpr" option for selecting tests according to their mark
+ - a new "markers" ini-variable for registering test markers for your project
+ - the new "--strict" bails out with an error if using unregistered markers.
+ - see examples at http://pytest.org/en/stable/example/markers.html
+
+* duration profiling: new "--duration=N" option showing the N slowest test
+ execution or setup/teardown calls. This is most useful if you want to
+ find out where your slowest test code is.
+
+* also 2.2.0 performs more eager calling of teardown/finalizers functions
+ resulting in better and more accurate reporting when they fail
+
+Besides there is the usual set of bug fixes along with a cleanup of
+pytest's own test suite allowing it to run on a wider range of environments.
+
+For general information, see extensive docs with examples here:
+
+ http://pytest.org/
+
+If you want to install or upgrade pytest you might just type::
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+Thanks to Ronny Pfannschmidt, David Burns, Jeff Donner, Daniel Nouri, Alfredo Deza and all who gave feedback or sent bug reports.
+
+best,
+holger krekel
+
+
+notes on incompatibility
+------------------------------
+
+While test suites should work unchanged you might need to upgrade plugins:
+
+* You need a new version of the pytest-xdist plugin (1.7) for distributing
+ test runs.
+
+* Other plugins might need an upgrade if they implement
+ the ``pytest_runtest_logreport`` hook which now is called unconditionally
+ for the setup/teardown fixture phases of a test. You may choose to
+ ignore setup/teardown failures by inserting "if rep.when != 'call': return"
+ or something similar. Note that most code probably "just" works because
+ the hook was already called for failing setup/teardown phases of a test
+ so a plugin should have been ready to grok such reports already.
+
+
+Changes between 2.1.3 and 2.2.0
+----------------------------------------
+
+- fix issue90: introduce eager tearing down of test items so that
+ teardown function are called earlier.
+- add an all-powerful metafunc.parametrize function which allows to
+ parametrize test function arguments in multiple steps and therefore
+ from independent plugins and places.
+- add a @pytest.mark.parametrize helper which allows to easily
+ call a test function with different argument values.
+- Add examples to the "parametrize" example page, including a quick port
+ of Test scenarios and the new parametrize function and decorator.
+- introduce registration for "pytest.mark.*" helpers via ini-files
+ or through plugin hooks. Also introduce a "--strict" option which
+ will treat unregistered markers as errors
+ allowing to avoid typos and maintain a well described set of markers
+ for your test suite. See examples at http://pytest.org/en/stable/how-to/mark.html
+ and its links.
+- issue50: introduce "-m marker" option to select tests based on markers
+ (this is a stricter and more predictable version of "-k" in that "-m"
+ only matches complete markers and has more obvious rules for and/or
+ semantics.
+- new feature to help optimizing the speed of your tests:
+ --durations=N option for displaying N slowest test calls
+ and setup/teardown methods.
+- fix issue87: --pastebin now works with python3
+- fix issue89: --pdb with unexpected exceptions in doctest work more sensibly
+- fix and cleanup pytest's own test suite to not leak FDs
+- fix issue83: link to generated funcarg list
+- fix issue74: pyarg module names are now checked against imp.find_module false positives
+- fix compatibility with twisted/trial-11.1.0 use cases
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.1.rst
new file mode 100644
index 0000000000..44281597ea
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.1.rst
@@ -0,0 +1,41 @@
+pytest-2.2.1: bug fixes, perfect teardowns
+===========================================================================
+
+
+pytest-2.2.1 is a minor backward-compatible release of the py.test
+testing tool. It contains bug fixes and little improvements, including
+documentation fixes. If you are using the distributed testing
+pluginmake sure to upgrade it to pytest-xdist-1.8.
+
+For general information see here:
+
+ http://pytest.org/
+
+To install or upgrade pytest:
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+Special thanks for helping on this release to Ronny Pfannschmidt, Jurko
+Gospodnetic and Ralf Schmitt.
+
+best,
+holger krekel
+
+
+Changes between 2.2.0 and 2.2.1
+----------------------------------------
+
+- fix issue99 (in pytest and py) internallerrors with resultlog now
+ produce better output - fixed by normalizing pytest_internalerror
+ input arguments.
+- fix issue97 / traceback issues (in pytest and py) improve traceback output
+ in conjunction with jinja2 and cython which hack tracebacks
+- fix issue93 (in pytest and pytest-xdist) avoid "delayed teardowns":
+ the final test in a test node will now run its teardown directly
+ instead of waiting for the end of the session. Thanks Dave Hunt for
+ the good reporting and feedback. The pytest_runtest_protocol as well
+ as the pytest_runtest_teardown hooks now have "nextitem" available
+ which will be None indicating the end of the test run.
+- fix collection crash due to unknown-source collected items, thanks
+ to Ralf Schmitt (fixed by depending on a more recent pylib)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.2.rst
new file mode 100644
index 0000000000..22ef0bc7a1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.2.rst
@@ -0,0 +1,43 @@
+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.
+
+For general information see here:
+
+ http://pytest.org/
+
+To install or upgrade pytest:
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+Special thanks for helping on this release to Ronny Pfannschmidt
+and Ralf Schmitt and the contributors of issues.
+
+best,
+holger krekel
+
+
+Changes between 2.2.1 and 2.2.2
+----------------------------------------
+
+- fix issue101: wrong args to unittest.TestCase test function now
+ produce better output
+- fix issue102: report more useful errors and hints for when a
+ test directory was renamed and some pyc/__pycache__ remain
+- fix issue106: allow parametrize to be applied multiple times
+ e.g. from module, class and at function level.
+- fix issue107: actually perform session scope finalization
+- don't check in parametrize if indirect parameters are funcarg names
+- add chdir method to monkeypatch funcarg
+- fix crash resulting from calling monkeypatch undo a second time
+- fix issue115: make --collectonly robust against early failure
+ (missing files/directories)
+- "-qq --collectonly" now shows only files and the number of tests in them
+- "-q --collectonly" now shows test ids
+- allow adding of attributes to test reports such that it also works
+ with distributed testing (no upgrade of pytest-xdist needed)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.4.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.4.rst
new file mode 100644
index 0000000000..a8fb9b93c5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.2.4.rst
@@ -0,0 +1,38 @@
+pytest-2.2.4: bug fixes, better junitxml/unittest/python3 compat
+===========================================================================
+
+pytest-2.2.4 is a minor backward-compatible release of the versatile
+py.test testing tool. It contains bug fixes and a few refinements
+to junitxml reporting, better unittest- and python3 compatibility.
+
+For general information see here:
+
+ http://pytest.org/
+
+To install or upgrade pytest:
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+Special thanks for helping on this release to Ronny Pfannschmidt
+and Benjamin Peterson and the contributors of issues.
+
+best,
+holger krekel
+
+Changes between 2.2.3 and 2.2.4
+-----------------------------------
+
+- fix error message for rewritten assertions involving the % operator
+- fix issue 126: correctly match all invalid xml characters for junitxml
+ binary escape
+- fix issue with unittest: now @unittest.expectedFailure markers should
+ be processed correctly (you can also use @pytest.mark markers)
+- document integration with the extended distribute/setuptools test commands
+- fix issue 140: properly get the real functions
+ of bound classmethods for setup/teardown_class
+- fix issue #141: switch from the deceased paste.pocoo.org to bpaste.net
+- fix issue #143: call unconfigure/sessionfinish always when
+ configure/sessionstart where called
+- fix issue #144: better mangle test ids to junitxml classnames
+- upgrade distribute_setup.py to 0.6.27
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.0.rst
new file mode 100644
index 0000000000..6905b77b92
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.0.rst
@@ -0,0 +1,133 @@
+pytest-2.3: improved fixtures / better unittest integration
+=============================================================================
+
+pytest-2.3 comes with many major improvements for fixture/funcarg management
+and parametrized testing in Python. It is now easier, more efficient and
+more predictable to re-run the same tests with different fixture
+instances. Also, you can directly declare the caching "scope" of
+fixtures so that dependent tests throughout your whole test suite can
+re-use database or other expensive fixture objects with ease. Lastly,
+it's possible for fixture functions (formerly known as funcarg
+factories) to use other fixtures, allowing for a completely modular and
+re-usable fixture design.
+
+For detailed info and tutorial-style examples, see:
+
+ http://pytest.org/en/stable/explanation/fixtures.html
+
+Moreover, there is now support for using pytest fixtures/funcargs with
+unittest-style suites, see here for examples:
+
+ http://pytest.org/en/stable/how-to/unittest.html
+
+Besides, more unittest-test suites are now expected to "simply work"
+with pytest.
+
+All changes are backward compatible and you should be able to continue
+to run your test suites and 3rd party plugins that worked with
+pytest-2.2.4.
+
+If you are interested in the precise reasoning (including examples) of the
+pytest-2.3 fixture evolution, please consult
+http://pytest.org/en/stable/funcarg_compare.html
+
+For general info on installation and getting started:
+
+ http://pytest.org/en/stable/getting-started.html
+
+Docs and PDF access as usual at:
+
+ http://pytest.org
+
+and more details for those already in the knowing of pytest can be found
+in the CHANGELOG below.
+
+Particular thanks for this release go to Floris Bruynooghe, Alex Okrushko
+Carl Meyer, Ronny Pfannschmidt, Benjamin Peterson and Alex Gaynor for helping
+to get the new features right and well integrated. Ronny and Floris
+also helped to fix a number of bugs and yet more people helped by
+providing bug reports.
+
+have fun,
+holger krekel
+
+
+Changes between 2.2.4 and 2.3.0
+-----------------------------------
+
+- fix issue202 - better automatic names for parametrized test functions
+- fix issue139 - introduce @pytest.fixture which allows direct scoping
+ and parametrization of funcarg factories. Introduce new @pytest.setup
+ marker to allow the writing of setup functions which accept funcargs.
+- fix issue198 - conftest fixtures were not found on windows32 in some
+ circumstances with nested directory structures due to path manipulation issues
+- fix issue193 skip test functions with were parametrized with empty
+ parameter sets
+- fix python3.3 compat, mostly reporting bits that previously depended
+ on dict ordering
+- introduce re-ordering of tests by resource and parametrization setup
+ which takes precedence to the usual file-ordering
+- fix issue185 monkeypatching time.time does not cause pytest to fail
+- fix issue172 duplicate call of pytest.setup-decoratored setup_module
+ functions
+- fix junitxml=path construction so that if tests change the
+ current working directory and the path is a relative path
+ it is constructed correctly from the original current working dir.
+- fix "python setup.py test" example to cause a proper "errno" return
+- fix issue165 - fix broken doc links and mention stackoverflow for FAQ
+- catch unicode-issues when writing failure representations
+ to terminal to prevent the whole session from crashing
+- fix xfail/skip confusion: a skip-mark or an imperative pytest.skip
+ will now take precedence before xfail-markers because we
+ can't determine xfail/xpass status in case of a skip. see also:
+ http://stackoverflow.com/questions/11105828/in-py-test-when-i-explicitly-skip-a-test-that-is-marked-as-xfail-how-can-i-get
+
+- always report installed 3rd party plugins in the header of a test run
+
+- fix issue160: a failing setup of an xfail-marked tests should
+ be reported as xfail (not xpass)
+
+- fix issue128: show captured output when capsys/capfd are used
+
+- fix issue179: properly show the dependency chain of factories
+
+- pluginmanager.register(...) now raises ValueError if the
+ plugin has been already registered or the name is taken
+
+- fix issue159: improve https://docs.pytest.org/en/6.0.1/faq.html
+ especially with respect to the "magic" history, also mention
+ pytest-django, trial and unittest integration.
+
+- make request.keywords and node.keywords writable. All descendant
+ collection nodes will see keyword values. Keywords are dictionaries
+ containing markers and other info.
+
+- fix issue 178: xml binary escapes are now wrapped in py.xml.raw
+
+- fix issue 176: correctly catch the builtin AssertionError
+ even when we replaced AssertionError with a subclass on the
+ python level
+
+- factory discovery no longer fails with magic global callables
+ that provide no sane __code__ object (mock.call for example)
+
+- fix issue 182: testdir.inprocess_run now considers passed plugins
+
+- fix issue 188: ensure sys.exc_info is clear on python2
+ before calling into a test
+
+- fix issue 191: add unittest TestCase runTest method support
+- fix issue 156: monkeypatch correctly handles class level descriptors
+
+- reporting refinements:
+
+ - pytest_report_header now receives a "startdir" so that
+ you can use startdir.bestrelpath(yourpath) to show
+ nice relative path
+
+ - allow plugins to implement both pytest_report_header and
+ pytest_sessionstart (sessionstart is invoked first).
+
+ - don't show deselected reason line if there is none
+
+ - py.test -vv will show all of assert comparisons instead of truncating
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.1.rst
new file mode 100644
index 0000000000..6f8770b345
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.1.rst
@@ -0,0 +1,39 @@
+pytest-2.3.1: fix regression with factory functions
+===========================================================================
+
+pytest-2.3.1 is a quick follow-up release:
+
+- fix issue202 - regression with fixture functions/funcarg factories:
+ using "self" is now safe again and works as in 2.2.4. Thanks
+ to Eduard Schettino for the quick bug report.
+
+- disable pexpect pytest self tests on Freebsd - thanks Koob for the
+ quick reporting
+
+- fix/improve interactive docs with --markers
+
+See
+
+ http://pytest.org/
+
+for general information. To install or upgrade pytest:
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+best,
+holger krekel
+
+
+Changes between 2.3.0 and 2.3.1
+-----------------------------------
+
+- fix issue202 - fix regression: using "self" from fixture functions now
+ works as expected (it's the same "self" instance that a test method
+ which uses the fixture sees)
+
+- skip pexpect using tests (test_pdb.py mostly) on freebsd* systems
+ due to pexpect not supporting it properly (hanging)
+
+- link to web pages from --markers output which provides help for
+ pytest.mark.* usage.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.2.rst
new file mode 100644
index 0000000000..484feaaa5a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.2.rst
@@ -0,0 +1,57 @@
+pytest-2.3.2: some fixes and more traceback-printing speed
+===========================================================================
+
+pytest-2.3.2 is another stabilization release:
+
+- issue 205: fixes a regression with conftest detection
+- issue 208/29: fixes traceback-printing speed in some bad cases
+- fix teardown-ordering for parametrized setups
+- fix unittest and trial compat behaviour with respect to runTest() methods
+- issue 206 and others: some improvements to packaging
+- fix issue127 and others: improve some docs
+
+See
+
+ http://pytest.org/
+
+for general information. To install or upgrade pytest:
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+best,
+holger krekel
+
+
+Changes between 2.3.1 and 2.3.2
+-----------------------------------
+
+- fix issue208 and fix issue29 use new py version to avoid long pauses
+ when printing tracebacks in long modules
+
+- fix issue205 - conftests in subdirs customizing
+ pytest_pycollect_makemodule and pytest_pycollect_makeitem
+ now work properly
+
+- fix teardown-ordering for parametrized setups
+
+- fix issue127 - better documentation for pytest_addoption
+ and related objects.
+
+- fix unittest behaviour: TestCase.runtest only called if there are
+ test methods defined
+
+- improve trial support: don't collect its empty
+ unittest.TestCase.runTest() method
+
+- "python setup.py test" now works with pytest itself
+
+- fix/improve internal/packaging related bits:
+
+ - exception message check of test_nose.py now passes on python33 as well
+
+ - issue206 - fix test_assertrewrite.py to work when a global
+ PYTHONDONTWRITEBYTECODE=1 is present
+
+ - add tox.ini to pytest distribution so that ignore-dirs and others config
+ bits are properly distributed for maintainers who run pytest-own tests
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.3.rst
new file mode 100644
index 0000000000..0cb598a426
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.3.rst
@@ -0,0 +1,61 @@
+pytest-2.3.3: integration fixes, py24 support, ``*/**`` shown in traceback
+===========================================================================
+
+pytest-2.3.3 is another stabilization release of the py.test tool
+which offers uebersimple assertions, scalable fixture mechanisms
+and deep customization for testing with Python. Particularly,
+this release provides:
+
+- integration fixes and improvements related to flask, numpy, nose,
+ unittest, mock
+
+- makes pytest work on py24 again (yes, people sometimes still need to use it)
+
+- show ``*,**`` args in pytest tracebacks
+
+Thanks to Manuel Jacob, Thomas Waldmann, Ronny Pfannschmidt, Pavel Repin
+and Andreas Taumoefolau for providing patches and all for the issues.
+
+See
+
+ http://pytest.org/
+
+for general information. To install or upgrade pytest:
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+best,
+holger krekel
+
+Changes between 2.3.2 and 2.3.3
+-----------------------------------
+
+- fix issue214 - parse modules that contain special objects like e. g.
+ flask's request object which blows up on getattr access if no request
+ is active. thanks Thomas Waldmann.
+
+- fix issue213 - allow to parametrize with values like numpy arrays that
+ do not support an __eq__ operator
+
+- fix issue215 - split test_python.org into multiple files
+
+- fix issue148 - @unittest.skip on classes is now recognized and avoids
+ calling setUpClass/tearDownClass, thanks Pavel Repin
+
+- fix issue209 - reintroduce python2.4 support by depending on newer
+ pylib which re-introduced statement-finding for pre-AST interpreters
+
+- nose support: only call setup if it's a callable, thanks Andrew
+ Taumoefolau
+
+- fix issue219 - add py2.4-3.3 classifiers to TROVE list
+
+- in tracebacks *,** arg values are now shown next to normal arguments
+ (thanks Manuel Jacob)
+
+- fix issue217 - support mock.patch with pytest's fixtures - note that
+ you need either mock-1.0.1 or the python3.3 builtin unittest.mock.
+
+- fix issue127 - improve documentation for pytest_addoption() and
+ add a ``config.getoption(name)`` helper function for consistency.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.4.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.4.rst
new file mode 100644
index 0000000000..43bf03b02b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.4.rst
@@ -0,0 +1,39 @@
+pytest-2.3.4: stabilization, more flexible selection via "-k expr"
+===========================================================================
+
+pytest-2.3.4 is a small stabilization release of the py.test tool
+which offers uebersimple assertions, scalable fixture mechanisms
+and deep customization for testing with Python. This release
+comes with the following fixes and features:
+
+- make "-k" option accept an expressions the same as with "-m" so that one
+ can write: -k "name1 or name2" etc. This is a slight usage incompatibility
+ if you used special syntax like "TestClass.test_method" which you now
+ need to write as -k "TestClass and test_method" to match a certain
+ method in a certain test class.
+- allow to dynamically define markers via
+ item.keywords[...]=assignment integrating with "-m" option
+- yielded test functions will now have autouse-fixtures active but
+ cannot accept fixtures as funcargs - it's anyway recommended to
+ rather use the post-2.0 parametrize features instead of yield, see:
+ http://pytest.org/en/stable/example/how-to/parametrize.html
+- fix autouse-issue where autouse-fixtures would not be discovered
+ if defined in an a/conftest.py file and tests in a/tests/test_some.py
+- fix issue226 - LIFO ordering for fixture teardowns
+- fix issue224 - invocations with >256 char arguments now work
+- fix issue91 - add/discuss package/directory level setups in example
+- fixes related to autouse discovery and calling
+
+Thanks in particular to Thomas Waldmann for spotting and reporting issues.
+
+See
+
+ http://pytest.org/
+
+for general information. To install or upgrade pytest:
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+best,
+holger krekel
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.5.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.5.rst
new file mode 100644
index 0000000000..d68780a244
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.3.5.rst
@@ -0,0 +1,96 @@
+pytest-2.3.5: bug fixes and little improvements
+===========================================================================
+
+pytest-2.3.5 is a maintenance release with many bug fixes and little
+improvements. See the changelog below for details. No backward
+compatibility issues are foreseen and all plugins which worked with the
+prior version are expected to work unmodified. Speaking of which, a
+few interesting new plugins saw the light last month:
+
+- pytest-instafail: show failure information while tests are running
+- pytest-qt: testing of GUI applications written with QT/Pyside
+- pytest-xprocess: managing external processes across test runs
+- pytest-random: randomize test ordering
+
+And several others like pytest-django saw maintenance releases.
+For a more complete list, check out
+https://pypi.org/search/?q=pytest
+
+For general information see:
+
+ http://pytest.org/
+
+To install or upgrade pytest:
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+Particular thanks to Floris, Ronny, Benjamin and the many bug reporters
+and fix providers.
+
+may the fixtures be with you,
+holger krekel
+
+
+Changes between 2.3.4 and 2.3.5
+-----------------------------------
+
+- never consider a fixture function for test function collection
+
+- allow re-running of test items / helps to fix pytest-reruntests plugin
+ and also help to keep less fixture/resource references alive
+
+- put captured stdout/stderr into junitxml output even for passing tests
+ (thanks Adam Goucher)
+
+- Issue 265 - integrate nose setup/teardown with setupstate
+ so it doesn't try to teardown if it did not setup
+
+- issue 271 - don't write junitxml on worker nodes
+
+- Issue 274 - don't try to show full doctest example
+ when doctest does not know the example location
+
+- issue 280 - disable assertion rewriting on buggy CPython 2.6.0
+
+- inject "getfixture()" helper to retrieve fixtures from doctests,
+ thanks Andreas Zeidler
+
+- issue 259 - when assertion rewriting, be consistent with the default
+ source encoding of ASCII on Python 2
+
+- issue 251 - report a skip instead of ignoring classes with init
+
+- issue250 unicode/str mixes in parametrization names and values now works
+
+- issue257, assertion-triggered compilation of source ending in a
+ comment line doesn't blow up in python2.5 (fixed through py>=1.4.13.dev6)
+
+- fix --genscript option to generate standalone scripts that also
+ work with python3.3 (importer ordering)
+
+- issue171 - in assertion rewriting, show the repr of some
+ global variables
+
+- fix option help for "-k"
+
+- move long description of distribution into README.rst
+
+- improve docstring for metafunc.parametrize()
+
+- fix bug where using capsys with pytest.set_trace() in a test
+ function would break when looking at capsys.readouterr()
+
+- allow to specify prefixes starting with "_" when
+ customizing python_functions test discovery. (thanks Graham Horler)
+
+- improve PYTEST_DEBUG tracing output by putting
+ extra data on a new lines with additional indent
+
+- ensure OutcomeExceptions like skip/fail have initialized exception attributes
+
+- issue 260 - don't use nose special setup on plain unittest cases
+
+- fix issue134 - print the collect errors that prevent running specified test items
+
+- fix issue266 - accept unicode in MarkEvaluator expressions
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.4.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.4.0.rst
new file mode 100644
index 0000000000..138cc89576
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.4.0.rst
@@ -0,0 +1,223 @@
+pytest-2.4.0: new fixture features/hooks and bug fixes
+===========================================================================
+
+The just released pytest-2.4.0 brings many improvements and numerous
+bug fixes while remaining plugin- and test-suite compatible apart
+from a few supposedly very minor incompatibilities. See below for
+a full list of details. A few feature highlights:
+
+- new yield-style fixtures `pytest.yield_fixture
+ <http://pytest.org/en/stable/yieldfixture.html>`_, allowing to use
+ existing with-style context managers in fixture functions.
+
+- improved pdb support: ``import pdb ; pdb.set_trace()`` now works
+ without requiring prior disabling of stdout/stderr capturing.
+ Also the ``--pdb`` options works now on collection and internal errors
+ and we introduced a new experimental hook for IDEs/plugins to
+ intercept debugging: ``pytest_exception_interact(node, call, report)``.
+
+- shorter monkeypatch variant to allow specifying an import path as
+ a target, for example: ``monkeypatch.setattr("requests.get", myfunc)``
+
+- better unittest/nose compatibility: all teardown methods are now only
+ called if the corresponding setup method succeeded.
+
+- integrate tab-completion on command line options if you
+ have :pypi:`argcomplete` configured.
+
+- allow boolean expression directly with skipif/xfail
+ if a "reason" is also specified.
+
+- a new hook ``pytest_load_initial_conftests`` allows plugins like
+ :pypi:`pytest-django` to
+ influence the environment before conftest files import ``django``.
+
+- reporting: color the last line red or green depending if
+ failures/errors occurred or everything passed.
+
+The documentation has been updated to accommodate the changes,
+see `http://pytest.org <http://pytest.org>`_
+
+To install or upgrade pytest::
+
+ pip install -U pytest # or
+ easy_install -U pytest
+
+
+**Many thanks to all who helped, including Floris Bruynooghe,
+Brianna Laugher, Andreas Pelme, Anthon van der Neut, Anatoly Bubenkoff,
+Vladimir Keleshev, Mathieu Agopian, Ronny Pfannschmidt, Christian
+Theunert and many others.**
+
+may passing tests be with you,
+
+holger krekel
+
+Changes between 2.3.5 and 2.4
+-----------------------------------
+
+known incompatibilities:
+
+- if calling --genscript from python2.7 or above, you only get a
+ standalone script which works on python2.7 or above. Use Python2.6
+ to also get a python2.5 compatible version.
+
+- all xunit-style teardown methods (nose-style, pytest-style,
+ unittest-style) will not be called if the corresponding setup method failed,
+ see issue322 below.
+
+- the pytest_plugin_unregister hook wasn't ever properly called
+ and there is no known implementation of the hook - so it got removed.
+
+- pytest.fixture-decorated functions cannot be generators (i.e. use
+ yield) anymore. This change might be reversed in 2.4.1 if it causes
+ unforeseen real-life issues. However, you can always write and return
+ an inner function/generator and change the fixture consumer to iterate
+ over the returned generator. This change was done in lieu of the new
+ ``pytest.yield_fixture`` decorator, see below.
+
+new features:
+
+- experimentally introduce a new ``pytest.yield_fixture`` decorator
+ which accepts exactly the same parameters as pytest.fixture but
+ mandates a ``yield`` statement instead of a ``return statement`` from
+ fixture functions. This allows direct integration with "with-style"
+ context managers in fixture functions and generally avoids registering
+ of finalization callbacks in favour of treating the "after-yield" as
+ teardown code. Thanks Andreas Pelme, Vladimir Keleshev, Floris
+ Bruynooghe, Ronny Pfannschmidt and many others for discussions.
+
+- allow boolean expression directly with skipif/xfail
+ if a "reason" is also specified. Rework skipping documentation
+ to recommend "condition as booleans" because it prevents surprises
+ when importing markers between modules. Specifying conditions
+ as strings will remain fully supported.
+
+- reporting: color the last line red or green depending if
+ failures/errors occurred or everything passed. thanks Christian
+ Theunert.
+
+- make "import pdb ; pdb.set_trace()" work natively wrt capturing (no
+ "-s" needed anymore), making ``pytest.set_trace()`` a mere shortcut.
+
+- fix issue181: --pdb now also works on collect errors (and
+ on internal errors) . This was implemented by a slight internal
+ refactoring and the introduction of a new hook
+ ``pytest_exception_interact`` hook (see next item).
+
+- fix issue341: introduce new experimental hook for IDEs/terminals to
+ intercept debugging: ``pytest_exception_interact(node, call, report)``.
+
+- new monkeypatch.setattr() variant to provide a shorter
+ invocation for patching out classes/functions from modules:
+
+ monkeypatch.setattr("requests.get", myfunc)
+
+ will replace the "get" function of the "requests" module with ``myfunc``.
+
+- fix issue322: tearDownClass is not run if setUpClass failed. Thanks
+ Mathieu Agopian for the initial fix. Also make all of pytest/nose
+ finalizer mimic the same generic behaviour: if a setupX exists and
+ fails, don't run teardownX. This internally introduces a new method
+ "node.addfinalizer()" helper which can only be called during the setup
+ phase of a node.
+
+- simplify pytest.mark.parametrize() signature: allow to pass a
+ CSV-separated string to specify argnames. For example:
+ ``pytest.mark.parametrize("input,expected", [(1,2), (2,3)])``
+ works as well as the previous:
+ ``pytest.mark.parametrize(("input", "expected"), ...)``.
+
+- add support for setUpModule/tearDownModule detection, thanks Brian Okken.
+
+- integrate tab-completion on options through use of "argcomplete".
+ Thanks Anthon van der Neut for the PR.
+
+- change option names to be hyphen-separated long options but keep the
+ old spelling backward compatible. py.test -h will only show the
+ hyphenated version, for example "--collect-only" but "--collectonly"
+ will remain valid as well (for backward-compat reasons). Many thanks to
+ Anthon van der Neut for the implementation and to Hynek Schlawack for
+ pushing us.
+
+- fix issue 308 - allow to mark/xfail/skip individual parameter sets
+ when parametrizing. Thanks Brianna Laugher.
+
+- call new experimental pytest_load_initial_conftests hook to allow
+ 3rd party plugins to do something before a conftest is loaded.
+
+Bug fixes:
+
+- fix issue358 - capturing options are now parsed more properly
+ by using a new parser.parse_known_args method.
+
+- pytest now uses argparse instead of optparse (thanks Anthon) which
+ means that "argparse" is added as a dependency if installing into python2.6
+ environments or below.
+
+- fix issue333: fix a case of bad unittest/pytest hook interaction.
+
+- PR27: correctly handle nose.SkipTest during collection. Thanks
+ Antonio Cuni, Ronny Pfannschmidt.
+
+- fix issue355: junitxml puts name="pytest" attribute to testsuite tag.
+
+- fix issue336: autouse fixture in plugins should work again.
+
+- fix issue279: improve object comparisons on assertion failure
+ for standard datatypes and recognise collections.abc. Thanks to
+ Brianna Laugher and Mathieu Agopian.
+
+- fix issue317: assertion rewriter support for the is_package method
+
+- fix issue335: document py.code.ExceptionInfo() object returned
+ from pytest.raises(), thanks Mathieu Agopian.
+
+- remove implicit distribute_setup support from setup.py.
+
+- fix issue305: ignore any problems when writing pyc files.
+
+- SO-17664702: call fixture finalizers even if the fixture function
+ 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.
+
+- you can specify "-q" or "-qq" to get different levels of "quieter"
+ reporting (thanks Katarzyna Jachim)
+
+- fix issue300 - Fix order of conftest loading when starting py.test
+ in a subdirectory.
+
+- fix issue323 - sorting of many module-scoped arg parametrizations
+
+- make sessionfinish hooks execute with the same cwd-context as at
+ session start (helps fix plugin behaviour which write output files
+ with relative path such as pytest-cov)
+
+- fix issue316 - properly reference collection hooks in docs
+
+- fix issue 306 - cleanup of -k/-m options to only match markers/test
+ names/keywords respectively. Thanks Wouter van Ackooy.
+
+- improved doctest counting for doctests in python modules --
+ files without any doctest items will not show up anymore
+ and doctest examples are counted as separate test items.
+ thanks Danilo Bellini.
+
+- fix issue245 by depending on the released py-1.4.14
+ which fixes py.io.dupfile to work with files with no
+ mode. Thanks Jason R. Coombs.
+
+- fix junitxml generation when test output contains control characters,
+ addressing issue267, thanks Jaap Broekhuizen
+
+- fix issue338: honor --tb style for setup/teardown errors as well. Thanks Maho.
+
+- fix issue307 - use yaml.safe_load in example, thanks Mark Eichin.
+
+- better parametrize error messages, thanks Brianna Laugher
+
+- pytest_terminal_summary(terminalreporter) hooks can now use
+ ".section(title)" and ".line(msg)" methods to print extra
+ information at the end of a test run.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.4.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.4.1.rst
new file mode 100644
index 0000000000..308df6bdc4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.4.1.rst
@@ -0,0 +1,25 @@
+pytest-2.4.1: fixing three regressions compared to 2.3.5
+===========================================================================
+
+pytest-2.4.1 is a quick follow up release to fix three regressions
+compared to 2.3.5 before they hit more people:
+
+- When using parser.addoption() unicode arguments to the
+ "type" keyword should also be converted to the respective types.
+ thanks Floris Bruynooghe, @dnozay. (fixes issue360 and issue362)
+
+- fix dotted filename completion when using argcomplete
+ thanks Anthon van der Neuth. (fixes issue361)
+
+- fix regression when a 1-tuple ("arg",) is used for specifying
+ parametrization (the values of the parametrization were passed
+ nested in a tuple). Thanks Donald Stufft.
+
+- also merge doc typo fixes, thanks Andy Dirnberger
+
+as usual, docs at http://pytest.org and upgrades via::
+
+ pip install -U pytest
+
+have fun,
+holger krekel
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.4.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.4.2.rst
new file mode 100644
index 0000000000..ab08b72aaf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.4.2.rst
@@ -0,0 +1,39 @@
+pytest-2.4.2: colorama on windows, plugin/tmpdir fixes
+===========================================================================
+
+pytest-2.4.2 is another bug-fixing release:
+
+- on Windows require colorama and a newer py lib so that py.io.TerminalWriter()
+ now uses colorama instead of its own ctypes hacks. (fixes issue365)
+ thanks Paul Moore for bringing it up.
+
+- fix "-k" matching of tests where "repr" and "attr" and other names would
+ cause wrong matches because of an internal implementation quirk
+ (don't ask) which is now properly implemented. fixes issue345.
+
+- avoid tmpdir fixture to create too long filenames especially
+ when parametrization is used (issue354)
+
+- fix pytest-pep8 and pytest-flakes / pytest interactions
+ (collection names in mark plugin was assuming an item always
+ has a function which is not true for those plugins etc.)
+ Thanks Andi Zeidler.
+
+- introduce node.get_marker/node.add_marker API for plugins
+ like pytest-pep8 and pytest-flakes to avoid the messy
+ details of the node.keywords pseudo-dicts. Adapted
+ docs.
+
+- remove attempt to "dup" stdout at startup as it's icky.
+ the normal capturing should catch enough possibilities
+ of tests messing up standard FDs.
+
+- add pluginmanager.do_configure(config) as a link to
+ config.do_configure() for plugin-compatibility
+
+as usual, docs at http://pytest.org and upgrades via::
+
+ pip install -U pytest
+
+have fun,
+holger krekel
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.5.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.5.0.rst
new file mode 100644
index 0000000000..c6cdcdd8a8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.5.0.rst
@@ -0,0 +1,174 @@
+pytest-2.5.0: now down to ZERO reported bugs!
+===========================================================================
+
+pytest-2.5.0 is a big fixing release, the result of two community bug
+fixing days plus numerous additional works from many people and
+reporters. The release should be fully compatible to 2.4.2, existing
+plugins and test suites. We aim at maintaining this level of ZERO reported
+bugs because it's no fun if your testing tool has bugs, is it? Under a
+condition, though: when submitting a bug report please provide
+clear information about the circumstances and a simple example which
+reproduces the problem.
+
+The issue tracker is of course not empty now. We have many remaining
+"enhancement" issues which we'll hopefully can tackle in 2014 with your
+help.
+
+For those who use older Python versions, please note that pytest is not
+automatically tested on python2.5 due to virtualenv, setuptools and tox
+not supporting it anymore. Manual verification shows that it mostly
+works fine but it's not going to be part of the automated release
+process and thus likely to break in the future.
+
+As usual, current docs are at
+
+ http://pytest.org
+
+and you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Particular thanks for helping with this release go to Anatoly Bubenkoff,
+Floris Bruynooghe, Marc Abramowitz, Ralph Schmitt, Ronny Pfannschmidt,
+Donald Stufft, James Lan, Rob Dennis, Jason R. Coombs, Mathieu Agopian,
+Virgil Dupras, Bruno Oliveira, Alex Gaynor and others.
+
+have fun,
+holger krekel
+
+
+2.5.0
+-----------------------------------
+
+- dropped python2.5 from automated release testing of pytest itself
+ which means it's probably going to break soon (but still works
+ with this release we believe).
+
+- simplified and fixed implementation for calling finalizers when
+ parametrized fixtures or function arguments are involved. finalization
+ is now performed lazily at setup time instead of in the "teardown phase".
+ While this might sound odd at first, it helps to ensure that we are
+ correctly handling setup/teardown even in complex code. User-level code
+ should not be affected unless it's implementing the pytest_runtest_teardown
+ hook and expecting certain fixture instances are torn down within (very
+ unlikely and would have been unreliable anyway).
+
+- PR90: add --color=yes|no|auto option to force terminal coloring
+ mode ("auto" is default). Thanks Marc Abramowitz.
+
+- fix issue319 - correctly show unicode in assertion errors. Many
+ thanks to Floris Bruynooghe for the complete PR. Also means
+ we depend on py>=1.4.19 now.
+
+- fix issue396 - correctly sort and finalize class-scoped parametrized
+ tests independently from number of methods on the class.
+
+- refix issue323 in a better way -- parametrization should now never
+ cause Runtime Recursion errors because the underlying algorithm
+ for re-ordering tests per-scope/per-fixture is not recursive
+ anymore (it was tail-call recursive before which could lead
+ to problems for more than >966 non-function scoped parameters).
+
+- fix issue290 - there is preliminary support now for parametrizing
+ with repeated same values (sometimes useful to test if calling
+ a second time works as with the first time).
+
+- close issue240 - document precisely how pytest module importing
+ works, discuss the two common test directory layouts, and how it
+ interacts with PEP420-namespace packages.
+
+- fix issue246 fix finalizer order to be LIFO on independent fixtures
+ depending on a parametrized higher-than-function scoped fixture.
+ (was quite some effort so please bear with the complexity of this sentence :)
+ Thanks Ralph Schmitt for the precise failure example.
+
+- fix issue244 by implementing special index for parameters to only use
+ indices for paramentrized test ids
+
+- fix issue287 by running all finalizers but saving the exception
+ from the first failing finalizer and re-raising it so teardown will
+ still have failed. We reraise the first failing exception because
+ 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 fixes issue346 and should
+ help with random "xdist" collection failures. Thanks to
+ Ronny Pfannschmidt and Donald Stufft for helping to isolate it.
+
+- fix issue357 - special case "-k" expressions to allow for
+ filtering with simple strings that are not valid python expressions.
+ Examples: "-k 1.3" matches all tests parametrized with 1.3.
+ "-k None" filters all tests that have "None" in their name
+ and conversely "-k 'not None'".
+ Previously these examples would raise syntax errors.
+
+- fix issue384 by removing the trial support code
+ since the unittest compat enhancements allow
+ trial to handle it on its own
+
+- don't hide an ImportError when importing a plugin produces one.
+ fixes issue375.
+
+- fix issue275 - allow usefixtures and autouse fixtures
+ for running doctest text files.
+
+- fix issue380 by making --resultlog only rely on longrepr instead
+ of the "reprcrash" attribute which only exists sometimes.
+
+- address issue122: allow @pytest.fixture(params=iterator) by exploding
+ into a list early on.
+
+- fix pexpect-3.0 compatibility for pytest's own tests.
+ (fixes issue386)
+
+- allow nested parametrize-value markers, thanks James Lan for the PR.
+
+- fix unicode handling with new monkeypatch.setattr(import_path, value)
+ API. Thanks Rob Dennis. Fixes issue371.
+
+- fix unicode handling with junitxml, fixes issue368.
+
+- In assertion rewriting mode on Python 2, fix the detection of coding
+ cookies. See issue #330.
+
+- make "--runxfail" turn imperative pytest.xfail calls into no ops
+ (it already did neutralize pytest.mark.xfail markers)
+
+- refine pytest / pkg_resources interactions: The AssertionRewritingHook
+ PEP302 compliant loader now registers itself with setuptools/pkg_resources
+ properly so that the pkg_resources.resource_stream method works properly.
+ Fixes issue366. Thanks for the investigations and full PR to Jason R. Coombs.
+
+- pytestconfig fixture is now session-scoped as it is the same object during the
+ whole test run. Fixes issue370.
+
+- avoid one surprising case of marker malfunction/confusion::
+
+ @pytest.mark.some(lambda arg: ...)
+ def test_function():
+
+ would not work correctly because pytest assumes @pytest.mark.some
+ gets a function to be decorated already. We now at least detect if this
+ arg is a lambda and thus the example will work. Thanks Alex Gaynor
+ for bringing it up.
+
+- xfail a test on pypy that checks wrong encoding/ascii (pypy does
+ not error out). fixes issue385.
+
+- internally make varnames() deal with classes's __init__,
+ although it's not needed by pytest itself atm. Also
+ fix caching. Fixes issue376.
+
+- fix issue221 - handle importing of namespace-package with no
+ __init__.py properly.
+
+- refactor internal FixtureRequest handling to avoid monkeypatching.
+ One of the positive user-facing effects is that the "request" object
+ can now be used in closures.
+
+- fixed version comparison in pytest.importskip(modname, minverstring)
+
+- fix issue377 by clarifying in the nose-compat docs that pytest
+ does not duplicate the unittest-API into the "plain" namespace.
+
+- fix verbose reporting for @mock'd test functions
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.5.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.5.1.rst
new file mode 100644
index 0000000000..ff39db2d52
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.5.1.rst
@@ -0,0 +1,46 @@
+pytest-2.5.1: fixes and new home page styling
+===========================================================================
+
+pytest is a mature Python testing tool with more than 1000 tests
+against itself, passing on many different interpreters and platforms.
+
+The 2.5.1 release maintains the "zero-reported-bugs" promise by fixing
+the three bugs reported since the last release a few days ago. It also
+features a new home page styling implemented by Tobias Bieniek, based on
+the flask theme from Armin Ronacher:
+
+ http://pytest.org
+
+If you have anything more to improve styling and docs,
+we'd be very happy to merge further pull requests.
+
+On the coding side, the release also contains a little enhancement to
+fixture decorators allowing to directly influence generation of test
+ids, thanks to Floris Bruynooghe. Other thanks for helping with
+this release go to Anatoly Bubenkoff and Ronny Pfannschmidt.
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+have fun and a nice remaining "bug-free" time of the year :)
+holger krekel
+
+2.5.1
+-----------------------------------
+
+- merge new documentation styling PR from Tobias Bieniek.
+
+- fix issue403: allow parametrize of multiple same-name functions within
+ a collection node. Thanks Andreas Kloeckner and Alex Gaynor for reporting
+ and analysis.
+
+- Allow parameterized fixtures to specify the ID of the parameters by
+ adding an ids argument to pytest.fixture() and pytest.yield_fixture().
+ Thanks Floris Bruynooghe.
+
+- fix issue404 by always using the binary xml escape in the junitxml
+ plugin. Thanks Ronny Pfannschmidt.
+
+- fix issue407: fix addoption docstring to point to argparse instead of
+ optparse. Thanks Daniel D. Wright.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.5.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.5.2.rst
new file mode 100644
index 0000000000..edc4da6e19
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.5.2.rst
@@ -0,0 +1,63 @@
+pytest-2.5.2: fixes
+===========================================================================
+
+pytest is a mature Python testing tool with more than 1000 tests
+against itself, passing on many different interpreters and platforms.
+
+The 2.5.2 release fixes a few bugs with two maybe-bugs remaining and
+actively being worked on (and waiting for the bug reporter's input).
+We also have a new contribution guide thanks to Piotr Banaszkiewicz
+and others.
+
+See docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to the following people who contributed to this release:
+
+ Anatoly Bubenkov
+ Ronny Pfannschmidt
+ Floris Bruynooghe
+ Bruno Oliveira
+ Andreas Pelme
+ Jurko Gospodnetić
+ Piotr Banaszkiewicz
+ Simon Liedtke
+ lakka
+ Lukasz Balcerzak
+ Philippe Muller
+ Daniel Hahler
+
+have fun,
+holger krekel
+
+2.5.2
+-----------------------------------
+
+- fix issue409 -- better interoperate with cx_freeze by not
+ trying to import from collections.abc which causes problems
+ for py27/cx_freeze. Thanks Wolfgang L. for reporting and tracking it down.
+
+- fixed docs and code to use "pytest" instead of "py.test" almost everywhere.
+ Thanks Jurko Gospodnetic for the complete PR.
+
+- fix issue425: mention at end of "py.test -h" that --markers
+ and --fixtures work according to specified test path (or current dir)
+
+- fix issue413: exceptions with unicode attributes are now printed
+ correctly also on python2 and with pytest-xdist runs. (the fix
+ requires py-1.4.20)
+
+- copy, cleanup and integrate py.io capture
+ from pylib 1.4.20.dev2 (rev 13d9af95547e)
+
+- address issue416: clarify docs as to conftest.py loading semantics
+
+- fix issue429: comparing byte strings with non-ascii chars in assert
+ expressions now work better. Thanks Floris Bruynooghe.
+
+- make capfd/capsys.capture private, its unused and shouldn't be exposed
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.0.rst
new file mode 100644
index 0000000000..56fbd6cc1e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.0.rst
@@ -0,0 +1,153 @@
+pytest-2.6.0: shorter tracebacks, new warning system, test runner compat
+===========================================================================
+
+pytest is a mature Python testing tool with more than 1000 tests
+against itself, passing on many different interpreters and platforms.
+
+The 2.6.0 release should be drop-in backward compatible to 2.5.2 and
+fixes a number of bugs and brings some new features, mainly:
+
+- shorter tracebacks by default: only the first (test function) entry
+ and the last (failure location) entry are shown, the ones between
+ only in "short" format. Use ``--tb=long`` to get back the old
+ behaviour of showing "long" entries everywhere.
+
+- a new warning system which reports oddities during collection
+ and execution. For example, ignoring collecting Test* classes with an
+ ``__init__`` now produces a warning.
+
+- various improvements to nose/mock/unittest integration
+
+Note also that 2.6.0 departs with the "zero reported bugs" policy
+because it has been too hard to keep up with it, unfortunately.
+Instead we are for now rather bound to work on "upvoted" issues in
+the https://bitbucket.org/pytest-dev/pytest/issues?status=new&status=open&sort=-votes
+issue tracker.
+
+See docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed, among them:
+
+ Benjamin Peterson
+ Jurko Gospodnetić
+ Floris Bruynooghe
+ Marc Abramowitz
+ Marc Schlaich
+ Trevor Bekolay
+ Bruno Oliveira
+ Alex Groenholm
+
+have fun,
+holger krekel
+
+2.6.0
+-----------------------------------
+
+- fix issue537: Avoid importing old assertion reinterpretation code by default.
+ Thanks Benjamin Peterson.
+
+- fix issue364: shorten and enhance tracebacks representation by default.
+ The new "--tb=auto" option (default) will only display long tracebacks
+ for the first and last entry. You can get the old behaviour of printing
+ all entries as long entries with "--tb=long". Also short entries by
+ default are now printed very similarly to "--tb=native" ones.
+
+- fix issue514: teach assertion reinterpretation about private class attributes
+ Thanks Benjamin Peterson.
+
+- change -v output to include full node IDs of tests. Users can copy
+ a node ID from a test run, including line number, and use it as a
+ positional argument in order to run only a single test.
+
+- fix issue 475: fail early and comprehensible if calling
+ pytest.raises with wrong exception type.
+
+- fix issue516: tell in getting-started about current dependencies.
+
+- 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
+ with -v.
+
+- fix issue473: work around mock putting an unbound method into a class
+ dict when double-patching.
+
+- fix issue498: if a fixture finalizer fails, make sure that
+ the fixture is still invalidated.
+
+- fix issue453: the result of the pytest_assertrepr_compare hook now gets
+ it's newlines escaped so that format_exception does not blow up.
+
+- internal new warning system: pytest will now produce warnings when
+ it detects oddities in your test collection or execution.
+ Warnings are ultimately sent to a new pytest_logwarning hook which is
+ currently only implemented by the terminal plugin which displays
+ warnings in the summary line and shows more details when -rw (report on
+ warnings) is specified.
+
+- change skips into warnings for test classes with an __init__ and
+ callables in test modules which look like a test but are not functions.
+
+- fix issue436: improved finding of initial conftest files from command
+ line arguments by using the result of parse_known_args rather than
+ the previous flaky heuristics. Thanks Marc Abramowitz for tests
+ and initial fixing approaches in this area.
+
+- fix issue #479: properly handle nose/unittest(2) SkipTest exceptions
+ during collection/loading of test modules. Thanks to Marc Schlaich
+ for the complete PR.
+
+- fix issue490: include pytest_load_initial_conftests in documentation
+ and improve docstring.
+
+- fix issue472: clarify that ``pytest.config.getvalue()`` cannot work
+ if it's triggered ahead of command line parsing.
+
+- merge PR123: improved integration with mock.patch decorator on tests.
+
+- fix issue412: messing with stdout/stderr FD-level streams is now
+ captured without crashes.
+
+- fix issue483: trial/py33 works now properly. Thanks Daniel Grana for PR.
+
+- improve example for pytest integration with "python setup.py test"
+ which now has a generic "-a" or "--pytest-args" option where you
+ can pass additional options as a quoted string. Thanks Trevor Bekolay.
+
+- simplified internal capturing mechanism and made it more robust
+ against tests or setups changing FD1/FD2, also better integrated
+ now with pytest.pdb() in single tests.
+
+- improvements to pytest's own test-suite leakage detection, courtesy of PRs
+ from Marc Abramowitz
+
+- fix issue492: avoid leak in test_writeorg. Thanks Marc Abramowitz.
+
+- fix issue493: don't run tests in doc directory with ``python setup.py test``
+ (use tox -e doctesting for that)
+
+- fix issue486: better reporting and handling of early conftest loading failures
+
+- some cleanup and simplification of internal conftest handling.
+
+- work a bit harder to break reference cycles when catching exceptions.
+ Thanks Jurko Gospodnetic.
+
+- fix issue443: fix skip examples to use proper comparison. Thanks Alex
+ Groenholm.
+
+- support nose-style ``__test__`` attribute on modules, classes and
+ functions, including unittest-style Classes. If set to False, the
+ test will not be collected.
+
+- fix issue512: show "<notset>" for arguments which might not be set
+ in monkeypatch plugin. Improves output in documentation.
+
+- avoid importing "py.test" (an old alias module for "pytest")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.1.rst
new file mode 100644
index 0000000000..7469c488e5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.1.rst
@@ -0,0 +1,58 @@
+pytest-2.6.1: fixes and new xfail feature
+===========================================================================
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+The 2.6.1 release is drop-in compatible to 2.5.2 and actually fixes some
+regressions introduced with 2.6.0. It also brings a little feature
+to the xfail marker which now recognizes expected exceptions,
+see the CHANGELOG below.
+
+See docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed, among them:
+
+ Floris Bruynooghe
+ Bruno Oliveira
+ Nicolas Delaby
+
+have fun,
+holger krekel
+
+Changes 2.6.1
+=================
+
+- No longer show line numbers in the --verbose output, the output is now
+ purely the nodeid. The line number is still shown in failure reports.
+ Thanks Floris Bruynooghe.
+
+- fix issue437 where assertion rewriting could cause pytest-xdist worker nodes
+ to collect different tests. Thanks Bruno Oliveira.
+
+- fix issue555: add "errors" attribute to capture-streams to satisfy
+ some distutils and possibly other code accessing sys.stdout.errors.
+
+- fix issue547 capsys/capfd also work when output capturing ("-s") is disabled.
+
+- address issue170: allow pytest.mark.xfail(...) to specify expected exceptions via
+ an optional "raises=EXC" argument where EXC can be a single exception
+ or a tuple of exception classes. Thanks David Mohr for the complete
+ PR.
+
+- fix integration of pytest with unittest.mock.patch decorator when
+ it uses the "new" argument. Thanks Nicolas Delaby for test and PR.
+
+- fix issue with detecting conftest files if the arguments contain
+ "::" node id specifications (copy pasted from "-v" output)
+
+- fix issue544 by only removing "@NUM" at the end of "::" separated parts
+ and if the part has a ".py" extension
+
+- don't use py.std import helper, rather import things directly.
+ Thanks Bruno Oliveira.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.2.rst
new file mode 100644
index 0000000000..9c3b7d96b0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.2.rst
@@ -0,0 +1,51 @@
+pytest-2.6.2: few fixes and cx_freeze support
+===========================================================================
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+This release is drop-in compatible to 2.5.2 and 2.6.X. It also
+brings support for including pytest with cx_freeze or similar
+freezing tools into your single-file app distribution. For details
+see the CHANGELOG below.
+
+See docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed, among them:
+
+ Floris Bruynooghe
+ Benjamin Peterson
+ Bruno Oliveira
+
+have fun,
+holger krekel
+
+2.6.2
+-----------
+
+- Added function pytest.freeze_includes(), which makes it easy to embed
+ pytest into executables using tools like cx_freeze.
+ See docs for examples and rationale. Thanks Bruno Oliveira.
+
+- Improve assertion rewriting cache invalidation precision.
+
+- fixed issue561: adapt autouse fixture example for python3.
+
+- fixed issue453: assertion rewriting issue with __repr__ containing
+ "\n{", "\n}" and "\n~".
+
+- fix issue560: correctly display code if an "else:" or "finally:" is
+ followed by statements on the same line.
+
+- Fix example in monkeypatch documentation, thanks t-8ch.
+
+- fix issue572: correct tmpdir doc example for python3.
+
+- Do not mark as universal wheel because Python 2.6 is different from
+ other builds due to the extra argparse dependency. Fixes issue566.
+ Thanks sontek.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.3.rst
new file mode 100644
index 0000000000..56973a2b2f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.6.3.rst
@@ -0,0 +1,51 @@
+pytest-2.6.3: fixes and little improvements
+===========================================================================
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+This release is drop-in compatible to 2.5.2 and 2.6.X.
+See below for the changes and see docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed, among them:
+
+ Floris Bruynooghe
+ Oleg Sinyavskiy
+ Uwe Schmitt
+ Charles Cloud
+ Wolfgang Schnerring
+
+have fun,
+holger krekel
+
+Changes 2.6.3
+======================
+
+- fix issue575: xunit-xml was reporting collection errors as failures
+ instead of errors, thanks Oleg Sinyavskiy.
+
+- fix issue582: fix setuptools example, thanks Laszlo Papp and Ronny
+ Pfannschmidt.
+
+- Fix infinite recursion bug when pickling capture.EncodedFile, thanks
+ Uwe Schmitt.
+
+- fix issue589: fix bad interaction with numpy and others when showing
+ exceptions. Check for precise "maximum recursion depth exceed" exception
+ instead of presuming any RuntimeError is that one (implemented in py
+ dep). Thanks Charles Cloud for analysing the issue.
+
+- fix conftest related fixture visibility issue: when running with a
+ CWD outside of a test package pytest would get fixture discovery wrong.
+ Thanks to Wolfgang Schnerring for figuring out a reproducible example.
+
+- Introduce pytest_enter_pdb hook (needed e.g. by pytest_timeout to cancel the
+ timeout when interactively entering pdb). Thanks Wolfgang Schnerring.
+
+- check xfail/skip also with non-python function test items. Thanks
+ Floris Bruynooghe.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.7.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.7.0.rst
new file mode 100644
index 0000000000..2840178a07
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.7.0.rst
@@ -0,0 +1,100 @@
+pytest-2.7.0: fixes, features, speed improvements
+===========================================================================
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+This release is supposed to be drop-in compatible to 2.6.X.
+
+See below for the changes and see docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed, among them:
+
+ Anatoly Bubenkoff
+ Floris Bruynooghe
+ Brianna Laugher
+ Eric Siegerman
+ Daniel Hahler
+ Charles Cloud
+ Tom Viner
+ Holger Peters
+ Ldiary Translations
+ almarklein
+
+have fun,
+holger krekel
+
+2.7.0 (compared to 2.6.4)
+-----------------------------
+
+- fix issue435: make reload() work when assert rewriting is active.
+ Thanks Daniel Hahler.
+
+- fix issue616: conftest.py files and their contained fixtures are now
+ properly considered for visibility, independently from the exact
+ current working directory and test arguments that are used.
+ Many thanks to Eric Siegerman and his PR235 which contains
+ systematic tests for conftest visibility and now passes.
+ This change also introduces the concept of a ``rootdir`` which
+ is printed as a new pytest header and documented in the pytest
+ customize web page.
+
+- change reporting of "diverted" tests, i.e. tests that are collected
+ in one file but actually come from another (e.g. when tests in a test class
+ come from a base class in a different file). We now show the nodeid
+ and indicate via a postfix the other file.
+
+- add ability to set command line options by environment variable PYTEST_ADDOPTS.
+
+- added documentation on the new pytest-dev teams on bitbucket and
+ 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
+ will turn import errors in doctests into skips. Thanks Charles Cloud
+ for the complete PR.
+
+- fix issue655: work around different ways that cause python2/3
+ to leak sys.exc_info into fixtures/tests causing failures in 3rd party code
+
+- fix issue615: assertion rewriting did not correctly escape % signs
+ when formatting boolean operations, which tripped over mixing
+ booleans with modulo operators. Thanks to Tom Viner for the report,
+ triaging and fix.
+
+- implement issue351: add ability to specify parametrize ids as a callable
+ to generate custom test ids. Thanks Brianna Laugher for the idea and
+ implementation.
+
+- introduce and document new hookwrapper mechanism useful for plugins
+ which want to wrap the execution of certain hooks for their purposes.
+ This supersedes the undocumented ``__multicall__`` protocol which
+ pytest itself and some external plugins use. Note that pytest-2.8
+ is scheduled to drop supporting the old ``__multicall__``
+ and only support the hookwrapper protocol.
+
+- majorly speed up invocation of plugin hooks
+
+- use hookwrapper mechanism in builtin pytest plugins.
+
+- add a doctest ini option for doctest flags, thanks Holger Peters.
+
+- add note to docs that if you want to mark a parameter and the
+ parameter is a callable, you also need to pass in a reason to disambiguate
+ it from the "decorator" case. Thanks Tom Viner.
+
+- "python_classes" and "python_functions" options now support glob-patterns
+ for test discovery, as discussed in issue600. Thanks Ldiary Translations.
+
+- allow to override parametrized fixtures with non-parametrized ones and vice versa (bubenkoff).
+
+- fix issue463: raise specific error for 'parameterize' misspelling (pfctdayelise).
+
+- On failure, the ``sys.last_value``, ``sys.last_type`` and
+ ``sys.last_traceback`` are set, so that a user can inspect the error
+ via postmortem debugging (almarklein).
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.7.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.7.1.rst
new file mode 100644
index 0000000000..5110c085e0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.7.1.rst
@@ -0,0 +1,58 @@
+pytest-2.7.1: bug fixes
+=======================
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+This release is supposed to be drop-in compatible to 2.7.0.
+
+See below for the changes and see docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+ Bruno Oliveira
+ Holger Krekel
+ Ionel Maries Cristian
+ Floris Bruynooghe
+
+Happy testing,
+The py.test Development Team
+
+
+2.7.1 (compared to 2.7.0)
+-------------------------
+
+- fix issue731: do not get confused by the braces which may be present
+ and unbalanced in an object's repr while collapsing False
+ explanations. Thanks Carl Meyer for the report and test case.
+
+- fix issue553: properly handling inspect.getsourcelines failures in
+ FixtureLookupError which would lead to an internal error,
+ obfuscating the original problem. Thanks talljosh for initial
+ diagnose/patch and Bruno Oliveira for final patch.
+
+- fix issue660: properly report scope-mismatch-access errors
+ independently from ordering of fixture arguments. Also
+ avoid the pytest internal traceback which does not provide
+ information to the user. Thanks Holger Krekel.
+
+- streamlined and documented release process. Also all versions
+ (in setup.py and documentation generation) are now read
+ from _pytest/__init__.py. Thanks Holger Krekel.
+
+- fixed docs to remove the notion that yield-fixtures are experimental.
+ They are here to stay :) Thanks Bruno Oliveira.
+
+- Support building wheels by using environment markers for the
+ requirements. Thanks Ionel Maries Cristian.
+
+- fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing
+ when tests raised SystemExit. Thanks Holger Krekel.
+
+- reintroduced _pytest fixture of the pytester plugin which is used
+ at least by pytest-xdist.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.7.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.7.2.rst
new file mode 100644
index 0000000000..93e5b64eee
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.7.2.rst
@@ -0,0 +1,57 @@
+pytest-2.7.2: bug fixes
+=======================
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+This release is supposed to be drop-in compatible to 2.7.1.
+
+See below for the changes and see docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+ Bruno Oliveira
+ Floris Bruynooghe
+ Punyashloka Biswal
+ Aron Curzon
+ Benjamin Peterson
+ Thomas De Schampheleire
+ Edison Gustavo Muenz
+ Holger Krekel
+
+Happy testing,
+The py.test Development Team
+
+
+2.7.2 (compared to 2.7.1)
+-----------------------------
+
+- fix issue767: pytest.raises value attribute does not contain the exception
+ instance on Python 2.6. Thanks Eric Siegerman for providing the test
+ case and Bruno Oliveira for PR.
+
+- Automatically create directory for junitxml and results log.
+ Thanks Aron Curzon.
+
+- fix issue713: JUnit XML reports for doctest failures.
+ Thanks Punyashloka Biswal.
+
+- fix issue735: assertion failures on debug versions of Python 3.4+
+ Thanks Benjamin Peterson.
+
+- fix issue114: skipif marker reports to internal skipping plugin;
+ Thanks Floris Bruynooghe for reporting and Bruno Oliveira for the PR.
+
+- fix issue748: unittest.SkipTest reports to internal pytest unittest plugin.
+ Thanks Thomas De Schampheleire for reporting and Bruno Oliveira for the PR.
+
+- fix issue718: failed to create representation of sets containing unsortable
+ elements in python 2. Thanks Edison Gustavo Muenz
+
+- fix issue756, fix issue752 (and similar issues): depend on py-1.4.29
+ which has a refined algorithm for traceback generation.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.2.rst
new file mode 100644
index 0000000000..e472633885
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.2.rst
@@ -0,0 +1,44 @@
+pytest-2.8.2: bug fixes
+=======================
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+This release is supposed to be drop-in compatible to 2.8.1.
+
+See below for the changes and see docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+ Bruno Oliveira
+ Demian Brecht
+ Florian Bruhin
+ Ionel Cristian Mărieș
+ Raphael Pierzina
+ Ronny Pfannschmidt
+ holger krekel
+
+Happy testing,
+The py.test Development Team
+
+
+2.8.2 (compared to 2.7.2)
+-----------------------------
+
+- fix #1085: proper handling of encoding errors when passing encoded byte
+ strings to pytest.parametrize in Python 2.
+ Thanks Themanwithoutaplan for the report and Bruno Oliveira for the PR.
+
+- fix #1087: handling SystemError when passing empty byte strings to
+ pytest.parametrize in Python 3.
+ Thanks Paul Kehrer for the report and Bruno Oliveira for the PR.
+
+- fix #995: fixed internal error when filtering tracebacks where one entry
+ was generated by an exec() statement.
+ Thanks Daniel Hahler, Ashley C Straw, Philippe Gauthier and Pavel Savchenko
+ for contributing and Bruno Oliveira for the PR.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.3.rst
new file mode 100644
index 0000000000..3f357252bb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.3.rst
@@ -0,0 +1,58 @@
+pytest-2.8.3: bug fixes
+=======================
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+This release is supposed to be drop-in compatible to 2.8.2.
+
+See below for the changes and see docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+ Bruno Oliveira
+ Florian Bruhin
+ Gabe Hollombe
+ Gabriel Reis
+ Hartmut Goebel
+ John Vandenberg
+ Lee Kamentsky
+ Michael Birtwell
+ Raphael Pierzina
+ Ronny Pfannschmidt
+ William Martin Stewart
+
+Happy testing,
+The py.test Development Team
+
+
+2.8.3 (compared to 2.8.2)
+-----------------------------
+
+- fix #1169: add __name__ attribute to testcases in TestCaseFunction to
+ support the @unittest.skip decorator on functions and methods.
+ Thanks Lee Kamentsky for the PR.
+
+- fix #1035: collecting tests if test module level obj has __getattr__().
+ Thanks Suor for the report and Bruno Oliveira / Tom Viner for the PR.
+
+- fix #331: don't collect tests if their failure cannot be reported correctly
+ e.g. they are a callable instance of a class.
+
+- fix #1133: fixed internal error when filtering tracebacks where one entry
+ belongs to a file which is no longer available.
+ Thanks Bruno Oliveira for the PR.
+
+- enhancement made to highlight in red the name of the failing tests so
+ they stand out in the output.
+ Thanks Gabriel Reis for the PR.
+
+- add more talks to the documentation
+- extend documentation on the --ignore cli option
+- use pytest-runner for setuptools integration
+- minor fixes for interaction with OS X El Capitan system integrity protection (thanks Florian)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.4.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.4.rst
new file mode 100644
index 0000000000..adbdecc87e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.4.rst
@@ -0,0 +1,52 @@
+pytest-2.8.4
+============
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+This release is supposed to be drop-in compatible to 2.8.2.
+
+See below for the changes and see docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+ Bruno Oliveira
+ Florian Bruhin
+ Jeff Widman
+ Mehdy Khoshnoody
+ Nicholas Chammas
+ Ronny Pfannschmidt
+ Tim Chan
+
+
+Happy testing,
+The py.test Development Team
+
+
+2.8.4 (compared to 2.8.3)
+-----------------------------
+
+- fix #1190: ``deprecated_call()`` now works when the deprecated
+ function has been already called by another test in the same
+ module. Thanks Mikhail Chernykh for the report and Bruno Oliveira for the
+ PR.
+
+- fix #1198: ``--pastebin`` option now works on Python 3. Thanks
+ Mehdy Khoshnoody for the PR.
+
+- fix #1219: ``--pastebin`` now works correctly when captured output contains
+ non-ascii characters. Thanks Bruno Oliveira for the PR.
+
+- fix #1204: another error when collecting with a nasty __getattr__().
+ Thanks Florian Bruhin for the PR.
+
+- fix the summary printed when no tests did run.
+ Thanks Florian Bruhin for the PR.
+
+- a number of documentation modernizations wrt good practices.
+ Thanks Bruno Oliveira for the PR.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.5.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.5.rst
new file mode 100644
index 0000000000..c5343d1ea7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.5.rst
@@ -0,0 +1,39 @@
+pytest-2.8.5
+============
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+This release is supposed to be drop-in compatible to 2.8.4.
+
+See below for the changes and see docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+ Alex Gaynor
+ aselus-hub
+ Bruno Oliveira
+ Ronny Pfannschmidt
+
+
+Happy testing,
+The py.test Development Team
+
+
+2.8.5 (compared to 2.8.4)
+-------------------------
+
+- fix #1243: fixed issue where class attributes injected during collection could break pytest.
+ PR by Alexei Kozlenok, thanks Ronny Pfannschmidt and Bruno Oliveira for the review and help.
+
+- fix #1074: precompute junitxml chunks instead of storing the whole tree in objects
+ Thanks Bruno Oliveira for the report and Ronny Pfannschmidt for the PR
+
+- fix #1238: fix ``pytest.deprecated_call()`` receiving multiple arguments
+ (Regression introduced in 2.8.4). Thanks Alex Gaynor for the report and
+ Bruno Oliveira for the PR.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.6.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.6.rst
new file mode 100644
index 0000000000..5d6565b16a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.6.rst
@@ -0,0 +1,67 @@
+pytest-2.8.6
+============
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+This release is supposed to be drop-in compatible to 2.8.5.
+
+See below for the changes and see docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+ AMiT Kumar
+ Bruno Oliveira
+ Erik M. Bray
+ Florian Bruhin
+ Georgy Dyuldin
+ Jeff Widman
+ Kartik Singhal
+ Loïc Estève
+ Manu Phatak
+ Peter Demin
+ Rick van Hattem
+ Ronny Pfannschmidt
+ Ulrich Petri
+ foxx
+
+
+Happy testing,
+The py.test Development Team
+
+
+2.8.6 (compared to 2.8.5)
+-------------------------
+
+- fix #1259: allow for double nodeids in junitxml,
+ this was a regression failing plugins combinations
+ like pytest-pep8 + pytest-flakes
+
+- Workaround for exception that occurs in pyreadline when using
+ ``--pdb`` with standard I/O capture enabled.
+ Thanks Erik M. Bray for the PR.
+
+- fix #900: Better error message in case the target of a ``monkeypatch`` call
+ raises an ``ImportError``.
+
+- fix #1292: monkeypatch calls (setattr, setenv, etc.) are now O(1).
+ Thanks David R. MacIver for the report and Bruno Oliveira for the PR.
+
+- fix #1223: captured stdout and stderr are now properly displayed before
+ entering pdb when ``--pdb`` is used instead of being thrown away.
+ Thanks Cal Leeming for the PR.
+
+- fix #1305: pytest warnings emitted during ``pytest_terminal_summary`` are now
+ properly displayed.
+ Thanks Ionel Maries Cristian for the report and Bruno Oliveira for the PR.
+
+- fix #628: fixed internal UnicodeDecodeError when doctests contain unicode.
+ Thanks Jason R. Coombs for the report and Bruno Oliveira for the PR.
+
+- fix #1334: Add captured stdout to jUnit XML report on setup error.
+ Thanks Georgy Dyuldin for the PR.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.7.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.7.rst
new file mode 100644
index 0000000000..8236a09666
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.8.7.rst
@@ -0,0 +1,31 @@
+pytest-2.8.7
+============
+
+This is a hotfix release to solve a regression
+in the builtin monkeypatch plugin that got introduced in 2.8.6.
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+This release is supposed to be drop-in compatible to 2.8.5.
+
+See below for the changes and see docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+ Ronny Pfannschmidt
+
+
+Happy testing,
+The py.test Development Team
+
+
+2.8.7 (compared to 2.8.6)
+-------------------------
+
+- fix #1338: use predictable object resolution for monkeypatch
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.9.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.9.0.rst
new file mode 100644
index 0000000000..3aea08cb22
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.9.0.rst
@@ -0,0 +1,134 @@
+pytest-2.9.0
+============
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+
+See below for the changes and see docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+ Anatoly Bubenkov
+ Bruno Oliveira
+ Buck Golemon
+ David Vierra
+ Florian Bruhin
+ Galaczi Endre
+ Georgy Dyuldin
+ Lukas Bednar
+ Luke Murphy
+ Marcin Biernat
+ Matt Williams
+ Michael Aquilina
+ Raphael Pierzina
+ Ronny Pfannschmidt
+ Ryan Wooden
+ Tiemo Kieft
+ TomV
+ holger krekel
+ jab
+
+
+Happy testing,
+The py.test Development Team
+
+
+2.9.0 (compared to 2.8.7)
+-------------------------
+
+**New Features**
+
+* New ``pytest.mark.skip`` mark, which unconditionally skips marked tests.
+ Thanks :user:`MichaelAquilina` for the complete PR (:pull:`1040`).
+
+* ``--doctest-glob`` may now be passed multiple times in the command-line.
+ Thanks :user:`jab` and :user:`nicoddemus` for the PR.
+
+* New ``-rp`` and ``-rP`` reporting options give the summary and full output
+ of passing tests, respectively. Thanks to :user:`codewarrior0` for the PR.
+
+* ``pytest.mark.xfail`` now has a ``strict`` option which makes ``XPASS``
+ tests to fail the test suite, defaulting to ``False``. There's also a
+ ``xfail_strict`` ini option that can be used to configure it project-wise.
+ Thanks :user:`rabbbit` for the request and :user:`nicoddemus` for the PR (:issue:`1355`).
+
+* ``Parser.addini`` now supports options of type ``bool``. Thanks
+ :user:`nicoddemus` for the PR.
+
+* New ``ALLOW_BYTES`` doctest option strips ``b`` prefixes from byte strings
+ in doctest output (similar to ``ALLOW_UNICODE``).
+ Thanks :user:`jaraco` for the request and :user:`nicoddemus` for the PR (:issue:`1287`).
+
+* give a hint on KeyboardInterrupt to use the --fulltrace option to show the errors,
+ this fixes :issue:`1366`.
+ Thanks to :user:`hpk42` for the report and :user:`RonnyPfannschmidt` for the PR.
+
+* catch IndexError exceptions when getting exception source location. This fixes
+ pytest internal error for dynamically generated code (fixtures and tests)
+ where source lines are fake by intention
+
+**Changes**
+
+* **Important**: `py.code <https://pylib.readthedocs.io/en/stable/code.html>`_ has been
+ merged into the ``pytest`` repository as ``pytest._code``. This decision
+ was made because ``py.code`` had very few uses outside ``pytest`` and the
+ fact that it was in a different repository made it difficult to fix bugs on
+ its code in a timely manner. The team hopes with this to be able to better
+ refactor out and improve that code.
+ This change shouldn't affect users, but it is useful to let users aware
+ if they encounter any strange behavior.
+
+ Keep in mind that the code for ``pytest._code`` is **private** and
+ **experimental**, so you definitely should not import it explicitly!
+
+ Please note that the original ``py.code`` is still available in
+ `pylib <https://pylib.readthedocs.io/en/stable/>`_.
+
+* ``pytest_enter_pdb`` now optionally receives the pytest config object.
+ Thanks :user:`nicoddemus` for the PR.
+
+* Removed code and documentation for Python 2.5 or lower versions,
+ including removal of the obsolete ``_pytest.assertion.oldinterpret`` module.
+ Thanks :user:`nicoddemus` for the PR (:issue:`1226`).
+
+* Comparisons now always show up in full when ``CI`` or ``BUILD_NUMBER`` is
+ found in the environment, even when -vv isn't used.
+ Thanks :user:`The-Compiler` for the PR.
+
+* ``--lf`` and ``--ff`` now support long names: ``--last-failed`` and
+ ``--failed-first`` respectively.
+ Thanks :user:`MichaelAquilina` for the PR.
+
+* Added expected exceptions to pytest.raises fail message
+
+* Collection only displays progress ("collecting X items") when in a terminal.
+ This avoids cluttering the output when using ``--color=yes`` to obtain
+ colors in CI integrations systems (:issue:`1397`).
+
+**Bug Fixes**
+
+* The ``-s`` and ``-c`` options should now work under ``xdist``;
+ ``Config.fromdictargs`` now represents its input much more faithfully.
+ Thanks to :user:`bukzor` for the complete PR (:issue:`680`).
+
+* Fix (:issue:`1290`): support Python 3.5's ``@`` operator in assertion rewriting.
+ Thanks :user:`Shinkenjoe` for report with test case and :user:`tomviner` for the PR.
+
+* Fix formatting utf-8 explanation messages (:issue:`1379`).
+ Thanks :user:`biern` for the PR.
+
+* Fix `traceback style docs`_ to describe all of the available options
+ (auto/long/short/line/native/no), with ``auto`` being the default since v2.6.
+ Thanks :user:`hackebrot` for the PR.
+
+* Fix (:issue:`1422`): junit record_xml_property doesn't allow multiple records
+ with same name.
+
+
+.. _`traceback style docs`: https://pytest.org/en/stable/how-to/output.html#modifying-python-traceback-printing
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.9.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.9.1.rst
new file mode 100644
index 0000000000..6a627ad3cd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.9.1.rst
@@ -0,0 +1,57 @@
+pytest-2.9.1
+============
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+
+See below for the changes and see docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+ Bruno Oliveira
+ Daniel Hahler
+ Dmitry Malinovsky
+ Florian Bruhin
+ Floris Bruynooghe
+ Matt Bachmann
+ Ronny Pfannschmidt
+ TomV
+ Vladimir Bolshakov
+ Zearin
+ palaviv
+
+
+Happy testing,
+The py.test Development Team
+
+
+2.9.1 (compared to 2.9.0)
+-------------------------
+
+**Bug Fixes**
+
+* Improve error message when a plugin fails to load.
+ Thanks :user:`nicoddemus` for the PR.
+
+* Fix (:issue:`1178`):
+ ``pytest.fail`` with non-ascii characters raises an internal pytest error.
+ Thanks :user:`nicoddemus` for the PR.
+
+* Fix (:issue:`469`): junit parses report.nodeid incorrectly, when params IDs
+ contain ``::``. Thanks :user:`tomviner` for the PR (:pull:`1431`).
+
+* Fix (:issue:`578`): SyntaxErrors
+ containing non-ascii lines at the point of failure generated an internal
+ py.test error.
+ Thanks :user:`asottile` for the report and :user:`nicoddemus` for the PR.
+
+* Fix (:issue:`1437`): When passing in a bytestring regex pattern to parameterize
+ attempt to decode it as utf-8 ignoring errors.
+
+* Fix (:issue:`649`): parametrized test nodes cannot be specified to run on the command line.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.9.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.9.2.rst
new file mode 100644
index 0000000000..2dc82a1117
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-2.9.2.rst
@@ -0,0 +1,65 @@
+pytest-2.9.2
+============
+
+pytest is a mature Python testing tool with more than 1100 tests
+against itself, passing on many different interpreters and platforms.
+
+See below for the changes and see docs at:
+
+ http://pytest.org
+
+As usual, you can upgrade from pypi via::
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+ Adam Chainz
+ Benjamin Dopplinger
+ Bruno Oliveira
+ Florian Bruhin
+ John Towler
+ Martin Prusse
+ Meng Jue
+ MengJueM
+ Omar Kohl
+ Quentin Pradet
+ Ronny Pfannschmidt
+ Thomas Güttler
+ TomV
+ Tyler Goodlet
+
+
+Happy testing,
+The py.test Development Team
+
+
+2.9.2 (compared to 2.9.1)
+---------------------------
+
+**Bug Fixes**
+
+* fix :issue:`510`: skip tests where one parameterize dimension was empty
+ thanks Alex Stapleton for the Report and :user:`RonnyPfannschmidt` for the PR
+
+* Fix Xfail does not work with condition keyword argument.
+ Thanks :user:`astraw38` for reporting the issue (:issue:`1496`) and :user:`tomviner`
+ for PR the (:pull:`1524`).
+
+* Fix win32 path issue when putting custom config file with absolute path
+ in ``pytest.main("-c your_absolute_path")``.
+
+* Fix maximum recursion depth detection when raised error class is not aware
+ of unicode/encoded bytes.
+ Thanks :user:`prusse-martin` for the PR (:pull:`1506`).
+
+* Fix ``pytest.mark.skip`` mark when used in strict mode.
+ Thanks :user:`pquentin` for the PR and :user:`RonnyPfannschmidt` for
+ showing how to fix the bug.
+
+* Minor improvements and fixes to the documentation.
+ Thanks :user:`omarkohl` for the PR.
+
+* Fix ``--fixtures`` to show all fixture definitions as opposed to just
+ one per fixture name.
+ Thanks to :user:`hackebrot` for the PR.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.0.rst
new file mode 100644
index 0000000000..5de3891148
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.0.rst
@@ -0,0 +1,82 @@
+pytest-3.0.0
+============
+
+The pytest team is proud to announce the 3.0.0 release!
+
+pytest is a mature Python testing tool with more than 1600 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a lot of bugs fixes and improvements, and much of
+the work done on it was possible because of the 2016 Sprint[1], which
+was funded by an indiegogo campaign which raised over US$12,000 with
+nearly 100 backers.
+
+There's a "What's new in pytest 3.0" [2] blog post highlighting the
+major features in this release.
+
+To see the complete changelog and documentation, please visit:
+
+ http://docs.pytest.org
+
+As usual, you can upgrade from pypi via:
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+ AbdealiJK
+ Ana Ribeiro
+ Antony Lee
+ Brandon W Maister
+ Brianna Laugher
+ Bruno Oliveira
+ Ceridwen
+ Christian Boelsen
+ Daniel Hahler
+ Danielle Jenkins
+ Dave Hunt
+ Diego Russo
+ Dmitry Dygalo
+ Edoardo Batini
+ Eli Boyarski
+ Florian Bruhin
+ Floris Bruynooghe
+ Greg Price
+ Guyzmo
+ HEAD KANGAROO
+ JJ
+ Javi Romero
+ Javier Domingo Cansino
+ Kale Kundert
+ Kalle Bronsen
+ Marius Gedminas
+ Matt Williams
+ Mike Lundy
+ Oliver Bestwalter
+ Omar Kohl
+ Raphael Pierzina
+ RedBeardCode
+ Roberto Polli
+ Romain Dorgueil
+ Roman Bolshakov
+ Ronny Pfannschmidt
+ Stefan Zimmermann
+ Steffen Allner
+ Tareq Alayan
+ Ted Xiao
+ Thomas Grainger
+ Tom Viner
+ TomV
+ Vasily Kuznetsov
+ aostr
+ marscher
+ palaviv
+ satoru
+ taschini
+
+
+Happy testing,
+The Pytest Development Team
+
+[1] http://blog.pytest.org/2016/pytest-development-sprint/
+[2] http://blog.pytest.org/2016/whats-new-in-pytest-30/
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.1.rst
new file mode 100644
index 0000000000..8f5cfe411a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.1.rst
@@ -0,0 +1,26 @@
+pytest-3.0.1
+============
+
+pytest 3.0.1 has just been released to PyPI.
+
+This release fixes some regressions reported in version 3.0.0, being a
+drop-in replacement. To upgrade:
+
+ pip install --upgrade pytest
+
+The changelog is available at http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+ Adam Chainz
+ Andrew Svetlov
+ Bruno Oliveira
+ Daniel Hahler
+ Dmitry Dygalo
+ Florian Bruhin
+ Marcin Bachry
+ Ronny Pfannschmidt
+ matthiasha
+
+Happy testing,
+The py.test Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.2.rst
new file mode 100644
index 0000000000..86ba82ca6e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.2.rst
@@ -0,0 +1,24 @@
+pytest-3.0.2
+============
+
+pytest 3.0.2 has just been released to PyPI.
+
+This release fixes some regressions and bugs reported in version 3.0.1, being a
+drop-in replacement. To upgrade::
+
+ pip install --upgrade pytest
+
+The changelog is available at http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Ahn Ki-Wook
+* Bruno Oliveira
+* Florian Bruhin
+* Jordan Guymon
+* Raphael Pierzina
+* Ronny Pfannschmidt
+* mbyt
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.3.rst
new file mode 100644
index 0000000000..89a2e0c744
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.3.rst
@@ -0,0 +1,27 @@
+pytest-3.0.3
+============
+
+pytest 3.0.3 has just been released to PyPI.
+
+This release fixes some regressions and bugs reported in the last version,
+being a drop-in replacement. To upgrade::
+
+ pip install --upgrade pytest
+
+The changelog is available at http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Bruno Oliveira
+* Florian Bruhin
+* Floris Bruynooghe
+* Huayi Zhang
+* Lev Maximov
+* Raquel Alegre
+* Ronny Pfannschmidt
+* Roy Williams
+* Tyler Goodlet
+* mbyt
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.4.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.4.rst
new file mode 100644
index 0000000000..72c2d29464
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.4.rst
@@ -0,0 +1,29 @@
+pytest-3.0.4
+============
+
+pytest 3.0.4 has just been released to PyPI.
+
+This release fixes some regressions and bugs reported in the last version,
+being a drop-in replacement. To upgrade::
+
+ pip install --upgrade pytest
+
+The changelog is available at http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Bruno Oliveira
+* Dan Wandschneider
+* Florian Bruhin
+* Georgy Dyuldin
+* Grigorii Eremeev
+* Jason R. Coombs
+* Manuel Jacob
+* Mathieu Clabaut
+* Michael Seifert
+* Nikolaus Rath
+* Ronny Pfannschmidt
+* Tom V
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.5.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.5.rst
new file mode 100644
index 0000000000..97edb7d462
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.5.rst
@@ -0,0 +1,27 @@
+pytest-3.0.5
+============
+
+pytest 3.0.5 has just been released to PyPI.
+
+This is a bug-fix release, being a drop-in replacement. To upgrade::
+
+ pip install --upgrade pytest
+
+The changelog is available at http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Ana Vojnovic
+* Bruno Oliveira
+* Daniel Hahler
+* Duncan Betts
+* Igor Starikov
+* Ismail
+* Luke Murphy
+* Ned Batchelder
+* Ronny Pfannschmidt
+* Sebastian Ramacher
+* nmundar
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.6.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.6.rst
new file mode 100644
index 0000000000..9c072cedcc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.6.rst
@@ -0,0 +1,33 @@
+pytest-3.0.6
+============
+
+pytest 3.0.6 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 http://doc.pytest.org/en/stable/changelog.html.
+
+
+Thanks to all who contributed to this release, among them:
+
+* Andreas Pelme
+* Bruno Oliveira
+* Dmitry Malinovsky
+* Eli Boyarski
+* Jakub Wilk
+* Jeff Widman
+* Loïc Estève
+* Luke Murphy
+* Miro Hrončok
+* Oscar Hellström
+* Peter Heatwole
+* Philippe Ombredanne
+* Ronny Pfannschmidt
+* Rutger Prins
+* Stefan Scherfke
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.7.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.7.rst
new file mode 100644
index 0000000000..4b7e075e76
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.0.7.rst
@@ -0,0 +1,33 @@
+pytest-3.0.7
+============
+
+pytest 3.0.7 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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Anthony Sottile
+* Barney Gale
+* Bruno Oliveira
+* Florian Bruhin
+* Floris Bruynooghe
+* Ionel Cristian Mărieș
+* Katerina Koukiou
+* NODA, Kai
+* Omer Hadari
+* Patrick Hayes
+* Ran Benita
+* Ronny Pfannschmidt
+* Victor Uriarte
+* Vidar Tonaas Fauske
+* Ville Skyttä
+* fbjorn
+* mbyt
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.0.rst
new file mode 100644
index 0000000000..5527706794
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.0.rst
@@ -0,0 +1,61 @@
+pytest-3.1.0
+=======================================
+
+The pytest team is proud to announce the 3.1.0 release!
+
+pytest is a mature Python testing tool with more than 1600 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a bugs fixes and improvements, so users are encouraged
+to take a look at the CHANGELOG:
+
+http://doc.pytest.org/en/stable/changelog.html
+
+For complete documentation, please visit:
+
+ http://docs.pytest.org
+
+As usual, you can upgrade from pypi via:
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+* Anthony Sottile
+* Ben Lloyd
+* Bruno Oliveira
+* David Giese
+* David Szotten
+* Dmitri Pribysh
+* Florian Bruhin
+* Florian Schulze
+* Floris Bruynooghe
+* John Towler
+* Jonas Obrist
+* Katerina Koukiou
+* Kodi Arfer
+* Krzysztof Szularz
+* Lev Maximov
+* Loïc Estève
+* Luke Murphy
+* Manuel Krebber
+* Matthew Duck
+* Matthias Bussonnier
+* Michael Howitz
+* Michal Wajszczuk
+* Paweł Adamczak
+* Rafael Bertoldi
+* Ravi Chandra
+* Ronny Pfannschmidt
+* Skylar Downes
+* Thomas Kriechbaumer
+* Vitaly Lashmanov
+* Vlad Dragos
+* Wheerd
+* Xander Johnson
+* mandeep
+* reut
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.1.rst
new file mode 100644
index 0000000000..135b2fe844
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.1.rst
@@ -0,0 +1,23 @@
+pytest-3.1.1
+=======================================
+
+pytest 3.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Bruno Oliveira
+* Florian Bruhin
+* Floris Bruynooghe
+* Jason R. Coombs
+* Ronny Pfannschmidt
+* wanghui
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.2.rst
new file mode 100644
index 0000000000..a9b85c4715
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.2.rst
@@ -0,0 +1,23 @@
+pytest-3.1.2
+=======================================
+
+pytest 3.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Andreas Pelme
+* ApaDoctor
+* Bruno Oliveira
+* Florian Bruhin
+* Ronny Pfannschmidt
+* Segev Finer
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.3.rst
new file mode 100644
index 0000000000..bc2b85fcfd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.1.3.rst
@@ -0,0 +1,23 @@
+pytest-3.1.3
+=======================================
+
+pytest 3.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Antoine Legrand
+* Bruno Oliveira
+* Max Moroz
+* Raphael Pierzina
+* Ronny Pfannschmidt
+* Ryan Fitzpatrick
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.10.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.10.0.rst
new file mode 100644
index 0000000000..ff3c000b0e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.10.0.rst
@@ -0,0 +1,43 @@
+pytest-3.10.0
+=======================================
+
+The pytest team is proud to announce the 3.10.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Anders Hovmöller
+* Andreu Vallbona Plazas
+* Ankit Goel
+* Anthony Sottile
+* Bernardo Gomes
+* Brianna Laugher
+* Bruno Oliveira
+* Daniel Hahler
+* David Szotten
+* Mick Koch
+* Niclas Olofsson
+* Palash Chatterjee
+* Ronny Pfannschmidt
+* Sven-Hendrik Haase
+* Ville Skyttä
+* William Jamir Silva
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.10.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.10.1.rst
new file mode 100644
index 0000000000..ad365f6347
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.10.1.rst
@@ -0,0 +1,24 @@
+pytest-3.10.1
+=======================================
+
+pytest 3.10.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Boris Feld
+* Bruno Oliveira
+* Daniel Hahler
+* Fabien ZARIFIAN
+* Jon Dufresne
+* Ronny Pfannschmidt
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.0.rst
new file mode 100644
index 0000000000..edc66a28e7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.0.rst
@@ -0,0 +1,48 @@
+pytest-3.2.0
+=======================================
+
+The pytest team is proud to announce the 3.2.0 release!
+
+pytest is a mature Python testing tool with more than 1600 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at the CHANGELOG:
+
+ http://doc.pytest.org/en/stable/changelog.html
+
+For complete documentation, please visit:
+
+ http://docs.pytest.org
+
+As usual, you can upgrade from pypi via:
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+* Alex Hartoto
+* Andras Tim
+* Bruno Oliveira
+* Daniel Hahler
+* Florian Bruhin
+* Floris Bruynooghe
+* John Still
+* Jordan Moldow
+* Kale Kundert
+* Lawrence Mitchell
+* Llandy Riveron Del Risco
+* Maik Figura
+* Martin Altmayer
+* Mihai Capotă
+* Nathaniel Waisbrot
+* Nguyễn Hồng Quân
+* Pauli Virtanen
+* Raphael Pierzina
+* Ronny Pfannschmidt
+* Segev Finer
+* V.Kuznetsov
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.1.rst
new file mode 100644
index 0000000000..c40217d311
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.1.rst
@@ -0,0 +1,22 @@
+pytest-3.2.1
+=======================================
+
+pytest 3.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Alex Gaynor
+* Bruno Oliveira
+* Florian Bruhin
+* Ronny Pfannschmidt
+* Srinivas Reddy Thatiparthy
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.2.rst
new file mode 100644
index 0000000000..5e6c43ab17
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.2.rst
@@ -0,0 +1,28 @@
+pytest-3.2.2
+=======================================
+
+pytest 3.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Andreas Pelme
+* Antonio Hidalgo
+* Bruno Oliveira
+* Felipe Dau
+* Fernando Macedo
+* Jesús Espino
+* Joan Massich
+* Joe Talbott
+* Kirill Pinchuk
+* Ronny Pfannschmidt
+* Xuan Luong
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.3.rst
new file mode 100644
index 0000000000..50dce29c1a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.3.rst
@@ -0,0 +1,23 @@
+pytest-3.2.3
+=======================================
+
+pytest 3.2.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Bruno Oliveira
+* Evan
+* Joe Hamman
+* Oliver Bestwalter
+* Ronny Pfannschmidt
+* Xuan Luong
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.4.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.4.rst
new file mode 100644
index 0000000000..ff0b35781b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.4.rst
@@ -0,0 +1,36 @@
+pytest-3.2.4
+=======================================
+
+pytest 3.2.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Bruno Oliveira
+* Christian Boelsen
+* Christoph Buchner
+* Daw-Ran Liou
+* Florian Bruhin
+* Franck Michea
+* Leonard Lausen
+* Matty G
+* Owen Tuz
+* Pavel Karateev
+* Pierre GIRAUD
+* Ronny Pfannschmidt
+* Stephen Finucane
+* Sviatoslav Abakumov
+* Thomas Hisch
+* Tom Dalton
+* Xuan Luong
+* Yorgos Pagles
+* Семён Марьясин
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.5.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.5.rst
new file mode 100644
index 0000000000..68caccbdbc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.2.5.rst
@@ -0,0 +1,18 @@
+pytest-3.2.5
+=======================================
+
+pytest 3.2.5 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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Bruno Oliveira
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.3.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.3.0.rst
new file mode 100644
index 0000000000..1cbf2c448c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.3.0.rst
@@ -0,0 +1,50 @@
+pytest-3.3.0
+=======================================
+
+The pytest team is proud to announce the 3.3.0 release!
+
+pytest is a mature Python testing tool with more than 1600 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at the CHANGELOG:
+
+ http://doc.pytest.org/en/stable/changelog.html
+
+For complete documentation, please visit:
+
+ http://docs.pytest.org
+
+As usual, you can upgrade from pypi via:
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Ceridwen
+* Daniel Hahler
+* Dirk Thomas
+* Dmitry Malinovsky
+* Florian Bruhin
+* George Y. Kussumoto
+* Hugo
+* Jesús Espino
+* Joan Massich
+* Ofir
+* OfirOshir
+* Ronny Pfannschmidt
+* Samuel Dion-Girardeau
+* Srinivas Reddy Thatiparthy
+* Sviatoslav Abakumov
+* Tarcisio Fischer
+* Thomas Hisch
+* Tyler Goodlet
+* hugovk
+* je
+* prokaktus
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.3.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.3.1.rst
new file mode 100644
index 0000000000..98b6fa6c1b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.3.1.rst
@@ -0,0 +1,25 @@
+pytest-3.3.1
+=======================================
+
+pytest 3.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Bruno Oliveira
+* Daniel Hahler
+* Eugene Prikazchikov
+* Florian Bruhin
+* Roland Puntaier
+* Ronny Pfannschmidt
+* Sebastian Rahlf
+* Tom Viner
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.3.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.3.2.rst
new file mode 100644
index 0000000000..7a2577d1ff
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.3.2.rst
@@ -0,0 +1,28 @@
+pytest-3.3.2
+=======================================
+
+pytest 3.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Anthony Sottile
+* Antony Lee
+* Austin
+* Bruno Oliveira
+* Florian Bruhin
+* Floris Bruynooghe
+* Henk-Jaap Wagenaar
+* Jurko Gospodnetić
+* Ronny Pfannschmidt
+* Srinivas Reddy Thatiparthy
+* Thomas Hisch
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.4.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.4.0.rst
new file mode 100644
index 0000000000..6ab5b124a2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.4.0.rst
@@ -0,0 +1,52 @@
+pytest-3.4.0
+=======================================
+
+The pytest team is proud to announce the 3.4.0 release!
+
+pytest is a mature Python testing tool with more than 1600 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at the CHANGELOG:
+
+ http://doc.pytest.org/en/stable/changelog.html
+
+For complete documentation, please visit:
+
+ http://docs.pytest.org
+
+As usual, you can upgrade from pypi via:
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+* Aaron
+* Alan Velasco
+* Anders Hovmöller
+* Andrew Toolan
+* Anthony Sottile
+* Aron Coyle
+* Brian Maissy
+* Bruno Oliveira
+* Cyrus Maden
+* Florian Bruhin
+* Henk-Jaap Wagenaar
+* Ian Lesperance
+* Jon Dufresne
+* Jurko Gospodnetić
+* Kate
+* Kimberly
+* Per A. Brodtkorb
+* Pierre-Alexandre Fonta
+* Raphael Castaneda
+* Ronny Pfannschmidt
+* ST John
+* Segev Finer
+* Thomas Hisch
+* Tzu-ping Chung
+* feuillemorte
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.4.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.4.1.rst
new file mode 100644
index 0000000000..d83949453a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.4.1.rst
@@ -0,0 +1,27 @@
+pytest-3.4.1
+=======================================
+
+pytest 3.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Aaron
+* Alan Velasco
+* Andy Freeland
+* Brian Maissy
+* Bruno Oliveira
+* Florian Bruhin
+* Jason R. Coombs
+* Marcin Bachry
+* Pedro Algarvio
+* Ronny Pfannschmidt
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.4.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.4.2.rst
new file mode 100644
index 0000000000..07cd9d3a8b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.4.2.rst
@@ -0,0 +1,28 @@
+pytest-3.4.2
+=======================================
+
+pytest 3.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Allan Feldman
+* Bruno Oliveira
+* Florian Bruhin
+* Jason R. Coombs
+* Kyle Altendorf
+* Maik Figura
+* Ronny Pfannschmidt
+* codetriage-readme-bot
+* feuillemorte
+* joshm91
+* mike
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.5.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.5.0.rst
new file mode 100644
index 0000000000..6bc2f3cd0c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.5.0.rst
@@ -0,0 +1,51 @@
+pytest-3.5.0
+=======================================
+
+The pytest team is proud to announce the 3.5.0 release!
+
+pytest is a mature Python testing tool with more than 1600 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at the CHANGELOG:
+
+ http://doc.pytest.org/en/stable/changelog.html
+
+For complete documentation, please visit:
+
+ http://docs.pytest.org
+
+As usual, you can upgrade from pypi via:
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+* Allan Feldman
+* Brian Maissy
+* Bruno Oliveira
+* Carlos Jenkins
+* Daniel Hahler
+* Florian Bruhin
+* Jason R. Coombs
+* Jeffrey Rackauckas
+* Jordan Speicher
+* Julien Palard
+* Kale Kundert
+* Kostis Anagnostopoulos
+* Kyle Altendorf
+* Maik Figura
+* Pedro Algarvio
+* Ronny Pfannschmidt
+* Tadeu Manoel
+* Tareq Alayan
+* Thomas Hisch
+* William Lee
+* codetriage-readme-bot
+* feuillemorte
+* joshm91
+* mike
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.5.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.5.1.rst
new file mode 100644
index 0000000000..802be03684
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.5.1.rst
@@ -0,0 +1,30 @@
+pytest-3.5.1
+=======================================
+
+pytest 3.5.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Brian Maissy
+* Bruno Oliveira
+* Darren Burns
+* David Chudzicki
+* Floris Bruynooghe
+* Holger Kohr
+* Irmen de Jong
+* Jeffrey Rackauckas
+* Rachel Kogan
+* Ronny Pfannschmidt
+* Stefan Scherfke
+* Tim Strazny
+* Семён Марьясин
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.0.rst
new file mode 100644
index 0000000000..44b178c169
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.0.rst
@@ -0,0 +1,41 @@
+pytest-3.6.0
+=======================================
+
+The pytest team is proud to announce the 3.6.0 release!
+
+pytest is a mature Python testing tool with more than 1600 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at the CHANGELOG:
+
+ http://doc.pytest.org/en/stable/changelog.html
+
+For complete documentation, please visit:
+
+ http://docs.pytest.org
+
+As usual, you can upgrade from pypi via:
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+* Anthony Shaw
+* ApaDoctor
+* Brian Maissy
+* Bruno Oliveira
+* Jon Dufresne
+* Katerina Koukiou
+* Miro Hrončok
+* Rachel Kogan
+* Ronny Pfannschmidt
+* Tim Hughes
+* Tyler Goodlet
+* Ville Skyttä
+* aviral1701
+* feuillemorte
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.1.rst
new file mode 100644
index 0000000000..d971a3d490
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.1.rst
@@ -0,0 +1,24 @@
+pytest-3.6.1
+=======================================
+
+pytest 3.6.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Jeffrey Rackauckas
+* Miro Hrončok
+* Niklas Meinzer
+* Oliver Bestwalter
+* Ronny Pfannschmidt
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.2.rst
new file mode 100644
index 0000000000..9d91995793
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.2.rst
@@ -0,0 +1,29 @@
+pytest-3.6.2
+=======================================
+
+pytest 3.6.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Alan Velasco
+* Alex Barbato
+* Anthony Sottile
+* Bartosz Cierocki
+* Bruno Oliveira
+* Daniel Hahler
+* Guoqiang Zhang
+* Hynek Schlawack
+* John T. Wodder II
+* Michael Käufl
+* Ronny Pfannschmidt
+* Samuel Dion-Girardeau
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.3.rst
new file mode 100644
index 0000000000..4dda2460da
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.3.rst
@@ -0,0 +1,27 @@
+pytest-3.6.3
+=======================================
+
+pytest 3.6.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* AdamEr8
+* Anthony Sottile
+* Bruno Oliveira
+* Jean-Paul Calderone
+* Jon Dufresne
+* Marcelo Duarte Trevisani
+* Ondřej Súkup
+* Ronny Pfannschmidt
+* T.E.A de Souza
+* Victor Maryama
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.4.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.4.rst
new file mode 100644
index 0000000000..2c0f9efecc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.6.4.rst
@@ -0,0 +1,24 @@
+pytest-3.6.4
+=======================================
+
+pytest 3.6.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Anthony Sottile
+* Bernhard M. Wiedemann
+* Bruno Oliveira
+* Drew
+* E Hershey
+* Hugo Martins
+* Vlad Shcherbina
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.0.rst
new file mode 100644
index 0000000000..89908a9101
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.0.rst
@@ -0,0 +1,41 @@
+pytest-3.7.0
+=======================================
+
+The pytest team is proud to announce the 3.7.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at the CHANGELOG:
+
+ http://doc.pytest.org/en/stable/changelog.html
+
+For complete documentation, please visit:
+
+ http://docs.pytest.org
+
+As usual, you can upgrade from pypi via:
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+* Alan
+* Alan Brammer
+* Ammar Najjar
+* Anthony Sottile
+* Bruno Oliveira
+* Jeffrey Rackauckas
+* Kale Kundert
+* Ronny Pfannschmidt
+* Serhii Mozghovyi
+* Tadek Teleżyński
+* Wil Cooley
+* abrammer
+* avirlrma
+* turturica
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.1.rst
new file mode 100644
index 0000000000..7da5a3e1f7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.1.rst
@@ -0,0 +1,21 @@
+pytest-3.7.1
+=======================================
+
+pytest 3.7.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Kale Kundert
+* Ronny Pfannschmidt
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.2.rst
new file mode 100644
index 0000000000..fcc6121752
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.2.rst
@@ -0,0 +1,25 @@
+pytest-3.7.2
+=======================================
+
+pytest 3.7.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Josh Holland
+* Ronny Pfannschmidt
+* Sankt Petersbug
+* Wes Thomas
+* turturica
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.3.rst
new file mode 100644
index 0000000000..ee87da60d2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.3.rst
@@ -0,0 +1,32 @@
+pytest-3.7.3
+=======================================
+
+pytest 3.7.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 http://doc.pytest.org/en/stable/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Andrew Champion
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Gandalf Saxe
+* Jennifer Rinker
+* Natan Lao
+* Ondřej Súkup
+* Ronny Pfannschmidt
+* Sankt Petersbug
+* Tyler Richard
+* Victor Maryama
+* Vlad Shcherbina
+* turturica
+* wim glenn
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.4.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.4.rst
new file mode 100644
index 0000000000..45be429388
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.7.4.rst
@@ -0,0 +1,22 @@
+pytest-3.7.4
+=======================================
+
+pytest 3.7.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Jiri Kuncar
+* Steve Piercy
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.8.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.8.0.rst
new file mode 100644
index 0000000000..8c35a44f6d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.8.0.rst
@@ -0,0 +1,38 @@
+pytest-3.8.0
+=======================================
+
+The pytest team is proud to announce the 3.8.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* CrazyMerlyn
+* Daniel Hahler
+* Fabio Zadrozny
+* Jeffrey Rackauckas
+* Ronny Pfannschmidt
+* Virgil Dupras
+* dhirensr
+* hoefling
+* wim glenn
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.8.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.8.1.rst
new file mode 100644
index 0000000000..f8f8accc4c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.8.1.rst
@@ -0,0 +1,25 @@
+pytest-3.8.1
+=======================================
+
+pytest 3.8.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 who contributed to this release, among them:
+
+* Ankit Goel
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Maximilian Albert
+* Ronny Pfannschmidt
+* William Jamir Silva
+* wim glenn
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.8.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.8.2.rst
new file mode 100644
index 0000000000..9ea94c98a2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.8.2.rst
@@ -0,0 +1,28 @@
+pytest-3.8.2
+=======================================
+
+pytest 3.8.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 who contributed to this release, among them:
+
+* Ankit Goel
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Denis Otkidach
+* Harry Percival
+* Jeffrey Rackauckas
+* Jose Carlos Menezes
+* Ronny Pfannschmidt
+* Zac Hatfield-Dodds
+* iwanb
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.0.rst
new file mode 100644
index 0000000000..0be6cf5be8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.0.rst
@@ -0,0 +1,43 @@
+pytest-3.9.0
+=======================================
+
+The pytest team is proud to announce the 3.9.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Andrea Cimatoribus
+* Ankit Goel
+* Anthony Sottile
+* Ben Eyal
+* Bruno Oliveira
+* Daniel Hahler
+* Jeffrey Rackauckas
+* Jose Carlos Menezes
+* Kyle Altendorf
+* Niklas JQ
+* Palash Chatterjee
+* Ronny Pfannschmidt
+* Thomas Hess
+* Thomas Hisch
+* Tomer Keren
+* Victor Maryama
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.1.rst
new file mode 100644
index 0000000000..e1afb3759d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.1.rst
@@ -0,0 +1,20 @@
+pytest-3.9.1
+=======================================
+
+pytest 3.9.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 who contributed to this release, among them:
+
+* Bruno Oliveira
+* Ronny Pfannschmidt
+* Thomas Hisch
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.2.rst
new file mode 100644
index 0000000000..63e94e5aab
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.2.rst
@@ -0,0 +1,23 @@
+pytest-3.9.2
+=======================================
+
+pytest 3.9.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 who contributed to this release, among them:
+
+* Ankit Goel
+* Anthony Sottile
+* Bruno Oliveira
+* Ronny Pfannschmidt
+* Vincent Barbaresi
+* ykantor
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.3.rst
new file mode 100644
index 0000000000..661ddb5cb5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-3.9.3.rst
@@ -0,0 +1,24 @@
+pytest-3.9.3
+=======================================
+
+pytest 3.9.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 who contributed to this release, among them:
+
+* Andreas Profous
+* Ankit Goel
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Jon Dufresne
+* Ronny Pfannschmidt
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.0.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.0.0.rst
new file mode 100644
index 0000000000..5eb0107758
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.0.0.rst
@@ -0,0 +1,30 @@
+pytest-4.0.0
+=======================================
+
+The pytest team is proud to announce the 4.0.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Bruno Oliveira
+* Daniel Hahler
+* Ronny Pfannschmidt
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.0.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.0.1.rst
new file mode 100644
index 0000000000..2902a6db9f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.0.1.rst
@@ -0,0 +1,23 @@
+pytest-4.0.1
+=======================================
+
+pytest 4.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Michael D. Hoyle
+* Ronny Pfannschmidt
+* Slam
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.0.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.0.2.rst
new file mode 100644
index 0000000000..f439b88fe2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.0.2.rst
@@ -0,0 +1,24 @@
+pytest-4.0.2
+=======================================
+
+pytest 4.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Pedro Algarvio
+* Ronny Pfannschmidt
+* Tomer Keren
+* Yash Todi
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.1.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.1.0.rst
new file mode 100644
index 0000000000..314564eeb6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.1.0.rst
@@ -0,0 +1,44 @@
+pytest-4.1.0
+=======================================
+
+The pytest team is proud to announce the 4.1.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Adam Johnson
+* Aly Sivji
+* Andrey Paramonov
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* David Vo
+* Hyunchel Kim
+* Jeffrey Rackauckas
+* Kanguros
+* Nicholas Devenish
+* Pedro Algarvio
+* Randy Barlow
+* Ronny Pfannschmidt
+* Tomer Keren
+* feuillemorte
+* wim glenn
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.1.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.1.1.rst
new file mode 100644
index 0000000000..1f45e082f8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.1.1.rst
@@ -0,0 +1,27 @@
+pytest-4.1.1
+=======================================
+
+pytest 4.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Anton Lodder
+* Bruno Oliveira
+* Daniel Hahler
+* David Vo
+* Oscar Benjamin
+* Ronny Pfannschmidt
+* Victor Maryama
+* Yoav Caspi
+* dmitry.dygalo
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.2.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.2.0.rst
new file mode 100644
index 0000000000..bcd7f77547
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.2.0.rst
@@ -0,0 +1,37 @@
+pytest-4.2.0
+=======================================
+
+The pytest team is proud to announce the 4.2.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Adam Uhlir
+* Anthony Sottile
+* Bruno Oliveira
+* Christopher Dignam
+* Daniel Hahler
+* Joseph Hunkeler
+* Kristoffer Nordstroem
+* Ronny Pfannschmidt
+* Thomas Hisch
+* wim glenn
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.2.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.2.1.rst
new file mode 100644
index 0000000000..36beafe11d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.2.1.rst
@@ -0,0 +1,30 @@
+pytest-4.2.1
+=======================================
+
+pytest 4.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Arel Cordero
+* Bruno Oliveira
+* Daniel Hahler
+* Holger Kohr
+* Kevin J. Foley
+* Nick Murphy
+* Paweł Stradomski
+* Raphael Pierzina
+* Ronny Pfannschmidt
+* Sam Brightman
+* Thomas Hisch
+* Zac Hatfield-Dodds
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.3.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.3.0.rst
new file mode 100644
index 0000000000..3b0b428092
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.3.0.rst
@@ -0,0 +1,36 @@
+pytest-4.3.0
+=======================================
+
+The pytest team is proud to announce the 4.3.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Andras Mitzki
+* Anthony Sottile
+* Bruno Oliveira
+* Christian Fetzer
+* Daniel Hahler
+* Grygorii Iermolenko
+* R. Alex Matevish
+* Ronny Pfannschmidt
+* cclauss
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.3.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.3.1.rst
new file mode 100644
index 0000000000..4251c744e5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.3.1.rst
@@ -0,0 +1,28 @@
+pytest-4.3.1
+=======================================
+
+pytest 4.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 who contributed to this release, among them:
+
+* Andras Mitzki
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Danilo Horta
+* Grygorii Iermolenko
+* Jeff Hale
+* Kyle Altendorf
+* Stephan Hoyer
+* Zac Hatfield-Dodds
+* songbowen
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.4.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.4.0.rst
new file mode 100644
index 0000000000..dc89739d0a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.4.0.rst
@@ -0,0 +1,39 @@
+pytest-4.4.0
+=======================================
+
+The pytest team is proud to announce the 4.4.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Anthony Sottile
+* ApaDoctor
+* Bernhard M. Wiedemann
+* Brian Skinn
+* Bruno Oliveira
+* Daniel Hahler
+* Gary Tyler
+* Jeong YunWon
+* Miro Hrončok
+* Takafumi Arakaki
+* henrykironde
+* smheidrich
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.4.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.4.1.rst
new file mode 100644
index 0000000000..1272cd8fde
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.4.1.rst
@@ -0,0 +1,20 @@
+pytest-4.4.1
+=======================================
+
+pytest 4.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.4.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.4.2.rst
new file mode 100644
index 0000000000..5876e83b3b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.4.2.rst
@@ -0,0 +1,33 @@
+pytest-4.4.2
+=======================================
+
+pytest 4.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 who contributed to this release, among them:
+
+* Allan Lewis
+* Anthony Sottile
+* Bruno Oliveira
+* DamianSkrzypczak
+* Daniel Hahler
+* Don Kirkby
+* Douglas Thor
+* Hugo
+* Ilya Konstantinov
+* Jon Dufresne
+* Matt Cooper
+* Nikolay Kondratyev
+* Ondřej Súkup
+* Peter Schutt
+* Romain Chossart
+* Sitaktif
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.5.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.5.0.rst
new file mode 100644
index 0000000000..d2a05d4f79
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.5.0.rst
@@ -0,0 +1,34 @@
+pytest-4.5.0
+=======================================
+
+The pytest team is proud to announce the 4.5.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Floris Bruynooghe
+* Pulkit Goyal
+* Samuel Searles-Bryant
+* Zac Hatfield-Dodds
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.0.rst
new file mode 100644
index 0000000000..a82fdd47d6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.0.rst
@@ -0,0 +1,43 @@
+pytest-4.6.0
+=======================================
+
+The pytest team is proud to announce the 4.6.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Akiomi Kamakura
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* David Röthlisberger
+* Evan Kepner
+* Jeffrey Rackauckas
+* MyComputer
+* Nikita Krokosh
+* Raul Tambre
+* Thomas Hisch
+* Tim Hoffmann
+* Tomer Keren
+* Victor Maryama
+* danielx123
+* oleg-yegorov
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.1.rst
new file mode 100644
index 0000000000..c79839b7b5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.1.rst
@@ -0,0 +1,19 @@
+pytest-4.6.1
+=======================================
+
+pytest 4.6.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.2.rst
new file mode 100644
index 0000000000..cfc595293a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.2.rst
@@ -0,0 +1,18 @@
+pytest-4.6.2
+=======================================
+
+pytest 4.6.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 who contributed to this release, among them:
+
+* Anthony Sottile
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.3.rst
new file mode 100644
index 0000000000..f578464a7a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.3.rst
@@ -0,0 +1,21 @@
+pytest-4.6.3
+=======================================
+
+pytest 4.6.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Dirk Thomas
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.4.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.4.rst
new file mode 100644
index 0000000000..0eefcbeb1c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.4.rst
@@ -0,0 +1,22 @@
+pytest-4.6.4
+=======================================
+
+pytest 4.6.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Thomas Grainger
+* Zac Hatfield-Dodds
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.5.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.5.rst
new file mode 100644
index 0000000000..1ebf361fdf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.5.rst
@@ -0,0 +1,21 @@
+pytest-4.6.5
+=======================================
+
+pytest 4.6.5 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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Thomas Grainger
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.6.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.6.rst
new file mode 100644
index 0000000000..b3bf1e431c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.6.rst
@@ -0,0 +1,20 @@
+pytest-4.6.6
+=======================================
+
+pytest 4.6.6 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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Michael Goerz
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.7.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.7.rst
new file mode 100644
index 0000000000..f9d01845ec
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.7.rst
@@ -0,0 +1,19 @@
+pytest-4.6.7
+=======================================
+
+pytest 4.6.7 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 who contributed to this release, among them:
+
+* Bruno Oliveira
+* Daniel Hahler
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.8.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.8.rst
new file mode 100644
index 0000000000..5cabe7826e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.8.rst
@@ -0,0 +1,20 @@
+pytest-4.6.8
+=======================================
+
+pytest 4.6.8 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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Ryan Mast
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.9.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.9.rst
new file mode 100644
index 0000000000..7f7bb5996e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-4.6.9.rst
@@ -0,0 +1,21 @@
+pytest-4.6.9
+=======================================
+
+pytest 4.6.9 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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Felix Yan
+* Hugo
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.0.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.0.0.rst
new file mode 100644
index 0000000000..f5e593e9d8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.0.0.rst
@@ -0,0 +1,46 @@
+pytest-5.0.0
+=======================================
+
+The pytest team is proud to announce the 5.0.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Dirk Thomas
+* Evan Kepner
+* Florian Bruhin
+* Hugo
+* Kevin J. Foley
+* Pulkit Goyal
+* Ralph Giles
+* Ronny Pfannschmidt
+* Thomas Grainger
+* Thomas Hisch
+* Tim Gates
+* Victor Maryama
+* Yuri Apollov
+* Zac Hatfield-Dodds
+* curiousjazz77
+* patriksevallius
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.0.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.0.1.rst
new file mode 100644
index 0000000000..e16a8f716f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.0.1.rst
@@ -0,0 +1,25 @@
+pytest-5.0.1
+=======================================
+
+pytest 5.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 who contributed to this release, among them:
+
+* AmirElkess
+* Andreu Vallbona Plazas
+* Anthony Sottile
+* Bruno Oliveira
+* Florian Bruhin
+* Michael Moore
+* Niklas Meinzer
+* Thomas Grainger
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.0.rst
new file mode 100644
index 0000000000..9ab54ff973
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.0.rst
@@ -0,0 +1,56 @@
+pytest-5.1.0
+=======================================
+
+The pytest team is proud to announce the 5.1.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Albert Tugushev
+* Alexey Zankevich
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* David Röthlisberger
+* Florian Bruhin
+* Ilya Stepin
+* Jon Dufresne
+* Kaiqi
+* Max R
+* Miro Hrončok
+* Oliver Bestwalter
+* Ran Benita
+* Ronny Pfannschmidt
+* Samuel Searles-Bryant
+* Semen Zhydenko
+* Steffen Schroeder
+* Thomas Grainger
+* Tim Hoffmann
+* William Woodall
+* Wojtek Erbetowski
+* Xixi Zhao
+* Yash Todi
+* boris
+* dmitry.dygalo
+* helloocc
+* martbln
+* mei-li
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.1.rst
new file mode 100644
index 0000000000..bb8de48014
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.1.rst
@@ -0,0 +1,24 @@
+pytest-5.1.1
+=======================================
+
+pytest 5.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Florian Bruhin
+* Hugo van Kemenade
+* Ran Benita
+* Ronny Pfannschmidt
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.2.rst
new file mode 100644
index 0000000000..c4cb8e3fb4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.2.rst
@@ -0,0 +1,23 @@
+pytest-5.1.2
+=======================================
+
+pytest 5.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 who contributed to this release, among them:
+
+* Andrzej Klajnert
+* Anthony Sottile
+* Bruno Oliveira
+* Christian Neumüller
+* Robert Holt
+* linchiwei123
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.3.rst
new file mode 100644
index 0000000000..c4e88aed28
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.1.3.rst
@@ -0,0 +1,23 @@
+pytest-5.1.3
+=======================================
+
+pytest 5.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Christian Neumüller
+* Daniel Hahler
+* Gene Wood
+* Hugo
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.0.rst
new file mode 100644
index 0000000000..f43767b750
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.0.rst
@@ -0,0 +1,35 @@
+pytest-5.2.0
+=======================================
+
+The pytest team is proud to announce the 5.2.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Andrzej Klajnert
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* James Cooke
+* Michael Goerz
+* Ran Benita
+* Tomáš Chvátal
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.1.rst
new file mode 100644
index 0000000000..fe42b9bf15
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.1.rst
@@ -0,0 +1,23 @@
+pytest-5.2.1
+=======================================
+
+pytest 5.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Florian Bruhin
+* Hynek Schlawack
+* Kevin J. Foley
+* tadashigaki
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.2.rst
new file mode 100644
index 0000000000..89fd6a534d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.2.rst
@@ -0,0 +1,29 @@
+pytest-5.2.2
+=======================================
+
+pytest 5.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 who contributed to this release, among them:
+
+* Albert Tugushev
+* Andrzej Klajnert
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Florian Bruhin
+* Nattaphoom Chaipreecha
+* Oliver Bestwalter
+* Philipp Loose
+* Ran Benita
+* Victor Maryama
+* Yoav Caspi
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.3.rst
new file mode 100644
index 0000000000..bab174495d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.3.rst
@@ -0,0 +1,28 @@
+pytest-5.2.3
+=======================================
+
+pytest 5.2.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Brett Cannon
+* Bruno Oliveira
+* Daniel Hahler
+* Daniil Galiev
+* David Szotten
+* Florian Bruhin
+* Patrick Harmon
+* Ran Benita
+* Zac Hatfield-Dodds
+* Zak Hassan
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.4.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.4.rst
new file mode 100644
index 0000000000..5f51896797
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.2.4.rst
@@ -0,0 +1,22 @@
+pytest-5.2.4
+=======================================
+
+pytest 5.2.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Hugo
+* Michael Shields
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.0.rst
new file mode 100644
index 0000000000..e13a71f09a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.0.rst
@@ -0,0 +1,45 @@
+pytest-5.3.0
+=======================================
+
+The pytest team is proud to announce the 5.3.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bugs fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* AnjoMan
+* Anthony Sottile
+* Anton Lodder
+* Bruno Oliveira
+* Daniel Hahler
+* Gregory Lee
+* Josh Karpel
+* JoshKarpel
+* Joshua Storck
+* Kale Kundert
+* MarcoGorelli
+* Michael Krebs
+* NNRepos
+* Ran Benita
+* TH3CHARLie
+* Tibor Arpas
+* Zac Hatfield-Dodds
+* 林玮
+
+
+Happy testing,
+The Pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.1.rst
new file mode 100644
index 0000000000..d575bb70e3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.1.rst
@@ -0,0 +1,26 @@
+pytest-5.3.1
+=======================================
+
+pytest 5.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Felix Yan
+* Florian Bruhin
+* Mark Dickinson
+* Nikolay Kondratyev
+* Steffen Schroeder
+* Zac Hatfield-Dodds
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.2.rst
new file mode 100644
index 0000000000..d562a33fb0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.2.rst
@@ -0,0 +1,26 @@
+pytest-5.3.2
+=======================================
+
+pytest 5.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Claudio Madotto
+* Daniel Hahler
+* Jared Vasquez
+* Michael Rose
+* Ran Benita
+* Ronny Pfannschmidt
+* Zac Hatfield-Dodds
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.3.rst
new file mode 100644
index 0000000000..40a6fb5b56
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.3.rst
@@ -0,0 +1,30 @@
+pytest-5.3.3
+=======================================
+
+pytest 5.3.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 who contributed to this release, among them:
+
+* Adam Johnson
+* Alexandre Mulatinho
+* Anthony Sottile
+* Bruno Oliveira
+* Chris NeJame
+* Daniel Hahler
+* Hugo van Kemenade
+* Marcelo Duarte Trevisani
+* PaulC
+* Ran Benita
+* Ryan Barner
+* Seth Junot
+* marc
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.4.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.4.rst
new file mode 100644
index 0000000000..0750a9d404
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.4.rst
@@ -0,0 +1,20 @@
+pytest-5.3.4
+=======================================
+
+pytest 5.3.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 who contributed to this release, among them:
+
+* Bruno Oliveira
+* Daniel Hahler
+* Ran Benita
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.5.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.5.rst
new file mode 100644
index 0000000000..e632ce8538
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.3.5.rst
@@ -0,0 +1,19 @@
+pytest-5.3.5
+=======================================
+
+pytest 5.3.5 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 who contributed to this release, among them:
+
+* Daniel Hahler
+* Ran Benita
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.0.rst
new file mode 100644
index 0000000000..43dffc9290
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.0.rst
@@ -0,0 +1,59 @@
+pytest-5.4.0
+=======================================
+
+The pytest team is proud to announce the 5.4.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bug fixes and improvements, so users are encouraged
+to take a look at 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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Christoph Buelter
+* Christoph Bülter
+* Daniel Arndt
+* Daniel Hahler
+* Holger Kohr
+* Hugo
+* Hugo van Kemenade
+* Jakub Mitoraj
+* Kyle Altendorf
+* Minuddin Ahmed Rana
+* Nathaniel Compton
+* ParetoLife
+* Pauli Virtanen
+* Philipp Loose
+* Ran Benita
+* Ronny Pfannschmidt
+* Stefan Scherfke
+* Stefano Mazzucco
+* TWood67
+* Tobias Schmidt
+* Tomáš Gavenčiak
+* Vinay Calastry
+* Vladyslav Rachek
+* Zac Hatfield-Dodds
+* captainCapitalism
+* cmachalo
+* gftea
+* kpinc
+* rebecca-palmer
+* sdementen
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.1.rst
new file mode 100644
index 0000000000..f6a64efa49
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.1.rst
@@ -0,0 +1,18 @@
+pytest-5.4.1
+=======================================
+
+pytest 5.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 who contributed to this release, among them:
+
+* Bruno Oliveira
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.2.rst
new file mode 100644
index 0000000000..d742dd4aad
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.2.rst
@@ -0,0 +1,22 @@
+pytest-5.4.2
+=======================================
+
+pytest 5.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Ran Benita
+* Ronny Pfannschmidt
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.3.rst
new file mode 100644
index 0000000000..6c995c1633
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-5.4.3.rst
@@ -0,0 +1,21 @@
+pytest-5.4.3
+=======================================
+
+pytest 5.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 who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Ran Benita
+* Tor Colvin
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.0.rst
new file mode 100644
index 0000000000..9706fe59bc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.0.rst
@@ -0,0 +1,40 @@
+pytest-6.0.0
+=======================================
+
+The pytest team is proud to announce the 6.0.0 release!
+
+pytest is a mature Python testing tool with more than 2000 tests
+against itself, passing on many different interpreters and platforms.
+
+This release contains a number of bug fixes and improvements, so users are encouraged
+to take a look at the CHANGELOG:
+
+ https://docs.pytest.org/en/latest/changelog.html
+
+For complete documentation, please visit:
+
+ https://docs.pytest.org/en/latest/
+
+As usual, you can upgrade from PyPI via:
+
+ pip install -U pytest
+
+Thanks to all who contributed to this release, among them:
+
+* Anthony Sottile
+* Arvin Firouzi
+* Bruno Oliveira
+* Debi Mishra
+* Garrett Thomas
+* Hugo van Kemenade
+* Kelton Bassingthwaite
+* Kostis Anagnostopoulos
+* Lewis Cowles
+* Miro Hrončok
+* Ran Benita
+* Simon K
+* Zac Hatfield-Dodds
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.0rc1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.0rc1.rst
new file mode 100644
index 0000000000..5690b514ba
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.0rc1.rst
@@ -0,0 +1,67 @@
+pytest-6.0.0rc1
+=======================================
+
+pytest 6.0.0rc1 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/latest/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Alfredo Deza
+* Andreas Maier
+* Andrew
+* Anthony Sottile
+* ArtyomKaltovich
+* Bruno Oliveira
+* Claire Cecil
+* Curt J. Sampson
+* Daniel
+* Daniel Hahler
+* Danny Sepler
+* David Diaz Barquero
+* Fabio Zadrozny
+* Felix Nieuwenhuizen
+* Florian Bruhin
+* Florian Dahlitz
+* Gleb Nikonorov
+* Hugo van Kemenade
+* Hunter Richards
+* Katarzyna Król
+* Katrin Leinweber
+* Keri Volans
+* Lewis Belcher
+* Lukas Geiger
+* Martin Michlmayr
+* Mattwmaster58
+* Maximilian Cosmo Sitter
+* Nikolay Kondratyev
+* Pavel Karateev
+* Paweł Wilczyński
+* Prashant Anand
+* Ram Rachum
+* Ran Benita
+* Ronny Pfannschmidt
+* Ruaridh Williamson
+* Simon K
+* Tim Hoffmann
+* Tor Colvin
+* Vlad-Radz
+* Xinbin Huang
+* Zac Hatfield-Dodds
+* earonesty
+* gaurav dhameeja
+* gdhameeja
+* ibriquem
+* mcsitter
+* piotrhm
+* smarie
+* symonk
+* xuiqzy
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.1.rst
new file mode 100644
index 0000000000..33fdbed3f6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.1.rst
@@ -0,0 +1,21 @@
+pytest-6.0.1
+=======================================
+
+pytest 6.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/latest/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Bruno Oliveira
+* Mattreex
+* Ran Benita
+* hp310780
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.2.rst
new file mode 100644
index 0000000000..16eabc5863
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.0.2.rst
@@ -0,0 +1,19 @@
+pytest-6.0.2
+=======================================
+
+pytest 6.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:
+
+* Bruno Oliveira
+* Ran Benita
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.1.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.1.0.rst
new file mode 100644
index 0000000000..f4b571ae84
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.1.0.rst
@@ -0,0 +1,44 @@
+pytest-6.1.0
+=======================================
+
+The pytest team is proud to announce the 6.1.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:
+
+* Anthony Sottile
+* Bruno Oliveira
+* C. Titus Brown
+* Drew Devereux
+* Faris A Chugthai
+* Florian Bruhin
+* Hugo van Kemenade
+* Hynek Schlawack
+* Joseph Lucas
+* Kamran Ahmad
+* Mattreex
+* Maximilian Cosmo Sitter
+* Ran Benita
+* Rüdiger Busche
+* Sam Estep
+* Sorin Sbarnea
+* Thomas Grainger
+* Vipul Kumar
+* Yutaro Ikeda
+* hp310780
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.1.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.1.1.rst
new file mode 100644
index 0000000000..e09408fdee
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.1.1.rst
@@ -0,0 +1,18 @@
+pytest-6.1.1
+=======================================
+
+pytest 6.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/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.1.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.1.2.rst
new file mode 100644
index 0000000000..aa2c809520
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.1.2.rst
@@ -0,0 +1,22 @@
+pytest-6.1.2
+=======================================
+
+pytest 6.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
+* Manuel Mariñez
+* Ran Benita
+* Vasilis Gerakaris
+* William Jamir Silva
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.0.rst
new file mode 100644
index 0000000000..af16b830dd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.0.rst
@@ -0,0 +1,76 @@
+pytest-6.2.0
+=======================================
+
+The pytest team is proud to announce the 6.2.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:
+
+* Adam Johnson
+* Albert Villanova del Moral
+* Anthony Sottile
+* Anton
+* Ariel Pillemer
+* Bruno Oliveira
+* Charles Aracil
+* Christine M
+* Christine Mecklenborg
+* Cserna Zsolt
+* Dominic Mortlock
+* Emiel van de Laar
+* Florian Bruhin
+* Garvit Shubham
+* Gustavo Camargo
+* Hugo Martins
+* Hugo van Kemenade
+* Jakob van Santen
+* Josias Aurel
+* Jürgen Gmach
+* Karthikeyan Singaravelan
+* Katarzyna
+* Kyle Altendorf
+* Manuel Mariñez
+* Matthew Hughes
+* Matthias Gabriel
+* Max Voitko
+* Maximilian Cosmo Sitter
+* Mikhail Fesenko
+* Nimesh Vashistha
+* Pedro Algarvio
+* Petter Strandmark
+* Prakhar Gurunani
+* Prashant Sharma
+* Ran Benita
+* Ronny Pfannschmidt
+* Sanket Duthade
+* Shubham Adep
+* Simon K
+* Tanvi Mehta
+* Thomas Grainger
+* Tim Hoffmann
+* Vasilis Gerakaris
+* William Jamir Silva
+* Zac Hatfield-Dodds
+* crricks
+* dependabot[bot]
+* duthades
+* frankgerhardt
+* kwgchi
+* mickeypash
+* symonk
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.1.rst
new file mode 100644
index 0000000000..f9e7161835
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.1.rst
@@ -0,0 +1,20 @@
+pytest-6.2.1
+=======================================
+
+pytest 6.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
+* Jakob van Santen
+* Ran Benita
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.2.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.2.rst
new file mode 100644
index 0000000000..c3999c5386
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.2.rst
@@ -0,0 +1,21 @@
+pytest-6.2.2
+=======================================
+
+pytest 6.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:
+
+* Adam Johnson
+* Bruno Oliveira
+* Chris NeJame
+* Ran Benita
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.3.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.3.rst
new file mode 100644
index 0000000000..e45aa6a03e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.3.rst
@@ -0,0 +1,19 @@
+pytest-6.2.3
+=======================================
+
+pytest 6.2.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
+* Ran Benita
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.4.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.4.rst
new file mode 100644
index 0000000000..fa2e3e7813
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.4.rst
@@ -0,0 +1,22 @@
+pytest-6.2.4
+=======================================
+
+pytest 6.2.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:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Christian Maurer
+* Florian Bruhin
+* Ran Benita
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.5.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.5.rst
new file mode 100644
index 0000000000..bc6b4cf422
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-6.2.5.rst
@@ -0,0 +1,30 @@
+pytest-6.2.5
+=======================================
+
+pytest 6.2.5 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
+* Brylie Christopher Oxley
+* Daniel Asztalos
+* Florian Bruhin
+* Jason Haugen
+* MapleCCC
+* Michał Górny
+* Miro Hrončok
+* Ran Benita
+* Ronny Pfannschmidt
+* Sylvain Bellemare
+* Thomas Güttler
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-7.0.0.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-7.0.0.rst
new file mode 100644
index 0000000000..3ce4335564
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-7.0.0.rst
@@ -0,0 +1,74 @@
+pytest-7.0.0
+=======================================
+
+The pytest team is proud to announce the 7.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:
+
+* Adam J. Stewart
+* Alexander King
+* Amin Alaee
+* Andrew Neitsch
+* Anthony Sottile
+* Ben Davies
+* Bernát Gábor
+* Brian Okken
+* Bruno Oliveira
+* Cristian Vera
+* Dan Alvizu
+* David Szotten
+* Eddie
+* Emmanuel Arias
+* Emmanuel Meric de Bellefon
+* Eric Liu
+* Florian Bruhin
+* GergelyKalmar
+* Graeme Smecher
+* Harshna
+* Hugo van Kemenade
+* Jakub Kulík
+* James Myatt
+* Jeff Rasley
+* Kale Kundert
+* Kian Meng, Ang
+* Miro Hrončok
+* Naveen-Pratap
+* Oleg Höfling
+* Olga Matoula
+* Ran Benita
+* Ronny Pfannschmidt
+* Simon K
+* Srip
+* Sören Wegener
+* Taneli Hukkinen
+* Terje Runde
+* Thomas Grainger
+* Thomas Hisch
+* William Jamir Silva
+* Yuval Shimon
+* Zac Hatfield-Dodds
+* andrewdotn
+* denivyruck
+* ericluoliu
+* oleg.hoefling
+* symonk
+* ziebam
+* Éloi Rivard
+* Éric
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-7.0.0rc1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-7.0.0rc1.rst
new file mode 100644
index 0000000000..a5bf0ed3c4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-7.0.0rc1.rst
@@ -0,0 +1,74 @@
+pytest-7.0.0rc1
+=======================================
+
+The pytest team is proud to announce the 7.0.0rc1 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==7.0.0rc1
+
+Users are encouraged to take a look at the CHANGELOG carefully:
+
+ https://docs.pytest.org/en/7.0.x/changelog.html
+
+Thanks to all the contributors to this release:
+
+* Adam J. Stewart
+* Alexander King
+* Amin Alaee
+* Andrew Neitsch
+* Anthony Sottile
+* Ben Davies
+* Bernát Gábor
+* Brian Okken
+* Bruno Oliveira
+* Cristian Vera
+* David Szotten
+* Eddie
+* Emmanuel Arias
+* Emmanuel Meric de Bellefon
+* Eric Liu
+* Florian Bruhin
+* GergelyKalmar
+* Graeme Smecher
+* Harshna
+* Hugo van Kemenade
+* Jakub Kulík
+* James Myatt
+* Jeff Rasley
+* Kale Kundert
+* Miro Hrončok
+* Naveen-Pratap
+* Oleg Höfling
+* Ran Benita
+* Ronny Pfannschmidt
+* Simon K
+* Srip
+* Sören Wegener
+* Taneli Hukkinen
+* Terje Runde
+* Thomas Grainger
+* Thomas Hisch
+* William Jamir Silva
+* Zac Hatfield-Dodds
+* andrewdotn
+* denivyruck
+* ericluoliu
+* oleg.hoefling
+* symonk
+* ziebam
+* Éloi Rivard
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-7.0.1.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-7.0.1.rst
new file mode 100644
index 0000000000..5accfbad0d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/release-7.0.1.rst
@@ -0,0 +1,20 @@
+pytest-7.0.1
+=======================================
+
+pytest 7.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:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Ran Benita
+
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/sprint2016.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/sprint2016.rst
new file mode 100644
index 0000000000..8e70658987
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/announce/sprint2016.rst
@@ -0,0 +1,64 @@
+python testing sprint June 20th-26th 2016
+======================================================
+
+.. image:: ../img/freiburg2.jpg
+ :width: 400
+
+The pytest core group held the biggest sprint
+in its history in June 2016, taking place in the black forest town Freiburg
+in Germany. In February 2016 we started a `funding
+campaign on Indiegogo to cover expenses
+<http://igg.me/at/pytest-sprint/x/4034848>`_ The page also mentions
+some preliminary topics:
+
+- improving pytest-xdist test scheduling to take into account
+ fixture setups and explicit user hints.
+
+- provide info on fixture dependencies during --collect-only
+
+- tying pytest-xdist to tox so that you can do "py.test -e py34"
+ to run tests in a particular tox-managed virtualenv. Also
+ look into making pytest-xdist use tox environments on
+ remote ssh-sides so that remote dependency management becomes
+ easier.
+
+- refactoring the fixture system so more people understand it :)
+
+- integrating PyUnit setup methods as autouse fixtures.
+ possibly adding ways to influence ordering of same-scoped
+ fixtures (so you can make a choice of which fixtures come
+ before others)
+
+- fixing bugs and issues from the tracker, really an endless source :)
+
+
+Participants
+--------------
+
+Over 20 participants took part from 4 continents, including employees
+from Splunk, Personalkollen, Cobe.io, FanDuel and Dolby. Some newcomers
+mixed with developers who have worked on pytest since its beginning, and
+of course everyone in between.
+
+
+Sprint organisation, schedule
+-------------------------------
+
+People arrived in Freiburg on the 19th, with sprint development taking
+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
+as well.
+
+
+Money / funding
+---------------
+
+
+The Indiegogo campaign aimed for 11000 USD and in the end raised over
+12000, to reimburse travel costs, pay for a sprint venue and catering.
+
+Excess money is reserved for further sprint/travel funding for pytest/tox
+contributors.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/backwards-compatibility.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/backwards-compatibility.rst
new file mode 100644
index 0000000000..3a0ff12616
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/backwards-compatibility.rst
@@ -0,0 +1,79 @@
+.. _backwards-compatibility:
+
+Backwards Compatibility Policy
+==============================
+
+.. versionadded: 6.0
+
+pytest is actively evolving and is a project that has been decades in the making,
+we keep learning about new and better structures to express different details about testing.
+
+While we implement those modifications we try to ensure an easy transition and don't want to impose unnecessary churn on our users and community/plugin authors.
+
+As of now, pytest considers multiple types of backward compatibility transitions:
+
+a) trivial: APIs which trivially translate to the new mechanism,
+ and do not cause problematic changes.
+
+ We try to support those indefinitely while encouraging users to switch to newer/better mechanisms through documentation.
+
+b) transitional: the old and new API don't conflict
+ and we can help users transition by using warnings, while supporting both for a prolonged time.
+
+ 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`).
+
+ 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.
+
+
+c) true breakage: should only be considered when normal transition is unreasonably unsustainable and would offset important development/features by years.
+ In addition, they should be limited to APIs where the number of actual users is very small (for example only impacting some plugins), and can be coordinated with the community in advance.
+
+ Examples for such upcoming changes:
+
+ * removal of ``pytest_runtest_protocol/nextitem`` - :issue:`895`
+ * rearranging of the node tree to include ``FunctionDefinition``
+ * rearranging of ``SetupState`` :issue:`895`
+
+ True breakages must be announced first in an issue containing:
+
+ * Detailed description of the change
+ * Rationale
+ * Expected impact on users and plugin authors (example in :issue:`895`)
+
+ After there's no hard *-1* on the issue it should be followed up by an initial proof-of-concept Pull Request.
+
+ This POC serves as both a coordination point to assess impact and potential inspiration to come up with a transitional solution after all.
+
+ After a reasonable amount of time the PR can be merged to base a new major release.
+
+ For the PR to mature from POC to acceptance, it must contain:
+ * Setup of deprecation errors/warnings that help users fix and port their code. If it is possible to introduce a deprecation period under the current series, before the true breakage, it should be introduced in a separate PR and be part of the current release stream.
+ * Detailed description of the rationale and examples on how to port code in ``doc/en/deprecations.rst``.
+
+
+History
+=========
+
+
+Focus primary on smooth transition - stance (pre 6.0)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Keeping backwards compatibility has a very high priority in the pytest project. Although we have deprecated functionality over the years, most of it is still supported. All deprecations in pytest were done because simpler or more efficient ways of accomplishing the same tasks have emerged, making the old way of doing things unnecessary.
+
+With the pytest 3.0 release we introduced a clear communication scheme for when we will actually remove the old busted joint and politely ask you to use the new hotness instead, while giving you enough time to adjust your tests or raise concerns if there are valid reasons to keep deprecated functionality around.
+
+To communicate changes we issue deprecation warnings using a custom warning hierarchy (see :ref:`internal-warnings`). These warnings may be suppressed using the standard means: ``-W`` command-line flag or ``filterwarnings`` ini options (see :ref:`warnings`), but we suggest to use these sparingly and temporarily, and heed the warnings when possible.
+
+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).
+
+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 them 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.
+
+
+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.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/builtin.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/builtin.rst
new file mode 100644
index 0000000000..c7e7863b21
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/builtin.rst
@@ -0,0 +1,197 @@
+:orphan:
+
+.. _`pytest helpers`:
+
+Pytest API and builtin fixtures
+================================================
+
+
+Most of the information of this page has been moved over to :ref:`api-reference`.
+
+For information on plugin hooks and objects, see :ref:`plugins`.
+
+For information on the ``pytest.mark`` mechanism, see :ref:`mark`.
+
+For information about fixtures, see :ref:`fixtures`. To see a complete list of available fixtures (add ``-v`` to also see fixtures with leading ``_``), type :
+
+.. code-block:: pytest
+
+ $ pytest --fixtures -v
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.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
+ Return a cache object that can persist state between testing sessions.
+
+ cache.get(key, default)
+ cache.set(key, value)
+
+ Keys must be ``/`` separated strings, where the first part is usually the
+ name of your plugin or application to avoid clashes with other cache users.
+
+ 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
+ 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
+ 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
+ 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
+ Fixture that returns a :py:class:`dict` that will be injected into the
+ namespace of doctests.
+
+ pytestconfig [session scope] -- .../_pytest/fixtures.py:1365
+ Session-scoped fixture that returns the session's :class:`pytest.Config`
+ object.
+
+ Example::
+
+ def test_foo(pytestconfig):
+ if pytestconfig.getoption("verbose") > 0:
+ ...
+
+ record_property -- .../_pytest/junitxml.py:282
+ Add extra properties to the calling test.
+
+ User properties become part of the test report and are available to the
+ configured reporters, like JUnit XML.
+
+ The fixture is callable with ``name, value``. The value is automatically
+ XML-encoded.
+
+ Example::
+
+ def test_function(record_property):
+ record_property("example_key", 1)
+
+ record_xml_attribute -- .../_pytest/junitxml.py:305
+ 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 a new ``<property>`` tag as child of the root ``<testsuite>``.
+
+ This is suitable to writing global information regarding the entire test
+ suite, and is compatible with ``xunit2`` JUnit family.
+
+ This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
+
+ .. code-block:: python
+
+ def test_foo(record_testsuite_property):
+ 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.
+
+ .. warning::
+
+ Currently this fixture **does not work** with the
+ `pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
+ :issue:`7767` for details.
+
+ tmpdir_factory [session scope] -- .../_pytest/legacypath.py:295
+ Return a :class:`pytest.TempdirFactory` instance for the test session.
+
+ tmpdir -- .../_pytest/legacypath.py:302
+ 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`.
+
+ The returned object is a `legacy_path`_ object.
+
+ .. _legacy_path: https://py.readthedocs.io/en/latest/path.html
+
+ caplog -- .../_pytest/logging.py:483
+ Access and control log capturing.
+
+ Captured logs are available through the following properties/methods::
+
+ * caplog.messages -> list of format-interpolated log messages
+ * caplog.text -> string containing formatted log output
+ * caplog.records -> list of logging.LogRecord instances
+ * 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
+ A convenient fixture for monkey-patching.
+
+ The fixture provides these methods to modify objects, dictionaries or
+ 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)
+
+ 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.
+
+ recwarn -- .../_pytest/recwarn.py:29
+ 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
+ on warning categories.
+
+ tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:183
+ Return a :class:`pytest.TempPathFactory` instance for the test session.
+
+ tmp_path -- .../_pytest/tmpdir.py:198
+ 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`.
+
+ The returned object is a :class:`pathlib.Path` object.
+
+
+ ========================== no tests ran in 0.12s ===========================
+
+You can also interactively ask for help, e.g. by typing on the Python interactive prompt something like:
+
+.. code-block:: python
+
+ import pytest
+
+ help(pytest)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/changelog.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/changelog.rst
new file mode 100644
index 0000000000..1acdad366d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/changelog.rst
@@ -0,0 +1,9044 @@
+.. _`changelog`:
+
+=========
+Changelog
+=========
+
+Versions follow `Semantic Versioning <https://semver.org/>`_ (``<major>.<minor>.<patch>``).
+
+Backward incompatible (breaking) changes will only be introduced in major versions
+with advance notice in the **Deprecations** section of releases.
+
+
+..
+ 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
+
+pytest 7.0.1 (2022-02-11)
+=========================
+
+Bug Fixes
+---------
+
+- `#9608 <https://github.com/pytest-dev/pytest/issues/9608>`_: Fix invalid importing of ``importlib.readers`` in Python 3.9.
+
+
+- `#9610 <https://github.com/pytest-dev/pytest/issues/9610>`_: Restore `UnitTestFunction.obj` to return unbound rather than bound method.
+ Fixes a crash during a failed teardown in unittest TestCases with non-default `__init__`.
+ Regressed in pytest 7.0.0.
+
+
+- `#9636 <https://github.com/pytest-dev/pytest/issues/9636>`_: The ``pythonpath`` plugin was renamed to ``python_path``. This avoids a conflict with the ``pytest-pythonpath`` plugin.
+
+
+- `#9642 <https://github.com/pytest-dev/pytest/issues/9642>`_: Fix running tests by id with ``::`` in the parametrize portion.
+
+
+- `#9643 <https://github.com/pytest-dev/pytest/issues/9643>`_: Delay issuing a :class:`~pytest.PytestWarning` about diamond inheritance involving :class:`~pytest.Item` and
+ :class:`~pytest.Collector` so it can be filtered using :ref:`standard warning filters <warnings>`.
+
+
+pytest 7.0.0 (2022-02-03)
+=========================
+
+(**Please see the full set of changes for this release also in the 7.0.0rc1 notes below**)
+
+Deprecations
+------------
+
+- `#9488 <https://github.com/pytest-dev/pytest/issues/9488>`_: If custom subclasses of nodes like :class:`pytest.Item` override the
+ ``__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
+ arguments pytest expected to pass. This deprecation was already part of pytest
+ 7.0.0rc1 but wasn't documented.
+
+
+
+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``).
+
+
+
+Improved Documentation
+----------------------
+
+- `#9404 <https://github.com/pytest-dev/pytest/issues/9404>`_: Added extra documentation on alternatives to common misuses of `pytest.warns(None)` ahead of its deprecation.
+
+
+- `#9505 <https://github.com/pytest-dev/pytest/issues/9505>`_: Clarify where the configuration files are located. To avoid confusions documentation mentions
+ that configuration file is located in the root of the repository.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- `#9521 <https://github.com/pytest-dev/pytest/issues/9521>`_: Add test coverage to assertion rewrite path.
+
+
+pytest 7.0.0rc1 (2021-12-06)
+============================
+
+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.
+
+ 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.
+
+ Note: pytest was not able to provide a deprecation period for this change.
+
+
+- `#8246 <https://github.com/pytest-dev/pytest/issues/8246>`_: ``--version`` now writes version information to ``stdout`` rather than ``stderr``.
+
+
+- `#8733 <https://github.com/pytest-dev/pytest/issues/8733>`_: Drop a workaround for `pyreadline <https://github.com/pyreadline/pyreadline>`__ that made it work with ``--pdb``.
+
+ The workaround was introduced in `#1281 <https://github.com/pytest-dev/pytest/pull/1281>`__ in 2015, however since then
+ `pyreadline seems to have gone unmaintained <https://github.com/pyreadline/pyreadline/issues/58>`__, is `generating
+ warnings <https://github.com/pytest-dev/pytest/issues/8847>`__, and will stop working on Python 3.10.
+
+
+- `#9061 <https://github.com/pytest-dev/pytest/issues/9061>`_: Using :func:`pytest.approx` in a boolean context now raises an error hinting at the proper usage.
+
+ It is apparently common for users to mistakenly use ``pytest.approx`` like this:
+
+ .. code-block:: python
+
+ assert pytest.approx(actual, expected)
+
+ While the correct usage is:
+
+ .. code-block:: python
+
+ assert actual == pytest.approx(expected)
+
+ The new error message helps catch those mistakes.
+
+
+- `#9277 <https://github.com/pytest-dev/pytest/issues/9277>`_: The ``pytest.Instance`` collector type has been removed.
+ Importing ``pytest.Instance`` or ``_pytest.python.Instance`` returns a dummy type and emits a deprecation warning.
+ See :ref:`instance-collector-deprecation` for details.
+
+
+- `#9308 <https://github.com/pytest-dev/pytest/issues/9308>`_: **PytestRemovedIn7Warning deprecation warnings are now errors by default.**
+
+ Following our plan to remove deprecated features with as little disruption as
+ possible, all warnings of type ``PytestRemovedIn7Warning`` now generate errors
+ instead of warning messages by default.
+
+ **The affected features will be effectively removed in pytest 7.1**, so please consult the
+ :ref:`deprecations` section in the docs for directions on how to update existing code.
+
+ In the pytest ``7.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.PytestRemovedIn7Warning
+
+ But this will stop working when pytest ``7.1`` is released.
+
+ **If you have concerns** about the removal of a specific feature, please add a
+ comment to :issue:`9308`.
+
+
+
+Deprecations
+------------
+
+- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: ``py.path.local`` arguments for hooks have been deprecated. See :ref:`the deprecation note <legacy-path-hooks-deprecated>` for full details.
+
+ ``py.path.local`` arguments to Node constructors have been deprecated. See :ref:`the deprecation note <node-ctor-fspath-deprecation>` for full details.
+
+ .. note::
+ The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
+ new attribute being ``path``) is **the opposite** of the situation for hooks
+ (the old argument being ``path``).
+
+ This is an unfortunate artifact due to historical reasons, which should be
+ resolved in future versions as we slowly get rid of the :pypi:`py`
+ dependency (see :issue:`9283` for a longer discussion).
+
+
+- `#7469 <https://github.com/pytest-dev/pytest/issues/7469>`_: Directly constructing the following classes is now deprecated:
+
+ - ``_pytest.mark.structures.Mark``
+ - ``_pytest.mark.structures.MarkDecorator``
+ - ``_pytest.mark.structures.MarkGenerator``
+ - ``_pytest.python.Metafunc``
+ - ``_pytest.runner.CallInfo``
+ - ``_pytest._code.ExceptionInfo``
+ - ``_pytest.config.argparsing.Parser``
+ - ``_pytest.config.argparsing.OptionGroup``
+ - ``_pytest.pytester.HookRecorder``
+
+ These constructors have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 8.
+
+
+- `#8242 <https://github.com/pytest-dev/pytest/issues/8242>`_: Raising :class:`unittest.SkipTest` to skip collection of tests during the
+ pytest collection phase is deprecated. Use :func:`pytest.skip` instead.
+
+ Note: This deprecation only relates to using :class:`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.
+
+
+- `#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):
+
+ - ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead.
+ - ``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.
+ 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.
+
+ See :ref:`the deprecation note <cmdline-preparse-deprecated>` for full details.
+
+
+- `#8645 <https://github.com/pytest-dev/pytest/issues/8645>`_: :func:`pytest.warns(None) <pytest.warns>` is now deprecated because many people used
+ it to mean "this code does not emit warnings", but it actually had the effect of
+ checking that the code emits at least one warning of any type - like ``pytest.warns()``
+ or ``pytest.warns(Warning)``.
+
+
+- `#8948 <https://github.com/pytest-dev/pytest/issues/8948>`_: :func:`pytest.skip(msg=...) <pytest.skip>`, :func:`pytest.fail(msg=...) <pytest.fail>` and :func:`pytest.exit(msg=...) <pytest.exit>`
+ signatures now accept a ``reason`` argument instead of ``msg``. Using ``msg`` still works, but is deprecated and will be removed in a future release.
+
+ This was changed for consistency with :func:`pytest.mark.skip <pytest.mark.skip>` and :func:`pytest.mark.xfail <pytest.mark.xfail>` which both accept
+ ``reason`` as an argument.
+
+- `#8174 <https://github.com/pytest-dev/pytest/issues/8174>`_: The following changes have been made to types reachable through :attr:`pytest.ExceptionInfo.traceback`:
+
+ - The ``path`` property of ``_pytest.code.Code`` returns ``Path`` instead of ``py.path.local``.
+ - The ``path`` property of ``_pytest.code.TracebackEntry`` returns ``Path`` instead of ``py.path.local``.
+
+ There was no deprecation period for this change (sorry!).
+
+
+Features
+--------
+
+- `#5196 <https://github.com/pytest-dev/pytest/issues/5196>`_: Tests are now ordered by definition order in more cases.
+
+ In a class hierarchy, tests from base classes are now consistently ordered before tests defined on their subclasses (reverse MRO order).
+
+
+- `#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>`,
+ but returns a :class:`pathlib.Path` instead of a legacy ``py.path.local``.
+
+ Added a ``paths`` type to :meth:`parser.addini() <pytest.Parser.addini>`,
+ as in ``parser.addini("mypaths", "my paths", type="paths")``,
+ which is similar to the existing ``pathlist``,
+ but returns a list of :class:`pathlib.Path` instead of legacy ``py.path.local``.
+
+
+- `#7469 <https://github.com/pytest-dev/pytest/issues/7469>`_: The types of objects used in pytest's API are now exported so they may be used in type annotations.
+
+ The newly-exported types are:
+
+ - ``pytest.Config`` for :class:`Config <pytest.Config>`.
+ - ``pytest.Mark`` for :class:`marks <pytest.Mark>`.
+ - ``pytest.MarkDecorator`` for :class:`mark decorators <pytest.MarkDecorator>`.
+ - ``pytest.MarkGenerator`` for the :class:`pytest.mark <pytest.MarkGenerator>` singleton.
+ - ``pytest.Metafunc`` for the :class:`metafunc <pytest.MarkGenerator>` argument to the :hook:`pytest_generate_tests` hook.
+ - ``pytest.CallInfo`` for the :class:`CallInfo <pytest.CallInfo>` type passed to various hooks.
+ - ``pytest.PytestPluginManager`` for :class:`PytestPluginManager <pytest.PytestPluginManager>`.
+ - ``pytest.ExceptionInfo`` for the :class:`ExceptionInfo <pytest.ExceptionInfo>` type returned from :func:`pytest.raises` and passed to various hooks.
+ - ``pytest.Parser`` for the :class:`Parser <pytest.Parser>` type passed to the :hook:`pytest_addoption` hook.
+ - ``pytest.OptionGroup`` for the :class:`OptionGroup <pytest.OptionGroup>` type returned from the :func:`parser.addgroup <pytest.Parser.getgroup>` method.
+ - ``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.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.
+
+ Constructing most of them directly is not supported; they are only meant for use in type annotations.
+ Doing so will emit a deprecation warning, and may become a hard-error in pytest 8.0.
+
+ Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy.
+
+
+- `#7856 <https://github.com/pytest-dev/pytest/issues/7856>`_: :ref:`--import-mode=importlib <import-modes>` now works with features that
+ depend on modules being on :py:data:`sys.modules`, such as :mod:`pickle` and :mod:`dataclasses`.
+
+
+- `#8144 <https://github.com/pytest-dev/pytest/issues/8144>`_: The following hooks now receive an additional ``pathlib.Path`` argument, equivalent to an existing ``py.path.local`` argument:
+
+ - :hook:`pytest_ignore_collect` - The ``collection_path`` parameter (equivalent to existing ``path`` parameter).
+ - :hook:`pytest_collect_file` - The ``file_path`` parameter (equivalent to existing ``path`` parameter).
+ - :hook:`pytest_pycollect_makemodule` - The ``module_path`` parameter (equivalent to existing ``path`` parameter).
+ - :hook:`pytest_report_header` - The ``start_path`` parameter (equivalent to existing ``startdir`` parameter).
+ - :hook:`pytest_report_collectionfinish` - The ``start_path`` parameter (equivalent to existing ``startdir`` parameter).
+
+ .. note::
+ The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
+ new attribute being ``path``) is **the opposite** of the situation for hooks
+ (the old argument being ``path``).
+
+ This is an unfortunate artifact due to historical reasons, which should be
+ resolved in future versions as we slowly get rid of the :pypi:`py`
+ dependency (see :issue:`9283` for a longer discussion).
+
+
+- `#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.
+
+ .. note::
+ The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
+ new attribute being ``path``) is **the opposite** of the situation for hooks
+ (the old argument being ``path``).
+
+ This is an unfortunate artifact due to historical reasons, which should be
+ resolved in future versions as we slowly get rid of the :pypi:`py`
+ dependency (see :issue:`9283` for a longer discussion).
+
+
+- `#8421 <https://github.com/pytest-dev/pytest/issues/8421>`_: :func:`pytest.approx` now works on :class:`~decimal.Decimal` within mappings/dicts and sequences/lists.
+
+
+- `#8606 <https://github.com/pytest-dev/pytest/issues/8606>`_: pytest invocations with ``--fixtures-per-test`` and ``--fixtures`` have been enriched with:
+
+ - Fixture location path printed with the fixture name.
+ - First section of the fixture's docstring printed under the fixture name.
+ - Whole of fixture's docstring printed under the fixture name using ``--verbose`` option.
+
+
+- `#8761 <https://github.com/pytest-dev/pytest/issues/8761>`_: New :ref:`version-tuple` attribute, which makes it simpler for users to do something depending on the pytest version (such as declaring hooks which are introduced in later versions).
+
+
+- `#8789 <https://github.com/pytest-dev/pytest/issues/8789>`_: Switch TOML parser from ``toml`` to ``tomli`` for TOML v1.0.0 support in ``pyproject.toml``.
+
+
+- `#8920 <https://github.com/pytest-dev/pytest/issues/8920>`_: Added :class:`pytest.Stash`, a facility for plugins to store their data on :class:`~pytest.Config` and :class:`~_pytest.nodes.Node`\s in a type-safe and conflict-free manner.
+ 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
+ ``warnings`` argument to assert the total number of warnings captured.
+
+
+- `#8954 <https://github.com/pytest-dev/pytest/issues/8954>`_: ``--debug`` flag now accepts a :class:`str` file to route debug logs into, remains defaulted to `pytestdebug.log`.
+
+
+- `#9023 <https://github.com/pytest-dev/pytest/issues/9023>`_: Full diffs are now always shown for equality assertions of iterables when
+ `CI` or ``BUILD_NUMBER`` is found in the environment, even when ``-v`` isn't
+ 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
+ ``deselected`` argument to assert the total number of deselected tests.
+
+
+- `#9114 <https://github.com/pytest-dev/pytest/issues/9114>`_: Added :confval:`pythonpath` setting that adds listed paths to :data:`sys.path` for the duration of the test session. If you currently use the pytest-pythonpath or pytest-srcpaths plugins, you should be able to replace them with built-in `pythonpath` setting.
+
+
+
+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.
+
+ See :ref:`backwards-compatibility` for more details.
+
+
+- `#7864 <https://github.com/pytest-dev/pytest/issues/7864>`_: Improved error messages when parsing warning filters.
+
+ Previously pytest would show an internal traceback, which besides being ugly sometimes would hide the cause
+ of the problem (for example an ``ImportError`` while importing a specific warning type).
+
+
+- `#8335 <https://github.com/pytest-dev/pytest/issues/8335>`_: Improved :func:`pytest.approx` assertion messages for sequences of numbers.
+
+ The assertion messages now dumps a table with the index and the error of each diff.
+ Example::
+
+ > assert [1, 2, 3, 4] == pytest.approx([1, 3, 3, 5])
+ E assert comparison failed for 2 values:
+ E Index | Obtained | Expected
+ E 1 | 2 | 3 +- 3.0e-06
+ E 3 | 4 | 5 +- 5.0e-06
+
+
+- `#8403 <https://github.com/pytest-dev/pytest/issues/8403>`_: By default, pytest will truncate long strings in assert errors so they don't clutter the output too much,
+ currently at ``240`` characters by default.
+
+ However, in some cases the longer output helps, or is even crucial, to diagnose a failure. Using ``-v`` will
+ now increase the truncation threshold to ``2400`` characters, and ``-vv`` or higher will disable truncation entirely.
+
+
+- `#8509 <https://github.com/pytest-dev/pytest/issues/8509>`_: Fixed issue where :meth:`unittest.TestCase.setUpClass` is not called when a test has `/` in its name since pytest 6.2.0.
+
+ This refers to the path part in pytest node IDs, e.g. ``TestClass::test_it`` in the node ID ``tests/test_file.py::TestClass::test_it``.
+
+ Now, instead of assuming that the test name does not contain ``/``, it is assumed that test path does not contain ``::``. We plan to hopefully make both of these work in the future.
+
+
+- `#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::
+
+ logging_plugin = config.pluginmanager.get_plugin('logging-plugin')
+ logging_plugin.log_cli_handler.formatter.add_color_level(logging.INFO, 'cyan')
+ logging_plugin.log_cli_handler.formatter.add_color_level(logging.SPAM, 'blue')
+
+ See :ref:`log_colors` for more information.
+
+
+- `#8822 <https://github.com/pytest-dev/pytest/issues/8822>`_: When showing fixture paths in `--fixtures` or `--fixtures-by-test`, fixtures coming from pytest itself now display an elided path, rather than the full path to the file in the `site-packages` directory.
+
+
+- `#8898 <https://github.com/pytest-dev/pytest/issues/8898>`_: Complex numbers are now treated like floats and integers when generating parameterization IDs.
+
+
+- `#9062 <https://github.com/pytest-dev/pytest/issues/9062>`_: ``--stepwise-skip`` now implicitly enables ``--stepwise`` and can be used on its own.
+
+
+- `#9205 <https://github.com/pytest-dev/pytest/issues/9205>`_: :meth:`pytest.Cache.set` now preserves key order when saving dicts.
+
+
+
+Bug Fixes
+---------
+
+- `#7124 <https://github.com/pytest-dev/pytest/issues/7124>`_: Fixed an issue where ``__main__.py`` would raise an ``ImportError`` when ``--doctest-modules`` was provided.
+
+
+- `#8061 <https://github.com/pytest-dev/pytest/issues/8061>`_: Fixed failing ``staticmethod`` test cases if they are inherited from a parent test class.
+
+
+- `#8192 <https://github.com/pytest-dev/pytest/issues/8192>`_: ``testdir.makefile`` now silently accepts values which don't start with ``.`` to maintain backward compatibility with older pytest versions.
+
+ ``pytester.makefile`` now issues a clearer error if the ``.`` is missing in the ``ext`` argument.
+
+
+- `#8258 <https://github.com/pytest-dev/pytest/issues/8258>`_: Fixed issue where pytest's ``faulthandler`` support would not dump traceback on crashes
+ if the :mod:`faulthandler` module was already enabled during pytest startup (using
+ ``python -X dev -m pytest`` for example).
+
+
+- `#8317 <https://github.com/pytest-dev/pytest/issues/8317>`_: Fixed an issue where illegal directory characters derived from ``getpass.getuser()`` raised an ``OSError``.
+
+
+- `#8367 <https://github.com/pytest-dev/pytest/issues/8367>`_: Fix ``Class.from_parent`` so it forwards extra keyword arguments to the constructor.
+
+
+- `#8377 <https://github.com/pytest-dev/pytest/issues/8377>`_: The test selection options ``pytest -k`` and ``pytest -m`` now support matching
+ names containing forward slash (``/``) characters.
+
+
+- `#8384 <https://github.com/pytest-dev/pytest/issues/8384>`_: The ``@pytest.mark.skip`` decorator now correctly handles its arguments. When the ``reason`` argument is accidentally given both positional and as a keyword (e.g. because it was confused with ``skipif``), a ``TypeError`` now occurs. Before, such tests were silently skipped, and the positional argument ignored. Additionally, ``reason`` is now documented correctly as positional or keyword (rather than keyword-only).
+
+
+- `#8394 <https://github.com/pytest-dev/pytest/issues/8394>`_: Use private names for internal fixtures that handle classic setup/teardown so that they don't show up with the default ``--fixtures`` invocation (but they still show up with ``--fixtures -v``).
+
+
+- `#8456 <https://github.com/pytest-dev/pytest/issues/8456>`_: The :confval:`required_plugins` config option now works correctly when pre-releases of plugins are installed, rather than falsely claiming that those plugins aren't installed at all.
+
+
+- `#8464 <https://github.com/pytest-dev/pytest/issues/8464>`_: ``-c <config file>`` now also properly defines ``rootdir`` as the directory that contains ``<config file>``.
+
+
+- `#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
+ ``pkg_resources`` was previously imported, because it is not needed otherwise.
+
+
+- `#8548 <https://github.com/pytest-dev/pytest/issues/8548>`_: Introduce fix to handle precision width in ``log-cli-format`` in turn to fix output coloring for certain formats.
+
+
+- `#8796 <https://github.com/pytest-dev/pytest/issues/8796>`_: Fixed internal error when skipping doctests.
+
+
+- `#8983 <https://github.com/pytest-dev/pytest/issues/8983>`_: The test selection options ``pytest -k`` and ``pytest -m`` now support matching names containing backslash (`\\`) characters.
+ Backslashes are treated literally, not as escape characters (the values being matched against are already escaped).
+
+
+- `#8990 <https://github.com/pytest-dev/pytest/issues/8990>`_: Fix `pytest -vv` crashing with an internal exception `AttributeError: 'str' object has no attribute 'relative_to'` in some cases.
+
+
+- `#9077 <https://github.com/pytest-dev/pytest/issues/9077>`_: Fixed confusing error message when ``request.fspath`` / ``request.path`` was accessed from a session-scoped fixture.
+
+
+- `#9131 <https://github.com/pytest-dev/pytest/issues/9131>`_: Fixed the URL used by ``--pastebin`` to use `bpa.st <http://bpa.st>`__.
+
+
+- `#9163 <https://github.com/pytest-dev/pytest/issues/9163>`_: The end line number and end column offset are now properly set for rewritten assert statements.
+
+
+- `#9169 <https://github.com/pytest-dev/pytest/issues/9169>`_: Support for the ``files`` API from ``importlib.resources`` within rewritten files.
+
+
+- `#9272 <https://github.com/pytest-dev/pytest/issues/9272>`_: The nose compatibility module-level fixtures `setup()` and `teardown()` are now only called once per module, instead of for each test function.
+ They are now called even if object-level `setup`/`teardown` is defined.
+
+
+
+Improved Documentation
+----------------------
+
+- `#4320 <https://github.com/pytest-dev/pytest/issues/4320>`_: Improved docs for `pytester.copy_example`.
+
+
+- `#5105 <https://github.com/pytest-dev/pytest/issues/5105>`_: Add automatically generated :ref:`plugin-list`. The list is updated on a periodic schedule.
+
+
+- `#8337 <https://github.com/pytest-dev/pytest/issues/8337>`_: Recommend `numpy.testing <https://numpy.org/doc/stable/reference/routines.testing.html>`__ module on :func:`pytest.approx` documentation.
+
+
+- `#8655 <https://github.com/pytest-dev/pytest/issues/8655>`_: Help text for ``--pdbcls`` more accurately reflects the option's behavior.
+
+
+- `#9210 <https://github.com/pytest-dev/pytest/issues/9210>`_: Remove incorrect docs about ``confcutdir`` being a configuration option: it can only be set through the ``--confcutdir`` command-line option.
+
+
+- `#9242 <https://github.com/pytest-dev/pytest/issues/9242>`_: Upgrade readthedocs configuration to use a `newer Ubuntu version <https://blog.readthedocs.com/new-build-specification/>`__` with better unicode support for PDF docs.
+
+
+- `#9341 <https://github.com/pytest-dev/pytest/issues/9341>`_: Various methods commonly used for :ref:`non-python tests` are now correctly documented in the reference docs. They were undocumented previously.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- `#8133 <https://github.com/pytest-dev/pytest/issues/8133>`_: Migrate to ``setuptools_scm`` 6.x to use ``SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST`` for more robust release tooling.
+
+
+- `#8174 <https://github.com/pytest-dev/pytest/issues/8174>`_: The following changes have been made to internal pytest types/functions:
+
+ - The ``_pytest.code.getfslineno()`` function returns ``Path`` instead of ``py.path.local``.
+ - The ``_pytest.python.path_matches_patterns()`` function takes ``Path`` instead of ``py.path.local``.
+ - The ``_pytest._code.Traceback.cut()`` function accepts any ``os.PathLike[str]``, not just ``py.path.local``.
+
+
+- `#8248 <https://github.com/pytest-dev/pytest/issues/8248>`_: Internal Restructure: let ``python.PyObjMixin`` inherit from ``nodes.Node`` to carry over typing information.
+
+
+- `#8432 <https://github.com/pytest-dev/pytest/issues/8432>`_: Improve error message when :func:`pytest.skip` is used at module level without passing `allow_module_level=True`.
+
+
+- `#8818 <https://github.com/pytest-dev/pytest/issues/8818>`_: Ensure ``regendoc`` opts out of ``TOX_ENV`` cachedir selection to ensure independent example test runs.
+
+
+- `#8913 <https://github.com/pytest-dev/pytest/issues/8913>`_: The private ``CallSpec2._arg2scopenum`` attribute has been removed after an internal refactoring.
+
+
+- `#8967 <https://github.com/pytest-dev/pytest/issues/8967>`_: :hook:`pytest_assertion_pass` is no longer considered experimental and
+ future changes to it will be considered more carefully.
+
+
+- `#9202 <https://github.com/pytest-dev/pytest/issues/9202>`_: Add github action to upload coverage report to codecov instead of bash uploader.
+
+
+- `#9225 <https://github.com/pytest-dev/pytest/issues/9225>`_: Changed the command used to create sdist and wheel artifacts: using the build package instead of setup.py.
+
+
+- `#9351 <https://github.com/pytest-dev/pytest/issues/9351>`_: Correct minor typos in doc/en/example/special.rst.
+
+
+pytest 6.2.5 (2021-08-29)
+=========================
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`8494`: Python 3.10 is now supported.
+
+
+- :issue:`9040`: Enable compatibility with ``pluggy 1.0`` or later.
+
+
+pytest 6.2.4 (2021-05-04)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`8539`: Fixed assertion rewriting on Python 3.10.
+
+
+pytest 6.2.3 (2021-04-03)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`8414`: pytest used to create directories under ``/tmp`` with world-readable
+ permissions. This means that any user in the system was able to read
+ information written by tests in temporary directories (such as those created by
+ 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,
+ 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.
+
+
+pytest 6.2.2 (2021-01-25)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`8152`: Fixed "(<Skipped instance>)" being shown as a skip reason in the verbose test summary line when the reason is empty.
+
+
+- :issue:`8249`: Fix the ``faulthandler`` plugin for occasions when running with ``twisted.logger`` and using ``pytest --capture=no``.
+
+
+pytest 6.2.1 (2020-12-15)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`7678`: Fixed bug where ``ImportPathMismatchError`` would be raised for files compiled in
+ the host and loaded later from an UNC mounted path (Windows).
+
+
+- :issue:`8132`: Fixed regression in ``approx``: in 6.2.0 ``approx`` no longer raises
+ ``TypeError`` when dealing with non-numeric types, falling back to normal comparison.
+ Before 6.2.0, array types like tf.DeviceArray fell through to the scalar case,
+ and happened to compare correctly to a scalar if they had only one element.
+ After 6.2.0, these types began failing, because they inherited neither from
+ standard Python number hierarchy nor from ``numpy.ndarray``.
+
+ ``approx`` now converts arguments to ``numpy.ndarray`` if they expose the array
+ protocol and are not scalars. This treats array-like objects like numpy arrays,
+ regardless of size.
+
+
+pytest 6.2.0 (2020-12-12)
+=========================
+
+Breaking Changes
+----------------
+
+- :issue:`7808`: pytest now supports python3.6+ only.
+
+
+
+Deprecations
+------------
+
+- :issue:`7469`: Directly constructing/calling the following classes/functions is now deprecated:
+
+ - ``_pytest.cacheprovider.Cache``
+ - ``_pytest.cacheprovider.Cache.for_config()``
+ - ``_pytest.cacheprovider.Cache.clear_cache()``
+ - ``_pytest.cacheprovider.Cache.cache_dir_from_config()``
+ - ``_pytest.capture.CaptureFixture``
+ - ``_pytest.fixtures.FixtureRequest``
+ - ``_pytest.fixtures.SubRequest``
+ - ``_pytest.logging.LogCaptureFixture``
+ - ``_pytest.pytester.Pytester``
+ - ``_pytest.pytester.Testdir``
+ - ``_pytest.recwarn.WarningsRecorder``
+ - ``_pytest.recwarn.WarningsChecker``
+ - ``_pytest.tmpdir.TempPathFactory``
+ - ``_pytest.tmpdir.TempdirFactory``
+
+ These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 8.0.0.
+
+
+- :issue:`7530`: The ``--strict`` command-line option has been deprecated, use ``--strict-markers`` instead.
+
+ We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing flag for all strictness
+ related options (``--strict-markers`` and ``--strict-config`` at the moment, more might be introduced in the future).
+
+
+- :issue:`7988`: The ``@pytest.yield_fixture`` decorator/function is now deprecated. Use :func:`pytest.fixture` instead.
+
+ ``yield_fixture`` has been an alias for ``fixture`` for a very long time, so can be search/replaced safely.
+
+
+
+Features
+--------
+
+- :issue:`5299`: pytest now warns about unraisable exceptions and unhandled thread exceptions that occur in tests on Python>=3.8.
+ See :ref:`unraisable` for more information.
+
+
+- :issue:`7425`: New :fixture:`pytester` fixture, which is identical to :fixture:`testdir` but its methods return :class:`pathlib.Path` when appropriate instead of ``py.path.local``.
+
+ 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.
+
+
+- :issue:`7695`: A new hook was added, `pytest_markeval_namespace` which should return a dictionary.
+ This dictionary will be used to augment the "global" variables available to evaluate skipif/xfail/xpass markers.
+
+ Pseudo example
+
+ ``conftest.py``:
+
+ .. code-block:: python
+
+ def pytest_markeval_namespace():
+ return {"color": "red"}
+
+ ``test_func.py``:
+
+ .. code-block:: python
+
+ @pytest.mark.skipif("color == 'blue'", reason="Color is not red")
+ def test_func():
+ assert False
+
+
+- :issue:`8006`: It is now possible to construct a :class:`~pytest.MonkeyPatch` object directly as ``pytest.MonkeyPatch()``,
+ in cases when the :fixture:`monkeypatch` fixture cannot be used. Previously some users imported it
+ from the private `_pytest.monkeypatch.MonkeyPatch` namespace.
+
+ Additionally, :meth:`MonkeyPatch.context <pytest.MonkeyPatch.context>` is now a classmethod,
+ and can be used as ``with MonkeyPatch.context() as mp: ...``. This is the recommended way to use
+ ``MonkeyPatch`` directly, since unlike the ``monkeypatch`` fixture, an instance created directly
+ is not ``undo()``-ed automatically.
+
+
+
+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:`2044`: Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS".
+
+
+- :issue:`7469` The types of builtin pytest fixtures are now exported so they may be used in type annotations of test functions.
+ The newly-exported types are:
+
+ - ``pytest.FixtureRequest`` for the :fixture:`request` fixture.
+ - ``pytest.Cache`` for the :fixture:`cache` fixture.
+ - ``pytest.CaptureFixture[str]`` for the :fixture:`capfd` and :fixture:`capsys` fixtures.
+ - ``pytest.CaptureFixture[bytes]`` for the :fixture:`capfdbinary` and :fixture:`capsysbinary` fixtures.
+ - ``pytest.LogCaptureFixture`` for the :fixture:`caplog` fixture.
+ - ``pytest.Pytester`` for the :fixture:`pytester` fixture.
+ - ``pytest.Testdir`` for the :fixture:`testdir` fixture.
+ - ``pytest.TempdirFactory`` for the :fixture:`tmpdir_factory` fixture.
+ - ``pytest.TempPathFactory`` for the :fixture:`tmp_path_factory` fixture.
+ - ``pytest.MonkeyPatch`` for the :fixture:`monkeypatch` fixture.
+ - ``pytest.WarningsRecorder`` for the :fixture:`recwarn` fixture.
+
+ Constructing them is not supported (except for `MonkeyPatch`); they are only meant for use in type annotations.
+ Doing so will emit a deprecation warning, and may become a hard-error in pytest 8.0.
+
+ Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy.
+
+
+- :issue:`7527`: When a comparison between :func:`namedtuple <collections.namedtuple>` instances of the same type fails, pytest now shows the differing field names (possibly nested) instead of their indexes.
+
+
+- :issue:`7615`: :meth:`Node.warn <_pytest.nodes.Node.warn>` now permits any subclass of :class:`Warning`, not just :class:`PytestWarning <pytest.PytestWarning>`.
+
+
+- :issue:`7701`: Improved reporting when using ``--collected-only``. It will now show the number of collected tests in the summary stats.
+
+
+- :issue:`7710`: Use strict equality comparison for non-numeric types in :func:`pytest.approx` instead of
+ raising :class:`TypeError`.
+
+ This was the undocumented behavior before 3.7, but is now officially a supported feature.
+
+
+- :issue:`7938`: New ``--sw-skip`` argument which is a shorthand for ``--stepwise-skip``.
+
+
+- :issue:`8023`: Added ``'node_modules'`` to default value for :confval:`norecursedirs`.
+
+
+- :issue:`8032`: :meth:`doClassCleanups <unittest.TestCase.doClassCleanups>` (introduced in :mod:`unittest` in Python and 3.8) is now called appropriately.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`4824`: Fixed quadratic behavior and improved performance of collection of items using autouse fixtures and xunit fixtures.
+
+
+- :issue:`7758`: Fixed an issue where some files in packages are getting lost from ``--lf`` even though they contain tests that failed. Regressed in pytest 5.4.0.
+
+
+- :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:`7951`: Fixed handling of recursive symlinks when collecting tests.
+
+
+- :issue:`7981`: Fixed symlinked directories not being followed during collection. Regressed in pytest 6.1.0.
+
+
+- :issue:`8016`: Fixed only one doctest being collected when using ``pytest --doctest-modules path/to/an/__init__.py``.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`7429`: Add more information and use cases about skipping doctests.
+
+
+- :issue:`7780`: Classes which should not be inherited from are now marked ``final class`` in the API reference.
+
+
+- :issue:`7872`: ``_pytest.config.argparsing.Parser.addini()`` accepts explicit ``None`` and ``"string"``.
+
+
+- :issue:`7878`: In pull request section, ask to commit after editing changelog and authors file.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`7802`: The ``attrs`` dependency requirement is now >=19.2.0 instead of >=17.4.0.
+
+
+- :issue:`8014`: `.pyc` files created by pytest's assertion rewriting now conform to the newer :pep:`552` format on Python>=3.7.
+ (These files are internal and only interpreted by pytest itself.)
+
+
+pytest 6.1.2 (2020-10-28)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`7758`: Fixed an issue where some files in packages are getting lost from ``--lf`` even though they contain tests that failed. Regressed in pytest 5.4.0.
+
+
+- :issue:`7911`: Directories created by `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.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`7815`: Improve deprecation warning message for ``pytest._fillfuncargs()``.
+
+
+pytest 6.1.1 (2020-10-03)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`7807`: Fixed regression in pytest 6.1.0 causing incorrect rootdir to be determined in some non-trivial cases where parent directories have config files as well.
+
+
+- :issue:`7814`: Fixed crash in header reporting when :confval:`testpaths` is used and contains absolute paths (regression in 6.1.0).
+
+
+pytest 6.1.0 (2020-09-26)
+=========================
+
+Breaking Changes
+----------------
+
+- :issue:`5585`: As per our policy, the following features which have been deprecated in the 5.X series are now
+ removed:
+
+ * The ``funcargnames`` read-only property of ``FixtureRequest``, ``Metafunc``, and ``Function`` classes. Use ``fixturenames`` attribute.
+
+ * ``@pytest.fixture`` no longer supports positional arguments, pass all arguments by keyword instead.
+
+ * Direct construction of ``Node`` subclasses now raise an error, use ``from_parent`` instead.
+
+ * The default value for ``junit_family`` has changed to ``xunit2``. If you require the old format, add ``junit_family=xunit1`` to your configuration file.
+
+ * The ``TerminalReporter`` no longer has a ``writer`` attribute. Plugin authors may use the public functions of the ``TerminalReporter`` instead of accessing the ``TerminalWriter`` object directly.
+
+ * The ``--result-log`` option has been removed. Users are recommended to use the `pytest-reportlog <https://github.com/pytest-dev/pytest-reportlog>`__ plugin instead.
+
+
+ For more information consult :std:doc:`deprecations` in the docs.
+
+
+
+Deprecations
+------------
+
+- :issue:`6981`: The ``pytest.collect`` module is deprecated: all its names can be imported from ``pytest`` directly.
+
+
+- :issue:`7097`: The ``pytest._fillfuncargs`` function is deprecated. This function was kept
+ for backward compatibility with an older plugin.
+
+ It's functionality is not meant to be used directly, but if you must replace
+ it, use `function._request._fillfixtures()` instead, though note this is not
+ a public API and may break in the future.
+
+
+- :issue:`7210`: The special ``-k '-expr'`` syntax to ``-k`` is deprecated. Use ``-k 'not expr'``
+ instead.
+
+ The special ``-k 'expr:'`` syntax to ``-k`` is deprecated. Please open an issue
+ if you use this and want a replacement.
+
+
+- :issue:`7255`: The :hook:`pytest_warning_captured` hook is deprecated in favor
+ of :hook:`pytest_warning_recorded`, and will be removed in a future version.
+
+
+- :issue:`7648`: The ``gethookproxy()`` and ``isinitpath()`` methods of ``FSCollector`` and ``Package`` are deprecated;
+ use ``self.session.gethookproxy()`` and ``self.session.isinitpath()`` instead.
+ This should work on all pytest versions.
+
+
+
+Features
+--------
+
+- :issue:`7667`: New ``--durations-min`` command-line flag controls the minimal duration for inclusion in the slowest list of tests shown by ``--durations``. Previously this was hard-coded to ``0.005s``.
+
+
+
+Improvements
+------------
+
+- :issue:`6681`: Internal pytest warnings issued during the early stages of initialization are now properly handled and can filtered through :confval:`filterwarnings` or ``--pythonwarnings/-W``.
+
+ This also fixes a number of long standing issues: :issue:`2891`, :issue:`7620`, :issue:`7426`.
+
+
+- :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,
+ and should be preferred over them when possible.
+
+
+- :issue:`7780`: Public classes which are not designed to be inherited from are now marked :func:`@final <typing.final>`.
+ Code which inherits from these classes will trigger a type-checking (e.g. mypy) error, but will still work in runtime.
+ Currently the ``final`` designation does not appear in the API Reference but hopefully will in the future.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`1953`: Fixed error when overwriting a parametrized fixture, while also reusing the super fixture value.
+
+ .. code-block:: python
+
+ # conftest.py
+ import pytest
+
+
+ @pytest.fixture(params=[1, 2])
+ def foo(request):
+ return request.param
+
+
+ # test_foo.py
+ import pytest
+
+
+ @pytest.fixture
+ def foo(foo):
+ return foo * 2
+
+
+- :issue:`4984`: Fixed an internal error crash with ``IndexError: list index out of range`` when
+ collecting a module which starts with a decorated function, the decorator
+ raises, and assertion rewriting is enabled.
+
+
+- :issue:`7591`: pylint shouldn't complain anymore about unimplemented abstract methods when inheriting from :ref:`File <non-python tests>`.
+
+
+- :issue:`7628`: Fixed test collection when a full path without a drive letter was passed to pytest on Windows (for example ``\projects\tests\test.py`` instead of ``c:\projects\tests\pytest.py``).
+
+
+- :issue:`7638`: Fix handling of command-line options that appear as paths but trigger an OS-level syntax error on Windows, such as the options used internally by ``pytest-xdist``.
+
+
+- :issue:`7742`: Fixed INTERNALERROR when accessing locals / globals with faulty ``exec``.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`1477`: Removed faq.rst and its reference in contents.rst.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`7536`: The internal ``junitxml`` plugin has rewritten to use ``xml.etree.ElementTree``.
+ The order of attributes in XML elements might differ. Some unneeded escaping is
+ no longer performed.
+
+
+- :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,
+ but should behave like one in all respects. This was done for technical reasons.
+
+
+- :issue:`7671`: When collecting tests, pytest finds test classes and functions by examining the
+ attributes of python objects (modules, classes and instances). To speed up this
+ process, pytest now ignores builtin attributes (like ``__class__``,
+ ``__delattr__`` and ``__new__``) without consulting the :confval:`python_classes` and
+ :confval:`python_functions` configuration options and without passing them to plugins
+ using the :hook:`pytest_pycollect_makeitem` hook.
+
+
+pytest 6.0.2 (2020-09-04)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`7148`: Fixed ``--log-cli`` potentially causing unrelated ``print`` output to be swallowed.
+
+
+- :issue:`7672`: Fixed log-capturing level restored incorrectly if ``caplog.set_level`` is called more than once.
+
+
+- :issue:`7686`: Fixed `NotSetType.token` being used as the parameter ID when the parametrization list is empty.
+ Regressed in pytest 6.0.0.
+
+
+- :issue:`7707`: Fix internal error when handling some exceptions that contain multiple lines or the style uses multiple lines (``--tb=line`` for example).
+
+
+pytest 6.0.1 (2020-07-30)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`7394`: Passing an empty ``help`` value to ``Parser.add_option`` is now accepted instead of crashing when running ``pytest --help``.
+ Passing ``None`` raises a more informative ``TypeError``.
+
+
+- :issue:`7558`: Fix pylint ``not-callable`` lint on ``pytest.mark.parametrize()`` and the other builtin marks:
+ ``skip``, ``skipif``, ``xfail``, ``usefixtures``, ``filterwarnings``.
+
+
+- :issue:`7559`: Fix regression in plugins using ``TestReport.longreprtext`` (such as ``pytest-html``) when ``TestReport.longrepr`` is not a string.
+
+
+- :issue:`7569`: Fix logging capture handler's level not reset on teardown after a call to ``caplog.set_level()``.
+
+
+pytest 6.0.0 (2020-07-28)
+=========================
+
+(**Please see the full set of changes for this release also in the 6.0.0rc1 notes below**)
+
+Breaking Changes
+----------------
+
+- :issue:`5584`: **PytestDeprecationWarning are now errors by default.**
+
+ Following our plan to remove deprecated features with as little disruption as
+ possible, all warnings of type ``PytestDeprecationWarning`` now generate errors
+ instead of warning messages.
+
+ **The affected features will be effectively removed in pytest 6.1**, so please consult the
+ :std:doc:`deprecations` section in the docs for directions on how to update existing code.
+
+ In the pytest ``6.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.PytestDeprecationWarning
+
+ But this will stop working when pytest ``6.1`` is released.
+
+ **If you have concerns** about the removal of a specific feature, please add a
+ comment to :issue:`5584`.
+
+
+- :issue:`7472`: The ``exec_()`` and ``is_true()`` methods of ``_pytest._code.Frame`` have been removed.
+
+
+
+Features
+--------
+
+- :issue:`7464`: Added support for :envvar:`NO_COLOR` and :envvar:`FORCE_COLOR` environment variables to control colored output.
+
+
+
+Improvements
+------------
+
+- :issue:`7467`: ``--log-file`` CLI option and ``log_file`` ini marker now create subdirectories if needed.
+
+
+- :issue:`7489`: The :func:`pytest.raises` function has a clearer error message when ``match`` equals the obtained string but is not a regex match. In this case it is suggested to escape the regex.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`7392`: Fix the reported location of tests skipped with ``@pytest.mark.skip`` when ``--runxfail`` is used.
+
+
+- :issue:`7491`: :fixture:`tmpdir` and :fixture:`tmp_path` no longer raise an error if the lock to check for
+ stale temporary directories is not accessible.
+
+
+- :issue:`7517`: Preserve line endings when captured via ``capfd``.
+
+
+- :issue:`7534`: Restored the previous formatting of ``TracebackEntry.__str__`` which was changed by accident.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`7422`: Clarified when the ``usefixtures`` mark can apply fixtures to test.
+
+
+- :issue:`7441`: Add a note about ``-q`` option used in getting started guide.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`7389`: Fixture scope ``package`` is no longer considered experimental.
+
+
+pytest 6.0.0rc1 (2020-07-08)
+============================
+
+Breaking Changes
+----------------
+
+- :issue:`1316`: ``TestReport.longrepr`` is now always an instance of ``ReprExceptionInfo``. Previously it was a ``str`` when a test failed with ``pytest.fail(..., pytrace=False)``.
+
+
+- :issue:`5965`: symlinks are no longer resolved during collection and matching `conftest.py` files with test file paths.
+
+ Resolving symlinks for the current directory and during collection was introduced as a bugfix in 3.9.0, but it actually is a new feature which had unfortunate consequences in Windows and surprising results in other platforms.
+
+ The team decided to step back on resolving symlinks at all, planning to review this in the future with a more solid solution (see discussion in
+ :pull:`6523` for details).
+
+ This might break test suites which made use of this feature; the fix is to create a symlink
+ for the entire test tree, and not only to partial files/tress as it was possible previously.
+
+
+- :issue:`6505`: ``Testdir.run().parseoutcomes()`` now always returns the parsed nouns in plural form.
+
+ Originally ``parseoutcomes()`` would always returns the nouns in plural form, but a change
+ meant to improve the terminal summary by using singular form single items (``1 warning`` or ``1 error``)
+ caused an unintended regression by changing the keys returned by ``parseoutcomes()``.
+
+ Now the API guarantees to always return the plural form, so calls like this:
+
+ .. code-block:: python
+
+ result = testdir.runpytest()
+ result.assert_outcomes(error=1)
+
+ Need to be changed to:
+
+
+ .. code-block:: python
+
+ result = testdir.runpytest()
+ result.assert_outcomes(errors=1)
+
+
+- :issue:`6903`: The ``os.dup()`` function is now assumed to exist. We are not aware of any
+ supported Python 3 implementations which do not provide it.
+
+
+- :issue:`7040`: ``-k`` no longer matches against the names of the directories outside the test session root.
+
+ Also, ``pytest.Package.name`` is now just the name of the directory containing the package's
+ ``__init__.py`` file, instead of the full path. This is consistent with how the other nodes
+ are named, and also one of the reasons why ``-k`` would match against any directory containing
+ the test suite.
+
+
+- :issue:`7122`: Expressions given to the ``-m`` and ``-k`` options are no longer evaluated using Python's :func:`eval`.
+ The format supports ``or``, ``and``, ``not``, parenthesis and general identifiers to match against.
+ Python constants, keywords or other operators are no longer evaluated differently.
+
+
+- :issue:`7135`: Pytest now uses its own ``TerminalWriter`` class instead of using the one from the ``py`` library.
+ Plugins generally access this class through ``TerminalReporter.writer``, ``TerminalReporter.write()``
+ (and similar methods), or ``_pytest.config.create_terminal_writer()``.
+
+ The following breaking changes were made:
+
+ - Output (``write()`` method and others) no longer flush implicitly; the flushing behavior
+ of the underlying file is respected. To flush explicitly (for example, if you
+ want output to be shown before an end-of-line is printed), use ``write(flush=True)`` or
+ ``terminal_writer.flush()``.
+ - Explicit Windows console support was removed, delegated to the colorama library.
+ - Support for writing ``bytes`` was removed.
+ - The ``reline`` method and ``chars_on_current_line`` property were removed.
+ - The ``stringio`` and ``encoding`` arguments was removed.
+ - Support for passing a callable instead of a file was removed.
+
+
+- :issue:`7224`: The `item.catch_log_handler` and `item.catch_log_handlers` attributes, set by the
+ logging plugin and never meant to be public, are no longer available.
+
+ The deprecated ``--no-print-logs`` option and ``log_print`` ini option are removed. Use ``--show-capture`` instead.
+
+
+- :issue:`7226`: Removed the unused ``args`` parameter from ``pytest.Function.__init__``.
+
+
+- :issue:`7418`: Removed the `pytest_doctest_prepare_content` hook specification. This hook
+ hasn't been triggered by pytest for at least 10 years.
+
+
+- :issue:`7438`: Some changes were made to the internal ``_pytest._code.source``, listed here
+ for the benefit of plugin authors who may be using it:
+
+ - The ``deindent`` argument to ``Source()`` has been removed, now it is always true.
+ - Support for zero or multiple arguments to ``Source()`` has been removed.
+ - Support for comparing ``Source`` with an ``str`` has been removed.
+ - The methods ``Source.isparseable()`` and ``Source.putaround()`` have been removed.
+ - The method ``Source.compile()`` and function ``_pytest._code.compile()`` have
+ been removed; use plain ``compile()`` instead.
+ - The function ``_pytest._code.source.getsource()`` has been removed; use
+ ``Source()`` directly instead.
+
+
+
+Deprecations
+------------
+
+- :issue:`7210`: The special ``-k '-expr'`` syntax to ``-k`` is deprecated. Use ``-k 'not expr'``
+ instead.
+
+ The special ``-k 'expr:'`` syntax to ``-k`` is deprecated. Please open an issue
+ if you use this and want a replacement.
+
+- :issue:`4049`: ``pytest_warning_captured`` is deprecated in favor of the ``pytest_warning_recorded`` hook.
+
+
+Features
+--------
+
+- :issue:`1556`: pytest now supports ``pyproject.toml`` files for configuration.
+
+ The configuration options is similar to the one available in other formats, but must be defined
+ in a ``[tool.pytest.ini_options]`` table to be picked up by pytest:
+
+ .. code-block:: toml
+
+ # pyproject.toml
+ [tool.pytest.ini_options]
+ minversion = "6.0"
+ addopts = "-ra -q"
+ testpaths = [
+ "tests",
+ "integration",
+ ]
+
+ More information can be found :ref:`in the docs <config file formats>`.
+
+
+- :issue:`3342`: pytest now includes inline type annotations and exposes them to user programs.
+ Most of the user-facing API is covered, as well as internal code.
+
+ If you are running a type checker such as mypy on your tests, you may start
+ noticing type errors indicating incorrect usage. If you run into an error that
+ you believe to be incorrect, please let us know in an issue.
+
+ The types were developed against mypy version 0.780. Versions before 0.750
+ are known not to work. We recommend using the latest version. Other type
+ checkers may work as well, but they are not officially verified to work by
+ pytest yet.
+
+
+- :issue:`4049`: Introduced a new hook named `pytest_warning_recorded` to convey information about warnings captured by the internal `pytest` warnings plugin.
+
+ This hook is meant to replace `pytest_warning_captured`, which is deprecated and will be removed in a future release.
+
+
+- :issue:`6471`: New command-line flags:
+
+ * `--no-header`: disables the initial header, including platform, version, and plugins.
+ * `--no-summary`: disables the final test summary, including warnings.
+
+
+- :issue:`6856`: A warning is now shown when an unknown key is read from a config INI file.
+
+ The `--strict-config` flag has been added to treat these warnings as errors.
+
+
+- :issue:`6906`: Added `--code-highlight` command line option to enable/disable code highlighting in terminal output.
+
+
+- :issue:`7245`: New ``--import-mode=importlib`` option that uses :mod:`importlib` to import test modules.
+
+ Traditionally pytest used ``__import__`` while changing ``sys.path`` to import test modules (which
+ 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
+ require pytest to change ``sys.path`` or ``sys.modules`` at all, eliminating much of the drawbacks
+ of the previous mode.
+
+ We intend to make ``--import-mode=importlib`` the default in future versions, so users are encouraged
+ to try the new mode and provide feedback (both positive or negative) in issue :issue:`7245`.
+
+ You can read more about this option in :std:ref:`the documentation <import-modes>`.
+
+
+- :issue:`7305`: New ``required_plugins`` configuration option allows the user to specify a list of plugins, including version information, that are required for pytest to run. An error is raised if any required plugins are not found when running pytest.
+
+
+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
+ closed by the piped-to program (common examples are ``less`` and ``head``).
+
+
+- :issue:`4391`: Improved precision of test durations measurement. ``CallInfo`` items now have a new ``<CallInfo>.duration`` attribute, created using ``time.perf_counter()``. This attribute is used to fill the ``<TestReport>.duration`` attribute, which is more accurate than the previous ``<CallInfo>.stop - <CallInfo>.start`` (as these are based on ``time.time()``).
+
+
+- :issue:`4675`: Rich comparison for dataclasses and `attrs`-classes is now recursive.
+
+
+- :issue:`6285`: Exposed the `pytest.FixtureLookupError` exception which is raised by `request.getfixturevalue()`
+ (where `request` is a `FixtureRequest` fixture) when a fixture with the given name cannot be returned.
+
+
+- :issue:`6433`: If an error is encountered while formatting the message in a logging call, for
+ example ``logging.warning("oh no!: %s: %s", "first")`` (a second argument is
+ missing), pytest now propagates the error, likely causing the test to fail.
+
+ Previously, such a mistake would cause an error to be printed to stderr, which
+ is not displayed by default for passing tests. This change makes the mistake
+ visible during testing.
+
+ You may suppress this behavior temporarily or permanently by setting
+ ``logging.raiseExceptions = False``.
+
+
+- :issue:`6817`: Explicit new-lines in help texts of command-line options are preserved, allowing plugins better control
+ of the help displayed to users.
+
+
+- :issue:`6940`: When using the ``--duration`` option, the terminal message output is now more precise about the number and duration of hidden items.
+
+
+- :issue:`6991`: Collected files are displayed after any reports from hooks, e.g. the status from ``--lf``.
+
+
+- :issue:`7091`: When ``fd`` capturing is used, through ``--capture=fd`` or the ``capfd`` and
+ ``capfdbinary`` fixtures, and the file descriptor (0, 1, 2) cannot be
+ duplicated, FD capturing is still performed. Previously, direct writes to the
+ file descriptors would fail or be lost in this case.
+
+
+- :issue:`7119`: Exit with an error if the ``--basetemp`` argument is empty, is the current working directory or is one of the parent directories.
+ This is done to protect against accidental data loss, as any directory passed to this argument is cleared.
+
+
+- :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:`7159`: :meth:`caplog.set_level() <_pytest.logging.LogCaptureFixture.set_level>` and :meth:`caplog.at_level() <_pytest.logging.LogCaptureFixture.at_level>` no longer affect
+ the level of logs that are shown in the *Captured log report* report section.
+
+
+- :issue:`7348`: Improve recursive diff report for comparison asserts on dataclasses / attrs.
+
+
+- :issue:`7385`: ``--junitxml`` now includes the exception cause in the ``message`` XML attribute for failures during setup and teardown.
+
+ Previously:
+
+ .. code-block:: xml
+
+ <error message="test setup failure">
+
+ Now:
+
+ .. code-block:: xml
+
+ <error message="failed on setup with &quot;ValueError: Some error during setup&quot;">
+
+
+
+Bug Fixes
+---------
+
+- :issue:`1120`: Fix issue where directories from :fixture:`tmpdir` are not removed properly when multiple instances of pytest are running in parallel.
+
+
+- :issue:`4583`: Prevent crashing and provide a user-friendly error when a marker expression (`-m`) invoking of :func:`eval` raises any exception.
+
+
+- :issue:`4677`: The path shown in the summary report for SKIPPED tests is now always relative. Previously it was sometimes absolute.
+
+
+- :issue:`5456`: Fix a possible race condition when trying to remove lock files used to control access to folders
+ created by :fixture:`tmp_path` and :fixture:`tmpdir`.
+
+
+- :issue:`6240`: Fixes an issue where logging during collection step caused duplication of log
+ messages to stderr.
+
+
+- :issue:`6428`: Paths appearing in error messages are now correct in case the current working directory has
+ changed since the start of the session.
+
+
+- :issue:`6755`: Support deleting paths longer than 260 characters on windows created inside :fixture:`tmpdir`.
+
+
+- :issue:`6871`: Fix crash with captured output when using :fixture:`capsysbinary`.
+
+
+- :issue:`6909`: Revert the change introduced by :pull:`6330`, which required all arguments to ``@pytest.mark.parametrize`` to be explicitly defined in the function signature.
+
+ The intention of the original change was to remove what was expected to be an unintended/surprising behavior, but it turns out many people relied on it, so the restriction has been reverted.
+
+
+- :issue:`6910`: Fix crash when plugins return an unknown stats while using the ``--reportlog`` option.
+
+
+- :issue:`6924`: Ensure a ``unittest.IsolatedAsyncioTestCase`` is actually awaited.
+
+
+- :issue:`6925`: Fix `TerminalRepr` instances to be hashable again.
+
+
+- :issue:`6947`: Fix regression where functions registered with :meth:`unittest.TestCase.addCleanup` were not being called on test failures.
+
+
+- :issue:`6951`: Allow users to still set the deprecated ``TerminalReporter.writer`` attribute.
+
+
+- :issue:`6956`: Prevent pytest from printing `ConftestImportFailure` traceback to stdout.
+
+
+- :issue:`6991`: Fix regressions with `--lf` filtering too much since pytest 5.4.
+
+
+- :issue:`6992`: Revert "tmpdir: clean up indirection via config for factories" :issue:`6767` as it breaks pytest-xdist.
+
+
+- :issue:`7061`: When a yielding fixture fails to yield a value, report a test setup error instead of crashing.
+
+
+- :issue:`7076`: The path of file skipped by ``@pytest.mark.skip`` in the SKIPPED report is now relative to invocation directory. Previously it was relative to root directory.
+
+
+- :issue:`7110`: Fixed regression: ``asyncbase.TestCase`` tests are executed correctly again.
+
+
+- :issue:`7126`: ``--setup-show`` now doesn't raise an error when a bytes value is used as a ``parametrize``
+ 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:`7145`: Classes with broken ``__getattribute__`` methods are displayed correctly during failures.
+
+
+- :issue:`7150`: Prevent hiding the underlying exception when ``ConfTestImportFailure`` is raised.
+
+
+- :issue:`7180`: Fix ``_is_setup_py`` for files encoded differently than locale.
+
+
+- :issue:`7215`: Fix regression where running with ``--pdb`` would call :meth:`unittest.TestCase.tearDown` for skipped tests.
+
+
+- :issue:`7253`: When using ``pytest.fixture`` on a function directly, as in ``pytest.fixture(func)``,
+ if the ``autouse`` or ``params`` arguments are also passed, the function is no longer
+ ignored, but is marked as a fixture.
+
+
+- :issue:`7360`: Fix possibly incorrect evaluation of string expressions passed to ``pytest.mark.skipif`` and ``pytest.mark.xfail``,
+ in rare circumstances where the exact same string is used but refers to different global values.
+
+
+- :issue:`7383`: Fixed exception causes all over the codebase, i.e. use `raise new_exception from old_exception` when wrapping an exception.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`7202`: The development guide now links to the contributing section of the docs and `RELEASING.rst` on GitHub.
+
+
+- :issue:`7233`: Add a note about ``--strict`` and ``--strict-markers`` and the preference for the latter one.
+
+
+- :issue:`7345`: Explain indirect parametrization and markers for fixtures.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`7035`: The ``originalname`` attribute of ``_pytest.python.Function`` now defaults to ``name`` if not
+ provided explicitly, and is always set.
+
+
+- :issue:`7264`: The dependency on the ``wcwidth`` package has been removed.
+
+
+- :issue:`7291`: Replaced ``py.iniconfig`` with :pypi:`iniconfig`.
+
+
+- :issue:`7295`: ``src/_pytest/config/__init__.py`` now uses the ``warnings`` module to report warnings instead of ``sys.stderr.write``.
+
+
+- :issue:`7356`: Remove last internal uses of deprecated *slave* term from old ``pytest-xdist``.
+
+
+- :issue:`7357`: ``py``>=1.8.2 is now required.
+
+
+pytest 5.4.3 (2020-06-02)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`6428`: Paths appearing in error messages are now correct in case the current working directory has
+ changed since the start of the session.
+
+
+- :issue:`6755`: Support deleting paths longer than 260 characters on windows created inside tmpdir.
+
+
+- :issue:`6956`: Prevent pytest from printing ConftestImportFailure traceback to stdout.
+
+
+- :issue:`7150`: Prevent hiding the underlying exception when ``ConfTestImportFailure`` is raised.
+
+
+- :issue:`7215`: Fix regression where running with ``--pdb`` would call the ``tearDown`` methods of ``unittest.TestCase``
+ subclasses for skipped tests.
+
+
+pytest 5.4.2 (2020-05-08)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`6871`: Fix crash with captured output when using the :fixture:`capsysbinary fixture <capsysbinary>`.
+
+
+- :issue:`6924`: Ensure a ``unittest.IsolatedAsyncioTestCase`` is actually awaited.
+
+
+- :issue:`6925`: Fix TerminalRepr instances to be hashable again.
+
+
+- :issue:`6947`: Fix regression where functions registered with ``TestCase.addCleanup`` were not being called on test failures.
+
+
+- :issue:`6951`: Allow users to still set the deprecated ``TerminalReporter.writer`` attribute.
+
+
+- :issue:`6992`: Revert "tmpdir: clean up indirection via config for factories" #6767 as it breaks pytest-xdist.
+
+
+- :issue:`7110`: Fixed regression: ``asyncbase.TestCase`` tests are executed correctly again.
+
+
+- :issue:`7143`: Fix ``File.from_parent`` so it forwards extra keyword arguments to the constructor.
+
+
+- :issue:`7145`: Classes with broken ``__getattribute__`` methods are displayed correctly during failures.
+
+
+- :issue:`7180`: Fix ``_is_setup_py`` for files encoded differently than locale.
+
+
+pytest 5.4.1 (2020-03-13)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`6909`: Revert the change introduced by :pull:`6330`, which required all arguments to ``@pytest.mark.parametrize`` to be explicitly defined in the function signature.
+
+ The intention of the original change was to remove what was expected to be an unintended/surprising behavior, but it turns out many people relied on it, so the restriction has been reverted.
+
+
+- :issue:`6910`: Fix crash when plugins return an unknown stats while using the ``--reportlog`` option.
+
+
+pytest 5.4.0 (2020-03-12)
+=========================
+
+Breaking Changes
+----------------
+
+- :issue:`6316`: Matching of ``-k EXPRESSION`` to test names is now case-insensitive.
+
+
+- :issue:`6443`: Plugins specified with ``-p`` are now loaded after internal plugins, which results in their hooks being called *before* the internal ones.
+
+ This makes the ``-p`` behavior consistent with ``PYTEST_PLUGINS``.
+
+
+- :issue:`6637`: Removed the long-deprecated ``pytest_itemstart`` hook.
+
+ 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:`6737`: The ``cached_result`` attribute of ``FixtureDef`` is now set to ``None`` when
+ the result is unavailable, instead of being deleted.
+
+ If your plugin performs checks like ``hasattr(fixturedef, 'cached_result')``,
+ for example in a ``pytest_fixture_post_finalizer`` hook implementation, replace
+ it with ``fixturedef.cached_result is not None``. If you ``del`` the attribute,
+ set it to ``None`` instead.
+
+
+
+Deprecations
+------------
+
+- :issue:`3238`: Option ``--no-print-logs`` is deprecated and meant to be removed in a future release. If you use ``--no-print-logs``, please try out ``--show-capture`` and
+ provide feedback.
+
+ ``--show-capture`` command-line option was added in ``pytest 3.5.0`` and allows to specify how to
+ display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` or ``all`` (the default).
+
+
+- :issue:`571`: Deprecate the unused/broken `pytest_collect_directory` hook.
+ It was misaligned since the removal of the ``Directory`` collector in 2010
+ and incorrect/unusable as soon as collection was split from test execution.
+
+
+- :issue:`5975`: Deprecate using direct constructors for ``Nodes``.
+
+ Instead they are now constructed via ``Node.from_parent``.
+
+ This transitional mechanism enables us to untangle the very intensely
+ entangled ``Node`` relationships by enforcing more controlled creation/configuration patterns.
+
+ As part of this change, session/config are already disallowed parameters and as we work on the details we might need disallow a few more as well.
+
+ Subclasses are expected to use `super().from_parent` if they intend to expand the creation of `Nodes`.
+
+
+- :issue:`6779`: The ``TerminalReporter.writer`` attribute has been deprecated and should no longer be used. This
+ was inadvertently exposed as part of the public API of that plugin and ties it too much
+ with ``py.io.TerminalWriter``.
+
+
+
+Features
+--------
+
+- :issue:`4597`: New :ref:`--capture=tee-sys <capture-method>` option to allow both live printing and capturing of test output.
+
+
+- :issue:`5712`: Now all arguments to ``@pytest.mark.parametrize`` need to be explicitly declared in the function signature or via ``indirect``.
+ Previously it was possible to omit an argument if a fixture with the same name existed, which was just an accident of implementation and was not meant to be a part of the API.
+
+
+- :issue:`6454`: Changed default for `-r` to `fE`, which displays failures and errors in the :ref:`short test summary <pytest.detailed_failed_tests_usage>`. `-rN` can be used to disable it (the old behavior).
+
+
+- :issue:`6469`: New options have been added to the :confval:`junit_logging` option: ``log``, ``out-err``, and ``all``.
+
+
+- :issue:`6834`: Excess warning summaries are now collapsed per file to ensure readable display of warning summaries.
+
+
+
+Improvements
+------------
+
+- :issue:`1857`: ``pytest.mark.parametrize`` accepts integers for ``ids`` again, converting it to strings.
+
+
+- :issue:`449`: Use "yellow" main color with any XPASSED tests.
+
+
+- :issue:`4639`: Revert "A warning is now issued when assertions are made for ``None``".
+
+ The warning proved to be less useful than initially expected and had quite a
+ few false positive cases.
+
+
+- :issue:`5686`: ``tmpdir_factory.mktemp`` now fails when given absolute and non-normalized paths.
+
+
+- :issue:`5984`: The ``pytest_warning_captured`` hook now receives a ``location`` parameter with the code location that generated the warning.
+
+
+- :issue:`6213`: pytester: the ``testdir`` fixture respects environment settings from the ``monkeypatch`` fixture for inner runs.
+
+
+- :issue:`6247`: ``--fulltrace`` is honored with collection errors.
+
+
+- :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:`6658`: Code is now highlighted in tracebacks when ``pygments`` is installed.
+
+ Users are encouraged to install ``pygments`` into their environment and provide feedback, because
+ the plan is to make ``pygments`` a regular dependency in the future.
+
+
+- :issue:`6795`: Import usage error message with invalid `-o` option.
+
+
+- :issue:`759`: ``pytest.mark.parametrize`` supports iterators and generators for ``ids``.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`310`: Add support for calling `pytest.xfail()` and `pytest.importorskip()` with doctests.
+
+
+- :issue:`3823`: ``--trace`` now works with unittests.
+
+
+- :issue:`4445`: Fixed some warning reports produced by pytest to point to the correct location of the warning in the user's code.
+
+
+- :issue:`5301`: Fix ``--last-failed`` to collect new tests from files with known failures.
+
+
+- :issue:`5928`: Report ``PytestUnknownMarkWarning`` at the level of the user's code, not ``pytest``'s.
+
+
+- :issue:`5991`: Fix interaction with ``--pdb`` and unittests: do not use unittest's ``TestCase.debug()``.
+
+
+- :issue:`6334`: Fix summary entries appearing twice when ``f/F`` and ``s/S`` report chars were used at the same time in the ``-r`` command-line option (for example ``-rFf``).
+
+ The upper case variants were never documented and the preferred form should be the lower case.
+
+
+- :issue:`6409`: Fallback to green (instead of yellow) for non-last items without previous passes with colored terminal progress indicator.
+
+
+- :issue:`6454`: `--disable-warnings` is honored with `-ra` and `-rA`.
+
+
+- :issue:`6497`: Fix bug in the comparison of request key with cached key in fixture.
+
+ A construct ``if key == cached_key:`` can fail either because ``==`` is explicitly disallowed, or for, e.g., NumPy arrays, where the result of ``a == b`` cannot generally be converted to :class:`bool`.
+ The implemented fix replaces `==` with ``is``.
+
+
+- :issue:`6557`: Make capture output streams ``.write()`` method return the same return value from original streams.
+
+
+- :issue:`6566`: Fix ``EncodedFile.writelines`` to call the underlying buffer's ``writelines`` method.
+
+
+- :issue:`6575`: Fix internal crash when ``faulthandler`` starts initialized
+ (for example with ``PYTHONFAULTHANDLER=1`` environment variable set) and ``faulthandler_timeout`` defined
+ in the configuration file.
+
+
+- :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:`6660`: :py:func:`pytest.exit` is handled when emitted from the :hook:`pytest_sessionfinish` hook. This includes quitting from a debugger.
+
+
+- :issue:`6752`: When :py:func:`pytest.raises` is used as a function (as opposed to a context manager),
+ a `match` keyword argument is now passed through to the tested function. Previously
+ it was swallowed and ignored (regression in pytest 5.1.0).
+
+
+- :issue:`6801`: Do not display empty lines in between traceback for unexpected exceptions with doctests.
+
+
+- :issue:`6802`: The :fixture:`testdir fixture <testdir>` works within doctests now.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`6696`: Add list of fixtures to start of fixture chapter.
+
+
+- :issue:`6742`: Expand first sentence on fixtures into a paragraph.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`6404`: Remove usage of ``parser`` module, deprecated in Python 3.9.
+
+
+pytest 5.3.5 (2020-01-29)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`6517`: Fix regression in pytest 5.3.4 causing an INTERNALERROR due to a wrong assertion.
+
+
+pytest 5.3.4 (2020-01-20)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`6496`: Revert :issue:`6436`: unfortunately this change has caused a number of regressions in many suites,
+ so the team decided to revert this change and make a new release while we continue to look for a solution.
+
+
+pytest 5.3.3 (2020-01-16)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`2780`: Captured output during teardown is shown with ``-rP``.
+
+
+- :issue:`5971`: Fix a ``pytest-xdist`` crash when dealing with exceptions raised in subprocesses created by the
+ ``multiprocessing`` module.
+
+
+- :issue:`6436`: :class:`FixtureDef <_pytest.fixtures.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.
+
+
+- :issue:`6532`: Fix parsing of outcomes containing multiple errors with ``testdir`` results (regression in 5.3.0).
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`6350`: Optimized automatic renaming of test parameter IDs.
+
+
+pytest 5.3.2 (2019-12-13)
+=========================
+
+Improvements
+------------
+
+- :issue:`4639`: Revert "A warning is now issued when assertions are made for ``None``".
+
+ The warning proved to be less useful than initially expected and had quite a
+ few false positive cases.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`5430`: junitxml: Logs for failed test are now passed to junit report in case the test fails during call phase.
+
+
+- :issue:`6290`: The supporting files in the ``.pytest_cache`` directory are kept with ``--cache-clear``, which only clears cached values now.
+
+
+- :issue:`6301`: Fix assertion rewriting for egg-based distributions and ``editable`` installs (``pip install --editable``).
+
+
+pytest 5.3.1 (2019-11-25)
+=========================
+
+Improvements
+------------
+
+- :issue:`6231`: Improve check for misspelling of :ref:`pytest.mark.parametrize ref`.
+
+
+- :issue:`6257`: Handle :func:`pytest.exit` being used via :hook:`pytest_internalerror`, e.g. when quitting pdb from post mortem.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`5914`: pytester: fix :py:func:`~_pytest.pytester.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`.
+
+
+- :issue:`6254`: Fix compatibility with pytest-parallel (regression in pytest 5.3.0).
+
+
+- :issue:`6255`: Clear the :py:data:`sys.last_traceback`, :py:data:`sys.last_type`
+ and :py:data:`sys.last_value` attributes by deleting them instead
+ of setting them to ``None``. This better matches the behaviour of
+ the Python standard library.
+
+
+pytest 5.3.0 (2019-11-19)
+=========================
+
+Deprecations
+------------
+
+- :issue:`6179`: The default value of :confval:`junit_family` option will change to ``"xunit2"`` in pytest 6.0, given
+ that this is the version supported by default in modern tools that manipulate this type of file.
+
+ In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option
+ is given in the command line but :confval:`junit_family` is not explicitly configured in ``pytest.ini``.
+
+ For more information, :ref:`see the docs <junit-family changed default value>`.
+
+
+
+Features
+--------
+
+- :issue:`4488`: The pytest team has created the `pytest-reportlog <https://github.com/pytest-dev/pytest-reportlog>`__
+ plugin, which provides a new ``--report-log=FILE`` option that writes *report logs* into a file as the test session executes.
+
+ Each line of the report log contains a self contained JSON object corresponding to a testing event,
+ such as a collection or a test result report. The file is guaranteed to be flushed after writing
+ each line, so systems can read and process events in real-time.
+
+ The plugin is meant to replace the ``--resultlog`` option, which is deprecated and meant to be removed
+ in a future release. If you use ``--resultlog``, please try out ``pytest-reportlog`` and
+ provide feedback.
+
+
+- :issue:`4730`: When :py:data:`sys.pycache_prefix` (Python 3.8+) is set, it will be used by pytest to cache test files changed by the assertion rewriting mechanism.
+
+ This makes it easier to benefit of cached ``.pyc`` files even on file systems without permissions.
+
+
+- :issue:`5515`: Allow selective auto-indentation of multiline log messages.
+
+ Adds command line option ``--log-auto-indent``, config option
+ :confval:`log_auto_indent` and support for per-entry configuration of
+ indentation behavior on calls to :py:func:`python:logging.log()`.
+
+ Alters the default for auto-indention from ``"on"`` to ``"off"``. This
+ restores the older behavior that existed prior to v4.6.0. This
+ reversion to earlier behavior was done because it is better to
+ activate new features that may lead to broken tests explicitly
+ 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`.
+
+ The functions are used to ensure the captured text *does not* match the given
+ pattern.
+
+ The previous idiom was to use :py:func:`python:re.match`:
+
+ .. code-block:: python
+
+ result = testdir.runpytest()
+ assert re.match(pat, result.stdout.str()) is None
+
+ Or the ``in`` operator:
+
+ .. code-block:: python
+
+ result = testdir.runpytest()
+ assert text in result.stdout.str()
+
+ But the new functions produce best output on failure.
+
+
+- :issue:`6057`: Added tolerances to complex values when printing ``pytest.approx``.
+
+ For example, ``repr(pytest.approx(3+4j))`` returns ``(3+4j) ± 5e-06 ∠ ±180°``. This is polar notation indicating a circle around the expected value, with a radius of 5e-06. For ``approx`` comparisons to return ``True``, the actual value should fall within this circle.
+
+
+- :issue:`6061`: Added the pluginmanager as an argument to ``pytest_addoption``
+ so that hooks can be invoked when setting up command line options. This is
+ useful for having one plugin communicate things to another plugin,
+ such as default values or which set of command line options to add.
+
+
+
+Improvements
+------------
+
+- :issue:`5061`: Use multiple colors with terminal summary statistics.
+
+
+- :issue:`5630`: Quitting from debuggers is now properly handled in ``doctest`` items.
+
+
+- :issue:`5924`: Improved verbose diff output with sequences.
+
+ Before:
+
+ ::
+
+ E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...]
+ E Right contains 3 more items, first extra item: ' '
+ E Full diff:
+ E - ['version', 'version_info', 'sys.version', 'sys.version_info']
+ E + ['version',
+ E + 'version_info',
+ E + 'sys.version',
+ E + 'sys.version_info',
+ E + ' ',
+ E + 'sys.version',
+ E + 'sys.version_info']
+
+ After:
+
+ ::
+
+ E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...]
+ E Right contains 3 more items, first extra item: ' '
+ E Full diff:
+ E [
+ E 'version',
+ E 'version_info',
+ E 'sys.version',
+ E 'sys.version_info',
+ E + ' ',
+ E + 'sys.version',
+ E + 'sys.version_info',
+ E ]
+
+
+- :issue:`5934`: ``repr`` of ``ExceptionInfo`` objects has been improved to honor the ``__repr__`` method of the underlying exception.
+
+- :issue:`5936`: Display untruncated assertion message with ``-vv``.
+
+
+- :issue:`5990`: Fixed plurality mismatch in test summary (e.g. display "1 error" instead of "1 errors").
+
+
+- :issue:`6008`: ``Config.InvocationParams.args`` is now always a ``tuple`` to better convey that it should be
+ immutable and avoid accidental modifications.
+
+
+- :issue:`6023`: ``pytest.main`` returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still).
+
+
+- :issue:`6026`: Align prefixes in output of pytester's ``LineMatcher``.
+
+
+- :issue:`6059`: Collection errors are reported as errors (and not failures like before) in the terminal's short test summary.
+
+
+- :issue:`6069`: ``pytester.spawn`` does not skip/xfail tests on FreeBSD anymore unconditionally.
+
+
+- :issue:`6097`: The "[...%]" indicator in the test summary is now colored according to the final (new) multi-colored line's main color.
+
+
+- :issue:`6116`: Added ``--co`` as a synonym to ``--collect-only``.
+
+
+- :issue:`6148`: ``atomicwrites`` is now only used on Windows, fixing a performance regression with assertion rewriting on Unix.
+
+
+- :issue:`6152`: Now parametrization will use the ``__name__`` attribute of any object for the id, if present. Previously it would only use ``__name__`` for functions and classes.
+
+
+- :issue:`6176`: Improved failure reporting with pytester's ``Hookrecorder.assertoutcome``.
+
+
+- :issue:`6181`: The reason for a stopped session, e.g. with ``--maxfail`` / ``-x``, now gets reported in the test summary.
+
+
+- :issue:`6206`: Improved ``cache.set`` robustness and performance.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`2049`: Fixed ``--setup-plan`` showing inaccurate information about fixture lifetimes.
+
+
+- :issue:`2548`: Fixed line offset mismatch of skipped tests in terminal summary.
+
+
+- :issue:`6039`: The ``PytestDoctestRunner`` is now properly invalidated when unconfiguring the doctest plugin.
+
+ This is important when used with ``pytester``'s ``runpytest_inprocess``.
+
+
+- :issue:`6047`: BaseExceptions are now handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc.
+
+
+- :issue:`6074`: pytester: fixed order of arguments in ``rm_rf`` warning when cleaning up temporary directories, and do not emit warnings for errors with ``os.open``.
+
+
+- :issue:`6189`: Fixed result of ``getmodpath`` method.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`4901`: ``RunResult`` from ``pytester`` now displays the mnemonic of the ``ret`` attribute when it is a
+ valid ``pytest.ExitCode`` value.
+
+
+pytest 5.2.4 (2019-11-15)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`6194`: Fix incorrect discovery of non-test ``__init__.py`` files.
+
+
+- :issue:`6197`: Revert "The first test in a package (``__init__.py``) marked with ``@pytest.mark.skip`` is now correctly skipped.".
+
+
+pytest 5.2.3 (2019-11-14)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5830`: The first test in a package (``__init__.py``) marked with ``@pytest.mark.skip`` is now correctly skipped.
+
+
+- :issue:`6099`: Fix ``--trace`` when used with parametrized functions.
+
+
+- :issue:`6183`: Using ``request`` as a parameter name in ``@pytest.mark.parametrize`` now produces a more
+ user-friendly error.
+
+
+pytest 5.2.2 (2019-10-24)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5206`: Fix ``--nf`` to not forget about known nodeids with partial test selection.
+
+
+- :issue:`5906`: Fix crash with ``KeyboardInterrupt`` during ``--setup-show``.
+
+
+- :issue:`5946`: Fixed issue when parametrizing fixtures with numpy arrays (and possibly other sequence-like types).
+
+
+- :issue:`6044`: Properly ignore ``FileNotFoundError`` exceptions when trying to remove old temporary directories,
+ for instance when multiple processes try to remove the same directory (common with ``pytest-xdist``
+ for example).
+
+
+pytest 5.2.1 (2019-10-06)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5902`: Fix warnings about deprecated ``cmp`` attribute in ``attrs>=19.2``.
+
+
+pytest 5.2.0 (2019-09-28)
+=========================
+
+Deprecations
+------------
+
+- :issue:`1682`: Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them
+ as a keyword argument instead.
+
+
+
+Features
+--------
+
+- :issue:`1682`: The ``scope`` parameter of ``@pytest.fixture`` can now be a callable that receives
+ the fixture name and the ``config`` object as keyword-only parameters.
+ See :ref:`the docs <dynamic scope>` for more information.
+
+
+- :issue:`5764`: New behavior of the ``--pastebin`` option: failures to connect to the pastebin server are reported, without failing the pytest run
+
+
+
+Bug Fixes
+---------
+
+- :issue:`5806`: Fix "lexer" being used when uploading to bpaste.net from ``--pastebin`` to "text".
+
+
+- :issue:`5884`: Fix ``--setup-only`` and ``--setup-show`` for custom pytest items.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`5056`: The HelpFormatter uses ``py.io.get_terminal_width`` for better width detection.
+
+
+pytest 5.1.3 (2019-09-18)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5807`: Fix pypy3.6 (nightly) on windows.
+
+
+- :issue:`5811`: Handle ``--fulltrace`` correctly with ``pytest.raises``.
+
+
+- :issue:`5819`: Windows: Fix regression with conftest whose qualified name contains uppercase
+ characters (introduced by #5792).
+
+
+pytest 5.1.2 (2019-08-30)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`2270`: Fixed ``self`` reference in function-scoped fixtures defined plugin classes: previously ``self``
+ would be a reference to a *test* class, not the *plugin* class.
+
+
+- :issue:`570`: Fixed long standing issue where fixture scope was not respected when indirect fixtures were used during
+ parametrization.
+
+
+- :issue:`5782`: Fix decoding error when printing an error response from ``--pastebin``.
+
+
+- :issue:`5786`: Chained exceptions in test and collection reports are now correctly serialized, allowing plugins like
+ ``pytest-xdist`` to display them properly.
+
+
+- :issue:`5792`: Windows: Fix error that occurs in certain circumstances when loading
+ ``conftest.py`` from a working directory that has casing other than the one stored
+ in the filesystem (e.g., ``c:\test`` instead of ``C:\test``).
+
+
+pytest 5.1.1 (2019-08-20)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5751`: Fixed ``TypeError`` when importing pytest on Python 3.5.0 and 3.5.1.
+
+
+pytest 5.1.0 (2019-08-15)
+=========================
+
+Removals
+--------
+
+- :issue:`5180`: As per our policy, the following features have been deprecated in the 4.X series and are now
+ removed:
+
+ * ``Request.getfuncargvalue``: use ``Request.getfixturevalue`` instead.
+
+ * ``pytest.raises`` and ``pytest.warns`` no longer support strings as the second argument.
+
+ * ``message`` parameter of ``pytest.raises``.
+
+ * ``pytest.raises``, ``pytest.warns`` and ``ParameterSet.param`` now use native keyword-only
+ syntax. This might change the exception message from previous versions, but they still raise
+ ``TypeError`` on unknown keyword arguments as before.
+
+ * ``pytest.config`` global variable.
+
+ * ``tmpdir_factory.ensuretemp`` method.
+
+ * ``pytest_logwarning`` hook.
+
+ * ``RemovedInPytest4Warning`` warning type.
+
+ * ``request`` is now a reserved name for fixtures.
+
+
+ For more information consult :std:doc:`deprecations` in the docs.
+
+
+- :issue:`5565`: Removed unused support code for :pypi:`unittest2`.
+
+ The ``unittest2`` backport module is no longer
+ necessary since Python 3.3+, and the small amount of code in pytest to support it also doesn't seem
+ to be used: after removed, all tests still pass unchanged.
+
+ Although our policy is to introduce a deprecation period before removing any features or support
+ for third party libraries, because this code is apparently not used
+ at all (even if ``unittest2`` is used by a test suite executed by pytest), it was decided to
+ remove it in this release.
+
+ If you experience a regression because of this, please :issue:`file an issue <new>`.
+
+
+- :issue:`5615`: ``pytest.fail``, ``pytest.xfail`` and ``pytest.skip`` no longer support bytes for the message argument.
+
+ This was supported for Python 2 where it was tempting to use ``"message"``
+ instead of ``u"message"``.
+
+ Python 3 code is unlikely to pass ``bytes`` to these functions. If you do,
+ please decode it to an ``str`` beforehand.
+
+
+
+Features
+--------
+
+- :issue:`5564`: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``.
+
+
+- :issue:`5576`: New :ref:`NUMBER <using doctest options>`
+ option for doctests to ignore irrelevant differences in floating-point numbers.
+ Inspired by Sébastien Boisgérault's `numtest <https://github.com/boisgera/numtest>`__
+ extension for doctest.
+
+
+
+Improvements
+------------
+
+- :issue:`5471`: JUnit XML now includes a timestamp and hostname in the testsuite tag.
+
+
+- :issue:`5707`: Time taken to run the test suite now includes a human-readable representation when it takes over
+ 60 seconds, for example::
+
+ ===== 2 failed in 102.70s (0:01:42) =====
+
+
+
+Bug Fixes
+---------
+
+- :issue:`4344`: Fix RuntimeError/StopIteration when trying to collect package with "__init__.py" only.
+
+
+- :issue:`5115`: Warnings issued during ``pytest_configure`` are explicitly not treated as errors, even if configured as such, because it otherwise completely breaks pytest.
+
+
+- :issue:`5477`: The XML file produced by ``--junitxml`` now correctly contain a ``<testsuites>`` root element.
+
+
+- :issue:`5524`: Fix issue where ``tmp_path`` and ``tmpdir`` would not remove directories containing files marked as read-only,
+ which could lead to pytest crashing when executed a second time with the ``--basetemp`` option.
+
+
+- :issue:`5537`: Replace ``importlib_metadata`` backport with ``importlib.metadata`` from the
+ standard library on Python 3.8+.
+
+
+- :issue:`5578`: Improve type checking for some exception-raising functions (``pytest.xfail``, ``pytest.skip``, etc)
+ so they provide better error messages when users meant to use marks (for example ``@pytest.xfail``
+ instead of ``@pytest.mark.xfail``).
+
+
+- :issue:`5606`: Fixed internal error when test functions were patched with objects that cannot be compared
+ for truth values against others, like ``numpy`` arrays.
+
+
+- :issue:`5634`: ``pytest.exit`` is now correctly handled in ``unittest`` cases.
+ This makes ``unittest`` cases handle ``quit`` from pytest's pdb correctly.
+
+
+- :issue:`5650`: Improved output when parsing an ini configuration file fails.
+
+
+- :issue:`5701`: Fix collection of ``staticmethod`` objects defined with ``functools.partial``.
+
+
+- :issue:`5734`: Skip async generator test functions, and update the warning message to refer to ``async def`` functions.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`5669`: Add docstring for ``Testdir.copy_example``.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`5095`: XML files of the ``xunit2`` family are now validated against the schema by pytest's own test suite
+ to avoid future regressions.
+
+
+- :issue:`5516`: Cache node splitting function which can improve collection performance in very large test suites.
+
+
+- :issue:`5603`: Simplified internal ``SafeRepr`` class and removed some dead code.
+
+
+- :issue:`5664`: When invoking pytest's own testsuite with ``PYTHONDONTWRITEBYTECODE=1``,
+ the ``test_xfail_handling`` test no longer fails.
+
+
+- :issue:`5684`: Replace manual handling of ``OSError.errno`` in the codebase by new ``OSError`` subclasses (``PermissionError``, ``FileNotFoundError``, etc.).
+
+
+pytest 5.0.1 (2019-07-04)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5479`: Improve quoting in ``raises`` match failure message.
+
+
+- :issue:`5523`: Fixed using multiple short options together in the command-line (for example ``-vs``) in Python 3.8+.
+
+
+- :issue:`5547`: ``--step-wise`` now handles ``xfail(strict=True)`` markers properly.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`5517`: Improve "Declaring new hooks" section in chapter "Writing Plugins"
+
+
+pytest 5.0.0 (2019-06-28)
+=========================
+
+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>`.
+
+Removals
+--------
+
+- :issue:`1149`: Pytest no longer accepts prefixes of command-line arguments, for example
+ typing ``pytest --doctest-mod`` inplace of ``--doctest-modules``.
+ This was previously allowed where the ``ArgumentParser`` thought it was unambiguous,
+ but this could be incorrect due to delayed parsing of options for plugins.
+ See for example issues :issue:`1149`,
+ :issue:`3413`, and
+ :issue:`4009`.
+
+
+- :issue:`5402`: **PytestDeprecationWarning are now errors by default.**
+
+ Following our plan to remove deprecated features with as little disruption as
+ possible, all warnings of type ``PytestDeprecationWarning`` now generate errors
+ instead of warning messages.
+
+ **The affected features will be effectively removed in pytest 5.1**, so please consult the
+ :std:doc:`deprecations` section in the docs for directions on how to update existing code.
+
+ In the pytest ``5.0.X`` series, it is possible to change the errors back into warnings as a stop
+ gap measure by adding this to your ``pytest.ini`` file:
+
+ .. code-block:: ini
+
+ [pytest]
+ filterwarnings =
+ ignore::pytest.PytestDeprecationWarning
+
+ But this will stop working when pytest ``5.1`` is released.
+
+ **If you have concerns** about the removal of a specific feature, please add a
+ comment to :issue:`5402`.
+
+
+- :issue:`5412`: ``ExceptionInfo`` objects (returned by ``pytest.raises``) now have the same ``str`` representation as ``repr``, which
+ avoids some confusion when users use ``print(e)`` to inspect the object.
+
+ This means code like:
+
+ .. code-block:: python
+
+ with pytest.raises(SomeException) as e:
+ ...
+ assert "some message" in str(e)
+
+
+ Needs to be changed to:
+
+ .. code-block:: python
+
+ with pytest.raises(SomeException) as e:
+ ...
+ assert "some message" in str(e.value)
+
+
+
+
+Deprecations
+------------
+
+- :issue:`4488`: The removal of the ``--result-log`` option and module has been postponed to (tentatively) pytest 6.0 as
+ the team has not yet got around to implement a good alternative for it.
+
+
+- :issue:`466`: The ``funcargnames`` attribute has been an alias for ``fixturenames`` since
+ pytest 2.3, and is now deprecated in code too.
+
+
+
+Features
+--------
+
+- :issue:`3457`: New :hook:`pytest_assertion_pass`
+ hook, called with context information when an assertion *passes*.
+
+ This hook is still **experimental** so use it with caution.
+
+
+- :issue:`5440`: The :mod:`faulthandler` standard library
+ module is now enabled by default to help users diagnose crashes in C modules.
+
+ This functionality was provided by integrating the external
+ `pytest-faulthandler <https://github.com/pytest-dev/pytest-faulthandler>`__ plugin into the core,
+ so users should remove that plugin from their requirements if used.
+
+ For more information see the docs: :ref:`faulthandler`.
+
+
+- :issue:`5452`: When warnings are configured as errors, pytest warnings now appear as originating from ``pytest.`` instead of the internal ``_pytest.warning_types.`` module.
+
+
+- :issue:`5125`: ``Session.exitcode`` values are now coded in ``pytest.ExitCode``, an ``IntEnum``. This makes the exit code available for consumer code and are more explicit other than just documentation. User defined exit codes are still valid, but should be used with caution.
+
+ The team doesn't expect this change to break test suites or plugins in general, except in esoteric/specific scenarios.
+
+ **pytest-xdist** users should upgrade to ``1.29.0`` or later, as ``pytest-xdist`` required a compatibility fix because of this change.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`1403`: Switch from ``imp`` to ``importlib``.
+
+
+- :issue:`1671`: The name of the ``.pyc`` files cached by the assertion writer now includes the pytest version
+ to avoid stale caches.
+
+
+- :issue:`2761`: Honor :pep:`235` on case-insensitive file systems.
+
+
+- :issue:`5078`: Test module is no longer double-imported when using ``--pyargs``.
+
+
+- :issue:`5260`: Improved comparison of byte strings.
+
+ When comparing bytes, the assertion message used to show the byte numeric value when showing the differences::
+
+ def test():
+ > assert b'spam' == b'eggs'
+ E AssertionError: assert b'spam' == b'eggs'
+ E At index 0 diff: 115 != 101
+ E Use -v to get the full diff
+
+ It now shows the actual ascii representation instead, which is often more useful::
+
+ def test():
+ > assert b'spam' == b'eggs'
+ E AssertionError: assert b'spam' == b'eggs'
+ E At index 0 diff: b's' != b'e'
+ E Use -v to get the full diff
+
+
+- :issue:`5335`: Colorize level names when the level in the logging format is formatted using
+ '%(levelname).Xs' (truncated fixed width alignment), where X is an integer.
+
+
+- :issue:`5354`: Fix ``pytest.mark.parametrize`` when the argvalues is an iterator.
+
+
+- :issue:`5370`: Revert unrolling of ``all()`` to fix ``NameError`` on nested comprehensions.
+
+
+- :issue:`5371`: Revert unrolling of ``all()`` to fix incorrect handling of generators with ``if``.
+
+
+- :issue:`5372`: Revert unrolling of ``all()`` to fix incorrect assertion when using ``all()`` in an expression.
+
+
+- :issue:`5383`: ``-q`` has again an impact on the style of the collected items
+ (``--collect-only``) when ``--log-cli-level`` is used.
+
+
+- :issue:`5389`: Fix regressions of :pull:`5063` for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``.
+
+
+- :issue:`5390`: Fix regression where the ``obj`` attribute of ``TestCase`` items was no longer bound to methods.
+
+
+- :issue:`5404`: Emit a warning when attempting to unwrap a broken object raises an exception,
+ for easier debugging (:issue:`5080`).
+
+
+- :issue:`5432`: Prevent "already imported" warnings from assertion rewriter when invoking pytest in-process multiple times.
+
+
+- :issue:`5433`: Fix assertion rewriting in packages (``__init__.py``).
+
+
+- :issue:`5444`: Fix ``--stepwise`` mode when the first file passed on the command-line fails to collect.
+
+
+- :issue:`5482`: Fix bug introduced in 4.6.0 causing collection errors when passing
+ more than 2 positional arguments to ``pytest.mark.parametrize``.
+
+
+- :issue:`5505`: Fix crash when discovery fails while using ``-p no:terminal``.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`5315`: Expand docs on mocking classes and dictionaries with ``monkeypatch``.
+
+
+- :issue:`5416`: Fix PytestUnknownMarkWarning in run/skip example.
+
+
+pytest 4.6.11 (2020-06-04)
+==========================
+
+Bug Fixes
+---------
+
+- :issue:`6334`: Fix summary entries appearing twice when ``f/F`` and ``s/S`` report chars were used at the same time in the ``-r`` command-line option (for example ``-rFf``).
+
+ The upper case variants were never documented and the preferred form should be the lower case.
+
+
+- :issue:`7310`: Fix ``UnboundLocalError: local variable 'letter' referenced before
+ assignment`` in ``_pytest.terminal.pytest_report_teststatus()``
+ when plugins return report objects in an unconventional state.
+
+ This was making ``pytest_report_teststatus()`` skip
+ entering if-block branches that declare the ``letter`` variable.
+
+ The fix was to set the initial value of the ``letter`` before
+ the if-block cascade so that it always has a value.
+
+
+pytest 4.6.10 (2020-05-08)
+==========================
+
+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.
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`6404`: Remove usage of ``parser`` module, deprecated in Python 3.9.
+
+
+pytest 4.6.9 (2020-01-04)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`6301`: Fix assertion rewriting for egg-based distributions and ``editable`` installs (``pip install --editable``).
+
+
+pytest 4.6.8 (2019-12-19)
+=========================
+
+Features
+--------
+
+- :issue:`5471`: JUnit XML now includes a timestamp and hostname in the testsuite tag.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`5430`: junitxml: Logs for failed test are now passed to junit report in case the test fails during call phase.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`6345`: Pin ``colorama`` to ``0.4.1`` only for Python 3.4 so newer Python versions can still receive colorama updates.
+
+
+pytest 4.6.7 (2019-12-05)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5477`: The XML file produced by ``--junitxml`` now correctly contain a ``<testsuites>`` root element.
+
+
+- :issue:`6044`: Properly ignore ``FileNotFoundError`` (``OSError.errno == NOENT`` in Python 2) exceptions when trying to remove old temporary directories,
+ for instance when multiple processes try to remove the same directory (common with ``pytest-xdist``
+ for example).
+
+
+pytest 4.6.6 (2019-10-11)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5523`: Fixed using multiple short options together in the command-line (for example ``-vs``) in Python 3.8+.
+
+
+- :issue:`5537`: Replace ``importlib_metadata`` backport with ``importlib.metadata`` from the
+ standard library on Python 3.8+.
+
+
+- :issue:`5806`: Fix "lexer" being used when uploading to bpaste.net from ``--pastebin`` to "text".
+
+
+- :issue:`5902`: Fix warnings about deprecated ``cmp`` attribute in ``attrs>=19.2``.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`5801`: Fixes python version checks (detected by ``flake8-2020``) in case python4 becomes a thing.
+
+
+pytest 4.6.5 (2019-08-05)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`4344`: Fix RuntimeError/StopIteration when trying to collect package with "__init__.py" only.
+
+
+- :issue:`5478`: Fix encode error when using unicode strings in exceptions with ``pytest.raises``.
+
+
+- :issue:`5524`: Fix issue where ``tmp_path`` and ``tmpdir`` would not remove directories containing files marked as read-only,
+ which could lead to pytest crashing when executed a second time with the ``--basetemp`` option.
+
+
+- :issue:`5547`: ``--step-wise`` now handles ``xfail(strict=True)`` markers properly.
+
+
+- :issue:`5650`: Improved output when parsing an ini configuration file fails.
+
+pytest 4.6.4 (2019-06-28)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5404`: Emit a warning when attempting to unwrap a broken object raises an exception,
+ for easier debugging (:issue:`5080`).
+
+
+- :issue:`5444`: Fix ``--stepwise`` mode when the first file passed on the command-line fails to collect.
+
+
+- :issue:`5482`: Fix bug introduced in 4.6.0 causing collection errors when passing
+ more than 2 positional arguments to ``pytest.mark.parametrize``.
+
+
+- :issue:`5505`: Fix crash when discovery fails while using ``-p no:terminal``.
+
+
+pytest 4.6.3 (2019-06-11)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5383`: ``-q`` has again an impact on the style of the collected items
+ (``--collect-only``) when ``--log-cli-level`` is used.
+
+
+- :issue:`5389`: Fix regressions of :pull:`5063` for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``.
+
+
+- :issue:`5390`: Fix regression where the ``obj`` attribute of ``TestCase`` items was no longer bound to methods.
+
+
+pytest 4.6.2 (2019-06-03)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5370`: Revert unrolling of ``all()`` to fix ``NameError`` on nested comprehensions.
+
+
+- :issue:`5371`: Revert unrolling of ``all()`` to fix incorrect handling of generators with ``if``.
+
+
+- :issue:`5372`: Revert unrolling of ``all()`` to fix incorrect assertion when using ``all()`` in an expression.
+
+
+pytest 4.6.1 (2019-06-02)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5354`: Fix ``pytest.mark.parametrize`` when the argvalues is an iterator.
+
+
+- :issue:`5358`: Fix assertion rewriting of ``all()`` calls to deal with non-generators.
+
+
+pytest 4.6.0 (2019-05-31)
+=========================
+
+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>`.
+
+
+Features
+--------
+
+- :issue:`4559`: Added the ``junit_log_passing_tests`` ini value which can be used to enable or disable logging of passing test output in the Junit XML file.
+
+
+- :issue:`4956`: pytester's ``testdir.spawn`` uses ``tmpdir`` as HOME/USERPROFILE directory.
+
+
+- :issue:`5062`: Unroll calls to ``all`` to full for-loops with assertion rewriting for better failure messages, especially when using Generator Expressions.
+
+
+- :issue:`5063`: Switch from ``pkg_resources`` to ``importlib-metadata`` for entrypoint detection for improved performance and import time.
+
+
+- :issue:`5091`: The output for ini options in ``--help`` has been improved.
+
+
+- :issue:`5269`: ``pytest.importorskip`` includes the ``ImportError`` now in the default ``reason``.
+
+
+- :issue:`5311`: Captured logs that are output for each failing test are formatted using the
+ ColoredLevelFormatter.
+
+
+- :issue:`5312`: Improved formatting of multiline log messages in Python 3.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`2064`: The debugging plugin imports the wrapped ``Pdb`` class (``--pdbcls``) on-demand now.
+
+
+- :issue:`4908`: The ``pytest_enter_pdb`` hook gets called with post-mortem (``--pdb``).
+
+
+- :issue:`5036`: Fix issue where fixtures dependent on other parametrized fixtures would be erroneously parametrized.
+
+
+- :issue:`5256`: Handle internal error due to a lone surrogate unicode character not being representable in Jython.
+
+
+- :issue:`5257`: Ensure that ``sys.stdout.mode`` does not include ``'b'`` as it is a text stream.
+
+
+- :issue:`5278`: Pytest's internal python plugin can be disabled using ``-p no:python`` again.
+
+
+- :issue:`5286`: Fix issue with ``disable_test_id_escaping_and_forfeit_all_rights_to_community_support`` option not working when using a list of test IDs in parametrized tests.
+
+
+- :issue:`5330`: Show the test module being collected when emitting ``PytestCollectionWarning`` messages for
+ test classes with ``__init__`` and ``__new__`` methods to make it easier to pin down the problem.
+
+
+- :issue:`5333`: Fix regression in 4.5.0 with ``--lf`` not re-running all tests with known failures from non-selected tests.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`5250`: Expand docs on use of ``setenv`` and ``delenv`` with ``monkeypatch``.
+
+
+pytest 4.5.0 (2019-05-11)
+=========================
+
+Features
+--------
+
+- :issue:`4826`: A warning is now emitted when unknown marks are used as a decorator.
+ This is often due to a typo, which can lead to silently broken tests.
+
+
+- :issue:`4907`: Show XFail reason as part of JUnitXML message field.
+
+
+- :issue:`5013`: Messages from crash reports are displayed within test summaries now, truncated to the terminal width.
+
+
+- :issue:`5023`: New flag ``--strict-markers`` that triggers an error when unknown markers (e.g. those not registered using the :confval:`markers` option in the configuration file) are used in the test suite.
+
+ The existing ``--strict`` option has the same behavior currently, but can be augmented in the future for additional checks.
+
+
+- :issue:`5026`: Assertion failure messages for sequences and dicts contain the number of different items now.
+
+
+- :issue:`5034`: Improve reporting with ``--lf`` and ``--ff`` (run-last-failure).
+
+
+- :issue:`5035`: The ``--cache-show`` option/action accepts an optional glob to show only matching cache entries.
+
+
+- :issue:`5059`: Standard input (stdin) can be given to pytester's ``Testdir.run()`` and ``Testdir.popen()``.
+
+
+- :issue:`5068`: The ``-r`` option learnt about ``A`` to display all reports (including passed ones) in the short test summary.
+
+
+- :issue:`5108`: The short test summary is displayed after passes with output (``-rP``).
+
+
+- :issue:`5172`: The ``--last-failed`` (``--lf``) option got smarter and will now skip entire files if all tests
+ of that test file have passed in previous runs, greatly speeding up collection.
+
+
+- :issue:`5177`: Introduce new specific warning ``PytestWarning`` subclasses to make it easier to filter warnings based on the class, rather than on the message. The new subclasses are:
+
+
+ * ``PytestAssertRewriteWarning``
+
+ * ``PytestCacheWarning``
+
+ * ``PytestCollectionWarning``
+
+ * ``PytestConfigWarning``
+
+ * ``PytestUnhandledCoroutineWarning``
+
+ * ``PytestUnknownMarkWarning``
+
+
+- :issue:`5202`: New ``record_testsuite_property`` session-scoped fixture allows users to log ``<property>`` tags at the ``testsuite``
+ level with the ``junitxml`` plugin.
+
+ The generated XML is compatible with the latest xunit standard, contrary to
+ the properties recorded by ``record_property`` and ``record_xml_attribute``.
+
+
+- :issue:`5214`: The default logging format has been changed to improve readability. Here is an
+ example of a previous logging message::
+
+ test_log_cli_enabled_disabled.py 3 CRITICAL critical message logged by test
+
+ This has now become::
+
+ CRITICAL root:test_log_cli_enabled_disabled.py:3 critical message logged by test
+
+ The formatting can be changed through the :confval:`log_format` configuration option.
+
+
+- :issue:`5220`: ``--fixtures`` now also shows fixture scope for scopes other than ``"function"``.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`5113`: Deselected items from plugins using ``pytest_collect_modifyitems`` as a hookwrapper are correctly reported now.
+
+
+- :issue:`5144`: With usage errors ``exitstatus`` is set to ``EXIT_USAGEERROR`` in the ``pytest_sessionfinish`` hook now as expected.
+
+
+- :issue:`5235`: ``outcome.exit`` is not used with ``EOF`` in the pdb wrapper anymore, but only with ``quit``.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`4935`: Expand docs on registering marks and the effect of ``--strict``.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`4942`: ``logging.raiseExceptions`` is not set to ``False`` anymore.
+
+
+- :issue:`5013`: pytest now depends on :pypi:`wcwidth` to properly track unicode character sizes for more precise terminal output.
+
+
+- :issue:`5059`: pytester's ``Testdir.popen()`` uses ``stdout`` and ``stderr`` via keyword arguments with defaults now (``subprocess.PIPE``).
+
+
+- :issue:`5069`: The code for the short test summary in the terminal was moved to the terminal plugin.
+
+
+- :issue:`5082`: Improved validation of kwargs for various methods in the pytester plugin.
+
+
+- :issue:`5202`: ``record_property`` now emits a ``PytestWarning`` when used with ``junit_family=xunit2``: the fixture generates
+ ``property`` tags as children of ``testcase``, which is not permitted according to the most
+ `recent schema <https://github.com/jenkinsci/xunit-plugin/blob/master/
+ src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd>`__.
+
+
+- :issue:`5239`: Pin ``pluggy`` to ``< 1.0`` so we don't update to ``1.0`` automatically when
+ it gets released: there are planned breaking changes, and we want to ensure
+ pytest properly supports ``pluggy 1.0``.
+
+
+pytest 4.4.2 (2019-05-08)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5089`: Fix crash caused by error in ``__repr__`` function with both ``showlocals`` and verbose output enabled.
+
+
+- :issue:`5139`: Eliminate core dependency on 'terminal' plugin.
+
+
+- :issue:`5229`: Require ``pluggy>=0.11.0`` which reverts a dependency to ``importlib-metadata`` added in ``0.10.0``.
+ The ``importlib-metadata`` package cannot be imported when installed as an egg and causes issues when relying on ``setup.py`` to install test dependencies.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`5171`: Doc: ``pytest_ignore_collect``, ``pytest_collect_directory``, ``pytest_collect_file`` and ``pytest_pycollect_makemodule`` hooks's 'path' parameter documented type is now ``py.path.local``
+
+
+- :issue:`5188`: Improve help for ``--runxfail`` flag.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`5182`: Removed internal and unused ``_pytest.deprecated.MARK_INFO_ATTRIBUTE``.
+
+
+pytest 4.4.1 (2019-04-15)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`5031`: Environment variables are properly restored when using pytester's ``testdir`` fixture.
+
+
+- :issue:`5039`: Fix regression with ``--pdbcls``, which stopped working with local modules in 4.0.0.
+
+
+- :issue:`5092`: Produce a warning when unknown keywords are passed to ``pytest.param(...)``.
+
+
+- :issue:`5098`: Invalidate import caches with ``monkeypatch.syspath_prepend``, which is required with namespace packages being used.
+
+
+pytest 4.4.0 (2019-03-29)
+=========================
+
+Features
+--------
+
+- :issue:`2224`: ``async`` test functions are skipped and a warning is emitted when a suitable
+ async plugin is not installed (such as ``pytest-asyncio`` or ``pytest-trio``).
+
+ Previously ``async`` functions would not execute at all but still be marked as "passed".
+
+
+- :issue:`2482`: Include new ``disable_test_id_escaping_and_forfeit_all_rights_to_community_support`` option to disable ascii-escaping in parametrized values. This may cause a series of problems and as the name makes clear, use at your own risk.
+
+
+- :issue:`4718`: The ``-p`` option can now be used to early-load plugins also by entry-point name, instead of just
+ by module name.
+
+ This makes it possible to early load external plugins like ``pytest-cov`` in the command-line::
+
+ pytest -p pytest_cov
+
+
+- :issue:`4855`: The ``--pdbcls`` option handles classes via module attributes now (e.g.
+ ``pdb:pdb.Pdb`` with :pypi:`pdbpp`), and its validation was improved.
+
+
+- :issue:`4875`: The :confval:`testpaths` configuration option is now displayed next
+ to the ``rootdir`` and ``inifile`` lines in the pytest header if the option is in effect, i.e., directories or file names were
+ not explicitly passed in the command line.
+
+ Also, ``inifile`` is only displayed if there's a configuration file, instead of an empty ``inifile:`` string.
+
+
+- :issue:`4911`: Doctests can be skipped now dynamically using ``pytest.skip()``.
+
+
+- :issue:`4920`: Internal refactorings have been made in order to make the implementation of the
+ `pytest-subtests <https://github.com/pytest-dev/pytest-subtests>`__ plugin
+ possible, which adds unittest sub-test support and a new ``subtests`` fixture as discussed in
+ :issue:`1367`.
+
+ For details on the internal refactorings, please see the details on the related PR.
+
+
+- :issue:`4931`: pytester's ``LineMatcher`` asserts that the passed lines are a sequence.
+
+
+- :issue:`4936`: Handle ``-p plug`` after ``-p no:plug``.
+
+ This can be used to override a blocked plugin (e.g. in "addopts") from the
+ command line etc.
+
+
+- :issue:`4951`: Output capturing is handled correctly when only capturing via fixtures (capsys, capfs) with ``pdb.set_trace()``.
+
+
+- :issue:`4956`: ``pytester`` sets ``$HOME`` and ``$USERPROFILE`` to the temporary directory during test runs.
+
+ This ensures to not load configuration files from the real user's home directory.
+
+
+- :issue:`4980`: Namespace packages are handled better with ``monkeypatch.syspath_prepend`` and ``testdir.syspathinsert`` (via ``pkg_resources.fixup_namespace_packages``).
+
+
+- :issue:`4993`: The stepwise plugin reports status information now.
+
+
+- :issue:`5008`: If a ``setup.cfg`` file contains ``[tool:pytest]`` and also the no longer supported ``[pytest]`` section, pytest will use ``[tool:pytest]`` ignoring ``[pytest]``. Previously it would unconditionally error out.
+
+ This makes it simpler for plugins to support old pytest versions.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`1895`: Fix bug where fixtures requested dynamically via ``request.getfixturevalue()`` might be teardown
+ before the requesting fixture.
+
+
+- :issue:`4851`: pytester unsets ``PYTEST_ADDOPTS`` now to not use outer options with ``testdir.runpytest()``.
+
+
+- :issue:`4903`: Use the correct modified time for years after 2038 in rewritten ``.pyc`` files.
+
+
+- :issue:`4928`: Fix line offsets with ``ScopeMismatch`` errors.
+
+
+- :issue:`4957`: ``-p no:plugin`` is handled correctly for default (internal) plugins now, e.g. with ``-p no:capture``.
+
+ Previously they were loaded (imported) always, making e.g. the ``capfd`` fixture available.
+
+
+- :issue:`4968`: The pdb ``quit`` command is handled properly when used after the ``debug`` command with :pypi:`pdbpp`.
+
+
+- :issue:`4975`: Fix the interpretation of ``-qq`` option where it was being considered as ``-v`` instead.
+
+
+- :issue:`4978`: ``outcomes.Exit`` is not swallowed in ``assertrepr_compare`` anymore.
+
+
+- :issue:`4988`: Close logging's file handler explicitly when the session finishes.
+
+
+- :issue:`5003`: Fix line offset with mark collection error (off by one).
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`4974`: Update docs for ``pytest_cmdline_parse`` hook to note availability liminations
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`4718`: ``pluggy>=0.9`` is now required.
+
+
+- :issue:`4815`: ``funcsigs>=1.0`` is now required for Python 2.7.
+
+
+- :issue:`4829`: Some left-over internal code related to ``yield`` tests has been removed.
+
+
+- :issue:`4890`: Remove internally unused ``anypython`` fixture from the pytester plugin.
+
+
+- :issue:`4912`: Remove deprecated Sphinx directive, ``add_description_unit()``,
+ pin sphinx-removed-in to >= 0.2.0 to support Sphinx 2.0.
+
+
+- :issue:`4913`: Fix pytest tests invocation with custom ``PYTHONPATH``.
+
+
+- :issue:`4965`: New ``pytest_report_to_serializable`` and ``pytest_report_from_serializable`` **experimental** hooks.
+
+ These hooks will be used by ``pytest-xdist``, ``pytest-subtests``, and the replacement for
+ resultlog to serialize and customize reports.
+
+ They are experimental, meaning that their details might change or even be removed
+ completely in future patch releases without warning.
+
+ Feedback is welcome from plugin authors and users alike.
+
+
+- :issue:`4987`: ``Collector.repr_failure`` respects the ``--tb`` option, but only defaults to ``short`` now (with ``auto``).
+
+
+pytest 4.3.1 (2019-03-11)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`4810`: Logging messages inside ``pytest_runtest_logreport()`` are now properly captured and displayed.
+
+
+- :issue:`4861`: Improve validation of contents written to captured output so it behaves the same as when capture is disabled.
+
+
+- :issue:`4898`: Fix ``AttributeError: FixtureRequest has no 'confg' attribute`` bug in ``testdir.copy_example``.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`4768`: Avoid pkg_resources import at the top-level.
+
+
+pytest 4.3.0 (2019-02-16)
+=========================
+
+Deprecations
+------------
+
+- :issue:`4724`: ``pytest.warns()`` now emits a warning when it receives unknown keyword arguments.
+
+ This will be changed into an error in the future.
+
+
+
+Features
+--------
+
+- :issue:`2753`: Usage errors from argparse are mapped to pytest's ``UsageError``.
+
+
+- :issue:`3711`: Add the ``--ignore-glob`` parameter to exclude test-modules with Unix shell-style wildcards.
+ Add the :globalvar:`collect_ignore_glob` for ``conftest.py`` to exclude test-modules with Unix shell-style wildcards.
+
+
+- :issue:`4698`: The warning about Python 2.7 and 3.4 not being supported in pytest 5.0 has been removed.
+
+ In the end it was considered to be more
+ of a nuisance than actual utility and users of those Python versions shouldn't have problems as ``pip`` will not
+ install pytest 5.0 on those interpreters.
+
+
+- :issue:`4707`: With the help of new ``set_log_path()`` method there is a way to set ``log_file`` paths from hooks.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`4651`: ``--help`` and ``--version`` are handled with ``UsageError``.
+
+
+- :issue:`4782`: Fix ``AssertionError`` with collection of broken symlinks with packages.
+
+
+pytest 4.2.1 (2019-02-12)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`2895`: The ``pytest_report_collectionfinish`` hook now is also called with ``--collect-only``.
+
+
+- :issue:`3899`: Do not raise ``UsageError`` when an imported package has a ``pytest_plugins.py`` child module.
+
+
+- :issue:`4347`: Fix output capturing when using pdb++ with recursive debugging.
+
+
+- :issue:`4592`: Fix handling of ``collect_ignore`` via parent ``conftest.py``.
+
+
+- :issue:`4700`: Fix regression where ``setUpClass`` would always be called in subclasses even if all tests
+ were skipped by a ``unittest.skip()`` decorator applied in the subclass.
+
+
+- :issue:`4739`: Fix ``parametrize(... ids=<function>)`` when the function returns non-strings.
+
+
+- :issue:`4745`: Fix/improve collection of args when passing in ``__init__.py`` and a test file.
+
+
+- :issue:`4770`: ``more_itertools`` is now constrained to <6.0.0 when required for Python 2.7 compatibility.
+
+
+- :issue:`526`: Fix "ValueError: Plugin already registered" exceptions when running in build directories that symlink to actual source.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`3899`: Add note to ``plugins.rst`` that ``pytest_plugins`` should not be used as a name for a user module containing plugins.
+
+
+- :issue:`4324`: Document how to use ``raises`` and ``does_not_raise`` to write parametrized tests with conditional raises.
+
+
+- :issue:`4709`: Document how to customize test failure messages when using
+ ``pytest.warns``.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`4741`: Some verbosity related attributes of the TerminalReporter plugin are now
+ read only properties.
+
+
+pytest 4.2.0 (2019-01-30)
+=========================
+
+Features
+--------
+
+- :issue:`3094`: :doc:`Classic xunit-style <how-to/xunit_setup>` functions and methods
+ now obey the scope of *autouse* fixtures.
+
+ This fixes a number of surprising issues like ``setup_method`` being called before session-scoped
+ autouse fixtures (see :issue:`517` for an example).
+
+
+- :issue:`4627`: Display a message at the end of the test session when running under Python 2.7 and 3.4 that pytest 5.0 will no longer
+ support those Python versions.
+
+
+- :issue:`4660`: The number of *selected* tests now are also displayed when the ``-k`` or ``-m`` flags are used.
+
+
+- :issue:`4688`: ``pytest_report_teststatus`` hook now can also receive a ``config`` parameter.
+
+
+- :issue:`4691`: ``pytest_terminal_summary`` hook now can also receive a ``config`` parameter.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`3547`: ``--junitxml`` can emit XML compatible with Jenkins xUnit.
+ ``junit_family`` INI option accepts ``legacy|xunit1``, which produces old style output, and ``xunit2`` that conforms more strictly to https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
+
+
+- :issue:`4280`: Improve quitting from pdb, especially with ``--trace``.
+
+ Using ``q[quit]`` after ``pdb.set_trace()`` will quit pytest also.
+
+
+- :issue:`4402`: Warning summary now groups warnings by message instead of by test id.
+
+ This makes the output more compact and better conveys the general idea of how much code is
+ actually generating warnings, instead of how many tests call that code.
+
+
+- :issue:`4536`: ``monkeypatch.delattr`` handles class descriptors like ``staticmethod``/``classmethod``.
+
+
+- :issue:`4649`: Restore marks being considered keywords for keyword expressions.
+
+
+- :issue:`4653`: ``tmp_path`` fixture and other related ones provides resolved path (a.k.a real path)
+
+
+- :issue:`4667`: ``pytest_terminal_summary`` uses result from ``pytest_report_teststatus`` hook, rather than hardcoded strings.
+
+
+- :issue:`4669`: Correctly handle ``unittest.SkipTest`` exception containing non-ascii characters on Python 2.
+
+
+- :issue:`4680`: Ensure the ``tmpdir`` and the ``tmp_path`` fixtures are the same folder.
+
+
+- :issue:`4681`: Ensure ``tmp_path`` is always a real path.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`4643`: Use ``a.item()`` instead of the deprecated ``np.asscalar(a)`` in ``pytest.approx``.
+
+ ``np.asscalar`` has been :doc:`deprecated <numpy:release/1.16.0-notes>` in ``numpy 1.16.``.
+
+
+- :issue:`4657`: Copy saferepr from pylib
+
+
+- :issue:`4668`: The verbose word for expected failures in the teststatus report changes from ``xfail`` to ``XFAIL`` to be consistent with other test outcomes.
+
+
+pytest 4.1.1 (2019-01-12)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`2256`: Show full repr with ``assert a==b`` and ``-vv``.
+
+
+- :issue:`3456`: Extend Doctest-modules to ignore mock objects.
+
+
+- :issue:`4617`: Fixed ``pytest.warns`` bug when context manager is reused (e.g. multiple parametrization).
+
+
+- :issue:`4631`: Don't rewrite assertion when ``__getattr__`` is broken
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`3375`: Document that using ``setup.cfg`` may crash other tools or cause hard to track down problems because it uses a different parser than ``pytest.ini`` or ``tox.ini`` files.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`4602`: Uninstall ``hypothesis`` in regen tox env.
+
+
+pytest 4.1.0 (2019-01-05)
+=========================
+
+Removals
+--------
+
+- :issue:`2169`: ``pytest.mark.parametrize``: in previous versions, errors raised by id functions were suppressed and changed into warnings. Now the exceptions are propagated, along with a pytest message informing the node, parameter value and index where the exception occurred.
+
+
+- :issue:`3078`: Remove legacy internal warnings system: ``config.warn``, ``Node.warn``. The ``pytest_logwarning`` now issues a warning when implemented.
+
+ See our :ref:`docs <config.warn and node.warn deprecated>` on information on how to update your code.
+
+
+- :issue:`3079`: Removed support for yield tests - they are fundamentally broken because they don't support fixtures properly since collection and test execution were separated.
+
+ See our :ref:`docs <yield tests deprecated>` on information on how to update your code.
+
+
+- :issue:`3082`: Removed support for applying marks directly to values in ``@pytest.mark.parametrize``. Use ``pytest.param`` instead.
+
+ See our :ref:`docs <marks in pytest.parametrize deprecated>` on information on how to update your code.
+
+
+- :issue:`3083`: Removed ``Metafunc.addcall``. This was the predecessor mechanism to ``@pytest.mark.parametrize``.
+
+ See our :ref:`docs <metafunc.addcall deprecated>` on information on how to update your code.
+
+
+- :issue:`3085`: Removed support for passing strings to ``pytest.main``. Now, always pass a list of strings instead.
+
+ See our :ref:`docs <passing command-line string to pytest.main deprecated>` on information on how to update your code.
+
+
+- :issue:`3086`: ``[pytest]`` section in **setup.cfg** files is no longer supported, use ``[tool:pytest]`` instead. ``setup.cfg`` files
+ are meant for use with ``distutils``, and a section named ``pytest`` has notoriously been a source of conflicts and bugs.
+
+ Note that for **pytest.ini** and **tox.ini** files the section remains ``[pytest]``.
+
+
+- :issue:`3616`: Removed the deprecated compat properties for ``node.Class/Function/Module`` - use ``pytest.Class/Function/Module`` now.
+
+ See our :ref:`docs <internal classes accessed through node deprecated>` on information on how to update your code.
+
+
+- :issue:`4421`: Removed the implementation of the ``pytest_namespace`` hook.
+
+ See our :ref:`docs <pytest.namespace deprecated>` on information on how to update your code.
+
+
+- :issue:`4489`: Removed ``request.cached_setup``. This was the predecessor mechanism to modern fixtures.
+
+ See our :ref:`docs <cached_setup deprecated>` on information on how to update your code.
+
+
+- :issue:`4535`: Removed the deprecated ``PyCollector.makeitem`` method. This method was made public by mistake a long time ago.
+
+
+- :issue:`4543`: Removed support to define fixtures using the ``pytest_funcarg__`` prefix. Use the ``@pytest.fixture`` decorator instead.
+
+ See our :ref:`docs <pytest_funcarg__ prefix deprecated>` on information on how to update your code.
+
+
+- :issue:`4545`: Calling fixtures directly is now always an error instead of a warning.
+
+ 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.
+
+ Use ``Node.get_closest_marker(name)`` as a replacement.
+
+
+- :issue:`4547`: The deprecated ``record_xml_property`` fixture has been removed, use the more generic ``record_property`` instead.
+
+ See our :ref:`docs <record_xml_property deprecated>` for more information.
+
+
+- :issue:`4548`: An error is now raised if the ``pytest_plugins`` variable is defined in a non-top-level ``conftest.py`` file (i.e., not residing in the ``rootdir``).
+
+ See our :ref:`docs <pytest_plugins in non-top-level conftest files deprecated>` for more information.
+
+
+- :issue:`891`: Remove ``testfunction.markername`` attributes - use ``Node.iter_markers(name=None)`` to iterate them.
+
+
+
+Deprecations
+------------
+
+- :issue:`3050`: Deprecated the ``pytest.config`` global.
+
+ See :ref:`pytest.config global deprecated` for rationale.
+
+
+- :issue:`3974`: Passing the ``message`` parameter of ``pytest.raises`` now issues a ``DeprecationWarning``.
+
+ It is a common mistake to think this parameter will match the exception message, while in fact
+ it only serves to provide a custom message in case the ``pytest.raises`` check fails. To avoid this
+ mistake and because it is believed to be little used, pytest is deprecating it without providing
+ an alternative for the moment.
+
+ If you have concerns about this, please comment on :issue:`3974`.
+
+
+- :issue:`4435`: Deprecated ``raises(..., 'code(as_a_string)')`` and ``warns(..., 'code(as_a_string)')``.
+
+ See :std:ref:`raises-warns-exec` for rationale and examples.
+
+
+
+Features
+--------
+
+- :issue:`3191`: A warning is now issued when assertions are made for ``None``.
+
+ This is a common source of confusion among new users, which write:
+
+ .. code-block:: python
+
+ assert mocked_object.assert_called_with(3, 4, 5, key="value")
+
+ When they should write:
+
+ .. code-block:: python
+
+ mocked_object.assert_called_with(3, 4, 5, key="value")
+
+ Because the ``assert_called_with`` method of mock objects already executes an assertion.
+
+ This warning will not be issued when ``None`` is explicitly checked. An assertion like:
+
+ .. code-block:: python
+
+ assert variable is None
+
+ will not issue the warning.
+
+
+- :issue:`3632`: Richer equality comparison introspection on ``AssertionError`` for objects created using `attrs <https://www.attrs.org/en/stable/>`__ or :mod:`dataclasses` (Python 3.7+, :pypi:`backported to 3.6 <dataclasses>`).
+
+
+- :issue:`4278`: ``CACHEDIR.TAG`` files are now created inside cache directories.
+
+ Those files are part of the `Cache Directory Tagging Standard <https://bford.info/cachedir/spec.html>`__, and can
+ be used by backup or synchronization programs to identify pytest's cache directory as such.
+
+
+- :issue:`4292`: ``pytest.outcomes.Exit`` is derived from ``SystemExit`` instead of ``KeyboardInterrupt``. This allows us to better handle ``pdb`` exiting.
+
+
+- :issue:`4371`: Updated the ``--collect-only`` option to display test descriptions when ran using ``--verbose``.
+
+
+- :issue:`4386`: Restructured ``ExceptionInfo`` object construction and ensure incomplete instances have a ``repr``/``str``.
+
+
+- :issue:`4416`: pdb: added support for keyword arguments with ``pdb.set_trace``.
+
+ It handles ``header`` similar to Python 3.7 does it, and forwards any
+ other keyword arguments to the ``Pdb`` constructor.
+
+ This allows for ``__import__("pdb").set_trace(skip=["foo.*"])``.
+
+
+- :issue:`4483`: Added ini parameter ``junit_duration_report`` to optionally report test call durations, excluding setup and teardown times.
+
+ The JUnit XML specification and the default pytest behavior is to include setup and teardown times in the test duration
+ report. You can include just the call durations instead (excluding setup and teardown) by adding this to your ``pytest.ini`` file:
+
+ .. code-block:: ini
+
+ [pytest]
+ junit_duration_report = call
+
+
+- :issue:`4532`: ``-ra`` now will show errors and failures last, instead of as the first items in the summary.
+
+ This makes it easier to obtain a list of errors and failures to run tests selectively.
+
+
+- :issue:`4599`: ``pytest.importorskip`` now supports a ``reason`` parameter, which will be shown when the
+ requested module cannot be imported.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`3532`: ``-p`` now accepts its argument without a space between the value, for example ``-pmyplugin``.
+
+
+- :issue:`4327`: ``approx`` again works with more generic containers, more precisely instances of ``Iterable`` and ``Sized`` instead of more restrictive ``Sequence``.
+
+
+- :issue:`4397`: Ensure that node ids are printable.
+
+
+- :issue:`4435`: Fixed ``raises(..., 'code(string)')`` frame filename.
+
+
+- :issue:`4458`: Display actual test ids in ``--collect-only``.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`4557`: Markers example documentation page updated to support latest pytest version.
+
+
+- :issue:`4558`: Update cache documentation example to correctly show cache hit and miss.
+
+
+- :issue:`4580`: Improved detailed summary report documentation.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`4447`: Changed the deprecation type of ``--result-log`` to ``PytestDeprecationWarning``.
+
+ It was decided to remove this feature at the next major revision.
+
+
+pytest 4.0.2 (2018-12-13)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`4265`: Validate arguments from the ``PYTEST_ADDOPTS`` environment variable and the ``addopts`` ini option separately.
+
+
+- :issue:`4435`: Fix ``raises(..., 'code(string)')`` frame filename.
+
+
+- :issue:`4500`: When a fixture yields and a log call is made after the test runs, and, if the test is interrupted, capture attributes are ``None``.
+
+
+- :issue:`4538`: Raise ``TypeError`` for ``with raises(..., match=<non-None falsey value>)``.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`1495`: Document common doctest fixture directory tree structure pitfalls
+
+
+pytest 4.0.1 (2018-11-23)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`3952`: Display warnings before "short test summary info" again, but still later warnings in the end.
+
+
+- :issue:`4386`: Handle uninitialized exceptioninfo in repr/str.
+
+
+- :issue:`4393`: Do not create ``.gitignore``/``README.md`` files in existing cache directories.
+
+
+- :issue:`4400`: Rearrange warning handling for the yield test errors so the opt-out in 4.0.x correctly works.
+
+
+- :issue:`4405`: Fix collection of testpaths with ``--pyargs``.
+
+
+- :issue:`4412`: Fix assertion rewriting involving ``Starred`` + side-effects.
+
+
+- :issue:`4425`: Ensure we resolve the absolute path when the given ``--basetemp`` is a relative path.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`4315`: Use ``pkg_resources.parse_version`` instead of ``LooseVersion`` in minversion check.
+
+
+- :issue:`4440`: Adjust the stack level of some internal pytest warnings.
+
+
+pytest 4.0.0 (2018-11-13)
+=========================
+
+Removals
+--------
+
+- :issue:`3737`: **RemovedInPytest4Warnings are now errors by default.**
+
+ Following our plan to remove deprecated features with as little disruption as
+ possible, all warnings of type ``RemovedInPytest4Warnings`` now generate errors
+ instead of warning messages.
+
+ **The affected features will be effectively removed in pytest 4.1**, so please consult the
+ :std:doc:`deprecations` section in the docs for directions on how to update existing code.
+
+ In the pytest ``4.0.X`` series, it is possible to change the errors back into warnings as a stop
+ gap measure by adding this to your ``pytest.ini`` file:
+
+ .. code-block:: ini
+
+ [pytest]
+ filterwarnings =
+ ignore::pytest.RemovedInPytest4Warning
+
+ But this will stop working when pytest ``4.1`` is released.
+
+ **If you have concerns** about the removal of a specific feature, please add a
+ comment to :issue:`4348`.
+
+
+- :issue:`4358`: Remove the ``::()`` notation to denote a test class instance in node ids.
+
+ Previously, node ids that contain test instances would use ``::()`` to denote the instance like this::
+
+ test_foo.py::Test::()::test_bar
+
+ The extra ``::()`` was puzzling to most users and has been removed, so that the test id becomes now::
+
+ test_foo.py::Test::test_bar
+
+ This change could not accompany a deprecation period as is usual when user-facing functionality changes because
+ it was not really possible to detect when the functionality was being used explicitly.
+
+ The extra ``::()`` might have been removed in some places internally already,
+ which then led to confusion in places where it was expected, e.g. with
+ ``--deselect`` (:issue:`4127`).
+
+ Test class instances are also not listed with ``--collect-only`` anymore.
+
+
+
+Features
+--------
+
+- :issue:`4270`: The ``cache_dir`` option uses ``$TOX_ENV_DIR`` as prefix (if set in the environment).
+
+ This uses a different cache per tox environment by default.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`3554`: Fix ``CallInfo.__repr__`` for when the call is not finished yet.
+
+
+pytest 3.10.1 (2018-11-11)
+==========================
+
+Bug Fixes
+---------
+
+- :issue:`4287`: Fix nested usage of debugging plugin (pdb), e.g. with pytester's ``testdir.runpytest``.
+
+
+- :issue:`4304`: Block the ``stepwise`` plugin if ``cacheprovider`` is also blocked, as one depends on the other.
+
+
+- :issue:`4306`: Parse ``minversion`` as an actual version and not as dot-separated strings.
+
+
+- :issue:`4310`: Fix duplicate collection due to multiple args matching the same packages.
+
+
+- :issue:`4321`: Fix ``item.nodeid`` with resolved symlinks.
+
+
+- :issue:`4325`: Fix collection of direct symlinked files, where the target does not match ``python_files``.
+
+
+- :issue:`4329`: Fix TypeError in report_collect with _collect_report_last_write.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`4305`: Replace byte/unicode helpers in test_capture with python level syntax.
+
+
+pytest 3.10.0 (2018-11-03)
+==========================
+
+Features
+--------
+
+- :issue:`2619`: Resume capturing output after ``continue`` with ``__import__("pdb").set_trace()``.
+
+ This also adds a new ``pytest_leave_pdb`` hook, and passes in ``pdb`` to the
+ existing ``pytest_enter_pdb`` hook.
+
+
+- :issue:`4147`: Add ``--sw``, ``--stepwise`` as an alternative to ``--lf -x`` for stopping at the first failure, but starting the next test invocation from that test. See :ref:`the documentation <cache stepwise>` for more info.
+
+
+- :issue:`4188`: Make ``--color`` emit colorful dots when not running in verbose mode. Earlier, it would only colorize the test-by-test output if ``--verbose`` was also passed.
+
+
+- :issue:`4225`: Improve performance with collection reporting in non-quiet mode with terminals.
+
+ The "collecting …" message is only printed/updated every 0.5s.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`2701`: Fix false ``RemovedInPytest4Warning: usage of Session... is deprecated, please use pytest`` warnings.
+
+
+- :issue:`4046`: Fix problems with running tests in package ``__init__.py`` files.
+
+
+- :issue:`4260`: Swallow warnings during anonymous compilation of source.
+
+
+- :issue:`4262`: Fix access denied error when deleting stale directories created by ``tmpdir`` / ``tmp_path``.
+
+
+- :issue:`611`: Naming a fixture ``request`` will now raise a warning: the ``request`` fixture is internal and
+ should not be overwritten as it will lead to internal errors.
+
+- :issue:`4266`: Handle (ignore) exceptions raised during collection, e.g. with Django's LazySettings proxy class.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`4255`: Added missing documentation about the fact that module names passed to filter warnings are not regex-escaped.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`4272`: Display cachedir also in non-verbose mode if non-default.
+
+
+- :issue:`4277`: pdb: improve message about output capturing with ``set_trace``.
+
+ Do not display "IO-capturing turned off/on" when ``-s`` is used to avoid
+ confusion.
+
+
+- :issue:`4279`: Improve message and stack level of warnings issued by ``monkeypatch.setenv`` when the value of the environment variable is not a ``str``.
+
+
+pytest 3.9.3 (2018-10-27)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`4174`: Fix "ValueError: Plugin already registered" with conftest plugins via symlink.
+
+
+- :issue:`4181`: Handle race condition between creation and deletion of temporary folders.
+
+
+- :issue:`4221`: Fix bug where the warning summary at the end of the test session was not showing the test where the warning was originated.
+
+
+- :issue:`4243`: Fix regression when ``stacklevel`` for warnings was passed as positional argument on python2.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`3851`: Add reference to ``empty_parameter_set_mark`` ini option in documentation of ``@pytest.mark.parametrize``
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`4028`: Revert patching of ``sys.breakpointhook`` since it appears to do nothing.
+
+
+- :issue:`4233`: Apply an import sorter (``reorder-python-imports``) to the codebase.
+
+
+- :issue:`4248`: Remove use of unnecessary compat shim, six.binary_type
+
+
+pytest 3.9.2 (2018-10-22)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`2909`: Improve error message when a recursive dependency between fixtures is detected.
+
+
+- :issue:`3340`: Fix logging messages not shown in hooks ``pytest_sessionstart()`` and ``pytest_sessionfinish()``.
+
+
+- :issue:`3533`: Fix unescaped XML raw objects in JUnit report for skipped tests
+
+
+- :issue:`3691`: Python 2: safely format warning message about passing unicode strings to ``warnings.warn``, which may cause
+ surprising ``MemoryError`` exception when monkey patching ``warnings.warn`` itself.
+
+
+- :issue:`4026`: Improve error message when it is not possible to determine a function's signature.
+
+
+- :issue:`4177`: Pin ``setuptools>=40.0`` to support ``py_modules`` in ``setup.cfg``
+
+
+- :issue:`4179`: Restore the tmpdir behaviour of symlinking the current test run.
+
+
+- :issue:`4192`: Fix filename reported by ``warnings.warn`` when using ``recwarn`` under python2.
+
+
+pytest 3.9.1 (2018-10-16)
+=========================
+
+Features
+--------
+
+- :issue:`4159`: For test-suites containing test classes, the information about the subclassed
+ module is now output only if a higher verbosity level is specified (at least
+ "-vv").
+
+
+pytest 3.9.0 (2018-10-15 - not published due to a release automation bug)
+=========================================================================
+
+Deprecations
+------------
+
+- :issue:`3616`: The following accesses have been documented as deprecated for years, but are now actually emitting deprecation warnings.
+
+ * Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances. Now
+ users will this warning::
+
+ usage of Function.Module is deprecated, please use pytest.Module instead
+
+ Users should just ``import pytest`` and access those objects using the ``pytest`` module.
+
+ * ``request.cached_setup``, this was the precursor of the setup/teardown mechanism available to fixtures. You can
+ consult :std:doc:`funcarg comparison section in the docs <funcarg_compare>`.
+
+ * Using objects named ``"Class"`` as a way to customize the type of nodes that are collected in ``Collector``
+ subclasses has been deprecated. Users instead should use ``pytest_collect_make_item`` to customize node types during
+ collection.
+
+ This issue should affect only advanced plugins who create new collection types, so if you see this warning
+ message please contact the authors so they can change the code.
+
+ * The warning that produces the message below has changed to ``RemovedInPytest4Warning``::
+
+ getfuncargvalue is deprecated, use getfixturevalue
+
+
+- :issue:`3988`: Add a Deprecation warning for pytest.ensuretemp as it was deprecated since a while.
+
+
+
+Features
+--------
+
+- :issue:`2293`: Improve usage errors messages by hiding internal details which can be distracting and noisy.
+
+ This has the side effect that some error conditions that previously raised generic errors (such as
+ ``ValueError`` for unregistered marks) are now raising ``Failed`` exceptions.
+
+
+- :issue:`3332`: Improve the error displayed when a ``conftest.py`` file could not be imported.
+
+ In order to implement this, a new ``chain`` parameter was added to ``ExceptionInfo.getrepr``
+ to show or hide chained tracebacks in Python 3 (defaults to ``True``).
+
+
+- :issue:`3849`: Add ``empty_parameter_set_mark=fail_at_collect`` ini option for raising an exception when parametrize collects an empty set.
+
+
+- :issue:`3964`: Log messages generated in the collection phase are shown when
+ live-logging is enabled and/or when they are logged to a file.
+
+
+- :issue:`3985`: Introduce ``tmp_path`` as a fixture providing a Path object. Also introduce ``tmp_path_factory`` as
+ a session-scoped fixture for creating arbitrary temporary directories from any other fixture or test.
+
+
+- :issue:`4013`: Deprecation warnings are now shown even if you customize the warnings filters yourself. In the previous version
+ any customization would override pytest's filters and deprecation warnings would fall back to being hidden by default.
+
+
+- :issue:`4073`: Allow specification of timeout for ``Testdir.runpytest_subprocess()`` and ``Testdir.run()``.
+
+
+- :issue:`4098`: Add returncode argument to pytest.exit() to exit pytest with a specific return code.
+
+
+- :issue:`4102`: Reimplement ``pytest.deprecated_call`` using ``pytest.warns`` so it supports the ``match='...'`` keyword argument.
+
+ This has the side effect that ``pytest.deprecated_call`` now raises ``pytest.fail.Exception`` instead
+ of ``AssertionError``.
+
+
+- :issue:`4149`: Require setuptools>=30.3 and move most of the metadata to ``setup.cfg``.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`2535`: Improve error message when test functions of ``unittest.TestCase`` subclasses use a parametrized fixture.
+
+
+- :issue:`3057`: ``request.fixturenames`` now correctly returns the name of fixtures created by ``request.getfixturevalue()``.
+
+
+- :issue:`3946`: Warning filters passed as command line options using ``-W`` now take precedence over filters defined in ``ini``
+ configuration files.
+
+
+- :issue:`4066`: Fix source reindenting by using ``textwrap.dedent`` directly.
+
+
+- :issue:`4102`: ``pytest.warn`` will capture previously-warned warnings in Python 2. Previously they were never raised.
+
+
+- :issue:`4108`: Resolve symbolic links for args.
+
+ This fixes running ``pytest tests/test_foo.py::test_bar``, where ``tests``
+ is a symlink to ``project/app/tests``:
+ previously ``project/app/conftest.py`` would be ignored for fixtures then.
+
+
+- :issue:`4132`: Fix duplicate printing of internal errors when using ``--pdb``.
+
+
+- :issue:`4135`: pathlib based tmpdir cleanup now correctly handles symlinks in the folder.
+
+
+- :issue:`4152`: Display the filename when encountering ``SyntaxWarning``.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`3713`: Update usefixtures documentation to clarify that it can't be used with fixture functions.
+
+
+- :issue:`4058`: Update fixture documentation to specify that a fixture can be invoked twice in the scope it's defined for.
+
+
+- :issue:`4064`: According to unittest.rst, setUpModule and tearDownModule were not implemented, but it turns out they are. So updated the documentation for unittest.
+
+
+- :issue:`4151`: Add tempir testing example to CONTRIBUTING.rst guide
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`2293`: The internal ``MarkerError`` exception has been removed.
+
+
+- :issue:`3988`: Port the implementation of tmpdir to pathlib.
+
+
+- :issue:`4063`: Exclude 0.00 second entries from ``--duration`` output unless ``-vv`` is passed on the command-line.
+
+
+- :issue:`4093`: Fixed formatting of string literals in internal tests.
+
+
+pytest 3.8.2 (2018-10-02)
+=========================
+
+Deprecations and Removals
+-------------------------
+
+- :issue:`4036`: The ``item`` parameter of ``pytest_warning_captured`` hook is now documented as deprecated. We realized only after
+ the ``3.8`` release that this parameter is incompatible with ``pytest-xdist``.
+
+ Our policy is to not deprecate features during bug-fix releases, but in this case we believe it makes sense as we are
+ only documenting it as deprecated, without issuing warnings which might potentially break test suites. This will get
+ the word out that hook implementers should not use this parameter at all.
+
+ In a future release ``item`` will always be ``None`` and will emit a proper warning when a hook implementation
+ makes use of it.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`3539`: Fix reload on assertion rewritten modules.
+
+
+- :issue:`4034`: The ``.user_properties`` attribute of ``TestReport`` objects is a list
+ of (name, value) tuples, but could sometimes be instantiated as a tuple
+ of tuples. It is now always a list.
+
+
+- :issue:`4039`: No longer issue warnings about using ``pytest_plugins`` in non-top-level directories when using ``--pyargs``: the
+ current ``--pyargs`` mechanism is not reliable and might give false negatives.
+
+
+- :issue:`4040`: Exclude empty reports for passed tests when ``-rP`` option is used.
+
+
+- :issue:`4051`: Improve error message when an invalid Python expression is passed to the ``-m`` option.
+
+
+- :issue:`4056`: ``MonkeyPatch.setenv`` and ``MonkeyPatch.delenv`` issue a warning if the environment variable name is not ``str`` on Python 2.
+
+ In Python 2, adding ``unicode`` keys to ``os.environ`` causes problems with ``subprocess`` (and possible other modules),
+ making this a subtle bug specially susceptible when used with ``from __future__ import unicode_literals``.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`3928`: Add possible values for fixture scope to docs.
+
+
+pytest 3.8.1 (2018-09-22)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`3286`: ``.pytest_cache`` directory is now automatically ignored by Git. Users who would like to contribute a solution for other SCMs please consult/comment on this issue.
+
+
+- :issue:`3749`: Fix the following error during collection of tests inside packages::
+
+ TypeError: object of type 'Package' has no len()
+
+
+- :issue:`3941`: Fix bug where indirect parametrization would consider the scope of all fixtures used by the test function to determine the parametrization scope, and not only the scope of the fixtures being parametrized.
+
+
+- :issue:`3973`: Fix crash of the assertion rewriter if a test changed the current working directory without restoring it afterwards.
+
+
+- :issue:`3998`: Fix issue that prevented some caplog properties (for example ``record_tuples``) from being available when entering the debugger with ``--pdb``.
+
+
+- :issue:`3999`: Fix ``UnicodeDecodeError`` in python2.x when a class returns a non-ascii binary ``__repr__`` in an assertion which also contains non-ascii text.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`3996`: New :std:doc:`deprecations` page shows all currently
+ deprecated features, the rationale to do so, and alternatives to update your code. It also list features removed
+ from pytest in past major releases to help those with ancient pytest versions to upgrade.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`3955`: Improve pre-commit detection for changelog filenames
+
+
+- :issue:`3975`: Remove legacy code around im_func as that was python2 only
+
+
+pytest 3.8.0 (2018-09-05)
+=========================
+
+Deprecations and Removals
+-------------------------
+
+- :issue:`2452`: ``Config.warn`` and ``Node.warn`` have been
+ deprecated, see :ref:`config.warn and node.warn deprecated` for rationale and
+ examples.
+
+- :issue:`3936`: ``@pytest.mark.filterwarnings`` second parameter is no longer regex-escaped,
+ making it possible to actually use regular expressions to check the warning message.
+
+ **Note**: regex-escaping the match string was an implementation oversight that might break test suites which depend
+ on the old behavior.
+
+
+
+Features
+--------
+
+- :issue:`2452`: Internal pytest warnings are now issued using the standard ``warnings`` module, making it possible to use
+ the standard warnings filters to manage those warnings. This introduces ``PytestWarning``,
+ ``PytestDeprecationWarning`` and ``RemovedInPytest4Warning`` warning types as part of the public API.
+
+ Consult :ref:`the documentation <internal-warnings>` for more info.
+
+
+- :issue:`2908`: ``DeprecationWarning`` and ``PendingDeprecationWarning`` are now shown by default if no other warning filter is
+ configured. This makes pytest more compliant with
+ :pep:`506#recommended-filter-settings-for-test-runners`. See
+ :ref:`the docs <deprecation-warnings>` for
+ more info.
+
+
+- :issue:`3251`: Warnings are now captured and displayed during test collection.
+
+
+- :issue:`3784`: ``PYTEST_DISABLE_PLUGIN_AUTOLOAD`` environment variable disables plugin auto-loading when set.
+
+
+- :issue:`3829`: Added the ``count`` option to ``console_output_style`` to enable displaying the progress as a count instead of a percentage.
+
+
+- :issue:`3837`: Added support for 'xfailed' and 'xpassed' outcomes to the ``pytester.RunResult.assert_outcomes`` signature.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`3911`: Terminal writer now takes into account unicode character width when writing out progress.
+
+
+- :issue:`3913`: Pytest now returns with correct exit code (EXIT_USAGEERROR, 4) when called with unknown arguments.
+
+
+- :issue:`3918`: Improve performance of assertion rewriting.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`3566`: Added a blurb in usage.rst for the usage of -r flag which is used to show an extra test summary info.
+
+
+- :issue:`3907`: Corrected type of the exceptions collection passed to ``xfail``: ``raises`` argument accepts a ``tuple`` instead of ``list``.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`3853`: Removed ``"run all (no recorded failures)"`` message printed with ``--failed-first`` and ``--last-failed`` when there are no failed tests.
+
+
+pytest 3.7.4 (2018-08-29)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`3506`: Fix possible infinite recursion when writing ``.pyc`` files.
+
+
+- :issue:`3853`: Cache plugin now obeys the ``-q`` flag when ``--last-failed`` and ``--failed-first`` flags are used.
+
+
+- :issue:`3883`: Fix bad console output when using ``console_output_style=classic``.
+
+
+- :issue:`3888`: Fix macOS specific code using ``capturemanager`` plugin in doctests.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`3902`: Fix pytest.org links
+
+
+pytest 3.7.3 (2018-08-26)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`3033`: Fixtures during teardown can again use ``capsys`` and ``capfd`` to inspect output captured during tests.
+
+
+- :issue:`3773`: Fix collection of tests from ``__init__.py`` files if they match the ``python_files`` configuration option.
+
+
+- :issue:`3796`: Fix issue where teardown of fixtures of consecutive sub-packages were executed once, at the end of the outer
+ package.
+
+
+- :issue:`3816`: Fix bug where ``--show-capture=no`` option would still show logs printed during fixture teardown.
+
+
+- :issue:`3819`: Fix ``stdout/stderr`` not getting captured when real-time cli logging is active.
+
+
+- :issue:`3843`: Fix collection error when specifying test functions directly in the command line using ``test.py::test`` syntax together with ``--doctest-modules``.
+
+
+- :issue:`3848`: Fix bugs where unicode arguments could not be passed to ``testdir.runpytest`` on Python 2.
+
+
+- :issue:`3854`: Fix double collection of tests within packages when the filename starts with a capital letter.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`3824`: Added example for multiple glob pattern matches in ``python_files``.
+
+
+- :issue:`3833`: Added missing docs for ``pytester.Testdir``.
+
+
+- :issue:`3870`: Correct documentation for setuptools integration.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`3826`: Replace broken type annotations with type comments.
+
+
+- :issue:`3845`: Remove a reference to issue :issue:`568` from the documentation, which has since been
+ fixed.
+
+
+pytest 3.7.2 (2018-08-16)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`3671`: Fix ``filterwarnings`` not being registered as a builtin mark.
+
+
+- :issue:`3768`, :issue:`3789`: Fix test collection from packages mixed with normal directories.
+
+
+- :issue:`3771`: Fix infinite recursion during collection if a ``pytest_ignore_collect`` hook returns ``False`` instead of ``None``.
+
+
+- :issue:`3774`: Fix bug where decorated fixtures would lose functionality (for example ``@mock.patch``).
+
+
+- :issue:`3775`: Fix bug where importing modules or other objects with prefix ``pytest_`` prefix would raise a ``PluginValidationError``.
+
+
+- :issue:`3788`: Fix ``AttributeError`` during teardown of ``TestCase`` subclasses which raise an exception during ``__init__``.
+
+
+- :issue:`3804`: Fix traceback reporting for exceptions with ``__cause__`` cycles.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`3746`: Add documentation for ``metafunc.config`` that had been mistakenly hidden.
+
+
+pytest 3.7.1 (2018-08-02)
+=========================
+
+Bug Fixes
+---------
+
+- :issue:`3473`: Raise immediately if ``approx()`` is given an expected value of a type it doesn't understand (e.g. strings, nested dicts, etc.).
+
+
+- :issue:`3712`: Correctly represent the dimensions of a numpy array when calling ``repr()`` on ``approx()``.
+
+- :issue:`3742`: Fix incompatibility with third party plugins during collection, which produced the error ``object has no attribute '_collectfile'``.
+
+- :issue:`3745`: Display the absolute path if ``cache_dir`` is not relative to the ``rootdir`` instead of failing.
+
+
+- :issue:`3747`: Fix compatibility problem with plugins and the warning code issued by fixture functions when they are called directly.
+
+
+- :issue:`3748`: Fix infinite recursion in ``pytest.approx`` with arrays in ``numpy<1.13``.
+
+
+- :issue:`3757`: Pin pathlib2 to ``>=2.2.0`` as we require ``__fspath__`` support.
+
+
+- :issue:`3763`: Fix ``TypeError`` when the assertion message is ``bytes`` in python 3.
+
+
+pytest 3.7.0 (2018-07-30)
+=========================
+
+Deprecations and Removals
+-------------------------
+
+- :issue:`2639`: ``pytest_namespace`` has been :ref:`deprecated <pytest.namespace deprecated>`.
+
+
+- :issue:`3661`: Calling a fixture function directly, as opposed to request them in a test function, now issues a ``RemovedInPytest4Warning``. See :ref:`the documentation for rationale and examples <calling fixtures directly deprecated>`.
+
+
+
+Features
+--------
+
+- :issue:`2283`: New ``package`` fixture scope: fixtures are finalized when the last test of a *package* finishes. This feature is considered **experimental**, so use it sparingly.
+
+
+- :issue:`3576`: ``Node.add_marker`` now supports an ``append=True/False`` parameter to determine whether the mark comes last (default) or first.
+
+
+- :issue:`3579`: Fixture ``caplog`` now has a ``messages`` property, providing convenient access to the format-interpolated log messages without the extra data provided by the formatter/handler.
+
+
+- :issue:`3610`: New ``--trace`` option to enter the debugger at the start of a test.
+
+
+- :issue:`3623`: Introduce ``pytester.copy_example`` as helper to do acceptance tests against examples from the project.
+
+
+
+Bug Fixes
+---------
+
+- :issue:`2220`: Fix a bug where fixtures overridden by direct parameters (for example parametrization) were being instantiated even if they were not being used by a test.
+
+
+- :issue:`3695`: Fix ``ApproxNumpy`` initialisation argument mixup, ``abs`` and ``rel`` tolerances were flipped causing strange comparison results.
+ Add tests to check ``abs`` and ``rel`` tolerances for ``np.array`` and test for expecting ``nan`` with ``np.array()``
+
+
+- :issue:`980`: Fix truncated locals output in verbose mode.
+
+
+
+Improved Documentation
+----------------------
+
+- :issue:`3295`: Correct the usage documentation of ``--last-failed-no-failures`` by adding the missing ``--last-failed`` argument in the presented examples, because they are misleading and lead to think that the missing argument is not needed.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- :issue:`3519`: Now a ``README.md`` file is created in ``.pytest_cache`` to make it clear why the directory exists.
+
+
+pytest 3.6.4 (2018-07-28)
+=========================
+
+Bug Fixes
+---------
+
+- Invoke pytest using ``-mpytest`` so ``sys.path`` does not get polluted by packages installed in ``site-packages``. (:issue:`742`)
+
+
+Improved Documentation
+----------------------
+
+- Use ``smtp_connection`` instead of ``smtp`` in fixtures documentation to avoid possible confusion. (:issue:`3592`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- Remove obsolete ``__future__`` imports. (:issue:`2319`)
+
+- Add CITATION to provide information on how to formally cite pytest. (:issue:`3402`)
+
+- Replace broken type annotations with type comments. (:issue:`3635`)
+
+- Pin ``pluggy`` to ``<0.8``. (:issue:`3727`)
+
+
+pytest 3.6.3 (2018-07-04)
+=========================
+
+Bug Fixes
+---------
+
+- Fix ``ImportWarning`` triggered by explicit relative imports in
+ assertion-rewritten package modules. (:issue:`3061`)
+
+- Fix error in ``pytest.approx`` when dealing with 0-dimension numpy
+ arrays. (:issue:`3593`)
+
+- No longer raise ``ValueError`` when using the ``get_marker`` API. (:issue:`3605`)
+
+- Fix problem where log messages with non-ascii characters would not
+ appear in the output log file.
+ (:issue:`3630`)
+
+- No longer raise ``AttributeError`` when legacy marks can't be stored in
+ functions. (:issue:`3631`)
+
+
+Improved Documentation
+----------------------
+
+- The description above the example for ``@pytest.mark.skipif`` now better
+ matches the code. (:issue:`3611`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- Internal refactoring: removed unused ``CallSpec2tox ._globalid_args``
+ attribute and ``metafunc`` parameter from ``CallSpec2.copy()``. (:issue:`3598`)
+
+- Silence usage of ``reduce`` warning in Python 2 (:issue:`3609`)
+
+- Fix usage of ``attr.ib`` deprecated ``convert`` parameter. (:issue:`3653`)
+
+
+pytest 3.6.2 (2018-06-20)
+=========================
+
+Bug Fixes
+---------
+
+- Fix regression in ``Node.add_marker`` by extracting the mark object of a
+ ``MarkDecorator``. (:issue:`3555`)
+
+- Warnings without ``location`` were reported as ``None``. This is corrected to
+ now report ``<undetermined location>``. (:issue:`3563`)
+
+- Continue to call finalizers in the stack when a finalizer in a former scope
+ raises an exception. (:issue:`3569`)
+
+- Fix encoding error with ``print`` statements in doctests (:issue:`3583`)
+
+
+Improved Documentation
+----------------------
+
+- Add documentation for the ``--strict`` flag. (:issue:`3549`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- Update old quotation style to parens in fixture.rst documentation. (:issue:`3525`)
+
+- Improve display of hint about ``--fulltrace`` with ``KeyboardInterrupt``.
+ (:issue:`3545`)
+
+- pytest's testsuite is no longer runnable through ``python setup.py test`` --
+ instead invoke ``pytest`` or ``tox`` directly. (:issue:`3552`)
+
+- Fix typo in documentation (:issue:`3567`)
+
+
+pytest 3.6.1 (2018-06-05)
+=========================
+
+Bug Fixes
+---------
+
+- Fixed a bug where stdout and stderr were logged twice by junitxml when a test
+ was marked xfail. (:issue:`3491`)
+
+- Fix ``usefixtures`` mark applied to unittest tests by correctly instantiating
+ ``FixtureInfo``. (:issue:`3498`)
+
+- Fix assertion rewriter compatibility with libraries that monkey patch
+ ``file`` objects. (:issue:`3503`)
+
+
+Improved Documentation
+----------------------
+
+- Added a section on how to use fixtures as factories to the fixture
+ documentation. (:issue:`3461`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- Enable caching for pip/pre-commit in order to reduce build time on
+ travis/appveyor. (:issue:`3502`)
+
+- Switch pytest to the src/ layout as we already suggested it for good practice
+ - now we implement it as well. (:issue:`3513`)
+
+- Fix if in tests to support 3.7.0b5, where a docstring handling in AST got
+ reverted. (:issue:`3530`)
+
+- Remove some python2.5 compatibility code. (:issue:`3529`)
+
+
+pytest 3.6.0 (2018-05-23)
+=========================
+
+Features
+--------
+
+- Revamp the internals of the ``pytest.mark`` implementation with correct per
+ node handling which fixes a number of long standing bugs caused by the old
+ design. This introduces new ``Node.iter_markers(name)`` and
+ ``Node.get_closest_marker(name)`` APIs. Users are **strongly encouraged** to
+ read the :ref:`reasons for the revamp in the docs <marker-revamp>`,
+ or jump over to details about :ref:`updating existing code to use the new APIs
+ <update marker code>`.
+ (:issue:`3317`)
+
+- Now when ``@pytest.fixture`` is applied more than once to the same function a
+ ``ValueError`` is raised. This buggy behavior would cause surprising problems
+ and if was working for a test suite it was mostly by accident. (:issue:`2334`)
+
+- Support for Python 3.7's builtin ``breakpoint()`` method, see
+ :ref:`Using the builtin breakpoint function <breakpoint-builtin>` for
+ details. (:issue:`3180`)
+
+- ``monkeypatch`` now supports a ``context()`` function which acts as a context
+ manager which undoes all patching done within the ``with`` block. (:issue:`3290`)
+
+- The ``--pdb`` option now causes KeyboardInterrupt to enter the debugger,
+ instead of stopping the test session. On python 2.7, hitting CTRL+C again
+ exits the debugger. On python 3.2 and higher, use CTRL+D. (:issue:`3299`)
+
+- pytest no longer changes the log level of the root logger when the
+ ``log-level`` parameter has greater numeric value than that of the level of
+ the root logger, which makes it play better with custom logging configuration
+ in user code. (:issue:`3307`)
+
+
+Bug Fixes
+---------
+
+- A rare race-condition which might result in corrupted ``.pyc`` files on
+ Windows has been hopefully solved. (:issue:`3008`)
+
+- Also use iter_marker for discovering the marks applying for marker
+ expressions from the cli to avoid the bad data from the legacy mark storage.
+ (:issue:`3441`)
+
+- When showing diffs of failed assertions where the contents contain only
+ whitespace, escape them using ``repr()`` first to make it easy to spot the
+ differences. (:issue:`3443`)
+
+
+Improved Documentation
+----------------------
+
+- Change documentation copyright year to a range which auto-updates itself each
+ time it is published. (:issue:`3303`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- ``pytest`` now depends on the `python-atomicwrites
+ <https://github.com/untitaker/python-atomicwrites>`_ library. (:issue:`3008`)
+
+- Update all pypi.python.org URLs to pypi.org. (:issue:`3431`)
+
+- Detect `pytest_` prefixed hooks using the internal plugin manager since
+ ``pluggy`` is deprecating the ``implprefix`` argument to ``PluginManager``.
+ (:issue:`3487`)
+
+- Import ``Mapping`` and ``Sequence`` from ``_pytest.compat`` instead of
+ directly from ``collections`` in ``python_api.py::approx``. Add ``Mapping``
+ to ``_pytest.compat``, import it from ``collections`` on python 2, but from
+ ``collections.abc`` on Python 3 to avoid a ``DeprecationWarning`` on Python
+ 3.7 or newer. (:issue:`3497`)
+
+
+pytest 3.5.1 (2018-04-23)
+=========================
+
+
+Bug Fixes
+---------
+
+- Reset ``sys.last_type``, ``sys.last_value`` and ``sys.last_traceback`` before
+ each test executes. Those attributes are added by pytest during the test run
+ to aid debugging, but were never reset so they would create a leaking
+ reference to the last failing test's frame which in turn could never be
+ reclaimed by the garbage collector. (:issue:`2798`)
+
+- ``pytest.raises`` now raises ``TypeError`` when receiving an unknown keyword
+ argument. (:issue:`3348`)
+
+- ``pytest.raises`` now works with exception classes that look like iterables.
+ (:issue:`3372`)
+
+
+Improved Documentation
+----------------------
+
+- Fix typo in ``caplog`` fixture documentation, which incorrectly identified
+ certain attributes as methods. (:issue:`3406`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- Added a more indicative error message when parametrizing a function whose
+ argument takes a default value. (:issue:`3221`)
+
+- Remove internal ``_pytest.terminal.flatten`` function in favor of
+ ``more_itertools.collapse``. (:issue:`3330`)
+
+- Import some modules from ``collections.abc`` instead of ``collections`` as
+ the former modules trigger ``DeprecationWarning`` in Python 3.7. (:issue:`3339`)
+
+- record_property is no longer experimental, removing the warnings was
+ forgotten. (:issue:`3360`)
+
+- Mention in documentation and CLI help that fixtures with leading ``_`` are
+ printed by ``pytest --fixtures`` only if the ``-v`` option is added. (:issue:`3398`)
+
+
+pytest 3.5.0 (2018-03-21)
+=========================
+
+Deprecations and Removals
+-------------------------
+
+- ``record_xml_property`` fixture is now deprecated in favor of the more
+ generic ``record_property``. (:issue:`2770`)
+
+- Defining ``pytest_plugins`` is now deprecated in non-top-level conftest.py
+ files, because they "leak" to the entire directory tree.
+ :ref:`See the docs <pytest_plugins in non-top-level conftest files deprecated>`
+ for the rationale behind this decision (:issue:`3084`)
+
+
+Features
+--------
+
+- New ``--show-capture`` command-line option that allows to specify how to
+ display captured output when tests fail: ``no``, ``stdout``, ``stderr``,
+ ``log`` or ``all`` (the default). (:issue:`1478`)
+
+- New ``--rootdir`` command-line option to override the rules for discovering
+ the root directory. See :doc:`customize <reference/customize>` in the documentation for
+ details. (:issue:`1642`)
+
+- Fixtures are now instantiated based on their scopes, with higher-scoped
+ fixtures (such as ``session``) being instantiated first than lower-scoped
+ fixtures (such as ``function``). The relative order of fixtures of the same
+ scope is kept unchanged, based in their declaration order and their
+ dependencies. (:issue:`2405`)
+
+- ``record_xml_property`` renamed to ``record_property`` and is now compatible
+ with xdist, markers and any reporter. ``record_xml_property`` name is now
+ deprecated. (:issue:`2770`)
+
+- New ``--nf``, ``--new-first`` options: run new tests first followed by the
+ rest of the tests, in both cases tests are also sorted by the file modified
+ time, with more recent files coming first. (:issue:`3034`)
+
+- New ``--last-failed-no-failures`` command-line option that allows to specify
+ the behavior of the cache plugin's ```--last-failed`` feature when no tests
+ failed in the last run (or no cache was found): ``none`` or ``all`` (the
+ default). (:issue:`3139`)
+
+- New ``--doctest-continue-on-failure`` command-line option to enable doctests
+ to show multiple failures for each snippet, instead of stopping at the first
+ failure. (:issue:`3149`)
+
+- Captured log messages are added to the ``<system-out>`` tag in the generated
+ junit xml file if the ``junit_logging`` ini option is set to ``system-out``.
+ If the value of this ini option is ``system-err``, the logs are written to
+ ``<system-err>``. The default value for ``junit_logging`` is ``no``, meaning
+ captured logs are not written to the output file. (:issue:`3156`)
+
+- Allow the logging plugin to handle ``pytest_runtest_logstart`` and
+ ``pytest_runtest_logfinish`` hooks when live logs are enabled. (:issue:`3189`)
+
+- Passing ``--log-cli-level`` in the command-line now automatically activates
+ live logging. (:issue:`3190`)
+
+- Add command line option ``--deselect`` to allow deselection of individual
+ tests at collection time. (:issue:`3198`)
+
+- Captured logs are printed before entering pdb. (:issue:`3204`)
+
+- Deselected item count is now shown before tests are run, e.g. ``collected X
+ items / Y deselected``. (:issue:`3213`)
+
+- The builtin module ``platform`` is now available for use in expressions in
+ ``pytest.mark``. (:issue:`3236`)
+
+- The *short test summary info* section now is displayed after tracebacks and
+ warnings in the terminal. (:issue:`3255`)
+
+- New ``--verbosity`` flag to set verbosity level explicitly. (:issue:`3296`)
+
+- ``pytest.approx`` now accepts comparing a numpy array with a scalar. (:issue:`3312`)
+
+
+Bug Fixes
+---------
+
+- Suppress ``IOError`` when closing the temporary file used for capturing
+ streams in Python 2.7. (:issue:`2370`)
+
+- Fixed ``clear()`` method on ``caplog`` fixture which cleared ``records``, but
+ not the ``text`` property. (:issue:`3297`)
+
+- During test collection, when stdin is not allowed to be read, the
+ ``DontReadFromStdin`` object still allow itself to be iterable and resolved
+ to an iterator without crashing. (:issue:`3314`)
+
+
+Improved Documentation
+----------------------
+
+- Added a :doc:`reference <reference/reference>` page
+ to the docs. (:issue:`1713`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- Change minimum requirement of ``attrs`` to ``17.4.0``. (:issue:`3228`)
+
+- Renamed example directories so all tests pass when ran from the base
+ directory. (:issue:`3245`)
+
+- Internal ``mark.py`` module has been turned into a package. (:issue:`3250`)
+
+- ``pytest`` now depends on the `more-itertools
+ <https://github.com/erikrose/more-itertools>`_ package. (:issue:`3265`)
+
+- Added warning when ``[pytest]`` section is used in a ``.cfg`` file passed
+ with ``-c`` (:issue:`3268`)
+
+- ``nodeids`` can now be passed explicitly to ``FSCollector`` and ``Node``
+ constructors. (:issue:`3291`)
+
+- Internal refactoring of ``FormattedExcinfo`` to use ``attrs`` facilities and
+ remove old support code for legacy Python versions. (:issue:`3292`)
+
+- Refactoring to unify how verbosity is handled internally. (:issue:`3296`)
+
+- Internal refactoring to better integrate with argparse. (:issue:`3304`)
+
+- Fix a python example when calling a fixture in doc/en/usage.rst (:issue:`3308`)
+
+
+pytest 3.4.2 (2018-03-04)
+=========================
+
+Bug Fixes
+---------
+
+- Removed progress information when capture option is ``no``. (:issue:`3203`)
+
+- Refactor check of bindir from ``exists`` to ``isdir``. (:issue:`3241`)
+
+- Fix ``TypeError`` issue when using ``approx`` with a ``Decimal`` value.
+ (:issue:`3247`)
+
+- Fix reference cycle generated when using the ``request`` fixture. (:issue:`3249`)
+
+- ``[tool:pytest]`` sections in ``*.cfg`` files passed by the ``-c`` option are
+ now properly recognized. (:issue:`3260`)
+
+
+Improved Documentation
+----------------------
+
+- Add logging plugin to plugins list. (:issue:`3209`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- Fix minor typo in fixture.rst (:issue:`3259`)
+
+
+pytest 3.4.1 (2018-02-20)
+=========================
+
+Bug Fixes
+---------
+
+- Move import of ``doctest.UnexpectedException`` to top-level to avoid possible
+ errors when using ``--pdb``. (:issue:`1810`)
+
+- Added printing of captured stdout/stderr before entering pdb, and improved a
+ test which was giving false negatives about output capturing. (:issue:`3052`)
+
+- Fix ordering of tests using parametrized fixtures which can lead to fixtures
+ being created more than necessary. (:issue:`3161`)
+
+- Fix bug where logging happening at hooks outside of "test run" hooks would
+ cause an internal error. (:issue:`3184`)
+
+- Detect arguments injected by ``unittest.mock.patch`` decorator correctly when
+ pypi ``mock.patch`` is installed and imported. (:issue:`3206`)
+
+- Errors shown when a ``pytest.raises()`` with ``match=`` fails are now cleaner
+ on what happened: When no exception was raised, the "matching '...'" part got
+ removed as it falsely implies that an exception was raised but it didn't
+ match. When a wrong exception was raised, it's now thrown (like
+ ``pytest.raised()`` without ``match=`` would) instead of complaining about
+ the unmatched text. (:issue:`3222`)
+
+- Fixed output capture handling in doctests on macOS. (:issue:`985`)
+
+
+Improved Documentation
+----------------------
+
+- Add Sphinx parameter docs for ``match`` and ``message`` args to
+ ``pytest.raises``. (:issue:`3202`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- pytest has changed the publication procedure and is now being published to
+ PyPI directly from Travis. (:issue:`3060`)
+
+- Rename ``ParameterSet._for_parameterize()`` to ``_for_parametrize()`` in
+ order to comply with the naming convention. (:issue:`3166`)
+
+- Skip failing pdb/doctest test on mac. (:issue:`985`)
+
+
+pytest 3.4.0 (2018-01-30)
+=========================
+
+Deprecations and Removals
+-------------------------
+
+- All pytest classes now subclass ``object`` for better Python 2/3 compatibility.
+ This should not affect user code except in very rare edge cases. (:issue:`2147`)
+
+
+Features
+--------
+
+- Introduce ``empty_parameter_set_mark`` ini option to select which mark to
+ apply when ``@pytest.mark.parametrize`` is given an empty set of parameters.
+ Valid options are ``skip`` (default) and ``xfail``. Note that it is planned
+ to change the default to ``xfail`` in future releases as this is considered
+ less error prone. (:issue:`2527`)
+
+- **Incompatible change**: after community feedback the :doc:`logging <how-to/logging>` functionality has
+ undergone some changes. Please consult the :ref:`logging documentation <log_changes_3_4>`
+ for details. (:issue:`3013`)
+
+- Console output falls back to "classic" mode when capturing is disabled (``-s``),
+ otherwise the output gets garbled to the point of being useless. (:issue:`3038`)
+
+- New :hook:`pytest_runtest_logfinish`
+ hook which is called when a test item has finished executing, analogous to
+ :hook:`pytest_runtest_logstart`.
+ (:issue:`3101`)
+
+- Improve performance when collecting tests using many fixtures. (:issue:`3107`)
+
+- New ``caplog.get_records(when)`` method which provides access to the captured
+ records for the ``"setup"``, ``"call"`` and ``"teardown"``
+ testing stages. (:issue:`3117`)
+
+- New fixture ``record_xml_attribute`` that allows modifying and inserting
+ attributes on the ``<testcase>`` xml node in JUnit reports. (:issue:`3130`)
+
+- The default cache directory has been renamed from ``.cache`` to
+ ``.pytest_cache`` after community feedback that the name ``.cache`` did not
+ make it clear that it was used by pytest. (:issue:`3138`)
+
+- Colorize the levelname column in the live-log output. (:issue:`3142`)
+
+
+Bug Fixes
+---------
+
+- 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
+ ``pytester`` plugin; this may break tests using multiple inprocess
+ pytest runs if later ones depend on earlier ones leaking global interpreter
+ changes. (:issue:`3016`)
+
+- Fix skipping plugin reporting hook when test aborted before plugin setup
+ hook. (:issue:`3074`)
+
+- Fix progress percentage reported when tests fail during teardown. (:issue:`3088`)
+
+- **Incompatible change**: ``-o/--override`` option no longer eats all the
+ remaining options, which can lead to surprising behavior: for example,
+ ``pytest -o foo=1 /path/to/test.py`` would fail because ``/path/to/test.py``
+ would be considered as part of the ``-o`` command-line argument. One
+ consequence of this is that now multiple configuration overrides need
+ multiple ``-o`` flags: ``pytest -o foo=1 -o bar=2``. (:issue:`3103`)
+
+
+Improved Documentation
+----------------------
+
+- Document hooks (defined with ``historic=True``) which cannot be used with
+ ``hookwrapper=True``. (:issue:`2423`)
+
+- Clarify that warning capturing doesn't change the warning filter by default.
+ (:issue:`2457`)
+
+- Clarify a possible confusion when using pytest_fixture_setup with fixture
+ functions that return None. (:issue:`2698`)
+
+- Fix the wording of a sentence on doctest flags used in pytest. (:issue:`3076`)
+
+- Prefer ``https://*.readthedocs.io`` over ``http://*.rtfd.org`` for links in
+ the documentation. (:issue:`3092`)
+
+- Improve readability (wording, grammar) of Getting Started guide (:issue:`3131`)
+
+- Added note that calling pytest.main multiple times from the same process is
+ not recommended because of import caching. (:issue:`3143`)
+
+
+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
+ the ``import`` keyword in expressions). (:issue:`2953`)
+
+- Change parametrized automatic test id generation to use the ``__name__``
+ attribute of functions instead of the fallback argument name plus counter.
+ (:issue:`2976`)
+
+- Replace py.std with stdlib imports. (:issue:`3067`)
+
+- Corrected 'you' to 'your' in logging docs. (:issue:`3129`)
+
+
+pytest 3.3.2 (2017-12-25)
+=========================
+
+Bug Fixes
+---------
+
+- pytester: ignore files used to obtain current user metadata in the fd leak
+ detector. (:issue:`2784`)
+
+- Fix **memory leak** where objects returned by fixtures were never destructed
+ by the garbage collector. (:issue:`2981`)
+
+- Fix conversion of pyargs to filename to not convert symlinks on Python 2. (:issue:`2985`)
+
+- ``PYTEST_DONT_REWRITE`` is now checked for plugins too rather than only for
+ test modules. (:issue:`2995`)
+
+
+Improved Documentation
+----------------------
+
+- Add clarifying note about behavior of multiple parametrized arguments (:issue:`3001`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- Code cleanup. (:issue:`3015`,
+ :issue:`3021`)
+
+- Clean up code by replacing imports and references of ``_ast`` to ``ast``.
+ (:issue:`3018`)
+
+
+pytest 3.3.1 (2017-12-05)
+=========================
+
+Bug Fixes
+---------
+
+- Fix issue about ``-p no:<plugin>`` having no effect. (:issue:`2920`)
+
+- Fix regression with warnings that contained non-strings in their arguments in
+ Python 2. (:issue:`2956`)
+
+- Always escape null bytes when setting ``PYTEST_CURRENT_TEST``. (:issue:`2957`)
+
+- Fix ``ZeroDivisionError`` when using the ``testmon`` plugin when no tests
+ were actually collected. (:issue:`2971`)
+
+- Bring back ``TerminalReporter.writer`` as an alias to
+ ``TerminalReporter._tw``. This alias was removed by accident in the ``3.3.0``
+ release. (:issue:`2984`)
+
+- The ``pytest-capturelog`` plugin is now also blacklisted, avoiding errors when
+ running pytest with it still installed. (:issue:`3004`)
+
+
+Improved Documentation
+----------------------
+
+- Fix broken link to plugin ``pytest-localserver``. (:issue:`2963`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- Update github "bugs" link in ``CONTRIBUTING.rst`` (:issue:`2949`)
+
+
+pytest 3.3.0 (2017-11-23)
+=========================
+
+Deprecations and Removals
+-------------------------
+
+- pytest no longer supports Python **2.6** and **3.3**. Those Python versions
+ are EOL for some time now and incur maintenance and compatibility costs on
+ the pytest core team, and following up with the rest of the community we
+ decided that they will no longer be supported starting on this version. Users
+ which still require those versions should pin pytest to ``<3.3``. (:issue:`2812`)
+
+- Remove internal ``_preloadplugins()`` function. This removal is part of the
+ ``pytest_namespace()`` hook deprecation. (:issue:`2636`)
+
+- Internally change ``CallSpec2`` to have a list of marks instead of a broken
+ mapping of keywords. This removes the keywords attribute of the internal
+ ``CallSpec2`` class. (:issue:`2672`)
+
+- Remove ParameterSet.deprecated_arg_dict - its not a public api and the lack
+ of the underscore was a naming error. (:issue:`2675`)
+
+- Remove the internal multi-typed attribute ``Node._evalskip`` and replace it
+ with the boolean ``Node._skipped_by_mark``. (:issue:`2767`)
+
+- The ``params`` list passed to ``pytest.fixture`` is now for
+ all effects considered immutable and frozen at the moment of the ``pytest.fixture``
+ call. Previously the list could be changed before the first invocation of the fixture
+ allowing for a form of dynamic parametrization (for example, updated from command-line options),
+ but this was an unwanted implementation detail which complicated the internals and prevented
+ some internal cleanup. See issue :issue:`2959`
+ for details and a recommended workaround.
+
+Features
+--------
+
+- ``pytest_fixture_post_finalizer`` hook can now receive a ``request``
+ argument. (:issue:`2124`)
+
+- Replace the old introspection code in compat.py that determines the available
+ arguments of fixtures with inspect.signature on Python 3 and
+ funcsigs.signature on Python 2. This should respect ``__signature__``
+ declarations on functions. (:issue:`2267`)
+
+- Report tests with global ``pytestmark`` variable only once. (:issue:`2549`)
+
+- Now pytest displays the total progress percentage while running tests. The
+ previous output style can be set by configuring the ``console_output_style``
+ setting to ``classic``. (:issue:`2657`)
+
+- Match ``warns`` signature to ``raises`` by adding ``match`` keyword. (:issue:`2708`)
+
+- pytest now captures and displays output from the standard ``logging`` module.
+ The user can control the logging level to be captured by specifying options
+ in ``pytest.ini``, the command line and also during individual tests using
+ markers. Also, a ``caplog`` fixture is available that enables users to test
+ the captured log during specific tests (similar to ``capsys`` for example).
+ For more information, please see the :doc:`logging docs <how-to/logging>`. This feature was
+ introduced by merging the popular :pypi:`pytest-catchlog` plugin, thanks to :user:`thisch`.
+ Be advised that during the merging the
+ backward compatibility interface with the defunct ``pytest-capturelog`` has
+ been dropped. (:issue:`2794`)
+
+- Add ``allow_module_level`` kwarg to ``pytest.skip()``, enabling to skip the
+ whole module. (:issue:`2808`)
+
+- Allow setting ``file_or_dir``, ``-c``, and ``-o`` in PYTEST_ADDOPTS. (:issue:`2824`)
+
+- Return stdout/stderr capture results as a ``namedtuple``, so ``out`` and
+ ``err`` can be accessed by attribute. (:issue:`2879`)
+
+- Add ``capfdbinary``, a version of ``capfd`` which returns bytes from
+ ``readouterr()``. (:issue:`2923`)
+
+- Add ``capsysbinary`` a version of ``capsys`` which returns bytes from
+ ``readouterr()``. (:issue:`2934`)
+
+- Implement feature to skip ``setup.py`` files when run with
+ ``--doctest-modules``. (:issue:`502`)
+
+
+Bug Fixes
+---------
+
+- Resume output capturing after ``capsys/capfd.disabled()`` context manager.
+ (:issue:`1993`)
+
+- ``pytest_fixture_setup`` and ``pytest_fixture_post_finalizer`` hooks are now
+ called for all ``conftest.py`` files. (:issue:`2124`)
+
+- If an exception happens while loading a plugin, pytest no longer hides the
+ original traceback. In Python 2 it will show the original traceback with a new
+ message that explains in which plugin. In Python 3 it will show 2 canonized
+ exceptions, the original exception while loading the plugin in addition to an
+ exception that pytest throws about loading a plugin. (:issue:`2491`)
+
+- ``capsys`` and ``capfd`` can now be used by other fixtures. (:issue:`2709`)
+
+- Internal ``pytester`` plugin properly encodes ``bytes`` arguments to
+ ``utf-8``. (:issue:`2738`)
+
+- ``testdir`` now uses use the same method used by ``tmpdir`` to create its
+ temporary directory. This changes the final structure of the ``testdir``
+ directory slightly, but should not affect usage in normal scenarios and
+ avoids a number of potential problems. (:issue:`2751`)
+
+- pytest no longer complains about warnings with unicode messages being
+ non-ascii compatible even for ascii-compatible messages. As a result of this,
+ warnings with unicode messages are converted first to an ascii representation
+ for safety. (:issue:`2809`)
+
+- Change return value of pytest command when ``--maxfail`` is reached from
+ ``2`` (interrupted) to ``1`` (failed). (:issue:`2845`)
+
+- Fix issue in assertion rewriting which could lead it to rewrite modules which
+ should not be rewritten. (:issue:`2939`)
+
+- Handle marks without description in ``pytest.ini``. (:issue:`2942`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- pytest now depends on :pypi:`attrs` for internal
+ structures to ease code maintainability. (:issue:`2641`)
+
+- Refactored internal Python 2/3 compatibility code to use ``six``. (:issue:`2642`)
+
+- Stop vendoring ``pluggy`` - we're missing out on its latest changes for not
+ much benefit (:issue:`2719`)
+
+- Internal refactor: simplify ascii string escaping by using the
+ backslashreplace error handler in newer Python 3 versions. (:issue:`2734`)
+
+- Remove unnecessary mark evaluator in unittest plugin (:issue:`2767`)
+
+- Calls to ``Metafunc.addcall`` now emit a deprecation warning. This function
+ is scheduled to be removed in ``pytest-4.0``. (:issue:`2876`)
+
+- Internal move of the parameterset extraction to a more maintainable place.
+ (:issue:`2877`)
+
+- Internal refactoring to simplify scope node lookup. (:issue:`2910`)
+
+- Configure ``pytest`` to prevent pip from installing pytest in unsupported
+ Python versions. (:issue:`2922`)
+
+
+pytest 3.2.5 (2017-11-15)
+=========================
+
+Bug Fixes
+---------
+
+- Remove ``py<1.5`` restriction from ``pytest`` as this can cause version
+ conflicts in some installations. (:issue:`2926`)
+
+
+pytest 3.2.4 (2017-11-13)
+=========================
+
+Bug Fixes
+---------
+
+- Fix the bug where running with ``--pyargs`` will result in items with
+ empty ``parent.nodeid`` if run from a different root directory. (:issue:`2775`)
+
+- Fix issue with ``@pytest.parametrize`` if argnames was specified as keyword arguments.
+ (:issue:`2819`)
+
+- Strip whitespace from marker names when reading them from INI config. (:issue:`2856`)
+
+- Show full context of doctest source in the pytest output, if the line number of
+ failed example in the docstring is < 9. (:issue:`2882`)
+
+- Match fixture paths against actual path segments in order to avoid matching folders which share a prefix.
+ (:issue:`2836`)
+
+Improved Documentation
+----------------------
+
+- Introduce a dedicated section about conftest.py. (:issue:`1505`)
+
+- Explicitly mention ``xpass`` in the documentation of ``xfail``. (:issue:`1997`)
+
+- Append example for pytest.param in the example/parametrize document. (:issue:`2658`)
+
+- Clarify language of proposal for fixtures parameters (:issue:`2893`)
+
+- List python 3.6 in the documented supported versions in the getting started
+ document. (:issue:`2903`)
+
+- Clarify the documentation of available fixture scopes. (:issue:`538`)
+
+- Add documentation about the ``python -m pytest`` invocation adding the
+ current directory to sys.path. (:issue:`911`)
+
+
+pytest 3.2.3 (2017-10-03)
+=========================
+
+Bug Fixes
+---------
+
+- Fix crash in tab completion when no prefix is given. (:issue:`2748`)
+
+- The equality checking function (``__eq__``) of ``MarkDecorator`` returns
+ ``False`` if one object is not an instance of ``MarkDecorator``. (:issue:`2758`)
+
+- When running ``pytest --fixtures-per-test``: don't crash if an item has no
+ _fixtureinfo attribute (e.g. doctests) (:issue:`2788`)
+
+
+Improved Documentation
+----------------------
+
+- In help text of ``-k`` option, add example of using ``not`` to not select
+ certain tests whose names match the provided expression. (:issue:`1442`)
+
+- Add note in ``parametrize.rst`` about calling ``metafunc.parametrize``
+ multiple times. (:issue:`1548`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- Set ``xfail_strict=True`` in pytest's own test suite to catch expected
+ failures as soon as they start to pass. (:issue:`2722`)
+
+- Fix typo in example of passing a callable to markers (in example/markers.rst)
+ (:issue:`2765`)
+
+
+pytest 3.2.2 (2017-09-06)
+=========================
+
+Bug Fixes
+---------
+
+- Calling the deprecated ``request.getfuncargvalue()`` now shows the source of
+ the call. (:issue:`2681`)
+
+- Allow tests declared as ``@staticmethod`` to use fixtures. (:issue:`2699`)
+
+- Fixed edge-case during collection: attributes which raised ``pytest.fail``
+ when accessed would abort the entire collection. (:issue:`2707`)
+
+- Fix ``ReprFuncArgs`` with mixed unicode and UTF-8 args. (:issue:`2731`)
+
+
+Improved Documentation
+----------------------
+
+- In examples on working with custom markers, add examples demonstrating the
+ usage of ``pytest.mark.MARKER_NAME.with_args`` in comparison with
+ ``pytest.mark.MARKER_NAME.__call__`` (:issue:`2604`)
+
+- In one of the simple examples, use ``pytest_collection_modifyitems()`` to skip
+ tests based on a command-line option, allowing its sharing while preventing a
+ user error when accessing ``pytest.config`` before the argument parsing.
+ (:issue:`2653`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- Fixed minor error in 'Good Practices/Manual Integration' code snippet.
+ (:issue:`2691`)
+
+- Fixed typo in goodpractices.rst. (:issue:`2721`)
+
+- Improve user guidance regarding ``--resultlog`` deprecation. (:issue:`2739`)
+
+
+pytest 3.2.1 (2017-08-08)
+=========================
+
+Bug Fixes
+---------
+
+- Fixed small terminal glitch when collecting a single test item. (:issue:`2579`)
+
+- Correctly consider ``/`` as the file separator to automatically mark plugin
+ files for rewrite on Windows. (:issue:`2591`)
+
+- Properly escape test names when setting ``PYTEST_CURRENT_TEST`` environment
+ variable. (:issue:`2644`)
+
+- Fix error on Windows and Python 3.6+ when ``sys.stdout`` has been replaced
+ with a stream-like object which does not implement the full ``io`` module
+ buffer protocol. In particular this affects ``pytest-xdist`` users on the
+ aforementioned platform. (:issue:`2666`)
+
+
+Improved Documentation
+----------------------
+
+- Explicitly document which pytest features work with ``unittest``. (:issue:`2626`)
+
+
+pytest 3.2.0 (2017-07-30)
+=========================
+
+Deprecations and Removals
+-------------------------
+
+- ``pytest.approx`` no longer supports ``>``, ``>=``, ``<`` and ``<=``
+ operators to avoid surprising/inconsistent behavior. See the :func:`~pytest.approx` docs for more
+ information. (:issue:`2003`)
+
+- All old-style specific behavior in current classes in the pytest's API is
+ considered deprecated at this point and will be removed in a future release.
+ This affects Python 2 users only and in rare situations. (:issue:`2147`)
+
+- A deprecation warning is now raised when using marks for parameters
+ in ``pytest.mark.parametrize``. Use ``pytest.param`` to apply marks to
+ parameters instead. (:issue:`2427`)
+
+
+Features
+--------
+
+- Add support for numpy arrays (and dicts) to approx. (:issue:`1994`)
+
+- Now test function objects have a ``pytestmark`` attribute containing a list
+ of marks applied directly to the test function, as opposed to marks inherited
+ from parent classes or modules. (:issue:`2516`)
+
+- Collection ignores local virtualenvs by default; ``--collect-in-virtualenv``
+ overrides this behavior. (:issue:`2518`)
+
+- Allow class methods decorated as ``@staticmethod`` to be candidates for
+ collection as a test function. (Only for Python 2.7 and above. Python 2.6
+ will still ignore static methods.) (:issue:`2528`)
+
+- Introduce ``mark.with_args`` in order to allow passing functions/classes as
+ sole argument to marks. (:issue:`2540`)
+
+- New ``cache_dir`` ini option: sets the directory where the contents of the
+ cache plugin are stored. Directory may be relative or absolute path: if relative path, then
+ directory is created relative to ``rootdir``, otherwise it is used as is.
+ Additionally path may contain environment variables which are expanded during
+ runtime. (:issue:`2543`)
+
+- Introduce the ``PYTEST_CURRENT_TEST`` environment variable that is set with
+ the ``nodeid`` and stage (``setup``, ``call`` and ``teardown``) of the test
+ being currently executed. See the :ref:`documentation <pytest current test env>`
+ for more info. (:issue:`2583`)
+
+- Introduced ``@pytest.mark.filterwarnings`` mark which allows overwriting the
+ warnings filter on a per test, class or module level. See the :ref:`docs <filterwarnings>`
+ for more information. (:issue:`2598`)
+
+- ``--last-failed`` now remembers forever when a test has failed and only
+ forgets it if it passes again. This makes it easy to fix a test suite by
+ selectively running files and fixing tests incrementally. (:issue:`2621`)
+
+- New ``pytest_report_collectionfinish`` hook which allows plugins to add
+ messages to the terminal reporting after collection has been finished
+ successfully. (:issue:`2622`)
+
+- Added support for :pep:`415`\'s
+ ``Exception.__suppress_context__``. Now if a ``raise exception from None`` is
+ caught by pytest, pytest will no longer chain the context in the test report.
+ The behavior now matches Python's traceback behavior. (:issue:`2631`)
+
+- Exceptions raised by ``pytest.fail``, ``pytest.skip`` and ``pytest.xfail``
+ now subclass BaseException, making them harder to be caught unintentionally
+ by normal code. (:issue:`580`)
+
+
+Bug Fixes
+---------
+
+- Set ``stdin`` to a closed ``PIPE`` in ``pytester.py.Testdir.popen()`` for
+ avoid unwanted interactive ``pdb`` (:issue:`2023`)
+
+- Add missing ``encoding`` attribute to ``sys.std*`` streams when using
+ ``capsys`` capture mode. (:issue:`2375`)
+
+- Fix terminal color changing to black on Windows if ``colorama`` is imported
+ in a ``conftest.py`` file. (:issue:`2510`)
+
+- Fix line number when reporting summary of skipped tests. (:issue:`2548`)
+
+- capture: ensure that EncodedFile.name is a string. (:issue:`2555`)
+
+- The options ``--fixtures`` and ``--fixtures-per-test`` will now keep
+ indentation within docstrings. (:issue:`2574`)
+
+- doctests line numbers are now reported correctly, fixing `pytest-sugar#122
+ <https://github.com/Frozenball/pytest-sugar/issues/122>`_. (:issue:`2610`)
+
+- Fix non-determinism in order of fixture collection. Adds new dependency
+ (ordereddict) for Python 2.6. (:issue:`920`)
+
+
+Improved Documentation
+----------------------
+
+- Clarify ``pytest_configure`` hook call order. (:issue:`2539`)
+
+- Extend documentation for testing plugin code with the ``pytester`` plugin.
+ (:issue:`971`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- Update help message for ``--strict`` to make it clear it only deals with
+ unregistered markers, not warnings. (:issue:`2444`)
+
+- Internal code move: move code for pytest.approx/pytest.raises to own files in
+ order to cut down the size of python.py (:issue:`2489`)
+
+- Renamed the utility function ``_pytest.compat._escape_strings`` to
+ ``_ascii_escaped`` to better communicate the function's purpose. (:issue:`2533`)
+
+- Improve error message for CollectError with skip/skipif. (:issue:`2546`)
+
+- Emit warning about ``yield`` tests being deprecated only once per generator.
+ (:issue:`2562`)
+
+- Ensure final collected line doesn't include artifacts of previous write.
+ (:issue:`2571`)
+
+- Fixed all flake8 errors and warnings. (:issue:`2581`)
+
+- Added ``fix-lint`` tox environment to run automatic pep8 fixes on the code.
+ (:issue:`2582`)
+
+- Turn warnings into errors in pytest's own test suite in order to catch
+ regressions due to deprecations more promptly. (:issue:`2588`)
+
+- Show multiple issue links in CHANGELOG entries. (:issue:`2620`)
+
+
+pytest 3.1.3 (2017-07-03)
+=========================
+
+Bug Fixes
+---------
+
+- Fix decode error in Python 2 for doctests in docstrings. (:issue:`2434`)
+
+- Exceptions raised during teardown by finalizers are now suppressed until all
+ finalizers are called, with the initial exception reraised. (:issue:`2440`)
+
+- Fix incorrect "collected items" report when specifying tests on the command-
+ line. (:issue:`2464`)
+
+- ``deprecated_call`` in context-manager form now captures deprecation warnings
+ even if the same warning has already been raised. Also, ``deprecated_call``
+ will always produce the same error message (previously it would produce
+ different messages in context-manager vs. function-call mode). (:issue:`2469`)
+
+- Fix issue where paths collected by pytest could have triple leading ``/``
+ characters. (:issue:`2475`)
+
+- Fix internal error when trying to detect the start of a recursive traceback.
+ (:issue:`2486`)
+
+
+Improved Documentation
+----------------------
+
+- Explicitly state for which hooks the calls stop after the first non-None
+ result. (:issue:`2493`)
+
+
+Trivial/Internal Changes
+------------------------
+
+- Create invoke tasks for updating the vendored packages. (:issue:`2474`)
+
+- Update copyright dates in LICENSE, README.rst and in the documentation.
+ (:issue:`2499`)
+
+
+pytest 3.1.2 (2017-06-08)
+=========================
+
+Bug Fixes
+---------
+
+- Required options added via ``pytest_addoption`` will no longer prevent using
+ --help without passing them. (#1999)
+
+- Respect ``python_files`` in assertion rewriting. (#2121)
+
+- Fix recursion error detection when frames in the traceback contain objects
+ that can't be compared (like ``numpy`` arrays). (#2459)
+
+- ``UnicodeWarning`` is issued from the internal pytest warnings plugin only
+ when the message contains non-ascii unicode (Python 2 only). (#2463)
+
+- Added a workaround for Python 3.6 ``WindowsConsoleIO`` breaking due to Pytests's
+ ``FDCapture``. Other code using console handles might still be affected by the
+ very same issue and might require further workarounds/fixes, i.e. ``colorama``.
+ (#2467)
+
+
+Improved Documentation
+----------------------
+
+- Fix internal API links to ``pluggy`` objects. (#2331)
+
+- Make it clear that ``pytest.xfail`` stops test execution at the calling point
+ and improve overall flow of the ``skipping`` docs. (#810)
+
+
+pytest 3.1.1 (2017-05-30)
+=========================
+
+Bug Fixes
+---------
+
+- pytest warning capture no longer overrides existing warning filters. The
+ previous behaviour would override all filters and caused regressions in test
+ suites which configure warning filters to match their needs. Note that as a
+ side-effect of this is that ``DeprecationWarning`` and
+ ``PendingDeprecationWarning`` are no longer shown by default. (#2430)
+
+- Fix issue with non-ascii contents in doctest text files. (#2434)
+
+- Fix encoding errors for unicode warnings in Python 2. (#2436)
+
+- ``pytest.deprecated_call`` now captures ``PendingDeprecationWarning`` in
+ context manager form. (#2441)
+
+
+Improved Documentation
+----------------------
+
+- Addition of towncrier for changelog management. (#2390)
+
+
+3.1.0 (2017-05-22)
+==================
+
+
+New Features
+------------
+
+* The ``pytest-warnings`` plugin has been integrated into the core and now ``pytest`` automatically
+ captures and displays warnings at the end of the test session.
+
+ .. warning::
+
+ This feature may disrupt test suites which apply and treat warnings themselves, and can be
+ disabled in your ``pytest.ini``:
+
+ .. code-block:: ini
+
+ [pytest]
+ addopts = -p no:warnings
+
+ See the :doc:`warnings documentation page <how-to/capture-warnings>` for more
+ information.
+
+ Thanks :user:`nicoddemus` for the PR.
+
+* Added ``junit_suite_name`` ini option to specify root ``<testsuite>`` name for JUnit XML reports (:issue:`533`).
+
+* Added an ini option ``doctest_encoding`` to specify which encoding to use for doctest files.
+ Thanks :user:`wheerd` for the PR (:pull:`2101`).
+
+* ``pytest.warns`` now checks for subclass relationship rather than
+ class equality. Thanks :user:`lesteve` for the PR (:pull:`2166`)
+
+* ``pytest.raises`` now asserts that the error message matches a text or regex
+ with the ``match`` keyword argument. Thanks :user:`Kriechi` for the PR.
+
+* ``pytest.param`` can be used to declare test parameter sets with marks and test ids.
+ Thanks :user:`RonnyPfannschmidt` for the PR.
+
+
+Changes
+-------
+
+* remove all internal uses of pytest_namespace hooks,
+ this is to prepare the removal of preloadconfig in pytest 4.0
+ Thanks to :user:`RonnyPfannschmidt` for the PR.
+
+* pytest now warns when a callable ids raises in a parametrized test. Thanks :user:`fogo` for the PR.
+
+* It is now possible to skip test classes from being collected by setting a
+ ``__test__`` attribute to ``False`` in the class body (:issue:`2007`). Thanks
+ to :user:`syre` for the report and :user:`lwm` for the PR.
+
+* Change junitxml.py to produce reports that comply with Junitxml schema.
+ If the same test fails with failure in call and then errors in teardown
+ we split testcase element into two, one containing the error and the other
+ the failure. (:issue:`2228`) Thanks to :user:`kkoukiou` for the PR.
+
+* Testcase reports with a ``url`` attribute will now properly write this to junitxml.
+ Thanks :user:`fushi` for the PR (:pull:`1874`).
+
+* Remove common items from dict comparison output when verbosity=1. Also update
+ the truncation message to make it clearer that pytest truncates all
+ assertion messages if verbosity < 2 (:issue:`1512`).
+ Thanks :user:`mattduck` for the PR
+
+* ``--pdbcls`` no longer implies ``--pdb``. This makes it possible to use
+ ``addopts=--pdbcls=module.SomeClass`` on ``pytest.ini``. Thanks :user:`davidszotten` for
+ the PR (:pull:`1952`).
+
+* 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.
+ Thanks :user:`RonnyPfannschmidt` for the report and PR.
+
+* Hooks are now verified after collection is complete, rather than right after loading installed plugins. This
+ makes it easy to write hooks for plugins which will be loaded during collection, for example using the
+ ``pytest_plugins`` special variable (:issue:`1821`).
+ Thanks :user:`nicoddemus` for the PR.
+
+* Modify ``pytest_make_parametrize_id()`` hook to accept ``argname`` as an
+ additional parameter.
+ Thanks :user:`unsignedint` for the PR.
+
+* Add ``venv`` to the default ``norecursedirs`` setting.
+ Thanks :user:`The-Compiler` for the PR.
+
+* ``PluginManager.import_plugin`` now accepts unicode plugin names in Python 2.
+ Thanks :user:`reutsharabani` for the PR.
+
+* fix :issue:`2308`: When using both ``--lf`` and ``--ff``, only the last failed tests are run.
+ Thanks :user:`ojii` for the PR.
+
+* Replace minor/patch level version numbers in the documentation with placeholders.
+ This significantly reduces change-noise as different contributors regenerate
+ the documentation on different platforms.
+ Thanks :user:`RonnyPfannschmidt` for the PR.
+
+* fix :issue:`2391`: consider pytest_plugins on all plugin modules
+ Thanks :user:`RonnyPfannschmidt` for the PR.
+
+
+Bug Fixes
+---------
+
+* Fix ``AttributeError`` on ``sys.stdout.buffer`` / ``sys.stderr.buffer``
+ while using ``capsys`` fixture in python 3. (:issue:`1407`).
+ Thanks to :user:`asottile`.
+
+* Change capture.py's ``DontReadFromInput`` class to throw ``io.UnsupportedOperation`` errors rather
+ than ValueErrors in the ``fileno`` method (:issue:`2276`).
+ Thanks :user:`metasyn` and :user:`vlad-dragos` for the PR.
+
+* Fix exception formatting while importing modules when the exception message
+ contains non-ascii characters (:issue:`2336`).
+ Thanks :user:`fabioz` for the report and :user:`nicoddemus` for the PR.
+
+* Added documentation related to issue (:issue:`1937`)
+ Thanks :user:`skylarjhdownes` for the PR.
+
+* Allow collecting files with any file extension as Python modules (:issue:`2369`).
+ Thanks :user:`Kodiologist` for the PR.
+
+* Show the correct error message when collect "parametrize" func with wrong args (:issue:`2383`).
+ Thanks :user:`The-Compiler` for the report and :user:`robin0371` for the PR.
+
+
+3.0.7 (2017-03-14)
+==================
+
+
+* Fix issue in assertion rewriting breaking due to modules silently discarding
+ other modules when importing fails
+ Notably, importing the ``anydbm`` module is fixed. (:issue:`2248`).
+ Thanks :user:`pfhayes` for the PR.
+
+* junitxml: Fix problematic case where system-out tag occurred twice per testcase
+ element in the XML report. Thanks :user:`kkoukiou` for the PR.
+
+* Fix regression, pytest now skips unittest correctly if run with ``--pdb``
+ (:issue:`2137`). Thanks to :user:`gst` for the report and :user:`mbyt` for the PR.
+
+* Ignore exceptions raised from descriptors (e.g. properties) during Python test collection (:issue:`2234`).
+ Thanks to :user:`bluetech`.
+
+* ``--override-ini`` now correctly overrides some fundamental options like ``python_files`` (:issue:`2238`).
+ Thanks :user:`sirex` for the report and :user:`nicoddemus` for the PR.
+
+* Replace ``raise StopIteration`` usages in the code by simple ``returns`` to finish generators, in accordance to :pep:`479` (:issue:`2160`).
+ Thanks to :user:`nicoddemus` for the PR.
+
+* Fix internal errors when an unprintable ``AssertionError`` is raised inside a test.
+ Thanks :user:`omerhadari` for the PR.
+
+* Skipping plugin now also works with test items generated by custom collectors (:issue:`2231`).
+ Thanks to :user:`vidartf`.
+
+* Fix trailing whitespace in console output if no .ini file presented (:issue:`2281`). Thanks :user:`fbjorn` for the PR.
+
+* Conditionless ``xfail`` markers no longer rely on the underlying test item
+ being an instance of ``PyobjMixin``, and can therefore apply to tests not
+ collected by the built-in python test collector. Thanks :user:`barneygale` for the
+ PR.
+
+
+3.0.6 (2017-01-22)
+==================
+
+* pytest no longer generates ``PendingDeprecationWarning`` from its own operations, which was introduced by mistake in version ``3.0.5`` (:issue:`2118`).
+ Thanks to :user:`nicoddemus` for the report and :user:`RonnyPfannschmidt` for the PR.
+
+
+* pytest no longer recognizes coroutine functions as yield tests (:issue:`2129`).
+ Thanks to :user:`malinoff` for the PR.
+
+* Plugins loaded by the ``PYTEST_PLUGINS`` environment variable are now automatically
+ considered for assertion rewriting (:issue:`2185`).
+ Thanks :user:`nicoddemus` for the PR.
+
+* Improve error message when pytest.warns fails (:issue:`2150`). The type(s) of the
+ expected warnings and the list of caught warnings is added to the
+ error message. Thanks :user:`lesteve` for the PR.
+
+* Fix ``pytester`` internal plugin to work correctly with latest versions of
+ ``zope.interface`` (:issue:`1989`). Thanks :user:`nicoddemus` for the PR.
+
+* Assert statements of the ``pytester`` plugin again benefit from assertion rewriting (:issue:`1920`).
+ Thanks :user:`RonnyPfannschmidt` for the report and :user:`nicoddemus` for the PR.
+
+* Specifying tests with colons like ``test_foo.py::test_bar`` for tests in
+ subdirectories with ini configuration files now uses the correct ini file
+ (:issue:`2148`). Thanks :user:`pelme`.
+
+* Fail ``testdir.runpytest().assert_outcomes()`` explicitly if the pytest
+ terminal output it relies on is missing. Thanks to :user:`eli-b` for the PR.
+
+
+3.0.5 (2016-12-05)
+==================
+
+* Add warning when not passing ``option=value`` correctly to ``-o/--override-ini`` (:issue:`2105`).
+ Also improved the help documentation. Thanks to :user:`mbukatov` for the report and
+ :user:`lwm` for the PR.
+
+* Now ``--confcutdir`` and ``--junit-xml`` are properly validated if they are directories
+ and filenames, respectively (:issue:`2089` and :issue:`2078`). Thanks to :user:`lwm` for the PR.
+
+* Add hint to error message hinting possible missing ``__init__.py`` (:issue:`478`). Thanks :user:`DuncanBetts`.
+
+* More accurately describe when fixture finalization occurs in documentation (:issue:`687`). Thanks :user:`DuncanBetts`.
+
+* Provide ``:ref:`` targets for ``recwarn.rst`` so we can use intersphinx referencing.
+ Thanks to :user:`dupuy` for the report and :user:`lwm` for the PR.
+
+* In Python 2, use a simple ``+-`` ASCII string in the string representation of ``pytest.approx`` (for example ``"4 +- 4.0e-06"``)
+ because it is brittle to handle that in different contexts and representations internally in pytest
+ which can result in bugs such as :issue:`2111`. In Python 3, the representation still uses ``±`` (for example ``4 ± 4.0e-06``).
+ Thanks :user:`kerrick-lyft` for the report and :user:`nicoddemus` for the PR.
+
+* Using ``item.Function``, ``item.Module``, etc., is now issuing deprecation warnings, prefer
+ ``pytest.Function``, ``pytest.Module``, etc., instead (:issue:`2034`).
+ Thanks :user:`nmundar` for the PR.
+
+* Fix error message using ``approx`` with complex numbers (:issue:`2082`).
+ Thanks :user:`adler-j` for the report and :user:`nicoddemus` for the PR.
+
+* Fixed false-positives warnings from assertion rewrite hook for modules imported more than
+ once by the ``pytest_plugins`` mechanism.
+ Thanks :user:`nicoddemus` for the PR.
+
+* Remove an internal cache which could cause hooks from ``conftest.py`` files in
+ sub-directories to be called in other directories incorrectly (:issue:`2016`).
+ Thanks :user:`d-b-w` for the report and :user:`nicoddemus` for the PR.
+
+* Remove internal code meant to support earlier Python 3 versions that produced the side effect
+ of leaving ``None`` in ``sys.modules`` when expressions were evaluated by pytest (for example passing a condition
+ as a string to ``pytest.mark.skipif``)(:issue:`2103`).
+ Thanks :user:`jaraco` for the report and :user:`nicoddemus` for the PR.
+
+* Cope gracefully with a .pyc file with no matching .py file (:issue:`2038`). Thanks
+ :user:`nedbat`.
+
+
+3.0.4 (2016-11-09)
+==================
+
+* Import errors when collecting test modules now display the full traceback (:issue:`1976`).
+ Thanks :user:`cwitty` for the report and :user:`nicoddemus` for the PR.
+
+* Fix confusing command-line help message for custom options with two or more ``metavar`` properties (:issue:`2004`).
+ Thanks :user:`okulynyak` and :user:`davehunt` for the report and :user:`nicoddemus` for the PR.
+
+* When loading plugins, import errors which contain non-ascii messages are now properly handled in Python 2 (:issue:`1998`).
+ Thanks :user:`nicoddemus` for the PR.
+
+* Fixed cyclic reference when ``pytest.raises`` is used in context-manager form (:issue:`1965`). Also as a
+ result of this fix, ``sys.exc_info()`` is left empty in both context-manager and function call usages.
+ Previously, ``sys.exc_info`` would contain the exception caught by the context manager,
+ even when the expected exception occurred.
+ Thanks :user:`MSeifert04` for the report and the PR.
+
+* Fixed false-positives warnings from assertion rewrite hook for modules that were rewritten but
+ were later marked explicitly by ``pytest.register_assert_rewrite``
+ or implicitly as a plugin (:issue:`2005`).
+ Thanks :user:`RonnyPfannschmidt` for the report and :user:`nicoddemus` for the PR.
+
+* Report teardown output on test failure (:issue:`442`).
+ Thanks :user:`matclab` for the PR.
+
+* Fix teardown error message in generated xUnit XML.
+ Thanks :user:`gdyuldin` for the PR.
+
+* Properly handle exceptions in ``multiprocessing`` tasks (:issue:`1984`).
+ 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.
+
+
+3.0.3 (2016-09-28)
+==================
+
+* The ``ids`` argument to ``parametrize`` again accepts ``unicode`` strings
+ in Python 2 (:issue:`1905`).
+ Thanks :user:`philpep` for the report and :user:`nicoddemus` for the PR.
+
+* Assertions are now being rewritten for plugins in development mode
+ (``pip install -e``) (:issue:`1934`).
+ Thanks :user:`nicoddemus` for the PR.
+
+* Fix pkg_resources import error in Jython projects (:issue:`1853`).
+ Thanks :user:`raquel-ucl` for the PR.
+
+* Got rid of ``AttributeError: 'Module' object has no attribute '_obj'`` exception
+ in Python 3 (:issue:`1944`).
+ Thanks :user:`axil` for the PR.
+
+* Explain a bad scope value passed to ``@fixture`` declarations or
+ a ``MetaFunc.parametrize()`` call.
+
+* This version includes ``pluggy-0.4.0``, which correctly handles
+ ``VersionConflict`` errors in plugins (:issue:`704`).
+ Thanks :user:`nicoddemus` for the PR.
+
+
+3.0.2 (2016-09-01)
+==================
+
+* Improve error message when passing non-string ids to ``pytest.mark.parametrize`` (:issue:`1857`).
+ Thanks :user:`okken` for the report and :user:`nicoddemus` for the PR.
+
+* Add ``buffer`` attribute to stdin stub class ``pytest.capture.DontReadFromInput``
+ Thanks :user:`joguSD` for the PR.
+
+* Fix ``UnicodeEncodeError`` when string comparison with unicode has failed. (:issue:`1864`)
+ Thanks :user:`AiOO` for the PR.
+
+* ``pytest_plugins`` is now handled correctly if defined as a string (as opposed as
+ a sequence of strings) when modules are considered for assertion rewriting.
+ Due to this bug, much more modules were being rewritten than necessary
+ if a test suite uses ``pytest_plugins`` to load internal plugins (:issue:`1888`).
+ Thanks :user:`jaraco` for the report and :user:`nicoddemus` for the PR (:pull:`1891`).
+
+* Do not call tearDown and cleanups when running tests from
+ ``unittest.TestCase`` subclasses with ``--pdb``
+ enabled. This allows proper post mortem debugging for all applications
+ which have significant logic in their tearDown machinery (:issue:`1890`). Thanks
+ :user:`mbyt` for the PR.
+
+* Fix use of deprecated ``getfuncargvalue`` method in the internal doctest plugin.
+ Thanks :user:`ViviCoder` for the report (:issue:`1898`).
+
+
+3.0.1 (2016-08-23)
+==================
+
+* Fix regression when ``importorskip`` is used at module level (:issue:`1822`).
+ Thanks :user:`jaraco` and :user:`The-Compiler` for the report and :user:`nicoddemus` for the PR.
+
+* Fix parametrization scope when session fixtures are used in conjunction
+ with normal parameters in the same call (:issue:`1832`).
+ Thanks :user:`The-Compiler` for the report, :user:`Kingdread` and :user:`nicoddemus` for the PR.
+
+* Fix internal error when parametrizing tests or fixtures using an empty ``ids`` argument (:issue:`1849`).
+ Thanks :user:`OPpuolitaival` for the report and :user:`nicoddemus` for the PR.
+
+* Fix loader error when running ``pytest`` embedded in a zipfile.
+ Thanks :user:`mbachry` for the PR.
+
+
+.. _release-3.0.0:
+
+3.0.0 (2016-08-18)
+==================
+
+**Incompatible changes**
+
+
+A number of incompatible changes were made in this release, with the intent of removing features deprecated for a long
+time or change existing behaviors in order to make them less surprising/more useful.
+
+* Reinterpretation mode has now been removed. Only plain and rewrite
+ mode are available, consequently the ``--assert=reinterp`` option is
+ no longer available. This also means files imported from plugins or
+ ``conftest.py`` will not benefit from improved assertions by
+ default, you should use ``pytest.register_assert_rewrite()`` to
+ explicitly turn on assertion rewriting for those files. Thanks
+ :user:`flub` for the PR.
+
+* The following deprecated commandline options were removed:
+
+ * ``--genscript``: no longer supported;
+ * ``--no-assert``: use ``--assert=plain`` instead;
+ * ``--nomagic``: use ``--assert=plain`` instead;
+ * ``--report``: use ``-r`` instead;
+
+ Thanks to :user:`RedBeardCode` for the PR (:pull:`1664`).
+
+* ImportErrors in plugins now are a fatal error instead of issuing a
+ pytest warning (:issue:`1479`). Thanks to :user:`The-Compiler` for the PR.
+
+* Removed support code for Python 3 versions < 3.3 (:pull:`1627`).
+
+* Removed all ``py.test-X*`` entry points. The versioned, suffixed entry points
+ were never documented and a leftover from a pre-virtualenv era. These entry
+ points also created broken entry points in wheels, so removing them also
+ removes a source of confusion for users (:issue:`1632`).
+ Thanks :user:`obestwalter` for the PR.
+
+* ``pytest.skip()`` now raises an error when used to decorate a test function,
+ as opposed to its original intent (to imperatively skip a test inside a test function). Previously
+ this usage would cause the entire module to be skipped (:issue:`607`).
+ Thanks :user:`omarkohl` for the complete PR (:pull:`1519`).
+
+* Exit tests if a collection error occurs. A poll indicated most users will hit CTRL-C
+ anyway as soon as they see collection errors, so pytest might as well make that the default behavior (:issue:`1421`).
+ A ``--continue-on-collection-errors`` option has been added to restore the previous behaviour.
+ Thanks :user:`olegpidsadnyi` and :user:`omarkohl` for the complete PR (:pull:`1628`).
+
+* Renamed the pytest ``pdb`` module (plugin) into ``debugging`` to avoid clashes with the builtin ``pdb`` module.
+
+* Raise a helpful failure message when requesting a parametrized fixture at runtime,
+ e.g. with ``request.getfixturevalue``. Previously these parameters were simply
+ never defined, so a fixture decorated like ``@pytest.fixture(params=[0, 1, 2])``
+ only ran once (:pull:`460`).
+ Thanks to :user:`nikratio` for the bug report, :user:`RedBeardCode` and :user:`tomviner` for the PR.
+
+* ``_pytest.monkeypatch.monkeypatch`` class has been renamed to ``_pytest.monkeypatch.MonkeyPatch``
+ so it doesn't conflict with the ``monkeypatch`` fixture.
+
+* ``--exitfirst / -x`` can now be overridden by a following ``--maxfail=N``
+ and is just a synonym for ``--maxfail=1``.
+
+
+**New Features**
+
+* Support nose-style ``__test__`` attribute on methods of classes,
+ including unittest-style Classes. If set to ``False``, the test will not be
+ collected.
+
+* New ``doctest_namespace`` fixture for injecting names into the
+ namespace in which doctests run.
+ Thanks :user:`milliams` for the complete PR (:pull:`1428`).
+
+* New ``--doctest-report`` option available to change the output format of diffs
+ when running (failing) doctests (implements :issue:`1749`).
+ Thanks :user:`hartym` for the PR.
+
+* New ``name`` argument to ``pytest.fixture`` decorator which allows a custom name
+ for a fixture (to solve the funcarg-shadowing-fixture problem).
+ Thanks :user:`novas0x2a` for the complete PR (:pull:`1444`).
+
+* New ``approx()`` function for easily comparing floating-point numbers in
+ tests.
+ Thanks :user:`kalekundert` for the complete PR (:pull:`1441`).
+
+* Ability to add global properties in the final xunit output file by accessing
+ the internal ``junitxml`` plugin (experimental).
+ Thanks :user:`tareqalayan` for the complete PR :pull:`1454`).
+
+* New ``ExceptionInfo.match()`` method to match a regular expression on the
+ string representation of an exception (:issue:`372`).
+ Thanks :user:`omarkohl` for the complete PR (:pull:`1502`).
+
+* ``__tracebackhide__`` can now also be set to a callable which then can decide
+ whether to filter the traceback based on the ``ExceptionInfo`` object passed
+ to it. Thanks :user:`The-Compiler` for the complete PR (:pull:`1526`).
+
+* New ``pytest_make_parametrize_id(config, val)`` hook which can be used by plugins to provide
+ friendly strings for custom types.
+ Thanks :user:`palaviv` for the PR.
+
+* ``capsys`` and ``capfd`` now have a ``disabled()`` context-manager method, which
+ can be used to temporarily disable capture within a test.
+ Thanks :user:`nicoddemus` for the PR.
+
+* New cli flag ``--fixtures-per-test``: shows which fixtures are being used
+ for each selected test item. Features doc strings of fixtures by default.
+ Can also show where fixtures are defined if combined with ``-v``.
+ Thanks :user:`hackebrot` for the PR.
+
+* Introduce ``pytest`` command as recommended entry point. Note that ``py.test``
+ still works and is not scheduled for removal. Closes proposal
+ :issue:`1629`. Thanks :user:`obestwalter` and :user:`davehunt` for the complete PR
+ (:pull:`1633`).
+
+* New cli flags:
+
+ + ``--setup-plan``: performs normal collection and reports
+ the potential setup and teardown and does not execute any fixtures and tests;
+ + ``--setup-only``: performs normal collection, executes setup and teardown of
+ fixtures and reports them;
+ + ``--setup-show``: performs normal test execution and additionally shows
+ setup and teardown of fixtures;
+ + ``--keep-duplicates``: py.test now ignores duplicated paths given in the command
+ line. To retain the previous behavior where the same test could be run multiple
+ times by specifying it in the command-line multiple times, pass the ``--keep-duplicates``
+ argument (:issue:`1609`);
+
+ Thanks :user:`d6e`, :user:`kvas-it`, :user:`sallner`, :user:`ioggstream` and :user:`omarkohl` for the PRs.
+
+* New CLI flag ``--override-ini``/``-o``: overrides values from the ini file.
+ For example: ``"-o xfail_strict=True"``'.
+ Thanks :user:`blueyed` and :user:`fengxx` for the PR.
+
+* New hooks:
+
+ + ``pytest_fixture_setup(fixturedef, request)``: executes fixture setup;
+ + ``pytest_fixture_post_finalizer(fixturedef)``: called after the fixture's
+ finalizer and has access to the fixture's result cache.
+
+ Thanks :user:`d6e`, :user:`sallner`.
+
+* Issue warnings for asserts whose test is a tuple literal. Such asserts will
+ never fail because tuples are always truthy and are usually a mistake
+ (see :issue:`1562`). Thanks :user:`kvas-it`, for the PR.
+
+* Allow passing a custom debugger class (e.g. ``--pdbcls=IPython.core.debugger:Pdb``).
+ Thanks to :user:`anntzer` for the PR.
+
+
+**Changes**
+
+* Plugins now benefit from assertion rewriting. Thanks
+ :user:`sober7`, :user:`nicoddemus` and :user:`flub` for the PR.
+
+* Change ``report.outcome`` for ``xpassed`` tests to ``"passed"`` in non-strict
+ mode and ``"failed"`` in strict mode. Thanks to :user:`hackebrot` for the PR
+ (:pull:`1795`) and :user:`gprasad84` for report (:issue:`1546`).
+
+* Tests marked with ``xfail(strict=False)`` (the default) now appear in
+ JUnitXML reports as passing tests instead of skipped.
+ Thanks to :user:`hackebrot` for the PR (:pull:`1795`).
+
+* Highlight path of the file location in the error report to make it easier to copy/paste.
+ Thanks :user:`suzaku` for the PR (:pull:`1778`).
+
+* Fixtures marked with ``@pytest.fixture`` can now use ``yield`` statements exactly like
+ those marked with the ``@pytest.yield_fixture`` decorator. This change renders
+ ``@pytest.yield_fixture`` deprecated and makes ``@pytest.fixture`` with ``yield`` statements
+ the preferred way to write teardown code (:pull:`1461`).
+ Thanks :user:`csaftoiu` for bringing this to attention and :user:`nicoddemus` for the PR.
+
+* Explicitly passed parametrize ids do not get escaped to ascii (:issue:`1351`).
+ Thanks :user:`ceridwen` for the PR.
+
+* Fixtures are now sorted in the error message displayed when an unknown
+ fixture is declared in a test function.
+ Thanks :user:`nicoddemus` for the PR.
+
+* ``pytest_terminal_summary`` hook now receives the ``exitstatus``
+ of the test session as argument. Thanks :user:`blueyed` for the PR (:pull:`1809`).
+
+* Parametrize ids can accept ``None`` as specific test id, in which case the
+ automatically generated id for that argument will be used.
+ Thanks :user:`palaviv` for the complete PR (:pull:`1468`).
+
+* The parameter to xunit-style setup/teardown methods (``setup_method``,
+ ``setup_module``, etc.) is now optional and may be omitted.
+ Thanks :user:`okken` for bringing this to attention and :user:`nicoddemus` for the PR.
+
+* Improved automatic id generation selection in case of duplicate ids in
+ parametrize.
+ Thanks :user:`palaviv` for the complete PR (:pull:`1474`).
+
+* Now pytest warnings summary is shown up by default. Added a new flag
+ ``--disable-pytest-warnings`` to explicitly disable the warnings summary (:issue:`1668`).
+
+* Make ImportError during collection more explicit by reminding
+ the user to check the name of the test module/package(s) (:issue:`1426`).
+ Thanks :user:`omarkohl` for the complete PR (:pull:`1520`).
+
+* Add ``build/`` and ``dist/`` to the default ``--norecursedirs`` list. Thanks
+ :user:`mikofski` for the report and :user:`tomviner` for the PR (:issue:`1544`).
+
+* ``pytest.raises`` in the context manager form accepts a custom
+ ``message`` to raise when no exception occurred.
+ Thanks :user:`palaviv` for the complete PR (:pull:`1616`).
+
+* ``conftest.py`` files now benefit from assertion rewriting; previously it
+ was only available for test modules. Thanks :user:`flub`, :user:`sober7` and
+ :user:`nicoddemus` for the PR (:issue:`1619`).
+
+* Text documents without any doctests no longer appear as "skipped".
+ Thanks :user:`graingert` for reporting and providing a full PR (:pull:`1580`).
+
+* Ensure that a module within a namespace package can be found when it
+ is specified on the command line together with the ``--pyargs``
+ option. Thanks to :user:`taschini` for the PR (:pull:`1597`).
+
+* Always include full assertion explanation during assertion rewriting. The previous behaviour was hiding
+ sub-expressions that happened to be ``False``, assuming this was redundant information.
+ Thanks :user:`bagerard` for reporting (:issue:`1503`). Thanks to :user:`davehunt` and
+ :user:`tomviner` for the PR.
+
+* ``OptionGroup.addoption()`` now checks if option names were already
+ added before, to make it easier to track down issues like :issue:`1618`.
+ Before, you only got exceptions later from ``argparse`` library,
+ giving no clue about the actual reason for double-added options.
+
+* ``yield``-based tests are considered deprecated and will be removed in pytest-4.0.
+ Thanks :user:`nicoddemus` for the PR.
+
+* ``[pytest]`` sections in ``setup.cfg`` files should now be named ``[tool:pytest]``
+ to avoid conflicts with other distutils commands (see :pull:`567`). ``[pytest]`` sections in
+ ``pytest.ini`` or ``tox.ini`` files are supported and unchanged.
+ Thanks :user:`nicoddemus` for the PR.
+
+* Using ``pytest_funcarg__`` prefix to declare fixtures is considered deprecated and will be
+ removed in pytest-4.0 (:pull:`1684`).
+ Thanks :user:`nicoddemus` for the PR.
+
+* Passing a command-line string to ``pytest.main()`` is considered deprecated and scheduled
+ for removal in pytest-4.0. It is recommended to pass a list of arguments instead (:pull:`1723`).
+
+* Rename ``getfuncargvalue`` to ``getfixturevalue``. ``getfuncargvalue`` is
+ still present but is now considered deprecated. Thanks to :user:`RedBeardCode` and :user:`tomviner`
+ for the PR (:pull:`1626`).
+
+* ``optparse`` type usage now triggers DeprecationWarnings (:issue:`1740`).
+
+
+* ``optparse`` backward compatibility supports float/complex types (:issue:`457`).
+
+* Refined logic for determining the ``rootdir``, considering only valid
+ paths which fixes a number of issues: :issue:`1594`, :issue:`1435` and :issue:`1471`.
+ Updated the documentation according to current behavior. Thanks to
+ :user:`blueyed`, :user:`davehunt` and :user:`matthiasha` for the PR.
+
+* Always include full assertion explanation. The previous behaviour was hiding
+ sub-expressions that happened to be False, assuming this was redundant information.
+ Thanks :user:`bagerard` for reporting (:issue:`1503`). Thanks to :user:`davehunt` and
+ :user:`tomviner` for PR.
+
+* Better message in case of not using parametrized variable (see :issue:`1539`).
+ Thanks to :user:`tramwaj29` for the PR.
+
+* Updated docstrings with a more uniform style.
+
+* Add stderr write for ``pytest.exit(msg)`` during startup. Previously the message was never shown.
+ Thanks :user:`BeyondEvil` for reporting :issue:`1210`. Thanks to @jgsonesen and
+ :user:`tomviner` for the PR.
+
+* No longer display the incorrect test deselection reason (:issue:`1372`).
+ Thanks :user:`ronnypfannschmidt` for the PR.
+
+* The ``--resultlog`` command line option has been deprecated: it is little used
+ and there are more modern and better alternatives (see :issue:`830`).
+ Thanks :user:`nicoddemus` for the PR.
+
+* Improve error message with fixture lookup errors: add an 'E' to the first
+ line and '>' to the rest. Fixes :issue:`717`. Thanks :user:`blueyed` for reporting and
+ a PR, :user:`eolo999` for the initial PR and :user:`tomviner` for his guidance during
+ EuroPython2016 sprint.
+
+
+**Bug Fixes**
+
+* Parametrize now correctly handles duplicated test ids.
+
+* Fix internal error issue when the ``method`` argument is missing for
+ ``teardown_method()`` (:issue:`1605`).
+
+* Fix exception visualization in case the current working directory (CWD) gets
+ deleted during testing (:issue:`1235`). Thanks :user:`bukzor` for reporting. PR by
+ :user:`marscher`.
+
+* Improve test output for logical expression with brackets (:issue:`925`).
+ Thanks :user:`DRMacIver` for reporting and :user:`RedBeardCode` for the PR.
+
+* Create correct diff for strings ending with newlines (:issue:`1553`).
+ Thanks :user:`Vogtinator` for reporting and :user:`RedBeardCode` and
+ :user:`tomviner` for the PR.
+
+* ``ConftestImportFailure`` now shows the traceback making it easier to
+ identify bugs in ``conftest.py`` files (:pull:`1516`). Thanks :user:`txomon` for
+ the PR.
+
+* Text documents without any doctests no longer appear as "skipped".
+ Thanks :user:`graingert` for reporting and providing a full PR (:pull:`1580`).
+
+* Fixed collection of classes with custom ``__new__`` method.
+ Fixes :issue:`1579`. Thanks to :user:`Stranger6667` for the PR.
+
+* Fixed scope overriding inside metafunc.parametrize (:issue:`634`).
+ Thanks to :user:`Stranger6667` for the PR.
+
+* Fixed the total tests tally in junit xml output (:pull:`1798`).
+ Thanks to :user:`cboelsen` for the PR.
+
+* Fixed off-by-one error with lines from ``request.node.warn``.
+ Thanks to :user:`blueyed` for the PR.
+
+
+2.9.2 (2016-05-31)
+==================
+
+**Bug Fixes**
+
+* fix :issue:`510`: skip tests where one parameterize dimension was empty
+ thanks Alex Stapleton for the Report and :user:`RonnyPfannschmidt` for the PR
+
+* Fix Xfail does not work with condition keyword argument.
+ Thanks :user:`astraw38` for reporting the issue (:issue:`1496`) and :user:`tomviner`
+ for PR the (:pull:`1524`).
+
+* Fix win32 path issue when putting custom config file with absolute path
+ in ``pytest.main("-c your_absolute_path")``.
+
+* Fix maximum recursion depth detection when raised error class is not aware
+ of unicode/encoded bytes.
+ Thanks :user:`prusse-martin` for the PR (:pull:`1506`).
+
+* Fix ``pytest.mark.skip`` mark when used in strict mode.
+ Thanks :user:`pquentin` for the PR and :user:`RonnyPfannschmidt` for
+ showing how to fix the bug.
+
+* Minor improvements and fixes to the documentation.
+ Thanks :user:`omarkohl` for the PR.
+
+* Fix ``--fixtures`` to show all fixture definitions as opposed to just
+ one per fixture name.
+ Thanks to :user:`hackebrot` for the PR.
+
+
+2.9.1 (2016-03-17)
+==================
+
+**Bug Fixes**
+
+* Improve error message when a plugin fails to load.
+ Thanks :user:`nicoddemus` for the PR.
+
+* Fix (:issue:`1178`):
+ ``pytest.fail`` with non-ascii characters raises an internal pytest error.
+ Thanks :user:`nicoddemus` for the PR.
+
+* Fix (:issue:`469`): junit parses report.nodeid incorrectly, when params IDs
+ contain ``::``. Thanks :user:`tomviner` for the PR (:pull:`1431`).
+
+* Fix (:issue:`578`): SyntaxErrors
+ containing non-ascii lines at the point of failure generated an internal
+ py.test error.
+ Thanks :user:`asottile` for the report and :user:`nicoddemus` for the PR.
+
+* Fix (:issue:`1437`): When passing in a bytestring regex pattern to parameterize
+ attempt to decode it as utf-8 ignoring errors.
+
+* Fix (:issue:`649`): parametrized test nodes cannot be specified to run on the command line.
+
+* Fix (:issue:`138`): better reporting for python 3.3+ chained exceptions
+
+
+2.9.0 (2016-02-29)
+==================
+
+**New Features**
+
+* New ``pytest.mark.skip`` mark, which unconditionally skips marked tests.
+ Thanks :user:`MichaelAquilina` for the complete PR (:pull:`1040`).
+
+* ``--doctest-glob`` may now be passed multiple times in the command-line.
+ Thanks :user:`jab` and :user:`nicoddemus` for the PR.
+
+* New ``-rp`` and ``-rP`` reporting options give the summary and full output
+ of passing tests, respectively. Thanks to :user:`codewarrior0` for the PR.
+
+* ``pytest.mark.xfail`` now has a ``strict`` option, which makes ``XPASS``
+ tests to fail the test suite (defaulting to ``False``). There's also a
+ ``xfail_strict`` ini option that can be used to configure it project-wise.
+ Thanks :user:`rabbbit` for the request and :user:`nicoddemus` for the PR (:pull:`1355`).
+
+* ``Parser.addini`` now supports options of type ``bool``.
+ Thanks :user:`nicoddemus` for the PR.
+
+* New ``ALLOW_BYTES`` doctest option. This strips ``b`` prefixes from byte strings
+ in doctest output (similar to ``ALLOW_UNICODE``).
+ Thanks :user:`jaraco` for the request and :user:`nicoddemus` for the PR (:pull:`1287`).
+
+* Give a hint on ``KeyboardInterrupt`` to use the ``--fulltrace`` option to show the errors.
+ Fixes :issue:`1366`.
+ Thanks to :user:`hpk42` for the report and :user:`RonnyPfannschmidt` for the PR.
+
+* Catch ``IndexError`` exceptions when getting exception source location.
+ Fixes a pytest internal error for dynamically generated code (fixtures and tests)
+ where source lines are fake by intention.
+
+**Changes**
+
+* **Important**: `py.code <https://pylib.readthedocs.io/en/stable/code.html>`_ has been
+ merged into the ``pytest`` repository as ``pytest._code``. This decision
+ was made because ``py.code`` had very few uses outside ``pytest`` and the
+ fact that it was in a different repository made it difficult to fix bugs on
+ its code in a timely manner. The team hopes with this to be able to better
+ refactor out and improve that code.
+ This change shouldn't affect users, but it is useful to let users aware
+ if they encounter any strange behavior.
+
+ Keep in mind that the code for ``pytest._code`` is **private** and
+ **experimental**, so you definitely should not import it explicitly!
+
+ Please note that the original ``py.code`` is still available in
+ `pylib <https://pylib.readthedocs.io>`_.
+
+* ``pytest_enter_pdb`` now optionally receives the pytest config object.
+ Thanks :user:`nicoddemus` for the PR.
+
+* Removed code and documentation for Python 2.5 or lower versions,
+ including removal of the obsolete ``_pytest.assertion.oldinterpret`` module.
+ Thanks :user:`nicoddemus` for the PR (:pull:`1226`).
+
+* Comparisons now always show up in full when ``CI`` or ``BUILD_NUMBER`` is
+ found in the environment, even when ``-vv`` isn't used.
+ Thanks :user:`The-Compiler` for the PR.
+
+* ``--lf`` and ``--ff`` now support long names: ``--last-failed`` and
+ ``--failed-first`` respectively.
+ Thanks :user:`MichaelAquilina` for the PR.
+
+* Added expected exceptions to ``pytest.raises`` fail message.
+
+* Collection only displays progress ("collecting X items") when in a terminal.
+ This avoids cluttering the output when using ``--color=yes`` to obtain
+ colors in CI integrations systems (:issue:`1397`).
+
+**Bug Fixes**
+
+* The ``-s`` and ``-c`` options should now work under ``xdist``;
+ ``Config.fromdictargs`` now represents its input much more faithfully.
+ Thanks to :user:`bukzor` for the complete PR (:issue:`680`).
+
+* Fix (:issue:`1290`): support Python 3.5's ``@`` operator in assertion rewriting.
+ Thanks :user:`Shinkenjoe` for report with test case and :user:`tomviner` for the PR.
+
+* Fix formatting utf-8 explanation messages (:issue:`1379`).
+ Thanks :user:`biern` for the PR.
+
+* Fix :ref:`traceback style docs <how-to-modifying-python-tb-printing>` to describe all of the available options
+ (auto/long/short/line/native/no), with ``auto`` being the default since v2.6.
+ Thanks :user:`hackebrot` for the PR.
+
+* Fix (:issue:`1422`): junit record_xml_property doesn't allow multiple records
+ with same name.
+
+
+2.8.7 (2016-01-24)
+==================
+
+- fix #1338: use predictable object resolution for monkeypatch
+
+2.8.6 (2016-01-21)
+==================
+
+- fix #1259: allow for double nodeids in junitxml,
+ this was a regression failing plugins combinations
+ like pytest-pep8 + pytest-flakes
+
+- Workaround for exception that occurs in pyreadline when using
+ ``--pdb`` with standard I/O capture enabled.
+ Thanks Erik M. Bray for the PR.
+
+- fix #900: Better error message in case the target of a ``monkeypatch`` call
+ raises an ``ImportError``.
+
+- fix #1292: monkeypatch calls (setattr, setenv, etc.) are now O(1).
+ Thanks David R. MacIver for the report and Bruno Oliveira for the PR.
+
+- fix #1223: captured stdout and stderr are now properly displayed before
+ entering pdb when ``--pdb`` is used instead of being thrown away.
+ Thanks Cal Leeming for the PR.
+
+- fix #1305: pytest warnings emitted during ``pytest_terminal_summary`` are now
+ properly displayed.
+ Thanks Ionel Maries Cristian for the report and Bruno Oliveira for the PR.
+
+- fix #628: fixed internal UnicodeDecodeError when doctests contain unicode.
+ Thanks Jason R. Coombs for the report and Bruno Oliveira for the PR.
+
+- fix #1334: Add captured stdout to jUnit XML report on setup error.
+ Thanks Georgy Dyuldin for the PR.
+
+
+2.8.5 (2015-12-11)
+==================
+
+- fix #1243: fixed issue where class attributes injected during collection could break pytest.
+ PR by Alexei Kozlenok, thanks Ronny Pfannschmidt and Bruno Oliveira for the review and help.
+
+- fix #1074: precompute junitxml chunks instead of storing the whole tree in objects
+ Thanks Bruno Oliveira for the report and Ronny Pfannschmidt for the PR
+
+- fix #1238: fix ``pytest.deprecated_call()`` receiving multiple arguments
+ (Regression introduced in 2.8.4). Thanks Alex Gaynor for the report and
+ Bruno Oliveira for the PR.
+
+
+2.8.4 (2015-12-06)
+==================
+
+- fix #1190: ``deprecated_call()`` now works when the deprecated
+ function has been already called by another test in the same
+ module. Thanks Mikhail Chernykh for the report and Bruno Oliveira for the
+ PR.
+
+- fix #1198: ``--pastebin`` option now works on Python 3. Thanks
+ Mehdy Khoshnoody for the PR.
+
+- fix #1219: ``--pastebin`` now works correctly when captured output contains
+ non-ascii characters. Thanks Bruno Oliveira for the PR.
+
+- fix #1204: another error when collecting with a nasty __getattr__().
+ Thanks Florian Bruhin for the PR.
+
+- fix the summary printed when no tests did run.
+ Thanks Florian Bruhin for the PR.
+- fix #1185 - ensure MANIFEST.in exactly matches what should go to a sdist
+
+- a number of documentation modernizations wrt good practices.
+ Thanks Bruno Oliveira for the PR.
+
+2.8.3 (2015-11-18)
+==================
+
+- fix #1169: add __name__ attribute to testcases in TestCaseFunction to
+ support the @unittest.skip decorator on functions and methods.
+ Thanks Lee Kamentsky for the PR.
+
+- fix #1035: collecting tests if test module level obj has __getattr__().
+ Thanks Suor for the report and Bruno Oliveira / Tom Viner for the PR.
+
+- fix #331: don't collect tests if their failure cannot be reported correctly
+ e.g. they are a callable instance of a class.
+
+- fix #1133: fixed internal error when filtering tracebacks where one entry
+ belongs to a file which is no longer available.
+ Thanks Bruno Oliveira for the PR.
+
+- enhancement made to highlight in red the name of the failing tests so
+ they stand out in the output.
+ Thanks Gabriel Reis for the PR.
+
+- add more talks to the documentation
+- extend documentation on the --ignore cli option
+- use pytest-runner for setuptools integration
+- minor fixes for interaction with OS X El Capitan
+ system integrity protection (thanks Florian)
+
+
+2.8.2 (2015-10-07)
+==================
+
+- fix #1085: proper handling of encoding errors when passing encoded byte
+ strings to pytest.parametrize in Python 2.
+ Thanks Themanwithoutaplan for the report and Bruno Oliveira for the PR.
+
+- fix #1087: handling SystemError when passing empty byte strings to
+ pytest.parametrize in Python 3.
+ Thanks Paul Kehrer for the report and Bruno Oliveira for the PR.
+
+- fix #995: fixed internal error when filtering tracebacks where one entry
+ was generated by an exec() statement.
+ Thanks Daniel Hahler, Ashley C Straw, Philippe Gauthier and Pavel Savchenko
+ for contributing and Bruno Oliveira for the PR.
+
+- fix #1100 and #1057: errors when using autouse fixtures and doctest modules.
+ Thanks Sergey B Kirpichev and Vital Kudzelka for contributing and Bruno
+ Oliveira for the PR.
+
+2.8.1 (2015-09-29)
+==================
+
+- fix #1034: Add missing nodeid on pytest_logwarning call in
+ addhook. Thanks Simon Gomizelj for the PR.
+
+- 'deprecated_call' is now only satisfied with a DeprecationWarning or
+ PendingDeprecationWarning. Before 2.8.0, it accepted any warning, and 2.8.0
+ made it accept only DeprecationWarning (but not PendingDeprecationWarning).
+ Thanks Alex Gaynor for the issue and Eric Hunsberger for the PR.
+
+- fix issue #1073: avoid calling __getattr__ on potential plugin objects.
+ This fixes an incompatibility with pytest-django. Thanks Andreas Pelme,
+ Bruno Oliveira and Ronny Pfannschmidt for contributing and Holger Krekel
+ for the fix.
+
+- Fix issue #704: handle versionconflict during plugin loading more
+ gracefully. Thanks Bruno Oliveira for the PR.
+
+- Fix issue #1064: ""--junitxml" regression when used with the
+ "pytest-xdist" plugin, with test reports being assigned to the wrong tests.
+ Thanks Daniel Grunwald for the report and Bruno Oliveira for the PR.
+
+- (experimental) adapt more SEMVER style versioning and change meaning of
+ master branch in git repo: "master" branch now keeps the bug fixes, changes
+ aimed for micro releases. "features" branch will only be released
+ with minor or major pytest releases.
+
+- Fix issue #766 by removing documentation references to distutils.
+ Thanks Russel Winder.
+
+- Fix issue #1030: now byte-strings are escaped to produce item node ids
+ to make them always serializable.
+ Thanks Andy Freeland for the report and Bruno Oliveira for the PR.
+
+- Python 2: if unicode parametrized values are convertible to ascii, their
+ ascii representation is used for the node id.
+
+- Fix issue #411: Add __eq__ method to assertion comparison example.
+ Thanks Ben Webb.
+- Fix issue #653: deprecated_call can be used as context manager.
+
+- fix issue 877: properly handle assertion explanations with non-ascii repr
+ Thanks Mathieu Agopian for the report and Ronny Pfannschmidt for the PR.
+
+- fix issue 1029: transform errors when writing cache values into pytest-warnings
+
+2.8.0 (2015-09-18)
+==================
+
+- new ``--lf`` and ``-ff`` options to run only the last failing tests or
+ "failing tests first" from the last run. This functionality is provided
+ through porting the formerly external pytest-cache plugin into pytest core.
+ BACKWARD INCOMPAT: if you used pytest-cache's functionality to persist
+ data between test runs be aware that we don't serialize sets anymore.
+ Thanks Ronny Pfannschmidt for most of the merging work.
+
+- "-r" option now accepts "a" to include all possible reports, similar
+ to passing "fEsxXw" explicitly (issue960).
+ Thanks Abhijeet Kasurde for the PR.
+
+- avoid python3.5 deprecation warnings by introducing version
+ specific inspection helpers, thanks Michael Droettboom.
+
+- fix issue562: @nose.tools.istest now fully respected.
+
+- fix issue934: when string comparison fails and a diff is too large to display
+ without passing -vv, still show a few lines of the diff.
+ Thanks Florian Bruhin for the report and Bruno Oliveira for the PR.
+
+- fix issue736: Fix a bug where fixture params would be discarded when combined
+ with parametrization markers.
+ Thanks to Markus Unterwaditzer for the PR.
+
+- fix issue710: introduce ALLOW_UNICODE doctest option: when enabled, the
+ ``u`` prefix is stripped from unicode strings in expected doctest output. This
+ allows doctests which use unicode to run in Python 2 and 3 unchanged.
+ Thanks Jason R. Coombs for the report and Bruno Oliveira for the PR.
+
+- parametrize now also generates meaningful test IDs for enum, regex and class
+ objects (as opposed to class instances).
+ Thanks to Florian Bruhin for the PR.
+
+- Add 'warns' to assert that warnings are thrown (like 'raises').
+ Thanks to Eric Hunsberger for the PR.
+
+- Fix issue683: Do not apply an already applied mark. Thanks ojake for the PR.
+
+- Deal with capturing failures better so fewer exceptions get lost to
+ /dev/null. Thanks David Szotten for the PR.
+
+- fix issue730: deprecate and warn about the --genscript option.
+ Thanks Ronny Pfannschmidt for the report and Christian Pommranz for the PR.
+
+- fix issue751: multiple parametrize with ids bug if it parametrizes class with
+ two or more test methods. Thanks Sergey Chipiga for reporting and Jan
+ Bednarik for PR.
+
+- fix issue82: avoid loading conftest files from setup.cfg/pytest.ini/tox.ini
+ files and upwards by default (--confcutdir can still be set to override this).
+ Thanks Bruno Oliveira for the PR.
+
+- fix issue768: docstrings found in python modules were not setting up session
+ fixtures. Thanks Jason R. Coombs for reporting and Bruno Oliveira for the PR.
+
+- added ``tmpdir_factory``, a session-scoped fixture that can be used to create
+ directories under the base temporary directory. Previously this object was
+ installed as a ``_tmpdirhandler`` attribute of the ``config`` object, but now it
+ is part of the official API and using ``config._tmpdirhandler`` is
+ deprecated.
+ Thanks Bruno Oliveira for the PR.
+
+- fix issue808: pytest's internal assertion rewrite hook now implements the
+ optional :pep:`302` get_data API so tests can access data files next to them.
+ Thanks xmo-odoo for request and example and Bruno Oliveira for
+ the PR.
+
+- rootdir and inifile are now displayed during usage errors to help
+ users diagnose problems such as unexpected ini files which add
+ unknown options being picked up by pytest. Thanks to Pavel Savchenko for
+ bringing the problem to attention in #821 and Bruno Oliveira for the PR.
+
+- Summary bar now is colored yellow for warning
+ situations such as: all tests either were skipped or xpass/xfailed,
+ or no tests were run at all (this is a partial fix for issue500).
+
+- fix issue812: pytest now exits with status code 5 in situations where no
+ tests were run at all, such as the directory given in the command line does
+ not contain any tests or as result of a command line option filters
+ all out all tests (-k for example).
+ Thanks Eric Siegerman (issue812) and Bruno Oliveira for the PR.
+
+- Summary bar now is colored yellow for warning
+ situations such as: all tests either were skipped or xpass/xfailed,
+ or no tests were run at all (related to issue500).
+ Thanks Eric Siegerman.
+
+- New ``testpaths`` ini option: list of directories to search for tests
+ when executing pytest from the root directory. This can be used
+ to speed up test collection when a project has well specified directories
+ for tests, being usually more practical than configuring norecursedirs for
+ all directories that do not contain tests.
+ Thanks to Adrian for idea (#694) and Bruno Oliveira for the PR.
+
+- fix issue713: JUnit XML reports for doctest failures.
+ Thanks Punyashloka Biswal.
+
+- fix issue970: internal pytest warnings now appear as "pytest-warnings" in
+ the terminal instead of "warnings", so it is clear for users that those
+ warnings are from pytest and not from the builtin "warnings" module.
+ Thanks Bruno Oliveira.
+
+- Include setup and teardown in junitxml test durations.
+ Thanks Janne Vanhala.
+
+- fix issue735: assertion failures on debug versions of Python 3.4+
+
+- new option ``--import-mode`` to allow to change test module importing
+ behaviour to append to sys.path instead of prepending. This better allows
+ to run test modules against installed versions of a package even if the
+ package under test has the same import root. In this example::
+
+ testing/__init__.py
+ testing/test_pkg_under_test.py
+ pkg_under_test/
+
+ the tests will run against the installed version
+ of pkg_under_test when ``--import-mode=append`` is used whereas
+ by default they would always pick up the local version. Thanks Holger Krekel.
+
+- pytester: add method ``TmpTestdir.delete_loaded_modules()``, and call it
+ from ``inline_run()`` to allow temporary modules to be reloaded.
+ Thanks Eduardo Schettino.
+
+- internally refactor pluginmanager API and code so that there
+ is a clear distinction between a pytest-agnostic rather simple
+ pluginmanager and the PytestPluginManager which adds a lot of
+ behaviour, among it handling of the local conftest files.
+ In terms of documented methods this is a backward compatible
+ change but it might still break 3rd party plugins which relied on
+ details like especially the pluginmanager.add_shutdown() API.
+ Thanks Holger Krekel.
+
+- pluginmanagement: introduce ``pytest.hookimpl`` and
+ ``pytest.hookspec`` decorators for setting impl/spec
+ specific parameters. This substitutes the previous
+ now deprecated use of ``pytest.mark`` which is meant to
+ contain markers for test functions only.
+
+- write/refine docs for "writing plugins" which now have their
+ own page and are separate from the "using/installing plugins`` page.
+
+- fix issue732: properly unregister plugins from any hook calling
+ sites allowing to have temporary plugins during test execution.
+
+- deprecate and warn about ``__multicall__`` argument in hook
+ implementations. Use the ``hookwrapper`` mechanism instead already
+ introduced with pytest-2.7.
+
+- speed up pytest's own test suite considerably by using inprocess
+ tests by default (testrun can be modified with --runpytest=subprocess
+ to create subprocesses in many places instead). The main
+ APIs to run pytest in a test is "runpytest()" or "runpytest_subprocess"
+ and "runpytest_inprocess" if you need a particular way of running
+ the test. In all cases you get back a RunResult but the inprocess
+ 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.
+
+- fix issue741: make running output from testdir.run copy/pasteable
+ Thanks Bruno Oliveira.
+
+- add a new ``--noconftest`` argument which ignores all ``conftest.py`` files.
+
+- add ``file`` and ``line`` attributes to JUnit-XML output.
+
+- fix issue890: changed extension of all documentation files from ``txt`` to
+ ``rst``. Thanks to Abhijeet for the PR.
+
+- fix issue714: add ability to apply indirect=True parameter on particular argnames.
+ Thanks Elizaveta239.
+
+- fix issue890: changed extension of all documentation files from ``txt`` to
+ ``rst``. Thanks to Abhijeet for the PR.
+
+- fix issue957: "# doctest: SKIP" option will now register doctests as SKIPPED
+ rather than PASSED.
+ Thanks Thomas Grainger for the report and Bruno Oliveira for the PR.
+
+- issue951: add new record_xml_property fixture, that supports logging
+ additional information on xml output. Thanks David Diaz for the PR.
+
+- issue949: paths after normal options (for example ``-s``, ``-v``, etc) are now
+ properly used to discover ``rootdir`` and ``ini`` files.
+ Thanks Peter Lauri for the report and Bruno Oliveira for the PR.
+
+2.7.3 (2015-09-15)
+==================
+
+- Allow 'dev', 'rc', or other non-integer version strings in ``importorskip``.
+ Thanks to Eric Hunsberger for the PR.
+
+- fix issue856: consider --color parameter in all outputs (for example
+ --fixtures). Thanks Barney Gale for the report and Bruno Oliveira for the PR.
+
+- fix issue855: passing str objects as ``plugins`` argument to pytest.main
+ is now interpreted as a module name to be imported and registered as a
+ plugin, instead of silently having no effect.
+ Thanks xmo-odoo for the report and Bruno Oliveira for the PR.
+
+- fix issue744: fix for ast.Call changes in Python 3.5+. Thanks
+ Guido van Rossum, Matthias Bussonnier, Stefan Zimmermann and
+ Thomas Kluyver.
+
+- fix issue842: applying markers in classes no longer propagate this markers
+ to superclasses which also have markers.
+ Thanks xmo-odoo for the report and Bruno Oliveira for the PR.
+
+- preserve warning functions after call to pytest.deprecated_call. Thanks
+ Pieter Mulder for PR.
+
+- 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.
+
+- fix issue833: --fixtures now shows all fixtures of collected test files, instead of just the
+ fixtures declared on the first one.
+ Thanks Florian Bruhin for reporting and Bruno Oliveira for the PR.
+
+- fix issue863: skipped tests now report the correct reason when a skip/xfail
+ condition is met when using multiple markers.
+ Thanks Raphael Pierzina for reporting and Bruno Oliveira for the PR.
+
+- optimized tmpdir fixture initialization, which should make test sessions
+ faster (specially when using pytest-xdist). The only visible effect
+ is that now pytest uses a subdirectory in the $TEMP directory for all
+ directories created by this fixture (defaults to $TEMP/pytest-$USER).
+ Thanks Bruno Oliveira for the PR.
+
+2.7.2 (2015-06-23)
+==================
+
+- fix issue767: pytest.raises value attribute does not contain the exception
+ instance on Python 2.6. Thanks Eric Siegerman for providing the test
+ case and Bruno Oliveira for PR.
+
+- Automatically create directory for junitxml and results log.
+ Thanks Aron Curzon.
+
+- fix issue713: JUnit XML reports for doctest failures.
+ Thanks Punyashloka Biswal.
+
+- fix issue735: assertion failures on debug versions of Python 3.4+
+ Thanks Benjamin Peterson.
+
+- fix issue114: skipif marker reports to internal skipping plugin;
+ Thanks Floris Bruynooghe for reporting and Bruno Oliveira for the PR.
+
+- fix issue748: unittest.SkipTest reports to internal pytest unittest plugin.
+ Thanks Thomas De Schampheleire for reporting and Bruno Oliveira for the PR.
+
+- fix issue718: failed to create representation of sets containing unsortable
+ elements in python 2. Thanks Edison Gustavo Muenz.
+
+- fix issue756, fix issue752 (and similar issues): depend on py-1.4.29
+ which has a refined algorithm for traceback generation.
+
+
+2.7.1 (2015-05-19)
+==================
+
+- fix issue731: do not get confused by the braces which may be present
+ and unbalanced in an object's repr while collapsing False
+ explanations. Thanks Carl Meyer for the report and test case.
+
+- fix issue553: properly handling inspect.getsourcelines failures in
+ FixtureLookupError which would lead to an internal error,
+ obfuscating the original problem. Thanks talljosh for initial
+ diagnose/patch and Bruno Oliveira for final patch.
+
+- fix issue660: properly report scope-mismatch-access errors
+ independently from ordering of fixture arguments. Also
+ avoid the pytest internal traceback which does not provide
+ information to the user. Thanks Holger Krekel.
+
+- streamlined and documented release process. Also all versions
+ (in setup.py and documentation generation) are now read
+ from _pytest/__init__.py. Thanks Holger Krekel.
+
+- fixed docs to remove the notion that yield-fixtures are experimental.
+ They are here to stay :) Thanks Bruno Oliveira.
+
+- Support building wheels by using environment markers for the
+ requirements. Thanks Ionel Maries Cristian.
+
+- fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing
+ when tests raised SystemExit. Thanks Holger Krekel.
+
+- reintroduced _pytest fixture of the pytester plugin which is used
+ at least by pytest-xdist.
+
+2.7.0 (2015-03-26)
+==================
+
+- fix issue435: make reload() work when assert rewriting is active.
+ Thanks Daniel Hahler.
+
+- fix issue616: conftest.py files and their contained fixtures are now
+ properly considered for visibility, independently from the exact
+ current working directory and test arguments that are used.
+ Many thanks to Eric Siegerman and his PR235 which contains
+ systematic tests for conftest visibility and now passes.
+ This change also introduces the concept of a ``rootdir`` which
+ is printed as a new pytest header and documented in the pytest
+ customize web page.
+
+- change reporting of "diverted" tests, i.e. tests that are collected
+ in one file but actually come from another (e.g. when tests in a test class
+ come from a base class in a different file). We now show the nodeid
+ and indicate via a postfix the other file.
+
+- add ability to set command line options by environment variable PYTEST_ADDOPTS.
+
+- added documentation on the new pytest-dev teams on bitbucket and
+ 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
+ will turn import errors in doctests into skips. Thanks Charles Cloud
+ for the complete PR.
+
+- fix issue655: work around different ways that cause python2/3
+ to leak sys.exc_info into fixtures/tests causing failures in 3rd party code
+
+- fix issue615: assertion rewriting did not correctly escape % signs
+ when formatting boolean operations, which tripped over mixing
+ booleans with modulo operators. Thanks to Tom Viner for the report,
+ triaging and fix.
+
+- implement issue351: add ability to specify parametrize ids as a callable
+ to generate custom test ids. Thanks Brianna Laugher for the idea and
+ implementation.
+
+- introduce and document new hookwrapper mechanism useful for plugins
+ which want to wrap the execution of certain hooks for their purposes.
+ This supersedes the undocumented ``__multicall__`` protocol which
+ pytest itself and some external plugins use. Note that pytest-2.8
+ is scheduled to drop supporting the old ``__multicall__``
+ and only support the hookwrapper protocol.
+
+- majorly speed up invocation of plugin hooks
+
+- use hookwrapper mechanism in builtin pytest plugins.
+
+- add a doctest ini option for doctest flags, thanks Holger Peters.
+
+- add note to docs that if you want to mark a parameter and the
+ parameter is a callable, you also need to pass in a reason to disambiguate
+ it from the "decorator" case. Thanks Tom Viner.
+
+- "python_classes" and "python_functions" options now support glob-patterns
+ for test discovery, as discussed in issue600. Thanks Ldiary Translations.
+
+- allow to override parametrized fixtures with non-parametrized ones and vice versa (bubenkoff).
+
+- fix issue463: raise specific error for 'parameterize' misspelling (pfctdayelise).
+
+- On failure, the ``sys.last_value``, ``sys.last_type`` and
+ ``sys.last_traceback`` are set, so that a user can inspect the error
+ via postmortem debugging (almarklein).
+
+2.6.4 (2014-10-24)
+==================
+
+- Improve assertion failure reporting on iterables, by using ndiff and
+ pprint.
+
+- removed outdated japanese docs from source tree.
+
+- docs for "pytest_addhooks" hook. Thanks Bruno Oliveira.
+
+- updated plugin index docs. Thanks Bruno Oliveira.
+
+- fix issue557: with "-k" we only allow the old style "-" for negation
+ at the beginning of strings and even that is deprecated. Use "not" instead.
+ This should allow to pick parametrized tests where "-" appeared in the parameter.
+
+- fix issue604: Escape % character in the assertion message.
+
+- fix issue620: add explanation in the --genscript target about what
+ the binary blob means. Thanks Dinu Gherman.
+
+- fix issue614: fixed pastebin support.
+
+
+- fix issue620: add explanation in the --genscript target about what
+ the binary blob means. Thanks Dinu Gherman.
+
+- fix issue614: fixed pastebin support.
+
+2.6.3 (2014-09-24)
+==================
+
+- fix issue575: xunit-xml was reporting collection errors as failures
+ instead of errors, thanks Oleg Sinyavskiy.
+
+- fix issue582: fix setuptools example, thanks Laszlo Papp and Ronny
+ Pfannschmidt.
+
+- Fix infinite recursion bug when pickling capture.EncodedFile, thanks
+ Uwe Schmitt.
+
+- fix issue589: fix bad interaction with numpy and others when showing
+ exceptions. Check for precise "maximum recursion depth exceed" exception
+ instead of presuming any RuntimeError is that one (implemented in py
+ dep). Thanks Charles Cloud for analysing the issue.
+
+- fix conftest related fixture visibility issue: when running with a
+ CWD outside of a test package pytest would get fixture discovery wrong.
+ Thanks to Wolfgang Schnerring for figuring out a reproducible example.
+
+- Introduce pytest_enter_pdb hook (needed e.g. by pytest_timeout to cancel the
+ timeout when interactively entering pdb). Thanks Wolfgang Schnerring.
+
+- check xfail/skip also with non-python function test items. Thanks
+ Floris Bruynooghe.
+
+2.6.2 (2014-09-05)
+==================
+
+- Added function pytest.freeze_includes(), which makes it easy to embed
+ pytest into executables using tools like cx_freeze.
+ See docs for examples and rationale. Thanks Bruno Oliveira.
+
+- Improve assertion rewriting cache invalidation precision.
+
+- fixed issue561: adapt autouse fixture example for python3.
+
+- fixed issue453: assertion rewriting issue with __repr__ containing
+ "\n{", "\n}" and "\n~".
+
+- fix issue560: correctly display code if an "else:" or "finally:" is
+ followed by statements on the same line.
+
+- Fix example in monkeypatch documentation, thanks t-8ch.
+
+- fix issue572: correct tmpdir doc example for python3.
+
+- Do not mark as universal wheel because Python 2.6 is different from
+ other builds due to the extra argparse dependency. Fixes issue566.
+ Thanks sontek.
+
+- Implement issue549: user-provided assertion messages now no longer
+ replace the py.test introspection message but are shown in addition
+ to them.
+
+2.6.1 (2014-08-07)
+==================
+
+- No longer show line numbers in the --verbose output, the output is now
+ purely the nodeid. The line number is still shown in failure reports.
+ Thanks Floris Bruynooghe.
+
+- fix issue437 where assertion rewriting could cause pytest-xdist worker nodes
+ to collect different tests. Thanks Bruno Oliveira.
+
+- fix issue555: add "errors" attribute to capture-streams to satisfy
+ some distutils and possibly other code accessing sys.stdout.errors.
+
+- fix issue547 capsys/capfd also work when output capturing ("-s") is disabled.
+
+- address issue170: allow pytest.mark.xfail(...) to specify expected exceptions via
+ an optional "raises=EXC" argument where EXC can be a single exception
+ or a tuple of exception classes. Thanks David Mohr for the complete
+ PR.
+
+- fix integration of pytest with unittest.mock.patch decorator when
+ it uses the "new" argument. Thanks Nicolas Delaby for test and PR.
+
+- fix issue with detecting conftest files if the arguments contain
+ "::" node id specifications (copy pasted from "-v" output)
+
+- fix issue544 by only removing "@NUM" at the end of "::" separated parts
+ and if the part has a ".py" extension
+
+- don't use py.std import helper, rather import things directly.
+ Thanks Bruno Oliveira.
+
+2.6
+===
+
+- Cache exceptions from fixtures according to their scope (issue 467).
+
+- fix issue537: Avoid importing old assertion reinterpretation code by default.
+
+- fix issue364: shorten and enhance tracebacks representation by default.
+ The new "--tb=auto" option (default) will only display long tracebacks
+ for the first and last entry. You can get the old behaviour of printing
+ all entries as long entries with "--tb=long". Also short entries by
+ default are now printed very similarly to "--tb=native" ones.
+
+- fix issue514: teach assertion reinterpretation about private class attributes
+
+- change -v output to include full node IDs of tests. Users can copy
+ a node ID from a test run, including line number, and use it as a
+ positional argument in order to run only a single test.
+
+- fix issue 475: fail early and comprehensible if calling
+ pytest.raises with wrong exception type.
+
+- fix issue516: tell in getting-started about current dependencies.
+
+- 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
+ with -v.
+
+- fix issue473: work around mock putting an unbound method into a class
+ dict when double-patching.
+
+- fix issue498: if a fixture finalizer fails, make sure that
+ the fixture is still invalidated.
+
+- fix issue453: the result of the pytest_assertrepr_compare hook now gets
+ it's newlines escaped so that format_exception does not blow up.
+
+- internal new warning system: pytest will now produce warnings when
+ it detects oddities in your test collection or execution.
+ Warnings are ultimately sent to a new pytest_logwarning hook which is
+ currently only implemented by the terminal plugin which displays
+ warnings in the summary line and shows more details when -rw (report on
+ warnings) is specified.
+
+- change skips into warnings for test classes with an __init__ and
+ callables in test modules which look like a test but are not functions.
+
+- fix issue436: improved finding of initial conftest files from command
+ line arguments by using the result of parse_known_args rather than
+ the previous flaky heuristics. Thanks Marc Abramowitz for tests
+ and initial fixing approaches in this area.
+
+- fix issue #479: properly handle nose/unittest(2) SkipTest exceptions
+ during collection/loading of test modules. Thanks to Marc Schlaich
+ for the complete PR.
+
+- fix issue490: include pytest_load_initial_conftests in documentation
+ and improve docstring.
+
+- fix issue472: clarify that ``pytest.config.getvalue()`` cannot work
+ if it's triggered ahead of command line parsing.
+
+- merge PR123: improved integration with mock.patch decorator on tests.
+
+- fix issue412: messing with stdout/stderr FD-level streams is now
+ captured without crashes.
+
+- fix issue483: trial/py33 works now properly. Thanks Daniel Grana for PR.
+
+- improve example for pytest integration with "python setup.py test"
+ which now has a generic "-a" or "--pytest-args" option where you
+ can pass additional options as a quoted string. Thanks Trevor Bekolay.
+
+- simplified internal capturing mechanism and made it more robust
+ against tests or setups changing FD1/FD2, also better integrated
+ now with pytest.pdb() in single tests.
+
+- improvements to pytest's own test-suite leakage detection, courtesy of PRs
+ from Marc Abramowitz
+
+- fix issue492: avoid leak in test_writeorg. Thanks Marc Abramowitz.
+
+- fix issue493: don't run tests in doc directory with ``python setup.py test``
+ (use tox -e doctesting for that)
+
+- fix issue486: better reporting and handling of early conftest loading failures
+
+- some cleanup and simplification of internal conftest handling.
+
+- work a bit harder to break reference cycles when catching exceptions.
+ Thanks Jurko Gospodnetic.
+
+- fix issue443: fix skip examples to use proper comparison. Thanks Alex
+ Groenholm.
+
+- support nose-style ``__test__`` attribute on modules, classes and
+ functions, including unittest-style Classes. If set to False, the
+ test will not be collected.
+
+- fix issue512: show "<notset>" for arguments which might not be set
+ in monkeypatch plugin. Improves output in documentation.
+
+
+2.5.2 (2014-01-29)
+==================
+
+- fix issue409 -- better interoperate with cx_freeze by not
+ trying to import from collections.abc which causes problems
+ for py27/cx_freeze. Thanks Wolfgang L. for reporting and tracking it down.
+
+- fixed docs and code to use "pytest" instead of "py.test" almost everywhere.
+ Thanks Jurko Gospodnetic for the complete PR.
+
+- fix issue425: mention at end of "py.test -h" that --markers
+ and --fixtures work according to specified test path (or current dir)
+
+- fix issue413: exceptions with unicode attributes are now printed
+ correctly also on python2 and with pytest-xdist runs. (the fix
+ requires py-1.4.20)
+
+- copy, cleanup and integrate py.io capture
+ from pylib 1.4.20.dev2 (rev 13d9af95547e)
+
+- address issue416: clarify docs as to conftest.py loading semantics
+
+- fix issue429: comparing byte strings with non-ascii chars in assert
+ expressions now work better. Thanks Floris Bruynooghe.
+
+- make capfd/capsys.capture private, its unused and shouldn't be exposed
+
+
+2.5.1 (2013-12-17)
+==================
+
+- merge new documentation styling PR from Tobias Bieniek.
+
+- fix issue403: allow parametrize of multiple same-name functions within
+ a collection node. Thanks Andreas Kloeckner and Alex Gaynor for reporting
+ and analysis.
+
+- Allow parameterized fixtures to specify the ID of the parameters by
+ adding an ids argument to pytest.fixture() and pytest.yield_fixture().
+ Thanks Floris Bruynooghe.
+
+- fix issue404 by always using the binary xml escape in the junitxml
+ plugin. Thanks Ronny Pfannschmidt.
+
+- fix issue407: fix addoption docstring to point to argparse instead of
+ optparse. Thanks Daniel D. Wright.
+
+
+
+2.5.0 (2013-12-12)
+==================
+
+- dropped python2.5 from automated release testing of pytest itself
+ which means it's probably going to break soon (but still works
+ with this release we believe).
+
+- simplified and fixed implementation for calling finalizers when
+ parametrized fixtures or function arguments are involved. finalization
+ is now performed lazily at setup time instead of in the "teardown phase".
+ While this might sound odd at first, it helps to ensure that we are
+ correctly handling setup/teardown even in complex code. User-level code
+ should not be affected unless it's implementing the pytest_runtest_teardown
+ hook and expecting certain fixture instances are torn down within (very
+ unlikely and would have been unreliable anyway).
+
+- PR90: add --color=yes|no|auto option to force terminal coloring
+ mode ("auto" is default). Thanks Marc Abramowitz.
+
+- fix issue319 - correctly show unicode in assertion errors. Many
+ thanks to Floris Bruynooghe for the complete PR. Also means
+ we depend on py>=1.4.19 now.
+
+- fix issue396 - correctly sort and finalize class-scoped parametrized
+ tests independently from number of methods on the class.
+
+- refix issue323 in a better way -- parametrization should now never
+ cause Runtime Recursion errors because the underlying algorithm
+ for re-ordering tests per-scope/per-fixture is not recursive
+ anymore (it was tail-call recursive before which could lead
+ to problems for more than >966 non-function scoped parameters).
+
+- fix issue290 - there is preliminary support now for parametrizing
+ with repeated same values (sometimes useful to test if calling
+ a second time works as with the first time).
+
+- close issue240 - document precisely how pytest module importing
+ works, discuss the two common test directory layouts, and how it
+ interacts with :pep:`420`\-namespace packages.
+
+- fix issue246 fix finalizer order to be LIFO on independent fixtures
+ depending on a parametrized higher-than-function scoped fixture.
+ (was quite some effort so please bear with the complexity of this sentence :)
+ Thanks Ralph Schmitt for the precise failure example.
+
+- fix issue244 by implementing special index for parameters to only use
+ indices for paramentrized test ids
+
+- fix issue287 by running all finalizers but saving the exception
+ from the first failing finalizer and re-raising it so teardown will
+ still have failed. We reraise the first failing exception because
+ 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
+ help with random "xdist" collection failures. Thanks to
+ Ronny Pfannschmidt and Donald Stufft for helping to isolate it.
+
+- fix issue357 - special case "-k" expressions to allow for
+ filtering with simple strings that are not valid python expressions.
+ Examples: "-k 1.3" matches all tests parametrized with 1.3.
+ "-k None" filters all tests that have "None" in their name
+ and conversely "-k 'not None'".
+ Previously these examples would raise syntax errors.
+
+- fix issue384 by removing the trial support code
+ since the unittest compat enhancements allow
+ trial to handle it on its own
+
+- don't hide an ImportError when importing a plugin produces one.
+ fixes issue375.
+
+- fix issue275 - allow usefixtures and autouse fixtures
+ for running doctest text files.
+
+- fix issue380 by making --resultlog only rely on longrepr instead
+ of the "reprcrash" attribute which only exists sometimes.
+
+- address issue122: allow @pytest.fixture(params=iterator) by exploding
+ into a list early on.
+
+- fix pexpect-3.0 compatibility for pytest's own tests.
+ (fixes issue386)
+
+- allow nested parametrize-value markers, thanks James Lan for the PR.
+
+- fix unicode handling with new monkeypatch.setattr(import_path, value)
+ API. Thanks Rob Dennis. Fixes issue371.
+
+- fix unicode handling with junitxml, fixes issue368.
+
+- In assertion rewriting mode on Python 2, fix the detection of coding
+ cookies. See issue #330.
+
+- make "--runxfail" turn imperative pytest.xfail calls into no ops
+ (it already did neutralize pytest.mark.xfail markers)
+
+- refine pytest / pkg_resources interactions: The AssertionRewritingHook
+ :pep:`302` compliant loader now registers itself with setuptools/pkg_resources
+ properly so that the pkg_resources.resource_stream method works properly.
+ Fixes issue366. Thanks for the investigations and full PR to Jason R. Coombs.
+
+- pytestconfig fixture is now session-scoped as it is the same object during the
+ whole test run. Fixes issue370.
+
+- avoid one surprising case of marker malfunction/confusion::
+
+ @pytest.mark.some(lambda arg: ...)
+ def test_function():
+
+ would not work correctly because pytest assumes @pytest.mark.some
+ gets a function to be decorated already. We now at least detect if this
+ arg is a lambda and thus the example will work. Thanks Alex Gaynor
+ for bringing it up.
+
+- xfail a test on pypy that checks wrong encoding/ascii (pypy does
+ not error out). fixes issue385.
+
+- internally make varnames() deal with classes's __init__,
+ although it's not needed by pytest itself atm. Also
+ fix caching. Fixes issue376.
+
+- fix issue221 - handle importing of namespace-package with no
+ __init__.py properly.
+
+- refactor internal FixtureRequest handling to avoid monkeypatching.
+ One of the positive user-facing effects is that the "request" object
+ can now be used in closures.
+
+- fixed version comparison in pytest.importskip(modname, minverstring)
+
+- fix issue377 by clarifying in the nose-compat docs that pytest
+ does not duplicate the unittest-API into the "plain" namespace.
+
+- fix verbose reporting for @mock'd test functions
+
+2.4.2 (2013-10-04)
+==================
+
+- on Windows require colorama and a newer py lib so that py.io.TerminalWriter()
+ now uses colorama instead of its own ctypes hacks. (fixes issue365)
+ thanks Paul Moore for bringing it up.
+
+- fix "-k" matching of tests where "repr" and "attr" and other names would
+ cause wrong matches because of an internal implementation quirk
+ (don't ask) which is now properly implemented. fixes issue345.
+
+- avoid tmpdir fixture to create too long filenames especially
+ when parametrization is used (issue354)
+
+- fix pytest-pep8 and pytest-flakes / pytest interactions
+ (collection names in mark plugin was assuming an item always
+ has a function which is not true for those plugins etc.)
+ Thanks Andi Zeidler.
+
+- introduce node.get_marker/node.add_marker API for plugins
+ like pytest-pep8 and pytest-flakes to avoid the messy
+ details of the node.keywords pseudo-dicts. Adapted
+ docs.
+
+- remove attempt to "dup" stdout at startup as it's icky.
+ the normal capturing should catch enough possibilities
+ of tests messing up standard FDs.
+
+- add pluginmanager.do_configure(config) as a link to
+ config.do_configure() for plugin-compatibility
+
+2.4.1 (2013-10-02)
+==================
+
+- When using parser.addoption() unicode arguments to the
+ "type" keyword should also be converted to the respective types.
+ thanks Floris Bruynooghe, @dnozay. (fixes issue360 and issue362)
+
+- fix dotted filename completion when using argcomplete
+ thanks Anthon van der Neuth. (fixes issue361)
+
+- fix regression when a 1-tuple ("arg",) is used for specifying
+ parametrization (the values of the parametrization were passed
+ nested in a tuple). Thanks Donald Stufft.
+
+- merge doc typo fixes, thanks Andy Dirnberger
+
+2.4
+===
+
+known incompatibilities:
+
+- if calling --genscript from python2.7 or above, you only get a
+ standalone script which works on python2.7 or above. Use Python2.6
+ to also get a python2.5 compatible version.
+
+- all xunit-style teardown methods (nose-style, pytest-style,
+ unittest-style) will not be called if the corresponding setup method failed,
+ see issue322 below.
+
+- the pytest_plugin_unregister hook wasn't ever properly called
+ and there is no known implementation of the hook - so it got removed.
+
+- pytest.fixture-decorated functions cannot be generators (i.e. use
+ yield) anymore. This change might be reversed in 2.4.1 if it causes
+ unforeseen real-life issues. However, you can always write and return
+ an inner function/generator and change the fixture consumer to iterate
+ over the returned generator. This change was done in lieu of the new
+ ``pytest.yield_fixture`` decorator, see below.
+
+new features:
+
+- experimentally introduce a new ``pytest.yield_fixture`` decorator
+ which accepts exactly the same parameters as pytest.fixture but
+ mandates a ``yield`` statement instead of a ``return statement`` from
+ fixture functions. This allows direct integration with "with-style"
+ context managers in fixture functions and generally avoids registering
+ of finalization callbacks in favour of treating the "after-yield" as
+ teardown code. Thanks Andreas Pelme, Vladimir Keleshev, Floris
+ Bruynooghe, Ronny Pfannschmidt and many others for discussions.
+
+- allow boolean expression directly with skipif/xfail
+ if a "reason" is also specified. Rework skipping documentation
+ to recommend "condition as booleans" because it prevents surprises
+ when importing markers between modules. Specifying conditions
+ as strings will remain fully supported.
+
+- reporting: color the last line red or green depending if
+ failures/errors occurred or everything passed. thanks Christian
+ Theunert.
+
+- make "import pdb ; pdb.set_trace()" work natively wrt capturing (no
+ "-s" needed anymore), making ``pytest.set_trace()`` a mere shortcut.
+
+- fix issue181: --pdb now also works on collect errors (and
+ on internal errors) . This was implemented by a slight internal
+ refactoring and the introduction of a new hook
+ ``pytest_exception_interact`` hook (see next item).
+
+- fix issue341: introduce new experimental hook for IDEs/terminals to
+ intercept debugging: ``pytest_exception_interact(node, call, report)``.
+
+- new monkeypatch.setattr() variant to provide a shorter
+ invocation for patching out classes/functions from modules:
+
+ monkeypatch.setattr("requests.get", myfunc)
+
+ will replace the "get" function of the "requests" module with ``myfunc``.
+
+- fix issue322: tearDownClass is not run if setUpClass failed. Thanks
+ Mathieu Agopian for the initial fix. Also make all of pytest/nose
+ finalizer mimic the same generic behaviour: if a setupX exists and
+ fails, don't run teardownX. This internally introduces a new method
+ "node.addfinalizer()" helper which can only be called during the setup
+ phase of a node.
+
+- simplify pytest.mark.parametrize() signature: allow to pass a
+ CSV-separated string to specify argnames. For example:
+ ``pytest.mark.parametrize("input,expected", [(1,2), (2,3)])``
+ works as well as the previous:
+ ``pytest.mark.parametrize(("input", "expected"), ...)``.
+
+- add support for setUpModule/tearDownModule detection, thanks Brian Okken.
+
+- integrate tab-completion on options through use of "argcomplete".
+ Thanks Anthon van der Neut for the PR.
+
+- change option names to be hyphen-separated long options but keep the
+ old spelling backward compatible. py.test -h will only show the
+ hyphenated version, for example "--collect-only" but "--collectonly"
+ will remain valid as well (for backward-compat reasons). Many thanks to
+ Anthon van der Neut for the implementation and to Hynek Schlawack for
+ pushing us.
+
+- fix issue 308 - allow to mark/xfail/skip individual parameter sets
+ when parametrizing. Thanks Brianna Laugher.
+
+- call new experimental pytest_load_initial_conftests hook to allow
+ 3rd party plugins to do something before a conftest is loaded.
+
+Bug fixes:
+
+- fix issue358 - capturing options are now parsed more properly
+ by using a new parser.parse_known_args method.
+
+- pytest now uses argparse instead of optparse (thanks Anthon) which
+ means that "argparse" is added as a dependency if installing into python2.6
+ environments or below.
+
+- fix issue333: fix a case of bad unittest/pytest hook interaction.
+
+- PR27: correctly handle nose.SkipTest during collection. Thanks
+ Antonio Cuni, Ronny Pfannschmidt.
+
+- fix issue355: junitxml puts name="pytest" attribute to testsuite tag.
+
+- fix issue336: autouse fixture in plugins should work again.
+
+- fix issue279: improve object comparisons on assertion failure
+ for standard datatypes and recognise collections.abc. Thanks to
+ Brianna Laugher and Mathieu Agopian.
+
+- fix issue317: assertion rewriter support for the is_package method
+
+- fix issue335: document py.code.ExceptionInfo() object returned
+ from pytest.raises(), thanks Mathieu Agopian.
+
+- remove implicit distribute_setup support from setup.py.
+
+- fix issue305: ignore any problems when writing pyc files.
+
+- SO-17664702: call fixture finalizers even if the fixture function
+ 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.
+
+- you can specify "-q" or "-qq" to get different levels of "quieter"
+ reporting (thanks Katarzyna Jachim)
+
+- fix issue300 - Fix order of conftest loading when starting py.test
+ in a subdirectory.
+
+- fix issue323 - sorting of many module-scoped arg parametrizations
+
+- make sessionfinish hooks execute with the same cwd-context as at
+ session start (helps fix plugin behaviour which write output files
+ with relative path such as pytest-cov)
+
+- fix issue316 - properly reference collection hooks in docs
+
+- fix issue 306 - cleanup of -k/-m options to only match markers/test
+ names/keywords respectively. Thanks Wouter van Ackooy.
+
+- improved doctest counting for doctests in python modules --
+ files without any doctest items will not show up anymore
+ and doctest examples are counted as separate test items.
+ thanks Danilo Bellini.
+
+- fix issue245 by depending on the released py-1.4.14
+ which fixes py.io.dupfile to work with files with no
+ mode. Thanks Jason R. Coombs.
+
+- fix junitxml generation when test output contains control characters,
+ addressing issue267, thanks Jaap Broekhuizen
+
+- fix issue338: honor --tb style for setup/teardown errors as well. Thanks Maho.
+
+- fix issue307 - use yaml.safe_load in example, thanks Mark Eichin.
+
+- better parametrize error messages, thanks Brianna Laugher
+
+- pytest_terminal_summary(terminalreporter) hooks can now use
+ ".section(title)" and ".line(msg)" methods to print extra
+ information at the end of a test run.
+
+2.3.5 (2013-04-30)
+==================
+
+- fix issue169: respect --tb=style with setup/teardown errors as well.
+
+- never consider a fixture function for test function collection
+
+- allow re-running of test items / helps to fix pytest-reruntests plugin
+ and also help to keep less fixture/resource references alive
+
+- put captured stdout/stderr into junitxml output even for passing tests
+ (thanks Adam Goucher)
+
+- Issue 265 - integrate nose setup/teardown with setupstate
+ so it doesn't try to teardown if it did not setup
+
+- issue 271 - don't write junitxml on worker nodes
+
+- Issue 274 - don't try to show full doctest example
+ when doctest does not know the example location
+
+- issue 280 - disable assertion rewriting on buggy CPython 2.6.0
+
+- inject "getfixture()" helper to retrieve fixtures from doctests,
+ thanks Andreas Zeidler
+
+- issue 259 - when assertion rewriting, be consistent with the default
+ source encoding of ASCII on Python 2
+
+- issue 251 - report a skip instead of ignoring classes with init
+
+- issue250 unicode/str mixes in parametrization names and values now works
+
+- issue257, assertion-triggered compilation of source ending in a
+ comment line doesn't blow up in python2.5 (fixed through py>=1.4.13.dev6)
+
+- fix --genscript option to generate standalone scripts that also
+ work with python3.3 (importer ordering)
+
+- issue171 - in assertion rewriting, show the repr of some
+ global variables
+
+- fix option help for "-k"
+
+- move long description of distribution into README.rst
+
+- improve docstring for metafunc.parametrize()
+
+- fix bug where using capsys with pytest.set_trace() in a test
+ function would break when looking at capsys.readouterr()
+
+- allow to specify prefixes starting with "_" when
+ customizing python_functions test discovery. (thanks Graham Horler)
+
+- improve PYTEST_DEBUG tracing output by putting
+ extra data on a new lines with additional indent
+
+- ensure OutcomeExceptions like skip/fail have initialized exception attributes
+
+- issue 260 - don't use nose special setup on plain unittest cases
+
+- fix issue134 - print the collect errors that prevent running specified test items
+
+- fix issue266 - accept unicode in MarkEvaluator expressions
+
+2.3.4 (2012-11-20)
+==================
+
+- yielded test functions will now have autouse-fixtures active but
+ cannot accept fixtures as funcargs - it's anyway recommended to
+ rather use the post-2.0 parametrize features instead of yield, see:
+ http://pytest.org/en/stable/example/how-to/parametrize.html
+- fix autouse-issue where autouse-fixtures would not be discovered
+ if defined in an a/conftest.py file and tests in a/tests/test_some.py
+- fix issue226 - LIFO ordering for fixture teardowns
+- fix issue224 - invocations with >256 char arguments now work
+- fix issue91 - add/discuss package/directory level setups in example
+- allow to dynamically define markers via
+ item.keywords[...]=assignment integrating with "-m" option
+- make "-k" accept an expressions the same as with "-m" so that one
+ can write: -k "name1 or name2" etc. This is a slight incompatibility
+ if you used special syntax like "TestClass.test_method" which you now
+ need to write as -k "TestClass and test_method" to match a certain
+ method in a certain test class.
+
+2.3.3 (2012-11-06)
+==================
+
+- fix issue214 - parse modules that contain special objects like e. g.
+ flask's request object which blows up on getattr access if no request
+ is active. thanks Thomas Waldmann.
+
+- fix issue213 - allow to parametrize with values like numpy arrays that
+ do not support an __eq__ operator
+
+- fix issue215 - split test_python.org into multiple files
+
+- fix issue148 - @unittest.skip on classes is now recognized and avoids
+ calling setUpClass/tearDownClass, thanks Pavel Repin
+
+- fix issue209 - reintroduce python2.4 support by depending on newer
+ pylib which re-introduced statement-finding for pre-AST interpreters
+
+- nose support: only call setup if it's a callable, thanks Andrew
+ Taumoefolau
+
+- fix issue219 - add py2.4-3.3 classifiers to TROVE list
+
+- in tracebacks *,** arg values are now shown next to normal arguments
+ (thanks Manuel Jacob)
+
+- fix issue217 - support mock.patch with pytest's fixtures - note that
+ you need either mock-1.0.1 or the python3.3 builtin unittest.mock.
+
+- fix issue127 - improve documentation for pytest_addoption() and
+ add a ``config.getoption(name)`` helper function for consistency.
+
+2.3.2 (2012-10-25)
+==================
+
+- fix issue208 and fix issue29 use new py version to avoid long pauses
+ when printing tracebacks in long modules
+
+- fix issue205 - conftests in subdirs customizing
+ pytest_pycollect_makemodule and pytest_pycollect_makeitem
+ now work properly
+
+- fix teardown-ordering for parametrized setups
+
+- fix issue127 - better documentation for pytest_addoption
+ and related objects.
+
+- fix unittest behaviour: TestCase.runtest only called if there are
+ test methods defined
+
+- improve trial support: don't collect its empty
+ unittest.TestCase.runTest() method
+
+- "python setup.py test" now works with pytest itself
+
+- fix/improve internal/packaging related bits:
+
+ - exception message check of test_nose.py now passes on python33 as well
+
+ - issue206 - fix test_assertrewrite.py to work when a global
+ PYTHONDONTWRITEBYTECODE=1 is present
+
+ - add tox.ini to pytest distribution so that ignore-dirs and others config
+ bits are properly distributed for maintainers who run pytest-own tests
+
+2.3.1 (2012-10-20)
+==================
+
+- fix issue202 - fix regression: using "self" from fixture functions now
+ works as expected (it's the same "self" instance that a test method
+ which uses the fixture sees)
+
+- skip pexpect using tests (test_pdb.py mostly) on freebsd* systems
+ due to pexpect not supporting it properly (hanging)
+
+- link to web pages from --markers output which provides help for
+ pytest.mark.* usage.
+
+2.3.0 (2012-10-19)
+==================
+
+- fix issue202 - better automatic names for parametrized test functions
+- fix issue139 - introduce @pytest.fixture which allows direct scoping
+ and parametrization of funcarg factories.
+- fix issue198 - conftest fixtures were not found on windows32 in some
+ circumstances with nested directory structures due to path manipulation issues
+- fix issue193 skip test functions with were parametrized with empty
+ parameter sets
+- fix python3.3 compat, mostly reporting bits that previously depended
+ on dict ordering
+- introduce re-ordering of tests by resource and parametrization setup
+ which takes precedence to the usual file-ordering
+- fix issue185 monkeypatching time.time does not cause pytest to fail
+- fix issue172 duplicate call of pytest.fixture decoratored setup_module
+ functions
+- fix junitxml=path construction so that if tests change the
+ current working directory and the path is a relative path
+ it is constructed correctly from the original current working dir.
+- fix "python setup.py test" example to cause a proper "errno" return
+- fix issue165 - fix broken doc links and mention stackoverflow for FAQ
+- catch unicode-issues when writing failure representations
+ to terminal to prevent the whole session from crashing
+- fix xfail/skip confusion: a skip-mark or an imperative pytest.skip
+ will now take precedence before xfail-markers because we
+ can't determine xfail/xpass status in case of a skip. see also:
+ http://stackoverflow.com/questions/11105828/in-py-test-when-i-explicitly-skip-a-test-that-is-marked-as-xfail-how-can-i-get
+
+- always report installed 3rd party plugins in the header of a test run
+
+- fix issue160: a failing setup of an xfail-marked tests should
+ be reported as xfail (not xpass)
+
+- fix issue128: show captured output when capsys/capfd are used
+
+- fix issue179: properly show the dependency chain of factories
+
+- pluginmanager.register(...) now raises ValueError if the
+ plugin has been already registered or the name is taken
+
+- fix issue159: improve https://docs.pytest.org/en/6.0.1/faq.html
+ especially with respect to the "magic" history, also mention
+ pytest-django, trial and unittest integration.
+
+- make request.keywords and node.keywords writable. All descendant
+ collection nodes will see keyword values. Keywords are dictionaries
+ containing markers and other info.
+
+- fix issue 178: xml binary escapes are now wrapped in py.xml.raw
+
+- fix issue 176: correctly catch the builtin AssertionError
+ even when we replaced AssertionError with a subclass on the
+ python level
+
+- factory discovery no longer fails with magic global callables
+ that provide no sane __code__ object (mock.call for example)
+
+- fix issue 182: testdir.inprocess_run now considers passed plugins
+
+- fix issue 188: ensure sys.exc_info is clear on python2
+ before calling into a test
+
+- fix issue 191: add unittest TestCase runTest method support
+- fix issue 156: monkeypatch correctly handles class level descriptors
+
+- reporting refinements:
+
+ - pytest_report_header now receives a "startdir" so that
+ you can use startdir.bestrelpath(yourpath) to show
+ nice relative path
+
+ - allow plugins to implement both pytest_report_header and
+ pytest_sessionstart (sessionstart is invoked first).
+
+ - don't show deselected reason line if there is none
+
+ - py.test -vv will show all of assert comparisons instead of truncating
+
+2.2.4 (2012-05-22)
+==================
+
+- fix error message for rewritten assertions involving the % operator
+- fix issue 126: correctly match all invalid xml characters for junitxml
+ binary escape
+- fix issue with unittest: now @unittest.expectedFailure markers should
+ be processed correctly (you can also use @pytest.mark markers)
+- document integration with the extended distribute/setuptools test commands
+- fix issue 140: properly get the real functions
+ of bound classmethods for setup/teardown_class
+- fix issue #141: switch from the deceased paste.pocoo.org to bpaste.net
+- fix issue #143: call unconfigure/sessionfinish always when
+ configure/sessionstart where called
+- fix issue #144: better mangle test ids to junitxml classnames
+- upgrade distribute_setup.py to 0.6.27
+
+2.2.3 (2012-02-05)
+==================
+
+- fix uploaded package to only include necessary files
+
+2.2.2 (2012-02-05)
+==================
+
+- fix issue101: wrong args to unittest.TestCase test function now
+ produce better output
+- fix issue102: report more useful errors and hints for when a
+ test directory was renamed and some pyc/__pycache__ remain
+- fix issue106: allow parametrize to be applied multiple times
+ e.g. from module, class and at function level.
+- fix issue107: actually perform session scope finalization
+- don't check in parametrize if indirect parameters are funcarg names
+- add chdir method to monkeypatch funcarg
+- fix crash resulting from calling monkeypatch undo a second time
+- fix issue115: make --collectonly robust against early failure
+ (missing files/directories)
+- "-qq --collectonly" now shows only files and the number of tests in them
+- "-q --collectonly" now shows test ids
+- allow adding of attributes to test reports such that it also works
+ with distributed testing (no upgrade of pytest-xdist needed)
+
+2.2.1 (2011-12-16)
+==================
+
+- fix issue99 (in pytest and py) internallerrors with resultlog now
+ produce better output - fixed by normalizing pytest_internalerror
+ input arguments.
+- fix issue97 / traceback issues (in pytest and py) improve traceback output
+ in conjunction with jinja2 and cython which hack tracebacks
+- fix issue93 (in pytest and pytest-xdist) avoid "delayed teardowns":
+ the final test in a test node will now run its teardown directly
+ instead of waiting for the end of the session. Thanks Dave Hunt for
+ the good reporting and feedback. The pytest_runtest_protocol as well
+ as the pytest_runtest_teardown hooks now have "nextitem" available
+ which will be None indicating the end of the test run.
+- fix collection crash due to unknown-source collected items, thanks
+ to Ralf Schmitt (fixed by depending on a more recent pylib)
+
+2.2.0 (2011-11-18)
+==================
+
+- fix issue90: introduce eager tearing down of test items so that
+ teardown function are called earlier.
+- add an all-powerful metafunc.parametrize function which allows to
+ parametrize test function arguments in multiple steps and therefore
+ from independent plugins and places.
+- add a @pytest.mark.parametrize helper which allows to easily
+ call a test function with different argument values
+- Add examples to the "parametrize" example page, including a quick port
+ of Test scenarios and the new parametrize function and decorator.
+- introduce registration for "pytest.mark.*" helpers via ini-files
+ or through plugin hooks. Also introduce a "--strict" option which
+ will treat unregistered markers as errors
+ allowing to avoid typos and maintain a well described set of markers
+ for your test suite. See examples at http://pytest.org/en/stable/how-to/mark.html
+ and its links.
+- issue50: introduce "-m marker" option to select tests based on markers
+ (this is a stricter and more predictable version of '-k' in that "-m"
+ only matches complete markers and has more obvious rules for and/or
+ semantics.
+- new feature to help optimizing the speed of your tests:
+ --durations=N option for displaying N slowest test calls
+ and setup/teardown methods.
+- fix issue87: --pastebin now works with python3
+- fix issue89: --pdb with unexpected exceptions in doctest work more sensibly
+- fix and cleanup pytest's own test suite to not leak FDs
+- fix issue83: link to generated funcarg list
+- fix issue74: pyarg module names are now checked against imp.find_module false positives
+- fix compatibility with twisted/trial-11.1.0 use cases
+- simplify Node.listchain
+- simplify junitxml output code by relying on py.xml
+- add support for skip properties on unittest classes and functions
+
+2.1.3 (2011-10-18)
+==================
+
+- fix issue79: assertion rewriting failed on some comparisons in boolops
+- correctly handle zero length arguments (a la pytest '')
+- fix issue67 / junitxml now contains correct test durations, thanks ronny
+- fix issue75 / skipping test failure on jython
+- fix issue77 / Allow assertrepr_compare hook to apply to a subset of tests
+
+2.1.2 (2011-09-24)
+==================
+
+- fix assertion rewriting on files with windows newlines on some Python versions
+- refine test discovery by package/module name (--pyargs), thanks Florian Mayer
+- fix issue69 / assertion rewriting fixed on some boolean operations
+- fix issue68 / packages now work with assertion rewriting
+- fix issue66: use different assertion rewriting caches when the -O option is passed
+- don't try assertion rewriting on Jython, use reinterp
+
+2.1.1
+=====
+
+- fix issue64 / pytest.set_trace now works within pytest_generate_tests hooks
+- fix issue60 / fix error conditions involving the creation of __pycache__
+- fix issue63 / assertion rewriting on inserts involving strings containing '%'
+- fix assertion rewriting on calls with a ** arg
+- don't cache rewritten modules if bytecode generation is disabled
+- fix assertion rewriting in read-only directories
+- fix issue59: provide system-out/err tags for junitxml output
+- fix issue61: assertion rewriting on boolean operations with 3 or more operands
+- you can now build a man page with "cd doc ; make man"
+
+2.1.0 (2011-07-09)
+==================
+
+- fix issue53 call nosestyle setup functions with correct ordering
+- fix issue58 and issue59: new assertion code fixes
+- merge Benjamin's assertionrewrite branch: now assertions
+ for test modules on python 2.6 and above are done by rewriting
+ the AST and saving the pyc file before the test module is imported.
+ see doc/assert.txt for more info.
+- fix issue43: improve doctests with better traceback reporting on
+ 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 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
+- report KeyboardInterrupt even if interrupted during session startup
+- fix issue 35 - provide PDF doc version and download link from index page
+
+2.0.3 (2011-05-11)
+==================
+
+- fix issue38: nicer tracebacks on calls to hooks, particularly early
+ configure/sessionstart ones
+
+- fix missing skip reason/meta information in junitxml files, reported
+ via http://lists.idyll.org/pipermail/testing-in-python/2011-March/003928.html
+
+- fix issue34: avoid collection failure with "test" prefixed classes
+ deriving from object.
+
+- don't require zlib (and other libs) for genscript plugin without
+ --genscript actually being used.
+
+- speed up skips (by not doing a full traceback representation
+ internally)
+
+- fix issue37: avoid invalid characters in junitxml's output
+
+2.0.2 (2011-03-09)
+==================
+
+- tackle issue32 - speed up test runs of very quick test functions
+ by reducing the relative overhead
+
+- fix issue30 - extended xfail/skipif handling and improved reporting.
+ If you have a syntax error in your skip/xfail
+ expressions you now get nice error reports.
+
+ Also you can now access module globals from xfail/skipif
+ expressions so that this for example works now::
+
+ import pytest
+ import mymodule
+ @pytest.mark.skipif("mymodule.__version__[0] == "1")
+ def test_function():
+ pass
+
+ This will not run the test function if the module's version string
+ does not start with a "1". Note that specifying a string instead
+ of a boolean expressions allows py.test to report meaningful information
+ when summarizing a test run as to what conditions lead to skipping
+ (or xfail-ing) tests.
+
+- fix issue28 - setup_method and pytest_generate_tests work together
+ The setup_method fixture method now gets called also for
+ test function invocations generated from the pytest_generate_tests
+ hook.
+
+- fix issue27 - collectonly and keyword-selection (-k) now work together
+ Also, if you do "py.test --collectonly -q" you now get a flat list
+ of test ids that you can use to paste to the py.test commandline
+ in order to execute a particular test.
+
+- fix issue25 avoid reported problems with --pdb and python3.2/encodings output
+
+- fix issue23 - tmpdir argument now works on Python3.2 and WindowsXP
+ Starting with Python3.2 os.symlink may be supported. By requiring
+ a newer py lib version the py.path.local() implementation acknowledges
+ this.
+
+- fixed typos in the docs (thanks Victor Garcia, Brianna Laugher) and particular
+ thanks to Laura Creighton who also reviewed parts of the documentation.
+
+- fix slightly wrong output of verbose progress reporting for classes
+ (thanks Amaury)
+
+- more precise (avoiding of) deprecation warnings for node.Class|Function accesses
+
+- avoid std unittest assertion helper code in tracebacks (thanks Ronny)
+
+2.0.1 (2011-02-07)
+==================
+
+- refine and unify initial capturing so that it works nicely
+ even if the logging module is used on an early-loaded conftest.py
+ file or plugin.
+- allow to omit "()" in test ids to allow for uniform test ids
+ as produced by Alfredo's nice pytest.vim plugin.
+- fix issue12 - show plugin versions with "--version" and
+ "--traceconfig" and also document how to add extra information
+ to reporting test header
+- fix issue17 (import-* reporting issue on python3) by
+ requiring py>1.4.0 (1.4.1 is going to include it)
+- fix issue10 (numpy arrays truth checking) by refining
+ assertion interpretation in py lib
+- fix issue15: make nose compatibility tests compatible
+ with python3 (now that nose-1.0 supports python3)
+- remove somewhat surprising "same-conftest" detection because
+ it ignores conftest.py when they appear in several subdirs.
+- improve assertions ("not in"), thanks Floris Bruynooghe
+- improve behaviour/warnings when running on top of "python -OO"
+ (assertions and docstrings are turned off, leading to potential
+ false positives)
+- introduce a pytest_cmdline_processargs(args) hook
+ to allow dynamic computation of command line arguments.
+ This fixes a regression because py.test prior to 2.0
+ allowed to set command line options from conftest.py
+ files which so far pytest-2.0 only allowed from ini-files now.
+- fix issue7: assert failures in doctest modules.
+ unexpected failures in doctests will not generally
+ show nicer, i.e. within the doctest failing context.
+- fix issue9: setup/teardown functions for an xfail-marked
+ test will report as xfail if they fail but report as normally
+ passing (not xpassing) if they succeed. This only is true
+ for "direct" setup/teardown invocations because teardown_class/
+ teardown_module cannot closely relate to a single test.
+- fix issue14: no logging errors at process exit
+- refinements to "collecting" output on non-ttys
+- refine internal plugin registration and --traceconfig output
+- introduce a mechanism to prevent/unregister plugins from the
+ command line, see http://pytest.org/en/stable/how-to/plugins.html#cmdunregister
+- activate resultlog plugin by default
+- fix regression wrt yielded tests which due to the
+ 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"
+ mechanism, see the docs.
+
+2.0.0 (2010-11-25)
+==================
+
+- pytest-2.0 is now its own package and depends on pylib-2.0
+- new ability: python -m pytest / python -m pytest.main ability
+- new python invocation: pytest.main(args, plugins) to load
+ some custom plugins early.
+- try harder to run unittest test suites in a more compatible manner
+ by deferring setup/teardown semantics to the unittest package.
+ also work harder to run twisted/trial and Django tests which
+ should now basically work by default.
+- introduce a new way to set config options via ini-style files,
+ by default setup.cfg and tox.ini files are searched. The old
+ ways (certain environment variables, dynamic conftest.py reading
+ is removed).
+- add a new "-q" option which decreases verbosity and prints a more
+ nose/unittest-style "dot" output.
+- fix issue135 - marks now work with unittest test cases as well
+- fix issue126 - introduce py.test.set_trace() to trace execution via
+ PDB during the running of tests even if capturing is ongoing.
+- fix issue123 - new "python -m py.test" invocation for py.test
+ (requires Python 2.5 or above)
+- fix issue124 - make reporting more resilient against tests opening
+ files on filedescriptor 1 (stdout).
+- fix issue109 - sibling conftest.py files will not be loaded.
+ (and Directory collectors cannot be customized anymore from a Directory's
+ conftest.py - this needs to happen at least one level up).
+- introduce (customizable) assertion failure representations and enhance
+ output on assertion failures for comparisons and other cases (Floris Bruynooghe)
+- nose-plugin: pass through type-signature failures in setup/teardown
+ functions instead of not calling them (Ed Singleton)
+- remove py.test.collect.Directory (follows from a major refactoring
+ and simplification of the collection process)
+- majorly reduce py.test core code, shift function/python testing to own plugin
+- fix issue88 (finding custom test nodes from command line arg)
+- refine 'tmpdir' creation, will now create basenames better associated
+ with test names (thanks Ronny)
+- "xpass" (unexpected pass) tests don't cause exitcode!=0
+- fix issue131 / issue60 - importing doctests in __init__ files used as namespace packages
+- fix issue93 stdout/stderr is captured while importing conftest.py
+- fix bug: unittest collected functions now also can have "pytestmark"
+ applied at class/module level
+- add ability to use "class" level for cached_setup helper
+- fix strangeness: mark.* objects are now immutable, create new instances
+
+1.3.4 (2010-09-14)
+==================
+
+- 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
+
+1.3.3 (2010-07-30)
+==================
+
+- 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
+
+1.3.2 (2010-07-08)
+==================
+
+**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**
+
+- 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 deferring 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 a 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
+
+1.3.1 (2010-05-25)
+==================
+
+**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.
+
+**Bug Fixes**
+
+- 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 seamlessly 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)
+
+
+1.3.0 (2010-05-05)
+==================
+
+- 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
+ specifying ``--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
+ https://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
+
+
+1.2.0 (2010-01-18)
+==================
+
+- 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
+
+1.1.1 (2009-11-24)
+==================
+
+- 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 names ("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 worker nodes 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
+
+
+1.1.0 (2009-11-05)
+==================
+
+- 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
+
+1.0.3
+=====
+
+* 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 occurring
+ 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
+
+1.0.2 (2009-08-27)
+==================
+
+* 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.
+
+1.0.1 (2009-08-19)
+==================
+
+* 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
+
+1.0.0 (2009-08-04)
+==================
+
+* more terse reporting try to show filesystem path relatively to current dir
+* improve xfail output a bit
+
+1.0.0b9 (2009-07-31)
+====================
+
+* 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)
+
+
+1.0.0b8 (2009-07-22)
+====================
+
+* 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.
+
+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
+
+1.0.0b3 (2009-06-19)
+====================
+
+* 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.
+
+
+1.0.0b1
+=======
+
+* introduced new "funcarg" setup method,
+ see doc/test/funcarg.txt
+
+* introduced plugin architecture 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
+
+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
+
+0.9.1
+=====
+
+This is a fairly complete list of v0.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 ignored 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 atomic 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/testing/web-platform/tests/tools/third_party/pytest/doc/en/conf.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/conf.py
new file mode 100644
index 0000000000..b316163532
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/conf.py
@@ -0,0 +1,478 @@
+#
+# pytest documentation build configuration file, created by
+# sphinx-quickstart on Fri Oct 8 17:54:28 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.
+# 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 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
+
+
+release = ".".join(version.split(".")[:2])
+
+# 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('.'))
+
+autodoc_member_order = "bysource"
+autodoc_typehints = "description"
+todo_include_todos = 1
+
+latex_engine = "lualatex"
+
+latex_elements = {
+ "preamble": dedent(
+ r"""
+ \directlua{
+ luaotfload.add_fallback("fallbacks", {
+ "Noto Serif CJK SC:style=Regular;",
+ "Symbola:Style=Regular;"
+ })
+ }
+
+ \setmainfont{FreeSerif}[RawFeature={fallback=fallbacks}]
+ """
+ )
+}
+
+# -- 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 = [
+ "pallets_sphinx_themes",
+ "pygments_pytest",
+ "sphinx.ext.autodoc",
+ "sphinx.ext.autosummary",
+ "sphinx.ext.extlinks",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.todo",
+ "sphinx.ext.viewcode",
+ "sphinx_removed_in",
+ "sphinxcontrib_trio",
+]
+
+# Building PDF docs on readthedocs requires inkscape for svg to pdf
+# conversion. The relevant plugin is not useful for normal HTML builds, but
+# it still raises warnings and fails CI if inkscape is not available. So
+# only use the plugin if inkscape is actually available.
+if shutil.which("inkscape"):
+ extensions.append("sphinxcontrib.inkscapeconverter")
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# The suffix of source filenames.
+source_suffix = ".rst"
+
+# The encoding of source files.
+# source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = "contents"
+
+# General information about the project.
+project = "pytest"
+copyright = "2015, holger krekel and pytest-dev team"
+
+
+# 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",
+ "naming20.rst",
+ "test/*",
+ "old_*",
+ "*attic*",
+ "*/attic*",
+ "funcargs.rst",
+ "setup.rst",
+ "example/remoteinterp.rst",
+]
+
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+default_role = "literal"
+
+# 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 = False
+
+# 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 = []
+
+# A list of regular expressions that match URIs that should not be checked when
+# doing a linkcheck.
+linkcheck_ignore = [
+ "https://blogs.msdn.microsoft.com/bharry/2017/06/28/testing-in-a-cloud-delivery-cadence/",
+ "http://pythontesting.net/framework/pytest-introduction/",
+ r"https://github.com/pytest-dev/pytest/issues/\d+",
+ r"https://github.com/pytest-dev/pytest/pull/\d+",
+]
+
+# The number of worker threads to use when checking links (default=5).
+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", "@"),
+}
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+sys.path.append(os.path.abspath("_themes"))
+html_theme_path = ["_themes"]
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = "flask"
+
+# 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 = {"index_logo": None}
+
+# 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 = "pytest documentation"
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+html_short_title = "pytest-%s" % release
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+html_logo = "img/pytest_logo_curves.svg"
+
+# 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 = "img/favicon.png"
+
+# 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 = {}
+# html_sidebars = {'index': 'indexsidebar.html'}
+
+html_sidebars = {
+ "index": [
+ "slim_searchbox.html",
+ "sidebarintro.html",
+ "globaltoc.html",
+ "links.html",
+ "sourcelink.html",
+ ],
+ "**": [
+ "slim_searchbox.html",
+ "globaltoc.html",
+ "relations.html",
+ "links.html",
+ "sourcelink.html",
+ ],
+}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+# html_additional_pages = {}
+# html_additional_pages = {'index': 'index.html'}
+
+
+# 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 = False
+
+# 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 = "pytestdoc"
+
+
+# -- 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 = [
+ (
+ "contents",
+ "pytest.tex",
+ "pytest Documentation",
+ "holger krekel, trainer and consultant, https://merlinux.eu/",
+ "manual",
+ )
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+latex_logo = "img/pytest1.png"
+
+# 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 = False
+
+# -- Options for manual page output --------------------------------------------
+
+# 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)]
+
+
+# -- Options for Epub output ---------------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = "pytest"
+epub_author = "holger krekel at merlinux eu"
+epub_publisher = "holger krekel at merlinux eu"
+epub_copyright = "2013, holger krekel et alii"
+
+# 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
+
+
+# -- Options for texinfo output ------------------------------------------------
+
+texinfo_documents = [
+ (
+ master_doc,
+ "pytest",
+ "pytest Documentation",
+ (
+ "Holger Krekel@*Benjamin Peterson@*Ronny Pfannschmidt@*"
+ "Floris Bruynooghe@*others"
+ ),
+ "pytest",
+ "simple powerful testing with Python",
+ "Programming",
+ 1,
+ )
+]
+
+
+# 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),
+ "numpy": ("https://numpy.org/doc/stable", None),
+ "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),
+}
+
+
+def configure_logging(app: "sphinx.application.Sphinx") -> None:
+ """Configure Sphinx's WarningHandler to handle (expected) missing include."""
+ import sphinx.util.logging
+ import 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:
+ # from sphinx.ext.autodoc import cut_lines
+ # app.connect('autodoc-process-docstring', cut_lines(4, what=['module']))
+ app.add_crossref_type(
+ "fixture",
+ "fixture",
+ objname="built-in fixture",
+ indextemplate="pair: %s; fixture",
+ )
+
+ app.add_object_type(
+ "confval",
+ "confval",
+ objname="configuration value",
+ indextemplate="pair: %s; configuration value",
+ )
+
+ app.add_object_type(
+ "globalvar",
+ "globalvar",
+ objname="global variable interpreted by pytest",
+ indextemplate="pair: %s; global variable interpreted by pytest",
+ )
+
+ app.add_crossref_type(
+ directivename="hook",
+ rolename="hook",
+ objname="pytest hook",
+ indextemplate="pair: %s; hook",
+ )
+
+ 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/testing/web-platform/tests/tools/third_party/pytest/doc/en/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/conftest.py
new file mode 100644
index 0000000000..1a62e1b5df
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/conftest.py
@@ -0,0 +1 @@
+collect_ignore = ["conf.py"]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/contact.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/contact.rst
new file mode 100644
index 0000000000..beed10d7f2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/contact.rst
@@ -0,0 +1,54 @@
+
+.. _`contact channels`:
+.. _`contact`:
+
+Contact channels
+===================================
+
+- `pytest issue tracker`_ to report bugs or suggest features (for version
+ 2.0 and above).
+- `pytest discussions`_ at github for general questions.
+- `pytest discord server <https://discord.com/invite/pytest-dev>`_
+ for pytest development visibility and general assistance.
+- `pytest on stackoverflow.com <http://stackoverflow.com/search?q=pytest>`_
+ to post precise questions with the tag ``pytest``. New Questions will usually
+ be seen by pytest users or developers and answered quickly.
+
+- `Testing In Python`_: a mailing list for Python testing tools and discussion.
+
+- `pytest-dev at python.org (mailing list)`_ pytest specific announcements and discussions.
+
+- :doc:`contribution guide <contributing>` for help on submitting pull
+ requests to GitHub.
+
+- ``#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>`_).
+
+- private mail to Holger.Krekel at gmail com if you want to communicate sensitive issues
+
+
+- `merlinux.eu`_ offers pytest and tox-related professional teaching and
+ consulting.
+
+.. _`pytest issue tracker`: https://github.com/pytest-dev/pytest/issues
+.. _`old issue tracker`: https://bitbucket.org/hpk42/py-trunk/issues/
+
+.. _`pytest discussions`: https://github.com/pytest-dev/pytest/discussions
+
+.. _`merlinux.eu`: https://merlinux.eu/
+
+.. _`get an account`:
+
+.. _tetamap: https://tetamap.wordpress.com/
+
+.. _`@pylibcommit`: https://twitter.com/pylibcommit
+
+
+.. _`Testing in Python`: http://lists.idyll.org/listinfo/testing-in-python
+.. _FOAF: https://en.wikipedia.org/wiki/FOAF
+.. _`py-dev`:
+.. _`development mailing list`:
+.. _`pytest-dev at python.org (mailing list)`: http://mail.python.org/mailman/listinfo/pytest-dev
+.. _`pytest-commit at python.org (mailing list)`: http://mail.python.org/mailman/listinfo/pytest-commit
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/contents.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/contents.rst
new file mode 100644
index 0000000000..049d44ba9d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/contents.rst
@@ -0,0 +1,116 @@
+.. _toc:
+
+Full pytest documentation
+===========================
+
+`Download latest version as PDF <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_
+
+.. `Download latest version as EPUB <http://media.readthedocs.org/epub/pytest/latest/pytest.epub>`_
+
+
+Start here
+-----------
+
+.. toctree::
+ :maxdepth: 2
+
+ getting-started
+
+
+How-to guides
+-------------
+
+.. toctree::
+ :maxdepth: 2
+
+ how-to/usage
+ how-to/assert
+ how-to/fixtures
+ how-to/mark
+ how-to/parametrize
+ how-to/tmp_path
+ how-to/monkeypatch
+ how-to/doctest
+ how-to/cache
+
+ how-to/logging
+ how-to/capture-stdout-stderr
+ how-to/capture-warnings
+ how-to/skipping
+
+ how-to/plugins
+ how-to/writing_plugins
+ how-to/writing_hook_functions
+
+ how-to/existingtestsuite
+ how-to/unittest
+ how-to/nose
+ how-to/xunit_setup
+
+ how-to/bash-completion
+
+
+Reference guides
+-----------------
+
+.. toctree::
+ :maxdepth: 2
+
+ reference/fixtures
+ reference/plugin_list
+ reference/customize
+ reference/reference
+
+
+Explanation
+-----------------
+
+.. toctree::
+ :maxdepth: 2
+
+ explanation/anatomy
+ explanation/fixtures
+ explanation/goodpractices
+ explanation/flaky
+ explanation/pythonpath
+
+
+Further topics
+-----------------
+
+.. toctree::
+ :maxdepth: 2
+
+ example/index
+
+ backwards-compatibility
+ deprecations
+ py27-py34-deprecation
+
+ contributing
+ development_guide
+
+ sponsor
+ tidelift
+ license
+ contact
+
+ history
+ historical-notes
+ talks
+
+
+.. only:: html
+
+ .. toctree::
+ :maxdepth: 1
+
+ announce/index
+
+.. only:: html
+
+ .. toctree::
+ :hidden:
+ :maxdepth: 1
+
+ changelog
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/contributing.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/contributing.rst
new file mode 100644
index 0000000000..2b6578f6b9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/contributing.rst
@@ -0,0 +1,3 @@
+.. _contributing:
+
+.. include:: ../../CONTRIBUTING.rst
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/deprecations.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/deprecations.rst
new file mode 100644
index 0000000000..0f19744ade
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/deprecations.rst
@@ -0,0 +1,954 @@
+.. _deprecations:
+
+Deprecations and Removals
+=========================
+
+This page lists all pytest features that are currently deprecated or have been removed in past major releases.
+The objective is to give users a clear rationale why a certain feature has been removed, and what alternatives
+should be used instead.
+
+.. contents::
+ :depth: 3
+ :local:
+
+
+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>`.
+
+.. _instance-collector-deprecation:
+
+The ``pytest.Instance`` collector
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. 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 instanted in ``pytest.Instance`` and ``_pytest.python.Instance``,
+and importing it emits a deprecation warning. This will be removed in pytest 8.
+
+
+.. _node-ctor-fspath-deprecation:
+
+``fspath`` argument for Node constructors replaced with ``pathlib.Path``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 7.0
+
+In order to support the transition from ``py.path.local`` to :mod:`pathlib`,
+the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like
+:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()`
+is now deprecated.
+
+Plugins which construct nodes should pass the ``path`` argument, of type
+:class:`pathlib.Path`, instead of the ``fspath`` argument.
+
+Plugins which implement custom items and collectors are encouraged to replace
+``fspath`` parameters (``py.path.local``) with ``path`` parameters
+(``pathlib.Path``), and drop any other usage of the ``py`` library if possible.
+
+If possible, plugins with custom items should use :ref:`cooperative
+constructors <uncooperative-constructors-deprecated>` to avoid hardcoding
+arguments they only pass on to the superclass.
+
+.. note::
+ The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
+ new attribute being ``path``) is **the opposite** of the situation for
+ hooks, :ref:`outlined below <legacy-path-hooks-deprecated>` (the old
+ argument being ``path``).
+
+ This is an unfortunate artifact due to historical reasons, which should be
+ 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`
+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.
+
+.. _legacy-path-hooks-deprecated:
+
+``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 7.0
+
+In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
+
+* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) <pytest_ignore_collect>` as equivalent to ``path``
+* :hook:`pytest_collect_file(file_path: pathlib.Path) <pytest_collect_file>` as equivalent to ``path``
+* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) <pytest_pycollect_makemodule>` as equivalent to ``path``
+* :hook:`pytest_report_header(start_path: pathlib.Path) <pytest_report_header>` as equivalent to ``startdir``
+* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) <pytest_report_collectionfinish>` as equivalent to ``startdir``
+
+The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments.
+
+.. note::
+ The name of the :class:`~_pytest.nodes.Node` arguments and attributes,
+ :ref:`outlined above <node-ctor-fspath-deprecation>` (the new attribute
+ being ``path``) is **the opposite** of the situation for hooks (the old
+ argument being ``path``).
+
+ This is an unfortunate artifact due to historical reasons, which should be
+ resolved in future versions as we slowly get rid of the :pypi:`py`
+ dependency (see :issue:`9283` for a longer discussion).
+
+Directly constructing internal classes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 7.0
+
+Directly constructing the following classes is now deprecated:
+
+- ``_pytest.mark.structures.Mark``
+- ``_pytest.mark.structures.MarkDecorator``
+- ``_pytest.mark.structures.MarkGenerator``
+- ``_pytest.python.Metafunc``
+- ``_pytest.runner.CallInfo``
+- ``_pytest._code.ExceptionInfo``
+- ``_pytest.config.argparsing.Parser``
+- ``_pytest.config.argparsing.OptionGroup``
+- ``_pytest.pytester.HookRecorder``
+
+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`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. 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.
+It was never sanely supported and triggers hard to debug errors.
+
+Some plugins providing linting/code analysis have been using this as a hack.
+Instead, a separate collector node should be used, which collects the item. See
+:ref:`non-python tests` for an example, as well as an `example pr fixing inheritance`_.
+
+.. _example pr fixing inheritance: https://github.com/asmeurer/pytest-flakes/pull/40/files
+
+
+.. _uncooperative-constructors-deprecated:
+
+Constructors of custom :class:`pytest.Node` subclasses should take ``**kwargs``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 7.0
+
+If custom subclasses of nodes like :class:`pytest.Item` override the
+``__init__`` method, they should take ``**kwargs``. Thus,
+
+.. code-block:: python
+
+ class CustomItem(pytest.Item):
+ def __init__(self, name, parent, additional_arg):
+ super().__init__(name, parent)
+ self.additional_arg = additional_arg
+
+should be turned into:
+
+.. code-block:: python
+
+ class CustomItem(pytest.Item):
+ def __init__(self, *, additional_arg, **kwargs):
+ super().__init__(**kwargs)
+ self.additional_arg = additional_arg
+
+to avoid hard-coding the arguments pytest can pass to the superclass.
+See :ref:`non-python tests` for a full example.
+
+For cases without conflicts, no deprecation warning is emitted. For cases with
+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``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 2.4
+
+Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now
+scheduled for removal 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.
+
+
+Raising ``unittest.SkipTest`` during collection
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 7.0
+
+Raising :class:`unittest.SkipTest` to skip collection of tests during the
+pytest collection phase is deprecated. Use :func:`pytest.skip` instead.
+
+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
+
+: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()``
+or ``pytest.warns(Warning)``.
+
+See :ref:`warns use cases` for examples.
+
+The ``--strict`` command-line option
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 6.2
+
+The ``--strict`` command-line option has been deprecated in favor of ``--strict-markers``, which
+better conveys what the option does.
+
+We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing
+flag for all strictness related options (``--strict-markers`` and ``--strict-config``
+at the moment, more might be introduced in the future).
+
+
+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.
+
+
+The ``pytest_warning_captured`` hook
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 6.0
+
+This hook has an `item` parameter which cannot be serialized by ``pytest-xdist``.
+
+Use the ``pytest_warning_recored`` hook instead, which replaces the ``item`` parameter
+by a ``nodeid`` parameter.
+
+The ``pytest.collect`` module
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 6.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._fillfuncargs`` function
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 6.0
+
+This function was kept for backward compatibility with an older plugin.
+
+It's functionality is not meant to be used directly, but if you must replace
+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
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 5.4
+.. versionremoved:: 6.0
+
+
+The ``--no-print-logs`` option and ``log_print`` ini setting are removed. If
+you used them, please use ``--show-capture`` instead.
+
+A ``--show-capture`` command-line option was added in ``pytest 3.5.0`` which allows to specify how to
+display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` or ``all`` (the default).
+
+
+.. _resultlog deprecated:
+
+Result log (``--result-log``)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 4.0
+.. versionremoved:: 6.0
+
+The ``--result-log`` option produces a stream of test reports which can be
+analysed at runtime, but it uses a custom format which requires users to implement their own
+parser.
+
+The `pytest-reportlog <https://github.com/pytest-dev/pytest-reportlog>`__ plugin provides a ``--report-log`` option, a more standard and extensible alternative, producing
+one JSON object per-line, and should cover the same use cases. Please try it out and provide feedback.
+
+The ``pytest-reportlog`` plugin might even be merged into the core
+at some point, depending on the plans for the plugins and number of users using it.
+
+``pytest_collect_directory`` hook
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 6.0
+
+The ``pytest_collect_directory`` hook has not worked properly for years (it was called
+but the results were ignored). Users may consider using :hook:`pytest_collection_modifyitems` instead.
+
+TerminalReporter.writer
+~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 6.0
+
+The ``TerminalReporter.writer`` attribute has been deprecated and should no longer be used. This
+was inadvertently exposed as part of the public API of that plugin and ties it too much
+with ``py.io.TerminalWriter``.
+
+Plugins that used ``TerminalReporter.writer`` directly should instead use ``TerminalReporter``
+methods that provide the same functionality.
+
+.. _junit-family changed default value:
+
+``junit_family`` default value change to "xunit2"
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionchanged:: 6.0
+
+The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, which
+is an update of the old ``xunit1`` format and is supported by default in modern tools
+that manipulate this type of file (for example, Jenkins, Azure Pipelines, etc.).
+
+Users are recommended to try the new ``xunit2`` format and see if their tooling that consumes the JUnit
+XML file supports it.
+
+To use the new format, update your ``pytest.ini``:
+
+.. code-block:: ini
+
+ [pytest]
+ junit_family=xunit2
+
+If you discover that your tooling does not support the new format, and want to keep using the
+legacy version, set the option to ``legacy`` instead:
+
+.. code-block:: ini
+
+ [pytest]
+ junit_family=legacy
+
+By using ``legacy`` you will keep using the legacy/xunit1 format when upgrading to
+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
+configured in ``pytest.ini``.
+
+Services known to support the ``xunit2`` format:
+
+* `Jenkins <https://www.jenkins.io/>`__ with the `JUnit <https://plugins.jenkins.io/junit>`__ plugin.
+* `Azure Pipelines <https://azure.microsoft.com/en-us/services/devops/pipelines>`__.
+
+Node Construction changed to ``Node.from_parent``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionchanged:: 6.0
+
+The construction of nodes now should use the named constructor ``from_parent``.
+This limitation in api surface intends to enable better/simpler refactoring of the collection tree.
+
+This means that instead of :code:`MyItem(name="foo", parent=collector, obj=42)`
+one now has to invoke :code:`MyItem.from_parent(collector, name="foo")`.
+
+Plugins that wish to support older versions of pytest and suppress the warning can use
+`hasattr` to check if `from_parent` exists in that version:
+
+.. code-block:: python
+
+ def pytest_pycollect_makeitem(collector, name, obj):
+ if hasattr(MyItem, "from_parent"):
+ item = MyItem.from_parent(collector, name="foo")
+ item.obj = 42
+ return item
+ else:
+ return MyItem(name="foo", parent=collector, obj=42)
+
+Note that ``from_parent`` should only be called with keyword arguments for the parameters.
+
+
+``pytest.fixture`` arguments are keyword only
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 6.0
+
+Passing arguments to pytest.fixture() as positional arguments has been removed - pass them by keyword instead.
+
+``funcargnames`` alias for ``fixturenames``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 6.0
+
+The ``FixtureRequest``, ``Metafunc``, and ``Function`` classes track the names of
+their associated fixtures, with the aptly-named ``fixturenames`` attribute.
+
+Prior to pytest 2.3, this attribute was named ``funcargnames``, and we have kept
+that as an alias since. It is finally due for removal, as it is often confusing
+in places where we or plugin authors must distinguish between fixture names and
+names supplied by non-fixture things such as ``pytest.mark.parametrize``.
+
+
+.. _pytest.config global deprecated:
+
+``pytest.config`` global
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 5.0
+
+The ``pytest.config`` global object is deprecated. Instead use
+``request.config`` (via the ``request`` fixture) or if you are a plugin author
+use the ``pytest_configure(config)`` hook. Note that many hooks can also access
+the ``config`` object indirectly, through ``session.config`` or ``item.config`` for example.
+
+
+.. _`raises message deprecated`:
+
+``"message"`` parameter of ``pytest.raises``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 5.0
+
+It is a common mistake to think this parameter will match the exception message, while in fact
+it only serves to provide a custom message in case the ``pytest.raises`` check fails. To prevent
+users from making this mistake, and because it is believed to be little used, pytest is
+deprecating it without providing an alternative for the moment.
+
+If you have a valid use case for this parameter, consider that to obtain the same results
+you can just call ``pytest.fail`` manually at the end of the ``with`` statement.
+
+For example:
+
+.. code-block:: python
+
+ with pytest.raises(TimeoutError, message="Client got unexpected message"):
+ wait_for(websocket.recv(), 0.5)
+
+
+Becomes:
+
+.. code-block:: python
+
+ with pytest.raises(TimeoutError):
+ wait_for(websocket.recv(), 0.5)
+ pytest.fail("Client got unexpected message")
+
+
+If you still have concerns about this deprecation and future removal, please comment on
+:issue:`3974`.
+
+
+.. _raises-warns-exec:
+
+``raises`` / ``warns`` with a string as the second argument
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 5.0
+
+Use the context manager form of these instead. When necessary, invoke ``exec``
+directly.
+
+Example:
+
+.. code-block:: python
+
+ pytest.raises(ZeroDivisionError, "1 / 0")
+ pytest.raises(SyntaxError, "a $ b")
+
+ pytest.warns(DeprecationWarning, "my_function()")
+ pytest.warns(SyntaxWarning, "assert(1, 2)")
+
+Becomes:
+
+.. code-block:: python
+
+ with pytest.raises(ZeroDivisionError):
+ 1 / 0
+ with pytest.raises(SyntaxError):
+ exec("a $ b") # exec is required for invalid syntax
+
+ with pytest.warns(DeprecationWarning):
+ my_function()
+ with pytest.warns(SyntaxWarning):
+ exec("assert(1, 2)") # exec is used to avoid a top-level warning
+
+
+
+
+Using ``Class`` in custom Collectors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+Using objects named ``"Class"`` as a way to customize the type of nodes that are collected in ``Collector``
+subclasses has been deprecated. Users instead should use ``pytest_pycollect_makeitem`` to customize node types during
+collection.
+
+This issue should affect only advanced plugins who create new collection types, so if you see this warning
+message please contact the authors so they can change the code.
+
+
+.. _marks in pytest.parametrize deprecated:
+
+marks in ``pytest.mark.parametrize``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated. For example:
+
+.. code-block:: python
+
+ @pytest.mark.parametrize(
+ "a, b",
+ [
+ (3, 9),
+ pytest.mark.xfail(reason="flaky")(6, 36),
+ (10, 100),
+ (20, 200),
+ (40, 400),
+ (50, 500),
+ ],
+ )
+ 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.
+
+This was considered hard to read and understand, and also its implementation presented problems to the code preventing
+further internal improvements in the marks architecture.
+
+To update the code, use ``pytest.param``:
+
+.. code-block:: python
+
+ @pytest.mark.parametrize(
+ "a, b",
+ [
+ (3, 9),
+ pytest.param(6, 36, marks=pytest.mark.xfail(reason="flaky")),
+ (10, 100),
+ (20, 200),
+ (40, 400),
+ (50, 500),
+ ],
+ )
+ def test_foo(a, b):
+ ...
+
+
+.. _pytest_funcarg__ prefix deprecated:
+
+``pytest_funcarg__`` prefix
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+In very early pytest versions fixtures could be defined using the ``pytest_funcarg__`` prefix:
+
+.. code-block:: python
+
+ def pytest_funcarg__data():
+ return SomeData()
+
+Switch over to the ``@pytest.fixture`` decorator:
+
+.. code-block:: python
+
+ @pytest.fixture
+ def data():
+ return SomeData()
+
+
+
+[pytest] section in setup.cfg files
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+``[pytest]`` sections in ``setup.cfg`` files should now be named ``[tool:pytest]``
+to avoid conflicts with other distutils commands.
+
+
+.. _metafunc.addcall deprecated:
+
+Metafunc.addcall
+~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+``Metafunc.addcall`` was a precursor to the current parametrized mechanism. Users should use
+:meth:`pytest.Metafunc.parametrize` instead.
+
+Example:
+
+.. code-block:: python
+
+ def pytest_generate_tests(metafunc):
+ metafunc.addcall({"i": 1}, id="1")
+ metafunc.addcall({"i": 2}, id="2")
+
+Becomes:
+
+.. code-block:: python
+
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize("i", [1, 2], ids=["1", "2"])
+
+
+.. _cached_setup deprecated:
+
+``cached_setup``
+~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+``request.cached_setup`` was the precursor of the setup/teardown mechanism available to fixtures.
+
+Example:
+
+.. code-block:: python
+
+ @pytest.fixture
+ def db_session():
+ return request.cached_setup(
+ setup=Session.create, teardown=lambda session: session.close(), scope="module"
+ )
+
+This should be updated to make use of standard fixture mechanisms:
+
+.. code-block:: python
+
+ @pytest.fixture(scope="module")
+ def db_session():
+ session = Session.create()
+ yield session
+ session.close()
+
+
+You can consult :std:doc:`funcarg comparison section in the docs <funcarg_compare>` for
+more information.
+
+
+.. _pytest_plugins in non-top-level conftest files deprecated:
+
+pytest_plugins in non-top-level conftest files
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+Defining :globalvar:`pytest_plugins` is now deprecated in non-top-level conftest.py
+files because they will activate referenced plugins *globally*, which is surprising because for all other pytest
+features ``conftest.py`` files are only *active* for tests at or below it.
+
+
+.. _config.warn and node.warn deprecated:
+
+``Config.warn`` and ``Node.warn``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+Those methods were part of the internal pytest warnings system, but since ``3.8`` pytest is using the builtin warning
+system for its own warnings, so those two functions are now deprecated.
+
+``Config.warn`` should be replaced by calls to the standard ``warnings.warn``, example:
+
+.. code-block:: python
+
+ config.warn("C1", "some warning")
+
+Becomes:
+
+.. code-block:: python
+
+ warnings.warn(pytest.PytestWarning("some warning"))
+
+``Node.warn`` now supports two signatures:
+
+* ``node.warn(PytestWarning("some message"))``: is now the **recommended** way to call this function.
+ The warning instance must be a PytestWarning or subclass.
+
+* ``node.warn("CI", "some message")``: this code/message form has been **removed** and should be converted to the warning instance form above.
+
+.. _record_xml_property deprecated:
+
+record_xml_property
+~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+The ``record_xml_property`` fixture is now deprecated in favor of the more generic ``record_property``, which
+can be used by other consumers (for example ``pytest-html``) to obtain custom information about the test run.
+
+This is just a matter of renaming the fixture as the API is the same:
+
+.. code-block:: python
+
+ def test_foo(record_xml_property):
+ ...
+
+Change to:
+
+.. code-block:: python
+
+ def test_foo(record_property):
+ ...
+
+
+.. _passing command-line string to pytest.main deprecated:
+
+Passing command-line string to ``pytest.main()``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+Passing a command-line string to ``pytest.main()`` is deprecated:
+
+.. code-block:: python
+
+ pytest.main("-v -s")
+
+Pass a list instead:
+
+.. code-block:: python
+
+ pytest.main(["-v", "-s"])
+
+
+By passing a string, users expect that pytest will interpret that command-line using the shell rules they are working
+on (for example ``bash`` or ``Powershell``), but this is very hard/impossible to do in a portable way.
+
+
+.. _calling fixtures directly deprecated:
+
+Calling fixtures directly
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+Calling a fixture function directly, as opposed to request them in a test function, is deprecated.
+
+For example:
+
+.. code-block:: python
+
+ @pytest.fixture
+ def cell():
+ return ...
+
+
+ @pytest.fixture
+ def full_cell():
+ cell = cell()
+ cell.make_full()
+ return cell
+
+This is a great source of confusion to new users, which will often call the fixture functions and request them from test functions interchangeably, which breaks the fixture resolution model.
+
+In those cases just request the function directly in the dependent fixture:
+
+.. code-block:: python
+
+ @pytest.fixture
+ def cell():
+ return ...
+
+
+ @pytest.fixture
+ def full_cell(cell):
+ cell.make_full()
+ return cell
+
+Alternatively if the fixture function is called multiple times inside a test (making it hard to apply the above pattern) or
+if you would like to make minimal changes to the code, you can create a fixture which calls the original function together
+with the ``name`` parameter:
+
+.. code-block:: python
+
+ def cell():
+ return ...
+
+
+ @pytest.fixture(name="cell")
+ def cell_fixture():
+ return cell()
+
+
+.. _yield tests deprecated:
+
+``yield`` tests
+~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+pytest supported ``yield``-style tests, where a test function actually ``yield`` functions and values
+that are then turned into proper test methods. Example:
+
+.. code-block:: python
+
+ def check(x, y):
+ assert x ** x == y
+
+
+ def test_squared():
+ yield check, 2, 4
+ yield check, 3, 9
+
+This would result into two actual test functions being generated.
+
+This form of test function doesn't support fixtures properly, and users should switch to ``pytest.mark.parametrize``:
+
+.. code-block:: python
+
+ @pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
+ def test_squared(x, y):
+ assert x ** x == y
+
+.. _internal classes accessed through node deprecated:
+
+Internal classes accessed through ``Node``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances now issue
+this warning:
+
+.. code-block:: text
+
+ usage of Function.Module is deprecated, please use pytest.Module instead
+
+Users should just ``import pytest`` and access those objects using the ``pytest`` module.
+
+This has been documented as deprecated for years, but only now we are actually emitting deprecation warnings.
+
+``Node.get_marker``
+~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+As part of a large :ref:`marker-revamp`, ``_pytest.nodes.Node.get_marker`` is removed. See
+:ref:`the documentation <update marker code>` on tips on how to update your code.
+
+
+``somefunction.markname``
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+As part of a large :ref:`marker-revamp` we already deprecated using ``MarkInfo``
+the only correct way to get markers of an element is via ``node.iter_markers(name)``.
+
+
+.. _pytest.namespace deprecated:
+
+``pytest_namespace``
+~~~~~~~~~~~~~~~~~~~~
+
+.. versionremoved:: 4.0
+
+This hook is deprecated because it greatly complicates the pytest internals regarding configuration and initialization, making some
+bug fixes and refactorings impossible.
+
+Example of usage:
+
+.. code-block:: python
+
+ class MySymbol:
+ ...
+
+
+ def pytest_namespace():
+ return {"my_symbol": MySymbol()}
+
+
+Plugin authors relying on this hook should instead require that users now import the plugin modules directly (with an appropriate public API).
+
+As a stopgap measure, plugin authors may still inject their names into pytest's namespace, usually during ``pytest_configure``:
+
+.. code-block:: python
+
+ import pytest
+
+
+ def pytest_configure():
+ pytest.my_symbol = MySymbol()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/development_guide.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/development_guide.rst
new file mode 100644
index 0000000000..3ee0ebbc23
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/development_guide.rst
@@ -0,0 +1,7 @@
+=================
+Development Guide
+=================
+
+The contributing guidelines are to be found :ref:`here <contributing>`.
+The release procedure for pytest is documented on
+`GitHub <https://github.com/pytest-dev/pytest/blob/main/RELEASING.rst>`_.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/failure_demo.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/failure_demo.py
new file mode 100644
index 0000000000..abb9bce509
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/failure_demo.py
@@ -0,0 +1,281 @@
+import pytest
+from pytest import raises
+
+
+def otherfunc(a, b):
+ assert a == b
+
+
+def somefunc(x, y):
+ otherfunc(x, y)
+
+
+def otherfunc_multi(a, b):
+ assert a == b
+
+
+@pytest.mark.parametrize("param1, param2", [(3, 6)])
+def test_generative(param1, param2):
+ assert param1 * 2 < param2
+
+
+class TestFailing:
+ def test_simple(self):
+ def f():
+ return 42
+
+ def g():
+ return 43
+
+ assert f() == g()
+
+ def test_simple_multiline(self):
+ otherfunc_multi(42, 6 * 9)
+
+ def test_not(self):
+ def f():
+ return 42
+
+ assert not f()
+
+
+class TestSpecialisedExplanations:
+ def test_eq_text(self):
+ assert "spam" == "eggs"
+
+ def test_eq_similar_text(self):
+ assert "foo 1 bar" == "foo 2 bar"
+
+ def test_eq_multiline_text(self):
+ assert "foo\nspam\nbar" == "foo\neggs\nbar"
+
+ def test_eq_long_text(self):
+ a = "1" * 100 + "a" + "2" * 100
+ b = "1" * 100 + "b" + "2" * 100
+ assert a == b
+
+ def test_eq_long_text_multiline(self):
+ a = "1\n" * 100 + "a" + "2\n" * 100
+ b = "1\n" * 100 + "b" + "2\n" * 100
+ assert a == b
+
+ def test_eq_list(self):
+ assert [0, 1, 2] == [0, 1, 3]
+
+ def test_eq_list_long(self):
+ a = [0] * 100 + [1] + [3] * 100
+ b = [0] * 100 + [2] + [3] * 100
+ assert a == b
+
+ def test_eq_dict(self):
+ assert {"a": 0, "b": 1, "c": 0} == {"a": 0, "b": 2, "d": 0}
+
+ def test_eq_set(self):
+ assert {0, 10, 11, 12} == {0, 20, 21}
+
+ def test_eq_longer_list(self):
+ assert [1, 2] == [1, 2, 3]
+
+ def test_in_list(self):
+ assert 1 in [0, 2, 3, 4, 5]
+
+ def test_not_in_text_multiline(self):
+ text = "some multiline\ntext\nwhich\nincludes foo\nand a\ntail"
+ assert "foo" not in text
+
+ def test_not_in_text_single(self):
+ text = "single foo line"
+ assert "foo" not in text
+
+ def test_not_in_text_single_long(self):
+ text = "head " * 50 + "foo " + "tail " * 20
+ assert "foo" not in text
+
+ def test_not_in_text_single_long_term(self):
+ text = "head " * 50 + "f" * 70 + "tail " * 20
+ assert "f" * 70 not in text
+
+ def test_eq_dataclass(self):
+ from dataclasses import dataclass
+
+ @dataclass
+ class Foo:
+ a: int
+ b: str
+
+ left = Foo(1, "b")
+ right = Foo(1, "c")
+ assert left == right
+
+ def test_eq_attrs(self):
+ import attr
+
+ @attr.s
+ class Foo:
+ a = attr.ib()
+ b = attr.ib()
+
+ left = Foo(1, "b")
+ right = Foo(1, "c")
+ assert left == right
+
+
+def test_attribute():
+ class Foo:
+ b = 1
+
+ i = Foo()
+ assert i.b == 2
+
+
+def test_attribute_instance():
+ class Foo:
+ b = 1
+
+ assert Foo().b == 2
+
+
+def test_attribute_failure():
+ class Foo:
+ def _get_b(self):
+ raise Exception("Failed to get attrib")
+
+ b = property(_get_b)
+
+ i = Foo()
+ assert i.b == 2
+
+
+def test_attribute_multiple():
+ class Foo:
+ b = 1
+
+ class Bar:
+ b = 2
+
+ assert Foo().b == Bar().b
+
+
+def globf(x):
+ return x + 1
+
+
+class TestRaises:
+ def test_raises(self):
+ s = "qwe"
+ raises(TypeError, int, s)
+
+ def test_raises_doesnt(self):
+ raises(OSError, int, "3")
+
+ def test_raise(self):
+ raise ValueError("demo error")
+
+ def test_tupleerror(self):
+ a, b = [1] # NOQA
+
+ def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
+ items = [1, 2, 3]
+ print(f"items is {items!r}")
+ a, b = items.pop()
+
+ def test_some_error(self):
+ if namenotexi: # NOQA
+ pass
+
+ def func1(self):
+ assert 41 == 42
+
+
+# thanks to Matthew Scott for this test
+def test_dynamic_compile_shows_nicely():
+ import importlib.util
+ import sys
+
+ src = "def foo():\n assert 1 == 0\n"
+ name = "abc-123"
+ spec = importlib.util.spec_from_loader(name, loader=None)
+ module = importlib.util.module_from_spec(spec)
+ code = compile(src, name, "exec")
+ exec(code, module.__dict__)
+ sys.modules[name] = module
+ module.foo()
+
+
+class TestMoreErrors:
+ def test_complex_error(self):
+ def f():
+ return 44
+
+ def g():
+ return 43
+
+ somefunc(f(), g())
+
+ def test_z1_unpack_error(self):
+ items = []
+ a, b = items
+
+ def test_z2_type_error(self):
+ items = 3
+ a, b = items
+
+ def test_startswith(self):
+ s = "123"
+ g = "456"
+ assert s.startswith(g)
+
+ def test_startswith_nested(self):
+ def f():
+ return "123"
+
+ def g():
+ return "456"
+
+ assert f().startswith(g())
+
+ def test_global_func(self):
+ assert isinstance(globf(42), float)
+
+ def test_instance(self):
+ self.x = 6 * 7
+ assert self.x != 42
+
+ def test_compare(self):
+ assert globf(10) < 5
+
+ def test_try_finally(self):
+ x = 1
+ try:
+ assert x == 0
+ finally:
+ x = 0
+
+
+class TestCustomAssertMsg:
+ def test_single_line(self):
+ class A:
+ a = 1
+
+ b = 2
+ assert A.a == b, "A.a appears not to be b"
+
+ def test_multiline(self):
+ class A:
+ a = 1
+
+ b = 2
+ assert (
+ A.a == b
+ ), "A.a appears not to be b\nor does not appear to be b\none of those"
+
+ def test_custom_repr(self):
+ class JSON:
+ a = 1
+
+ def __repr__(self):
+ return "This is JSON\n{\n 'foo': 'bar'\n}"
+
+ a = JSON()
+ b = 2
+ assert a.a == b, a
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/global_testmodule_config/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/global_testmodule_config/conftest.py
new file mode 100644
index 0000000000..7cdf18cdbc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/global_testmodule_config/conftest.py
@@ -0,0 +1,14 @@
+import os.path
+
+import pytest
+
+mydir = os.path.dirname(__file__)
+
+
+def pytest_runtest_setup(item):
+ if isinstance(item, pytest.Function):
+ if not item.fspath.relto(mydir):
+ return
+ mod = item.getparent(pytest.Module).obj
+ if hasattr(mod, "hello"):
+ print(f"mod.hello {mod.hello!r}")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/global_testmodule_config/test_hello_world.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/global_testmodule_config/test_hello_world.py
new file mode 100644
index 0000000000..a31a601a1c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/global_testmodule_config/test_hello_world.py
@@ -0,0 +1,5 @@
+hello = "world"
+
+
+def test_func():
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/test_failures.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/test_failures.py
new file mode 100644
index 0000000000..350518b43c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/test_failures.py
@@ -0,0 +1,13 @@
+import os.path
+import shutil
+
+failure_demo = os.path.join(os.path.dirname(__file__), "failure_demo.py")
+pytest_plugins = ("pytester",)
+
+
+def test_failure_demo_fails_properly(pytester):
+ target = pytester.path.joinpath(os.path.basename(failure_demo))
+ shutil.copy(failure_demo, target)
+ result = pytester.runpytest(target, syspathinsert=True)
+ result.stdout.fnmatch_lines(["*44 failed*"])
+ assert result.ret != 0
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/test_setup_flow_example.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/test_setup_flow_example.py
new file mode 100644
index 0000000000..0e7eded06b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/assertion/test_setup_flow_example.py
@@ -0,0 +1,44 @@
+def setup_module(module):
+ module.TestStateFullThing.classcount = 0
+
+
+class TestStateFullThing:
+ def setup_class(cls):
+ cls.classcount += 1
+
+ def teardown_class(cls):
+ cls.classcount -= 1
+
+ def setup_method(self, method):
+ self.id = eval(method.__name__[5:])
+
+ def test_42(self):
+ assert self.classcount == 1
+ assert self.id == 42
+
+ def test_23(self):
+ assert self.classcount == 1
+ assert self.id == 23
+
+
+def teardown_module(module):
+ assert module.TestStateFullThing.classcount == 0
+
+
+""" For this example the control flow happens as follows::
+ import test_setup_flow_example
+ setup_module(test_setup_flow_example)
+ setup_class(TestStateFullThing)
+ instance = TestStateFullThing()
+ setup_method(instance, instance.test_42)
+ instance.test_42()
+ setup_method(instance, instance.test_23)
+ instance.test_23()
+ teardown_class(TestStateFullThing)
+ teardown_module(test_setup_flow_example)
+
+Note that ``setup_class(TestStateFullThing)`` is called and not
+``TestStateFullThing.setup_class()`` which would require you
+to insert ``setup_class = classmethod(setup_class)`` to make
+your setup function callable.
+"""
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/attic.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/attic.rst
new file mode 100644
index 0000000000..2ea8700620
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/attic.rst
@@ -0,0 +1,83 @@
+
+.. _`accept example`:
+
+example: specifying and selecting acceptance tests
+--------------------------------------------------------------
+
+.. sourcecode:: python
+
+ # ./conftest.py
+ def pytest_option(parser):
+ group = parser.getgroup("myproject")
+ group.addoption(
+ "-A", dest="acceptance", action="store_true", help="run (slow) acceptance tests"
+ )
+
+
+ def pytest_funcarg__accept(request):
+ return AcceptFixture(request)
+
+
+ class AcceptFixture:
+ def __init__(self, request):
+ if not request.config.getoption("acceptance"):
+ pytest.skip("specify -A to run 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. """
+ self.tmpdir.chdir()
+ return subprocess.check_output(cmd).decode()
+
+
+and the actual test function example:
+
+.. sourcecode:: python
+
+ def test_some_acceptance_aspect(accept):
+ accept.tmpdir.mkdir("somesub")
+ result = accept.run("ls", "-la")
+ assert "somesub" in result
+
+If you run this test without specifying a command line option
+the test will get skipped with an appropriate message. Otherwise
+you can start to add convenience and test support methods
+to your AcceptFixture and drive running of tools or
+applications and provide ways to do assertions about
+the output.
+
+.. _`decorate a funcarg`:
+
+example: decorating a funcarg in a test module
+--------------------------------------------------------------
+
+For larger scale setups it's sometimes useful to decorate
+a funcarg just for a particular test module. We can
+extend the `accept example`_ by putting this in our test module:
+
+.. sourcecode:: python
+
+ def pytest_funcarg__accept(request):
+ # call the next factory (living in our conftest.py)
+ arg = request.getfuncargvalue("accept")
+ # create a special layout in our tempdir
+ arg.tmpdir.mkdir("special")
+ return arg
+
+
+ class TestSpecialAcceptance:
+ def test_sometest(self, accept):
+ assert accept.tmpdir.join("special").check()
+
+Our module level factory will be invoked first and it can
+ask its request object to call the next factory and then
+decorate its result. This mechanism allows us to stay
+ignorant of how/where the function argument is provided -
+in our example from a `conftest plugin`_.
+
+sidenote: the temporary directory used here are instances of
+the `py.path.local`_ class which provides many of the os.path
+methods in a convenient way.
+
+.. _`py.path.local`: ../path.html#local
+.. _`conftest plugin`: customize.html#conftestplugin
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/conftest.py
new file mode 100644
index 0000000000..f905738c4f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/conftest.py
@@ -0,0 +1 @@
+collect_ignore = ["nonpython"]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/fixture_availability.svg b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/fixture_availability.svg
new file mode 100644
index 0000000000..066caac344
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/fixture_availability.svg
@@ -0,0 +1,132 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="572" height="542">
+ <style>
+ text {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ dominant-baseline: middle;
+ text-anchor: middle;
+ fill: #062886;
+ font-size: medium;
+ }
+ ellipse.fixture, rect.test {
+ fill: #eeffcc;
+ stroke: #007020;
+ stroke-width: 2;
+ }
+ text.fixture {
+ color: #06287e;
+ }
+ circle.class, circle.module, circle.package {
+ fill: #c3e0ec;
+ stroke: #0e84b5;
+ stroke-width: 2;
+ }
+ text.class, text.module, text.package {
+ fill: #0e84b5;
+ }
+ line, path {
+ stroke: black;
+ stroke-width: 2;
+ fill: none;
+ }
+ </style>
+
+ <!-- main scope -->
+ <circle class="package" r="270" cx="286" cy="271" />
+ <!-- scope name -->
+ <defs>
+ <path d="M 26,271 A 260 260 0 0 1 546 271" id="testp"/>
+ </defs>
+ <text class="package">
+ <textPath xlink:href="#testp" startOffset="50%">tests</textPath>
+ </text>
+
+ <!-- subpackage -->
+ <circle class="package" r="140" cx="186" cy="271" />
+ <!-- scope name -->
+ <defs>
+ <path d="M 56,271 A 130 130 0 0 1 316 271" id="subpackage"/>
+ </defs>
+ <text class="package">
+ <textPath xlink:href="#subpackage" startOffset="50%">subpackage</textPath>
+ </text>
+
+ <!-- test_subpackage.py -->
+ <circle class="module" r="90" cx="186" cy="311" />
+ <!-- scope name -->
+ <defs>
+ <path d="M 106,311 A 80 80 0 0 1 266 311" id="testSubpackage"/>
+ </defs>
+ <text class="module">
+ <textPath xlink:href="#testSubpackage" startOffset="50%">test_subpackage.py</textPath>
+ </text>
+ <!-- innermost -->
+ <line x1="186" x2="186" y1="271" y2="351"/>
+ <!-- mid -->
+ <path d="M 186 351 L 136 351 L 106 331 L 106 196" />
+ <!-- order -->
+ <path d="M 186 351 L 256 351 L 316 291 L 316 136" />
+ <!-- top -->
+ <path d="M 186 351 L 186 391 L 231 436 L 331 436" />
+ <ellipse class="fixture" rx="50" ry="25" cx="186" cy="271" />
+ <text x="186" y="271">innermost</text>
+ <rect class="test" width="110" height="50" x="131" y="326" />
+ <text x="186" y="351">test_order</text>
+ <ellipse class="fixture" rx="50" ry="25" cx="126" cy="196" />
+ <text x="126" y="196">mid</text>
+ <!-- scope order number -->
+ <mask id="testSubpackageOrderMask">
+ <rect x="0" y="0" width="100%" height="100%" fill="white"/>
+ <circle fill="black" stroke="white" stroke-width="2" r="90" cx="186" cy="311" />
+ </mask>
+ <circle class="module" r="15" cx="96" cy="311" mask="url(#testSubpackageOrderMask)"/>
+ <text class="module" x="96" y="311">1</text>
+ <!-- scope order number -->
+ <mask id="subpackageOrderMask">
+ <rect x="0" y="0" width="100%" height="100%" fill="white"/>
+ <circle fill="black" stroke="white" stroke-width="2" r="140" cx="186" cy="271" />
+ </mask>
+ <circle class="module" r="15" cx="46" cy="271" mask="url(#subpackageOrderMask)"/>
+ <text class="module" x="46" y="271">2</text>
+ <!-- scope order number -->
+ <mask id="testsOrderMask">
+ <rect x="0" y="0" width="100%" height="100%" fill="white"/>
+ <circle fill="black" stroke="white" stroke-width="2" r="270" cx="286" cy="271" />
+ </mask>
+ <circle class="module" r="15" cx="16" cy="271" mask="url(#testsOrderMask)"/>
+ <text class="module" x="16" y="271">3</text>
+
+ <!-- test_top.py -->
+ <circle class="module" r="85" cx="441" cy="271" />
+ <!-- scope name -->
+ <defs>
+ <path d="M 366,271 A 75 75 0 0 1 516 271" id="testTop"/>
+ </defs>
+ <text class="module">
+ <textPath xlink:href="#testTop" startOffset="50%">test_top.py</textPath>
+ </text>
+ <!-- innermost -->
+ <line x1="441" x2="441" y1="306" y2="236"/>
+ <!-- order -->
+ <path d="M 441 306 L 376 306 L 346 276 L 346 136" />
+ <!-- top -->
+ <path d="M 441 306 L 441 411 L 411 436 L 331 436" />
+ <ellipse class="fixture" rx="50" ry="25" cx="441" cy="236" />
+ <text x="441" y="236">innermost</text>
+ <rect class="test" width="110" height="50" x="386" y="281" />
+ <text x="441" y="306">test_order</text>
+ <!-- scope order number -->
+ <mask id="testTopOrderMask">
+ <rect x="0" y="0" width="100%" height="100%" fill="white"/>
+ <circle fill="black" stroke="white" stroke-width="2" r="85" cx="441" cy="271" />
+ </mask>
+ <circle class="module" r="15" cx="526" cy="271" mask="url(#testTopOrderMask)"/>
+ <text class="module" x="526" y="271">1</text>
+ <!-- scope order number -->
+ <circle class="module" r="15" cx="556" cy="271" mask="url(#testsOrderMask)"/>
+ <text class="module" x="556" y="271">2</text>
+
+ <ellipse class="fixture" rx="50" ry="25" cx="331" cy="436" />
+ <text x="331" y="436">top</text>
+ <ellipse class="fixture" rx="50" ry="25" cx="331" cy="136" />
+ <text x="331" y="136">order</text>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/fixture_availability_plugins.svg b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/fixture_availability_plugins.svg
new file mode 100644
index 0000000000..36e3005507
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/fixture_availability_plugins.svg
@@ -0,0 +1,142 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="587" height="382">
+ <style>
+ text {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ dominant-baseline: middle;
+ text-anchor: middle;
+ fill: #062886;
+ font-size: medium;
+ alignment-baseline: center;
+ }
+ ellipse.fixture, rect.test {
+ fill: #eeffcc;
+ stroke: #007020;
+ stroke-width: 2;
+ }
+ text.fixture {
+ color: #06287e;
+ }
+ circle.class, circle.module, circle.package, circle.plugin {
+ fill: #c3e0ec;
+ stroke: #0e84b5;
+ stroke-width: 2;
+ }
+ text.class, text.module, text.package, text.plugin {
+ fill: #0e84b5;
+ }
+ line, path {
+ stroke: black;
+ stroke-width: 2;
+ fill: none;
+ }
+ </style>
+
+ <!-- plugin_a.py scope -->
+ <circle class="plugin" r="85" cx="486" cy="86" />
+ <!-- plugin name -->
+ <defs>
+ <path d="M 411,86 A 75 75 0 0 1 561 86" id="pluginA"/>
+ </defs>
+ <text class="plugin">
+ <textPath xlink:href="#pluginA" startOffset="50%">plugin_a</textPath>
+ </text>
+ <!-- scope order number -->
+ <mask id="pluginAOrderMask">
+ <rect x="0" y="0" width="100%" height="100%" fill="white"/>
+ <circle fill="black" stroke="white" stroke-width="2" r="85" cx="486" cy="86" />
+ </mask>
+ <circle class="module" r="15" cx="571" cy="86" mask="url(#pluginAOrderMask)"/>
+ <text class="module" x="571" y="86">4</text>
+
+ <!-- plugin_b.py scope -->
+ <circle class="plugin" r="85" cx="486" cy="296" />
+ <!-- plugin name -->
+ <defs>
+ <path d="M 411,296 A 75 75 0 0 1 561 296" id="pluginB"/>
+ </defs>
+ <text class="plugin">
+ <textPath xlink:href="#pluginB" startOffset="50%">plugin_b</textPath>
+ </text>
+ <!-- scope order number -->
+ <mask id="pluginBOrderMask">
+ <rect x="0" y="0" width="100%" height="100%" fill="white"/>
+ <circle fill="black" stroke="white" stroke-width="2" r="85" cx="486" cy="296" />
+ </mask>
+ <circle class="module" r="15" cx="571" cy="296" mask="url(#pluginBOrderMask)"/>
+ <text class="module" x="571" y="296">4</text>
+
+ <!-- main scope -->
+ <circle class="package" r="190" cx="191" cy="191" />
+ <!-- scope name -->
+ <defs>
+ <path d="M 11,191 A 180 180 0 0 1 371 191" id="testp"/>
+ </defs>
+ <text class="package">
+ <textPath xlink:href="#testp" startOffset="50%">tests</textPath>
+ </text>
+ <!-- scope order number -->
+ <mask id="mainOrderMask">
+ <rect x="0" y="0" width="100%" height="100%" fill="white"/>
+ <circle fill="black" stroke="white" stroke-width="2" r="190" cx="191" cy="191" />
+ </mask>
+ <circle class="module" r="15" cx="381" cy="191" mask="url(#mainOrderMask)"/>
+ <text class="module" x="381" y="191">3</text>
+
+ <!-- subpackage -->
+ <circle class="package" r="140" cx="191" cy="231" />
+ <!-- scope name -->
+ <defs>
+ <path d="M 61,231 A 130 130 0 0 1 321 231" id="subpackage"/>
+ </defs>
+ <text class="package">
+ <textPath xlink:href="#subpackage" startOffset="50%">subpackage</textPath>
+ </text>
+ <!-- scope order number -->
+ <mask id="subpackageOrderMask">
+ <rect x="0" y="0" width="100%" height="100%" fill="white"/>
+ <circle fill="black" stroke="white" stroke-width="2" r="140" cx="191" cy="231" />
+ </mask>
+ <circle class="module" r="15" cx="331" cy="231" mask="url(#subpackageOrderMask)"/>
+ <text class="module" x="331" y="231">2</text>
+
+ <!-- test_subpackage.py -->
+ <circle class="module" r="90" cx="191" cy="271" />
+ <!-- scope name -->
+ <defs>
+ <path d="M 111,271 A 80 80 0 0 1 271 271" id="testSubpackage"/>
+ </defs>
+ <text class="module">
+ <textPath xlink:href="#testSubpackage" startOffset="50%">test_subpackage.py</textPath>
+ </text>
+ <!-- scope order number -->
+ <mask id="testSubpackageOrderMask">
+ <rect x="0" y="0" width="100%" height="100%" fill="white"/>
+ <circle fill="black" stroke="white" stroke-width="2" r="90" cx="191" cy="271" />
+ </mask>
+ <circle class="module" r="15" cx="281" cy="271" mask="url(#testSubpackageOrderMask)"/>
+ <text class="module" x="281" y="271">1</text>
+
+ <!-- innermost -->
+ <line x1="191" x2="191" y1="231" y2="311"/>
+ <!-- mid -->
+ <path d="M 191 306 L 101 306 L 91 296 L 91 156 L 101 146 L 191 146" />
+ <!-- order -->
+ <path d="M 191 316 L 91 316 L 81 306 L 81 61 L 91 51 L 191 51" />
+ <!-- a_fix -->
+ <path d="M 191 306 L 291 306 L 301 296 L 301 96 L 311 86 L 486 86" />
+ <!-- b_fix -->
+ <path d="M 191 316 L 316 316 L 336 296 L 486 296" />
+ <ellipse class="fixture" rx="50" ry="25" cx="191" cy="231" />
+ <text x="191" y="231">inner</text>
+ <rect class="test" width="110" height="50" x="136" y="286" />
+ <text x="191" y="311">test_order</text>
+ <ellipse class="fixture" rx="50" ry="25" cx="191" cy="146" />
+ <text x="191" y="146">mid</text>
+ <ellipse class="fixture" rx="50" ry="25" cx="191" cy="51" />
+ <text x="191" y="51">order</text>
+
+ <ellipse class="fixture" rx="50" ry="25" cx="486" cy="86" />
+ <text x="486" y="86">a_fix</text>
+ <ellipse class="fixture" rx="50" ry="25" cx="486" cy="296" />
+ <text x="486" y="296">b_fix</text>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse.py
new file mode 100644
index 0000000000..ec282ab4b2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse.py
@@ -0,0 +1,45 @@
+import pytest
+
+
+@pytest.fixture
+def order():
+ return []
+
+
+@pytest.fixture
+def a(order):
+ order.append("a")
+
+
+@pytest.fixture
+def b(a, order):
+ order.append("b")
+
+
+@pytest.fixture(autouse=True)
+def c(b, order):
+ order.append("c")
+
+
+@pytest.fixture
+def d(b, order):
+ order.append("d")
+
+
+@pytest.fixture
+def e(d, order):
+ order.append("e")
+
+
+@pytest.fixture
+def f(e, order):
+ order.append("f")
+
+
+@pytest.fixture
+def g(f, c, order):
+ order.append("g")
+
+
+def test_order_and_g(g, order):
+ assert order == ["a", "b", "c", "d", "e", "f", "g"]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse.svg b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse.svg
new file mode 100644
index 0000000000..36362e4fb0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse.svg
@@ -0,0 +1,64 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="252" height="682">
+ <style>
+ text {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ dominant-baseline: middle;
+ text-anchor: middle;
+ fill: #062886;
+ font-size: medium;
+ }
+ ellipse.fixture, rect.test {
+ fill: #eeffcc;
+ stroke: #007020;
+ stroke-width: 2;
+ }
+ text.fixture {
+ color: #06287e;
+ }
+ circle.class {
+ fill: #c3e0ec;
+ stroke: #0e84b5;
+ stroke-width: 2;
+ }
+ text.class {
+ fill: #0e84b5;
+ }
+ path, line {
+ stroke: black;
+ stroke-width: 2;
+ fill: none;
+ }
+ rect.autouse {
+ fill: #ca7f3d;
+ }
+ </style>
+ <path d="M126,586 L26,506 L26,236" />
+ <path d="M226,446 L226,236 L126,166" />
+ <line x1="126" x2="126" y1="656" y2="516" />
+ <line x1="126" x2="226" y1="516" y2="446" />
+ <line x1="226" x2="126" y1="446" y2="376" />
+ <line x1="126" x2="126" y1="376" y2="166" />
+ <line x1="26" x2="126" y1="236" y2="166" />
+ <line x1="126" x2="126" y1="166" y2="26" />
+ <line x1="126" x2="126" y1="96" y2="26" />
+ <rect class="autouse" width="251" height="40" x="0" y="286" />
+ <text x="126" y="306">autouse</text>
+ <ellipse class="fixture" rx="50" ry="25" cx="126" cy="26" />
+ <text x="126" y="26">order</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="126" cy="96" />
+ <text x="126" y="96">a</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="126" cy="166" />
+ <text x="126" y="166">b</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="26" cy="236" />
+ <text x="26" y="236">c</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="126" cy="376" />
+ <text x="126" y="376">d</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="226" cy="446" />
+ <text x="226" y="446">e</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="126" cy="516" />
+ <text x="126" y="516">f</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="126" cy="586" />
+ <text x="126" y="586">g</text>
+ <rect class="test" width="110" height="50" x="71" y="631" />
+ <text x="126" y="656">test_order</text>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_flat.svg b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_flat.svg
new file mode 100644
index 0000000000..03c4598272
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_flat.svg
@@ -0,0 +1,56 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="112" height="682">
+ <style>
+ text {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ dominant-baseline: middle;
+ text-anchor: middle;
+ fill: #062886;
+ font-size: medium;
+ }
+ ellipse.fixture, rect.test {
+ fill: #eeffcc;
+ stroke: #007020;
+ stroke-width: 2;
+ }
+ text.fixture {
+ color: #06287e;
+ }
+ circle.class {
+ fill: #c3e0ec;
+ stroke: #0e84b5;
+ stroke-width: 2;
+ }
+ text.class {
+ fill: #0e84b5;
+ }
+ path, line {
+ stroke: black;
+ stroke-width: 2;
+ fill: none;
+ }
+ rect.autouse {
+ fill: #ca7f3d;
+ }
+ </style>
+ <line x1="56" x2="56" y1="681" y2="26" />
+ <ellipse class="fixture" rx="50" ry="25" cx="56" cy="26" />
+ <text x="56" y="26">order</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="96" />
+ <text x="56" y="96">a</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="166" />
+ <text x="56" y="166">b</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="236" />
+ <text x="56" y="236">c</text>
+ <rect class="autouse" width="112" height="40" x="0" y="286" />
+ <text x="56" y="306">autouse</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="376" />
+ <text x="56" y="376">d</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="446" />
+ <text x="56" y="446">e</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="516" />
+ <text x="56" y="516">f</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="586" />
+ <text x="56" y="586">g</text>
+ <rect class="test" width="110" height="50" x="1" y="631" />
+ <text x="56" y="656">test_order</text>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.py
new file mode 100644
index 0000000000..de0c264279
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.py
@@ -0,0 +1,31 @@
+import pytest
+
+
+@pytest.fixture(scope="class")
+def order():
+ return []
+
+
+@pytest.fixture(scope="class", autouse=True)
+def c1(order):
+ order.append("c1")
+
+
+@pytest.fixture(scope="class")
+def c2(order):
+ order.append("c2")
+
+
+@pytest.fixture(scope="class")
+def c3(order, c1):
+ order.append("c3")
+
+
+class TestClassWithC1Request:
+ def test_order(self, order, c1, c3):
+ assert order == ["c1", "c3"]
+
+
+class TestClassWithoutC1Request:
+ def test_order(self, order, c2):
+ assert order == ["c1", "c2"]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.svg b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.svg
new file mode 100644
index 0000000000..fe5772993e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.svg
@@ -0,0 +1,76 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="862" height="402">
+ <style>
+ text {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ dominant-baseline: middle;
+ text-anchor: middle;
+ fill: #062886;
+ font-size: medium;
+ }
+ ellipse.fixture, rect.test {
+ fill: #eeffcc;
+ stroke: #007020;
+ stroke-width: 2;
+ }
+ text.fixture {
+ color: #06287e;
+ }
+ circle.class {
+ fill: #c3e0ec;
+ stroke: #0e84b5;
+ stroke-width: 2;
+ }
+ text.class {
+ fill: #0e84b5;
+ }
+ line {
+ stroke: black;
+ stroke-width: 2;
+ }
+ rect.autouse {
+ fill: #ca7f3d;
+ }
+ </style>
+
+ <!-- TestWithC1Request -->
+ <circle class="class" r="200" cx="221" cy="201" />
+ <line x1="221" x2="221" y1="61" y2="316"/>
+ <ellipse class="fixture" rx="50" ry="25" cx="221" cy="61" />
+ <text x="221" y="61">order</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="221" cy="131" />
+ <text x="221" y="131">c1</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="221" cy="271" />
+ <text x="221" y="271">c3</text>
+ <rect class="test" width="110" height="50" x="166" y="316" />
+ <text x="221" y="341">test_order</text>
+ <!-- scope name -->
+ <defs>
+ <path d="M31,201 A 190 190 0 0 1 411 201" id="testClassWith"/>
+ </defs>
+ <text class="class">
+ <textPath xlink:href="#testClassWith" startOffset="50%">TestWithC1Request</textPath>
+ </text>
+
+ <!-- TestWithoutC1Request -->
+ <circle class="class" r="200" cx="641" cy="201" />
+ <line x1="641" x2="641" y1="61" y2="316"/>
+ <ellipse class="fixture" rx="50" ry="25" cx="641" cy="61" />
+ <text x="641" y="61">order</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="641" cy="131" />
+ <text x="641" y="131">c1</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="641" cy="271" />
+ <text x="641" y="271">c2</text>
+ <rect class="test" width="110" height="50" x="586" y="316" />
+ <text x="641" y="341">test_order</text>
+ <!-- scope name -->
+ <defs>
+ <path d="M451,201 A 190 190 0 0 1 831 201" id="testClassWithout"/>
+ </defs>
+ <text class="class">
+ <textPath xlink:href="#testClassWithout" startOffset="50%">TestWithoutC1Request</textPath>
+ </text>
+
+ <rect class="autouse" width="862" height="40" x="1" y="181" />
+ <rect width="10" height="100" class="autouse" x="426" y="151" />
+ <text x="431" y="201">autouse</text>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.py
new file mode 100644
index 0000000000..ba01ad32f5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.py
@@ -0,0 +1,36 @@
+import pytest
+
+
+@pytest.fixture
+def order():
+ return []
+
+
+@pytest.fixture
+def c1(order):
+ order.append("c1")
+
+
+@pytest.fixture
+def c2(order):
+ order.append("c2")
+
+
+class TestClassWithAutouse:
+ @pytest.fixture(autouse=True)
+ def c3(self, order, c2):
+ order.append("c3")
+
+ def test_req(self, order, c1):
+ assert order == ["c2", "c3", "c1"]
+
+ def test_no_req(self, order):
+ assert order == ["c2", "c3"]
+
+
+class TestClassWithoutAutouse:
+ def test_req(self, order, c1):
+ assert order == ["c1"]
+
+ def test_no_req(self, order):
+ assert order == []
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.svg b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.svg
new file mode 100644
index 0000000000..2a9f51673f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.svg
@@ -0,0 +1,100 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="862" height="502">
+ <style>
+ text {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ dominant-baseline: middle;
+ text-anchor: middle;
+ fill: #062886;
+ font-size: medium;
+ }
+ ellipse.fixture, rect.test {
+ fill: #eeffcc;
+ stroke: #007020;
+ stroke-width: 2;
+ }
+ text.fixture {
+ color: #06287e;
+ }
+ circle.class {
+ fill: #c3e0ec;
+ stroke: #0e84b5;
+ stroke-width: 2;
+ }
+ text.class {
+ fill: #0e84b5;
+ }
+ line {
+ stroke: black;
+ stroke-width: 2;
+ }
+ rect.autouse {
+ fill: #ca7f3d;
+ }
+ </style>
+
+ <!-- TestWithAutouse -->
+ <circle class="class" r="250" cx="251" cy="251" />
+ <!-- scope name -->
+ <defs>
+ <path d="M11,251 A 240 240 0 0 1 491 251" id="testClassWith"/>
+ </defs>
+ <text class="class">
+ <textPath xlink:href="#testClassWith" startOffset="50%">TestWithAutouse</textPath>
+ </text>
+ <mask id="autouseScope">
+ <circle fill="white" r="249" cx="251" cy="251" />
+ </mask>
+
+ <!-- TestWithAutouse.test_req -->
+ <line x1="176" x2="176" y1="76" y2="426"/>
+ <ellipse class="fixture" rx="50" ry="25" cx="176" cy="76" />
+ <text x="176" y="76">order</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="176" cy="146" />
+ <text x="176" y="146">c2</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="176" cy="216" />
+ <text x="176" y="216">c3</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="176" cy="356" />
+ <text x="176" y="356">c1</text>
+ <rect class="test" width="100" height="50" x="126" y="401" />
+ <text x="176" y="426">test_req</text>
+
+ <!-- TestWithAutouse.test_no_req -->
+ <line x1="326" x2="326" y1="76" y2="346"/>
+ <ellipse class="fixture" rx="50" ry="25" cx="326" cy="76" />
+ <text x="326" y="76">order</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="326" cy="146" />
+ <text x="326" y="146">c2</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="326" cy="216" />
+ <text x="326" y="216">c3</text>
+ <rect class="test" width="120" height="50" x="266" y="331" />
+ <text x="326" y="356">test_no_req</text>
+
+ <rect class="autouse" width="500" height="40" x="1" y="266" mask="url(#autouseScope)"/>
+ <text x="261" y="286">autouse</text>
+
+ <!-- TestWithoutAutouse -->
+ <circle class="class" r="170" cx="691" cy="251" />
+ <!-- scope name -->
+ <defs>
+ <path d="M 531,251 A 160 160 0 0 1 851 251" id="testClassWithout"/>
+ </defs>
+ <text class="class">
+ <textPath xlink:href="#testClassWithout" startOffset="50%">TestWithoutAutouse</textPath>
+ </text>
+
+ <!-- TestWithoutAutouse.test_req -->
+ <line x1="616" x2="616" y1="181" y2="321"/>
+ <ellipse class="fixture" rx="50" ry="25" cx="616" cy="181" />
+ <text x="616" y="181">order</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="616" cy="251" />
+ <text x="616" y="251">c1</text>
+ <rect class="test" width="100" height="50" x="566" y="296" />
+ <text x="616" y="321">test_req</text>
+
+ <!-- TestWithoutAutouse.test_no_req -->
+ <line x1="766" x2="766" y1="181" y2="251"/>
+ <ellipse class="fixture" rx="50" ry="25" cx="766" cy="181" />
+ <text x="766" y="181">order</text>
+ <rect class="test" width="120" height="50" x="706" y="226" />
+ <text x="766" y="251">test_no_req</text>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies.py
new file mode 100644
index 0000000000..b3512c2a64
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies.py
@@ -0,0 +1,45 @@
+import pytest
+
+
+@pytest.fixture
+def order():
+ return []
+
+
+@pytest.fixture
+def a(order):
+ order.append("a")
+
+
+@pytest.fixture
+def b(a, order):
+ order.append("b")
+
+
+@pytest.fixture
+def c(a, b, order):
+ order.append("c")
+
+
+@pytest.fixture
+def d(c, b, order):
+ order.append("d")
+
+
+@pytest.fixture
+def e(d, b, order):
+ order.append("e")
+
+
+@pytest.fixture
+def f(e, order):
+ order.append("f")
+
+
+@pytest.fixture
+def g(f, c, order):
+ order.append("g")
+
+
+def test_order(g, order):
+ assert order == ["a", "b", "c", "d", "e", "f", "g"]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies.svg b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies.svg
new file mode 100644
index 0000000000..24418e63c9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies.svg
@@ -0,0 +1,60 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="252" height="612">
+ <style>
+ text {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ dominant-baseline: middle;
+ text-anchor: middle;
+ fill: #062886;
+ font-size: medium;
+ }
+ ellipse.fixture, rect.test {
+ fill: #eeffcc;
+ stroke: #007020;
+ stroke-width: 2;
+ }
+ text.fixture {
+ color: #06287e;
+ }
+ circle.class {
+ fill: #c3e0ec;
+ stroke: #0e84b5;
+ stroke-width: 2;
+ }
+ text.class {
+ fill: #0e84b5;
+ }
+ path, line {
+ stroke: black;
+ stroke-width: 2;
+ fill: none;
+ }
+ </style>
+ <path d="M126,516 L26,436 L26,236" />
+ <path d="M226,376 L226,236 L126,166" />
+ <line x1="126" x2="126" y1="586" y2="446" />
+ <line x1="126" x2="226" y1="446" y2="376" />
+ <line x1="226" x2="126" y1="376" y2="306" />
+ <line x1="126" x2="26" y1="306" y2="236" />
+ <line x1="126" x2="126" y1="306" y2="166" />
+ <line x1="26" x2="126" y1="236" y2="166" />
+ <line x1="126" x2="126" y1="166" y2="26" />
+ <line x1="126" x2="126" y1="96" y2="26" />
+ <ellipse class="fixture" rx="50" ry="25" cx="126" cy="26" />
+ <text x="126" y="26">order</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="126" cy="96" />
+ <text x="126" y="96">a</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="126" cy="166" />
+ <text x="126" y="166">b</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="26" cy="236" />
+ <text x="26" y="236">c</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="126" cy="306" />
+ <text x="126" y="306">d</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="226" cy="376" />
+ <text x="226" y="376">e</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="126" cy="446" />
+ <text x="126" y="446">f</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="126" cy="516" />
+ <text x="126" y="516">g</text>
+ <rect class="test" width="110" height="50" x="71" y="561" />
+ <text x="126" y="586">test_order</text>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies_flat.svg b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies_flat.svg
new file mode 100644
index 0000000000..bbe7ad2833
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies_flat.svg
@@ -0,0 +1,51 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="112" height="612">
+ <style>
+ text {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ dominant-baseline: middle;
+ text-anchor: middle;
+ fill: #062886;
+ font-size: medium;
+ }
+ ellipse.fixture, rect.test {
+ fill: #eeffcc;
+ stroke: #007020;
+ stroke-width: 2;
+ }
+ text.fixture {
+ color: #06287e;
+ }
+ circle.class {
+ fill: #c3e0ec;
+ stroke: #0e84b5;
+ stroke-width: 2;
+ }
+ text.class {
+ fill: #0e84b5;
+ }
+ path, line {
+ stroke: black;
+ stroke-width: 2;
+ fill: none;
+ }
+ </style>
+ <line x1="56" x2="56" y1="611" y2="26" />
+ <ellipse class="fixture" rx="50" ry="25" cx="56" cy="26" />
+ <text x="56" y="26">order</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="96" />
+ <text x="56" y="96">a</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="166" />
+ <text x="56" y="166">b</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="236" />
+ <text x="56" y="236">c</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="306" />
+ <text x="56" y="306">d</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="376" />
+ <text x="56" y="376">e</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="446" />
+ <text x="56" y="446">f</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="56" cy="516" />
+ <text x="56" y="516">g</text>
+ <rect class="test" width="110" height="50" x="1" y="561" />
+ <text x="56" y="586">test_order</text>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies_unclear.svg b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies_unclear.svg
new file mode 100644
index 0000000000..150724f80a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies_unclear.svg
@@ -0,0 +1,60 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="252" height="542">
+ <style>
+ text {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ dominant-baseline: middle;
+ text-anchor: middle;
+ fill: #062886;
+ font-size: medium;
+ }
+ ellipse.fixture, rect.test {
+ fill: #eeffcc;
+ stroke: #007020;
+ stroke-width: 2;
+ }
+ text.fixture {
+ color: #06287e;
+ }
+ circle.class {
+ fill: #c3e0ec;
+ stroke: #0e84b5;
+ stroke-width: 2;
+ }
+ text.class {
+ fill: #0e84b5;
+ }
+ path, line {
+ stroke: black;
+ stroke-width: 2;
+ fill: none;
+ }
+ </style>
+ <path d="M126,446 L26,376 L26,236" />
+ <path d="M226,306 L126,236 L126,166" />
+ <line x1="126" x2="126" y1="516" y2="446" />
+ <line x1="226" x2="226" y1="376" y2="306" />
+ <line x1="226" x2="226" y1="306" y2="236" />
+ <line x1="226" x2="126" y1="236" y2="166" />
+ <line x1="126" x2="226" y1="446" y2="376" />
+ <line x1="26" x2="126" y1="236" y2="166" />
+ <line x1="126" x2="126" y1="166" y2="96" />
+ <line x1="126" x2="126" y1="96" y2="26" />
+ <ellipse class="fixture" rx="50" ry="25" cx="126" cy="26" />
+ <text x="126" y="26">order</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="126" cy="96" />
+ <text x="126" y="96">a</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="126" cy="166" />
+ <text x="126" y="166">b</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="26" cy="236" />
+ <text x="26" y="236">c</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="226" cy="236" />
+ <text x="226" y="236">d</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="226" cy="306" />
+ <text x="226" y="306">e</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="226" cy="376" />
+ <text x="226" y="376">f</text>
+ <ellipse class="fixture" rx="25" ry="25" cx="126" cy="446" />
+ <text x="126" y="446">g</text>
+ <rect class="test" width="110" height="50" x="71" y="491" />
+ <text x="126" y="516">test_order</text>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_scope.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_scope.py
new file mode 100644
index 0000000000..5d9487cab3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_scope.py
@@ -0,0 +1,36 @@
+import pytest
+
+
+@pytest.fixture(scope="session")
+def order():
+ return []
+
+
+@pytest.fixture
+def func(order):
+ order.append("function")
+
+
+@pytest.fixture(scope="class")
+def cls(order):
+ order.append("class")
+
+
+@pytest.fixture(scope="module")
+def mod(order):
+ order.append("module")
+
+
+@pytest.fixture(scope="package")
+def pack(order):
+ order.append("package")
+
+
+@pytest.fixture(scope="session")
+def sess(order):
+ order.append("session")
+
+
+class TestClass:
+ def test_order(self, func, cls, mod, pack, sess, order):
+ assert order == ["session", "package", "module", "class", "function"]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_scope.svg b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_scope.svg
new file mode 100644
index 0000000000..f38ee60f1f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_scope.svg
@@ -0,0 +1,55 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="262" height="537">
+ <style>
+ text {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ dominant-baseline: middle;
+ text-anchor: middle;
+ fill: #062886;
+ font-size: medium;
+ }
+ ellipse.fixture, rect.test {
+ fill: #eeffcc;
+ stroke: #007020;
+ stroke-width: 2;
+ }
+ text.fixture {
+ color: #06287e;
+ }
+ circle.class {
+ fill: #c3e0ec;
+ stroke: #0e84b5;
+ stroke-width: 2;
+ }
+ text.class {
+ fill: #0e84b5;
+ }
+ line {
+ stroke: black;
+ stroke-width: 2;
+ }
+ </style>
+ <!-- TestClass -->
+ <circle class="class" r="130" cx="131" cy="406" />
+ <line x1="131" x2="131" y1="21" y2="446"/>
+ <ellipse class="fixture" rx="50" ry="25" cx="131" cy="26" />
+ <text x="131" y="26">order</text>
+ <ellipse class="fixture" rx="50" ry="25" cx="131" cy="96" />
+ <text x="131" y="96">sess</text>
+ <ellipse class="fixture" rx="50" ry="25" cx="131" cy="166" />
+ <text x="131" y="166">pack</text>
+ <ellipse class="fixture" rx="50" ry="25" cx="131" cy="236" />
+ <text x="131" y="236">mod</text>
+ <ellipse class="fixture" rx="50" ry="25" cx="131" cy="306" />
+ <text x="131" y="306">cls</text>
+ <ellipse class="fixture" rx="50" ry="25" cx="131" cy="376" />
+ <text x="131" y="376">func</text>
+ <rect class="test" width="110" height="50" x="76" y="421" />
+ <text x="131" y="446">test_order</text>
+ <!-- scope name -->
+ <defs>
+ <path d="M131,526 A 120 120 0 0 1 136 286" id="testClass"/>
+ </defs>
+ <text class="class">
+ <textPath xlink:href="#testClass" startOffset="50%">TestClass</textPath>
+ </text>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_request_different_scope.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_request_different_scope.py
new file mode 100644
index 0000000000..00e2e46d84
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_request_different_scope.py
@@ -0,0 +1,29 @@
+import pytest
+
+
+@pytest.fixture
+def order():
+ return []
+
+
+@pytest.fixture
+def outer(order, inner):
+ order.append("outer")
+
+
+class TestOne:
+ @pytest.fixture
+ def inner(self, order):
+ order.append("one")
+
+ def test_order(self, order, outer):
+ assert order == ["one", "outer"]
+
+
+class TestTwo:
+ @pytest.fixture
+ def inner(self, order):
+ order.append("two")
+
+ def test_order(self, order, outer):
+ assert order == ["two", "outer"]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_request_different_scope.svg b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_request_different_scope.svg
new file mode 100644
index 0000000000..0a78a889fd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_request_different_scope.svg
@@ -0,0 +1,115 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="562" height="532">
+ <style>
+ text {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ dominant-baseline: middle;
+ text-anchor: middle;
+ fill: #062886;
+ font-size: medium;
+ }
+ ellipse.fixture, rect.test {
+ fill: #eeffcc;
+ stroke: #007020;
+ stroke-width: 2;
+ }
+ text.fixture {
+ color: #06287e;
+ }
+ circle.class {
+ fill: #c3e0ec;
+ stroke: #0e84b5;
+ stroke-width: 2;
+ }
+ circle.module {
+ fill: #c3e0ec;
+ stroke: #0e84b5;
+ stroke-width: 2;
+ }
+ text.class, text.module {
+ fill: #0e84b5;
+ }
+ line, path {
+ stroke: black;
+ stroke-width: 2;
+ fill: none;
+ }
+ </style>
+ <!-- main scope -->
+ <circle class="module" r="265" cx="281" cy="266" />
+ <!-- scope name -->
+ <defs>
+ <path d="M 26,266 A 255 255 0 0 1 536 266" id="testModule"/>
+ </defs>
+ <text class="module">
+ <textPath xlink:href="#testModule" startOffset="50%">test_fixtures_request_different_scope.py</textPath>
+ </text>
+
+ <!-- TestOne -->
+ <circle class="class" r="100" cx="141" cy="266" />
+ <!-- inner -->
+ <line x1="141" x2="141" y1="231" y2="301"/>
+ <!-- order -->
+ <path d="M 141 296 L 201 296 L 211 286 L 211 146 L 221 136 L 281 136" />
+ <!-- outer -->
+ <path d="M 141 306 L 201 306 L 211 316 L 211 386 L 221 396 L 281 396" />
+ <ellipse class="fixture" rx="50" ry="25" cx="141" cy="231" />
+ <text x="141" y="231">inner</text>
+ <rect class="test" width="110" height="50" x="86" y="276" />
+ <text x="141" y="301">test_order</text>
+ <!-- scope name -->
+ <defs>
+ <path d="M 51,266 A 90 90 0 0 1 231 266" id="testOne"/>
+ </defs>
+ <text class="class">
+ <textPath xlink:href="#testOne" startOffset="50%">TestOne</textPath>
+ </text>
+ <!-- scope order number -->
+ <mask id="testOneOrderMask">
+ <rect x="0" y="0" width="100%" height="100%" fill="white"/>
+ <circle fill="black" stroke="white" stroke-width="2" r="100" cx="141" cy="266" />
+ </mask>
+ <circle class="module" r="15" cx="41" cy="266" mask="url(#testOneOrderMask)"/>
+ <text class="module" x="41" y="266">1</text>
+ <!-- scope order number -->
+ <mask id="testMainOrderMask">
+ <rect x="0" y="0" width="100%" height="100%" fill="white"/>
+ <circle fill="black" stroke="white" stroke-width="2" r="265" cx="281" cy="266" />
+ </mask>
+ <circle class="module" r="15" cx="16" cy="266" mask="url(#testMainOrderMask)"/>
+ <text class="module" x="16" y="266">2</text>
+
+ <!-- TestTwo -->
+ <circle class="class" r="100" cx="421" cy="266" />
+ <!-- inner -->
+ <line x1="421" x2="421" y1="231" y2="301"/>
+ <!-- order -->
+ <path d="M 421 296 L 361 296 L 351 286 L 351 146 L 341 136 L 281 136" />
+ <!-- outer -->
+ <path d="M 421 306 L 361 306 L 351 316 L 351 386 L 341 396 L 281 396" />
+ <ellipse class="fixture" rx="50" ry="25" cx="421" cy="231" />
+ <text x="421" y="231">inner</text>
+ <rect class="test" width="110" height="50" x="366" y="276" />
+ <text x="421" y="301">test_order</text>
+ <!-- scope name -->
+ <defs>
+ <path d="M 331,266 A 90 90 0 0 1 511 266" id="testTwo"/>
+ </defs>
+ <text class="class">
+ <textPath xlink:href="#testTwo" startOffset="50%">TestTwo</textPath>
+ </text>
+ <!-- scope order number -->
+ <mask id="testTwoOrderMask">
+ <rect x="0" y="0" width="100%" height="100%" fill="white"/>
+ <circle fill="black" stroke="white" stroke-width="2" r="100" cx="421" cy="266" />
+ </mask>
+ <circle class="module" r="15" cx="521" cy="266" mask="url(#testTwoOrderMask)"/>
+ <text class="module" x="521" y="266">1</text>
+ <!-- scope order number -->
+ <circle class="module" r="15" cx="546" cy="266" mask="url(#testMainOrderMask)"/>
+ <text class="module" x="546" y="266">2</text>
+
+ <ellipse class="fixture" rx="50" ry="25" cx="281" cy="396" />
+ <text x="281" y="396">outer</text>
+ <ellipse class="fixture" rx="50" ry="25" cx="281" cy="136" />
+ <text x="281" y="136">order</text>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/index.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/index.rst
new file mode 100644
index 0000000000..71e855534f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/index.rst
@@ -0,0 +1,34 @@
+
+.. _examples:
+
+Examples and customization tricks
+=================================
+
+Here is a (growing) list of examples. :ref:`Contact <contact>` us if you
+need more examples or have questions. Also take a look at the
+:ref:`comprehensive documentation <toc>` which contains many example
+snippets as well. Also, `pytest on stackoverflow.com
+<http://stackoverflow.com/search?q=pytest>`_ often comes with example
+answers.
+
+For basic examples, see
+
+- :ref:`get-started` for basic introductory examples
+- :ref:`assert` for basic assertion examples
+- :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.
+
+.. toctree::
+ :maxdepth: 2
+
+ reportingdemo
+ simple
+ parametrize
+ markers
+ special
+ pythoncollection
+ nonpython
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/markers.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/markers.rst
new file mode 100644
index 0000000000..3226c0871e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/markers.rst
@@ -0,0 +1,734 @@
+
+.. _`mark examples`:
+
+Working with custom markers
+=================================================
+
+Here are some examples using the :ref:`mark` mechanism.
+
+.. _`mark run`:
+
+Marking test functions and selecting them for a run
+----------------------------------------------------
+
+You can "mark" a test function with custom metadata like this:
+
+.. code-block:: python
+
+ # content of test_server.py
+
+ import pytest
+
+
+ @pytest.mark.webtest
+ def test_send_http():
+ pass # perform some webtest test for your app
+
+
+ def test_something_quick():
+ pass
+
+
+ def test_another():
+ pass
+
+
+ class TestClass:
+ def test_method(self):
+ pass
+
+
+
+You can then restrict a test run to only run tests marked with ``webtest``:
+
+.. code-block:: pytest
+
+ $ 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
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project
+ collecting ... collected 4 items / 3 deselected / 1 selected
+
+ test_server.py::test_send_http PASSED [100%]
+
+ ===================== 1 passed, 3 deselected in 0.12s ======================
+
+Or the inverse, running all tests except the webtest ones:
+
+.. code-block:: pytest
+
+ $ 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
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project
+ collecting ... collected 4 items / 1 deselected / 3 selected
+
+ test_server.py::test_something_quick PASSED [ 33%]
+ test_server.py::test_another PASSED [ 66%]
+ test_server.py::TestClass::test_method PASSED [100%]
+
+ ===================== 3 passed, 1 deselected in 0.12s ======================
+
+Selecting tests based on their node ID
+--------------------------------------
+
+You can provide one or more :ref:`node IDs <node-id>` as positional
+arguments to select only specified tests. This makes it easy to select
+tests based on their module, class, method, or function name:
+
+.. code-block:: pytest
+
+ $ 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
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project
+ collecting ... collected 1 item
+
+ test_server.py::TestClass::test_method PASSED [100%]
+
+ ============================ 1 passed in 0.12s =============================
+
+You can also select on the class:
+
+.. code-block:: pytest
+
+ $ 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
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project
+ collecting ... collected 1 item
+
+ test_server.py::TestClass::test_method PASSED [100%]
+
+ ============================ 1 passed in 0.12s =============================
+
+Or select multiple nodes:
+
+.. code-block:: pytest
+
+ $ 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
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project
+ collecting ... collected 2 items
+
+ test_server.py::TestClass::test_method PASSED [ 50%]
+ test_server.py::test_send_http PASSED [100%]
+
+ ============================ 2 passed in 0.12s =============================
+
+.. _node-id:
+
+.. note::
+
+ Node IDs are of the form ``module.py::class::method`` or
+ ``module.py::function``. Node IDs control which tests are
+ collected, so ``module.py::class`` will select all test methods
+ on the class. Nodes are also created for each parameter of a
+ parametrized fixture or test, so selecting a parametrized test
+ must include the parameter value, e.g.
+ ``module.py::function[param]``.
+
+ 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``.
+
+Using ``-k expr`` to select tests based on their name
+-------------------------------------------------------
+
+.. versionadded:: 2.0/2.3.4
+
+You can use the ``-k`` command line option to specify an expression
+which implements a substring match on the test names instead of the
+exact match on markers that ``-m`` provides. This makes it easy to
+select tests based on their names:
+
+.. versionchanged:: 5.4
+
+The expression matching is now case-insensitive.
+
+.. code-block:: pytest
+
+ $ 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
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project
+ collecting ... collected 4 items / 3 deselected / 1 selected
+
+ test_server.py::test_send_http PASSED [100%]
+
+ ===================== 1 passed, 3 deselected in 0.12s ======================
+
+And you can also run all tests except the ones that match the keyword:
+
+.. code-block:: pytest
+
+ $ 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
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project
+ collecting ... collected 4 items / 1 deselected / 3 selected
+
+ test_server.py::test_something_quick PASSED [ 33%]
+ test_server.py::test_another PASSED [ 66%]
+ test_server.py::TestClass::test_method PASSED [100%]
+
+ ===================== 3 passed, 1 deselected in 0.12s ======================
+
+Or to select "http" and "quick" tests:
+
+.. code-block:: pytest
+
+ $ 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
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project
+ collecting ... collected 4 items / 2 deselected / 2 selected
+
+ test_server.py::test_send_http PASSED [ 50%]
+ test_server.py::test_something_quick PASSED [100%]
+
+ ===================== 2 passed, 2 deselected in 0.12s ======================
+
+You can use ``and``, ``or``, ``not`` and parentheses.
+
+
+In addition to the test's name, ``-k`` also matches the names of the test's parents (usually, the name of the file and class it's in),
+attributes set on the test function, markers applied to it or its parents and any :attr:`extra keywords <_pytest.nodes.Node.extra_keyword_matches>`
+explicitly added to it or its parents.
+
+
+Registering markers
+-------------------------------------
+
+
+
+.. ini-syntax for custom markers:
+
+Registering markers for your test suite is simple:
+
+.. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ markers =
+ webtest: mark a test as a webtest.
+ slow: mark test as slow.
+
+Multiple custom markers can be registered, by defining each one in its own line, as shown in above example.
+
+You can ask which markers exist for your test suite - the list includes our just defined ``webtest`` and ``slow`` markers:
+
+.. code-block:: pytest
+
+ $ pytest --markers
+ @pytest.mark.webtest: mark a test as a webtest.
+
+ @pytest.mark.slow: mark test as slow.
+
+ @pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings
+
+ @pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.
+
+ @pytest.mark.skipif(condition, ..., *, reason=...): skip the given test function if any of the conditions evaluate to True. Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-skipif
+
+ @pytest.mark.xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict): mark the test function as an expected failure if any of the conditions evaluate to True. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-xfail
+
+ @pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://docs.pytest.org/en/stable/how-to/parametrize.html for more info and examples.
+
+ @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.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
+
+
+For an example on how to add and work with markers from a plugin, see
+:ref:`adding a custom marker from a plugin`.
+
+.. note::
+
+ It is recommended to explicitly register markers so that:
+
+ * There is one place in your test suite defining your markers
+
+ * Asking for existing markers via ``pytest --markers`` gives good output
+
+ * Typos in function markers are treated as an error if you use
+ the ``--strict-markers`` option.
+
+.. _`scoped-marking`:
+
+Marking whole classes or modules
+----------------------------------------------------
+
+You may use ``pytest.mark`` decorators with classes to apply markers to all of
+its test methods:
+
+.. code-block:: python
+
+ # content of test_mark_classlevel.py
+ import pytest
+
+
+ @pytest.mark.webtest
+ class TestClass:
+ def test_startup(self):
+ pass
+
+ def test_startup_and_more(self):
+ pass
+
+This is equivalent to directly applying the decorator to the
+two test functions.
+
+To apply marks at the module level, use the :globalvar:`pytestmark` global variable::
+
+ import pytest
+ pytestmark = pytest.mark.webtest
+
+or multiple markers::
+
+ pytestmark = [pytest.mark.webtest, pytest.mark.slowtest]
+
+
+Due to legacy reasons, before class decorators were introduced, it is possible to set the
+:globalvar:`pytestmark` attribute on a test class like this:
+
+.. code-block:: python
+
+ import pytest
+
+
+ class TestClass:
+ pytestmark = pytest.mark.webtest
+
+.. _`marking individual tests when using parametrize`:
+
+Marking individual tests when using parametrize
+-----------------------------------------------
+
+When using parametrize, applying a mark will make it apply
+to each individual test. However it is also possible to
+apply a marker to an individual test instance:
+
+.. code-block:: python
+
+ import pytest
+
+
+ @pytest.mark.foo
+ @pytest.mark.parametrize(
+ ("n", "expected"), [(1, 2), pytest.param(1, 3, marks=pytest.mark.bar), (2, 3)]
+ )
+ def test_increment(n, expected):
+ assert n + 1 == expected
+
+In this example the mark "foo" will apply to each of the three
+tests, whereas the "bar" mark is only applied to the second test.
+Skip and xfail marks can also be applied in this way, see :ref:`skip/xfail with parametrize`.
+
+.. _`adding a custom marker from a plugin`:
+
+Custom marker and command line option to control test runs
+----------------------------------------------------------
+
+.. regendoc:wipe
+
+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:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+ import pytest
+
+
+ def pytest_addoption(parser):
+ parser.addoption(
+ "-E",
+ action="store",
+ metavar="NAME",
+ help="only run tests matching the environment NAME.",
+ )
+
+
+ def pytest_configure(config):
+ # register an additional marker
+ config.addinivalue_line(
+ "markers", "env(name): mark test to run only on named environment"
+ )
+
+
+ def pytest_runtest_setup(item):
+ 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))
+
+A test file using this local plugin:
+
+.. code-block:: python
+
+ # content of test_someenv.py
+
+ import pytest
+
+
+ @pytest.mark.env("stage1")
+ def test_basic_db_operation():
+ pass
+
+and an example invocations specifying a different environment than what
+the test needs:
+
+.. code-block:: pytest
+
+ $ pytest -E stage2
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 1 item
+
+ test_someenv.py s [100%]
+
+ ============================ 1 skipped in 0.12s ============================
+
+and here is one that specifies exactly the environment needed:
+
+.. code-block:: pytest
+
+ $ pytest -E stage1
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 1 item
+
+ test_someenv.py . [100%]
+
+ ============================ 1 passed in 0.12s =============================
+
+The ``--markers`` option always gives you a list of available markers:
+
+.. code-block:: pytest
+
+ $ pytest --markers
+ @pytest.mark.env(name): mark test to run only on named environment
+
+ @pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings
+
+ @pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.
+
+ @pytest.mark.skipif(condition, ..., *, reason=...): skip the given test function if any of the conditions evaluate to True. Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-skipif
+
+ @pytest.mark.xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict): mark the test function as an expected failure if any of the conditions evaluate to True. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-xfail
+
+ @pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://docs.pytest.org/en/stable/how-to/parametrize.html for more info and examples.
+
+ @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.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
+
+
+.. _`passing callables to custom markers`:
+
+Passing a callable to custom markers
+--------------------------------------------
+
+.. regendoc:wipe
+
+Below is the config file that will be used in the next examples:
+
+.. code-block:: python
+
+ # content of conftest.py
+ import sys
+
+
+ def pytest_runtest_setup(item):
+ for marker in item.iter_markers(name="my_marker"):
+ print(marker)
+ sys.stdout.flush()
+
+A custom marker can have its argument set, i.e. ``args`` and ``kwargs`` properties, defined by either invoking it as a callable or using ``pytest.mark.MARKER_NAME.with_args``. These two methods achieve the same effect most of the time.
+
+However, if there is a callable as the single positional argument with no keyword arguments, using the ``pytest.mark.MARKER_NAME(c)`` will not pass ``c`` as a positional argument but decorate ``c`` with the custom marker (see :ref:`MarkDecorator <mark>`). Fortunately, ``pytest.mark.MARKER_NAME.with_args`` comes to the rescue:
+
+.. code-block:: python
+
+ # content of test_custom_marker.py
+ import pytest
+
+
+ def hello_world(*args, **kwargs):
+ return "Hello World"
+
+
+ @pytest.mark.my_marker.with_args(hello_world)
+ def test_with_args():
+ pass
+
+The output is as follows:
+
+.. code-block:: pytest
+
+ $ pytest -q -s
+ Mark(name='my_marker', args=(<function hello_world at 0xdeadbeef0001>,), kwargs={})
+ .
+ 1 passed in 0.12s
+
+We can see that the custom marker has its argument set extended with the function ``hello_world``. This is the key difference between creating a custom marker as a callable, which invokes ``__call__`` behind the scenes, and using ``with_args``.
+
+
+Reading markers which were set from multiple places
+----------------------------------------------------
+
+.. versionadded: 2.2.2
+
+.. regendoc:wipe
+
+If you are heavily using markers in your test suite you may encounter the case where a marker is applied several times to a test function. From plugin
+code you can read over all such settings. Example:
+
+.. code-block:: python
+
+ # content of test_mark_three_times.py
+ import pytest
+
+ pytestmark = pytest.mark.glob("module", x=1)
+
+
+ @pytest.mark.glob("class", x=2)
+ class TestClass:
+ @pytest.mark.glob("function", x=3)
+ def test_something(self):
+ pass
+
+Here we have the marker "glob" applied three times to the same
+test function. From a conftest file we can read it like this:
+
+.. code-block:: python
+
+ # content of conftest.py
+ import sys
+
+
+ def pytest_runtest_setup(item):
+ for mark in item.iter_markers(name="glob"):
+ print("glob args={} kwargs={}".format(mark.args, mark.kwargs))
+ sys.stdout.flush()
+
+Let's run this without capturing output and see what we get:
+
+.. code-block:: pytest
+
+ $ pytest -q -s
+ glob args=('function',) kwargs={'x': 3}
+ glob args=('class',) kwargs={'x': 2}
+ glob args=('module',) kwargs={'x': 1}
+ .
+ 1 passed in 0.12s
+
+Marking platform specific tests with pytest
+--------------------------------------------------------------
+
+.. regendoc:wipe
+
+Consider you have a test suite which marks tests for particular platforms,
+namely ``pytest.mark.darwin``, ``pytest.mark.win32`` etc. and you
+also have tests that run on all platforms and have no specific
+marker. If you now want to have a way to only run the tests
+for your particular platform, you could use the following plugin:
+
+.. code-block:: python
+
+ # content of conftest.py
+ #
+ import sys
+ import pytest
+
+ ALL = set("darwin linux win32".split())
+
+
+ def pytest_runtest_setup(item):
+ 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))
+
+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:
+
+.. code-block:: python
+
+ # content of test_plat.py
+
+ import pytest
+
+
+ @pytest.mark.darwin
+ def test_if_apple_is_evil():
+ pass
+
+
+ @pytest.mark.linux
+ def test_if_linux_works():
+ pass
+
+
+ @pytest.mark.win32
+ def test_if_win32_crashes():
+ pass
+
+
+ def test_runs_everywhere():
+ pass
+
+then you will see two tests skipped and two executed tests as expected:
+
+.. code-block:: pytest
+
+ $ pytest -rs # this option reports skip reasons
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.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
+ ======================= 2 passed, 2 skipped in 0.12s =======================
+
+Note that if you specify a platform via the marker-command line option like this:
+
+.. code-block:: pytest
+
+ $ pytest -m linux
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 4 items / 3 deselected / 1 selected
+
+ test_plat.py . [100%]
+
+ ===================== 1 passed, 3 deselected in 0.12s ======================
+
+then the unmarked-tests will not be run. It is thus a way to restrict the run to the specific tests.
+
+Automatically adding markers based on test names
+--------------------------------------------------------
+
+.. regendoc:wipe
+
+If you have a test suite where test function names indicate a certain
+type of test, you can implement a hook that automatically defines
+markers so that you can use the ``-m`` option with it. Let's look
+at this test module:
+
+.. code-block:: python
+
+ # content of test_module.py
+
+
+ def test_interface_simple():
+ assert 0
+
+
+ def test_interface_complex():
+ assert 0
+
+
+ def test_event_simple():
+ assert 0
+
+
+ def test_something_else():
+ assert 0
+
+We want to dynamically define two markers and can do it in a
+``conftest.py`` plugin:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+ import pytest
+
+
+ def pytest_collection_modifyitems(items):
+ for item in items:
+ if "interface" in item.nodeid:
+ item.add_marker(pytest.mark.interface)
+ elif "event" in item.nodeid:
+ item.add_marker(pytest.mark.event)
+
+We can now use the ``-m option`` to select one set:
+
+.. code-block:: pytest
+
+ $ pytest -m interface --tb=short
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 4 items / 2 deselected / 2 selected
+
+ test_module.py FF [100%]
+
+ ================================= FAILURES =================================
+ __________________________ test_interface_simple ___________________________
+ test_module.py:4: in test_interface_simple
+ assert 0
+ E assert 0
+ __________________________ test_interface_complex __________________________
+ test_module.py:8: in test_interface_complex
+ assert 0
+ E assert 0
+ ========================= short test summary info ==========================
+ FAILED test_module.py::test_interface_simple - assert 0
+ FAILED test_module.py::test_interface_complex - assert 0
+ ===================== 2 failed, 2 deselected in 0.12s ======================
+
+or to select both "event" and "interface" tests:
+
+.. code-block:: pytest
+
+ $ pytest -m "interface or event" --tb=short
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 4 items / 1 deselected / 3 selected
+
+ test_module.py FFF [100%]
+
+ ================================= FAILURES =================================
+ __________________________ test_interface_simple ___________________________
+ test_module.py:4: in test_interface_simple
+ assert 0
+ E assert 0
+ __________________________ test_interface_complex __________________________
+ test_module.py:8: in test_interface_complex
+ assert 0
+ E assert 0
+ ____________________________ test_event_simple _____________________________
+ test_module.py:12: in test_event_simple
+ assert 0
+ E assert 0
+ ========================= short test summary info ==========================
+ FAILED test_module.py::test_interface_simple - assert 0
+ FAILED test_module.py::test_interface_complex - assert 0
+ FAILED test_module.py::test_event_simple - assert 0
+ ===================== 3 failed, 1 deselected in 0.12s ======================
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/multipython.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/multipython.py
new file mode 100644
index 0000000000..9005d31add
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/multipython.py
@@ -0,0 +1,72 @@
+"""
+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"]
+
+
+@pytest.fixture(params=pythonlist)
+def python1(request, tmp_path):
+ picklefile = tmp_path / "data.pickle"
+ return Python(request.param, picklefile)
+
+
+@pytest.fixture(params=pythonlist)
+def python2(request, python1):
+ return Python(request.param, python1.picklefile)
+
+
+class Python:
+ def __init__(self, version, picklefile):
+ self.pythonpath = shutil.which(version)
+ if not self.pythonpath:
+ pytest.skip(f"{version!r} not found")
+ self.picklefile = picklefile
+
+ def dumps(self, obj):
+ dumpfile = self.picklefile.with_name("dump.py")
+ dumpfile.write_text(
+ textwrap.dedent(
+ r"""
+ import pickle
+ f = open({!r}, 'wb')
+ s = pickle.dump({!r}, f, protocol=2)
+ f.close()
+ """.format(
+ str(self.picklefile), obj
+ )
+ )
+ )
+ subprocess.check_call((self.pythonpath, str(dumpfile)))
+
+ def load_and_is_true(self, expression):
+ loadfile = self.picklefile.with_name("load.py")
+ loadfile.write_text(
+ textwrap.dedent(
+ r"""
+ import pickle
+ f = open({!r}, 'rb')
+ obj = pickle.load(f)
+ f.close()
+ res = eval({!r})
+ if not res:
+ raise SystemExit(1)
+ """.format(
+ str(self.picklefile), expression
+ )
+ )
+ )
+ print(loadfile)
+ subprocess.check_call((self.pythonpath, str(loadfile)))
+
+
+@pytest.mark.parametrize("obj", [42, {}, {1: 3}])
+def test_basic_objects(python1, python2, obj):
+ python1.dumps(obj)
+ python2.load_and_is_true(f"obj == {obj}")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython.rst
new file mode 100644
index 0000000000..f79f15b4f7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython.rst
@@ -0,0 +1,102 @@
+
+.. _`non-python tests`:
+
+Working with non-python tests
+====================================================
+
+.. _`yaml plugin`:
+
+A basic example for specifying tests in Yaml files
+--------------------------------------------------------------
+
+.. _`pytest-yamlwsgi`: http://bitbucket.org/aafshar/pytest-yamlwsgi/src/tip/pytest_yamlwsgi.py
+
+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:
+
+.. include:: nonpython/conftest.py
+ :literal:
+
+You can create a simple example file:
+
+.. include:: nonpython/test_simple.yaml
+ :literal:
+
+and if you installed :pypi:`PyYAML` or a compatible YAML-parser you can
+now execute the test specification:
+
+.. code-block:: pytest
+
+ nonpython $ pytest test_simple.yaml
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project/nonpython
+ collected 2 items
+
+ test_simple.yaml F. [100%]
+
+ ================================= FAILURES =================================
+ ______________________________ usecase: hello ______________________________
+ usecase execution failed
+ spec failed: 'some': 'other'
+ no further details known at this point.
+ ========================= short test summary info ==========================
+ FAILED test_simple.yaml::hello
+ ======================= 1 failed, 1 passed in 0.12s ========================
+
+.. regendoc:wipe
+
+You get one dot for the passing ``sub1: sub1`` check and one failure.
+Obviously in the above ``conftest.py`` you'll want to implement a more
+interesting interpretation of the yaml-values. You can easily write
+your own domain specific testing language this way.
+
+.. note::
+
+ ``repr_failure(excinfo)`` is called for representing test failures.
+ If you create custom collection nodes you can return an error
+ representation string of your choice. It
+ will be reported as a (red) string.
+
+``reportinfo()`` is used for representing the test location and is also
+consulted when reporting in ``verbose`` mode:
+
+.. code-block:: pytest
+
+ nonpython $ pytest -v
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project/nonpython
+ collecting ... collected 2 items
+
+ test_simple.yaml::hello FAILED [ 50%]
+ test_simple.yaml::ok PASSED [100%]
+
+ ================================= FAILURES =================================
+ ______________________________ usecase: hello ______________________________
+ usecase execution failed
+ spec failed: 'some': 'other'
+ no further details known at this point.
+ ========================= short test summary info ==========================
+ FAILED test_simple.yaml::hello
+ ======================= 1 failed, 1 passed in 0.12s ========================
+
+.. regendoc:wipe
+
+While developing your custom test collection and execution it's also
+interesting to just look at the collection tree:
+
+.. code-block:: pytest
+
+ nonpython $ pytest --collect-only
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project/nonpython
+ collected 2 items
+
+ <Package nonpython>
+ <YamlFile test_simple.yaml>
+ <YamlItem hello>
+ <YamlItem ok>
+
+ ======================== 2 tests collected in 0.12s ========================
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython/conftest.py
new file mode 100644
index 0000000000..bc39a1f6b2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython/conftest.py
@@ -0,0 +1,47 @@
+# content of conftest.py
+import pytest
+
+
+def pytest_collect_file(parent, file_path):
+ if file_path.suffix == ".yaml" and file_path.name.startswith("test"):
+ return YamlFile.from_parent(parent, path=file_path)
+
+
+class YamlFile(pytest.File):
+ def collect(self):
+ # We need a yaml parser, e.g. PyYAML.
+ import yaml
+
+ raw = yaml.safe_load(self.path.open())
+ for name, spec in sorted(raw.items()):
+ yield YamlItem.from_parent(self, name=name, spec=spec)
+
+
+class YamlItem(pytest.Item):
+ def __init__(self, *, spec, **kwargs):
+ super().__init__(**kwargs)
+ self.spec = spec
+
+ def runtest(self):
+ for name, value in sorted(self.spec.items()):
+ # Some custom test execution (dumb example follows).
+ if name != value:
+ raise YamlException(self, name, value)
+
+ def repr_failure(self, excinfo):
+ """Called when self.runtest() raises an exception."""
+ if isinstance(excinfo.value, YamlException):
+ return "\n".join(
+ [
+ "usecase execution failed",
+ " spec failed: {1!r}: {2!r}".format(*excinfo.value.args),
+ " no further details known at this point.",
+ ]
+ )
+
+ def reportinfo(self):
+ return self.path, 0, f"usecase: {self.name}"
+
+
+class YamlException(Exception):
+ """Custom exception for error reporting."""
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython/test_simple.yaml b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython/test_simple.yaml
new file mode 100644
index 0000000000..8e3e7a4bbc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/nonpython/test_simple.yaml
@@ -0,0 +1,7 @@
+# test_simple.yaml
+ok:
+ sub1: sub1
+
+hello:
+ world: world
+ some: other
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/parametrize.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/parametrize.rst
new file mode 100644
index 0000000000..66d72f3cc0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/parametrize.rst
@@ -0,0 +1,708 @@
+
+.. _paramexamples:
+
+Parametrizing tests
+=================================================
+
+.. currentmodule:: _pytest.python
+
+``pytest`` allows to easily parametrize test functions.
+For basic docs, see :ref:`parametrize-basics`.
+
+In the following we provide some examples using
+the builtin mechanisms.
+
+Generating parameters combinations, depending on command line
+----------------------------------------------------------------------------
+
+.. regendoc:wipe
+
+Let's say we want to execute a test with different computation
+parameters and the parameter range shall be determined by a command
+line argument. Let's first write a simple (do-nothing) computation test:
+
+.. code-block:: python
+
+ # content of test_compute.py
+
+
+ def test_compute(param1):
+ assert param1 < 4
+
+Now we add a test configuration like this:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+
+ def pytest_addoption(parser):
+ parser.addoption("--all", action="store_true", help="run all combinations")
+
+
+ def pytest_generate_tests(metafunc):
+ if "param1" in metafunc.fixturenames:
+ if metafunc.config.getoption("all"):
+ end = 5
+ else:
+ end = 2
+ metafunc.parametrize("param1", range(end))
+
+This means that we only run 2 tests if we do not pass ``--all``:
+
+.. code-block:: pytest
+
+ $ pytest -q test_compute.py
+ .. [100%]
+ 2 passed in 0.12s
+
+We run only two computations, so we see two dots.
+let's run the full monty:
+
+.. code-block:: pytest
+
+ $ pytest -q --all
+ ....F [100%]
+ ================================= FAILURES =================================
+ _____________________________ test_compute[4] ______________________________
+
+ param1 = 4
+
+ def test_compute(param1):
+ > assert param1 < 4
+ E assert 4 < 4
+
+ test_compute.py:4: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_compute.py::test_compute[4] - assert 4 < 4
+ 1 failed, 4 passed in 0.12s
+
+As expected when running the full range of ``param1`` values
+we'll get an error on the last one.
+
+
+Different options for test IDs
+------------------------------------
+
+pytest will build a string that is the test ID for each set of values in a
+parametrized test. These IDs can be used with ``-k`` to select specific cases
+to run, and they will also identify the specific case when one is failing.
+Running pytest with ``--collect-only`` will show the generated IDs.
+
+Numbers, strings, booleans and None will have their usual string representation
+used in the test ID. For other objects, pytest will make a string based on
+the argument name:
+
+.. code-block:: python
+
+ # content of test_time.py
+
+ from datetime import datetime, timedelta
+
+ import pytest
+
+ testdata = [
+ (datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)),
+ (datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)),
+ ]
+
+
+ @pytest.mark.parametrize("a,b,expected", testdata)
+ def test_timedistance_v0(a, b, expected):
+ diff = a - b
+ assert diff == expected
+
+
+ @pytest.mark.parametrize("a,b,expected", testdata, ids=["forward", "backward"])
+ def test_timedistance_v1(a, b, expected):
+ diff = a - b
+ assert diff == expected
+
+
+ def idfn(val):
+ if isinstance(val, (datetime,)):
+ # note this wouldn't show any hours/minutes/seconds
+ return val.strftime("%Y%m%d")
+
+
+ @pytest.mark.parametrize("a,b,expected", testdata, ids=idfn)
+ def test_timedistance_v2(a, b, expected):
+ diff = a - b
+ assert diff == expected
+
+
+ @pytest.mark.parametrize(
+ "a,b,expected",
+ [
+ pytest.param(
+ datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1), id="forward"
+ ),
+ pytest.param(
+ datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1), id="backward"
+ ),
+ ],
+ )
+ def test_timedistance_v3(a, b, expected):
+ diff = a - b
+ assert diff == expected
+
+In ``test_timedistance_v0``, we let pytest generate the test IDs.
+
+In ``test_timedistance_v1``, we specified ``ids`` as a list of strings which were
+used as the test IDs. These are succinct, but can be a pain to maintain.
+
+In ``test_timedistance_v2``, we specified ``ids`` as a function that can generate a
+string representation to make part of the test ID. So our ``datetime`` values use the
+label generated by ``idfn``, but because we didn't generate a label for ``timedelta``
+objects, they are still using the default pytest representation:
+
+.. code-block:: pytest
+
+ $ pytest test_time.py --collect-only
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.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]>
+
+ ======================== 8 tests collected in 0.12s ========================
+
+In ``test_timedistance_v3``, we used ``pytest.param`` to specify the test IDs
+together with the actual data, instead of listing them separately.
+
+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`:
+
+.. code-block:: python
+
+ # content of test_scenarios.py
+
+
+ def pytest_generate_tests(metafunc):
+ idlist = []
+ argvalues = []
+ for scenario in metafunc.cls.scenarios:
+ idlist.append(scenario[0])
+ items = scenario[1].items()
+ argnames = [x[0] for x in items]
+ argvalues.append([x[1] for x in items])
+ metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class")
+
+
+ scenario1 = ("basic", {"attribute": "value"})
+ scenario2 = ("advanced", {"attribute": "value2"})
+
+
+ class TestSampleWithScenarios:
+ scenarios = [scenario1, scenario2]
+
+ def test_demo1(self, attribute):
+ assert isinstance(attribute, str)
+
+ def test_demo2(self, attribute):
+ assert isinstance(attribute, str)
+
+this is a fully self-contained example which you can run with:
+
+.. code-block:: pytest
+
+ $ pytest test_scenarios.py
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 4 items
+
+ test_scenarios.py .... [100%]
+
+ ============================ 4 passed in 0.12s =============================
+
+If you just collect tests you'll also nicely see 'advanced' and 'basic' as variants for the test function:
+
+.. code-block:: pytest
+
+ $ pytest --collect-only test_scenarios.py
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.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]>
+
+ ======================== 4 tests collected in 0.12s ========================
+
+Note that we told ``metafunc.parametrize()`` that your scenario values
+should be considered class-scoped. With pytest-2.3 this leads to a
+resource-based ordering.
+
+Deferring the setup of parametrized resources
+---------------------------------------------------
+
+.. regendoc:wipe
+
+The parametrization of test functions happens at collection
+time. It is a good idea to setup expensive resources like DB
+connections or subprocess only when the actual test is run.
+Here is a simple example how you can achieve that. This test
+requires a ``db`` object fixture:
+
+.. code-block:: python
+
+ # content of test_backends.py
+
+ import pytest
+
+
+ def test_db_initialized(db):
+ # a dummy test
+ if db.__class__.__name__ == "DB2":
+ pytest.fail("deliberately failing for demo purposes")
+
+We can now add a test configuration that generates two invocations of
+the ``test_db_initialized`` function and also implements a factory that
+creates a database object for the actual test invocations:
+
+.. code-block:: python
+
+ # content of conftest.py
+ import pytest
+
+
+ def pytest_generate_tests(metafunc):
+ if "db" in metafunc.fixturenames:
+ metafunc.parametrize("db", ["d1", "d2"], indirect=True)
+
+
+ class DB1:
+ "one database object"
+
+
+ class DB2:
+ "alternative database object"
+
+
+ @pytest.fixture
+ def db(request):
+ if request.param == "d1":
+ return DB1()
+ elif request.param == "d2":
+ return DB2()
+ else:
+ raise ValueError("invalid internal test config")
+
+Let's first see how it looks like at collection time:
+
+.. code-block:: pytest
+
+ $ pytest test_backends.py --collect-only
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.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]>
+
+ ======================== 2 tests collected in 0.12s ========================
+
+And then when we run the test:
+
+.. code-block:: pytest
+
+ $ pytest -q test_backends.py
+ .F [100%]
+ ================================= FAILURES =================================
+ _________________________ test_db_initialized[d2] __________________________
+
+ db = <conftest.DB2 object at 0xdeadbeef0001>
+
+ def test_db_initialized(db):
+ # a dummy test
+ if db.__class__.__name__ == "DB2":
+ > pytest.fail("deliberately failing for demo purposes")
+ E Failed: deliberately failing for demo purposes
+
+ test_backends.py:8: Failed
+ ========================= short test summary info ==========================
+ FAILED test_backends.py::test_db_initialized[d2] - Failed: deliberately f...
+ 1 failed, 1 passed in 0.12s
+
+The first invocation with ``db == "DB1"`` passed while the second with ``db == "DB2"`` failed. Our ``db`` fixture function has instantiated each of the DB values during the setup phase while the ``pytest_generate_tests`` generated two according calls to the ``test_db_initialized`` during the collection phase.
+
+Indirect parametrization
+---------------------------------------------------
+
+Using the ``indirect=True`` parameter when parametrizing a test allows to
+parametrize a test with a fixture receiving the values before passing them to a
+test:
+
+.. code-block:: python
+
+ import pytest
+
+
+ @pytest.fixture
+ def fixt(request):
+ return request.param * 3
+
+
+ @pytest.mark.parametrize("fixt", ["a", "b"], indirect=True)
+ def test_indirect(fixt):
+ assert len(fixt) == 3
+
+This can be used, for example, to do more expensive setup at test run time in
+the fixture, rather than having to run those setup steps at collection time.
+
+.. regendoc:wipe
+
+Apply indirect on particular arguments
+---------------------------------------------------
+
+Very often parametrization uses more than one argument name. There is opportunity to apply ``indirect``
+parameter on particular arguments. It can be done by passing list or tuple of
+arguments' names to ``indirect``. In the example below there is a function ``test_indirect`` which uses
+two fixtures: ``x`` and ``y``. Here we give to indirect the list, which contains the name of the
+fixture ``x``. The indirect parameter will be applied to this argument only, and the value ``a``
+will be passed to respective fixture function:
+
+.. code-block:: python
+
+ # content of test_indirect_list.py
+
+ import pytest
+
+
+ @pytest.fixture(scope="function")
+ def x(request):
+ return request.param * 3
+
+
+ @pytest.fixture(scope="function")
+ def y(request):
+ return request.param * 2
+
+
+ @pytest.mark.parametrize("x, y", [("a", "b")], indirect=["x"])
+ def test_indirect(x, y):
+ assert x == "aaa"
+ assert y == "b"
+
+The result of this test will be successful:
+
+.. code-block:: pytest
+
+ $ 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
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project
+ collecting ... collected 1 item
+
+ test_indirect_list.py::test_indirect[a-b] PASSED [100%]
+
+ ============================ 1 passed in 0.12s =============================
+
+.. regendoc:wipe
+
+Parametrizing test methods through per-class configuration
+--------------------------------------------------------------
+
+.. _`unittest parametrizer`: https://github.com/testing-cabal/unittest-ext/blob/master/params.py
+
+
+Here is an example ``pytest_generate_tests`` function implementing a
+parametrization scheme similar to Michael Foord's `unittest
+parametrizer`_ but in a lot less code:
+
+.. code-block:: python
+
+ # content of ./test_parametrize.py
+ import pytest
+
+
+ def pytest_generate_tests(metafunc):
+ # called once per each test function
+ funcarglist = metafunc.cls.params[metafunc.function.__name__]
+ argnames = sorted(funcarglist[0])
+ metafunc.parametrize(
+ argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist]
+ )
+
+
+ class TestClass:
+ # a map specifying multiple argument sets for a test method
+ params = {
+ "test_equals": [dict(a=1, b=2), dict(a=3, b=3)],
+ "test_zerodivision": [dict(a=1, b=0)],
+ }
+
+ def test_equals(self, a, b):
+ assert a == b
+
+ def test_zerodivision(self, a, b):
+ with pytest.raises(ZeroDivisionError):
+ a / b
+
+Our test generator looks up a class-level definition which specifies which
+argument sets to use for each test function. Let's run it:
+
+.. code-block:: pytest
+
+ $ pytest -q
+ F.. [100%]
+ ================================= FAILURES =================================
+ ________________________ TestClass.test_equals[1-2] ________________________
+
+ self = <test_parametrize.TestClass object at 0xdeadbeef0002>, a = 1, b = 2
+
+ def test_equals(self, a, b):
+ > assert a == b
+ E assert 1 == 2
+
+ test_parametrize.py:21: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_parametrize.py::TestClass::test_equals[1-2] - assert 1 == 2
+ 1 failed, 2 passed in 0.12s
+
+Indirect parametrization with multiple fixtures
+--------------------------------------------------------------
+
+Here is a stripped down real-life example of using parametrized
+testing for testing serialization of objects between different python
+interpreters. We define a ``test_basic_objects`` function which
+is to be run with different sets of arguments for its three arguments:
+
+* ``python1``: first python interpreter, run to pickle-dump an object to a file
+* ``python2``: second interpreter, run to pickle-load an object from a file
+* ``obj``: object to be dumped/loaded
+
+.. literalinclude:: multipython.py
+
+Running it results in some skips if we don't have all the python interpreters installed and otherwise runs all combinations (3 interpreters times 3 interpreters times 3 objects to serialize/deserialize):
+
+.. code-block:: pytest
+
+ . $ pytest -rs -q multipython.py
+ sssssssssssssssssssssssssss [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
+
+Indirect 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
+and get skipped in case the implementation is not importable/available. Let's
+say we have a "base" implementation and the other (possibly optimized ones)
+need to provide similar results:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+ import pytest
+
+
+ @pytest.fixture(scope="session")
+ def basemod(request):
+ return pytest.importorskip("base")
+
+
+ @pytest.fixture(scope="session", params=["opt1", "opt2"])
+ def optmod(request):
+ return pytest.importorskip(request.param)
+
+And then a base implementation of a simple function:
+
+.. code-block:: python
+
+ # content of base.py
+ def func1():
+ return 1
+
+And an optimized version:
+
+.. code-block:: python
+
+ # content of opt1.py
+ def func1():
+ return 1.0001
+
+And finally a little test module:
+
+.. code-block:: python
+
+ # content of test_module.py
+
+
+ def test_func1(basemod, optmod):
+ assert round(basemod.func1(), 3) == round(optmod.func1(), 3)
+
+
+If you run this with reporting for skips enabled:
+
+.. code-block:: pytest
+
+ $ pytest -rs test_module.py
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.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'
+ ======================= 1 passed, 1 skipped in 0.12s =======================
+
+You'll see that we don't have an ``opt2`` module and thus the second test run
+of our ``test_func1`` was skipped. A few notes:
+
+- the fixture functions in the ``conftest.py`` file are "session-scoped" because we
+ don't need to import more than once
+
+- if you have multiple test functions and a skipped import, you will see
+ the ``[1]`` count increasing in the report
+
+- you can put :ref:`@pytest.mark.parametrize <@pytest.mark.parametrize>` style
+ parametrization on the test functions to parametrize input/output
+ values as well.
+
+
+Set marks or test ID for individual parametrized test
+--------------------------------------------------------------------
+
+Use ``pytest.param`` to apply marks or set test ID to individual parametrized test.
+For example:
+
+.. code-block:: python
+
+ # content of test_pytest_param_example.py
+ import pytest
+
+
+ @pytest.mark.parametrize(
+ "test_input,expected",
+ [
+ ("3+5", 8),
+ pytest.param("1+7", 8, marks=pytest.mark.basic),
+ pytest.param("2+4", 6, marks=pytest.mark.basic, id="basic_2+4"),
+ pytest.param(
+ "6*9", 42, marks=[pytest.mark.basic, pytest.mark.xfail], id="basic_6*9"
+ ),
+ ],
+ )
+ def test_eval(test_input, expected):
+ assert eval(test_input) == expected
+
+In this example, we have 4 parametrized tests. Except for the first test,
+we mark the rest three parametrized tests with the custom marker ``basic``,
+and for the fourth test we also use the built-in mark ``xfail`` to indicate this
+test is expected to fail. For explicitness, we set test ids for some tests.
+
+Then run ``pytest`` with verbose mode and with only the ``basic`` marker:
+
+.. code-block:: pytest
+
+ $ 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
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project
+ collecting ... collected 24 items / 21 deselected / 3 selected
+
+ test_pytest_param_example.py::test_eval[1+7-8] PASSED [ 33%]
+ test_pytest_param_example.py::test_eval[basic_2+4] PASSED [ 66%]
+ test_pytest_param_example.py::test_eval[basic_6*9] XFAIL [100%]
+
+ =============== 2 passed, 21 deselected, 1 xfailed in 0.12s ================
+
+As the result:
+
+- Four tests were collected
+- One test was deselected because it doesn't have the ``basic`` mark.
+- Three tests with the ``basic`` mark was selected.
+- The test ``test_eval[1+7-8]`` passed, but the name is autogenerated and confusing.
+- The test ``test_eval[basic_2+4]`` passed.
+- The test ``test_eval[basic_6*9]`` was expected to fail and did fail.
+
+.. _`parametrizing_conditional_raising`:
+
+Parametrizing conditional raising
+--------------------------------------------------------------------
+
+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:
+
+.. code-block:: python
+
+ from contextlib import contextmanager
+ import pytest
+
+
+ @contextmanager
+ def does_not_raise():
+ yield
+
+
+ @pytest.mark.parametrize(
+ "example_input,expectation",
+ [
+ (3, does_not_raise()),
+ (2, does_not_raise()),
+ (1, does_not_raise()),
+ (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
+
+ from contextlib2 import nullcontext as does_not_raise
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/pythoncollection.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/pythoncollection.py
new file mode 100644
index 0000000000..8742526a19
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/pythoncollection.py
@@ -0,0 +1,14 @@
+# run this with $ pytest --collect-only test_collectonly.py
+#
+
+
+def test_function():
+ pass
+
+
+class TestClass:
+ def test_method(self):
+ pass
+
+ def test_anothermethod(self):
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/pythoncollection.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/pythoncollection.rst
new file mode 100644
index 0000000000..b9c2386ac5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/pythoncollection.rst
@@ -0,0 +1,321 @@
+Changing standard (Python) test discovery
+===============================================
+
+Ignore paths during test collection
+-----------------------------------
+
+You can easily ignore certain test directories and modules during collection
+by passing the ``--ignore=path`` option on the cli. ``pytest`` allows multiple
+``--ignore`` options. Example:
+
+.. code-block:: text
+
+ tests/
+ |-- example
+ | |-- test_example_01.py
+ | |-- test_example_02.py
+ | '-- test_example_03.py
+ |-- foobar
+ | |-- test_foobar_01.py
+ | |-- test_foobar_02.py
+ | '-- test_foobar_03.py
+ '-- hello
+ '-- world
+ |-- test_world_01.py
+ |-- test_world_02.py
+ '-- test_world_03.py
+
+Now if you invoke ``pytest`` with ``--ignore=tests/foobar/test_foobar_03.py --ignore=tests/hello/``,
+you will see that ``pytest`` only collects test-modules, which do not match the patterns specified:
+
+.. code-block:: pytest
+
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y
+ rootdir: $REGENDOC_TMPDIR, inifile:
+ collected 5 items
+
+ tests/example/test_example_01.py . [ 20%]
+ tests/example/test_example_02.py . [ 40%]
+ tests/example/test_example_03.py . [ 60%]
+ tests/foobar/test_foobar_01.py . [ 80%]
+ tests/foobar/test_foobar_02.py . [100%]
+
+ ========================= 5 passed in 0.02 seconds =========================
+
+The ``--ignore-glob`` option allows to ignore test file paths based on Unix shell-style wildcards.
+If you want to exclude test-modules that end with ``_01.py``, execute ``pytest`` with ``--ignore-glob='*_01.py'``.
+
+Deselect tests during test collection
+-------------------------------------
+
+Tests can individually be deselected during collection by passing the ``--deselect=item`` option.
+For example, say ``tests/foobar/test_foobar_01.py`` contains ``test_a`` and ``test_b``.
+You can run all of the tests within ``tests/`` *except* for ``tests/foobar/test_foobar_01.py::test_a``
+by invoking ``pytest`` with ``--deselect tests/foobar/test_foobar_01.py::test_a``.
+``pytest`` allows multiple ``--deselect`` options.
+
+Keeping duplicate paths specified from command line
+----------------------------------------------------
+
+Default behavior of ``pytest`` is to ignore duplicate paths specified from the command line.
+Example:
+
+.. code-block:: pytest
+
+ pytest path_a path_a
+
+ ...
+ collected 1 item
+ ...
+
+Just collect tests once.
+
+To collect duplicate tests, use the ``--keep-duplicates`` option on the cli.
+Example:
+
+.. code-block:: pytest
+
+ pytest --keep-duplicates path_a path_a
+
+ ...
+ collected 2 items
+ ...
+
+As the collector just works on directories, if you specify twice a single test file, ``pytest`` will
+still collect it twice, no matter if the ``--keep-duplicates`` is not specified.
+Example:
+
+.. code-block:: pytest
+
+ pytest test_a.py test_a.py
+
+ ...
+ collected 2 items
+ ...
+
+
+Changing directory recursion
+-----------------------------------------------------
+
+You can set the :confval:`norecursedirs` option in an ini-file, for example your ``pytest.ini`` in the project root directory:
+
+.. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ norecursedirs = .svn _build tmp*
+
+This would tell ``pytest`` to not recurse into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory.
+
+.. _`change naming conventions`:
+
+Changing naming conventions
+-----------------------------------------------------
+
+You can configure different naming conventions by setting
+the :confval:`python_files`, :confval:`python_classes` and
+:confval:`python_functions` in your :ref:`configuration file <config file formats>`.
+Here is an example:
+
+.. code-block:: ini
+
+ # content of pytest.ini
+ # Example 1: have pytest look for "check" instead of "test"
+ [pytest]
+ python_files = check_*.py
+ python_classes = Check
+ python_functions = *_check
+
+This would make ``pytest`` look for tests in files that match the ``check_*
+.py`` glob-pattern, ``Check`` prefixes in classes, and functions and methods
+that match ``*_check``. For example, if we have:
+
+.. code-block:: python
+
+ # content of check_myapp.py
+ class CheckMyApp:
+ def simple_check(self):
+ pass
+
+ def complex_check(self):
+ pass
+
+The test collection would look like this:
+
+.. code-block:: pytest
+
+ $ 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
+ collected 2 items
+
+ <Module check_myapp.py>
+ <Class CheckMyApp>
+ <Function simple_check>
+ <Function complex_check>
+
+ ======================== 2 tests collected in 0.12s ========================
+
+You can check for multiple glob patterns by adding a space between the patterns:
+
+.. code-block:: ini
+
+ # Example 2: have pytest look for files with "test" and "example"
+ # content of pytest.ini
+ [pytest]
+ python_files = test_*.py example_*.py
+
+.. note::
+
+ the ``python_functions`` and ``python_classes`` options has no effect
+ for ``unittest.TestCase`` test discovery because pytest delegates
+ discovery of test case methods to unittest code.
+
+Interpreting cmdline arguments as Python packages
+-----------------------------------------------------
+
+You can use the ``--pyargs`` option to make ``pytest`` try
+interpreting arguments as python package names, deriving
+their file system path and then running the test. For
+example if you have unittest2 installed you can type:
+
+.. code-block:: bash
+
+ pytest --pyargs unittest2.test.test_skipping -q
+
+which would run the respective test module. Like with
+other options, through an ini-file and the :confval:`addopts` option you
+can make this change more permanently:
+
+.. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ addopts = --pyargs
+
+Now a simple invocation of ``pytest NAME`` will check
+if NAME exists as an importable package/module and otherwise
+treat it as a filesystem path.
+
+Finding out what is collected
+-----------------------------------------------
+
+You can always peek at the collection tree without running tests like this:
+
+.. code-block:: pytest
+
+ . $ 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
+ collected 3 items
+
+ <Module CWD/pythoncollection.py>
+ <Function test_function>
+ <Class TestClass>
+ <Function test_method>
+ <Function test_anothermethod>
+
+ ======================== 3 tests collected in 0.12s ========================
+
+.. _customizing-test-collection:
+
+Customizing test collection
+---------------------------
+
+.. regendoc:wipe
+
+You can easily instruct ``pytest`` to discover tests from every Python file:
+
+.. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ python_files = *.py
+
+However, many projects will have a ``setup.py`` which they don't want to be
+imported. Moreover, there may files only importable by a specific python
+version. For such cases you can dynamically define files to be ignored by
+listing them in a ``conftest.py`` file:
+
+.. code-block:: python
+
+ # content of conftest.py
+ import sys
+
+ collect_ignore = ["setup.py"]
+ if sys.version_info[0] > 2:
+ collect_ignore.append("pkg/module_py2.py")
+
+and then if you have a module file like this:
+
+.. code-block:: python
+
+ # content of pkg/module_py2.py
+ def test_only_on_python2():
+ try:
+ assert 0
+ except Exception, e:
+ pass
+
+and a ``setup.py`` dummy file like this:
+
+.. code-block:: python
+
+ # content of setup.py
+ 0 / 0 # will raise exception if imported
+
+If you run with a Python 2 interpreter then you will find the one test and will
+leave out the ``setup.py`` file:
+
+.. code-block:: pytest
+
+ #$ pytest --collect-only
+ ====== test session starts ======
+ platform linux2 -- Python 2.7.10, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini
+ collected 1 items
+ <Module 'pkg/module_py2.py'>
+ <Function 'test_only_on_python2'>
+
+ ====== 1 tests found in 0.04 seconds ======
+
+If you run with a Python 3 interpreter both the one test and the ``setup.py``
+file will be left out:
+
+.. code-block:: pytest
+
+ $ 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
+ collected 0 items
+
+ ======================= no tests collected in 0.12s ========================
+
+It's also possible to ignore files based on Unix shell-style wildcards by adding
+patterns to :globalvar:`collect_ignore_glob`.
+
+The following example ``conftest.py`` ignores the file ``setup.py`` and in
+addition all files that end with ``*_py2.py`` when executed with a Python 3
+interpreter:
+
+.. code-block:: python
+
+ # content of conftest.py
+ import sys
+
+ collect_ignore = ["setup.py"]
+ if sys.version_info[0] > 2:
+ collect_ignore_glob = ["*_py2.py"]
+
+Since Pytest 2.6, users can prevent pytest from discovering classes that start
+with ``Test`` by setting a boolean ``__test__`` attribute to ``False``.
+
+.. code-block:: python
+
+ # Will not be discovered as a test
+ class TestClass:
+ __test__ = False
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/reportingdemo.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/reportingdemo.rst
new file mode 100644
index 0000000000..cab9314361
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/reportingdemo.rst
@@ -0,0 +1,708 @@
+.. _`tbreportdemo`:
+
+Demo of Python failure reports with pytest
+==========================================
+
+Here is a nice run of several failures and how ``pytest`` presents things:
+
+.. code-block:: pytest
+
+ assertion $ pytest failure_demo.py
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project/assertion
+ collected 44 items
+
+ failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF [100%]
+
+ ================================= FAILURES =================================
+ ___________________________ test_generative[3-6] ___________________________
+
+ param1 = 3, param2 = 6
+
+ @pytest.mark.parametrize("param1, param2", [(3, 6)])
+ def test_generative(param1, param2):
+ > assert param1 * 2 < param2
+ E assert (3 * 2) < 6
+
+ failure_demo.py:19: AssertionError
+ _________________________ TestFailing.test_simple __________________________
+
+ self = <failure_demo.TestFailing object at 0xdeadbeef0001>
+
+ def test_simple(self):
+ def f():
+ return 42
+
+ def g():
+ return 43
+
+ > assert f() == g()
+ E assert 42 == 43
+ E + where 42 = <function TestFailing.test_simple.<locals>.f at 0xdeadbeef0002>()
+ E + and 43 = <function TestFailing.test_simple.<locals>.g at 0xdeadbeef0003>()
+
+ failure_demo.py:30: AssertionError
+ ____________________ TestFailing.test_simple_multiline _____________________
+
+ self = <failure_demo.TestFailing object at 0xdeadbeef0004>
+
+ def test_simple_multiline(self):
+ > otherfunc_multi(42, 6 * 9)
+
+ failure_demo.py:33:
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+
+ a = 42, b = 54
+
+ def otherfunc_multi(a, b):
+ > assert a == b
+ E assert 42 == 54
+
+ failure_demo.py:14: AssertionError
+ ___________________________ TestFailing.test_not ___________________________
+
+ self = <failure_demo.TestFailing object at 0xdeadbeef0005>
+
+ def test_not(self):
+ def f():
+ return 42
+
+ > assert not f()
+ E assert not 42
+ E + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef0006>()
+
+ failure_demo.py:39: AssertionError
+ _________________ TestSpecialisedExplanations.test_eq_text _________________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0007>
+
+ def test_eq_text(self):
+ > assert "spam" == "eggs"
+ E AssertionError: assert 'spam' == 'eggs'
+ E - eggs
+ E + spam
+
+ failure_demo.py:44: AssertionError
+ _____________ TestSpecialisedExplanations.test_eq_similar_text _____________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0008>
+
+ def test_eq_similar_text(self):
+ > assert "foo 1 bar" == "foo 2 bar"
+ E AssertionError: assert 'foo 1 bar' == 'foo 2 bar'
+ E - foo 2 bar
+ E ? ^
+ E + foo 1 bar
+ E ? ^
+
+ failure_demo.py:47: AssertionError
+ ____________ TestSpecialisedExplanations.test_eq_multiline_text ____________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0009>
+
+ def test_eq_multiline_text(self):
+ > assert "foo\nspam\nbar" == "foo\neggs\nbar"
+ E AssertionError: assert 'foo\nspam\nbar' == 'foo\neggs\nbar'
+ E foo
+ E - eggs
+ E + spam
+ E bar
+
+ failure_demo.py:50: AssertionError
+ ______________ TestSpecialisedExplanations.test_eq_long_text _______________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000a>
+
+ def test_eq_long_text(self):
+ a = "1" * 100 + "a" + "2" * 100
+ b = "1" * 100 + "b" + "2" * 100
+ > assert a == b
+ E AssertionError: assert '111111111111...2222222222222' == '111111111111...2222222222222'
+ 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
+ E ? ^
+ E + 1111111111a222222222
+ E ? ^
+
+ failure_demo.py:55: AssertionError
+ _________ TestSpecialisedExplanations.test_eq_long_text_multiline __________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000b>
+
+ def test_eq_long_text_multiline(self):
+ a = "1\n" * 100 + "a" + "2\n" * 100
+ 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 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
+
+ failure_demo.py:60: AssertionError
+ _________________ TestSpecialisedExplanations.test_eq_list _________________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000c>
+
+ def test_eq_list(self):
+ > assert [0, 1, 2] == [0, 1, 3]
+ E assert [0, 1, 2] == [0, 1, 3]
+ E At index 2 diff: 2 != 3
+ E Use -v to get the full diff
+
+ failure_demo.py:63: AssertionError
+ ______________ TestSpecialisedExplanations.test_eq_list_long _______________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000d>
+
+ def test_eq_list_long(self):
+ a = [0] * 100 + [1] + [3] * 100
+ b = [0] * 100 + [2] + [3] * 100
+ > assert a == b
+ E assert [0, 0, 0, 0, 0, 0, ...] == [0, 0, 0, 0, 0, 0, ...]
+ E At index 100 diff: 1 != 2
+ E Use -v to get the full diff
+
+ failure_demo.py:68: AssertionError
+ _________________ TestSpecialisedExplanations.test_eq_dict _________________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000e>
+
+ 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 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
+
+ failure_demo.py:71: AssertionError
+ _________________ TestSpecialisedExplanations.test_eq_set __________________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000f>
+
+ def test_eq_set(self):
+ > assert {0, 10, 11, 12} == {0, 20, 21}
+ E AssertionError: assert {0, 10, 11, 12} == {0, 20, 21}
+ 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
+
+ failure_demo.py:74: AssertionError
+ _____________ TestSpecialisedExplanations.test_eq_longer_list ______________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0010>
+
+ def test_eq_longer_list(self):
+ > assert [1, 2] == [1, 2, 3]
+ E assert [1, 2] == [1, 2, 3]
+ E Right contains one more item: 3
+ E Use -v to get the full diff
+
+ failure_demo.py:77: AssertionError
+ _________________ TestSpecialisedExplanations.test_in_list _________________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0011>
+
+ def test_in_list(self):
+ > assert 1 in [0, 2, 3, 4, 5]
+ E assert 1 in [0, 2, 3, 4, 5]
+
+ failure_demo.py:80: AssertionError
+ __________ TestSpecialisedExplanations.test_not_in_text_multiline __________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0012>
+
+ def test_not_in_text_multiline(self):
+ 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 '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
+
+ failure_demo.py:84: AssertionError
+ ___________ TestSpecialisedExplanations.test_not_in_text_single ____________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0013>
+
+ def test_not_in_text_single(self):
+ text = "single foo line"
+ > assert "foo" not in text
+ E AssertionError: assert 'foo' not in 'single foo line'
+ E 'foo' is contained here:
+ E single foo line
+ E ? +++
+
+ failure_demo.py:88: AssertionError
+ _________ TestSpecialisedExplanations.test_not_in_text_single_long _________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0014>
+
+ def test_not_in_text_single_long(self):
+ text = "head " * 50 + "foo " + "tail " * 20
+ > assert "foo" not in text
+ E AssertionError: assert 'foo' not in 'head head h...l tail tail '
+ 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 ? +++
+
+ failure_demo.py:92: AssertionError
+ ______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0015>
+
+ def test_not_in_text_single_long_term(self):
+ 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 '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 ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+ failure_demo.py:96: AssertionError
+ ______________ TestSpecialisedExplanations.test_eq_dataclass _______________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0016>
+
+ def test_eq_dataclass(self):
+ from dataclasses import dataclass
+
+ @dataclass
+ class Foo:
+ a: int
+ b: str
+
+ left = Foo(1, "b")
+ right = Foo(1, "c")
+ > assert left == right
+ E AssertionError: assert TestSpecialis...oo(a=1, b='b') == TestSpecialis...oo(a=1, b='c')
+ E
+ E Omitting 1 identical items, use -vv to show
+ E Differing attributes:
+ 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
+
+ failure_demo.py:108: AssertionError
+ ________________ TestSpecialisedExplanations.test_eq_attrs _________________
+
+ self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0017>
+
+ def test_eq_attrs(self):
+ import attr
+
+ @attr.s
+ class Foo:
+ a = attr.ib()
+ b = attr.ib()
+
+ left = Foo(1, "b")
+ right = Foo(1, "c")
+ > assert left == right
+ E AssertionError: assert Foo(a=1, b='b') == Foo(a=1, b='c')
+ E
+ E Omitting 1 identical items, use -vv to show
+ E Differing attributes:
+ 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
+
+ failure_demo.py:120: AssertionError
+ ______________________________ test_attribute ______________________________
+
+ def test_attribute():
+ class Foo:
+ b = 1
+
+ i = Foo()
+ > assert i.b == 2
+ E assert 1 == 2
+ E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0xdeadbeef0018>.b
+
+ failure_demo.py:128: AssertionError
+ _________________________ test_attribute_instance __________________________
+
+ def test_attribute_instance():
+ class Foo:
+ b = 1
+
+ > assert Foo().b == 2
+ E AssertionError: assert 1 == 2
+ E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0019>.b
+ E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0019> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
+
+ failure_demo.py:135: AssertionError
+ __________________________ test_attribute_failure __________________________
+
+ def test_attribute_failure():
+ class Foo:
+ def _get_b(self):
+ raise Exception("Failed to get attrib")
+
+ b = property(_get_b)
+
+ i = Foo()
+ > assert i.b == 2
+
+ failure_demo.py:146:
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+
+ self = <failure_demo.test_attribute_failure.<locals>.Foo object at 0xdeadbeef001a>
+
+ def _get_b(self):
+ > raise Exception("Failed to get attrib")
+ E Exception: Failed to get attrib
+
+ failure_demo.py:141: Exception
+ _________________________ test_attribute_multiple __________________________
+
+ def test_attribute_multiple():
+ class Foo:
+ b = 1
+
+ class Bar:
+ b = 2
+
+ > assert Foo().b == Bar().b
+ E AssertionError: assert 1 == 2
+ E + where 1 = <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001b>.b
+ E + where <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001b> = <class 'failure_demo.test_attribute_multiple.<locals>.Foo'>()
+ E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001c>.b
+ E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001c> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
+
+ failure_demo.py:156: AssertionError
+ __________________________ TestRaises.test_raises __________________________
+
+ self = <failure_demo.TestRaises object at 0xdeadbeef001d>
+
+ def test_raises(self):
+ s = "qwe"
+ > raises(TypeError, int, s)
+ E ValueError: invalid literal for int() with base 10: 'qwe'
+
+ failure_demo.py:166: ValueError
+ ______________________ TestRaises.test_raises_doesnt _______________________
+
+ self = <failure_demo.TestRaises object at 0xdeadbeef001e>
+
+ def test_raises_doesnt(self):
+ > raises(OSError, int, "3")
+ E Failed: DID NOT RAISE <class 'OSError'>
+
+ failure_demo.py:169: Failed
+ __________________________ TestRaises.test_raise ___________________________
+
+ self = <failure_demo.TestRaises object at 0xdeadbeef001f>
+
+ def test_raise(self):
+ > raise ValueError("demo error")
+ E ValueError: demo error
+
+ failure_demo.py:172: ValueError
+ ________________________ TestRaises.test_tupleerror ________________________
+
+ self = <failure_demo.TestRaises object at 0xdeadbeef0020>
+
+ def test_tupleerror(self):
+ > a, b = [1] # NOQA
+ E ValueError: not enough values to unpack (expected 2, got 1)
+
+ failure_demo.py:175: ValueError
+ ______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______
+
+ self = <failure_demo.TestRaises object at 0xdeadbeef0021>
+
+ def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
+ items = [1, 2, 3]
+ print(f"items is {items!r}")
+ > a, b = items.pop()
+ E TypeError: cannot unpack non-iterable int object
+
+ failure_demo.py:180: TypeError
+ --------------------------- Captured stdout call ---------------------------
+ items is [1, 2, 3]
+ ________________________ TestRaises.test_some_error ________________________
+
+ self = <failure_demo.TestRaises object at 0xdeadbeef0022>
+
+ def test_some_error(self):
+ > if namenotexi: # NOQA
+ E NameError: name 'namenotexi' is not defined
+
+ failure_demo.py:183: NameError
+ ____________________ test_dynamic_compile_shows_nicely _____________________
+
+ def test_dynamic_compile_shows_nicely():
+ import importlib.util
+ import sys
+
+ src = "def foo():\n assert 1 == 0\n"
+ name = "abc-123"
+ spec = importlib.util.spec_from_loader(name, loader=None)
+ module = importlib.util.module_from_spec(spec)
+ code = compile(src, name, "exec")
+ exec(code, module.__dict__)
+ sys.modules[name] = module
+ > module.foo()
+
+ failure_demo.py:202:
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+
+ > ???
+ E AssertionError
+
+ abc-123:2: AssertionError
+ ____________________ TestMoreErrors.test_complex_error _____________________
+
+ self = <failure_demo.TestMoreErrors object at 0xdeadbeef0023>
+
+ def test_complex_error(self):
+ def f():
+ return 44
+
+ def g():
+ return 43
+
+ > somefunc(f(), g())
+
+ failure_demo.py:213:
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ failure_demo.py:10: in somefunc
+ otherfunc(x, y)
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+
+ a = 44, b = 43
+
+ def otherfunc(a, b):
+ > assert a == b
+ E assert 44 == 43
+
+ failure_demo.py:6: AssertionError
+ ___________________ TestMoreErrors.test_z1_unpack_error ____________________
+
+ self = <failure_demo.TestMoreErrors object at 0xdeadbeef0024>
+
+ def test_z1_unpack_error(self):
+ items = []
+ > a, b = items
+ E ValueError: not enough values to unpack (expected 2, got 0)
+
+ failure_demo.py:217: ValueError
+ ____________________ TestMoreErrors.test_z2_type_error _____________________
+
+ self = <failure_demo.TestMoreErrors object at 0xdeadbeef0025>
+
+ def test_z2_type_error(self):
+ items = 3
+ > a, b = items
+ E TypeError: cannot unpack non-iterable int object
+
+ failure_demo.py:221: TypeError
+ ______________________ TestMoreErrors.test_startswith ______________________
+
+ self = <failure_demo.TestMoreErrors object at 0xdeadbeef0026>
+
+ def test_startswith(self):
+ s = "123"
+ g = "456"
+ > assert s.startswith(g)
+ E AssertionError: assert False
+ E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
+ E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
+
+ failure_demo.py:226: AssertionError
+ __________________ TestMoreErrors.test_startswith_nested ___________________
+
+ self = <failure_demo.TestMoreErrors object at 0xdeadbeef0028>
+
+ def test_startswith_nested(self):
+ def f():
+ return "123"
+
+ def g():
+ return "456"
+
+ > assert f().startswith(g())
+ E AssertionError: assert False
+ E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
+ E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
+ E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0029>()
+ E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef002a>()
+
+ failure_demo.py:235: AssertionError
+ _____________________ TestMoreErrors.test_global_func ______________________
+
+ self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
+
+ def test_global_func(self):
+ > assert isinstance(globf(42), float)
+ E assert False
+ E + where False = isinstance(43, float)
+ E + where 43 = globf(42)
+
+ failure_demo.py:238: AssertionError
+ _______________________ TestMoreErrors.test_instance _______________________
+
+ self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
+
+ def test_instance(self):
+ self.x = 6 * 7
+ > assert self.x != 42
+ E assert 42 != 42
+ E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>.x
+
+ failure_demo.py:242: AssertionError
+ _______________________ TestMoreErrors.test_compare ________________________
+
+ self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
+
+ def test_compare(self):
+ > assert globf(10) < 5
+ E assert 11 < 5
+ E + where 11 = globf(10)
+
+ failure_demo.py:245: AssertionError
+ _____________________ TestMoreErrors.test_try_finally ______________________
+
+ self = <failure_demo.TestMoreErrors object at 0xdeadbeef002e>
+
+ def test_try_finally(self):
+ x = 1
+ try:
+ > assert x == 0
+ E assert 1 == 0
+
+ failure_demo.py:250: AssertionError
+ ___________________ TestCustomAssertMsg.test_single_line ___________________
+
+ self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
+
+ def test_single_line(self):
+ class A:
+ a = 1
+
+ b = 2
+ > assert A.a == b, "A.a appears not to be b"
+ E AssertionError: A.a appears not to be b
+ E assert 1 == 2
+ E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_single_line.<locals>.A'>.a
+
+ failure_demo.py:261: AssertionError
+ ____________________ TestCustomAssertMsg.test_multiline ____________________
+
+ self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
+
+ def test_multiline(self):
+ class A:
+ a = 1
+
+ b = 2
+ > assert (
+ A.a == b
+ ), "A.a appears not to be b\nor does not appear to be b\none of those"
+ E AssertionError: A.a appears not to be b
+ E or does not appear to be b
+ E one of those
+ E assert 1 == 2
+ E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_multiline.<locals>.A'>.a
+
+ failure_demo.py:268: AssertionError
+ ___________________ TestCustomAssertMsg.test_custom_repr ___________________
+
+ self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0031>
+
+ def test_custom_repr(self):
+ class JSON:
+ a = 1
+
+ def __repr__(self):
+ return "This is JSON\n{\n 'foo': 'bar'\n}"
+
+ a = JSON()
+ b = 2
+ > assert a.a == b, a
+ E AssertionError: This is JSON
+ E {
+ E 'foo': 'bar'
+ E }
+ E assert 1 == 2
+ E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a
+
+ failure_demo.py:281: AssertionError
+ ========================= short test summary info ==========================
+ FAILED failure_demo.py::test_generative[3-6] - assert (3 * 2) < 6
+ FAILED failure_demo.py::TestFailing::test_simple - assert 42 == 43
+ FAILED failure_demo.py::TestFailing::test_simple_multiline - assert 42 == 54
+ FAILED failure_demo.py::TestFailing::test_not - assert not 42
+ FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_text - Asser...
+ FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_similar_text
+ FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_multiline_text
+ FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_long_text - ...
+ FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_long_text_multiline
+ 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_longer_list
+ FAILED failure_demo.py::TestSpecialisedExplanations::test_in_list - asser...
+ FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_multiline
+ FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_single
+ FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_single_long
+ FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_single_long_term
+ FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_dataclass - ...
+ FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_attrs - Asse...
+ FAILED failure_demo.py::test_attribute - assert 1 == 2
+ FAILED failure_demo.py::test_attribute_instance - AssertionError: assert ...
+ FAILED failure_demo.py::test_attribute_failure - Exception: Failed to get...
+ FAILED failure_demo.py::test_attribute_multiple - AssertionError: assert ...
+ FAILED failure_demo.py::TestRaises::test_raises - ValueError: invalid lit...
+ FAILED failure_demo.py::TestRaises::test_raises_doesnt - Failed: DID NOT ...
+ FAILED failure_demo.py::TestRaises::test_raise - ValueError: demo error
+ FAILED failure_demo.py::TestRaises::test_tupleerror - ValueError: not eno...
+ FAILED failure_demo.py::TestRaises::test_reinterpret_fails_with_print_for_the_fun_of_it
+ FAILED failure_demo.py::TestRaises::test_some_error - NameError: name 'na...
+ FAILED failure_demo.py::test_dynamic_compile_shows_nicely - AssertionError
+ FAILED failure_demo.py::TestMoreErrors::test_complex_error - assert 44 == 43
+ FAILED failure_demo.py::TestMoreErrors::test_z1_unpack_error - ValueError...
+ FAILED failure_demo.py::TestMoreErrors::test_z2_type_error - TypeError: c...
+ FAILED failure_demo.py::TestMoreErrors::test_startswith - AssertionError:...
+ FAILED failure_demo.py::TestMoreErrors::test_startswith_nested - Assertio...
+ FAILED failure_demo.py::TestMoreErrors::test_global_func - assert False
+ FAILED failure_demo.py::TestMoreErrors::test_instance - assert 42 != 42
+ FAILED failure_demo.py::TestMoreErrors::test_compare - assert 11 < 5
+ FAILED failure_demo.py::TestMoreErrors::test_try_finally - assert 1 == 0
+ FAILED failure_demo.py::TestCustomAssertMsg::test_single_line - Assertion...
+ FAILED failure_demo.py::TestCustomAssertMsg::test_multiline - AssertionEr...
+ FAILED failure_demo.py::TestCustomAssertMsg::test_custom_repr - Assertion...
+ ============================ 44 failed in 0.12s ============================
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/simple.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/simple.rst
new file mode 100644
index 0000000000..a70f340499
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/simple.rst
@@ -0,0 +1,1086 @@
+
+
+Basic patterns and examples
+==========================================================
+
+How to change command line options defaults
+-------------------------------------------
+
+It can be tedious to type the same series of command line options
+every time you use ``pytest``. For example, if you always want to see
+detailed info on skipped and xfailed tests, as well as have terser "dot"
+progress output, you can write it into a configuration file:
+
+.. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ addopts = -ra -q
+
+
+Alternatively, you can set a ``PYTEST_ADDOPTS`` environment variable to add command
+line options while the environment is in use:
+
+.. code-block:: bash
+
+ export PYTEST_ADDOPTS="-v"
+
+Here's how the command-line is built in the presence of ``addopts`` or the environment variable:
+
+.. code-block:: text
+
+ <pytest.ini:addopts> $PYTEST_ADDOPTS <extra command-line arguments>
+
+So if the user executes in the command-line:
+
+.. code-block:: bash
+
+ pytest -m slow
+
+The actual command line executed is:
+
+.. code-block:: bash
+
+ pytest -ra -q -v -m slow
+
+Note that as usual for other command-line applications, in case of conflicting options the last one wins, so the example
+above will show verbose output because ``-v`` overwrites ``-q``.
+
+
+.. _request example:
+
+Pass different values to a test function, depending on command line options
+----------------------------------------------------------------------------
+
+.. regendoc:wipe
+
+Suppose we want to write a test that depends on a command line option.
+Here is a basic pattern to achieve this:
+
+.. code-block:: python
+
+ # content of test_sample.py
+ def test_answer(cmdopt):
+ if cmdopt == "type1":
+ print("first")
+ elif cmdopt == "type2":
+ print("second")
+ assert 0 # to see what was printed
+
+
+For this to work we need to add a command line option and
+provide the ``cmdopt`` through a :ref:`fixture function <fixture>`:
+
+.. code-block:: python
+
+ # content of conftest.py
+ import pytest
+
+
+ def pytest_addoption(parser):
+ parser.addoption(
+ "--cmdopt", action="store", default="type1", help="my option: type1 or type2"
+ )
+
+
+ @pytest.fixture
+ def cmdopt(request):
+ return request.config.getoption("--cmdopt")
+
+Let's run this without supplying our new option:
+
+.. code-block:: pytest
+
+ $ pytest -q test_sample.py
+ F [100%]
+ ================================= FAILURES =================================
+ _______________________________ test_answer ________________________________
+
+ cmdopt = 'type1'
+
+ def test_answer(cmdopt):
+ if cmdopt == "type1":
+ print("first")
+ elif cmdopt == "type2":
+ print("second")
+ > assert 0 # to see what was printed
+ E assert 0
+
+ test_sample.py:6: AssertionError
+ --------------------------- Captured stdout call ---------------------------
+ first
+ ========================= short test summary info ==========================
+ FAILED test_sample.py::test_answer - assert 0
+ 1 failed in 0.12s
+
+And now with supplying a command line option:
+
+.. code-block:: pytest
+
+ $ pytest -q --cmdopt=type2
+ F [100%]
+ ================================= FAILURES =================================
+ _______________________________ test_answer ________________________________
+
+ cmdopt = 'type2'
+
+ def test_answer(cmdopt):
+ if cmdopt == "type1":
+ print("first")
+ elif cmdopt == "type2":
+ print("second")
+ > assert 0 # to see what was printed
+ E assert 0
+
+ test_sample.py:6: AssertionError
+ --------------------------- Captured stdout call ---------------------------
+ second
+ ========================= short test summary info ==========================
+ FAILED test_sample.py::test_answer - assert 0
+ 1 failed in 0.12s
+
+You can see that the command line option arrived in our test.
+
+We could add simple validation for the input by listing the choices:
+
+.. code-block:: python
+
+ # content of conftest.py
+ import pytest
+
+
+ def pytest_addoption(parser):
+ parser.addoption(
+ "--cmdopt",
+ action="store",
+ default="type1",
+ help="my option: type1 or type2",
+ choices=("type1", "type2"),
+ )
+
+Now we'll get feedback on a bad argument:
+
+.. code-block:: pytest
+
+ $ pytest -q --cmdopt=type3
+ ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
+ pytest: error: argument --cmdopt: invalid choice: 'type3' (choose from 'type1', 'type2')
+
+
+If you need to provide more detailed error messages, you can use the
+``type`` parameter and raise ``pytest.UsageError``:
+
+.. code-block:: python
+
+ # content of conftest.py
+ import pytest
+
+
+ def type_checker(value):
+ msg = "cmdopt must specify a numeric type as typeNNN"
+ if not value.startswith("type"):
+ raise pytest.UsageError(msg)
+ try:
+ int(value[4:])
+ except ValueError:
+ raise pytest.UsageError(msg)
+
+ return value
+
+
+ def pytest_addoption(parser):
+ parser.addoption(
+ "--cmdopt",
+ action="store",
+ default="type1",
+ help="my option: type1 or type2",
+ type=type_checker,
+ )
+
+This completes the basic pattern. However, one often rather wants to
+process command line options outside of the test and rather pass in
+different or more complex objects.
+
+Dynamically adding command line options
+--------------------------------------------------------------
+
+.. regendoc:wipe
+
+Through :confval:`addopts` you can statically add command line
+options for your project. You can also dynamically modify
+the command line arguments before they get processed:
+
+.. code-block:: python
+
+ # setuptools plugin
+ import sys
+
+
+ def pytest_load_initial_conftests(args):
+ if "xdist" in sys.modules: # pytest-xdist plugin
+ import multiprocessing
+
+ num = max(multiprocessing.cpu_count() / 2, 1)
+ args[:] = ["-n", str(num)] + args
+
+If you have the :pypi:`xdist plugin <pytest-xdist>` installed
+you will now always perform test runs using a number
+of subprocesses close to your CPU. Running in an empty
+directory with the above conftest.py:
+
+.. code-block:: pytest
+
+ $ pytest
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 0 items
+
+ ========================== no tests ran in 0.12s ===========================
+
+.. _`excontrolskip`:
+
+Control skipping of tests according to command line option
+--------------------------------------------------------------
+
+.. regendoc:wipe
+
+Here is a ``conftest.py`` file adding a ``--runslow`` command
+line option to control skipping of ``pytest.mark.slow`` marked tests:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+ import pytest
+
+
+ def pytest_addoption(parser):
+ parser.addoption(
+ "--runslow", action="store_true", default=False, help="run slow tests"
+ )
+
+
+ def pytest_configure(config):
+ config.addinivalue_line("markers", "slow: mark test as slow to run")
+
+
+ def pytest_collection_modifyitems(config, items):
+ if config.getoption("--runslow"):
+ # --runslow given in cli: do not skip slow tests
+ return
+ skip_slow = pytest.mark.skip(reason="need --runslow option to run")
+ for item in items:
+ if "slow" in item.keywords:
+ item.add_marker(skip_slow)
+
+We can now write a test module like this:
+
+.. code-block:: python
+
+ # content of test_module.py
+ import pytest
+
+
+ def test_func_fast():
+ pass
+
+
+ @pytest.mark.slow
+ def test_func_slow():
+ pass
+
+and when running it will see a skipped "slow" test:
+
+.. code-block:: pytest
+
+ $ 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
+ rootdir: /home/sweet/project
+ collected 2 items
+
+ test_module.py .s [100%]
+
+ ========================= short test summary info ==========================
+ SKIPPED [1] test_module.py:8: need --runslow option to run
+ ======================= 1 passed, 1 skipped in 0.12s =======================
+
+Or run it including the ``slow`` marked test:
+
+.. code-block:: pytest
+
+ $ pytest --runslow
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 2 items
+
+ test_module.py .. [100%]
+
+ ============================ 2 passed in 0.12s =============================
+
+.. _`__tracebackhide__`:
+
+Writing well integrated assertion helpers
+-----------------------------------------
+
+.. regendoc:wipe
+
+If you have a test helper function called from a test you can
+use the ``pytest.fail`` marker to fail a test with a certain message.
+The test support function will not show up in the traceback if you
+set the ``__tracebackhide__`` option somewhere in the helper function.
+Example:
+
+.. code-block:: python
+
+ # content of test_checkconfig.py
+ import pytest
+
+
+ def checkconfig(x):
+ __tracebackhide__ = True
+ if not hasattr(x, "config"):
+ pytest.fail("not configured: {}".format(x))
+
+
+ def test_something():
+ checkconfig(42)
+
+The ``__tracebackhide__`` setting influences ``pytest`` showing
+of tracebacks: the ``checkconfig`` function will not be shown
+unless the ``--full-trace`` command line option is specified.
+Let's run our little function:
+
+.. code-block:: pytest
+
+ $ pytest -q test_checkconfig.py
+ F [100%]
+ ================================= FAILURES =================================
+ ______________________________ test_something ______________________________
+
+ def test_something():
+ > checkconfig(42)
+ E Failed: not configured: 42
+
+ test_checkconfig.py:11: Failed
+ ========================= short test summary info ==========================
+ FAILED test_checkconfig.py::test_something - Failed: not configured: 42
+ 1 failed in 0.12s
+
+If you only want to hide certain exceptions, you can set ``__tracebackhide__``
+to a callable which gets the ``ExceptionInfo`` object. You can for example use
+this to make sure unexpected exception types aren't hidden:
+
+.. code-block:: python
+
+ import operator
+ import pytest
+
+
+ class ConfigException(Exception):
+ pass
+
+
+ def checkconfig(x):
+ __tracebackhide__ = operator.methodcaller("errisinstance", ConfigException)
+ if not hasattr(x, "config"):
+ raise ConfigException("not configured: {}".format(x))
+
+
+ def test_something():
+ checkconfig(42)
+
+This will avoid hiding the exception traceback on unrelated exceptions (i.e.
+bugs in assertion helpers).
+
+
+Detect if running from within a pytest run
+--------------------------------------------------------------
+
+.. regendoc:wipe
+
+Usually it is a bad idea to make application code
+behave differently if called from a test. But if you
+absolutely must find out if your application code is
+running from a test you can do something like this:
+
+.. code-block:: python
+
+ # content of your_module.py
+
+
+ _called_from_test = False
+
+.. code-block:: python
+
+ # content of conftest.py
+
+
+ def pytest_configure(config):
+ your_module._called_from_test = True
+
+and then check for the ``your_module._called_from_test`` flag:
+
+.. code-block:: python
+
+ if your_module._called_from_test:
+ # called from within a test run
+ ...
+ else:
+ # called "normally"
+ ...
+
+accordingly in your application.
+
+Adding info to test report header
+--------------------------------------------------------------
+
+.. regendoc:wipe
+
+It's easy to present extra information in a ``pytest`` run:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+
+ def pytest_report_header(config):
+ return "project deps: mylib-1.1"
+
+which will add the string to the test header accordingly:
+
+.. code-block:: pytest
+
+ $ pytest
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ project deps: mylib-1.1
+ rootdir: /home/sweet/project
+ collected 0 items
+
+ ========================== no tests ran in 0.12s ===========================
+
+.. regendoc:wipe
+
+It is also possible to return a list of strings which will be considered as several
+lines of information. You may consider ``config.getoption('verbose')`` in order to
+display more information if applicable:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+
+ def pytest_report_header(config):
+ if config.getoption("verbose") > 0:
+ return ["info1: did you know that ...", "did you?"]
+
+which will add info only when run with "--v":
+
+.. code-block:: pytest
+
+ $ pytest -v
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
+ cachedir: .pytest_cache
+ info1: did you know that ...
+ did you?
+ rootdir: /home/sweet/project
+ collecting ... collected 0 items
+
+ ========================== no tests ran in 0.12s ===========================
+
+and nothing when run plainly:
+
+.. code-block:: pytest
+
+ $ pytest
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 0 items
+
+ ========================== no tests ran in 0.12s ===========================
+
+Profiling test duration
+--------------------------
+
+.. regendoc:wipe
+
+.. versionadded: 2.2
+
+If you have a slow running large test suite you might want to find
+out which tests are the slowest. Let's make an artificial test suite:
+
+.. code-block:: python
+
+ # content of test_some_are_slow.py
+ import time
+
+
+ def test_funcfast():
+ time.sleep(0.1)
+
+
+ def test_funcslow1():
+ time.sleep(0.2)
+
+
+ def test_funcslow2():
+ time.sleep(0.3)
+
+Now we can profile which test functions execute the slowest:
+
+.. code-block:: pytest
+
+ $ pytest --durations=3
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 3 items
+
+ test_some_are_slow.py ... [100%]
+
+ =========================== slowest 3 durations ============================
+ 0.30s call test_some_are_slow.py::test_funcslow2
+ 0.20s call test_some_are_slow.py::test_funcslow1
+ 0.10s call test_some_are_slow.py::test_funcfast
+ ============================ 3 passed in 0.12s =============================
+
+Incremental testing - test steps
+---------------------------------------------------
+
+.. regendoc:wipe
+
+Sometimes you may have a testing situation which consists of a series
+of test steps. If one step fails it makes no sense to execute further
+steps as they are all expected to fail anyway and their tracebacks
+add no insight. Here is a simple ``conftest.py`` file which introduces
+an ``incremental`` marker which is to be used on classes:
+
+.. code-block:: python
+
+ # 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)
+ _test_failed_incremental: Dict[str, Dict[Tuple[int, ...], str]] = {}
+
+
+ def pytest_runtest_makereport(item, call):
+ if "incremental" in item.keywords:
+ # incremental marker is used
+ if call.excinfo is not None:
+ # the test has failed
+ # retrieve the class name of the test
+ cls_name = str(item.cls)
+ # 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 test function
+ test_name = item.originalname or item.name
+ # store in _test_failed_incremental the original name of the failed test
+ _test_failed_incremental.setdefault(cls_name, {}).setdefault(
+ parametrize_index, test_name
+ )
+
+
+ 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("previous test failed ({})".format(test_name))
+
+
+These two hook implementations work together to abort incremental-marked
+tests in a class. Here is a test module example:
+
+.. code-block:: python
+
+ # content of test_step.py
+
+ import pytest
+
+
+ @pytest.mark.incremental
+ class TestUserHandling:
+ def test_login(self):
+ pass
+
+ def test_modification(self):
+ assert 0
+
+ def test_deletion(self):
+ pass
+
+
+ def test_normal():
+ pass
+
+If we run this:
+
+.. code-block:: pytest
+
+ $ pytest -rx
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 4 items
+
+ test_step.py .Fx. [100%]
+
+ ================================= FAILURES =================================
+ ____________________ TestUserHandling.test_modification ____________________
+
+ self = <test_step.TestUserHandling object at 0xdeadbeef0001>
+
+ def test_modification(self):
+ > assert 0
+ E assert 0
+
+ test_step.py:11: AssertionError
+ ========================= short test summary info ==========================
+ 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``
+failed. It is reported as an "expected failure".
+
+
+Package/Directory-level fixtures (setups)
+-------------------------------------------------------
+
+If you have nested test directories, you can have per-directory fixture scopes
+by placing fixture functions in a ``conftest.py`` file in that directory.
+You can use all types of fixtures including :ref:`autouse fixtures
+<autouse fixtures>` which are the equivalent of xUnit's setup/teardown
+concept. It's however recommended to have explicit fixture references in your
+tests or test classes rather than relying on implicitly executing
+setup/teardown functions, especially if they are far away from the actual tests.
+
+Here is an example for making a ``db`` fixture available in a directory:
+
+.. code-block:: python
+
+ # content of a/conftest.py
+ import pytest
+
+
+ class DB:
+ pass
+
+
+ @pytest.fixture(scope="session")
+ def db():
+ return DB()
+
+and then a test module in that directory:
+
+.. code-block:: python
+
+ # content of a/test_db.py
+ def test_a1(db):
+ assert 0, db # to show value
+
+another test module:
+
+.. code-block:: python
+
+ # content of a/test_db2.py
+ def test_a2(db):
+ assert 0, db # to show value
+
+and then a module in a sister directory which will not see
+the ``db`` fixture:
+
+.. code-block:: python
+
+ # content of b/test_error.py
+ def test_root(db): # no db here, will error out
+ pass
+
+We can run this:
+
+.. code-block:: pytest
+
+ $ pytest
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.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%]
+
+ ================================== ERRORS ==================================
+ _______________________ ERROR at setup of test_root ________________________
+ file /home/sweet/project/b/test_error.py, line 1
+ def test_root(db): # no db here, will error out
+ E fixture 'db' not found
+ > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
+ > use 'pytest --fixtures [testpath]' for help on them.
+
+ /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>
+
+ def test_a1(db):
+ > assert 0, db # to show value
+ E AssertionError: <conftest.DB object at 0xdeadbeef0003>
+ E assert 0
+
+ a/test_db.py:2: AssertionError
+ _________________________________ test_a2 __________________________________
+
+ db = <conftest.DB object at 0xdeadbeef0003>
+
+ def test_a2(db):
+ > assert 0, db # to show value
+ E AssertionError: <conftest.DB object at 0xdeadbeef0003>
+ E assert 0
+
+ a/test_db2.py:2: 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...
+ ERROR b/test_error.py::test_root
+ ============= 3 failed, 2 passed, 1 xfailed, 1 error in 0.12s ==============
+
+The two test modules in the ``a`` directory see the same ``db`` fixture instance
+while the one test in the sister-directory ``b`` doesn't see it. We could of course
+also define a ``db`` fixture in that sister directory's ``conftest.py`` file.
+Note that each fixture is only instantiated if there is a test actually needing
+it (unless you use "autouse" fixture which are always executed ahead of the first test
+executing).
+
+
+Post-process test reports / failures
+---------------------------------------
+
+If you want to postprocess test reports and need access to the executing
+environment you can implement a hook that gets called when the test
+"report" object is about to be created. Here we write out all failing
+test calls and also access a fixture (if it was used by the test) in
+case you want to query/look at it during your post processing. In our
+case we just write some information out to a ``failures`` file:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+ import pytest
+ import os.path
+
+
+ @pytest.hookimpl(tryfirst=True, hookwrapper=True)
+ def pytest_runtest_makereport(item, call):
+ # execute all other hooks to obtain the report object
+ outcome = yield
+ rep = outcome.get_result()
+
+ # 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:
+ # let's also access a fixture for the fun of it
+ if "tmp_path" in item.fixturenames:
+ extra = " ({})".format(item.funcargs["tmp_path"])
+ else:
+ extra = ""
+
+ f.write(rep.nodeid + extra + "\n")
+
+
+if you then have failing tests:
+
+.. code-block:: python
+
+ # content of test_module.py
+ def test_fail1(tmp_path):
+ assert 0
+
+
+ def test_fail2():
+ assert 0
+
+and run them:
+
+.. code-block:: pytest
+
+ $ pytest test_module.py
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 2 items
+
+ test_module.py FF [100%]
+
+ ================================= FAILURES =================================
+ ________________________________ test_fail1 ________________________________
+
+ tmp_path = PosixPath('PYTEST_TMPDIR/test_fail10')
+
+ def test_fail1(tmp_path):
+ > assert 0
+ E assert 0
+
+ test_module.py:2: AssertionError
+ ________________________________ test_fail2 ________________________________
+
+ def test_fail2():
+ > assert 0
+ E assert 0
+
+ test_module.py:6: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_module.py::test_fail1 - assert 0
+ FAILED test_module.py::test_fail2 - assert 0
+ ============================ 2 failed in 0.12s =============================
+
+you will have a "failures" file which contains the failing test ids:
+
+.. code-block:: bash
+
+ $ cat failures
+ test_module.py::test_fail1 (PYTEST_TMPDIR/test_fail10)
+ test_module.py::test_fail2
+
+Making test result information available in fixtures
+-----------------------------------------------------------
+
+.. regendoc:wipe
+
+If you want to make test result reports available in fixture finalizers
+here is a little example implemented via a local plugin:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+ import pytest
+
+
+ @pytest.hookimpl(tryfirst=True, hookwrapper=True)
+ def pytest_runtest_makereport(item, call):
+ # execute all other hooks to obtain the report object
+ outcome = yield
+ rep = outcome.get_result()
+
+ # set a report attribute for each phase of a call, which can
+ # be "setup", "call", "teardown"
+
+ setattr(item, "rep_" + rep.when, rep)
+
+
+ @pytest.fixture
+ def something(request):
+ 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)
+
+
+if you then have failing tests:
+
+.. code-block:: python
+
+ # content of test_module.py
+
+ import pytest
+
+
+ @pytest.fixture
+ def other():
+ assert 0
+
+
+ def test_setup_fails(something, other):
+ pass
+
+
+ def test_call_fails(something):
+ assert 0
+
+
+ def test_fail2():
+ assert 0
+
+and run it:
+
+.. code-block:: pytest
+
+ $ pytest -s test_module.py
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.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
+ F
+
+ ================================== ERRORS ==================================
+ ____________________ ERROR at setup of test_setup_fails ____________________
+
+ @pytest.fixture
+ def other():
+ > assert 0
+ E assert 0
+
+ test_module.py:7: AssertionError
+ ================================= FAILURES =================================
+ _____________________________ test_call_fails ______________________________
+
+ something = None
+
+ def test_call_fails(something):
+ > assert 0
+ E assert 0
+
+ test_module.py:15: AssertionError
+ ________________________________ test_fail2 ________________________________
+
+ def test_fail2():
+ > assert 0
+ E assert 0
+
+ test_module.py:19: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_module.py::test_call_fails - assert 0
+ FAILED test_module.py::test_fail2 - assert 0
+ ERROR test_module.py::test_setup_fails - assert 0
+ ======================== 2 failed, 1 error in 0.12s ========================
+
+You'll see that the fixture finalizers could use the precise reporting
+information.
+
+.. _pytest current test env:
+
+``PYTEST_CURRENT_TEST`` environment variable
+--------------------------------------------
+
+
+
+Sometimes a test session might get stuck and there might be no easy way to figure out
+which test got stuck, for example if pytest was run in quiet mode (``-q``) or you don't have access to the console
+output. This is particularly a problem if the problem happens only sporadically, the famous "flaky" kind of tests.
+
+``pytest`` sets the :envvar:`PYTEST_CURRENT_TEST` environment variable when running tests, which can be inspected
+by process monitoring utilities or libraries like :pypi:`psutil` to discover which test got stuck if necessary:
+
+.. code-block:: python
+
+ import psutil
+
+ for pid in psutil.pids():
+ environ = psutil.Process(pid).environ()
+ if "PYTEST_CURRENT_TEST" in environ:
+ print(f'pytest process {pid} running: {environ["PYTEST_CURRENT_TEST"]}')
+
+During the test session pytest will set ``PYTEST_CURRENT_TEST`` to the current test
+:ref:`nodeid <nodeids>` and the current stage, which can be ``setup``, ``call``,
+or ``teardown``.
+
+For example, when running a single test function named ``test_foo`` from ``foo_module.py``,
+``PYTEST_CURRENT_TEST`` will be set to:
+
+#. ``foo_module.py::test_foo (setup)``
+#. ``foo_module.py::test_foo (call)``
+#. ``foo_module.py::test_foo (teardown)``
+
+In that order.
+
+.. note::
+
+ The contents of ``PYTEST_CURRENT_TEST`` is meant to be human readable and the actual format
+ can be changed between releases (even bug fixes) so it shouldn't be relied on for scripting
+ or automation.
+
+.. _freezing-pytest:
+
+Freezing pytest
+---------------
+
+If you freeze your application using a tool like
+`PyInstaller <https://pyinstaller.readthedocs.io>`_
+in order to distribute it to your end-users, it is a good idea to also package
+your test runner and run your tests using the frozen application. This way packaging
+errors such as dependencies not being included into the executable can be detected early
+while also allowing you to send test files to users so they can run them in their
+machines, which can be useful to obtain more information about a hard to reproduce bug.
+
+Fortunately recent ``PyInstaller`` releases already have a custom hook
+for pytest, but if you are using another tool to freeze executables
+such as ``cx_freeze`` or ``py2exe``, you can use ``pytest.freeze_includes()``
+to obtain the full list of internal pytest modules. How to configure the tools
+to find the internal modules varies from tool to tool, however.
+
+Instead of freezing the pytest runner as a separate executable, you can make
+your frozen program work as the pytest runner by some clever
+argument handling during program startup. This allows you to
+have a single executable, which is usually more convenient.
+Please note that the mechanism for plugin discovery used by pytest
+(setuptools entry points) doesn't work with frozen executables so pytest
+can't find any third party plugins automatically. To include third party plugins
+like ``pytest-timeout`` they must be imported explicitly and passed on to pytest.main.
+
+.. code-block:: python
+
+ # contents of app_main.py
+ import sys
+ import pytest_timeout # Third party plugin
+
+ if len(sys.argv) > 1 and sys.argv[1] == "--pytest":
+ import pytest
+
+ sys.exit(pytest.main(sys.argv[2:], plugins=[pytest_timeout]))
+ else:
+ # normal application execution: at this point argv can be parsed
+ # by your argument-parsing library of choice as usual
+ ...
+
+
+This allows you to execute tests using the frozen
+application with standard ``pytest`` command-line options:
+
+.. code-block:: bash
+
+ ./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/special.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/special.rst
new file mode 100644
index 0000000000..ace37c7278
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/special.rst
@@ -0,0 +1,84 @@
+A session-fixture which can look at all collected tests
+----------------------------------------------------------------
+
+A session-scoped fixture effectively has access to all
+collected test items. Here is an example of a fixture
+function which walks all collected tests and looks
+if their test class defines a ``callme`` method and
+calls it:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+ import pytest
+
+
+ @pytest.fixture(scope="session", autouse=True)
+ def callattr_ahead_of_alltests(request):
+ print("callattr_ahead_of_alltests called")
+ seen = {None}
+ session = request.node
+ for item in session.items:
+ cls = item.getparent(pytest.Class)
+ if cls not in seen:
+ if hasattr(cls.obj, "callme"):
+ cls.obj.callme()
+ seen.add(cls)
+
+test classes may now define a ``callme`` method which
+will be called ahead of running any tests:
+
+.. code-block:: python
+
+ # content of test_module.py
+
+
+ class TestHello:
+ @classmethod
+ def callme(cls):
+ print("callme called!")
+
+ def test_method1(self):
+ print("test_method1 called")
+
+ def test_method2(self):
+ print("test_method2 called")
+
+
+ class TestOther:
+ @classmethod
+ def callme(cls):
+ print("callme other called")
+
+ def test_other(self):
+ print("test other")
+
+
+ # works with unittest as well ...
+ import unittest
+
+
+ class SomeTest(unittest.TestCase):
+ @classmethod
+ def callme(self):
+ print("SomeTest callme called")
+
+ def test_unit1(self):
+ print("test_unit1 method called")
+
+If you run this without output capturing:
+
+.. code-block:: pytest
+
+ $ pytest -q -s test_module.py
+ callattr_ahead_of_alltests called
+ callme called!
+ callme other called
+ SomeTest callme called
+ test_method1 called
+ .test_method2 called
+ .test other
+ .test_unit1 method called
+ .
+ 4 passed in 0.12s
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/xfail_demo.py b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/xfail_demo.py
new file mode 100644
index 0000000000..01e6da1ad2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/example/xfail_demo.py
@@ -0,0 +1,38 @@
+import pytest
+
+xfail = pytest.mark.xfail
+
+
+@xfail
+def test_hello():
+ assert 0
+
+
+@xfail(run=False)
+def test_hello2():
+ assert 0
+
+
+@xfail("hasattr(os, 'sep')")
+def test_hello3():
+ assert 0
+
+
+@xfail(reason="bug 110")
+def test_hello4():
+ assert 0
+
+
+@xfail('pytest.__version__[0] != "17"')
+def test_hello5():
+ assert 0
+
+
+def test_hello6():
+ pytest.xfail("reason")
+
+
+@xfail(raises=IndexError)
+def test_hello7():
+ x = []
+ x[1] = 1
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/anatomy.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/anatomy.rst
new file mode 100644
index 0000000000..e86dd74251
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/anatomy.rst
@@ -0,0 +1,46 @@
+.. _test-anatomy:
+
+Anatomy of a test
+=================
+
+In the simplest terms, a test is meant to look at the result of a particular
+behavior, and make sure that result aligns with what you would expect.
+Behavior is not something that can be empirically measured, which is why writing
+tests can be challenging.
+
+"Behavior" is the way in which some system **acts in response** to a particular
+situation and/or stimuli. But exactly *how* or *why* something is done is not
+quite as important as *what* was done.
+
+You can think of a test as being broken down into four steps:
+
+1. **Arrange**
+2. **Act**
+3. **Assert**
+4. **Cleanup**
+
+**Arrange** is where we prepare everything for our test. This means pretty
+much everything except for the "**act**". It's lining up the dominoes so that
+the **act** can do its thing in one, state-changing step. This can mean
+preparing objects, starting/killing services, entering records into a database,
+or even things like defining a URL to query, generating some credentials for a
+user that doesn't exist yet, or just waiting for some process to finish.
+
+**Act** is the singular, state-changing action that kicks off the **behavior**
+we want to test. This behavior is what carries out the changing of the state of
+the system under test (SUT), and it's the resulting changed state that we can
+look at to make a judgement about the behavior. This typically takes the form of
+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
+is where we take that measurement/observation and apply our judgement to it. If
+something should be green, we'd say ``assert thing == "green"``.
+
+**Cleanup** is where the test picks up after itself, so other tests aren't being
+accidentally influenced by it.
+
+At its core, the test is ultimately the **act** and **assert** steps, with the
+**arrange** step only providing the context. **Behavior** exists between **act**
+and **assert**.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/fixtures.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/fixtures.rst
new file mode 100644
index 0000000000..194e576493
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/fixtures.rst
@@ -0,0 +1,174 @@
+.. _about-fixtures:
+
+About fixtures
+===============
+
+.. seealso:: :ref:`how-to-fixtures`
+.. seealso:: :ref:`Fixtures reference <reference-fixtures>`
+
+pytest fixtures are designed to be explicit, modular and scalable.
+
+What fixtures are
+-----------------
+
+In testing, a `fixture <https://en.wikipedia.org/wiki/Test_fixture#Software>`_
+provides a defined, reliable and consistent context for the tests. This could
+include environment (for example a database configured with known parameters)
+or content (such as a dataset).
+
+Fixtures define the steps and data that constitute the *arrange* phase of a
+test (see :ref:`test-anatomy`). In pytest, they are functions you define that
+serve this purpose. They can also be used to define a test's *act* phase; this
+is a powerful technique for designing more complex tests.
+
+The services, state, or other operating environments set up by fixtures are
+accessed by test functions through arguments. For each fixture used by a test
+function there is typically a parameter (named after the fixture) in the test
+function's definition.
+
+We can tell pytest that a particular function is a fixture by decorating it with
+:py:func:`@pytest.fixture <pytest.fixture>`. Here's a simple example of
+what a fixture in pytest might look like:
+
+.. code-block:: python
+
+ import pytest
+
+
+ class Fruit:
+ def __init__(self, name):
+ self.name = name
+
+ def __eq__(self, other):
+ return self.name == other.name
+
+
+ @pytest.fixture
+ def my_fruit():
+ return Fruit("apple")
+
+
+ @pytest.fixture
+ def fruit_basket(my_fruit):
+ return [Fruit("banana"), my_fruit]
+
+
+ def test_my_fruit_in_basket(my_fruit, fruit_basket):
+ assert my_fruit in fruit_basket
+
+Tests don't have to be limited to a single fixture, either. They can depend on
+as many fixtures as you want, and fixtures can use other fixtures, as well. This
+is where pytest's fixture system really shines.
+
+
+Improvements over xUnit-style setup/teardown functions
+-----------------------------------------------------------
+
+pytest fixtures offer dramatic improvements over the classic xUnit
+style of setup/teardown functions:
+
+* fixtures have explicit names and are activated by declaring their use
+ from test functions, modules, classes or whole projects.
+
+* fixtures are implemented in a modular manner, as each fixture name
+ triggers a *fixture function* which can itself use other fixtures.
+
+* fixture management scales from simple unit to complex
+ functional testing, allowing to parametrize fixtures and tests according
+ to configuration and component options, or to re-use fixtures
+ across function, class, module or whole test session scopes.
+
+* teardown logic can be easily, and safely managed, no matter how many fixtures
+ are used, without the need to carefully handle errors by hand or micromanage
+ the order that cleanup steps are added.
+
+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.
+
+
+
+Fixture errors
+--------------
+
+pytest does its best to put all the fixtures for a given test in a linear order
+so that it can see which fixture happens first, second, third, and so on. If an
+earlier fixture has a problem, though, and raises an exception, pytest will stop
+executing fixtures for that test and mark the test as having an error.
+
+When a test is marked as having an error, it doesn't mean the test failed,
+though. It just means the test couldn't even be attempted because one of the
+things it depends on had a problem.
+
+This is one reason why it's a good idea to cut out as many unnecessary
+dependencies as possible for a given test. That way a problem in something
+unrelated isn't causing us to have an incomplete picture of what may or may not
+have issues.
+
+Here's a quick example to help explain:
+
+.. code-block:: python
+
+ import pytest
+
+
+ @pytest.fixture
+ def order():
+ return []
+
+
+ @pytest.fixture
+ def append_first(order):
+ order.append(1)
+
+
+ @pytest.fixture
+ def append_second(order, append_first):
+ order.extend([2])
+
+
+ @pytest.fixture(autouse=True)
+ def append_third(order, append_second):
+ order += [3]
+
+
+ def test_order(order):
+ assert order == [1, 2, 3]
+
+
+If, for whatever reason, ``order.append(1)`` had a bug and it raises an exception,
+we wouldn't be able to know if ``order.extend([2])`` or ``order += [3]`` would
+also have problems. After ``append_first`` throws an exception, pytest won't run
+any more fixtures for ``test_order``, and it won't even try to run
+``test_order`` itself. The only things that would've run would be ``order`` and
+``append_first``.
+
+
+Sharing test data
+-----------------
+
+If you want to make test data from files available to your tests, a good way
+to do this is by loading these data in a fixture for use by your tests.
+This makes use of the automatic caching mechanisms of pytest.
+
+Another good approach is by adding the data files in the ``tests`` folder.
+There are also community plugins available to help to manage this aspect of
+testing, e.g. :pypi:`pytest-datadir` and :pypi:`pytest-datafiles`.
+
+.. _fixtures-signal-cleanup:
+
+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
+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.
+
+The reason pytest does not handle those signals to perform fixture cleanup is that signal handlers are global,
+and changing them might interfere with the code under execution.
+
+If fixtures in your suite need special care regarding termination in those scenarios,
+see :issue:`this comment <5243#issuecomment-491522595>` in the issue
+tracker for a possible workaround.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/flaky.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/flaky.rst
new file mode 100644
index 0000000000..50121c7a76
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/flaky.rst
@@ -0,0 +1,126 @@
+
+Flaky tests
+-----------
+
+A "flaky" test is one that exhibits intermittent or sporadic failure, that seems to have non-deterministic behaviour. Sometimes it passes, sometimes it fails, and it's not clear why. This page discusses pytest features that can help and other general strategies for identifying, fixing or mitigating them.
+
+Why flaky tests are a problem
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Flaky tests are particularly troublesome when a continuous integration (CI) server is being used, so that all tests must pass before a new code change can be merged. If the test result is not a reliable signal -- that a test failure means the code change broke the test -- developers can become mistrustful of the test results, which can lead to overlooking genuine failures. It is also a source of wasted time as developers must re-run test suites and investigate spurious failures.
+
+
+Potential root causes
+^^^^^^^^^^^^^^^^^^^^^
+
+System state
+~~~~~~~~~~~~
+
+Broadly speaking, a flaky test indicates that the test relies on some system state that is not being appropriately controlled - the test environment is not sufficiently isolated. Higher level tests are more likely to be flaky as they rely on more state.
+
+Flaky tests sometimes appear when a test suite is run in parallel (such as use of pytest-xdist). This can indicate a test is reliant on test ordering.
+
+- Perhaps a different test is failing to clean up after itself and leaving behind data which causes the flaky test to fail.
+- The flaky test is reliant on data from a previous test that doesn't clean up after itself, and in parallel runs that previous test is not always present
+- Tests that modify global state typically cannot be run in parallel.
+
+
+Overly strict assertion
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Overly strict assertions can cause problems with floating point comparison as well as timing issues. :func:`pytest.approx` is useful here.
+
+
+Pytest features
+^^^^^^^^^^^^^^^
+
+Xfail strict
+~~~~~~~~~~~~
+
+:ref:`pytest.mark.xfail ref` with ``strict=False`` can be used to mark a test so that its failure does not cause the whole build to break. This could be considered like a manual quarantine, and is rather dangerous to use permanently.
+
+
+PYTEST_CURRENT_TEST
+~~~~~~~~~~~~~~~~~~~
+
+:envvar:`PYTEST_CURRENT_TEST` may be useful for figuring out "which test got stuck".
+See :ref:`pytest current test env` for more details.
+
+
+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.
+
+Plugins to deliberately randomize tests can help expose tests with state problems:
+
+* `pytest-random-order <https://github.com/jbasko/pytest-random-order>`_
+* `pytest-randomly <https://github.com/pytest-dev/pytest-randomly>`_
+
+
+Other general strategies
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Split up test suites
+~~~~~~~~~~~~~~~~~~~~
+
+It can be common to split a single test suite into two, such as unit vs integration, and only use the unit test suite as a CI gate. This also helps keep build times manageable as high level tests tend to be slower. However, it means it does become possible for code that breaks the build to be merged, so extra vigilance is needed for monitoring the integration test results.
+
+
+Video/screenshot on failure
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For UI tests these are important for understanding what the state of the UI was when the test failed. pytest-splinter can be used with plugins like pytest-bdd and can `save a screenshot on test failure <https://pytest-splinter.readthedocs.io/en/latest/#automatic-screenshots-on-test-failure>`_, which can help to isolate the cause.
+
+
+Delete or rewrite the test
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If the functionality is covered by other tests, perhaps the test can be removed. If not, perhaps it can be rewritten at a lower level which will remove the flakiness or make its source more apparent.
+
+
+Quarantine
+~~~~~~~~~~
+
+Mark Lapierre discusses the `Pros and Cons of Quarantined Tests <https://dev.to/mlapierre/pros-and-cons-of-quarantined-tests-2emj>`_ in a post from 2018.
+
+
+
+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.
+
+
+
+Research
+^^^^^^^^
+
+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>`__
+
+
+Resources
+^^^^^^^^^
+
+* `Eradicating Non-Determinism in Tests <https://martinfowler.com/articles/nonDeterminism.html>`_ by Martin Fowler, 2011
+* `No more flaky tests on the Go team <https://www.thoughtworks.com/insights/blog/no-more-flaky-tests-go-team>`_ by Pavan Sudarshan, 2012
+* `The Build That Cried Broken: Building Trust in your Continuous Integration Tests <https://www.youtube.com/embed/VotJqV4n8ig>`_ talk (video) by `Angie Jones <https://angiejones.tech/>`_ at SeleniumConf Austin 2017
+* `Test and Code Podcast: Flaky Tests and How to Deal with Them <https://testandcode.com/50>`_ by Brian Okken and Anthony Shaw, 2018
+* Microsoft:
+
+ * `How we approach testing VSTS to enable continuous delivery <https://blogs.msdn.microsoft.com/bharry/2017/06/28/testing-in-a-cloud-delivery-cadence/>`_ by Brian Harry MS, 2017
+ * `Eliminating Flaky Tests <https://docs.microsoft.com/en-us/azure/devops/learn/devops-at-microsoft/eliminating-flaky-tests>`_ blog and talk (video) by Munil Shah, 2017
+
+* Google:
+
+ * `Flaky Tests at Google and How We Mitigate Them <https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html>`_ by John Micco, 2016
+ * `Where do Google's flaky tests come from? <https://testing.googleblog.com/2017/04/where-do-our-flaky-tests-come-from.html>`_ by Jeff Listfield, 2017
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/goodpractices.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/goodpractices.rst
new file mode 100644
index 0000000000..32a14991ae
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/goodpractices.rst
@@ -0,0 +1,288 @@
+.. highlight:: python
+.. _`goodpractices`:
+
+Good Integration Practices
+=================================================
+
+Install package with pip
+-------------------------------------------------
+
+For development, we recommend you use :mod:`venv` for virtual environments and
+:doc:`pip:index` for installing your application and any dependencies,
+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:
+
+.. code-block:: toml
+
+ [build-system]
+ requires = ["setuptools>=42", "wheel"]
+ build-backend = "setuptools.build_meta"
+
+and a ``setup.cfg`` file containing your package's metadata with the following minimum content:
+
+.. 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()
+
+You can then install your package in "editable" mode by running from the same directory:
+
+.. code-block:: bash
+
+ pip install -e .
+
+which lets you change your source code (both tests and application) and rerun tests at will.
+
+.. _`test discovery`:
+.. _`Python test discovery`:
+
+Conventions for Python test discovery
+-------------------------------------------------
+
+``pytest`` implements the following standard test discovery:
+
+* If no arguments are specified then collection starts from :confval:`testpaths`
+ (if configured) or the current directory. Alternatively, command line arguments
+ can be used in any combination of directories, file names or node ids.
+* Recurse into directories, unless they match :confval:`norecursedirs`.
+* 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)
+
+For examples of how to customize your test discovery :doc:`/example/pythoncollection`.
+
+Within Python modules, ``pytest`` also discovers tests using the standard
+:ref:`unittest.TestCase <unittest.TestCase>` subclassing technique.
+
+
+Choosing a test layout / import rules
+-------------------------------------
+
+``pytest`` supports two common test layouts:
+
+Tests outside application code
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Putting tests into an extra directory outside your actual application code
+might be useful if you have many functional tests or for other reasons want
+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
+ tests/
+ test_app.py
+ test_view.py
+ ...
+
+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::
+
+ See :ref:`pytest vs python -m pytest` for more information about the difference between calling ``pytest`` and
+ ``python -m pytest``.
+
+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``.
+
+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:
+
+.. code-block:: text
+
+ pyproject.toml
+ setup.cfg
+ 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.
+
+.. _`src-layout`:
+
+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:: text
+
+ 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
+
+
+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>`_.
+
+.. 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.
+
+
+Tests as part of application code
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Inlining test directories into your application package
+is useful if you have direct relation between tests and application modules and
+want to distribute them along with your application:
+
+.. code-block:: text
+
+ pyproject.toml
+ setup.cfg
+ mypkg/
+ __init__.py
+ app.py
+ view.py
+ test/
+ __init__.py
+ test_app.py
+ test_view.py
+ ...
+
+In this scheme, it is easy to run your tests using the ``--pyargs`` option:
+
+.. code-block:: bash
+
+ pytest --pyargs mypkg
+
+``pytest`` will discover where ``mypkg`` is installed and collect tests from there.
+
+Note that this layout also works in conjunction with the ``src`` layout mentioned in the previous section.
+
+
+.. note::
+
+ You can use namespace packages (PEP420) for your application
+ but pytest will still perform `test package name`_ discovery based on the
+ presence of ``__init__.py`` files. If you use one of the
+ two recommended file system layouts above but leave away the ``__init__.py``
+ files from your directories, it should just work. From
+ "inlined tests", however, you will need to use absolute imports for
+ getting at your application code.
+
+.. _`test package name`:
+
+.. note::
+
+ In ``prepend`` and ``append`` import-modes, if pytest finds a ``"a/b/test_module.py"``
+ test file while recursing into the filesystem it determines the import name
+ as follows:
+
+ * determine ``basedir``: this is the first "upward" (towards the root)
+ directory not containing an ``__init__.py``. If e.g. both ``a``
+ and ``b`` contain an ``__init__.py`` file then the parent directory
+ of ``a`` will become the ``basedir``.
+
+ * perform ``sys.path.insert(0, basedir)`` to make the test module
+ importable under the fully qualified import name.
+
+ * ``import a.b.test_module`` where the path is determined
+ by converting path separators ``/`` into "." characters. This means
+ you must follow the convention of having directory and file
+ names map directly to the import names.
+
+ The reason for this somewhat evolved importing technique is
+ that in larger projects multiple test modules might import
+ from each other and thus deriving a canonical import name helps
+ to avoid surprises such as a test module getting imported twice.
+
+ With ``--import-mode=importlib`` things are less convoluted because
+ pytest doesn't need to change ``sys.path`` or ``sys.modules``, making things
+ much less surprising.
+
+
+.. _`buildout`: http://www.buildout.org/en/latest/
+
+.. _`use tox`:
+
+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
+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
+glitches.
+
+Do not run via setuptools
+-------------------------
+
+Integration with setuptools is **not recommended**,
+i.e. you should not be using ``python setup.py test`` or ``pytest-runner``,
+and may stop working in the future.
+
+This is deprecated since it depends on deprecated features of setuptools
+and relies on features that break security mechanisms in pip.
+For example 'setup_requires' and 'tests_require' bypass ``pip --require-hashes``.
+For more information and migration instructions,
+see the `pytest-runner notice <https://github.com/pytest-dev/pytest-runner#deprecation-notice>`_.
+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>`_.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/index.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/index.rst
new file mode 100644
index 0000000000..53910f1eb7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/index.rst
@@ -0,0 +1,15 @@
+:orphan:
+
+.. _explanation:
+
+Explanation
+================
+
+.. toctree::
+ :maxdepth: 1
+
+ anatomy
+ fixtures
+ goodpractices
+ flaky
+ pythonpath
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/pythonpath.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/pythonpath.rst
new file mode 100644
index 0000000000..2330356b86
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/explanation/pythonpath.rst
@@ -0,0 +1,133 @@
+.. _pythonpath:
+
+pytest import mechanisms and ``sys.path``/``PYTHONPATH``
+========================================================
+
+.. _`import-modes`:
+
+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
+import process can be controlled through the ``--import-mode`` command-line flag, which can assume
+these values:
+
+* ``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.
+
+ 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.
+
+ This is the classic mechanism, dating back from the time Python 2 was still supported.
+
+* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
+ there, and imported with ``__import__``.
+
+ 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:
+
+ ::
+
+ testing/__init__.py
+ testing/test_pkg_under_test.py
+ pkg_under_test/
+
+ 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.
+
+ 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`.
+
+ For this reason this doesn't require test module names to be unique, but also makes test
+ modules non-importable by each other.
+
+ 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
+might encounter because of that.
+
+Test modules / ``conftest.py`` files inside packages
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Consider this file and directory layout::
+
+ root/
+ |- foo/
+ |- __init__.py
+ |- conftest.py
+ |- bar/
+ |- __init__.py
+ |- tests/
+ |- __init__.py
+ |- test_foo.py
+
+
+When executing:
+
+.. code-block:: bash
+
+ pytest root/
+
+pytest will find ``foo/bar/tests/test_foo.py`` and realize it is part of a package given that
+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
+``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.
+
+Preserving the full package name is important when tests live in a package to avoid problems
+and allow test modules to have duplicated names. This is also discussed in details in
+:ref:`test discovery`.
+
+Standalone test modules / ``conftest.py`` files
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Consider this file and directory layout::
+
+ root/
+ |- foo/
+ |- conftest.py
+ |- bar/
+ |- tests/
+ |- test_foo.py
+
+
+When executing:
+
+.. code-block:: bash
+
+ pytest root/
+
+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``.
+
+For this reason this layout cannot have test modules with the same name, as they all will be
+imported in the global import namespace.
+
+This is also discussed in details in :ref:`test discovery`.
+
+.. _`pytest vs python -m pytest`:
+
+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
+is standard ``python`` behavior.
+
+See also :ref:`invoke-python`.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/funcarg_compare.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/funcarg_compare.rst
new file mode 100644
index 0000000000..3bf4527cfb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/funcarg_compare.rst
@@ -0,0 +1,230 @@
+:orphan:
+
+.. _`funcargcompare`:
+
+pytest-2.3: reasoning for fixture/funcarg evolution
+=============================================================
+
+**Target audience**: Reading this document requires basic knowledge of
+python testing, xUnit setup methods and the (previous) basic pytest
+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
+--------------------------------------------------------------
+
+The pre pytest-2.3 funcarg mechanism calls a factory each time a
+funcarg for a test function is required. If a factory wants to
+re-use a resource across different scopes, it often used
+the ``request.cached_setup()`` helper to manage caching of
+resources. Here is a basic example how we could implement
+a per-session Database object:
+
+.. code-block:: python
+
+ # content of conftest.py
+ class Database:
+ def __init__(self):
+ print("database instance created")
+
+ def destroy(self):
+ print("database instance destroyed")
+
+
+ def pytest_funcarg__db(request):
+ return request.cached_setup(
+ setup=DataBase, teardown=lambda db: db.destroy, scope="session"
+ )
+
+There are several limitations and difficulties with this approach:
+
+1. Scoping funcarg resource creation is not straight forward, instead one must
+ understand the intricate cached_setup() method mechanics.
+
+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
+ 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
+ ``extrakey`` parameter containing ``request.param`` to the
+ ``Request.cached_setup`` call.
+
+3. Multiple parametrized session-scoped resources will be active
+ at the same time, making it hard for them to affect global state
+ of the application under test.
+
+4. there is no way how you can make use of funcarg factories
+ in xUnit setup methods.
+
+5. A non-parametrized fixture function cannot use a parametrized
+ funcarg resource if it isn't stated in the test function signature.
+
+All of these limitations are addressed with pytest-2.3 and its
+improved :ref:`fixture mechanism <fixture>`.
+
+
+Direct scoping of fixture/funcarg factories
+--------------------------------------------------------
+
+Instead of calling cached_setup() with a cache scope, you can use the
+:ref:`@pytest.fixture <pytest.fixture>` decorator and directly state
+the scope:
+
+.. code-block:: python
+
+ @pytest.fixture(scope="session")
+ def db(request):
+ # factory will only be invoked once per session -
+ db = DataBase()
+ request.addfinalizer(db.destroy) # destroy when session is finished
+ return db
+
+This factory implementation does not need to call ``cached_setup()`` anymore
+because it will only be invoked once per session. Moreover, the
+``request.addfinalizer()`` registers a finalizer according to the specified
+resource scope on which the factory function is operating.
+
+
+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
+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
+
+Here the factory will be invoked twice (with the respective "mysql"
+and "pg" values set as ``request.param`` attributes) and all of
+the tests requiring "db" will run twice as well. The "mysql" and
+"pg" values will also be used for reporting the test-invocation variants.
+
+This new way of parametrizing funcarg factories should in many cases
+allow to re-use already written factories because effectively
+``request.param`` was already used when test functions/classes were
+parametrized via
+:py:func:`metafunc.parametrize(indirect=True) <pytest.Metafunc.parametrize>` calls.
+
+Of course it's perfectly fine to combine parametrization and scoping:
+
+.. code-block:: python
+
+ @pytest.fixture(scope="session", params=["mysql", "pg"])
+ def db(request):
+ if request.param == "mysql":
+ db = MySQL()
+ elif request.param == "pg":
+ db = PG()
+ request.addfinalizer(db.destroy) # destroy when session is finished
+ return db
+
+This would execute all tests requiring the per-session "db" resource twice,
+receiving the values created by the two respective invocations to the
+factory function.
+
+
+No ``pytest_funcarg__`` prefix when using @fixture decorator
+-------------------------------------------------------------------
+
+When using the ``@fixture`` decorator the name of the function
+denotes the name under which the resource can be accessed as a function
+argument:
+
+.. code-block:: python
+
+ @pytest.fixture()
+ def db(request):
+ ...
+
+The name under which the funcarg resource can be requested is ``db``.
+
+You can still use the "old" non-decorator way of specifying funcarg factories
+aka:
+
+.. code-block:: python
+
+ def pytest_funcarg__db(request):
+ ...
+
+
+But it is then not possible to define scoping and parametrization.
+It is thus recommended to use the factory decorator.
+
+
+solving per-session setup / autouse fixtures
+--------------------------------------------------------------
+
+pytest for a long time offered a pytest_configure and a pytest_sessionstart
+hook which are often used to setup global resources. This suffers from
+several problems:
+
+1. in distributed testing the managing process would setup test resources
+ that are never needed because it only co-ordinates the test run
+ activities of the worker processes.
+
+2. if you only perform a collection (with "--collect-only")
+ resource-setup will still be executed.
+
+3. If a pytest_sessionstart is contained in some subdirectories
+ conftest.py file, it will not be called. This stems from the
+ fact that this hook is actually used for reporting, in particular
+ the test-header with platform/custom information.
+
+Moreover, it was not easy to define a scoped setup from plugins or
+conftest files other than to implement a ``pytest_runtest_setup()`` hook
+and caring for scoping/caching yourself. And it's virtually impossible
+to do this with parametrization as ``pytest_runtest_setup()`` is called
+during test execution and parametrization happens at collection time.
+
+It follows that pytest_configure/session/runtest_setup are often not
+appropriate for implementing common fixture needs. Therefore,
+pytest-2.3 introduces :ref:`autouse fixtures` which fully
+integrate with the generic :ref:`fixture mechanism <fixture>`
+and obsolete many prior uses of pytest hooks.
+
+funcargs/fixture discovery now happens at collection time
+---------------------------------------------------------------------
+
+Since pytest-2.3, discovery of fixture/funcarg factories are taken care of
+at collection time. This is more efficient especially for large test suites.
+Moreover, a call to "pytest --collect-only" should be able to in the future
+show a lot of setup-information and thus presents a nice method to get an
+overview of fixture management in your project.
+
+.. _`compatibility notes`:
+
+.. _`funcargscompat`:
+
+Conclusion and compatibility notes
+---------------------------------------------------------
+
+**funcargs** were originally introduced to pytest-2.0. In pytest-2.3
+the mechanism was extended and refined and is now described as
+fixtures:
+
+* previously funcarg factories were specified with a special
+ ``pytest_funcarg__NAME`` prefix instead of using the
+ ``@pytest.fixture`` decorator.
+
+* Factories received a ``request`` object which managed caching through
+ ``request.cached_setup()`` calls and allowed using other funcargs via
+ ``request.getfuncargvalue()`` calls. These intricate APIs made it hard
+ to do proper parametrization and implement resource caching. The
+ new :py:func:`pytest.fixture` decorator allows to declare the scope
+ and let pytest figure things out for you.
+
+* if you used parametrization and funcarg factories which made use of
+ ``request.cached_setup()`` it is recommended to invest a few minutes
+ and simplify your fixture function code to use the :ref:`@pytest.fixture`
+ decorator instead. This will also allow to take advantage of
+ the automatic per-resource grouping of tests.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/funcargs.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/funcargs.rst
new file mode 100644
index 0000000000..4173675cdd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/funcargs.rst
@@ -0,0 +1,13 @@
+
+=======================================================
+funcargs: resource injection and parametrization
+=======================================================
+
+pytest-2.3 introduces major refinements to fixture management
+of which the funcarg mechanism introduced with pytest-2.0 remains
+a core part. The documentation has been refactored as well
+and you can read on here:
+
+- :ref:`fixtures`
+- :ref:`parametrize`
+- :ref:`funcargcompare`
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/getting-started.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/getting-started.rst
new file mode 100644
index 0000000000..5d13a76806
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/getting-started.rst
@@ -0,0 +1,257 @@
+.. _get-started:
+
+Get Started
+===================================
+
+.. _`getstarted`:
+.. _`installation`:
+
+Install ``pytest``
+----------------------------------------
+
+``pytest`` requires: Python 3.6, 3.7, 3.8, 3.9, or PyPy3.
+
+1. Run the following command in your command line:
+
+.. code-block:: bash
+
+ pip install -U pytest
+
+2. Check that you installed the correct version:
+
+.. code-block:: bash
+
+ $ pytest --version
+ pytest 7.0.1
+
+.. _`simpletest`:
+
+Create your first test
+----------------------------------------------------------
+
+Create a new file called ``test_sample.py``, containing a function, and a test:
+
+.. code-block:: python
+
+ # content of test_sample.py
+ def func(x):
+ return x + 1
+
+
+ def test_answer():
+ assert func(3) == 5
+
+The test
+
+.. code-block:: pytest
+
+ $ pytest
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 1 item
+
+ test_sample.py F [100%]
+
+ ================================= FAILURES =================================
+ _______________________________ test_answer ________________________________
+
+ def test_answer():
+ > assert func(3) == 5
+ E assert 4 == 5
+ E + where 4 = func(3)
+
+ test_sample.py:6: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_sample.py::test_answer - assert 4 == 5
+ ============================ 1 failed in 0.12s =============================
+
+The ``[100%]`` refers to the overall progress of running all test cases. After it finishes, pytest then shows a failure report because ``func(3)`` does not return ``5``.
+
+.. note::
+
+ You can use the ``assert`` statement to verify test expectations. pytest’s :ref:`Advanced assertion introspection <python:assert>` will intelligently report intermediate values of the assert expression so you can avoid the many names :ref:`of JUnit legacy methods <testcase-objects>`.
+
+Run multiple tests
+----------------------------------------------------------
+
+``pytest`` will run all files of the form test_*.py or \*_test.py in the current directory and its subdirectories. More generally, it follows :ref:`standard test discovery rules <test discovery>`.
+
+
+Assert that a certain exception is raised
+--------------------------------------------------------------
+
+Use the :ref:`raises <assertraises>` helper to assert that some code raises an exception:
+
+.. code-block:: python
+
+ # content of test_sysexit.py
+ import pytest
+
+
+ def f():
+ raise SystemExit(1)
+
+
+ def test_mytest():
+ with pytest.raises(SystemExit):
+ f()
+
+Execute the test function with “quiet” reporting mode:
+
+.. code-block:: pytest
+
+ $ pytest -q test_sysexit.py
+ . [100%]
+ 1 passed in 0.12s
+
+.. note::
+
+ The ``-q/--quiet`` flag keeps the output brief in this and following examples.
+
+Group multiple tests in a class
+--------------------------------------------------------------
+
+.. regendoc:wipe
+
+Once you develop multiple tests, you may want to group them into a class. pytest makes it easy to create a class containing more than one test:
+
+.. code-block:: python
+
+ # content of test_class.py
+ class TestClass:
+ def test_one(self):
+ x = "this"
+ assert "h" in x
+
+ def test_two(self):
+ x = "hello"
+ assert hasattr(x, "check")
+
+``pytest`` discovers all tests following its :ref:`Conventions for Python test discovery <test discovery>`, so it finds both ``test_`` prefixed functions. There is no need to subclass anything, but make sure to prefix your class with ``Test`` otherwise the class will be skipped. We can simply run the module by passing its filename:
+
+.. code-block:: pytest
+
+ $ pytest -q test_class.py
+ .F [100%]
+ ================================= FAILURES =================================
+ ____________________________ TestClass.test_two ____________________________
+
+ self = <test_class.TestClass object at 0xdeadbeef0001>
+
+ def test_two(self):
+ x = "hello"
+ > assert hasattr(x, "check")
+ E AssertionError: assert False
+ E + where False = hasattr('hello', 'check')
+
+ test_class.py:8: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_class.py::TestClass::test_two - AssertionError: assert False
+ 1 failed, 1 passed in 0.12s
+
+The first test passed and the second failed. You can easily see the intermediate values in the assertion to help you understand the reason for the failure.
+
+Grouping tests in classes can be beneficial for the following reasons:
+
+ * Test organization
+ * Sharing fixtures for tests only in that particular class
+ * Applying marks at the class level and having them implicitly apply to all tests
+
+Something to be aware of when grouping tests inside classes is that each test has a unique instance of the class.
+Having each test share the same class instance would be very detrimental to test isolation and would promote poor test practices.
+This is outlined below:
+
+.. regendoc:wipe
+
+.. code-block:: python
+
+ # content of test_class_demo.py
+ class TestClassDemoInstance:
+ value = 0
+
+ def test_one(self):
+ self.value = 1
+ assert self.value == 1
+
+ def test_two(self):
+ assert self.value == 1
+
+
+.. code-block:: pytest
+
+ $ pytest -k TestClassDemoInstance -q
+ .F [100%]
+ ================================= FAILURES =================================
+ ______________________ TestClassDemoInstance.test_two ______________________
+
+ self = <test_class_demo.TestClassDemoInstance object at 0xdeadbeef0002>
+
+ def test_two(self):
+ > assert self.value == 1
+ E assert 0 == 1
+ E + where 0 = <test_class_demo.TestClassDemoInstance object at 0xdeadbeef0002>.value
+
+ test_class_demo.py:9: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_class_demo.py::TestClassDemoInstance::test_two - assert 0 == 1
+ 1 failed, 1 passed in 0.12s
+
+Note that attributes added at class level are *class attributes*, so they will be shared between tests.
+
+Request a unique temporary directory for functional tests
+--------------------------------------------------------------
+
+``pytest`` provides :std:doc:`Builtin fixtures/function arguments <builtin>` to request arbitrary resources, like a unique temporary directory:
+
+.. code-block:: python
+
+ # content of test_tmp_path.py
+ def test_needsfiles(tmp_path):
+ print(tmp_path)
+ assert 0
+
+List the name ``tmp_path`` in the test function signature and ``pytest`` will lookup and call a fixture factory to create the resource before performing the test function call. Before the test runs, ``pytest`` creates a unique-per-test-invocation temporary directory:
+
+.. code-block:: pytest
+
+ $ pytest -q test_tmp_path.py
+ F [100%]
+ ================================= FAILURES =================================
+ _____________________________ test_needsfiles ______________________________
+
+ tmp_path = PosixPath('PYTEST_TMPDIR/test_needsfiles0')
+
+ def test_needsfiles(tmp_path):
+ print(tmp_path)
+ > assert 0
+ E assert 0
+
+ test_tmp_path.py:3: AssertionError
+ --------------------------- Captured stdout call ---------------------------
+ PYTEST_TMPDIR/test_needsfiles0
+ ========================= short test summary info ==========================
+ FAILED test_tmp_path.py::test_needsfiles - assert 0
+ 1 failed in 0.12s
+
+More info on temporary directory handling is available at :ref:`Temporary directories and files <tmp_path handling>`.
+
+Find out what kind of builtin :ref:`pytest fixtures <fixtures>` exist with the command:
+
+.. code-block:: bash
+
+ pytest --fixtures # shows builtin and custom fixtures
+
+Note that this command omits fixtures with leading ``_`` unless the ``-v`` option is added.
+
+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:`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
+* ":ref:`goodpractices`" for virtualenv and test layouts
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/historical-notes.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/historical-notes.rst
new file mode 100644
index 0000000000..29ebbd5d19
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/historical-notes.rst
@@ -0,0 +1,312 @@
+Historical Notes
+================
+
+This page lists features or behavior from previous versions of pytest which have changed over the years. They are
+kept here as a historical note so users looking at old code can find documentation related to them.
+
+
+.. _marker-revamp:
+
+Marker revamp and iteration
+---------------------------
+
+.. versionchanged:: 3.6
+
+pytest's marker implementation traditionally worked by simply updating the ``__dict__`` attribute of functions to cumulatively add markers. As a result, markers would unintentionally be passed along class hierarchies in surprising ways. Further, the API for retrieving them was inconsistent, as markers from parameterization would be stored differently than markers applied using the ``@pytest.mark`` decorator and markers added via ``node.add_marker``.
+
+This state of things made it technically next to impossible to use data from markers correctly without having a deep understanding of the internals, leading to subtle and hard to understand bugs in more advanced usages.
+
+Depending on how a marker got declared/changed one would get either a ``MarkerInfo`` which might contain markers from sibling classes,
+``MarkDecorators`` when marks came from parameterization or from a ``node.add_marker`` call, discarding prior marks. Also ``MarkerInfo`` acts like a single mark, when it in fact represents a merged view on multiple marks with the same name.
+
+On top of that markers were not accessible in the same way for modules, classes, and functions/methods.
+In fact, markers were only accessible in functions, even if they were declared on classes/modules.
+
+A new API to access markers has been introduced in pytest 3.6 in order to solve the problems with
+the initial design, providing the :func:`_pytest.nodes.Node.iter_markers` method to iterate over
+markers in a consistent manner and reworking the internals, which solved a great deal of problems
+with the initial design.
+
+
+.. _update marker code:
+
+Updating code
+~~~~~~~~~~~~~
+
+The old ``Node.get_marker(name)`` function is considered deprecated because it returns an internal ``MarkerInfo`` object
+which contains the merged name, ``*args`` and ``**kwargs`` of all the markers which apply to that node.
+
+In general there are two scenarios on how markers should be handled:
+
+1. Marks overwrite each other. Order matters but you only want to think of your mark as a single item. E.g.
+``log_level('info')`` at a module level can be overwritten by ``log_level('debug')`` for a specific test.
+
+ In this case, use ``Node.get_closest_marker(name)``:
+
+ .. code-block:: python
+
+ # replace this:
+ marker = item.get_marker("log_level")
+ if marker:
+ level = marker.args[0]
+
+ # by this:
+ marker = item.get_closest_marker("log_level")
+ if marker:
+ level = marker.args[0]
+
+2. Marks compose in an additive manner. E.g. ``skipif(condition)`` marks mean you just want to evaluate all of them,
+order doesn't even matter. You probably want to think of your marks as a set here.
+
+ In this case iterate over each mark and handle their ``*args`` and ``**kwargs`` individually.
+
+ .. code-block:: python
+
+ # replace this
+ skipif = item.get_marker("skipif")
+ if skipif:
+ for condition in skipif.args:
+ # eval condition
+ ...
+
+ # by this:
+ for skipif in item.iter_markers("skipif"):
+ condition = skipif.args[0]
+ # eval condition
+
+
+If you are unsure or have any questions, please consider opening
+:issue:`an issue <new>`.
+
+Related issues
+~~~~~~~~~~~~~~
+
+Here is a non-exhaustive list of issues fixed by the new implementation:
+
+* Marks don't pick up nested classes (:issue:`199`).
+
+* Markers stain on all related classes (:issue:`568`).
+
+* Combining marks - args and kwargs calculation (:issue:`2897`).
+
+* ``request.node.get_marker('name')`` returns ``None`` for markers applied in classes (:issue:`902`).
+
+* Marks applied in parametrize are stored as markdecorator (:issue:`2400`).
+
+* Fix marker interaction in a backward incompatible way (:issue:`1670`).
+
+* Refactor marks to get rid of the current "marks transfer" mechanism (:issue:`2363`).
+
+* Introduce FunctionDefinition node, use it in generate_tests (:issue:`2522`).
+
+* Remove named marker attributes and collect markers in items (:issue:`891`).
+
+* skipif mark from parametrize hides module level skipif mark (:issue:`1540`).
+
+* skipif + parametrize not skipping tests (:issue:`1296`).
+
+* Marker transfer incompatible with inheritance (:issue:`535`).
+
+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`.
+
+
+cache plugin integrated into the core
+-------------------------------------
+
+
+
+The functionality of the :ref:`core cache <cache>` plugin was previously distributed
+as a third party plugin named ``pytest-cache``. The core plugin
+is compatible regarding command line options and API usage except that you
+can only store/receive data between test runs that is json-serializable.
+
+.. _historical funcargs and pytest.funcargs:
+
+funcargs and ``pytest_funcarg__``
+---------------------------------
+
+
+
+In versions prior to 2.3 there was no ``@pytest.fixture`` marker
+and you had to use a magic ``pytest_funcarg__NAME`` prefix
+for the fixture factory. This remains and will remain supported
+but is not anymore advertised as the primary means of declaring fixture
+functions.
+
+
+``@pytest.yield_fixture`` decorator
+-----------------------------------
+
+
+
+Prior to version 2.10, in order to use a ``yield`` statement to execute teardown code one
+had to mark a fixture using the ``yield_fixture`` marker. From 2.10 onward, normal
+fixtures can use ``yield`` directly so the ``yield_fixture`` decorator is no longer needed
+and considered deprecated.
+
+
+``[pytest]`` header in ``setup.cfg``
+------------------------------------
+
+
+
+Prior to 3.0, the supported section name was ``[pytest]``. Due to how
+this may collide with some distutils commands, the recommended
+section name for ``setup.cfg`` files is now ``[tool:pytest]``.
+
+Note that for ``pytest.ini`` and ``tox.ini`` files the section
+name is ``[pytest]``.
+
+
+Applying marks to ``@pytest.mark.parametrize`` parameters
+---------------------------------------------------------
+
+
+
+Prior to version 3.1 the supported mechanism for marking values
+used the syntax:
+
+.. code-block:: python
+
+ import pytest
+
+
+ @pytest.mark.parametrize(
+ "test_input,expected", [("3+5", 8), ("2+4", 6), pytest.mark.xfail(("6*9", 42))]
+ )
+ def test_eval(test_input, expected):
+ assert eval(test_input) == expected
+
+
+This was an initial hack to support the feature but soon was demonstrated to be incomplete,
+broken for passing functions or applying multiple marks with the same name but different parameters.
+
+The old syntax is planned to be removed in pytest-4.0.
+
+
+``@pytest.mark.parametrize`` argument names as a tuple
+------------------------------------------------------
+
+
+
+In versions prior to 2.4 one needed to specify the argument
+names as a tuple. This remains valid but the simpler ``"name1,name2,..."``
+comma-separated-string syntax is now advertised first because
+it's easier to write and produces less line noise.
+
+
+setup: is now an "autouse fixture"
+----------------------------------
+
+
+
+During development prior to the pytest-2.3 release the name
+``pytest.setup`` was used but before the release it was renamed
+and moved to become part of the general fixture mechanism,
+namely :ref:`autouse fixtures`
+
+
+.. _string conditions:
+
+Conditions as strings instead of booleans
+-----------------------------------------
+
+
+
+Prior to pytest-2.4 the only way to specify skipif/xfail conditions was
+to use strings:
+
+.. code-block:: python
+
+ import sys
+
+
+ @pytest.mark.skipif("sys.version_info >= (3,3)")
+ def test_function():
+ ...
+
+During test function setup the skipif condition is evaluated by calling
+``eval('sys.version_info >= (3,0)', namespace)``. The namespace contains
+all the module globals, and ``os`` and ``sys`` as a minimum.
+
+Since pytest-2.4 :ref:`boolean conditions <condition booleans>` are considered preferable
+because markers can then be freely imported between test modules.
+With strings you need to import not only the marker but all variables
+used by the marker, which violates encapsulation.
+
+The reason for specifying the condition as a string was that ``pytest`` can
+report a summary of skip conditions based purely on the condition string.
+With conditions as booleans you are required to specify a ``reason`` string.
+
+Note that string conditions will remain fully supported and you are free
+to use them if you have no need for cross-importing markers.
+
+The evaluation of a condition string in ``pytest.mark.skipif(conditionstring)``
+or ``pytest.mark.xfail(conditionstring)`` takes place in a namespace
+dictionary which is constructed as follows:
+
+* the namespace is initialized by putting the ``sys`` and ``os`` modules
+ and the pytest ``config`` object into it.
+
+* updated with the module globals of the test function for which the
+ expression is applied.
+
+The pytest ``config`` object allows you to skip based on a test
+configuration value which you might have added:
+
+.. code-block:: python
+
+ @pytest.mark.skipif("not config.getvalue('db')")
+ def test_function():
+ ...
+
+The equivalent with "boolean conditions" is:
+
+.. code-block:: python
+
+ @pytest.mark.skipif(not pytest.config.getvalue("db"), reason="--db was not specified")
+ def test_function():
+ pass
+
+.. note::
+
+ You cannot use ``pytest.config.getvalue()`` in code
+ imported before pytest's argument parsing takes place. For example,
+ ``conftest.py`` files are imported before command line parsing and thus
+ ``config.getvalue()`` will not execute correctly.
+
+``pytest.set_trace()``
+----------------------
+
+
+
+Previous to version 2.4 to set a break point in code one needed to use ``pytest.set_trace()``:
+
+.. code-block:: python
+
+ import pytest
+
+
+ def test_function():
+ ...
+ pytest.set_trace() # invoke PDB debugger and tracing
+
+
+This is no longer needed and one can use the native ``import pdb;pdb.set_trace()`` call directly.
+
+For more details see :ref:`breakpoints`.
+
+"compat" properties
+-------------------
+
+
+
+Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances have long
+been documented as deprecated, but started to emit warnings from pytest ``3.9`` and onward.
+
+Users should just ``import pytest`` and access those objects using the ``pytest`` module.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/history.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/history.rst
new file mode 100644
index 0000000000..bb5aa49302
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/history.rst
@@ -0,0 +1,145 @@
+History
+=======
+
+pytest has a long and interesting history. The `first commit
+<https://github.com/pytest-dev/pytest/commit/5992a8ef21424d7571305a8d7e2a3431ee7e1e23>`__
+in this repository is from January 2007, and even that commit alone already
+tells a lot: The repository originally was from the :pypi:`py`
+library (later split off to pytest), and it
+originally was a SVN revision, migrated to Mercurial, and finally migrated to
+git.
+
+However, the commit says “create the new development trunk” and is
+already quite big: *435 files changed, 58640 insertions(+)*. This is because
+pytest originally was born as part of `PyPy <https://www.pypy.org/>`__, to make
+it easier to write tests for it. Here's how it evolved from there to its own
+project:
+
+
+- Late 2002 / early 2003, `PyPy was
+ born <https://morepypy.blogspot.com/2018/09/the-first-15-years-of-pypy.html>`__.
+- Like that blog post mentioned, from very early on, there was a big
+ focus on testing. There were various ``testsupport`` files on top of
+ unittest.py, and as early as June 2003, Holger Krekel (:user:`hpk42`)
+ `refactored <https://mail.python.org/pipermail/pypy-dev/2003-June/000787.html>`__
+ its test framework to clean things up (``pypy.tool.test``, but still
+ on top of ``unittest.py``, with nothing pytest-like yet).
+- In December 2003, there was `another
+ iteration <https://foss.heptapod.net/pypy/pypy/-/commit/02752373e1b29d89c6bb0a97e5f940caa22bdd63>`__
+ at improving their testing situation, by Stefan Schwarzer, called
+ ``pypy.tool.newtest``.
+- However, it didn’t seem to be around for long, as around June/July
+ 2004, efforts started on a thing called ``utest``, offering plain
+ assertions. This seems like the start of something pytest-like, but
+ unfortunately, it's unclear where the test runner's code was at the time.
+ The closest thing still around is `this
+ file <https://foss.heptapod.net/pypy/pypy/-/commit/0735f9ed287ec20950a7dd0a16fc10810d4f6847>`__,
+ but that doesn’t seem like a complete test runner at all. What can be seen
+ is that there were `various
+ efforts <https://foss.heptapod.net/pypy/pypy/-/commits/branch/default?utf8=%E2%9C%93&search=utest>`__
+ by Laura Creighton and Samuele Pedroni (:user:`pedronis`) at automatically
+ converting existing tests to the new ``utest`` framework.
+- Around the same time, for Europython 2004, @hpk42 `started a
+ project <http://web.archive.org/web/20041020215353/http://codespeak.net/svn/user/hpk/talks/std-talk.txt>`__
+ originally called “std”, intended to be a “complementary standard
+ library” - already laying out the principles behind what later became
+ pytest:
+
+ - current “batteries included” are very useful, but
+
+ - some of them are written in a pretty much java-like style,
+ especially the unittest-framework
+ - […]
+ - the best API is one that doesn’t exist
+
+ […]
+
+ - a testing package should require as few boilerplate code as
+ possible and offer much flexibility
+ - it should provide premium quality tracebacks and debugging aid
+
+ […]
+
+ - first of all … forget about limited “assertXYZ APIs” and use the
+ real thing, e.g.::
+
+ assert x == y
+
+ - this works with plain python but you get unhelpful “assertion
+ failed” errors with no information
+
+ - std.utest (magic!) actually reinterprets the assertion expression
+ and offers detailed information about underlying values
+
+- In September 2004, the ``py-dev`` mailinglist gets born, which `is
+ now <https://mail.python.org/pipermail/pytest-dev/>`__ ``pytest-dev``,
+ but thankfully with all the original archives still intact.
+
+- Around September/October 2004, the ``std`` project `was renamed
+ <https://mail.python.org/pipermail/pypy-dev/2004-September/001565.html>`__ to
+ ``py`` and ``std.utest`` became ``py.test``. This is also the first time the
+ `entire source
+ code <https://foss.heptapod.net/pypy/pypy/-/commit/42cf50c412026028e20acd23d518bd92e623ac11>`__,
+ seems to be available, with much of the API still being around today:
+
+ - ``py.path.local``, which is being phased out of pytest (in favour of
+ pathlib) some 16-17 years later
+ - The idea of the collection tree, including ``Collector``,
+ ``FSCollector``, ``Directory``, ``PyCollector``, ``Module``,
+ ``Class``
+ - Arguments like ``-x`` / ``--exitfirst``, ``-l`` /
+ ``--showlocals``, ``--fulltrace``, ``--pdb``, ``-S`` /
+ ``--nocapture`` (``-s`` / ``--capture=off`` today),
+ ``--collectonly`` (``--collect-only`` today)
+
+- In the same month, the ``py`` library `gets split off
+ <https://foss.heptapod.net/pypy/pypy/-/commit/6bdafe9203ad92eb259270b267189141c53bce33>`__
+ from ``PyPy``
+
+- It seemed to get rather quiet for a while, and little seemed to happen
+ between October 2004 (removing ``py`` from PyPy) and January
+ 2007 (first commit in the now-pytest repository). However, there were
+ various discussions about features/ideas on the mailinglist, and
+ :pypi:`a couple of releases <py/0.8.0-alpha2/#history>` every
+ couple of months:
+
+ - March 2006: py 0.8.0-alpha2
+ - May 2007: py 0.9.0
+ - March 2008: py 0.9.1 (first release to be found `in the pytest
+ changelog <https://github.com/pytest-dev/pytest/blob/main/doc/en/changelog.rst#091>`__!)
+ - August 2008: py 0.9.2
+
+- In August 2009, py 1.0.0 was released, `introducing a lot of
+ fundamental
+ features <https://holgerkrekel.net/2009/08/04/pylib-1-0-0-released-the-testing-with-python-innovations-continue/>`__:
+
+ - funcargs/fixtures
+ - A `plugin
+ architecture <http://web.archive.org/web/20090629032718/https://codespeak.net/py/dist/test/extend.html>`__
+ which still looks very much the same today!
+ - Various `default
+ plugins <http://web.archive.org/web/20091005181132/https://codespeak.net/py/dist/test/plugin/index.html>`__,
+ including
+ `monkeypatch <http://web.archive.org/web/20091012022829/http://codespeak.net/py/dist/test/plugin/how-to/monkeypatch.html>`__
+
+- Even back there, the
+ `FAQ <http://web.archive.org/web/20091005222413/http://codespeak.net/py/dist/faq.html>`__
+ said:
+
+ Clearly, [a second standard library] was ambitious and the naming has
+ maybe haunted the project rather than helping it. There may be a
+ project name change and possibly a split up into different projects
+ sometime.
+
+ and that finally happened in November 2010, when pytest 2.0.0 `was
+ released <https://mail.python.org/pipermail/pytest-dev/2010-November/001687.html>`__
+ as a package separate from ``py`` (but still called ``py.test``).
+
+- In August 2016, pytest 3.0.0 :std:ref:`was released <release-3.0.0>`,
+ which adds ``pytest`` (rather than ``py.test``) as the recommended
+ command-line entry point
+
+Due to this history, it's difficult to answer the question when pytest was started.
+It depends what point should really be seen as the start of it all. One
+possible interpretation is to pick Europython 2004, i.e. around June/July
+2004.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/assert.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/assert.rst
new file mode 100644
index 0000000000..cb70db6b8e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/assert.rst
@@ -0,0 +1,336 @@
+.. _`assert`:
+
+How to write and report assertions in tests
+==================================================
+
+.. _`assert with the assert statement`:
+
+Asserting with the ``assert`` statement
+---------------------------------------------------------
+
+``pytest`` allows you to use the standard Python ``assert`` for verifying
+expectations and values in Python tests. For example, you can write the
+following:
+
+.. code-block:: python
+
+ # content of test_assert1.py
+ def f():
+ return 3
+
+
+ def test_function():
+ assert f() == 4
+
+to assert that your function returns a certain value. If this assertion fails
+you will see the return value of the function call:
+
+.. code-block:: pytest
+
+ $ pytest test_assert1.py
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 1 item
+
+ test_assert1.py F [100%]
+
+ ================================= FAILURES =================================
+ ______________________________ test_function _______________________________
+
+ def test_function():
+ > assert f() == 4
+ E assert 3 == 4
+ E + where 3 = f()
+
+ test_assert1.py:6: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_assert1.py::test_function - assert 3 == 4
+ ============================ 1 failed in 0.12s =============================
+
+``pytest`` has support for showing the values of the most common subexpressions
+including calls, attributes, comparisons, and binary and unary
+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:
+
+.. 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.
+
+See :ref:`assert-details` for more information on assertion introspection.
+
+.. _`assertraises`:
+
+Assertions about expected exceptions
+------------------------------------------
+
+In order to write assertions about raised exceptions, you can use
+:func:`pytest.raises` as a context manager like this:
+
+.. code-block:: python
+
+ import pytest
+
+
+ def test_zero_division():
+ with pytest.raises(ZeroDivisionError):
+ 1 / 0
+
+and if you need to have access to the actual exception info you may use:
+
+.. code-block:: python
+
+ def test_recursion_depth():
+ with pytest.raises(RuntimeError) as excinfo:
+
+ def f():
+ f()
+
+ f()
+ assert "maximum recursion" in str(excinfo.value)
+
+``excinfo`` is an :class:`~pytest.ExceptionInfo` instance, which is a wrapper around
+the actual exception raised. The main attributes of interest are
+``.type``, ``.value`` and ``.traceback``.
+
+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``):
+
+.. code-block:: python
+
+ import pytest
+
+
+ def myfunc():
+ raise ValueError("Exception 123 raised")
+
+
+ def test_match():
+ 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.
+
+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:
+
+.. code-block:: python
+
+ pytest.raises(ExpectedException, func, *args, **kwargs)
+
+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
+specific way than just having any exception raised:
+
+.. code-block:: python
+
+ @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.
+
+
+.. _`assertwarns`:
+
+Assertions about expected warnings
+-----------------------------------------
+
+
+
+You can check that code raises a particular warning using
+:ref:`pytest.warns <warns>`.
+
+
+.. _newreport:
+
+Making use of context-sensitive comparisons
+-------------------------------------------------
+
+
+
+``pytest`` has rich support for providing context-sensitive information
+when it encounters comparisons. For example:
+
+.. code-block:: python
+
+ # content of test_assert2.py
+ def test_set_comparison():
+ set1 = set("1308")
+ set2 = set("8035")
+ assert set1 == set2
+
+if you run this module:
+
+.. code-block:: pytest
+
+ $ pytest test_assert2.py
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 1 item
+
+ test_assert2.py F [100%]
+
+ ================================= FAILURES =================================
+ ___________________________ test_set_comparison ____________________________
+
+ def test_set_comparison():
+ set1 = set("1308")
+ set2 = set("8035")
+ > assert set1 == set2
+ E AssertionError: assert {'0', '1', '3', '8'} == {'0', '3', '5', '8'}
+ 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
+
+ test_assert2.py:4: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_assert2.py::test_set_comparison - AssertionError: assert {'0'...
+ ============================ 1 failed in 0.12s =============================
+
+Special comparisons are done for a number of cases:
+
+* comparing long strings: a context diff is shown
+* comparing long sequences: first failing indices
+* comparing dicts: different entries
+
+See the :ref:`reporting demo <tbreportdemo>` for many more examples.
+
+Defining your own explanation for failed assertions
+---------------------------------------------------
+
+It is possible to add your own detailed explanations by implementing
+the ``pytest_assertrepr_compare`` hook.
+
+.. autofunction:: _pytest.hookspec.pytest_assertrepr_compare
+ :noindex:
+
+As an example consider adding the following hook in a :ref:`conftest.py <conftest.py>`
+file which provides an alternative explanation for ``Foo`` objects:
+
+.. code-block:: python
+
+ # content of conftest.py
+ from test_foocompare import Foo
+
+
+ def pytest_assertrepr_compare(op, left, right):
+ if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
+ return [
+ "Comparing Foo instances:",
+ " vals: {} != {}".format(left.val, right.val),
+ ]
+
+now, given this test module:
+
+.. code-block:: python
+
+ # content of test_foocompare.py
+ class Foo:
+ def __init__(self, val):
+ self.val = val
+
+ def __eq__(self, other):
+ return self.val == other.val
+
+
+ def test_compare():
+ f1 = Foo(1)
+ f2 = Foo(2)
+ assert f1 == f2
+
+you can run the test module and get the custom output defined in
+the conftest file:
+
+.. code-block:: pytest
+
+ $ pytest -q test_foocompare.py
+ F [100%]
+ ================================= FAILURES =================================
+ _______________________________ test_compare _______________________________
+
+ def test_compare():
+ f1 = Foo(1)
+ f2 = Foo(2)
+ > assert f1 == f2
+ E assert Comparing Foo instances:
+ E vals: 1 != 2
+
+ test_foocompare.py:12: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_foocompare.py::test_compare - assert Comparing Foo instances:
+ 1 failed in 0.12s
+
+.. _assert-details:
+.. _`assert introspection`:
+
+Assertion introspection details
+-------------------------------
+
+
+Reporting details about a failing assertion is achieved by rewriting assert
+statements before they are run. Rewritten assert statements put introspection
+information into the assertion failure message. ``pytest`` only rewrites test
+modules directly discovered by its test collection process, so **asserts in
+supporting modules which are not themselves test modules will not be rewritten**.
+
+You can manually enable assertion rewriting for an imported module by calling
+:ref:`register_assert_rewrite <assertion-rewriting>`
+before you import it (a good place to do that is in your root ``conftest.py``).
+
+For further information, Benjamin Peterson wrote up `Behind the scenes of pytest's new assertion rewriting <http://pybites.blogspot.com/2011/07/behind-scenes-of-pytests-new-assertion.html>`_.
+
+Assertion rewriting caches files on disk
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``pytest`` will write back the rewritten modules to disk for caching. You can disable
+this behavior (for example to avoid leaving stale ``.pyc`` files around in projects that
+move files around a lot) by adding this to the top of your ``conftest.py`` file:
+
+.. code-block:: python
+
+ import sys
+
+ sys.dont_write_bytecode = True
+
+Note that you still get the benefits of assertion introspection, the only change is that
+the ``.pyc`` files won't be cached on disk.
+
+Additionally, rewriting will silently skip caching if it cannot write new ``.pyc`` files,
+i.e. in a read-only filesystem or a zipfile.
+
+
+Disabling assert rewriting
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``pytest`` rewrites test modules on import by using an import
+hook to write new ``pyc`` files. Most of the time this works transparently.
+However, if you are working with the import machinery yourself, the import hook may
+interfere.
+
+If this is the case you have two options:
+
+* Disable rewriting for a specific module by adding the string
+ ``PYTEST_DONT_REWRITE`` to its docstring.
+
+* Disable rewriting for all modules by using ``--assert=plain``.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/bash-completion.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/bash-completion.rst
new file mode 100644
index 0000000000..245dfd6d9a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/bash-completion.rst
@@ -0,0 +1,33 @@
+
+.. _bash_completion:
+
+How to set up bash completion
+=============================
+
+When using bash as your shell, ``pytest`` can use argcomplete
+(https://argcomplete.readthedocs.io/) for auto-completion.
+For this ``argcomplete`` needs to be installed **and** enabled.
+
+Install argcomplete using:
+
+.. code-block:: bash
+
+ sudo pip install 'argcomplete>=0.5.7'
+
+For global activation of all argcomplete enabled python applications run:
+
+.. code-block:: bash
+
+ sudo activate-global-python-argcomplete
+
+For permanent (but not global) ``pytest`` activation, use:
+
+.. code-block:: bash
+
+ register-python-argcomplete pytest >> ~/.bashrc
+
+For one-time activation of argcomplete for ``pytest`` only, use:
+
+.. code-block:: bash
+
+ eval "$(register-python-argcomplete pytest)"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/cache.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/cache.rst
new file mode 100644
index 0000000000..e7994645dd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/cache.rst
@@ -0,0 +1,329 @@
+.. _`cache_provider`:
+.. _cache:
+
+
+How to re-run failed tests and maintain state between test runs
+===============================================================
+
+
+
+Usage
+---------
+
+The plugin provides two command line options to rerun failures from the
+last ``pytest`` invocation:
+
+* ``--lf``, ``--last-failed`` - to only re-run the failures.
+* ``--ff``, ``--failed-first`` - to run the failures first and then the rest of
+ the tests.
+
+For cleanup (usually not needed), a ``--cache-clear`` option allows to remove
+all cross-session cache contents ahead of a test run.
+
+Other plugins may access the `config.cache`_ object to set/get
+**json encodable** values between ``pytest`` invocations.
+
+.. note::
+
+ This plugin is enabled by default, but can be disabled if needed: see
+ :ref:`cmdunregister` (the internal name for this plugin is
+ ``cacheprovider``).
+
+
+Rerunning only failures or failures first
+-----------------------------------------------
+
+First, let's create 50 test invocation of which only 2 fail:
+
+.. code-block:: python
+
+ # content of test_50.py
+ import pytest
+
+
+ @pytest.mark.parametrize("i", range(50))
+ def test_num(i):
+ if i in (17, 25):
+ pytest.fail("bad luck")
+
+If you run this for the first time you will see two failures:
+
+.. code-block:: pytest
+
+ $ pytest -q
+ .................F.......F........................ [100%]
+ ================================= FAILURES =================================
+ _______________________________ test_num[17] _______________________________
+
+ i = 17
+
+ @pytest.mark.parametrize("i", range(50))
+ def test_num(i):
+ if i in (17, 25):
+ > pytest.fail("bad luck")
+ E Failed: bad luck
+
+ test_50.py:7: Failed
+ _______________________________ test_num[25] _______________________________
+
+ i = 25
+
+ @pytest.mark.parametrize("i", range(50))
+ def test_num(i):
+ if i in (17, 25):
+ > pytest.fail("bad luck")
+ E Failed: bad luck
+
+ test_50.py:7: Failed
+ ========================= short test summary info ==========================
+ FAILED test_50.py::test_num[17] - Failed: bad luck
+ FAILED test_50.py::test_num[25] - Failed: bad luck
+ 2 failed, 48 passed in 0.12s
+
+If you then run it with ``--lf``:
+
+.. code-block:: pytest
+
+ $ pytest --lf
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 2 items
+ run-last-failure: rerun previous 2 failures
+
+ test_50.py FF [100%]
+
+ ================================= FAILURES =================================
+ _______________________________ test_num[17] _______________________________
+
+ i = 17
+
+ @pytest.mark.parametrize("i", range(50))
+ def test_num(i):
+ if i in (17, 25):
+ > pytest.fail("bad luck")
+ E Failed: bad luck
+
+ test_50.py:7: Failed
+ _______________________________ test_num[25] _______________________________
+
+ i = 25
+
+ @pytest.mark.parametrize("i", range(50))
+ def test_num(i):
+ if i in (17, 25):
+ > pytest.fail("bad luck")
+ E Failed: bad luck
+
+ test_50.py:7: Failed
+ ========================= short test summary info ==========================
+ FAILED test_50.py::test_num[17] - Failed: bad luck
+ FAILED test_50.py::test_num[25] - Failed: bad luck
+ ============================ 2 failed in 0.12s =============================
+
+You have run only the two failing tests from the last run, while the 48 passing
+tests have not been run ("deselected").
+
+Now, if you run with the ``--ff`` option, all tests will be run but the first
+previous failures will be executed first (as can be seen from the series
+of ``FF`` and dots):
+
+.. code-block:: pytest
+
+ $ pytest --ff
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 50 items
+ run-last-failure: rerun previous 2 failures first
+
+ test_50.py FF................................................ [100%]
+
+ ================================= FAILURES =================================
+ _______________________________ test_num[17] _______________________________
+
+ i = 17
+
+ @pytest.mark.parametrize("i", range(50))
+ def test_num(i):
+ if i in (17, 25):
+ > pytest.fail("bad luck")
+ E Failed: bad luck
+
+ test_50.py:7: Failed
+ _______________________________ test_num[25] _______________________________
+
+ i = 25
+
+ @pytest.mark.parametrize("i", range(50))
+ def test_num(i):
+ if i in (17, 25):
+ > pytest.fail("bad luck")
+ E Failed: bad luck
+
+ test_50.py:7: Failed
+ ========================= short test summary info ==========================
+ FAILED test_50.py::test_num[17] - Failed: bad luck
+ FAILED test_50.py::test_num[25] - Failed: bad luck
+ ======================= 2 failed, 48 passed in 0.12s =======================
+
+.. _`config.cache`:
+
+New ``--nf``, ``--new-first`` options: run new tests first followed by the rest
+of the tests, in both cases tests are also sorted by the file modified time,
+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:
+
+.. 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
+
+The new config.cache object
+--------------------------------
+
+.. regendoc:wipe
+
+Plugins or conftest.py support code can get a cached value using the
+pytest ``config`` object. Here is a basic example plugin which
+implements a :ref:`fixture <fixture>` which re-uses previously created state
+across pytest invocations:
+
+.. code-block:: python
+
+ # content of test_caching.py
+ import pytest
+ import time
+
+
+ def expensive_computation():
+ print("running expensive computation...")
+
+
+ @pytest.fixture
+ def mydata(request):
+ val = request.config.cache.get("example/value", None)
+ if val is None:
+ expensive_computation()
+ val = 42
+ request.config.cache.set("example/value", val)
+ return val
+
+
+ def test_function(mydata):
+ assert mydata == 23
+
+If you run this command for the first time, you can see the print statement:
+
+.. code-block:: pytest
+
+ $ pytest -q
+ F [100%]
+ ================================= FAILURES =================================
+ ______________________________ test_function _______________________________
+
+ mydata = 42
+
+ def test_function(mydata):
+ > assert mydata == 23
+ E assert 42 == 23
+
+ test_caching.py:20: AssertionError
+ -------------------------- Captured stdout setup ---------------------------
+ running expensive computation...
+ ========================= short test summary info ==========================
+ FAILED test_caching.py::test_function - assert 42 == 23
+ 1 failed in 0.12s
+
+If you run it a second time, the value will be retrieved from
+the cache and nothing will be printed:
+
+.. code-block:: pytest
+
+ $ pytest -q
+ F [100%]
+ ================================= FAILURES =================================
+ ______________________________ test_function _______________________________
+
+ mydata = 42
+
+ def test_function(mydata):
+ > assert mydata == 23
+ E assert 42 == 23
+
+ test_caching.py:20: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_caching.py::test_function - assert 42 == 23
+ 1 failed in 0.12s
+
+See the :fixture:`config.cache fixture <cache>` for more details.
+
+
+Inspecting Cache content
+------------------------
+
+You can always peek at the content of the cache using the
+``--cache-show`` command line option:
+
+.. code-block:: pytest
+
+ $ pytest --cache-show
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ cachedir: /home/sweet/project/.pytest_cache
+ --------------------------- cache values for '*' ---------------------------
+ cache/lastfailed contains:
+ {'test_caching.py::test_function': True}
+ cache/nodeids contains:
+ ['test_caching.py::test_function']
+ cache/stepwise contains:
+ []
+ example/value contains:
+ 42
+
+ ========================== no tests ran in 0.12s ===========================
+
+``--cache-show`` takes an optional argument to specify a glob pattern for
+filtering:
+
+.. code-block:: pytest
+
+ $ pytest --cache-show example/*
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ cachedir: /home/sweet/project/.pytest_cache
+ ----------------------- cache values for 'example/*' -----------------------
+ example/value contains:
+ 42
+
+ ========================== no tests ran in 0.12s ===========================
+
+Clearing Cache content
+----------------------
+
+You can instruct pytest to clear all cache files and values
+by adding the ``--cache-clear`` option like this:
+
+.. code-block:: bash
+
+ pytest --cache-clear
+
+This is recommended for invocations from Continuous Integration
+servers where isolation and correctness is more important
+than speed.
+
+
+.. _cache stepwise:
+
+Stepwise
+--------
+
+As an alternative to ``--lf -x``, especially for cases where you expect a large part of the test suite will fail, ``--sw``, ``--stepwise`` allows you to fix them one at a time. The test suite will run until the first failure and then stop. At the next invocation, tests will continue from the last failing test and then run until the next failing test. You may use the ``--stepwise-skip`` option to ignore one failing test and stop the test execution on the second failing test instead. This is useful if you get stuck on a failing test and just want to ignore it until later. Providing ``--stepwise-skip`` will also enable ``--stepwise`` implicitly.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/capture-stdout-stderr.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/capture-stdout-stderr.rst
new file mode 100644
index 0000000000..9ccea719b6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/capture-stdout-stderr.rst
@@ -0,0 +1,170 @@
+
+.. _`captures`:
+
+How to capture stdout/stderr output
+=========================================================
+
+Default stdout/stderr/stdin capturing behaviour
+---------------------------------------------------------
+
+During test execution any output sent to ``stdout`` and ``stderr`` is
+captured. If a test or a setup method fails its according captured
+output will usually be shown along with the failure traceback. (this
+behavior can be configured by the ``--show-capture`` command-line option).
+
+In addition, ``stdin`` is set to a "null" object which will
+fail on attempts to read from it because it is rarely desired
+to wait for interactive input when running automated tests.
+
+By default capturing is done by intercepting writes to low level
+file descriptors. This allows to capture output from simple
+print statements as well as output from a subprocess started by
+a test.
+
+.. _capture-method:
+
+Setting capturing methods or disabling capturing
+-------------------------------------------------
+
+There are three ways in which ``pytest`` can perform capturing:
+
+* ``fd`` (file descriptor) level capturing (default): All writes going to the
+ operating system file descriptors 1 and 2 will be captured.
+
+* ``sys`` level capturing: Only writes to Python files ``sys.stdout``
+ and ``sys.stderr`` will be captured. No capturing of writes to
+ filedescriptors is performed.
+
+* ``tee-sys`` capturing: Python writes to ``sys.stdout`` and ``sys.stderr``
+ will be captured, however the writes will also be passed-through to
+ the actual ``sys.stdout`` and ``sys.stderr``. This allows output to be
+ 'live printed' and captured for plugin use, such as junitxml (new in pytest 5.4).
+
+.. _`disable capturing`:
+
+You can influence output capturing mechanisms from the command line:
+
+.. code-block:: bash
+
+ pytest -s # disable all capturing
+ pytest --capture=sys # replace sys.stdout/stderr with in-mem files
+ pytest --capture=fd # also point filedescriptors 1 and 2 to temp file
+ pytest --capture=tee-sys # combines 'sys' and '-s', capturing sys.stdout/stderr
+ # and passing it along to the actual sys.stdout/stderr
+
+.. _printdebugging:
+
+Using print statements for debugging
+---------------------------------------------------
+
+One primary benefit of the default capturing of stdout/stderr output
+is that you can use print statements for debugging:
+
+.. code-block:: python
+
+ # content of test_module.py
+
+
+ def setup_function(function):
+ print("setting up", function)
+
+
+ def test_func1():
+ assert True
+
+
+ def test_func2():
+ assert False
+
+and running this module will show you precisely the output
+of the failing function and hide the other one:
+
+.. code-block:: pytest
+
+ $ pytest
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 2 items
+
+ test_module.py .F [100%]
+
+ ================================= FAILURES =================================
+ ________________________________ test_func2 ________________________________
+
+ def test_func2():
+ > assert False
+ E assert False
+
+ test_module.py:12: AssertionError
+ -------------------------- Captured stdout setup ---------------------------
+ setting up <function test_func2 at 0xdeadbeef0001>
+ ========================= short test summary info ==========================
+ FAILED test_module.py::test_func2 - assert False
+ ======================= 1 failed, 1 passed in 0.12s ========================
+
+Accessing captured output from a test function
+---------------------------------------------------
+
+The ``capsys``, ``capsysbinary``, ``capfd``, and ``capfdbinary`` fixtures
+allow access to stdout/stderr output created during test execution. Here is
+an example test function that performs some output related checks:
+
+.. code-block:: python
+
+ def test_myoutput(capsys): # or use "capfd" for fd-level
+ print("hello")
+ sys.stderr.write("world\n")
+ captured = capsys.readouterr()
+ assert captured.out == "hello\n"
+ assert captured.err == "world\n"
+ print("next")
+ captured = capsys.readouterr()
+ assert captured.out == "next\n"
+
+The ``readouterr()`` call snapshots the output so far -
+and capturing will be continued. After the test
+function finishes the original streams will
+be restored. Using ``capsys`` this way frees your
+test from having to care about setting/resetting
+output streams and also interacts well with pytest's
+own per-test capturing.
+
+If you want to capture on filedescriptor level you can use
+the ``capfd`` fixture which offers the exact
+same interface but allows to also capture output from
+libraries or subprocesses that directly write to operating
+system level output streams (FD1 and FD2).
+
+
+
+The return value from ``readouterr`` changed to a ``namedtuple`` with two attributes, ``out`` and ``err``.
+
+
+
+If the code under test writes non-textual data, you can capture this using
+the ``capsysbinary`` fixture which instead returns ``bytes`` from
+the ``readouterr`` method.
+
+
+
+
+If the code under test writes non-textual data, you can capture this using
+the ``capfdbinary`` fixture which instead returns ``bytes`` from
+the ``readouterr`` method. The ``capfdbinary`` fixture operates on the
+filedescriptor level.
+
+
+
+
+To temporarily disable capture within a test, both ``capsys``
+and ``capfd`` have a ``disabled()`` method that can be used
+as a context manager, disabling capture inside the ``with`` block:
+
+.. code-block:: python
+
+ def test_disabling_capturing(capsys):
+ print("this output is captured")
+ with capsys.disabled():
+ print("output not captured, going directly to sys.stdout")
+ print("this output is also captured")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/capture-warnings.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/capture-warnings.rst
new file mode 100644
index 0000000000..065c11e610
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/capture-warnings.rst
@@ -0,0 +1,443 @@
+.. _`warnings`:
+
+How to capture warnings
+=======================
+
+
+
+Starting from version ``3.1``, pytest now automatically catches warnings during test execution
+and displays them at the end of the session:
+
+.. code-block:: python
+
+ # content of test_show_warnings.py
+ import warnings
+
+
+ def api_v1():
+ warnings.warn(UserWarning("api v1, should use functions from v2"))
+ return 1
+
+
+ def test_one():
+ assert api_v1() == 1
+
+Running pytest now produces this output:
+
+.. code-block:: pytest
+
+ $ pytest test_show_warnings.py
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 1 item
+
+ test_show_warnings.py . [100%]
+
+ ============================= warnings summary =============================
+ test_show_warnings.py::test_one
+ /home/sweet/project/test_show_warnings.py:5: UserWarning: api v1, should use functions from v2
+ warnings.warn(UserWarning("api v1, should use functions from v2"))
+
+ -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+ ======================= 1 passed, 1 warning in 0.12s =======================
+
+Controlling warnings
+--------------------
+
+Similar to Python's `warning filter`_ and :option:`-W option <python:-W>` flag, pytest provides
+its own ``-W`` flag to control which warnings are ignored, displayed, or turned into
+errors. See the `warning filter`_ documentation for more
+advanced use-cases.
+
+.. _`warning filter`: https://docs.python.org/3/library/warnings.html#warning-filter
+
+This code sample shows how to treat any ``UserWarning`` category class of warning
+as an error:
+
+.. code-block:: pytest
+
+ $ pytest -q test_show_warnings.py -W error::UserWarning
+ F [100%]
+ ================================= FAILURES =================================
+ _________________________________ test_one _________________________________
+
+ def test_one():
+ > assert api_v1() == 1
+
+ test_show_warnings.py:10:
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+
+ def api_v1():
+ > warnings.warn(UserWarning("api v1, should use functions from v2"))
+ E UserWarning: api v1, should use functions from v2
+
+ test_show_warnings.py:5: UserWarning
+ ========================= short test summary info ==========================
+ FAILED test_show_warnings.py::test_one - UserWarning: api v1, should use ...
+ 1 failed in 0.12s
+
+The same option can be set in the ``pytest.ini`` or ``pyproject.toml`` file using the
+``filterwarnings`` ini option. For example, the configuration below will ignore all
+user warnings and specific deprecation warnings matching a regex, but will transform
+all other warnings into errors.
+
+.. code-block:: ini
+
+ # pytest.ini
+ [pytest]
+ filterwarnings =
+ error
+ ignore::UserWarning
+ ignore:function ham\(\) is deprecated:DeprecationWarning
+
+.. code-block:: toml
+
+ # pyproject.toml
+ [tool.pytest.ini_options]
+ filterwarnings = [
+ "error",
+ "ignore::UserWarning",
+ # note the use of single quote below to denote "raw" strings in TOML
+ 'ignore:function ham\(\) is deprecated:DeprecationWarning',
+ ]
+
+
+When a warning matches more than one option in the list, the action for the last matching option
+is performed.
+
+
+.. _`filterwarnings`:
+
+``@pytest.mark.filterwarnings``
+-------------------------------
+
+
+
+You can use the ``@pytest.mark.filterwarnings`` to add warning filters to specific test items,
+allowing you to have finer control of which warnings should be captured at test, class or
+even module level:
+
+.. code-block:: python
+
+ import warnings
+
+
+ def api_v1():
+ warnings.warn(UserWarning("api v1, should use functions from v2"))
+ return 1
+
+
+ @pytest.mark.filterwarnings("ignore:api v1")
+ def test_one():
+ assert api_v1() == 1
+
+
+Filters applied using a mark take precedence over filters passed on the command line or configured
+by the ``filterwarnings`` ini option.
+
+You may apply a filter to all tests of a class by using the ``filterwarnings`` mark as a class
+decorator or to all tests in a module by setting the :globalvar:`pytestmark` variable:
+
+.. code-block:: python
+
+ # turns all warnings into errors for this module
+ pytestmark = pytest.mark.filterwarnings("error")
+
+
+
+*Credits go to Florian Schulze for the reference implementation in the* `pytest-warnings`_
+*plugin.*
+
+.. _`pytest-warnings`: https://github.com/fschulze/pytest-warnings
+
+Disabling warnings summary
+--------------------------
+
+Although not recommended, you can use the ``--disable-warnings`` command-line option to suppress the
+warning summary entirely from the test run output.
+
+Disabling warning capture entirely
+----------------------------------
+
+This plugin is enabled by default but can be disabled entirely in your ``pytest.ini`` file with:
+
+ .. code-block:: ini
+
+ [pytest]
+ addopts = -p no:warnings
+
+Or passing ``-p no:warnings`` in the command-line. This might be useful if your test suites handles warnings
+using an external system.
+
+
+.. _`deprecation-warnings`:
+
+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.
+
+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.
+
+For example:
+
+.. code-block:: ini
+
+ [pytest]
+ filterwarnings =
+ ignore:.*U.*mode is deprecated:DeprecationWarning
+
+
+This will ignore all warnings of type ``DeprecationWarning`` where the start of the message matches
+the regular expression ``".*U.*mode is deprecated"``.
+
+.. note::
+
+ If warnings are configured at the interpreter level, using
+ the :envvar:`python:PYTHONWARNINGS` environment variable or the
+ ``-W`` command-line option, pytest will not configure any filters by default.
+
+ Also pytest doesn't follow :pep:`506` suggestion of resetting all warning filters because
+ it might break test suites that configure warning filters themselves
+ by calling :func:`warnings.simplefilter` (see :issue:`2430` for an example of that).
+
+
+.. _`ensuring a function triggers a deprecation warning`:
+
+.. _ensuring_function_triggers:
+
+Ensuring code triggers a deprecation warning
+--------------------------------------------
+
+You can also use :func:`pytest.deprecated_call` for checking
+that a certain function call triggers a ``DeprecationWarning`` or
+``PendingDeprecationWarning``:
+
+.. code-block:: python
+
+ import pytest
+
+
+ def test_myfunction_deprecated():
+ with pytest.deprecated_call():
+ myfunction(17)
+
+This test will fail if ``myfunction`` does not issue a deprecation warning
+when called with a ``17`` argument.
+
+
+
+
+.. _`asserting warnings`:
+
+.. _assertwarnings:
+
+.. _`asserting warnings with the warns function`:
+
+.. _warns:
+
+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>`:
+
+.. code-block:: python
+
+ import warnings
+ import pytest
+
+
+ def test_warning():
+ 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::
+
+ >>> 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+$'):
+ ... warnings.warn("value must be 42", UserWarning)
+
+ >>> 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...
+
+You can also call :func:`pytest.warns` on a function or code string:
+
+.. code-block:: python
+
+ pytest.warns(expected_warning, func, *args, **kwargs)
+ pytest.warns(expected_warning, "func(*args, **kwargs)")
+
+The function also returns a list of all raised warnings (as
+``warnings.WarningMessage`` objects), which you can query for
+additional information:
+
+.. code-block:: python
+
+ with pytest.warns(RuntimeWarning) as record:
+ warnings.warn("another warning", RuntimeWarning)
+
+ # check that only one warning was raised
+ assert len(record) == 1
+ # check that the message matches
+ assert record[0].message.args[0] == "another warning"
+
+Alternatively, you can examine raised warnings in detail using the
+:ref:`recwarn <recwarn>` fixture (see below).
+
+
+The :ref:`recwarn <recwarn>` fixture automatically ensures to reset the warnings
+filter at the end of the test, so no global state is leaked.
+
+.. _`recording warnings`:
+
+.. _recwarn:
+
+Recording warnings
+------------------
+
+You can record raised warnings either using :func:`pytest.warns` or with
+the ``recwarn`` fixture.
+
+To record with :func:`pytest.warns` without asserting anything about the warnings,
+pass no arguments as the expected warning type and it will default to a generic Warning:
+
+.. code-block:: python
+
+ with pytest.warns() as record:
+ warnings.warn("user", UserWarning)
+ warnings.warn("runtime", RuntimeWarning)
+
+ assert len(record) == 2
+ assert str(record[0].message) == "user"
+ assert str(record[1].message) == "runtime"
+
+The ``recwarn`` fixture will record warnings for the whole function:
+
+.. code-block:: python
+
+ import warnings
+
+
+ def test_hello(recwarn):
+ warnings.warn("hello", UserWarning)
+ assert len(recwarn) == 1
+ w = recwarn.pop(UserWarning)
+ assert issubclass(w.category, UserWarning)
+ assert str(w.message) == "hello"
+ assert w.filename
+ assert w.lineno
+
+Both ``recwarn`` and :func:`pytest.warns` return the same interface for recorded
+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`:
+
+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:
+
+.. code-block:: python
+
+ with pytest.warns():
+ ...
+
+- To ensure that **no** warnings are emitted, use:
+
+.. code-block:: python
+
+ with warnings.catch_warnings():
+ warnings.simplefilter("error")
+ ...
+
+- To suppress warnings, use:
+
+.. code-block:: python
+
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ ...
+
+
+.. _custom_failure_messages:
+
+Custom failure messages
+-----------------------
+
+Recording warnings provides an opportunity to produce custom test
+failure messages for when no warnings are issued or other conditions
+are met.
+
+.. code-block:: python
+
+ def test():
+ with pytest.warns(Warning) as record:
+ f()
+ if not record:
+ pytest.fail("Expected a warning!")
+
+If no warnings are issued when calling ``f``, then ``not record`` will
+evaluate to ``True``. You can then call :func:`pytest.fail` with a
+custom error message.
+
+.. _internal-warnings:
+
+Internal pytest warnings
+------------------------
+
+pytest may generate its own warnings in some situations, such as improper usage or deprecated features.
+
+For example, pytest will emit a warning if it encounters a class that matches :confval:`python_classes` but also
+defines an ``__init__`` constructor, as this prevents the class from being instantiated:
+
+.. code-block:: python
+
+ # content of test_pytest_warnings.py
+ class Test:
+ def __init__(self):
+ pass
+
+ def test_foo(self):
+ assert 1 == 1
+
+.. code-block:: pytest
+
+ $ pytest test_pytest_warnings.py -q
+
+ ============================= warnings summary =============================
+ test_pytest_warnings.py:1
+ /home/sweet/project/test_pytest_warnings.py:1: PytestCollectionWarning: cannot collect test class 'Test' because it has a __init__ constructor (from: test_pytest_warnings.py)
+ class Test:
+
+ -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+ 1 warning in 0.12s
+
+These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings.
+
+Please read our :ref:`backwards-compatibility` to learn how we proceed about deprecating and eventually removing
+features.
+
+The full list of warnings is listed in :ref:`the reference documentation <warnings ref>`.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/doctest.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/doctest.rst
new file mode 100644
index 0000000000..ce0b5a5f64
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/doctest.rst
@@ -0,0 +1,312 @@
+.. _doctest:
+
+How to run doctests
+=========================================================
+
+By default, all files matching the ``test*.txt`` pattern will
+be run through the python standard :mod:`doctest` module. You
+can change the pattern by issuing:
+
+.. code-block:: bash
+
+ pytest --doctest-glob="*.rst"
+
+on the command line. ``--doctest-glob`` can be given multiple times in the command-line.
+
+If you then have a text file like this:
+
+.. code-block:: text
+
+ # content of test_example.txt
+
+ hello this is a doctest
+ >>> x = 3
+ >>> x
+ 3
+
+then you can just invoke ``pytest`` directly:
+
+.. code-block:: pytest
+
+ $ pytest
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 1 item
+
+ test_example.txt . [100%]
+
+ ============================ 1 passed in 0.12s =============================
+
+By default, pytest will collect ``test*.txt`` files looking for doctest directives, but you
+can pass additional globs using the ``--doctest-glob`` option (multi-allowed).
+
+In addition to text files, you can also execute doctests directly from docstrings of your classes
+and functions, including from test modules:
+
+.. code-block:: python
+
+ # content of mymodule.py
+ def something():
+ """a doctest in a docstring
+ >>> something()
+ 42
+ """
+ return 42
+
+.. code-block:: bash
+
+ $ pytest --doctest-modules
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 2 items
+
+ mymodule.py . [ 50%]
+ test_example.txt . [100%]
+
+ ============================ 2 passed in 0.12s =============================
+
+You can make these changes permanent in your project by
+putting them into a pytest.ini file like this:
+
+.. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ addopts = --doctest-modules
+
+
+Encoding
+--------
+
+The default encoding is **UTF-8**, but you can specify the encoding
+that will be used for those doctest files using the
+``doctest_encoding`` ini option:
+
+.. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ doctest_encoding = latin1
+
+.. _using doctest options:
+
+Using 'doctest' options
+-----------------------
+
+Python's standard :mod:`doctest` module provides some :ref:`options <python:option-flags-and-directives>`
+to configure the strictness of doctest tests. In pytest, you can enable those flags using the
+configuration file.
+
+For example, to make pytest ignore trailing whitespaces and ignore
+lengthy exception stack traces you can just write:
+
+.. code-block:: ini
+
+ [pytest]
+ doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
+
+Alternatively, options can be enabled by an inline comment in the doc test
+itself:
+
+.. code-block:: rst
+
+ >>> something_that_raises() # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ValueError: ...
+
+pytest also introduces new options:
+
+* ``ALLOW_UNICODE``: when enabled, the ``u`` prefix is stripped from unicode
+ strings in expected doctest output. This allows doctests to run in Python 2
+ and Python 3 unchanged.
+
+* ``ALLOW_BYTES``: similarly, the ``b`` prefix is stripped from byte strings
+ 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::
+
+ >>> math.pi
+ 3.14
+
+ If you wrote ``3.1416`` then the actual output would need to match to 4
+ decimal places; and so on.
+
+ This avoids false positives caused by limited floating-point precision, like
+ this::
+
+ Expected:
+ 0.233
+ Got:
+ 0.23300000000000001
+
+ ``NUMBER`` also supports lists of floating-point numbers -- in fact, it
+ matches floating-point numbers appearing anywhere in the output, even inside
+ a string! This means that it may not be appropriate to enable globally in
+ ``doctest_optionflags`` in your configuration file.
+
+ .. versionadded:: 5.1
+
+
+Continue on failure
+-------------------
+
+By default, pytest would report only the first failure for a given doctest. If
+you want to continue the test even when you have failures, do:
+
+.. code-block:: bash
+
+ pytest --doctest-modules --doctest-continue-on-failure
+
+
+Output format
+-------------
+
+You can change the diff output format on failure for your doctests
+by using one of standard doctest modules format in options
+(see :data:`python:doctest.REPORT_UDIFF`, :data:`python:doctest.REPORT_CDIFF`,
+:data:`python:doctest.REPORT_NDIFF`, :data:`python:doctest.REPORT_ONLY_FIRST_FAILURE`):
+
+.. code-block:: bash
+
+ pytest --doctest-modules --doctest-report none
+ pytest --doctest-modules --doctest-report udiff
+ pytest --doctest-modules --doctest-report cdiff
+ pytest --doctest-modules --doctest-report ndiff
+ pytest --doctest-modules --doctest-report only_first_failure
+
+
+pytest-specific features
+------------------------
+
+Some features are provided to make writing doctests easier or with better integration with
+your existing test suite. Keep in mind however that by using those features you will make
+your doctests incompatible with the standard ``doctests`` module.
+
+Using fixtures
+^^^^^^^^^^^^^^
+
+It is possible to use fixtures using the ``getfixture`` helper:
+
+.. code-block:: text
+
+ # content of example.rst
+ >>> tmp = getfixture('tmp_path')
+ >>> ...
+ >>>
+
+Note that the fixture needs to be defined in a place visible by pytest, for example, a `conftest.py`
+file or plugin; normal python files containing docstrings are not normally scanned for fixtures
+unless explicitly configured by :confval:`python_files`.
+
+Also, the :ref:`usefixtures <usefixtures>` mark and fixtures marked as :ref:`autouse <autouse>` are supported
+when executing text doctest files.
+
+
+.. _`doctest_namespace`:
+
+'doctest_namespace' fixture
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``doctest_namespace`` fixture can be used to inject items into the
+namespace in which your doctests run. It is intended to be used within
+your own fixtures to provide the tests that use them with context.
+
+``doctest_namespace`` is a standard ``dict`` object into which you
+place the objects you want to appear in the doctest namespace:
+
+.. code-block:: python
+
+ # content of conftest.py
+ import numpy
+
+
+ @pytest.fixture(autouse=True)
+ def add_np(doctest_namespace):
+ doctest_namespace["np"] = numpy
+
+which can then be used in your doctests directly:
+
+.. code-block:: python
+
+ # content of numpy.py
+ def arange():
+ """
+ >>> a = np.arange(10)
+ >>> 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.
+Fixtures will not be discovered in a sibling directory tree!
+
+Skipping tests
+^^^^^^^^^^^^^^
+
+For the same reasons one might want to skip normal tests, it is also possible to skip
+tests inside doctests.
+
+To skip a single check inside a doctest you can use the standard
+:data:`doctest.SKIP` directive:
+
+.. code-block:: python
+
+ def test_random(y):
+ """
+ >>> random.random() # doctest: +SKIP
+ 0.156231223
+
+ >>> 1 + 1
+ 2
+ """
+
+This will skip the first check, but not the second.
+
+pytest also allows using the standard pytest functions :func:`pytest.skip` and
+:func:`pytest.xfail` inside doctests, which might be useful because you can
+then skip/xfail tests based on external conditions:
+
+
+.. code-block:: text
+
+ >>> import sys, pytest
+ >>> if sys.platform.startswith('win'):
+ ... pytest.skip('this doctest does not work on Windows')
+ ...
+ >>> import fcntl
+ >>> ...
+
+However using those functions is discouraged because it reduces the readability of the
+docstring.
+
+.. note::
+
+ :func:`pytest.skip` and :func:`pytest.xfail` behave differently depending
+ if the doctests are in a Python file (in docstrings) or a text file containing
+ doctests intermingled with text:
+
+ * Python modules (docstrings): the functions only act in that specific docstring,
+ letting the other docstrings in the same module execute as normal.
+
+ * Text files: the functions will skip/xfail the checks for the rest of the entire
+ file.
+
+
+Alternatives
+------------
+
+While the built-in pytest support provides a good set of functionalities for using
+doctests, if you use them extensively you might be interested in those external packages
+which add many more features, and include pytest integration:
+
+* `pytest-doctestplus <https://github.com/astropy/pytest-doctestplus>`__: provides
+ advanced doctest support and enables the testing of reStructuredText (".rst") files.
+
+* `Sybil <https://sybil.readthedocs.io>`__: provides a way to test examples in
+ your documentation by parsing them from the documentation source and evaluating
+ the parsed examples as part of your normal test run.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/existingtestsuite.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/existingtestsuite.rst
new file mode 100644
index 0000000000..9909e7d113
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/existingtestsuite.rst
@@ -0,0 +1,34 @@
+.. _existingtestsuite:
+
+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.
+
+Before using this section you will want to :ref:`install pytest <getstarted>`.
+
+Running an existing test suite with pytest
+---------------------------------------------
+
+Say you want to contribute to an existing repository somewhere.
+After pulling the code into your development space using some
+flavor of version control and (optionally) setting up a virtualenv
+you will want to run:
+
+.. code-block:: bash
+
+ cd <repository>
+ pip install -e . # Environment dependent alternatives include
+ # 'python setup.py develop' and 'conda develop'
+
+in your project root. This will set up a symlink to your code in
+site-packages, allowing you to edit your code while your tests
+run against it as if it were installed.
+
+Setting up your project in development mode lets you avoid having to
+reinstall every time you want to run your tests, and is less brittle than
+mucking about with sys.path to point your tests at local code.
+
+Also consider using :ref:`tox <use tox>`.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/failures.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/failures.rst
new file mode 100644
index 0000000000..ef87550915
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/failures.rst
@@ -0,0 +1,160 @@
+.. _how-to-handle-failures:
+
+How to handle test failures
+=============================
+
+.. _maxfail:
+
+Stopping after the first (or N) failures
+---------------------------------------------------
+
+To stop the testing process after the first (N) failures:
+
+.. code-block:: bash
+
+ pytest -x # stop after first failure
+ pytest --maxfail=2 # stop after two failures
+
+
+.. _pdb-option:
+
+Using :doc:`python:library/pdb` with pytest
+-------------------------------------------
+
+Dropping to :doc:`pdb <python:library/pdb>` on failures
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Python comes with a builtin Python debugger called :doc:`pdb <python:library/pdb>`. ``pytest``
+allows one to drop into the :doc:`pdb <python:library/pdb>` prompt via a command line option:
+
+.. code-block:: bash
+
+ pytest --pdb
+
+This will invoke the Python debugger on every failure (or KeyboardInterrupt).
+Often you might only want to do this for the first failing test to understand
+a certain failure situation:
+
+.. code-block:: bash
+
+ pytest -x --pdb # drop to PDB on first failure, then end test session
+ pytest --pdb --maxfail=3 # drop to PDB for first three failures
+
+Note that on any failure the exception information is stored on
+``sys.last_value``, ``sys.last_type`` and ``sys.last_traceback``. In
+interactive use, this allows one to drop into postmortem debugging with
+any debug tool. One can also manually access the exception information,
+for example::
+
+ >>> import sys
+ >>> sys.last_traceback.tb_lineno
+ 42
+ >>> sys.last_value
+ AssertionError('assert result == "ok"',)
+
+
+.. _trace-option:
+
+Dropping to :doc:`pdb <python:library/pdb>` at the start of a test
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``pytest`` allows one to drop into the :doc:`pdb <python:library/pdb>` prompt immediately at the start of each test via a command line option:
+
+.. code-block:: bash
+
+ pytest --trace
+
+This will invoke the Python debugger at the start of every test.
+
+.. _breakpoints:
+
+Setting breakpoints
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded: 2.4.0
+
+To set a breakpoint in your code use the native Python ``import pdb;pdb.set_trace()`` call
+in your code and pytest automatically disables its output capture for that test:
+
+* Output capture in other tests is not affected.
+* Any prior test output that has already been captured and will be processed as
+ such.
+* Output capture gets resumed when ending the debugger session (via the
+ ``continue`` command).
+
+
+.. _`breakpoint-builtin`:
+
+Using the builtin breakpoint function
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Python 3.7 introduces a builtin ``breakpoint()`` function.
+Pytest supports the use of ``breakpoint()`` with the following behaviours:
+
+ - When ``breakpoint()`` is called and ``PYTHONBREAKPOINT`` is set to the default value, pytest will use the custom internal PDB trace UI instead of the system default ``Pdb``.
+ - When tests are complete, the system will default back to the system ``Pdb`` trace UI.
+ - With ``--pdb`` passed to pytest, the custom internal Pdb trace UI is used with both ``breakpoint()`` and failed tests/unhandled exceptions.
+ - ``--pdbcls`` can be used to specify a custom debugger class.
+
+
+.. _faulthandler:
+
+Fault Handler
+-------------
+
+.. versionadded:: 5.0
+
+The :mod:`faulthandler` standard module
+can be used to dump Python tracebacks on a segfault or after a timeout.
+
+The module is automatically enabled for pytest runs, unless the ``-p no:faulthandler`` is given
+on the command-line.
+
+Also the :confval:`faulthandler_timeout=X<faulthandler_timeout>` configuration option can be used
+to dump the traceback of all threads if a test takes longer than ``X``
+seconds to finish (not available on Windows).
+
+.. note::
+
+ This functionality has been integrated from the external
+ `pytest-faulthandler <https://github.com/pytest-dev/pytest-faulthandler>`__ plugin, with two
+ small differences:
+
+ * To disable it, use ``-p no:faulthandler`` instead of ``--no-faulthandler``: the former
+ can be used with any plugin, so it saves one option.
+
+ * The ``--faulthandler-timeout`` command-line option has become the
+ :confval:`faulthandler_timeout` configuration option. It can still be configured from
+ the command-line using ``-o faulthandler_timeout=X``.
+
+
+.. _unraisable:
+
+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.
+
+Unhandled thread exceptions are exceptions raised in a :class:`~threading.Thread`
+but not handled, causing the thread to terminate uncleanly.
+
+Both types of exceptions are normally considered bugs, but may go unnoticed
+because they don't cause the program itself to crash. Pytest detects these
+conditions and issues a warning that is visible in the test run summary.
+
+The plugins are automatically enabled for pytest runs, unless the
+``-p no:unraisableexception`` (for unraisable exceptions) and
+``-p no:threadexception`` (for thread exceptions) options are given on the
+command-line.
+
+The warnings may be silenced selectively using the :ref:`pytest.mark.filterwarnings ref`
+mark. The warning categories are :class:`pytest.PytestUnraisableExceptionWarning` and
+:class:`pytest.PytestUnhandledThreadExceptionWarning`.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/fixtures.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/fixtures.rst
new file mode 100644
index 0000000000..0801387745
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/fixtures.rst
@@ -0,0 +1,1887 @@
+.. _how-to-fixtures:
+
+How to use fixtures
+====================
+
+.. seealso:: :ref:`about-fixtures`
+.. seealso:: :ref:`Fixtures reference <reference-fixtures>`
+
+
+"Requesting" fixtures
+---------------------
+
+At a basic level, test functions request fixtures they require by declaring
+them as arguments.
+
+When pytest goes to run a test, it looks at the parameters in that test
+function's signature, and then searches for fixtures that have the same names as
+those parameters. Once pytest finds them, it runs those fixtures, captures what
+they returned (if anything), and passes those objects into the test function as
+arguments.
+
+
+Quick example
+^^^^^^^^^^^^^
+
+.. code-block:: python
+
+ import pytest
+
+
+ class Fruit:
+ def __init__(self, name):
+ self.name = name
+ self.cubed = False
+
+ def cube(self):
+ self.cubed = True
+
+
+ class FruitSalad:
+ def __init__(self, *fruit_bowl):
+ self.fruit = fruit_bowl
+ self._cube_fruit()
+
+ def _cube_fruit(self):
+ for fruit in self.fruit:
+ fruit.cube()
+
+
+ # Arrange
+ @pytest.fixture
+ def fruit_bowl():
+ return [Fruit("apple"), Fruit("banana")]
+
+
+ def test_fruit_salad(fruit_bowl):
+ # Act
+ fruit_salad = FruitSalad(*fruit_bowl)
+
+ # Assert
+ assert all(fruit.cubed for fruit in fruit_salad.fruit)
+
+In this example, ``test_fruit_salad`` "**requests**" ``fruit_bowl`` (i.e.
+``def test_fruit_salad(fruit_bowl):``), and when pytest sees this, it will
+execute the ``fruit_bowl`` fixture function and pass the object it returns into
+``test_fruit_salad`` as the ``fruit_bowl`` argument.
+
+Here's roughly
+what's happening if we were to do it by hand:
+
+.. code-block:: python
+
+ def fruit_bowl():
+ return [Fruit("apple"), Fruit("banana")]
+
+
+ def test_fruit_salad(fruit_bowl):
+ # Act
+ fruit_salad = FruitSalad(*fruit_bowl)
+
+ # Assert
+ assert all(fruit.cubed for fruit in fruit_salad.fruit)
+
+
+ # Arrange
+ bowl = fruit_bowl()
+ test_fruit_salad(fruit_bowl=bowl)
+
+
+Fixtures can **request** other fixtures
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+One of pytest's greatest strengths is its extremely flexible fixture system. It
+allows us to boil down complex requirements for tests into more simple and
+organized functions, where we only need to have each one describe the things
+they are dependent on. We'll get more into this further down, but for now,
+here's a quick example to demonstrate how fixtures can use other fixtures:
+
+.. code-block:: python
+
+ # contents of test_append.py
+ import pytest
+
+
+ # Arrange
+ @pytest.fixture
+ def first_entry():
+ return "a"
+
+
+ # Arrange
+ @pytest.fixture
+ def order(first_entry):
+ return [first_entry]
+
+
+ def test_string(order):
+ # Act
+ order.append("b")
+
+ # Assert
+ assert order == ["a", "b"]
+
+
+Notice that this is the same example from above, but very little changed. The
+fixtures in pytest **request** fixtures just like tests. All the same
+**requesting** rules apply to fixtures that do for tests. Here's how this
+example would work if we did it by hand:
+
+.. code-block:: python
+
+ def first_entry():
+ return "a"
+
+
+ def order(first_entry):
+ return [first_entry]
+
+
+ def test_string(order):
+ # Act
+ order.append("b")
+
+ # Assert
+ assert order == ["a", "b"]
+
+
+ entry = first_entry()
+ the_list = order(first_entry=entry)
+ test_string(order=the_list)
+
+Fixtures are reusable
+^^^^^^^^^^^^^^^^^^^^^
+
+One of the things that makes pytest's fixture system so powerful, is that it
+gives us the ability to define a generic setup step that can be reused over and
+over, just like a normal function would be used. Two different tests can request
+the same fixture and have pytest give each test their own result from that
+fixture.
+
+This is extremely useful for making sure tests aren't affected by each other. We
+can use this system to make sure each test gets its own fresh batch of data and
+is starting from a clean state so it can provide consistent, repeatable results.
+
+Here's an example of how this can come in handy:
+
+.. code-block:: python
+
+ # contents of test_append.py
+ import pytest
+
+
+ # Arrange
+ @pytest.fixture
+ def first_entry():
+ return "a"
+
+
+ # Arrange
+ @pytest.fixture
+ def order(first_entry):
+ return [first_entry]
+
+
+ def test_string(order):
+ # Act
+ order.append("b")
+
+ # Assert
+ assert order == ["a", "b"]
+
+
+ def test_int(order):
+ # Act
+ order.append(2)
+
+ # Assert
+ assert order == ["a", 2]
+
+
+Each test here is being given its own copy of that ``list`` object,
+which means the ``order`` fixture is getting executed twice (the same
+is true for the ``first_entry`` fixture). If we were to do this by hand as
+well, it would look something like this:
+
+.. code-block:: python
+
+ def first_entry():
+ return "a"
+
+
+ def order(first_entry):
+ return [first_entry]
+
+
+ def test_string(order):
+ # Act
+ order.append("b")
+
+ # Assert
+ assert order == ["a", "b"]
+
+
+ def test_int(order):
+ # Act
+ order.append(2)
+
+ # Assert
+ assert order == ["a", 2]
+
+
+ entry = first_entry()
+ the_list = order(first_entry=entry)
+ test_string(order=the_list)
+
+ entry = first_entry()
+ the_list = order(first_entry=entry)
+ test_int(order=the_list)
+
+A test/fixture can **request** more than one fixture at a time
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Tests and fixtures aren't limited to **requesting** a single fixture at a time.
+They can request as many as they like. Here's another quick example to
+demonstrate:
+
+.. code-block:: python
+
+ # contents of test_append.py
+ import pytest
+
+
+ # Arrange
+ @pytest.fixture
+ def first_entry():
+ return "a"
+
+
+ # Arrange
+ @pytest.fixture
+ def second_entry():
+ return 2
+
+
+ # Arrange
+ @pytest.fixture
+ def order(first_entry, second_entry):
+ return [first_entry, second_entry]
+
+
+ # Arrange
+ @pytest.fixture
+ def expected_list():
+ return ["a", 2, 3.0]
+
+
+ def test_string(order, expected_list):
+ # Act
+ order.append(3.0)
+
+ # Assert
+ assert order == expected_list
+
+Fixtures can be **requested** more than once per test (return values are cached)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Fixtures can also be **requested** more than once during the same test, and
+pytest won't execute them again for that test. This means we can **request**
+fixtures in multiple fixtures that are dependent on them (and even again in the
+test itself) without those fixtures being executed more than once.
+
+.. code-block:: python
+
+ # contents of test_append.py
+ import pytest
+
+
+ # Arrange
+ @pytest.fixture
+ def first_entry():
+ return "a"
+
+
+ # Arrange
+ @pytest.fixture
+ def order():
+ return []
+
+
+ # Act
+ @pytest.fixture
+ def append_first(order, first_entry):
+ return order.append(first_entry)
+
+
+ def test_string_only(append_first, order, first_entry):
+ # Assert
+ assert order == [first_entry]
+
+If a **requested** fixture was executed once for every time it was **requested**
+during a test, then this test would fail because both ``append_first`` and
+``test_string_only`` would see ``order`` as an empty list (i.e. ``[]``), but
+since the return value of ``order`` was cached (along with any side effects
+executing it may have had) after the first time it was called, both the test and
+``append_first`` were referencing the same object, and the test saw the effect
+``append_first`` had on that object.
+
+.. _`autouse`:
+.. _`autouse fixtures`:
+
+Autouse fixtures (fixtures you don't have to request)
+-----------------------------------------------------
+
+Sometimes you may want to have a fixture (or even several) that you know all
+your tests will depend on. "Autouse" fixtures are a convenient way to make all
+tests automatically **request** them. This can cut out a
+lot of redundant **requests**, and can even provide more advanced fixture usage
+(more on that further down).
+
+We can make a fixture an autouse fixture by passing in ``autouse=True`` to the
+fixture's decorator. Here's a simple example for how they can be used:
+
+.. code-block:: python
+
+ # contents of test_append.py
+ import pytest
+
+
+ @pytest.fixture
+ def first_entry():
+ return "a"
+
+
+ @pytest.fixture
+ def order(first_entry):
+ return []
+
+
+ @pytest.fixture(autouse=True)
+ def append_first(order, first_entry):
+ return order.append(first_entry)
+
+
+ def test_string_only(order, first_entry):
+ assert order == [first_entry]
+
+
+ def test_string_and_int(order, first_entry):
+ order.append(2)
+ assert order == [first_entry, 2]
+
+In this example, the ``append_first`` fixture is an autouse fixture. Because it
+happens automatically, both tests are affected by it, even though neither test
+**requested** it. That doesn't mean they *can't* be **requested** though; just
+that it isn't *necessary*.
+
+.. _smtpshared:
+
+Scope: sharing fixtures across classes, modules, packages or session
+--------------------------------------------------------------------
+
+.. regendoc:wipe
+
+Fixtures requiring network access depend on connectivity and are
+usually time-expensive to create. Extending the previous example, we
+can add a ``scope="module"`` parameter to the
+:py:func:`@pytest.fixture <pytest.fixture>` invocation
+to cause a ``smtp_connection`` fixture function, responsible to create a connection to a preexisting SMTP server, to only be invoked
+once per test *module* (the default is to invoke once per test *function*).
+Multiple test functions in a test module will thus
+each receive the same ``smtp_connection`` fixture instance, thus saving time.
+Possible values for ``scope`` are: ``function``, ``class``, ``module``, ``package`` or ``session``.
+
+The next example puts the fixture function into a separate ``conftest.py`` file
+so that tests from multiple test modules in the directory can
+access the fixture function:
+
+.. code-block:: python
+
+ # content of conftest.py
+ import pytest
+ import smtplib
+
+
+ @pytest.fixture(scope="module")
+ def smtp_connection():
+ return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
+
+
+.. code-block:: python
+
+ # content of test_module.py
+
+
+ def test_ehlo(smtp_connection):
+ response, msg = smtp_connection.ehlo()
+ assert response == 250
+ assert b"smtp.gmail.com" in msg
+ assert 0 # for demo purposes
+
+
+ def test_noop(smtp_connection):
+ response, msg = smtp_connection.noop()
+ assert response == 250
+ assert 0 # for demo purposes
+
+Here, the ``test_ehlo`` needs the ``smtp_connection`` fixture value. pytest
+will discover and call the :py:func:`@pytest.fixture <pytest.fixture>`
+marked ``smtp_connection`` fixture function. Running the test looks like this:
+
+.. code-block:: pytest
+
+ $ pytest test_module.py
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 2 items
+
+ test_module.py FF [100%]
+
+ ================================= FAILURES =================================
+ ________________________________ test_ehlo _________________________________
+
+ smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001>
+
+ def test_ehlo(smtp_connection):
+ response, msg = smtp_connection.ehlo()
+ assert response == 250
+ assert b"smtp.gmail.com" in msg
+ > assert 0 # for demo purposes
+ E assert 0
+
+ test_module.py:7: AssertionError
+ ________________________________ test_noop _________________________________
+
+ smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001>
+
+ def test_noop(smtp_connection):
+ response, msg = smtp_connection.noop()
+ assert response == 250
+ > assert 0 # for demo purposes
+ E assert 0
+
+ test_module.py:13: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_module.py::test_ehlo - assert 0
+ FAILED test_module.py::test_noop - assert 0
+ ============================ 2 failed in 0.12s =============================
+
+You see the two ``assert 0`` failing and more importantly you can also see
+that the **exactly same** ``smtp_connection`` object was passed into the
+two test functions because pytest shows the incoming argument values in the
+traceback. As a result, the two test functions using ``smtp_connection`` run
+as quick as a single one because they reuse the same instance.
+
+If you decide that you rather want to have a session-scoped ``smtp_connection``
+instance, you can simply declare it:
+
+.. code-block:: python
+
+ @pytest.fixture(scope="session")
+ def smtp_connection():
+ # the returned fixture value will be shared for
+ # all tests requesting it
+ ...
+
+
+Fixture scopes
+^^^^^^^^^^^^^^
+
+Fixtures are created when first requested by a test, and are destroyed based on their ``scope``:
+
+* ``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.
+* ``session``: the fixture is destroyed at the end of the test session.
+
+.. note::
+
+ Pytest only caches one instance of a fixture at a time, which
+ means that when using a parametrized fixture, pytest may invoke a fixture more than once in
+ the given scope.
+
+.. _dynamic scope:
+
+Dynamic scope
+^^^^^^^^^^^^^
+
+.. versionadded:: 5.2
+
+In some cases, you might want to change the scope of the fixture without changing the code.
+To do that, pass a callable to ``scope``. The callable must return a string with a valid scope
+and will be executed only once - during the fixture definition. It will be called with two
+keyword arguments - ``fixture_name`` as a string and ``config`` with a configuration object.
+
+This can be especially useful when dealing with fixtures that need time for setup, like spawning
+a docker container. You can use the command-line argument to control the scope of the spawned
+containers for different environments. See the example below.
+
+.. code-block:: python
+
+ def determine_scope(fixture_name, config):
+ if config.getoption("--keep-containers", None):
+ return "session"
+ return "function"
+
+
+ @pytest.fixture(scope=determine_scope)
+ def docker_container():
+ yield spawn_container()
+
+
+
+.. _`finalization`:
+
+Teardown/Cleanup (AKA Fixture finalization)
+-------------------------------------------
+
+When we run our tests, we'll want to make sure they clean up after themselves so
+they don't mess with any other tests (and also so that we don't leave behind a
+mountain of test data to bloat the system). Fixtures in pytest offer a very
+useful teardown system, which allows us to define the specific steps necessary
+for each fixture to clean up after itself.
+
+This system can be leveraged in two ways.
+
+.. _`yield fixtures`:
+
+1. ``yield`` fixtures (recommended)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. regendoc: wipe
+
+"Yield" fixtures ``yield`` instead of ``return``. With these
+fixtures, we can run some code and pass an object back to the requesting
+fixture/test, just like with the other fixtures. The only differences are:
+
+1. ``return`` is swapped out for ``yield``.
+2. Any teardown code for that fixture is placed *after* the ``yield``.
+
+Once pytest figures out a linear order for the fixtures, it will run each one up
+until it returns or yields, and then move on to the next fixture in the list to
+do the same thing.
+
+Once the test is finished, pytest will go back down the list of fixtures, but in
+the *reverse order*, taking each one that yielded, and running the code inside
+it that was *after* the ``yield`` statement.
+
+As a simple example, consider this basic email module:
+
+.. code-block:: python
+
+ # content of emaillib.py
+ class MailAdminClient:
+ def create_user(self):
+ return MailUser()
+
+ def delete_user(self, user):
+ # do some cleanup
+ pass
+
+
+ class MailUser:
+ def __init__(self):
+ self.inbox = []
+
+ def send_email(self, email, other):
+ other.inbox.append(email)
+
+ def clear_mailbox(self):
+ self.inbox.clear()
+
+
+ class Email:
+ def __init__(self, subject, body):
+ self.subject = subject
+ self.body = body
+
+Let's say we want to test sending email from one user to another. We'll have to
+first make each user, then send the email from one user to the other, and
+finally assert that the other user received that message in their inbox. If we
+want to clean up after the test runs, we'll likely have to make sure the other
+user's mailbox is emptied before deleting that user, otherwise the system may
+complain.
+
+Here's what that might look like:
+
+.. code-block:: python
+
+ # content of test_emaillib.py
+ import pytest
+
+ from emaillib import Email, MailAdminClient
+
+
+ @pytest.fixture
+ def mail_admin():
+ return MailAdminClient()
+
+
+ @pytest.fixture
+ def sending_user(mail_admin):
+ user = mail_admin.create_user()
+ yield user
+ mail_admin.delete_user(user)
+
+
+ @pytest.fixture
+ def receiving_user(mail_admin):
+ user = mail_admin.create_user()
+ yield user
+ mail_admin.delete_user(user)
+
+
+ def test_email_received(sending_user, receiving_user):
+ email = Email(subject="Hey!", body="How's it going?")
+ sending_user.send_email(email, receiving_user)
+ assert email in receiving_user.inbox
+
+Because ``receiving_user`` is the last fixture to run during setup, it's the first to run
+during teardown.
+
+There is a risk that even having the order right on the teardown side of things
+doesn't guarantee a safe cleanup. That's covered in a bit more detail in
+:ref:`safe teardowns`.
+
+.. code-block:: pytest
+
+ $ pytest -q test_emaillib.py
+ . [100%]
+ 1 passed in 0.12s
+
+Handling errors for yield fixture
+"""""""""""""""""""""""""""""""""
+
+If a yield fixture raises an exception before yielding, pytest won't try to run
+the teardown code after that yield fixture's ``yield`` statement. But, for every
+fixture that has already run successfully for that test, pytest will still
+attempt to tear them down as it normally would.
+
+2. Adding finalizers directly
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+While yield fixtures are considered to be the cleaner and more straightforward
+option, there is another choice, and that is to add "finalizer" functions
+directly to the test's `request-context`_ object. It brings a similar result as
+yield fixtures, but requires a bit more verbosity.
+
+In order to use this approach, we have to request the `request-context`_ object
+(just like we would request another fixture) in the fixture we need to add
+teardown code for, and then pass a callable, containing that teardown code, to
+its ``addfinalizer`` method.
+
+We have to be careful though, because pytest will run that finalizer once it's
+been added, even if that fixture raises an exception after adding the finalizer.
+So to make sure we don't run the finalizer code when we wouldn't need to, we
+would only add the finalizer once the fixture would have done something that
+we'd need to teardown.
+
+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
+
+
+ @pytest.fixture
+ def mail_admin():
+ return MailAdminClient()
+
+
+ @pytest.fixture
+ def sending_user(mail_admin):
+ user = mail_admin.create_user()
+ yield user
+ mail_admin.delete_user(user)
+
+
+ @pytest.fixture
+ def receiving_user(mail_admin, request):
+ user = mail_admin.create_user()
+
+ def delete_user():
+ mail_admin.delete_user(user)
+
+ request.addfinalizer(delete_user)
+ return user
+
+
+ @pytest.fixture
+ def email(sending_user, receiving_user, request):
+ _email = Email(subject="Hey!", body="How's it going?")
+ sending_user.send_email(_email, receiving_user)
+
+ def empty_mailbox():
+ receiving_user.clear_mailbox()
+
+ request.addfinalizer(empty_mailbox)
+ return _email
+
+
+ def test_email_received(receiving_user, email):
+ assert email in receiving_user.inbox
+
+
+It's a bit longer than yield fixtures and a bit more complex, but it
+does offer some nuances for when you're in a pinch.
+
+.. code-block:: pytest
+
+ $ pytest -q test_emaillib.py
+ . [100%]
+ 1 passed in 0.12s
+
+.. _`safe teardowns`:
+
+Safe teardowns
+--------------
+
+The fixture system of pytest is *very* powerful, but it's still being run by a
+computer, so it isn't able to figure out how to safely teardown everything we
+throw at it. If we aren't careful, an error in the wrong spot might leave stuff
+from our tests behind, and that can cause further issues pretty quickly.
+
+For example, consider the following tests (based off of the mail example from
+above):
+
+.. code-block:: python
+
+ # content of test_emaillib.py
+ import pytest
+
+ from emaillib import Email, MailAdminClient
+
+
+ @pytest.fixture
+ def setup():
+ mail_admin = MailAdminClient()
+ sending_user = mail_admin.create_user()
+ receiving_user = mail_admin.create_user()
+ email = Email(subject="Hey!", body="How's it going?")
+ sending_user.send_email(email, receiving_user)
+ yield receiving_user, email
+ receiving_user.clear_mailbox()
+ mail_admin.delete_user(sending_user)
+ mail_admin.delete_user(receiving_user)
+
+
+ def test_email_received(setup):
+ receiving_user, email = setup
+ assert email in receiving_user.inbox
+
+This version is a lot more compact, but it's also harder to read, doesn't have a
+very descriptive fixture name, and none of the fixtures can be reused easily.
+
+There's also a more serious issue, which is that if any of those steps in the
+setup raise an exception, none of the teardown code will run.
+
+One option might be to go with the ``addfinalizer`` method instead of yield
+fixtures, but that might get pretty complex and difficult to maintain (and it
+wouldn't be compact anymore).
+
+.. code-block:: pytest
+
+ $ pytest -q test_emaillib.py
+ . [100%]
+ 1 passed in 0.12s
+
+.. _`safe fixture structure`:
+
+Safe fixture structure
+^^^^^^^^^^^^^^^^^^^^^^
+
+The safest and simplest fixture structure requires limiting fixtures to only
+making one state-changing action each, and then bundling them together with
+their teardown code, as :ref:`the email examples above <yield fixtures>` showed.
+
+The chance that a state-changing operation can fail but still modify state is
+negligible, as most of these operations tend to be `transaction
+<https://en.wikipedia.org/wiki/Transaction_processing>`_-based (at least at the
+level of testing where state could be left behind). So if we make sure that any
+successful state-changing action gets torn down by moving it to a separate
+fixture function and separating it from other, potentially failing
+state-changing actions, then our tests will stand the best chance at leaving
+the test environment the way they found it.
+
+For an example, let's say we have a website with a login page, and we have
+access to an admin API where we can generate users. For our test, we want to:
+
+1. Create a user through that admin API
+2. Launch a browser using Selenium
+3. Go to the login page of our site
+4. Log in as the user we created
+5. Assert that their name is in the header of the landing page
+
+We wouldn't want to leave that user in the system, nor would we want to leave
+that browser session running, so we'll want to make sure the fixtures that
+create those things clean up after themselves.
+
+Here's what that might look like:
+
+.. note::
+
+ For this example, certain fixtures (i.e. ``base_url`` and
+ ``admin_credentials``) are implied to exist elsewhere. So for now, let's
+ assume they exist, and we're just not looking at them.
+
+.. code-block:: python
+
+ from uuid import uuid4
+ from urllib.parse import urljoin
+
+ from selenium.webdriver import Chrome
+ import pytest
+
+ from src.utils.pages import LoginPage, LandingPage
+ from src.utils import AdminApiClient
+ from src.utils.data_types import User
+
+
+ @pytest.fixture
+ def admin_client(base_url, admin_credentials):
+ return AdminApiClient(base_url, **admin_credentials)
+
+
+ @pytest.fixture
+ def user(admin_client):
+ _user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word")
+ admin_client.create_user(_user)
+ yield _user
+ admin_client.delete_user(_user)
+
+
+ @pytest.fixture
+ def driver():
+ _driver = Chrome()
+ yield _driver
+ _driver.quit()
+
+
+ @pytest.fixture
+ def login(driver, base_url, user):
+ driver.get(urljoin(base_url, "/login"))
+ page = LoginPage(driver)
+ page.login(user)
+
+
+ @pytest.fixture
+ def landing_page(driver, login):
+ return LandingPage(driver)
+
+
+ def test_name_on_landing_page_after_login(landing_page, user):
+ assert landing_page.header == f"Welcome, {user.name}!"
+
+The way the dependencies are laid out means it's unclear if the ``user``
+fixture would execute before the ``driver`` fixture. But that's ok, because
+those are atomic operations, and so it doesn't matter which one runs first
+because the sequence of events for the test is still `linearizable
+<https://en.wikipedia.org/wiki/Linearizability>`_. But what *does* matter is
+that, no matter which one runs first, if the one raises an exception while the
+other would not have, neither will have left anything behind. If ``driver``
+executes before ``user``, and ``user`` raises an exception, the driver will
+still quit, and the user was never made. And if ``driver`` was the one to raise
+the exception, then the driver would never have been started and the user would
+never have been made.
+
+.. note:
+
+ While the ``user`` fixture doesn't *actually* need to happen before the
+ ``driver`` fixture, if we made ``driver`` request ``user``, it might save
+ some time in the event that making the user raises an exception, since it
+ won't bother trying to start the driver, which is a fairly expensive
+ operation.
+
+
+Running multiple ``assert`` statements safely
+---------------------------------------------
+
+Sometimes you may want to run multiple asserts after doing all that setup, which
+makes sense as, in more complex systems, a single action can kick off multiple
+behaviors. pytest has a convenient way of handling this and it combines a bunch
+of what we've gone over so far.
+
+All that's needed is stepping up to a larger scope, then having the **act**
+step defined as an autouse fixture, and finally, making sure all the fixtures
+are targeting that higher level scope.
+
+Let's pull :ref:`an example from above <safe fixture structure>`, and tweak it a
+bit. Let's say that in addition to checking for a welcome message in the header,
+we also want to check for a sign out button, and a link to the user's profile.
+
+Let's take a look at how we can structure that so we can run multiple asserts
+without having to repeat all those steps again.
+
+.. note::
+
+ For this example, certain fixtures (i.e. ``base_url`` and
+ ``admin_credentials``) are implied to exist elsewhere. So for now, let's
+ assume they exist, and we're just not looking at them.
+
+.. code-block:: python
+
+ # contents of tests/end_to_end/test_login.py
+ from uuid import uuid4
+ from urllib.parse import urljoin
+
+ from selenium.webdriver import Chrome
+ import pytest
+
+ from src.utils.pages import LoginPage, LandingPage
+ from src.utils import AdminApiClient
+ from src.utils.data_types import User
+
+
+ @pytest.fixture(scope="class")
+ def admin_client(base_url, admin_credentials):
+ return AdminApiClient(base_url, **admin_credentials)
+
+
+ @pytest.fixture(scope="class")
+ def user(admin_client):
+ _user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word")
+ admin_client.create_user(_user)
+ yield _user
+ admin_client.delete_user(_user)
+
+
+ @pytest.fixture(scope="class")
+ def driver():
+ _driver = Chrome()
+ yield _driver
+ _driver.quit()
+
+
+ @pytest.fixture(scope="class")
+ def landing_page(driver, login):
+ return LandingPage(driver)
+
+
+ class TestLandingPageSuccess:
+ @pytest.fixture(scope="class", autouse=True)
+ def login(self, driver, base_url, user):
+ driver.get(urljoin(base_url, "/login"))
+ page = LoginPage(driver)
+ page.login(user)
+
+ def test_name_in_header(self, landing_page, user):
+ assert landing_page.header == f"Welcome, {user.name}!"
+
+ def test_sign_out_button(self, landing_page):
+ assert landing_page.sign_out_button.is_displayed()
+
+ def test_profile_link(self, landing_page, user):
+ profile_href = urljoin(base_url, f"/profile?id={user.profile_id}")
+ assert landing_page.profile_link.get_attribute("href") == profile_href
+
+Notice that the methods are only referencing ``self`` in the signature as a
+formality. No state is tied to the actual test class as it might be in the
+``unittest.TestCase`` framework. Everything is managed by the pytest fixture
+system.
+
+Each method only has to request the fixtures that it actually needs without
+worrying about order. This is because the **act** fixture is an autouse fixture,
+and it made sure all the other fixtures executed before it. There's no more
+changes of state that need to take place, so the tests are free to make as many
+non-state-changing queries as they want without risking stepping on the toes of
+the other tests.
+
+The ``login`` fixture is defined inside the class as well, because not every one
+of the other tests in the module will be expecting a successful login, and the **act** may need to
+be handled a little differently for another test class. For example, if we
+wanted to write another test scenario around submitting bad credentials, we
+could handle it by adding something like this to the test file:
+
+.. note:
+
+ It's assumed that the page object for this (i.e. ``LoginPage``) raises a
+ custom exception, ``BadCredentialsException``, when it recognizes text
+ signifying that on the login form after attempting to log in.
+
+.. code-block:: python
+
+ class TestLandingPageBadCredentials:
+ @pytest.fixture(scope="class")
+ def faux_user(self, user):
+ _user = deepcopy(user)
+ _user.password = "badpass"
+ return _user
+
+ def test_raises_bad_credentials_exception(self, login_page, faux_user):
+ with pytest.raises(BadCredentialsException):
+ login_page.login(faux_user)
+
+
+.. _`request-context`:
+
+Fixtures can introspect the requesting test context
+-------------------------------------------------------------
+
+Fixture functions can accept the :py:class:`request <_pytest.fixtures.FixtureRequest>` object
+to introspect the "requesting" test function, class or module context.
+Further extending the previous ``smtp_connection`` fixture example, let's
+read an optional server URL from the test module which uses our fixture:
+
+.. code-block:: python
+
+ # content of conftest.py
+ import pytest
+ import smtplib
+
+
+ @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))
+ smtp_connection.close()
+
+We use the ``request.module`` attribute to optionally obtain an
+``smtpserver`` attribute from the test module. If we just execute
+again, nothing much has changed:
+
+.. code-block:: pytest
+
+ $ pytest -s -q --tb=no test_module.py
+ FFfinalizing <smtplib.SMTP object at 0xdeadbeef0002> (smtp.gmail.com)
+
+ ========================= short test summary info ==========================
+ FAILED test_module.py::test_ehlo - assert 0
+ FAILED test_module.py::test_noop - assert 0
+ 2 failed in 0.12s
+
+Let's quickly create another test module that actually sets the
+server URL in its module namespace:
+
+.. code-block:: python
+
+ # content of test_anothersmtp.py
+
+ smtpserver = "mail.python.org" # will be read by smtp fixture
+
+
+ def test_showhelo(smtp_connection):
+ assert 0, smtp_connection.helo()
+
+Running it:
+
+.. code-block:: pytest
+
+ $ pytest -qq --tb=short test_anothersmtp.py
+ F [100%]
+ ================================= FAILURES =================================
+ ______________________________ test_showhelo _______________________________
+ test_anothersmtp.py:6: in test_showhelo
+ assert 0, smtp_connection.helo()
+ E AssertionError: (250, b'mail.python.org')
+ E assert 0
+ ------------------------- Captured stdout teardown -------------------------
+ finalizing <smtplib.SMTP object at 0xdeadbeef0003> (mail.python.org)
+ ========================= short test summary info ==========================
+ FAILED test_anothersmtp.py::test_showhelo - AssertionError: (250, b'mail....
+
+voila! The ``smtp_connection`` fixture function picked up our mail server name
+from the module namespace.
+
+.. _`using-markers`:
+
+Using markers to pass data to fixtures
+-------------------------------------------------------------
+
+Using the :py:class:`request <_pytest.fixtures.FixtureRequest>` object, a fixture can also access
+markers which are applied to a test function. This can be useful to pass data
+into a fixture from a test:
+
+.. code-block:: python
+
+ import pytest
+
+
+ @pytest.fixture
+ def fixt(request):
+ marker = request.node.get_closest_marker("fixt_data")
+ if marker is None:
+ # Handle missing marker in some way...
+ data = None
+ else:
+ data = marker.args[0]
+
+ # Do something with the data
+ return data
+
+
+ @pytest.mark.fixt_data(42)
+ def test_fixt(fixt):
+ assert fixt == 42
+
+.. _`fixture-factory`:
+
+Factories as fixtures
+-------------------------------------------------------------
+
+The "factory as fixture" pattern can help in situations where the result
+of a fixture is needed multiple times in a single test. Instead of returning
+data directly, the fixture instead returns a function which generates the data.
+This function can then be called multiple times in the test.
+
+Factories can have parameters as needed:
+
+.. code-block:: python
+
+ @pytest.fixture
+ def make_customer_record():
+ def _make_customer_record(name):
+ return {"name": name, "orders": []}
+
+ return _make_customer_record
+
+
+ def test_customer_records(make_customer_record):
+ customer_1 = make_customer_record("Lisa")
+ customer_2 = make_customer_record("Mike")
+ customer_3 = make_customer_record("Meredith")
+
+If the data created by the factory requires managing, the fixture can take care of that:
+
+.. code-block:: python
+
+ @pytest.fixture
+ def make_customer_record():
+
+ created_records = []
+
+ def _make_customer_record(name):
+ record = models.Customer(name=name, orders=[])
+ created_records.append(record)
+ return record
+
+ yield _make_customer_record
+
+ for record in created_records:
+ record.destroy()
+
+
+ def test_customer_records(make_customer_record):
+ customer_1 = make_customer_record("Lisa")
+ customer_2 = make_customer_record("Mike")
+ customer_3 = make_customer_record("Meredith")
+
+
+.. _`fixture-parametrize`:
+
+Parametrizing fixtures
+-----------------------------------------------------------------
+
+Fixture functions can be parametrized in which case they will be called
+multiple times, each time executing the set of dependent tests, i.e. the
+tests that depend on this fixture. Test functions usually do not need
+to be aware of their re-running. Fixture parametrization helps to
+write exhaustive functional tests for components which themselves can be
+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:
+
+.. code-block:: python
+
+ # content of conftest.py
+ import pytest
+ import smtplib
+
+
+ @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))
+ smtp_connection.close()
+
+The main change is the declaration of ``params`` with
+:py:func:`@pytest.fixture <pytest.fixture>`, a list of values
+for each of which the fixture function will execute and can access
+a value via ``request.param``. No test function code needs to change.
+So let's just do another run:
+
+.. code-block:: pytest
+
+ $ pytest -q test_module.py
+ FFFF [100%]
+ ================================= FAILURES =================================
+ ________________________ test_ehlo[smtp.gmail.com] _________________________
+
+ smtp_connection = <smtplib.SMTP object at 0xdeadbeef0004>
+
+ def test_ehlo(smtp_connection):
+ response, msg = smtp_connection.ehlo()
+ assert response == 250
+ assert b"smtp.gmail.com" in msg
+ > assert 0 # for demo purposes
+ E assert 0
+
+ test_module.py:7: AssertionError
+ ________________________ test_noop[smtp.gmail.com] _________________________
+
+ smtp_connection = <smtplib.SMTP object at 0xdeadbeef0004>
+
+ def test_noop(smtp_connection):
+ response, msg = smtp_connection.noop()
+ assert response == 250
+ > assert 0 # for demo purposes
+ E assert 0
+
+ test_module.py:13: AssertionError
+ ________________________ test_ehlo[mail.python.org] ________________________
+
+ smtp_connection = <smtplib.SMTP object at 0xdeadbeef0005>
+
+ def test_ehlo(smtp_connection):
+ response, msg = smtp_connection.ehlo()
+ assert response == 250
+ > assert b"smtp.gmail.com" in msg
+ E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8\nCHUNKING'
+
+ test_module.py:6: AssertionError
+ -------------------------- Captured stdout setup ---------------------------
+ finalizing <smtplib.SMTP object at 0xdeadbeef0004>
+ ________________________ test_noop[mail.python.org] ________________________
+
+ smtp_connection = <smtplib.SMTP object at 0xdeadbeef0005>
+
+ def test_noop(smtp_connection):
+ response, msg = smtp_connection.noop()
+ assert response == 250
+ > assert 0 # for demo purposes
+ E assert 0
+
+ test_module.py:13: AssertionError
+ ------------------------- Captured stdout teardown -------------------------
+ finalizing <smtplib.SMTP object at 0xdeadbeef0005>
+ ========================= short test summary info ==========================
+ FAILED test_module.py::test_ehlo[smtp.gmail.com] - assert 0
+ FAILED test_module.py::test_noop[smtp.gmail.com] - assert 0
+ FAILED test_module.py::test_ehlo[mail.python.org] - AssertionError: asser...
+ FAILED test_module.py::test_noop[mail.python.org] - assert 0
+ 4 failed in 0.12s
+
+We see that our two test functions each ran twice, against the different
+``smtp_connection`` instances. Note also, that with the ``mail.python.org``
+connection the second test fails in ``test_ehlo`` because a
+different server string is expected than what arrived.
+
+pytest will build a string that is the test ID for each fixture value
+in a parametrized fixture, e.g. ``test_ehlo[smtp.gmail.com]`` and
+``test_ehlo[mail.python.org]`` in the above examples. These IDs can
+be used with ``-k`` to select specific cases to run, and they will
+also identify the specific case when one is failing. Running pytest
+with ``--collect-only`` will show the generated IDs.
+
+Numbers, strings, booleans and ``None`` will have their usual string
+representation used in the test ID. For other objects, pytest will
+make a string based on the argument name. It is possible to customise
+the string used in a test ID for a certain fixture value by using the
+``ids`` keyword argument:
+
+.. code-block:: python
+
+ # content of test_ids.py
+ import pytest
+
+
+ @pytest.fixture(params=[0, 1], ids=["spam", "ham"])
+ def a(request):
+ return request.param
+
+
+ def test_a(a):
+ pass
+
+
+ def idfn(fixture_value):
+ if fixture_value == 0:
+ return "eggs"
+ else:
+ return None
+
+
+ @pytest.fixture(params=[0, 1], ids=idfn)
+ def b(request):
+ return request.param
+
+
+ def test_b(b):
+ pass
+
+The above shows how ``ids`` can be either a list of strings to use or
+a function which will be called with the fixture value and then
+has to return a string to use. In the latter case if the function
+returns ``None`` then pytest's auto-generated ID will be used.
+
+Running the above tests results in the following test IDs being used:
+
+.. code-block:: pytest
+
+ $ pytest --collect-only
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.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 ========================
+
+.. _`fixture-parametrize-marks`:
+
+Using marks with parametrized fixtures
+--------------------------------------
+
+:func:`pytest.param` can be used to apply marks in values sets of parametrized fixtures in the same way
+that they can be used with :ref:`@pytest.mark.parametrize <@pytest.mark.parametrize>`.
+
+Example:
+
+.. code-block:: python
+
+ # content of test_fixture_marks.py
+ import pytest
+
+
+ @pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)])
+ def data_set(request):
+ return request.param
+
+
+ def test_data(data_set):
+ pass
+
+Running this test will *skip* the invocation of ``data_set`` with value ``2``:
+
+.. code-block:: pytest
+
+ $ 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
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project
+ collecting ... collected 3 items
+
+ test_fixture_marks.py::test_data[0] PASSED [ 33%]
+ test_fixture_marks.py::test_data[1] PASSED [ 66%]
+ test_fixture_marks.py::test_data[2] SKIPPED (unconditional skip) [100%]
+
+ ======================= 2 passed, 1 skipped in 0.12s =======================
+
+.. _`interdependent fixtures`:
+
+Modularity: using fixtures from a fixture function
+----------------------------------------------------------
+
+In addition to using fixtures in test functions, fixture functions
+can use other fixtures themselves. This contributes to a modular design
+of your fixtures and allows re-use of framework-specific fixtures across
+many projects. As a simple example, we can extend the previous example
+and instantiate an object ``app`` where we stick the already defined
+``smtp_connection`` resource into it:
+
+.. code-block:: python
+
+ # content of test_appsetup.py
+
+ import pytest
+
+
+ class App:
+ def __init__(self, smtp_connection):
+ self.smtp_connection = smtp_connection
+
+
+ @pytest.fixture(scope="module")
+ def app(smtp_connection):
+ return App(smtp_connection)
+
+
+ def test_smtp_connection_exists(app):
+ assert app.smtp_connection
+
+Here we declare an ``app`` fixture which receives the previously defined
+``smtp_connection`` fixture and instantiates an ``App`` object with it. Let's run it:
+
+.. code-block:: pytest
+
+ $ 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
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project
+ collecting ... collected 2 items
+
+ test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%]
+ test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100%]
+
+ ============================ 2 passed in 0.12s =============================
+
+Due to the parametrization of ``smtp_connection``, the test will run twice with two
+different ``App`` instances and respective smtp servers. There is no
+need for the ``app`` fixture to be aware of the ``smtp_connection``
+parametrization because pytest will fully analyse the fixture dependency graph.
+
+Note that the ``app`` fixture has a scope of ``module`` and uses a
+module-scoped ``smtp_connection`` fixture. The example would still work if
+``smtp_connection`` was cached on a ``session`` scope: it is fine for fixtures to use
+"broader" scoped fixtures but not the other way round:
+A session-scoped fixture could not use a module-scoped one in a
+meaningful way.
+
+
+.. _`automatic per-resource grouping`:
+
+Automatic grouping of tests by fixture instances
+----------------------------------------------------------
+
+.. regendoc: wipe
+
+pytest minimizes the number of active fixtures during test runs.
+If you have a parametrized fixture, then all the tests using it will
+first execute with one instance and then finalizers are called
+before the next fixture instance is created. Among other things,
+this eases testing of applications which create and use global state.
+
+The following example uses two parametrized fixtures, one of which is
+scoped on a per-module basis, and all the functions perform ``print`` calls
+to show the setup/teardown flow:
+
+.. code-block:: python
+
+ # content of test_module.py
+ import pytest
+
+
+ @pytest.fixture(scope="module", params=["mod1", "mod2"])
+ def modarg(request):
+ param = request.param
+ print(" SETUP modarg", param)
+ yield param
+ print(" TEARDOWN modarg", param)
+
+
+ @pytest.fixture(scope="function", params=[1, 2])
+ def otherarg(request):
+ param = request.param
+ print(" SETUP otherarg", param)
+ yield param
+ print(" TEARDOWN otherarg", param)
+
+
+ def test_0(otherarg):
+ print(" RUN test0 with otherarg", otherarg)
+
+
+ def test_1(modarg):
+ print(" RUN test1 with modarg", modarg)
+
+
+ def test_2(otherarg, modarg):
+ print(" RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg))
+
+
+Let's run the tests in verbose mode and with looking at the print-output:
+
+.. code-block:: pytest
+
+ $ 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
+ cachedir: .pytest_cache
+ rootdir: /home/sweet/project
+ collecting ... collected 8 items
+
+ test_module.py::test_0[1] SETUP otherarg 1
+ RUN test0 with otherarg 1
+ PASSED TEARDOWN otherarg 1
+
+ test_module.py::test_0[2] SETUP otherarg 2
+ RUN test0 with otherarg 2
+ PASSED TEARDOWN otherarg 2
+
+ test_module.py::test_1[mod1] SETUP modarg mod1
+ RUN test1 with modarg mod1
+ PASSED
+ test_module.py::test_2[mod1-1] SETUP otherarg 1
+ RUN test2 with otherarg 1 and modarg mod1
+ PASSED TEARDOWN otherarg 1
+
+ test_module.py::test_2[mod1-2] SETUP otherarg 2
+ RUN test2 with otherarg 2 and modarg mod1
+ PASSED TEARDOWN otherarg 2
+
+ test_module.py::test_1[mod2] TEARDOWN modarg mod1
+ SETUP modarg mod2
+ RUN test1 with modarg mod2
+ PASSED
+ test_module.py::test_2[mod2-1] SETUP otherarg 1
+ RUN test2 with otherarg 1 and modarg mod2
+ PASSED TEARDOWN otherarg 1
+
+ test_module.py::test_2[mod2-2] SETUP otherarg 2
+ RUN test2 with otherarg 2 and modarg mod2
+ PASSED TEARDOWN otherarg 2
+ TEARDOWN modarg mod2
+
+
+ ============================ 8 passed in 0.12s =============================
+
+You can see that the parametrized module-scoped ``modarg`` resource caused an
+ordering of test execution that lead to the fewest possible "active" resources.
+The finalizer for the ``mod1`` parametrized resource was executed before the
+``mod2`` resource was setup.
+
+In particular notice that test_0 is completely independent and finishes first.
+Then test_1 is executed with ``mod1``, then test_2 with ``mod1``, then test_1
+with ``mod2`` and finally test_2 with ``mod2``.
+
+The ``otherarg`` parametrized resource (having function scope) was set up before
+and teared down after every test that used it.
+
+
+.. _`usefixtures`:
+
+Use fixtures in classes and modules with ``usefixtures``
+--------------------------------------------------------
+
+.. regendoc:wipe
+
+Sometimes test functions do not directly need access to a fixture object.
+For example, tests may require to operate with an empty directory as the
+current working directory but otherwise do not care for the concrete
+directory. Here is how you can use the standard :mod:`tempfile`
+and pytest fixtures to
+achieve it. We separate the creation of the fixture into a :file:`conftest.py`
+file:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+ import os
+ import tempfile
+
+ import pytest
+
+
+ @pytest.fixture
+ def cleandir():
+ with tempfile.TemporaryDirectory() as newpath:
+ old_cwd = os.getcwd()
+ os.chdir(newpath)
+ yield
+ os.chdir(old_cwd)
+
+and declare its use in a test module via a ``usefixtures`` marker:
+
+.. code-block:: python
+
+ # content of test_setenv.py
+ import os
+ import pytest
+
+
+ @pytest.mark.usefixtures("cleandir")
+ class TestDirectoryInit:
+ def test_cwd_starts_empty(self):
+ assert os.listdir(os.getcwd()) == []
+ with open("myfile", "w") as f:
+ f.write("hello")
+
+ def test_cwd_again_starts_empty(self):
+ assert os.listdir(os.getcwd()) == []
+
+Due to the ``usefixtures`` marker, the ``cleandir`` fixture
+will be required for the execution of each test method, just as if
+you specified a "cleandir" function argument to each of them. Let's run it
+to verify our fixture is activated and the tests pass:
+
+.. code-block:: pytest
+
+ $ pytest -q
+ .. [100%]
+ 2 passed in 0.12s
+
+You can specify multiple fixtures like this:
+
+.. code-block:: python
+
+ @pytest.mark.usefixtures("cleandir", "anotherfixture")
+ def test():
+ ...
+
+and you may specify fixture usage at the test module level using :globalvar:`pytestmark`:
+
+.. code-block:: python
+
+ pytestmark = pytest.mark.usefixtures("cleandir")
+
+
+It is also possible to put fixtures required by all tests in your project
+into an ini-file:
+
+.. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ usefixtures = cleandir
+
+
+.. warning::
+
+ Note this mark has no effect in **fixture functions**. For example,
+ this **will not work as expected**:
+
+ .. code-block:: python
+
+ @pytest.mark.usefixtures("my_other_fixture")
+ @pytest.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`.
+
+.. _`override fixtures`:
+
+Overriding fixtures on various levels
+-------------------------------------
+
+In relatively large test suite, you most likely need to ``override`` a ``global`` or ``root`` fixture with a ``locally``
+defined one, keeping the test code readable and maintainable.
+
+Override a fixture on a folder (conftest) level
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Given the tests file structure is:
+
+::
+
+ tests/
+ __init__.py
+
+ conftest.py
+ # content of tests/conftest.py
+ import pytest
+
+ @pytest.fixture
+ def username():
+ return 'username'
+
+ test_something.py
+ # content of tests/test_something.py
+ def test_username(username):
+ assert username == 'username'
+
+ subfolder/
+ __init__.py
+
+ conftest.py
+ # content of tests/subfolder/conftest.py
+ import pytest
+
+ @pytest.fixture
+ def username(username):
+ return 'overridden-' + username
+
+ test_something.py
+ # content of tests/subfolder/test_something.py
+ def test_username(username):
+ assert username == 'overridden-username'
+
+As you can see, a fixture with the same name can be overridden for certain test folder level.
+Note that the ``base`` or ``super`` fixture can be accessed from the ``overriding``
+fixture easily - used in the example above.
+
+Override a fixture on a test module level
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Given the tests file structure is:
+
+::
+
+ tests/
+ __init__.py
+
+ conftest.py
+ # content of tests/conftest.py
+ import pytest
+
+ @pytest.fixture
+ def username():
+ return 'username'
+
+ test_something.py
+ # content of tests/test_something.py
+ import pytest
+
+ @pytest.fixture
+ def username(username):
+ return 'overridden-' + username
+
+ def test_username(username):
+ assert username == 'overridden-username'
+
+ test_something_else.py
+ # content of tests/test_something_else.py
+ import pytest
+
+ @pytest.fixture
+ def username(username):
+ return 'overridden-else-' + username
+
+ def test_username(username):
+ assert username == 'overridden-else-username'
+
+In the example above, a fixture with the same name can be overridden for certain test module.
+
+
+Override a fixture with direct test parametrization
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Given the tests file structure is:
+
+::
+
+ tests/
+ __init__.py
+
+ conftest.py
+ # content of tests/conftest.py
+ import pytest
+
+ @pytest.fixture
+ def username():
+ return 'username'
+
+ @pytest.fixture
+ def other_username(username):
+ return 'other-' + username
+
+ test_something.py
+ # content of tests/test_something.py
+ import pytest
+
+ @pytest.mark.parametrize('username', ['directly-overridden-username'])
+ def test_username(username):
+ assert username == 'directly-overridden-username'
+
+ @pytest.mark.parametrize('username', ['directly-overridden-username-other'])
+ def test_username_other(other_username):
+ assert other_username == 'other-directly-overridden-username-other'
+
+In the example above, a fixture value is overridden by the test parameter value. Note that the value of the fixture
+can be overridden this way even if the test doesn't use it directly (doesn't mention it in the function prototype).
+
+
+Override a parametrized fixture with non-parametrized one and vice versa
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Given the tests file structure is:
+
+::
+
+ tests/
+ __init__.py
+
+ conftest.py
+ # content of tests/conftest.py
+ import pytest
+
+ @pytest.fixture(params=['one', 'two', 'three'])
+ def parametrized_username(request):
+ return request.param
+
+ @pytest.fixture
+ def non_parametrized_username(request):
+ return 'username'
+
+ test_something.py
+ # content of tests/test_something.py
+ import pytest
+
+ @pytest.fixture
+ def parametrized_username():
+ return 'overridden-username'
+
+ @pytest.fixture(params=['one', 'two', 'three'])
+ def non_parametrized_username(request):
+ return request.param
+
+ def test_username(parametrized_username):
+ assert parametrized_username == 'overridden-username'
+
+ def test_parametrized_username(non_parametrized_username):
+ assert non_parametrized_username in ['one', 'two', 'three']
+
+ test_something_else.py
+ # content of tests/test_something_else.py
+ def test_username(parametrized_username):
+ assert parametrized_username in ['one', 'two', 'three']
+
+ def test_username(non_parametrized_username):
+ assert non_parametrized_username == 'username'
+
+In the example above, a parametrized fixture is overridden with a non-parametrized version, and
+a non-parametrized fixture is overridden with a parametrized version for certain test module.
+The same applies for the test folder level obviously.
+
+
+Using fixtures from other projects
+----------------------------------
+
+Usually projects that provide pytest support will use :ref:`entry points <setuptools entry points>`,
+so just installing those projects into an environment will make those fixtures available for use.
+
+In case you want to use fixtures from a project that does not use entry points, you can
+define :globalvar:`pytest_plugins` in your top ``conftest.py`` file to register that module
+as a plugin.
+
+Suppose you have some fixtures in ``mylibrary.fixtures`` and you want to reuse them into your
+``app/tests`` directory.
+
+All you need to do is to define :globalvar:`pytest_plugins` in ``app/tests/conftest.py``
+pointing to that module.
+
+.. code-block:: python
+
+ pytest_plugins = "mylibrary.fixtures"
+
+This effectively registers ``mylibrary.fixtures`` as a plugin, making all its fixtures and
+hooks available to tests in ``app/tests``.
+
+.. note::
+
+ Sometimes users will *import* fixtures from other projects for use, however this is not
+ recommended: importing fixtures into a module will register them in pytest
+ as *defined* in that module.
+
+ This has minor consequences, such as appearing multiple times in ``pytest --help``,
+ but it is not **recommended** because this behavior might change/stop working
+ in future versions.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/index.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/index.rst
new file mode 100644
index 0000000000..6f52aaecdc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/index.rst
@@ -0,0 +1,64 @@
+:orphan:
+
+.. _how-to:
+
+How-to guides
+================
+
+Core pytest functionality
+-------------------------
+
+.. toctree::
+ :maxdepth: 1
+
+ usage
+ assert
+ fixtures
+ mark
+ parametrize
+ tmp_path
+ monkeypatch
+ doctest
+ cache
+
+Test output and outcomes
+----------------------------
+
+.. toctree::
+ :maxdepth: 1
+
+ failures
+ output
+ logging
+ capture-stdout-stderr
+ capture-warnings
+ skipping
+
+Plugins
+----------------------------
+
+.. toctree::
+ :maxdepth: 1
+
+ plugins
+ writing_plugins
+ writing_hook_functions
+
+pytest and other test systems
+-----------------------------
+
+.. toctree::
+ :maxdepth: 1
+
+ existingtestsuite
+ unittest
+ nose
+ xunit_setup
+
+pytest development environment
+------------------------------
+
+.. toctree::
+ :maxdepth: 1
+
+ bash-completion
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/logging.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/logging.rst
new file mode 100644
index 0000000000..2e8734fa6a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/logging.rst
@@ -0,0 +1,292 @@
+.. _logging:
+
+How to manage logging
+---------------------
+
+pytest captures log messages of level ``WARNING`` or above automatically and displays them in their own section
+for each failed test in the same manner as captured stdout and stderr.
+
+Running without options:
+
+.. code-block:: bash
+
+ pytest
+
+Shows failed tests like so:
+
+.. code-block:: pytest
+
+ ----------------------- Captured stdlog call ----------------------
+ test_reporting.py 26 WARNING text going to logger
+ ----------------------- Captured stdout call ----------------------
+ text going to stdout
+ ----------------------- Captured stderr call ----------------------
+ text going to stderr
+ ==================== 2 failed in 0.02 seconds =====================
+
+By default each captured log message shows the module, line number, log level
+and message.
+
+If desired the log and date format can be specified to
+anything that the logging module supports by passing specific formatting options:
+
+.. code-block:: bash
+
+ pytest --log-format="%(asctime)s %(levelname)s %(message)s" \
+ --log-date-format="%Y-%m-%d %H:%M:%S"
+
+Shows failed tests like so:
+
+.. code-block:: pytest
+
+ ----------------------- Captured stdlog call ----------------------
+ 2010-04-10 14:48:44 WARNING text going to logger
+ ----------------------- Captured stdout call ----------------------
+ text going to stdout
+ ----------------------- Captured stderr call ----------------------
+ text going to stderr
+ ==================== 2 failed in 0.02 seconds =====================
+
+These options can also be customized through ``pytest.ini`` file:
+
+.. code-block:: ini
+
+ [pytest]
+ log_format = %(asctime)s %(levelname)s %(message)s
+ log_date_format = %Y-%m-%d %H:%M:%S
+
+Further it is possible to disable reporting of captured content (stdout,
+stderr and logs) on failed tests completely with:
+
+.. code-block:: bash
+
+ pytest --show-capture=no
+
+
+caplog fixture
+^^^^^^^^^^^^^^
+
+Inside tests it is possible to change the log level for the captured log
+messages. This is supported by the ``caplog`` fixture:
+
+.. code-block:: python
+
+ 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
+logger:
+
+.. code-block:: python
+
+ 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.
+
+It is also possible to use a context manager to temporarily change the log
+level inside a ``with`` block:
+
+.. code-block:: python
+
+ def test_bar(caplog):
+ with caplog.at_level(logging.INFO):
+ pass
+
+Again, by default the level of the root logger is affected but the level of any
+logger can be changed instead with:
+
+.. code-block:: python
+
+ def test_bar(caplog):
+ with caplog.at_level(logging.CRITICAL, logger="root.baz"):
+ pass
+
+Lastly all the logs sent to the logger during the test run are made available on
+the fixture in the form of both the ``logging.LogRecord`` instances and the final log text.
+This is useful for when you want to assert on the contents of a message:
+
+.. code-block:: python
+
+ def test_baz(caplog):
+ func_under_test()
+ for record in caplog.records:
+ assert record.levelname != "CRITICAL"
+ assert "wally" not in caplog.text
+
+For all the available attributes of the log records see the
+``logging.LogRecord`` class.
+
+You can also resort to ``record_tuples`` if all you want to do is to ensure,
+that certain messages have been logged under a given logger name with a given
+severity and message:
+
+.. code-block:: python
+
+ def test_foo(caplog):
+ logging.getLogger().info("boo %s", "arg")
+
+ assert caplog.record_tuples == [("root", logging.INFO, "boo arg")]
+
+You can call ``caplog.clear()`` to reset the captured log records in a test:
+
+.. code-block:: python
+
+ def test_something_with_clearing_records(caplog):
+ some_method_that_creates_log_records()
+ caplog.clear()
+ your_test_method()
+ assert ["Foo"] == [rec.message for rec in caplog.records]
+
+
+The ``caplog.records`` attribute contains records from the current stage only, so
+inside the ``setup`` phase it contains only setup logs, same with the ``call`` and
+``teardown`` phases.
+
+To access logs from other stages, use the ``caplog.get_records(when)`` method. As an example,
+if you want to make sure that tests which use a certain fixture never log any warnings, you can inspect
+the records for the ``setup`` and ``call`` stages during teardown like so:
+
+.. code-block:: python
+
+ @pytest.fixture
+ def window(caplog):
+ window = create_window()
+ yield window
+ for when in ("setup", "call"):
+ messages = [
+ 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)
+ )
+
+
+
+The full API is available at :class:`pytest.LogCaptureFixture`.
+
+
+.. _live_logs:
+
+Live Logs
+^^^^^^^^^
+
+By setting the :confval:`log_cli` configuration option to ``true``, pytest will output
+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.
+
+Additionally, you can also specify ``--log-cli-format`` and
+``--log-cli-date-format`` which mirror and default to ``--log-format`` and
+``--log-date-format`` if not provided, but are applied only to the console
+logging handler.
+
+All of the CLI log options can also be set in the configuration INI file. The
+option names are:
+
+* ``log_cli_level``
+* ``log_cli_format``
+* ``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
+means that it will be overwritten at each run tests session.
+
+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.
+
+Additionally, you can also specify ``--log-file-format`` and
+``--log-file-date-format`` which are equal to ``--log-format`` and
+``--log-date-format`` but are applied to the log file logging handler.
+
+All of the log file options can also be set in the configuration INI file. The
+option names are:
+
+* ``log_file``
+* ``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**.
+
+.. _log_colors:
+
+Customizing Colors
+^^^^^^^^^^^^^^^^^^
+
+Log levels are colored if colored terminal output is enabled. Changing
+from default colors or putting color on custom log levels is supported
+through ``add_color_level()``. Example:
+
+.. code-block:: python
+
+ @pytest.hookimpl
+ def pytest_configure(config):
+ logging_plugin = config.pluginmanager.get_plugin("logging-plugin")
+
+ # Change color on existing log level
+ logging_plugin.log_cli_handler.formatter.add_color_level(logging.INFO, "cyan")
+
+ # Add color to a custom log level (a custom log level `SPAM` is already set up)
+ logging_plugin.log_cli_handler.formatter.add_color_level(logging.SPAM, "blue")
+.. warning::
+
+ This feature and its API are considered **experimental** and might change
+ between releases without a deprecation notice.
+.. _log_release_notes:
+
+Release notes
+^^^^^^^^^^^^^
+
+This feature was introduced as a drop-in replacement for the
+:pypi:`pytest-catchlog` plugin and they conflict
+with each other. The backward compatibility API with ``pytest-capturelog``
+has been dropped when this feature was introduced, so if for that reason you
+still need ``pytest-catchlog`` you can disable the internal feature by
+adding to your ``pytest.ini``:
+
+.. code-block:: ini
+
+ [pytest]
+ addopts=-p no:logging
+
+
+.. _log_changes_3_4:
+
+Incompatible changes in pytest 3.4
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This feature was introduced in ``3.3`` and some **incompatible changes** have been
+made in ``3.4`` after community feedback:
+
+* Log levels are no longer changed unless explicitly requested by the :confval:`log_level` configuration
+ or ``--log-level`` command-line options. This allows users to configure logger objects themselves.
+ Setting :confval:`log_level` will set the level that is captured globally so if a specific test requires
+ a lower level than this, use the ``caplog.set_level()`` functionality otherwise that test will be prone to
+ failure.
+* :ref:`Live Logs <live_logs>` is now disabled by default and can be enabled setting the
+ :confval:`log_cli` configuration option to ``true``. When enabled, the verbosity is increased so logging for each
+ test is visible.
+* :ref:`Live Logs <live_logs>` are now sent to ``sys.stdout`` and no longer require the ``-s`` command-line option
+ to work.
+
+If you want to partially restore the logging behavior of version ``3.3``, you can add this options to your ``ini``
+file:
+
+.. code-block:: ini
+
+ [pytest]
+ log_cli=true
+ log_level=NOTSET
+
+More details about the discussion that lead to this changes can be read in :issue:`3013`.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/mark.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/mark.rst
new file mode 100644
index 0000000000..33f9d18bfe
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/mark.rst
@@ -0,0 +1,93 @@
+.. _mark:
+
+How to mark test functions with attributes
+===========================================
+
+By using the ``pytest.mark`` helper you can easily set
+metadata on your test functions. You can find the full list of builtin markers
+in the :ref:`API Reference<marks ref>`. Or you can list all the markers, including
+builtin and custom, using the CLI - :code:`pytest --markers`.
+
+Here are some of the builtin markers:
+
+* :ref:`usefixtures <usefixtures>` - use fixtures on a test function or class
+* :ref:`filterwarnings <filterwarnings>` - filter certain warnings of a test function
+* :ref:`skip <skip>` - always skip a test function
+* :ref:`skipif <skipif>` - skip a test function if a certain condition is met
+* :ref:`xfail <xfail>` - produce an "expected failure" outcome if a certain
+ condition is met
+* :ref:`parametrize <parametrizemark>` - perform multiple calls
+ to the same test function.
+
+It's easy to create custom markers or to apply markers
+to whole test classes or modules. Those markers can be used by plugins, and also
+are commonly used to :ref:`select tests <mark run>` on the command-line with the ``-m`` option.
+
+See :ref:`mark examples` for examples which also serve as documentation.
+
+.. note::
+
+ Marks can only be applied to tests, having no effect on
+ :ref:`fixtures <fixtures>`.
+
+
+Registering marks
+-----------------
+
+You can register custom marks in your ``pytest.ini`` file like this:
+
+.. code-block:: ini
+
+ [pytest]
+ markers =
+ slow: marks tests as slow (deselect with '-m "not slow"')
+ serial
+
+or in your ``pyproject.toml`` file like this:
+
+.. code-block:: toml
+
+ [tool.pytest.ini_options]
+ markers = [
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')",
+ "serial",
+ ]
+
+Note that everything past the ``:`` after the mark name is an optional description.
+
+Alternatively, you can register new markers programmatically in a
+:ref:`pytest_configure <initialization-hooks>` hook:
+
+.. code-block:: python
+
+ def pytest_configure(config):
+ config.addinivalue_line(
+ "markers", "env(name): mark test to run only on named environment"
+ )
+
+
+Registered marks appear in pytest's help text and do not emit warnings (see the next section). It
+is recommended that third-party plugins always :ref:`register their markers <registering-markers>`.
+
+.. _unknown-marks:
+
+Raising errors on unknown marks
+-------------------------------
+
+Unregistered marks applied with the ``@pytest.mark.name_of_the_mark`` decorator
+will always emit a warning in order to avoid silently doing something
+surprising due to mistyped names. As described in the previous section, you can disable
+the warning for custom marks by registering them in your ``pytest.ini`` file or
+using a custom ``pytest_configure`` hook.
+
+When the ``--strict-markers`` command-line flag is passed, any unknown marks applied
+with the ``@pytest.mark.name_of_the_mark`` decorator will trigger an error. You can
+enforce this validation in your project by adding ``--strict-markers`` to ``addopts``:
+
+.. code-block:: ini
+
+ [pytest]
+ addopts = --strict-markers
+ markers =
+ slow: marks tests as slow (deselect with '-m "not slow"')
+ serial
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/monkeypatch.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/monkeypatch.rst
new file mode 100644
index 0000000000..9c61233f7e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/monkeypatch.rst
@@ -0,0 +1,444 @@
+.. _monkeypatching:
+
+How to monkeypatch/mock modules and environments
+================================================================
+
+.. currentmodule:: _pytest.monkeypatch
+
+Sometimes tests need to invoke functionality which depends
+on global settings or which invokes code which cannot be easily
+tested such as network access. The ``monkeypatch`` fixture
+helps you to safely set/delete an attribute, dictionary item or
+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
+
+ 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``
+parameter determines if a ``KeyError`` or ``AttributeError``
+will be raised if the target of the set/deletion operation does not exist.
+
+Consider the following scenarios:
+
+1. Modifying the behavior of a function or the property of a class for a test e.g.
+there is an API call or database connection you will not make for a test but you know
+what the expected output should be. Use :py:meth:`monkeypatch.setattr <MonkeyPatch.setattr>` to patch the
+function or property with your desired testing behavior. This can include your own functions.
+Use :py:meth:`monkeypatch.delattr <MonkeyPatch.delattr>` to remove the function or property for the test.
+
+2. Modifying the values of dictionaries e.g. you have a global configuration that
+you want to modify for certain test cases. Use :py:meth:`monkeypatch.setitem <MonkeyPatch.setitem>` to patch the
+dictionary for the test. :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` can be used to remove items.
+
+3. Modifying environment variables for a test e.g. to test program behavior if an
+environment variable is missing, or to set multiple values to a known variable.
+:py:meth:`monkeypatch.setenv <MonkeyPatch.setenv>` and :py:meth:`monkeypatch.delenv <MonkeyPatch.delenv>` can be used for
+these patches.
+
+4. Use ``monkeypatch.setenv("PATH", value, prepend=os.pathsep)`` to modify ``$PATH``, and
+:py:meth:`monkeypatch.chdir <MonkeyPatch.chdir>` to change the context of the current working directory
+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`.
+
+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
+----------------------------------------
+
+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``
+can be used to patch functions dependent on the user to always return a
+specific value.
+
+In this example, :py:meth:`monkeypatch.setattr <MonkeyPatch.setattr>` is used to patch ``Path.home``
+so that the known testing path ``Path("/abc")`` is always used when the test is run.
+This removes any dependency on the running user for testing purposes.
+:py:meth:`monkeypatch.setattr <MonkeyPatch.setattr>` must be called before the function which will use
+the patched function is called.
+After the test function finishes the ``Path.home`` modification will be undone.
+
+.. code-block:: python
+
+ # contents of test_module.py with source code and the test
+ from pathlib import Path
+
+
+ def getssh():
+ """Simple function to return expanded homedir ssh path."""
+ return Path.home() / ".ssh"
+
+
+ def test_getssh(monkeypatch):
+ # mocked return function to replace Path.home
+ # always return '/abc'
+ def mockreturn():
+ return Path("/abc")
+
+ # Application of the monkeypatch to replace Path.home
+ # with the behavior of mockreturn defined above.
+ monkeypatch.setattr(Path, "home", mockreturn)
+
+ # Calling getssh() will use mockreturn in place of Path.home
+ # for this test with the monkeypatch.
+ x = getssh()
+ assert x == Path("/abc/.ssh")
+
+Monkeypatching returned objects: building mock classes
+------------------------------------------------------
+
+:py:meth:`monkeypatch.setattr <MonkeyPatch.setattr>` can be used in conjunction with classes to mock returned
+objects from functions instead of values.
+Imagine a simple function to take an API url and return the json response.
+
+.. code-block:: python
+
+ # contents of app.py, a simple API retrieval example
+ import requests
+
+
+ def get_json(url):
+ """Takes a URL, and returns the JSON."""
+ r = requests.get(url)
+ return r.json()
+
+We need to mock ``r``, the returned response object for testing purposes.
+The mock of ``r`` needs a ``.json()`` method which returns a dictionary.
+This can be done in our test file by defining a class to represent ``r``.
+
+.. code-block:: python
+
+ # contents of test_app.py, a simple test for our API retrieval
+ # import requests for the purposes of monkeypatching
+ import requests
+
+ # our app.py that includes the get_json() function
+ # 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():
+ return {"mock_key": "mock_response"}
+
+
+ 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):
+ return MockResponse()
+
+ # apply the monkeypatch for requests.get to mock_get
+ monkeypatch.setattr(requests, "get", mock_get)
+
+ # app.get_json, which contains requests.get, uses the monkeypatch
+ result = app.get_json("https://fakeurl")
+ assert result["mock_key"] == "mock_response"
+
+
+``monkeypatch`` applies the mock for ``requests.get`` with our ``mock_get`` function.
+The ``mock_get`` function returns an instance of the ``MockResponse`` class, which
+has a ``json()`` method defined to return a known testing dictionary and does not
+require any outside API connection.
+
+You can build the ``MockResponse`` class with the appropriate degree of complexity for
+the scenario you are testing. For instance, it could include an ``ok`` property that
+always returns ``True``, or return different values from the ``json()`` mocked method
+based on input strings.
+
+This mock can be shared across tests using a ``fixture``:
+
+.. code-block:: python
+
+ # contents of test_app.py, a simple test for our API retrieval
+ import pytest
+ import requests
+
+ # app.py that includes the get_json() function
+ import app
+
+ # custom class to be the mock return value of requests.get()
+ class MockResponse:
+ @staticmethod
+ def json():
+ return {"mock_key": "mock_response"}
+
+
+ # monkeypatched requests.get moved to a fixture
+ @pytest.fixture
+ def mock_response(monkeypatch):
+ """Requests.get() mocked to return {'mock_key':'mock_response'}."""
+
+ def mock_get(*args, **kwargs):
+ return MockResponse()
+
+ monkeypatch.setattr(requests, "get", mock_get)
+
+
+ # notice our test uses the custom fixture instead of monkeypatch directly
+ def test_get_json(mock_response):
+ result = app.get_json("https://fakeurl")
+ assert result["mock_key"] == "mock_response"
+
+
+Furthermore, if the mock was designed to be applied to all tests, the ``fixture`` could
+be moved to a ``conftest.py`` file and use the with ``autouse=True`` option.
+
+
+Global patch example: preventing "requests" from remote operations
+------------------------------------------------------------------
+
+If you want to prevent the "requests" library from performing http
+requests in all your tests, you can do:
+
+.. code-block:: python
+
+ # contents of conftest.py
+ import pytest
+
+
+ @pytest.fixture(autouse=True)
+ def no_requests(monkeypatch):
+ """Remove requests.sessions.Session.request for all tests."""
+ monkeypatch.delattr("requests.sessions.Session.request")
+
+This autouse fixture will be executed for each test function and it
+will delete the method ``request.session.Session.request``
+so that any attempts within tests to create http requests will fail.
+
+
+.. note::
+
+ Be advised that it is not recommended to patch builtin functions such as ``open``,
+ ``compile``, etc., because it might break pytest's internals. If that's
+ unavoidable, passing ``--tb=native``, ``--assert=plain`` and ``--capture=no`` might
+ help although there's no guarantee.
+
+.. note::
+
+ Mind that patching ``stdlib`` functions and some third-party libraries used by pytest
+ might break pytest itself, therefore in those cases it is recommended to use
+ :meth:`MonkeyPatch.context` to limit the patching to the block you want tested:
+
+ .. code-block:: python
+
+ import functools
+
+
+ def test_partial(monkeypatch):
+ with monkeypatch.context() as m:
+ m.setattr(functools, "partial", 3)
+ assert functools.partial == 3
+
+ See :issue:`3290` for details.
+
+
+Monkeypatching environment variables
+------------------------------------
+
+If you are working with environment variables you often need to safely change the values
+or delete them from the system for testing purposes. ``monkeypatch`` provides a mechanism
+to do this using the ``setenv`` and ``delenv`` method. Our example code to test:
+
+.. code-block:: python
+
+ # contents of our original code file e.g. code.py
+ import os
+
+
+ def get_os_user_lower():
+ """Simple retrieval function.
+ Returns lowercase USER or raises OSError."""
+ username = os.getenv("USER")
+
+ if username is None:
+ raise OSError("USER environment is not set.")
+
+ return username.lower()
+
+There are two potential paths. First, the ``USER`` environment variable is set to a
+value. Second, the ``USER`` environment variable does not exist. Using ``monkeypatch``
+both paths can be safely tested without impacting the running environment:
+
+.. code-block:: python
+
+ # contents of our test file e.g. test_code.py
+ import pytest
+
+
+ def test_upper_to_lower(monkeypatch):
+ """Set the USER env var to assert the behavior."""
+ monkeypatch.setenv("USER", "TestingUser")
+ assert get_os_user_lower() == "testinguser"
+
+
+ def test_raise_exception(monkeypatch):
+ """Remove the USER env var and assert OSError is raised."""
+ monkeypatch.delenv("USER", raising=False)
+
+ with pytest.raises(OSError):
+ _ = get_os_user_lower()
+
+This behavior can be moved into ``fixture`` structures and shared across tests:
+
+.. code-block:: python
+
+ # contents of our test file e.g. test_code.py
+ import pytest
+
+
+ @pytest.fixture
+ def mock_env_user(monkeypatch):
+ monkeypatch.setenv("USER", "TestingUser")
+
+
+ @pytest.fixture
+ def mock_env_missing(monkeypatch):
+ monkeypatch.delenv("USER", raising=False)
+
+
+ # notice the tests reference the fixtures for mocks
+ def test_upper_to_lower(mock_env_user):
+ assert get_os_user_lower() == "testinguser"
+
+
+ def test_raise_exception(mock_env_missing):
+ with pytest.raises(OSError):
+ _ = get_os_user_lower()
+
+
+Monkeypatching dictionaries
+---------------------------
+
+:py:meth:`monkeypatch.setitem <MonkeyPatch.setitem>` can be used to safely set the values of dictionaries
+to specific values during tests. Take this simplified connection string example:
+
+.. code-block:: python
+
+ # contents of app.py to generate a simple connection string
+ DEFAULT_CONFIG = {"user": "user1", "database": "db1"}
+
+
+ def create_connection_string(config=None):
+ """Creates a connection string from input or defaults."""
+ config = config or DEFAULT_CONFIG
+ return f"User Id={config['user']}; Location={config['database']};"
+
+For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific values.
+
+.. code-block:: python
+
+ # contents of test_app.py
+ # app.py with the connection string function (prior code block)
+ import app
+
+
+ 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")
+ monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db")
+
+ # expected result based on the mocks
+ expected = "User Id=test_user; Location=test_db;"
+
+ # the test uses the monkeypatched dictionary settings
+ result = app.create_connection_string()
+ assert result == expected
+
+You can use the :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` to remove values.
+
+.. code-block:: python
+
+ # contents of test_app.py
+ import pytest
+
+ # app.py with the connection string function
+ import app
+
+
+ def test_missing_user(monkeypatch):
+
+ # patch the DEFAULT_CONFIG t be missing the 'user' key
+ monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
+
+ # Key error expected because a config is not passed, and the
+ # default is now missing the 'user' entry.
+ with pytest.raises(KeyError):
+ _ = app.create_connection_string()
+
+
+The modularity of fixtures gives you the flexibility to define
+separate fixtures for each potential mock and reference them in the needed tests.
+
+.. code-block:: python
+
+ # contents of test_app.py
+ import pytest
+
+ # 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):
+ """Set the DEFAULT_CONFIG user to test_user."""
+ monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
+
+
+ @pytest.fixture
+ def mock_test_database(monkeypatch):
+ """Set the DEFAULT_CONFIG database to test_db."""
+ monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db")
+
+
+ @pytest.fixture
+ def mock_missing_default_user(monkeypatch):
+ """Remove the user key from DEFAULT_CONFIG"""
+ monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
+
+
+ # 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()
+ assert result == expected
+
+
+ def test_missing_user(mock_missing_default_user):
+
+ with pytest.raises(KeyError):
+ _ = app.create_connection_string()
+
+
+.. currentmodule:: _pytest.monkeypatch
+
+API Reference
+-------------
+
+Consult the docs for the :class:`MonkeyPatch` class.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/nose.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/nose.rst
new file mode 100644
index 0000000000..4bf8b06c32
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/nose.rst
@@ -0,0 +1,79 @@
+.. _`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/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/output.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/output.rst
new file mode 100644
index 0000000000..4b90988f49
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/output.rst
@@ -0,0 +1,710 @@
+.. _how-to-manage-output:
+
+Managing pytest's output
+=========================
+
+.. _how-to-modifying-python-tb-printing:
+
+Modifying Python traceback printing
+--------------------------------------------------
+
+Examples for modifying traceback printing:
+
+.. code-block:: bash
+
+ pytest --showlocals # show local variables in tracebacks
+ pytest -l # show local variables (shortcut)
+
+ pytest --tb=auto # (default) 'long' tracebacks for the first and last
+ # entry, but 'short' style for the other entries
+ pytest --tb=long # exhaustive, informative traceback formatting
+ pytest --tb=short # shorter traceback format
+ pytest --tb=line # only one line per failure
+ pytest --tb=native # Python standard library formatting
+ pytest --tb=no # no traceback at all
+
+The ``--full-trace`` causes very long traces to be printed on error (longer
+than ``--tb=long``). It also ensures that a stack trace is printed on
+**KeyboardInterrupt** (Ctrl+C).
+This is very useful if the tests are taking too long and you interrupt them
+with Ctrl+C to find out where the tests are *hanging*. By default no output
+will be shown (because KeyboardInterrupt is caught by pytest). By using this
+option you make sure a trace is shown.
+
+
+Verbosity
+--------------------------------------------------
+
+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.
+
+.. regendoc:wipe
+
+Consider this simple file:
+
+.. code-block:: python
+
+ # content of test_verbosity_example.py
+ 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 = "Lorem ipsum dolor sit amet " * 10
+ assert "hello world" in long_text
+
+Executing pytest normally gives us this output (we are skipping the header to focus on the rest):
+
+.. code-block:: pytest
+
+ $ pytest --no-header
+ =========================== test session starts ============================
+ collected 4 items
+
+ test_verbosity_example.py .FFF [100%]
+
+ ================================= FAILURES =================================
+ _____________________________ test_words_fail ______________________________
+
+ def test_words_fail():
+ fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
+ fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
+ > assert fruits1 == fruits2
+ E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
+ E At index 2 diff: 'grapes' != 'orange'
+ E Use -v to get the full diff
+
+ test_verbosity_example.py:8: AssertionError
+ ____________________________ test_numbers_fail _____________________________
+
+ 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
+ E AssertionError: assert {'0': 0, '1':..., '3': 3, ...} == {'0': 0, '10'...'30': 30, ...}
+ 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
+
+ test_verbosity_example.py:14: AssertionError
+ ___________________________ test_long_text_fail ____________________________
+
+ def test_long_text_fail():
+ long_text = "Lorem ipsum dolor sit amet " * 10
+ > assert "hello world" in long_text
+ E 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 ips... sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '
+
+ 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...
+ ======================= 3 failed, 1 passed in 0.12s ========================
+
+Notice that:
+
+* Each test inside the file is shown by a single character in the output: ``.`` for passing, ``F`` for failure.
+* ``test_words_fail`` failed, and we are shown a short summary indicating the index 2 of the two lists differ.
+* ``test_numbers_fail`` failed, and we are shown a summary of left/right differences on dictionary items. Identical items are omitted.
+* ``test_long_text_fail`` failed, and the right hand side of the ``in`` statement is truncated using ``...```
+ because it is longer than an internal threshold (240 characters currently).
+
+Now we can increase pytest's verbosity:
+
+.. code-block:: pytest
+
+ $ pytest --no-header -v
+ =========================== test session starts ============================
+ collecting ... collected 4 items
+
+ test_verbosity_example.py::test_ok PASSED [ 25%]
+ test_verbosity_example.py::test_words_fail FAILED [ 50%]
+ test_verbosity_example.py::test_numbers_fail FAILED [ 75%]
+ test_verbosity_example.py::test_long_text_fail FAILED [100%]
+
+ ================================= FAILURES =================================
+ _____________________________ test_words_fail ______________________________
+
+ def test_words_fail():
+ fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
+ fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
+ > assert fruits1 == fruits2
+ E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
+ E At index 2 diff: 'grapes' != 'orange'
+ E Full diff:
+ E - ['banana', 'apple', 'orange', 'melon', 'kiwi']
+ E ? ^ ^^
+ E + ['banana', 'apple', 'grapes', 'melon', 'kiwi']
+ E ? ^ ^ +
+
+ test_verbosity_example.py:8: AssertionError
+ ____________________________ test_numbers_fail _____________________________
+
+ 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
+ E AssertionError: assert {'0': 0, '1':..., '3': 3, ...} == {'0': 0, '10'...'30': 30, ...}
+ 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 ...Full output truncated (3 lines hidden), use '-vv' to show
+
+ test_verbosity_example.py:14: AssertionError
+ ___________________________ test_long_text_fail ____________________________
+
+ def test_long_text_fail():
+ long_text = "Lorem ipsum dolor sit amet " * 10
+ > assert "hello world" in long_text
+ E 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 '
+
+ 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...
+ ======================= 3 failed, 1 passed in 0.12s ========================
+
+Notice now that:
+
+* Each test inside the file gets its own line in the output.
+* ``test_words_fail`` now shows the two failing lists in full, in addition to which index differs.
+* ``test_numbers_fail`` now shows a text diff of the two dictionaries, truncated.
+* ``test_long_text_fail`` no longer truncates the right hand side of the ``in`` statement, because the internal
+ threshold for truncation is larger now (2400 characters currently).
+
+Now if we increase verbosity even more:
+
+.. code-block:: pytest
+
+ $ pytest --no-header -vv
+ =========================== test session starts ============================
+ collecting ... collected 4 items
+
+ test_verbosity_example.py::test_ok PASSED [ 25%]
+ test_verbosity_example.py::test_words_fail FAILED [ 50%]
+ test_verbosity_example.py::test_numbers_fail FAILED [ 75%]
+ test_verbosity_example.py::test_long_text_fail FAILED [100%]
+
+ ================================= FAILURES =================================
+ _____________________________ test_words_fail ______________________________
+
+ def test_words_fail():
+ fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
+ fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
+ > assert fruits1 == fruits2
+ E AssertionError: assert ['banana', 'apple', 'grapes', 'melon', 'kiwi'] == ['banana', 'apple', 'orange', 'melon', 'kiwi']
+ E At index 2 diff: 'grapes' != 'orange'
+ E Full diff:
+ E - ['banana', 'apple', 'orange', 'melon', 'kiwi']
+ E ? ^ ^^
+ E + ['banana', 'apple', 'grapes', 'melon', 'kiwi']
+ E ? ^ ^ +
+
+ test_verbosity_example.py:8: AssertionError
+ ____________________________ test_numbers_fail _____________________________
+
+ 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
+ E AssertionError: assert {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} == {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
+ 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 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}
+
+ test_verbosity_example.py:14: AssertionError
+ ___________________________ test_long_text_fail ____________________________
+
+ def test_long_text_fail():
+ long_text = "Lorem ipsum dolor sit amet " * 10
+ > assert "hello world" in long_text
+ E 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 '
+
+ 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...
+ ======================= 3 failed, 1 passed in 0.12s ========================
+
+Notice now that:
+
+* Each test inside the file gets its own line in the output.
+* ``test_words_fail`` gives the same output as before in this case.
+* ``test_numbers_fail`` now shows a full text diff of the two dictionaries.
+* ``test_long_text_fail`` also doesn't truncate on the right hand side as before, but now pytest won't truncate any
+ text at all, regardless of its size.
+
+Those were examples of how verbosity affects normal test session output, but verbosity also is used in other
+situations, for example you are shown even fixtures that start with ``_`` if you use ``pytest --fixtures -v``.
+
+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.detailed_failed_tests_usage`:
+
+Producing a detailed summary report
+--------------------------------------------------
+
+The ``-r`` flag can be used to display a "short test summary info" at the end of the test session,
+making it easy in large test suites to get a clear picture of all failures, skips, xfails, etc.
+
+It defaults to ``fE`` to list failures and errors.
+
+.. regendoc:wipe
+
+Example:
+
+.. code-block:: python
+
+ # content of test_example.py
+ import pytest
+
+
+ @pytest.fixture
+ def error_fixture():
+ assert 0
+
+
+ def test_ok():
+ print("ok")
+
+
+ def test_fail():
+ assert 0
+
+
+ def test_error(error_fixture):
+ pass
+
+
+ def test_skip():
+ pytest.skip("skipping this test")
+
+
+ def test_xfail():
+ pytest.xfail("xfailing this test")
+
+
+ @pytest.mark.xfail(reason="always xfail")
+ def test_xpass():
+ pass
+
+
+.. code-block:: pytest
+
+ $ pytest -ra
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 6 items
+
+ test_example.py .FEsxX [100%]
+
+ ================================== ERRORS ==================================
+ _______________________ ERROR at setup of test_error _______________________
+
+ @pytest.fixture
+ def error_fixture():
+ > assert 0
+ E assert 0
+
+ test_example.py:6: AssertionError
+ ================================= FAILURES =================================
+ ________________________________ test_fail _________________________________
+
+ def test_fail():
+ > assert 0
+ E assert 0
+
+ test_example.py:14: AssertionError
+ ========================= 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
+ 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 ===
+
+The ``-r`` options accepts a number of characters after it, with ``a`` used
+above meaning "all except passes".
+
+Here is the full list of available characters that can be used:
+
+ - ``f`` - failed
+ - ``E`` - error
+ - ``s`` - skipped
+ - ``x`` - xfailed
+ - ``X`` - xpassed
+ - ``p`` - passed
+ - ``P`` - passed with output
+
+Special characters for (de)selection of groups:
+
+ - ``a`` - all except ``pP``
+ - ``A`` - all
+ - ``N`` - none, this can be used to display nothing (since ``fE`` is the default)
+
+More than one character can be used, so for example to only see failed and skipped tests, you can execute:
+
+.. code-block:: pytest
+
+ $ pytest -rfs
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 6 items
+
+ test_example.py .FEsxX [100%]
+
+ ================================== ERRORS ==================================
+ _______________________ ERROR at setup of test_error _______________________
+
+ @pytest.fixture
+ def error_fixture():
+ > assert 0
+ E assert 0
+
+ test_example.py:6: AssertionError
+ ================================= FAILURES =================================
+ ________________________________ test_fail _________________________________
+
+ def test_fail():
+ > assert 0
+ E assert 0
+
+ test_example.py:14: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_example.py::test_fail - assert 0
+ SKIPPED [1] test_example.py:22: skipping this test
+ == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===
+
+Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had
+captured output:
+
+.. code-block:: pytest
+
+ $ pytest -rpP
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 6 items
+
+ test_example.py .FEsxX [100%]
+
+ ================================== ERRORS ==================================
+ _______________________ ERROR at setup of test_error _______________________
+
+ @pytest.fixture
+ def error_fixture():
+ > assert 0
+ E assert 0
+
+ test_example.py:6: AssertionError
+ ================================= FAILURES =================================
+ ________________________________ test_fail _________________________________
+
+ def test_fail():
+ > assert 0
+ E assert 0
+
+ test_example.py:14: AssertionError
+ ================================== PASSES ==================================
+ _________________________________ test_ok __________________________________
+ --------------------------- Captured stdout call ---------------------------
+ ok
+ ========================= short test summary info ==========================
+ PASSED test_example.py::test_ok
+ == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===
+
+Creating resultlog format files
+--------------------------------------------------
+
+To create plain-text machine-readable result files you can issue:
+
+.. code-block:: bash
+
+ pytest --resultlog=path
+
+and look at the content at the ``path`` location. Such files are used e.g.
+by the `PyPy-test`_ web page to show test results over several revisions.
+
+.. warning::
+
+ This option is rarely used and is scheduled for removal in pytest 6.0.
+
+ If you use this option, consider using the new `pytest-reportlog <https://github.com/pytest-dev/pytest-reportlog>`__ plugin instead.
+
+ See :ref:`the deprecation docs <resultlog deprecated>` for more information.
+
+
+.. _`PyPy-test`: http://buildbot.pypy.org/summary
+
+
+Creating JUnitXML format files
+----------------------------------------------------
+
+To create result files which can be read by Jenkins_ or other Continuous
+integration servers, use this invocation:
+
+.. code-block:: bash
+
+ pytest --junitxml=path
+
+to create an XML file at ``path``.
+
+
+
+To set the name of the root test suite xml item, you can configure the ``junit_suite_name`` option in your config file:
+
+.. code-block:: ini
+
+ [pytest]
+ junit_suite_name = my_suite
+
+.. versionadded:: 4.0
+
+JUnit XML specification seems to indicate that ``"time"`` attribute
+should report total test execution times, including setup and teardown
+(`1 <http://windyroad.com.au/dl/Open%20Source/JUnit.xsd>`_, `2
+<https://www.ibm.com/support/knowledgecenter/en/SSQ2R2_14.1.0/com.ibm.rsar.analysis.codereview.cobol.doc/topics/cac_useresults_junit.html>`_).
+It is the default pytest behavior. To report just call durations
+instead, configure the ``junit_duration_report`` option like this:
+
+.. code-block:: ini
+
+ [pytest]
+ junit_duration_report = call
+
+.. _record_property example:
+
+record_property
+~~~~~~~~~~~~~~~~~
+
+If you want to log additional information for a test, you can use the
+``record_property`` fixture:
+
+.. code-block:: python
+
+ def test_function(record_property):
+ record_property("example_key", 1)
+ assert True
+
+This will add an extra property ``example_key="1"`` to the generated
+``testcase`` tag:
+
+.. code-block:: xml
+
+ <testcase classname="test_function" file="test_function.py" line="0" name="test_function" time="0.0009">
+ <properties>
+ <property name="example_key" value="1" />
+ </properties>
+ </testcase>
+
+Alternatively, you can integrate this functionality with custom markers:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+
+ def pytest_collection_modifyitems(session, config, items):
+ for item in items:
+ for marker in item.iter_markers(name="test_id"):
+ test_id = marker.args[0]
+ item.user_properties.append(("test_id", test_id))
+
+And in your tests:
+
+.. code-block:: python
+
+ # content of test_function.py
+ import pytest
+
+
+ @pytest.mark.test_id(1501)
+ def test_function():
+ assert True
+
+Will result in:
+
+.. code-block:: xml
+
+ <testcase classname="test_function" file="test_function.py" line="0" name="test_function" time="0.0009">
+ <properties>
+ <property name="test_id" value="1501" />
+ </properties>
+ </testcase>
+
+.. warning::
+
+ Please note that using this feature will break schema verifications for the latest JUnitXML schema.
+ This might be a problem when used with some CI servers.
+
+
+record_xml_attribute
+~~~~~~~~~~~~~~~~~~~~~~~
+
+To add an additional xml attribute to a testcase element, you can use
+``record_xml_attribute`` fixture. This can also be used to override existing values:
+
+.. code-block:: python
+
+ def test_function(record_xml_attribute):
+ record_xml_attribute("assertions", "REQ-1234")
+ record_xml_attribute("classname", "custom_classname")
+ print("hello world")
+ assert True
+
+Unlike ``record_property``, this will not add a new child element.
+Instead, this will add an attribute ``assertions="REQ-1234"`` inside the generated
+``testcase`` tag and override the default ``classname`` with ``"classname=custom_classname"``:
+
+.. code-block:: xml
+
+ <testcase classname="custom_classname" file="test_function.py" line="0" name="test_function" time="0.003" assertions="REQ-1234">
+ <system-out>
+ hello world
+ </system-out>
+ </testcase>
+
+.. warning::
+
+ ``record_xml_attribute`` is an experimental feature, and its interface might be replaced
+ by something more powerful and general in future versions. The
+ functionality per-se will be kept, however.
+
+ Using this over ``record_xml_property`` can help when using ci tools to parse the xml report.
+ However, some parsers are quite strict about the elements and attributes that are allowed.
+ Many tools use an xsd schema (like the example below) to validate incoming xml.
+ Make sure you are using attribute names that are allowed by your parser.
+
+ Below is the Scheme used by Jenkins to validate the XML report:
+
+ .. code-block:: xml
+
+ <xs:element name="testcase">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element ref="skipped" minOccurs="0" maxOccurs="1"/>
+ <xs:element ref="error" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element ref="failure" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element ref="system-out" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element ref="system-err" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ <xs:attribute name="name" type="xs:string" use="required"/>
+ <xs:attribute name="assertions" type="xs:string" use="optional"/>
+ <xs:attribute name="time" type="xs:string" use="optional"/>
+ <xs:attribute name="classname" type="xs:string" use="optional"/>
+ <xs:attribute name="status" type="xs:string" use="optional"/>
+ </xs:complexType>
+ </xs:element>
+
+.. warning::
+
+ Please note that using this feature will break schema verifications for the latest JUnitXML schema.
+ This might be a problem when used with some CI servers.
+
+.. _record_testsuite_property example:
+
+record_testsuite_property
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. versionadded:: 4.5
+
+If you want to add a properties node at the test-suite level, which may contains properties
+that are relevant to all tests, you can use the ``record_testsuite_property`` session-scoped fixture:
+
+The ``record_testsuite_property`` session-scoped fixture can be used to add properties relevant
+to all tests.
+
+.. code-block:: python
+
+ import pytest
+
+
+ @pytest.fixture(scope="session", autouse=True)
+ def log_global_env_facts(record_testsuite_property):
+ record_testsuite_property("ARCH", "PPC")
+ record_testsuite_property("STORAGE_TYPE", "CEPH")
+
+
+ class TestMe:
+ def test_foo(self):
+ assert True
+
+The fixture is a callable which receives ``name`` and ``value`` of a ``<property>`` tag
+added at the test-suite level of the generated xml:
+
+.. code-block:: xml
+
+ <testsuite errors="0" failures="0" name="pytest" skipped="0" tests="1" time="0.006">
+ <properties>
+ <property name="ARCH" value="PPC"/>
+ <property name="STORAGE_TYPE" value="CEPH"/>
+ </properties>
+ <testcase classname="test_me.TestMe" file="test_me.py" line="16" name="test_foo" time="0.000243663787842"/>
+ </testsuite>
+
+``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.
+
+The generated XML is compatible with the latest ``xunit`` standard, contrary to `record_property`_
+and `record_xml_attribute`_.
+
+
+Sending test report to an online pastebin service
+--------------------------------------------------
+
+**Creating a URL for each test failure**:
+
+.. code-block:: bash
+
+ pytest --pastebin=failed
+
+This will submit test run information to a remote Paste service and
+provide a URL for each failure. You may select tests as usual or add
+for example ``-x`` if you only want to send one particular failure.
+
+**Creating a URL for a whole test session log**:
+
+.. code-block:: bash
+
+ pytest --pastebin=all
+
+Currently only pasting to the https://bpaste.net/ service is implemented.
+
+.. versionchanged:: 5.2
+
+If creating the URL fails for any reason, a warning is generated instead of failing the
+entire test suite.
+
+.. _jenkins: https://jenkins-ci.org
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/parametrize.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/parametrize.rst
new file mode 100644
index 0000000000..a0c9968428
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/parametrize.rst
@@ -0,0 +1,298 @@
+
+.. _`test generators`:
+.. _`parametrizing-tests`:
+.. _`parametrized test functions`:
+.. _`parametrize`:
+
+.. _`parametrize-basics`:
+
+How to parametrize fixtures and test functions
+==========================================================================
+
+pytest enables test parametrization at several levels:
+
+- :py:func:`pytest.fixture` allows one to :ref:`parametrize fixture
+ functions <fixture-parametrize>`.
+
+* `@pytest.mark.parametrize`_ allows one to define multiple sets of
+ arguments and fixtures at the test function or class.
+
+* `pytest_generate_tests`_ allows one to define custom parametrization
+ schemes or extensions.
+
+.. _parametrizemark:
+.. _`@pytest.mark.parametrize`:
+
+
+``@pytest.mark.parametrize``: parametrizing test functions
+---------------------------------------------------------------------
+
+.. regendoc: wipe
+
+
+
+ Several improvements.
+
+The builtin :ref:`pytest.mark.parametrize ref` decorator enables
+parametrization of arguments for a test function. Here is a typical example
+of a test function that implements checking that a certain input leads
+to an expected output:
+
+.. code-block:: python
+
+ # content of test_expectation.py
+ import pytest
+
+
+ @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
+ def test_eval(test_input, expected):
+ assert eval(test_input) == expected
+
+Here, the ``@parametrize`` decorator defines three different ``(test_input,expected)``
+tuples so that the ``test_eval`` function will run three times using
+them in turn:
+
+.. code-block:: pytest
+
+ $ pytest
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 3 items
+
+ test_expectation.py ..F [100%]
+
+ ================================= FAILURES =================================
+ ____________________________ test_eval[6*9-42] _____________________________
+
+ test_input = '6*9', expected = 42
+
+ @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
+ def test_eval(test_input, expected):
+ > assert eval(test_input) == expected
+ E AssertionError: assert 54 == 42
+ E + where 54 = eval('6*9')
+
+ test_expectation.py:6: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_expectation.py::test_eval[6*9-42] - AssertionError: assert 54...
+ ======================= 1 failed, 2 passed in 0.12s ========================
+
+.. note::
+
+ Parameter values are passed as-is to tests (no copy whatsoever).
+
+ For example, if you pass a list or a dict as a parameter value, and
+ the test case code mutates it, the mutations will be reflected in subsequent
+ test case calls.
+
+.. note::
+
+ pytest by default escapes any non-ascii characters used in unicode strings
+ for the parametrization because it has several downsides.
+ If however you would like to use unicode strings in parametrization
+ and see them in the terminal as is (non-escaped), use this option
+ in your ``pytest.ini``:
+
+ .. code-block:: ini
+
+ [pytest]
+ disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
+
+ Keep in mind however that this might cause unwanted side effects and
+ even bugs depending on the OS used and plugins currently installed,
+ so use it at your own risk.
+
+
+As designed in this example, only one pair of input/output values fails
+the simple test function. And as usual with test function arguments,
+you can see the ``input`` and ``output`` values in the traceback.
+
+Note that you could also use the parametrize marker on a class or a module
+(see :ref:`mark`) which would invoke several functions with the argument sets,
+for instance:
+
+
+.. code-block:: python
+
+ import pytest
+
+
+ @pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])
+ class TestClass:
+ def test_simple_case(self, n, expected):
+ assert n + 1 == expected
+
+ def test_weird_simple_case(self, n, expected):
+ assert (n * 1) + 1 == expected
+
+
+To parametrize all tests in a module, you can assign to the :globalvar:`pytestmark` global variable:
+
+
+.. code-block:: python
+
+ import pytest
+
+ pytestmark = pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])
+
+
+ class TestClass:
+ def test_simple_case(self, n, expected):
+ assert n + 1 == expected
+
+ def test_weird_simple_case(self, n, expected):
+ assert (n * 1) + 1 == expected
+
+
+It is also possible to mark individual test instances within parametrize,
+for example with the builtin ``mark.xfail``:
+
+.. code-block:: python
+
+ # content of test_expectation.py
+ import pytest
+
+
+ @pytest.mark.parametrize(
+ "test_input,expected",
+ [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)],
+ )
+ def test_eval(test_input, expected):
+ assert eval(test_input) == expected
+
+Let's run this:
+
+.. code-block:: pytest
+
+ $ pytest
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 3 items
+
+ test_expectation.py ..x [100%]
+
+ ======================= 2 passed, 1 xfailed in 0.12s =======================
+
+The one parameter set which caused a failure previously now
+shows up as an "xfailed" (expected to fail) test.
+
+In case the values provided to ``parametrize`` result in an empty list - for
+example, if they're dynamically generated by some function - the behaviour of
+pytest is defined by the :confval:`empty_parameter_set_mark` option.
+
+To get all combinations of multiple parametrized arguments you can stack
+``parametrize`` decorators:
+
+.. code-block:: python
+
+ import pytest
+
+
+ @pytest.mark.parametrize("x", [0, 1])
+ @pytest.mark.parametrize("y", [2, 3])
+ def test_foo(x, y):
+ pass
+
+This will run the test with the arguments set to ``x=0/y=2``, ``x=1/y=2``,
+``x=0/y=3``, and ``x=1/y=3`` exhausting parameters in the order of the decorators.
+
+.. _`pytest_generate_tests`:
+
+Basic ``pytest_generate_tests`` example
+---------------------------------------------
+
+Sometimes you may want to implement your own parametrization scheme
+or implement some dynamism for determining the parameters or scope
+of a fixture. For this, you can use the ``pytest_generate_tests`` hook
+which is called when collecting a test function. Through the passed in
+``metafunc`` object you can inspect the requesting test context and, most
+importantly, you can call ``metafunc.parametrize()`` to cause
+parametrization.
+
+For example, let's say we want to run a test taking string inputs which
+we want to set via a new ``pytest`` command line option. Let's first write
+a simple test accepting a ``stringinput`` fixture function argument:
+
+.. code-block:: python
+
+ # content of test_strings.py
+
+
+ def test_valid_string(stringinput):
+ assert stringinput.isalpha()
+
+Now we add a ``conftest.py`` file containing the addition of a
+command line option and the parametrization of our test function:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+
+ def pytest_addoption(parser):
+ parser.addoption(
+ "--stringinput",
+ action="append",
+ default=[],
+ help="list of stringinputs to pass to test functions",
+ )
+
+
+ def pytest_generate_tests(metafunc):
+ if "stringinput" in metafunc.fixturenames:
+ metafunc.parametrize("stringinput", metafunc.config.getoption("stringinput"))
+
+If we now pass two stringinput values, our test will run twice:
+
+.. code-block:: pytest
+
+ $ pytest -q --stringinput="hello" --stringinput="world" test_strings.py
+ .. [100%]
+ 2 passed in 0.12s
+
+Let's also run with a stringinput that will lead to a failing test:
+
+.. code-block:: pytest
+
+ $ pytest -q --stringinput="!" test_strings.py
+ F [100%]
+ ================================= FAILURES =================================
+ ___________________________ test_valid_string[!] ___________________________
+
+ stringinput = '!'
+
+ def test_valid_string(stringinput):
+ > assert stringinput.isalpha()
+ E AssertionError: assert False
+ E + where False = <built-in method isalpha of str object at 0xdeadbeef0001>()
+ E + where <built-in method isalpha of str object at 0xdeadbeef0001> = '!'.isalpha
+
+ test_strings.py:4: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_strings.py::test_valid_string[!] - AssertionError: assert False
+ 1 failed in 0.12s
+
+As expected our test function fails.
+
+If you don't specify a stringinput it will be skipped because
+``metafunc.parametrize()`` will be called with an empty parameter
+list:
+
+.. code-block:: pytest
+
+ $ pytest -q -rs test_strings.py
+ s [100%]
+ ========================= short test summary info ==========================
+ SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at /home/sweet/project/test_strings.py:2
+ 1 skipped in 0.12s
+
+Note that when calling ``metafunc.parametrize`` multiple times with different parameter sets, all parameter names across
+those sets cannot be duplicated, otherwise an error will be raised.
+
+More examples
+-------------
+
+For further examples, you might want to look at :ref:`more
+parametrization examples <paramexamples>`.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/plugins.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/plugins.rst
new file mode 100644
index 0000000000..cae737e96e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/plugins.rst
@@ -0,0 +1,136 @@
+.. _`external plugins`:
+.. _`extplugins`:
+.. _`using plugins`:
+
+How to install and use plugins
+===============================
+
+This section talks about installing and using third party plugins.
+For writing your own plugins, please refer to :ref:`writing-plugins`.
+
+Installing a third party plugin can be easily done with ``pip``:
+
+.. code-block:: bash
+
+ pip install pytest-NAME
+ pip uninstall pytest-NAME
+
+If a plugin is installed, ``pytest`` automatically finds and integrates it,
+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.
+
+* :pypi:`pytest-twisted`: write tests
+ for `twisted <https://twistedmatrix.com/>`_ apps, starting a reactor and
+ processing deferreds from test functions.
+
+* :pypi:`pytest-cov`:
+ coverage reporting, compatible with distributed testing
+
+* :pypi:`pytest-xdist`:
+ to distribute tests to CPUs and remote hosts, to run in boxed
+ mode which allows to survive segmentation faults, to run in
+ looponfailing mode, automatically re-running failing tests
+ on file changes.
+
+* :pypi:`pytest-instafail`:
+ to report failures while the test run is happening.
+
+* :pypi:`pytest-bdd`:
+ to write tests using behaviour-driven testing.
+
+* :pypi:`pytest-timeout`:
+ to timeout tests based on function marks or global definitions.
+
+* :pypi:`pytest-pep8`:
+ a ``--pep8`` option to enable PEP8 compliance checking.
+
+* :pypi:`pytest-flakes`:
+ check source code with pyflakes.
+
+* :pypi:`oejskit`:
+ a plugin to run javascript unittests in live browsers.
+
+To see a complete list of all plugins with their latest testing
+status against different pytest and Python versions, please visit
+:ref:`plugin-list`.
+
+You may also discover more plugins through a `pytest- pypi.org search`_.
+
+.. _`pytest- pypi.org search`: https://pypi.org/search/?q=pytest-
+
+
+.. _`available installable plugins`:
+
+Requiring/Loading plugins in a test module or conftest file
+-----------------------------------------------------------
+
+You can require plugins in a test module or a conftest file using :globalvar:`pytest_plugins`:
+
+.. code-block:: python
+
+ pytest_plugins = ("myapp.testsupport.myplugin",)
+
+When the test module or conftest plugin is loaded the specified plugins
+will be loaded as well.
+
+.. note::
+
+ Requiring plugins using a ``pytest_plugins`` variable in non-root
+ ``conftest.py`` files is deprecated. See
+ :ref:`full explanation <requiring plugins in non-root conftests>`
+ in the Writing plugins section.
+
+.. note::
+ The name ``pytest_plugins`` is reserved and should not be used as a
+ name for a custom plugin module.
+
+
+.. _`findpluginname`:
+
+Finding out which plugins are active
+------------------------------------
+
+If you want to find out which plugins are active in your
+environment you can type:
+
+.. code-block:: bash
+
+ pytest --trace-config
+
+and will get an extended test header which shows activated plugins
+and their names. It will also print local plugins aka
+:ref:`conftest.py <conftest.py plugins>` files when they are loaded.
+
+.. _`cmdunregister`:
+
+Deactivating / unregistering a plugin by name
+---------------------------------------------
+
+You can prevent plugins from loading or unregister them:
+
+.. code-block:: bash
+
+ pytest -p no:NAME
+
+This means that any subsequent try to activate/load the named
+plugin will not work.
+
+If you want to unconditionally disable a plugin for a project, you can add
+this option to your ``pytest.ini`` file:
+
+.. code-block:: ini
+
+ [pytest]
+ addopts = -p no:NAME
+
+Alternatively to disable it only in certain environments (for example in a
+CI server), you can set ``PYTEST_ADDOPTS`` environment variable to
+``-p no:name``.
+
+See :ref:`findpluginname` for how to obtain the name of a plugin.
+
+.. _`builtin plugins`:
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/skipping.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/skipping.rst
new file mode 100644
index 0000000000..e2f59c77ae
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/skipping.rst
@@ -0,0 +1,430 @@
+.. _`skip and xfail`:
+
+.. _skipping:
+
+How to use skip and xfail to deal with tests that cannot succeed
+=================================================================
+
+You can mark test functions that cannot be run on certain platforms
+or that you expect to fail so pytest can deal with them accordingly and
+present a summary of the test session, while keeping the test suite *green*.
+
+A **skip** means that you expect your test to pass only if some conditions are met,
+otherwise pytest should skip running the test altogether. Common examples are skipping
+windows-only tests on non-windows platforms, or skipping tests that depend on an external
+resource which is not available at the moment (for example a database).
+
+An **xfail** means that you expect a test to fail for some reason.
+A common example is a test for a feature not yet implemented, or a bug not yet fixed.
+When a test passes despite being expected to fail (marked with ``pytest.mark.xfail``),
+it's an **xpass** and will be reported in the test summary.
+
+``pytest`` counts and lists *skip* and *xfail* tests separately. Detailed
+information about skipped/xfailed tests is not shown by default to avoid
+cluttering the output. You can use the ``-r`` option to see details
+corresponding to the "short" letters shown in the test progress:
+
+.. code-block:: bash
+
+ pytest -rxXs # show extra info on xfailed, xpassed, and skipped tests
+
+More details on the ``-r`` option can be found by running ``pytest -h``.
+
+(See :ref:`how to change command line options defaults`)
+
+.. _skipif:
+.. _skip:
+.. _`condition booleans`:
+
+Skipping test functions
+-----------------------
+
+
+
+The simplest way to skip a test function is to mark it with the ``skip`` decorator
+which may be passed an optional ``reason``:
+
+.. code-block:: python
+
+ @pytest.mark.skip(reason="no way of currently testing this")
+ def test_the_unknown():
+ ...
+
+
+Alternatively, it is also possible to skip imperatively during test execution or setup
+by calling the ``pytest.skip(reason)`` function:
+
+.. code-block:: python
+
+ def test_function():
+ if not valid_config():
+ pytest.skip("unsupported configuration")
+
+The imperative method is useful when it is not possible to evaluate the skip condition
+during import time.
+
+It is also possible to skip the whole module using
+``pytest.skip(reason, allow_module_level=True)`` at the module level:
+
+.. code-block:: python
+
+ import sys
+ import pytest
+
+ if not sys.platform.startswith("win"):
+ pytest.skip("skipping windows-only tests", allow_module_level=True)
+
+
+**Reference**: :ref:`pytest.mark.skip ref`
+
+``skipif``
+~~~~~~~~~~
+
+
+
+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:
+
+.. code-block:: python
+
+ import sys
+
+
+ @pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 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``.
+
+You can share ``skipif`` markers between modules. Consider this test module:
+
+.. code-block:: python
+
+ # content of test_mymodule.py
+ import mymodule
+
+ minversion = pytest.mark.skipif(
+ mymodule.__versioninfo__ < (1, 1), reason="at least mymodule-1.1 required"
+ )
+
+
+ @minversion
+ def test_function():
+ ...
+
+You can import the marker and reuse it in another test module:
+
+.. code-block:: python
+
+ # test_myothermodule.py
+ from test_mymodule import minversion
+
+
+ @minversion
+ 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
+throughout your test suite.
+
+Alternatively, you can use :ref:`condition strings
+<string conditions>` instead of booleans, but they can't be shared between modules easily
+so they are supported mainly for backward compatibility reasons.
+
+**Reference**: :ref:`pytest.mark.skipif ref`
+
+
+Skip all test functions of a class or module
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can use the ``skipif`` marker (as any other marker) on classes:
+
+.. code-block:: python
+
+ @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
+ class TestPosixCalls:
+ def test_function(self):
+ "will not be setup or run under 'win32' platform"
+
+If the condition is ``True``, this marker will produce a skip result for
+each of the test methods of that class.
+
+If you want to skip all test functions of a module, you may use the
+:globalvar:`pytestmark` global:
+
+.. code-block:: python
+
+ # test_module.py
+ pytestmark = pytest.mark.skipif(...)
+
+If multiple ``skipif`` decorators are applied to a test function, it
+will be skipped if any of the skip conditions is true.
+
+.. _`whole class- or module level`: mark.html#scoped-marking
+
+
+Skipping files or directories
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sometimes you may need to skip an entire file or directory, for example if the
+tests rely on Python version-specific features or contain code that you do not
+wish pytest to run. In this case, you must exclude the files and directories
+from collection. Refer to :ref:`customizing-test-collection` for more
+information.
+
+
+Skipping on a missing import dependency
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can skip tests on a missing import by using :ref:`pytest.importorskip ref`
+at module level, within a test, or test setup function.
+
+.. code-block:: python
+
+ docutils = pytest.importorskip("docutils")
+
+If ``docutils`` cannot be imported here, this will lead to a skip outcome of
+the test. You can also skip based on the version number of a library:
+
+.. code-block:: python
+
+ docutils = pytest.importorskip("docutils", minversion="0.3")
+
+The version will be read from the specified
+module's ``__version__`` attribute.
+
+Summary
+~~~~~~~
+
+Here's a quick guide on how to skip tests in a module in different situations:
+
+1. Skip all tests in a module unconditionally:
+
+ .. code-block:: python
+
+ pytestmark = pytest.mark.skip("all tests still WIP")
+
+2. Skip all tests in a module based on some condition:
+
+ .. code-block:: python
+
+ pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="tests for linux only")
+
+3. Skip all tests in a module if some import is missing:
+
+ .. code-block:: python
+
+ pexpect = pytest.importorskip("pexpect")
+
+
+.. _xfail:
+
+XFail: mark test functions as expected to fail
+----------------------------------------------
+
+You can use the ``xfail`` marker to indicate that you
+expect a test to fail:
+
+.. code-block:: python
+
+ @pytest.mark.xfail
+ 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
+passing" (``XPASS``) sections.
+
+Alternatively, you can also mark a test as ``XFAIL`` from within the test or its setup function
+imperatively:
+
+.. code-block:: python
+
+ def test_function():
+ if not valid_config():
+ pytest.xfail("failing configuration (but should work)")
+
+.. code-block:: python
+
+ def test_function2():
+ import slow_module
+
+ if slow_module.slow_function():
+ pytest.xfail("slow_module taking too long")
+
+These two examples illustrate situations where you don't want to check for a condition
+at the module level, which is when a condition would otherwise be evaluated for marks.
+
+This will make ``test_function`` ``XFAIL``. Note that no other code is executed after
+the :func:`pytest.xfail` call, differently from the marker. That's because it is implemented
+internally by raising a known exception.
+
+**Reference**: :ref:`pytest.mark.xfail ref`
+
+
+``condition`` parameter
+~~~~~~~~~~~~~~~~~~~~~~~
+
+If a test is only expected to fail under a certain condition, you can pass
+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():
+ ...
+
+Note that you have to pass a reason as well (see the parameter description at
+:ref:`pytest.mark.xfail ref`).
+
+``reason`` parameter
+~~~~~~~~~~~~~~~~~~~~
+
+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():
+ ...
+
+
+``raises`` parameter
+~~~~~~~~~~~~~~~~~~~~
+
+If you want to be more specific as to why the test is failing, you can specify
+a single exception, or a tuple of exceptions, in the ``raises`` argument.
+
+.. code-block:: python
+
+ @pytest.mark.xfail(raises=RuntimeError)
+ def test_function():
+ ...
+
+Then the test will be reported as a regular failure if it fails with an
+exception not mentioned in ``raises``.
+
+``run`` parameter
+~~~~~~~~~~~~~~~~~
+
+If a test should be marked as xfail and reported as such but should not be
+even executed, use the ``run`` parameter as ``False``:
+
+.. code-block:: python
+
+ @pytest.mark.xfail(run=False)
+ def test_function():
+ ...
+
+This is specially useful for xfailing tests that are crashing the interpreter and should be
+investigated later.
+
+.. _`xfail strict tutorial`:
+
+``strict`` parameter
+~~~~~~~~~~~~~~~~~~~~
+
+Both ``XFAIL`` and ``XPASS`` don't fail the test suite by default.
+You can change this by setting the ``strict`` keyword-only parameter to ``True``:
+
+.. code-block:: python
+
+ @pytest.mark.xfail(strict=True)
+ def test_function():
+ ...
+
+
+This will make ``XPASS`` ("unexpectedly passing") results from this test to fail the test suite.
+
+You can change the default value of the ``strict`` parameter using the
+``xfail_strict`` ini option:
+
+.. code-block:: ini
+
+ [pytest]
+ xfail_strict=true
+
+
+Ignoring xfail
+~~~~~~~~~~~~~~
+
+By specifying on the commandline:
+
+.. code-block:: bash
+
+ pytest --runxfail
+
+you can force the running and reporting of an ``xfail`` marked test
+as if it weren't marked at all. This also causes :func:`pytest.xfail` to produce no effect.
+
+Examples
+~~~~~~~~
+
+Here is a simple test file with the several usages:
+
+.. literalinclude:: /example/xfail_demo.py
+
+Running it with the report-on-xfail option gives this output:
+
+.. FIXME: Use $ instead of ! again to re-enable regendoc once it's fixed:
+ https://github.com/pytest-dev/pytest/issues/8807
+
+.. code-block:: pytest
+
+ ! pytest -rx xfail_demo.py
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-1.x.y
+ cachedir: $PYTHON_PREFIX/.pytest_cache
+ rootdir: $REGENDOC_TMPDIR/example
+ collected 7 items
+
+ xfail_demo.py xxxxxxx [100%]
+
+ ========================= short test summary info ==========================
+ XFAIL xfail_demo.py::test_hello
+ XFAIL xfail_demo.py::test_hello2
+ reason: [NOTRUN]
+ XFAIL xfail_demo.py::test_hello3
+ condition: hasattr(os, 'sep')
+ XFAIL xfail_demo.py::test_hello4
+ bug 110
+ XFAIL xfail_demo.py::test_hello5
+ condition: pytest.__version__[0] != "17"
+ XFAIL xfail_demo.py::test_hello6
+ reason: reason
+ XFAIL xfail_demo.py::test_hello7
+ ============================ 7 xfailed in 0.12s ============================
+
+.. _`skip/xfail with parametrize`:
+
+Skip/xfail with parametrize
+---------------------------
+
+It is possible to apply markers like skip and xfail to individual
+test instances when using parametrize:
+
+.. code-block:: python
+
+ import sys
+ import pytest
+
+
+ @pytest.mark.parametrize(
+ ("n", "expected"),
+ [
+ (1, 2),
+ pytest.param(1, 0, marks=pytest.mark.xfail),
+ pytest.param(1, 3, marks=pytest.mark.xfail(reason="some bug")),
+ (2, 3),
+ (3, 4),
+ (4, 5),
+ pytest.param(
+ 10, 11, marks=pytest.mark.skipif(sys.version_info >= (3, 0), reason="py2k")
+ ),
+ ],
+ )
+ def test_increment(n, expected):
+ assert n + 1 == expected
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/tmp_path.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/tmp_path.rst
new file mode 100644
index 0000000000..ebd74d42e9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/tmp_path.rst
@@ -0,0 +1,139 @@
+
+.. _`tmp_path handling`:
+.. _tmp_path:
+
+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`_.
+
+``tmp_path`` is a :class:`pathlib.Path` object. Here is an example test usage:
+
+.. code-block:: python
+
+ # content of test_tmp_path.py
+ CONTENT = "content"
+
+
+ def test_create_file(tmp_path):
+ d = tmp_path / "sub"
+ d.mkdir()
+ p = d / "hello.txt"
+ p.write_text(CONTENT)
+ assert p.read_text() == CONTENT
+ assert len(list(tmp_path.iterdir())) == 1
+ assert 0
+
+Running this would result in a passed test except for the last
+``assert 0`` line which we use to look at values:
+
+.. code-block:: pytest
+
+ $ pytest test_tmp_path.py
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 1 item
+
+ test_tmp_path.py F [100%]
+
+ ================================= FAILURES =================================
+ _____________________________ test_create_file _____________________________
+
+ tmp_path = PosixPath('PYTEST_TMPDIR/test_create_file0')
+
+ def test_create_file(tmp_path):
+ d = tmp_path / "sub"
+ d.mkdir()
+ p = d / "hello.txt"
+ p.write_text(CONTENT)
+ assert p.read_text() == CONTENT
+ assert len(list(tmp_path.iterdir())) == 1
+ > assert 0
+ E assert 0
+
+ test_tmp_path.py:11: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_tmp_path.py::test_create_file - assert 0
+ ============================ 1 failed in 0.12s =============================
+
+.. _`tmp_path_factory example`:
+
+The ``tmp_path_factory`` fixture
+--------------------------------
+
+The ``tmp_path_factory`` is a session-scoped fixture which can be used
+to create arbitrary temporary directories from any other fixture or test.
+
+For example, suppose your test suite needs a large image on disk, which is
+generated procedurally. Instead of computing the same image for each test
+that uses it into its own ``tmp_path``, you can generate it once per-session
+to save time:
+
+.. code-block:: python
+
+ # contents of conftest.py
+ import pytest
+
+
+ @pytest.fixture(scope="session")
+ def image_file(tmp_path_factory):
+ img = compute_expensive_image()
+ fn = tmp_path_factory.mktemp("data") / "img.png"
+ img.save(fn)
+ return fn
+
+
+ # contents of test_image.py
+ def test_histogram(image_file):
+ img = load_image(image_file)
+ # compute and test histogram
+
+See :ref:`tmp_path_factory API <tmp_path_factory factory api>` for details.
+
+.. _`tmpdir and tmpdir_factory`:
+.. _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``.
+
+See :fixture:`tmpdir <tmpdir>` :fixture:`tmpdir_factory <tmpdir_factory>`
+API for details.
+
+
+.. _`base temporary directory`:
+
+The default base temporary directory
+-----------------------------------------------
+
+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.
+
+You can override the default temporary directory setting like this:
+
+.. code-block:: bash
+
+ pytest --basetemp=mydir
+
+.. warning::
+
+ The contents of ``mydir`` will be completely removed, so make sure to use a directory
+ 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.
+
+.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/unittest.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/unittest.rst
new file mode 100644
index 0000000000..bff7511077
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/unittest.rst
@@ -0,0 +1,251 @@
+
+.. _`unittest.TestCase`:
+.. _`unittest`:
+
+How to use ``unittest``-based tests with pytest
+===============================================
+
+``pytest`` supports running Python ``unittest``-based tests out of the box.
+It's meant for leveraging existing ``unittest``-based test suites
+to use pytest as a test runner and also allow to incrementally adapt
+the test suite to take full advantage of pytest's features.
+
+To run an existing ``unittest``-style test suite using ``pytest``, type:
+
+.. code-block:: bash
+
+ pytest tests
+
+
+pytest will automatically collect ``unittest.TestCase`` subclasses and
+their ``test`` methods in ``test_*.py`` or ``*_test.py`` files.
+
+Almost all ``unittest`` features are supported:
+
+* ``@unittest.skip`` style decorators;
+* ``setUp/tearDown``;
+* ``setUpClass/tearDownClass``;
+* ``setUpModule/tearDownModule``;
+
+.. _`load_tests protocol`: https://docs.python.org/3/library/unittest.html#load-tests-protocol
+
+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
+-----------------------
+
+By running your test suite with pytest you can make use of several features,
+in most cases without having to modify existing code:
+
+* Obtain :ref:`more informative tracebacks <tbreportdemo>`;
+* :ref:`stdout and stderr <captures>` capturing;
+* :ref:`Test selection options <select-tests>` using ``-k`` and ``-m`` flags;
+* :ref:`maxfail`;
+* :ref:`--pdb <pdb-option>` command-line option for debugging on test failures
+ (see :ref:`note <pdb-unittest-note>` below);
+* Distribute tests to multiple CPUs using the :pypi:`pytest-xdist` plugin;
+* Use :ref:`plain assert-statements <assert>` instead of ``self.assert*`` functions
+ (:pypi:`unittest2pytest` is immensely helpful in this);
+
+
+pytest features in ``unittest.TestCase`` subclasses
+---------------------------------------------------
+
+The following pytest features work in ``unittest.TestCase`` subclasses:
+
+* :ref:`Marks <mark>`: :ref:`skip <skip>`, :ref:`skipif <skipif>`, :ref:`xfail <xfail>`;
+* :ref:`Auto-use fixtures <mixing-fixtures>`;
+
+The following pytest features **do not** work, and probably
+never will due to different design philosophies:
+
+* :ref:`Fixtures <fixture>` (except for ``autouse`` fixtures, see :ref:`below <mixing-fixtures>`);
+* :ref:`Parametrization <parametrize>`;
+* :ref:`Custom hooks <writing-plugins>`;
+
+
+Third party plugins may or may not work well, depending on the plugin and the test suite.
+
+.. _mixing-fixtures:
+
+Mixing pytest fixtures into ``unittest.TestCase`` subclasses using marks
+------------------------------------------------------------------------
+
+Running your unittest with ``pytest`` allows you to use its
+:ref:`fixture mechanism <fixture>` with ``unittest.TestCase`` style
+tests. Assuming you have at least skimmed the pytest fixture features,
+let's jump-start into an example that integrates a pytest ``db_class``
+fixture, setting up a class-cached database object, and then reference
+it from a unittest-style test:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+ # we define a fixture function below and it will be "used" by
+ # referencing its name from tests
+
+ import pytest
+
+
+ @pytest.fixture(scope="class")
+ def db_class(request):
+ class DummyDB:
+ pass
+
+ # set a class attribute on the invoking test context
+ request.cls.db = DummyDB()
+
+This defines a fixture function ``db_class`` which - if used - is
+called once for each test class and which sets the class-level
+``db`` attribute to a ``DummyDB`` instance. The fixture function
+achieves this by receiving a special ``request`` object which gives
+access to :ref:`the requesting test context <request-context>` such
+as the ``cls`` attribute, denoting the class from which the fixture
+is used. This architecture de-couples fixture writing from actual test
+code and allows re-use of the fixture by a minimal reference, the fixture
+name. So let's write an actual ``unittest.TestCase`` class using our
+fixture definition:
+
+.. code-block:: python
+
+ # content of test_unittest_db.py
+
+ import unittest
+ import pytest
+
+
+ @pytest.mark.usefixtures("db_class")
+ class MyTest(unittest.TestCase):
+ def test_method1(self):
+ assert hasattr(self, "db")
+ assert 0, self.db # fail for demo purposes
+
+ def test_method2(self):
+ assert 0, self.db # fail for demo purposes
+
+The ``@pytest.mark.usefixtures("db_class")`` class-decorator makes sure that
+the pytest fixture function ``db_class`` is called once per class.
+Due to the deliberately failing assert statements, we can take a look at
+the ``self.db`` values in the traceback:
+
+.. code-block:: pytest
+
+ $ pytest test_unittest_db.py
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 2 items
+
+ test_unittest_db.py FF [100%]
+
+ ================================= FAILURES =================================
+ ___________________________ MyTest.test_method1 ____________________________
+
+ self = <test_unittest_db.MyTest testMethod=test_method1>
+
+ def test_method1(self):
+ assert hasattr(self, "db")
+ > assert 0, self.db # fail for demo purposes
+ E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
+ E assert 0
+
+ test_unittest_db.py:10: AssertionError
+ ___________________________ MyTest.test_method2 ____________________________
+
+ self = <test_unittest_db.MyTest testMethod=test_method2>
+
+ def test_method2(self):
+ > assert 0, self.db # fail for demo purposes
+ E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
+ E assert 0
+
+ test_unittest_db.py:13: 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...
+ ============================ 2 failed in 0.12s =============================
+
+This default pytest traceback shows that the two test methods
+share the same ``self.db`` instance which was our intention
+when writing the class-scoped fixture function above.
+
+
+Using autouse fixtures and accessing other fixtures
+---------------------------------------------------
+
+Although it's usually better to explicitly declare use of fixtures you need
+for a given test, you may sometimes want to have fixtures that are
+automatically used in a given context. After all, the traditional
+style of unittest-setup mandates the use of this implicit fixture writing
+and chances are, you are used to it or like it.
+
+You can flag fixture functions with ``@pytest.fixture(autouse=True)``
+and define the fixture function in the context where you want it used.
+Let's look at an ``initdir`` fixture which makes all test methods of a
+``TestCase`` class execute in a temporary directory with a
+pre-initialized ``samplefile.ini``. Our ``initdir`` fixture itself uses
+the pytest builtin :fixture:`tmp_path` fixture to delegate the
+creation of a per-test temporary directory:
+
+.. code-block:: python
+
+ # content of test_unittest_cleandir.py
+ import os
+ import pytest
+ import unittest
+
+
+ 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")
+
+ def test_method(self):
+ with open("samplefile.ini") as f:
+ s = f.read()
+ assert "testdata" in s
+
+Due to the ``autouse`` flag the ``initdir`` fixture function will be
+used for all methods of the class where it is defined. This is a
+shortcut for using a ``@pytest.mark.usefixtures("initdir")`` marker
+on the class like in the previous example.
+
+Running this test module ...:
+
+.. code-block:: pytest
+
+ $ pytest -q test_unittest_cleandir.py
+ . [100%]
+ 1 passed in 0.12s
+
+... gives us one passed test because the ``initdir`` fixture function
+was executed ahead of the ``test_method``.
+
+.. note::
+
+ ``unittest.TestCase`` methods cannot directly receive fixture
+ arguments as implementing that is likely to inflict
+ on the ability to run general unittest.TestCase test suites.
+
+ The above ``usefixtures`` and ``autouse`` examples should help to mix in
+ pytest fixtures into unittest suites.
+
+ You can also gradually move away from subclassing from ``unittest.TestCase`` to *plain asserts*
+ and then start to benefit from the full pytest feature set step by step.
+
+.. _pdb-unittest-note:
+
+.. note::
+
+ Due to architectural differences between the two frameworks, setup and
+ teardown for ``unittest``-based tests is performed during the ``call`` phase
+ of testing instead of in ``pytest``'s standard ``setup`` and ``teardown``
+ stages. This can be important to understand in some situations, particularly
+ when reasoning about errors. For example, if a ``unittest``-based suite
+ exhibits errors during setup, ``pytest`` will report no errors during its
+ ``setup`` phase and will instead raise the error during ``call``.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/usage.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/usage.rst
new file mode 100644
index 0000000000..3522b258dc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/usage.rst
@@ -0,0 +1,214 @@
+
+.. _usage:
+
+How to invoke pytest
+==========================================
+
+.. seealso:: :ref:`Complete pytest command-line flag reference <command-line-flags>`
+
+In general, pytest is invoked with the command ``pytest`` (see below for :ref:`other ways to invoke pytest
+<invoke-other>`). This will execute all tests in all files whose names follow the form ``test_*.py`` or ``\*_test.py``
+in the current directory and its subdirectories. More generally, pytest follows :ref:`standard test discovery rules
+<test discovery>`.
+
+
+.. _select-tests:
+
+Specifying which tests to run
+------------------------------
+
+Pytest supports several ways to run and select tests from the command-line.
+
+**Run tests in a module**
+
+.. code-block:: bash
+
+ pytest test_mod.py
+
+**Run tests in a directory**
+
+.. code-block:: bash
+
+ pytest testing/
+
+**Run tests by keyword expressions**
+
+.. code-block:: bash
+
+ 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``.
+
+.. _nodeids:
+
+**Run tests by node ids**
+
+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.
+
+To run a specific test within a module:
+
+.. code-block:: bash
+
+ pytest test_mod.py::test_func
+
+
+Another example specifying a test method in the command line:
+
+.. code-block:: bash
+
+ pytest test_mod.py::TestClass::test_method
+
+**Run tests by marker expressions**
+
+.. code-block:: bash
+
+ pytest -m slow
+
+Will run all tests which are decorated with the ``@pytest.mark.slow`` decorator.
+
+For more information see :ref:`marks <mark>`.
+
+**Run tests from packages**
+
+.. code-block:: bash
+
+ pytest --pyargs pkg.testing
+
+This will import ``pkg.testing`` and use its filesystem location to find and run tests from.
+
+
+Getting help on version, option names, environment variables
+--------------------------------------------------------------
+
+.. code-block:: bash
+
+ pytest --version # shows where pytest was imported from
+ pytest --fixtures # show available builtin function arguments
+ pytest -h | --help # show help on command line and config file options
+
+
+.. _durations:
+
+Profiling test execution duration
+-------------------------------------
+
+.. versionchanged:: 6.0
+
+To get a list of the slowest 10 test durations over 1.0s long:
+
+.. code-block:: bash
+
+ pytest --durations=10 --durations-min=1.0
+
+By default, pytest will not show test durations that are too small (<0.005s) unless ``-vv`` is passed on the command-line.
+
+
+Managing loading of plugins
+-------------------------------
+
+Early loading plugins
+~~~~~~~~~~~~~~~~~~~~~~~
+
+You can early-load plugins (internal and external) explicitly in the command-line with the ``-p`` option::
+
+ pytest -p mypluginmodule
+
+The option receives a ``name`` parameter, which can be:
+
+* A full module dotted name, for example ``myproject.plugins``. This dotted name must be importable.
+* The entry-point name of a plugin. This is the name passed to ``setuptools`` when the plugin is
+ registered. For example to early-load the :pypi:`pytest-cov` plugin you can use::
+
+ pytest -p pytest_cov
+
+
+Disabling plugins
+~~~~~~~~~~~~~~~~~~
+
+To disable loading specific plugins at invocation time, use the ``-p`` option
+together with the prefix ``no:``.
+
+Example: to disable loading the plugin ``doctest``, which is responsible for
+executing doctest tests from text files, invoke pytest like this:
+
+.. code-block:: bash
+
+ pytest -p no:doctest
+
+
+.. _invoke-other:
+
+Other ways of calling pytest
+-----------------------------------------------------
+
+.. _invoke-python:
+
+Calling pytest through ``python -m pytest``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can invoke testing through the Python interpreter from the command line:
+
+.. code-block:: text
+
+ python -m pytest [...]
+
+This is almost equivalent to invoking the command line script ``pytest [...]``
+directly, except that calling via ``python`` will also add the current directory to ``sys.path``.
+
+
+.. _`pytest.main-usage`:
+
+Calling pytest from Python code
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can invoke ``pytest`` from Python code directly:
+
+.. code-block:: python
+
+ retcode = pytest.main()
+
+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:
+
+.. code-block:: python
+
+ retcode = pytest.main(["-x", "mytestdir"])
+
+You can specify additional plugins to ``pytest.main``:
+
+.. code-block:: python
+
+ # content of myinvoke.py
+ import pytest
+ import sys
+
+
+ class MyPlugin:
+ def pytest_sessionfinish(self):
+ print("*** test run reporting finishing")
+
+
+ if __name__ == "__main__":
+ sys.exit(pytest.main(["-qq"], plugins=[MyPlugin()]))
+
+Running it will show that ``MyPlugin`` was added and its
+hook was invoked:
+
+.. code-block:: pytest
+
+ $ python myinvoke.py
+ *** test run reporting finishing
+
+
+.. note::
+
+ Calling ``pytest.main()`` will result in importing your tests and any modules
+ that they import. Due to the caching mechanism of python's import system,
+ making subsequent calls to ``pytest.main()`` from the same process will not
+ reflect changes to those files between the calls. For this reason, making
+ multiple calls to ``pytest.main()`` from the same process (in order to re-run
+ tests, for example) is not recommended.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/writing_hook_functions.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/writing_hook_functions.rst
new file mode 100644
index 0000000000..f615fced86
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/writing_hook_functions.rst
@@ -0,0 +1,352 @@
+.. _`writinghooks`:
+
+Writing hook functions
+======================
+
+
+.. _validation:
+
+hook function validation and execution
+--------------------------------------
+
+pytest calls hook functions from registered plugins for any
+given hook specification. Let's look at a typical hook function
+for the ``pytest_collection_modifyitems(session, config,
+items)`` hook which pytest calls after collection of all test items is
+completed.
+
+When we implement a ``pytest_collection_modifyitems`` function in our plugin
+pytest will during registration verify that you use argument
+names which match the specification and bail out if not.
+
+Let's look at a possible implementation:
+
+.. code-block:: python
+
+ def pytest_collection_modifyitems(config, items):
+ # called after collection is completed
+ # you can modify the ``items`` list
+ ...
+
+Here, ``pytest`` will pass in ``config`` (the pytest config object)
+and ``items`` (the list of collected test items) but will not pass
+in the ``session`` argument because we didn't list it in the function
+signature. This dynamic "pruning" of arguments allows ``pytest`` to
+be "future-compatible": we can introduce new hook named parameters without
+breaking the signatures of existing hook implementations. It is one of
+the reasons for the general long-lived compatibility of pytest plugins.
+
+Note that hook functions other than ``pytest_runtest_*`` are not
+allowed to raise exceptions. Doing so will break the pytest run.
+
+
+
+.. _firstresult:
+
+firstresult: stop at first non-None result
+-------------------------------------------
+
+Most calls to ``pytest`` hooks result in a **list of results** which contains
+all non-None results of the called hook functions.
+
+Some hook specifications use the ``firstresult=True`` option so that the hook
+call only executes until the first of N registered functions returns a
+non-None result which is then taken as result of the overall hook call.
+The remaining hook functions will not be called in this case.
+
+.. _`hookwrapper`:
+
+hookwrapper: 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).
+
+Here is an example definition of a hook wrapper:
+
+.. code-block:: python
+
+ import pytest
+
+
+ @pytest.hookimpl(hookwrapper=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
+
+ res = outcome.get_result() # will raise if outcome was exception
+
+ post_process_result(res)
+
+ outcome.force_result(new_res) # to override the return value to the plugin system
+
+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.
+
+For more information, consult the
+:ref:`pluggy documentation about hookwrappers <pluggy:hookwrappers>`.
+
+.. _plugin-hookorder:
+
+Hook function ordering / call example
+-------------------------------------
+
+For any given hook specification there may be more than one
+implementation and we thus generally view ``hook`` execution as a
+``1:N`` function call where ``N`` is the number of registered functions.
+There are ways to influence if a hook implementation comes before or
+after others, i.e. the position in the ``N``-sized list of functions:
+
+.. code-block:: python
+
+ # Plugin 1
+ @pytest.hookimpl(tryfirst=True)
+ def pytest_collection_modifyitems(items):
+ # will execute as early as possible
+ ...
+
+
+ # Plugin 2
+ @pytest.hookimpl(trylast=True)
+ def pytest_collection_modifyitems(items):
+ # will execute as late as possible
+ ...
+
+
+ # Plugin 3
+ @pytest.hookimpl(hookwrapper=True)
+ def pytest_collection_modifyitems(items):
+ # will execute even before the tryfirst one above!
+ outcome = yield
+ # will execute after all non-hookwrappers executed
+
+Here is the order of execution:
+
+1. Plugin3's pytest_collection_modifyitems called until the yield point
+ because it is a hook wrapper.
+
+2. Plugin1's pytest_collection_modifyitems is called because it is marked
+ with ``tryfirst=True``.
+
+3. Plugin2's pytest_collection_modifyitems is called because it is marked
+ with ``trylast=True`` (but even without this mark it would come after
+ 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.
+
+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.
+
+
+Declaring new hooks
+------------------------
+
+.. note::
+
+ 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
+ :noindex:
+
+Hooks are usually declared as do-nothing functions that contain only
+documentation describing when the hook will be called and what return values
+are expected. The names of the functions must start with `pytest_` otherwise pytest won't recognize them.
+
+Here's an example. Let's assume this code is in the ``sample_hook.py`` module.
+
+.. code-block:: python
+
+ def pytest_my_hook(config):
+ """
+ Receives the pytest config and does things with it
+ """
+
+To register the hooks with pytest they need to be structured in their own module or class. This
+class or module can then be passed to the ``pluginmanager`` using the ``pytest_addhooks`` function
+(which itself is a hook exposed by pytest).
+
+.. code-block:: python
+
+ def pytest_addhooks(pluginmanager):
+ """ This example assumes the hooks are grouped in the 'sample_hook' module. """
+ from my_app.tests import sample_hook
+
+ pluginmanager.add_hookspecs(sample_hook)
+
+For a real world example, see `newhooks.py`_ from `xdist <https://github.com/pytest-dev/pytest-xdist>`_.
+
+.. _`newhooks.py`: https://github.com/pytest-dev/pytest-xdist/blob/974bd566c599dc6a9ea291838c6f226197208b46/xdist/newhooks.py
+
+Hooks may be called both from fixtures or from other hooks. In both cases, hooks are called
+through the ``hook`` object, available in the ``config`` object. Most hooks receive a
+``config`` object directly, while fixtures may use the ``pytestconfig`` fixture which provides the same object.
+
+.. code-block:: python
+
+ @pytest.fixture()
+ def my_fixture(pytestconfig):
+ # call the hook called "pytest_my_hook"
+ # 'result' will be a list of return values from all registered functions.
+ result = pytestconfig.hook.pytest_my_hook(config=pytestconfig)
+
+.. note::
+ Hooks receive parameters using only keyword arguments.
+
+Now your hook is ready to be used. To register a function at the hook, other plugins or users must
+now simply define the function ``pytest_my_hook`` with the correct signature in their ``conftest.py``.
+
+Example:
+
+.. code-block:: python
+
+ def pytest_my_hook(config):
+ """
+ Print all active hooks to the screen.
+ """
+ print(config.hook)
+
+
+.. _`addoptionhooks`:
+
+
+Using hooks in pytest_addoption
+-------------------------------
+
+Occasionally, it is necessary to change the way in which command line options
+are defined by one plugin based on hooks in another plugin. For example,
+a plugin may expose a command line option for which another plugin needs
+to define the default value. The pluginmanager can be used to install and
+use hooks to accomplish this. The plugin would define and add the hooks
+and use pytest_addoption as follows:
+
+.. code-block:: python
+
+ # 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. """
+
+
+ # contents of myplugin.py
+
+
+ def pytest_addhooks(pluginmanager):
+ """ This example assumes the hooks are grouped in the 'hooks' module. """
+ from . import hooks
+
+ pluginmanager.add_hookspecs(hooks)
+
+
+ def pytest_addoption(parser, pluginmanager):
+ default_value = pluginmanager.hook.pytest_config_file_default_value()
+ parser.addoption(
+ "--config-file",
+ help="Config file to use, defaults to %(default)s",
+ default=default_value,
+ )
+
+The conftest.py that is using myplugin would simply define the hook as follows:
+
+.. code-block:: python
+
+ def pytest_config_file_default_value():
+ return "config.yaml"
+
+
+Optionally using hooks from 3rd party plugins
+---------------------------------------------
+
+Using new hooks from plugins as explained above might be a little tricky
+because of the standard :ref:`validation mechanism <validation>`:
+if you depend on a plugin that is not installed, validation will fail and
+the error message will not make much sense to your users.
+
+One approach is to defer the hook implementation to a new plugin instead of
+declaring the hook functions directly in your plugin module, for example:
+
+.. code-block:: python
+
+ # contents of myplugin.py
+
+
+ class DeferPlugin:
+ """Simple plugin to defer pytest-xdist hook functions."""
+
+ def pytest_testnodedown(self, node, error):
+ """standard xdist hook function."""
+
+
+ def pytest_configure(config):
+ if config.pluginmanager.hasplugin("xdist"):
+ config.pluginmanager.register(DeferPlugin())
+
+This has the added benefit of allowing you to conditionally install hooks
+depending on which plugins are installed.
+
+.. _plugin-stash:
+
+Storing data on items across hook functions
+-------------------------------------------
+
+Plugins often need to store data on :class:`~pytest.Item`\s in one hook
+implementation, and access it in another. One common solution is to just
+assign some private attribute directly on the item, but type-checkers like
+mypy frown upon this, and it may also cause conflicts with other plugins.
+So pytest offers a better way to do this, :attr:`item.stash <_pytest.nodes.Node.stash>`.
+
+To use the "stash" in your plugins, first create "stash keys" somewhere at the
+top level of your plugin:
+
+.. code-block:: python
+
+ been_there_key = pytest.StashKey[bool]()
+ done_that_key = pytest.StashKey[str]()
+
+then use the keys to stash your data at some point:
+
+.. code-block:: python
+
+ def pytest_runtest_setup(item: pytest.Item) -> None:
+ item.stash[been_there_key] = True
+ item.stash[done_that_key] = "no"
+
+and retrieve them at another point:
+
+.. code-block:: python
+
+ def pytest_runtest_teardown(item: pytest.Item) -> None:
+ if not item.stash[been_there_key]:
+ print("Oh?")
+ item.stash[done_that_key] = "yes!"
+
+Stashes are available on all node types (like :class:`~pytest.Class`,
+:class:`~pytest.Session`) and also on :class:`~pytest.Config`, if needed.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/writing_plugins.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/writing_plugins.rst
new file mode 100644
index 0000000000..b2d2b6563d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/writing_plugins.rst
@@ -0,0 +1,458 @@
+.. _plugins:
+.. _`writing-plugins`:
+
+Writing plugins
+===============
+
+It is easy to implement `local conftest plugins`_ for your own project
+or `pip-installable plugins`_ that can be used throughout many projects,
+including third party projects. Please refer to :ref:`using plugins` if you
+only want to use but not write plugins.
+
+A plugin contains one or multiple hook functions. :ref:`Writing hooks <writinghooks>`
+explains the basics and details of how you can write a hook function yourself.
+``pytest`` implements all aspects of configuration, collection, running and
+reporting by calling :ref:`well specified hooks <hook-reference>` of the following plugins:
+
+* builtin plugins: loaded from pytest's internal ``_pytest`` directory.
+
+* :ref:`external plugins <extplugins>`: modules discovered through
+ `setuptools entry points`_
+
+* `conftest.py plugins`_: modules auto-discovered in test directories
+
+In principle, each hook call is a ``1:N`` Python function call where ``N`` is the
+number of registered implementation functions for a given specification.
+All specifications and implementations follow the ``pytest_`` prefix
+naming convention, making them easy to distinguish and find.
+
+.. _`pluginorder`:
+
+Plugin discovery order at tool startup
+--------------------------------------
+
+``pytest`` loads plugin modules at tool startup in the following way:
+
+1. by scanning the command line for the ``-p no:name`` option
+ and *blocking* that plugin from being loaded (even builtin plugins can
+ be blocked this way). This happens before normal command-line parsing.
+
+2. by loading all builtin plugins.
+
+3. by scanning the command line for the ``-p name`` option
+ and loading the specified plugin. This happens before normal command-line parsing.
+
+4. by loading all plugins registered through `setuptools entry points`_.
+
+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:
+
+ - 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.
+
+ 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`:
+
+conftest.py: local per-directory plugins
+----------------------------------------
+
+Local ``conftest.py`` plugins contain directory-specific hook
+implementations. Hook Session and test running activities will
+invoke all hooks defined in ``conftest.py`` files closer to the
+root of the filesystem. Example of implementing the
+``pytest_runtest_setup`` hook so that is called for tests in the ``a``
+sub directory but not for other directories::
+
+ a/conftest.py:
+ def pytest_runtest_setup(item):
+ # called for running each test in 'a' directory
+ print("setting up", item)
+
+ a/test_sub.py:
+ def test_sub():
+ pass
+
+ test_flat.py:
+ def test_flat():
+ pass
+
+Here is how you might run it::
+
+     pytest test_flat.py --capture=no # will not show "setting up"
+ pytest a/test_sub.py --capture=no # will show "setting up"
+
+.. note::
+ If you have ``conftest.py`` files which do not reside in a
+ python package directory (i.e. one containing an ``__init__.py``) then
+ "import conftest" can be ambiguous because there might be other
+ ``conftest.py`` files as well on your ``PYTHONPATH`` or ``sys.path``.
+ It is thus good practice for projects to either put ``conftest.py``
+ under a package scope or to never import anything from a
+ ``conftest.py`` file.
+
+ 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.
+
+Writing your own plugin
+-----------------------
+
+If you want to write a plugin, there are many real-life examples
+you can copy from:
+
+* a custom collection example plugin: :ref:`yaml plugin`
+* builtin plugins which provide pytest's own functionality
+* many :ref:`external plugins <plugin-list>` providing additional features
+
+All of these plugins implement :ref:`hooks <hook-reference>` and/or :ref:`fixtures <fixture>`
+to extend and add functionality.
+
+.. note::
+ Make sure to check out the excellent
+ `cookiecutter-pytest-plugin <https://github.com/pytest-dev/cookiecutter-pytest-plugin>`_
+ project, which is a `cookiecutter template <https://github.com/audreyr/cookiecutter>`_
+ for authoring plugins.
+
+ The template provides an excellent starting point with a working plugin,
+ tests running with tox, a comprehensive README file as well as a
+ pre-configured entry-point.
+
+Also consider :ref:`contributing your plugin to pytest-dev<submitplugin>`
+once it has some happy users other than yourself.
+
+
+.. _`setuptools entry points`:
+.. _`pip-installable plugins`:
+
+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:
+
+.. sourcecode:: python
+
+ # sample ./setup.py file
+ from setuptools import setup
+
+ 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"],
+ )
+
+If a package is installed this way, ``pytest`` will load
+``myproject.pluginmodule`` as a plugin which can define
+:ref:`hooks <hook-reference>`.
+
+.. note::
+
+ Make sure to include ``Framework :: Pytest`` in your list of
+ `PyPI classifiers <https://pypi.org/classifiers/>`_
+ to make it easy for users to find your plugin.
+
+
+.. _assertion-rewriting:
+
+Assertion Rewriting
+-------------------
+
+One of the main features of ``pytest`` is the use of plain assert
+statements and the detailed introspection of expressions upon
+assertion failures. This is provided by "assertion rewriting" which
+modifies the parsed AST before it gets compiled to bytecode. This is
+done via a :pep:`302` import hook which gets installed early on when
+``pytest`` starts up and will perform this rewriting when modules get
+imported. However, since we do not want to test different bytecode
+from what you will run in production, this hook only rewrites test modules
+themselves (as defined by the :confval:`python_files` configuration option),
+and any modules which are part of plugins.
+Any other imported module will not be rewritten and normal assertion behaviour
+will happen.
+
+If you have assertion helpers in other modules where you would need
+assertion rewriting to be enabled you need to ask ``pytest``
+explicitly to rewrite this module before it gets imported.
+
+.. autofunction:: pytest.register_assert_rewrite
+ :noindex:
+
+This is especially important when you write a pytest plugin which is
+created using a package. The import hook only treats ``conftest.py``
+files and any modules which are listed in the ``pytest11`` entrypoint
+as plugins. As an example consider the following package::
+
+ pytest_foo/__init__.py
+ pytest_foo/plugin.py
+ pytest_foo/helper.py
+
+With the following typical ``setup.py`` extract:
+
+.. code-block:: python
+
+ setup(..., entry_points={"pytest11": ["foo = pytest_foo.plugin"]}, ...)
+
+In this case only ``pytest_foo/plugin.py`` will be rewritten. If the
+helper module also contains assert statements which need to be
+rewritten it needs to be marked as such, before it gets imported.
+This is easiest by marking it for rewriting inside the
+``__init__.py`` module, which will always be imported first when a
+module inside a package is imported. This way ``plugin.py`` can still
+import ``helper.py`` normally. The contents of
+``pytest_foo/__init__.py`` will then need to look like this:
+
+.. code-block:: python
+
+ import pytest
+
+ pytest.register_assert_rewrite("pytest_foo.helper")
+
+
+Requiring/Loading plugins in a test module or conftest file
+-----------------------------------------------------------
+
+You can require plugins in a test module or a ``conftest.py`` file using :globalvar:`pytest_plugins`:
+
+.. code-block:: python
+
+ pytest_plugins = ["name1", "name2"]
+
+When the test module or conftest plugin is loaded the specified plugins
+will be loaded as well. Any module can be blessed as a plugin, including internal
+application modules:
+
+.. code-block:: python
+
+ pytest_plugins = "myapp.testsupport.myplugin"
+
+:globalvar:`pytest_plugins` are processed recursively, so note that in the example above
+if ``myapp.testsupport.myplugin`` also declares :globalvar:`pytest_plugins`, the contents
+of the variable will also be loaded as plugins, and so on.
+
+.. _`requiring plugins in non-root conftests`:
+
+.. note::
+ Requiring plugins using :globalvar:`pytest_plugins` variable in non-root
+ ``conftest.py`` files is deprecated.
+
+ This is important because ``conftest.py`` files implement per-directory
+ hook implementations, but once a plugin is imported, it will affect the
+ entire directory tree. In order to avoid confusion, defining
+ :globalvar:`pytest_plugins` in any ``conftest.py`` file which is not located in the
+ tests root directory is deprecated, and will raise a warning.
+
+This mechanism makes it easy to share fixtures within applications or even
+external applications without the need to create external plugins using
+the ``setuptools``'s entry point technique.
+
+Plugins imported by :globalvar:`pytest_plugins` will also automatically be marked
+for assertion rewriting (see :func:`pytest.register_assert_rewrite`).
+However for this to have any effect the module must not be
+imported already; if it was already imported at the time the
+:globalvar:`pytest_plugins` statement is processed, a warning will result and
+assertions inside the plugin will not be rewritten. To fix this you
+can either call :func:`pytest.register_assert_rewrite` yourself before
+the module is imported, or you can arrange the code to delay the
+importing until after the plugin is registered.
+
+
+Accessing another plugin by name
+--------------------------------
+
+If a plugin wants to collaborate with code from
+another plugin it can obtain a reference through
+the plugin manager like this:
+
+.. sourcecode:: python
+
+ plugin = config.pluginmanager.get_plugin("name_of_plugin")
+
+If you want to look at the names of existing plugins, use
+the ``--trace-config`` option.
+
+
+.. _registering-markers:
+
+Registering custom markers
+--------------------------
+
+If your plugin uses any markers, you should register them so that they appear in
+pytest's help text and do not :ref:`cause spurious warnings <unknown-marks>`.
+For example, the following plugin would register ``cool_marker`` and
+``mark_with`` for all users:
+
+.. code-block:: python
+
+ def pytest_configure(config):
+ config.addinivalue_line("markers", "cool_marker: this one is for cool tests.")
+ config.addinivalue_line(
+ "markers", "mark_with(arg, arg2): this marker takes arguments."
+ )
+
+
+Testing plugins
+---------------
+
+pytest comes with a plugin named ``pytester`` that helps you write tests for
+your plugin code. The plugin is disabled by default, so you will have to enable
+it before you can use it.
+
+You can do so by adding the following line to a ``conftest.py`` file in your
+testing directory:
+
+.. code-block:: python
+
+ # content of conftest.py
+
+ pytest_plugins = ["pytester"]
+
+Alternatively you can invoke pytest with the ``-p pytester`` command line
+option.
+
+This will allow you to use the :py:class:`pytester <pytest.Pytester>`
+fixture for testing your plugin code.
+
+Let's demonstrate what you can do with the plugin with an example. Imagine we
+developed a plugin that provides a fixture ``hello`` which yields a function
+and we can invoke this function with one optional parameter. It will return a
+string value of ``Hello World!`` if we do not supply a value or ``Hello
+{value}!`` if we do supply a string value.
+
+.. code-block:: python
+
+ import pytest
+
+
+ def pytest_addoption(parser):
+ group = parser.getgroup("helloworld")
+ group.addoption(
+ "--name",
+ action="store",
+ dest="name",
+ default="World",
+ help='Default "name" for hello().',
+ )
+
+
+ @pytest.fixture
+ def hello(request):
+ name = request.config.getoption("name")
+
+ def _hello(name=None):
+ if not name:
+ name = request.config.getoption("name")
+ return "Hello {name}!".format(name=name)
+
+ return _hello
+
+
+Now the ``pytester`` fixture provides a convenient API for creating temporary
+``conftest.py`` files and test files. It also allows us to run the tests and
+return a result object, with which we can assert the tests' outcomes.
+
+.. code-block:: python
+
+ def test_hello(pytester):
+ """Make sure that our plugin works."""
+
+ # create a temporary conftest.py file
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(params=[
+ "Brianna",
+ "Andreas",
+ "Floris",
+ ])
+ def name(request):
+ return request.param
+ """
+ )
+
+ # create a temporary pytest test file
+ pytester.makepyfile(
+ """
+ def test_hello_default(hello):
+ assert hello() == "Hello World!"
+
+ def test_hello_name(hello, name):
+ assert hello(name) == "Hello {0}!".format(name)
+ """
+ )
+
+ # run all tests with pytest
+ result = pytester.runpytest()
+
+ # check that all 4 tests passed
+ result.assert_outcomes(passed=4)
+
+
+Additionally it is possible to copy examples to the ``pytester``'s isolated environment
+before running pytest on it. This way we can abstract the tested logic to separate files,
+which is especially useful for longer tests and/or longer ``conftest.py`` files.
+
+Note that for ``pytester.copy_example`` to work we need to set `pytester_example_dir`
+in our ``pytest.ini`` to tell pytest where to look for example files.
+
+.. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ pytester_example_dir = .
+
+
+.. code-block:: python
+
+ # content of test_example.py
+
+
+ def test_plugin(pytester):
+ pytester.copy_example("test_example.py")
+ pytester.runpytest("-k", "test_example")
+
+
+ def test_example():
+ pass
+
+.. code-block:: pytest
+
+ $ 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
+ collected 2 items
+
+ test_example.py .. [100%]
+
+ ============================ 2 passed in 0.12s =============================
+
+For more information about the result object that ``runpytest()`` returns, and
+the methods that it provides please check out the :py:class:`RunResult
+<_pytest.pytester.RunResult>` documentation.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/xunit_setup.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/xunit_setup.rst
new file mode 100644
index 0000000000..5a97b2c85f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/how-to/xunit_setup.rst
@@ -0,0 +1,117 @@
+
+.. _`classic xunit`:
+.. _xunitsetup:
+
+How to implement xunit-style set-up
+========================================
+
+This section describes a classic and popular way how you can implement
+fixtures (setup and teardown test state) on a per-module/class/function basis.
+
+
+.. note::
+
+ While these setup/teardown methods are simple and familiar to those
+ coming from a ``unittest`` or ``nose`` background, you may also consider
+ using pytest's more powerful :ref:`fixture mechanism
+ <fixture>` which leverages the concept of dependency injection, allowing
+ for a more modular and more scalable approach for managing test state,
+ especially for larger projects and for functional testing. You can
+ mix both fixture mechanisms in the same file but
+ test methods of ``unittest.TestCase`` subclasses
+ cannot receive fixture arguments.
+
+
+Module level setup/teardown
+--------------------------------------
+
+If you have multiple test functions and test classes in a single
+module you can optionally implement the following fixture methods
+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."""
+
+
+ def teardown_module(module):
+ """teardown any state that was previously setup with a setup_module
+ method.
+ """
+
+As of pytest-3.0, the ``module`` parameter is optional.
+
+Class level setup/teardown
+----------------------------------
+
+Similarly, the following methods are called at class level before
+and after all test methods of the class are called:
+
+.. code-block:: python
+
+ @classmethod
+ def setup_class(cls):
+ """setup any state specific to the execution of the given class (which
+ usually contains tests).
+ """
+
+
+ @classmethod
+ def teardown_class(cls):
+ """teardown any state that was previously setup with a call to
+ setup_class.
+ """
+
+Method and function level setup/teardown
+-----------------------------------------------
+
+Similarly, the following methods are called around each method invocation:
+
+.. code-block:: python
+
+ def setup_method(self, method):
+ """setup any state tied to the execution of the given method in a
+ class. setup_method is invoked for every test method of a class.
+ """
+
+
+ def teardown_method(self, method):
+ """teardown any state that was previously setup with a setup_method
+ call.
+ """
+
+As of pytest-3.0, the ``method`` parameter is optional.
+
+If you would rather define test functions directly at module level
+you can also use the following functions to implement fixtures:
+
+.. code-block:: python
+
+ def setup_function(function):
+ """setup any state tied to the execution of the given function.
+ Invoked for every test function in the module.
+ """
+
+
+ def teardown_function(function):
+ """teardown any state that was previously setup with a setup_function
+ call.
+ """
+
+As of pytest-3.0, the ``function`` parameter is optional.
+
+Remarks:
+
+* It is possible for setup/teardown pairs to be invoked multiple times
+ per testing process.
+
+* teardown functions are not called if the corresponding setup function existed
+ and failed/was skipped.
+
+* Prior to pytest-4.2, xunit-style functions did not obey the scope rules of fixtures, so
+ it was possible, for example, for a ``setup_method`` to be called before a
+ session-scoped autouse fixture.
+
+ Now the xunit-style functions are integrated with the fixture mechanism and obey the proper
+ scope rules of fixtures involved in the call.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/cramer2.png b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/cramer2.png
new file mode 100644
index 0000000000..6bf0e92e20
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/cramer2.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/favicon.png b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/favicon.png
new file mode 100644
index 0000000000..5c8824d67d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/favicon.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/freiburg2.jpg b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/freiburg2.jpg
new file mode 100644
index 0000000000..3383d3023d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/freiburg2.jpg
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/gaynor3.png b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/gaynor3.png
new file mode 100644
index 0000000000..a577c168b3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/gaynor3.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/keleshev.png b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/keleshev.png
new file mode 100644
index 0000000000..0d5e571e26
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/keleshev.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pullrequest.png b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pullrequest.png
new file mode 100644
index 0000000000..4af293b213
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pullrequest.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pylib.png b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pylib.png
new file mode 100644
index 0000000000..2e10d43886
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pylib.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pytest1.png b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pytest1.png
new file mode 100644
index 0000000000..e8064a694c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pytest1.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pytest_logo_curves.svg b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pytest_logo_curves.svg
new file mode 100644
index 0000000000..e05ceb1123
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/pytest_logo_curves.svg
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="1500" height="1500" viewBox="0, 0, 1500, 1500">
+ <g id="pytest_logo">
+ <g id="graphics">
+ <path d="M521.576,213.75 L952.616,213.75 C964.283,213.75 973.741,223.208 973.741,234.875 L973.741,234.875 C973.741,246.542 964.283,256 952.616,256 L521.576,256 C509.909,256 500.451,246.542 500.451,234.875 L500.451,234.875 C500.451,223.208 509.909,213.75 521.576,213.75 z" fill="#696969" id="horizontal_bar"/>
+ <g id="top_bars">
+ <path d="M525.333,171 L612,171 L612,191 L525.333,191 L525.333,171 z" fill="#009FE3"/>
+ <path d="M638.667,171 L725.333,171 L725.333,191 L638.667,191 L638.667,171 z" fill="#C7D302"/>
+ <path d="M750.5,171 L837.167,171 L837.167,191 L750.5,191 L750.5,171 z" fill="#F07E16"/>
+ <path d="M861.861,171 L948.528,171 L948.528,191 L861.861,191 L861.861,171 z" fill="#DF2815"/>
+ </g>
+ <g id="bottom_bars">
+ <path d="M861.861,278 L948.528,278 L948.528,424.5 L861.861,424.5 L861.861,278 z" fill="#DF2815"/>
+ <path d="M750.5,278 L837.328,278 L837.328,516 L750.5,516 L750.5,278 z" fill="#F07E16"/>
+ <path d="M638.667,278 L725.328,278 L725.328,634.5 L638.667,634.5 L638.667,278 z" fill="#C7D302"/>
+ <path d="M525.333,278 L612,278 L612,712.5 L525.333,712.5 L525.333,278 z" fill="#009FE3"/>
+ </g>
+ </g>
+ <g id="pytest">
+ <path d="M252.959,1173.846 Q240.139,1173.846 229.71,1171.021 Q219.28,1168.196 210.914,1163.525 Q202.549,1158.853 196.139,1152.552 Q189.729,1146.25 184.732,1139.297 L182.124,1139.297 Q182.776,1146.685 183.428,1153.421 Q183.862,1159.07 184.297,1165.046 Q184.732,1171.021 184.732,1174.498 L184.732,1276.404 L145.186,1276.404 L145.186,930.921 L177.344,930.921 L182.993,963.079 L184.732,963.079 Q189.729,955.474 196.03,948.847 Q202.332,942.22 210.697,937.331 Q219.063,932.442 229.492,929.509 Q239.922,926.575 252.959,926.575 Q273.384,926.575 290.115,934.397 Q306.846,942.22 318.688,957.756 Q330.53,973.292 337.048,996.324 Q343.567,1019.356 343.567,1049.776 Q343.567,1080.413 337.048,1103.554 Q330.53,1126.695 318.688,1142.339 Q306.846,1157.984 290.115,1165.915 Q273.384,1173.846 252.959,1173.846 z M245.354,959.385 Q228.84,959.385 217.433,964.383 Q206.025,969.38 198.964,979.593 Q191.902,989.805 188.534,1005.015 Q185.166,1020.225 184.732,1040.867 L184.732,1049.776 Q184.732,1071.722 187.665,1088.779 Q190.598,1105.835 197.66,1117.46 Q204.722,1129.085 216.455,1135.06 Q228.189,1141.036 245.789,1141.036 Q275.122,1141.036 288.92,1117.352 Q302.717,1093.667 302.717,1049.341 Q302.717,1004.146 288.92,981.766 Q275.122,959.385 245.354,959.385 z" fill="#696969"/>
+ <path d="M370.293,930.921 L411.36,930.921 L458.076,1064.117 Q461.118,1072.808 464.269,1082.369 Q467.42,1091.929 470.136,1101.49 Q472.852,1111.05 474.807,1119.959 Q476.763,1128.868 477.632,1136.473 L478.936,1136.473 Q480.022,1131.041 482.412,1121.697 Q484.802,1112.354 487.736,1101.816 Q490.669,1091.277 493.82,1081.065 Q496.97,1070.853 499.36,1063.682 L542.6,930.921 L583.45,930.921 L489.148,1200.572 Q483.064,1218.172 476.002,1232.187 Q468.941,1246.202 459.597,1255.979 Q450.254,1265.757 437.651,1271.081 Q425.049,1276.404 407.666,1276.404 Q396.367,1276.404 388.11,1275.209 Q379.854,1274.014 373.987,1272.71 L373.987,1241.204 Q378.55,1242.291 385.503,1243.051 Q392.456,1243.812 400.061,1243.812 Q410.491,1243.812 418.096,1241.313 Q425.701,1238.814 431.35,1234.034 Q437,1229.253 441.019,1222.3 Q445.039,1215.347 448.298,1206.438 L460.684,1171.673 z" fill="#696969"/>
+ <path d="M695.568,1141.47 Q699.479,1141.47 704.368,1141.036 Q709.257,1140.601 713.82,1139.949 Q718.383,1139.297 722.186,1138.428 Q725.988,1137.559 727.944,1136.907 L727.944,1166.893 Q725.119,1168.196 720.773,1169.5 Q716.428,1170.804 711.213,1171.781 Q705.998,1172.759 700.349,1173.302 Q694.699,1173.846 689.267,1173.846 Q675.795,1173.846 664.279,1170.369 Q652.763,1166.893 644.398,1158.418 Q636.032,1149.944 631.252,1135.495 Q626.472,1121.045 626.472,1099.1 L626.472,960.689 L592.792,960.689 L592.792,943.089 L626.472,926.141 L643.42,876.165 L666.235,876.165 L666.235,930.921 L726.206,930.921 L726.206,960.689 L666.235,960.689 L666.235,1099.1 Q666.235,1120.176 673.079,1130.823 Q679.924,1141.47 695.568,1141.47 z" fill="#009FE3"/>
+ <path d="M868.527,1173.846 Q844.626,1173.846 824.853,1165.806 Q805.08,1157.767 790.848,1142.339 Q776.616,1126.912 768.793,1104.097 Q760.971,1081.282 760.971,1051.949 Q760.971,1022.398 768.142,999.148 Q775.312,975.899 788.349,959.711 Q801.386,943.523 819.529,935.049 Q837.673,926.575 859.619,926.575 Q881.13,926.575 898.295,934.289 Q915.461,942.002 927.412,956.017 Q939.362,970.032 945.772,989.697 Q952.182,1009.361 952.182,1033.262 L952.182,1057.815 L801.821,1057.815 Q802.907,1099.751 819.529,1119.524 Q836.152,1139.297 868.962,1139.297 Q880.043,1139.297 889.495,1138.211 Q898.947,1137.125 907.747,1135.06 Q916.547,1132.996 924.804,1129.845 Q933.061,1126.695 941.535,1122.784 L941.535,1157.984 Q932.844,1162.112 924.478,1165.154 Q916.113,1168.196 907.313,1170.152 Q898.513,1172.107 889.061,1172.977 Q879.609,1173.846 868.527,1173.846 z M858.749,959.385 Q833.979,959.385 819.529,976.333 Q805.08,993.282 802.69,1025.657 L909.594,1025.657 Q909.594,1010.882 906.661,998.605 Q903.727,986.329 897.535,977.637 Q891.342,968.946 881.782,964.166 Q872.221,959.385 858.749,959.385 z" fill="#009FE3"/>
+ <path d="M1155.126,1104.097 Q1155.126,1121.48 1148.825,1134.517 Q1142.524,1147.554 1130.682,1156.354 Q1118.84,1165.154 1102.109,1169.5 Q1085.378,1173.846 1064.518,1173.846 Q1040.834,1173.846 1023.886,1170.043 Q1006.938,1166.241 994.118,1158.853 L994.118,1122.784 Q1000.854,1126.26 1009.111,1129.628 Q1017.368,1132.996 1026.494,1135.604 Q1035.62,1138.211 1045.289,1139.841 Q1054.958,1141.47 1064.518,1141.47 Q1078.642,1141.47 1088.528,1139.08 Q1098.415,1136.69 1104.608,1132.236 Q1110.8,1127.781 1113.625,1121.371 Q1116.45,1114.961 1116.45,1107.139 Q1116.45,1100.403 1114.277,1094.971 Q1112.104,1089.539 1106.346,1084.216 Q1100.588,1078.892 1090.593,1073.46 Q1080.598,1068.028 1064.953,1061.292 Q1049.308,1054.556 1036.815,1048.038 Q1024.321,1041.519 1015.629,1033.479 Q1006.938,1025.44 1002.266,1014.902 Q997.595,1004.363 997.595,989.805 Q997.595,974.595 1003.57,962.753 Q1009.545,950.911 1020.41,942.872 Q1031.274,934.832 1046.484,930.704 Q1061.694,926.575 1080.38,926.575 Q1101.457,926.575 1118.948,931.138 Q1136.44,935.701 1152.084,943.089 L1138.395,975.03 Q1124.272,968.729 1109.388,964.057 Q1094.504,959.385 1079.077,959.385 Q1056.913,959.385 1046.266,966.664 Q1035.62,973.943 1035.62,987.415 Q1035.62,995.02 1038.118,1000.669 Q1040.617,1006.319 1046.701,1011.316 Q1052.785,1016.314 1062.997,1021.42 Q1073.21,1026.526 1088.42,1032.828 Q1104.064,1039.346 1116.341,1045.865 Q1128.618,1052.383 1137.309,1060.531 Q1146,1068.68 1150.563,1079.109 Q1155.126,1089.539 1155.126,1104.097 z" fill="#009FE3"/>
+ <path d="M1285.28,1141.47 Q1289.191,1141.47 1294.08,1141.036 Q1298.969,1140.601 1303.532,1139.949 Q1308.095,1139.297 1311.898,1138.428 Q1315.7,1137.559 1317.656,1136.907 L1317.656,1166.893 Q1314.831,1168.196 1310.485,1169.5 Q1306.14,1170.804 1300.925,1171.781 Q1295.71,1172.759 1290.06,1173.302 Q1284.411,1173.846 1278.979,1173.846 Q1265.507,1173.846 1253.991,1170.369 Q1242.475,1166.893 1234.109,1158.418 Q1225.744,1149.944 1220.964,1135.495 Q1216.183,1121.045 1216.183,1099.1 L1216.183,960.689 L1182.504,960.689 L1182.504,943.089 L1216.183,926.141 L1233.132,876.165 L1255.947,876.165 L1255.947,930.921 L1315.917,930.921 L1315.917,960.689 L1255.947,960.689 L1255.947,1099.1 Q1255.947,1120.176 1262.791,1130.823 Q1269.636,1141.47 1285.28,1141.47 z" fill="#009FE3"/>
+ </g>
+ </g>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/theuni.png b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/theuni.png
new file mode 100644
index 0000000000..abeb737e79
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/img/theuni.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/index.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/index.rst
new file mode 100644
index 0000000000..d1b3d2e8a0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/index.rst
@@ -0,0 +1,148 @@
+:orphan:
+
+..
+ .. sidebar:: Next Open Trainings
+
+ - `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.
+
+ Also see `previous talks and blogposts <talks.html>`_.
+
+.. _features:
+
+pytest: helps you write better programs
+=======================================
+
+.. module:: pytest
+
+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.
+
+**PyPI package name**: :pypi:`pytest`
+
+**Documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_
+
+
+A quick example
+---------------
+
+.. code-block:: python
+
+ # content of test_sample.py
+ def inc(x):
+ return x + 1
+
+
+ def test_answer():
+ assert inc(3) == 5
+
+
+To execute it:
+
+.. code-block:: pytest
+
+ $ pytest
+ =========================== test session starts ============================
+ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
+ rootdir: /home/sweet/project
+ collected 1 item
+
+ test_sample.py F [100%]
+
+ ================================= FAILURES =================================
+ _______________________________ test_answer ________________________________
+
+ def test_answer():
+ > assert inc(3) == 5
+ E assert 4 == 5
+ E + where 4 = inc(3)
+
+ test_sample.py:6: AssertionError
+ ========================= short test summary info ==========================
+ FAILED test_sample.py::test_answer - assert 4 == 5
+ ============================ 1 failed in 0.12s =============================
+
+Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used.
+See :ref:`Get started <getstarted>` for a basic introduction to using pytest.
+
+
+Features
+--------
+
+- Detailed info on failing :ref:`assert statements <assert>` (no need to remember ``self.assert*`` names)
+
+- :ref:`Auto-discovery <test discovery>` of test modules and functions
+
+- :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
+
+- Python 3.6+ and PyPy 3
+
+- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community
+
+
+Documentation
+-------------
+
+* :ref:`Get started <get-started>` - install pytest and grasp its basics 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
+
+
+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
+--------------
+
+`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.
+
+See more details in the `pytest collective`_.
+
+.. _Open Collective: https://opencollective.com
+.. _pytest collective: https://opencollective.com/pytest
+
+
+pytest for enterprise
+---------------------
+
+Available as part of the Tidelift Subscription.
+
+The maintainers of pytest and thousands of other packages are working with Tidelift to deliver commercial support and
+maintenance for the open source dependencies you use to build your applications.
+Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use.
+
+`Learn more. <https://tidelift.com/subscription/pkg/pypi-pytest?utm_source=pypi-pytest&utm_medium=referral&utm_campaign=enterprise&utm_term=repo>`_
+
+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/testing/web-platform/tests/tools/third_party/pytest/doc/en/license.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/license.rst
new file mode 100644
index 0000000000..acbfb8bdb1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/license.rst
@@ -0,0 +1,32 @@
+.. _license:
+
+License
+-------
+
+Distributed under the terms of the `MIT`_ license, pytest is free and open source software.
+
+.. code-block:: text
+
+ The MIT License (MIT)
+
+ Copyright (c) 2004 Holger Krekel and others
+
+ 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.
+
+.. _`MIT`: https://github.com/pytest-dev/pytest/blob/main/LICENSE
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/naming20.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/naming20.rst
new file mode 100644
index 0000000000..5a81df2698
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/naming20.rst
@@ -0,0 +1,20 @@
+
+.. _naming20:
+
+New pytest names in 2.0 (flat is better than nested)
+----------------------------------------------------
+
+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
+these renaming rules::
+
+ py.test.XYZ -> pytest.XYZ
+ py.test.collect.XYZ -> pytest.XYZ
+ py.test.cmdline.main -> pytest.main
+
+The old ``py.test.*`` ways to access functionality remain
+valid but you are encouraged to do global renaming according
+to the above rules in your test code.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/proposals/parametrize_with_fixtures.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/proposals/parametrize_with_fixtures.rst
new file mode 100644
index 0000000000..f6814ec78d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/proposals/parametrize_with_fixtures.rst
@@ -0,0 +1,164 @@
+:orphan:
+
+===================================
+PROPOSAL: Parametrize with fixtures
+===================================
+
+.. warning::
+
+ This document outlines a proposal around using fixtures as input
+ of parametrized tests or fixtures.
+
+Problem
+-------
+
+As a user I have functional tests that I would like to run against various
+scenarios.
+
+In this particular example we want to generate a new project based on a
+cookiecutter template. We want to test default values but also data that
+emulates user input.
+
+- use default values
+
+- emulate user input
+
+ - specify 'author'
+
+ - specify 'project_slug'
+
+ - specify 'author' and 'project_slug'
+
+This is how a functional test could look like:
+
+.. code-block:: python
+
+ import pytest
+
+
+ @pytest.fixture
+ def default_context():
+ return {"extra_context": {}}
+
+
+ @pytest.fixture(
+ params=[
+ {"author": "alice"},
+ {"project_slug": "helloworld"},
+ {"author": "bob", "project_slug": "foobar"},
+ ]
+ )
+ def extra_context(request):
+ return {"extra_context": request.param}
+
+
+ @pytest.fixture(params=["default", "extra"])
+ def context(request):
+ if request.param == "default":
+ return request.getfuncargvalue("default_context")
+ else:
+ return request.getfuncargvalue("extra_context")
+
+
+ def test_generate_project(cookies, context):
+ """Call the cookiecutter API to generate a new project from a
+ template.
+ """
+ result = cookies.bake(extra_context=context)
+
+ assert result.exit_code == 0
+ assert result.exception is None
+ assert result.project.isdir()
+
+
+Issues
+------
+
+* By using ``request.getfuncargvalue()`` we rely on actual fixture function
+ execution to know what fixtures are involved, due to its dynamic nature
+* More importantly, ``request.getfuncargvalue()`` cannot be combined with
+ parametrized fixtures, such as ``extra_context``
+* This is very inconvenient if you wish to extend an existing test suite by
+ certain parameters for fixtures that are already used by tests
+
+pytest version 3.0 reports an error if you try to run above code::
+
+ Failed: The requested fixture has no parameter defined for the current
+ test.
+
+ Requested fixture 'extra_context'
+
+
+Proposed solution
+-----------------
+
+A new function that can be used in modules can be used to dynamically define
+fixtures from existing ones.
+
+.. code-block:: python
+
+ pytest.define_combined_fixture(
+ name="context", fixtures=["default_context", "extra_context"]
+ )
+
+The new fixture ``context`` inherits the scope from the used fixtures and yield
+the following values.
+
+- ``{}``
+
+- ``{'author': 'alice'}``
+
+- ``{'project_slug': 'helloworld'}``
+
+- ``{'author': 'bob', 'project_slug': 'foobar'}``
+
+Alternative approach
+--------------------
+
+A new helper function named ``fixture_request`` would tell pytest to yield
+all parameters marked as a fixture.
+
+.. note::
+
+ The :pypi:`pytest-lazy-fixture` plugin implements a very
+ similar solution to the proposal below, make sure to check it out.
+
+.. code-block:: python
+
+ @pytest.fixture(
+ params=[
+ pytest.fixture_request("default_context"),
+ pytest.fixture_request("extra_context"),
+ ]
+ )
+ def context(request):
+ """Returns all values for ``default_context``, one-by-one before it
+ does the same for ``extra_context``.
+
+ request.param:
+ - {}
+ - {'author': 'alice'}
+ - {'project_slug': 'helloworld'}
+ - {'author': 'bob', 'project_slug': 'foobar'}
+ """
+ return request.param
+
+The same helper can be used in combination with ``pytest.mark.parametrize``.
+
+.. code-block:: python
+
+
+ @pytest.mark.parametrize(
+ "context, expected_response_code",
+ [
+ (pytest.fixture_request("default_context"), 0),
+ (pytest.fixture_request("extra_context"), 0),
+ ],
+ )
+ def test_generate_project(cookies, context, exit_code):
+ """Call the cookiecutter API to generate a new project from a
+ template.
+ """
+ result = cookies.bake(extra_context=context)
+
+ assert result.exit_code == exit_code
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/py27-py34-deprecation.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/py27-py34-deprecation.rst
new file mode 100644
index 0000000000..660b078e30
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/py27-py34-deprecation.rst
@@ -0,0 +1,99 @@
+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/testing/web-platform/tests/tools/third_party/pytest/doc/en/pytest.ini b/testing/web-platform/tests/tools/third_party/pytest/doc/en/pytest.ini
new file mode 100644
index 0000000000..7604360561
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+# just defined to prevent the root level tox.ini from kicking in
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/recwarn.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/recwarn.rst
new file mode 100644
index 0000000000..513af0d450
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/recwarn.rst
@@ -0,0 +1,3 @@
+:orphan:
+
+This page has been moved, please see :ref:`assertwarnings`.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/customize.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/customize.rst
new file mode 100644
index 0000000000..fe10ca066b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/customize.rst
@@ -0,0 +1,248 @@
+Configuration
+=============
+
+Command line options and configuration file settings
+-----------------------------------------------------------------
+
+You can get help on command line options and values in INI-style
+configurations files by using the general help option:
+
+.. code-block:: bash
+
+ pytest -h # prints options _and_ config file settings
+
+This will display command line and configuration file settings
+which were registered by installed plugins.
+
+.. _`config file formats`:
+
+Configuration file formats
+--------------------------
+
+Many :ref:`pytest settings <ini options ref>` can be set in a *configuration file*, which
+by convention resides in the root directory of your repository.
+
+A quick example of the configuration files supported by pytest:
+
+pytest.ini
+~~~~~~~~~~
+
+``pytest.ini`` files take precedence over other files, even when empty.
+
+.. code-block:: ini
+
+ # pytest.ini
+ [pytest]
+ minversion = 6.0
+ addopts = -ra -q
+ testpaths =
+ tests
+ integration
+
+
+pyproject.toml
+~~~~~~~~~~~~~~
+
+.. versionadded:: 6.0
+
+``pyproject.toml`` are considered for configuration when they contain a ``tool.pytest.ini_options`` table.
+
+.. code-block:: toml
+
+ # pyproject.toml
+ [tool.pytest.ini_options]
+ minversion = "6.0"
+ addopts = "-ra -q"
+ testpaths = [
+ "tests",
+ "integration",
+ ]
+
+.. note::
+
+ One might wonder why ``[tool.pytest.ini_options]`` instead of ``[tool.pytest]`` as is the
+ case with other tools.
+
+ The reason is that the pytest team intends to fully utilize the rich TOML data format
+ for configuration in the future, reserving the ``[tool.pytest]`` table for that.
+ The ``ini_options`` table is being used, for now, as a bridge between the existing
+ ``.ini`` configuration system and the future configuration format.
+
+tox.ini
+~~~~~~~
+
+``tox.ini`` files are the configuration files of the `tox <https://tox.readthedocs.io>`__ project,
+and can also be used to hold pytest configuration if they have a ``[pytest]`` section.
+
+.. code-block:: ini
+
+ # tox.ini
+ [pytest]
+ minversion = 6.0
+ addopts = -ra -q
+ testpaths =
+ tests
+ integration
+
+
+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
+if they have a ``[tool:pytest]`` section.
+
+.. code-block:: ini
+
+ # setup.cfg
+ [tool:pytest]
+ minversion = 6.0
+ addopts = -ra -q
+ testpaths =
+ tests
+ integration
+
+.. warning::
+
+ Usage of ``setup.cfg`` is not recommended unless for very simple use cases. ``.cfg``
+ files use a different parser than ``pytest.ini`` and ``tox.ini`` which might cause hard to track
+ down problems.
+ When possible, it is recommended to use the latter files, or ``pyproject.toml``, to hold your
+ pytest configuration.
+
+
+.. _rootdir:
+.. _configfiles:
+
+Initialization: determining rootdir and configfile
+--------------------------------------------------
+
+pytest determines a ``rootdir`` for each test run which depends on
+the command line arguments (specified test files, paths) and on
+the existence of configuration files. The determined ``rootdir`` and ``configfile`` are
+printed as part of the pytest header during startup.
+
+Here's a summary what ``pytest`` uses ``rootdir`` for:
+
+* Construct *nodeids* during collection; each test is assigned
+ a unique *nodeid* which is rooted at the ``rootdir`` and takes into account
+ the full path, class name, function name and parametrization (if any).
+
+* Is used by plugins as a stable location to store project/test run specific information;
+ for example, the internal :ref:`cache <cache>` plugin creates a ``.pytest_cache`` subdirectory
+ in ``rootdir`` to store its cross-test run state.
+
+``rootdir`` is **NOT** used to modify ``sys.path``/``PYTHONPATH`` or
+influence how modules are imported. See :ref:`pythonpath` for more details.
+
+The ``--rootdir=path`` command-line option can be used to force a specific directory.
+Note that contrary to other command-line options, ``--rootdir`` cannot be used with
+:confval:`addopts` inside ``pytest.ini`` because the ``rootdir`` is used to *find* ``pytest.ini``
+already.
+
+Finding the ``rootdir``
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Here is the algorithm which finds the rootdir from ``args``:
+
+- If ``-c`` is passed in the command-line, use that as configuration file, and its directory as ``rootdir``.
+
+- Determine the common ancestor directory for the specified ``args`` that are
+ recognised as paths that exist in the file system. If no such paths are
+ found, the common ancestor directory is set to the current working directory.
+
+- Look for ``pytest.ini``, ``pyproject.toml``, ``tox.ini``, and ``setup.cfg`` files in the ancestor
+ directory and upwards. If one is matched, it becomes the ``configfile`` and its
+ directory becomes the ``rootdir``.
+
+- If no configuration file was found, look for ``setup.py`` upwards from the common
+ ancestor directory to determine the ``rootdir``.
+
+- If no ``setup.py`` was found, look for ``pytest.ini``, ``pyproject.toml``, ``tox.ini``, and
+ ``setup.cfg`` in each of the specified ``args`` and upwards. If one is
+ matched, it becomes the ``configfile`` and its directory becomes the ``rootdir``.
+
+- If no ``configfile`` was found and no configuration argument is passed, use the already determined common ancestor as root
+ directory. This allows the use of pytest in structures that are not part of
+ a package and don't have any particular configuration file.
+
+If no ``args`` are given, pytest collects test below the current working
+directory and also starts determining the ``rootdir`` from there.
+
+Files will only be matched for configuration if:
+
+* ``pytest.ini``: will always match and take precedence, even if empty.
+* ``pyproject.toml``: contains a ``[tool.pytest.ini_options]`` table.
+* ``tox.ini``: contains a ``[pytest]`` section.
+* ``setup.cfg``: contains a ``[tool:pytest]`` section.
+
+The files are considered in the order above. Options from multiple ``configfiles`` candidates
+are never merged - the first match wins.
+
+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.inipath <pytest.Config.inipath>`: the determined ``configfile``, may be ``None``
+ (it is named ``inipath`` for historical reasons).
+
+.. versionadded:: 6.1
+ The ``config.rootpath`` and ``config.inipath`` properties. They are :class:`pathlib.Path`
+ 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:
+
+.. code-block:: bash
+
+ pytest path/to/testdir path/other/
+
+will determine the common ancestor as ``path`` and then
+check for configuration files as follows:
+
+.. code-block:: text
+
+ # first look for pytest.ini files
+ path/pytest.ini
+ path/pyproject.toml # must contain a [tool.pytest.ini_options] table to match
+ path/tox.ini # must contain [pytest] section to match
+ path/setup.cfg # must contain [tool:pytest] section to match
+ pytest.ini
+ ... # all the way up to the root
+
+ # now look for setup.py
+ path/setup.py
+ setup.py
+ ... # all the way up to the root
+
+
+.. warning::
+
+ Custom pytest plugin commandline arguments may include a path, as in
+ ``pytest --log-output ../../test.log args``. Then ``args`` is mandatory,
+ otherwise pytest uses the folder of test.log for rootdir determination
+ (see also :issue:`1435`).
+ A dot ``.`` for referencing to the current working directory is also
+ possible.
+
+
+.. _`how to change command line options defaults`:
+.. _`adding default options`:
+
+
+Builtin configuration file options
+----------------------------------------------
+
+For the full list of options consult the :ref:`reference documentation <ini options ref>`.
+
+Syntax highlighting theme customization
+---------------------------------------
+
+The syntax highlighting themes used by pytest can be customized using two environment variables:
+
+- :envvar:`PYTEST_THEME` sets a `pygment style <https://pygments.org/docs/styles/>`_ to use.
+- :envvar:`PYTEST_THEME_MODE` sets this style to *light* or *dark*.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/exit-codes.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/exit-codes.rst
new file mode 100644
index 0000000000..b695ca3702
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/exit-codes.rst
@@ -0,0 +1,26 @@
+.. _exit-codes:
+
+Exit codes
+========================================================
+
+Running ``pytest`` can result in six different exit codes:
+
+:Exit code 0: All tests were collected and passed successfully
+:Exit code 1: Tests were collected and run but some of the tests failed
+:Exit code 2: Test execution was interrupted by the user
+:Exit code 3: Internal error happened while executing tests
+:Exit code 4: pytest command line usage error
+:Exit code 5: No tests were collected
+
+They are represented by the :class:`pytest.ExitCode` enum. The exit codes being a part of the public API can be imported and accessed directly using:
+
+.. code-block:: python
+
+ from pytest import ExitCode
+
+.. note::
+
+ If you would like to customize the exit code in some scenarios, specially when
+ no tests are collected, consider using the
+ `pytest-custom_exit_code <https://github.com/yashtodi94/pytest-custom_exit_code>`__
+ plugin.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/fixtures.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/fixtures.rst
new file mode 100644
index 0000000000..d25979ab95
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/fixtures.rst
@@ -0,0 +1,455 @@
+.. _reference-fixtures:
+.. _fixture:
+.. _fixtures:
+.. _`@pytest.fixture`:
+.. _`pytest.fixture`:
+
+
+Fixtures reference
+========================================================
+
+.. seealso:: :ref:`about-fixtures`
+.. seealso:: :ref:`how-to-fixtures`
+
+
+.. currentmodule:: _pytest.python
+
+.. _`Dependency injection`: https://en.wikipedia.org/wiki/Dependency_injection
+
+
+Built-in fixtures
+-----------------
+
+:ref:`Fixtures <fixtures-api>` are defined using the :ref:`@pytest.fixture
+<pytest.fixture-api>` decorator. Pytest has several useful built-in fixtures:
+
+ :fixture:`capfd`
+ Capture, as text, output to file descriptors ``1`` and ``2``.
+
+ :fixture:`capfdbinary`
+ Capture, as bytes, output to file descriptors ``1`` and ``2``.
+
+ :fixture:`caplog`
+ Control logging and access log entries.
+
+ :fixture:`capsys`
+ Capture, as text, output to ``sys.stdout`` and ``sys.stderr``.
+
+ :fixture:`capsysbinary`
+ Capture, as bytes, output to ``sys.stdout`` and ``sys.stderr``.
+
+ :fixture:`cache`
+ Store and retrieve values across pytest runs.
+
+ :fixture:`doctest_namespace`
+ Provide a dict injected into the docstests namespace.
+
+ :fixture:`monkeypatch`
+ Temporarily modify classes, functions, dictionaries,
+ ``os.environ``, and other objects.
+
+ :fixture:`pytestconfig`
+ Access to configuration values, pluginmanager and plugin hooks.
+
+ :fixture:`record_property`
+ Add extra properties to the test.
+
+ :fixture:`record_testsuite_property`
+ Add extra properties to the test suite.
+
+ :fixture:`recwarn`
+ Record warnings emitted by test functions.
+
+ :fixture:`request`
+ Provide information on the executing test function.
+
+ :fixture:`testdir`
+ Provide a temporary test directory to aid in running, and
+ testing, pytest plugins.
+
+ :fixture:`tmp_path`
+ Provide a :class:`pathlib.Path` object to a temporary directory
+ which is unique to each test function.
+
+ :fixture:`tmp_path_factory`
+ Make session-scoped temporary directories and return
+ :class:`pathlib.Path` objects.
+
+ :fixture:`tmpdir`
+ Provide a :class:`py.path.local` 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;
+ replaced by :fixture:`tmp_path_factory`.
+
+
+.. _`conftest.py`:
+.. _`conftest`:
+
+Fixture availability
+---------------------
+
+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,
+can request it.
+
+Similarly, a test can also only be affected by an autouse fixture if that test
+is in the same scope that autouse fixture is defined in (see
+:ref:`autouse order`).
+
+A fixture can also request any other fixture, no matter where it's defined, so
+long as the test requesting them can see all fixtures involved.
+
+For example, here's a test file with a fixture (``outer``) that requests a
+fixture (``inner``) from a scope it wasn't defined in:
+
+.. literalinclude:: /example/fixtures/test_fixtures_request_different_scope.py
+
+From the tests' perspectives, they have no problem seeing each of the fixtures
+they're dependent on:
+
+.. image:: /example/fixtures/test_fixtures_request_different_scope.*
+ :align: center
+
+So when they run, ``outer`` will have no problem finding ``inner``, because
+pytest searched from the tests' perspectives.
+
+.. note::
+ The scope a fixture is defined in has no bearing on the order it will be
+ instantiated in: the order is mandated by the logic described
+ :ref:`here <fixture order>`.
+
+``conftest.py``: sharing fixtures across multiple files
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``conftest.py`` file serves as a means of providing fixtures for an entire
+directory. Fixtures defined in a ``conftest.py`` can be used by any test
+in that package without needing to import them (pytest will automatically
+discover them).
+
+You can have multiple nested directories/packages containing your tests, and
+each directory can have its own ``conftest.py`` with its own fixtures, adding on
+to the ones provided by the ``conftest.py`` files in parent directories.
+
+For example, given a test file structure like this:
+
+::
+
+ tests/
+ __init__.py
+
+ conftest.py
+ # content of tests/conftest.py
+ import pytest
+
+ @pytest.fixture
+ def order():
+ return []
+
+ @pytest.fixture
+ def top(order, innermost):
+ order.append("top")
+
+ test_top.py
+ # content of tests/test_top.py
+ import pytest
+
+ @pytest.fixture
+ def innermost(order):
+ order.append("innermost top")
+
+ def test_order(order, top):
+ assert order == ["innermost top", "top"]
+
+ subpackage/
+ __init__.py
+
+ conftest.py
+ # content of tests/subpackage/conftest.py
+ import pytest
+
+ @pytest.fixture
+ def mid(order):
+ order.append("mid subpackage")
+
+ test_subpackage.py
+ # content of tests/subpackage/test_subpackage.py
+ import pytest
+
+ @pytest.fixture
+ def innermost(order, mid):
+ order.append("innermost subpackage")
+
+ def test_order(order, top):
+ assert order == ["mid subpackage", "innermost subpackage", "top"]
+
+The boundaries of the scopes can be visualized like this:
+
+.. image:: /example/fixtures/fixture_availability.*
+ :align: center
+
+The directories become their own sort of scope where fixtures that are defined
+in a ``conftest.py`` file in that directory become available for that whole
+scope.
+
+Tests are allowed to search upward (stepping outside a circle) for fixtures, but
+can never go down (stepping inside a circle) to continue their search. So
+``tests/subpackage/test_subpackage.py::test_order`` would be able to find the
+``innermost`` fixture defined in ``tests/subpackage/test_subpackage.py``, but
+the one defined in ``tests/test_top.py`` would be unavailable to it because it
+would have to step down a level (step inside a circle) to find it.
+
+The first fixture the test finds is the one that will be used, so
+:ref:`fixtures can be overridden <override fixtures>` if you need to change or
+extend what one does for a particular scope.
+
+You can also use the ``conftest.py`` file to implement
+:ref:`local per-directory plugins <conftest.py plugins>`.
+
+Fixtures from third-party plugins
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Fixtures don't have to be defined in this structure to be available for tests,
+though. They can also be provided by third-party plugins that are installed, and
+this is how many pytest plugins operate. As long as those plugins are installed,
+the fixtures they provide can be requested from anywhere in your test suite.
+
+Because they're provided from outside the structure of your test suite,
+third-party plugins don't really provide a scope like `conftest.py` files and
+the directories in your test suite do. As a result, pytest will search for
+fixtures stepping out through scopes as explained previously, only reaching
+fixtures defined in plugins *last*.
+
+For example, given the following file structure:
+
+::
+
+ tests/
+ __init__.py
+
+ conftest.py
+ # content of tests/conftest.py
+ import pytest
+
+ @pytest.fixture
+ def order():
+ return []
+
+ subpackage/
+ __init__.py
+
+ conftest.py
+ # content of tests/subpackage/conftest.py
+ import pytest
+
+ @pytest.fixture(autouse=True)
+ def mid(order, b_fix):
+ order.append("mid subpackage")
+
+ test_subpackage.py
+ # content of tests/subpackage/test_subpackage.py
+ import pytest
+
+ @pytest.fixture
+ def inner(order, mid, a_fix):
+ order.append("inner subpackage")
+
+ def test_order(order, inner):
+ assert order == ["b_fix", "mid subpackage", "a_fix", "inner subpackage"]
+
+If ``plugin_a`` is installed and provides the fixture ``a_fix``, and
+``plugin_b`` is installed and provides the fixture ``b_fix``, then this is what
+the test's search for fixtures would look like:
+
+.. image:: /example/fixtures/fixture_availability_plugins.svg
+ :align: center
+
+pytest will only search for ``a_fix`` and ``b_fix`` in the plugins after
+searching for them first in the scopes inside ``tests/``.
+
+.. note:
+
+ pytest can tell you what fixtures are available for a given test if you call
+ ``pytests`` along with the test's name (or the scope it's in), and provide
+ the ``--fixtures`` flag, e.g. ``pytest --fixtures test_something.py``
+ (fixtures with names that start with ``_`` will only be shown if you also
+ provide the ``-v`` flag).
+
+
+.. _`fixture order`:
+
+Fixture instantiation order
+---------------------------
+
+When pytest wants to execute a test, once it knows what fixtures will be
+executed, it has to figure out the order they'll be executed in. To do this, it
+considers 3 factors:
+
+1. scope
+2. dependencies
+3. autouse
+
+Names of fixtures or tests, where they're defined, the order they're defined in,
+and the order fixtures are requested in have no bearing on execution order
+beyond coincidence. While pytest will try to make sure coincidences like these
+stay consistent from run to run, it's not something that should be depended on.
+If you want to control the order, it's safest to rely on these 3 things and make
+sure dependencies are clearly established.
+
+Higher-scoped fixtures are executed first
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Within a function request for fixtures, those of higher-scopes (such as
+``session``) are executed before lower-scoped fixtures (such as ``function`` or
+``class``).
+
+Here's an example:
+
+.. literalinclude:: /example/fixtures/test_fixtures_order_scope.py
+
+The test will pass because the larger scoped fixtures are executing first.
+
+The order breaks down to this:
+
+.. image:: /example/fixtures/test_fixtures_order_scope.*
+ :align: center
+
+Fixtures of the same order execute based on dependencies
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When a fixture requests another fixture, the other fixture is executed first.
+So if fixture ``a`` requests fixture ``b``, fixture ``b`` will execute first,
+because ``a`` depends on ``b`` and can't operate without it. Even if ``a``
+doesn't need the result of ``b``, it can still request ``b`` if it needs to make
+sure it is executed after ``b``.
+
+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:
+
+.. image:: /example/fixtures/test_fixtures_order_dependencies.*
+ :align: center
+
+The rules provided by each fixture (as to what fixture(s) each one has to come
+after) are comprehensive enough that it can be flattened to this:
+
+.. image:: /example/fixtures/test_fixtures_order_dependencies_flat.*
+ :align: center
+
+Enough information has to be provided through these requests in order for pytest
+to be able to figure out a clear, linear chain of dependencies, and as a result,
+an order of operations for a given test. If there's any ambiguity, and the order
+of operations can be interpreted more than one way, you should assume pytest
+could go with any one of those interpretations at any point.
+
+For example, if ``d`` didn't request ``c``, i.e.the graph would look like this:
+
+.. image:: /example/fixtures/test_fixtures_order_dependencies_unclear.*
+ :align: center
+
+Because nothing requested ``c`` other than ``g``, and ``g`` also requests ``f``,
+it's now unclear if ``c`` should go before/after ``f``, ``e``, or ``d``. The
+only rules that were set for ``c`` is that it must execute after ``b`` and
+before ``g``.
+
+pytest doesn't know where ``c`` should go in the case, so it should be assumed
+that it could go anywhere between ``g`` and ``b``.
+
+This isn't necessarily bad, but it's something to keep in mind. If the order
+they execute in could affect the behavior a test is targeting, or could
+otherwise influence the result of a test, then the order should be defined
+explicitly in a way that allows pytest to linearize/"flatten" that order.
+
+.. _`autouse order`:
+
+Autouse fixtures are executed first within their scope
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Autouse fixtures are assumed to apply to every test that could reference them,
+so they are executed before other fixtures in that scope. Fixtures that are
+requested by autouse fixtures effectively become autouse fixtures themselves for
+the tests that the real autouse fixture applies to.
+
+So if fixture ``a`` is autouse and fixture ``b`` is not, but fixture ``a``
+requests fixture ``b``, then fixture ``b`` will effectively be an autouse
+fixture as well, but only for the tests that ``a`` applies to.
+
+In the last example, the graph became unclear if ``d`` didn't request ``c``. But
+if ``c`` was autouse, then ``b`` and ``a`` would effectively also be autouse
+because ``c`` depends on them. As a result, they would all be shifted above
+non-autouse fixtures within that scope.
+
+So if the test file looked like this:
+
+.. literalinclude:: /example/fixtures/test_fixtures_order_autouse.py
+
+the graph would look like this:
+
+.. image:: /example/fixtures/test_fixtures_order_autouse.*
+ :align: center
+
+Because ``c`` can now be put above ``d`` in the graph, pytest can once again
+linearize the graph to this:
+
+.. image:: /example/fixtures/test_fixtures_order_autouse_flat.*
+ :align: center
+
+In this example, ``c`` makes ``b`` and ``a`` effectively autouse fixtures as
+well.
+
+Be careful with autouse, though, as an autouse fixture will automatically
+execute for every test that can reach it, even if they don't request it. For
+example, consider this file:
+
+.. literalinclude:: /example/fixtures/test_fixtures_order_autouse_multiple_scopes.py
+
+Even though nothing in ``TestClassWithoutC1Request`` is requesting ``c1``, it still
+is executed for the tests inside it anyway:
+
+.. image:: /example/fixtures/test_fixtures_order_autouse_multiple_scopes.*
+ :align: center
+
+But just because one autouse fixture requested a non-autouse fixture, that
+doesn't mean the non-autouse fixture becomes an autouse fixture for all contexts
+that it can apply to. It only effectively becomes an autouse fixture for the
+contexts the real autouse fixture (the one that requested the non-autouse
+fixture) can apply to.
+
+For example, take a look at this test file:
+
+.. literalinclude:: /example/fixtures/test_fixtures_order_autouse_temp_effects.py
+
+It would break down to something like this:
+
+.. image:: /example/fixtures/test_fixtures_order_autouse_temp_effects.*
+ :align: center
+
+For ``test_req`` and ``test_no_req`` inside ``TestClassWithAutouse``, ``c3``
+effectively makes ``c2`` an autouse fixture, which is why ``c2`` and ``c3`` are
+executed for both tests, despite not being requested, and why ``c2`` and ``c3``
+are executed before ``c1`` for ``test_req``.
+
+If this made ``c2`` an *actual* autouse fixture, then ``c2`` would also execute
+for the tests inside ``TestClassWithoutAutouse``, since they can reference
+``c2`` if they wanted to. But it doesn't, because from the perspective of the
+``TestClassWithoutAutouse`` tests, ``c2`` isn't an autouse fixture, since they
+can't see ``c3``.
+
+
+.. note:
+
+ pytest can tell you what order the fixtures will execute in for a given test
+ if you call ``pytests`` along with the test's name (or the scope it's in),
+ and provide the ``--setup-plan`` flag, e.g.
+ ``pytest --setup-plan test_something.py`` (fixtures with names that start
+ with ``_`` will only be shown if you also provide the ``-v`` flag).
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/index.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/index.rst
new file mode 100644
index 0000000000..d964840031
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/index.rst
@@ -0,0 +1,15 @@
+:orphan:
+
+.. _reference:
+
+Reference guides
+================
+
+.. toctree::
+ :maxdepth: 1
+
+ fixtures
+ plugin_list
+ customize
+ reference
+ exit-codes
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/plugin_list.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/plugin_list.rst
new file mode 100644
index 0000000000..ebf4009136
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/plugin_list.rst
@@ -0,0 +1,7728 @@
+
+.. _plugin-list:
+
+Plugin List
+===========
+
+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.
+
+.. 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)
+ =============================================== ======================================================================================================================================================================== ============== ===================== ================================================
+
+.. only:: latex
+
+
+ :pypi:`pytest-accept`
+ *last release*: Nov 22, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=6,<7)
+
+ A pytest-plugin for updating doctest outputs
+
+ :pypi:`pytest-adaptavist`
+ *last release*: Nov 30, 2021,
+ *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,
+ *requires*: pytest (>=6.2.4,<7.0.0)
+
+ 用于测试pytest的插件
+
+ :pypi:`pytest-adf`
+ *last release*: May 10, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Pytest plugin for writing Azure Data Factory integration tests
+
+ :pypi:`pytest-adf-azure-identity`
+ *last release*: Mar 06, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Pytest plugin for writing Azure Data Factory integration tests
+
+ :pypi:`pytest-agent`
+ *last release*: Nov 25, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ 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.
+
+ :pypi:`pytest-aggreport`
+ *last release*: Mar 07, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.2.2)
+
+ pytest plugin for pytest-repeat that generate aggregate report of the same test cases with additional statistics details.
+
+ :pypi:`pytest-aio`
+ *last release*: Oct 20, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ Pytest plugin for testing async python code
+
+ :pypi:`pytest-aiofiles`
+ *last release*: May 14, 2017,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ pytest fixtures for writing aiofiles tests with pyfakefs
+
+ :pypi:`pytest-aiohttp`
+ *last release*: Dec 05, 2017,
+ *status*: N/A,
+ *requires*: pytest
+
+ pytest plugin for aiohttp support
+
+ :pypi:`pytest-aiohttp-client`
+ *last release*: Nov 01, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=6)
+
+ Pytest \`client\` fixture for the Aiohttp
+
+ :pypi:`pytest-aioresponses`
+ *last release*: Jul 29, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ py.test integration for aioresponses
+
+ :pypi:`pytest-aioworkers`
+ *last release*: Dec 04, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A plugin to test aioworkers project with pytest
+
+ :pypi:`pytest-airflow`
+ *last release*: Apr 03, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=4.4.0)
+
+ pytest support for airflow.
+
+ :pypi:`pytest-airflow-utils`
+ *last release*: Nov 15, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-alembic`
+ *last release*: Dec 02, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=1.0)
+
+ A pytest plugin for verifying alembic migrations.
+
+ :pypi:`pytest-allclose`
+ *last release*: Jul 30, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Pytest fixture extending Numpy's allclose function
+
+ :pypi:`pytest-allure-adaptor`
+ *last release*: Jan 10, 2018,
+ *status*: N/A,
+ *requires*: pytest (>=2.7.3)
+
+ Plugin for py.test to generate allure xml reports
+
+ :pypi:`pytest-allure-adaptor2`
+ *last release*: Oct 14, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=2.7.3)
+
+ Plugin for py.test to generate allure xml reports
+
+ :pypi:`pytest-allure-dsl`
+ *last release*: Oct 25, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ pytest plugin to test case doc string dls instructions
+
+ :pypi:`pytest-allure-spec-coverage`
+ *last release*: Oct 26, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ 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,
+ *requires*: pytest (>=3.5.0)
+
+ Static code checks used at Alphamoon
+
+ :pypi:`pytest-android`
+ *last release*: Feb 21, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ This fixture provides a configured "driver" for Android Automated Testing, using uiautomator2.
+
+ :pypi:`pytest-anki`
+ *last release*: Oct 14, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A pytest plugin for testing Anki add-ons
+
+ :pypi:`pytest-annotate`
+ *last release*: Nov 29, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (<7.0.0,>=3.2.0)
+
+ pytest-annotate: Generate PyAnnotate annotations from your pytest tests.
+
+ :pypi:`pytest-ansible`
+ *last release*: May 25, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Plugin for py.test to simplify calling ansible modules from tests or fixtures
+
+ :pypi:`pytest-ansible-playbook`
+ *last release*: Mar 08, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Pytest fixture which runs given ansible playbook file.
+
+ :pypi:`pytest-ansible-playbook-runner`
+ *last release*: Dec 02, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.1.0)
+
+ Pytest fixture which runs given ansible playbook file.
+
+ :pypi:`pytest-antilru`
+ *last release*: Apr 11, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Bust functools.lru_cache when running pytest to avoid test pollution
+
+ :pypi:`pytest-anyio`
+ *last release*: Jun 29, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ The pytest anyio plugin is built into anyio. You don't need this package.
+
+ :pypi:`pytest-anything`
+ *last release*: Feb 18, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest fixtures to assert anything and something
+
+ :pypi:`pytest-aoc`
+ *last release*: Nov 23, 2021,
+ *status*: N/A,
+ *requires*: pytest ; extra == 'test'
+
+ Downloads puzzle inputs for Advent of Code and synthesizes PyTest fixtures
+
+ :pypi:`pytest-api`
+ *last release*: May 04, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ PyTest-API Python Web Framework built for testing purposes.
+
+ :pypi:`pytest-apistellar`
+ *last release*: Jun 18, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ apistellar plugin for pytest.
+
+ :pypi:`pytest-appengine`
+ *last release*: Feb 27, 2017,
+ *status*: N/A,
+ *requires*: N/A
+
+ AppEngine integration that works well with pytest-django
+
+ :pypi:`pytest-appium`
+ *last release*: Dec 05, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest plugin for appium
+
+ :pypi:`pytest-approvaltests`
+ *last release*: Feb 07, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A plugin to use approvaltests with pytest
+
+ :pypi:`pytest-argus`
+ *last release*: Jun 24, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=6.2.4)
+
+ pyest results colection plugin
+
+ :pypi:`pytest-arraydiff`
+ *last release*: Dec 06, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ pytest plugin to help with comparing array output from tests
+
+ :pypi:`pytest-asgi-server`
+ *last release*: Dec 12, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=5.4.1)
+
+ Convenient ASGI client/server fixtures for Pytest
+
+ :pypi:`pytest-asptest`
+ *last release*: Apr 28, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ test Answer Set Programming programs
+
+ :pypi:`pytest-assertutil`
+ *last release*: May 10, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest-assertutil
+
+ :pypi:`pytest-assert-utils`
+ *last release*: Sep 21, 2021,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Useful assertion utilities for use with pytest
+
+ :pypi:`pytest-assume`
+ *last release*: Jun 24, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=2.7)
+
+ A pytest plugin that allows multiple failures per test
+
+ :pypi:`pytest-ast-back-to-python`
+ *last release*: Sep 29, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A plugin for pytest devs to view how assertion rewriting recodes the AST
+
+ :pypi:`pytest-astropy`
+ *last release*: Sep 21, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.6)
+
+ Meta-package containing dependencies for testing
+
+ :pypi:`pytest-astropy-header`
+ *last release*: Dec 18, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=2.8)
+
+ pytest plugin to add diagnostic information to the header of the test output
+
+ :pypi:`pytest-ast-transformer`
+ *last release*: May 04, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+
+
+ :pypi:`pytest-asyncio`
+ *last release*: Oct 15, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=5.4.0)
+
+ Pytest support for asyncio.
+
+ :pypi:`pytest-asyncio-cooperative`
+ *last release*: Oct 12, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Run all your asynchronous tests cooperatively.
+
+ :pypi:`pytest-asyncio-network-simulator`
+ *last release*: Jul 31, 2018,
+ *status*: 3 - Alpha,
+ *requires*: pytest (<3.7.0,>=3.3.2)
+
+ pytest-asyncio-network-simulator: Plugin for pytest for simulator the network in tests
+
+ :pypi:`pytest-async-mongodb`
+ *last release*: Oct 18, 2017,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.5.2)
+
+ pytest plugin for async MongoDB
+
+ :pypi:`pytest-async-sqlalchemy`
+ *last release*: Oct 07, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.0.0)
+
+ Database testing fixtures using the SQLAlchemy asyncio API
+
+ :pypi:`pytest-atomic`
+ *last release*: Nov 24, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Skip rest of tests if previous test failed.
+
+ :pypi:`pytest-attrib`
+ *last release*: May 24, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ pytest plugin to select tests based on attributes similar to the nose-attrib plugin
+
+ :pypi:`pytest-austin`
+ *last release*: Oct 11, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Austin plugin for pytest
+
+ :pypi:`pytest-autochecklog`
+ *last release*: Apr 25, 2015,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ automatically check condition and log all the checks
+
+ :pypi:`pytest-automation`
+ *last release*: Oct 01, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ pytest plugin for building a test suite, using YAML files to extend pytest parameterize functionality.
+
+ :pypi:`pytest-automock`
+ *last release*: Apr 22, 2020,
+ *status*: N/A,
+ *requires*: pytest ; extra == 'dev'
+
+ Pytest plugin for automatical mocks creation
+
+ :pypi:`pytest-auto-parametrize`
+ *last release*: Oct 02, 2016,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest plugin: avoid repeating arguments in parametrize
+
+ :pypi:`pytest-autotest`
+ *last release*: Aug 25, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ This fixture provides a configured "driver" for Android Automated Testing, using uiautomator2.
+
+ :pypi:`pytest-avoidance`
+ *last release*: May 23, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Makes pytest skip tests that don not need rerunning
+
+ :pypi:`pytest-aws`
+ *last release*: Oct 04, 2017,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ pytest plugin for testing AWS resource configurations
+
+ :pypi:`pytest-aws-config`
+ *last release*: May 28, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Protect your AWS credentials in unit tests
+
+ :pypi:`pytest-axe`
+ *last release*: Nov 12, 2018,
+ *status*: N/A,
+ *requires*: pytest (>=3.0.0)
+
+ pytest plugin for axe-selenium-python
+
+ :pypi:`pytest-azurepipelines`
+ *last release*: Jul 23, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Formatting PyTest output for Azure Pipelines UI
+
+ :pypi:`pytest-bandit`
+ *last release*: Feb 23, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A bandit plugin for pytest
+
+ :pypi:`pytest-base-url`
+ *last release*: Jun 19, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.7.3)
+
+ pytest plugin for URL based testing
+
+ :pypi:`pytest-bdd`
+ *last release*: Oct 25, 2021,
+ *status*: 6 - Mature,
+ *requires*: pytest (>=4.3)
+
+ BDD for pytest
+
+ :pypi:`pytest-bdd-splinter`
+ *last release*: Aug 12, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.0.0)
+
+ Common steps for pytest bdd and splinter integration
+
+ :pypi:`pytest-bdd-web`
+ *last release*: Jan 02, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A simple plugin to use with pytest
+
+ :pypi:`pytest-bdd-wrappers`
+ *last release*: Feb 11, 2020,
+ *status*: 2 - Pre-Alpha,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-beakerlib`
+ *last release*: Mar 17, 2017,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ A pytest plugin that reports test results to the BeakerLib framework
+
+ :pypi:`pytest-beds`
+ *last release*: Jun 07, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Fixtures for testing Google Appengine (GAE) apps
+
+ :pypi:`pytest-bench`
+ *last release*: Jul 21, 2014,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Benchmark utility that plugs into pytest.
+
+ :pypi:`pytest-benchmark`
+ *last release*: Apr 17, 2021,
+ *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-bg-process`
+ *last release*: Aug 17, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Pytest plugin to initialize background process
+
+ :pypi:`pytest-bigchaindb`
+ *last release*: Aug 17, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A BigchainDB plugin for pytest.
+
+ :pypi:`pytest-bigquery-mock`
+ *last release*: Aug 05, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=5.0)
+
+ Provides a mock fixture for python bigquery client
+
+ :pypi:`pytest-black`
+ *last release*: Oct 05, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A pytest plugin to enable format checking with black
+
+ :pypi:`pytest-black-multipy`
+ *last release*: Jan 14, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (!=3.7.3,>=3.5) ; extra == 'testing'
+
+ Allow '--black' on older Pythons
+
+ :pypi:`pytest-blame`
+ *last release*: May 04, 2019,
+ *status*: N/A,
+ *requires*: pytest (>=4.4.0)
+
+ A pytest plugin helps developers to debug by providing useful commits history.
+
+ :pypi:`pytest-blender`
+ *last release*: Oct 29, 2021,
+ *status*: N/A,
+ *requires*: pytest (==6.2.5) ; extra == 'dev'
+
+ Blender Pytest plugin.
+
+ :pypi:`pytest-blink1`
+ *last release*: Jan 07, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Pytest plugin to emit notifications via the Blink(1) RGB LED
+
+ :pypi:`pytest-blockage`
+ *last release*: Feb 13, 2019,
+ *status*: N/A,
+ *requires*: pytest
+
+ Disable network requests during a test run.
+
+ :pypi:`pytest-blocker`
+ *last release*: Sep 07, 2015,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ pytest plugin to mark a test as blocker and skip all other tests
+
+ :pypi:`pytest-board`
+ *last release*: Jan 20, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Local continuous test runner with pytest and watchdog.
+
+ :pypi:`pytest-bpdb`
+ *last release*: Jan 19, 2015,
+ *status*: 2 - Pre-Alpha,
+ *requires*: N/A
+
+ A py.test plug-in to enable drop to bpdb debugger on test failure.
+
+ :pypi:`pytest-bravado`
+ *last release*: Jul 19, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest-bravado automatically generates from OpenAPI specification client fixtures.
+
+ :pypi:`pytest-breakword`
+ *last release*: Aug 04, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=6.2.4,<7.0.0)
+
+ Use breakword with pytest
+
+ :pypi:`pytest-breed-adapter`
+ *last release*: Nov 07, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A simple plugin to connect with breed-server
+
+ :pypi:`pytest-briefcase`
+ *last release*: Jun 14, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A pytest plugin for running tests on a Briefcase project.
+
+ :pypi:`pytest-browser`
+ *last release*: Dec 10, 2016,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ A pytest plugin for console based browser test selection just after the collection phase
+
+ :pypi:`pytest-browsermob-proxy`
+ *last release*: Jun 11, 2013,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ BrowserMob proxy plugin for py.test.
+
+ :pypi:`pytest-browserstack-local`
+ *last release*: Feb 09, 2018,
+ *status*: N/A,
+ *requires*: N/A
+
+ \`\`py.test\`\` plugin to run \`\`BrowserStackLocal\`\` in background.
+
+ :pypi:`pytest-bug`
+ *last release*: Jun 02, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.6.0)
+
+ Pytest plugin for marking tests as a bug
+
+ :pypi:`pytest-bugtong-tag`
+ *last release*: Apr 23, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest-bugtong-tag is a plugin for pytest
+
+ :pypi:`pytest-bugzilla`
+ *last release*: May 05, 2010,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ py.test bugzilla integration plugin
+
+ :pypi:`pytest-bugzilla-notifier`
+ *last release*: Jun 15, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.9.2)
+
+ A plugin that allows you to execute create, update, and read information from BugZilla bugs
+
+ :pypi:`pytest-buildkite`
+ *last release*: Jul 13, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Plugin for pytest that automatically publishes coverage and pytest report annotations to Buildkite.
+
+ :pypi:`pytest-builtin-types`
+ *last release*: Nov 17, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+
+
+ :pypi:`pytest-bwrap`
+ *last release*: Oct 26, 2018,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Run your tests in Bubblewrap sandboxes
+
+ :pypi:`pytest-cache`
+ *last release*: Jun 04, 2013,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest plugin with mechanisms for caching across test runs
+
+ :pypi:`pytest-cache-assert`
+ *last release*: Nov 03, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=5)
+
+ Cache assertion data to simplify regression testing of complex serializable data
+
+ :pypi:`pytest-cagoule`
+ *last release*: Jan 01, 2020,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Pytest plugin to only run tests affected by changes
+
+ :pypi:`pytest-camel-collect`
+ *last release*: Aug 02, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=2.9)
+
+ Enable CamelCase-aware pytest class collection
+
+ :pypi:`pytest-canonical-data`
+ *last release*: May 08, 2020,
+ *status*: 2 - Pre-Alpha,
+ *requires*: pytest (>=3.5.0)
+
+ A plugin which allows to compare results with canonical results, based on previous runs
+
+ :pypi:`pytest-caprng`
+ *last release*: May 02, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A plugin that replays pRNG state on failure.
+
+ :pypi:`pytest-capture-deprecatedwarnings`
+ *last release*: Apr 30, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ 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
+
+ A sample Python project
+
+ :pypi:`pytest-cases`
+ *last release*: Nov 08, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Separate test code from test cases in pytest.
+
+ :pypi:`pytest-cassandra`
+ *last release*: Nov 04, 2017,
+ *status*: 1 - Planning,
+ *requires*: N/A
+
+ Cassandra CCM Test Fixtures for pytest
+
+ :pypi:`pytest-catchlog`
+ *last release*: Jan 24, 2016,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.6)
+
+ py.test plugin to catch log messages. This is a fork of pytest-capturelog.
+
+ :pypi:`pytest-catch-server`
+ *last release*: Dec 12, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Pytest plugin with server for catching HTTP requests.
+
+ :pypi:`pytest-celery`
+ *last release*: May 06, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest-celery a shim pytest plugin to enable celery.contrib.pytest
+
+ :pypi:`pytest-chainmaker`
+ *last release*: Oct 15, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest plugin for chainmaker
+
+ :pypi:`pytest-chalice`
+ *last release*: Jul 01, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A set of py.test fixtures for AWS Chalice
+
+ :pypi:`pytest-change-report`
+ *last release*: Sep 14, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ turn . into √,turn F into x
+
+ :pypi:`pytest-chdir`
+ *last release*: Jan 28, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=5.0.0,<6.0.0)
+
+ A pytest fixture for changing current working directory
+
+ :pypi:`pytest-checkdocs`
+ *last release*: Jul 31, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.6) ; extra == 'testing'
+
+ check the README when running tests
+
+ :pypi:`pytest-checkipdb`
+ *last release*: Jul 22, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.9.2)
+
+ plugin to check if there are ipdb debugs left
+
+ :pypi:`pytest-check-links`
+ *last release*: Jul 29, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=4.6)
+
+ Check links in files
+
+ :pypi:`pytest-check-mk`
+ *last release*: Nov 19, 2015,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ pytest plugin to test Check_MK checks
+
+ :pypi:`pytest-circleci`
+ *last release*: May 03, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ py.test plugin for CircleCI
+
+ :pypi:`pytest-circleci-parallelized`
+ *last release*: Mar 26, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Parallelize pytest across CircleCI workers.
+
+ :pypi:`pytest-ckan`
+ *last release*: Apr 28, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ Backport of CKAN 2.9 pytest plugin and fixtures to CAKN 2.8
+
+ :pypi:`pytest-clarity`
+ *last release*: Jun 11, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ A plugin providing an alternative, colourful diff output for failing assertions.
+
+ :pypi:`pytest-cldf`
+ *last release*: May 06, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Easy quality control for CLDF datasets using pytest
+
+ :pypi:`pytest-click`
+ *last release*: Aug 29, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.0)
+
+ Py.test plugin for Click
+
+ :pypi:`pytest-clld`
+ *last release*: Nov 29, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=3.6)
+
+
+
+ :pypi:`pytest-cloud`
+ *last release*: Oct 05, 2020,
+ *status*: 6 - Mature,
+ *requires*: N/A
+
+ Distributed tests planner plugin for pytest testing framework.
+
+ :pypi:`pytest-cloudflare-worker`
+ *last release*: Mar 30, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.0.0)
+
+ pytest plugin for testing cloudflare workers
+
+ :pypi:`pytest-cobra`
+ *last release*: Jun 29, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest (<4.0.0,>=3.7.1)
+
+ PyTest plugin for testing Smart Contracts for Ethereum blockchain.
+
+ :pypi:`pytest-codeblocks`
+ *last release*: Oct 13, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6)
+
+ Test code blocks in your READMEs
+
+ :pypi:`pytest-codecheckers`
+ *last release*: Feb 13, 2010,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest plugin to add source code sanity checks (pep8 and friends)
+
+ :pypi:`pytest-codecov`
+ *last release*: Oct 27, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=4.6.0)
+
+ Pytest plugin for uploading pytest-cov results to codecov.io
+
+ :pypi:`pytest-codegen`
+ *last release*: Aug 23, 2020,
+ *status*: 2 - Pre-Alpha,
+ *requires*: N/A
+
+ Automatically create pytest test signatures
+
+ :pypi:`pytest-codestyle`
+ *last release*: Mar 23, 2020,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest plugin to run pycodestyle
+
+ :pypi:`pytest-collect-formatter`
+ *last release*: Mar 29, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Formatter for pytest collect output
+
+ :pypi:`pytest-collect-formatter2`
+ *last release*: May 31, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Formatter for pytest collect output
+
+ :pypi:`pytest-colordots`
+ *last release*: Oct 06, 2017,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Colorizes the progress indicators
+
+ :pypi:`pytest-commander`
+ *last release*: Aug 17, 2021,
+ *status*: N/A,
+ *requires*: pytest (<7.0.0,>=6.2.4)
+
+ An interactive GUI test runner for PyTest
+
+ :pypi:`pytest-common-subject`
+ *last release*: Nov 12, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=3.6,<7)
+
+ pytest framework for testing different aspects of a common method
+
+ :pypi:`pytest-concurrent`
+ *last release*: Jan 12, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.1.1)
+
+ Concurrently execute test cases with multithread, multiprocess and gevent
+
+ :pypi:`pytest-config`
+ *last release*: Nov 07, 2014,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Base configurations and utilities for developing your Python project test suite with pytest.
+
+ :pypi:`pytest-confluence-report`
+ *last release*: Nov 06, 2020,
+ *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,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Pytest plugin for testing console scripts
+
+ :pypi:`pytest-consul`
+ *last release*: Nov 24, 2018,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ pytest plugin with fixtures for testing consul aware apps
+
+ :pypi:`pytest-container`
+ *last release*: Nov 19, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.10)
+
+ Pytest fixtures for writing container based tests
+
+ :pypi:`pytest-contextfixture`
+ *last release*: Mar 12, 2013,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Define pytest fixtures as context managers.
+
+ :pypi:`pytest-contexts`
+ *last release*: May 19, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A plugin to run tests written with the Contexts framework using pytest
+
+ :pypi:`pytest-cookies`
+ *last release*: May 24, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.3.0)
+
+ The pytest plugin for your Cookiecutter templates. 🍪
+
+ :pypi:`pytest-couchdbkit`
+ *last release*: Apr 17, 2012,
+ *status*: N/A,
+ *requires*: N/A
+
+ py.test extension for per-test couchdb databases using couchdbkit
+
+ :pypi:`pytest-count`
+ *last release*: Jan 12, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ count erros and send email
+
+ :pypi:`pytest-cov`
+ *last release*: Oct 04, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.6)
+
+ Pytest plugin for measuring coverage.
+
+ :pypi:`pytest-cover`
+ *last release*: Aug 01, 2015,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Pytest plugin for measuring coverage. Forked from \`pytest-cov\`.
+
+ :pypi:`pytest-coverage`
+ *last release*: Jun 17, 2015,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-coverage-context`
+ *last release*: Jan 04, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.1.0)
+
+ Coverage dynamic context support for PyTest, including sub-processes
+
+ :pypi:`pytest-cov-exclude`
+ *last release*: Apr 29, 2016,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.8.0,<2.9.0); extra == 'dev'
+
+ Pytest plugin for excluding tests based on coverage data
+
+ :pypi:`pytest-cpp`
+ *last release*: Dec 03, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (!=5.4.0,!=5.4.1)
+
+ Use pytest's runner to discover and execute C++ tests
+
+ :pypi:`pytest-cram`
+ *last release*: Aug 08, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ Run cram tests with pytest.
+
+ :pypi:`pytest-crate`
+ *last release*: May 28, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=4.0)
+
+ Manages CrateDB instances during your integration tests
+
+ :pypi:`pytest-cricri`
+ *last release*: Jan 27, 2018,
+ *status*: N/A,
+ *requires*: pytest
+
+ A Cricri plugin for pytest.
+
+ :pypi:`pytest-crontab`
+ *last release*: Dec 09, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ add crontab task in crontab
+
+ :pypi:`pytest-csv`
+ *last release*: Apr 22, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=6.0)
+
+ CSV output for pytest.
+
+ :pypi:`pytest-curio`
+ *last release*: Oct 07, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest support for curio.
+
+ :pypi:`pytest-curl-report`
+ *last release*: Dec 11, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ pytest plugin to generate curl command line report
+
+ :pypi:`pytest-custom-concurrency`
+ *last release*: Feb 08, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Custom grouping concurrence for pytest
+
+ :pypi:`pytest-custom-exit-code`
+ *last release*: Aug 07, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=4.0.2)
+
+ Exit pytest test session with custom exit code in different scenarios
+
+ :pypi:`pytest-custom-nodeid`
+ *last release*: Mar 07, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Custom grouping for pytest-xdist, rename test cases name and test cases nodeid, support allure report
+
+ :pypi:`pytest-custom-report`
+ *last release*: Jan 30, 2019,
+ *status*: N/A,
+ *requires*: pytest
+
+ Configure the symbols displayed for test outcomes
+
+ :pypi:`pytest-custom-scheduling`
+ *last release*: Mar 01, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ 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)
+
+ A plugin for testing Cython extension modules
+
+ :pypi:`pytest-darker`
+ *last release*: Aug 16, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=6.0.1) ; extra == 'test'
+
+ A pytest plugin for checking of modified code using Darker
+
+ :pypi:`pytest-dash`
+ *last release*: Mar 18, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest fixtures to run dash applications.
+
+ :pypi:`pytest-data`
+ *last release*: Nov 01, 2016,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Useful functions for managing data for pytest fixtures
+
+ :pypi:`pytest-databricks`
+ *last release*: Jul 29, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ Pytest plugin for remote Databricks notebooks testing
+
+ :pypi:`pytest-datadir`
+ *last release*: Oct 22, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.7.0)
+
+ pytest plugin for test data directories and files
+
+ :pypi:`pytest-datadir-mgr`
+ *last release*: Aug 16, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Manager for test data providing downloads, caching of generated files, and a context for temp directories.
+
+ :pypi:`pytest-datadir-ng`
+ *last release*: Dec 25, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Fixtures for pytest allowing test functions/methods to easily retrieve test resources from the local filesystem.
+
+ :pypi:`pytest-data-file`
+ *last release*: Dec 04, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Fixture "data" and "case_data" for test from yaml file
+
+ :pypi:`pytest-datafiles`
+ *last release*: Oct 07, 2018,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.6)
+
+ py.test plugin to create a 'tmpdir' containing predefined files/directories.
+
+ :pypi:`pytest-datafixtures`
+ *last release*: Dec 05, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Data fixtures for pytest made simple
+
+ :pypi:`pytest-data-from-files`
+ *last release*: Oct 13, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ pytest plugin to provide data from files loaded automatically
+
+ :pypi:`pytest-dataplugin`
+ *last release*: Sep 16, 2017,
+ *status*: 1 - Planning,
+ *requires*: N/A
+
+ A pytest plugin for managing an archive of test data.
+
+ :pypi:`pytest-datarecorder`
+ *last release*: Apr 20, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ A py.test plugin recording and comparing test output.
+
+ :pypi:`pytest-datatest`
+ *last release*: Oct 15, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.3)
+
+ A pytest plugin for test driven data-wrangling (this is the development version of datatest's pytest integration).
+
+ :pypi:`pytest-db`
+ *last release*: Dec 04, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Session scope fixture "db" for mysql query or change
+
+ :pypi:`pytest-dbfixtures`
+ *last release*: Dec 07, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Databases fixtures plugin for py.test.
+
+ :pypi:`pytest-db-plugin`
+ *last release*: Nov 27, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=5.0)
+
+
+
+ :pypi:`pytest-dbt-adapter`
+ *last release*: Nov 24, 2021,
+ *status*: N/A,
+ *requires*: pytest (<7,>=6)
+
+ A pytest plugin for testing dbt adapter plugins
+
+ :pypi:`pytest-dbus-notification`
+ *last release*: Mar 05, 2014,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ D-BUS notifications for pytest results.
+
+ :pypi:`pytest-deadfixtures`
+ *last release*: Jul 23, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ A simple plugin to list unused fixtures in pytest
+
+ :pypi:`pytest-deepcov`
+ *last release*: Mar 30, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ deepcov
+
+ :pypi:`pytest-defer`
+ *last release*: Aug 24, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-demo-plugin`
+ *last release*: May 15, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest示例插件
+
+ :pypi:`pytest-dependency`
+ *last release*: Feb 14, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Manage dependencies of tests
+
+ :pypi:`pytest-depends`
+ *last release*: Apr 05, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3)
+
+ Tests that depend on other tests
+
+ :pypi:`pytest-deprecate`
+ *last release*: Jul 01, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ 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)
+
+ Describe-style plugin for pytest
+
+ :pypi:`pytest-describe-it`
+ *last release*: Jul 19, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ plugin for rich text descriptions
+
+ :pypi:`pytest-devpi-server`
+ *last release*: May 28, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ DevPI server fixture for py.test
+
+ :pypi:`pytest-diamond`
+ *last release*: Aug 31, 2015,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ pytest plugin for diamond
+
+ :pypi:`pytest-dicom`
+ *last release*: Dec 19, 2018,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ pytest plugin to provide DICOM fixtures
+
+ :pypi:`pytest-dictsdiff`
+ *last release*: Jul 26, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-diff`
+ *last release*: Mar 30, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A simple plugin to use with pytest
+
+ :pypi:`pytest-disable`
+ *last release*: Sep 10, 2015,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ pytest plugin to disable a test and skip it from testrun
+
+ :pypi:`pytest-disable-plugin`
+ *last release*: Feb 28, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Disable plugins per test
+
+ :pypi:`pytest-discord`
+ *last release*: Mar 20, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (!=6.0.0,<7,>=3.3.2)
+
+ A pytest plugin to notify test results to a Discord channel.
+
+ :pypi:`pytest-django`
+ *last release*: Dec 02, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.4.0)
+
+ A Django plugin for pytest.
+
+ :pypi:`pytest-django-ahead`
+ *last release*: Oct 27, 2016,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.9)
+
+ A Django plugin for pytest.
+
+ :pypi:`pytest-djangoapp`
+ *last release*: Aug 04, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Nice pytest plugin to help you with Django pluggable application testing.
+
+ :pypi:`pytest-django-cache-xdist`
+ *last release*: May 12, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A djangocachexdist plugin for pytest
+
+ :pypi:`pytest-django-casperjs`
+ *last release*: Mar 15, 2015,
+ *status*: 2 - Pre-Alpha,
+ *requires*: N/A
+
+ Integrate CasperJS with your django tests as a pytest fixture.
+
+ :pypi:`pytest-django-dotenv`
+ *last release*: Nov 26, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.6.0)
+
+ Pytest plugin used to setup environment variables with django-dotenv
+
+ :pypi:`pytest-django-factories`
+ *last release*: Nov 12, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Factories for your Django models that can be used as Pytest fixtures.
+
+ :pypi:`pytest-django-gcir`
+ *last release*: Mar 06, 2018,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ A Django plugin for pytest.
+
+ :pypi:`pytest-django-haystack`
+ *last release*: Sep 03, 2017,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.3.4)
+
+ Cleanup your Haystack indexes between tests
+
+ :pypi:`pytest-django-ifactory`
+ *last release*: Jan 13, 2021,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ A model instance factory for pytest-django
+
+ :pypi:`pytest-django-lite`
+ *last release*: Jan 30, 2014,
+ *status*: N/A,
+ *requires*: N/A
+
+ The bare minimum to integrate py.test with Django.
+
+ :pypi:`pytest-django-liveserver-ssl`
+ *last release*: Jul 30, 2021,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-django-model`
+ *last release*: Feb 14, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A Simple Way to Test your Django Models
+
+ :pypi:`pytest-django-ordering`
+ *last release*: Jul 25, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.3.0)
+
+ A pytest plugin for preserving the order in which Django runs tests.
+
+ :pypi:`pytest-django-queries`
+ *last release*: Mar 01, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Generate performance reports from your django database performance tests.
+
+ :pypi:`pytest-djangorestframework`
+ *last release*: Aug 11, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A djangorestframework plugin for pytest
+
+ :pypi:`pytest-django-rq`
+ *last release*: Apr 13, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A pytest plugin to help writing unit test for django-rq
+
+ :pypi:`pytest-django-sqlcounts`
+ *last release*: Jun 16, 2015,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ 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,
+ *requires*: N/A
+
+ Use a temporary PostgreSQL database with pytest-django
+
+ :pypi:`pytest-doc`
+ *last release*: Jun 28, 2015,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ A documentation plugin for py.test.
+
+ :pypi:`pytest-docgen`
+ *last release*: Apr 17, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ An RST Documentation Generator for pytest-based test suites
+
+ :pypi:`pytest-docker`
+ *last release*: Jun 14, 2021,
+ *status*: N/A,
+ *requires*: pytest (<7.0,>=4.0)
+
+ Simple pytest fixtures for Docker and docker-compose based tests
+
+ :pypi:`pytest-docker-butla`
+ *last release*: Jun 16, 2019,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-dockerc`
+ *last release*: Oct 09, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.0)
+
+ Run, manage and stop Docker Compose project from Docker API
+
+ :pypi:`pytest-docker-compose`
+ *last release*: Jan 26, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.3)
+
+ Manages Docker containers during your integration tests
+
+ :pypi:`pytest-docker-db`
+ *last release*: Mar 20, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.1.1)
+
+ A plugin to use docker databases for pytests
+
+ :pypi:`pytest-docker-fixtures`
+ *last release*: Nov 23, 2021,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest docker fixtures
+
+ :pypi:`pytest-docker-git-fixtures`
+ *last release*: Mar 11, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ Pytest fixtures for testing with git scm.
+
+ :pypi:`pytest-docker-pexpect`
+ *last release*: Jan 14, 2019,
+ *status*: N/A,
+ *requires*: pytest
+
+ pytest plugin for writing functional tests with pexpect and docker
+
+ :pypi:`pytest-docker-postgresql`
+ *last release*: Sep 24, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A simple plugin to use with pytest
+
+ :pypi:`pytest-docker-py`
+ *last release*: Nov 27, 2018,
+ *status*: N/A,
+ *requires*: pytest (==4.0.0)
+
+ Easy to use, simple to extend, pytest plugin that minimally leverages docker-py.
+
+ :pypi:`pytest-docker-registry-fixtures`
+ *last release*: Mar 04, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ Pytest fixtures for testing with docker registries.
+
+ :pypi:`pytest-docker-tools`
+ *last release*: Jul 23, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.0.1,<7.0.0)
+
+ Docker integration tests for pytest
+
+ :pypi:`pytest-docs`
+ *last release*: Nov 11, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Documentation tool for pytest
+
+ :pypi:`pytest-docstyle`
+ *last release*: Mar 23, 2020,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest plugin to run pydocstyle
+
+ :pypi:`pytest-doctest-custom`
+ *last release*: Jul 25, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A py.test plugin for customizing string representations of doctest results.
+
+ :pypi:`pytest-doctest-ellipsis-markers`
+ *last release*: Jan 12, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Setup additional values for ELLIPSIS_MARKER for doctests
+
+ :pypi:`pytest-doctest-import`
+ *last release*: Nov 13, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.3.0)
+
+ A simple pytest plugin to import names and add them to the doctest namespace.
+
+ :pypi:`pytest-doctestplus`
+ *last release*: Nov 16, 2021,
+ *status*: 3 - Alpha,
+ *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)
+
+ A plugin to run doctests in docstrings of Numpy ufuncs
+
+ :pypi:`pytest-dolphin`
+ *last release*: Nov 30, 2016,
+ *status*: 4 - Beta,
+ *requires*: pytest (==3.0.4)
+
+ Some extra stuff that we use ininternally
+
+ :pypi:`pytest-doorstop`
+ *last release*: Jun 09, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A pytest plugin for adding test results into doorstop items.
+
+ :pypi:`pytest-dotenv`
+ *last release*: Jun 16, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=5.0.0)
+
+ A py.test plugin that parses environment files before running tests
+
+ :pypi:`pytest-drf`
+ *last release*: Nov 12, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.6)
+
+ A Django REST framework plugin for pytest.
+
+ :pypi:`pytest-drivings`
+ *last release*: Jan 13, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ 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)
+
+ A Pytest plugin to drop duplicated tests during collection
+
+ :pypi:`pytest-dummynet`
+ *last release*: Oct 13, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ A py.test plugin providing access to a dummynet.
+
+ :pypi:`pytest-dump2json`
+ *last release*: Jun 29, 2015,
+ *status*: N/A,
+ *requires*: N/A
+
+ A pytest plugin for dumping test results to json.
+
+ :pypi:`pytest-duration-insights`
+ *last release*: Jun 25, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-dynamicrerun`
+ *last release*: Aug 15, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A pytest plugin to rerun tests dynamically based off of test outcome and output.
+
+ :pypi:`pytest-dynamodb`
+ *last release*: Jun 03, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ DynamoDB fixtures for pytest
+
+ :pypi:`pytest-easy-addoption`
+ *last release*: Jan 22, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest-easy-addoption: Easy way to work with pytest addoption
+
+ :pypi:`pytest-easy-api`
+ *last release*: Mar 26, 2018,
+ *status*: N/A,
+ *requires*: N/A
+
+ Simple API testing with pytest
+
+ :pypi:`pytest-easyMPI`
+ *last release*: Oct 21, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ Package that supports mpi tests in pytest
+
+ :pypi:`pytest-easyread`
+ *last release*: Nov 17, 2017,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest plugin that makes terminal printouts of the reports easier to read
+
+ :pypi:`pytest-easy-server`
+ *last release*: May 01, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (<5.0.0,>=4.3.1) ; python_version < "3.5"
+
+ Pytest plugin for easy testing against servers
+
+ :pypi:`pytest-ec2`
+ *last release*: Oct 22, 2019,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Pytest execution on EC2 instance
+
+ :pypi:`pytest-echo`
+ *last release*: Jan 08, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ pytest plugin with mechanisms for echoing environment variables, package version and generic attributes
+
+ :pypi:`pytest-elasticsearch`
+ *last release*: May 12, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.0.0)
+
+ Elasticsearch fixtures and fixture factories for Pytest.
+
+ :pypi:`pytest-elements`
+ *last release*: Jan 13, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=5.4,<6.0)
+
+ Tool to help automate user interfaces
+
+ :pypi:`pytest-elk-reporter`
+ *last release*: Jan 24, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A simple plugin to use with pytest
+
+ :pypi:`pytest-email`
+ *last release*: Jul 08, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ Send execution result email
+
+ :pypi:`pytest-embedded`
+ *last release*: Nov 29, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=6.2.0)
+
+ pytest embedded plugin
+
+ :pypi:`pytest-embedded-idf`
+ *last release*: Nov 29, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest embedded plugin for esp-idf project
+
+ :pypi:`pytest-embedded-jtag`
+ *last release*: Nov 29, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest embedded plugin for testing with jtag
+
+ :pypi:`pytest-embedded-qemu`
+ *last release*: Nov 29, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest embedded plugin for qemu, not target chip
+
+ :pypi:`pytest-embedded-qemu-idf`
+ *last release*: Jun 29, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest embedded plugin for esp-idf project by qemu, not target chip
+
+ :pypi:`pytest-embedded-serial`
+ *last release*: Nov 29, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest embedded plugin for testing serial ports
+
+ :pypi:`pytest-embedded-serial-esp`
+ *last release*: Nov 29, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest embedded plugin for testing espressif boards via serial ports
+
+ :pypi:`pytest-emoji`
+ *last release*: Feb 19, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=4.2.1)
+
+ A pytest plugin that adds emojis to your test result report
+
+ :pypi:`pytest-emoji-output`
+ *last release*: Oct 10, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (==6.0.1)
+
+ Pytest plugin to represent test output with emoji support
+
+ :pypi:`pytest-enabler`
+ *last release*: Nov 08, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=6) ; extra == 'testing'
+
+ Enable installed pytest plugins
+
+ :pypi:`pytest-encode`
+ *last release*: Nov 06, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ set your encoding and logger
+
+ :pypi:`pytest-encode-kane`
+ *last release*: Nov 16, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ set your encoding and logger
+
+ :pypi:`pytest-enhancements`
+ *last release*: Oct 30, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Improvements for pytest (rejected upstream)
+
+ :pypi:`pytest-env`
+ *last release*: Jun 16, 2017,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ py.test plugin that allows you to add environment variables.
+
+ :pypi:`pytest-envfiles`
+ *last release*: Oct 08, 2015,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ A py.test plugin that parses environment files before running tests
+
+ :pypi:`pytest-env-info`
+ *last release*: Nov 25, 2017,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.1.1)
+
+ Push information about the running pytest into envvars
+
+ :pypi:`pytest-envraw`
+ *last release*: Aug 27, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.6.0)
+
+ py.test plugin that allows you to add environment variables.
+
+ :pypi:`pytest-envvars`
+ *last release*: Jun 13, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.0.0)
+
+ Pytest plugin to validate use of envvars on your tests
+
+ :pypi:`pytest-env-yaml`
+ *last release*: Apr 02, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-eradicate`
+ *last release*: Sep 08, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=2.4.2)
+
+ pytest plugin to check for commented out code
+
+ :pypi:`pytest-error-for-skips`
+ *last release*: Dec 19, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=4.6)
+
+ Pytest plugin to treat skipped tests a test failure
+
+ :pypi:`pytest-eth`
+ *last release*: Aug 14, 2020,
+ *status*: 1 - Planning,
+ *requires*: N/A
+
+ PyTest plugin for testing Smart Contracts for Ethereum Virtual Machine (EVM).
+
+ :pypi:`pytest-ethereum`
+ *last release*: Jun 24, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest (==3.3.2); extra == 'dev'
+
+ pytest-ethereum: Pytest library for ethereum projects.
+
+ :pypi:`pytest-eucalyptus`
+ *last release*: Aug 13, 2019,
+ *status*: N/A,
+ *requires*: pytest (>=4.2.0)
+
+ Pytest Plugin for BDD
+
+ :pypi:`pytest-eventlet`
+ *last release*: Oct 04, 2021,
+ *status*: N/A,
+ *requires*: pytest ; extra == 'dev'
+
+ Applies eventlet monkey-patch as a pytest plugin.
+
+ :pypi:`pytest-excel`
+ *last release*: Oct 06, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ pytest plugin for generating excel reports
+
+ :pypi:`pytest-exceptional`
+ *last release*: Mar 16, 2017,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Better exceptions
+
+ :pypi:`pytest-exception-script`
+ *last release*: Aug 04, 2020,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ 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)
+
+ pytest plugin for testing executables
+
+ :pypi:`pytest-expect`
+ *last release*: Apr 21, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ py.test plugin to store test expectations and mark tests based on them
+
+ :pypi:`pytest-expecter`
+ *last release*: Jul 08, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Better testing with expecter and pytest.
+
+ :pypi:`pytest-expectr`
+ *last release*: Oct 05, 2018,
+ *status*: N/A,
+ *requires*: pytest (>=2.4.2)
+
+ This plugin is used to expect multiple assert using pytest framework.
+
+ :pypi:`pytest-explicit`
+ *last release*: Jun 15, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ A Pytest plugin to ignore certain marked tests by default
+
+ :pypi:`pytest-exploratory`
+ *last release*: Aug 03, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=5.3)
+
+ Interactive console for pytest.
+
+ :pypi:`pytest-external-blockers`
+ *last release*: Oct 05, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ a special outcome for tests that are blocked for external reasons
+
+ :pypi:`pytest-extra-durations`
+ *last release*: Apr 21, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A pytest plugin to get durations on a per-function basis and per module basis.
+
+ :pypi:`pytest-fabric`
+ *last release*: Sep 12, 2018,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Provides test utilities to run fabric task tests by using docker containers
+
+ :pypi:`pytest-factory`
+ *last release*: Sep 06, 2020,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>4.3)
+
+ Use factories for test setup with py.test
+
+ :pypi:`pytest-factoryboy`
+ *last release*: Dec 30, 2020,
+ *status*: 6 - Mature,
+ *requires*: pytest (>=4.6)
+
+ Factory Boy support for pytest.
+
+ :pypi:`pytest-factoryboy-fixtures`
+ *last release*: Jun 25, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ Generates pytest fixtures that allow the use of type hinting
+
+ :pypi:`pytest-factoryboy-state`
+ *last release*: Dec 11, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=5.0)
+
+ Simple factoryboy random state management
+
+ :pypi:`pytest-failed-screenshot`
+ *last release*: Apr 21, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Test case fails,take a screenshot,save it,attach it to the allure
+
+ :pypi:`pytest-failed-to-verify`
+ *last release*: Aug 08, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.1.0)
+
+ A pytest plugin that helps better distinguishing real test failures from setup flakiness.
+
+ :pypi:`pytest-faker`
+ *last release*: Dec 19, 2016,
+ *status*: 6 - Mature,
+ *requires*: N/A
+
+ Faker integration with the pytest framework.
+
+ :pypi:`pytest-falcon`
+ *last release*: Sep 07, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Pytest helpers for Falcon.
+
+ :pypi:`pytest-falcon-client`
+ *last release*: Mar 19, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest \`client\` fixture for the Falcon Framework
+
+ :pypi:`pytest-fantasy`
+ *last release*: Mar 14, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest plugin for Flask Fantasy Framework
+
+ :pypi:`pytest-fastapi`
+ *last release*: Dec 27, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-fastest`
+ *last release*: Mar 05, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ Use SCM and coverage to run only needed tests
+
+ :pypi:`pytest-fast-first`
+ *last release*: Apr 02, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ Pytest plugin that runs fast tests first
+
+ :pypi:`pytest-faulthandler`
+ *last release*: Jul 04, 2019,
+ *status*: 6 - Mature,
+ *requires*: pytest (>=5.0)
+
+ py.test plugin that activates the fault handler module for tests (dummy package)
+
+ :pypi:`pytest-fauxfactory`
+ *last release*: Dec 06, 2017,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.2)
+
+ Integration of fauxfactory into pytest.
+
+ :pypi:`pytest-figleaf`
+ *last release*: Jan 18, 2010,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ py.test figleaf coverage plugin
+
+ :pypi:`pytest-filecov`
+ *last release*: Jun 27, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ A pytest plugin to detect unused files
+
+ :pypi:`pytest-filedata`
+ *last release*: Jan 17, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ easily load data from files
+
+ :pypi:`pytest-filemarker`
+ *last release*: Dec 01, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ A pytest plugin that runs marked tests when files change.
+
+ :pypi:`pytest-filter-case`
+ *last release*: Nov 05, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ run test cases filter by mark
+
+ :pypi:`pytest-filter-subpackage`
+ *last release*: Jan 09, 2020,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.0)
+
+ Pytest plugin for filtering based on sub-packages
+
+ :pypi:`pytest-find-dependencies`
+ *last release*: Apr 21, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A pytest plugin to find dependencies between tests
+
+ :pypi:`pytest-finer-verdicts`
+ *last release*: Jun 18, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=5.4.3)
+
+ A pytest plugin to treat non-assertion failures as test errors.
+
+ :pypi:`pytest-firefox`
+ *last release*: Aug 08, 2017,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.0.2)
+
+ pytest plugin to manipulate firefox
+
+ :pypi:`pytest-fixture-config`
+ *last release*: May 28, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Fixture configuration utils for py.test
+
+ :pypi:`pytest-fixture-maker`
+ *last release*: Sep 21, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest plugin to load fixtures from YAML files
+
+ :pypi:`pytest-fixture-marker`
+ *last release*: Oct 11, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ A pytest plugin to add markers based on fixtures used.
+
+ :pypi:`pytest-fixture-order`
+ *last release*: Aug 25, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=3.0)
+
+ pytest plugin to control fixture evaluation order
+
+ :pypi:`pytest-fixtures`
+ *last release*: May 01, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Common fixtures for pytest
+
+ :pypi:`pytest-fixture-tools`
+ *last release*: Aug 18, 2020,
+ *status*: 6 - Mature,
+ *requires*: pytest
+
+ Plugin for pytest which provides tools for fixtures
+
+ :pypi:`pytest-fixture-typecheck`
+ *last release*: Aug 24, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ A pytest plugin to assert type annotations at runtime.
+
+ :pypi:`pytest-flake8`
+ *last release*: Dec 16, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5)
+
+ pytest plugin to check FLAKE8 requirements
+
+ :pypi:`pytest-flake8-path`
+ *last release*: Aug 11, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ A pytest fixture for testing flake8 plugins.
+
+ :pypi:`pytest-flakefinder`
+ *last release*: Jul 28, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.7.1)
+
+ Runs tests multiple times to expose flakiness.
+
+ :pypi:`pytest-flakes`
+ *last release*: Dec 02, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5)
+
+ pytest plugin to check source code with pyflakes
+
+ :pypi:`pytest-flaptastic`
+ *last release*: Mar 17, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Flaptastic py.test plugin
+
+ :pypi:`pytest-flask`
+ *last release*: Feb 27, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.2)
+
+ A set of py.test fixtures to test Flask applications.
+
+ :pypi:`pytest-flask-sqlalchemy`
+ *last release*: Apr 04, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.2.1)
+
+ A pytest plugin for preserving test isolation in Flask-SQlAlchemy using database transactions.
+
+ :pypi:`pytest-flask-sqlalchemy-transactions`
+ *last release*: Aug 02, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.2.1)
+
+ Run tests in transactions using pytest, Flask, and SQLalchemy.
+
+ :pypi:`pytest-flyte`
+ *last release*: May 03, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ Pytest fixtures for simplifying Flyte integration testing
+
+ :pypi:`pytest-focus`
+ *last release*: May 04, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ A pytest plugin that alerts user of failed test cases with screen notifications
+
+ :pypi:`pytest-forcefail`
+ *last release*: May 15, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ py.test plugin to make the test failing regardless of pytest.mark.xfail
+
+ :pypi:`pytest-forward-compatability`
+ *last release*: Sep 06, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ A name to avoid typosquating pytest-foward-compatibility
+
+ :pypi:`pytest-forward-compatibility`
+ *last release*: Sep 29, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ A pytest plugin to shim pytest commandline options for fowards compatibility
+
+ :pypi:`pytest-freezegun`
+ *last release*: Jul 19, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.0.0)
+
+ Wrap tests with fixtures in freeze_time
+
+ :pypi:`pytest-freeze-reqs`
+ *last release*: Apr 29, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Check if requirement files are frozen
+
+ :pypi:`pytest-frozen-uuids`
+ *last release*: Oct 19, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=3.0)
+
+ Deterministically frozen UUID's for your tests
+
+ :pypi:`pytest-func-cov`
+ *last release*: Apr 15, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=5)
+
+ Pytest plugin for measuring function coverage
+
+ :pypi:`pytest-funparam`
+ *last release*: Dec 02, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest >=4.6.0
+
+ An alternative way to parametrize test cases.
+
+ :pypi:`pytest-fxa`
+ *last release*: Aug 28, 2018,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ pytest plugin for Firefox Accounts
+
+ :pypi:`pytest-fxtest`
+ *last release*: Oct 27, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-gc`
+ *last release*: Feb 01, 2018,
+ *status*: N/A,
+ *requires*: N/A
+
+ The garbage collector plugin for py.test
+
+ :pypi:`pytest-gcov`
+ *last release*: Feb 01, 2018,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Uses gcov to measure test coverage of a C library
+
+ :pypi:`pytest-gevent`
+ *last release*: Feb 25, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ Ensure that gevent is properly patched when invoking pytest
+
+ :pypi:`pytest-gherkin`
+ *last release*: Jul 27, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=5.0.0)
+
+ A flexible framework for executing BDD gherkin tests
+
+ :pypi:`pytest-ghostinspector`
+ *last release*: May 17, 2016,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ For finding/executing Ghost Inspector tests
+
+ :pypi:`pytest-girder`
+ *last release*: Nov 30, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ A set of pytest fixtures for testing Girder applications.
+
+ :pypi:`pytest-git`
+ *last release*: May 28, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Git repository fixture for py.test
+
+ :pypi:`pytest-gitcov`
+ *last release*: Jan 11, 2020,
+ *status*: 2 - Pre-Alpha,
+ *requires*: N/A
+
+ Pytest plugin for reporting on coverage of the last git commit.
+
+ :pypi:`pytest-git-fixtures`
+ *last release*: Mar 11, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ Pytest fixtures for testing with git.
+
+ :pypi:`pytest-github`
+ *last release*: Mar 07, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ 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,
+ *requires*: pytest (>=4.0.0)
+
+ pytest plugin to annotate failed tests with a workflow command for GitHub Actions
+
+ :pypi:`pytest-gitignore`
+ *last release*: Jul 17, 2015,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ py.test plugin to ignore the same files as git
+
+ :pypi:`pytest-glamor-allure`
+ *last release*: Nov 26, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ Extends allure-pytest functionality
+
+ :pypi:`pytest-gnupg-fixtures`
+ *last release*: Mar 04, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ Pytest fixtures for testing with gnupg.
+
+ :pypi:`pytest-golden`
+ *last release*: Nov 23, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=6.1.2,<7.0.0)
+
+ Plugin for pytest that offloads expected outputs to data files
+
+ :pypi:`pytest-graphql-schema`
+ *last release*: Oct 18, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Get graphql schema as fixture for pytest
+
+ :pypi:`pytest-greendots`
+ *last release*: Feb 08, 2014,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Green progress dots
+
+ :pypi:`pytest-growl`
+ *last release*: Jan 13, 2014,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Growl notifications for pytest results.
+
+ :pypi:`pytest-grpc`
+ *last release*: May 01, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=3.6.0)
+
+ pytest plugin for grpc
+
+ :pypi:`pytest-hammertime`
+ *last release*: Jul 28, 2018,
+ *status*: N/A,
+ *requires*: pytest
+
+ Display "🔨 " instead of "." for passed pytest tests.
+
+ :pypi:`pytest-harvest`
+ *last release*: Apr 01, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Store data created during your pytest tests execution, and retrieve it at the end of the session, e.g. for applicative benchmarking purposes.
+
+ :pypi:`pytest-helm-chart`
+ *last release*: Jun 15, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=5.4.2,<6.0.0)
+
+ 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,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.1.2,<7.0.0)
+
+ A plugin to provide different types and configs of Kubernetes clusters that can be used for testing.
+
+ :pypi:`pytest-helper`
+ *last release*: May 31, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Functions to help in using the pytest testing framework
+
+ :pypi:`pytest-helpers`
+ *last release*: May 17, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ pytest helpers
+
+ :pypi:`pytest-helpers-namespace`
+ *last release*: Apr 29, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=6.0.0)
+
+ Pytest Helpers Namespace Plugin
+
+ :pypi:`pytest-hidecaptured`
+ *last release*: May 04, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.8.5)
+
+ Hide captured output
+
+ :pypi:`pytest-historic`
+ *last release*: Apr 08, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ Custom report to display pytest historical execution records
+
+ :pypi:`pytest-historic-hook`
+ *last release*: Apr 08, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ Custom listener to store execution results into MYSQL DB, which is used for pytest-historic report
+
+ :pypi:`pytest-homeassistant`
+ *last release*: Aug 12, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A pytest plugin for use with homeassistant custom components.
+
+ :pypi:`pytest-homeassistant-custom-component`
+ *last release*: Nov 20, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (==6.2.5)
+
+ Experimental package to automatically extract test plugins for Home Assistant custom components
+
+ :pypi:`pytest-honors`
+ *last release*: Mar 06, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Report on tests that honor constraints, and guard against regressions
+
+ :pypi:`pytest-hoverfly`
+ *last release*: Jul 12, 2021,
+ *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
+
+ Integrates the Hoverfly HTTP proxy into Pytest
+
+ :pypi:`pytest-hpfeeds`
+ *last release*: Aug 27, 2021,
+ *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,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (!=6.0.0,>=5.0)
+
+ pytest plugin for generating HTML reports
+
+ :pypi:`pytest-html-lee`
+ *last release*: Jun 30, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.0)
+
+ optimized pytest plugin for generating HTML reports
+
+ :pypi:`pytest-html-profiling`
+ *last release*: Feb 11, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.0)
+
+ 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,
+ *status*: N/A,
+ *requires*: N/A
+
+ Generates a static html report based on pytest framework
+
+ :pypi:`pytest-html-thread`
+ *last release*: Dec 29, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ pytest plugin for generating HTML reports
+
+ :pypi:`pytest-http`
+ *last release*: Dec 05, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Fixture "http" for http requests
+
+ :pypi:`pytest-httpbin`
+ *last release*: Feb 11, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Easily test your HTTP library against a local copy of httpbin
+
+ :pypi:`pytest-http-mocker`
+ *last release*: Oct 20, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest plugin for http mocking (via https://github.com/vilus/mocker)
+
+ :pypi:`pytest-httpretty`
+ *last release*: Feb 16, 2014,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ A thin wrapper of HTTPretty for pytest
+
+ :pypi:`pytest-httpserver`
+ *last release*: Oct 18, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest ; extra == 'dev'
+
+ pytest-httpserver is a httpserver for pytest
+
+ :pypi:`pytest-httpx`
+ *last release*: Nov 16, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (==6.*)
+
+ Send responses to httpx.
+
+ :pypi:`pytest-httpx-blockage`
+ *last release*: Nov 16, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=6.2.5)
+
+ Disable httpx requests during a test run
+
+ :pypi:`pytest-hue`
+ *last release*: May 09, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Visualise PyTest status via your Phillips Hue lights
+
+ :pypi:`pytest-hylang`
+ *last release*: Mar 28, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ Pytest plugin to allow running tests written in hylang
+
+ :pypi:`pytest-hypo-25`
+ *last release*: Jan 12, 2020,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ help hypo module for pytest
+
+ :pypi:`pytest-ibutsu`
+ *last release*: Jun 16, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ A plugin to sent pytest results to an Ibutsu server
+
+ :pypi:`pytest-icdiff`
+ *last release*: Apr 08, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ use icdiff for better error messages in pytest assertions
+
+ :pypi:`pytest-idapro`
+ *last release*: Nov 03, 2018,
+ *status*: N/A,
+ *requires*: N/A
+
+ 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-idempotent`
+ *last release*: Nov 26, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest plugin for testing function idempotence.
+
+ :pypi:`pytest-ignore-flaky`
+ *last release*: Apr 23, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ ignore failures from flaky tests (pytest plugin)
+
+ :pypi:`pytest-image-diff`
+ *last release*: Jul 28, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+
+
+ :pypi:`pytest-incremental`
+ *last release*: Apr 24, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ an incremental test runner (pytest plugin)
+
+ :pypi:`pytest-influxdb`
+ *last release*: Apr 20, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Plugin for influxdb and pytest integration.
+
+ :pypi:`pytest-info-collector`
+ *last release*: May 26, 2019,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest plugin to collect information from tests
+
+ :pypi:`pytest-informative-node`
+ *last release*: Apr 25, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ display more node ininformation.
+
+ :pypi:`pytest-infrastructure`
+ *last release*: Apr 12, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ pytest stack validation prior to testing executing
+
+ :pypi:`pytest-ini`
+ *last release*: Sep 30, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Reuse pytest.ini to store env variables
+
+ :pypi:`pytest-inmanta`
+ *last release*: Aug 17, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ A py.test plugin providing fixtures to simplify inmanta modules testing.
+
+ :pypi:`pytest-inmanta-extensions`
+ *last release*: May 27, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Inmanta tests package
+
+ :pypi:`pytest-Inomaly`
+ *last release*: Feb 13, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A simple image diff plugin for pytest
+
+ :pypi:`pytest-insta`
+ *last release*: Apr 07, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=6.0.2,<7.0.0)
+
+ A practical snapshot testing plugin for pytest
+
+ :pypi:`pytest-instafail`
+ *last release*: Jun 14, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.9)
+
+ pytest plugin to show failures instantly
+
+ :pypi:`pytest-instrument`
+ *last release*: Apr 05, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.1.0)
+
+ pytest plugin to instrument tests
+
+ :pypi:`pytest-integration`
+ *last release*: Apr 16, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ Organizing pytests by integration or not
+
+ :pypi:`pytest-integration-mark`
+ *last release*: Jul 19, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=5.2,<7.0)
+
+ Automatic integration test marking and excluding plugin for pytest
+
+ :pypi:`pytest-interactive`
+ *last release*: Nov 30, 2017,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ A pytest plugin for console based interactive test selection just after the collection phase
+
+ :pypi:`pytest-intercept-remote`
+ *last release*: May 24, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=4.6)
+
+ Pytest plugin for intercepting outgoing connection requests during pytest run.
+
+ :pypi:`pytest-invenio`
+ *last release*: May 11, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (<7,>=6)
+
+ Pytest fixtures for Invenio.
+
+ :pypi:`pytest-involve`
+ *last release*: Feb 02, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Run tests covering a specific file or changeset
+
+ :pypi:`pytest-ipdb`
+ *last release*: Sep 02, 2014,
+ *status*: 2 - Pre-Alpha,
+ *requires*: N/A
+
+ A py.test plug-in to enable drop to ipdb debugger on test failure.
+
+ :pypi:`pytest-ipynb`
+ *last release*: Jan 29, 2019,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ THIS PROJECT IS ABANDONED
+
+ :pypi:`pytest-isort`
+ *last release*: Apr 27, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ py.test plugin to check import ordering using isort
+
+ :pypi:`pytest-it`
+ *last release*: Jan 22, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Pytest plugin to display test reports as a plaintext spec, inspired by Rspec: https://github.com/mattduck/pytest-it.
+
+ :pypi:`pytest-iterassert`
+ *last release*: May 11, 2020,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Nicer list and iterable assertion messages for pytest
+
+ :pypi:`pytest-jasmine`
+ *last release*: Nov 04, 2017,
+ *status*: 1 - Planning,
+ *requires*: N/A
+
+ Run jasmine tests from your pytest test suite
+
+ :pypi:`pytest-jest`
+ *last release*: May 22, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.3.2)
+
+ A custom jest-pytest oriented Pytest reporter
+
+ :pypi:`pytest-jira`
+ *last release*: Dec 02, 2021,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ py.test JIRA integration plugin, using markers
+
+ :pypi:`pytest-jira-xray`
+ *last release*: Nov 28, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ pytest plugin to integrate tests with JIRA XRAY
+
+ :pypi:`pytest-jobserver`
+ *last release*: May 15, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Limit parallel tests with posix jobserver.
+
+ :pypi:`pytest-joke`
+ *last release*: Oct 08, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=4.2.1)
+
+ Test failures are better served with humor.
+
+ :pypi:`pytest-json`
+ *last release*: Jan 18, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Generate JSON test reports
+
+ :pypi:`pytest-jsonlint`
+ *last release*: Aug 04, 2016,
+ *status*: N/A,
+ *requires*: N/A
+
+ UNKNOWN
+
+ :pypi:`pytest-json-report`
+ *last release*: Sep 24, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.8.0)
+
+ A pytest plugin to report test results as JSON files
+
+ :pypi:`pytest-kafka`
+ *last release*: Aug 24, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ Zookeeper, Kafka server, and Kafka consumer fixtures for Pytest
+
+ :pypi:`pytest-kafkavents`
+ *last release*: Sep 08, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ A plugin to send pytest events to Kafka
+
+ :pypi:`pytest-kind`
+ *last release*: Jan 24, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Kubernetes test support with KIND for pytest
+
+ :pypi:`pytest-kivy`
+ *last release*: Jul 06, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.6)
+
+ Kivy GUI tests fixtures using pytest
+
+ :pypi:`pytest-knows`
+ *last release*: Aug 22, 2014,
+ *status*: N/A,
+ *requires*: N/A
+
+ A pytest plugin that can automaticly skip test case based on dependence info calculated by trace
+
+ :pypi:`pytest-konira`
+ *last release*: Oct 09, 2011,
+ *status*: N/A,
+ *requires*: N/A
+
+ Run Konira DSL tests with py.test
+
+ :pypi:`pytest-krtech-common`
+ *last release*: Nov 28, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ pytest krtech common library
+
+ :pypi:`pytest-kwparametrize`
+ *last release*: Jan 22, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=6)
+
+ Alternate syntax for @pytest.mark.parametrize with test cases as dictionaries and default value fallbacks
+
+ :pypi:`pytest-lambda`
+ *last release*: Aug 23, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.6,<7)
+
+ Define pytest fixtures with lambda functions.
+
+ :pypi:`pytest-lamp`
+ *last release*: Jan 06, 2017,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-layab`
+ *last release*: Oct 05, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Pytest fixtures for layab.
+
+ :pypi:`pytest-lazy-fixture`
+ *last release*: Feb 01, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.2.5)
+
+ It helps to use fixtures in pytest.mark.parametrize
+
+ :pypi:`pytest-ldap`
+ *last release*: Aug 18, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ python-ldap fixtures for pytest
+
+ :pypi:`pytest-leaks`
+ *last release*: Nov 27, 2019,
+ *status*: 1 - Planning,
+ *requires*: N/A
+
+ A pytest plugin to trace resource leaks.
+
+ :pypi:`pytest-level`
+ *last release*: Oct 21, 2019,
+ *status*: N/A,
+ *requires*: pytest
+
+ Select tests of a given level or lower
+
+ :pypi:`pytest-libfaketime`
+ *last release*: Dec 22, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.0.0)
+
+ A python-libfaketime plugin for pytest.
+
+ :pypi:`pytest-libiio`
+ *last release*: Oct 29, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A pytest plugin to manage interfacing with libiio contexts
+
+ :pypi:`pytest-libnotify`
+ *last release*: Apr 02, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ Pytest plugin that shows notifications about the test run
+
+ :pypi:`pytest-ligo`
+ *last release*: Jan 16, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-lineno`
+ *last release*: Dec 04, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ A pytest plugin to show the line numbers of test functions
+
+ :pypi:`pytest-line-profiler`
+ *last release*: May 03, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Profile code executed by pytest
+
+ :pypi:`pytest-lisa`
+ *last release*: Jan 21, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=6.1.2,<7.0.0)
+
+ Pytest plugin for organizing tests.
+
+ :pypi:`pytest-listener`
+ *last release*: May 28, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ A simple network listener
+
+ :pypi:`pytest-litf`
+ *last release*: Jan 18, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.1.1)
+
+ A pytest plugin that stream output in LITF format
+
+ :pypi:`pytest-live`
+ *last release*: Mar 08, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ Live results for pytest
+
+ :pypi:`pytest-localftpserver`
+ *last release*: Aug 25, 2021,
+ *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,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ py.test plugin to test server connections locally.
+
+ :pypi:`pytest-localstack`
+ *last release*: Aug 22, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.3.0)
+
+ Pytest plugin for AWS integration tests
+
+ :pypi:`pytest-lockable`
+ *last release*: Nov 09, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ lockable resource plugin for pytest
+
+ :pypi:`pytest-locker`
+ *last release*: Oct 29, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=5.4)
+
+ Used to lock object during testing. Essentially changing assertions from being hard coded to asserting that nothing changed
+
+ :pypi:`pytest-log`
+ *last release*: Aug 15, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=3.8)
+
+ print log
+
+ :pypi:`pytest-logbook`
+ *last release*: Nov 23, 2015,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.8)
+
+ py.test plugin to capture logbook log messages
+
+ :pypi:`pytest-logdog`
+ *last release*: Jun 15, 2021,
+ *status*: 1 - Planning,
+ *requires*: pytest (>=6.2.0)
+
+ Pytest plugin to test logging
+
+ :pypi:`pytest-logfest`
+ *last release*: Jul 21, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ 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,
+ *requires*: pytest (>=3.2)
+
+ Plugin configuring handlers for loggers from Python logging module.
+
+ :pypi:`pytest-logging`
+ *last release*: Nov 04, 2015,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Configures logging and allows tweaking the log level with a py.test flag
+
+ :pypi:`pytest-log-report`
+ *last release*: Dec 26, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Package for creating a pytest test run reprot
+
+ :pypi:`pytest-manual-marker`
+ *last release*: Oct 11, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=6)
+
+ pytest marker for marking manual tests
+
+ :pypi:`pytest-markdown`
+ *last release*: Jan 15, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.0.1,<7.0.0)
+
+ Test your markdown docs with pytest
+
+ :pypi:`pytest-marker-bugzilla`
+ *last release*: Jan 09, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ py.test bugzilla integration plugin, using markers
+
+ :pypi:`pytest-markers-presence`
+ *last release*: Feb 04, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.0)
+
+ A simple plugin to detect missed pytest tags and markers"
+
+ :pypi:`pytest-markfiltration`
+ *last release*: Nov 08, 2011,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ UNKNOWN
+
+ :pypi:`pytest-mark-no-py3`
+ *last release*: May 17, 2019,
+ *status*: N/A,
+ *requires*: pytest
+
+ pytest plugin and bowler codemod to help migrate tests to Python 3
+
+ :pypi:`pytest-marks`
+ *last release*: Nov 23, 2012,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ UNKNOWN
+
+ :pypi:`pytest-matcher`
+ *last release*: Apr 23, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.4)
+
+ Match test output against patterns stored in files
+
+ :pypi:`pytest-match-skip`
+ *last release*: May 15, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=4.4.1)
+
+ Skip matching marks. Matches partial marks using wildcards.
+
+ :pypi:`pytest-mat-report`
+ *last release*: Jan 20, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ this is report
+
+ :pypi:`pytest-matrix`
+ *last release*: Jun 24, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.4.3,<6.0.0)
+
+ Provide tools for generating tests from combinations of fixtures.
+
+ :pypi:`pytest-mccabe`
+ *last release*: Jul 22, 2020,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=5.4.0)
+
+ pytest plugin to run the mccabe code complexity checker.
+
+ :pypi:`pytest-md`
+ *last release*: Jul 11, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=4.2.1)
+
+ Plugin for generating Markdown reports for pytest results
+
+ :pypi:`pytest-md-report`
+ *last release*: May 04, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (!=6.0.0,<7,>=3.3.2)
+
+ A pytest plugin to make a test results report with Markdown table format.
+
+ :pypi:`pytest-memprof`
+ *last release*: Mar 29, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Estimates memory consumption of test functions
+
+ :pypi:`pytest-menu`
+ *last release*: Oct 04, 2017,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=2.4.2)
+
+ A pytest plugin for console based interactive test selection just after the collection phase
+
+ :pypi:`pytest-mercurial`
+ *last release*: Nov 21, 2020,
+ *status*: 1 - Planning,
+ *requires*: N/A
+
+ pytest plugin to write integration tests for projects using Mercurial Python internals
+
+ :pypi:`pytest-message`
+ *last release*: Nov 04, 2021,
+ *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,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Pytest to Slack reporting plugin
+
+ :pypi:`pytest-metadata`
+ *last release*: Nov 27, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.9.0)
+
+ pytest plugin for test session metadata
+
+ :pypi:`pytest-metrics`
+ *last release*: Apr 04, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ Custom metrics report for pytest
+
+ :pypi:`pytest-mimesis`
+ *last release*: Mar 21, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.2)
+
+ Mimesis integration with the pytest test runner
+
+ :pypi:`pytest-minecraft`
+ *last release*: Sep 26, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=6.0.1,<7.0.0)
+
+ A pytest plugin for running tests against Minecraft releases
+
+ :pypi:`pytest-missing-fixtures`
+ *last release*: Oct 14, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Pytest plugin that creates missing fixtures
+
+ :pypi:`pytest-ml`
+ *last release*: May 04, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Test your machine learning!
+
+ :pypi:`pytest-mocha`
+ *last release*: Apr 02, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=5.4.0)
+
+ pytest plugin to display test execution output like a mochajs
+
+ :pypi:`pytest-mock`
+ *last release*: May 06, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.0)
+
+ Thin-wrapper around the mock package for easier use with pytest
+
+ :pypi:`pytest-mock-api`
+ *last release*: Feb 13, 2019,
+ *status*: 1 - Planning,
+ *requires*: pytest (>=4.0.0)
+
+ A mock API server with configurable routes and responses available as a fixture.
+
+ :pypi:`pytest-mock-generator`
+ *last release*: Aug 10, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ A pytest fixture wrapper for https://pypi.org/project/mock-generator
+
+ :pypi:`pytest-mock-helper`
+ *last release*: Jan 24, 2018,
+ *status*: N/A,
+ *requires*: pytest
+
+ Help you mock HTTP call and generate mock code
+
+ :pypi:`pytest-mockito`
+ *last release*: Jul 11, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Base fixtures for mockito
+
+ :pypi:`pytest-mockredis`
+ *last release*: Jan 02, 2018,
+ *status*: 2 - Pre-Alpha,
+ *requires*: N/A
+
+ 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,
+ *status*: N/A,
+ *requires*: pytest (>=1.0)
+
+ A pytest plugin for easily instantiating reproducible mock resources.
+
+ :pypi:`pytest-mock-server`
+ *last release*: Apr 06, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Mock server plugin for pytest
+
+ :pypi:`pytest-mockservers`
+ *last release*: Mar 31, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=4.3.0)
+
+ A set of fixtures to test your requests to HTTP/UDP servers
+
+ :pypi:`pytest-modifyjunit`
+ *last release*: Jan 10, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Utility for adding additional properties to junit xml for IDM QE
+
+ :pypi:`pytest-modifyscope`
+ *last release*: Apr 12, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ pytest plugin to modify fixture scope
+
+ :pypi:`pytest-molecule`
+ *last release*: Oct 06, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ PyTest Molecule Plugin :: discover and run molecule tests
+
+ :pypi:`pytest-mongo`
+ *last release*: Jun 07, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ MongoDB process and client fixtures plugin for Pytest.
+
+ :pypi:`pytest-mongodb`
+ *last release*: Dec 07, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.5.2)
+
+ pytest plugin for MongoDB fixtures
+
+ :pypi:`pytest-monitor`
+ *last release*: Aug 24, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Pytest plugin for analyzing resource usage.
+
+ :pypi:`pytest-monkeyplus`
+ *last release*: Sep 18, 2012,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ pytest's monkeypatch subclass with extra functionalities
+
+ :pypi:`pytest-monkeytype`
+ *last release*: Jul 29, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ pytest-monkeytype: Generate Monkeytype annotations from your pytest tests.
+
+ :pypi:`pytest-moto`
+ *last release*: Aug 28, 2015,
+ *status*: 1 - Planning,
+ *requires*: N/A
+
+ Fixtures for integration tests of AWS services,uses moto mocking library.
+
+ :pypi:`pytest-motor`
+ *last release*: Jul 21, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ A pytest plugin for motor, the non-blocking MongoDB driver.
+
+ :pypi:`pytest-mp`
+ *last release*: May 23, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ A test batcher for multiprocessed Pytest runs
+
+ :pypi:`pytest-mpi`
+ *last release*: Mar 14, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ pytest plugin to collect information from tests
+
+ :pypi:`pytest-mpl`
+ *last release*: Jul 02, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ pytest plugin to help with testing figures output from Matplotlib
+
+ :pypi:`pytest-mproc`
+ *last release*: Mar 07, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ low-startup-overhead, scalable, distributed-testing pytest plugin
+
+ :pypi:`pytest-multi-check`
+ *last release*: Jun 03, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ Pytest-плагин, реализует возможность мульти проверок и мягких проверок
+
+ :pypi:`pytest-multihost`
+ *last release*: Apr 07, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Utility for writing multi-host tests for pytest
+
+ :pypi:`pytest-multilog`
+ *last release*: Jun 10, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Multi-process logs handling and other helpers for pytest
+
+ :pypi:`pytest-multithreading`
+ *last release*: Aug 12, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=3.6)
+
+ a pytest plugin for th and concurrent testing
+
+ :pypi:`pytest-mutagen`
+ *last release*: Jul 24, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=5.4)
+
+ Add the mutation testing feature to pytest
+
+ :pypi:`pytest-mypy`
+ *last release*: Mar 21, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5)
+
+ Mypy static type checker plugin for Pytest
+
+ :pypi:`pytest-mypyd`
+ *last release*: Aug 20, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (<4.7,>=2.8) ; python_version < "3.5"
+
+ Mypy static type checker plugin for Pytest
+
+ :pypi:`pytest-mypy-plugins`
+ *last release*: Oct 19, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=6.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
+
+ Substitute for "pytest-mypy-plugins" for Python implementations which aren't supported by mypy.
+
+ :pypi:`pytest-mypy-testing`
+ *last release*: Jun 13, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ Pytest plugin to check mypy output.
+
+ :pypi:`pytest-mysql`
+ *last release*: Nov 22, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ MySQL process and client fixtures for pytest
+
+ :pypi:`pytest-needle`
+ *last release*: Dec 10, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest (<5.0.0,>=3.0.0)
+
+ pytest plugin for visual testing websites using selenium
+
+ :pypi:`pytest-neo`
+ *last release*: Apr 23, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.7.2)
+
+ pytest-neo is a plugin for pytest that shows tests like screen of Matrix.
+
+ :pypi:`pytest-network`
+ *last release*: May 07, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ A simple plugin to disable network on socket level.
+
+ :pypi:`pytest-never-sleep`
+ *last release*: May 05, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.5.1)
+
+ pytest plugin helps to avoid adding tests without mock \`time.sleep\`
+
+ :pypi:`pytest-nginx`
+ *last release*: Aug 12, 2017,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ nginx fixture for pytest
+
+ :pypi:`pytest-nginx-iplweb`
+ *last release*: Mar 01, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ nginx fixture for pytest - iplweb temporary fork
+
+ :pypi:`pytest-ngrok`
+ *last release*: Jan 22, 2020,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-ngsfixtures`
+ *last release*: Sep 06, 2019,
+ *status*: 2 - Pre-Alpha,
+ *requires*: pytest (>=5.0.0)
+
+ pytest ngs fixtures
+
+ :pypi:`pytest-nice`
+ *last release*: May 04, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ A pytest plugin that alerts user of failed test cases with screen notifications
+
+ :pypi:`pytest-nice-parametrize`
+ *last release*: Apr 17, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ A small snippet for nicer PyTest's Parametrize
+
+ :pypi:`pytest-nlcov`
+ *last release*: Jul 07, 2021,
+ *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,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Run all tests without custom markers
+
+ :pypi:`pytest-nodev`
+ *last release*: Jul 21, 2016,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.8.1)
+
+ Test-driven source code search for Python.
+
+ :pypi:`pytest-nogarbage`
+ *last release*: Aug 29, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.6.0)
+
+ Ensure a test produces no garbage
+
+ :pypi:`pytest-notebook`
+ *last release*: Sep 16, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A pytest plugin for testing Jupyter Notebooks
+
+ :pypi:`pytest-notice`
+ *last release*: Nov 05, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ Send pytest execution result email
+
+ :pypi:`pytest-notification`
+ *last release*: Jun 19, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=4)
+
+ A pytest plugin for sending a desktop notification and playing a sound upon completion of tests
+
+ :pypi:`pytest-notifier`
+ *last release*: Jun 12, 2020,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ A pytest plugin to notify test result
+
+ :pypi:`pytest-notimplemented`
+ *last release*: Aug 27, 2019,
+ *status*: N/A,
+ *requires*: pytest (>=5.1,<6.0)
+
+ Pytest markers for not implemented features and tests.
+
+ :pypi:`pytest-notion`
+ *last release*: Aug 07, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ 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)
+
+ A pytest plugin for generating NUnit3 test result XML output
+
+ :pypi:`pytest-ochrus`
+ *last release*: Feb 21, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ pytest results data-base and HTML reporter
+
+ :pypi:`pytest-odoo`
+ *last release*: Nov 04, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.9)
+
+ py.test plugin to run Odoo tests
+
+ :pypi:`pytest-odoo-fixtures`
+ *last release*: Jun 25, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Project description
+
+ :pypi:`pytest-oerp`
+ *last release*: Feb 28, 2012,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest plugin to test OpenERP modules
+
+ :pypi:`pytest-ok`
+ *last release*: Apr 01, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ The ultimate pytest output plugin
+
+ :pypi:`pytest-only`
+ *last release*: Jan 19, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ Use @pytest.mark.only to run a single test
+
+ :pypi:`pytest-oot`
+ *last release*: Sep 18, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Run object-oriented tests in a simple format
+
+ :pypi:`pytest-openfiles`
+ *last release*: Apr 16, 2020,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=4.6)
+
+ Pytest plugin for detecting inadvertent open file handles
+
+ :pypi:`pytest-opentmi`
+ *last release*: Nov 04, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.0)
+
+ pytest plugin for publish results to opentmi
+
+ :pypi:`pytest-operator`
+ *last release*: Oct 26, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Fixtures for Operators
+
+ :pypi:`pytest-optional`
+ *last release*: Oct 07, 2015,
+ *status*: N/A,
+ *requires*: N/A
+
+ include/exclude values of fixtures in pytest
+
+ :pypi:`pytest-optional-tests`
+ *last release*: Jul 09, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=4.5.0)
+
+ Easy declaration of optional tests (i.e., that are not run by default)
+
+ :pypi:`pytest-orchestration`
+ *last release*: Jul 18, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ A pytest plugin for orchestrating tests
+
+ :pypi:`pytest-order`
+ *last release*: May 30, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=5.0)
+
+ pytest plugin to run your tests in a specific order
+
+ :pypi:`pytest-ordering`
+ *last release*: Nov 14, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ pytest plugin to run your tests in a specific order
+
+ :pypi:`pytest-osxnotify`
+ *last release*: May 15, 2015,
+ *status*: N/A,
+ *requires*: N/A
+
+ OS X notifications for py.test results.
+
+ :pypi:`pytest-otel`
+ *last release*: Dec 03, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest-otel report OpenTelemetry traces about test executed
+
+ :pypi:`pytest-pact`
+ *last release*: Jan 07, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A simple plugin to use with pytest
+
+ :pypi:`pytest-pahrametahrize`
+ *last release*: Nov 24, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.0,<7.0)
+
+ Parametrize your tests with a Boston accent.
+
+ :pypi:`pytest-parallel`
+ *last release*: Oct 10, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.0.0)
+
+ a pytest plugin for parallel and concurrent testing
+
+ :pypi:`pytest-parallel-39`
+ *last release*: Jul 12, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.0.0)
+
+ a pytest plugin for parallel and concurrent testing
+
+ :pypi:`pytest-param`
+ *last release*: Sep 11, 2016,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.6.0)
+
+ pytest plugin to test all, first, last or random params
+
+ :pypi:`pytest-paramark`
+ *last release*: Jan 10, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=4.5.0)
+
+ Configure pytest fixtures using a combination of"parametrize" and markers
+
+ :pypi:`pytest-parametrization`
+ *last release*: Nov 30, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Simpler PyTest parametrization
+
+ :pypi:`pytest-parametrize-cases`
+ *last release*: Dec 12, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=6.1.2,<7.0.0)
+
+ A more user-friendly way to write parametrized tests.
+
+ :pypi:`pytest-parametrized`
+ *last release*: Oct 19, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Pytest plugin for parametrizing tests with default iterables.
+
+ :pypi:`pytest-parawtf`
+ *last release*: Dec 03, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.6.0)
+
+ Finally spell paramete?ri[sz]e correctly
+
+ :pypi:`pytest-pass`
+ *last release*: Dec 04, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Check out https://github.com/elilutsky/pytest-pass
+
+ :pypi:`pytest-passrunner`
+ *last release*: Feb 10, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.6.0)
+
+ Pytest plugin providing the 'run_on_pass' marker
+
+ :pypi:`pytest-paste-config`
+ *last release*: Sep 18, 2013,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Allow setting the path to a paste config file
+
+ :pypi:`pytest-patches`
+ *last release*: Aug 30, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A contextmanager pytest fixture for handling multiple mock patches
+
+ :pypi:`pytest-pdb`
+ *last release*: Jul 31, 2018,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest plugin which adds pdb helper commands related to pytest.
+
+ :pypi:`pytest-peach`
+ *last release*: Apr 12, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.8.7)
+
+ pytest plugin for fuzzing with Peach API Security
+
+ :pypi:`pytest-pep257`
+ *last release*: Jul 09, 2016,
+ *status*: N/A,
+ *requires*: N/A
+
+ py.test plugin for pep257
+
+ :pypi:`pytest-pep8`
+ *last release*: Apr 27, 2014,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest plugin to check PEP8 requirements
+
+ :pypi:`pytest-percent`
+ *last release*: May 21, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=5.2.0)
+
+ Change the exit code of pytest test sessions when a required percent of tests pass.
+
+ :pypi:`pytest-perf`
+ *last release*: Jun 27, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.6) ; extra == 'testing'
+
+ pytest-perf
+
+ :pypi:`pytest-performance`
+ *last release*: Sep 11, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.7.0)
+
+ A simple plugin to ensure the execution of critical sections of code has not been impacted
+
+ :pypi:`pytest-persistence`
+ *last release*: Nov 06, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest tool for persistent objects
+
+ :pypi:`pytest-pgsql`
+ *last release*: May 13, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.0.0)
+
+ Pytest plugins and helpers for tests using a Postgres database.
+
+ :pypi:`pytest-phmdoctest`
+ *last release*: Nov 10, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.2) ; extra == 'test'
+
+ pytest plugin to test Python examples in Markdown using phmdoctest.
+
+ :pypi:`pytest-picked`
+ *last release*: Dec 23, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=3.5.0)
+
+ Run the tests related to the changed files
+
+ :pypi:`pytest-pigeonhole`
+ *last release*: Jun 25, 2018,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.4)
+
+
+
+ :pypi:`pytest-pikachu`
+ *last release*: Aug 05, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Show surprise when tests are passing
+
+ :pypi:`pytest-pilot`
+ *last release*: Oct 09, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Slice in your test base thanks to powerful markers.
+
+ :pypi:`pytest-pings`
+ *last release*: Jun 29, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=5.0.0)
+
+ 🦊 The pytest plugin for Firefox Telemetry 📊
+
+ :pypi:`pytest-pinned`
+ *last release*: Sep 17, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A simple pytest plugin for pinning tests
+
+ :pypi:`pytest-pinpoint`
+ *last release*: Sep 25, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=4.4.0)
+
+ A pytest plugin which runs SBFL algorithms to detect faults.
+
+ :pypi:`pytest-pipeline`
+ *last release*: Jan 24, 2017,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Pytest plugin for functional testing of data analysispipelines
+
+ :pypi:`pytest-platform-markers`
+ *last release*: Sep 09, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.6.0)
+
+ Markers for pytest to skip tests on specific platforms
+
+ :pypi:`pytest-play`
+ *last release*: Jun 12, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ pytest plugin that let you automate actions and assertions with test metrics reporting executing plain YAML files
+
+ :pypi:`pytest-playbook`
+ *last release*: Jan 21, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=6.1.2,<7.0.0)
+
+ Pytest plugin for reading playbooks.
+
+ :pypi:`pytest-playwright`
+ *last release*: Oct 28, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ A pytest wrapper with fixtures for Playwright to automate web browsers
+
+ :pypi:`pytest-playwrights`
+ *last release*: Dec 02, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ A pytest wrapper with fixtures for Playwright to automate web browsers
+
+ :pypi:`pytest-playwright-snapshot`
+ *last release*: Aug 19, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ A pytest wrapper for snapshot testing with playwright
+
+ :pypi:`pytest-plt`
+ *last release*: Aug 17, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Fixtures for quickly making Matplotlib plots in tests
+
+ :pypi:`pytest-plugin-helpers`
+ *last release*: Nov 23, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A plugin to help developing and testing other plugins
+
+ :pypi:`pytest-plus`
+ *last release*: Mar 19, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.50)
+
+ PyTest Plus Plugin :: extends pytest functionality
+
+ :pypi:`pytest-pmisc`
+ *last release*: Mar 21, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-pointers`
+ *last release*: Oct 14, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest plugin to define functions you test with special marks for better navigation and reports
+
+ :pypi:`pytest-polarion-cfme`
+ *last release*: Nov 13, 2017,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest plugin for collecting test cases and recording test results
+
+ :pypi:`pytest-polarion-collect`
+ *last release*: Jun 18, 2020,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ pytest plugin for collecting polarion test cases data
+
+ :pypi:`pytest-polecat`
+ *last release*: Aug 12, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Provides Polecat pytest fixtures
+
+ :pypi:`pytest-ponyorm`
+ *last release*: Oct 31, 2018,
+ *status*: N/A,
+ *requires*: pytest (>=3.1.1)
+
+ PonyORM in Pytest
+
+ :pypi:`pytest-poo`
+ *last release*: Mar 25, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.3.4)
+
+ Visualize your crappy tests
+
+ :pypi:`pytest-poo-fail`
+ *last release*: Feb 12, 2015,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Visualize your failed tests with poo
+
+ :pypi:`pytest-pop`
+ *last release*: Aug 19, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ A pytest plugin to help with testing pop projects
+
+ :pypi:`pytest-portion`
+ *last release*: Jan 28, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Select a portion of the collected tests
+
+ :pypi:`pytest-postgres`
+ *last release*: Mar 22, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ Run PostgreSQL in Docker container in Pytest.
+
+ :pypi:`pytest-postgresql`
+ *last release*: Nov 05, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.0.0)
+
+ Postgresql fixtures and fixture factories for Pytest.
+
+ :pypi:`pytest-power`
+ *last release*: Dec 31, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=5.4)
+
+ pytest plugin with powerful fixtures
+
+ :pypi:`pytest-pretty-terminal`
+ *last release*: Nov 24, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=3.4.1)
+
+ pytest plugin for generating prettier terminal output
+
+ :pypi:`pytest-pride`
+ *last release*: Apr 02, 2016,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Minitest-style test colors
+
+ :pypi:`pytest-print`
+ *last release*: Jun 17, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=6)
+
+ pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout)
+
+ :pypi:`pytest-profiling`
+ *last release*: May 28, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Profiling plugin for py.test
+
+ :pypi:`pytest-progress`
+ *last release*: Nov 09, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.7)
+
+ pytest plugin for instant test progress status
+
+ :pypi:`pytest-prometheus`
+ *last release*: Oct 03, 2017,
+ *status*: N/A,
+ *requires*: N/A
+
+ Report test pass / failures to a Prometheus PushGateway
+
+ :pypi:`pytest-prosper`
+ *last release*: Sep 24, 2018,
+ *status*: N/A,
+ *requires*: N/A
+
+ Test helpers for Prosper projects
+
+ :pypi:`pytest-pspec`
+ *last release*: Jun 02, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.0.0)
+
+ A rspec format reporter for Python ptest
+
+ :pypi:`pytest-psqlgraph`
+ *last release*: Oct 19, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.0)
+
+ pytest plugin for testing applications that use psqlgraph
+
+ :pypi:`pytest-ptera`
+ *last release*: Oct 20, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=6.2.4,<7.0.0)
+
+ Use ptera probes in tests
+
+ :pypi:`pytest-pudb`
+ *last release*: Oct 25, 2018,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=2.0)
+
+ Pytest PuDB debugger integration
+
+ :pypi:`pytest-purkinje`
+ *last release*: Oct 28, 2017,
+ *status*: 2 - Pre-Alpha,
+ *requires*: N/A
+
+ py.test plugin for purkinje test runner
+
+ :pypi:`pytest-pycharm`
+ *last release*: Aug 13, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.3)
+
+ Plugin for py.test to enter PyCharm debugger on uncaught exceptions
+
+ :pypi:`pytest-pycodestyle`
+ *last release*: Aug 10, 2020,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest plugin to run pycodestyle
+
+ :pypi:`pytest-pydev`
+ *last release*: Nov 15, 2017,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ py.test plugin to connect to a remote debug server with PyDev or PyCharm.
+
+ :pypi:`pytest-pydocstyle`
+ *last release*: Aug 10, 2020,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest plugin to run pydocstyle
+
+ :pypi:`pytest-pylint`
+ *last release*: Nov 09, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.4)
+
+ pytest plugin to check source code with pylint
+
+ :pypi:`pytest-pypi`
+ *last release*: Mar 04, 2018,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Easily test your HTTP library against a local copy of pypi
+
+ :pypi:`pytest-pypom-navigation`
+ *last release*: Feb 18, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.0.7)
+
+ 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)
+
+ A plugin to run pyppeteer in pytest.
+
+ :pypi:`pytest-pyq`
+ *last release*: Mar 10, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Pytest fixture "q" for pyq
+
+ :pypi:`pytest-pyramid`
+ *last release*: Oct 15, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ pytest_pyramid - provides fixtures for testing pyramid applications with pytest test suite
+
+ :pypi:`pytest-pyramid-server`
+ *last release*: May 28, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Pyramid server fixture for py.test
+
+ :pypi:`pytest-pyright`
+ *last release*: Aug 16, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Pytest plugin for type checking code with Pyright
+
+ :pypi:`pytest-pytestrail`
+ *last release*: Aug 27, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.8.0)
+
+ Pytest plugin for interaction with TestRail
+
+ :pypi:`pytest-pythonpath`
+ *last release*: Aug 22, 2018,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ pytest plugin for adding to the PYTHONPATH from command line or configs.
+
+ :pypi:`pytest-pytorch`
+ *last release*: May 25, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ pytest plugin for a better developer experience when working with the PyTorch test suite
+
+ :pypi:`pytest-qasync`
+ *last release*: Jul 12, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=5.4.0)
+
+ Pytest support for qasync.
+
+ :pypi:`pytest-qatouch`
+ *last release*: Jun 26, 2021,
+ *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,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=6.2.3)
+
+ A pytest plugin for testing QGIS python plugins
+
+ :pypi:`pytest-qml`
+ *last release*: Dec 02, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.0.0)
+
+ Run QML Tests with pytest
+
+ :pypi:`pytest-qr`
+ *last release*: Nov 25, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ pytest plugin to generate test result QR codes
+
+ :pypi:`pytest-qt`
+ *last release*: Jun 13, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.0.0)
+
+ pytest support for PyQt and PySide applications
+
+ :pypi:`pytest-qt-app`
+ *last release*: Dec 23, 2015,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ QT app fixture for py.test
+
+ :pypi:`pytest-quarantine`
+ *last release*: Nov 24, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.6)
+
+ A plugin for pytest to manage expected test failures
+
+ :pypi:`pytest-quickcheck`
+ *last release*: Nov 15, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (<6.0.0,>=4.0)
+
+ pytest plugin to generate random data inspired by QuickCheck
+
+ :pypi:`pytest-rabbitmq`
+ *last release*: Jun 02, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.0.0)
+
+ RabbitMQ process and client fixtures for pytest
+
+ :pypi:`pytest-race`
+ *last release*: Nov 21, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Race conditions tester for pytest
+
+ :pypi:`pytest-rage`
+ *last release*: Oct 21, 2011,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest plugin to implement PEP712
+
+ :pypi:`pytest-railflow-testrail-reporter`
+ *last release*: Dec 02, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Generate json reports along with specified metadata defined in test markers.
+
+ :pypi:`pytest-raises`
+ *last release*: Apr 23, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=3.2.2)
+
+ An implementation of pytest.raises as a pytest.mark fixture
+
+ :pypi:`pytest-raisesregexp`
+ *last release*: Dec 18, 2015,
+ *status*: N/A,
+ *requires*: N/A
+
+ Simple pytest plugin to look for regex in Exceptions
+
+ :pypi:`pytest-raisin`
+ *last release*: Jun 25, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ Plugin enabling the use of exception instances with pytest.raises
+
+ :pypi:`pytest-random`
+ *last release*: Apr 28, 2013,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ py.test plugin to randomize tests
+
+ :pypi:`pytest-randomly`
+ *last release*: Nov 30, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Pytest plugin to randomly order tests and control random.seed.
+
+ :pypi:`pytest-randomness`
+ *last release*: May 30, 2019,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Pytest plugin about random seed management
+
+ :pypi:`pytest-random-num`
+ *last release*: Oct 19, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Randomise the order in which pytest tests are run with some control over the randomness
+
+ :pypi:`pytest-random-order`
+ *last release*: Nov 30, 2018,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.0.0)
+
+ Randomise the order in which pytest tests are run with some control over the randomness
+
+ :pypi:`pytest-readme`
+ *last release*: Dec 28, 2014,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Test your README.md file
+
+ :pypi:`pytest-reana`
+ *last release*: Nov 22, 2021,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Pytest fixtures for REANA.
+
+ :pypi:`pytest-recording`
+ *last release*: Jul 08, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A pytest plugin that allows you recording of network interactions via VCR.py
+
+ :pypi:`pytest-recordings`
+ *last release*: Aug 13, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ Provides pytest plugins for reporting request/response traffic, screenshots, and more to ReportPortal
+
+ :pypi:`pytest-redis`
+ *last release*: Nov 03, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Redis fixtures and fixture factories for Pytest.
+
+ :pypi:`pytest-redislite`
+ *last release*: Sep 19, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ Pytest plugin for testing code using Redis
+
+ :pypi:`pytest-redmine`
+ *last release*: Mar 19, 2018,
+ *status*: 1 - Planning,
+ *requires*: N/A
+
+ Pytest plugin for redmine
+
+ :pypi:`pytest-ref`
+ *last release*: Nov 23, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A plugin to store reference files to ease regression testing
+
+ :pypi:`pytest-reference-formatter`
+ *last release*: Oct 01, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Conveniently run pytest with a dot-formatted test reference.
+
+ :pypi:`pytest-regressions`
+ *last release*: Jan 27, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.5.0)
+
+ Easy to use fixtures to write regression tests.
+
+ :pypi:`pytest-regtest`
+ *last release*: Jun 03, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest plugin for regression tests
+
+ :pypi:`pytest-relative-order`
+ *last release*: May 17, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ a pytest plugin that sorts tests using "before" and "after" markers
+
+ :pypi:`pytest-relaxed`
+ *last release*: Jun 14, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (<5,>=3)
+
+ Relaxed test discovery/organization for pytest
+
+ :pypi:`pytest-remfiles`
+ *last release*: Jul 01, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ 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)
+
+ Pytest plugin for controlling remote data access.
+
+ :pypi:`pytest-remote-response`
+ *last release*: Jun 30, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=4.6)
+
+ Pytest plugin for capturing and mocking connection requests.
+
+ :pypi:`pytest-remove-stale-bytecode`
+ *last release*: Mar 04, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ py.test plugin to remove stale byte code files.
+
+ :pypi:`pytest-reorder`
+ *last release*: May 31, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ Reorder tests depending on their paths and names.
+
+ :pypi:`pytest-repeat`
+ *last release*: Oct 31, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.6)
+
+ pytest plugin for repeating tests
+
+ :pypi:`pytest-replay`
+ *last release*: Jun 09, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.0.0)
+
+ 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,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ A pytest plugin to report on repository standards conformance
+
+ :pypi:`pytest-report`
+ *last release*: May 11, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Creates json report that is compatible with atom.io's linter message format
+
+ :pypi:`pytest-reporter`
+ *last release*: Jul 22, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ Generate Pytest reports with templates
+
+ :pypi:`pytest-reporter-html1`
+ *last release*: Jun 08, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A basic HTML report template for Pytest
+
+ :pypi:`pytest-reportinfra`
+ *last release*: Aug 11, 2019,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Pytest plugin for reportinfra
+
+ :pypi:`pytest-reporting`
+ *last release*: Oct 25, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A plugin to report summarized results in a table format
+
+ :pypi:`pytest-reportlog`
+ *last release*: Dec 11, 2020,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=5.2)
+
+ Replacement for the --resultlog option, focused in simplicity and extensibility
+
+ :pypi:`pytest-report-me`
+ *last release*: Dec 31, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ A pytest plugin to generate report.
+
+ :pypi:`pytest-report-parameters`
+ *last release*: Jun 18, 2020,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=2.4.2)
+
+ pytest plugin for adding tests' parameters to junit report
+
+ :pypi:`pytest-reportportal`
+ *last release*: Jun 18, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=3.8.0)
+
+ Agent for Reporting results of tests to the Report Portal
+
+ :pypi:`pytest-reqs`
+ *last release*: May 12, 2019,
+ *status*: N/A,
+ *requires*: pytest (>=2.4.2)
+
+ pytest plugin to check pinned requirements
+
+ :pypi:`pytest-requests`
+ *last release*: Jun 24, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A simple plugin to use with pytest
+
+ :pypi:`pytest-reraise`
+ *last release*: Jun 17, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.6)
+
+ Make multi-threaded pytest test cases fail when they should
+
+ :pypi:`pytest-rerun`
+ *last release*: Jul 08, 2019,
+ *status*: N/A,
+ *requires*: pytest (>=3.6)
+
+ Re-run only changed files in specified branch
+
+ :pypi:`pytest-rerunfailures`
+ *last release*: Sep 17, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.3)
+
+ pytest plugin to re-run tests to eliminate flaky failures
+
+ :pypi:`pytest-resilient-circuits`
+ *last release*: Nov 15, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Resilient Circuits fixtures for PyTest.
+
+ :pypi:`pytest-resource`
+ *last release*: Nov 14, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Load resource fixture plugin to use with pytest
+
+ :pypi:`pytest-resource-path`
+ *last release*: May 01, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.5.0)
+
+ Provides path for uniform access to test resources in isolated directory
+
+ :pypi:`pytest-responsemock`
+ *last release*: Oct 10, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Simplified requests calls mocking for pytest
+
+ :pypi:`pytest-responses`
+ *last release*: Apr 26, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=2.5)
+
+ py.test integration for responses
+
+ :pypi:`pytest-restrict`
+ *last release*: Aug 12, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Pytest plugin to restrict the test types allowed
+
+ :pypi:`pytest-rethinkdb`
+ *last release*: Jul 24, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A RethinkDB plugin for pytest.
+
+ :pypi:`pytest-reverse`
+ *last release*: Aug 12, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Pytest plugin to reverse test order.
+
+ :pypi:`pytest-ringo`
+ *last release*: Sep 27, 2017,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest plugin to test webapplications using the Ringo webframework
+
+ :pypi:`pytest-rng`
+ *last release*: Aug 08, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Fixtures for seeding tests and making randomness reproducible
+
+ :pypi:`pytest-roast`
+ *last release*: Jul 29, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ pytest plugin for ROAST configuration override and fixtures
+
+ :pypi:`pytest-rocketchat`
+ *last release*: Apr 18, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Pytest to Rocket.Chat reporting plugin
+
+ :pypi:`pytest-rotest`
+ *last release*: Sep 08, 2019,
+ *status*: N/A,
+ *requires*: pytest (>=3.5.0)
+
+ Pytest integration with rotest
+
+ :pypi:`pytest-rpc`
+ *last release*: Feb 22, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (~=3.6)
+
+ Extend py.test for RPC OpenStack testing.
+
+ :pypi:`pytest-rst`
+ *last release*: Sep 21, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ Test code from RST documents with pytest
+
+ :pypi:`pytest-rt`
+ *last release*: Sep 04, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest data collector plugin for Testgr
+
+ :pypi:`pytest-rts`
+ *last release*: May 17, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ Coverage-based regression test selection (RTS) plugin for pytest
+
+ :pypi:`pytest-run-changed`
+ *last release*: Apr 02, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ Pytest plugin that runs changed tests only
+
+ :pypi:`pytest-runfailed`
+ *last release*: Mar 24, 2016,
+ *status*: N/A,
+ *requires*: N/A
+
+ implement a --failed option for pytest
+
+ :pypi:`pytest-runner`
+ *last release*: May 19, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.6) ; extra == 'testing'
+
+ Invoke py.test as distutils command with dependency resolution
+
+ :pypi:`pytest-runtime-xfail`
+ *last release*: Aug 26, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Call runtime_xfail() to mark running test as xfail.
+
+ :pypi:`pytest-salt`
+ *last release*: Jan 27, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Pytest Salt Plugin
+
+ :pypi:`pytest-salt-containers`
+ *last release*: Nov 09, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ 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)
+
+ Pytest Salt Plugin
+
+ :pypi:`pytest-salt-from-filenames`
+ *last release*: Jan 29, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=4.1)
+
+ Simple PyTest Plugin For Salt's Test Suite Specifically
+
+ :pypi:`pytest-salt-runtests-bridge`
+ *last release*: Dec 05, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=4.1)
+
+ Simple PyTest Plugin For Salt's Test Suite Specifically
+
+ :pypi:`pytest-sanic`
+ *last release*: Oct 25, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=5.2)
+
+ a pytest plugin for Sanic
+
+ :pypi:`pytest-sanity`
+ *last release*: Dec 07, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-sa-pg`
+ *last release*: May 14, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-sbase`
+ *last release*: Dec 03, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ A complete web automation framework for end-to-end testing.
+
+ :pypi:`pytest-scenario`
+ *last release*: Feb 06, 2017,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest plugin for test scenarios
+
+ :pypi:`pytest-schema`
+ *last release*: Aug 31, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.5.0)
+
+ 👍 Validate return values against a schema-like object in testing
+
+ :pypi:`pytest-securestore`
+ *last release*: Nov 08, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ An encrypted password store for use within pytest cases
+
+ :pypi:`pytest-select`
+ *last release*: Jan 18, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.0)
+
+ A pytest plugin which allows to (de-)select tests from a file.
+
+ :pypi:`pytest-selenium`
+ *last release*: Sep 19, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.0.0)
+
+ pytest plugin for Selenium
+
+ :pypi:`pytest-seleniumbase`
+ *last release*: Dec 03, 2021,
+ *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,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ pytest plugin for Selenium
+
+ :pypi:`pytest-selenium-pdiff`
+ *last release*: Apr 06, 2017,
+ *status*: 2 - Pre-Alpha,
+ *requires*: N/A
+
+ A pytest package implementing perceptualdiff for Selenium tests.
+
+ :pypi:`pytest-send-email`
+ *last release*: Dec 04, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Send pytest execution result email
+
+ :pypi:`pytest-sentry`
+ *last release*: Apr 21, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ A pytest plugin to send testrun information to Sentry.io
+
+ :pypi:`pytest-server-fixtures`
+ *last release*: May 28, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Extensible server fixures for py.test
+
+ :pypi:`pytest-serverless`
+ *last release*: Nov 27, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Automatically mocks resources from serverless.yml in pytest using moto.
+
+ :pypi:`pytest-services`
+ *last release*: Oct 30, 2020,
+ *status*: 6 - Mature,
+ *requires*: N/A
+
+ Services plugin for pytest testing framework
+
+ :pypi:`pytest-session2file`
+ *last release*: Jan 26, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ 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.
+
+ :pypi:`pytest-session-fixture-globalize`
+ *last release*: May 15, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ py.test plugin to make session fixtures behave as if written in conftest, even if it is written in some modules
+
+ :pypi:`pytest-session_to_file`
+ *last release*: Oct 01, 2015,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest-session_to_file is a py.test plugin for capturing and saving to file the stdout of py.test.
+
+ :pypi:`pytest-sftpserver`
+ *last release*: Sep 16, 2019,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ py.test plugin to locally test sftp server connections.
+
+ :pypi:`pytest-shard`
+ *last release*: Dec 11, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+
+
+ :pypi:`pytest-shell`
+ *last release*: Nov 07, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ A pytest plugin to help with testing shell scripts / black box commands
+
+ :pypi:`pytest-sheraf`
+ *last release*: Feb 11, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ Versatile ZODB abstraction layer - pytest fixtures
+
+ :pypi:`pytest-sherlock`
+ *last release*: Nov 18, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.5.1)
+
+ pytest plugin help to find coupled tests
+
+ :pypi:`pytest-shortcuts`
+ *last release*: Oct 29, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Expand command-line shortcuts listed in pytest configuration
+
+ :pypi:`pytest-shutil`
+ *last release*: May 28, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ A goodie-bag of unix shell and environment tools for py.test
+
+ :pypi:`pytest-simplehttpserver`
+ *last release*: Jun 24, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Simple pytest fixture to spin up an HTTP server
+
+ :pypi:`pytest-simple-plugin`
+ *last release*: Nov 27, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ Simple pytest plugin
+
+ :pypi:`pytest-simple-settings`
+ *last release*: Nov 17, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ simple-settings plugin for pytest
+
+ :pypi:`pytest-single-file-logging`
+ *last release*: May 05, 2016,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.8.1)
+
+ 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)
+
+ Pytest Salt Plugin
+
+ :pypi:`pytest-skipper`
+ *last release*: Mar 26, 2017,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.0.6)
+
+ A plugin that selects only tests with changes in execution path
+
+ :pypi:`pytest-skippy`
+ *last release*: Jan 27, 2018,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=2.3.4)
+
+ Automatically skip tests that don't need to run!
+
+ :pypi:`pytest-skip-slow`
+ *last release*: Sep 28, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ A pytest plugin to skip \`@pytest.mark.slow\` tests by default.
+
+ :pypi:`pytest-slack`
+ *last release*: Dec 15, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Pytest to Slack reporting plugin
+
+ :pypi:`pytest-slow`
+ *last release*: Sep 28, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ A pytest plugin to skip \`@pytest.mark.slow\` tests by default.
+
+ :pypi:`pytest-smartcollect`
+ *last release*: Oct 04, 2018,
+ *status*: N/A,
+ *requires*: pytest (>=3.5.0)
+
+ A plugin for collecting tests that touch changed code
+
+ :pypi:`pytest-smartcov`
+ *last release*: Sep 30, 2017,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Smart coverage plugin for pytest.
+
+ :pypi:`pytest-smtp`
+ *last release*: Feb 20, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ Send email with pytest execution result
+
+ :pypi:`pytest-snail`
+ *last release*: Nov 04, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=5.0.1)
+
+ Plugin for adding a marker to slow running tests. 🐌
+
+ :pypi:`pytest-snapci`
+ *last release*: Nov 12, 2015,
+ *status*: N/A,
+ *requires*: N/A
+
+ py.test plugin for Snap-CI
+
+ :pypi:`pytest-snapshot`
+ *last release*: Dec 02, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.0.0)
+
+ A plugin for snapshot testing with pytest.
+
+ :pypi:`pytest-snmpserver`
+ *last release*: May 12, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-socket`
+ *last release*: Aug 28, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.6.3)
+
+ Pytest Plugin to disable socket calls during tests
+
+ :pypi:`pytest-soft-assertions`
+ *last release*: May 05, 2020,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+
+
+ :pypi:`pytest-solr`
+ *last release*: May 11, 2020,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.0.0)
+
+ Solr process and client fixtures for py.test.
+
+ :pypi:`pytest-sorter`
+ *last release*: Apr 20, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.1.1)
+
+ A simple plugin to first execute tests that historically failed more
+
+ :pypi:`pytest-sourceorder`
+ *last release*: Sep 01, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ Test-ordering plugin for pytest
+
+ :pypi:`pytest-spark`
+ *last release*: Feb 23, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ pytest plugin to run the tests with support of pyspark.
+
+ :pypi:`pytest-spawner`
+ *last release*: Jul 31, 2015,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ py.test plugin to spawn process and communicate with them.
+
+ :pypi:`pytest-spec`
+ *last release*: May 04, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Library pytest-spec is a pytest plugin to display test execution output like a SPECIFICATION.
+
+ :pypi:`pytest-sphinx`
+ *last release*: Aug 05, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Doctest plugin for pytest with support for Sphinx-specific doctest-directives
+
+ :pypi:`pytest-spiratest`
+ *last release*: Oct 13, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Exports unit tests as test runs in SpiraTest/Team/Plan
+
+ :pypi:`pytest-splinter`
+ *last release*: Dec 25, 2020,
+ *status*: 6 - Mature,
+ *requires*: N/A
+
+ Splinter plugin for pytest testing framework
+
+ :pypi:`pytest-split`
+ *last release*: Nov 09, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=5,<7)
+
+ Pytest plugin which splits the test suite to equally sized sub suites based on test execution time.
+
+ :pypi:`pytest-splitio`
+ *last release*: Sep 22, 2020,
+ *status*: N/A,
+ *requires*: pytest (<7,>=5.0)
+
+ Split.io SDK integration for e2e tests
+
+ :pypi:`pytest-split-tests`
+ *last release*: Jul 30, 2021,
+ *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. Forked from Mark Adams' original project pytest-test-groups.
+
+ :pypi:`pytest-split-tests-tresorit`
+ *last release*: Feb 22, 2021,
+ *status*: 1 - Planning,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-splunk-addon`
+ *last release*: Nov 29, 2021,
+ *status*: N/A,
+ *requires*: pytest (>5.4.0,<6.3)
+
+ A Dynamic test tool for Splunk Apps and Add-ons
+
+ :pypi:`pytest-splunk-addon-ui-smartx`
+ *last release*: Oct 07, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Library to support testing Splunk Add-on UX
+
+ :pypi:`pytest-splunk-env`
+ *last release*: Oct 22, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=6.1.1,<7.0.0)
+
+ pytest fixtures for interaction with Splunk Enterprise and Splunk Cloud
+
+ :pypi:`pytest-sqitch`
+ *last release*: Apr 06, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ sqitch for pytest
+
+ :pypi:`pytest-sqlalchemy`
+ *last release*: Mar 13, 2018,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest plugin with sqlalchemy related fixtures
+
+ :pypi:`pytest-sql-bigquery`
+ *last release*: Dec 19, 2019,
+ *status*: N/A,
+ *requires*: pytest
+
+ Yet another SQL-testing framework for BigQuery provided by pytest plugin
+
+ :pypi:`pytest-srcpaths`
+ *last release*: Oct 15, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Add paths to sys.path
+
+ :pypi:`pytest-ssh`
+ *last release*: May 27, 2019,
+ *status*: N/A,
+ *requires*: pytest
+
+ pytest plugin for ssh command run
+
+ :pypi:`pytest-start-from`
+ *last release*: Apr 11, 2016,
+ *status*: N/A,
+ *requires*: N/A
+
+ Start pytest run from a given point
+
+ :pypi:`pytest-statsd`
+ *last release*: Nov 30, 2018,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.0.0)
+
+ pytest plugin for reporting to graphite
+
+ :pypi:`pytest-stepfunctions`
+ *last release*: May 08, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ A small description
+
+ :pypi:`pytest-steps`
+ *last release*: Sep 23, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Create step-wise / incremental tests in pytest.
+
+ :pypi:`pytest-stepwise`
+ *last release*: Dec 01, 2015,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Run a test suite one failing test at a time.
+
+ :pypi:`pytest-stoq`
+ *last release*: Feb 09, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A plugin to pytest stoq
+
+ :pypi:`pytest-stress`
+ *last release*: Dec 07, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.6.0)
+
+ A Pytest plugin that allows you to loop tests for a user defined amount of time.
+
+ :pypi:`pytest-structlog`
+ *last release*: Sep 21, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ Structured logging assertions
+
+ :pypi:`pytest-structmpd`
+ *last release*: Oct 17, 2018,
+ *status*: N/A,
+ *requires*: N/A
+
+ provide structured temporary directory
+
+ :pypi:`pytest-stub`
+ *last release*: Apr 28, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Stub packages, modules and attributes.
+
+ :pypi:`pytest-stubprocess`
+ *last release*: Sep 17, 2018,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.5.0)
+
+ Provide stub implementations for subprocesses in Python tests
+
+ :pypi:`pytest-study`
+ *last release*: Sep 26, 2017,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=2.0)
+
+ A pytest plugin to organize long run tests (named studies) without interfering the regular tests
+
+ :pypi:`pytest-subprocess`
+ *last release*: Nov 07, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.0.0)
+
+ A plugin to fake subprocess for pytest
+
+ :pypi:`pytest-subtesthack`
+ *last release*: Mar 02, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ A hack to explicitly set up and tear down fixtures.
+
+ :pypi:`pytest-subtests`
+ *last release*: May 29, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=5.3.0)
+
+ unittest subTest() support and subtests fixture
+
+ :pypi:`pytest-subunit`
+ *last release*: Aug 29, 2017,
+ *status*: N/A,
+ *requires*: N/A
+
+ 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
+
+ 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'
+
+ Workaround for https://github.com/Frozenball/pytest-sugar/issues/159
+
+ :pypi:`pytest-super-check`
+ *last release*: Aug 12, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Pytest plugin to check your TestCase classes call super in setUp, tearDown, etc.
+
+ :pypi:`pytest-svn`
+ *last release*: May 28, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ SVN repository fixture for py.test
+
+ :pypi:`pytest-symbols`
+ *last release*: Nov 20, 2017,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ pytest-symbols is a pytest plugin that adds support for passing test environment symbols into pytest tests.
+
+ :pypi:`pytest-takeltest`
+ *last release*: Oct 13, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Fixtures for ansible, testinfra and molecule
+
+ :pypi:`pytest-talisker`
+ *last release*: Nov 28, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-tap`
+ *last release*: Oct 27, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.0)
+
+ Test Anything Protocol (TAP) reporting plugin for pytest
+
+ :pypi:`pytest-tape`
+ *last release*: Mar 17, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ easy assertion with expected results saved to yaml files
+
+ :pypi:`pytest-target`
+ *last release*: Jan 21, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=6.1.2,<7.0.0)
+
+ Pytest plugin for remote target orchestration.
+
+ :pypi:`pytest-tblineinfo`
+ *last release*: Dec 01, 2015,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=2.0)
+
+ 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-teamcity-logblock`
+ *last release*: May 15, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ py.test plugin to introduce block structure in teamcity build log, if output is not captured
+
+ :pypi:`pytest-telegram`
+ *last release*: Dec 10, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Pytest to Telegram reporting plugin
+
+ :pypi:`pytest-tempdir`
+ *last release*: Oct 11, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.8.1)
+
+ Predictable and repeatable tempdir support.
+
+ :pypi:`pytest-terraform`
+ *last release*: Nov 10, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=6.0)
+
+ A pytest plugin for using terraform fixtures
+
+ :pypi:`pytest-terraform-fixture`
+ *last release*: Nov 14, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ generate terraform resources to use with pytest
+
+ :pypi:`pytest-testbook`
+ *last release*: Dec 11, 2016,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ A plugin to run tests written in Jupyter notebook
+
+ :pypi:`pytest-testconfig`
+ *last release*: Jan 11, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Test configuration plugin for pytest.
+
+ :pypi:`pytest-testdirectory`
+ *last release*: Nov 06, 2018,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ A py.test plugin providing temporary directories in unit tests.
+
+ :pypi:`pytest-testdox`
+ *last release*: Oct 13, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.7.0)
+
+ A testdox format reporter for pytest
+
+ :pypi:`pytest-test-groups`
+ *last release*: Oct 25, 2016,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ 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,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (!=3.0.2)
+
+ Test infrastructures
+
+ :pypi:`pytest-testlink-adaptor`
+ *last release*: Dec 20, 2018,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.6)
+
+ pytest reporting plugin for testlink
+
+ :pypi:`pytest-testmon`
+ *last release*: Oct 22, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ selects tests affected by changed files and methods
+
+ :pypi:`pytest-testobject`
+ *last release*: Sep 24, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.1.1)
+
+ Plugin to use TestObject Suites with Pytest
+
+ :pypi:`pytest-testrail`
+ *last release*: Aug 27, 2020,
+ *status*: N/A,
+ *requires*: pytest (>=3.6)
+
+ 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,
+ *status*: N/A,
+ *requires*: pytest (>=5.5)
+
+ Плагин Pytest, для интеграции с TestRail
+
+ :pypi:`pytest-testrail-api-client`
+ *last release*: Dec 03, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ TestRail Api Python Client
+
+ :pypi:`pytest-testrail-appetize`
+ *last release*: Sep 29, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ pytest plugin for creating TestRail runs and adding results
+
+ :pypi:`pytest-testrail-client`
+ *last release*: Sep 29, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ pytest plugin for Testrail
+
+ :pypi:`pytest-testrail-e2e`
+ *last release*: Oct 11, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=3.6)
+
+ pytest plugin for creating TestRail runs and adding results
+
+ :pypi:`pytest-testrail-ns`
+ *last release*: Oct 08, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=3.6)
+
+ pytest plugin for creating TestRail runs and adding results
+
+ :pypi:`pytest-testrail-plugin`
+ *last release*: Apr 21, 2020,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ PyTest plugin for TestRail
+
+ :pypi:`pytest-testrail-reporter`
+ *last release*: Sep 10, 2018,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-testreport`
+ *last release*: Nov 12, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+
+
+ :pypi:`pytest-testslide`
+ *last release*: Jan 07, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (~=6.2)
+
+ TestSlide fixture for pytest
+
+ :pypi:`pytest-test-this`
+ *last release*: Sep 15, 2019,
+ *status*: 2 - Pre-Alpha,
+ *requires*: pytest (>=2.3)
+
+ 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,
+ *status*: N/A,
+ *requires*: pytest (>=5)
+
+
+
+ :pypi:`pytest-tesults`
+ *last release*: Jul 31, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.5.0)
+
+ Tesults plugin for pytest
+
+ :pypi:`pytest-tezos`
+ *last release*: Jan 16, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ pytest-ligo
+
+ :pypi:`pytest-thawgun`
+ *last release*: May 26, 2020,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Pytest plugin for time travel
+
+ :pypi:`pytest-threadleak`
+ *last release*: Sep 08, 2017,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Detects thread leaks
+
+ :pypi:`pytest-tick`
+ *last release*: Aug 31, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=6.2.5,<7.0.0)
+
+ Ticking on tests
+
+ :pypi:`pytest-timeit`
+ *last release*: Oct 13, 2016,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A pytest plugin to time test function runs
+
+ :pypi:`pytest-timeout`
+ *last release*: Oct 11, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.0.0)
+
+ pytest plugin to abort hanging tests
+
+ :pypi:`pytest-timeouts`
+ *last release*: Sep 21, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Linux-only Pytest plugin to control durations of various test case execution phases
+
+ :pypi:`pytest-timer`
+ *last release*: Jun 02, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ A timer plugin for pytest
+
+ :pypi:`pytest-timestamper`
+ *last release*: Jun 06, 2021,
+ *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,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.0.0)
+
+
+
+ :pypi:`pytest-tipsi-testing`
+ *last release*: Nov 04, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.3.0)
+
+ Better fixtures management. Various helpers
+
+ :pypi:`pytest-tldr`
+ *last release*: Mar 12, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A pytest plugin that limits the output to just the things you need.
+
+ :pypi:`pytest-tm4j-reporter`
+ *last release*: Sep 01, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ Cloud Jira Test Management (TM4J) PyTest reporter plugin
+
+ :pypi:`pytest-tmreport`
+ *last release*: Nov 17, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ this is a vue-element ui report for pytest
+
+ :pypi:`pytest-todo`
+ *last release*: May 23, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ A small plugin for the pytest testing framework, marking TODO comments as failure
+
+ :pypi:`pytest-tomato`
+ *last release*: Mar 01, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-toolbelt`
+ *last release*: Aug 12, 2019,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ This is just a collection of utilities for pytest, but don't really belong in pytest proper.
+
+ :pypi:`pytest-toolbox`
+ *last release*: Apr 07, 2018,
+ *status*: N/A,
+ *requires*: pytest (>=3.5.0)
+
+ Numerous useful plugins for pytest.
+
+ :pypi:`pytest-tornado`
+ *last release*: Jun 17, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.6)
+
+ A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications.
+
+ :pypi:`pytest-tornado5`
+ *last release*: Nov 16, 2018,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.6)
+
+ A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications.
+
+ :pypi:`pytest-tornado-yen3`
+ *last release*: Oct 15, 2018,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications.
+
+ :pypi:`pytest-tornasync`
+ *last release*: Jul 15, 2019,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.0)
+
+ py.test plugin for testing Python 3.5+ Tornado code
+
+ :pypi:`pytest-track`
+ *last release*: Feb 26, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.0)
+
+
+
+ :pypi:`pytest-translations`
+ *last release*: Nov 05, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Test your translation files.
+
+ :pypi:`pytest-travis-fold`
+ *last release*: Nov 29, 2017,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.6.0)
+
+ Folds captured output sections in Travis CI build log
+
+ :pypi:`pytest-trello`
+ *last release*: Nov 20, 2015,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Plugin for py.test that integrates trello using markers
+
+ :pypi:`pytest-trepan`
+ *last release*: Jul 28, 2018,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Pytest plugin for trepan debugger.
+
+ :pypi:`pytest-trialtemp`
+ *last release*: Jun 08, 2015,
+ *status*: N/A,
+ *requires*: N/A
+
+ py.test plugin for using the same _trial_temp working directory as trial
+
+ :pypi:`pytest-trio`
+ *last release*: Oct 16, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest plugin for trio
+
+ :pypi:`pytest-tspwplib`
+ *last release*: Jan 08, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ A simple plugin to use with tspwplib
+
+ :pypi:`pytest-tstcls`
+ *last release*: Mar 23, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Test Class Base
+
+ :pypi:`pytest-twisted`
+ *last release*: Aug 30, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.3)
+
+ A twisted plugin for pytest.
+
+ :pypi:`pytest-typhoon-xray`
+ *last release*: Nov 03, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Typhoon HIL plugin for pytest
+
+ :pypi:`pytest-tytest`
+ *last release*: May 25, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=5.4.2)
+
+ Typhoon HIL plugin for pytest
+
+ :pypi:`pytest-ubersmith`
+ *last release*: Apr 13, 2015,
+ *status*: N/A,
+ *requires*: N/A
+
+ Easily mock calls to ubersmith at the \`requests\` level.
+
+ :pypi:`pytest-ui`
+ *last release*: Jul 05, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ Text User Interface for running python tests
+
+ :pypi:`pytest-unhandled-exception-exit-code`
+ *last release*: Jun 22, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.3)
+
+ Plugin for py.test set a different exit code on uncaught exceptions
+
+ :pypi:`pytest-unittest-filter`
+ *last release*: Jan 12, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.1.0)
+
+ A pytest plugin for filtering unittest-based test classes
+
+ :pypi:`pytest-unmarked`
+ *last release*: Aug 27, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Run only unmarked tests
+
+ :pypi:`pytest-unordered`
+ *last release*: Mar 28, 2021,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Test equality of unordered collections in pytest
+
+ :pypi:`pytest-upload-report`
+ *last release*: Jun 18, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ pytest-upload-report is a plugin for pytest that upload your test report for test results.
+
+ :pypi:`pytest-utils`
+ *last release*: Dec 04, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.2.5,<7.0.0)
+
+ Some helpers for pytest.
+
+ :pypi:`pytest-vagrant`
+ *last release*: Sep 07, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ A py.test plugin providing access to vagrant.
+
+ :pypi:`pytest-valgrind`
+ *last release*: May 19, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-variables`
+ *last release*: Oct 23, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=2.4.2)
+
+ pytest plugin for providing variables to tests/fixtures
+
+ :pypi:`pytest-variant`
+ *last release*: Jun 20, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Variant support for Pytest
+
+ :pypi:`pytest-vcr`
+ *last release*: Apr 26, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=3.6.0)
+
+ 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)
+
+ A pytest plugin that automates vcrpy cassettes deletion on test failure.
+
+ :pypi:`pytest-vcrpandas`
+ *last release*: Jan 12, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest
+
+ Test from HTTP interactions to dataframe processed.
+
+ :pypi:`pytest-venv`
+ *last release*: Aug 04, 2020,
+ *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
+
+ Pytest module with Verification Report
+
+ :pypi:`pytest-verbose-parametrize`
+ *last release*: May 28, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ More descriptive output for parametrized py.test tests
+
+ :pypi:`pytest-vimqf`
+ *last release*: Feb 08, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=6.2.2,<7.0.0)
+
+ A simple pytest plugin that will shrink pytest output when specified, to fit vim quickfix window.
+
+ :pypi:`pytest-virtualenv`
+ *last release*: May 28, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Virtualenv fixture for py.test
+
+ :pypi:`pytest-voluptuous`
+ *last release*: Jun 09, 2020,
+ *status*: N/A,
+ *requires*: pytest
+
+ Pytest plugin for asserting data against voluptuous schema.
+
+ :pypi:`pytest-vscodedebug`
+ *last release*: Dec 04, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ A pytest plugin to easily enable debugging tests within Visual Studio Code
+
+ :pypi:`pytest-vts`
+ *last release*: Jun 05, 2019,
+ *status*: N/A,
+ *requires*: pytest (>=2.3)
+
+ pytest plugin for automatic recording of http stubbed tests
+
+ :pypi:`pytest-vw`
+ *last release*: Oct 07, 2015,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ pytest-vw makes your failing test cases succeed under CI tools scrutiny
+
+ :pypi:`pytest-vyper`
+ *last release*: May 28, 2020,
+ *status*: 2 - Pre-Alpha,
+ *requires*: N/A
+
+ Plugin for the vyper smart contract language.
+
+ :pypi:`pytest-wa-e2e-plugin`
+ *last release*: Feb 18, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.5.0)
+
+ Pytest plugin for testing whatsapp bots with end to end tests
+
+ :pypi:`pytest-watch`
+ *last release*: May 20, 2018,
+ *status*: N/A,
+ *requires*: N/A
+
+ Local continuous test runner with pytest and watchdog.
+
+ :pypi:`pytest-watcher`
+ *last release*: Sep 18, 2021,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+ Continiously runs pytest on changes in \*.py files
+
+ :pypi:`pytest-wdl`
+ *last release*: Nov 17, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: N/A
+
+ Pytest plugin for testing WDL workflows.
+
+ :pypi:`pytest-webdriver`
+ *last release*: May 28, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest
+
+ Selenium webdriver fixture for py.test
+
+ :pypi:`pytest-wetest`
+ *last release*: Nov 10, 2018,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Welian API Automation test framework pytest plugin
+
+ :pypi:`pytest-whirlwind`
+ *last release*: Jun 12, 2020,
+ *status*: N/A,
+ *requires*: N/A
+
+ Testing Tornado.
+
+ :pypi:`pytest-wholenodeid`
+ *last release*: Aug 26, 2015,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.0)
+
+ pytest addon for displaying the whole node id for failures
+
+ :pypi:`pytest-win32consoletitle`
+ *last release*: Aug 08, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest progress in console title (Win32 only)
+
+ :pypi:`pytest-winnotify`
+ *last release*: Apr 22, 2016,
+ *status*: N/A,
+ *requires*: N/A
+
+ Windows tray notifications for py.test results.
+
+ :pypi:`pytest-with-docker`
+ *last release*: Nov 09, 2021,
+ *status*: N/A,
+ *requires*: pytest
+
+ pytest with docker helpers.
+
+ :pypi:`pytest-workflow`
+ *last release*: Dec 03, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.4.0)
+
+ A pytest plugin for configuring workflow/pipeline tests using YAML files
+
+ :pypi:`pytest-xdist`
+ *last release*: Sep 21, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=6.0.0)
+
+ pytest xdist plugin for distributed testing and loop-on-failing modes
+
+ :pypi:`pytest-xdist-debug-for-graingert`
+ *last release*: Jul 24, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.4.0)
+
+ pytest xdist plugin for distributed testing and loop-on-failing modes
+
+ :pypi:`pytest-xdist-forked`
+ *last release*: Feb 10, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.4.0)
+
+ forked from pytest-xdist
+
+ :pypi:`pytest-xdist-tracker`
+ *last release*: Nov 18, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=3.5.1)
+
+ pytest plugin helps to reproduce failures for particular xdist node
+
+ :pypi:`pytest-xfaillist`
+ *last release*: Sep 17, 2021,
+ *status*: N/A,
+ *requires*: pytest (>=6.2.2,<7.0.0)
+
+ Maintain a xfaillist in an additional file to avoid merge-conflicts.
+
+ :pypi:`pytest-xfiles`
+ *last release*: Feb 27, 2018,
+ *status*: N/A,
+ *requires*: N/A
+
+ Pytest fixtures providing data read from function, module or package related (x)files.
+
+ :pypi:`pytest-xlog`
+ *last release*: May 31, 2020,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ Extended logging for test and decorators
+
+ :pypi:`pytest-xpara`
+ *last release*: Oct 30, 2017,
+ *status*: 3 - Alpha,
+ *requires*: pytest
+
+ An extended parametrizing plugin of pytest.
+
+ :pypi:`pytest-xprocess`
+ *last release*: Jul 28, 2021,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.8)
+
+ A pytest plugin for managing processes across test runs.
+
+ :pypi:`pytest-xray`
+ *last release*: May 30, 2019,
+ *status*: 3 - Alpha,
+ *requires*: N/A
+
+
+
+ :pypi:`pytest-xrayjira`
+ *last release*: Mar 17, 2020,
+ *status*: 3 - Alpha,
+ *requires*: pytest (==4.3.1)
+
+
+
+ :pypi:`pytest-xray-server`
+ *last release*: Oct 27, 2021,
+ *status*: 3 - Alpha,
+ *requires*: pytest (>=5.3.1)
+
+
+
+ :pypi:`pytest-xvfb`
+ *last release*: Jun 09, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=2.8.1)
+
+ A pytest plugin to run Xvfb for tests.
+
+ :pypi:`pytest-yaml`
+ *last release*: Oct 05, 2018,
+ *status*: N/A,
+ *requires*: pytest
+
+ This plugin is used to load yaml output to your test using pytest framework.
+
+ :pypi:`pytest-yamltree`
+ *last release*: Mar 02, 2020,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.1.1)
+
+ Create or check file/directory trees described by YAML
+
+ :pypi:`pytest-yamlwsgi`
+ *last release*: May 11, 2010,
+ *status*: N/A,
+ *requires*: N/A
+
+ Run tests against wsgi apps defined in yaml
+
+ :pypi:`pytest-yapf`
+ *last release*: Jul 06, 2017,
+ *status*: 4 - Beta,
+ *requires*: pytest (>=3.1.1)
+
+ Run yapf
+
+ :pypi:`pytest-yapf3`
+ *last release*: Aug 03, 2020,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=5.4)
+
+ Validate your Python file format with yapf
+
+ :pypi:`pytest-yield`
+ *last release*: Jan 23, 2019,
+ *status*: N/A,
+ *requires*: N/A
+
+ PyTest plugin to run tests concurrently, each \`yield\` switch context to other one
+
+ :pypi:`pytest-yuk`
+ *last release*: Mar 26, 2021,
+ *status*: N/A,
+ *requires*: N/A
+
+ Display tests you are uneasy with, using 🤢/🤮 for pass/fail of tests marked with yuk.
+
+ :pypi:`pytest-zafira`
+ *last release*: Sep 18, 2019,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (==4.1.1)
+
+ A Zafira plugin for pytest
+
+ :pypi:`pytest-zap`
+ *last release*: May 12, 2014,
+ *status*: 4 - Beta,
+ *requires*: N/A
+
+ OWASP ZAP plugin for py.test.
+
+ :pypi:`pytest-zebrunner`
+ *last release*: Dec 02, 2021,
+ *status*: 5 - Production/Stable,
+ *requires*: pytest (>=4.5.0)
+
+ Pytest connector for Zebrunner reporting
+
+ :pypi:`pytest-zigzag`
+ *last release*: Feb 27, 2019,
+ *status*: 4 - Beta,
+ *requires*: pytest (~=3.6)
+
+ Extend py.test for RPC OpenStack testing.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/reference.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/reference.rst
new file mode 100644
index 0000000000..0d80c80680
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/reference/reference.rst
@@ -0,0 +1,2101 @@
+.. _`api-reference`:
+
+API Reference
+=============
+
+This page contains the full reference to pytest's API.
+
+.. contents::
+ :depth: 3
+ :local:
+
+Constants
+---------
+
+pytest.__version__
+~~~~~~~~~~~~~~~~~~
+
+The current pytest version, as a string::
+
+ >>> import pytest
+ >>> pytest.__version__
+ '7.0.0'
+
+
+.. _`version-tuple`:
+
+pytest.version_tuple
+~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 7.0
+
+The current pytest version, as a tuple::
+
+ >>> import pytest
+ >>> pytest.version_tuple
+ (7, 0, 0)
+
+For pre-releases, the last component will be a string with the prerelease version::
+
+ >>> import pytest
+ >>> pytest.version_tuple
+ (7, 0, '0rc1')
+
+
+Functions
+---------
+
+pytest.approx
+~~~~~~~~~~~~~
+
+.. autofunction:: pytest.approx
+
+pytest.fail
+~~~~~~~~~~~
+
+**Tutorial**: :ref:`skipping`
+
+.. autofunction:: pytest.fail(reason, [pytrace=True, msg=None])
+
+pytest.skip
+~~~~~~~~~~~
+
+.. autofunction:: pytest.skip(reason, [allow_module_level=False, msg=None])
+
+.. _`pytest.importorskip ref`:
+
+pytest.importorskip
+~~~~~~~~~~~~~~~~~~~
+
+.. autofunction:: pytest.importorskip
+
+pytest.xfail
+~~~~~~~~~~~~
+
+.. autofunction:: pytest.xfail
+
+pytest.exit
+~~~~~~~~~~~
+
+.. autofunction:: pytest.exit(reason, [returncode=False, msg=None])
+
+pytest.main
+~~~~~~~~~~~
+
+.. autofunction:: pytest.main
+
+pytest.param
+~~~~~~~~~~~~
+
+.. autofunction:: pytest.param(*values, [id], [marks])
+
+pytest.raises
+~~~~~~~~~~~~~
+
+**Tutorial**: :ref:`assertraises`.
+
+.. autofunction:: pytest.raises(expected_exception: Exception [, *, match])
+ :with: excinfo
+
+pytest.deprecated_call
+~~~~~~~~~~~~~~~~~~~~~~
+
+**Tutorial**: :ref:`ensuring_function_triggers`.
+
+.. autofunction:: pytest.deprecated_call()
+ :with:
+
+pytest.register_assert_rewrite
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+**Tutorial**: :ref:`assertion-rewriting`.
+
+.. autofunction:: pytest.register_assert_rewrite
+
+pytest.warns
+~~~~~~~~~~~~
+
+**Tutorial**: :ref:`assertwarnings`
+
+.. autofunction:: pytest.warns(expected_warning: Exception, [match])
+ :with:
+
+pytest.freeze_includes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+**Tutorial**: :ref:`freezing-pytest`.
+
+.. autofunction:: pytest.freeze_includes
+
+.. _`marks ref`:
+
+Marks
+-----
+
+Marks can be used apply meta data to *test functions* (but not fixtures), which can then be accessed by
+fixtures or plugins.
+
+
+
+
+.. _`pytest.mark.filterwarnings ref`:
+
+pytest.mark.filterwarnings
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+**Tutorial**: :ref:`filterwarnings`.
+
+Add warning filters to marked test items.
+
+.. py:function:: pytest.mark.filterwarnings(filter)
+
+ :keyword str filter:
+ A *warning specification string*, which is composed of contents of the tuple ``(action, message, category, module, lineno)``
+ as specified in :ref:`python:warning-filter` section of
+ the Python documentation, separated by ``":"``. Optional fields can be omitted.
+ Module names passed for filtering are not regex-escaped.
+
+ For example:
+
+ .. code-block:: python
+
+ @pytest.mark.filterwarnings("ignore:.*usage will be deprecated.*:DeprecationWarning")
+ def test_foo():
+ ...
+
+
+.. _`pytest.mark.parametrize ref`:
+
+pytest.mark.parametrize
+~~~~~~~~~~~~~~~~~~~~~~~
+
+:ref:`parametrize`.
+
+This mark has the same signature as :py:meth:`pytest.Metafunc.parametrize`; see there.
+
+
+.. _`pytest.mark.skip ref`:
+
+pytest.mark.skip
+~~~~~~~~~~~~~~~~
+
+:ref:`skip`.
+
+Unconditionally skip a test function.
+
+.. py:function:: pytest.mark.skip(reason=None)
+
+ :keyword str reason: Reason why the test function is being skipped.
+
+
+.. _`pytest.mark.skipif ref`:
+
+pytest.mark.skipif
+~~~~~~~~~~~~~~~~~~
+
+:ref:`skipif`.
+
+Skip a test function if a condition is ``True``.
+
+.. py:function:: pytest.mark.skipif(condition, *, reason=None)
+
+ :type condition: bool or str
+ :param condition: ``True/False`` if the condition should be skipped or a :ref:`condition string <string conditions>`.
+ :keyword str reason: Reason why the test function is being skipped.
+
+
+.. _`pytest.mark.usefixtures ref`:
+
+pytest.mark.usefixtures
+~~~~~~~~~~~~~~~~~~~~~~~
+
+**Tutorial**: :ref:`usefixtures`.
+
+Mark a test function as using the given fixture names.
+
+.. py:function:: pytest.mark.usefixtures(*names)
+
+ :param args: The names of the fixture to use, as strings.
+
+.. note::
+
+ When using `usefixtures` in hooks, it can only load fixtures when applied to a test function before test setup
+ (for example in the `pytest_collection_modifyitems` hook).
+
+ Also note that this mark has no effect when applied to **fixtures**.
+
+
+
+.. _`pytest.mark.xfail ref`:
+
+pytest.mark.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)
+
+ :type condition: bool or str
+ :param condition:
+ Condition for marking the test function as xfail (``True/False`` or a
+ :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 bool run:
+ If 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
+ 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
+ unexpectedly passes then it will **fail** the test suite. This is particularly useful to mark functions
+ 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).
+
+
+Custom marks
+~~~~~~~~~~~~
+
+Marks are created dynamically using the factory object ``pytest.mark`` and applied as a decorator.
+
+For example:
+
+.. code-block:: python
+
+ @pytest.mark.timeout(10, "slow", method="thread")
+ 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
+:meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`. The ``mark`` object will have the following attributes:
+
+.. code-block:: python
+
+ mark.args == (10, "slow")
+ mark.kwargs == {"method": "thread"}
+
+Example for using multiple custom markers:
+
+.. code-block:: python
+
+ @pytest.mark.timeout(10, "slow", method="thread")
+ @pytest.mark.slow
+ 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(...)``.
+
+.. _`fixtures-api`:
+
+Fixtures
+--------
+
+**Tutorial**: :ref:`fixture`.
+
+Fixtures are requested by test functions or other fixtures by declaring them as argument names.
+
+
+Example of a test requiring a fixture:
+
+.. code-block:: python
+
+ def test_output(capsys):
+ print("hello")
+ out, err = capsys.readouterr()
+ assert out == "hello\n"
+
+
+Example of a fixture requiring another fixture:
+
+.. code-block:: python
+
+ @pytest.fixture
+ def db_session(tmp_path):
+ fn = tmp_path / "db.file"
+ return connect(fn)
+
+For more details, consult the full :ref:`fixtures docs <fixture>`.
+
+
+.. _`pytest.fixture-api`:
+
+@pytest.fixture
+~~~~~~~~~~~~~~~
+
+.. autofunction:: pytest.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`.
+
+.. 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`.
+
+.. 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`.
+
+.. autofunction:: _pytest.doctest.doctest_namespace()
+
+ 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`.
+
+
+.. fixture:: request
+
+request
+~~~~~~~
+
+:ref:`request example`.
+
+The ``request`` fixture is a special fixture providing information of the requesting test function.
+
+.. autoclass:: pytest.FixtureRequest()
+ :members:
+
+
+.. fixture:: pytestconfig
+
+pytestconfig
+~~~~~~~~~~~~
+
+.. autofunction:: _pytest.fixtures.pytestconfig()
+
+
+.. fixture:: record_property
+
+record_property
+~~~~~~~~~~~~~~~~~~~
+
+**Tutorial**: :ref:`record_property example`.
+
+.. 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:: caplog
+
+caplog
+~~~~~~
+
+:ref:`logging`.
+
+.. autofunction:: _pytest.logging.caplog()
+ :no-auto-options:
+
+ Returns a :class:`pytest.LogCaptureFixture` instance.
+
+.. autoclass:: pytest.LogCaptureFixture()
+ :members:
+
+
+.. fixture:: monkeypatch
+
+monkeypatch
+~~~~~~~~~~~
+
+:ref:`monkeypatching`.
+
+.. autofunction:: _pytest.monkeypatch.monkeypatch()
+ :no-auto-options:
+
+ Returns a :class:`~pytest.MonkeyPatch` instance.
+
+.. autoclass:: pytest.MonkeyPatch
+ :members:
+
+
+.. fixture:: pytester
+
+pytester
+~~~~~~~~
+
+.. versionadded:: 6.2
+
+Provides a :class:`~pytest.Pytester` instance that can be used to run and test pytest itself.
+
+It provides an empty directory where pytest can be executed in isolation, and contains facilities
+to write tests, configuration files, and match against expected output.
+
+To use it, include in your topmost ``conftest.py`` file:
+
+.. code-block:: python
+
+ pytest_plugins = "pytester"
+
+
+
+.. autoclass:: pytest.Pytester()
+ :members:
+
+.. autoclass:: pytest.RunResult()
+ :members:
+
+.. autoclass:: pytest.LineMatcher()
+ :members:
+ :special-members: __str__
+
+.. autoclass:: pytest.HookRecorder()
+ :members:
+
+.. autoclass:: pytest.RecordedHookCall()
+ :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:
+
+
+.. fixture:: recwarn
+
+recwarn
+~~~~~~~
+
+**Tutorial**: :ref:`assertwarnings`
+
+.. autofunction:: _pytest.recwarn.recwarn()
+ :no-auto-options:
+
+.. 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:: tmp_path
+
+tmp_path
+~~~~~~~~
+
+:ref:`tmp_path`
+
+.. autofunction:: _pytest.tmpdir.tmp_path()
+ :no-auto-options:
+
+
+.. fixture:: tmp_path_factory
+
+tmp_path_factory
+~~~~~~~~~~~~~~~~
+
+:ref:`tmp_path_factory example`
+
+.. _`tmp_path_factory factory api`:
+
+``tmp_path_factory`` is an instance of :class:`~pytest.TempPathFactory`:
+
+.. autoclass:: pytest.TempPathFactory()
+ :members:
+
+
+.. fixture:: tmpdir
+
+tmpdir
+~~~~~~
+
+:ref:`tmpdir and tmpdir_factory`
+
+.. autofunction:: _pytest.legacypath.LegacyTmpdirPlugin.tmpdir()
+ :no-auto-options:
+
+
+.. fixture:: tmpdir_factory
+
+tmpdir_factory
+~~~~~~~~~~~~~~
+
+:ref:`tmpdir and tmpdir_factory`
+
+``tmpdir_factory`` is an instance of :class:`~pytest.TempdirFactory`:
+
+.. autoclass:: pytest.TempdirFactory()
+ :members:
+
+
+.. _`hook-reference`:
+
+Hooks
+-----
+
+:ref:`writing-plugins`.
+
+.. currentmodule:: _pytest.hookspec
+
+Reference to all hooks which can be implemented by :ref:`conftest.py files <localplugin>` and :ref:`plugins <plugins>`.
+
+Bootstrapping hooks
+~~~~~~~~~~~~~~~~~~~
+
+Bootstrapping hooks called for plugins registered early enough (internal and setuptools plugins).
+
+.. 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
+.. autofunction:: pytest_cmdline_main
+
+.. _`initialization-hooks`:
+
+Initialization hooks
+~~~~~~~~~~~~~~~~~~~~
+
+Initialization hooks called for plugins and ``conftest.py`` files.
+
+.. hook:: pytest_addoption
+.. autofunction:: pytest_addoption
+.. hook:: pytest_addhooks
+.. autofunction:: pytest_addhooks
+.. hook:: pytest_configure
+.. autofunction:: pytest_configure
+.. hook:: pytest_unconfigure
+.. autofunction:: pytest_unconfigure
+.. hook:: pytest_sessionstart
+.. autofunction:: pytest_sessionstart
+.. hook:: pytest_sessionfinish
+.. autofunction:: pytest_sessionfinish
+
+.. hook:: pytest_plugin_registered
+.. autofunction:: pytest_plugin_registered
+
+Collection hooks
+~~~~~~~~~~~~~~~~
+
+``pytest`` calls the following hooks for collecting files and directories:
+
+.. hook:: pytest_collection
+.. autofunction:: pytest_collection
+.. hook:: pytest_ignore_collect
+.. autofunction:: pytest_ignore_collect
+.. hook:: pytest_collect_file
+.. autofunction:: pytest_collect_file
+.. hook:: pytest_pycollect_makemodule
+.. autofunction:: pytest_pycollect_makemodule
+
+For influencing the collection of objects in Python modules
+you can use the following hook:
+
+.. hook:: pytest_pycollect_makeitem
+.. autofunction:: pytest_pycollect_makeitem
+.. hook:: pytest_generate_tests
+.. autofunction:: pytest_generate_tests
+.. hook:: pytest_make_parametrize_id
+.. autofunction:: pytest_make_parametrize_id
+
+Hooks for influencing test skipping:
+
+.. hook:: pytest_markeval_namespace
+.. autofunction:: pytest_markeval_namespace
+
+After collection is complete, you can modify the order of
+items, delete or otherwise amend the test items:
+
+.. hook:: pytest_collection_modifyitems
+.. autofunction:: pytest_collection_modifyitems
+
+.. note::
+ If this hook is implemented in ``conftest.py`` files, it always receives all collected items, not only those
+ under the ``conftest.py`` where it is implemented.
+
+.. autofunction:: pytest_collection_finish
+
+Test running (runtest) hooks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+All runtest related hooks receive a :py:class:`pytest.Item <pytest.Item>` object.
+
+.. hook:: pytest_runtestloop
+.. autofunction:: pytest_runtestloop
+.. hook:: pytest_runtest_protocol
+.. autofunction:: pytest_runtest_protocol
+.. hook:: pytest_runtest_logstart
+.. autofunction:: pytest_runtest_logstart
+.. hook:: pytest_runtest_logfinish
+.. autofunction:: pytest_runtest_logfinish
+.. hook:: pytest_runtest_setup
+.. autofunction:: pytest_runtest_setup
+.. hook:: pytest_runtest_call
+.. autofunction:: pytest_runtest_call
+.. hook:: pytest_runtest_teardown
+.. autofunction:: pytest_runtest_teardown
+.. hook:: pytest_runtest_makereport
+.. autofunction:: pytest_runtest_makereport
+
+For deeper understanding you may look at the default implementation of
+these hooks in ``_pytest.runner`` and maybe also
+in ``_pytest.pdb`` which interacts with ``_pytest.capture``
+and its input/output capturing in order to immediately drop
+into interactive debugging when a test failure occurs.
+
+.. hook:: pytest_pyfunc_call
+.. autofunction:: pytest_pyfunc_call
+
+Reporting hooks
+~~~~~~~~~~~~~~~
+
+Session related reporting hooks:
+
+.. hook:: pytest_collectstart
+.. autofunction:: pytest_collectstart
+.. hook:: pytest_make_collect_report
+.. autofunction:: pytest_make_collect_report
+.. hook:: pytest_itemcollected
+.. autofunction:: pytest_itemcollected
+.. hook:: pytest_collectreport
+.. autofunction:: pytest_collectreport
+.. hook:: pytest_deselected
+.. autofunction:: pytest_deselected
+.. hook:: pytest_report_header
+.. autofunction:: pytest_report_header
+.. hook:: pytest_report_collectionfinish
+.. autofunction:: pytest_report_collectionfinish
+.. hook:: pytest_report_teststatus
+.. autofunction:: pytest_report_teststatus
+.. hook:: pytest_report_to_serializable
+.. autofunction:: pytest_report_to_serializable
+.. hook:: pytest_report_from_serializable
+.. autofunction:: pytest_report_from_serializable
+.. hook:: pytest_terminal_summary
+.. autofunction:: pytest_terminal_summary
+.. hook:: pytest_fixture_setup
+.. 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
+
+Central hook for reporting about test execution:
+
+.. hook:: pytest_runtest_logreport
+.. autofunction:: pytest_runtest_logreport
+
+Assertion related hooks:
+
+.. hook:: pytest_assertrepr_compare
+.. autofunction:: pytest_assertrepr_compare
+.. hook:: pytest_assertion_pass
+.. autofunction:: pytest_assertion_pass
+
+
+Debugging/Interaction hooks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are few hooks which can be used for special
+reporting or interaction with exceptions:
+
+.. hook:: pytest_internalerror
+.. autofunction:: pytest_internalerror
+.. hook:: pytest_keyboard_interrupt
+.. autofunction:: pytest_keyboard_interrupt
+.. hook:: pytest_exception_interact
+.. autofunction:: pytest_exception_interact
+.. hook:: pytest_enter_pdb
+.. autofunction:: pytest_enter_pdb
+.. hook:: pytest_leave_pdb
+.. autofunction:: pytest_leave_pdb
+
+
+Objects
+-------
+
+Full reference to objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`.
+
+
+CallInfo
+~~~~~~~~
+
+.. autoclass:: pytest.CallInfo()
+ :members:
+
+
+Class
+~~~~~
+
+.. autoclass:: pytest.Class()
+ :members:
+ :show-inheritance:
+
+Collector
+~~~~~~~~~
+
+.. autoclass:: pytest.Collector()
+ :members:
+ :show-inheritance:
+
+CollectReport
+~~~~~~~~~~~~~
+
+.. autoclass:: pytest.CollectReport()
+ :members:
+ :show-inheritance:
+ :inherited-members:
+
+Config
+~~~~~~
+
+.. autoclass:: pytest.Config()
+ :members:
+
+ExceptionInfo
+~~~~~~~~~~~~~
+
+.. autoclass:: pytest.ExceptionInfo()
+ :members:
+
+
+ExitCode
+~~~~~~~~
+
+.. autoclass:: pytest.ExitCode
+ :members:
+
+File
+~~~~
+
+.. autoclass:: pytest.File()
+ :members:
+ :show-inheritance:
+
+
+FixtureDef
+~~~~~~~~~~
+
+.. autoclass:: _pytest.fixtures.FixtureDef()
+ :members:
+ :show-inheritance:
+
+FSCollector
+~~~~~~~~~~~
+
+.. autoclass:: _pytest.nodes.FSCollector()
+ :members:
+ :show-inheritance:
+
+Function
+~~~~~~~~
+
+.. autoclass:: pytest.Function()
+ :members:
+ :show-inheritance:
+
+FunctionDefinition
+~~~~~~~~~~~~~~~~~~
+
+.. autoclass:: _pytest.python.FunctionDefinition()
+ :members:
+ :show-inheritance:
+
+Item
+~~~~
+
+.. autoclass:: pytest.Item()
+ :members:
+ :show-inheritance:
+
+MarkDecorator
+~~~~~~~~~~~~~
+
+.. autoclass:: pytest.MarkDecorator()
+ :members:
+
+
+MarkGenerator
+~~~~~~~~~~~~~
+
+.. autoclass:: pytest.MarkGenerator()
+ :members:
+
+
+Mark
+~~~~
+
+.. autoclass:: pytest.Mark()
+ :members:
+
+
+Metafunc
+~~~~~~~~
+
+.. autoclass:: pytest.Metafunc()
+ :members:
+
+Module
+~~~~~~
+
+.. autoclass:: pytest.Module()
+ :members:
+ :show-inheritance:
+
+Node
+~~~~
+
+.. autoclass:: _pytest.nodes.Node()
+ :members:
+
+Parser
+~~~~~~
+
+.. autoclass:: pytest.Parser()
+ :members:
+
+OptionGroup
+~~~~~~~~~~~
+
+.. autoclass:: pytest.OptionGroup()
+ :members:
+
+PytestPluginManager
+~~~~~~~~~~~~~~~~~~~
+
+.. autoclass:: pytest.PytestPluginManager()
+ :members:
+ :undoc-members:
+ :inherited-members:
+ :show-inheritance:
+
+Session
+~~~~~~~
+
+.. autoclass:: pytest.Session()
+ :members:
+ :show-inheritance:
+
+TestReport
+~~~~~~~~~~
+
+.. autoclass:: pytest.TestReport()
+ :members:
+ :show-inheritance:
+ :inherited-members:
+
+_Result
+~~~~~~~
+
+Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`_Result in the pluggy documentation <pluggy._callers._Result>` for more information.
+
+Stash
+~~~~~
+
+.. autoclass:: pytest.Stash
+ :special-members: __setitem__, __getitem__, __delitem__, __contains__, __len__
+ :members:
+
+.. autoclass:: pytest.StashKey
+ :show-inheritance:
+ :members:
+
+
+Global Variables
+----------------
+
+pytest treats some global variables in a special manner when defined in a test module or
+``conftest.py`` files.
+
+
+.. globalvar:: collect_ignore
+
+**Tutorial**: :ref:`customizing-test-collection`
+
+Can be declared in *conftest.py files* to exclude test directories or modules.
+Needs to be a list of paths (``str``, :class:`pathlib.Path` or any :class:`os.PathLike`).
+
+.. code-block:: python
+
+ collect_ignore = ["setup.py"]
+
+
+.. globalvar:: collect_ignore_glob
+
+**Tutorial**: :ref:`customizing-test-collection`
+
+Can be declared in *conftest.py files* to exclude test directories or modules
+with Unix shell-style wildcards. Needs to be ``list[str]`` where ``str`` can
+contain glob patterns.
+
+.. code-block:: python
+
+ collect_ignore_glob = ["*_ignore.py"]
+
+
+.. globalvar:: pytest_plugins
+
+**Tutorial**: :ref:`available installable plugins`
+
+Can be declared at the **global** level in *test modules* and *conftest.py files* to register additional plugins.
+Can be either a ``str`` or ``Sequence[str]``.
+
+.. code-block:: python
+
+ pytest_plugins = "myapp.testsupport.myplugin"
+
+.. code-block:: python
+
+ pytest_plugins = ("myapp.testsupport.tools", "myapp.testsupport.regression")
+
+
+.. globalvar:: pytestmark
+
+**Tutorial**: :ref:`scoped-marking`
+
+Can be declared at the **global** level in *test modules* to apply one or more :ref:`marks <marks ref>` to all
+test functions and methods. Can be either a single mark or a list of marks (applied in left-to-right order).
+
+.. code-block:: python
+
+ import pytest
+
+ pytestmark = pytest.mark.webtest
+
+
+.. code-block:: python
+
+ import pytest
+
+ pytestmark = [pytest.mark.integration, pytest.mark.slow]
+
+
+Environment Variables
+---------------------
+
+Environment variables that can be used to change pytest's behavior.
+
+.. 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_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
+processes can inspect it, see :ref:`pytest current test env` for more information.
+
+.. envvar:: PYTEST_DEBUG
+
+When set, pytest will print tracing and debug information.
+
+.. envvar:: PYTEST_DISABLE_PLUGIN_AUTOLOAD
+
+When set, disables plugin auto-loading through setuptools entrypoints. Only explicitly specified plugins will be
+loaded.
+
+.. envvar:: PYTEST_PLUGINS
+
+Contains comma-separated list of modules that should be loaded as plugins:
+
+.. code-block:: bash
+
+ export PYTEST_PLUGINS=mymodule.plugin,xdist
+
+.. envvar:: PYTEST_THEME
+
+Sets a `pygment style <https://pygments.org/docs/styles/>`_ to use for the code output.
+
+.. envvar:: PYTEST_THEME_MODE
+
+Sets the :envvar:`PYTEST_THEME` to be either *dark* or *light*.
+
+.. envvar:: PY_COLORS
+
+When set to ``1``, pytest will use color in terminal output.
+When set to ``0``, pytest will not use color.
+``PY_COLORS`` takes precedence over ``NO_COLOR`` and ``FORCE_COLOR``.
+
+.. envvar:: NO_COLOR
+
+When set (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.
+``PY_COLORS`` and ``NO_COLOR`` take precedence over ``FORCE_COLOR``.
+
+Exceptions
+----------
+
+.. autoclass:: pytest.UsageError()
+ :show-inheritance:
+
+.. _`warnings ref`:
+
+Warnings
+--------
+
+Custom warnings generated in some situations such as improper usage or deprecated features.
+
+.. autoclass:: pytest.PytestWarning
+ :show-inheritance:
+
+.. autoclass:: pytest.PytestAssertRewriteWarning
+ :show-inheritance:
+
+.. autoclass:: pytest.PytestCacheWarning
+ :show-inheritance:
+
+.. autoclass:: pytest.PytestCollectionWarning
+ :show-inheritance:
+
+.. autoclass:: pytest.PytestConfigWarning
+ :show-inheritance:
+
+.. autoclass:: pytest.PytestDeprecationWarning
+ :show-inheritance:
+
+.. autoclass:: pytest.PytestExperimentalApiWarning
+ :show-inheritance:
+
+.. autoclass:: pytest.PytestUnhandledCoroutineWarning
+ :show-inheritance:
+
+.. autoclass:: pytest.PytestUnknownMarkWarning
+ :show-inheritance:
+
+.. autoclass:: pytest.PytestUnraisableExceptionWarning
+ :show-inheritance:
+
+.. autoclass:: pytest.PytestUnhandledThreadExceptionWarning
+ :show-inheritance:
+
+
+Consult the :ref:`internal-warnings` section in the documentation for more information.
+
+
+.. _`ini options ref`:
+
+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`.
+
+.. warning::
+ Usage of ``setup.cfg`` is not recommended except for very simple use cases. ``.cfg``
+ files use a different parser than ``pytest.ini`` and ``tox.ini`` which might cause hard to track
+ down problems.
+ When possible, it is recommended to use the latter files, or ``pyproject.toml``, to hold your pytest configuration.
+
+Configuration options may be overwritten in the command-line by using ``-o/--override-ini``, which can also be
+passed multiple times. The expected format is ``name=value``. For example::
+
+ pytest -o console_output_style=classic -o cache_dir=/tmp/mycache
+
+
+.. confval:: addopts
+
+ Add the specified ``OPTS`` to the set of command line arguments as if they
+ had been specified by the user. Example: if you have this ini file content:
+
+ .. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ addopts = --maxfail=2 -rf # exit after 2 failures, report fail info
+
+ issuing ``pytest test_hello.py`` actually means:
+
+ .. code-block:: bash
+
+ pytest --maxfail=2 -rf test_hello.py
+
+ Default is to add no options.
+
+
+.. confval:: cache_dir
+
+ Sets a directory where stores content of cache plugin. Default directory is
+ ``.pytest_cache`` which is created in :ref:`rootdir <rootdir>`. Directory may be
+ relative or absolute path. If setting relative path, then directory is created
+ relative to :ref:`rootdir <rootdir>`. Additionally path may contain environment
+ variables, that will be expanded. For more information about cache plugin
+ please refer to :ref:`cache_provider`.
+
+.. 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.
+ * ``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
+ the new mode is causing unexpected problems:
+
+ .. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ console_output_style = classic
+
+
+.. confval:: doctest_encoding
+
+
+
+ Default encoding to use to decode text files with docstrings.
+ :ref:`See how pytest handles doctests <doctest>`.
+
+
+.. confval:: doctest_optionflags
+
+ One or more doctest flag names from the standard ``doctest`` module.
+ :ref:`See how pytest handles doctests <doctest>`.
+
+
+.. confval:: empty_parameter_set_mark
+
+
+
+ Allows to pick the action for empty parametersets in parameterization
+
+ * ``skip`` skips tests with an empty parameterset (default)
+ * ``xfail`` marks tests with an empty parameterset as xfail(run=False)
+ * ``fail_at_collect`` raises an exception if parametrize collects an empty parameter set
+
+ .. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ empty_parameter_set_mark = xfail
+
+ .. note::
+
+ The default value of this option is planned to change to ``xfail`` in future releases
+ as this is considered less error prone, see :issue:`3155` for more details.
+
+
+.. confval:: faulthandler_timeout
+
+ Dumps the tracebacks of all threads if a test takes longer than ``X`` seconds to run (including
+ fixture setup and teardown). Implemented using the :func:`faulthandler.dump_traceback_later` function,
+ so all caveats there apply.
+
+ .. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ faulthandler_timeout=5
+
+ For more information please refer to :ref:`faulthandler`.
+
+.. confval:: filterwarnings
+
+
+
+ Sets a list of filters and actions that should be taken for matched
+ warnings. By default all warnings emitted during the test session
+ will be displayed in a summary at the end of the test session.
+
+ .. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ filterwarnings =
+ error
+ ignore::DeprecationWarning
+
+ This tells pytest to ignore deprecation warnings and turn all other warnings
+ into errors. For more information please refer to :ref:`warnings`.
+
+
+.. confval:: junit_duration_report
+
+ .. versionadded:: 4.1
+
+ Configures how durations are recorded into the JUnit XML report:
+
+ * ``total`` (the default): duration times reported include setup, call, and teardown times.
+ * ``call``: duration times reported include only call times, excluding setup and teardown.
+
+ .. code-block:: ini
+
+ [pytest]
+ junit_duration_report = call
+
+
+.. confval:: junit_family
+
+ .. versionadded:: 4.2
+ .. versionchanged:: 6.1
+ Default changed to ``xunit2``.
+
+ Configures the format of the generated JUnit XML file. The possible options are:
+
+ * ``xunit1`` (or ``legacy``): produces old style output, compatible with the xunit 1.0 format.
+ * ``xunit2``: produces `xunit 2.0 style output <https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd>`__, which should be more compatible with latest Jenkins versions. **This is the default**.
+
+ .. code-block:: ini
+
+ [pytest]
+ junit_family = xunit2
+
+
+.. confval:: junit_logging
+
+ .. versionadded:: 3.5
+ .. versionchanged:: 5.4
+ ``log``, ``all``, ``out-err`` options added.
+
+ Configures if captured output should be written to the JUnit XML file. Valid values are:
+
+ * ``log``: write only ``logging`` captured output.
+ * ``system-out``: write captured ``stdout`` contents.
+ * ``system-err``: write captured ``stderr`` contents.
+ * ``out-err``: write both captured ``stdout`` and ``stderr`` contents.
+ * ``all``: write captured ``logging``, ``stdout`` and ``stderr`` contents.
+ * ``no`` (the default): no captured output is written.
+
+ .. code-block:: ini
+
+ [pytest]
+ junit_logging = system-out
+
+
+.. confval:: junit_log_passing_tests
+
+ .. versionadded:: 4.6
+
+ If ``junit_logging != "no"``, configures if the captured output should be written
+ to the JUnit XML file for **passing** tests. Default is ``True``.
+
+ .. code-block:: ini
+
+ [pytest]
+ junit_log_passing_tests = False
+
+
+.. confval:: junit_suite_name
+
+ To set the name of the root test suite xml item, you can configure the ``junit_suite_name`` option in your config file:
+
+ .. code-block:: ini
+
+ [pytest]
+ junit_suite_name = my_suite
+
+.. confval:: log_auto_indent
+
+ Allow selective auto-indentation of multiline log messages.
+
+ Supports command line option ``--log-auto-indent [value]``
+ and config option ``log_auto_indent = [value]`` to set the
+ auto-indentation behavior for all logging.
+
+ ``[value]`` can be:
+ * True or "On" - Dynamically auto-indent multiline log messages
+ * False or "Off" or 0 - Do not auto-indent multiline log messages (the default behavior)
+ * [positive integer] - auto-indent multiline log messages by [value] spaces
+
+ .. code-block:: ini
+
+ [pytest]
+ log_auto_indent = False
+
+ Supports passing kwarg ``extra={"auto_indent": [value]}`` to
+ calls to ``logging.log()`` to specify auto-indentation behavior for
+ a specific entry in the log. ``extra`` kwarg overrides the value specified
+ on the command line or in the config.
+
+.. confval:: log_cli
+
+ Enable log display during test run (also known as :ref:`"live logging" <live_logs>`).
+ The default is ``False``.
+
+ .. code-block:: ini
+
+ [pytest]
+ log_cli = True
+
+.. confval:: log_cli_date_format
+
+
+
+ Sets a :py:func:`time.strftime`-compatible string that will be used when formatting dates for live logging.
+
+ .. code-block:: ini
+
+ [pytest]
+ log_cli_date_format = %Y-%m-%d %H:%M:%S
+
+ For more information, see :ref:`live_logs`.
+
+.. confval:: log_cli_format
+
+
+
+ Sets a :py:mod:`logging`-compatible string used to format live logging messages.
+
+ .. code-block:: ini
+
+ [pytest]
+ log_cli_format = %(asctime)s %(levelname)s %(message)s
+
+ For more information, see :ref:`live_logs`.
+
+
+.. confval:: log_cli_level
+
+
+
+ Sets the minimum log message level that should be captured for live logging. The integer value or
+ the names of the levels can be used.
+
+ .. code-block:: ini
+
+ [pytest]
+ log_cli_level = INFO
+
+ For more information, see :ref:`live_logs`.
+
+
+.. confval:: log_date_format
+
+
+
+ Sets a :py:func:`time.strftime`-compatible string that will be used when formatting dates for logging capture.
+
+ .. code-block:: ini
+
+ [pytest]
+ log_date_format = %Y-%m-%d %H:%M:%S
+
+ For more information, see :ref:`logging`.
+
+
+.. confval:: log_file
+
+
+
+ Sets a file name relative to the ``pytest.ini`` file where log messages should be written to, in addition
+ to the other logging facilities that are active.
+
+ .. code-block:: ini
+
+ [pytest]
+ log_file = logs/pytest-logs.txt
+
+ For more information, see :ref:`logging`.
+
+
+.. confval:: log_file_date_format
+
+
+
+ Sets a :py:func:`time.strftime`-compatible string that will be used when formatting dates for the logging file.
+
+ .. code-block:: ini
+
+ [pytest]
+ log_file_date_format = %Y-%m-%d %H:%M:%S
+
+ For more information, see :ref:`logging`.
+
+.. confval:: log_file_format
+
+
+
+ Sets a :py:mod:`logging`-compatible string used to format logging messages redirected to the logging file.
+
+ .. code-block:: ini
+
+ [pytest]
+ log_file_format = %(asctime)s %(levelname)s %(message)s
+
+ For more information, see :ref:`logging`.
+
+.. confval:: log_file_level
+
+
+
+ Sets the minimum log message level that should be captured for the logging file. The integer value or
+ the names of the levels can be used.
+
+ .. code-block:: ini
+
+ [pytest]
+ log_file_level = INFO
+
+ For more information, see :ref:`logging`.
+
+
+.. confval:: log_format
+
+
+
+ Sets a :py:mod:`logging`-compatible string used to format captured logging messages.
+
+ .. code-block:: ini
+
+ [pytest]
+ log_format = %(asctime)s %(levelname)s %(message)s
+
+ For more information, see :ref:`logging`.
+
+
+.. confval:: log_level
+
+
+
+ Sets the minimum log message level that should be captured for logging capture. The integer value or
+ the names of the levels can be used.
+
+ .. code-block:: ini
+
+ [pytest]
+ log_level = INFO
+
+ For more information, see :ref:`logging`.
+
+
+.. confval:: markers
+
+ When the ``--strict-markers`` or ``--strict`` command-line arguments are used,
+ only known markers - defined in code by core pytest or some plugin - are allowed.
+
+ You can list additional markers in this setting to add them to the whitelist,
+ in which case you probably want to add ``--strict-markers`` to ``addopts``
+ to avoid future regressions:
+
+ .. code-block:: ini
+
+ [pytest]
+ addopts = --strict-markers
+ markers =
+ slow
+ serial
+
+ .. note::
+ The use of ``--strict-markers`` is highly preferred. ``--strict`` was kept for
+ backward compatibility only and may be confusing for others as it only applies to
+ markers and not to other options.
+
+.. confval:: minversion
+
+ Specifies a minimal pytest version required for running tests.
+
+ .. code-block:: ini
+
+ # content of pytest.ini
+ [pytest]
+ minversion = 3.0 # will fail if we run with pytest-2.8
+
+
+.. confval:: norecursedirs
+
+ Set the directory basename patterns to avoid when recursing
+ for test discovery. The individual (fnmatch-style) patterns are
+ applied to the basename of a directory to decide if to recurse into it.
+ Pattern matching characters::
+
+ * matches everything
+ ? matches any single character
+ [seq] matches any character in seq
+ [!seq] matches any char not in seq
+
+ Default patterns are ``'*.egg'``, ``'.*'``, ``'_darcs'``, ``'build'``,
+ ``'CVS'``, ``'dist'``, ``'node_modules'``, ``'venv'``, ``'{arch}'``.
+ Setting a ``norecursedirs`` replaces the default. Here is an example of
+ how to avoid certain directories:
+
+ .. code-block:: ini
+
+ [pytest]
+ norecursedirs = .svn _build tmp*
+
+ This would tell ``pytest`` to not look into typical subversion or
+ sphinx-build directories or into any ``tmp`` prefixed directory.
+
+ 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
+ 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.
+
+
+.. confval:: python_classes
+
+ One or more name prefixes or glob-style patterns determining which classes
+ are considered for test collection. Search for multiple glob patterns by
+ adding a space between patterns. By default, pytest will consider any
+ class prefixed with ``Test`` as a test collection. Here is an example of how
+ to collect tests from classes that end in ``Suite``:
+
+ .. code-block:: ini
+
+ [pytest]
+ python_classes = *Suite
+
+ Note that ``unittest.TestCase`` derived classes are always collected
+ regardless of this option, as ``unittest``'s own collection framework is used
+ to collect those tests.
+
+
+.. confval:: python_files
+
+ One or more Glob-style file patterns determining which python files
+ are considered as test modules. Search for multiple glob patterns by
+ adding a space between patterns:
+
+ .. code-block:: ini
+
+ [pytest]
+ python_files = test_*.py check_*.py example_*.py
+
+ Or one per line:
+
+ .. code-block:: ini
+
+ [pytest]
+ python_files =
+ test_*.py
+ check_*.py
+ example_*.py
+
+ By default, files matching ``test_*.py`` and ``*_test.py`` will be considered
+ test modules.
+
+
+.. confval:: python_functions
+
+ One or more name prefixes or glob-patterns determining which test functions
+ and methods are considered tests. Search for multiple glob patterns by
+ adding a space between patterns. By default, pytest will consider any
+ function prefixed with ``test`` as a test. Here is an example of how
+ to collect test functions and methods that end in ``_test``:
+
+ .. code-block:: ini
+
+ [pytest]
+ python_functions = *_test
+
+ Note that this has no effect on methods that live on a ``unittest.TestCase``
+ derived class, as ``unittest``'s own collection framework is used
+ to collect those tests.
+
+ See :ref:`change naming conventions` for more detailed examples.
+
+
+.. confval:: pythonpath
+
+ Sets list of directories that should be added to the python search path.
+ Directories will be added to the head of :data:`sys.path`.
+ Similar to the :envvar:`PYTHONPATH` environment variable, the directories will be
+ included in where Python will look for imported modules.
+ Paths are relative to the :ref:`rootdir <rootdir>` directory.
+ Directories remain in path for the duration of the test session.
+
+ .. code-block:: ini
+
+ [pytest]
+ pythonpath = src1 src2
+
+
+.. confval:: required_plugins
+
+ A space separated list of plugins that must be present for pytest to run.
+ Plugins can be listed with or without version specifiers directly following
+ their name. Whitespace between different version specifiers is not allowed.
+ If any one of the plugins is not found, emit an error.
+
+ .. code-block:: ini
+
+ [pytest]
+ required_plugins = pytest-django>=3.0.0,<4.0.0 pytest-html pytest-xdist>=1.0.0
+
+
+.. 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.
+ Useful when all project tests are in a known location to speed up
+ test collection and to avoid picking up undesired tests by accident.
+
+ .. code-block:: ini
+
+ [pytest]
+ testpaths = testing doc
+
+ This tells pytest to only look for tests in ``testing`` and ``doc``
+ directories when executing from the root directory.
+
+
+.. confval:: usefixtures
+
+ List of fixtures that will be applied to all test functions; this is semantically the same to apply
+ the ``@pytest.mark.usefixtures`` marker to all test functions.
+
+
+ .. code-block:: ini
+
+ [pytest]
+ usefixtures =
+ clean_db
+
+
+.. confval:: xfail_strict
+
+ If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
+ test suite.
+ For more information, see :ref:`xfail strict tutorial`.
+
+
+ .. code-block:: ini
+
+ [pytest]
+ xfail_strict = True
+
+
+.. _`command-line-flags`:
+
+Command-line Flags
+------------------
+
+All the command-line flags can be obtained by running ``pytest --help``::
+
+ $ pytest --help
+ usage: pytest [options] [file_or_dir] [file_or_dir] [...]
+
+ positional arguments:
+ file_or_dir
+
+ general:
+ -k EXPRESSION only run tests which match the given substring
+ expression. An expression is a python evaluatable
+ 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 contains
+ 'test_method' or 'test_other', while -k 'not
+ test_method' matches those that don't contain
+ 'test_method' in their names. -k 'not test_method
+ and not test_other' will eliminate the matches.
+ Additionally keywords are matched to classes and
+ functions containing extra names in their
+ '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'.
+ --markers show markers (builtin, plugin and per-project ones).
+ -x, --exitfirst exit instantly on first error or failed test.
+ --fixtures, --funcargs
+ 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.
+ --pdbcls=modulename:classname
+ 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
+ not marked
+ --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
+ tests sorted by file mtime
+ --cache-show=[CACHESHOW]
+ show cache contents, don't perform collection or
+ tests. Optional argument: glob (default: '*').
+ --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
+ test next time
+ --sw-skip, --stepwise-skip
+ 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).
+ --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:
+ (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).
+ --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).
+ --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
+
+ 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
+ locate one of the implicit configuration files.
+ --continue-on-collection-errors
+ 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:
+ '/home/user/root_dir'; path with variables:
+ '$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).
+ --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.
+ --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
+ --doctest-report={none,cdiff,ndiff,udiff,only_first_failure}
+ choose another output format for diffs on doctest
+ failure
+ --doctest-glob=pat doctests file matching pattern, default: test*.txt
+ --doctest-ignore-import-errors
+ ignore doctest ImportErrors
+ --doctest-continue-on-failure
+ 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
+ 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.
+ --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'.
+ -o OVERRIDE_INI, --override-ini=OVERRIDE_INI
+ 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.
+
+ 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-format=LOG_FORMAT
+ log format as used by the logging module.
+ --log-date-format=LOG_DATE_FORMAT
+ log date format as used by the logging module.
+ --log-cli-level=LOG_CLI_LEVEL
+ cli logging level.
+ --log-cli-format=LOG_CLI_FORMAT
+ log format as 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-file-level=LOG_FILE_LEVEL
+ log file logging level.
+ --log-file-format=LOG_FILE_FORMAT
+ log format as used by the logging module.
+ --log-file-date-format=LOG_FILE_DATE_FORMAT
+ log date format as 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.
+
+ [pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:
+
+ markers (linelist): 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.
+ 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
+ project
+ python_files (args): glob-style file patterns for Python test module
+ discovery
+ python_classes (args):
+ prefixes or glob names for Python test class
+ discovery
+ python_functions (args):
+ 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
+ cause unwanted side effects(use at your own risk)
+ console_output_style (string):
+ console output: "classic", or with additional
+ progress information ("progress" (percentage) |
+ "count").
+ xfail_strict (bool): default for the strict parameter of xfail markers
+ when not given explicitly (default: False)
+ enable_assertion_pass_hook (bool):
+ Enables the pytest_assertion_pass hook.Make sure to
+ delete any previously generated pyc cache files.
+ junit_suite_name (string):
+ Test suite name for JUnit report
+ junit_logging (string):
+ Write captured log messages to JUnit report: one of
+ no|log|system-out|system-err|out-err|all
+ junit_log_passing_tests (bool):
+ Capture log information for passing tests to JUnit
+ report:
+ junit_duration_report (string):
+ Duration time to report: one of total|call
+ junit_family (string):
+ Emit XML for schema: one of legacy|xunit1|xunit2
+ doctest_optionflags (args):
+ 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
+ log_date_format (string):
+ 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
+ log_cli_format (string):
+ 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
+ log_file_level (string):
+ default value for --log-file-level
+ log_file_format (string):
+ default value for --log-file-format
+ log_file_date_format (string):
+ default value for --log-file-date-format
+ log_auto_indent (string):
+ 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
+ required_plugins (args):
+ 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
+
+
+ to see available markers type: pytest --markers
+ to see available fixtures type: pytest --fixtures
+ (shown according to specified file_or_dir or current dir if not specified; fixtures with leading '_' are only shown with the '-v' option
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/requirements.txt b/testing/web-platform/tests/tools/third_party/pytest/doc/en/requirements.txt
new file mode 100644
index 0000000000..5b49cb7fcc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/requirements.txt
@@ -0,0 +1,7 @@
+pallets-sphinx-themes
+pluggy>=1.0
+pygments-pytest>=2.2.0
+sphinx-removed-in>=0.2.0
+sphinx>=3.1,<4
+sphinxcontrib-trio
+sphinxcontrib-svg2pdfconverter
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/sponsor.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/sponsor.rst
new file mode 100644
index 0000000000..8362a7f0a3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/sponsor.rst
@@ -0,0 +1,26 @@
+Sponsor
+=======
+
+pytest is maintained by a team of volunteers from all around the world in their free time. While
+we work on pytest because we love the project and use it daily at our daily jobs, monetary
+compensation when possible is welcome to justify time away from friends, family and personal time.
+
+Money is also used to fund local sprints, merchandising (stickers to distribute in conferences for example)
+and every few years a large sprint involving all members.
+
+OpenCollective
+--------------
+
+`Open Collective`_ is an online funding platform for open and transparent communities.
+It provide 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.
+
+See more details in the `pytest collective`_.
+
+
+.. _Tidelift: https://tidelift.com
+.. _Tidelift subscription: https://tidelift.com/subscription/pkg/pypi-pytest
+.. _Open Collective: https://opencollective.com
+.. _pytest collective: https://opencollective.com/pytest
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/talks.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/talks.rst
new file mode 100644
index 0000000000..6843c82bab
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/talks.rst
@@ -0,0 +1,109 @@
+
+Talks and Tutorials
+==========================
+
+Books
+---------------------------------------------
+
+- `pytest Quick Start Guide, by Bruno Oliveira (2018)
+ <https://www.packtpub.com/web-development/pytest-quick-start-guide>`_.
+
+- `Python Testing with pytest, by Brian Okken (2017)
+ <https://pragprog.com/book/bopytest/python-testing-with-pytest>`_.
+
+Talks and blog postings
+---------------------------------------------
+
+- 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
+
+- Training: `Introduction to pytest - simple, rapid and fun testing with Python <https://www.youtube.com/watch?v=CMuSn9cofbI>`_, Florian Bruhin, PyConDE 2019
+
+- Abridged metaprogramming classics - this episode: pytest, Oliver Bestwalter, PyConDE 2019 (`repository <https://github.com/obestwalter/abridged-meta-programming-classics>`__, `recording <https://www.youtube.com/watch?v=zHpeMTJsBRk&feature=youtu.be>`__)
+
+- Testing PySide/PyQt code easily using the pytest framework, Florian Bruhin, Qt World Summit 2019 (`slides <https://bruhin.software/talks/qtws19.pdf>`__, `recording <https://www.youtube.com/watch?v=zdsBS5BXGqQ>`__)
+
+- `pytest: recommendations, basic packages for testing in Python and Django, Andreu Vallbona, PyBCN June 2019 <https://www.slideshare.net/AndreuVallbonaPlazas/pybcn-pytest-recomendaciones-paquetes-bsicos-para-testing-en-python-y-django>`_.
+
+- pytest: recommendations, basic packages for testing in Python and Django, Andreu Vallbona, PyconES 2017 (`slides in english <http://talks.apsl.io/testing-pycones-2017/>`_, `video in spanish <https://www.youtube.com/watch?v=K20GeR-lXDk>`_)
+
+- `pytest advanced, Andrew Svetlov (Russian, PyCon Russia, 2016)
+ <https://www.youtube.com/watch?v=7KgihdKTWY4>`_.
+
+- `Pythonic testing, Igor Starikov (Russian, PyNsk, November 2016)
+ <https://www.youtube.com/watch?v=_92nfdd5nK8>`_.
+
+- `pytest - Rapid Simple Testing, Florian Bruhin, Swiss Python Summit 2016
+ <https://www.youtube.com/watch?v=rCBHkQ_LVIs>`_.
+
+- `Improve your testing with Pytest and Mock, Gabe Hollombe, PyCon SG 2015
+ <https://www.youtube.com/watch?v=RcN26hznmk4>`_.
+
+- `Introduction to pytest, Andreas Pelme, EuroPython 2014
+ <https://www.youtube.com/watch?v=LdVJj65ikRY>`_.
+
+- `Advanced Uses of py.test Fixtures, Floris Bruynooghe, EuroPython
+ 2014 <https://www.youtube.com/watch?v=IBC_dxr-4ps>`_.
+
+- `Why i use py.test and maybe you should too, Andy Todd, Pycon AU 2013
+ <https://www.youtube.com/watch?v=P-AhpukDIik>`_
+
+- `3-part blog series about pytest from @pydanny alias Daniel Greenfeld (January
+ 2014) <https://daniel.roygreenfeld.com/pytest-no-boilerplate-testing.html>`_
+
+- `pytest: helps you write better Django apps, Andreas Pelme, DjangoCon
+ Europe 2014 <https://www.youtube.com/watch?v=aaArYVh6XSM>`_.
+
+- `Testing Django Applications with pytest, Andreas Pelme, EuroPython
+ 2013 <https://www.youtube.com/watch?v=aUf8Fkb7TaY>`_.
+
+- `Testes pythonics com py.test, Vinicius Belchior Assef Neto, Plone
+ Conf 2013, Brazil <https://www.youtube.com/watch?v=QUKoq2K7bis>`_.
+
+- `Introduction to py.test fixtures, FOSDEM 2013, Floris Bruynooghe
+ <https://www.youtube.com/watch?v=bJhRW4eZMco>`_.
+
+- `pytest feature and release highlights, Holger Krekel (GERMAN, October 2013)
+ <http://pyvideo.org/video/2429/pytest-feature-and-new-release-highlights>`_
+
+- `pytest introduction from Brian Okken (January 2013)
+ <http://pythontesting.net/framework/pytest-introduction/>`_
+
+- pycon australia 2012 pytest talk from Brianna Laugher (`video <https://www.youtube.com/watch?v=DTNejE9EraI>`_, `slides <https://www.slideshare.net/pfctdayelise/funcargs-other-fun-with-pytest>`_, `code <https://gist.github.com/3386951>`_)
+- `pycon 2012 US talk video from Holger Krekel <https://www.youtube.com/watch?v=9LVqBQcFmyw>`_
+
+- `monkey patching done right`_ (blog post, consult `monkeypatch plugin`_ for up-to-date API)
+
+Test parametrization:
+
+- `generating parametrized tests with fixtures`_.
+- `test generators and cached setup`_
+- `parametrizing tests, generalized`_ (blog post)
+- `putting test-hooks into local or global plugins`_ (blog post)
+
+Assertion introspection:
+
+- `(07/2011) Behind the scenes of pytest's new assertion rewriting
+ <http://pybites.blogspot.com/2011/07/behind-scenes-of-pytests-new-assertion.html>`_
+
+Distributed testing:
+
+- `simultaneously test your code on all platforms`_ (blog entry)
+
+Plugin specific examples:
+
+- `skipping slow tests by default in pytest`_ (blog entry)
+
+- `many examples in the docs for plugins`_
+
+.. _`skipping slow tests by default in pytest`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html
+.. _`many examples in the docs for plugins`: plugins.html
+.. _`monkeypatch plugin`: monkeypatch.html
+.. _`application setup in test functions with fixtures`: fixture.html#interdependent-fixtures
+.. _`simultaneously test your code on all platforms`: https://tetamap.wordpress.com//2009/03/23/new-simultanously-test-your-code-on-all-platforms/
+.. _`monkey patching done right`: https://tetamap.wordpress.com//2009/03/03/monkeypatching-in-unit-tests-done-right/
+.. _`putting test-hooks into local or global plugins`: https://tetamap.wordpress.com/2009/05/14/putting-test-hooks-into-local-and-global-plugins/
+.. _`parametrizing tests, generalized`: https://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/
+.. _`generating parametrized tests with fixtures`: parametrize.html#test-generators
+.. _`test generators and cached setup`: http://bruynooghe.blogspot.com/2010/06/pytest-test-generators-and-cached-setup.html
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/tidelift.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/tidelift.rst
new file mode 100644
index 0000000000..8ce55e97b3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/tidelift.rst
@@ -0,0 +1,45 @@
+pytest for enterprise
+=====================
+
+`Tidelift`_ is working with the maintainers of pytest and thousands of other
+open source projects to deliver commercial support and maintenance for the open source dependencies you use
+to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the
+exact dependencies you use.
+
+`Get more details <https://tidelift.com/subscription/pkg/pypi-pytest?utm_source=pypi-pytest&utm_medium=referral&utm_campaign=enterprise>`_
+
+The Tidelift Subscription is a managed open source subscription for application dependencies covering millions of open source projects across JavaScript, Python, Java, PHP, Ruby, .NET, and more.
+
+Your subscription includes:
+
+* **Security updates**
+
+ - Tidelift's security response team coordinates patches for new breaking security vulnerabilities and alerts immediately through a private channel, so your software supply chain is always secure.
+
+* **Licensing verification and indemnification**
+
+ - Tidelift verifies license information to enable easy policy enforcement and adds intellectual property indemnification to cover creators and users in case something goes wrong. You always have a 100% up-to-date bill of materials for your dependencies to share with your legal team, customers, or partners.
+
+* **Maintenance and code improvement**
+
+ - Tidelift ensures the software you rely on keeps working as long as you need it to work. Your managed dependencies are actively maintained and we recruit additional maintainers where required.
+
+* **Package selection and version guidance**
+
+ - Tidelift helps you choose the best open source packages from the start—and then guide you through updates to stay on the best releases as new issues arise.
+
+* **Roadmap input**
+
+ - Take a seat at the table with the creators behind the software you use. Tidelift's participating maintainers earn more income as their software is used by more subscribers, so they're interested in knowing what you need.
+
+* **Tooling and cloud integration**
+
+ - Tidelift works with GitHub, GitLab, BitBucket, and every cloud platform (and other deployment targets, too).
+
+The end result? All of the capabilities you expect from commercial-grade software, for the full breadth of open
+source you use. That means less time grappling with esoteric open source trivia, and more time building your own
+applications—and your business.
+
+`Request a demo <https://tidelift.com/subscription/request-a-demo?utm_source=pypi-pytest&utm_medium=referral&utm_campaign=enterprise>`_
+
+.. _Tidelift: https://tidelift.com
diff --git a/testing/web-platform/tests/tools/third_party/pytest/doc/en/yieldfixture.rst b/testing/web-platform/tests/tools/third_party/pytest/doc/en/yieldfixture.rst
new file mode 100644
index 0000000000..47590f9db9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/doc/en/yieldfixture.rst
@@ -0,0 +1,18 @@
+:orphan:
+
+.. _yieldfixture:
+
+"yield_fixture" functions
+---------------------------------------------------------------
+
+
+
+
+
+.. important::
+ Since pytest-3.0, fixtures using the normal ``fixture`` decorator can use a ``yield``
+ statement to provide fixture values and execute teardown code, exactly like ``yield_fixture``
+ in previous versions.
+
+ Marking functions as ``yield_fixture`` is still supported, but deprecated and should not
+ be used in new code.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/extra/get_issues.py b/testing/web-platform/tests/tools/third_party/pytest/extra/get_issues.py
new file mode 100644
index 0000000000..4aaa3c3ec3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/extra/get_issues.py
@@ -0,0 +1,85 @@
+import json
+from pathlib import Path
+
+import requests
+
+issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"
+
+
+def get_issues():
+ issues = []
+ url = issues_url
+ while 1:
+ get_data = {"state": "all"}
+ r = requests.get(url, params=get_data)
+ data = r.json()
+ if r.status_code == 403:
+ # API request limit exceeded
+ print(data["message"])
+ exit(1)
+ issues.extend(data)
+
+ # Look for next page
+ links = requests.utils.parse_header_links(r.headers["Link"])
+ another_page = False
+ for link in links:
+ if link["rel"] == "next":
+ url = link["url"]
+ another_page = True
+ if not another_page:
+ return issues
+
+
+def main(args):
+ cachefile = Path(args.cache)
+ if not cachefile.exists() or args.refresh:
+ issues = get_issues()
+ cachefile.write_text(json.dumps(issues), "utf-8")
+ else:
+ issues = json.loads(cachefile.read_text("utf-8"))
+
+ open_issues = [x for x in issues if x["state"] == "open"]
+
+ open_issues.sort(key=lambda x: x["number"])
+ report(open_issues)
+
+
+def _get_kind(issue):
+ labels = [label["name"] for label in issue["labels"]]
+ for key in ("bug", "enhancement", "proposal"):
+ if key in labels:
+ return key
+ return "issue"
+
+
+def report(issues):
+ for issue in issues:
+ title = issue["title"]
+ # body = issue["body"]
+ kind = _get_kind(issue)
+ status = issue["state"]
+ number = issue["number"]
+ link = "https://github.com/pytest-dev/pytest/issues/%s/" % number
+ print("----")
+ print(status, kind, link)
+ print(title)
+ # print()
+ # lines = body.split("\n")
+ # print("\n".join(lines[:3]))
+ # if len(lines) > 3 or len(body) > 240:
+ # print("...")
+ print("\n\nFound %s open issues" % len(issues))
+
+
+if __name__ == "__main__":
+ import argparse
+
+ parser = argparse.ArgumentParser("process bitbucket issues")
+ parser.add_argument(
+ "--refresh", action="store_true", help="invalidate cache, refresh issues"
+ )
+ parser.add_argument(
+ "--cache", action="store", default="issues.json", help="cache file"
+ )
+ args = parser.parse_args()
+ main(args)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/extra/setup-py.test/setup.py b/testing/web-platform/tests/tools/third_party/pytest/extra/setup-py.test/setup.py
new file mode 100644
index 0000000000..d0560ce1f5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/extra/setup-py.test/setup.py
@@ -0,0 +1,11 @@
+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/testing/web-platform/tests/tools/third_party/pytest/pyproject.toml b/testing/web-platform/tests/tools/third_party/pytest/pyproject.toml
new file mode 100644
index 0000000000..5d32b755c7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/pyproject.toml
@@ -0,0 +1,116 @@
+[build-system]
+requires = [
+ # sync with setup.py until we discard non-pep-517/518
+ "setuptools>=45.0",
+ "setuptools-scm[toml]>=6.2.3",
+ "wheel",
+]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
+write_to = "src/_pytest/_version.py"
+
+[tool.pytest.ini_options]
+minversion = "2.0"
+addopts = "-rfEX -p pytester --strict-markers"
+python_files = ["test_*.py", "*_test.py", "testing/python/*.py"]
+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"]
+xfail_strict = true
+filterwarnings = [
+ "error",
+ "default:Using or importing the ABCs:DeprecationWarning:unittest2.*",
+ # produced by older pyparsing<=2.2.0.
+ "default:Using or importing the ABCs:DeprecationWarning:pyparsing.*",
+ "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)
+ "ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning",
+ # pytest's own futurewarnings
+ "ignore::pytest.PytestExperimentalApiWarning",
+ # Do not cause SyntaxError for invalid escape sequences in py37.
+ # 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 use of unregistered marks, because we use many to test the implementation
+ "ignore::_pytest.warning_types.PytestUnknownMarkWarning",
+ # https://github.com/benjaminp/six/issues/341
+ "ignore:_SixMetaPathImporter\\.exec_module\\(\\) not found; falling back to load_module\\(\\):ImportWarning",
+ # https://github.com/benjaminp/six/pull/352
+ "ignore:_SixMetaPathImporter\\.find_spec\\(\\) not found; falling back to find_module\\(\\):ImportWarning",
+ # https://github.com/pypa/setuptools/pull/2517
+ "ignore:VendorImporter\\.find_spec\\(\\) not found; falling back to find_module\\(\\):ImportWarning",
+ # https://github.com/pytest-dev/execnet/pull/127
+ "ignore:isSet\\(\\) is deprecated, use is_set\\(\\) instead:DeprecationWarning",
+]
+pytester_example_dir = "testing/example_scripts"
+markers = [
+ # dummy markers for testing
+ "foo",
+ "bar",
+ "baz",
+ # conftest.py reorders tests moving slow ones to the end of the list
+ "slow",
+ # experimental mark for all tests using pexpect
+ "uses_pexpect",
+]
+
+
+[tool.towncrier]
+package = "pytest"
+package_dir = "src"
+filename = "doc/en/changelog.rst"
+directory = "changelog/"
+title_format = "pytest {version} ({project_date})"
+template = "changelog/_template.rst"
+
+ [[tool.towncrier.type]]
+ directory = "breaking"
+ name = "Breaking Changes"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "deprecation"
+ name = "Deprecations"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "feature"
+ name = "Features"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "improvement"
+ name = "Improvements"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "bugfix"
+ name = "Bug Fixes"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "vendor"
+ name = "Vendored Libraries"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "doc"
+ name = "Improved Documentation"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "trivial"
+ name = "Trivial/Internal Changes"
+ showcontent = true
+
+[tool.black]
+target-version = ['py36']
diff --git a/testing/web-platform/tests/tools/third_party/pytest/scripts/prepare-release-pr.py b/testing/web-platform/tests/tools/third_party/pytest/scripts/prepare-release-pr.py
new file mode 100644
index 0000000000..7a80de7eda
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/scripts/prepare-release-pr.py
@@ -0,0 +1,174 @@
+"""
+This script is part of the pytest release process which is triggered manually in the Actions
+tab of the repository.
+
+The user will need to enter the base branch to start the release from (for example
+``6.1.x`` or ``main``) and if it should be a major release.
+
+The appropriate version will be obtained based on the given branch automatically.
+
+After that, it will create a release using the `release` tox environment, and push a new PR.
+
+**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
+from subprocess import check_call
+from subprocess import check_output
+from subprocess import run
+
+from colorama import Fore
+from colorama import init
+from github3.repos import Repository
+
+
+class InvalidFeatureRelease(Exception):
+ pass
+
+
+SLUG = "pytest-dev/pytest"
+
+PR_BODY = """\
+Created automatically from manual trigger.
+
+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.
+"""
+
+
+def login(token: str) -> Repository:
+ import github3
+
+ github = github3.login(token=token)
+ owner, repo = SLUG.split("/")
+ return github.repository(owner, repo)
+
+
+def prepare_release_pr(
+ base_branch: str, is_major: bool, token: str, prerelease: str
+) -> None:
+ print()
+ print(f"Processing release for branch {Fore.CYAN}{base_branch}")
+
+ check_call(["git", "checkout", f"origin/{base_branch}"])
+
+ changelog = Path("changelog")
+
+ features = list(changelog.glob("*.feature.rst"))
+ breaking = list(changelog.glob("*.breaking.rst"))
+ is_feature_release = bool(features or breaking)
+
+ try:
+ version = find_next_version(
+ base_branch, is_major, is_feature_release, prerelease
+ )
+ except InvalidFeatureRelease as e:
+ print(f"{Fore.RED}{e}")
+ raise SystemExit(1)
+
+ print(f"Version: {Fore.CYAN}{version}")
+
+ release_branch = f"release-{version}"
+
+ run(
+ ["git", "config", "user.name", "pytest bot"],
+ check=True,
+ )
+ run(
+ ["git", "config", "user.email", "pytestbot@gmail.com"],
+ check=True,
+ )
+
+ run(
+ ["git", "checkout", "-b", release_branch, f"origin/{base_branch}"],
+ check=True,
+ )
+
+ print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.")
+
+ if is_major:
+ template_name = "release.major.rst"
+ elif prerelease:
+ template_name = "release.pre.rst"
+ elif is_feature_release:
+ template_name = "release.minor.rst"
+ else:
+ template_name = "release.patch.rst"
+
+ # important to use tox here because we have changed branches, so dependencies
+ # might have changed as well
+ cmdline = [
+ "tox",
+ "-e",
+ "release",
+ "--",
+ version,
+ template_name,
+ release_branch, # doc_version
+ "--skip-check-links",
+ ]
+ print("Running", " ".join(cmdline))
+ run(
+ cmdline,
+ check=True,
+ )
+
+ oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git"
+ run(
+ ["git", "push", oauth_url, f"HEAD:{release_branch}", "--force"],
+ check=True,
+ )
+ print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} pushed.")
+
+ body = PR_BODY.format(version=version)
+ repo = login(token)
+ pr = repo.create_pull(
+ f"Prepare release {version}",
+ base=base_branch,
+ head=release_branch,
+ body=body,
+ )
+ print(f"Pull request {Fore.CYAN}{pr.url}{Fore.RESET} created.")
+
+
+def find_next_version(
+ base_branch: str, is_major: bool, is_feature_release: bool, prerelease: str
+) -> str:
+ output = check_output(["git", "tag"], encoding="UTF-8")
+ valid_versions = []
+ for v in output.splitlines():
+ m = re.match(r"\d.\d.\d+$", v.strip())
+ if m:
+ valid_versions.append(tuple(int(x) for x in v.split(".")))
+
+ valid_versions.sort()
+ last_version = valid_versions[-1]
+
+ if is_major:
+ return f"{last_version[0]+1}.0.0{prerelease}"
+ elif is_feature_release:
+ return f"{last_version[0]}.{last_version[1] + 1}.0{prerelease}"
+ else:
+ return f"{last_version[0]}.{last_version[1]}.{last_version[2] + 1}{prerelease}"
+
+
+def main() -> None:
+ init(autoreset=True)
+ parser = argparse.ArgumentParser()
+ parser.add_argument("base_branch")
+ parser.add_argument("token")
+ parser.add_argument("--major", action="store_true", default=False)
+ parser.add_argument("--prerelease", default="")
+ options = parser.parse_args()
+ prepare_release_pr(
+ base_branch=options.base_branch,
+ is_major=options.major,
+ token=options.token,
+ prerelease=options.prerelease,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/scripts/publish-gh-release-notes.py b/testing/web-platform/tests/tools/third_party/pytest/scripts/publish-gh-release-notes.py
new file mode 100644
index 0000000000..68cbd7adff
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/scripts/publish-gh-release-notes.py
@@ -0,0 +1,102 @@
+"""
+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/testing/web-platform/tests/tools/third_party/pytest/scripts/release.major.rst b/testing/web-platform/tests/tools/third_party/pytest/scripts/release.major.rst
new file mode 100644
index 0000000000..76e447f0c6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/scripts/release.major.rst
@@ -0,0 +1,24 @@
+pytest-{version}
+=======================================
+
+The pytest team is proud to announce the {version} 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:
+
+{contributors}
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/scripts/release.minor.rst b/testing/web-platform/tests/tools/third_party/pytest/scripts/release.minor.rst
new file mode 100644
index 0000000000..9a06d3d414
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/scripts/release.minor.rst
@@ -0,0 +1,24 @@
+pytest-{version}
+=======================================
+
+The pytest team is proud to announce the {version} 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:
+
+{contributors}
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/scripts/release.patch.rst b/testing/web-platform/tests/tools/third_party/pytest/scripts/release.patch.rst
new file mode 100644
index 0000000000..59fbe50ce0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/scripts/release.patch.rst
@@ -0,0 +1,17 @@
+pytest-{version}
+=======================================
+
+pytest {version} 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:
+
+{contributors}
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/scripts/release.pre.rst b/testing/web-platform/tests/tools/third_party/pytest/scripts/release.pre.rst
new file mode 100644
index 0000000000..960fae7e4f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/scripts/release.pre.rst
@@ -0,0 +1,29 @@
+pytest-{version}
+=======================================
+
+The pytest team is proud to announce the {version} 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=={version}
+
+Users are encouraged to take a look at the CHANGELOG carefully:
+
+ https://docs.pytest.org/en/{doc_version}/changelog.html
+
+Thanks to all the contributors to this release:
+
+{contributors}
+
+Happy testing,
+The pytest Development Team
diff --git a/testing/web-platform/tests/tools/third_party/pytest/scripts/release.py b/testing/web-platform/tests/tools/third_party/pytest/scripts/release.py
new file mode 100644
index 0000000000..19fef42842
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/scripts/release.py
@@ -0,0 +1,131 @@
+"""Invoke development tasks."""
+import argparse
+import os
+from pathlib import Path
+from subprocess import call
+from subprocess import check_call
+from subprocess import check_output
+
+from colorama import Fore
+from colorama import init
+
+
+def announce(version, template_name, doc_version):
+ """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")
+ last_version = stdout.strip()
+
+ stdout = check_output(["git", "log", f"{last_version}..HEAD", "--format=%aN"])
+ stdout = stdout.decode("utf-8")
+
+ contributors = {
+ name
+ for name in stdout.splitlines()
+ if not name.endswith("[bot]") and name != "pytest bot"
+ }
+
+ template_text = (
+ Path(__file__).parent.joinpath(template_name).read_text(encoding="UTF-8")
+ )
+
+ contributors_text = "\n".join(f"* {name}" for name in sorted(contributors)) + "\n"
+ text = template_text.format(
+ version=version, contributors=contributors_text, doc_version=doc_version
+ )
+
+ target = Path(__file__).parent.joinpath(f"../doc/en/announce/release-{version}.rst")
+ target.write_text(text, encoding="UTF-8")
+ print(f"{Fore.CYAN}[generate.announce] {Fore.RESET}Generated {target.name}")
+
+ # Update index with the new release entry
+ index_path = Path(__file__).parent.joinpath("../doc/en/announce/index.rst")
+ lines = index_path.read_text(encoding="UTF-8").splitlines()
+ indent = " "
+ for index, line in enumerate(lines):
+ if line.startswith(f"{indent}release-"):
+ new_line = indent + target.stem
+ if line != new_line:
+ lines.insert(index, new_line)
+ index_path.write_text("\n".join(lines) + "\n", encoding="UTF-8")
+ print(
+ f"{Fore.CYAN}[generate.announce] {Fore.RESET}Updated {index_path.name}"
+ )
+ else:
+ print(
+ f"{Fore.CYAN}[generate.announce] {Fore.RESET}Skip {index_path.name} (already contains release)"
+ )
+ break
+
+ check_call(["git", "add", str(target)])
+
+
+def regen(version):
+ """Call regendoc tool to update examples and pytest output in the docs."""
+ print(f"{Fore.CYAN}[generate.regen] {Fore.RESET}Updating docs")
+ check_call(
+ ["tox", "-e", "regen"],
+ env={**os.environ, "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST": version},
+ )
+
+
+def fix_formatting():
+ """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"
+ )
+ call(["pre-commit", "run", "--all-files"])
+
+
+def check_links():
+ """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):
+ """Generates new docs, release announcements and creates a local tag."""
+ announce(version, template_name, doc_version)
+ regen(version)
+ changelog(version, write_out=True)
+ fix_formatting()
+ if not skip_check_links:
+ check_links()
+
+ msg = f"Prepare release version {version}"
+ check_call(["git", "commit", "-a", "-m", msg])
+
+ print()
+ print(f"{Fore.CYAN}[generate.pre_release] {Fore.GREEN}All done!")
+ print()
+ print("Please push your branch and open a PR.")
+
+
+def changelog(version, write_out=False):
+ addopts = [] if write_out else ["--draft"]
+ check_call(["towncrier", "--yes", "--version", version] + addopts)
+
+
+def main():
+ init(autoreset=True)
+ parser = argparse.ArgumentParser()
+ parser.add_argument("version", help="Release version")
+ parser.add_argument(
+ "template_name", help="Name of template file to use for release announcement"
+ )
+ parser.add_argument(
+ "doc_version", help="For prereleases, the version to link to in the docs"
+ )
+ parser.add_argument("--skip-check-links", action="store_true", default=False)
+ options = parser.parse_args()
+ pre_release(
+ options.version,
+ options.template_name,
+ options.doc_version,
+ skip_check_links=options.skip_check_links,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/scripts/towncrier-draft-to-file.py b/testing/web-platform/tests/tools/third_party/pytest/scripts/towncrier-draft-to-file.py
new file mode 100644
index 0000000000..81507b40b7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/scripts/towncrier-draft-to-file.py
@@ -0,0 +1,15 @@
+import sys
+from subprocess import call
+
+
+def main():
+ """
+ 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:
+ return call(("towncrier", "--draft"), stdout=draft_file)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/testing/web-platform/tests/tools/third_party/pytest/scripts/update-plugin-list.py b/testing/web-platform/tests/tools/third_party/pytest/scripts/update-plugin-list.py
new file mode 100644
index 0000000000..c034c72420
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/scripts/update-plugin-list.py
@@ -0,0 +1,140 @@
+import datetime
+import pathlib
+import re
+from textwrap import dedent
+from textwrap import indent
+
+import packaging.version
+import requests
+import tabulate
+import wcwidth
+from tqdm import tqdm
+
+FILE_HEAD = r"""
+.. _plugin-list:
+
+Plugin List
+===========
+
+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.
+
+"""
+DEVELOPMENT_STATUS_CLASSIFIERS = (
+ "Development Status :: 1 - Planning",
+ "Development Status :: 2 - Pre-Alpha",
+ "Development Status :: 3 - Alpha",
+ "Development Status :: 4 - Beta",
+ "Development Status :: 5 - Production/Stable",
+ "Development Status :: 6 - Mature",
+ "Development Status :: 7 - Inactive",
+)
+
+
+def escape_rst(text: str) -> str:
+ """Rudimentary attempt to escape special RST characters to appear as
+ plain text."""
+ text = (
+ text.replace("*", "\\*")
+ .replace("<", "\\<")
+ .replace(">", "\\>")
+ .replace("`", "\\`")
+ )
+ text = re.sub(r"_\b", "", text)
+ return text
+
+
+def iter_plugins():
+ regex = r">([\d\w-]*)</a>"
+ response = requests.get("https://pypi.org/simple")
+
+ matches = list(
+ match
+ for match in re.finditer(regex, response.text)
+ if match.groups()[0].startswith("pytest-")
+ )
+
+ for match in tqdm(matches, smoothing=0):
+ name = match.groups()[0]
+ response = requests.get(f"https://pypi.org/pypi/{name}/json")
+ 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.
+ continue
+ response.raise_for_status()
+ info = response.json()["info"]
+ if "Development Status :: 7 - Inactive" in info["classifiers"]:
+ continue
+ for classifier in DEVELOPMENT_STATUS_CLASSIFIERS:
+ if classifier in info["classifiers"]:
+ status = classifier[22:]
+ break
+ else:
+ status = "N/A"
+ requires = "N/A"
+ if info["requires_dist"]:
+ for requirement in info["requires_dist"]:
+ if requirement == "pytest" or "pytest " in requirement:
+ requires = requirement
+ break
+ releases = response.json()["releases"]
+ for release in sorted(releases, key=packaging.version.parse, reverse=True):
+ if releases[release]:
+ release_date = datetime.date.fromisoformat(
+ releases[release][-1]["upload_time_iso_8601"].split("T")[0]
+ )
+ last_release = release_date.strftime("%b %d, %Y")
+ break
+ name = f':pypi:`{info["name"]}`'
+ summary = escape_rst(info["summary"].replace("\n", ""))
+ yield {
+ "name": name,
+ "summary": summary.strip(),
+ "last release": last_release,
+ "status": status,
+ "requires": requires,
+ }
+
+
+def plugin_definitions(plugins):
+ """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"]},
+ *status*: {plugin["status"]},
+ *requires*: {plugin["requires"]}
+
+ {plugin["summary"]}
+ """
+ )
+
+
+def main():
+ plugins = list(iter_plugins())
+
+ reference_dir = pathlib.Path("doc", "en", "reference")
+
+ plugin_list = reference_dir / "plugin_list.rst"
+ with plugin_list.open("w") 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
+ plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
+ f.write(indent(plugin_table, " "))
+ f.write("\n\n")
+
+ f.write(".. only:: latex\n\n")
+ f.write(indent("".join(plugin_definitions(plugins)), " "))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/setup.cfg b/testing/web-platform/tests/tools/third_party/pytest/setup.cfg
new file mode 100644
index 0000000000..26a5d2e63e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/setup.cfg
@@ -0,0 +1,105 @@
+[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/testing/web-platform/tests/tools/third_party/pytest/setup.py b/testing/web-platform/tests/tools/third_party/pytest/setup.py
new file mode 100644
index 0000000000..7f1a1763ca
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/setup.py
@@ -0,0 +1,4 @@
+from setuptools import setup
+
+if __name__ == "__main__":
+ setup()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/__init__.py
new file mode 100644
index 0000000000..8a406c5c75
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/__init__.py
@@ -0,0 +1,9 @@
+__all__ = ["__version__", "version_tuple"]
+
+try:
+ from ._version import version as __version__, 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]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_argcomplete.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_argcomplete.py
new file mode 100644
index 0000000000..41d9d9407c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_argcomplete.py
@@ -0,0 +1,117 @@
+"""Allow bash-completion for argparse with argcomplete if installed.
+
+Needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
+to find the magic string, so _ARGCOMPLETE env. var is never set, and
+this does not need special code).
+
+Function try_argcomplete(parser) should be called directly before
+the call to ArgumentParser.parse_args().
+
+The filescompleter is what you normally would use on the positional
+arguments specification, in order to get "dirname/" after "dirn<TAB>"
+instead of the default "dirname ":
+
+ optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter
+
+Other, application specific, completers should go in the file
+doing the add_argument calls as they need to be specified as .completer
+attributes as well. (If argcomplete is not installed, the function the
+attribute points to will not be used).
+
+SPEEDUP
+=======
+
+The generic argcomplete script for bash-completion
+(/etc/bash_completion.d/python-argcomplete.sh)
+uses a python program to determine startup script generated by pip.
+You can speed up completion somewhat by changing this script to include
+ # PYTHON_ARGCOMPLETE_OK
+so the python-argcomplete-check-easy-install-script does not
+need to be called to find the entry point of the code and see if that is
+marked with PYTHON_ARGCOMPLETE_OK.
+
+INSTALL/DEBUGGING
+=================
+
+To include this support in another application that has setup.py generated
+scripts:
+
+- Add the line:
+ # PYTHON_ARGCOMPLETE_OK
+ near the top of the main python entry point.
+
+- Include in the file calling parse_args():
+ from _argcomplete import try_argcomplete, filescompleter
+ Call try_argcomplete just before parse_args(), and optionally add
+ filescompleter to the positional arguments' add_argument().
+
+If things do not work right away:
+
+- Switch on argcomplete debugging with (also helpful when doing custom
+ completers):
+ export _ARC_DEBUG=1
+
+- Run:
+ python-argcomplete-check-easy-install-script $(which appname)
+ echo $?
+ will echo 0 if the magic line has been found, 1 if not.
+
+- Sometimes it helps to find early on errors using:
+ _ARGCOMPLETE=1 _ARC_DEBUG=1 appname
+ which should throw a KeyError: 'COMPLINE' (which is properly set by the
+ global argcomplete script).
+"""
+import argparse
+import os
+import sys
+from glob import glob
+from typing import Any
+from typing import List
+from typing import Optional
+
+
+class FastFilesCompleter:
+ """Fast file completer class."""
+
+ def __init__(self, directories: bool = True) -> None:
+ self.directories = directories
+
+ 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)
+ 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:
+ globbed.extend(glob(prefix + ".*"))
+ prefix += "*"
+ globbed.extend(glob(prefix))
+ for x in sorted(globbed):
+ if os.path.isdir(x):
+ x += "/"
+ # Append stripping the prefix (like bash, not like compgen).
+ completion.append(x[prefix_dir:])
+ return completion
+
+
+if os.environ.get("_ARGCOMPLETE"):
+ try:
+ import argcomplete.completers
+ except ImportError:
+ sys.exit(-1)
+ filescompleter: Optional[FastFilesCompleter] = FastFilesCompleter()
+
+ def try_argcomplete(parser: argparse.ArgumentParser) -> None:
+ argcomplete.autocomplete(parser, always_complete_options=False)
+
+
+else:
+
+ def try_argcomplete(parser: argparse.ArgumentParser) -> None:
+ pass
+
+ filescompleter = None
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_code/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_code/__init__.py
new file mode 100644
index 0000000000..511d0dde66
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_code/__init__.py
@@ -0,0 +1,22 @@
+"""Python inspection/code generation API."""
+from .code import Code
+from .code import ExceptionInfo
+from .code import filter_traceback
+from .code import Frame
+from .code import getfslineno
+from .code import Traceback
+from .code import TracebackEntry
+from .source import getrawcode
+from .source import Source
+
+__all__ = [
+ "Code",
+ "ExceptionInfo",
+ "filter_traceback",
+ "Frame",
+ "getfslineno",
+ "getrawcode",
+ "Traceback",
+ "TracebackEntry",
+ "Source",
+]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_code/code.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_code/code.py
new file mode 100644
index 0000000000..5b758a8848
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_code/code.py
@@ -0,0 +1,1274 @@
+import ast
+import inspect
+import os
+import re
+import sys
+import traceback
+from inspect import CO_VARARGS
+from inspect import CO_VARKEYWORDS
+from io import StringIO
+from pathlib import Path
+from traceback import format_exception_only
+from types import CodeType
+from types import FrameType
+from types import TracebackType
+from typing import Any
+from typing import Callable
+from typing import ClassVar
+from typing import Dict
+from typing import Generic
+from typing import Iterable
+from typing import List
+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 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
+from _pytest._code.source import findsource
+from _pytest._code.source import getrawcode
+from _pytest._code.source import getstatementrange_ast
+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"]
+
+
+class Code:
+ """Wrapper around Python code objects."""
+
+ __slots__ = ("raw",)
+
+ def __init__(self, obj: CodeType) -> None:
+ self.raw = obj
+
+ @classmethod
+ def from_function(cls, obj: object) -> "Code":
+ return cls(getrawcode(obj))
+
+ def __eq__(self, other):
+ return self.raw == other.raw
+
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
+ __hash__ = None # type: ignore
+
+ @property
+ def firstlineno(self) -> int:
+ return self.raw.co_firstlineno - 1
+
+ @property
+ def name(self) -> str:
+ return self.raw.co_name
+
+ @property
+ def path(self) -> Union[Path, str]:
+ """Return a path object pointing to source code, or an ``str`` in
+ case of ``OSError`` / non-existing file."""
+ if not self.raw.co_filename:
+ return ""
+ try:
+ p = absolutepath(self.raw.co_filename)
+ # maybe don't try this checking
+ if not p.exists():
+ raise OSError("path check failed.")
+ return p
+ except OSError:
+ # XXX maybe try harder like the weird logic
+ # in the standard lib [linecache.updatecache] does?
+ return self.raw.co_filename
+
+ @property
+ def fullsource(self) -> Optional["Source"]:
+ """Return a _pytest._code.Source object for the full source file of the code."""
+ full, _ = findsource(self.raw)
+ return full
+
+ def source(self) -> "Source":
+ """Return a _pytest._code.Source object for the code object's source only."""
+ # return source only for that part of code
+ return Source(self.raw)
+
+ def getargs(self, var: bool = False) -> Tuple[str, ...]:
+ """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.
+ """
+ # Handy 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:
+ """Wrapper around a Python frame holding f_locals and f_globals
+ in which expressions can be evaluated."""
+
+ __slots__ = ("raw",)
+
+ def __init__(self, frame: FrameType) -> None:
+ self.raw = frame
+
+ @property
+ def lineno(self) -> int:
+ return self.raw.f_lineno - 1
+
+ @property
+ def f_globals(self) -> Dict[str, Any]:
+ return self.raw.f_globals
+
+ @property
+ def f_locals(self) -> Dict[str, Any]:
+ return self.raw.f_locals
+
+ @property
+ def code(self) -> Code:
+ return Code(self.raw.f_code)
+
+ @property
+ def statement(self) -> "Source":
+ """Statement this frame is at."""
+ if self.code.fullsource is None:
+ return 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 repr(self, object: object) -> str:
+ """Return a 'safe' (non-recursive, one-line) string repr for 'object'."""
+ return saferepr(object)
+
+ def getargs(self, var: bool = 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:
+ """A single entry in a Traceback."""
+
+ __slots__ = ("_rawentry", "_excinfo", "_repr_style")
+
+ def __init__(
+ self,
+ rawentry: TracebackType,
+ excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
+ ) -> None:
+ self._rawentry = rawentry
+ self._excinfo = excinfo
+ self._repr_style: Optional['Literal["short", "long"]'] = None
+
+ @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)
+
+ @property
+ def relline(self) -> int:
+ return self.lineno - self.frame.code.firstlineno
+
+ def __repr__(self) -> str:
+ return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)
+
+ @property
+ def statement(self) -> "Source":
+ """_pytest._code.Source object for the current statement."""
+ source = self.frame.code.fullsource
+ assert source is not None
+ return source.getstatement(self.lineno)
+
+ @property
+ def path(self) -> Union[Path, str]:
+ """Path to the source code."""
+ return self.frame.code.path
+
+ @property
+ def locals(self) -> Dict[str, Any]:
+ """Locals of underlying frame."""
+ return self.frame.f_locals
+
+ def getfirstlinesource(self) -> int:
+ return self.frame.code.firstlineno
+
+ def getsource(
+ self, astcache: Optional[Dict[Union[str, Path], ast.AST]] = None
+ ) -> Optional["Source"]:
+ """Return failing source code."""
+ # we use the passed in astcache to not reparse asttrees
+ # within exception info printing
+ 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 and astcache is not None:
+ astcache[key] = astnode
+ return source[start:end]
+
+ source = property(getsource)
+
+ def ishidden(self) -> bool:
+ """Return True if the current frame has a var __tracebackhide__
+ resolving to True.
+
+ If __tracebackhide__ is a callable, it gets called with the
+ ExceptionInfo instance and can decide whether to hide the traceback.
+
+ Mostly for internal use.
+ """
+ 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
+ # (even incorrect types!).
+ # as such, we suppress all exceptions while accessing __tracebackhide__
+ try:
+ tbh = maybe_ns_dct["__tracebackhide__"]
+ except Exception:
+ pass
+ else:
+ break
+ if tbh and callable(tbh):
+ return tbh(None if self._excinfo is None else self._excinfo())
+ return tbh
+
+ def __str__(self) -> str:
+ name = self.frame.code.name
+ try:
+ line = str(self.statement).lstrip()
+ except KeyboardInterrupt:
+ raise
+ except BaseException:
+ line = "???"
+ # This output does not quite match Python's repr for traceback entries,
+ # but changing it to do so would break certain plugins. See
+ # https://github.com/pytest-dev/pytest/pull/7535/ for details.
+ return " File %r:%d in %s\n %s\n" % (
+ str(self.path),
+ self.lineno + 1,
+ name,
+ line,
+ )
+
+ @property
+ def name(self) -> str:
+ """co_name of underlying code."""
+ return self.frame.code.raw.co_name
+
+
+class Traceback(List[TracebackEntry]):
+ """Traceback objects encapsulate and offer higher level access to Traceback entries."""
+
+ 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)
+ cur_ = cur_.tb_next
+
+ super().__init__(f(tb))
+ else:
+ super().__init__(tb)
+
+ def cut(
+ self,
+ path: Optional[Union["os.PathLike[str]", str]] = None,
+ lineno: Optional[int] = None,
+ firstlineno: Optional[int] = None,
+ excludepath: Optional["os.PathLike[str]"] = None,
+ ) -> "Traceback":
+ """Return a Traceback instance wrapping part of this Traceback.
+
+ By providing 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).
+ """
+ path_ = None if path is None else os.fspath(path)
+ excludepath_ = None if excludepath is None else os.fspath(excludepath)
+ for x in self:
+ code = x.frame.code
+ codepath = code.path
+ if path is not None and str(codepath) != path_:
+ continue
+ if (
+ excludepath is not None
+ and isinstance(codepath, Path)
+ and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator]
+ ):
+ continue
+ if lineno is not None and x.lineno != lineno:
+ continue
+ if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
+ continue
+ return Traceback(x._rawentry, self._excinfo)
+ return self
+
+ @overload
+ def __getitem__(self, key: "SupportsIndex") -> TracebackEntry:
+ ...
+
+ @overload
+ def __getitem__(self, key: slice) -> "Traceback":
+ ...
+
+ def __getitem__(
+ self, key: Union["SupportsIndex", slice]
+ ) -> Union[TracebackEntry, "Traceback"]:
+ if isinstance(key, slice):
+ return self.__class__(super().__getitem__(key))
+ else:
+ return super().__getitem__(key)
+
+ def filter(
+ self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden()
+ ) -> "Traceback":
+ """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.
+
+ By default this removes all the TracebackEntries which are hidden
+ (see ishidden() above).
+ """
+ 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]
+
+ def recursionindex(self) -> Optional[int]:
+ """Return the index of the frame/TracebackEntry where recursion originates if
+ appropriate, None if no recursion occurred."""
+ cache: Dict[Tuple[Any, int, int], List[Dict[str, Any]]] = {}
+ 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
+ values = cache.setdefault(key, [])
+ if values:
+ f = entry.frame
+ loc = f.f_locals
+ for otherloc in values:
+ if otherloc == loc:
+ return i
+ values.append(entry.frame.f_locals)
+ return None
+
+
+E = TypeVar("E", bound=BaseException, covariant=True)
+
+
+@final
+@attr.s(repr=False, init=False, auto_attribs=True)
+class ExceptionInfo(Generic[E]):
+ """Wraps sys.exc_info() objects and offers help for navigating the traceback."""
+
+ _assert_start_repr: ClassVar = "AssertionError('assert "
+
+ _excinfo: Optional[Tuple[Type["E"], "E", TracebackType]]
+ _striptext: str
+ _traceback: Optional[Traceback]
+
+ def __init__(
+ self,
+ excinfo: Optional[Tuple[Type["E"], "E", TracebackType]],
+ striptext: str = "",
+ traceback: Optional[Traceback] = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._excinfo = excinfo
+ self._striptext = striptext
+ self._traceback = traceback
+
+ @classmethod
+ def from_exc_info(
+ cls,
+ exc_info: Tuple[Type[E], E, TracebackType],
+ exprinfo: Optional[str] = None,
+ ) -> "ExceptionInfo[E]":
+ """Return an ExceptionInfo for an existing exc_info tuple.
+
+ .. warning::
+
+ Experimental API
+
+ :param exprinfo:
+ A text string helping to determine if we should strip
+ ``AssertionError`` from the output. Defaults to the exception
+ message/``__str__()``.
+ """
+ _striptext = ""
+ if exprinfo is None and isinstance(exc_info[1], AssertionError):
+ exprinfo = getattr(exc_info[1], "msg", None)
+ if exprinfo is None:
+ exprinfo = saferepr(exc_info[1])
+ if exprinfo and exprinfo.startswith(cls._assert_start_repr):
+ _striptext = "AssertionError: "
+
+ return cls(exc_info, _striptext, _ispytest=True)
+
+ @classmethod
+ def from_current(
+ cls, exprinfo: Optional[str] = None
+ ) -> "ExceptionInfo[BaseException]":
+ """Return an ExceptionInfo matching the current traceback.
+
+ .. warning::
+
+ Experimental API
+
+ :param exprinfo:
+ A text string helping to determine if we should strip
+ ``AssertionError`` from the output. Defaults to the exception
+ message/``__str__()``.
+ """
+ tup = sys.exc_info()
+ assert tup[0] is not None, "no current exception"
+ assert tup[1] is not None, "no current exception"
+ assert tup[2] is not None, "no current exception"
+ exc_info = (tup[0], tup[1], tup[2])
+ return ExceptionInfo.from_exc_info(exc_info, exprinfo)
+
+ @classmethod
+ def for_later(cls) -> "ExceptionInfo[E]":
+ """Return an unfilled ExceptionInfo."""
+ return cls(None, _ispytest=True)
+
+ def fill_unfilled(self, exc_info: Tuple[Type[E], E, TracebackType]) -> None:
+ """Fill an unfilled ExceptionInfo created with ``for_later()``."""
+ assert self._excinfo is None, "ExceptionInfo was already filled"
+ self._excinfo = exc_info
+
+ @property
+ def type(self) -> Type[E]:
+ """The exception class."""
+ assert (
+ self._excinfo is not None
+ ), ".type can only be used after the context manager exits"
+ return self._excinfo[0]
+
+ @property
+ def value(self) -> E:
+ """The exception value."""
+ assert (
+ self._excinfo is not None
+ ), ".value can only be used after the context manager exits"
+ return self._excinfo[1]
+
+ @property
+ def tb(self) -> TracebackType:
+ """The exception raw traceback."""
+ assert (
+ self._excinfo is not None
+ ), ".tb can only be used after the context manager exits"
+ return self._excinfo[2]
+
+ @property
+ def typename(self) -> str:
+ """The type name of the exception."""
+ assert (
+ self._excinfo is not None
+ ), ".typename can only be used after the context manager exits"
+ return self.type.__name__
+
+ @property
+ def traceback(self) -> Traceback:
+ """The traceback."""
+ if self._traceback is None:
+ self._traceback = Traceback(self.tb, excinfo=ref(self))
+ return self._traceback
+
+ @traceback.setter
+ def traceback(self, value: Traceback) -> None:
+ self._traceback = value
+
+ 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)
+ )
+
+ def exconly(self, tryshort: bool = False) -> str:
+ """Return the exception as a string.
+
+ When 'tryshort' resolves to True, and the exception is an
+ 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: Union[Type[BaseException], Tuple[Type[BaseException], ...]]
+ ) -> bool:
+ """Return True if the exception is an instance of exc.
+
+ Consider using ``isinstance(excinfo.value, exc)`` instead.
+ """
+ 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 getrepr(
+ self,
+ showlocals: bool = False,
+ style: "_TracebackStyle" = "long",
+ abspath: bool = False,
+ tbfilter: bool = True,
+ funcargs: bool = False,
+ truncate_locals: bool = True,
+ chain: bool = True,
+ ) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]:
+ """Return str()able representation of this exception info.
+
+ :param bool showlocals:
+ Show locals per traceback entry.
+ Ignored if ``style=="native"``.
+
+ :param str style:
+ long|short|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 bool funcargs:
+ Show fixtures ("funcargs" for legacy purposes) per traceback entry.
+
+ :param bool truncate_locals:
+ With ``showlocals==True``, make sure locals can be safely represented as strings.
+
+ :param bool chain:
+ If chained exceptions in Python 3 should be shown.
+
+ .. versionchanged:: 3.9
+
+ Added the ``chain`` parameter.
+ """
+ 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,
+ truncate_locals=truncate_locals,
+ chain=chain,
+ )
+ return fmt.repr_excinfo(self)
+
+ 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`.
+
+ 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))
+ # Return True to allow for "assert excinfo.match()".
+ return True
+
+
+@attr.s(auto_attribs=True)
+class FormattedExcinfo:
+ """Presenting information about failing Functions and Generators."""
+
+ # for traceback entries
+ flow_marker: ClassVar = ">"
+ fail_marker: ClassVar = "E"
+
+ showlocals: bool = False
+ style: "_TracebackStyle" = "long"
+ abspath: bool = True
+ tbfilter: bool = 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
+ )
+
+ def _getindent(self, source: "Source") -> int:
+ # Figure out indent for the given source.
+ try:
+ s = str(source.getstatement(len(source) - 1))
+ except KeyboardInterrupt:
+ raise
+ except BaseException:
+ try:
+ s = str(source[-1])
+ except KeyboardInterrupt:
+ raise
+ except BaseException:
+ return 0
+ return 4 + (len(s) - len(s.lstrip()))
+
+ def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]:
+ source = entry.getsource(self.astcache)
+ if source is not None:
+ source = source.deindent()
+ return source
+
+ def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]:
+ if self.funcargs:
+ args = []
+ for argname, argvalue in entry.frame.getargs(var=True):
+ args.append((argname, saferepr(argvalue)))
+ return ReprFuncArgs(args)
+ return None
+
+ def get_source(
+ self,
+ source: Optional["Source"],
+ line_index: int = -1,
+ excinfo: Optional[ExceptionInfo[BaseException]] = None,
+ short: bool = False,
+ ) -> List[str]:
+ """Return formatted and marked up source lines."""
+ lines = []
+ if source is None or line_index >= len(source.lines):
+ 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())
+ 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: ExceptionInfo[BaseException],
+ indent: int = 4,
+ markall: bool = False,
+ ) -> List[str]:
+ lines = []
+ indentstr = " " * indent
+ # Get the real exception information out.
+ exlines = excinfo.exconly(tryshort=True).split("\n")
+ failindent = self.fail_marker + indentstr[1:]
+ for line in exlines:
+ lines.append(failindent + line)
+ if not markall:
+ failindent = indentstr
+ return lines
+
+ def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]:
+ 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.
+ if self.truncate_locals:
+ str_repr = saferepr(value)
+ else:
+ str_repr = safeformat(value)
+ # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
+ lines.append(f"{name:<10} = {str_repr}")
+ # else:
+ # self._line("%-10s =\\" % (name,))
+ # # XXX
+ # pprint.pprint(value, stream=self.excinfowriter)
+ return ReprLocals(lines)
+ return None
+
+ def repr_traceback_entry(
+ self,
+ entry: 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"):
+ source = self._getentrysource(entry)
+ if source is None:
+ source = Source("???")
+ line_index = 0
+ else:
+ line_index = entry.lineno - entry.getfirstlinesource()
+ 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 ""
+ entry_path = entry.path
+ path = self._makepath(entry_path)
+ reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
+ localsrepr = self.repr_locals(entry.locals)
+ return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)
+ elif style == "value":
+ if excinfo:
+ lines.extend(str(excinfo.value).split("\n"))
+ return ReprEntry(lines, None, None, None, style)
+ else:
+ if excinfo:
+ lines.extend(self.get_exconly(excinfo, indent=4))
+ return ReprEntry(lines, None, None, None, style)
+
+ def _makepath(self, path: Union[Path, str]) -> str:
+ if not self.abspath and isinstance(path, Path):
+ try:
+ np = bestrelpath(Path.cwd(), path)
+ except OSError:
+ return str(path)
+ if len(np) < len(str(path)):
+ return np
+ return str(path)
+
+ def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
+ traceback = excinfo.traceback
+ if self.tbfilter:
+ traceback = traceback.filter()
+
+ if isinstance(excinfo.value, RecursionError):
+ traceback, extraline = self._truncate_recursive_traceback(traceback)
+ else:
+ extraline = None
+
+ last = traceback[-1]
+ entries = []
+ if self.style == "value":
+ reprentry = self.repr_traceback_entry(last, excinfo)
+ entries.append(reprentry)
+ 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)
+ return ReprTraceback(entries, extraline, style=self.style)
+
+ def _truncate_recursive_traceback(
+ self, traceback: Traceback
+ ) -> Tuple[Traceback, Optional[str]]:
+ """Truncate the given recursive traceback trying to find the starting
+ point of the recursion.
+
+ The detection is done by going through each traceback entry and
+ finding the point in which the locals of the frame are equal to the
+ locals of a previous frame (see ``recursionindex()``).
+
+ Handle the situation where the recursion process might raise an
+ exception (for example comparing numpy arrays using equality raises a
+ TypeError), in which case we do our best to warn the user of the
+ error and show a limited traceback.
+ """
+ try:
+ recursionindex = traceback.recursionindex()
+ except Exception as e:
+ max_frames = 10
+ 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),
+ )
+ # Type ignored because adding two instances of a List subtype
+ # currently incorrectly has type List instead of the subtype.
+ traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
+ else:
+ if recursionindex is not None:
+ extraline = "!!! Recursion detected (same locals & position)"
+ traceback = traceback[: recursionindex + 1]
+ else:
+ extraline = None
+
+ return traceback, extraline
+
+ def repr_excinfo(
+ self, excinfo: ExceptionInfo[BaseException]
+ ) -> "ExceptionChainRepr":
+ repr_chain: List[
+ Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]
+ ] = []
+ e: Optional[BaseException] = excinfo.value
+ excinfo_: Optional[ExceptionInfo[BaseException]] = excinfo
+ descr = None
+ 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
+ )
+ else:
+ # Fallback to native repr if the exception doesn't have a traceback:
+ # ExceptionInfo objects require a full traceback to work.
+ reprtraceback = ReprTracebackNative(
+ 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
+ )
+ 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
+ )
+ descr = "During handling of the above exception, another exception occurred:"
+ else:
+ e = None
+ repr_chain.reverse()
+ return ExceptionChainRepr(repr_chain)
+
+
+@attr.s(eq=False, auto_attribs=True)
+class TerminalRepr:
+ def __str__(self) -> str:
+ # FYI this is called from pytest-xdist's serialization of exception
+ # information.
+ io = StringIO()
+ tw = TerminalWriter(file=io)
+ self.toterminal(tw)
+ return io.getvalue().strip()
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__} instance at {id(self):0x}>"
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ raise NotImplementedError()
+
+
+# This class is abstract -- only subclasses are instantiated.
+@attr.s(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]] = []
+
+ def addsection(self, name: str, content: str, sep: str = "-") -> None:
+ self.sections.append((name, content, sep))
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ for name, content, sep in self.sections:
+ tw.sep(sep, name)
+ tw.line(content)
+
+
+@attr.s(eq=False, auto_attribs=True)
+class ExceptionChainRepr(ExceptionRepr):
+ chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
+
+ def __attrs_post_init__(self) -> None:
+ super().__attrs_post_init__()
+ # reprcrash and reprtraceback of the outermost (the newest) exception
+ # in the chain.
+ self.reprtraceback = self.chain[-1][0]
+ self.reprcrash = self.chain[-1][1]
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ for element in self.chain:
+ element[0].toterminal(tw)
+ if element[2] is not None:
+ tw.line("")
+ tw.line(element[2], yellow=True)
+ super().toterminal(tw)
+
+
+@attr.s(eq=False, auto_attribs=True)
+class ReprExceptionInfo(ExceptionRepr):
+ reprtraceback: "ReprTraceback"
+ reprcrash: "ReprFileLocation"
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ self.reprtraceback.toterminal(tw)
+ super().toterminal(tw)
+
+
+@attr.s(eq=False, auto_attribs=True)
+class ReprTraceback(TerminalRepr):
+ reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
+ extraline: Optional[str]
+ style: "_TracebackStyle"
+
+ entrysep: ClassVar = "_ "
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ # The entries might have different styles.
+ 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: Sequence[str]) -> None:
+ self.style = "native"
+ self.reprentries = [ReprEntryNative(tblines)]
+ self.extraline = None
+
+
+@attr.s(eq=False, auto_attribs=True)
+class ReprEntryNative(TerminalRepr):
+ lines: Sequence[str]
+
+ style: ClassVar["_TracebackStyle"] = "native"
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ tw.write("".join(self.lines))
+
+
+@attr.s(eq=False, auto_attribs=True)
+class ReprEntry(TerminalRepr):
+ lines: Sequence[str]
+ reprfuncargs: Optional["ReprFuncArgs"]
+ reprlocals: Optional["ReprLocals"]
+ reprfileloc: Optional["ReprFileLocation"]
+ style: "_TracebackStyle"
+
+ def _write_entry_lines(self, tw: TerminalWriter) -> None:
+ """Write the source code portions of a list of traceback entries with syntax highlighting.
+
+ Usually entries are lines like these:
+
+ " x = 1"
+ "> assert x == 2"
+ "E assert 1 == 2"
+
+ This function takes care of rendering the "source" portions of it (the lines without
+ 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
+
+ # separate indents and source lines that are not failures: we want to
+ # highlight the code but not the indentation, which may contain markers
+ # such as "> assert 0"
+ fail_marker = f"{FormattedExcinfo.fail_marker} "
+ indent_size = len(fail_marker)
+ indents: List[str] = []
+ source_lines: List[str] = []
+ failure_lines: List[str] = []
+ for index, line in enumerate(self.lines):
+ is_failure_line = line.startswith(fail_marker)
+ if is_failure_line:
+ # from this point on all lines are considered part of the failure
+ failure_lines.extend(self.lines[index:])
+ break
+ else:
+ if self.style == "value":
+ source_lines.append(line)
+ else:
+ indents.append(line[:indent_size])
+ source_lines.append(line[indent_size:])
+
+ tw._write_source(source_lines, indents)
+
+ # failure lines are always completely red and bold
+ for line in failure_lines:
+ tw.line(line, bold=True, red=True)
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ if self.style == "short":
+ assert self.reprfileloc is not None
+ self.reprfileloc.toterminal(tw)
+ self._write_entry_lines(tw)
+ if self.reprlocals:
+ self.reprlocals.toterminal(tw, indent=" " * 8)
+ return
+
+ if self.reprfuncargs:
+ self.reprfuncargs.toterminal(tw)
+
+ self._write_entry_lines(tw)
+
+ if self.reprlocals:
+ tw.line("")
+ self.reprlocals.toterminal(tw)
+ if self.reprfileloc:
+ if self.lines:
+ tw.line("")
+ self.reprfileloc.toterminal(tw)
+
+ def __str__(self) -> str:
+ return "{}\n{}\n{}".format(
+ "\n".join(self.lines), self.reprlocals, self.reprfileloc
+ )
+
+
+@attr.s(eq=False, auto_attribs=True)
+class ReprFileLocation(TerminalRepr):
+ path: str = attr.ib(converter=str)
+ lineno: int
+ message: str
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ # Filename and lineno output for each entry, using an output format
+ # that most editors understand.
+ msg = self.message
+ i = msg.find("\n")
+ if i != -1:
+ msg = msg[:i]
+ tw.write(self.path, bold=True, red=True)
+ tw.line(f":{self.lineno}: {msg}")
+
+
+@attr.s(eq=False, auto_attribs=True)
+class ReprLocals(TerminalRepr):
+ lines: Sequence[str]
+
+ def toterminal(self, tw: TerminalWriter, indent="") -> None:
+ for line in self.lines:
+ tw.line(indent + line)
+
+
+@attr.s(eq=False, auto_attribs=True)
+class ReprFuncArgs(TerminalRepr):
+ args: Sequence[Tuple[str, object]]
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ if self.args:
+ linesofar = ""
+ for name, value in self.args:
+ ns = f"{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("")
+
+
+def getfslineno(obj: object) -> Tuple[Union[str, Path], int]:
+ """Return source location (path, lineno) for the given object.
+
+ If the source cannot be determined return ("", -1).
+
+ The line number is 0-based.
+ """
+ # xxx let decorators etc specify a sane ordering
+ # NOTE: this used to be done in _pytest.compat.getfslineno, initially added
+ # 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]
+
+ try:
+ code = Code.from_function(obj)
+ except TypeError:
+ try:
+ fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type]
+ except TypeError:
+ return "", -1
+
+ fspath = fn and absolutepath(fn) or ""
+ lineno = -1
+ if fspath:
+ try:
+ _, lineno = findsource(obj)
+ except OSError:
+ pass
+ return fspath, lineno
+
+ return code.path, code.firstlineno
+
+
+# Relative paths that we use to filter traceback entries from appearing to the user;
+# see filter_traceback.
+# note: if we need to add more paths than what we have now we should probably use a list
+# for better maintenance.
+
+_PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
+# pluggy is either a package or a single module depending on the version
+if _PLUGGY_DIR.name == "__init__.py":
+ _PLUGGY_DIR = _PLUGGY_DIR.parent
+_PYTEST_DIR = Path(_pytest.__file__).parent
+
+
+def filter_traceback(entry: TracebackEntry) -> bool:
+ """Return True if a TracebackEntry instance should be included in tracebacks.
+
+ We hide traceback entries of:
+
+ * dynamically generated code (no code to show up for it);
+ * internal traceback from pytest or its internal libraries, py and pluggy.
+ """
+ # entry.path might sometimes return a str object when the entry
+ # points to dynamically generated code.
+ # See https://bitbucket.org/pytest-dev/py/issues/71.
+ raw_filename = entry.frame.code.raw.co_filename
+ is_generated = "<" in raw_filename and ">" in raw_filename
+ if is_generated:
+ return False
+
+ # entry.path might point to a non-existing file, in which case it will
+ # also return a str object. See #1133.
+ p = Path(entry.path)
+
+ parents = p.parents
+ if _PLUGGY_DIR in parents:
+ return False
+ if _PYTEST_DIR in parents:
+ return False
+
+ return True
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_code/source.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_code/source.py
new file mode 100644
index 0000000000..208cfb8003
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_code/source.py
@@ -0,0 +1,217 @@
+import ast
+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
+from typing import Optional
+from typing import overload
+from typing import Tuple
+from typing import Union
+
+
+class Source:
+ """An immutable object holding a source code fragment.
+
+ When using Source(...), the source lines are deindented.
+ """
+
+ def __init__(self, obj: object = None) -> None:
+ if not obj:
+ self.lines: List[str] = []
+ elif isinstance(obj, Source):
+ self.lines = obj.lines
+ elif isinstance(obj, (tuple, list)):
+ self.lines = deindent(x.rstrip("\n") for x in obj)
+ elif isinstance(obj, str):
+ self.lines = deindent(obj.split("\n"))
+ else:
+ try:
+ rawcode = getrawcode(obj)
+ src = inspect.getsource(rawcode)
+ except TypeError:
+ src = inspect.getsource(obj) # type: ignore[arg-type]
+ self.lines = deindent(src.split("\n"))
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Source):
+ return NotImplemented
+ return self.lines == other.lines
+
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
+ __hash__ = None # type: ignore
+
+ @overload
+ def __getitem__(self, key: int) -> str:
+ ...
+
+ @overload
+ def __getitem__(self, key: slice) -> "Source":
+ ...
+
+ def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]:
+ 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")
+ newsource = Source()
+ newsource.lines = self.lines[key.start : key.stop]
+ return newsource
+
+ def __iter__(self) -> Iterator[str]:
+ return iter(self.lines)
+
+ def __len__(self) -> int:
+ return len(self.lines)
+
+ def strip(self) -> "Source":
+ """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 indent(self, indent: str = " " * 4) -> "Source":
+ """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: int) -> "Source":
+ """Return Source statement which contains the given linenumber
+ (counted from 0)."""
+ start, end = self.getstatementrange(lineno)
+ return self[start:end]
+
+ def getstatementrange(self, lineno: int) -> Tuple[int, int]:
+ """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) -> "Source":
+ """Return a new Source object deindented."""
+ newsource = Source()
+ newsource.lines[:] = deindent(self.lines)
+ return newsource
+
+ def __str__(self) -> str:
+ return "\n".join(self.lines)
+
+
+#
+# helper functions
+#
+
+
+def findsource(obj) -> Tuple[Optional[Source], int]:
+ try:
+ sourcelines, lineno = inspect.findsource(obj)
+ except Exception:
+ return None, -1
+ source = Source()
+ source.lines = [line.rstrip() for line in sourcelines]
+ return source, lineno
+
+
+def getrawcode(obj: object, trycall: bool = True) -> types.CodeType:
+ """Return code object for given function."""
+ try:
+ return obj.__code__ # type: ignore[attr-defined,no-any-return]
+ except AttributeError:
+ pass
+ if trycall:
+ call = getattr(obj, "__call__", None)
+ if call and not isinstance(obj, type):
+ return getrawcode(call, trycall=False)
+ raise TypeError(f"could not get code object for {obj!r}")
+
+
+def deindent(lines: Iterable[str]) -> List[str]:
+ return textwrap.dedent("\n".join(lines)).splitlines()
+
+
+def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]:
+ # Flatten all statements and except handlers into one lineno-list.
+ # AST's line numbers start indexing at 1.
+ 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.
+ if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
+ for d in x.decorator_list:
+ values.append(d.lineno - 1)
+ values.append(x.lineno - 1)
+ for name in ("finalbody", "orelse"):
+ val: Optional[List[ast.stmt]] = getattr(x, name, None)
+ if val:
+ # Treat the finally/orelse part as its own statement.
+ values.append(val[0].lineno - 1 - 1)
+ values.sort()
+ insert_index = bisect_right(values, lineno)
+ start = values[insert_index - 1]
+ if insert_index >= len(values):
+ end = None
+ else:
+ end = values[insert_index]
+ return start, end
+
+
+def getstatementrange_ast(
+ lineno: int,
+ source: Source,
+ assertion: bool = False,
+ astnode: Optional[ast.AST] = None,
+) -> Tuple[ast.AST, int, int]:
+ if astnode is None:
+ content = str(source)
+ # See #4260:
+ # Don't produce duplicate warnings when compiling source to find AST.
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ astnode = ast.parse(content, "source", "exec")
+
+ 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
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/__init__.py
new file mode 100644
index 0000000000..db001e918c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/__init__.py
@@ -0,0 +1,8 @@
+from .terminalwriter import get_terminal_width
+from .terminalwriter import TerminalWriter
+
+
+__all__ = [
+ "TerminalWriter",
+ "get_terminal_width",
+]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/saferepr.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/saferepr.py
new file mode 100644
index 0000000000..e7ff5cab20
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/saferepr.py
@@ -0,0 +1,153 @@
+import pprint
+import reprlib
+from typing import Any
+from typing import Dict
+from typing import IO
+from typing import Optional
+
+
+def _try_repr_or_str(obj: object) -> str:
+ try:
+ return repr(obj)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except BaseException:
+ return f'{type(obj).__name__}("{obj}")'
+
+
+def _format_repr_exception(exc: BaseException, obj: object) -> str:
+ try:
+ exc_info = _try_repr_or_str(exc)
+ except (KeyboardInterrupt, SystemExit):
+ 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)
+ )
+
+
+def _ellipsize(s: str, maxsize: int) -> str:
+ if len(s) > maxsize:
+ i = max(0, (maxsize - 3) // 2)
+ j = max(0, maxsize - 3 - i)
+ return s[:i] + "..." + s[len(s) - j :]
+ return s
+
+
+class SafeRepr(reprlib.Repr):
+ """
+ repr.Repr that limits the resulting size of repr() and includes
+ information on exceptions raised during the call.
+ """
+
+ def __init__(self, maxsize: Optional[int]) -> None:
+ """
+ :param maxsize:
+ If not None, will truncate the resulting repr to that specific size, using ellipsis
+ somewhere in the middle to hide the extra text.
+ If None, will not impose any size limits on the returning repr.
+ """
+ super().__init__()
+ # ``maxstring`` is used by the superclass, and needs to be an int; using a
+ # very large number in case maxsize is None, meaning we want to disable
+ # truncation.
+ self.maxstring = maxsize if maxsize is not None else 1_000_000_000
+ self.maxsize = maxsize
+
+ def repr(self, x: object) -> str:
+ try:
+ s = super().repr(x)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except BaseException as exc:
+ s = _format_repr_exception(exc, x)
+ if self.maxsize is not None:
+ s = _ellipsize(s, self.maxsize)
+ return s
+
+ def repr_instance(self, x: object, level: int) -> str:
+ try:
+ s = repr(x)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except BaseException as exc:
+ s = _format_repr_exception(exc, x)
+ if self.maxsize is not None:
+ s = _ellipsize(s, self.maxsize)
+ return s
+
+
+def safeformat(obj: object) -> str:
+ """Return a pretty printed string for the given object.
+
+ Failing __repr__ functions of user instances will be represented
+ with a short exception info.
+ """
+ try:
+ return pprint.pformat(obj)
+ except Exception as exc:
+ return _format_repr_exception(exc, obj)
+
+
+# Maximum size of overall repr of objects to display during assertion errors.
+DEFAULT_REPR_MAX_SIZE = 240
+
+
+def saferepr(obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE) -> str:
+ """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
+ 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)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/terminalwriter.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/terminalwriter.py
new file mode 100644
index 0000000000..379035d858
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/terminalwriter.py
@@ -0,0 +1,233 @@
+"""Helper functions for writing to terminals and files."""
+import os
+import shutil
+import sys
+from typing import Optional
+from typing import Sequence
+from typing import TextIO
+
+from .wcwidth import wcswidth
+from _pytest.compat import final
+
+
+# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
+
+
+def get_terminal_width() -> int:
+ width, _ = shutil.get_terminal_size(fallback=(80, 24))
+
+ # The Windows get_terminal_size may be bogus, let's sanify a bit.
+ if width < 40:
+ width = 80
+
+ return width
+
+
+def should_do_markup(file: TextIO) -> bool:
+ 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
+ if "FORCE_COLOR" in os.environ:
+ return True
+ return (
+ hasattr(file, "isatty") and file.isatty() and os.environ.get("TERM") != "dumb"
+ )
+
+
+@final
+class TerminalWriter:
+ _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,
+ )
+
+ def __init__(self, file: Optional[TextIO] = None) -> None:
+ if file is None:
+ file = sys.stdout
+ if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32":
+ try:
+ import colorama
+ except ImportError:
+ pass
+ else:
+ file = colorama.AnsiToWin32(file).stream
+ assert file is not None
+ self._file = file
+ self.hasmarkup = should_do_markup(file)
+ self._current_line = ""
+ self._terminal_width: Optional[int] = None
+ self.code_highlight = True
+
+ @property
+ def fullwidth(self) -> int:
+ if self._terminal_width is not None:
+ return self._terminal_width
+ return get_terminal_width()
+
+ @fullwidth.setter
+ def fullwidth(self, value: int) -> None:
+ self._terminal_width = value
+
+ @property
+ def width_of_current_line(self) -> int:
+ """Return an estimate of the width so far in the current line."""
+ return wcswidth(self._current_line)
+
+ def markup(self, text: str, **markup: bool) -> str:
+ for name in markup:
+ if name not in self._esctable:
+ raise ValueError(f"unknown markup: {name!r}")
+ if self.hasmarkup:
+ esc = [self._esctable[name] for name, on in markup.items() if on]
+ if esc:
+ text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m"
+ return text
+
+ def sep(
+ self,
+ sepchar: str,
+ title: Optional[str] = None,
+ fullwidth: Optional[int] = None,
+ **markup: bool,
+ ) -> None:
+ 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 = f"{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, **markup)
+
+ def write(self, msg: str, *, flush: bool = False, **markup: bool) -> None:
+ if msg:
+ current_line = msg.rsplit("\n", 1)[-1]
+ if "\n" in msg:
+ self._current_line = current_line
+ else:
+ self._current_line += current_line
+
+ msg = self.markup(msg, **markup)
+
+ try:
+ self._file.write(msg)
+ except UnicodeEncodeError:
+ # Some environments don't support printing general Unicode
+ # strings, due to misconfiguration or otherwise; in that case,
+ # print the string escaped to ASCII.
+ # When the Unicode situation improves we should consider
+ # letting the error propagate instead of masking it (see #7475
+ # for one brief attempt).
+ msg = msg.encode("unicode-escape").decode("ascii")
+ self._file.write(msg)
+
+ if flush:
+ self.flush()
+
+ def line(self, s: str = "", **markup: bool) -> None:
+ self.write(s, **markup)
+ self.write("\n")
+
+ def flush(self) -> None:
+ self._file.flush()
+
+ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> None:
+ """Write lines of source code possibly highlighted.
+
+ Keeping this private for now because the API is clunky. We should discuss how
+ to evolve the terminal writer so we can have more precise color support, for example
+ being able to write part of a line in one color and the rest in another, and so on.
+ """
+ if indents and len(indents) != len(lines):
+ raise ValueError(
+ "indents size ({}) should have same size as lines ({})".format(
+ len(indents), len(lines)
+ )
+ )
+ if not indents:
+ indents = [""] * len(lines)
+ source = "\n".join(lines)
+ new_lines = self._highlight(source).splitlines()
+ 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."""
+ from _pytest.config.exceptions import UsageError
+
+ if not self.hasmarkup or not self.code_highlight:
+ return source
+ try:
+ from pygments.formatters.terminal import TerminalFormatter
+ from pygments.lexers.python import PythonLexer
+ from pygments import highlight
+ import pygments.util
+ except ImportError:
+ return source
+ else:
+ try:
+ highlighted: str = highlight(
+ source,
+ PythonLexer(),
+ TerminalFormatter(
+ bg=os.getenv("PYTEST_THEME_MODE", "dark"),
+ style=os.getenv("PYTEST_THEME"),
+ ),
+ )
+ return highlighted
+ except pygments.util.ClassNotFound:
+ 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:
+ 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")
+ )
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/wcwidth.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/wcwidth.py
new file mode 100644
index 0000000000..e5c7bf4d86
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_io/wcwidth.py
@@ -0,0 +1,55 @@
+import unicodedata
+from functools import lru_cache
+
+
+@lru_cache(100)
+def wcwidth(c: str) -> int:
+ """Determine how many columns are needed to display a character in a terminal.
+
+ Returns -1 if the character is not printable.
+ Returns 0, 1 or 2 for other characters.
+ """
+ o = ord(c)
+
+ # ASCII fast path.
+ if 0x20 <= o < 0x07F:
+ return 1
+
+ # Some Cf/Zp/Zl characters which should be zero-width.
+ if (
+ o == 0x0000
+ or 0x200B <= o <= 0x200F
+ or 0x2028 <= o <= 0x202E
+ or 0x2060 <= o <= 0x2063
+ ):
+ return 0
+
+ category = unicodedata.category(c)
+
+ # Control characters.
+ if category == "Cc":
+ return -1
+
+ # Combining characters with zero width.
+ if category in ("Me", "Mn"):
+ return 0
+
+ # Full/Wide east asian characters.
+ if unicodedata.east_asian_width(c) in ("F", "W"):
+ return 2
+
+ return 1
+
+
+def wcswidth(s: str) -> int:
+ """Determine how many columns are needed to display a string in a terminal.
+
+ Returns -1 if the string contains non-printable characters.
+ """
+ width = 0
+ for c in unicodedata.normalize("NFC", s):
+ wc = wcwidth(c)
+ if wc < 0:
+ return -1
+ width += wc
+ return width
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_version.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_version.py
new file mode 100644
index 0000000000..5515abadad
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/_version.py
@@ -0,0 +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)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/__init__.py
new file mode 100644
index 0000000000..480a26ad86
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/__init__.py
@@ -0,0 +1,181 @@
+"""Support for presenting detailed information in failing assertions."""
+import sys
+from typing import Any
+from typing import Generator
+from typing import List
+from typing import Optional
+from typing import TYPE_CHECKING
+
+from _pytest.assertion import rewrite
+from _pytest.assertion import truncate
+from _pytest.assertion import util
+from _pytest.assertion.rewrite import assertstate_key
+from _pytest.config import Config
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.nodes import Item
+
+if TYPE_CHECKING:
+ from _pytest.main import Session
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("debugconfig")
+ group.addoption(
+ "--assert",
+ action="store",
+ dest="assertmode",
+ choices=("rewrite", "plain"),
+ default="rewrite",
+ metavar="MODE",
+ help=(
+ "Control assertion debugging tools.\n"
+ "'plain' performs no assertion debugging.\n"
+ "'rewrite' (the default) rewrites assert statements in test modules"
+ " on import to provide assert expression information."
+ ),
+ )
+ parser.addini(
+ "enable_assertion_pass_hook",
+ type="bool",
+ default=False,
+ help="Enables the pytest_assertion_pass hook."
+ "Make sure to delete any previously generated pyc cache files.",
+ )
+
+
+def register_assert_rewrite(*names: str) -> None:
+ """Register one or more module names to be rewritten on import.
+
+ This function will make sure that this module or all modules inside
+ the package will get their assert statements rewritten.
+ Thus you should make sure to call this before the module is
+ 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.
+ """
+ for name in names:
+ if not isinstance(name, str):
+ msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable]
+ raise TypeError(msg.format(repr(names)))
+ for hook in sys.meta_path:
+ if isinstance(hook, rewrite.AssertionRewritingHook):
+ importhook = hook
+ break
+ else:
+ # TODO(typing): Add a protocol for mark_rewrite() and use it
+ # for importhook and for PytestPluginManager.rewrite_hook.
+ importhook = DummyRewriteHook() # type: ignore
+ importhook.mark_rewrite(*names)
+
+
+class DummyRewriteHook:
+ """A no-op import hook for when rewriting is disabled."""
+
+ def mark_rewrite(self, *names: str) -> None:
+ pass
+
+
+class AssertionState:
+ """State for the assertion plugin."""
+
+ def __init__(self, config: Config, mode) -> None:
+ self.mode = mode
+ self.trace = config.trace.root.get("assertion")
+ self.hook: Optional[rewrite.AssertionRewritingHook] = None
+
+
+def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
+ """Try to install the rewrite hook, raise SystemError if it fails."""
+ config.stash[assertstate_key] = AssertionState(config, "rewrite")
+ config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
+ sys.meta_path.insert(0, hook)
+ config.stash[assertstate_key].trace("installed rewrite import hook")
+
+ def undo() -> None:
+ hook = config.stash[assertstate_key].hook
+ if hook is not None and hook in sys.meta_path:
+ sys.meta_path.remove(hook)
+
+ config.add_cleanup(undo)
+ return hook
+
+
+def pytest_collection(session: "Session") -> None:
+ # This hook is only called when test modules are collected
+ # so for example not in the managing process of pytest-xdist
+ # (which does not collect test modules).
+ assertstate = session.config.stash.get(assertstate_key, None)
+ if assertstate:
+ if assertstate.hook is not None:
+ assertstate.hook.set_session(session)
+
+
+@hookimpl(tryfirst=True, hookwrapper=True)
+def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
+ """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]:
+ """Call the pytest_assertrepr_compare hook and prepare the result.
+
+ This uses the first result from the hook and then ensures the
+ following:
+ * Overly verbose explanations are truncated unless configured otherwise
+ (eg. if running in verbose mode).
+ * Embedded newlines are escaped to help util.format_explanation()
+ later.
+ * If the rewrite mode is used embedded %-characters are replaced
+ to protect later % formatting.
+
+ The result can be formatted by util.format_explanation() for
+ pretty printing.
+ """
+ hook_result = ihook.pytest_assertrepr_compare(
+ config=item.config, op=op, left=left, right=right
+ )
+ for new_expl in hook_result:
+ if new_expl:
+ new_expl = truncate.truncate_if_required(new_expl, item)
+ new_expl = [line.replace("\n", "\\n") for line in new_expl]
+ res = "\n~".join(new_expl)
+ if item.config.getvalue("assertmode") == "rewrite":
+ res = res.replace("%", "%%")
+ return res
+ return None
+
+ saved_assert_hooks = util._reprcompare, util._assertion_pass
+ util._reprcompare = callbinrepr
+ util._config = item.config
+
+ if ihook.pytest_assertion_pass.get_hookimpls():
+
+ def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None:
+ ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl)
+
+ util._assertion_pass = call_assertion_pass_hook
+
+ yield
+
+ util._reprcompare, util._assertion_pass = saved_assert_hooks
+ util._config = None
+
+
+def pytest_sessionfinish(session: "Session") -> None:
+ assertstate = session.config.stash.get(assertstate_key, None)
+ if assertstate:
+ if assertstate.hook is not None:
+ assertstate.hook.set_session(None)
+
+
+def pytest_assertrepr_compare(
+ config: Config, op: str, left: Any, right: Any
+) -> Optional[List[str]]:
+ return util.assertrepr_compare(config=config, op=op, left=left, right=right)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/rewrite.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/rewrite.py
new file mode 100644
index 0000000000..88ac6cab36
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/rewrite.py
@@ -0,0 +1,1136 @@
+"""Rewrite assertion AST to produce nice error messages."""
+import ast
+import errno
+import functools
+import importlib.abc
+import importlib.machinery
+import importlib.util
+import io
+import itertools
+import marshal
+import os
+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
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+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
+
+if TYPE_CHECKING:
+ from _pytest.assertion import AssertionState
+
+
+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
+
+
+class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
+ """PEP302/PEP451 import hook which rewrites asserts."""
+
+ def __init__(self, config: Config) -> None:
+ self.config = config
+ try:
+ self.fnpats = config.getini("python_files")
+ except ValueError:
+ self.fnpats = ["test_*.py", "*_test.py"]
+ self.session: Optional[Session] = None
+ self._rewritten_names: Dict[str, Path] = {}
+ self._must_rewrite: Set[str] = set()
+ # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
+ # which might result in infinite recursion (#3506)
+ self._writing_pyc = False
+ self._basenames_to_check_rewrite = {"conftest"}
+ self._marked_for_rewrite_cache: Dict[str, bool] = {}
+ self._session_paths_checked = False
+
+ def set_session(self, session: Optional[Session]) -> None:
+ self.session = session
+ self._session_paths_checked = False
+
+ # Indirection so we can mock calls to find_spec originated from the hook during testing
+ _find_spec = importlib.machinery.PathFinder.find_spec
+
+ def find_spec(
+ self,
+ name: str,
+ path: Optional[Sequence[Union[str, bytes]]] = None,
+ target: Optional[types.ModuleType] = None,
+ ) -> Optional[importlib.machinery.ModuleSpec]:
+ if self._writing_pyc:
+ return None
+ state = self.config.stash[assertstate_key]
+ if self._early_rewrite_bailout(name, state):
+ return None
+ state.trace("find_module called for: %s" % name)
+
+ # Type ignored because mypy is confused about the `self` binding here.
+ spec = self._find_spec(name, path) # type: ignore
+ if (
+ # the import machinery could not find a file to import
+ 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)
+ # if the file doesn't exist, we can't rewrite it
+ or not os.path.exists(spec.origin)
+ ):
+ return None
+ else:
+ fn = spec.origin
+
+ if not self._should_rewrite(name, fn, state):
+ return None
+
+ return importlib.util.spec_from_file_location(
+ name,
+ fn,
+ loader=self,
+ submodule_search_locations=spec.submodule_search_locations,
+ )
+
+ def create_module(
+ self, spec: importlib.machinery.ModuleSpec
+ ) -> Optional[types.ModuleType]:
+ return None # default behaviour is fine
+
+ def exec_module(self, module: types.ModuleType) -> None:
+ assert module.__spec__ is not None
+ assert module.__spec__.origin is not None
+ fn = Path(module.__spec__.origin)
+ state = self.config.stash[assertstate_key]
+
+ self._rewritten_names[module.__name__] = fn
+
+ # The requested module looks like a test file, so rewrite it. This is
+ # the most magical part of the process: load the source, rewrite the
+ # asserts, and load the rewritten source. We also cache the rewritten
+ # module code in a special pyc. We must be aware of the possibility of
+ # concurrent pytest processes rewriting and loading pycs. To avoid
+ # tricky race conditions, we maintain the following invariant: The
+ # cached pyc is always a complete, valid pyc. Operations on it must be
+ # atomic. POSIX's atomic rename comes in handy.
+ write = not sys.dont_write_bytecode
+ cache_dir = get_cache_dir(fn)
+ if write:
+ ok = try_makedirs(cache_dir)
+ if not ok:
+ write = False
+ state.trace(f"read only directory: {cache_dir}")
+
+ cache_name = fn.name[:-3] + PYC_TAIL
+ pyc = cache_dir / cache_name
+ # Notice that even if we're in a read-only directory, I'm going
+ # to check for a cached pyc. This may not be optimal...
+ co = _read_pyc(fn, pyc, state.trace)
+ if co is None:
+ state.trace(f"rewriting {fn!r}")
+ source_stat, co = _rewrite_test(fn, self.config)
+ if write:
+ self._writing_pyc = True
+ try:
+ _write_pyc(state, co, source_stat, pyc)
+ finally:
+ self._writing_pyc = False
+ else:
+ state.trace(f"found cached rewritten pyc for {fn}")
+ exec(co, module.__dict__)
+
+ def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool:
+ """A fast way to get out of rewriting modules.
+
+ Profiling has shown that the call to PathFinder.find_spec (inside of
+ the find_spec from this class) is a major slowdown, so, this method
+ tries to filter what we're sure won't be rewritten before getting to
+ it.
+ """
+ if self.session is not None and not self._session_paths_checked:
+ self._session_paths_checked = True
+ 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)
+ # add 'path' to basenames to be checked.
+ self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
+
+ # Note: conftest already by default in _basenames_to_check_rewrite.
+ parts = name.split(".")
+ if parts[-1] in self._basenames_to_check_rewrite:
+ return False
+
+ # For matching the name it must be as if it was a filename.
+ path = PurePath(os.path.sep.join(parts) + ".py")
+
+ for pat in self.fnpats:
+ # if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based
+ # on the name alone because we need to match against the full path
+ if os.path.dirname(pat):
+ return False
+ if fnmatch_ex(pat, path):
+ return False
+
+ if self._is_marked_for_rewrite(name, state):
+ return False
+
+ state.trace(f"early skip of rewriting module: {name}")
+ return True
+
+ def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool:
+ # always rewrite conftest files
+ if os.path.basename(fn) == "conftest.py":
+ state.trace(f"rewriting conftest file: {fn!r}")
+ return True
+
+ if self.session is not None:
+ if self.session.isinitpath(absolutepath(fn)):
+ state.trace(f"matched test file (was specified on cmdline): {fn!r}")
+ return True
+
+ # modules not passed explicitly on the command line are only
+ # rewritten if they match the naming convention for test files
+ fn_path = PurePath(fn)
+ for pat in self.fnpats:
+ if fnmatch_ex(pat, fn_path):
+ state.trace(f"matched test file {fn!r}")
+ return True
+
+ return self._is_marked_for_rewrite(name, state)
+
+ def _is_marked_for_rewrite(self, name: str, state: "AssertionState") -> bool:
+ try:
+ return self._marked_for_rewrite_cache[name]
+ except KeyError:
+ for marked in self._must_rewrite:
+ if name == marked or name.startswith(marked + "."):
+ state.trace(f"matched marked file {name!r} (from {marked!r})")
+ self._marked_for_rewrite_cache[name] = True
+ return True
+
+ self._marked_for_rewrite_cache[name] = False
+ return False
+
+ def mark_rewrite(self, *names: str) -> None:
+ """Mark import names as needing to be rewritten.
+
+ The named module or package as well as any nested modules will
+ be rewritten on import.
+ """
+ already_imported = (
+ set(names).intersection(sys.modules).difference(self._rewritten_names)
+ )
+ for name in already_imported:
+ mod = sys.modules[name]
+ if not AssertionRewriter.is_rewrite_disabled(
+ mod.__doc__ or ""
+ ) and not isinstance(mod.__loader__, type(self)):
+ self._warn_already_imported(name)
+ self._must_rewrite.update(names)
+ self._marked_for_rewrite_cache.clear()
+
+ def _warn_already_imported(self, name: str) -> None:
+ from _pytest.warning_types import PytestAssertRewriteWarning
+
+ self.config.issue_config_time_warning(
+ PytestAssertRewriteWarning(
+ "Module already imported so cannot be rewritten: %s" % name
+ ),
+ stacklevel=5,
+ )
+
+ def get_data(self, pathname: Union[str, bytes]) -> bytes:
+ """Optional PEP302 get_data API."""
+ with open(pathname, "rb") as f:
+ return f.read()
+
+ if sys.version_info >= (3, 10):
+
+ def get_resource_reader(self, name: str) -> importlib.abc.TraversableResources: # type: ignore
+ if sys.version_info < (3, 11):
+ from importlib.readers import FileReader
+ else:
+ from importlib.resources.readers import FileReader
+
+ return FileReader(types.SimpleNamespace(path=self._rewritten_names[name]))
+
+
+def _write_pyc_fp(
+ fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType
+) -> None:
+ # Technically, we don't have to have the same pyc format as
+ # (C)Python, since these "pycs" should never be seen by builtin
+ # 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)
+ # 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
+ # "<LL" stands for 2 unsigned longs, little-endian.
+ fp.write(struct.pack("<LL", mtime, size))
+ 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:
+ _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
+
+
+def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]:
+ """Read and rewrite *fn* and return the code object."""
+ stat = os.stat(fn)
+ source = fn.read_bytes()
+ strfn = str(fn)
+ tree = ast.parse(source, filename=strfn)
+ rewrite_asserts(tree, source, strfn, config)
+ co = compile(tree, strfn, "exec", dont_inherit=True)
+ return stat, co
+
+
+def _read_pyc(
+ source: Path, pyc: Path, trace: Callable[[str], None] = lambda x: None
+) -> Optional[types.CodeType]:
+ """Possibly read a pytest pyc containing rewritten code.
+
+ Return rewritten code if successful or None if not.
+ """
+ try:
+ fp = open(pyc, "rb")
+ 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)
+ 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):
+ 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":
+ 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]
+ 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]
+ if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
+ trace("_read_pyc(%s): invalid pyc (incorrect size)" % source)
+ return None
+ try:
+ co = marshal.load(fp)
+ except Exception as e:
+ trace(f"_read_pyc({source}): marshal.load error {e}")
+ return None
+ if not isinstance(co, types.CodeType):
+ trace("_read_pyc(%s): not a code object" % source)
+ return None
+ return co
+
+
+def rewrite_asserts(
+ mod: ast.Module,
+ source: bytes,
+ module_path: Optional[str] = None,
+ config: Optional[Config] = None,
+) -> None:
+ """Rewrite the assert statements in mod."""
+ AssertionRewriter(module_path, config, source).run(mod)
+
+
+def _saferepr(obj: object) -> str:
+ r"""Get a safe repr of an object for assertion error messages.
+
+ The assertion formatting (util.format_explanation()) requires
+ newlines to be escaped since they are a special character for it.
+ Normally assertion.util.format_explanation() does this but for a
+ custom repr it is possible to contain one of the special escape
+ sequences, especially '\n{' and '\n}' are likely to be present in
+ JSON reprs.
+ """
+ maxsize = _get_maxsize_for_saferepr(util._config)
+ return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")
+
+
+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 verbosity >= 2:
+ return None
+ if verbosity >= 1:
+ return DEFAULT_REPR_MAX_SIZE * 10
+ return DEFAULT_REPR_MAX_SIZE
+
+
+def _format_assertmsg(obj: object) -> str:
+ r"""Format the custom assertion message given.
+
+ For strings this simply replaces newlines with '\n~' so that
+ util.format_explanation() will preserve them instead of escaping
+ newlines. For other objects saferepr() is used first.
+ """
+ # reprlib appears to have a bug which means that if a string
+ # contains a newline it gets escaped, however if an object has a
+ # .__repr__() which contains newlines it does not get escaped.
+ # However in either case we want to preserve the newline.
+ replaces = [("\n", "\n~"), ("%", "%%")]
+ if not isinstance(obj, str):
+ obj = saferepr(obj)
+ replaces.append(("\\n", "\n~"))
+
+ for r1, r2 in replaces:
+ obj = obj.replace(r1, r2)
+
+ return obj
+
+
+def _should_repr_global_name(obj: object) -> bool:
+ if callable(obj):
+ return False
+
+ try:
+ return not hasattr(obj, "__name__")
+ except Exception:
+ return True
+
+
+def _format_boolop(explanations: Iterable[str], is_or: bool) -> str:
+ explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")"
+ return explanation.replace("%", "%%")
+
+
+def _call_reprcompare(
+ ops: Sequence[str],
+ results: Sequence[bool],
+ expls: Sequence[str],
+ each_obj: Sequence[object],
+) -> str:
+ for i, res, expl in zip(range(len(ops)), results, expls):
+ try:
+ done = not res
+ except Exception:
+ done = True
+ if done:
+ break
+ if util._reprcompare is not None:
+ custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1])
+ if custom is not None:
+ return custom
+ return expl
+
+
+def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None:
+ if util._assertion_pass is not None:
+ util._assertion_pass(lineno, orig, expl)
+
+
+def _check_if_assertion_pass_impl() -> bool:
+ """Check if any plugins implement the pytest_assertion_pass hook
+ in order not to generate explanation unnecessarily (might be expensive)."""
+ return True if util._assertion_pass else False
+
+
+UNARY_MAP = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"}
+
+BINOP_MAP = {
+ ast.BitOr: "|",
+ ast.BitXor: "^",
+ ast.BitAnd: "&",
+ ast.LShift: "<<",
+ ast.RShift: ">>",
+ ast.Add: "+",
+ ast.Sub: "-",
+ ast.Mult: "*",
+ ast.Div: "/",
+ ast.FloorDiv: "//",
+ ast.Mod: "%%", # escaped for string formatting
+ 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",
+ ast.MatMult: "@",
+}
+
+
+def traverse_node(node: ast.AST) -> Iterator[ast.AST]:
+ """Recursively yield node and all its children in depth-first order."""
+ yield node
+ for child in ast.iter_child_nodes(node):
+ yield from traverse_node(child)
+
+
+@functools.lru_cache(maxsize=1)
+def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
+ """Return a mapping from {lineno: "assertion test expression"}."""
+ ret: Dict[int, str] = {}
+
+ depth = 0
+ lines: List[str] = []
+ assert_lineno: Optional[int] = None
+ seen_lines: Set[int] = set()
+
+ def _write_and_reset() -> None:
+ nonlocal depth, lines, assert_lineno, seen_lines
+ assert assert_lineno is not None
+ ret[assert_lineno] = "".join(lines).rstrip().rstrip("\\")
+ depth = 0
+ lines = []
+ assert_lineno = None
+ seen_lines = set()
+
+ tokens = tokenize.tokenize(io.BytesIO(src).readline)
+ for tp, source, (lineno, offset), _, line in tokens:
+ if tp == tokenize.NAME and source == "assert":
+ assert_lineno = lineno
+ elif assert_lineno is not None:
+ # keep track of depth for the assert-message `,` lookup
+ if tp == tokenize.OP and source in "([{":
+ depth += 1
+ elif tp == tokenize.OP and source in ")]}":
+ depth -= 1
+
+ if not lines:
+ lines.append(line[offset:])
+ seen_lines.add(lineno)
+ # a non-nested comma separates the expression from the message
+ elif depth == 0 and tp == tokenize.OP and source == ",":
+ # one line assert with message
+ if lineno in seen_lines and len(lines) == 1:
+ offset_in_trimmed = offset + len(lines[-1]) - len(line)
+ lines[-1] = lines[-1][:offset_in_trimmed]
+ # multi-line assert with message
+ elif lineno in seen_lines:
+ lines[-1] = lines[-1][:offset]
+ # multi line assert with escapd newline before message
+ else:
+ lines.append(line[:offset])
+ _write_and_reset()
+ elif tp in {tokenize.NEWLINE, tokenize.ENDMARKER}:
+ _write_and_reset()
+ elif lines and lineno not in seen_lines:
+ lines.append(line)
+ seen_lines.add(lineno)
+
+ return ret
+
+
+class AssertionRewriter(ast.NodeVisitor):
+ """Assertion rewriting implementation.
+
+ The main entrypoint is to call .run() with an ast.Module instance,
+ this will then find all the assert statements and rewrite them to
+ provide intermediate values and a detailed assertion error. See
+ http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html
+ for an overview of how this works.
+
+ The entry point here is .run() which will iterate over all the
+ statements in an ast.Module and for each ast.Assert statement it
+ finds call .visit() with it. Then .visit_Assert() takes over and
+ is responsible for creating new ast statements to replace the
+ original assert statement: it rewrites the test of an assertion
+ to provide intermediate values and replace it with an if statement
+ which raises an assertion error with a detailed explanation in
+ case the expression is false and calls pytest_assertion_pass hook
+ if expression is true.
+
+ For this .visit_Assert() uses the visitor pattern to visit all the
+ AST nodes of the ast.Assert.test field, each visit call returning
+ an AST node and the corresponding explanation string. During this
+ state is kept in several instance attributes:
+
+ :statements: All the AST statements which will replace the assert
+ statement.
+
+ :variables: This is populated by .variable() with each variable
+ used by the statements so that they can all be set to None at
+ the end of the statements.
+
+ :variable_counter: Counter to create new unique variables needed
+ by statements. Variables are created using .variable() and
+ have the form of "@py_assert0".
+
+ :expl_stmts: The AST statements which will be executed to get
+ data from the assertion. This is the code which will construct
+ the detailed assertion message that is used in the AssertionError
+ or for the pytest_assertion_pass hook.
+
+ :explanation_specifiers: A dict filled by .explanation_param()
+ with %-formatting placeholders and their corresponding
+ expressions to use in the building of an assertion message.
+ This is used by .pop_format_context() to build a message.
+
+ :stack: A stack of the explanation_specifiers dicts maintained by
+ .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.
+ """
+
+ def __init__(
+ self, module_path: Optional[str], config: Optional[Config], source: bytes
+ ) -> None:
+ super().__init__()
+ self.module_path = module_path
+ self.config = config
+ if config is not None:
+ self.enable_assertion_pass_hook = config.getini(
+ "enable_assertion_pass_hook"
+ )
+ else:
+ self.enable_assertion_pass_hook = False
+ self.source = source
+
+ def run(self, mod: ast.Module) -> None:
+ """Find all assert statements in *mod* and rewrite them."""
+ if not mod.body:
+ # Nothing to do.
+ return
+
+ # We'll insert some special imports at the top of the module, but after any
+ # docstrings and __future__ imports, so first figure out where that is.
+ doc = getattr(mod, "docstring", None)
+ expect_docstring = doc is None
+ if doc is not None and self.is_rewrite_disabled(doc):
+ return
+ pos = 0
+ lineno = 1
+ for item in mod.body:
+ if (
+ expect_docstring
+ and isinstance(item, ast.Expr)
+ and isinstance(item.value, ast.Str)
+ ):
+ doc = item.value.s
+ if self.is_rewrite_disabled(doc):
+ return
+ expect_docstring = False
+ elif (
+ isinstance(item, ast.ImportFrom)
+ and item.level == 0
+ and item.module == "__future__"
+ ):
+ pass
+ else:
+ break
+ pos += 1
+ # Special case: for a decorated function, set the lineno to that of the
+ # first decorator, not the `def`. Issue #4984.
+ if isinstance(item, ast.FunctionDef) and item.decorator_list:
+ lineno = item.decorator_list[0].lineno
+ else:
+ lineno = item.lineno
+ # Now actually insert the special imports.
+ if sys.version_info >= (3, 10):
+ aliases = [
+ ast.alias("builtins", "@py_builtins", lineno=lineno, col_offset=0),
+ ast.alias(
+ "_pytest.assertion.rewrite",
+ "@pytest_ar",
+ lineno=lineno,
+ col_offset=0,
+ ),
+ ]
+ else:
+ aliases = [
+ ast.alias("builtins", "@py_builtins"),
+ ast.alias("_pytest.assertion.rewrite", "@pytest_ar"),
+ ]
+ imports = [
+ ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases
+ ]
+ mod.body[pos:pos] = imports
+
+ # Collect asserts.
+ nodes: List[ast.AST] = [mod]
+ while nodes:
+ node = nodes.pop()
+ for name, field in ast.iter_fields(node):
+ if isinstance(field, list):
+ new: List[ast.AST] = []
+ for i, child in enumerate(field):
+ if isinstance(child, ast.Assert):
+ # Transform assert.
+ new.extend(self.visit(child))
+ else:
+ new.append(child)
+ if isinstance(child, ast.AST):
+ nodes.append(child)
+ setattr(node, name, new)
+ elif (
+ isinstance(field, ast.AST)
+ # Don't recurse into expressions as they can't contain
+ # asserts.
+ and not isinstance(field, ast.expr)
+ ):
+ nodes.append(field)
+
+ @staticmethod
+ def is_rewrite_disabled(docstring: str) -> bool:
+ return "PYTEST_DONT_REWRITE" in docstring
+
+ def variable(self) -> str:
+ """Get a new variable."""
+ # Use a character invalid in python identifiers to avoid clashing.
+ name = "@py_assert" + str(next(self.variable_counter))
+ self.variables.append(name)
+ return name
+
+ def assign(self, expr: ast.expr) -> ast.Name:
+ """Give *expr* a name."""
+ name = self.variable()
+ self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr))
+ return ast.Name(name, ast.Load())
+
+ def display(self, expr: ast.expr) -> ast.expr:
+ """Call saferepr on the expression."""
+ return self.helper("_saferepr", expr)
+
+ def helper(self, name: str, *args: ast.expr) -> ast.expr:
+ """Call a helper in this module."""
+ py_name = ast.Name("@pytest_ar", ast.Load())
+ attr = ast.Attribute(py_name, name, ast.Load())
+ return ast.Call(attr, list(args), [])
+
+ def builtin(self, name: str) -> ast.Attribute:
+ """Return the builtin called *name*."""
+ builtin_name = ast.Name("@py_builtins", ast.Load())
+ return ast.Attribute(builtin_name, name, ast.Load())
+
+ def explanation_param(self, expr: ast.expr) -> str:
+ """Return a new named %-formatting placeholder for expr.
+
+ This creates a %-formatting placeholder for expr in the
+ current formatting context, e.g. ``%(py0)s``. The placeholder
+ and expr are placed in the current format context so that it
+ can be used on the next call to .pop_format_context().
+ """
+ specifier = "py" + str(next(self.variable_counter))
+ self.explanation_specifiers[specifier] = expr
+ return "%(" + specifier + ")s"
+
+ def push_format_context(self) -> None:
+ """Create a new formatting context.
+
+ The format context is used for when an explanation wants to
+ have a variable value formatted in the assertion message. In
+ this case the value required can be added using
+ .explanation_param(). Finally .pop_format_context() is used
+ to format a string of %-formatted values as added by
+ .explanation_param().
+ """
+ self.explanation_specifiers: Dict[str, ast.expr] = {}
+ self.stack.append(self.explanation_specifiers)
+
+ def pop_format_context(self, expl_expr: ast.expr) -> ast.Name:
+ """Format the %-formatted string with current format context.
+
+ The expl_expr should be an str ast.expr instance constructed from
+ the %-placeholders created by .explanation_param(). This will
+ add the required code to format said string to .expl_stmts and
+ return the ast.Name instance of the formatted string.
+ """
+ current = self.stack.pop()
+ if self.stack:
+ self.explanation_specifiers = self.stack[-1]
+ keys = [ast.Str(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))
+ if self.enable_assertion_pass_hook:
+ self.format_variables.append(name)
+ self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form))
+ return ast.Name(name, ast.Load())
+
+ def generic_visit(self, node: ast.AST) -> Tuple[ast.Name, str]:
+ """Handle expressions we don't have custom code for."""
+ assert isinstance(node, ast.expr)
+ res = self.assign(node)
+ return res, self.explanation_param(self.display(res))
+
+ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]:
+ """Return the AST statements to replace the ast.Assert instance.
+
+ This rewrites the test of an assertion to provide
+ intermediate values and replace it with an if statement which
+ raises an assertion error with a detailed explanation in case
+ the expression is false.
+ """
+ if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
+ from _pytest.warning_types import PytestAssertRewriteWarning
+ import warnings
+
+ # TODO: This assert should not be needed.
+ assert self.module_path is not None
+ warnings.warn_explicit(
+ PytestAssertRewriteWarning(
+ "assertion is always true, perhaps remove parentheses?"
+ ),
+ category=None,
+ filename=self.module_path,
+ lineno=assert_.lineno,
+ )
+
+ self.statements: List[ast.stmt] = []
+ self.variables: List[str] = []
+ self.variable_counter = itertools.count()
+
+ if self.enable_assertion_pass_hook:
+ self.format_variables: List[str] = []
+
+ self.stack: List[Dict[str, ast.expr]] = []
+ self.expl_stmts: List[ast.stmt] = []
+ self.push_format_context()
+ # Rewrite assert into a bunch of statements.
+ top_condition, explanation = self.visit(assert_.test)
+
+ 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))
+
+ # Failed
+ if assert_.msg:
+ assertmsg = self.helper("_format_assertmsg", assert_.msg)
+ gluestr = "\n>assert "
+ else:
+ assertmsg = ast.Str("")
+ gluestr = "assert "
+ err_explanation = ast.BinOp(ast.Str(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)
+ exc = ast.Call(err_name, [fmt], [])
+ raise_ = ast.Raise(exc, None)
+ statements_fail = []
+ statements_fail.extend(self.expl_stmts)
+ statements_fail.append(raise_)
+
+ # Passed
+ fmt_pass = self.helper("_format_explanation", msg)
+ orig = _get_assertion_exprs(self.source)[assert_.lineno]
+ hook_call_pass = ast.Expr(
+ self.helper(
+ "_call_assertion_pass",
+ ast.Num(assert_.lineno),
+ ast.Str(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],
+ [],
+ )
+ statements_pass = [hook_impl_test]
+
+ # Test for assertion condition
+ main_test = ast.If(negation, statements_fail, statements_pass)
+ self.statements.append(main_test)
+ if self.format_variables:
+ variables = [
+ ast.Name(name, ast.Store()) for name in self.format_variables
+ ]
+ clear_format = ast.Assign(variables, ast.NameConstant(None))
+ self.statements.append(clear_format)
+
+ else: # Original assertion rewriting
+ # Create failure message.
+ body = self.expl_stmts
+ self.statements.append(ast.If(negation, body, []))
+ if assert_.msg:
+ assertmsg = self.helper("_format_assertmsg", assert_.msg)
+ explanation = "\n>assert " + explanation
+ else:
+ assertmsg = ast.Str("")
+ explanation = "assert " + explanation
+ template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
+ msg = self.pop_format_context(template)
+ fmt = self.helper("_format_explanation", msg)
+ err_name = ast.Name("AssertionError", ast.Load())
+ exc = ast.Call(err_name, [fmt], [])
+ raise_ = ast.Raise(exc, None)
+
+ body.append(raise_)
+
+ # 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))
+ self.statements.append(clear)
+ # Fix locations (line numbers/column offsets).
+ for stmt in self.statements:
+ for node in traverse_node(stmt):
+ ast.copy_location(node, assert_)
+ return self.statements
+
+ 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])
+ 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))
+ return name, self.explanation_param(expr)
+
+ def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
+ res_var = self.variable()
+ expl_list = self.assign(ast.List([], ast.Load()))
+ app = ast.Attribute(expl_list, "append", ast.Load())
+ is_or = int(isinstance(boolop.op, ast.Or))
+ body = save = self.statements
+ fail_save = self.expl_stmts
+ levels = len(boolop.values) - 1
+ self.push_format_context()
+ # Process each operand, short-circuiting if needed.
+ for i, v in enumerate(boolop.values):
+ 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 = fail_inner
+ 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))
+ call = ast.Call(app, [expl_format], [])
+ self.expl_stmts.append(ast.Expr(call))
+ if i < levels:
+ cond: ast.expr = res
+ if is_or:
+ cond = ast.UnaryOp(ast.Not(), cond)
+ inner: List[ast.stmt] = []
+ self.statements.append(ast.If(cond, inner, []))
+ 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 = self.pop_format_context(expl_template)
+ return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
+
+ def visit_UnaryOp(self, unary: ast.UnaryOp) -> Tuple[ast.Name, str]:
+ pattern = UNARY_MAP[unary.op.__class__]
+ operand_res, operand_expl = self.visit(unary.operand)
+ res = self.assign(ast.UnaryOp(unary.op, operand_res))
+ return res, pattern % (operand_expl,)
+
+ def visit_BinOp(self, binop: ast.BinOp) -> Tuple[ast.Name, str]:
+ symbol = BINOP_MAP[binop.op.__class__]
+ left_expr, left_expl = self.visit(binop.left)
+ right_expr, right_expl = self.visit(binop.right)
+ explanation = f"({left_expl} {symbol} {right_expl})"
+ res = self.assign(ast.BinOp(left_expr, binop.op, right_expr))
+ return res, explanation
+
+ def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]:
+ new_func, func_expl = self.visit(call.func)
+ arg_expls = []
+ new_args = []
+ new_kwargs = []
+ for arg in call.args:
+ res, expl = self.visit(arg)
+ arg_expls.append(expl)
+ new_args.append(res)
+ for keyword in call.keywords:
+ res, expl = self.visit(keyword.value)
+ new_kwargs.append(ast.keyword(keyword.arg, res))
+ if keyword.arg:
+ arg_expls.append(keyword.arg + "=" + expl)
+ else: # **args have `arg` keywords with an .arg of None
+ arg_expls.append("**" + expl)
+
+ expl = "{}({})".format(func_expl, ", ".join(arg_expls))
+ new_call = ast.Call(new_func, new_args, new_kwargs)
+ res = self.assign(new_call)
+ res_expl = self.explanation_param(self.display(res))
+ outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}"
+ return res, outer_expl
+
+ def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]:
+ # A Starred node can appear in a function call.
+ res, expl = self.visit(starred.value)
+ new_starred = ast.Starred(res, starred.ctx)
+ return new_starred, "*" + expl
+
+ def visit_Attribute(self, attr: ast.Attribute) -> Tuple[ast.Name, str]:
+ if not isinstance(attr.ctx, ast.Load):
+ return self.generic_visit(attr)
+ value, value_expl = self.visit(attr.value)
+ res = self.assign(ast.Attribute(value, attr.attr, ast.Load()))
+ res_expl = self.explanation_param(self.display(res))
+ pat = "%s\n{%s = %s.%s\n}"
+ expl = pat % (res_expl, res_expl, value_expl, attr.attr)
+ return res, expl
+
+ def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
+ self.push_format_context()
+ left_res, left_expl = self.visit(comp.left)
+ if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
+ left_expl = f"({left_expl})"
+ res_variables = [self.variable() for i in range(len(comp.ops))]
+ load_names = [ast.Name(v, ast.Load()) for v in res_variables]
+ store_names = [ast.Name(v, ast.Store()) for v in res_variables]
+ it = zip(range(len(comp.ops)), comp.ops, comp.comparators)
+ expls = []
+ syms = []
+ results = [left_res]
+ for i, op, next_operand in it:
+ 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))
+ expl = f"{left_expl} {sym} {next_expl}"
+ expls.append(ast.Str(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
+ # Use pytest.assertion.util._reprcompare if that's available.
+ expl_call = self.helper(
+ "_call_reprcompare",
+ ast.Tuple(syms, ast.Load()),
+ ast.Tuple(load_names, ast.Load()),
+ ast.Tuple(expls, ast.Load()),
+ ast.Tuple(results, ast.Load()),
+ )
+ if len(comp.ops) > 1:
+ 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))
+
+
+def try_makedirs(cache_dir: Path) -> bool:
+ """Attempt to create the given directory and sub-directories exist.
+
+ Returns True if successful or if it already exists.
+ """
+ try:
+ os.makedirs(cache_dir, exist_ok=True)
+ except (FileNotFoundError, NotADirectoryError, FileExistsError):
+ # One of the path components was not a directory:
+ # - we're in a zip file
+ # - it is a file
+ return False
+ except PermissionError:
+ return False
+ except OSError as e:
+ # as of now, EROFS doesn't have an equivalent OSError-subclass
+ if e.errno == errno.EROFS:
+ return False
+ raise
+ return True
+
+
+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:
+ # given:
+ # prefix = '/tmp/pycs'
+ # path = '/home/user/proj/test_app.py'
+ # we want:
+ # '/tmp/pycs/home/user/proj'
+ return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1])
+ else:
+ # classic pycache directory
+ return file_path.parent / "__pycache__"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/truncate.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/truncate.py
new file mode 100644
index 0000000000..ce148dca09
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/truncate.py
@@ -0,0 +1,94 @@
+"""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.
+"""
+from typing import List
+from typing import Optional
+
+from _pytest.assertion import util
+from _pytest.nodes import Item
+
+
+DEFAULT_MAX_LINES = 8
+DEFAULT_MAX_CHARS = 8 * 80
+USAGE_MSG = "use '-vv' to show"
+
+
+def truncate_if_required(
+ explanation: List[str], item: Item, max_length: Optional[int] = None
+) -> List[str]:
+ """Truncate this assertion explanation if the given test item is eligible."""
+ if _should_truncate_item(item):
+ return _truncate_explanation(explanation)
+ return explanation
+
+
+def _should_truncate_item(item: Item) -> bool:
+ """Whether or not this test item is eligible for truncation."""
+ verbose = item.config.option.verbose
+ return verbose < 2 and not util.running_on_ci()
+
+
+def _truncate_explanation(
+ input_lines: List[str],
+ max_lines: Optional[int] = None,
+ max_chars: Optional[int] = None,
+) -> List[str]:
+ """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.
+ """
+
+ if max_lines is None:
+ max_lines = DEFAULT_MAX_LINES
+ if max_chars is None:
+ max_chars = DEFAULT_MAX_CHARS
+
+ # Check if truncation required
+ input_char_count = len("".join(input_lines))
+ if len(input_lines) <= max_lines and input_char_count <= max_chars:
+ return input_lines
+
+ # Truncate first to max_lines, and then truncate to max_chars if max_chars
+ # is exceeded.
+ 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] + "..."
+
+ # 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)"
+ else:
+ msg += f" ({truncated_line_count} lines hidden)"
+ msg += f", {USAGE_MSG}"
+ truncated_explanation.extend(["", str(msg)])
+ return truncated_explanation
+
+
+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):
+ if iterated_char_count + len(input_line) > max_chars:
+ break
+ iterated_char_count += len(input_line)
+
+ # Create truncated explanation with modified final line
+ truncated_result = input_lines[:iterated_index]
+ final_line = input_lines[iterated_index]
+ if final_line:
+ final_line_truncate_point = max_chars - iterated_char_count
+ final_line = final_line[:final_line_truncate_point]
+ truncated_result.append(final_line)
+ return truncated_result
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/util.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/util.py
new file mode 100644
index 0000000000..19f1089c20
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/assertion/util.py
@@ -0,0 +1,498 @@
+"""Utilities for assertion debugging."""
+import collections.abc
+import os
+import pprint
+from typing import AbstractSet
+from typing import Any
+from typing import Callable
+from typing import Iterable
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Sequence
+
+import _pytest._code
+from _pytest import outcomes
+from _pytest._io.saferepr import _pformat_dispatch
+from _pytest._io.saferepr import safeformat
+from _pytest._io.saferepr import saferepr
+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
+# DebugInterpreter.
+_reprcompare: Optional[Callable[[str, object, object], Optional[str]]] = None
+
+# Works similarly as _reprcompare attribute. Is populated with the hook call
+# when pytest_runtest_setup is called.
+_assertion_pass: Optional[Callable[[int, str, str], None]] = None
+
+# Config object which is assigned during pytest_runtest_protocol.
+_config: Optional[Config] = None
+
+
+def format_explanation(explanation: str) -> str:
+ r"""Format 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.
+ """
+ lines = _split_explanation(explanation)
+ result = _format_lines(lines)
+ return "\n".join(result)
+
+
+def _split_explanation(explanation: str) -> List[str]:
+ r"""Return a list of individual lines in the explanation.
+
+ This will return a list of lines split on '\n{', '\n}' and '\n~'.
+ Any other newlines will be escaped and appear in the line as the
+ literal '\n' characters.
+ """
+ raw_lines = (explanation or "").split("\n")
+ lines = [raw_lines[0]]
+ for values in raw_lines[1:]:
+ if values and values[0] in ["{", "}", "~", ">"]:
+ lines.append(values)
+ else:
+ lines[-1] += "\\n" + values
+ return lines
+
+
+def _format_lines(lines: Sequence[str]) -> List[str]:
+ """Format the individual lines.
+
+ This will replace the '{', '}' and '~' characters of our mini formatting
+ language with the proper 'where ...', 'and ...' and ' + ...' text, taking
+ care of indentation along the way.
+
+ Return a list of formatted lines.
+ """
+ result = list(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("}"):
+ stack.pop()
+ stackcnt.pop()
+ result[stack[-1]] += line[1:]
+ else:
+ assert line[0] in ["~", ">"]
+ stack[-1] += 1
+ indent = len(stack) if line.startswith("~") else len(stack) - 1
+ result.append(" " * indent + line[1:])
+ assert len(stack) == 1
+ return result
+
+
+def issequence(x: Any) -> bool:
+ return isinstance(x, collections.abc.Sequence) and not isinstance(x, str)
+
+
+def istext(x: Any) -> bool:
+ return isinstance(x, str)
+
+
+def isdict(x: Any) -> bool:
+ return isinstance(x, dict)
+
+
+def isset(x: Any) -> bool:
+ return isinstance(x, (set, frozenset))
+
+
+def isnamedtuple(obj: Any) -> bool:
+ return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None
+
+
+def isdatacls(obj: Any) -> bool:
+ return getattr(obj, "__dataclass_fields__", None) is not None
+
+
+def isattrs(obj: Any) -> bool:
+ return getattr(obj, "__attrs_attrs__", None) is not None
+
+
+def isiterable(obj: Any) -> bool:
+ try:
+ iter(obj)
+ return not istext(obj)
+ except TypeError:
+ return False
+
+
+def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
+ """Return specialised explanations for some operators/operands."""
+ verbose = config.getoption("verbose")
+ if verbose > 1:
+ left_repr = safeformat(left)
+ right_repr = safeformat(right)
+ 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)
+
+ summary = f"{left_repr} {op} {right_repr}"
+
+ explanation = None
+ try:
+ if op == "==":
+ explanation = _compare_eq_any(left, right, verbose)
+ elif op == "not in":
+ if istext(left) and istext(right):
+ explanation = _notin_text(left, right, verbose)
+ except outcomes.Exit:
+ raise
+ except Exception:
+ explanation = [
+ "(pytest_assertion plugin: representation of details failed: {}.".format(
+ _pytest._code.ExceptionInfo.from_current()._getreprcrash()
+ ),
+ " Probably an object has a faulty __repr__.)",
+ ]
+
+ if not explanation:
+ return None
+
+ return [summary] + explanation
+
+
+def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
+ explanation = []
+ if istext(left) and istext(right):
+ explanation = _diff_text(left, right, verbose)
+ else:
+ from _pytest.python_api import ApproxBase
+
+ if isinstance(left, ApproxBase) or isinstance(right, ApproxBase):
+ # Although the common order should be obtained == expected, this ensures both ways
+ approx_side = left if isinstance(left, ApproxBase) else right
+ other_side = right if isinstance(left, ApproxBase) else left
+
+ explanation = approx_side._repr_compare(other_side)
+ elif type(left) == 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)
+ elif issequence(left) and issequence(right):
+ explanation = _compare_eq_sequence(left, right, verbose)
+ elif isset(left) and isset(right):
+ explanation = _compare_eq_set(left, right, verbose)
+ elif isdict(left) and isdict(right):
+ explanation = _compare_eq_dict(left, right, verbose)
+ elif verbose > 0:
+ explanation = _compare_eq_verbose(left, right)
+
+ if isiterable(left) and isiterable(right):
+ expl = _compare_eq_iterable(left, right, verbose)
+ explanation.extend(expl)
+
+ return explanation
+
+
+def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
+ """Return the explanation for the diff between text.
+
+ Unless --verbose is used this will skip leading and trailing
+ characters which are identical to keep the diff minimal.
+ """
+ from difflib import ndiff
+
+ explanation: List[str] = []
+
+ if verbose < 1:
+ i = 0 # just in case left or right has zero length
+ for i in range(min(len(left), len(right))):
+ if left[i] != right[i]:
+ break
+ if i > 42:
+ i -= 10 # Provide some context
+ explanation = [
+ "Skipping %s identical leading characters in diff, use -v to show" % i
+ ]
+ left = left[i:]
+ right = right[i:]
+ if len(left) == len(right):
+ for i in range(len(left)):
+ if left[-i] != right[-i]:
+ break
+ if i > 42:
+ i -= 10 # Provide some context
+ explanation += [
+ "Skipping {} identical trailing "
+ "characters in diff, use -v to show".format(i)
+ ]
+ left = left[:-i]
+ right = right[:-i]
+ keepends = True
+ if left.isspace() or right.isspace():
+ left = repr(str(left))
+ right = repr(str(right))
+ explanation += ["Strings contain only whitespace, escaping them using repr()"]
+ # "right" is the expected base against which we compare "left",
+ # see https://github.com/pytest-dev/pytest/issues/3333
+ explanation += [
+ line.strip("\n")
+ for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
+ ]
+ 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
+) -> List[str]:
+ if not verbose and not running_on_ci():
+ return ["Use -v to get the full 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)
+
+ 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)
+ )
+ return explanation
+
+
+def _compare_eq_sequence(
+ left: Sequence[Any], right: Sequence[Any], verbose: int = 0
+) -> List[str]:
+ comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
+ explanation: List[str] = []
+ len_left = len(left)
+ len_right = len(right)
+ for i in range(min(len_left, len_right)):
+ if left[i] != right[i]:
+ if comparing_bytes:
+ # when comparing bytes, we want to see their ascii representation
+ # instead of their numeric values (#5260)
+ # using a slice gives us the ascii representation:
+ # >>> s = b'foo'
+ # >>> s[0]
+ # 102
+ # >>> s[0:1]
+ # b'f'
+ left_value = left[i : i + 1]
+ right_value = right[i : i + 1]
+ else:
+ left_value = left[i]
+ right_value = right[i]
+
+ explanation += [f"At index {i} diff: {left_value!r} != {right_value!r}"]
+ break
+
+ if comparing_bytes:
+ # when comparing bytes, it doesn't help to show the "sides contain one or more
+ # items" longer explanation, so skip it
+
+ return explanation
+
+ len_diff = len_left - len_right
+ if len_diff:
+ if len_diff > 0:
+ dir_with_more = "Left"
+ extra = saferepr(left[len_right])
+ else:
+ len_diff = 0 - len_diff
+ dir_with_more = "Right"
+ extra = saferepr(right[len_left])
+
+ if len_diff == 1:
+ explanation += [f"{dir_with_more} contains one more item: {extra}"]
+ else:
+ explanation += [
+ "%s contains %d more items, first extra item: %s"
+ % (dir_with_more, len_diff, extra)
+ ]
+ return explanation
+
+
+def _compare_eq_set(
+ left: AbstractSet[Any], right: AbstractSet[Any], 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))
+ return explanation
+
+
+def _compare_eq_dict(
+ left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0
+) -> List[str]:
+ explanation: List[str] = []
+ set_left = set(left)
+ set_right = set(right)
+ common = set_left.intersection(set_right)
+ same = {k: left[k] for k in common if left[k] == right[k]}
+ if same and verbose < 2:
+ explanation += ["Omitting %s identical items, use -vv to show" % len(same)]
+ elif same:
+ explanation += ["Common items:"]
+ explanation += 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]})]
+ extra_left = set_left - set_right
+ len_extra_left = len(extra_left)
+ if len_extra_left:
+ explanation.append(
+ "Left contains %d more item%s:"
+ % (len_extra_left, "" if len_extra_left == 1 else "s")
+ )
+ explanation.extend(
+ pprint.pformat({k: left[k] for k in extra_left}).splitlines()
+ )
+ extra_right = set_right - set_left
+ len_extra_right = len(extra_right)
+ if len_extra_right:
+ explanation.append(
+ "Right contains %d more item%s:"
+ % (len_extra_right, "" if len_extra_right == 1 else "s")
+ )
+ explanation.extend(
+ 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]:
+ if isdatacls(left):
+ all_fields = left.__dataclass_fields__
+ fields_to_check = [field for field, info in all_fields.items() 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")]
+ elif isnamedtuple(left):
+ fields_to_check = left._fields
+ else:
+ assert False
+
+ indent = " "
+ same = []
+ diff = []
+ for field in fields_to_check:
+ if getattr(left, field) == getattr(right, field):
+ same.append(field)
+ else:
+ diff.append(field)
+
+ explanation = []
+ if same or diff:
+ explanation += [""]
+ if same and verbose < 2:
+ explanation.append("Omitting %s identical items, use -vv to show" % len(same))
+ elif same:
+ explanation += ["Matching attributes:"]
+ explanation += pprint.pformat(same).splitlines()
+ if diff:
+ explanation += ["Differing attributes:"]
+ explanation += 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),
+ ]
+ explanation += [
+ indent + line
+ for line in _compare_eq_any(field_left, field_right, verbose)
+ ]
+ return explanation
+
+
+def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]:
+ index = text.find(term)
+ head = text[:index]
+ tail = text[index + len(term) :]
+ correct_text = head + tail
+ diff = _diff_text(text, correct_text, verbose)
+ newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)]
+ for line in diff:
+ if line.startswith("Skipping"):
+ continue
+ if line.startswith("- "):
+ continue
+ if line.startswith("+ "):
+ newdiff.append(" " + line[2:])
+ else:
+ newdiff.append(line)
+ return newdiff
+
+
+def running_on_ci() -> bool:
+ """Check if we're currently running on a CI system."""
+ env_vars = ["CI", "BUILD_NUMBER"]
+ return any(var in os.environ for var in env_vars)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/cacheprovider.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/cacheprovider.py
new file mode 100644
index 0000000000..681d02b409
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/cacheprovider.py
@@ -0,0 +1,580 @@
+"""Implementation of the cache provider."""
+# This plugin was not named "cache" to avoid conflicts with the external
+# pytest-cache version.
+import json
+import os
+from pathlib import Path
+from typing import Dict
+from typing import Generator
+from typing import Iterable
+from typing import List
+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
+from _pytest.config.argparsing import Parser
+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.reports import TestReport
+
+
+README_CONTENT = """\
+# pytest cache directory #
+
+This directory contains data from the pytest's cache plugin,
+which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
+
+**Do not** commit this to version control.
+
+See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
+"""
+
+CACHEDIR_TAG_CONTENT = b"""\
+Signature: 8a477f597d28d172789f06886806bc55
+# This file is a cache directory tag created by pytest.
+# For information about cache directory tags, see:
+# https://bford.info/cachedir/spec.html
+"""
+
+
+@final
+@attr.s(init=False, auto_attribs=True)
+class Cache:
+ _cachedir: Path = attr.ib(repr=False)
+ _config: Config = attr.ib(repr=False)
+
+ # Sub-directory under cache-dir for directories created by `mkdir()`.
+ _CACHE_PREFIX_DIRS = "d"
+
+ # Sub-directory under cache-dir for values created by `set()`.
+ _CACHE_PREFIX_VALUES = "v"
+
+ def __init__(
+ self, cachedir: Path, config: Config, *, _ispytest: bool = False
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._cachedir = cachedir
+ self._config = config
+
+ @classmethod
+ def for_config(cls, config: Config, *, _ispytest: bool = False) -> "Cache":
+ """Create the Cache instance for a Config.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ cachedir = cls.cache_dir_from_config(config, _ispytest=True)
+ if config.getoption("cacheclear") and cachedir.is_dir():
+ cls.clear_cache(cachedir, _ispytest=True)
+ return cls(cachedir, config, _ispytest=True)
+
+ @classmethod
+ def clear_cache(cls, cachedir: Path, _ispytest: bool = False) -> None:
+ """Clear the sub-directories used to hold cached directories and values.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
+ d = cachedir / prefix
+ if d.is_dir():
+ rm_rf(d)
+
+ @staticmethod
+ def cache_dir_from_config(config: Config, *, _ispytest: bool = False) -> Path:
+ """Get the path to the cache directory for a Config.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ return resolve_from_str(config.getini("cache_dir"), config.rootpath)
+
+ def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None:
+ """Issue a cache warning.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ import warnings
+ from _pytest.warning_types import PytestCacheWarning
+
+ warnings.warn(
+ PytestCacheWarning(fmt.format(**args) if args else fmt),
+ self._config.hook,
+ stacklevel=3,
+ )
+
+ def mkdir(self, name: str) -> Path:
+ """Return a directory path object with the given name.
+
+ If the directory does not yet exist, it will be created. You can use
+ it to manage files to e.g. store/retrieve database dumps across test
+ sessions.
+
+ .. versionadded:: 7.0
+
+ :param name:
+ Must be a string not containing a ``/`` separator.
+ Make sure the name contains your plugin or application
+ identifiers to prevent clashes with other cache users.
+ """
+ path = Path(name)
+ 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)
+ return res
+
+ def _getvaluepath(self, key: str) -> Path:
+ return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key))
+
+ def get(self, key: str, default):
+ """Return the cached value for the given key.
+
+ If no value was yet cached or the value cannot be read, the specified
+ default is returned.
+
+ :param key:
+ Must be a ``/`` separated value. Usually the first
+ name is the name of your plugin or your application.
+ :param default:
+ The value to return in case of a cache-miss or invalid cache value.
+ """
+ path = self._getvaluepath(key)
+ try:
+ with path.open("r") as f:
+ return json.load(f)
+ except (ValueError, OSError):
+ return default
+
+ def set(self, key: str, value: object) -> None:
+ """Save value for the given key.
+
+ :param key:
+ Must be a ``/`` separated value. Usually the first
+ name is the name of your plugin or your application.
+ :param value:
+ Must be of any combination of basic python types,
+ including nested types like lists of dictionaries.
+ """
+ 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)
+ return
+ if not cache_dir_exists_already:
+ self._ensure_supporting_files()
+ data = json.dumps(value, indent=2)
+ try:
+ f = path.open("w")
+ except OSError:
+ self.warn("cache could not write path {path}", path=path, _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")
+
+ cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG")
+ cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT)
+
+
+class LFPluginCollWrapper:
+ def __init__(self, lfplugin: "LFPlugin") -> None:
+ 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()
+
+ # Sort any lf-paths to the beginning.
+ lf_paths = self.lfplugin._last_failed_paths
+
+ res.result = sorted(
+ res.result,
+ # use stable sort to priorize last failed
+ key=lambda x: x.path in lf_paths,
+ reverse=True,
+ )
+ return
+
+ elif isinstance(collector, Module):
+ 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
+ self.lfplugin.config.pluginmanager.register(
+ LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
+ )
+ self._collected_at_least_one_failure = True
+
+ session = collector.session
+ result[:] = [
+ x
+ for x in result
+ if x.nodeid in lastfailed
+ # Include any passed arguments (not trivial to filter).
+ or session.isinitpath(x.path)
+ # Keep all sub-collectors.
+ or isinstance(x, nodes.Collector)
+ ]
+ return
+ yield
+
+
+class LFPluginCollSkipfiles:
+ def __init__(self, lfplugin: "LFPlugin") -> None:
+ self.lfplugin = lfplugin
+
+ @hookimpl
+ 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 collector.path not in self.lfplugin._last_failed_paths:
+ self.lfplugin._skipped_files += 1
+
+ return CollectReport(
+ collector.nodeid, "passed", longrepr=None, result=[]
+ )
+ return None
+
+
+class LFPlugin:
+ """Plugin which implements the --lf (run last-failing) option."""
+
+ def __init__(self, config: Config) -> None:
+ self.config = config
+ active_keys = "lf", "failedfirst"
+ self.active = any(config.getoption(key) for key in active_keys)
+ assert config.cache
+ self.lastfailed: Dict[str, bool] = config.cache.get("cache/lastfailed", {})
+ self._previously_failed_count: Optional[int] = None
+ self._report_status: Optional[str] = None
+ self._skipped_files = 0 # count skipped files during collection due to --lf
+
+ if config.getoption("lf"):
+ self._last_failed_paths = self.get_last_failed_paths()
+ config.pluginmanager.register(
+ LFPluginCollWrapper(self), "lfplugin-collwrapper"
+ )
+
+ def get_last_failed_paths(self) -> Set[Path]:
+ """Return a set with all Paths()s of the previously failed nodeids."""
+ rootpath = self.config.rootpath
+ result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
+ return {x for x in result if x.exists()}
+
+ def pytest_report_collectionfinish(self) -> Optional[str]:
+ if self.active and self.config.getoption("verbose") >= 0:
+ return "run-last-failure: %s" % self._report_status
+ return None
+
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
+ if (report.when == "call" and report.passed) or report.skipped:
+ self.lastfailed.pop(report.nodeid, None)
+ elif report.failed:
+ self.lastfailed[report.nodeid] = True
+
+ def pytest_collectreport(self, report: CollectReport) -> None:
+ passed = report.outcome in ("passed", "skipped")
+ if passed:
+ if report.nodeid in self.lastfailed:
+ self.lastfailed.pop(report.nodeid)
+ self.lastfailed.update((item.nodeid, True) for item in report.result)
+ else:
+ self.lastfailed[report.nodeid] = True
+
+ @hookimpl(hookwrapper=True, tryfirst=True)
+ def pytest_collection_modifyitems(
+ self, config: Config, items: List[nodes.Item]
+ ) -> Generator[None, None, None]:
+ yield
+
+ if not self.active:
+ return
+
+ if self.lastfailed:
+ previously_failed = []
+ previously_passed = []
+ for item in items:
+ if item.nodeid in self.lastfailed:
+ previously_failed.append(item)
+ else:
+ previously_passed.append(item)
+ self._previously_failed_count = len(previously_failed)
+
+ if not previously_failed:
+ # Running a subset of all tests with recorded failures
+ # only outside of it.
+ self._report_status = "%d known failures not in selected tests" % (
+ len(self.lastfailed),
+ )
+ else:
+ if self.config.getoption("lf"):
+ items[:] = previously_failed
+ config.hook.pytest_deselected(items=previously_passed)
+ else: # --failedfirst
+ items[:] = previously_failed + previously_passed
+
+ 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
+ )
+
+ 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
+ )
+ else:
+ self._report_status = "no previously failed tests, "
+ if self.config.getoption("last_failed_no_failures") == "none":
+ self._report_status += "deselecting all items."
+ config.hook.pytest_deselected(items=items[:])
+ items[:] = []
+ else:
+ self._report_status += "not deselecting items."
+
+ def pytest_sessionfinish(self, session: Session) -> None:
+ config = self.config
+ if config.getoption("cacheshow") or hasattr(config, "workerinput"):
+ return
+
+ assert config.cache is not None
+ saved_lastfailed = config.cache.get("cache/lastfailed", {})
+ if saved_lastfailed != self.lastfailed:
+ config.cache.set("cache/lastfailed", self.lastfailed)
+
+
+class NFPlugin:
+ """Plugin which implements the --nf (run new-first) option."""
+
+ def __init__(self, config: Config) -> None:
+ self.config = config
+ self.active = config.option.newfirst
+ assert config.cache is not None
+ self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
+
+ @hookimpl(hookwrapper=True, tryfirst=True)
+ def pytest_collection_modifyitems(
+ self, items: List[nodes.Item]
+ ) -> Generator[None, None, None]:
+ yield
+
+ if self.active:
+ new_items: Dict[str, nodes.Item] = {}
+ other_items: Dict[str, nodes.Item] = {}
+ for item in items:
+ if item.nodeid not in self.cached_nodeids:
+ new_items[item.nodeid] = item
+ else:
+ other_items[item.nodeid] = item
+
+ items[:] = self._get_increasing_order(
+ new_items.values()
+ ) + self._get_increasing_order(other_items.values())
+ self.cached_nodeids.update(new_items)
+ else:
+ self.cached_nodeids.update(item.nodeid for item in items)
+
+ 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]
+
+ def pytest_sessionfinish(self) -> None:
+ config = self.config
+ if config.getoption("cacheshow") or hasattr(config, "workerinput"):
+ return
+
+ if config.getoption("collectonly"):
+ return
+
+ assert config.cache is not None
+ config.cache.set("cache/nodeids", sorted(self.cached_nodeids))
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group.addoption(
+ "--lf",
+ "--last-failed",
+ action="store_true",
+ dest="lf",
+ help="rerun only the tests that failed "
+ "at the last run (or all if none failed)",
+ )
+ group.addoption(
+ "--ff",
+ "--failed-first",
+ action="store_true",
+ dest="failedfirst",
+ help="run all tests, but run the last failures first.\n"
+ "This may re-order tests and thus lead to "
+ "repeated fixture setup/teardown.",
+ )
+ group.addoption(
+ "--nf",
+ "--new-first",
+ action="store_true",
+ dest="newfirst",
+ help="run tests from new files first, then the rest of the tests "
+ "sorted by file mtime",
+ )
+ group.addoption(
+ "--cache-show",
+ action="append",
+ nargs="?",
+ dest="cacheshow",
+ help=(
+ "show cache contents, don't perform collection or tests. "
+ "Optional argument: glob (default: '*')."
+ ),
+ )
+ group.addoption(
+ "--cache-clear",
+ action="store_true",
+ dest="cacheclear",
+ 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.")
+ group.addoption(
+ "--lfnf",
+ "--last-failed-no-failures",
+ action="store",
+ dest="last_failed_no_failures",
+ choices=("all", "none"),
+ default="all",
+ help="which tests to run with no previously (known) failures.",
+ )
+
+
+def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
+ if config.option.cacheshow:
+ from _pytest.main import wrap_session
+
+ return wrap_session(config, cacheshow)
+ return None
+
+
+@hookimpl(tryfirst=True)
+def pytest_configure(config: Config) -> None:
+ config.cache = Cache.for_config(config, _ispytest=True)
+ config.pluginmanager.register(LFPlugin(config), "lfplugin")
+ config.pluginmanager.register(NFPlugin(config), "nfplugin")
+
+
+@fixture
+def cache(request: FixtureRequest) -> Cache:
+ """Return a cache object that can persist state between testing sessions.
+
+ cache.get(key, default)
+ cache.set(key, value)
+
+ Keys must be ``/`` separated strings, where the first part is usually the
+ name of your plugin or application to avoid clashes with other cache users.
+
+ Values can be any object handled by the json stdlib module.
+ """
+ assert request.config.cache is not None
+ return request.config.cache
+
+
+def pytest_report_header(config: Config) -> Optional[str]:
+ """Display cachedir with --cache-show and if non-default."""
+ if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache":
+ assert config.cache is not None
+ cachedir = config.cache._cachedir
+ # TODO: evaluate generating upward relative paths
+ # starting with .., ../.. if sensible
+
+ try:
+ displaypath = cachedir.relative_to(config.rootpath)
+ except ValueError:
+ displaypath = cachedir
+ return f"cachedir: {displaypath}"
+ return None
+
+
+def cacheshow(config: Config, session: Session) -> int:
+ from pprint import pformat
+
+ assert config.cache is not None
+
+ tw = TerminalWriter()
+ tw.line("cachedir: " + str(config.cache._cachedir))
+ if not config.cache._cachedir.is_dir():
+ tw.line("cache is empty")
+ return 0
+
+ glob = config.option.cacheshow[0]
+ if glob is None:
+ glob = "*"
+
+ dummy = object()
+ basedir = config.cache._cachedir
+ vdir = basedir / Cache._CACHE_PREFIX_VALUES
+ tw.sep("-", "cache values for %r" % glob)
+ for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()):
+ key = str(valpath.relative_to(vdir))
+ val = config.cache.get(key, dummy)
+ if val is dummy:
+ tw.line("%s contains unreadable content, will be ignored" % key)
+ else:
+ tw.line("%s contains:" % key)
+ for line in pformat(val).splitlines():
+ tw.line(" " + line)
+
+ ddir = basedir / Cache._CACHE_PREFIX_DIRS
+ if ddir.is_dir():
+ contents = sorted(ddir.rglob(glob))
+ tw.sep("-", "cache directories for %r" % glob)
+ for p in contents:
+ # if p.is_dir():
+ # print("%s/" % p.relative_to(basedir))
+ if p.is_file():
+ key = str(p.relative_to(basedir))
+ tw.line(f"{key} is a file of length {p.stat().st_size:d}")
+ return 0
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/capture.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/capture.py
new file mode 100644
index 0000000000..884f035e29
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/capture.py
@@ -0,0 +1,942 @@
+"""Per-test stdout/stderr capturing mechanism."""
+import contextlib
+import functools
+import io
+import os
+import sys
+from io import UnsupportedOperation
+from tempfile import TemporaryFile
+from typing import Any
+from typing import AnyStr
+from typing import Generator
+from typing import Generic
+from typing import Iterator
+from typing import Optional
+from typing import TextIO
+from typing import Tuple
+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
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import SubRequest
+from _pytest.nodes import Collector
+from _pytest.nodes import File
+from _pytest.nodes import Item
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group._addoption(
+ "--capture",
+ action="store",
+ default="fd",
+ metavar="method",
+ choices=["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.",
+ )
+
+
+def _colorama_workaround() -> None:
+ """Ensure colorama is imported so that it attaches to the correct stdio
+ handles on Windows.
+
+ colorama uses the terminal on import time. So if something does the
+ first import of colorama while I/O capture is active, colorama will
+ fail in various ways.
+ """
+ if sys.platform.startswith("win32"):
+ try:
+ import colorama # noqa: F401
+ except ImportError:
+ pass
+
+
+def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
+ """Workaround for Windows Unicode console handling on Python>=3.6.
+
+ Python 3.6 implemented Unicode console handling for Windows. This works
+ by reading/writing to the raw console handle using
+ ``{Read,Write}ConsoleW``.
+
+ The problem is that we are going to ``dup2`` over the stdio file
+ descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the
+ handles used by Python to write to the console. Though there is still some
+ weirdness and the console handle seems to only be closed randomly and not
+ on the first call to ``CloseHandle``, or maybe it gets reopened with the
+ same handle value when we suspend capturing.
+
+ The workaround in this case will reopen stdio with a different fd which
+ also means a different handle by replicating the logic in
+ "Py_lifecycle.c:initstdio/create_stdio".
+
+ :param stream:
+ In practice ``sys.stdout`` or ``sys.stderr``, but given
+ here as parameter for unittesting purposes.
+
+ See https://github.com/pytest-dev/py/issues/103.
+ """
+ if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"):
+ return
+
+ # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
+ if not hasattr(stream, "buffer"): # type: ignore[unreachable]
+ return
+
+ buffered = hasattr(stream.buffer, "raw")
+ raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined]
+
+ if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined]
+ return
+
+ def _reopen_stdio(f, mode):
+ if not buffered and mode[0] == "w":
+ buffering = 0
+ else:
+ buffering = -1
+
+ return io.TextIOWrapper(
+ open(os.dup(f.fileno()), mode, buffering), # type: ignore[arg-type]
+ f.encoding,
+ f.errors,
+ f.newlines,
+ f.line_buffering,
+ )
+
+ sys.stdin = _reopen_stdio(sys.stdin, "rb")
+ sys.stdout = _reopen_stdio(sys.stdout, "wb")
+ sys.stderr = _reopen_stdio(sys.stderr, "wb")
+
+
+@hookimpl(hookwrapper=True)
+def pytest_load_initial_conftests(early_config: Config):
+ ns = early_config.known_args_namespace
+ if ns.capture == "fd":
+ _py36_windowsconsoleio_workaround(sys.stdout)
+ _colorama_workaround()
+ pluginmanager = early_config.pluginmanager
+ capman = CaptureManager(ns.capture)
+ pluginmanager.register(capman, "capturemanager")
+
+ # Make sure that capturemanager is properly reset at final shutdown.
+ early_config.add_cleanup(capman.stop_global_capturing)
+
+ # 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:
+ out, err = capman.read_global_capture()
+ sys.stdout.write(out)
+ sys.stderr.write(err)
+
+
+# IO Helpers.
+
+
+class EncodedFile(io.TextIOWrapper):
+ __slots__ = ()
+
+ @property
+ def name(self) -> str:
+ # Ensure that file.name is a string. Workaround for a Python bug
+ # fixed in >=3.7.4: https://bugs.python.org/issue36015
+ return repr(self.buffer)
+
+ @property
+ def mode(self) -> str:
+ # TextIOWrapper doesn't expose a mode, but at least some of our
+ # tests check it.
+ return self.buffer.mode.replace("b", "")
+
+
+class CaptureIO(io.TextIOWrapper):
+ def __init__(self) -> None:
+ super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True)
+
+ def getvalue(self) -> str:
+ assert isinstance(self.buffer, io.BytesIO)
+ return self.buffer.getvalue().decode("UTF-8")
+
+
+class TeeCaptureIO(CaptureIO):
+ def __init__(self, other: TextIO) -> None:
+ self._other = other
+ super().__init__()
+
+ def write(self, s: str) -> int:
+ super().write(s)
+ return self._other.write(s)
+
+
+class DontReadFromInput:
+ encoding = None
+
+ def read(self, *args):
+ raise OSError(
+ "pytest: reading from stdin while output is captured! Consider using `-s`."
+ )
+
+ readline = read
+ readlines = read
+ __next__ = read
+
+ def __iter__(self):
+ return self
+
+ def fileno(self) -> int:
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()")
+
+ def isatty(self) -> bool:
+ return False
+
+ def close(self) -> None:
+ pass
+
+ @property
+ def buffer(self):
+ return self
+
+
+# Capture classes.
+
+
+patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
+
+
+class NoCapture:
+ EMPTY_BUFFER = None
+ __init__ = start = done = suspend = resume = lambda *args: None
+
+
+class SysCaptureBinary:
+
+ EMPTY_BUFFER = b""
+
+ def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None:
+ name = patchsysdict[fd]
+ self._old = getattr(sys, name)
+ self.name = name
+ if tmpfile is None:
+ if name == "stdin":
+ tmpfile = DontReadFromInput()
+ else:
+ tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old)
+ self.tmpfile = tmpfile
+ self._state = "initialized"
+
+ def repr(self, class_name: str) -> str:
+ return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
+ class_name,
+ self.name,
+ hasattr(self, "_old") and repr(self._old) or "<UNSET>",
+ self._state,
+ self.tmpfile,
+ )
+
+ def __repr__(self) -> str:
+ return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
+ self.__class__.__name__,
+ self.name,
+ hasattr(self, "_old") and repr(self._old) or "<UNSET>",
+ self._state,
+ self.tmpfile,
+ )
+
+ def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
+ assert (
+ self._state in states
+ ), "cannot {} in state {!r}: expected one of {}".format(
+ op, self._state, ", ".join(states)
+ )
+
+ def start(self) -> None:
+ self._assert_state("start", ("initialized",))
+ 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":
+ return
+ setattr(sys, self.name, self._old)
+ del self._old
+ self.tmpfile.close()
+ self._state = "done"
+
+ def suspend(self) -> None:
+ self._assert_state("suspend", ("started", "suspended"))
+ setattr(sys, self.name, self._old)
+ self._state = "suspended"
+
+ def resume(self) -> None:
+ self._assert_state("resume", ("started", "suspended"))
+ if self._state == "started":
+ return
+ setattr(sys, self.name, self.tmpfile)
+ self._state = "started"
+
+ def writeorg(self, data) -> 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]
+
+ def snap(self):
+ res = self.tmpfile.getvalue()
+ self.tmpfile.seek(0)
+ self.tmpfile.truncate()
+ return res
+
+ def writeorg(self, data):
+ 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""
+
+ def __init__(self, targetfd: int) -> None:
+ self.targetfd = targetfd
+
+ try:
+ os.fstat(targetfd)
+ except OSError:
+ # FD capturing is conceptually simple -- create a temporary file,
+ # redirect the FD to it, redirect back when done. But when the
+ # target FD is invalid it throws a wrench into this lovely scheme.
+ #
+ # Tests themselves shouldn't care if the FD is valid, FD capturing
+ # should work regardless of external circumstances. So falling back
+ # to just sys capturing is not a good option.
+ #
+ # Further complications are the need to support suspend() and the
+ # possibility of FD reuse (e.g. the tmpfile getting the very same
+ # target FD). The following approach is robust, I believe.
+ self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR)
+ os.dup2(self.targetfd_invalid, targetfd)
+ else:
+ self.targetfd_invalid = None
+ self.targetfd_save = os.dup(targetfd)
+
+ if targetfd == 0:
+ self.tmpfile = open(os.devnull)
+ self.syscapture = SysCapture(targetfd)
+ else:
+ self.tmpfile = EncodedFile(
+ TemporaryFile(buffering=0),
+ encoding="utf-8",
+ errors="replace",
+ newline="",
+ write_through=True,
+ )
+ if targetfd in patchsysdict:
+ self.syscapture = SysCapture(targetfd, self.tmpfile)
+ else:
+ self.syscapture = NoCapture()
+
+ 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,
+ )
+
+ def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
+ assert (
+ self._state in states
+ ), "cannot {} in state {!r}: expected one of {}".format(
+ op, self._state, ", ".join(states)
+ )
+
+ def start(self) -> None:
+ """Start capturing on targetfd using memorized tmpfile."""
+ self._assert_state("start", ("initialized",))
+ os.dup2(self.tmpfile.fileno(), self.targetfd)
+ 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."""
+ self._assert_state("done", ("initialized", "started", "suspended", "done"))
+ if self._state == "done":
+ return
+ os.dup2(self.targetfd_save, self.targetfd)
+ os.close(self.targetfd_save)
+ if self.targetfd_invalid is not None:
+ if self.targetfd_invalid != self.targetfd:
+ os.close(self.targetfd)
+ os.close(self.targetfd_invalid)
+ self.syscapture.done()
+ self.tmpfile.close()
+ self._state = "done"
+
+ def suspend(self) -> None:
+ self._assert_state("suspend", ("started", "suspended"))
+ if self._state == "suspended":
+ return
+ self.syscapture.suspend()
+ os.dup2(self.targetfd_save, self.targetfd)
+ self._state = "suspended"
+
+ def resume(self) -> None:
+ self._assert_state("resume", ("started", "suspended"))
+ if self._state == "started":
+ return
+ self.syscapture.resume()
+ os.dup2(self.tmpfile.fileno(), self.targetfd)
+ self._state = "started"
+
+ def writeorg(self, data):
+ """Write to original file descriptor."""
+ self._assert_state("writeorg", ("started", "suspended"))
+ os.write(self.targetfd_save, data)
+
+
+class FDCapture(FDCaptureBinary):
+ """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
+
+ def snap(self):
+ self._assert_state("snap", ("started", "suspended"))
+ self.tmpfile.seek(0)
+ res = self.tmpfile.read()
+ self.tmpfile.seek(0)
+ self.tmpfile.truncate()
+ return res
+
+ def writeorg(self, data):
+ """Write to original file descriptor."""
+ super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream
+
+
+# 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)
+
+ def index(self, value) -> int:
+ return tuple(self).index(value)
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, (CaptureResult, tuple)):
+ return NotImplemented
+ return tuple(self) == tuple(other)
+
+ def __hash__(self) -> int:
+ return hash(tuple(self))
+
+ def __lt__(self, other: object) -> bool:
+ if not isinstance(other, (CaptureResult, tuple)):
+ return NotImplemented
+ return tuple(self) < tuple(other)
+
+ def __repr__(self) -> str:
+ return f"CaptureResult(out={self.out!r}, err={self.err!r})"
+
+
+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 __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,
+ )
+
+ def start_capturing(self) -> None:
+ self._state = "started"
+ if self.in_:
+ self.in_.start()
+ if self.out:
+ self.out.start()
+ if self.err:
+ self.err.start()
+
+ def pop_outerr_to_orig(self) -> Tuple[AnyStr, AnyStr]:
+ """Pop current snapshot out/err capture and flush to orig streams."""
+ out, err = self.readouterr()
+ if out:
+ self.out.writeorg(out)
+ if err:
+ self.err.writeorg(err)
+ return out, err
+
+ def suspend_capturing(self, in_: bool = False) -> None:
+ self._state = "suspended"
+ if self.out:
+ self.out.suspend()
+ if self.err:
+ self.err.suspend()
+ if in_ and self.in_:
+ self.in_.suspend()
+ self._in_suspended = True
+
+ def resume_capturing(self) -> None:
+ self._state = "started"
+ if self.out:
+ self.out.resume()
+ if self.err:
+ self.err.resume()
+ if self._in_suspended:
+ self.in_.resume()
+ self._in_suspended = False
+
+ def stop_capturing(self) -> None:
+ """Stop capturing and reset capturing streams."""
+ if self._state == "stopped":
+ raise ValueError("was already stopped")
+ self._state = "stopped"
+ if self.out:
+ self.out.done()
+ if self.err:
+ self.err.done()
+ if self.in_:
+ self.in_.done()
+
+ def is_started(self) -> bool:
+ """Whether actively capturing -- not suspended or stopped."""
+ return self._state == "started"
+
+ 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)
+
+
+def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
+ if method == "fd":
+ return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
+ elif method == "sys":
+ return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2))
+ elif method == "no":
+ return MultiCapture(in_=None, out=None, err=None)
+ elif method == "tee-sys":
+ return MultiCapture(
+ in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True)
+ )
+ raise ValueError(f"unknown capturing method: {method!r}")
+
+
+# CaptureManager and CaptureFixture
+
+
+class CaptureManager:
+ """The capture plugin.
+
+ Manages that the appropriate capture method is enabled/disabled during
+ collection and each test phase (setup, call, teardown). After each of
+ those points, the captured output is obtained and attached to the
+ collection/runtest report.
+
+ There are two levels of capture:
+
+ * global: enabled by default and can be suppressed by the ``-s``
+ option. This is always enabled/disabled during collection and each test
+ phase.
+
+ * fixture: when a test function or one of its fixture depend on the
+ ``capsys`` or ``capfd`` fixtures. In this case special handling is
+ needed to ensure the fixtures take precedence over the global capture.
+ """
+
+ def __init__(self, method: "_CaptureMethod") -> None:
+ self._method = 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
+ )
+
+ def is_capturing(self) -> Union[str, bool]:
+ if self.is_globally_capturing():
+ return "global"
+ if self._capture_fixture:
+ return "fixture %s" % self._capture_fixture.request.fixturename
+ return False
+
+ # Global capturing control
+
+ def is_globally_capturing(self) -> bool:
+ return self._method != "no"
+
+ def start_global_capturing(self) -> None:
+ assert self._global_capturing is None
+ self._global_capturing = _get_multicapture(self._method)
+ self._global_capturing.start_capturing()
+
+ def stop_global_capturing(self) -> None:
+ if self._global_capturing is not None:
+ self._global_capturing.pop_outerr_to_orig()
+ self._global_capturing.stop_capturing()
+ self._global_capturing = None
+
+ def resume_global_capture(self) -> None:
+ # During teardown of the python process, and on rare occasions, capture
+ # attributes can be `None` while trying to resume global capture.
+ if self._global_capturing is not None:
+ self._global_capturing.resume_capturing()
+
+ def suspend_global_capture(self, in_: bool = False) -> None:
+ if self._global_capturing is not None:
+ self._global_capturing.suspend_capturing(in_=in_)
+
+ def suspend(self, in_: bool = False) -> None:
+ # Need to undo local capsys-et-al if it exists before disabling global capture.
+ self.suspend_fixture()
+ self.suspend_global_capture(in_)
+
+ def resume(self) -> None:
+ self.resume_global_capture()
+ self.resume_fixture()
+
+ def read_global_capture(self) -> CaptureResult[str]:
+ assert self._global_capturing is not None
+ return self._global_capturing.readouterr()
+
+ # Fixture Control
+
+ def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None:
+ if self._capture_fixture:
+ 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
+ )
+ )
+ self._capture_fixture = capture_fixture
+
+ def unset_fixture(self) -> None:
+ self._capture_fixture = None
+
+ def activate_fixture(self) -> None:
+ """If the current item is using ``capsys`` or ``capfd``, activate
+ them so they take precedence over the global capture."""
+ if self._capture_fixture:
+ self._capture_fixture._start()
+
+ def deactivate_fixture(self) -> None:
+ """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any."""
+ if self._capture_fixture:
+ self._capture_fixture.close()
+
+ def suspend_fixture(self) -> None:
+ if self._capture_fixture:
+ self._capture_fixture._suspend()
+
+ def resume_fixture(self) -> None:
+ if self._capture_fixture:
+ self._capture_fixture._resume()
+
+ # Helper context managers
+
+ @contextlib.contextmanager
+ def global_and_fixture_disabled(self) -> Generator[None, None, None]:
+ """Context manager to temporarily disable global and current fixture capturing."""
+ do_fixture = self._capture_fixture and self._capture_fixture._is_started()
+ if do_fixture:
+ self.suspend_fixture()
+ do_global = self._global_capturing and self._global_capturing.is_started()
+ if do_global:
+ self.suspend_global_capture()
+ try:
+ yield
+ finally:
+ if do_global:
+ self.resume_global_capture()
+ if do_fixture:
+ self.resume_fixture()
+
+ @contextlib.contextmanager
+ def item_capture(self, when: str, item: Item) -> Generator[None, None, None]:
+ self.resume_global_capture()
+ self.activate_fixture()
+ try:
+ yield
+ finally:
+ 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)
+
+ # Hooks
+
+ @hookimpl(hookwrapper=True)
+ def pytest_make_collect_report(self, collector: Collector):
+ if isinstance(collector, File):
+ self.resume_global_capture()
+ outcome = yield
+ 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
+
+ @hookimpl(hookwrapper=True)
+ def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:
+ with self.item_capture("setup", item):
+ yield
+
+ @hookimpl(hookwrapper=True)
+ def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
+ with self.item_capture("call", item):
+ yield
+
+ @hookimpl(hookwrapper=True)
+ def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
+ with self.item_capture("teardown", item):
+ yield
+
+ @hookimpl(tryfirst=True)
+ def pytest_keyboard_interrupt(self) -> None:
+ self.stop_global_capturing()
+
+ @hookimpl(tryfirst=True)
+ def pytest_internalerror(self) -> None:
+ self.stop_global_capturing()
+
+
+class CaptureFixture(Generic[AnyStr]):
+ """Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`,
+ :fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
+
+ def __init__(
+ self, captureclass, request: SubRequest, *, _ispytest: bool = False
+ ) -> None:
+ check_ispytest(_ispytest)
+ self.captureclass = captureclass
+ self.request = request
+ self._capture: Optional[MultiCapture[AnyStr]] = None
+ self._captured_out = self.captureclass.EMPTY_BUFFER
+ self._captured_err = self.captureclass.EMPTY_BUFFER
+
+ def _start(self) -> None:
+ if self._capture is None:
+ self._capture = MultiCapture(
+ in_=None,
+ out=self.captureclass(1),
+ err=self.captureclass(2),
+ )
+ self._capture.start_capturing()
+
+ def close(self) -> None:
+ if self._capture is not None:
+ out, err = self._capture.pop_outerr_to_orig()
+ self._captured_out += out
+ self._captured_err += err
+ self._capture.stop_capturing()
+ self._capture = None
+
+ def readouterr(self) -> CaptureResult[AnyStr]:
+ """Read and return the captured output so far, resetting the internal
+ buffer.
+
+ :returns:
+ The captured content as a namedtuple with ``out`` and ``err``
+ string attributes.
+ """
+ captured_out, captured_err = self._captured_out, self._captured_err
+ if self._capture is not None:
+ out, err = self._capture.readouterr()
+ captured_out += out
+ captured_err += err
+ self._captured_out = self.captureclass.EMPTY_BUFFER
+ self._captured_err = self.captureclass.EMPTY_BUFFER
+ return CaptureResult(captured_out, captured_err)
+
+ def _suspend(self) -> None:
+ """Suspend this fixture's own capturing temporarily."""
+ if self._capture is not None:
+ self._capture.suspend_capturing()
+
+ def _resume(self) -> None:
+ """Resume this fixture's own capturing temporarily."""
+ if self._capture is not None:
+ self._capture.resume_capturing()
+
+ def _is_started(self) -> bool:
+ """Whether actively capturing -- not disabled or closed."""
+ if self._capture is not None:
+ return self._capture.is_started()
+ return False
+
+ @contextlib.contextmanager
+ def disabled(self) -> Generator[None, None, None]:
+ """Temporarily disable capturing while inside the ``with`` block."""
+ capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
+ with capmanager.global_and_fixture_disabled():
+ yield
+
+
+# The fixtures.
+
+
+@fixture
+def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
+ """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.
+ """
+ capman = request.config.pluginmanager.getplugin("capturemanager")
+ capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True)
+ capman.set_fixture(capture_fixture)
+ capture_fixture._start()
+ yield capture_fixture
+ capture_fixture.close()
+ capman.unset_fixture()
+
+
+@fixture
+def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
+ """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.
+ """
+ capman = request.config.pluginmanager.getplugin("capturemanager")
+ capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True)
+ capman.set_fixture(capture_fixture)
+ capture_fixture._start()
+ yield capture_fixture
+ capture_fixture.close()
+ capman.unset_fixture()
+
+
+@fixture
+def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
+ """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.
+ """
+ capman = request.config.pluginmanager.getplugin("capturemanager")
+ capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True)
+ capman.set_fixture(capture_fixture)
+ capture_fixture._start()
+ yield capture_fixture
+ capture_fixture.close()
+ capman.unset_fixture()
+
+
+@fixture
+def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
+ """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.
+ """
+ capman = request.config.pluginmanager.getplugin("capturemanager")
+ capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True)
+ capman.set_fixture(capture_fixture)
+ capture_fixture._start()
+ yield capture_fixture
+ capture_fixture.close()
+ capman.unset_fixture()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/compat.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/compat.py
new file mode 100644
index 0000000000..7703dee8c5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/compat.py
@@ -0,0 +1,417 @@
+"""Python version compatibility code."""
+import enum
+import functools
+import inspect
+import os
+import sys
+from contextlib import contextmanager
+from inspect import Parameter
+from inspect import signature
+from pathlib import Path
+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
+
+
+_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
+
+# fmt: off
+# intentional space to create a fake difference for the verification
+LEGACY_PATH = py.path. local
+# fmt: on
+
+
+def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
+ """Internal wrapper to prepare lazy proxies for legacy_path instances"""
+ return LEGACY_PATH(path)
+
+
+# fmt: off
+# Singleton type for NOTSET, as described in:
+# 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
+# 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)
+ return genfunc and not iscoroutinefunction(func)
+
+
+def iscoroutinefunction(func: object) -> bool:
+ """Return True if func is a coroutine function (a function defined with async
+ 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
+ importing asyncio directly, which in turns also initializes the "logging"
+ module as a side-effect (see issue #8).
+ """
+ return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)
+
+
+def is_async_function(func: object) -> bool:
+ """Return True if the given function seems to be an async function or
+ an async generator."""
+ return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
+
+
+def getlocation(function, curdir: Optional[str] = None) -> str:
+ function = get_real_func(function)
+ fn = Path(inspect.getfile(function))
+ lineno = function.__code__.co_firstlineno
+ if curdir is not None:
+ try:
+ relfn = fn.relative_to(curdir)
+ except ValueError:
+ pass
+ else:
+ return "%s:%d" % (relfn, lineno + 1)
+ return "%s:%d" % (fn, lineno + 1)
+
+
+def num_mock_patch_args(function) -> int:
+ """Return number of arguments used up by mock arguments (if any)."""
+ patchings = getattr(function, "patchings", None)
+ if not patchings:
+ return 0
+
+ mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object())
+ ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object())
+
+ return len(
+ [
+ p
+ for p in patchings
+ if not p.attribute_name
+ and (p.new is mock_sentinel or p.new is ut_mock_sentinel)
+ ]
+ )
+
+
+def getfuncargnames(
+ function: Callable[..., Any],
+ *,
+ name: str = "",
+ is_method: bool = False,
+ cls: Optional[type] = None,
+) -> Tuple[str, ...]:
+ """Return the names of a function's mandatory arguments.
+
+ Should return the names of all function arguments that:
+ * Aren't bound to an instance or type as in instance or class methods.
+ * Don't have default values.
+ * 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 name parameter should be the original name in which the function was collected.
+ """
+ # TODO(RonnyPfannschmidt): This function should be refactored when we
+ # revisit fixtures. The fixture mechanism should ask the node for
+ # the fixture names, and not try to obtain directly from the
+ # function object well after collection has occurred.
+
+ # The parameters attribute of a Signature object contains an
+ # ordered mapping of parameter names to Parameter instances. This
+ # creates a tuple of the names of the parameters that don't have
+ # defaults.
+ try:
+ parameters = signature(function).parameters
+ except (ValueError, TypeError) as e:
+ from _pytest.outcomes import fail
+
+ fail(
+ f"Could not determine arguments of {function!r}: {e}",
+ pytrace=False,
+ )
+
+ arg_names = tuple(
+ p.name
+ for p in parameters.values()
+ if (
+ p.kind is Parameter.POSITIONAL_OR_KEYWORD
+ or p.kind is Parameter.KEYWORD_ONLY
+ )
+ and p.default is Parameter.empty
+ )
+ if not name:
+ name = function.__name__
+
+ # 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 (
+ # 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
+ and not isinstance(
+ inspect.getattr_static(cls, name, default=None), staticmethod
+ )
+ ):
+ arg_names = arg_names[1:]
+ # Remove any names that will be replaced with mocks.
+ if hasattr(function, "__wrapped__"):
+ arg_names = arg_names[num_mock_patch_args(function) :]
+ 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, ...]:
+ # 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.
+ return tuple(
+ p.name
+ for p in signature(function).parameters.values()
+ if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
+ and p.default is not Parameter.empty
+ )
+
+
+_non_printable_ascii_translate_table = {
+ i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127)
+}
+_non_printable_ascii_translate_table.update(
+ {ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"}
+)
+
+
+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:
+ 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.:
+
+ r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
+
+ Note:
+ The obvious "v.decode('unicode-escape')" will return
+ valid UTF-8 unicode if it finds them in bytes, but we
+ want to return escaped bytes for any byte, even if they match
+ a UTF-8 string.
+ """
+ if isinstance(val, bytes):
+ ret = _bytes_to_ascii(val)
+ else:
+ ret = val.encode("unicode_escape").decode("ascii")
+ return _translate_non_printable(ret)
+
+
+@attr.s
+class _PytestWrapper:
+ """Dummy wrapper around a function object for internal use only.
+
+ Used to correctly unwrap the underlying function object when we are
+ creating fixtures, because we wrap the function object ourselves with a
+ decorator to issue warnings when the fixture function is called directly.
+ """
+
+ obj = attr.ib()
+
+
+def get_real_func(obj):
+ """Get the real function object of the (possibly) wrapped object by
+ functools.wraps or functools.partial."""
+ start_obj = obj
+ for i in range(100):
+ # __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function
+ # to trigger a warning if it gets called directly instead of by pytest: we don't
+ # want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774)
+ new_obj = getattr(obj, "__pytest_wrapped__", None)
+ if isinstance(new_obj, _PytestWrapper):
+ obj = new_obj.obj
+ break
+ new_obj = getattr(obj, "__wrapped__", None)
+ if new_obj is None:
+ break
+ obj = new_obj
+ else:
+ 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)
+ )
+ )
+ if isinstance(obj, functools.partial):
+ obj = obj.func
+ return obj
+
+
+def get_real_method(obj, holder):
+ """Attempt to obtain the real function object that might be wrapping
+ ``obj``, while at the same time returning a bound method to ``holder`` if
+ the original object was a bound method."""
+ try:
+ is_method = hasattr(obj, "__func__")
+ obj = get_real_func(obj)
+ except Exception: # pragma: no cover
+ return obj
+ if is_method and hasattr(obj, "__get__") and callable(obj.__get__):
+ obj = obj.__get__(holder)
+ return obj
+
+
+def getimfunc(func):
+ try:
+ return func.__func__
+ except AttributeError:
+ return func
+
+
+def safe_getattr(object: Any, name: str, default: Any) -> Any:
+ """Like getattr but return default upon any Exception or any OutcomeException.
+
+ Attribute access can potentially fail for 'evil' Python objects.
+ See issue #214.
+ It catches OutcomeException because of #2490 (issue #580), new outcomes
+ are derived from BaseException instead of Exception (for more details
+ check #2707).
+ """
+ from _pytest.outcomes import TEST_OUTCOME
+
+ try:
+ return getattr(object, name, default)
+ except TEST_OUTCOME:
+ return default
+
+
+def safe_isclass(obj: object) -> bool:
+ """Ignore any exception via isinstance on Python 3."""
+ try:
+ return inspect.isclass(obj)
+ except Exception:
+ 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__
+
+ @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
+
+
+# Perform exhaustiveness checking.
+#
+# Consider this example:
+#
+# MyUnion = Union[int, str]
+#
+# def handle(x: MyUnion) -> int {
+# if isinstance(x, int):
+# return 1
+# elif isinstance(x, str):
+# return 2
+# else:
+# raise Exception('unreachable')
+#
+# Now suppose we add a new variant:
+#
+# MyUnion = Union[int, str, bytes]
+#
+# After doing this, we must remember ourselves to go and update the handle
+# function to handle the new variant.
+#
+# With `assert_never` we can do better:
+#
+# // raise Exception('unreachable')
+# return assert_never(x)
+#
+# Now, if we forget to handle the new variant, the type-checker will emit a
+# compile-time error, instead of the runtime error we would have gotten
+# previously.
+#
+# This also work for Enums (if you use `is` to compare) and Literals.
+def assert_never(value: "NoReturn") -> "NoReturn":
+ assert False, f"Unhandled value: {value} ({type(value).__name__})"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/__init__.py
new file mode 100644
index 0000000000..ebf6e1b950
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/__init__.py
@@ -0,0 +1,1697 @@
+"""Command line options, ini-file and conftest.py processing."""
+import argparse
+import collections.abc
+import contextlib
+import copy
+import enum
+import inspect
+import os
+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
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import Generator
+from typing import IO
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import TextIO
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+import attr
+from pluggy import HookimplMarker
+from pluggy import HookspecMarker
+from pluggy import PluginManager
+
+import _pytest._code
+import _pytest.deprecated
+import _pytest.hookspec
+from .exceptions import PrintHelp as PrintHelp
+from .exceptions import UsageError as UsageError
+from .findpaths import determine_setup
+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
+from _pytest.outcomes import fail
+from _pytest.outcomes import Skipped
+from _pytest.pathlib import absolutepath
+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.stash import Stash
+from _pytest.warning_types import PytestConfigWarning
+
+if TYPE_CHECKING:
+
+ from _pytest._code.code import _TracebackStyle
+ from _pytest.terminal import TerminalReporter
+ from .argparsing import Argument
+
+
+_PluggyPlugin = object
+"""A type to represent plugin objects.
+
+Plugins can be any namespace, so we can't narrow it down much, but we use an
+alias to make the intent clear.
+
+Ideally this type would be provided by pluggy itself.
+"""
+
+
+hookimpl = HookimplMarker("pytest")
+hookspec = HookspecMarker("pytest")
+
+
+@final
+class ExitCode(enum.IntEnum):
+ """Encodes the valid exit codes by pytest.
+
+ Currently users and plugins may supply other exit codes as well.
+
+ .. versionadded:: 5.0
+ """
+
+ #: Tests passed.
+ OK = 0
+ #: Tests failed.
+ TESTS_FAILED = 1
+ #: pytest was interrupted.
+ INTERRUPTED = 2
+ #: An internal error got in the way.
+ INTERNAL_ERROR = 3
+ #: pytest was misused.
+ USAGE_ERROR = 4
+ #: pytest couldn't find tests.
+ NO_TESTS_COLLECTED = 5
+
+
+class ConftestImportFailure(Exception):
+ def __init__(
+ self,
+ path: Path,
+ excinfo: Tuple[Type[Exception], Exception, TracebackType],
+ ) -> None:
+ super().__init__(path, excinfo)
+ self.path = path
+ self.excinfo = excinfo
+
+ def __str__(self) -> str:
+ return "{}: {} (from {})".format(
+ self.excinfo[0].__name__, self.excinfo[1], self.path
+ )
+
+
+def filter_traceback_for_conftest_import_failure(
+ entry: _pytest._code.TracebackEntry,
+) -> bool:
+ """Filter tracebacks entries which point to pytest internals or importlib.
+
+ Make a special case for importlib because we use it to import test modules and conftest files
+ in _pytest.pathlib.import_path.
+ """
+ return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep)
+
+
+def main(
+ args: Optional[Union[List[str], "os.PathLike[str]"]] = None,
+ plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
+) -> Union[int, ExitCode]:
+ """Perform an in-process test run.
+
+ :param args: List of command line arguments.
+ :param plugins: List of plugin objects to be auto-registered during initialization.
+
+ :returns: An exit code.
+ """
+ try:
+ try:
+ config = _prepareconfig(args, plugins)
+ except ConftestImportFailure as e:
+ exc_info = ExceptionInfo.from_exc_info(e.excinfo)
+ tw = TerminalWriter(sys.stderr)
+ tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
+ exc_info.traceback = exc_info.traceback.filter(
+ filter_traceback_for_conftest_import_failure
+ )
+ exc_repr = (
+ exc_info.getrepr(style="short", chain=False)
+ if exc_info.traceback
+ else exc_info.exconly()
+ )
+ formatted_tb = str(exc_repr)
+ for line in formatted_tb.splitlines():
+ tw.line(line.rstrip(), red=True)
+ return ExitCode.USAGE_ERROR
+ else:
+ try:
+ ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main(
+ config=config
+ )
+ try:
+ return ExitCode(ret)
+ except ValueError:
+ return ret
+ finally:
+ config._ensure_unconfigure()
+ except UsageError as e:
+ tw = TerminalWriter(sys.stderr)
+ for msg in e.args:
+ tw.line(f"ERROR: {msg}\n", red=True)
+ return ExitCode.USAGE_ERROR
+
+
+def console_main() -> int:
+ """The CLI entry point of pytest.
+
+ This function is not meant for programmable use; use `main()` instead.
+ """
+ # https://docs.python.org/3/library/signal.html#note-on-sigpipe
+ try:
+ code = main()
+ sys.stdout.flush()
+ return code
+ except BrokenPipeError:
+ # Python flushes standard streams on exit; redirect remaining output
+ # to devnull to avoid another BrokenPipeError at shutdown
+ devnull = os.open(os.devnull, os.O_WRONLY)
+ os.dup2(devnull, sys.stdout.fileno())
+ return 1 # Python exits with error code 1 on EPIPE
+
+
+class cmdline: # compatibility namespace
+ main = staticmethod(main)
+
+
+def filename_arg(path: str, optname: str) -> str:
+ """Argparse type validator for filename arguments.
+
+ :path: Path of filename.
+ :optname: Name of the option.
+ """
+ if os.path.isdir(path):
+ raise UsageError(f"{optname} must be a filename, given: {path}")
+ return path
+
+
+def directory_arg(path: str, optname: str) -> str:
+ """Argparse type validator for directory arguments.
+
+ :path: Path of directory.
+ :optname: Name of the option.
+ """
+ if not os.path.isdir(path):
+ raise UsageError(f"{optname} must be a directory, given: {path}")
+ return path
+
+
+# Plugins that cannot be disabled via "-p no:X" currently.
+essential_plugins = (
+ "mark",
+ "main",
+ "runner",
+ "fixtures",
+ "helpconfig", # Provides -p.
+)
+
+default_plugins = essential_plugins + (
+ "python",
+ "terminal",
+ "debugging",
+ "unittest",
+ "capture",
+ "skipping",
+ "legacypath",
+ "tmpdir",
+ "monkeypatch",
+ "recwarn",
+ "pastebin",
+ "nose",
+ "assertion",
+ "junitxml",
+ "doctest",
+ "cacheprovider",
+ "freeze_support",
+ "setuponly",
+ "setupplan",
+ "stepwise",
+ "warnings",
+ "logging",
+ "reports",
+ "python_path",
+ *(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []),
+ "faulthandler",
+)
+
+builtin_plugins = set(default_plugins)
+builtin_plugins.add("pytester")
+builtin_plugins.add("pytester_assertions")
+
+
+def get_config(
+ args: Optional[List[str]] = None,
+ plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
+) -> "Config":
+ # subsequent calls to main will create a fresh instance
+ pluginmanager = PytestPluginManager()
+ config = Config(
+ pluginmanager,
+ invocation_params=Config.InvocationParams(
+ args=args or (),
+ plugins=plugins,
+ dir=Path.cwd(),
+ ),
+ )
+
+ if args is not None:
+ # Handle any "-p no:plugin" args.
+ pluginmanager.consider_preparse(args, exclude_only=True)
+
+ for spec in default_plugins:
+ pluginmanager.import_plugin(spec)
+
+ return config
+
+
+def get_plugin_manager() -> "PytestPluginManager":
+ """Obtain a new instance of the
+ :py:class:`pytest.PytestPluginManager`, with default plugins
+ already loaded.
+
+ This function can be used by integration with other tools, like hooking
+ into pytest to run tests into an IDE.
+ """
+ return get_config().pluginmanager
+
+
+def _prepareconfig(
+ args: Optional[Union[List[str], "os.PathLike[str]"]] = None,
+ plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
+) -> "Config":
+ if args is None:
+ args = sys.argv[1:]
+ 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: {})"
+ raise TypeError(msg.format(args, type(args)))
+
+ config = get_config(args, plugins)
+ pluginmanager = config.pluginmanager
+ try:
+ if plugins:
+ for plugin in plugins:
+ if isinstance(plugin, str):
+ pluginmanager.consider_pluginarg(plugin)
+ else:
+ pluginmanager.register(plugin)
+ config = pluginmanager.hook.pytest_cmdline_parse(
+ pluginmanager=pluginmanager, args=args
+ )
+ return config
+ except BaseException:
+ config._ensure_unconfigure()
+ raise
+
+
+def _get_directory(path: Path) -> Path:
+ """Get the directory of a path - itself if already a directory."""
+ if path.is_file():
+ return path.parent
+ else:
+ return path
+
+
+@final
+class PytestPluginManager(PluginManager):
+ """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
+ additional pytest-specific functionality:
+
+ * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
+ ``pytest_plugins`` global variables found in plugins being loaded.
+ * ``conftest.py`` loading during start-up.
+ """
+
+ def __init__(self) -> None:
+ 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.
+ self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {}
+ self._conftestpath2mod: Dict[Path, types.ModuleType] = {}
+ self._confcutdir: Optional[Path] = None
+ self._noconftest = False
+
+ # _getconftestmodules()'s call to _get_directory() causes a stat
+ # storm when it's called potentially thousands of times in a test
+ # 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
+ # since we refactored warnings as first citizens of Config, they are
+ # just stored here to be used later.
+ self.skipped_plugins: List[Tuple[str, str]] = []
+
+ self.add_hookspecs(_pytest.hookspec)
+ self.register(self)
+ if os.environ.get("PYTEST_DEBUG"):
+ err: IO[str] = sys.stderr
+ encoding: str = getattr(err, "encoding", "utf8")
+ try:
+ err = open(
+ os.dup(err.fileno()),
+ mode=err.mode,
+ buffering=1,
+ encoding=encoding,
+ )
+ except Exception:
+ pass
+ self.trace.root.setwriter(err.write)
+ self.enable_tracing()
+
+ # Config._consider_importhook will set a real object if required.
+ self.rewrite_hook = _pytest.assertion.DummyRewriteHook()
+ # 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):
+ # 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.
+ if name == "pytest_plugins":
+ return
+
+ method = getattr(plugin, name)
+ opts = super().parse_hookimpl_opts(plugin, name)
+
+ # Consider only actual functions for hooks (#3775).
+ if not inspect.isroutine(method):
+ return
+
+ # 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
+
+ def parse_hookspec_opts(self, module_or_class, name: str):
+ 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,
+ }
+ return opts
+
+ def register(
+ self, plugin: _PluggyPlugin, name: Optional[str] = None
+ ) -> Optional[str]:
+ if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
+ warnings.warn(
+ PytestConfigWarning(
+ "{} plugin has been merged into the core, "
+ "please remove it from your requirements.".format(
+ name.replace("_", "-")
+ )
+ )
+ )
+ return None
+ ret: Optional[str] = super().register(plugin, name)
+ if ret:
+ self.hook.pytest_plugin_registered.call_historic(
+ kwargs=dict(plugin=plugin, manager=self)
+ )
+
+ if isinstance(plugin, types.ModuleType):
+ self.consider_module(plugin)
+ return ret
+
+ def getplugin(self, name: str):
+ # Support deprecated naming because plugins (xdist e.g.) use it.
+ plugin: Optional[_PluggyPlugin] = self.get_plugin(name)
+ return plugin
+
+ def hasplugin(self, name: str) -> bool:
+ """Return whether a plugin with the given name is registered."""
+ return bool(self.get_plugin(name))
+
+ def pytest_configure(self, config: "Config") -> None:
+ """:meta private:"""
+ # XXX now that the pluginmanager exposes hookimpl(tryfirst...)
+ # we should remove tryfirst/trylast as markers.
+ 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.",
+ )
+ 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.",
+ )
+ self._configured = True
+
+ #
+ # Internal API for local conftest plugin handling.
+ #
+ def _set_initial_conftests(
+ self, namespace: argparse.Namespace, rootpath: Path
+ ) -> None:
+ """Load initial conftest files given a preparsed "namespace".
+
+ As conftest files may add their own command line options which have
+ arguments ('--my-opt somepath') we might get some false positives.
+ 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
+ )
+ self._noconftest = namespace.noconftest
+ self._using_pyargs = namespace.pyargs
+ testpaths = namespace.file_or_dir
+ foundanchor = False
+ for testpath in testpaths:
+ path = str(testpath)
+ # 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)
+ foundanchor = True
+ if not foundanchor:
+ self._try_load_conftest(current, namespace.importmode, rootpath)
+
+ def _try_load_conftest(
+ self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
+ ) -> None:
+ self._getconftestmodules(anchor, importmode, rootpath)
+ # 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)
+
+ def _getconftestmodules(
+ self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
+ ) -> List[types.ModuleType]:
+ if self._noconftest:
+ 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
+
+ # 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)
+ self._dirpath2confmods[directory] = clist
+ return clist
+
+ 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)
+ for mod in reversed(modules):
+ try:
+ return mod, getattr(mod, name)
+ except AttributeError:
+ continue
+ raise KeyError(name)
+
+ def _importconftest(
+ self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
+ ) -> 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]
+
+ pkgpath = resolve_package_path(conftestpath)
+ if pkgpath is None:
+ _ensure_removed_sysmodule(conftestpath.stem)
+
+ try:
+ mod = import_path(conftestpath, mode=importmode, root=rootpath)
+ except Exception as e:
+ assert e.__traceback__ is not None
+ exc_info = (type(e), e, e.__traceback__)
+ raise ConftestImportFailure(conftestpath, exc_info) 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
+ mods.append(mod)
+ self.trace(f"loading conftestmodule {mod!r}")
+ self.consider_conftest(mod)
+ return mod
+
+ def _check_non_top_pytest_plugins(
+ self,
+ mod: types.ModuleType,
+ conftestpath: Path,
+ ) -> None:
+ if (
+ hasattr(mod, "pytest_plugins")
+ and self._configured
+ and not self._using_pyargs
+ ):
+ msg = (
+ "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n"
+ "It affects the entire test suite instead of just below the conftest as expected.\n"
+ " {}\n"
+ "Please move it to a top level conftest file at the rootdir:\n"
+ " {}\n"
+ "For more information, visit:\n"
+ " https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files"
+ )
+ fail(msg.format(conftestpath, self._confcutdir), pytrace=False)
+
+ #
+ # API for bootstrapping plugin loading
+ #
+ #
+
+ def consider_preparse(
+ self, args: Sequence[str], *, exclude_only: bool = False
+ ) -> None:
+ """:meta private:"""
+ i = 0
+ n = len(args)
+ while i < n:
+ opt = args[i]
+ i += 1
+ if isinstance(opt, str):
+ if opt == "-p":
+ try:
+ parg = args[i]
+ except IndexError:
+ return
+ i += 1
+ elif opt.startswith("-p"):
+ parg = opt[2:]
+ else:
+ continue
+ if exclude_only and not parg.startswith("no:"):
+ continue
+ self.consider_pluginarg(parg)
+
+ def consider_pluginarg(self, arg: str) -> None:
+ """:meta private:"""
+ if arg.startswith("no:"):
+ name = arg[3:]
+ if name in essential_plugins:
+ raise UsageError("plugin %s cannot be disabled" % name)
+
+ # PR #4304: remove stepwise if cacheprovider is blocked.
+ if name == "cacheprovider":
+ self.set_blocked("stepwise")
+ self.set_blocked("pytest_stepwise")
+
+ self.set_blocked(name)
+ if not name.startswith("pytest_"):
+ 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]
+ if not name.startswith("pytest_"):
+ if self._name2plugin.get("pytest_" + name, -1) is None:
+ del self._name2plugin["pytest_" + name]
+ self.import_plugin(arg, consider_entry_points=True)
+
+ def consider_conftest(self, conftestmodule: types.ModuleType) -> None:
+ """:meta private:"""
+ self.register(conftestmodule, name=conftestmodule.__file__)
+
+ def consider_env(self) -> None:
+ """:meta private:"""
+ self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))
+
+ def consider_module(self, mod: types.ModuleType) -> None:
+ """:meta private:"""
+ self._import_plugin_specs(getattr(mod, "pytest_plugins", []))
+
+ def _import_plugin_specs(
+ self, spec: Union[None, types.ModuleType, str, Sequence[str]]
+ ) -> None:
+ plugins = _get_plugin_specs_as_list(spec)
+ for import_spec in plugins:
+ self.import_plugin(import_spec)
+
+ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:
+ """Import a plugin with ``modname``.
+
+ If ``consider_entry_points`` is True, entry point names are also
+ considered to find a plugin.
+ """
+ # Most often modname refers to builtin modules, e.g. "pytester",
+ # "terminal" or "capture". Those plugins are registered under their
+ # basename for historic purposes but must be imported with the
+ # _pytest prefix.
+ assert isinstance(modname, str), (
+ "module name as text required, got %r" % modname
+ )
+ if self.is_blocked(modname) or self.get_plugin(modname) is not None:
+ return
+
+ importspec = "_pytest." + modname if modname in builtin_plugins else modname
+ self.rewrite_hook.mark_rewrite(importspec)
+
+ if consider_entry_points:
+ loaded = self.load_setuptools_entrypoints("pytest11", name=modname)
+ if loaded:
+ return
+
+ try:
+ __import__(importspec)
+ except ImportError as e:
+ raise ImportError(
+ f'Error importing plugin "{modname}": {e.args[0]}'
+ ).with_traceback(e.__traceback__) from e
+
+ except Skipped as e:
+ self.skipped_plugins.append((modname, e.msg or ""))
+ else:
+ mod = sys.modules[importspec]
+ self.register(mod, modname)
+
+
+def _get_plugin_specs_as_list(
+ specs: Union[None, types.ModuleType, str, Sequence[str]]
+) -> List[str]:
+ """Parse a plugins specification into a list of plugin names."""
+ # None means empty.
+ if specs is None:
+ return []
+ # Workaround for #3899 - a submodule which happens to be called "pytest_plugins".
+ if isinstance(specs, types.ModuleType):
+ return []
+ # Comma-separated list.
+ if isinstance(specs, str):
+ return specs.split(",") if specs else []
+ # Direct specification.
+ if isinstance(specs, collections.abc.Sequence):
+ return list(specs)
+ raise UsageError(
+ "Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %r"
+ % specs
+ )
+
+
+def _ensure_removed_sysmodule(modname: str) -> None:
+ try:
+ del sys.modules[modname]
+ except KeyError:
+ pass
+
+
+class Notset:
+ def __repr__(self):
+ return "<NOTSET>"
+
+
+notset = Notset()
+
+
+def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
+ """Given an iterable of file names in a source distribution, return the "names" that should
+ be marked for assertion rewrite.
+
+ For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in
+ the assertion rewrite mechanism.
+
+ This function has to deal with dist-info based distributions and egg based distributions
+ (which are still very much in use for "editable" installs).
+
+ Here are the file names as seen in a dist-info based distribution:
+
+ pytest_mock/__init__.py
+ pytest_mock/_version.py
+ pytest_mock/plugin.py
+ pytest_mock.egg-info/PKG-INFO
+
+ Here are the file names as seen in an egg based distribution:
+
+ src/pytest_mock/__init__.py
+ src/pytest_mock/_version.py
+ src/pytest_mock/plugin.py
+ src/pytest_mock.egg-info/PKG-INFO
+ LICENSE
+ setup.py
+
+ We have to take in account those two distribution flavors in order to determine which
+ names should be considered for assertion rewriting.
+
+ More information:
+ https://github.com/pytest-dev/pytest-mock/issues/167
+ """
+ package_files = list(package_files)
+ seen_some = False
+ for fn in package_files:
+ is_simple_module = "/" not in fn and fn.endswith(".py")
+ is_package = fn.count("/") == 1 and fn.endswith("__init__.py")
+ if is_simple_module:
+ module_name, _ = os.path.splitext(fn)
+ # we ignore "setup.py" at the root of the distribution
+ if module_name != "setup":
+ seen_some = True
+ yield module_name
+ elif is_package:
+ package_name = os.path.dirname(fn)
+ seen_some = True
+ yield package_name
+
+ if not seen_some:
+ # At this point we did not find any packages or modules suitable for assertion
+ # rewriting, so we try again by stripping the first path component (to account for
+ # "src" based source trees for example).
+ # This approach lets us have the common case continue to be fast, as egg-distributions
+ # are rarer.
+ new_package_files = []
+ for fn in package_files:
+ parts = fn.split("/")
+ new_fn = "/".join(parts[1:])
+ if new_fn:
+ new_package_files.append(new_fn)
+ if new_package_files:
+ 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.
+
+ :param PytestPluginManager pluginmanager:
+ A pytest PluginManager.
+
+ :param InvocationParams invocation_params:
+ Object containing parameters regarding the :func:`pytest.main`
+ invocation.
+ """
+
+ @final
+ @attr.s(frozen=True, auto_attribs=True)
+ class InvocationParams:
+ """Holds parameters passed during :func:`pytest.main`.
+
+ The object attributes are read-only.
+
+ .. versionadded:: 5.1
+
+ .. note::
+
+ Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts``
+ ini option are handled by pytest, not being included in the ``args`` attribute.
+
+ Plugins accessing ``InvocationParams`` must be aware of that.
+ """
+
+ args: Tuple[str, ...] = attr.ib(converter=_args_converter)
+ """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,
+ pluginmanager: PytestPluginManager,
+ *,
+ invocation_params: Optional[InvocationParams] = None,
+ ) -> None:
+ from .argparsing import Parser, FILE_OR_DIR
+
+ if invocation_params is None:
+ invocation_params = self.InvocationParams(
+ args=(), plugins=None, dir=Path.cwd()
+ )
+
+ self.option = argparse.Namespace()
+ """Access to command line option as attributes.
+
+ :type: argparse.Namespace
+ """
+
+ self.invocation_params = invocation_params
+ """The parameters with which pytest was invoked.
+
+ :type: InvocationParams
+ """
+
+ _a = FILE_OR_DIR
+ self._parser = Parser(
+ usage=f"%(prog)s [options] [{_a}] [{_a}] [...]",
+ processopt=self._processopt,
+ _ispytest=True,
+ )
+ self.pluginmanager = pluginmanager
+ """The plugin manager handles plugin registration and hook invocation.
+
+ :type: PytestPluginManager
+ """
+
+ self.stash = Stash()
+ """A place where plugins can store information on the config for their
+ own use.
+
+ :type: Stash
+ """
+ # 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._inicache: Dict[str, Any] = {}
+ self._override_ini: Sequence[str] = ()
+ self._opt2dest: Dict[str, str] = {}
+ self._cleanup: List[Callable[[], None]] = []
+ self.pluginmanager.register(self, "pytestconfig")
+ self._configured = False
+ self.hook.pytest_addoption.call_historic(
+ kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
+ )
+
+ if TYPE_CHECKING:
+ from _pytest.cacheprovider import Cache
+
+ self.cache: Optional[Cache] = None
+
+ @property
+ def rootpath(self) -> Path:
+ """The path to the :ref:`rootdir <rootdir>`.
+
+ :type: pathlib.Path
+
+ .. versionadded:: 6.1
+ """
+ return self._rootpath
+
+ @property
+ def inipath(self) -> Optional[Path]:
+ """The path to the :ref:`configfile <configfiles>`.
+
+ :type: Optional[pathlib.Path]
+
+ .. versionadded:: 6.1
+ """
+ return self._inipath
+
+ def add_cleanup(self, func: Callable[[], None]) -> None:
+ """Add a function to be called when the config object gets out of
+ use (usually coinciding with pytest_unconfigure)."""
+ self._cleanup.append(func)
+
+ def _do_configure(self) -> None:
+ assert not self._configured
+ self._configured = True
+ with warnings.catch_warnings():
+ warnings.simplefilter("default")
+ self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
+
+ def _ensure_unconfigure(self) -> None:
+ if self._configured:
+ self._configured = False
+ self.hook.pytest_unconfigure(config=self)
+ self.hook.pytest_configure._call_history = []
+ while self._cleanup:
+ fin = self._cleanup.pop()
+ fin()
+
+ def get_terminal_writer(self) -> TerminalWriter:
+ terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
+ "terminalreporter"
+ )
+ return terminalreporter._tw
+
+ def pytest_cmdline_parse(
+ self, pluginmanager: PytestPluginManager, args: List[str]
+ ) -> "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.
+ if getattr(self.option, "version", False) or "--version" in args:
+ from _pytest.helpconfig import showversion
+
+ showversion(self)
+ elif (
+ getattr(self.option, "help", False) or "--help" in args or "-h" in args
+ ):
+ self._parser._getparser().print_help()
+ sys.stdout.write(
+ "\nNOTE: displaying only minimal help due to UsageError.\n\n"
+ )
+
+ raise
+
+ return self
+
+ def notify_exception(
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ option: Optional[argparse.Namespace] = None,
+ ) -> None:
+ if option and getattr(option, "fulltrace", False):
+ style: _TracebackStyle = "long"
+ else:
+ style = "native"
+ excrepr = excinfo.getrepr(
+ funcargs=True, showlocals=getattr(option, "showlocals", False), style=style
+ )
+ res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
+ if not any(res):
+ for line in str(excrepr).split("\n"):
+ sys.stderr.write("INTERNALERROR> %s\n" % line)
+ sys.stderr.flush()
+
+ def cwd_relative_nodeid(self, nodeid: str) -> str:
+ # nodeid's are relative to the rootpath, compute relative to cwd.
+ if self.invocation_params.dir != self.rootpath:
+ fullpath = self.rootpath / nodeid
+ nodeid = bestrelpath(self.invocation_params.dir, fullpath)
+ return nodeid
+
+ @classmethod
+ def fromdictargs(cls, option_dict, args) -> "Config":
+ """Constructor usable for subprocesses."""
+ config = get_config(args)
+ config.option.__dict__.update(option_dict)
+ config.parse(args, addopts=False)
+ for x in config.option.plugins:
+ config.pluginmanager.consider_pluginarg(x)
+ return config
+
+ def _processopt(self, opt: "Argument") -> None:
+ for name in opt._short_opts + opt._long_opts:
+ self._opt2dest[name] = opt.dest
+
+ if hasattr(opt, "default"):
+ if not hasattr(self.option, opt.dest):
+ setattr(self.option, opt.dest, opt.default)
+
+ @hookimpl(trylast=True)
+ def pytest_load_initial_conftests(self, early_config: "Config") -> None:
+ self.pluginmanager._set_initial_conftests(
+ early_config.known_args_namespace, rootpath=early_config.rootpath
+ )
+
+ def _initini(self, args: Sequence[str]) -> None:
+ ns, unknown_args = self._parser.parse_known_and_unknown_args(
+ args, namespace=copy.copy(self.option)
+ )
+ rootpath, inipath, inicfg = determine_setup(
+ ns.inifilename,
+ ns.file_or_dir + unknown_args,
+ rootdir_cmd_arg=ns.rootdir or None,
+ config=self,
+ )
+ 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(
+ "required_plugins",
+ "plugins that must be present for pytest to run",
+ type="args",
+ default=[],
+ )
+ self._override_ini = ns.override_ini or ()
+
+ def _consider_importhook(self, args: Sequence[str]) -> None:
+ """Install the PEP 302 import hook if using assertion rewriting.
+
+ Needs to parse the --assert=<mode> option from the commandline
+ and find all the installed plugins to mark them for rewriting
+ by the importhook.
+ """
+ ns, unknown_args = self._parser.parse_known_and_unknown_args(args)
+ mode = getattr(ns, "assertmode", "plain")
+ if mode == "rewrite":
+ import _pytest.assertion
+
+ try:
+ hook = _pytest.assertion.install_importhook(self)
+ except SystemError:
+ mode = "plain"
+ else:
+ self._mark_plugins_for_rewrite(hook)
+ self._warn_about_missing_assertion(mode)
+
+ def _mark_plugins_for_rewrite(self, hook) -> None:
+ """Given an importhook, mark for rewrite any top-level
+ modules or packages in the distribution package for
+ all pytest plugins."""
+ self.pluginmanager.rewrite_hook = hook
+
+ if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
+ # We don't autoload from setuptools entry points, no need to continue.
+ return
+
+ package_files = (
+ str(file)
+ for dist in importlib_metadata.distributions()
+ if any(ep.group == "pytest11" for ep in dist.entry_points)
+ for file in dist.files or []
+ )
+
+ for name in _iter_rewritable_modules(package_files):
+ hook.mark_rewrite(name)
+
+ def _validate_args(self, args: List[str], via: str) -> List[str]:
+ """Validate known args."""
+ self._parser._config_source_hint = via # type: ignore
+ try:
+ self._parser.parse_known_and_unknown_args(
+ args, namespace=copy.copy(self.option)
+ )
+ finally:
+ del self._parser._config_source_hint # type: ignore
+
+ return args
+
+ def _preparse(self, args: List[str], addopts: bool = True) -> None:
+ if addopts:
+ env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
+ if len(env_addopts):
+ args[:] = (
+ self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
+ + args
+ )
+ self._initini(args)
+ if addopts:
+ args[:] = (
+ self._validate_args(self.getini("addopts"), "via addopts config") + args
+ )
+
+ self.known_args_namespace = self._parser.parse_known_args(
+ args, namespace=copy.copy(self.option)
+ )
+ self._checkversion()
+ self._consider_importhook(args)
+ self.pluginmanager.consider_preparse(args, exclude_only=False)
+ if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
+ # Don't autoload from setuptools entry point. Only explicitly specified
+ # plugins are going to be loaded.
+ self.pluginmanager.load_setuptools_entrypoints("pytest11")
+ self.pluginmanager.consider_env()
+
+ self.known_args_namespace = self._parser.parse_known_args(
+ args, namespace=copy.copy(self.known_args_namespace)
+ )
+
+ 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)
+ self.known_args_namespace.confcutdir = confcutdir
+ try:
+ self.hook.pytest_load_initial_conftests(
+ early_config=self, args=args, parser=self._parser
+ )
+ except ConftestImportFailure as e:
+ if self.known_args_namespace.help or self.known_args_namespace.version:
+ # we don't want to prevent --help/--version to work
+ # so just let is pass and print a warning at the end
+ self.issue_config_time_warning(
+ PytestConfigWarning(f"could not load initial conftests: {e.path}"),
+ stacklevel=2,
+ )
+ else:
+ raise
+
+ @hookimpl(hookwrapper=True)
+ def pytest_collection(self) -> Generator[None, None, None]:
+ # 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()
+
+ def _checkversion(self) -> None:
+ import pytest
+
+ minver = self.inicfg.get("minversion", None)
+ if minver:
+ # Imported lazily to improve start-up time.
+ from packaging.version import Version
+
+ if not isinstance(minver, str):
+ raise pytest.UsageError(
+ "%s: 'minversion' must be a single value" % self.inipath
+ )
+
+ if Version(minver) > Version(pytest.__version__):
+ raise pytest.UsageError(
+ "%s: 'minversion' requires pytest-%s, actual pytest-%s'"
+ % (
+ self.inipath,
+ minver,
+ pytest.__version__,
+ )
+ )
+
+ def _validate_config_options(self) -> None:
+ for key in sorted(self._get_unknown_ini_keys()):
+ self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")
+
+ def _validate_plugins(self) -> None:
+ required_plugins = sorted(self.getini("required_plugins"))
+ if not required_plugins:
+ return
+
+ # Imported lazily to improve start-up time.
+ 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}
+
+ missing_plugins = []
+ for required_plugin in required_plugins:
+ try:
+ req = Requirement(required_plugin)
+ except InvalidRequirement:
+ missing_plugins.append(required_plugin)
+ continue
+
+ if req.name not in plugin_dist_info:
+ missing_plugins.append(required_plugin)
+ elif not req.specifier.contains(
+ Version(plugin_dist_info[req.name]), prereleases=True
+ ):
+ missing_plugins.append(required_plugin)
+
+ if missing_plugins:
+ raise UsageError(
+ "Missing required plugins: {}".format(", ".join(missing_plugins)),
+ )
+
+ def _warn_or_fail_if_strict(self, message: str) -> None:
+ if self.known_args_namespace.strict_config:
+ raise UsageError(message)
+
+ self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3)
+
+ def _get_unknown_ini_keys(self) -> List[str]:
+ parser_inicfg = self._parser._inidict
+ return [name for name in self.inicfg if name not in parser_inicfg]
+
+ def parse(self, args: List[str], addopts: bool = True) -> None:
+ # Parse given cmdline arguments into this config object.
+ assert not hasattr(
+ 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
+ except PrintHelp:
+ pass
+
+ def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None:
+ """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``.
+
+ This function is mainly intended for plugins that need to issue warnings during
+ ``pytest_configure`` (or similar stages).
+
+ :param warning: The warning instance.
+ :param stacklevel: stacklevel forwarded to warnings.warn.
+ """
+ if self.pluginmanager.is_blocked("warnings"):
+ return
+
+ cmdline_filters = self.known_args_namespace.pythonwarnings or []
+ config_filters = self.getini("filterwarnings")
+
+ with warnings.catch_warnings(record=True) as records:
+ warnings.simplefilter("always", type(warning))
+ apply_warning_filters(config_filters, cmdline_filters)
+ warnings.warn(warning, stacklevel=stacklevel)
+
+ 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],
+ when="config",
+ nodeid="",
+ location=location,
+ )
+ )
+
+ def addinivalue_line(self, name: str, line: str) -> None:
+ """Add a line to an ini-file option. The option must have been
+ declared but might not yet be set in which case the line becomes
+ the first line in its value."""
+ x = self.getini(name)
+ assert isinstance(x, list)
+ x.append(line) # modifies the cached list inline
+
+ def getini(self, name: str):
+ """Return configuration value from an :ref:`ini file <configfiles>`.
+
+ 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.
+ """
+ try:
+ return self._inicache[name]
+ except KeyError:
+ self._inicache[name] = val = self._getini(name)
+ return val
+
+ # Meant for easy monkeypatching by legacypath plugin.
+ # Can be inlined back (with no cover removed) once legacypath is gone.
+ def _getini_unknown_type(self, name: str, type: str, value: Union[str, List[str]]):
+ msg = f"unknown configuration type: {type}"
+ raise ValueError(msg, value) # pragma: no cover
+
+ def _getini(self, name: str):
+ try:
+ description, type, default = self._parser._inidict[name]
+ except KeyError as e:
+ raise ValueError(f"unknown configuration value: {name!r}") from e
+ override_value = self._get_override_ini_value(name)
+ if override_value is None:
+ try:
+ value = self.inicfg[name]
+ except KeyError:
+ if default is not None:
+ return default
+ if type is None:
+ return ""
+ return []
+ else:
+ value = override_value
+ # Coerce the values based on types.
+ #
+ # Note: some coercions are only required if we are reading from .ini files, because
+ # the file format doesn't contain type information, but when reading from toml we will
+ # get either str or list of str values (see _parse_ini_config_from_pyproject_toml).
+ # For example:
+ #
+ # ini:
+ # a_line_list = "tests acceptance"
+ # in this case, we need to split the string to obtain a list of strings.
+ #
+ # toml:
+ # a_line_list = ["tests", "acceptance"]
+ # 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
+ input_values = shlex.split(value) if isinstance(value, str) else value
+ return [dp / x for x in input_values]
+ elif type == "args":
+ return shlex.split(value) if isinstance(value, str) else value
+ elif type == "linelist":
+ if isinstance(value, str):
+ return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
+ else:
+ return value
+ elif type == "bool":
+ return _strtobool(str(value).strip())
+ elif type == "string":
+ return value
+ elif type is None:
+ return value
+ else:
+ return self._getini_unknown_type(name, type, value)
+
+ def _getconftest_pathlist(
+ self, name: str, path: Path, rootpath: Path
+ ) -> Optional[List[Path]]:
+ try:
+ mod, relroots = self.pluginmanager._rget_with_confmod(
+ name, path, self.getoption("importmode"), rootpath
+ )
+ except KeyError:
+ return None
+ modpath = Path(mod.__file__).parent
+ values: List[Path] = []
+ for relroot in relroots:
+ if isinstance(relroot, os.PathLike):
+ relroot = Path(relroot)
+ else:
+ relroot = relroot.replace("/", os.sep)
+ relroot = absolutepath(modpath / relroot)
+ values.append(relroot)
+ return values
+
+ def _get_override_ini_value(self, name: str) -> Optional[str]:
+ value = None
+ # override_ini is a list of "ini=value" options.
+ # Always use the last item if multiple values are set for same ini-name,
+ # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2.
+ for ini_config in self._override_ini:
+ try:
+ 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
+ )
+ ) from e
+ else:
+ if key == name:
+ value = user_ini_value
+ return value
+
+ def getoption(self, name: str, default=notset, skip: bool = False):
+ """Return command line option value.
+
+ :param name: Name of the option. You may also specify
+ the literal ``--OPT`` option instead of the "dest" option name.
+ :param default: Default value if no option of that name exists.
+ :param skip: If True, raise pytest.skip if option does not exists
+ or has a None value.
+ """
+ name = self._opt2dest.get(name, name)
+ try:
+ val = getattr(self.option, name)
+ if val is None and skip:
+ raise AttributeError(name)
+ return val
+ except AttributeError as e:
+ if default is not notset:
+ return default
+ if skip:
+ import pytest
+
+ pytest.skip(f"no {name!r} option found")
+ raise ValueError(f"no option named {name!r}") from e
+
+ def getvalue(self, name: str, path=None):
+ """Deprecated, use getoption() instead."""
+ return self.getoption(name)
+
+ def getvalueorskip(self, name: str, path=None):
+ """Deprecated, use getoption(skip=True) instead."""
+ return self.getoption(name, skip=True)
+
+ def _warn_about_missing_assertion(self, mode: str) -> None:
+ if not _assertion_supported():
+ if mode == "plain":
+ warning_text = (
+ "ASSERTIONS ARE NOT EXECUTED"
+ " and FAILING TESTS WILL PASS. Are you"
+ " using python -O?"
+ )
+ else:
+ warning_text = (
+ "assertions not in test modules or"
+ " plugins will be ignored"
+ " because assert statements are not executed "
+ "by the underlying Python interpreter "
+ "(are you using python -O?)\n"
+ )
+ self.issue_config_time_warning(
+ PytestConfigWarning(warning_text),
+ stacklevel=3,
+ )
+
+ def _warn_about_skipped_plugins(self) -> None:
+ for module_name, msg in self.pluginmanager.skipped_plugins:
+ self.issue_config_time_warning(
+ PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"),
+ stacklevel=2,
+ )
+
+
+def _assertion_supported() -> bool:
+ try:
+ assert False
+ except AssertionError:
+ return True
+ else:
+ return False # type: ignore[unreachable]
+
+
+def create_terminal_writer(
+ config: Config, file: Optional[TextIO] = None
+) -> TerminalWriter:
+ """Create a TerminalWriter instance configured according to the options
+ in the config object.
+
+ Every code which requires a TerminalWriter object and has access to a
+ config object should use this function.
+ """
+ tw = TerminalWriter(file=file)
+
+ if config.option.color == "yes":
+ tw.hasmarkup = True
+ elif config.option.color == "no":
+ tw.hasmarkup = False
+
+ if config.option.code_highlight == "yes":
+ tw.code_highlight = True
+ elif config.option.code_highlight == "no":
+ tw.code_highlight = False
+
+ return tw
+
+
+def _strtobool(val: str) -> bool:
+ """Convert a string representation of truth to True or False.
+
+ True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
+ are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
+ 'val' is anything else.
+
+ .. note:: Copied from distutils.util.
+ """
+ val = val.lower()
+ if val in ("y", "yes", "t", "true", "on", "1"):
+ return True
+ elif val in ("n", "no", "f", "false", "off", "0"):
+ return False
+ else:
+ raise ValueError(f"invalid truth value {val!r}")
+
+
+@lru_cache(maxsize=50)
+def parse_warning_filter(
+ arg: str, *, escape: bool
+) -> Tuple[str, str, Type[Warning], str, int]:
+ """Parse a warnings filter string.
+
+ This is copied from warnings._setoption with the following changes:
+
+ * Does not apply the filter.
+ * Escaping is optional.
+ * Raises UsageError so we get nice error messages on failure.
+ """
+ __tracebackhide__ = True
+ error_template = dedent(
+ f"""\
+ while parsing the following warning configuration:
+
+ {arg}
+
+ This error occurred:
+
+ {{error}}
+ """
+ )
+
+ parts = arg.split(":")
+ if len(parts) > 5:
+ doc_url = (
+ "https://docs.python.org/3/library/warnings.html#describing-warning-filters"
+ )
+ error = dedent(
+ f"""\
+ Too many fields ({len(parts)}), expected at most 5 separated by colons:
+
+ action:message:category:module:line
+
+ For more information please consult: {doc_url}
+ """
+ )
+ raise UsageError(error_template.format(error=error))
+
+ while len(parts) < 5:
+ parts.append("")
+ action_, message, category_, module, lineno_ = (s.strip() for s in parts)
+ try:
+ action: str = warnings._getaction(action_) # type: ignore[attr-defined]
+ except warnings._OptionError as e:
+ raise UsageError(error_template.format(error=str(e)))
+ 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))
+ if message and escape:
+ message = re.escape(message)
+ if module and escape:
+ module = re.escape(module) + r"\Z"
+ if lineno_:
+ try:
+ lineno = int(lineno_)
+ if lineno < 0:
+ raise ValueError("number is negative")
+ except ValueError as e:
+ raise UsageError(
+ error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
+ )
+ else:
+ lineno = 0
+ return action, message, category, module, lineno
+
+
+def _resolve_warning_category(category: str) -> Type[Warning]:
+ """
+ Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors)
+ propagate so we can get access to their tracebacks (#9218).
+ """
+ __tracebackhide__ = True
+ if not category:
+ return Warning
+
+ if "." not in category:
+ import builtins as m
+
+ klass = category
+ else:
+ module, _, klass = category.rpartition(".")
+ m = __import__(module, None, None, [klass])
+ cat = getattr(m, klass)
+ if not issubclass(cat, Warning):
+ raise UsageError(f"{cat} is not a Warning subclass")
+ return cast(Type[Warning], cat)
+
+
+def apply_warning_filters(
+ config_filters: Iterable[str], cmdline_filters: Iterable[str]
+) -> None:
+ """Applies pytest-configured filters to the warnings module"""
+ # Filters should have this precedence: cmdline options, config.
+ # Filters should be applied in the inverse order of precedence.
+ for arg in config_filters:
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
+
+ for arg in cmdline_filters:
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=True))
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/argparsing.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/argparsing.py
new file mode 100644
index 0000000000..b0bb3f168f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/argparsing.py
@@ -0,0 +1,535 @@
+import argparse
+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 List
+from typing import Mapping
+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"
+
+
+@final
+class Parser:
+ """Parser for command line arguments and ini-file values.
+
+ :ivar extra_info: Dict of generic param -> value to display in case
+ there's an error processing the command line arguments.
+ """
+
+ prog: Optional[str] = None
+
+ def __init__(
+ self,
+ usage: Optional[str] = None,
+ processopt: Optional[Callable[["Argument"], None]] = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._anonymous = OptionGroup("custom options", parser=self, _ispytest=True)
+ self._groups: List[OptionGroup] = []
+ self._processopt = processopt
+ self._usage = usage
+ self._inidict: Dict[str, Tuple[str, Optional[str], Any]] = {}
+ self._ininames: List[str] = []
+ self.extra_info: Dict[str, Any] = {}
+
+ def processoption(self, option: "Argument") -> None:
+ if self._processopt:
+ if option.dest:
+ self._processopt(option)
+
+ def getgroup(
+ self, name: str, description: str = "", after: Optional[str] = None
+ ) -> "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.
+
+ 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``.
+ """
+ for group in self._groups:
+ if group.name == name:
+ return group
+ group = OptionGroup(name, description, parser=self, _ispytest=True)
+ i = 0
+ for i, grp in enumerate(self._groups):
+ if grp.name == after:
+ break
+ self._groups.insert(i + 1, group)
+ return group
+
+ 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.
+
+ After command line parsing, options are available on the pytest config
+ object via ``config.option.NAME`` where ``NAME`` is usually set
+ by passing a ``dest`` attribute, for example
+ ``addoption("--long", dest="NAME", ...)``.
+ """
+ self._anonymous.addoption(*opts, **attrs)
+
+ def parse(
+ self,
+ args: Sequence[Union[str, "os.PathLike[str]"]],
+ namespace: Optional[argparse.Namespace] = None,
+ ) -> argparse.Namespace:
+ from _pytest._argcomplete import try_argcomplete
+
+ self.optparser = self._getparser()
+ try_argcomplete(self.optparser)
+ strargs = [os.fspath(x) for x in args]
+ return self.optparser.parse_args(strargs, namespace=namespace)
+
+ def _getparser(self) -> "MyOptionParser":
+ from _pytest._argcomplete import filescompleter
+
+ optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
+ groups = self._groups + [self._anonymous]
+ for group in groups:
+ if group.options:
+ desc = group.description or group.name
+ arggroup = optparser.add_argument_group(desc)
+ for option in group.options:
+ n = option.names()
+ a = option.attrs()
+ arggroup.add_argument(*n, **a)
+ file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*")
+ # bash like autocompletion for dirs (appending '/')
+ # Type ignored because typeshed doesn't know about argcomplete.
+ file_or_dir_arg.completer = filescompleter # type: ignore
+ return optparser
+
+ def parse_setoption(
+ self,
+ args: Sequence[Union[str, "os.PathLike[str]"]],
+ option: argparse.Namespace,
+ namespace: Optional[argparse.Namespace] = None,
+ ) -> List[str]:
+ parsedoption = self.parse(args, namespace=namespace)
+ for name, value in parsedoption.__dict__.items():
+ setattr(option, name, value)
+ return cast(List[str], getattr(parsedoption, FILE_OR_DIR))
+
+ def parse_known_args(
+ self,
+ 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."""
+ return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
+
+ def parse_known_and_unknown_args(
+ self,
+ 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."""
+ optparser = self._getparser()
+ strargs = [os.fspath(x) for x in args]
+ return optparser.parse_known_args(strargs, namespace=namespace)
+
+ def addini(
+ self,
+ name: str,
+ help: str,
+ type: Optional[
+ "Literal['string', 'paths', 'pathlist', 'args', 'linelist', 'bool']"
+ ] = None,
+ default=None,
+ ) -> None:
+ """Register an ini-file option.
+
+ :name:
+ Name of the ini-variable.
+ :type:
+ Type of the variable. Can be:
+
+ * ``string``: a string
+ * ``bool``: a boolean
+ * ``args``: a list of strings, separated as in a shell
+ * ``linelist``: a list of strings, separated by line breaks
+ * ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
+ * ``pathlist``: a list of ``py.path``, separated as in a shell
+
+ .. versionadded:: 7.0
+ The ``paths`` variable type.
+
+ Defaults to ``string`` if ``None`` or not passed.
+ :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")
+ self._inidict[name] = (help, type, default)
+ self._ininames.append(name)
+
+
+class ArgumentError(Exception):
+ """Raised if an Argument instance is created with invalid or
+ inconsistent arguments."""
+
+ def __init__(self, msg: str, option: Union["Argument", str]) -> None:
+ self.msg = msg
+ self.option_id = str(option)
+
+ def __str__(self) -> str:
+ if self.option_id:
+ return f"option {self.option_id}: {self.msg}"
+ else:
+ return self.msg
+
+
+class Argument:
+ """Class that mimics the necessary behaviour of optparse.Option.
+
+ It's currently a least effort implementation and ignoring choices
+ and integer prefixes.
+
+ 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."""
+ 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"]
+ 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"]
+ except KeyError:
+ pass
+ self._set_opt_strings(names)
+ dest: Optional[str] = attrs.get("dest")
+ if dest:
+ self.dest = dest
+ elif self._long_opts:
+ self.dest = self._long_opts[0][2:].replace("-", "_")
+ else:
+ try:
+ self.dest = self._short_opts[0][1:]
+ except IndexError as e:
+ self.dest = "???" # Needed for the error repr.
+ raise ArgumentError("need a long or short option", self) from e
+
+ def names(self) -> List[str]:
+ return self._short_opts + self._long_opts
+
+ def attrs(self) -> Mapping[str, Any]:
+ # Update any attributes set by processopt.
+ attrs = "default dest help".split()
+ attrs.append(self.dest)
+ for attr in attrs:
+ try:
+ 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:
+ """Directly from optparse.
+
+ Might not be necessary as this is passed to argparse later on.
+ """
+ for opt in opts:
+ if len(opt) < 2:
+ raise ArgumentError(
+ "invalid option string %r: "
+ "must be at least two characters long" % opt,
+ self,
+ )
+ elif len(opt) == 2:
+ if not (opt[0] == "-" and opt[1] != "-"):
+ raise ArgumentError(
+ "invalid short option string %r: "
+ "must be of the form -x, (x any non-dash char)" % opt,
+ self,
+ )
+ self._short_opts.append(opt)
+ else:
+ if not (opt[0:2] == "--" and opt[2] != "-"):
+ raise ArgumentError(
+ "invalid long option string %r: "
+ "must start with --, followed by non-dash" % opt,
+ self,
+ )
+ self._long_opts.append(opt)
+
+ def __repr__(self) -> str:
+ args: List[str] = []
+ if self._short_opts:
+ args += ["_short_opts: " + repr(self._short_opts)]
+ if self._long_opts:
+ args += ["_long_opts: " + repr(self._long_opts)]
+ args += ["dest: " + repr(self.dest)]
+ if hasattr(self, "type"):
+ args += ["type: " + repr(self.type)]
+ if hasattr(self, "default"):
+ args += ["default: " + repr(self.default)]
+ return "Argument({})".format(", ".join(args))
+
+
+class OptionGroup:
+ """A group of options shown in its own section."""
+
+ def __init__(
+ self,
+ name: str,
+ description: str = "",
+ parser: Optional[Parser] = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self.name = name
+ self.description = description
+ self.options: List[Argument] = []
+ self.parser = parser
+
+ def addoption(self, *optnames: 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``.
+ """
+ conflict = set(optnames).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)
+ self._addoption_instance(option, shortupper=False)
+
+ def _addoption(self, *optnames: str, **attrs: Any) -> None:
+ option = Argument(*optnames, **attrs)
+ self._addoption_instance(option, shortupper=True)
+
+ def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None:
+ if not shortupper:
+ for opt in option._short_opts:
+ if opt[0] == "-" and opt[1].islower():
+ raise ValueError("lowercase shortoptions reserved")
+ if self.parser:
+ self.parser.processoption(option)
+ self.options.append(option)
+
+
+class MyOptionParser(argparse.ArgumentParser):
+ def __init__(
+ self,
+ parser: Parser,
+ extra_info: Optional[Dict[str, Any]] = None,
+ prog: Optional[str] = None,
+ ) -> None:
+ self._parser = parser
+ super().__init__(
+ prog=prog,
+ usage=parser._usage,
+ add_help=False,
+ formatter_class=DropShorterLongHelpFormatter,
+ allow_abbrev=False,
+ )
+ # 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":
+ """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
+
+ raise UsageError(self.format_usage() + msg)
+
+ # Type ignored because typeshed has a very complex type in the superclass.
+ def parse_args( # type: ignore
+ self,
+ args: Optional[Sequence[str]] = None,
+ namespace: Optional[argparse.Namespace] = None,
+ ) -> argparse.Namespace:
+ """Allow splitting of positional arguments."""
+ parsed, unrecognized = self.parse_known_args(args, namespace)
+ if unrecognized:
+ for arg in unrecognized:
+ if arg and arg[0] == "-":
+ lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))]
+ for k, v in sorted(self.extra_info.items()):
+ lines.append(f" {k}: {v}")
+ self.error("\n".join(lines))
+ getattr(parsed, FILE_OR_DIR).extend(unrecognized)
+ return parsed
+
+ if sys.version_info[:2] < (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(
+ self, arg_string: str
+ ) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
+ if not arg_string:
+ return None
+ if not arg_string[0] in self.prefix_chars:
+ return None
+ if arg_string in self._option_string_actions:
+ action = self._option_string_actions[arg_string]
+ return action, arg_string, None
+ if len(arg_string) == 1:
+ return None
+ if "=" in arg_string:
+ option_string, explicit_arg = arg_string.split("=", 1)
+ if option_string in self._option_string_actions:
+ action = self._option_string_actions[option_string]
+ return action, option_string, explicit_arg
+ if self.allow_abbrev or not arg_string.startswith("--"):
+ option_tuples = self._get_option_tuples(arg_string)
+ if len(option_tuples) > 1:
+ msg = gettext(
+ "ambiguous option: %(option)s could match %(matches)s"
+ )
+ options = ", ".join(option for _, option, _ in option_tuples)
+ self.error(msg % {"option": arg_string, "matches": options})
+ elif len(option_tuples) == 1:
+ (option_tuple,) = option_tuples
+ return option_tuple
+ if self._negative_number_matcher.match(arg_string):
+ if not self._has_negative_number_optionals:
+ return None
+ if " " in arg_string:
+ return None
+ return None, arg_string, None
+
+
+class DropShorterLongHelpFormatter(argparse.HelpFormatter):
+ """Shorten help for long options that differ only in extra hyphens.
+
+ - Collapse **long** options that are the same except for extra hyphens.
+ - Shortcut if there are only two options and one of them is a short one.
+ - Cache result on the action object as this is called at least 2 times.
+ """
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ # Use more accurate terminal width.
+ if "width" not in kwargs:
+ kwargs["width"] = _pytest._io.get_terminal_width()
+ super().__init__(*args, **kwargs)
+
+ def _format_action_invocation(self, action: argparse.Action) -> str:
+ orgstr = super()._format_action_invocation(action)
+ if orgstr and orgstr[0] != "-": # only optional arguments
+ return orgstr
+ res: Optional[str] = getattr(action, "_formatted_action_invocation", None)
+ if res:
+ return res
+ options = orgstr.split(", ")
+ if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2):
+ # a shortcut for '-h, --help' or '--abc', '-a'
+ action._formatted_action_invocation = orgstr # type: ignore
+ return orgstr
+ return_list = []
+ short_long: Dict[str, str] = {}
+ for option in options:
+ if len(option) == 2 or option[2] == " ":
+ continue
+ if not option.startswith("--"):
+ raise ArgumentError(
+ 'long optional argument without "--": [%s]' % (option), option
+ )
+ xxoption = option[2:]
+ shortened = xxoption.replace("-", "")
+ if shortened not in short_long or len(short_long[shortened]) < len(
+ xxoption
+ ):
+ short_long[shortened] = xxoption
+ # now short_long has been filled out to the longest with dashes
+ # **and** we keep the right option ordering from add_argument
+ for option in options:
+ if len(option) == 2 or option[2] == " ":
+ return_list.append(option)
+ if option[2:] == short_long.get(option.replace("-", "")):
+ return_list.append(option.replace(" ", "=", 1))
+ formatted_action_invocation = ", ".join(return_list)
+ action._formatted_action_invocation = formatted_action_invocation # type: ignore
+ return formatted_action_invocation
+
+ def _split_lines(self, text, width):
+ """Wrap lines after splitting on original newlines.
+
+ This allows to have explicit line breaks in the help text.
+ """
+ import textwrap
+
+ lines = []
+ for line in text.splitlines():
+ lines.extend(textwrap.wrap(line.strip(), width))
+ return lines
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/compat.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/compat.py
new file mode 100644
index 0000000000..ba267d2150
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/compat.py
@@ -0,0 +1,71 @@
+import functools
+import warnings
+from pathlib import Path
+from typing import Optional
+
+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 = {
+ "pytest_ignore_collect": ("collection_path", "path"),
+ "pytest_collect_file": ("file_path", "path"),
+ "pytest_pycollect_makemodule": ("module_path", "path"),
+ "pytest_report_header": ("start_path", "startdir"),
+ "pytest_report_collectionfinish": ("start_path", "startdir"),
+}
+
+
+class PathAwareHookProxy:
+ """
+ this helper wraps around hook callers
+ until pluggy supports fixingcalls, this one will do
+
+ it currently doesn't return full hook caller proxies for fixed hooks,
+ this may have to be changed later depending on bugs
+ """
+
+ def __init__(self, hook_caller):
+ self.__hook_caller = hook_caller
+
+ def __dir__(self):
+ return dir(self.__hook_caller)
+
+ def __getattr__(self, key, _wraps=functools.wraps):
+ hook = getattr(self.__hook_caller, 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)
+ if fspath_value is not None:
+ warnings.warn(
+ HOOK_LEGACY_PATH_ARG.format(
+ pylib_path_arg=fspath_var, pathlib_path_arg=path_var
+ ),
+ stacklevel=2,
+ )
+ if path_value is not None:
+ if fspath_value is not None:
+ _check_path(path_value, fspath_value)
+ else:
+ fspath_value = legacy_path(path_value)
+ else:
+ assert fspath_value is not None
+ path_value = Path(fspath_value)
+
+ kw[path_var] = path_value
+ kw[fspath_var] = fspath_value
+ return hook(**kw)
+
+ fixed_hook.__name__ = key
+ self.__dict__[key] = fixed_hook
+ return fixed_hook
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/exceptions.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/exceptions.py
new file mode 100644
index 0000000000..4f1320e758
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/exceptions.py
@@ -0,0 +1,11 @@
+from _pytest.compat import final
+
+
+@final
+class UsageError(Exception):
+ """Error in pytest usage or invocation."""
+
+
+class PrintHelp(Exception):
+ """Raised when pytest should print its help to skip the rest of the
+ argument parsing and validation."""
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/findpaths.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/findpaths.py
new file mode 100644
index 0000000000..89ade5f23b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/config/findpaths.py
@@ -0,0 +1,213 @@
+import os
+from pathlib import Path
+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
+
+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
+
+
+def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
+ """Parse the given generic '.ini' file using legacy IniConfig parser, returning
+ the parsed object.
+
+ Raise UsageError if the file cannot be parsed.
+ """
+ try:
+ return iniconfig.IniConfig(str(path))
+ except iniconfig.ParseError as exc:
+ raise UsageError(str(exc)) from exc
+
+
+def load_config_dict_from_file(
+ filepath: Path,
+) -> Optional[Dict[str, Union[str, List[str]]]]:
+ """Load pytest configuration from the given file path, if supported.
+
+ 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)
+
+ if "pytest" in iniconfig:
+ return dict(iniconfig["pytest"].items())
+ else:
+ # "pytest.ini" files are always the source of configuration, even if empty.
+ if filepath.name == "pytest.ini":
+ return {}
+
+ # '.cfg' files are considered if they contain a "[tool:pytest]" section.
+ elif filepath.suffix == ".cfg":
+ iniconfig = _parse_ini_config(filepath)
+
+ if "tool:pytest" in iniconfig.sections:
+ return dict(iniconfig["tool:pytest"].items())
+ elif "pytest" in iniconfig.sections:
+ # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that
+ # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
+ fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
+
+ # '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
+ elif filepath.suffix == ".toml":
+ import tomli
+
+ 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
+
+ result = config.get("tool", {}).get("pytest", {}).get("ini_options", None)
+ if result is not None:
+ # TOML supports richer data types than ini files (strings, arrays, floats, ints, etc),
+ # however we need to convert all scalar values to str for compatibility with the rest
+ # of the configuration system, which expects strings only.
+ def make_scalar(v: object) -> Union[str, List[str]]:
+ return v if isinstance(v, list) else str(v)
+
+ return {k: make_scalar(v) for k, v in result.items()}
+
+ return None
+
+
+def locate_config(
+ 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",
+ "pyproject.toml",
+ "tox.ini",
+ "setup.cfg",
+ ]
+ args = [x for x in args if not str(x).startswith("-")]
+ if not args:
+ args = [Path.cwd()]
+ 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():
+ ini_config = load_config_dict_from_file(p)
+ if ini_config is not None:
+ return base, p, ini_config
+ return None, None, {}
+
+
+def get_common_ancestor(paths: Iterable[Path]) -> Path:
+ common_ancestor: Optional[Path] = None
+ for path in paths:
+ if not path.exists():
+ continue
+ if common_ancestor is None:
+ common_ancestor = path
+ else:
+ if common_ancestor in path.parents or path == common_ancestor:
+ continue
+ elif path in common_ancestor.parents:
+ common_ancestor = path
+ else:
+ shared = commonpath(path, common_ancestor)
+ if shared is not None:
+ common_ancestor = shared
+ if common_ancestor is None:
+ common_ancestor = Path.cwd()
+ elif common_ancestor.is_file():
+ common_ancestor = common_ancestor.parent
+ return common_ancestor
+
+
+def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
+ def is_option(x: str) -> bool:
+ return x.startswith("-")
+
+ def get_file_part_from_node_id(x: str) -> str:
+ return x.split("::")[0]
+
+ def get_dir_from_path(path: Path) -> Path:
+ if path.is_dir():
+ 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))
+ for arg in args
+ if not is_option(arg)
+ )
+
+ return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)]
+
+
+CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
+
+
+def determine_setup(
+ inifile: Optional[str],
+ args: Sequence[str],
+ rootdir_cmd_arg: Optional[str] = None,
+ config: Optional["Config"] = None,
+) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
+ rootdir = None
+ dirs = get_dirs_from_args(args)
+ if inifile:
+ inipath_ = absolutepath(inifile)
+ inipath: Optional[Path] = inipath_
+ inicfg = load_config_dict_from_file(inipath_) or {}
+ if rootdir_cmd_arg is None:
+ rootdir = inipath_.parent
+ else:
+ ancestor = get_common_ancestor(dirs)
+ rootdir, inipath, inicfg = locate_config([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():
+ rootdir = possible_rootdir
+ break
+ else:
+ if dirs != [ancestor]:
+ rootdir, inipath, inicfg = locate_config(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 = 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
+ )
+ )
+ assert rootdir is not None
+ return rootdir, inipath, inicfg or {}
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/debugging.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/debugging.py
new file mode 100644
index 0000000000..452fb18ac3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/debugging.py
@@ -0,0 +1,388 @@
+"""Interactive debugging with PDB, the Python Debugger."""
+import argparse
+import functools
+import sys
+import types
+from typing import Any
+from typing import Callable
+from typing import Generator
+from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+from _pytest import outcomes
+from _pytest._code import ExceptionInfo
+from _pytest.config import Config
+from _pytest.config import ConftestImportFailure
+from _pytest.config import hookimpl
+from _pytest.config import PytestPluginManager
+from _pytest.config.argparsing import Parser
+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
+
+
+def _validate_usepdb_cls(value: str) -> Tuple[str, str]:
+ """Validate syntax of --pdbcls option."""
+ try:
+ modname, classname = value.split(":")
+ except ValueError as e:
+ raise argparse.ArgumentTypeError(
+ f"{value!r} is not in the format 'modname:classname'"
+ ) from e
+ return (modname, classname)
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group._addoption(
+ "--pdb",
+ dest="usepdb",
+ action="store_true",
+ 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."
+ "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb",
+ )
+ group._addoption(
+ "--trace",
+ dest="trace",
+ action="store_true",
+ help="Immediately break when running each test.",
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ import pdb
+
+ if config.getvalue("trace"):
+ config.pluginmanager.register(PdbTrace(), "pdbtrace")
+ if config.getvalue("usepdb"):
+ config.pluginmanager.register(PdbInvoke(), "pdbinvoke")
+
+ pytestPDB._saved.append(
+ (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config)
+ )
+ pdb.set_trace = pytestPDB.set_trace
+ pytestPDB._pluginmanager = config.pluginmanager
+ pytestPDB._config = config
+
+ # NOTE: not using pytest_unconfigure, since it might get called although
+ # pytest_configure was not (if another plugin raises UsageError).
+ def fin() -> None:
+ (
+ pdb.set_trace,
+ pytestPDB._pluginmanager,
+ pytestPDB._config,
+ ) = pytestPDB._saved.pop()
+
+ config.add_cleanup(fin)
+
+
+class pytestPDB:
+ """Pseudo PDB that defers to the real pdb."""
+
+ _pluginmanager: Optional[PytestPluginManager] = None
+ _config: Optional[Config] = None
+ _saved: List[
+ Tuple[Callable[..., None], Optional[PytestPluginManager], Optional[Config]]
+ ] = []
+ _recursive_debug = 0
+ _wrapped_pdb_cls: Optional[Tuple[Type[Any], Type[Any]]] = None
+
+ @classmethod
+ def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]:
+ if capman:
+ return capman.is_capturing()
+ return False
+
+ @classmethod
+ def _import_pdb_cls(cls, capman: Optional["CaptureManager"]):
+ if not cls._config:
+ import pdb
+
+ # Happens when using pytest.set_trace outside of a test.
+ return pdb.Pdb
+
+ usepdb_cls = cls._config.getvalue("usepdb_cls")
+
+ if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls:
+ return cls._wrapped_pdb_cls[1]
+
+ if usepdb_cls:
+ modname, classname = usepdb_cls
+
+ try:
+ __import__(modname)
+ mod = sys.modules[modname]
+
+ # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
+ parts = classname.split(".")
+ pdb_cls = getattr(mod, parts[0])
+ for part in parts[1:]:
+ pdb_cls = getattr(pdb_cls, part)
+ except Exception as exc:
+ value = ":".join((modname, classname))
+ raise UsageError(
+ f"--pdbcls: could not import {value!r}: {exc}"
+ ) from exc
+ else:
+ import pdb
+
+ pdb_cls = pdb.Pdb
+
+ wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
+ cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls)
+ return wrapped_cls
+
+ @classmethod
+ 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]
+ _pytest_capman = capman
+ _continued = False
+
+ def do_debug(self, arg):
+ cls._recursive_debug += 1
+ ret = super().do_debug(arg)
+ cls._recursive_debug -= 1
+ return ret
+
+ def do_continue(self, arg):
+ ret = super().do_continue(arg)
+ if cls._recursive_debug == 0:
+ assert cls._config is not None
+ tw = _pytest.config.create_terminal_writer(cls._config)
+ tw.line()
+
+ capman = self._pytest_capman
+ capturing = pytestPDB._is_capturing(capman)
+ if capturing:
+ if capturing == "global":
+ tw.sep(">", "PDB continue (IO-capturing resumed)")
+ else:
+ tw.sep(
+ ">",
+ "PDB continue (IO-capturing resumed for %s)"
+ % capturing,
+ )
+ assert capman is not None
+ capman.resume()
+ else:
+ tw.sep(">", "PDB continue")
+ assert cls._pluginmanager is not None
+ cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self)
+ self._continued = True
+ return ret
+
+ do_c = do_cont = do_continue
+
+ def do_quit(self, arg):
+ """Raise Exit outcome when quit command is used in pdb.
+
+ This is a bit of a hack - it would be better if BdbQuit
+ could be handled, but this would require to wrap the
+ whole pytest run, and adjust the report etc.
+ """
+ ret = super().do_quit(arg)
+
+ if cls._recursive_debug == 0:
+ outcomes.exit("Quitting debugger")
+
+ return ret
+
+ do_q = do_quit
+ do_exit = do_quit
+
+ def setup(self, f, tb):
+ """Suspend on setup().
+
+ Needed after do_continue resumed, and entering another
+ breakpoint again.
+ """
+ ret = super().setup(f, tb)
+ if not ret and self._continued:
+ # pdb.setup() returns True if the command wants to exit
+ # from the interaction: do not suspend capturing then.
+ if self._pytest_capman:
+ self._pytest_capman.suspend_global_capture(in_=True)
+ return ret
+
+ def get_stack(self, f, t):
+ stack, i = super().get_stack(f, t)
+ if f is None:
+ # Find last non-hidden frame.
+ i = max(0, len(stack) - 1)
+ while i and stack[i][0].f_locals.get("__tracebackhide__", False):
+ i -= 1
+ return stack, i
+
+ return PytestPdbWrapper
+
+ @classmethod
+ def _init_pdb(cls, method, *args, **kwargs):
+ """Initialize PDB debugging, dropping any IO capturing."""
+ import _pytest.config
+
+ if cls._pluginmanager is None:
+ capman: Optional[CaptureManager] = None
+ else:
+ capman = cls._pluginmanager.getplugin("capturemanager")
+ if capman:
+ capman.suspend(in_=True)
+
+ if cls._config:
+ tw = _pytest.config.create_terminal_writer(cls._config)
+ tw.line()
+
+ if cls._recursive_debug == 0:
+ # Handle header similar to pdb.set_trace in py37+.
+ header = kwargs.pop("header", None)
+ if header is not None:
+ tw.sep(">", header)
+ else:
+ capturing = cls._is_capturing(capman)
+ if capturing == "global":
+ tw.sep(">", f"PDB {method} (IO-capturing turned off)")
+ elif capturing:
+ tw.sep(
+ ">",
+ "PDB %s (IO-capturing turned off for %s)"
+ % (method, capturing),
+ )
+ else:
+ tw.sep(">", f"PDB {method}")
+
+ _pdb = cls._import_pdb_cls(capman)(**kwargs)
+
+ if cls._pluginmanager:
+ cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
+ return _pdb
+
+ @classmethod
+ def set_trace(cls, *args, **kwargs) -> None:
+ """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
+ frame = sys._getframe().f_back
+ _pdb = cls._init_pdb("set_trace", *args, **kwargs)
+ _pdb.set_trace(frame)
+
+
+class PdbInvoke:
+ def pytest_exception_interact(
+ self, node: Node, call: "CallInfo[Any]", report: BaseReport
+ ) -> None:
+ capman = node.config.pluginmanager.getplugin("capturemanager")
+ if capman:
+ capman.suspend_global_capture(in_=True)
+ out, err = capman.read_global_capture()
+ sys.stdout.write(out)
+ sys.stdout.write(err)
+ assert call.excinfo is not None
+ _enter_pdb(node, call.excinfo, report)
+
+ def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None:
+ tb = _postmortem_traceback(excinfo)
+ post_mortem(tb)
+
+
+class PdbTrace:
+ @hookimpl(hookwrapper=True)
+ def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]:
+ wrap_pytest_function_for_tracing(pyfuncitem)
+ yield
+
+
+def wrap_pytest_function_for_tracing(pyfuncitem):
+ """Change the Python function object of the given Function item by a
+ wrapper which actually enters pdb before calling the python function
+ itself, effectively leaving the user in the pdb prompt in the first
+ statement of the function."""
+ _pdb = pytestPDB._init_pdb("runcall")
+ testfunction = pyfuncitem.obj
+
+ # we can't just return `partial(pdb.runcall, testfunction)` because (on
+ # python < 3.7.4) runcall's first param is `func`, which means we'd get
+ # an exception if one of the kwargs to testfunction was called `func`.
+ @functools.wraps(testfunction)
+ def wrapper(*args, **kwargs):
+ func = functools.partial(testfunction, *args, **kwargs)
+ _pdb.runcall(func)
+
+ pyfuncitem.obj = wrapper
+
+
+def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
+ """Wrap the given pytestfunct item for tracing support if --trace was given in
+ the command line."""
+ if pyfuncitem.config.getvalue("trace"):
+ wrap_pytest_function_for_tracing(pyfuncitem)
+
+
+def _enter_pdb(
+ node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport
+) -> BaseReport:
+ # XXX we re-use the TerminalReporter's terminalwriter
+ # because this seems to avoid some encoding related troubles
+ # for not completely clear reasons.
+ tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
+ tw.line()
+
+ showcapture = node.config.option.showcapture
+
+ for sectionname, content in (
+ ("stdout", rep.capstdout),
+ ("stderr", rep.capstderr),
+ ("log", rep.caplog),
+ ):
+ if showcapture in (sectionname, "all") and content:
+ tw.sep(">", "captured " + sectionname)
+ if content[-1:] == "\n":
+ content = content[:-1]
+ tw.line(content)
+
+ tw.sep(">", "traceback")
+ rep.toterminal(tw)
+ tw.sep(">", "entering PDB")
+ tb = _postmortem_traceback(excinfo)
+ rep._pdbshown = True # type: ignore[attr-defined]
+ post_mortem(tb)
+ return rep
+
+
+def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.TracebackType:
+ from doctest import UnexpectedException
+
+ if isinstance(excinfo.value, UnexpectedException):
+ # A doctest.UnexpectedException is not useful for post_mortem.
+ # Use the underlying exception instead:
+ return excinfo.value.exc_info[2]
+ elif isinstance(excinfo.value, ConftestImportFailure):
+ # A config.ConftestImportFailure is not useful for post_mortem.
+ # Use the underlying exception instead:
+ return excinfo.value.excinfo[2]
+ else:
+ assert excinfo._excinfo is not None
+ return excinfo._excinfo[2]
+
+
+def post_mortem(t: types.TracebackType) -> None:
+ p = pytestPDB._init_pdb("post_mortem")
+ p.reset()
+ p.interaction(None, t)
+ if p.quitting:
+ outcomes.exit("Quitting debugger")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/deprecated.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/deprecated.py
new file mode 100644
index 0000000000..5248927113
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/deprecated.py
@@ -0,0 +1,155 @@
+"""Deprecation messages and bits of code used elsewhere in the codebase that
+is planned to be removed in the next pytest release.
+
+Keeping it in a central location makes it easy to track what is deprecated and should
+be removed when the time comes.
+
+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 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 = {
+ "pytest_catchlog",
+ "pytest_capturelog",
+ "pytest_faulthandler",
+}
+
+
+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(
+ "@pytest.yield_fixture is deprecated.\n"
+ "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,
+ "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,
+ "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",
+)
+
+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",
+)
+
+# You want to make some `__init__` or function "private".
+#
+# def my_private_function(some, args):
+# ...
+#
+# Do this:
+#
+# def my_private_function(some, args, *, _ispytest: bool = False):
+# check_ispytest(_ispytest)
+# ...
+#
+# Change all internal/allowed calls to
+#
+# my_private_function(some, args, _ispytest=True)
+#
+# All other calls will get the default _ispytest=False and trigger
+# the warning (possibly error in the future).
+
+
+def check_ispytest(ispytest: bool) -> None:
+ if not ispytest:
+ warn(PRIVATE, stacklevel=3)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/doctest.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/doctest.py
new file mode 100644
index 0000000000..0784f431b8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/doctest.py
@@ -0,0 +1,734 @@
+"""Discover and run doctests in modules and test files."""
+import bdb
+import inspect
+import os
+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
+from typing import Generator
+from typing import Iterable
+from typing import List
+from typing import Optional
+from typing import Pattern
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+import pytest
+from _pytest import outcomes
+from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import ReprFileLocation
+from _pytest._code.code import TerminalRepr
+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.nodes import Collector
+from _pytest.outcomes import OutcomeException
+from _pytest.pathlib import fnmatch_ex
+from _pytest.pathlib import import_path
+from _pytest.python_api import approx
+from _pytest.warning_types import PytestWarning
+
+if TYPE_CHECKING:
+ import doctest
+
+DOCTEST_REPORT_CHOICE_NONE = "none"
+DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
+DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
+DOCTEST_REPORT_CHOICE_UDIFF = "udiff"
+DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure"
+
+DOCTEST_REPORT_CHOICES = (
+ DOCTEST_REPORT_CHOICE_NONE,
+ DOCTEST_REPORT_CHOICE_CDIFF,
+ DOCTEST_REPORT_CHOICE_NDIFF,
+ DOCTEST_REPORT_CHOICE_UDIFF,
+ DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
+)
+
+# Lazy definition of runner class
+RUNNER_CLASS = None
+# Lazy definition of output checker class
+CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addini(
+ "doctest_optionflags",
+ "option flags for doctests",
+ type="args",
+ default=["ELLIPSIS"],
+ )
+ parser.addini(
+ "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",
+ dest="doctestmodules",
+ )
+ group.addoption(
+ "--doctest-report",
+ type=str.lower,
+ default="udiff",
+ help="choose another output format for diffs on doctest failure",
+ choices=DOCTEST_REPORT_CHOICES,
+ dest="doctestreport",
+ )
+ group.addoption(
+ "--doctest-glob",
+ action="append",
+ default=[],
+ metavar="pat",
+ 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",
+ 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",
+ dest="doctest_continue_on_failure",
+ )
+
+
+def pytest_unconfigure() -> None:
+ global RUNNER_CLASS
+
+ RUNNER_CLASS = None
+
+
+def pytest_collect_file(
+ file_path: Path,
+ parent: Collector,
+) -> Optional[Union["DoctestModule", "DoctestTextfile"]]:
+ config = parent.config
+ if file_path.suffix == ".py":
+ 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
+ elif _is_doctest(config, file_path, parent):
+ txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path)
+ return txt
+ return None
+
+
+def _is_setup_py(path: Path) -> bool:
+ if path.name != "setup.py":
+ return False
+ contents = path.read_bytes()
+ return b"setuptools" in contents or b"distutils" in contents
+
+
+def _is_doctest(config: Config, path: Path, parent: Collector) -> bool:
+ if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path):
+ return True
+ globs = config.getoption("doctestglob") or ["test*.txt"]
+ return any(fnmatch_ex(glob, path) for glob in globs)
+
+
+def _is_main_py(path: Path) -> bool:
+ return path.name == "__main__.py"
+
+
+class ReprFailDoctest(TerminalRepr):
+ def __init__(
+ self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]]
+ ) -> None:
+ self.reprlocation_lines = reprlocation_lines
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ for reprlocation, lines in self.reprlocation_lines:
+ for line in lines:
+ tw.line(line)
+ reprlocation.toterminal(tw)
+
+
+class MultipleDoctestFailures(Exception):
+ def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None:
+ super().__init__()
+ self.failures = failures
+
+
+def _init_runner_class() -> Type["doctest.DocTestRunner"]:
+ import doctest
+
+ class PytestDoctestRunner(doctest.DebugRunner):
+ """Runner to collect failures.
+
+ Note that the out variable in this case is a list instead of a
+ stdout-like object.
+ """
+
+ def __init__(
+ self,
+ checker: Optional["doctest.OutputChecker"] = None,
+ verbose: Optional[bool] = None,
+ optionflags: int = 0,
+ continue_on_failure: bool = True,
+ ) -> None:
+ super().__init__(checker=checker, verbose=verbose, optionflags=optionflags)
+ self.continue_on_failure = continue_on_failure
+
+ def report_failure(
+ self,
+ out,
+ test: "doctest.DocTest",
+ example: "doctest.Example",
+ got: str,
+ ) -> None:
+ failure = doctest.DocTestFailure(test, example, got)
+ if self.continue_on_failure:
+ out.append(failure)
+ else:
+ raise failure
+
+ def report_unexpected_exception(
+ self,
+ out,
+ test: "doctest.DocTest",
+ example: "doctest.Example",
+ exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType],
+ ) -> None:
+ if isinstance(exc_info[1], OutcomeException):
+ raise exc_info[1]
+ if isinstance(exc_info[1], bdb.BdbQuit):
+ outcomes.exit("Quitting debugger")
+ failure = doctest.UnexpectedException(test, example, exc_info)
+ if self.continue_on_failure:
+ out.append(failure)
+ else:
+ raise failure
+
+ return PytestDoctestRunner
+
+
+def _get_runner(
+ checker: Optional["doctest.OutputChecker"] = None,
+ verbose: Optional[bool] = None,
+ optionflags: int = 0,
+ continue_on_failure: bool = True,
+) -> "doctest.DocTestRunner":
+ # We need this in order to do a lazy import on doctest
+ global RUNNER_CLASS
+ if RUNNER_CLASS is None:
+ RUNNER_CLASS = _init_runner_class()
+ # Type ignored because the continue_on_failure argument is only defined on
+ # PytestDoctestRunner, which is lazily defined so can't be used as a type.
+ return RUNNER_CLASS( # type: ignore
+ checker=checker,
+ verbose=verbose,
+ optionflags=optionflags,
+ continue_on_failure=continue_on_failure,
+ )
+
+
+class DoctestItem(pytest.Item):
+ def __init__(
+ self,
+ name: str,
+ parent: "Union[DoctestTextfile, DoctestModule]",
+ runner: Optional["doctest.DocTestRunner"] = None,
+ dtest: Optional["doctest.DocTest"] = None,
+ ) -> None:
+ super().__init__(name, parent)
+ self.runner = runner
+ self.dtest = dtest
+ self.obj = None
+ self.fixture_request: Optional[FixtureRequest] = None
+
+ @classmethod
+ def from_parent( # type: ignore
+ cls,
+ parent: "Union[DoctestTextfile, DoctestModule]",
+ *,
+ name: str,
+ runner: "doctest.DocTestRunner",
+ dtest: "doctest.DocTest",
+ ):
+ # 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 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)
+
+ 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"] = []
+ # Type ignored because we change the type of `out` from what
+ # doctest expects.
+ self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
+ if failures:
+ raise MultipleDoctestFailures(failures)
+
+ def _disable_output_capturing_for_darwin(self) -> None:
+ """Disable output capturing. Otherwise, stdout is lost to doctest (#985)."""
+ if platform.system() != "Darwin":
+ return
+ capman = self.config.pluginmanager.getplugin("capturemanager")
+ if capman:
+ capman.suspend_global_capture(in_=True)
+ out, err = capman.read_global_capture()
+ sys.stdout.write(out)
+ sys.stderr.write(err)
+
+ # TODO: Type ignored -- breaks Liskov Substitution.
+ def repr_failure( # type: ignore[override]
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ ) -> Union[str, TerminalRepr]:
+ import doctest
+
+ failures: Optional[
+ Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]
+ ] = None
+ if isinstance(
+ excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException)
+ ):
+ failures = [excinfo.value]
+ elif isinstance(excinfo.value, MultipleDoctestFailures):
+ failures = excinfo.value.failures
+
+ if failures is None:
+ return super().repr_failure(excinfo)
+
+ reprlocation_lines = []
+ for failure in failures:
+ example = failure.example
+ test = failure.test
+ filename = test.filename
+ if test.lineno is None:
+ lineno = None
+ else:
+ lineno = test.lineno + example.lineno + 1
+ message = type(failure).__name__
+ # TODO: ReprFileLocation doesn't expect a None lineno.
+ reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type]
+ checker = _get_checker()
+ report_choice = _get_report_choice(self.config.getoption("doctestreport"))
+ if lineno is not None:
+ assert failure.test.docstring is not None
+ lines = failure.test.docstring.splitlines(False)
+ # add line numbers to the left of the error message
+ assert test.lineno is not None
+ lines = [
+ "%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines)
+ ]
+ # trim docstring error lines to 10
+ lines = lines[max(example.lineno - 9, 0) : example.lineno + 1]
+ else:
+ lines = [
+ "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example"
+ ]
+ indent = ">>>"
+ for line in example.source.splitlines():
+ lines.append(f"??? {indent} {line}")
+ indent = "..."
+ if isinstance(failure, doctest.DocTestFailure):
+ lines += checker.output_difference(
+ example, failure.got, report_choice
+ ).split("\n")
+ else:
+ inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
+ lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
+ lines += [
+ x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
+ ]
+ reprlocation_lines.append((reprlocation, lines))
+ 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
+
+
+def _get_flag_lookup() -> Dict[str, int]:
+ import doctest
+
+ return dict(
+ DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
+ DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,
+ NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
+ ELLIPSIS=doctest.ELLIPSIS,
+ IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
+ COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
+ ALLOW_UNICODE=_get_allow_unicode_flag(),
+ ALLOW_BYTES=_get_allow_bytes_flag(),
+ NUMBER=_get_number_flag(),
+ )
+
+
+def get_optionflags(parent):
+ optionflags_str = parent.config.getini("doctest_optionflags")
+ flag_lookup_table = _get_flag_lookup()
+ flag_acc = 0
+ for flag in optionflags_str:
+ flag_acc |= flag_lookup_table[flag]
+ return flag_acc
+
+
+def _get_continue_on_failure(config):
+ continue_on_failure = 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.
+ if config.getvalue("usepdb"):
+ continue_on_failure = False
+ return continue_on_failure
+
+
+class DoctestTextfile(pytest.Module):
+ obj = None
+
+ def collect(self) -> Iterable[DoctestItem]:
+ import doctest
+
+ # Inspired by doctest.testfile; ideally we would use it directly,
+ # but it doesn't support passing a custom checker.
+ encoding = self.config.getini("doctest_encoding")
+ text = self.path.read_text(encoding)
+ filename = str(self.path)
+ name = self.path.name
+ globs = {"__name__": "__main__"}
+
+ optionflags = get_optionflags(self)
+
+ runner = _get_runner(
+ verbose=False,
+ optionflags=optionflags,
+ checker=_get_checker(),
+ continue_on_failure=_get_continue_on_failure(self.config),
+ )
+
+ parser = doctest.DocTestParser()
+ test = parser.get_doctest(text, globs, name, filename, 0)
+ if test.examples:
+ yield DoctestItem.from_parent(
+ self, name=test.name, runner=runner, dtest=test
+ )
+
+
+def _check_all_skipped(test: "doctest.DocTest") -> None:
+ """Raise pytest.skip() if all examples in the given DocTest have the SKIP
+ option set."""
+ import doctest
+
+ 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")
+
+
+def _is_mocked(obj: object) -> bool:
+ """Return if an object is possibly a mock object by checking the
+ existence of a highly improbable attribute."""
+ return (
+ safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
+ is not None
+ )
+
+
+@contextmanager
+def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
+ """Context manager which replaces ``inspect.unwrap`` with a version
+ that's aware of mock objects and doesn't recurse into them."""
+ real_unwrap = inspect.unwrap
+
+ def _mock_aware_unwrap(
+ func: Callable[..., Any], *, stop: Optional[Callable[[Any], Any]] = None
+ ) -> Any:
+ try:
+ if stop is None or stop is _is_mocked:
+ return real_unwrap(func, stop=_is_mocked)
+ _stop = stop
+ 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 "
+ "by a violation of Python's object protocol; see e.g. "
+ "https://github.com/pytest-dev/pytest/issues/5080" % (e, func),
+ PytestWarning,
+ )
+ raise
+
+ inspect.unwrap = _mock_aware_unwrap
+ try:
+ yield
+ finally:
+ inspect.unwrap = real_unwrap
+
+
+class DoctestModule(pytest.Module):
+ def collect(self) -> Iterable[DoctestItem]:
+ import doctest
+
+ class MockAwareDocTestFinder(doctest.DocTestFinder):
+ """A hackish doctest finder that overrides stdlib internals to fix a stdlib bug.
+
+ https://github.com/pytest-dev/pytest/issues/3456
+ https://bugs.python.org/issue25532
+ """
+
+ def _find_lineno(self, obj, source_lines):
+ """Doctest code does not take into account `@property`, this
+ is a hackish way to fix it. https://bugs.python.org/issue17446
+
+ Wrapped Doctests will need to be unwrapped so the correct
+ line number is returned. This will be reported upstream. #8796
+ """
+ if isinstance(obj, property):
+ obj = getattr(obj, "fget", obj)
+
+ if hasattr(obj, "__wrapped__"):
+ # Get the main obj in case of it being wrapped
+ obj = inspect.unwrap(obj)
+
+ # Type ignored because this is a private function.
+ return super()._find_lineno( # type:ignore[misc]
+ obj,
+ source_lines,
+ )
+
+ def _find(
+ self, tests, obj, name, module, source_lines, globs, seen
+ ) -> None:
+ 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
+ # Uses internal doctest module parsing mechanism.
+ finder = MockAwareDocTestFinder()
+ optionflags = get_optionflags(self)
+ runner = _get_runner(
+ verbose=False,
+ optionflags=optionflags,
+ checker=_get_checker(),
+ continue_on_failure=_get_continue_on_failure(self.config),
+ )
+
+ for test in finder.find(module, module.__name__):
+ if test.examples: # skip empty doctests
+ yield DoctestItem.from_parent(
+ self, name=test.name, runner=runner, dtest=test
+ )
+
+
+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
+
+ class LiteralsOutputChecker(doctest.OutputChecker):
+ # Based on doctest_nose_plugin.py from the nltk project
+ # (https://github.com/nltk/nltk) and on the "numtest" doctest extension
+ # by Sebastien Boisgerault (https://github.com/boisgera/numtest).
+
+ _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
+ _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
+ _number_re = re.compile(
+ r"""
+ (?P<number>
+ (?P<mantissa>
+ (?P<integer1> [+-]?\d*)\.(?P<fraction>\d+)
+ |
+ (?P<integer2> [+-]?\d+)\.
+ )
+ (?:
+ [Ee]
+ (?P<exponent1> [+-]?\d+)
+ )?
+ |
+ (?P<integer3> [+-]?\d+)
+ (?:
+ [Ee]
+ (?P<exponent2> [+-]?\d+)
+ )
+ )
+ """,
+ re.VERBOSE,
+ )
+
+ def check_output(self, want: str, got: str, optionflags: int) -> bool:
+ if super().check_output(want, got, optionflags):
+ return True
+
+ allow_unicode = optionflags & _get_allow_unicode_flag()
+ allow_bytes = optionflags & _get_allow_bytes_flag()
+ allow_number = optionflags & _get_number_flag()
+
+ if not allow_unicode and not allow_bytes and not allow_number:
+ return False
+
+ def remove_prefixes(regex: Pattern[str], txt: str) -> str:
+ return re.sub(regex, r"\1\2", txt)
+
+ if allow_unicode:
+ want = remove_prefixes(self._unicode_literal_re, want)
+ got = remove_prefixes(self._unicode_literal_re, got)
+
+ if allow_bytes:
+ want = remove_prefixes(self._bytes_literal_re, want)
+ got = remove_prefixes(self._bytes_literal_re, got)
+
+ if allow_number:
+ got = self._remove_unwanted_precision(want, got)
+
+ return super().check_output(want, got, optionflags)
+
+ def _remove_unwanted_precision(self, want: str, got: str) -> str:
+ wants = list(self._number_re.finditer(want))
+ gots = list(self._number_re.finditer(got))
+ if len(wants) != len(gots):
+ return got
+ offset = 0
+ for w, g in zip(wants, gots):
+ fraction: Optional[str] = w.group("fraction")
+ exponent: Optional[str] = w.group("exponent1")
+ if exponent is None:
+ exponent = w.group("exponent2")
+ 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):
+ # 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.
+ got = (
+ got[: g.start() + offset] + w.group() + got[g.end() + offset :]
+ )
+ offset += w.end() - w.start() - (g.end() - g.start())
+ return got
+
+ return LiteralsOutputChecker
+
+
+def _get_checker() -> "doctest.OutputChecker":
+ """Return a doctest.OutputChecker subclass that supports some
+ additional options:
+
+ * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
+ prefixes (respectively) in string literals. Useful when the same
+ doctest should run in Python 2 and Python 3.
+
+ * NUMBER to ignore floating-point differences smaller than the
+ precision of the literal number in the doctest.
+
+ An inner class is used to avoid importing "doctest" at the module
+ level.
+ """
+ global CHECKER_CLASS
+ if CHECKER_CLASS is None:
+ CHECKER_CLASS = _init_checker_class()
+ return CHECKER_CLASS()
+
+
+def _get_allow_unicode_flag() -> int:
+ """Register and return the ALLOW_UNICODE flag."""
+ import doctest
+
+ return doctest.register_optionflag("ALLOW_UNICODE")
+
+
+def _get_allow_bytes_flag() -> int:
+ """Register and return the ALLOW_BYTES flag."""
+ import doctest
+
+ return doctest.register_optionflag("ALLOW_BYTES")
+
+
+def _get_number_flag() -> int:
+ """Register and return the NUMBER flag."""
+ import doctest
+
+ return doctest.register_optionflag("NUMBER")
+
+
+def _get_report_choice(key: str) -> int:
+ """Return the actual `doctest` module flag value.
+
+ We want to do it as late as possible to avoid importing `doctest` and all
+ its dependencies when parsing options, as it adds overhead and breaks tests.
+ """
+ import doctest
+
+ return {
+ DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF,
+ DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF,
+ DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,
+ DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,
+ DOCTEST_REPORT_CHOICE_NONE: 0,
+ }[key]
+
+
+@pytest.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."""
+ return dict()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/faulthandler.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/faulthandler.py
new file mode 100644
index 0000000000..aaee307ff2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/faulthandler.py
@@ -0,0 +1,97 @@
+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
+
+
+fault_handler_stderr_key = StashKey[TextIO]()
+fault_handler_originally_enabled_key = StashKey[bool]()
+
+
+def pytest_addoption(parser: Parser) -> None:
+ help = (
+ "Dump the traceback of all threads if a test takes "
+ "more than TIMEOUT seconds to finish."
+ )
+ parser.addini("faulthandler_timeout", help, default=0.0)
+
+
+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])
+
+
+def pytest_unconfigure(config: Config) -> None:
+ import faulthandler
+
+ 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())
+
+
+def get_stderr_fileno() -> int:
+ try:
+ fileno = sys.stderr.fileno()
+ # The Twisted Logger will return an invalid file descriptor since it is not backed
+ # by an FD. So, let's also forward this to the same code path as with pytest-xdist.
+ if fileno == -1:
+ raise AttributeError()
+ return fileno
+ except (AttributeError, io.UnsupportedOperation):
+ # 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.
+ return sys.__stderr__.fileno()
+
+
+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]:
+ timeout = get_timeout_config_value(item.config)
+ stderr = item.config.stash[fault_handler_stderr_key]
+ if timeout > 0 and stderr is not None:
+ import faulthandler
+
+ faulthandler.dump_traceback_later(timeout, file=stderr)
+ try:
+ yield
+ finally:
+ faulthandler.cancel_dump_traceback_later()
+ else:
+ yield
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_enter_pdb() -> None:
+ """Cancel any traceback dumping due to timeout before entering pdb."""
+ import faulthandler
+
+ faulthandler.cancel_dump_traceback_later()
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_exception_interact() -> None:
+ """Cancel any traceback dumping due to an interactive exception being
+ raised."""
+ import faulthandler
+
+ faulthandler.cancel_dump_traceback_later()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/fixtures.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/fixtures.py
new file mode 100644
index 0000000000..fddff931c5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/fixtures.py
@@ -0,0 +1,1686 @@
+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
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+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 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 _pytest
+from _pytest import nodes
+from _pytest._code import getfslineno
+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
+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 safe_getattr
+from _pytest.config import _PluggyPlugin
+from _pytest.config import Config
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.deprecated import FILLFUNCARGS
+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 TEST_OUTCOME
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+from _pytest.scope import HIGH_SCOPES
+from _pytest.scope import Scope
+from _pytest.stash import StashKey
+
+
+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
+ from _pytest.python import Metafunc
+
+
+# The value of the fixture -- return/yield of the fixture function (type variable).
+FixtureValue = TypeVar("FixtureValue")
+# The type of the fixture function (type variable).
+FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object])
+# The type of a fixture function (type alias generic in fixture value).
+_FixtureFunc = Union[
+ Callable[..., FixtureValue], Callable[..., Generator[FixtureValue, None, None]]
+]
+# The type of FixtureDef.cached_result (type alias generic in fixture value).
+_FixtureCachedResult = Union[
+ Tuple[
+ # The result.
+ FixtureValue,
+ # Cache key.
+ object,
+ None,
+ ],
+ Tuple[
+ None,
+ # Cache key.
+ object,
+ # Exc info if raised.
+ Tuple[Type[BaseException], BaseException, TracebackType],
+ ],
+]
+
+
+@attr.s(frozen=True, auto_attribs=True)
+class PseudoFixtureDef(Generic[FixtureValue]):
+ cached_result: "_FixtureCachedResult[FixtureValue]"
+ _scope: Scope
+
+
+def pytest_sessionstart(session: "Session") -> None:
+ session._fixturemanager = FixtureManager(session)
+
+
+def get_scope_package(node, fixturedef: "FixtureDef[object]"):
+ import pytest
+
+ 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
+
+
+def get_scope_node(
+ node: nodes.Node, scope: Scope
+) -> Optional[Union[nodes.Item, nodes.Collector]]:
+ import _pytest.python
+
+ if scope is Scope.Function:
+ return node.getparent(nodes.Item)
+ elif scope is Scope.Class:
+ return node.getparent(_pytest.python.Class)
+ elif scope is Scope.Module:
+ return node.getparent(_pytest.python.Module)
+ elif scope is Scope.Package:
+ return node.getparent(_pytest.python.Package)
+ elif scope is Scope.Session:
+ return node.getparent(_pytest.main.Session)
+ else:
+ 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
+
+
+# Parametrized fixture key, helper alias for code below.
+_Key = Tuple[object, ...]
+
+
+def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_Key]:
+ """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]
+ 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
+
+
+# Algorithm for sorting on a per-parametrized resource setup basis.
+# It is called for Session scope first and performs sorting
+# down to the lower scopes such as to minimize number of "high scope"
+# setups and teardowns.
+
+
+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]]] = {}
+ 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
+ for item in items:
+ keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
+ if keys:
+ d[item] = keys
+ for key in keys:
+ item_d[key].append(item)
+ items_dict = dict.fromkeys(items, None)
+ return list(
+ reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
+ )
+
+
+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]"]],
+) -> None:
+ for scope in HIGH_SCOPES:
+ for key in argkeys_cache[scope].get(item, []):
+ items_by_argkey[scope][key].appendleft(item)
+
+
+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]"]],
+ scope: Scope,
+) -> Dict[nodes.Item, None]:
+ if scope is Scope.Function or len(items) < 3:
+ return items
+ ignore: Set[Optional[_Key]] = set()
+ items_deque = deque(items)
+ items_done: Dict[nodes.Item, None] = {}
+ scoped_items_by_argkey = items_by_argkey[scope]
+ scoped_argkeys_cache = argkeys_cache[scope]
+ while items_deque:
+ no_argkey_group: Dict[nodes.Item, None] = {}
+ slicing_argkey = None
+ while items_deque:
+ item = items_deque.popleft()
+ if item in items_done or item in no_argkey_group:
+ continue
+ argkeys = dict.fromkeys(
+ (k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None
+ )
+ if not argkeys:
+ no_argkey_group[item] = None
+ else:
+ slicing_argkey, _ = argkeys.popitem()
+ # We don't have to remove relevant items from later in the
+ # deque because they'll just be ignored.
+ matching_items = [
+ i for i in scoped_items_by_argkey[slicing_argkey] if i in items
+ ]
+ for i in reversed(matching_items):
+ fix_cache_order(i, argkeys_cache, items_by_argkey)
+ items_deque.appendleft(i)
+ break
+ if no_argkey_group:
+ no_argkey_group = reorder_items_atscope(
+ no_argkey_group, argkeys_cache, items_by_argkey, scope.next_lower()
+ )
+ for item in no_argkey_group:
+ items_done[item] = None
+ ignore.add(slicing_argkey)
+ 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()
+
+
+def get_direct_param_fixture_func(request):
+ return request.param
+
+
+@attr.s(slots=True, auto_attribs=True)
+class FuncFixtureInfo:
+ # Original function argument names.
+ argnames: Tuple[str, ...]
+ # Argnames that function immediately requires. These include argnames +
+ # fixture names specified via usefixtures and via autouse=True in fixture
+ # definitions.
+ initialnames: Tuple[str, ...]
+ names_closure: List[str]
+ name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]]
+
+ def prune_dependency_tree(self) -> None:
+ """Recompute names_closure from initialnames and name2fixturedefs.
+
+ Can only reduce names_closure, which means that the new closure will
+ always be a subset of the old one. The order is preserved.
+
+ This method is needed because direct parametrization may shadow some
+ of the fixtures that were included in the originally built dependency
+ tree. In this way the dependency tree can get pruned, and the closure
+ of argnames may get reduced.
+ """
+ closure: Set[str] = set()
+ working_set = set(self.initialnames)
+ while working_set:
+ argname = working_set.pop()
+ # Argname may be smth 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
+ # been missing in the original tree (closure).
+ if argname not in closure and argname in self.names_closure:
+ closure.add(argname)
+ if argname in self.name2fixturedefs:
+ working_set.update(self.name2fixturedefs[argname][-1].argnames)
+
+ self.names_closure[:] = sorted(closure, key=self.names_closure.index)
+
+
+class FixtureRequest:
+ """A request for a fixture from a test or fixture function.
+
+ A request object gives access to the requesting test context and has
+ an optional ``param`` attribute in case the fixture is parametrized
+ indirectly.
+ """
+
+ def __init__(self, pyfuncitem, *, _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
+
+ @property
+ def scope(self) -> "_ScopeName":
+ """Scope string, one of "function", "class", "module", "package", "session"."""
+ return self._scope.value
+
+ @property
+ def fixturenames(self) -> List[str]:
+ """Names of all active fixtures in this request."""
+ result = list(self._pyfuncitem._fixtureinfo.names_closure)
+ result.extend(set(self._fixture_defs).difference(result))
+ return result
+
+ @property
+ 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]
+
+ @property
+ def config(self) -> Config:
+ """The pytest config object associated with this request."""
+ return self._pyfuncitem.config # type: ignore[no-any-return]
+
+ @property
+ def function(self):
+ """Test function object if the request has a per-function scope."""
+ if self.scope != "function":
+ raise AttributeError(
+ f"function not available in {self.scope}-scoped context"
+ )
+ return self._pyfuncitem.obj
+
+ @property
+ def cls(self):
+ """Class (can be None) where the test function was collected."""
+ if self.scope not in ("class", "function"):
+ raise AttributeError(f"cls not available in {self.scope}-scoped context")
+ clscol = self._pyfuncitem.getparent(_pytest.python.Class)
+ if clscol:
+ return clscol.obj
+
+ @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)
+
+ @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
+
+ @property
+ def path(self) -> Path:
+ 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
+
+ @property
+ def keywords(self) -> MutableMapping[str, Any]:
+ """Keywords/markers dictionary for the underlying node."""
+ node: nodes.Node = self.node
+ return node.keywords
+
+ @property
+ def session(self) -> "Session":
+ """Pytest session object."""
+ return self._pyfuncitem.session # type: ignore[no-any-return]
+
+ 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)
+
+ def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
+ """Apply a marker to a single test function invocation.
+
+ This method is useful if you don't want to have a keyword/marker
+ on all function invocations.
+
+ :param marker:
+ A :class:`pytest.MarkDecorator` 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 _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)
+
+ def getfixturevalue(self, argname: str) -> Any:
+ """Dynamically run a named fixture function.
+
+ Declaring fixtures via function argument is recommended where possible.
+ But if you can only decide whether to use another fixture at test
+ setup time, you may use this function to retrieve it inside a fixture
+ or test function body.
+
+ :raises pytest.FixtureLookupError:
+ If the given fixture could not be found.
+ """
+ fixturedef = self._get_active_fixturedef(argname)
+ assert fixturedef.cached_result is not None
+ 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 _get_fixturestack(self) -> List["FixtureDef[Any]"]:
+ current = self
+ values: List[FixtureDef[Any]] = []
+ while isinstance(current, SubRequest):
+ values.append(current._fixturedef) # type: ignore[has-type]
+ 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.
+
+ 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
+ try:
+ param = funcitem.callspec.getparam(argname)
+ except (AttributeError, ValueError):
+ 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]
+
+ 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)
+
+ 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))
+
+ def _check_scope(
+ self,
+ argname: str,
+ invoking_scope: Scope,
+ 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,
+ )
+
+ 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
+
+ def __repr__(self) -> str:
+ return "<FixtureRequest for %r>" % (self.node)
+
+
+@final
+class SubRequest(FixtureRequest):
+ """A sub request for handling getting a fixture from a test function/fixture."""
+
+ def __init__(
+ self,
+ request: "FixtureRequest",
+ scope: Scope,
+ param: Any,
+ param_index: int,
+ fixturedef: "FixtureDef[object]",
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._parent_request = request
+ self.fixturename = fixturedef.argname
+ 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
+
+ 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)
+
+ def _schedule_finalizers(
+ self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
+ ) -> 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)
+ )
+ super()._schedule_finalizers(fixturedef, subrequest)
+
+
+@final
+class FixtureLookupError(LookupError):
+ """Could not return a requested fixture (missing or invalid)."""
+
+ def __init__(
+ self, argname: Optional[str], request: FixtureRequest, msg: Optional[str] = None
+ ) -> None:
+ self.argname = argname
+ self.request = request
+ self.fixturestack = request._get_fixturestack()
+ self.msg = msg
+
+ def formatrepr(self) -> "FixtureLookupErrorRepr":
+ tblines: List[str] = []
+ addline = tblines.append
+ stack = [self.request._pyfuncitem.obj]
+ stack.extend(map(lambda x: x.func, self.fixturestack))
+ msg = self.msg
+ if msg is not None:
+ # The last fixture raise an error, let's present
+ # it at the requesting side.
+ stack = stack[:-1]
+ for function in stack:
+ fspath, lineno = getfslineno(function)
+ try:
+ lines, _ = inspect.getsourcelines(get_real_func(function))
+ except (OSError, IndexError, TypeError):
+ error_msg = "file %s, line %s: source code not available"
+ addline(error_msg % (fspath, lineno + 1))
+ else:
+ addline(f"file {fspath}, line {lineno + 1}")
+ for i, line in enumerate(lines):
+ line = line.rstrip()
+ addline(" " + line)
+ if line.lstrip().startswith("def"):
+ break
+
+ if msg is None:
+ fm = self.request._fixturemanager
+ available = set()
+ parentid = self.request._pyfuncitem.parent.nodeid
+ for name, fixturedefs in fm._arg2fixturedefs.items():
+ faclist = list(fm._matchfactories(fixturedefs, parentid))
+ if faclist:
+ available.add(name)
+ if self.argname in available:
+ msg = " recursive dependency involving fixture '{}' detected".format(
+ self.argname
+ )
+ else:
+ msg = f"fixture '{self.argname}' not found"
+ msg += "\n available fixtures: {}".format(", ".join(sorted(available)))
+ msg += "\n use 'pytest --fixtures [testpath]' for help on them."
+
+ return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname)
+
+
+class FixtureLookupErrorRepr(TerminalRepr):
+ def __init__(
+ self,
+ filename: Union[str, "os.PathLike[str]"],
+ firstlineno: int,
+ tblines: Sequence[str],
+ errorstring: str,
+ argname: Optional[str],
+ ) -> None:
+ self.tblines = tblines
+ self.errorstring = errorstring
+ self.filename = filename
+ self.firstlineno = firstlineno
+ self.argname = argname
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ # tw.line("FixtureLookupError: %s" %(self.argname), red=True)
+ for tbline in self.tblines:
+ tw.line(tbline.rstrip())
+ lines = self.errorstring.split("\n")
+ if lines:
+ tw.line(
+ f"{FormattedExcinfo.fail_marker} {lines[0].strip()}",
+ red=True,
+ )
+ for line in lines[1:]:
+ tw.line(
+ f"{FormattedExcinfo.flow_marker} {line.strip()}",
+ red=True,
+ )
+ tw.line()
+ 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:
+ if is_generator(fixturefunc):
+ fixturefunc = cast(
+ Callable[..., Generator[FixtureValue, None, None]], fixturefunc
+ )
+ generator = fixturefunc(**kwargs)
+ try:
+ fixture_result = next(generator)
+ except StopIteration:
+ raise ValueError(f"{request.fixturename} did not yield a value") from None
+ finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
+ request.addfinalizer(finalizer)
+ else:
+ fixturefunc = cast(Callable[..., FixtureValue], fixturefunc)
+ fixture_result = fixturefunc(**kwargs)
+ return fixture_result
+
+
+def _teardown_yield_fixture(fixturefunc, it) -> None:
+ """Execute the teardown of a fixture function by advancing the iterator
+ after the yield and ensure the iteration ends (if not it means there is
+ more than one yield in the function)."""
+ try:
+ next(it)
+ except StopIteration:
+ pass
+ else:
+ fail_fixturefunc(fixturefunc, "fixture function has more than one 'yield'")
+
+
+def _eval_scope_callable(
+ scope_callable: "Callable[[str, Config], _ScopeName]",
+ fixture_name: str,
+ config: Config,
+) -> "_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
+ )
+ ) 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),
+ pytrace=False,
+ )
+ return result
+
+
+@final
+class FixtureDef(Generic[FixtureValue]):
+ """A container for a factory definition."""
+
+ def __init__(
+ self,
+ fixturemanager: "FixtureManager",
+ baseid: Optional[str],
+ argname: str,
+ func: "_FixtureFunc[FixtureValue]",
+ 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]],
+ ]
+ ] = None,
+ ) -> None:
+ self._fixturemanager = fixturemanager
+ self.baseid = baseid or ""
+ self.has_location = baseid is not None
+ self.func = func
+ self.argname = argname
+ if scope is None:
+ scope = Scope.Function
+ elif callable(scope):
+ scope = _eval_scope_callable(scope, argname, fixturemanager.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.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
+ self._finalizers: List[Callable[[], object]] = []
+
+ @property
+ def scope(self) -> "_ScopeName":
+ """Scope string, one of "function", "class", "module", "package", "session"."""
+ return self._scope.value
+
+ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
+ 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 = []
+
+ def execute(self, request: SubRequest) -> FixtureValue:
+ # Get required arguments and register our own finish()
+ # with their finalization.
+ 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))
+
+ my_cache_key = self.cache_key(request)
+ if self.cached_result is not None:
+ # 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)
+ else:
+ result = self.cached_result[0]
+ return result
+ # We have a previous but differently parametrized fixture instance
+ # so we need to tear it down before creating a new one.
+ 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)
+ return result
+
+ def cache_key(self, request: SubRequest) -> object:
+ return request.param_index if not hasattr(request, "param") else request.param
+
+ def __repr__(self) -> str:
+ return "<FixtureDef argname={!r} scope={!r} baseid={!r}>".format(
+ self.argname, self.scope, self.baseid
+ )
+
+
+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."""
+ 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]
+ return fixturefunc
+
+
+def pytest_fixture_setup(
+ fixturedef: FixtureDef[FixtureValue], request: SubRequest
+) -> FixtureValue:
+ """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
+
+ 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)
+ 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."""
+ message = (
+ '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):
+ fail(message, pytrace=False)
+
+ # Keep reference to the original function in our own custom attribute so we don't unwrap
+ # further than this point and lose useful wrappings like @mock.patch (#3774).
+ result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined]
+
+ return cast(FixtureFunction, result)
+
+
+@final
+@attr.s(frozen=True, auto_attribs=True)
+class FixtureFunctionMarker:
+ scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
+ params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter)
+ autouse: bool = False
+ ids: Union[
+ Tuple[Union[None, str, float, int, bool], ...],
+ Callable[[Any], Optional[object]],
+ ] = attr.ib(
+ default=None,
+ converter=_ensure_immutable_ids,
+ )
+ name: Optional[str] = None
+
+ 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"
+ )
+
+ 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
+ ),
+ pytrace=False,
+ )
+
+ # Type ignored because https://github.com/python/mypy/issues/2087.
+ function._pytestfixturefunction = self # type: ignore[attr-defined]
+ return function
+
+
+@overload
+def fixture(
+ fixture_function: FixtureFunction,
+ *,
+ scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
+ params: Optional[Iterable[object]] = ...,
+ autouse: bool = ...,
+ ids: Optional[
+ Union[
+ Iterable[Union[None, str, float, int, bool]],
+ Callable[[Any], Optional[object]],
+ ]
+ ] = ...,
+ name: Optional[str] = ...,
+) -> FixtureFunction:
+ ...
+
+
+@overload
+def fixture(
+ fixture_function: None = ...,
+ *,
+ scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
+ params: Optional[Iterable[object]] = ...,
+ autouse: bool = ...,
+ ids: Optional[
+ Union[
+ Iterable[Union[None, str, float, int, bool]],
+ Callable[[Any], Optional[object]],
+ ]
+ ] = ...,
+ name: Optional[str] = None,
+) -> FixtureFunctionMarker:
+ ...
+
+
+def fixture(
+ fixture_function: Optional[FixtureFunction] = None,
+ *,
+ scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = "function",
+ params: Optional[Iterable[object]] = None,
+ autouse: bool = False,
+ ids: Optional[
+ Union[
+ Iterable[Union[None, str, float, int, bool]],
+ Callable[[Any], Optional[object]],
+ ]
+ ] = None,
+ name: Optional[str] = None,
+) -> Union[FixtureFunctionMarker, FixtureFunction]:
+ """Decorator to mark a fixture factory function.
+
+ This decorator can be used, with or without parameters, to define a
+ fixture function.
+
+ The name of the fixture function can later be referenced to cause its
+ invocation ahead of running tests: test modules or classes can use the
+ ``pytest.mark.usefixtures(fixturename)`` marker.
+
+ Test functions can directly use fixture names as input arguments in which
+ case the fixture instance returned from the fixture function will be
+ injected.
+
+ Fixtures can provide their values to test functions using ``return`` or
+ ``yield`` statements. When using ``yield`` the code block after the
+ ``yield`` statement is executed as teardown code regardless of the test
+ outcome, and must yield exactly once.
+
+ :param scope:
+ The scope for which this fixture is shared; one of ``"function"``
+ (default), ``"class"``, ``"module"``, ``"package"`` or ``"session"``.
+
+ This parameter may also be a callable which receives ``(fixture_name, config)``
+ as parameters, and must return a ``str`` with one of the values mentioned above.
+
+ See :ref:`dynamic scope` in the docs for more information.
+
+ :param params:
+ An optional list of parameters which will cause multiple invocations
+ of the fixture function and all of the tests using it. The current
+ parameter is available in ``request.param``.
+
+ :param autouse:
+ If True, the fixture func is activated for all tests that can see it.
+ If False (the default), an explicit reference is needed to activate
+ the fixture.
+
+ :param ids:
+ List of string 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.
+
+ :param name:
+ The name of the fixture. This defaults to the name of the decorated
+ function. If a fixture is used in the same module in which it is
+ defined, the function name of the fixture will be shadowed by the
+ function arg that requests the fixture; one way to resolve this is to
+ name the decorated function ``fixture_<fixturename>`` and then use
+ ``@pytest.fixture(name='<fixturename>')``.
+ """
+ fixture_marker = FixtureFunctionMarker(
+ scope=scope,
+ params=params,
+ autouse=autouse,
+ ids=ids,
+ name=name,
+ )
+
+ # Direct decoration.
+ if fixture_function:
+ return fixture_marker(fixture_function)
+
+ return fixture_marker
+
+
+def yield_fixture(
+ fixture_function=None,
+ *args,
+ scope="function",
+ params=None,
+ autouse=False,
+ ids=None,
+ name=None,
+):
+ """(Return a) decorator to mark a yield-fixture factory function.
+
+ .. deprecated:: 3.0
+ Use :py:func:`pytest.fixture` directly instead.
+ """
+ warnings.warn(YIELD_FIXTURE, stacklevel=2)
+ return fixture(
+ fixture_function,
+ *args,
+ scope=scope,
+ params=params,
+ autouse=autouse,
+ ids=ids,
+ name=name,
+ )
+
+
+@fixture(scope="session")
+def pytestconfig(request: FixtureRequest) -> Config:
+ """Session-scoped fixture that returns the session's :class:`pytest.Config`
+ object.
+
+ Example::
+
+ def test_foo(pytestconfig):
+ if pytestconfig.getoption("verbose") > 0:
+ ...
+
+ """
+ return request.config
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addini(
+ "usefixtures",
+ type="args",
+ default=[],
+ help="list of default fixtures to be used with this project",
+ )
+
+
+class FixtureManager:
+ """pytest fixture definitions and information is stored and managed
+ from this class.
+
+ During collection fm.parsefactories() is called multiple times to parse
+ fixture function definitions into FixtureDef objects and internal
+ data structures.
+
+ During collection of test functions, metafunc-mechanics instantiate
+ a FuncFixtureInfo object which is cached per node/func-name.
+ This FuncFixtureInfo object is later retrieved by Function nodes
+ which themselves offer a fixturenames attribute.
+
+ The FuncFixtureInfo object holds information about fixtures and FixtureDefs
+ relevant for a particular function. An initial list of fixtures is
+ assembled like this:
+
+ - ini-defined usefixtures
+ - autouse-marked fixtures along the collection chain up from the function
+ - usefixtures markers at module/class/function level
+ - test function funcargs
+
+ Subsequently the funcfixtureinfo.fixturenames attribute is computed
+ as the closure of the fixtures needed to setup the initial fixtures,
+ i.e. fixtures needed by fixture functions themselves are appended
+ to the fixturenames list.
+
+ Upon the test-setup phases all fixturenames are instantiated, retrieved
+ 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()
+ # A mapping from a nodeid to a list of autouse fixtures it defines.
+ self._nodeid_autousenames: 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.
+
+ 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: 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):
+ argnames = getfuncargnames(func, name=node.name, cls=cls)
+ else:
+ 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)
+ )
+ 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
+ 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)
+
+ 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)
+ if basenames:
+ yield from basenames
+
+ def getfixtureclosure(
+ self,
+ fixturenames: Tuple[str, ...],
+ parentnode: nodes.Node,
+ ignore_args: Sequence[str] = (),
+ ) -> Tuple[Tuple[str, ...], 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
+ # mapping so that the caller can reuse it and does not have
+ # 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)
+
+ arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
+ lastlen = -1
+ while lastlen != len(fixturenames_closure):
+ lastlen = len(fixturenames_closure)
+ for argname in fixturenames_closure:
+ if argname in ignore_args:
+ continue
+ if argname in arg2fixturedefs:
+ continue
+ fixturedefs = self.getfixturedefs(argname, parentid)
+ if fixturedefs:
+ arg2fixturedefs[argname] = fixturedefs
+ merge(fixturedefs[-1].argnames)
+
+ def sort_by_scope(arg_name: str) -> Scope:
+ try:
+ fixturedefs = arg2fixturedefs[arg_name]
+ except KeyError:
+ return Scope.Function
+ else:
+ return fixturedefs[-1]._scope
+
+ fixturenames_closure.sort(key=sort_by_scope, reverse=True)
+ return initialnames, fixturenames_closure, arg2fixturedefs
+
+ def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
+ """Generate new tests based on parametrized fixtures used by the given metafunc"""
+
+ def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]:
+ args, _ = ParameterSet._parse_parametrize_args(*mark.args, **mark.kwargs)
+ return args
+
+ for argname in metafunc.fixturenames:
+ # Get the FixtureDefs for the argname.
+ fixture_defs = metafunc._arg2fixturedefs.get(argname)
+ if not fixture_defs:
+ # Will raise FixtureLookupError at setup time if not parametrized somewhere
+ # else (e.g @pytest.mark.parametrize)
+ continue
+
+ # If the test itself parametrizes using this argname, give it
+ # precedence.
+ if any(
+ argname in get_parametrize_mark_argnames(mark)
+ for mark in metafunc.definition.iter_markers("parametrize")
+ ):
+ continue
+
+ # In the common case we only look at the fixture def with the
+ # closest scope (last in the list). But if the fixture overrides
+ # another fixture, while requesting the super fixture, keep going
+ # in case the super fixture is parametrized (#1953).
+ for fixturedef in reversed(fixture_defs):
+ # Fixture is parametrized, apply it and stop.
+ if fixturedef.params is not None:
+ metafunc.parametrize(
+ argname,
+ fixturedef.params,
+ indirect=True,
+ scope=fixturedef.scope,
+ ids=fixturedef.ids,
+ )
+ break
+
+ # Not requesting the overridden super fixture, stop.
+ if argname not in fixturedef.argnames:
+ break
+
+ # Try next super fixture, if any.
+
+ def pytest_collection_modifyitems(self, items: List[nodes.Item]) -> None:
+ # Separate parametrized setups.
+ items[:] = reorder_items(items)
+
+ def parsefactories(
+ self, node_or_obj, nodeid=NOTSET, unittest: bool = False
+ ) -> None:
+ if nodeid is not NOTSET:
+ holderobj = node_or_obj
+ else:
+ holderobj = node_or_obj.obj
+ 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)
+ marker = getfixturemarker(obj)
+ if not isinstance(marker, FixtureFunctionMarker):
+ # Magic globals with __getattr__ might have got us a wrong
+ # fixture attribute.
+ continue
+
+ if marker.name:
+ name = marker.name
+
+ # During fixture definition we wrap the original fixture function
+ # 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)
+
+ fixture_def = FixtureDef(
+ fixturemanager=self,
+ baseid=nodeid,
+ argname=name,
+ func=obj,
+ scope=marker.scope,
+ params=marker.params,
+ unittest=unittest,
+ ids=marker.ids,
+ )
+
+ 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
+ ) -> Optional[Sequence[FixtureDef[Any]]]:
+ """Get a list of fixtures which are applicable to the given node id.
+
+ :param str argname: Name of the fixture to search for.
+ :param str nodeid: Full node id of the requesting test.
+ :rtype: Sequence[FixtureDef]
+ """
+ try:
+ fixturedefs = self._arg2fixturedefs[argname]
+ except KeyError:
+ return None
+ return tuple(self._matchfactories(fixturedefs, nodeid))
+
+ def _matchfactories(
+ self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
+ ) -> Iterator[FixtureDef[Any]]:
+ parentnodeids = set(nodes.iterparentnodeids(nodeid))
+ for fixturedef in fixturedefs:
+ if fixturedef.baseid in parentnodeids:
+ yield fixturedef
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/freeze_support.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/freeze_support.py
new file mode 100644
index 0000000000..9f8ea231fe
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/freeze_support.py
@@ -0,0 +1,44 @@
+"""Provides a function to report all internal modules for using freezing
+tools."""
+import types
+from typing import Iterator
+from typing import List
+from typing import Union
+
+
+def freeze_includes() -> List[str]:
+ """Return a list of module names used by pytest that should be
+ included by cx_freeze."""
+ import _pytest
+
+ result = list(_iter_all_modules(_pytest))
+ return result
+
+
+def _iter_all_modules(
+ package: Union[str, types.ModuleType],
+ prefix: str = "",
+) -> Iterator[str]:
+ """Iterate over the names of all modules that can be found in the given
+ package, recursively.
+
+ >>> import _pytest
+ >>> list(_iter_all_modules(_pytest))
+ ['_pytest._argcomplete', '_pytest._code.code', ...]
+ """
+ import os
+ import pkgutil
+
+ if isinstance(package, str):
+ path = package
+ else:
+ # Type ignored because typeshed doesn't define ModuleType.__path__
+ # (only defined on packages).
+ package_path = package.__path__ # type: ignore[attr-defined]
+ path, prefix = package_path[0], package.__name__ + "."
+ for _, name, is_package in pkgutil.iter_modules([path]):
+ if is_package:
+ for m in _iter_all_modules(os.path.join(path, name), prefix=name + "."):
+ yield prefix + m
+ else:
+ yield prefix + name
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/helpconfig.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/helpconfig.py
new file mode 100644
index 0000000000..aca2cd391e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/helpconfig.py
@@ -0,0 +1,264 @@
+"""Version info, help messages, tracing configuration."""
+import os
+import sys
+from argparse import Action
+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
+
+
+class HelpAction(Action):
+ """An argparse Action that will raise an exception in order to skip the
+ rest of the argument parsing when --help is passed.
+
+ This prevents argparse from quitting due to missing required arguments
+ when any are defined, for example by ``pytest_addoption``.
+ This is similar to the way that the builtin argparse --help option is
+ implemented by raising SystemExit.
+ """
+
+ def __init__(self, option_strings, dest=None, default=False, help=None):
+ super().__init__(
+ option_strings=option_strings,
+ dest=dest,
+ const=True,
+ default=default,
+ nargs=0,
+ help=help,
+ )
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ setattr(namespace, self.dest, self.const)
+
+ # We should only skip the rest of the parsing after preparse is done.
+ if getattr(parser._parser, "after_preparse", False):
+ raise PrintHelp
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("debugconfig")
+ group.addoption(
+ "--version",
+ "-V",
+ action="count",
+ default=0,
+ dest="version",
+ help="display pytest version and information about plugins. "
+ "When given twice, also display information about plugins.",
+ )
+ group._addoption(
+ "-h",
+ "--help",
+ action=HelpAction,
+ dest="help",
+ help="show help message and configuration info",
+ )
+ group._addoption(
+ "-p",
+ action="append",
+ dest="plugins",
+ default=[],
+ metavar="name",
+ help="early-load given plugin module name or entry point (multi-allowed).\n"
+ "To avoid loading of plugins, use the `no:` prefix, e.g. "
+ "`no:doctest`.",
+ )
+ group.addoption(
+ "--traceconfig",
+ "--trace-config",
+ action="store_true",
+ default=False,
+ help="trace considerations of conftest.py files.",
+ )
+ group.addoption(
+ "--debug",
+ action="store",
+ nargs="?",
+ 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'.",
+ )
+ 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`.',
+ )
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_cmdline_parse():
+ outcome = yield
+ config: Config = outcome.get_result()
+
+ if config.option.debug:
+ # --debug | --debug <file.log> was provided.
+ path = config.option.debug
+ debugfile = open(path, "w")
+ debugfile.write(
+ "versions pytest-%s, "
+ "python-%s\ncwd=%s\nargs=%s\n\n"
+ % (
+ pytest.__version__,
+ ".".join(map(str, sys.version_info)),
+ os.getcwd(),
+ config.invocation_params.args,
+ )
+ )
+ config.trace.root.setwriter(debugfile.write)
+ undo_tracing = config.pluginmanager.enable_tracing()
+ sys.stderr.write("writing pytest debug information to %s\n" % path)
+
+ def unset_tracing() -> None:
+ debugfile.close()
+ sys.stderr.write("wrote pytest debug information to %s\n" % debugfile.name)
+ config.trace.root.setwriter(None)
+ undo_tracing()
+
+ config.add_cleanup(unset_tracing)
+
+
+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__
+ )
+ )
+ plugininfo = getpluginversioninfo(config)
+ if plugininfo:
+ for line in plugininfo:
+ sys.stdout.write(line + "\n")
+ else:
+ sys.stdout.write(f"pytest {pytest.__version__}\n")
+
+
+def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
+ if config.option.version > 0:
+ showversion(config)
+ return 0
+ elif config.option.help:
+ config._do_configure()
+ showhelp(config)
+ config._ensure_unconfigure()
+ return 0
+ return None
+
+
+def showhelp(config: Config) -> None:
+ import textwrap
+
+ reporter = config.pluginmanager.get_plugin("terminalreporter")
+ 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:"
+ )
+ tw.line()
+
+ columns = tw.fullwidth # costly call
+ indent_len = 24 # based on argparse's max_help_position=24
+ indent = " " * indent_len
+ for name in config._parser._ininames:
+ help, type, default = config._parser._inidict[name]
+ if type is None:
+ type = "string"
+ if help is None:
+ raise TypeError(f"help argument cannot be None for {name}")
+ spec = f"{name} ({type}):"
+ tw.write(" %s" % spec)
+ spec_len = len(spec)
+ if spec_len > (indent_len - 3):
+ # Display help starting at a new line.
+ tw.line()
+ helplines = textwrap.wrap(
+ help,
+ columns,
+ initial_indent=indent,
+ subsequent_indent=indent,
+ break_on_hyphens=False,
+ )
+
+ for line in helplines:
+ tw.line(line)
+ else:
+ # Display help starting after the spec, following lines indented.
+ tw.write(" " * (indent_len - spec_len - 2))
+ wrapped = textwrap.wrap(help, columns - indent_len, break_on_hyphens=False)
+
+ if wrapped:
+ tw.line(wrapped[0])
+ for line in wrapped[1:]:
+ tw.line(indent + line)
+
+ tw.line()
+ 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"),
+ ]
+ for name, help in vars:
+ tw.line(f" {name:<24} {help}")
+ tw.line()
+ tw.line()
+
+ tw.line("to see available markers type: pytest --markers")
+ tw.line("to see available fixtures type: pytest --fixtures")
+ tw.line(
+ "(shown according to specified file_or_dir or current dir "
+ "if not specified; fixtures with leading '_' are only shown "
+ "with the '-v' option"
+ )
+
+ for warningreport in reporter.stats.get("warnings", []):
+ tw.line("warning : " + warningreport.message, red=True)
+ return
+
+
+conftest_options = [("pytest_plugins", "list of plugin names to load")]
+
+
+def getpluginversioninfo(config: Config) -> List[str]:
+ lines = []
+ plugininfo = config.pluginmanager.list_plugin_distinfo()
+ if plugininfo:
+ lines.append("setuptools registered plugins:")
+ for plugin, dist in plugininfo:
+ loc = getattr(plugin, "__file__", repr(plugin))
+ content = f"{dist.project_name}-{dist.version} at {loc}"
+ lines.append(" " + content)
+ return lines
+
+
+def pytest_report_header(config: Config) -> List[str]:
+ lines = []
+ if config.option.debug or config.option.traceconfig:
+ lines.append(f"using: pytest-{pytest.__version__}")
+
+ verinfo = getpluginversioninfo(config)
+ if verinfo:
+ lines.extend(verinfo)
+
+ if config.option.traceconfig:
+ lines.append("active plugins:")
+ items = config.pluginmanager.list_name_plugin()
+ for name, plugin in items:
+ if hasattr(plugin, "__file__"):
+ r = plugin.__file__
+ else:
+ r = repr(plugin)
+ lines.append(f" {name:<20}: {r}")
+ return lines
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/hookspec.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/hookspec.py
new file mode 100644
index 0000000000..79251315d8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/hookspec.py
@@ -0,0 +1,928 @@
+"""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
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+from pluggy import HookspecMarker
+
+from _pytest.deprecated import WARNING_CAPTURED_HOOK
+from _pytest.deprecated import WARNING_CMDLINE_PREPARSE_HOOK
+
+if TYPE_CHECKING:
+ import pdb
+ import warnings
+ from typing_extensions import Literal
+
+ from _pytest._code.code import ExceptionRepr
+ from _pytest.code import ExceptionInfo
+ 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
+ from _pytest.main import Session
+ from _pytest.nodes import Collector
+ from _pytest.nodes import Item
+ from _pytest.outcomes import Exit
+ 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
+
+
+hookspec = HookspecMarker("pytest")
+
+# -------------------------------------------------------------------------
+# Initialization hooks called for every plugin
+# -------------------------------------------------------------------------
+
+
+@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)``.
+
+ :param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
+
+ .. note::
+ This hook is incompatible with ``hookwrapper=True``.
+ """
+
+
+@hookspec(historic=True)
+def pytest_plugin_registered(
+ plugin: "_PluggyPlugin", manager: "PytestPluginManager"
+) -> None:
+ """A new pytest plugin got registered.
+
+ :param plugin: The plugin module or instance.
+ :param pytest.PytestPluginManager manager: pytest plugin manager.
+
+ .. note::
+ This hook is incompatible with ``hookwrapper=True``.
+ """
+
+
+@hookspec(historic=True)
+def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> None:
+ """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:
+ 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
+ to change how command line options are added.
+
+ Options can later be accessed through the
+ :py:class:`config <pytest.Config>` object, respectively:
+
+ - :py:func:`config.getoption(name) <pytest.Config.getoption>` to
+ retrieve the value of a command line option.
+
+ - :py:func:`config.getini(name) <pytest.Config.getini>` to retrieve
+ a value read from an ini-style file.
+
+ The config object is passed around on many internal objects via the ``.config``
+ attribute or can be retrieved as the ``pytestconfig`` fixture.
+
+ .. note::
+ This hook is incompatible with ``hookwrapper=True``.
+ """
+
+
+@hookspec(historic=True)
+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.
+
+ After that, the hook is called for other conftest files as they are
+ imported.
+
+ .. note::
+ This hook is incompatible with ``hookwrapper=True``.
+
+ :param pytest.Config config: The pytest config object.
+ """
+
+
+# -------------------------------------------------------------------------
+# Bootstrapping hooks called for plugins registered early enough:
+# internal and 3rd party plugins.
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_cmdline_parse(
+ pluginmanager: "PytestPluginManager", args: List[str]
+) -> Optional["Config"]:
+ """Return an initialized config object, 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
+ ``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.
+ """
+
+
+@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.
+
+ This hook is considered deprecated and will be removed in a future pytest version. Consider
+ using :hook:`pytest_load_initial_conftests` instead.
+
+ .. note::
+ This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
+
+ :param pytest.Config config: The pytest config object.
+ :param List[str] args: Arguments passed on the command line.
+ """
+
+
+@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.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param pytest.Config config: The pytest config object.
+ """
+
+
+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.
+
+ .. note::
+ This hook will not be called for ``conftest.py`` files, only for setuptools 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.
+ """
+
+
+# -------------------------------------------------------------------------
+# collection hooks
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_collection(session: "Session") -> Optional[object]:
+ """Perform the collection phase for the given session.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ The return value is not used, but only stops further processing.
+
+ The default collection phase is this (see individual hooks for full details):
+
+ 1. Starting from ``session`` as the initial collector:
+
+ 1. ``pytest_collectstart(collector)``
+ 2. ``report = pytest_make_collect_report(collector)``
+ 3. ``pytest_exception_interact(collector, call, report)`` if an interactive exception occurred
+ 4. For each collected node:
+
+ 1. If an item, ``pytest_itemcollected(item)``
+ 2. If a collector, recurse into it.
+
+ 5. ``pytest_collectreport(report)``
+
+ 2. ``pytest_collection_modifyitems(session, config, items)``
+
+ 1. ``pytest_deselected(items)`` for any deselected items (may be called multiple times)
+
+ 3. ``pytest_collection_finish(session)``
+ 4. Set ``session.items`` to the list of collected items
+ 5. Set ``session.testscollected`` to the number of collected items
+
+ You can implement this hook to only perform some action before collection,
+ for example the terminal plugin uses it to start displaying the collection
+ counter (and returns `None`).
+
+ :param pytest.Session session: The pytest session object.
+ """
+
+
+def pytest_collection_modifyitems(
+ session: "Session", config: "Config", items: List["Item"]
+) -> None:
+ """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.
+ """
+
+
+def pytest_collection_finish(session: "Session") -> None:
+ """Called after collection has been performed and modified.
+
+ :param pytest.Session session: The pytest session object.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_ignore_collect(
+ collection_path: Path, path: "LEGACY_PATH", config: "Config"
+) -> Optional[bool]:
+ """Return True to prevent considering this path for collection.
+
+ This hook is consulted for all files and directories prior to calling
+ more specific hooks.
+
+ 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.
+
+ .. 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.
+ """
+
+
+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.
+
+ 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).
+
+ .. 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.
+ """
+
+
+# logging hooks for collection
+
+
+def pytest_collectstart(collector: "Collector") -> None:
+ """Collector starts collecting."""
+
+
+def pytest_itemcollected(item: "Item") -> None:
+ """We just collected a test item."""
+
+
+def pytest_collectreport(report: "CollectReport") -> None:
+ """Collector finished collecting."""
+
+
+def pytest_deselected(items: Sequence["Item"]) -> None:
+ """Called for deselected test items, e.g. by keyword.
+
+ May be called multiple times.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectReport]":
+ """Perform :func:`collector.collect() <pytest.Collector.collect>` and return
+ a :class:`~pytest.CollectReport`.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ """
+
+
+# -------------------------------------------------------------------------
+# Python test function related hooks
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_pycollect_makemodule(
+ module_path: Path, path: "LEGACY_PATH", parent
+) -> Optional["Module"]:
+ """Return a 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
+ 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).
+
+ .. 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``.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_pycollect_makeitem(
+ collector: "PyCollector", 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`.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
+ """Call underlying test function.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ """
+
+
+def pytest_generate_tests(metafunc: "Metafunc") -> None:
+ """Generate (multiple) parametrized calls to a test function."""
+
+
+@hookspec(firstresult=True)
+def pytest_make_parametrize_id(
+ config: "Config", val: object, argname: str
+) -> Optional[str]:
+ """Return a user-friendly string representation of the given ``val``
+ that will be used by @pytest.mark.parametrize calls, or None if the hook
+ doesn't know about ``val``.
+
+ The parameter name is available as ``argname``, if required.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param pytest.Config config: The pytest config object.
+ :param val: The parametrized value.
+ :param str argname: The automatic parameter name produced by pytest.
+ """
+
+
+# -------------------------------------------------------------------------
+# runtest related hooks
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_runtestloop(session: "Session") -> Optional[object]:
+ """Perform the main runtest loop (after collection finished).
+
+ The default hook implementation performs the runtest protocol for all items
+ collected in the session (``session.items``), unless the collection failed
+ or the ``collectonly`` pytest option is set.
+
+ If at any point :py:func:`pytest.exit` is called, the loop is
+ terminated immediately.
+
+ 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.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ The return value is not used, but only stops further processing.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_runtest_protocol(
+ item: "Item", nextitem: "Optional[Item]"
+) -> Optional[object]:
+ """Perform the runtest protocol for a single test item.
+
+ The default runtest protocol is this (see individual hooks for full details):
+
+ - ``pytest_runtest_logstart(nodeid, location)``
+
+ - Setup phase:
+ - ``call = pytest_runtest_setup(item)`` (wrapped in ``CallInfo(when="setup")``)
+ - ``report = pytest_runtest_makereport(item, call)``
+ - ``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 = pytest_runtest_call(item)`` (wrapped in ``CallInfo(when="call")``)
+ - ``report = pytest_runtest_makereport(item, call)``
+ - ``pytest_runtest_logreport(report)``
+ - ``pytest_exception_interact(call, report)`` if an interactive exception occurred
+
+ - Teardown phase:
+ - ``call = pytest_runtest_teardown(item, nextitem)`` (wrapped in ``CallInfo(when="teardown")``)
+ - ``report = pytest_runtest_makereport(item, call)``
+ - ``pytest_runtest_logreport(report)``
+ - ``pytest_exception_interact(call, report)`` if an interactive exception occurred
+
+ - ``pytest_runtest_logfinish(nodeid, location)``
+
+ :param item: Test item for which the runtest protocol is performed.
+ :param nextitem: The scheduled-to-be-next test item (or None if this is the end my friend).
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ The return value is not used, but only stops further processing.
+ """
+
+
+def pytest_runtest_logstart(
+ nodeid: str, location: Tuple[str, Optional[int], str]
+) -> None:
+ """Called at the start of running the runtest protocol for a single item.
+
+ 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)``.
+ """
+
+
+def pytest_runtest_logfinish(
+ nodeid: str, location: Tuple[str, Optional[int], str]
+) -> None:
+ """Called at the end of running the runtest protocol for a single item.
+
+ 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)``.
+ """
+
+
+def pytest_runtest_setup(item: "Item") -> None:
+ """Called to perform the setup phase for a test item.
+
+ The default implementation runs ``setup()`` on ``item`` and all of its
+ parents (which haven't been setup yet). This includes obtaining the
+ values of fixtures required by the item (which haven't been obtained
+ yet).
+ """
+
+
+def pytest_runtest_call(item: "Item") -> None:
+ """Called to run the test for test item (the call phase).
+
+ The default implementation calls ``item.runtest()``.
+ """
+
+
+def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
+ """Called to perform the teardown phase for a test item.
+
+ The default implementation runs the finalizers and calls ``teardown()``
+ on ``item`` and all of its parents (which need to be torn down). This
+ includes running the teardown phase of fixtures required by the item (if
+ they go out of scope).
+
+ :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.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_runtest_makereport(
+ item: "Item", call: "CallInfo[None]"
+) -> Optional["TestReport"]:
+ """Called to create a :class:`~pytest.TestReport` for each of
+ the setup, call and teardown runtest phases of a test item.
+
+ See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
+
+ :param call: The :class:`~pytest.CallInfo` for the phase.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ """
+
+
+def pytest_runtest_logreport(report: "TestReport") -> None:
+ """Process the :class:`~pytest.TestReport` produced for each
+ of the setup, call and teardown runtest phases of an item.
+
+ See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_report_to_serializable(
+ config: "Config",
+ 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."""
+
+
+@hookspec(firstresult=True)
+def pytest_report_from_serializable(
+ config: "Config",
+ data: Dict[str, Any],
+) -> Optional[Union["CollectReport", "TestReport"]]:
+ """Restore a report object previously serialized with
+ :hook:`pytest_report_to_serializable`."""
+
+
+# -------------------------------------------------------------------------
+# Fixture related hooks
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_fixture_setup(
+ fixturedef: "FixtureDef[Any]", request: "SubRequest"
+) -> Optional[object]:
+ """Perform fixture setup execution.
+
+ :returns: The return value of the call to the fixture function.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ .. note::
+ 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.
+ """
+
+
+def pytest_fixture_post_finalizer(
+ fixturedef: "FixtureDef[Any]", request: "SubRequest"
+) -> None:
+ """Called after fixture teardown, but before the cache is cleared, so
+ the fixture result ``fixturedef.cached_result`` is still available (not
+ ``None``)."""
+
+
+# -------------------------------------------------------------------------
+# test session related hooks
+# -------------------------------------------------------------------------
+
+
+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.
+ """
+
+
+def pytest_sessionfinish(
+ session: "Session",
+ exitstatus: Union[int, "ExitCode"],
+) -> 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.
+ """
+
+
+def pytest_unconfigure(config: "Config") -> None:
+ """Called before test process is exited.
+
+ :param pytest.Config config: The pytest config object.
+ """
+
+
+# -------------------------------------------------------------------------
+# hooks for customizing the assert methods
+# -------------------------------------------------------------------------
+
+
+def pytest_assertrepr_compare(
+ config: "Config", op: str, left: object, right: object
+) -> Optional[List[str]]:
+ """Return explanation for comparisons in failing assert expressions.
+
+ Return None for no custom explanation, otherwise return a list
+ of strings. The strings will be joined by newlines but any newlines
+ *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.
+ """
+
+
+def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> None:
+ """Called whenever an assertion passes.
+
+ .. versionadded:: 5.0
+
+ Use this hook to do some processing after a passing assertion.
+ The original assertion information is available in the `orig` string
+ and the pytest introspected assertion information is available in the
+ `expl` string.
+
+ This hook must be explicitly enabled by the ``enable_assertion_pass_hook``
+ ini-file option:
+
+ .. code-block:: ini
+
+ [pytest]
+ enable_assertion_pass_hook=true
+
+ 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.
+ """
+
+
+# -------------------------------------------------------------------------
+# Hooks for influencing reporting (invoked from _pytest_terminal).
+# -------------------------------------------------------------------------
+
+
+def pytest_report_header(
+ 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).
+
+ .. note::
+
+ Lines returned by a plugin are displayed before those of plugins which
+ ran before it.
+ 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.
+ """
+
+
+def pytest_report_collectionfinish(
+ config: "Config",
+ start_path: Path,
+ startdir: "LEGACY_PATH",
+ items: Sequence["Item"],
+) -> Union[str, List[str]]:
+ """Return a string or list of strings to be displayed after collection
+ has finished successfully.
+
+ These strings will be displayed after the standard "collected X items" message.
+
+ .. 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 items: List of pytest items that are going to be executed; this list should not be modified.
+
+ .. note::
+
+ Lines returned by a plugin are displayed before those of plugins which
+ ran before it.
+ If you want to have your line(s) displayed first, use
+ :ref:`trylast=True <plugin-hookorder>`.
+
+ .. 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.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_report_teststatus(
+ report: Union["CollectReport", "TestReport"], config: "Config"
+) -> Tuple[str, str, Union[str, Mapping[str, bool]]]:
+ """Return result-category, shortletter and verbose word for status
+ reporting.
+
+ The result-category is a category in which to count the result, for
+ example "passed", "skipped", "error" or the empty string.
+
+ The shortletter is shown as testing progresses, for example ".", "s",
+ "E" or the empty string.
+
+ The verbose word is shown as testing progresses in verbose mode, for
+ example "PASSED", "SKIPPED", "ERROR" or the empty string.
+
+ pytest may style these implicitly according to the report outcome.
+ To provide explicit styling, return a tuple for the verbose word,
+ for example ``"rerun", "R", ("RERUN", {"yellow": True})``.
+
+ :param report: The report object whose status is to be returned.
+ :param config: The pytest config object.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ """
+
+
+def pytest_terminal_summary(
+ terminalreporter: "TerminalReporter",
+ exitstatus: "ExitCode",
+ config: "Config",
+) -> 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.
+
+ .. 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``.
+
+ :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.
+ """
+
+
+@hookspec(historic=True)
+def pytest_warning_recorded(
+ warning_message: "warnings.WarningMessage",
+ when: "Literal['config', 'collect', 'runtest']",
+ nodeid: str,
+ location: Optional[Tuple[str, int, str]],
+) -> 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 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 str nodeid:
+ Full id of the item.
+
+ :param tuple|None 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
+ """
+
+
+# -------------------------------------------------------------------------
+# Hooks for influencing skipping
+# -------------------------------------------------------------------------
+
+
+def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]:
+ """Called when constructing the globals dictionary used for
+ evaluating string conditions in xfail/skipif markers.
+
+ This is useful when the condition for a marker requires
+ objects that are expensive or impossible to obtain during
+ collection time, which is required by normal boolean
+ conditions.
+
+ .. versionadded:: 6.2
+
+ :param pytest.Config config: The pytest config object.
+ :returns: A dictionary of additional globals to add.
+ """
+
+
+# -------------------------------------------------------------------------
+# error handling and internal debugging hooks
+# -------------------------------------------------------------------------
+
+
+def pytest_internalerror(
+ excrepr: "ExceptionRepr",
+ excinfo: "ExceptionInfo[BaseException]",
+) -> Optional[bool]:
+ """Called for internal errors.
+
+ Return True to suppress the fallback handling of printing an
+ INTERNALERROR message directly to sys.stderr.
+ """
+
+
+def pytest_keyboard_interrupt(
+ excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]",
+) -> None:
+ """Called for keyboard interrupt."""
+
+
+def pytest_exception_interact(
+ node: Union["Item", "Collector"],
+ call: "CallInfo[Any]",
+ report: Union["CollectReport", "TestReport"],
+) -> None:
+ """Called when an exception was raised which can potentially be
+ interactively handled.
+
+ May be called during collection (see :hook:`pytest_make_collect_report`),
+ in which case ``report`` is a :class:`CollectReport`.
+
+ May be called during runtest of an item (see :hook:`pytest_runtest_protocol`),
+ in which case ``report`` is a :class:`TestReport`.
+
+ This hook is not called if the exception that was raised is an internal
+ exception like ``skip.Exception``.
+ """
+
+
+def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
+ """Called upon pdb.set_trace().
+
+ 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.
+ """
+
+
+def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
+ """Called when leaving pdb (e.g. with continue after pdb.set_trace()).
+
+ 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.
+ """
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/junitxml.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/junitxml.py
new file mode 100644
index 0000000000..4af5fbab0c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/junitxml.py
@@ -0,0 +1,696 @@
+"""Report test results in JUnit-XML format, for use with Jenkins and build
+integration servers.
+
+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
+"""
+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
+from typing import Match
+from typing import Optional
+from typing import Tuple
+from typing import Union
+
+import pytest
+from _pytest import nodes
+from _pytest import timing
+from _pytest._code.code import ExceptionRepr
+from _pytest._code.code import ReprFileLocation
+from _pytest.config import Config
+from _pytest.config import filename_arg
+from _pytest.config.argparsing import Parser
+from _pytest.fixtures import FixtureRequest
+from _pytest.reports import TestReport
+from _pytest.stash import StashKey
+from _pytest.terminal import TerminalReporter
+
+
+xml_key = StashKey["LogXML"]()
+
+
+def bin_xml_escape(arg: object) -> str:
+ r"""Visually escape invalid XML characters.
+
+ For example, transforms
+ 'hello\aworld\b'
+ into
+ 'hello#x07world#x08'
+ Note that the #xABs are *not* XML escapes - missing the ampersand &#xAB.
+ The idea is to escape visually for the user rather than for XML itself.
+ """
+
+ def repl(matchobj: Match[str]) -> str:
+ i = ord(matchobj.group())
+ if i <= 0xFF:
+ return "#x%02X" % i
+ else:
+ return "#x%04X" % i
+
+ # The spec range of valid chars is:
+ # 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]"
+ )
+ return re.sub(illegal_xml_re, repl, str(arg))
+
+
+def merge_family(left, right) -> None:
+ result = {}
+ for kl, vl in left.items():
+ for kr, vr in right.items():
+ if not isinstance(vl, list):
+ raise TypeError(type(vl))
+ result[kl] = vl + vr
+ left.update(result)
+
+
+families = {}
+families["_base"] = {"testcase": ["classname", "name"]}
+families["_base_legacy"] = {"testcase": ["file", "line", "url"]}
+
+# xUnit 1.x inherits legacy attributes.
+families["xunit1"] = families["_base"].copy()
+merge_family(families["xunit1"], families["_base_legacy"])
+
+# xUnit 2.x uses strict base attributes.
+families["xunit2"] = families["_base"]
+
+
+class _NodeReporter:
+ def __init__(self, nodeid: Union[str, TestReport], xml: "LogXML") -> None:
+ self.id = nodeid
+ self.xml = xml
+ self.add_stats = self.xml.add_stats
+ self.family = self.xml.family
+ self.duration = 0
+ self.properties: List[Tuple[str, str]] = []
+ self.nodes: List[ET.Element] = []
+ self.attrs: Dict[str, str] = {}
+
+ def append(self, node: ET.Element) -> None:
+ self.xml.add_stats(node.tag)
+ self.nodes.append(node)
+
+ def add_property(self, name: str, value: object) -> None:
+ self.properties.append((str(name), bin_xml_escape(value)))
+
+ def add_attribute(self, name: str, value: object) -> None:
+ self.attrs[str(name)] = bin_xml_escape(value)
+
+ def make_properties_node(self) -> Optional[ET.Element]:
+ """Return a Junit node containing custom properties, if any."""
+ if self.properties:
+ properties = ET.Element("properties")
+ for name, value in self.properties:
+ properties.append(ET.Element("property", name=name, value=value))
+ return properties
+ return None
+
+ def record_testreport(self, testreport: TestReport) -> None:
+ names = mangle_test_address(testreport.nodeid)
+ existing_attrs = self.attrs
+ classnames = names[:-1]
+ if self.xml.prefix:
+ classnames.insert(0, self.xml.prefix)
+ attrs: Dict[str, str] = {
+ "classname": ".".join(classnames),
+ "name": bin_xml_escape(names[-1]),
+ "file": testreport.location[0],
+ }
+ if testreport.location[1] is not None:
+ attrs["line"] = str(testreport.location[1])
+ if hasattr(testreport, "url"):
+ attrs["url"] = testreport.url
+ self.attrs = attrs
+ self.attrs.update(existing_attrs) # Restore any user-defined attributes.
+
+ # Preserve legacy testcase behavior.
+ if self.family == "xunit1":
+ return
+
+ # 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():
+ if key in families[self.family]["testcase"]:
+ temp_attrs[key] = self.attrs[key]
+ self.attrs = temp_attrs
+
+ def to_xml(self) -> ET.Element:
+ testcase = ET.Element("testcase", self.attrs, time="%.3f" % self.duration)
+ properties = self.make_properties_node()
+ if properties is not None:
+ testcase.append(properties)
+ testcase.extend(self.nodes)
+ return testcase
+
+ def _add_simple(self, tag: str, message: str, data: Optional[str] = None) -> None:
+ node = ET.Element(tag, message=message)
+ node.text = bin_xml_escape(data)
+ self.append(node)
+
+ def write_captured_output(self, report: TestReport) -> None:
+ if not self.xml.log_passing_tests and report.passed:
+ return
+
+ content_out = report.capstdout
+ content_log = report.caplog
+ content_err = report.capstderr
+ if self.xml.logging == "no":
+ return
+ content_all = ""
+ if self.xml.logging in ["log", "all"]:
+ content_all = self._prepare_content(content_log, " Captured Log ")
+ if self.xml.logging in ["system-out", "out-err", "all"]:
+ content_all += self._prepare_content(content_out, " Captured Out ")
+ self._write_content(report, content_all, "system-out")
+ content_all = ""
+ if self.xml.logging in ["system-err", "out-err", "all"]:
+ content_all += self._prepare_content(content_err, " Captured Err ")
+ self._write_content(report, content_all, "system-err")
+ content_all = ""
+ if content_all:
+ self._write_content(report, content_all, "system-out")
+
+ def _prepare_content(self, content: str, header: str) -> str:
+ return "\n".join([header.center(80, "-"), content, ""])
+
+ def _write_content(self, report: TestReport, content: str, jheader: str) -> None:
+ tag = ET.Element(jheader)
+ tag.text = bin_xml_escape(content)
+ self.append(tag)
+
+ def append_pass(self, report: TestReport) -> None:
+ self.add_stats("passed")
+
+ def append_failure(self, report: TestReport) -> None:
+ # msg = str(report.longrepr.reprtraceback.extraline)
+ if hasattr(report, "wasxfail"):
+ self._add_simple("skipped", "xfail-marked test passes unexpectedly")
+ else:
+ assert report.longrepr is not None
+ reprcrash: Optional[ReprFileLocation] = getattr(
+ report.longrepr, "reprcrash", None
+ )
+ if reprcrash is not None:
+ message = reprcrash.message
+ else:
+ message = str(report.longrepr)
+ message = bin_xml_escape(message)
+ self._add_simple("failure", message, str(report.longrepr))
+
+ def append_collect_error(self, report: TestReport) -> None:
+ # msg = str(report.longrepr.reprtraceback.extraline)
+ assert report.longrepr is not None
+ self._add_simple("error", "collection failure", str(report.longrepr))
+
+ def append_collect_skipped(self, report: TestReport) -> None:
+ self._add_simple("skipped", "collection skipped", str(report.longrepr))
+
+ def append_error(self, report: TestReport) -> None:
+ assert report.longrepr is not None
+ reprcrash: Optional[ReprFileLocation] = getattr(
+ report.longrepr, "reprcrash", None
+ )
+ if reprcrash is not None:
+ reason = reprcrash.message
+ else:
+ reason = str(report.longrepr)
+
+ if report.when == "teardown":
+ msg = f'failed on teardown with "{reason}"'
+ else:
+ msg = f'failed on setup with "{reason}"'
+ self._add_simple("error", msg, str(report.longrepr))
+
+ def append_skipped(self, report: TestReport) -> None:
+ if hasattr(report, "wasxfail"):
+ xfailreason = report.wasxfail
+ if xfailreason.startswith("reason: "):
+ xfailreason = xfailreason[8:]
+ xfailreason = bin_xml_escape(xfailreason)
+ skipped = ET.Element("skipped", type="pytest.xfail", message=xfailreason)
+ self.append(skipped)
+ else:
+ assert isinstance(report.longrepr, tuple)
+ filename, lineno, skipreason = report.longrepr
+ if skipreason.startswith("Skipped: "):
+ skipreason = skipreason[9:]
+ details = f"{filename}:{lineno}: {skipreason}"
+
+ skipped = ET.Element("skipped", type="pytest.skip", message=skipreason)
+ skipped.text = bin_xml_escape(details)
+ self.append(skipped)
+ self.write_captured_output(report)
+
+ def finalize(self) -> None:
+ data = self.to_xml()
+ 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]
+
+
+def _warn_incompatibility_with_xunit2(
+ request: FixtureRequest, fixture_name: str
+) -> None:
+ """Emit a PytestWarning about the given fixture being incompatible with newer xunit revisions."""
+ from _pytest.warning_types import PytestWarning
+
+ xml = request.config.stash.get(xml_key, None)
+ 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
+ )
+ )
+ )
+
+
+@pytest.fixture
+def record_property(request: FixtureRequest) -> Callable[[str, object], None]:
+ """Add extra properties to the calling test.
+
+ User properties become part of the test report and are available to the
+ configured reporters, like JUnit XML.
+
+ The fixture is callable with ``name, value``. The value is automatically
+ XML-encoded.
+
+ Example::
+
+ def test_function(record_property):
+ record_property("example_key", 1)
+ """
+ _warn_incompatibility_with_xunit2(request, "record_property")
+
+ def append_property(name: str, value: object) -> None:
+ request.node.user_properties.append((name, value))
+
+ return append_property
+
+
+@pytest.fixture
+def record_xml_attribute(request: FixtureRequest) -> Callable[[str, object], None]:
+ """Add extra xml attributes to the tag for the calling test.
+
+ The fixture is callable with ``name, value``. The value is
+ automatically XML-encoded.
+ """
+ from _pytest.warning_types import PytestExperimentalApiWarning
+
+ request.node.warn(
+ PytestExperimentalApiWarning("record_xml_attribute is an experimental feature")
+ )
+
+ _warn_incompatibility_with_xunit2(request, "record_xml_attribute")
+
+ # Declare noop
+ def add_attr_noop(name: str, value: object) -> None:
+ pass
+
+ attr_func = add_attr_noop
+
+ xml = request.config.stash.get(xml_key, None)
+ if xml is not None:
+ node_reporter = xml.node_reporter(request.node.nodeid)
+ attr_func = node_reporter.add_attribute
+
+ return attr_func
+
+
+def _check_record_param_type(param: str, v: str) -> None:
+ """Used by record_testsuite_property to check that the given parameter name is of the proper
+ type."""
+ __tracebackhide__ = True
+ if not isinstance(v, str):
+ msg = "{param} parameter needs to be a string, but {g} given" # type: ignore[unreachable]
+ raise TypeError(msg.format(param=param, g=type(v).__name__))
+
+
+@pytest.fixture(scope="session")
+def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]:
+ """Record a new ``<property>`` tag as child of the root ``<testsuite>``.
+
+ This is suitable to writing global information regarding the entire test
+ suite, and is compatible with ``xunit2`` JUnit family.
+
+ This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
+
+ .. code-block:: python
+
+ def test_foo(record_testsuite_property):
+ 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.
+
+ .. warning::
+
+ Currently this fixture **does not work** with the
+ `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."""
+ __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
+ return record_func
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("terminal reporting")
+ group.addoption(
+ "--junitxml",
+ "--junit-xml",
+ action="store",
+ dest="xmlpath",
+ metavar="path",
+ type=functools.partial(filename_arg, optname="--junitxml"),
+ default=None,
+ help="create junit-xml style report file at given path.",
+ )
+ group.addoption(
+ "--junitprefix",
+ "--junit-prefix",
+ action="store",
+ metavar="str",
+ default=None,
+ help="prepend prefix to classnames in junit-xml output",
+ )
+ parser.addini(
+ "junit_suite_name", "Test suite name for JUnit report", default="pytest"
+ )
+ parser.addini(
+ "junit_logging",
+ "Write captured log messages to JUnit report: "
+ "one of no|log|system-out|system-err|out-err|all",
+ default="no",
+ )
+ parser.addini(
+ "junit_log_passing_tests",
+ "Capture log information for passing tests to JUnit report: ",
+ type="bool",
+ default=True,
+ )
+ parser.addini(
+ "junit_duration_report",
+ "Duration time to report: one of total|call",
+ default="total",
+ ) # choices=['total', 'call'])
+ parser.addini(
+ "junit_family",
+ "Emit XML for schema: one of legacy|xunit1|xunit2",
+ default="xunit2",
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ xmlpath = config.option.xmlpath
+ # Prevent opening xmllog on worker nodes (xdist).
+ if xmlpath and not hasattr(config, "workerinput"):
+ junit_family = config.getini("junit_family")
+ config.stash[xml_key] = LogXML(
+ xmlpath,
+ config.option.junitprefix,
+ config.getini("junit_suite_name"),
+ config.getini("junit_logging"),
+ config.getini("junit_duration_report"),
+ junit_family,
+ config.getini("junit_log_passing_tests"),
+ )
+ config.pluginmanager.register(config.stash[xml_key])
+
+
+def pytest_unconfigure(config: Config) -> None:
+ xml = config.stash.get(xml_key, None)
+ if xml:
+ del config.stash[xml_key]
+ config.pluginmanager.unregister(xml)
+
+
+def mangle_test_address(address: str) -> List[str]:
+ path, possible_open_bracket, params = address.partition("[")
+ names = path.split("::")
+ # Convert file path to dotted path.
+ names[0] = names[0].replace(nodes.SEP, ".")
+ names[0] = re.sub(r"\.py$", "", names[0])
+ # Put any params back.
+ names[-1] += possible_open_bracket + params
+ return names
+
+
+class LogXML:
+ def __init__(
+ self,
+ logfile,
+ prefix: Optional[str],
+ suite_name: str = "pytest",
+ logging: str = "no",
+ report_duration: str = "total",
+ family="xunit1",
+ log_passing_tests: bool = True,
+ ) -> None:
+ logfile = os.path.expanduser(os.path.expandvars(logfile))
+ self.logfile = os.path.normpath(os.path.abspath(logfile))
+ self.prefix = prefix
+ self.suite_name = suite_name
+ self.logging = logging
+ self.log_passing_tests = log_passing_tests
+ self.report_duration = report_duration
+ self.family = family
+ self.stats: Dict[str, int] = dict.fromkeys(
+ ["error", "passed", "failure", "skipped"], 0
+ )
+ self.node_reporters: Dict[
+ Tuple[Union[str, TestReport], object], _NodeReporter
+ ] = {}
+ self.node_reporters_ordered: List[_NodeReporter] = []
+ self.global_properties: List[Tuple[str, str]] = []
+
+ # List of reports that failed on call but teardown is pending.
+ self.open_reports: List[TestReport] = []
+ self.cnt_double_fail_tests = 0
+
+ # Replaces convenience family with real family.
+ if self.family == "legacy":
+ self.family = "xunit1"
+
+ def finalize(self, report: TestReport) -> None:
+ nodeid = getattr(report, "nodeid", report)
+ # Local hack to handle xdist report order.
+ workernode = getattr(report, "node", None)
+ reporter = self.node_reporters.pop((nodeid, workernode))
+ if reporter is not None:
+ reporter.finalize()
+
+ def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter:
+ nodeid: Union[str, TestReport] = getattr(report, "nodeid", report)
+ # Local hack to handle xdist report order.
+ workernode = getattr(report, "node", None)
+
+ key = nodeid, workernode
+
+ if key in self.node_reporters:
+ # TODO: breaks for --dist=each
+ return self.node_reporters[key]
+
+ reporter = _NodeReporter(nodeid, self)
+
+ self.node_reporters[key] = reporter
+ self.node_reporters_ordered.append(reporter)
+
+ return reporter
+
+ def add_stats(self, key: str) -> None:
+ if key in self.stats:
+ self.stats[key] += 1
+
+ def _opentestcase(self, report: TestReport) -> _NodeReporter:
+ reporter = self.node_reporter(report)
+ reporter.record_testreport(report)
+ return reporter
+
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
+ """Handle a setup/call/teardown report, generating the appropriate
+ XML tags as necessary.
+
+ Note: due to plugins like xdist, this hook may be called in interlaced
+ order with reports from other nodes. For example:
+
+ Usual call order:
+ -> setup node1
+ -> call node1
+ -> teardown node1
+ -> setup node2
+ -> call node2
+ -> teardown node2
+
+ Possible call order in xdist:
+ -> setup node1
+ -> call node1
+ -> setup node2
+ -> call node2
+ -> teardown node2
+ -> teardown node1
+ """
+ close_report = None
+ if report.passed:
+ if report.when == "call": # ignore setup/teardown
+ reporter = self._opentestcase(report)
+ reporter.append_pass(report)
+ elif report.failed:
+ if report.when == "teardown":
+ # The following vars are needed when xdist plugin is used.
+ report_wid = getattr(report, "worker_id", None)
+ report_ii = getattr(report, "item_index", None)
+ close_report = next(
+ (
+ rep
+ for rep in self.open_reports
+ if (
+ rep.nodeid == report.nodeid
+ and getattr(rep, "item_index", None) == report_ii
+ and getattr(rep, "worker_id", None) == report_wid
+ )
+ ),
+ None,
+ )
+ if close_report:
+ # We need to open new testcase in case we have failure in
+ # call and error in teardown in order to follow junit
+ # schema.
+ self.finalize(close_report)
+ self.cnt_double_fail_tests += 1
+ reporter = self._opentestcase(report)
+ if report.when == "call":
+ reporter.append_failure(report)
+ self.open_reports.append(report)
+ if not self.log_passing_tests:
+ reporter.write_captured_output(report)
+ else:
+ reporter.append_error(report)
+ elif report.skipped:
+ reporter = self._opentestcase(report)
+ reporter.append_skipped(report)
+ self.update_testcase_duration(report)
+ if report.when == "teardown":
+ 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)
+ close_report = next(
+ (
+ rep
+ for rep in self.open_reports
+ if (
+ rep.nodeid == report.nodeid
+ and getattr(rep, "item_index", None) == report_ii
+ and getattr(rep, "worker_id", None) == report_wid
+ )
+ ),
+ None,
+ )
+ if close_report:
+ self.open_reports.remove(close_report)
+
+ 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:
+ reporter = self.node_reporter(report)
+ reporter.duration += getattr(report, "duration", 0.0)
+
+ def pytest_collectreport(self, report: TestReport) -> None:
+ if not report.passed:
+ reporter = self._opentestcase(report)
+ if report.failed:
+ reporter.append_collect_error(report)
+ else:
+ reporter.append_collect_skipped(report)
+
+ def pytest_internalerror(self, excrepr: ExceptionRepr) -> None:
+ reporter = self.node_reporter("internal")
+ reporter.attrs.update(classname="pytest", name="internal")
+ reporter._add_simple("error", "internal error", str(excrepr))
+
+ def pytest_sessionstart(self) -> None:
+ self.suite_start_time = timing.time()
+
+ def pytest_sessionfinish(self) -> None:
+ dirname = os.path.dirname(os.path.abspath(self.logfile))
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+
+ with open(self.logfile, "w", encoding="utf-8") as logfile:
+ suite_stop_time = timing.time()
+ suite_time_delta = suite_stop_time - self.suite_start_time
+
+ numtests = (
+ self.stats["passed"]
+ + self.stats["failure"]
+ + self.stats["skipped"]
+ + self.stats["error"]
+ - self.cnt_double_fail_tests
+ )
+ logfile.write('<?xml version="1.0" encoding="utf-8"?>')
+
+ suite_node = ET.Element(
+ "testsuite",
+ name=self.suite_name,
+ errors=str(self.stats["error"]),
+ failures=str(self.stats["failure"]),
+ skipped=str(self.stats["skipped"]),
+ tests=str(numtests),
+ time="%.3f" % suite_time_delta,
+ timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(),
+ hostname=platform.node(),
+ )
+ global_properties = self._get_global_properties_node()
+ if global_properties is not None:
+ suite_node.append(global_properties)
+ for node_reporter in self.node_reporters_ordered:
+ suite_node.append(node_reporter.to_xml())
+ testsuites = ET.Element("testsuites")
+ testsuites.append(suite_node)
+ logfile.write(ET.tostring(testsuites, encoding="unicode"))
+
+ def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None:
+ terminalreporter.write_sep("-", f"generated xml file: {self.logfile}")
+
+ def add_global_property(self, name: str, value: object) -> None:
+ __tracebackhide__ = True
+ _check_record_param_type("name", name)
+ self.global_properties.append((name, bin_xml_escape(value)))
+
+ def _get_global_properties_node(self) -> Optional[ET.Element]:
+ """Return a Junit node containing custom properties, if any."""
+ if self.global_properties:
+ properties = ET.Element("properties")
+ for name, value in self.global_properties:
+ properties.append(ET.Element("property", name=name, value=value))
+ return properties
+ return None
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/legacypath.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/legacypath.py
new file mode 100644
index 0000000000..37e8c24220
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/legacypath.py
@@ -0,0 +1,467 @@
+"""Add backward compatibility support for the legacy py path type."""
+import shlex
+import subprocess
+from pathlib import Path
+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
+from _pytest.config import hookimpl
+from _pytest.config import PytestPluginManager
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.main import Session
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+from _pytest.nodes import Node
+from _pytest.pytester import HookRecorder
+from _pytest.pytester import Pytester
+from _pytest.pytester import RunResult
+from _pytest.terminal import TerminalReporter
+from _pytest.tmpdir import TempPathFactory
+
+if TYPE_CHECKING:
+ from typing_extensions import Final
+
+ import pexpect
+
+
+@final
+class Testdir:
+ """
+ Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead.
+
+ All methods just forward to an internal :class:`Pytester` instance, converting results
+ to `legacy_path` objects as necessary.
+ """
+
+ __test__ = False
+
+ CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN
+ TimeoutExpired: "Final" = Pytester.TimeoutExpired
+
+ def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None:
+ check_ispytest(_ispytest)
+ self._pytester = pytester
+
+ @property
+ def tmpdir(self) -> LEGACY_PATH:
+ """Temporary directory where tests are executed."""
+ return legacy_path(self._pytester.path)
+
+ @property
+ def test_tmproot(self) -> LEGACY_PATH:
+ return legacy_path(self._pytester._test_tmproot)
+
+ @property
+ def request(self):
+ return self._pytester._request
+
+ @property
+ def plugins(self):
+ return self._pytester.plugins
+
+ @plugins.setter
+ def plugins(self, plugins):
+ self._pytester.plugins = plugins
+
+ @property
+ def monkeypatch(self) -> MonkeyPatch:
+ return self._pytester._monkeypatch
+
+ def make_hook_recorder(self, pluginmanager) -> HookRecorder:
+ """See :meth:`Pytester.make_hook_recorder`."""
+ return self._pytester.make_hook_recorder(pluginmanager)
+
+ def chdir(self) -> None:
+ """See :meth:`Pytester.chdir`."""
+ return self._pytester.chdir()
+
+ def finalize(self) -> None:
+ """See :meth:`Pytester._finalize`."""
+ return self._pytester._finalize()
+
+ def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH:
+ """See :meth:`Pytester.makefile`."""
+ if ext and not ext.startswith("."):
+ # pytester.makefile is going to throw a ValueError in a way that
+ # testdir.makefile did not, because
+ # pathlib.Path is stricter suffixes than py.path
+ # This ext arguments is likely user error, but since testdir has
+ # allowed this, we will prepend "." as a workaround to avoid breaking
+ # testdir usage that worked before
+ ext = "." + ext
+ return legacy_path(self._pytester.makefile(ext, *args, **kwargs))
+
+ def makeconftest(self, source) -> LEGACY_PATH:
+ """See :meth:`Pytester.makeconftest`."""
+ return legacy_path(self._pytester.makeconftest(source))
+
+ def makeini(self, source) -> LEGACY_PATH:
+ """See :meth:`Pytester.makeini`."""
+ return legacy_path(self._pytester.makeini(source))
+
+ def getinicfg(self, source: str) -> SectionWrapper:
+ """See :meth:`Pytester.getinicfg`."""
+ return self._pytester.getinicfg(source)
+
+ def makepyprojecttoml(self, source) -> LEGACY_PATH:
+ """See :meth:`Pytester.makepyprojecttoml`."""
+ return legacy_path(self._pytester.makepyprojecttoml(source))
+
+ def makepyfile(self, *args, **kwargs) -> LEGACY_PATH:
+ """See :meth:`Pytester.makepyfile`."""
+ return legacy_path(self._pytester.makepyfile(*args, **kwargs))
+
+ def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH:
+ """See :meth:`Pytester.maketxtfile`."""
+ return legacy_path(self._pytester.maketxtfile(*args, **kwargs))
+
+ def syspathinsert(self, path=None) -> None:
+ """See :meth:`Pytester.syspathinsert`."""
+ return self._pytester.syspathinsert(path)
+
+ def mkdir(self, name) -> LEGACY_PATH:
+ """See :meth:`Pytester.mkdir`."""
+ return legacy_path(self._pytester.mkdir(name))
+
+ def mkpydir(self, name) -> LEGACY_PATH:
+ """See :meth:`Pytester.mkpydir`."""
+ return legacy_path(self._pytester.mkpydir(name))
+
+ def copy_example(self, name=None) -> LEGACY_PATH:
+ """See :meth:`Pytester.copy_example`."""
+ return legacy_path(self._pytester.copy_example(name))
+
+ def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]:
+ """See :meth:`Pytester.getnode`."""
+ return self._pytester.getnode(config, arg)
+
+ def getpathnode(self, path):
+ """See :meth:`Pytester.getpathnode`."""
+ return self._pytester.getpathnode(path)
+
+ def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]:
+ """See :meth:`Pytester.genitems`."""
+ return self._pytester.genitems(colitems)
+
+ def runitem(self, source):
+ """See :meth:`Pytester.runitem`."""
+ return self._pytester.runitem(source)
+
+ def inline_runsource(self, source, *cmdlineargs):
+ """See :meth:`Pytester.inline_runsource`."""
+ return self._pytester.inline_runsource(source, *cmdlineargs)
+
+ def inline_genitems(self, *args):
+ """See :meth:`Pytester.inline_genitems`."""
+ return self._pytester.inline_genitems(*args)
+
+ def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False):
+ """See :meth:`Pytester.inline_run`."""
+ return self._pytester.inline_run(
+ *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc
+ )
+
+ def runpytest_inprocess(self, *args, **kwargs) -> RunResult:
+ """See :meth:`Pytester.runpytest_inprocess`."""
+ return self._pytester.runpytest_inprocess(*args, **kwargs)
+
+ def runpytest(self, *args, **kwargs) -> RunResult:
+ """See :meth:`Pytester.runpytest`."""
+ return self._pytester.runpytest(*args, **kwargs)
+
+ def parseconfig(self, *args) -> Config:
+ """See :meth:`Pytester.parseconfig`."""
+ return self._pytester.parseconfig(*args)
+
+ def parseconfigure(self, *args) -> Config:
+ """See :meth:`Pytester.parseconfigure`."""
+ return self._pytester.parseconfigure(*args)
+
+ def getitem(self, source, funcname="test_func"):
+ """See :meth:`Pytester.getitem`."""
+ return self._pytester.getitem(source, funcname)
+
+ def getitems(self, source):
+ """See :meth:`Pytester.getitems`."""
+ return self._pytester.getitems(source)
+
+ def getmodulecol(self, source, configargs=(), withinit=False):
+ """See :meth:`Pytester.getmodulecol`."""
+ return self._pytester.getmodulecol(
+ source, configargs=configargs, withinit=withinit
+ )
+
+ def collect_by_name(
+ self, modcol: Collector, name: str
+ ) -> Optional[Union[Item, Collector]]:
+ """See :meth:`Pytester.collect_by_name`."""
+ return self._pytester.collect_by_name(modcol, name)
+
+ def popen(
+ self,
+ cmdargs,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ stdin=CLOSE_STDIN,
+ **kw,
+ ):
+ """See :meth:`Pytester.popen`."""
+ return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw)
+
+ def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult:
+ """See :meth:`Pytester.run`."""
+ return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin)
+
+ def runpython(self, script) -> RunResult:
+ """See :meth:`Pytester.runpython`."""
+ return self._pytester.runpython(script)
+
+ def runpython_c(self, command):
+ """See :meth:`Pytester.runpython_c`."""
+ return self._pytester.runpython_c(command)
+
+ def runpytest_subprocess(self, *args, timeout=None) -> RunResult:
+ """See :meth:`Pytester.runpytest_subprocess`."""
+ return self._pytester.runpytest_subprocess(*args, timeout=timeout)
+
+ def spawn_pytest(
+ self, string: str, expect_timeout: float = 10.0
+ ) -> "pexpect.spawn":
+ """See :meth:`Pytester.spawn_pytest`."""
+ return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout)
+
+ def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn":
+ """See :meth:`Pytester.spawn`."""
+ return self._pytester.spawn(cmd, expect_timeout=expect_timeout)
+
+ def __repr__(self) -> str:
+ return f"<Testdir {self.tmpdir!r}>"
+
+ def __str__(self) -> str:
+ return str(self.tmpdir)
+
+
+class LegacyTestdirPlugin:
+ @staticmethod
+ @fixture
+ def testdir(pytester: Pytester) -> Testdir:
+ """
+ Identical to :fixture:`pytester`, and provides an instance whose methods return
+ legacy ``LEGACY_PATH`` objects instead when applicable.
+
+ New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
+ """
+ return Testdir(pytester, _ispytest=True)
+
+
+@final
+@attr.s(init=False, auto_attribs=True)
+class TempdirFactory:
+ """Backward compatibility wrapper that implements :class:``_pytest.compat.LEGACY_PATH``
+ for :class:``TempPathFactory``."""
+
+ _tmppath_factory: TempPathFactory
+
+ def __init__(
+ self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False
+ ) -> None:
+ check_ispytest(_ispytest)
+ 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."""
+ return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve())
+
+ def getbasetemp(self) -> LEGACY_PATH:
+ """Backward compat wrapper for ``_tmppath_factory.getbasetemp``."""
+ return legacy_path(self._tmppath_factory.getbasetemp().resolve())
+
+
+class LegacyTmpdirPlugin:
+ @staticmethod
+ @fixture(scope="session")
+ def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
+ """Return a :class:`pytest.TempdirFactory` instance for the test session."""
+ # Set dynamically by pytest_configure().
+ return request.config._tmpdirhandler # type: ignore
+
+ @staticmethod
+ @fixture
+ def tmpdir(tmp_path: Path) -> LEGACY_PATH:
+ """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`.
+
+ The returned object is a `legacy_path`_ object.
+
+ .. _legacy_path: https://py.readthedocs.io/en/latest/path.html
+ """
+ return legacy_path(tmp_path)
+
+
+def Cache_makedir(self: Cache, name: str) -> LEGACY_PATH:
+ """Return a directory path object with the given name.
+
+ Same as :func:`mkdir`, but returns a legacy py path instance.
+ """
+ return legacy_path(self.mkdir(name))
+
+
+def FixtureRequest_fspath(self: FixtureRequest) -> LEGACY_PATH:
+ """(deprecated) The file system path of the test module which collected this test."""
+ return legacy_path(self.path)
+
+
+def TerminalReporter_startdir(self: TerminalReporter) -> LEGACY_PATH:
+ """The directory from which pytest was invoked.
+
+ Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
+
+ :type: LEGACY_PATH
+ """
+ return legacy_path(self.startpath)
+
+
+def Config_invocation_dir(self: Config) -> LEGACY_PATH:
+ """The directory from which pytest was invoked.
+
+ Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`,
+ which is a :class:`pathlib.Path`.
+
+ :type: LEGACY_PATH
+ """
+ return legacy_path(str(self.invocation_params.dir))
+
+
+def Config_rootdir(self: Config) -> LEGACY_PATH:
+ """The path to the :ref:`rootdir <rootdir>`.
+
+ Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`.
+
+ :type: LEGACY_PATH
+ """
+ return legacy_path(str(self.rootpath))
+
+
+def Config_inifile(self: Config) -> Optional[LEGACY_PATH]:
+ """The path to the :ref:`configfile <configfiles>`.
+
+ Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`.
+
+ :type: Optional[LEGACY_PATH]
+ """
+ return legacy_path(str(self.inipath)) if self.inipath else None
+
+
+def Session_stardir(self: Session) -> LEGACY_PATH:
+ """The path from which pytest was invoked.
+
+ Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
+
+ :type: LEGACY_PATH
+ """
+ return legacy_path(self.startpath)
+
+
+def Config__getini_unknown_type(
+ self, name: str, type: str, value: Union[str, List[str]]
+):
+ if type == "pathlist":
+ # TODO: This assert is probably not valid in all cases.
+ assert self.inipath is not None
+ dp = self.inipath.parent
+ input_values = shlex.split(value) if isinstance(value, str) else value
+ return [legacy_path(str(dp / x)) for x in input_values]
+ else:
+ raise ValueError(f"unknown configuration type: {type}", value)
+
+
+def Node_fspath(self: Node) -> LEGACY_PATH:
+ """(deprecated) returns a legacy_path copy of self.path"""
+ return legacy_path(self.path)
+
+
+def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None:
+ self.path = Path(value)
+
+
+@hookimpl(tryfirst=True)
+def pytest_load_initial_conftests(early_config: Config) -> None:
+ """Monkeypatch legacy path attributes in several classes, as early as possible."""
+ mp = MonkeyPatch()
+ early_config.add_cleanup(mp.undo)
+
+ # Add Cache.makedir().
+ mp.setattr(Cache, "makedir", Cache_makedir, raising=False)
+
+ # Add FixtureRequest.fspath property.
+ mp.setattr(FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False)
+
+ # Add TerminalReporter.startdir property.
+ mp.setattr(
+ TerminalReporter, "startdir", property(TerminalReporter_startdir), raising=False
+ )
+
+ # Add Config.{invocation_dir,rootdir,inifile} properties.
+ mp.setattr(Config, "invocation_dir", property(Config_invocation_dir), raising=False)
+ mp.setattr(Config, "rootdir", property(Config_rootdir), raising=False)
+ mp.setattr(Config, "inifile", property(Config_inifile), raising=False)
+
+ # Add Session.startdir property.
+ mp.setattr(Session, "startdir", property(Session_stardir), raising=False)
+
+ # Add pathlist configuration type.
+ mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type)
+
+ # Add Node.fspath property.
+ mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False)
+
+
+@hookimpl
+def pytest_configure(config: Config) -> None:
+ """Installs the LegacyTmpdirPlugin if the ``tmpdir`` plugin is also installed."""
+ if config.pluginmanager.has_plugin("tmpdir"):
+ mp = MonkeyPatch()
+ config.add_cleanup(mp.undo)
+ # Create TmpdirFactory and attach it to the config object.
+ #
+ # This is to comply with existing plugins which expect the handler to be
+ # available at pytest_configure time, but ideally should be moved entirely
+ # to the tmpdir_factory session fixture.
+ try:
+ tmp_path_factory = config._tmp_path_factory # type: ignore[attr-defined]
+ except AttributeError:
+ # tmpdir plugin is blocked.
+ pass
+ else:
+ _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True)
+ mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False)
+
+ config.pluginmanager.register(LegacyTmpdirPlugin, "legacypath-tmpdir")
+
+
+@hookimpl
+def pytest_plugin_registered(plugin: object, manager: PytestPluginManager) -> None:
+ # pytester is not loaded by default and is commonly loaded from a conftest,
+ # so checking for it in `pytest_configure` is not enough.
+ is_pytester = plugin is manager.get_plugin("pytester")
+ if is_pytester and not manager.is_registered(LegacyTestdirPlugin):
+ manager.register(LegacyTestdirPlugin, "legacypath-pytester")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/logging.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/logging.py
new file mode 100644
index 0000000000..31ad830107
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/logging.py
@@ -0,0 +1,831 @@
+"""Access and control log capturing."""
+import logging
+import os
+import re
+import sys
+from contextlib import contextmanager
+from io import StringIO
+from pathlib import Path
+from typing import AbstractSet
+from typing import Dict
+from typing import Generator
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Tuple
+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
+from _pytest.config import hookimpl
+from _pytest.config import UsageError
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.main import Session
+from _pytest.stash import StashKey
+from _pytest.terminal import TerminalReporter
+
+
+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")
+caplog_handler_key = StashKey["LogCaptureHandler"]()
+caplog_records_key = StashKey[Dict[str, List[logging.LogRecord]]]()
+
+
+def _remove_ansi_escape_sequences(text: str) -> str:
+ return _ANSI_ESCAPE_SEQ.sub("", text)
+
+
+class ColoredLevelFormatter(logging.Formatter):
+ """A logging formatter which colorizes the %(levelname)..s part of the
+ log format passed to __init__."""
+
+ LOGLEVEL_COLOROPTS: Mapping[int, AbstractSet[str]] = {
+ logging.CRITICAL: {"red"},
+ logging.ERROR: {"red", "bold"},
+ logging.WARNING: {"yellow"},
+ logging.WARN: {"yellow"},
+ logging.INFO: {"green"},
+ logging.DEBUG: {"purple"},
+ logging.NOTSET: set(),
+ }
+ LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-.]?\d*(?:\.\d+)?s)")
+
+ def __init__(self, terminalwriter: TerminalWriter, *args, **kwargs) -> None:
+ super().__init__(*args, **kwargs)
+ self._terminalwriter = terminalwriter
+ self._original_fmt = self._style._fmt
+ self._level_to_fmt_mapping: Dict[int, str] = {}
+
+ for level, color_opts in self.LOGLEVEL_COLOROPTS.items():
+ self.add_color_level(level, *color_opts)
+
+ def add_color_level(self, level: int, *color_opts: str) -> None:
+ """Add or update color opts for a log level.
+
+ :param level:
+ Log level to apply a style to, e.g. ``logging.INFO``.
+ :param color_opts:
+ ANSI escape sequence color options. Capitalized colors indicates
+ background color, i.e. ``'green', 'Yellow', 'bold'`` will give bold
+ green text on yellow background.
+
+ .. 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:
+ return
+ levelname_fmt = levelname_fmt_match.group()
+
+ formatted_levelname = levelname_fmt % {"levelname": logging.getLevelName(level)}
+
+ # add ANSI escape sequences around the formatted levelname
+ color_kwargs = {name: True for name in color_opts}
+ colorized_formatted_levelname = self._terminalwriter.markup(
+ formatted_levelname, **color_kwargs
+ )
+ self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub(
+ colorized_formatted_levelname, self._fmt
+ )
+
+ def format(self, record: logging.LogRecord) -> str:
+ fmt = self._level_to_fmt_mapping.get(record.levelno, self._original_fmt)
+ self._style._fmt = fmt
+ return super().format(record)
+
+
+class PercentStyleMultiline(logging.PercentStyle):
+ """A logging style with special support for multiline messages.
+
+ If the message of a record consists of multiple lines, this style
+ formats the message as if each line were logged separately.
+ """
+
+ def __init__(self, fmt: str, auto_indent: Union[int, str, bool, None]) -> None:
+ super().__init__(fmt)
+ self._auto_indent = self._get_auto_indent(auto_indent)
+
+ @staticmethod
+ def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int:
+ """Determine the current auto indentation setting.
+
+ Specify auto indent behavior (on/off/fixed) by passing in
+ extra={"auto_indent": [value]} to the call to logging.log() or
+ using a --log-auto-indent [value] command line or the
+ log_auto_indent [value] config option.
+
+ Default behavior is auto-indent off.
+
+ Using the string "True" or "on" or the boolean True as the value
+ turns auto indent on, using the string "False" or "off" or the
+ boolean False or the int 0 turns it off, and specifying a
+ positive integer fixes the indentation position to the value
+ specified.
+
+ Any other values for the option are invalid, and will silently be
+ converted to the default.
+
+ :param None|bool|int|str auto_indent_option:
+ User specified option for indentation from command line, config
+ or extra kwarg. Accepts int, bool or str. str option accepts the
+ same range of values as boolean config options, as well as
+ positive integers represented in str form.
+
+ :returns:
+ Indentation value, which can be
+ -1 (automatically determine indentation) or
+ 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):
+ if auto_indent_option:
+ return -1
+ else:
+ return 0
+ elif isinstance(auto_indent_option, int):
+ return int(auto_indent_option)
+ elif isinstance(auto_indent_option, str):
+ try:
+ return int(auto_indent_option)
+ except ValueError:
+ pass
+ try:
+ if _strtobool(auto_indent_option):
+ return -1
+ except ValueError:
+ return 0
+
+ return 0
+
+ def format(self, record: logging.LogRecord) -> str:
+ 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]
+ else:
+ auto_indent = self._auto_indent
+
+ if auto_indent:
+ lines = record.message.splitlines()
+ formatted = self._fmt % {**record.__dict__, "message": lines[0]}
+
+ if auto_indent < 0:
+ indentation = _remove_ansi_escape_sequences(formatted).find(
+ lines[0]
+ )
+ else:
+ # Optimizes logging by allowing a fixed indentation.
+ indentation = auto_indent
+ lines[0] = formatted
+ return ("\n" + " " * indentation).join(lines)
+ return self._fmt % record.__dict__
+
+
+def get_option_ini(config: Config, *names: str):
+ for name in names:
+ ret = config.getoption(name) # 'default' arg won't work as expected
+ if ret is None:
+ ret = config.getini(name)
+ if ret:
+ return ret
+
+
+def pytest_addoption(parser: Parser) -> None:
+ """Add options to control log capturing."""
+ group = parser.getgroup("logging")
+
+ def add_option_ini(option, dest, default=None, type=None, **kwargs):
+ parser.addini(
+ dest, default=default, type=type, help="default value for " + option
+ )
+ group.addoption(option, dest=dest, **kwargs)
+
+ add_option_ini(
+ "--log-level",
+ dest="log_level",
+ 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"
+ ' effective level, where it is "WARNING" by default.'
+ ),
+ )
+ add_option_ini(
+ "--log-format",
+ dest="log_format",
+ default=DEFAULT_LOG_FORMAT,
+ help="log format as 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.",
+ )
+ parser.addini(
+ "log_cli",
+ default=False,
+ type="bool",
+ 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."
+ )
+ add_option_ini(
+ "--log-cli-format",
+ dest="log_cli_format",
+ default=None,
+ help="log format as 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.",
+ )
+ add_option_ini(
+ "--log-file",
+ dest="log_file",
+ default=None,
+ help="path to a file when logging will be written to.",
+ )
+ add_option_ini(
+ "--log-file-level",
+ dest="log_file_level",
+ default=None,
+ 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.",
+ )
+ 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.",
+ )
+ add_option_ini(
+ "--log-auto-indent",
+ dest="log_auto_indent",
+ default=None,
+ help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.",
+ )
+
+
+_HandlerType = TypeVar("_HandlerType", bound=logging.Handler)
+
+
+# Not using @contextmanager for performance reasons.
+class catching_logs:
+ """Context manager that prepares the whole logging machinery properly."""
+
+ __slots__ = ("handler", "level", "orig_level")
+
+ def __init__(self, handler: _HandlerType, level: Optional[int] = None) -> None:
+ self.handler = handler
+ self.level = level
+
+ def __enter__(self):
+ root_logger = logging.getLogger()
+ if self.level is not None:
+ self.handler.setLevel(self.level)
+ root_logger.addHandler(self.handler)
+ if self.level is not None:
+ self.orig_level = root_logger.level
+ root_logger.setLevel(min(self.orig_level, self.level))
+ return self.handler
+
+ def __exit__(self, type, value, traceback):
+ 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):
+ """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())
+ self.records: List[logging.LogRecord] = []
+
+ def emit(self, record: logging.LogRecord) -> None:
+ """Keep the log records in a list in addition to the log text."""
+ self.records.append(record)
+ super().emit(record)
+
+ def reset(self) -> None:
+ self.records = []
+ self.stream = StringIO()
+
+ def handleError(self, record: logging.LogRecord) -> None:
+ if logging.raiseExceptions:
+ # Fail the test if the log message is bad (emit failed).
+ # The default behavior of logging is to print "Logging error"
+ # to stderr with the call stack and some extra details.
+ # pytest wants to make such mistakes visible during testing.
+ raise
+
+
+@final
+class LogCaptureFixture:
+ """Provides access and control of log capturing."""
+
+ def __init__(self, item: nodes.Node, *, _ispytest: bool = False) -> None:
+ check_ispytest(_ispytest)
+ self._item = item
+ self._initial_handler_level: Optional[int] = None
+ # Dict of log name -> log level.
+ self._initial_logger_levels: Dict[Optional[str], int] = {}
+
+ def _finalize(self) -> None:
+ """Finalize the fixture.
+
+ This restores the log levels changed by :meth:`set_level`.
+ """
+ # Restore log levels.
+ if self._initial_handler_level is not None:
+ self.handler.setLevel(self._initial_handler_level)
+ for logger_name, level in self._initial_logger_levels.items():
+ logger = logging.getLogger(logger_name)
+ logger.setLevel(level)
+
+ @property
+ def handler(self) -> LogCaptureHandler:
+ """Get the logging handler used by the fixture.
+
+ :rtype: LogCaptureHandler
+ """
+ return self._item.stash[caplog_handler_key]
+
+ def get_records(self, when: str) -> 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".
+
+ :returns: The list of captured records at the given stage.
+ :rtype: List[logging.LogRecord]
+
+ .. versionadded:: 3.4
+ """
+ return self._item.stash[caplog_records_key].get(when, [])
+
+ @property
+ def text(self) -> str:
+ """The formatted log text."""
+ return _remove_ansi_escape_sequences(self.handler.stream.getvalue())
+
+ @property
+ def records(self) -> List[logging.LogRecord]:
+ """The list of log records."""
+ return self.handler.records
+
+ @property
+ def record_tuples(self) -> List[Tuple[str, int, str]]:
+ """A list of a stripped down version of log records intended
+ for use in assertion comparison.
+
+ The format of the tuple is:
+
+ (logger_name, log_level, message)
+ """
+ return [(r.name, r.levelno, r.getMessage()) for r in self.records]
+
+ @property
+ def messages(self) -> List[str]:
+ """A list of format-interpolated log messages.
+
+ Unlike 'records', which contains the format string and parameters for
+ interpolation, log messages in this list are all interpolated.
+
+ Unlike 'text', which contains the output from the handler, log
+ messages in this list are unadorned with levels, timestamps, etc,
+ making exact comparisons more reliable.
+
+ Note that traceback or stack info (from :func:`logging.exception` or
+ the `exc_info` or `stack_info` arguments to the logging functions) is
+ not included, as this is added by the formatter in the handler.
+
+ .. versionadded:: 3.7
+ """
+ return [r.getMessage() for r in self.records]
+
+ def clear(self) -> None:
+ """Reset the list of log records and the captured log text."""
+ self.handler.reset()
+
+ 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.
+
+ .. 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.
+ """
+ logger_obj = logging.getLogger(logger)
+ # Save the original log-level to restore it during teardown.
+ self._initial_logger_levels.setdefault(logger, logger_obj.level)
+ logger_obj.setLevel(level)
+ if self._initial_handler_level is None:
+ self._initial_handler_level = self.handler.level
+ self.handler.setLevel(level)
+
+ @contextmanager
+ def at_level(
+ self, level: Union[int, str], logger: Optional[str] = None
+ ) -> Generator[None, None, None]:
+ """Context manager that sets the level for capturing of logs. After
+ 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.
+ """
+ logger_obj = logging.getLogger(logger)
+ orig_level = logger_obj.level
+ logger_obj.setLevel(level)
+ handler_orig_level = self.handler.level
+ self.handler.setLevel(level)
+ try:
+ yield
+ finally:
+ logger_obj.setLevel(orig_level)
+ self.handler.setLevel(handler_orig_level)
+
+
+@fixture
+def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]:
+ """Access and control log capturing.
+
+ Captured logs are available through the following properties/methods::
+
+ * caplog.messages -> list of format-interpolated log messages
+ * caplog.text -> string containing formatted log output
+ * caplog.records -> list of logging.LogRecord instances
+ * caplog.record_tuples -> list of (logger_name, level, message) tuples
+ * caplog.clear() -> clear captured records and formatted log output string
+ """
+ result = LogCaptureFixture(request.node, _ispytest=True)
+ yield result
+ result._finalize()
+
+
+def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[int]:
+ for setting_name in setting_names:
+ log_level = config.getoption(setting_name)
+ if log_level is None:
+ log_level = config.getini(setting_name)
+ if log_level:
+ break
+ else:
+ return None
+
+ if isinstance(log_level, str):
+ log_level = log_level.upper()
+ try:
+ return int(getattr(logging, log_level, log_level))
+ 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)
+ ) from e
+
+
+# run after terminalreporter/capturemanager are configured
+@hookimpl(trylast=True)
+def pytest_configure(config: Config) -> None:
+ config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
+
+
+class LoggingPlugin:
+ """Attaches to the logging module and captures log messages for each test."""
+
+ def __init__(self, config: Config) -> None:
+ """Create a new plugin to capture log messages.
+
+ The formatter can be safely shared across all handlers so
+ create a single one for the entire test session here.
+ """
+ self._config = config
+
+ # Report logging.
+ self.formatter = self._create_formatter(
+ get_option_ini(config, "log_format"),
+ get_option_ini(config, "log_date_format"),
+ get_option_ini(config, "log_auto_indent"),
+ )
+ self.log_level = get_log_level_for_setting(config, "log_level")
+ self.caplog_handler = LogCaptureHandler()
+ self.caplog_handler.setFormatter(self.formatter)
+ self.report_handler = LogCaptureHandler()
+ self.report_handler.setFormatter(self.formatter)
+
+ # File logging.
+ self.log_file_level = get_log_level_for_setting(config, "log_file_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")
+ 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_format, datefmt=log_file_date_format
+ )
+ self.log_file_handler.setFormatter(log_file_formatter)
+
+ # CLI/live logging.
+ self.log_cli_level = get_log_level_for_setting(
+ config, "log_cli_level", "log_level"
+ )
+ if self._log_cli_enabled():
+ terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
+ capture_manager = config.pluginmanager.get_plugin("capturemanager")
+ # if capturemanager plugin is disabled, live logging still works.
+ self.log_cli_handler: Union[
+ _LiveLoggingStreamHandler, _LiveLoggingNullHandler
+ ] = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
+ else:
+ self.log_cli_handler = _LiveLoggingNullHandler()
+ log_cli_formatter = self._create_formatter(
+ get_option_ini(config, "log_cli_format", "log_format"),
+ get_option_ini(config, "log_cli_date_format", "log_date_format"),
+ get_option_ini(config, "log_auto_indent"),
+ )
+ self.log_cli_handler.setFormatter(log_cli_formatter)
+
+ def _create_formatter(self, log_format, log_date_format, auto_indent):
+ # Color option doesn't exist if terminal plugin is disabled.
+ color = getattr(self._config.option, "color", "no")
+ if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
+ log_format
+ ):
+ formatter: logging.Formatter = ColoredLevelFormatter(
+ create_terminal_writer(self._config), log_format, log_date_format
+ )
+ else:
+ formatter = logging.Formatter(log_format, log_date_format)
+
+ formatter._style = PercentStyleMultiline(
+ formatter._style._fmt, auto_indent=auto_indent
+ )
+
+ return formatter
+
+ def set_log_path(self, fname: str) -> None:
+ """Set the filename parameter for Logging.FileHandler().
+
+ Creates parent directory if it does not exist.
+
+ .. warning::
+ This is an experimental API.
+ """
+ fpath = Path(fname)
+
+ if not fpath.is_absolute():
+ fpath = self._config.rootpath / fpath
+
+ 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()
+ if old_stream:
+ # https://github.com/python/typeshed/pull/5663
+ old_stream.close() # type:ignore[attr-defined]
+
+ def _log_cli_enabled(self):
+ """Return whether live logging is enabled."""
+ enabled = self._config.getoption(
+ "--log-cli-level"
+ ) is not None or self._config.getini("log_cli")
+ if not enabled:
+ return False
+
+ terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter")
+ if terminal_reporter is None:
+ # terminal reporter is disabled e.g. by pytest-xdist.
+ return False
+
+ return True
+
+ @hookimpl(hookwrapper=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
+
+ @hookimpl(hookwrapper=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
+
+ @hookimpl(hookwrapper=True)
+ def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]:
+ if session.config.option.collectonly:
+ yield
+ return
+
+ if self._log_cli_enabled() and self._config.getoption("verbose") < 1:
+ # The verbose flag is needed to avoid messy test progress output.
+ self._config.option.verbose = 1
+
+ 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.
+
+ @hookimpl
+ def pytest_runtest_logstart(self) -> None:
+ self.log_cli_handler.reset()
+ self.log_cli_handler.set_when("start")
+
+ @hookimpl
+ def pytest_runtest_logreport(self) -> None:
+ self.log_cli_handler.set_when("logreport")
+
+ def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]:
+ """Implement the internals of the pytest_runtest_xxx() hooks."""
+ with catching_logs(
+ self.caplog_handler,
+ level=self.log_level,
+ ) as caplog_handler, catching_logs(
+ self.report_handler,
+ level=self.log_level,
+ ) as report_handler:
+ caplog_handler.reset()
+ report_handler.reset()
+ 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)
+
+ @hookimpl(hookwrapper=True)
+ def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]:
+ self.log_cli_handler.set_when("setup")
+
+ empty: Dict[str, List[logging.LogRecord]] = {}
+ item.stash[caplog_records_key] = empty
+ yield from self._runtest_for(item, "setup")
+
+ @hookimpl(hookwrapper=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)
+ 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]
+
+ @hookimpl
+ def pytest_runtest_logfinish(self) -> None:
+ self.log_cli_handler.set_when("finish")
+
+ @hookimpl(hookwrapper=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
+
+ @hookimpl
+ def pytest_unconfigure(self) -> None:
+ # Close the FileHandler explicitly.
+ # (logging.shutdown might have lost the weakref?!)
+ self.log_file_handler.close()
+
+
+class _FileHandler(logging.FileHandler):
+ """A logging FileHandler with pytest tweaks."""
+
+ def handleError(self, record: logging.LogRecord) -> None:
+ # Handled by LogCaptureHandler.
+ pass
+
+
+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.
+
+ During live logging we must also explicitly disable stdout/stderr
+ capturing otherwise it will get captured and won't appear in the
+ terminal.
+ """
+
+ # Officially stream needs to be a IO[str], but TerminalReporter
+ # isn't. So force it.
+ stream: TerminalReporter = None # type: ignore
+
+ def __init__(
+ self,
+ terminal_reporter: TerminalReporter,
+ capture_manager: Optional[CaptureManager],
+ ) -> None:
+ super().__init__(stream=terminal_reporter) # type: ignore[arg-type]
+ self.capture_manager = capture_manager
+ self.reset()
+ self.set_when(None)
+ self._test_outcome_written = False
+
+ def reset(self) -> None:
+ """Reset the handler; should be called before the start of each test."""
+ self._first_record_emitted = False
+
+ def set_when(self, when: Optional[str]) -> None:
+ """Prepare for the given test phase (setup/call/teardown)."""
+ self._when = when
+ self._section_name_shown = False
+ if when == "start":
+ self._test_outcome_written = False
+
+ def emit(self, record: logging.LogRecord) -> None:
+ ctx_manager = (
+ self.capture_manager.global_and_fixture_disabled()
+ if self.capture_manager
+ else nullcontext()
+ )
+ with ctx_manager:
+ if not self._first_record_emitted:
+ self.stream.write("\n")
+ self._first_record_emitted = True
+ elif self._when in ("teardown", "finish"):
+ if not self._test_outcome_written:
+ self._test_outcome_written = True
+ self.stream.write("\n")
+ if not self._section_name_shown and self._when:
+ self.stream.section("live log " + self._when, sep="-", bold=True)
+ self._section_name_shown = True
+ super().emit(record)
+
+ def handleError(self, record: logging.LogRecord) -> None:
+ # Handled by LogCaptureHandler.
+ pass
+
+
+class _LiveLoggingNullHandler(logging.NullHandler):
+ """A logging handler used when live logging is disabled."""
+
+ def reset(self) -> None:
+ pass
+
+ def set_when(self, when: str) -> None:
+ pass
+
+ def handleError(self, record: logging.LogRecord) -> None:
+ # Handled by LogCaptureHandler.
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/main.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/main.py
new file mode 100644
index 0000000000..fea8179ca7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/main.py
@@ -0,0 +1,896 @@
+"""Core implementation of the testing process: init, session, runtest loop."""
+import argparse
+import fnmatch
+import functools
+import importlib
+import os
+import sys
+from pathlib import Path
+from typing import Callable
+from typing import Dict
+from typing import FrozenSet
+from typing import Iterator
+from typing import List
+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 attr
+
+import _pytest._code
+from _pytest import nodes
+from _pytest.compat import final
+from _pytest.config import Config
+from _pytest.config import directory_arg
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config import PytestPluginManager
+from _pytest.config import UsageError
+from _pytest.config.argparsing import Parser
+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.reports import CollectReport
+from _pytest.reports import TestReport
+from _pytest.runner import collect_one_node
+from _pytest.runner import SetupState
+
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addini(
+ "norecursedirs",
+ "directory patterns to avoid for recursion",
+ type="args",
+ default=[
+ "*.egg",
+ ".*",
+ "_darcs",
+ "build",
+ "CVS",
+ "dist",
+ "node_modules",
+ "venv",
+ "{arch}",
+ ],
+ )
+ parser.addini(
+ "testpaths",
+ "directories to search for tests when no files or directories are given in the "
+ "command line.",
+ type="args",
+ default=[],
+ )
+ 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.",
+ )
+ group = parser.getgroup("pytest-warnings")
+ group.addoption(
+ "-W",
+ "--pythonwarnings",
+ action="append",
+ help="set which warnings to report, see -W option of python itself.",
+ )
+ parser.addini(
+ "filterwarnings",
+ type="linelist",
+ help="Each line specifies a pattern for "
+ "warnings.filterwarnings. "
+ "Processed after -W/--pythonwarnings.",
+ )
+ group._addoption(
+ "--maxfail",
+ metavar="num",
+ action="store",
+ type=int,
+ dest="maxfail",
+ default=0,
+ 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.",
+ )
+ group._addoption(
+ "--strict-markers",
+ action="store_true",
+ 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.",
+ )
+ group._addoption(
+ "-c",
+ metavar="file",
+ type=str,
+ dest="inifilename",
+ 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.",
+ )
+ group._addoption(
+ "--rootdir",
+ action="store",
+ dest="rootdir",
+ help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', "
+ "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: "
+ "'$HOME/root_dir'.",
+ )
+
+ group = parser.getgroup("collect", "collection")
+ group.addoption(
+ "--collectonly",
+ "--collect-only",
+ "--co",
+ action="store_true",
+ help="only collect tests, don't execute them.",
+ )
+ group.addoption(
+ "--pyargs",
+ action="store_true",
+ help="try to interpret all arguments as python packages.",
+ )
+ group.addoption(
+ "--ignore",
+ action="append",
+ metavar="path",
+ help="ignore path during collection (multi-allowed).",
+ )
+ group.addoption(
+ "--ignore-glob",
+ action="append",
+ metavar="path",
+ 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).",
+ )
+ group.addoption(
+ "--confcutdir",
+ dest="confcutdir",
+ default=None,
+ metavar="dir",
+ type=functools.partial(directory_arg, optname="--confcutdir"),
+ 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.",
+ )
+ group.addoption(
+ "--keepduplicates",
+ "--keep-duplicates",
+ action="store_true",
+ dest="keepduplicates",
+ default=False,
+ help="Keep duplicate tests.",
+ )
+ group.addoption(
+ "--collect-in-virtualenv",
+ action="store_true",
+ dest="collect_in_virtualenv",
+ default=False,
+ help="Don't ignore tests in a local virtualenv directory",
+ )
+ group.addoption(
+ "--import-mode",
+ 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.",
+ )
+
+ group = parser.getgroup("debugconfig", "test session debugging and configuration")
+ group.addoption(
+ "--basetemp",
+ dest="basetemp",
+ default=None,
+ type=validate_basetemp,
+ metavar="dir",
+ help=(
+ "base temporary directory for this test run."
+ "(warning: this directory is removed if it exists)"
+ ),
+ )
+
+
+def validate_basetemp(path: str) -> str:
+ # GH 7119
+ msg = "basetemp must not be empty, the current working directory or any parent directory of it"
+
+ # empty path
+ if not path:
+ raise argparse.ArgumentTypeError(msg)
+
+ def is_ancestor(base: Path, query: Path) -> bool:
+ """Return whether query is an ancestor of base."""
+ if base == query:
+ return True
+ return query in base.parents
+
+ # check if path is an ancestor of cwd
+ if is_ancestor(Path.cwd(), Path(path).absolute()):
+ raise argparse.ArgumentTypeError(msg)
+
+ # check symlinks for ancestors
+ if is_ancestor(Path.cwd().resolve(), Path(path).resolve()):
+ raise argparse.ArgumentTypeError(msg)
+
+ return path
+
+
+def wrap_session(
+ config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]]
+) -> Union[int, ExitCode]:
+ """Skeleton command line program."""
+ session = Session.from_config(config)
+ session.exitstatus = ExitCode.OK
+ initstate = 0
+ try:
+ try:
+ config._do_configure()
+ initstate = 1
+ config.hook.pytest_sessionstart(session=session)
+ initstate = 2
+ session.exitstatus = doit(config, session) or 0
+ except UsageError:
+ session.exitstatus = ExitCode.USAGE_ERROR
+ raise
+ except Failed:
+ session.exitstatus = ExitCode.TESTS_FAILED
+ except (KeyboardInterrupt, exit.Exception):
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ exitstatus: Union[int, ExitCode] = ExitCode.INTERRUPTED
+ if isinstance(excinfo.value, exit.Exception):
+ if excinfo.value.returncode is not None:
+ exitstatus = excinfo.value.returncode
+ if initstate < 2:
+ sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n")
+ config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
+ session.exitstatus = exitstatus
+ except BaseException:
+ session.exitstatus = ExitCode.INTERNAL_ERROR
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ try:
+ config.notify_exception(excinfo, config.option)
+ except exit.Exception as exc:
+ if exc.returncode is not None:
+ session.exitstatus = exc.returncode
+ sys.stderr.write(f"{type(exc).__name__}: {exc}\n")
+ else:
+ if isinstance(excinfo.value, SystemExit):
+ sys.stderr.write("mainloop: caught unexpected SystemExit!\n")
+
+ finally:
+ # Explicitly break reference cycle.
+ excinfo = None # type: ignore
+ os.chdir(session.startpath)
+ if initstate >= 2:
+ try:
+ config.hook.pytest_sessionfinish(
+ session=session, exitstatus=session.exitstatus
+ )
+ except exit.Exception as exc:
+ if exc.returncode is not None:
+ session.exitstatus = exc.returncode
+ sys.stderr.write(f"{type(exc).__name__}: {exc}\n")
+ config._ensure_unconfigure()
+ return session.exitstatus
+
+
+def pytest_cmdline_main(config: Config) -> Union[int, ExitCode]:
+ return wrap_session(config, _main)
+
+
+def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]:
+ """Default command line protocol for initialization, session,
+ running tests and reporting."""
+ config.hook.pytest_collection(session=session)
+ config.hook.pytest_runtestloop(session=session)
+
+ if session.testsfailed:
+ return ExitCode.TESTS_FAILED
+ elif session.testscollected == 0:
+ return ExitCode.NO_TESTS_COLLECTED
+ return None
+
+
+def pytest_collection(session: "Session") -> None:
+ session.perform_collect()
+
+
+def pytest_runtestloop(session: "Session") -> bool:
+ if session.testsfailed and not session.config.option.continue_on_collection_errors:
+ raise session.Interrupted(
+ "%d error%s during collection"
+ % (session.testsfailed, "s" if session.testsfailed != 1 else "")
+ )
+
+ if session.config.option.collectonly:
+ return True
+
+ for i, item in enumerate(session.items):
+ nextitem = session.items[i + 1] if i + 1 < len(session.items) else None
+ item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
+ if session.shouldfail:
+ raise session.Failed(session.shouldfail)
+ if session.shouldstop:
+ raise session.Interrupted(session.shouldstop)
+ return True
+
+
+def _in_venv(path: Path) -> bool:
+ """Attempt to detect if ``path`` is the root of a Virtual Environment by
+ checking for the existence of the appropriate activate script."""
+ bindir = path.joinpath("Scripts" if sys.platform.startswith("win") else "bin")
+ try:
+ if not bindir.is_dir():
+ return False
+ except OSError:
+ return False
+ activates = (
+ "activate",
+ "activate.csh",
+ "activate.fish",
+ "Activate",
+ "Activate.bat",
+ "Activate.ps1",
+ )
+ return any(fname.name in activates for fname in bindir.iterdir())
+
+
+def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:
+ ignore_paths = config._getconftest_pathlist(
+ "collect_ignore", path=collection_path.parent, rootpath=config.rootpath
+ )
+ ignore_paths = ignore_paths or []
+ excludeopt = config.getoption("ignore")
+ if excludeopt:
+ ignore_paths.extend(absolutepath(x) for x in excludeopt)
+
+ if collection_path in ignore_paths:
+ return True
+
+ ignore_globs = config._getconftest_pathlist(
+ "collect_ignore_glob", path=collection_path.parent, rootpath=config.rootpath
+ )
+ ignore_globs = ignore_globs or []
+ excludeglobopt = config.getoption("ignore_glob")
+ if excludeglobopt:
+ ignore_globs.extend(absolutepath(x) for x in excludeglobopt)
+
+ if any(fnmatch.fnmatch(str(collection_path), str(glob)) for glob in ignore_globs):
+ return True
+
+ allow_in_venv = config.getoption("collect_in_virtualenv")
+ if not allow_in_venv and _in_venv(collection_path):
+ return True
+ return None
+
+
+def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> None:
+ deselect_prefixes = tuple(config.getoption("deselect") or [])
+ if not deselect_prefixes:
+ return
+
+ remaining = []
+ deselected = []
+ for colitem in items:
+ if colitem.nodeid.startswith(deselect_prefixes):
+ deselected.append(colitem)
+ else:
+ remaining.append(colitem)
+
+ if deselected:
+ config.hook.pytest_deselected(items=deselected)
+ items[:] = remaining
+
+
+class FSHookProxy:
+ def __init__(self, pm: PytestPluginManager, remove_mods) -> None:
+ self.pm = pm
+ self.remove_mods = remove_mods
+
+ def __getattr__(self, name: str):
+ x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
+ self.__dict__[name] = x
+ return x
+
+
+class Interrupted(KeyboardInterrupt):
+ """Signals that the test run was interrupted."""
+
+ __module__ = "builtins" # For py3.
+
+
+class Failed(Exception):
+ """Signals a stop as failed test run."""
+
+
+@attr.s(slots=True, auto_attribs=True)
+class _bestrelpath_cache(Dict[Path, str]):
+ path: Path
+
+ def __missing__(self, path: Path) -> str:
+ r = bestrelpath(self.path, path)
+ self[path] = r
+ return r
+
+
+@final
+class Session(nodes.FSCollector):
+ Interrupted = Interrupted
+ Failed = Failed
+ # Set on the session by runner.pytest_sessionstart.
+ _setupstate: SetupState
+ # Set on the session by fixtures.pytest_sessionstart.
+ _fixturemanager: FixtureManager
+ exitstatus: Union[int, ExitCode]
+
+ def __init__(self, config: Config) -> None:
+ super().__init__(
+ path=config.rootpath,
+ fspath=None,
+ parent=None,
+ config=config,
+ session=self,
+ nodeid="",
+ )
+ self.testsfailed = 0
+ self.testscollected = 0
+ 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._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath)
+
+ self.config.pluginmanager.register(self, name="session")
+
+ @classmethod
+ def from_config(cls, config: Config) -> "Session":
+ session: Session = cls._create(config=config)
+ return session
+
+ def __repr__(self) -> str:
+ return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % (
+ self.__class__.__name__,
+ self.name,
+ getattr(self, "exitstatus", "<UNSET>"),
+ self.testsfailed,
+ self.testscollected,
+ )
+
+ @property
+ def startpath(self) -> Path:
+ """The path from which pytest was invoked.
+
+ .. versionadded:: 7.0.0
+ """
+ return self.config.invocation_params.dir
+
+ def _node_location_to_relpath(self, node_path: Path) -> str:
+ # bestrelpath is a quite slow function.
+ return self._bestrelpathcache[node_path]
+
+ @hookimpl(tryfirst=True)
+ def pytest_collectstart(self) -> None:
+ if self.shouldfail:
+ raise self.Failed(self.shouldfail)
+ if self.shouldstop:
+ raise self.Interrupted(self.shouldstop)
+
+ @hookimpl(tryfirst=True)
+ def pytest_runtest_logreport(
+ self, report: Union[TestReport, CollectReport]
+ ) -> None:
+ if report.failed and not hasattr(report, "wasxfail"):
+ self.testsfailed += 1
+ maxfail = self.config.getvalue("maxfail")
+ if maxfail and self.testsfailed >= maxfail:
+ self.shouldfail = "stopping after %d failures" % (self.testsfailed)
+
+ pytest_collectreport = pytest_runtest_logreport
+
+ def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
+ # Optimization: Path(Path(...)) is much slower than isinstance.
+ path_ = path if isinstance(path, Path) else Path(path)
+ return path_ in self._initialpaths
+
+ def gethookproxy(self, fspath: "os.PathLike[str]"):
+ # 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,
+ )
+ remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
+ 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))
+ 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
+ ) -> 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 ()
+
+ 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]
+
+ @overload
+ def perform_collect(
+ self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ...
+ ) -> Sequence[nodes.Item]:
+ ...
+
+ @overload
+ def perform_collect(
+ self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
+ ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
+ ...
+
+ def perform_collect(
+ self, args: Optional[Sequence[str]] = None, genitems: bool = True
+ ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
+ """Perform the collection phase for this session.
+
+ This is called by the default :hook:`pytest_collection` hook
+ implementation; see the documentation of this hook for more details.
+ For testing purposes, it may also be called directly on a fresh
+ ``Session``.
+
+ This function normally recursively expands any collectors collected
+ from the session to their items, and only items are returned. For
+ testing purposes, this may be suppressed by passing ``genitems=False``,
+ in which case the return value contains these collectors unexpanded,
+ and ``session.items`` is empty.
+ """
+ if args is None:
+ args = self.config.args
+
+ 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
+
+ items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items
+ try:
+ initialpaths: List[Path] = []
+ for arg in args:
+ fspath, parts = 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._initialpaths = frozenset(initialpaths)
+ 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}")
+ raise UsageError(*errors)
+ if not genitems:
+ items = rep.result
+ else:
+ if rep.passed:
+ for node in rep.result:
+ self.items.extend(self.genitems(node))
+
+ self.config.pluginmanager.check_pending()
+ hook.pytest_collection_modifyitems(
+ session=self, config=self.config, items=items
+ )
+ finally:
+ hook.pytest_collection_finish(session=self)
+
+ self.testscollected = len(items)
+ return items
+
+ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
+ from _pytest.python import Package
+
+ # 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] = {}
+
+ # 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] = {}
+
+ # Dirnames of pkgs with dunder-init files.
+ pkg_roots: Dict[str, Package] = {}
+
+ for argpath, names in self._initial_parts:
+ self.trace("processing argument", (argpath, names))
+ 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
+
+ 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.
+ 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
+ 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))
+ 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
+ continue
+
+ yield from matching
+
+ self.trace.root.indent -= 1
+
+ def genitems(
+ self, node: Union[nodes.Item, nodes.Collector]
+ ) -> Iterator[nodes.Item]:
+ self.trace("genitems", node)
+ if isinstance(node, nodes.Item):
+ node.ihook.pytest_itemcollected(item=node)
+ yield node
+ else:
+ assert isinstance(node, nodes.Collector)
+ rep = collect_one_node(node)
+ if rep.passed:
+ for subnode in rep.result:
+ yield from self.genitems(subnode)
+ 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."""
+ 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
+ if spec is None or spec.origin is None or spec.origin == "namespace":
+ return module_name
+ elif spec.submodule_search_locations:
+ return os.path.dirname(spec.origin)
+ else:
+ return spec.origin
+
+
+def resolve_collection_argument(
+ invocation_path: Path, arg: str, *, as_pypath: bool = False
+) -> Tuple[Path, List[str]]:
+ """Parse path arguments optionally containing selection parts and return (fspath, names).
+
+ Command-line arguments can point to files and/or directories, and optionally contain
+ parts for specific tests selection, for example:
+
+ "pkg/tests/test_foo.py::TestClass::test_foo"
+
+ This function ensures the path exists, and returns a tuple:
+
+ (Path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"])
+
+ When as_pypath is True, expects that the command-line argument actually contains
+ module paths instead of file-system paths:
+
+ "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.
+
+ If the path doesn't exist, raise UsageError.
+ If the path is a directory and selection parts are present, raise UsageError.
+ """
+ base, squacket, rest = str(arg).partition("[")
+ strpath, *parts = base.split("::")
+ if parts:
+ parts[-1] = f"{parts[-1]}{squacket}{rest}"
+ if as_pypath:
+ strpath = search_pypath(strpath)
+ fspath = invocation_path / strpath
+ fspath = absolutepath(fspath)
+ if not fspath.exists():
+ msg = (
+ "module or package not found: {arg} (missing __init__.py?)"
+ if as_pypath
+ else "file or directory not found: {arg}"
+ )
+ raise UsageError(msg.format(arg=arg))
+ if parts and fspath.is_dir():
+ msg = (
+ "package argument cannot contain :: selection parts: {arg}"
+ if as_pypath
+ else "directory argument cannot contain :: selection parts: {arg}"
+ )
+ raise UsageError(msg.format(arg=arg))
+ return fspath, parts
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/mark/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/mark/__init__.py
new file mode 100644
index 0000000000..7e082f2e6e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/mark/__init__.py
@@ -0,0 +1,282 @@
+"""Generic mechanism for marking and selecting python functions."""
+import warnings
+from typing import AbstractSet
+from typing import Collection
+from typing import List
+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
+from .structures import get_empty_parameterset_mark
+from .structures import Mark
+from .structures import MARK_GEN
+from .structures import MarkDecorator
+from .structures import MarkGenerator
+from .structures import ParameterSet
+from _pytest.config import Config
+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
+
+
+__all__ = [
+ "MARK_GEN",
+ "Mark",
+ "MarkDecorator",
+ "MarkGenerator",
+ "ParameterSet",
+ "get_empty_parameterset_mark",
+]
+
+
+old_mark_config_key = StashKey[Optional[Config]]()
+
+
+def param(
+ *values: object,
+ marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (),
+ id: Optional[str] = None,
+) -> ParameterSet:
+ """Specify a parameter in `pytest.mark.parametrize`_ calls or
+ :ref:`parametrized fixtures <fixture-parametrize-marks>`.
+
+ .. code-block:: python
+
+ @pytest.mark.parametrize(
+ "test_input,expected",
+ [
+ ("3+5", 8),
+ pytest.param("6*9", 42, marks=pytest.mark.xfail),
+ ],
+ )
+ def test_eval(test_input, expected):
+ 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.
+ """
+ return ParameterSet.param(*values, marks=marks, id=id)
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group._addoption(
+ "-k",
+ action="store",
+ dest="keyword",
+ default="",
+ metavar="EXPRESSION",
+ help="only run tests which match the given substring expression. "
+ "An expression is a python evaluatable 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 "
+ "contains 'test_method' or 'test_other', while -k 'not test_method' "
+ "matches those that don't contain 'test_method' in their names. "
+ "-k 'not test_method and not test_other' will eliminate the matches. "
+ "Additionally keywords are matched to classes and functions "
+ "containing extra names in their 'extra_keyword_matches' set, "
+ "as well as functions which have names assigned directly to them. "
+ "The matching is case-insensitive.",
+ )
+
+ group._addoption(
+ "-m",
+ action="store",
+ dest="markexpr",
+ default="",
+ metavar="MARKEXPR",
+ help="only run tests matching given mark expression.\n"
+ "For example: -m 'mark1 and not mark2'.",
+ )
+
+ group.addoption(
+ "--markers",
+ action="store_true",
+ 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")
+
+
+@hookimpl(tryfirst=True)
+def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
+ import _pytest.config
+
+ if config.option.markers:
+ config._do_configure()
+ tw = _pytest.config.create_terminal_writer(config)
+ for line in config.getini("markers"):
+ parts = line.split(":", 1)
+ name = parts[0]
+ rest = parts[1] if len(parts) == 2 else ""
+ tw.write("@pytest.mark.%s:" % name, bold=True)
+ tw.line(rest)
+ tw.line()
+ config._ensure_unconfigure()
+ return 0
+
+ return None
+
+
+@attr.s(slots=True, auto_attribs=True)
+class KeywordMatcher:
+ """A matcher for keywords.
+
+ Given a list of names, matches any substring of one of these names. The
+ string inclusion check is case-insensitive.
+
+ Will match on the name of colitem, including the names of its parents.
+ Only matches names of items which are either a :class:`Class` or a
+ :class:`Function`.
+
+ Additionally, matches on names in the 'extra_keyword_matches' set of
+ any item, as well as names directly assigned to test functions.
+ """
+
+ _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.
+ import pytest
+
+ for node in item.listchain():
+ if not isinstance(node, pytest.Session):
+ mapped_names.add(node.name)
+
+ # Add the names added as extra keywords to current or parent items.
+ mapped_names.update(item.listextrakeywords())
+
+ # Add the names attached to the current function through direct assignment.
+ function_obj = getattr(item, "function", None)
+ if function_obj:
+ mapped_names.update(function_obj.__dict__)
+
+ # Add the markers to the keywords as we no longer handle them correctly.
+ mapped_names.update(mark.name for mark in item.iter_markers())
+
+ return cls(mapped_names)
+
+ def __call__(self, subname: str) -> bool:
+ subname = subname.lower()
+ names = (name.lower() for name in self._names)
+
+ for name in names:
+ if subname in name:
+ return True
+ return False
+
+
+def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
+ keywordexpr = config.option.keyword.lstrip()
+ 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)):
+ deselected.append(colitem)
+ else:
+ if selectuntil:
+ keywordexpr = None
+ remaining.append(colitem)
+
+ if deselected:
+ config.hook.pytest_deselected(items=deselected)
+ items[:] = remaining
+
+
+@attr.s(slots=True, auto_attribs=True)
+class MarkMatcher:
+ """A matcher for markers which are present.
+
+ Tries to match on any marker names, attached to the given colitem.
+ """
+
+ own_mark_names: AbstractSet[str]
+
+ @classmethod
+ def from_item(cls, item: "Item") -> "MarkMatcher":
+ mark_names = {mark.name for mark in item.iter_markers()}
+ return cls(mark_names)
+
+ def __call__(self, name: str) -> bool:
+ return name in self.own_mark_names
+
+
+def deselect_by_mark(items: "List[Item]", config: Config) -> None:
+ matchexpr = config.option.markexpr
+ if not matchexpr:
+ return
+
+ expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'")
+ remaining: List[Item] = []
+ deselected: List[Item] = []
+ for item in items:
+ if expr.evaluate(MarkMatcher.from_item(item)):
+ remaining.append(item)
+ else:
+ deselected.append(item)
+ if deselected:
+ config.hook.pytest_deselected(items=deselected)
+ items[:] = remaining
+
+
+def _parse_expression(expr: str, exc_message: str) -> Expression:
+ try:
+ return Expression.compile(expr)
+ except ParseError as e:
+ raise UsageError(f"{exc_message}: {expr}: {e}") from None
+
+
+def pytest_collection_modifyitems(items: "List[Item]", config: Config) -> None:
+ deselect_by_keyword(items, config)
+ deselect_by_mark(items, config)
+
+
+def pytest_configure(config: Config) -> None:
+ config.stash[old_mark_config_key] = MARK_GEN._config
+ MARK_GEN._config = config
+
+ empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
+
+ 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)
+ )
+
+
+def pytest_unconfigure(config: Config) -> None:
+ MARK_GEN._config = config.stash.get(old_mark_config_key, None)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/mark/expression.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/mark/expression.py
new file mode 100644
index 0000000000..92220d7723
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/mark/expression.py
@@ -0,0 +1,225 @@
+r"""Evaluate match expressions, as used by `-k` and `-m`.
+
+The grammar is:
+
+expression: expr? EOF
+expr: and_expr ('or' and_expr)*
+and_expr: not_expr ('and' not_expr)*
+not_expr: 'not' not_expr | '(' expr ')' | ident
+ident: (\w|:|\+|-|\.|\[|\]|\\|/)+
+
+The semantics are:
+
+- Empty expression evaluates to False.
+- 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 enum
+import re
+import types
+from typing import Callable
+from typing import Iterator
+from typing import Mapping
+from typing import Optional
+from typing import Sequence
+from typing import TYPE_CHECKING
+
+import attr
+
+if TYPE_CHECKING:
+ from typing import NoReturn
+
+
+__all__ = [
+ "Expression",
+ "ParseError",
+]
+
+
+class TokenType(enum.Enum):
+ LPAREN = "left parenthesis"
+ RPAREN = "right parenthesis"
+ OR = "or"
+ AND = "and"
+ NOT = "not"
+ IDENT = "identifier"
+ EOF = "end of input"
+
+
+@attr.s(frozen=True, slots=True, auto_attribs=True)
+class Token:
+ type: TokenType
+ value: str
+ pos: int
+
+
+class ParseError(Exception):
+ """The expression contains invalid syntax.
+
+ :param column: The column in the line where the error occurred (1-based).
+ :param message: A description of the error.
+ """
+
+ def __init__(self, column: int, message: str) -> None:
+ self.column = column
+ self.message = message
+
+ def __str__(self) -> str:
+ return f"at column {self.column}: {self.message}"
+
+
+class Scanner:
+ __slots__ = ("tokens", "current")
+
+ def __init__(self, input: str) -> None:
+ self.tokens = self.lex(input)
+ self.current = next(self.tokens)
+
+ def lex(self, input: str) -> Iterator[Token]:
+ pos = 0
+ while pos < len(input):
+ if input[pos] in (" ", "\t"):
+ pos += 1
+ elif input[pos] == "(":
+ yield Token(TokenType.LPAREN, "(", pos)
+ pos += 1
+ elif input[pos] == ")":
+ yield Token(TokenType.RPAREN, ")", pos)
+ pos += 1
+ else:
+ match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:])
+ if match:
+ value = match.group(0)
+ if value == "or":
+ yield Token(TokenType.OR, value, pos)
+ elif value == "and":
+ yield Token(TokenType.AND, value, pos)
+ elif value == "not":
+ yield Token(TokenType.NOT, value, pos)
+ else:
+ yield Token(TokenType.IDENT, value, pos)
+ pos += len(value)
+ else:
+ raise ParseError(
+ pos + 1,
+ f'unexpected character "{input[pos]}"',
+ )
+ yield Token(TokenType.EOF, "", pos)
+
+ def accept(self, type: TokenType, *, reject: bool = False) -> Optional[Token]:
+ if self.current.type is type:
+ token = self.current
+ if token.type is not TokenType.EOF:
+ self.current = next(self.tokens)
+ return token
+ if reject:
+ self.reject((type,))
+ return None
+
+ def reject(self, expected: Sequence[TokenType]) -> "NoReturn":
+ raise ParseError(
+ self.current.pos + 1,
+ "expected {}; got {}".format(
+ " OR ".join(type.value for type in expected),
+ self.current.type.value,
+ ),
+ )
+
+
+# True, False and None are legal match expression identifiers,
+# but illegal as Python identifiers. To fix this, this prefix
+# is added to identifiers in the conversion to Python AST.
+IDENT_PREFIX = "$"
+
+
+def expression(s: Scanner) -> ast.Expression:
+ if s.accept(TokenType.EOF):
+ ret: ast.expr = ast.NameConstant(False)
+ else:
+ ret = expr(s)
+ s.accept(TokenType.EOF, reject=True)
+ return ast.fix_missing_locations(ast.Expression(ret))
+
+
+def expr(s: Scanner) -> ast.expr:
+ ret = and_expr(s)
+ while s.accept(TokenType.OR):
+ rhs = and_expr(s)
+ ret = ast.BoolOp(ast.Or(), [ret, rhs])
+ return ret
+
+
+def and_expr(s: Scanner) -> ast.expr:
+ ret = not_expr(s)
+ while s.accept(TokenType.AND):
+ rhs = not_expr(s)
+ ret = ast.BoolOp(ast.And(), [ret, rhs])
+ return ret
+
+
+def not_expr(s: Scanner) -> ast.expr:
+ if s.accept(TokenType.NOT):
+ return ast.UnaryOp(ast.Not(), not_expr(s))
+ if s.accept(TokenType.LPAREN):
+ ret = expr(s)
+ s.accept(TokenType.RPAREN, reject=True)
+ return ret
+ ident = s.accept(TokenType.IDENT)
+ if ident:
+ return ast.Name(IDENT_PREFIX + ident.value, ast.Load())
+ s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT))
+
+
+class MatcherAdapter(Mapping[str, bool]):
+ """Adapts a matcher function to a locals mapping as required by eval()."""
+
+ def __init__(self, matcher: Callable[[str], bool]) -> None:
+ self.matcher = matcher
+
+ def __getitem__(self, key: str) -> bool:
+ return self.matcher(key[len(IDENT_PREFIX) :])
+
+ def __iter__(self) -> Iterator[str]:
+ raise NotImplementedError()
+
+ def __len__(self) -> int:
+ raise NotImplementedError()
+
+
+class Expression:
+ """A compiled match expression as used by -k and -m.
+
+ The expression can be evaluated against different matchers.
+ """
+
+ __slots__ = ("code",)
+
+ def __init__(self, code: types.CodeType) -> None:
+ self.code = code
+
+ @classmethod
+ def compile(self, input: str) -> "Expression":
+ """Compile a match expression.
+
+ :param input: The input expression - one line.
+ """
+ astexpr = expression(Scanner(input))
+ code: types.CodeType = compile(
+ astexpr,
+ filename="<pytest match expression>",
+ mode="eval",
+ )
+ return Expression(code)
+
+ def evaluate(self, matcher: Callable[[str], bool]) -> bool:
+ """Evaluate the match expression.
+
+ :param matcher:
+ Given an identifier, should return whether it matches or not.
+ Should be prepared to handle arbitrary strings as input.
+
+ :returns: Whether the expression matches or not.
+ """
+ ret: bool = eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher))
+ return ret
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/mark/structures.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/mark/structures.py
new file mode 100644
index 0000000000..0e42cd8de5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/mark/structures.py
@@ -0,0 +1,595 @@
+import collections.abc
+import inspect
+import warnings
+from typing import Any
+from typing import Callable
+from typing import Collection
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Mapping
+from typing import MutableMapping
+from typing import NamedTuple
+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
+
+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.outcomes import fail
+from _pytest.warning_types import PytestUnknownMarkWarning
+
+if TYPE_CHECKING:
+ from ..nodes import Node
+
+
+EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
+
+
+def istestfunc(func) -> bool:
+ return callable(func) and getattr(func, "__name__", "<lambda>") != "<lambda>"
+
+
+def get_empty_parameterset_mark(
+ config: Config, argnames: Sequence[str], func
+) -> "MarkDecorator":
+ from ..nodes import Collector
+
+ fs, lineno = getfslineno(func)
+ reason = "got empty parameter set %r, function %s at %s:%d" % (
+ argnames,
+ func.__name__,
+ fs,
+ lineno,
+ )
+
+ requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
+ if requested_mark in ("", None, "skip"):
+ mark = MARK_GEN.skip(reason=reason)
+ elif requested_mark == "xfail":
+ mark = MARK_GEN.xfail(reason=reason, run=False)
+ elif requested_mark == "fail_at_collect":
+ f_name = func.__name__
+ _, lineno = getfslineno(func)
+ raise Collector.CollectError(
+ "Empty parameter set in '%s' at line %d" % (f_name, lineno + 1)
+ )
+ else:
+ raise LookupError(requested_mark)
+ return mark
+
+
+class ParameterSet(
+ NamedTuple(
+ "ParameterSet",
+ [
+ ("values", Sequence[Union[object, NotSetType]]),
+ ("marks", Collection[Union["MarkDecorator", "Mark"]]),
+ ("id", Optional[str]),
+ ],
+ )
+):
+ @classmethod
+ def param(
+ cls,
+ *values: object,
+ marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (),
+ id: Optional[str] = None,
+ ) -> "ParameterSet":
+ if isinstance(marks, MarkDecorator):
+ marks = (marks,)
+ else:
+ assert isinstance(marks, collections.abc.Collection)
+
+ if id is not None:
+ if not isinstance(id, str):
+ raise TypeError(f"Expected id to be a string, got {type(id)}: {id!r}")
+ id = ascii_escaped(id)
+ return cls(values, marks, id)
+
+ @classmethod
+ def extract_from(
+ cls,
+ parameterset: Union["ParameterSet", Sequence[object], object],
+ force_tuple: bool = False,
+ ) -> "ParameterSet":
+ """Extract from an object or objects.
+
+ :param parameterset:
+ A legacy style parameterset that may or may not be a tuple,
+ and may or may not be wrapped into a mess of mark objects.
+
+ :param force_tuple:
+ Enforce tuple wrapping so single argument tuple values
+ don't get decomposed and break tests.
+ """
+
+ if isinstance(parameterset, cls):
+ return parameterset
+ if force_tuple:
+ return cls.param(parameterset)
+ else:
+ # TODO: Refactor to fix this type-ignore. Currently the following
+ # passes type-checking but crashes:
+ #
+ # @pytest.mark.parametrize(('x', 'y'), [1, 2])
+ # def test_foo(x, y): pass
+ return cls(parameterset, marks=[], id=None) # type: ignore[arg-type]
+
+ @staticmethod
+ def _parse_parametrize_args(
+ argnames: Union[str, List[str], Tuple[str, ...]],
+ argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
+ *args,
+ **kwargs,
+ ) -> Tuple[Union[List[str], Tuple[str, ...]], bool]:
+ if not isinstance(argnames, (tuple, list)):
+ argnames = [x.strip() for x in argnames.split(",") if x.strip()]
+ force_tuple = len(argnames) == 1
+ else:
+ force_tuple = False
+ return argnames, force_tuple
+
+ @staticmethod
+ def _parse_parametrize_parameters(
+ argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
+ force_tuple: bool,
+ ) -> List["ParameterSet"]:
+ return [
+ ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
+ ]
+
+ @classmethod
+ def _for_parametrize(
+ cls,
+ argnames: Union[str, List[str], Tuple[str, ...]],
+ argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
+ func,
+ config: Config,
+ nodeid: str,
+ ) -> Tuple[Union[List[str], Tuple[str, ...]], List["ParameterSet"]]:
+ argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
+ parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
+ del argvalues
+
+ if parameters:
+ # Check all parameter sets have the correct number of values.
+ for param in parameters:
+ if len(param.values) != len(argnames):
+ msg = (
+ '{nodeid}: in "parametrize" the number of names ({names_len}):\n'
+ " {names}\n"
+ "must be equal to the number of values ({values_len}):\n"
+ " {values}"
+ )
+ fail(
+ msg.format(
+ nodeid=nodeid,
+ values=param.values,
+ names=argnames,
+ names_len=len(argnames),
+ values_len=len(param.values),
+ ),
+ pytrace=False,
+ )
+ else:
+ # Empty parameter set (likely computed at runtime): create a single
+ # parameter set with NOTSET values, with the "empty parameter set" mark applied to it.
+ mark = get_empty_parameterset_mark(config, argnames, func)
+ parameters.append(
+ ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None)
+ )
+ return argnames, parameters
+
+
+@final
+@attr.s(frozen=True, init=False, auto_attribs=True)
+class Mark:
+ #: Name of the mark.
+ name: str
+ #: Positional arguments of the mark decorator.
+ args: Tuple[Any, ...]
+ #: Keyword arguments of the mark decorator.
+ kwargs: Mapping[str, Any]
+
+ #: Source Mark for ids with parametrize Marks.
+ _param_ids_from: Optional["Mark"] = attr.ib(default=None, repr=False)
+ #: Resolved/generated ids with parametrize Marks.
+ _param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False)
+
+ def __init__(
+ self,
+ name: str,
+ args: Tuple[Any, ...],
+ kwargs: Mapping[str, Any],
+ param_ids_from: Optional["Mark"] = None,
+ param_ids_generated: Optional[Sequence[str]] = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ """:meta private:"""
+ check_ispytest(_ispytest)
+ # Weirdness to bypass frozen=True.
+ object.__setattr__(self, "name", name)
+ object.__setattr__(self, "args", args)
+ object.__setattr__(self, "kwargs", kwargs)
+ object.__setattr__(self, "_param_ids_from", param_ids_from)
+ object.__setattr__(self, "_param_ids_generated", param_ids_generated)
+
+ def _has_param_ids(self) -> bool:
+ return "ids" in self.kwargs or len(self.args) >= 4
+
+ def combined_with(self, other: "Mark") -> "Mark":
+ """Return a new Mark which is a combination of this
+ Mark and another Mark.
+
+ Combines by appending args and merging kwargs.
+
+ :param Mark other: The mark to combine with.
+ :rtype: Mark
+ """
+ assert self.name == other.name
+
+ # Remember source of ids with parametrize Marks.
+ param_ids_from: Optional[Mark] = None
+ if self.name == "parametrize":
+ if other._has_param_ids():
+ param_ids_from = other
+ elif self._has_param_ids():
+ param_ids_from = self
+
+ return Mark(
+ self.name,
+ self.args + other.args,
+ dict(self.kwargs, **other.kwargs),
+ param_ids_from=param_ids_from,
+ _ispytest=True,
+ )
+
+
+# A generic parameter designating an object to which a Mark may
+# be applied -- a test function (callable) or class.
+# Note: a lambda is not allowed, but this can't be represented.
+Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
+
+
+@attr.s(init=False, auto_attribs=True)
+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
+
+ and can then be applied as decorators to test functions::
+
+ @mark2
+ def test_function():
+ pass
+
+ When a ``MarkDecorator`` is called, it does the following:
+
+ 1. If called with a single class as its only positional argument and no
+ additional keyword arguments, it attaches the mark to the class so it
+ gets applied automatically to all test cases found in that class.
+
+ 2. If called with a single function as its only positional argument and
+ no additional keyword arguments, it attaches the mark to the function,
+ containing all the arguments already stored internally in the
+ ``MarkDecorator``.
+
+ 3. When called in any other case, it returns a new ``MarkDecorator``
+ instance with the original ``MarkDecorator``'s content updated with
+ the arguments passed to this call.
+
+ Note: The rules above prevent a ``MarkDecorator`` from storing only a
+ single function or class reference as its positional argument with no
+ additional keyword or positional arguments. You can work around this by
+ using `with_args()`.
+ """
+
+ mark: Mark
+
+ def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None:
+ """:meta private:"""
+ check_ispytest(_ispytest)
+ self.mark = mark
+
+ @property
+ def name(self) -> str:
+ """Alias for mark.name."""
+ return self.mark.name
+
+ @property
+ def args(self) -> Tuple[Any, ...]:
+ """Alias for mark.args."""
+ return self.mark.args
+
+ @property
+ def kwargs(self) -> Mapping[str, Any]:
+ """Alias for mark.kwargs."""
+ return self.mark.kwargs
+
+ @property
+ def markname(self) -> str:
+ """:meta private:"""
+ return self.name # for backward-compat (2.4.1 had this attr)
+
+ def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator":
+ """Return a MarkDecorator with extra arguments added.
+
+ Unlike calling the MarkDecorator, with_args() can be used even
+ if the sole argument is a callable/class.
+ """
+ mark = Mark(self.name, args, kwargs, _ispytest=True)
+ return MarkDecorator(self.mark.combined_with(mark), _ispytest=True)
+
+ # Type ignored because the overloads overlap with an incompatible
+ # 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]
+ pass
+
+ @overload
+ def __call__(self, *args: object, **kwargs: object) -> "MarkDecorator":
+ pass
+
+ def __call__(self, *args: object, **kwargs: object):
+ """Call the MarkDecorator."""
+ if args and not kwargs:
+ func = args[0]
+ is_class = inspect.isclass(func)
+ if len(args) == 1 and (istestfunc(func) or is_class):
+ store_mark(func, self.mark)
+ 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 normalize_mark_list(
+ mark_list: Iterable[Union[Mark, MarkDecorator]]
+) -> Iterable[Mark]:
+ """
+ Normalize an iterable of Mark or MarkDecorator objects into a list of marks
+ by retrieving the `mark` attribute on MarkDecorator instances.
+
+ :param mark_list: marks to normalize
+ :returns: A new list of the extracted Mark objects
+ """
+ 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")
+ yield mark_obj
+
+
+def store_mark(obj, mark: Mark) -> None:
+ """Store a Mark on an object.
+
+ This is used to implement the Mark declarations/decorators correctly.
+ """
+ assert isinstance(mark, Mark), mark
+ # Always reassign name to avoid updating pytestmark in a reference that
+ # was only borrowed.
+ obj.pytestmark = [*get_unpacked_marks(obj), mark]
+
+
+# Typing for builtin pytest marks. This is cheating; it gives builtin marks
+# special privilege, and breaks modularity. But practicality beats purity...
+if TYPE_CHECKING:
+ from _pytest.scope import _ScopeName
+
+ class _SkipMarkDecorator(MarkDecorator):
+ @overload # type: ignore[override,misc]
+ def __call__(self, arg: Markable) -> Markable:
+ ...
+
+ @overload
+ def __call__(self, reason: str = ...) -> "MarkDecorator":
+ ...
+
+ class _SkipifMarkDecorator(MarkDecorator):
+ def __call__( # type: ignore[override]
+ self,
+ condition: Union[str, bool] = ...,
+ *conditions: Union[str, bool],
+ reason: str = ...,
+ ) -> MarkDecorator:
+ ...
+
+ class _XfailMarkDecorator(MarkDecorator):
+ @overload # type: ignore[override,misc]
+ def __call__(self, arg: Markable) -> Markable:
+ ...
+
+ @overload
+ def __call__(
+ self,
+ condition: Union[str, bool] = ...,
+ *conditions: Union[str, bool],
+ reason: str = ...,
+ run: bool = ...,
+ raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ...,
+ strict: bool = ...,
+ ) -> MarkDecorator:
+ ...
+
+ class _ParametrizeMarkDecorator(MarkDecorator):
+ def __call__( # type: ignore[override]
+ self,
+ argnames: Union[str, List[str], Tuple[str, ...]],
+ argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
+ *,
+ indirect: Union[bool, Sequence[str]] = ...,
+ ids: Optional[
+ Union[
+ Iterable[Union[None, str, float, int, bool]],
+ Callable[[Any], Optional[object]],
+ ]
+ ] = ...,
+ scope: Optional[_ScopeName] = ...,
+ ) -> MarkDecorator:
+ ...
+
+ class _UsefixturesMarkDecorator(MarkDecorator):
+ def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]
+ ...
+
+ class _FilterwarningsMarkDecorator(MarkDecorator):
+ def __call__(self, *filters: str) -> MarkDecorator: # type: ignore[override]
+ ...
+
+
+@final
+class MarkGenerator:
+ """Factory for :class:`MarkDecorator` objects - exposed as
+ a ``pytest.mark`` singleton instance.
+
+ Example::
+
+ import pytest
+
+ @pytest.mark.slowtest
+ def test_function():
+ pass
+
+ applies a 'slowtest' :class:`Mark` on ``test_function``.
+ """
+
+ # See TYPE_CHECKING above.
+ if TYPE_CHECKING:
+ skip: _SkipMarkDecorator
+ skipif: _SkipifMarkDecorator
+ xfail: _XfailMarkDecorator
+ parametrize: _ParametrizeMarkDecorator
+ usefixtures: _UsefixturesMarkDecorator
+ filterwarnings: _FilterwarningsMarkDecorator
+
+ def __init__(self, *, _ispytest: bool = False) -> None:
+ check_ispytest(_ispytest)
+ self._config: Optional[Config] = None
+ self._markers: Set[str] = set()
+
+ def __getattr__(self, name: str) -> MarkDecorator:
+ """Generate a new :class:`MarkDecorator` with the given name."""
+ if name[0] == "_":
+ raise AttributeError("Marker name must NOT start with underscore")
+
+ if self._config is not None:
+ # We store a set of markers as a performance optimisation - if a mark
+ # name is in the set we definitely know it, but a mark may be known and
+ # not in the set. We therefore start by updating the set!
+ if name not in self._markers:
+ for line in self._config.getini("markers"):
+ # example lines: "skipif(condition): skip the given test if..."
+ # or "hypothesis: tests which use Hypothesis", so to get the
+ # marker name we split on both `:` and `(`.
+ marker = line.split(":")[0].split("(")[0].strip()
+ self._markers.add(marker)
+
+ # If the name is not in the set of known marks after updating,
+ # then it really is time to issue a warning or an error.
+ if name not in self._markers:
+ if self._config.option.strict_markers or self._config.option.strict:
+ fail(
+ f"{name!r} not found in `markers` configuration option",
+ pytrace=False,
+ )
+
+ # Raise a specific error for common misspellings of "parametrize".
+ if name in ["parameterize", "parametrise", "parameterise"]:
+ __tracebackhide__ = True
+ fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
+
+ warnings.warn(
+ "Unknown pytest.mark.%s - is this a typo? You can register "
+ "custom marks to avoid this warning - for details, see "
+ "https://docs.pytest.org/en/stable/how-to/mark.html" % name,
+ PytestUnknownMarkWarning,
+ 2,
+ )
+
+ return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True)
+
+
+MARK_GEN = MarkGenerator(_ispytest=True)
+
+
+@final
+class NodeKeywords(MutableMapping[str, Any]):
+ __slots__ = ("node", "parent", "_markers")
+
+ def __init__(self, node: "Node") -> None:
+ self.node = node
+ self.parent = node.parent
+ self._markers = {node.name: True}
+
+ def __getitem__(self, key: str) -> Any:
+ try:
+ return self._markers[key]
+ except KeyError:
+ if self.parent is None:
+ raise
+ return self.parent.keywords[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self._markers[key] = value
+
+ # Note: we could've avoided explicitly implementing some of the methods
+ # below and use the collections.abc fallback, but that would be slow.
+
+ def __contains__(self, key: object) -> bool:
+ return (
+ key in self._markers
+ or self.parent is not None
+ and key in self.parent.keywords
+ )
+
+ def update( # type: ignore[override]
+ self,
+ other: Union[Mapping[str, Any], Iterable[Tuple[str, Any]]] = (),
+ **kwds: Any,
+ ) -> None:
+ self._markers.update(other)
+ self._markers.update(kwds)
+
+ def __delitem__(self, key: str) -> None:
+ raise ValueError("cannot delete key in keywords dict")
+
+ def __iter__(self) -> Iterator[str]:
+ # Doesn't need to be fast.
+ yield from self._markers
+ if self.parent is not None:
+ for keyword in self.parent.keywords:
+ # self._marks and self.parent.keywords can have duplicates.
+ if keyword not in self._markers:
+ yield keyword
+
+ def __len__(self) -> int:
+ # Doesn't need to be fast.
+ return sum(1 for keyword in self)
+
+ def __repr__(self) -> str:
+ return f"<NodeKeywords for node {self.node}>"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/monkeypatch.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/monkeypatch.py
new file mode 100644
index 0000000000..31f95a95ab
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/monkeypatch.py
@@ -0,0 +1,383 @@
+"""Monkeypatching and mocking functionality."""
+import os
+import re
+import sys
+import warnings
+from contextlib import contextmanager
+from typing import Any
+from typing import Generator
+from typing import List
+from typing import MutableMapping
+from typing import Optional
+from typing import overload
+from typing import Tuple
+from typing import TypeVar
+from typing import Union
+
+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 (.*)$")
+
+
+K = TypeVar("K")
+V = TypeVar("V")
+
+
+@fixture
+def monkeypatch() -> Generator["MonkeyPatch", None, None]:
+ """A convenient fixture for monkey-patching.
+
+ The fixture provides these methods to modify objects, dictionaries or
+ 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)
+
+ 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.
+ """
+ mpatch = MonkeyPatch()
+ yield mpatch
+ mpatch.undo()
+
+
+def resolve(name: str) -> object:
+ # Simplified from zope.dottedname.
+ parts = name.split(".")
+
+ used = parts.pop(0)
+ found = __import__(used)
+ for part in parts:
+ used += "." + part
+ try:
+ found = getattr(found, part)
+ except AttributeError:
+ pass
+ else:
+ continue
+ # We use explicit un-nesting of the handling block in order
+ # to avoid nested exceptions.
+ try:
+ __import__(used)
+ except ImportError as ex:
+ expected = str(ex).split()[-1]
+ if expected == used:
+ raise
+ else:
+ raise ImportError(f"import error in {used}: {ex}") from ex
+ found = annotated_getattr(found, part, used)
+ return found
+
+
+def annotated_getattr(obj: object, name: str, ann: str) -> object:
+ try:
+ obj = getattr(obj, name)
+ except AttributeError as e:
+ raise AttributeError(
+ "{!r} object at {} has no attribute {!r}".format(
+ type(obj).__name__, ann, name
+ )
+ ) from e
+ return obj
+
+
+def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]:
+ if not isinstance(import_path, str) or "." not in import_path:
+ raise TypeError(f"must be absolute import path string, not {import_path!r}")
+ module, attr = import_path.rsplit(".", 1)
+ target = resolve(module)
+ if raising:
+ annotated_getattr(target, attr, ann=module)
+ return attr, target
+
+
+class Notset:
+ def __repr__(self) -> str:
+ return "<notset>"
+
+
+notset = Notset()
+
+
+@final
+class MonkeyPatch:
+ """Helper to conveniently monkeypatch attributes/items/environment
+ variables/syspath.
+
+ Returned by the :fixture:`monkeypatch` fixture.
+
+ :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
+ :meth:`undo` explicitly.
+ """
+
+ def __init__(self) -> None:
+ self._setattr: List[Tuple[object, str, object]] = []
+ self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = []
+ self._cwd: Optional[str] = None
+ self._savesyspath: Optional[List[str]] = None
+
+ @classmethod
+ @contextmanager
+ def context(cls) -> Generator["MonkeyPatch", None, None]:
+ """Context manager that returns a new :class:`MonkeyPatch` object
+ which undoes any patching done inside the ``with`` block upon exit.
+
+ Example:
+
+ .. code-block:: python
+
+ import functools
+
+
+ def test_partial(monkeypatch):
+ with monkeypatch.context() as m:
+ m.setattr(functools, "partial", 3)
+
+ Useful in situations where it is desired to undo some patches before the test ends,
+ such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples
+ of this see :issue:`3290`).
+ """
+ m = cls()
+ try:
+ yield m
+ finally:
+ m.undo()
+
+ @overload
+ def setattr(
+ self,
+ target: str,
+ name: object,
+ value: Notset = ...,
+ raising: bool = ...,
+ ) -> None:
+ ...
+
+ @overload
+ def setattr(
+ self,
+ target: object,
+ name: str,
+ value: object,
+ raising: bool = ...,
+ ) -> None:
+ ...
+
+ def setattr(
+ self,
+ target: Union[str, object],
+ name: Union[object, str],
+ value: object = notset,
+ raising: bool = True,
+ ) -> None:
+ """Set attribute value on target, memorizing the old value.
+
+ 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.
+
+ Raises AttributeError if the attribute does not exist, unless
+ ``raising`` is set to False.
+ """
+ __tracebackhide__ = True
+ import inspect
+
+ if isinstance(value, Notset):
+ if not isinstance(target, str):
+ raise TypeError(
+ "use setattr(target, name, value) or "
+ "setattr(target, value) with target being a dotted "
+ "import string"
+ )
+ value = name
+ name, target = derive_importpath(target, raising)
+ else:
+ if not isinstance(name, str):
+ raise TypeError(
+ "use setattr(target, name, value) with name being a string or "
+ "setattr(target, value) with target being a dotted "
+ "import string"
+ )
+
+ oldval = getattr(target, name, notset)
+ if raising and oldval is notset:
+ raise AttributeError(f"{target!r} has no attribute {name!r}")
+
+ # avoid class descriptors like staticmethod/classmethod
+ if inspect.isclass(target):
+ oldval = target.__dict__.get(name, notset)
+ self._setattr.append((target, name, oldval))
+ setattr(target, name, value)
+
+ def delattr(
+ self,
+ target: Union[object, str],
+ name: Union[str, Notset] = notset,
+ raising: bool = True,
+ ) -> None:
+ """Delete attribute ``name`` from ``target``.
+
+ If no ``name`` is specified and ``target`` is a string
+ it will be interpreted as a dotted import path with the
+ last part being the attribute name.
+
+ Raises AttributeError it the attribute does not exist, unless
+ ``raising`` is set to False.
+ """
+ __tracebackhide__ = True
+ import inspect
+
+ if isinstance(name, Notset):
+ if not isinstance(target, str):
+ raise TypeError(
+ "use delattr(target, name) or "
+ "delattr(target) with target being a dotted "
+ "import string"
+ )
+ name, target = derive_importpath(target, raising)
+
+ if not hasattr(target, name):
+ if raising:
+ raise AttributeError(name)
+ else:
+ oldval = getattr(target, name, notset)
+ # Avoid class descriptors like staticmethod/classmethod.
+ if inspect.isclass(target):
+ oldval = target.__dict__.get(name, notset)
+ self._setattr.append((target, name, oldval))
+ delattr(target, name)
+
+ def setitem(self, dic: MutableMapping[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
+
+ def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
+ """Delete ``name`` from dict.
+
+ Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
+ False.
+ """
+ if name not in dic:
+ if raising:
+ raise KeyError(name)
+ else:
+ self._setitem.append((dic, name, dic.get(name, notset)))
+ del dic[name]
+
+ def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
+ """Set environment variable ``name`` to ``value``.
+
+ If ``prepend`` is a character, read the current environment variable
+ value and prepend the ``value`` adjoined with the ``prepend``
+ character.
+ """
+ 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__
+ )
+ ),
+ stacklevel=2,
+ )
+ value = str(value)
+ if prepend and name in os.environ:
+ value = value + prepend + os.environ[name]
+ self.setitem(os.environ, name, value)
+
+ def delenv(self, name: str, raising: bool = True) -> None:
+ """Delete ``name`` from the environment.
+
+ Raises ``KeyError`` if it does not exist, unless ``raising`` is set to
+ False.
+ """
+ environ: MutableMapping[str, str] = os.environ
+ self.delitem(environ, name, raising=raising)
+
+ 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))
+
+ # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171
+ # this is only needed when pkg_resources was already loaded by the namespace package
+ if "pkg_resources" in sys.modules:
+ from pkg_resources import fixup_namespace_packages
+
+ fixup_namespace_packages(str(path))
+
+ # A call to syspathinsert() usually means that the caller wants to
+ # import some dynamically created files, thus with python3 we
+ # invalidate its import caches.
+ # This is especially important when any namespace package is in use,
+ # since then the mtime based FileFinder cache (that gets created in
+ # this case already) gets not invalidated when writing the new files
+ # quickly afterwards.
+ from importlib import invalidate_caches
+
+ invalidate_caches()
+
+ 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.
+ """
+ if self._cwd is None:
+ self._cwd = os.getcwd()
+ os.chdir(path)
+
+ def undo(self) -> None:
+ """Undo previous changes.
+
+ This call consumes the undo stack. Calling it a second time has no
+ effect unless you do more monkeypatching after the undo call.
+
+ 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.
+ """
+ for obj, name, value in reversed(self._setattr):
+ if value is not notset:
+ setattr(obj, name, value)
+ else:
+ delattr(obj, name)
+ self._setattr[:] = []
+ for dictionary, key, value in reversed(self._setitem):
+ if value is notset:
+ try:
+ del dictionary[key]
+ except KeyError:
+ pass # Was already deleted, so we have the desired state.
+ else:
+ dictionary[key] = value
+ self._setitem[:] = []
+ if self._savesyspath is not None:
+ sys.path[:] = self._savesyspath
+ self._savesyspath = None
+
+ if self._cwd is not None:
+ os.chdir(self._cwd)
+ self._cwd = None
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/nodes.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/nodes.py
new file mode 100644
index 0000000000..e49c1b003e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/nodes.py
@@ -0,0 +1,762 @@
+import os
+import warnings
+from inspect import signature
+from pathlib import Path
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import MutableMapping
+from typing import Optional
+from typing import overload
+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 _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.compat import LEGACY_PATH
+from _pytest.config import Config
+from _pytest.config import ConftestImportFailure
+from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
+from _pytest.deprecated import NODE_CTOR_FSPATH_ARG
+from _pytest.mark.structures import Mark
+from _pytest.mark.structures import MarkDecorator
+from _pytest.mark.structures import NodeKeywords
+from _pytest.outcomes import fail
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import commonpath
+from _pytest.stash import Stash
+from _pytest.warning_types import PytestWarning
+
+if TYPE_CHECKING:
+ # Imported here due to circular import.
+ from _pytest.main import Session
+ from _pytest._code.code import _TracebackStyle
+
+
+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"
+ )
+
+
+def _imply_path(
+ node_type: Type["Node"],
+ path: Optional[Path],
+ fspath: Optional[LEGACY_PATH],
+) -> Path:
+ if fspath is not None:
+ warnings.warn(
+ NODE_CTOR_FSPATH_ARG.format(
+ node_type_name=node_type.__name__,
+ ),
+ stacklevel=3,
+ )
+ if path is not None:
+ if fspath is not None:
+ _check_path(path, fspath)
+ return path
+ else:
+ assert fspath is not None
+ return Path(fspath)
+
+
+_NodeType = TypeVar("_NodeType", bound="Node")
+
+
+class NodeMeta(type):
+ def __call__(self, *k, **kw):
+ 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__}")
+ fail(msg, pytrace=False)
+
+ def _create(self, *k, **kw):
+ try:
+ return super().__call__(*k, **kw)
+ except TypeError:
+ sig = signature(getattr(self, "__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"
+ "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)
+
+
+class Node(metaclass=NodeMeta):
+ """Base class for Collector and Item, the components of the test
+ collection tree.
+
+ Collector subclasses have children; Items are 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.
+ fspath: LEGACY_PATH
+
+ # Use __slots__ to make attribute access faster.
+ # Note that __dict__ is still available.
+ __slots__ = (
+ "name",
+ "parent",
+ "config",
+ "session",
+ "path",
+ "_nodeid",
+ "_store",
+ "__dict__",
+ )
+
+ def __init__(
+ self,
+ name: str,
+ parent: "Optional[Node]" = None,
+ config: Optional[Config] = None,
+ session: "Optional[Session]" = None,
+ fspath: Optional[LEGACY_PATH] = None,
+ path: Optional[Path] = None,
+ nodeid: Optional[str] = None,
+ ) -> None:
+ #: A unique name within the scope of the parent node.
+ self.name = name
+
+ #: The parent collector node.
+ self.parent = parent
+
+ if config:
+ #: The pytest config object.
+ self.config: Config = config
+ else:
+ if not parent:
+ raise TypeError("config or parent must be provided")
+ self.config = parent.config
+
+ if session:
+ #: The pytest session this node is part of.
+ self.session = session
+ else:
+ if not parent:
+ raise TypeError("session or parent must be provided")
+ self.session = parent.session
+
+ 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)
+
+ # The explicit annotation is to avoid publicly exposing NodeKeywords.
+ #: Keywords/markers collected from all scopes.
+ self.keywords: MutableMapping[str, Any] = NodeKeywords(self)
+
+ #: The marker objects belonging to this node.
+ self.own_markers: List[Mark] = []
+
+ #: Allow adding of extra keywords to use for matching.
+ self.extra_keyword_matches: Set[str] = set()
+
+ if nodeid is not None:
+ assert "::()" not in nodeid
+ self._nodeid = nodeid
+ else:
+ if not self.parent:
+ raise TypeError("nodeid or parent must be provided")
+ self._nodeid = self.parent.nodeid + "::" + self.name
+
+ #: A place where plugins can store information on the node for their
+ #: own use.
+ #:
+ #: :type: Stash
+ self.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):
+ """Public constructor for Nodes.
+
+ This indirection got introduced in order to enable removing
+ the fragile logic from the node constructors.
+
+ Subclasses can use ``super().from_parent(...)`` when overriding the
+ construction.
+
+ :param parent: The parent node of this Node.
+ """
+ if "config" in kw:
+ raise TypeError("config is not a valid argument for from_parent")
+ if "session" in kw:
+ raise TypeError("session is not a valid argument for from_parent")
+ return cls._create(parent=parent, **kw)
+
+ @property
+ def ihook(self):
+ """fspath-sensitive hook proxy used to call pytest hooks."""
+ return self.session.gethookproxy(self.path)
+
+ def __repr__(self) -> str:
+ return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None))
+
+ def warn(self, warning: Warning) -> None:
+ """Issue a warning for this Node.
+
+ Warnings will be displayed after the test session, unless explicitly suppressed.
+
+ :param Warning warning:
+ The warning instance to issue.
+
+ :raises ValueError: If ``warning`` instance is not a subclass of Warning.
+
+ Example usage:
+
+ .. code-block:: python
+
+ node.warn(PytestWarning("some message"))
+ node.warn(UserWarning("some message"))
+
+ .. versionchanged:: 6.2
+ Any subclass of :class:`Warning` is now accepted, rather than only
+ :class:`PytestWarning <pytest.PytestWarning>` subclasses.
+ """
+ # 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
+ )
+ )
+ path, lineno = get_fslocation_from_item(self)
+ assert lineno is not None
+ warnings.warn_explicit(
+ warning,
+ category=None,
+ filename=str(path),
+ lineno=lineno + 1,
+ )
+
+ # Methods for ordering nodes.
+
+ @property
+ def nodeid(self) -> str:
+ """A ::-separated string denoting its collection tree address."""
+ return self._nodeid
+
+ def __hash__(self) -> int:
+ return hash(self._nodeid)
+
+ def setup(self) -> None:
+ pass
+
+ def teardown(self) -> None:
+ pass
+
+ def listchain(self) -> List["Node"]:
+ """Return list of all parent collectors up to self, starting from
+ the root of collection tree."""
+ chain = []
+ item: Optional[Node] = self
+ while item is not None:
+ chain.append(item)
+ item = item.parent
+ chain.reverse()
+ return chain
+
+ def add_marker(
+ self, marker: Union[str, MarkDecorator], append: bool = True
+ ) -> None:
+ """Dynamically add a marker object to the node.
+
+ :param append:
+ Whether to append the marker, or prepend it.
+ """
+ from _pytest.mark import MARK_GEN
+
+ if isinstance(marker, MarkDecorator):
+ marker_ = marker
+ elif isinstance(marker, str):
+ marker_ = getattr(MARK_GEN, marker)
+ else:
+ raise ValueError("is not a string or pytest.mark.* Marker")
+ self.keywords[marker_.name] = marker_
+ if append:
+ self.own_markers.append(marker_.mark)
+ else:
+ self.own_markers.insert(0, marker_.mark)
+
+ def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]:
+ """Iterate over all markers of the node.
+
+ :param name: If given, filter the results by the name attribute.
+ """
+ return (x[1] for x in self.iter_markers_with_node(name=name))
+
+ def iter_markers_with_node(
+ self, name: Optional[str] = None
+ ) -> Iterator[Tuple["Node", Mark]]:
+ """Iterate over all markers of the node.
+
+ :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 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]:
+ ...
+
+ @overload
+ def get_closest_marker(self, name: str, default: Mark) -> Mark:
+ ...
+
+ def get_closest_marker(
+ self, name: str, default: Optional[Mark] = None
+ ) -> Optional[Mark]:
+ """Return the first marker matching the name, from closest (for
+ example function) to farther level (for example module level).
+
+ :param default: Fallback return value if no marker was found.
+ :param name: Name to filter by.
+ """
+ return next(self.iter_markers(name=name), default)
+
+ def listextrakeywords(self) -> Set[str]:
+ """Return a set of all extra keywords in self and any parents."""
+ extra_keywords: Set[str] = set()
+ for item in self.listchain():
+ extra_keywords.update(item.extra_keyword_matches)
+ return extra_keywords
+
+ def listnames(self) -> List[str]:
+ 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.
+
+ This method can only be called when this node is active
+ in a setup chain, for example during self.setup().
+ """
+ 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
+
+ def _repr_failure_py(
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ style: "Optional[_TracebackStyle]" = None,
+ ) -> TerminalRepr:
+ from _pytest.fixtures import FixtureLookupError
+
+ if isinstance(excinfo.value, ConftestImportFailure):
+ excinfo = ExceptionInfo.from_exc_info(excinfo.value.excinfo)
+ if isinstance(excinfo.value, fail.Exception):
+ if not excinfo.value.pytrace:
+ style = "value"
+ if isinstance(excinfo.value, FixtureLookupError):
+ return excinfo.value.formatrepr()
+ if self.config.getoption("fulltrace", False):
+ style = "long"
+ else:
+ tb = _pytest._code.Traceback([excinfo.traceback[-1]])
+ self._prunetraceback(excinfo)
+ if len(excinfo.traceback) == 0:
+ excinfo.traceback = tb
+ if style == "auto":
+ style = "long"
+ # XXX should excinfo.getrepr record all data and toterminal() process it?
+ if style is None:
+ if self.config.getoption("tbstyle", "auto") == "short":
+ style = "short"
+ else:
+ style = "long"
+
+ if self.config.getoption("verbose", 0) > 1:
+ truncate_locals = False
+ else:
+ truncate_locals = True
+
+ # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False.
+ # It is possible for a fixture/test to change the CWD while this code runs, which
+ # would then result in the user seeing confusing paths in the failure message.
+ # To fix this, if the CWD changed, always display the full absolute path.
+ # It will be better to just always display paths relative to invocation_dir, but
+ # this requires a lot of plumbing (#6428).
+ try:
+ abspath = Path(os.getcwd()) != self.config.invocation_params.dir
+ except OSError:
+ abspath = True
+
+ return excinfo.getrepr(
+ funcargs=True,
+ abspath=abspath,
+ showlocals=self.config.getoption("showlocals", False),
+ style=style,
+ tbfilter=False, # pruned already, or in --fulltrace mode.
+ truncate_locals=truncate_locals,
+ )
+
+ def repr_failure(
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ style: "Optional[_TracebackStyle]" = None,
+ ) -> Union[str, TerminalRepr]:
+ """Return a representation of a collection or test failure.
+
+ .. seealso:: :ref:`non-python tests`
+
+ :param excinfo: Exception information for the failure.
+ """
+ return self._repr_failure_py(excinfo, style)
+
+
+def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[int]]:
+ """Try to extract the actual location from a node, depending on available attributes:
+
+ * "location": a pair (path, lineno)
+ * "obj": a Python object that the node wraps.
+ * "fspath": just a path
+
+ :rtype: A tuple of (str|Path, int) with filename and line number.
+ """
+ # See Item.location.
+ location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None)
+ if location is not None:
+ return location[:2]
+ obj = getattr(node, "obj", None)
+ if obj is not None:
+ return getfslineno(obj)
+ return getattr(node, "fspath", "unknown location"), -1
+
+
+class Collector(Node):
+ """Collector instances create children through collect() and thus
+ iteratively build a tree."""
+
+ class CollectError(Exception):
+ """An error during collection, contains a custom message."""
+
+ def collect(self) -> Iterable[Union["Item", "Collector"]]:
+ """Return a list of children (items and collectors) for this
+ collection node."""
+ raise NotImplementedError("abstract")
+
+ # TODO: This omits the style= parameter which breaks Liskov Substitution.
+ def repr_failure( # type: ignore[override]
+ self, excinfo: ExceptionInfo[BaseException]
+ ) -> Union[str, TerminalRepr]:
+ """Return a representation of a collection failure.
+
+ :param excinfo: Exception information for the failure.
+ """
+ if isinstance(excinfo.value, self.CollectError) and not self.config.getoption(
+ "fulltrace", False
+ ):
+ exc = excinfo.value
+ return str(exc.args[0])
+
+ # Respect explicit tbstyle option, but default to "short"
+ # (_repr_failure_py uses "long" with "fulltrace" option always).
+ tbstyle = self.config.getoption("tbstyle", "auto")
+ if tbstyle == "auto":
+ tbstyle = "short"
+
+ return self._repr_failure_py(excinfo, style=tbstyle)
+
+ def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
+ 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()
+
+
+def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
+ for initial_path in session._initialpaths:
+ if commonpath(path, initial_path) == initial_path:
+ rel = str(path.relative_to(initial_path))
+ return "" if rel == "." else rel
+ return None
+
+
+class FSCollector(Collector):
+ def __init__(
+ self,
+ fspath: Optional[LEGACY_PATH] = None,
+ path_or_parent: Optional[Union[Path, Node]] = None,
+ path: Optional[Path] = None,
+ name: Optional[str] = None,
+ parent: Optional[Node] = None,
+ config: Optional[Config] = None,
+ session: Optional["Session"] = None,
+ nodeid: Optional[str] = None,
+ ) -> None:
+ if path_or_parent:
+ if isinstance(path_or_parent, Node):
+ assert parent is None
+ parent = cast(FSCollector, path_or_parent)
+ elif isinstance(path_or_parent, Path):
+ assert path is None
+ path = path_or_parent
+
+ path = _imply_path(type(self), path, fspath=fspath)
+ if name is None:
+ name = path.name
+ if parent is not None and parent.path != path:
+ try:
+ rel = path.relative_to(parent.path)
+ except ValueError:
+ pass
+ else:
+ name = str(rel)
+ name = name.replace(os.sep, SEP)
+ self.path = path
+
+ if session is None:
+ assert parent is not None
+ session = parent.session
+
+ if nodeid is None:
+ try:
+ nodeid = str(self.path.relative_to(session.config.rootpath))
+ except ValueError:
+ nodeid = _check_initialpaths_for_relpath(session, path)
+
+ if nodeid and os.sep != SEP:
+ nodeid = nodeid.replace(os.sep, SEP)
+
+ super().__init__(
+ name=name,
+ parent=parent,
+ config=config,
+ session=session,
+ nodeid=nodeid,
+ path=path,
+ )
+
+ @classmethod
+ def from_parent(
+ cls,
+ parent,
+ *,
+ fspath: Optional[LEGACY_PATH] = None,
+ path: Optional[Path] = None,
+ **kw,
+ ):
+ """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):
+ """Base class for collecting tests from a file.
+
+ :ref:`non-python tests`.
+ """
+
+
+class Item(Node):
+ """A basic test invocation item.
+
+ Note that for a single function there might be multiple test invocation items.
+ """
+
+ nextitem = None
+
+ def __init__(
+ self,
+ name,
+ parent=None,
+ config: Optional[Config] = None,
+ session: Optional["Session"] = None,
+ nodeid: Optional[str] = None,
+ **kw,
+ ) -> None:
+ # The first two arguments are intentionally passed positionally,
+ # to keep plugins who define a node type which inherits from
+ # (pytest.Item, pytest.File) working (see issue #8435).
+ # They can be made kwargs when the deprecation above is done.
+ super().__init__(
+ name,
+ parent,
+ config=config,
+ session=session,
+ nodeid=nodeid,
+ **kw,
+ )
+ self._report_sections: List[Tuple[str, str, str]] = []
+
+ #: A list of tuples (name, value) that holds user defined properties
+ #: for this test.
+ self.user_properties: List[Tuple[str, object]] = []
+
+ self._check_item_and_collector_diamond_inheritance()
+
+ def _check_item_and_collector_diamond_inheritance(self) -> None:
+ """
+ Check if the current type inherits from both File and Collector
+ at the same time, emitting a warning accordingly (#8447).
+ """
+ cls = type(self)
+
+ # We inject an attribute in the type to avoid issuing this warning
+ # for the same class more than once, which is not helpful.
+ # It is a hack, but was deemed acceptable in order to avoid
+ # flooding the user in the common case.
+ attr_name = "_pytest_diamond_inheritance_warning_shown"
+ if getattr(cls, attr_name, False):
+ return
+ setattr(cls, attr_name, True)
+
+ problems = ", ".join(
+ base.__name__ for base in cls.__bases__ if issubclass(base, Collector)
+ )
+ if problems:
+ warnings.warn(
+ f"{cls.__name__} is an Item subclass and should not be a collector, "
+ f"however its bases {problems} are collectors.\n"
+ "Please split the Collectors and the Item into separate node types.\n"
+ "Pytest Doc example: https://docs.pytest.org/en/latest/example/nonpython.html\n"
+ "example pull request on a plugin: https://github.com/asmeurer/pytest-flakes/pull/40/",
+ PytestWarning,
+ )
+
+ def runtest(self) -> None:
+ """Run the test case for this item.
+
+ Must be implemented by subclasses.
+
+ .. seealso:: :ref:`non-python tests`
+ """
+ raise NotImplementedError("runtest must be implemented by Item subclass")
+
+ def add_report_section(self, when: str, key: str, content: str) -> None:
+ """Add a new report section, similar to what's done internally to add
+ stdout and stderr captured output::
+
+ item.add_report_section("call", "stdout", "report section contents")
+
+ :param str when:
+ One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
+ :param str key:
+ Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
+ ``"stderr"`` internally.
+ :param str content:
+ The full contents as a string.
+ """
+ if content:
+ self._report_sections.append((when, key, content))
+
+ def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
+ """Get location information for this item for test reports.
+
+ Returns a tuple with three elements:
+
+ - The path of the test (default ``self.path``)
+ - The line number of the test (default ``None``)
+ - A name of the test to be shown (default ``""``)
+
+ .. seealso:: :ref:`non-python tests`
+ """
+ return self.path, None, ""
+
+ @cached_property
+ def location(self) -> Tuple[str, Optional[int], str]:
+ location = self.reportinfo()
+ path = absolutepath(os.fspath(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/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/nose.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/nose.py
new file mode 100644
index 0000000000..b0699d22bd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/nose.py
@@ -0,0 +1,42 @@
+"""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/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/outcomes.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/outcomes.py
new file mode 100644
index 0000000000..25206fe0e8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/outcomes.py
@@ -0,0 +1,307 @@
+"""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 Optional
+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
+
+
+class OutcomeException(BaseException):
+ """OutcomeException and its subclass instances indicate and contain info
+ about test and collection outcomes."""
+
+ def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
+ if msg is not None and not isinstance(msg, str):
+ error_msg = ( # type: ignore[unreachable]
+ "{} expected string as 'msg' parameter, got '{}' instead.\n"
+ "Perhaps you meant to use a mark?"
+ )
+ raise TypeError(error_msg.format(type(self).__name__, type(msg).__name__))
+ super().__init__(msg)
+ self.msg = msg
+ self.pytrace = pytrace
+
+ def __repr__(self) -> str:
+ if self.msg is not None:
+ return self.msg
+ return f"<{self.__class__.__name__} instance>"
+
+ __str__ = __repr__
+
+
+TEST_OUTCOME = (OutcomeException, Exception)
+
+
+class Skipped(OutcomeException):
+ # XXX hackish: on 3k we fake to live in the builtins
+ # in order to have Skipped exception printing shorter/nicer
+ __module__ = "builtins"
+
+ def __init__(
+ self,
+ msg: Optional[str] = None,
+ pytrace: bool = True,
+ allow_module_level: bool = False,
+ *,
+ _use_item_location: bool = False,
+ ) -> None:
+ super().__init__(msg=msg, pytrace=pytrace)
+ self.allow_module_level = allow_module_level
+ # If true, the skip location is reported as the item's location,
+ # instead of the place that raises the exception/calls skip().
+ self._use_item_location = _use_item_location
+
+
+class Failed(OutcomeException):
+ """Raised from an explicit call to pytest.fail()."""
+
+ __module__ = "builtins"
+
+
+class Exit(Exception):
+ """Raised for immediate program exits (no tracebacks/summaries)."""
+
+ def __init__(
+ self, msg: str = "unknown reason", returncode: Optional[int] = None
+ ) -> None:
+ self.msg = msg
+ self.returncode = returncode
+ super().__init__(msg)
+
+
+# Elaborate hack to work around https://github.com/python/mypy/issues/2087.
+# Ideally would just be `exit.Exception = Exit` etc.
+
+_F = TypeVar("_F", bound=Callable[..., object])
+_ET = TypeVar("_ET", bound=Type[BaseException])
+
+
+class _WithException(Protocol[_F, _ET]):
+ Exception: _ET
+ __call__: _F
+
+
+def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _ET]]:
+ def decorate(func: _F) -> _WithException[_F, _ET]:
+ func_with_exception = cast(_WithException[_F, _ET], func)
+ func_with_exception.Exception = exception_type
+ return func_with_exception
+
+ return decorate
+
+
+# Exposed helper methods.
+
+
+@_with_exception(Exit)
+def exit(
+ reason: str = "", returncode: Optional[int] = None, *, msg: Optional[str] = None
+) -> "NoReturn":
+ """Exit testing process.
+
+ :param reason:
+ The message to show as the reason for exiting pytest. reason has a default value
+ only because `msg` is deprecated.
+
+ :param returncode:
+ Return code to be used when exiting pytest.
+
+ :param msg:
+ Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
+ """
+ __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":
+ """Skip an executing test with the given message.
+
+ This function should be called only during testing (setup, call or teardown) or
+ during collection by using the ``allow_module_level`` flag. This function can
+ be called in doctests as well.
+
+ :param reason:
+ 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.
+
+ :param msg:
+ Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
+
+ .. note::
+ It is better to use the :ref:`pytest.mark.skipif ref` marker when
+ possible to declare a test to be skipped under certain conditions
+ like mismatching platforms or dependencies.
+ Similarly, use the ``# doctest: +SKIP`` directive (see :py:data:`doctest.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":
+ """Explicitly fail an executing test with the given message.
+
+ :param reason:
+ The message to show the user as reason for the failure.
+
+ :param pytrace:
+ 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.
+ """
+ __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":
+ """Imperatively xfail an executing test or setup function with the given reason.
+
+ This function should be called only during testing (setup, call or teardown).
+
+ .. 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.
+ """
+ __tracebackhide__ = True
+ raise XFailed(reason)
+
+
+def importorskip(
+ modname: str, minversion: Optional[str] = None, reason: Optional[str] = None
+) -> Any:
+ """Import and return the requested module ``modname``, or skip the
+ current test if the module cannot be imported.
+
+ :param str modname:
+ The name of the module to import.
+ :param str 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:
+ If given, this reason is shown as the message when the module cannot
+ be imported.
+
+ :returns:
+ The imported module. This should be assigned to its canonical name.
+
+ Example::
+
+ docutils = pytest.importorskip("docutils")
+ """
+ import warnings
+
+ __tracebackhide__ = True
+ compile(modname, "", "eval") # to catch syntaxerrors
+
+ 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:
+ if reason is None:
+ reason = f"could not import {modname!r}: {exc}"
+ raise Skipped(reason, allow_module_level=True) from None
+ mod = sys.modules[modname]
+ if minversion is None:
+ return mod
+ verattr = getattr(mod, "__version__", None)
+ if minversion is not None:
+ # Imported lazily to improve start-up time.
+ from packaging.version import Version
+
+ if verattr is None or Version(verattr) < Version(minversion):
+ raise Skipped(
+ "module %r has __version__ %r, required is: %r"
+ % (modname, verattr, minversion),
+ allow_module_level=True,
+ )
+ return mod
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pastebin.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pastebin.py
new file mode 100644
index 0000000000..385b3022cc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pastebin.py
@@ -0,0 +1,110 @@
+"""Submit failure or test session information to a pastebin service."""
+import tempfile
+from io import StringIO
+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
+
+
+pastebinfile_key = StashKey[IO[bytes]]()
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("terminal reporting")
+ group._addoption(
+ "--pastebin",
+ metavar="mode",
+ action="store",
+ dest="pastebin",
+ default=None,
+ choices=["failed", "all"],
+ help="send failed|all info to bpaste.net pastebin service.",
+ )
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_configure(config: Config) -> None:
+ if config.option.pastebin == "all":
+ tr = config.pluginmanager.getplugin("terminalreporter")
+ # If no terminal reporter plugin is present, nothing we can do here;
+ # this can happen when this function executes in a worker node
+ # when using pytest-xdist, for example.
+ if tr is not None:
+ # pastebin file will be UTF-8 encoded binary file.
+ config.stash[pastebinfile_key] = tempfile.TemporaryFile("w+b")
+ oldwrite = tr._tw.write
+
+ def tee_write(s, **kwargs):
+ oldwrite(s, **kwargs)
+ if isinstance(s, str):
+ s = s.encode("utf-8")
+ config.stash[pastebinfile_key].write(s)
+
+ tr._tw.write = tee_write
+
+
+def pytest_unconfigure(config: Config) -> None:
+ if pastebinfile_key in config.stash:
+ pastebinfile = config.stash[pastebinfile_key]
+ # Get terminal contents and delete file.
+ pastebinfile.seek(0)
+ sessionlog = pastebinfile.read()
+ pastebinfile.close()
+ del config.stash[pastebinfile_key]
+ # Undo our patching in the terminal reporter.
+ tr = config.pluginmanager.getplugin("terminalreporter")
+ del tr._tw.__dict__["write"]
+ # Write summary.
+ tr.write_sep("=", "Sending information to Paste Service")
+ pastebinurl = create_new_paste(sessionlog)
+ tr.write_line("pastebin session-log: %s\n" % pastebinurl)
+
+
+def create_new_paste(contents: Union[str, bytes]) -> str:
+ """Create a new paste using the bpaste.net service.
+
+ :contents: Paste contents string.
+ :returns: URL to the pasted contents, or an error message.
+ """
+ import re
+ from urllib.request import urlopen
+ from urllib.parse import urlencode
+
+ params = {"code": contents, "lexer": "text", "expiry": "1week"}
+ url = "https://bpa.st"
+ try:
+ response: str = (
+ urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")
+ )
+ except OSError as exc_info: # urllib errors
+ return "bad response: %s" % exc_info
+ m = re.search(r'href="/raw/(\w+)"', response)
+ if m:
+ return f"{url}/show/{m.group(1)}"
+ else:
+ return "bad response: invalid format ('" + response + "')"
+
+
+def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:
+ if terminalreporter.config.option.pastebin != "failed":
+ return
+ if "failed" in terminalreporter.stats:
+ terminalreporter.write_sep("=", "Sending information to Paste Service")
+ for rep in terminalreporter.stats["failed"]:
+ try:
+ msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc
+ except AttributeError:
+ msg = terminalreporter._getfailureheadline(rep)
+ file = StringIO()
+ tw = create_terminal_writer(terminalreporter.config, file)
+ rep.toterminal(tw)
+ s = file.getvalue()
+ assert len(s)
+ pastebinurl = create_new_paste(s)
+ terminalreporter.write_line(f"{msg} --> {pastebinurl}")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pathlib.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pathlib.py
new file mode 100644
index 0000000000..b44753e1a4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pathlib.py
@@ -0,0 +1,724 @@
+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
+from functools import partial
+from os.path import expanduser
+from os.path import expandvars
+from os.path import isabs
+from os.path import sep
+from pathlib import Path
+from pathlib import PurePath
+from posixpath import sep as posix_sep
+from types import ModuleType
+from typing import Callable
+from typing import Dict
+from typing import Iterable
+from typing import Iterator
+from typing import Optional
+from typing import Set
+from typing import TypeVar
+from typing import Union
+
+from _pytest.compat import assert_never
+from _pytest.outcomes import skip
+from _pytest.warning_types import PytestWarning
+
+LOCK_TIMEOUT = 60 * 60 * 24 * 3
+
+
+_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath)
+
+# The following function, variables and comments were
+# copied from cpython 3.9 Lib/pathlib.py file.
+
+# EBADF - guard against macOS `stat` throwing EBADF
+_IGNORED_ERRORS = (ENOENT, ENOTDIR, EBADF, ELOOP)
+
+_IGNORED_WINERRORS = (
+ 21, # ERROR_NOT_READY - drive exists but is not accessible
+ 1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself
+)
+
+
+def _ignore_error(exception):
+ return (
+ getattr(exception, "errno", None) in _IGNORED_ERRORS
+ or getattr(exception, "winerror", None) in _IGNORED_WINERRORS
+ )
+
+
+def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
+ return path.joinpath(".lock")
+
+
+def on_rm_rf_error(func, path: str, exc, *, 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]
+
+ # 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):
+ return False
+
+ if not isinstance(excvalue, PermissionError):
+ warnings.warn(
+ PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}")
+ )
+ return False
+
+ if func not in (os.rmdir, os.remove, os.unlink):
+ if func not in (os.open,):
+ warnings.warn(
+ PytestWarning(
+ "(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
+ func, path, exctype, excvalue
+ )
+ )
+ )
+ return False
+
+ # Chmod + retry.
+ import stat
+
+ def chmod_rw(p: str) -> None:
+ mode = os.stat(p).st_mode
+ os.chmod(p, mode | stat.S_IRUSR | stat.S_IWUSR)
+
+ # For files, we need to recursively go upwards in the directories to
+ # ensure they all are also writable.
+ p = Path(path)
+ if p.is_file():
+ for parent in p.parents:
+ chmod_rw(str(parent))
+ # Stop when we reach the original path passed to rm_rf.
+ if parent == start_path:
+ break
+ chmod_rw(str(path))
+
+ func(path)
+ return True
+
+
+def ensure_extended_length_path(path: Path) -> Path:
+ """Get the extended-length version of a path (Windows).
+
+ On Windows, by default, the maximum length of a path (MAX_PATH) is 260
+ characters, and operations on paths longer than that fail. But it is possible
+ to overcome this by converting the path to "extended-length" form before
+ performing the operation:
+ https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation
+
+ On Windows, this function returns the extended-length absolute version of path.
+ On other platforms it returns path unchanged.
+ """
+ if sys.platform.startswith("win32"):
+ path = path.resolve()
+ path = Path(get_extended_length_path_str(str(path)))
+ return path
+
+
+def get_extended_length_path_str(path: str) -> str:
+ """Convert a path to a Windows extended length path."""
+ long_path_prefix = "\\\\?\\"
+ unc_long_path_prefix = "\\\\?\\UNC\\"
+ if path.startswith((long_path_prefix, unc_long_path_prefix)):
+ return path
+ # UNC
+ if path.startswith("\\\\"):
+ return unc_long_path_prefix + path[2:]
+ return long_path_prefix + path
+
+
+def rm_rf(path: Path) -> None:
+ """Remove the path contents recursively, even if some elements
+ are read-only."""
+ path = ensure_extended_length_path(path)
+ onerror = partial(on_rm_rf_error, start_path=path)
+ 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."""
+ l_prefix = prefix.lower()
+ for x in root.iterdir():
+ if x.name.lower().startswith(l_prefix):
+ yield x
+
+
+def extract_suffixes(iter: Iterable[PurePath], 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:]
+
+
+def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
+ """Combine find_prefixes and extract_suffixes."""
+ return extract_suffixes(find_prefixed(root, prefix), prefix)
+
+
+def parse_num(maybe_num) -> int:
+ """Parse number path suffixes, returns -1 on error."""
+ try:
+ return int(maybe_num)
+ except ValueError:
+ return -1
+
+
+def _force_symlink(
+ root: Path, target: Union[str, PurePath], link_to: Union[str, Path]
+) -> None:
+ """Helper to create the current symlink.
+
+ It's full of race conditions that are reasonably OK to ignore
+ for the context of best effort linking to the latest test run.
+
+ The presumption being that in case of much parallelism
+ the inaccuracy is going to be acceptable.
+ """
+ current_symlink = root.joinpath(target)
+ try:
+ current_symlink.unlink()
+ except OSError:
+ pass
+ try:
+ current_symlink.symlink_to(link_to)
+ except Exception:
+ pass
+
+
+def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path:
+ """Create a directory with an increased number as suffix for the given prefix."""
+ for i in range(10):
+ # try up to 10 times to create the folder
+ max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
+ new_number = max_existing + 1
+ new_path = root.joinpath(f"{prefix}{new_number}")
+ try:
+ new_path.mkdir(mode=mode)
+ except Exception:
+ pass
+ else:
+ _force_symlink(root, prefix + "current", new_path)
+ return new_path
+ else:
+ raise OSError(
+ "could not create numbered dir with prefix "
+ "{prefix} in {root} after 10 tries".format(prefix=prefix, root=root)
+ )
+
+
+def create_cleanup_lock(p: Path) -> Path:
+ """Create a lock to prevent premature folder cleanup."""
+ lock_path = get_lock_path(p)
+ try:
+ fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
+ except FileExistsError as e:
+ raise OSError(f"cannot create lockfile in {p}") from e
+ else:
+ pid = os.getpid()
+ spid = str(pid).encode()
+ os.write(fd, spid)
+ os.close(fd)
+ if not lock_path.is_file():
+ raise OSError("lock path got renamed after successful creation")
+ return lock_path
+
+
+def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
+ """Register a cleanup function for removing a lock, by default on atexit."""
+ pid = os.getpid()
+
+ def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None:
+ current_pid = os.getpid()
+ if current_pid != original_pid:
+ # fork
+ return
+ try:
+ lock_path.unlink()
+ except OSError:
+ pass
+
+ return register(cleanup_on_exit)
+
+
+def maybe_delete_a_numbered_dir(path: Path) -> None:
+ """Remove a numbered directory if its lock can be obtained and it does
+ not seem to be in use."""
+ path = ensure_extended_length_path(path)
+ lock_path = None
+ try:
+ lock_path = create_cleanup_lock(path)
+ parent = path.parent
+
+ garbage = parent.joinpath(f"garbage-{uuid.uuid4()}")
+ path.rename(garbage)
+ rm_rf(garbage)
+ except OSError:
+ # known races:
+ # * other process did a cleanup at the same time
+ # * deletable folder was found
+ # * process cwd (Windows)
+ return
+ finally:
+ # If we created the lock, ensure we remove it even if we failed
+ # to properly remove the numbered dir.
+ if lock_path is not None:
+ try:
+ lock_path.unlink()
+ except OSError:
+ pass
+
+
+def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool:
+ """Check if `path` is deletable based on whether the lock file is expired."""
+ if path.is_symlink():
+ return False
+ lock = get_lock_path(path)
+ try:
+ if not lock.is_file():
+ return True
+ except OSError:
+ # we might not have access to the lock file at all, in this case assume
+ # we don't have access to the entire directory (#7491).
+ return False
+ try:
+ lock_time = lock.stat().st_mtime
+ except Exception:
+ return False
+ else:
+ if lock_time < consider_lock_dead_if_created_before:
+ # We want to ignore any errors while trying to remove the lock such as:
+ # - PermissionDenied, like the file permissions have changed since the lock creation;
+ # - FileNotFoundError, in case another pytest process got here first;
+ # and any other cause of failure.
+ with contextlib.suppress(OSError):
+ lock.unlink()
+ return True
+ return False
+
+
+def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None:
+ """Try to cleanup a folder if we can ensure it's deletable."""
+ if ensure_deletable(path, consider_lock_dead_if_created_before):
+ maybe_delete_a_numbered_dir(path)
+
+
+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):
+ if number <= max_delete:
+ yield path
+
+
+def cleanup_numbered_dir(
+ root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
+) -> None:
+ """Cleanup for lock driven numbered directories."""
+ 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)
+
+
+def make_numbered_dir_with_cleanup(
+ root: Path,
+ prefix: str,
+ keep: int,
+ lock_timeout: float,
+ mode: int,
+) -> Path:
+ """Create a numbered dir with a cleanup lock and remove old ones."""
+ e = None
+ 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)
+ except Exception as exc:
+ e = exc
+ else:
+ consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout
+ # Register a cleanup for program exit
+ atexit.register(
+ cleanup_numbered_dir,
+ root,
+ prefix,
+ keep,
+ consider_lock_dead_if_created_before,
+ )
+ return p
+ assert e is not None
+ raise e
+
+
+def resolve_from_str(input: str, rootpath: Path) -> Path:
+ input = expanduser(input)
+ input = expandvars(input)
+ if isabs(input):
+ return Path(input)
+ else:
+ return rootpath.joinpath(input)
+
+
+def fnmatch_ex(pattern: str, path: Union[str, "os.PathLike[str]"]) -> bool:
+ """A port of FNMatcher from py.path.common which works with PurePath() instances.
+
+ The difference between this algorithm and PurePath.match() is that the
+ latter matches "**" glob expressions for each part of the path, while
+ this algorithm uses the whole path instead.
+
+ For example:
+ "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py"
+ with this algorithm, but not with PurePath.match().
+
+ This algorithm was ported to keep backward-compatibility with existing
+ settings which assume paths match according this logic.
+
+ References:
+ * https://bugs.python.org/issue29249
+ * https://bugs.python.org/issue34731
+ """
+ path = PurePath(path)
+ iswin32 = sys.platform.startswith("win")
+
+ if iswin32 and sep not in pattern and posix_sep in pattern:
+ # 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(posix_sep, sep)
+
+ if sep not in pattern:
+ name = path.name
+ else:
+ name = str(path)
+ if path.is_absolute() and not os.path.isabs(pattern):
+ pattern = f"*{os.sep}{pattern}"
+ return fnmatch.fnmatch(name, pattern)
+
+
+def parts(s: str) -> Set[str]:
+ parts = s.split(sep)
+ return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))}
+
+
+def symlink_or_skip(src, dst, **kwargs):
+ """Make a symlink, or skip the test in case symlinks are not supported."""
+ try:
+ os.symlink(str(src), str(dst), **kwargs)
+ except OSError as e:
+ skip(f"symlinks not supported: {e}")
+
+
+class ImportMode(Enum):
+ """Possible values for `mode` parameter of `import_path`."""
+
+ prepend = "prepend"
+ append = "append"
+ importlib = "importlib"
+
+
+class ImportPathMismatchError(ImportError):
+ """Raised on import_path() if there is a mismatch of __file__'s.
+
+ This can happen when `import_path` is called multiple times with different filenames that has
+ the same basename but reside in packages
+ (for example "/tests1/test_foo.py" and "/tests2/test_foo.py").
+ """
+
+
+def import_path(
+ p: Union[str, "os.PathLike[str]"],
+ *,
+ mode: Union[str, ImportMode] = ImportMode.prepend,
+ root: Path,
+) -> ModuleType:
+ """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:
+
+ * `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__.
+
+ * `mode == ImportMode.append`: same as `prepend`, but the directory will be appended
+ to the end of `sys.path`, if not already in `sys.path`.
+
+ * `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.
+
+ :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``.
+
+ :raises ImportPathMismatchError:
+ If after importing the given `path` and the module `__file__`
+ are different. Only raised in `prepend` and `append` modes.
+ """
+ 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
+ else:
+ spec = importlib.util.spec_from_file_location(module_name, str(path))
+
+ if spec 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
+
+ # 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
+ # might have local imports, which would fail at runtime if we restored sys.path.
+ if mode is ImportMode.append:
+ if str(pkg_root) not in sys.path:
+ sys.path.append(str(pkg_root))
+ elif mode is ImportMode.prepend:
+ if str(pkg_root) != sys.path[0]:
+ sys.path.insert(0, str(pkg_root))
+ else:
+ assert_never(mode)
+
+ importlib.import_module(module_name)
+
+ mod = sys.modules[module_name]
+ if path.name == "__init__.py":
+ return mod
+
+ ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "")
+ if ignore != "1":
+ module_file = mod.__file__
+ 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"))]
+
+ try:
+ is_same = _is_same(str(path), module_file)
+ except FileNotFoundError:
+ is_same = False
+
+ if not is_same:
+ raise ImportPathMismatchError(module_name, module_file, path)
+
+ return mod
+
+
+# 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"):
+
+ 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:
+ return os.path.samefile(f1, f2)
+
+
+def module_name_from_path(path: Path, root: Path) -> str:
+ """
+ Return a dotted module name based on the given path, anchored on root.
+
+ For example: path="projects/src/tests/test_foo.py" and root="/projects", the
+ resulting module name will be "src.tests.test_foo".
+ """
+ path = path.with_suffix("")
+ try:
+ relative_path = path.relative_to(root)
+ except ValueError:
+ # If we can't get a relative path to root, use the full path, except
+ # for the first part ("d:\\" or "/" depending on the platform, for example).
+ path_parts = path.parts[1:]
+ else:
+ # Use the parts for the relative path to the root path.
+ path_parts = relative_path.parts
+
+ return ".".join(path_parts)
+
+
+def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) -> None:
+ """
+ Used by ``import_path`` to create intermediate modules when using mode=importlib.
+
+ When we want to import a module as "src.tests.test_foo" for example, we need
+ to create empty modules "src" and "src.tests" after inserting "src.tests.test_foo",
+ otherwise "src.tests.test_foo" is not importable by ``__import__``.
+ """
+ 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
+ module_parts.pop(-1)
+ module_name = ".".join(module_parts)
+
+
+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.
+ """
+ result = None
+ for parent in itertools.chain((path,), path.parents):
+ if parent.is_dir():
+ if not parent.joinpath("__init__.py").is_file():
+ break
+ if not parent.name.isidentifier():
+ break
+ result = parent
+ return result
+
+
+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.
+
+ 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)
+
+ 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:
+ """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)))
+
+
+def commonpath(path1: Path, path2: Path) -> Optional[Path]:
+ """Return the common part shared with the other path, or None if there is
+ no common part.
+
+ If one path is relative and one is absolute, returns None.
+ """
+ try:
+ return Path(os.path.commonpath((str(path1), str(path2))))
+ except ValueError:
+ return None
+
+
+def bestrelpath(directory: Path, dest: Path) -> str:
+ """Return a string which is a relative path from directory to dest such
+ that directory/bestrelpath == dest.
+
+ The paths must be either both absolute or both relative.
+
+ If no such path can be determined, returns dest.
+ """
+ assert isinstance(directory, Path)
+ assert isinstance(dest, Path)
+ if dest == directory:
+ return os.curdir
+ # Find the longest common directory.
+ base = commonpath(directory, dest)
+ # Can be the case on Windows for two absolute paths on different drives.
+ # Can be the case for two relative paths without common prefix.
+ # Can be the case for a relative path and an absolute path.
+ if not base:
+ return str(dest)
+ reldirectory = directory.relative_to(base)
+ reldest = dest.relative_to(base)
+ return os.path.join(
+ # Back from directory to base.
+ *([os.pardir] * len(reldirectory.parts)),
+ # Forward from base to dest.
+ *reldest.parts,
+ )
+
+
+# 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)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/py.typed b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/py.typed
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/py.typed
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pytester.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pytester.py
new file mode 100644
index 0000000000..363a372744
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pytester.py
@@ -0,0 +1,1748 @@
+"""(Disabled by default) support for testing pytest and pytest plugins.
+
+PYTEST_DONT_REWRITE
+"""
+import collections.abc
+import contextlib
+import gc
+import importlib
+import os
+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 Generator
+from typing import IO
+from typing import Iterable
+from typing import List
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import TextIO
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+from weakref import WeakKeyDictionary
+
+from iniconfig import IniConfig
+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
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config import main
+from _pytest.config import PytestPluginManager
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.main import Session
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+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
+from _pytest.tmpdir import TempPathFactory
+from _pytest.warning_types import PytestWarning
+
+
+if TYPE_CHECKING:
+ from typing_extensions import Final
+ from typing_extensions import Literal
+
+ import pexpect
+
+
+pytest_plugins = ["pytester_assertions"]
+
+
+IGNORE_PAM = [ # filenames added when obtaining details about the current user
+ "/var/lib/sss/mc/passwd"
+]
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addoption(
+ "--lsof",
+ action="store_true",
+ dest="lsof",
+ default=False,
+ help="run FD checks if lsof is available",
+ )
+
+ parser.addoption(
+ "--runpytest",
+ default="inprocess",
+ dest="runpytest",
+ choices=("inprocess", "subprocess"),
+ help=(
+ "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"
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ if config.getvalue("lsof"):
+ checker = LsofFdLeakChecker()
+ if checker.matching_platform():
+ config.pluginmanager.register(checker)
+
+ config.addinivalue_line(
+ "markers",
+ "pytester_example_path(*path_segments): join the given path "
+ "segments to `pytester_example_dir` for this test.",
+ )
+
+
+class LsofFdLeakChecker:
+ def get_open_files(self) -> List[Tuple[str, str]]:
+ out = subprocess.run(
+ ("lsof", "-Ffn0", "-p", str(os.getpid())),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ check=True,
+ universal_newlines=True,
+ ).stdout
+
+ def isopen(line: str) -> bool:
+ return line.startswith("f") and (
+ "deleted" not in line
+ and "mem" not in line
+ and "txt" not in line
+ and "cwd" not in line
+ )
+
+ open_files = []
+
+ for line in out.split("\n"):
+ if isopen(line):
+ fields = line.split("\0")
+ fd = fields[0][1:]
+ filename = fields[1][1:]
+ if filename in IGNORE_PAM:
+ continue
+ if filename.startswith("/"):
+ open_files.append((fd, filename))
+
+ return open_files
+
+ def matching_platform(self) -> bool:
+ try:
+ subprocess.run(("lsof", "-v"), check=True)
+ except (OSError, subprocess.CalledProcessError):
+ return False
+ else:
+ return True
+
+ @hookimpl(hookwrapper=True, tryfirst=True)
+ def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]:
+ 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)))
+
+
+# used at least by pytest-xdist plugin
+
+
+@fixture
+def _pytest(request: FixtureRequest) -> "PytestArg":
+ """Return a helper which offers a gethookrecorder(hook) method which
+ returns a HookRecorder instance which helps to make assertions about called
+ hooks."""
+ return PytestArg(request)
+
+
+class PytestArg:
+ def __init__(self, request: FixtureRequest) -> None:
+ self._request = request
+
+ def gethookrecorder(self, hook) -> "HookRecorder":
+ hookrecorder = HookRecorder(hook._pm)
+ self._request.addfinalizer(hookrecorder.finish_recording)
+ return hookrecorder
+
+
+def get_public_names(values: Iterable[str]) -> List[str]:
+ """Only return names from iterator values without a leading underscore."""
+ return [x for x in values if x[0] != "_"]
+
+
+@final
+class RecordedHookCall:
+ """A recorded call to a hook.
+
+ The arguments to the hook call are set as attributes.
+ For example:
+
+ .. code-block:: python
+
+ calls = hook_recorder.getcalls("pytest_runtest_setup")
+ # Suppose pytest_runtest_setup was called once with `item=an_item`.
+ assert calls[0].item is an_item
+ """
+
+ def __init__(self, name: str, kwargs) -> None:
+ self.__dict__.update(kwargs)
+ self._name = name
+
+ def __repr__(self) -> str:
+ d = self.__dict__.copy()
+ del d["_name"]
+ return f"<RecordedHookCall {self._name!r}(**{d!r})>"
+
+ if TYPE_CHECKING:
+ # The class has undetermined attributes, this tells mypy about it.
+ def __getattr__(self, key: str):
+ ...
+
+
+@final
+class HookRecorder:
+ """Record all hooks called in a plugin manager.
+
+ Hook recorders are created by :class:`Pytester`.
+
+ This wraps all the hook calls in the plugin manager, recording each call
+ before propagating the normal calls.
+ """
+
+ def __init__(
+ self, pluginmanager: PytestPluginManager, *, _ispytest: bool = False
+ ) -> None:
+ check_ispytest(_ispytest)
+
+ self._pluginmanager = pluginmanager
+ self.calls: List[RecordedHookCall] = []
+ self.ret: Optional[Union[int, ExitCode]] = None
+
+ def before(hook_name: str, hook_impls, kwargs) -> None:
+ self.calls.append(RecordedHookCall(hook_name, kwargs))
+
+ def after(outcome, hook_name: str, hook_impls, kwargs) -> None:
+ pass
+
+ self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after)
+
+ def finish_recording(self) -> None:
+ self._undo_wrapping()
+
+ def getcalls(self, names: Union[str, Iterable[str]]) -> List[RecordedHookCall]:
+ """Get all recorded calls to hooks with the given names (or name)."""
+ if isinstance(names, str):
+ names = names.split()
+ return [call for call in self.calls if call._name in names]
+
+ def assert_contains(self, entries: Sequence[Tuple[str, str]]) -> None:
+ __tracebackhide__ = True
+ i = 0
+ entries = list(entries)
+ backlocals = sys._getframe(1).f_locals
+ while entries:
+ name, check = entries.pop(0)
+ for ind, call in enumerate(self.calls[i:]):
+ if call._name == name:
+ print("NAMEMATCH", name, call)
+ if eval(check, backlocals, call.__dict__):
+ print("CHECKERMATCH", repr(check), "->", call)
+ else:
+ print("NOCHECKERMATCH", repr(check), "-", call)
+ continue
+ i += ind + 1
+ break
+ print("NONAMEMATCH", name, "with", call)
+ else:
+ fail(f"could not find {name!r} check {check!r}")
+
+ def popcall(self, name: str) -> RecordedHookCall:
+ __tracebackhide__ = True
+ for i, call in enumerate(self.calls):
+ if call._name == name:
+ del self.calls[i]
+ return call
+ lines = [f"could not find call {name!r}, in:"]
+ lines.extend([" %s" % x for x in self.calls])
+ fail("\n".join(lines))
+
+ def getcall(self, name: str) -> RecordedHookCall:
+ values = self.getcalls(name)
+ assert len(values) == 1, (name, values)
+ return values[0]
+
+ # functionality for test reports
+
+ @overload
+ def getreports(
+ self,
+ names: "Literal['pytest_collectreport']",
+ ) -> Sequence[CollectReport]:
+ ...
+
+ @overload
+ def getreports(
+ self,
+ names: "Literal['pytest_runtest_logreport']",
+ ) -> Sequence[TestReport]:
+ ...
+
+ @overload
+ def getreports(
+ self,
+ names: Union[str, Iterable[str]] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[Union[CollectReport, TestReport]]:
+ ...
+
+ def getreports(
+ self,
+ names: Union[str, Iterable[str]] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[Union[CollectReport, TestReport]]:
+ return [x.report for x in self.getcalls(names)]
+
+ def matchreport(
+ self,
+ inamepart: str = "",
+ names: Union[str, Iterable[str]] = (
+ "pytest_runtest_logreport",
+ "pytest_collectreport",
+ ),
+ when: Optional[str] = None,
+ ) -> Union[CollectReport, TestReport]:
+ """Return a testreport whose dotted import path matches."""
+ values = []
+ for rep in self.getreports(names=names):
+ if not when and rep.when != "call" and rep.passed:
+ # setup/teardown passing reports - let's ignore those
+ continue
+ if when and rep.when != when:
+ continue
+ if not inamepart or inamepart in rep.nodeid.split("::"):
+ values.append(rep)
+ if not values:
+ raise ValueError(
+ "could not find test report matching %r: "
+ "no test reports at all!" % (inamepart,)
+ )
+ if len(values) > 1:
+ raise ValueError(
+ "found 2 or more testreports matching {!r}: {}".format(
+ inamepart, values
+ )
+ )
+ return values[0]
+
+ @overload
+ def getfailures(
+ self,
+ names: "Literal['pytest_collectreport']",
+ ) -> Sequence[CollectReport]:
+ ...
+
+ @overload
+ def getfailures(
+ self,
+ names: "Literal['pytest_runtest_logreport']",
+ ) -> Sequence[TestReport]:
+ ...
+
+ @overload
+ def getfailures(
+ self,
+ names: Union[str, Iterable[str]] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[Union[CollectReport, TestReport]]:
+ ...
+
+ def getfailures(
+ self,
+ names: Union[str, Iterable[str]] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[Union[CollectReport, TestReport]]:
+ return [rep for rep in self.getreports(names) if rep.failed]
+
+ def getfailedcollections(self) -> Sequence[CollectReport]:
+ return self.getfailures("pytest_collectreport")
+
+ def listoutcomes(
+ self,
+ ) -> Tuple[
+ Sequence[TestReport],
+ Sequence[Union[CollectReport, TestReport]],
+ Sequence[Union[CollectReport, TestReport]],
+ ]:
+ passed = []
+ skipped = []
+ failed = []
+ for rep in self.getreports(
+ ("pytest_collectreport", "pytest_runtest_logreport")
+ ):
+ if rep.passed:
+ if rep.when == "call":
+ assert isinstance(rep, TestReport)
+ passed.append(rep)
+ elif rep.skipped:
+ skipped.append(rep)
+ else:
+ assert rep.failed, f"Unexpected outcome: {rep!r}"
+ failed.append(rep)
+ return passed, skipped, failed
+
+ def countoutcomes(self) -> List[int]:
+ return [len(x) for x in self.listoutcomes()]
+
+ def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None:
+ __tracebackhide__ = True
+ from _pytest.pytester_assertions import assertoutcome
+
+ outcomes = self.listoutcomes()
+ assertoutcome(
+ outcomes,
+ passed=passed,
+ skipped=skipped,
+ failed=failed,
+ )
+
+ def clear(self) -> None:
+ self.calls[:] = []
+
+
+@fixture
+def linecomp() -> "LineComp":
+ """A :class: `LineComp` instance for checking that an input linearly
+ contains a sequence of strings."""
+ return LineComp()
+
+
+@fixture(name="LineMatcher")
+def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]:
+ """A reference to the :class: `LineMatcher`.
+
+ This is instantiable with a list of lines (without their trailing newlines).
+ This is useful for testing large texts, such as the output of commands.
+ """
+ return LineMatcher
+
+
+@fixture
+def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pytester":
+ """
+ Facilities to write tests/configuration files, execute pytest in isolation, and match
+ 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.
+
+ 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)
+
+
+@fixture
+def _sys_snapshot() -> Generator[None, None, None]:
+ snappaths = SysPathsSnapshot()
+ snapmods = SysModulesSnapshot()
+ yield
+ snapmods.restore()
+ snappaths.restore()
+
+
+@fixture
+def _config_for_test() -> Generator[Config, None, None]:
+ from _pytest.config import get_config
+
+ config = get_config()
+ yield config
+ config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles.
+
+
+# Regex to match the session duration string in the summary: "74.34s".
+rex_session_duration = re.compile(r"\d+\.\d\ds")
+# Regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped".
+rex_outcome = re.compile(r"(\d+) (\w+)")
+
+
+@final
+class RunResult:
+ """The result of running a command from :class:`~pytest.Pytester`."""
+
+ def __init__(
+ self,
+ ret: Union[int, ExitCode],
+ outlines: List[str],
+ errlines: List[str],
+ duration: float,
+ ) -> None:
+ try:
+ self.ret: Union[int, ExitCode] = ExitCode(ret)
+ """The return value."""
+ except ValueError:
+ self.ret = ret
+ self.outlines = outlines
+ """List of lines captured from stdout."""
+ self.errlines = errlines
+ """List of lines captured from stderr."""
+ self.stdout = LineMatcher(outlines)
+ """:class:`~pytest.LineMatcher` of stdout.
+
+ Use e.g. :func:`str(stdout) <pytest.LineMatcher.__str__()>` to reconstruct stdout, or the commonly used
+ :func:`stdout.fnmatch_lines() <pytest.LineMatcher.fnmatch_lines()>` method.
+ """
+ self.stderr = LineMatcher(errlines)
+ """:class:`~pytest.LineMatcher` of stderr."""
+ self.duration = duration
+ """Duration in seconds."""
+
+ def __repr__(self) -> str:
+ return (
+ "<RunResult ret=%s len(stdout.lines)=%d len(stderr.lines)=%d duration=%.2fs>"
+ % (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration)
+ )
+
+ def parseoutcomes(self) -> Dict[str, int]:
+ """Return a dictionary of outcome noun -> count from parsing the terminal
+ output that the test process produced.
+
+ The returned nouns will always be in plural form::
+
+ ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
+
+ Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
+ """
+ return self.parse_summary_nouns(self.outlines)
+
+ @classmethod
+ def parse_summary_nouns(cls, lines) -> Dict[str, int]:
+ """Extract the nouns from a pytest terminal summary line.
+
+ It always returns the plural noun for consistency::
+
+ ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
+
+ Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
+ """
+ for line in reversed(lines):
+ if rex_session_duration.search(line):
+ outcomes = rex_outcome.findall(line)
+ ret = {noun: int(count) for (count, noun) in outcomes}
+ break
+ else:
+ raise ValueError("Pytest terminal summary report not found")
+
+ to_plural = {
+ "warning": "warnings",
+ "error": "errors",
+ }
+ return {to_plural.get(k, k): v for k, v in ret.items()}
+
+ def assert_outcomes(
+ self,
+ passed: int = 0,
+ skipped: int = 0,
+ failed: int = 0,
+ errors: int = 0,
+ xpassed: int = 0,
+ xfailed: int = 0,
+ warnings: Optional[int] = None,
+ deselected: Optional[int] = None,
+ ) -> None:
+ """
+ Assert that the specified outcomes appear with the respective
+ numbers (0 means it didn't occur) in the text output from a test run.
+
+ ``warnings`` and ``deselected`` are only checked if not None.
+ """
+ __tracebackhide__ = True
+ from _pytest.pytester_assertions import assert_outcomes
+
+ outcomes = self.parseoutcomes()
+ assert_outcomes(
+ outcomes,
+ passed=passed,
+ skipped=skipped,
+ failed=failed,
+ errors=errors,
+ xpassed=xpassed,
+ xfailed=xfailed,
+ warnings=warnings,
+ deselected=deselected,
+ )
+
+
+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
+ self.__saved = dict(sys.modules)
+
+ def restore(self) -> None:
+ if self.__preserve:
+ self.__saved.update(
+ (k, m) for k, m in sys.modules.items() if self.__preserve(k)
+ )
+ sys.modules.clear()
+ sys.modules.update(self.__saved)
+
+
+class SysPathsSnapshot:
+ def __init__(self) -> None:
+ self.__saved = list(sys.path), list(sys.meta_path)
+
+ def restore(self) -> None:
+ sys.path[:], sys.meta_path[:] = self.__saved
+
+
+@final
+class Pytester:
+ """
+ Facilities to write tests/configuration files, execute pytest in isolation, and match
+ 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.
+ """
+
+ __test__ = False
+
+ CLOSE_STDIN: "Final" = NOTSET
+
+ class TimeoutExpired(Exception):
+ pass
+
+ def __init__(
+ self,
+ request: FixtureRequest,
+ tmp_path_factory: TempPathFactory,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._request = request
+ self._mod_collections: WeakKeyDictionary[
+ Collector, List[Union[Item, Collector]]
+ ] = WeakKeyDictionary()
+ if request.function:
+ name: str = request.function.__name__
+ else:
+ name = request.node.name
+ self._name = name
+ self._path: Path = tmp_path_factory.mktemp(name, numbered=True)
+ 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()
+ mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot))
+ # Ensure no unexpected caching via tox.
+ mp.delenv("TOX_ENV_DIR", raising=False)
+ # Discard outer pytest options.
+ mp.delenv("PYTEST_ADDOPTS", raising=False)
+ # Ensure no user config is used.
+ tmphome = str(self.path)
+ mp.setenv("HOME", tmphome)
+ mp.setenv("USERPROFILE", tmphome)
+ # Do not use colors for inner runs by default.
+ mp.setenv("PY_COLORS", "0")
+
+ @property
+ def path(self) -> Path:
+ """Temporary directory where files are created and pytest is executed."""
+ return self._path
+
+ def __repr__(self) -> str:
+ return f"<Pytester {self.path!r}>"
+
+ def _finalize(self) -> None:
+ """
+ Clean up global state artifacts.
+
+ Some methods modify the global interpreter state and this tries to
+ clean this up. It does not remove the temporary directory however so
+ it can be looked at after the test run has finished.
+ """
+ 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
+ # and can't be deleted; we had some trouble in the past with
+ # `zope.interface` for example.
+ #
+ # Preserve readline due to https://bugs.python.org/issue41033.
+ # pexpect issues a SIGWINCH.
+ def preserve_module(name):
+ return name.startswith(("zope", "readline"))
+
+ 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)
+ self._request.addfinalizer(reprec.finish_recording)
+ return reprec
+
+ def chdir(self) -> None:
+ """Cd into the temporary directory.
+
+ This is done automatically upon instantiation.
+ """
+ os.chdir(self.path)
+
+ def _makefile(
+ self,
+ ext: str,
+ lines: Sequence[Union[Any, bytes]],
+ files: Dict[str, str],
+ encoding: str = "utf-8",
+ ) -> Path:
+ items = list(files.items())
+
+ if ext and not ext.startswith("."):
+ raise ValueError(
+ f"pytester.makefile expects a file extension, try .{ext} instead of {ext}"
+ )
+
+ def to_text(s: Union[Any, bytes]) -> str:
+ return s.decode(encoding) if isinstance(s, bytes) else str(s)
+
+ if lines:
+ source = "\n".join(to_text(x) for x in lines)
+ basename = self._name
+ items.insert(0, (basename, source))
+
+ ret = None
+ for basename, value in items:
+ p = self.path.joinpath(basename).with_suffix(ext)
+ p.parent.mkdir(parents=True, exist_ok=True)
+ source_ = Source(value)
+ source = "\n".join(to_text(line) for line in source_.lines)
+ p.write_text(source.strip(), encoding=encoding)
+ if ret is None:
+ ret = p
+ assert ret is not None
+ return ret
+
+ def makefile(self, ext: str, *args: str, **kwargs: str) -> Path:
+ r"""Create new text file(s) in the test directory.
+
+ :param str 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.
+ The result is written as contents to the file. The name of the
+ file is based on the test function requesting this fixture.
+ :param kwargs:
+ Each keyword is the name of a file, while the value of it will
+ be written as contents of the file.
+
+ Examples:
+
+ .. code-block:: python
+
+ pytester.makefile(".txt", "line1", "line2")
+
+ pytester.makefile(".ini", pytest="[pytest]\naddopts=-rs\n")
+
+ To create binary files, use :meth:`pathlib.Path.write_bytes` directly:
+
+ .. code-block:: python
+
+ filename = pytester.path.joinpath("foo.bin")
+ filename.write_bytes(b"...")
+ """
+ return self._makefile(ext, args, kwargs)
+
+ def makeconftest(self, source: str) -> Path:
+ """Write a contest.py file with 'source' as contents."""
+ return self.makepyfile(conftest=source)
+
+ def makeini(self, source: str) -> Path:
+ """Write a tox.ini file with 'source' as contents."""
+ return self.makefile(".ini", tox=source)
+
+ def getinicfg(self, source: str) -> SectionWrapper:
+ """Return the pytest section from the tox.ini config file."""
+ p = self.makeini(source)
+ return IniConfig(str(p))["pytest"]
+
+ def makepyprojecttoml(self, source: str) -> Path:
+ """Write a pyproject.toml file with 'source' as contents.
+
+ .. versionadded:: 6.0
+ """
+ return self.makefile(".toml", pyproject=source)
+
+ def makepyfile(self, *args, **kwargs) -> Path:
+ r"""Shortcut for .makefile() with a .py extension.
+
+ Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting
+ existing files.
+
+ Examples:
+
+ .. code-block:: python
+
+ def test_something(pytester):
+ # Initial file is created test_something.py.
+ pytester.makepyfile("foobar")
+ # To create multiple files, pass kwargs accordingly.
+ pytester.makepyfile(custom="foobar")
+ # At this point, both 'test_something.py' & 'custom.py' exist in the test directory.
+
+ """
+ return self._makefile(".py", args, kwargs)
+
+ def maketxtfile(self, *args, **kwargs) -> Path:
+ r"""Shortcut for .makefile() with a .txt extension.
+
+ Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting
+ existing files.
+
+ Examples:
+
+ .. code-block:: python
+
+ def test_something(pytester):
+ # Initial file is created test_something.txt.
+ pytester.maketxtfile("foobar")
+ # To create multiple files, pass kwargs accordingly.
+ pytester.maketxtfile(custom="foobar")
+ # At this point, both 'test_something.txt' & 'custom.txt' exist in the test directory.
+
+ """
+ return self._makefile(".txt", args, kwargs)
+
+ def syspathinsert(
+ self, path: Optional[Union[str, "os.PathLike[str]"]] = None
+ ) -> None:
+ """Prepend a directory to sys.path, defaults to :attr:`path`.
+
+ This is undone automatically when this object dies at the end of each
+ test.
+ """
+ if path is None:
+ path = self.path
+
+ self._monkeypatch.syspath_prepend(str(path))
+
+ def mkdir(self, name: str) -> Path:
+ """Create a new (sub)directory."""
+ p = self.path / name
+ p.mkdir()
+ return p
+
+ def mkpydir(self, name: str) -> Path:
+ """Create a new python package.
+
+ This creates a (sub)directory with an empty ``__init__.py`` file so it
+ gets recognised as a Python package.
+ """
+ p = self.path / name
+ p.mkdir()
+ p.joinpath("__init__.py").touch()
+ return p
+
+ 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``).
+
+ """
+ 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
+
+ for extra_element in self._request.node.iter_markers("pytester_example_path"):
+ assert extra_element.args
+ example_dir = example_dir.joinpath(*extra_element.args)
+
+ if name is None:
+ func_name = self._name
+ maybe_dir = example_dir / func_name
+ maybe_file = example_dir / (func_name + ".py")
+
+ if maybe_dir.is_dir():
+ example_path = maybe_dir
+ elif maybe_file.is_file():
+ example_path = maybe_file
+ else:
+ raise LookupError(
+ f"{func_name} can't be found as module or package in {example_dir}"
+ )
+ else:
+ 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)
+ return self.path
+ elif example_path.is_file():
+ result = self.path.joinpath(example_path.name)
+ shutil.copy(example_path, result)
+ return result
+ else:
+ raise LookupError(
+ f'example "{example_path}" is not found as a file or directory'
+ )
+
+ def getnode(
+ self, config: Config, arg: Union[str, "os.PathLike[str]"]
+ ) -> Optional[Union[Collector, Item]]:
+ """Return the collection node of a file.
+
+ :param pytest.Config config:
+ A pytest config.
+ See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it.
+ :param os.PathLike[str] arg:
+ Path to the file.
+ """
+ session = Session.from_config(config)
+ assert "::" not in str(arg)
+ p = Path(os.path.abspath(arg))
+ config.hook.pytest_sessionstart(session=session)
+ res = session.perform_collect([str(p)], genitems=False)[0]
+ config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
+ return res
+
+ def getpathnode(self, path: Union[str, "os.PathLike[str]"]):
+ """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.
+ """
+ path = Path(path)
+ config = self.parseconfigure(path)
+ session = Session.from_config(config)
+ x = bestrelpath(session.path, path)
+ config.hook.pytest_sessionstart(session=session)
+ res = session.perform_collect([x], genitems=False)[0]
+ config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
+ return res
+
+ def genitems(self, colitems: Sequence[Union[Item, Collector]]) -> List[Item]:
+ """Generate all test items from a collection node.
+
+ This recurses into the collection node and returns a list of all the
+ test items contained within.
+ """
+ session = colitems[0].session
+ result: List[Item] = []
+ for colitem in colitems:
+ result.extend(session.genitems(colitem))
+ return result
+
+ def runitem(self, source: str) -> Any:
+ """Run the "test_func" Item.
+
+ 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`.
+ """
+ # used from runner functional tests
+ item = self.getitem(source)
+ # the test class where we are called from wants to provide the runner
+ testclassinstance = self._request.instance
+ runner = testclassinstance.getrunner()
+ return runner(item)
+
+ def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder:
+ """Run a test module in process using ``pytest.main()``.
+
+ This run writes "source" into a temporary file and runs
+ ``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance
+ for the result.
+
+ :param source: The source code of the test module.
+ :param cmdlineargs: Any extra command line arguments to use.
+ """
+ p = self.makepyfile(source)
+ values = list(cmdlineargs) + [p]
+ return self.inline_run(*values)
+
+ def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]:
+ """Run ``pytest.main(['--collectonly'])`` 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
+ tuple of the collected items and a :py:class:`HookRecorder` instance.
+ """
+ rec = self.inline_run("--collect-only", *args)
+ items = [x.item for x in rec.getcalls("pytest_itemcollected")]
+ return items, rec
+
+ def inline_run(
+ self,
+ *args: Union[str, "os.PathLike[str]"],
+ plugins=(),
+ no_reraise_ctrlc: bool = False,
+ ) -> HookRecorder:
+ """Run ``pytest.main()`` in-process, returning a HookRecorder.
+
+ Runs the :py:func:`pytest.main` function to run all of pytest inside
+ the test process itself. This means it can return a
+ :py:class:`HookRecorder` instance which gives more detailed results
+ from that run than can be done by matching stdout/stderr from
+ :py:meth:`runpytest`.
+
+ :param args:
+ Command line arguments to pass to :py:func:`pytest.main`.
+ :param plugins:
+ Extra plugin instances the ``pytest.main()`` instance should use.
+ :param no_reraise_ctrlc:
+ Typically we reraise keyboard interrupts from the child run. If
+ True, the KeyboardInterrupt exception is captured.
+ """
+ # (maybe a cpython bug?) the importlib cache sometimes isn't updated
+ # properly between file creation and inline_run (especially if imports
+ # are interspersed with file creation)
+ importlib.invalidate_caches()
+
+ plugins = list(plugins)
+ finalizers = []
+ try:
+ # Any sys.module or sys.path changes done while running pytest
+ # inline should be reverted after the test run completes to avoid
+ # clashing with later inline tests run within the same pytest test,
+ # e.g. just because they use matching test module names.
+ finalizers.append(self.__take_sys_modules_snapshot().restore)
+ finalizers.append(SysPathsSnapshot().restore)
+
+ # Important note:
+ # - our tests should not leave any other references/registrations
+ # laying around other than possibly loaded test modules
+ # referenced from sys.modules, as nothing will clean those up
+ # automatically
+
+ rec = []
+
+ class Collect:
+ def pytest_configure(x, config: Config) -> None:
+ rec.append(self.make_hook_recorder(config.pluginmanager))
+
+ plugins.append(Collect())
+ ret = main([str(x) for x in args], plugins=plugins)
+ if len(rec) == 1:
+ reprec = rec.pop()
+ else:
+
+ class reprec: # type: ignore
+ pass
+
+ reprec.ret = ret
+
+ # Typically we reraise keyboard interrupts from the child run
+ # because it's our user requesting interruption of the testing.
+ if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc:
+ calls = reprec.getcalls("pytest_keyboard_interrupt")
+ if calls and calls[-1].excinfo.type == KeyboardInterrupt:
+ raise KeyboardInterrupt()
+ return reprec
+ finally:
+ for finalizer in finalizers:
+ finalizer()
+
+ def runpytest_inprocess(
+ self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any
+ ) -> RunResult:
+ """Return result of running pytest in-process, providing a similar
+ interface to what self.runpytest() provides."""
+ syspathinsert = kwargs.pop("syspathinsert", False)
+
+ if syspathinsert:
+ self.syspathinsert()
+ now = timing.time()
+ capture = _get_multicapture("sys")
+ capture.start_capturing()
+ try:
+ try:
+ reprec = self.inline_run(*args, **kwargs)
+ except SystemExit as e:
+ ret = e.args[0]
+ try:
+ ret = ExitCode(e.args[0])
+ except ValueError:
+ pass
+
+ class reprec: # type: ignore
+ ret = ret
+
+ except Exception:
+ traceback.print_exc()
+
+ class reprec: # type: ignore
+ ret = ExitCode(3)
+
+ finally:
+ out, err = capture.readouterr()
+ capture.stop_capturing()
+ sys.stdout.write(out)
+ sys.stderr.write(err)
+
+ assert reprec.ret is not None
+ res = RunResult(
+ reprec.ret, out.splitlines(), err.splitlines(), timing.time() - now
+ )
+ res.reprec = reprec # type: ignore
+ return res
+
+ def runpytest(
+ self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any
+ ) -> RunResult:
+ """Run pytest inline or in a subprocess, depending on the command line
+ option "--runpytest" and return a :py:class:`~pytest.RunResult`."""
+ new_args = self._ensure_basetemp(args)
+ if self._method == "inprocess":
+ return self.runpytest_inprocess(*new_args, **kwargs)
+ elif self._method == "subprocess":
+ return self.runpytest_subprocess(*new_args, **kwargs)
+ raise RuntimeError(f"Unrecognized runpytest option: {self._method}")
+
+ def _ensure_basetemp(
+ self, args: Sequence[Union[str, "os.PathLike[str]"]]
+ ) -> List[Union[str, "os.PathLike[str]"]]:
+ new_args = list(args)
+ for x in new_args:
+ if str(x).startswith("--basetemp"):
+ break
+ else:
+ new_args.append("--basetemp=%s" % self.path.parent.joinpath("basetemp"))
+ return new_args
+
+ def parseconfig(self, *args: Union[str, "os.PathLike[str]"]) -> Config:
+ """Return a new 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.
+
+ If :py:attr:`plugins` has been populated they should be plugin modules
+ to be registered with the PluginManager.
+ """
+ import _pytest.config
+
+ new_args = self._ensure_basetemp(args)
+ new_args = [str(x) for x in new_args]
+
+ config = _pytest.config._prepareconfig(new_args, self.plugins) # type: ignore[arg-type]
+ # we don't know what the test will do with this half-setup config
+ # object and thus we make sure it gets unconfigured properly in any
+ # case (otherwise capturing could still be active, for example)
+ self._request.addfinalizer(config._ensure_unconfigure)
+ return config
+
+ def parseconfigure(self, *args: Union[str, "os.PathLike[str]"]) -> Config:
+ """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.
+ """
+ config = self.parseconfig(*args)
+ config._do_configure()
+ return config
+
+ def getitem(
+ self, source: Union[str, "os.PathLike[str]"], funcname: str = "test_func"
+ ) -> Item:
+ """Return the test item for a test function.
+
+ Writes the source to a python file and runs pytest's collection on
+ the resulting module, returning the test item for the requested
+ function name.
+
+ :param source:
+ The module source.
+ :param funcname:
+ The name of the test function for which to return a 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
+ )
+
+ def getitems(self, source: Union[str, "os.PathLike[str]"]) -> List[Item]:
+ """Return all test items collected from the module.
+
+ Writes the source to a Python file and runs pytest's collection on
+ the resulting module, returning all test items contained within.
+ """
+ modcol = self.getmodulecol(source)
+ return self.genitems([modcol])
+
+ def getmodulecol(
+ self,
+ source: Union[str, "os.PathLike[str]"],
+ configargs=(),
+ *,
+ withinit: bool = False,
+ ):
+ """Return the module collection node for ``source``.
+
+ Writes ``source`` to a file using :py:meth:`makepyfile` and then
+ runs the pytest collection on it, returning the collection node for the
+ test module.
+
+ :param source:
+ The source code of the module to collect.
+
+ :param configargs:
+ Any extra arguments to pass to :py:meth:`parseconfigure`.
+
+ :param withinit:
+ Whether to also write an ``__init__.py`` file to the same
+ directory to ensure it is a package.
+ """
+ if isinstance(source, os.PathLike):
+ path = self.path.joinpath(source)
+ assert not withinit, "not supported for paths"
+ else:
+ kw = {self._name: str(source)}
+ path = self.makepyfile(**kw)
+ if withinit:
+ self.makepyfile(__init__="#")
+ self.config = config = self.parseconfigure(path, *configargs)
+ return self.getnode(config, path)
+
+ def collect_by_name(
+ self, modcol: Collector, name: str
+ ) -> Optional[Union[Item, Collector]]:
+ """Return the collection node for name from the module collection.
+
+ Searches a module collection node for a collection node matching the
+ given name.
+
+ :param modcol: A module collection node; see :py:meth:`getmodulecol`.
+ :param name: The name of the node to return.
+ """
+ if modcol not in self._mod_collections:
+ self._mod_collections[modcol] = list(modcol.collect())
+ for colitem in self._mod_collections[modcol]:
+ if colitem.name == name:
+ return colitem
+ return None
+
+ def popen(
+ self,
+ cmdargs: Sequence[Union[str, "os.PathLike[str]"]],
+ stdout: Union[int, TextIO] = subprocess.PIPE,
+ stderr: Union[int, TextIO] = subprocess.PIPE,
+ stdin: Union[NotSetType, bytes, IO[Any], int] = CLOSE_STDIN,
+ **kw,
+ ):
+ """Invoke :py:class:`subprocess.Popen`.
+
+ Calls :py:class:`subprocess.Popen` making sure the current working
+ directory is in ``PYTHONPATH``.
+
+ You probably want to use :py:meth:`run` instead.
+ """
+ env = os.environ.copy()
+ env["PYTHONPATH"] = os.pathsep.join(
+ filter(None, [os.getcwd(), env.get("PYTHONPATH", "")])
+ )
+ kw["env"] = env
+
+ if stdin is self.CLOSE_STDIN:
+ kw["stdin"] = subprocess.PIPE
+ elif isinstance(stdin, bytes):
+ kw["stdin"] = subprocess.PIPE
+ else:
+ kw["stdin"] = stdin
+
+ popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw)
+ if stdin is self.CLOSE_STDIN:
+ assert popen.stdin is not None
+ popen.stdin.close()
+ elif isinstance(stdin, bytes):
+ assert popen.stdin is not None
+ popen.stdin.write(stdin)
+
+ return popen
+
+ def run(
+ self,
+ *cmdargs: Union[str, "os.PathLike[str]"],
+ timeout: Optional[float] = None,
+ stdin: Union[NotSetType, bytes, IO[Any], int] = CLOSE_STDIN,
+ ) -> RunResult:
+ """Run a command with arguments.
+
+ Run a process using :py:class:`subprocess.Popen` saving the stdout and
+ stderr.
+
+ :param cmdargs:
+ The sequence of arguments to pass to :py:class:`subprocess.Popen`,
+ with path-like objects being converted to :py:class:`str`
+ automatically.
+ :param timeout:
+ The period in seconds after which to timeout and raise
+ :py:class:`Pytester.TimeoutExpired`.
+ :param stdin:
+ Optional standard input.
+
+ - If it is :py:attr:`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.
+
+ - If it is of type :py:class:`bytes`, these bytes are sent to the
+ standard input of the command.
+
+ - 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`.
+ """
+ __tracebackhide__ = True
+
+ cmdargs = tuple(os.fspath(arg) for arg in cmdargs)
+ p1 = self.path.joinpath("stdout")
+ p2 = self.path.joinpath("stderr")
+ print("running:", *cmdargs)
+ print(" in:", Path.cwd())
+
+ with p1.open("w", encoding="utf8") as f1, p2.open("w", encoding="utf8") as f2:
+ now = timing.time()
+ popen = self.popen(
+ cmdargs,
+ stdin=stdin,
+ stdout=f1,
+ stderr=f2,
+ close_fds=(sys.platform != "win32"),
+ )
+ if popen.stdin is not None:
+ popen.stdin.close()
+
+ def handle_timeout() -> None:
+ __tracebackhide__ = True
+
+ timeout_message = (
+ "{seconds} second timeout expired running:"
+ " {command}".format(seconds=timeout, command=cmdargs)
+ )
+
+ popen.kill()
+ popen.wait()
+ raise self.TimeoutExpired(timeout_message)
+
+ if timeout is None:
+ ret = popen.wait()
+ else:
+ try:
+ ret = popen.wait(timeout)
+ except subprocess.TimeoutExpired:
+ handle_timeout()
+
+ with p1.open(encoding="utf8") as f1, p2.open(encoding="utf8") as f2:
+ out = f1.read().splitlines()
+ err = f2.read().splitlines()
+
+ self._dump_lines(out, sys.stdout)
+ self._dump_lines(err, sys.stderr)
+
+ with contextlib.suppress(ValueError):
+ ret = ExitCode(ret)
+ return RunResult(ret, out, err, timing.time() - now)
+
+ def _dump_lines(self, lines, fp):
+ try:
+ for line in lines:
+ print(line, file=fp)
+ except UnicodeEncodeError:
+ print(f"couldn't print to {fp} because of encoding")
+
+ def _getpytestargs(self) -> Tuple[str, ...]:
+ return sys.executable, "-mpytest"
+
+ def runpython(self, script: "os.PathLike[str]") -> RunResult:
+ """Run a python script using sys.executable as interpreter."""
+ return self.run(sys.executable, script)
+
+ def runpython_c(self, command: str) -> RunResult:
+ """Run ``python -c "command"``."""
+ return self.run(sys.executable, "-c", command)
+
+ def runpytest_subprocess(
+ self, *args: Union[str, "os.PathLike[str]"], timeout: Optional[float] = None
+ ) -> RunResult:
+ """Run pytest as a subprocess with given arguments.
+
+ Any plugins added to the :py:attr:`plugins` list will be added using the
+ ``-p`` command line option. Additionally ``--basetemp`` is used to put
+ any temporary files and directories in a numbered directory prefixed
+ with "runpytest-" to not conflict with the normal numbered pytest
+ location for temporary files and directories.
+
+ :param args:
+ The sequence of arguments to pass to the pytest subprocess.
+ :param timeout:
+ The period in seconds after which to timeout and raise
+ :py:class:`Pytester.TimeoutExpired`.
+ """
+ __tracebackhide__ = True
+ p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700)
+ args = ("--basetemp=%s" % p,) + args
+ plugins = [x for x in self.plugins if isinstance(x, str)]
+ if plugins:
+ args = ("-p", plugins[0]) + args
+ args = self._getpytestargs() + args
+ return self.run(*args, timeout=timeout)
+
+ def spawn_pytest(
+ self, string: str, expect_timeout: float = 10.0
+ ) -> "pexpect.spawn":
+ """Run pytest using pexpect.
+
+ This makes sure to use the right pytest and sets up the temporary
+ directory locations.
+
+ The pexpect child is returned.
+ """
+ basetemp = self.path / "temp-pexpect"
+ basetemp.mkdir(mode=0o700)
+ invoke = " ".join(map(str, self._getpytestargs()))
+ cmd = f"{invoke} --basetemp={basetemp} {string}"
+ return self.spawn(cmd, expect_timeout=expect_timeout)
+
+ def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn":
+ """Run a command using pexpect.
+
+ The pexpect child is returned.
+ """
+ pexpect = importorskip("pexpect", "3.0")
+ if hasattr(sys, "pypy_version_info") and "64" in platform.machine():
+ skip("pypy-64 bit not supported")
+ if not hasattr(pexpect, "spawn"):
+ skip("pexpect.spawn not available")
+ logfile = self.path.joinpath("spawn.out").open("wb")
+
+ child = pexpect.spawn(cmd, logfile=logfile, timeout=expect_timeout)
+ self._request.addfinalizer(logfile.close)
+ return child
+
+
+class LineComp:
+ def __init__(self) -> None:
+ self.stringio = StringIO()
+ """:class:`python:io.StringIO()` instance used for input."""
+
+ def assert_contains_lines(self, lines2: Sequence[str]) -> None:
+ """Assert that ``lines2`` are contained (linearly) in :attr:`stringio`'s value.
+
+ Lines are matched using :func:`LineMatcher.fnmatch_lines <pytest.LineMatcher.fnmatch_lines>`.
+ """
+ __tracebackhide__ = True
+ val = self.stringio.getvalue()
+ self.stringio.truncate(0)
+ self.stringio.seek(0)
+ lines1 = val.split("\n")
+ LineMatcher(lines1).fnmatch_lines(lines2)
+
+
+class LineMatcher:
+ """Flexible matching of text.
+
+ This is a convenience class to test large texts like the output of
+ commands.
+
+ The constructor takes a list of lines without their trailing newlines, i.e.
+ ``text.splitlines()``.
+ """
+
+ def __init__(self, lines: List[str]) -> None:
+ self.lines = lines
+ self._log_output: List[str] = []
+
+ def __str__(self) -> str:
+ """Return the entire original text.
+
+ .. versionadded:: 6.2
+ You can use :meth:`str` in older versions.
+ """
+ return "\n".join(self.lines)
+
+ def _getlines(self, lines2: Union[str, Sequence[str], Source]) -> Sequence[str]:
+ if isinstance(lines2, str):
+ lines2 = Source(lines2)
+ if isinstance(lines2, Source):
+ lines2 = lines2.strip().lines
+ return lines2
+
+ def fnmatch_lines_random(self, lines2: Sequence[str]) -> None:
+ """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`)."""
+ __tracebackhide__ = True
+ self._match_lines_random(lines2, fnmatch)
+
+ def re_match_lines_random(self, lines2: Sequence[str]) -> None:
+ """Check lines exist in the output in any order (using :func:`python:re.match`)."""
+ __tracebackhide__ = True
+ self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name)))
+
+ def _match_lines_random(
+ self, lines2: Sequence[str], match_func: Callable[[str, str], bool]
+ ) -> None:
+ __tracebackhide__ = True
+ lines2 = self._getlines(lines2)
+ for line in lines2:
+ for x in self.lines:
+ if line == x or match_func(x, line):
+ self._log("matched: ", repr(line))
+ break
+ else:
+ msg = "line %r not found in output" % line
+ self._log(msg)
+ self._fail(msg)
+
+ def get_lines_after(self, fnline: str) -> Sequence[str]:
+ """Return all lines following the given line in the text.
+
+ The given line can contain glob wildcards.
+ """
+ for i, line in enumerate(self.lines):
+ if fnline == line or fnmatch(line, fnline):
+ return self.lines[i + 1 :]
+ raise ValueError("line %r not found in output" % fnline)
+
+ def _log(self, *args) -> None:
+ self._log_output.append(" ".join(str(x) for x in args))
+
+ @property
+ def _log_text(self) -> str:
+ return "\n".join(self._log_output)
+
+ def fnmatch_lines(
+ self, lines2: Sequence[str], *, consecutive: bool = False
+ ) -> None:
+ """Check lines exist in the output (using :func:`python:fnmatch.fnmatch`).
+
+ The argument is a list of lines which have to match and can use glob
+ wildcards. If they do not match a pytest.fail() is called. The
+ matches and non-matches are also shown as part of the error message.
+
+ :param lines2: String patterns to match.
+ :param consecutive: Match lines consecutively?
+ """
+ __tracebackhide__ = True
+ self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive)
+
+ def re_match_lines(
+ self, lines2: Sequence[str], *, consecutive: bool = False
+ ) -> None:
+ """Check lines exist in the output (using :func:`python:re.match`).
+
+ The argument is a list of lines which have to match using ``re.match``.
+ If they do not match a pytest.fail() is called.
+
+ The matches and non-matches are also shown as part of the error message.
+
+ :param lines2: string patterns to match.
+ :param consecutive: match lines consecutively?
+ """
+ __tracebackhide__ = True
+ self._match_lines(
+ lines2,
+ lambda name, pat: bool(re.match(pat, name)),
+ "re.match",
+ consecutive=consecutive,
+ )
+
+ def _match_lines(
+ self,
+ lines2: Sequence[str],
+ match_func: Callable[[str, str], bool],
+ match_nickname: str,
+ *,
+ consecutive: bool = False,
+ ) -> None:
+ """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
+
+ :param Sequence[str] lines2:
+ List of string patterns to match. The actual format depends on
+ ``match_func``.
+ :param match_func:
+ A callable ``match_func(line, pattern)`` where line is the
+ captured line from stdout/stderr and pattern is the matching
+ pattern.
+ :param str match_nickname:
+ The nickname for the match function that will be logged to stdout
+ when a match occurs.
+ :param consecutive:
+ Match lines consecutively?
+ """
+ if not isinstance(lines2, collections.abc.Sequence):
+ raise TypeError(f"invalid type for lines2: {type(lines2).__name__}")
+ lines2 = self._getlines(lines2)
+ lines1 = self.lines[:]
+ extralines = []
+ __tracebackhide__ = True
+ wnick = len(match_nickname) + 1
+ started = False
+ for line in lines2:
+ nomatchprinted = False
+ while lines1:
+ nextline = lines1.pop(0)
+ if line == nextline:
+ self._log("exact match:", repr(line))
+ started = True
+ break
+ elif match_func(nextline, line):
+ self._log("%s:" % match_nickname, repr(line))
+ self._log(
+ "{:>{width}}".format("with:", width=wnick), repr(nextline)
+ )
+ started = True
+ break
+ else:
+ if consecutive and started:
+ msg = f"no consecutive match: {line!r}"
+ self._log(msg)
+ self._log(
+ "{:>{width}}".format("with:", width=wnick), repr(nextline)
+ )
+ self._fail(msg)
+ if not nomatchprinted:
+ self._log(
+ "{:>{width}}".format("nomatch:", width=wnick), repr(line)
+ )
+ nomatchprinted = True
+ self._log("{:>{width}}".format("and:", width=wnick), repr(nextline))
+ extralines.append(nextline)
+ else:
+ msg = f"remains unmatched: {line!r}"
+ self._log(msg)
+ self._fail(msg)
+ self._log_output = []
+
+ def no_fnmatch_line(self, pat: str) -> None:
+ """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
+
+ :param str pat: The pattern to match lines.
+ """
+ __tracebackhide__ = True
+ self._no_match_line(pat, fnmatch, "fnmatch")
+
+ def no_re_match_line(self, pat: str) -> None:
+ """Ensure captured lines do not match the given pattern, using ``re.match``.
+
+ :param str pat: The regular expression to match lines.
+ """
+ __tracebackhide__ = True
+ self._no_match_line(
+ pat, lambda name, pat: bool(re.match(pat, name)), "re.match"
+ )
+
+ def _no_match_line(
+ self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str
+ ) -> None:
+ """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``.
+
+ :param str pat: The pattern to match lines.
+ """
+ __tracebackhide__ = True
+ nomatch_printed = False
+ wnick = len(match_nickname) + 1
+ for line in self.lines:
+ if match_func(line, pat):
+ msg = f"{match_nickname}: {pat!r}"
+ self._log(msg)
+ self._log("{:>{width}}".format("with:", width=wnick), repr(line))
+ self._fail(msg)
+ else:
+ if not nomatch_printed:
+ self._log("{:>{width}}".format("nomatch:", width=wnick), repr(pat))
+ nomatch_printed = True
+ self._log("{:>{width}}".format("and:", width=wnick), repr(line))
+ self._log_output = []
+
+ def _fail(self, msg: str) -> None:
+ __tracebackhide__ = True
+ log_text = self._log_text
+ self._log_output = []
+ fail(log_text)
+
+ def str(self) -> str:
+ """Return the entire original text."""
+ return str(self)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pytester_assertions.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pytester_assertions.py
new file mode 100644
index 0000000000..657e4db5fc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/pytester_assertions.py
@@ -0,0 +1,75 @@
+"""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
+# module to not be already imported.
+from typing import Dict
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import Union
+
+from _pytest.reports import CollectReport
+from _pytest.reports import TestReport
+
+
+def assertoutcome(
+ outcomes: Tuple[
+ Sequence[TestReport],
+ Sequence[Union[CollectReport, TestReport]],
+ Sequence[Union[CollectReport, TestReport]],
+ ],
+ passed: int = 0,
+ skipped: int = 0,
+ failed: int = 0,
+) -> None:
+ __tracebackhide__ = True
+
+ realpassed, realskipped, realfailed = outcomes
+ obtained = {
+ "passed": len(realpassed),
+ "skipped": len(realskipped),
+ "failed": len(realfailed),
+ }
+ expected = {"passed": passed, "skipped": skipped, "failed": failed}
+ assert obtained == expected, outcomes
+
+
+def assert_outcomes(
+ outcomes: Dict[str, int],
+ passed: int = 0,
+ skipped: int = 0,
+ failed: int = 0,
+ errors: int = 0,
+ xpassed: int = 0,
+ xfailed: int = 0,
+ warnings: Optional[int] = None,
+ deselected: Optional[int] = None,
+) -> None:
+ """Assert that the specified outcomes appear with the respective
+ numbers (0 means it didn't occur) in the text output from a test run."""
+ __tracebackhide__ = True
+
+ obtained = {
+ "passed": outcomes.get("passed", 0),
+ "skipped": outcomes.get("skipped", 0),
+ "failed": outcomes.get("failed", 0),
+ "errors": outcomes.get("errors", 0),
+ "xpassed": outcomes.get("xpassed", 0),
+ "xfailed": outcomes.get("xfailed", 0),
+ }
+ expected = {
+ "passed": passed,
+ "skipped": skipped,
+ "failed": failed,
+ "errors": errors,
+ "xpassed": xpassed,
+ "xfailed": xfailed,
+ }
+ if warnings is not None:
+ obtained["warnings"] = outcomes.get("warnings", 0)
+ expected["warnings"] = warnings
+ if deselected is not None:
+ obtained["deselected"] = outcomes.get("deselected", 0)
+ expected["deselected"] = deselected
+ assert obtained == expected
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/python.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/python.py
new file mode 100644
index 0000000000..0fd5702a5c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/python.py
@@ -0,0 +1,1764 @@
+"""Python test discovery, setup and run of test functions."""
+import enum
+import fnmatch
+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
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import Generator
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Pattern
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+import attr
+
+import _pytest
+from _pytest import fixtures
+from _pytest import nodes
+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._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 FuncFixtureInfo
+from _pytest.main import Session
+from _pytest.mark import MARK_GEN
+from _pytest.mark import ParameterSet
+from _pytest.mark.structures import get_unpacked_marks
+from _pytest.mark.structures import Mark
+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.scope import Scope
+from _pytest.warning_types import PytestCollectionWarning
+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
+
+
+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",
+ )
+ parser.addini(
+ "python_classes",
+ type="args",
+ default=["Test"],
+ 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",
+ )
+ 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 "
+ "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)
+
+
+def pytest_configure(config: Config) -> None:
+ config.addinivalue_line(
+ "markers",
+ "parametrize(argnames, argvalues): call a test function multiple "
+ "times passing in different arguments in turn. argvalues generally "
+ "needs to be a list of values if argnames specifies only one name "
+ "or a list of tuples of values if argnames specifies multiple names. "
+ "Example: @parametrize('arg1', [1,2]) would lead to two calls of the "
+ "decorated test function, one with arg1=1 and another with arg1=2."
+ "see https://docs.pytest.org/en/stable/how-to/parametrize.html for more info "
+ "and examples.",
+ )
+ config.addinivalue_line(
+ "markers",
+ "usefixtures(fixturename1, fixturename2, ...): mark tests as needing "
+ "all of the specified fixtures. see "
+ "https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures ",
+ )
+
+
+def async_warn_and_skip(nodeid: str) -> None:
+ msg = "async def functions are not natively supported and have been skipped.\n"
+ msg += (
+ "You need to install a suitable plugin for your async framework, for example:\n"
+ )
+ msg += " - anyio\n"
+ msg += " - pytest-asyncio\n"
+ msg += " - pytest-tornasync\n"
+ msg += " - pytest-trio\n"
+ msg += " - pytest-twisted"
+ warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid)))
+ skip(reason="async def function and no async plugin installed (see warnings)")
+
+
+@hookimpl(trylast=True)
+def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
+ testfunction = pyfuncitem.obj
+ if is_async_function(testfunction):
+ async_warn_and_skip(pyfuncitem.nodeid)
+ funcargs = pyfuncitem.funcargs
+ testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
+ result = testfunction(**testargs)
+ if hasattr(result, "__await__") or hasattr(result, "__aiter__"):
+ async_warn_and_skip(pyfuncitem.nodeid)
+ return True
+
+
+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"]
+ ):
+ return None
+ ihook = parent.session.gethookproxy(file_path)
+ module: Module = ihook.pytest_pycollect_makemodule(
+ module_path=file_path, parent=parent
+ )
+ return module
+ return None
+
+
+def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool:
+ """Return whether path matches any of the patterns in the list of globs given."""
+ return any(fnmatch_ex(pattern, path) for pattern in patterns)
+
+
+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
+
+
+@hookimpl(trylast=True)
+def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object):
+ # Nothing was collected elsewhere, let's do it here.
+ if safe_isclass(obj):
+ if collector.istestclass(obj, name):
+ return Class.from_parent(collector, name=name, obj=obj)
+ elif collector.istestfunction(obj, name):
+ # mock seems to store unbound methods (issue473), normalize it.
+ obj = getattr(obj, "__func__", obj)
+ # We need to try and unwrap the function if it's a functools.partial
+ # or a functools.wrapped.
+ # We mustn't if it's been wrapped with mock.patch (python 2 only).
+ if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))):
+ filename, lineno = getfslineno(obj)
+ warnings.warn_explicit(
+ message=PytestCollectionWarning(
+ "cannot collect %r because it is not a function." % name
+ ),
+ category=None,
+ filename=str(filename),
+ lineno=lineno + 1,
+ )
+ 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
+ )
+ res.add_marker(MARK_GEN.xfail(run=False, reason=reason))
+ res.warn(PytestCollectionWarning(reason))
+ else:
+ res = list(collector._genfunctions(name, obj))
+ return res
+
+
+class PyobjMixin(nodes.Node):
+ """this mix-in inherits from Node to carry over the typing information
+
+ as its intended to always mix in before a node
+ its position in the mro is unaffected"""
+
+ _ALLOW_MARKERS = True
+
+ @property
+ def module(self):
+ """Python module object this node was collected from (can be None)."""
+ node = self.getparent(Module)
+ return node.obj if node is not None else None
+
+ @property
+ def cls(self):
+ """Python class object this node was collected from (can be None)."""
+ node = self.getparent(Class)
+ return node.obj if node is not None else None
+
+ @property
+ def instance(self):
+ """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.
+ """
+ node = self.getparent(Function)
+ return getattr(node.obj, "__self__", None) if node is not None else None
+
+ @property
+ def obj(self):
+ """Underlying Python object."""
+ obj = getattr(self, "_obj", None)
+ if obj is None:
+ self._obj = obj = self._getobj()
+ # XXX evil hack
+ # used to avoid Function marker duplication
+ if self._ALLOW_MARKERS:
+ self.own_markers.extend(get_unpacked_marks(self.obj))
+ return obj
+
+ @obj.setter
+ def obj(self, value):
+ self._obj = value
+
+ def _getobj(self):
+ """Get the underlying Python object. May be overwritten by subclasses."""
+ # TODO: Improve the type of `parent` such that assert/ignore aren't needed.
+ assert self.parent is not None
+ obj = self.parent.obj # type: ignore[attr-defined]
+ return getattr(obj, self.name)
+
+ 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:
+ name = node.name
+ if isinstance(node, Module):
+ name = os.path.splitext(name)[0]
+ if stopatmodule:
+ if includemodule:
+ parts.append(name)
+ break
+ parts.append(name)
+ parts.reverse()
+ return ".".join(parts)
+
+ 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)
+ modpath = self.getmodpath()
+ assert isinstance(lineno, int)
+ return path, lineno, modpath
+
+
+# As an optimization, these builtin attribute names are pre-ignored when
+# iterating over an object during collection -- the pytest_pycollect_makeitem
+# hook is not called for them.
+# fmt: off
+class _EmptyClass: pass # noqa: E701
+IGNORED_ATTRIBUTES = frozenset.union( # noqa: E305
+ frozenset(),
+ # Module.
+ dir(types.ModuleType("empty_module")),
+ # Some extra module attributes the above doesn't catch.
+ {"__builtins__", "__file__", "__cached__"},
+ # Class.
+ dir(_EmptyClass),
+ # Instance.
+ dir(_EmptyClass()),
+)
+del _EmptyClass
+# fmt: on
+
+
+class PyCollector(PyobjMixin, nodes.Collector):
+ def funcnamefilter(self, name: str) -> bool:
+ return self._matches_prefix_or_glob_option("python_functions", name)
+
+ def isnosetest(self, obj: object) -> bool:
+ """Look for the __test__ attribute, which is applied by the
+ @nose.tools.istest decorator.
+ """
+ # We explicitly check for "is True" here to not mistakenly treat
+ # classes with a custom __getattr__ returning something truthy (like a
+ # function) as test classes.
+ return safe_getattr(obj, "__test__", False) is True
+
+ def classnamefilter(self, name: str) -> bool:
+ return self._matches_prefix_or_glob_option("python_classes", name)
+
+ 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.
+ obj = safe_getattr(obj, "__func__", False)
+ return callable(obj) and fixtures.getfixturemarker(obj) is None
+ else:
+ return False
+
+ def istestclass(self, obj: object, name: str) -> bool:
+ return self.classnamefilter(name) or self.isnosetest(obj)
+
+ def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
+ """Check if the given name matches the prefix or glob-pattern defined
+ in ini configuration."""
+ for option in self.config.getini(option_name):
+ if name.startswith(option):
+ return True
+ # Check that name looks like a glob-string before calling fnmatch
+ # because this is called for every name in each collected module,
+ # and fnmatch is somewhat expensive to call.
+ elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch(
+ name, option
+ ):
+ return True
+ return False
+
+ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
+ if not getattr(self.obj, "__test__", True):
+ return []
+
+ # Avoid random getattrs and peek in the __dict__ instead.
+ dicts = [getattr(self.obj, "__dict__", {})]
+ if isinstance(self.obj, type):
+ for basecls in self.obj.__mro__:
+ dicts.append(basecls.__dict__)
+
+ # In each class, nodes should be definition ordered. Since Python 3.6,
+ # __dict__ is definition ordered.
+ seen: Set[str] = set()
+ dict_values: List[List[Union[nodes.Item, nodes.Collector]]] = []
+ ihook = self.ihook
+ for dic in dicts:
+ values: List[Union[nodes.Item, nodes.Collector]] = []
+ # Note: seems like the dict can change during iteration -
+ # be careful not to remove the list() without consideration.
+ for name, obj in list(dic.items()):
+ if name in IGNORED_ATTRIBUTES:
+ continue
+ if name in seen:
+ continue
+ seen.add(name)
+ res = ihook.pytest_pycollect_makeitem(
+ collector=self, name=name, obj=obj
+ )
+ if res is None:
+ continue
+ elif isinstance(res, list):
+ values.extend(res)
+ else:
+ values.append(res)
+ dict_values.append(values)
+
+ # Between classes in the class hierarchy, reverse-MRO order -- nodes
+ # inherited from base classes should come before subclasses.
+ result = []
+ for values in reversed(dict_values):
+ result.extend(values)
+ return result
+
+ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
+ modulecol = self.getparent(Module)
+ assert modulecol is not None
+ module = modulecol.obj
+ clscol = self.getparent(Class)
+ cls = clscol and clscol.obj or None
+
+ definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
+ fixtureinfo = definition._fixtureinfo
+
+ # pytest_generate_tests impls call metafunc.parametrize() which fills
+ # metafunc._calls, the outcome of the hook.
+ metafunc = Metafunc(
+ definition=definition,
+ fixtureinfo=fixtureinfo,
+ config=self.config,
+ cls=cls,
+ module=module,
+ _ispytest=True,
+ )
+ methods = []
+ if hasattr(module, "pytest_generate_tests"):
+ methods.append(module.pytest_generate_tests)
+ if cls is not None and hasattr(cls, "pytest_generate_tests"):
+ methods.append(cls().pytest_generate_tests)
+ self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
+
+ 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.
+ fixtureinfo.prune_dependency_tree()
+
+ for callspec in metafunc._calls:
+ subname = f"{name}[{callspec.id}]"
+ yield Function.from_parent(
+ self,
+ name=subname,
+ callspec=callspec,
+ fixtureinfo=fixtureinfo,
+ keywords={callspec.id: True},
+ originalname=name,
+ )
+
+
+class Module(nodes.File, PyCollector):
+ """Collector for test classes and functions."""
+
+ def _getobj(self):
+ return self._importtestmodule()
+
+ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
+ self._inject_setup_module_fixture()
+ self._inject_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
+ 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]:
+ if setup_module is not None:
+ _call_with_optional_argument(setup_module, request.module)
+ yield
+ if teardown_module is not None:
+ _call_with_optional_argument(teardown_module, request.module)
+
+ self.obj.__pytest_setup_module = xunit_setup_module_fixture
+
+ def _inject_setup_function_fixture(self) -> None:
+ """Inject a hidden autouse, function scoped fixture into 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
+ other fixtures (#517).
+ """
+ setup_function = _get_first_non_fixture_func(self.obj, ("setup_function",))
+ teardown_function = _get_first_non_fixture_func(
+ self.obj, ("teardown_function",)
+ )
+ 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
+ if setup_function is not None:
+ _call_with_optional_argument(setup_function, request.function)
+ yield
+ if teardown_function is not None:
+ _call_with_optional_argument(teardown_function, request.function)
+
+ self.obj.__pytest_setup_function = xunit_setup_function_fixture
+
+ 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],
+ parent: nodes.Collector,
+ # NOTE: following args are unused:
+ config=None,
+ session=None,
+ nodeid=None,
+ path=Optional[Path],
+ ) -> None:
+ # NOTE: Could be just the following, but kept as-is for compat.
+ # nodes.FSCollector.__init__(self, fspath, parent=parent)
+ session = parent.session
+ nodes.FSCollector.__init__(
+ self,
+ fspath=fspath,
+ path=path,
+ parent=parent,
+ config=config,
+ session=session,
+ nodeid=nodeid,
+ )
+ self.name = self.path.parent.name
+
+ def setup(self) -> None:
+ # 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")
+ )
+ if setup_module is not None:
+ _call_with_optional_argument(setup_module, self.obj)
+
+ teardown_module = _get_first_non_fixture_func(
+ self.obj, ("tearDownModule", "teardown_module")
+ )
+ if teardown_module is not None:
+ func = partial(_call_with_optional_argument, teardown_module, self.obj)
+ 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
+
+ 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)
+
+
+def _call_with_optional_argument(func, arg) -> None:
+ """Call the given function with the given argument if func accepts one argument, otherwise
+ calls func without arguments."""
+ arg_count = func.__code__.co_argcount
+ if inspect.ismethod(func):
+ arg_count -= 1
+ if arg_count:
+ func(arg)
+ else:
+ func()
+
+
+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."""
+ for name in names:
+ meth: Optional[object] = getattr(obj, name, None)
+ if meth is not None and fixtures.getfixturemarker(meth) is None:
+ return meth
+ return None
+
+
+class Class(PyCollector):
+ """Collector for test methods."""
+
+ @classmethod
+ def from_parent(cls, parent, *, name, obj=None, **kw):
+ """The public constructor."""
+ return super().from_parent(name=name, parent=parent, **kw)
+
+ def newinstance(self):
+ return self.obj()
+
+ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
+ if not safe_getattr(self.obj, "__test__", True):
+ return []
+ if hasinit(self.obj):
+ 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)
+ )
+ )
+ return []
+ elif hasnew(self.obj):
+ 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)
+ )
+ )
+ return []
+
+ self._inject_setup_class_fixture()
+ self._inject_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
+ 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)
+ 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]:
+ if setup_class is not None:
+ func = getimfunc(setup_class)
+ _call_with_optional_argument(func, self.obj)
+ yield
+ if teardown_class is not None:
+ func = getimfunc(teardown_class)
+ _call_with_optional_argument(func, self.obj)
+
+ self.obj.__pytest_setup_class = xunit_setup_class_fixture
+
+ def _inject_setup_method_fixture(self) -> None:
+ """Inject a hidden 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
+ 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)
+ 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]:
+ method = request.function
+ if setup_method is not None:
+ func = getattr(self, setup_name)
+ _call_with_optional_argument(func, method)
+ yield
+ if teardown_method is not None:
+ func = getattr(self, 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}")
+
+
+def hasinit(obj: object) -> bool:
+ init: object = getattr(obj, "__init__", None)
+ if init:
+ return init != object.__init__
+ return False
+
+
+def hasnew(obj: object) -> bool:
+ new: object = getattr(obj, "__new__", None)
+ if new:
+ return new != object.__new__
+ return False
+
+
+@final
+@attr.s(frozen=True, slots=True, auto_attribs=True)
+class CallSpec2:
+ """A planned parameterized invocation of a test function.
+
+ Calculated during collection for a given test function's Metafunc.
+ Once collection is over, each callspec is turned into a single Item
+ 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 index.
+ indices: Dict[str, int] = attr.Factory(dict)
+ # Used for sorting parametrized resources.
+ _arg2scope: Dict[str, Scope] = attr.Factory(dict)
+ # Parts which will be added to the item's name in `[..]` separated by "-".
+ _idlist: List[str] = attr.Factory(list)
+ # Marks which will be applied to the item.
+ marks: List[Mark] = attr.Factory(list)
+
+ def setmulti(
+ self,
+ *,
+ valtypes: Mapping[str, "Literal['params', 'funcargs']"],
+ argnames: Iterable[str],
+ valset: Iterable[object],
+ id: str,
+ marks: Iterable[Union[Mark, MarkDecorator]],
+ scope: Scope,
+ param_index: int,
+ ) -> "CallSpec2":
+ funcargs = self.funcargs.copy()
+ params = self.params.copy()
+ indices = self.indices.copy()
+ arg2scope = self._arg2scope.copy()
+ 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)
+ indices[arg] = param_index
+ arg2scope[arg] = scope
+ return CallSpec2(
+ funcargs=funcargs,
+ params=params,
+ arg2scope=arg2scope,
+ indices=indices,
+ idlist=[*self._idlist, id],
+ marks=[*self.marks, *normalize_mark_list(marks)],
+ )
+
+ def getparam(self, name: str) -> object:
+ try:
+ return self.params[name]
+ except KeyError as e:
+ raise ValueError(name) from e
+
+ @property
+ def id(self) -> str:
+ return "-".join(self._idlist)
+
+
+@final
+class Metafunc:
+ """Objects passed to the :hook:`pytest_generate_tests` hook.
+
+ They help to inspect a test function and to generate tests according to
+ test configuration or values specified in the class or module where a
+ test function is defined.
+ """
+
+ def __init__(
+ self,
+ definition: "FunctionDefinition",
+ fixtureinfo: fixtures.FuncFixtureInfo,
+ config: Config,
+ cls=None,
+ module=None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+
+ #: Access to the underlying :class:`_pytest.python.FunctionDefinition`.
+ self.definition = definition
+
+ #: Access to the :class:`pytest.Config` object for the test session.
+ self.config = config
+
+ #: The module object where the test function is defined in.
+ self.module = module
+
+ #: Underlying Python test function.
+ self.function = definition.obj
+
+ #: Set of fixture names required by the test function.
+ self.fixturenames = fixtureinfo.names_closure
+
+ #: Class object where the test function is defined in or ``None``.
+ self.cls = cls
+
+ self._arg2fixturedefs = fixtureinfo.name2fixturedefs
+
+ # Result of parametrize().
+ self._calls: List[CallSpec2] = []
+
+ def parametrize(
+ self,
+ argnames: Union[str, List[str], Tuple[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]],
+ ]
+ ] = None,
+ scope: "Optional[_ScopeName]" = None,
+ *,
+ _param_mark: Optional[Mark] = None,
+ ) -> None:
+ """Add new invocations to the underlying test function using the list
+ of argvalues for the given argnames. Parametrization is performed
+ 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.
+
+ ::
+
+ unparametrized: t
+ parametrize ["x", "y"]: t[x], t[y]
+ parametrize [1, 2]: t[x-1], t[x-2], t[y-1], t[y-2]
+
+ :param argnames:
+ A comma-separated string denoting one or more argument names, or
+ a list/tuple of argument strings.
+
+ :param argvalues:
+ The list of argvalues determines how often a test is invoked with
+ different argument values.
+
+ If only one argname was specified argvalues is a list of values.
+ If N argnames were specified, argvalues must be a list of
+ N-tuples, where each tuple-element specifies a value for its
+ respective argname.
+
+ :param indirect:
+ A list of arguments' names (subset of argnames) or a boolean.
+ If True the list contains all names from the argnames. Each
+ argvalue corresponding to an argname in this list will
+ be passed as request.param to its respective argname fixture
+ function so that it can perform more expensive setups during the
+ setup phase of a test rather than at collection time.
+
+ :param ids:
+ Sequence of (or generator for) ids for ``argvalues``,
+ or a callable to return part of the id for each argvalue.
+
+ With sequences (and generators like ``itertools.count()``) the
+ returned ids should be of type ``string``, ``int``, ``float``,
+ ``bool``, or ``None``.
+ They are mapped to the corresponding index in ``argvalues``.
+ ``None`` means to use the auto-generated id.
+
+ If it is a callable it will be called for each entry in
+ ``argvalues``, and the return value is used as part of the
+ auto-generated id for the whole set (where parts are joined with
+ dashes ("-")).
+ This is useful to provide more specific ids for certain items, e.g.
+ dates. Returning ``None`` will use an auto-generated id.
+
+ If no ids are provided they will be generated automatically from
+ the argvalues.
+
+ :param scope:
+ If specified it denotes the scope of the parameters.
+ The scope is used for grouping tests by parameter instances.
+ 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,
+ argvalues,
+ self.function,
+ self.config,
+ nodeid=self.definition.nodeid,
+ )
+ del argvalues
+
+ if "request" in argnames:
+ fail(
+ "'request' is a reserved name and cannot be used in @pytest.mark.parametrize",
+ pytrace=False,
+ )
+
+ if scope is not None:
+ scope_ = Scope.from_user(
+ scope, descr=f"parametrize() call in {self.function.__name__}"
+ )
+ else:
+ scope_ = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)
+
+ 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
+ )
+
+ # 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)
+
+ # 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)):
+ newcallspec = callspec.setmulti(
+ valtypes=arg_values_types,
+ argnames=argnames,
+ valset=param_set.values,
+ id=param_id,
+ marks=param_set.marks,
+ scope=scope_,
+ param_index=param_index,
+ )
+ newcalls.append(newcallspec)
+ self._calls = newcalls
+
+ def _resolve_arg_ids(
+ self,
+ argnames: Sequence[str],
+ ids: Optional[
+ Union[
+ Iterable[Union[None, str, float, int, bool]],
+ Callable[[Any], Optional[object]],
+ ]
+ ],
+ parameters: Sequence[ParameterSet],
+ nodeid: str,
+ ) -> List[str]:
+ """Resolve the actual ids for the given argnames, based on the ``ids`` parameter given
+ to ``parametrize``.
+
+ :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.
+ """
+ if ids is None:
+ idfn = None
+ ids_ = None
+ elif callable(ids):
+ idfn = ids
+ ids_ = None
+ else:
+ idfn = None
+ ids_ = self._validate_ids(ids, parameters, self.function.__name__)
+ return idmaker(argnames, parameters, idfn, ids_, self.config, nodeid=nodeid)
+
+ def _validate_ids(
+ self,
+ ids: Iterable[Union[None, str, float, int, bool]],
+ parameters: Sequence[ParameterSet],
+ func_name: str,
+ ) -> List[Union[None, str]]:
+ try:
+ num_ids = len(ids) # type: ignore[arg-type]
+ except TypeError:
+ try:
+ iter(ids)
+ except TypeError as e:
+ raise TypeError("ids must be a callable or an iterable") from e
+ num_ids = len(parameters)
+
+ # num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849
+ if num_ids != len(parameters) 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
+
+ def _resolve_arg_value_types(
+ 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.
+
+ :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.
+ """
+ if isinstance(indirect, bool):
+ valtypes: Dict[str, Literal["params", "funcargs"]] = dict.fromkeys(
+ argnames, "params" if indirect else "funcargs"
+ )
+ elif isinstance(indirect, Sequence):
+ valtypes = dict.fromkeys(argnames, "funcargs")
+ for arg in indirect:
+ if arg not in argnames:
+ fail(
+ "In {}: indirect fixture '{}' doesn't exist".format(
+ self.function.__name__, arg
+ ),
+ pytrace=False,
+ )
+ valtypes[arg] = "params"
+ else:
+ fail(
+ "In {func}: expected Sequence or boolean for indirect, got {type}".format(
+ type=type(indirect).__name__, func=self.function.__name__
+ ),
+ pytrace=False,
+ )
+ return valtypes
+
+ def _validate_if_using_arg_names(
+ self,
+ argnames: Sequence[str],
+ indirect: Union[bool, Sequence[str]],
+ ) -> None:
+ """Check if all argnames are being used, by default values, or directly/indirectly.
+
+ :param List[str] argnames: List of argument names passed to ``parametrize()``.
+ :param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
+ :raises ValueError: If validation fails.
+ """
+ default_arg_names = set(get_default_arg_names(self.function))
+ func_name = self.function.__name__
+ for arg in argnames:
+ 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
+ ),
+ pytrace=False,
+ )
+ else:
+ if isinstance(indirect, Sequence):
+ name = "fixture" if arg in indirect else "argument"
+ else:
+ name = "fixture" if indirect else "argument"
+ fail(
+ f"In {func_name}: function uses no {name} '{arg}'",
+ pytrace=False,
+ )
+
+
+def _find_parametrized_scope(
+ argnames: Sequence[str],
+ arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]],
+ indirect: Union[bool, Sequence[str]],
+) -> Scope:
+ """Find the most appropriate scope for a parametrized call based on its arguments.
+
+ When there's at least one direct argument, always use "function" scope.
+
+ When a test function is parametrized and all its arguments are indirect
+ (e.g. fixtures), return the most narrow scope based on the fixtures used.
+
+ Related to issue #1832, based on code posted by @Kingdread.
+ """
+ if isinstance(indirect, Sequence):
+ all_arguments_are_fixtures = len(indirect) == len(argnames)
+ else:
+ all_arguments_are_fixtures = bool(indirect)
+
+ if all_arguments_are_fixtures:
+ fixturedefs = arg2fixturedefs or {}
+ used_scopes = [
+ fixturedef[0]._scope
+ for name, fixturedef in fixturedefs.items()
+ if name in argnames
+ ]
+ # Takes the most narrow scope from used fixtures.
+ return min(used_scopes, default=Scope.Function)
+
+ return Scope.Function
+
+
+def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -> str:
+ if config is None:
+ escape_option = False
+ else:
+ escape_option = config.getini(
+ "disable_test_id_escaping_and_forfeit_all_rights_to_community_support"
+ )
+ # TODO: If escaping is turned off and the user passes bytes,
+ # will return a bytes. For now we ignore this but the
+ # code *probably* doesn't handle this case.
+ 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.
+
+ :param name:
+ The full function name, including any decorations like those
+ added by parametrization (``my_func[my_param]``).
+ :param parent:
+ The parent Node.
+ :param config:
+ The pytest Config object.
+ :param callspec:
+ If given, this is 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,
+ otherwise the callobj will be obtained from ``parent`` using ``originalname``.
+ :param keywords:
+ Keywords bound to the function object for "-k" matching.
+ :param session:
+ The pytest Session object.
+ :param fixtureinfo:
+ Fixture information already resolved at this fixture node..
+ :param originalname:
+ The attribute name to use for accessing the underlying function object.
+ Defaults to ``name``. Set this if name is different from the original name,
+ for example when it contains decorations like those added by parametrization
+ (``my_func[my_param]``).
+ """
+
+ # Disable since functions handle it themselves.
+ _ALLOW_MARKERS = False
+
+ def __init__(
+ self,
+ name: str,
+ parent,
+ config: Optional[Config] = None,
+ callspec: Optional[CallSpec2] = None,
+ callobj=NOTSET,
+ keywords=None,
+ session: Optional[Session] = None,
+ fixtureinfo: Optional[FuncFixtureInfo] = None,
+ originalname: Optional[str] = None,
+ ) -> None:
+ super().__init__(name, parent, config=config, session=session)
+
+ if callobj is not NOTSET:
+ self.obj = callobj
+
+ #: Original function name, without any decorations (for example
+ #: parametrization adds a ``"[...]"`` suffix to function names), used to access
+ #: the underlying function object from ``parent`` (in case ``callobj`` is not given
+ #: explicitly).
+ #:
+ #: .. versionadded:: 3.0
+ self.originalname = originalname or name
+
+ # 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)
+
+ # 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
+ }
+ )
+
+ if fixtureinfo is None:
+ fixtureinfo = self.session._fixturemanager.getfixtureinfo(
+ self, self.obj, self.cls, funcargs=True
+ )
+ self._fixtureinfo: FuncFixtureInfo = fixtureinfo
+ self.fixturenames = fixtureinfo.names_closure
+ self._initrequest()
+
+ @classmethod
+ def from_parent(cls, parent, **kw): # todo: determine sound type limitations
+ """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)
+
+ @property
+ def function(self):
+ """Underlying python 'function' object."""
+ return getimfunc(self.obj)
+
+ def _getobj(self):
+ assert self.parent is not None
+ if isinstance(self.parent, Class):
+ # Each Function gets a fresh class instance.
+ parent_obj = self.parent.newinstance()
+ else:
+ parent_obj = self.parent.obj # type: ignore[attr-defined]
+ return getattr(parent_obj, self.originalname)
+
+ @property
+ def _pyfuncitem(self):
+ """(compatonly) for code expecting pytest-2.2 style request objects."""
+ return self
+
+ def runtest(self) -> None:
+ """Execute the underlying test function."""
+ self.ihook.pytest_pyfunc_call(pyfuncitem=self)
+
+ def setup(self) -> None:
+ self._request._fillfixtures()
+
+ def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
+ 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
+ traceback = excinfo.traceback
+ ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
+ if ntraceback == traceback:
+ ntraceback = ntraceback.cut(path=path)
+ if ntraceback == traceback:
+ ntraceback = ntraceback.filter(filter_traceback)
+ if not ntraceback:
+ ntraceback = traceback
+
+ 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")
+
+ # TODO: Type ignored -- breaks Liskov Substitution.
+ def repr_failure( # type: ignore[override]
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ ) -> Union[str, TerminalRepr]:
+ style = self.config.getoption("tbstyle", "auto")
+ if style == "auto":
+ style = "long"
+ return self._repr_failure_py(excinfo, style=style)
+
+
+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``.
+ """
+
+ def runtest(self) -> None:
+ raise RuntimeError("function definitions are not supposed to be run as tests")
+
+ setup = runtest
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/python_api.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/python_api.py
new file mode 100644
index 0000000000..cb72fde1e1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/python_api.py
@@ -0,0 +1,961 @@
+import math
+import pprint
+from collections.abc import Sized
+from decimal import Decimal
+from numbers import Complex
+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 List
+from typing import Mapping
+from typing import Optional
+from typing import overload
+from typing import Pattern
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+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
+ )
+ )
+
+
+def _compare_approx(
+ full_object: object,
+ message_data: Sequence[Tuple[str, str, str]],
+ number_of_elements: int,
+ different_ids: Sequence[object],
+ max_abs_diff: float,
+ max_rel_diff: float,
+) -> List[str]:
+ message_list = list(message_data)
+ message_list.insert(0, ("Index", "Obtained", "Expected"))
+ max_sizes = [0, 0, 0]
+ for index, obtained, expected in message_list:
+ max_sizes[0] = max(max_sizes[0], len(index))
+ max_sizes[1] = max(max_sizes[1], len(obtained))
+ max_sizes[2] = max(max_sizes[2], len(expected))
+ explanation = [
+ f"comparison failed. Mismatched elements: {len(different_ids)} / {number_of_elements}:",
+ f"Max absolute difference: {max_abs_diff}",
+ f"Max relative difference: {max_rel_diff}",
+ ] + [
+ f"{indexes:<{max_sizes[0]}} | {obtained:<{max_sizes[1]}} | {expected:<{max_sizes[2]}}"
+ for indexes, obtained, expected in message_list
+ ]
+ return explanation
+
+
+# builtin pytest.approx helper
+
+
+class ApproxBase:
+ """Provide shared utilities for making approximate comparisons between
+ numbers or sequences of numbers."""
+
+ # Tell numpy to use our `__eq__` operator instead of its.
+ __array_ufunc__ = None
+ __array_priority__ = 100
+
+ def __init__(self, expected, rel=None, abs=None, nan_ok: bool = False) -> None:
+ __tracebackhide__ = True
+ self.expected = expected
+ self.abs = abs
+ self.rel = rel
+ self.nan_ok = nan_ok
+ self._check_type()
+
+ def __repr__(self) -> str:
+ raise NotImplementedError
+
+ def _repr_compare(self, other_side: Any) -> List[str]:
+ return [
+ "comparison failed",
+ f"Obtained: {other_side}",
+ f"Expected: {self}",
+ ]
+
+ def __eq__(self, actual) -> bool:
+ return all(
+ a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual)
+ )
+
+ def __bool__(self):
+ __tracebackhide__ = True
+ raise AssertionError(
+ "approx() is not supported in a boolean context.\nDid you mean: `assert a == approx(b)`?"
+ )
+
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
+ __hash__ = None # type: ignore
+
+ def __ne__(self, actual) -> bool:
+ return not (actual == self)
+
+ def _approx_scalar(self, x) -> "ApproxScalar":
+ if isinstance(x, Decimal):
+ return ApproxDecimal(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
+ return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
+
+ def _yield_comparisons(self, actual):
+ """Yield all the pairs of numbers to be compared.
+
+ This is used to implement the `__eq__` method.
+ """
+ raise NotImplementedError
+
+ def _check_type(self) -> None:
+ """Raise a TypeError if the expected value is not a valid type."""
+ # This is only a concern if the expected value is a sequence. In every
+ # other case, the approx() function ensures that the expected value has
+ # 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]
+ else:
+ return f(x)
+
+
+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())
+ return f"approx({list_scalars!r})"
+
+ def _repr_compare(self, other_side: "ndarray") -> List[str]:
+ import itertools
+ import math
+
+ def get_value_from_nested_list(
+ nested_list: List[Any], nd_index: Tuple[Any, ...]
+ ) -> Any:
+ """
+ Helper function to get the value out of a nested list, given an n-dimensional index.
+ This mimics numpy's indexing, but for raw nested python lists.
+ """
+ value: Any = nested_list
+ for i in nd_index:
+ value = value[i]
+ return value
+
+ np_array_shape = self.expected.shape
+ approx_side_as_list = _recursive_list_map(
+ self._approx_scalar, self.expected.tolist()
+ )
+
+ if np_array_shape != other_side.shape:
+ return [
+ "Impossible to compare arrays with different shapes.",
+ f"Shapes: {np_array_shape} and {other_side.shape}",
+ ]
+
+ number_of_elements = self.expected.size
+ max_abs_diff = -math.inf
+ 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)
+ if approx_value != other_value:
+ 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 = math.inf
+ else:
+ max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value))
+ different_ids.append(index)
+
+ message_data = [
+ (
+ str(index),
+ str(get_value_from_nested_list(other_side, index)),
+ str(get_value_from_nested_list(approx_side_as_list, index)),
+ )
+ for index in different_ids
+ ]
+ return _compare_approx(
+ self.expected,
+ message_data,
+ number_of_elements,
+ different_ids,
+ max_abs_diff,
+ max_rel_diff,
+ )
+
+ def __eq__(self, actual) -> bool:
+ import numpy as np
+
+ # self.expected is supposed to always be an array here.
+
+ if not np.isscalar(actual):
+ try:
+ actual = np.asarray(actual)
+ except Exception as e:
+ raise TypeError(f"cannot compare '{actual}' to numpy.ndarray") from e
+
+ if not np.isscalar(actual) and actual.shape != self.expected.shape:
+ return False
+
+ return super().__eq__(actual)
+
+ def _yield_comparisons(self, actual):
+ import numpy as np
+
+ # `actual` can either be a numpy array or a scalar, it is treated in
+ # `__eq__` before being passed to `ApproxBase.__eq__`, which is the
+ # only method that calls this one.
+
+ if np.isscalar(actual):
+ for i in np.ndindex(self.expected.shape):
+ yield actual, self.expected[i].item()
+ else:
+ for i in np.ndindex(self.expected.shape):
+ yield actual[i].item(), self.expected[i].item()
+
+
+class ApproxMapping(ApproxBase):
+ """Perform approximate comparisons where the expected value is a mapping
+ 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()}
+ )
+
+ def _repr_compare(self, other_side: Mapping[object, float]) -> List[str]:
+ import math
+
+ approx_side_as_map = {
+ k: self._approx_scalar(v) for k, v in self.expected.items()
+ }
+
+ number_of_elements = len(approx_side_as_map)
+ max_abs_diff = -math.inf
+ max_rel_diff = -math.inf
+ different_ids = []
+ for (approx_key, approx_value), other_value in zip(
+ 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),
+ )
+ different_ids.append(approx_key)
+
+ message_data = [
+ (str(key), str(other_side[key]), str(approx_side_as_map[key]))
+ for key in different_ids
+ ]
+
+ return _compare_approx(
+ self.expected,
+ message_data,
+ number_of_elements,
+ different_ids,
+ max_abs_diff,
+ max_rel_diff,
+ )
+
+ def __eq__(self, actual) -> bool:
+ try:
+ if set(actual.keys()) != set(self.expected.keys()):
+ return False
+ except AttributeError:
+ return False
+
+ return super().__eq__(actual)
+
+ def _yield_comparisons(self, actual):
+ for k in self.expected.keys():
+ yield actual[k], self.expected[k]
+
+ def _check_type(self) -> None:
+ __tracebackhide__ = True
+ for key, value in self.expected.items():
+ if isinstance(value, type(self.expected)):
+ msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}"
+ raise TypeError(msg.format(key, value, pprint.pformat(self.expected)))
+
+
+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):
+ seq_type = list
+ return "approx({!r})".format(
+ seq_type(self._approx_scalar(x) for x in self.expected)
+ )
+
+ def _repr_compare(self, other_side: Sequence[float]) -> List[str]:
+ import math
+ import numpy as np
+
+ if len(self.expected) != len(other_side):
+ return [
+ "Impossible to compare lists with different sizes.",
+ f"Lengths: {len(self.expected)} and {len(other_side)}",
+ ]
+
+ approx_side_as_map = _recursive_list_map(self._approx_scalar, self.expected)
+
+ number_of_elements = len(approx_side_as_map)
+ max_abs_diff = -math.inf
+ max_rel_diff = -math.inf
+ different_ids = []
+ for i, (approx_value, other_value) in enumerate(
+ zip(approx_side_as_map, other_side)
+ ):
+ if approx_value != other_value:
+ 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
+ else:
+ max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value))
+ different_ids.append(i)
+
+ message_data = [
+ (str(i), str(other_side[i]), str(approx_side_as_map[i]))
+ for i in different_ids
+ ]
+
+ return _compare_approx(
+ self.expected,
+ message_data,
+ number_of_elements,
+ different_ids,
+ max_abs_diff,
+ max_rel_diff,
+ )
+
+ def __eq__(self, actual) -> bool:
+ try:
+ if len(actual) != len(self.expected):
+ return False
+ except TypeError:
+ return False
+ return super().__eq__(actual)
+
+ def _yield_comparisons(self, actual):
+ return zip(actual, self.expected)
+
+ def _check_type(self) -> None:
+ __tracebackhide__ = True
+ for index, x in enumerate(self.expected):
+ if isinstance(x, type(self.expected)):
+ msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}"
+ raise TypeError(msg.format(x, index, pprint.pformat(self.expected)))
+
+
+class ApproxScalar(ApproxBase):
+ """Perform approximate comparisons where the expected value is a single number."""
+
+ # Using Real should be better than this Union, but not possible yet:
+ # https://github.com/python/typeshed/pull/3108
+ DEFAULT_ABSOLUTE_TOLERANCE: Union[float, Decimal] = 1e-12
+ DEFAULT_RELATIVE_TOLERANCE: Union[float, Decimal] = 1e-6
+
+ def __repr__(self) -> str:
+ """Return a string communicating both the expected value and the
+ tolerance for the comparison being made.
+
+ For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``.
+ """
+ # Don't show a tolerance for values that aren't compared using
+ # 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]
+ ):
+ return str(self.expected)
+
+ # If a sensible tolerance can't be calculated, self.tolerance will
+ # raise a ValueError. In this case, display '???'.
+ try:
+ vetted_tolerance = f"{self.tolerance:.1e}"
+ if (
+ isinstance(self.expected, Complex)
+ and self.expected.imag
+ and not math.isinf(self.tolerance)
+ ):
+ vetted_tolerance += " ∠ ±180°"
+ except ValueError:
+ vetted_tolerance = "???"
+
+ return f"{self.expected} ± {vetted_tolerance}"
+
+ def __eq__(self, actual) -> bool:
+ """Return whether the given value is equal to the expected value
+ within the pre-specified tolerance."""
+ asarray = _as_numpy_array(actual)
+ if asarray is not None:
+ # Call ``__eq__()`` manually to prevent infinite-recursion with
+ # numpy<1.13. See #3748.
+ return all(self.__eq__(a) for a in asarray.flat)
+
+ # Short-circuit exact equality.
+ if actual == self.expected:
+ return True
+
+ # If either type is non-numeric, fall back to strict equality.
+ # NB: we need Complex, rather than just Number, to ensure that __abs__,
+ # __sub__, and __float__ are defined.
+ if not (
+ isinstance(self.expected, (Complex, Decimal))
+ and isinstance(actual, (Complex, Decimal))
+ ):
+ return False
+
+ # 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]
+
+ # Infinity shouldn't be approximately equal to anything but itself, but
+ # if there's a relative tolerance, it will be infinite and infinity
+ # will seem approximately equal to everything. The equal-to-itself
+ # 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]
+ return False
+
+ # Return true if the two numbers are within the tolerance.
+ result: bool = abs(self.expected - actual) <= self.tolerance
+ return result
+
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
+ __hash__ = None # type: ignore
+
+ @property
+ def tolerance(self):
+ """Return the tolerance for the comparison.
+
+ This could be either an absolute tolerance or a relative tolerance,
+ depending on what the user specified or which would be larger.
+ """
+
+ def set_default(x, default):
+ return x if x is not None else default
+
+ # Figure out what the absolute tolerance should be. ``self.abs`` is
+ # either None or a value specified by the user.
+ absolute_tolerance = set_default(self.abs, self.DEFAULT_ABSOLUTE_TOLERANCE)
+
+ if absolute_tolerance < 0:
+ raise ValueError(
+ f"absolute tolerance can't be negative: {absolute_tolerance}"
+ )
+ if math.isnan(absolute_tolerance):
+ raise ValueError("absolute tolerance can't be NaN.")
+
+ # If the user specified an absolute tolerance but not a relative one,
+ # just return the absolute tolerance.
+ if self.rel is None:
+ if self.abs is not None:
+ return absolute_tolerance
+
+ # Figure out what the relative tolerance should be. ``self.rel`` is
+ # either None or a value specified by the user. This is done after
+ # we've made sure the user didn't ask for an absolute tolerance only,
+ # because we don't want to raise errors about the relative tolerance if
+ # we aren't even going to use it.
+ relative_tolerance = set_default(
+ self.rel, self.DEFAULT_RELATIVE_TOLERANCE
+ ) * abs(self.expected)
+
+ if relative_tolerance < 0:
+ raise ValueError(
+ f"relative tolerance can't be negative: {relative_tolerance}"
+ )
+ if math.isnan(relative_tolerance):
+ raise ValueError("relative tolerance can't be NaN.")
+
+ # Return the larger of the relative and absolute tolerances.
+ return max(relative_tolerance, absolute_tolerance)
+
+
+class ApproxDecimal(ApproxScalar):
+ """Perform approximate comparisons where the expected value is a Decimal."""
+
+ DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12")
+ DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6")
+
+
+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
+ within some tolerance.
+
+ Due to the :std:doc:`tutorial/floatingpoint`, numbers that we
+ would intuitively expect to be equal are not always so::
+
+ >>> 0.1 + 0.2 == 0.3
+ False
+
+ This problem is commonly encountered when writing tests, e.g. when making
+ sure that floating-point values are what you expect them to be. One way to
+ deal with this problem is to assert that two floating-point numbers are
+ equal to within some appropriate tolerance::
+
+ >>> abs((0.1 + 0.2) - 0.3) < 1e-6
+ True
+
+ However, comparisons like this are tedious to write and difficult to
+ understand. Furthermore, absolute comparisons like the one above are
+ usually discouraged because there's no tolerance that works well for all
+ situations. ``1e-6`` is good for numbers around ``1``, but too small for
+ very big numbers and too big for very small ones. It's better to express
+ the tolerance as a fraction of the expected value, but relative comparisons
+ like that are even more difficult to write correctly and concisely.
+
+ The ``approx`` class performs floating-point comparisons using a syntax
+ that's as intuitive as possible::
+
+ >>> from pytest import approx
+ >>> 0.1 + 0.2 == approx(0.3)
+ True
+
+ The same syntax also works for 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
+ >>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == approx(np.array([0.3, 0.6])) # doctest: +SKIP
+ True
+
+ And for a ``numpy`` array against a scalar::
+
+ >>> import numpy as np # doctest: +SKIP
+ >>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP
+ True
+
+ 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
+ ``0.0``, because nothing but ``0.0`` itself is relatively close to ``0.0``.
+ To handle this case less surprisingly, ``approx`` also considers numbers
+ within an absolute tolerance of ``1e-12`` of its expected value to be
+ equal. Infinity and NaN are special cases. Infinity is only considered
+ equal to itself, regardless of the relative tolerance. NaN is not
+ considered equal to anything by default, but you can make it be equal to
+ itself by setting the ``nan_ok`` argument to True. (This is meant to
+ facilitate comparing arrays that use NaN to mean "no data".)
+
+ Both the relative and absolute tolerances can be changed by passing
+ arguments to the ``approx`` constructor::
+
+ >>> 1.0001 == approx(1)
+ False
+ >>> 1.0001 == approx(1, rel=1e-3)
+ True
+ >>> 1.0001 == approx(1, abs=1e-3)
+ True
+
+ If you specify ``abs`` but not ``rel``, the comparison will not consider
+ the relative tolerance at all. In other words, two numbers that are within
+ the default relative tolerance of ``1e-6`` will still be considered unequal
+ if they exceed the specified absolute tolerance. If you specify both
+ ``abs`` and ``rel``, the numbers will be considered equal if either
+ tolerance is met::
+
+ >>> 1 + 1e-8 == approx(1)
+ True
+ >>> 1 + 1e-8 == approx(1, abs=1e-12)
+ False
+ >>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12)
+ True
+
+ You can also use ``approx`` to compare nonnumeric types, or dicts and
+ sequences containing nonnumeric types, in which case it falls back to
+ strict equality. This can be useful for comparing dicts and sequences that
+ can contain optional values::
+
+ >>> {"required": 1.0000005, "optional": None} == approx({"required": 1, "optional": None})
+ True
+ >>> [None, 1.0000005] == approx([None,1])
+ True
+ >>> ["foo", 1.0000005] == approx([None,1])
+ False
+
+ If you're thinking about using ``approx``, then you might want to know how
+ it compares to other good ways of comparing floating-point numbers. All of
+ these algorithms are based on relative and absolute tolerances and should
+ agree for the most part, but they do have meaningful differences:
+
+ - ``math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)``: True if the relative
+ tolerance is met w.r.t. either ``a`` or ``b`` or if the absolute
+ tolerance is met. Because the relative tolerance is calculated w.r.t.
+ both ``a`` and ``b``, this test is symmetric (i.e. neither ``a`` nor
+ ``b`` is a "reference value"). You have to specify an absolute tolerance
+ if you want to compare to ``0.0`` because there is no tolerance by
+ default. More information: :py:func:`math.isclose`.
+
+ - ``numpy.isclose(a, b, rtol=1e-5, atol=1e-8)``: True if the difference
+ between ``a`` and ``b`` is less that the sum of the relative tolerance
+ w.r.t. ``b`` and the absolute tolerance. Because the relative tolerance
+ is only calculated w.r.t. ``b``, this test is asymmetric and you can
+ think of ``b`` as the reference value. Support for comparing sequences
+ is provided by :py:func:`numpy.allclose`. More information:
+ :std:doc:`numpy:reference/generated/numpy.isclose`.
+
+ - ``unittest.TestCase.assertAlmostEqual(a, b)``: True if ``a`` and ``b``
+ are within an absolute tolerance of ``1e-7``. No relative tolerance is
+ considered , so this function is not appropriate for very large or very
+ small numbers. Also, it's only available in subclasses of ``unittest.TestCase``
+ and it's ugly because it doesn't follow PEP8. More information:
+ :py:meth:`unittest.TestCase.assertAlmostEqual`.
+
+ - ``a == pytest.approx(b, rel=1e-6, abs=1e-12)``: True if the relative
+ tolerance is met w.r.t. ``b`` or if the absolute tolerance is met.
+ Because the relative tolerance is only calculated w.r.t. ``b``, this test
+ is asymmetric and you can think of ``b`` as the reference value. In the
+ special case that you explicitly specify an absolute tolerance but not a
+ relative tolerance, only the absolute tolerance is considered.
+
+ .. note::
+
+ ``approx`` can handle numpy arrays, but we recommend the
+ specialised test helpers in :std:doc:`numpy:reference/routines.testing`
+ if you need support for comparisons, NaNs, or ULP-based tolerances.
+
+ .. warning::
+
+ .. versionchanged:: 3.2
+
+ In order to avoid inconsistent behavior, :py:exc:`TypeError` is
+ raised for ``>``, ``>=``, ``<`` and ``<=`` comparisons.
+ The example below illustrates the problem::
+
+ assert approx(0.1) > 0.1 + 1e-10 # calls approx(0.1).__gt__(0.1 + 1e-10)
+ assert 0.1 + 1e-10 > approx(0.1) # calls approx(0.1).__lt__(0.1 + 1e-10)
+
+ In the second example one expects ``approx(0.1).__le__(0.1 + 1e-10)``
+ to be called. But instead, ``approx(0.1).__lt__(0.1 + 1e-10)`` is used to
+ comparison. This is because the call hierarchy of rich comparisons
+ follows a fixed behavior. More information: :py:meth:`object.__ge__`
+
+ .. versionchanged:: 3.7.1
+ ``approx`` raises ``TypeError`` when it encounters a dict value or
+ sequence element of nonnumeric type.
+
+ .. versionchanged:: 6.1.0
+ ``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).
+ #
+ # The primary responsibility of these classes is to implement ``__eq__()``
+ # and ``__repr__()``. The former is used to actually check if some
+ # "actual" value is equivalent to the given expected value within the
+ # allowed tolerance. The latter is used to show the user the expected
+ # value and tolerance, in the case that a test failed.
+ #
+ # The actual logic for making approximate comparisons can be found in
+ # ApproxScalar, which is used to compare individual numbers. All of the
+ # other Approx classes eventually delegate to this class. The ApproxBase
+ # class provides some convenient methods and overloads, but isn't really
+ # essential.
+
+ __tracebackhide__ = True
+
+ if isinstance(expected, Decimal):
+ cls: Type[ApproxBase] = ApproxDecimal
+ elif isinstance(expected, Mapping):
+ cls = ApproxMapping
+ elif _is_numpy_array(expected):
+ expected = _as_numpy_array(expected)
+ cls = ApproxNumpy
+ elif (
+ isinstance(expected, Iterable)
+ and isinstance(expected, Sized)
+ # Type ignored because the error is wrong -- not unreachable.
+ and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
+ ):
+ cls = ApproxSequencelike
+ else:
+ cls = ApproxScalar
+
+ return cls(expected, rel, abs, nan_ok)
+
+
+def _is_numpy_array(obj: object) -> bool:
+ """
+ Return true if the given object is implicitly convertible to ndarray,
+ and numpy is already imported.
+ """
+ return _as_numpy_array(obj) is not None
+
+
+def _as_numpy_array(obj: object) -> Optional["ndarray"]:
+ """
+ Return an ndarray if the given object is implicitly convertible to ndarray,
+ and numpy is already imported, otherwise None.
+ """
+ import sys
+
+ np: Any = sys.modules.get("numpy")
+ if np is not None:
+ # avoid infinite recursion on numpy scalars, which have __array__
+ if np.isscalar(obj):
+ return None
+ elif isinstance(obj, np.ndarray):
+ return obj
+ elif hasattr(obj, "__array__") or hasattr("obj", "__array_interface__"):
+ return np.asarray(obj)
+ return None
+
+
+# builtin pytest.raises helper
+
+E = TypeVar("E", bound=BaseException)
+
+
+@overload
+def raises(
+ expected_exception: Union[Type[E], Tuple[Type[E], ...]],
+ *,
+ match: Optional[Union[str, Pattern[str]]] = ...,
+) -> "RaisesContext[E]":
+ ...
+
+
+@overload
+def raises(
+ expected_exception: Union[Type[E], Tuple[Type[E], ...]],
+ func: Callable[..., Any],
+ *args: Any,
+ **kwargs: Any,
+) -> _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.
+
+ :kwparam 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`.
+
+ (This is only used when :py:func:`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:
+ ``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::
+
+ >>> import pytest
+ >>> with pytest.raises(ZeroDivisionError):
+ ... 1/0
+
+ If the code block does not raise the expected exception (``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
+ exception matches a text or regex::
+
+ >>> with pytest.raises(ValueError, match='must be 0 or None'):
+ ... raise ValueError("value must be 0 or None")
+
+ >>> with pytest.raises(ValueError, match=r'must be \d+$'):
+ ... raise ValueError("value must be 42")
+
+ The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the
+ details of the captured exception::
+
+ >>> with pytest.raises(ValueError) as exc_info:
+ ... raise ValueError("value must be 42")
+ >>> assert exc_info.type is ValueError
+ >>> assert exc_info.value.args[0] == "value must be 42"
+
+ .. note::
+
+ When using ``pytest.raises`` as a context manager, it's worthwhile to
+ note that normal context manager rules apply and that the exception
+ raised *must* be the final line in the scope of the context manager.
+ Lines of code after that, within the scope of the context manager will
+ not be executed. For example::
+
+ >>> value = 15
+ >>> 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
+
+ Instead, the following approach must be taken (note the difference in
+ scope)::
+
+ >>> with pytest.raises(ValueError) as exc_info:
+ ... if value > 10:
+ ... raise ValueError("value must be <= 10")
+ ...
+ >>> assert exc_info.type is ValueError
+
+ **Using with** ``pytest.mark.parametrize``
+
+ When using :ref:`pytest.mark.parametrize ref`
+ it is possible to parametrize tests such that
+ some runs raise an exception and others do not.
+
+ See :ref:`parametrizing_conditional_raising` for an example.
+
+ **Legacy form**
+
+ It is possible to specify a callable by passing a to-be-called lambda::
+
+ >>> raises(ZeroDivisionError, lambda: 1/0)
+ <ExceptionInfo ...>
+
+ or you can specify an arbitrary callable with arguments::
+
+ >>> def f(x): return 1/x
+ ...
+ >>> raises(ZeroDivisionError, f, 0)
+ <ExceptionInfo ...>
+ >>> raises(ZeroDivisionError, f, x=0)
+ <ExceptionInfo ...>
+
+ The form above is fully supported but discouraged for new code because the
+ context manager form is regarded as more readable and less error-prone.
+
+ .. note::
+ Similar to caught exception objects in Python, explicitly clearing
+ local references to returned ``ExceptionInfo`` objects can
+ help the Python interpreter speed up its garbage collection.
+
+ Clearing those references breaks a reference cycle
+ (``ExceptionInfo`` --> caught exception --> frame stack raising
+ the exception --> current frame stack --> local variables -->
+ ``ExceptionInfo``) which makes Python keep all objects referenced
+ from that cycle (including all local variables in the current
+ frame) alive until the next cyclic garbage collection run.
+ More detailed information can be found in the official Python
+ documentation for :ref:`the try statement <python:try>`.
+ """
+ __tracebackhide__ = True
+
+ if isinstance(expected_exception, type):
+ excepted_exceptions: Tuple[Type[E], ...] = (expected_exception,)
+ else:
+ excepted_exceptions = expected_exception
+ for exc in excepted_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__
+ raise TypeError(msg.format(not_a))
+
+ message = f"DID NOT RAISE {expected_exception}"
+
+ if not args:
+ match: Optional[Union[str, Pattern[str]]] = kwargs.pop("match", None)
+ if kwargs:
+ msg = "Unexpected keyword arguments passed to pytest.raises: "
+ msg += ", ".join(sorted(kwargs))
+ msg += "\nUse context-manager form instead?"
+ raise TypeError(msg)
+ return RaisesContext(expected_exception, message, match)
+ else:
+ func = args[0]
+ if not callable(func):
+ raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
+ 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__)
+ )
+ fail(message)
+
+
+# This doesn't work with mypy for now. Use fail.Exception instead.
+raises.Exception = fail.Exception # type: ignore
+
+
+@final
+class RaisesContext(Generic[E]):
+ def __init__(
+ self,
+ expected_exception: Union[Type[E], Tuple[Type[E], ...]],
+ message: str,
+ match_expr: Optional[Union[str, Pattern[str]]] = None,
+ ) -> None:
+ self.expected_exception = expected_exception
+ self.message = message
+ self.match_expr = match_expr
+ self.excinfo: Optional[_pytest._code.ExceptionInfo[E]] = None
+
+ def __enter__(self) -> _pytest._code.ExceptionInfo[E]:
+ self.excinfo = _pytest._code.ExceptionInfo.for_later()
+ return self.excinfo
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> bool:
+ __tracebackhide__ = True
+ if exc_type is None:
+ fail(self.message)
+ assert self.excinfo is not None
+ if not issubclass(exc_type, self.expected_exception):
+ return False
+ # Cast to narrow the exception type now that it's verified.
+ exc_info = cast(Tuple[Type[E], E, TracebackType], (exc_type, exc_val, exc_tb))
+ self.excinfo.fill_unfilled(exc_info)
+ if self.match_expr is not None:
+ self.excinfo.match(self.match_expr)
+ return True
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/python_path.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/python_path.py
new file mode 100644
index 0000000000..cceabbca12
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/python_path.py
@@ -0,0 +1,24 @@
+import sys
+
+import pytest
+from pytest import Config
+from pytest import Parser
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addini("pythonpath", type="paths", help="Add paths to sys.path", default=[])
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_load_initial_conftests(early_config: Config) -> None:
+ # `pythonpath = a b` will set `sys.path` to `[a, b, x, y, z, ...]`
+ for path in reversed(early_config.getini("pythonpath")):
+ sys.path.insert(0, str(path))
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_unconfigure(config: Config) -> None:
+ for path in config.getini("pythonpath"):
+ path_str = str(path)
+ if path_str in sys.path:
+ sys.path.remove(path_str)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/recwarn.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/recwarn.py
new file mode 100644
index 0000000000..175b571a80
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/recwarn.py
@@ -0,0 +1,296 @@
+"""Record warnings during test function execution."""
+import re
+import warnings
+from types import TracebackType
+from typing import Any
+from typing import Callable
+from typing import Generator
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import overload
+from typing import Pattern
+from typing import Tuple
+from typing import Type
+from typing import TypeVar
+from typing import Union
+
+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 fail
+
+
+T = TypeVar("T")
+
+
+@fixture
+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
+ on warning categories.
+ """
+ wrec = WarningsRecorder(_ispytest=True)
+ with wrec:
+ warnings.simplefilter("default")
+ yield wrec
+
+
+@overload
+def deprecated_call(
+ *, match: Optional[Union[str, Pattern[str]]] = ...
+) -> "WarningsRecorder":
+ ...
+
+
+@overload
+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``.
+
+ This function can be used as a context manager::
+
+ >>> import warnings
+ >>> def api_call_v2():
+ ... warnings.warn('use v3 of this api', DeprecationWarning)
+ ... return 200
+
+ >>> import pytest
+ >>> with pytest.deprecated_call():
+ ... assert api_call_v2() == 200
+
+ It can also be used by passing a function and ``*args`` and ``**kwargs``,
+ in which case it will ensure calling ``func(*args, **kwargs)`` produces one of
+ the warnings types above. The return value is the return value of the function.
+
+ In the context manager form you may use the keyword argument ``match`` to assert
+ that the warning matches a text or regex.
+
+ The context manager produces a list of :class:`warnings.WarningMessage` objects,
+ one for each warning raised.
+ """
+ __tracebackhide__ = True
+ if func is not None:
+ args = (func,) + args
+ return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs)
+
+
+@overload
+def warns(
+ expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ...,
+ *,
+ match: Optional[Union[str, Pattern[str]]] = ...,
+) -> "WarningsChecker":
+ ...
+
+
+@overload
+def warns(
+ expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]],
+ func: Callable[..., T],
+ *args: Any,
+ **kwargs: Any,
+) -> T:
+ ...
+
+
+def warns(
+ expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning,
+ *args: Any,
+ match: Optional[Union[str, Pattern[str]]] = None,
+ **kwargs: Any,
+) -> 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.
+
+ This helper produces a list of :class:`warnings.WarningMessage` objects,
+ one for each warning raised.
+
+ This function can be used as a context manager, or any of the other ways
+ :func:`pytest.raises` can be used::
+
+ >>> import pytest
+ >>> with pytest.warns(RuntimeWarning):
+ ... warnings.warn("my warning", RuntimeWarning)
+
+ In the context manager form you may use the keyword argument ``match`` to assert
+ that the warning matches a text or regex::
+
+ >>> with pytest.warns(UserWarning, match='must be 0 or None'):
+ ... warnings.warn("value must be 0 or None", UserWarning)
+
+ >>> 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)
+ Traceback (most recent call last):
+ ...
+ Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
+
+ """
+ __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)
+ return WarningsChecker(expected_warning, match_expr=match, _ispytest=True)
+ else:
+ func = args[0]
+ if not callable(func):
+ raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
+ with WarningsChecker(expected_warning, _ispytest=True):
+ return func(*args[1:], **kwargs)
+
+
+class WarningsRecorder(warnings.catch_warnings):
+ """A context manager to record raised warnings.
+
+ Adapted from `warnings.catch_warnings`.
+ """
+
+ 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]
+ self._entered = False
+ self._list: List[warnings.WarningMessage] = []
+
+ @property
+ def list(self) -> List["warnings.WarningMessage"]:
+ """The list of recorded warnings."""
+ return self._list
+
+ def __getitem__(self, i: int) -> "warnings.WarningMessage":
+ """Get a recorded warning by index."""
+ return self._list[i]
+
+ def __iter__(self) -> Iterator["warnings.WarningMessage"]:
+ """Iterate through the recorded warnings."""
+ return iter(self._list)
+
+ def __len__(self) -> int:
+ """The number of recorded warnings."""
+ return len(self._list)
+
+ def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage":
+ """Pop the first recorded warning, raise exception if not exists."""
+ for i, w in enumerate(self._list):
+ if issubclass(w.category, cls):
+ return self._list.pop(i)
+ __tracebackhide__ = True
+ raise AssertionError("%r not found in warning list" % cls)
+
+ def clear(self) -> None:
+ """Clear the list of recorded warnings."""
+ self._list[:] = []
+
+ # Type ignored because it doesn't exactly warnings.catch_warnings.__enter__
+ # -- it returns a List but we only emulate one.
+ def __enter__(self) -> "WarningsRecorder": # type: ignore
+ if self._entered:
+ __tracebackhide__ = True
+ raise RuntimeError("Cannot enter %r twice" % self)
+ _list = super().__enter__()
+ # record=True means it's None.
+ assert _list is not None
+ self._list = _list
+ warnings.simplefilter("always")
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ if not self._entered:
+ __tracebackhide__ = True
+ raise RuntimeError("Cannot exit %r without entering first" % self)
+
+ super().__exit__(exc_type, exc_val, exc_tb)
+
+ # Built-in catch_warnings does not reset entered state so we do it
+ # manually here for this context manager to become reusable.
+ self._entered = False
+
+
+@final
+class WarningsChecker(WarningsRecorder):
+ def __init__(
+ self,
+ expected_warning: Optional[
+ Union[Type[Warning], Tuple[Type[Warning], ...]]
+ ] = Warning,
+ match_expr: Optional[Union[str, Pattern[str]]] = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ 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):
+ 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):
+ expected_warning_tup = (expected_warning,)
+ else:
+ raise TypeError(msg % type(expected_warning))
+
+ self.expected_warning = expected_warning_tup
+ self.match_expr = match_expr
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ super().__exit__(exc_type, exc_val, exc_tb)
+
+ __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]
+ )
+ )
+ 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],
+ )
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/reports.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/reports.py
new file mode 100644
index 0000000000..a68e68bc52
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/reports.py
@@ -0,0 +1,598 @@
+import os
+from io import StringIO
+from pprint import pprint
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import Iterable
+from typing import Iterator
+from typing import List
+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
+
+import attr
+
+from _pytest._code.code import ExceptionChainRepr
+from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import ExceptionRepr
+from _pytest._code.code import ReprEntry
+from _pytest._code.code import ReprEntryNative
+from _pytest._code.code import ReprExceptionInfo
+from _pytest._code.code import ReprFileLocation
+from _pytest._code.code import ReprFuncArgs
+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
+
+ from _pytest.runner import CallInfo
+
+
+def getworkerinfoline(node):
+ try:
+ return node._workerinfocache
+ except AttributeError:
+ d = node.workerinfo
+ ver = "%s.%s.%s" % d["version_info"][:3]
+ node._workerinfocache = s = "[{}] {} -- Python {} {}".format(
+ d["id"], d["sysplatform"], ver, d["executable"]
+ )
+ return s
+
+
+_R = TypeVar("_R", bound="BaseReport")
+
+
+class BaseReport:
+ when: Optional[str]
+ location: Optional[Tuple[str, Optional[int], str]]
+ longrepr: Union[
+ None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
+ ]
+ sections: List[Tuple[str, str]]
+ nodeid: str
+ 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 toterminal(self, out: TerminalWriter) -> None:
+ if hasattr(self, "node"):
+ worker_info = getworkerinfoline(self.node)
+ if worker_info:
+ out.line(worker_info)
+
+ longrepr = self.longrepr
+ if longrepr is None:
+ return
+
+ if hasattr(longrepr, "toterminal"):
+ longrepr_terminal = cast(TerminalRepr, longrepr)
+ longrepr_terminal.toterminal(out)
+ else:
+ try:
+ s = str(longrepr)
+ except UnicodeEncodeError:
+ s = "<unprintable longrepr>"
+ out.line(s)
+
+ def get_sections(self, prefix: str) -> Iterator[Tuple[str, str]]:
+ for name, content in self.sections:
+ if name.startswith(prefix):
+ yield prefix, content
+
+ @property
+ def longreprtext(self) -> str:
+ """Read-only property that returns the full string representation of
+ ``longrepr``.
+
+ .. versionadded:: 3.0
+ """
+ file = StringIO()
+ tw = TerminalWriter(file)
+ tw.hasmarkup = False
+ self.toterminal(tw)
+ exc = file.getvalue()
+ return exc.strip()
+
+ @property
+ def caplog(self) -> str:
+ """Return captured log lines, if log capturing is enabled.
+
+ .. versionadded:: 3.5
+ """
+ return "\n".join(
+ content for (prefix, content) in self.get_sections("Captured log")
+ )
+
+ @property
+ def capstdout(self) -> str:
+ """Return captured text from stdout, if capturing is enabled.
+
+ .. versionadded:: 3.0
+ """
+ return "".join(
+ content for (prefix, content) in self.get_sections("Captured stdout")
+ )
+
+ @property
+ def capstderr(self) -> str:
+ """Return captured text from stderr, if capturing is enabled.
+
+ .. versionadded:: 3.0
+ """
+ return "".join(
+ content for (prefix, content) in self.get_sections("Captured stderr")
+ )
+
+ @property
+ def passed(self) -> bool:
+ """Whether the outcome is passed."""
+ return self.outcome == "passed"
+
+ @property
+ def failed(self) -> bool:
+ """Whether the outcome is failed."""
+ return self.outcome == "failed"
+
+ @property
+ def skipped(self) -> bool:
+ """Whether the outcome is skipped."""
+ return self.outcome == "skipped"
+
+ @property
+ def fspath(self) -> str:
+ """The path portion of the reported node, as a string."""
+ return self.nodeid.split("::")[0]
+
+ @property
+ def count_towards_summary(self) -> bool:
+ """**Experimental** Whether this report should be counted towards the
+ totals shown at the end of the test session: "1 passed, 1 failure, etc".
+
+ .. note::
+
+ This function is considered **experimental**, so beware that it is subject to changes
+ even in patch releases.
+ """
+ return True
+
+ @property
+ def head_line(self) -> Optional[str]:
+ """**Experimental** The head line shown with longrepr output for this
+ report, more commonly during traceback representation during
+ failures::
+
+ ________ Test.foo ________
+
+
+ In the example above, the head_line is "Test.foo".
+
+ .. note::
+
+ This function is considered **experimental**, so beware that it is subject to changes
+ even in patch releases.
+ """
+ if self.location is not None:
+ fspath, lineno, domain = self.location
+ return domain
+ return None
+
+ def _get_verbose_word(self, config: Config):
+ _category, _short, verbose = config.hook.pytest_report_teststatus(
+ report=self, config=config
+ )
+ return verbose
+
+ def _to_json(self) -> Dict[str, Any]:
+ """Return the contents of this report as a dict of builtin entries,
+ suitable for serialization.
+
+ This was originally the serialize_report() function from xdist (ca03269).
+
+ Experimental method.
+ """
+ return _report_to_json(self)
+
+ @classmethod
+ def _from_json(cls: Type[_R], reportdict: Dict[str, object]) -> _R:
+ """Create either a TestReport or CollectReport, depending on the calling class.
+
+ It is the callers responsibility to know which class to pass here.
+
+ This was originally the serialize_report() function from xdist (ca03269).
+
+ Experimental method.
+ """
+ kwargs = _report_kwargs_from_json(reportdict)
+ return cls(**kwargs)
+
+
+def _report_unserialization_failure(
+ type_name: str, report_class: Type[BaseReport], reportdict
+) -> "NoReturn":
+ url = "https://github.com/pytest-dev/pytest/issues"
+ stream = StringIO()
+ pprint("-" * 100, stream=stream)
+ pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream)
+ pprint("report_name: %s" % report_class, stream=stream)
+ pprint(reportdict, stream=stream)
+ pprint("Please report this bug at %s" % url, stream=stream)
+ pprint("-" * 100, stream=stream)
+ raise RuntimeError(stream.getvalue())
+
+
+@final
+class TestReport(BaseReport):
+ """Basic test report object (also used for setup and teardown calls if
+ they fail).
+
+ Reports can contain arbitrary extra attributes.
+ """
+
+ __test__ = False
+
+ def __init__(
+ self,
+ nodeid: str,
+ location: Tuple[str, Optional[int], str],
+ keywords,
+ outcome: "Literal['passed', 'failed', 'skipped']",
+ longrepr: Union[
+ None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
+ ],
+ when: "Literal['setup', 'call', 'teardown']",
+ sections: Iterable[Tuple[str, str]] = (),
+ duration: float = 0,
+ user_properties: Optional[Iterable[Tuple[str, object]]] = None,
+ **extra,
+ ) -> None:
+ #: Normalized collection nodeid.
+ self.nodeid = nodeid
+
+ #: 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.
+ 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
+
+ #: Test outcome, always one of "passed", "failed", "skipped".
+ self.outcome = outcome
+
+ #: None or a failure representation.
+ self.longrepr = longrepr
+
+ #: One of 'setup', 'call', 'teardown' to indicate runtest phase.
+ self.when = when
+
+ #: User properties is a list of tuples (name, value) that holds user
+ #: defined properties of the test.
+ self.user_properties = list(user_properties or [])
+
+ #: Tuples of str ``(heading, content)`` with extra information
+ #: for the test report. Used by pytest to add text captured
+ #: from ``stdout``, ``stderr``, and intercepted logging events. May
+ #: be used by other plugins to add arbitrary information to reports.
+ self.sections = list(sections)
+
+ #: Time it took to run just the test.
+ self.duration = duration
+
+ self.__dict__.update(extra)
+
+ def __repr__(self) -> str:
+ return "<{} {!r} when={!r} outcome={!r}>".format(
+ self.__class__.__name__, self.nodeid, self.when, self.outcome
+ )
+
+ @classmethod
+ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
+ """Create and fill a TestReport with standard item and call info."""
+ when = call.when
+ # Remove "collect" from the Literal type -- only for collection calls.
+ assert when != "collect"
+ duration = call.duration
+ keywords = {x: 1 for x in item.keywords}
+ excinfo = call.excinfo
+ sections = []
+ if not call.excinfo:
+ outcome: Literal["passed", "failed", "skipped"] = "passed"
+ longrepr: Union[
+ None,
+ ExceptionInfo[BaseException],
+ Tuple[str, int, str],
+ str,
+ TerminalRepr,
+ ] = None
+ else:
+ if not isinstance(excinfo, ExceptionInfo):
+ outcome = "failed"
+ longrepr = excinfo
+ elif isinstance(excinfo.value, skip.Exception):
+ outcome = "skipped"
+ r = excinfo._getreprcrash()
+ if excinfo.value._use_item_location:
+ path, line = item.reportinfo()[:2]
+ assert line is not None
+ longrepr = os.fspath(path), line + 1, r.message
+ else:
+ longrepr = (str(r.path), r.lineno, r.message)
+ else:
+ outcome = "failed"
+ if call.when == "call":
+ longrepr = item.repr_failure(excinfo)
+ else: # exception in setup or teardown
+ longrepr = item._repr_failure_py(
+ excinfo, style=item.config.getoption("tbstyle", "auto")
+ )
+ for rwhen, key, content in item._report_sections:
+ sections.append((f"Captured {key} {rwhen}", content))
+ return cls(
+ item.nodeid,
+ item.location,
+ keywords,
+ outcome,
+ longrepr,
+ when,
+ sections,
+ duration,
+ user_properties=item.user_properties,
+ )
+
+
+@final
+class CollectReport(BaseReport):
+ """Collection report object.
+
+ Reports can contain arbitrary extra attributes.
+ """
+
+ when = "collect"
+
+ def __init__(
+ self,
+ nodeid: str,
+ outcome: "Literal['passed', 'failed', 'skipped']",
+ longrepr: Union[
+ None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
+ ],
+ result: Optional[List[Union[Item, Collector]]],
+ sections: Iterable[Tuple[str, str]] = (),
+ **extra,
+ ) -> None:
+ #: Normalized collection nodeid.
+ self.nodeid = nodeid
+
+ #: Test outcome, always one of "passed", "failed", "skipped".
+ self.outcome = outcome
+
+ #: None or a failure representation.
+ self.longrepr = longrepr
+
+ #: The collected items and collection nodes.
+ self.result = result or []
+
+ #: Tuples of str ``(heading, content)`` with extra information
+ #: for the test report. Used by pytest to add text captured
+ #: from ``stdout``, ``stderr``, and intercepted logging events. May
+ #: be used by other plugins to add arbitrary information to reports.
+ self.sections = list(sections)
+
+ self.__dict__.update(extra)
+
+ @property
+ def location(self):
+ return (self.fspath, None, self.fspath)
+
+ def __repr__(self) -> str:
+ return "<CollectReport {!r} lenresult={} outcome={!r}>".format(
+ self.nodeid, len(self.result), self.outcome
+ )
+
+
+class CollectErrorRepr(TerminalRepr):
+ def __init__(self, msg: str) -> None:
+ self.longrepr = msg
+
+ def toterminal(self, out: TerminalWriter) -> None:
+ out.line(self.longrepr, red=True)
+
+
+def pytest_report_to_serializable(
+ report: Union[CollectReport, TestReport]
+) -> Optional[Dict[str, Any]]:
+ if isinstance(report, (TestReport, CollectReport)):
+ data = report._to_json()
+ data["$report_type"] = report.__class__.__name__
+ return data
+ # TODO: Check if this is actually reachable.
+ return None # type: ignore[unreachable]
+
+
+def pytest_report_from_serializable(
+ data: Dict[str, Any],
+) -> Optional[Union[CollectReport, TestReport]]:
+ if "$report_type" in data:
+ if data["$report_type"] == "TestReport":
+ return TestReport._from_json(data)
+ elif data["$report_type"] == "CollectReport":
+ return CollectReport._from_json(data)
+ assert False, "Unknown report_type unserialize data: {}".format(
+ data["$report_type"]
+ )
+ return None
+
+
+def _report_to_json(report: BaseReport) -> Dict[str, Any]:
+ """Return the contents of this report as a dict of builtin entries,
+ suitable for serialization.
+
+ This was originally the serialize_report() function from xdist (ca03269).
+ """
+
+ def serialize_repr_entry(
+ entry: Union[ReprEntry, ReprEntryNative]
+ ) -> Dict[str, Any]:
+ data = attr.asdict(entry)
+ for key, value in data.items():
+ if hasattr(value, "__dict__"):
+ data[key] = attr.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["reprentries"] = [
+ serialize_repr_entry(x) for x in reprtraceback.reprentries
+ ]
+ return result
+
+ def serialize_repr_crash(
+ reprcrash: Optional[ReprFileLocation],
+ ) -> Optional[Dict[str, Any]]:
+ if reprcrash is not None:
+ return attr.asdict(reprcrash)
+ else:
+ return None
+
+ def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]:
+ assert rep.longrepr is not None
+ # TODO: Investigate whether the duck typing is really necessary here.
+ longrepr = cast(ExceptionRepr, rep.longrepr)
+ result: Dict[str, Any] = {
+ "reprcrash": serialize_repr_crash(longrepr.reprcrash),
+ "reprtraceback": serialize_repr_traceback(longrepr.reprtraceback),
+ "sections": longrepr.sections,
+ }
+ if isinstance(longrepr, ExceptionChainRepr):
+ result["chain"] = []
+ for repr_traceback, repr_crash, description in longrepr.chain:
+ result["chain"].append(
+ (
+ serialize_repr_traceback(repr_traceback),
+ serialize_repr_crash(repr_crash),
+ description,
+ )
+ )
+ else:
+ result["chain"] = None
+ return result
+
+ d = report.__dict__.copy()
+ if hasattr(report.longrepr, "toterminal"):
+ if hasattr(report.longrepr, "reprtraceback") and hasattr(
+ report.longrepr, "reprcrash"
+ ):
+ d["longrepr"] = serialize_exception_longrepr(report)
+ else:
+ d["longrepr"] = str(report.longrepr)
+ else:
+ d["longrepr"] = report.longrepr
+ for name in d:
+ if isinstance(d[name], os.PathLike):
+ d[name] = os.fspath(d[name])
+ elif name == "result":
+ d[name] = None # for now
+ return d
+
+
+def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
+ """Return **kwargs that can be used to construct a TestReport or
+ CollectReport instance.
+
+ This was originally the serialize_report() function from xdist (ca03269).
+ """
+
+ def deserialize_repr_entry(entry_data):
+ data = entry_data["data"]
+ entry_type = entry_data["type"]
+ if entry_type == "ReprEntry":
+ reprfuncargs = None
+ reprfileloc = None
+ reprlocals = None
+ if data["reprfuncargs"]:
+ reprfuncargs = ReprFuncArgs(**data["reprfuncargs"])
+ if data["reprfileloc"]:
+ reprfileloc = ReprFileLocation(**data["reprfileloc"])
+ if data["reprlocals"]:
+ reprlocals = ReprLocals(data["reprlocals"]["lines"])
+
+ reprentry: Union[ReprEntry, ReprEntryNative] = ReprEntry(
+ lines=data["lines"],
+ reprfuncargs=reprfuncargs,
+ reprlocals=reprlocals,
+ reprfileloc=reprfileloc,
+ style=data["style"],
+ )
+ elif entry_type == "ReprEntryNative":
+ reprentry = ReprEntryNative(data["lines"])
+ else:
+ _report_unserialization_failure(entry_type, TestReport, reportdict)
+ return reprentry
+
+ def deserialize_repr_traceback(repr_traceback_dict):
+ repr_traceback_dict["reprentries"] = [
+ deserialize_repr_entry(x) for x in repr_traceback_dict["reprentries"]
+ ]
+ return ReprTraceback(**repr_traceback_dict)
+
+ def deserialize_repr_crash(repr_crash_dict: Optional[Dict[str, Any]]):
+ if repr_crash_dict is not None:
+ return ReprFileLocation(**repr_crash_dict)
+ else:
+ return None
+
+ if (
+ reportdict["longrepr"]
+ and "reprcrash" in reportdict["longrepr"]
+ and "reprtraceback" in reportdict["longrepr"]
+ ):
+
+ reprtraceback = deserialize_repr_traceback(
+ reportdict["longrepr"]["reprtraceback"]
+ )
+ reprcrash = deserialize_repr_crash(reportdict["longrepr"]["reprcrash"])
+ if reportdict["longrepr"]["chain"]:
+ chain = []
+ for repr_traceback_data, repr_crash_data, description in reportdict[
+ "longrepr"
+ ]["chain"]:
+ chain.append(
+ (
+ deserialize_repr_traceback(repr_traceback_data),
+ deserialize_repr_crash(repr_crash_data),
+ description,
+ )
+ )
+ exception_info: Union[
+ ExceptionChainRepr, ReprExceptionInfo
+ ] = ExceptionChainRepr(chain)
+ else:
+ exception_info = ReprExceptionInfo(reprtraceback, reprcrash)
+
+ for section in reportdict["longrepr"]["sections"]:
+ exception_info.addsection(*section)
+ reportdict["longrepr"] = exception_info
+
+ return reportdict
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/runner.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/runner.py
new file mode 100644
index 0000000000..e43dd2dc81
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/runner.py
@@ -0,0 +1,548 @@
+"""Basic collect and runtest protocol implementations."""
+import bdb
+import os
+import sys
+import warnings
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import Generic
+from typing import List
+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
+
+import attr
+
+from .reports import BaseReport
+from .reports import CollectErrorRepr
+from .reports import CollectReport
+from .reports import TestReport
+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 Item
+from _pytest.nodes import Node
+from _pytest.outcomes import Exit
+from _pytest.outcomes import OutcomeException
+from _pytest.outcomes import Skipped
+from _pytest.outcomes import TEST_OUTCOME
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from _pytest.main import Session
+ from _pytest.terminal import TerminalReporter
+
+#
+# pytest plugin hooks.
+
+
+def pytest_addoption(parser: Parser) -> None:
+ 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).",
+ )
+ group.addoption(
+ "--durations-min",
+ action="store",
+ type=float,
+ default=0.005,
+ metavar="N",
+ help="Minimal duration in seconds for inclusion in slowest list. Default 0.005",
+ )
+
+
+def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
+ durations = terminalreporter.config.option.durations
+ durations_min = terminalreporter.config.option.durations_min
+ verbose = terminalreporter.config.getvalue("verbose")
+ if durations is None:
+ return
+ tr = terminalreporter
+ dlist = []
+ for replist in tr.stats.values():
+ for rep in replist:
+ if hasattr(rep, "duration"):
+ dlist.append(rep)
+ if not dlist:
+ return
+ dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return]
+ if not durations:
+ tr.write_sep("=", "slowest durations")
+ else:
+ tr.write_sep("=", "slowest %s durations" % durations)
+ dlist = dlist[:durations]
+
+ for i, rep in enumerate(dlist):
+ 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)
+ )
+ break
+ tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}")
+
+
+def pytest_sessionstart(session: "Session") -> None:
+ session._setupstate = SetupState()
+
+
+def pytest_sessionfinish(session: "Session") -> None:
+ session._setupstate.teardown_exact(None)
+
+
+def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool:
+ ihook = item.ihook
+ ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
+ runtestprotocol(item, nextitem=nextitem)
+ ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
+ return True
+
+
+def runtestprotocol(
+ item: Item, log: bool = True, nextitem: Optional[Item] = None
+) -> List[TestReport]:
+ hasrequest = hasattr(item, "_request")
+ if hasrequest and not item._request: # type: ignore[attr-defined]
+ # This only happens if the item is re-run, as is done by
+ # pytest-rerunfailures.
+ item._initrequest() # type: ignore[attr-defined]
+ rep = call_and_report(item, "setup", log)
+ reports = [rep]
+ if rep.passed:
+ if item.config.getoption("setupshow", False):
+ show_test_item(item)
+ if not item.config.getoption("setuponly", False):
+ reports.append(call_and_report(item, "call", log))
+ reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
+ # After all teardown hooks have been called
+ # want funcargs and request info to go away.
+ if hasrequest:
+ item._request = False # type: ignore[attr-defined]
+ item.funcargs = None # type: ignore[attr-defined]
+ return reports
+
+
+def show_test_item(item: Item) -> None:
+ """Show test function, parameters and the fixtures of the test item."""
+ tw = item.config.get_terminal_writer()
+ tw.line()
+ tw.write(" " * 8)
+ tw.write(item.nodeid)
+ used_fixtures = sorted(getattr(item, "fixturenames", []))
+ if used_fixtures:
+ tw.write(" (fixtures used: {})".format(", ".join(used_fixtures)))
+ tw.flush()
+
+
+def pytest_runtest_setup(item: Item) -> None:
+ _update_current_test_var(item, "setup")
+ item.session._setupstate.setup(item)
+
+
+def pytest_runtest_call(item: Item) -> None:
+ _update_current_test_var(item, "call")
+ try:
+ del sys.last_type
+ del sys.last_value
+ del sys.last_traceback
+ except AttributeError:
+ pass
+ try:
+ item.runtest()
+ except Exception as e:
+ # Store trace info to allow postmortem debugging
+ sys.last_type = type(e)
+ sys.last_value = e
+ assert e.__traceback__ is not None
+ # Skip *this* frame
+ sys.last_traceback = e.__traceback__.tb_next
+ raise e
+
+
+def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None:
+ _update_current_test_var(item, "teardown")
+ item.session._setupstate.teardown_exact(nextitem)
+ _update_current_test_var(item, None)
+
+
+def _update_current_test_var(
+ item: Item, when: Optional["Literal['setup', 'call', 'teardown']"]
+) -> None:
+ """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage.
+
+ If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment.
+ """
+ var_name = "PYTEST_CURRENT_TEST"
+ if when:
+ value = f"{item.nodeid} ({when})"
+ # don't allow null bytes on environment variables (see #2644, #2957)
+ value = value.replace("\x00", "(null)")
+ os.environ[var_name] = value
+ else:
+ os.environ.pop(var_name)
+
+
+def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]:
+ if report.when in ("setup", "teardown"):
+ if report.failed:
+ # category, shortletter, verbose-word
+ return "error", "E", "ERROR"
+ elif report.skipped:
+ return "skipped", "s", "SKIPPED"
+ else:
+ return "", "", ""
+ return None
+
+
+#
+# Implementation
+
+
+def call_and_report(
+ 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)
+ if log:
+ hook.pytest_runtest_logreport(report=report)
+ if check_interactive_exception(call, report):
+ hook.pytest_exception_interact(node=item, call=call, report=report)
+ return report
+
+
+def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> bool:
+ """Check whether the call raised an exception that should be reported as
+ interactive."""
+ if call.excinfo is None:
+ # Didn't raise.
+ return False
+ if hasattr(report, "wasxfail"):
+ # Exception was expected.
+ return False
+ if isinstance(call.excinfo.value, (Skipped, bdb.BdbQuit)):
+ # Special control flow exception.
+ return False
+ 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)
+class CallInfo(Generic[TResult]):
+ """Result/Exception info of a function invocation."""
+
+ _result: Optional[TResult]
+ #: The captured exception of the call, if it raised.
+ excinfo: Optional[ExceptionInfo[BaseException]]
+ #: The system time when the call started, in seconds since the epoch.
+ start: float
+ #: The system time when the call ended, in seconds since the epoch.
+ stop: float
+ #: The call duration, in seconds.
+ duration: float
+ #: The context of invocation: "collect", "setup", "call" or "teardown".
+ when: "Literal['collect', 'setup', 'call', 'teardown']"
+
+ def __init__(
+ self,
+ result: Optional[TResult],
+ excinfo: Optional[ExceptionInfo[BaseException]],
+ start: float,
+ stop: float,
+ duration: float,
+ when: "Literal['collect', 'setup', 'call', 'teardown']",
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._result = result
+ self.excinfo = excinfo
+ self.start = start
+ self.stop = stop
+ self.duration = duration
+ self.when = when
+
+ @property
+ def result(self) -> TResult:
+ """The return value of the call, if it didn't raise.
+
+ Can only be accessed if excinfo is None.
+ """
+ if self.excinfo is not None:
+ raise AttributeError(f"{self!r} has no valid result")
+ # The cast is safe because an exception wasn't raised, hence
+ # _result has the expected function return type (which may be
+ # None, that's why a cast and not an assert).
+ return cast(TResult, self._result)
+
+ @classmethod
+ def from_call(
+ cls,
+ func: "Callable[[], TResult]",
+ when: "Literal['collect', 'setup', 'call', 'teardown']",
+ reraise: Optional[
+ Union[Type[BaseException], Tuple[Type[BaseException], ...]]
+ ] = None,
+ ) -> "CallInfo[TResult]":
+ """Call func, wrapping the result in a CallInfo.
+
+ :param func:
+ The function to call. Called without arguments.
+ :param when:
+ The phase in which the function is called.
+ :param reraise:
+ Exception or exceptions that shall propagate if raised by the
+ function, instead of being wrapped in the CallInfo.
+ """
+ excinfo = None
+ start = timing.time()
+ precise_start = timing.perf_counter()
+ try:
+ result: Optional[TResult] = func()
+ except BaseException:
+ excinfo = ExceptionInfo.from_current()
+ if reraise is not None and isinstance(excinfo.value, reraise):
+ raise
+ result = None
+ # use the perf counter
+ precise_stop = timing.perf_counter()
+ duration = precise_stop - precise_start
+ stop = timing.time()
+ return cls(
+ start=start,
+ stop=stop,
+ duration=duration,
+ when=when,
+ result=result,
+ excinfo=excinfo,
+ _ispytest=True,
+ )
+
+ def __repr__(self) -> str:
+ if self.excinfo is None:
+ return f"<CallInfo when={self.when!r} result: {self._result!r}>"
+ return f"<CallInfo when={self.when!r} excinfo={self.excinfo!r}>"
+
+
+def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport:
+ return TestReport.from_item_and_call(item, call)
+
+
+def pytest_make_collect_report(collector: Collector) -> CollectReport:
+ call = CallInfo.from_call(lambda: list(collector.collect()), "collect")
+ longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None
+ if not call.excinfo:
+ outcome: Literal["passed", "skipped", "failed"] = "passed"
+ else:
+ 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
+ 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_)
+ r = r_.reprcrash
+ assert r
+ longrepr = (str(r.path), r.lineno, r.message)
+ else:
+ outcome = "failed"
+ errorinfo = collector.repr_failure(call.excinfo)
+ if not hasattr(errorinfo, "toterminal"):
+ assert isinstance(errorinfo, str)
+ errorinfo = CollectErrorRepr(errorinfo)
+ longrepr = errorinfo
+ result = call.result if not call.excinfo else None
+ rep = CollectReport(collector.nodeid, outcome, longrepr, result)
+ rep.call = call # type: ignore # see collect_one_node
+ return rep
+
+
+class SetupState:
+ """Shared state for setting up/tearing down test items or collectors
+ in a session.
+
+ Suppose we have a collection tree as follows:
+
+ <Session session>
+ <Module mod1>
+ <Function item1>
+ <Module mod2>
+ <Function item2>
+
+ The SetupState maintains a stack. The stack starts out empty:
+
+ []
+
+ During the setup phase of item1, setup(item1) is called. What it does
+ is:
+
+ push session to stack, run session.setup()
+ push mod1 to stack, run mod1.setup()
+ push item1 to stack, run item1.setup()
+
+ The stack is:
+
+ [session, mod1, item1]
+
+ While the stack is in this shape, it is allowed to add finalizers to
+ each of session, mod1, item1 using addfinalizer().
+
+ During the teardown phase of item1, teardown_exact(item2) is called,
+ where item2 is the next item to item1. What it does is:
+
+ pop item1 from stack, run its teardowns
+ pop mod1 from stack, run its teardowns
+
+ mod1 was popped because it ended its purpose with item1. The stack is:
+
+ [session]
+
+ During the setup phase of item2, setup(item2) is called. What it does
+ is:
+
+ push mod2 to stack, run mod2.setup()
+ push item2 to stack, run item2.setup()
+
+ Stack:
+
+ [session, mod2, item2]
+
+ During the teardown phase of item2, teardown_exact(None) is called,
+ because item2 is the last item. What it does is:
+
+ pop item2 from stack, run its teardowns
+ pop mod2 from stack, run its teardowns
+ pop session from stack, run its teardowns
+
+ Stack:
+
+ []
+
+ The end!
+ """
+
+ def __init__(self) -> None:
+ # The stack is in the dict insertion order.
+ self.stack: Dict[
+ Node,
+ Tuple[
+ # Node's finalizers.
+ List[Callable[[], object]],
+ # Node's exception, if its setup raised.
+ Optional[Union[OutcomeException, Exception]],
+ ],
+ ] = {}
+
+ def setup(self, item: Item) -> None:
+ """Setup objects along the collector chain to the item."""
+ needed_collectors = item.listchain()
+
+ # If a collector fails its setup, fail its entire subtree of items.
+ # The setup is not retried for each item - the same exception is used.
+ for col, (finalizers, exc) in self.stack.items():
+ assert col in needed_collectors, "previous item was not torn down properly"
+ if exc:
+ raise exc
+
+ for col in needed_collectors[len(self.stack) :]:
+ assert col not in self.stack
+ # Push onto the stack.
+ self.stack[col] = ([col.teardown], None)
+ try:
+ col.setup()
+ except TEST_OUTCOME as exc:
+ self.stack[col] = (self.stack[col][0], exc)
+ raise exc
+
+ def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None:
+ """Attach a finalizer to the given node.
+
+ The node must be currently active in the stack.
+ """
+ assert node and not isinstance(node, tuple)
+ assert callable(finalizer)
+ assert node in self.stack, (node, self.stack)
+ self.stack[node][0].append(finalizer)
+
+ def teardown_exact(self, nextitem: Optional[Item]) -> None:
+ """Teardown the current stack up until reaching nodes that nextitem
+ also descends from.
+
+ When nextitem is None (meaning we're at the last item), the entire
+ stack is torn down.
+ """
+ needed_collectors = nextitem and nextitem.listchain() or []
+ exc = None
+ while self.stack:
+ if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
+ break
+ node, (finalizers, _) = self.stack.popitem()
+ 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
+ if nextitem is None:
+ assert not self.stack
+
+
+def collect_one_node(collector: Collector) -> CollectReport:
+ ihook = collector.ihook
+ ihook.pytest_collectstart(collector=collector)
+ rep: CollectReport = ihook.pytest_make_collect_report(collector=collector)
+ call = rep.__dict__.pop("call", None)
+ if call and check_interactive_exception(call, rep):
+ ihook.pytest_exception_interact(node=collector, call=call, report=rep)
+ return rep
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/scope.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/scope.py
new file mode 100644
index 0000000000..7a746fb9fa
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/scope.py
@@ -0,0 +1,91 @@
+"""
+Scope definition and related utilities.
+
+Those are defined here, instead of in the 'fixtures' module because
+their use is spread across many other pytest modules, and centralizing it in 'fixtures'
+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 Optional
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ _ScopeName = Literal["session", "package", "module", "class", "function"]
+
+
+@total_ordering
+class Scope(Enum):
+ """
+ Represents one of the possible fixture scopes in pytest.
+
+ Scopes are ordered from lower to higher, that is:
+
+ ->>> higher ->>>
+
+ Function < Class < Module < Package < Session
+
+ <<<- lower <<<-
+ """
+
+ # Scopes need to be listed from lower to higher.
+ 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."""
+ index = _SCOPE_INDICES[self]
+ if index == 0:
+ raise ValueError(f"{self} is the lower-most scope")
+ return _ALL_SCOPES[index - 1]
+
+ def next_higher(self) -> "Scope":
+ """Return the next higher scope."""
+ index = _SCOPE_INDICES[self]
+ if index == len(_SCOPE_INDICES) - 1:
+ raise ValueError(f"{self} is the upper-most scope")
+ return _ALL_SCOPES[index + 1]
+
+ def __lt__(self, other: "Scope") -> bool:
+ self_index = _SCOPE_INDICES[self]
+ other_index = _SCOPE_INDICES[other]
+ return self_index < other_index
+
+ @classmethod
+ def from_user(
+ 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
+ whenever we want to convert a user provided scope name to its enum object.
+
+ If the scope name is invalid, construct a user friendly message and call pytest.fail.
+ """
+ from _pytest.outcomes import fail
+
+ try:
+ # Holding this reference is necessary for mypy at the moment.
+ scope = Scope(scope_name)
+ except ValueError:
+ fail(
+ "{} {}got an unexpected scope value '{}'".format(
+ descr, f"from {where} " if where else "", scope_name
+ ),
+ pytrace=False,
+ )
+ return scope
+
+
+_ALL_SCOPES = list(Scope)
+_SCOPE_INDICES = {scope: index for index, scope in enumerate(_ALL_SCOPES)}
+
+
+# Ordered list of scopes which can contain many tests (in practice all except Function).
+HIGH_SCOPES = [x for x in Scope if x is not Scope.Function]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/setuponly.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/setuponly.py
new file mode 100644
index 0000000000..531131ce72
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/setuponly.py
@@ -0,0 +1,97 @@
+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
+from _pytest.config.argparsing import Parser
+from _pytest.fixtures import FixtureDef
+from _pytest.fixtures import SubRequest
+from _pytest.scope import Scope
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("debugconfig")
+ group.addoption(
+ "--setuponly",
+ "--setup-only",
+ action="store_true",
+ help="only setup fixtures, do not execute tests.",
+ )
+ group.addoption(
+ "--setupshow",
+ "--setup-show",
+ action="store_true",
+ help="show setup of fixtures while executing tests.",
+ )
+
+
+@pytest.hookimpl(hookwrapper=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)
+ else:
+ param = fixturedef.ids[request.param_index]
+ else:
+ param = request.param
+ fixturedef.cached_param = param # type: ignore[attr-defined]
+ _show_fixture_action(fixturedef, "SETUP")
+
+
+def pytest_fixture_post_finalizer(fixturedef: FixtureDef[object]) -> None:
+ if fixturedef.cached_result is not None:
+ config = fixturedef._fixturemanager.config
+ if config.option.setupshow:
+ _show_fixture_action(fixturedef, "TEARDOWN")
+ if hasattr(fixturedef, "cached_param"):
+ del fixturedef.cached_param # type: ignore[attr-defined]
+
+
+def _show_fixture_action(fixturedef: FixtureDef[object], msg: str) -> None:
+ config = fixturedef._fixturemanager.config
+ capman = config.pluginmanager.getplugin("capturemanager")
+ if capman:
+ capman.suspend_global_capture()
+
+ tw = config.get_terminal_writer()
+ tw.line()
+ # Use smaller indentation the higher the scope: Session = 0, Package = 1, etc.
+ scope_indent = list(reversed(Scope)).index(fixturedef._scope)
+ tw.write(" " * 2 * scope_indent)
+ tw.write(
+ "{step} {scope} {fixture}".format(
+ step=msg.ljust(8), # align the output to TEARDOWN
+ scope=fixturedef.scope[0].upper(),
+ fixture=fixturedef.argname,
+ )
+ )
+
+ if msg == "SETUP":
+ deps = sorted(arg for arg in fixturedef.argnames if arg != "request")
+ if deps:
+ 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.flush()
+
+ if capman:
+ capman.resume_global_capture()
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
+ if config.option.setuponly:
+ config.option.setupshow = True
+ return None
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/setupplan.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/setupplan.py
new file mode 100644
index 0000000000..9ba81ccaf0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/setupplan.py
@@ -0,0 +1,40 @@
+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
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("debugconfig")
+ group.addoption(
+ "--setupplan",
+ "--setup-plan",
+ action="store_true",
+ help="show what fixtures and tests would be executed but "
+ "don't execute anything.",
+ )
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_fixture_setup(
+ fixturedef: FixtureDef[object], request: SubRequest
+) -> Optional[object]:
+ # Will return a dummy fixture if the setuponly option is provided.
+ if request.config.option.setupplan:
+ my_cache_key = fixturedef.cache_key(request)
+ fixturedef.cached_result = (None, my_cache_key, None)
+ return fixturedef.cached_result
+ return None
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
+ if config.option.setupplan:
+ config.option.setuponly = True
+ config.option.setupshow = True
+ return None
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/skipping.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/skipping.py
new file mode 100644
index 0000000000..ac7216f838
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/skipping.py
@@ -0,0 +1,296 @@
+"""Support for skip/xfail functions and markers."""
+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
+from _pytest.mark.structures import Mark
+from _pytest.nodes import Item
+from _pytest.outcomes import fail
+from _pytest.outcomes import skip
+from _pytest.outcomes import xfail
+from _pytest.reports import BaseReport
+from _pytest.runner import CallInfo
+from _pytest.stash import StashKey
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group.addoption(
+ "--runxfail",
+ action="store_true",
+ dest="runxfail",
+ default=False,
+ help="report the results of xfail tests as if they were not marked",
+ )
+
+ parser.addini(
+ "xfail_strict",
+ "default for the strict parameter of xfail "
+ "markers when not given explicitly (default: False)",
+ default=False,
+ type="bool",
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ if config.option.runxfail:
+ # yay a hack
+ import pytest
+
+ old = pytest.xfail
+ config.add_cleanup(lambda: setattr(pytest, "xfail", old))
+
+ def nop(*args, **kwargs):
+ pass
+
+ nop.Exception = xfail.Exception # type: ignore[attr-defined]
+ setattr(pytest, "xfail", nop)
+
+ config.addinivalue_line(
+ "markers",
+ "skip(reason=None): skip the given test function with an optional reason. "
+ 'Example: skip(reason="no way of currently testing this") skips the '
+ "test.",
+ )
+ config.addinivalue_line(
+ "markers",
+ "skipif(condition, ..., *, reason=...): "
+ "skip the given test function if any of the conditions evaluate to True. "
+ "Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. "
+ "See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-skipif",
+ )
+ config.addinivalue_line(
+ "markers",
+ "xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict): "
+ "mark the test function as an expected failure if any of the conditions "
+ "evaluate to True. Optionally specify a reason for better reporting "
+ "and run=False if you don't even want to execute the test function. "
+ "If only specific exception(s) are expected, you can list them in "
+ "raises, and if the test fails in other ways, it will be reported as "
+ "a true failure. See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-xfail",
+ )
+
+
+def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, str]:
+ """Evaluate a single skipif/xfail condition.
+
+ If an old-style string condition is given, it is eval()'d, otherwise the
+ condition is bool()'d. If this fails, an appropriately formatted pytest.fail
+ is raised.
+
+ Returns (result, reason). The reason is only relevant if the result is True.
+ """
+ # String condition.
+ if isinstance(condition, str):
+ globals_ = {
+ "os": os,
+ "sys": sys,
+ "platform": platform,
+ "config": item.config,
+ }
+ for dictionary in reversed(
+ item.ihook.pytest_markeval_namespace(config=item.config)
+ ):
+ if not isinstance(dictionary, Mapping):
+ raise ValueError(
+ "pytest_markeval_namespace() needs to return a dict, got {!r}".format(
+ dictionary
+ )
+ )
+ globals_.update(dictionary)
+ if hasattr(item, "obj"):
+ globals_.update(item.obj.__globals__) # type: ignore[attr-defined]
+ try:
+ filename = f"<{mark.name} condition>"
+ condition_code = compile(condition, filename, "eval")
+ result = eval(condition_code, globals_)
+ except SyntaxError as exc:
+ msglines = [
+ "Error evaluating %r condition" % mark.name,
+ " " + condition,
+ " " + " " * (exc.offset or 0) + "^",
+ "SyntaxError: invalid syntax",
+ ]
+ fail("\n".join(msglines), pytrace=False)
+ except Exception as exc:
+ msglines = [
+ "Error evaluating %r condition" % mark.name,
+ " " + condition,
+ *traceback.format_exception_only(type(exc), exc),
+ ]
+ fail("\n".join(msglines), pytrace=False)
+
+ # Boolean condition.
+ else:
+ try:
+ result = bool(condition)
+ except Exception as exc:
+ msglines = [
+ "Error evaluating %r condition as a boolean" % mark.name,
+ *traceback.format_exception_only(type(exc), exc),
+ ]
+ fail("\n".join(msglines), pytrace=False)
+
+ reason = mark.kwargs.get("reason", None)
+ if reason is None:
+ if isinstance(condition, str):
+ reason = "condition: " + condition
+ else:
+ # XXX better be checked at collection time
+ msg = (
+ "Error evaluating %r: " % mark.name
+ + "you need to specify reason=STRING when using booleans as conditions."
+ )
+ fail(msg, pytrace=False)
+
+ return result, reason
+
+
+@attr.s(slots=True, frozen=True, auto_attribs=True)
+class Skip:
+ """The result of evaluate_skip_marks()."""
+
+ reason: str = "unconditional skip"
+
+
+def evaluate_skip_marks(item: Item) -> Optional[Skip]:
+ """Evaluate skip and skipif marks on item, returning Skip if triggered."""
+ for mark in item.iter_markers(name="skipif"):
+ if "condition" not in mark.kwargs:
+ conditions = mark.args
+ else:
+ conditions = (mark.kwargs["condition"],)
+
+ # Unconditional.
+ if not conditions:
+ reason = mark.kwargs.get("reason", "")
+ return Skip(reason)
+
+ # If any of the conditions are true.
+ for condition in conditions:
+ result, reason = evaluate_condition(item, mark, condition)
+ if result:
+ return Skip(reason)
+
+ for mark in item.iter_markers(name="skip"):
+ try:
+ return Skip(*mark.args, **mark.kwargs)
+ except TypeError as e:
+ raise TypeError(str(e) + " - maybe you meant pytest.mark.skipif?") from None
+
+ return None
+
+
+@attr.s(slots=True, frozen=True, auto_attribs=True)
+class Xfail:
+ """The result of evaluate_xfail_marks()."""
+
+ reason: str
+ run: bool
+ strict: bool
+ raises: Optional[Tuple[Type[BaseException], ...]]
+
+
+def evaluate_xfail_marks(item: Item) -> Optional[Xfail]:
+ """Evaluate xfail marks on item, returning Xfail if triggered."""
+ for mark in item.iter_markers(name="xfail"):
+ run = mark.kwargs.get("run", True)
+ strict = mark.kwargs.get("strict", item.config.getini("xfail_strict"))
+ raises = mark.kwargs.get("raises", None)
+ if "condition" not in mark.kwargs:
+ conditions = mark.args
+ else:
+ conditions = (mark.kwargs["condition"],)
+
+ # Unconditional.
+ if not conditions:
+ reason = mark.kwargs.get("reason", "")
+ return Xfail(reason, run, strict, raises)
+
+ # If any of the conditions are true.
+ for condition in conditions:
+ result, reason = evaluate_condition(item, mark, condition)
+ if result:
+ return Xfail(reason, run, strict, raises)
+
+ return None
+
+
+# Saves the xfail mark evaluation. Can be refreshed during call if None.
+xfailed_key = StashKey[Optional[Xfail]]()
+
+
+@hookimpl(tryfirst=True)
+def pytest_runtest_setup(item: Item) -> None:
+ skipped = evaluate_skip_marks(item)
+ if skipped:
+ raise skip.Exception(skipped.reason, _use_item_location=True)
+
+ item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
+ if xfailed and not item.config.option.runxfail and not xfailed.run:
+ xfail("[NOTRUN] " + xfailed.reason)
+
+
+@hookimpl(hookwrapper=True)
+def pytest_runtest_call(item: Item) -> Generator[None, None, None]:
+ xfailed = item.stash.get(xfailed_key, None)
+ if xfailed is None:
+ item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
+
+ 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)
+
+
+@hookimpl(hookwrapper=True)
+def pytest_runtest_makereport(item: Item, call: CallInfo[None]):
+ outcome = yield
+ rep = outcome.get_result()
+ xfailed = item.stash.get(xfailed_key, None)
+ if item.config.option.runxfail:
+ pass # don't interfere
+ elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception):
+ assert call.excinfo.value.msg is not None
+ rep.wasxfail = "reason: " + call.excinfo.value.msg
+ rep.outcome = "skipped"
+ elif not rep.skipped and xfailed:
+ if call.excinfo:
+ raises = xfailed.raises
+ if raises is not None and not isinstance(call.excinfo.value, raises):
+ rep.outcome = "failed"
+ else:
+ rep.outcome = "skipped"
+ rep.wasxfail = xfailed.reason
+ elif call.when == "call":
+ if xfailed.strict:
+ rep.outcome = "failed"
+ rep.longrepr = "[XPASS(strict)] " + xfailed.reason
+ else:
+ rep.outcome = "passed"
+ rep.wasxfail = xfailed.reason
+
+
+def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]:
+ if hasattr(report, "wasxfail"):
+ if report.skipped:
+ return "xfailed", "x", "XFAIL"
+ elif report.passed:
+ return "xpassed", "X", "XPASS"
+ return None
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/stash.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/stash.py
new file mode 100644
index 0000000000..e61d75b95f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/stash.py
@@ -0,0 +1,112 @@
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import Generic
+from typing import TypeVar
+from typing import Union
+
+
+__all__ = ["Stash", "StashKey"]
+
+
+T = TypeVar("T")
+D = TypeVar("D")
+
+
+class StashKey(Generic[T]):
+ """``StashKey`` is an object used as a key to a :class:`Stash`.
+
+ A ``StashKey`` is associated with the type ``T`` of the value of the key.
+
+ A ``StashKey`` is unique and cannot conflict with another key.
+ """
+
+ __slots__ = ()
+
+
+class Stash:
+ r"""``Stash`` is a type-safe heterogeneous mutable mapping that
+ allows keys and value types to be defined separately from
+ where it (the ``Stash``) is created.
+
+ Usually you will be given an object which has a ``Stash``, for example
+ :class:`~pytest.Config` or a :class:`~_pytest.nodes.Node`:
+
+ .. code-block:: python
+
+ stash: Stash = some_object.stash
+
+ If a module or plugin wants to store data in this ``Stash``, it creates
+ :class:`StashKey`\s for its keys (at the module level):
+
+ .. code-block:: python
+
+ # At the top-level of the module
+ some_str_key = StashKey[str]()
+ some_bool_key = StashKey[bool]()
+
+ To store information:
+
+ .. code-block:: python
+
+ # Value type must match the key.
+ stash[some_str_key] = "value"
+ stash[some_bool_key] = True
+
+ To retrieve the information:
+
+ .. code-block:: python
+
+ # The static type of some_str is str.
+ some_str = stash[some_str_key]
+ # The static type of some_bool is bool.
+ some_bool = stash[some_bool_key]
+ """
+
+ __slots__ = ("_storage",)
+
+ def __init__(self) -> None:
+ self._storage: Dict[StashKey[Any], object] = {}
+
+ def __setitem__(self, key: StashKey[T], value: T) -> None:
+ """Set a value for key."""
+ self._storage[key] = value
+
+ def __getitem__(self, key: StashKey[T]) -> T:
+ """Get the value for key.
+
+ Raises ``KeyError`` if the key wasn't set before.
+ """
+ return cast(T, self._storage[key])
+
+ def get(self, key: StashKey[T], default: D) -> Union[T, D]:
+ """Get the value for key, or return default if the key wasn't set
+ before."""
+ try:
+ return self[key]
+ except KeyError:
+ return default
+
+ def setdefault(self, key: StashKey[T], default: T) -> T:
+ """Return the value of key if already set, otherwise set the value
+ of key to default and return default."""
+ try:
+ return self[key]
+ except KeyError:
+ self[key] = default
+ return default
+
+ def __delitem__(self, key: StashKey[T]) -> None:
+ """Delete the value for key.
+
+ Raises ``KeyError`` if the key wasn't set before.
+ """
+ del self._storage[key]
+
+ def __contains__(self, key: StashKey[T]) -> bool:
+ """Return whether key was set."""
+ return key in self._storage
+
+ def __len__(self) -> int:
+ """Return how many items exist in the stash."""
+ return len(self._storage)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/stepwise.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/stepwise.py
new file mode 100644
index 0000000000..4d95a96b87
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/stepwise.py
@@ -0,0 +1,122 @@
+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
+
+if TYPE_CHECKING:
+ from _pytest.cacheprovider import Cache
+
+STEPWISE_CACHE_DIR = "cache/stepwise"
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group.addoption(
+ "--sw",
+ "--stepwise",
+ action="store_true",
+ default=False,
+ dest="stepwise",
+ help="exit on test failure and continue from last failing test next time",
+ )
+ group.addoption(
+ "--sw-skip",
+ "--stepwise-skip",
+ 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.",
+ )
+
+
+@pytest.hookimpl
+def pytest_configure(config: Config) -> None:
+ if config.option.stepwise_skip:
+ # allow --stepwise-skip to work on it's own merits.
+ config.option.stepwise = True
+ if config.getoption("stepwise"):
+ config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")
+
+
+def pytest_sessionfinish(session: Session) -> None:
+ if not session.config.getoption("stepwise"):
+ assert session.config.cache is not None
+ # Clear the list of failing tests if the plugin is not active.
+ session.config.cache.set(STEPWISE_CACHE_DIR, [])
+
+
+class StepwisePlugin:
+ def __init__(self, config: Config) -> None:
+ self.config = config
+ self.session: Optional[Session] = None
+ self.report_status = ""
+ assert config.cache is not None
+ self.cache: Cache = config.cache
+ self.lastfailed: Optional[str] = self.cache.get(STEPWISE_CACHE_DIR, None)
+ self.skip: bool = config.getoption("stepwise_skip")
+
+ def pytest_sessionstart(self, session: Session) -> None:
+ self.session = session
+
+ def pytest_collection_modifyitems(
+ self, config: Config, items: List[nodes.Item]
+ ) -> None:
+ if not self.lastfailed:
+ self.report_status = "no previously failed tests, not skipping."
+ return
+
+ # check all item nodes until we find a match on last failed
+ failed_index = None
+ for index, item in enumerate(items):
+ if item.nodeid == self.lastfailed:
+ failed_index = index
+ break
+
+ # If the previously failed test was not found among the test items,
+ # do not skip any tests.
+ if failed_index is None:
+ self.report_status = "previously failed test not found, not skipping."
+ else:
+ self.report_status = f"skipping {failed_index} already passed items."
+ deselected = items[:failed_index]
+ del items[:failed_index]
+ config.hook.pytest_deselected(items=deselected)
+
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
+ if report.failed:
+ if self.skip:
+ # Remove test from the failed ones (if it exists) and unset the skip option
+ # to make sure the following tests will not be skipped.
+ if report.nodeid == self.lastfailed:
+ self.lastfailed = None
+
+ self.skip = False
+ else:
+ # Mark test as the last failing and interrupt the test session.
+ self.lastfailed = report.nodeid
+ assert self.session is not None
+ self.session.shouldstop = (
+ "Test failed, continuing from this test next run."
+ )
+
+ else:
+ # If the test was actually run and did pass.
+ if report.when == "call":
+ # Remove test from the failed ones, if exists.
+ if report.nodeid == self.lastfailed:
+ self.lastfailed = None
+
+ def pytest_report_collectionfinish(self) -> Optional[str]:
+ if self.config.getoption("verbose") >= 0 and self.report_status:
+ return f"stepwise: {self.report_status}"
+ return None
+
+ def pytest_sessionfinish(self) -> None:
+ self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/terminal.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/terminal.py
new file mode 100644
index 0000000000..ccbd84d7d7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/terminal.py
@@ -0,0 +1,1394 @@
+"""Terminal reporting of the full testing process.
+
+This is a good source for looking at the various reporting hooks.
+"""
+import argparse
+import datetime
+import inspect
+import platform
+import sys
+import warnings
+from collections import Counter
+from functools import partial
+from pathlib import Path
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import ClassVar
+from typing import Dict
+from typing import Generator
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import TextIO
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+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.wcwidth import wcswidth
+from _pytest.compat import final
+from _pytest.config import _PluggyPlugin
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.nodes import Item
+from _pytest.nodes import Node
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+from _pytest.reports import BaseReport
+from _pytest.reports import CollectReport
+from _pytest.reports import TestReport
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from _pytest.main import Session
+
+
+REPORT_COLLECTING_RESOLUTION = 0.5
+
+KNOWN_TYPES = (
+ "failed",
+ "passed",
+ "skipped",
+ "deselected",
+ "xfailed",
+ "xpassed",
+ "warnings",
+ "error",
+)
+
+_REPORTCHARS_DEFAULT = "fE"
+
+
+class MoreQuietAction(argparse.Action):
+ """A modified copy of the argparse count action which counts down and updates
+ the legacy quiet attribute at the same time.
+
+ Used to unify verbosity handling.
+ """
+
+ def __init__(
+ self,
+ option_strings: Sequence[str],
+ dest: str,
+ default: object = None,
+ required: bool = False,
+ help: Optional[str] = None,
+ ) -> None:
+ super().__init__(
+ option_strings=option_strings,
+ dest=dest,
+ nargs=0,
+ default=default,
+ required=required,
+ help=help,
+ )
+
+ def __call__(
+ self,
+ parser: argparse.ArgumentParser,
+ namespace: argparse.Namespace,
+ values: Union[str, Sequence[object], None],
+ option_string: Optional[str] = None,
+ ) -> None:
+ new_count = getattr(namespace, self.dest, 0) - 1
+ setattr(namespace, self.dest, new_count)
+ # todo Deprecate config.quiet
+ namespace.quiet = getattr(namespace, "quiet", 0) + 1
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("terminal reporting", "reporting", after="general")
+ group._addoption(
+ "-v",
+ "--verbose",
+ action="count",
+ default=0,
+ dest="verbose",
+ help="increase verbosity.",
+ )
+ group._addoption(
+ "--no-header",
+ action="store_true",
+ default=False,
+ dest="no_header",
+ help="disable header",
+ )
+ group._addoption(
+ "--no-summary",
+ action="store_true",
+ default=False,
+ dest="no_summary",
+ help="disable summary",
+ )
+ group._addoption(
+ "-q",
+ "--quiet",
+ action=MoreQuietAction,
+ default=0,
+ dest="verbose",
+ help="decrease verbosity.",
+ )
+ group._addoption(
+ "--verbosity",
+ dest="verbose",
+ type=int,
+ default=0,
+ help="set verbosity. Default is 0.",
+ )
+ group._addoption(
+ "-r",
+ action="store",
+ dest="reportchars",
+ default=_REPORTCHARS_DEFAULT,
+ metavar="chars",
+ 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), "
+ "'N' can be used to reset the list. (default: 'fE').",
+ )
+ group._addoption(
+ "--disable-warnings",
+ "--disable-pytest-warnings",
+ default=False,
+ dest="disable_warnings",
+ action="store_true",
+ help="disable warnings summary",
+ )
+ group._addoption(
+ "-l",
+ "--showlocals",
+ action="store_true",
+ dest="showlocals",
+ default=False,
+ help="show locals in tracebacks (disabled by default).",
+ )
+ group._addoption(
+ "--tb",
+ metavar="style",
+ action="store",
+ dest="tbstyle",
+ default="auto",
+ choices=["auto", "long", "short", "no", "line", "native"],
+ help="traceback print mode (auto/long/short/line/native/no).",
+ )
+ group._addoption(
+ "--show-capture",
+ action="store",
+ dest="showcapture",
+ choices=["no", "stdout", "stderr", "log", "all"],
+ default="all",
+ help="Controls how captured stdout/stderr/log is shown on failed tests. "
+ "Default is 'all'.",
+ )
+ group._addoption(
+ "--fulltrace",
+ "--full-trace",
+ action="store_true",
+ default=False,
+ help="don't cut any tracebacks (default is to cut).",
+ )
+ group._addoption(
+ "--color",
+ metavar="color",
+ action="store",
+ dest="color",
+ default="auto",
+ choices=["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)",
+ )
+
+ parser.addini(
+ "console_output_style",
+ help='console output: "classic", or with additional progress information ("progress" (percentage) | "count").',
+ default="progress",
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ reporter = TerminalReporter(config, sys.stdout)
+ config.pluginmanager.register(reporter, "terminalreporter")
+ if config.option.debug or config.option.traceconfig:
+
+ def mywriter(tags, args):
+ msg = " ".join(map(str, args))
+ reporter.write_line("[traceconfig] " + msg)
+
+ config.trace.root.setprocessor("pytest:config", mywriter)
+
+
+def getreportopt(config: Config) -> str:
+ reportchars: str = config.option.reportchars
+
+ old_aliases = {"F", "S"}
+ reportopts = ""
+ for char in reportchars:
+ if char in old_aliases:
+ char = char.lower()
+ if char == "a":
+ reportopts = "sxXEf"
+ elif char == "A":
+ reportopts = "PpsxXEf"
+ elif char == "N":
+ reportopts = ""
+ elif char not in reportopts:
+ reportopts += char
+
+ if not config.option.disable_warnings and "w" not in reportopts:
+ reportopts = "w" + reportopts
+ elif config.option.disable_warnings and "w" in reportopts:
+ reportopts = reportopts.replace("w", "")
+
+ return reportopts
+
+
+@hookimpl(trylast=True) # after _pytest.runner
+def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]:
+ letter = "F"
+ if report.passed:
+ letter = "."
+ elif report.skipped:
+ letter = "s"
+
+ outcome: str = report.outcome
+ if report.when in ("collect", "setup", "teardown") and outcome == "failed":
+ outcome = "error"
+ letter = "E"
+
+ return outcome, letter, outcome.upper()
+
+
+@attr.s(auto_attribs=True)
+class WarningReport:
+ """Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
+
+ :ivar str message:
+ User friendly message about the warning.
+ :ivar str|None nodeid:
+ nodeid that generated the warning (see ``get_location``).
+ :ivar tuple fslocation:
+ File system location of the source of the warning (see ``get_location``).
+ """
+
+ message: str
+ nodeid: Optional[str] = None
+ fslocation: Optional[Tuple[str, int]] = None
+
+ count_towards_summary: ClassVar = True
+
+ def get_location(self, config: Config) -> Optional[str]:
+ """Return the more user-friendly information about the location of a warning, or None."""
+ if self.nodeid:
+ return self.nodeid
+ if self.fslocation:
+ filename, linenum = self.fslocation
+ relpath = bestrelpath(config.invocation_params.dir, absolutepath(filename))
+ return f"{relpath}:{linenum}"
+ return None
+
+
+@final
+class TerminalReporter:
+ def __init__(self, config: Config, file: Optional[TextIO] = None) -> None:
+ import _pytest.config
+
+ self.config = config
+ self._numcollected = 0
+ self._session: Optional[Session] = None
+ self._showfspath: Optional[bool] = None
+
+ self.stats: Dict[str, List[Any]] = {}
+ self._main_color: Optional[str] = None
+ self._known_types: Optional[List[str]] = None
+ self.startpath = config.invocation_params.dir
+ if file is None:
+ file = sys.stdout
+ self._tw = _pytest.config.create_terminal_writer(config, file)
+ self._screen_width = self._tw.fullwidth
+ self.currentfspath: Union[None, Path, str, int] = None
+ self.reportchars = getreportopt(config)
+ self.hasmarkup = self._tw.hasmarkup
+ self.isatty = file.isatty()
+ self._progress_nodeids_reported: Set[str] = set()
+ self._show_progress_info = self._determine_show_progress_info()
+ self._collect_report_last_write: Optional[float] = None
+ self._already_displayed_warnings: Optional[int] = None
+ self._keyboardinterrupt_memo: Optional[ExceptionRepr] = None
+
+ 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":
+ 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":
+ return "progress"
+ elif cfg == "count":
+ return "count"
+ else:
+ return False
+
+ @property
+ def verbosity(self) -> int:
+ verbosity: int = self.config.option.verbose
+ return verbosity
+
+ @property
+ def showheader(self) -> bool:
+ return self.verbosity >= 0
+
+ @property
+ def no_header(self) -> bool:
+ return bool(self.config.option.no_header)
+
+ @property
+ def no_summary(self) -> bool:
+ return bool(self.config.option.no_summary)
+
+ @property
+ def showfspath(self) -> bool:
+ if self._showfspath is None:
+ return self.verbosity >= 0
+ return self._showfspath
+
+ @showfspath.setter
+ def showfspath(self, value: Optional[bool]) -> None:
+ self._showfspath = value
+
+ @property
+ def showlongtestinfo(self) -> bool:
+ return self.verbosity > 0
+
+ def hasopt(self, char: str) -> bool:
+ char = {"xfailed": "x", "skipped": "s"}.get(char, char)
+ return char in self.reportchars
+
+ def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None:
+ fspath = self.config.rootpath / nodeid.split("::")[0]
+ if self.currentfspath is None or fspath != self.currentfspath:
+ if self.currentfspath is not None and self._show_progress_info:
+ self._write_progress_information_filling_space()
+ self.currentfspath = fspath
+ relfspath = bestrelpath(self.startpath, fspath)
+ self._tw.line()
+ self._tw.write(relfspath + " ")
+ self._tw.write(res, flush=True, **markup)
+
+ def write_ensure_prefix(self, prefix: str, extra: str = "", **kwargs) -> None:
+ if self.currentfspath != prefix:
+ self._tw.line()
+ self.currentfspath = prefix
+ self._tw.write(prefix)
+ if extra:
+ self._tw.write(extra, **kwargs)
+ self.currentfspath = -2
+
+ def ensure_newline(self) -> None:
+ if self.currentfspath:
+ self._tw.line()
+ self.currentfspath = None
+
+ def write(self, content: str, *, flush: bool = False, **markup: bool) -> None:
+ self._tw.write(content, flush=flush, **markup)
+
+ def flush(self) -> None:
+ self._tw.flush()
+
+ def write_line(self, line: Union[str, bytes], **markup: bool) -> None:
+ if not isinstance(line, str):
+ line = str(line, errors="replace")
+ self.ensure_newline()
+ self._tw.line(line, **markup)
+
+ def rewrite(self, line: str, **markup: bool) -> None:
+ """Rewinds the terminal cursor to the beginning and writes the given line.
+
+ :param erase:
+ If True, will also add spaces until the full terminal width to ensure
+ previous lines are properly erased.
+
+ The rest of the keyword arguments are markup instructions.
+ """
+ erase = markup.pop("erase", False)
+ if erase:
+ fill_count = self._tw.fullwidth - len(line) - 1
+ fill = " " * fill_count
+ else:
+ fill = ""
+ line = str(line)
+ self._tw.write("\r" + line + fill, **markup)
+
+ def write_sep(
+ self,
+ sep: str,
+ title: Optional[str] = None,
+ fullwidth: Optional[int] = None,
+ **markup: bool,
+ ) -> None:
+ self.ensure_newline()
+ self._tw.sep(sep, title, fullwidth, **markup)
+
+ def section(self, title: str, sep: str = "=", **kw: bool) -> None:
+ self._tw.sep(sep, title, **kw)
+
+ def line(self, msg: str, **kw: bool) -> None:
+ self._tw.line(msg, **kw)
+
+ def _add_stats(self, category: str, items: Sequence[Any]) -> None:
+ set_main_color = category not in self.stats
+ self.stats.setdefault(category, []).extend(items)
+ if set_main_color:
+ self._set_main_color()
+
+ def pytest_internalerror(self, excrepr: ExceptionRepr) -> bool:
+ for line in str(excrepr).split("\n"):
+ self.write_line("INTERNALERROR> " + line)
+ return True
+
+ def pytest_warning_recorded(
+ self,
+ warning_message: warnings.WarningMessage,
+ nodeid: str,
+ ) -> None:
+ from _pytest.warnings import warning_record_to_str
+
+ fslocation = warning_message.filename, warning_message.lineno
+ message = warning_record_to_str(warning_message)
+
+ warning_report = WarningReport(
+ fslocation=fslocation, message=message, nodeid=nodeid
+ )
+ self._add_stats("warnings", [warning_report])
+
+ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
+ if self.config.option.traceconfig:
+ msg = f"PLUGIN registered: {plugin}"
+ # XXX This event may happen during setup/teardown time
+ # which unfortunately captures our output here
+ # which garbles our output if we use self.write_line.
+ self.write_line(msg)
+
+ def pytest_deselected(self, items: Sequence[Item]) -> None:
+ self._add_stats("deselected", items)
+
+ def pytest_runtest_logstart(
+ self, nodeid: str, location: Tuple[str, Optional[int], str]
+ ) -> None:
+ # Ensure that the path is printed before the
+ # 1st test of a module starts running.
+ if self.showlongtestinfo:
+ line = self._locationline(nodeid, *location)
+ self.write_ensure_prefix(line, "")
+ self.flush()
+ elif self.showfspath:
+ self.write_fspath_result(nodeid, "")
+ self.flush()
+
+ 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
+ if not isinstance(word, tuple):
+ markup = None
+ else:
+ word, markup = word
+ self._add_stats(category, [rep])
+ if not letter and not word:
+ # Probably passed setup/teardown.
+ return
+ running_xdist = hasattr(rep, "node")
+ if markup is None:
+ was_xfail = hasattr(report, "wasxfail")
+ if rep.passed and not was_xfail:
+ markup = {"green": True}
+ elif rep.passed and was_xfail:
+ markup = {"yellow": True}
+ elif rep.failed:
+ markup = {"red": True}
+ elif rep.skipped:
+ markup = {"yellow": True}
+ else:
+ markup = {}
+ if self.verbosity <= 0:
+ self._tw.write(letter, **markup)
+ else:
+ self._progress_nodeids_reported.add(rep.nodeid)
+ line = self._locationline(rep.nodeid, *rep.location)
+ 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._show_progress_info:
+ self._write_progress_information_filling_space()
+ else:
+ self.ensure_newline()
+ self._tw.write("[%s]" % rep.node.gateway.id)
+ if self._show_progress_info:
+ self._tw.write(
+ self._get_progress_information_message() + " ", cyan=True
+ )
+ else:
+ self._tw.write(" ")
+ self._tw.write(word, **markup)
+ self._tw.write(" " + line)
+ self.currentfspath = -2
+ self.flush()
+
+ @property
+ def _is_last_item(self) -> bool:
+ assert self._session is not None
+ return len(self._progress_nodeids_reported) == self._session.testscollected
+
+ def pytest_runtest_logfinish(self, nodeid: str) -> None:
+ assert self._session
+ if self.verbosity <= 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}]")
+ else:
+ progress_length = len(" [100%]")
+
+ self._progress_nodeids_reported.add(nodeid)
+
+ if self._is_last_item:
+ self._write_progress_information_filling_space()
+ else:
+ main_color, _ = self._get_main_color()
+ w = self._width_of_current_line
+ past_edge = w + progress_length + 1 >= self._screen_width
+ if past_edge:
+ msg = self._get_progress_information_message()
+ self._tw.write(msg + "\n", **{main_color: True})
+
+ def _get_progress_information_message(self) -> str:
+ assert self._session
+ collected = self._session.testscollected
+ if self._show_progress_info == "count":
+ if collected:
+ progress = self._progress_nodeids_reported
+ counter_format = f"{{:{len(str(collected))}d}}"
+ format_string = f" [{counter_format}/{{}}]"
+ return format_string.format(len(progress), collected)
+ return f" [ {collected} / {collected} ]"
+ else:
+ if collected:
+ return " [{:3d}%]".format(
+ len(self._progress_nodeids_reported) * 100 // collected
+ )
+ return " [100%]"
+
+ def _write_progress_information_filling_space(self) -> None:
+ color, _ = self._get_main_color()
+ msg = self._get_progress_information_message()
+ w = self._width_of_current_line
+ fill = self._tw.fullwidth - w - 1
+ self.write(msg.rjust(fill), flush=True, **{color: True})
+
+ @property
+ def _width_of_current_line(self) -> int:
+ """Return the width of the current line."""
+ return self._tw.width_of_current_line
+
+ def pytest_collection(self) -> None:
+ if self.isatty:
+ if self.config.option.verbose >= 0:
+ self.write("collecting ... ", flush=True, bold=True)
+ self._collect_report_last_write = timing.time()
+ elif self.config.option.verbose >= 1:
+ self.write("collecting ... ", flush=True, bold=True)
+
+ def pytest_collectreport(self, report: CollectReport) -> None:
+ if report.failed:
+ self._add_stats("error", [report])
+ elif report.skipped:
+ self._add_stats("skipped", [report])
+ items = [x for x in report.result if isinstance(x, Item)]
+ self._numcollected += len(items)
+ if self.isatty:
+ self.report_collect()
+
+ def report_collect(self, final: bool = False) -> None:
+ if self.config.option.verbose < 0:
+ return
+
+ if not final:
+ # Only write "collecting" report every 0.5s.
+ t = timing.time()
+ if (
+ self._collect_report_last_write is not None
+ and self._collect_report_last_write > t - REPORT_COLLECTING_RESOLUTION
+ ):
+ return
+ self._collect_report_last_write = t
+
+ errors = len(self.stats.get("error", []))
+ skipped = len(self.stats.get("skipped", []))
+ deselected = len(self.stats.get("deselected", []))
+ selected = self._numcollected - errors - skipped - deselected
+ line = "collected " if final else "collecting "
+ line += (
+ str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
+ )
+ if errors:
+ line += " / %d error%s" % (errors, "s" if errors != 1 else "")
+ if deselected:
+ line += " / %d deselected" % deselected
+ if skipped:
+ line += " / %d skipped" % skipped
+ if self._numcollected > selected > 0:
+ line += " / %d selected" % selected
+ if self.isatty:
+ self.rewrite(line, bold=True, erase=True)
+ if final:
+ self.write("\n")
+ else:
+ self.write_line(line)
+
+ @hookimpl(trylast=True)
+ def pytest_sessionstart(self, session: "Session") -> None:
+ self._session = session
+ self._sessionstarttime = timing.time()
+ if not self.showheader:
+ return
+ self.write_sep("=", "test session starts", bold=True)
+ verinfo = platform.python_version()
+ if not self.no_header:
+ msg = f"platform {sys.platform} -- Python {verinfo}"
+ pypy_version_info = getattr(sys, "pypy_version_info", None)
+ 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__
+ )
+ if (
+ self.verbosity > 0
+ or self.config.option.debug
+ or getattr(self.config.option, "pastebin", None)
+ ):
+ msg += " -- " + str(sys.executable)
+ self.write_line(msg)
+ lines = self.config.hook.pytest_report_header(
+ config=self.config, start_path=self.startpath
+ )
+ self._write_report_lines_from_hooks(lines)
+
+ def _write_report_lines_from_hooks(
+ self, lines: Sequence[Union[str, Sequence[str]]]
+ ) -> None:
+ for line_or_lines in reversed(lines):
+ if isinstance(line_or_lines, str):
+ self.write_line(line_or_lines)
+ else:
+ for line in line_or_lines:
+ self.write_line(line)
+
+ def pytest_report_header(self, config: Config) -> List[str]:
+ line = "rootdir: %s" % 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 = [line]
+
+ plugininfo = config.pluginmanager.list_plugin_distinfo()
+ if plugininfo:
+ result.append("plugins: %s" % ", ".join(_plugin_nameversions(plugininfo)))
+ return result
+
+ def pytest_collection_finish(self, session: "Session") -> None:
+ self.report_collect(True)
+
+ lines = self.config.hook.pytest_report_collectionfinish(
+ config=self.config,
+ start_path=self.startpath,
+ items=session.items,
+ )
+ self._write_report_lines_from_hooks(lines)
+
+ if self.config.getoption("collectonly"):
+ if session.items:
+ if self.config.option.verbose > -1:
+ self._tw.line("")
+ self._printcollecteditems(session.items)
+
+ failed = self.stats.get("failed")
+ if failed:
+ self._tw.sep("!", "collection failures")
+ for rep in failed:
+ rep.toterminal(self._tw)
+
+ def _printcollecteditems(self, items: Sequence[Item]) -> None:
+ if self.config.option.verbose < 0:
+ if self.config.option.verbose < -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))
+ else:
+ for item in items:
+ self._tw.line(item.nodeid)
+ return
+ stack: List[Node] = []
+ indent = ""
+ for item in items:
+ needed_collectors = item.listchain()[1:] # strip root node
+ while stack:
+ if stack == needed_collectors[: len(stack)]:
+ break
+ stack.pop()
+ for col in needed_collectors[len(stack) :]:
+ stack.append(col)
+ indent = (len(stack) - 1) * " "
+ self._tw.line(f"{indent}{col}")
+ if self.config.option.verbose >= 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)
+ def pytest_sessionfinish(
+ self, session: "Session", exitstatus: Union[int, ExitCode]
+ ):
+ outcome = yield
+ outcome.get_result()
+ self._tw.line("")
+ summary_exit_codes = (
+ ExitCode.OK,
+ ExitCode.TESTS_FAILED,
+ ExitCode.INTERRUPTED,
+ ExitCode.USAGE_ERROR,
+ ExitCode.NO_TESTS_COLLECTED,
+ )
+ if exitstatus in summary_exit_codes and not self.no_summary:
+ self.config.hook.pytest_terminal_summary(
+ terminalreporter=self, exitstatus=exitstatus, config=self.config
+ )
+ if session.shouldfail:
+ self.write_sep("!", str(session.shouldfail), red=True)
+ if exitstatus == ExitCode.INTERRUPTED:
+ self._report_keyboardinterrupt()
+ self._keyboardinterrupt_memo = None
+ elif session.shouldstop:
+ self.write_sep("!", str(session.shouldstop), red=True)
+ self.summary_stats()
+
+ @hookimpl(hookwrapper=True)
+ def pytest_terminal_summary(self) -> Generator[None, None, None]:
+ self.summary_errors()
+ self.summary_failures()
+ self.summary_warnings()
+ self.summary_passes()
+ yield
+ 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)
+
+ def pytest_unconfigure(self) -> None:
+ if self._keyboardinterrupt_memo is not None:
+ self._report_keyboardinterrupt()
+
+ def _report_keyboardinterrupt(self) -> None:
+ excrepr = self._keyboardinterrupt_memo
+ assert excrepr is not None
+ assert excrepr.reprcrash is not None
+ msg = excrepr.reprcrash.message
+ self.write_sep("!", msg)
+ if "KeyboardInterrupt" in msg:
+ if self.config.option.fulltrace:
+ excrepr.toterminal(self._tw)
+ else:
+ excrepr.reprcrash.toterminal(self._tw)
+ self._tw.line(
+ "(to show a full traceback on KeyboardInterrupt use --full-trace)",
+ yellow=True,
+ )
+
+ def _locationline(
+ self, nodeid: str, fspath: str, lineno: Optional[int], domain: str
+ ) -> str:
+ def mkrel(nodeid: str) -> str:
+ line = self.config.cwd_relative_nodeid(nodeid)
+ if domain and line.endswith(domain):
+ line = line[: -len(domain)]
+ values = domain.split("[")
+ values[0] = values[0].replace(".", "::") # don't replace '.' in params
+ line += "[".join(values)
+ return line
+
+ # collect_fspath comes from testid which has a "/"-normalized path.
+ if fspath:
+ res = mkrel(nodeid)
+ if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(
+ "\\", nodes.SEP
+ ):
+ res += " <- " + bestrelpath(self.startpath, Path(fspath))
+ else:
+ res = "[location]"
+ return res + " "
+
+ def _getfailureheadline(self, rep):
+ head_line = rep.head_line
+ if head_line:
+ return head_line
+ return "test session" # XXX?
+
+ def _getcrashline(self, rep):
+ try:
+ return str(rep.longrepr.reprcrash)
+ except AttributeError:
+ try:
+ return str(rep.longrepr)[:50]
+ except AttributeError:
+ return ""
+
+ #
+ # Summaries for sessionfinish.
+ #
+ def getreports(self, name: str):
+ return [x for x in self.stats.get(name, ()) if not hasattr(x, "_pdbshown")]
+
+ def summary_warnings(self) -> None:
+ if self.hasopt("w"):
+ all_warnings: Optional[List[WarningReport]] = self.stats.get("warnings")
+ if not all_warnings:
+ return
+
+ final = self._already_displayed_warnings is not None
+ if final:
+ warning_reports = all_warnings[self._already_displayed_warnings :]
+ else:
+ warning_reports = all_warnings
+ self._already_displayed_warnings = len(warning_reports)
+ if not warning_reports:
+ return
+
+ reports_grouped_by_message: Dict[str, List[WarningReport]] = {}
+ for wr in warning_reports:
+ reports_grouped_by_message.setdefault(wr.message, []).append(wr)
+
+ def collapsed_location_report(reports: List[WarningReport]) -> str:
+ locations = []
+ for w in reports:
+ location = w.get_location(self.config)
+ if location:
+ locations.append(location)
+
+ if len(locations) < 10:
+ return "\n".join(map(str, locations))
+
+ counts_by_filename = Counter(
+ str(loc).split("::", 1)[0] for loc in locations
+ )
+ return "\n".join(
+ "{}: {} warning{}".format(k, v, "s" if v > 1 else "")
+ for k, v in counts_by_filename.items()
+ )
+
+ title = "warnings summary (final)" if final else "warnings summary"
+ self.write_sep("=", title, yellow=True, bold=False)
+ for message, message_reports in reports_grouped_by_message.items():
+ maybe_location = collapsed_location_report(message_reports)
+ if maybe_location:
+ self._tw.line(maybe_location)
+ lines = message.splitlines()
+ indented = "\n".join(" " + x for x in lines)
+ message = indented.rstrip()
+ else:
+ message = message.rstrip()
+ self._tw.line(message)
+ self._tw.line()
+ self._tw.line(
+ "-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html"
+ )
+
+ def summary_passes(self) -> None:
+ if self.config.option.tbstyle != "no":
+ if self.hasopt("P"):
+ reports: List[TestReport] = self.getreports("passed")
+ if not reports:
+ return
+ self.write_sep("=", "PASSES")
+ for rep in reports:
+ if rep.sections:
+ msg = self._getfailureheadline(rep)
+ self.write_sep("_", msg, green=True, bold=True)
+ self._outrep_summary(rep)
+ self._handle_teardown_sections(rep.nodeid)
+
+ def _get_teardown_reports(self, nodeid: str) -> List[TestReport]:
+ reports = self.getreports("")
+ return [
+ report
+ for report in reports
+ if report.when == "teardown" and report.nodeid == nodeid
+ ]
+
+ def _handle_teardown_sections(self, nodeid: str) -> None:
+ for report in self._get_teardown_reports(nodeid):
+ self.print_teardown_sections(report)
+
+ def print_teardown_sections(self, rep: TestReport) -> None:
+ showcapture = self.config.option.showcapture
+ if showcapture == "no":
+ return
+ for secname, content in rep.sections:
+ if showcapture != "all" and showcapture not in secname:
+ continue
+ if "teardown" in secname:
+ self._tw.sep("-", secname)
+ if content[-1:] == "\n":
+ content = content[:-1]
+ self._tw.line(content)
+
+ def summary_failures(self) -> 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)
+
+ def summary_errors(self) -> None:
+ if self.config.option.tbstyle != "no":
+ reports: List[BaseReport] = self.getreports("error")
+ if not reports:
+ return
+ self.write_sep("=", "ERRORS")
+ for rep in self.stats["error"]:
+ msg = self._getfailureheadline(rep)
+ if rep.when == "collect":
+ msg = "ERROR collecting " + msg
+ else:
+ msg = f"ERROR at {rep.when} of {msg}"
+ self.write_sep("_", msg, red=True, bold=True)
+ self._outrep_summary(rep)
+
+ def _outrep_summary(self, rep: BaseReport) -> None:
+ rep.toterminal(self._tw)
+ showcapture = self.config.option.showcapture
+ if showcapture == "no":
+ return
+ for secname, content in rep.sections:
+ if showcapture != "all" and showcapture not in secname:
+ continue
+ self._tw.sep("-", secname)
+ if content[-1:] == "\n":
+ content = content[:-1]
+ self._tw.line(content)
+
+ def summary_stats(self) -> None:
+ if self.verbosity < -1:
+ return
+
+ session_duration = timing.time() - self._sessionstarttime
+ (parts, main_color) = self.build_summary_stats_line()
+ line_parts = []
+
+ display_sep = self.verbosity >= 0
+ if display_sep:
+ fullwidth = self._tw.fullwidth
+ for text, markup in parts:
+ with_markup = self._tw.markup(text, **markup)
+ if display_sep:
+ fullwidth += len(with_markup) - len(text)
+ line_parts.append(with_markup)
+ msg = ", ".join(line_parts)
+
+ main_markup = {main_color: True}
+ duration = f" in {format_session_duration(session_duration)}"
+ duration_with_markup = self._tw.markup(duration, **main_markup)
+ if display_sep:
+ fullwidth += len(duration_with_markup) - len(duration)
+ msg += duration_with_markup
+
+ if display_sep:
+ markup_for_end_sep = self._tw.markup("", **main_markup)
+ if markup_for_end_sep.endswith("\x1b[0m"):
+ markup_for_end_sep = markup_for_end_sep[:-4]
+ fullwidth += len(markup_for_end_sep)
+ msg += markup_for_end_sep
+
+ if display_sep:
+ self.write_sep("=", msg, fullwidth=fullwidth, **main_markup)
+ else:
+ self.write_line(msg, **main_markup)
+
+ def short_test_summary(self) -> None:
+ if not self.reportchars:
+ return
+
+ def show_simple(stat, lines: List[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)
+ 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}")
+ reason = rep.wasxfail
+ if reason:
+ lines.append(" " + str(reason))
+
+ 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)
+ reason = rep.wasxfail
+ lines.append(f"{verbose_word} {pos} {reason}")
+
+ def show_skipped(lines: List[str]) -> None:
+ skipped: List[CollectReport] = self.stats.get("skipped", [])
+ fskips = _folded_skips(self.startpath, skipped) if skipped else []
+ if not fskips:
+ return
+ verbose_word = skipped[0]._get_verbose_word(self.config)
+ for num, fspath, lineno, reason in fskips:
+ if reason.startswith("Skipped: "):
+ reason = reason[9:]
+ if lineno is not None:
+ lines.append(
+ "%s [%d] %s:%d: %s"
+ % (verbose_word, num, fspath, lineno, reason)
+ )
+ else:
+ lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason))
+
+ REPORTCHAR_ACTIONS: Mapping[str, Callable[[List[str]], None]] = {
+ "x": show_xfailed,
+ "X": show_xpassed,
+ "f": partial(show_simple, "failed"),
+ "s": show_skipped,
+ "p": partial(show_simple, "passed"),
+ "E": partial(show_simple, "error"),
+ }
+
+ lines: List[str] = []
+ for char in self.reportchars:
+ action = REPORTCHAR_ACTIONS.get(char)
+ if action: # skipping e.g. "P" (passed with output) here.
+ action(lines)
+
+ if lines:
+ self.write_sep("=", "short test summary info")
+ for line in lines:
+ self.write_line(line)
+
+ def _get_main_color(self) -> Tuple[str, List[str]]:
+ if self._main_color is None or self._known_types is None or self._is_last_item:
+ self._set_main_color()
+ assert self._main_color
+ assert self._known_types
+ return self._main_color, self._known_types
+
+ def _determine_main_color(self, unknown_type_seen: bool) -> str:
+ stats = self.stats
+ if "failed" in stats or "error" in stats:
+ main_color = "red"
+ elif "warnings" in stats or "xpassed" in stats or unknown_type_seen:
+ main_color = "yellow"
+ elif "passed" in stats or not self._is_last_item:
+ main_color = "green"
+ else:
+ main_color = "yellow"
+ return main_color
+
+ def _set_main_color(self) -> None:
+ unknown_types: List[str] = []
+ for found_type in self.stats.keys():
+ 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)
+ self._known_types = list(KNOWN_TYPES) + unknown_types
+ self._main_color = self._determine_main_color(bool(unknown_types))
+
+ def build_summary_stats_line(self) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
+ """
+ Build the parts used in the last summary stats line.
+
+ The summary stats line is the line shown at the end, "=== 12 passed, 2 errors in Xs===".
+
+ This function builds a list of the "parts" that make up for the text in that line, in
+ the example above it would be:
+
+ [
+ ("12 passed", {"green": True}),
+ ("2 errors", {"red": True}
+ ]
+
+ That last dict for each line is a "markup dictionary", used by TerminalWriter to
+ color output.
+
+ The final color of the line is also determined by this function, and is the second
+ element of the returned tuple.
+ """
+ if self.config.getoption("collectonly"):
+ return self._build_collect_only_summary_stats_line()
+ else:
+ return self._build_normal_summary_stats_line()
+
+ def _get_reports_to_display(self, key: str) -> List[Any]:
+ """Get test/collection reports for the given status key, such as `passed` or `error`."""
+ reports = self.stats.get(key, [])
+ return [x for x in reports if getattr(x, "count_towards_summary", True)]
+
+ def _build_normal_summary_stats_line(
+ self,
+ ) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
+ main_color, known_types = self._get_main_color()
+ parts = []
+
+ for key in known_types:
+ reports = self._get_reports_to_display(key)
+ if reports:
+ count = len(reports)
+ color = _color_for_type.get(key, _color_for_type_default)
+ markup = {color: True, "bold": color == main_color}
+ parts.append(("%d %s" % pluralize(count, key), markup))
+
+ if not parts:
+ parts = [("no tests ran", {_color_for_type_default: True})]
+
+ return parts, main_color
+
+ def _build_collect_only_summary_stats_line(
+ self,
+ ) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
+ deselected = len(self._get_reports_to_display("deselected"))
+ errors = len(self._get_reports_to_display("error"))
+
+ if self._numcollected == 0:
+ parts = [("no tests collected", {"yellow": True})]
+ main_color = "yellow"
+
+ elif deselected == 0:
+ main_color = "green"
+ collected_output = "%d %s collected" % pluralize(self._numcollected, "test")
+ parts = [(collected_output, {main_color: True})]
+ else:
+ all_tests_were_deselected = self._numcollected == deselected
+ if all_tests_were_deselected:
+ main_color = "yellow"
+ collected_output = f"no tests collected ({deselected} deselected)"
+ else:
+ main_color = "green"
+ selected = self._numcollected - deselected
+ collected_output = f"{selected}/{self._numcollected} tests collected ({deselected} deselected)"
+
+ parts = [(collected_output, {main_color: True})]
+
+ if errors:
+ main_color = _color_for_type["error"]
+ parts += [("%d %s" % pluralize(errors, "error"), {main_color: True})]
+
+ return parts, main_color
+
+
+def _get_pos(config: Config, rep: BaseReport):
+ nodeid = config.cwd_relative_nodeid(rep.nodeid)
+ return nodeid
+
+
+def _format_trimmed(format: str, msg: str, available_width: int) -> Optional[str]:
+ """Format msg into format, ellipsizing it if doesn't fit in available_width.
+
+ Returns None if even the ellipsis can't fit.
+ """
+ # Only use the first line.
+ i = msg.find("\n")
+ if i != -1:
+ msg = msg[:i]
+
+ ellipsis = "..."
+ format_width = wcswidth(format.format(""))
+ if format_width + len(ellipsis) > available_width:
+ return None
+
+ if format_width + wcswidth(msg) > available_width:
+ available_width -= len(ellipsis)
+ msg = msg[:available_width]
+ while format_width + wcswidth(msg) > available_width:
+ msg = msg[:-1]
+ msg += ellipsis
+
+ return format.format(msg)
+
+
+def _get_line_with_reprcrash_message(
+ config: Config, rep: BaseReport, termwidth: int
+) -> str:
+ """Get summary line for a report, trying to add reprcrash message."""
+ verbose_word = rep._get_verbose_word(config)
+ pos = _get_pos(config, rep)
+
+ line = f"{verbose_word} {pos}"
+ line_width = wcswidth(line)
+
+ try:
+ # Type ignored intentionally -- possible AttributeError expected.
+ msg = rep.longrepr.reprcrash.message # type: ignore[union-attr]
+ except AttributeError:
+ pass
+ else:
+ available_width = termwidth - line_width
+ msg = _format_trimmed(" - {}", msg, available_width)
+ if msg is not None:
+ line += msg
+
+ return line
+
+
+def _folded_skips(
+ startpath: Path,
+ skipped: Sequence[CollectReport],
+) -> List[Tuple[int, str, Optional[int], str]]:
+ d: Dict[Tuple[str, Optional[int], str], List[CollectReport]] = {}
+ for event in skipped:
+ assert event.longrepr is not None
+ assert isinstance(event.longrepr, tuple), (event, event.longrepr)
+ assert len(event.longrepr) == 3, (event, event.longrepr)
+ fspath, lineno, reason = event.longrepr
+ # For consistency, report all fspaths in relative form.
+ fspath = bestrelpath(startpath, Path(fspath))
+ keywords = getattr(event, "keywords", {})
+ # Folding reports with global pytestmark variable.
+ # This is a workaround, because for now we cannot identify the scope of a skip marker
+ # TODO: Revisit after marks scope would be fixed.
+ if (
+ event.when == "setup"
+ and "skip" in keywords
+ and "pytestmark" not in keywords
+ ):
+ key: Tuple[str, Optional[int], str] = (fspath, None, reason)
+ else:
+ key = (fspath, lineno, reason)
+ d.setdefault(key, []).append(event)
+ values: List[Tuple[int, str, Optional[int], str]] = []
+ for key, events in d.items():
+ values.append((len(events), *key))
+ return values
+
+
+_color_for_type = {
+ "failed": "red",
+ "error": "red",
+ "warnings": "yellow",
+ "passed": "green",
+}
+_color_for_type_default = "yellow"
+
+
+def pluralize(count: int, noun: str) -> Tuple[int, str]:
+ # No need to pluralize words such as `failed` or `passed`.
+ if noun not in ["error", "warnings", "test"]:
+ return count, noun
+
+ # The `warnings` key is plural. To avoid API breakage, we keep it that way but
+ # set it to singular here so we can determine plurality in the same way as we do
+ # for `error`.
+ noun = noun.replace("warnings", "warning")
+
+ return count, noun + "s" if count != 1 else noun
+
+
+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)
+ # Questionable convenience, but it keeps things short.
+ if name.startswith("pytest-"):
+ name = name[7:]
+ # We decided to print python package names they can have more than one plugin.
+ if name not in values:
+ values.append(name)
+ return values
+
+
+def format_session_duration(seconds: float) -> str:
+ """Format the given seconds in a human readable manner to show in the final summary."""
+ if seconds < 60:
+ return f"{seconds:.2f}s"
+ else:
+ dt = datetime.timedelta(seconds=int(seconds))
+ return f"{seconds:.2f}s ({dt})"
+
+
+def _get_raw_skip_reason(report: TestReport) -> str:
+ """Get the reason string of a skip/xfail/xpass test report.
+
+ The string is just the part given by the user.
+ """
+ if hasattr(report, "wasxfail"):
+ reason = cast(str, report.wasxfail)
+ if reason.startswith("reason: "):
+ reason = reason[len("reason: ") :]
+ return reason
+ else:
+ assert report.skipped
+ assert isinstance(report.longrepr, tuple)
+ _, _, reason = report.longrepr
+ if reason.startswith("Skipped: "):
+ reason = reason[len("Skipped: ") :]
+ elif reason == "Skipped":
+ reason = ""
+ return reason
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/threadexception.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/threadexception.py
new file mode 100644
index 0000000000..43341e739a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/threadexception.py
@@ -0,0 +1,88 @@
+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 pytest
+
+
+# Copied from cpython/Lib/test/support/threading_helper.py, with modifications.
+class catch_threading_exception:
+ """Context manager catching threading.Thread exception using
+ threading.excepthook.
+
+ Storing exc_value using a custom hook can create a reference cycle. The
+ reference cycle is broken explicitly when the context manager exits.
+
+ Storing thread using a custom hook can resurrect it if it is set to an
+ object which is being finalized. Exiting the context manager clears the
+ stored object.
+
+ Usage:
+ with threading_helper.catch_threading_exception() as cm:
+ # code spawning a thread which raises an exception
+ ...
+ # check the thread exception: use cm.args
+ ...
+ # cm.args attribute no longer exists at this point
+ # (to break a reference cycle)
+ """
+
+ def __init__(self) -> None:
+ self.args: Optional["threading.ExceptHookArgs"] = None
+ self._old_hook: Optional[Callable[["threading.ExceptHookArgs"], Any]] = None
+
+ def _hook(self, args: "threading.ExceptHookArgs") -> None:
+ self.args = args
+
+ def __enter__(self) -> "catch_threading_exception":
+ self._old_hook = threading.excepthook
+ threading.excepthook = self._hook
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ assert self._old_hook is not None
+ threading.excepthook = self._old_hook
+ self._old_hook = None
+ del self.args
+
+
+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,
+ )
+ )
+ warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))
+
+
+@pytest.hookimpl(hookwrapper=True, trylast=True)
+def pytest_runtest_setup() -> Generator[None, None, None]:
+ yield from thread_exception_runtest_hook()
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_runtest_call() -> Generator[None, None, None]:
+ yield from thread_exception_runtest_hook()
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_runtest_teardown() -> Generator[None, None, None]:
+ yield from thread_exception_runtest_hook()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/timing.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/timing.py
new file mode 100644
index 0000000000..925163a585
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/timing.py
@@ -0,0 +1,12 @@
+"""Indirection for time functions.
+
+We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect
+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/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/tmpdir.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/tmpdir.py
new file mode 100644
index 0000000000..f901fd5727
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/tmpdir.py
@@ -0,0 +1,211 @@
+"""Support for providing temporary directories to test functions."""
+import os
+import re
+import sys
+import tempfile
+from pathlib import Path
+from typing import Optional
+
+import attr
+
+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.config import Config
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.monkeypatch import MonkeyPatch
+
+
+@final
+@attr.s(init=False)
+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])
+
+ def __init__(
+ self,
+ given_basetemp: Optional[Path],
+ trace,
+ basetemp: Optional[Path] = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ if given_basetemp is None:
+ self._given_basetemp = None
+ else:
+ # Use os.path.abspath() to get absolute path instead of resolve() as it
+ # does not work the same in all platforms (see #4427).
+ # 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._basetemp = basetemp
+
+ @classmethod
+ def from_config(
+ cls,
+ config: Config,
+ *,
+ _ispytest: bool = False,
+ ) -> "TempPathFactory":
+ """Create a factory according to pytest configuration.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ return cls(
+ given_basetemp=config.option.basetemp,
+ trace=config.trace.get("tmpdir"),
+ _ispytest=True,
+ )
+
+ def _ensure_relative_to_basetemp(self, basename: str) -> str:
+ basename = os.path.normpath(basename)
+ if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp():
+ raise ValueError(f"{basename} is not a normalized and relative path")
+ return basename
+
+ def mktemp(self, basename: str, numbered: bool = True) -> Path:
+ """Create a new temporary directory managed by the factory.
+
+ :param basename:
+ Directory base name, must be a relative path.
+
+ :param numbered:
+ If ``True``, ensure the directory is unique by adding a numbered
+ suffix greater than any existing one: ``basename="foo-"`` and ``numbered=True``
+ means that this function will create directories named ``"foo-0"``,
+ ``"foo-1"``, ``"foo-2"`` and so on.
+
+ :returns:
+ The path to the new directory.
+ """
+ basename = self._ensure_relative_to_basetemp(basename)
+ if not numbered:
+ p = self.getbasetemp().joinpath(basename)
+ p.mkdir(mode=0o700)
+ else:
+ p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700)
+ self._trace("mktemp", p)
+ return p
+
+ def getbasetemp(self) -> Path:
+ """Return the base temporary directory, creating it if needed."""
+ if self._basetemp is not None:
+ return self._basetemp
+
+ if self._given_basetemp is not None:
+ basetemp = self._given_basetemp
+ if basetemp.exists():
+ rm_rf(basetemp)
+ basetemp.mkdir(mode=0o700)
+ basetemp = basetemp.resolve()
+ else:
+ from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
+ temproot = Path(from_env or tempfile.gettempdir()).resolve()
+ user = get_user() or "unknown"
+ # use a sub-directory in the temproot to speed-up
+ # make_numbered_dir() call
+ rootdir = temproot.joinpath(f"pytest-of-{user}")
+ try:
+ rootdir.mkdir(mode=0o700, exist_ok=True)
+ except OSError:
+ # getuser() likely returned illegal characters for the platform, use unknown back off mechanism
+ rootdir = temproot.joinpath("pytest-of-unknown")
+ rootdir.mkdir(mode=0o700, exist_ok=True)
+ # Because we use exist_ok=True with a predictable name, make sure
+ # we are the owners, to prevent any funny business (on unix, where
+ # temproot is usually shared).
+ # 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()
+ 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)
+ basetemp = make_numbered_dir_with_cleanup(
+ prefix="pytest-",
+ root=rootdir,
+ keep=3,
+ lock_timeout=LOCK_TIMEOUT,
+ mode=0o700,
+ )
+ assert basetemp is not None, basetemp
+ self._basetemp = basetemp
+ self._trace("new basetemp", basetemp)
+ return basetemp
+
+
+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:
+ return getpass.getuser()
+ except (ImportError, KeyError):
+ return None
+
+
+def pytest_configure(config: Config) -> None:
+ """Create a TempPathFactory and attach it to the config object.
+
+ This is to comply with existing plugins which expect the handler to be
+ available at pytest_configure time, but ideally should be moved entirely
+ to the tmp_path_factory session fixture.
+ """
+ mp = MonkeyPatch()
+ config.add_cleanup(mp.undo)
+ _tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True)
+ mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False)
+
+
+@fixture(scope="session")
+def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
+ """Return a :class:`pytest.TempPathFactory` instance for the test session."""
+ # Set dynamically by pytest_configure() above.
+ return request.config._tmp_path_factory # type: ignore
+
+
+def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
+ name = request.node.name
+ name = re.sub(r"[\W]", "_", name)
+ MAXVAL = 30
+ name = name[:MAXVAL]
+ return factory.mktemp(name, numbered=True)
+
+
+@fixture
+def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
+ """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`.
+
+ The returned object is a :class:`pathlib.Path` object.
+ """
+
+ return _mk_tmp(request, tmp_path_factory)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/unittest.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/unittest.py
new file mode 100644
index 0000000000..0315168b04
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/unittest.py
@@ -0,0 +1,414 @@
+"""Discover and run std-library "unittest" style tests."""
+import sys
+import traceback
+import types
+from typing import Any
+from typing import Callable
+from typing import Generator
+from typing import Iterable
+from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import Type
+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
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+from _pytest.outcomes import exit
+from _pytest.outcomes import fail
+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.runner import CallInfo
+from _pytest.scope import Scope
+
+if TYPE_CHECKING:
+ import unittest
+ import twisted.trial.unittest
+
+ _SysExcInfoType = Union[
+ Tuple[Type[BaseException], BaseException, types.TracebackType],
+ Tuple[None, None, None],
+ ]
+
+
+def pytest_pycollect_makeitem(
+ collector: PyCollector, name: str, obj: object
+) -> Optional["UnitTestCase"]:
+ # Has unittest been imported and is obj a subclass of its TestCase?
+ try:
+ ut = sys.modules["unittest"]
+ # Type ignored because `ut` is an opaque module.
+ if not issubclass(obj, ut.TestCase): # type: ignore
+ return None
+ except Exception:
+ return None
+ # Yes, so let's collect it.
+ item: UnitTestCase = UnitTestCase.from_parent(collector, name=name, obj=obj)
+ return item
+
+
+class UnitTestCase(Class):
+ # Marker for fixturemanger.getfixtureinfo()
+ # to declare that our children do not support funcargs.
+ nofuncargs = True
+
+ def collect(self) -> Iterable[Union[Item, Collector]]:
+ from unittest import TestLoader
+
+ cls = self.obj
+ if not getattr(cls, "__test__", True):
+ return
+
+ skipped = _is_skipped(cls)
+ if not skipped:
+ self._inject_setup_teardown_fixtures(cls)
+ self._inject_setup_class_fixture()
+
+ 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)
+ 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
+ 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:
+ setup()
+ # unittest does not call the cleanup function for every BaseException, so we
+ # follow this here.
+ except Exception:
+ if pass_self:
+ cleanup(self)
+ else:
+ cleanup()
+
+ raise
+ yield
+ try:
+ if teardown is not None:
+ if pass_self:
+ teardown(self, request.function)
+ else:
+ teardown()
+ finally:
+ if pass_self:
+ cleanup(self)
+ else:
+ cleanup()
+
+ return fixture
+
+
+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 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()
+
+ def teardown(self) -> None:
+ 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:
+ pass
+
+ def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None:
+ # 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]
+ # Invoke the attributes to trigger storing the traceback
+ # trial causes some issue there.
+ excinfo.value
+ excinfo.traceback
+ except TypeError:
+ try:
+ try:
+ values = traceback.format_exception(*rawexcinfo)
+ values.insert(
+ 0,
+ "NOTE: Incompatible Exception Representation, "
+ "displaying natively:\n\n",
+ )
+ fail("".join(values), pytrace=False)
+ except (fail.Exception, KeyboardInterrupt):
+ raise
+ except BaseException:
+ fail(
+ "ERROR: Unknown Incompatible Exception "
+ "representation:\n%r" % (rawexcinfo,),
+ pytrace=False,
+ )
+ except KeyboardInterrupt:
+ raise
+ except fail.Exception:
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ self.__dict__.setdefault("_excinfo", []).append(excinfo)
+
+ def addError(
+ self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType"
+ ) -> None:
+ try:
+ if isinstance(rawexcinfo[1], exit.Exception):
+ exit(rawexcinfo[1].msg)
+ except TypeError:
+ pass
+ self._addexcinfo(rawexcinfo)
+
+ def addFailure(
+ self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType"
+ ) -> None:
+ self._addexcinfo(rawexcinfo)
+
+ def addSkip(self, testcase: "unittest.TestCase", reason: str) -> None:
+ try:
+ raise pytest.skip.Exception(reason, _use_item_location=True)
+ except skip.Exception:
+ self._addexcinfo(sys.exc_info())
+
+ def addExpectedFailure(
+ self,
+ testcase: "unittest.TestCase",
+ rawexcinfo: "_SysExcInfoType",
+ reason: str = "",
+ ) -> None:
+ try:
+ xfail(str(reason))
+ except xfail.Exception:
+ self._addexcinfo(sys.exc_info())
+
+ def addUnexpectedSuccess(
+ self,
+ testcase: "unittest.TestCase",
+ reason: Optional["twisted.trial.unittest.Todo"] = None,
+ ) -> None:
+ msg = "Unexpected success"
+ if reason:
+ msg += f": {reason.reason}"
+ # Preserve unittest behaviour - fail the test. Explicitly not an XPASS.
+ try:
+ fail(msg, pytrace=False)
+ except fail.Exception:
+ self._addexcinfo(sys.exc_info())
+
+ def addSuccess(self, testcase: "unittest.TestCase") -> None:
+ pass
+
+ def stopTest(self, testcase: "unittest.TestCase") -> None:
+ pass
+
+ def runtest(self) -> None:
+ from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
+
+ assert self._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]
+ else:
+ # When --pdb is given, we want to postpone calling tearDown() otherwise
+ # when entering the pdb prompt, tearDown() would have probably cleaned up
+ # instance variables, which makes it difficult to debug.
+ # 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 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)
+ try:
+ self._testcase(result=self) # type: ignore[arg-type]
+ finally:
+ delattr(self._testcase, self.name)
+
+ def _prunetraceback(
+ self, excinfo: _pytest._code.ExceptionInfo[BaseException]
+ ) -> None:
+ super()._prunetraceback(excinfo)
+ traceback = excinfo.traceback.filter(
+ lambda x: not x.frame.f_globals.get("__unittest")
+ )
+ if traceback:
+ excinfo.traceback = traceback
+
+
+@hookimpl(tryfirst=True)
+def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
+ if isinstance(item, TestCaseFunction):
+ if item._excinfo:
+ call.excinfo = item._excinfo.pop(0)
+ try:
+ del call.result
+ except AttributeError:
+ pass
+
+ # Convert unittest.SkipTest to pytest.skip.
+ # This is actually only needed for nose, which reuses unittest.SkipTest for
+ # 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]
+ ):
+ excinfo = call.excinfo
+ call2 = CallInfo[None].from_call(
+ lambda: pytest.skip(str(excinfo.value)), call.when
+ )
+ call.excinfo = call2.excinfo
+
+
+# Twisted trial support.
+
+
+@hookimpl(hookwrapper=True)
+def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
+ if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules:
+ ut: Any = sys.modules["twisted.python.failure"]
+ Failure__init__ = ut.Failure.__init__
+ check_testcase_implements_trial_reporter()
+
+ def excstore(
+ self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None
+ ):
+ if exc_value is None:
+ self._rawexcinfo = sys.exc_info()
+ else:
+ if exc_type is None:
+ exc_type = type(exc_value)
+ self._rawexcinfo = (exc_type, exc_value, exc_tb)
+ try:
+ Failure__init__(
+ self, exc_value, exc_type, exc_tb, captureVars=captureVars
+ )
+ except TypeError:
+ Failure__init__(self, exc_value, exc_type, exc_tb)
+
+ ut.Failure.__init__ = excstore
+ yield
+ 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)
+
+
+def _is_skipped(obj) -> bool:
+ """Return True if the given object has been marked with @unittest.skip."""
+ return bool(getattr(obj, "__unittest_skip__", False))
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/unraisableexception.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/unraisableexception.py
new file mode 100644
index 0000000000..fcb5d8237c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/unraisableexception.py
@@ -0,0 +1,93 @@
+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 pytest
+
+
+# Copied from cpython/Lib/test/support/__init__.py, with modifications.
+class catch_unraisable_exception:
+ """Context manager catching unraisable exception using sys.unraisablehook.
+
+ Storing the exception value (cm.unraisable.exc_value) creates a reference
+ cycle. The reference cycle is broken explicitly when the context manager
+ exits.
+
+ Storing the object (cm.unraisable.object) can resurrect it if it is set to
+ an object which is being finalized. Exiting the context manager clears the
+ stored object.
+
+ Usage:
+ with catch_unraisable_exception() as cm:
+ # code creating an "unraisable exception"
+ ...
+ # check the unraisable exception: use cm.unraisable
+ ...
+ # cm.unraisable attribute no longer exists at this point
+ # (to break a reference cycle)
+ """
+
+ def __init__(self) -> None:
+ self.unraisable: Optional["sys.UnraisableHookArgs"] = None
+ self._old_hook: Optional[Callable[["sys.UnraisableHookArgs"], Any]] = None
+
+ def _hook(self, unraisable: "sys.UnraisableHookArgs") -> None:
+ # Storing unraisable.object can resurrect an object which is being
+ # finalized. Storing unraisable.exc_value creates a reference cycle.
+ self.unraisable = unraisable
+
+ def __enter__(self) -> "catch_unraisable_exception":
+ self._old_hook = sys.unraisablehook
+ sys.unraisablehook = self._hook
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ assert self._old_hook is not None
+ sys.unraisablehook = self._old_hook
+ self._old_hook = None
+ del self.unraisable
+
+
+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,
+ )
+ )
+ warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_runtest_setup() -> Generator[None, None, None]:
+ yield from unraisable_exception_runtest_hook()
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_runtest_call() -> Generator[None, None, None]:
+ yield from unraisable_exception_runtest_hook()
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_runtest_teardown() -> Generator[None, None, None]:
+ yield from unraisable_exception_runtest_hook()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/warning_types.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/warning_types.py
new file mode 100644
index 0000000000..2a97a31978
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/warning_types.py
@@ -0,0 +1,145 @@
+from typing import Any
+from typing import Generic
+from typing import Type
+from typing import TypeVar
+
+import attr
+
+from _pytest.compat import final
+
+
+class PytestWarning(UserWarning):
+ """Base class for all warnings emitted by pytest."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestAssertRewriteWarning(PytestWarning):
+ """Warning emitted by the pytest assert rewrite module."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestCacheWarning(PytestWarning):
+ """Warning emitted by the cache plugin in various situations."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestConfigWarning(PytestWarning):
+ """Warning emitted for configuration issues."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestCollectionWarning(PytestWarning):
+ """Warning emitted when pytest is not able to collect a file or symbol in a module."""
+
+ __module__ = "pytest"
+
+
+class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
+ """Warning class for features that will be removed in a future version."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestRemovedIn7Warning(PytestDeprecationWarning):
+ """Warning class for features that will be removed in pytest 7."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestRemovedIn8Warning(PytestDeprecationWarning):
+ """Warning class for features that will be removed in pytest 8."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
+ """Warning category used to denote experiments in pytest.
+
+ Use sparingly as the API might change or even be removed completely in a
+ future version.
+ """
+
+ __module__ = "pytest"
+
+ @classmethod
+ def simple(cls, apiname: str) -> "PytestExperimentalApiWarning":
+ return cls(
+ "{apiname} is an experimental api that may change over time".format(
+ apiname=apiname
+ )
+ )
+
+
+@final
+class PytestUnhandledCoroutineWarning(PytestWarning):
+ """Warning emitted for an unhandled coroutine.
+
+ A coroutine was encountered when collecting test functions, but was not
+ handled by any async-aware plugin.
+ Coroutine test functions are not natively supported.
+ """
+
+ __module__ = "pytest"
+
+
+@final
+class PytestUnknownMarkWarning(PytestWarning):
+ """Warning emitted on use of unknown markers.
+
+ See :ref:`mark` for details.
+ """
+
+ __module__ = "pytest"
+
+
+@final
+class PytestUnraisableExceptionWarning(PytestWarning):
+ """An unraisable exception was reported.
+
+ Unraisable exceptions are exceptions raised in :meth:`__del__ <object.__del__>`
+ implementations and similar situations when the exception cannot be raised
+ as normal.
+ """
+
+ __module__ = "pytest"
+
+
+@final
+class PytestUnhandledThreadExceptionWarning(PytestWarning):
+ """An unhandled exception occurred in a :class:`~threading.Thread`.
+
+ Such exceptions don't propagate normally.
+ """
+
+ __module__ = "pytest"
+
+
+_W = TypeVar("_W", bound=PytestWarning)
+
+
+@final
+@attr.s(auto_attribs=True)
+class UnformattedWarning(Generic[_W]):
+ """A warning meant to be formatted during runtime.
+
+ This is used to hold warnings that need to format their message at runtime,
+ as opposed to a direct message.
+ """
+
+ category: Type["_W"]
+ template: str
+
+ def format(self, **kwargs: Any) -> _W:
+ """Return an instance of the warning category, formatted with given kwargs."""
+ return self.category(self.template.format(**kwargs))
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/warnings.py b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/warnings.py
new file mode 100644
index 0000000000..c0c946cbde
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/_pytest/warnings.py
@@ -0,0 +1,141 @@
+import sys
+import warnings
+from contextlib import contextmanager
+from typing import Generator
+from typing import Optional
+from typing import TYPE_CHECKING
+
+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
+
+
+def pytest_configure(config: Config) -> None:
+ config.addinivalue_line(
+ "markers",
+ "filterwarnings(warning): add a warning filter to the given test. "
+ "see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings ",
+ )
+
+
+@contextmanager
+def catch_warnings_for_item(
+ config: Config,
+ ihook,
+ when: "Literal['config', 'collect', 'runtest']",
+ item: Optional[Item],
+) -> Generator[None, None, None]:
+ """Context manager that catches warnings generated in the contained execution block.
+
+ ``item`` can be None if we are not in the context of an item execution.
+
+ Each warning captured triggers the ``pytest_warning_recorded`` hook.
+ """
+ config_filters = config.getini("filterwarnings")
+ cmdline_filters = config.known_args_namespace.pythonwarnings or []
+ with warnings.catch_warnings(record=True) as log:
+ # mypy can't infer that record=True means log is not None; help it.
+ assert log is not None
+
+ if not sys.warnoptions:
+ # If user is not explicitly configuring warning filters, show deprecation warnings by default (#2908).
+ warnings.filterwarnings("always", category=DeprecationWarning)
+ warnings.filterwarnings("always", category=PendingDeprecationWarning)
+
+ warnings.filterwarnings("error", category=pytest.PytestRemovedIn7Warning)
+
+ apply_warning_filters(config_filters, cmdline_filters)
+
+ # apply filters from "filterwarnings" marks
+ nodeid = "" if item is None else item.nodeid
+ if item is not None:
+ for mark in item.iter_markers(name="filterwarnings"):
+ 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,
+ )
+ )
+
+
+def warning_record_to_str(warning_message: warnings.WarningMessage) -> str:
+ """Convert a warnings.WarningMessage to a string."""
+ warn_msg = warning_message.message
+ msg = warnings.formatwarning(
+ str(warn_msg),
+ warning_message.category,
+ warning_message.filename,
+ warning_message.lineno,
+ warning_message.line,
+ )
+ return msg
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
+ with catch_warnings_for_item(
+ config=item.config, ihook=item.ihook, when="runtest", item=item
+ ):
+ yield
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_collection(session: Session) -> Generator[None, None, None]:
+ config = session.config
+ with catch_warnings_for_item(
+ config=config, ihook=config.hook, when="collect", item=None
+ ):
+ yield
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_terminal_summary(
+ terminalreporter: TerminalReporter,
+) -> Generator[None, None, None]:
+ config = terminalreporter.config
+ with catch_warnings_for_item(
+ config=config, ihook=config.hook, when="config", item=None
+ ):
+ yield
+
+
+@pytest.hookimpl(hookwrapper=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
+
+
+@pytest.hookimpl(hookwrapper=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
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/pytest/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/src/pytest/__init__.py
new file mode 100644
index 0000000000..6050fd1124
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/pytest/__init__.py
@@ -0,0 +1,171 @@
+# 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
+from _pytest.assertion import register_assert_rewrite
+from _pytest.cacheprovider import Cache
+from _pytest.capture import CaptureFixture
+from _pytest.config import cmdline
+from _pytest.config import Config
+from _pytest.config import console_main
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config import hookspec
+from _pytest.config import main
+from _pytest.config import PytestPluginManager
+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.fixtures import fixture
+from _pytest.fixtures import FixtureLookupError
+from _pytest.fixtures import FixtureRequest
+from _pytest.fixtures import yield_fixture
+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 Session
+from _pytest.mark import Mark
+from _pytest.mark import MARK_GEN as mark
+from _pytest.mark import MarkDecorator
+from _pytest.mark import MarkGenerator
+from _pytest.mark import param
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.nodes import Collector
+from _pytest.nodes import File
+from _pytest.nodes import Item
+from _pytest.outcomes import exit
+from _pytest.outcomes import fail
+from _pytest.outcomes import importorskip
+from _pytest.outcomes import skip
+from _pytest.outcomes import xfail
+from _pytest.pytester import HookRecorder
+from _pytest.pytester import LineMatcher
+from _pytest.pytester import Pytester
+from _pytest.pytester import RecordedHookCall
+from _pytest.pytester import RunResult
+from _pytest.python import Class
+from _pytest.python import Function
+from _pytest.python import Metafunc
+from _pytest.python import Module
+from _pytest.python import Package
+from _pytest.python_api import approx
+from _pytest.python_api import raises
+from _pytest.recwarn import deprecated_call
+from _pytest.recwarn import WarningsRecorder
+from _pytest.recwarn import warns
+from _pytest.reports import CollectReport
+from _pytest.reports import TestReport
+from _pytest.runner import CallInfo
+from _pytest.stash import Stash
+from _pytest.stash import StashKey
+from _pytest.tmpdir import TempPathFactory
+from _pytest.warning_types import PytestAssertRewriteWarning
+from _pytest.warning_types import PytestCacheWarning
+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 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",
+ "exit",
+ "ExceptionInfo",
+ "ExitCode",
+ "fail",
+ "File",
+ "fixture",
+ "FixtureLookupError",
+ "FixtureRequest",
+ "freeze_includes",
+ "Function",
+ "hookimpl",
+ "HookRecorder",
+ "hookspec",
+ "importorskip",
+ "Item",
+ "LineMatcher",
+ "LogCaptureFixture",
+ "main",
+ "mark",
+ "Mark",
+ "MarkDecorator",
+ "MarkGenerator",
+ "Metafunc",
+ "Module",
+ "MonkeyPatch",
+ "OptionGroup",
+ "Package",
+ "param",
+ "Parser",
+ "PytestAssertRewriteWarning",
+ "PytestCacheWarning",
+ "PytestCollectionWarning",
+ "PytestConfigWarning",
+ "PytestDeprecationWarning",
+ "PytestExperimentalApiWarning",
+ "PytestRemovedIn7Warning",
+ "PytestRemovedIn8Warning",
+ "Pytester",
+ "PytestPluginManager",
+ "PytestUnhandledCoroutineWarning",
+ "PytestUnhandledThreadExceptionWarning",
+ "PytestUnknownMarkWarning",
+ "PytestUnraisableExceptionWarning",
+ "PytestWarning",
+ "raises",
+ "RecordedHookCall",
+ "register_assert_rewrite",
+ "RunResult",
+ "Session",
+ "set_trace",
+ "skip",
+ "Stash",
+ "StashKey",
+ "version_tuple",
+ "TempdirFactory",
+ "TempPathFactory",
+ "Testdir",
+ "TestReport",
+ "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/testing/web-platform/tests/tools/third_party/pytest/src/pytest/__main__.py b/testing/web-platform/tests/tools/third_party/pytest/src/pytest/__main__.py
new file mode 100644
index 0000000000..b170152937
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/pytest/__main__.py
@@ -0,0 +1,5 @@
+"""The pytest entry point."""
+import pytest
+
+if __name__ == "__main__":
+ raise SystemExit(pytest.console_main())
diff --git a/testing/web-platform/tests/tools/third_party/pytest/src/pytest/collect.py b/testing/web-platform/tests/tools/third_party/pytest/src/pytest/collect.py
new file mode 100644
index 0000000000..4b2b581806
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/pytest/collect.py
@@ -0,0 +1,38 @@
+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/testing/web-platform/tests/tools/third_party/pytest/src/pytest/py.typed b/testing/web-platform/tests/tools/third_party/pytest/src/pytest/py.typed
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/src/pytest/py.typed
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/acceptance_test.py b/testing/web-platform/tests/tools/third_party/pytest/testing/acceptance_test.py
new file mode 100644
index 0000000000..8b8d4a4a6e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/acceptance_test.py
@@ -0,0 +1,1297 @@
+import os
+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
+
+
+def prepend_pythonpath(*dirs) -> str:
+ cur = os.getenv("PYTHONPATH")
+ if cur:
+ dirs += (cur,)
+ return os.pathsep.join(str(p) for p in dirs)
+
+
+class TestGeneralUsage:
+ def test_config_error(self, pytester: Pytester) -> None:
+ pytester.copy_example("conftest_usageerror/conftest.py")
+ result = pytester.runpytest(pytester.path)
+ assert result.ret == ExitCode.USAGE_ERROR
+ result.stderr.fnmatch_lines(["*ERROR: hello"])
+ result.stdout.fnmatch_lines(["*pytest_unconfigure_called"])
+
+ def test_root_conftest_syntax_error(self, pytester: Pytester) -> None:
+ pytester.makepyfile(conftest="raise SyntaxError\n")
+ result = pytester.runpytest()
+ result.stderr.fnmatch_lines(["*raise SyntaxError*"])
+ assert result.ret != 0
+
+ def test_early_hook_error_issue38_1(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_sessionstart():
+ 0 / 0
+ """
+ )
+ result = pytester.runpytest(pytester.path)
+ assert result.ret != 0
+ # tracestyle is native by default for hook failures
+ result.stdout.fnmatch_lines(
+ ["*INTERNALERROR*File*conftest.py*line 2*", "*0 / 0*"]
+ )
+ result = pytester.runpytest(pytester.path, "--fulltrace")
+ assert result.ret != 0
+ # tracestyle is native by default for hook failures
+ result.stdout.fnmatch_lines(
+ ["*INTERNALERROR*def pytest_sessionstart():*", "*INTERNALERROR*0 / 0*"]
+ )
+
+ def test_early_hook_configure_error_issue38(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_configure():
+ 0 / 0
+ """
+ )
+ result = pytester.runpytest(pytester.path)
+ assert result.ret != 0
+ # here we get it on stderr
+ result.stderr.fnmatch_lines(
+ ["*INTERNALERROR*File*conftest.py*line 2*", "*0 / 0*"]
+ )
+
+ def test_file_not_found(self, pytester: Pytester) -> None:
+ result = pytester.runpytest("asd")
+ assert result.ret != 0
+ result.stderr.fnmatch_lines(["ERROR: file or directory not found: asd"])
+
+ def test_file_not_found_unconfigure_issue143(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_configure():
+ print("---configure")
+ def pytest_unconfigure():
+ print("---unconfigure")
+ """
+ )
+ result = pytester.runpytest("-s", "asd")
+ assert result.ret == ExitCode.USAGE_ERROR
+ result.stderr.fnmatch_lines(["ERROR: file or directory not found: asd"])
+ result.stdout.fnmatch_lines(["*---configure", "*---unconfigure"])
+
+ def test_config_preparse_plugin_option(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ pytest_xyz="""
+ def pytest_addoption(parser):
+ parser.addoption("--xyz", dest="xyz", action="store")
+ """
+ )
+ pytester.makepyfile(
+ test_one="""
+ def test_option(pytestconfig):
+ assert pytestconfig.option.xyz == "123"
+ """
+ )
+ result = pytester.runpytest("-p", "pytest_xyz", "--xyz=123", syspathinsert=True)
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ @pytest.mark.parametrize("load_cov_early", [True, False])
+ def test_early_load_setuptools_name(
+ self, pytester: Pytester, monkeypatch, load_cov_early
+ ) -> None:
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
+
+ pytester.makepyfile(mytestplugin1_module="")
+ pytester.makepyfile(mytestplugin2_module="")
+ pytester.makepyfile(mycov_module="")
+ pytester.syspathinsert()
+
+ loaded = []
+
+ @attr.s
+ class DummyEntryPoint:
+ name = attr.ib()
+ module = attr.ib()
+ group = "pytest11"
+
+ def load(self):
+ __import__(self.module)
+ loaded.append(self.name)
+ return sys.modules[self.module]
+
+ entry_points = [
+ DummyEntryPoint("myplugin1", "mytestplugin1_module"),
+ DummyEntryPoint("myplugin2", "mytestplugin2_module"),
+ DummyEntryPoint("mycov", "mycov_module"),
+ ]
+
+ @attr.s
+ class DummyDist:
+ entry_points = attr.ib()
+ files = ()
+
+ def my_dists():
+ return (DummyDist(entry_points),)
+
+ monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
+ params = ("-p", "mycov") if load_cov_early else ()
+ pytester.runpytest_inprocess(*params)
+ if load_cov_early:
+ assert loaded == ["mycov", "myplugin1", "myplugin2"]
+ else:
+ assert loaded == ["myplugin1", "myplugin2", "mycov"]
+
+ @pytest.mark.parametrize("import_mode", ["prepend", "append", "importlib"])
+ def test_assertion_rewrite(self, pytester: Pytester, import_mode) -> None:
+ p = pytester.makepyfile(
+ """
+ def test_this():
+ x = 0
+ assert x
+ """
+ )
+ result = pytester.runpytest(p, f"--import-mode={import_mode}")
+ result.stdout.fnmatch_lines(["> assert x", "E assert 0"])
+ assert result.ret == 1
+
+ def test_nested_import_error(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import import_fails
+ def test_this():
+ assert import_fails.a == 1
+ """
+ )
+ pytester.makepyfile(import_fails="import does_not_work")
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(
+ [
+ "ImportError while importing test module*",
+ "*No module named *does_not_work*",
+ ]
+ )
+ assert result.ret == 2
+
+ def test_not_collectable_arguments(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile("")
+ p2 = pytester.makefile(".pyc", "123")
+ result = pytester.runpytest(p1, p2)
+ assert result.ret == ExitCode.USAGE_ERROR
+ result.stderr.fnmatch_lines(
+ [
+ f"ERROR: not found: {p2}",
+ f"(no name {str(p2)!r} in any of [[][]])",
+ "",
+ ]
+ )
+
+ @pytest.mark.filterwarnings("default")
+ def test_better_reporting_on_conftest_load_failure(
+ self, pytester: Pytester
+ ) -> None:
+ """Show a user-friendly traceback on conftest import failures (#486, #3332)"""
+ pytester.makepyfile("")
+ conftest = pytester.makeconftest(
+ """
+ def foo():
+ import qwerty
+ foo()
+ """
+ )
+ result = pytester.runpytest("--help")
+ result.stdout.fnmatch_lines(
+ """
+ *--version*
+ *warning*conftest.py*
+ """
+ )
+ result = pytester.runpytest()
+ assert result.stdout.lines == []
+ assert result.stderr.lines == [
+ f"ImportError while loading conftest '{conftest}'.",
+ "conftest.py:3: in <module>",
+ " foo()",
+ "conftest.py:2: in foo",
+ " import qwerty",
+ "E ModuleNotFoundError: No module named 'qwerty'",
+ ]
+
+ def test_early_skip(self, pytester: Pytester) -> None:
+ pytester.mkdir("xyz")
+ pytester.makeconftest(
+ """
+ import pytest
+ def pytest_collect_file():
+ pytest.skip("early")
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+ result.stdout.fnmatch_lines(["*1 skip*"])
+
+ def test_issue88_initial_file_multinodes(self, pytester: Pytester) -> None:
+ pytester.copy_example("issue88_initial_file_multinodes")
+ p = pytester.makepyfile("def test_hello(): pass")
+ result = pytester.runpytest(p, "--collect-only")
+ result.stdout.fnmatch_lines(["*MyFile*test_issue88*", "*Module*test_issue88*"])
+
+ def test_issue93_initialnode_importing_capturing(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import sys
+ print("should not be seen")
+ sys.stderr.write("stder42\\n")
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+ result.stdout.no_fnmatch_line("*should not be seen*")
+ assert "stderr42" not in result.stderr.str()
+
+ def test_conftest_printing_shows_if_error(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ print("should be seen")
+ assert 0
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret != 0
+ assert "should be seen" in result.stdout.str()
+
+ 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")
+ result = pytester.runpytest(sub2)
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+ sub2.joinpath("__init__.py").touch()
+ p = sub2.joinpath("test_hello.py")
+ p.touch()
+ result = pytester.runpytest(p)
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+ result = pytester.runpytest(sub1)
+ assert result.ret == ExitCode.USAGE_ERROR
+
+ def test_directory_skipped(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ def pytest_ignore_collect():
+ pytest.skip("intentional")
+ """
+ )
+ pytester.makepyfile("def test_hello(): pass")
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+ result.stdout.fnmatch_lines(["*1 skipped*"])
+
+ def test_multiple_items_per_collector_byid(self, pytester: Pytester) -> None:
+ c = pytester.makeconftest(
+ """
+ import pytest
+ class MyItem(pytest.Item):
+ def runtest(self):
+ pass
+ class MyCollector(pytest.File):
+ def collect(self):
+ return [MyItem.from_parent(name="xyz", parent=self)]
+ def pytest_collect_file(file_path, parent):
+ if file_path.name.startswith("conftest"):
+ return MyCollector.from_parent(path=file_path, parent=parent)
+ """
+ )
+ result = pytester.runpytest(c.name + "::" + "xyz")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*1 pass*"])
+
+ def test_skip_on_generated_funcarg_id(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize('x', [3], ids=['hello-123'])
+ def pytest_runtest_setup(item):
+ print(item.keywords)
+ if 'hello-123' in item.keywords:
+ pytest.skip("hello")
+ assert 0
+ """
+ )
+ p = pytester.makepyfile("""def test_func(x): pass""")
+ res = pytester.runpytest(p)
+ assert res.ret == 0
+ res.stdout.fnmatch_lines(["*1 skipped*"])
+
+ def test_direct_addressing_selects(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize('i', [1, 2], ids=["1", "2"])
+ def test_func(i):
+ pass
+ """
+ )
+ res = pytester.runpytest(p.name + "::" + "test_func[1]")
+ assert res.ret == 0
+ res.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_direct_addressing_notfound(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def test_func():
+ pass
+ """
+ )
+ res = pytester.runpytest(p.name + "::" + "test_notfound")
+ assert res.ret
+ res.stderr.fnmatch_lines(["*ERROR*not found*"])
+
+ def test_docstring_on_hookspec(self) -> None:
+ from _pytest import hookspec
+
+ for name, value in vars(hookspec).items():
+ if name.startswith("pytest_"):
+ assert value.__doc__, "no docstring for %s" % name
+
+ def test_initialization_error_issue49(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_configure():
+ x
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 3 # internal error
+ result.stderr.fnmatch_lines(["INTERNAL*pytest_configure*", "INTERNAL*x*"])
+ assert "sessionstarttime" not in result.stderr.str()
+
+ @pytest.mark.parametrize("lookfor", ["test_fun.py::test_a"])
+ def test_issue134_report_error_when_collecting_member(
+ self, pytester: Pytester, lookfor
+ ) -> None:
+ pytester.makepyfile(
+ test_fun="""
+ def test_a():
+ pass
+ def"""
+ )
+ result = pytester.runpytest(lookfor)
+ result.stdout.fnmatch_lines(["*SyntaxError*"])
+ if "::" in lookfor:
+ result.stderr.fnmatch_lines(["*ERROR*"])
+ assert result.ret == 4 # usage error only if item not found
+
+ def test_report_all_failed_collections_initargs(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ from _pytest.config import ExitCode
+
+ def pytest_sessionfinish(exitstatus):
+ assert exitstatus == ExitCode.USAGE_ERROR
+ print("pytest_sessionfinish_called")
+ """
+ )
+ pytester.makepyfile(test_a="def", test_b="def")
+ result = pytester.runpytest("test_a.py::a", "test_b.py::b")
+ result.stderr.fnmatch_lines(["*ERROR*test_a.py::a*", "*ERROR*test_b.py::b*"])
+ result.stdout.fnmatch_lines(["pytest_sessionfinish_called"])
+ assert result.ret == ExitCode.USAGE_ERROR
+
+ def test_namespace_import_doesnt_confuse_import_hook(
+ self, pytester: Pytester
+ ) -> None:
+ """Ref #383.
+
+ Python 3.3's namespace package messed with our import hooks.
+ Importing a module that didn't exist, even if the ImportError was
+ gracefully handled, would make our test crash.
+ """
+ pytester.mkdir("not_a_package")
+ p = pytester.makepyfile(
+ """
+ try:
+ from not_a_package import doesnt_exist
+ except ImportError:
+ # We handle the import error gracefully here
+ pass
+
+ def test_whatever():
+ pass
+ """
+ )
+ res = pytester.runpytest(p.name)
+ assert res.ret == 0
+
+ def test_unknown_option(self, pytester: Pytester) -> None:
+ result = pytester.runpytest("--qwlkej")
+ result.stderr.fnmatch_lines(
+ """
+ *unrecognized*
+ """
+ )
+
+ def test_getsourcelines_error_issue553(
+ self, pytester: Pytester, monkeypatch
+ ) -> None:
+ monkeypatch.setattr("inspect.getsourcelines", None)
+ p = pytester.makepyfile(
+ """
+ def raise_error(obj):
+ raise OSError('source code not available')
+
+ import inspect
+ inspect.getsourcelines = raise_error
+
+ def test_foo(invalid_fixture):
+ pass
+ """
+ )
+ res = pytester.runpytest(p)
+ res.stdout.fnmatch_lines(
+ ["*source code not available*", "E*fixture 'invalid_fixture' not found"]
+ )
+
+ def test_plugins_given_as_strings(
+ self, pytester: Pytester, monkeypatch, _sys_snapshot
+ ) -> None:
+ """Test that str values passed to main() as `plugins` arg are
+ interpreted as module names to be imported and registered (#855)."""
+ with pytest.raises(ImportError) as excinfo:
+ pytest.main([str(pytester.path)], plugins=["invalid.module"])
+ assert "invalid" in str(excinfo.value)
+
+ p = pytester.path.joinpath("test_test_plugins_given_as_strings.py")
+ p.write_text("def test_foo(): pass")
+ mod = types.ModuleType("myplugin")
+ monkeypatch.setitem(sys.modules, "myplugin", mod)
+ assert pytest.main(args=[str(pytester.path)], plugins=["myplugin"]) == 0
+
+ def test_parametrized_with_bytes_regex(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import re
+ import pytest
+ @pytest.mark.parametrize('r', [re.compile(b'foo')])
+ def test_stuff(r):
+ pass
+ """
+ )
+ res = pytester.runpytest(p)
+ res.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_parametrized_with_null_bytes(self, pytester: Pytester) -> None:
+ """Test parametrization with values that contain null bytes and unicode characters (#2644, #2957)"""
+ p = pytester.makepyfile(
+ """\
+ import pytest
+
+ @pytest.mark.parametrize("data", [b"\\x00", "\\x00", 'ação'])
+ def test_foo(data):
+ assert data
+ """
+ )
+ res = pytester.runpytest(p)
+ res.assert_outcomes(passed=3)
+
+
+class TestInvocationVariants:
+ def test_earlyinit(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ assert hasattr(pytest, 'mark')
+ """
+ )
+ result = pytester.runpython(p)
+ assert result.ret == 0
+
+ def test_pydoc(self, pytester: Pytester) -> None:
+ result = pytester.runpython_c("import pytest;help(pytest)")
+ assert result.ret == 0
+ s = result.stdout.str()
+ assert "MarkGenerator" in s
+
+ def test_import_star_pytest(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ from pytest import *
+ #Item
+ #File
+ main
+ skip
+ xfail
+ """
+ )
+ result = pytester.runpython(p)
+ assert result.ret == 0
+
+ def test_double_pytestcmdline(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ run="""
+ import pytest
+ pytest.main()
+ pytest.main()
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_hello():
+ pass
+ """
+ )
+ result = pytester.runpython(p)
+ result.stdout.fnmatch_lines(["*1 passed*", "*1 passed*"])
+
+ def test_python_minus_m_invocation_ok(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile("def test_hello(): pass")
+ res = pytester.run(sys.executable, "-m", "pytest", str(p1))
+ assert res.ret == 0
+
+ def test_python_minus_m_invocation_fail(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile("def test_fail(): 0/0")
+ res = pytester.run(sys.executable, "-m", "pytest", str(p1))
+ assert res.ret == 1
+
+ def test_python_pytest_package(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile("def test_pass(): pass")
+ res = pytester.run(sys.executable, "-m", "pytest", str(p1))
+ assert res.ret == 0
+ res.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_invoke_with_invalid_type(self) -> None:
+ with pytest.raises(
+ TypeError, match="expected to be a list of strings, got: '-h'"
+ ):
+ pytest.main("-h") # type: ignore[arg-type]
+
+ def test_invoke_with_path(self, pytester: Pytester, capsys) -> None:
+ retcode = pytest.main([str(pytester.path)])
+ assert retcode == ExitCode.NO_TESTS_COLLECTED
+ out, err = capsys.readouterr()
+
+ def test_invoke_plugin_api(self, capsys) -> None:
+ class MyPlugin:
+ def pytest_addoption(self, parser):
+ parser.addoption("--myopt")
+
+ pytest.main(["-h"], plugins=[MyPlugin()])
+ out, err = capsys.readouterr()
+ assert "--myopt" in out
+
+ 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")
+
+ result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
+ assert result.ret != 0
+
+ result.stdout.fnmatch_lines(["collected*0*items*/*1*error"])
+
+ 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"
+ )
+ pkg.joinpath("conftest.py").write_text(
+ "def pytest_configure(config): print('configuring')"
+ )
+
+ result = pytester.runpytest(
+ "--pyargs", "foo.test_foo", "-s", syspathinsert=True
+ )
+ # should only import once
+ assert result.outlines.count("hello from test_foo") == 1
+ # should only configure once
+ assert result.outlines.count("configuring") == 1
+
+ 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")
+ result = pytester.runpytest("--pyargs", "t.py")
+ assert result.ret == ExitCode.OK
+
+ def test_cmdline_python_package(self, pytester: Pytester, monkeypatch) -> None:
+ import warnings
+
+ 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")
+ result = pytester.runpytest("--pyargs", "tpkg")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*2 passed*"])
+ result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ empty_package = pytester.mkpydir("empty_package")
+ monkeypatch.setenv("PYTHONPATH", str(empty_package), prepend=os.pathsep)
+ # the path which is not a package raises a warning on pypy;
+ # no idea why only pypy and not normal python warn about it here
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", ImportWarning)
+ result = pytester.runpytest("--pyargs", ".")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+ monkeypatch.setenv("PYTHONPATH", str(pytester), prepend=os.pathsep)
+ result = pytester.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True)
+ assert result.ret != 0
+ result.stderr.fnmatch_lines(["*not*found*test_missing*"])
+
+ def test_cmdline_python_namespace_package(
+ self, pytester: Pytester, monkeypatch
+ ) -> None:
+ """Test --pyargs option with namespace packages (#1567).
+
+ Ref: https://packaging.python.org/guides/packaging-namespace-packages/
+ """
+ monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
+
+ search_path = []
+ for dirname in "hello", "world":
+ d = pytester.mkdir(dirname)
+ search_path.append(d)
+ ns = d.joinpath("ns_pkg")
+ ns.mkdir()
+ ns.joinpath("__init__.py").write_text(
+ "__import__('pkg_resources').declare_namespace(__name__)"
+ )
+ 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"
+ )
+
+ # The structure of the test directory is now:
+ # .
+ # ├── hello
+ # │ └── ns_pkg
+ # │ ├── __init__.py
+ # │ └── hello
+ # │ ├── __init__.py
+ # │ └── test_hello.py
+ # └── world
+ # └── ns_pkg
+ # ├── __init__.py
+ # └── world
+ # ├── __init__.py
+ # └── test_world.py
+
+ # NOTE: the different/reversed ordering is intentional here.
+ monkeypatch.setenv("PYTHONPATH", prepend_pythonpath(*search_path))
+ for p in search_path:
+ monkeypatch.syspath_prepend(p)
+
+ # mixed module and filenames:
+ monkeypatch.chdir("world")
+ result = pytester.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(
+ [
+ "test_hello.py::test_hello*PASSED*",
+ "test_hello.py::test_other*PASSED*",
+ "ns_pkg/world/test_world.py::test_world*PASSED*",
+ "ns_pkg/world/test_world.py::test_other*PASSED*",
+ "*4 passed in*",
+ ]
+ )
+
+ # specify tests within a module
+ pytester.chdir()
+ result = pytester.runpytest(
+ "--pyargs", "-v", "ns_pkg.world.test_world::test_other"
+ )
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(
+ ["*test_world.py::test_other*PASSED*", "*1 passed*"]
+ )
+
+ def test_invoke_test_and_doctestmodules(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def test():
+ pass
+ """
+ )
+ result = pytester.runpytest(str(p) + "::test", "--doctest-modules")
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_cmdline_python_package_symlink(
+ self, pytester: Pytester, monkeypatch
+ ) -> None:
+ """
+ --pyargs with packages with path containing symlink can have conftest.py in
+ their package (#2985)
+ """
+ monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
+
+ dirname = "lib"
+ d = pytester.mkdir(dirname)
+ foo = d.joinpath("foo")
+ foo.mkdir()
+ foo.joinpath("__init__.py").touch()
+ lib = foo.joinpath("bar")
+ lib.mkdir()
+ lib.joinpath("__init__.py").touch()
+ lib.joinpath("test_bar.py").write_text(
+ "def test_bar(): pass\ndef test_other(a_fixture):pass"
+ )
+ lib.joinpath("conftest.py").write_text(
+ "import pytest\n@pytest.fixture\ndef a_fixture():pass"
+ )
+
+ d_local = pytester.mkdir("symlink_root")
+ symlink_location = d_local / "lib"
+ symlink_or_skip(d, symlink_location, target_is_directory=True)
+
+ # The structure of the test directory is now:
+ # .
+ # ├── symlink_root
+ # │ └── lib -> ../lib
+ # └── lib
+ # └── foo
+ # ├── __init__.py
+ # └── bar
+ # ├── __init__.py
+ # ├── conftest.py
+ # └── test_bar.py
+
+ # NOTE: the different/reversed ordering is intentional here.
+ search_path = ["lib", os.path.join("symlink_root", "lib")]
+ monkeypatch.setenv("PYTHONPATH", prepend_pythonpath(*search_path))
+ for p in search_path:
+ monkeypatch.syspath_prepend(p)
+
+ # module picked up in symlink-ed directory:
+ # It picks up symlink_root/lib/foo/bar (symlink) via sys.path.
+ result = pytester.runpytest("--pyargs", "-v", "foo.bar")
+ pytester.chdir()
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(
+ [
+ "symlink_root/lib/foo/bar/test_bar.py::test_bar PASSED*",
+ "symlink_root/lib/foo/bar/test_bar.py::test_other PASSED*",
+ "*2 passed*",
+ ]
+ )
+
+ def test_cmdline_python_package_not_exists(self, pytester: Pytester) -> None:
+ result = pytester.runpytest("--pyargs", "tpkgwhatv")
+ assert result.ret
+ result.stderr.fnmatch_lines(["ERROR*module*or*package*not*found*"])
+
+ @pytest.mark.xfail(reason="decide: feature or bug")
+ def test_noclass_discovery_if_not_testcase(self, pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+ class TestHello(object):
+ def test_hello(self):
+ assert self.attr
+
+ class RealTest(unittest.TestCase, TestHello):
+ attr = 42
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ reprec.assertoutcome(passed=1)
+
+ def test_doctest_id(self, pytester: Pytester) -> None:
+ pytester.makefile(
+ ".txt",
+ """
+ >>> x=3
+ >>> x
+ 4
+ """,
+ )
+ testid = "test_doctest_id.txt::test_doctest_id.txt"
+ expected_lines = [
+ "*= FAILURES =*",
+ "*_ ?doctest? test_doctest_id.txt _*",
+ "FAILED test_doctest_id.txt::test_doctest_id.txt",
+ "*= 1 failed in*",
+ ]
+ result = pytester.runpytest(testid, "-rf", "--tb=short")
+ result.stdout.fnmatch_lines(expected_lines)
+
+ # Ensure that re-running it will still handle it as
+ # doctest.DocTestFailure, which was not the case before when
+ # re-importing doctest, but not creating a new RUNNER_CLASS.
+ result = pytester.runpytest(testid, "-rf", "--tb=short")
+ result.stdout.fnmatch_lines(expected_lines)
+
+ def test_core_backward_compatibility(self) -> None:
+ """Test backward compatibility for get_plugin_manager function. See #787."""
+ import _pytest.config
+
+ assert (
+ type(_pytest.config.get_plugin_manager())
+ is _pytest.config.PytestPluginManager
+ )
+
+ def test_has_plugin(self, request) -> None:
+ """Test hasplugin function of the plugin manager (#932)."""
+ assert request.config.pluginmanager.hasplugin("python")
+
+
+class TestDurations:
+ source = """
+ from _pytest import timing
+ def test_something():
+ pass
+ def test_2():
+ timing.sleep(0.010)
+ def test_1():
+ timing.sleep(0.002)
+ def test_3():
+ timing.sleep(0.020)
+ """
+
+ def test_calls(self, pytester: Pytester, mock_timing) -> None:
+ pytester.makepyfile(self.source)
+ result = pytester.runpytest_inprocess("--durations=10")
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines_random(
+ ["*durations*", "*call*test_3*", "*call*test_2*"]
+ )
+
+ result.stdout.fnmatch_lines(
+ ["(8 durations < 0.005s hidden. Use -vv to show these durations.)"]
+ )
+
+ 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
+
+ lines = result.stdout.get_lines_after("*slowest*durations*")
+ assert "4 passed" in lines[2]
+
+ def test_calls_showall(self, pytester: Pytester, mock_timing) -> None:
+ pytester.makepyfile(self.source)
+ result = pytester.runpytest_inprocess("--durations=0")
+ assert result.ret == 0
+
+ tested = "3"
+ for x in tested:
+ for y in ("call",): # 'setup', 'call', 'teardown':
+ for line in result.stdout.lines:
+ if ("test_%s" % x) in line and y in line:
+ break
+ else:
+ raise AssertionError(f"not found {x} {y}")
+
+ def test_calls_showall_verbose(self, pytester: Pytester, mock_timing) -> None:
+ pytester.makepyfile(self.source)
+ result = pytester.runpytest_inprocess("--durations=0", "-vv")
+ assert result.ret == 0
+
+ for x in "123":
+ for y in ("call",): # 'setup', 'call', 'teardown':
+ for line in result.stdout.lines:
+ if ("test_%s" % x) in line and y in line:
+ break
+ else:
+ raise AssertionError(f"not found {x} {y}")
+
+ def test_with_deselected(self, pytester: Pytester, mock_timing) -> None:
+ pytester.makepyfile(self.source)
+ result = pytester.runpytest_inprocess("--durations=2", "-k test_3")
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(["*durations*", "*call*test_3*"])
+
+ def test_with_failing_collection(self, pytester: Pytester, mock_timing) -> None:
+ pytester.makepyfile(self.source)
+ pytester.makepyfile(test_collecterror="""xyz""")
+ result = pytester.runpytest_inprocess("--durations=2", "-k test_1")
+ assert result.ret == 2
+
+ result.stdout.fnmatch_lines(["*Interrupted: 1 error during collection*"])
+ # Collection errors abort test execution, therefore no duration is
+ # output
+ result.stdout.no_fnmatch_line("*duration*")
+
+ def test_with_not(self, pytester: Pytester, mock_timing) -> None:
+ pytester.makepyfile(self.source)
+ result = pytester.runpytest_inprocess("-k not 1")
+ assert result.ret == 0
+
+
+class TestDurationsWithFixture:
+ source = """
+ import pytest
+ from _pytest import timing
+
+ @pytest.fixture
+ def setup_fixt():
+ timing.sleep(2)
+
+ def test_1(setup_fixt):
+ timing.sleep(5)
+ """
+
+ def test_setup_function(self, pytester: Pytester, mock_timing) -> None:
+ pytester.makepyfile(self.source)
+ result = pytester.runpytest_inprocess("--durations=10")
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines_random(
+ """
+ *durations*
+ 5.00s call *test_1*
+ 2.00s setup *test_1*
+ """
+ )
+
+
+def test_zipimport_hook(pytester: Pytester) -> None:
+ """Test package loader is being used correctly (see #1837)."""
+ zipapp = pytest.importorskip("zipapp")
+ pytester.path.joinpath("app").mkdir()
+ pytester.makepyfile(
+ **{
+ "app/foo.py": """
+ import pytest
+ def main():
+ pytest.main(['--pyargs', 'foo'])
+ """
+ }
+ )
+ target = pytester.path.joinpath("foo.zip")
+ zipapp.create_archive(
+ str(pytester.path.joinpath("app")), str(target), main="foo:main"
+ )
+ result = pytester.runpython(target)
+ assert result.ret == 0
+ result.stderr.fnmatch_lines(["*not found*foo*"])
+ result.stdout.no_fnmatch_line("*INTERNALERROR>*")
+
+
+def test_import_plugin_unicode_name(pytester: Pytester) -> None:
+ pytester.makepyfile(myplugin="")
+ pytester.makepyfile("def test(): pass")
+ pytester.makeconftest("pytest_plugins = ['myplugin']")
+ r = pytester.runpytest()
+ assert r.ret == 0
+
+
+def test_pytest_plugins_as_module(pytester: Pytester) -> None:
+ """Do not raise an error if pytest_plugins attribute is a module (#3899)"""
+ pytester.makepyfile(
+ **{
+ "__init__.py": "",
+ "pytest_plugins.py": "",
+ "conftest.py": "from . import pytest_plugins",
+ "test_foo.py": "def test(): pass",
+ }
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 1 passed in *"])
+
+
+def test_deferred_hook_checking(pytester: Pytester) -> None:
+ """Check hooks as late as possible (#1821)."""
+ pytester.syspathinsert()
+ pytester.makepyfile(
+ **{
+ "plugin.py": """
+ class Hooks(object):
+ def pytest_my_hook(self, config):
+ pass
+
+ def pytest_configure(config):
+ config.pluginmanager.add_hookspecs(Hooks)
+ """,
+ "conftest.py": """
+ pytest_plugins = ['plugin']
+ def pytest_my_hook(config):
+ return 40
+ """,
+ "test_foo.py": """
+ def test(request):
+ assert request.config.hook.pytest_my_hook(config=request.config) == [40]
+ """,
+ }
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 1 passed *"])
+
+
+def test_fixture_values_leak(pytester: Pytester) -> None:
+ """Ensure that fixture objects are properly destroyed by the garbage collector at the end of their expected
+ life-times (#2981).
+ """
+ pytester.makepyfile(
+ """
+ import attr
+ import gc
+ import pytest
+ import weakref
+
+ @attr.s
+ class SomeObj(object):
+ name = attr.ib()
+
+ fix_of_test1_ref = None
+ session_ref = None
+
+ @pytest.fixture(scope='session')
+ def session_fix():
+ global session_ref
+ obj = SomeObj(name='session-fixture')
+ session_ref = weakref.ref(obj)
+ return obj
+
+ @pytest.fixture
+ def fix(session_fix):
+ global fix_of_test1_ref
+ obj = SomeObj(name='local-fixture')
+ fix_of_test1_ref = weakref.ref(obj)
+ return obj
+
+ def test1(fix):
+ assert fix_of_test1_ref() is fix
+
+ def test2():
+ gc.collect()
+ # fixture "fix" created during test1 must have been destroyed by now
+ assert fix_of_test1_ref() is None
+ """
+ )
+ # Running on subprocess does not activate the HookRecorder
+ # which holds itself a reference to objects in case of the
+ # pytest_assert_reprcompare hook
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(["* 2 passed *"])
+
+
+def test_fixture_order_respects_scope(pytester: Pytester) -> None:
+ """Ensure that fixtures are created according to scope order (#2405)."""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ data = {}
+
+ @pytest.fixture(scope='module')
+ def clean_data():
+ data.clear()
+
+ @pytest.fixture(autouse=True)
+ def add_data():
+ data.update(value=True)
+
+ @pytest.mark.usefixtures('clean_data')
+ def test_value():
+ assert data.get('value')
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+
+
+def test_frame_leak_on_failing_test(pytester: Pytester) -> None:
+ """Pytest would leak garbage referencing the frames of tests that failed
+ that could never be reclaimed (#2798).
+
+ Unfortunately it was not possible to remove the actual circles because most of them
+ are made of traceback objects which cannot be weakly referenced. Those objects at least
+ can be eventually claimed by the garbage collector.
+ """
+ pytester.makepyfile(
+ """
+ import gc
+ import weakref
+
+ class Obj:
+ pass
+
+ ref = None
+
+ def test1():
+ obj = Obj()
+ global ref
+ ref = weakref.ref(obj)
+ assert 0
+
+ def test2():
+ gc.collect()
+ assert ref() is None
+ """
+ )
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(["*1 failed, 1 passed in*"])
+
+
+def test_fixture_mock_integration(pytester: Pytester) -> None:
+ """Test that decorators applied to fixture are left working (#3774)"""
+ p = pytester.copy_example("acceptance/fixture_mock_integration.py")
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+def test_usage_error_code(pytester: Pytester) -> None:
+ result = pytester.runpytest("-unknown-option-")
+ 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"
+ # which messes with other tests.
+ pytester.makepyfile(
+ test_async="""
+ async def test_1():
+ pass
+ async def test_2():
+ pass
+ def test_3():
+ coro = test_2()
+ coro.close()
+ return coro
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "test_async.py::test_1",
+ "test_async.py::test_2",
+ "test_async.py::test_3",
+ "*async def functions are not natively supported*",
+ "*3 skipped, 3 warnings in*",
+ ]
+ )
+ # ensure our warning message appears only once
+ assert (
+ result.stdout.str().count("async def functions are not natively supported") == 1
+ )
+
+
+@pytest.mark.filterwarnings("default::pytest.PytestUnhandledCoroutineWarning")
+def test_warn_on_async_gen_function(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_async="""
+ async def test_1():
+ yield
+ async def test_2():
+ yield
+ def test_3():
+ return test_2()
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "test_async.py::test_1",
+ "test_async.py::test_2",
+ "test_async.py::test_3",
+ "*async def functions are not natively supported*",
+ "*3 skipped, 3 warnings in*",
+ ]
+ )
+ # ensure our warning message appears only once
+ assert (
+ result.stdout.str().count("async def functions are not natively supported") == 1
+ )
+
+
+def test_pdb_can_be_rewritten(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ **{
+ "conftest.py": """
+ import pytest
+ pytest.register_assert_rewrite("pdb")
+ """,
+ "__init__.py": "",
+ "pdb.py": """
+ def check():
+ assert 1 == 2
+ """,
+ "test_pdb.py": """
+ def test():
+ import pdb
+ assert pdb.check()
+ """,
+ }
+ )
+ # Disable debugging plugin itself to avoid:
+ # > INTERNALERROR> AttributeError: module 'pdb' has no attribute 'set_trace'
+ result = pytester.runpytest_subprocess("-p", "no:debugging", "-vv")
+ result.stdout.fnmatch_lines(
+ [
+ " def check():",
+ "> assert 1 == 2",
+ "E assert 1 == 2",
+ "E +1",
+ "E -2",
+ "",
+ "pdb.py:2: AssertionError",
+ "*= 1 failed in *",
+ ]
+ )
+ assert result.ret == 1
+
+
+def test_tee_stdio_captures_and_live_prints(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import sys
+ def test_simple():
+ print ("@this is stdout@")
+ print ("@this is stderr@", file=sys.stderr)
+ """
+ )
+ result = pytester.runpytest_subprocess(
+ testpath,
+ "--capture=tee-sys",
+ "--junitxml=output.xml",
+ "-o",
+ "junit_logging=all",
+ )
+
+ # ensure stdout/stderr were 'live printed'
+ result.stdout.fnmatch_lines(["*@this is stdout@*"])
+ 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()
+ assert "@this is stdout@\n" in fullXml
+ assert "@this is stderr@\n" in fullXml
+
+
+@pytest.mark.skipif(
+ sys.platform == "win32",
+ reason="Windows raises `OSError: [Errno 22] Invalid argument` instead",
+)
+def test_no_brokenpipeerror_message(pytester: Pytester) -> None:
+ """Ensure that the broken pipe error message is suppressed.
+
+ In some Python versions, it reaches sys.unraisablehook, in others
+ a BrokenPipeError exception is propagated, but either way it prints
+ to stderr on shutdown, so checking nothing is printed is enough.
+ """
+ popen = pytester.popen((*pytester._getpytestargs(), "--help"))
+ popen.stdout.close()
+ ret = popen.wait()
+ assert popen.stderr.read() == b""
+ assert ret == 1
+
+ # Cleanup.
+ popen.stderr.close()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/code/test_code.py b/testing/web-platform/tests/tools/third_party/pytest/testing/code/test_code.py
new file mode 100644
index 0000000000..33809528a0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/code/test_code.py
@@ -0,0 +1,212 @@
+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
+
+
+def test_ne() -> None:
+ code1 = Code(compile('foo = "bar"', "", "exec"))
+ assert code1 == code1
+ code2 = Code(compile('foo = "baz"', "", "exec"))
+ assert code2 != code1
+
+
+def test_code_gives_back_name_for_not_existing_file() -> None:
+ name = "abc-123"
+ co_code = compile("pass\n", name, "exec")
+ assert co_code.co_filename == name
+ code = Code(co_code)
+ assert str(code.path) == name
+ assert code.fullsource is None
+
+
+def test_code_from_function_with_class() -> None:
+ class A:
+ pass
+
+ with pytest.raises(TypeError):
+ Code.from_function(A)
+
+
+def x() -> None:
+ raise NotImplementedError()
+
+
+def test_code_fullsource() -> None:
+ code = Code.from_function(x)
+ full = code.fullsource
+ assert "test_code_fullsource()" in str(full)
+
+
+def test_code_source() -> None:
+ code = Code.from_function(x)
+ src = code.source()
+ expected = """def x() -> None:
+ raise NotImplementedError()"""
+ assert str(src) == expected
+
+
+def test_frame_getsourcelineno_myself() -> None:
+ def func() -> FrameType:
+ return sys._getframe(0)
+
+ f = Frame(func())
+ source, lineno = f.code.fullsource, f.lineno
+ assert source is not None
+ assert source[lineno].startswith(" return sys._getframe(0)")
+
+
+def test_getstatement_empty_fullsource() -> None:
+ def func() -> FrameType:
+ return sys._getframe(0)
+
+ f = Frame(func())
+ with mock.patch.object(f.code.__class__, "fullsource", None):
+ assert f.statement == Source("")
+
+
+def test_code_from_func() -> None:
+ co = Code.from_function(test_frame_getsourcelineno_myself)
+ assert co.firstlineno
+ assert co.path
+
+
+def test_unicode_handling() -> None:
+ value = "ąć".encode()
+
+ def f() -> None:
+ raise Exception(value)
+
+ excinfo = pytest.raises(Exception, f)
+ str(excinfo)
+
+
+def test_code_getargs() -> None:
+ def f1(x):
+ raise NotImplementedError()
+
+ c1 = Code.from_function(f1)
+ assert c1.getargs(var=True) == ("x",)
+
+ def f2(x, *y):
+ raise NotImplementedError()
+
+ c2 = Code.from_function(f2)
+ assert c2.getargs(var=True) == ("x", "y")
+
+ def f3(x, **z):
+ raise NotImplementedError()
+
+ c3 = Code.from_function(f3)
+ assert c3.getargs(var=True) == ("x", "z")
+
+ def f4(x, *y, **z):
+ raise NotImplementedError()
+
+ c4 = Code.from_function(f4)
+ assert c4.getargs(var=True) == ("x", "y", "z")
+
+
+def test_frame_getargs() -> None:
+ def f1(x) -> FrameType:
+ return sys._getframe(0)
+
+ fr1 = Frame(f1("a"))
+ assert fr1.getargs(var=True) == [("x", "a")]
+
+ def f2(x, *y) -> FrameType:
+ return sys._getframe(0)
+
+ fr2 = Frame(f2("a", "b", "c"))
+ assert fr2.getargs(var=True) == [("x", "a"), ("y", ("b", "c"))]
+
+ def f3(x, **z) -> FrameType:
+ return sys._getframe(0)
+
+ fr3 = Frame(f3("a", b="c"))
+ assert fr3.getargs(var=True) == [("x", "a"), ("z", {"b": "c"})]
+
+ def f4(x, *y, **z) -> FrameType:
+ return sys._getframe(0)
+
+ fr4 = 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) -> None:
+ try:
+ if False:
+ pass
+ else:
+ assert False
+ except AssertionError:
+ exci = ExceptionInfo.from_current()
+ assert exci.getrepr()
+
+ def test_from_current_with_missing(self) -> None:
+ with pytest.raises(AssertionError, match="no current exception"):
+ ExceptionInfo.from_current()
+
+
+class TestTracebackEntry:
+ def test_getsource(self) -> None:
+ try:
+ if False:
+ pass
+ else:
+ assert False
+ except AssertionError:
+ exci = ExceptionInfo.from_current()
+ entry = exci.traceback[0]
+ source = entry.getsource()
+ assert source is not None
+ assert len(source) == 6
+ assert "assert False" in source[5]
+
+ def test_tb_entry_str(self):
+ try:
+ assert False
+ except AssertionError:
+ exci = ExceptionInfo.from_current()
+ pattern = r" File '.*test_code.py':\d+ in test_tb_entry_str\n assert False"
+ entry = str(exci.traceback[0])
+ assert re.match(pattern, entry)
+
+
+class TestReprFuncArgs:
+ def test_not_raise_exception_with_mixed_encoding(self, tw_mock) -> None:
+ args = [("unicode_string", "São Paulo"), ("utf8_string", b"S\xc3\xa3o Paulo")]
+
+ r = ReprFuncArgs(args)
+ r.toterminal(tw_mock)
+
+ assert (
+ tw_mock.lines[0]
+ == r"unicode_string = São Paulo, utf8_string = b'S\xc3\xa3o Paulo'"
+ )
+
+
+def test_ExceptionChainRepr():
+ """Test ExceptionChainRepr, especially with regard to being hashable."""
+ try:
+ raise ValueError()
+ except ValueError:
+ excinfo1 = ExceptionInfo.from_current()
+ excinfo2 = ExceptionInfo.from_current()
+
+ repr1 = excinfo1.getrepr()
+ repr2 = excinfo2.getrepr()
+ assert repr1 != repr2
+
+ assert isinstance(repr1, ExceptionChainRepr)
+ assert hash(repr1) != hash(repr2)
+ assert repr1 is not excinfo1.getrepr()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/code/test_excinfo.py b/testing/web-platform/tests/tools/third_party/pytest/testing/code/test_excinfo.py
new file mode 100644
index 0000000000..61aa4406ad
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/code/test_excinfo.py
@@ -0,0 +1,1470 @@
+import importlib
+import io
+import operator
+import queue
+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
+from _pytest._code.code import ExceptionChainRepr
+from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import FormattedExcinfo
+from _pytest._io import TerminalWriter
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pathlib import bestrelpath
+from _pytest.pathlib import import_path
+from _pytest.pytester import LineMatcher
+from _pytest.pytester import Pytester
+
+
+if TYPE_CHECKING:
+ from _pytest._code.code import _TracebackStyle
+
+
+@pytest.fixture
+def limited_recursion_depth():
+ before = sys.getrecursionlimit()
+ sys.setrecursionlimit(150)
+ yield
+ sys.setrecursionlimit(before)
+
+
+def test_excinfo_simple() -> None:
+ try:
+ raise ValueError
+ except ValueError:
+ info = _pytest._code.ExceptionInfo.from_current()
+ assert info.type == ValueError
+
+
+def test_excinfo_from_exc_info_simple() -> None:
+ try:
+ raise ValueError
+ except ValueError as e:
+ assert e.__traceback__ is not None
+ info = _pytest._code.ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
+ assert info.type == ValueError
+
+
+def test_excinfo_getstatement():
+ def g():
+ raise ValueError
+
+ def f():
+ g()
+
+ try:
+ f()
+ except ValueError:
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ linenumbers = [
+ f.__code__.co_firstlineno - 1 + 4,
+ f.__code__.co_firstlineno - 1 + 1,
+ g.__code__.co_firstlineno - 1 + 1,
+ ]
+ values = list(excinfo.traceback)
+ foundlinenumbers = [x.lineno for x in values]
+ 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 = _pytest._code.ExceptionInfo.from_current()
+
+ 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")
+
+ def test_traceback_entry_getsource_in_construct(self):
+ def xyz():
+ try:
+ raise ValueError
+ except somenoname: # type: ignore[name-defined] # noqa: F821
+ pass # pragma: no cover
+
+ try:
+ xyz()
+ except NameError:
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ else:
+ assert False, "did not raise NameError"
+
+ tb = excinfo.traceback
+ source = tb[-1].getsource()
+ assert source is not None
+ assert source.deindent().lines == [
+ "def xyz():",
+ " try:",
+ " raise ValueError",
+ " except somenoname: # type: ignore[name-defined] # noqa: F821",
+ ]
+
+ def test_traceback_cut(self) -> None:
+ co = _pytest._code.Code.from_function(f)
+ path, firstlineno = co.path, co.firstlineno
+ assert isinstance(path, Path)
+ 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, 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]
+ basedir = Path(pytest.__file__).parent
+ newtraceback = excinfo.traceback.cut(excludepath=basedir)
+ for x in newtraceback:
+ assert isinstance(x.path, Path)
+ assert basedir not in x.path.parents
+ 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
+
+ @pytest.mark.parametrize(
+ "tracebackhide, matching",
+ [
+ (lambda info: True, True),
+ (lambda info: False, False),
+ (operator.methodcaller("errisinstance", ValueError), True),
+ (operator.methodcaller("errisinstance", IndexError), False),
+ ],
+ )
+ def test_traceback_filter_selective(self, tracebackhide, matching):
+ def f():
+ #
+ raise ValueError
+ #
+
+ def g():
+ #
+ __tracebackhide__ = tracebackhide
+ f()
+ #
+
+ def h():
+ #
+ g()
+ #
+
+ excinfo = pytest.raises(ValueError, h)
+ traceback = excinfo.traceback
+ ntraceback = traceback.filter()
+ print(f"old: {traceback!r}")
+ print(f"new: {ntraceback!r}")
+
+ if matching:
+ assert len(ntraceback) == len(traceback) - 2
+ else:
+ # -1 because of the __tracebackhide__ in pytest.raises
+ assert len(ntraceback) == len(traceback) - 1
+
+ def test_traceback_recursion_index(self):
+ def f(n):
+ if n < 10:
+ n += 1
+ f(n)
+
+ excinfo = pytest.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, 25)
+ monkeypatch.delattr(excinfo.traceback.__class__, "recursionindex")
+ repr = excinfo.getrepr()
+ assert "RuntimeError: hello" in str(repr.reprcrash)
+
+ def test_traceback_no_recursion_index(self) -> None:
+ def do_stuff() -> None:
+ raise RuntimeError
+
+ def reraise_me() -> None:
+ import sys
+
+ exc, val, tb = sys.exc_info()
+ assert val is not None
+ raise val.with_traceback(tb)
+
+ def f(n: int) -> None:
+ try:
+ do_stuff()
+ except BaseException:
+ reraise_me()
+
+ excinfo = pytest.raises(RuntimeError, f, 8)
+ assert excinfo is not None
+ traceback = excinfo.traceback
+ recindex = traceback.recursionindex()
+ assert recindex is None
+
+ def test_traceback_messy_recursion(self):
+ # XXX: simplified locally testable version
+ decorator = pytest.importorskip("decorator").decorator
+
+ def log(f, *k, **kw):
+ print(f"{k} {kw}")
+ f(*k, **kw)
+
+ log = decorator(log)
+
+ def fail():
+ raise ValueError("")
+
+ fail = log(log(fail))
+
+ excinfo = pytest.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 = pytest.raises(ValueError, f)
+ tb = excinfo.traceback
+ entry = tb.getcrashentry()
+ 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"
+
+ def test_traceback_getcrashentry_empty(self):
+ def g():
+ __tracebackhide__ = True
+ raise ValueError
+
+ def f():
+ __tracebackhide__ = True
+ 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"
+
+
+def test_excinfo_exconly():
+ excinfo = pytest.raises(ValueError, h)
+ assert excinfo.exconly().startswith("ValueError")
+ with pytest.raises(ValueError) as excinfo:
+ raise ValueError("hello\nworld")
+ msg = excinfo.exconly(tryshort=True)
+ assert msg.startswith("ValueError")
+ assert msg.endswith("world")
+
+
+def test_excinfo_repr_str() -> None:
+ excinfo1 = pytest.raises(ValueError, h)
+ assert repr(excinfo1) == "<ExceptionInfo ValueError() tblen=4>"
+ assert str(excinfo1) == "<ExceptionInfo ValueError() tblen=4>"
+
+ class CustomException(Exception):
+ def __repr__(self):
+ return "custom_repr"
+
+ def raises() -> None:
+ raise CustomException()
+
+ excinfo2 = pytest.raises(CustomException, raises)
+ assert repr(excinfo2) == "<ExceptionInfo custom_repr tblen=2>"
+ assert str(excinfo2) == "<ExceptionInfo custom_repr tblen=2>"
+
+
+def test_excinfo_for_later() -> None:
+ e = ExceptionInfo[BaseException].for_later()
+ assert "for raises" in repr(e)
+ assert "for raises" in str(e)
+
+
+def test_excinfo_errisinstance():
+ excinfo = pytest.raises(ValueError, h)
+ assert excinfo.errisinstance(ValueError)
+
+
+def test_excinfo_no_sourcecode():
+ try:
+ exec("raise ValueError()")
+ except ValueError:
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ s = str(excinfo.traceback[-1])
+ assert 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()}}:")
+
+ jinja2 = pytest.importorskip("jinja2")
+ loader = jinja2.FileSystemLoader(str(tmp_path))
+ env = jinja2.Environment(loader=loader)
+ template = env.get_template("test.txt")
+ 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
+ if isinstance(item.path, Path) and item.path.name == "test.txt":
+ assert str(item.source) == "{{ h()}}:"
+
+
+def test_entrysource_Queue_example():
+ try:
+ queue.Queue().get(timeout=0.001)
+ except queue.Empty:
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ 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() -> None:
+ try:
+ queue.Queue().get(timeout=0.001)
+ except queue.Empty:
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ entry = excinfo.traceback[-1]
+ path = entry.path
+ assert isinstance(path, Path)
+ assert path.name.lower() == "queue.py"
+ assert path.exists()
+
+
+def test_match_succeeds():
+ with pytest.raises(ZeroDivisionError) as excinfo:
+ 0 // 0
+ excinfo.match(r".*zero.*")
+
+
+def test_match_raises_error(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_division_zero():
+ with pytest.raises(ZeroDivisionError) as excinfo:
+ 0 / 0
+ excinfo.match(r'[123]+')
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret != 0
+
+ exc_msg = "Regex pattern '[[]123[]]+' does not match 'division by zero'."
+ result.stdout.fnmatch_lines([f"E * AssertionError: {exc_msg}"])
+ 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}"]
+ )
+
+
+class TestFormattedExcinfo:
+ @pytest.fixture
+ def importasmod(self, tmp_path: Path, _sys_snapshot):
+ def importasmod(source):
+ source = textwrap.dedent(source)
+ modpath = tmp_path.joinpath("mod.py")
+ tmp_path.joinpath("__init__.py").touch()
+ modpath.write_text(source)
+ importlib.invalidate_caches()
+ return import_path(modpath, root=tmp_path)
+
+ return importasmod
+
+ def test_repr_source(self):
+ pr = FormattedExcinfo()
+ source = _pytest._code.Source(
+ """\
+ def f(x):
+ pass
+ """
+ ).strip()
+ pr.flow_marker = "|" # type: ignore[misc]
+ lines = pr.get_source(source, 0)
+ assert len(lines) == 2
+ assert lines[0] == "| def f(x):"
+ assert lines[1] == " pass"
+
+ def test_repr_source_excinfo(self) -> None:
+ """Check if indentation is right."""
+ try:
+
+ def f():
+ 1 / 0
+
+ f()
+
+ except BaseException:
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ else:
+ assert False, "did not raise"
+
+ pr = FormattedExcinfo()
+ source = pr._getentrysource(excinfo.traceback[-1])
+ assert source is not None
+ lines = pr.get_source(source, 1, excinfo)
+ for line in lines:
+ print(line)
+ assert lines == [
+ " def f():",
+ "> 1 / 0",
+ "E ZeroDivisionError: division by zero",
+ ]
+
+ def test_repr_source_not_existing(self):
+ pr = FormattedExcinfo()
+ co = compile("raise ValueError()", "", "exec")
+ try:
+ exec(co)
+ except ValueError:
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ repr = pr.repr_excinfo(excinfo)
+ assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
+ assert repr.chain[0][0].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 = _pytest._code.ExceptionInfo.from_current()
+ repr = pr.repr_excinfo(excinfo)
+ assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
+ assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
+
+ def test_repr_source_failing_fullsource(self, monkeypatch) -> None:
+ pr = FormattedExcinfo()
+
+ try:
+ 1 / 0
+ except ZeroDivisionError:
+ excinfo = ExceptionInfo.from_current()
+
+ with monkeypatch.context() as m:
+ m.setattr(_pytest._code.Code, "fullsource", property(lambda self: None))
+ repr = pr.repr_excinfo(excinfo)
+
+ assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
+ assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
+
+ def test_repr_local(self) -> None:
+ p = FormattedExcinfo(showlocals=True)
+ loc = {"y": 5, "z": 7, "x": 3, "@x": 2, "__builtins__": {}}
+ reprlocals = p.repr_locals(loc)
+ assert reprlocals is not None
+ 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_local_with_error(self) -> None:
+ class ObjWithErrorInRepr:
+ def __repr__(self):
+ raise NotImplementedError
+
+ p = FormattedExcinfo(showlocals=True, truncate_locals=False)
+ loc = {"x": ObjWithErrorInRepr(), "__builtins__": {}}
+ reprlocals = p.repr_locals(loc)
+ assert reprlocals is not None
+ assert reprlocals.lines
+ assert reprlocals.lines[0] == "__builtins__ = <builtins>"
+ assert "[NotImplementedError() raised in repr()]" in reprlocals.lines[1]
+
+ def test_repr_local_with_exception_in_class_property(self) -> None:
+ class ExceptionWithBrokenClass(Exception):
+ # Type ignored because it's bypassed intentionally.
+ @property # type: ignore
+ def __class__(self):
+ raise TypeError("boom!")
+
+ class ObjWithErrorInRepr:
+ def __repr__(self):
+ raise ExceptionWithBrokenClass()
+
+ p = FormattedExcinfo(showlocals=True, truncate_locals=False)
+ loc = {"x": ObjWithErrorInRepr(), "__builtins__": {}}
+ reprlocals = p.repr_locals(loc)
+ assert reprlocals is not None
+ assert reprlocals.lines
+ assert reprlocals.lines[0] == "__builtins__ = <builtins>"
+ assert "[ExceptionWithBrokenClass() raised in repr()]" in reprlocals.lines[1]
+
+ def test_repr_local_truncated(self) -> None:
+ loc = {"l": [i for i in range(10)]}
+ p = FormattedExcinfo(showlocals=True)
+ truncated_reprlocals = p.repr_locals(loc)
+ assert truncated_reprlocals is not None
+ assert truncated_reprlocals.lines
+ assert truncated_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, ...]"
+
+ q = FormattedExcinfo(showlocals=True, truncate_locals=False)
+ full_reprlocals = q.repr_locals(loc)
+ assert full_reprlocals is not None
+ assert full_reprlocals.lines
+ assert full_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
+
+ def test_repr_tracebackentry_lines(self, importasmod) -> None:
+ mod = importasmod(
+ """
+ def func1():
+ raise ValueError("hello\\nworld")
+ """
+ )
+ excinfo = pytest.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.reprfileloc
+ assert loc is not None
+ assert loc.path == mod.__file__
+ assert loc.lineno == 3
+ # assert loc.message == "ValueError: hello"
+
+ def test_repr_tracebackentry_lines2(self, importasmod, tw_mock) -> None:
+ mod = importasmod(
+ """
+ def func1(m, x, y, z):
+ raise ValueError("hello\\nworld")
+ """
+ )
+ excinfo = pytest.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 is not None
+ 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 is not None
+ assert repr_entry.reprfuncargs.args == reprfuncargs.args
+ repr_entry.toterminal(tw_mock)
+ assert tw_mock.lines[0] == "m = " + repr("m" * 90)
+ assert tw_mock.lines[1] == "x = 5, y = 13"
+ assert tw_mock.lines[2] == "z = " + repr("z" * 120)
+
+ def test_repr_tracebackentry_lines_var_kw_args(self, importasmod, tw_mock) -> None:
+ mod = importasmod(
+ """
+ def func1(x, *y, **z):
+ raise ValueError("hello\\nworld")
+ """
+ )
+ excinfo = pytest.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 is not None
+ 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
+ assert repr_entry.reprfuncargs.args == reprfuncargs.args
+ repr_entry.toterminal(tw_mock)
+ assert tw_mock.lines[0] == "x = 'a', y = ('b',), z = {'c': 'd'}"
+
+ def test_repr_tracebackentry_short(self, importasmod) -> None:
+ mod = importasmod(
+ """
+ def func1():
+ raise ValueError("hello")
+ def entry():
+ func1()
+ """
+ )
+ excinfo = pytest.raises(ValueError, mod.entry)
+ p = FormattedExcinfo(style="short")
+ reprtb = p.repr_traceback_entry(excinfo.traceback[-2])
+ lines = reprtb.lines
+ basename = Path(mod.__file__).name
+ assert lines[0] == " func1()"
+ assert reprtb.reprfileloc is not None
+ 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 reprtb.reprfileloc is not None
+ 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 = pytest.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 = pytest.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) -> None:
+ mod = importasmod(
+ """
+ def func1():
+ raise ValueError("hello")
+ def entry():
+ func1()
+ """
+ )
+ 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()
+ 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) -> None:
+ mod = importasmod(
+ """
+ def f(x):
+ raise ValueError(x)
+ def entry():
+ f(0)
+ """
+ )
+ excinfo = pytest.raises(ValueError, mod.entry)
+
+ styles: Tuple[_TracebackStyle, ...] = ("long", "short")
+ for style in styles:
+ 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.chain[0][0]
+ assert len(repr.chain[0][0].reprentries) == len(reprtb.reprentries)
+ assert repr.reprcrash is not None
+ assert repr.reprcrash.path.endswith("mod.py")
+ assert repr.reprcrash.message == "ValueError: 0"
+
+ def test_repr_traceback_with_invalid_cwd(self, importasmod, monkeypatch) -> None:
+ mod = importasmod(
+ """
+ def f(x):
+ raise ValueError(x)
+ def entry():
+ f(0)
+ """
+ )
+ excinfo = pytest.raises(ValueError, mod.entry)
+
+ p = FormattedExcinfo(abspath=False)
+
+ raised = 0
+
+ orig_path_cwd = Path.cwd
+
+ def raiseos():
+ nonlocal raised
+ upframe = sys._getframe().f_back
+ assert upframe is not None
+ if upframe.f_code.co_name == "_makepath":
+ # Only raise with expected calls, but not via e.g. inspect for
+ # py38-windows.
+ raised += 1
+ raise OSError(2, "custom_oserror")
+ return orig_path_cwd()
+
+ monkeypatch.setattr(Path, "cwd", raiseos)
+ assert p._makepath(Path(__file__)) == __file__
+ assert raised == 1
+ repr_tb = p.repr_traceback(excinfo)
+
+ matcher = LineMatcher(str(repr_tb).splitlines())
+ matcher.fnmatch_lines(
+ [
+ "def entry():",
+ "> f(0)",
+ "",
+ f"{mod.__file__}:5: ",
+ "_ _ *",
+ "",
+ " def f(x):",
+ "> raise ValueError(x)",
+ "E ValueError: 0",
+ "",
+ f"{mod.__file__}:3: ValueError",
+ ]
+ )
+ assert raised == 3
+
+ def test_repr_excinfo_addouterr(self, importasmod, tw_mock):
+ mod = importasmod(
+ """
+ def entry():
+ raise ValueError()
+ """
+ )
+ excinfo = pytest.raises(ValueError, mod.entry)
+ repr = excinfo.getrepr()
+ repr.addsection("title", "content")
+ repr.toterminal(tw_mock)
+ assert tw_mock.lines[-1] == "content"
+ assert tw_mock.lines[-2] == ("-", "title")
+
+ def test_repr_excinfo_reprcrash(self, importasmod) -> None:
+ mod = importasmod(
+ """
+ def entry():
+ raise ValueError()
+ """
+ )
+ excinfo = pytest.raises(ValueError, mod.entry)
+ repr = excinfo.getrepr()
+ assert repr.reprcrash is not None
+ 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 = pytest.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)
+
+ def test_reprexcinfo_getrepr(self, importasmod) -> None:
+ mod = importasmod(
+ """
+ def f(x):
+ raise ValueError(x)
+ def entry():
+ f(0)
+ """
+ )
+ excinfo = pytest.raises(ValueError, mod.entry)
+
+ styles: Tuple[_TracebackStyle, ...] = ("short", "long", "no")
+ for style in styles:
+ for showlocals in (True, False):
+ repr = excinfo.getrepr(style=style, showlocals=showlocals)
+ assert repr.reprtraceback.style == style
+
+ assert isinstance(repr, ExceptionChainRepr)
+ for r in repr.chain:
+ assert r[0].style == style
+
+ def test_reprexcinfo_unicode(self):
+ from _pytest._code.code import TerminalRepr
+
+ class MyRepr(TerminalRepr):
+ def toterminal(self, tw: TerminalWriter) -> None:
+ tw.line("я")
+
+ x = str(MyRepr())
+ assert x == "я"
+
+ def test_toterminal_long(self, importasmod, tw_mock):
+ mod = importasmod(
+ """
+ def g(x):
+ raise ValueError(x)
+ def f():
+ g(3)
+ """
+ )
+ excinfo = pytest.raises(ValueError, mod.f)
+ excinfo.traceback = excinfo.traceback.filter()
+ repr = excinfo.getrepr()
+ repr.toterminal(tw_mock)
+ assert tw_mock.lines[0] == ""
+ tw_mock.lines.pop(0)
+ assert tw_mock.lines[0] == " def f():"
+ assert tw_mock.lines[1] == "> g(3)"
+ assert tw_mock.lines[2] == ""
+ line = tw_mock.get_write_msg(3)
+ assert line.endswith("mod.py")
+ assert tw_mock.lines[4] == (":5: ")
+ assert tw_mock.lines[5] == ("_ ", None)
+ assert tw_mock.lines[6] == ""
+ assert tw_mock.lines[7] == " def g(x):"
+ assert tw_mock.lines[8] == "> raise ValueError(x)"
+ assert tw_mock.lines[9] == "E ValueError: 3"
+ assert tw_mock.lines[10] == ""
+ line = tw_mock.get_write_msg(11)
+ assert line.endswith("mod.py")
+ assert tw_mock.lines[12] == ":3: ValueError"
+
+ def test_toterminal_long_missing_source(
+ self, importasmod, tmp_path: Path, tw_mock
+ ) -> None:
+ mod = importasmod(
+ """
+ def g(x):
+ raise ValueError(x)
+ def f():
+ g(3)
+ """
+ )
+ excinfo = pytest.raises(ValueError, mod.f)
+ tmp_path.joinpath("mod.py").unlink()
+ excinfo.traceback = excinfo.traceback.filter()
+ repr = excinfo.getrepr()
+ repr.toterminal(tw_mock)
+ assert tw_mock.lines[0] == ""
+ tw_mock.lines.pop(0)
+ assert tw_mock.lines[0] == "> ???"
+ assert tw_mock.lines[1] == ""
+ line = tw_mock.get_write_msg(2)
+ assert line.endswith("mod.py")
+ assert tw_mock.lines[3] == ":5: "
+ assert tw_mock.lines[4] == ("_ ", None)
+ assert tw_mock.lines[5] == ""
+ assert tw_mock.lines[6] == "> ???"
+ assert tw_mock.lines[7] == "E ValueError: 3"
+ assert tw_mock.lines[8] == ""
+ line = tw_mock.get_write_msg(9)
+ assert line.endswith("mod.py")
+ assert tw_mock.lines[10] == ":3: ValueError"
+
+ def test_toterminal_long_incomplete_source(
+ self, importasmod, tmp_path: Path, tw_mock
+ ) -> None:
+ mod = importasmod(
+ """
+ def g(x):
+ raise ValueError(x)
+ def f():
+ g(3)
+ """
+ )
+ excinfo = pytest.raises(ValueError, mod.f)
+ tmp_path.joinpath("mod.py").write_text("asdf")
+ excinfo.traceback = excinfo.traceback.filter()
+ repr = excinfo.getrepr()
+ repr.toterminal(tw_mock)
+ assert tw_mock.lines[0] == ""
+ tw_mock.lines.pop(0)
+ assert tw_mock.lines[0] == "> ???"
+ assert tw_mock.lines[1] == ""
+ line = tw_mock.get_write_msg(2)
+ assert line.endswith("mod.py")
+ assert tw_mock.lines[3] == ":5: "
+ assert tw_mock.lines[4] == ("_ ", None)
+ assert tw_mock.lines[5] == ""
+ assert tw_mock.lines[6] == "> ???"
+ assert tw_mock.lines[7] == "E ValueError: 3"
+ assert tw_mock.lines[8] == ""
+ line = tw_mock.get_write_msg(9)
+ assert line.endswith("mod.py")
+ assert tw_mock.lines[10] == ":3: ValueError"
+
+ def test_toterminal_long_filenames(
+ self, importasmod, tw_mock, monkeypatch: MonkeyPatch
+ ) -> None:
+ mod = importasmod(
+ """
+ def f():
+ raise ValueError()
+ """
+ )
+ excinfo = pytest.raises(ValueError, mod.f)
+ path = Path(mod.__file__)
+ monkeypatch.chdir(path.parent)
+ repr = excinfo.getrepr(abspath=False)
+ repr.toterminal(tw_mock)
+ x = bestrelpath(Path.cwd(), path)
+ if len(x) < len(str(path)):
+ msg = tw_mock.get_write_msg(-2)
+ assert msg == "mod.py"
+ assert tw_mock.lines[-1] == ":3: ValueError"
+
+ repr = excinfo.getrepr(abspath=True)
+ repr.toterminal(tw_mock)
+ msg = tw_mock.get_write_msg(-2)
+ assert msg == str(path)
+ line = tw_mock.lines[-1]
+ assert line == ":3: ValueError"
+
+ @pytest.mark.parametrize(
+ "reproptions",
+ [
+ pytest.param(
+ {
+ "style": style,
+ "showlocals": showlocals,
+ "funcargs": funcargs,
+ "tbfilter": tbfilter,
+ },
+ id="style={},showlocals={},funcargs={},tbfilter={}".format(
+ style, showlocals, funcargs, tbfilter
+ ),
+ )
+ for style in ["long", "short", "line", "no", "native", "value", "auto"]
+ for showlocals in (True, False)
+ for tbfilter in (True, False)
+ for funcargs in (True, False)
+ ],
+ )
+ def test_format_excinfo(self, reproptions: Dict[str, Any]) -> None:
+ def bar():
+ assert False, "some error"
+
+ def foo():
+ bar()
+
+ # using inline functions as opposed to importasmod so we get source code lines
+ # in the tracebacks (otherwise getinspect doesn't find the source code).
+ with pytest.raises(AssertionError) as excinfo:
+ foo()
+ file = io.StringIO()
+ tw = TerminalWriter(file=file)
+ repr = excinfo.getrepr(**reproptions)
+ repr.toterminal(tw)
+ assert file.getvalue()
+
+ def test_traceback_repr_style(self, importasmod, tw_mock):
+ mod = importasmod(
+ """
+ def f():
+ g()
+ def g():
+ h()
+ def h():
+ i()
+ def i():
+ 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")
+ r = excinfo.getrepr(style="long")
+ r.toterminal(tw_mock)
+ for line in tw_mock.lines:
+ print(line)
+ assert tw_mock.lines[0] == ""
+ assert tw_mock.lines[1] == " def f():"
+ assert tw_mock.lines[2] == "> g()"
+ assert tw_mock.lines[3] == ""
+ msg = tw_mock.get_write_msg(4)
+ assert msg.endswith("mod.py")
+ assert tw_mock.lines[5] == ":3: "
+ assert tw_mock.lines[6] == ("_ ", None)
+ tw_mock.get_write_msg(7)
+ assert tw_mock.lines[8].endswith("in g")
+ assert tw_mock.lines[9] == " h()"
+ tw_mock.get_write_msg(10)
+ assert tw_mock.lines[11].endswith("in h")
+ assert tw_mock.lines[12] == " i()"
+ assert tw_mock.lines[13] == ("_ ", None)
+ assert tw_mock.lines[14] == ""
+ assert tw_mock.lines[15] == " def i():"
+ assert tw_mock.lines[16] == "> raise ValueError()"
+ assert tw_mock.lines[17] == "E ValueError"
+ assert tw_mock.lines[18] == ""
+ msg = tw_mock.get_write_msg(19)
+ msg.endswith("mod.py")
+ assert tw_mock.lines[20] == ":9: ValueError"
+
+ def test_exc_chain_repr(self, importasmod, tw_mock):
+ mod = importasmod(
+ """
+ class Err(Exception):
+ pass
+ def f():
+ try:
+ g()
+ except Exception as e:
+ raise Err() from e
+ finally:
+ h()
+ def g():
+ raise ValueError()
+
+ def h():
+ raise AttributeError()
+ """
+ )
+ excinfo = pytest.raises(AttributeError, mod.f)
+ r = excinfo.getrepr(style="long")
+ r.toterminal(tw_mock)
+ for line in tw_mock.lines:
+ print(line)
+ assert tw_mock.lines[0] == ""
+ assert tw_mock.lines[1] == " def f():"
+ assert tw_mock.lines[2] == " try:"
+ assert tw_mock.lines[3] == "> g()"
+ assert tw_mock.lines[4] == ""
+ line = tw_mock.get_write_msg(5)
+ assert line.endswith("mod.py")
+ assert tw_mock.lines[6] == ":6: "
+ assert tw_mock.lines[7] == ("_ ", None)
+ assert tw_mock.lines[8] == ""
+ assert tw_mock.lines[9] == " def g():"
+ assert tw_mock.lines[10] == "> raise ValueError()"
+ assert tw_mock.lines[11] == "E ValueError"
+ assert tw_mock.lines[12] == ""
+ line = tw_mock.get_write_msg(13)
+ assert line.endswith("mod.py")
+ assert tw_mock.lines[14] == ":12: ValueError"
+ assert tw_mock.lines[15] == ""
+ assert (
+ tw_mock.lines[16]
+ == "The above exception was the direct cause of the following exception:"
+ )
+ assert tw_mock.lines[17] == ""
+ assert tw_mock.lines[18] == " def f():"
+ assert tw_mock.lines[19] == " try:"
+ assert tw_mock.lines[20] == " g()"
+ assert tw_mock.lines[21] == " except Exception as e:"
+ assert tw_mock.lines[22] == "> raise Err() from e"
+ assert tw_mock.lines[23] == "E test_exc_chain_repr0.mod.Err"
+ assert tw_mock.lines[24] == ""
+ line = tw_mock.get_write_msg(25)
+ assert line.endswith("mod.py")
+ assert tw_mock.lines[26] == ":8: Err"
+ assert tw_mock.lines[27] == ""
+ assert (
+ tw_mock.lines[28]
+ == "During handling of the above exception, another exception occurred:"
+ )
+ assert tw_mock.lines[29] == ""
+ assert tw_mock.lines[30] == " def f():"
+ assert tw_mock.lines[31] == " try:"
+ assert tw_mock.lines[32] == " g()"
+ assert tw_mock.lines[33] == " except Exception as e:"
+ assert tw_mock.lines[34] == " raise Err() from e"
+ assert tw_mock.lines[35] == " finally:"
+ assert tw_mock.lines[36] == "> h()"
+ assert tw_mock.lines[37] == ""
+ line = tw_mock.get_write_msg(38)
+ assert line.endswith("mod.py")
+ assert tw_mock.lines[39] == ":10: "
+ assert tw_mock.lines[40] == ("_ ", None)
+ assert tw_mock.lines[41] == ""
+ assert tw_mock.lines[42] == " def h():"
+ assert tw_mock.lines[43] == "> raise AttributeError()"
+ assert tw_mock.lines[44] == "E AttributeError"
+ assert tw_mock.lines[45] == ""
+ line = tw_mock.get_write_msg(46)
+ assert line.endswith("mod.py")
+ assert tw_mock.lines[47] == ":15: AttributeError"
+
+ @pytest.mark.parametrize("mode", ["from_none", "explicit_suppress"])
+ def test_exc_repr_chain_suppression(self, importasmod, mode, tw_mock):
+ """Check that exc repr does not show chained exceptions in Python 3.
+ - When the exception is raised with "from None"
+ - Explicitly suppressed with "chain=False" to ExceptionInfo.getrepr().
+ """
+ raise_suffix = " from None" if mode == "from_none" else ""
+ mod = importasmod(
+ """
+ def f():
+ try:
+ g()
+ except Exception:
+ 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")
+ r.toterminal(tw_mock)
+ for line in tw_mock.lines:
+ print(line)
+ assert tw_mock.lines[0] == ""
+ assert tw_mock.lines[1] == " def f():"
+ 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[6] == "E AttributeError"
+ assert tw_mock.lines[7] == ""
+ line = tw_mock.get_write_msg(8)
+ assert line.endswith("mod.py")
+ assert tw_mock.lines[9] == ":6: AttributeError"
+ assert len(tw_mock.lines) == 10
+
+ @pytest.mark.parametrize(
+ "reason, description",
+ [
+ pytest.param(
+ "cause",
+ "The above exception was the direct cause of the following exception:",
+ id="cause",
+ ),
+ pytest.param(
+ "context",
+ "During handling of the above exception, another exception occurred:",
+ id="context",
+ ),
+ ],
+ )
+ def test_exc_chain_repr_without_traceback(self, importasmod, reason, description):
+ """
+ Handle representation of exception chains where one of the exceptions doesn't have a
+ real traceback, such as those raised in a subprocess submitted by the multiprocessing
+ module (#1984).
+ """
+ exc_handling_code = " from e" if reason == "cause" else ""
+ mod = importasmod(
+ """
+ def f():
+ try:
+ g()
+ except Exception as e:
+ 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:
+ mod.f()
+
+ # emulate the issue described in #1984
+ attr = "__%s__" % reason
+ getattr(excinfo.value, attr).__traceback__ = None
+
+ r = excinfo.getrepr()
+ file = io.StringIO()
+ tw = TerminalWriter(file=file)
+ tw.hasmarkup = False
+ r.toterminal(tw)
+
+ matcher = LineMatcher(file.getvalue().splitlines())
+ matcher.fnmatch_lines(
+ [
+ "ValueError: invalid value",
+ description,
+ "* except Exception as e:",
+ "> * raise RuntimeError('runtime problem')" + exc_handling_code,
+ "E *RuntimeError: runtime problem",
+ ]
+ )
+
+ def test_exc_chain_repr_cycle(self, importasmod, tw_mock):
+ mod = importasmod(
+ """
+ class Err(Exception):
+ pass
+ def fail():
+ return 0 / 0
+ def reraise():
+ try:
+ fail()
+ except ZeroDivisionError as e:
+ raise Err() from e
+ def unreraise():
+ try:
+ reraise()
+ except Err as e:
+ raise e.__cause__
+ """
+ )
+ excinfo = pytest.raises(ZeroDivisionError, mod.unreraise)
+ r = excinfo.getrepr(style="short")
+ r.toterminal(tw_mock)
+ out = "\n".join(line for line in tw_mock.lines if isinstance(line, str))
+ expected_out = textwrap.dedent(
+ """\
+ :13: in unreraise
+ reraise()
+ :10: in reraise
+ raise Err() from e
+ E test_exc_chain_repr_cycle0.mod.Err
+
+ During handling of the above exception, another exception occurred:
+ :15: in unreraise
+ raise e.__cause__
+ :8: in reraise
+ fail()
+ :5: in fail
+ return 0 / 0
+ E ZeroDivisionError: division by zero"""
+ )
+ assert out == expected_out
+
+ def test_exec_type_error_filter(self, importasmod):
+ """See #7742"""
+ mod = importasmod(
+ """\
+ def f():
+ exec("a = 1", {}, [])
+ """
+ )
+ with pytest.raises(TypeError) as excinfo:
+ mod.f()
+ # previously crashed with `AttributeError: list has no attribute get`
+ excinfo.traceback.filter()
+
+
+@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] = "☹"
+ else:
+ msg = "☹".encode(encoding)
+ try:
+ raise RuntimeError(msg)
+ except RuntimeError:
+ e_info = ExceptionInfo.from_current()
+ formatter = FormattedExcinfo(style=style)
+ repr_traceback = formatter.repr_traceback(e_info)
+ assert repr_traceback is not None
+
+
+def test_cwd_deleted(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import os
+
+ def test(tmp_path):
+ os.chdir(tmp_path)
+ tmp_path.unlink()
+ assert False
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 1 failed in *"])
+ result.stdout.no_fnmatch_line("*INTERNALERROR*")
+ result.stderr.no_fnmatch_line("*INTERNALERROR*")
+
+
+def test_regression_nagative_line_index(pytester: Pytester) -> None:
+ """
+ With Python 3.10 alphas, there was an INTERNALERROR reported in
+ https://github.com/pytest-dev/pytest/pull/8227
+ This test ensures it does not regress.
+ """
+ pytester.makepyfile(
+ """
+ import ast
+ import pytest
+
+
+ def test_literal_eval():
+ with pytest.raises(ValueError, match="^$"):
+ ast.literal_eval("pytest")
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 1 failed in *"])
+ result.stdout.no_fnmatch_line("*INTERNALERROR*")
+ result.stderr.no_fnmatch_line("*INTERNALERROR*")
+
+
+@pytest.mark.usefixtures("limited_recursion_depth")
+def test_exception_repr_extraction_error_on_recursion():
+ """
+ Ensure we can properly detect a recursion error even
+ if some locals raise error on comparison (#2459).
+ """
+
+ class numpy_like:
+ def __eq__(self, other):
+ if type(other) is numpy_like:
+ raise ValueError(
+ "The truth value of an array "
+ "with more than one element is ambiguous."
+ )
+
+ def a(x):
+ return b(numpy_like())
+
+ def b(x):
+ return a(numpy_like())
+
+ with pytest.raises(RuntimeError) as excinfo:
+ a(numpy_like())
+
+ matcher = LineMatcher(str(excinfo.getrepr()).splitlines())
+ matcher.fnmatch_lines(
+ [
+ "!!! Recursion error detected, but an error occurred locating the origin of recursion.",
+ "*The following exception happened*",
+ "*ValueError: The truth value of an array*",
+ ]
+ )
+
+
+@pytest.mark.usefixtures("limited_recursion_depth")
+def test_no_recursion_index_on_recursion_error():
+ """
+ Ensure that we don't break in case we can't find the recursion index
+ during a recursion error (#2486).
+ """
+
+ class RecursionDepthError:
+ def __getattr__(self, attr):
+ return getattr(self, "_" + attr)
+
+ with pytest.raises(RuntimeError) as excinfo:
+ RecursionDepthError().trigger
+ assert "maximum recursion" in str(excinfo.getrepr())
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/code/test_source.py b/testing/web-platform/tests/tools/third_party/pytest/testing/code/test_source.py
new file mode 100644
index 0000000000..9f7be5e245
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/code/test_source.py
@@ -0,0 +1,656 @@
+# 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
+from _pytest._code import Frame
+from _pytest._code import getfslineno
+from _pytest._code import Source
+from _pytest.pathlib import import_path
+
+
+def test_source_str_function() -> None:
+ x = Source("3")
+ assert str(x) == "3"
+
+ x = Source(" 3")
+ assert str(x) == "3"
+
+ x = Source(
+ """
+ 3
+ """
+ )
+ assert str(x) == "\n3"
+
+
+def test_source_from_function() -> None:
+ source = Source(test_source_str_function)
+ assert str(source).startswith("def test_source_str_function() -> None:")
+
+
+def test_source_from_method() -> None:
+ class TestClass:
+ def test_method(self):
+ pass
+
+ source = Source(TestClass().test_method)
+ assert source.lines == ["def test_method(self):", " pass"]
+
+
+def test_source_from_lines() -> None:
+ lines = ["a \n", "b\n", "c"]
+ source = Source(lines)
+ assert source.lines == ["a ", "b", "c"]
+
+
+def test_source_from_inner_function() -> None:
+ def f():
+ raise NotImplementedError()
+
+ source = Source(f)
+ assert str(source).startswith("def f():")
+
+
+def test_source_strips() -> None:
+ source = Source("")
+ assert source == Source()
+ assert str(source) == ""
+ assert source.strip() == source
+
+
+def test_source_strip_multiline() -> None:
+ source = Source()
+ source.lines = ["", " hello", " "]
+ source2 = source.strip()
+ assert source2.lines == [" hello"]
+
+
+class TestAccesses:
+ def setup_class(self) -> None:
+ self.source = Source(
+ """\
+ def f(x):
+ pass
+ def g(x):
+ pass
+ """
+ )
+
+ def test_getrange(self) -> None:
+ x = self.source[0:2]
+ assert len(x.lines) == 2
+ assert str(x) == "def f(x):\n pass"
+
+ def test_getrange_step_not_supported(self) -> None:
+ with pytest.raises(IndexError, match=r"step"):
+ self.source[::2]
+
+ def test_getline(self) -> None:
+ x = self.source[0]
+ assert x == "def f(x):"
+
+ def test_len(self) -> None:
+ assert len(self.source) == 4
+
+ def test_iter(self) -> None:
+ values = [x for x in self.source]
+ assert len(values) == 4
+
+
+class TestSourceParsing:
+ def setup_class(self) -> None:
+ self.source = Source(
+ """\
+ def f(x):
+ assert (x ==
+ 3 +
+ 4)
+ """
+ ).strip()
+
+ def test_getstatement(self) -> None:
+ # 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) -> None:
+ # print str(self.source)
+ source = Source(
+ """hello('''
+ ''')"""
+ )
+ s = source.getstatement(0)
+ assert s == source
+ s = source.getstatement(1)
+ assert s == source
+
+ def test_getstatementrange_within_constructs(self) -> None:
+ 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) -> None:
+ source = Source(
+ """\
+ try:
+ x = (
+ y +
+ z)
+ except:
+ pass
+ """
+ )
+ assert len(source) == 6
+ assert source.getstatementrange(2) == (1, 4)
+
+ def test_getstatementrange_bug2(self) -> None:
+ 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) -> None:
+ 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) -> None:
+ source = Source("if xxx:\n from .collections import something")
+ r = source.getstatementrange(1)
+ assert r == (1, 2)
+
+ def test_getstatementrange_with_syntaxerror_issue7(self) -> None:
+ source = Source(":")
+ pytest.raises(SyntaxError, lambda: source.getstatementrange(0))
+
+
+def test_getstartingblock_singleline() -> None:
+ class A:
+ def __init__(self, *args) -> None:
+ frame = sys._getframe(1)
+ self.source = Frame(frame).statement
+
+ x = A("x", "y")
+
+ values = [i for i in x.source.lines if i.strip()]
+ assert len(values) == 1
+
+
+def test_getline_finally() -> None:
+ def c() -> None:
+ pass
+
+ with pytest.raises(TypeError) as excinfo:
+ teardown = None
+ try:
+ c(1) # type: ignore
+ finally:
+ if teardown:
+ teardown() # type: ignore[unreachable]
+ source = excinfo.traceback[-1].statement
+ assert str(source).strip() == "c(1) # type: ignore"
+
+
+def test_getfuncsource_dynamic() -> None:
+ def f():
+ raise NotImplementedError()
+
+ def g():
+ pass # pragma: no cover
+
+ f_source = Source(f)
+ g_source = Source(g)
+ assert str(f_source).strip() == "def f():\n raise NotImplementedError()"
+ assert str(g_source).strip() == "def g():\n pass # pragma: no cover"
+
+
+def test_getfuncsource_with_multine_string() -> None:
+ def f():
+ c = """while True:
+ pass
+"""
+
+ expected = '''\
+ def f():
+ c = """while True:
+ pass
+"""
+'''
+ assert str(Source(f)) == expected.rstrip()
+
+
+def test_deindent() -> None:
+ from _pytest._code.source import deindent as deindent
+
+ assert deindent(["\tfoo", "\tbar"]) == ["foo", "bar"]
+
+ 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(_sys_snapshot, tmp_path: Path) -> None:
+ # this test fails because the implicit inspect.getsource(A) below
+ # does not return the "x = 1" last line.
+ source = Source(
+ """
+ class A:
+ def method(self):
+ x = 1
+ """
+ )
+ path = tmp_path.joinpath("a.py")
+ path.write_text(str(source))
+ mod: Any = import_path(path, root=tmp_path)
+ s2 = Source(mod.A)
+ assert str(source).strip() == str(s2).strip()
+
+
+if True:
+
+ def x():
+ pass
+
+
+def test_source_fallback() -> None:
+ src = Source(x)
+ expected = """def x():
+ pass"""
+ assert str(src) == expected
+
+
+def test_findsource_fallback() -> None:
+ from _pytest._code.source import findsource
+
+ src, lineno = findsource(x)
+ assert src is not None
+ assert "test_findsource_simple" in str(src)
+ assert src[lineno] == " def x():"
+
+
+def test_findsource(monkeypatch) -> None:
+ from _pytest._code.source import findsource
+
+ filename = "<pytest-test_findsource>"
+ 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]
+
+ src, lineno = findsource(co)
+ assert src is not None
+ assert "if 1:" in str(src)
+
+ d: Dict[str, Any] = {}
+ eval(co, d)
+ src, lineno = findsource(d["x"])
+ assert src is not None
+ assert "if 1:" in str(src)
+ assert src[lineno] == " def x():"
+
+
+def test_getfslineno() -> None:
+ def f(x) -> None:
+ raise NotImplementedError()
+
+ fspath, lineno = getfslineno(f)
+
+ assert isinstance(fspath, Path)
+ assert fspath.name == "test_source.py"
+ assert lineno == f.__code__.co_firstlineno - 1 # see findsource
+
+ class A:
+ pass
+
+ fspath, lineno = getfslineno(A)
+
+ _, A_lineno = inspect.findsource(A)
+ assert isinstance(fspath, Path)
+ assert fspath.name == "test_source.py"
+ assert lineno == A_lineno
+
+ assert getfslineno(3) == ("", -1)
+
+ class B:
+ pass
+
+ B.__name__ = B.__qualname__ = "B2"
+ assert getfslineno(B)[1] == -1
+
+
+def test_code_of_object_instance_with_call() -> None:
+ class A:
+ pass
+
+ pytest.raises(TypeError, lambda: Source(A()))
+
+ class WithCall:
+ def __call__(self) -> None:
+ pass
+
+ code = Code.from_function(WithCall())
+ assert "pass" in str(code.source())
+
+ class Hello:
+ def __call__(self) -> None:
+ pass
+
+ pytest.raises(TypeError, lambda: Code.from_function(Hello))
+
+
+def getstatement(lineno: int, source) -> Source:
+ from _pytest._code.source import getstatementrange_ast
+
+ src = Source(source)
+ ast, start, end = getstatementrange_ast(lineno, src)
+ return src[start:end]
+
+
+def test_oneline() -> None:
+ source = getstatement(0, "raise ValueError")
+ assert str(source) == "raise ValueError"
+
+
+def test_comment_and_no_newline_at_end() -> None:
+ from _pytest._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() -> None:
+ source = getstatement(0, "raise ValueError\n#hello")
+ assert str(source) == "raise ValueError"
+
+
+def test_comments() -> None:
+ 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() -> None:
+ 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_source_with_decorator() -> None:
+ """Test behavior with Source / Code().source with regard to decorators."""
+ from _pytest.compat import get_real_func
+
+ @pytest.mark.foo
+ def deco_mark():
+ assert False
+
+ src = inspect.getsource(deco_mark)
+ assert textwrap.indent(str(Source(deco_mark)), " ") + "\n" == src
+ assert src.startswith(" @pytest.mark.foo")
+
+ @pytest.fixture
+ def deco_fixture():
+ assert False
+
+ src = inspect.getsource(deco_fixture)
+ assert src == " @pytest.fixture\n def deco_fixture():\n assert False\n"
+ # currently Source does not unwrap decorators, testing the
+ # existing behavior here for explicitness, but perhaps we should revisit/change this
+ # in the future
+ assert str(Source(deco_fixture)).startswith("@functools.wraps(function)")
+ assert (
+ textwrap.indent(str(Source(get_real_func(deco_fixture))), " ") + "\n" == src
+ )
+
+
+def test_single_line_else() -> None:
+ source = getstatement(1, "if False: 2\nelse: 3")
+ assert str(source) == "else: 3"
+
+
+def test_single_line_finally() -> None:
+ source = getstatement(1, "try: 1\nfinally: 3")
+ assert str(source) == "finally: 3"
+
+
+def test_issue55() -> None:
+ 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 test_multiline() -> None:
+ source = getstatement(
+ 0,
+ """\
+raise ValueError(
+ 23
+)
+x = 3
+""",
+ )
+ assert str(source) == "raise ValueError(\n 23\n)"
+
+
+class TestTry:
+ def setup_class(self) -> None:
+ self.source = """\
+try:
+ raise ValueError
+except Something:
+ raise IndexError(1)
+else:
+ raise KeyError()
+"""
+
+ def test_body(self) -> None:
+ source = getstatement(1, self.source)
+ assert str(source) == " raise ValueError"
+
+ def test_except_line(self) -> None:
+ source = getstatement(2, self.source)
+ assert str(source) == "except Something:"
+
+ def test_except_body(self) -> None:
+ source = getstatement(3, self.source)
+ assert str(source) == " raise IndexError(1)"
+
+ def test_else(self) -> None:
+ source = getstatement(5, self.source)
+ assert str(source) == " raise KeyError()"
+
+
+class TestTryFinally:
+ def setup_class(self) -> None:
+ self.source = """\
+try:
+ raise ValueError
+finally:
+ raise IndexError(1)
+"""
+
+ def test_body(self) -> None:
+ source = getstatement(1, self.source)
+ assert str(source) == " raise ValueError"
+
+ def test_finally(self) -> None:
+ source = getstatement(3, self.source)
+ assert str(source) == " raise IndexError(1)"
+
+
+class TestIf:
+ def setup_class(self) -> None:
+ self.source = """\
+if 1:
+ y = 3
+elif False:
+ y = 5
+else:
+ y = 7
+"""
+
+ def test_body(self) -> None:
+ source = getstatement(1, self.source)
+ assert str(source) == " y = 3"
+
+ def test_elif_clause(self) -> None:
+ source = getstatement(2, self.source)
+ assert str(source) == "elif False:"
+
+ def test_elif(self) -> None:
+ source = getstatement(3, self.source)
+ assert str(source) == " y = 5"
+
+ def test_else(self) -> None:
+ source = getstatement(5, self.source)
+ assert str(source) == " y = 7"
+
+
+def test_semicolon() -> None:
+ s = """\
+hello ; pytest.skip()
+"""
+ source = getstatement(0, s)
+ assert str(source) == s.strip()
+
+
+def test_def_online() -> None:
+ s = """\
+def func(): raise ValueError(42)
+
+def something():
+ pass
+"""
+ source = getstatement(0, s)
+ assert str(source) == "def func(): raise ValueError(42)"
+
+
+def test_decorator() -> None:
+ s = """\
+def foo(f):
+ pass
+
+@foo
+def bar():
+ pass
+ """
+ source = getstatement(3, s)
+ assert "@foo" in str(source)
+
+
+def XXX_test_expression_multiline() -> None:
+ source = """\
+something
+'''
+'''"""
+ result = getstatement(1, source)
+ assert str(result) == "'''\n'''"
+
+
+def test_getstartingblock_multiline() -> None:
+ class A:
+ def __init__(self, *args):
+ frame = sys._getframe(1)
+ self.source = Frame(frame).statement
+
+ # fmt: off
+ x = A('x',
+ 'y'
+ ,
+ 'z')
+ # fmt: on
+ values = [i for i in x.source.lines if i.strip()]
+ assert len(values) == 4
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/conftest.py
new file mode 100644
index 0000000000..107aad86b2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/conftest.py
@@ -0,0 +1,216 @@
+import re
+import sys
+from typing import List
+
+import pytest
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pytester import Pytester
+
+if sys.gettrace():
+
+ @pytest.fixture(autouse=True)
+ def restore_tracing():
+ """Restore tracing function (when run with Coverage.py).
+
+ https://bugs.python.org/issue37011
+ """
+ orig_trace = sys.gettrace()
+ yield
+ if sys.gettrace() != orig_trace:
+ sys.settrace(orig_trace)
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_collection_modifyitems(items):
+ """Prefer faster tests.
+
+ Use a hookwrapper to do this in the beginning, so e.g. --ff still works
+ correctly.
+ """
+ fast_items = []
+ slow_items = []
+ slowest_items = []
+ neutral_items = []
+
+ spawn_names = {"spawn_pytest", "spawn"}
+
+ for item in items:
+ try:
+ fixtures = item.fixturenames
+ except AttributeError:
+ # doctest at least
+ # (https://github.com/pytest-dev/pytest/issues/5070)
+ neutral_items.append(item)
+ else:
+ if "pytester" in fixtures:
+ co_names = item.function.__code__.co_names
+ if spawn_names.intersection(co_names):
+ item.add_marker(pytest.mark.uses_pexpect)
+ slowest_items.append(item)
+ elif "runpytest_subprocess" in co_names:
+ slowest_items.append(item)
+ else:
+ slow_items.append(item)
+ item.add_marker(pytest.mark.slow)
+ else:
+ marker = item.get_closest_marker("slow")
+ if marker:
+ slowest_items.append(item)
+ else:
+ fast_items.append(item)
+
+ items[:] = fast_items + neutral_items + slow_items + slowest_items
+
+ yield
+
+
+@pytest.fixture
+def tw_mock():
+ """Returns a mock terminal writer"""
+
+ class TWMock:
+ WRITE = object()
+
+ def __init__(self):
+ self.lines = []
+ self.is_writing = False
+
+ def sep(self, sep, line=None):
+ self.lines.append((sep, line))
+
+ def write(self, msg, **kw):
+ self.lines.append((TWMock.WRITE, msg))
+
+ def _write_source(self, lines, indents=()):
+ if not indents:
+ indents = [""] * len(lines)
+ for indent, line in zip(indents, lines):
+ self.line(indent + line)
+
+ def line(self, line, **kw):
+ self.lines.append(line)
+
+ def markup(self, text, **kw):
+ return text
+
+ def get_write_msg(self, idx):
+ flag, msg = self.lines[idx]
+ assert flag == TWMock.WRITE
+ return msg
+
+ fullwidth = 80
+
+ return TWMock()
+
+
+@pytest.fixture
+def dummy_yaml_custom_test(pytester: Pytester):
+ """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
+ tests which needs custom items collected.
+ """
+ pytester.makeconftest(
+ """
+ import pytest
+
+ def pytest_collect_file(parent, file_path):
+ if file_path.suffix == ".yaml" and file_path.name.startswith("test"):
+ return YamlFile.from_parent(path=file_path, parent=parent)
+
+ class YamlFile(pytest.File):
+ def collect(self):
+ yield YamlItem.from_parent(name=self.path.name, parent=self)
+
+ class YamlItem(pytest.Item):
+ def runtest(self):
+ pass
+ """
+ )
+ pytester.makefile(".yaml", test1="")
+
+
+@pytest.fixture
+def pytester(pytester: Pytester, monkeypatch: MonkeyPatch) -> Pytester:
+ monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
+ return pytester
+
+
+@pytest.fixture(scope="session")
+def color_mapping():
+ """Returns a utility class which can replace keys in strings in the form "{NAME}"
+ by their equivalent ASCII codes in the terminal.
+
+ Used by tests which check the actual colors output by pytest.
+ """
+
+ class ColorMapping:
+ COLORS = {
+ "red": "\x1b[31m",
+ "green": "\x1b[32m",
+ "yellow": "\x1b[33m",
+ "bold": "\x1b[1m",
+ "reset": "\x1b[0m",
+ "kw": "\x1b[94m",
+ "hl-reset": "\x1b[39;49;00m",
+ "function": "\x1b[92m",
+ "number": "\x1b[94m",
+ "str": "\x1b[33m",
+ "print": "\x1b[96m",
+ }
+ RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()}
+
+ @classmethod
+ def format(cls, lines: List[str]) -> List[str]:
+ """Straightforward replacement of color names to their ASCII codes."""
+ return [line.format(**cls.COLORS) for line in lines]
+
+ @classmethod
+ def format_for_fnmatch(cls, lines: List[str]) -> List[str]:
+ """Replace color names for use with LineMatcher.fnmatch_lines"""
+ return [line.format(**cls.COLORS).replace("[", "[[]") for line in lines]
+
+ @classmethod
+ def format_for_rematch(cls, lines: List[str]) -> List[str]:
+ """Replace color names for use with LineMatcher.re_match_lines"""
+ return [line.format(**cls.RE_COLORS) for line in lines]
+
+ return ColorMapping
+
+
+@pytest.fixture
+def mock_timing(monkeypatch: MonkeyPatch):
+ """Mocks _pytest.timing with a known object that can be used to control timing in tests
+ deterministically.
+
+ pytest itself should always use functions from `_pytest.timing` instead of `time` directly.
+
+ This then allows us more control over time during testing, if testing code also
+ uses `_pytest.timing` functions.
+
+ 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
+ class MockTiming:
+
+ _current_time = attr.ib(default=1590150050.0)
+
+ def sleep(self, seconds):
+ self._current_time += seconds
+
+ def time(self):
+ return self._current_time
+
+ def patch(self):
+ from _pytest import timing
+
+ monkeypatch.setattr(timing, "sleep", self.sleep)
+ monkeypatch.setattr(timing, "time", self.time)
+ monkeypatch.setattr(timing, "perf_counter", self.time)
+
+ result = MockTiming()
+ result.patch()
+ return result
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/deprecated_test.py b/testing/web-platform/tests/tools/third_party/pytest/testing/deprecated_test.py
new file mode 100644
index 0000000000..9ac7fe1cac
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/deprecated_test.py
@@ -0,0 +1,310 @@
+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
+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:
+ pytester.syspathinsert()
+ pytester.makepyfile(**{plugin: ""})
+
+ with pytest.warns(pytest.PytestConfigWarning):
+ 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_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*"])
+
+
+def test_fscollector_gethookproxy_isinitpath(pytester: Pytester) -> None:
+ module = pytester.getmodulecol(
+ """
+ def test_foo(): pass
+ """,
+ withinit=True,
+ )
+ assert isinstance(module, pytest.Module)
+ package = module.parent
+ assert isinstance(package, pytest.Package)
+
+ with pytest.warns(pytest.PytestDeprecationWarning, match="gethookproxy"):
+ package.gethookproxy(pytester.path)
+
+ with pytest.warns(pytest.PytestDeprecationWarning, match="isinitpath"):
+ package.isinitpath(pytester.path)
+
+ # 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
+
+
+def test_strict_option_is_deprecated(pytester: Pytester) -> None:
+ """--strict is a deprecated alias to --strict-markers (#7530)."""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @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.",
+ ]
+ )
+
+
+def test_yield_fixture_is_deprecated() -> None:
+ with pytest.warns(DeprecationWarning, match=r"yield_fixture is deprecated"):
+
+ @pytest.yield_fixture
+ def fix():
+ assert False
+
+
+def test_private_is_deprecated() -> None:
+ class PrivateInit:
+ def __init__(self, foo: int, *, _ispytest: bool = False) -> None:
+ deprecated.check_ispytest(_ispytest)
+
+ with pytest.warns(
+ pytest.PytestDeprecationWarning, match="private pytest class or function"
+ ):
+ PrivateInit(10)
+
+ # Doesn't warn.
+ 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)
+
+ PATH_WARN_MATCH = r".*path: py\.path\.local\) argument is deprecated, please use \(collection_path: pathlib\.Path.*"
+ if hooktype == "ihook":
+ hooks = request.node.ihook
+ else:
+ hooks = request.config.hook
+
+ with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r:
+ l1 = sys._getframe().f_lineno
+ hooks.pytest_ignore_collect(
+ config=request.config, path=path, collection_path=tmp_path
+ )
+ l2 = sys._getframe().f_lineno
+
+ (record,) = r
+ assert record.filename == __file__
+ assert l1 < record.lineno < l2
+
+ hooks.pytest_ignore_collect(config=request.config, collection_path=tmp_path)
+
+ # Passing entirely *different* paths is an outright error.
+ with pytest.raises(ValueError, match=r"path.*fspath.*need to be equal"):
+ with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r:
+ hooks.pytest_ignore_collect(
+ config=request.config, path=path, collection_path=Path("/bla/bla")
+ )
+
+
+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_fail_with_msg_is_deprecated(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ 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 test_exit_with_msg_is_deprecated(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ 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 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.*",
+ ]
+ )
+
+
+def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
+ mod = pytester.getmodulecol("")
+
+ with pytest.warns(
+ pytest.PytestDeprecationWarning,
+ match=re.escape("The (fspath: py.path.local) argument to File is deprecated."),
+ ):
+ pytest.File.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:
+ with pytest.warns(
+ pytest.PytestDeprecationWarning,
+ match=re.escape("The pytest.Instance collector type is deprecated"),
+ ):
+ pytest.Instance
+
+ with pytest.warns(
+ pytest.PytestDeprecationWarning,
+ match=re.escape("The pytest.Instance collector type is deprecated"),
+ ):
+ from _pytest.python import Instance # noqa: F401
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/README.rst b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/README.rst
new file mode 100644
index 0000000000..97d0fda5c5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/README.rst
@@ -0,0 +1,9 @@
+Example test scripts
+=====================
+
+
+The files in this folder are not direct tests, but rather example test suites that demonstrate certain issues/behaviours.
+
+In the future we will move part of the content of the acceptance tests here in order to have directly testable code instead of writing out things and then running them in nested pytest sessions/subprocesses.
+
+This will aid debugging and comprehension.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/acceptance/fixture_mock_integration.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/acceptance/fixture_mock_integration.py
new file mode 100644
index 0000000000..5b00ac90e1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/acceptance/fixture_mock_integration.py
@@ -0,0 +1,16 @@
+"""Reproduces issue #3774"""
+from unittest import mock
+
+import pytest
+
+config = {"mykey": "ORIGINAL"}
+
+
+@pytest.fixture(scope="function")
+@mock.patch.dict(config, {"mykey": "MOCKED"})
+def my_fixture():
+ return config["mykey"]
+
+
+def test_foobar(my_fixture):
+ assert my_fixture == "MOCKED"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/pytest.ini b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/pytest.ini
new file mode 100644
index 0000000000..7c47955402
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+python_files = *.py
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/__init__.py
new file mode 100644
index 0000000000..9cd366295e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/__init__.py
@@ -0,0 +1,2 @@
+def test_init():
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/test_foo.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/test_foo.py
new file mode 100644
index 0000000000..8f2d73cfa4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/test_foo.py
@@ -0,0 +1,2 @@
+def test_foo():
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/__init__.pyi b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/__init__.pyi
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/__init__.pyi
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/conftest.py
new file mode 100644
index 0000000000..973ccc0c03
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/conftest.py
@@ -0,0 +1,2 @@
+def pytest_ignore_collect(collection_path):
+ return False
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/tests/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/tests/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py
new file mode 100644
index 0000000000..f174823854
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py
@@ -0,0 +1,2 @@
+def test():
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/test_foo.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/test_foo.py
new file mode 100644
index 0000000000..f174823854
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/test_foo.py
@@ -0,0 +1,2 @@
+def test():
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/__init__.pyi b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/__init__.pyi
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/__init__.pyi
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/conftest.py
new file mode 100644
index 0000000000..2da4ffe2fe
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/conftest.py
@@ -0,0 +1,2 @@
+class pytest_something:
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/test_foo.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/test_foo.py
new file mode 100644
index 0000000000..8f2d73cfa4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/test_foo.py
@@ -0,0 +1,2 @@
+def test_foo():
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/conftest_usageerror/__init__.pyi b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/conftest_usageerror/__init__.pyi
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/conftest_usageerror/__init__.pyi
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/conftest_usageerror/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/conftest_usageerror/conftest.py
new file mode 100644
index 0000000000..8973e4252d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/conftest_usageerror/conftest.py
@@ -0,0 +1,8 @@
+def pytest_configure(config):
+ import pytest
+
+ raise pytest.UsageError("hello")
+
+
+def pytest_unconfigure(config):
+ print("pytest_unconfigure_called")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses.py
new file mode 100644
index 0000000000..d96c90a91b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses.py
@@ -0,0 +1,14 @@
+from dataclasses import dataclass
+from dataclasses import field
+
+
+def test_dataclasses() -> None:
+ @dataclass
+ class SimpleDataObject:
+ field_a: int = field()
+ field_b: str = field()
+
+ left = SimpleDataObject(1, "b")
+ right = SimpleDataObject(1, "c")
+
+ assert left == right
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py
new file mode 100644
index 0000000000..7479c66c1b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py
@@ -0,0 +1,14 @@
+from dataclasses import dataclass
+from dataclasses import field
+
+
+def test_dataclasses_with_attribute_comparison_off() -> None:
+ @dataclass
+ class SimpleDataObject:
+ field_a: int = field()
+ field_b: str = field(compare=False)
+
+ left = SimpleDataObject(1, "b")
+ right = SimpleDataObject(1, "c")
+
+ assert left == right
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py
new file mode 100644
index 0000000000..4737ef904e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py
@@ -0,0 +1,14 @@
+from dataclasses import dataclass
+from dataclasses import field
+
+
+def test_dataclasses_verbose() -> None:
+ @dataclass
+ class SimpleDataObject:
+ field_a: int = field()
+ field_b: str = field()
+
+ left = SimpleDataObject(1, "b")
+ right = SimpleDataObject(1, "c")
+
+ assert left == right
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py
new file mode 100644
index 0000000000..0945790f00
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py
@@ -0,0 +1,44 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class S:
+ a: int
+ b: str
+
+
+@dataclass
+class C:
+ c: S
+ d: S
+
+
+@dataclass
+class C2:
+ e: C
+ f: S
+
+
+@dataclass
+class C3:
+ g: S
+ h: C2
+ i: str
+ j: str
+
+
+def test_recursive_dataclasses():
+ left = C3(
+ S(10, "ten"),
+ C2(C(S(1, "one"), S(2, "two")), S(2, "three")),
+ "equal",
+ "left",
+ )
+ right = C3(
+ S(20, "xxx"),
+ C2(C(S(1, "one"), S(2, "yyy")), S(3, "three")),
+ "equal",
+ "right",
+ )
+
+ assert left == right
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py
new file mode 100644
index 0000000000..0a4820c69b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py
@@ -0,0 +1,19 @@
+from dataclasses import dataclass
+from dataclasses import field
+
+
+def test_comparing_two_different_data_classes() -> None:
+ @dataclass
+ class SimpleDataObjectOne:
+ field_a: int = field()
+ field_b: str = field()
+
+ @dataclass
+ class SimpleDataObjectTwo:
+ field_a: int = field()
+ field_b: str = field()
+
+ left = SimpleDataObjectOne(1, "b")
+ right = SimpleDataObjectTwo(1, "c")
+
+ assert left != right # type: ignore[comparison-overlap]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/__main__.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/__main__.py
new file mode 100644
index 0000000000..e471d06d64
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/__main__.py
@@ -0,0 +1,2 @@
+def test_this_is_ignored():
+ assert True
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/test_normal_module.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/test_normal_module.py
new file mode 100644
index 0000000000..700cc9750c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/test_normal_module.py
@@ -0,0 +1,6 @@
+def test_doc():
+ """
+ >>> 10 > 5
+ True
+ """
+ assert False
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/__init__.pyi b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/__init__.pyi
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/__init__.pyi
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/conftest.py
new file mode 100644
index 0000000000..a7a5e9db80
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/conftest.py
@@ -0,0 +1,15 @@
+import pytest
+
+
+class CustomItem(pytest.Item):
+ def runtest(self):
+ pass
+
+
+class CustomFile(pytest.File):
+ def collect(self):
+ yield CustomItem.from_parent(name="foo", parent=self)
+
+
+def pytest_collect_file(file_path, parent):
+ return CustomFile.from_parent(path=file_path, parent=parent)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/foo/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/foo/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/foo/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/foo/test_foo.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/foo/test_foo.py
new file mode 100644
index 0000000000..f174823854
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/foo/test_foo.py
@@ -0,0 +1,2 @@
+def test():
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py
new file mode 100644
index 0000000000..be5adbeb6e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py
@@ -0,0 +1,7 @@
+import pytest
+
+
+@pytest.fixture
+def arg1(request):
+ with pytest.raises(pytest.FixtureLookupError):
+ request.getfixturevalue("arg2")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/test_in_sub1.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/test_in_sub1.py
new file mode 100644
index 0000000000..df36da1369
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/test_in_sub1.py
@@ -0,0 +1,2 @@
+def test_1(arg1):
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py
new file mode 100644
index 0000000000..00981c5dc1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py
@@ -0,0 +1,6 @@
+import pytest
+
+
+@pytest.fixture
+def arg2(request):
+ pytest.raises(Exception, request.getfixturevalue, "arg1")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/test_in_sub2.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/test_in_sub2.py
new file mode 100644
index 0000000000..1c34f94acc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/test_in_sub2.py
@@ -0,0 +1,2 @@
+def test_2(arg2):
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_detect_recursive_dependency_error.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_detect_recursive_dependency_error.py
new file mode 100644
index 0000000000..d1efcbb338
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_detect_recursive_dependency_error.py
@@ -0,0 +1,15 @@
+import pytest
+
+
+@pytest.fixture
+def fix1(fix2):
+ return 1
+
+
+@pytest.fixture
+def fix2(fix1):
+ return 1
+
+
+def test(fix1):
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/__init__.pyi b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/__init__.pyi
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/__init__.pyi
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/conftest.py
new file mode 100644
index 0000000000..5dfd2f7795
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/conftest.py
@@ -0,0 +1,6 @@
+import pytest
+
+
+@pytest.fixture
+def spam():
+ return "spam"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/__init__.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/conftest.py
new file mode 100644
index 0000000000..4e22ce5a13
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/conftest.py
@@ -0,0 +1,6 @@
+import pytest
+
+
+@pytest.fixture
+def spam(spam):
+ return spam * 2
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/test_spam.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/test_spam.py
new file mode 100644
index 0000000000..0d891fbb50
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/test_spam.py
@@ -0,0 +1,2 @@
+def test_spam(spam):
+ assert spam == "spamspam"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/__init__.pyi b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/__init__.pyi
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/__init__.pyi
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/conftest.py
new file mode 100644
index 0000000000..5dfd2f7795
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/conftest.py
@@ -0,0 +1,6 @@
+import pytest
+
+
+@pytest.fixture
+def spam():
+ return "spam"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/test_extend_fixture_conftest_module.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/test_extend_fixture_conftest_module.py
new file mode 100644
index 0000000000..46d1446f47
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/test_extend_fixture_conftest_module.py
@@ -0,0 +1,10 @@
+import pytest
+
+
+@pytest.fixture
+def spam(spam):
+ return spam * 2
+
+
+def test_spam(spam):
+ assert spam == "spamspam"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_module_class.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_module_class.py
new file mode 100644
index 0000000000..87a0c89411
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_module_class.py
@@ -0,0 +1,15 @@
+import pytest
+
+
+@pytest.fixture
+def spam():
+ return "spam"
+
+
+class TestSpam:
+ @pytest.fixture
+ def spam(self, spam):
+ return spam * 2
+
+ def test_spam(self, spam):
+ assert spam == "spamspam"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_basic.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_basic.py
new file mode 100644
index 0000000000..0661cb301f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_basic.py
@@ -0,0 +1,15 @@
+import pytest
+
+
+@pytest.fixture
+def some(request):
+ return request.function.__name__
+
+
+@pytest.fixture
+def other(request):
+ return 42
+
+
+def test_func(some, other):
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_classlevel.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_classlevel.py
new file mode 100644
index 0000000000..256b92a17d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_classlevel.py
@@ -0,0 +1,10 @@
+import pytest
+
+
+class TestClass:
+ @pytest.fixture
+ def something(self, request):
+ return request.instance
+
+ def test_method(self, something):
+ assert something is self
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_modulelevel.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_modulelevel.py
new file mode 100644
index 0000000000..e15dbd2ca4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_modulelevel.py
@@ -0,0 +1,15 @@
+import pytest
+
+
+@pytest.fixture
+def something(request):
+ return request.function.__name__
+
+
+class TestClass:
+ def test_method(self, something):
+ assert something == "test_method"
+
+
+def test_func(something):
+ assert something == "test_func"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookupfails.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookupfails.py
new file mode 100644
index 0000000000..b775203231
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookupfails.py
@@ -0,0 +1,10 @@
+import pytest
+
+
+@pytest.fixture
+def xyzsomething(request):
+ return 42
+
+
+def test_func(some):
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_fixture_named_request.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_fixture_named_request.py
new file mode 100644
index 0000000000..75514bf8b8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_fixture_named_request.py
@@ -0,0 +1,10 @@
+import pytest
+
+
+@pytest.fixture
+def request():
+ pass
+
+
+def test():
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py
new file mode 100644
index 0000000000..055a1220b1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py
@@ -0,0 +1,20 @@
+import pytest
+
+
+@pytest.fixture
+def dynamic():
+ pass
+
+
+@pytest.fixture
+def a(request):
+ request.getfixturevalue("dynamic")
+
+
+@pytest.fixture
+def b(a):
+ pass
+
+
+def test(b, request):
+ assert request.fixturenames == ["b", "request", "a", "dynamic"]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/__init__.pyi b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/__init__.pyi
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/__init__.pyi
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/conftest.py
new file mode 100644
index 0000000000..cb8f5d671e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/conftest.py
@@ -0,0 +1,14 @@
+import pytest
+
+
+class MyFile(pytest.File):
+ def collect(self):
+ return [MyItem.from_parent(name="hello", parent=self)]
+
+
+def pytest_collect_file(file_path, parent):
+ return MyFile.from_parent(path=file_path, parent=parent)
+
+
+class MyItem(pytest.Item):
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/test_hello.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/test_hello.py
new file mode 100644
index 0000000000..56444d1474
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/test_hello.py
@@ -0,0 +1,2 @@
+def test_hello():
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue_519.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue_519.py
new file mode 100644
index 0000000000..e44367fca0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/issue_519.py
@@ -0,0 +1,53 @@
+import pprint
+from typing import List
+from typing import Tuple
+
+import pytest
+
+
+def pytest_generate_tests(metafunc):
+ if "arg1" in metafunc.fixturenames:
+ metafunc.parametrize("arg1", ["arg1v1", "arg1v2"], scope="module")
+
+ if "arg2" in metafunc.fixturenames:
+ metafunc.parametrize("arg2", ["arg2v1", "arg2v2"], scope="function")
+
+
+@pytest.fixture(scope="session")
+def checked_order():
+ order: List[Tuple[str, str, str]] = []
+
+ yield order
+ pprint.pprint(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-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-arg2v2]", "fix2", "arg2v2"),
+ ]
+
+
+@pytest.fixture(scope="module")
+def fix1(request, arg1, checked_order):
+ checked_order.append((request.node.name, "fix1", arg1))
+ yield "fix1-" + arg1
+
+
+@pytest.fixture(scope="function")
+def fix2(request, fix1, arg2, checked_order):
+ checked_order.append((request.node.name, "fix2", arg2))
+ yield "fix2-" + arg2 + fix1
+
+
+def test_one(fix2):
+ pass
+
+
+def test_two(fix2):
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/junit-10.xsd b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/junit-10.xsd
new file mode 100644
index 0000000000..286fbf7c87
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/junit-10.xsd
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+The MIT License (MIT)
+
+Copyright (c) 2014, Gregory Boissinot
+
+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.
+-->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:simpleType name="SUREFIRE_TIME">
+ <xs:restriction base="xs:string">
+ <xs:pattern value="(([0-9]{0,3},)*[0-9]{3}|[0-9]{0,3})*(\.[0-9]{0,3})?"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <xs:complexType name="rerunType" mixed="true"> <!-- mixed (XML contains text) to be compatible with version previous than 2.22.1 -->
+ <xs:sequence>
+ <xs:element name="stackTrace" type="xs:string" minOccurs="0" /> <!-- optional to be compatible with version previous than 2.22.1 -->
+ <xs:element name="system-out" type="xs:string" minOccurs="0" />
+ <xs:element name="system-err" type="xs:string" minOccurs="0" />
+ </xs:sequence>
+ <xs:attribute name="message" type="xs:string" />
+ <xs:attribute name="type" type="xs:string" use="required" />
+ </xs:complexType>
+
+ <xs:element name="failure">
+ <xs:complexType mixed="true">
+ <xs:attribute name="type" type="xs:string"/>
+ <xs:attribute name="message" type="xs:string"/>
+ </xs:complexType>
+ </xs:element>
+
+ <xs:element name="error">
+ <xs:complexType mixed="true">
+ <xs:attribute name="type" type="xs:string"/>
+ <xs:attribute name="message" type="xs:string"/>
+ </xs:complexType>
+ </xs:element>
+
+ <xs:element name="skipped">
+ <xs:complexType mixed="true">
+ <xs:attribute name="type" type="xs:string"/>
+ <xs:attribute name="message" type="xs:string"/>
+ </xs:complexType>
+ </xs:element>
+
+ <xs:element name="properties">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element ref="property" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+
+ <xs:element name="property">
+ <xs:complexType>
+ <xs:attribute name="name" type="xs:string" use="required"/>
+ <xs:attribute name="value" type="xs:string" use="required"/>
+ </xs:complexType>
+ </xs:element>
+
+ <xs:element name="system-err" type="xs:string"/>
+ <xs:element name="system-out" type="xs:string"/>
+ <xs:element name="rerunFailure" type="rerunType"/>
+ <xs:element name="rerunError" type="rerunType"/>
+ <xs:element name="flakyFailure" type="rerunType"/>
+ <xs:element name="flakyError" type="rerunType"/>
+
+ <xs:element name="testcase">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:choice minOccurs="0" maxOccurs="unbounded">
+ <xs:element ref="skipped"/>
+ <xs:element ref="error"/>
+ <xs:element ref="failure"/>
+ <xs:element ref="rerunFailure" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element ref="rerunError" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element ref="flakyFailure" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element ref="flakyError" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element ref="system-out"/>
+ <xs:element ref="system-err"/>
+ </xs:choice>
+ </xs:sequence>
+ <xs:attribute name="name" type="xs:string" use="required"/>
+ <xs:attribute name="time" type="xs:string"/>
+ <xs:attribute name="classname" type="xs:string"/>
+ <xs:attribute name="group" type="xs:string"/>
+ </xs:complexType>
+ </xs:element>
+
+ <xs:element name="testsuite">
+ <xs:complexType>
+ <xs:choice minOccurs="0" maxOccurs="unbounded">
+ <xs:element ref="testsuite"/>
+ <xs:element ref="properties"/>
+ <xs:element ref="testcase"/>
+ <xs:element ref="system-out"/>
+ <xs:element ref="system-err"/>
+ </xs:choice>
+ <xs:attribute name="name" type="xs:string" use="required"/>
+ <xs:attribute name="tests" type="xs:string" use="required"/>
+ <xs:attribute name="failures" type="xs:string" use="required"/>
+ <xs:attribute name="errors" type="xs:string" use="required"/>
+ <xs:attribute name="group" type="xs:string" />
+ <xs:attribute name="time" type="SUREFIRE_TIME"/>
+ <xs:attribute name="skipped" type="xs:string" />
+ <xs:attribute name="timestamp" type="xs:string" />
+ <xs:attribute name="hostname" type="xs:string" />
+ <xs:attribute name="id" type="xs:string" />
+ <xs:attribute name="package" type="xs:string" />
+ <xs:attribute name="file" type="xs:string"/>
+ <xs:attribute name="log" type="xs:string"/>
+ <xs:attribute name="url" type="xs:string"/>
+ <xs:attribute name="version" type="xs:string"/>
+ </xs:complexType>
+ </xs:element>
+
+ <xs:element name="testsuites">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element ref="testsuite" minOccurs="0" maxOccurs="unbounded" />
+ </xs:sequence>
+ <xs:attribute name="name" type="xs:string" />
+ <xs:attribute name="time" type="SUREFIRE_TIME"/>
+ <xs:attribute name="tests" type="xs:string" />
+ <xs:attribute name="failures" type="xs:string" />
+ <xs:attribute name="errors" type="xs:string" />
+ </xs:complexType>
+ </xs:element>
+
+</xs:schema>
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/__init__.pyi b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/__init__.pyi
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/__init__.pyi
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/conftest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/conftest.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/conftest.py
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/test_marks_as_keywords.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/test_marks_as_keywords.py
new file mode 100644
index 0000000000..35a2c7b762
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/test_marks_as_keywords.py
@@ -0,0 +1,6 @@
+import pytest
+
+
+@pytest.mark.foo
+def test_mark():
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/.gitignore b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/.gitignore
new file mode 100644
index 0000000000..1c45c2ea35
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/.gitignore
@@ -0,0 +1 @@
+foo_*
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/generate_folders.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/generate_folders.py
new file mode 100644
index 0000000000..ff1eaf7d6b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/generate_folders.py
@@ -0,0 +1,27 @@
+import argparse
+import pathlib
+
+HERE = pathlib.Path(__file__).parent
+TEST_CONTENT = (HERE / "template_test.py").read_bytes()
+
+parser = argparse.ArgumentParser()
+parser.add_argument("numbers", nargs="*", type=int)
+
+
+def generate_folders(root, elements, *more_numbers):
+ fill_len = len(str(elements))
+ if more_numbers:
+ for i in range(elements):
+ new_folder = root.joinpath(f"foo_{i:0>{fill_len}}")
+ new_folder.mkdir()
+ new_folder.joinpath("__init__.py").write_bytes(TEST_CONTENT)
+ generate_folders(new_folder, *more_numbers)
+ else:
+ for i in range(elements):
+ new_test = root.joinpath(f"test_{i:0<{fill_len}}.py")
+ new_test.write_bytes(TEST_CONTENT)
+
+
+if __name__ == "__main__":
+ args = parser.parse_args()
+ generate_folders(HERE, *(args.numbers or (10, 100)))
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/template_test.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/template_test.py
new file mode 100644
index 0000000000..064ade190a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/template_test.py
@@ -0,0 +1,2 @@
+def test_x():
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/pytest.ini b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/pytest.ini
new file mode 100644
index 0000000000..ec5fe0e83a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+# dummy pytest.ini to ease direct running of example scripts
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/tmpdir/tmp_path_fixture.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/tmpdir/tmp_path_fixture.py
new file mode 100644
index 0000000000..8675eb2fa6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/tmpdir/tmp_path_fixture.py
@@ -0,0 +1,7 @@
+import pytest
+
+
+@pytest.mark.parametrize("a", [r"qwe/\abc"])
+def test_fixture(tmp_path, a):
+ assert tmp_path.is_dir()
+ assert list(tmp_path.iterdir()) == []
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_parametrized_fixture_error_message.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_parametrized_fixture_error_message.py
new file mode 100644
index 0000000000..d421ce927c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_parametrized_fixture_error_message.py
@@ -0,0 +1,14 @@
+import unittest
+
+import pytest
+
+
+@pytest.fixture(params=[1, 2])
+def two(request):
+ return request.param
+
+
+@pytest.mark.usefixtures("two")
+class TestSomethingElse(unittest.TestCase):
+ def test_two(self):
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip.py
new file mode 100644
index 0000000000..93f79bb3b2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip.py
@@ -0,0 +1,13 @@
+"""Skipping an entire subclass with unittest.skip() should *not* call setUp from a base class."""
+import unittest
+
+
+class Base(unittest.TestCase):
+ def setUp(self):
+ assert 0
+
+
+@unittest.skip("skip all tests")
+class Test(Base):
+ def test_foo(self):
+ assert 0
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_class.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_class.py
new file mode 100644
index 0000000000..4f251dcba1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_class.py
@@ -0,0 +1,14 @@
+"""Skipping an entire subclass with unittest.skip() should *not* call setUpClass from a base class."""
+import unittest
+
+
+class Base(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ assert 0
+
+
+@unittest.skip("skip all tests")
+class Test(Base):
+ def test_foo(self):
+ assert 0
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_module.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_module.py
new file mode 100644
index 0000000000..98befbe510
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_module.py
@@ -0,0 +1,12 @@
+"""setUpModule is always called, even if all tests in the module are skipped"""
+import unittest
+
+
+def setUpModule():
+ assert 0
+
+
+@unittest.skip("skip all tests")
+class Base(unittest.TestCase):
+ def test(self):
+ assert 0
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asyncio.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asyncio.py
new file mode 100644
index 0000000000..1cd2168604
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asyncio.py
@@ -0,0 +1,25 @@
+from typing import List
+from unittest import IsolatedAsyncioTestCase
+
+
+teardowns: List[None] = []
+
+
+class AsyncArguments(IsolatedAsyncioTestCase):
+ async def asyncTearDown(self):
+ teardowns.append(None)
+
+ async def test_something_async(self):
+ async def addition(x, y):
+ return x + y
+
+ self.assertEqual(await addition(2, 2), 4)
+
+ async def test_something_async_fails(self):
+ async def addition(x, y):
+ return x + y
+
+ self.assertEqual(await addition(2, 2), 3)
+
+ def test_teardowns(self):
+ assert len(teardowns) == 2
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asynctest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asynctest.py
new file mode 100644
index 0000000000..fb26617067
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asynctest.py
@@ -0,0 +1,23 @@
+"""Issue #7110"""
+import asyncio
+from typing import List
+
+import asynctest
+
+
+teardowns: List[None] = []
+
+
+class Test(asynctest.TestCase):
+ async def tearDown(self):
+ teardowns.append(None)
+
+ async def test_error(self):
+ await asyncio.sleep(0)
+ self.fail("failing on purpose")
+
+ async def test_ok(self):
+ await asyncio.sleep(0)
+
+ def test_teardowns(self):
+ assert len(teardowns) == 2
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_plain_async.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_plain_async.py
new file mode 100644
index 0000000000..78dfece684
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_plain_async.py
@@ -0,0 +1,6 @@
+import unittest
+
+
+class Test(unittest.TestCase):
+ async def test_foo(self):
+ assert False
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message.py
new file mode 100644
index 0000000000..6985caa440
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message.py
@@ -0,0 +1,21 @@
+import warnings
+
+import pytest
+
+
+def func(msg):
+ warnings.warn(UserWarning(msg))
+
+
+@pytest.mark.parametrize("i", range(5))
+def test_foo(i):
+ func("foo")
+
+
+def test_foo_1():
+ func("foo")
+
+
+@pytest.mark.parametrize("i", range(5))
+def test_bar(i):
+ func("bar")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py
new file mode 100644
index 0000000000..b8c11cb71c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py
@@ -0,0 +1,21 @@
+import warnings
+
+import pytest
+
+
+def func(msg):
+ warnings.warn(UserWarning(msg))
+
+
+@pytest.mark.parametrize("i", range(20))
+def test_foo(i):
+ func("foo")
+
+
+def test_foo_1():
+ func("foo")
+
+
+@pytest.mark.parametrize("i", range(20))
+def test_bar(i):
+ func("bar")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py
new file mode 100644
index 0000000000..636d04a550
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py
@@ -0,0 +1,5 @@
+from test_1 import func
+
+
+def test_2():
+ func("foo")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/examples/test_issue519.py b/testing/web-platform/tests/tools/third_party/pytest/testing/examples/test_issue519.py
new file mode 100644
index 0000000000..7b9c109889
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/examples/test_issue519.py
@@ -0,0 +1,7 @@
+from _pytest.pytester import Pytester
+
+
+def test_519(pytester: Pytester) -> None:
+ pytester.copy_example("issue_519.py")
+ res = pytester.runpytest("issue_519.py")
+ res.assert_outcomes(passed=8)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/.gitignore b/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/.gitignore
new file mode 100644
index 0000000000..b533190872
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/.gitignore
@@ -0,0 +1,3 @@
+build/
+dist/
+*.spec
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/create_executable.py b/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/create_executable.py
new file mode 100644
index 0000000000..998df7b1ca
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/create_executable.py
@@ -0,0 +1,11 @@
+"""Generate an executable with pytest runner embedded using PyInstaller."""
+if __name__ == "__main__":
+ import pytest
+ import subprocess
+
+ hidden = []
+ for x in pytest.freeze_includes():
+ hidden.extend(["--hidden-import", x])
+ hidden.extend(["--hidden-import", "distutils"])
+ args = ["pyinstaller", "--noconfirm"] + hidden + ["runtests_script.py"]
+ subprocess.check_call(" ".join(args), shell=True)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/runtests_script.py b/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/runtests_script.py
new file mode 100644
index 0000000000..591863016a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/runtests_script.py
@@ -0,0 +1,10 @@
+"""
+This is the script that is actually frozen into an executable: simply executes
+pytest main().
+"""
+
+if __name__ == "__main__":
+ import sys
+ import pytest
+
+ sys.exit(pytest.main())
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/tests/test_doctest.txt b/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/tests/test_doctest.txt
new file mode 100644
index 0000000000..e18a4b68cc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/tests/test_doctest.txt
@@ -0,0 +1,6 @@
+
+
+Testing doctest::
+
+ >>> 1 + 1
+ 2
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/tests/test_trivial.py b/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/tests/test_trivial.py
new file mode 100644
index 0000000000..08a55552ab
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/tests/test_trivial.py
@@ -0,0 +1,6 @@
+def test_upper():
+ assert "foo".upper() == "FOO"
+
+
+def test_lower():
+ assert "FOO".lower() == "foo"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/tox_run.py b/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/tox_run.py
new file mode 100644
index 0000000000..678a69c858
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/freeze/tox_run.py
@@ -0,0 +1,12 @@
+"""
+Called by tox.ini: uses the generated executable to run the tests in ./tests/
+directory.
+"""
+if __name__ == "__main__":
+ import os
+ import sys
+
+ executable = os.path.join(os.getcwd(), "dist", "runtests_script", "runtests_script")
+ if sys.platform.startswith("win"):
+ executable += ".exe"
+ sys.exit(os.system("%s tests" % executable))
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/io/test_saferepr.py b/testing/web-platform/tests/tools/third_party/pytest/testing/io/test_saferepr.py
new file mode 100644
index 0000000000..63d3af822b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/io/test_saferepr.py
@@ -0,0 +1,181 @@
+import pytest
+from _pytest._io.saferepr import _pformat_dispatch
+from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
+from _pytest._io.saferepr import saferepr
+
+
+def test_simple_repr():
+ assert saferepr(1) == "1"
+ assert saferepr(None) == "None"
+
+
+def test_maxsize():
+ s = saferepr("x" * 50, maxsize=25)
+ assert len(s) == 25
+ expected = repr("x" * 10 + "..." + "x" * 10)
+ assert s == expected
+
+
+def test_no_maxsize():
+ text = "x" * DEFAULT_REPR_MAX_SIZE * 10
+ s = saferepr(text, maxsize=None)
+ expected = repr(text)
+ assert s == expected
+
+
+def test_maxsize_error_on_instance():
+ 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() -> None:
+ class BrokenRepr:
+ def __init__(self, ex):
+ self.ex = ex
+
+ def __repr__(self):
+ raise self.ex
+
+ class BrokenReprException(Exception):
+ __str__ = None # type: ignore[assignment]
+ __repr__ = None # type: ignore[assignment]
+
+ assert "Exception" in saferepr(BrokenRepr(Exception("broken")))
+ s = saferepr(BrokenReprException("really broken"))
+ assert "TypeError" in s
+ assert "TypeError" in saferepr(BrokenRepr("string"))
+
+ none = None
+ try:
+ none() # type: ignore[misc]
+ except BaseException as exc:
+ exp_exc = repr(exc)
+ 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)
+ )
+ )
+
+
+def test_baseexception():
+ """Test saferepr() with BaseExceptions, which includes pytest outcomes."""
+
+ class RaisingOnStrRepr(BaseException):
+ def __init__(self, exc_types):
+ self.exc_types = exc_types
+
+ def raise_exc(self, *args):
+ try:
+ self.exc_type = self.exc_types.pop(0)
+ except IndexError:
+ pass
+ if hasattr(self.exc_type, "__call__"):
+ raise self.exc_type(*args)
+ raise self.exc_type
+
+ def __str__(self):
+ self.raise_exc("__str__")
+
+ def __repr__(self):
+ self.raise_exc("__repr__")
+
+ class BrokenObj:
+ def __init__(self, exc):
+ self.exc = exc
+
+ def __repr__(self):
+ raise self.exc
+
+ __str__ = __repr__
+
+ 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))
+ )
+ obj = BrokenObj(RaisingOnStrRepr([RaisingOnStrRepr([BaseException])]))
+ assert saferepr(obj) == (
+ "<[{!r} raised in repr()] BrokenObj object at 0x{:x}>".format(
+ baseexc_str, id(obj)
+ )
+ )
+
+ with pytest.raises(KeyboardInterrupt):
+ saferepr(BrokenObj(KeyboardInterrupt()))
+
+ with pytest.raises(SystemExit):
+ saferepr(BrokenObj(SystemExit()))
+
+ with pytest.raises(KeyboardInterrupt):
+ saferepr(BrokenObj(RaisingOnStrRepr([KeyboardInterrupt])))
+
+ with pytest.raises(SystemExit):
+ saferepr(BrokenObj(RaisingOnStrRepr([SystemExit])))
+
+ with pytest.raises(KeyboardInterrupt):
+ print(saferepr(BrokenObj(RaisingOnStrRepr([BaseException, KeyboardInterrupt]))))
+
+ with pytest.raises(SystemExit):
+ saferepr(BrokenObj(RaisingOnStrRepr([BaseException, SystemExit])))
+
+
+def test_buggy_builtin_repr():
+ # Simulate a case where a repr for a builtin raises.
+ # reprlib dispatches by type name, so use "int".
+
+ class int:
+ def __repr__(self):
+ raise ValueError("Buggy repr!")
+
+ assert "Buggy" in saferepr(int())
+
+
+def test_big_repr():
+ from _pytest._io.saferepr import SafeRepr
+
+ assert len(saferepr(range(1000))) <= len("[" + SafeRepr(0).maxlist * "1000" + "]")
+
+
+def test_repr_on_newstyle() -> None:
+ class Function:
+ def __repr__(self):
+ return "<%s>" % (self.name) # type: ignore[attr-defined]
+
+ assert saferepr(Function())
+
+
+def test_unicode():
+ val = "£€"
+ reprval = "'£€'"
+ 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)
+ """
+
+ class SomeClass:
+ def __getattribute__(self, attr):
+ raise RuntimeError
+
+ def __repr__(self):
+ raise RuntimeError
+
+ assert saferepr(SomeClass()).startswith(
+ "<[RuntimeError() raised in repr()] SomeClass object at 0x"
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/io/test_terminalwriter.py b/testing/web-platform/tests/tools/third_party/pytest/testing/io/test_terminalwriter.py
new file mode 100644
index 0000000000..4866c94a55
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/io/test_terminalwriter.py
@@ -0,0 +1,293 @@
+import io
+import os
+import re
+import shutil
+import sys
+from pathlib import Path
+from typing import Generator
+from unittest import mock
+
+import pytest
+from _pytest._io import terminalwriter
+from _pytest.monkeypatch import MonkeyPatch
+
+
+# These tests were initially copied from py 1.8.1.
+
+
+def test_terminal_width_COLUMNS(monkeypatch: MonkeyPatch) -> None:
+ monkeypatch.setenv("COLUMNS", "42")
+ assert terminalwriter.get_terminal_width() == 42
+ monkeypatch.delenv("COLUMNS", raising=False)
+
+
+def test_terminalwriter_width_bogus(monkeypatch: MonkeyPatch) -> None:
+ monkeypatch.setattr(shutil, "get_terminal_size", mock.Mock(return_value=(10, 10)))
+ monkeypatch.delenv("COLUMNS", raising=False)
+ tw = terminalwriter.TerminalWriter()
+ assert tw.fullwidth == 80
+
+
+def test_terminalwriter_computes_width(monkeypatch: MonkeyPatch) -> None:
+ monkeypatch.setattr(terminalwriter, "get_terminal_width", lambda: 42)
+ tw = terminalwriter.TerminalWriter()
+ assert tw.fullwidth == 42
+
+
+def test_terminalwriter_dumb_term_no_markup(monkeypatch: MonkeyPatch) -> None:
+ monkeypatch.setattr(os, "environ", {"TERM": "dumb", "PATH": ""})
+
+ class MyFile:
+ closed = False
+
+ def isatty(self):
+ return True
+
+ with monkeypatch.context() as m:
+ m.setattr(sys, "stdout", MyFile())
+ assert sys.stdout.isatty()
+ tw = terminalwriter.TerminalWriter()
+ assert not tw.hasmarkup
+
+
+def test_terminalwriter_not_unicode() -> None:
+ """If the file doesn't support Unicode, the string is unicode-escaped (#7475)."""
+ buffer = io.BytesIO()
+ 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"
+
+
+win32 = int(sys.platform == "win32")
+
+
+class TestTerminalWriter:
+ @pytest.fixture(params=["path", "stringio"])
+ def tw(
+ self, request, tmp_path: Path
+ ) -> Generator[terminalwriter.TerminalWriter, None, None]:
+ if request.param == "path":
+ p = tmp_path.joinpath("tmpfile")
+ f = open(str(p), "w+", encoding="utf8")
+ tw = terminalwriter.TerminalWriter(f)
+
+ def getlines():
+ f.flush()
+ with open(str(p), encoding="utf8") as fp:
+ return fp.readlines()
+
+ elif request.param == "stringio":
+ f = io.StringIO()
+ tw = terminalwriter.TerminalWriter(f)
+
+ def getlines():
+ f.seek(0)
+ return f.readlines()
+
+ tw.getlines = getlines # type: ignore
+ tw.getvalue = lambda: "".join(getlines()) # type: ignore
+
+ with f:
+ yield tw
+
+ def test_line(self, tw) -> None:
+ tw.line("hello")
+ lines = tw.getlines()
+ assert len(lines) == 1
+ assert lines[0] == "hello\n"
+
+ def test_line_unicode(self, tw) -> None:
+ msg = "b\u00f6y"
+ tw.line(msg)
+ lines = tw.getlines()
+ assert lines[0] == msg + "\n"
+
+ def test_sep_no_title(self, tw) -> None:
+ tw.sep("-", fullwidth=60)
+ lines = tw.getlines()
+ assert len(lines) == 1
+ assert lines[0] == "-" * (60 - win32) + "\n"
+
+ def test_sep_with_title(self, tw) -> None:
+ tw.sep("-", "hello", fullwidth=60)
+ lines = tw.getlines()
+ assert len(lines) == 1
+ assert lines[0] == "-" * 26 + " hello " + "-" * (27 - win32) + "\n"
+
+ def test_sep_longer_than_width(self, tw) -> None:
+ 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"
+
+ @pytest.mark.skipif(sys.platform == "win32", reason="win32 has no native ansi")
+ @pytest.mark.parametrize("bold", (True, False))
+ @pytest.mark.parametrize("color", ("red", "green"))
+ def test_markup(self, tw, bold: bool, color: str) -> None:
+ text = tw.markup("hello", **{color: True, "bold": bold})
+ assert "hello" in text
+
+ def test_markup_bad(self, tw) -> None:
+ with pytest.raises(ValueError):
+ tw.markup("x", wronkw=3)
+ with pytest.raises(ValueError):
+ tw.markup("x", wronkw=0)
+
+ def test_line_write_markup(self, tw) -> None:
+ tw.hasmarkup = True
+ tw.line("x", bold=True)
+ tw.write("x\n", red=True)
+ lines = tw.getlines()
+ if sys.platform != "win32":
+ assert len(lines[0]) >= 2, lines
+ assert len(lines[1]) >= 2, lines
+
+ def test_attr_fullwidth(self, tw) -> None:
+ tw.sep("-", "hello", fullwidth=70)
+ tw.fullwidth = 70
+ tw.sep("-", "hello")
+ lines = tw.getlines()
+ assert len(lines[0]) == len(lines[1])
+
+
+@pytest.mark.skipif(sys.platform == "win32", reason="win32 has no native ansi")
+def test_attr_hasmarkup() -> None:
+ file = io.StringIO()
+ tw = terminalwriter.TerminalWriter(file)
+ assert not tw.hasmarkup
+ tw.hasmarkup = True
+ 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_set():
+ file = io.StringIO()
+ tw = terminalwriter.TerminalWriter(file)
+ assert tw.hasmarkup
+ 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"
+
+
+def test_should_do_markup_PY_COLORS_eq_1(monkeypatch: MonkeyPatch) -> None:
+ monkeypatch.setitem(os.environ, "PY_COLORS", "1")
+ assert_color_set()
+
+
+def test_should_not_do_markup_PY_COLORS_eq_0(monkeypatch: MonkeyPatch) -> None:
+ monkeypatch.setitem(os.environ, "PY_COLORS", "0")
+ assert_color_not_set()
+
+
+def test_should_not_do_markup_NO_COLOR(monkeypatch: MonkeyPatch) -> None:
+ monkeypatch.setitem(os.environ, "NO_COLOR", "1")
+ assert_color_not_set()
+
+
+def test_should_do_markup_FORCE_COLOR(monkeypatch: MonkeyPatch) -> None:
+ monkeypatch.setitem(os.environ, "FORCE_COLOR", "1")
+ assert_color_set()
+
+
+def test_should_not_do_markup_NO_COLOR_and_FORCE_COLOR(
+ monkeypatch: MonkeyPatch,
+) -> None:
+ monkeypatch.setitem(os.environ, "NO_COLOR", "1")
+ monkeypatch.setitem(os.environ, "FORCE_COLOR", "1")
+ assert_color_not_set()
+
+
+class TestTerminalWriterLineWidth:
+ def test_init(self) -> None:
+ tw = terminalwriter.TerminalWriter()
+ assert tw.width_of_current_line == 0
+
+ def test_update(self) -> None:
+ tw = terminalwriter.TerminalWriter()
+ tw.write("hello world")
+ assert tw.width_of_current_line == 11
+
+ def test_update_with_newline(self) -> None:
+ tw = terminalwriter.TerminalWriter()
+ tw.write("hello\nworld")
+ assert tw.width_of_current_line == 5
+
+ def test_update_with_wide_text(self) -> None:
+ tw = terminalwriter.TerminalWriter()
+ tw.write("乇乂ㄒ尺卂 ㄒ卄丨匚匚")
+ assert tw.width_of_current_line == 21 # 5*2 + 1 + 5*2
+
+ def test_composed(self) -> None:
+ tw = terminalwriter.TerminalWriter()
+ text = "café food"
+ assert len(text) == 9
+ tw.write(text)
+ assert tw.width_of_current_line == 9
+
+ def test_combining(self) -> None:
+ tw = terminalwriter.TerminalWriter()
+ text = "café food"
+ assert len(text) == 10
+ tw.write(text)
+ assert tw.width_of_current_line == 9
+
+
+@pytest.mark.parametrize(
+ ("has_markup", "code_highlight", "expected"),
+ [
+ pytest.param(
+ True,
+ True,
+ "{kw}assert{hl-reset} {number}0{hl-reset}\n",
+ id="with markup and code_highlight",
+ ),
+ pytest.param(
+ True,
+ False,
+ "assert 0\n",
+ id="with markup but no code_highlight",
+ ),
+ pytest.param(
+ False,
+ True,
+ "assert 0\n",
+ id="without markup but with code_highlight",
+ ),
+ pytest.param(
+ False,
+ False,
+ "assert 0\n",
+ id="neither markup nor code_highlight",
+ ),
+ ],
+)
+def test_code_highlight(has_markup, code_highlight, expected, color_mapping):
+ f = io.StringIO()
+ tw = terminalwriter.TerminalWriter(f)
+ tw.hasmarkup = has_markup
+ tw.code_highlight = code_highlight
+ tw._write_source(["assert 0"])
+
+ assert f.getvalue().splitlines(keepends=True) == color_mapping.format([expected])
+
+ with pytest.raises(
+ ValueError,
+ match=re.escape("indents size (2) should have same size as lines (1)"),
+ ):
+ tw._write_source(["assert 0"], [" ", " "])
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/io/test_wcwidth.py b/testing/web-platform/tests/tools/third_party/pytest/testing/io/test_wcwidth.py
new file mode 100644
index 0000000000..7cc74df5d0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/io/test_wcwidth.py
@@ -0,0 +1,38 @@
+import pytest
+from _pytest._io.wcwidth import wcswidth
+from _pytest._io.wcwidth import wcwidth
+
+
+@pytest.mark.parametrize(
+ ("c", "expected"),
+ [
+ ("\0", 0),
+ ("\n", -1),
+ ("a", 1),
+ ("1", 1),
+ ("א", 1),
+ ("\u200B", 0),
+ ("\u1ABE", 0),
+ ("\u0591", 0),
+ ("🉐", 2),
+ ("$", 2),
+ ],
+)
+def test_wcwidth(c: str, expected: int) -> None:
+ assert wcwidth(c) == expected
+
+
+@pytest.mark.parametrize(
+ ("s", "expected"),
+ [
+ ("", 0),
+ ("hello, world!", 13),
+ ("hello, world!\n", -1),
+ ("0123456789", 10),
+ ("שלום, עולם!", 11),
+ ("שְבֻעָיים", 6),
+ ("🉐🉐🉐", 6),
+ ],
+)
+def test_wcswidth(s: str, expected: int) -> None:
+ assert wcswidth(s) == expected
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/logging/test_fixture.py b/testing/web-platform/tests/tools/third_party/pytest/testing/logging/test_fixture.py
new file mode 100644
index 0000000000..bcb20de580
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/logging/test_fixture.py
@@ -0,0 +1,310 @@
+import logging
+
+import pytest
+from _pytest.logging import caplog_records_key
+from _pytest.pytester import Pytester
+
+logger = logging.getLogger(__name__)
+sublogger = logging.getLogger(__name__ + ".baz")
+
+
+def test_fixture_help(pytester: Pytester) -> None:
+ result = pytester.runpytest("--fixtures")
+ result.stdout.fnmatch_lines(["*caplog*"])
+
+
+def test_change_level(caplog):
+ caplog.set_level(logging.INFO)
+ logger.debug("handler DEBUG level")
+ logger.info("handler INFO level")
+
+ caplog.set_level(logging.CRITICAL, logger=sublogger.name)
+ sublogger.warning("logger WARNING level")
+ sublogger.critical("logger CRITICAL level")
+
+ assert "DEBUG" not in caplog.text
+ assert "INFO" in caplog.text
+ assert "WARNING" not in caplog.text
+ assert "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).
+ """
+ pytester.makepyfile(
+ """
+ import logging
+
+ def test1(caplog):
+ 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
+ logging.info('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_undos_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.
+ """
+ pytester.makepyfile(
+ """
+ import logging
+
+ def test1(caplog):
+ assert caplog.handler.level == 0
+ caplog.set_level(9999)
+ caplog.set_level(41)
+ assert caplog.handler.level == 41
+
+ def test2(caplog):
+ assert caplog.handler.level == 0
+
+ def test3(caplog):
+ assert caplog.handler.level == 0
+ caplog.set_level(43)
+ assert caplog.handler.level == 43
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=3)
+
+
+def test_with_statement(caplog):
+ with caplog.at_level(logging.INFO):
+ logger.debug("handler DEBUG level")
+ logger.info("handler INFO level")
+
+ with caplog.at_level(logging.CRITICAL, logger=sublogger.name):
+ sublogger.warning("logger WARNING level")
+ sublogger.critical("logger CRITICAL level")
+
+ assert "DEBUG" not in caplog.text
+ assert "INFO" in caplog.text
+ assert "WARNING" not in caplog.text
+ assert "CRITICAL" in caplog.text
+
+
+def test_log_access(caplog):
+ caplog.set_level(logging.INFO)
+ logger.info("boo %s", "arg")
+ assert caplog.records[0].levelname == "INFO"
+ assert caplog.records[0].msg == "boo %s"
+ assert "boo arg" in caplog.text
+
+
+def test_messages(caplog):
+ caplog.set_level(logging.INFO)
+ logger.info("boo %s", "arg")
+ logger.info("bar %s\nbaz %s", "arg1", "arg2")
+ assert "boo arg" == caplog.messages[0]
+ assert "bar arg1\nbaz arg2" == caplog.messages[1]
+ assert caplog.text.count("\n") > len(caplog.messages)
+ assert len(caplog.text.splitlines()) > len(caplog.messages)
+
+ try:
+ raise Exception("test")
+ except Exception:
+ logger.exception("oops")
+
+ assert "oops" in caplog.text
+ assert "oops" in caplog.messages[-1]
+ # Tracebacks are stored in the record and not added until the formatter or handler.
+ assert "Exception" in caplog.text
+ assert "Exception" not in caplog.messages[-1]
+
+
+def test_record_tuples(caplog):
+ caplog.set_level(logging.INFO)
+ logger.info("boo %s", "arg")
+
+ assert caplog.record_tuples == [(__name__, logging.INFO, "boo arg")]
+
+
+def test_unicode(caplog):
+ caplog.set_level(logging.INFO)
+ logger.info("bū")
+ assert caplog.records[0].levelname == "INFO"
+ assert caplog.records[0].msg == "bū"
+ assert "bū" in caplog.text
+
+
+def test_clear(caplog):
+ caplog.set_level(logging.INFO)
+ logger.info("bū")
+ assert len(caplog.records)
+ assert caplog.text
+ caplog.clear()
+ assert not len(caplog.records)
+ assert not caplog.text
+
+
+@pytest.fixture
+def logging_during_setup_and_teardown(caplog):
+ caplog.set_level("INFO")
+ logger.info("a_setup_log")
+ yield
+ logger.info("a_teardown_log")
+ 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):
+ assert not caplog.records
+ assert not caplog.get_records("call")
+ 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"]
+
+ # 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"}
+
+
+def test_ini_controls_global_log_level(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ import logging
+ def test_log_level_override(request, caplog):
+ plugin = request.config.pluginmanager.getplugin('logging-plugin')
+ assert plugin.log_level == logging.ERROR
+ logger = logging.getLogger('catchlog')
+ logger.warning("WARNING message won't be shown")
+ logger.error("ERROR message will be shown")
+ assert 'WARNING' not in caplog.text
+ assert 'ERROR' in caplog.text
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ log_level=ERROR
+ """
+ )
+
+ result = pytester.runpytest()
+ # make sure that 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:
+ pytester.makepyfile(
+ """
+ import pytest
+ import logging
+ def test_log_level_override(request, caplog):
+ logger = logging.getLogger('catchlog')
+ plugin = request.config.pluginmanager.getplugin('logging-plugin')
+ assert plugin.log_level == logging.WARNING
+
+ logger.info("INFO message won't be shown")
+
+ caplog.set_level(logging.INFO, logger.name)
+
+ with caplog.at_level(logging.DEBUG, logger.name):
+ logger.debug("DEBUG message will be shown")
+
+ logger.debug("DEBUG message won't be shown")
+
+ with caplog.at_level(logging.CRITICAL, logger.name):
+ logger.warning("WARNING message won't be shown")
+
+ logger.debug("DEBUG message won't be shown")
+ logger.info("INFO message will be shown")
+
+ assert "message won't be shown" not in caplog.text
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ log_level=WARNING
+ """
+ )
+
+ result = pytester.runpytest()
+ assert result.ret == 0
+
+
+def test_caplog_captures_despite_exception(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ import logging
+ def test_log_level_override(request, caplog):
+ logger = logging.getLogger('catchlog')
+ plugin = request.config.pluginmanager.getplugin('logging-plugin')
+ assert plugin.log_level == logging.WARNING
+
+ logger.error("ERROR message " + "will be shown")
+
+ with caplog.at_level(logging.DEBUG, logger.name):
+ logger.debug("DEBUG message " + "won't be shown")
+ raise Exception()
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ log_level=WARNING
+ """
+ )
+
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*ERROR message will be shown*"])
+ result.stdout.no_fnmatch_line("*DEBUG message won't be shown*")
+ assert result.ret == 1
+
+
+def test_log_report_captures_according_to_config_option_upon_failure(
+ pytester: Pytester,
+) -> None:
+ """Test that upon failure:
+ (1) `caplog` succeeded to capture the DEBUG message and assert on it => No `Exception` is raised.
+ (2) The `DEBUG` message does NOT appear in the `Captured log call` report.
+ (3) The stdout, `INFO`, and `WARNING` messages DO appear in the test reports due to `--log-level=INFO`.
+ """
+ pytester.makepyfile(
+ """
+ import pytest
+ import logging
+
+ def function_that_logs():
+ logging.debug('DEBUG log ' + 'message')
+ logging.info('INFO log ' + 'message')
+ logging.warning('WARNING log ' + 'message')
+ print('Print ' + 'message')
+
+ def test_that_fails(request, caplog):
+ plugin = request.config.pluginmanager.getplugin('logging-plugin')
+ assert plugin.log_level == logging.INFO
+
+ with caplog.at_level(logging.DEBUG):
+ function_that_logs()
+
+ if 'DEBUG log ' + 'message' not in caplog.text:
+ raise Exception('caplog failed to ' + 'capture DEBUG')
+
+ assert False
+ """
+ )
+
+ result = pytester.runpytest("--log-level=INFO")
+ result.stdout.no_fnmatch_line("*Exception: caplog failed to capture DEBUG*")
+ result.stdout.no_fnmatch_line("*DEBUG log message*")
+ result.stdout.fnmatch_lines(
+ ["*Print message*", "*INFO log message*", "*WARNING log message*"]
+ )
+ assert result.ret == 1
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/logging/test_formatter.py b/testing/web-platform/tests/tools/third_party/pytest/testing/logging/test_formatter.py
new file mode 100644
index 0000000000..3797129372
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/logging/test_formatter.py
@@ -0,0 +1,173 @@
+import logging
+from typing import Any
+
+from _pytest._io import TerminalWriter
+from _pytest.logging import ColoredLevelFormatter
+
+
+def test_coloredlogformatter() -> None:
+ logfmt = "%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s"
+
+ record = logging.LogRecord(
+ name="dummy",
+ level=logging.INFO,
+ pathname="dummypath",
+ lineno=10,
+ msg="Test Message",
+ args=(),
+ exc_info=None,
+ )
+
+ tw = TerminalWriter()
+ tw.hasmarkup = True
+ formatter = ColoredLevelFormatter(tw, logfmt)
+ output = formatter.format(record)
+ assert output == (
+ "dummypath 10 \x1b[32mINFO \x1b[0m Test Message"
+ )
+
+ tw.hasmarkup = False
+ formatter = ColoredLevelFormatter(tw, logfmt)
+ output = formatter.format(record)
+ assert output == ("dummypath 10 INFO Test Message")
+
+
+def test_coloredlogformatter_with_width_precision() -> None:
+ logfmt = "%(filename)-25s %(lineno)4d %(levelname)-8.8s %(message)s"
+
+ record = logging.LogRecord(
+ name="dummy",
+ level=logging.INFO,
+ pathname="dummypath",
+ lineno=10,
+ msg="Test Message",
+ args=(),
+ exc_info=None,
+ )
+
+ tw = TerminalWriter()
+ tw.hasmarkup = True
+ formatter = ColoredLevelFormatter(tw, logfmt)
+ output = formatter.format(record)
+ assert output == (
+ "dummypath 10 \x1b[32mINFO \x1b[0m Test Message"
+ )
+
+ tw.hasmarkup = False
+ formatter = ColoredLevelFormatter(tw, logfmt)
+ output = formatter.format(record)
+ assert output == ("dummypath 10 INFO Test Message")
+
+
+def test_multiline_message() -> None:
+ from _pytest.logging import PercentStyleMultiline
+
+ logfmt = "%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s"
+
+ record: Any = logging.LogRecord(
+ name="dummy",
+ level=logging.INFO,
+ pathname="dummypath",
+ lineno=10,
+ msg="Test Message line1\nline2",
+ args=(),
+ exc_info=None,
+ )
+ # this is called by logging.Formatter.format
+ record.message = record.getMessage()
+
+ ai_on_style = PercentStyleMultiline(logfmt, True)
+ output = ai_on_style.format(record)
+ assert output == (
+ "dummypath 10 INFO Test Message line1\n"
+ " line2"
+ )
+
+ ai_off_style = PercentStyleMultiline(logfmt, False)
+ output = ai_off_style.format(record)
+ assert output == (
+ "dummypath 10 INFO Test Message line1\nline2"
+ )
+
+ ai_none_style = PercentStyleMultiline(logfmt, None)
+ output = ai_none_style.format(record)
+ assert output == (
+ "dummypath 10 INFO Test Message line1\nline2"
+ )
+
+ record.auto_indent = False
+ output = ai_on_style.format(record)
+ assert output == (
+ "dummypath 10 INFO Test Message line1\nline2"
+ )
+
+ record.auto_indent = True
+ output = ai_off_style.format(record)
+ assert output == (
+ "dummypath 10 INFO Test Message line1\n"
+ " line2"
+ )
+
+ record.auto_indent = "False"
+ output = ai_on_style.format(record)
+ assert output == (
+ "dummypath 10 INFO Test Message line1\nline2"
+ )
+
+ record.auto_indent = "True"
+ output = ai_off_style.format(record)
+ assert output == (
+ "dummypath 10 INFO Test Message line1\n"
+ " line2"
+ )
+
+ # bad string values default to False
+ record.auto_indent = "junk"
+ output = ai_off_style.format(record)
+ assert output == (
+ "dummypath 10 INFO Test Message line1\nline2"
+ )
+
+ # anything other than string or int will default to False
+ record.auto_indent = dict()
+ output = ai_off_style.format(record)
+ assert output == (
+ "dummypath 10 INFO Test Message line1\nline2"
+ )
+
+ record.auto_indent = "5"
+ output = ai_off_style.format(record)
+ assert output == (
+ "dummypath 10 INFO Test Message line1\n line2"
+ )
+
+ record.auto_indent = 5
+ output = ai_off_style.format(record)
+ assert output == (
+ "dummypath 10 INFO Test Message line1\n line2"
+ )
+
+
+def test_colored_short_level() -> None:
+ logfmt = "%(levelname).1s %(message)s"
+
+ record = logging.LogRecord(
+ name="dummy",
+ level=logging.INFO,
+ pathname="dummypath",
+ lineno=10,
+ msg="Test Message",
+ args=(),
+ exc_info=None,
+ )
+
+ class ColorConfig:
+ class option:
+ pass
+
+ tw = TerminalWriter()
+ tw.hasmarkup = True
+ formatter = ColoredLevelFormatter(tw, logfmt)
+ output = formatter.format(record)
+ # the I (of INFO) is colored
+ assert output == ("\x1b[32mI\x1b[0m Test Message")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/logging/test_reporting.py b/testing/web-platform/tests/tools/third_party/pytest/testing/logging/test_reporting.py
new file mode 100644
index 0000000000..323ff7b244
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/logging/test_reporting.py
@@ -0,0 +1,1167 @@
+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
+
+
+def test_nothing_logged(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import sys
+
+ def test_foo():
+ sys.stdout.write('text going to stdout')
+ sys.stderr.write('text going to stderr')
+ assert False
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(["*- Captured stdout call -*", "text going to stdout"])
+ result.stdout.fnmatch_lines(["*- Captured stderr call -*", "text going to stderr"])
+ with pytest.raises(pytest.fail.Exception):
+ result.stdout.fnmatch_lines(["*- Captured *log call -*"])
+
+
+def test_messages_logged(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import sys
+ import logging
+
+ logger = logging.getLogger(__name__)
+
+ def test_foo():
+ sys.stdout.write('text going to stdout')
+ sys.stderr.write('text going to stderr')
+ logger.info('text going to logger')
+ assert False
+ """
+ )
+ result = pytester.runpytest("--log-level=INFO")
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(["*- Captured *log call -*", "*text going to logger*"])
+ result.stdout.fnmatch_lines(["*- Captured stdout call -*", "text going to stdout"])
+ result.stdout.fnmatch_lines(["*- Captured stderr call -*", "text going to stderr"])
+
+
+def test_root_logger_affected(pytester: Pytester) -> None:
+ 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-file=pytest.log")
+ assert result.ret == 1
+
+ # The capture log calls in the stdout section only contain the
+ # logger.error msg, because of --log-level=ERROR.
+ result.stdout.fnmatch_lines(["*error text going to logger*"])
+ stdout = result.stdout.str()
+ 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.
+ assert os.path.isfile(log_file)
+ with open(log_file) as rfh:
+ contents = rfh.read()
+ assert "info text going to logger" not in contents
+ assert "warning text going to logger" in contents
+ assert "error text going to logger" in contents
+
+
+def test_log_cli_level_log_level_interaction(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import logging
+ logger = logging.getLogger()
+
+ def test_foo():
+ logger.debug('debug text ' + 'going to logger')
+ logger.info('info text ' + 'going to logger')
+ logger.warning('warning text ' + 'going to logger')
+ logger.error('error text ' + 'going to logger')
+ assert 0
+ """
+ )
+
+ result = pytester.runpytest("--log-cli-level=INFO", "--log-level=ERROR")
+ assert result.ret == 1
+
+ result.stdout.fnmatch_lines(
+ [
+ "*-- live log call --*",
+ "*INFO*info text going to logger",
+ "*WARNING*warning text going to logger",
+ "*ERROR*error text going to logger",
+ "=* 1 failed in *=",
+ ]
+ )
+ result.stdout.no_re_match_line("DEBUG")
+
+
+def test_setup_logging(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import logging
+
+ logger = logging.getLogger(__name__)
+
+ def setup_function(function):
+ logger.info('text going to logger from setup')
+
+ def test_foo():
+ logger.info('text going to logger from call')
+ assert False
+ """
+ )
+ result = pytester.runpytest("--log-level=INFO")
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(
+ [
+ "*- Captured *log setup -*",
+ "*text going to logger from setup*",
+ "*- Captured *log call -*",
+ "*text going to logger from call*",
+ ]
+ )
+
+
+def test_teardown_logging(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import logging
+
+ logger = logging.getLogger(__name__)
+
+ def test_foo():
+ logger.info('text going to logger from call')
+
+ def teardown_function(function):
+ logger.info('text going to logger from teardown')
+ assert False
+ """
+ )
+ result = pytester.runpytest("--log-level=INFO")
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(
+ [
+ "*- Captured *log call -*",
+ "*text going to logger from call*",
+ "*- Captured *log teardown -*",
+ "*text going to logger from teardown*",
+ ]
+ )
+
+
+@pytest.mark.parametrize("enabled", [True, False])
+def test_log_cli_enabled_disabled(pytester: Pytester, enabled: bool) -> None:
+ msg = "critical message logged by test"
+ pytester.makepyfile(
+ """
+ import logging
+ def test_log_cli():
+ logging.critical("{}")
+ """.format(
+ msg
+ )
+ )
+ if enabled:
+ pytester.makeini(
+ """
+ [pytest]
+ log_cli=true
+ """
+ )
+ result = pytester.runpytest()
+ if enabled:
+ result.stdout.fnmatch_lines(
+ [
+ "test_log_cli_enabled_disabled.py::test_log_cli ",
+ "*-- live log call --*",
+ "CRITICAL *test_log_cli_enabled_disabled.py* critical message logged by test",
+ "PASSED*",
+ ]
+ )
+ else:
+ assert msg not in result.stdout.str()
+
+
+def test_log_cli_default_level(pytester: Pytester) -> None:
+ # Default log file level
+ pytester.makepyfile(
+ """
+ import pytest
+ import logging
+ def test_log_cli(request):
+ plugin = request.config.pluginmanager.getplugin('logging-plugin')
+ assert plugin.log_cli_handler.level == logging.NOTSET
+ logging.getLogger('catchlog').info("INFO message won't be shown")
+ logging.getLogger('catchlog').warning("WARNING message will be shown")
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ log_cli=true
+ """
+ )
+
+ result = pytester.runpytest()
+
+ # fnmatch_lines does an assertion internally
+ result.stdout.fnmatch_lines(
+ [
+ "test_log_cli_default_level.py::test_log_cli ",
+ "WARNING*test_log_cli_default_level.py* message will be shown*",
+ ]
+ )
+ result.stdout.no_fnmatch_line("*INFO message won't be shown*")
+ # make sure that we get a '0' exit code for the testsuite
+ assert result.ret == 0
+
+
+def test_log_cli_default_level_multiple_tests(
+ pytester: Pytester, request: FixtureRequest
+) -> None:
+ """Ensure we reset the first newline added by the live logger between tests"""
+ filename = request.node.name + ".py"
+ pytester.makepyfile(
+ """
+ import logging
+
+ def test_log_1():
+ logging.warning("log message from test_log_1")
+
+ def test_log_2():
+ logging.warning("log message from test_log_2")
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ log_cli=true
+ """
+ )
+
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ f"{filename}::test_log_1 ",
+ "*WARNING*log message from test_log_1*",
+ "PASSED *50%*",
+ f"{filename}::test_log_2 ",
+ "*WARNING*log message from test_log_2*",
+ "PASSED *100%*",
+ "=* 2 passed in *=",
+ ]
+ )
+
+
+def test_log_cli_default_level_sections(
+ pytester: Pytester, request: FixtureRequest
+) -> None:
+ """Check that with live logging enable we are printing the correct headers during
+ start/setup/call/teardown/finish."""
+ filename = request.node.name + ".py"
+ pytester.makeconftest(
+ """
+ import pytest
+ import logging
+
+ def pytest_runtest_logstart():
+ logging.warning('>>>>> START >>>>>')
+
+ def pytest_runtest_logfinish():
+ logging.warning('<<<<< END <<<<<<<')
+ """
+ )
+
+ pytester.makepyfile(
+ """
+ import pytest
+ import logging
+
+ @pytest.fixture
+ def fix(request):
+ logging.warning("log message from setup of {}".format(request.node.name))
+ yield
+ logging.warning("log message from teardown of {}".format(request.node.name))
+
+ def test_log_1(fix):
+ logging.warning("log message from test_log_1")
+
+ def test_log_2(fix):
+ logging.warning("log message from test_log_2")
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ log_cli=true
+ """
+ )
+
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ f"{filename}::test_log_1 ",
+ "*-- live log start --*",
+ "*WARNING* >>>>> START >>>>>*",
+ "*-- live log setup --*",
+ "*WARNING*log message from setup of test_log_1*",
+ "*-- live log call --*",
+ "*WARNING*log message from test_log_1*",
+ "PASSED *50%*",
+ "*-- live log teardown --*",
+ "*WARNING*log message from teardown of test_log_1*",
+ "*-- live log finish --*",
+ "*WARNING* <<<<< END <<<<<<<*",
+ f"{filename}::test_log_2 ",
+ "*-- live log start --*",
+ "*WARNING* >>>>> START >>>>>*",
+ "*-- live log setup --*",
+ "*WARNING*log message from setup of test_log_2*",
+ "*-- live log call --*",
+ "*WARNING*log message from test_log_2*",
+ "PASSED *100%*",
+ "*-- live log teardown --*",
+ "*WARNING*log message from teardown of test_log_2*",
+ "*-- live log finish --*",
+ "*WARNING* <<<<< END <<<<<<<*",
+ "=* 2 passed in *=",
+ ]
+ )
+
+
+def test_live_logs_unknown_sections(
+ pytester: Pytester, request: FixtureRequest
+) -> None:
+ """Check that with live logging enable we are printing the correct headers during
+ start/setup/call/teardown/finish."""
+ filename = request.node.name + ".py"
+ pytester.makeconftest(
+ """
+ import pytest
+ import logging
+
+ def pytest_runtest_protocol(item, nextitem):
+ logging.warning('Unknown Section!')
+
+ def pytest_runtest_logstart():
+ logging.warning('>>>>> START >>>>>')
+
+ def pytest_runtest_logfinish():
+ logging.warning('<<<<< END <<<<<<<')
+ """
+ )
+
+ pytester.makepyfile(
+ """
+ import pytest
+ import logging
+
+ @pytest.fixture
+ def fix(request):
+ logging.warning("log message from setup of {}".format(request.node.name))
+ yield
+ logging.warning("log message from teardown of {}".format(request.node.name))
+
+ def test_log_1(fix):
+ logging.warning("log message from test_log_1")
+
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ log_cli=true
+ """
+ )
+
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*WARNING*Unknown Section*",
+ f"{filename}::test_log_1 ",
+ "*WARNING* >>>>> START >>>>>*",
+ "*-- live log setup --*",
+ "*WARNING*log message from setup of test_log_1*",
+ "*-- live log call --*",
+ "*WARNING*log message from test_log_1*",
+ "PASSED *100%*",
+ "*-- live log teardown --*",
+ "*WARNING*log message from teardown of test_log_1*",
+ "*WARNING* <<<<< END <<<<<<<*",
+ "=* 1 passed in *=",
+ ]
+ )
+
+
+def test_sections_single_new_line_after_test_outcome(
+ pytester: Pytester, request: FixtureRequest
+) -> None:
+ """Check that only a single new line is written between log messages during
+ teardown/finish."""
+ filename = request.node.name + ".py"
+ pytester.makeconftest(
+ """
+ import pytest
+ import logging
+
+ def pytest_runtest_logstart():
+ logging.warning('>>>>> START >>>>>')
+
+ def pytest_runtest_logfinish():
+ logging.warning('<<<<< END <<<<<<<')
+ logging.warning('<<<<< END <<<<<<<')
+ """
+ )
+
+ pytester.makepyfile(
+ """
+ import pytest
+ import logging
+
+ @pytest.fixture
+ def fix(request):
+ logging.warning("log message from setup of {}".format(request.node.name))
+ yield
+ logging.warning("log message from teardown of {}".format(request.node.name))
+ logging.warning("log message from teardown of {}".format(request.node.name))
+
+ def test_log_1(fix):
+ logging.warning("log message from test_log_1")
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ log_cli=true
+ """
+ )
+
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ f"{filename}::test_log_1 ",
+ "*-- live log start --*",
+ "*WARNING* >>>>> START >>>>>*",
+ "*-- live log setup --*",
+ "*WARNING*log message from setup of test_log_1*",
+ "*-- live log call --*",
+ "*WARNING*log message from test_log_1*",
+ "PASSED *100%*",
+ "*-- live log teardown --*",
+ "*WARNING*log message from teardown of test_log_1*",
+ "*-- live log finish --*",
+ "*WARNING* <<<<< END <<<<<<<*",
+ "*WARNING* <<<<< END <<<<<<<*",
+ "=* 1 passed in *=",
+ ]
+ )
+ assert (
+ re.search(
+ r"(.+)live log teardown(.+)\nWARNING(.+)\nWARNING(.+)",
+ result.stdout.str(),
+ re.MULTILINE,
+ )
+ is not None
+ )
+ assert (
+ re.search(
+ r"(.+)live log finish(.+)\nWARNING(.+)\nWARNING(.+)",
+ result.stdout.str(),
+ re.MULTILINE,
+ )
+ is not None
+ )
+
+
+def test_log_cli_level(pytester: Pytester) -> None:
+ # Default log file level
+ pytester.makepyfile(
+ """
+ import pytest
+ import logging
+ def test_log_cli(request):
+ plugin = request.config.pluginmanager.getplugin('logging-plugin')
+ assert plugin.log_cli_handler.level == logging.INFO
+ logging.getLogger('catchlog').debug("This log message won't be shown")
+ logging.getLogger('catchlog').info("This log message will be shown")
+ print('PASSED')
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ log_cli=true
+ """
+ )
+
+ result = pytester.runpytest("-s", "--log-cli-level=INFO")
+
+ # fnmatch_lines does an assertion internally
+ result.stdout.fnmatch_lines(
+ [
+ "*test_log_cli_level.py*This log message will be shown",
+ "PASSED", # 'PASSED' on its own line because the log message prints a new line
+ ]
+ )
+ result.stdout.no_fnmatch_line("*This log message won't be shown*")
+
+ # make sure that we get a '0' exit code for the testsuite
+ assert result.ret == 0
+
+ result = pytester.runpytest("-s", "--log-level=INFO")
+
+ # fnmatch_lines does an assertion internally
+ result.stdout.fnmatch_lines(
+ [
+ "*test_log_cli_level.py* This log message will be shown",
+ "PASSED", # 'PASSED' on its own line because the log message prints a new line
+ ]
+ )
+ result.stdout.no_fnmatch_line("*This log message won't be shown*")
+
+ # make sure that we get a '0' exit code for the testsuite
+ assert result.ret == 0
+
+
+def test_log_cli_ini_level(pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ log_cli=true
+ log_cli_level = INFO
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+ import logging
+ def test_log_cli(request):
+ plugin = request.config.pluginmanager.getplugin('logging-plugin')
+ assert plugin.log_cli_handler.level == logging.INFO
+ logging.getLogger('catchlog').debug("This log message won't be shown")
+ logging.getLogger('catchlog').info("This log message will be shown")
+ print('PASSED')
+ """
+ )
+
+ result = pytester.runpytest("-s")
+
+ # fnmatch_lines does an assertion internally
+ result.stdout.fnmatch_lines(
+ [
+ "*test_log_cli_ini_level.py* This log message will be shown",
+ "PASSED", # 'PASSED' on its own line because the log message prints a new line
+ ]
+ )
+ result.stdout.no_fnmatch_line("*This log message won't be shown*")
+
+ # make sure that we get a '0' exit code for the testsuite
+ assert result.ret == 0
+
+
+@pytest.mark.parametrize(
+ "cli_args",
+ ["", "--log-level=WARNING", "--log-file-level=WARNING", "--log-cli-level=WARNING"],
+)
+def test_log_cli_auto_enable(pytester: Pytester, cli_args: str) -> None:
+ """Check that live logs are enabled if --log-level or --log-cli-level is passed on the CLI.
+ It should not be auto enabled if the same configs are set on the INI file.
+ """
+ pytester.makepyfile(
+ """
+ import logging
+
+ def test_log_1():
+ logging.info("log message from test_log_1 not to be shown")
+ logging.warning("log message from test_log_1")
+
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ log_level=INFO
+ log_cli_level=INFO
+ """
+ )
+
+ result = pytester.runpytest(cli_args)
+ stdout = result.stdout.str()
+ if cli_args == "--log-cli-level=WARNING":
+ result.stdout.fnmatch_lines(
+ [
+ "*::test_log_1 ",
+ "*-- live log call --*",
+ "*WARNING*log message from test_log_1*",
+ "PASSED *100%*",
+ "=* 1 passed in *=",
+ ]
+ )
+ assert "INFO" not in stdout
+ else:
+ result.stdout.fnmatch_lines(
+ ["*test_log_cli_auto_enable*100%*", "=* 1 passed in *="]
+ )
+ assert "INFO" not in stdout
+ assert "WARNING" not in stdout
+
+
+def test_log_file_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"))
+
+ result = pytester.runpytest(
+ "-s", f"--log-file={log_file}", "--log-file-level=WARNING"
+ )
+
+ # fnmatch_lines does an assertion internally
+ result.stdout.fnmatch_lines(["test_log_file_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) 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_cli_level(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.INFO
+ logging.getLogger('catchlog').debug("This log message won't be shown")
+ logging.getLogger('catchlog').info("This log message will be shown")
+ print('PASSED')
+ """
+ )
+
+ log_file = str(pytester.path.joinpath("pytest.log"))
+
+ result = pytester.runpytest("-s", f"--log-file={log_file}", "--log-file-level=INFO")
+
+ # fnmatch_lines does an assertion internally
+ result.stdout.fnmatch_lines(["test_log_file_cli_level.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) 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_level_not_changed_by_default(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import logging
+ def test_log_file():
+ assert logging.getLogger().level == logging.WARNING
+ """
+ )
+ result = pytester.runpytest("-s")
+ result.stdout.fnmatch_lines(["* 1 passed in *"])
+
+
+def test_log_file_ini(pytester: Pytester) -> None:
+ log_file = str(pytester.path.joinpath("pytest.log"))
+
+ pytester.makeini(
+ """
+ [pytest]
+ log_file={}
+ log_file_level=WARNING
+ """.format(
+ log_file
+ )
+ )
+ 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')
+ """
+ )
+
+ result = pytester.runpytest("-s")
+
+ # fnmatch_lines does an assertion internally
+ result.stdout.fnmatch_lines(["test_log_file_ini.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) 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:
+ log_file = str(pytester.path.joinpath("pytest.log"))
+
+ pytester.makeini(
+ """
+ [pytest]
+ log_file={}
+ log_file_level = INFO
+ """.format(
+ log_file
+ )
+ )
+ 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.INFO
+ logging.getLogger('catchlog').debug("This log message won't be shown")
+ logging.getLogger('catchlog').info("This log message will be shown")
+ print('PASSED')
+ """
+ )
+
+ result = pytester.runpytest("-s")
+
+ # fnmatch_lines does an assertion internally
+ result.stdout.fnmatch_lines(["test_log_file_ini_level.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) 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_unicode(pytester: Pytester) -> None:
+ log_file = str(pytester.path.joinpath("pytest.log"))
+
+ pytester.makeini(
+ """
+ [pytest]
+ log_file={}
+ log_file_level = INFO
+ """.format(
+ log_file
+ )
+ )
+ pytester.makepyfile(
+ """\
+ import logging
+
+ def test_log_file():
+ logging.getLogger('catchlog').info("Normal message")
+ logging.getLogger('catchlog').info("├")
+ logging.getLogger('catchlog').info("Another normal message")
+ """
+ )
+
+ result = pytester.runpytest()
+
+ # 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 "Normal message" in contents
+ assert "├" in contents
+ assert "Another normal message" in contents
+
+
+@pytest.mark.parametrize("has_capture_manager", [True, False])
+def test_live_logging_suspends_capture(
+ has_capture_manager: bool, request: FixtureRequest
+) -> None:
+ """Test that capture manager is suspended when we emitting messages for live logging.
+
+ This tests the implementation calls instead of behavior because it is difficult/impossible to do it using
+ ``pytester`` facilities because they do their own capturing.
+
+ 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
+ from _pytest.logging import _LiveLoggingStreamHandler
+
+ class MockCaptureManager:
+ calls = []
+
+ @contextlib.contextmanager
+ def global_and_fixture_disabled(self):
+ self.calls.append("enter disabled")
+ yield
+ self.calls.append("exit disabled")
+
+ class DummyTerminal(io.StringIO):
+ def section(self, *args, **kwargs):
+ pass
+
+ out_file = cast(TerminalReporter, DummyTerminal())
+ capture_manager = (
+ cast(CaptureManager, MockCaptureManager()) if has_capture_manager else None
+ )
+ handler = _LiveLoggingStreamHandler(out_file, capture_manager)
+ handler.set_when("call")
+
+ logger = logging.getLogger(__name__ + ".test_live_logging_suspends_capture")
+ logger.addHandler(handler)
+ request.addfinalizer(partial(logger.removeHandler, handler))
+
+ logger.critical("some message")
+ if has_capture_manager:
+ assert MockCaptureManager.calls == ["enter disabled", "exit disabled"]
+ else:
+ assert MockCaptureManager.calls == []
+ assert cast(io.StringIO, out_file).getvalue() == "\nsome message\n"
+
+
+def test_collection_live_logging(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import logging
+
+ logging.getLogger().info("Normal message")
+ """
+ )
+
+ result = pytester.runpytest("--log-cli-level=INFO")
+ result.stdout.fnmatch_lines(
+ ["*--- live log collection ---*", "*Normal message*", "collected 0 items"]
+ )
+
+
+@pytest.mark.parametrize("verbose", ["", "-q", "-qq"])
+def test_collection_collect_only_live_logging(pytester: Pytester, verbose: str) -> None:
+ pytester.makepyfile(
+ """
+ def test_simple():
+ pass
+ """
+ )
+
+ result = pytester.runpytest("--collect-only", "--log-cli-level=INFO", verbose)
+
+ expected_lines = []
+
+ if not verbose:
+ expected_lines.extend(
+ [
+ "*collected 1 item*",
+ "*<Module test_collection_collect_only_live_logging.py>*",
+ "*1 test collected*",
+ ]
+ )
+ elif verbose == "-q":
+ result.stdout.no_fnmatch_line("*collected 1 item**")
+ expected_lines.extend(
+ [
+ "*test_collection_collect_only_live_logging.py::test_simple*",
+ "1 test collected in [0-9].[0-9][0-9]s",
+ ]
+ )
+ elif verbose == "-qq":
+ result.stdout.no_fnmatch_line("*collected 1 item**")
+ expected_lines.extend(["*test_collection_collect_only_live_logging.py: 1*"])
+
+ result.stdout.fnmatch_lines(expected_lines)
+
+
+def test_collection_logging_to_file(pytester: Pytester) -> None:
+ log_file = str(pytester.path.joinpath("pytest.log"))
+
+ pytester.makeini(
+ """
+ [pytest]
+ log_file={}
+ log_file_level = INFO
+ """.format(
+ log_file
+ )
+ )
+
+ pytester.makepyfile(
+ """
+ import logging
+
+ logging.getLogger().info("Normal message")
+
+ def test_simple():
+ logging.getLogger().debug("debug message in test_simple")
+ logging.getLogger().info("info message in test_simple")
+ """
+ )
+
+ result = pytester.runpytest()
+
+ result.stdout.no_fnmatch_line("*--- live log collection ---*")
+
+ assert result.ret == 0
+ assert os.path.isfile(log_file)
+ with open(log_file, encoding="utf-8") as rfh:
+ contents = rfh.read()
+ assert "Normal message" in contents
+ assert "debug message in test_simple" not in contents
+ assert "info message in test_simple" in contents
+
+
+def test_log_in_hooks(pytester: Pytester) -> None:
+ log_file = str(pytester.path.joinpath("pytest.log"))
+
+ pytester.makeini(
+ """
+ [pytest]
+ log_file={}
+ log_file_level = INFO
+ log_cli=true
+ """.format(
+ log_file
+ )
+ )
+ pytester.makeconftest(
+ """
+ import logging
+
+ def pytest_runtestloop(session):
+ logging.info('runtestloop')
+
+ def pytest_sessionstart(session):
+ logging.info('sessionstart')
+
+ def pytest_sessionfinish(session, exitstatus):
+ logging.info('sessionfinish')
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*sessionstart*", "*runtestloop*", "*sessionfinish*"])
+ with open(log_file) as rfh:
+ contents = rfh.read()
+ assert "sessionstart" in contents
+ assert "runtestloop" in contents
+ assert "sessionfinish" in contents
+
+
+def test_log_in_runtest_logreport(pytester: Pytester) -> None:
+ log_file = str(pytester.path.joinpath("pytest.log"))
+
+ pytester.makeini(
+ """
+ [pytest]
+ log_file={}
+ log_file_level = INFO
+ log_cli=true
+ """.format(
+ log_file
+ )
+ )
+ pytester.makeconftest(
+ """
+ import logging
+ logger = logging.getLogger(__name__)
+
+ def pytest_runtest_logreport(report):
+ logger.info("logreport")
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_first():
+ assert True
+ """
+ )
+ pytester.runpytest()
+ with open(log_file) as rfh:
+ contents = rfh.read()
+ assert contents.count("logreport") == 3
+
+
+def test_log_set_path(pytester: Pytester) -> None:
+ report_dir_base = str(pytester.path)
+
+ pytester.makeini(
+ """
+ [pytest]
+ log_file_level = DEBUG
+ log_cli=true
+ """
+ )
+ pytester.makeconftest(
+ """
+ import os
+ import pytest
+ @pytest.hookimpl(hookwrapper=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)
+ logging_plugin.set_log_path(report_file)
+ yield
+ """.format(
+ repr(report_dir_base)
+ )
+ )
+ 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
+ """
+ )
+ pytester.runpytest()
+ with open(os.path.join(report_dir_base, "test_first")) as rfh:
+ content = rfh.read()
+ assert "message from test 1" in content
+
+ with open(os.path.join(report_dir_base, "test_second")) as rfh:
+ content = rfh.read()
+ assert "message from test 2" in content
+
+
+def test_colored_captured_log(pytester: Pytester) -> None:
+ """Test that the level names of captured log messages of a failing test
+ are colored."""
+ pytester.makepyfile(
+ """
+ import logging
+
+ logger = logging.getLogger(__name__)
+
+ def test_foo():
+ logger.info('text going to logger from call')
+ assert False
+ """
+ )
+ result = pytester.runpytest("--log-level=INFO", "--color=yes")
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(
+ [
+ "*-- Captured log call --*",
+ "\x1b[32mINFO \x1b[0m*text going to logger from call",
+ ]
+ )
+
+
+def test_colored_ansi_esc_caplogtext(pytester: Pytester) -> None:
+ """Make sure that caplog.text does not contain ANSI escape sequences."""
+ pytester.makepyfile(
+ """
+ import logging
+
+ logger = logging.getLogger(__name__)
+
+ def test_foo(caplog):
+ logger.info('text going to logger from call')
+ assert '\x1b' not in caplog.text
+ """
+ )
+ result = pytester.runpytest("--log-level=INFO", "--color=yes")
+ assert result.ret == 0
+
+
+def test_logging_emit_error(pytester: Pytester) -> None:
+ """An exception raised during emit() should fail the test.
+
+ The default behavior of logging is to print "Logging error"
+ to stderr with the call stack and some extra details.
+
+ pytest overrides this behavior to propagate the exception.
+ """
+ pytester.makepyfile(
+ """
+ import logging
+
+ def test_bad_log():
+ logging.warning('oops', 'first', 2)
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(failed=1)
+ result.stdout.fnmatch_lines(
+ [
+ "====* FAILURES *====",
+ "*not all arguments converted during string formatting*",
+ ]
+ )
+
+
+def test_logging_emit_error_supressed(pytester: Pytester) -> None:
+ """If logging is configured to silently ignore errors, pytest
+ doesn't propagate errors either."""
+ pytester.makepyfile(
+ """
+ import logging
+
+ def test_bad_log(monkeypatch):
+ monkeypatch.setattr(logging, 'raiseExceptions', False)
+ logging.warning('oops', 'first', 2)
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=1)
+
+
+def test_log_file_cli_subdirectories_are_successfully_created(
+ pytester: Pytester,
+) -> None:
+ path = pytester.makepyfile(""" def test_logger(): pass """)
+ expected = os.path.join(os.path.dirname(str(path)), "foo", "bar")
+ result = pytester.runpytest("--log-file=foo/bar/logf.log")
+ assert "logf.log" in os.listdir(expected)
+ assert result.ret == ExitCode.OK
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/.gitignore b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/.gitignore
new file mode 100644
index 0000000000..d934447a03
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/.gitignore
@@ -0,0 +1,2 @@
+*.html
+assets/
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/README.rst b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/README.rst
new file mode 100644
index 0000000000..8f027c3bd3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/README.rst
@@ -0,0 +1,13 @@
+This folder contains tests and support files for smoke testing popular plugins against the current pytest version.
+
+The objective is to gauge if any intentional or unintentional changes in pytest break plugins.
+
+As a rule of thumb, we should add plugins here:
+
+1. That are used at large. This might be subjective in some cases, but if answer is yes to
+ the question: *if a new release of pytest causes pytest-X to break, will this break a ton of test suites out there?*.
+2. That don't have large external dependencies: such as external services.
+
+Besides adding the plugin as dependency, we should also add a quick test which uses some
+minimal part of the plugin, a smoke test. Also consider reusing one of the existing tests if that's
+possible.
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/bdd_wallet.feature b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/bdd_wallet.feature
new file mode 100644
index 0000000000..e404c4948e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/bdd_wallet.feature
@@ -0,0 +1,9 @@
+Feature: Buy things with apple
+
+ Scenario: Buy fruits
+ Given A wallet with 50
+
+ When I buy some apples for 1
+ And I buy some bananas for 2
+
+ Then I have 47 left
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/bdd_wallet.py b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/bdd_wallet.py
new file mode 100644
index 0000000000..35927ea587
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/bdd_wallet.py
@@ -0,0 +1,39 @@
+from pytest_bdd import given
+from pytest_bdd import scenario
+from pytest_bdd import then
+from pytest_bdd import when
+
+import pytest
+
+
+@scenario("bdd_wallet.feature", "Buy fruits")
+def test_publish():
+ pass
+
+
+@pytest.fixture
+def wallet():
+ class Wallet:
+ amount = 0
+
+ return Wallet()
+
+
+@given("A wallet with 50")
+def fill_wallet(wallet):
+ wallet.amount = 50
+
+
+@when("I buy some apples for 1")
+def buy_apples(wallet):
+ wallet.amount -= 1
+
+
+@when("I buy some bananas for 2")
+def buy_bananas(wallet):
+ wallet.amount -= 2
+
+
+@then("I have 47 left")
+def check(wallet):
+ assert wallet.amount == 47
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/django_settings.py b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/django_settings.py
new file mode 100644
index 0000000000..0715f47653
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/django_settings.py
@@ -0,0 +1 @@
+SECRET_KEY = "mysecret"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest.ini b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest.ini
new file mode 100644
index 0000000000..b42b07d145
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest.ini
@@ -0,0 +1,5 @@
+[pytest]
+addopts = --strict-markers
+filterwarnings =
+ error::pytest.PytestWarning
+ ignore:.*.fspath is deprecated and will be replaced by .*.path.*:pytest.PytestDeprecationWarning
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_anyio_integration.py b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_anyio_integration.py
new file mode 100644
index 0000000000..65c2f59366
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_anyio_integration.py
@@ -0,0 +1,8 @@
+import anyio
+
+import pytest
+
+
+@pytest.mark.anyio
+async def test_sleep():
+ await anyio.sleep(0)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_asyncio_integration.py b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_asyncio_integration.py
new file mode 100644
index 0000000000..5d2a3faccf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_asyncio_integration.py
@@ -0,0 +1,8 @@
+import asyncio
+
+import pytest
+
+
+@pytest.mark.asyncio
+async def test_sleep():
+ await asyncio.sleep(0)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_mock_integration.py b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_mock_integration.py
new file mode 100644
index 0000000000..740469d00f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_mock_integration.py
@@ -0,0 +1,2 @@
+def test_mocker(mocker):
+ mocker.MagicMock()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_trio_integration.py b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_trio_integration.py
new file mode 100644
index 0000000000..199f7850bc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_trio_integration.py
@@ -0,0 +1,8 @@
+import trio
+
+import pytest
+
+
+@pytest.mark.trio
+async def test_sleep():
+ await trio.sleep(0)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_twisted_integration.py b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_twisted_integration.py
new file mode 100644
index 0000000000..94748d036e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/pytest_twisted_integration.py
@@ -0,0 +1,18 @@
+import pytest_twisted
+from twisted.internet.task import deferLater
+
+
+def sleep():
+ import twisted.internet.reactor
+
+ return deferLater(clock=twisted.internet.reactor, delay=0)
+
+
+@pytest_twisted.inlineCallbacks
+def test_inlineCallbacks():
+ yield sleep()
+
+
+@pytest_twisted.ensureDeferred
+async def test_inlineCallbacks_async():
+ await sleep()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/requirements.txt b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/requirements.txt
new file mode 100644
index 0000000000..90b253cc6d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/requirements.txt
@@ -0,0 +1,15 @@
+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
+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-trio==0.7.0
+pytest-twisted==1.13.4
+twisted==21.7.0
+pytest-xvfb==2.0.0
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/simple_integration.py b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/simple_integration.py
new file mode 100644
index 0000000000..20b2fc4b5b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/plugins_integration/simple_integration.py
@@ -0,0 +1,10 @@
+import pytest
+
+
+def test_foo():
+ assert True
+
+
+@pytest.mark.parametrize("i", range(3))
+def test_bar(i):
+ assert True
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/python/approx.py b/testing/web-platform/tests/tools/third_party/pytest/testing/python/approx.py
new file mode 100644
index 0000000000..0d411d8a6d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/python/approx.py
@@ -0,0 +1,872 @@
+import operator
+import sys
+from contextlib import contextmanager
+from decimal import Decimal
+from fractions import Fraction
+from operator import eq
+from operator import ne
+from typing import Optional
+
+import pytest
+from _pytest.pytester import Pytester
+from pytest import approx
+
+inf, nan = float("inf"), float("nan")
+
+
+@pytest.fixture
+def mocked_doctest_runner(monkeypatch):
+ import doctest
+
+ class MockedPdb:
+ def __init__(self, out):
+ pass
+
+ def set_trace(self):
+ raise NotImplementedError("not used")
+
+ def reset(self):
+ pass
+
+ def set_continue(self):
+ pass
+
+ monkeypatch.setattr("doctest._OutputRedirectingPdb", MockedPdb)
+
+ 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()
+ )
+ )
+
+ return MyDocTestRunner()
+
+
+@contextmanager
+def temporary_verbosity(config, verbosity=0):
+ original_verbosity = config.getoption("verbose")
+ config.option.verbose = verbosity
+ try:
+ yield
+ finally:
+ config.option.verbose = original_verbosity
+
+
+@pytest.fixture
+def assert_approx_raises_regex(pytestconfig):
+ def do_assert(lhs, rhs, expected_message, verbosity_level=0):
+ import re
+
+ with temporary_verbosity(pytestconfig, verbosity_level):
+ with pytest.raises(AssertionError) as e:
+ assert lhs == approx(rhs)
+
+ nl = "\n"
+ obtained_message = str(e.value).splitlines()[1:]
+ assert len(obtained_message) == len(expected_message), (
+ "Regex message length doesn't match obtained.\n"
+ "Obtained:\n"
+ f"{nl.join(obtained_message)}\n\n"
+ "Expected regex:\n"
+ f"{nl.join(expected_message)}\n\n"
+ )
+
+ for i, (obtained_line, expected_line) in enumerate(
+ zip(obtained_message, expected_message)
+ ):
+ regex = re.compile(expected_line)
+ assert regex.match(obtained_line) is not None, (
+ "Unexpected error message:\n"
+ f"{nl.join(obtained_message)}\n\n"
+ "Did not match regex:\n"
+ f"{nl.join(expected_message)}\n\n"
+ f"With verbosity level = {verbosity_level}, on line {i}"
+ )
+
+ return do_assert
+
+
+SOME_FLOAT = r"[+-]?([0-9]*[.])?[0-9]+\s*"
+SOME_INT = r"[0-9]+\s*"
+
+
+class TestApprox:
+ def test_error_messages(self, assert_approx_raises_regex):
+ np = pytest.importorskip("numpy")
+
+ assert_approx_raises_regex(
+ 2.0,
+ 1.0,
+ [
+ " comparison failed",
+ f" Obtained: {SOME_FLOAT}",
+ f" Expected: {SOME_FLOAT} ± {SOME_FLOAT}",
+ ],
+ )
+
+ assert_approx_raises_regex(
+ {"a": 1.0, "b": 1000.0, "c": 1000000.0},
+ {
+ "a": 2.0,
+ "b": 1000.0,
+ "c": 3000000.0,
+ },
+ [
+ r" comparison failed. Mismatched elements: 2 / 3:",
+ rf" Max absolute difference: {SOME_FLOAT}",
+ rf" Max relative difference: {SOME_FLOAT}",
+ r" Index \| Obtained\s+\| Expected ",
+ rf" a \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}",
+ rf" c \| {SOME_FLOAT} \| {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" comparison failed. Mismatched elements: 2 / 4:",
+ 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}",
+ rf" 3 \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}",
+ ],
+ )
+
+ a = np.linspace(0, 100, 20)
+ b = np.linspace(0, 100, 20)
+ a[10] += 0.5
+ assert_approx_raises_regex(
+ a,
+ b,
+ [
+ r" comparison failed. Mismatched elements: 1 / 20:",
+ rf" Max absolute difference: {SOME_FLOAT}",
+ rf" Max relative difference: {SOME_FLOAT}",
+ r" Index \| Obtained\s+\| Expected",
+ rf" \(10,\) \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}",
+ ],
+ )
+
+ assert_approx_raises_regex(
+ np.array(
+ [
+ [[1.1987311, 12412342.3], [3.214143244, 1423412423415.677]],
+ [[1, 2], [3, 219371297321973]],
+ ]
+ ),
+ np.array(
+ [
+ [[1.12313, 12412342.3], [3.214143244, 534523542345.677]],
+ [[1, 2], [3, 7]],
+ ]
+ ),
+ [
+ r" comparison failed. Mismatched elements: 3 / 8:",
+ rf" Max absolute difference: {SOME_FLOAT}",
+ rf" Max relative difference: {SOME_FLOAT}",
+ r" Index\s+\| Obtained\s+\| Expected\s+",
+ rf" \(0, 0, 0\) \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}",
+ rf" \(0, 1, 1\) \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}",
+ rf" \(1, 1, 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" 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" 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_invalid_args(self, assert_approx_raises_regex):
+ np = pytest.importorskip("numpy")
+ with pytest.raises(AssertionError) as e:
+ assert np.array([[1.2, 3.4], [4.0, 5.0]]) == pytest.approx(
+ np.array([[4.0], [5.0]])
+ )
+ 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)",
+ ]
+ )
+
+ with pytest.raises(AssertionError) as e:
+ assert [1.0, 2.0, 3.0] == pytest.approx([4.0, 5.0])
+ message = "\n".join(str(e.value).split("\n")[1:])
+ assert message == "\n".join(
+ [
+ " Impossible to compare lists with different sizes.",
+ " Lengths: 2 and 3",
+ ]
+ )
+
+ def test_error_messages_with_different_verbosity(self, assert_approx_raises_regex):
+ np = pytest.importorskip("numpy")
+ for v in [0, 1, 2]:
+ # Verbosity level doesn't affect the error message for scalars
+ assert_approx_raises_regex(
+ 2.0,
+ 1.0,
+ [
+ " comparison failed",
+ f" Obtained: {SOME_FLOAT}",
+ f" Expected: {SOME_FLOAT} ± {SOME_FLOAT}",
+ ],
+ verbosity_level=v,
+ )
+
+ a = np.linspace(1, 101, 20)
+ b = np.linspace(2, 102, 20)
+ assert_approx_raises_regex(
+ 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",
+ ],
+ verbosity_level=0,
+ )
+
+ assert_approx_raises_regex(
+ 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" \({i},\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}"
+ for i in range(20)
+ ],
+ verbosity_level=2,
+ )
+
+ def test_repr_string(self):
+ assert repr(approx(1.0)) == "1.0 ± 1.0e-06"
+ assert repr(approx([1.0, 2.0])) == "approx([1.0 ± 1.0e-06, 2.0 ± 2.0e-06])"
+ assert repr(approx((1.0, 2.0))) == "approx((1.0 ± 1.0e-06, 2.0 ± 2.0e-06))"
+ assert repr(approx(inf)) == "inf"
+ assert repr(approx(1.0, rel=nan)) == "1.0 ± ???"
+ assert repr(approx(1.0, rel=inf)) == "1.0 ± inf"
+
+ # Dictionaries aren't ordered, so we need to check both orders.
+ assert repr(approx({"a": 1.0, "b": 2.0})) in (
+ "approx({'a': 1.0 ± 1.0e-06, 'b': 2.0 ± 2.0e-06})",
+ "approx({'b': 2.0 ± 2.0e-06, 'a': 1.0 ± 1.0e-06})",
+ )
+
+ def test_repr_complex_numbers(self):
+ assert repr(approx(inf + 1j)) == "(inf+1j)"
+ assert repr(approx(1.0j, rel=inf)) == "1j ± inf"
+
+ # can't compute a sensible tolerance
+ assert repr(approx(nan + 1j)) == "(nan+1j) ± ???"
+
+ assert repr(approx(1.0j)) == "1j ± 1.0e-06 ∠ ±180°"
+
+ # relative tolerance is scaled to |3+4j| = 5
+ assert repr(approx(3 + 4 * 1j)) == "(3+4j) ± 5.0e-06 ∠ ±180°"
+
+ # absolute tolerance is not scaled
+ assert repr(approx(3.3 + 4.4 * 1j, abs=0.02)) == "(3.3+4.4j) ± 2.0e-02 ∠ ±180°"
+
+ @pytest.mark.parametrize(
+ "value, expected_repr_string",
+ [
+ (5.0, "approx(5.0 ± 5.0e-06)"),
+ ([5.0], "approx([5.0 ± 5.0e-06])"),
+ ([[5.0]], "approx([[5.0 ± 5.0e-06]])"),
+ ([[5.0, 6.0]], "approx([[5.0 ± 5.0e-06, 6.0 ± 6.0e-06]])"),
+ ([[5.0], [6.0]], "approx([[5.0 ± 5.0e-06], [6.0 ± 6.0e-06]])"),
+ ],
+ )
+ def test_repr_nd_array(self, value, expected_repr_string):
+ """Make sure that arrays of all different dimensions are repr'd correctly."""
+ np = pytest.importorskip("numpy")
+ np_array = np.array(value)
+ assert repr(approx(np_array)) == expected_repr_string
+
+ def test_bool(self):
+ with pytest.raises(AssertionError) as err:
+ assert approx(1)
+
+ assert err.match(r"approx\(\) is not supported in a boolean context")
+
+ def test_operator_overloading(self):
+ assert 1 == approx(1, rel=1e-6, abs=1e-12)
+ assert not (1 != approx(1, rel=1e-6, abs=1e-12))
+ assert 10 != approx(1, rel=1e-6, abs=1e-12)
+ assert not (10 == approx(1, rel=1e-6, abs=1e-12))
+
+ def test_exactly_equal(self):
+ examples = [
+ (2.0, 2.0),
+ (0.1e200, 0.1e200),
+ (1.123e-300, 1.123e-300),
+ (12345, 12345.0),
+ (0.0, -0.0),
+ (345678, 345678),
+ (Decimal("1.0001"), Decimal("1.0001")),
+ (Fraction(1, 3), Fraction(-1, -3)),
+ ]
+ for a, x in examples:
+ assert a == approx(x)
+
+ def test_opposite_sign(self):
+ examples = [(eq, 1e-100, -1e-100), (ne, 1e100, -1e100)]
+ for op, a, x in examples:
+ assert op(a, approx(x))
+
+ def test_zero_tolerance(self):
+ within_1e10 = [(1.1e-100, 1e-100), (-1.1e-100, -1e-100)]
+ for a, x in within_1e10:
+ assert x == approx(x, rel=0.0, abs=0.0)
+ assert a != approx(x, rel=0.0, abs=0.0)
+ assert a == approx(x, rel=0.0, abs=5e-101)
+ assert a != approx(x, rel=0.0, abs=5e-102)
+ assert a == approx(x, rel=5e-1, abs=0.0)
+ assert a != approx(x, rel=5e-2, abs=0.0)
+
+ @pytest.mark.parametrize(
+ ("rel", "abs"),
+ [
+ (-1e100, None),
+ (None, -1e100),
+ (1e100, -1e100),
+ (-1e100, 1e100),
+ (-1e100, -1e100),
+ ],
+ )
+ def test_negative_tolerance(
+ self, rel: Optional[float], abs: Optional[float]
+ ) -> None:
+ # Negative tolerances are not allowed.
+ with pytest.raises(ValueError):
+ 1.1 == approx(1, rel, abs)
+
+ def test_negative_tolerance_message(self):
+ # Error message for negative tolerance should include the value.
+ with pytest.raises(ValueError, match="-3"):
+ 0 == approx(1, abs=-3)
+ with pytest.raises(ValueError, match="-3"):
+ 0 == approx(1, rel=-3)
+
+ def test_inf_tolerance(self):
+ # Everything should be equal if the tolerance is infinite.
+ large_diffs = [(1, 1000), (1e-50, 1e50), (-1.0, -1e300), (0.0, 10)]
+ for a, x in large_diffs:
+ assert a != approx(x, rel=0.0, abs=0.0)
+ assert a == approx(x, rel=inf, abs=0.0)
+ assert a == approx(x, rel=0.0, abs=inf)
+ assert a == approx(x, rel=inf, abs=inf)
+
+ def test_inf_tolerance_expecting_zero(self) -> None:
+ # If the relative tolerance is zero but the expected value is infinite,
+ # the actual tolerance is a NaN, which should be an error.
+ with pytest.raises(ValueError):
+ 1 == approx(0, rel=inf, abs=0.0)
+ with pytest.raises(ValueError):
+ 1 == approx(0, rel=inf, abs=inf)
+
+ def test_nan_tolerance(self) -> None:
+ with pytest.raises(ValueError):
+ 1.1 == approx(1, rel=nan)
+ with pytest.raises(ValueError):
+ 1.1 == approx(1, abs=nan)
+ with pytest.raises(ValueError):
+ 1.1 == approx(1, rel=nan, abs=nan)
+
+ def test_reasonable_defaults(self):
+ # Whatever the defaults are, they should work for numbers close to 1
+ # than have a small amount of floating-point error.
+ assert 0.1 + 0.2 == approx(0.3)
+
+ def test_default_tolerances(self):
+ # This tests the defaults as they are currently set. If you change the
+ # defaults, this test will fail but you should feel free to change it.
+ # None of the other tests (except the doctests) should be affected by
+ # the choice of defaults.
+ examples = [
+ # Relative tolerance used.
+ (eq, 1e100 + 1e94, 1e100),
+ (ne, 1e100 + 2e94, 1e100),
+ (eq, 1e0 + 1e-6, 1e0),
+ (ne, 1e0 + 2e-6, 1e0),
+ # Absolute tolerance used.
+ (eq, 1e-100, +1e-106),
+ (eq, 1e-100, +2e-106),
+ (eq, 1e-100, 0),
+ ]
+ for op, a, x in examples:
+ assert op(a, approx(x))
+
+ def test_custom_tolerances(self):
+ assert 1e8 + 1e0 == approx(1e8, rel=5e-8, abs=5e0)
+ assert 1e8 + 1e0 == approx(1e8, rel=5e-9, abs=5e0)
+ assert 1e8 + 1e0 == approx(1e8, rel=5e-8, abs=5e-1)
+ assert 1e8 + 1e0 != approx(1e8, rel=5e-9, abs=5e-1)
+
+ assert 1e0 + 1e-8 == approx(1e0, rel=5e-8, abs=5e-8)
+ assert 1e0 + 1e-8 == approx(1e0, rel=5e-9, abs=5e-8)
+ assert 1e0 + 1e-8 == approx(1e0, rel=5e-8, abs=5e-9)
+ assert 1e0 + 1e-8 != approx(1e0, rel=5e-9, abs=5e-9)
+
+ assert 1e-8 + 1e-16 == approx(1e-8, rel=5e-8, abs=5e-16)
+ assert 1e-8 + 1e-16 == approx(1e-8, rel=5e-9, abs=5e-16)
+ assert 1e-8 + 1e-16 == approx(1e-8, rel=5e-8, abs=5e-17)
+ assert 1e-8 + 1e-16 != approx(1e-8, rel=5e-9, abs=5e-17)
+
+ def test_relative_tolerance(self):
+ within_1e8_rel = [(1e8 + 1e0, 1e8), (1e0 + 1e-8, 1e0), (1e-8 + 1e-16, 1e-8)]
+ for a, x in within_1e8_rel:
+ assert a == approx(x, rel=5e-8, abs=0.0)
+ assert a != approx(x, rel=5e-9, abs=0.0)
+
+ def test_absolute_tolerance(self):
+ within_1e8_abs = [(1e8 + 9e-9, 1e8), (1e0 + 9e-9, 1e0), (1e-8 + 9e-9, 1e-8)]
+ for a, x in within_1e8_abs:
+ assert a == approx(x, rel=0, abs=5e-8)
+ assert a != approx(x, rel=0, abs=5e-9)
+
+ def test_expecting_zero(self):
+ examples = [
+ (ne, 1e-6, 0.0),
+ (ne, -1e-6, 0.0),
+ (eq, 1e-12, 0.0),
+ (eq, -1e-12, 0.0),
+ (ne, 2e-12, 0.0),
+ (ne, -2e-12, 0.0),
+ (ne, inf, 0.0),
+ (ne, nan, 0.0),
+ ]
+ for op, a, x in examples:
+ assert op(a, approx(x, rel=0.0, abs=1e-12))
+ assert op(a, approx(x, rel=1e-6, abs=1e-12))
+
+ def test_expecting_inf(self):
+ examples = [
+ (eq, inf, inf),
+ (eq, -inf, -inf),
+ (ne, inf, -inf),
+ (ne, 0.0, inf),
+ (ne, nan, inf),
+ ]
+ for op, a, x in examples:
+ assert op(a, approx(x))
+
+ def test_expecting_nan(self):
+ examples = [
+ (eq, nan, nan),
+ (eq, -nan, -nan),
+ (eq, nan, -nan),
+ (ne, 0.0, nan),
+ (ne, inf, nan),
+ ]
+ for op, a, x in examples:
+ # Nothing is equal to NaN by default.
+ assert a != approx(x)
+
+ # If ``nan_ok=True``, then NaN is equal to NaN.
+ assert op(a, approx(x, nan_ok=True))
+
+ def test_int(self):
+ within_1e6 = [(1000001, 1000000), (-1000001, -1000000)]
+ for a, x in within_1e6:
+ assert a == approx(x, rel=5e-6, abs=0)
+ assert a != approx(x, rel=5e-7, abs=0)
+ assert approx(x, rel=5e-6, abs=0) == a
+ assert approx(x, rel=5e-7, abs=0) != a
+
+ def test_decimal(self):
+ within_1e6 = [
+ (Decimal("1.000001"), Decimal("1.0")),
+ (Decimal("-1.000001"), Decimal("-1.0")),
+ ]
+ for a, x in within_1e6:
+ assert a == approx(x)
+ assert a == approx(x, rel=Decimal("5e-6"), abs=0)
+ assert a != approx(x, rel=Decimal("5e-7"), abs=0)
+ assert approx(x, rel=Decimal("5e-6"), abs=0) == a
+ assert approx(x, rel=Decimal("5e-7"), abs=0) != a
+
+ def test_fraction(self):
+ within_1e6 = [
+ (1 + Fraction(1, 1000000), Fraction(1)),
+ (-1 - Fraction(-1, 1000000), Fraction(-1)),
+ ]
+ for a, x in within_1e6:
+ assert a == approx(x, rel=5e-6, abs=0)
+ assert a != approx(x, rel=5e-7, abs=0)
+ assert approx(x, rel=5e-6, abs=0) == a
+ assert approx(x, rel=5e-7, abs=0) != a
+
+ def test_complex(self):
+ within_1e6 = [
+ (1.000001 + 1.0j, 1.0 + 1.0j),
+ (1.0 + 1.000001j, 1.0 + 1.0j),
+ (-1.000001 + 1.0j, -1.0 + 1.0j),
+ (1.0 - 1.000001j, 1.0 - 1.0j),
+ ]
+ for a, x in within_1e6:
+ assert a == approx(x, rel=5e-6, abs=0)
+ assert a != approx(x, rel=5e-7, abs=0)
+ assert approx(x, rel=5e-6, abs=0) == a
+ assert approx(x, rel=5e-7, abs=0) != a
+
+ def test_list(self):
+ actual = [1 + 1e-7, 2 + 1e-8]
+ expected = [1, 2]
+
+ # Return false if any element is outside the tolerance.
+ assert actual == approx(expected, rel=5e-7, abs=0)
+ assert actual != approx(expected, rel=5e-8, abs=0)
+ assert approx(expected, rel=5e-7, abs=0) == actual
+ assert approx(expected, rel=5e-8, abs=0) != actual
+
+ def test_list_decimal(self):
+ actual = [Decimal("1.000001"), Decimal("2.000001")]
+ expected = [Decimal("1"), Decimal("2")]
+
+ assert actual == approx(expected)
+
+ def test_list_wrong_len(self):
+ assert [1, 2] != approx([1])
+ assert [1, 2] != approx([1, 2, 3])
+
+ def test_tuple(self):
+ actual = (1 + 1e-7, 2 + 1e-8)
+ expected = (1, 2)
+
+ # Return false if any element is outside the tolerance.
+ assert actual == approx(expected, rel=5e-7, abs=0)
+ assert actual != approx(expected, rel=5e-8, abs=0)
+ assert approx(expected, rel=5e-7, abs=0) == actual
+ assert approx(expected, rel=5e-8, abs=0) != actual
+
+ def test_tuple_wrong_len(self):
+ assert (1, 2) != approx((1,))
+ assert (1, 2) != approx((1, 2, 3))
+
+ def test_tuple_vs_other(self):
+ assert 1 != approx((1,))
+
+ def test_dict(self):
+ actual = {"a": 1 + 1e-7, "b": 2 + 1e-8}
+ # Dictionaries became ordered in python3.6, so switch up the order here
+ # to make sure it doesn't matter.
+ expected = {"b": 2, "a": 1}
+
+ # Return false if any element is outside the tolerance.
+ assert actual == approx(expected, rel=5e-7, abs=0)
+ assert actual != approx(expected, rel=5e-8, abs=0)
+ assert approx(expected, rel=5e-7, abs=0) == actual
+ assert approx(expected, rel=5e-8, abs=0) != actual
+
+ def test_dict_decimal(self):
+ actual = {"a": Decimal("1.000001"), "b": Decimal("2.000001")}
+ # Dictionaries became ordered in python3.6, so switch up the order here
+ # to make sure it doesn't matter.
+ expected = {"b": Decimal("2"), "a": Decimal("1")}
+
+ assert actual == approx(expected)
+
+ def test_dict_wrong_len(self):
+ assert {"a": 1, "b": 2} != approx({"a": 1})
+ assert {"a": 1, "b": 2} != approx({"a": 1, "c": 2})
+ assert {"a": 1, "b": 2} != approx({"a": 1, "b": 2, "c": 3})
+
+ def test_dict_nonnumeric(self):
+ assert {"a": 1.0, "b": None} == pytest.approx({"a": 1.0, "b": None})
+ assert {"a": 1.0, "b": 1} != pytest.approx({"a": 1.0, "b": None})
+
+ def test_dict_vs_other(self):
+ assert 1 != approx({"a": 0})
+
+ def test_numpy_array(self):
+ np = pytest.importorskip("numpy")
+
+ actual = np.array([1 + 1e-7, 2 + 1e-8])
+ expected = np.array([1, 2])
+
+ # Return false if any element is outside the tolerance.
+ assert actual == approx(expected, rel=5e-7, abs=0)
+ assert actual != approx(expected, rel=5e-8, abs=0)
+ assert approx(expected, rel=5e-7, abs=0) == expected
+ assert approx(expected, rel=5e-8, abs=0) != actual
+
+ # Should be able to compare lists with numpy arrays.
+ assert list(actual) == approx(expected, rel=5e-7, abs=0)
+ assert list(actual) != approx(expected, rel=5e-8, abs=0)
+ assert actual == approx(list(expected), rel=5e-7, abs=0)
+ assert actual != approx(list(expected), rel=5e-8, abs=0)
+
+ def test_numpy_tolerance_args(self):
+ """
+ Check that numpy rel/abs args are handled correctly
+ for comparison against an np.array
+ Check both sides of the operator, hopefully it doesn't impact things.
+ Test all permutations of where the approx and np.array() can show up
+ """
+ np = pytest.importorskip("numpy")
+ expected = 100.0
+ actual = 99.0
+ abs_diff = expected - actual
+ rel_diff = (expected - actual) / expected
+
+ tests = [
+ (eq, abs_diff, 0),
+ (eq, 0, rel_diff),
+ (ne, 0, rel_diff / 2.0), # rel diff fail
+ (ne, abs_diff / 2.0, 0), # abs diff fail
+ ]
+
+ for op, _abs, _rel in tests:
+ assert op(np.array(actual), approx(expected, abs=_abs, rel=_rel)) # a, b
+ assert op(approx(expected, abs=_abs, rel=_rel), np.array(actual)) # b, a
+
+ assert op(actual, approx(np.array(expected), abs=_abs, rel=_rel)) # a, b
+ assert op(approx(np.array(expected), abs=_abs, rel=_rel), actual) # b, a
+
+ assert op(np.array(actual), approx(np.array(expected), abs=_abs, rel=_rel))
+ assert op(approx(np.array(expected), abs=_abs, rel=_rel), np.array(actual))
+
+ def test_numpy_expecting_nan(self):
+ np = pytest.importorskip("numpy")
+ examples = [
+ (eq, nan, nan),
+ (eq, -nan, -nan),
+ (eq, nan, -nan),
+ (ne, 0.0, nan),
+ (ne, inf, nan),
+ ]
+ for op, a, x in examples:
+ # Nothing is equal to NaN by default.
+ assert np.array(a) != approx(x)
+ assert a != approx(np.array(x))
+
+ # If ``nan_ok=True``, then NaN is equal to NaN.
+ assert op(np.array(a), approx(x, nan_ok=True))
+ assert op(a, approx(np.array(x), nan_ok=True))
+
+ def test_numpy_expecting_inf(self):
+ np = pytest.importorskip("numpy")
+ examples = [
+ (eq, inf, inf),
+ (eq, -inf, -inf),
+ (ne, inf, -inf),
+ (ne, 0.0, inf),
+ (ne, nan, inf),
+ ]
+ for op, a, x in examples:
+ assert op(np.array(a), approx(x))
+ assert op(a, approx(np.array(x)))
+ assert op(np.array(a), approx(np.array(x)))
+
+ def test_numpy_array_wrong_shape(self):
+ np = pytest.importorskip("numpy")
+
+ a12 = np.array([[1, 2]])
+ a21 = np.array([[1], [2]])
+
+ assert a12 != approx(a21)
+ assert a21 != approx(a12)
+
+ def test_numpy_array_protocol(self):
+ """
+ array-like objects such as tensorflow's DeviceArray are handled like ndarray.
+ See issue #8132
+ """
+ np = pytest.importorskip("numpy")
+
+ class DeviceArray:
+ def __init__(self, value, size):
+ self.value = value
+ self.size = size
+
+ def __array__(self):
+ return self.value * np.ones(self.size)
+
+ class DeviceScalar:
+ def __init__(self, value):
+ self.value = value
+
+ def __array__(self):
+ return np.array(self.value)
+
+ expected = 1
+ actual = 1 + 1e-6
+ assert approx(expected) == DeviceArray(actual, size=1)
+ assert approx(expected) == DeviceArray(actual, size=2)
+ assert approx(expected) == DeviceScalar(actual)
+ assert approx(DeviceScalar(expected)) == actual
+ assert approx(DeviceScalar(expected)) == DeviceScalar(actual)
+
+ def test_doctests(self, mocked_doctest_runner) -> None:
+ import doctest
+
+ parser = doctest.DocTestParser()
+ assert approx.__doc__ is not None
+ test = parser.get_doctest(
+ approx.__doc__, {"approx": approx}, approx.__name__, None, None
+ )
+ mocked_doctest_runner.run(test)
+
+ def test_unicode_plus_minus(self, pytester: Pytester) -> None:
+ """
+ Comparing approx instances inside lists should not produce an error in the detailed diff.
+ Integration test for issue #2111.
+ """
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_foo():
+ assert [3] == [pytest.approx(4)]
+ """
+ )
+ expected = "4.0e-06"
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [f"*At index 0 diff: 3 != 4 ± {expected}", "=* 1 failed in *="]
+ )
+
+ @pytest.mark.parametrize(
+ "x, name",
+ [
+ pytest.param([[1]], "data structures", id="nested-list"),
+ pytest.param({"key": {"key": 1}}, "dictionaries", id="nested-dict"),
+ ],
+ )
+ def test_expected_value_type_error(self, x, name):
+ with pytest.raises(
+ TypeError,
+ match=fr"pytest.approx\(\) does not support nested {name}:",
+ ):
+ approx(x)
+
+ @pytest.mark.parametrize(
+ "x",
+ [
+ pytest.param(None),
+ pytest.param("string"),
+ pytest.param(["string"], id="nested-str"),
+ pytest.param({"key": "string"}, id="dict-with-string"),
+ ],
+ )
+ def test_nonnumeric_okay_if_equal(self, x):
+ assert x == approx(x)
+
+ @pytest.mark.parametrize(
+ "x",
+ [
+ pytest.param("string"),
+ pytest.param(["string"], id="nested-str"),
+ pytest.param({"key": "string"}, id="dict-with-string"),
+ ],
+ )
+ def test_nonnumeric_false_if_unequal(self, x):
+ """For nonnumeric types, x != pytest.approx(y) reduces to x != y"""
+ assert "ab" != approx("abc")
+ assert ["ab"] != approx(["abc"])
+ # in particular, both of these should return False
+ assert {"a": 1.0} != approx({"a": None})
+ assert {"a": None} != approx({"a": 1.0})
+
+ assert 1.0 != approx(None)
+ assert None != approx(1.0) # noqa: E711
+
+ 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}
+ assert (
+ repr(approx(x1))
+ == "approx({'foo': 1.0000005 ± 1.0e-06, 'bar': None, 'foobar': inf})"
+ )
+
+ def test_nonnumeric_list_repr(self):
+ """Lists with non-numerics and infinites have no tolerances"""
+ x1 = [1.0000005, None, inf]
+ assert repr(approx(x1)) == "approx([1.0000005 ± 1.0e-06, None, inf])"
+
+ @pytest.mark.parametrize(
+ "op",
+ [
+ pytest.param(operator.le, id="<="),
+ pytest.param(operator.lt, id="<"),
+ pytest.param(operator.ge, id=">="),
+ pytest.param(operator.gt, id=">"),
+ ],
+ )
+ def test_comparison_operator_type_error(self, op):
+ """pytest.approx should raise TypeError for operators other than == and != (#2003)."""
+ with pytest.raises(TypeError):
+ op(1, approx(1, rel=1e-6, abs=1e-12))
+
+ def test_numpy_array_with_scalar(self):
+ np = pytest.importorskip("numpy")
+
+ actual = np.array([1 + 1e-7, 1 - 1e-8])
+ expected = 1.0
+
+ assert actual == approx(expected, rel=5e-7, abs=0)
+ assert actual != approx(expected, rel=5e-8, abs=0)
+ assert approx(expected, rel=5e-7, abs=0) == actual
+ assert approx(expected, rel=5e-8, abs=0) != actual
+
+ def test_numpy_scalar_with_array(self):
+ np = pytest.importorskip("numpy")
+
+ actual = 1.0
+ expected = np.array([1 + 1e-7, 1 - 1e-8])
+
+ assert actual == approx(expected, rel=5e-7, abs=0)
+ assert actual != approx(expected, rel=5e-8, abs=0)
+ 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 __len__(self):
+ return 4
+
+ expected = MySizedIterable()
+ assert [1, 2, 3, 4] == approx(expected)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/python/collect.py b/testing/web-platform/tests/tools/third_party/pytest/testing/python/collect.py
new file mode 100644
index 0000000000..ac3edd395a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/python/collect.py
@@ -0,0 +1,1493 @@
+import os
+import sys
+import textwrap
+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
+from _pytest.nodes import Collector
+from _pytest.pytester import Pytester
+from _pytest.python import Class
+from _pytest.python import Function
+
+
+class TestModule:
+ def test_failing_import(self, pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol("import alksdjalskdjalkjals")
+ pytest.raises(Collector.CollectError, modcol.collect)
+
+ def test_import_duplicate(self, pytester: Pytester) -> None:
+ a = pytester.mkdir("a")
+ b = pytester.mkdir("b")
+ p1 = a.joinpath("test_whatever.py")
+ p1.touch()
+ p2 = b.joinpath("test_whatever.py")
+ p2.touch()
+ # ensure we don't have it imported already
+ sys.modules.pop(p1.stem, None)
+
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*import*mismatch*",
+ "*imported*test_whatever*",
+ "*%s*" % p1,
+ "*not the same*",
+ "*%s*" % p2,
+ "*HINT*",
+ ]
+ )
+
+ def test_import_prepend_append(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ root1 = pytester.mkdir("root1")
+ root2 = pytester.mkdir("root2")
+ root1.joinpath("x456.py").touch()
+ root2.joinpath("x456.py").touch()
+ p = root2.joinpath("test_x456.py")
+ monkeypatch.syspath_prepend(str(root1))
+ p.write_text(
+ textwrap.dedent(
+ """\
+ import x456
+ def test():
+ assert x456.__file__.startswith({!r})
+ """.format(
+ str(root2)
+ )
+ )
+ )
+ with monkeypatch.context() as mp:
+ mp.chdir(root2)
+ reprec = pytester.inline_run("--import-mode=append")
+ reprec.assertoutcome(passed=0, failed=1)
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_syntax_error_in_module(self, pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol("this is a syntax error")
+ pytest.raises(modcol.CollectError, modcol.collect)
+ pytest.raises(modcol.CollectError, modcol.collect)
+
+ def test_module_considers_pluginmanager_at_import(self, pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol("pytest_plugins='xasdlkj',")
+ pytest.raises(ImportError, lambda: modcol.obj)
+
+ def test_invalid_test_module_name(self, pytester: Pytester) -> None:
+ a = pytester.mkdir("a")
+ a.joinpath("test_one.part1.py").touch()
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "ImportError while importing test module*test_one.part1*",
+ "Hint: make sure your test modules/packages have valid Python names.",
+ ]
+ )
+
+ @pytest.mark.parametrize("verbose", [0, 1, 2])
+ def test_show_traceback_import_error(
+ self, pytester: Pytester, verbose: int
+ ) -> None:
+ """Import errors when collecting modules should display the traceback (#1976).
+
+ With low verbosity we omit pytest and internal modules, otherwise show all traceback entries.
+ """
+ pytester.makepyfile(
+ foo_traceback_import_error="""
+ from bar_traceback_import_error import NOT_AVAILABLE
+ """,
+ bar_traceback_import_error="",
+ )
+ pytester.makepyfile(
+ """
+ import foo_traceback_import_error
+ """
+ )
+ args = ("-v",) * verbose
+ result = pytester.runpytest(*args)
+ result.stdout.fnmatch_lines(
+ [
+ "ImportError while importing test module*",
+ "Traceback:",
+ "*from bar_traceback_import_error import NOT_AVAILABLE",
+ "*cannot import name *NOT_AVAILABLE*",
+ ]
+ )
+ assert result.ret == 2
+
+ stdout = result.stdout.str()
+ if verbose == 2:
+ assert "_pytest" in stdout
+ else:
+ assert "_pytest" not in stdout
+
+ def test_show_traceback_import_error_unicode(self, pytester: Pytester) -> None:
+ """Check test modules collected which raise ImportError with unicode messages
+ are handled properly (#2336).
+ """
+ pytester.makepyfile("raise ImportError('Something bad happened ☺')")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "ImportError while importing test module*",
+ "Traceback:",
+ "*raise ImportError*Something bad happened*",
+ ]
+ )
+ assert result.ret == 2
+
+
+class TestClass:
+ def test_class_with_init_warning(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ class TestClass1(object):
+ def __init__(self):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*cannot collect test class 'TestClass1' because it has "
+ "a __init__ constructor (from: test_class_with_init_warning.py)"
+ ]
+ )
+
+ def test_class_with_new_warning(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ class TestClass1(object):
+ def __new__(self):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*cannot collect test class 'TestClass1' because it has "
+ "a __new__ constructor (from: test_class_with_new_warning.py)"
+ ]
+ )
+
+ def test_class_subclassobject(self, pytester: Pytester) -> None:
+ pytester.getmodulecol(
+ """
+ class test(object):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*collected 0*"])
+
+ def test_static_method(self, pytester: Pytester) -> None:
+ """Support for collecting staticmethod tests (#2528, #2699)"""
+ pytester.getmodulecol(
+ """
+ import pytest
+ class Test(object):
+ @staticmethod
+ def test_something():
+ pass
+
+ @pytest.fixture
+ def fix(self):
+ return 1
+
+ @staticmethod
+ def test_fix(fix):
+ assert fix == 1
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*collected 2 items*", "*2 passed in*"])
+
+ def test_setup_teardown_class_as_classmethod(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_mod1="""
+ class TestClassMethod(object):
+ @classmethod
+ def setup_class(cls):
+ pass
+ def test_1(self):
+ pass
+ @classmethod
+ def teardown_class(cls):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_issue1035_obj_has_getattr(self, pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol(
+ """
+ class Chameleon(object):
+ def __getattr__(self, name):
+ return True
+ chameleon = Chameleon()
+ """
+ )
+ colitems = modcol.collect()
+ assert len(colitems) == 0
+
+ def test_issue1579_namedtuple(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import collections
+
+ TestCase = collections.namedtuple('TestCase', ['a'])
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ "*cannot collect test class 'TestCase' "
+ "because it has a __new__ constructor*"
+ )
+
+ def test_issue2234_property(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ class TestCase(object):
+ @property
+ def prop(self):
+ raise NotImplementedError()
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+
+class TestFunction:
+ def test_getmodulecollector(self, pytester: Pytester) -> None:
+ item = pytester.getitem("def test_func(): pass")
+ modcol = item.getparent(pytest.Module)
+ assert isinstance(modcol, pytest.Module)
+ assert hasattr(modcol.obj, "test_func")
+
+ @pytest.mark.filterwarnings("default")
+ def test_function_as_object_instance_ignored(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ class A(object):
+ def __call__(self, tmp_path):
+ 0/0
+
+ test_a = A()
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "collected 0 items",
+ "*test_function_as_object_instance_ignored.py:2: "
+ "*cannot collect 'test_a' because it is not a function.",
+ ]
+ )
+
+ @staticmethod
+ def make_function(pytester: Pytester, **kwargs: Any) -> Any:
+ from _pytest.fixtures import FixtureManager
+
+ config = pytester.parseconfigure()
+ session = Session.from_config(config)
+ session._fixturemanager = FixtureManager(session)
+
+ return pytest.Function.from_parent(parent=session, **kwargs)
+
+ def test_function_equality(self, pytester: Pytester) -> None:
+ def func1():
+ pass
+
+ def func2():
+ pass
+
+ f1 = self.make_function(pytester, name="name", callobj=func1)
+ assert f1 == f1
+ f2 = self.make_function(
+ pytester, name="name", callobj=func2, originalname="foobar"
+ )
+ assert f1 != f2
+
+ def test_repr_produces_actual_test_id(self, pytester: Pytester) -> None:
+ f = self.make_function(
+ pytester, name=r"test[\xe5]", callobj=self.test_repr_produces_actual_test_id
+ )
+ assert repr(f) == r"<Function test[\xe5]>"
+
+ def test_issue197_parametrize_emptyset(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize('arg', [])
+ def test_function(arg):
+ pass
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(skipped=1)
+
+ def test_single_tuple_unwraps_values(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize(('arg',), [(1,)])
+ def test_function(arg):
+ assert arg == 1
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_issue213_parametrize_value_no_equal(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ class A(object):
+ def __eq__(self, other):
+ raise ValueError("not possible")
+ @pytest.mark.parametrize('arg', [A()])
+ def test_function(arg):
+ assert arg.__class__.__name__ == "A"
+ """
+ )
+ reprec = pytester.inline_run("--fulltrace")
+ reprec.assertoutcome(passed=1)
+
+ def test_parametrize_with_non_hashable_values(self, pytester: Pytester) -> None:
+ """Test parametrization with non-hashable values."""
+ pytester.makepyfile(
+ """
+ archival_mapping = {
+ '1.0': {'tag': '1.0'},
+ '1.2.2a1': {'tag': 'release-1.2.2a1'},
+ }
+
+ import pytest
+ @pytest.mark.parametrize('key value'.split(),
+ archival_mapping.items())
+ def test_archival_to_version(key, value):
+ assert key in archival_mapping
+ assert value == archival_mapping[key]
+ """
+ )
+ rec = pytester.inline_run()
+ rec.assertoutcome(passed=2)
+
+ def test_parametrize_with_non_hashable_values_indirect(
+ self, pytester: Pytester
+ ) -> None:
+ """Test parametrization with non-hashable values with indirect parametrization."""
+ pytester.makepyfile(
+ """
+ archival_mapping = {
+ '1.0': {'tag': '1.0'},
+ '1.2.2a1': {'tag': 'release-1.2.2a1'},
+ }
+
+ import pytest
+
+ @pytest.fixture
+ def key(request):
+ return request.param
+
+ @pytest.fixture
+ def value(request):
+ return request.param
+
+ @pytest.mark.parametrize('key value'.split(),
+ archival_mapping.items(), indirect=True)
+ def test_archival_to_version(key, value):
+ assert key in archival_mapping
+ assert value == archival_mapping[key]
+ """
+ )
+ rec = pytester.inline_run()
+ rec.assertoutcome(passed=2)
+
+ def test_parametrize_overrides_fixture(self, pytester: Pytester) -> None:
+ """Test parametrization when parameter overrides existing fixture with same name."""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def value():
+ return 'value'
+
+ @pytest.mark.parametrize('value',
+ ['overridden'])
+ def test_overridden_via_param(value):
+ assert value == 'overridden'
+
+ @pytest.mark.parametrize('somevalue', ['overridden'])
+ def test_not_overridden(value, somevalue):
+ assert value == 'value'
+ assert somevalue == 'overridden'
+
+ @pytest.mark.parametrize('other,value', [('foo', 'overridden')])
+ def test_overridden_via_multiparam(other, value):
+ assert other == 'foo'
+ assert value == 'overridden'
+ """
+ )
+ rec = pytester.inline_run()
+ rec.assertoutcome(passed=3)
+
+ def test_parametrize_overrides_parametrized_fixture(
+ self, pytester: Pytester
+ ) -> None:
+ """Test parametrization when parameter overrides existing parametrized fixture with same name."""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(params=[1, 2])
+ def value(request):
+ return request.param
+
+ @pytest.mark.parametrize('value',
+ ['overridden'])
+ def test_overridden_via_param(value):
+ assert value == 'overridden'
+ """
+ )
+ rec = pytester.inline_run()
+ rec.assertoutcome(passed=1)
+
+ def test_parametrize_overrides_indirect_dependency_fixture(
+ self, pytester: Pytester
+ ) -> None:
+ """Test parametrization when parameter overrides a fixture that a test indirectly depends on"""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ fix3_instantiated = False
+
+ @pytest.fixture
+ def fix1(fix2):
+ return fix2 + '1'
+
+ @pytest.fixture
+ def fix2(fix3):
+ return fix3 + '2'
+
+ @pytest.fixture
+ def fix3():
+ global fix3_instantiated
+ fix3_instantiated = True
+ return '3'
+
+ @pytest.mark.parametrize('fix2', ['2'])
+ def test_it(fix1):
+ assert fix1 == '21'
+ assert not fix3_instantiated
+ """
+ )
+ rec = pytester.inline_run()
+ rec.assertoutcome(passed=1)
+
+ def test_parametrize_with_mark(self, pytester: Pytester) -> None:
+ items = pytester.getitems(
+ """
+ import pytest
+ @pytest.mark.foo
+ @pytest.mark.parametrize('arg', [
+ 1,
+ pytest.param(2, marks=[pytest.mark.baz, pytest.mark.bar])
+ ])
+ def test_function(arg):
+ pass
+ """
+ )
+ keywords = [item.keywords for item in items]
+ assert (
+ "foo" in keywords[0]
+ and "bar" not in keywords[0]
+ and "baz" not in keywords[0]
+ )
+ assert "foo" in keywords[1] and "bar" in keywords[1] and "baz" in keywords[1]
+
+ def test_parametrize_with_empty_string_arguments(self, pytester: Pytester) -> None:
+ items = pytester.getitems(
+ """\
+ import pytest
+
+ @pytest.mark.parametrize('v', ('', ' '))
+ @pytest.mark.parametrize('w', ('', ' '))
+ def test(v, w): ...
+ """
+ )
+ names = {item.name for item in items}
+ assert names == {"test[-]", "test[ -]", "test[- ]", "test[ - ]"}
+
+ def test_function_equality_with_callspec(self, pytester: Pytester) -> None:
+ items = pytester.getitems(
+ """
+ import pytest
+ @pytest.mark.parametrize('arg', [1,2])
+ def test_function(arg):
+ pass
+ """
+ )
+ assert items[0] != items[1]
+ assert not (items[0] == items[1])
+
+ def test_pyfunc_call(self, pytester: Pytester) -> None:
+ item = pytester.getitem("def test_func(): raise ValueError")
+ config = item.config
+
+ class MyPlugin1:
+ def pytest_pyfunc_call(self):
+ raise ValueError
+
+ class MyPlugin2:
+ def pytest_pyfunc_call(self):
+ return True
+
+ config.pluginmanager.register(MyPlugin1())
+ config.pluginmanager.register(MyPlugin2())
+ config.hook.pytest_runtest_setup(item=item)
+ config.hook.pytest_pyfunc_call(pyfuncitem=item)
+
+ def test_multiple_parametrize(self, pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol(
+ """
+ import pytest
+ @pytest.mark.parametrize('x', [0, 1])
+ @pytest.mark.parametrize('y', [2, 3])
+ def test1(x, y):
+ pass
+ """
+ )
+ colitems = modcol.collect()
+ assert colitems[0].name == "test1[2-0]"
+ assert colitems[1].name == "test1[2-1]"
+ assert colitems[2].name == "test1[3-0]"
+ assert colitems[3].name == "test1[3-1]"
+
+ def test_issue751_multiple_parametrize_with_ids(self, pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol(
+ """
+ import pytest
+ @pytest.mark.parametrize('x', [0], ids=['c'])
+ @pytest.mark.parametrize('y', [0, 1], ids=['a', 'b'])
+ class Test(object):
+ def test1(self, x, y):
+ pass
+ def test2(self, x, y):
+ pass
+ """
+ )
+ colitems = modcol.collect()[0].collect()
+ assert colitems[0].name == "test1[a-c]"
+ assert colitems[1].name == "test1[b-c]"
+ assert colitems[2].name == "test2[a-c]"
+ assert colitems[3].name == "test2[b-c]"
+
+ def test_parametrize_skipif(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ m = pytest.mark.skipif('True')
+
+ @pytest.mark.parametrize('x', [0, 1, pytest.param(2, marks=m)])
+ def test_skip_if(x):
+ assert x < 2
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 2 passed, 1 skipped in *"])
+
+ def test_parametrize_skip(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ m = pytest.mark.skip('')
+
+ @pytest.mark.parametrize('x', [0, 1, pytest.param(2, marks=m)])
+ def test_skip(x):
+ assert x < 2
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 2 passed, 1 skipped in *"])
+
+ def test_parametrize_skipif_no_skip(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ m = pytest.mark.skipif('False')
+
+ @pytest.mark.parametrize('x', [0, 1, m(2)])
+ def test_skipif_no_skip(x):
+ assert x < 2
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 1 failed, 2 passed in *"])
+
+ def test_parametrize_xfail(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ m = pytest.mark.xfail('True')
+
+ @pytest.mark.parametrize('x', [0, 1, pytest.param(2, marks=m)])
+ def test_xfail(x):
+ assert x < 2
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 2 passed, 1 xfailed in *"])
+
+ def test_parametrize_passed(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ m = pytest.mark.xfail('True')
+
+ @pytest.mark.parametrize('x', [0, 1, pytest.param(2, marks=m)])
+ def test_xfail(x):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 2 passed, 1 xpassed in *"])
+
+ def test_parametrize_xfail_passed(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ m = pytest.mark.xfail('False')
+
+ @pytest.mark.parametrize('x', [0, 1, m(2)])
+ def test_passed(x):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 3 passed in *"])
+
+ def test_function_originalname(self, pytester: Pytester) -> None:
+ items = pytester.getitems(
+ """
+ import pytest
+
+ @pytest.mark.parametrize('arg', [1,2])
+ def test_func(arg):
+ pass
+
+ def test_no_param():
+ pass
+ """
+ )
+ originalnames = []
+ for x in items:
+ assert isinstance(x, pytest.Function)
+ originalnames.append(x.originalname)
+ assert originalnames == [
+ "test_func",
+ "test_func",
+ "test_no_param",
+ ]
+
+ def test_function_with_square_brackets(self, pytester: Pytester) -> None:
+ """Check that functions with square brackets don't cause trouble."""
+ p1 = pytester.makepyfile(
+ """
+ locals()["test_foo[name]"] = lambda: None
+ """
+ )
+ result = pytester.runpytest("-v", str(p1))
+ result.stdout.fnmatch_lines(
+ [
+ "test_function_with_square_brackets.py::test_foo[[]name[]] PASSED *",
+ "*= 1 passed in *",
+ ]
+ )
+
+
+class TestSorting:
+ def test_check_equality(self, pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol(
+ """
+ def test_pass(): pass
+ def test_fail(): assert 0
+ """
+ )
+ fn1 = pytester.collect_by_name(modcol, "test_pass")
+ assert isinstance(fn1, pytest.Function)
+ fn2 = pytester.collect_by_name(modcol, "test_pass")
+ assert isinstance(fn2, pytest.Function)
+
+ assert fn1 == fn2
+ assert fn1 != modcol
+ assert hash(fn1) == hash(fn2)
+
+ fn3 = pytester.collect_by_name(modcol, "test_fail")
+ assert isinstance(fn3, pytest.Function)
+ assert not (fn1 == fn3)
+ assert fn1 != fn3
+
+ for fn in fn1, fn2, fn3:
+ assert fn != 3 # type: ignore[comparison-overlap]
+ assert fn != modcol
+ assert fn != [1, 2, 3] # type: ignore[comparison-overlap]
+ assert [1, 2, 3] != fn # type: ignore[comparison-overlap]
+ assert modcol != fn
+
+ def test_allow_sane_sorting_for_decorators(self, pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol(
+ """
+ def dec(f):
+ g = lambda: f(2)
+ g.place_as = f
+ return g
+
+
+ def test_b(y):
+ pass
+ test_b = dec(test_b)
+
+ def test_a(y):
+ pass
+ test_a = dec(test_a)
+ """
+ )
+ colitems = modcol.collect()
+ assert len(colitems) == 2
+ assert [item.name for item in colitems] == ["test_b", "test_a"]
+
+ def test_ordered_by_definition_order(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """\
+ class Test1:
+ def test_foo(): pass
+ def test_bar(): pass
+ class Test2:
+ def test_foo(): pass
+ test_bar = Test1.test_bar
+ class Test3(Test2):
+ def test_baz(): pass
+ """
+ )
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(
+ [
+ "*Class Test1*",
+ "*Function test_foo*",
+ "*Function test_bar*",
+ "*Class Test2*",
+ # previously the order was flipped due to Test1.test_bar reference
+ "*Function test_foo*",
+ "*Function test_bar*",
+ "*Class Test3*",
+ "*Function test_foo*",
+ "*Function test_bar*",
+ "*Function test_baz*",
+ ]
+ )
+
+
+class TestConftestCustomization:
+ def test_pytest_pycollect_module(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ class MyModule(pytest.Module):
+ pass
+ def pytest_pycollect_makemodule(module_path, parent):
+ if module_path.name == "test_xyz.py":
+ return MyModule.from_parent(path=module_path, parent=parent)
+ """
+ )
+ pytester.makepyfile("def test_some(): pass")
+ pytester.makepyfile(test_xyz="def test_func(): pass")
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(["*<Module*test_pytest*", "*<MyModule*xyz*"])
+
+ def test_customized_pymakemodule_issue205_subdir(self, pytester: Pytester) -> None:
+ b = pytester.path.joinpath("a", "b")
+ b.mkdir(parents=True)
+ b.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ @pytest.hookimpl(hookwrapper=True)
+ def pytest_pycollect_makemodule():
+ outcome = yield
+ mod = outcome.get_result()
+ mod.obj.hello = "world"
+ """
+ )
+ )
+ b.joinpath("test_module.py").write_text(
+ textwrap.dedent(
+ """\
+ def test_hello():
+ assert hello == "world"
+ """
+ )
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_customized_pymakeitem(self, pytester: Pytester) -> None:
+ b = pytester.path.joinpath("a", "b")
+ b.mkdir(parents=True)
+ b.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ @pytest.hookimpl(hookwrapper=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"
+ """
+ )
+ )
+ b.joinpath("test_module.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ @pytest.fixture()
+ def obj(request):
+ return request.node._some123
+ def test_hello(obj):
+ assert obj == "world"
+ """
+ )
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_pytest_pycollect_makeitem(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ class MyFunction(pytest.Function):
+ pass
+ def pytest_pycollect_makeitem(collector, name, obj):
+ if name == "some":
+ return MyFunction.from_parent(name=name, parent=collector)
+ """
+ )
+ pytester.makepyfile("def some(): pass")
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(["*MyFunction*some*"])
+
+ 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
+ # Python source code whose file extension is ".narf".
+ pytester.makeconftest(
+ """
+ import sys, os, imp
+ 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())
+
+ def pytest_collect_file(file_path, parent):
+ if file_path.suffix == ".narf":
+ return Module.from_parent(path=file_path, parent=parent)"""
+ )
+ pytester.makefile(
+ ".narf",
+ """\
+ def test_something():
+ assert 1 + 1 == 2""",
+ )
+ # Use runpytest_subprocess, since we're futzing with sys.meta_path.
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_early_ignored_attributes(self, pytester: Pytester) -> None:
+ """Builtin attributes should be ignored early on, even if
+ configuration would otherwise allow them.
+
+ This tests a performance optimization, not correctness, really,
+ although it tests PytestCollectionWarning is not raised, while
+ it would have been raised otherwise.
+ """
+ pytester.makeini(
+ """
+ [pytest]
+ python_classes=*
+ python_functions=*
+ """
+ )
+ pytester.makepyfile(
+ """
+ class TestEmpty:
+ pass
+ test_empty = TestEmpty()
+ def test_real():
+ pass
+ """
+ )
+ items, rec = pytester.inline_genitems()
+ assert rec.ret == 0
+ assert len(items) == 1
+
+
+def test_setup_only_available_in_subdir(pytester: Pytester) -> None:
+ sub1 = pytester.mkpydir("sub1")
+ sub2 = pytester.mkpydir("sub2")
+ sub1.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ def pytest_runtest_setup(item):
+ assert item.path.stem == "test_in_sub1"
+ def pytest_runtest_call(item):
+ assert item.path.stem == "test_in_sub1"
+ def pytest_runtest_teardown(item):
+ assert item.path.stem == "test_in_sub1"
+ """
+ )
+ )
+ sub2.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ def pytest_runtest_setup(item):
+ assert item.path.stem == "test_in_sub2"
+ def pytest_runtest_call(item):
+ assert item.path.stem == "test_in_sub2"
+ def pytest_runtest_teardown(item):
+ assert item.path.stem == "test_in_sub2"
+ """
+ )
+ )
+ 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("-v", "-s")
+ result.assert_outcomes(passed=2)
+
+
+def test_modulecol_roundtrip(pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol("pass", withinit=False)
+ trail = modcol.nodeid
+ newcol = modcol.session.perform_collect([trail], genitems=0)[0]
+ assert modcol.name == newcol.name
+
+
+class TestTracebackCutting:
+ def test_skip_simple(self):
+ 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[-2].frame.code.name == "test_skip_simple"
+ assert not excinfo.traceback[-2].ishidden()
+
+ def test_traceback_argsetup(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture
+ def hello(request):
+ raise ValueError("xyz")
+ """
+ )
+ p = pytester.makepyfile("def test(hello): pass")
+ result = pytester.runpytest(p)
+ assert result.ret != 0
+ out = result.stdout.str()
+ assert "xyz" in out
+ assert "conftest.py:5: ValueError" in out
+ numentries = out.count("_ _ _") # separator for traceback entries
+ assert numentries == 0
+
+ result = pytester.runpytest("--fulltrace", p)
+ out = result.stdout.str()
+ assert "conftest.py:5: ValueError" in out
+ numentries = out.count("_ _ _ _") # separator for traceback entries
+ assert numentries > 3
+
+ def test_traceback_error_during_import(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ x = 1
+ x = 2
+ x = 17
+ asd
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret != 0
+ out = result.stdout.str()
+ assert "x = 1" not in out
+ assert "x = 2" not in out
+ result.stdout.fnmatch_lines([" *asd*", "E*NameError*"])
+ result = pytester.runpytest("--fulltrace")
+ out = result.stdout.str()
+ assert "x = 1" in out
+ assert "x = 2" in out
+ result.stdout.fnmatch_lines([">*asd*", "E*NameError*"])
+
+ def test_traceback_filter_error_during_fixture_collection(
+ self, pytester: Pytester
+ ) -> None:
+ """Integration test for issue #995."""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ def fail_me(func):
+ ns = {}
+ exec('def w(): raise ValueError("fail me")', ns)
+ return ns['w']
+
+ @pytest.fixture(scope='class')
+ @fail_me
+ def fail_fixture():
+ pass
+
+ def test_failing_fixture(fail_fixture):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret != 0
+ out = result.stdout.str()
+ assert "INTERNALERROR>" not in out
+ result.stdout.fnmatch_lines(["*ValueError: fail me*", "* 1 error in *"])
+
+ def test_filter_traceback_generated_code(self) -> None:
+ """Test that filter_traceback() works with the fact that
+ _pytest._code.code.Code.path attribute might return an str object.
+
+ In this case, one of the entries on the traceback was produced by
+ dynamically generated code.
+ See: https://bitbucket.org/pytest-dev/py/issues/71
+ This fixes #995.
+ """
+ from _pytest._code import filter_traceback
+
+ tb = None
+ try:
+ ns: Dict[str, Any] = {}
+ exec("def foo(): raise ValueError", ns)
+ ns["foo"]()
+ except ValueError:
+ _, _, tb = sys.exc_info()
+
+ assert tb is not None
+ traceback = _pytest._code.Traceback(tb)
+ assert isinstance(traceback[-1].path, str)
+ assert not filter_traceback(traceback[-1])
+
+ def test_filter_traceback_path_no_longer_valid(self, pytester: Pytester) -> None:
+ """Test that filter_traceback() works with the fact that
+ _pytest._code.code.Code.path attribute might return an str object.
+
+ In this case, one of the files in the traceback no longer exists.
+ This fixes #1133.
+ """
+ from _pytest._code import filter_traceback
+
+ pytester.syspathinsert()
+ pytester.makepyfile(
+ filter_traceback_entry_as_str="""
+ def foo():
+ raise ValueError
+ """
+ )
+ tb = None
+ try:
+ import filter_traceback_entry_as_str
+
+ filter_traceback_entry_as_str.foo()
+ except ValueError:
+ _, _, tb = sys.exc_info()
+
+ assert tb is not None
+ pytester.path.joinpath("filter_traceback_entry_as_str.py").unlink()
+ traceback = _pytest._code.Traceback(tb)
+ assert isinstance(traceback[-1].path, str)
+ assert filter_traceback(traceback[-1])
+
+
+class TestReportInfo:
+ def test_itemreport_reportinfo(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ class MyFunction(pytest.Function):
+ def reportinfo(self):
+ return "ABCDE", 42, "custom"
+ def pytest_pycollect_makeitem(collector, name, obj):
+ if name == "test_func":
+ return MyFunction.from_parent(name=name, parent=collector)
+ """
+ )
+ item = pytester.getitem("def test_func(): pass")
+ item.config.pluginmanager.getplugin("runner")
+ assert item.location == ("ABCDE", 42, "custom")
+
+ def test_func_reportinfo(self, pytester: Pytester) -> None:
+ item = pytester.getitem("def test_func(): pass")
+ path, lineno, modpath = item.reportinfo()
+ assert os.fspath(path) == str(item.path)
+ assert lineno == 0
+ assert modpath == "test_func"
+
+ def test_class_reportinfo(self, pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol(
+ """
+ # lineno 0
+ class TestClass(object):
+ def test_hello(self): pass
+ """
+ )
+ classcol = pytester.collect_by_name(modcol, "TestClass")
+ assert isinstance(classcol, Class)
+ path, lineno, msg = classcol.reportinfo()
+ assert os.fspath(path) == str(modcol.path)
+ assert lineno == 1
+ assert msg == "TestClass"
+
+ @pytest.mark.filterwarnings(
+ "ignore:usage of Generator.Function is deprecated, please use pytest.Function instead"
+ )
+ def test_reportinfo_with_nasty_getattr(self, pytester: Pytester) -> None:
+ # https://github.com/pytest-dev/pytest/issues/1204
+ modcol = pytester.getmodulecol(
+ """
+ # lineno 0
+ class TestClass:
+ def __getattr__(self, name):
+ return "this is not an int"
+
+ def __class_getattr__(cls, name):
+ return "this is not an int"
+
+ def intest_foo(self):
+ pass
+
+ def test_bar(self):
+ pass
+ """
+ )
+ classcol = pytester.collect_by_name(modcol, "TestClass")
+ assert isinstance(classcol, Class)
+ path, lineno, msg = classcol.reportinfo()
+ func = list(classcol.collect())[0]
+ assert isinstance(func, Function)
+ path, lineno, msg = func.reportinfo()
+
+
+def test_customized_python_discovery(pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ python_files=check_*.py
+ python_classes=Check
+ python_functions=check
+ """
+ )
+ p = pytester.makepyfile(
+ """
+ def check_simple():
+ pass
+ class CheckMyApp(object):
+ def check_meth(self):
+ pass
+ """
+ )
+ p2 = p.with_name(p.name.replace("test", "check"))
+ p.rename(p2)
+ result = pytester.runpytest("--collect-only", "-s")
+ result.stdout.fnmatch_lines(
+ ["*check_customized*", "*check_simple*", "*CheckMyApp*", "*check_meth*"]
+ )
+
+ result = pytester.runpytest()
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+
+def test_customized_python_discovery_functions(pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ python_functions=_test
+ """
+ )
+ pytester.makepyfile(
+ """
+ def _test_underscore():
+ pass
+ """
+ )
+ result = pytester.runpytest("--collect-only", "-s")
+ result.stdout.fnmatch_lines(["*_test_underscore*"])
+
+ result = pytester.runpytest()
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+def test_unorderable_types(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ class TestJoinEmpty(object):
+ pass
+
+ def make_test():
+ class Test(object):
+ pass
+ Test.__name__ = "TestFoo"
+ return Test
+ TestFoo = make_test()
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.no_fnmatch_line("*TypeError*")
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+
+@pytest.mark.filterwarnings("default::pytest.PytestCollectionWarning")
+def test_dont_collect_non_function_callable(pytester: Pytester) -> None:
+ """Test for issue https://github.com/pytest-dev/pytest/issues/331
+
+ In this case an INTERNALERROR occurred trying to report the failure of
+ a test like this one because pytest failed to get the source lines.
+ """
+ pytester.makepyfile(
+ """
+ class Oh(object):
+ def __call__(self):
+ pass
+
+ test_a = Oh()
+
+ def test_real():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*collected 1 item*",
+ "*test_dont_collect_non_function_callable.py:2: *cannot collect 'test_a' because it is not a function*",
+ "*1 passed, 1 warning in *",
+ ]
+ )
+
+
+def test_class_injection_does_not_break_collection(pytester: Pytester) -> None:
+ """Tests whether injection during collection time will terminate testing.
+
+ In this case the error should not occur if the TestClass itself
+ is modified during collection time, and the original method list
+ is still used for collection.
+ """
+ pytester.makeconftest(
+ """
+ from test_inject import TestClass
+ def pytest_generate_tests(metafunc):
+ TestClass.changed_var = {}
+ """
+ )
+ pytester.makepyfile(
+ test_inject='''
+ class TestClass(object):
+ def test_injection(self):
+ """Test being parametrized."""
+ pass
+ '''
+ )
+ result = pytester.runpytest()
+ assert (
+ "RuntimeError: dictionary changed size during iteration"
+ not in result.stdout.str()
+ )
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+def test_syntax_error_with_non_ascii_chars(pytester: Pytester) -> None:
+ """Fix decoding issue while formatting SyntaxErrors during collection (#578)."""
+ pytester.makepyfile("☃")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*ERROR collecting*", "*SyntaxError*", "*1 error in*"])
+
+
+def test_collect_error_with_fulltrace(pytester: Pytester) -> None:
+ pytester.makepyfile("assert 0")
+ result = pytester.runpytest("--fulltrace")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 0 items / 1 error",
+ "",
+ "*= ERRORS =*",
+ "*_ ERROR collecting test_collect_error_with_fulltrace.py _*",
+ "",
+ "> assert 0",
+ "E assert 0",
+ "",
+ "test_collect_error_with_fulltrace.py:1: AssertionError",
+ "*! Interrupted: 1 error during collection !*",
+ ]
+ )
+
+
+def test_skip_duplicates_by_default(pytester: Pytester) -> None:
+ """Test for issue https://github.com/pytest-dev/pytest/issues/1609 (#1609)
+
+ Ignore duplicate directories.
+ """
+ a = pytester.mkdir("a")
+ fh = a.joinpath("test_a.py")
+ fh.write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ def test_real():
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest(str(a), str(a))
+ result.stdout.fnmatch_lines(["*collected 1 item*"])
+
+
+def test_keep_duplicates(pytester: Pytester) -> None:
+ """Test for issue https://github.com/pytest-dev/pytest/issues/1609 (#1609)
+
+ Use --keep-duplicates to collect tests from duplicate directories.
+ """
+ a = pytester.mkdir("a")
+ fh = a.joinpath("test_a.py")
+ fh.write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ def test_real():
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("--keep-duplicates", str(a), str(a))
+ result.stdout.fnmatch_lines(["*collected 2 item*"])
+
+
+def test_package_collection_infinite_recursion(pytester: Pytester) -> None:
+ pytester.copy_example("collect/package_infinite_recursion")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+def test_package_collection_init_given_as_argument(pytester: Pytester) -> None:
+ """Regression test for #3749"""
+ p = pytester.copy_example("collect/package_init_given_as_arg")
+ result = pytester.runpytest(p / "pkg" / "__init__.py")
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+def test_package_with_modules(pytester: Pytester) -> None:
+ """
+ .
+ └── root
+ ├── __init__.py
+ ├── sub1
+ │ ├── __init__.py
+ │ └── sub1_1
+ │ ├── __init__.py
+ │ └── test_in_sub1.py
+ └── sub2
+ └── test
+ └── test_in_sub2.py
+
+ """
+ root = pytester.mkpydir("root")
+ sub1 = root.joinpath("sub1")
+ sub1_test = sub1.joinpath("sub1_1")
+ sub1_test.mkdir(parents=True)
+ for d in (sub1, sub1_test):
+ d.joinpath("__init__.py").touch()
+
+ sub2 = root.joinpath("sub2")
+ 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")
+
+ # Execute from .
+ result = pytester.runpytest("-v", "-s")
+ result.assert_outcomes(passed=2)
+
+ # Execute from . with one argument "root"
+ result = pytester.runpytest("-v", "-s", "root")
+ result.assert_outcomes(passed=2)
+
+ # Chdir into package's root and execute with no args
+ os.chdir(root)
+ result = pytester.runpytest("-v", "-s")
+ result.assert_outcomes(passed=2)
+
+
+def test_package_ordering(pytester: Pytester) -> None:
+ """
+ .
+ └── root
+ ├── Test_root.py
+ ├── __init__.py
+ ├── sub1
+ │ ├── Test_sub1.py
+ │ └── __init__.py
+ └── sub2
+ └── test
+ └── test_sub2.py
+
+ """
+ pytester.makeini(
+ """
+ [pytest]
+ python_files=*.py
+ """
+ )
+ root = pytester.mkpydir("root")
+ sub1 = root.joinpath("sub1")
+ sub1.mkdir()
+ sub1.joinpath("__init__.py").touch()
+ sub2 = root.joinpath("sub2")
+ 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")
+
+ # Execute from .
+ result = pytester.runpytest("-v", "-s")
+ result.assert_outcomes(passed=3)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/python/fixtures.py b/testing/web-platform/tests/tools/third_party/pytest/testing/python/fixtures.py
new file mode 100644
index 0000000000..f29ca1dfa5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/python/fixtures.py
@@ -0,0 +1,4474 @@
+import os
+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.monkeypatch import MonkeyPatch
+from _pytest.pytester import get_public_names
+from _pytest.pytester import Pytester
+from _pytest.python import Function
+
+
+def test_getfuncargnames_functions():
+ """Test getfuncargnames for normal functions"""
+
+ def f():
+ raise NotImplementedError()
+
+ assert not getfuncargnames(f)
+
+ def g(arg):
+ raise NotImplementedError()
+
+ assert getfuncargnames(g) == ("arg",)
+
+ def h(arg1, arg2="hello"):
+ raise NotImplementedError()
+
+ assert getfuncargnames(h) == ("arg1",)
+
+ def j(arg1, arg2, arg3="hello"):
+ raise NotImplementedError()
+
+ assert getfuncargnames(j) == ("arg1", "arg2")
+
+
+def test_getfuncargnames_methods():
+ """Test getfuncargnames for normal methods"""
+
+ class A:
+ def f(self, arg1, arg2="hello"):
+ raise NotImplementedError()
+
+ assert getfuncargnames(A().f) == ("arg1",)
+
+
+def test_getfuncargnames_staticmethod():
+ """Test getfuncargnames for staticmethods"""
+
+ class A:
+ @staticmethod
+ def static(arg1, arg2, x=1):
+ raise NotImplementedError()
+
+ assert getfuncargnames(A.static, cls=A) == ("arg1", "arg2")
+
+
+def test_getfuncargnames_staticmethod_inherited() -> None:
+ """Test getfuncargnames for inherited staticmethods (#8061)"""
+
+ class A:
+ @staticmethod
+ def static(arg1, arg2, x=1):
+ raise NotImplementedError()
+
+ class B(A):
+ pass
+
+ assert getfuncargnames(B.static, cls=B) == ("arg1", "arg2")
+
+
+def test_getfuncargnames_partial():
+ """Check getfuncargnames for methods defined with functools.partial (#5701)"""
+ import functools
+
+ def check(arg1, arg2, i):
+ raise NotImplementedError()
+
+ class T:
+ test_ok = functools.partial(check, i=2)
+
+ values = getfuncargnames(T().test_ok, name="test_ok")
+ assert values == ("arg1", "arg2")
+
+
+def test_getfuncargnames_staticmethod_partial():
+ """Check getfuncargnames for staticmethods defined with functools.partial (#5701)"""
+ import functools
+
+ def check(arg1, arg2, i):
+ raise NotImplementedError()
+
+ class T:
+ test_ok = staticmethod(functools.partial(check, i=2))
+
+ values = getfuncargnames(T().test_ok, name="test_ok")
+ assert values == ("arg1", "arg2")
+
+
+@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")
+ assert result.ret != 0
+ result.stdout.fnmatch_lines(
+ """
+ *def test_func(some)*
+ *fixture*some*not found*
+ *xyzsomething*
+ """
+ )
+
+ def test_detect_recursive_dependency_error(self, pytester: Pytester) -> None:
+ pytester.copy_example()
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ ["*recursive dependency involving fixture 'fix1' detected*"]
+ )
+
+ def test_funcarg_basic(self, pytester: Pytester) -> None:
+ pytester.copy_example()
+ item = pytester.getitem(Path("test_funcarg_basic.py"))
+ assert isinstance(item, Function)
+ # Execute's item's setup, which fills fixtures.
+ item.session._setupstate.setup(item)
+ del item.funcargs["request"]
+ assert len(get_public_names(item.funcargs)) == 2
+ assert item.funcargs["some"] == "test_func"
+ assert item.funcargs["other"] == 42
+
+ def test_funcarg_lookup_modulelevel(self, pytester: Pytester) -> None:
+ pytester.copy_example()
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ def test_funcarg_lookup_classlevel(self, pytester: Pytester) -> None:
+ p = pytester.copy_example()
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_conftest_funcargs_only_available_in_subdir(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.copy_example()
+ result = pytester.runpytest("-v")
+ result.assert_outcomes(passed=2)
+
+ def test_extend_fixture_module_class(self, pytester: Pytester) -> None:
+ testfile = pytester.copy_example()
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+ result = pytester.runpytest(testfile)
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_extend_fixture_conftest_module(self, pytester: Pytester) -> None:
+ p = pytester.copy_example()
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+ result = pytester.runpytest(str(next(Path(str(p)).rglob("test_*.py"))))
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_extend_fixture_conftest_conftest(self, pytester: Pytester) -> None:
+ p = pytester.copy_example()
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+ result = pytester.runpytest(str(next(Path(str(p)).rglob("test_*.py"))))
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_extend_fixture_conftest_plugin(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ testplugin="""
+ import pytest
+
+ @pytest.fixture
+ def foo():
+ return 7
+ """
+ )
+ pytester.syspathinsert()
+ pytester.makeconftest(
+ """
+ import pytest
+
+ pytest_plugins = 'testplugin'
+
+ @pytest.fixture
+ def foo(foo):
+ return foo + 7
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_foo(foo):
+ assert foo == 14
+ """
+ )
+ result = pytester.runpytest("-s")
+ assert result.ret == 0
+
+ def test_extend_fixture_plugin_plugin(self, pytester: Pytester) -> None:
+ # Two plugins should extend each order in loading order
+ pytester.makepyfile(
+ testplugin0="""
+ import pytest
+
+ @pytest.fixture
+ def foo():
+ return 7
+ """
+ )
+ pytester.makepyfile(
+ testplugin1="""
+ import pytest
+
+ @pytest.fixture
+ def foo(foo):
+ return foo + 7
+ """
+ )
+ pytester.syspathinsert()
+ pytester.makepyfile(
+ """
+ pytest_plugins = ['testplugin0', 'testplugin1']
+
+ def test_foo(foo):
+ assert foo == 14
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+
+ def test_override_parametrized_fixture_conftest_module(
+ self, pytester: Pytester
+ ) -> None:
+ """Test override of the parametrized fixture with non-parametrized one on the test module level."""
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(params=[1, 2, 3])
+ def spam(request):
+ return request.param
+ """
+ )
+ testfile = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def spam():
+ return 'spam'
+
+ def test_spam(spam):
+ assert spam == 'spam'
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+ result = pytester.runpytest(testfile)
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_override_parametrized_fixture_conftest_conftest(
+ self, pytester: Pytester
+ ) -> None:
+ """Test override of the parametrized fixture with non-parametrized one on the conftest level."""
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(params=[1, 2, 3])
+ def spam(request):
+ return request.param
+ """
+ )
+ subdir = pytester.mkpydir("subdir")
+ subdir.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ @pytest.fixture
+ def spam():
+ return 'spam'
+ """
+ )
+ )
+ testfile = subdir.joinpath("test_spam.py")
+ testfile.write_text(
+ textwrap.dedent(
+ """\
+ def test_spam(spam):
+ assert spam == "spam"
+ """
+ )
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+ result = pytester.runpytest(testfile)
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_override_non_parametrized_fixture_conftest_module(
+ self, pytester: Pytester
+ ) -> None:
+ """Test override of the non-parametrized fixture with parametrized one on the test module level."""
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture
+ def spam():
+ return 'spam'
+ """
+ )
+ testfile = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(params=[1, 2, 3])
+ def spam(request):
+ return request.param
+
+ params = {'spam': 1}
+
+ def test_spam(spam):
+ assert spam == params['spam']
+ params['spam'] += 1
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*3 passed*"])
+ result = pytester.runpytest(testfile)
+ result.stdout.fnmatch_lines(["*3 passed*"])
+
+ def test_override_non_parametrized_fixture_conftest_conftest(
+ self, pytester: Pytester
+ ) -> None:
+ """Test override of the non-parametrized fixture with parametrized one on the conftest level."""
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture
+ def spam():
+ return 'spam'
+ """
+ )
+ subdir = pytester.mkpydir("subdir")
+ subdir.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ @pytest.fixture(params=[1, 2, 3])
+ def spam(request):
+ return request.param
+ """
+ )
+ )
+ testfile = subdir.joinpath("test_spam.py")
+ testfile.write_text(
+ textwrap.dedent(
+ """\
+ params = {'spam': 1}
+
+ def test_spam(spam):
+ assert spam == params['spam']
+ params['spam'] += 1
+ """
+ )
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*3 passed*"])
+ result = pytester.runpytest(testfile)
+ result.stdout.fnmatch_lines(["*3 passed*"])
+
+ def test_override_autouse_fixture_with_parametrized_fixture_conftest_conftest(
+ self, pytester: Pytester
+ ) -> None:
+ """Test override of the autouse fixture with parametrized one on the conftest level.
+ This test covers the issue explained in issue 1601
+ """
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(autouse=True)
+ def spam():
+ return 'spam'
+ """
+ )
+ subdir = pytester.mkpydir("subdir")
+ subdir.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ @pytest.fixture(params=[1, 2, 3])
+ def spam(request):
+ return request.param
+ """
+ )
+ )
+ testfile = subdir.joinpath("test_spam.py")
+ testfile.write_text(
+ textwrap.dedent(
+ """\
+ params = {'spam': 1}
+
+ def test_spam(spam):
+ assert spam == params['spam']
+ params['spam'] += 1
+ """
+ )
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*3 passed*"])
+ result = pytester.runpytest(testfile)
+ result.stdout.fnmatch_lines(["*3 passed*"])
+
+ def test_override_fixture_reusing_super_fixture_parametrization(
+ self, pytester: Pytester
+ ) -> None:
+ """Override a fixture at a lower level, reusing the higher-level fixture that
+ is parametrized (#1953).
+ """
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(params=[1, 2])
+ def foo(request):
+ return request.param
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def foo(foo):
+ return foo * 2
+
+ def test_spam(foo):
+ assert foo in (2, 4)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+ def test_override_parametrize_fixture_and_indirect(
+ self, pytester: Pytester
+ ) -> None:
+ """Override a fixture at a lower level, reusing the higher-level fixture that
+ is parametrized, while also using indirect parametrization.
+ """
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(params=[1, 2])
+ def foo(request):
+ return request.param
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def foo(foo):
+ return foo * 2
+
+ @pytest.fixture
+ def bar(request):
+ return request.param * 100
+
+ @pytest.mark.parametrize("bar", [42], indirect=True)
+ def test_spam(bar, foo):
+ assert bar == 4200
+ assert foo in (2, 4)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+ def test_override_top_level_fixture_reusing_super_fixture_parametrization(
+ self, pytester: Pytester
+ ) -> None:
+ """Same as the above test, but with another level of overwriting."""
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(params=['unused', 'unused'])
+ def foo(request):
+ return request.param
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(params=[1, 2])
+ def foo(request):
+ return request.param
+
+ class Test:
+
+ @pytest.fixture
+ def foo(self, foo):
+ return foo * 2
+
+ def test_spam(self, foo):
+ assert foo in (2, 4)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+ def test_override_parametrized_fixture_with_new_parametrized_fixture(
+ self, pytester: Pytester
+ ) -> None:
+ """Overriding a parametrized fixture, while also parametrizing the new fixture and
+ simultaneously requesting the overwritten fixture as parameter, yields the same value
+ as ``request.param``.
+ """
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(params=['ignored', 'ignored'])
+ def foo(request):
+ return request.param
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(params=[10, 20])
+ def foo(foo, request):
+ assert request.param == foo
+ return foo * 2
+
+ def test_spam(foo):
+ assert foo in (20, 40)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+ def test_autouse_fixture_plugin(self, pytester: Pytester) -> None:
+ # A fixture from a plugin has no baseid set, which screwed up
+ # the autouse fixture handling.
+ pytester.makepyfile(
+ testplugin="""
+ import pytest
+
+ @pytest.fixture(autouse=True)
+ def foo(request):
+ request.function.foo = 7
+ """
+ )
+ pytester.syspathinsert()
+ pytester.makepyfile(
+ """
+ pytest_plugins = 'testplugin'
+
+ def test_foo(request):
+ assert request.function.foo == 7
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+
+ def test_funcarg_lookup_error(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture
+ def a_fixture(): pass
+
+ @pytest.fixture
+ def b_fixture(): pass
+
+ @pytest.fixture
+ def c_fixture(): pass
+
+ @pytest.fixture
+ def d_fixture(): pass
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_lookup_error(unknown):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*ERROR at setup of test_lookup_error*",
+ " def test_lookup_error(unknown):*",
+ "E fixture 'unknown' not found",
+ "> available fixtures:*a_fixture,*b_fixture,*c_fixture,*d_fixture*monkeypatch,*",
+ # sorted
+ "> use 'py*test --fixtures *' for help on them.",
+ "*1 error*",
+ ]
+ )
+ result.stdout.no_fnmatch_line("*INTERNAL*")
+
+ def test_fixture_excinfo_leak(self, pytester: Pytester) -> None:
+ # on python2 sys.excinfo would leak into fixture executions
+ pytester.makepyfile(
+ """
+ import sys
+ import traceback
+ import pytest
+
+ @pytest.fixture
+ def leak():
+ if sys.exc_info()[0]: # python3 bug :)
+ traceback.print_exc()
+ #fails
+ assert sys.exc_info() == (None, None, None)
+
+ def test_leak(leak):
+ if sys.exc_info()[0]: # python3 bug :)
+ traceback.print_exc()
+ assert sys.exc_info() == (None, None, None)
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+
+
+class TestRequestBasic:
+ def test_request_attributes(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+
+ @pytest.fixture
+ def something(request): pass
+ def test_func(something): pass
+ """
+ )
+ assert isinstance(item, Function)
+ req = fixtures.FixtureRequest(item, _ispytest=True)
+ assert req.function == item.obj
+ assert req.keywords == item.keywords
+ assert hasattr(req.module, "test_func")
+ assert req.cls is None
+ assert req.function.__name__ == "test_func"
+ assert req.config == item.config
+ assert repr(req).find(req.function.__name__) != -1
+
+ def test_request_attributes_method(self, pytester: Pytester) -> None:
+ (item,) = pytester.getitems(
+ """
+ import pytest
+ class TestB(object):
+
+ @pytest.fixture
+ def something(self, request):
+ return 1
+ def test_func(self, something):
+ pass
+ """
+ )
+ assert isinstance(item, Function)
+ req = item._request
+ assert req.cls.__name__ == "TestB"
+ assert req.instance.__class__ == req.cls
+
+ def test_request_contains_funcarg_arg2fixturedefs(self, pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol(
+ """
+ import pytest
+ @pytest.fixture
+ def something(request):
+ pass
+ class TestClass(object):
+ def test_method(self, something):
+ pass
+ """
+ )
+ (item1,) = pytester.genitems([modcol])
+ assert item1.name == "test_method"
+ arg2fixturedefs = fixtures.FixtureRequest(
+ item1, _ispytest=True
+ )._arg2fixturedefs
+ assert len(arg2fixturedefs) == 1
+ assert arg2fixturedefs["something"][0].argname == "something"
+
+ @pytest.mark.skipif(
+ hasattr(sys, "pypy_version_info"),
+ reason="this method of test doesn't work on pypy",
+ )
+ def test_request_garbage(self, pytester: Pytester) -> None:
+ try:
+ import xdist # noqa
+ except ImportError:
+ pass
+ else:
+ pytest.xfail("this test is flaky when executed with xdist")
+ pytester.makepyfile(
+ """
+ import sys
+ import pytest
+ from _pytest.fixtures import PseudoFixtureDef
+ import gc
+
+ @pytest.fixture(autouse=True)
+ def something(request):
+ original = gc.get_debug()
+ gc.set_debug(gc.DEBUG_SAVEALL)
+ gc.collect()
+
+ yield
+
+ try:
+ gc.collect()
+ leaked = [x for _ in gc.garbage if isinstance(_, PseudoFixtureDef)]
+ assert leaked == []
+ finally:
+ gc.set_debug(original)
+
+ def test_func():
+ pass
+ """
+ )
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(["* 1 passed in *"])
+
+ def test_getfixturevalue_recursive(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture
+ def something(request):
+ return 1
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def something(request):
+ return request.getfixturevalue("something") + 1
+ def test_func(something):
+ assert something == 2
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_getfixturevalue_teardown(self, pytester: Pytester) -> None:
+ """
+ Issue #1895
+
+ `test_inner` requests `inner` fixture, which in turn requests `resource`
+ using `getfixturevalue`. `test_func` then requests `resource`.
+
+ `resource` is teardown before `inner` because the fixture mechanism won't consider
+ `inner` dependent on `resource` when it is used via `getfixturevalue`: `test_func`
+ will then cause the `resource`'s finalizer to be called first because of this.
+ """
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope='session')
+ def resource():
+ r = ['value']
+ yield r
+ r.pop()
+
+ @pytest.fixture(scope='session')
+ def inner(request):
+ resource = request.getfixturevalue('resource')
+ assert resource == ['value']
+ yield
+ assert resource == ['value']
+
+ def test_inner(inner):
+ pass
+
+ def test_func(resource):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 2 passed in *"])
+
+ def test_getfixturevalue(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+
+ @pytest.fixture
+ def something(request):
+ return 1
+
+ values = [2]
+ @pytest.fixture
+ def other(request):
+ return values.pop()
+
+ def test_func(something): pass
+ """
+ )
+ assert isinstance(item, Function)
+ req = item._request
+
+ # Execute item's setup.
+ item.session._setupstate.setup(item)
+
+ with pytest.raises(pytest.FixtureLookupError):
+ req.getfixturevalue("notexists")
+ val = req.getfixturevalue("something")
+ assert val == 1
+ val = req.getfixturevalue("something")
+ assert val == 1
+ val2 = req.getfixturevalue("other")
+ assert val2 == 2
+ val2 = req.getfixturevalue("other") # see about caching
+ assert val2 == 2
+ assert item.funcargs["something"] == 1
+ assert len(get_public_names(item.funcargs)) == 2
+ assert "request" in item.funcargs
+
+ def test_request_addfinalizer(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ teardownlist = []
+ @pytest.fixture
+ def something(request):
+ request.addfinalizer(lambda: teardownlist.append(1))
+ def test_func(something): pass
+ """
+ )
+ assert isinstance(item, Function)
+ item.session._setupstate.setup(item)
+ item._request._fillfixtures()
+ # successively check finalization calls
+ parent = item.getparent(pytest.Module)
+ assert parent is not None
+ teardownlist = parent.obj.teardownlist
+ ss = item.session._setupstate
+ assert not teardownlist
+ ss.teardown_exact(None)
+ print(ss.stack)
+ assert teardownlist == [1]
+
+ def test_request_addfinalizer_failing_setup(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = [1]
+ @pytest.fixture
+ def myfix(request):
+ request.addfinalizer(values.pop)
+ assert 0
+ def test_fix(myfix):
+ pass
+ def test_finalizer_ran():
+ assert not values
+ """
+ )
+ reprec = pytester.inline_run("-s")
+ reprec.assertoutcome(failed=1, passed=1)
+
+ def test_request_addfinalizer_failing_setup_module(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = [1, 2]
+ @pytest.fixture(scope="module")
+ def myfix(request):
+ request.addfinalizer(values.pop)
+ request.addfinalizer(values.pop)
+ assert 0
+ def test_fix(myfix):
+ pass
+ """
+ )
+ reprec = pytester.inline_run("-s")
+ mod = reprec.getcalls("pytest_runtest_setup")[0].item.module
+ assert not mod.values
+
+ def test_request_addfinalizer_partial_setup_failure(
+ self, pytester: Pytester
+ ) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ @pytest.fixture
+ def something(request):
+ request.addfinalizer(lambda: values.append(None))
+ def test_func(something, missingarg):
+ pass
+ def test_second():
+ assert len(values) == 1
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(
+ ["*1 error*"] # XXX the whole module collection fails
+ )
+
+ def test_request_subrequest_addfinalizer_exceptions(
+ 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)
+ """
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ def _excepts(where):
+ raise Exception('Error in %s fixture' % where)
+ @pytest.fixture
+ def subrequest(request):
+ return request
+ @pytest.fixture
+ def something(subrequest):
+ subrequest.addfinalizer(lambda: values.append(1))
+ subrequest.addfinalizer(lambda: values.append(2))
+ subrequest.addfinalizer(lambda: _excepts('something'))
+ @pytest.fixture
+ def excepts(subrequest):
+ subrequest.addfinalizer(lambda: _excepts('excepts'))
+ subrequest.addfinalizer(lambda: values.append(3))
+ def test_first(something, excepts):
+ pass
+ def test_second():
+ assert values == [3, 2, 1]
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ ["*Exception: Error in excepts fixture", "* 2 passed, 1 error in *"]
+ )
+
+ 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 req.path == modcol.path
+
+ def test_request_fixturenames(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ from _pytest.pytester import get_public_names
+ @pytest.fixture()
+ def arg1():
+ pass
+ @pytest.fixture()
+ def farg(arg1):
+ pass
+ @pytest.fixture(autouse=True)
+ def sarg(tmp_path):
+ pass
+ def test_function(request, farg):
+ assert set(get_public_names(request.fixturenames)) == \
+ set(["sarg", "arg1", "request", "farg",
+ "tmp_path", "tmp_path_factory"])
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_request_fixturenames_dynamic_fixture(self, pytester: Pytester) -> None:
+ """Regression test for #3057"""
+ pytester.copy_example("fixtures/test_getfixturevalue_dynamic.py")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_setupdecorator_and_xunit(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ @pytest.fixture(scope='module', autouse=True)
+ def setup_module():
+ values.append("module")
+ @pytest.fixture(autouse=True)
+ def setup_function():
+ values.append("function")
+
+ def test_func():
+ pass
+
+ class TestClass(object):
+ @pytest.fixture(scope="class", autouse=True)
+ def setup_class(self):
+ values.append("class")
+ @pytest.fixture(autouse=True)
+ def setup_method(self):
+ values.append("method")
+ def test_method(self):
+ pass
+ def test_all():
+ assert values == ["module", "function", "class",
+ "function", "method", "function"]
+ """
+ )
+ reprec = pytester.inline_run("-v")
+ reprec.assertoutcome(passed=3)
+
+ def test_fixtures_sub_subdir_normalize_sep(self, pytester: Pytester) -> None:
+ # this tests that normalization of nodeids takes place
+ b = pytester.path.joinpath("tests", "unit")
+ b.mkdir(parents=True)
+ b.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ @pytest.fixture
+ def arg1():
+ pass
+ """
+ )
+ )
+ p = b.joinpath("test_module.py")
+ p.write_text("def test_func(arg1): pass")
+ result = pytester.runpytest(p, "--fixtures")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(
+ """
+ *fixtures defined*conftest*
+ *arg1*
+ """
+ )
+
+ def test_show_fixtures_color_yes(self, pytester: Pytester) -> None:
+ pytester.makepyfile("def test_this(): assert 1")
+ result = pytester.runpytest("--color=yes", "--fixtures")
+ assert "\x1b[32mtmp_path" in result.stdout.str()
+
+ def test_newstyle_with_request(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture()
+ def arg(request):
+ pass
+ def test_1(arg):
+ pass
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_setupcontext_no_param(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(params=[1,2])
+ def arg(request):
+ return request.param
+
+ @pytest.fixture(autouse=True)
+ def mysetup(request, arg):
+ assert not hasattr(request, "param")
+ def test_1(arg):
+ assert arg in (1,2)
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+
+class TestRequestSessionScoped:
+ @pytest.fixture(scope="session")
+ def session_request(self, request):
+ return request
+
+ @pytest.mark.parametrize("name", ["path", "module"])
+ def test_session_scoped_unavailable_attributes(self, session_request, name):
+ with pytest.raises(
+ AttributeError,
+ match=f"{name} not available in session-scoped context",
+ ):
+ getattr(session_request, name)
+
+
+class TestRequestMarking:
+ def test_applymarker(self, pytester: Pytester) -> None:
+ item1, item2 = pytester.getitems(
+ """
+ import pytest
+
+ @pytest.fixture
+ def something(request):
+ pass
+ class TestClass(object):
+ def test_func1(self, something):
+ pass
+ def test_func2(self, something):
+ pass
+ """
+ )
+ req1 = fixtures.FixtureRequest(item1, _ispytest=True)
+ assert "xfail" not in item1.keywords
+ req1.applymarker(pytest.mark.xfail)
+ assert "xfail" in item1.keywords
+ assert "skipif" not in item1.keywords
+ req1.applymarker(pytest.mark.skipif)
+ assert "skipif" in item1.keywords
+ with pytest.raises(ValueError):
+ req1.applymarker(42) # type: ignore[arg-type]
+
+ def test_accesskeywords(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture()
+ def keywords(request):
+ return request.keywords
+ @pytest.mark.XYZ
+ def test_function(keywords):
+ assert keywords["XYZ"]
+ assert "abc" not in keywords
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_accessmarker_dynamic(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ @pytest.fixture()
+ def keywords(request):
+ return request.keywords
+
+ @pytest.fixture(scope="class", autouse=True)
+ def marking(request):
+ request.applymarker(pytest.mark.XYZ("hello"))
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_fun1(keywords):
+ assert keywords["XYZ"] is not None
+ assert "abc" not in keywords
+ def test_fun2(keywords):
+ assert keywords["XYZ"] is not None
+ assert "abc" not in keywords
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+
+class TestFixtureUsages:
+ def test_noargfixturedec(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture
+ def arg1():
+ return 1
+
+ def test_func(arg1):
+ assert arg1 == 1
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_receives_funcargs(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture()
+ def arg1():
+ return 1
+
+ @pytest.fixture()
+ def arg2(arg1):
+ return arg1 + 1
+
+ def test_add(arg2):
+ assert arg2 == 2
+ def test_all(arg1, arg2):
+ assert arg1 == 1
+ assert arg2 == 2
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ def test_receives_funcargs_scope_mismatch(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope="function")
+ def arg1():
+ return 1
+
+ @pytest.fixture(scope="module")
+ def arg2(arg1):
+ return arg1 + 1
+
+ def test_add(arg2):
+ assert arg2 == 2
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*ScopeMismatch*involved factories*",
+ "test_receives_funcargs_scope_mismatch.py:6: def arg2(arg1)",
+ "test_receives_funcargs_scope_mismatch.py:2: def arg1()",
+ "*1 error*",
+ ]
+ )
+
+ def test_receives_funcargs_scope_mismatch_issue660(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope="function")
+ def arg1():
+ return 1
+
+ @pytest.fixture(scope="module")
+ def arg2(arg1):
+ return arg1 + 1
+
+ def test_add(arg1, arg2):
+ assert arg2 == 2
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ ["*ScopeMismatch*involved factories*", "* def arg2*", "*1 error*"]
+ )
+
+ def test_invalid_scope(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope="functions")
+ def badscope():
+ pass
+
+ def test_nothing(badscope):
+ pass
+ """
+ )
+ result = pytester.runpytest_inprocess()
+ result.stdout.fnmatch_lines(
+ "*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'"
+ )
+
+ @pytest.mark.parametrize("scope", ["function", "session"])
+ def test_parameters_without_eq_semantics(self, scope, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ class NoEq1: # fails on `a == b` statement
+ def __eq__(self, _):
+ raise RuntimeError
+
+ class NoEq2: # fails on `if a == b:` statement
+ def __eq__(self, _):
+ class NoBool:
+ def __bool__(self):
+ raise RuntimeError
+ return NoBool()
+
+ import pytest
+ @pytest.fixture(params=[NoEq1(), NoEq2()], scope={scope!r})
+ def no_eq(request):
+ return request.param
+
+ def test1(no_eq):
+ pass
+
+ def test2(no_eq):
+ pass
+ """.format(
+ scope=scope
+ )
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*4 passed*"])
+
+ def test_funcarg_parametrized_and_used_twice(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ @pytest.fixture(params=[1,2])
+ def arg1(request):
+ values.append(1)
+ return request.param
+
+ @pytest.fixture()
+ def arg2(arg1):
+ return arg1 + 1
+
+ def test_add(arg1, arg2):
+ assert arg2 == arg1 + 1
+ assert len(values) == arg1
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+ def test_factory_uses_unknown_funcarg_as_dependency_error(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture()
+ def fail(missing):
+ return
+
+ @pytest.fixture()
+ def call_fail(fail):
+ return
+
+ def test_missing(call_fail):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ """
+ *pytest.fixture()*
+ *def call_fail(fail)*
+ *pytest.fixture()*
+ *def fail*
+ *fixture*'missing'*not found*
+ """
+ )
+
+ def test_factory_setup_as_classes_fails(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ class arg1(object):
+ def __init__(self, request):
+ self.x = 1
+ arg1 = pytest.fixture()(arg1)
+
+ """
+ )
+ reprec = pytester.inline_run()
+ values = reprec.getfailedcollections()
+ assert len(values) == 1
+
+ def test_usefixtures_marker(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ values = []
+
+ @pytest.fixture(scope="class")
+ def myfix(request):
+ request.cls.hello = "world"
+ values.append(1)
+
+ class TestClass(object):
+ def test_one(self):
+ assert self.hello == "world"
+ assert len(values) == 1
+ def test_two(self):
+ assert self.hello == "world"
+ assert len(values) == 1
+ pytest.mark.usefixtures("myfix")(TestClass)
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ def test_usefixtures_ini(self, pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ usefixtures = myfix
+ """
+ )
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(scope="class")
+ def myfix(request):
+ request.cls.hello = "world"
+
+ """
+ )
+ pytester.makepyfile(
+ """
+ class TestClass(object):
+ def test_one(self):
+ assert self.hello == "world"
+ def test_two(self):
+ assert self.hello == "world"
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ def test_usefixtures_seen_in_showmarkers(self, pytester: Pytester) -> None:
+ result = pytester.runpytest("--markers")
+ result.stdout.fnmatch_lines(
+ """
+ *usefixtures(fixturename1*mark tests*fixtures*
+ """
+ )
+
+ def test_request_instance_issue203(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ class TestClass(object):
+ @pytest.fixture
+ def setup1(self, request):
+ assert self == request.instance
+ self.arg1 = 1
+ def test_hello(self, setup1):
+ assert self.arg1 == 1
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_fixture_parametrized_with_iterator(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ values = []
+ def f():
+ yield 1
+ yield 2
+ dec = pytest.fixture(scope="module", params=f())
+
+ @dec
+ def arg(request):
+ return request.param
+ @dec
+ def arg2(request):
+ return request.param
+
+ def test_1(arg):
+ values.append(arg)
+ def test_2(arg2):
+ values.append(arg2*10)
+ """
+ )
+ reprec = pytester.inline_run("-v")
+ reprec.assertoutcome(passed=4)
+ values = reprec.getcalls("pytest_runtest_call")[0].item.module.values
+ assert values == [1, 2, 10, 20]
+
+ def test_setup_functions_as_fixtures(self, pytester: Pytester) -> None:
+ """Ensure setup_* methods obey fixture scope rules (#517, #3094)."""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ DB_INITIALIZED = None
+
+ @pytest.fixture(scope="session", autouse=True)
+ def db():
+ global DB_INITIALIZED
+ DB_INITIALIZED = True
+ yield
+ DB_INITIALIZED = False
+
+ def setup_module():
+ assert DB_INITIALIZED
+
+ def teardown_module():
+ assert DB_INITIALIZED
+
+ class TestClass(object):
+
+ def setup_method(self, method):
+ assert DB_INITIALIZED
+
+ def teardown_method(self, method):
+ assert DB_INITIALIZED
+
+ def test_printer_1(self):
+ pass
+
+ def test_printer_2(self):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 2 passed in *"])
+
+
+class TestFixtureManagerParseFactories:
+ @pytest.fixture
+ def pytester(self, pytester: Pytester) -> Pytester:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture
+ def hello(request):
+ return "conftest"
+
+ @pytest.fixture
+ def fm(request):
+ return request._fixturemanager
+
+ @pytest.fixture
+ def item(request):
+ return request._pyfuncitem
+ """
+ )
+ return pytester
+
+ def test_parsefactories_evil_objects_issue214(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ class A(object):
+ def __call__(self):
+ pass
+ def __getattr__(self, name):
+ raise RuntimeError()
+ a = A()
+ def test_hello():
+ pass
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1, failed=0)
+
+ def test_parsefactories_conftest(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_hello(item, fm):
+ for name in ("fm", "hello", "item"):
+ faclist = fm.getfixturedefs(name, item.nodeid)
+ assert len(faclist) == 1
+ fac = faclist[0]
+ assert fac.func.__name__ == name
+ """
+ )
+ reprec = pytester.inline_run("-s")
+ reprec.assertoutcome(passed=1)
+
+ def test_parsefactories_conftest_and_module_and_class(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ """\
+ import pytest
+
+ @pytest.fixture
+ def hello(request):
+ return "module"
+ class TestClass(object):
+ @pytest.fixture
+ def hello(self, request):
+ return "class"
+ def test_hello(self, item, fm):
+ faclist = fm.getfixturedefs("hello", item.nodeid)
+ print(faclist)
+ assert len(faclist) == 3
+
+ assert faclist[0].func(item._request) == "conftest"
+ assert faclist[1].func(item._request) == "module"
+ assert faclist[2].func(item._request) == "class"
+ """
+ )
+ reprec = pytester.inline_run("-s")
+ reprec.assertoutcome(passed=1)
+
+ def test_parsefactories_relative_node_ids(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ # example mostly taken from:
+ # https://mail.python.org/pipermail/pytest-dev/2014-September/002617.html
+ runner = pytester.mkdir("runner")
+ package = pytester.mkdir("package")
+ package.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ @pytest.fixture
+ def one():
+ return 1
+ """
+ )
+ )
+ package.joinpath("test_x.py").write_text(
+ textwrap.dedent(
+ """\
+ def test_x(one):
+ assert one == 1
+ """
+ )
+ )
+ sub = package.joinpath("sub")
+ sub.mkdir()
+ sub.joinpath("__init__.py").touch()
+ sub.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ @pytest.fixture
+ def one():
+ return 2
+ """
+ )
+ )
+ sub.joinpath("test_y.py").write_text(
+ textwrap.dedent(
+ """\
+ def test_x(one):
+ assert one == 2
+ """
+ )
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+ with monkeypatch.context() as mp:
+ mp.chdir(runner)
+ reprec = pytester.inline_run("..")
+ reprec.assertoutcome(passed=2)
+
+ def test_package_xunit_fixture(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ __init__="""\
+ values = []
+ """
+ )
+ package = pytester.mkdir("package")
+ package.joinpath("__init__.py").write_text(
+ textwrap.dedent(
+ """\
+ from .. import values
+ def setup_module():
+ values.append("package")
+ def teardown_module():
+ values[:] = []
+ """
+ )
+ )
+ package.joinpath("test_x.py").write_text(
+ textwrap.dedent(
+ """\
+ from .. import values
+ def test_x():
+ assert values == ["package"]
+ """
+ )
+ )
+ package = pytester.mkdir("package2")
+ package.joinpath("__init__.py").write_text(
+ textwrap.dedent(
+ """\
+ from .. import values
+ def setup_module():
+ values.append("package2")
+ def teardown_module():
+ values[:] = []
+ """
+ )
+ )
+ package.joinpath("test_x.py").write_text(
+ textwrap.dedent(
+ """\
+ from .. import values
+ def test_x():
+ assert values == ["package2"]
+ """
+ )
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ def test_package_fixture_complex(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ __init__="""\
+ values = []
+ """
+ )
+ pytester.syspathinsert(pytester.path.name)
+ package = pytester.mkdir("package")
+ package.joinpath("__init__.py").write_text("")
+ package.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ from .. import values
+ @pytest.fixture(scope="package")
+ def one():
+ values.append("package")
+ yield values
+ values.pop()
+ @pytest.fixture(scope="package", autouse=True)
+ def two():
+ values.append("package-auto")
+ yield values
+ values.pop()
+ """
+ )
+ )
+ package.joinpath("test_x.py").write_text(
+ textwrap.dedent(
+ """\
+ from .. import values
+ def test_package_autouse():
+ assert values == ["package-auto"]
+ def test_package(one):
+ assert values == ["package-auto", "package"]
+ """
+ )
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ def test_collect_custom_items(self, pytester: Pytester) -> None:
+ pytester.copy_example("fixtures/custom_item")
+ result = pytester.runpytest("foo")
+ result.stdout.fnmatch_lines(["*passed*"])
+
+
+class TestAutouseDiscovery:
+ @pytest.fixture
+ def pytester(self, pytester: Pytester) -> Pytester:
+ pytester.makeconftest(
+ """
+ import pytest
+ @pytest.fixture(autouse=True)
+ def perfunction(request, tmp_path):
+ pass
+
+ @pytest.fixture()
+ def arg1(tmp_path):
+ pass
+ @pytest.fixture(autouse=True)
+ def perfunction2(arg1):
+ pass
+
+ @pytest.fixture
+ def fm(request):
+ return request._fixturemanager
+
+ @pytest.fixture
+ def item(request):
+ return request._pyfuncitem
+ """
+ )
+ return pytester
+
+ def test_parsefactories_conftest(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ from _pytest.pytester import get_public_names
+ def test_check_setup(item, fm):
+ autousenames = list(fm._getautousenames(item.nodeid))
+ assert len(get_public_names(autousenames)) == 2
+ assert "perfunction2" in autousenames
+ assert "perfunction" in autousenames
+ """
+ )
+ reprec = pytester.inline_run("-s")
+ reprec.assertoutcome(passed=1)
+
+ def test_two_classes_separated_autouse(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ class TestA(object):
+ values = []
+ @pytest.fixture(autouse=True)
+ def setup1(self):
+ self.values.append(1)
+ def test_setup1(self):
+ assert self.values == [1]
+ class TestB(object):
+ values = []
+ @pytest.fixture(autouse=True)
+ def setup2(self):
+ self.values.append(1)
+ def test_setup2(self):
+ assert self.values == [1]
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ def test_setup_at_classlevel(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ class TestClass(object):
+ @pytest.fixture(autouse=True)
+ def permethod(self, request):
+ request.instance.funcname = request.function.__name__
+ def test_method1(self):
+ assert self.funcname == "test_method1"
+ def test_method2(self):
+ assert self.funcname == "test_method2"
+ """
+ )
+ reprec = pytester.inline_run("-s")
+ reprec.assertoutcome(passed=2)
+
+ @pytest.mark.xfail(reason="'enabled' feature not implemented")
+ def test_setup_enabled_functionnode(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ def enabled(parentnode, markers):
+ return "needsdb" in markers
+
+ @pytest.fixture(params=[1,2])
+ def db(request):
+ return request.param
+
+ @pytest.fixture(enabled=enabled, autouse=True)
+ def createdb(db):
+ pass
+
+ def test_func1(request):
+ assert "db" not in request.fixturenames
+
+ @pytest.mark.needsdb
+ def test_func2(request):
+ assert "db" in request.fixturenames
+ """
+ )
+ reprec = pytester.inline_run("-s")
+ reprec.assertoutcome(passed=2)
+
+ def test_callables_nocode(self, pytester: Pytester) -> None:
+ """An imported mock.call would break setup/factory discovery due to
+ it being callable and __code__ not being a code object."""
+ pytester.makepyfile(
+ """
+ class _call(tuple):
+ def __call__(self, *k, **kw):
+ pass
+ def __getattr__(self, k):
+ return self
+
+ call = _call()
+ """
+ )
+ reprec = pytester.inline_run("-s")
+ reprec.assertoutcome(failed=0, passed=0)
+
+ def test_autouse_in_conftests(self, pytester: Pytester) -> None:
+ a = pytester.mkdir("a")
+ b = pytester.mkdir("a1")
+ conftest = pytester.makeconftest(
+ """
+ import pytest
+ @pytest.fixture(autouse=True)
+ def hello():
+ xxx
+ """
+ )
+ 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")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ """
+ *1 passed*1 error*
+ """
+ )
+
+ def test_autouse_in_module_and_two_classes(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ @pytest.fixture(autouse=True)
+ def append1():
+ values.append("module")
+ def test_x():
+ assert values == ["module"]
+
+ class TestA(object):
+ @pytest.fixture(autouse=True)
+ def append2(self):
+ values.append("A")
+ def test_hello(self):
+ assert values == ["module", "module", "A"], values
+ class TestA2(object):
+ def test_world(self):
+ assert values == ["module", "module", "A", "module"], values
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=3)
+
+
+class TestAutouseManagement:
+ def test_autouse_conftest_mid_directory(self, pytester: Pytester) -> None:
+ pkgdir = pytester.mkpydir("xyz123")
+ pkgdir.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ @pytest.fixture(autouse=True)
+ def app():
+ import sys
+ sys._myapp = "hello"
+ """
+ )
+ )
+ sub = pkgdir.joinpath("tests")
+ sub.mkdir()
+ t = sub.joinpath("test_app.py")
+ t.touch()
+ t.write_text(
+ textwrap.dedent(
+ """\
+ import sys
+ def test_app():
+ assert sys._myapp == "hello"
+ """
+ )
+ )
+ reprec = pytester.inline_run("-s")
+ reprec.assertoutcome(passed=1)
+
+ def test_funcarg_and_setup(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ @pytest.fixture(scope="module")
+ def arg():
+ values.append(1)
+ return 0
+ @pytest.fixture(scope="module", autouse=True)
+ def something(arg):
+ values.append(2)
+
+ def test_hello(arg):
+ assert len(values) == 2
+ assert values == [1,2]
+ assert arg == 0
+
+ def test_hello2(arg):
+ assert len(values) == 2
+ assert values == [1,2]
+ assert arg == 0
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ def test_uses_parametrized_resource(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ @pytest.fixture(params=[1,2])
+ def arg(request):
+ return request.param
+
+ @pytest.fixture(autouse=True)
+ def something(arg):
+ values.append(arg)
+
+ def test_hello():
+ if len(values) == 1:
+ assert values == [1]
+ elif len(values) == 2:
+ assert values == [1, 2]
+ else:
+ 0/0
+
+ """
+ )
+ reprec = pytester.inline_run("-s")
+ reprec.assertoutcome(passed=2)
+
+ def test_session_parametrized_function(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ values = []
+
+ @pytest.fixture(scope="session", params=[1,2])
+ def arg(request):
+ return request.param
+
+ @pytest.fixture(scope="function", autouse=True)
+ def append(request, arg):
+ if request.function.__name__ == "test_some":
+ values.append(arg)
+
+ def test_some():
+ pass
+
+ def test_result(arg):
+ assert len(values) == arg
+ assert values[:arg] == [1,2][:arg]
+ """
+ )
+ reprec = pytester.inline_run("-v", "-s")
+ reprec.assertoutcome(passed=4)
+
+ def test_class_function_parametrization_finalization(
+ self, pytester: Pytester
+ ) -> None:
+ p = pytester.makeconftest(
+ """
+ import pytest
+ import pprint
+
+ values = []
+
+ @pytest.fixture(scope="function", params=[1,2])
+ def farg(request):
+ return request.param
+
+ @pytest.fixture(scope="class", params=list("ab"))
+ def carg(request):
+ return request.param
+
+ @pytest.fixture(scope="function", autouse=True)
+ def append(request, farg, carg):
+ def fin():
+ values.append("fin_%s%s" % (carg, farg))
+ request.addfinalizer(fin)
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+
+ class TestClass(object):
+ def test_1(self):
+ pass
+ class TestClass2(object):
+ def test_2(self):
+ pass
+ """
+ )
+ 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
+ assert values == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2
+
+ def test_scope_ordering(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ @pytest.fixture(scope="function", autouse=True)
+ def fappend2():
+ values.append(2)
+ @pytest.fixture(scope="class", autouse=True)
+ def classappend3():
+ values.append(3)
+ @pytest.fixture(scope="module", autouse=True)
+ def mappend():
+ values.append(1)
+
+ class TestHallo(object):
+ def test_method(self):
+ assert values == [1,3,2]
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_parametrization_setup_teardown_ordering(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ def pytest_generate_tests(metafunc):
+ if metafunc.cls is None:
+ assert metafunc.function is test_finish
+ if metafunc.cls is not None:
+ metafunc.parametrize("item", [1,2], scope="class")
+ class TestClass(object):
+ @pytest.fixture(scope="class", autouse=True)
+ def addteardown(self, item, request):
+ values.append("setup-%d" % item)
+ request.addfinalizer(lambda: values.append("teardown-%d" % item))
+ def test_step1(self, item):
+ values.append("step1-%d" % item)
+ def test_step2(self, item):
+ values.append("step2-%d" % item)
+
+ def test_finish():
+ print(values)
+ assert values == ["setup-1", "step1-1", "step2-1", "teardown-1",
+ "setup-2", "step1-2", "step2-2", "teardown-2",]
+ """
+ )
+ reprec = pytester.inline_run("-s")
+ reprec.assertoutcome(passed=5)
+
+ def test_ordering_autouse_before_explicit(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ values = []
+ @pytest.fixture(autouse=True)
+ def fix1():
+ values.append(1)
+ @pytest.fixture()
+ def arg1():
+ values.append(2)
+ def test_hello(arg1):
+ assert values == [1,2]
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ @pytest.mark.parametrize("param1", ["", "params=[1]"], ids=["p00", "p01"])
+ @pytest.mark.parametrize("param2", ["", "params=[1]"], ids=["p10", "p11"])
+ def test_ordering_dependencies_torndown_first(
+ self, pytester: Pytester, param1, param2
+ ) -> None:
+ """#226"""
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ @pytest.fixture(%(param1)s)
+ def arg1(request):
+ request.addfinalizer(lambda: values.append("fin1"))
+ values.append("new1")
+ @pytest.fixture(%(param2)s)
+ def arg2(request, arg1):
+ request.addfinalizer(lambda: values.append("fin2"))
+ values.append("new2")
+
+ def test_arg(arg2):
+ pass
+ def test_check():
+ assert values == ["new1", "new2", "fin2", "fin1"]
+ """
+ % locals()
+ )
+ reprec = pytester.inline_run("-s")
+ reprec.assertoutcome(passed=2)
+
+
+class TestFixtureMarker:
+ def test_parametrize(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(params=["a", "b", "c"])
+ def arg(request):
+ return request.param
+ values = []
+ def test_param(arg):
+ values.append(arg)
+ def test_result():
+ assert values == list("abc")
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=4)
+
+ def test_multiple_parametrization_issue_736(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(params=[1,2,3])
+ def foo(request):
+ return request.param
+
+ @pytest.mark.parametrize('foobar', [4,5,6])
+ def test_issue(foo, foobar):
+ assert foo in [1,2,3]
+ assert foobar in [4,5,6]
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=9)
+
+ @pytest.mark.parametrize(
+ "param_args",
+ ["'fixt, val'", "'fixt,val'", "['fixt', 'val']", "('fixt', 'val')"],
+ )
+ def test_override_parametrized_fixture_issue_979(
+ self, pytester: Pytester, param_args
+ ) -> None:
+ """Make sure a parametrized argument can override a parametrized fixture.
+
+ This was a regression introduced in the fix for #736.
+ """
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(params=[1, 2])
+ def fixt(request):
+ return request.param
+
+ @pytest.mark.parametrize(%s, [(3, 'x'), (4, 'x')])
+ def test_foo(fixt, val):
+ pass
+ """
+ % param_args
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ def test_scope_session(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ @pytest.fixture(scope="module")
+ def arg():
+ values.append(1)
+ return 1
+
+ def test_1(arg):
+ assert arg == 1
+ def test_2(arg):
+ assert arg == 1
+ assert len(values) == 1
+ class TestClass(object):
+ def test3(self, arg):
+ assert arg == 1
+ assert len(values) == 1
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=3)
+
+ def test_scope_session_exc(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ @pytest.fixture(scope="session")
+ def fix():
+ values.append(1)
+ pytest.skip('skipping')
+
+ def test_1(fix):
+ pass
+ def test_2(fix):
+ pass
+ def test_last():
+ assert values == [1]
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(skipped=2, passed=1)
+
+ def test_scope_session_exc_two_fix(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ m = []
+ @pytest.fixture(scope="session")
+ def a():
+ values.append(1)
+ pytest.skip('skipping')
+ @pytest.fixture(scope="session")
+ def b(a):
+ m.append(1)
+
+ def test_1(b):
+ pass
+ def test_2(b):
+ pass
+ def test_last():
+ assert values == [1]
+ assert m == []
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(skipped=2, passed=1)
+
+ def test_scope_exc(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_foo="""
+ def test_foo(fix):
+ pass
+ """,
+ test_bar="""
+ def test_bar(fix):
+ pass
+ """,
+ conftest="""
+ import pytest
+ reqs = []
+ @pytest.fixture(scope="session")
+ def fix(request):
+ reqs.append(1)
+ pytest.skip()
+ @pytest.fixture
+ def req_list():
+ return reqs
+ """,
+ test_real="""
+ def test_last(req_list):
+ assert req_list == [1]
+ """,
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(skipped=2, passed=1)
+
+ def test_scope_module_uses_session(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ @pytest.fixture(scope="module")
+ def arg():
+ values.append(1)
+ return 1
+
+ def test_1(arg):
+ assert arg == 1
+ def test_2(arg):
+ assert arg == 1
+ assert len(values) == 1
+ class TestClass(object):
+ def test3(self, arg):
+ assert arg == 1
+ assert len(values) == 1
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=3)
+
+ def test_scope_module_and_finalizer(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ finalized_list = []
+ created_list = []
+ @pytest.fixture(scope="module")
+ def arg(request):
+ created_list.append(1)
+ assert request.scope == "module"
+ request.addfinalizer(lambda: finalized_list.append(1))
+ @pytest.fixture
+ def created(request):
+ return len(created_list)
+ @pytest.fixture
+ def finalized(request):
+ return len(finalized_list)
+ """
+ )
+ pytester.makepyfile(
+ test_mod1="""
+ def test_1(arg, created, finalized):
+ assert created == 1
+ assert finalized == 0
+ def test_2(arg, created, finalized):
+ assert created == 1
+ assert finalized == 0""",
+ test_mod2="""
+ def test_3(arg, created, finalized):
+ assert created == 2
+ assert finalized == 1""",
+ test_mode3="""
+ def test_4(arg, created, finalized):
+ assert created == 3
+ assert finalized == 2
+ """,
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=4)
+
+ def test_scope_mismatch_various(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ finalized = []
+ created = []
+ @pytest.fixture(scope="function")
+ def arg(request):
+ pass
+ """
+ )
+ pytester.makepyfile(
+ test_mod1="""
+ import pytest
+ @pytest.fixture(scope="session")
+ def arg(request):
+ request.getfixturevalue("arg")
+ def test_1(arg):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret != 0
+ result.stdout.fnmatch_lines(
+ ["*ScopeMismatch*You tried*function*session*request*"]
+ )
+
+ def test_dynamic_scope(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+
+
+ def pytest_addoption(parser):
+ parser.addoption("--extend-scope", action="store_true", default=False)
+
+
+ def dynamic_scope(fixture_name, config):
+ if config.getoption("--extend-scope"):
+ return "session"
+ return "function"
+
+
+ @pytest.fixture(scope=dynamic_scope)
+ def dynamic_fixture(calls=[]):
+ calls.append("call")
+ return len(calls)
+
+ """
+ )
+
+ pytester.makepyfile(
+ """
+ def test_first(dynamic_fixture):
+ assert dynamic_fixture == 1
+
+
+ def test_second(dynamic_fixture):
+ assert dynamic_fixture == 2
+
+ """
+ )
+
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ reprec = pytester.inline_run("--extend-scope")
+ reprec.assertoutcome(passed=1, failed=1)
+
+ def test_dynamic_scope_bad_return(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ def dynamic_scope(**_):
+ return "wrong-scope"
+
+ @pytest.fixture(scope=dynamic_scope)
+ def fixture():
+ pass
+
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ "Fixture 'fixture' from test_dynamic_scope_bad_return.py "
+ "got an unexpected scope value 'wrong-scope'"
+ )
+
+ def test_register_only_with_mark(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ @pytest.fixture()
+ def arg():
+ return 1
+ """
+ )
+ pytester.makepyfile(
+ test_mod1="""
+ import pytest
+ @pytest.fixture()
+ def arg(arg):
+ return arg + 1
+ def test_1(arg):
+ assert arg == 2
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_parametrize_and_scope(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope="module", params=["a", "b", "c"])
+ def arg(request):
+ return request.param
+ values = []
+ def test_param(arg):
+ values.append(arg)
+ """
+ )
+ reprec = pytester.inline_run("-v")
+ reprec.assertoutcome(passed=3)
+ values = reprec.getcalls("pytest_runtest_call")[0].item.module.values
+ assert len(values) == 3
+ assert "a" in values
+ assert "b" in values
+ assert "c" in values
+
+ def test_scope_mismatch(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ @pytest.fixture(scope="function")
+ def arg(request):
+ pass
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope="session")
+ def arg(arg):
+ pass
+ def test_mismatch(arg):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*ScopeMismatch*", "*1 error*"])
+
+ def test_parametrize_separated_order(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope="module", params=[1, 2])
+ def arg(request):
+ return request.param
+
+ values = []
+ def test_1(arg):
+ values.append(arg)
+ def test_2(arg):
+ values.append(arg)
+ """
+ )
+ reprec = pytester.inline_run("-v")
+ reprec.assertoutcome(passed=4)
+ values = reprec.getcalls("pytest_runtest_call")[0].item.module.values
+ assert values == [1, 1, 2, 2]
+
+ def test_module_parametrized_ordering(self, pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ console_output_style=classic
+ """
+ )
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(scope="session", params="s1 s2".split())
+ def sarg():
+ pass
+ @pytest.fixture(scope="module", params="m1 m2".split())
+ def marg():
+ pass
+ """
+ )
+ pytester.makepyfile(
+ test_mod1="""
+ def test_func(sarg):
+ pass
+ def test_func1(marg):
+ pass
+ """,
+ test_mod2="""
+ def test_func2(sarg):
+ pass
+ def test_func3(sarg, marg):
+ pass
+ def test_func3b(sarg, marg):
+ pass
+ def test_func4(marg):
+ pass
+ """,
+ )
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(
+ """
+ test_mod1.py::test_func[s1] PASSED
+ test_mod2.py::test_func2[s1] PASSED
+ test_mod2.py::test_func3[s1-m1] PASSED
+ test_mod2.py::test_func3b[s1-m1] PASSED
+ test_mod2.py::test_func3[s1-m2] PASSED
+ test_mod2.py::test_func3b[s1-m2] PASSED
+ test_mod1.py::test_func[s2] PASSED
+ test_mod2.py::test_func2[s2] PASSED
+ test_mod2.py::test_func3[s2-m1] PASSED
+ test_mod2.py::test_func3b[s2-m1] PASSED
+ test_mod2.py::test_func4[m1] PASSED
+ test_mod2.py::test_func3[s2-m2] PASSED
+ test_mod2.py::test_func3b[s2-m2] PASSED
+ test_mod2.py::test_func4[m2] PASSED
+ test_mod1.py::test_func1[m1] PASSED
+ test_mod1.py::test_func1[m2] PASSED
+ """
+ )
+
+ def test_dynamic_parametrized_ordering(self, pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ console_output_style=classic
+ """
+ )
+ pytester.makeconftest(
+ """
+ import pytest
+
+ def pytest_configure(config):
+ class DynamicFixturePlugin(object):
+ @pytest.fixture(scope='session', params=['flavor1', 'flavor2'])
+ def flavor(self, request):
+ return request.param
+ config.pluginmanager.register(DynamicFixturePlugin(), 'flavor-fixture')
+
+ @pytest.fixture(scope='session', params=['vxlan', 'vlan'])
+ def encap(request):
+ return request.param
+
+ @pytest.fixture(scope='session', autouse='True')
+ def reprovision(request, flavor, encap):
+ pass
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test(reprovision):
+ pass
+ def test2(reprovision):
+ pass
+ """
+ )
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(
+ """
+ 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
+ """
+ )
+
+ def test_class_ordering(self, pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ console_output_style=classic
+ """
+ )
+ pytester.makeconftest(
+ """
+ import pytest
+
+ values = []
+
+ @pytest.fixture(scope="function", params=[1,2])
+ def farg(request):
+ return request.param
+
+ @pytest.fixture(scope="class", params=list("ab"))
+ def carg(request):
+ return request.param
+
+ @pytest.fixture(scope="function", autouse=True)
+ def append(request, farg, carg):
+ def fin():
+ values.append("fin_%s%s" % (carg, farg))
+ request.addfinalizer(fin)
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+
+ class TestClass2(object):
+ def test_1(self):
+ pass
+ def test_2(self):
+ pass
+ class TestClass(object):
+ def test_3(self):
+ pass
+ """
+ )
+ result = pytester.runpytest("-vs")
+ result.stdout.re_match_lines(
+ r"""
+ test_class_ordering.py::TestClass2::test_1\[a-1\] PASSED
+ test_class_ordering.py::TestClass2::test_1\[a-2\] PASSED
+ test_class_ordering.py::TestClass2::test_2\[a-1\] PASSED
+ test_class_ordering.py::TestClass2::test_2\[a-2\] PASSED
+ test_class_ordering.py::TestClass2::test_1\[b-1\] PASSED
+ test_class_ordering.py::TestClass2::test_1\[b-2\] PASSED
+ test_class_ordering.py::TestClass2::test_2\[b-1\] PASSED
+ test_class_ordering.py::TestClass2::test_2\[b-2\] PASSED
+ test_class_ordering.py::TestClass::test_3\[a-1\] PASSED
+ test_class_ordering.py::TestClass::test_3\[a-2\] PASSED
+ test_class_ordering.py::TestClass::test_3\[b-1\] PASSED
+ test_class_ordering.py::TestClass::test_3\[b-2\] PASSED
+ """
+ )
+
+ def test_parametrize_separated_order_higher_scope_first(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope="function", params=[1, 2])
+ def arg(request):
+ param = request.param
+ request.addfinalizer(lambda: values.append("fin:%s" % param))
+ values.append("create:%s" % param)
+ return request.param
+
+ @pytest.fixture(scope="module", params=["mod1", "mod2"])
+ def modarg(request):
+ param = request.param
+ request.addfinalizer(lambda: values.append("fin:%s" % param))
+ values.append("create:%s" % param)
+ return request.param
+
+ values = []
+ def test_1(arg):
+ values.append("test1")
+ def test_2(modarg):
+ values.append("test2")
+ def test_3(arg, modarg):
+ values.append("test3")
+ def test_4(modarg, arg):
+ values.append("test4")
+ """
+ )
+ reprec = pytester.inline_run("-v")
+ reprec.assertoutcome(passed=12)
+ values = reprec.getcalls("pytest_runtest_call")[0].item.module.values
+ expected = [
+ "create:1",
+ "test1",
+ "fin:1",
+ "create:2",
+ "test1",
+ "fin:2",
+ "create:mod1",
+ "test2",
+ "create:1",
+ "test3",
+ "fin:1",
+ "create:2",
+ "test3",
+ "fin:2",
+ "create:1",
+ "test4",
+ "fin:1",
+ "create:2",
+ "test4",
+ "fin:2",
+ "fin:mod1",
+ "create:mod2",
+ "test2",
+ "create:1",
+ "test3",
+ "fin:1",
+ "create:2",
+ "test3",
+ "fin:2",
+ "create:1",
+ "test4",
+ "fin:1",
+ "create:2",
+ "test4",
+ "fin:2",
+ "fin:mod2",
+ ]
+ import pprint
+
+ pprint.pprint(list(zip(values, expected)))
+ assert values == expected
+
+ def test_parametrized_fixture_teardown_order(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(params=[1,2], scope="class")
+ def param1(request):
+ return request.param
+
+ values = []
+
+ class TestClass(object):
+ @classmethod
+ @pytest.fixture(scope="class", autouse=True)
+ def setup1(self, request, param1):
+ values.append(1)
+ request.addfinalizer(self.teardown1)
+ @classmethod
+ def teardown1(self):
+ assert values.pop() == 1
+ @pytest.fixture(scope="class", autouse=True)
+ def setup2(self, request, param1):
+ values.append(2)
+ request.addfinalizer(self.teardown2)
+ @classmethod
+ def teardown2(self):
+ assert values.pop() == 2
+ def test(self):
+ pass
+
+ def test_finish():
+ assert not values
+ """
+ )
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(
+ """
+ *3 passed*
+ """
+ )
+ result.stdout.no_fnmatch_line("*error*")
+
+ def test_fixture_finalizer(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ import sys
+
+ @pytest.fixture
+ def browser(request):
+
+ def finalize():
+ sys.stdout.write_text('Finalized')
+ request.addfinalizer(finalize)
+ return {}
+ """
+ )
+ b = pytester.mkdir("subdir")
+ b.joinpath("test_overridden_fixture_finalizer.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ @pytest.fixture
+ def browser(browser):
+ browser['visited'] = True
+ return browser
+
+ def test_browser(browser):
+ assert browser['visited'] is True
+ """
+ )
+ )
+ reprec = pytester.runpytest("-s")
+ for test in ["test_browser"]:
+ reprec.stdout.fnmatch_lines(["*Finalized*"])
+
+ def test_class_scope_with_normal_tests(self, pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import pytest
+
+ class Box(object):
+ value = 0
+
+ @pytest.fixture(scope='class')
+ def a(request):
+ Box.value += 1
+ return Box.value
+
+ def test_a(a):
+ assert a == 1
+
+ class Test1(object):
+ def test_b(self, a):
+ assert a == 2
+
+ class Test2(object):
+ def test_c(self, a):
+ assert a == 3"""
+ )
+ reprec = pytester.inline_run(testpath)
+ for test in ["test_a", "test_b", "test_c"]:
+ assert reprec.matchreport(test).passed
+
+ def test_request_is_clean(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ @pytest.fixture(params=[1, 2])
+ def fix(request):
+ request.addfinalizer(lambda: values.append(request.param))
+ def test_fix(fix):
+ pass
+ """
+ )
+ reprec = pytester.inline_run("-s")
+ values = reprec.getcalls("pytest_runtest_call")[0].item.module.values
+ assert values == [1, 2]
+
+ def test_parametrize_separated_lifecycle(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ values = []
+ @pytest.fixture(scope="module", params=[1, 2])
+ def arg(request):
+ x = request.param
+ request.addfinalizer(lambda: values.append("fin%s" % x))
+ return request.param
+ def test_1(arg):
+ values.append(arg)
+ def test_2(arg):
+ values.append(arg)
+ """
+ )
+ reprec = pytester.inline_run("-vs")
+ reprec.assertoutcome(passed=4)
+ values = reprec.getcalls("pytest_runtest_call")[0].item.module.values
+ import pprint
+
+ pprint.pprint(values)
+ # assert len(values) == 6
+ assert values[0] == values[1] == 1
+ assert values[2] == "fin1"
+ assert values[3] == values[4] == 2
+ assert values[5] == "fin2"
+
+ def test_parametrize_function_scoped_finalizers_called(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope="function", params=[1, 2])
+ def arg(request):
+ x = request.param
+ request.addfinalizer(lambda: values.append("fin%s" % x))
+ return request.param
+
+ values = []
+ def test_1(arg):
+ values.append(arg)
+ def test_2(arg):
+ values.append(arg)
+ def test_3():
+ assert len(values) == 8
+ assert values == [1, "fin1", 2, "fin2", 1, "fin1", 2, "fin2"]
+ """
+ )
+ reprec = pytester.inline_run("-v")
+ reprec.assertoutcome(passed=5)
+
+ @pytest.mark.parametrize("scope", ["session", "function", "module"])
+ def test_finalizer_order_on_parametrization(
+ self, scope, pytester: Pytester
+ ) -> None:
+ """#246"""
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+
+ @pytest.fixture(scope=%(scope)r, params=["1"])
+ def fix1(request):
+ return request.param
+
+ @pytest.fixture(scope=%(scope)r)
+ def fix2(request, base):
+ def cleanup_fix2():
+ assert not values, "base should not have been finalized"
+ request.addfinalizer(cleanup_fix2)
+
+ @pytest.fixture(scope=%(scope)r)
+ def base(request, fix1):
+ def cleanup_base():
+ values.append("fin_base")
+ print("finalizing base")
+ request.addfinalizer(cleanup_base)
+
+ def test_begin():
+ pass
+ def test_baz(base, fix2):
+ pass
+ def test_other():
+ pass
+ """
+ % {"scope": scope}
+ )
+ reprec = pytester.inline_run("-lvs")
+ reprec.assertoutcome(passed=3)
+
+ def test_class_scope_parametrization_ordering(self, pytester: Pytester) -> None:
+ """#396"""
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ @pytest.fixture(params=["John", "Doe"], scope="class")
+ def human(request):
+ request.addfinalizer(lambda: values.append("fin %s" % request.param))
+ return request.param
+
+ class TestGreetings(object):
+ def test_hello(self, human):
+ values.append("test_hello")
+
+ class TestMetrics(object):
+ def test_name(self, human):
+ values.append("test_name")
+
+ def test_population(self, human):
+ values.append("test_population")
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=6)
+ values = reprec.getcalls("pytest_runtest_call")[0].item.module.values
+ assert values == [
+ "test_hello",
+ "fin John",
+ "test_hello",
+ "fin Doe",
+ "test_name",
+ "test_population",
+ "fin John",
+ "test_name",
+ "test_population",
+ "fin Doe",
+ ]
+
+ def test_parametrize_setup_function(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope="module", params=[1, 2])
+ def arg(request):
+ return request.param
+
+ @pytest.fixture(scope="module", autouse=True)
+ def mysetup(request, arg):
+ request.addfinalizer(lambda: values.append("fin%s" % arg))
+ values.append("setup%s" % arg)
+
+ values = []
+ def test_1(arg):
+ values.append(arg)
+ def test_2(arg):
+ values.append(arg)
+ def test_3():
+ import pprint
+ pprint.pprint(values)
+ if arg == 1:
+ assert values == ["setup1", 1, 1, ]
+ elif arg == 2:
+ assert values == ["setup1", 1, 1, "fin1",
+ "setup2", 2, 2, ]
+
+ """
+ )
+ reprec = pytester.inline_run("-v")
+ reprec.assertoutcome(passed=6)
+
+ def test_fixture_marked_function_not_collected_as_test(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture
+ def test_app():
+ return 1
+
+ def test_something(test_app):
+ assert test_app == 1
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_params_and_ids(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(params=[object(), object()],
+ ids=['alpha', 'beta'])
+ def fix(request):
+ return request.param
+
+ def test_foo(fix):
+ assert 1
+ """
+ )
+ res = pytester.runpytest("-v")
+ res.stdout.fnmatch_lines(["*test_foo*alpha*", "*test_foo*beta*"])
+
+ def test_params_and_ids_yieldfixture(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(params=[object(), object()], ids=['alpha', 'beta'])
+ def fix(request):
+ yield request.param
+
+ def test_foo(fix):
+ assert 1
+ """
+ )
+ res = pytester.runpytest("-v")
+ res.stdout.fnmatch_lines(["*test_foo*alpha*", "*test_foo*beta*"])
+
+ def test_deterministic_fixture_collection(
+ self, pytester: Pytester, monkeypatch
+ ) -> None:
+ """#920"""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope="module",
+ params=["A",
+ "B",
+ "C"])
+ def A(request):
+ return request.param
+
+ @pytest.fixture(scope="module",
+ params=["DDDDDDDDD", "EEEEEEEEEEEE", "FFFFFFFFFFF", "banansda"])
+ def B(request, A):
+ return request.param
+
+ def test_foo(B):
+ # Something funky is going on here.
+ # Despite specified seeds, on what is collected,
+ # sometimes we get unexpected passes. hashing B seems
+ # to help?
+ assert hash(B) or True
+ """
+ )
+ monkeypatch.setenv("PYTHONHASHSEED", "1")
+ out1 = pytester.runpytest_subprocess("-v")
+ monkeypatch.setenv("PYTHONHASHSEED", "2")
+ out2 = pytester.runpytest_subprocess("-v")
+ output1 = [
+ line
+ for line in out1.outlines
+ if line.startswith("test_deterministic_fixture_collection.py::test_foo")
+ ]
+ output2 = [
+ line
+ for line in out2.outlines
+ if line.startswith("test_deterministic_fixture_collection.py::test_foo")
+ ]
+ assert len(output1) == 12
+ assert output1 == output2
+
+
+class TestRequestScopeAccess:
+ pytestmark = pytest.mark.parametrize(
+ ("scope", "ok", "error"),
+ [
+ ["session", "", "path class function module"],
+ ["module", "module path", "cls function"],
+ ["class", "module path cls", "function"],
+ ["function", "module path cls function", ""],
+ ],
+ )
+
+ def test_setup(self, pytester: Pytester, scope, ok, error) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope=%r, autouse=True)
+ def myscoped(request):
+ for x in %r:
+ assert hasattr(request, x)
+ for x in %r:
+ pytest.raises(AttributeError, lambda:
+ getattr(request, x))
+ assert request.session
+ assert request.config
+ def test_func():
+ pass
+ """
+ % (scope, ok.split(), error.split())
+ )
+ reprec = pytester.inline_run("-l")
+ reprec.assertoutcome(passed=1)
+
+ def test_funcarg(self, pytester: Pytester, scope, ok, error) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope=%r)
+ def arg(request):
+ for x in %r:
+ assert hasattr(request, x)
+ for x in %r:
+ pytest.raises(AttributeError, lambda:
+ getattr(request, x))
+ assert request.session
+ assert request.config
+ def test_func(arg):
+ pass
+ """
+ % (scope, ok.split(), error.split())
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+class TestErrors:
+ def test_subfactory_missing_funcarg(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture()
+ def gen(qwe123):
+ return 1
+ def test_something(gen):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret != 0
+ result.stdout.fnmatch_lines(
+ ["*def gen(qwe123):*", "*fixture*qwe123*not found*", "*1 error*"]
+ )
+
+ def test_issue498_fixture_finalizer_failing(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture
+ def fix1(request):
+ def f():
+ raise KeyError
+ request.addfinalizer(f)
+ return object()
+
+ values = []
+ def test_1(fix1):
+ values.append(fix1)
+ def test_2(fix1):
+ values.append(fix1)
+ def test_3():
+ assert values[0] != values[1]
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ """
+ *ERROR*teardown*test_1*
+ *KeyError*
+ *ERROR*teardown*test_2*
+ *KeyError*
+ *3 pass*2 errors*
+ """
+ )
+
+ def test_setupfunc_missing_funcarg(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(autouse=True)
+ def gen(qwe123):
+ return 1
+ def test_something():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret != 0
+ result.stdout.fnmatch_lines(
+ ["*def gen(qwe123):*", "*fixture*qwe123*not found*", "*1 error*"]
+ )
+
+
+class TestShowFixtures:
+ def test_funcarg_compat(self, pytester: Pytester) -> None:
+ config = pytester.parseconfigure("--funcargs")
+ assert config.option.showfixtures
+
+ def test_show_fixtures(self, pytester: Pytester) -> None:
+ result = pytester.runpytest("--fixtures")
+ result.stdout.fnmatch_lines(
+ [
+ "tmp_path_factory [[]session scope[]] -- .../_pytest/tmpdir.py:*",
+ "*for the test session*",
+ "tmp_path -- .../_pytest/tmpdir.py:*",
+ "*temporary directory*",
+ ]
+ )
+
+ def test_show_fixtures_verbose(self, pytester: Pytester) -> None:
+ result = pytester.runpytest("--fixtures", "-v")
+ result.stdout.fnmatch_lines(
+ [
+ "tmp_path_factory [[]session scope[]] -- .../_pytest/tmpdir.py:*",
+ "*for the test session*",
+ "tmp_path -- .../_pytest/tmpdir.py:*",
+ "*temporary directory*",
+ ]
+ )
+
+ def test_show_fixtures_testmodule(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ '''
+ import pytest
+ @pytest.fixture
+ def _arg0():
+ """ hidden """
+ @pytest.fixture
+ def arg1():
+ """ hello world """
+ '''
+ )
+ result = pytester.runpytest("--fixtures", p)
+ result.stdout.fnmatch_lines(
+ """
+ *tmp_path -- *
+ *fixtures defined from*
+ *arg1 -- test_show_fixtures_testmodule.py:6*
+ *hello world*
+ """
+ )
+ result.stdout.no_fnmatch_line("*arg0*")
+
+ @pytest.mark.parametrize("testmod", [True, False])
+ def test_show_fixtures_conftest(self, pytester: Pytester, testmod) -> None:
+ pytester.makeconftest(
+ '''
+ import pytest
+ @pytest.fixture
+ def arg1():
+ """ hello world """
+ '''
+ )
+ if testmod:
+ pytester.makepyfile(
+ """
+ def test_hello():
+ pass
+ """
+ )
+ result = pytester.runpytest("--fixtures")
+ result.stdout.fnmatch_lines(
+ """
+ *tmp_path*
+ *fixtures defined from*conftest*
+ *arg1*
+ *hello world*
+ """
+ )
+
+ def test_show_fixtures_trimmed_doc(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ textwrap.dedent(
+ '''\
+ import pytest
+ @pytest.fixture
+ def arg1():
+ """
+ line1
+ line2
+
+ """
+ @pytest.fixture
+ def arg2():
+ """
+ line1
+ line2
+
+ """
+ '''
+ )
+ )
+ result = pytester.runpytest("--fixtures", p)
+ result.stdout.fnmatch_lines(
+ textwrap.dedent(
+ """\
+ * fixtures defined from test_show_fixtures_trimmed_doc *
+ arg2 -- test_show_fixtures_trimmed_doc.py:10
+ line1
+ line2
+ arg1 -- test_show_fixtures_trimmed_doc.py:3
+ line1
+ line2
+ """
+ )
+ )
+
+ def test_show_fixtures_indented_doc(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ textwrap.dedent(
+ '''\
+ import pytest
+ @pytest.fixture
+ def fixture1():
+ """
+ line1
+ indented line
+ """
+ '''
+ )
+ )
+ result = pytester.runpytest("--fixtures", p)
+ result.stdout.fnmatch_lines(
+ textwrap.dedent(
+ """\
+ * fixtures defined from test_show_fixtures_indented_doc *
+ fixture1 -- test_show_fixtures_indented_doc.py:3
+ line1
+ indented line
+ """
+ )
+ )
+
+ def test_show_fixtures_indented_doc_first_line_unindented(
+ self, pytester: Pytester
+ ) -> None:
+ p = pytester.makepyfile(
+ textwrap.dedent(
+ '''\
+ import pytest
+ @pytest.fixture
+ def fixture1():
+ """line1
+ line2
+ indented line
+ """
+ '''
+ )
+ )
+ result = pytester.runpytest("--fixtures", p)
+ result.stdout.fnmatch_lines(
+ textwrap.dedent(
+ """\
+ * fixtures defined from test_show_fixtures_indented_doc_first_line_unindented *
+ fixture1 -- test_show_fixtures_indented_doc_first_line_unindented.py:3
+ line1
+ line2
+ indented line
+ """
+ )
+ )
+
+ def test_show_fixtures_indented_in_class(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ textwrap.dedent(
+ '''\
+ import pytest
+ class TestClass(object):
+ @pytest.fixture
+ def fixture1(self):
+ """line1
+ line2
+ indented line
+ """
+ '''
+ )
+ )
+ result = pytester.runpytest("--fixtures", p)
+ result.stdout.fnmatch_lines(
+ textwrap.dedent(
+ """\
+ * fixtures defined from test_show_fixtures_indented_in_class *
+ fixture1 -- test_show_fixtures_indented_in_class.py:4
+ line1
+ line2
+ indented line
+ """
+ )
+ )
+
+ def test_show_fixtures_different_files(self, pytester: Pytester) -> None:
+ """`--fixtures` only shows fixtures from first file (#833)."""
+ pytester.makepyfile(
+ test_a='''
+ import pytest
+
+ @pytest.fixture
+ def fix_a():
+ """Fixture A"""
+ pass
+
+ def test_a(fix_a):
+ pass
+ '''
+ )
+ pytester.makepyfile(
+ test_b='''
+ import pytest
+
+ @pytest.fixture
+ def fix_b():
+ """Fixture B"""
+ pass
+
+ def test_b(fix_b):
+ pass
+ '''
+ )
+ result = pytester.runpytest("--fixtures")
+ result.stdout.fnmatch_lines(
+ """
+ * fixtures defined from test_a *
+ fix_a -- test_a.py:4
+ Fixture A
+
+ * fixtures defined from test_b *
+ fix_b -- test_b.py:4
+ Fixture B
+ """
+ )
+
+ def test_show_fixtures_with_same_name(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ '''
+ import pytest
+ @pytest.fixture
+ def arg1():
+ """Hello World in conftest.py"""
+ return "Hello World"
+ '''
+ )
+ pytester.makepyfile(
+ """
+ def test_foo(arg1):
+ assert arg1 == "Hello World"
+ """
+ )
+ pytester.makepyfile(
+ '''
+ import pytest
+ @pytest.fixture
+ def arg1():
+ """Hi from test module"""
+ return "Hi"
+ def test_bar(arg1):
+ assert arg1 == "Hi"
+ '''
+ )
+ result = pytester.runpytest("--fixtures")
+ result.stdout.fnmatch_lines(
+ """
+ * fixtures defined from conftest *
+ arg1 -- conftest.py:3
+ Hello World in conftest.py
+
+ * fixtures defined from test_show_fixtures_with_same_name *
+ arg1 -- test_show_fixtures_with_same_name.py:3
+ Hi from test module
+ """
+ )
+
+ def test_fixture_disallow_twice(self):
+ """Test that applying @pytest.fixture twice generates an error (#2334)."""
+ with pytest.raises(ValueError):
+
+ @pytest.fixture
+ @pytest.fixture
+ def foo():
+ raise NotImplementedError()
+
+
+class TestContextManagerFixtureFuncs:
+ def test_simple(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture
+ def arg1():
+ print("setup")
+ yield 1
+ print("teardown")
+ def test_1(arg1):
+ print("test1", arg1)
+ def test_2(arg1):
+ print("test2", arg1)
+ assert 0
+ """
+ )
+ result = pytester.runpytest("-s")
+ result.stdout.fnmatch_lines(
+ """
+ *setup*
+ *test1 1*
+ *teardown*
+ *setup*
+ *test2 1*
+ *teardown*
+ """
+ )
+
+ def test_scoped(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope="module")
+ def arg1():
+ print("setup")
+ yield 1
+ print("teardown")
+ def test_1(arg1):
+ print("test1", arg1)
+ def test_2(arg1):
+ print("test2", arg1)
+ """
+ )
+ result = pytester.runpytest("-s")
+ result.stdout.fnmatch_lines(
+ """
+ *setup*
+ *test1 1*
+ *test2 1*
+ *teardown*
+ """
+ )
+
+ def test_setup_exception(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope="module")
+ def arg1():
+ pytest.fail("setup")
+ yield 1
+ def test_1(arg1):
+ pass
+ """
+ )
+ result = pytester.runpytest("-s")
+ result.stdout.fnmatch_lines(
+ """
+ *pytest.fail*setup*
+ *1 error*
+ """
+ )
+
+ def test_teardown_exception(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope="module")
+ def arg1():
+ yield 1
+ pytest.fail("teardown")
+ def test_1(arg1):
+ pass
+ """
+ )
+ result = pytester.runpytest("-s")
+ result.stdout.fnmatch_lines(
+ """
+ *pytest.fail*teardown*
+ *1 passed*1 error*
+ """
+ )
+
+ def test_yields_more_than_one(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope="module")
+ def arg1():
+ yield 1
+ yield 2
+ def test_1(arg1):
+ pass
+ """
+ )
+ result = pytester.runpytest("-s")
+ result.stdout.fnmatch_lines(
+ """
+ *fixture function*
+ *test_yields*:2*
+ """
+ )
+
+ def test_custom_name(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(name='meow')
+ def arg1():
+ return 'mew'
+ def test_1(meow):
+ print(meow)
+ """
+ )
+ result = pytester.runpytest("-s")
+ result.stdout.fnmatch_lines(["*mew*"])
+
+
+class TestParameterizedSubRequest:
+ def test_call_from_fixture(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_call_from_fixture="""
+ import pytest
+
+ @pytest.fixture(params=[0, 1, 2])
+ def fix_with_param(request):
+ return request.param
+
+ @pytest.fixture
+ def get_named_fixture(request):
+ return request.getfixturevalue('fix_with_param')
+
+ def test_foo(request, get_named_fixture):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "The requested fixture has no parameter defined for test:",
+ " test_call_from_fixture.py::test_foo",
+ "Requested fixture 'fix_with_param' defined in:",
+ "test_call_from_fixture.py:4",
+ "Requested here:",
+ "test_call_from_fixture.py:9",
+ "*1 error in*",
+ ]
+ )
+
+ def test_call_from_test(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_call_from_test="""
+ import pytest
+
+ @pytest.fixture(params=[0, 1, 2])
+ def fix_with_param(request):
+ return request.param
+
+ def test_foo(request):
+ request.getfixturevalue('fix_with_param')
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "The requested fixture has no parameter defined for test:",
+ " test_call_from_test.py::test_foo",
+ "Requested fixture 'fix_with_param' defined in:",
+ "test_call_from_test.py:4",
+ "Requested here:",
+ "test_call_from_test.py:8",
+ "*1 failed*",
+ ]
+ )
+
+ def test_external_fixture(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(params=[0, 1, 2])
+ def fix_with_param(request):
+ return request.param
+ """
+ )
+
+ pytester.makepyfile(
+ test_external_fixture="""
+ def test_foo(request):
+ request.getfixturevalue('fix_with_param')
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "The requested fixture has no parameter defined for test:",
+ " test_external_fixture.py::test_foo",
+ "",
+ "Requested fixture 'fix_with_param' defined in:",
+ "conftest.py:4",
+ "Requested here:",
+ "test_external_fixture.py:2",
+ "*1 failed*",
+ ]
+ )
+
+ def test_non_relative_path(self, pytester: Pytester) -> None:
+ tests_dir = pytester.mkdir("tests")
+ fixdir = pytester.mkdir("fixtures")
+ fixfile = fixdir.joinpath("fix.py")
+ fixfile.write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ @pytest.fixture(params=[0, 1, 2])
+ def fix_with_param(request):
+ return request.param
+ """
+ )
+ )
+
+ testfile = tests_dir.joinpath("test_foos.py")
+ testfile.write_text(
+ textwrap.dedent(
+ """\
+ from fix import fix_with_param
+
+ def test_foo(request):
+ request.getfixturevalue('fix_with_param')
+ """
+ )
+ )
+
+ os.chdir(tests_dir)
+ pytester.syspathinsert(fixdir)
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "The requested fixture has no parameter defined for test:",
+ " test_foos.py::test_foo",
+ "",
+ "Requested fixture 'fix_with_param' defined in:",
+ f"{fixfile}:4",
+ "Requested here:",
+ "test_foos.py:4",
+ "*1 failed*",
+ ]
+ )
+
+ # With non-overlapping rootdir, passing tests_dir.
+ rootdir = pytester.mkdir("rootdir")
+ os.chdir(rootdir)
+ result = pytester.runpytest("--rootdir", rootdir, tests_dir)
+ result.stdout.fnmatch_lines(
+ [
+ "The requested fixture has no parameter defined for test:",
+ " test_foos.py::test_foo",
+ "",
+ "Requested fixture 'fix_with_param' defined in:",
+ f"{fixfile}:4",
+ "Requested here:",
+ f"{testfile}:4",
+ "*1 failed*",
+ ]
+ )
+
+
+def test_pytest_fixture_setup_and_post_finalizer_hook(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_fixture_setup(fixturedef, request):
+ print('ROOT setup hook called for {0} from {1}'.format(fixturedef.argname, request.node.name))
+ def pytest_fixture_post_finalizer(fixturedef, request):
+ print('ROOT finalizer hook called for {0} from {1}'.format(fixturedef.argname, request.node.name))
+ """
+ )
+ pytester.makepyfile(
+ **{
+ "tests/conftest.py": """
+ def pytest_fixture_setup(fixturedef, request):
+ print('TESTS setup hook called for {0} from {1}'.format(fixturedef.argname, request.node.name))
+ def pytest_fixture_post_finalizer(fixturedef, request):
+ print('TESTS finalizer hook called for {0} from {1}'.format(fixturedef.argname, request.node.name))
+ """,
+ "tests/test_hooks.py": """
+ import pytest
+
+ @pytest.fixture()
+ def my_fixture():
+ return 'some'
+
+ def test_func(my_fixture):
+ print('TEST test_func')
+ assert my_fixture == 'some'
+ """,
+ }
+ )
+ result = pytester.runpytest("-s")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(
+ [
+ "*TESTS setup hook called for my_fixture from test_func*",
+ "*ROOT setup hook called for my_fixture from test_func*",
+ "*TEST test_func*",
+ "*TESTS finalizer hook called for my_fixture from test_func*",
+ "*ROOT finalizer hook called for my_fixture from test_func*",
+ ]
+ )
+
+
+class TestScopeOrdering:
+ """Class of tests that ensure fixtures are ordered based on their scopes (#2405)"""
+
+ @pytest.mark.parametrize("variant", ["mark", "autouse"])
+ def test_func_closure_module_auto(
+ self, pytester: Pytester, variant, monkeypatch
+ ) -> None:
+ """Semantically identical to the example posted in #2405 when ``use_mark=True``"""
+ monkeypatch.setenv("FIXTURE_ACTIVATION_VARIANT", variant)
+ pytester.makepyfile(
+ """
+ import warnings
+ import os
+ import pytest
+ VAR = 'FIXTURE_ACTIVATION_VARIANT'
+ VALID_VARS = ('autouse', 'mark')
+
+ VARIANT = os.environ.get(VAR)
+ if VARIANT is None or VARIANT not in VALID_VARS:
+ warnings.warn("{!r} is not in {}, assuming autouse".format(VARIANT, VALID_VARS) )
+ variant = 'mark'
+
+ @pytest.fixture(scope='module', autouse=VARIANT == 'autouse')
+ def m1(): pass
+
+ if VARIANT=='mark':
+ pytestmark = pytest.mark.usefixtures('m1')
+
+ @pytest.fixture(scope='function', autouse=True)
+ def f1(): pass
+
+ def test_func(m1):
+ pass
+ """
+ )
+ items, _ = pytester.inline_genitems()
+ request = FixtureRequest(items[0], _ispytest=True)
+ assert request.fixturenames == "m1 f1".split()
+
+ def test_func_closure_with_native_fixtures(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ """Sanity check that verifies the order returned by the closures and the actual fixture execution order:
+ The execution order may differ because of fixture inter-dependencies.
+ """
+ monkeypatch.setattr(pytest, "FIXTURE_ORDER", [], raising=False)
+ pytester.makepyfile(
+ """
+ import pytest
+
+ FIXTURE_ORDER = pytest.FIXTURE_ORDER
+
+ @pytest.fixture(scope="session")
+ def s1():
+ FIXTURE_ORDER.append('s1')
+
+ @pytest.fixture(scope="package")
+ def p1():
+ FIXTURE_ORDER.append('p1')
+
+ @pytest.fixture(scope="module")
+ def m1():
+ FIXTURE_ORDER.append('m1')
+
+ @pytest.fixture(scope='session')
+ def my_tmp_path_factory():
+ FIXTURE_ORDER.append('my_tmp_path_factory')
+
+ @pytest.fixture
+ def my_tmp_path(my_tmp_path_factory):
+ FIXTURE_ORDER.append('my_tmp_path')
+
+ @pytest.fixture
+ def f1(my_tmp_path):
+ FIXTURE_ORDER.append('f1')
+
+ @pytest.fixture
+ def f2():
+ FIXTURE_ORDER.append('f2')
+
+ def test_foo(f1, p1, m1, f2, s1): pass
+ """
+ )
+ items, _ = pytester.inline_genitems()
+ request = FixtureRequest(items[0], _ispytest=True)
+ # order of fixtures based on their scope and position in the parameter list
+ assert (
+ request.fixturenames
+ == "s1 my_tmp_path_factory p1 m1 f1 f2 my_tmp_path".split()
+ )
+ pytester.runpytest()
+ # actual fixture execution differs: dependent fixtures must be created first ("my_tmp_path")
+ FIXTURE_ORDER = pytest.FIXTURE_ORDER # type: ignore[attr-defined]
+ assert FIXTURE_ORDER == "s1 my_tmp_path_factory p1 m1 my_tmp_path f1 f2".split()
+
+ def test_func_closure_module(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope='module')
+ def m1(): pass
+
+ @pytest.fixture(scope='function')
+ def f1(): pass
+
+ def test_func(f1, m1):
+ pass
+ """
+ )
+ items, _ = pytester.inline_genitems()
+ request = FixtureRequest(items[0], _ispytest=True)
+ assert request.fixturenames == "m1 f1".split()
+
+ def test_func_closure_scopes_reordered(self, pytester: Pytester) -> None:
+ """Test ensures that fixtures are ordered by scope regardless of the order of the parameters, although
+ fixtures of same scope keep the declared order
+ """
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope='session')
+ def s1(): pass
+
+ @pytest.fixture(scope='module')
+ def m1(): pass
+
+ @pytest.fixture(scope='function')
+ def f1(): pass
+
+ @pytest.fixture(scope='function')
+ def f2(): pass
+
+ class Test:
+
+ @pytest.fixture(scope='class')
+ def c1(cls): pass
+
+ def test_func(self, f2, f1, c1, m1, s1):
+ pass
+ """
+ )
+ items, _ = pytester.inline_genitems()
+ request = FixtureRequest(items[0], _ispytest=True)
+ assert request.fixturenames == "s1 m1 c1 f2 f1".split()
+
+ def test_func_closure_same_scope_closer_root_first(
+ self, pytester: Pytester
+ ) -> None:
+ """Auto-use fixtures of same scope are ordered by closer-to-root first"""
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(scope='module', autouse=True)
+ def m_conf(): pass
+ """
+ )
+ pytester.makepyfile(
+ **{
+ "sub/conftest.py": """
+ import pytest
+
+ @pytest.fixture(scope='package', autouse=True)
+ def p_sub(): pass
+
+ @pytest.fixture(scope='module', autouse=True)
+ def m_sub(): pass
+ """,
+ "sub/__init__.py": "",
+ "sub/test_func.py": """
+ import pytest
+
+ @pytest.fixture(scope='module', autouse=True)
+ def m_test(): pass
+
+ @pytest.fixture(scope='function')
+ def f1(): pass
+
+ def test_func(m_test, f1):
+ pass
+ """,
+ }
+ )
+ items, _ = pytester.inline_genitems()
+ request = FixtureRequest(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:
+ """Complex test involving all scopes and mixing autouse with normal fixtures"""
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(scope='session')
+ def s1(): pass
+
+ @pytest.fixture(scope='package', autouse=True)
+ def p1(): pass
+ """
+ )
+ pytester.makepyfile(**{"__init__.py": ""})
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope='module', autouse=True)
+ def m1(): pass
+
+ @pytest.fixture(scope='module')
+ def m2(s1): pass
+
+ @pytest.fixture(scope='function')
+ def f1(): pass
+
+ @pytest.fixture(scope='function')
+ def f2(): pass
+
+ class Test:
+
+ @pytest.fixture(scope='class', autouse=True)
+ def c1(self):
+ pass
+
+ def test_func(self, f2, f1, m2):
+ pass
+ """
+ )
+ items, _ = pytester.inline_genitems()
+ request = FixtureRequest(items[0], _ispytest=True)
+ assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split()
+
+ def test_multiple_packages(self, pytester: Pytester) -> None:
+ """Complex test involving multiple package fixtures. Make sure teardowns
+ are executed in order.
+ .
+ └── root
+ ├── __init__.py
+ ├── sub1
+ │ ├── __init__.py
+ │ ├── conftest.py
+ │ └── test_1.py
+ └── sub2
+ ├── __init__.py
+ ├── conftest.py
+ └── test_2.py
+ """
+ root = pytester.mkdir("root")
+ root.joinpath("__init__.py").write_text("values = []")
+ sub1 = root.joinpath("sub1")
+ sub1.mkdir()
+ sub1.joinpath("__init__.py").touch()
+ sub1.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ from .. import values
+ @pytest.fixture(scope="package")
+ def fix():
+ values.append("pre-sub1")
+ yield values
+ assert values.pop() == "pre-sub1"
+ """
+ )
+ )
+ sub1.joinpath("test_1.py").write_text(
+ textwrap.dedent(
+ """\
+ from .. import values
+ def test_1(fix):
+ assert values == ["pre-sub1"]
+ """
+ )
+ )
+ sub2 = root.joinpath("sub2")
+ sub2.mkdir()
+ sub2.joinpath("__init__.py").touch()
+ sub2.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ from .. import values
+ @pytest.fixture(scope="package")
+ def fix():
+ values.append("pre-sub2")
+ yield values
+ assert values.pop() == "pre-sub2"
+ """
+ )
+ )
+ sub2.joinpath("test_2.py").write_text(
+ textwrap.dedent(
+ """\
+ from .. import values
+ def test_2(fix):
+ assert values == ["pre-sub2"]
+ """
+ )
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ def test_class_fixture_self_instance(self, pytester: Pytester) -> None:
+ """Check that plugin classes which implement fixtures receive the plugin instance
+ as self (see #2270).
+ """
+ pytester.makeconftest(
+ """
+ import pytest
+
+ def pytest_configure(config):
+ config.pluginmanager.register(MyPlugin())
+
+ class MyPlugin():
+ def __init__(self):
+ self.arg = 1
+
+ @pytest.fixture(scope='function')
+ def myfix(self):
+ assert isinstance(self, MyPlugin)
+ return self.arg
+ """
+ )
+
+ pytester.makepyfile(
+ """
+ class TestClass(object):
+ def test_1(self, myfix):
+ assert myfix == 1
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+def test_call_fixture_function_error():
+ """Check if an error is raised if a fixture function is called directly (#4545)"""
+
+ @pytest.fixture
+ def fix():
+ raise NotImplementedError()
+
+ with pytest.raises(pytest.fail.Exception):
+ assert fix() == 1
+
+
+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(
+ """
+ import pytest
+
+ @pytest.fixture(params=['a', 'b'])
+ def argroot(request):
+ return request.param
+
+ @pytest.fixture
+ def arg(argroot):
+ return argroot
+
+ # This should only be parametrized directly
+ @pytest.mark.parametrize("arg", [1])
+ def test_direct(arg):
+ assert arg == 1
+
+ # This should be parametrized based on the fixtures
+ def test_normal_fixture(arg):
+ assert isinstance(arg, str)
+
+ # Indirect should still work:
+
+ @pytest.fixture
+ def arg2(request):
+ return 2*request.param
+
+ @pytest.mark.parametrize("arg2", [1], indirect=True)
+ def test_indirect(arg2):
+ assert arg2 == 2
+ """
+ )
+ # Only one test should have run
+ result = pytester.runpytest("-v")
+ result.assert_outcomes(passed=4)
+ result.stdout.fnmatch_lines(["*::test_direct[[]1[]]*"])
+ result.stdout.fnmatch_lines(["*::test_normal_fixture[[]a[]]*"])
+ result.stdout.fnmatch_lines(["*::test_normal_fixture[[]b[]]*"])
+ result.stdout.fnmatch_lines(["*::test_indirect[[]1[]]*"])
+
+
+def test_fixture_named_request(pytester: Pytester) -> None:
+ pytester.copy_example("fixtures/test_fixture_named_request.py")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*'request' is a reserved word for fixtures, use another name:",
+ " *test_fixture_named_request.py:5",
+ ]
+ )
+
+
+def test_indirect_fixture_does_not_break_scope(pytester: Pytester) -> None:
+ """Ensure that fixture scope is respected when using indirect fixtures (#570)"""
+ pytester.makepyfile(
+ """
+ import pytest
+ instantiated = []
+
+ @pytest.fixture(scope="session")
+ def fixture_1(request):
+ instantiated.append(("fixture_1", request.param))
+
+
+ @pytest.fixture(scope="session")
+ def fixture_2(request):
+ instantiated.append(("fixture_2", request.param))
+
+
+ scenarios = [
+ ("A", "a1"),
+ ("A", "a2"),
+ ("B", "b1"),
+ ("B", "b2"),
+ ("C", "c1"),
+ ("C", "c2"),
+ ]
+
+ @pytest.mark.parametrize(
+ "fixture_1,fixture_2", scenarios, indirect=["fixture_1", "fixture_2"]
+ )
+ def test_create_fixtures(fixture_1, fixture_2):
+ pass
+
+
+ def test_check_fixture_instantiations():
+ assert instantiated == [
+ ('fixture_1', 'A'),
+ ('fixture_2', 'a1'),
+ ('fixture_2', 'a2'),
+ ('fixture_1', 'B'),
+ ('fixture_2', 'b1'),
+ ('fixture_2', 'b2'),
+ ('fixture_1', 'C'),
+ ('fixture_2', 'c1'),
+ ('fixture_2', 'c2'),
+ ]
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=7)
+
+
+def test_fixture_parametrization_nparray(pytester: Pytester) -> None:
+ pytest.importorskip("numpy")
+
+ pytester.makepyfile(
+ """
+ from numpy import linspace
+ from pytest import fixture
+
+ @fixture(params=linspace(1, 10, 10))
+ def value(request):
+ return request.param
+
+ def test_bug(value):
+ assert value == value
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=10)
+
+
+def test_fixture_arg_ordering(pytester: Pytester) -> None:
+ """
+ This test describes how fixtures in the same scope but without explicit dependencies
+ between them are created. While users should make dependencies explicit, often
+ they rely on this order, so this test exists to catch regressions in this regard.
+ See #6540 and #6492.
+ """
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+
+ suffixes = []
+
+ @pytest.fixture
+ def fix_1(): suffixes.append("fix_1")
+ @pytest.fixture
+ def fix_2(): suffixes.append("fix_2")
+ @pytest.fixture
+ def fix_3(): suffixes.append("fix_3")
+ @pytest.fixture
+ def fix_4(): suffixes.append("fix_4")
+ @pytest.fixture
+ def fix_5(): suffixes.append("fix_5")
+
+ @pytest.fixture
+ def fix_combined(fix_1, fix_2, fix_3, fix_4, fix_5): pass
+
+ def test_suffix(fix_combined):
+ assert suffixes == ["fix_1", "fix_2", "fix_3", "fix_4", "fix_5"]
+ """
+ )
+ result = pytester.runpytest("-vv", str(p1))
+ assert result.ret == 0
+
+
+def test_yield_fixture_with_no_value(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(name='custom')
+ def empty_yield():
+ if False:
+ yield
+
+ def test_fixt(custom):
+ pass
+ """
+ )
+ expected = "E ValueError: custom did not yield a value"
+ result = pytester.runpytest()
+ result.assert_outcomes(errors=1)
+ result.stdout.fnmatch_lines([expected])
+ assert result.ret == ExitCode.TESTS_FAILED
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/python/integration.py b/testing/web-platform/tests/tools/third_party/pytest/testing/python/integration.py
new file mode 100644
index 0000000000..d138b72663
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/python/integration.py
@@ -0,0 +1,503 @@
+from typing import Any
+
+import pytest
+from _pytest import runner
+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
+
+
+def test_wrapped_getfslineno() -> None:
+ def func():
+ pass
+
+ def wrap(f):
+ func.__wrapped__ = f # type: ignore
+ func.patchings = ["qwe"] # type: ignore
+ return func
+
+ @wrap
+ def wrapped_func(x, y, z):
+ pass
+
+ fs, lineno = getfslineno(wrapped_func)
+ fs2, lineno2 = getfslineno(wrap)
+ assert lineno > lineno2, "getfslineno does not unwrap correctly"
+
+
+class TestMockDecoration:
+ def test_wrapped_getfuncargnames(self) -> None:
+ from _pytest.compat import getfuncargnames
+
+ def wrap(f):
+ def func():
+ pass
+
+ func.__wrapped__ = f # type: ignore
+ return func
+
+ @wrap
+ def f(x):
+ pass
+
+ values = getfuncargnames(f)
+ assert values == ("x",)
+
+ def test_getfuncargnames_patching(self):
+ from _pytest.compat import getfuncargnames
+ from unittest.mock import patch
+
+ class T:
+ def original(self, x, y, z):
+ pass
+
+ @patch.object(T, "original")
+ def f(x, y, z):
+ pass
+
+ values = getfuncargnames(f)
+ assert values == ("y", "z")
+
+ def test_unittest_mock(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import unittest.mock
+ class T(unittest.TestCase):
+ @unittest.mock.patch("os.path.abspath")
+ def test_hello(self, abspath):
+ import os
+ os.path.abspath("hello")
+ abspath.assert_any_call("hello")
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_unittest_mock_and_fixture(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import os.path
+ import unittest.mock
+ import pytest
+
+ @pytest.fixture
+ def inject_me():
+ pass
+
+ @unittest.mock.patch.object(os.path, "abspath",
+ new=unittest.mock.MagicMock)
+ def test_hello(inject_me):
+ import os
+ os.path.abspath("hello")
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_unittest_mock_and_pypi_mock(self, pytester: Pytester) -> None:
+ pytest.importorskip("mock", "1.0.1")
+ pytester.makepyfile(
+ """
+ import mock
+ import unittest.mock
+ class TestBoth(object):
+ @unittest.mock.patch("os.path.abspath")
+ def test_hello(self, abspath):
+ import os
+ os.path.abspath("hello")
+ abspath.assert_any_call("hello")
+
+ @mock.patch("os.path.abspath")
+ def test_hello_mock(self, abspath):
+ import os
+ os.path.abspath("hello")
+ abspath.assert_any_call("hello")
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ def test_mock_sentinel_check_against_numpy_like(self, pytester: Pytester) -> None:
+ """Ensure our function that detects mock arguments compares against sentinels using
+ identity to circumvent objects which can't be compared with equality against others
+ in a truth context, like with numpy arrays (#5606).
+ """
+ pytester.makepyfile(
+ dummy="""
+ class NumpyLike:
+ def __init__(self, value):
+ self.value = value
+ def __eq__(self, other):
+ raise ValueError("like numpy, cannot compare against others for truth")
+ FOO = NumpyLike(10)
+ """
+ )
+ pytester.makepyfile(
+ """
+ from unittest.mock import patch
+ import dummy
+ class Test(object):
+ @patch("dummy.FOO", new=dummy.NumpyLike(50))
+ def test_hello(self):
+ assert dummy.FOO.value == 50
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_mock(self, pytester: Pytester) -> None:
+ pytest.importorskip("mock", "1.0.1")
+ pytester.makepyfile(
+ """
+ import os
+ import unittest
+ import mock
+
+ class T(unittest.TestCase):
+ @mock.patch("os.path.abspath")
+ def test_hello(self, abspath):
+ os.path.abspath("hello")
+ abspath.assert_any_call("hello")
+ def mock_basename(path):
+ return "mock_basename"
+ @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):
+ abspath.return_value = "this"
+ os.path.normpath(os.path.abspath("hello"))
+ normpath.assert_any_call("this")
+ assert os.path.basename("123") == "mock_basename"
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+ calls = reprec.getcalls("pytest_runtest_logreport")
+ funcnames = [
+ call.report.location[2] for call in calls if call.report.when == "call"
+ ]
+ assert funcnames == ["T.test_hello", "test_someting"]
+
+ def test_mock_sorting(self, pytester: Pytester) -> None:
+ pytest.importorskip("mock", "1.0.1")
+ pytester.makepyfile(
+ """
+ import os
+ import mock
+
+ @mock.patch("os.path.abspath")
+ def test_one(abspath):
+ pass
+ @mock.patch("os.path.abspath")
+ def test_two(abspath):
+ pass
+ @mock.patch("os.path.abspath")
+ def test_three(abspath):
+ pass
+ """
+ )
+ reprec = pytester.inline_run()
+ calls = reprec.getreports("pytest_runtest_logreport")
+ calls = [x for x in calls if x.when == "call"]
+ names = [x.nodeid.split("::")[-1] for x in calls]
+ assert names == ["test_one", "test_two", "test_three"]
+
+ def test_mock_double_patch_issue473(self, pytester: Pytester) -> None:
+ pytest.importorskip("mock", "1.0.1")
+ pytester.makepyfile(
+ """
+ from mock import patch
+ from pytest import mark
+
+ @patch('os.getcwd')
+ @patch('os.path')
+ @mark.slow
+ class TestSimple(object):
+ def test_simple_thing(self, mock_path, mock_getcwd):
+ pass
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+class TestReRunTests:
+ def test_rerun(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ from _pytest.runner import runtestprotocol
+ def pytest_runtest_protocol(item, nextitem):
+ runtestprotocol(item, log=False, nextitem=nextitem)
+ runtestprotocol(item, log=True, nextitem=nextitem)
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+ count = 0
+ req = None
+ @pytest.fixture
+ def fix(request):
+ global count, req
+ assert request != req
+ req = request
+ print("fix count %s" % count)
+ count += 1
+ def test_fix(fix):
+ pass
+ """
+ )
+ result = pytester.runpytest("-s")
+ result.stdout.fnmatch_lines(
+ """
+ *fix count 0*
+ *fix count 1*
+ """
+ )
+ result.stdout.fnmatch_lines(
+ """
+ *2 passed*
+ """
+ )
+
+
+def test_pytestconfig_is_session_scoped() -> None:
+ from _pytest.fixtures import pytestconfig
+
+ marker = getfixturemarker(pytestconfig)
+ assert marker is not None
+ assert marker.scope == "session"
+
+
+class TestNoselikeTestAttribute:
+ def test_module_with_global_test(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ __test__ = False
+ def test_hello():
+ pass
+ """
+ )
+ reprec = pytester.inline_run()
+ assert not reprec.getfailedcollections()
+ calls = reprec.getreports("pytest_runtest_logreport")
+ assert not calls
+
+ def test_class_and_method(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ __test__ = True
+ def test_func():
+ pass
+ test_func.__test__ = False
+
+ class TestSome(object):
+ __test__ = False
+ def test_method(self):
+ pass
+ """
+ )
+ reprec = pytester.inline_run()
+ assert not reprec.getfailedcollections()
+ calls = reprec.getreports("pytest_runtest_logreport")
+ assert not calls
+
+ def test_unittest_class(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import unittest
+ class TC(unittest.TestCase):
+ def test_1(self):
+ pass
+ class TC2(unittest.TestCase):
+ __test__ = False
+ def test_2(self):
+ pass
+ """
+ )
+ reprec = pytester.inline_run()
+ assert not reprec.getfailedcollections()
+ call = reprec.getcalls("pytest_collection_modifyitems")[0]
+ assert len(call.items) == 1
+ assert call.items[0].cls.__name__ == "TC"
+
+ def test_class_with_nasty_getattr(self, pytester: Pytester) -> None:
+ """Make sure we handle classes with a custom nasty __getattr__ right.
+
+ With a custom __getattr__ which e.g. returns a function (like with a
+ RPC wrapper), we shouldn't assume this meant "__test__ = True".
+ """
+ # https://github.com/pytest-dev/pytest/issues/1204
+ pytester.makepyfile(
+ """
+ class MetaModel(type):
+
+ def __getattr__(cls, key):
+ return lambda: None
+
+
+ BaseModel = MetaModel('Model', (), {})
+
+
+ class Model(BaseModel):
+
+ __metaclass__ = MetaModel
+
+ def test_blah(self):
+ pass
+ """
+ )
+ reprec = pytester.inline_run()
+ assert not reprec.getfailedcollections()
+ call = reprec.getcalls("pytest_collection_modifyitems")[0]
+ assert not call.items
+
+
+class TestParameterize:
+ """#351"""
+
+ def test_idfn_marker(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ def idfn(param):
+ if param == 0:
+ return 'spam'
+ elif param == 1:
+ return 'ham'
+ else:
+ return None
+
+ @pytest.mark.parametrize('a,b', [(0, 2), (1, 2)], ids=idfn)
+ def test_params(a, b):
+ pass
+ """
+ )
+ res = pytester.runpytest("--collect-only")
+ res.stdout.fnmatch_lines(["*spam-2*", "*ham-2*"])
+
+ def test_idfn_fixture(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ def idfn(param):
+ if param == 0:
+ return 'spam'
+ elif param == 1:
+ return 'ham'
+ else:
+ return None
+
+ @pytest.fixture(params=[0, 1], ids=idfn)
+ def a(request):
+ return request.param
+
+ @pytest.fixture(params=[1, 2], ids=idfn)
+ def b(request):
+ return request.param
+
+ def test_params(a, b):
+ pass
+ """
+ )
+ res = pytester.runpytest("--collect-only")
+ res.stdout.fnmatch_lines(["*spam-2*", "*ham-2*"])
+
+
+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 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"
+ assert isinstance(items[2], Function)
+ assert items[2].name == "test_static"
+ assert items[2].instance is None
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/python/metafunc.py b/testing/web-platform/tests/tools/third_party/pytest/testing/python/metafunc.py
new file mode 100644
index 0000000000..fc0082eb6b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/python/metafunc.py
@@ -0,0 +1,1907 @@
+import itertools
+import re
+import sys
+import textwrap
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import Iterator
+from typing import List
+from typing import Optional
+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.scope import Scope
+
+
+class TestMetafunc:
+ def Metafunc(self, func, config=None) -> python.Metafunc:
+ # The unit tests of this class check if things work correctly
+ # on the funcarg level, so we don't need a full blown
+ # initialization.
+ class FuncFixtureInfoMock:
+ name2fixturedefs = None
+
+ def __init__(self, names):
+ self.names_closure = names
+
+ @attr.s
+ class DefinitionMock(python.FunctionDefinition):
+ obj = attr.ib()
+ _nodeid = attr.ib()
+
+ names = getfuncargnames(func)
+ fixtureinfo: Any = FuncFixtureInfoMock(names)
+ definition: Any = DefinitionMock._create(func, "mock::nodeid")
+ return python.Metafunc(definition, fixtureinfo, config, _ispytest=True)
+
+ def test_no_funcargs(self) -> None:
+ def function():
+ pass
+
+ metafunc = self.Metafunc(function)
+ assert not metafunc.fixturenames
+ repr(metafunc._calls)
+
+ def test_function_basic(self) -> None:
+ def func(arg1, arg2="qwe"):
+ pass
+
+ metafunc = self.Metafunc(func)
+ assert len(metafunc.fixturenames) == 1
+ assert "arg1" in metafunc.fixturenames
+ assert metafunc.function is func
+ assert metafunc.cls is None
+
+ def test_parametrize_error(self) -> None:
+ def func(x, y):
+ pass
+
+ metafunc = self.Metafunc(func)
+ metafunc.parametrize("x", [1, 2])
+ pytest.raises(ValueError, lambda: metafunc.parametrize("x", [5, 6]))
+ pytest.raises(ValueError, lambda: metafunc.parametrize("x", [5, 6]))
+ metafunc.parametrize("y", [1, 2])
+ pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5, 6]))
+ pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5, 6]))
+
+ with pytest.raises(TypeError, match="^ids must be a callable or an iterable$"):
+ metafunc.parametrize("y", [5, 6], ids=42) # type: ignore[arg-type]
+
+ def test_parametrize_error_iterator(self) -> None:
+ def func(x):
+ raise NotImplementedError()
+
+ class Exc(Exception):
+ def __repr__(self):
+ return "Exc(from_gen)"
+
+ def gen() -> Iterator[Union[int, None, Exc]]:
+ yield 0
+ yield None
+ yield Exc()
+
+ 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] == [
+ ({"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"
+ ),
+ ):
+ metafunc.parametrize("x", [1, 2, 3], ids=gen()) # type: ignore[arg-type]
+
+ def test_parametrize_bad_scope(self) -> None:
+ def func(x):
+ pass
+
+ metafunc = self.Metafunc(func)
+ with pytest.raises(
+ fail.Exception,
+ match=r"parametrize\(\) call in func got an unexpected scope value 'doggy'",
+ ):
+ metafunc.parametrize("x", [1], scope="doggy") # type: ignore[arg-type]
+
+ def test_parametrize_request_name(self, pytester: Pytester) -> None:
+ """Show proper error when 'request' is used as a parameter name in parametrize (#6183)"""
+
+ def func(request):
+ raise NotImplementedError()
+
+ metafunc = self.Metafunc(func)
+ with pytest.raises(
+ fail.Exception,
+ match=r"'request' is a reserved name and cannot be used in @pytest.mark.parametrize",
+ ):
+ metafunc.parametrize("request", [1])
+
+ def test_find_parametrized_scope(self) -> None:
+ """Unit test for _find_parametrized_scope (#3941)."""
+ from _pytest.python import _find_parametrized_scope
+
+ @attr.s
+ class DummyFixtureDef:
+ _scope = attr.ib()
+
+ fixtures_defs = cast(
+ Dict[str, Sequence[fixtures.FixtureDef[object]]],
+ dict(
+ session_fix=[DummyFixtureDef(Scope.Session)],
+ package_fix=[DummyFixtureDef(Scope.Package)],
+ module_fix=[DummyFixtureDef(Scope.Module)],
+ class_fix=[DummyFixtureDef(Scope.Class)],
+ func_fix=[DummyFixtureDef(Scope.Function)],
+ ),
+ )
+
+ # use arguments to determine narrow scope; the cause of the bug is that it would look on all
+ # fixture defs given to the method
+ def find_scope(argnames, indirect):
+ return _find_parametrized_scope(argnames, fixtures_defs, indirect=indirect)
+
+ assert find_scope(["func_fix"], indirect=True) == Scope.Function
+ assert find_scope(["class_fix"], indirect=True) == Scope.Class
+ assert find_scope(["module_fix"], indirect=True) == Scope.Module
+ assert find_scope(["package_fix"], indirect=True) == Scope.Package
+ assert find_scope(["session_fix"], indirect=True) == Scope.Session
+
+ assert find_scope(["class_fix", "func_fix"], indirect=True) == Scope.Function
+ assert find_scope(["func_fix", "session_fix"], indirect=True) == Scope.Function
+ assert find_scope(["session_fix", "class_fix"], indirect=True) == Scope.Class
+ assert (
+ find_scope(["package_fix", "session_fix"], indirect=True) == Scope.Package
+ )
+ assert find_scope(["module_fix", "session_fix"], indirect=True) == Scope.Module
+
+ # when indirect is False or is not for all scopes, always use function
+ assert (
+ find_scope(["session_fix", "module_fix"], indirect=False) == Scope.Function
+ )
+ assert (
+ find_scope(["session_fix", "module_fix"], indirect=["module_fix"])
+ == Scope.Function
+ )
+ assert (
+ find_scope(
+ ["session_fix", "module_fix"], indirect=["session_fix", "module_fix"]
+ )
+ == Scope.Module
+ )
+
+ def test_parametrize_and_id(self) -> None:
+ def func(x, y):
+ pass
+
+ metafunc = self.Metafunc(func)
+
+ metafunc.parametrize("x", [1, 2], ids=["basic", "advanced"])
+ metafunc.parametrize("y", ["abc", "def"])
+ ids = [x.id for x in metafunc._calls]
+ assert ids == ["basic-abc", "basic-def", "advanced-abc", "advanced-def"]
+
+ def test_parametrize_and_id_unicode(self) -> None:
+ """Allow unicode strings for "ids" parameter in Python 2 (##1905)"""
+
+ def func(x):
+ pass
+
+ metafunc = self.Metafunc(func)
+ metafunc.parametrize("x", [1, 2], ids=["basic", "advanced"])
+ ids = [x.id for x in metafunc._calls]
+ assert ids == ["basic", "advanced"]
+
+ def test_parametrize_with_wrong_number_of_ids(self) -> None:
+ def func(x, y):
+ pass
+
+ metafunc = self.Metafunc(func)
+
+ with pytest.raises(fail.Exception):
+ metafunc.parametrize("x", [1, 2], ids=["basic"])
+
+ with pytest.raises(fail.Exception):
+ metafunc.parametrize(
+ ("x", "y"), [("abc", "def"), ("ghi", "jkl")], ids=["one"]
+ )
+
+ def test_parametrize_ids_iterator_without_mark(self) -> None:
+ def func(x, y):
+ pass
+
+ it = itertools.count()
+
+ metafunc = self.Metafunc(func)
+ metafunc.parametrize("x", [1, 2], ids=it)
+ metafunc.parametrize("y", [3, 4], ids=it)
+ ids = [x.id for x in metafunc._calls]
+ assert ids == ["0-2", "0-3", "1-2", "1-3"]
+
+ metafunc = self.Metafunc(func)
+ metafunc.parametrize("x", [1, 2], ids=it)
+ metafunc.parametrize("y", [3, 4], ids=it)
+ ids = [x.id for x in metafunc._calls]
+ assert ids == ["4-6", "4-7", "5-6", "5-7"]
+
+ def test_parametrize_empty_list(self) -> None:
+ """#510"""
+
+ def func(y):
+ pass
+
+ class MockConfig:
+ def getini(self, name):
+ return ""
+
+ @property
+ def hook(self):
+ return self
+
+ def pytest_make_parametrize_id(self, **kw):
+ pass
+
+ metafunc = self.Metafunc(func, MockConfig())
+ metafunc.parametrize("y", [])
+ assert "skip" == metafunc._calls[0].marks[0].name
+
+ def test_parametrize_with_userobjects(self) -> None:
+ def func(x, y):
+ pass
+
+ metafunc = self.Metafunc(func)
+
+ class A:
+ pass
+
+ metafunc.parametrize("x", [A(), A()])
+ metafunc.parametrize("y", list("ab"))
+ assert metafunc._calls[0].id == "x0-a"
+ assert metafunc._calls[1].id == "x0-b"
+ assert metafunc._calls[2].id == "x1-a"
+ assert metafunc._calls[3].id == "x1-b"
+
+ @hypothesis.given(strategies.text() | strategies.binary())
+ @hypothesis.settings(
+ 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)
+ assert isinstance(escaped, str)
+ escaped.encode("ascii")
+
+ def test_unicode_idval(self) -> None:
+ """Test that Unicode strings outside the ASCII character set get
+ escaped, using byte escapes if they're in that range or unicode
+ escapes if they're not.
+
+ """
+ values = [
+ ("", r""),
+ ("ascii", r"ascii"),
+ ("ação", r"a\xe7\xe3o"),
+ ("josé@blah.com", r"jos\xe9@blah.com"),
+ (
+ r"δοκ.ιμή@παράδειγμα.δοκιμή",
+ r"\u03b4\u03bf\u03ba.\u03b9\u03bc\u03ae@\u03c0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3"
+ r"\u03bc\u03b1.\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae",
+ ),
+ ]
+ for val, expected in values:
+ assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected
+
+ def test_unicode_idval_with_config(self) -> None:
+ """Unit test for expected behavior to obtain ids with
+ disable_test_id_escaping_and_forfeit_all_rights_to_community_support
+ option (#5294)."""
+
+ class MockConfig:
+ def __init__(self, config):
+ self.config = config
+
+ @property
+ def hook(self):
+ return self
+
+ def pytest_make_parametrize_id(self, **kw):
+ pass
+
+ def getini(self, name):
+ return self.config[name]
+
+ option = "disable_test_id_escaping_and_forfeit_all_rights_to_community_support"
+
+ values: List[Tuple[str, Any, str]] = [
+ ("ação", MockConfig({option: True}), "ação"),
+ ("ação", MockConfig({option: False}), "a\\xe7\\xe3o"),
+ ]
+ for val, config, expected in values:
+ actual = _idval(val, "a", 6, None, nodeid=None, config=config)
+ assert actual == expected
+
+ def test_bytes_idval(self) -> None:
+ """Unit test for the expected behavior to obtain ids for parametrized
+ bytes values: bytes objects are always escaped using "binary escape"."""
+ values = [
+ (b"", r""),
+ (b"\xc3\xb4\xff\xe4", r"\xc3\xb4\xff\xe4"),
+ (b"ascii", r"ascii"),
+ ("αρά".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
+
+ def test_class_or_function_idval(self) -> None:
+ """Unit test for the expected behavior to obtain ids for parametrized
+ values that are classes or functions: their __name__."""
+
+ class TestClass:
+ pass
+
+ def test_function():
+ pass
+
+ values = [(TestClass, "TestClass"), (test_function, "test_function")]
+ for val, expected in values:
+ assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected
+
+ def test_notset_idval(self) -> None:
+ """Test that a NOTSET value (used by an empty parameterset) generates
+ a proper ID.
+
+ Regression test for #7686.
+ """
+ assert _idval(NOTSET, "a", 0, None, nodeid=None, config=None) == "a0"
+
+ def test_idmaker_autoname(self) -> None:
+ """#250"""
+ result = idmaker(
+ ("a", "b"), [pytest.param("string", 1.0), pytest.param("st-ring", 2.0)]
+ )
+ assert result == ["string-1.0", "st-ring-2.0"]
+
+ result = idmaker(
+ ("a", "b"), [pytest.param(object(), 1.0), pytest.param(object(), object())]
+ )
+ assert result == ["a0-1.0", "a1-b1"]
+ # unicode mixing, issue250
+ result = idmaker(("a", "b"), [pytest.param({}, b"\xc3\xb4")])
+ assert result == ["a0-\\xc3\\xb4"]
+
+ def test_idmaker_with_bytes_regex(self) -> None:
+ result = idmaker(("a"), [pytest.param(re.compile(b"foo"), 1.0)])
+ assert result == ["foo"]
+
+ def test_idmaker_native_strings(self) -> None:
+ result = idmaker(
+ ("a", "b"),
+ [
+ pytest.param(1.0, -1.1),
+ pytest.param(2, -202),
+ pytest.param("three", "three hundred"),
+ pytest.param(True, False),
+ pytest.param(None, None),
+ pytest.param(re.compile("foo"), re.compile("bar")),
+ pytest.param(str, int),
+ pytest.param(list("six"), [66, 66]),
+ pytest.param({7}, set("seven")),
+ pytest.param(tuple("eight"), (8, -8, 8)),
+ pytest.param(b"\xc3\xb4", b"name"),
+ pytest.param(b"\xc3\xb4", "other"),
+ pytest.param(1.0j, -2.0j),
+ ],
+ )
+ assert result == [
+ "1.0--1.1",
+ "2--202",
+ "three-three hundred",
+ "True-False",
+ "None-None",
+ "foo-bar",
+ "str-int",
+ "a7-b7",
+ "a8-b8",
+ "a9-b9",
+ "\\xc3\\xb4-name",
+ "\\xc3\\xb4-other",
+ "1j-(-0-2j)",
+ ]
+
+ def test_idmaker_non_printable_characters(self) -> None:
+ result = idmaker(
+ ("s", "n"),
+ [
+ pytest.param("\x00", 1),
+ pytest.param("\x05", 2),
+ pytest.param(b"\x00", 3),
+ pytest.param(b"\x05", 4),
+ pytest.param("\t", 5),
+ pytest.param(b"\t", 6),
+ ],
+ )
+ 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(
+ ("s",),
+ [
+ pytest.param("x00", id="hello \x00"),
+ pytest.param("x05", id="hello \x05"),
+ ],
+ )
+ 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)])
+ assert result == ["Foo.one-Foo.two"]
+
+ def test_idmaker_idfn(self) -> None:
+ """#351"""
+
+ def ids(val: object) -> Optional[str]:
+ if isinstance(val, Exception):
+ return repr(val)
+ return None
+
+ result = idmaker(
+ ("a", "b"),
+ [
+ pytest.param(10.0, IndexError()),
+ pytest.param(20, KeyError()),
+ pytest.param("three", [1, 2, 3]),
+ ],
+ idfn=ids,
+ )
+ assert result == ["10.0-IndexError()", "20-KeyError()", "three-b2"]
+
+ def test_idmaker_idfn_unique_names(self) -> None:
+ """#351"""
+
+ def ids(val: object) -> str:
+ return "a"
+
+ result = idmaker(
+ ("a", "b"),
+ [
+ pytest.param(10.0, IndexError()),
+ pytest.param(20, KeyError()),
+ pytest.param("three", [1, 2, 3]),
+ ],
+ idfn=ids,
+ )
+ assert result == ["a-a0", "a-a1", "a-a2"]
+
+ def test_idmaker_with_idfn_and_config(self) -> None:
+ """Unit test for expected behavior to create ids with idfn and
+ disable_test_id_escaping_and_forfeit_all_rights_to_community_support
+ option (#5294).
+ """
+
+ class MockConfig:
+ def __init__(self, config):
+ self.config = config
+
+ @property
+ def hook(self):
+ return self
+
+ def pytest_make_parametrize_id(self, **kw):
+ pass
+
+ def getini(self, name):
+ return self.config[name]
+
+ option = "disable_test_id_escaping_and_forfeit_all_rights_to_community_support"
+
+ values: List[Tuple[Any, str]] = [
+ (MockConfig({option: True}), "ação"),
+ (MockConfig({option: False}), "a\\xe7\\xe3o"),
+ ]
+ for config, expected in values:
+ result = idmaker(
+ ("a",),
+ [pytest.param("string")],
+ idfn=lambda _: "ação",
+ config=config,
+ )
+ assert result == [expected]
+
+ def test_idmaker_with_ids_and_config(self) -> None:
+ """Unit test for expected behavior to create ids with ids and
+ disable_test_id_escaping_and_forfeit_all_rights_to_community_support
+ option (#5294).
+ """
+
+ class MockConfig:
+ def __init__(self, config):
+ self.config = config
+
+ @property
+ def hook(self):
+ return self
+
+ def pytest_make_parametrize_id(self, **kw):
+ pass
+
+ def getini(self, name):
+ return self.config[name]
+
+ option = "disable_test_id_escaping_and_forfeit_all_rights_to_community_support"
+
+ values: List[Tuple[Any, str]] = [
+ (MockConfig({option: True}), "ação"),
+ (MockConfig({option: False}), "a\\xe7\\xe3o"),
+ ]
+ for config, expected in values:
+ result = idmaker(
+ ("a",),
+ [pytest.param("string")],
+ ids=["ação"],
+ config=config,
+ )
+ assert result == [expected]
+
+ def test_parametrize_ids_exception(self, pytester: Pytester) -> None:
+ """
+ :param pytester: the instance of Pytester class, a temporary
+ test directory.
+ """
+ pytester.makepyfile(
+ """
+ import pytest
+
+ def ids(arg):
+ raise Exception("bad ids")
+
+ @pytest.mark.parametrize("arg", ["a", "b"], ids=ids)
+ def test_foo(arg):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*Exception: bad ids",
+ "*test_foo: error raised while trying to determine id of parameter 'arg' at position 0",
+ ]
+ )
+
+ def test_parametrize_ids_returns_non_string(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """\
+ import pytest
+
+ def ids(d):
+ return d
+
+ @pytest.mark.parametrize("arg", ({1: 2}, {3, 4}), ids=ids)
+ def test(arg):
+ assert arg
+
+ @pytest.mark.parametrize("arg", (1, 2.0, True), ids=ids)
+ def test_int(arg):
+ assert arg
+ """
+ )
+ result = pytester.runpytest("-vv", "-s")
+ result.stdout.fnmatch_lines(
+ [
+ "test_parametrize_ids_returns_non_string.py::test[arg0] PASSED",
+ "test_parametrize_ids_returns_non_string.py::test[arg1] PASSED",
+ "test_parametrize_ids_returns_non_string.py::test_int[1] PASSED",
+ "test_parametrize_ids_returns_non_string.py::test_int[2.0] PASSED",
+ "test_parametrize_ids_returns_non_string.py::test_int[True] PASSED",
+ ]
+ )
+
+ def test_idmaker_with_ids(self) -> None:
+ result = idmaker(
+ ("a", "b"), [pytest.param(1, 2), pytest.param(3, 4)], ids=["a", None]
+ )
+ assert result == ["a", "3-4"]
+
+ def test_idmaker_with_paramset_id(self) -> None:
+ result = idmaker(
+ ("a", "b"),
+ [pytest.param(1, 2, id="me"), pytest.param(3, 4, id="you")],
+ ids=["a", None],
+ )
+ 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"]
+ )
+ assert result == ["a0", "a1", "b0", "c", "b1"]
+
+ def test_parametrize_indirect(self) -> None:
+ """#714"""
+
+ def func(x, y):
+ pass
+
+ metafunc = self.Metafunc(func)
+ 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)
+
+ def test_parametrize_indirect_list(self) -> None:
+ """#714"""
+
+ def func(x, y):
+ pass
+
+ 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")
+
+ def test_parametrize_indirect_list_all(self) -> None:
+ """#714"""
+
+ def func(x, y):
+ pass
+
+ 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")
+
+ def test_parametrize_indirect_list_empty(self) -> None:
+ """#714"""
+
+ def func(x, y):
+ pass
+
+ 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 == {}
+
+ def test_parametrize_indirect_wrong_type(self) -> None:
+ def func(x, y):
+ pass
+
+ metafunc = self.Metafunc(func)
+ with pytest.raises(
+ fail.Exception,
+ match="In func: expected Sequence or boolean for indirect, got dict",
+ ):
+ metafunc.parametrize("x, y", [("a", "b")], indirect={}) # type: ignore[arg-type]
+
+ def test_parametrize_indirect_list_functional(self, pytester: Pytester) -> None:
+ """
+ #714
+ Test parametrization with 'indirect' parameter applied on
+ particular arguments. As y is direct, its value should
+ be used directly rather than being passed to the fixture y.
+
+ :param pytester: the instance of Pytester class, a temporary
+ test directory.
+ """
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope='function')
+ def x(request):
+ return request.param * 3
+ @pytest.fixture(scope='function')
+ def y(request):
+ return request.param * 2
+ @pytest.mark.parametrize('x, y', [('a', 'b')], indirect=['x'])
+ def test_simple(x,y):
+ assert len(x) == 3
+ assert len(y) == 1
+ """
+ )
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(["*test_simple*a-b*", "*1 passed*"])
+
+ def test_parametrize_indirect_list_error(self) -> None:
+ """#714"""
+
+ def func(x, y):
+ pass
+
+ metafunc = self.Metafunc(func)
+ with pytest.raises(fail.Exception):
+ metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "z"])
+
+ def test_parametrize_uses_no_fixture_error_indirect_false(
+ self, pytester: Pytester
+ ) -> None:
+ """The 'uses no fixture' error tells the user at collection time
+ that the parametrize data they've set up doesn't correspond to the
+ fixtures in their test function, rather than silently ignoring this
+ and letting the test potentially pass.
+
+ #714
+ """
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.parametrize('x, y', [('a', 'b')], indirect=False)
+ def test_simple(x):
+ assert len(x) == 3
+ """
+ )
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(["*uses no argument 'y'*"])
+
+ def test_parametrize_uses_no_fixture_error_indirect_true(
+ self, pytester: Pytester
+ ) -> None:
+ """#714"""
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope='function')
+ def x(request):
+ return request.param * 3
+ @pytest.fixture(scope='function')
+ def y(request):
+ return request.param * 2
+
+ @pytest.mark.parametrize('x, y', [('a', 'b')], indirect=True)
+ def test_simple(x):
+ assert len(x) == 3
+ """
+ )
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(["*uses no fixture 'y'*"])
+
+ def test_parametrize_indirect_uses_no_fixture_error_indirect_string(
+ self, pytester: Pytester
+ ) -> None:
+ """#714"""
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope='function')
+ def x(request):
+ return request.param * 3
+
+ @pytest.mark.parametrize('x, y', [('a', 'b')], indirect='y')
+ def test_simple(x):
+ assert len(x) == 3
+ """
+ )
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(["*uses no fixture 'y'*"])
+
+ def test_parametrize_indirect_uses_no_fixture_error_indirect_list(
+ self, pytester: Pytester
+ ) -> None:
+ """#714"""
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope='function')
+ def x(request):
+ return request.param * 3
+
+ @pytest.mark.parametrize('x, y', [('a', 'b')], indirect=['y'])
+ def test_simple(x):
+ assert len(x) == 3
+ """
+ )
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(["*uses no fixture 'y'*"])
+
+ def test_parametrize_argument_not_in_indirect_list(
+ self, pytester: Pytester
+ ) -> None:
+ """#714"""
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope='function')
+ def x(request):
+ return request.param * 3
+
+ @pytest.mark.parametrize('x, y', [('a', 'b')], indirect=['x'])
+ def test_simple(x):
+ assert len(x) == 3
+ """
+ )
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(["*uses no argument 'y'*"])
+
+ def test_parametrize_gives_indicative_error_on_function_with_default_argument(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.parametrize('x, y', [('a', 'b')])
+ def test_simple(x, y=1):
+ assert len(x) == 1
+ """
+ )
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(
+ ["*already takes an argument 'y' with a default value"]
+ )
+
+ def test_parametrize_functional(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize('x', [1,2], indirect=True)
+ metafunc.parametrize('y', [2])
+ @pytest.fixture
+ def x(request):
+ return request.param * 10
+
+ def test_simple(x,y):
+ assert x in (10,20)
+ assert y == 2
+ """
+ )
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(
+ ["*test_simple*1-2*", "*test_simple*2-2*", "*2 passed*"]
+ )
+
+ def test_parametrize_onearg(self) -> None:
+ 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].id == "1"
+ assert metafunc._calls[1].funcargs == dict(x=2)
+ assert metafunc._calls[1].id == "2"
+
+ def test_parametrize_onearg_indirect(self) -> None:
+ metafunc = self.Metafunc(lambda x: None)
+ metafunc.parametrize("x", [1, 2], indirect=True)
+ assert metafunc._calls[0].params == dict(x=1)
+ assert metafunc._calls[0].id == "1"
+ assert metafunc._calls[1].params == dict(x=2)
+ assert metafunc._calls[1].id == "2"
+
+ def test_parametrize_twoargs(self) -> None:
+ 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].id == "1-2"
+ assert metafunc._calls[1].funcargs == dict(x=3, y=4)
+ assert metafunc._calls[1].id == "3-4"
+
+ def test_parametrize_multiple_times(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ pytestmark = pytest.mark.parametrize("x", [1,2])
+ def test_func(x):
+ assert 0, x
+ class TestClass(object):
+ pytestmark = pytest.mark.parametrize("y", [3,4])
+ def test_meth(self, x, y):
+ assert 0, x
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 1
+ result.assert_outcomes(failed=6)
+
+ def test_parametrize_CSV(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize("x, y,", [(1,2), (2,3)])
+ def test_func(x, y):
+ assert x+1 == y
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ def test_parametrize_class_scenarios(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ # same as doc/en/example/parametrize scenario example
+ def pytest_generate_tests(metafunc):
+ idlist = []
+ argvalues = []
+ for scenario in metafunc.cls.scenarios:
+ idlist.append(scenario[0])
+ items = scenario[1].items()
+ argnames = [x[0] for x in items]
+ argvalues.append(([x[1] for x in items]))
+ metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class")
+
+ class Test(object):
+ scenarios = [['1', {'arg': {1: 2}, "arg2": "value2"}],
+ ['2', {'arg':'value2', "arg2": "value2"}]]
+
+ def test_1(self, arg, arg2):
+ pass
+
+ def test_2(self, arg2, arg):
+ pass
+
+ def test_3(self, arg, arg2):
+ pass
+ """
+ )
+ result = pytester.runpytest("-v")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(
+ """
+ *test_1*1*
+ *test_2*1*
+ *test_3*1*
+ *test_1*2*
+ *test_2*2*
+ *test_3*2*
+ *6 passed*
+ """
+ )
+
+ 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:
+ p = pytester.makepyfile(
+ """
+ # assumes that generate/provide runs in the same process
+ import sys, pytest
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize('metafunc', [metafunc])
+
+ @pytest.fixture
+ def metafunc(request):
+ return request.param
+
+ def test_function(metafunc, pytestconfig):
+ assert metafunc.config == pytestconfig
+ assert metafunc.module.__name__ == __name__
+ assert metafunc.function == test_function
+ assert metafunc.cls is None
+
+ class TestClass(object):
+ def test_method(self, metafunc, pytestconfig):
+ assert metafunc.config == pytestconfig
+ assert metafunc.module.__name__ == __name__
+ unbound = TestClass.test_method
+ assert metafunc.function == unbound
+ assert metafunc.cls == TestClass
+ """
+ )
+ result = pytester.runpytest(p, "-v")
+ result.assert_outcomes(passed=2)
+
+ def test_two_functions(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize('arg1', [10, 20], ids=['0', '1'])
+
+ def test_func1(arg1):
+ assert arg1 == 10
+
+ def test_func2(arg1):
+ assert arg1 in (10, 20)
+ """
+ )
+ result = pytester.runpytest("-v", p)
+ result.stdout.fnmatch_lines(
+ [
+ "*test_func1*0*PASS*",
+ "*test_func1*1*FAIL*",
+ "*test_func2*PASS*",
+ "*test_func2*PASS*",
+ "*1 failed, 3 passed*",
+ ]
+ )
+
+ def test_noself_in_method(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def pytest_generate_tests(metafunc):
+ assert 'xyz' not in metafunc.fixturenames
+
+ class TestHello(object):
+ def test_hello(xyz):
+ pass
+ """
+ )
+ result = pytester.runpytest(p)
+ result.assert_outcomes(passed=1)
+
+ def test_generate_tests_in_class(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ class TestClass(object):
+ def pytest_generate_tests(self, metafunc):
+ metafunc.parametrize('hello', ['world'], ids=['hellow'])
+
+ def test_myfunc(self, hello):
+ assert hello == "world"
+ """
+ )
+ result = pytester.runpytest("-v", p)
+ result.stdout.fnmatch_lines(["*test_myfunc*hello*PASS*", "*1 passed*"])
+
+ def test_two_functions_not_same_instance(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize('arg1', [10, 20], ids=["0", "1"])
+
+ class TestClass(object):
+ def test_func(self, arg1):
+ assert not hasattr(self, 'x')
+ self.x = 1
+ """
+ )
+ result = pytester.runpytest("-v", p)
+ result.stdout.fnmatch_lines(
+ ["*test_func*0*PASS*", "*test_func*1*PASS*", "*2 pass*"]
+ )
+
+ def test_issue28_setup_method_in_generate_tests(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize('arg1', [1])
+
+ class TestClass(object):
+ def test_method(self, arg1):
+ assert arg1 == self.val
+ def setup_method(self, func):
+ self.val = 1
+ """
+ )
+ result = pytester.runpytest(p)
+ result.assert_outcomes(passed=1)
+
+ def test_parametrize_functional2(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize("arg1", [1,2])
+ metafunc.parametrize("arg2", [4,5])
+ def test_hello(arg1, arg2):
+ assert 0, (arg1, arg2)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ ["*(1, 4)*", "*(1, 5)*", "*(2, 4)*", "*(2, 5)*", "*4 failed*"]
+ )
+
+ def test_parametrize_and_inner_getfixturevalue(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize("arg1", [1], indirect=True)
+ metafunc.parametrize("arg2", [10], indirect=True)
+
+ import pytest
+ @pytest.fixture
+ def arg1(request):
+ x = request.getfixturevalue("arg2")
+ return x + request.param
+
+ @pytest.fixture
+ def arg2(request):
+ return request.param
+
+ def test_func1(arg1, arg2):
+ assert arg1 == 11
+ """
+ )
+ result = pytester.runpytest("-v", p)
+ result.stdout.fnmatch_lines(["*test_func1*1*PASS*", "*1 passed*"])
+
+ def test_parametrize_on_setup_arg(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def pytest_generate_tests(metafunc):
+ assert "arg1" in metafunc.fixturenames
+ metafunc.parametrize("arg1", [1], indirect=True)
+
+ import pytest
+ @pytest.fixture
+ def arg1(request):
+ return request.param
+
+ @pytest.fixture
+ def arg2(request, arg1):
+ return 10 * arg1
+
+ def test_func(arg2):
+ assert arg2 == 10
+ """
+ )
+ result = pytester.runpytest("-v", p)
+ result.stdout.fnmatch_lines(["*test_func*1*PASS*", "*1 passed*"])
+
+ def test_parametrize_with_ids(self, pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ console_output_style=classic
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize(("a", "b"), [(1,1), (1,2)],
+ ids=["basic", "advanced"])
+
+ def test_function(a, b):
+ assert a == b
+ """
+ )
+ result = pytester.runpytest("-v")
+ assert result.ret == 1
+ result.stdout.fnmatch_lines_random(
+ ["*test_function*basic*PASSED", "*test_function*advanced*FAILED"]
+ )
+
+ def test_parametrize_without_ids(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize(("a", "b"),
+ [(1,object()), (1.3,object())])
+
+ def test_function(a, b):
+ assert 1
+ """
+ )
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(
+ """
+ *test_function*1-b0*
+ *test_function*1.3-b1*
+ """
+ )
+
+ def test_parametrize_with_None_in_ids(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize(("a", "b"), [(1,1), (1,1), (1,2)],
+ ids=["basic", None, "advanced"])
+
+ def test_function(a, b):
+ assert a == b
+ """
+ )
+ result = pytester.runpytest("-v")
+ assert result.ret == 1
+ result.stdout.fnmatch_lines_random(
+ [
+ "*test_function*basic*PASSED*",
+ "*test_function*1-1*PASSED*",
+ "*test_function*advanced*FAILED*",
+ ]
+ )
+
+ def test_fixture_parametrized_empty_ids(self, pytester: Pytester) -> None:
+ """Fixtures parametrized with empty ids cause an internal error (#1849)."""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope="module", ids=[], params=[])
+ def temp(request):
+ return request.param
+
+ def test_temp(temp):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 1 skipped *"])
+
+ def test_parametrized_empty_ids(self, pytester: Pytester) -> None:
+ """Tests parametrized with empty ids cause an internal error (#1849)."""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.parametrize('temp', [], ids=list())
+ def test_temp(temp):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 1 skipped *"])
+
+ def test_parametrized_ids_invalid_type(self, pytester: Pytester) -> None:
+ """Test error with non-strings/non-ints, without generator (#1857)."""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, type))
+ def test_ids_numbers(x,expected):
+ assert x * 2 == expected
+ """
+ )
+ 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"
+ ]
+ )
+
+ def test_parametrize_with_identical_ids_get_unique_names(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def pytest_generate_tests(metafunc):
+ metafunc.parametrize(("a", "b"), [(1,1), (1,2)],
+ ids=["a", "a"])
+
+ def test_function(a, b):
+ assert a == b
+ """
+ )
+ result = pytester.runpytest("-v")
+ assert result.ret == 1
+ result.stdout.fnmatch_lines_random(
+ ["*test_function*a0*PASSED*", "*test_function*a1*FAILED*"]
+ )
+
+ @pytest.mark.parametrize(("scope", "length"), [("module", 2), ("function", 4)])
+ def test_parametrize_scope_overrides(
+ self, pytester: Pytester, scope: str, length: int
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ values = []
+ def pytest_generate_tests(metafunc):
+ if "arg" in metafunc.fixturenames:
+ metafunc.parametrize("arg", [1,2], indirect=True,
+ scope=%r)
+ @pytest.fixture
+ def arg(request):
+ values.append(request.param)
+ return request.param
+ def test_hello(arg):
+ assert arg in (1,2)
+ def test_world(arg):
+ assert arg in (1,2)
+ def test_checklength():
+ assert len(values) == %d
+ """
+ % (scope, length)
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=5)
+
+ def test_parametrize_issue323(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope='module', params=range(966))
+ def foo(request):
+ return request.param
+
+ def test_it(foo):
+ pass
+ def test_it2(foo):
+ pass
+ """
+ )
+ reprec = pytester.inline_run("--collect-only")
+ assert not reprec.getcalls("pytest_internalerror")
+
+ def test_usefixtures_seen_in_generate_tests(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def pytest_generate_tests(metafunc):
+ assert "abc" in metafunc.fixturenames
+ metafunc.parametrize("abc", [1])
+
+ @pytest.mark.usefixtures("abc")
+ def test_function():
+ pass
+ """
+ )
+ reprec = pytester.runpytest()
+ reprec.assert_outcomes(passed=1)
+
+ def test_generate_tests_only_done_in_subdir(self, pytester: Pytester) -> None:
+ sub1 = pytester.mkpydir("sub1")
+ sub2 = pytester.mkpydir("sub2")
+ sub1.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ def pytest_generate_tests(metafunc):
+ assert metafunc.function.__name__ == "test_1"
+ """
+ )
+ )
+ sub2.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ def pytest_generate_tests(metafunc):
+ assert metafunc.function.__name__ == "test_2"
+ """
+ )
+ )
+ 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)
+
+ def test_generate_same_function_names_issue403(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ def make_tests():
+ @pytest.mark.parametrize("x", range(2))
+ def test_foo(x):
+ pass
+ return test_foo
+
+ test_x = make_tests()
+ test_y = make_tests()
+ """
+ )
+ reprec = pytester.runpytest()
+ reprec.assert_outcomes(passed=4)
+
+ def test_parametrize_misspelling(self, pytester: Pytester) -> None:
+ """#463"""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.parametrise("x", range(2))
+ def test_foo(x):
+ pass
+ """
+ )
+ result = pytester.runpytest("--collectonly")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 0 items / 1 error",
+ "",
+ "*= ERRORS =*",
+ "*_ ERROR collecting test_parametrize_misspelling.py _*",
+ "test_parametrize_misspelling.py:3: in <module>",
+ ' @pytest.mark.parametrise("x", range(2))',
+ "E Failed: Unknown 'parametrise' mark, did you mean 'parametrize'?",
+ "*! Interrupted: 1 error during collection !*",
+ "*= no tests collected, 1 error in *",
+ ]
+ )
+
+
+class TestMetafuncFunctionalAuto:
+ """Tests related to automatically find out the correct scope for
+ parametrized tests (#1832)."""
+
+ def test_parametrize_auto_scope(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope='session', autouse=True)
+ def fixture():
+ return 1
+
+ @pytest.mark.parametrize('animal', ["dog", "cat"])
+ def test_1(animal):
+ assert animal in ('dog', 'cat')
+
+ @pytest.mark.parametrize('animal', ['fish'])
+ def test_2(animal):
+ assert animal == 'fish'
+
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 3 passed *"])
+
+ def test_parametrize_auto_scope_indirect(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope='session')
+ def echo(request):
+ return request.param
+
+ @pytest.mark.parametrize('animal, echo', [("dog", 1), ("cat", 2)], indirect=['echo'])
+ def test_1(animal, echo):
+ assert animal in ('dog', 'cat')
+ assert echo in (1, 2, 3)
+
+ @pytest.mark.parametrize('animal, echo', [('fish', 3)], indirect=['echo'])
+ def test_2(animal, echo):
+ assert animal == 'fish'
+ assert echo in (1, 2, 3)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 3 passed *"])
+
+ def test_parametrize_auto_scope_override_fixture(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope='session', autouse=True)
+ def animal():
+ return 'fox'
+
+ @pytest.mark.parametrize('animal', ["dog", "cat"])
+ def test_1(animal):
+ assert animal in ('dog', 'cat')
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 2 passed *"])
+
+ def test_parametrize_all_indirects(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture()
+ def animal(request):
+ return request.param
+
+ @pytest.fixture(scope='session')
+ def echo(request):
+ return request.param
+
+ @pytest.mark.parametrize('animal, echo', [("dog", 1), ("cat", 2)], indirect=True)
+ def test_1(animal, echo):
+ assert animal in ('dog', 'cat')
+ assert echo in (1, 2, 3)
+
+ @pytest.mark.parametrize('animal, echo', [("fish", 3)], indirect=True)
+ def test_2(animal, echo):
+ assert animal == 'fish'
+ assert echo in (1, 2, 3)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 3 passed *"])
+
+ def test_parametrize_some_arguments_auto_scope(
+ self, pytester: Pytester, monkeypatch
+ ) -> None:
+ """Integration test for (#3941)"""
+ class_fix_setup: List[object] = []
+ monkeypatch.setattr(sys, "class_fix_setup", class_fix_setup, raising=False)
+ func_fix_setup: List[object] = []
+ monkeypatch.setattr(sys, "func_fix_setup", func_fix_setup, raising=False)
+
+ pytester.makepyfile(
+ """
+ import pytest
+ import sys
+
+ @pytest.fixture(scope='class', autouse=True)
+ def class_fix(request):
+ sys.class_fix_setup.append(request.param)
+
+ @pytest.fixture(autouse=True)
+ def func_fix():
+ sys.func_fix_setup.append(True)
+
+ @pytest.mark.parametrize('class_fix', [10, 20], indirect=True)
+ class Test:
+ def test_foo(self):
+ pass
+ def test_bar(self):
+ pass
+ """
+ )
+ result = pytester.runpytest_inprocess()
+ result.stdout.fnmatch_lines(["* 4 passed in *"])
+ assert func_fix_setup == [True] * 4
+ assert class_fix_setup == [10, 20]
+
+ def test_parametrize_issue634(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(scope='module')
+ def foo(request):
+ print('preparing foo-%d' % request.param)
+ return 'foo-%d' % request.param
+
+ def test_one(foo):
+ pass
+
+ def test_two(foo):
+ pass
+
+ test_two.test_with = (2, 3)
+
+ def pytest_generate_tests(metafunc):
+ params = (1, 2, 3, 4)
+ if not 'foo' in metafunc.fixturenames:
+ return
+
+ test_with = getattr(metafunc.function, 'test_with', None)
+ if test_with:
+ params = test_with
+ metafunc.parametrize('foo', params, indirect=True)
+ """
+ )
+ result = pytester.runpytest("-s")
+ output = result.stdout.str()
+ assert output.count("preparing foo-2") == 1
+ assert output.count("preparing foo-3") == 1
+
+
+class TestMarkersWithParametrization:
+ """#308"""
+
+ def test_simple_mark(self, pytester: Pytester) -> None:
+ s = """
+ import pytest
+
+ @pytest.mark.foo
+ @pytest.mark.parametrize(("n", "expected"), [
+ (1, 2),
+ pytest.param(1, 3, marks=pytest.mark.bar),
+ (2, 3),
+ ])
+ def test_increment(n, expected):
+ assert n + 1 == expected
+ """
+ items = pytester.getitems(s)
+ assert len(items) == 3
+ for item in items:
+ assert "foo" in item.keywords
+ assert "bar" not in items[0].keywords
+ assert "bar" in items[1].keywords
+ assert "bar" not in items[2].keywords
+
+ def test_select_based_on_mark(self, pytester: Pytester) -> None:
+ s = """
+ import pytest
+
+ @pytest.mark.parametrize(("n", "expected"), [
+ (1, 2),
+ pytest.param(2, 3, marks=pytest.mark.foo),
+ (3, 4),
+ ])
+ def test_increment(n, expected):
+ assert n + 1 == expected
+ """
+ pytester.makepyfile(s)
+ rec = pytester.inline_run("-m", "foo")
+ passed, skipped, fail = rec.listoutcomes()
+ assert len(passed) == 1
+ assert len(skipped) == 0
+ assert len(fail) == 0
+
+ def test_simple_xfail(self, pytester: Pytester) -> None:
+ s = """
+ import pytest
+
+ @pytest.mark.parametrize(("n", "expected"), [
+ (1, 2),
+ pytest.param(1, 3, marks=pytest.mark.xfail),
+ (2, 3),
+ ])
+ def test_increment(n, expected):
+ assert n + 1 == expected
+ """
+ pytester.makepyfile(s)
+ reprec = pytester.inline_run()
+ # xfail is skip??
+ reprec.assertoutcome(passed=2, skipped=1)
+
+ def test_simple_xfail_single_argname(self, pytester: Pytester) -> None:
+ s = """
+ import pytest
+
+ @pytest.mark.parametrize("n", [
+ 2,
+ pytest.param(3, marks=pytest.mark.xfail),
+ 4,
+ ])
+ def test_isEven(n):
+ assert n % 2 == 0
+ """
+ pytester.makepyfile(s)
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2, skipped=1)
+
+ def test_xfail_with_arg(self, pytester: Pytester) -> None:
+ s = """
+ import pytest
+
+ @pytest.mark.parametrize(("n", "expected"), [
+ (1, 2),
+ pytest.param(1, 3, marks=pytest.mark.xfail("True")),
+ (2, 3),
+ ])
+ def test_increment(n, expected):
+ assert n + 1 == expected
+ """
+ pytester.makepyfile(s)
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2, skipped=1)
+
+ def test_xfail_with_kwarg(self, pytester: Pytester) -> None:
+ s = """
+ import pytest
+
+ @pytest.mark.parametrize(("n", "expected"), [
+ (1, 2),
+ pytest.param(1, 3, marks=pytest.mark.xfail(reason="some bug")),
+ (2, 3),
+ ])
+ def test_increment(n, expected):
+ assert n + 1 == expected
+ """
+ pytester.makepyfile(s)
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2, skipped=1)
+
+ def test_xfail_with_arg_and_kwarg(self, pytester: Pytester) -> None:
+ s = """
+ import pytest
+
+ @pytest.mark.parametrize(("n", "expected"), [
+ (1, 2),
+ pytest.param(1, 3, marks=pytest.mark.xfail("True", reason="some bug")),
+ (2, 3),
+ ])
+ def test_increment(n, expected):
+ assert n + 1 == expected
+ """
+ pytester.makepyfile(s)
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2, skipped=1)
+
+ @pytest.mark.parametrize("strict", [True, False])
+ def test_xfail_passing_is_xpass(self, pytester: Pytester, strict: bool) -> None:
+ s = """
+ import pytest
+
+ m = pytest.mark.xfail("sys.version_info > (0, 0, 0)", reason="some bug", strict={strict})
+
+ @pytest.mark.parametrize(("n", "expected"), [
+ (1, 2),
+ pytest.param(2, 3, marks=m),
+ (3, 4),
+ ])
+ 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)
+ reprec.assertoutcome(passed=passed, failed=failed)
+
+ def test_parametrize_called_in_generate_tests(self, pytester: Pytester) -> None:
+ s = """
+ import pytest
+
+
+ def pytest_generate_tests(metafunc):
+ passingTestData = [(1, 2),
+ (2, 3)]
+ failingTestData = [(1, 3),
+ (2, 2)]
+
+ testData = passingTestData + [pytest.param(*d, marks=pytest.mark.xfail)
+ for d in failingTestData]
+ metafunc.parametrize(("n", "expected"), testData)
+
+
+ def test_increment(n, expected):
+ assert n + 1 == expected
+ """
+ pytester.makepyfile(s)
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2, skipped=2)
+
+ def test_parametrize_ID_generation_string_int_works(
+ self, pytester: Pytester
+ ) -> None:
+ """#290"""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def myfixture():
+ return 'example'
+ @pytest.mark.parametrize(
+ 'limit', (0, '0'))
+ def test_limit(limit, myfixture):
+ return
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=2)
+
+ @pytest.mark.parametrize("strict", [True, False])
+ def test_parametrize_marked_value(self, pytester: Pytester, strict: bool) -> None:
+ s = """
+ import pytest
+
+ @pytest.mark.parametrize(("n", "expected"), [
+ pytest.param(
+ 2,3,
+ marks=pytest.mark.xfail("sys.version_info > (0, 0, 0)", reason="some bug", strict={strict}),
+ ),
+ pytest.param(
+ 2,3,
+ marks=[pytest.mark.xfail("sys.version_info > (0, 0, 0)", reason="some bug", strict={strict})],
+ ),
+ ])
+ 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)
+ reprec.assertoutcome(passed=passed, failed=failed)
+
+ def test_pytest_make_parametrize_id(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_make_parametrize_id(config, val):
+ return str(val * 2)
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.parametrize("x", range(2))
+ def test_func(x):
+ pass
+ """
+ )
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(["*test_func*0*PASS*", "*test_func*2*PASS*"])
+
+ def test_pytest_make_parametrize_id_with_argname(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_make_parametrize_id(config, val, argname):
+ return str(val * 2 if argname == 'x' else val * 10)
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.parametrize("x", range(2))
+ def test_func_a(x):
+ pass
+
+ @pytest.mark.parametrize("y", [1])
+ def test_func_b(y):
+ pass
+ """
+ )
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(
+ ["*test_func_a*0*PASS*", "*test_func_a*2*PASS*", "*test_func_b*10*PASS*"]
+ )
+
+ def test_parametrize_positional_args(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.parametrize("a", [1], False)
+ def test_foo(a):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=1)
+
+ def test_parametrize_iterator(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import itertools
+ import pytest
+
+ id_parametrize = pytest.mark.parametrize(
+ ids=("param%d" % i for i in itertools.count())
+ )
+
+ @id_parametrize('y', ['a', 'b'])
+ def test1(y):
+ pass
+
+ @id_parametrize('y', ['a', 'b'])
+ def test2(y):
+ pass
+
+ @pytest.mark.parametrize("a, b", [(1, 2), (3, 4)], ids=itertools.count())
+ def test_converted_to_str(a, b):
+ pass
+ """
+ )
+ result = pytester.runpytest("-vv", "-s")
+ result.stdout.fnmatch_lines(
+ [
+ "test_parametrize_iterator.py::test1[param0] PASSED",
+ "test_parametrize_iterator.py::test1[param1] PASSED",
+ "test_parametrize_iterator.py::test2[param0] PASSED",
+ "test_parametrize_iterator.py::test2[param1] PASSED",
+ "test_parametrize_iterator.py::test_converted_to_str[0] PASSED",
+ "test_parametrize_iterator.py::test_converted_to_str[1] PASSED",
+ "*= 6 passed in *",
+ ]
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/python/raises.py b/testing/web-platform/tests/tools/third_party/pytest/testing/python/raises.py
new file mode 100644
index 0000000000..2d62e91091
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/python/raises.py
@@ -0,0 +1,298 @@
+import re
+import sys
+
+import pytest
+from _pytest.outcomes import Failed
+from _pytest.pytester import Pytester
+
+
+class TestRaises:
+ def test_check_callable(self) -> None:
+ with pytest.raises(TypeError, match=r".* must be callable"):
+ pytest.raises(RuntimeError, "int('qwe')") # type: ignore[call-overload]
+
+ def test_raises(self):
+ excinfo = pytest.raises(ValueError, int, "qwe")
+ assert "invalid literal" in str(excinfo.value)
+
+ def test_raises_function(self):
+ excinfo = pytest.raises(ValueError, int, "hello")
+ assert "invalid literal" in str(excinfo.value)
+
+ def test_raises_callable_no_exception(self) -> None:
+ class A:
+ def __call__(self):
+ pass
+
+ try:
+ pytest.raises(ValueError, A())
+ except pytest.fail.Exception:
+ pass
+
+ def test_raises_falsey_type_error(self) -> None:
+ with pytest.raises(TypeError):
+ with pytest.raises(AssertionError, match=0): # type: ignore[call-overload]
+ raise AssertionError("ohai")
+
+ def test_raises_repr_inflight(self):
+ """Ensure repr() on an exception info inside a pytest.raises with block works (#4386)"""
+
+ class E(Exception):
+ pass
+
+ with pytest.raises(E) as excinfo:
+ # this test prints the inflight uninitialized object
+ # using repr and str as well as pprint to demonstrate
+ # it works
+ print(str(excinfo))
+ print(repr(excinfo))
+ import pprint
+
+ pprint.pprint(excinfo)
+ raise E()
+
+ def test_raises_as_contextmanager(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ import _pytest._code
+
+ def test_simple():
+ with pytest.raises(ZeroDivisionError) as excinfo:
+ assert isinstance(excinfo, _pytest._code.ExceptionInfo)
+ 1/0
+ print(excinfo)
+ assert excinfo.type == ZeroDivisionError
+ assert isinstance(excinfo.value, ZeroDivisionError)
+
+ def test_noraise():
+ with pytest.raises(pytest.raises.Exception):
+ with pytest.raises(ValueError):
+ int()
+
+ def test_raise_wrong_exception_passes_by():
+ with pytest.raises(ZeroDivisionError):
+ with pytest.raises(ValueError):
+ 1/0
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*3 passed*"])
+
+ def test_does_not_raise(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ from contextlib import contextmanager
+ import pytest
+
+ @contextmanager
+ def does_not_raise():
+ yield
+
+ @pytest.mark.parametrize('example_input,expectation', [
+ (3, does_not_raise()),
+ (2, does_not_raise()),
+ (1, does_not_raise()),
+ (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
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*4 passed*"])
+
+ def test_does_not_raise_does_raise(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ from contextlib import contextmanager
+ import pytest
+
+ @contextmanager
+ def does_not_raise():
+ yield
+
+ @pytest.mark.parametrize('example_input,expectation', [
+ (0, does_not_raise()),
+ (1, pytest.raises(ZeroDivisionError)),
+ ])
+ def test_division(example_input, expectation):
+ '''Test how much I know division.'''
+ with expectation:
+ assert (6 / example_input) is not None
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*2 failed*"])
+
+ def test_noclass(self) -> None:
+ with pytest.raises(TypeError):
+ pytest.raises("wrong", lambda: None) # type: ignore[call-overload]
+
+ def test_invalid_arguments_to_raises(self) -> None:
+ with pytest.raises(TypeError, match="unknown"):
+ with pytest.raises(TypeError, unknown="bogus"): # type: ignore[call-overload]
+ raise ValueError()
+
+ def test_tuple(self):
+ with pytest.raises((KeyError, ValueError)):
+ raise KeyError("oops")
+
+ def test_no_raise_message(self) -> None:
+ try:
+ pytest.raises(ValueError, int, "0")
+ except pytest.fail.Exception as e:
+ assert e.msg == f"DID NOT RAISE {repr(ValueError)}"
+ else:
+ assert False, "Expected pytest.raises.Exception"
+
+ try:
+ with pytest.raises(ValueError):
+ pass
+ except pytest.fail.Exception as e:
+ assert e.msg == f"DID NOT RAISE {repr(ValueError)}"
+ else:
+ assert False, "Expected pytest.raises.Exception"
+
+ @pytest.mark.parametrize("method", ["function", "function_match", "with"])
+ def test_raises_cyclic_reference(self, method):
+ """Ensure pytest.raises does not leave a reference cycle (#1965)."""
+ import gc
+
+ class T:
+ def __call__(self):
+ raise ValueError
+
+ t = T()
+ refcount = len(gc.get_referrers(t))
+
+ if method == "function":
+ pytest.raises(ValueError, t)
+ elif method == "function_match":
+ pytest.raises(ValueError, t).match("^$")
+ else:
+ with pytest.raises(ValueError):
+ t()
+
+ # ensure both forms of pytest.raises don't leave exceptions in sys.exc_info()
+ assert sys.exc_info() == (None, None, None)
+
+ assert refcount == len(gc.get_referrers(t))
+
+ def test_raises_match(self) -> None:
+ msg = r"with base \d+"
+ with pytest.raises(ValueError, match=msg):
+ int("asdf")
+
+ msg = "with base 10"
+ with pytest.raises(ValueError, match=msg):
+ int("asdf")
+
+ msg = "with base 16"
+ expr = "Regex pattern {!r} does not match \"invalid literal for int() with base 10: 'asdf'\".".format(
+ msg
+ )
+ with pytest.raises(AssertionError, match=re.escape(expr)):
+ with pytest.raises(ValueError, match=msg):
+ int("asdf", base=10)
+
+ # "match" without context manager.
+ pytest.raises(ValueError, int, "asdf").match("invalid literal")
+ with pytest.raises(AssertionError) as excinfo:
+ pytest.raises(ValueError, int, "asdf").match(msg)
+ assert str(excinfo.value) == expr
+
+ pytest.raises(TypeError, int, match="invalid")
+
+ def tfunc(match):
+ raise ValueError(f"match={match}")
+
+ pytest.raises(ValueError, tfunc, match="asdf").match("match=asdf")
+ pytest.raises(ValueError, tfunc, match="").match("match=")
+
+ def test_match_failure_string_quoting(self):
+ with pytest.raises(AssertionError) as excinfo:
+ with pytest.raises(AssertionError, match="'foo"):
+ raise AssertionError("'bar")
+ (msg,) = excinfo.value.args
+ assert msg == 'Regex pattern "\'foo" does not match "\'bar".'
+
+ def test_match_failure_exact_string_message(self):
+ message = "Oh here is a message with (42) numbers in parameters"
+ with pytest.raises(AssertionError) as excinfo:
+ with pytest.raises(AssertionError, match=message):
+ 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?"
+ )
+
+ def test_raises_match_wrong_type(self):
+ """Raising an exception with the wrong type and match= given.
+
+ pytest should throw the unexpected exception - the pattern match is not
+ really relevant if we got a different exception.
+ """
+ with pytest.raises(ValueError):
+ with pytest.raises(IndexError, match="nomatch"):
+ int("asdf")
+
+ def test_raises_exception_looks_iterable(self):
+ class Meta(type):
+ def __getitem__(self, item):
+ return 1 / 0
+
+ def __len__(self):
+ return 1
+
+ class ClassLooksIterableException(Exception, metaclass=Meta):
+ pass
+
+ with pytest.raises(
+ Failed,
+ match=r"DID NOT RAISE <class 'raises(\..*)*ClassLooksIterableException'>",
+ ):
+ pytest.raises(ClassLooksIterableException, lambda: None)
+
+ def test_raises_with_raising_dunder_class(self) -> None:
+ """Test current behavior with regard to exceptions via __class__ (#4284)."""
+
+ class CrappyClass(Exception):
+ # Type ignored because it's bypassed intentionally.
+ @property # type: ignore
+ def __class__(self):
+ assert False, "via __class__"
+
+ with pytest.raises(AssertionError) as excinfo:
+ with pytest.raises(CrappyClass()): # type: ignore[call-overload]
+ pass
+ assert "via __class__" in excinfo.value.args[0]
+
+ def test_raises_context_manager_with_kwargs(self):
+ with pytest.raises(TypeError) as excinfo:
+ with pytest.raises(Exception, foo="bar"): # type: ignore[call-overload]
+ pass
+ assert "Unexpected keyword arguments" in str(excinfo.value)
+
+ def test_expected_exception_is_not_a_baseexception(self) -> None:
+ with pytest.raises(TypeError) as excinfo:
+ with pytest.raises("hello"): # type: ignore[call-overload]
+ pass # pragma: no cover
+ assert "must be a BaseException type, not str" in str(excinfo.value)
+
+ class NotAnException:
+ pass
+
+ with pytest.raises(TypeError) as excinfo:
+ with pytest.raises(NotAnException): # type: ignore[type-var]
+ pass # pragma: no cover
+ assert "must be a BaseException type, not NotAnException" in str(excinfo.value)
+
+ with pytest.raises(TypeError) as excinfo:
+ 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)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/python/show_fixtures_per_test.py b/testing/web-platform/tests/tools/third_party/pytest/testing/python/show_fixtures_per_test.py
new file mode 100644
index 0000000000..f756dca41c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/python/show_fixtures_per_test.py
@@ -0,0 +1,254 @@
+from _pytest.pytester import Pytester
+
+
+def test_no_items_should_not_show_output(pytester: Pytester) -> None:
+ result = pytester.runpytest("--fixtures-per-test")
+ result.stdout.no_fnmatch_line("*fixtures used by*")
+ assert result.ret == 0
+
+
+def test_fixtures_in_module(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ '''
+ import pytest
+ @pytest.fixture
+ def _arg0():
+ """hidden arg0 fixture"""
+ @pytest.fixture
+ def arg1():
+ """arg1 docstring"""
+ def test_arg1(arg1):
+ pass
+ '''
+ )
+
+ result = pytester.runpytest("--fixtures-per-test", p)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ [
+ "*fixtures used by test_arg1*",
+ "*(test_fixtures_in_module.py:9)*",
+ "arg1 -- test_fixtures_in_module.py:6",
+ " arg1 docstring",
+ ]
+ )
+ result.stdout.no_fnmatch_line("*_arg0*")
+
+
+def test_fixtures_in_conftest(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ '''
+ import pytest
+ @pytest.fixture
+ def arg1():
+ """arg1 docstring"""
+ @pytest.fixture
+ def arg2():
+ """arg2 docstring"""
+ @pytest.fixture
+ def arg3(arg1, arg2):
+ """arg3
+ docstring
+ """
+ '''
+ )
+ p = pytester.makepyfile(
+ """
+ def test_arg2(arg2):
+ pass
+ def test_arg3(arg3):
+ pass
+ """
+ )
+ result = pytester.runpytest("--fixtures-per-test", p)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ [
+ "*fixtures used by test_arg2*",
+ "*(test_fixtures_in_conftest.py:2)*",
+ "arg2 -- conftest.py:6",
+ " arg2 docstring",
+ "*fixtures used by test_arg3*",
+ "*(test_fixtures_in_conftest.py:4)*",
+ "arg1 -- conftest.py:3",
+ " arg1 docstring",
+ "arg2 -- conftest.py:6",
+ " arg2 docstring",
+ "arg3 -- conftest.py:9",
+ " arg3",
+ ]
+ )
+
+
+def test_should_show_fixtures_used_by_test(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ '''
+ import pytest
+ @pytest.fixture
+ def arg1():
+ """arg1 from conftest"""
+ @pytest.fixture
+ def arg2():
+ """arg2 from conftest"""
+ '''
+ )
+ p = pytester.makepyfile(
+ '''
+ import pytest
+ @pytest.fixture
+ def arg1():
+ """arg1 from testmodule"""
+ def test_args(arg1, arg2):
+ pass
+ '''
+ )
+ result = pytester.runpytest("--fixtures-per-test", p)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ [
+ "*fixtures used by test_args*",
+ "*(test_should_show_fixtures_used_by_test.py:6)*",
+ "arg1 -- test_should_show_fixtures_used_by_test.py:3",
+ " arg1 from testmodule",
+ "arg2 -- conftest.py:6",
+ " arg2 from conftest",
+ ]
+ )
+
+
+def test_verbose_include_private_fixtures_and_loc(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ '''
+ import pytest
+ @pytest.fixture
+ def _arg1():
+ """_arg1 from conftest"""
+ @pytest.fixture
+ def arg2(_arg1):
+ """arg2 from conftest"""
+ '''
+ )
+ p = pytester.makepyfile(
+ '''
+ import pytest
+ @pytest.fixture
+ def arg3():
+ """arg3 from testmodule"""
+ def test_args(arg2, arg3):
+ pass
+ '''
+ )
+ result = pytester.runpytest("--fixtures-per-test", "-v", p)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ [
+ "*fixtures used by test_args*",
+ "*(test_verbose_include_private_fixtures_and_loc.py:6)*",
+ "_arg1 -- conftest.py:3",
+ " _arg1 from conftest",
+ "arg2 -- conftest.py:6",
+ " arg2 from conftest",
+ "arg3 -- test_verbose_include_private_fixtures_and_loc.py:3",
+ " arg3 from testmodule",
+ ]
+ )
+
+
+def test_doctest_items(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ '''
+ def foo():
+ """
+ >>> 1 + 1
+ 2
+ """
+ '''
+ )
+ pytester.maketxtfile(
+ """
+ >>> 1 + 1
+ 2
+ """
+ )
+ result = pytester.runpytest(
+ "--fixtures-per-test", "--doctest-modules", "--doctest-glob=*.txt", "-v"
+ )
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(["*collected 2 items*"])
+
+
+def test_multiline_docstring_in_module(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ '''
+ import pytest
+ @pytest.fixture
+ def arg1():
+ """Docstring content that spans across multiple lines,
+ through second line,
+ and through third line.
+
+ Docstring content that extends into a second paragraph.
+
+ Docstring content that extends into a third paragraph.
+ """
+ def test_arg1(arg1):
+ pass
+ '''
+ )
+
+ result = pytester.runpytest("--fixtures-per-test", p)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ [
+ "*fixtures used by test_arg1*",
+ "*(test_multiline_docstring_in_module.py:13)*",
+ "arg1 -- test_multiline_docstring_in_module.py:3",
+ " Docstring content that spans across multiple lines,",
+ " through second line,",
+ " and through third line.",
+ ]
+ )
+
+
+def test_verbose_include_multiline_docstring(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ '''
+ import pytest
+ @pytest.fixture
+ def arg1():
+ """Docstring content that spans across multiple lines,
+ through second line,
+ and through third line.
+
+ Docstring content that extends into a second paragraph.
+
+ Docstring content that extends into a third paragraph.
+ """
+ def test_arg1(arg1):
+ pass
+ '''
+ )
+
+ result = pytester.runpytest("--fixtures-per-test", "-v", p)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ [
+ "*fixtures used by test_arg1*",
+ "*(test_verbose_include_multiline_docstring.py:13)*",
+ "arg1 -- test_verbose_include_multiline_docstring.py:3",
+ " Docstring content that spans across multiple lines,",
+ " through second line,",
+ " and through third line.",
+ " ",
+ " Docstring content that extends into a second paragraph.",
+ " ",
+ " Docstring content that extends into a third paragraph.",
+ ]
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_argcomplete.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_argcomplete.py
new file mode 100644
index 0000000000..8c10e230b0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_argcomplete.py
@@ -0,0 +1,95 @@
+import subprocess
+import sys
+from pathlib import Path
+
+import pytest
+from _pytest.monkeypatch import MonkeyPatch
+
+# Test for _argcomplete but not specific for any application.
+
+
+def equal_with_bash(prefix, ffc, fc, out=None):
+ res = ffc(prefix)
+ res_bash = set(fc(prefix))
+ retval = set(res) == res_bash
+ if out:
+ out.write(f"equal_with_bash({prefix}) {retval} {res}\n")
+ if not retval:
+ out.write(" python - bash: %s\n" % (set(res) - res_bash))
+ out.write(" bash - python: %s\n" % (res_bash - set(res)))
+ return retval
+
+
+# Copied from argcomplete.completers as import from there.
+# Also pulls in argcomplete.__init__ which opens filedescriptor 9.
+# This gives an OSError at the end of testrun.
+
+
+def _wrapcall(*args, **kargs):
+ try:
+ return subprocess.check_output(*args, **kargs).decode().splitlines()
+ except subprocess.CalledProcessError:
+ return []
+
+
+class FilesCompleter:
+ """File completer class, optionally takes a list of allowed extensions."""
+
+ def __init__(self, allowednames=(), directories=True):
+ # Fix if someone passes in a string instead of a list
+ if type(allowednames) is str:
+ allowednames = [allowednames]
+
+ self.allowednames = [x.lstrip("*").lstrip(".") for x in allowednames]
+ self.directories = directories
+
+ def __call__(self, prefix, **kwargs):
+ completion = []
+ if self.allowednames:
+ if self.directories:
+ files = _wrapcall(["bash", "-c", f"compgen -A directory -- '{prefix}'"])
+ completion += [f + "/" for f in files]
+ for x in self.allowednames:
+ completion += _wrapcall(
+ ["bash", "-c", f"compgen -A file -X '!*.{x}' -- '{prefix}'"]
+ )
+ else:
+ completion += _wrapcall(["bash", "-c", f"compgen -A file -- '{prefix}'"])
+
+ anticomp = _wrapcall(["bash", "-c", f"compgen -A directory -- '{prefix}'"])
+
+ completion = list(set(completion) - set(anticomp))
+
+ if self.directories:
+ completion += [f + "/" for f in anticomp]
+ return completion
+
+
+class TestArgComplete:
+ @pytest.mark.skipif("sys.platform in ('win32', 'darwin')")
+ def test_compare_with_compgen(
+ self, tmp_path: Path, monkeypatch: MonkeyPatch
+ ) -> None:
+ from _pytest._argcomplete import FastFilesCompleter
+
+ ffc = FastFilesCompleter()
+ fc = FilesCompleter()
+
+ monkeypatch.chdir(tmp_path)
+
+ assert equal_with_bash("", ffc, fc, out=sys.stdout)
+
+ tmp_path.cwd().joinpath("data").touch()
+
+ for x in ["d", "data", "doesnotexist", ""]:
+ assert equal_with_bash(x, ffc, fc, out=sys.stdout)
+
+ @pytest.mark.skipif("sys.platform in ('win32', 'darwin')")
+ def test_remove_dir_prefix(self):
+ """This is not compatible with compgen but it is with bash itself: ls /usr/<TAB>."""
+ from _pytest._argcomplete import FastFilesCompleter
+
+ ffc = FastFilesCompleter()
+ fc = FilesCompleter()
+ for x in "/usr/".split():
+ assert not equal_with_bash(x, ffc, fc, out=sys.stdout)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_assertion.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_assertion.py
new file mode 100644
index 0000000000..2516ff1629
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_assertion.py
@@ -0,0 +1,1685 @@
+import collections
+import sys
+import textwrap
+from typing import Any
+from typing import List
+from typing import MutableSequence
+from typing import Optional
+
+import attr
+
+import _pytest.assertion as plugin
+import pytest
+from _pytest import outcomes
+from _pytest.assertion import truncate
+from _pytest.assertion import util
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pytester import Pytester
+
+
+def mock_config(verbose=0):
+ class Config:
+ def getoption(self, name):
+ if name == "verbose":
+ return verbose
+ raise KeyError("Not mocked out: %s" % name)
+
+ return Config()
+
+
+class TestImportHookInstallation:
+ @pytest.mark.parametrize("initial_conftest", [True, False])
+ @pytest.mark.parametrize("mode", ["plain", "rewrite"])
+ def test_conftest_assertion_rewrite(
+ self, pytester: Pytester, initial_conftest, mode
+ ) -> None:
+ """Test that conftest files are using assertion rewrite on import (#1619)."""
+ pytester.mkdir("foo")
+ pytester.mkdir("foo/tests")
+ conftest_path = "conftest.py" if initial_conftest else "foo/conftest.py"
+ contents = {
+ conftest_path: """
+ import pytest
+ @pytest.fixture
+ def check_first():
+ def check(values, value):
+ assert values.pop(0) == value
+ return check
+ """,
+ "foo/tests/test_foo.py": """
+ def test(check_first):
+ check_first([10, 30], 30)
+ """,
+ }
+ pytester.makepyfile(**contents)
+ result = pytester.runpytest_subprocess("--assert=%s" % mode)
+ if mode == "plain":
+ expected = "E AssertionError"
+ elif mode == "rewrite":
+ expected = "*assert 10 == 30*"
+ else:
+ assert 0
+ result.stdout.fnmatch_lines([expected])
+
+ def test_rewrite_assertions_pytester_plugin(self, pytester: Pytester) -> None:
+ """
+ Assertions in the pytester plugin must also benefit from assertion
+ rewriting (#1920).
+ """
+ pytester.makepyfile(
+ """
+ pytest_plugins = ['pytester']
+ def test_dummy_failure(pytester): # how meta!
+ pytester.makepyfile('def test(): assert 0')
+ r = pytester.inline_run()
+ r.assertoutcome(passed=1)
+ """
+ )
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(
+ [
+ "> r.assertoutcome(passed=1)",
+ "E AssertionError: ([[][]], [[][]], [[]<TestReport *>[]])*",
+ "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",
+ ]
+ )
+ # XXX: unstable output.
+ result.stdout.fnmatch_lines_random(
+ [
+ "E {'failed': 1} != {'failed': 0}",
+ "E {'passed': 0} != {'passed': 1}",
+ ]
+ )
+
+ @pytest.mark.parametrize("mode", ["plain", "rewrite"])
+ def test_pytest_plugins_rewrite(self, pytester: Pytester, mode) -> None:
+ contents = {
+ "conftest.py": """
+ pytest_plugins = ['ham']
+ """,
+ "ham.py": """
+ import pytest
+ @pytest.fixture
+ def check_first():
+ def check(values, value):
+ assert values.pop(0) == value
+ return check
+ """,
+ "test_foo.py": """
+ def test_foo(check_first):
+ check_first([10, 30], 30)
+ """,
+ }
+ pytester.makepyfile(**contents)
+ result = pytester.runpytest_subprocess("--assert=%s" % mode)
+ if mode == "plain":
+ expected = "E AssertionError"
+ elif mode == "rewrite":
+ expected = "*assert 10 == 30*"
+ else:
+ assert 0
+ result.stdout.fnmatch_lines([expected])
+
+ @pytest.mark.parametrize("mode", ["str", "list"])
+ def test_pytest_plugins_rewrite_module_names(
+ self, pytester: Pytester, mode
+ ) -> None:
+ """Test that pluginmanager correct marks pytest_plugins variables
+ for assertion rewriting if they are defined as plain strings or
+ list of strings (#1888).
+ """
+ plugins = '"ham"' if mode == "str" else '["ham"]'
+ contents = {
+ "conftest.py": """
+ pytest_plugins = {plugins}
+ """.format(
+ plugins=plugins
+ ),
+ "ham.py": """
+ import pytest
+ """,
+ "test_foo.py": """
+ def test_foo(pytestconfig):
+ assert 'ham' in pytestconfig.pluginmanager.rewrite_hook._must_rewrite
+ """,
+ }
+ pytester.makepyfile(**contents)
+ result = pytester.runpytest_subprocess("--assert=rewrite")
+ assert result.ret == 0
+
+ def test_pytest_plugins_rewrite_module_names_correctly(
+ self, pytester: Pytester
+ ) -> None:
+ """Test that we match files correctly when they are marked for rewriting (#2939)."""
+ contents = {
+ "conftest.py": """\
+ pytest_plugins = "ham"
+ """,
+ "ham.py": "",
+ "hamster.py": "",
+ "test_foo.py": """\
+ def test_foo(pytestconfig):
+ assert pytestconfig.pluginmanager.rewrite_hook.find_spec('ham') is not None
+ assert pytestconfig.pluginmanager.rewrite_hook.find_spec('hamster') is None
+ """,
+ }
+ pytester.makepyfile(**contents)
+ result = pytester.runpytest_subprocess("--assert=rewrite")
+ assert result.ret == 0
+
+ @pytest.mark.parametrize("mode", ["plain", "rewrite"])
+ def test_installed_plugin_rewrite(
+ self, pytester: Pytester, mode, monkeypatch
+ ) -> None:
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
+ # Make sure the hook is installed early enough so that plugins
+ # installed via setuptools are rewritten.
+ pytester.mkdir("hampkg")
+ contents = {
+ "hampkg/__init__.py": """\
+ import pytest
+
+ @pytest.fixture
+ def check_first2():
+ def check(values, value):
+ assert values.pop(0) == value
+ return check
+ """,
+ "spamplugin.py": """\
+ import pytest
+ from hampkg import check_first2
+
+ @pytest.fixture
+ def check_first():
+ def check(values, value):
+ assert values.pop(0) == value
+ return check
+ """,
+ "mainwrapper.py": """\
+ import pytest
+ from _pytest.compat import importlib_metadata
+
+ class DummyEntryPoint(object):
+ name = 'spam'
+ module_name = 'spam.py'
+ group = 'pytest11'
+
+ def load(self):
+ import spamplugin
+ return spamplugin
+
+ class DummyDistInfo(object):
+ version = '1.0'
+ files = ('spamplugin.py', 'hampkg/__init__.py')
+ entry_points = (DummyEntryPoint(),)
+ metadata = {'name': 'foo'}
+
+ def distributions():
+ return (DummyDistInfo(),)
+
+ importlib_metadata.distributions = distributions
+ pytest.main()
+ """,
+ "test_foo.py": """\
+ def test(check_first):
+ check_first([10, 30], 30)
+
+ def test2(check_first2):
+ check_first([10, 30], 30)
+ """,
+ }
+ pytester.makepyfile(**contents)
+ result = pytester.run(
+ sys.executable, "mainwrapper.py", "-s", "--assert=%s" % mode
+ )
+ if mode == "plain":
+ expected = "E AssertionError"
+ elif mode == "rewrite":
+ expected = "*assert 10 == 30*"
+ else:
+ assert 0
+ result.stdout.fnmatch_lines([expected])
+
+ def test_rewrite_ast(self, pytester: Pytester) -> None:
+ pytester.mkdir("pkg")
+ contents = {
+ "pkg/__init__.py": """
+ import pytest
+ pytest.register_assert_rewrite('pkg.helper')
+ """,
+ "pkg/helper.py": """
+ def tool():
+ a, b = 2, 3
+ assert a == b
+ """,
+ "pkg/plugin.py": """
+ import pytest, pkg.helper
+ @pytest.fixture
+ def tool():
+ return pkg.helper.tool
+ """,
+ "pkg/other.py": """
+ values = [3, 2]
+ def tool():
+ assert values.pop() == 3
+ """,
+ "conftest.py": """
+ pytest_plugins = ['pkg.plugin']
+ """,
+ "test_pkg.py": """
+ import pkg.other
+ def test_tool(tool):
+ tool()
+ def test_other():
+ pkg.other.tool()
+ """,
+ }
+ pytester.makepyfile(**contents)
+ result = pytester.runpytest_subprocess("--assert=rewrite")
+ result.stdout.fnmatch_lines(
+ [
+ ">*assert a == b*",
+ "E*assert 2 == 3*",
+ ">*assert values.pop() == 3*",
+ "E*AssertionError",
+ ]
+ )
+
+ def test_register_assert_rewrite_checks_types(self) -> None:
+ with pytest.raises(TypeError):
+ pytest.register_assert_rewrite(["pytest_tests_internal_non_existing"]) # type: ignore
+ pytest.register_assert_rewrite(
+ "pytest_tests_internal_non_existing", "pytest_tests_internal_non_existing2"
+ )
+
+
+class TestBinReprIntegration:
+ def test_pytest_assertrepr_compare_called(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ values = []
+ def pytest_assertrepr_compare(op, left, right):
+ values.append((op, left, right))
+
+ @pytest.fixture
+ def list(request):
+ return values
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_hello():
+ assert 0 == 1
+ def test_check(list):
+ assert list == [("==", 0, 1)]
+ """
+ )
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(["*test_hello*FAIL*", "*test_check*PASS*"])
+
+
+def callop(op: str, left: Any, right: Any, verbose: int = 0) -> Optional[List[str]]:
+ config = mock_config(verbose=verbose)
+ return plugin.pytest_assertrepr_compare(config, op, left, right)
+
+
+def callequal(left: Any, right: Any, verbose: int = 0) -> Optional[List[str]]:
+ return callop("==", left, right, verbose)
+
+
+class TestAssert_reprcompare:
+ def test_different_types(self) -> None:
+ assert callequal([0, 1], "foo") is None
+
+ def test_summary(self) -> None:
+ lines = callequal([0, 1], [0, 2])
+ assert lines is not None
+ summary = lines[0]
+ assert len(summary) < 65
+
+ def test_text_diff(self) -> None:
+ assert callequal("spam", "eggs") == [
+ "'spam' == 'eggs'",
+ "- eggs",
+ "+ spam",
+ ]
+
+ def test_text_skipping(self) -> None:
+ lines = callequal("a" * 50 + "spam", "a" * 50 + "eggs")
+ assert lines is not None
+ assert "Skipping" in lines[1]
+ for line in lines:
+ assert "a" * 50 not in line
+
+ def test_text_skipping_verbose(self) -> None:
+ lines = callequal("a" * 50 + "spam", "a" * 50 + "eggs", verbose=1)
+ assert lines is not None
+ assert "- " + "a" * 50 + "eggs" in lines
+ assert "+ " + "a" * 50 + "spam" in lines
+
+ def test_multiline_text_diff(self) -> None:
+ left = "foo\nspam\nbar"
+ right = "foo\neggs\nbar"
+ diff = callequal(left, right)
+ assert diff is not None
+ assert "- eggs" in diff
+ assert "+ spam" in diff
+
+ def test_bytes_diff_normal(self) -> None:
+ """Check special handling for bytes diff (#5260)"""
+ diff = callequal(b"spam", b"eggs")
+
+ assert diff == [
+ "b'spam' == b'eggs'",
+ "At index 0 diff: b's' != b'e'",
+ "Use -v to get the full diff",
+ ]
+
+ def test_bytes_diff_verbose(self) -> None:
+ """Check special handling for bytes diff (#5260)"""
+ 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'",
+ ]
+
+ def test_list(self) -> None:
+ expl = callequal([0, 1], [0, 2])
+ assert expl is not None
+ assert len(expl) > 1
+
+ @pytest.mark.parametrize(
+ ["left", "right", "expected"],
+ [
+ pytest.param(
+ [0, 1],
+ [0, 2],
+ """
+ Full diff:
+ - [0, 2]
+ ? ^
+ + [0, 1]
+ ? ^
+ """,
+ id="lists",
+ ),
+ pytest.param(
+ {0: 1},
+ {0: 2},
+ """
+ Full diff:
+ - {0: 2}
+ ? ^
+ + {0: 1}
+ ? ^
+ """,
+ id="dicts",
+ ),
+ pytest.param(
+ {0, 1},
+ {0, 2},
+ """
+ Full diff:
+ - {0, 2}
+ ? ^
+ + {0, 1}
+ ? ^
+ """,
+ id="sets",
+ ),
+ ],
+ )
+ def test_iterable_full_diff(self, left, right, expected) -> None:
+ """Test the full diff assertion failure explanation.
+
+ When verbose is False, then just a -v notice to get the diff is rendered,
+ when verbose is True, then ndiff of the pprint is returned.
+ """
+ expl = callequal(left, right, verbose=0)
+ assert expl is not None
+ assert expl[-1] == "Use -v to get the full 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_full_diff_ci(
+ self, monkeypatch: MonkeyPatch, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ r"""
+ def test_full_diff():
+ left = [0, 1]
+ right = [0, 2]
+ assert left == right
+ """
+ )
+ monkeypatch.setenv("CI", "true")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["E Full diff:"])
+
+ monkeypatch.delenv("CI", raising=False)
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["E Use -v to get the full diff"])
+
+ def test_list_different_lengths(self) -> None:
+ expl = callequal([0, 1], [0, 1, 2])
+ assert expl is not None
+ assert len(expl) > 1
+ expl = callequal([0, 1, 2], [0, 1])
+ assert expl is not None
+ assert len(expl) > 1
+
+ def test_list_wrap_for_multiple_lines(self) -> None:
+ long_d = "d" * 80
+ l1 = ["a", "b", "c"]
+ l2 = ["a", "b", "c", long_d]
+ 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 + "',",
+ " ]",
+ ]
+
+ 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 + "',",
+ " ]",
+ ]
+
+ def test_list_wrap_for_width_rewrap_same_length(self) -> None:
+ long_a = "a" * 30
+ long_b = "b" * 30
+ long_c = "c" * 30
+ l1 = [long_a, long_b, long_c]
+ l2 = [long_b, long_c, long_a]
+ diff = callequal(l1, l2, verbose=True)
+ assert diff == [
+ "['aaaaaaaaaaa...cccccccccccc'] == ['bbbbbbbbbbb...aaaaaaaaaaaa']",
+ "At index 0 diff: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' != 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'",
+ "Full diff:",
+ " [",
+ "+ '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)]
+ 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',",
+ " ]",
+ ]
+
+ def test_dict_wrap(self) -> None:
+ d1 = {"common": 1, "env": {"env1": 1, "env2": 2}}
+ d2 = {"common": 1, "env": {"env1": 1}}
+
+ 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}}",
+ "? +++++++++++",
+ ]
+
+ long_a = "a" * 80
+ sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 2}}
+ 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,",
+ " }",
+ ]
+
+ def test_dict(self) -> None:
+ expl = callequal({"a": 0}, {"a": 1})
+ assert expl is not None
+ assert len(expl) > 1
+
+ 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 "Common items" not in lines
+ for line in lines[1:]:
+ assert "b" not in line
+
+ def test_dict_omitting_with_verbosity_1(self) -> None:
+ """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 "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}"
+
+ 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}",
+ ]
+ 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}",
+ ]
+
+ 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)",
+ ]
+ 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)",
+ ]
+
+ def test_set(self) -> None:
+ expl = callequal({0, 1}, {0, 2})
+ assert expl is not None
+ assert len(expl) > 1
+
+ def test_frozenzet(self) -> None:
+ expl = callequal(frozenset([0, 1]), {0, 2})
+ assert expl is not None
+ assert len(expl) > 1
+
+ def test_Sequence(self) -> None:
+ # Test comparing with a Sequence subclass.
+ class TestSequence(MutableSequence[int]):
+ def __init__(self, iterable):
+ self.elements = list(iterable)
+
+ def __getitem__(self, item):
+ return self.elements[item]
+
+ def __len__(self):
+ return len(self.elements)
+
+ def __setitem__(self, item, value):
+ pass
+
+ def __delitem__(self, item):
+ pass
+
+ def insert(self, item, index):
+ pass
+
+ expl = callequal(TestSequence([0, 1]), list([0, 2]))
+ assert expl is not None
+ assert len(expl) > 1
+
+ def test_list_tuples(self) -> None:
+ expl = callequal([], [(1, 2)])
+ assert expl is not None
+ assert len(expl) > 1
+ expl = callequal([(1, 2)], [])
+ 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):
+ raise ValueError(42)
+
+ expl = callequal([], [A()])
+ assert expl is not None
+ assert "ValueError" in "".join(expl)
+ expl = callequal({}, {"1": A()}, verbose=2)
+ assert expl is not None
+ assert expl[0].startswith("{} == <[ValueError")
+ assert "raised in repr" in expl[0]
+ assert expl[1:] == [
+ "(pytest_assertion plugin: representation of details failed:"
+ " {}:{}: ValueError: 42.".format(
+ __file__, A.__repr__.__code__.co_firstlineno + 1
+ ),
+ " Probably an object has a faulty __repr__.)",
+ ]
+
+ def test_one_repr_empty(self) -> None:
+ """The faulty empty string repr did trigger an unbound local error in _diff_text."""
+
+ class A(str):
+ def __repr__(self):
+ return ""
+
+ expl = callequal(A(), "")
+ assert not expl
+
+ def test_repr_no_exc(self) -> None:
+ expl = callequal("foo", "bar")
+ assert expl is not None
+ assert "raised in repr()" not in " ".join(expl)
+
+ def test_unicode(self) -> None:
+ assert callequal("£€", "£") == [
+ "'£€' == '£'",
+ "- £",
+ "+ £€",
+ ]
+
+ def test_nonascii_text(self) -> None:
+ """
+ :issue: 877
+ non ascii python2 str caused a UnicodeDecodeError
+ """
+
+ class A(str):
+ def __repr__(self):
+ return "\xff"
+
+ expl = callequal(A(), "1")
+ assert expl == ["ÿ == '1'", "- 1"]
+
+ def test_format_nonascii_explanation(self) -> None:
+ assert util.format_explanation("λ")
+
+ def test_mojibake(self) -> None:
+ # issue 429
+ left = b"e"
+ right = b"\xc3\xa9"
+ expl = callequal(left, right)
+ assert expl is not None
+ for line in expl:
+ assert isinstance(line, str)
+ msg = "\n".join(expl)
+ assert msg
+
+
+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)
+ result.assert_outcomes(failed=1, passed=0)
+ result.stdout.fnmatch_lines(
+ [
+ "E Omitting 1 identical items, use -vv to show",
+ "E Differing attributes:",
+ "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",
+ ],
+ 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)
+ result.assert_outcomes(failed=1, passed=0)
+ result.stdout.fnmatch_lines(
+ [
+ "E Omitting 1 identical items, use -vv to show",
+ "E Differing attributes:",
+ "E ['g', 'h', 'j']",
+ "E ",
+ "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",
+ ],
+ 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")
+ result.assert_outcomes(failed=1, passed=0)
+ result.stdout.fnmatch_lines(
+ [
+ "E Matching attributes:",
+ "E ['i']",
+ "E Differing attributes:",
+ "E ['g', 'h', 'j']",
+ "E ",
+ "E Drill down into differing attribute g:",
+ "E g: S(a=10, b='ten') != S(a=20, b='xxx')",
+ "E ",
+ "E Differing attributes:",
+ "E ['a', 'b']",
+ "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'",
+ "E - xxx",
+ "E + ten",
+ "E ",
+ "E Drill down into differing attribute h:",
+ ],
+ 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")
+ result.assert_outcomes(failed=1, passed=0)
+ result.stdout.fnmatch_lines(
+ [
+ "*Matching attributes:*",
+ "*['field_a']*",
+ "*Differing attributes:*",
+ "*field_b: 'b' != 'c'*",
+ ]
+ )
+
+ @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
+ def test_dataclasses_with_attribute_comparison_off(
+ self, pytester: Pytester
+ ) -> None:
+ p = pytester.copy_example(
+ "dataclasses/test_compare_dataclasses_field_comparison_off.py"
+ )
+ 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"
+ )
+ result = pytester.runpytest(p, "-vv")
+ result.assert_outcomes(failed=0, passed=1)
+
+
+class TestAssert_reprcompare_attrsclass:
+ def test_attrs(self) -> None:
+ @attr.s
+ class SimpleDataObject:
+ field_a = attr.ib()
+ field_b = attr.ib()
+
+ left = SimpleDataObject(1, "b")
+ right = SimpleDataObject(1, "c")
+
+ lines = callequal(left, right)
+ assert lines is not None
+ assert lines[2].startswith("Omitting 1 identical item")
+ assert "Matching attributes" not in lines
+ for line in lines[2:]:
+ assert "field_a" not in line
+
+ def test_attrs_recursive(self) -> None:
+ @attr.s
+ class OtherDataObject:
+ field_c = attr.ib()
+ field_d = attr.ib()
+
+ @attr.s
+ class SimpleDataObject:
+ field_a = attr.ib()
+ field_b = attr.ib()
+
+ left = SimpleDataObject(OtherDataObject(1, "a"), "b")
+ right = SimpleDataObject(OtherDataObject(1, "b"), "b")
+
+ lines = callequal(left, right)
+ assert lines is not None
+ assert "Matching attributes" not in lines
+ for line in lines[1:]:
+ assert "field_b:" not in line
+ assert "field_c:" not in line
+
+ def test_attrs_recursive_verbose(self) -> None:
+ @attr.s
+ class OtherDataObject:
+ field_c = attr.ib()
+ field_d = attr.ib()
+
+ @attr.s
+ class SimpleDataObject:
+ field_a = attr.ib()
+ field_b = attr.ib()
+
+ left = SimpleDataObject(OtherDataObject(1, "a"), "b")
+ right = SimpleDataObject(OtherDataObject(1, "b"), "b")
+
+ lines = callequal(left, right)
+ assert lines is not None
+ # indentation in output because of nested object structure
+ assert " field_d: 'a' != 'b'" in lines
+
+ def test_attrs_verbose(self) -> None:
+ @attr.s
+ class SimpleDataObject:
+ field_a = attr.ib()
+ field_b = attr.ib()
+
+ left = SimpleDataObject(1, "b")
+ right = SimpleDataObject(1, "c")
+
+ lines = callequal(left, right, verbose=2)
+ assert lines is not None
+ assert lines[2].startswith("Matching attributes:")
+ assert "Omitting" not in lines[2]
+ assert lines[3] == "['field_a']"
+
+ def test_attrs_with_attribute_comparison_off(self) -> None:
+ @attr.s
+ class SimpleDataObject:
+ field_a = attr.ib()
+ field_b = attr.ib(eq=False)
+
+ left = SimpleDataObject(1, "b")
+ 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]
+ assert lines[3] == "['field_a']"
+ for line in lines[3:]:
+ assert "field_b" not in line
+
+ def test_comparing_two_different_attrs_classes(self) -> None:
+ @attr.s
+ class SimpleDataObjectOne:
+ field_a = attr.ib()
+ field_b = attr.ib()
+
+ @attr.s
+ class SimpleDataObjectTwo:
+ field_a = attr.ib()
+ field_b = attr.ib()
+
+ left = SimpleDataObjectOne(1, "b")
+ right = SimpleDataObjectTwo(1, "c")
+
+ lines = callequal(left, right)
+ assert lines is None
+
+
+class TestAssert_reprcompare_namedtuple:
+ def test_namedtuple(self) -> None:
+ NT = collections.namedtuple("NT", ["a", "b"])
+
+ left = NT(1, "b")
+ right = NT(1, "c")
+
+ lines = callequal(left, right)
+ assert lines == [
+ "NT(a=1, b='b') == NT(a=1, b='c')",
+ "",
+ "Omitting 1 identical items, use -vv to show",
+ "Differing attributes:",
+ "['b']",
+ "",
+ "Drill down into differing attribute b:",
+ " b: 'b' != 'c'",
+ " - c",
+ " + b",
+ "Use -v to get the full diff",
+ ]
+
+ def test_comparing_two_different_namedtuple(self) -> None:
+ NT1 = collections.namedtuple("NT1", ["a", "b"])
+ NT2 = collections.namedtuple("NT2", ["a", "b"])
+
+ left = NT1(1, "b")
+ right = NT2(2, "b")
+
+ lines = callequal(left, right)
+ # 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",
+ ]
+
+
+class TestFormatExplanation:
+ def test_special_chars_full(self, pytester: Pytester) -> None:
+ # Issue 453, for the bug this would raise IndexError
+ pytester.makepyfile(
+ """
+ def test_foo():
+ assert '\\n}' == ''
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(["*AssertionError*"])
+
+ def test_fmt_simple(self) -> None:
+ expl = "assert foo"
+ assert util.format_explanation(expl) == "assert foo"
+
+ def test_fmt_where(self) -> None:
+ expl = "\n".join(["assert 1", "{1 = foo", "} == 2"])
+ res = "\n".join(["assert 1 == 2", " + where 1 = foo"])
+ assert util.format_explanation(expl) == res
+
+ def test_fmt_and(self) -> None:
+ expl = "\n".join(["assert 1", "{1 = foo", "} == 2", "{2 = bar", "}"])
+ res = "\n".join(["assert 1 == 2", " + where 1 = foo", " + and 2 = bar"])
+ assert util.format_explanation(expl) == res
+
+ def test_fmt_where_nested(self) -> None:
+ expl = "\n".join(["assert 1", "{1 = foo", "{foo = bar", "}", "} == 2"])
+ res = "\n".join(["assert 1 == 2", " + where 1 = foo", " + where foo = bar"])
+ assert util.format_explanation(expl) == res
+
+ def test_fmt_newline(self) -> None:
+ expl = "\n".join(['assert "foo" == "bar"', "~- foo", "~+ bar"])
+ res = "\n".join(['assert "foo" == "bar"', " - foo", " + bar"])
+ assert util.format_explanation(expl) == res
+
+ def test_fmt_newline_escaped(self) -> None:
+ expl = "\n".join(["assert foo == bar", "baz"])
+ res = "assert foo == bar\\nbaz"
+ assert util.format_explanation(expl) == res
+
+ def test_fmt_newline_before_where(self) -> None:
+ expl = "\n".join(
+ [
+ "the assertion message here",
+ ">assert 1",
+ "{1 = foo",
+ "} == 2",
+ "{2 = bar",
+ "}",
+ ]
+ )
+ res = "\n".join(
+ [
+ "the assertion message here",
+ "assert 1 == 2",
+ " + where 1 = foo",
+ " + and 2 = bar",
+ ]
+ )
+ assert util.format_explanation(expl) == res
+
+ def test_fmt_multi_newline_before_where(self) -> None:
+ expl = "\n".join(
+ [
+ "the assertion",
+ "~message here",
+ ">assert 1",
+ "{1 = foo",
+ "} == 2",
+ "{2 = bar",
+ "}",
+ ]
+ )
+ res = "\n".join(
+ [
+ "the assertion",
+ " message here",
+ "assert 1 == 2",
+ " + where 1 = foo",
+ " + and 2 = bar",
+ ]
+ )
+ assert util.format_explanation(expl) == res
+
+
+class TestTruncateExplanation:
+ # The number of lines in the truncation explanation message. Used
+ # to calculate that results have the expected length.
+ LINES_IN_TRUNCATION_MSG = 2
+
+ def test_doesnt_truncate_when_input_is_empty_list(self) -> None:
+ expl: List[str] = []
+ result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100)
+ assert result == expl
+
+ def test_doesnt_truncate_at_when_input_is_5_lines_and_LT_max_chars(self) -> None:
+ expl = ["a" * 100 for x in range(5)]
+ result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
+ assert result == expl
+
+ 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 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]
+ 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)]
+ 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]
+ 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_EQ_max_chars(self) -> None:
+ expl = ["a" * 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 "Full output truncated" in result[-1]
+ assert "9 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_4_lines_when_first_4_lines_are_GT_max_chars(self) -> None:
+ expl = ["a" * 250 for x in range(10)]
+ result = truncate._truncate_explanation(expl, max_lines=8, max_chars=999)
+ assert result != expl
+ assert len(result) == 4 + self.LINES_IN_TRUNCATION_MSG
+ assert "Full output truncated" in result[-1]
+ assert "7 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_1_line_when_first_line_is_GT_max_chars(self) -> None:
+ expl = ["a" * 250 for x in range(1000)]
+ result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100)
+ assert result != expl
+ assert len(result) == 1 + self.LINES_IN_TRUNCATION_MSG
+ assert "Full output truncated" in result[-1]
+ assert "1000 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_full_output_truncated(self, monkeypatch, pytester: Pytester) -> None:
+ """Test against full runpytest() output."""
+
+ line_count = 7
+ line_len = 100
+ expected_truncated_lines = 2
+ pytester.makepyfile(
+ r"""
+ def test_many_lines():
+ a = list([str(i)[0] * %d for i in range(%d)])
+ b = a[::2]
+ a = '\n'.join(map(str, a))
+ b = '\n'.join(map(str, b))
+ assert a == b
+ """
+ % (line_len, line_count)
+ )
+ monkeypatch.delenv("CI", raising=False)
+
+ result = pytester.runpytest()
+ # without -vv, truncate the message showing a few diff lines only
+ result.stdout.fnmatch_lines(
+ [
+ "*+ 1*",
+ "*+ 3*",
+ "*+ 5*",
+ "*truncated (%d lines hidden)*use*-vv*" % expected_truncated_lines,
+ ]
+ )
+
+ result = pytester.runpytest("-vv")
+ result.stdout.fnmatch_lines(["* 6*"])
+
+ monkeypatch.setenv("CI", "1")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 6*"])
+
+
+def test_python25_compile_issue257(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_rewritten():
+ assert 1 == 2
+ # some comment
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(
+ """
+ *E*assert 1 == 2*
+ *1 failed*
+ """
+ )
+
+
+def test_rewritten(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_rewritten():
+ assert "@py_builtins" in globals()
+ """
+ )
+ assert pytester.runpytest().ret == 0
+
+
+def test_reprcompare_notin() -> None:
+ assert callop("not in", "foo", "aaafoobbb") == [
+ "'foo' not in 'aaafoobbb'",
+ "'foo' is contained here:",
+ " aaafoobbb",
+ "? +++",
+ ]
+
+
+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'",
+ r"? ++",
+ ]
+
+
+def test_pytest_assertrepr_compare_integration(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ 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*",
+ ]
+ )
+
+
+def test_sequence_comparison_uses_repr(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ 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'*",
+ ]
+ )
+
+
+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("conftest.py").write_text(
+ 'def pytest_assertrepr_compare(): return ["summary a"]'
+ )
+ b = pytester.mkdir("b")
+ b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2")
+ b.joinpath("conftest.py").write_text(
+ 'def pytest_assertrepr_compare(): return ["summary b"]'
+ )
+
+ 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_assertion_options(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_hello():
+ x = 3
+ assert x == 4
+ """
+ )
+ result = pytester.runpytest()
+ assert "3 == 4" in result.stdout.str()
+ result = pytester.runpytest_subprocess("--assert=plain")
+ result.stdout.no_fnmatch_line("*3 == 4*")
+
+
+def test_triple_quoted_string_issue113(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_hello():
+ assert "" == '''
+ '''"""
+ )
+ result = pytester.runpytest("--fulltrace")
+ result.stdout.fnmatch_lines(["*1 failed*"])
+ result.stdout.no_fnmatch_line("*SyntaxError*")
+
+
+def test_traceback_failure(pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def g():
+ return 2
+ def f(x):
+ assert x == g()
+ def test_onefails():
+ f(3)
+ """
+ )
+ result = pytester.runpytest(p1, "--tb=long")
+ result.stdout.fnmatch_lines(
+ [
+ "*test_traceback_failure.py F*",
+ "====* FAILURES *====",
+ "____*____",
+ "",
+ " def test_onefails():",
+ "> f(3)",
+ "",
+ "*test_*.py:6: ",
+ "_ _ _ *",
+ # "",
+ " def f(x):",
+ "> assert x == g()",
+ "E assert 3 == 2",
+ "E + where 2 = g()",
+ "",
+ "*test_traceback_failure.py:4: AssertionError",
+ ]
+ )
+
+ result = pytester.runpytest(p1) # "auto"
+ result.stdout.fnmatch_lines(
+ [
+ "*test_traceback_failure.py F*",
+ "====* FAILURES *====",
+ "____*____",
+ "",
+ " def test_onefails():",
+ "> f(3)",
+ "",
+ "*test_*.py:6: ",
+ "",
+ " def f(x):",
+ "> assert x == g()",
+ "E assert 3 == 2",
+ "E + where 2 = g()",
+ "",
+ "*test_traceback_failure.py:4: AssertionError",
+ ]
+ )
+
+
+def test_exception_handling_no_traceback(pytester: Pytester) -> None:
+ """Handle chain exceptions in tasks submitted by the multiprocess module (#1984)."""
+ p1 = pytester.makepyfile(
+ """
+ from multiprocessing import Pool
+
+ def process_task(n):
+ assert n == 10
+
+ def multitask_job():
+ tasks = [1]
+ with Pool(processes=1) as pool:
+ pool.map(process_task, tasks)
+
+ def test_multitask_job():
+ multitask_job()
+ """
+ )
+ pytester.syspathinsert()
+ result = pytester.runpytest(p1, "--tb=long")
+ result.stdout.fnmatch_lines(
+ [
+ "====* FAILURES *====",
+ "*multiprocessing.pool.RemoteTraceback:*",
+ "Traceback (most recent call last):",
+ "*assert n == 10",
+ "The above exception was the direct cause of the following exception:",
+ "> * multitask_job()",
+ ]
+ )
+
+
+@pytest.mark.skipif("'__pypy__' in sys.builtin_module_names")
+@pytest.mark.parametrize(
+ "cmdline_args, warning_output",
+ [
+ (
+ ["-OO", "-m", "pytest", "-h"],
+ ["warning :*PytestConfigWarning:*assert statements are not executed*"],
+ ),
+ (
+ ["-OO", "-m", "pytest"],
+ [
+ "=*= warnings summary =*=",
+ "*PytestConfigWarning:*assert statements are not executed*",
+ ],
+ ),
+ (
+ ["-OO", "-m", "pytest", "--assert=plain"],
+ [
+ "=*= warnings summary =*=",
+ "*PytestConfigWarning: ASSERTIONS ARE NOT EXECUTED and FAILING TESTS WILL PASS. "
+ "Are you using python -O?",
+ ],
+ ),
+ ],
+)
+def test_warn_missing(pytester: Pytester, cmdline_args, warning_output) -> None:
+ pytester.makepyfile("")
+
+ result = pytester.run(sys.executable, *cmdline_args)
+ result.stdout.fnmatch_lines(warning_output)
+
+
+def test_recursion_source_decode(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_something():
+ pass
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ python_files = *.py
+ """
+ )
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(
+ """
+ <Module*>
+ """
+ )
+
+
+def test_AssertionError_message(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_hello():
+ x,y = 1,2
+ assert 0, (x,y)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ """
+ *def test_hello*
+ *assert 0, (x,y)*
+ *AssertionError: (1, 2)*
+ """
+ )
+
+
+def test_diff_newline_at_end(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ r"""
+ def test_diff():
+ assert 'asdf' == 'asdf\n'
+ """
+ )
+
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ r"""
+ *assert 'asdf' == 'asdf\n'
+ * - asdf
+ * ? -
+ * + asdf
+ """
+ )
+
+
+@pytest.mark.filterwarnings("default")
+def test_assert_tuple_warning(pytester: Pytester) -> None:
+ msg = "assertion is always true"
+ pytester.makepyfile(
+ """
+ def test_tuple():
+ assert(False, 'you shall not pass')
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines([f"*test_assert_tuple_warning.py:2:*{msg}*"])
+
+ # tuples with size != 2 should not trigger the warning
+ pytester.makepyfile(
+ """
+ def test_tuple():
+ assert ()
+ """
+ )
+ result = pytester.runpytest()
+ assert msg not in result.stdout.str()
+
+
+def test_assert_indirect_tuple_no_warning(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_tuple():
+ tpl = ('foo', 'bar')
+ assert tpl
+ """
+ )
+ result = pytester.runpytest()
+ output = "\n".join(result.stdout.lines)
+ assert "WR1" not in output
+
+
+def test_assert_with_unicode(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """\
+ def test_unicode():
+ assert '유니코드' == 'Unicode'
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*AssertionError*"])
+
+
+def test_raise_unprintable_assertion_error(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ r"""
+ def test_raise_assertion_error():
+ raise AssertionError('\xff')
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [r"> raise AssertionError('\xff')", "E AssertionError: *"]
+ )
+
+
+def test_raise_assertion_error_raising_repr(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ class RaisingRepr(object):
+ def __repr__(self):
+ raise Exception()
+ def test_raising_repr():
+ raise AssertionError(RaisingRepr())
+ """
+ )
+ 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>"]
+ )
+
+
+def test_issue_1944(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def f():
+ return
+
+ assert f() == 10
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 error*"])
+ assert (
+ "AttributeError: 'Module' object has no attribute '_obj'"
+ not in result.stdout.str()
+ )
+
+
+def test_exit_from_assertrepr_compare(monkeypatch) -> None:
+ def raise_exit(obj):
+ outcomes.exit("Quitting debugger")
+
+ monkeypatch.setattr(util, "istext", raise_exit)
+
+ with pytest.raises(outcomes.Exit, match="Quitting debugger"):
+ callequal(1, 1)
+
+
+def test_assertion_location_with_coverage(pytester: Pytester) -> None:
+ """This used to report the wrong location when run with coverage (#5754)."""
+ p = pytester.makepyfile(
+ """
+ def test():
+ assert False, 1
+ assert False, 2
+ """
+ )
+ result = pytester.runpytest(str(p))
+ result.stdout.fnmatch_lines(
+ [
+ "> assert False, 1",
+ "E AssertionError: 1",
+ "E assert False",
+ "*= 1 failed in*",
+ ]
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_assertrewrite.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_assertrewrite.py
new file mode 100644
index 0000000000..4417eb4350
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_assertrewrite.py
@@ -0,0 +1,1841 @@
+import ast
+import errno
+import glob
+import importlib
+import marshal
+import os
+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 List
+from typing import Mapping
+from typing import Optional
+from typing import Set
+
+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
+from _pytest.assertion.rewrite import _get_maxsize_for_saferepr
+from _pytest.assertion.rewrite import AssertionRewritingHook
+from _pytest.assertion.rewrite import get_cache_dir
+from _pytest.assertion.rewrite import PYC_TAIL
+from _pytest.assertion.rewrite import PYTEST_TAG
+from _pytest.assertion.rewrite import rewrite_asserts
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.pathlib import make_numbered_dir
+from _pytest.pytester import Pytester
+
+
+def rewrite(src: str) -> ast.Module:
+ tree = ast.parse(src)
+ rewrite_asserts(tree, src.encode())
+ return tree
+
+
+def getmsg(
+ f, extra_ns: Optional[Mapping[str, object]] = None, *, must_pass: bool = False
+) -> Optional[str]:
+ """Rewrite the assertions in f, run it, and get the failure message."""
+ src = "\n".join(_pytest._code.Code.from_function(f).source().lines)
+ mod = rewrite(src)
+ code = compile(mod, "<test>", "exec")
+ ns: Dict[str, object] = {}
+ if extra_ns is not None:
+ ns.update(extra_ns)
+ exec(code, ns)
+ func = ns[f.__name__]
+ try:
+ func() # type: ignore[operator]
+ except AssertionError:
+ if must_pass:
+ pytest.fail("shouldn't have raised")
+ s = str(sys.exc_info()[1])
+ if not s.startswith("assert"):
+ return "AssertionError: " + s
+ return s
+ else:
+ if not must_pass:
+ pytest.fail("function didn't raise at all")
+ return None
+
+
+class TestAssertionRewrite:
+ def test_place_initial_imports(self) -> None:
+ s = """'Doc string'\nother = stuff"""
+ m = rewrite(s)
+ assert isinstance(m.body[0], ast.Expr)
+ for imp in m.body[1:3]:
+ assert isinstance(imp, ast.Import)
+ assert imp.lineno == 2
+ assert imp.col_offset == 0
+ assert isinstance(m.body[3], ast.Assign)
+ s = """from __future__ import division\nother_stuff"""
+ m = rewrite(s)
+ assert isinstance(m.body[0], ast.ImportFrom)
+ for imp in m.body[1:3]:
+ assert isinstance(imp, ast.Import)
+ assert imp.lineno == 2
+ assert imp.col_offset == 0
+ assert isinstance(m.body[3], ast.Expr)
+ s = """'doc string'\nfrom __future__ import division"""
+ m = rewrite(s)
+ assert isinstance(m.body[0], ast.Expr)
+ assert isinstance(m.body[1], ast.ImportFrom)
+ for imp in m.body[2:4]:
+ assert isinstance(imp, ast.Import)
+ assert imp.lineno == 2
+ assert imp.col_offset == 0
+ s = """'doc string'\nfrom __future__ import division\nother"""
+ m = rewrite(s)
+ assert isinstance(m.body[0], ast.Expr)
+ assert isinstance(m.body[1], ast.ImportFrom)
+ for imp in m.body[2:4]:
+ assert isinstance(imp, ast.Import)
+ assert imp.lineno == 3
+ assert imp.col_offset == 0
+ assert isinstance(m.body[4], ast.Expr)
+ s = """from . import relative\nother_stuff"""
+ m = rewrite(s)
+ for imp in m.body[:2]:
+ assert isinstance(imp, ast.Import)
+ assert imp.lineno == 1
+ assert imp.col_offset == 0
+ assert isinstance(m.body[3], ast.Expr)
+
+ def test_location_is_set(self) -> None:
+ s = textwrap.dedent(
+ """
+
+ assert False, (
+
+ "Ouch"
+ )
+
+ """
+ )
+ m = rewrite(s)
+ for node in m.body:
+ if isinstance(node, ast.Import):
+ continue
+ 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
+
+ def test_dont_rewrite(self) -> None:
+ s = """'PYTEST_DONT_REWRITE'\nassert 14"""
+ m = rewrite(s)
+ assert len(m.body) == 2
+ assert isinstance(m.body[1], ast.Assert)
+ assert m.body[1].msg is None
+
+ def test_dont_rewrite_plugin(self, pytester: Pytester) -> None:
+ contents = {
+ "conftest.py": "pytest_plugins = 'plugin'; import plugin",
+ "plugin.py": "'PYTEST_DONT_REWRITE'",
+ "test_foo.py": "def test_foo(): pass",
+ }
+ pytester.makepyfile(**contents)
+ result = pytester.runpytest_subprocess()
+ assert "warning" not in "".join(result.outlines)
+
+ def test_rewrites_plugin_as_a_package(self, pytester: Pytester) -> None:
+ pkgdir = pytester.mkpydir("plugin")
+ pkgdir.joinpath("__init__.py").write_text(
+ "import pytest\n"
+ "@pytest.fixture\n"
+ "def special_asserter():\n"
+ " def special_assert(x, y):\n"
+ " assert x == y\n"
+ " return special_assert\n"
+ )
+ pytester.makeconftest('pytest_plugins = ["plugin"]')
+ pytester.makepyfile("def test(special_asserter): special_asserter(1, 2)\n")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*assert 1 == 2*"])
+
+ def test_honors_pep_235(self, pytester: Pytester, monkeypatch) -> None:
+ # note: couldn't make it fail on macos with a single `sys.path` entry
+ # note: these modules are named `test_*` to trigger rewriting
+ 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")
+ pytester.makepyfile(
+ "import test_y\n"
+ "import test_Y\n"
+ "def test():\n"
+ " assert test_y.x == 1\n"
+ " assert test_Y.x == 2\n"
+ )
+ monkeypatch.syspath_prepend(str(xdir))
+ pytester.runpytest().assert_outcomes(passed=1)
+
+ def test_name(self, request) -> None:
+ def f1() -> None:
+ assert False
+
+ assert getmsg(f1) == "assert False"
+
+ def f2() -> None:
+ f = False
+ assert f
+
+ assert getmsg(f2) == "assert False"
+
+ def f3() -> None:
+ assert a_global # type: ignore[name-defined] # noqa
+
+ 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"
+
+ def f5() -> None:
+ assert cls == 42 # type: ignore[name-defined] # noqa: F821
+
+ class X:
+ pass
+
+ 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"]
+
+ def test_assertrepr_compare_same_width(self, request) -> None:
+ """Should use same width/truncation with same initial width."""
+
+ def f() -> None:
+ assert "1234567890" * 5 + "A" == "1234567890" * 5 + "B"
+
+ msg = getmsg(f)
+ assert msg is not None
+ line = msg.splitlines()[0]
+ if request.config.getoption("verbose") > 1:
+ assert line == (
+ "assert '12345678901234567890123456789012345678901234567890A' "
+ "== '12345678901234567890123456789012345678901234567890B'"
+ )
+ else:
+ assert line == (
+ "assert '123456789012...901234567890A' "
+ "== '123456789012...901234567890B'"
+ )
+
+ def test_dont_rewrite_if_hasattr_fails(self, request) -> None:
+ class Y:
+ """A class whose getattr fails, but not with `AttributeError`."""
+
+ def __getattr__(self, attribute_name):
+ raise KeyError()
+
+ def __repr__(self) -> str:
+ return "Y"
+
+ def __init__(self) -> None:
+ self.foo = 3
+
+ def f() -> None:
+ assert cls().foo == 2 # type: ignore[name-defined] # noqa: F821
+
+ # XXX: looks like the "where" should also be there in verbose mode?!
+ 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()",
+ ]
+
+ def test_assert_already_has_message(self) -> None:
+ def f():
+ assert False, "something bad!"
+
+ assert getmsg(f) == "AssertionError: something bad!\nassert False"
+
+ def test_assertion_message(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_foo():
+ assert 1 == 2, "The failure message"
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(
+ ["*AssertionError*The failure message*", "*assert 1 == 2*"]
+ )
+
+ def test_assertion_message_multiline(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_foo():
+ assert 1 == 2, "A multiline\\nfailure message"
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(
+ ["*AssertionError*A multiline*", "*failure message*", "*assert 1 == 2*"]
+ )
+
+ def test_assertion_message_tuple(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_foo():
+ assert 1 == 2, (1, 2)
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(
+ ["*AssertionError*%s*" % repr((1, 2)), "*assert 1 == 2*"]
+ )
+
+ def test_assertion_message_expr(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_foo():
+ assert 1 == 2, 1 + 2
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(["*AssertionError*3*", "*assert 1 == 2*"])
+
+ def test_assertion_message_escape(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_foo():
+ assert 1 == 2, 'To be escaped: %'
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(
+ ["*AssertionError: To be escaped: %", "*assert 1 == 2"]
+ )
+
+ def test_assertion_messages_bytes(self, pytester: Pytester) -> None:
+ pytester.makepyfile("def test_bytes_assertion():\n assert False, b'ohai!'\n")
+ result = pytester.runpytest()
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(["*AssertionError: b'ohai!'", "*assert False"])
+
+ def test_boolop(self) -> None:
+ def f1() -> None:
+ f = g = False
+ assert f and g
+
+ assert getmsg(f1) == "assert (False)"
+
+ def f2() -> None:
+ f = True
+ g = False
+ assert f and g
+
+ assert getmsg(f2) == "assert (True and False)"
+
+ def f3() -> None:
+ f = False
+ g = True
+ assert f and g
+
+ assert getmsg(f3) == "assert (False)"
+
+ def f4() -> None:
+ f = g = False
+ assert f or g
+
+ assert getmsg(f4) == "assert (False or False)"
+
+ def f5() -> None:
+ f = g = False
+ assert not f and not g
+
+ getmsg(f5, must_pass=True)
+
+ def x() -> bool:
+ return False
+
+ def f6() -> None:
+ assert x() and x()
+
+ assert (
+ getmsg(f6, {"x": x})
+ == """assert (False)
+ + where False = x()"""
+ )
+
+ def f7() -> None:
+ assert False or x()
+
+ assert (
+ getmsg(f7, {"x": x})
+ == """assert (False or False)
+ + where False = x()"""
+ )
+
+ def f8() -> None:
+ assert 1 in {} and 2 in {}
+
+ assert getmsg(f8) == "assert (1 in {})"
+
+ def f9() -> None:
+ x = 1
+ y = 2
+ assert x in {1: None} and y in {}
+
+ assert getmsg(f9) == "assert (1 in {1: None} and 2 in {})"
+
+ def f10() -> None:
+ f = True
+ g = False
+ assert f or g
+
+ getmsg(f10, must_pass=True)
+
+ def f11() -> None:
+ f = g = h = lambda: True
+ assert f() and g() and h()
+
+ getmsg(f11, must_pass=True)
+
+ def test_short_circuit_evaluation(self) -> None:
+ def f1() -> None:
+ assert True or explode # type: ignore[name-defined,unreachable] # noqa: F821
+
+ getmsg(f1, must_pass=True)
+
+ def f2() -> None:
+ x = 1
+ assert x == 1 or x == 2
+
+ getmsg(f2, must_pass=True)
+
+ def test_unary_op(self) -> None:
+ def f1() -> None:
+ x = True
+ assert not x
+
+ assert getmsg(f1) == "assert not True"
+
+ def f2() -> None:
+ x = 0
+ assert ~x + 1
+
+ assert getmsg(f2) == "assert (~0 + 1)"
+
+ def f3() -> None:
+ x = 3
+ assert -x + x
+
+ assert getmsg(f3) == "assert (-3 + 3)"
+
+ def f4() -> None:
+ x = 0
+ assert +x + x
+
+ assert getmsg(f4) == "assert (+0 + 0)"
+
+ def test_binary_op(self) -> None:
+ def f1() -> None:
+ x = 1
+ y = -1
+ assert x + y
+
+ assert getmsg(f1) == "assert (1 + -1)"
+
+ def f2() -> None:
+ assert not 5 % 4
+
+ assert getmsg(f2) == "assert not (5 % 4)"
+
+ def test_boolop_percent(self) -> None:
+ def f1() -> None:
+ assert 3 % 2 and False
+
+ assert getmsg(f1) == "assert ((3 % 2) and False)"
+
+ def f2() -> None:
+ assert False or 4 % 2
+
+ assert getmsg(f2) == "assert (False or (4 % 2))"
+
+ def test_at_operator_issue1290(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ class Matrix(object):
+ def __init__(self, num):
+ self.num = num
+ def __matmul__(self, other):
+ return self.num * other.num
+
+ def test_multmat_operator():
+ assert Matrix(2) @ Matrix(3) == 6"""
+ )
+ pytester.runpytest().assert_outcomes(passed=1)
+
+ def test_starred_with_side_effect(self, pytester: Pytester) -> None:
+ """See #4412"""
+ pytester.makepyfile(
+ """\
+ def test():
+ f = lambda x: x
+ x = iter([1, 2, 3])
+ assert 2 * next(x) == f(*[next(x)])
+ """
+ )
+ pytester.runpytest().assert_outcomes(passed=1)
+
+ def test_call(self) -> None:
+ def g(a=42, *args, **kwargs) -> bool:
+ return False
+
+ ns = {"g": g}
+
+ def f1() -> None:
+ assert g()
+
+ assert (
+ getmsg(f1, ns)
+ == """assert False
+ + where False = g()"""
+ )
+
+ def f2() -> None:
+ assert g(1)
+
+ assert (
+ getmsg(f2, ns)
+ == """assert False
+ + where False = g(1)"""
+ )
+
+ def f3() -> None:
+ assert g(1, 2)
+
+ assert (
+ getmsg(f3, ns)
+ == """assert False
+ + where False = g(1, 2)"""
+ )
+
+ def f4() -> None:
+ assert g(1, g=42)
+
+ assert (
+ getmsg(f4, ns)
+ == """assert False
+ + where False = g(1, g=42)"""
+ )
+
+ def f5() -> None:
+ assert g(1, 3, g=23)
+
+ assert (
+ getmsg(f5, ns)
+ == """assert False
+ + where False = g(1, 3, g=23)"""
+ )
+
+ def f6() -> None:
+ seq = [1, 2, 3]
+ assert g(*seq)
+
+ assert (
+ getmsg(f6, ns)
+ == """assert False
+ + where False = g(*[1, 2, 3])"""
+ )
+
+ def f7() -> None:
+ x = "a"
+ assert g(**{x: 2})
+
+ assert (
+ getmsg(f7, ns)
+ == """assert False
+ + where False = g(**{'a': 2})"""
+ )
+
+ def test_attribute(self) -> None:
+ class X:
+ g = 3
+
+ ns = {"x": X}
+
+ def f1() -> None:
+ assert not x.g # type: ignore[name-defined] # noqa: F821
+
+ assert (
+ getmsg(f1, ns)
+ == """assert not 3
+ + where 3 = x.g"""
+ )
+
+ def f2() -> None:
+ x.a = False # type: ignore[name-defined] # noqa: F821
+ assert x.a # type: ignore[name-defined] # noqa: F821
+
+ assert (
+ getmsg(f2, ns)
+ == """assert False
+ + where False = x.a"""
+ )
+
+ def test_comparisons(self) -> None:
+ def f1() -> None:
+ a, b = range(2)
+ assert b < a
+
+ assert getmsg(f1) == """assert 1 < 0"""
+
+ def f2() -> None:
+ a, b, c = range(3)
+ assert a > b > c
+
+ assert getmsg(f2) == """assert 0 > 1"""
+
+ def f3() -> None:
+ a, b, c = range(3)
+ assert a < b > c
+
+ assert getmsg(f3) == """assert 1 > 2"""
+
+ def f4() -> None:
+ a, b, c = range(3)
+ assert a < b <= c
+
+ getmsg(f4, must_pass=True)
+
+ def f5() -> None:
+ a, b, c = range(3)
+ assert a < b
+ assert b < c
+
+ getmsg(f5, must_pass=True)
+
+ def test_len(self, request) -> None:
+ def f():
+ values = list(range(10))
+ 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, ...])"
+
+ def test_custom_reprcompare(self, monkeypatch) -> None:
+ def my_reprcompare1(op, left, right) -> str:
+ return "42"
+
+ monkeypatch.setattr(util, "_reprcompare", my_reprcompare1)
+
+ def f1() -> None:
+ assert 42 < 3
+
+ assert getmsg(f1) == "assert 42"
+
+ def my_reprcompare2(op, left, right) -> str:
+ return f"{left} {op} {right}"
+
+ monkeypatch.setattr(util, "_reprcompare", my_reprcompare2)
+
+ def f2() -> None:
+ assert 1 < 3 < 5 <= 4 < 7
+
+ assert getmsg(f2) == "assert 5 <= 4"
+
+ def test_assert_raising__bool__in_comparison(self) -> None:
+ def f() -> None:
+ class A:
+ def __bool__(self):
+ raise ValueError(42)
+
+ def __lt__(self, other):
+ return A()
+
+ def __repr__(self):
+ return "<MY42 object>"
+
+ def myany(x) -> bool:
+ return False
+
+ assert myany(A() < 0)
+
+ msg = getmsg(f)
+ assert msg is not None
+ assert "<MY42 object> < 0" in msg
+
+ def test_formatchar(self) -> None:
+ def f() -> None:
+ assert "%test" == "test" # type: ignore[comparison-overlap]
+
+ msg = getmsg(f)
+ assert msg is not None
+ assert msg.startswith("assert '%test' == 'test'")
+
+ def test_custom_repr(self, request) -> None:
+ def f() -> None:
+ class Foo:
+ a = 1
+
+ def __repr__(self):
+ return "\n{ \n~ \n}"
+
+ f = Foo()
+ assert 0 == f.a
+
+ 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"]
+
+ def test_custom_repr_non_ascii(self) -> None:
+ def f() -> None:
+ class A:
+ name = "ä"
+
+ def __repr__(self):
+ return self.name.encode("UTF-8") # only legal in python2
+
+ a = A()
+ assert not a.name
+
+ msg = getmsg(f)
+ assert msg is not None
+ assert "UnicodeDecodeError" not in msg
+ assert "UnicodeEncodeError" not in msg
+
+
+class TestRewriteOnImport:
+ def test_pycache_is_a_file(self, pytester: Pytester) -> None:
+ pytester.path.joinpath("__pycache__").write_text("Hello")
+ pytester.makepyfile(
+ """
+ def test_rewritten():
+ assert "@py_builtins" in globals()"""
+ )
+ assert pytester.runpytest().ret == 0
+
+ def test_pycache_is_readonly(self, pytester: Pytester) -> None:
+ cache = pytester.mkdir("__pycache__")
+ old_mode = cache.stat().st_mode
+ cache.chmod(old_mode ^ stat.S_IWRITE)
+ pytester.makepyfile(
+ """
+ def test_rewritten():
+ assert "@py_builtins" in globals()"""
+ )
+ try:
+ assert pytester.runpytest().ret == 0
+ finally:
+ cache.chmod(old_mode)
+
+ def test_zipfile(self, pytester: Pytester) -> None:
+ z = pytester.path.joinpath("myzip.zip")
+ z_fn = str(z)
+ f = zipfile.ZipFile(z_fn, "w")
+ try:
+ f.writestr("test_gum/__init__.py", "")
+ f.writestr("test_gum/test_lizard.py", "")
+ finally:
+ f.close()
+ z.chmod(256)
+ pytester.makepyfile(
+ """
+ import sys
+ sys.path.append(%r)
+ import test_gum.test_lizard"""
+ % (z_fn,)
+ )
+ assert pytester.runpytest().ret == ExitCode.NO_TESTS_COLLECTED
+
+ @pytest.mark.skipif(
+ sys.version_info < (3, 9),
+ reason="importlib.resources.files was introduced in 3.9",
+ )
+ def test_load_resource_via_files_with_rewrite(self, pytester: Pytester) -> None:
+ example = pytester.path.joinpath("demo") / "example"
+ init = pytester.path.joinpath("demo") / "__init__.py"
+ pytester.makepyfile(
+ **{
+ "demo/__init__.py": """
+ from importlib.resources import files
+
+ def load():
+ return files(__name__)
+ """,
+ "test_load": f"""
+ pytest_plugins = ["demo"]
+
+ def test_load():
+ from demo import load
+ found = {{str(i) for i in load().iterdir() if i.name != "__pycache__"}}
+ assert found == {{{str(example)!r}, {str(init)!r}}}
+ """,
+ }
+ )
+ example.mkdir()
+
+ assert pytester.runpytest("-vv").ret == ExitCode.OK
+
+ def test_readonly(self, pytester: Pytester) -> None:
+ sub = pytester.mkdir("testing")
+ sub.joinpath("test_readonly.py").write_bytes(
+ b"""
+def test_rewritten():
+ assert "@py_builtins" in globals()
+ """,
+ )
+ old_mode = sub.stat().st_mode
+ sub.chmod(320)
+ try:
+ assert pytester.runpytest().ret == 0
+ finally:
+ sub.chmod(old_mode)
+
+ def test_dont_write_bytecode(self, pytester: Pytester, monkeypatch) -> None:
+ monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False)
+
+ pytester.makepyfile(
+ """
+ import os
+ def test_no_bytecode():
+ assert "__pycache__" in __cached__
+ assert not os.path.exists(__cached__)
+ assert not os.path.exists(os.path.dirname(__cached__))"""
+ )
+ monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1")
+ assert pytester.runpytest_subprocess().ret == 0
+
+ def test_orphaned_pyc_file(self, pytester: Pytester, monkeypatch) -> None:
+ monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False)
+ monkeypatch.setattr(sys, "pycache_prefix", None, raising=False)
+
+ pytester.makepyfile(
+ """
+ import orphan
+ def test_it():
+ assert orphan.value == 17
+ """
+ )
+ pytester.makepyfile(
+ orphan="""
+ value = 17
+ """
+ )
+ py_compile.compile("orphan.py")
+ os.remove("orphan.py")
+
+ # Python 3 puts the .pyc files in a __pycache__ directory, and will
+ # not import from there without source. It will import a .pyc from
+ # the source location though.
+ if not os.path.exists("orphan.pyc"):
+ pycs = glob.glob("__pycache__/orphan.*.pyc")
+ assert len(pycs) == 1
+ os.rename(pycs[0], "orphan.pyc")
+
+ assert pytester.runpytest().ret == 0
+
+ def test_cached_pyc_includes_pytest_version(
+ self, pytester: Pytester, monkeypatch
+ ) -> None:
+ """Avoid stale caches (#1671)"""
+ monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
+ monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False)
+ pytester.makepyfile(
+ test_foo="""
+ def test_foo():
+ assert True
+ """
+ )
+ result = pytester.runpytest_subprocess()
+ assert result.ret == 0
+ found_names = glob.glob(f"__pycache__/*-pytest-{pytest.__version__}.pyc")
+ assert found_names, "pyc with expected tag not found in names: {}".format(
+ glob.glob("__pycache__/*.pyc")
+ )
+
+ @pytest.mark.skipif('"__pypy__" in sys.modules')
+ def test_pyc_vs_pyo(self, pytester: Pytester, monkeypatch) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_optimized():
+ "hello"
+ assert test_optimized.__doc__ is None"""
+ )
+ 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()
+ monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
+ monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False)
+ assert pytester.runpytest_subprocess(tmp).ret == 1
+ assert tagged + ".pyc" in os.listdir("__pycache__")
+
+ def test_package(self, pytester: Pytester) -> None:
+ pkg = pytester.path.joinpath("pkg")
+ pkg.mkdir()
+ pkg.joinpath("__init__.py")
+ pkg.joinpath("test_blah.py").write_text(
+ """
+def test_rewritten():
+ assert "@py_builtins" in globals()"""
+ )
+ assert pytester.runpytest().ret == 0
+
+ def test_translate_newlines(self, pytester: Pytester) -> None:
+ content = "def test_rewritten():\r\n assert '@py_builtins' in globals()"
+ b = content.encode("utf-8")
+ pytester.path.joinpath("test_newlines.py").write_bytes(b)
+ assert pytester.runpytest().ret == 0
+
+ def test_package_without__init__py(self, pytester: Pytester) -> None:
+ pkg = pytester.mkdir("a_package_without_init_py")
+ pkg.joinpath("module.py").touch()
+ pytester.makepyfile("import a_package_without_init_py.module")
+ assert pytester.runpytest().ret == ExitCode.NO_TESTS_COLLECTED
+
+ def test_rewrite_warning(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ pytest.register_assert_rewrite("_pytest")
+ """
+ )
+ # needs to be a subprocess because pytester explicitly disables this warning
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(["*Module already imported*: _pytest"])
+
+ def test_rewrite_module_imported_from_conftest(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import test_rewrite_module_imported
+ """
+ )
+ pytester.makepyfile(
+ test_rewrite_module_imported="""
+ def test_rewritten():
+ assert "@py_builtins" in globals()
+ """
+ )
+ assert pytester.runpytest_subprocess().ret == 0
+
+ def test_remember_rewritten_modules(
+ self, pytestconfig, pytester: Pytester, monkeypatch
+ ) -> None:
+ """`AssertionRewriteHook` should remember rewritten modules so it
+ doesn't give false positives (#2005)."""
+ monkeypatch.syspath_prepend(pytester.path)
+ pytester.makepyfile(test_remember_rewritten_modules="")
+ warnings = []
+ hook = AssertionRewritingHook(pytestconfig)
+ monkeypatch.setattr(
+ hook, "_warn_already_imported", lambda code, msg: warnings.append(msg)
+ )
+ spec = hook.find_spec("test_remember_rewritten_modules")
+ assert spec is not None
+ module = importlib.util.module_from_spec(spec)
+ hook.exec_module(module)
+ hook.mark_rewrite("test_remember_rewritten_modules")
+ hook.mark_rewrite("test_remember_rewritten_modules")
+ assert warnings == []
+
+ def test_rewrite_warning_using_pytest_plugins(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ **{
+ "conftest.py": "pytest_plugins = ['core', 'gui', 'sci']",
+ "core.py": "",
+ "gui.py": "pytest_plugins = ['core', 'sci']",
+ "sci.py": "pytest_plugins = ['core']",
+ "test_rewrite_warning_pytest_plugins.py": "def test(): pass",
+ }
+ )
+ pytester.chdir()
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(["*= 1 passed in *=*"])
+ result.stdout.no_fnmatch_line("*pytest-warning summary*")
+
+ def test_rewrite_warning_using_pytest_plugins_env_var(
+ self, pytester: Pytester, monkeypatch
+ ) -> None:
+ monkeypatch.setenv("PYTEST_PLUGINS", "plugin")
+ pytester.makepyfile(
+ **{
+ "plugin.py": "",
+ "test_rewrite_warning_using_pytest_plugins_env_var.py": """
+ import plugin
+ pytest_plugins = ['plugin']
+ def test():
+ pass
+ """,
+ }
+ )
+ pytester.chdir()
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(["*= 1 passed in *=*"])
+ result.stdout.no_fnmatch_line("*pytest-warning summary*")
+
+
+class TestAssertionRewriteHookDetails:
+ def test_sys_meta_path_munged(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_meta_path():
+ import sys; sys.meta_path = []"""
+ )
+ assert pytester.runpytest().ret == 0
+
+ def test_write_pyc(self, pytester: Pytester, tmp_path, monkeypatch) -> None:
+ from _pytest.assertion.rewrite import _write_pyc
+ from _pytest.assertion import AssertionState
+
+ config = pytester.parseconfig()
+ state = AssertionState(config, "rewrite")
+ tmp_path.joinpath("source.py").touch()
+ source_path = str(tmp_path)
+ pycpath = tmp_path.joinpath("pyc")
+ 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)
+
+ def test_resources_provider_for_loader(self, pytester: Pytester) -> None:
+ """
+ Attempts to load resources from a package should succeed normally,
+ even when the AssertionRewriteHook is used to load the modules.
+
+ See #366 for details.
+ """
+ pytest.importorskip("pkg_resources")
+
+ pytester.mkpydir("testpkg")
+ contents = {
+ "testpkg/test_pkg": """
+ import pkg_resources
+
+ import pytest
+ from _pytest.assertion.rewrite import AssertionRewritingHook
+
+ def test_load_resource():
+ assert isinstance(__loader__, AssertionRewritingHook)
+ res = pkg_resources.resource_string(__name__, 'resource.txt')
+ res = res.decode('ascii')
+ assert res == 'Load me please.'
+ """
+ }
+ pytester.makepyfile(**contents)
+ pytester.maketxtfile(**{"testpkg/resource": "Load me please."})
+
+ result = pytester.runpytest_subprocess()
+ result.assert_outcomes(passed=1)
+
+ def test_read_pyc(self, tmp_path: Path) -> None:
+ """
+ Ensure that the `_read_pyc` can properly deal with corrupted pyc files.
+ In those circumstances it should just give up instead of generating
+ 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")
+ py_compile.compile(str(source), str(pyc))
+
+ contents = pyc.read_bytes()
+ strip_bytes = 20 # header is around 16 bytes, strip a little more
+ assert len(contents) > strip_bytes
+ pyc.write_bytes(contents[:strip_bytes])
+
+ assert _read_pyc(source, pyc) is None # no error
+
+ def test_read_pyc_success(self, tmp_path: Path, pytester: Pytester) -> None:
+ """
+ Ensure that the _rewrite_test() -> _write_pyc() produces a pyc file
+ that can be properly read with _read_pyc()
+ """
+ from _pytest.assertion import AssertionState
+ from _pytest.assertion.rewrite import _read_pyc
+ from _pytest.assertion.rewrite import _rewrite_test
+ from _pytest.assertion.rewrite import _write_pyc
+
+ config = pytester.parseconfig()
+ state = AssertionState(config, "rewrite")
+
+ fn = tmp_path / "source.py"
+ pyc = Path(str(fn) + "c")
+
+ fn.write_text("def test(): assert True")
+
+ 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
+
+ source = tmp_path / "source.py"
+ pyc = tmp_path / "source.pyc"
+
+ source_bytes = b"def test(): pass\n"
+ source.write_bytes(source_bytes)
+
+ magic = importlib.util.MAGIC_NUMBER
+
+ flags = b"\x00\x00\x00\x00"
+
+ mtime = b"\x58\x3c\xb0\x5f"
+ mtime_int = int.from_bytes(mtime, "little")
+ os.utime(source, (mtime_int, mtime_int))
+
+ size = len(source_bytes).to_bytes(4, "little")
+
+ code = marshal.dumps(compile(source_bytes, str(source), "exec"))
+
+ # Good header.
+ pyc.write_bytes(magic + flags + mtime + size + code)
+ assert _read_pyc(source, pyc, print) is not None
+
+ # Too short.
+ pyc.write_bytes(magic + flags + mtime)
+ assert _read_pyc(source, pyc, print) is None
+
+ # Bad magic.
+ pyc.write_bytes(b"\x12\x34\x56\x78" + flags + mtime + size + code)
+ assert _read_pyc(source, pyc, print) is None
+
+ # Unsupported flags.
+ pyc.write_bytes(magic + b"\x00\xff\x00\x00" + mtime + size + code)
+ assert _read_pyc(source, pyc, print) is None
+
+ # Bad mtime.
+ pyc.write_bytes(magic + flags + b"\x58\x3d\xb0\x5f" + size + code)
+ assert _read_pyc(source, pyc, print) is None
+
+ # Bad size.
+ pyc.write_bytes(magic + flags + mtime + b"\x99\x00\x00\x00" + code)
+ assert _read_pyc(source, pyc, print) is None
+
+ def test_reload_is_same_and_reloads(self, pytester: Pytester) -> None:
+ """Reloading a (collected) module after change picks up the change."""
+ pytester.makeini(
+ """
+ [pytest]
+ python_files = *.py
+ """
+ )
+ pytester.makepyfile(
+ file="""
+ def reloaded():
+ return False
+
+ def rewrite_self():
+ with open(__file__, 'w') as self:
+ self.write('def reloaded(): return True')
+ """,
+ test_fun="""
+ import sys
+ from importlib import reload
+
+ def test_loader():
+ import file
+ assert not file.reloaded()
+ file.rewrite_self()
+ assert sys.modules["file"] is reload(file)
+ assert file.reloaded()
+ """,
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 1 passed*"])
+
+ def test_get_data_support(self, pytester: Pytester) -> None:
+ """Implement optional PEP302 api (#808)."""
+ path = pytester.mkpydir("foo")
+ path.joinpath("test_foo.py").write_text(
+ textwrap.dedent(
+ """\
+ class Test(object):
+ def test_foo(self):
+ import pkgutil
+ data = pkgutil.get_data('foo.test_foo', 'data.txt')
+ assert data == b'Hey'
+ """
+ )
+ )
+ path.joinpath("data.txt").write_text("Hey")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+def test_issue731(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ class LongReprWithBraces(object):
+ def __repr__(self):
+ return 'LongReprWithBraces({' + ('a' * 80) + '}' + ('a' * 120) + ')'
+
+ def some_method(self):
+ return False
+
+ def test_long_repr():
+ obj = LongReprWithBraces()
+ assert obj.some_method()
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.no_fnmatch_line("*unbalanced braces*")
+
+
+class TestIssue925:
+ def test_simple_case(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_ternary_display():
+ assert (False == False) == False
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*E*assert (False == False) == False"])
+
+ def test_long_case(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_ternary_display():
+ assert False == (False == True) == True
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*E*assert (False == True) == True"])
+
+ def test_many_brackets(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_ternary_display():
+ assert True == ((False == True) == True)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*E*assert True == ((False == True) == True)"])
+
+
+class TestIssue2121:
+ def test_rewrite_python_files_contain_subdirs(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ **{
+ "tests/file.py": """
+ def test_simple_failure():
+ assert 1 + 1 == 3
+ """
+ }
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ python_files = tests/**.py
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*E*assert (1 + 1) == 3"])
+
+
+@pytest.mark.skipif(
+ 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:
+ """Support modification dates after 2038 in rewritten files (#4903).
+
+ pytest would crash with:
+
+ fp.write(struct.pack("<ll", mtime, size))
+ E struct.error: argument out of range
+ """
+ p = pytester.makepyfile(
+ """
+ def test(): pass
+ """
+ )
+ # 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
+ os.utime(str(p), (timestamp, timestamp))
+ result = pytester.runpytest()
+ assert result.ret == 0
+
+
+def test_rewrite_infinite_recursion(
+ pytester: Pytester, pytestconfig, monkeypatch
+) -> None:
+ """Fix infinite recursion when writing pyc files: if an import happens to be triggered when writing the pyc
+ file, this would cause another call to the hook, which would trigger another pyc writing, which could
+ trigger another import, and so on. (#3506)"""
+ from _pytest.assertion import rewrite as rewritemod
+
+ pytester.syspathinsert()
+ pytester.makepyfile(test_foo="def test_foo(): pass")
+ pytester.makepyfile(test_bar="def test_bar(): pass")
+
+ original_write_pyc = rewritemod._write_pyc
+
+ write_pyc_called = []
+
+ def spy_write_pyc(*args, **kwargs):
+ # make a note that we have called _write_pyc
+ write_pyc_called.append(True)
+ # try to import a module at this point: we should not try to rewrite this module
+ assert hook.find_spec("test_bar") is None
+ return original_write_pyc(*args, **kwargs)
+
+ monkeypatch.setattr(rewritemod, "_write_pyc", spy_write_pyc)
+ monkeypatch.setattr(sys, "dont_write_bytecode", False)
+
+ hook = AssertionRewritingHook(pytestconfig)
+ spec = hook.find_spec("test_foo")
+ assert spec is not None
+ module = importlib.util.module_from_spec(spec)
+ hook.exec_module(module)
+ assert len(write_pyc_called) == 1
+
+
+class TestEarlyRewriteBailout:
+ @pytest.fixture
+ def hook(
+ self, pytestconfig, monkeypatch, pytester: Pytester
+ ) -> AssertionRewritingHook:
+ """Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track
+ if PathFinder.find_spec has been called.
+ """
+ import importlib.machinery
+
+ self.find_spec_calls: List[str] = []
+ self.initial_paths: Set[Path] = set()
+
+ class StubSession:
+ _initialpaths = self.initial_paths
+
+ def isinitpath(self, p):
+ return p in self._initialpaths
+
+ def spy_find_spec(name, path):
+ self.find_spec_calls.append(name)
+ return importlib.machinery.PathFinder.find_spec(name, path)
+
+ 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
+
+ def test_basic(self, pytester: Pytester, hook: AssertionRewritingHook) -> None:
+ """
+ Ensure we avoid calling PathFinder.find_spec when we know for sure a certain
+ module will not be rewritten to optimize assertion rewriting (#3918).
+ """
+ pytester.makeconftest(
+ """
+ import pytest
+ @pytest.fixture
+ def fix(): return 1
+ """
+ )
+ pytester.makepyfile(test_foo="def test_foo(): pass")
+ pytester.makepyfile(bar="def bar(): pass")
+ foobar_path = pytester.makepyfile(foobar="def foobar(): pass")
+ self.initial_paths.add(foobar_path)
+
+ # conftest files should always be rewritten
+ assert hook.find_spec("conftest") is not None
+ assert self.find_spec_calls == ["conftest"]
+
+ # files matching "python_files" mask should always be rewritten
+ assert hook.find_spec("test_foo") is not None
+ assert self.find_spec_calls == ["conftest", "test_foo"]
+
+ # file does not match "python_files": early bailout
+ assert hook.find_spec("bar") is None
+ assert self.find_spec_calls == ["conftest", "test_foo"]
+
+ # file is an initial path (passed on the command-line): should be rewritten
+ assert hook.find_spec("foobar") is not None
+ assert self.find_spec_calls == ["conftest", "test_foo", "foobar"]
+
+ def test_pattern_contains_subdirectories(
+ self, pytester: Pytester, hook: AssertionRewritingHook
+ ) -> None:
+ """If one of the python_files patterns contain subdirectories ("tests/**.py") we can't bailout early
+ because we need to match with the full path, which can only be found by calling PathFinder.find_spec
+ """
+ pytester.makepyfile(
+ **{
+ "tests/file.py": """\
+ def test_simple_failure():
+ assert 1 + 1 == 3
+ """
+ }
+ )
+ pytester.syspathinsert("tests")
+ 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"
+ )
+ @pytest.mark.skipif(
+ sys.platform.startswith("sunos5"), reason="cannot remove cwd on Solaris"
+ )
+ def test_cwd_changed(self, pytester: Pytester, monkeypatch) -> None:
+ # Setup conditions for py's fspath trying to import pathlib on py34
+ # always (previously triggered via xdist only).
+ # Ref: https://github.com/pytest-dev/py/pull/207
+ monkeypatch.syspath_prepend("")
+ monkeypatch.delitem(sys.modules, "pathlib", raising=False)
+
+ pytester.makepyfile(
+ **{
+ "test_setup_nonexisting_cwd.py": """\
+ import os
+ import tempfile
+
+ with tempfile.TemporaryDirectory() as d:
+ os.chdir(d)
+ """,
+ "test_test.py": """\
+ def test():
+ pass
+ """,
+ }
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["* 1 passed in *"])
+
+
+class TestAssertionPass:
+ def test_option_default(self, pytester: Pytester) -> None:
+ config = pytester.parseconfig()
+ assert config.getini("enable_assertion_pass_hook") is False
+
+ @pytest.fixture
+ def flag_on(self, pytester: Pytester):
+ pytester.makeini("[pytest]\nenable_assertion_pass_hook = True\n")
+
+ @pytest.fixture
+ def hook_on(self, pytester: Pytester):
+ pytester.makeconftest(
+ """\
+ def pytest_assertion_pass(item, lineno, orig, expl):
+ raise Exception("Assertion Passed: {} {} at line {}".format(orig, expl, lineno))
+ """
+ )
+
+ def test_hook_call(self, pytester: Pytester, flag_on, hook_on) -> None:
+ pytester.makepyfile(
+ """\
+ def test_simple():
+ a=1
+ b=2
+ c=3
+ d=0
+
+ assert a+b == c+d
+
+ # cover failing assertions with a message
+ def test_fails():
+ assert False, "assert with message"
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ "*Assertion Passed: a+b == c+d (1 + 2) == (3 + 0) at line 7*"
+ )
+
+ def test_hook_call_with_parens(self, pytester: Pytester, flag_on, hook_on) -> None:
+ pytester.makepyfile(
+ """\
+ def f(): return 1
+ def test():
+ assert f()
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines("*Assertion Passed: f() 1")
+
+ def test_hook_not_called_without_hookimpl(
+ self, pytester: Pytester, monkeypatch, flag_on
+ ) -> None:
+ """Assertion pass should not be called (and hence formatting should
+ not occur) if there is no hook declared for pytest_assertion_pass"""
+
+ def raise_on_assertionpass(*_, **__):
+ raise Exception("Assertion passed called when it shouldn't!")
+
+ monkeypatch.setattr(
+ _pytest.assertion.rewrite, "_call_assertion_pass", raise_on_assertionpass
+ )
+
+ pytester.makepyfile(
+ """\
+ def test_simple():
+ a=1
+ b=2
+ c=3
+ d=0
+
+ assert a+b == c+d
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=1)
+
+ def test_hook_not_called_without_cmd_option(
+ self, pytester: Pytester, monkeypatch
+ ) -> None:
+ """Assertion pass should not be called (and hence formatting should
+ not occur) if there is no hook declared for pytest_assertion_pass"""
+
+ def raise_on_assertionpass(*_, **__):
+ raise Exception("Assertion passed called when it shouldn't!")
+
+ monkeypatch.setattr(
+ _pytest.assertion.rewrite, "_call_assertion_pass", raise_on_assertionpass
+ )
+
+ pytester.makeconftest(
+ """\
+ def pytest_assertion_pass(item, lineno, orig, expl):
+ raise Exception("Assertion Passed: {} {} at line {}".format(orig, expl, lineno))
+ """
+ )
+
+ pytester.makepyfile(
+ """\
+ def test_simple():
+ a=1
+ b=2
+ c=3
+ d=0
+
+ assert a+b == c+d
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=1)
+
+
+@pytest.mark.parametrize(
+ ("src", "expected"),
+ (
+ # fmt: off
+ pytest.param(b"", {}, id="trivial"),
+ pytest.param(
+ b"def x(): assert 1\n",
+ {1: "1"},
+ id="assert statement not on own line",
+ ),
+ pytest.param(
+ b"def x():\n"
+ b" assert 1\n"
+ b" assert 1+2\n",
+ {2: "1", 3: "1+2"},
+ id="multiple assertions",
+ ),
+ pytest.param(
+ # changes in encoding cause the byte offsets to be different
+ "# -*- coding: latin1\n"
+ "def ÀÀÀÀÀ(): assert 1\n".encode("latin1"),
+ {2: "1"},
+ id="latin1 encoded on first line\n",
+ ),
+ pytest.param(
+ # using the default utf-8 encoding
+ "def ÀÀÀÀÀ(): assert 1\n".encode(),
+ {1: "1"},
+ id="utf-8 encoded on first line",
+ ),
+ pytest.param(
+ b"def x():\n"
+ b" assert (\n"
+ b" 1 + 2 # comment\n"
+ b" )\n",
+ {2: "(\n 1 + 2 # comment\n )"},
+ id="multi-line assertion",
+ ),
+ pytest.param(
+ b"def x():\n"
+ b" assert y == [\n"
+ b" 1, 2, 3\n"
+ b" ]\n",
+ {2: "y == [\n 1, 2, 3\n ]"},
+ id="multi line assert with list continuation",
+ ),
+ pytest.param(
+ b"def x():\n"
+ b" assert 1 + \\\n"
+ b" 2\n",
+ {2: "1 + \\\n 2"},
+ id="backslash continuation",
+ ),
+ pytest.param(
+ b"def x():\n"
+ b" assert x, y\n",
+ {2: "x"},
+ id="assertion with message",
+ ),
+ pytest.param(
+ b"def x():\n"
+ b" assert (\n"
+ b" f(1, 2, 3)\n"
+ b" ), 'f did not work!'\n",
+ {2: "(\n f(1, 2, 3)\n )"},
+ id="assertion with message, test spanning multiple lines",
+ ),
+ pytest.param(
+ b"def x():\n"
+ b" assert \\\n"
+ b" x\\\n"
+ b" , 'failure message'\n",
+ {2: "x"},
+ id="escaped newlines plus message",
+ ),
+ pytest.param(
+ b"def x(): assert 5",
+ {1: "5"},
+ id="no newline at end of file",
+ ),
+ # fmt: on
+ ),
+)
+def test_get_assertion_exprs(src, expected) -> None:
+ assert _get_assertion_exprs(src) == expected
+
+
+def test_try_makedirs(monkeypatch, tmp_path: Path) -> None:
+ from _pytest.assertion.rewrite import try_makedirs
+
+ p = tmp_path / "foo"
+
+ # create
+ assert try_makedirs(p)
+ assert p.is_dir()
+
+ # already exist
+ assert try_makedirs(p)
+
+ # monkeypatch to simulate all error situations
+ def fake_mkdir(p, exist_ok=False, *, exc):
+ assert isinstance(p, Path)
+ raise exc
+
+ monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=FileNotFoundError()))
+ assert not try_makedirs(p)
+
+ monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=NotADirectoryError()))
+ assert not try_makedirs(p)
+
+ monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=PermissionError()))
+ assert not try_makedirs(p)
+
+ err = OSError()
+ err.errno = errno.EROFS
+ monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err))
+ assert not try_makedirs(p)
+
+ # unhandled OSError should raise
+ err = OSError()
+ err.errno = errno.ECHILD
+ monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err))
+ with pytest.raises(OSError) as exc_info:
+ try_makedirs(p)
+ assert exc_info.value.errno == errno.ECHILD
+
+
+class TestPyCacheDir:
+ @pytest.mark.parametrize(
+ "prefix, source, expected",
+ [
+ ("c:/tmp/pycs", "d:/projects/src/foo.py", "c:/tmp/pycs/projects/src"),
+ (None, "d:/projects/src/foo.py", "d:/projects/src/__pycache__"),
+ ("/tmp/pycs", "/home/projects/src/foo.py", "/tmp/pycs/home/projects/src"),
+ (None, "/home/projects/src/foo.py", "/home/projects/src/__pycache__"),
+ ],
+ )
+ 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",
+ )
+ def test_sys_pycache_prefix_integration(
+ self, tmp_path, monkeypatch, pytester: Pytester
+ ) -> None:
+ """Integration test for sys.pycache_prefix (#4730)."""
+ pycache_prefix = tmp_path / "my/pycs"
+ monkeypatch.setattr(sys, "pycache_prefix", str(pycache_prefix))
+ monkeypatch.setattr(sys, "dont_write_bytecode", False)
+
+ pytester.makepyfile(
+ **{
+ "src/test_foo.py": """
+ import bar
+ def test_foo():
+ pass
+ """,
+ "src/bar/__init__.py": "",
+ }
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+
+ test_foo = pytester.path.joinpath("src/test_foo.py")
+ bar_init = pytester.path.joinpath("src/bar/__init__.py")
+ assert test_foo.is_file()
+ assert bar_init.is_file()
+
+ # test file: rewritten, custom pytest cache tag
+ test_foo_pyc = get_cache_dir(test_foo) / ("test_foo" + PYC_TAIL)
+ 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
+ )
+ assert bar_init_pyc.is_file()
+
+
+class TestReprSizeVerbosity:
+ """
+ Check that verbosity also controls the string length threshold to shorten it using
+ ellipsis.
+ """
+
+ @pytest.mark.parametrize(
+ "verbose, expected_size",
+ [
+ (0, DEFAULT_REPR_MAX_SIZE),
+ (1, DEFAULT_REPR_MAX_SIZE * 10),
+ (2, None),
+ (3, None),
+ ],
+ )
+ def test_get_maxsize_for_saferepr(self, verbose: int, expected_size) -> None:
+ class FakeConfig:
+ def getoption(self, name: str) -> int:
+ assert name == "verbose"
+ return verbose
+
+ config = FakeConfig()
+ assert _get_maxsize_for_saferepr(cast(Config, config)) == expected_size
+
+ def create_test_file(self, pytester: Pytester, size: int) -> None:
+ pytester.makepyfile(
+ f"""
+ def test_very_long_string():
+ text = "x" * {size}
+ assert "hello world" in text
+ """
+ )
+
+ def test_default_verbosity(self, pytester: Pytester) -> None:
+ self.create_test_file(pytester, DEFAULT_REPR_MAX_SIZE)
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*xxx...xxx*"])
+
+ def test_increased_verbosity(self, pytester: Pytester) -> None:
+ self.create_test_file(pytester, DEFAULT_REPR_MAX_SIZE)
+ result = pytester.runpytest("-v")
+ result.stdout.no_fnmatch_line("*xxx...xxx*")
+
+ def test_max_increased_verbosity(self, pytester: Pytester) -> None:
+ self.create_test_file(pytester, DEFAULT_REPR_MAX_SIZE * 10)
+ result = pytester.runpytest("-vv")
+ result.stdout.no_fnmatch_line("*xxx...xxx*")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_cacheprovider.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_cacheprovider.py
new file mode 100644
index 0000000000..cc6d547dfb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_cacheprovider.py
@@ -0,0 +1,1251 @@
+import os
+import shutil
+from pathlib import Path
+from typing import Generator
+from typing import List
+
+import pytest
+from _pytest.config import ExitCode
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pytester import Pytester
+from _pytest.tmpdir import TempPathFactory
+
+pytest_plugins = ("pytester",)
+
+
+class TestNewAPI:
+ def test_config_cache_mkdir(self, pytester: Pytester) -> None:
+ pytester.makeini("[pytest]")
+ config = pytester.parseconfigure()
+ assert config.cache is not None
+ with pytest.raises(ValueError):
+ config.cache.mkdir("key/name")
+
+ p = config.cache.mkdir("name")
+ assert p.is_dir()
+
+ def test_config_cache_dataerror(self, pytester: Pytester) -> None:
+ pytester.makeini("[pytest]")
+ config = pytester.parseconfigure()
+ assert config.cache is not None
+ cache = config.cache
+ pytest.raises(TypeError, lambda: cache.set("key/name", cache))
+ config.cache.set("key/name", 0)
+ config.cache._getvaluepath("key/name").write_bytes(b"123invalid")
+ val = config.cache.get("key/name", -2)
+ assert val == -2
+
+ @pytest.mark.filterwarnings("ignore:could not create cache path")
+ def test_cache_writefail_cachfile_silent(self, pytester: Pytester) -> None:
+ pytester.makeini("[pytest]")
+ pytester.path.joinpath(".pytest_cache").write_text("gone wrong")
+ config = pytester.parseconfigure()
+ cache = config.cache
+ assert cache is not None
+ cache.set("test/broken", [])
+
+ @pytest.fixture
+ def unwritable_cache_dir(self, pytester: Pytester) -> Generator[Path, None, None]:
+ cache_dir = pytester.path.joinpath(".pytest_cache")
+ cache_dir.mkdir()
+ mode = cache_dir.stat().st_mode
+ cache_dir.chmod(0)
+ if os.access(cache_dir, os.W_OK):
+ pytest.skip("Failed to make cache dir unwritable")
+
+ yield cache_dir
+ cache_dir.chmod(mode)
+
+ @pytest.mark.filterwarnings(
+ "ignore:could not create cache path:pytest.PytestWarning"
+ )
+ def test_cache_writefail_permissions(
+ self, unwritable_cache_dir: Path, pytester: Pytester
+ ) -> None:
+ pytester.makeini("[pytest]")
+ config = pytester.parseconfigure()
+ cache = config.cache
+ assert cache is not None
+ cache.set("test/broken", [])
+
+ @pytest.mark.filterwarnings("default")
+ def test_cache_failure_warns(
+ self,
+ pytester: Pytester,
+ monkeypatch: MonkeyPatch,
+ unwritable_cache_dir: Path,
+ ) -> None:
+ monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
+
+ pytester.makepyfile("def test_error(): raise Exception")
+ result = pytester.runpytest()
+ assert result.ret == 1
+ # warnings from nodeids, lastfailed, and stepwise
+ result.stdout.fnmatch_lines(
+ [
+ # Validate location/stacklevel of warning from cacheprovider.
+ "*= warnings summary =*",
+ "*/cacheprovider.py:*",
+ " */cacheprovider.py:*: PytestCacheWarning: could not create cache path "
+ f"{unwritable_cache_dir}/v/cache/nodeids",
+ ' config.cache.set("cache/nodeids", sorted(self.cached_nodeids))',
+ "*1 failed, 3 warnings in*",
+ ]
+ )
+
+ def test_config_cache(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_configure(config):
+ # see that we get cache information early on
+ assert hasattr(config, "cache")
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_session(pytestconfig):
+ assert hasattr(pytestconfig, "cache")
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_cachefuncarg(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_cachefuncarg(cache):
+ val = cache.get("some/thing", None)
+ assert val is None
+ cache.set("some/thing", [1])
+ pytest.raises(TypeError, lambda: cache.get("some/thing"))
+ val = cache.get("some/thing", [])
+ assert val == [1]
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_custom_rel_cache_dir(self, pytester: Pytester) -> None:
+ rel_cache_dir = os.path.join("custom_cache_dir", "subdir")
+ pytester.makeini(
+ """
+ [pytest]
+ cache_dir = {cache_dir}
+ """.format(
+ cache_dir=rel_cache_dir
+ )
+ )
+ pytester.makepyfile(test_errored="def test_error():\n assert False")
+ pytester.runpytest()
+ assert pytester.path.joinpath(rel_cache_dir).is_dir()
+
+ def test_custom_abs_cache_dir(
+ self, pytester: Pytester, tmp_path_factory: TempPathFactory
+ ) -> None:
+ tmp = tmp_path_factory.mktemp("tmp")
+ abs_cache_dir = tmp / "custom_cache_dir"
+ pytester.makeini(
+ """
+ [pytest]
+ cache_dir = {cache_dir}
+ """.format(
+ cache_dir=abs_cache_dir
+ )
+ )
+ pytester.makepyfile(test_errored="def test_error():\n assert False")
+ pytester.runpytest()
+ assert abs_cache_dir.is_dir()
+
+ def test_custom_cache_dir_with_env_var(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ monkeypatch.setenv("env_var", "custom_cache_dir")
+ pytester.makeini(
+ """
+ [pytest]
+ cache_dir = {cache_dir}
+ """.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:
+ pytester.makepyfile("""def test_foo(): pass""")
+ if env:
+ monkeypatch.setenv(*env)
+ expected = os.path.join(env[1], ".pytest_cache")
+ else:
+ monkeypatch.delenv("TOX_ENV_DIR", raising=False)
+ expected = ".pytest_cache"
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(["cachedir: %s" % expected])
+
+
+def test_cache_reportheader_external_abspath(
+ pytester: Pytester, tmp_path_factory: TempPathFactory
+) -> None:
+ external_cache = tmp_path_factory.mktemp(
+ "test_cache_reportheader_external_abspath_abs"
+ )
+
+ pytester.makepyfile("def test_hello(): pass")
+ pytester.makeini(
+ """
+ [pytest]
+ cache_dir = {abscache}
+ """.format(
+ abscache=external_cache
+ )
+ )
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines([f"cachedir: {external_cache}"])
+
+
+def test_cache_show(pytester: Pytester) -> None:
+ result = pytester.runpytest("--cache-show")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*cache is empty*"])
+ pytester.makeconftest(
+ """
+ def pytest_configure(config):
+ config.cache.set("my/name", [1,2,3])
+ config.cache.set("my/hello", "world")
+ config.cache.set("other/some", {1:2})
+ dp = config.cache.mkdir("mydb")
+ dp.joinpath("hello").touch()
+ dp.joinpath("world").touch()
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 5 # no tests executed
+
+ result = pytester.runpytest("--cache-show")
+ result.stdout.fnmatch_lines(
+ [
+ "*cachedir:*",
+ "*- cache values for '[*]' -*",
+ "cache/nodeids contains:",
+ "my/name contains:",
+ " [1, 2, 3]",
+ "other/some contains:",
+ " {*'1': 2}",
+ "*- cache directories for '[*]' -*",
+ "*mydb/hello*length 0*",
+ "*mydb/world*length 0*",
+ ]
+ )
+ assert result.ret == 0
+
+ result = pytester.runpytest("--cache-show", "*/hello")
+ result.stdout.fnmatch_lines(
+ [
+ "*cachedir:*",
+ "*- cache values for '[*]/hello' -*",
+ "my/hello contains:",
+ " *'world'",
+ "*- cache directories for '[*]/hello' -*",
+ "d/mydb/hello*length 0*",
+ ]
+ )
+ stdout = result.stdout.str()
+ assert "other/some" not in stdout
+ assert "d/mydb/world" not in stdout
+ assert result.ret == 0
+
+
+class TestLastFailed:
+ def test_lastfailed_usecase(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ monkeypatch.setattr("sys.dont_write_bytecode", True)
+ p = pytester.makepyfile(
+ """
+ def test_1(): assert 0
+ def test_2(): assert 0
+ def test_3(): assert 1
+ """
+ )
+ result = pytester.runpytest(str(p))
+ result.stdout.fnmatch_lines(["*2 failed*"])
+ p = pytester.makepyfile(
+ """
+ def test_1(): assert 1
+ def test_2(): assert 1
+ def test_3(): assert 0
+ """
+ )
+ result = pytester.runpytest(str(p), "--lf")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 3 items / 1 deselected / 2 selected",
+ "run-last-failure: rerun previous 2 failures",
+ "*= 2 passed, 1 deselected in *",
+ ]
+ )
+ result = pytester.runpytest(str(p), "--lf")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 3 items",
+ "run-last-failure: no previously failed tests, not deselecting items.",
+ "*1 failed*2 passed*",
+ ]
+ )
+ pytester.path.joinpath(".pytest_cache", ".git").mkdir(parents=True)
+ result = pytester.runpytest(str(p), "--lf", "--cache-clear")
+ result.stdout.fnmatch_lines(["*1 failed*2 passed*"])
+ assert pytester.path.joinpath(".pytest_cache", "README.md").is_file()
+ assert pytester.path.joinpath(".pytest_cache", ".git").is_dir()
+
+ # Run this again to make sure clear-cache is robust
+ if os.path.isdir(".pytest_cache"):
+ shutil.rmtree(".pytest_cache")
+ result = pytester.runpytest("--lf", "--cache-clear")
+ result.stdout.fnmatch_lines(["*1 failed*2 passed*"])
+
+ def test_failedfirst_order(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_a="def test_always_passes(): pass",
+ test_b="def test_always_fails(): assert 0",
+ )
+ result = pytester.runpytest()
+ # Test order will be collection order; alphabetical
+ result.stdout.fnmatch_lines(["test_a.py*", "test_b.py*"])
+ result = pytester.runpytest("--ff")
+ # Test order will be failing tests first
+ result.stdout.fnmatch_lines(
+ [
+ "collected 2 items",
+ "run-last-failure: rerun previous 1 failure first",
+ "test_b.py*",
+ "test_a.py*",
+ ]
+ )
+
+ def test_lastfailed_failedfirst_order(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_a="def test_always_passes(): assert 1",
+ test_b="def test_always_fails(): assert 0",
+ )
+ result = pytester.runpytest()
+ # Test order will be collection order; alphabetical
+ result.stdout.fnmatch_lines(["test_a.py*", "test_b.py*"])
+ result = pytester.runpytest("--lf", "--ff")
+ # Test order will be failing tests first
+ result.stdout.fnmatch_lines(["test_b.py*"])
+ result.stdout.no_fnmatch_line("*test_a.py*")
+
+ def test_lastfailed_difference_invocations(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ monkeypatch.setattr("sys.dont_write_bytecode", True)
+ pytester.makepyfile(
+ test_a="""
+ def test_a1(): assert 0
+ def test_a2(): assert 1
+ """,
+ test_b="def test_b1(): assert 0",
+ )
+ p = pytester.path.joinpath("test_a.py")
+ p2 = pytester.path.joinpath("test_b.py")
+
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*2 failed*"])
+ result = pytester.runpytest("--lf", p2)
+ result.stdout.fnmatch_lines(["*1 failed*"])
+
+ pytester.makepyfile(test_b="def test_b1(): assert 1")
+ result = pytester.runpytest("--lf", p2)
+ result.stdout.fnmatch_lines(["*1 passed*"])
+ result = pytester.runpytest("--lf", p)
+ result.stdout.fnmatch_lines(
+ [
+ "collected 2 items / 1 deselected / 1 selected",
+ "run-last-failure: rerun previous 1 failure",
+ "*= 1 failed, 1 deselected in *",
+ ]
+ )
+
+ def test_lastfailed_usecase_splice(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ monkeypatch.setattr("sys.dont_write_bytecode", True)
+ pytester.makepyfile(
+ "def test_1(): assert 0", test_something="def test_2(): assert 0"
+ )
+ p2 = pytester.path.joinpath("test_something.py")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*2 failed*"])
+ result = pytester.runpytest("--lf", p2)
+ result.stdout.fnmatch_lines(["*1 failed*"])
+ result = pytester.runpytest("--lf")
+ result.stdout.fnmatch_lines(["*2 failed*"])
+
+ def test_lastfailed_xpass(self, pytester: Pytester) -> None:
+ pytester.inline_runsource(
+ """
+ import pytest
+ @pytest.mark.xfail
+ def test_hello():
+ assert 1
+ """
+ )
+ config = pytester.parseconfigure()
+ assert config.cache is not None
+ lastfailed = config.cache.get("cache/lastfailed", -1)
+ assert lastfailed == -1
+
+ def test_non_serializable_parametrize(self, pytester: Pytester) -> None:
+ """Test that failed parametrized tests with unmarshable parameters
+ don't break pytest-cache.
+ """
+ pytester.makepyfile(
+ r"""
+ import pytest
+
+ @pytest.mark.parametrize('val', [
+ b'\xac\x10\x02G',
+ ])
+ def test_fail(val):
+ assert False
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 failed in*"])
+
+ def test_terminal_report_lastfailed(self, pytester: Pytester) -> None:
+ test_a = pytester.makepyfile(
+ test_a="""
+ def test_a1(): pass
+ def test_a2(): pass
+ """
+ )
+ test_b = pytester.makepyfile(
+ test_b="""
+ def test_b1(): assert 0
+ def test_b2(): assert 0
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["collected 4 items", "*2 failed, 2 passed in*"])
+
+ result = pytester.runpytest("--lf")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 2 items",
+ "run-last-failure: rerun previous 2 failures (skipped 1 file)",
+ "*2 failed in*",
+ ]
+ )
+
+ result = pytester.runpytest(test_a, "--lf")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 2 items",
+ "run-last-failure: 2 known failures not in selected tests",
+ "*2 passed in*",
+ ]
+ )
+
+ result = pytester.runpytest(test_b, "--lf")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 2 items",
+ "run-last-failure: rerun previous 2 failures",
+ "*2 failed in*",
+ ]
+ )
+
+ result = pytester.runpytest("test_b.py::test_b1", "--lf")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 1 item",
+ "run-last-failure: rerun previous 1 failure",
+ "*1 failed in*",
+ ]
+ )
+
+ def test_terminal_report_failedfirst(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_a="""
+ def test_a1(): assert 0
+ def test_a2(): pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["collected 2 items", "*1 failed, 1 passed in*"])
+
+ result = pytester.runpytest("--ff")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 2 items",
+ "run-last-failure: rerun previous 1 failure first",
+ "*1 failed, 1 passed in*",
+ ]
+ )
+
+ def test_lastfailed_collectfailure(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+
+ pytester.makepyfile(
+ test_maybe="""
+ import os
+ env = os.environ
+ if '1' == env['FAILIMPORT']:
+ raise ImportError('fail')
+ def test_hello():
+ assert '0' == env['FAILTEST']
+ """
+ )
+
+ def rlf(fail_import, fail_run):
+ monkeypatch.setenv("FAILIMPORT", str(fail_import))
+ monkeypatch.setenv("FAILTEST", str(fail_run))
+
+ pytester.runpytest("-q")
+ config = pytester.parseconfigure()
+ assert config.cache is not None
+ lastfailed = config.cache.get("cache/lastfailed", -1)
+ return lastfailed
+
+ lastfailed = rlf(fail_import=0, fail_run=0)
+ assert lastfailed == -1
+
+ lastfailed = rlf(fail_import=1, fail_run=0)
+ assert list(lastfailed) == ["test_maybe.py"]
+
+ lastfailed = rlf(fail_import=0, fail_run=1)
+ assert list(lastfailed) == ["test_maybe.py::test_hello"]
+
+ def test_lastfailed_failure_subset(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ pytester.makepyfile(
+ test_maybe="""
+ import os
+ env = os.environ
+ if '1' == env['FAILIMPORT']:
+ raise ImportError('fail')
+ def test_hello():
+ assert '0' == env['FAILTEST']
+ """
+ )
+
+ pytester.makepyfile(
+ test_maybe2="""
+ import os
+ env = os.environ
+ if '1' == env['FAILIMPORT']:
+ raise ImportError('fail')
+
+ def test_hello():
+ assert '0' == env['FAILTEST']
+
+ def test_pass():
+ pass
+ """
+ )
+
+ def rlf(fail_import, fail_run, args=()):
+ monkeypatch.setenv("FAILIMPORT", str(fail_import))
+ monkeypatch.setenv("FAILTEST", str(fail_run))
+
+ result = pytester.runpytest("-q", "--lf", *args)
+ config = pytester.parseconfigure()
+ assert config.cache is not None
+ lastfailed = config.cache.get("cache/lastfailed", -1)
+ return result, lastfailed
+
+ result, lastfailed = rlf(fail_import=0, fail_run=0)
+ assert lastfailed == -1
+ result.stdout.fnmatch_lines(["*3 passed*"])
+
+ result, lastfailed = rlf(fail_import=1, fail_run=0)
+ assert sorted(list(lastfailed)) == ["test_maybe.py", "test_maybe2.py"]
+
+ result, lastfailed = rlf(fail_import=0, fail_run=0, args=("test_maybe2.py",))
+ assert list(lastfailed) == ["test_maybe.py"]
+
+ # edge case of test selection - even if we remember failures
+ # from other tests we still need to run all tests if no test
+ # matches the failures
+ result, lastfailed = rlf(fail_import=0, fail_run=0, args=("test_maybe2.py",))
+ assert list(lastfailed) == ["test_maybe.py"]
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+ def test_lastfailed_creates_cache_when_needed(self, pytester: Pytester) -> None:
+ # Issue #1342
+ pytester.makepyfile(test_empty="")
+ pytester.runpytest("-q", "--lf")
+ assert not os.path.exists(".pytest_cache/v/cache/lastfailed")
+
+ pytester.makepyfile(test_successful="def test_success():\n assert True")
+ pytester.runpytest("-q", "--lf")
+ assert not os.path.exists(".pytest_cache/v/cache/lastfailed")
+
+ pytester.makepyfile(test_errored="def test_error():\n assert False")
+ pytester.runpytest("-q", "--lf")
+ assert os.path.exists(".pytest_cache/v/cache/lastfailed")
+
+ def test_xfail_not_considered_failure(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.xfail
+ def test(): assert 0
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 xfailed*"])
+ assert self.get_cached_last_failed(pytester) == []
+
+ def test_xfail_strict_considered_failure(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.xfail(strict=True)
+ def test(): pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 failed*"])
+ assert self.get_cached_last_failed(pytester) == [
+ "test_xfail_strict_considered_failure.py::test"
+ ]
+
+ @pytest.mark.parametrize("mark", ["mark.xfail", "mark.skip"])
+ def test_failed_changed_to_xfail_or_skip(
+ self, pytester: Pytester, mark: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def test(): assert 0
+ """
+ )
+ result = pytester.runpytest()
+ assert self.get_cached_last_failed(pytester) == [
+ "test_failed_changed_to_xfail_or_skip.py::test"
+ ]
+ assert result.ret == 1
+
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.{mark}
+ def test(): assert 0
+ """.format(
+ mark=mark
+ )
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+ assert self.get_cached_last_failed(pytester) == []
+ assert result.ret == 0
+
+ @pytest.mark.parametrize("quiet", [True, False])
+ @pytest.mark.parametrize("opt", ["--ff", "--lf"])
+ def test_lf_and_ff_prints_no_needless_message(
+ self, quiet: bool, opt: str, pytester: Pytester
+ ) -> None:
+ # Issue 3853
+ pytester.makepyfile("def test(): assert 0")
+ args = [opt]
+ if quiet:
+ args.append("-q")
+ result = pytester.runpytest(*args)
+ result.stdout.no_fnmatch_line("*run all*")
+
+ result = pytester.runpytest(*args)
+ if quiet:
+ result.stdout.no_fnmatch_line("*run all*")
+ else:
+ assert "rerun previous" in result.stdout.str()
+
+ def get_cached_last_failed(self, pytester: Pytester) -> List[str]:
+ config = pytester.parseconfigure()
+ assert config.cache is not None
+ return sorted(config.cache.get("cache/lastfailed", {}))
+
+ def test_cache_cumulative(self, pytester: Pytester) -> None:
+ """Test workflow where user fixes errors gradually file by file using --lf."""
+ # 1. initial run
+ test_bar = pytester.makepyfile(
+ test_bar="""
+ def test_bar_1(): pass
+ def test_bar_2(): assert 0
+ """
+ )
+ test_foo = pytester.makepyfile(
+ test_foo="""
+ def test_foo_3(): pass
+ def test_foo_4(): assert 0
+ """
+ )
+ pytester.runpytest()
+ assert self.get_cached_last_failed(pytester) == [
+ "test_bar.py::test_bar_2",
+ "test_foo.py::test_foo_4",
+ ]
+
+ # 2. fix test_bar_2, run only test_bar.py
+ pytester.makepyfile(
+ test_bar="""
+ def test_bar_1(): pass
+ def test_bar_2(): pass
+ """
+ )
+ result = pytester.runpytest(test_bar)
+ result.stdout.fnmatch_lines(["*2 passed*"])
+ # ensure cache does not forget that test_foo_4 failed once before
+ assert self.get_cached_last_failed(pytester) == ["test_foo.py::test_foo_4"]
+
+ result = pytester.runpytest("--last-failed")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 1 item",
+ "run-last-failure: rerun previous 1 failure (skipped 1 file)",
+ "*= 1 failed in *",
+ ]
+ )
+ assert self.get_cached_last_failed(pytester) == ["test_foo.py::test_foo_4"]
+
+ # 3. fix test_foo_4, run only test_foo.py
+ test_foo = pytester.makepyfile(
+ test_foo="""
+ def test_foo_3(): pass
+ def test_foo_4(): pass
+ """
+ )
+ result = pytester.runpytest(test_foo, "--last-failed")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 2 items / 1 deselected / 1 selected",
+ "run-last-failure: rerun previous 1 failure",
+ "*= 1 passed, 1 deselected in *",
+ ]
+ )
+ assert self.get_cached_last_failed(pytester) == []
+
+ result = pytester.runpytest("--last-failed")
+ result.stdout.fnmatch_lines(["*4 passed*"])
+ assert self.get_cached_last_failed(pytester) == []
+
+ def test_lastfailed_no_failures_behavior_all_passed(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_1(): pass
+ def test_2(): pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*2 passed*"])
+ result = pytester.runpytest("--lf")
+ result.stdout.fnmatch_lines(["*2 passed*"])
+ result = pytester.runpytest("--lf", "--lfnf", "all")
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+ # Ensure the list passed to pytest_deselected is a copy,
+ # and not a reference which is cleared right after.
+ pytester.makeconftest(
+ """
+ deselected = []
+
+ def pytest_deselected(items):
+ global deselected
+ deselected = items
+
+ def pytest_sessionfinish():
+ print("\\ndeselected={}".format(len(deselected)))
+ """
+ )
+
+ result = pytester.runpytest("--lf", "--lfnf", "none")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 2 items / 2 deselected",
+ "run-last-failure: no previously failed tests, deselecting all items.",
+ "deselected=2",
+ "* 2 deselected in *",
+ ]
+ )
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+ def test_lastfailed_no_failures_behavior_empty_cache(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_1(): pass
+ def test_2(): assert 0
+ """
+ )
+ result = pytester.runpytest("--lf", "--cache-clear")
+ result.stdout.fnmatch_lines(["*1 failed*1 passed*"])
+ result = pytester.runpytest("--lf", "--cache-clear", "--lfnf", "all")
+ result.stdout.fnmatch_lines(["*1 failed*1 passed*"])
+ result = pytester.runpytest("--lf", "--cache-clear", "--lfnf", "none")
+ result.stdout.fnmatch_lines(["*2 desel*"])
+
+ def test_lastfailed_skip_collection(self, pytester: Pytester) -> None:
+ """
+ Test --lf behavior regarding skipping collection of files that are not marked as
+ failed in the cache (#5172).
+ """
+ pytester.makepyfile(
+ **{
+ "pkg1/test_1.py": """
+ import pytest
+
+ @pytest.mark.parametrize('i', range(3))
+ def test_1(i): pass
+ """,
+ "pkg2/test_2.py": """
+ import pytest
+
+ @pytest.mark.parametrize('i', range(5))
+ def test_1(i):
+ assert i not in (1, 3)
+ """,
+ }
+ )
+ # first run: collects 8 items (test_1: 3, test_2: 5)
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["collected 8 items", "*2 failed*6 passed*"])
+ # second run: collects only 5 items from test_2, because all tests from test_1 have passed
+ result = pytester.runpytest("--lf")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 2 items",
+ "run-last-failure: rerun previous 2 failures (skipped 1 file)",
+ "*= 2 failed in *",
+ ]
+ )
+
+ # add another file and check if message is correct when skipping more than 1 file
+ pytester.makepyfile(
+ **{
+ "pkg1/test_3.py": """
+ def test_3(): pass
+ """
+ }
+ )
+ result = pytester.runpytest("--lf")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 2 items",
+ "run-last-failure: rerun previous 2 failures (skipped 2 files)",
+ "*= 2 failed in *",
+ ]
+ )
+
+ def test_lastfailed_with_known_failures_not_being_selected(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ **{
+ "pkg1/test_1.py": """def test_1(): assert 0""",
+ "pkg1/test_2.py": """def test_2(): pass""",
+ }
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"])
+
+ Path("pkg1/test_1.py").unlink()
+ result = pytester.runpytest("--lf")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 1 item",
+ "run-last-failure: 1 known failures not in selected tests",
+ "* 1 passed in *",
+ ]
+ )
+
+ # Recreate file with known failure.
+ pytester.makepyfile(**{"pkg1/test_1.py": """def test_1(): assert 0"""})
+ result = pytester.runpytest("--lf")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 1 item",
+ "run-last-failure: rerun previous 1 failure (skipped 1 file)",
+ "* 1 failed in *",
+ ]
+ )
+
+ # Remove/rename test: collects the file again.
+ pytester.makepyfile(**{"pkg1/test_1.py": """def test_renamed(): assert 0"""})
+ result = pytester.runpytest("--lf", "-rf")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 2 items",
+ "run-last-failure: 1 known failures not in selected tests",
+ "pkg1/test_1.py F *",
+ "pkg1/test_2.py . *",
+ "FAILED pkg1/test_1.py::test_renamed - assert 0",
+ "* 1 failed, 1 passed in *",
+ ]
+ )
+
+ result = pytester.runpytest("--lf", "--co")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 1 item",
+ "run-last-failure: rerun previous 1 failure (skipped 1 file)",
+ "",
+ "<Module pkg1/test_1.py>",
+ " <Function test_renamed>",
+ ]
+ )
+
+ def test_lastfailed_args_with_deselected(self, pytester: Pytester) -> None:
+ """Test regression with --lf running into NoMatch error.
+
+ This was caused by it not collecting (non-failed) nodes given as
+ arguments.
+ """
+ pytester.makepyfile(
+ **{
+ "pkg1/test_1.py": """
+ def test_pass(): pass
+ def test_fail(): assert 0
+ """,
+ }
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"])
+ assert result.ret == 1
+
+ result = pytester.runpytest("pkg1/test_1.py::test_pass", "--lf", "--co")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(
+ [
+ "*collected 1 item",
+ "run-last-failure: 1 known failures not in selected tests",
+ "",
+ "<Module pkg1/test_1.py>",
+ " <Function test_pass>",
+ ],
+ consecutive=True,
+ )
+
+ result = pytester.runpytest(
+ "pkg1/test_1.py::test_pass", "pkg1/test_1.py::test_fail", "--lf", "--co"
+ )
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(
+ [
+ "collected 2 items / 1 deselected / 1 selected",
+ "run-last-failure: rerun previous 1 failure",
+ "",
+ "<Module pkg1/test_1.py>",
+ " <Function test_fail>",
+ "*= 1/2 tests collected (1 deselected) in *",
+ ],
+ )
+
+ def test_lastfailed_with_class_items(self, pytester: Pytester) -> None:
+ """Test regression with --lf deselecting whole classes."""
+ pytester.makepyfile(
+ **{
+ "pkg1/test_1.py": """
+ class TestFoo:
+ def test_pass(self): pass
+ def test_fail(self): assert 0
+
+ def test_other(): assert 0
+ """,
+ }
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["collected 3 items", "* 2 failed, 1 passed in *"])
+ assert result.ret == 1
+
+ result = pytester.runpytest("--lf", "--co")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(
+ [
+ "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>",
+ "",
+ "*= 2/3 tests collected (1 deselected) in *",
+ ],
+ consecutive=True,
+ )
+
+ def test_lastfailed_with_all_filtered(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ **{
+ "pkg1/test_1.py": """
+ def test_fail(): assert 0
+ def test_pass(): pass
+ """,
+ }
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"])
+ assert result.ret == 1
+
+ # Remove known failure.
+ pytester.makepyfile(
+ **{
+ "pkg1/test_1.py": """
+ def test_pass(): pass
+ """,
+ }
+ )
+ result = pytester.runpytest("--lf", "--co")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 1 item",
+ "run-last-failure: 1 known failures not in selected tests",
+ "",
+ "<Module pkg1/test_1.py>",
+ " <Function test_pass>",
+ "",
+ "*= 1 test collected in*",
+ ],
+ consecutive=True,
+ )
+ assert result.ret == 0
+
+ def test_packages(self, pytester: Pytester) -> None:
+ """Regression test for #7758.
+
+ The particular issue here was that Package nodes were included in the
+ filtering, being themselves Modules for the __init__.py, even if they
+ had failed Modules in them.
+
+ The tests includes a test in an __init__.py file just to make sure the
+ fix doesn't somehow regress that, it is not critical for the issue.
+ """
+ pytester.makepyfile(
+ **{
+ "__init__.py": "",
+ "a/__init__.py": "def test_a_init(): assert False",
+ "a/test_one.py": "def test_1(): assert False",
+ "b/__init__.py": "",
+ "b/test_two.py": "def test_2(): assert False",
+ },
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ python_files = *.py
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(failed=3)
+ result = pytester.runpytest("--lf")
+ result.assert_outcomes(failed=3)
+
+
+class TestNewFirst:
+ def test_newfirst_usecase(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ **{
+ "test_1/test_1.py": """
+ def test_1(): assert 1
+ """,
+ "test_2/test_2.py": """
+ def test_1(): assert 1
+ """,
+ }
+ )
+
+ p1 = pytester.path.joinpath("test_1/test_1.py")
+ os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
+
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(
+ ["*test_1/test_1.py::test_1 PASSED*", "*test_2/test_2.py::test_1 PASSED*"]
+ )
+
+ result = pytester.runpytest("-v", "--nf")
+ result.stdout.fnmatch_lines(
+ ["*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")
+ os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
+
+ result = pytester.runpytest("--nf", "--collect-only", "-q")
+ result.stdout.fnmatch_lines(
+ [
+ "test_1/test_1.py::test_2",
+ "test_2/test_2.py::test_1",
+ "test_1/test_1.py::test_1",
+ ]
+ )
+
+ # Newest first with (plugin) pytest_collection_modifyitems hook.
+ pytester.makepyfile(
+ myplugin="""
+ def pytest_collection_modifyitems(items):
+ items[:] = sorted(items, key=lambda item: item.nodeid)
+ print("new_items:", [x.nodeid for x in items])
+ """
+ )
+ pytester.syspathinsert()
+ result = pytester.runpytest("--nf", "-p", "myplugin", "--collect-only", "-q")
+ result.stdout.fnmatch_lines(
+ [
+ "new_items: *test_1.py*test_1.py*test_2.py*",
+ "test_1/test_1.py::test_2",
+ "test_2/test_2.py::test_1",
+ "test_1/test_1.py::test_1",
+ ]
+ )
+
+ def test_newfirst_parametrize(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ **{
+ "test_1/test_1.py": """
+ import pytest
+ @pytest.mark.parametrize('num', [1, 2])
+ def test_1(num): assert num
+ """,
+ "test_2/test_2.py": """
+ import pytest
+ @pytest.mark.parametrize('num', [1, 2])
+ def test_1(num): assert num
+ """,
+ }
+ )
+
+ p1 = pytester.path.joinpath("test_1/test_1.py")
+ os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
+
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(
+ [
+ "*test_1/test_1.py::test_1[1*",
+ "*test_1/test_1.py::test_1[2*",
+ "*test_2/test_2.py::test_1[1*",
+ "*test_2/test_2.py::test_1[2*",
+ ]
+ )
+
+ result = pytester.runpytest("-v", "--nf")
+ result.stdout.fnmatch_lines(
+ [
+ "*test_2/test_2.py::test_1[1*",
+ "*test_2/test_2.py::test_1[2*",
+ "*test_1/test_1.py::test_1[1*",
+ "*test_1/test_1.py::test_1[2*",
+ ]
+ )
+
+ p1.write_text(
+ "import pytest\n"
+ "@pytest.mark.parametrize('num', [1, 2, 3])\n"
+ "def test_1(num): assert num\n"
+ )
+ os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
+
+ # Running only a subset does not forget about existing ones.
+ result = pytester.runpytest("-v", "--nf", "test_2/test_2.py")
+ result.stdout.fnmatch_lines(
+ ["*test_2/test_2.py::test_1[1*", "*test_2/test_2.py::test_1[2*"]
+ )
+
+ result = pytester.runpytest("-v", "--nf")
+ result.stdout.fnmatch_lines(
+ [
+ "*test_1/test_1.py::test_1[3*",
+ "*test_2/test_2.py::test_1[1*",
+ "*test_2/test_2.py::test_1[2*",
+ "*test_1/test_1.py::test_1[1*",
+ "*test_1/test_1.py::test_1[2*",
+ ]
+ )
+
+
+class TestReadme:
+ def check_readme(self, pytester: Pytester) -> bool:
+ config = pytester.parseconfigure()
+ assert config.cache is not None
+ readme = config.cache._cachedir.joinpath("README.md")
+ return readme.is_file()
+
+ def test_readme_passed(self, pytester: Pytester) -> None:
+ pytester.makepyfile("def test_always_passes(): pass")
+ pytester.runpytest()
+ assert self.check_readme(pytester) is True
+
+ def test_readme_failed(self, pytester: Pytester) -> None:
+ pytester.makepyfile("def test_always_fails(): assert 0")
+ pytester.runpytest()
+ assert self.check_readme(pytester) is True
+
+
+def test_gitignore(pytester: Pytester) -> 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")
+ 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")
+ assert gitignore_path.read_text(encoding="UTF-8") == "custom"
+
+
+def test_preserve_keys_order(pytester: Pytester) -> None:
+ """Ensure keys order is preserved when saving dicts (#9205)."""
+ from _pytest.cacheprovider import Cache
+
+ config = pytester.parseconfig()
+ cache = Cache.for_config(config, _ispytest=True)
+ cache.set("foo", {"z": 1, "b": 2, "a": 3, "d": 10})
+ read_back = cache.get("foo", None)
+ assert list(read_back.items()) == [("z", 1), ("b", 2), ("a", 3), ("d", 10)]
+
+
+def test_does_not_create_boilerplate_in_existing_dirs(pytester: Pytester) -> None:
+ from _pytest.cacheprovider import Cache
+
+ pytester.makeini(
+ """
+ [pytest]
+ cache_dir = .
+ """
+ )
+ config = pytester.parseconfig()
+ cache = Cache.for_config(config, _ispytest=True)
+ cache.set("foo", "bar")
+
+ assert os.path.isdir("v") # cache contents
+ assert not os.path.exists(".gitignore")
+ assert not os.path.exists("README.md")
+
+
+def test_cachedir_tag(pytester: Pytester) -> None:
+ """Ensure we automatically create CACHEDIR.TAG file in the pytest_cache directory (#4278)."""
+ from _pytest.cacheprovider import Cache
+ from _pytest.cacheprovider import CACHEDIR_TAG_CONTENT
+
+ config = pytester.parseconfig()
+ cache = Cache.for_config(config, _ispytest=True)
+ cache.set("foo", "bar")
+ cachedir_tag_path = cache._cachedir.joinpath("CACHEDIR.TAG")
+ assert cachedir_tag_path.read_bytes() == CACHEDIR_TAG_CONTENT
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_capture.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_capture.py
new file mode 100644
index 0000000000..1bc1f2f8db
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_capture.py
@@ -0,0 +1,1666 @@
+import contextlib
+import io
+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
+from _pytest.capture import CaptureManager
+from _pytest.capture import CaptureResult
+from _pytest.capture import MultiCapture
+from _pytest.config import ExitCode
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pytester import Pytester
+
+# note: py.io capture tests where copied from
+# pylib 1.4.20.dev2 (rev 13d9af95547e)
+
+
+def StdCaptureFD(
+ out: bool = True, err: bool = True, in_: bool = True
+) -> MultiCapture[str]:
+ return capture.MultiCapture(
+ in_=capture.FDCapture(0) if in_ else None,
+ out=capture.FDCapture(1) if out else None,
+ err=capture.FDCapture(2) if err else None,
+ )
+
+
+def StdCapture(
+ out: bool = True, err: bool = True, in_: bool = True
+) -> MultiCapture[str]:
+ return capture.MultiCapture(
+ in_=capture.SysCapture(0) if in_ else None,
+ out=capture.SysCapture(1) if out else None,
+ err=capture.SysCapture(2) if err else None,
+ )
+
+
+def TeeStdCapture(
+ out: bool = True, err: bool = True, in_: bool = True
+) -> MultiCapture[str]:
+ return capture.MultiCapture(
+ in_=capture.SysCapture(0, tee=True) if in_ else None,
+ out=capture.SysCapture(1, tee=True) if out else None,
+ err=capture.SysCapture(2, tee=True) if err else None,
+ )
+
+
+class TestCaptureManager:
+ @pytest.mark.parametrize("method", ["no", "sys", "fd"])
+ def test_capturing_basic_api(self, method) -> None:
+ capouter = StdCaptureFD()
+ old = sys.stdout, sys.stderr, sys.stdin
+ try:
+ capman = CaptureManager(method)
+ capman.start_global_capturing()
+ capman.suspend_global_capture()
+ outerr = capman.read_global_capture()
+ assert outerr == ("", "")
+ capman.suspend_global_capture()
+ outerr = capman.read_global_capture()
+ assert outerr == ("", "")
+ print("hello")
+ capman.suspend_global_capture()
+ out, err = capman.read_global_capture()
+ if method == "no":
+ assert old == (sys.stdout, sys.stderr, sys.stdin)
+ else:
+ assert not out
+ capman.resume_global_capture()
+ print("hello")
+ capman.suspend_global_capture()
+ out, err = capman.read_global_capture()
+ if method != "no":
+ assert out == "hello\n"
+ capman.stop_global_capturing()
+ finally:
+ capouter.stop_capturing()
+
+ def test_init_capturing(self):
+ capouter = StdCaptureFD()
+ try:
+ capman = CaptureManager("fd")
+ capman.start_global_capturing()
+ pytest.raises(AssertionError, capman.start_global_capturing)
+ capman.stop_global_capturing()
+ finally:
+ capouter.stop_capturing()
+
+
+@pytest.mark.parametrize("method", ["fd", "sys"])
+def test_capturing_unicode(pytester: Pytester, method: str) -> None:
+ obj = "'b\u00f6y'"
+ pytester.makepyfile(
+ """\
+ # taken from issue 227 from nosetests
+ def test_unicode():
+ import sys
+ print(sys.stdout)
+ print(%s)
+ """
+ % obj
+ )
+ result = pytester.runpytest("--capture=%s" % method)
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+@pytest.mark.parametrize("method", ["fd", "sys"])
+def test_capturing_bytes_in_utf8_encoding(pytester: Pytester, method: str) -> None:
+ pytester.makepyfile(
+ """\
+ def test_unicode():
+ print('b\\u00f6y')
+ """
+ )
+ result = pytester.runpytest("--capture=%s" % method)
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+def test_collect_capturing(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import sys
+
+ print("collect %s failure" % 13)
+ sys.stderr.write("collect %s_stderr failure" % 13)
+ import xyz42123
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(
+ [
+ "*Captured stdout*",
+ "collect 13 failure",
+ "*Captured stderr*",
+ "collect 13_stderr failure",
+ ]
+ )
+
+
+class TestPerTestCapturing:
+ def test_capture_and_fixtures(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def setup_module(mod):
+ print("setup module")
+ def setup_function(function):
+ print("setup " + function.__name__)
+ def test_func1():
+ print("in func1")
+ assert 0
+ def test_func2():
+ print("in func2")
+ assert 0
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(
+ [
+ "setup module*",
+ "setup test_func1*",
+ "in func1*",
+ "setup test_func2*",
+ "in func2*",
+ ]
+ )
+
+ @pytest.mark.xfail(reason="unimplemented feature")
+ def test_capture_scope_cache(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import sys
+ def setup_module(func):
+ print("module-setup")
+ def setup_function(func):
+ print("function-setup")
+ def test_func():
+ print("in function")
+ assert 0
+ def teardown_function(func):
+ print("in teardown")
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(
+ [
+ "*test_func():*",
+ "*Captured stdout during setup*",
+ "module-setup*",
+ "function-setup*",
+ "*Captured stdout*",
+ "in teardown*",
+ ]
+ )
+
+ def test_no_carry_over(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def test_func1():
+ print("in func1")
+ def test_func2():
+ print("in func2")
+ assert 0
+ """
+ )
+ result = pytester.runpytest(p)
+ s = result.stdout.str()
+ assert "in func1" not in s
+ assert "in func2" in s
+
+ def test_teardown_capturing(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def setup_function(function):
+ print("setup func1")
+ def teardown_function(function):
+ print("teardown func1")
+ assert 0
+ def test_func1():
+ print("in func1")
+ pass
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(
+ [
+ "*teardown_function*",
+ "*Captured stdout*",
+ "setup func1*",
+ "in func1*",
+ "teardown func1*",
+ # "*1 fixture failure*"
+ ]
+ )
+
+ def test_teardown_capturing_final(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def teardown_module(mod):
+ print("teardown module")
+ assert 0
+ def test_func():
+ pass
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(
+ [
+ "*def teardown_module(mod):*",
+ "*Captured stdout*",
+ "*teardown module*",
+ "*1 error*",
+ ]
+ )
+
+ def test_capturing_outerr(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """\
+ import sys
+ def test_capturing():
+ print(42)
+ sys.stderr.write(str(23))
+ def test_capturing_error():
+ print(1)
+ sys.stderr.write(str(2))
+ raise ValueError
+ """
+ )
+ result = pytester.runpytest(p1)
+ result.stdout.fnmatch_lines(
+ [
+ "*test_capturing_outerr.py .F*",
+ "====* FAILURES *====",
+ "____*____",
+ "*test_capturing_outerr.py:8: ValueError",
+ "*--- Captured stdout *call*",
+ "1",
+ "*--- Captured stderr *call*",
+ "2",
+ ]
+ )
+
+
+class TestLoggingInteraction:
+ def test_logging_stream_ownership(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """\
+ def test_logging():
+ import logging
+ import pytest
+ stream = capture.CaptureIO()
+ logging.basicConfig(stream=stream)
+ stream.close() # to free memory/release resources
+ """
+ )
+ result = pytester.runpytest_subprocess(p)
+ assert result.stderr.str().find("atexit") == -1
+
+ def test_logging_and_immediate_setupteardown(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """\
+ import logging
+ def setup_function(function):
+ logging.warning("hello1")
+
+ def test_logging():
+ logging.warning("hello2")
+ assert 0
+
+ def teardown_function(function):
+ logging.warning("hello3")
+ assert 0
+ """
+ )
+ for optargs in (("--capture=sys",), ("--capture=fd",)):
+ print(optargs)
+ result = pytester.runpytest_subprocess(p, *optargs)
+ s = result.stdout.str()
+ result.stdout.fnmatch_lines(
+ ["*WARN*hello3", "*WARN*hello1", "*WARN*hello2"] # errors show first!
+ )
+ # verify proper termination
+ assert "closed" not in s
+
+ def test_logging_and_crossscope_fixtures(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """\
+ import logging
+ def setup_module(function):
+ logging.warning("hello1")
+
+ def test_logging():
+ logging.warning("hello2")
+ assert 0
+
+ def teardown_module(function):
+ logging.warning("hello3")
+ assert 0
+ """
+ )
+ for optargs in (("--capture=sys",), ("--capture=fd",)):
+ print(optargs)
+ result = pytester.runpytest_subprocess(p, *optargs)
+ s = result.stdout.str()
+ result.stdout.fnmatch_lines(
+ ["*WARN*hello3", "*WARN*hello1", "*WARN*hello2"] # errors come first
+ )
+ # verify proper termination
+ assert "closed" not in s
+
+ def test_conftestlogging_is_shown(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """\
+ import logging
+ logging.basicConfig()
+ logging.warning("hello435")
+ """
+ )
+ # make sure that logging is still captured in tests
+ result = pytester.runpytest_subprocess("-s", "-p", "no:capturelog")
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+ result.stderr.fnmatch_lines(["WARNING*hello435*"])
+ assert "operation on closed file" not in result.stderr.str()
+
+ def test_conftestlogging_and_test_logging(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """\
+ import logging
+ logging.basicConfig()
+ """
+ )
+ # make sure that logging is still captured in tests
+ p = pytester.makepyfile(
+ """\
+ def test_hello():
+ import logging
+ logging.warning("hello433")
+ assert 0
+ """
+ )
+ result = pytester.runpytest_subprocess(p, "-p", "no:capturelog")
+ assert result.ret != 0
+ result.stdout.fnmatch_lines(["WARNING*hello433*"])
+ assert "something" not in result.stderr.str()
+ assert "operation on closed file" not in result.stderr.str()
+
+ def test_logging_after_cap_stopped(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """\
+ import pytest
+ import logging
+
+ log = logging.getLogger(__name__)
+
+ @pytest.fixture
+ def log_on_teardown():
+ yield
+ log.warning('Logging on teardown')
+ """
+ )
+ # make sure that logging is still captured in tests
+ p = pytester.makepyfile(
+ """\
+ def test_hello(log_on_teardown):
+ import logging
+ logging.warning("hello433")
+ assert 1
+ raise KeyboardInterrupt()
+ """
+ )
+ result = pytester.runpytest_subprocess(p, "--log-cli-level", "info")
+ assert result.ret != 0
+ result.stdout.fnmatch_lines(
+ ["*WARNING*hello433*", "*WARNING*Logging on teardown*"]
+ )
+ assert (
+ "AttributeError: 'NoneType' object has no attribute 'resume_capturing'"
+ not in result.stderr.str()
+ )
+
+
+class TestCaptureFixture:
+ @pytest.mark.parametrize("opt", [[], ["-s"]])
+ def test_std_functional(self, pytester: Pytester, opt) -> None:
+ reprec = pytester.inline_runsource(
+ """\
+ def test_hello(capsys):
+ print(42)
+ out, err = capsys.readouterr()
+ assert out.startswith("42")
+ """,
+ *opt,
+ )
+ reprec.assertoutcome(passed=1)
+
+ def test_capsyscapfd(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """\
+ def test_one(capsys, capfd):
+ pass
+ def test_two(capfd, capsys):
+ pass
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(
+ [
+ "*ERROR*setup*test_one*",
+ "E*capfd*capsys*same*time*",
+ "*ERROR*setup*test_two*",
+ "E*capsys*capfd*same*time*",
+ "*2 errors*",
+ ]
+ )
+
+ def test_capturing_getfixturevalue(self, pytester: Pytester) -> None:
+ """Test that asking for "capfd" and "capsys" using request.getfixturevalue
+ in the same test is an error.
+ """
+ pytester.makepyfile(
+ """\
+ def test_one(capsys, request):
+ request.getfixturevalue("capfd")
+ def test_two(capfd, request):
+ request.getfixturevalue("capsys")
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*test_one*",
+ "E * cannot use capfd and capsys at the same time",
+ "*test_two*",
+ "E * cannot use capsys and capfd at the same time",
+ "*2 failed in*",
+ ]
+ )
+
+ def test_capsyscapfdbinary(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """\
+ def test_one(capsys, capfdbinary):
+ pass
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(
+ ["*ERROR*setup*test_one*", "E*capfdbinary*capsys*same*time*", "*1 error*"]
+ )
+
+ @pytest.mark.parametrize("method", ["sys", "fd"])
+ def test_capture_is_represented_on_failure_issue128(
+ self, pytester: Pytester, method
+ ) -> None:
+ p = pytester.makepyfile(
+ """\
+ def test_hello(cap{}):
+ print("xxx42xxx")
+ assert 0
+ """.format(
+ method
+ )
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(["xxx42xxx"])
+
+ def test_stdfd_functional(self, pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """\
+ def test_hello(capfd):
+ import os
+ os.write(1, b"42")
+ out, err = capfd.readouterr()
+ assert out.startswith("42")
+ capfd.close()
+ """
+ )
+ reprec.assertoutcome(passed=1)
+
+ @pytest.mark.parametrize("nl", ("\n", "\r\n", "\r"))
+ def test_cafd_preserves_newlines(self, capfd, nl) -> None:
+ print("test", end=nl)
+ out, err = capfd.readouterr()
+ assert out.endswith(nl)
+
+ def test_capfdbinary(self, pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """\
+ def test_hello(capfdbinary):
+ import os
+ # some likely un-decodable bytes
+ os.write(1, b'\\xfe\\x98\\x20')
+ out, err = capfdbinary.readouterr()
+ assert out == b'\\xfe\\x98\\x20'
+ assert err == b''
+ """
+ )
+ reprec.assertoutcome(passed=1)
+
+ def test_capsysbinary(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ r"""
+ def test_hello(capsysbinary):
+ import sys
+
+ sys.stdout.buffer.write(b'hello')
+
+ # Some likely un-decodable bytes.
+ sys.stdout.buffer.write(b'\xfe\x98\x20')
+
+ sys.stdout.buffer.flush()
+
+ # Ensure writing in text mode still works and is captured.
+ # https://github.com/pytest-dev/pytest/issues/6871
+ print("world", flush=True)
+
+ out, err = capsysbinary.readouterr()
+ assert out == b'hello\xfe\x98\x20world\n'
+ assert err == b''
+
+ print("stdout after")
+ print("stderr after", file=sys.stderr)
+ """
+ )
+ result = pytester.runpytest(str(p1), "-rA")
+ result.stdout.fnmatch_lines(
+ [
+ "*- Captured stdout call -*",
+ "stdout after",
+ "*- Captured stderr call -*",
+ "stderr after",
+ "*= 1 passed in *",
+ ]
+ )
+
+ def test_partial_setup_failure(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """\
+ def test_hello(capsys, missingarg):
+ pass
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(["*test_partial_setup_failure*", "*1 error*"])
+
+ def test_keyboardinterrupt_disables_capturing(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """\
+ def test_hello(capfd):
+ import os
+ os.write(1, b'42')
+ raise KeyboardInterrupt()
+ """
+ )
+ result = pytester.runpytest_subprocess(p)
+ result.stdout.fnmatch_lines(["*KeyboardInterrupt*"])
+ assert result.ret == 2
+
+ def test_capture_and_logging(self, pytester: Pytester) -> None:
+ """#14"""
+ p = pytester.makepyfile(
+ """\
+ import logging
+ def test_log(capsys):
+ logging.error('x')
+ """
+ )
+ result = pytester.runpytest_subprocess(p)
+ assert "closed" not in result.stderr.str()
+
+ @pytest.mark.parametrize("fixture", ["capsys", "capfd"])
+ @pytest.mark.parametrize("no_capture", [True, False])
+ def test_disabled_capture_fixture(
+ self, pytester: Pytester, fixture: str, no_capture: bool
+ ) -> None:
+ pytester.makepyfile(
+ """\
+ def test_disabled({fixture}):
+ print('captured before')
+ with {fixture}.disabled():
+ print('while capture is disabled')
+ print('captured after')
+ assert {fixture}.readouterr() == ('captured before\\ncaptured after\\n', '')
+
+ def test_normal():
+ print('test_normal executed')
+ """.format(
+ fixture=fixture
+ )
+ )
+ args = ("-s",) if no_capture else ()
+ result = pytester.runpytest_subprocess(*args)
+ result.stdout.fnmatch_lines(["*while capture is disabled*", "*= 2 passed in *"])
+ result.stdout.no_fnmatch_line("*captured before*")
+ result.stdout.no_fnmatch_line("*captured after*")
+ if no_capture:
+ assert "test_normal executed" in result.stdout.str()
+ else:
+ result.stdout.no_fnmatch_line("*test_normal executed*")
+
+ def test_disabled_capture_fixture_twice(self, pytester: Pytester) -> None:
+ """Test that an inner disabled() exit doesn't undo an outer disabled().
+
+ Issue #7148.
+ """
+ pytester.makepyfile(
+ """
+ def test_disabled(capfd):
+ print('captured before')
+ with capfd.disabled():
+ print('while capture is disabled 1')
+ with capfd.disabled():
+ print('while capture is disabled 2')
+ print('while capture is disabled 1 after')
+ print('captured after')
+ assert capfd.readouterr() == ('captured before\\ncaptured after\\n', '')
+ """
+ )
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(
+ [
+ "*while capture is disabled 1",
+ "*while capture is disabled 2",
+ "*while capture is disabled 1 after",
+ ],
+ consecutive=True,
+ )
+
+ @pytest.mark.parametrize("fixture", ["capsys", "capfd"])
+ def test_fixture_use_by_other_fixtures(self, pytester: Pytester, fixture) -> None:
+ """Ensure that capsys and capfd can be used by other fixtures during
+ setup and teardown."""
+ pytester.makepyfile(
+ """\
+ import sys
+ import pytest
+
+ @pytest.fixture
+ def captured_print({fixture}):
+ print('stdout contents begin')
+ print('stderr contents begin', file=sys.stderr)
+ out, err = {fixture}.readouterr()
+
+ yield out, err
+
+ print('stdout contents end')
+ print('stderr contents end', file=sys.stderr)
+ out, err = {fixture}.readouterr()
+ assert out == 'stdout contents end\\n'
+ assert err == 'stderr contents end\\n'
+
+ def test_captured_print(captured_print):
+ 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*"])
+ result.stdout.no_fnmatch_line("*stdout contents begin*")
+ result.stdout.no_fnmatch_line("*stderr contents begin*")
+
+ @pytest.mark.parametrize("cap", ["capsys", "capfd"])
+ def test_fixture_use_by_other_fixtures_teardown(
+ self, pytester: Pytester, cap
+ ) -> None:
+ """Ensure we can access setup and teardown buffers from teardown when using capsys/capfd (##3033)"""
+ pytester.makepyfile(
+ """\
+ import sys
+ import pytest
+ import os
+
+ @pytest.fixture()
+ def fix({cap}):
+ print("setup out")
+ sys.stderr.write("setup err\\n")
+ yield
+ out, err = {cap}.readouterr()
+ assert out == 'setup out\\ncall out\\n'
+ assert err == 'setup err\\ncall err\\n'
+
+ def test_a(fix):
+ print("call out")
+ sys.stderr.write("call err\\n")
+ """.format(
+ cap=cap
+ )
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+def test_setup_failure_does_not_kill_capturing(pytester: Pytester) -> None:
+ sub1 = pytester.mkpydir("sub1")
+ sub1.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ def pytest_runtest_setup(item):
+ raise ValueError(42)
+ """
+ )
+ )
+ sub1.joinpath("test_mod.py").write_text("def test_func1(): pass")
+ result = pytester.runpytest(pytester.path, "--traceconfig")
+ result.stdout.fnmatch_lines(["*ValueError(42)*", "*1 error*"])
+
+
+def test_capture_conftest_runtest_setup(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_runtest_setup():
+ print("hello19")
+ """
+ )
+ pytester.makepyfile("def test_func(): pass")
+ result = pytester.runpytest()
+ assert result.ret == 0
+ result.stdout.no_fnmatch_line("*hello19*")
+
+
+def test_capture_badoutput_issue412(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import os
+
+ def test_func():
+ omg = bytearray([1,129,1])
+ os.write(1, omg)
+ assert 0
+ """
+ )
+ result = pytester.runpytest("--capture=fd")
+ result.stdout.fnmatch_lines(
+ """
+ *def test_func*
+ *assert 0*
+ *Captured*
+ *1 failed*
+ """
+ )
+
+
+def test_capture_early_option_parsing(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_runtest_setup():
+ print("hello19")
+ """
+ )
+ pytester.makepyfile("def test_func(): pass")
+ result = pytester.runpytest("-vs")
+ assert result.ret == 0
+ assert "hello19" in result.stdout.str()
+
+
+def test_capture_binary_output(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ r"""
+ import pytest
+
+ def test_a():
+ import sys
+ import subprocess
+ subprocess.call([sys.executable, __file__])
+
+ def test_foo():
+ import os;os.write(1, b'\xc3')
+
+ if __name__ == '__main__':
+ test_foo()
+ """
+ )
+ result = pytester.runpytest("--assert=plain")
+ result.assert_outcomes(passed=2)
+
+
+def test_error_during_readouterr(pytester: Pytester) -> None:
+ """Make sure we suspend capturing if errors occur during readouterr"""
+ pytester.makepyfile(
+ pytest_xyz="""
+ from _pytest.capture import FDCapture
+
+ def bad_snap(self):
+ raise Exception('boom')
+
+ assert FDCapture.snap
+ FDCapture.snap = bad_snap
+ """
+ )
+ result = pytester.runpytest_subprocess("-p", "pytest_xyz", "--version")
+ result.stderr.fnmatch_lines(
+ ["*in bad_snap", " raise Exception('boom')", "Exception: boom"]
+ )
+
+
+class TestCaptureIO:
+ def test_text(self) -> None:
+ f = capture.CaptureIO()
+ f.write("hello")
+ s = f.getvalue()
+ assert s == "hello"
+ f.close()
+
+ def test_unicode_and_str_mixture(self) -> None:
+ f = capture.CaptureIO()
+ f.write("\u00f6")
+ pytest.raises(TypeError, f.write, b"hello")
+
+ def test_write_bytes_to_buffer(self) -> None:
+ """In python3, stdout / stderr are text io wrappers (exposing a buffer
+ property of the underlying bytestream). See issue #1407
+ """
+ f = capture.CaptureIO()
+ f.buffer.write(b"foo\r\n")
+ assert f.getvalue() == "foo\r\n"
+
+
+class TestTeeCaptureIO(TestCaptureIO):
+ def test_text(self) -> None:
+ sio = io.StringIO()
+ f = capture.TeeCaptureIO(sio)
+ f.write("hello")
+ s1 = f.getvalue()
+ assert s1 == "hello"
+ s2 = sio.getvalue()
+ assert s2 == s1
+ f.close()
+ sio.close()
+
+ def test_unicode_and_str_mixture(self) -> None:
+ sio = io.StringIO()
+ f = capture.TeeCaptureIO(sio)
+ f.write("\u00f6")
+ pytest.raises(TypeError, f.write, b"hello")
+
+
+def test_dontreadfrominput() -> None:
+ from _pytest.capture import DontReadFromInput
+
+ f = DontReadFromInput()
+ assert f.buffer is f
+ 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)
+ f.close() # just for completeness
+
+
+def test_captureresult() -> None:
+ cr = CaptureResult("out", "err")
+ assert len(cr) == 2
+ assert cr.out == "out"
+ assert cr.err == "err"
+ out, err = cr
+ assert out == "out"
+ assert err == "err"
+ assert cr[0] == "out"
+ assert cr[1] == "err"
+ assert cr == cr
+ assert cr == CaptureResult("out", "err")
+ assert cr != CaptureResult("wrong", "err")
+ assert cr == ("out", "err")
+ assert cr != ("out", "wrong")
+ assert hash(cr) == hash(CaptureResult("out", "err"))
+ assert hash(cr) == hash(("out", "err"))
+ assert hash(cr) != hash(("out", "wrong"))
+ assert cr < ("z",)
+ assert cr < ("z", "b")
+ assert cr < ("z", "b", "c")
+ assert cr.count("err") == 1
+ assert cr.count("wrong") == 0
+ assert cr.index("err") == 1
+ with pytest.raises(ValueError):
+ assert cr.index("wrong") == 0
+ assert next(iter(cr)) == "out"
+ assert cr._replace(err="replaced") == ("out", "replaced")
+
+
+@pytest.fixture
+def tmpfile(pytester: Pytester) -> Generator[BinaryIO, None, None]:
+ f = pytester.makepyfile("").open("wb+")
+ yield f
+ if not f.closed:
+ f.close()
+
+
+@contextlib.contextmanager
+def lsof_check():
+ pid = os.getpid()
+ try:
+ out = subprocess.check_output(("lsof", "-p", str(pid))).decode()
+ except (OSError, subprocess.CalledProcessError, UnicodeDecodeError) as exc:
+ # about UnicodeDecodeError, see note on pytester
+ pytest.skip(f"could not run 'lsof' ({exc!r})")
+ yield
+ out2 = subprocess.check_output(("lsof", "-p", str(pid))).decode()
+ 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:
+ def test_simple(self, tmpfile: BinaryIO) -> None:
+ fd = tmpfile.fileno()
+ cap = capture.FDCapture(fd)
+ data = b"hello"
+ os.write(fd, data)
+ pytest.raises(AssertionError, cap.snap)
+ cap.done()
+ cap = capture.FDCapture(fd)
+ cap.start()
+ os.write(fd, data)
+ s = cap.snap()
+ cap.done()
+ assert s == "hello"
+
+ def test_simple_many(self, tmpfile: BinaryIO) -> None:
+ for i in range(10):
+ self.test_simple(tmpfile)
+
+ def test_simple_many_check_open_files(self, pytester: Pytester) -> None:
+ with lsof_check():
+ with pytester.makepyfile("").open("wb+") as tmpfile:
+ self.test_simple_many(tmpfile)
+
+ def test_simple_fail_second_start(self, tmpfile: BinaryIO) -> None:
+ fd = tmpfile.fileno()
+ cap = capture.FDCapture(fd)
+ cap.done()
+ pytest.raises(AssertionError, cap.start)
+
+ def test_stderr(self) -> None:
+ cap = capture.FDCapture(2)
+ cap.start()
+ print("hello", file=sys.stderr)
+ s = cap.snap()
+ cap.done()
+ assert s == "hello\n"
+
+ def test_stdin(self) -> None:
+ cap = capture.FDCapture(0)
+ cap.start()
+ x = os.read(0, 100).strip()
+ cap.done()
+ assert x == b""
+
+ def test_writeorg(self, tmpfile: BinaryIO) -> None:
+ data1, data2 = b"foo", b"bar"
+ cap = capture.FDCapture(tmpfile.fileno())
+ cap.start()
+ tmpfile.write(data1)
+ tmpfile.flush()
+ cap.writeorg(data2.decode("ascii"))
+ scap = cap.snap()
+ cap.done()
+ assert scap == data1.decode("ascii")
+ with open(tmpfile.name, "rb") as stmp_file:
+ stmp = stmp_file.read()
+ assert stmp == data2
+
+ def test_simple_resume_suspend(self) -> None:
+ with saved_fd(1):
+ cap = capture.FDCapture(1)
+ cap.start()
+ data = b"hello"
+ os.write(1, data)
+ sys.stdout.write("whatever")
+ s = cap.snap()
+ assert s == "hellowhatever"
+ cap.suspend()
+ os.write(1, b"world")
+ sys.stdout.write("qlwkej")
+ assert not cap.snap()
+ cap.resume()
+ os.write(1, b"but now")
+ sys.stdout.write(" yes\n")
+ s = cap.snap()
+ assert s == "but now yes\n"
+ cap.suspend()
+ cap.done()
+ pytest.raises(AssertionError, cap.suspend)
+
+ assert repr(cap) == (
+ "<FDCapture 1 oldfd={} _state='done' tmpfile={!r}>".format(
+ cap.targetfd_save, cap.tmpfile
+ )
+ )
+ # Should not crash with missing "_old".
+ assert repr(cap.syscapture) == (
+ "<SysCapture stdout _old=<UNSET> _state='done' tmpfile={!r}>".format(
+ cap.syscapture.tmpfile
+ )
+ )
+
+ def test_capfd_sys_stdout_mode(self, capfd) -> None:
+ assert "b" not in sys.stdout.mode
+
+
+@contextlib.contextmanager
+def saved_fd(fd):
+ new_fd = os.dup(fd)
+ try:
+ yield
+ finally:
+ os.dup2(new_fd, fd)
+ os.close(new_fd)
+
+
+class TestStdCapture:
+ captureclass = staticmethod(StdCapture)
+
+ @contextlib.contextmanager
+ def getcapture(self, **kw):
+ cap = self.__class__.captureclass(**kw)
+ cap.start_capturing()
+ try:
+ yield cap
+ finally:
+ cap.stop_capturing()
+
+ def test_capturing_done_simple(self) -> None:
+ with self.getcapture() as cap:
+ sys.stdout.write("hello")
+ sys.stderr.write("world")
+ out, err = cap.readouterr()
+ assert out == "hello"
+ assert err == "world"
+
+ def test_capturing_reset_simple(self) -> None:
+ with self.getcapture() as cap:
+ print("hello world")
+ sys.stderr.write("hello error\n")
+ out, err = cap.readouterr()
+ assert out == "hello world\n"
+ assert err == "hello error\n"
+
+ def test_capturing_readouterr(self) -> None:
+ with self.getcapture() as cap:
+ 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")
+ out, err = cap.readouterr()
+ assert err == "error2"
+
+ def test_capture_results_accessible_by_attribute(self) -> None:
+ with self.getcapture() as cap:
+ sys.stdout.write("hello")
+ sys.stderr.write("world")
+ capture_result = cap.readouterr()
+ assert capture_result.out == "hello"
+ assert capture_result.err == "world"
+
+ def test_capturing_readouterr_unicode(self) -> None:
+ with self.getcapture() as cap:
+ print("hxąć")
+ out, err = cap.readouterr()
+ assert out == "hxąć\n"
+
+ def test_reset_twice_error(self) -> None:
+ with self.getcapture() as cap:
+ print("hello")
+ out, err = cap.readouterr()
+ pytest.raises(ValueError, cap.stop_capturing)
+ assert out == "hello\n"
+ assert not err
+
+ def test_capturing_modify_sysouterr_in_between(self) -> None:
+ oldout = sys.stdout
+ olderr = sys.stderr
+ with self.getcapture() as cap:
+ sys.stdout.write("hello")
+ sys.stderr.write("world")
+ sys.stdout = capture.CaptureIO()
+ sys.stderr = capture.CaptureIO()
+ print("not seen")
+ sys.stderr.write("not seen\n")
+ out, err = cap.readouterr()
+ assert out == "hello"
+ assert err == "world"
+ assert sys.stdout == oldout
+ assert sys.stderr == olderr
+
+ def test_capturing_error_recursive(self) -> None:
+ with self.getcapture() as cap1:
+ print("cap1")
+ with self.getcapture() as cap2:
+ print("cap2")
+ out2, err2 = cap2.readouterr()
+ out1, err1 = cap1.readouterr()
+ assert out1 == "cap1\n"
+ assert out2 == "cap2\n"
+
+ def test_just_out_capture(self) -> None:
+ with self.getcapture(out=True, err=False) as cap:
+ sys.stdout.write("hello")
+ sys.stderr.write("world")
+ out, err = cap.readouterr()
+ assert out == "hello"
+ assert not err
+
+ def test_just_err_capture(self) -> None:
+ with self.getcapture(out=False, err=True) as cap:
+ sys.stdout.write("hello")
+ sys.stderr.write("world")
+ out, err = cap.readouterr()
+ assert err == "world"
+ assert not out
+
+ def test_stdin_restored(self) -> None:
+ old = sys.stdin
+ with self.getcapture(in_=True):
+ newstdin = sys.stdin
+ assert newstdin != sys.stdin
+ assert sys.stdin is old
+
+ def test_stdin_nulled_by_default(self) -> None:
+ print("XXX this test may well hang instead of crashing")
+ print("XXX which indicates an error in the underlying capturing")
+ print("XXX mechanisms")
+ with self.getcapture():
+ pytest.raises(OSError, sys.stdin.read)
+
+
+class TestTeeStdCapture(TestStdCapture):
+ captureclass = staticmethod(TeeStdCapture)
+
+ 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:
+ print("cap2")
+ out2, err2 = cap2.readouterr()
+ out1, err1 = cap1.readouterr()
+ assert out1 == "cap1\ncap2\n"
+ assert out2 == "cap2\n"
+
+
+class TestStdCaptureFD(TestStdCapture):
+ captureclass = staticmethod(StdCaptureFD)
+
+ def test_simple_only_fd(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """\
+ import os
+ def test_x():
+ os.write(1, b"hello\\n")
+ assert 0
+ """
+ )
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(
+ """
+ *test_x*
+ *assert 0*
+ *Captured stdout*
+ """
+ )
+
+ def test_intermingling(self):
+ with self.getcapture() as cap:
+ os.write(1, b"1")
+ sys.stdout.write(str(2))
+ sys.stdout.flush()
+ os.write(1, b"3")
+ os.write(2, b"a")
+ sys.stderr.write("b")
+ sys.stderr.flush()
+ os.write(2, b"c")
+ out, err = cap.readouterr()
+ assert out == "123"
+ assert err == "abc"
+
+ def test_many(self, capfd):
+ with lsof_check():
+ for i in range(10):
+ cap = StdCaptureFD()
+ cap.start_capturing()
+ cap.stop_capturing()
+
+
+class TestStdCaptureFDinvalidFD:
+ def test_stdcapture_fd_invalid_fd(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import os
+ from fnmatch import fnmatch
+ from _pytest import capture
+
+ def StdCaptureFD(out=True, err=True, in_=True):
+ return capture.MultiCapture(
+ in_=capture.FDCapture(0) if in_ else None,
+ out=capture.FDCapture(1) if out else None,
+ err=capture.FDCapture(2) if err else None,
+ )
+
+ def test_stdout():
+ os.close(1)
+ cap = StdCaptureFD(out=True, err=False, in_=False)
+ assert fnmatch(repr(cap.out), "<FDCapture 1 oldfd=* _state='initialized' tmpfile=*>")
+ cap.start_capturing()
+ os.write(1, b"stdout")
+ assert cap.readouterr() == ("stdout", "")
+ cap.stop_capturing()
+
+ def test_stderr():
+ os.close(2)
+ cap = StdCaptureFD(out=False, err=True, in_=False)
+ assert fnmatch(repr(cap.err), "<FDCapture 2 oldfd=* _state='initialized' tmpfile=*>")
+ cap.start_capturing()
+ os.write(2, b"stderr")
+ assert cap.readouterr() == ("", "stderr")
+ cap.stop_capturing()
+
+ def test_stdin():
+ os.close(0)
+ cap = StdCaptureFD(out=False, err=False, in_=True)
+ assert fnmatch(repr(cap.in_), "<FDCapture 0 oldfd=* _state='initialized' tmpfile=*>")
+ cap.stop_capturing()
+ """
+ )
+ result = pytester.runpytest_subprocess("--capture=fd")
+ assert result.ret == 0
+ assert result.parseoutcomes()["passed"] == 3
+
+ def test_fdcapture_invalid_fd_with_fd_reuse(self, pytester: Pytester) -> None:
+ with saved_fd(1):
+ os.close(1)
+ cap = capture.FDCaptureBinary(1)
+ cap.start()
+ os.write(1, b"started")
+ cap.suspend()
+ os.write(1, b" suspended")
+ cap.resume()
+ os.write(1, b" resumed")
+ assert cap.snap() == b"started resumed"
+ cap.done()
+ with pytest.raises(OSError):
+ os.write(1, b"done")
+
+ def test_fdcapture_invalid_fd_without_fd_reuse(self, pytester: Pytester) -> None:
+ with saved_fd(1), saved_fd(2):
+ os.close(1)
+ os.close(2)
+ cap = capture.FDCaptureBinary(2)
+ cap.start()
+ os.write(2, b"started")
+ cap.suspend()
+ os.write(2, b" suspended")
+ cap.resume()
+ os.write(2, b" resumed")
+ assert cap.snap() == b"started resumed"
+ cap.done()
+ with pytest.raises(OSError):
+ os.write(2, b"done")
+
+
+def test_capture_not_started_but_reset() -> None:
+ capsys = StdCapture()
+ capsys.stop_capturing()
+
+
+def test_using_capsys_fixture_works_with_sys_stdout_encoding(
+ capsys: CaptureFixture[str],
+) -> None:
+ test_text = "test text"
+
+ print(test_text.encode(sys.stdout.encoding, "replace"))
+ (out, err) = capsys.readouterr()
+ assert out
+ assert err == ""
+
+
+def test_capsys_results_accessible_by_attribute(capsys: CaptureFixture[str]) -> None:
+ sys.stdout.write("spam")
+ sys.stderr.write("eggs")
+ capture_result = capsys.readouterr()
+ assert capture_result.out == "spam"
+ assert capture_result.err == "eggs"
+
+
+def test_fdcapture_tmpfile_remains_the_same() -> None:
+ cap = StdCaptureFD(out=False, err=True)
+ try:
+ cap.start_capturing()
+ capfile = cap.err.tmpfile
+ cap.readouterr()
+ finally:
+ cap.stop_capturing()
+ capfile2 = cap.err.tmpfile
+ assert capfile2 == capfile
+
+
+def test_close_and_capture_again(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import os
+ def test_close():
+ os.close(1)
+ def test_capture_again():
+ os.write(1, b"hello\\n")
+ assert 0
+ """
+ )
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(
+ """
+ *test_capture_again*
+ *assert 0*
+ *stdout*
+ *hello*
+ """
+ )
+
+
+@pytest.mark.parametrize(
+ "method", ["SysCapture(2)", "SysCapture(2, tee=True)", "FDCapture(2)"]
+)
+def test_capturing_and_logging_fundamentals(pytester: Pytester, method: str) -> None:
+ # here we check a fundamental feature
+ p = pytester.makepyfile(
+ """
+ import sys, os, logging
+ from _pytest import capture
+ cap = capture.MultiCapture(
+ in_=None,
+ out=None,
+ err=capture.%s,
+ )
+ cap.start_capturing()
+
+ logging.warning("hello1")
+ outerr = cap.readouterr()
+ print("suspend, captured %%s" %%(outerr,))
+ logging.warning("hello2")
+
+ cap.pop_outerr_to_orig()
+ logging.warning("hello3")
+
+ outerr = cap.readouterr()
+ print("suspend2, captured %%s" %% (outerr,))
+ """
+ % (method,)
+ )
+ result = pytester.runpython(p)
+ result.stdout.fnmatch_lines(
+ """
+ suspend, captured*hello1*
+ suspend2, captured*WARNING:root:hello3*
+ """
+ )
+ result.stderr.fnmatch_lines(
+ """
+ WARNING:root:hello2
+ """
+ )
+ assert "atexit" not in result.stderr.str()
+
+
+def test_error_attribute_issue555(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import sys
+ def test_capattr():
+ assert sys.stdout.errors == "replace"
+ assert sys.stderr.errors == "replace"
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+@pytest.mark.skipif(
+ not sys.platform.startswith("win"),
+ reason="only on windows",
+)
+def test_py36_windowsconsoleio_workaround_non_standard_streams() -> None:
+ """
+ Ensure _py36_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
+
+ class DummyStream:
+ def write(self, s):
+ pass
+
+ stream = cast(TextIO, DummyStream())
+ _py36_windowsconsoleio_workaround(stream)
+
+
+def test_dontreadfrominput_has_encoding(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import sys
+ def test_capattr():
+ # should not raise AttributeError
+ assert sys.stdout.encoding
+ assert sys.stderr.encoding
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+def test_crash_on_closing_tmpfile_py27(
+ pytester: Pytester, monkeypatch: MonkeyPatch
+) -> None:
+ p = pytester.makepyfile(
+ """
+ import threading
+ import sys
+
+ printing = threading.Event()
+
+ def spam():
+ f = sys.stderr
+ print('SPAMBEFORE', end='', file=f)
+ printing.set()
+
+ while True:
+ try:
+ f.flush()
+ except (OSError, ValueError):
+ break
+
+ def test_spam_in_thread():
+ t = threading.Thread(target=spam)
+ t.daemon = True
+ t.start()
+
+ printing.wait()
+ """
+ )
+ # Do not consider plugins like hypothesis, which might output to stderr.
+ monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
+ result = pytester.runpytest_subprocess(str(p))
+ assert result.ret == 0
+ assert result.stderr.str() == ""
+ result.stdout.no_fnmatch_line("*OSError*")
+
+
+def test_global_capture_with_live_logging(pytester: Pytester) -> None:
+ # Issue 3819
+ # capture should work with live cli logging
+
+ # Teardown report seems to have the capture for the whole process (setup, capture, teardown)
+ pytester.makeconftest(
+ """
+ def pytest_runtest_logreport(report):
+ if "test_global" in report.nodeid:
+ if report.when == "teardown":
+ with open("caplog", "w") as f:
+ f.write(report.caplog)
+ with open("capstdout", "w") as f:
+ f.write(report.capstdout)
+ """
+ )
+
+ pytester.makepyfile(
+ """
+ import logging
+ import sys
+ import pytest
+
+ logger = logging.getLogger(__name__)
+
+ @pytest.fixture
+ def fix1():
+ print("fix setup")
+ logging.info("fix setup")
+ yield
+ logging.info("fix teardown")
+ print("fix teardown")
+
+ def test_global(fix1):
+ print("begin test")
+ logging.info("something in test")
+ print("end test")
+ """
+ )
+ result = pytester.runpytest_subprocess("--log-cli-level=INFO")
+ assert result.ret == 0
+
+ with open("caplog") 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:
+ capstdout = f.read()
+
+ assert "fix setup" in capstdout
+ assert "begin test" in capstdout
+ assert "end test" in capstdout
+ assert "fix teardown" in capstdout
+
+
+@pytest.mark.parametrize("capture_fixture", ["capsys", "capfd"])
+def test_capture_with_live_logging(
+ pytester: Pytester, capture_fixture: CaptureFixture[str]
+) -> None:
+ # Issue 3819
+ # capture should work with live cli logging
+
+ pytester.makepyfile(
+ """
+ import logging
+ import sys
+
+ logger = logging.getLogger(__name__)
+
+ def test_capture({0}):
+ print("hello")
+ sys.stderr.write("world\\n")
+ captured = {0}.readouterr()
+ assert captured.out == "hello\\n"
+ assert captured.err == "world\\n"
+
+ logging.info("something")
+ print("next")
+ logging.info("something")
+
+ captured = {0}.readouterr()
+ assert captured.out == "next\\n"
+ """.format(
+ capture_fixture
+ )
+ )
+
+ result = pytester.runpytest_subprocess("--log-cli-level=INFO")
+ assert result.ret == 0
+
+
+def test_typeerror_encodedfile_write(pytester: Pytester) -> None:
+ """It should behave the same with and without output capturing (#4861)."""
+ p = pytester.makepyfile(
+ """
+ def test_fails():
+ import sys
+ sys.stdout.write(b"foo")
+ """
+ )
+ result_without_capture = pytester.runpytest("-s", str(p))
+ result_with_capture = pytester.runpytest(str(p))
+
+ assert result_with_capture.ret == result_without_capture.ret
+ out = result_with_capture.stdout.str()
+ assert ("TypeError: write() argument must be str, not bytes" in out) or (
+ "TypeError: unicode argument expected, got 'bytes'" in out
+ )
+
+
+def test_stderr_write_returns_len(capsys: CaptureFixture[str]) -> None:
+ """Write on Encoded files, namely captured stderr, should return number of characters written."""
+ assert sys.stderr.write("Foo") == 3
+
+
+def test_encodedfile_writelines(tmpfile: BinaryIO) -> None:
+ ef = capture.EncodedFile(tmpfile, encoding="utf-8")
+ with pytest.raises(TypeError):
+ ef.writelines([b"line1", b"line2"]) # type: ignore[list-item]
+ assert ef.writelines(["line3", "line4"]) is None # type: ignore[func-returns-value]
+ ef.flush()
+ tmpfile.seek(0)
+ assert tmpfile.read() == b"line3line4"
+ tmpfile.close()
+ with pytest.raises(ValueError):
+ ef.read()
+
+
+def test__get_multicapture() -> None:
+ assert isinstance(_get_multicapture("no"), MultiCapture)
+ pytest.raises(ValueError, _get_multicapture, "unknown").match(
+ r"^unknown capturing method: 'unknown'"
+ )
+
+
+def test_logging_while_collecting(pytester: Pytester) -> None:
+ """Issue #6240: Calls to logging.xxx() during collection causes all logging calls to be duplicated to stderr"""
+ p = pytester.makepyfile(
+ """\
+ import logging
+
+ logging.warning("during collection")
+
+ def test_logging():
+ logging.warning("during call")
+ assert False
+ """
+ )
+ result = pytester.runpytest_subprocess(p)
+ assert result.ret == ExitCode.TESTS_FAILED
+ result.stdout.fnmatch_lines(
+ [
+ "*test_*.py F*",
+ "====* FAILURES *====",
+ "____*____",
+ "*--- Captured log call*",
+ "WARNING * during call",
+ "*1 failed*",
+ ]
+ )
+ result.stdout.no_fnmatch_line("*Captured stderr call*")
+ result.stdout.no_fnmatch_line("*during collection*")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_collection.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_collection.py
new file mode 100644
index 0000000000..6a8a5c1cef
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_collection.py
@@ -0,0 +1,1506 @@
+import os
+import pprint
+import shutil
+import sys
+import textwrap
+from pathlib import Path
+from typing import List
+
+import pytest
+from _pytest.config import ExitCode
+from _pytest.fixtures import FixtureRequest
+from _pytest.main import _in_venv
+from _pytest.main import Session
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.nodes import Item
+from _pytest.pathlib import symlink_or_skip
+from _pytest.pytester import HookRecorder
+from _pytest.pytester import Pytester
+
+
+def ensure_file(file_path: Path) -> Path:
+ """Ensure that file exists"""
+ file_path.parent.mkdir(parents=True, exist_ok=True)
+ file_path.touch(exist_ok=True)
+ return file_path
+
+
+class TestCollector:
+ def test_collect_versus_item(self) -> None:
+ from pytest import Collector
+ from pytest import Item
+
+ assert not issubclass(Collector, Item)
+ assert not issubclass(Item, Collector)
+
+ def test_check_equality(self, pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol(
+ """
+ def test_pass(): pass
+ def test_fail(): assert 0
+ """
+ )
+ fn1 = pytester.collect_by_name(modcol, "test_pass")
+ assert isinstance(fn1, pytest.Function)
+ fn2 = pytester.collect_by_name(modcol, "test_pass")
+ assert isinstance(fn2, pytest.Function)
+
+ assert fn1 == fn2
+ assert fn1 != modcol
+ assert hash(fn1) == hash(fn2)
+
+ fn3 = pytester.collect_by_name(modcol, "test_fail")
+ assert isinstance(fn3, pytest.Function)
+ assert not (fn1 == fn3)
+ assert fn1 != fn3
+
+ for fn in fn1, fn2, fn3:
+ assert isinstance(fn, pytest.Function)
+ assert fn != 3 # type: ignore[comparison-overlap]
+ assert fn != modcol
+ assert fn != [1, 2, 3] # type: ignore[comparison-overlap]
+ assert [1, 2, 3] != fn # type: ignore[comparison-overlap]
+ assert modcol != fn
+
+ assert pytester.collect_by_name(modcol, "doesnotexist") is None
+
+ def test_getparent_and_accessors(self, pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol(
+ """
+ class TestClass:
+ def test_foo(self):
+ pass
+ """
+ )
+ cls = pytester.collect_by_name(modcol, "TestClass")
+ assert isinstance(cls, pytest.Class)
+ fn = pytester.collect_by_name(cls, "test_foo")
+ assert isinstance(fn, pytest.Function)
+
+ assert fn.getparent(pytest.Module) is modcol
+ assert modcol.module is not None
+ assert modcol.cls is None
+ assert modcol.instance is None
+
+ assert fn.getparent(pytest.Class) is cls
+ assert cls.module is not None
+ assert cls.cls is not None
+ assert cls.instance is None
+
+ assert fn.getparent(pytest.Function) is fn
+ assert fn.module is not None
+ assert fn.cls is not None
+ assert fn.instance is not None
+ assert fn.function is not None
+
+ def test_getcustomfile_roundtrip(self, pytester: Pytester) -> None:
+ hello = pytester.makefile(".xxx", hello="world")
+ pytester.makepyfile(
+ conftest="""
+ import pytest
+ class CustomFile(pytest.File):
+ pass
+ def pytest_collect_file(file_path, parent):
+ if file_path.suffix == ".xxx":
+ return CustomFile.from_parent(path=file_path, parent=parent)
+ """
+ )
+ node = pytester.getpathnode(hello)
+ assert isinstance(node, pytest.File)
+ assert node.name == "hello.xxx"
+ nodes = node.session.perform_collect([node.nodeid], genitems=False)
+ assert len(nodes) == 1
+ assert isinstance(nodes[0], pytest.File)
+
+ def test_can_skip_class_with_test_attr(self, pytester: Pytester) -> None:
+ """Assure test class is skipped when using `__test__=False` (See #2007)."""
+ pytester.makepyfile(
+ """
+ class TestFoo(object):
+ __test__ = False
+ def __init__(self):
+ pass
+ def test_foo():
+ assert True
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["collected 0 items", "*no tests ran in*"])
+
+
+class TestCollectFS:
+ def test_ignored_certain_directories(self, pytester: Pytester) -> None:
+ tmp_path = pytester.path
+ ensure_file(tmp_path / "build" / "test_notfound.py")
+ ensure_file(tmp_path / "dist" / "test_notfound.py")
+ ensure_file(tmp_path / "_darcs" / "test_notfound.py")
+ ensure_file(tmp_path / "CVS" / "test_notfound.py")
+ ensure_file(tmp_path / "{arch}" / "test_notfound.py")
+ ensure_file(tmp_path / ".whatever" / "test_notfound.py")
+ 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")
+
+ result = pytester.runpytest("--collect-only")
+ s = result.stdout.str()
+ assert "test_notfound" not in s
+ assert "test_found" in s
+
+ @pytest.mark.parametrize(
+ "fname",
+ (
+ "activate",
+ "activate.csh",
+ "activate.fish",
+ "Activate",
+ "Activate.bat",
+ "Activate.ps1",
+ ),
+ )
+ def test_ignored_virtualenvs(self, pytester: Pytester, fname: str) -> None:
+ 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")
+
+ # by default, ignore tests inside a virtualenv
+ result = pytester.runpytest()
+ result.stdout.no_fnmatch_line("*test_invenv*")
+ # allow test collection if user insists
+ result = pytester.runpytest("--collect-in-virtualenv")
+ assert "test_invenv" in result.stdout.str()
+ # allow test collection if user directly passes in the directory
+ result = pytester.runpytest("virtual")
+ assert "test_invenv" in result.stdout.str()
+
+ @pytest.mark.parametrize(
+ "fname",
+ (
+ "activate",
+ "activate.csh",
+ "activate.fish",
+ "Activate",
+ "Activate.bat",
+ "Activate.ps1",
+ ),
+ )
+ def test_ignored_virtualenvs_norecursedirs_precedence(
+ self, pytester: Pytester, fname: str
+ ) -> None:
+ bindir = "Scripts" if sys.platform.startswith("win") else "bin"
+ # 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")
+ result = pytester.runpytest("--collect-in-virtualenv")
+ result.stdout.no_fnmatch_line("*test_invenv*")
+ # ...unless the virtualenv is explicitly given on the CLI
+ result = pytester.runpytest("--collect-in-virtualenv", ".virtual")
+ assert "test_invenv" in result.stdout.str()
+
+ @pytest.mark.parametrize(
+ "fname",
+ (
+ "activate",
+ "activate.csh",
+ "activate.fish",
+ "Activate",
+ "Activate.bat",
+ "Activate.ps1",
+ ),
+ )
+ def test__in_venv(self, pytester: Pytester, fname: str) -> None:
+ """Directly test the virtual env detection function"""
+ bindir = "Scripts" if sys.platform.startswith("win") else "bin"
+ # no bin/activate, not a virtualenv
+ base_path = pytester.mkdir("venv")
+ assert _in_venv(base_path) is False
+ # with bin/activate, totally a virtualenv
+ bin_path = base_path.joinpath(bindir)
+ bin_path.mkdir()
+ bin_path.joinpath(fname).touch()
+ assert _in_venv(base_path) is True
+
+ def test_custom_norecursedirs(self, pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ norecursedirs = mydir xyz*
+ """
+ )
+ tmp_path = pytester.path
+ ensure_file(tmp_path / "mydir" / "test_hello.py").write_text(
+ "def test_1(): pass"
+ )
+ 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")
+ rec.assertoutcome(failed=1)
+
+ def test_testpaths_ini(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ testpaths = gui uts
+ """
+ )
+ 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")
+
+ # 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"]
+
+ # check that explicitly passing directories in the command-line
+ # collects the tests
+ for dirname in ("env", "gui", "uts"):
+ 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"):
+ monkeypatch.chdir(pytester.path.joinpath(dirname))
+ items, reprec = pytester.inline_genitems()
+ assert [x.name for x in items] == ["test_%s" % dirname]
+
+
+class TestCollectPluginHookRelay:
+ def test_pytest_collect_file(self, pytester: Pytester) -> None:
+ wascalled = []
+
+ class Plugin:
+ def pytest_collect_file(self, file_path: Path) -> None:
+ if not file_path.name.startswith("."):
+ # Ignore hidden files, e.g. .testmondata.
+ wascalled.append(file_path)
+
+ pytester.makefile(".abc", "xyz")
+ pytest.main(pytester.path, plugins=[Plugin()])
+ assert len(wascalled) == 1
+ assert wascalled[0].suffix == ".abc"
+
+
+class TestPrunetraceback:
+ def test_custom_repr_failure(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import not_exists
+ """
+ )
+ pytester.makeconftest(
+ """
+ import pytest
+ def pytest_collect_file(file_path, parent):
+ return MyFile.from_parent(path=file_path, parent=parent)
+ class MyError(Exception):
+ pass
+ class MyFile(pytest.File):
+ def collect(self):
+ raise MyError()
+ def repr_failure(self, excinfo):
+ if isinstance(excinfo.value, MyError):
+ return "hello world"
+ return pytest.File.repr_failure(self, excinfo)
+ """
+ )
+
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(["*ERROR collecting*", "*hello world*"])
+
+ @pytest.mark.xfail(reason="other mechanism for adding to reporting needed")
+ def test_collect_report_postprocessing(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import not_exists
+ """
+ )
+ pytester.makeconftest(
+ """
+ import pytest
+ @pytest.hookimpl(hookwrapper=True)
+ def pytest_make_collect_report():
+ outcome = yield
+ rep = outcome.get_result()
+ rep.headerlines += ["header1"]
+ outcome.force_result(rep)
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(["*ERROR collecting*", "*header1*"])
+
+
+class TestCustomConftests:
+ def test_ignore_collect_path(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_ignore_collect(collection_path, config):
+ return collection_path.name.startswith("x") or collection_path.name == "test_one.py"
+ """
+ )
+ sub = pytester.mkdir("xy123")
+ ensure_file(sub / "test_hello.py").write_text("syntax error")
+ sub.joinpath("conftest.py").write_text("syntax error")
+ pytester.makepyfile("def test_hello(): pass")
+ pytester.makepyfile(test_one="syntax error")
+ result = pytester.runpytest("--fulltrace")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_ignore_collect_not_called_on_argument(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_ignore_collect(collection_path, config):
+ return True
+ """
+ )
+ p = pytester.makepyfile("def test_hello(): pass")
+ result = pytester.runpytest(p)
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*1 passed*"])
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+ result.stdout.fnmatch_lines(["*collected 0 items*"])
+
+ def test_collectignore_exclude_on_option(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ from pathlib import Path
+
+ class MyPathLike:
+ def __init__(self, path):
+ self.path = path
+ def __fspath__(self):
+ return "path"
+
+ collect_ignore = [MyPathLike('hello'), 'test_world.py', Path('bye')]
+
+ def pytest_addoption(parser):
+ parser.addoption("--XX", action="store_true", default=False)
+
+ def pytest_configure(config):
+ if config.getvalue("XX"):
+ collect_ignore[:] = []
+ """
+ )
+ pytester.mkdir("hello")
+ pytester.makepyfile(test_world="def test_hello(): pass")
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+ result.stdout.no_fnmatch_line("*passed*")
+ result = pytester.runpytest("--XX")
+ assert result.ret == 0
+ assert "passed" in result.stdout.str()
+
+ def test_collectignoreglob_exclude_on_option(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ collect_ignore_glob = ['*w*l[dt]*']
+ def pytest_addoption(parser):
+ parser.addoption("--XX", action="store_true", default=False)
+ def pytest_configure(config):
+ if config.getvalue("XX"):
+ collect_ignore_glob[:] = []
+ """
+ )
+ pytester.makepyfile(test_world="def test_hello(): pass")
+ pytester.makepyfile(test_welt="def test_hallo(): pass")
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+ result.stdout.fnmatch_lines(["*collected 0 items*"])
+ result = pytester.runpytest("--XX")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+ def test_pytest_fs_collect_hooks_are_seen(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ class MyModule(pytest.Module):
+ pass
+ def pytest_collect_file(file_path, parent):
+ if file_path.suffix == ".py":
+ return MyModule.from_parent(path=file_path, parent=parent)
+ """
+ )
+ pytester.mkdir("sub")
+ pytester.makepyfile("def test_x(): pass")
+ result = pytester.runpytest("--co")
+ result.stdout.fnmatch_lines(["*MyModule*", "*test_x*"])
+
+ def test_pytest_collect_file_from_sister_dir(self, pytester: Pytester) -> None:
+ sub1 = pytester.mkpydir("sub1")
+ sub2 = pytester.mkpydir("sub2")
+ conf1 = pytester.makeconftest(
+ """
+ import pytest
+ class MyModule1(pytest.Module):
+ pass
+ def pytest_collect_file(file_path, parent):
+ if file_path.suffix == ".py":
+ return MyModule1.from_parent(path=file_path, parent=parent)
+ """
+ )
+ conf1.replace(sub1.joinpath(conf1.name))
+ conf2 = pytester.makeconftest(
+ """
+ import pytest
+ class MyModule2(pytest.Module):
+ pass
+ def pytest_collect_file(file_path, parent):
+ if file_path.suffix == ".py":
+ return MyModule2.from_parent(path=file_path, parent=parent)
+ """
+ )
+ conf2.replace(sub2.joinpath(conf2.name))
+ p = pytester.makepyfile("def test_x(): pass")
+ shutil.copy(p, sub1.joinpath(p.name))
+ shutil.copy(p, sub2.joinpath(p.name))
+ result = pytester.runpytest("--co")
+ result.stdout.fnmatch_lines(["*MyModule1*", "*MyModule2*", "*test_x*"])
+
+
+class TestSession:
+ def test_collect_topdir(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile("def test_func(): pass")
+ id = "::".join([p.name, "test_func"])
+ # XXX migrate to collectonly? (see below)
+ config = pytester.parseconfig(id)
+ topdir = pytester.path
+ rcol = Session.from_config(config)
+ assert topdir == rcol.path
+ # rootid = rcol.nodeid
+ # root2 = rcol.perform_collect([rcol.nodeid], genitems=False)[0]
+ # assert root2 == rcol, rootid
+ colitems = rcol.perform_collect([rcol.nodeid], genitems=False)
+ assert len(colitems) == 1
+ assert colitems[0].path == p
+
+ def get_reported_items(self, hookrec: HookRecorder) -> List[Item]:
+ """Return pytest.Item instances reported by the pytest_collectreport hook"""
+ calls = hookrec.getcalls("pytest_collectreport")
+ return [
+ x
+ for call in calls
+ for x in call.report.result
+ if isinstance(x, pytest.Item)
+ ]
+
+ def test_collect_protocol_single_function(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile("def test_func(): pass")
+ id = "::".join([p.name, "test_func"])
+ items, hookrec = pytester.inline_genitems(id)
+ (item,) = items
+ assert item.name == "test_func"
+ newid = item.nodeid
+ assert newid == id
+ pprint.pprint(hookrec.calls)
+ topdir = pytester.path # noqa
+ hookrec.assert_contains(
+ [
+ ("pytest_collectstart", "collector.path == topdir"),
+ ("pytest_make_collect_report", "collector.path == topdir"),
+ ("pytest_collectstart", "collector.path == p"),
+ ("pytest_make_collect_report", "collector.path == p"),
+ ("pytest_pycollect_makeitem", "name == 'test_func'"),
+ ("pytest_collectreport", "report.result[0].name == 'test_func'"),
+ ]
+ )
+ # ensure we are reporting the collection of the single test item (#2464)
+ assert [x.name for x in self.get_reported_items(hookrec)] == ["test_func"]
+
+ def test_collect_protocol_method(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ class TestClass(object):
+ def test_method(self):
+ pass
+ """
+ )
+ normid = p.name + "::TestClass::test_method"
+ for id in [p.name, p.name + "::TestClass", normid]:
+ items, hookrec = pytester.inline_genitems(id)
+ assert len(items) == 1
+ assert items[0].name == "test_method"
+ newid = items[0].nodeid
+ assert newid == normid
+ # 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_custom_nodes_multi_id(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile("def test_func(): pass")
+ pytester.makeconftest(
+ """
+ import pytest
+ class SpecialItem(pytest.Item):
+ def runtest(self):
+ return # ok
+ class SpecialFile(pytest.File):
+ def collect(self):
+ return [SpecialItem.from_parent(name="check", parent=self)]
+ def pytest_collect_file(file_path, parent):
+ if file_path.name == %r:
+ return SpecialFile.from_parent(path=file_path, parent=parent)
+ """
+ % p.name
+ )
+ id = p.name
+
+ items, hookrec = pytester.inline_genitems(id)
+ pprint.pprint(hookrec.calls)
+ assert len(items) == 2
+ hookrec.assert_contains(
+ [
+ ("pytest_collectstart", "collector.path == collector.session.path"),
+ (
+ "pytest_collectstart",
+ "collector.__class__.__name__ == 'SpecialFile'",
+ ),
+ ("pytest_collectstart", "collector.__class__.__name__ == 'Module'"),
+ ("pytest_pycollect_makeitem", "name == 'test_func'"),
+ ("pytest_collectreport", "report.nodeid.startswith(p.name)"),
+ ]
+ )
+ assert len(self.get_reported_items(hookrec)) == 2
+
+ def test_collect_subdir_event_ordering(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile("def test_func(): pass")
+ aaa = pytester.mkpydir("aaa")
+ test_aaa = aaa.joinpath("test_aaa.py")
+ p.replace(test_aaa)
+
+ items, hookrec = pytester.inline_genitems()
+ assert len(items) == 1
+ pprint.pprint(hookrec.calls)
+ hookrec.assert_contains(
+ [
+ ("pytest_collectstart", "collector.path == test_aaa"),
+ ("pytest_pycollect_makeitem", "name == 'test_func'"),
+ ("pytest_collectreport", "report.nodeid.startswith('aaa/test_aaa.py')"),
+ ]
+ )
+
+ def test_collect_two_commandline_args(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile("def test_func(): pass")
+ aaa = pytester.mkpydir("aaa")
+ bbb = pytester.mkpydir("bbb")
+ test_aaa = aaa.joinpath("test_aaa.py")
+ shutil.copy(p, test_aaa)
+ test_bbb = bbb.joinpath("test_bbb.py")
+ p.replace(test_bbb)
+
+ id = "."
+
+ items, hookrec = pytester.inline_genitems(id)
+ assert len(items) == 2
+ pprint.pprint(hookrec.calls)
+ hookrec.assert_contains(
+ [
+ ("pytest_collectstart", "collector.path == test_aaa"),
+ ("pytest_pycollect_makeitem", "name == 'test_func'"),
+ ("pytest_collectreport", "report.nodeid == 'aaa/test_aaa.py'"),
+ ("pytest_collectstart", "collector.path == test_bbb"),
+ ("pytest_pycollect_makeitem", "name == 'test_func'"),
+ ("pytest_collectreport", "report.nodeid == 'bbb/test_bbb.py'"),
+ ]
+ )
+
+ def test_serialization_byid(self, pytester: Pytester) -> None:
+ pytester.makepyfile("def test_func(): pass")
+ items, hookrec = pytester.inline_genitems()
+ assert len(items) == 1
+ (item,) = items
+ items2, hookrec = pytester.inline_genitems(item.nodeid)
+ (item2,) = items2
+ assert item2.name == item.name
+ assert item2.path == item.path
+
+ def test_find_byid_without_instance_parents(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ class TestClass(object):
+ def test_method(self):
+ pass
+ """
+ )
+ arg = p.name + "::TestClass::test_method"
+ items, hookrec = pytester.inline_genitems(arg)
+ assert len(items) == 1
+ (item,) = items
+ assert item.nodeid.endswith("TestClass::test_method")
+ # 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"]
+
+
+class Test_getinitialnodes:
+ def test_global_file(self, pytester: Pytester) -> None:
+ tmp_path = pytester.path
+ x = ensure_file(tmp_path / "x.py")
+ config = pytester.parseconfigure(x)
+ col = pytester.getnode(config, x)
+ assert isinstance(col, pytest.Module)
+ assert col.name == "x.py"
+ assert col.parent is not None
+ assert col.parent.parent is None
+ for parent in col.listchain():
+ assert parent.config is config
+
+ def test_pkgfile(self, pytester: Pytester) -> 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.
+ """
+ tmp_path = pytester.path
+ subdir = tmp_path.joinpath("subdir")
+ x = ensure_file(subdir / "x.py")
+ ensure_file(subdir / "__init__.py")
+ with subdir.cwd():
+ config = pytester.parseconfigure(x)
+ col = pytester.getnode(config, x)
+ assert col is not None
+ assert col.name == "x.py"
+ assert isinstance(col, pytest.Module)
+ assert isinstance(col.parent, pytest.Package)
+ assert isinstance(col.parent.parent, pytest.Session)
+ # session is batman (has no parents)
+ assert col.parent.parent.parent is None
+ for parent in col.listchain():
+ assert parent.config is config
+
+
+class Test_genitems:
+ def test_check_collect_hashes(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def test_1():
+ pass
+
+ def test_2():
+ pass
+ """
+ )
+ shutil.copy(p, p.parent / (p.stem + "2" + ".py"))
+ items, reprec = pytester.inline_genitems(p.parent)
+ assert len(items) == 4
+ for numi, i in enumerate(items):
+ for numj, j in enumerate(items):
+ if numj != numi:
+ assert hash(i) != hash(j)
+ assert i != j
+
+ def test_example_items1(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ def testone():
+ pass
+
+ class TestX(object):
+ def testmethod_one(self):
+ pass
+
+ class TestY(TestX):
+ @pytest.mark.parametrize("arg0", [".["])
+ def testmethod_two(self, arg0):
+ pass
+ """
+ )
+ items, reprec = pytester.inline_genitems(p)
+ assert len(items) == 4
+ assert items[0].name == "testone"
+ assert items[1].name == "testmethod_one"
+ assert items[2].name == "testmethod_one"
+ assert items[3].name == "testmethod_two[.[]"
+
+ # let's also test getmodpath here
+ assert items[0].getmodpath() == "testone" # type: ignore[attr-defined]
+ assert items[1].getmodpath() == "TestX.testmethod_one" # type: ignore[attr-defined]
+ assert items[2].getmodpath() == "TestY.testmethod_one" # type: ignore[attr-defined]
+ # PR #6202: Fix incorrect result of getmodpath method. (Resolves issue #6189)
+ assert items[3].getmodpath() == "TestY.testmethod_two[.[]" # type: ignore[attr-defined]
+
+ s = items[0].getmodpath(stopatmodule=False) # type: ignore[attr-defined]
+ assert s.endswith("test_example_items1.testone")
+ print(s)
+
+ 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)."""
+ pytester.makeini(
+ """
+ [pytest]
+ python_classes = *Suite Test
+ python_functions = *_test test
+ """
+ )
+ p = pytester.makepyfile(
+ """
+ class MyTestSuite(object):
+ def x_test(self):
+ pass
+
+ class TestCase(object):
+ def test_y(self):
+ pass
+ """
+ )
+ items, reprec = pytester.inline_genitems(p)
+ ids = [x.getmodpath() for x in items] # type: ignore[attr-defined]
+ assert ids == ["MyTestSuite.x_test", "TestCase.test_y"]
+
+
+def test_matchnodes_two_collections_same_file(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ def pytest_configure(config):
+ config.pluginmanager.register(Plugin2())
+
+ class Plugin2(object):
+ def pytest_collect_file(self, file_path, parent):
+ if file_path.suffix == ".abc":
+ return MyFile2.from_parent(path=file_path, parent=parent)
+
+ def pytest_collect_file(file_path, parent):
+ if file_path.suffix == ".abc":
+ return MyFile1.from_parent(path=file_path, parent=parent)
+
+ class MyFile1(pytest.File):
+ def collect(self):
+ yield Item1.from_parent(name="item1", parent=self)
+
+ class MyFile2(pytest.File):
+ def collect(self):
+ yield Item2.from_parent(name="item2", parent=self)
+
+ class Item1(pytest.Item):
+ def runtest(self):
+ pass
+
+ class Item2(pytest.Item):
+ def runtest(self):
+ pass
+ """
+ )
+ p = pytester.makefile(".abc", "")
+ result = pytester.runpytest()
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*2 passed*"])
+ res = pytester.runpytest("%s::item2" % p.name)
+ res.stdout.fnmatch_lines(["*1 passed*"])
+
+
+class TestNodeKeywords:
+ def test_no_under(self, pytester: Pytester) -> None:
+ modcol = pytester.getmodulecol(
+ """
+ def test_pass(): pass
+ def test_fail(): assert 0
+ """
+ )
+ values = list(modcol.keywords)
+ assert modcol.name in values
+ for x in values:
+ assert not x.startswith("_")
+ assert modcol.name in repr(modcol.keywords)
+
+ def test_issue345(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_should_not_be_selected():
+ assert False, 'I should not have been selected to run'
+
+ def test___repr__():
+ pass
+ """
+ )
+ reprec = pytester.inline_run("-k repr")
+ reprec.assertoutcome(passed=1, failed=0)
+
+ def test_keyword_matching_is_case_insensitive_by_default(
+ self, pytester: Pytester
+ ) -> None:
+ """Check that selection via -k EXPRESSION is case-insensitive.
+
+ Since markers are also added to the node keywords, they too can
+ be matched without having to think about case sensitivity.
+
+ """
+ pytester.makepyfile(
+ """
+ import pytest
+
+ def test_sPeCiFiCToPiC_1():
+ assert True
+
+ class TestSpecificTopic_2:
+ def test(self):
+ assert True
+
+ @pytest.mark.sPeCiFiCToPic_3
+ def test():
+ assert True
+
+ @pytest.mark.sPeCiFiCToPic_4
+ class Test:
+ def test(self):
+ assert True
+
+ def test_failing_5():
+ assert False, "This should not match"
+
+ """
+ )
+ num_matching_tests = 4
+ for expression in ("specifictopic", "SPECIFICTOPIC", "SpecificTopic"):
+ reprec = pytester.inline_run("-k " + expression)
+ reprec.assertoutcome(passed=num_matching_tests, failed=0)
+
+ def test_duplicates_handled_correctly(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ pytestmark = pytest.mark.kw
+ class TestClass:
+ pytestmark = pytest.mark.kw
+ def test_method(self): pass
+ test_method.kw = 'method'
+ """,
+ "test_method",
+ )
+ assert item.parent is not None and item.parent.parent is not None
+ item.parent.parent.keywords["kw"] = "class"
+
+ assert item.keywords["kw"] == "method"
+ assert len(item.keywords) == len(set(item.keywords))
+
+
+COLLECTION_ERROR_PY_FILES = dict(
+ test_01_failure="""
+ def test_1():
+ assert False
+ """,
+ test_02_import_error="""
+ import asdfasdfasdf
+ def test_2():
+ assert True
+ """,
+ test_03_import_error="""
+ import asdfasdfasdf
+ def test_3():
+ assert True
+ """,
+ test_04_success="""
+ def test_4():
+ assert True
+ """,
+)
+
+
+def test_exit_on_collection_error(pytester: Pytester) -> None:
+ """Verify that all collection errors are collected and no tests executed"""
+ pytester.makepyfile(**COLLECTION_ERROR_PY_FILES)
+
+ res = pytester.runpytest()
+ assert res.ret == 2
+
+ res.stdout.fnmatch_lines(
+ [
+ "collected 2 items / 2 errors",
+ "*ERROR collecting test_02_import_error.py*",
+ "*No module named *asdfa*",
+ "*ERROR collecting test_03_import_error.py*",
+ "*No module named *asdfa*",
+ ]
+ )
+
+
+def test_exit_on_collection_with_maxfail_smaller_than_n_errors(
+ pytester: Pytester,
+) -> None:
+ """
+ Verify collection is aborted once maxfail errors are encountered ignoring
+ further modules which would cause more collection errors.
+ """
+ pytester.makepyfile(**COLLECTION_ERROR_PY_FILES)
+
+ res = pytester.runpytest("--maxfail=1")
+ assert res.ret == 1
+ res.stdout.fnmatch_lines(
+ [
+ "collected 1 item / 1 error",
+ "*ERROR collecting test_02_import_error.py*",
+ "*No module named *asdfa*",
+ "*! stopping after 1 failures !*",
+ "*= 1 error in *",
+ ]
+ )
+ res.stdout.no_fnmatch_line("*test_03*")
+
+
+def test_exit_on_collection_with_maxfail_bigger_than_n_errors(
+ pytester: Pytester,
+) -> None:
+ """
+ Verify the test run aborts due to collection errors even if maxfail count of
+ errors was not reached.
+ """
+ pytester.makepyfile(**COLLECTION_ERROR_PY_FILES)
+
+ res = pytester.runpytest("--maxfail=4")
+ assert res.ret == 2
+ res.stdout.fnmatch_lines(
+ [
+ "collected 2 items / 2 errors",
+ "*ERROR collecting test_02_import_error.py*",
+ "*No module named *asdfa*",
+ "*ERROR collecting test_03_import_error.py*",
+ "*No module named *asdfa*",
+ "*! Interrupted: 2 errors during collection !*",
+ "*= 2 errors in *",
+ ]
+ )
+
+
+def test_continue_on_collection_errors(pytester: Pytester) -> None:
+ """
+ Verify tests are executed even when collection errors occur when the
+ --continue-on-collection-errors flag is set
+ """
+ pytester.makepyfile(**COLLECTION_ERROR_PY_FILES)
+
+ res = pytester.runpytest("--continue-on-collection-errors")
+ assert res.ret == 1
+
+ res.stdout.fnmatch_lines(
+ ["collected 2 items / 2 errors", "*1 failed, 1 passed, 2 errors*"]
+ )
+
+
+def test_continue_on_collection_errors_maxfail(pytester: Pytester) -> None:
+ """
+ Verify tests are executed even when collection errors occur and that maxfail
+ is honoured (including the collection error count).
+ 4 tests: 2 collection errors + 1 failure + 1 success
+ test_4 is never executed because the test run is with --maxfail=3 which
+ means it is interrupted after the 2 collection errors + 1 failure.
+ """
+ pytester.makepyfile(**COLLECTION_ERROR_PY_FILES)
+
+ res = pytester.runpytest("--continue-on-collection-errors", "--maxfail=3")
+ assert res.ret == 1
+
+ res.stdout.fnmatch_lines(["collected 2 items / 2 errors", "*1 failed, 2 errors*"])
+
+
+def test_fixture_scope_sibling_conftests(pytester: Pytester) -> None:
+ """Regression test case for https://github.com/pytest-dev/pytest/issues/2836"""
+ foo_path = pytester.mkdir("foo")
+ foo_path.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ @pytest.fixture
+ def fix():
+ return 1
+ """
+ )
+ )
+ 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")
+
+ res = pytester.runpytest()
+ assert res.ret == 1
+
+ res.stdout.fnmatch_lines(
+ [
+ "*ERROR at setup of test_food*",
+ "E*fixture 'fix' not found",
+ "*1 passed, 1 error*",
+ ]
+ )
+
+
+def test_collect_init_tests(pytester: Pytester) -> None:
+ """Check that we collect files from __init__.py files when they patch the 'python_files' (#3773)"""
+ p = pytester.copy_example("collect/collect_init_tests")
+ result = pytester.runpytest(p, "--collect-only")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 2 items",
+ "<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>",
+ ]
+ )
+ # Ignores duplicates with "." and pkginit (#4310).
+ 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>",
+ ]
+ )
+ # Same as before, but different order.
+ 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>",
+ ]
+ )
+ result = pytester.runpytest("./tests/test_foo.py", "--collect-only")
+ result.stdout.fnmatch_lines(
+ ["<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>"]
+ )
+ result.stdout.no_fnmatch_line("*test_foo*")
+
+
+def test_collect_invalid_signature_message(pytester: Pytester) -> None:
+ """Check that we issue a proper message when we can't determine the signature of a test
+ function (#4026).
+ """
+ pytester.makepyfile(
+ """
+ import pytest
+
+ class TestCase:
+ @pytest.fixture
+ def fix():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ ["Could not determine arguments of *.fix *: invalid method signature"]
+ )
+
+
+def test_collect_handles_raising_on_dunder_class(pytester: Pytester) -> None:
+ """Handle proxy classes like Django's LazySettings that might raise on
+ ``isinstance`` (#4266).
+ """
+ pytester.makepyfile(
+ """
+ class ImproperlyConfigured(Exception):
+ pass
+
+ class RaisesOnGetAttr(object):
+ def raises(self):
+ raise ImproperlyConfigured
+
+ __class__ = property(raises)
+
+ raises = RaisesOnGetAttr()
+
+
+ def test_1():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed in*"])
+ assert result.ret == 0
+
+
+def test_collect_with_chdir_during_import(pytester: Pytester) -> None:
+ subdir = pytester.mkdir("sub")
+ pytester.path.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """
+ import os
+ os.chdir(%r)
+ """
+ % (str(subdir),)
+ )
+ )
+ pytester.makepyfile(
+ """
+ def test_1():
+ import os
+ assert os.getcwd() == %r
+ """
+ % (str(subdir),)
+ )
+ with pytester.path.cwd():
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed in*"])
+ assert result.ret == 0
+
+ # Handles relative testpaths.
+ pytester.makeini(
+ """
+ [pytest]
+ testpaths = .
+ """
+ )
+ with pytester.path.cwd():
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(["collected 1 item"])
+
+
+def test_collect_pyargs_with_testpaths(
+ pytester: Pytester, monkeypatch: MonkeyPatch
+) -> 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")
+
+ root = pytester.mkdir("root")
+ root.joinpath("pytest.ini").write_text(
+ textwrap.dedent(
+ """
+ [pytest]
+ addopts = --pyargs
+ testpaths = testmod
+ """
+ )
+ )
+ monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep)
+ with root.cwd():
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(["*1 passed in*"])
+
+
+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(
+ real="""
+ def test_nodeid(request):
+ assert request.node.nodeid == "symlink.py::test_nodeid"
+ """
+ )
+ symlink = pytester.path.joinpath("symlink.py")
+ symlink_or_skip(real, symlink)
+ result = pytester.runpytest("-v", symlink)
+ result.stdout.fnmatch_lines(["symlink.py::test_nodeid PASSED*", "*1 passed in*"])
+ assert result.ret == 0
+
+
+def test_collect_symlink_out_of_tree(pytester: Pytester) -> None:
+ """Test collection of symlink via out-of-tree rootdir."""
+ sub = pytester.mkdir("sub")
+ real = sub.joinpath("test_real.py")
+ real.write_text(
+ textwrap.dedent(
+ """
+ def test_nodeid(request):
+ # Should not contain sub/ prefix.
+ assert request.node.nodeid == "test_real.py::test_nodeid"
+ """
+ ),
+ )
+
+ out_of_tree = pytester.mkdir("out_of_tree")
+ symlink_to_sub = out_of_tree.joinpath("symlink_to_sub")
+ symlink_or_skip(sub, symlink_to_sub)
+ os.chdir(sub)
+ result = pytester.runpytest("-vs", "--rootdir=%s" % sub, symlink_to_sub)
+ result.stdout.fnmatch_lines(
+ [
+ # Should not contain "sub/"!
+ "test_real.py::test_nodeid PASSED"
+ ]
+ )
+ assert result.ret == 0
+
+
+def test_collect_symlink_dir(pytester: Pytester) -> None:
+ """A symlinked directory is collected."""
+ dir = pytester.mkdir("dir")
+ dir.joinpath("test_it.py").write_text("def test_it(): pass", "utf-8")
+ symlink_or_skip(pytester.path.joinpath("symlink_dir"), dir)
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=2)
+
+
+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']")
+
+ 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'")
+
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+
+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")
+ p = subdir.joinpath("test_file.py")
+ p.write_text("def test_file(): pass")
+
+ # 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))
+ result.stdout.fnmatch_lines(
+ [
+ "sub/test_file.py::test_file PASSED*",
+ "sub/test_file.py::test_file PASSED*",
+ "*2 passed in*",
+ ]
+ )
+
+ result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init), str(p))
+ result.stdout.fnmatch_lines(
+ [
+ "sub/__init__.py::test_init PASSED*",
+ "sub/test_file.py::test_file PASSED*",
+ "*2 passed in*",
+ ]
+ )
+
+
+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")
+
+ result = pytester.runpytest(str(init))
+ result.stdout.fnmatch_lines(["*no tests ran in*"])
+
+ result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init))
+ result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"])
+
+
+@pytest.mark.parametrize("use_pkg", (True, False))
+def test_collect_sub_with_symlinks(use_pkg: bool, pytester: Pytester) -> None:
+ """Collection works with symlinked files and broken symlinks"""
+ sub = pytester.mkdir("sub")
+ if use_pkg:
+ sub.joinpath("__init__.py").touch()
+ sub.joinpath("test_file.py").write_text("def test_file(): pass")
+
+ # Create a broken symlink.
+ symlink_or_skip("test_doesnotexist.py", sub.joinpath("test_broken.py"))
+
+ # Symlink that gets collected.
+ symlink_or_skip("test_file.py", sub.joinpath("test_symlink.py"))
+
+ result = pytester.runpytest("-v", str(sub))
+ result.stdout.fnmatch_lines(
+ [
+ "sub/test_file.py::test_file PASSED*",
+ "sub/test_symlink.py::test_file PASSED*",
+ "*2 passed in*",
+ ]
+ )
+
+
+def test_collector_respects_tbstyle(pytester: Pytester) -> None:
+ p1 = pytester.makepyfile("assert 0")
+ result = pytester.runpytest(p1, "--tb=native")
+ assert result.ret == ExitCode.INTERRUPTED
+ result.stdout.fnmatch_lines(
+ [
+ "*_ ERROR collecting test_collector_respects_tbstyle.py _*",
+ "Traceback (most recent call last):",
+ ' File "*/test_collector_respects_tbstyle.py", line 1, in <module>',
+ " assert 0",
+ "AssertionError: assert 0",
+ "*! Interrupted: 1 error during collection !*",
+ "*= 1 error in *",
+ ]
+ )
+
+
+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")
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.OK
+
+
+def test_does_not_put_src_on_path(pytester: Pytester) -> None:
+ # `src` is not on sys.path so it should not be importable
+ ensure_file(pytester.path / "src/nope/__init__.py")
+ pytester.makepyfile(
+ "import pytest\n"
+ "def test():\n"
+ " with pytest.raises(ImportError):\n"
+ " import nope\n"
+ )
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.OK
+
+
+def test_fscollector_from_parent(pytester: Pytester, request: FixtureRequest) -> None:
+ """Ensure File.from_parent can forward custom arguments to the constructor.
+
+ Context: https://github.com/pytest-dev/pytest-cpp/pull/47
+ """
+
+ class MyCollector(pytest.File):
+ def __init__(self, *k, x, **kw):
+ super().__init__(*k, **kw)
+ self.x = x
+
+ 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:
+ """Ensure Class.from_parent can forward custom arguments to the constructor."""
+
+ class MyCollector(pytest.Class):
+ def __init__(self, name, parent, x):
+ super().__init__(name, parent)
+ self.x = x
+
+ @classmethod
+ def from_parent(cls, parent, *, name, x):
+ return super().from_parent(parent=parent, name=name, x=x)
+
+ collector = MyCollector.from_parent(parent=request.session, name="foo", x=10)
+ assert collector.x == 10
+
+
+class TestImportModeImportlib:
+ def test_collect_duplicate_names(self, pytester: Pytester) -> None:
+ """--import-mode=importlib can import modules with same names that are not in packages."""
+ pytester.makepyfile(
+ **{
+ "tests_a/test_foo.py": "def test_foo1(): pass",
+ "tests_b/test_foo.py": "def test_foo2(): pass",
+ }
+ )
+ result = pytester.runpytest("-v", "--import-mode=importlib")
+ result.stdout.fnmatch_lines(
+ [
+ "tests_a/test_foo.py::test_foo1 *",
+ "tests_b/test_foo.py::test_foo2 *",
+ "* 2 passed in *",
+ ]
+ )
+
+ def test_conftest(self, pytester: Pytester) -> None:
+ """Directory containing conftest modules are not put in sys.path as a side-effect of
+ importing them."""
+ tests_dir = pytester.path.joinpath("tests")
+ pytester.makepyfile(
+ **{
+ "tests/conftest.py": "",
+ "tests/test_foo.py": """
+ 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")
+ result.stdout.fnmatch_lines(["* 1 passed in *"])
+
+ def setup_conftest_and_foo(self, pytester: Pytester) -> None:
+ """Setup a tests folder to be used to test if modules in that folder can be imported
+ due to side-effects of --import-mode or not."""
+ pytester.makepyfile(
+ **{
+ "tests/conftest.py": "",
+ "tests/foo.py": """
+ def foo(): return 42
+ """,
+ "tests/test_foo.py": """
+ def test_check():
+ from foo import foo
+ assert foo() == 42
+ """,
+ }
+ )
+
+ def test_modules_importable_as_side_effect(self, pytester: Pytester) -> None:
+ """In import-modes `prepend` and `append`, we are able to import modules from folders
+ containing conftest.py files due to the side effect of changing sys.path."""
+ self.setup_conftest_and_foo(pytester)
+ result = pytester.runpytest("-v", "--import-mode=prepend")
+ result.stdout.fnmatch_lines(["* 1 passed in *"])
+
+ def test_modules_not_importable_as_side_effect(self, pytester: Pytester) -> None:
+ """In import-mode `importlib`, modules in folders containing conftest.py are not
+ importable, as don't change sys.path or sys.modules as side effect of importing
+ the conftest.py file.
+ """
+ self.setup_conftest_and_foo(pytester)
+ result = pytester.runpytest("-v", "--import-mode=importlib")
+ result.stdout.fnmatch_lines(
+ [
+ "*ModuleNotFoundError: No module named 'foo'",
+ "tests?test_foo.py:2: ModuleNotFoundError",
+ "* 1 failed 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
+ assertion rewriting mangling lineno's (#4984)."""
+ pytester.makepyfile(
+ """
+ @pytest.fixture
+ def a(): return 4
+ """
+ )
+ result = pytester.runpytest()
+ # Not INTERNAL_ERROR
+ assert result.ret == ExitCode.INTERRUPTED
+
+
+def test_does_not_crash_on_recursive_symlink(pytester: Pytester) -> None:
+ """Regression test for an issue around recursive symlinks (#7951)."""
+ symlink_or_skip("recursive", pytester.path.joinpath("recursive"))
+ pytester.makepyfile(
+ """
+ def test_foo(): assert True
+ """
+ )
+ result = pytester.runpytest()
+
+ assert result.ret == ExitCode.OK
+ assert result.parseoutcomes() == {"passed": 1}
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_compat.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_compat.py
new file mode 100644
index 0000000000..37cf4a077d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_compat.py
@@ -0,0 +1,265 @@
+import enum
+import sys
+from functools import partial
+from functools import wraps
+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
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+
+def test_is_generator() -> None:
+ def zap():
+ yield # pragma: no cover
+
+ def foo():
+ pass # pragma: no cover
+
+ assert is_generator(zap)
+ assert not is_generator(foo)
+
+
+def test_real_func_loop_limit() -> None:
+ class Evil:
+ def __init__(self):
+ self.left = 1000
+
+ def __repr__(self):
+ return f"<Evil left={self.left}>"
+
+ def __getattr__(self, attr):
+ if not self.left:
+ raise RuntimeError("it's over") # pragma: no cover
+ self.left -= 1
+ return self
+
+ evil = Evil()
+
+ with pytest.raises(
+ ValueError,
+ match=(
+ "could not find real function of <Evil left=800>\n"
+ "stopped at <Evil left=800>"
+ ),
+ ):
+ get_real_func(evil)
+
+
+def test_get_real_func() -> None:
+ """Check that get_real_func correctly unwraps decorators until reaching the real function"""
+
+ def decorator(f):
+ @wraps(f)
+ def inner():
+ pass # pragma: no cover
+
+ return inner
+
+ def func():
+ pass # pragma: no cover
+
+ wrapped_func = decorator(decorator(func))
+ assert get_real_func(wrapped_func) is func
+
+ wrapped_func2 = decorator(decorator(wrapped_func))
+ assert get_real_func(wrapped_func2) is func
+
+ # special case for __pytest_wrapped__ attribute: used to obtain the function up until the point
+ # a function was wrapped by pytest itself
+ wrapped_func2.__pytest_wrapped__ = _PytestWrapper(wrapped_func)
+ assert get_real_func(wrapped_func2) is wrapped_func
+
+
+def test_get_real_func_partial() -> None:
+ """Test get_real_func handles partial instances correctly"""
+
+ def foo(x):
+ return x
+
+ assert get_real_func(foo) is foo
+ assert get_real_func(partial(foo)) is foo
+
+
+@pytest.mark.skipif(sys.version_info >= (3, 11), reason="couroutine removed")
+def test_is_generator_asyncio(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ from _pytest.compat import is_generator
+ import asyncio
+ @asyncio.coroutine
+ def baz():
+ yield from [1,2,3]
+
+ def test_is_generator_asyncio():
+ assert not is_generator(baz)
+ """
+ )
+ # avoid importing asyncio into pytest's own process,
+ # which in turn imports logging (#8)
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+def test_is_generator_async_syntax(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ from _pytest.compat import is_generator
+ def test_is_generator_py35():
+ async def foo():
+ await foo()
+
+ async def bar():
+ pass
+
+ assert not is_generator(foo)
+ assert not is_generator(bar)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+def test_is_generator_async_gen_syntax(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ from _pytest.compat import is_generator
+ def test_is_generator_py36():
+ async def foo():
+ yield
+ await foo()
+
+ async def bar():
+ yield
+
+ assert not is_generator(foo)
+ assert not is_generator(bar)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+class ErrorsHelper:
+ @property
+ def raise_baseexception(self):
+ raise BaseException("base exception should be raised")
+
+ @property
+ def raise_exception(self):
+ raise Exception("exception should be caught")
+
+ @property
+ def raise_fail_outcome(self):
+ pytest.fail("fail should be caught")
+
+
+def test_helper_failures() -> None:
+ helper = ErrorsHelper()
+ with pytest.raises(Exception):
+ helper.raise_exception
+ with pytest.raises(OutcomeException):
+ 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):
+ assert safe_getattr(helper, "raise_baseexception", "default")
+
+
+def test_safe_isclass() -> None:
+ assert safe_isclass(type) is True
+
+ class CrappyClass(Exception):
+ # Type ignored because it's bypassed intentionally.
+ @property # type: ignore
+ def __class__(self):
+ assert False, "Should be ignored"
+
+ assert safe_isclass(CrappyClass()) is False
+
+
+def test_cached_property() -> None:
+ ncalls = 0
+
+ class Class:
+ @cached_property
+ def prop(self) -> int:
+ nonlocal ncalls
+ ncalls += 1
+ return ncalls
+
+ c1 = Class()
+ assert ncalls == 0
+ assert c1.prop == 1
+ assert c1.prop == 1
+ c2 = Class()
+ assert ncalls == 1
+ assert c2.prop == 2
+ assert c1.prop == 1
+
+
+def test_assert_never_union() -> None:
+ x: Union[int, str] = 10
+
+ if isinstance(x, int):
+ pass
+ else:
+ with pytest.raises(AssertionError):
+ assert_never(x) # type: ignore[arg-type]
+
+ if isinstance(x, int):
+ pass
+ elif isinstance(x, str):
+ pass
+ else:
+ assert_never(x)
+
+
+def test_assert_never_enum() -> None:
+ E = enum.Enum("E", "a b")
+ x: E = E.a
+
+ if x is E.a:
+ pass
+ else:
+ with pytest.raises(AssertionError):
+ assert_never(x) # type: ignore[arg-type]
+
+ if x is E.a:
+ pass
+ elif x is E.b:
+ pass
+ else:
+ assert_never(x)
+
+
+def test_assert_never_literal() -> None:
+ x: Literal["a", "b"] = "a"
+
+ if x == "a":
+ pass
+ else:
+ with pytest.raises(AssertionError):
+ assert_never(x) # type: ignore[arg-type]
+
+ if x == "a":
+ pass
+ elif x == "b":
+ pass
+ else:
+ assert_never(x)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_config.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_config.py
new file mode 100644
index 0000000000..8013966f07
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_config.py
@@ -0,0 +1,2115 @@
+import os
+import re
+import sys
+import textwrap
+from pathlib import Path
+from typing import Dict
+from typing import List
+from typing import Sequence
+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
+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.exceptions import UsageError
+from _pytest.config.findpaths import determine_setup
+from _pytest.config.findpaths import get_common_ancestor
+from _pytest.config.findpaths import locate_config
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pathlib import absolutepath
+from _pytest.pytester import Pytester
+
+
+class TestParseIni:
+ @pytest.mark.parametrize(
+ "section, filename", [("pytest", "pytest.ini"), ("tool:pytest", "setup.cfg")]
+ )
+ def test_getcfg_and_config(
+ self,
+ pytester: Pytester,
+ tmp_path: Path,
+ section: str,
+ filename: str,
+ monkeypatch: MonkeyPatch,
+ ) -> None:
+ sub = tmp_path / "sub"
+ sub.mkdir()
+ monkeypatch.chdir(sub)
+ (tmp_path / filename).write_text(
+ textwrap.dedent(
+ """\
+ [{section}]
+ name = value
+ """.format(
+ section=section
+ )
+ ),
+ encoding="utf-8",
+ )
+ _, _, cfg = locate_config([sub])
+ assert cfg["name"] == "value"
+ config = pytester.parseconfigure(str(sub))
+ assert config.inicfg["name"] == "value"
+
+ def test_setupcfg_uses_toolpytest_with_pytest(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile("def test(): pass")
+ pytester.makefile(
+ ".cfg",
+ setup="""
+ [tool:pytest]
+ testpaths=%s
+ [pytest]
+ testpaths=ignored
+ """
+ % p1.name,
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*, configfile: setup.cfg, *", "* 1 passed in *"])
+ assert result.ret == 0
+
+ def test_append_parse_args(
+ self, pytester: Pytester, tmp_path: Path, monkeypatch: MonkeyPatch
+ ) -> None:
+ monkeypatch.setenv("PYTEST_ADDOPTS", '--color no -rs --tb="short"')
+ tmp_path.joinpath("pytest.ini").write_text(
+ textwrap.dedent(
+ """\
+ [pytest]
+ addopts = --verbose
+ """
+ )
+ )
+ config = pytester.parseconfig(tmp_path)
+ assert config.option.color == "no"
+ assert config.option.reportchars == "s"
+ assert config.option.tbstyle == "short"
+ assert config.option.verbose
+
+ def test_tox_ini_wrong_version(self, pytester: Pytester) -> None:
+ pytester.makefile(
+ ".ini",
+ tox="""
+ [pytest]
+ minversion=999.0
+ """,
+ )
+ result = pytester.runpytest()
+ assert result.ret != 0
+ result.stderr.fnmatch_lines(
+ ["*tox.ini: 'minversion' requires pytest-999.0, actual pytest-*"]
+ )
+
+ @pytest.mark.parametrize(
+ "section, name",
+ [("tool:pytest", "setup.cfg"), ("pytest", "tox.ini"), ("pytest", "pytest.ini")],
+ )
+ def test_ini_names(self, pytester: Pytester, name, section) -> None:
+ pytester.path.joinpath(name).write_text(
+ textwrap.dedent(
+ """
+ [{section}]
+ minversion = 1.0
+ """.format(
+ section=section
+ )
+ )
+ )
+ config = pytester.parseconfig()
+ assert config.getini("minversion") == "1.0"
+
+ def test_pyproject_toml(self, pytester: Pytester) -> None:
+ pytester.makepyprojecttoml(
+ """
+ [tool.pytest.ini_options]
+ minversion = "1.0"
+ """
+ )
+ config = pytester.parseconfig()
+ assert config.getini("minversion") == "1.0"
+
+ def test_toxini_before_lower_pytestini(self, pytester: Pytester) -> None:
+ sub = pytester.mkdir("sub")
+ sub.joinpath("tox.ini").write_text(
+ textwrap.dedent(
+ """
+ [pytest]
+ minversion = 2.0
+ """
+ )
+ )
+ pytester.path.joinpath("pytest.ini").write_text(
+ textwrap.dedent(
+ """
+ [pytest]
+ minversion = 1.5
+ """
+ )
+ )
+ 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")
+ result = pytester.runpytest()
+ assert result.ret != 0
+ result.stderr.fnmatch_lines(["ERROR: *pytest.ini:1: no section header defined"])
+
+ @pytest.mark.xfail(reason="probably not needed")
+ def test_confcutdir(self, pytester: Pytester) -> None:
+ sub = pytester.mkdir("sub")
+ os.chdir(sub)
+ pytester.makeini(
+ """
+ [pytest]
+ addopts = --qwe
+ """
+ )
+ result = pytester.inline_run("--confcutdir=.")
+ assert result.ret == 0
+
+ @pytest.mark.parametrize(
+ "ini_file_text, invalid_keys, warning_output, exception_text",
+ [
+ pytest.param(
+ """
+ [pytest]
+ unknown_ini = value1
+ another_unknown_ini = value2
+ """,
+ ["unknown_ini", "another_unknown_ini"],
+ [
+ "=*= warnings summary =*=",
+ "*PytestConfigWarning:*Unknown config option: another_unknown_ini",
+ "*PytestConfigWarning:*Unknown config option: unknown_ini",
+ ],
+ "Unknown config option: another_unknown_ini",
+ id="2-unknowns",
+ ),
+ pytest.param(
+ """
+ [pytest]
+ unknown_ini = value1
+ minversion = 5.0.0
+ """,
+ ["unknown_ini"],
+ [
+ "=*= warnings summary =*=",
+ "*PytestConfigWarning:*Unknown config option: unknown_ini",
+ ],
+ "Unknown config option: unknown_ini",
+ id="1-unknown",
+ ),
+ pytest.param(
+ """
+ [some_other_header]
+ unknown_ini = value1
+ [pytest]
+ minversion = 5.0.0
+ """,
+ [],
+ [],
+ "",
+ id="unknown-in-other-header",
+ ),
+ pytest.param(
+ """
+ [pytest]
+ minversion = 5.0.0
+ """,
+ [],
+ [],
+ "",
+ id="no-unknowns",
+ ),
+ pytest.param(
+ """
+ [pytest]
+ conftest_ini_key = 1
+ """,
+ [],
+ [],
+ "",
+ id="1-known",
+ ),
+ ],
+ )
+ @pytest.mark.filterwarnings("default")
+ def test_invalid_config_options(
+ self,
+ pytester: Pytester,
+ ini_file_text,
+ invalid_keys,
+ warning_output,
+ exception_text,
+ ) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("conftest_ini_key", "")
+ """
+ )
+ pytester.makepyfile("def test(): pass")
+ pytester.makeini(ini_file_text)
+
+ config = pytester.parseconfig()
+ assert sorted(config._get_unknown_ini_keys()) == sorted(invalid_keys)
+
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(warning_output)
+
+ result = pytester.runpytest("--strict-config")
+ if exception_text:
+ result.stderr.fnmatch_lines("ERROR: " + exception_text)
+ assert result.ret == pytest.ExitCode.USAGE_ERROR
+ else:
+ result.stderr.no_fnmatch_line(exception_text)
+ assert result.ret == pytest.ExitCode.OK
+
+ @pytest.mark.filterwarnings("default")
+ def test_silence_unknown_key_warning(self, pytester: Pytester) -> None:
+ """Unknown config key warnings can be silenced using filterwarnings (#7620)"""
+ pytester.makeini(
+ """
+ [pytest]
+ filterwarnings =
+ ignore:Unknown config option:pytest.PytestConfigWarning
+ foobar=1
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.no_fnmatch_line("*PytestConfigWarning*")
+
+ @pytest.mark.filterwarnings("default::pytest.PytestConfigWarning")
+ def test_disable_warnings_plugin_disables_config_warnings(
+ self, pytester: Pytester
+ ) -> None:
+ """Disabling 'warnings' plugin also disables config time warnings"""
+ pytester.makeconftest(
+ """
+ import pytest
+ def pytest_configure(config):
+ config.issue_config_time_warning(
+ pytest.PytestConfigWarning("custom config warning"),
+ stacklevel=2,
+ )
+ """
+ )
+ result = pytester.runpytest("-pno:warnings")
+ result.stdout.no_fnmatch_line("*PytestConfigWarning*")
+
+ @pytest.mark.parametrize(
+ "ini_file_text, plugin_version, exception_text",
+ [
+ pytest.param(
+ """
+ [pytest]
+ required_plugins = a z
+ """,
+ "1.5",
+ "Missing required plugins: a, z",
+ id="2-missing",
+ ),
+ pytest.param(
+ """
+ [pytest]
+ required_plugins = a z myplugin
+ """,
+ "1.5",
+ "Missing required plugins: a, z",
+ id="2-missing-1-ok",
+ ),
+ pytest.param(
+ """
+ [pytest]
+ required_plugins = myplugin
+ """,
+ "1.5",
+ None,
+ id="1-ok",
+ ),
+ pytest.param(
+ """
+ [pytest]
+ required_plugins = myplugin==1.5
+ """,
+ "1.5",
+ None,
+ id="1-ok-pin-exact",
+ ),
+ pytest.param(
+ """
+ [pytest]
+ required_plugins = myplugin>1.0,<2.0
+ """,
+ "1.5",
+ None,
+ id="1-ok-pin-loose",
+ ),
+ pytest.param(
+ """
+ [pytest]
+ required_plugins = myplugin
+ """,
+ "1.5a1",
+ None,
+ id="1-ok-prerelease",
+ ),
+ pytest.param(
+ """
+ [pytest]
+ required_plugins = myplugin==1.6
+ """,
+ "1.5",
+ "Missing required plugins: myplugin==1.6",
+ id="missing-version",
+ ),
+ pytest.param(
+ """
+ [pytest]
+ required_plugins = myplugin==1.6 other==1.0
+ """,
+ "1.5",
+ "Missing required plugins: myplugin==1.6, other==1.0",
+ id="missing-versions",
+ ),
+ pytest.param(
+ """
+ [some_other_header]
+ required_plugins = won't be triggered
+ [pytest]
+ """,
+ "1.5",
+ None,
+ id="invalid-header",
+ ),
+ ],
+ )
+ def test_missing_required_plugins(
+ self,
+ pytester: Pytester,
+ monkeypatch: MonkeyPatch,
+ ini_file_text: str,
+ plugin_version: str,
+ exception_text: str,
+ ) -> None:
+ """Check 'required_plugins' option with various settings.
+
+ This test installs a mock "myplugin-1.5" which is used in the parametrized test cases.
+ """
+
+ @attr.s
+ class DummyEntryPoint:
+ name = attr.ib()
+ module = attr.ib()
+ group = "pytest11"
+
+ def load(self):
+ __import__(self.module)
+ return sys.modules[self.module]
+
+ entry_points = [
+ DummyEntryPoint("myplugin1", "myplugin1_module"),
+ ]
+
+ @attr.s
+ class DummyDist:
+ entry_points = attr.ib()
+ files = ()
+ version = plugin_version
+
+ @property
+ def metadata(self):
+ return {"name": "myplugin"}
+
+ def my_dists():
+ return [DummyDist(entry_points)]
+
+ pytester.makepyfile(myplugin1_module="# my plugin module")
+ pytester.syspathinsert()
+
+ monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
+
+ pytester.makeini(ini_file_text)
+
+ if exception_text:
+ with pytest.raises(pytest.UsageError, match=exception_text):
+ pytester.parseconfig()
+ else:
+ pytester.parseconfig()
+
+ def test_early_config_cmdline(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ """early_config contains options registered by third-party plugins.
+
+ This is a regression involving pytest-cov (and possibly others) introduced in #7700.
+ """
+ pytester.makepyfile(
+ myplugin="""
+ def pytest_addoption(parser):
+ parser.addoption('--foo', default=None, dest='foo')
+
+ def pytest_load_initial_conftests(early_config, parser, args):
+ assert early_config.known_args_namespace.foo == "1"
+ """
+ )
+ monkeypatch.setenv("PYTEST_PLUGINS", "myplugin")
+ pytester.syspathinsert()
+ result = pytester.runpytest("--foo=1")
+ result.stdout.fnmatch_lines("* no tests ran in *")
+
+
+class TestConfigCmdlineParsing:
+ def test_parsing_again_fails(self, pytester: Pytester) -> None:
+ config = pytester.parseconfig()
+ pytest.raises(AssertionError, lambda: config.parse([]))
+
+ def test_explicitly_specified_config_file_is_loaded(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("custom", "")
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ custom = 0
+ """
+ )
+ pytester.makefile(
+ ".ini",
+ custom="""
+ [pytest]
+ custom = 1
+ """,
+ )
+ config = pytester.parseconfig("-c", "custom.ini")
+ assert config.getini("custom") == "1"
+
+ pytester.makefile(
+ ".cfg",
+ custom_tool_pytest_section="""
+ [tool:pytest]
+ custom = 1
+ """,
+ )
+ config = pytester.parseconfig("-c", "custom_tool_pytest_section.cfg")
+ assert config.getini("custom") == "1"
+
+ pytester.makefile(
+ ".toml",
+ custom="""
+ [tool.pytest.ini_options]
+ custom = 1
+ value = [
+ ] # this is here on purpose, as it makes this an invalid '.ini' file
+ """,
+ )
+ config = pytester.parseconfig("-c", "custom.toml")
+ assert config.getini("custom") == "1"
+
+ def test_absolute_win32_path(self, pytester: Pytester) -> None:
+ temp_ini_file = pytester.makefile(
+ ".ini",
+ custom="""
+ [pytest]
+ addopts = --version
+ """,
+ )
+ from os.path import normpath
+
+ temp_ini_file_norm = normpath(str(temp_ini_file))
+ ret = pytest.main(["-c", temp_ini_file_norm])
+ assert ret == ExitCode.OK
+
+
+class TestConfigAPI:
+ def test_config_trace(self, pytester: Pytester) -> None:
+ config = pytester.parseconfig()
+ values: List[str] = []
+ config.trace.root.setwriter(values.append)
+ config.trace("hello")
+ assert len(values) == 1
+ assert values[0] == "hello [config]\n"
+
+ def test_config_getoption(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addoption("--hello", "-X", dest="hello")
+ """
+ )
+ config = pytester.parseconfig("--hello=this")
+ for x in ("hello", "--hello", "-X"):
+ assert config.getoption(x) == "this"
+ pytest.raises(ValueError, config.getoption, "qweqwe")
+
+ def test_config_getoption_unicode(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addoption('--hello', type=str)
+ """
+ )
+ config = pytester.parseconfig("--hello=this")
+ assert config.getoption("hello") == "this"
+
+ def test_config_getvalueorskip(self, pytester: Pytester) -> None:
+ config = pytester.parseconfig()
+ pytest.raises(pytest.skip.Exception, config.getvalueorskip, "hello")
+ verbose = config.getvalueorskip("verbose")
+ assert verbose == config.option.verbose
+
+ def test_config_getvalueorskip_None(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addoption("--hello")
+ """
+ )
+ config = pytester.parseconfig()
+ with pytest.raises(pytest.skip.Exception):
+ config.getvalueorskip("hello")
+
+ def test_getoption(self, pytester: Pytester) -> None:
+ config = pytester.parseconfig()
+ with pytest.raises(ValueError):
+ config.getvalue("x")
+ assert config.getoption("x", 1) == 1
+
+ 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)]}")
+ 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
+
+ @pytest.mark.parametrize("maybe_type", ["not passed", "None", '"string"'])
+ def test_addini(self, pytester: Pytester, maybe_type: str) -> None:
+ if maybe_type == "not passed":
+ type_string = ""
+ else:
+ type_string = f", {maybe_type}"
+
+ pytester.makeconftest(
+ f"""
+ def pytest_addoption(parser):
+ parser.addini("myname", "my new ini value"{type_string})
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ myname=hello
+ """
+ )
+ config = pytester.parseconfig()
+ val = config.getini("myname")
+ assert val == "hello"
+ pytest.raises(ValueError, config.getini, "other")
+
+ @pytest.mark.parametrize("config_type", ["ini", "pyproject"])
+ def test_addini_paths(self, pytester: Pytester, config_type: str) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("paths", "my new ini value", type="paths")
+ parser.addini("abc", "abc value")
+ """
+ )
+ if config_type == "ini":
+ inipath = pytester.makeini(
+ """
+ [pytest]
+ paths=hello world/sub.py
+ """
+ )
+ elif config_type == "pyproject":
+ inipath = pytester.makepyprojecttoml(
+ """
+ [tool.pytest.ini_options]
+ paths=["hello", "world/sub.py"]
+ """
+ )
+ config = pytester.parseconfig()
+ values = config.getini("paths")
+ assert len(values) == 2
+ assert values[0] == inipath.parent.joinpath("hello")
+ assert values[1] == inipath.parent.joinpath("world/sub.py")
+ pytest.raises(ValueError, config.getini, "other")
+
+ def make_conftest_for_args(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("args", "new args", type="args")
+ parser.addini("a2", "", "args", default="1 2 3".split())
+ """
+ )
+
+ def test_addini_args_ini_files(self, pytester: Pytester) -> None:
+ self.make_conftest_for_args(pytester)
+ pytester.makeini(
+ """
+ [pytest]
+ args=123 "123 hello" "this"
+ """
+ )
+ self.check_config_args(pytester)
+
+ def test_addini_args_pyproject_toml(self, pytester: Pytester) -> None:
+ self.make_conftest_for_args(pytester)
+ pytester.makepyprojecttoml(
+ """
+ [tool.pytest.ini_options]
+ args = ["123", "123 hello", "this"]
+ """
+ )
+ self.check_config_args(pytester)
+
+ def check_config_args(self, pytester: Pytester) -> None:
+ config = pytester.parseconfig()
+ values = config.getini("args")
+ assert values == ["123", "123 hello", "this"]
+ values = config.getini("a2")
+ assert values == list("123")
+
+ def make_conftest_for_linelist(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("xy", "", type="linelist")
+ parser.addini("a2", "", "linelist")
+ """
+ )
+
+ def test_addini_linelist_ini_files(self, pytester: Pytester) -> None:
+ self.make_conftest_for_linelist(pytester)
+ pytester.makeini(
+ """
+ [pytest]
+ xy= 123 345
+ second line
+ """
+ )
+ self.check_config_linelist(pytester)
+
+ def test_addini_linelist_pprojecttoml(self, pytester: Pytester) -> None:
+ self.make_conftest_for_linelist(pytester)
+ pytester.makepyprojecttoml(
+ """
+ [tool.pytest.ini_options]
+ xy = ["123 345", "second line"]
+ """
+ )
+ self.check_config_linelist(pytester)
+
+ def check_config_linelist(self, pytester: Pytester) -> None:
+ config = pytester.parseconfig()
+ values = config.getini("xy")
+ assert len(values) == 2
+ assert values == ["123 345", "second line"]
+ values = config.getini("a2")
+ assert values == []
+
+ @pytest.mark.parametrize(
+ "str_val, bool_val", [("True", True), ("no", False), ("no-ini", True)]
+ )
+ def test_addini_bool(
+ self, pytester: Pytester, str_val: str, bool_val: bool
+ ) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("strip", "", type="bool", default=True)
+ """
+ )
+ if str_val != "no-ini":
+ pytester.makeini(
+ """
+ [pytest]
+ strip=%s
+ """
+ % str_val
+ )
+ config = pytester.parseconfig()
+ assert config.getini("strip") is bool_val
+
+ def test_addinivalue_line_existing(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("xy", "", type="linelist")
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ xy= 123
+ """
+ )
+ config = pytester.parseconfig()
+ values = config.getini("xy")
+ assert len(values) == 1
+ assert values == ["123"]
+ config.addinivalue_line("xy", "456")
+ values = config.getini("xy")
+ assert len(values) == 2
+ assert values == ["123", "456"]
+
+ def test_addinivalue_line_new(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("xy", "", type="linelist")
+ """
+ )
+ config = pytester.parseconfig()
+ assert not config.getini("xy")
+ config.addinivalue_line("xy", "456")
+ values = config.getini("xy")
+ assert len(values) == 1
+ assert values == ["456"]
+ config.addinivalue_line("xy", "123")
+ values = config.getini("xy")
+ assert len(values) == 2
+ assert values == ["456", "123"]
+
+ 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: "
+ with pytest.raises(pytest.UsageError, match=exp_match):
+ pytester.parseconfig("--confcutdir", pytester.path.joinpath("file"))
+ with pytest.raises(pytest.UsageError, match=exp_match):
+ pytester.parseconfig("--confcutdir", pytester.path.joinpath("nonexistent"))
+
+ p = pytester.mkdir("dir")
+ config = pytester.parseconfig("--confcutdir", p)
+ assert config.getoption("confcutdir") == str(p)
+
+ @pytest.mark.parametrize(
+ "names, expected",
+ [
+ # dist-info based distributions root are files as will be put in PYTHONPATH
+ (["bar.py"], ["bar"]),
+ (["foo/bar.py"], ["bar"]),
+ (["foo/bar.pyc"], []),
+ (["foo/__init__.py"], ["foo"]),
+ (["bar/__init__.py", "xz.py"], ["bar", "xz"]),
+ (["setup.py"], []),
+ # egg based distributions root contain the files from the dist root
+ (["src/bar/__init__.py"], ["bar"]),
+ (["src/bar/__init__.py", "setup.py"], ["bar"]),
+ (["source/python/bar/__init__.py", "setup.py"], ["bar"]),
+ ],
+ )
+ def test_iter_rewritable_modules(self, names, expected) -> None:
+ assert list(_iter_rewritable_modules(names)) == expected
+
+
+class TestConfigFromdictargs:
+ def test_basic_behavior(self, _sys_snapshot) -> None:
+ option_dict = {"verbose": 444, "foo": "bar", "capture": "no"}
+ args = ["a", "b"]
+
+ config = Config.fromdictargs(option_dict, args)
+ with pytest.raises(AssertionError):
+ config.parse(["should refuse to parse again"])
+ assert config.option.verbose == 444
+ assert config.option.foo == "bar"
+ assert config.option.capture == "no"
+ assert config.args == args
+
+ def test_invocation_params_args(self, _sys_snapshot) -> None:
+ """Show that fromdictargs can handle args in their "orig" format"""
+ option_dict: Dict[str, object] = {}
+ args = ["-vvvv", "-s", "a", "b"]
+
+ config = Config.fromdictargs(option_dict, args)
+ assert config.args == ["a", "b"]
+ assert config.invocation_params.args == tuple(args)
+ assert config.option.verbose == 4
+ assert config.option.capture == "no"
+
+ def test_inifilename(self, tmp_path: Path) -> None:
+ d1 = tmp_path.joinpath("foo")
+ d1.mkdir()
+ p1 = d1.joinpath("bar.ini")
+ p1.touch()
+ p1.write_text(
+ textwrap.dedent(
+ """\
+ [pytest]
+ name = value
+ """
+ )
+ )
+
+ inifilename = "../../foo/bar.ini"
+ option_dict = {"inifilename": inifilename, "capture": "no"}
+
+ cwd = tmp_path.joinpath("a/b")
+ cwd.mkdir(parents=True)
+ p2 = cwd.joinpath("pytest.ini")
+ p2.touch()
+ p2.write_text(
+ textwrap.dedent(
+ """\
+ [pytest]
+ name = wrong-value
+ should_not_be_set = true
+ """
+ )
+ )
+ with MonkeyPatch.context() as mp:
+ mp.chdir(cwd)
+ config = Config.fromdictargs(option_dict, ())
+ inipath = absolutepath(inifilename)
+
+ assert config.args == [str(cwd)]
+ assert config.option.inifilename == inifilename
+ assert config.option.capture == "no"
+
+ # this indicates this is the file used for getting configuration values
+ assert config.inipath == inipath
+ assert config.inicfg.get("name") == "value"
+ assert config.inicfg.get("should_not_be_set") is None
+
+
+def test_options_on_small_file_do_not_blow_up(pytester: Pytester) -> None:
+ def runfiletest(opts: Sequence[str]) -> None:
+ reprec = pytester.inline_run(*opts)
+ passed, skipped, failed = reprec.countoutcomes()
+ assert failed == 2
+ assert skipped == passed == 0
+
+ path = str(
+ pytester.makepyfile(
+ """
+ def test_f1(): assert 0
+ def test_f2(): assert 0
+ """
+ )
+ )
+
+ runfiletest([path])
+ runfiletest(["-l", path])
+ runfiletest(["-s", path])
+ runfiletest(["--tb=no", path])
+ runfiletest(["--tb=short", path])
+ runfiletest(["--tb=long", path])
+ runfiletest(["--fulltrace", path])
+ runfiletest(["--traceconfig", path])
+ runfiletest(["-v", path])
+ runfiletest(["-v", "-v", path])
+
+
+def test_preparse_ordering_with_setuptools(
+ pytester: Pytester, monkeypatch: MonkeyPatch
+) -> None:
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
+
+ class EntryPoint:
+ name = "mytestplugin"
+ group = "pytest11"
+
+ def load(self):
+ class PseudoPlugin:
+ x = 42
+
+ return PseudoPlugin()
+
+ class Dist:
+ files = ()
+ metadata = {"name": "foo"}
+ entry_points = (EntryPoint(),)
+
+ def my_dists():
+ return (Dist,)
+
+ monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
+ pytester.makeconftest(
+ """
+ pytest_plugins = "mytestplugin",
+ """
+ )
+ monkeypatch.setenv("PYTEST_PLUGINS", "mytestplugin")
+ config = pytester.parseconfig()
+ plugin = config.pluginmanager.getplugin("mytestplugin")
+ assert plugin.x == 42
+
+
+def test_setuptools_importerror_issue1479(
+ pytester: Pytester, monkeypatch: MonkeyPatch
+) -> None:
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
+
+ class DummyEntryPoint:
+ name = "mytestplugin"
+ group = "pytest11"
+
+ def load(self):
+ raise ImportError("Don't hide me!")
+
+ class Distribution:
+ version = "1.0"
+ files = ("foo.txt",)
+ metadata = {"name": "foo"}
+ entry_points = (DummyEntryPoint(),)
+
+ def distributions():
+ return (Distribution(),)
+
+ monkeypatch.setattr(importlib_metadata, "distributions", distributions)
+ with pytest.raises(ImportError):
+ pytester.parseconfig()
+
+
+def test_importlib_metadata_broken_distribution(
+ pytester: Pytester, monkeypatch: MonkeyPatch
+) -> None:
+ """Integration test for broken distributions with 'files' metadata being None (#5389)"""
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
+
+ class DummyEntryPoint:
+ name = "mytestplugin"
+ group = "pytest11"
+
+ def load(self):
+ return object()
+
+ class Distribution:
+ version = "1.0"
+ files = None
+ metadata = {"name": "foo"}
+ entry_points = (DummyEntryPoint(),)
+
+ def distributions():
+ return (Distribution(),)
+
+ monkeypatch.setattr(importlib_metadata, "distributions", distributions)
+ pytester.parseconfig()
+
+
+@pytest.mark.parametrize("block_it", [True, False])
+def test_plugin_preparse_prevents_setuptools_loading(
+ pytester: Pytester, monkeypatch: MonkeyPatch, block_it: bool
+) -> None:
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
+
+ plugin_module_placeholder = object()
+
+ class DummyEntryPoint:
+ name = "mytestplugin"
+ group = "pytest11"
+
+ def load(self):
+ return plugin_module_placeholder
+
+ class Distribution:
+ version = "1.0"
+ files = ("foo.txt",)
+ metadata = {"name": "foo"}
+ entry_points = (DummyEntryPoint(),)
+
+ def distributions():
+ return (Distribution(),)
+
+ monkeypatch.setattr(importlib_metadata, "distributions", distributions)
+ args = ("-p", "no:mytestplugin") if block_it else ()
+ config = pytester.parseconfig(*args)
+ config.pluginmanager.import_plugin("mytestplugin")
+ if block_it:
+ assert "mytestplugin" not in sys.modules
+ assert config.pluginmanager.get_plugin("mytestplugin") is None
+ else:
+ assert (
+ config.pluginmanager.get_plugin("mytestplugin") is plugin_module_placeholder
+ )
+
+
+@pytest.mark.parametrize(
+ "parse_args,should_load", [(("-p", "mytestplugin"), True), ((), False)]
+)
+def test_disable_plugin_autoload(
+ pytester: Pytester,
+ monkeypatch: MonkeyPatch,
+ parse_args: Union[Tuple[str, str], Tuple[()]],
+ should_load: bool,
+) -> None:
+ class DummyEntryPoint:
+ project_name = name = "mytestplugin"
+ group = "pytest11"
+ version = "1.0"
+
+ def load(self):
+ return sys.modules[self.name]
+
+ class Distribution:
+ metadata = {"name": "foo"}
+ entry_points = (DummyEntryPoint(),)
+ files = ()
+
+ class PseudoPlugin:
+ x = 42
+
+ attrs_used = []
+
+ def __getattr__(self, name):
+ assert name == "__loader__"
+ self.attrs_used.append(name)
+ return object()
+
+ def distributions():
+ return (Distribution(),)
+
+ monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
+ monkeypatch.setattr(importlib_metadata, "distributions", distributions)
+ monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) # type: ignore[misc]
+ config = pytester.parseconfig(*parse_args)
+ has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None
+ assert has_loaded == should_load
+ if should_load:
+ assert PseudoPlugin.attrs_used == ["__loader__"]
+ else:
+ assert PseudoPlugin.attrs_used == []
+
+
+def test_plugin_loading_order(pytester: Pytester) -> None:
+ """Test order of plugin loading with `-p`."""
+ p1 = pytester.makepyfile(
+ """
+ def test_terminal_plugin(request):
+ import myplugin
+ assert myplugin.terminal_plugin == [False, True]
+ """,
+ **{
+ "myplugin": """
+ terminal_plugin = []
+
+ def pytest_configure(config):
+ terminal_plugin.append(bool(config.pluginmanager.get_plugin("terminalreporter")))
+
+ 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."""
+ pytester.makeini(
+ """
+ [pytest]
+ addopts = --invalid-option
+ """
+ )
+ result = pytester.runpytest()
+ result.stderr.fnmatch_lines(
+ [
+ "*error: unrecognized arguments: --invalid-option*",
+ "* inifile: %s*" % pytester.path.joinpath("tox.ini"),
+ "* rootdir: %s*" % pytester.path,
+ ]
+ )
+
+
+@pytest.mark.parametrize(
+ "args",
+ [
+ ["dir1", "dir2", "-v"],
+ ["dir1", "-v", "dir2"],
+ ["dir2", "-v", "dir1"],
+ ["-v", "dir2", "dir1"],
+ ],
+)
+def test_consider_args_after_options_for_rootdir(
+ pytester: Pytester, args: List[str]
+) -> None:
+ """
+ Consider all arguments in the command-line for rootdir
+ discovery, even if they happen to occur after an option. #949
+ """
+ # replace "dir1" and "dir2" from "args" into their real directory
+ root = pytester.mkdir("myroot")
+ d1 = root.joinpath("dir1")
+ d1.mkdir()
+ d2 = root.joinpath("dir2")
+ d2.mkdir()
+ for i, arg in enumerate(args):
+ if arg == "dir1":
+ args[i] = str(d1)
+ elif arg == "dir2":
+ args[i] = str(d2)
+ with MonkeyPatch.context() as mp:
+ mp.chdir(root)
+ result = pytester.runpytest(*args)
+ result.stdout.fnmatch_lines(["*rootdir: *myroot"])
+
+
+def test_toolongargs_issue224(pytester: Pytester) -> None:
+ result = pytester.runpytest("-m", "hello" * 500)
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+
+def test_config_in_subdirectory_colon_command_line_issue2148(
+ pytester: Pytester,
+) -> None:
+ conftest_source = """
+ def pytest_addoption(parser):
+ parser.addini('foo', 'foo')
+ """
+
+ pytester.makefile(
+ ".ini",
+ **{"pytest": "[pytest]\nfoo = root", "subdir/pytest": "[pytest]\nfoo = subdir"},
+ )
+
+ pytester.makepyfile(
+ **{
+ "conftest": conftest_source,
+ "subdir/conftest": conftest_source,
+ "subdir/test_foo": """\
+ def test_foo(pytestconfig):
+ assert pytestconfig.getini('foo') == 'subdir'
+ """,
+ }
+ )
+
+ result = pytester.runpytest("subdir/test_foo.py::test_foo")
+ assert result.ret == 0
+
+
+def test_notify_exception(pytester: Pytester, capfd) -> None:
+ config = pytester.parseconfig()
+ with pytest.raises(ValueError) as excinfo:
+ raise ValueError(1)
+ config.notify_exception(excinfo, config.option)
+ _, err = capfd.readouterr()
+ assert "ValueError" in err
+
+ class A:
+ def pytest_internalerror(self):
+ return True
+
+ config.pluginmanager.register(A())
+ config.notify_exception(excinfo, config.option)
+ _, err = capfd.readouterr()
+ assert not err
+
+ config = pytester.parseconfig("-p", "no:terminal")
+ with pytest.raises(ValueError) as excinfo:
+ raise ValueError(1)
+ config.notify_exception(excinfo, config.option)
+ _, err = capfd.readouterr()
+ assert "ValueError" in err
+
+
+def test_no_terminal_discovery_error(pytester: Pytester) -> None:
+ pytester.makepyfile("raise TypeError('oops!')")
+ result = pytester.runpytest("-p", "no:terminal", "--collect-only")
+ assert result.ret == ExitCode.INTERRUPTED
+
+
+def test_load_initial_conftest_last_ordering(_config_for_test):
+ pm = _config_for_test.pluginmanager
+
+ class My:
+ def pytest_load_initial_conftests(self):
+ pass
+
+ m = My()
+ pm.register(m)
+ hc = pm.hook.pytest_load_initial_conftests
+ hookimpls = [
+ (
+ hookimpl.function.__module__,
+ "wrapper" if hookimpl.hookwrapper else "nonwrapper",
+ )
+ for hookimpl in hc.get_hookimpls()
+ ]
+ assert hookimpls == [
+ ("_pytest.config", "nonwrapper"),
+ (m.__module__, "nonwrapper"),
+ ("_pytest.legacypath", "nonwrapper"),
+ ("_pytest.python_path", "nonwrapper"),
+ ("_pytest.capture", "wrapper"),
+ ("_pytest.warnings", "wrapper"),
+ ]
+
+
+def test_get_plugin_specs_as_list() -> None:
+ def exp_match(val: object) -> str:
+ return (
+ "Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %s"
+ % re.escape(repr(val))
+ )
+
+ with pytest.raises(pytest.UsageError, match=exp_match({"foo"})):
+ _get_plugin_specs_as_list({"foo"}) # type: ignore[arg-type]
+ with pytest.raises(pytest.UsageError, match=exp_match({})):
+ _get_plugin_specs_as_list(dict()) # type: ignore[arg-type]
+
+ assert _get_plugin_specs_as_list(None) == []
+ assert _get_plugin_specs_as_list("") == []
+ assert _get_plugin_specs_as_list("foo") == ["foo"]
+ assert _get_plugin_specs_as_list("foo,bar") == ["foo", "bar"]
+ assert _get_plugin_specs_as_list(["foo", "bar"]) == ["foo", "bar"]
+ assert _get_plugin_specs_as_list(("foo", "bar")) == ["foo", "bar"]
+
+
+def test_collect_pytest_prefix_bug_integration(pytester: Pytester) -> None:
+ """Integration test for issue #3775"""
+ p = pytester.copy_example("config/collect_pytest_prefix")
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(["* 1 passed *"])
+
+
+def test_collect_pytest_prefix_bug(pytestconfig):
+ """Ensure we collect only actual functions from conftest files (#3775)"""
+
+ class Dummy:
+ class pytest_something:
+ pass
+
+ pm = pytestconfig.pluginmanager
+ assert pm.parse_hookimpl_opts(Dummy(), "pytest_something") is None
+
+
+class TestRootdir:
+ def test_simple_noini(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
+ assert get_common_ancestor([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
+ monkeypatch.chdir(tmp_path)
+ assert get_common_ancestor([]) == 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
+
+ @pytest.mark.parametrize(
+ "name, contents",
+ [
+ pytest.param("pytest.ini", "[pytest]\nx=10", id="pytest.ini"),
+ pytest.param(
+ "pyproject.toml", "[tool.pytest.ini_options]\nx=10", id="pyproject.toml"
+ ),
+ pytest.param("tox.ini", "[pytest]\nx=10", id="tox.ini"),
+ pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"),
+ ],
+ )
+ def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None:
+ inipath = tmp_path / name
+ inipath.write_text(contents, "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)
+ assert rootpath == tmp_path
+ assert parsed_inipath == inipath
+ rootpath, parsed_inipath, ini_config = determine_setup(None, [str(b), str(a)])
+ assert rootpath == tmp_path
+ assert parsed_inipath == inipath
+ assert ini_config == {"x": "10"}
+
+ @pytest.mark.parametrize("name", ["setup.cfg", "tox.ini"])
+ def test_pytestini_overrides_empty_other(self, tmp_path: Path, name: str) -> None:
+ inipath = tmp_path / "pytest.ini"
+ inipath.touch()
+ a = tmp_path / "a"
+ a.mkdir()
+ (a / name).touch()
+ rootpath, parsed_inipath, _ = determine_setup(None, [str(a)])
+ assert rootpath == tmp_path
+ assert parsed_inipath == inipath
+
+ def test_setuppy_fallback(self, tmp_path: Path) -> None:
+ a = tmp_path / "a"
+ a.mkdir()
+ (a / "setup.cfg").touch()
+ (tmp_path / "setup.py").touch()
+ rootpath, inipath, inicfg = determine_setup(None, [str(a)])
+ 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)])
+ assert rootpath == tmp_path
+ assert inipath is None
+ assert inicfg == {}
+
+ @pytest.mark.parametrize(
+ "name, contents",
+ [
+ # pytest.param("pytest.ini", "[pytest]\nx=10", id="pytest.ini"),
+ pytest.param(
+ "pyproject.toml", "[tool.pytest.ini_options]\nx=10", id="pyproject.toml"
+ ),
+ # pytest.param("tox.ini", "[pytest]\nx=10", id="tox.ini"),
+ # pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"),
+ ],
+ )
+ def test_with_specific_inifile(
+ self, tmp_path: Path, name: str, contents: str
+ ) -> None:
+ p = tmp_path / name
+ p.touch()
+ p.write_text(contents, "utf-8")
+ rootpath, inipath, ini_config = determine_setup(str(p), [str(tmp_path)])
+ assert rootpath == tmp_path
+ assert inipath == p
+ assert ini_config == {"x": "10"}
+
+ def test_explicit_config_file_sets_rootdir(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ tests_dir = tmp_path / "tests"
+ tests_dir.mkdir()
+
+ 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)])
+ 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)])
+ assert rootpath == tmp_path
+ assert found_inipath == inipath
+
+ def test_with_arg_outside_cwd_without_inifile(
+ self, tmp_path: Path, monkeypatch: MonkeyPatch
+ ) -> None:
+ monkeypatch.chdir(tmp_path)
+ a = tmp_path / "a"
+ a.mkdir()
+ b = tmp_path / "b"
+ b.mkdir()
+ rootpath, inifile, _ = determine_setup(None, [str(a), str(b)])
+ assert rootpath == tmp_path
+ assert inifile is None
+
+ def test_with_arg_outside_cwd_with_inifile(self, tmp_path: Path) -> None:
+ a = tmp_path / "a"
+ a.mkdir()
+ b = tmp_path / "b"
+ b.mkdir()
+ inipath = a / "pytest.ini"
+ inipath.touch()
+ rootpath, parsed_inipath, _ = determine_setup(None, [str(a), str(b)])
+ assert rootpath == a
+ assert inipath == parsed_inipath
+
+ @pytest.mark.parametrize("dirs", ([], ["does-not-exist"], ["a/does-not-exist"]))
+ def test_with_non_dir_arg(
+ self, dirs: Sequence[str], tmp_path: Path, monkeypatch: MonkeyPatch
+ ) -> None:
+ monkeypatch.chdir(tmp_path)
+ rootpath, inipath, _ = determine_setup(None, dirs)
+ assert rootpath == tmp_path
+ assert inipath is None
+
+ def test_with_existing_file_in_subdir(
+ self, tmp_path: Path, monkeypatch: MonkeyPatch
+ ) -> None:
+ a = tmp_path / "a"
+ a.mkdir()
+ (a / "exists").touch()
+ monkeypatch.chdir(tmp_path)
+ rootpath, inipath, _ = determine_setup(None, ["a/exist"])
+ assert rootpath == tmp_path
+ assert inipath is None
+
+ def test_with_config_also_in_parent_directory(
+ self, tmp_path: Path, monkeypatch: MonkeyPatch
+ ) -> None:
+ """Regression test for #7807."""
+ (tmp_path / "setup.cfg").write_text("[tool:pytest]\n", "utf-8")
+ (tmp_path / "myproject").mkdir()
+ (tmp_path / "myproject" / "setup.cfg").write_text("[tool:pytest]\n", "utf-8")
+ (tmp_path / "myproject" / "tests").mkdir()
+ monkeypatch.chdir(tmp_path / "myproject")
+
+ rootpath, inipath, _ = determine_setup(None, ["tests/"])
+
+ assert rootpath == tmp_path / "myproject"
+ assert inipath == tmp_path / "myproject" / "setup.cfg"
+
+
+class TestOverrideIniArgs:
+ @pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split())
+ def test_override_ini_names(self, pytester: Pytester, name: str) -> None:
+ section = "[pytest]" if name != "setup.cfg" else "[tool:pytest]"
+ pytester.path.joinpath(name).write_text(
+ textwrap.dedent(
+ """
+ {section}
+ custom = 1.0""".format(
+ section=section
+ )
+ )
+ )
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("custom", "")"""
+ )
+ pytester.makepyfile(
+ """
+ def test_pass(pytestconfig):
+ ini_val = pytestconfig.getini("custom")
+ print('\\ncustom_option:%s\\n' % ini_val)"""
+ )
+
+ result = pytester.runpytest("--override-ini", "custom=2.0", "-s")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["custom_option:2.0"])
+
+ result = pytester.runpytest(
+ "--override-ini", "custom=2.0", "--override-ini=custom=3.0", "-s"
+ )
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["custom_option:3.0"])
+
+ def test_override_ini_paths(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("paths", "my new ini value", type="paths")"""
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ paths=blah.py"""
+ )
+ pytester.makepyfile(
+ r"""
+ def test_overriden(pytestconfig):
+ config_paths = pytestconfig.getini("paths")
+ print(config_paths)
+ for cpf in config_paths:
+ print('\nuser_path:%s' % cpf.name)
+ """
+ )
+ result = pytester.runpytest(
+ "--override-ini", "paths=foo/bar1.py foo/bar2.py", "-s"
+ )
+ result.stdout.fnmatch_lines(["user_path:bar1.py", "user_path:bar2.py"])
+
+ def test_override_multiple_and_default(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ addini = parser.addini
+ addini("custom_option_1", "", default="o1")
+ addini("custom_option_2", "", default="o2")
+ addini("custom_option_3", "", default=False, type="bool")
+ addini("custom_option_4", "", default=True, type="bool")"""
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ custom_option_1=custom_option_1
+ custom_option_2=custom_option_2
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_multiple_options(pytestconfig):
+ prefix = "custom_option"
+ for x in range(1, 5):
+ ini_value=pytestconfig.getini("%s_%d" % (prefix, x))
+ print('\\nini%d:%s' % (x, ini_value))
+ """
+ )
+ result = pytester.runpytest(
+ "--override-ini",
+ "custom_option_1=fulldir=/tmp/user1",
+ "-o",
+ "custom_option_2=url=/tmp/user2?a=b&d=e",
+ "-o",
+ "custom_option_3=True",
+ "-o",
+ "custom_option_4=no",
+ "-s",
+ )
+ result.stdout.fnmatch_lines(
+ [
+ "ini1:fulldir=/tmp/user1",
+ "ini2:url=/tmp/user2?a=b&d=e",
+ "ini3:True",
+ "ini4:False",
+ ]
+ )
+
+ def test_override_ini_usage_error_bad_style(self, pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ xdist_strict=False
+ """
+ )
+ result = pytester.runpytest("--override-ini", "xdist_strict", "True")
+ result.stderr.fnmatch_lines(
+ [
+ "ERROR: -o/--override-ini expects option=value style (got: 'xdist_strict').",
+ ]
+ )
+
+ @pytest.mark.parametrize("with_ini", [True, False])
+ def test_override_ini_handled_asap(
+ self, pytester: Pytester, with_ini: bool
+ ) -> None:
+ """-o should be handled as soon as possible and always override what's in ini files (#2238)"""
+ if with_ini:
+ pytester.makeini(
+ """
+ [pytest]
+ python_files=test_*.py
+ """
+ )
+ pytester.makepyfile(
+ unittest_ini_handle="""
+ def test():
+ pass
+ """
+ )
+ result = pytester.runpytest("--override-ini", "python_files=unittest_*.py")
+ result.stdout.fnmatch_lines(["*1 passed in*"])
+
+ def test_addopts_before_initini(
+ self, monkeypatch: MonkeyPatch, _config_for_test, _sys_snapshot
+ ) -> None:
+ cache_dir = ".custom_cache"
+ monkeypatch.setenv("PYTEST_ADDOPTS", "-o cache_dir=%s" % cache_dir)
+ config = _config_for_test
+ config._preparse([], addopts=True)
+ assert config._override_ini == ["cache_dir=%s" % cache_dir]
+
+ def test_addopts_from_env_not_concatenated(
+ self, monkeypatch: MonkeyPatch, _config_for_test
+ ) -> None:
+ """PYTEST_ADDOPTS should not take values from normal args (#4265)."""
+ monkeypatch.setenv("PYTEST_ADDOPTS", "-o")
+ config = _config_for_test
+ with pytest.raises(UsageError) as excinfo:
+ config._preparse(["cache_dir=ignored"], addopts=True)
+ assert (
+ "error: argument -o/--override-ini: expected one argument (via PYTEST_ADDOPTS)"
+ in excinfo.value.args[0]
+ )
+
+ def test_addopts_from_ini_not_concatenated(self, pytester: Pytester) -> None:
+ """`addopts` from ini should not take values from normal args (#4265)."""
+ pytester.makeini(
+ """
+ [pytest]
+ addopts=-o
+ """
+ )
+ 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,)
+ ]
+ )
+ assert result.ret == _pytest.config.ExitCode.USAGE_ERROR
+
+ def test_override_ini_does_not_contain_paths(
+ self, _config_for_test, _sys_snapshot
+ ) -> None:
+ """Check that -o no longer swallows all options after it (#3103)"""
+ config = _config_for_test
+ config._preparse(["-o", "cache_dir=/cache", "/some/test/path"])
+ assert config._override_ini == ["cache_dir=/cache"]
+
+ def test_multiple_override_ini_options(self, pytester: Pytester) -> None:
+ """Ensure a file path following a '-o' option does not generate an error (#3103)"""
+ pytester.makepyfile(
+ **{
+ "conftest.py": """
+ def pytest_addoption(parser):
+ parser.addini('foo', default=None, help='some option')
+ parser.addini('bar', default=None, help='some option')
+ """,
+ "test_foo.py": """
+ def test(pytestconfig):
+ assert pytestconfig.getini('foo') == '1'
+ assert pytestconfig.getini('bar') == '0'
+ """,
+ "test_bar.py": """
+ def test():
+ assert False
+ """,
+ }
+ )
+ result = pytester.runpytest("-o", "foo=1", "-o", "bar=0", "test_foo.py")
+ assert "ERROR:" not in result.stderr.str()
+ result.stdout.fnmatch_lines(["collected 1 item", "*= 1 passed in *="])
+
+
+def test_help_via_addopts(pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ addopts = --unknown-option-should-allow-for-help --help
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(
+ [
+ "usage: *",
+ "positional arguments:",
+ # Displays full/default help.
+ "to see available markers type: pytest --markers",
+ ]
+ )
+
+
+def test_help_and_version_after_argument_error(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def validate(arg):
+ raise argparse.ArgumentTypeError("argerror")
+
+ def pytest_addoption(parser):
+ group = parser.getgroup('cov')
+ group.addoption(
+ "--invalid-option-should-allow-for-help",
+ type=validate,
+ )
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ addopts = --invalid-option-should-allow-for-help
+ """
+ )
+ result = pytester.runpytest("--help")
+ result.stdout.fnmatch_lines(
+ [
+ "usage: *",
+ "positional arguments:",
+ "NOTE: displaying only minimal help due to UsageError.",
+ ]
+ )
+ result.stderr.fnmatch_lines(
+ [
+ "ERROR: usage: *",
+ "%s: error: argument --invalid-option-should-allow-for-help: expected one argument"
+ % (pytester._request.config._parser.optparser.prog,),
+ ]
+ )
+ # Does not display full/default help.
+ assert "to see available markers type: pytest --markers" not in result.stdout.lines
+ assert result.ret == ExitCode.USAGE_ERROR
+
+ result = pytester.runpytest("--version")
+ result.stdout.fnmatch_lines([f"pytest {pytest.__version__}"])
+ assert result.ret == ExitCode.USAGE_ERROR
+
+
+def test_help_formatter_uses_py_get_terminal_width(monkeypatch: MonkeyPatch) -> None:
+ from _pytest.config.argparsing import DropShorterLongHelpFormatter
+
+ monkeypatch.setenv("COLUMNS", "90")
+ formatter = DropShorterLongHelpFormatter("prog")
+ assert formatter._width == 90
+
+ monkeypatch.setattr("_pytest._io.get_terminal_width", lambda: 160)
+ formatter = DropShorterLongHelpFormatter("prog")
+ assert formatter._width == 160
+
+ formatter = DropShorterLongHelpFormatter("prog", width=42)
+ assert formatter._width == 42
+
+
+def test_config_does_not_load_blocked_plugin_from_args(pytester: Pytester) -> None:
+ """This tests that pytest's config setup handles "-p no:X"."""
+ p = pytester.makepyfile("def test(capfd): pass")
+ result = pytester.runpytest(str(p), "-pno:capture")
+ result.stdout.fnmatch_lines(["E fixture 'capfd' not found"])
+ assert result.ret == ExitCode.TESTS_FAILED
+
+ result = pytester.runpytest(str(p), "-pno: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"""
+
+ class DummyPlugin:
+ pass
+
+ p = pytester.makepyfile("def test(): pass")
+ plugin = DummyPlugin()
+ rec = pytester.inline_run(p, "-v", plugins=[plugin])
+ calls = rec.getcalls("pytest_runtest_protocol")
+ assert len(calls) == 1
+ call = calls[0]
+ config = call.item.config
+
+ assert config.invocation_params.args == (str(p), "-v")
+ assert config.invocation_params.dir == pytester.path
+
+ plugins = config.invocation_params.plugins
+ assert len(plugins) == 2
+ assert plugins[0] is plugin
+ assert type(plugins[1]).__name__ == "Collect" # installed by pytester.inline_run()
+
+ # args cannot be None
+ with pytest.raises(TypeError):
+ Config.InvocationParams(args=None, plugins=None, dir=Path()) # type: ignore[arg-type]
+
+
+@pytest.mark.parametrize(
+ "plugin",
+ [
+ x
+ for x in _pytest.config.default_plugins
+ if x not in _pytest.config.essential_plugins
+ ],
+)
+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)
+
+ if plugin == "python":
+ assert result.ret == ExitCode.USAGE_ERROR
+ result.stderr.fnmatch_lines(
+ [
+ "ERROR: not found: */test_config_blocked_default_plugins.py",
+ "(no name '*/test_config_blocked_default_plugins.py' in any of [])",
+ ]
+ )
+ return
+
+ assert result.ret == ExitCode.OK
+ if plugin != "terminal":
+ result.stdout.fnmatch_lines(["* 1 passed in *"])
+
+ p = pytester.makepyfile("def test(): assert 0")
+ result = pytester.runpytest(str(p), "-pno:%s" % plugin)
+ assert result.ret == ExitCode.TESTS_FAILED
+ if plugin != "terminal":
+ result.stdout.fnmatch_lines(["* 1 failed in *"])
+ else:
+ assert result.stdout.lines == []
+
+
+class TestSetupCfg:
+ def test_pytest_setup_cfg_unsupported(self, pytester: Pytester) -> None:
+ pytester.makefile(
+ ".cfg",
+ setup="""
+ [pytest]
+ addopts = --verbose
+ """,
+ )
+ with pytest.raises(pytest.fail.Exception):
+ pytester.runpytest()
+
+ def test_pytest_custom_cfg_unsupported(self, pytester: Pytester) -> None:
+ pytester.makefile(
+ ".cfg",
+ custom="""
+ [pytest]
+ addopts = --verbose
+ """,
+ )
+ with pytest.raises(pytest.fail.Exception):
+ pytester.runpytest("-c", "custom.cfg")
+
+
+class TestPytestPluginsVariable:
+ def test_pytest_plugins_in_non_top_level_conftest_unsupported(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ **{
+ "subdirectory/conftest.py": """
+ pytest_plugins=['capture']
+ """
+ }
+ )
+ pytester.makepyfile(
+ """
+ def test_func():
+ pass
+ """
+ )
+ res = pytester.runpytest()
+ assert res.ret == 2
+ msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported"
+ res.stdout.fnmatch_lines([f"*{msg}*", f"*subdirectory{os.sep}conftest.py*"])
+
+ @pytest.mark.parametrize("use_pyargs", [True, False])
+ def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs(
+ 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": "",
+ "src/pkg/test_root.py": "def test(): pass",
+ "src/pkg/sub/__init__.py": "",
+ "src/pkg/sub/conftest.py": "pytest_plugins=['capture']",
+ "src/pkg/sub/test_bar.py": "def test(): pass",
+ }
+ pytester.makepyfile(**files)
+ pytester.syspathinsert(pytester.path.joinpath("src"))
+
+ 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"
+ if use_pyargs:
+ assert msg not in res.stdout.str()
+ else:
+ res.stdout.fnmatch_lines([f"*{msg}*"])
+
+ def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conftest(
+ self, pytester: Pytester
+ ) -> None:
+ subdirectory = pytester.path.joinpath("subdirectory")
+ subdirectory.mkdir()
+ pytester.makeconftest(
+ """
+ pytest_plugins=['capture']
+ """
+ )
+ pytester.path.joinpath("conftest.py").rename(
+ subdirectory.joinpath("conftest.py")
+ )
+
+ pytester.makepyfile(
+ """
+ def test_func():
+ pass
+ """
+ )
+
+ res = pytester.runpytest_subprocess()
+ assert res.ret == 2
+ msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported"
+ res.stdout.fnmatch_lines([f"*{msg}*", f"*subdirectory{os.sep}conftest.py*"])
+
+ def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ "def test_func(): pass",
+ **{
+ "subdirectory/conftest": "pass",
+ "conftest": """
+ import warnings
+ warnings.filterwarnings('always', category=DeprecationWarning)
+ pytest_plugins=['capture']
+ """,
+ },
+ )
+ res = pytester.runpytest_subprocess()
+ assert res.ret == 0
+ msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported"
+ assert msg not in res.stdout.str()
+
+
+def test_conftest_import_error_repr(tmp_path: Path) -> None:
+ """`ConftestImportFailure` should use a short error message and readable
+ path to the failed conftest.py file."""
+ path = tmp_path.joinpath("foo/conftest.py")
+ with pytest.raises(
+ ConftestImportFailure,
+ match=re.escape(f"RuntimeError: some error (from {path})"),
+ ):
+ 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
+
+
+def test_strtobool() -> None:
+ assert _strtobool("YES")
+ assert not _strtobool("NO")
+ with pytest.raises(ValueError):
+ _strtobool("unknown")
+
+
+@pytest.mark.parametrize(
+ "arg, escape, expected",
+ [
+ ("ignore", False, ("ignore", "", Warning, "", 0)),
+ (
+ "ignore::DeprecationWarning",
+ False,
+ ("ignore", "", DeprecationWarning, "", 0),
+ ),
+ (
+ "ignore:some msg:DeprecationWarning",
+ False,
+ ("ignore", "some msg", DeprecationWarning, "", 0),
+ ),
+ (
+ "ignore::DeprecationWarning:mod",
+ False,
+ ("ignore", "", DeprecationWarning, "mod", 0),
+ ),
+ (
+ "ignore::DeprecationWarning:mod:42",
+ False,
+ ("ignore", "", DeprecationWarning, "mod", 42),
+ ),
+ ("error:some\\msg:::", True, ("error", "some\\\\msg", Warning, "", 0)),
+ ("error:::mod\\foo:", True, ("error", "", Warning, "mod\\\\foo\\Z", 0)),
+ ],
+)
+def test_parse_warning_filter(
+ arg: str, escape: bool, expected: Tuple[str, str, Type[Warning], str, int]
+) -> None:
+ assert parse_warning_filter(arg, escape=escape) == expected
+
+
+@pytest.mark.parametrize(
+ "arg",
+ [
+ # Too much parts.
+ ":" * 5,
+ # Invalid action.
+ "FOO::",
+ # ImportError when importing the warning class.
+ "::test_parse_warning_filter_failure.NonExistentClass::",
+ # Class is not a Warning subclass.
+ "::list::",
+ # Negative line number.
+ "::::-1",
+ # Not a line number.
+ "::::not-a-number",
+ ],
+)
+def test_parse_warning_filter_failure(arg: str) -> None:
+ with pytest.raises(pytest.UsageError):
+ parse_warning_filter(arg, escape=True)
+
+
+class TestDebugOptions:
+ def test_without_debug_does_not_write_log(self, pytester: Pytester) -> None:
+ result = pytester.runpytest()
+ result.stderr.no_fnmatch_line(
+ "*writing pytest debug information to*pytestdebug.log"
+ )
+ result.stderr.no_fnmatch_line(
+ "*wrote pytest debug information to*pytestdebug.log"
+ )
+ assert not [f.name for f in pytester.path.glob("**/*.log")]
+
+ def test_with_only_debug_writes_pytestdebug_log(self, pytester: Pytester) -> None:
+ result = pytester.runpytest("--debug")
+ result.stderr.fnmatch_lines(
+ [
+ "*writing pytest debug information to*pytestdebug.log",
+ "*wrote pytest debug information to*pytestdebug.log",
+ ]
+ )
+ assert "pytestdebug.log" in [f.name for f in pytester.path.glob("**/*.log")]
+
+ def test_multiple_custom_debug_logs(self, pytester: Pytester) -> None:
+ result = pytester.runpytest("--debug", "bar.log")
+ result.stderr.fnmatch_lines(
+ [
+ "*writing pytest debug information to*bar.log",
+ "*wrote pytest debug information to*bar.log",
+ ]
+ )
+ result = pytester.runpytest("--debug", "foo.log")
+ result.stderr.fnmatch_lines(
+ [
+ "*writing pytest debug information to*foo.log",
+ "*wrote pytest debug information to*foo.log",
+ ]
+ )
+
+ assert {"bar.log", "foo.log"} == {
+ f.name for f in pytester.path.glob("**/*.log")
+ }
+
+ def test_debug_help(self, pytester: Pytester) -> None:
+ 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'.",
+ ]
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_conftest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_conftest.py
new file mode 100644
index 0000000000..64c1014a53
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_conftest.py
@@ -0,0 +1,696 @@
+import argparse
+import os
+import textwrap
+from pathlib import Path
+from typing import cast
+from typing import Dict
+from typing import Generator
+from typing import List
+from typing import Optional
+
+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
+
+
+def ConftestWithSetinitial(path) -> PytestPluginManager:
+ conftest = PytestPluginManager()
+ conftest_setinitial(conftest, [path])
+ return conftest
+
+
+def conftest_setinitial(
+ conftest: PytestPluginManager, args, confcutdir: Optional["os.PathLike[str]"] = 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]))
+
+
+@pytest.mark.usefixtures("_sys_snapshot")
+class TestConftestValueAccessGlobal:
+ @pytest.fixture(scope="module", params=["global", "inpackage"])
+ def basedir(
+ self, request, tmp_path_factory: TempPathFactory
+ ) -> 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")
+ if request.param == "inpackage":
+ tmp_path.joinpath("adir/__init__.py").touch()
+ tmp_path.joinpath("adir/b/__init__.py").touch()
+
+ yield tmp_path
+
+ 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
+ )
+
+ def test_immediate_initialiation_and_incremental_are_the_same(
+ self, basedir: Path
+ ) -> None:
+ conftest = PytestPluginManager()
+ assert not len(conftest._dirpath2confmods)
+ conftest._getconftestmodules(
+ basedir, importmode="prepend", rootpath=Path(basedir)
+ )
+ snap1 = len(conftest._dirpath2confmods)
+ assert snap1 == 1
+ conftest._getconftestmodules(
+ basedir / "adir", importmode="prepend", rootpath=basedir
+ )
+ assert len(conftest._dirpath2confmods) == snap1 + 1
+ conftest._getconftestmodules(
+ basedir / "b", importmode="prepend", rootpath=basedir
+ )
+ 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)
+ )
+
+ 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
+ )
+ assert (
+ conftest._rget_with_confmod(
+ "a", adir / "b", importmode="prepend", rootpath=basedir
+ )[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)
+ )
+ assert value == 1.5
+ path = Path(mod.__file__)
+ assert path.parent == basedir / "adir" / "b"
+ assert path.stem == "conftest"
+
+
+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/b/__init__.py").touch()
+ tmp_path.joinpath("adir-1.0/__init__.py").touch()
+ ConftestWithSetinitial(tmp_path.joinpath("adir-1.0", "b"))
+
+
+def test_doubledash_considered(pytester: Pytester) -> None:
+ conf = pytester.mkdir("--option")
+ conf.joinpath("conftest.py").touch()
+ conftest = PytestPluginManager()
+ conftest_setinitial(conftest, [conf.name, conf.name])
+ values = conftest._getconftestmodules(
+ conf, importmode="prepend", rootpath=pytester.path
+ )
+ assert len(values) == 1
+
+
+def test_issue151_load_all_conftests(pytester: Pytester) -> None:
+ names = "code proj src".split()
+ for name in names:
+ 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)
+
+
+def test_conftest_global_import(pytester: Pytester) -> None:
+ pytester.makeconftest("x=3")
+ p = pytester.makepyfile(
+ """
+ from pathlib import Path
+ import pytest
+ from _pytest.config import PytestPluginManager
+ conf = PytestPluginManager()
+ mod = conf._importconftest(Path("conftest.py"), importmode="prepend", rootpath=Path.cwd())
+ 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())
+ assert mod != mod2
+ assert mod2.y == 4
+ import conftest
+ assert conftest is mod2, (conftest, mod)
+ """
+ )
+ res = pytester.runpython(p)
+ assert res.ret == 0
+
+
+def test_conftestcutdir(pytester: Pytester) -> None:
+ conf = pytester.makeconftest("")
+ p = pytester.mkdir("x")
+ conftest = PytestPluginManager()
+ conftest_setinitial(conftest, [pytester.path], confcutdir=p)
+ values = conftest._getconftestmodules(
+ p, importmode="prepend", rootpath=pytester.path
+ )
+ assert len(values) == 0
+ values = conftest._getconftestmodules(
+ conf.parent, importmode="prepend", rootpath=pytester.path
+ )
+ assert len(values) == 0
+ assert Path(conf) not in conftest._conftestpath2mod
+ # 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
+ )
+ assert values[0].__file__.startswith(str(conf))
+ # and all sub paths get updated properly
+ values = conftest._getconftestmodules(
+ p, importmode="prepend", rootpath=pytester.path
+ )
+ assert len(values) == 1
+ assert values[0].__file__.startswith(str(conf))
+
+
+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
+ )
+ assert len(values) == 1
+ assert values[0].__file__.startswith(str(conf))
+
+
+@pytest.mark.parametrize("name", "test tests whatever .dotdir".split())
+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)
+ key = subconftest.resolve()
+ if name not in ("whatever", ".dotdir"):
+ assert key in conftest._conftestpath2mod
+ assert len(conftest._conftestpath2mod) == 1
+ else:
+ assert key not in conftest._conftestpath2mod
+ assert len(conftest._conftestpath2mod) == 0
+
+
+def test_conftest_confcutdir(pytester: Pytester) -> None:
+ pytester.makeconftest("assert 0")
+ x = pytester.mkdir("x")
+ x.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ def pytest_addoption(parser):
+ parser.addoption("--xyz", action="store_true")
+ """
+ )
+ )
+ 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_conftest_symlink(pytester: Pytester) -> None:
+ """`conftest.py` discovery follows normal path resolution and does not resolve symlinks."""
+ # Structure:
+ # /real
+ # /real/conftest.py
+ # /real/app
+ # /real/app/tests
+ # /real/app/tests/test_foo.py
+
+ # Links:
+ # /symlinktests -> /real/app/tests (running at symlinktests should fail)
+ # /symlink -> /real (running at /symlink should work)
+
+ real = pytester.mkdir("real")
+ realtests = real.joinpath("app/tests")
+ realtests.mkdir(parents=True)
+ symlink_or_skip(realtests, pytester.path.joinpath("symlinktests"))
+ symlink_or_skip(real, pytester.path.joinpath("symlink"))
+ pytester.makepyfile(
+ **{
+ "real/app/tests/test_foo.py": "def test1(fixture): pass",
+ "real/conftest.py": textwrap.dedent(
+ """
+ import pytest
+
+ print("conftest_loaded")
+
+ @pytest.fixture
+ def fixture():
+ print("fixture_used")
+ """
+ ),
+ }
+ )
+
+ # Should fail because conftest cannot be found from the link structure.
+ result = pytester.runpytest("-vs", "symlinktests")
+ result.stdout.fnmatch_lines(["*fixture 'fixture' not found*"])
+ assert result.ret == ExitCode.TESTS_FAILED
+
+ # Should not cause "ValueError: Plugin already registered" (#4174).
+ result = pytester.runpytest("-vs", "symlink")
+ assert result.ret == ExitCode.OK
+
+
+def test_conftest_symlink_files(pytester: Pytester) -> None:
+ """Symlinked conftest.py are found when pytest is executed in a directory with symlinked
+ files."""
+ real = pytester.mkdir("real")
+ source = {
+ "app/test_foo.py": "def test1(fixture): pass",
+ "app/__init__.py": "",
+ "app/conftest.py": textwrap.dedent(
+ """
+ import pytest
+
+ print("conftest_loaded")
+
+ @pytest.fixture
+ def fixture():
+ print("fixture_used")
+ """
+ ),
+ }
+ pytester.makepyfile(**{"real/%s" % k: v for k, v in source.items()})
+
+ # Create a build directory that contains symlinks to actual files
+ # but doesn't symlink actual directories.
+ build = pytester.mkdir("build")
+ build.joinpath("app").mkdir()
+ for f in source:
+ symlink_or_skip(real.joinpath(f), build.joinpath(f))
+ os.chdir(build)
+ result = pytester.runpytest("-vs", "app/test_foo.py")
+ result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"])
+ assert result.ret == ExitCode.OK
+
+
+@pytest.mark.skipif(
+ os.path.normcase("x") != os.path.normcase("X"),
+ 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)."""
+ pytester.path.joinpath("JenkinsRoot/test").mkdir(parents=True)
+ source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""}
+ pytester.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()})
+
+ os.chdir(pytester.path.joinpath("jenkinsroot/test"))
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+
+def test_conftest_uppercase(pytester: Pytester) -> None:
+ """Check conftest.py whose qualified name contains uppercase characters (#5819)"""
+ source = {"__init__.py": "", "Foo/conftest.py": "", "Foo/__init__.py": ""}
+ pytester.makepyfile(**source)
+
+ os.chdir(pytester.path)
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+
+def test_no_conftest(pytester: Pytester) -> None:
+ pytester.makeconftest("assert 0")
+ result = pytester.runpytest("--noconftest")
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.USAGE_ERROR
+
+
+def test_conftest_existing_junitxml(pytester: Pytester) -> None:
+ x = pytester.mkdir("tests")
+ x.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ def pytest_addoption(parser):
+ parser.addoption("--xyz", action="store_true")
+ """
+ )
+ )
+ pytester.makefile(ext=".xml", junit="") # Writes junit.xml
+ result = pytester.runpytest("-h", "--junitxml", "junit.xml")
+ result.stdout.fnmatch_lines(["*--xyz*"])
+
+
+def test_conftest_import_order(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
+ ct1 = pytester.makeconftest("")
+ sub = pytester.mkdir("sub")
+ ct2 = sub / "conftest.py"
+ ct2.write_text("")
+
+ def impct(p, importmode, root):
+ 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),
+ )
+ expected = [ct1, ct2]
+ assert mods == expected
+
+
+def test_fixture_dependency(pytester: Pytester) -> None:
+ pytester.makeconftest("")
+ pytester.path.joinpath("__init__.py").touch()
+ sub = pytester.mkdir("sub")
+ sub.joinpath("__init__.py").touch()
+ sub.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ @pytest.fixture
+ def not_needed():
+ assert False, "Should not be called!"
+
+ @pytest.fixture
+ def foo():
+ assert False, "Should not be called!"
+
+ @pytest.fixture
+ def bar(foo):
+ return 'bar'
+ """
+ )
+ )
+ subsub = sub.joinpath("subsub")
+ subsub.mkdir()
+ subsub.joinpath("__init__.py").touch()
+ subsub.joinpath("test_bar.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ @pytest.fixture
+ def bar():
+ return 'sub bar'
+
+ def test_event_fixture(bar):
+ assert bar == 'sub bar'
+ """
+ )
+ )
+ result = pytester.runpytest("sub")
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+def test_conftest_found_with_double_dash(pytester: Pytester) -> None:
+ sub = pytester.mkdir("sub")
+ sub.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ def pytest_addoption(parser):
+ parser.addoption("--hello-world", action="store_true")
+ """
+ )
+ )
+ p = sub.joinpath("test_hello.py")
+ p.write_text("def test_hello(): pass")
+ result = pytester.runpytest(str(p) + "::test_hello", "-h")
+ result.stdout.fnmatch_lines(
+ """
+ *--hello-world*
+ """
+ )
+
+
+class TestConftestVisibility:
+ def _setup_tree(self, pytester: Pytester) -> Dict[str, Path]: # for issue616
+ # example mostly taken from:
+ # https://mail.python.org/pipermail/pytest-dev/2014-September/002617.html
+ runner = pytester.mkdir("empty")
+ package = pytester.mkdir("package")
+
+ package.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ @pytest.fixture
+ def fxtr():
+ return "from-package"
+ """
+ )
+ )
+ package.joinpath("test_pkgroot.py").write_text(
+ textwrap.dedent(
+ """\
+ def test_pkgroot(fxtr):
+ assert fxtr == "from-package"
+ """
+ )
+ )
+
+ swc = package.joinpath("swc")
+ swc.mkdir()
+ swc.joinpath("__init__.py").touch()
+ swc.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ @pytest.fixture
+ def fxtr():
+ return "from-swc"
+ """
+ )
+ )
+ swc.joinpath("test_with_conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ def test_with_conftest(fxtr):
+ assert fxtr == "from-swc"
+ """
+ )
+ )
+
+ snc = package.joinpath("snc")
+ snc.mkdir()
+ snc.joinpath("__init__.py").touch()
+ snc.joinpath("test_no_conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ def test_no_conftest(fxtr):
+ assert fxtr == "from-package" # No local conftest.py, so should
+ # use value from parent dir's
+ """
+ )
+ )
+ print("created directory structure:")
+ for x in pytester.path.rglob(""):
+ print(" " + str(x.relative_to(pytester.path)))
+
+ return {"runner": runner, "package": package, "swc": swc, "snc": snc}
+
+ # N.B.: "swc" stands for "subdir with conftest.py"
+ # "snc" stands for "subdir no [i.e. without] conftest.py"
+ @pytest.mark.parametrize(
+ "chdir,testarg,expect_ntests_passed",
+ [
+ # Effective target: package/..
+ ("runner", "..", 3),
+ ("package", "..", 3),
+ ("swc", "../..", 3),
+ ("snc", "../..", 3),
+ # Effective target: package
+ ("runner", "../package", 3),
+ ("package", ".", 3),
+ ("swc", "..", 3),
+ ("snc", "..", 3),
+ # Effective target: package/swc
+ ("runner", "../package/swc", 1),
+ ("package", "./swc", 1),
+ ("swc", ".", 1),
+ ("snc", "../swc", 1),
+ # Effective target: package/snc
+ ("runner", "../package/snc", 1),
+ ("package", "./snc", 1),
+ ("swc", "../snc", 1),
+ ("snc", ".", 1),
+ ],
+ )
+ def test_parsefactories_relative_node_ids(
+ self, pytester: Pytester, chdir: str, testarg: str, expect_ntests_passed: int
+ ) -> None:
+ """#616"""
+ dirs = self._setup_tree(pytester)
+ print("pytest run in cwd: %s" % (dirs[chdir].relative_to(pytester.path)))
+ print("pytestarg : %s" % testarg)
+ print("expected pass : %s" % expect_ntests_passed)
+ os.chdir(dirs[chdir])
+ reprec = pytester.inline_run(testarg, "-q", "--traceconfig")
+ reprec.assertoutcome(passed=expect_ntests_passed)
+
+
+@pytest.mark.parametrize(
+ "confcutdir,passed,error", [(".", 2, 0), ("src", 1, 1), (None, 1, 1)]
+)
+def test_search_conftest_up_to_inifile(
+ pytester: Pytester, confcutdir: str, passed: int, error: int
+) -> None:
+ """Test that conftest files are detected only up to an ini file, unless
+ an explicit --confcutdir option is given.
+ """
+ root = pytester.path
+ src = root.joinpath("src")
+ src.mkdir()
+ src.joinpath("pytest.ini").write_text("[pytest]")
+ src.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ @pytest.fixture
+ def fix1(): pass
+ """
+ )
+ )
+ src.joinpath("test_foo.py").write_text(
+ textwrap.dedent(
+ """\
+ def test_1(fix1):
+ pass
+ def test_2(out_of_reach):
+ pass
+ """
+ )
+ )
+ root.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+ @pytest.fixture
+ def out_of_reach(): pass
+ """
+ )
+ )
+
+ args = [str(src)]
+ if confcutdir:
+ args = ["--confcutdir=%s" % root.joinpath(confcutdir)]
+ result = pytester.runpytest(*args)
+ match = ""
+ if passed:
+ match += "*%d passed*" % passed
+ if error:
+ match += "*%d error*" % error
+ result.stdout.fnmatch_lines(match)
+
+
+def test_issue1073_conftest_special_objects(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """\
+ class DontTouchMe(object):
+ def __getattr__(self, x):
+ raise Exception('cant touch me')
+
+ x = DontTouchMe()
+ """
+ )
+ pytester.makepyfile(
+ """\
+ def test_some():
+ pass
+ """
+ )
+ res = pytester.runpytest()
+ assert res.ret == 0
+
+
+def test_conftest_exception_handling(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """\
+ raise ValueError()
+ """
+ )
+ pytester.makepyfile(
+ """\
+ def test_some():
+ pass
+ """
+ )
+ res = pytester.runpytest()
+ assert res.ret == 4
+ assert "raise ValueError()" in [line.strip() for line in res.errlines]
+
+
+def test_hook_proxy(pytester: Pytester) -> None:
+ """Session's gethookproxy() would cache conftests incorrectly (#2016).
+ It was decided to remove the cache altogether.
+ """
+ pytester.makepyfile(
+ **{
+ "root/demo-0/test_foo1.py": "def test1(): pass",
+ "root/demo-a/test_foo2.py": "def test1(): pass",
+ "root/demo-a/conftest.py": """\
+ def pytest_ignore_collect(collection_path, config):
+ return True
+ """,
+ "root/demo-b/test_foo3.py": "def test1(): pass",
+ "root/demo-c/test_foo4.py": "def test1(): pass",
+ }
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ ["*test_foo1.py*", "*test_foo3.py*", "*test_foo4.py*", "*3 passed*"]
+ )
+
+
+def test_required_option_help(pytester: Pytester) -> None:
+ pytester.makeconftest("assert 0")
+ x = pytester.mkdir("x")
+ x.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ def pytest_addoption(parser):
+ parser.addoption("--xyz", action="store_true", required=True)
+ """
+ )
+ )
+ result = pytester.runpytest("-h", x)
+ result.stdout.no_fnmatch_line("*argument --xyz is required*")
+ assert "general:" in result.stdout.str()
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_debugging.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_debugging.py
new file mode 100644
index 0000000000..a822bb57f5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_debugging.py
@@ -0,0 +1,1327 @@
+import os
+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", "")
+
+
+@pytest.fixture(autouse=True)
+def pdb_env(request):
+ if "pytester" in request.fixturenames:
+ # Disable pdb++ with inner tests.
+ pytester = request.getfixturevalue("pytester")
+ pytester._monkeypatch.setenv("PDBPP_HIJACK_PDB", "0")
+
+
+def runpdb_and_get_report(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]
+ assert len(reports) == 3, reports # setup/call/teardown
+ return reports[1]
+
+
+@pytest.fixture
+def custom_pdb_calls() -> List[str]:
+ called = []
+
+ # install dummy debugger class and track which methods were called on it
+ class _CustomPdb:
+ quitting = False
+
+ def __init__(self, *args, **kwargs):
+ called.append("init")
+
+ def reset(self):
+ called.append("reset")
+
+ def interaction(self, *args):
+ called.append("interaction")
+
+ _pytest._CustomPdb = _CustomPdb # type: ignore
+ return called
+
+
+@pytest.fixture
+def custom_debugger_hook():
+ called = []
+
+ # install dummy debugger class and track which methods were called on it
+ class _CustomDebugger:
+ def __init__(self, *args, **kwargs):
+ called.append("init")
+
+ def reset(self):
+ called.append("reset")
+
+ def interaction(self, *args):
+ called.append("interaction")
+
+ def set_trace(self, frame):
+ print("**CustomDebugger**")
+ called.append("set_trace")
+
+ _pytest._CustomDebugger = _CustomDebugger # type: ignore
+ yield called
+ del _pytest._CustomDebugger # type: ignore
+
+
+class TestPDB:
+ @pytest.fixture
+ def pdblist(self, request):
+ monkeypatch = request.getfixturevalue("monkeypatch")
+ pdblist = []
+
+ def mypdb(*args):
+ pdblist.append(args)
+
+ plugin = request.config.pluginmanager.getplugin("debugging")
+ monkeypatch.setattr(plugin, "post_mortem", mypdb)
+ return pdblist
+
+ def test_pdb_on_fail(self, pytester: Pytester, pdblist) -> None:
+ rep = runpdb_and_get_report(
+ pytester,
+ """
+ def test_func():
+ assert 0
+ """,
+ )
+ assert rep.failed
+ assert len(pdblist) == 1
+ tb = _pytest._code.Traceback(pdblist[0][0])
+ assert tb[-1].name == "test_func"
+
+ def test_pdb_on_xfail(self, pytester: Pytester, pdblist) -> None:
+ rep = runpdb_and_get_report(
+ pytester,
+ """
+ import pytest
+ @pytest.mark.xfail
+ def test_func():
+ assert 0
+ """,
+ )
+ assert "xfail" in rep.keywords
+ assert not pdblist
+
+ def test_pdb_on_skip(self, pytester, pdblist) -> None:
+ rep = runpdb_and_get_report(
+ pytester,
+ """
+ import pytest
+ def test_func():
+ pytest.skip("hello")
+ """,
+ )
+ assert rep.skipped
+ assert len(pdblist) == 0
+
+ def test_pdb_on_BdbQuit(self, pytester, pdblist) -> None:
+ rep = runpdb_and_get_report(
+ pytester,
+ """
+ import bdb
+ def test_func():
+ raise bdb.BdbQuit
+ """,
+ )
+ assert rep.failed
+ assert len(pdblist) == 0
+
+ def test_pdb_on_KeyboardInterrupt(self, pytester, pdblist) -> None:
+ rep = runpdb_and_get_report(
+ pytester,
+ """
+ def test_func():
+ raise KeyboardInterrupt
+ """,
+ )
+ assert rep.failed
+ assert len(pdblist) == 1
+
+ @staticmethod
+ def flush(child):
+ if child.isalive():
+ # Read if the test has not (e.g. test_pdb_unittest_skip).
+ child.read()
+ child.wait()
+ assert not child.isalive()
+
+ def test_pdb_unittest_postmortem(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import unittest
+ class Blub(unittest.TestCase):
+ def tearDown(self):
+ self.filename = None
+ def test_false(self):
+ self.filename = 'debug' + '.me'
+ assert 0
+ """
+ )
+ child = pytester.spawn_pytest(f"--pdb {p1}")
+ child.expect("Pdb")
+ child.sendline("p self.filename")
+ child.sendeof()
+ rest = child.read().decode("utf8")
+ assert "debug.me" in rest
+ self.flush(child)
+
+ def test_pdb_unittest_skip(self, pytester: Pytester) -> None:
+ """Test for issue #2137"""
+ p1 = pytester.makepyfile(
+ """
+ import unittest
+ @unittest.skipIf(True, 'Skipping also with pdb active')
+ class MyTestCase(unittest.TestCase):
+ def test_one(self):
+ assert 0
+ """
+ )
+ child = pytester.spawn_pytest(f"-rs --pdb {p1}")
+ child.expect("Skipping also with pdb active")
+ child.expect_exact("= 1 skipped in")
+ child.sendeof()
+ self.flush(child)
+
+ def test_pdb_print_captured_stdout_and_stderr(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def test_1():
+ import sys
+ sys.stderr.write("get\\x20rekt")
+ print("get\\x20rekt")
+ assert False
+
+ def test_not_called_due_to_quit():
+ pass
+ """
+ )
+ child = pytester.spawn_pytest("--pdb %s" % p1)
+ child.expect("captured stdout")
+ child.expect("get rekt")
+ child.expect("captured stderr")
+ child.expect("get rekt")
+ child.expect("traceback")
+ child.expect("def test_1")
+ child.expect("Pdb")
+ child.sendeof()
+ rest = child.read().decode("utf8")
+ assert "Exit: Quitting debugger" in rest
+ assert "= 1 failed in" in rest
+ assert "def test_1" not in rest
+ assert "get rekt" not in rest
+ self.flush(child)
+
+ def test_pdb_dont_print_empty_captured_stdout_and_stderr(
+ self, pytester: Pytester
+ ) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def test_1():
+ assert False
+ """
+ )
+ child = pytester.spawn_pytest("--pdb %s" % p1)
+ child.expect("Pdb")
+ output = child.before.decode("utf8")
+ child.sendeof()
+ assert "captured stdout" not in output
+ assert "captured stderr" not in output
+ self.flush(child)
+
+ @pytest.mark.parametrize("showcapture", ["all", "no", "log"])
+ def test_pdb_print_captured_logs(self, pytester, showcapture: str) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def test_1():
+ import logging
+ logging.warn("get " + "rekt")
+ assert False
+ """
+ )
+ child = pytester.spawn_pytest(f"--show-capture={showcapture} --pdb {p1}")
+ if showcapture in ("all", "log"):
+ child.expect("captured log")
+ child.expect("get rekt")
+ child.expect("Pdb")
+ child.sendeof()
+ rest = child.read().decode("utf8")
+ assert "1 failed" in rest
+ self.flush(child)
+
+ def test_pdb_print_captured_logs_nologging(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def test_1():
+ import logging
+ logging.warn("get " + "rekt")
+ assert False
+ """
+ )
+ child = pytester.spawn_pytest("--show-capture=all --pdb -p no:logging %s" % p1)
+ child.expect("get rekt")
+ output = child.before.decode("utf8")
+ assert "captured log" not in output
+ child.expect("Pdb")
+ child.sendeof()
+ rest = child.read().decode("utf8")
+ assert "1 failed" in rest
+ self.flush(child)
+
+ def test_pdb_interaction_exception(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+ def globalfunc():
+ pass
+ def test_1():
+ pytest.raises(ValueError, globalfunc)
+ """
+ )
+ child = pytester.spawn_pytest("--pdb %s" % p1)
+ child.expect(".*def test_1")
+ child.expect(".*pytest.raises.*globalfunc")
+ child.expect("Pdb")
+ child.sendline("globalfunc")
+ child.expect(".*function")
+ child.sendeof()
+ child.expect("1 failed")
+ self.flush(child)
+
+ def test_pdb_interaction_on_collection_issue181(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+ xxx
+ """
+ )
+ child = pytester.spawn_pytest("--pdb %s" % p1)
+ # child.expect(".*import pytest.*")
+ child.expect("Pdb")
+ child.sendline("c")
+ child.expect("1 error")
+ self.flush(child)
+
+ def test_pdb_interaction_on_internal_error(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_runtest_protocol():
+ 0/0
+ """
+ )
+ p1 = pytester.makepyfile("def test_func(): pass")
+ child = pytester.spawn_pytest("--pdb %s" % p1)
+ child.expect("Pdb")
+
+ # INTERNALERROR is only displayed once via terminal reporter.
+ assert (
+ len(
+ [
+ x
+ for x in child.before.decode().splitlines()
+ if x.startswith("INTERNALERROR> Traceback")
+ ]
+ )
+ == 1
+ )
+
+ child.sendeof()
+ self.flush(child)
+
+ def test_pdb_prevent_ConftestImportFailure_hiding_exception(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile("def test_func(): pass")
+ sub_dir = pytester.path.joinpath("ns")
+ sub_dir.mkdir()
+ sub_dir.joinpath("conftest").with_suffix(".py").write_text(
+ "import unknown", "utf-8"
+ )
+ sub_dir.joinpath("test_file").with_suffix(".py").write_text(
+ "def test_func(): pass", "utf-8"
+ )
+
+ result = pytester.runpytest_subprocess("--pdb", ".")
+ result.stdout.fnmatch_lines(["-> import unknown"])
+
+ def test_pdb_interaction_capturing_simple(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+ def test_1():
+ i = 0
+ print("hello17")
+ pytest.set_trace()
+ i == 1
+ assert 0
+ """
+ )
+ child = pytester.spawn_pytest(str(p1))
+ child.expect(r"test_1\(\)")
+ child.expect("i == 1")
+ child.expect("Pdb")
+ child.sendline("c")
+ rest = child.read().decode("utf-8")
+ assert "AssertionError" in rest
+ assert "1 failed" in rest
+ assert "def test_1" in rest
+ assert "hello17" in rest # out is captured
+ self.flush(child)
+
+ def test_pdb_set_trace_kwargs(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+ def test_1():
+ i = 0
+ print("hello17")
+ pytest.set_trace(header="== my_header ==")
+ x = 3
+ assert 0
+ """
+ )
+ child = pytester.spawn_pytest(str(p1))
+ child.expect("== my_header ==")
+ assert "PDB set_trace" not in child.before.decode()
+ child.expect("Pdb")
+ child.sendline("c")
+ rest = child.read().decode("utf-8")
+ assert "1 failed" in rest
+ assert "def test_1" in rest
+ assert "hello17" in rest # out is captured
+ self.flush(child)
+
+ def test_pdb_set_trace_interception(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pdb
+ def test_1():
+ pdb.set_trace()
+ """
+ )
+ child = pytester.spawn_pytest(str(p1))
+ child.expect("test_1")
+ child.expect("Pdb")
+ child.sendline("q")
+ rest = child.read().decode("utf8")
+ assert "no tests ran" in rest
+ assert "reading from stdin while output" not in rest
+ assert "BdbQuit" not in rest
+ self.flush(child)
+
+ def test_pdb_and_capsys(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+ def test_1(capsys):
+ print("hello1")
+ pytest.set_trace()
+ """
+ )
+ child = pytester.spawn_pytest(str(p1))
+ child.expect("test_1")
+ child.send("capsys.readouterr()\n")
+ child.expect("hello1")
+ child.sendeof()
+ child.read()
+ self.flush(child)
+
+ def test_pdb_with_caplog_on_pdb_invocation(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def test_1(capsys, caplog):
+ import logging
+ logging.getLogger(__name__).warning("some_warning")
+ assert 0
+ """
+ )
+ child = pytester.spawn_pytest("--pdb %s" % str(p1))
+ child.send("caplog.record_tuples\n")
+ child.expect_exact(
+ "[('test_pdb_with_caplog_on_pdb_invocation', 30, 'some_warning')]"
+ )
+ child.sendeof()
+ child.read()
+ self.flush(child)
+
+ def test_set_trace_capturing_afterwards(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pdb
+ def test_1():
+ pdb.set_trace()
+ def test_2():
+ print("hello")
+ assert 0
+ """
+ )
+ child = pytester.spawn_pytest(str(p1))
+ child.expect("test_1")
+ child.send("c\n")
+ child.expect("test_2")
+ child.expect("Captured")
+ child.expect("hello")
+ child.sendeof()
+ child.read()
+ self.flush(child)
+
+ def test_pdb_interaction_doctest(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def function_1():
+ '''
+ >>> i = 0
+ >>> assert i == 1
+ '''
+ """
+ )
+ child = pytester.spawn_pytest("--doctest-modules --pdb %s" % p1)
+ child.expect("Pdb")
+
+ assert "UNEXPECTED EXCEPTION: AssertionError()" in child.before.decode("utf8")
+
+ child.sendline("'i=%i.' % i")
+ child.expect("Pdb")
+ assert "\r\n'i=0.'\r\n" in child.before.decode("utf8")
+
+ child.sendeof()
+ rest = child.read().decode("utf8")
+ assert "! _pytest.outcomes.Exit: Quitting debugger !" in rest
+ assert "BdbQuit" not in rest
+ assert "1 failed" in rest
+ self.flush(child)
+
+ def test_doctest_set_trace_quit(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def function_1():
+ '''
+ >>> __import__('pdb').set_trace()
+ '''
+ """
+ )
+ # NOTE: does not use pytest.set_trace, but Python's patched pdb,
+ # therefore "-s" is required.
+ child = pytester.spawn_pytest("--doctest-modules --pdb -s %s" % p1)
+ child.expect("Pdb")
+ child.sendline("q")
+ rest = child.read().decode("utf8")
+
+ assert "! _pytest.outcomes.Exit: Quitting debugger !" in rest
+ assert "= no tests ran in" in rest
+ assert "BdbQuit" not in rest
+ assert "UNEXPECTED EXCEPTION" not in rest
+
+ def test_pdb_interaction_capturing_twice(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+ def test_1():
+ i = 0
+ print("hello17")
+ pytest.set_trace()
+ x = 3
+ print("hello18")
+ pytest.set_trace()
+ x = 4
+ assert 0
+ """
+ )
+ child = pytester.spawn_pytest(str(p1))
+ child.expect(r"PDB set_trace \(IO-capturing turned off\)")
+ child.expect("test_1")
+ child.expect("x = 3")
+ child.expect("Pdb")
+ child.sendline("c")
+ child.expect(r"PDB continue \(IO-capturing resumed\)")
+ child.expect(r"PDB set_trace \(IO-capturing turned off\)")
+ child.expect("x = 4")
+ child.expect("Pdb")
+ child.sendline("c")
+ child.expect("_ test_1 _")
+ child.expect("def test_1")
+ rest = child.read().decode("utf8")
+ assert "Captured stdout call" in rest
+ assert "hello17" in rest # out is captured
+ assert "hello18" in rest # out is captured
+ assert "1 failed" in rest
+ self.flush(child)
+
+ 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.
+ """
+ p1 = pytester.makepyfile(
+ mytest="""
+ import pdb
+ import pytest
+
+ count_continue = 0
+
+ class CustomPdb(pdb.Pdb, object):
+ def do_debug(self, arg):
+ import sys
+ import types
+
+ do_debug_func = pdb.Pdb.do_debug
+
+ newglobals = do_debug_func.__globals__.copy()
+ newglobals['Pdb'] = self.__class__
+ orig_do_debug = types.FunctionType(
+ do_debug_func.__code__, newglobals,
+ do_debug_func.__name__, do_debug_func.__defaults__,
+ )
+ return orig_do_debug(self, arg)
+ do_debug.__doc__ = pdb.Pdb.do_debug.__doc__
+
+ def do_continue(self, *args, **kwargs):
+ global count_continue
+ count_continue += 1
+ return super(CustomPdb, self).do_continue(*args, **kwargs)
+
+ def foo():
+ print("print_from_foo")
+
+ def test_1():
+ i = 0
+ print("hello17")
+ pytest.set_trace()
+ x = 3
+ print("hello18")
+
+ assert count_continue == 2, "unexpected_failure: %d != 2" % count_continue
+ pytest.fail("expected_failure")
+ """
+ )
+ child = pytester.spawn_pytest("--pdbcls=mytest:CustomPdb %s" % str(p1))
+ child.expect(r"PDB set_trace \(IO-capturing turned off\)")
+ child.expect(r"\n\(Pdb")
+ child.sendline("debug foo()")
+ child.expect("ENTERING RECURSIVE DEBUGGER")
+ child.expect(r"\n\(\(Pdb")
+ child.sendline("c")
+ child.expect("LEAVING RECURSIVE DEBUGGER")
+ assert b"PDB continue" not in child.before
+ # No extra newline.
+ assert child.before.endswith(b"c\r\nprint_from_foo\r\n")
+
+ # set_debug should not raise outcomes. Exit, if used recursively.
+ child.sendline("debug 42")
+ child.sendline("q")
+ child.expect("LEAVING RECURSIVE DEBUGGER")
+ assert b"ENTERING RECURSIVE DEBUGGER" in child.before
+ assert b"Quitting debugger" not in child.before
+
+ child.sendline("c")
+ child.expect(r"PDB continue \(IO-capturing resumed\)")
+ rest = child.read().decode("utf8")
+ assert "hello17" in rest # out is captured
+ assert "hello18" in rest # out is captured
+ assert "1 failed" in rest
+ assert "Failed: expected_failure" in rest
+ assert "AssertionError: unexpected_failure" not in rest
+ self.flush(child)
+
+ def test_pdb_without_capture(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+ def test_1():
+ pytest.set_trace()
+ """
+ )
+ child = pytester.spawn_pytest("-s %s" % p1)
+ child.expect(r">>> PDB set_trace >>>")
+ child.expect("Pdb")
+ child.sendline("c")
+ child.expect(r">>> PDB continue >>>")
+ child.expect("1 passed")
+ self.flush(child)
+
+ @pytest.mark.parametrize("capture_arg", ("", "-s", "-p no:capture"))
+ def test_pdb_continue_with_recursive_debug(
+ self, capture_arg, pytester: Pytester
+ ) -> None:
+ """Full coverage for do_debug without capturing.
+
+ This is very similar to test_pdb_interaction_continue_recursive in general,
+ but mocks out ``pdb.set_trace`` for providing more coverage.
+ """
+ p1 = pytester.makepyfile(
+ """
+ try:
+ input = raw_input
+ except NameError:
+ pass
+
+ def set_trace():
+ __import__('pdb').set_trace()
+
+ def test_1(monkeypatch):
+ import _pytest.debugging
+
+ class pytestPDBTest(_pytest.debugging.pytestPDB):
+ @classmethod
+ def set_trace(cls, *args, **kwargs):
+ # Init PytestPdbWrapper to handle capturing.
+ _pdb = cls._init_pdb("set_trace", *args, **kwargs)
+
+ # Mock out pdb.Pdb.do_continue.
+ import pdb
+ pdb.Pdb.do_continue = lambda self, arg: None
+
+ print("===" + " SET_TRACE ===")
+ assert input() == "debug set_trace()"
+
+ # Simulate PytestPdbWrapper.do_debug
+ cls._recursive_debug += 1
+ print("ENTERING RECURSIVE DEBUGGER")
+ print("===" + " SET_TRACE_2 ===")
+
+ assert input() == "c"
+ _pdb.do_continue("")
+ print("===" + " SET_TRACE_3 ===")
+
+ # Simulate PytestPdbWrapper.do_debug
+ print("LEAVING RECURSIVE DEBUGGER")
+ cls._recursive_debug -= 1
+
+ print("===" + " SET_TRACE_4 ===")
+ assert input() == "c"
+ _pdb.do_continue("")
+
+ def do_continue(self, arg):
+ print("=== do_continue")
+
+ monkeypatch.setattr(_pytest.debugging, "pytestPDB", pytestPDBTest)
+
+ import pdb
+ monkeypatch.setattr(pdb, "set_trace", pytestPDBTest.set_trace)
+
+ set_trace()
+ """
+ )
+ child = pytester.spawn_pytest(f"--tb=short {p1} {capture_arg}")
+ child.expect("=== SET_TRACE ===")
+ before = child.before.decode("utf8")
+ if not capture_arg:
+ assert ">>> PDB set_trace (IO-capturing turned off) >>>" in before
+ else:
+ assert ">>> PDB set_trace >>>" in before
+ child.sendline("debug set_trace()")
+ child.expect("=== SET_TRACE_2 ===")
+ before = child.before.decode("utf8")
+ assert "\r\nENTERING RECURSIVE DEBUGGER\r\n" in before
+ child.sendline("c")
+ child.expect("=== SET_TRACE_3 ===")
+
+ # No continue message with recursive debugging.
+ before = child.before.decode("utf8")
+ assert ">>> PDB continue " not in before
+
+ child.sendline("c")
+ child.expect("=== SET_TRACE_4 ===")
+ before = child.before.decode("utf8")
+ assert "\r\nLEAVING RECURSIVE DEBUGGER\r\n" in before
+ child.sendline("c")
+ rest = child.read().decode("utf8")
+ if not capture_arg:
+ assert "> PDB continue (IO-capturing resumed) >" in rest
+ else:
+ assert "> PDB continue >" in rest
+ assert "= 1 passed in" in rest
+
+ def test_pdb_used_outside_test(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+ pytest.set_trace()
+ x = 5
+ """
+ )
+ child = pytester.spawn(f"{sys.executable} {p1}")
+ child.expect("x = 5")
+ child.expect("Pdb")
+ child.sendeof()
+ self.flush(child)
+
+ def test_pdb_used_in_generate_tests(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+ def pytest_generate_tests(metafunc):
+ pytest.set_trace()
+ x = 5
+ def test_foo(a):
+ pass
+ """
+ )
+ child = pytester.spawn_pytest(str(p1))
+ child.expect("x = 5")
+ child.expect("Pdb")
+ child.sendeof()
+ self.flush(child)
+
+ def test_pdb_collection_failure_is_shown(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile("xxx")
+ result = pytester.runpytest_subprocess("--pdb", p1)
+ result.stdout.fnmatch_lines(
+ ["E NameError: *xxx*", "*! *Exit: Quitting debugger !*"] # due to EOF
+ )
+
+ @pytest.mark.parametrize("post_mortem", (False, True))
+ def test_enter_leave_pdb_hooks_are_called(
+ self, post_mortem, pytester: Pytester
+ ) -> None:
+ pytester.makeconftest(
+ """
+ mypdb = None
+
+ def pytest_configure(config):
+ config.testing_verification = 'configured'
+
+ def pytest_enter_pdb(config, pdb):
+ assert config.testing_verification == 'configured'
+ print('enter_pdb_hook')
+
+ global mypdb
+ mypdb = pdb
+ mypdb.set_attribute = "bar"
+
+ def pytest_leave_pdb(config, pdb):
+ assert config.testing_verification == 'configured'
+ print('leave_pdb_hook')
+
+ global mypdb
+ assert mypdb is pdb
+ assert mypdb.set_attribute == "bar"
+ """
+ )
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+
+ def test_set_trace():
+ pytest.set_trace()
+ assert 0
+
+ def test_post_mortem():
+ assert 0
+ """
+ )
+ if post_mortem:
+ child = pytester.spawn_pytest(str(p1) + " --pdb -s -k test_post_mortem")
+ else:
+ child = pytester.spawn_pytest(str(p1) + " -k test_set_trace")
+ child.expect("enter_pdb_hook")
+ child.sendline("c")
+ if post_mortem:
+ child.expect(r"PDB continue")
+ else:
+ child.expect(r"PDB continue \(IO-capturing resumed\)")
+ child.expect("Captured stdout call")
+ rest = child.read().decode("utf8")
+ assert "leave_pdb_hook" in rest
+ assert "1 failed" in rest
+ self.flush(child)
+
+ def test_pdb_custom_cls(
+ self, pytester: Pytester, custom_pdb_calls: List[str]
+ ) -> None:
+ p1 = pytester.makepyfile("""xxx """)
+ result = pytester.runpytest_inprocess(
+ "--pdb", "--pdbcls=_pytest:_CustomPdb", p1
+ )
+ result.stdout.fnmatch_lines(["*NameError*xxx*", "*1 error*"])
+ assert custom_pdb_calls == ["init", "reset", "interaction"]
+
+ def test_pdb_custom_cls_invalid(self, pytester: Pytester) -> None:
+ result = pytester.runpytest_inprocess("--pdbcls=invalid")
+ result.stderr.fnmatch_lines(
+ [
+ "*: error: argument --pdbcls: 'invalid' is not in the format 'modname:classname'"
+ ]
+ )
+
+ def test_pdb_validate_usepdb_cls(self):
+ assert _validate_usepdb_cls("os.path:dirname.__name__") == (
+ "os.path",
+ "dirname.__name__",
+ )
+
+ assert _validate_usepdb_cls("pdb:DoesNotExist") == ("pdb", "DoesNotExist")
+
+ def test_pdb_custom_cls_without_pdb(
+ self, pytester: Pytester, custom_pdb_calls: List[str]
+ ) -> None:
+ p1 = pytester.makepyfile("""xxx """)
+ result = pytester.runpytest_inprocess("--pdbcls=_pytest:_CustomPdb", p1)
+ result.stdout.fnmatch_lines(["*NameError*xxx*", "*1 error*"])
+ assert custom_pdb_calls == []
+
+ def test_pdb_custom_cls_with_set_trace(
+ self,
+ pytester: Pytester,
+ monkeypatch: MonkeyPatch,
+ ) -> None:
+ pytester.makepyfile(
+ custom_pdb="""
+ class CustomPdb(object):
+ def __init__(self, *args, **kwargs):
+ skip = kwargs.pop("skip")
+ assert skip == ["foo.*"]
+ print("__init__")
+ super(CustomPdb, self).__init__(*args, **kwargs)
+
+ def set_trace(*args, **kwargs):
+ print('custom set_trace>')
+ """
+ )
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+
+ def test_foo():
+ pytest.set_trace(skip=['foo.*'])
+ """
+ )
+ monkeypatch.setenv("PYTHONPATH", str(pytester.path))
+ child = pytester.spawn_pytest("--pdbcls=custom_pdb:CustomPdb %s" % str(p1))
+
+ child.expect("__init__")
+ child.expect("custom set_trace>")
+ self.flush(child)
+
+
+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
+ ) -> None:
+ """
+ Test that sys.breakpointhook is set to the custom Pdb class once configured, test that
+ hook is reset to system value once pytest has been unconfigured
+ """
+ pytester.makeconftest(
+ """
+ import sys
+ from pytest import hookimpl
+ from _pytest.debugging import pytestPDB
+
+ def pytest_configure(config):
+ config.add_cleanup(check_restored)
+
+ def check_restored():
+ assert sys.breakpointhook == sys.__breakpointhook__
+
+ def test_check():
+ assert sys.breakpointhook == pytestPDB.set_trace
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_nothing(): pass
+ """
+ )
+ args = (arg,) if arg else ()
+ 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:
+ p1 = pytester.makepyfile(
+ """
+ def test_nothing():
+ breakpoint()
+ """
+ )
+ result = pytester.runpytest_inprocess(
+ "--pdb", "--pdbcls=_pytest:_CustomDebugger", p1
+ )
+ result.stdout.fnmatch_lines(["*CustomDebugger*", "*1 passed*"])
+ 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:
+ pytester.makeconftest(
+ """
+ import os
+ import sys
+
+ os.environ['PYTHONBREAKPOINT'] = '_pytest._CustomDebugger.set_trace'
+
+ def pytest_configure(config):
+ config.add_cleanup(check_restored)
+
+ def check_restored():
+ assert sys.breakpointhook == sys.__breakpointhook__
+
+ def test_check():
+ import _pytest
+ assert sys.breakpointhook is _pytest._CustomDebugger.set_trace
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_nothing(): pass
+ """
+ )
+ args = (arg,) if arg else ()
+ 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:
+ p1 = pytester.makepyfile(
+ """
+ def test_1():
+ breakpoint()
+ """
+ )
+ child = pytester.spawn_pytest(str(p1))
+ child.expect("test_1")
+ child.expect("Pdb")
+ child.sendline("quit")
+ rest = child.read().decode("utf8")
+ assert "Quitting debugger" in rest
+ assert "reading from stdin while output" not in rest
+ TestPDB.flush(child)
+
+ @pytest.mark.skipif(
+ not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin"
+ )
+ def test_pdb_not_altered(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pdb
+ def test_1():
+ pdb.set_trace()
+ assert 0
+ """
+ )
+ child = pytester.spawn_pytest(str(p1))
+ child.expect("test_1")
+ child.expect("Pdb")
+ child.sendline("c")
+ rest = child.read().decode("utf8")
+ assert "1 failed" in rest
+ assert "reading from stdin while output" not in rest
+ TestPDB.flush(child)
+
+
+class TestTraceOption:
+ def test_trace_sets_breakpoint(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def test_1():
+ assert True
+
+ def test_2():
+ pass
+
+ def test_3():
+ pass
+ """
+ )
+ child = pytester.spawn_pytest("--trace " + str(p1))
+ child.expect("test_1")
+ child.expect("Pdb")
+ child.sendline("c")
+ child.expect("test_2")
+ child.expect("Pdb")
+ child.sendline("c")
+ child.expect("test_3")
+ child.expect("Pdb")
+ child.sendline("q")
+ child.expect_exact("Exit: Quitting debugger")
+ rest = child.read().decode("utf8")
+ assert "= 2 passed in" in rest
+ assert "reading from stdin while output" not in rest
+ # Only printed once - not on stderr.
+ assert "Exit: Quitting debugger" not in child.before.decode("utf8")
+ TestPDB.flush(child)
+
+ def test_trace_with_parametrize_handles_shared_fixtureinfo(
+ self, pytester: Pytester
+ ) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize('myparam', [1,2])
+ def test_1(myparam, request):
+ assert myparam in (1, 2)
+ assert request.function.__name__ == "test_1"
+ @pytest.mark.parametrize('func', [1,2])
+ def test_func(func, request):
+ assert func in (1, 2)
+ assert request.function.__name__ == "test_func"
+ @pytest.mark.parametrize('myparam', [1,2])
+ def test_func_kw(myparam, request, func="func_kw"):
+ assert myparam in (1, 2)
+ assert func == "func_kw"
+ assert request.function.__name__ == "test_func_kw"
+ """
+ )
+ child = pytester.spawn_pytest("--trace " + str(p1))
+ for func, argname in [
+ ("test_1", "myparam"),
+ ("test_func", "func"),
+ ("test_func_kw", "myparam"),
+ ]:
+ child.expect_exact("> PDB runcall (IO-capturing turned off) >")
+ child.expect_exact(func)
+ child.expect_exact("Pdb")
+ child.sendline("args")
+ child.expect_exact(f"{argname} = 1\r\n")
+ child.expect_exact("Pdb")
+ child.sendline("c")
+ child.expect_exact("Pdb")
+ child.sendline("args")
+ child.expect_exact(f"{argname} = 2\r\n")
+ child.expect_exact("Pdb")
+ child.sendline("c")
+ child.expect_exact("> PDB continue (IO-capturing resumed) >")
+ rest = child.read().decode("utf8")
+ assert "= 6 passed in" in rest
+ assert "reading from stdin while output" not in rest
+ # Only printed once - not on stderr.
+ assert "Exit: Quitting debugger" not in child.before.decode("utf8")
+ TestPDB.flush(child)
+
+
+def test_trace_after_runpytest(pytester: Pytester) -> None:
+ """Test that debugging's pytest_configure is re-entrant."""
+ p1 = pytester.makepyfile(
+ """
+ from _pytest.debugging import pytestPDB
+
+ def test_outer(pytester) -> None:
+ assert len(pytestPDB._saved) == 1
+
+ pytester.makepyfile(
+ \"""
+ from _pytest.debugging import pytestPDB
+
+ def test_inner():
+ assert len(pytestPDB._saved) == 2
+ print()
+ print("test_inner_" + "end")
+ \"""
+ )
+
+ result = pytester.runpytest("-s", "-k", "test_inner")
+ assert result.ret == 0
+
+ assert len(pytestPDB._saved) == 1
+ """
+ )
+ result = pytester.runpytest_subprocess("-s", "-p", "pytester", str(p1))
+ result.stdout.fnmatch_lines(["test_inner_end"])
+ assert result.ret == 0
+
+
+def test_quit_with_swallowed_SystemExit(pytester: Pytester) -> None:
+ """Test that debugging's pytest_configure is re-entrant."""
+ p1 = pytester.makepyfile(
+ """
+ def call_pdb_set_trace():
+ __import__('pdb').set_trace()
+
+
+ def test_1():
+ try:
+ call_pdb_set_trace()
+ except SystemExit:
+ pass
+
+
+ def test_2():
+ pass
+ """
+ )
+ child = pytester.spawn_pytest(str(p1))
+ child.expect("Pdb")
+ child.sendline("q")
+ child.expect_exact("Exit: Quitting debugger")
+ rest = child.read().decode("utf8")
+ assert "no tests ran" in rest
+ TestPDB.flush(child)
+
+
+@pytest.mark.parametrize("fixture", ("capfd", "capsys"))
+def test_pdb_suspends_fixture_capturing(pytester: Pytester, fixture: str) -> None:
+ """Using "-s" with pytest should suspend/resume fixture capturing."""
+ p1 = pytester.makepyfile(
+ """
+ def test_inner({fixture}):
+ import sys
+
+ print("out_inner_before")
+ sys.stderr.write("err_inner_before\\n")
+
+ __import__("pdb").set_trace()
+
+ print("out_inner_after")
+ sys.stderr.write("err_inner_after\\n")
+
+ 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")
+
+ child.expect("Pdb")
+ before = child.before.decode("utf8")
+ assert (
+ "> PDB set_trace (IO-capturing turned off for fixture %s) >" % (fixture)
+ in before
+ )
+
+ # Test that capturing is really suspended.
+ child.sendline("p 40 + 2")
+ child.expect("Pdb")
+ assert "\r\n42\r\n" in child.before.decode("utf8")
+
+ child.sendline("c")
+ rest = child.read().decode("utf8")
+ assert "out_inner" not in rest
+ assert "err_inner" not in rest
+
+ TestPDB.flush(child)
+ assert child.exitstatus == 0
+ assert "= 1 passed in" in rest
+ assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest
+
+
+def test_pdbcls_via_local_module(pytester: Pytester) -> None:
+ """It should be imported in pytest_configure or later only."""
+ p1 = pytester.makepyfile(
+ """
+ def test():
+ print("before_set_trace")
+ __import__("pdb").set_trace()
+ """,
+ mypdb="""
+ class Wrapped:
+ class MyPdb:
+ def set_trace(self, *args):
+ print("set_trace_called", args)
+
+ def runcall(self, *args, **kwds):
+ print("runcall_called", args, kwds)
+ """,
+ )
+ result = pytester.runpytest(
+ str(p1), "--pdbcls=really.invalid:Value", syspathinsert=True
+ )
+ result.stdout.fnmatch_lines(
+ [
+ "*= FAILURES =*",
+ "E * --pdbcls: could not import 'really.invalid:Value': No module named *really*",
+ ]
+ )
+ assert result.ret == 1
+
+ result = pytester.runpytest(
+ str(p1), "--pdbcls=mypdb:Wrapped.MyPdb", syspathinsert=True
+ )
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*set_trace_called*", "* 1 passed in *"])
+
+ # Ensure that it also works with --trace.
+ result = pytester.runpytest(
+ str(p1), "--pdbcls=mypdb:Wrapped.MyPdb", "--trace", syspathinsert=True
+ )
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*runcall_called*", "* 1 passed in *"])
+
+
+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):
+ raise EOFError()
+
+ def test(monkeypatch):
+ import builtins
+ monkeypatch.setattr(builtins, "input", input_without_read)
+ __import__('pdb').set_trace()
+ """
+ )
+ result = pytester.runpytest(str(p1))
+ result.stdout.fnmatch_lines(["E *BdbQuit", "*= 1 failed in*"])
+ assert result.ret == 1
+
+
+def test_pdb_wrapper_class_is_reused(pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def test():
+ __import__("pdb").set_trace()
+ __import__("pdb").set_trace()
+
+ import mypdb
+ instances = mypdb.instances
+ assert len(instances) == 2
+ assert instances[0].__class__ is instances[1].__class__
+ """,
+ mypdb="""
+ instances = []
+
+ class MyPdb:
+ def __init__(self, *args, **kwargs):
+ instances.append(self)
+
+ def set_trace(self, *args):
+ print("set_trace_called", args)
+ """,
+ )
+ result = pytester.runpytest(str(p1), "--pdbcls=mypdb:MyPdb", syspathinsert=True)
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(
+ ["*set_trace_called*", "*set_trace_called*", "* 1 passed in *"]
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_doctest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_doctest.py
new file mode 100644
index 0000000000..67b8ccdb7e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_doctest.py
@@ -0,0 +1,1572 @@
+import inspect
+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
+from _pytest.doctest import _is_setup_py
+from _pytest.doctest import _patch_unwrap_mock_aware
+from _pytest.doctest import DoctestItem
+from _pytest.doctest import DoctestModule
+from _pytest.doctest import DoctestTextfile
+from _pytest.pytester import Pytester
+
+
+class TestDoctests:
+ def test_collect_testtextfile(self, pytester: Pytester):
+ w = pytester.maketxtfile(whatever="")
+ checkfile = pytester.maketxtfile(
+ test_something="""
+ alskdjalsdk
+ >>> i = 5
+ >>> i-1
+ 4
+ """
+ )
+
+ for x in (pytester.path, checkfile):
+ # print "checking that %s returns custom items" % (x,)
+ items, reprec = pytester.inline_genitems(x)
+ assert len(items) == 1
+ assert isinstance(items[0], DoctestItem)
+ assert isinstance(items[0].parent, DoctestTextfile)
+ # Empty file has no items.
+ items, reprec = pytester.inline_genitems(w)
+ assert len(items) == 0
+
+ def test_collect_module_empty(self, pytester: Pytester):
+ path = pytester.makepyfile(whatever="#")
+ for p in (path, pytester.path):
+ items, reprec = pytester.inline_genitems(p, "--doctest-modules")
+ assert len(items) == 0
+
+ def test_collect_module_single_modulelevel_doctest(self, pytester: Pytester):
+ path = pytester.makepyfile(whatever='""">>> pass"""')
+ for p in (path, pytester.path):
+ items, reprec = pytester.inline_genitems(p, "--doctest-modules")
+ assert len(items) == 1
+ assert isinstance(items[0], DoctestItem)
+ assert isinstance(items[0].parent, DoctestModule)
+
+ def test_collect_module_two_doctest_one_modulelevel(self, pytester: Pytester):
+ path = pytester.makepyfile(
+ whatever="""
+ '>>> x = None'
+ def my_func():
+ ">>> magic = 42 "
+ """
+ )
+ for p in (path, pytester.path):
+ items, reprec = pytester.inline_genitems(p, "--doctest-modules")
+ assert len(items) == 2
+ assert isinstance(items[0], DoctestItem)
+ assert isinstance(items[1], DoctestItem)
+ assert isinstance(items[0].parent, DoctestModule)
+ assert items[0].parent is items[1].parent
+
+ @pytest.mark.parametrize("filename", ["__init__", "whatever"])
+ def test_collect_module_two_doctest_no_modulelevel(
+ self,
+ pytester: Pytester,
+ filename: str,
+ ) -> None:
+ path = pytester.makepyfile(
+ **{
+ filename: """
+ '# Empty'
+ def my_func():
+ ">>> magic = 42 "
+ def useless():
+ '''
+ # This is a function
+ # >>> # it doesn't have any doctest
+ '''
+ def another():
+ '''
+ # This is another function
+ >>> import os # this one does have a doctest
+ '''
+ """,
+ },
+ )
+ for p in (path, pytester.path):
+ items, reprec = pytester.inline_genitems(p, "--doctest-modules")
+ assert len(items) == 2
+ assert isinstance(items[0], DoctestItem)
+ assert isinstance(items[1], DoctestItem)
+ assert isinstance(items[0].parent, DoctestModule)
+ assert items[0].parent is items[1].parent
+
+ def test_simple_doctestfile(self, pytester: Pytester):
+ p = pytester.maketxtfile(
+ test_doc="""
+ >>> x = 1
+ >>> x == 1
+ False
+ """
+ )
+ reprec = pytester.inline_run(p)
+ reprec.assertoutcome(failed=1)
+
+ def test_new_pattern(self, pytester: Pytester):
+ p = pytester.maketxtfile(
+ xdoc="""
+ >>> x = 1
+ >>> x == 1
+ False
+ """
+ )
+ reprec = pytester.inline_run(p, "--doctest-glob=x*.txt")
+ reprec.assertoutcome(failed=1)
+
+ def test_multiple_patterns(self, pytester: Pytester):
+ """Test support for multiple --doctest-glob arguments (#1255)."""
+ pytester.maketxtfile(
+ xdoc="""
+ >>> 1
+ 1
+ """
+ )
+ pytester.makefile(
+ ".foo",
+ test="""
+ >>> 1
+ 1
+ """,
+ )
+ pytester.maketxtfile(
+ test_normal="""
+ >>> 1
+ 1
+ """
+ )
+ expected = {"xdoc.txt", "test.foo", "test_normal.txt"}
+ assert {x.name for x in pytester.path.iterdir()} == expected
+ args = ["--doctest-glob=xdoc*.txt", "--doctest-glob=*.foo"]
+ result = pytester.runpytest(*args)
+ result.stdout.fnmatch_lines(["*test.foo *", "*xdoc.txt *", "*2 passed*"])
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*test_normal.txt *", "*1 passed*"])
+
+ @pytest.mark.parametrize(
+ " test_string, encoding",
+ [("foo", "ascii"), ("öäü", "latin1"), ("öäü", "utf-8")],
+ )
+ def test_encoding(self, pytester, test_string, encoding):
+ """Test support for doctest_encoding ini option."""
+ pytester.makeini(
+ """
+ [pytest]
+ doctest_encoding={}
+ """.format(
+ encoding
+ )
+ )
+ doctest = """
+ >>> "{}"
+ {}
+ """.format(
+ test_string, repr(test_string)
+ )
+ fn = pytester.path / "test_encoding.txt"
+ fn.write_text(doctest, encoding=encoding)
+
+ result = pytester.runpytest()
+
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_doctest_unexpected_exception(self, pytester: Pytester):
+ pytester.maketxtfile(
+ """
+ >>> i = 0
+ >>> 0 / i
+ 2
+ """
+ )
+ result = pytester.runpytest("--doctest-modules")
+ result.stdout.fnmatch_lines(
+ [
+ "test_doctest_unexpected_exception.txt F *",
+ "",
+ "*= FAILURES =*",
+ "*_ [[]doctest[]] test_doctest_unexpected_exception.txt _*",
+ "001 >>> i = 0",
+ "002 >>> 0 / i",
+ "UNEXPECTED EXCEPTION: ZeroDivisionError*",
+ "Traceback (most recent call last):",
+ ' File "*/doctest.py", line *, in __run',
+ " *",
+ *((" *^^^^*",) if sys.version_info >= (3, 11) else ()),
+ ' File "<doctest test_doctest_unexpected_exception.txt[1]>", line 1, in <module>',
+ "ZeroDivisionError: division by zero",
+ "*/test_doctest_unexpected_exception.txt:2: UnexpectedException",
+ ],
+ consecutive=True,
+ )
+
+ def test_doctest_outcomes(self, pytester: Pytester):
+ pytester.maketxtfile(
+ test_skip="""
+ >>> 1
+ 1
+ >>> import pytest
+ >>> pytest.skip("")
+ >>> 2
+ 3
+ """,
+ test_xfail="""
+ >>> import pytest
+ >>> pytest.xfail("xfail_reason")
+ >>> foo
+ bar
+ """,
+ test_importorskip="""
+ >>> import pytest
+ >>> pytest.importorskip("doesnotexist")
+ >>> foo
+ bar
+ """,
+ )
+ result = pytester.runpytest("--doctest-modules")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 3 items",
+ "",
+ "test_importorskip.txt s *",
+ "test_skip.txt s *",
+ "test_xfail.txt x *",
+ "",
+ "*= 2 skipped, 1 xfailed in *",
+ ]
+ )
+
+ def test_docstring_partial_context_around_error(self, pytester: Pytester):
+ """Test that we show some context before the actual line of a failing
+ doctest.
+ """
+ pytester.makepyfile(
+ '''
+ def foo():
+ """
+ text-line-1
+ text-line-2
+ text-line-3
+ text-line-4
+ text-line-5
+ text-line-6
+ text-line-7
+ text-line-8
+ text-line-9
+ text-line-10
+ text-line-11
+ >>> 1 + 1
+ 3
+
+ text-line-after
+ """
+ '''
+ )
+ result = pytester.runpytest("--doctest-modules")
+ result.stdout.fnmatch_lines(
+ [
+ "*docstring_partial_context_around_error*",
+ "005*text-line-3",
+ "006*text-line-4",
+ "013*text-line-11",
+ "014*>>> 1 + 1",
+ "Expected:",
+ " 3",
+ "Got:",
+ " 2",
+ ]
+ )
+ # lines below should be trimmed out
+ result.stdout.no_fnmatch_line("*text-line-2*")
+ result.stdout.no_fnmatch_line("*text-line-after*")
+
+ def test_docstring_full_context_around_error(self, pytester: Pytester):
+ """Test that we show the whole context before the actual line of a failing
+ doctest, provided that the context is up to 10 lines long.
+ """
+ pytester.makepyfile(
+ '''
+ def foo():
+ """
+ text-line-1
+ text-line-2
+
+ >>> 1 + 1
+ 3
+ """
+ '''
+ )
+ result = pytester.runpytest("--doctest-modules")
+ result.stdout.fnmatch_lines(
+ [
+ "*docstring_full_context_around_error*",
+ "003*text-line-1",
+ "004*text-line-2",
+ "006*>>> 1 + 1",
+ "Expected:",
+ " 3",
+ "Got:",
+ " 2",
+ ]
+ )
+
+ def test_doctest_linedata_missing(self, pytester: Pytester):
+ pytester.path.joinpath("hello.py").write_text(
+ textwrap.dedent(
+ """\
+ class Fun(object):
+ @property
+ def test(self):
+ '''
+ >>> a = 1
+ >>> 1/0
+ '''
+ """
+ )
+ )
+ result = pytester.runpytest("--doctest-modules")
+ result.stdout.fnmatch_lines(
+ ["*hello*", "006*>>> 1/0*", "*UNEXPECTED*ZeroDivision*", "*1 failed*"]
+ )
+
+ def test_doctest_linedata_on_property(self, pytester: Pytester):
+ pytester.makepyfile(
+ """
+ class Sample(object):
+ @property
+ def some_property(self):
+ '''
+ >>> Sample().some_property
+ 'another thing'
+ '''
+ return 'something'
+ """
+ )
+ result = pytester.runpytest("--doctest-modules")
+ result.stdout.fnmatch_lines(
+ [
+ "*= FAILURES =*",
+ "*_ [[]doctest[]] test_doctest_linedata_on_property.Sample.some_property _*",
+ "004 ",
+ "005 >>> Sample().some_property",
+ "Expected:",
+ " 'another thing'",
+ "Got:",
+ " 'something'",
+ "",
+ "*/test_doctest_linedata_on_property.py:5: DocTestFailure",
+ "*= 1 failed in *",
+ ]
+ )
+
+ def test_doctest_no_linedata_on_overriden_property(self, pytester: Pytester):
+ pytester.makepyfile(
+ """
+ class Sample(object):
+ @property
+ def some_property(self):
+ '''
+ >>> Sample().some_property
+ 'another thing'
+ '''
+ return 'something'
+ some_property = property(some_property.__get__, None, None, some_property.__doc__)
+ """
+ )
+ result = pytester.runpytest("--doctest-modules")
+ result.stdout.fnmatch_lines(
+ [
+ "*= FAILURES =*",
+ "*_ [[]doctest[]] test_doctest_no_linedata_on_overriden_property.Sample.some_property _*",
+ "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example",
+ "[?][?][?] >>> Sample().some_property",
+ "Expected:",
+ " 'another thing'",
+ "Got:",
+ " 'something'",
+ "",
+ "*/test_doctest_no_linedata_on_overriden_property.py:None: DocTestFailure",
+ "*= 1 failed in *",
+ ]
+ )
+
+ def test_doctest_unex_importerror_only_txt(self, pytester: Pytester):
+ pytester.maketxtfile(
+ """
+ >>> import asdalsdkjaslkdjasd
+ >>>
+ """
+ )
+ result = pytester.runpytest()
+ # doctest is never executed because of error during hello.py collection
+ result.stdout.fnmatch_lines(
+ [
+ "*>>> import asdals*",
+ "*UNEXPECTED*ModuleNotFoundError*",
+ "ModuleNotFoundError: No module named *asdal*",
+ ]
+ )
+
+ def test_doctest_unex_importerror_with_module(self, pytester: Pytester):
+ pytester.path.joinpath("hello.py").write_text(
+ textwrap.dedent(
+ """\
+ import asdalsdkjaslkdjasd
+ """
+ )
+ )
+ pytester.maketxtfile(
+ """
+ >>> import hello
+ >>>
+ """
+ )
+ result = pytester.runpytest("--doctest-modules")
+ # doctest is never executed because of error during hello.py collection
+ result.stdout.fnmatch_lines(
+ [
+ "*ERROR collecting hello.py*",
+ "*ModuleNotFoundError: No module named *asdals*",
+ "*Interrupted: 1 error during collection*",
+ ]
+ )
+
+ def test_doctestmodule(self, pytester: Pytester):
+ p = pytester.makepyfile(
+ """
+ '''
+ >>> x = 1
+ >>> x == 1
+ False
+
+ '''
+ """
+ )
+ reprec = pytester.inline_run(p, "--doctest-modules")
+ reprec.assertoutcome(failed=1)
+
+ def test_doctestmodule_external_and_issue116(self, pytester: Pytester):
+ p = pytester.mkpydir("hello")
+ p.joinpath("__init__.py").write_text(
+ textwrap.dedent(
+ """\
+ def somefunc():
+ '''
+ >>> i = 0
+ >>> i + 1
+ 2
+ '''
+ """
+ )
+ )
+ result = pytester.runpytest(p, "--doctest-modules")
+ result.stdout.fnmatch_lines(
+ [
+ "003 *>>> i = 0",
+ "004 *>>> i + 1",
+ "*Expected:",
+ "* 2",
+ "*Got:",
+ "* 1",
+ "*:4: DocTestFailure",
+ ]
+ )
+
+ def test_txtfile_failing(self, pytester: Pytester):
+ p = pytester.maketxtfile(
+ """
+ >>> i = 0
+ >>> i + 1
+ 2
+ """
+ )
+ result = pytester.runpytest(p, "-s")
+ result.stdout.fnmatch_lines(
+ [
+ "001 >>> i = 0",
+ "002 >>> i + 1",
+ "Expected:",
+ " 2",
+ "Got:",
+ " 1",
+ "*test_txtfile_failing.txt:2: DocTestFailure",
+ ]
+ )
+
+ def test_txtfile_with_fixtures(self, pytester: Pytester):
+ p = pytester.maketxtfile(
+ """
+ >>> p = getfixture('tmp_path')
+ >>> p.is_dir()
+ True
+ """
+ )
+ reprec = pytester.inline_run(p)
+ reprec.assertoutcome(passed=1)
+
+ def test_txtfile_with_usefixtures_in_ini(self, pytester: Pytester):
+ pytester.makeini(
+ """
+ [pytest]
+ usefixtures = myfixture
+ """
+ )
+ pytester.makeconftest(
+ """
+ import pytest
+ @pytest.fixture
+ def myfixture(monkeypatch):
+ monkeypatch.setenv("HELLO", "WORLD")
+ """
+ )
+
+ p = pytester.maketxtfile(
+ """
+ >>> import os
+ >>> os.environ["HELLO"]
+ 'WORLD'
+ """
+ )
+ reprec = pytester.inline_run(p)
+ reprec.assertoutcome(passed=1)
+
+ def test_doctestmodule_with_fixtures(self, pytester: Pytester):
+ p = pytester.makepyfile(
+ """
+ '''
+ >>> p = getfixture('tmp_path')
+ >>> p.is_dir()
+ True
+ '''
+ """
+ )
+ reprec = pytester.inline_run(p, "--doctest-modules")
+ reprec.assertoutcome(passed=1)
+
+ def test_doctestmodule_three_tests(self, pytester: Pytester):
+ p = pytester.makepyfile(
+ """
+ '''
+ >>> p = getfixture('tmp_path')
+ >>> p.is_dir()
+ True
+ '''
+ def my_func():
+ '''
+ >>> magic = 42
+ >>> magic - 42
+ 0
+ '''
+ def useless():
+ pass
+ def another():
+ '''
+ >>> import os
+ >>> os is os
+ True
+ '''
+ """
+ )
+ reprec = pytester.inline_run(p, "--doctest-modules")
+ reprec.assertoutcome(passed=3)
+
+ def test_doctestmodule_two_tests_one_fail(self, pytester: Pytester):
+ p = pytester.makepyfile(
+ """
+ class MyClass(object):
+ def bad_meth(self):
+ '''
+ >>> magic = 42
+ >>> magic
+ 0
+ '''
+ def nice_meth(self):
+ '''
+ >>> magic = 42
+ >>> magic - 42
+ 0
+ '''
+ """
+ )
+ reprec = pytester.inline_run(p, "--doctest-modules")
+ reprec.assertoutcome(failed=1, passed=1)
+
+ def test_ignored_whitespace(self, pytester: Pytester):
+ pytester.makeini(
+ """
+ [pytest]
+ doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE
+ """
+ )
+ p = pytester.makepyfile(
+ """
+ class MyClass(object):
+ '''
+ >>> a = "foo "
+ >>> print(a)
+ foo
+ '''
+ pass
+ """
+ )
+ reprec = pytester.inline_run(p, "--doctest-modules")
+ reprec.assertoutcome(passed=1)
+
+ def test_non_ignored_whitespace(self, pytester: Pytester):
+ pytester.makeini(
+ """
+ [pytest]
+ doctest_optionflags = ELLIPSIS
+ """
+ )
+ p = pytester.makepyfile(
+ """
+ class MyClass(object):
+ '''
+ >>> a = "foo "
+ >>> print(a)
+ foo
+ '''
+ pass
+ """
+ )
+ reprec = pytester.inline_run(p, "--doctest-modules")
+ reprec.assertoutcome(failed=1, passed=0)
+
+ def test_ignored_whitespace_glob(self, pytester: Pytester):
+ pytester.makeini(
+ """
+ [pytest]
+ doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE
+ """
+ )
+ p = pytester.maketxtfile(
+ xdoc="""
+ >>> a = "foo "
+ >>> print(a)
+ foo
+ """
+ )
+ reprec = pytester.inline_run(p, "--doctest-glob=x*.txt")
+ reprec.assertoutcome(passed=1)
+
+ def test_non_ignored_whitespace_glob(self, pytester: Pytester):
+ pytester.makeini(
+ """
+ [pytest]
+ doctest_optionflags = ELLIPSIS
+ """
+ )
+ p = pytester.maketxtfile(
+ xdoc="""
+ >>> a = "foo "
+ >>> print(a)
+ foo
+ """
+ )
+ reprec = pytester.inline_run(p, "--doctest-glob=x*.txt")
+ reprec.assertoutcome(failed=1, passed=0)
+
+ def test_contains_unicode(self, pytester: Pytester):
+ """Fix internal error with docstrings containing non-ascii characters."""
+ pytester.makepyfile(
+ '''\
+ def foo():
+ """
+ >>> name = 'с' # not letter 'c' but instead Cyrillic 's'.
+ 'anything'
+ """
+ '''
+ )
+ result = pytester.runpytest("--doctest-modules")
+ result.stdout.fnmatch_lines(["Got nothing", "* 1 failed in*"])
+
+ def test_ignore_import_errors_on_doctest(self, pytester: Pytester):
+ p = pytester.makepyfile(
+ """
+ import asdf
+
+ def add_one(x):
+ '''
+ >>> add_one(1)
+ 2
+ '''
+ return x + 1
+ """
+ )
+
+ reprec = pytester.inline_run(
+ p, "--doctest-modules", "--doctest-ignore-import-errors"
+ )
+ reprec.assertoutcome(skipped=1, failed=1, passed=0)
+
+ def test_junit_report_for_doctest(self, pytester: Pytester):
+ """#713: Fix --junit-xml option when used with --doctest-modules."""
+ p = pytester.makepyfile(
+ """
+ def foo():
+ '''
+ >>> 1 + 1
+ 3
+ '''
+ pass
+ """
+ )
+ reprec = pytester.inline_run(p, "--doctest-modules", "--junit-xml=junit.xml")
+ reprec.assertoutcome(failed=1)
+
+ def test_unicode_doctest(self, pytester: Pytester):
+ """
+ Test case for issue 2434: DecodeError on Python 2 when doctest contains non-ascii
+ characters.
+ """
+ p = pytester.maketxtfile(
+ test_unicode_doctest="""
+ .. doctest::
+
+ >>> print("Hi\\n\\nByé")
+ Hi
+ ...
+ Byé
+ >>> 1 / 0 # Byé
+ 1
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(
+ ["*UNEXPECTED EXCEPTION: ZeroDivisionError*", "*1 failed*"]
+ )
+
+ def test_unicode_doctest_module(self, pytester: Pytester):
+ """
+ Test case for issue 2434: DecodeError on Python 2 when doctest docstring
+ contains non-ascii characters.
+ """
+ p = pytester.makepyfile(
+ test_unicode_doctest_module="""
+ def fix_bad_unicode(text):
+ '''
+ >>> print(fix_bad_unicode('único'))
+ único
+ '''
+ return "único"
+ """
+ )
+ result = pytester.runpytest(p, "--doctest-modules")
+ result.stdout.fnmatch_lines(["* 1 passed *"])
+
+ def test_print_unicode_value(self, pytester: Pytester):
+ """
+ Test case for issue 3583: Printing Unicode in doctest under Python 2.7
+ doesn't work
+ """
+ p = pytester.maketxtfile(
+ test_print_unicode_value=r"""
+ Here is a doctest::
+
+ >>> print('\xE5\xE9\xEE\xF8\xFC')
+ åéîøü
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(["* 1 passed *"])
+
+ def test_reportinfo(self, pytester: Pytester):
+ """Make sure that DoctestItem.reportinfo() returns lineno."""
+ p = pytester.makepyfile(
+ test_reportinfo="""
+ def foo(x):
+ '''
+ >>> foo('a')
+ 'b'
+ '''
+ return 'c'
+ """
+ )
+ items, reprec = pytester.inline_genitems(p, "--doctest-modules")
+ reportinfo = items[0].reportinfo()
+ assert reportinfo[1] == 1
+
+ def test_valid_setup_py(self, pytester: Pytester):
+ """
+ Test to make sure that pytest ignores valid setup.py files when ran
+ with --doctest-modules
+ """
+ p = pytester.makepyfile(
+ setup="""
+ from setuptools import setup, find_packages
+ if __name__ == '__main__':
+ setup(name='sample',
+ version='0.0',
+ description='description',
+ packages=find_packages()
+ )
+ """
+ )
+ result = pytester.runpytest(p, "--doctest-modules")
+ result.stdout.fnmatch_lines(["*collected 0 items*"])
+
+ def test_main_py_does_not_cause_import_errors(self, pytester: Pytester):
+ p = pytester.copy_example("doctest/main_py")
+ result = pytester.runpytest(p, "--doctest-modules")
+ result.stdout.fnmatch_lines(["*collected 2 items*", "*1 failed, 1 passed*"])
+
+ def test_invalid_setup_py(self, pytester: Pytester):
+ """
+ Test to make sure that pytest reads setup.py files that are not used
+ for python packages when ran with --doctest-modules
+ """
+ p = pytester.makepyfile(
+ setup="""
+ def test_foo():
+ return 'bar'
+ """
+ )
+ result = pytester.runpytest(p, "--doctest-modules")
+ result.stdout.fnmatch_lines(["*collected 1 item*"])
+
+
+class TestLiterals:
+ @pytest.mark.parametrize("config_mode", ["ini", "comment"])
+ def test_allow_unicode(self, pytester, config_mode):
+ """Test that doctests which output unicode work in all python versions
+ tested by pytest when the ALLOW_UNICODE option is used (either in
+ the ini file or by an inline comment).
+ """
+ if config_mode == "ini":
+ pytester.makeini(
+ """
+ [pytest]
+ doctest_optionflags = ALLOW_UNICODE
+ """
+ )
+ comment = ""
+ else:
+ comment = "#doctest: +ALLOW_UNICODE"
+
+ pytester.maketxtfile(
+ test_doc="""
+ >>> b'12'.decode('ascii') {comment}
+ '12'
+ """.format(
+ comment=comment
+ )
+ )
+ pytester.makepyfile(
+ foo="""
+ def foo():
+ '''
+ >>> b'12'.decode('ascii') {comment}
+ '12'
+ '''
+ """.format(
+ comment=comment
+ )
+ )
+ reprec = pytester.inline_run("--doctest-modules")
+ reprec.assertoutcome(passed=2)
+
+ @pytest.mark.parametrize("config_mode", ["ini", "comment"])
+ def test_allow_bytes(self, pytester, config_mode):
+ """Test that doctests which output bytes work in all python versions
+ tested by pytest when the ALLOW_BYTES option is used (either in
+ the ini file or by an inline comment)(#1287).
+ """
+ if config_mode == "ini":
+ pytester.makeini(
+ """
+ [pytest]
+ doctest_optionflags = ALLOW_BYTES
+ """
+ )
+ comment = ""
+ else:
+ comment = "#doctest: +ALLOW_BYTES"
+
+ pytester.maketxtfile(
+ test_doc="""
+ >>> b'foo' {comment}
+ 'foo'
+ """.format(
+ comment=comment
+ )
+ )
+ pytester.makepyfile(
+ foo="""
+ def foo():
+ '''
+ >>> b'foo' {comment}
+ 'foo'
+ '''
+ """.format(
+ comment=comment
+ )
+ )
+ reprec = pytester.inline_run("--doctest-modules")
+ reprec.assertoutcome(passed=2)
+
+ def test_unicode_string(self, pytester: Pytester):
+ """Test that doctests which output unicode fail in Python 2 when
+ the ALLOW_UNICODE option is not used. The same test should pass
+ in Python 3.
+ """
+ pytester.maketxtfile(
+ test_doc="""
+ >>> b'12'.decode('ascii')
+ '12'
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_bytes_literal(self, pytester: Pytester):
+ """Test that doctests which output bytes fail in Python 3 when
+ the ALLOW_BYTES option is not used. (#1287).
+ """
+ pytester.maketxtfile(
+ test_doc="""
+ >>> b'foo'
+ 'foo'
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(failed=1)
+
+ def test_number_re(self) -> None:
+ _number_re = _get_checker()._number_re # type: ignore
+ for s in [
+ "1.",
+ "+1.",
+ "-1.",
+ ".1",
+ "+.1",
+ "-.1",
+ "0.1",
+ "+0.1",
+ "-0.1",
+ "1e5",
+ "+1e5",
+ "1e+5",
+ "+1e+5",
+ "1e-5",
+ "+1e-5",
+ "-1e-5",
+ "1.2e3",
+ "-1.2e-3",
+ ]:
+ print(s)
+ m = _number_re.match(s)
+ assert m is not None
+ assert float(m.group()) == pytest.approx(float(s))
+ for s in ["1", "abc"]:
+ print(s)
+ assert _number_re.match(s) is None
+
+ @pytest.mark.parametrize("config_mode", ["ini", "comment"])
+ def test_number_precision(self, pytester, config_mode):
+ """Test the NUMBER option."""
+ if config_mode == "ini":
+ pytester.makeini(
+ """
+ [pytest]
+ doctest_optionflags = NUMBER
+ """
+ )
+ comment = ""
+ else:
+ comment = "#doctest: +NUMBER"
+
+ pytester.maketxtfile(
+ test_doc="""
+
+ Scalars:
+
+ >>> import math
+ >>> math.pi {comment}
+ 3.141592653589793
+ >>> math.pi {comment}
+ 3.1416
+ >>> math.pi {comment}
+ 3.14
+ >>> -math.pi {comment}
+ -3.14
+ >>> math.pi {comment}
+ 3.
+ >>> 3. {comment}
+ 3.0
+ >>> 3. {comment}
+ 3.
+ >>> 3. {comment}
+ 3.01
+ >>> 3. {comment}
+ 2.99
+ >>> .299 {comment}
+ .3
+ >>> .301 {comment}
+ .3
+ >>> 951. {comment}
+ 1e3
+ >>> 1049. {comment}
+ 1e3
+ >>> -1049. {comment}
+ -1e3
+ >>> 1e3 {comment}
+ 1e3
+ >>> 1e3 {comment}
+ 1000.
+
+ Lists:
+
+ >>> [3.1415, 0.097, 13.1, 7, 8.22222e5, 0.598e-2] {comment}
+ [3.14, 0.1, 13., 7, 8.22e5, 6.0e-3]
+ >>> [[0.333, 0.667], [0.999, 1.333]] {comment}
+ [[0.33, 0.667], [0.999, 1.333]]
+ >>> [[[0.101]]] {comment}
+ [[[0.1]]]
+
+ Doesn't barf on non-numbers:
+
+ >>> 'abc' {comment}
+ 'abc'
+ >>> None {comment}
+ """.format(
+ comment=comment
+ )
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ @pytest.mark.parametrize(
+ "expression,output",
+ [
+ # ints shouldn't match floats:
+ ("3.0", "3"),
+ ("3e0", "3"),
+ ("1e3", "1000"),
+ ("3", "3.0"),
+ # Rounding:
+ ("3.1", "3.0"),
+ ("3.1", "3.2"),
+ ("3.1", "4.0"),
+ ("8.22e5", "810000.0"),
+ # Only the actual output is rounded up, not the expected output:
+ ("3.0", "2.98"),
+ ("1e3", "999"),
+ # The current implementation doesn't understand that numbers inside
+ # strings shouldn't be treated as numbers:
+ pytest.param("'3.1416'", "'3.14'", marks=pytest.mark.xfail),
+ ],
+ )
+ def test_number_non_matches(self, pytester, expression, output):
+ pytester.maketxtfile(
+ test_doc="""
+ >>> {expression} #doctest: +NUMBER
+ {output}
+ """.format(
+ expression=expression, output=output
+ )
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=0, failed=1)
+
+ def test_number_and_allow_unicode(self, pytester: Pytester):
+ pytester.maketxtfile(
+ test_doc="""
+ >>> from collections import namedtuple
+ >>> T = namedtuple('T', 'a b c')
+ >>> T(a=0.2330000001, b=u'str', c=b'bytes') # doctest: +ALLOW_UNICODE, +ALLOW_BYTES, +NUMBER
+ T(a=0.233, b=u'str', c='bytes')
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+class TestDoctestSkips:
+ """
+ If all examples in a doctest are skipped due to the SKIP option, then
+ the tests should be SKIPPED rather than PASSED. (#957)
+ """
+
+ @pytest.fixture(params=["text", "module"])
+ def makedoctest(self, pytester, request):
+ def makeit(doctest):
+ mode = request.param
+ if mode == "text":
+ pytester.maketxtfile(doctest)
+ else:
+ assert mode == "module"
+ pytester.makepyfile('"""\n%s"""' % doctest)
+
+ return makeit
+
+ def test_one_skipped(self, pytester, makedoctest):
+ makedoctest(
+ """
+ >>> 1 + 1 # doctest: +SKIP
+ 2
+ >>> 2 + 2
+ 4
+ """
+ )
+ reprec = pytester.inline_run("--doctest-modules")
+ reprec.assertoutcome(passed=1)
+
+ def test_one_skipped_failed(self, pytester, makedoctest):
+ makedoctest(
+ """
+ >>> 1 + 1 # doctest: +SKIP
+ 2
+ >>> 2 + 2
+ 200
+ """
+ )
+ reprec = pytester.inline_run("--doctest-modules")
+ reprec.assertoutcome(failed=1)
+
+ def test_all_skipped(self, pytester, makedoctest):
+ makedoctest(
+ """
+ >>> 1 + 1 # doctest: +SKIP
+ 2
+ >>> 2 + 2 # doctest: +SKIP
+ 200
+ """
+ )
+ reprec = pytester.inline_run("--doctest-modules")
+ reprec.assertoutcome(skipped=1)
+
+ def test_vacuous_all_skipped(self, pytester, makedoctest):
+ makedoctest("")
+ reprec = pytester.inline_run("--doctest-modules")
+ reprec.assertoutcome(passed=0, skipped=0)
+
+ def test_continue_on_failure(self, pytester: Pytester):
+ pytester.maketxtfile(
+ test_something="""
+ >>> i = 5
+ >>> def foo():
+ ... raise ValueError('error1')
+ >>> foo()
+ >>> i
+ >>> i + 2
+ 7
+ >>> i + 1
+ """
+ )
+ result = pytester.runpytest(
+ "--doctest-modules", "--doctest-continue-on-failure"
+ )
+ result.assert_outcomes(passed=0, failed=1)
+ # The lines that contains the failure are 4, 5, and 8. The first one
+ # is a stack trace and the other two are mismatches.
+ result.stdout.fnmatch_lines(
+ ["*4: UnexpectedException*", "*5: DocTestFailure*", "*8: DocTestFailure*"]
+ )
+
+ def test_skipping_wrapped_test(self, pytester):
+ """
+ Issue 8796: INTERNALERROR raised when skipping a decorated DocTest
+ through pytest_collection_modifyitems.
+ """
+ pytester.makeconftest(
+ """
+ import pytest
+ from _pytest.doctest import DoctestItem
+
+ def pytest_collection_modifyitems(config, items):
+ skip_marker = pytest.mark.skip()
+
+ for item in items:
+ if isinstance(item, DoctestItem):
+ item.add_marker(skip_marker)
+ """
+ )
+
+ pytester.makepyfile(
+ """
+ from contextlib import contextmanager
+
+ @contextmanager
+ def my_config_context():
+ '''
+ >>> import os
+ '''
+ """
+ )
+
+ result = pytester.runpytest("--doctest-modules")
+ assert "INTERNALERROR" not in result.stdout.str()
+ result.assert_outcomes(skipped=1)
+
+
+class TestDoctestAutoUseFixtures:
+
+ SCOPES = ["module", "session", "class", "function"]
+
+ def test_doctest_module_session_fixture(self, pytester: Pytester):
+ """Test that session fixtures are initialized for doctest modules (#768)."""
+ # session fixture which changes some global data, which will
+ # be accessed by doctests in a module
+ pytester.makeconftest(
+ """
+ import pytest
+ import sys
+
+ @pytest.fixture(autouse=True, scope='session')
+ def myfixture():
+ assert not hasattr(sys, 'pytest_session_data')
+ sys.pytest_session_data = 1
+ yield
+ del sys.pytest_session_data
+ """
+ )
+ pytester.makepyfile(
+ foo="""
+ import sys
+
+ def foo():
+ '''
+ >>> assert sys.pytest_session_data == 1
+ '''
+
+ def bar():
+ '''
+ >>> assert sys.pytest_session_data == 1
+ '''
+ """
+ )
+ result = pytester.runpytest("--doctest-modules")
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+ @pytest.mark.parametrize("scope", SCOPES)
+ @pytest.mark.parametrize("enable_doctest", [True, False])
+ def test_fixture_scopes(self, pytester, scope, enable_doctest):
+ """Test that auto-use fixtures work properly with doctest modules.
+ See #1057 and #1100.
+ """
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(autouse=True, scope="{scope}")
+ def auto(request):
+ return 99
+ """.format(
+ scope=scope
+ )
+ )
+ pytester.makepyfile(
+ test_1='''
+ def test_foo():
+ """
+ >>> getfixture('auto') + 1
+ 100
+ """
+ def test_bar():
+ assert 1
+ '''
+ )
+ params = ("--doctest-modules",) if enable_doctest else ()
+ passes = 3 if enable_doctest else 2
+ result = pytester.runpytest(*params)
+ result.stdout.fnmatch_lines(["*=== %d passed in *" % passes])
+
+ @pytest.mark.parametrize("scope", SCOPES)
+ @pytest.mark.parametrize("autouse", [True, False])
+ @pytest.mark.parametrize("use_fixture_in_doctest", [True, False])
+ def test_fixture_module_doctest_scopes(
+ self, pytester, scope, autouse, use_fixture_in_doctest
+ ):
+ """Test that auto-use fixtures work properly with doctest files.
+ See #1057 and #1100.
+ """
+ pytester.makeconftest(
+ """
+ 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(
+ test_doc="""
+ >>> getfixture('auto')
+ 99
+ """
+ )
+ else:
+ pytester.maketxtfile(
+ test_doc="""
+ >>> 1 + 1
+ 2
+ """
+ )
+ result = pytester.runpytest("--doctest-modules")
+ result.stdout.no_fnmatch_line("*FAILURES*")
+ result.stdout.fnmatch_lines(["*=== 1 passed in *"])
+
+ @pytest.mark.parametrize("scope", SCOPES)
+ def test_auto_use_request_attributes(self, pytester, scope):
+ """Check that all attributes of a request in an autouse fixture
+ behave as expected when requested for a doctest item.
+ """
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(autouse=True, scope="{scope}")
+ def auto(request):
+ if "{scope}" == 'module':
+ assert request.module is None
+ if "{scope}" == 'class':
+ assert request.cls is None
+ if "{scope}" == 'function':
+ assert request.function is None
+ return 99
+ """.format(
+ scope=scope
+ )
+ )
+ pytester.maketxtfile(
+ test_doc="""
+ >>> 1 + 1
+ 2
+ """
+ )
+ result = pytester.runpytest("--doctest-modules")
+ str(result.stdout.no_fnmatch_line("*FAILURES*"))
+ result.stdout.fnmatch_lines(["*=== 1 passed in *"])
+
+
+class TestDoctestNamespaceFixture:
+
+ SCOPES = ["module", "session", "class", "function"]
+
+ @pytest.mark.parametrize("scope", SCOPES)
+ def test_namespace_doctestfile(self, pytester, scope):
+ """
+ Check that inserting something into the namespace works in a
+ simple text file doctest
+ """
+ pytester.makeconftest(
+ """
+ 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(
+ """
+ >>> print(cl.__name__)
+ contextlib
+ """
+ )
+ reprec = pytester.inline_run(p)
+ reprec.assertoutcome(passed=1)
+
+ @pytest.mark.parametrize("scope", SCOPES)
+ def test_namespace_pyfile(self, pytester, scope):
+ """
+ Check that inserting something into the namespace works in a
+ simple Python file docstring doctest
+ """
+ pytester.makeconftest(
+ """
+ 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(
+ """
+ def foo():
+ '''
+ >>> print(cl.__name__)
+ contextlib
+ '''
+ """
+ )
+ reprec = pytester.inline_run(p, "--doctest-modules")
+ reprec.assertoutcome(passed=1)
+
+
+class TestDoctestReportingOption:
+ def _run_doctest_report(self, pytester, format):
+ pytester.makepyfile(
+ """
+ def foo():
+ '''
+ >>> foo()
+ a b
+ 0 1 4
+ 1 2 4
+ 2 3 6
+ '''
+ print(' a b\\n'
+ '0 1 4\\n'
+ '1 2 5\\n'
+ '2 3 6')
+ """
+ )
+ return pytester.runpytest("--doctest-modules", "--doctest-report", format)
+
+ @pytest.mark.parametrize("format", ["udiff", "UDIFF", "uDiFf"])
+ def test_doctest_report_udiff(self, pytester, format):
+ result = self._run_doctest_report(pytester, format)
+ result.stdout.fnmatch_lines(
+ [" 0 1 4", " -1 2 4", " +1 2 5", " 2 3 6"]
+ )
+
+ def test_doctest_report_cdiff(self, pytester: Pytester):
+ result = self._run_doctest_report(pytester, "cdiff")
+ result.stdout.fnmatch_lines(
+ [
+ " a b",
+ " 0 1 4",
+ " ! 1 2 4",
+ " 2 3 6",
+ " --- 1,4 ----",
+ " a b",
+ " 0 1 4",
+ " ! 1 2 5",
+ " 2 3 6",
+ ]
+ )
+
+ def test_doctest_report_ndiff(self, pytester: Pytester):
+ result = self._run_doctest_report(pytester, "ndiff")
+ result.stdout.fnmatch_lines(
+ [
+ " a b",
+ " 0 1 4",
+ " - 1 2 4",
+ " ? ^",
+ " + 1 2 5",
+ " ? ^",
+ " 2 3 6",
+ ]
+ )
+
+ @pytest.mark.parametrize("format", ["none", "only_first_failure"])
+ def test_doctest_report_none_or_only_first_failure(self, pytester, format):
+ result = self._run_doctest_report(pytester, format)
+ result.stdout.fnmatch_lines(
+ [
+ "Expected:",
+ " a b",
+ " 0 1 4",
+ " 1 2 4",
+ " 2 3 6",
+ "Got:",
+ " a b",
+ " 0 1 4",
+ " 1 2 5",
+ " 2 3 6",
+ ]
+ )
+
+ def test_doctest_report_invalid(self, pytester: Pytester):
+ result = self._run_doctest_report(pytester, "obviously_invalid_format")
+ result.stderr.fnmatch_lines(
+ [
+ "*error: argument --doctest-report: invalid choice: 'obviously_invalid_format' (choose from*"
+ ]
+ )
+
+
+@pytest.mark.parametrize("mock_module", ["mock", "unittest.mock"])
+def test_doctest_mock_objects_dont_recurse_missbehaved(mock_module, pytester: Pytester):
+ pytest.importorskip(mock_module)
+ pytester.makepyfile(
+ """
+ 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 *"])
+
+
+class Broken:
+ def __getattr__(self, _):
+ raise KeyError("This should be an AttributeError")
+
+
+@pytest.mark.parametrize( # pragma: no branch (lambdas are not called)
+ "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]]
+) -> None:
+ bad_instance = Broken()
+ assert inspect.unwrap.__module__ == "inspect"
+ with _patch_unwrap_mock_aware():
+ assert inspect.unwrap.__module__ != "inspect"
+ with pytest.warns(
+ pytest.PytestWarning, match="^Got KeyError.* when unwrapping"
+ ):
+ with pytest.raises(KeyError):
+ inspect.unwrap(bad_instance, stop=stop) # type: ignore[arg-type]
+ assert inspect.unwrap.__module__ == "inspect"
+
+
+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")')
+ assert not _is_setup_py(not_setup_py)
+
+
+@pytest.mark.parametrize("mod", ("setuptools", "distutils.core"))
+def test_is_setup_py_is_a_setup_py(tmp_path: Path, mod: str) -> None:
+ setup_py = tmp_path.joinpath("setup.py")
+ setup_py.write_text(f'from {mod} import setup; setup(name="foo")', "utf-8")
+ assert _is_setup_py(setup_py)
+
+
+@pytest.mark.parametrize("mod", ("setuptools", "distutils.core"))
+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)
+ )
+ setup_py.write_bytes(contents.encode("cp1252"))
+ assert _is_setup_py(setup_py)
+
+
+@pytest.mark.parametrize(
+ "name, expected", [("__main__.py", True), ("__init__.py", False)]
+)
+def test_is_main_py(tmp_path: Path, name: str, expected: bool) -> None:
+ dunder_main = tmp_path.joinpath(name)
+ assert _is_main_py(dunder_main) == expected
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_entry_points.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_entry_points.py
new file mode 100644
index 0000000000..5d00312736
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_entry_points.py
@@ -0,0 +1,7 @@
+from _pytest.compat import importlib_metadata
+
+
+def test_pytest_entry_points_are_identical():
+ 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/testing/web-platform/tests/tools/third_party/pytest/testing/test_error_diffs.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_error_diffs.py
new file mode 100644
index 0000000000..1668e929ab
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_error_diffs.py
@@ -0,0 +1,283 @@
+"""
+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
+
+
+TESTCASES = [
+ pytest.param(
+ """
+ def test_this():
+ result = [1, 4, 3]
+ expected = [1, 2, 3]
+ assert result == expected
+ """,
+ """
+ > assert result == expected
+ 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, 4, 3]
+ E ? ^
+ """,
+ id="Compare lists, one item differs",
+ ),
+ pytest.param(
+ """
+ def test_this():
+ result = [1, 2, 3]
+ expected = [1, 2]
+ assert result == expected
+ """,
+ """
+ > assert result == expected
+ 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 ? +++
+ """,
+ id="Compare lists, one extra item",
+ ),
+ pytest.param(
+ """
+ def test_this():
+ result = [1, 3]
+ expected = [1, 2, 3]
+ assert result == expected
+ """,
+ """
+ > assert result == expected
+ E assert [1, 3] == [1, 2, 3]
+ 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]
+ """,
+ id="Compare lists, one item missing",
+ ),
+ pytest.param(
+ """
+ def test_this():
+ result = (1, 4, 3)
+ expected = (1, 2, 3)
+ assert result == expected
+ """,
+ """
+ > assert result == expected
+ 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, 4, 3)
+ E ? ^
+ """,
+ id="Compare tuples",
+ ),
+ pytest.param(
+ """
+ def test_this():
+ result = {1, 3, 4}
+ expected = {1, 2, 3}
+ assert result == expected
+ """,
+ """
+ > assert result == expected
+ E assert {1, 3, 4} == {1, 2, 3}
+ E Extra items in the left set:
+ E 4
+ E Extra items in the right set:
+ E 2
+ E Full diff:
+ E - {1, 2, 3}
+ E ? ^ ^
+ E + {1, 3, 4}
+ E ? ^ ^
+ """,
+ id="Compare sets",
+ ),
+ pytest.param(
+ """
+ def test_this():
+ result = {1: 'spam', 3: 'eggs'}
+ expected = {1: 'spam', 2: 'eggs'}
+ assert result == expected
+ """,
+ """
+ > assert result == expected
+ E AssertionError: assert {1: 'spam', 3: 'eggs'} == {1: 'spam', 2: 'eggs'}
+ E Common items:
+ E {1: 'spam'}
+ E Left contains 1 more item:
+ E {3: 'eggs'}
+ E Right contains 1 more item:
+ E {2: 'eggs'}
+ E Full diff:
+ E - {1: 'spam', 2: 'eggs'}
+ E ? ^
+ E + {1: 'spam', 3: 'eggs'}
+ E ? ^
+ """,
+ id="Compare dicts with differing keys",
+ ),
+ pytest.param(
+ """
+ def test_this():
+ result = {1: 'spam', 2: 'eggs'}
+ expected = {1: 'spam', 2: 'bacon'}
+ assert result == expected
+ """,
+ """
+ > assert result == expected
+ E AssertionError: assert {1: 'spam', 2: 'eggs'} == {1: 'spam', 2: 'bacon'}
+ E Common items:
+ E {1: 'spam'}
+ E Differing items:
+ E {2: 'eggs'} != {2: 'bacon'}
+ E Full diff:
+ E - {1: 'spam', 2: 'bacon'}
+ E ? ^^^^^
+ E + {1: 'spam', 2: 'eggs'}
+ E ? ^^^^
+ """,
+ id="Compare dicts with differing values",
+ ),
+ pytest.param(
+ """
+ def test_this():
+ result = {1: 'spam', 2: 'eggs'}
+ expected = {1: 'spam', 3: 'bacon'}
+ assert result == expected
+ """,
+ """
+ > assert result == expected
+ E AssertionError: assert {1: 'spam', 2: 'eggs'} == {1: 'spam', 3: 'bacon'}
+ E Common items:
+ E {1: 'spam'}
+ E Left contains 1 more item:
+ E {2: 'eggs'}
+ E Right contains 1 more item:
+ E {3: 'bacon'}
+ E Full diff:
+ E - {1: 'spam', 3: 'bacon'}
+ E ? ^ ^^^^^
+ E + {1: 'spam', 2: 'eggs'}
+ E ? ^ ^^^^
+ """,
+ id="Compare dicts with differing items",
+ ),
+ pytest.param(
+ """
+ def test_this():
+ result = "spmaeggs"
+ expected = "spameggs"
+ assert result == expected
+ """,
+ """
+ > assert result == expected
+ E AssertionError: assert 'spmaeggs' == 'spameggs'
+ E - spameggs
+ E ? -
+ E + spmaeggs
+ E ? +
+ """,
+ id="Compare strings",
+ ),
+ pytest.param(
+ """
+ def test_this():
+ result = "spam bacon eggs"
+ assert "bacon" not in result
+ """,
+ """
+ > assert "bacon" not in result
+ E AssertionError: assert 'bacon' not in 'spam bacon eggs'
+ E 'bacon' is contained here:
+ E spam bacon eggs
+ E ? +++++
+ """,
+ id='Test "not in" string',
+ ),
+]
+if sys.version_info[:2] >= (3, 7):
+ TESTCASES.extend(
+ [
+ pytest.param(
+ """
+ from dataclasses import dataclass
+
+ @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
+
+ @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",
+ ),
+ ]
+ )
+
+
+@pytest.mark.parametrize("code, expected", TESTCASES)
+def test_error_diff(code: str, expected: str, pytester: Pytester) -> None:
+ expected_lines = [line.lstrip() for line in expected.splitlines()]
+ p = pytester.makepyfile(code)
+ result = pytester.runpytest(p, "-vv")
+ result.stdout.fnmatch_lines(expected_lines)
+ assert result.ret == 1
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_faulthandler.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_faulthandler.py
new file mode 100644
index 0000000000..5b7911f21f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_faulthandler.py
@@ -0,0 +1,172 @@
+import io
+import sys
+
+import pytest
+from _pytest.pytester import Pytester
+
+
+def test_enabled(pytester: Pytester) -> None:
+ """Test single crashing test displays a traceback."""
+ pytester.makepyfile(
+ """
+ import faulthandler
+ def test_crash():
+ faulthandler._sigabrt()
+ """
+ )
+ result = pytester.runpytest_subprocess()
+ result.stderr.fnmatch_lines(["*Fatal Python error*"])
+ assert result.ret != 0
+
+
+def setup_crashing_test(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import faulthandler
+ import atexit
+ def test_ok():
+ atexit.register(faulthandler._sigabrt)
+ """
+ )
+
+
+def test_crash_during_shutdown_captured(pytester: Pytester) -> None:
+ """
+ Re-enable faulthandler if pytest encountered it enabled during configure.
+ We should be able to then see crashes during interpreter shutdown.
+ """
+ setup_crashing_test(pytester)
+ args = (sys.executable, "-Xfaulthandler", "-mpytest")
+ result = pytester.run(*args)
+ result.stderr.fnmatch_lines(["*Fatal Python error*"])
+ assert result.ret != 0
+
+
+def test_crash_during_shutdown_not_captured(pytester: Pytester) -> None:
+ """
+ Check that pytest leaves faulthandler disabled if it was not enabled during configure.
+ This prevents us from seeing crashes during interpreter shutdown (see #8260).
+ """
+ setup_crashing_test(pytester)
+ args = (sys.executable, "-mpytest")
+ result = pytester.run(*args)
+ result.stderr.no_fnmatch_line("*Fatal Python error*")
+ assert result.ret != 0
+
+
+def test_disabled(pytester: Pytester) -> None:
+ """Test option to disable fault handler in the command line."""
+ pytester.makepyfile(
+ """
+ import faulthandler
+ def test_disabled():
+ assert not faulthandler.is_enabled()
+ """
+ )
+ result = pytester.runpytest_subprocess("-p", "no:faulthandler")
+ result.stdout.fnmatch_lines(["*1 passed*"])
+ assert result.ret == 0
+
+
+@pytest.mark.parametrize(
+ "enabled",
+ [
+ pytest.param(
+ True, marks=pytest.mark.skip(reason="sometimes crashes on CI (#7022)")
+ ),
+ False,
+ ],
+)
+def test_timeout(pytester: Pytester, enabled: bool) -> None:
+ """Test option to dump tracebacks after a certain timeout.
+
+ If faulthandler is disabled, no traceback will be dumped.
+ """
+ pytester.makepyfile(
+ """
+ import os, time
+ def test_timeout():
+ time.sleep(1 if "CI" in os.environ else 0.1)
+ """
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ faulthandler_timeout = 0.01
+ """
+ )
+ args = ["-p", "no:faulthandler"] if not enabled else []
+
+ result = pytester.runpytest_subprocess(*args)
+ tb_output = "most recent call first"
+ if enabled:
+ result.stderr.fnmatch_lines(["*%s*" % tb_output])
+ else:
+ assert tb_output not in result.stderr.str()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+ assert result.ret == 0
+
+
+@pytest.mark.parametrize("hook_name", ["pytest_enter_pdb", "pytest_exception_interact"])
+def test_cancel_timeout_on_hook(monkeypatch, hook_name) -> None:
+ """Make sure that we are cancelling any scheduled traceback dumping due
+ 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 = []
+
+ monkeypatch.setattr(
+ faulthandler, "cancel_dump_traceback_later", lambda: called.append(1)
+ )
+
+ # call our hook explicitly, we can trust that pytest will call the hook
+ # for us at the appropriate moment
+ hook_func = getattr(faulthandler_plugin, hook_name)
+ hook_func()
+ assert called == [1]
+
+
+def test_already_initialized_crash(pytester: Pytester) -> None:
+ """Even if faulthandler is already initialized, we still dump tracebacks on crashes (#8258)."""
+ pytester.makepyfile(
+ """
+ def test():
+ import faulthandler
+ faulthandler._sigabrt()
+ """
+ )
+ result = pytester.run(
+ sys.executable,
+ "-X",
+ "faulthandler",
+ "-mpytest",
+ pytester.path,
+ )
+ result.stderr.fnmatch_lines(["*Fatal Python error*"])
+ assert result.ret != 0
+
+
+def test_get_stderr_fileno_invalid_fd() -> None:
+ """Test for faulthandler being able to handle invalid file descriptors for stderr (#8249)."""
+ from _pytest.faulthandler import get_stderr_fileno
+
+ class StdErrWrapper(io.StringIO):
+ """
+ Mimic ``twisted.logger.LoggingFile`` to simulate returning an invalid file descriptor.
+
+ https://github.com/twisted/twisted/blob/twisted-20.3.0/src/twisted/logger/_io.py#L132-L139
+ """
+
+ def fileno(self):
+ return -1
+
+ wrapper = StdErrWrapper()
+
+ with pytest.MonkeyPatch.context() as mp:
+ mp.setattr("sys.stderr", wrapper)
+
+ # Even when the stderr wrapper signals an invalid file descriptor,
+ # ``_get_stderr_fileno()`` should return the real one.
+ assert get_stderr_fileno() == 2
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_findpaths.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_findpaths.py
new file mode 100644
index 0000000000..3a2917261a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_findpaths.py
@@ -0,0 +1,135 @@
+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 load_config_dict_from_file
+
+
+class TestLoadConfigDictFromFile:
+ def test_empty_pytest_ini(self, tmp_path: Path) -> None:
+ """pytest.ini files are always considered for configuration, even if empty"""
+ fn = tmp_path / "pytest.ini"
+ fn.write_text("", encoding="utf-8")
+ assert load_config_dict_from_file(fn) == {}
+
+ def test_pytest_ini(self, tmp_path: Path) -> None:
+ """[pytest] section in pytest.ini files is read correctly"""
+ fn = tmp_path / "pytest.ini"
+ fn.write_text("[pytest]\nx=1", encoding="utf-8")
+ assert load_config_dict_from_file(fn) == {"x": "1"}
+
+ def test_custom_ini(self, tmp_path: Path) -> None:
+ """[pytest] section in any .ini file is read correctly"""
+ fn = tmp_path / "custom.ini"
+ fn.write_text("[pytest]\nx=1", encoding="utf-8")
+ assert load_config_dict_from_file(fn) == {"x": "1"}
+
+ def test_custom_ini_without_section(self, tmp_path: Path) -> None:
+ """Custom .ini files without [pytest] section are not considered for configuration"""
+ fn = tmp_path / "custom.ini"
+ fn.write_text("[custom]", encoding="utf-8")
+ assert load_config_dict_from_file(fn) is None
+
+ def test_custom_cfg_file(self, tmp_path: Path) -> None:
+ """Custom .cfg files without [tool:pytest] section are not considered for configuration"""
+ fn = tmp_path / "custom.cfg"
+ fn.write_text("[custom]", encoding="utf-8")
+ assert load_config_dict_from_file(fn) is None
+
+ def test_valid_cfg_file(self, tmp_path: Path) -> None:
+ """Custom .cfg files with [tool:pytest] section are read correctly"""
+ fn = tmp_path / "custom.cfg"
+ fn.write_text("[tool:pytest]\nx=1", encoding="utf-8")
+ assert load_config_dict_from_file(fn) == {"x": "1"}
+
+ def test_unsupported_pytest_section_in_cfg_file(self, tmp_path: Path) -> None:
+ """.cfg files with [pytest] section are no longer supported and should fail to alert users"""
+ fn = tmp_path / "custom.cfg"
+ fn.write_text("[pytest]", encoding="utf-8")
+ with pytest.raises(pytest.fail.Exception):
+ load_config_dict_from_file(fn)
+
+ def test_invalid_toml_file(self, tmp_path: Path) -> None:
+ """Invalid .toml files should raise `UsageError`."""
+ fn = tmp_path / "myconfig.toml"
+ fn.write_text("]invalid toml[", encoding="utf-8")
+ with pytest.raises(UsageError):
+ load_config_dict_from_file(fn)
+
+ def test_custom_toml_file(self, tmp_path: Path) -> None:
+ """.toml files without [tool.pytest.ini_options] are not considered for configuration."""
+ fn = tmp_path / "myconfig.toml"
+ fn.write_text(
+ dedent(
+ """
+ [build_system]
+ x = 1
+ """
+ ),
+ encoding="utf-8",
+ )
+ assert load_config_dict_from_file(fn) is None
+
+ def test_valid_toml_file(self, tmp_path: Path) -> None:
+ """.toml files with [tool.pytest.ini_options] are read correctly, including changing
+ data types to str/list for compatibility with other configuration options."""
+ fn = tmp_path / "myconfig.toml"
+ fn.write_text(
+ dedent(
+ """
+ [tool.pytest.ini_options]
+ x = 1
+ y = 20.0
+ values = ["tests", "integration"]
+ name = "foo"
+ heterogeneous_array = [1, "str"]
+ """
+ ),
+ encoding="utf-8",
+ )
+ assert load_config_dict_from_file(fn) == {
+ "x": "1",
+ "y": "20.0",
+ "values": ["tests", "integration"],
+ "name": "foo",
+ "heterogeneous_array": [1, "str"],
+ }
+
+
+class TestCommonAncestor:
+ def test_has_ancestor(self, tmp_path: Path) -> None:
+ fn1 = tmp_path / "foo" / "bar" / "test_1.py"
+ fn1.parent.mkdir(parents=True)
+ fn1.touch()
+ 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"
+
+ def test_single_dir(self, tmp_path: Path) -> None:
+ assert get_common_ancestor([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
+
+
+def test_get_dirs_from_args(tmp_path):
+ """get_dirs_from_args() skips over non-existing directories and files"""
+ fn = tmp_path / "foo.py"
+ fn.touch()
+ d = tmp_path / "tests"
+ d.mkdir()
+ option = "--foobar=/foo.txt"
+ # xdist uses options in this format for its rsync feature (#7638)
+ xdist_rsync_option = "popen=c:/dest"
+ assert get_dirs_from_args(
+ [str(fn), str(tmp_path / "does_not_exist"), str(d), option, xdist_rsync_option]
+ ) == [fn.parent, d]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_helpconfig.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_helpconfig.py
new file mode 100644
index 0000000000..44c2c9295b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_helpconfig.py
@@ -0,0 +1,124 @@
+import pytest
+from _pytest.config import ExitCode
+from _pytest.pytester import Pytester
+
+
+def test_version_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None:
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
+ result = pytester.runpytest("--version", "--version")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines([f"*pytest*{pytest.__version__}*imported from*"])
+ if pytestconfig.pluginmanager.list_plugin_distinfo():
+ result.stdout.fnmatch_lines(["*setuptools registered plugins:", "*at*"])
+
+
+def test_version_less_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None:
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
+ result = pytester.runpytest("--version")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines([f"pytest {pytest.__version__}"])
+
+
+def test_versions():
+ """Regression check for the public version attributes in pytest."""
+ assert isinstance(pytest.__version__, str)
+ assert isinstance(pytest.version_tuple, tuple)
+
+
+def test_help(pytester: Pytester) -> None:
+ result = pytester.runpytest("--help")
+ 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:
+ --durations=N *
+ -V, --version display pytest version and information about plugins.
+ When given twice, also display information about
+ plugins.
+ *setup.cfg*
+ *minversion*
+ *to see*markers*pytest --markers*
+ *to see*fixtures*pytest --fixtures*
+ """
+ )
+
+
+def test_none_help_param_raises_exception(pytester: Pytester) -> None:
+ """Test that a None help param raises a TypeError."""
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("test_ini", None, default=True, type="bool")
+ """
+ )
+ result = pytester.runpytest("--help")
+ result.stderr.fnmatch_lines(
+ ["*TypeError: help argument cannot be None for test_ini*"]
+ )
+
+
+def test_empty_help_param(pytester: Pytester) -> None:
+ """Test that an empty help param is displayed correctly."""
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("test_ini", "", default=True, type="bool")
+ """
+ )
+ result = pytester.runpytest("--help")
+ assert result.ret == 0
+ lines = [
+ " required_plugins (args):",
+ " plugins that must be present for pytest to run*",
+ " test_ini (bool):*",
+ "environment variables:",
+ ]
+ result.stdout.fnmatch_lines(lines, consecutive=True)
+
+
+def test_hookvalidation_unknown(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_hello(xyz):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret != 0
+ result.stdout.fnmatch_lines(["*unknown hook*pytest_hello*"])
+
+
+def test_hookvalidation_optional(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ @pytest.hookimpl(optionalhook=True)
+ def pytest_hello(xyz):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+
+def test_traceconfig(pytester: Pytester) -> None:
+ result = pytester.runpytest("--traceconfig")
+ result.stdout.fnmatch_lines(["*using*pytest*", "*active plugins*"])
+
+
+def test_debug(pytester: Pytester) -> None:
+ result = pytester.runpytest_subprocess("--debug")
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+ p = pytester.path.joinpath("pytestdebug.log")
+ assert "pytest_sessionstart" in p.read_text("utf-8")
+
+
+def test_PYTEST_DEBUG(pytester: Pytester, monkeypatch) -> None:
+ monkeypatch.setenv("PYTEST_DEBUG", "1")
+ result = pytester.runpytest_subprocess()
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+ result.stderr.fnmatch_lines(
+ ["*pytest_plugin_registered*", "*manager*PluginManager*"]
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_junitxml.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_junitxml.py
new file mode 100644
index 0000000000..02531e8143
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_junitxml.py
@@ -0,0 +1,1703 @@
+import os
+import platform
+from datetime import datetime
+from pathlib import Path
+from typing import cast
+from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+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
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pytester import Pytester
+from _pytest.pytester import RunResult
+from _pytest.reports import BaseReport
+from _pytest.reports import TestReport
+from _pytest.stash import Stash
+
+
+@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:
+ return xmlschema.XMLSchema(f)
+
+
+class RunAndParse:
+ def __init__(self, pytester: Pytester, schema: xmlschema.XMLSchema) -> None:
+ self.pytester = pytester
+ self.schema = schema
+
+ def __call__(
+ self, *args: Union[str, "os.PathLike[str]"], family: Optional[str] = "xunit1"
+ ) -> Tuple[RunResult, "DomNode"]:
+ if family:
+ 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:
+ self.schema.validate(f)
+ xmldoc = minidom.parse(str(xml_path))
+ return result, DomNode(xmldoc)
+
+
+@pytest.fixture
+def run_and_parse(pytester: Pytester, schema: xmlschema.XMLSchema) -> RunAndParse:
+ """Fixture that returns a function that can be used to execute pytest and
+ return the parsed ``DomNode`` of the root xml node.
+
+ The ``family`` parameter is used to configure the ``junit_family`` of the written report.
+ "xunit2" is also automatically validated against the schema.
+ """
+ return RunAndParse(pytester, schema)
+
+
+def assert_attr(node, **kwargs):
+ __tracebackhide__ = True
+
+ def nodeval(node, name):
+ anode = node.getAttributeNode(name)
+ if anode is not None:
+ return anode.value
+
+ expected = {name: str(value) for name, value in kwargs.items()}
+ on_node = {name: nodeval(node, name) for name in expected}
+ assert on_node == expected
+
+
+class DomNode:
+ def __init__(self, dom):
+ self.__node = dom
+
+ def __repr__(self):
+ return self.__node.toxml()
+
+ def find_first_by_tag(self, tag):
+ return self.find_nth_by_tag(tag, 0)
+
+ def _by_tag(self, tag):
+ return self.__node.getElementsByTagName(tag)
+
+ @property
+ def children(self):
+ return [type(self)(x) for x in self.__node.childNodes]
+
+ @property
+ def get_unique_child(self):
+ children = self.children
+ assert len(children) == 1
+ return children[0]
+
+ def find_nth_by_tag(self, tag, n):
+ items = self._by_tag(tag)
+ try:
+ nth = items[n]
+ except IndexError:
+ pass
+ else:
+ return type(self)(nth)
+
+ def find_by_tag(self, tag):
+ t = type(self)
+ return [t(x) for x in self.__node.getElementsByTagName(tag)]
+
+ def __getitem__(self, key):
+ node = self.__node.getAttributeNode(key)
+ if node is not None:
+ return node.value
+
+ def assert_attr(self, **kwargs):
+ __tracebackhide__ = True
+ return assert_attr(self.__node, **kwargs)
+
+ def toxml(self):
+ return self.__node.toxml()
+
+ @property
+ def text(self):
+ return self.__node.childNodes[0].wholeText
+
+ @property
+ def tag(self):
+ return self.__node.tagName
+
+ @property
+ def next_sibling(self):
+ return type(self)(self.__node.nextSibling)
+
+
+parametrize_families = pytest.mark.parametrize("xunit_family", ["xunit1", "xunit2"])
+
+
+class TestPython:
+ @parametrize_families
+ def test_summing_simple(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_pass():
+ pass
+ def test_fail():
+ assert 0
+ def test_skip():
+ pytest.skip("")
+ @pytest.mark.xfail
+ def test_xfail():
+ assert 0
+ @pytest.mark.xfail
+ def test_xpass():
+ assert 1
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(name="pytest", errors=0, failures=1, skipped=2, tests=5)
+
+ @parametrize_families
+ def test_summing_simple_with_errors(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture
+ def fixture():
+ raise Exception()
+ def test_pass():
+ pass
+ def test_fail():
+ assert 0
+ def test_error(fixture):
+ pass
+ @pytest.mark.xfail
+ def test_xfail():
+ assert False
+ @pytest.mark.xfail(strict=True)
+ def test_xpass():
+ assert True
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(name="pytest", errors=1, failures=2, skipped=1, tests=5)
+
+ @parametrize_families
+ def test_hostname_in_xml(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_pass():
+ pass
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(hostname=platform.node())
+
+ @parametrize_families
+ def test_timestamp_in_xml(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_pass():
+ pass
+ """
+ )
+ start_time = datetime.now()
+ result, dom = run_and_parse(family=xunit_family)
+ node = dom.find_first_by_tag("testsuite")
+ timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f")
+ assert start_time <= timestamp < datetime.now()
+
+ def test_timing_function(
+ self, pytester: Pytester, run_and_parse: RunAndParse, mock_timing
+ ) -> None:
+ pytester.makepyfile(
+ """
+ from _pytest import timing
+ def setup_module():
+ timing.sleep(1)
+ def teardown_module():
+ timing.sleep(2)
+ def test_sleep():
+ timing.sleep(4)
+ """
+ )
+ result, dom = run_and_parse()
+ node = dom.find_first_by_tag("testsuite")
+ tnode = node.find_first_by_tag("testcase")
+ val = tnode["time"]
+ assert float(val) == 7.0
+
+ @pytest.mark.parametrize("duration_report", ["call", "total"])
+ def test_junit_duration_report(
+ self,
+ pytester: Pytester,
+ monkeypatch: MonkeyPatch,
+ 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
+
+ def node_reporter_wrapper(s, report):
+ report.duration = 1.0
+ reporter = original_node_reporter(s, report)
+ return reporter
+
+ monkeypatch.setattr(LogXML, "node_reporter", node_reporter_wrapper)
+
+ pytester.makepyfile(
+ """
+ def test_foo():
+ pass
+ """
+ )
+ result, dom = run_and_parse("-o", f"junit_duration_report={duration_report}")
+ node = dom.find_first_by_tag("testsuite")
+ tnode = node.find_first_by_tag("testcase")
+ val = float(tnode["time"])
+ if duration_report == "total":
+ assert val == 3.0
+ else:
+ assert duration_report == "call"
+ assert val == 1.0
+
+ @parametrize_families
+ def test_setup_error(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def arg(request):
+ raise ValueError("Error reason")
+ def test_function(arg):
+ pass
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(errors=1, tests=1)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(classname="test_setup_error", name="test_function")
+ fnode = tnode.find_first_by_tag("error")
+ fnode.assert_attr(message='failed on setup with "ValueError: Error reason"')
+ assert "ValueError" in fnode.toxml()
+
+ @parametrize_families
+ def test_teardown_error(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def arg():
+ yield
+ raise ValueError('Error reason')
+ def test_function(arg):
+ pass
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(classname="test_teardown_error", name="test_function")
+ fnode = tnode.find_first_by_tag("error")
+ fnode.assert_attr(message='failed on teardown with "ValueError: Error reason"')
+ assert "ValueError" in fnode.toxml()
+
+ @parametrize_families
+ def test_call_failure_teardown_error(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def arg():
+ yield
+ raise Exception("Teardown Exception")
+ def test_function(arg):
+ raise Exception("Call Exception")
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(errors=1, failures=1, tests=1)
+ first, second = dom.find_by_tag("testcase")
+ assert first
+ assert second
+ assert first != second
+ fnode = first.find_first_by_tag("failure")
+ fnode.assert_attr(message="Exception: Call Exception")
+ snode = second.find_first_by_tag("error")
+ snode.assert_attr(
+ message='failed on teardown with "Exception: Teardown Exception"'
+ )
+
+ @parametrize_families
+ def test_skip_contains_name_reason(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_skip():
+ pytest.skip("hello23")
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret == 0
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(skipped=1)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(classname="test_skip_contains_name_reason", name="test_skip")
+ snode = tnode.find_first_by_tag("skipped")
+ snode.assert_attr(type="pytest.skip", message="hello23")
+
+ @parametrize_families
+ def test_mark_skip_contains_name_reason(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skip(reason="hello24")
+ def test_skip():
+ assert True
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret == 0
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(skipped=1)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(
+ classname="test_mark_skip_contains_name_reason", name="test_skip"
+ )
+ snode = tnode.find_first_by_tag("skipped")
+ snode.assert_attr(type="pytest.skip", message="hello24")
+
+ @parametrize_families
+ def test_mark_skipif_contains_name_reason(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ GLOBAL_CONDITION = True
+ @pytest.mark.skipif(GLOBAL_CONDITION, reason="hello25")
+ def test_skip():
+ assert True
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret == 0
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(skipped=1)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(
+ classname="test_mark_skipif_contains_name_reason", name="test_skip"
+ )
+ snode = tnode.find_first_by_tag("skipped")
+ snode.assert_attr(type="pytest.skip", message="hello25")
+
+ @parametrize_families
+ def test_mark_skip_doesnt_capture_output(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skip(reason="foo")
+ def test_skip():
+ print("bar!")
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret == 0
+ node_xml = dom.find_first_by_tag("testsuite").toxml()
+ assert "bar!" not in node_xml
+
+ @parametrize_families
+ def test_classname_instance(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ class TestClass(object):
+ def test_method(self):
+ assert 0
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(failures=1)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(
+ classname="test_classname_instance.TestClass", name="test_method"
+ )
+
+ @parametrize_families
+ def test_classname_nested_dir(
+ 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")
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(failures=1)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(classname="sub.test_hello", name="test_func")
+
+ @parametrize_families
+ def test_internal_error(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makeconftest("def pytest_runtest_protocol(): 0 / 0")
+ pytester.makepyfile("def test_function(): pass")
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(errors=1, tests=1)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(classname="pytest", name="internal")
+ fnode = tnode.find_first_by_tag("error")
+ fnode.assert_attr(message="internal error")
+ assert "Division" in fnode.toxml()
+
+ @pytest.mark.parametrize(
+ "junit_logging", ["no", "log", "system-out", "system-err", "out-err", "all"]
+ )
+ @parametrize_families
+ def test_failure_function(
+ self,
+ pytester: Pytester,
+ junit_logging,
+ run_and_parse: RunAndParse,
+ xunit_family,
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import logging
+ import sys
+
+ def test_fail():
+ print("hello-stdout")
+ sys.stderr.write("hello-stderr\\n")
+ logging.info('info msg')
+ logging.warning('warning msg')
+ raise ValueError(42)
+ """
+ )
+
+ result, dom = run_and_parse(
+ "-o", "junit_logging=%s" % junit_logging, family=xunit_family
+ )
+ assert result.ret, "Expected ret > 0"
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(failures=1, tests=1)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(classname="test_failure_function", name="test_fail")
+ fnode = tnode.find_first_by_tag("failure")
+ fnode.assert_attr(message="ValueError: 42")
+ assert "ValueError" in fnode.toxml(), "ValueError not included"
+
+ if junit_logging in ["log", "all"]:
+ logdata = tnode.find_first_by_tag("system-out")
+ log_xml = logdata.toxml()
+ assert logdata.tag == "system-out", "Expected tag: system-out"
+ assert "info msg" not in log_xml, "Unexpected INFO message"
+ assert "warning msg" in log_xml, "Missing WARN message"
+ if junit_logging in ["system-out", "out-err", "all"]:
+ systemout = tnode.find_first_by_tag("system-out")
+ systemout_xml = systemout.toxml()
+ assert systemout.tag == "system-out", "Expected tag: system-out"
+ assert "info msg" not in systemout_xml, "INFO message found in system-out"
+ assert (
+ "hello-stdout" in systemout_xml
+ ), "Missing 'hello-stdout' in system-out"
+ if junit_logging in ["system-err", "out-err", "all"]:
+ systemerr = tnode.find_first_by_tag("system-err")
+ systemerr_xml = systemerr.toxml()
+ assert systemerr.tag == "system-err", "Expected tag: system-err"
+ assert "info msg" not in systemerr_xml, "INFO message found in system-err"
+ assert (
+ "hello-stderr" in systemerr_xml
+ ), "Missing 'hello-stderr' in system-err"
+ assert (
+ "warning msg" not in systemerr_xml
+ ), "WARN message found in system-err"
+ if junit_logging == "no":
+ assert not tnode.find_by_tag("log"), "Found unexpected content: log"
+ assert not tnode.find_by_tag(
+ "system-out"
+ ), "Found unexpected content: system-out"
+ assert not tnode.find_by_tag(
+ "system-err"
+ ), "Found unexpected content: system-err"
+
+ @parametrize_families
+ def test_failure_verbose_message(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import sys
+ def test_fail():
+ assert 0, "An error"
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ node = dom.find_first_by_tag("testsuite")
+ tnode = node.find_first_by_tag("testcase")
+ fnode = tnode.find_first_by_tag("failure")
+ fnode.assert_attr(message="AssertionError: An error\nassert 0")
+
+ @parametrize_families
+ def test_failure_escape(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize('arg1', "<&'", ids="<&'")
+ def test_func(arg1):
+ print(arg1)
+ assert 0
+ """
+ )
+ result, dom = run_and_parse(
+ "-o", "junit_logging=system-out", family=xunit_family
+ )
+ assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ 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
+ )
+ sysout = tnode.find_first_by_tag("system-out")
+ text = sysout.text
+ assert "%s\n" % char in text
+
+ @parametrize_families
+ def test_junit_prefixing(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_func():
+ assert 0
+ class TestHello(object):
+ def test_hello(self):
+ pass
+ """
+ )
+ result, dom = run_and_parse("--junitprefix=xyz", family=xunit_family)
+ assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(failures=1, tests=2)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(classname="xyz.test_junit_prefixing", name="test_func")
+ tnode = node.find_nth_by_tag("testcase", 1)
+ tnode.assert_attr(
+ classname="xyz.test_junit_prefixing.TestHello", name="test_hello"
+ )
+
+ @parametrize_families
+ def test_xfailure_function(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_xfail():
+ pytest.xfail("42")
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert not result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(skipped=1, tests=1)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(classname="test_xfailure_function", name="test_xfail")
+ fnode = tnode.find_first_by_tag("skipped")
+ fnode.assert_attr(type="pytest.xfail", message="42")
+
+ @parametrize_families
+ def test_xfailure_marker(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.xfail(reason="42")
+ def test_xfail():
+ assert False
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert not result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(skipped=1, tests=1)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(classname="test_xfailure_marker", name="test_xfail")
+ fnode = tnode.find_first_by_tag("skipped")
+ fnode.assert_attr(type="pytest.xfail", message="42")
+
+ @pytest.mark.parametrize(
+ "junit_logging", ["no", "log", "system-out", "system-err", "out-err", "all"]
+ )
+ def test_xfail_captures_output_once(
+ self, pytester: Pytester, junit_logging: str, run_and_parse: RunAndParse
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import sys
+ import pytest
+
+ @pytest.mark.xfail()
+ def test_fail():
+ sys.stdout.write('XFAIL This is stdout')
+ sys.stderr.write('XFAIL This is stderr')
+ assert 0
+ """
+ )
+ result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
+ node = dom.find_first_by_tag("testsuite")
+ tnode = node.find_first_by_tag("testcase")
+ if junit_logging in ["system-err", "out-err", "all"]:
+ assert len(tnode.find_by_tag("system-err")) == 1
+ else:
+ assert len(tnode.find_by_tag("system-err")) == 0
+
+ if junit_logging in ["log", "system-out", "out-err", "all"]:
+ assert len(tnode.find_by_tag("system-out")) == 1
+ else:
+ assert len(tnode.find_by_tag("system-out")) == 0
+
+ @parametrize_families
+ def test_xfailure_xpass(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.xfail
+ def test_xpass():
+ pass
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ # assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(skipped=0, tests=1)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(classname="test_xfailure_xpass", name="test_xpass")
+
+ @parametrize_families
+ def test_xfailure_xpass_strict(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.xfail(strict=True, reason="This needs to fail!")
+ def test_xpass():
+ pass
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ # assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(skipped=0, tests=1)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(classname="test_xfailure_xpass_strict", name="test_xpass")
+ fnode = tnode.find_first_by_tag("failure")
+ fnode.assert_attr(message="[XPASS(strict)] This needs to fail!")
+
+ @parametrize_families
+ def test_collect_error(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makepyfile("syntax error")
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(errors=1, tests=1)
+ tnode = node.find_first_by_tag("testcase")
+ fnode = tnode.find_first_by_tag("error")
+ fnode.assert_attr(message="collection failure")
+ assert "SyntaxError" in fnode.toxml()
+
+ def test_unicode(self, pytester: Pytester, run_and_parse: RunAndParse) -> None:
+ value = "hx\xc4\x85\xc4\x87\n"
+ pytester.makepyfile(
+ """\
+ # coding: latin1
+ def test_hello():
+ print(%r)
+ assert 0
+ """
+ % value
+ )
+ result, dom = run_and_parse()
+ assert result.ret == 1
+ tnode = dom.find_first_by_tag("testcase")
+ fnode = tnode.find_first_by_tag("failure")
+ assert "hx" in fnode.toxml()
+
+ def test_assertion_binchars(
+ self, pytester: Pytester, run_and_parse: RunAndParse
+ ) -> None:
+ """This test did fail when the escaping wasn't strict."""
+ pytester.makepyfile(
+ """
+
+ M1 = '\x01\x02\x03\x04'
+ M2 = '\x01\x02\x03\x05'
+
+ def test_str_compare():
+ assert M1 == M2
+ """
+ )
+ result, dom = run_and_parse()
+ print(dom.toxml())
+
+ @pytest.mark.parametrize("junit_logging", ["no", "system-out"])
+ def test_pass_captures_stdout(
+ self, pytester: Pytester, run_and_parse: RunAndParse, junit_logging: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_pass():
+ print('hello-stdout')
+ """
+ )
+ result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
+ node = dom.find_first_by_tag("testsuite")
+ pnode = node.find_first_by_tag("testcase")
+ if junit_logging == "no":
+ assert not node.find_by_tag(
+ "system-out"
+ ), "system-out should not be generated"
+ if junit_logging == "system-out":
+ systemout = pnode.find_first_by_tag("system-out")
+ assert (
+ "hello-stdout" in systemout.toxml()
+ ), "'hello-stdout' should be in system-out"
+
+ @pytest.mark.parametrize("junit_logging", ["no", "system-err"])
+ def test_pass_captures_stderr(
+ self, pytester: Pytester, run_and_parse: RunAndParse, junit_logging: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import sys
+ def test_pass():
+ sys.stderr.write('hello-stderr')
+ """
+ )
+ result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
+ node = dom.find_first_by_tag("testsuite")
+ pnode = node.find_first_by_tag("testcase")
+ if junit_logging == "no":
+ assert not node.find_by_tag(
+ "system-err"
+ ), "system-err should not be generated"
+ if junit_logging == "system-err":
+ systemerr = pnode.find_first_by_tag("system-err")
+ assert (
+ "hello-stderr" in systemerr.toxml()
+ ), "'hello-stderr' should be in system-err"
+
+ @pytest.mark.parametrize("junit_logging", ["no", "system-out"])
+ def test_setup_error_captures_stdout(
+ self, pytester: Pytester, run_and_parse: RunAndParse, junit_logging: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def arg(request):
+ print('hello-stdout')
+ raise ValueError()
+ def test_function(arg):
+ pass
+ """
+ )
+ result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
+ node = dom.find_first_by_tag("testsuite")
+ pnode = node.find_first_by_tag("testcase")
+ if junit_logging == "no":
+ assert not node.find_by_tag(
+ "system-out"
+ ), "system-out should not be generated"
+ if junit_logging == "system-out":
+ systemout = pnode.find_first_by_tag("system-out")
+ assert (
+ "hello-stdout" in systemout.toxml()
+ ), "'hello-stdout' should be in system-out"
+
+ @pytest.mark.parametrize("junit_logging", ["no", "system-err"])
+ def test_setup_error_captures_stderr(
+ self, pytester: Pytester, run_and_parse: RunAndParse, junit_logging: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import sys
+ import pytest
+
+ @pytest.fixture
+ def arg(request):
+ sys.stderr.write('hello-stderr')
+ raise ValueError()
+ def test_function(arg):
+ pass
+ """
+ )
+ result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
+ node = dom.find_first_by_tag("testsuite")
+ pnode = node.find_first_by_tag("testcase")
+ if junit_logging == "no":
+ assert not node.find_by_tag(
+ "system-err"
+ ), "system-err should not be generated"
+ if junit_logging == "system-err":
+ systemerr = pnode.find_first_by_tag("system-err")
+ assert (
+ "hello-stderr" in systemerr.toxml()
+ ), "'hello-stderr' should be in system-err"
+
+ @pytest.mark.parametrize("junit_logging", ["no", "system-out"])
+ def test_avoid_double_stdout(
+ self, pytester: Pytester, run_and_parse: RunAndParse, junit_logging: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ import sys
+ import pytest
+
+ @pytest.fixture
+ def arg(request):
+ yield
+ sys.stdout.write('hello-stdout teardown')
+ raise ValueError()
+ def test_function(arg):
+ sys.stdout.write('hello-stdout call')
+ """
+ )
+ result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
+ node = dom.find_first_by_tag("testsuite")
+ pnode = node.find_first_by_tag("testcase")
+ if junit_logging == "no":
+ assert not node.find_by_tag(
+ "system-out"
+ ), "system-out should not be generated"
+ if junit_logging == "system-out":
+ systemout = pnode.find_first_by_tag("system-out")
+ assert "hello-stdout call" in systemout.toxml()
+ assert "hello-stdout teardown" in systemout.toxml()
+
+
+def test_mangle_test_address() -> None:
+ from _pytest.junitxml import mangle_test_address
+
+ address = "::".join(["a/my.py.thing.py", "Class", "method", "[a-1-::]"])
+ newnames = mangle_test_address(address)
+ assert newnames == ["a.my.py.thing", "Class", "method", "[a-1-::]"]
+
+
+def test_dont_configure_on_workers(tmp_path: Path) -> None:
+ gotten: List[object] = []
+
+ class FakeConfig:
+ if TYPE_CHECKING:
+ workerinput = None
+
+ def __init__(self):
+ self.pluginmanager = self
+ self.option = self
+ self.stash = Stash()
+
+ def getini(self, name):
+ return "pytest"
+
+ junitprefix = None
+ # XXX: shouldn't need tmp_path ?
+ xmlpath = str(tmp_path.joinpath("junix.xml"))
+ register = gotten.append
+
+ fake_config = cast(Config, FakeConfig())
+ from _pytest import junitxml
+
+ junitxml.pytest_configure(fake_config)
+ assert len(gotten) == 1
+ FakeConfig.workerinput = None
+ junitxml.pytest_configure(fake_config)
+ assert len(gotten) == 1
+
+
+class TestNonPython:
+ @parametrize_families
+ def test_summing_simple(
+ self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+ ) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ def pytest_collect_file(file_path, parent):
+ if file_path.suffix == ".xyz":
+ return MyItem.from_parent(name=file_path.name, parent=parent)
+ class MyItem(pytest.Item):
+ def runtest(self):
+ raise ValueError(42)
+ def repr_failure(self, excinfo):
+ return "custom item runtest failed"
+ """
+ )
+ pytester.path.joinpath("myfile.xyz").write_text("hello")
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(errors=0, failures=1, skipped=0, tests=1)
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(name="myfile.xyz")
+ fnode = tnode.find_first_by_tag("failure")
+ fnode.assert_attr(message="custom item runtest failed")
+ assert "custom item runtest failed" in fnode.toxml()
+
+
+@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)
+ pytester.makepyfile(
+ """
+ import sys
+ def test_print_nullbyte():
+ sys.stdout.write('Here the null -->' + chr(0) + '<--')
+ sys.stdout.write('In repr form -->' + repr(chr(0)) + '<--')
+ assert False
+ """
+ )
+ xmlf = pytester.path.joinpath("junit.xml")
+ pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging)
+ text = xmlf.read_text()
+ assert "\x00" not in text
+ if junit_logging == "system-out":
+ assert "#x00" in text
+ if junit_logging == "no":
+ assert "#x00" not in text
+
+
+@pytest.mark.parametrize("junit_logging", ["no", "system-out"])
+def test_nullbyte_replace(pytester: Pytester, junit_logging: str) -> None:
+ # Check if the null byte gets replaced
+ pytester.makepyfile(
+ """
+ import sys
+ def test_print_nullbyte():
+ sys.stdout.write('Here the null -->' + chr(0) + '<--')
+ sys.stdout.write('In repr form -->' + repr(chr(0)) + '<--')
+ assert False
+ """
+ )
+ xmlf = pytester.path.joinpath("junit.xml")
+ pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging)
+ text = xmlf.read_text()
+ if junit_logging == "system-out":
+ assert "#x0" in text
+ if junit_logging == "no":
+ assert "#x0" not in text
+
+
+def test_invalid_xml_escape() -> None:
+ # Test some more invalid xml chars, the full range should be
+ # tested really but let's just test the edges of the ranges
+ # instead.
+ # XXX This only tests low unicode character points for now as
+ # there are some issues with the testing infrastructure for
+ # the higher ones.
+ # XXX Testing 0xD (\r) is tricky as it overwrites the just written
+ # line in the output, so we skip it too.
+ invalid = (
+ 0x00,
+ 0x1,
+ 0xB,
+ 0xC,
+ 0xE,
+ 0x19,
+ 27, # issue #126
+ 0xD800,
+ 0xDFFF,
+ 0xFFFE,
+ 0x0FFFF,
+ ) # , 0x110000)
+ valid = (0x9, 0xA, 0x20)
+ # 0xD, 0xD7FF, 0xE000, 0xFFFD, 0x10000, 0x10FFFF)
+
+ for i in invalid:
+ got = bin_xml_escape(chr(i))
+ if i <= 0xFF:
+ expected = "#x%02X" % i
+ else:
+ expected = "#x%04X" % i
+ assert got == expected
+ for i in valid:
+ assert chr(i) == bin_xml_escape(chr(i))
+
+
+def test_logxml_path_expansion(tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
+ home_tilde = Path(os.path.expanduser("~")).joinpath("test.xml")
+ xml_tilde = LogXML(Path("~", "test.xml"), None)
+ assert xml_tilde.logfile == str(home_tilde)
+
+ monkeypatch.setenv("HOME", str(tmp_path))
+ home_var = os.path.normpath(os.path.expandvars("$HOME/test.xml"))
+ xml_var = LogXML(Path("$HOME", "test.xml"), None)
+ assert xml_var.logfile == str(home_var)
+
+
+def test_logxml_changingdir(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_func():
+ import os
+ os.chdir("a")
+ """
+ )
+ pytester.mkdir("a")
+ result = pytester.runpytest("--junitxml=a/x.xml")
+ assert result.ret == 0
+ assert pytester.path.joinpath("a/x.xml").exists()
+
+
+def test_logxml_makedir(pytester: Pytester) -> None:
+ """--junitxml should automatically create directories for the xml file"""
+ pytester.makepyfile(
+ """
+ def test_pass():
+ pass
+ """
+ )
+ result = pytester.runpytest("--junitxml=path/to/results.xml")
+ assert result.ret == 0
+ assert pytester.path.joinpath("path/to/results.xml").exists()
+
+
+def test_logxml_check_isdir(pytester: Pytester) -> None:
+ """Give an error if --junit-xml is a directory (#2089)"""
+ result = pytester.runpytest("--junit-xml=.")
+ result.stderr.fnmatch_lines(["*--junitxml must be a filename*"])
+
+
+def test_escaped_parametrized_names_xml(
+ pytester: Pytester, run_and_parse: RunAndParse
+) -> None:
+ pytester.makepyfile(
+ """\
+ import pytest
+ @pytest.mark.parametrize('char', ["\\x00"])
+ def test_func(char):
+ assert char
+ """
+ )
+ result, dom = run_and_parse()
+ assert result.ret == 0
+ node = dom.find_first_by_tag("testcase")
+ node.assert_attr(name="test_func[\\x00]")
+
+
+def test_double_colon_split_function_issue469(
+ pytester: Pytester, run_and_parse: RunAndParse
+) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize('param', ["double::colon"])
+ def test_func(param):
+ pass
+ """
+ )
+ result, dom = run_and_parse()
+ assert result.ret == 0
+ node = dom.find_first_by_tag("testcase")
+ node.assert_attr(classname="test_double_colon_split_function_issue469")
+ node.assert_attr(name="test_func[double::colon]")
+
+
+def test_double_colon_split_method_issue469(
+ pytester: Pytester, run_and_parse: RunAndParse
+) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ class TestClass(object):
+ @pytest.mark.parametrize('param', ["double::colon"])
+ def test_func(self, param):
+ pass
+ """
+ )
+ result, dom = run_and_parse()
+ assert result.ret == 0
+ node = dom.find_first_by_tag("testcase")
+ node.assert_attr(classname="test_double_colon_split_method_issue469.TestClass")
+ node.assert_attr(name="test_func[double::colon]")
+
+
+def test_unicode_issue368(pytester: Pytester) -> None:
+ path = pytester.path.joinpath("test.xml")
+ log = LogXML(str(path), None)
+ ustr = "ВНИ!"
+
+ class Report(BaseReport):
+ longrepr = ustr
+ sections: List[Tuple[str, str]] = []
+ nodeid = "something"
+ location = "tests/filename.py", 42, "TestClass.method"
+ when = "teardown"
+
+ test_report = cast(TestReport, Report())
+
+ # hopefully this is not too brittle ...
+ log.pytest_sessionstart()
+ node_reporter = log._opentestcase(test_report)
+ node_reporter.append_failure(test_report)
+ node_reporter.append_collect_error(test_report)
+ node_reporter.append_collect_skipped(test_report)
+ node_reporter.append_error(test_report)
+ test_report.longrepr = "filename", 1, ustr
+ 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]
+ node_reporter.append_skipped(test_report)
+ log.pytest_sessionfinish()
+
+
+def test_record_property(pytester: Pytester, run_and_parse: RunAndParse) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def other(record_property):
+ record_property("bar", 1)
+ def test_record(record_property, other):
+ record_property("foo", "<1");
+ """
+ )
+ result, dom = run_and_parse()
+ node = dom.find_first_by_tag("testsuite")
+ tnode = node.find_first_by_tag("testcase")
+ psnode = tnode.find_first_by_tag("properties")
+ 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 passed in *"])
+
+
+def test_record_property_same_name(
+ pytester: Pytester, run_and_parse: RunAndParse
+) -> None:
+ pytester.makepyfile(
+ """
+ def test_record_with_same_name(record_property):
+ record_property("foo", "bar")
+ record_property("foo", "baz")
+ """
+ )
+ result, dom = run_and_parse()
+ node = dom.find_first_by_tag("testsuite")
+ tnode = node.find_first_by_tag("testcase")
+ psnode = tnode.find_first_by_tag("properties")
+ pnodes = psnode.find_by_tag("property")
+ pnodes[0].assert_attr(name="foo", value="bar")
+ pnodes[1].assert_attr(name="foo", value="baz")
+
+
+@pytest.mark.parametrize("fixture_name", ["record_property", "record_xml_attribute"])
+def test_record_fixtures_without_junitxml(
+ pytester: Pytester, fixture_name: str
+) -> None:
+ pytester.makepyfile(
+ """
+ def test_record({fixture_name}):
+ {fixture_name}("foo", "bar")
+ """.format(
+ fixture_name=fixture_name
+ )
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+
+
+@pytest.mark.filterwarnings("default")
+def test_record_attribute(pytester: Pytester, run_and_parse: RunAndParse) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ junit_family = xunit1
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def other(record_xml_attribute):
+ record_xml_attribute("bar", 1)
+ def test_record(record_xml_attribute, other):
+ record_xml_attribute("foo", "<1");
+ """
+ )
+ result, dom = run_and_parse()
+ node = dom.find_first_by_tag("testsuite")
+ tnode = node.find_first_by_tag("testcase")
+ tnode.assert_attr(bar="1")
+ tnode.assert_attr(foo="<1")
+ result.stdout.fnmatch_lines(
+ ["*test_record_attribute.py:6:*record_xml_attribute is an experimental feature"]
+ )
+
+
+@pytest.mark.filterwarnings("default")
+@pytest.mark.parametrize("fixture_name", ["record_xml_attribute", "record_property"])
+def test_record_fixtures_xunit2(
+ pytester: Pytester, fixture_name: str, run_and_parse: RunAndParse
+) -> None:
+ """Ensure record_xml_attribute and record_property drop values when outside of legacy family."""
+ pytester.makeini(
+ """
+ [pytest]
+ junit_family = xunit2
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def other({fixture_name}):
+ {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)
+ expected_lines = []
+ if fixture_name == "record_xml_attribute":
+ expected_lines.append(
+ "*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
+ )
+ ]
+ result.stdout.fnmatch_lines(expected_lines)
+
+
+def test_random_report_log_xdist(
+ pytester: Pytester, monkeypatch: MonkeyPatch, run_and_parse: RunAndParse
+) -> None:
+ """`xdist` calls pytest_runtest_logreport as they are executed by the workers,
+ with nodes from several nodes overlapping, so junitxml must cope with that
+ to produce correct reports (#1064)."""
+ pytest.importorskip("xdist")
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
+ pytester.makepyfile(
+ """
+ import pytest, time
+ @pytest.mark.parametrize('i', list(range(30)))
+ def test_x(i):
+ assert i != 22
+ """
+ )
+ _, dom = run_and_parse("-n2")
+ suite_node = dom.find_first_by_tag("testsuite")
+ failed = []
+ for case_node in suite_node.find_by_tag("testcase"):
+ if case_node.find_first_by_tag("failure"):
+ failed.append(case_node["name"])
+
+ assert failed == ["test_x[22]"]
+
+
+@parametrize_families
+def test_root_testsuites_tag(
+ pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+) -> None:
+ pytester.makepyfile(
+ """
+ def test_x():
+ pass
+ """
+ )
+ _, dom = run_and_parse(family=xunit_family)
+ root = dom.get_unique_child
+ assert root.tag == "testsuites"
+ suite_node = root.get_unique_child
+ assert suite_node.tag == "testsuite"
+
+
+def test_runs_twice(pytester: Pytester, run_and_parse: RunAndParse) -> None:
+ f = pytester.makepyfile(
+ """
+ def test_pass():
+ pass
+ """
+ )
+
+ result, dom = run_and_parse(f, f)
+ result.stdout.no_fnmatch_line("*INTERNALERROR*")
+ first, second = (x["classname"] for x in dom.find_by_tag("testcase"))
+ assert first == second
+
+
+def test_runs_twice_xdist(
+ pytester: Pytester, monkeypatch: MonkeyPatch, run_and_parse: RunAndParse
+) -> None:
+ pytest.importorskip("xdist")
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
+ f = pytester.makepyfile(
+ """
+ def test_pass():
+ pass
+ """
+ )
+
+ result, dom = run_and_parse(f, "--dist", "each", "--tx", "2*popen")
+ result.stdout.no_fnmatch_line("*INTERNALERROR*")
+ first, second = (x["classname"] for x in dom.find_by_tag("testcase"))
+ assert first == second
+
+
+def test_fancy_items_regression(pytester: Pytester, run_and_parse: RunAndParse) -> None:
+ # issue 1259
+ pytester.makeconftest(
+ """
+ import pytest
+ class FunItem(pytest.Item):
+ def runtest(self):
+ pass
+ class NoFunItem(pytest.Item):
+ def runtest(self):
+ pass
+
+ class FunCollector(pytest.File):
+ def collect(self):
+ return [
+ FunItem.from_parent(name='a', parent=self),
+ NoFunItem.from_parent(name='a', parent=self),
+ NoFunItem.from_parent(name='b', parent=self),
+ ]
+
+ def pytest_collect_file(file_path, parent):
+ if file_path.suffix == '.py':
+ return FunCollector.from_parent(path=file_path, parent=parent)
+ """
+ )
+
+ pytester.makepyfile(
+ """
+ def test_pass():
+ pass
+ """
+ )
+
+ result, dom = run_and_parse()
+
+ result.stdout.no_fnmatch_line("*INTERNALERROR*")
+
+ items = sorted("%(classname)s %(name)s" % x for x in dom.find_by_tag("testcase"))
+ import pprint
+
+ pprint.pprint(items)
+ assert items == [
+ "conftest a",
+ "conftest a",
+ "conftest b",
+ "test_fancy_items_regression a",
+ "test_fancy_items_regression a",
+ "test_fancy_items_regression b",
+ "test_fancy_items_regression test_pass",
+ ]
+
+
+@parametrize_families
+def test_global_properties(pytester: Pytester, xunit_family: str) -> None:
+ path = pytester.path.joinpath("test_global_properties.xml")
+ log = LogXML(str(path), None, family=xunit_family)
+
+ class Report(BaseReport):
+ sections: List[Tuple[str, str]] = []
+ nodeid = "test_node_id"
+
+ log.pytest_sessionstart()
+ log.add_global_property("foo", "1")
+ log.add_global_property("bar", "2")
+ log.pytest_sessionfinish()
+
+ dom = minidom.parse(str(path))
+
+ properties = dom.getElementsByTagName("properties")
+
+ assert properties.length == 1, "There must be one <properties> node"
+
+ property_list = dom.getElementsByTagName("property")
+
+ assert property_list.length == 2, "There most be only 2 property nodes"
+
+ expected = {"foo": "1", "bar": "2"}
+ actual = {}
+
+ for p in property_list:
+ k = str(p.getAttribute("name"))
+ v = str(p.getAttribute("value"))
+ actual[k] = v
+
+ assert actual == expected
+
+
+def test_url_property(pytester: Pytester) -> None:
+ test_url = "http://www.github.com/pytest-dev"
+ path = pytester.path.joinpath("test_url_property.xml")
+ log = LogXML(str(path), None)
+
+ class Report(BaseReport):
+ longrepr = "FooBarBaz"
+ sections: List[Tuple[str, str]] = []
+ nodeid = "something"
+ location = "tests/filename.py", 42, "TestClass.method"
+ url = test_url
+
+ test_report = cast(TestReport, Report())
+
+ log.pytest_sessionstart()
+ node_reporter = log._opentestcase(test_report)
+ node_reporter.append_failure(test_report)
+ log.pytest_sessionfinish()
+
+ test_case = minidom.parse(str(path)).getElementsByTagName("testcase")[0]
+
+ assert (
+ test_case.getAttribute("url") == test_url
+ ), "The URL did not get written to the xml"
+
+
+@parametrize_families
+def test_record_testsuite_property(
+ pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str
+) -> None:
+ pytester.makepyfile(
+ """
+ def test_func1(record_testsuite_property):
+ record_testsuite_property("stats", "all good")
+
+ def test_func2(record_testsuite_property):
+ record_testsuite_property("stats", 10)
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret == 0
+ node = dom.find_first_by_tag("testsuite")
+ properties_node = node.find_first_by_tag("properties")
+ p1_node = properties_node.find_nth_by_tag("property", 0)
+ p2_node = properties_node.find_nth_by_tag("property", 1)
+ p1_node.assert_attr(name="stats", value="all good")
+ p2_node.assert_attr(name="stats", value="10")
+
+
+def test_record_testsuite_property_junit_disabled(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_func1(record_testsuite_property):
+ record_testsuite_property("stats", "all good")
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+
+
+@pytest.mark.parametrize("junit", [True, False])
+def test_record_testsuite_property_type_checking(
+ pytester: Pytester, junit: bool
+) -> None:
+ pytester.makepyfile(
+ """
+ def test_func1(record_testsuite_property):
+ record_testsuite_property(1, 2)
+ """
+ )
+ args = ("--junitxml=tests.xml",) if junit else ()
+ result = pytester.runpytest(*args)
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(
+ ["*TypeError: name parameter needs to be a string, but int given"]
+ )
+
+
+@pytest.mark.parametrize("suite_name", ["my_suite", ""])
+@parametrize_families
+def test_set_suite_name(
+ pytester: Pytester, suite_name: str, run_and_parse: RunAndParse, xunit_family: str
+) -> None:
+ if suite_name:
+ pytester.makeini(
+ """
+ [pytest]
+ junit_suite_name={suite_name}
+ junit_family={family}
+ """.format(
+ suite_name=suite_name, family=xunit_family
+ )
+ )
+ expected = suite_name
+ else:
+ expected = "pytest"
+ pytester.makepyfile(
+ """
+ import pytest
+
+ def test_func():
+ pass
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret == 0
+ node = dom.find_first_by_tag("testsuite")
+ node.assert_attr(name=expected)
+
+
+def test_escaped_skipreason_issue3533(
+ pytester: Pytester, run_and_parse: RunAndParse
+) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skip(reason='1 <> 2')
+ def test_skip():
+ pass
+ """
+ )
+ _, dom = run_and_parse()
+ node = dom.find_first_by_tag("testcase")
+ snode = node.find_first_by_tag("skipped")
+ assert "1 <> 2" in snode.text
+ snode.assert_attr(message="1 <> 2")
+
+
+@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(
+ """
+ [pytest]
+ junit_log_passing_tests=False
+ junit_logging=system-out
+ junit_family={family}
+ """.format(
+ family=xunit_family
+ )
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+ import logging
+ import sys
+
+ def test_func():
+ sys.stdout.write('This is stdout')
+ sys.stderr.write('This is stderr')
+ logging.warning('hello')
+ """
+ )
+ result, dom = run_and_parse(family=xunit_family)
+ assert result.ret == 0
+ node = dom.find_first_by_tag("testcase")
+ assert len(node.find_by_tag("system-err")) == 0
+ assert len(node.find_by_tag("system-out")) == 0
+
+
+@parametrize_families
+@pytest.mark.parametrize("junit_logging", ["no", "system-out", "system-err"])
+def test_logging_passing_tests_disabled_logs_output_for_failing_test_issue5430(
+ pytester: Pytester,
+ junit_logging: str,
+ run_and_parse: RunAndParse,
+ xunit_family: str,
+) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ junit_log_passing_tests=False
+ junit_family={family}
+ """.format(
+ family=xunit_family
+ )
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+ import logging
+ import sys
+
+ def test_func():
+ logging.warning('hello')
+ assert 0
+ """
+ )
+ result, dom = run_and_parse(
+ "-o", "junit_logging=%s" % junit_logging, family=xunit_family
+ )
+ assert result.ret == 1
+ node = dom.find_first_by_tag("testcase")
+ if junit_logging == "system-out":
+ assert len(node.find_by_tag("system-err")) == 0
+ assert len(node.find_by_tag("system-out")) == 1
+ elif junit_logging == "system-err":
+ assert len(node.find_by_tag("system-err")) == 1
+ assert len(node.find_by_tag("system-out")) == 0
+ else:
+ assert junit_logging == "no"
+ assert len(node.find_by_tag("system-err")) == 0
+ assert len(node.find_by_tag("system-out")) == 0
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_legacypath.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_legacypath.py
new file mode 100644
index 0000000000..8acafe98e7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_legacypath.py
@@ -0,0 +1,180 @@
+from pathlib import Path
+
+import pytest
+from _pytest.compat import LEGACY_PATH
+from _pytest.legacypath import TempdirFactory
+from _pytest.legacypath import Testdir
+
+
+def test_item_fspath(pytester: pytest.Pytester) -> None:
+ pytester.makepyfile("def test_func(): pass")
+ items, hookrec = pytester.inline_genitems()
+ assert len(items) == 1
+ (item,) = items
+ 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.path == item.path
+
+
+def test_testdir_testtmproot(testdir: Testdir) -> None:
+ """Check test_tmproot is a py.path attribute for backward compatibility."""
+ assert testdir.test_tmproot.check(dir=1)
+
+
+def test_testdir_makefile_dot_prefixes_extension_silently(
+ testdir: Testdir,
+) -> None:
+ """For backwards compat #8192"""
+ p1 = testdir.makefile("foo.bar", "")
+ assert ".foo.bar" in str(p1)
+
+
+def test_testdir_makefile_ext_none_raises_type_error(testdir: Testdir) -> None:
+ """For backwards compat #8192"""
+ with pytest.raises(TypeError):
+ testdir.makefile(None, "")
+
+
+def test_testdir_makefile_ext_empty_string_makes_file(testdir: Testdir) -> None:
+ """For backwards compat #8192"""
+ p1 = testdir.makefile("", "")
+ assert "test_testdir_makefile" in str(p1)
+
+
+def attempt_symlink_to(path: str, to_path: str) -> None:
+ """Try to make a symlink from "path" to "to_path", skipping in case this platform
+ does not support it or we don't have sufficient privileges (common on Windows)."""
+ try:
+ Path(path).symlink_to(Path(to_path))
+ except OSError:
+ pytest.skip("could not create symbolic link")
+
+
+def test_tmpdir_factory(
+ tmpdir_factory: TempdirFactory,
+ tmp_path_factory: pytest.TempPathFactory,
+) -> None:
+ assert str(tmpdir_factory.getbasetemp()) == str(tmp_path_factory.getbasetemp())
+ dir = tmpdir_factory.mktemp("foo")
+ assert dir.exists()
+
+
+def test_tmpdir_equals_tmp_path(tmpdir: LEGACY_PATH, tmp_path: Path) -> None:
+ assert Path(tmpdir) == tmp_path
+
+
+def test_tmpdir_always_is_realpath(pytester: pytest.Pytester) -> None:
+ # See test_tmp_path_always_is_realpath.
+ realtemp = pytester.mkdir("myrealtemp")
+ linktemp = pytester.path.joinpath("symlinktemp")
+ attempt_symlink_to(str(linktemp), str(realtemp))
+ p = pytester.makepyfile(
+ """
+ def test_1(tmpdir):
+ import os
+ assert os.path.realpath(str(tmpdir)) == str(tmpdir)
+ """
+ )
+ result = pytester.runpytest("-s", p, "--basetemp=%s/bt" % linktemp)
+ assert not result.ret
+
+
+def test_cache_makedir(cache: pytest.Cache) -> None:
+ dir = cache.makedir("foo") # type: ignore[attr-defined]
+ assert dir.exists()
+ dir.remove()
+
+
+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 req.path == modcol.path
+ assert req.fspath == modcol.fspath # type: ignore[attr-defined]
+
+
+class TestFixtureRequestSessionScoped:
+ @pytest.fixture(scope="session")
+ def session_request(self, request):
+ return request
+
+ def test_session_scoped_unavailable_attributes(self, session_request):
+ with pytest.raises(
+ AttributeError,
+ match="path not available in session-scoped context",
+ ):
+ session_request.fspath
+
+
+@pytest.mark.parametrize("config_type", ["ini", "pyproject"])
+def test_addini_paths(pytester: pytest.Pytester, config_type: str) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("paths", "my new ini value", type="pathlist")
+ parser.addini("abc", "abc value")
+ """
+ )
+ if config_type == "ini":
+ inipath = pytester.makeini(
+ """
+ [pytest]
+ paths=hello world/sub.py
+ """
+ )
+ elif config_type == "pyproject":
+ inipath = pytester.makepyprojecttoml(
+ """
+ [tool.pytest.ini_options]
+ paths=["hello", "world/sub.py"]
+ """
+ )
+ config = pytester.parseconfig()
+ values = config.getini("paths")
+ assert len(values) == 2
+ assert values[0] == inipath.parent.joinpath("hello")
+ assert values[1] == inipath.parent.joinpath("world/sub.py")
+ pytest.raises(ValueError, config.getini, "other")
+
+
+def test_override_ini_paths(pytester: pytest.Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_addoption(parser):
+ parser.addini("paths", "my new ini value", type="pathlist")"""
+ )
+ pytester.makeini(
+ """
+ [pytest]
+ paths=blah.py"""
+ )
+ pytester.makepyfile(
+ r"""
+ def test_overriden(pytestconfig):
+ config_paths = pytestconfig.getini("paths")
+ print(config_paths)
+ for cpf in config_paths:
+ print('\nuser_path:%s' % cpf.basename)
+ """
+ )
+ result = pytester.runpytest("--override-ini", "paths=foo/bar1.py foo/bar2.py", "-s")
+ result.stdout.fnmatch_lines(["user_path:bar1.py", "user_path:bar2.py"])
+
+
+def test_inifile_from_cmdline_main_hook(pytester: pytest.Pytester) -> None:
+ """Ensure Config.inifile is available during pytest_cmdline_main (#9396)."""
+ p = pytester.makeini(
+ """
+ [pytest]
+ """
+ )
+ pytester.makeconftest(
+ """
+ def pytest_cmdline_main(config):
+ print("pytest_cmdline_main inifile =", config.inifile)
+ """
+ )
+ result = pytester.runpytest_subprocess("-s")
+ result.stdout.fnmatch_lines(f"*pytest_cmdline_main inifile = {p}")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_link_resolve.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_link_resolve.py
new file mode 100644
index 0000000000..60a86ada36
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_link_resolve.py
@@ -0,0 +1,80 @@
+import os.path
+import subprocess
+import sys
+import textwrap
+from contextlib import contextmanager
+from pathlib import Path
+from string import ascii_lowercase
+
+from _pytest.pytester import Pytester
+
+
+@contextmanager
+def subst_path_windows(filepath: Path):
+ for c in ascii_lowercase[7:]: # Create a subst drive from H-Z.
+ c += ":"
+ if not os.path.exists(c):
+ drive = c
+ break
+ else:
+ raise AssertionError("Unable to find suitable drive letter for subst.")
+
+ directory = filepath.parent
+ basename = filepath.name
+
+ args = ["subst", drive, str(directory)]
+ subprocess.check_call(args)
+ assert os.path.exists(drive)
+ try:
+ filename = Path(drive, os.sep, basename)
+ yield filename
+ finally:
+ args = ["subst", "/D", drive]
+ subprocess.check_call(args)
+
+
+@contextmanager
+def subst_path_linux(filepath: Path):
+ directory = filepath.parent
+ basename = filepath.name
+
+ target = directory / ".." / "sub2"
+ os.symlink(str(directory), str(target), target_is_directory=True)
+ try:
+ filename = target / basename
+ yield filename
+ finally:
+ # We don't need to unlink (it's all in the tempdir).
+ pass
+
+
+def test_link_resolve(pytester: Pytester) -> None:
+ """See: https://github.com/pytest-dev/pytest/issues/5965."""
+ sub1 = pytester.mkpydir("sub1")
+ p = sub1.joinpath("test_foo.py")
+ p.write_text(
+ textwrap.dedent(
+ """
+ import pytest
+ def test_foo():
+ raise AssertionError()
+ """
+ )
+ )
+
+ subst = subst_path_linux
+ if sys.platform == "win32":
+ subst = subst_path_windows
+
+ with subst(p) as subst_p:
+ result = pytester.runpytest(str(subst_p), "-v")
+ # i.e.: Make sure that the error is reported as a relative path, not as a
+ # resolved path.
+ # See: https://github.com/pytest-dev/pytest/issues/5965
+ stdout = result.stdout.str()
+ assert "sub1/test_foo.py" not in stdout
+
+ # i.e.: Expect drive on windows because we just have drive:filename, whereas
+ # we expect a relative path on Linux.
+ expect = f"*{subst_p}*" if sys.platform == "win32" else "*sub2/test_foo.py*"
+ result.stdout.fnmatch_lines([expect])
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_main.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_main.py
new file mode 100644
index 0000000000..2df51bb7bb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_main.py
@@ -0,0 +1,264 @@
+import argparse
+import os
+import re
+import sys
+from pathlib import Path
+from typing import Optional
+
+import pytest
+from _pytest.config import ExitCode
+from _pytest.config import UsageError
+from _pytest.main import resolve_collection_argument
+from _pytest.main import validate_basetemp
+from _pytest.pytester import Pytester
+
+
+@pytest.mark.parametrize(
+ "ret_exc",
+ (
+ pytest.param((None, ValueError)),
+ pytest.param((42, SystemExit)),
+ pytest.param((False, SystemExit)),
+ ),
+)
+def test_wrap_session_notify_exception(ret_exc, pytester: Pytester) -> None:
+ returncode, exc = ret_exc
+ c1 = pytester.makeconftest(
+ """
+ import pytest
+
+ def pytest_sessionstart():
+ raise {exc}("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:
+ assert result.ret == returncode
+ else:
+ 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:]
+ )
+
+ 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:
+ assert result.stderr.lines == ["mainloop: caught unexpected SystemExit!"]
+ else:
+ assert result.stderr.lines == [f"Exit: exiting after {exc.__name__}..."]
+
+
+@pytest.mark.parametrize("returncode", (None, 42))
+def test_wrap_session_exit_sessionfinish(
+ returncode: Optional[int], pytester: Pytester
+) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ def pytest_sessionfinish():
+ pytest.exit(reason="exit_pytest_sessionfinish", returncode={returncode})
+ """.format(
+ returncode=returncode
+ )
+ )
+ result = pytester.runpytest()
+ if returncode:
+ assert result.ret == returncode
+ else:
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+ assert result.stdout.lines[-1] == "collected 0 items"
+ assert result.stderr.lines == ["Exit: exit_pytest_sessionfinish"]
+
+
+@pytest.mark.parametrize("basetemp", ["foo", "foo/bar"])
+def test_validate_basetemp_ok(tmp_path, basetemp, monkeypatch):
+ monkeypatch.chdir(str(tmp_path))
+ validate_basetemp(tmp_path / basetemp)
+
+
+@pytest.mark.parametrize("basetemp", ["", ".", ".."])
+def test_validate_basetemp_fails(tmp_path, basetemp, monkeypatch):
+ monkeypatch.chdir(str(tmp_path))
+ msg = "basetemp must not be empty, the current working directory or any parent directory of it"
+ with pytest.raises(argparse.ArgumentTypeError, match=msg):
+ if basetemp:
+ basetemp = tmp_path / basetemp
+ validate_basetemp(basetemp)
+
+
+def test_validate_basetemp_integration(pytester: Pytester) -> None:
+ result = pytester.runpytest("--basetemp=.")
+ result.stderr.fnmatch_lines("*basetemp must not be*")
+
+
+class TestResolveCollectionArgument:
+ @pytest.fixture
+ def invocation_path(self, pytester: Pytester) -> Path:
+ pytester.syspathinsert(pytester.path / "src")
+ pytester.chdir()
+
+ pkg = pytester.path.joinpath("src/pkg")
+ pkg.mkdir(parents=True)
+ pkg.joinpath("__init__.py").touch()
+ pkg.joinpath("test.py").touch()
+ return pytester.path
+
+ 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::") == (
+ invocation_path / "src/pkg/test.py",
+ [""],
+ )
+ assert resolve_collection_argument(
+ invocation_path, "src/pkg/test.py::foo::bar"
+ ) == (invocation_path / "src/pkg/test.py", ["foo", "bar"])
+ assert resolve_collection_argument(
+ invocation_path, "src/pkg/test.py::foo::bar::"
+ ) == (invocation_path / "src/pkg/test.py", ["foo", "bar", ""])
+
+ def test_dir(self, invocation_path: Path) -> None:
+ """Directory and parts."""
+ assert resolve_collection_argument(invocation_path, "src/pkg") == (
+ invocation_path / "src/pkg",
+ [],
+ )
+
+ with pytest.raises(
+ UsageError, match=r"directory argument cannot contain :: selection parts"
+ ):
+ resolve_collection_argument(invocation_path, "src/pkg::")
+
+ with pytest.raises(
+ UsageError, match=r"directory argument cannot contain :: selection parts"
+ ):
+ resolve_collection_argument(invocation_path, "src/pkg::foo::bar")
+
+ def test_pypath(self, invocation_path: Path) -> None:
+ """Dotted name and parts."""
+ assert resolve_collection_argument(
+ invocation_path, "pkg.test", as_pypath=True
+ ) == (invocation_path / "src/pkg/test.py", [])
+ 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",
+ [],
+ )
+
+ with pytest.raises(
+ UsageError, match=r"package argument cannot contain :: selection parts"
+ ):
+ resolve_collection_argument(
+ invocation_path, "pkg::foo::bar", as_pypath=True
+ )
+
+ def test_parametrized_name_with_colons(self, invocation_path: Path) -> None:
+ ret = resolve_collection_argument(
+ invocation_path, "src/pkg/test.py::test[a::b]"
+ )
+ 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."""
+ with pytest.raises(
+ UsageError, match=re.escape("file or directory not found: foobar")
+ ):
+ resolve_collection_argument(invocation_path, "foobar")
+
+ with pytest.raises(
+ UsageError,
+ match=re.escape(
+ "module or package not found: foobar (missing __init__.py?)"
+ ),
+ ):
+ resolve_collection_argument(invocation_path, "foobar", as_pypath=True)
+
+ 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")),
+ [],
+ )
+
+ # ensure full paths given in the command-line without the drive letter resolve
+ # to the full path correctly (#7628)
+ 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")), [])
+
+
+def test_module_full_path_without_drive(pytester: Pytester) -> None:
+ """Collect and run test using full path except for the drive letter (#7628).
+
+ Passing a full path without a drive letter would trigger a bug in legacy_path
+ where it would keep the full path without the drive letter around, instead of resolving
+ to the full path, resulting in fixtures node ids not matching against test node ids correctly.
+ """
+ pytester.makepyfile(
+ **{
+ "project/conftest.py": """
+ import pytest
+ @pytest.fixture
+ def fix(): return 1
+ """,
+ }
+ )
+
+ pytester.makepyfile(
+ **{
+ "project/tests/dummy_test.py": """
+ def test(fix):
+ assert fix == 1
+ """
+ }
+ )
+ fn = pytester.path.joinpath("project/tests/dummy_test.py")
+ assert fn.is_file()
+
+ drive, path = os.path.splitdrive(str(fn))
+
+ result = pytester.runpytest(path, "-v")
+ result.stdout.fnmatch_lines(
+ [
+ os.path.join("project", "tests", "dummy_test.py") + "::test PASSED *",
+ "* 1 passed in *",
+ ]
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_mark.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_mark.py
new file mode 100644
index 0000000000..da67d1ea7b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_mark.py
@@ -0,0 +1,1130 @@
+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
+
+
+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
+
+ def test_pytest_mark_notcallable(self) -> None:
+ mark = MarkGenerator(_ispytest=True)
+ with pytest.raises(TypeError):
+ mark() # type: ignore[operator]
+
+ def test_mark_with_param(self):
+ def some_function(abc):
+ pass
+
+ class SomeClass:
+ pass
+
+ 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 pytest.mark.foo(SomeClass) is SomeClass
+ assert pytest.mark.foo.with_args(SomeClass) is not SomeClass # type: ignore[comparison-overlap]
+
+ def test_pytest_mark_name_starts_with_underscore(self) -> None:
+ mark = MarkGenerator(_ispytest=True)
+ with pytest.raises(AttributeError):
+ mark._some_name
+
+
+def test_marked_class_run_twice(pytester: Pytester) -> None:
+ """Test fails file is run twice that contains marked class.
+ See issue#683.
+ """
+ py_file = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize('abc', [1, 2, 3])
+ class Test1(object):
+ def test_1(self, abc):
+ assert abc in [1, 2, 3]
+ """
+ )
+ file_name = os.path.basename(py_file)
+ rec = pytester.inline_run(file_name, file_name)
+ rec.assertoutcome(passed=6)
+
+
+def test_ini_markers(pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ markers =
+ a1: this is a webtest marker
+ a2: this is a smoke marker
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_markers(pytestconfig):
+ markers = pytestconfig.getini("markers")
+ print(markers)
+ assert len(markers) >= 2
+ assert markers[0].startswith("a1:")
+ assert markers[1].startswith("a2:")
+ """
+ )
+ rec = pytester.inline_run()
+ rec.assertoutcome(passed=1)
+
+
+def test_markers_option(pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ markers =
+ a1: this is a webtest marker
+ a1some: another marker
+ nodescription
+ """
+ )
+ result = pytester.runpytest("--markers")
+ result.stdout.fnmatch_lines(
+ ["*a1*this is a webtest*", "*a1some*another marker", "*nodescription*"]
+ )
+
+
+def test_ini_markers_whitespace(pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ markers =
+ a1 : this is a whitespace marker
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.a1
+ def test_markers():
+ assert True
+ """
+ )
+ rec = pytester.inline_run("--strict-markers", "-m", "a1")
+ rec.assertoutcome(passed=1)
+
+
+def test_marker_without_description(pytester: Pytester) -> None:
+ pytester.makefile(
+ ".cfg",
+ setup="""
+ [tool:pytest]
+ markers=slow
+ """,
+ )
+ pytester.makeconftest(
+ """
+ import pytest
+ pytest.mark.xfail('FAIL')
+ """
+ )
+ ftdir = pytester.mkdir("ft1_dummy")
+ pytester.path.joinpath("conftest.py").replace(ftdir.joinpath("conftest.py"))
+ rec = pytester.runpytest("--strict-markers")
+ rec.assert_outcomes()
+
+
+def test_markers_option_with_plugin_in_current_dir(pytester: Pytester) -> None:
+ pytester.makeconftest('pytest_plugins = "flip_flop"')
+ pytester.makepyfile(
+ flip_flop="""\
+ def pytest_configure(config):
+ config.addinivalue_line("markers", "flip:flop")
+
+ def pytest_generate_tests(metafunc):
+ try:
+ mark = metafunc.function.flipper
+ except AttributeError:
+ return
+ metafunc.parametrize("x", (10, 20))"""
+ )
+ pytester.makepyfile(
+ """\
+ import pytest
+ @pytest.mark.flipper
+ def test_example(x):
+ assert x"""
+ )
+
+ result = pytester.runpytest("--markers")
+ result.stdout.fnmatch_lines(["*flip*flop*"])
+
+
+def test_mark_on_pseudo_function(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.r(lambda x: 0/0)
+ def test_hello():
+ pass
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+@pytest.mark.parametrize("option_name", ["--strict-markers", "--strict"])
+def test_strict_prohibits_unregistered_markers(
+ pytester: Pytester, option_name: str
+) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.unregisteredmark
+ def test_hello():
+ pass
+ """
+ )
+ result = pytester.runpytest(option_name)
+ assert result.ret != 0
+ result.stdout.fnmatch_lines(
+ ["'unregisteredmark' not found in `markers` configuration option"]
+ )
+
+
+@pytest.mark.parametrize(
+ ("expr", "expected_passed"),
+ [
+ ("xyz", ["test_one"]),
+ ("((( xyz)) )", ["test_one"]),
+ ("not not xyz", ["test_one"]),
+ ("xyz and xyz2", []),
+ ("xyz2", ["test_two"]),
+ ("xyz or xyz2", ["test_one", "test_two"]),
+ ],
+)
+def test_mark_option(
+ expr: str, expected_passed: List[Optional[str]], pytester: Pytester
+) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.xyz
+ def test_one():
+ pass
+ @pytest.mark.xyz2
+ def test_two():
+ pass
+ """
+ )
+ rec = pytester.inline_run("-m", expr)
+ passed, skipped, fail = rec.listoutcomes()
+ passed_str = [x.nodeid.split("::")[-1] for x in passed]
+ assert passed_str == expected_passed
+
+
+@pytest.mark.parametrize(
+ ("expr", "expected_passed"),
+ [("interface", ["test_interface"]), ("not interface", ["test_nointer"])],
+)
+def test_mark_option_custom(
+ expr: str, expected_passed: List[str], pytester: Pytester
+) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ def pytest_collection_modifyitems(items):
+ for item in items:
+ if "interface" in item.nodeid:
+ item.add_marker(pytest.mark.interface)
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_interface():
+ pass
+ def test_nointer():
+ pass
+ """
+ )
+ rec = pytester.inline_run("-m", expr)
+ passed, skipped, fail = rec.listoutcomes()
+ passed_str = [x.nodeid.split("::")[-1] for x in passed]
+ assert passed_str == expected_passed
+
+
+@pytest.mark.parametrize(
+ ("expr", "expected_passed"),
+ [
+ ("interface", ["test_interface"]),
+ ("not interface", ["test_nointer", "test_pass", "test_1", "test_2"]),
+ ("pass", ["test_pass"]),
+ ("not pass", ["test_interface", "test_nointer", "test_1", "test_2"]),
+ ("not not not (pass)", ["test_interface", "test_nointer", "test_1", "test_2"]),
+ ("1 or 2", ["test_1", "test_2"]),
+ ("not (1 or 2)", ["test_interface", "test_nointer", "test_pass"]),
+ ],
+)
+def test_keyword_option_custom(
+ expr: str, expected_passed: List[str], pytester: Pytester
+) -> None:
+ pytester.makepyfile(
+ """
+ def test_interface():
+ pass
+ def test_nointer():
+ pass
+ def test_pass():
+ pass
+ def test_1():
+ pass
+ def test_2():
+ pass
+ """
+ )
+ rec = pytester.inline_run("-k", expr)
+ passed, skipped, fail = rec.listoutcomes()
+ passed_str = [x.nodeid.split("::")[-1] for x in passed]
+ assert passed_str == expected_passed
+
+
+def test_keyword_option_considers_mark(pytester: Pytester) -> None:
+ pytester.copy_example("marks/marks_considered_keywords")
+ rec = pytester.inline_run("-k", "foo")
+ passed = rec.listoutcomes()[0]
+ assert len(passed) == 1
+
+
+@pytest.mark.parametrize(
+ ("expr", "expected_passed"),
+ [
+ ("None", ["test_func[None]"]),
+ ("[1.3]", ["test_func[1.3]"]),
+ ("2-3", ["test_func[2-3]"]),
+ ],
+)
+def test_keyword_option_parametrize(
+ expr: str, expected_passed: List[str], pytester: Pytester
+) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize("arg", [None, 1.3, "2-3"])
+ def test_func(arg):
+ pass
+ """
+ )
+ rec = pytester.inline_run("-k", expr)
+ passed, skipped, fail = rec.listoutcomes()
+ passed_str = [x.nodeid.split("::")[-1] for x in passed]
+ assert passed_str == expected_passed
+
+
+def test_parametrize_with_module(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize("arg", [pytest,])
+ def test_func(arg):
+ pass
+ """
+ )
+ rec = pytester.inline_run()
+ passed, skipped, fail = rec.listoutcomes()
+ expected_id = "test_func[" + pytest.__name__ + "]"
+ assert passed[0].nodeid.split("::")[-1] == expected_id
+
+
+@pytest.mark.parametrize(
+ ("expr", "expected_error"),
+ [
+ (
+ "foo or",
+ "at column 7: expected not OR left parenthesis OR identifier; got end of input",
+ ),
+ (
+ "foo or or",
+ "at column 8: expected not OR left parenthesis OR identifier; got or",
+ ),
+ (
+ "(foo",
+ "at column 5: expected right parenthesis; got end of input",
+ ),
+ (
+ "foo bar",
+ "at column 5: expected end of input; got identifier",
+ ),
+ (
+ "or or",
+ "at column 1: expected not OR left parenthesis OR identifier; got or",
+ ),
+ (
+ "not or",
+ "at column 5: expected not OR left parenthesis OR identifier; got or",
+ ),
+ ],
+)
+def test_keyword_option_wrong_arguments(
+ expr: str, expected_error: str, pytester: Pytester, capsys
+) -> None:
+ pytester.makepyfile(
+ """
+ def test_func(arg):
+ pass
+ """
+ )
+ pytester.inline_run("-k", expr)
+ err = capsys.readouterr().err
+ assert expected_error in err
+
+
+def test_parametrized_collected_from_command_line(pytester: Pytester) -> None:
+ """Parametrized test not collected if test named specified in command
+ line issue#649."""
+ py_file = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize("arg", [None, 1.3, "2-3"])
+ def test_func(arg):
+ pass
+ """
+ )
+ file_name = os.path.basename(py_file)
+ rec = pytester.inline_run(file_name + "::" + "test_func")
+ rec.assertoutcome(passed=3)
+
+
+def test_parametrized_collect_with_wrong_args(pytester: Pytester) -> None:
+ """Test collect parametrized func with wrong number of args."""
+ py_file = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.parametrize('foo, bar', [(1, 2, 3)])
+ def test_func(foo, bar):
+ pass
+ """
+ )
+
+ result = pytester.runpytest(py_file)
+ result.stdout.fnmatch_lines(
+ [
+ 'test_parametrized_collect_with_wrong_args.py::test_func: in "parametrize" the number of names (2):',
+ " ['foo', 'bar']",
+ "must be equal to the number of values (3):",
+ " (1, 2, 3)",
+ ]
+ )
+
+
+def test_parametrized_with_kwargs(pytester: Pytester) -> None:
+ """Test collect parametrized func with wrong number of args."""
+ py_file = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(params=[1,2])
+ def a(request):
+ return request.param
+
+ @pytest.mark.parametrize(argnames='b', argvalues=[1, 2])
+ def test_func(a, b):
+ pass
+ """
+ )
+
+ result = pytester.runpytest(py_file)
+ assert result.ret == 0
+
+
+def test_parametrize_iterator(pytester: Pytester) -> None:
+ """`parametrize` should work with generators (#5354)."""
+ py_file = pytester.makepyfile(
+ """\
+ import pytest
+
+ def gen():
+ yield 1
+ yield 2
+ yield 3
+
+ @pytest.mark.parametrize('a', gen())
+ def test(a):
+ assert a >= 1
+ """
+ )
+ result = pytester.runpytest(py_file)
+ assert result.ret == 0
+ # should not skip any tests
+ result.stdout.fnmatch_lines(["*3 passed*"])
+
+
+class TestFunctional:
+ def test_merging_markers_deep(self, pytester: Pytester) -> None:
+ # issue 199 - propagate markers into nested classes
+ p = pytester.makepyfile(
+ """
+ import pytest
+ class TestA(object):
+ pytestmark = pytest.mark.a
+ def test_b(self):
+ assert True
+ class TestC(object):
+ # this one didn't get marked
+ def test_d(self):
+ assert True
+ """
+ )
+ items, rec = pytester.inline_genitems(p)
+ for item in items:
+ print(item, item.keywords)
+ assert [x for x in item.iter_markers() if x.name == "a"]
+
+ def test_mark_decorator_subclass_does_not_propagate_to_base(
+ self, pytester: Pytester
+ ) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.a
+ class Base(object): pass
+
+ @pytest.mark.b
+ class Test1(Base):
+ def test_foo(self): pass
+
+ class Test2(Base):
+ def test_bar(self): pass
+ """
+ )
+ items, rec = pytester.inline_genitems(p)
+ self.assert_markers(items, test_foo=("a", "b"), test_bar=("a",))
+
+ def test_mark_should_not_pass_to_siebling_class(self, pytester: Pytester) -> None:
+ """#568"""
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ class TestBase(object):
+ def test_foo(self):
+ pass
+
+ @pytest.mark.b
+ class TestSub(TestBase):
+ pass
+
+
+ class TestOtherSub(TestBase):
+ pass
+
+ """
+ )
+ items, rec = pytester.inline_genitems(p)
+ base_item, sub_item, sub_item_other = items
+ print(items, [x.nodeid for x in items])
+ # new api segregates
+ assert not list(base_item.iter_markers(name="b"))
+ assert not list(sub_item_other.iter_markers(name="b"))
+ assert list(sub_item.iter_markers(name="b"))
+
+ def test_mark_decorator_baseclasses_merged(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.a
+ class Base(object): pass
+
+ @pytest.mark.b
+ class Base2(Base): pass
+
+ @pytest.mark.c
+ class Test1(Base2):
+ def test_foo(self): pass
+
+ class Test2(Base2):
+ @pytest.mark.d
+ def test_bar(self): pass
+ """
+ )
+ items, rec = pytester.inline_genitems(p)
+ self.assert_markers(items, test_foo=("a", "b", "c"), test_bar=("a", "b", "d"))
+
+ def test_mark_closest(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.c(location="class")
+ class Test:
+ @pytest.mark.c(location="function")
+ def test_has_own(self):
+ pass
+
+ def test_has_inherited(self):
+ pass
+
+ """
+ )
+ items, rec = pytester.inline_genitems(p)
+ has_own, has_inherited = items
+ has_own_marker = has_own.get_closest_marker("c")
+ has_inherited_marker = has_inherited.get_closest_marker("c")
+ assert has_own_marker is not None
+ assert has_inherited_marker is not None
+ assert has_own_marker.kwargs == {"location": "function"}
+ assert has_inherited_marker.kwargs == {"location": "class"}
+ assert has_own.get_closest_marker("missing") is None
+
+ def test_mark_with_wrong_marker(self, pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ import pytest
+ class pytestmark(object):
+ pass
+ def test_func():
+ pass
+ """
+ )
+ values = reprec.getfailedcollections()
+ assert len(values) == 1
+ assert "TypeError" in str(values[0].longrepr)
+
+ def test_mark_dynamically_in_funcarg(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ @pytest.fixture
+ def arg(request):
+ request.applymarker(pytest.mark.hello)
+ def pytest_terminal_summary(terminalreporter):
+ values = terminalreporter.stats['passed']
+ terminalreporter._tw.line("keyword: %s" % values[0].keywords)
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_func(arg):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["keyword: *hello*"])
+
+ def test_no_marker_match_on_unmarked_names(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.shouldmatch
+ def test_marked():
+ assert 1
+
+ def test_unmarked():
+ assert 1
+ """
+ )
+ reprec = pytester.inline_run("-m", "test_unmarked", p)
+ passed, skipped, failed = reprec.listoutcomes()
+ assert len(passed) + len(skipped) + len(failed) == 0
+ dlist = reprec.getcalls("pytest_deselected")
+ deselected_tests = dlist[0].items
+ assert len(deselected_tests) == 2
+
+ def test_keywords_at_node_level(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope="session", autouse=True)
+ def some(request):
+ request.keywords["hello"] = 42
+ assert "world" not in request.keywords
+
+ @pytest.fixture(scope="function", autouse=True)
+ def funcsetup(request):
+ assert "world" in request.keywords
+ assert "hello" in request.keywords
+
+ @pytest.mark.world
+ def test_function():
+ pass
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+ def test_keyword_added_for_session(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ def pytest_collection_modifyitems(session):
+ session.add_marker("mark1")
+ session.add_marker(pytest.mark.mark2)
+ session.add_marker(pytest.mark.mark3)
+ pytest.raises(ValueError, lambda:
+ session.add_marker(10))
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_some(request):
+ assert "mark1" in request.keywords
+ assert "mark2" in request.keywords
+ assert "mark3" in request.keywords
+ assert 10 not in request.keywords
+ marker = request.node.get_closest_marker("mark1")
+ assert marker.name == "mark1"
+ assert marker.args == ()
+ assert marker.kwargs == {}
+ """
+ )
+ reprec = pytester.inline_run("-m", "mark1")
+ reprec.assertoutcome(passed=1)
+
+ def assert_markers(self, items, **expected) -> None:
+ """Assert that given items have expected marker names applied to them.
+ expected should be a dict of (item name -> seq of expected marker names).
+
+ Note: this could be moved to ``pytester`` if proven to be useful
+ to other modules.
+ """
+ items = {x.name: x for x in items}
+ for name, expected_markers in expected.items():
+ markers = {m.name for m in items[name].iter_markers()}
+ assert markers == set(expected_markers)
+
+ @pytest.mark.filterwarnings("ignore")
+ def test_mark_from_parameters(self, pytester: Pytester) -> None:
+ """#1540"""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ pytestmark = pytest.mark.skipif(True, reason='skip all')
+
+ # skipifs inside fixture params
+ params = [pytest.mark.skipif(False, reason='dont skip')('parameter')]
+
+
+ @pytest.fixture(params=params)
+ def parameter(request):
+ return request.param
+
+
+ def test_1(parameter):
+ assert True
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(skipped=1)
+
+ def test_reevaluate_dynamic_expr(self, pytester: Pytester) -> None:
+ """#7360"""
+ py_file1 = pytester.makepyfile(
+ test_reevaluate_dynamic_expr1="""
+ import pytest
+
+ skip = True
+
+ @pytest.mark.skipif("skip")
+ def test_should_skip():
+ assert True
+ """
+ )
+ py_file2 = pytester.makepyfile(
+ test_reevaluate_dynamic_expr2="""
+ import pytest
+
+ skip = False
+
+ @pytest.mark.skipif("skip")
+ def test_should_not_skip():
+ assert True
+ """
+ )
+
+ file_name1 = os.path.basename(py_file1)
+ file_name2 = os.path.basename(py_file2)
+ reprec = pytester.inline_run(file_name1, file_name2)
+ reprec.assertoutcome(passed=1, skipped=1)
+
+
+class TestKeywordSelection:
+ def test_select_simple(self, pytester: Pytester) -> None:
+ file_test = pytester.makepyfile(
+ """
+ def test_one():
+ assert 0
+ class TestClass(object):
+ def test_method_one(self):
+ assert 42 == 43
+ """
+ )
+
+ def check(keyword, name):
+ reprec = pytester.inline_run("-s", "-k", keyword, file_test)
+ passed, skipped, failed = reprec.listoutcomes()
+ assert len(failed) == 1
+ assert failed[0].nodeid.split("::")[-1] == name
+ assert len(reprec.getcalls("pytest_deselected")) == 1
+
+ for keyword in ["test_one", "est_on"]:
+ check(keyword, "test_one")
+ check("TestClass and test", "test_method_one")
+
+ @pytest.mark.parametrize(
+ "keyword",
+ [
+ "xxx",
+ "xxx and test_2",
+ "TestClass",
+ "xxx and not test_1",
+ "TestClass and test_2",
+ "xxx and TestClass and test_2",
+ ],
+ )
+ def test_select_extra_keywords(self, pytester: Pytester, keyword) -> None:
+ p = pytester.makepyfile(
+ test_select="""
+ def test_1():
+ pass
+ class TestClass(object):
+ def test_2(self):
+ pass
+ """
+ )
+ pytester.makepyfile(
+ conftest="""
+ import pytest
+ @pytest.hookimpl(hookwrapper=True)
+ def pytest_pycollect_makeitem(name):
+ outcome = yield
+ if name == "TestClass":
+ item = outcome.get_result()
+ item.extra_keyword_matches.add("xxx")
+ """
+ )
+ reprec = pytester.inline_run(p.parent, "-s", "-k", keyword)
+ print("keyword", repr(keyword))
+ passed, skipped, failed = reprec.listoutcomes()
+ assert len(passed) == 1
+ assert passed[0].nodeid.endswith("test_2")
+ dlist = reprec.getcalls("pytest_deselected")
+ 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(
+ """
+ def test_one():
+ assert 0
+ test_one.mykeyword = True
+ """
+ )
+ reprec = pytester.inline_run("-k", "mykeyword", p)
+ passed, skipped, failed = reprec.countoutcomes()
+ assert failed == 1
+
+ @pytest.mark.xfail
+ def test_keyword_extra_dash(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def test_one():
+ assert 0
+ test_one.mykeyword = True
+ """
+ )
+ # with argparse the argument to an option cannot
+ # start with '-'
+ reprec = pytester.inline_run("-k", "-mykeyword", p)
+ passed, skipped, failed = reprec.countoutcomes()
+ assert passed + skipped + failed == 0
+
+ @pytest.mark.parametrize(
+ "keyword",
+ ["__", "+", ".."],
+ )
+ def test_no_magic_values(self, pytester: Pytester, keyword: str) -> None:
+ """Make sure the tests do not match on magic values,
+ no double underscored values, like '__dict__' and '+'.
+ """
+ p = pytester.makepyfile(
+ """
+ def test_one(): assert 1
+ """
+ )
+
+ reprec = pytester.inline_run("-k", keyword, p)
+ passed, skipped, failed = reprec.countoutcomes()
+ dlist = reprec.getcalls("pytest_deselected")
+ assert passed + skipped + failed == 0
+ deselected_tests = dlist[0].items
+ assert len(deselected_tests) == 1
+
+ def test_no_match_directories_outside_the_suite(self, pytester: Pytester) -> None:
+ """`-k` should not match against directories containing the test suite (#7040)."""
+ test_contents = """
+ def test_aaa(): pass
+ def test_ddd(): pass
+ """
+ pytester.makepyfile(
+ **{"ddd/tests/__init__.py": "", "ddd/tests/test_foo.py": test_contents}
+ )
+
+ def get_collected_names(*args):
+ _, rec = pytester.inline_genitems(*args)
+ calls = rec.getcalls("pytest_collection_finish")
+ assert len(calls) == 1
+ return [x.name for x in calls[0].session.items]
+
+ # sanity check: collect both tests in normal runs
+ assert get_collected_names() == ["test_aaa", "test_ddd"]
+
+ # 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(
+ "lhs, rhs, expected",
+ [
+ (pytest.mark.foo(), pytest.mark.foo(), True),
+ (pytest.mark.foo(), pytest.mark.bar(), False),
+ (pytest.mark.foo(), "bar", False),
+ ("foo", pytest.mark.bar(), False),
+ ],
+ )
+ def test__eq__(self, lhs, rhs, expected) -> None:
+ assert (lhs == rhs) == expected
+
+ def test_aliases(self) -> None:
+ md = pytest.mark.foo(1, "2", three=3)
+ assert md.name == "foo"
+ assert md.args == (1, "2")
+ assert md.kwargs == {"three": 3}
+
+
+@pytest.mark.parametrize("mark", [None, "", "skip", "xfail"])
+def test_parameterset_for_parametrize_marks(
+ pytester: Pytester, mark: Optional[str]
+) -> None:
+ if mark is not None:
+ pytester.makeini(
+ """
+ [pytest]
+ {}={}
+ """.format(
+ EMPTY_PARAMETERSET_OPTION, mark
+ )
+ )
+
+ config = pytester.parseconfig()
+ from _pytest.mark import pytest_configure, get_empty_parameterset_mark
+
+ pytest_configure(config)
+ result_mark = get_empty_parameterset_mark(config, ["a"], all)
+ if mark in (None, ""):
+ # normalize to the requested name
+ mark = "skip"
+ assert result_mark.name == mark
+ assert result_mark.kwargs["reason"].startswith("got empty parameter set ")
+ if mark == "xfail":
+ assert result_mark.kwargs.get("run") is False
+
+
+def test_parameterset_for_fail_at_collect(pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ {}=fail_at_collect
+ """.format(
+ EMPTY_PARAMETERSET_OPTION
+ )
+ )
+
+ config = pytester.parseconfig()
+ from _pytest.mark import pytest_configure, get_empty_parameterset_mark
+
+ pytest_configure(config)
+
+ with pytest.raises(
+ Collector.CollectError,
+ match=r"Empty parameter set in 'pytest_configure' at line \d\d+",
+ ):
+ get_empty_parameterset_mark(config, ["a"], pytest_configure)
+
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.parametrize("empty", [])
+ def test():
+ pass
+ """
+ )
+ result = pytester.runpytest(str(p1))
+ result.stdout.fnmatch_lines(
+ [
+ "collected 0 items / 1 error",
+ "* ERROR collecting test_parameterset_for_fail_at_collect.py *",
+ "Empty parameter set in 'test' at line 3",
+ "*= 1 error in *",
+ ]
+ )
+ assert result.ret == ExitCode.INTERRUPTED
+
+
+def test_parameterset_for_parametrize_bad_markname(pytester: Pytester) -> None:
+ with pytest.raises(pytest.UsageError):
+ test_parameterset_for_parametrize_marks(pytester, "bad")
+
+
+def test_mark_expressions_no_smear(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ class BaseTests(object):
+ def test_something(self):
+ pass
+
+ @pytest.mark.FOO
+ class TestFooClass(BaseTests):
+ pass
+
+ @pytest.mark.BAR
+ class TestBarClass(BaseTests):
+ pass
+ """
+ )
+
+ reprec = pytester.inline_run("-m", "FOO")
+ passed, skipped, failed = reprec.countoutcomes()
+ dlist = reprec.getcalls("pytest_deselected")
+ assert passed == 1
+ assert skipped == failed == 0
+ deselected_tests = dlist[0].items
+ assert len(deselected_tests) == 1
+
+ # todo: fixed
+ # keywords smear - expected behaviour
+ # reprec_keywords = pytester.inline_run("-k", "FOO")
+ # passed_k, skipped_k, failed_k = reprec_keywords.countoutcomes()
+ # assert passed_k == 2
+ # assert skipped_k == failed_k == 0
+
+
+def test_addmarker_order(pytester) -> None:
+ session = mock.Mock()
+ session.own_markers = []
+ session.parent = None
+ session.nodeid = ""
+ session.path = pytester.path
+ node = Node.from_parent(session, name="Test")
+ node.add_marker("foo")
+ node.add_marker("bar")
+ node.add_marker("baz", append=False)
+ extracted = [x.name for x in node.iter_markers()]
+ assert extracted == ["baz", "foo", "bar"]
+
+
+@pytest.mark.filterwarnings("ignore")
+def test_markers_from_parametrize(pytester: Pytester) -> None:
+ """#3605"""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ first_custom_mark = pytest.mark.custom_marker
+ custom_mark = pytest.mark.custom_mark
+ @pytest.fixture(autouse=True)
+ def trigger(request):
+ custom_mark = list(request.node.iter_markers('custom_mark'))
+ print("Custom mark %s" % custom_mark)
+
+ @custom_mark("custom mark non parametrized")
+ def test_custom_mark_non_parametrized():
+ print("Hey from test")
+
+ @pytest.mark.parametrize(
+ "obj_type",
+ [
+ first_custom_mark("first custom mark")("template"),
+ pytest.param( # Think this should be recommended way?
+ "disk",
+ marks=custom_mark('custom mark1')
+ ),
+ custom_mark("custom mark2")("vm"), # Tried also this
+ ]
+ )
+ def test_custom_mark_parametrized(obj_type):
+ print("obj_type is:", obj_type)
+ """
+ )
+
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=4)
+
+
+def test_pytest_param_id_requires_string() -> None:
+ with pytest.raises(TypeError) as excinfo:
+ pytest.param(id=True) # type: ignore[arg-type]
+ (msg,) = excinfo.value.args
+ assert msg == "Expected id to be a string, got <class 'bool'>: True"
+
+
+@pytest.mark.parametrize("s", (None, "hello world"))
+def test_pytest_param_id_allows_none_or_string(s) -> None:
+ assert pytest.param(id=s)
+
+
+@pytest.mark.parametrize("expr", ("NOT internal_err", "NOT (internal_err)", "bogus="))
+def test_marker_expr_eval_failure_handling(pytester: Pytester, expr) -> None:
+ foo = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.internal_err
+ def test_foo():
+ pass
+ """
+ )
+ expected = f"ERROR: Wrong expression passed to '-m': {expr}: *"
+ result = pytester.runpytest(foo, "-m", expr)
+ result.stderr.fnmatch_lines([expected])
+ assert result.ret == ExitCode.USAGE_ERROR
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_mark_expression.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_mark_expression.py
new file mode 100644
index 0000000000..f3643e7b40
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_mark_expression.py
@@ -0,0 +1,195 @@
+from typing import Callable
+
+import pytest
+from _pytest.mark.expression import Expression
+from _pytest.mark.expression import ParseError
+
+
+def evaluate(input: str, matcher: Callable[[str], bool]) -> bool:
+ return Expression.compile(input).evaluate(matcher)
+
+
+def test_empty_is_false() -> None:
+ assert not evaluate("", lambda ident: False)
+ assert not evaluate("", lambda ident: True)
+ assert not evaluate(" ", lambda ident: False)
+ assert not evaluate("\t", lambda ident: False)
+
+
+@pytest.mark.parametrize(
+ ("expr", "expected"),
+ (
+ ("true", True),
+ ("true", True),
+ ("false", False),
+ ("not true", False),
+ ("not false", True),
+ ("not not true", True),
+ ("not not false", False),
+ ("true and true", True),
+ ("true and false", False),
+ ("false and true", False),
+ ("true and true and true", True),
+ ("true and true and false", False),
+ ("true and true and not true", False),
+ ("false or false", False),
+ ("false or true", True),
+ ("true or true", True),
+ ("true or true or false", True),
+ ("true and true or false", True),
+ ("not true or true", True),
+ ("(not true) or true", True),
+ ("not (true or true)", False),
+ ("true and true or false and false", True),
+ ("true and (true or false) and false", False),
+ ("true and (true or (not (not false))) and false", False),
+ ),
+)
+def test_basic(expr: str, expected: bool) -> None:
+ matcher = {"true": True, "false": False}.__getitem__
+ assert evaluate(expr, matcher) is expected
+
+
+@pytest.mark.parametrize(
+ ("expr", "expected"),
+ (
+ (" true ", True),
+ (" ((((((true)))))) ", True),
+ (" ( ((\t (((true))))) \t \t)", True),
+ ("( true and (((false))))", False),
+ ("not not not not true", True),
+ ("not not not not not true", False),
+ ),
+)
+def test_syntax_oddeties(expr: str, expected: bool) -> None:
+ matcher = {"true": True, "false": False}.__getitem__
+ assert evaluate(expr, matcher) is expected
+
+
+def test_backslash_not_treated_specially() -> None:
+ r"""When generating nodeids, if the source name contains special characters
+ like a newline, they are escaped into two characters like \n. Therefore, a
+ user will never need to insert a literal newline, only \n (two chars). So
+ mark expressions themselves do not support escaping, instead they treat
+ backslashes as regular identifier characters."""
+ matcher = {r"\nfoo\n"}.__contains__
+
+ assert evaluate(r"\nfoo\n", matcher)
+ assert not evaluate(r"foo", matcher)
+ with pytest.raises(ParseError):
+ evaluate("\nfoo\n", matcher)
+
+
+@pytest.mark.parametrize(
+ ("expr", "column", "message"),
+ (
+ ("(", 2, "expected not OR left parenthesis OR identifier; got end of input"),
+ (
+ " (",
+ 3,
+ "expected not OR left parenthesis OR identifier; got end of input",
+ ),
+ (
+ ")",
+ 1,
+ "expected not OR left parenthesis OR identifier; got right parenthesis",
+ ),
+ (
+ ") ",
+ 1,
+ "expected not OR left parenthesis OR identifier; got right parenthesis",
+ ),
+ (
+ "not",
+ 4,
+ "expected not OR left parenthesis OR identifier; got end of input",
+ ),
+ (
+ "not not",
+ 8,
+ "expected not OR left parenthesis OR identifier; got end of input",
+ ),
+ (
+ "(not)",
+ 5,
+ "expected not OR left parenthesis OR identifier; got right parenthesis",
+ ),
+ ("and", 1, "expected not OR left parenthesis OR identifier; got and"),
+ (
+ "ident and",
+ 10,
+ "expected not OR left parenthesis OR identifier; got end of input",
+ ),
+ (
+ "ident and or",
+ 11,
+ "expected not OR left parenthesis OR identifier; got or",
+ ),
+ ("ident ident", 7, "expected end of input; got identifier"),
+ ),
+)
+def test_syntax_errors(expr: str, column: int, message: str) -> None:
+ with pytest.raises(ParseError) as excinfo:
+ evaluate(expr, lambda ident: True)
+ assert excinfo.value.column == column
+ assert excinfo.value.message == message
+
+
+@pytest.mark.parametrize(
+ "ident",
+ (
+ ".",
+ "...",
+ ":::",
+ "a:::c",
+ "a+-b",
+ r"\nhe\\l\lo\n\t\rbye",
+ "a/b",
+ "אבגד",
+ "aaאבגדcc",
+ "a[bcd]",
+ "1234",
+ "1234abcd",
+ "1234and",
+ "notandor",
+ "not_and_or",
+ "not[and]or",
+ "1234+5678",
+ "123.232",
+ "True",
+ "False",
+ "None",
+ "if",
+ "else",
+ "while",
+ ),
+)
+def test_valid_idents(ident: str) -> None:
+ assert evaluate(ident, {ident: True}.__getitem__)
+
+
+@pytest.mark.parametrize(
+ "ident",
+ (
+ "^",
+ "*",
+ "=",
+ "&",
+ "%",
+ "$",
+ "#",
+ "@",
+ "!",
+ "~",
+ "{",
+ "}",
+ '"',
+ "'",
+ "|",
+ ";",
+ "←",
+ ),
+)
+def test_invalid_idents(ident: str) -> None:
+ with pytest.raises(ParseError):
+ evaluate(ident, lambda ident: True)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_meta.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_meta.py
new file mode 100644
index 0000000000..9201bd2161
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_meta.py
@@ -0,0 +1,32 @@
+"""Test importing of all internal packages and modules.
+
+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
+from typing import List
+
+import _pytest
+import pytest
+
+
+def _modules() -> List[str]:
+ pytest_pkg: str = _pytest.__path__ # type: ignore
+ return sorted(
+ n
+ for _, n, _ in pkgutil.walk_packages(pytest_pkg, prefix=_pytest.__name__ + ".")
+ )
+
+
+@pytest.mark.slow
+@pytest.mark.parametrize("module", _modules())
+def test_no_warnings(module: str) -> None:
+ # fmt: off
+ subprocess.check_call((
+ sys.executable,
+ "-W", "error",
+ "-c", f"__import__({module!r})",
+ ))
+ # fmt: on
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_monkeypatch.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_monkeypatch.py
new file mode 100644
index 0000000000..9552181802
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_monkeypatch.py
@@ -0,0 +1,455 @@
+import os
+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
+
+
+@pytest.fixture
+def mp() -> Generator[MonkeyPatch, None, None]:
+ cwd = os.getcwd()
+ sys_path = list(sys.path)
+ yield MonkeyPatch()
+ sys.path[:] = sys_path
+ os.chdir(cwd)
+
+
+def test_setattr() -> None:
+ class A:
+ x = 1
+
+ monkeypatch = MonkeyPatch()
+ pytest.raises(AttributeError, monkeypatch.setattr, A, "notexists", 2)
+ monkeypatch.setattr(A, "y", 2, raising=False)
+ assert A.y == 2 # type: ignore
+ monkeypatch.undo()
+ assert not hasattr(A, "y")
+
+ monkeypatch = MonkeyPatch()
+ monkeypatch.setattr(A, "x", 2)
+ assert A.x == 2
+ monkeypatch.setattr(A, "x", 3)
+ assert A.x == 3
+ monkeypatch.undo()
+ assert A.x == 1
+
+ A.x = 5
+ monkeypatch.undo() # double-undo makes no modification
+ assert A.x == 5
+
+ with pytest.raises(TypeError):
+ monkeypatch.setattr(A, "y") # type: ignore[call-overload]
+
+
+class TestSetattrWithImportPath:
+ def test_string_expression(self, monkeypatch: MonkeyPatch) -> None:
+ monkeypatch.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
+
+ assert _pytest.config.Config == 42 # type: ignore
+
+ def test_unicode_string(self, monkeypatch: MonkeyPatch) -> None:
+ monkeypatch.setattr("_pytest.config.Config", 42)
+ import _pytest
+
+ assert _pytest.config.Config == 42 # type: ignore
+ monkeypatch.delattr("_pytest.config.Config")
+
+ def test_wrong_target(self, monkeypatch: MonkeyPatch) -> None:
+ with pytest.raises(TypeError):
+ monkeypatch.setattr(None, None) # type: ignore[call-overload]
+
+ def test_unknown_import(self, monkeypatch: MonkeyPatch) -> None:
+ with pytest.raises(ImportError):
+ monkeypatch.setattr("unkn123.classx", None)
+
+ def test_unknown_attr(self, monkeypatch: MonkeyPatch) -> None:
+ with pytest.raises(AttributeError):
+ monkeypatch.setattr("os.path.qweqwe", None)
+
+ 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
+
+ def test_delattr(self, monkeypatch: MonkeyPatch) -> None:
+ monkeypatch.delattr("os.path.abspath")
+ assert not hasattr(os.path, "abspath")
+ monkeypatch.undo()
+ assert os.path.abspath
+
+
+def test_delattr() -> None:
+ class A:
+ x = 1
+
+ monkeypatch = MonkeyPatch()
+ monkeypatch.delattr(A, "x")
+ assert not hasattr(A, "x")
+ monkeypatch.undo()
+ assert A.x == 1
+
+ monkeypatch = MonkeyPatch()
+ monkeypatch.delattr(A, "x")
+ pytest.raises(AttributeError, monkeypatch.delattr, A, "y")
+ monkeypatch.delattr(A, "y", raising=False)
+ monkeypatch.setattr(A, "x", 5, raising=False)
+ assert A.x == 5
+ monkeypatch.undo()
+ assert A.x == 1
+
+
+def test_setitem() -> None:
+ d = {"x": 1}
+ monkeypatch = MonkeyPatch()
+ monkeypatch.setitem(d, "x", 2)
+ monkeypatch.setitem(d, "y", 1700)
+ monkeypatch.setitem(d, "y", 1700)
+ assert d["x"] == 2
+ assert d["y"] == 1700
+ monkeypatch.setitem(d, "x", 3)
+ assert d["x"] == 3
+ monkeypatch.undo()
+ assert d["x"] == 1
+ assert "y" not in d
+ d["x"] = 5
+ monkeypatch.undo()
+ assert d["x"] == 5
+
+
+def test_setitem_deleted_meanwhile() -> None:
+ d: Dict[str, object] = {}
+ monkeypatch = MonkeyPatch()
+ monkeypatch.setitem(d, "x", 2)
+ del d["x"]
+ monkeypatch.undo()
+ assert not d
+
+
+@pytest.mark.parametrize("before", [True, False])
+def test_setenv_deleted_meanwhile(before: bool) -> None:
+ key = "qwpeoip123"
+ if before:
+ os.environ[key] = "world"
+ monkeypatch = MonkeyPatch()
+ monkeypatch.setenv(key, "hello")
+ del os.environ[key]
+ monkeypatch.undo()
+ if before:
+ assert os.environ[key] == "world"
+ del os.environ[key]
+ else:
+ assert key not in os.environ
+
+
+def test_delitem() -> None:
+ d: Dict[str, object] = {"x": 1}
+ monkeypatch = MonkeyPatch()
+ monkeypatch.delitem(d, "x")
+ assert "x" not in d
+ monkeypatch.delitem(d, "y", raising=False)
+ pytest.raises(KeyError, monkeypatch.delitem, d, "y")
+ assert not d
+ monkeypatch.setitem(d, "y", 1700)
+ assert d["y"] == 1700
+ d["hello"] = "world"
+ monkeypatch.setitem(d, "x", 1500)
+ assert d["x"] == 1500
+ monkeypatch.undo()
+ assert d == {"hello": "world", "x": 1}
+
+
+def test_setenv() -> None:
+ monkeypatch = MonkeyPatch()
+ with pytest.warns(pytest.PytestWarning):
+ monkeypatch.setenv("XYZ123", 2) # type: ignore[arg-type]
+ import os
+
+ assert os.environ["XYZ123"] == "2"
+ monkeypatch.undo()
+ assert "XYZ123" not in os.environ
+
+
+def test_delenv() -> None:
+ name = "xyz1234"
+ assert name not in os.environ
+ monkeypatch = MonkeyPatch()
+ pytest.raises(KeyError, monkeypatch.delenv, name, raising=True)
+ monkeypatch.delenv(name, raising=False)
+ monkeypatch.undo()
+ os.environ[name] = "1"
+ try:
+ monkeypatch = MonkeyPatch()
+ monkeypatch.delenv(name)
+ assert name not in os.environ
+ monkeypatch.setenv(name, "3")
+ assert os.environ[name] == "3"
+ monkeypatch.undo()
+ assert os.environ[name] == "1"
+ finally:
+ if name in os.environ:
+ del os.environ[name]
+
+
+class TestEnvironWarnings:
+ """
+ os.environ keys and values should be native strings, otherwise it will cause problems with other modules (notably
+ subprocess). On Python 2 os.environ accepts anything without complaining, while Python 3 does the right thing
+ and raises an error.
+ """
+
+ VAR_NAME = "PYTEST_INTERNAL_MY_VAR"
+
+ def test_setenv_non_str_warning(self, monkeypatch: MonkeyPatch) -> None:
+ value = 2
+ msg = (
+ "Value of environment variable PYTEST_INTERNAL_MY_VAR type should be str, "
+ "but got 2 (type: int); converted to str implicitly"
+ )
+ with pytest.warns(pytest.PytestWarning, match=re.escape(msg)):
+ monkeypatch.setenv(str(self.VAR_NAME), value) # type: ignore[arg-type]
+
+
+def test_setenv_prepend() -> None:
+ import os
+
+ monkeypatch = MonkeyPatch()
+ monkeypatch.setenv("XYZ123", "2", prepend="-")
+ monkeypatch.setenv("XYZ123", "3", prepend="-")
+ assert os.environ["XYZ123"] == "3-2"
+ monkeypatch.undo()
+ assert "XYZ123" not in os.environ
+
+
+def test_monkeypatch_plugin(pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ def test_method(monkeypatch):
+ assert monkeypatch.__class__.__name__ == "MonkeyPatch"
+ """
+ )
+ res = reprec.countoutcomes()
+ assert tuple(res) == (1, 0, 0), res
+
+
+def test_syspath_prepend(mp: MonkeyPatch) -> None:
+ old = list(sys.path)
+ mp.syspath_prepend("world")
+ mp.syspath_prepend("hello")
+ assert sys.path[0] == "hello"
+ assert sys.path[1] == "world"
+ mp.undo()
+ assert sys.path == old
+ mp.undo()
+ assert sys.path == old
+
+
+def test_syspath_prepend_double_undo(mp: MonkeyPatch) -> None:
+ old_syspath = sys.path[:]
+ try:
+ mp.syspath_prepend("hello world")
+ mp.undo()
+ sys.path.append("more hello world")
+ mp.undo()
+ assert sys.path[-1] == "more hello world"
+ finally:
+ sys.path[:] = old_syspath
+
+
+def test_chdir_with_path_local(mp: MonkeyPatch, tmp_path: Path) -> None:
+ mp.chdir(tmp_path)
+ assert os.getcwd() == str(tmp_path)
+
+
+def test_chdir_with_str(mp: MonkeyPatch, tmp_path: Path) -> None:
+ mp.chdir(str(tmp_path))
+ assert os.getcwd() == str(tmp_path)
+
+
+def test_chdir_undo(mp: MonkeyPatch, tmp_path: Path) -> None:
+ cwd = os.getcwd()
+ mp.chdir(tmp_path)
+ mp.undo()
+ assert os.getcwd() == cwd
+
+
+def test_chdir_double_undo(mp: MonkeyPatch, tmp_path: Path) -> None:
+ mp.chdir(str(tmp_path))
+ mp.undo()
+ os.chdir(tmp_path)
+ mp.undo()
+ assert os.getcwd() == str(tmp_path)
+
+
+def test_issue185_time_breaks(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import time
+ def test_m(monkeypatch):
+ def f():
+ raise Exception
+ monkeypatch.setattr(time, "time", f)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ """
+ *1 passed*
+ """
+ )
+
+
+def test_importerror(pytester: Pytester) -> None:
+ p = pytester.mkpydir("package")
+ p.joinpath("a.py").write_text(
+ textwrap.dedent(
+ """\
+ import doesnotexist
+
+ x = 1
+ """
+ )
+ )
+ pytester.path.joinpath("test_importerror.py").write_text(
+ textwrap.dedent(
+ """\
+ def test_importerror(monkeypatch):
+ monkeypatch.setattr('package.a.x', 2)
+ """
+ )
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ """
+ *import error in package.a: No module named 'doesnotexist'*
+ """
+ )
+
+
+class Sample:
+ @staticmethod
+ def hello() -> bool:
+ return True
+
+
+class SampleInherit(Sample):
+ pass
+
+
+@pytest.mark.parametrize(
+ "Sample",
+ [Sample, SampleInherit],
+ ids=["new", "new-inherit"],
+)
+def test_issue156_undo_staticmethod(Sample: Type[Sample]) -> None:
+ monkeypatch = MonkeyPatch()
+
+ monkeypatch.setattr(Sample, "hello", None)
+ assert Sample.hello is None
+
+ monkeypatch.undo() # type: ignore[unreachable]
+ assert Sample.hello()
+
+
+def test_undo_class_descriptors_delattr() -> None:
+ class SampleParent:
+ @classmethod
+ def hello(_cls):
+ pass
+
+ @staticmethod
+ def world():
+ pass
+
+ class SampleChild(SampleParent):
+ pass
+
+ monkeypatch = MonkeyPatch()
+
+ original_hello = SampleChild.hello
+ original_world = SampleChild.world
+ monkeypatch.delattr(SampleParent, "hello")
+ monkeypatch.delattr(SampleParent, "world")
+ assert getattr(SampleParent, "hello", None) is None
+ assert getattr(SampleParent, "world", None) is None
+
+ monkeypatch.undo()
+ assert original_hello == SampleChild.hello
+ assert original_world == SampleChild.world
+
+
+def test_issue1338_name_resolving() -> None:
+ pytest.importorskip("requests")
+ monkeypatch = MonkeyPatch()
+ try:
+ monkeypatch.delattr("requests.sessions.Session.request")
+ finally:
+ monkeypatch.undo()
+
+
+def test_context() -> None:
+ monkeypatch = MonkeyPatch()
+
+ import functools
+ import inspect
+
+ with monkeypatch.context() as m:
+ m.setattr(functools, "partial", 3)
+ assert not inspect.isclass(functools.partial)
+ assert inspect.isclass(functools.partial)
+
+
+def test_context_classmethod() -> None:
+ class A:
+ x = 1
+
+ with MonkeyPatch.context() as m:
+ m.setattr(A, "x", 2)
+ assert A.x == 2
+ assert A.x == 1
+
+
+def test_syspath_prepend_with_namespace_packages(
+ pytester: Pytester, monkeypatch: MonkeyPatch
+) -> None:
+ for dirname in "hello", "world":
+ d = pytester.mkdir(dirname)
+ ns = d.joinpath("ns_pkg")
+ ns.mkdir()
+ ns.joinpath("__init__.py").write_text(
+ "__import__('pkg_resources').declare_namespace(__name__)"
+ )
+ lib = ns.joinpath(dirname)
+ lib.mkdir()
+ lib.joinpath("__init__.py").write_text("def check(): return %r" % dirname)
+
+ monkeypatch.syspath_prepend("hello")
+ import ns_pkg.hello
+
+ assert ns_pkg.hello.check() == "hello"
+
+ with pytest.raises(ImportError):
+ import ns_pkg.world
+
+ # Prepending should call fixup_namespace_packages.
+ monkeypatch.syspath_prepend("world")
+ import ns_pkg.world
+
+ assert ns_pkg.world.check() == "world"
+
+ # 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")
+ from main_app import app # noqa: F401
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_nodes.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_nodes.py
new file mode 100644
index 0000000000..df1439e1c4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_nodes.py
@@ -0,0 +1,167 @@
+import re
+import warnings
+from pathlib import Path
+from typing import cast
+from typing import List
+from typing import Type
+
+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
+
+
+def test_node_from_parent_disallowed_arguments() -> None:
+ with pytest.raises(TypeError, match="session is"):
+ nodes.Node.from_parent(None, session=None) # type: ignore[arg-type]
+ with pytest.raises(TypeError, match="config is"):
+ nodes.Node.from_parent(None, config=None) # type: ignore[arg-type]
+
+
+def test_node_direct_construction_deprecated() -> None:
+ with pytest.raises(
+ OutcomeException,
+ match=(
+ "Direct construction of _pytest.nodes.Node has been deprecated, please "
+ "use _pytest.nodes.Node.from_parent.\nSee "
+ "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
+ " for more details."
+ ),
+ ):
+ nodes.Node(None, session=None) # type: ignore[arg-type]
+
+
+def test_subclassing_both_item_and_collector_deprecated(
+ request, tmp_path: Path
+) -> None:
+ """
+ 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")
+
+ class SoWrong(nodes.Item, nodes.File):
+ def __init__(self, fspath, parent):
+ """Legacy ctor with legacy call # don't wana see"""
+ super().__init__(fspath, parent)
+
+ with pytest.warns(PytestWarning) as rec:
+ SoWrong.from_parent(
+ request.session, fspath=legacy_path(tmp_path / "broken.txt")
+ )
+ messages = [str(x.message) for x in rec]
+ assert any(
+ re.search(".*SoWrong.* not using a cooperative constructor.*", x)
+ for x in messages
+ )
+ assert any(
+ re.search("(?m)SoWrong .* should not be a collector", x) for x in messages
+ )
+
+
+@pytest.mark.parametrize(
+ "warn_type, msg", [(DeprecationWarning, "deprecated"), (PytestWarning, "pytest")]
+)
+def test_node_warn_is_no_longer_only_pytest_warnings(
+ pytester: Pytester, warn_type: Type[Warning], msg: str
+) -> None:
+ items = pytester.getitems(
+ """
+ def test():
+ pass
+ """
+ )
+ with pytest.warns(warn_type, match=msg):
+ items[0].warn(warn_type(msg))
+
+
+def test_node_warning_enforces_warning_types(pytester: Pytester) -> None:
+ items = pytester.getitems(
+ """
+ def test():
+ pass
+ """
+ )
+ with pytest.raises(
+ ValueError, match="warning must be an instance of Warning or subclass"
+ ):
+ items[0].warn(Exception("ok")) # type: ignore[arg-type]
+
+
+def test__check_initialpaths_for_relpath() -> None:
+ """Ensure that it handles dirs, and does not always use dirname."""
+ cwd = Path.cwd()
+
+ class FakeSession1:
+ _initialpaths = frozenset({cwd})
+
+ session = cast(pytest.Session, FakeSession1)
+
+ assert nodes._check_initialpaths_for_relpath(session, cwd) == ""
+
+ sub = cwd / "file"
+
+ class FakeSession2:
+ _initialpaths = frozenset({cwd})
+
+ session = cast(pytest.Session, FakeSession2)
+
+ assert nodes._check_initialpaths_for_relpath(session, sub) == "file"
+
+ outside = Path("/outside-this-does-not-exist")
+ assert nodes._check_initialpaths_for_relpath(session, outside) is None
+
+
+def test_failure_with_changed_cwd(pytester: Pytester) -> None:
+ """
+ Test failure lines should use absolute paths if cwd has changed since
+ invocation, so the path is correct (#6428).
+ """
+ p = pytester.makepyfile(
+ """
+ import os
+ import pytest
+
+ @pytest.fixture
+ def private_dir():
+ out_dir = 'ddd'
+ os.mkdir(out_dir)
+ old_dir = os.getcwd()
+ os.chdir(out_dir)
+ yield out_dir
+ os.chdir(old_dir)
+
+ def test_show_wrong_path(private_dir):
+ assert False
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines([str(p) + ":*: AssertionError", "*1 failed in *"])
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_nose.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_nose.py
new file mode 100644
index 0000000000..1ded8854bb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_nose.py
@@ -0,0 +1,498 @@
+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/testing/web-platform/tests/tools/third_party/pytest/testing/test_parseopt.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_parseopt.py
new file mode 100644
index 0000000000..28529d0437
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_parseopt.py
@@ -0,0 +1,344 @@
+import argparse
+import os
+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
+
+
+@pytest.fixture
+def parser() -> parseopt.Parser:
+ return parseopt.Parser(_ispytest=True)
+
+
+class TestParser:
+ def test_no_help_by_default(self) -> None:
+ parser = parseopt.Parser(usage="xyz", _ispytest=True)
+ pytest.raises(UsageError, lambda: parser.parse(["-h"]))
+
+ def test_custom_prog(self, parser: parseopt.Parser) -> None:
+ """Custom prog can be set for `argparse.ArgumentParser`."""
+ assert parser._getparser().prog == os.path.basename(sys.argv[0])
+ parser.prog = "custom-prog"
+ assert parser._getparser().prog == "custom-prog"
+
+ def test_argument(self) -> None:
+ with pytest.raises(parseopt.ArgumentError):
+ # need a short or long option
+ argument = parseopt.Argument()
+ argument = parseopt.Argument("-t")
+ assert argument._short_opts == ["-t"]
+ assert argument._long_opts == []
+ assert argument.dest == "t"
+ argument = parseopt.Argument("-t", "--test")
+ assert argument._short_opts == ["-t"]
+ assert argument._long_opts == ["--test"]
+ assert argument.dest == "test"
+ argument = parseopt.Argument("-t", "--test", dest="abc")
+ assert argument.dest == "abc"
+ assert str(argument) == (
+ "Argument(_short_opts: ['-t'], _long_opts: ['--test'], dest: 'abc')"
+ )
+
+ def test_argument_type(self) -> None:
+ argument = parseopt.Argument("-t", dest="abc", type=int)
+ assert argument.type is int
+ argument = parseopt.Argument("-t", dest="abc", type=str)
+ 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"]
+ )
+ assert argument.type is str
+
+ def test_argument_processopt(self) -> None:
+ argument = parseopt.Argument("-t", type=int)
+ argument.default = 42
+ argument.dest = "abc"
+ res = argument.attrs()
+ assert res["default"] == 42
+ assert res["dest"] == "abc"
+
+ def test_group_add_and_get(self, parser: parseopt.Parser) -> None:
+ group = parser.getgroup("hello", description="desc")
+ assert group.name == "hello"
+ assert group.description == "desc"
+
+ def test_getgroup_simple(self, parser: parseopt.Parser) -> None:
+ group = parser.getgroup("hello", description="desc")
+ assert group.name == "hello"
+ assert group.description == "desc"
+ group2 = parser.getgroup("hello")
+ assert group2 is group
+
+ def test_group_ordering(self, parser: parseopt.Parser) -> None:
+ parser.getgroup("1")
+ parser.getgroup("2")
+ parser.getgroup("3", after="1")
+ groups = parser._groups
+ groups_names = [x.name for x in groups]
+ assert groups_names == list("132")
+
+ def test_group_addoption(self) -> None:
+ group = parseopt.OptionGroup("hello", _ispytest=True)
+ group.addoption("--option1", action="store_true")
+ assert len(group.options) == 1
+ assert isinstance(group.options[0], parseopt.Argument)
+
+ def test_group_addoption_conflict(self) -> None:
+ group = parseopt.OptionGroup("hello again", _ispytest=True)
+ group.addoption("--option1", "--option-1", action="store_true")
+ with pytest.raises(ValueError) as err:
+ group.addoption("--option1", "--option-one", action="store_true")
+ assert str({"--option1"}) in str(err.value)
+
+ def test_group_shortopt_lowercase(self, parser: parseopt.Parser) -> None:
+ group = parser.getgroup("hello")
+ with pytest.raises(ValueError):
+ group.addoption("-x", action="store_true")
+ assert len(group.options) == 0
+ group._addoption("-x", action="store_true")
+ assert len(group.options) == 1
+
+ def test_parser_addoption(self, parser: parseopt.Parser) -> None:
+ group = parser.getgroup("custom options")
+ assert len(group.options) == 0
+ group.addoption("--option1", action="store_true")
+ assert len(group.options) == 1
+
+ def test_parse(self, parser: parseopt.Parser) -> None:
+ parser.addoption("--hello", dest="hello", action="store")
+ args = parser.parse(["--hello", "world"])
+ assert args.hello == "world"
+ assert not getattr(args, parseopt.FILE_OR_DIR)
+
+ def test_parse2(self, parser: parseopt.Parser) -> None:
+ args = parser.parse([Path(".")])
+ assert getattr(args, parseopt.FILE_OR_DIR)[0] == "."
+
+ def test_parse_known_args(self, parser: parseopt.Parser) -> None:
+ parser.parse_known_args([Path(".")])
+ parser.addoption("--hello", action="store_true")
+ ns = parser.parse_known_args(["x", "--y", "--hello", "this"])
+ assert ns.hello
+ assert ns.file_or_dir == ["x"]
+
+ def test_parse_known_and_unknown_args(self, parser: parseopt.Parser) -> None:
+ parser.addoption("--hello", action="store_true")
+ ns, unknown = parser.parse_known_and_unknown_args(
+ ["x", "--y", "--hello", "this"]
+ )
+ assert ns.hello
+ assert ns.file_or_dir == ["x"]
+ assert unknown == ["--y", "this"]
+
+ def test_parse_will_set_default(self, parser: parseopt.Parser) -> None:
+ parser.addoption("--hello", dest="hello", default="x", action="store")
+ option = parser.parse([])
+ assert option.hello == "x"
+ del option.hello
+ parser.parse_setoption([], option)
+ assert option.hello == "x"
+
+ def test_parse_setoption(self, parser: parseopt.Parser) -> None:
+ parser.addoption("--hello", dest="hello", action="store")
+ parser.addoption("--world", dest="world", default=42)
+
+ option = argparse.Namespace()
+ args = parser.parse_setoption(["--hello", "world"], option)
+ assert option.hello == "world"
+ assert option.world == 42
+ assert not args
+
+ def test_parse_special_destination(self, parser: parseopt.Parser) -> None:
+ parser.addoption("--ultimate-answer", type=int)
+ args = parser.parse(["--ultimate-answer", "42"])
+ assert args.ultimate_answer == 42
+
+ def test_parse_split_positional_arguments(self, parser: parseopt.Parser) -> None:
+ parser.addoption("-R", action="store_true")
+ parser.addoption("-S", action="store_false")
+ args = parser.parse(["-R", "4", "2", "-S"])
+ assert getattr(args, parseopt.FILE_OR_DIR) == ["4", "2"]
+ args = parser.parse(["-R", "-S", "4", "2", "-R"])
+ assert getattr(args, parseopt.FILE_OR_DIR) == ["4", "2"]
+ assert args.R is True
+ assert args.S is False
+ args = parser.parse(["-R", "4", "-S", "2"])
+ assert getattr(args, parseopt.FILE_OR_DIR) == ["4", "2"]
+ assert args.R is True
+ assert args.S is False
+
+ def test_parse_defaultgetter(self) -> None:
+ def defaultget(option):
+ if not hasattr(option, "type"):
+ return
+ if option.type is int:
+ option.default = 42
+ elif option.type is str:
+ option.default = "world"
+
+ parser = parseopt.Parser(processopt=defaultget, _ispytest=True)
+ parser.addoption("--this", dest="this", type=int, action="store")
+ parser.addoption("--hello", dest="hello", type=str, action="store")
+ parser.addoption("--no", dest="no", action="store_true")
+ option = parser.parse([])
+ assert option.hello == "world"
+ assert option.this == 42
+ assert option.no is False
+
+ def test_drop_short_helper(self) -> None:
+ parser = argparse.ArgumentParser(
+ formatter_class=parseopt.DropShorterLongHelpFormatter, allow_abbrev=False
+ )
+ parser.add_argument(
+ "-t", "--twoword", "--duo", "--two-word", "--two", help="foo"
+ )
+ # throws error on --deux only!
+ parser.add_argument(
+ "-d", "--deuxmots", "--deux-mots", action="store_true", help="foo"
+ )
+ parser.add_argument("-s", action="store_true", help="single short")
+ parser.add_argument("--abc", "-a", action="store_true", help="bar")
+ parser.add_argument("--klm", "-k", "--kl-m", action="store_true", help="bar")
+ parser.add_argument(
+ "-P", "--pq-r", "-p", "--pqr", action="store_true", help="bar"
+ )
+ parser.add_argument(
+ "--zwei-wort", "--zweiwort", "--zweiwort", action="store_true", help="bar"
+ )
+ parser.add_argument(
+ "-x", "--exit-on-first", "--exitfirst", action="store_true", help="spam"
+ )
+ parser.add_argument("files_and_dirs", nargs="*")
+ args = parser.parse_args(["-k", "--duo", "hallo", "--exitfirst"])
+ assert args.twoword == "hallo"
+ assert args.klm is True
+ assert args.zwei_wort is False
+ assert args.exit_on_first is True
+ assert args.s is False
+ args = parser.parse_args(["--deux-mots"])
+ with pytest.raises(AttributeError):
+ assert args.deux_mots is True
+ assert args.deuxmots is True
+ args = parser.parse_args(["file", "dir"])
+ assert "|".join(args.files_and_dirs) == "file|dir"
+
+ def test_drop_short_0(self, parser: parseopt.Parser) -> None:
+ parser.addoption("--funcarg", "--func-arg", action="store_true")
+ parser.addoption("--abc-def", "--abc-def", action="store_true")
+ parser.addoption("--klm-hij", action="store_true")
+ with pytest.raises(UsageError):
+ parser.parse(["--funcarg", "--k"])
+
+ def test_drop_short_2(self, parser: parseopt.Parser) -> None:
+ parser.addoption("--func-arg", "--doit", action="store_true")
+ args = parser.parse(["--doit"])
+ assert args.func_arg is True
+
+ def test_drop_short_3(self, parser: parseopt.Parser) -> None:
+ parser.addoption("--func-arg", "--funcarg", "--doit", action="store_true")
+ args = parser.parse(["abcd"])
+ assert args.func_arg is False
+ assert args.file_or_dir == ["abcd"]
+
+ def test_drop_short_help0(self, parser: parseopt.Parser) -> None:
+ parser.addoption("--func-args", "--doit", help="foo", action="store_true")
+ parser.parse([])
+ help = parser.optparser.format_help()
+ assert "--func-args, --doit foo" in help
+
+ # testing would be more helpful with all help generated
+ def test_drop_short_help1(self, parser: parseopt.Parser) -> None:
+ group = parser.getgroup("general")
+ group.addoption("--doit", "--func-args", action="store_true", help="foo")
+ group._addoption(
+ "-h",
+ "--help",
+ action="store_true",
+ dest="help",
+ help="show help message and configuration info",
+ )
+ parser.parse(["-h"])
+ help = parser.optparser.format_help()
+ assert "-doit, --func-args foo" in help
+
+ def test_multiple_metavar_help(self, parser: parseopt.Parser) -> None:
+ """
+ Help text for options with a metavar tuple should display help
+ in the form "--preferences=value1 value2 value3" (#2004).
+ """
+ group = parser.getgroup("general")
+ group.addoption(
+ "--preferences", metavar=("value1", "value2", "value3"), nargs=3
+ )
+ group._addoption("-h", "--help", action="store_true", dest="help")
+ parser.parse(["-h"])
+ help = parser.optparser.format_help()
+ assert "--preferences=value1 value2 value3" in help
+
+
+def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
+ try:
+ bash_version = subprocess.run(
+ ["bash", "--version"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ check=True,
+ universal_newlines=True,
+ ).stdout
+ except (OSError, subprocess.CalledProcessError):
+ pytest.skip("bash is not available")
+ if "GNU bash" not in bash_version:
+ # See #7518.
+ pytest.skip("not a real bash")
+
+ script = str(pytester.path.joinpath("test_argcomplete"))
+
+ with open(str(script), "w") 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)
+ )
+ )
+ # alternative would be extended Pytester.{run(),_run(),popen()} to be able
+ # to handle a keyword argument env that replaces os.environ in popen or
+ # extends the copy, advantage: could not forget to restore
+ monkeypatch.setenv("_ARGCOMPLETE", "1")
+ monkeypatch.setenv("_ARGCOMPLETE_IFS", "\x0b")
+ monkeypatch.setenv("COMP_WORDBREAKS", " \\t\\n\"\\'><=;|&(:")
+
+ arg = "--fu"
+ monkeypatch.setenv("COMP_LINE", "pytest " + arg)
+ monkeypatch.setenv("COMP_POINT", str(len("pytest " + arg)))
+ result = pytester.run("bash", str(script), arg)
+ if result.ret == 255:
+ # argcomplete not found
+ 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()
+ )
+ )
+ else:
+ result.stdout.fnmatch_lines(["--funcargs", "--fulltrace"])
+ os.mkdir("test_argcomplete.d")
+ arg = "test_argc"
+ monkeypatch.setenv("COMP_LINE", "pytest " + arg)
+ monkeypatch.setenv("COMP_POINT", str(len("pytest " + arg)))
+ result = pytester.run("bash", str(script), arg)
+ result.stdout.fnmatch_lines(["test_argcomplete", "test_argcomplete.d/"])
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_pastebin.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_pastebin.py
new file mode 100644
index 0000000000..b338519ae1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_pastebin.py
@@ -0,0 +1,184 @@
+import io
+from typing import List
+from typing import Union
+
+import pytest
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pytester import Pytester
+
+
+class TestPasteCapture:
+ @pytest.fixture
+ def pastebinlist(self, monkeypatch, request) -> List[Union[str, bytes]]:
+ pastebinlist: List[Union[str, bytes]] = []
+ plugin = request.config.pluginmanager.getplugin("pastebin")
+ monkeypatch.setattr(plugin, "create_new_paste", pastebinlist.append)
+ return pastebinlist
+
+ def test_failed(self, pytester: Pytester, pastebinlist) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import pytest
+ def test_pass() -> None:
+ pass
+ def test_fail():
+ assert 0
+ def test_skip():
+ pytest.skip("")
+ """
+ )
+ reprec = pytester.inline_run(testpath, "--pastebin=failed")
+ assert len(pastebinlist) == 1
+ s = pastebinlist[0]
+ assert s.find("def test_fail") != -1
+ assert reprec.countoutcomes() == [1, 1, 1]
+
+ def test_all(self, pytester: Pytester, pastebinlist) -> None:
+ from _pytest.pytester import LineMatcher
+
+ testpath = pytester.makepyfile(
+ """
+ import pytest
+ def test_pass():
+ pass
+ def test_fail():
+ assert 0
+ def test_skip():
+ pytest.skip("")
+ """
+ )
+ reprec = pytester.inline_run(testpath, "--pastebin=all", "-v")
+ assert reprec.countoutcomes() == [1, 1, 1]
+ assert len(pastebinlist) == 1
+ contents = pastebinlist[0].decode("utf-8")
+ matcher = LineMatcher(contents.splitlines())
+ matcher.fnmatch_lines(
+ [
+ "*test_pass PASSED*",
+ "*test_fail FAILED*",
+ "*test_skip SKIPPED*",
+ "*== 1 failed, 1 passed, 1 skipped in *",
+ ]
+ )
+
+ def test_non_ascii_paste_text(self, pytester: Pytester, pastebinlist) -> None:
+ """Make sure that text which contains non-ascii characters is pasted
+ correctly. See #1219.
+ """
+ pytester.makepyfile(
+ test_unicode="""\
+ def test():
+ assert '☺' == 1
+ """
+ )
+ result = pytester.runpytest("--pastebin=all")
+ expected_msg = "*assert '☺' == 1*"
+ result.stdout.fnmatch_lines(
+ [
+ expected_msg,
+ "*== 1 failed in *",
+ "*Sending information to Paste Service*",
+ ]
+ )
+ assert len(pastebinlist) == 1
+
+
+class TestPaste:
+ @pytest.fixture
+ def pastebin(self, request):
+ return request.config.pluginmanager.getplugin("pastebin")
+
+ @pytest.fixture
+ def mocked_urlopen_fail(self, monkeypatch: MonkeyPatch):
+ """Monkeypatch the actual urlopen call to emulate a HTTP Error 400."""
+ calls = []
+
+ import urllib.error
+ import urllib.request
+
+ def mocked(url, data):
+ calls.append((url, data))
+ raise urllib.error.HTTPError(url, 400, "Bad request", {}, io.BytesIO())
+
+ monkeypatch.setattr(urllib.request, "urlopen", mocked)
+ return calls
+
+ @pytest.fixture
+ def mocked_urlopen_invalid(self, monkeypatch: MonkeyPatch):
+ """Monkeypatch the actual urlopen calls done by the internal plugin
+ function that connects to bpaste service, but return a url in an
+ unexpected format."""
+ calls = []
+
+ def mocked(url, data):
+ calls.append((url, data))
+
+ class DummyFile:
+ def read(self):
+ # part of html of a normal response
+ return b'View <a href="/invalid/3c0c6750bd">raw</a>.'
+
+ return DummyFile()
+
+ import urllib.request
+
+ monkeypatch.setattr(urllib.request, "urlopen", mocked)
+ return calls
+
+ @pytest.fixture
+ def mocked_urlopen(self, monkeypatch: MonkeyPatch):
+ """Monkeypatch the actual urlopen calls done by the internal plugin
+ function that connects to bpaste service."""
+ calls = []
+
+ def mocked(url, data):
+ calls.append((url, data))
+
+ class DummyFile:
+ def read(self):
+ # part of html of a normal response
+ return b'View <a href="/raw/3c0c6750bd">raw</a>.'
+
+ return DummyFile()
+
+ import urllib.request
+
+ monkeypatch.setattr(urllib.request, "urlopen", mocked)
+ return calls
+
+ def test_pastebin_invalid_url(self, pastebin, mocked_urlopen_invalid) -> None:
+ result = pastebin.create_new_paste(b"full-paste-contents")
+ assert (
+ result
+ == "bad response: invalid format ('View <a href=\"/invalid/3c0c6750bd\">raw</a>.')"
+ )
+ assert len(mocked_urlopen_invalid) == 1
+
+ def test_pastebin_http_error(self, pastebin, mocked_urlopen_fail) -> None:
+ result = pastebin.create_new_paste(b"full-paste-contents")
+ assert result == "bad response: HTTP Error 400: Bad request"
+ assert len(mocked_urlopen_fail) == 1
+
+ def test_create_new_paste(self, pastebin, mocked_urlopen) -> None:
+ result = pastebin.create_new_paste(b"full-paste-contents")
+ assert result == "https://bpa.st/show/3c0c6750bd"
+ assert len(mocked_urlopen) == 1
+ url, data = mocked_urlopen[0]
+ assert type(data) is bytes
+ lexer = "text"
+ assert url == "https://bpa.st"
+ assert "lexer=%s" % lexer in data.decode()
+ assert "code=full-paste-contents" in data.decode()
+ assert "expiry=1week" in data.decode()
+
+ def test_create_new_paste_failure(self, pastebin, monkeypatch: MonkeyPatch) -> None:
+ import io
+ import urllib.request
+
+ def response(url, data):
+ stream = io.BytesIO(b"something bad occurred")
+ return stream
+
+ monkeypatch.setattr(urllib.request, "urlopen", response)
+ result = pastebin.create_new_paste(b"full-paste-contents")
+ assert result == "bad response: invalid format ('something bad occurred')"
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_pathlib.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_pathlib.py
new file mode 100644
index 0000000000..5eb153e847
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_pathlib.py
@@ -0,0 +1,574 @@
+import os.path
+import pickle
+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
+
+import pytest
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pathlib import bestrelpath
+from _pytest.pathlib import commonpath
+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 ImportPathMismatchError
+from _pytest.pathlib import insert_missing_modules
+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 symlink_or_skip
+from _pytest.pathlib import visit
+from _pytest.tmpdir import TempPathFactory
+
+
+class TestFNMatcherPort:
+ """Test our port of py.common.FNMatcher (fnmatch_ex)."""
+
+ if sys.platform == "win32":
+ drv1 = "c:"
+ drv2 = "d:"
+ else:
+ drv1 = "/c"
+ drv2 = "/d"
+
+ @pytest.mark.parametrize(
+ "pattern, path",
+ [
+ ("*.py", "foo.py"),
+ ("*.py", "bar/foo.py"),
+ ("test_*.py", "foo/test_foo.py"),
+ ("tests/*.py", "tests/foo.py"),
+ (f"{drv1}/*.py", f"{drv1}/foo.py"),
+ (f"{drv1}/foo/*.py", f"{drv1}/foo/foo.py"),
+ ("tests/**/test*.py", "tests/foo/test_foo.py"),
+ ("tests/**/doc/test*.py", "tests/foo/bar/doc/test_foo.py"),
+ ("tests/**/doc/**/test*.py", "tests/foo/doc/bar/test_foo.py"),
+ ],
+ )
+ def test_matching(self, pattern: str, path: str) -> None:
+ assert fnmatch_ex(pattern, path)
+
+ def test_matching_abspath(self) -> None:
+ abspath = os.path.abspath(os.path.join("tests/foo.py"))
+ assert fnmatch_ex("tests/foo.py", abspath)
+
+ @pytest.mark.parametrize(
+ "pattern, path",
+ [
+ ("*.py", "foo.pyc"),
+ ("*.py", "foo/foo.pyc"),
+ ("tests/*.py", "foo/foo.py"),
+ (f"{drv1}/*.py", f"{drv2}/foo.py"),
+ (f"{drv1}/foo/*.py", f"{drv2}/foo/foo.py"),
+ ("tests/**/test*.py", "tests/foo.py"),
+ ("tests/**/test*.py", "foo/test_foo.py"),
+ ("tests/**/doc/test*.py", "tests/foo/bar/doc/foo.py"),
+ ("tests/**/doc/test*.py", "tests/foo/bar/test_foo.py"),
+ ],
+ )
+ def test_not_matching(self, pattern: str, path: str) -> None:
+ assert not fnmatch_ex(pattern, path)
+
+
+class TestImportPath:
+ """
+
+ Most of the tests here were copied from py lib's tests for "py.local.path.pyimport".
+
+ Having our own pyimport-like function is inline with removing py.path dependency in the future.
+ """
+
+ @pytest.fixture(scope="session")
+ def path1(self, tmp_path_factory: TempPathFactory) -> Generator[Path, None, None]:
+ path = tmp_path_factory.mktemp("path")
+ self.setuptestfs(path)
+ yield path
+ assert path.joinpath("samplefile").exists()
+
+ def setuptestfs(self, path: Path) -> None:
+ # print "setting up test fs for", repr(path)
+ samplefile = path / "samplefile"
+ samplefile.write_text("samplefile\n")
+
+ execfile = path / "execfile"
+ execfile.write_text("x=42")
+
+ execfilepy = path / "execfile.py"
+ execfilepy.write_text("x=42")
+
+ d = {1: 2, "hello": "world", "answer": 42}
+ path.joinpath("samplepickle").write_bytes(pickle.dumps(d, 1))
+
+ sampledir = path / "sampledir"
+ sampledir.mkdir()
+ sampledir.joinpath("otherfile").touch()
+
+ otherdir = path / "otherdir"
+ otherdir.mkdir()
+ otherdir.joinpath("__init__.py").touch()
+
+ module_a = otherdir / "a.py"
+ module_a.write_text("from .b import stuff as result\n")
+ module_b = otherdir / "b.py"
+ module_b.write_text('stuff="got it"\n')
+ module_c = otherdir / "c.py"
+ module_c.write_text(
+ dedent(
+ """
+ import pluggy;
+ import otherdir.a
+ value = otherdir.a.result
+ """
+ )
+ )
+ module_d = otherdir / "d.py"
+ module_d.write_text(
+ dedent(
+ """
+ import pluggy;
+ from otherdir import a
+ value2 = a.result
+ """
+ )
+ )
+
+ def test_smoke_test(self, path1: Path) -> None:
+ obj = import_path(path1 / "execfile.py", root=path1)
+ assert obj.x == 42 # type: ignore[attr-defined]
+ assert obj.__name__ == "execfile"
+
+ def test_renamed_dir_creates_mismatch(
+ self, tmp_path: Path, monkeypatch: MonkeyPatch
+ ) -> None:
+ tmp_path.joinpath("a").mkdir()
+ p = tmp_path.joinpath("a", "test_x123.py")
+ p.touch()
+ import_path(p, root=tmp_path)
+ 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)
+
+ # Errors can be ignored.
+ monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "1")
+ import_path(tmp_path.joinpath("b", "test_x123.py"), root=tmp_path)
+
+ # 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)
+
+ def test_messy_name(self, tmp_path: Path) -> None:
+ # https://bitbucket.org/hpk42/py-trunk/issue/129
+ path = tmp_path / "foo__init__.py"
+ path.touch()
+ module = import_path(path, root=tmp_path)
+ assert module.__name__ == "foo__init__"
+
+ def test_dir(self, tmp_path: Path) -> None:
+ p = tmp_path / "hello_123"
+ p.mkdir()
+ p_init = p / "__init__.py"
+ p_init.touch()
+ m = import_path(p, root=tmp_path)
+ assert m.__name__ == "hello_123"
+ m = import_path(p_init, root=tmp_path)
+ assert m.__name__ == "hello_123"
+
+ def test_a(self, path1: Path) -> None:
+ otherdir = path1 / "otherdir"
+ mod = import_path(otherdir / "a.py", root=path1)
+ assert mod.result == "got it" # type: ignore[attr-defined]
+ assert mod.__name__ == "otherdir.a"
+
+ def test_b(self, path1: Path) -> None:
+ otherdir = path1 / "otherdir"
+ mod = import_path(otherdir / "b.py", root=path1)
+ assert mod.stuff == "got it" # type: ignore[attr-defined]
+ assert mod.__name__ == "otherdir.b"
+
+ def test_c(self, path1: Path) -> None:
+ otherdir = path1 / "otherdir"
+ mod = import_path(otherdir / "c.py", root=path1)
+ assert mod.value == "got it" # type: ignore[attr-defined]
+
+ def test_d(self, path1: Path) -> None:
+ otherdir = path1 / "otherdir"
+ mod = import_path(otherdir / "d.py", root=path1)
+ assert mod.value2 == "got it" # type: ignore[attr-defined]
+
+ def test_import_after(self, tmp_path: Path) -> 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)
+ assert mod1.__name__ == "xxxpackage.module1"
+ from xxxpackage import module1
+
+ assert module1 is mod1
+
+ def test_check_filepath_consistency(
+ self, monkeypatch: MonkeyPatch, tmp_path: Path
+ ) -> 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()
+ 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)
+ 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:
+ 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)
+ assert str(root1) == sys.path[-1]
+ assert str(root1) not in sys.path[:-1]
+
+ def test_invalid_path(self, tmp_path: Path) -> None:
+ with pytest.raises(ImportError):
+ import_path(tmp_path / "invalid.py", root=tmp_path)
+
+ @pytest.fixture
+ def simple_module(self, tmp_path: Path) -> Path:
+ fn = tmp_path / "_src/tests/mymod.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:
+ """`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]
+ assert str(simple_module.parent) not in sys.path
+ assert module.__name__ in sys.modules
+ assert module.__name__ == "_src.tests.mymod"
+ 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
+ ) -> 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
+
+ def test_no_meta_path_found(
+ self, simple_module: Path, monkeypatch: MonkeyPatch, tmp_path: Path
+ ) -> 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]
+
+ # mode='importlib' fails if no spec is found to load the module
+ import importlib.util
+
+ monkeypatch.setattr(
+ importlib.util, "spec_from_file_location", lambda *args: None
+ )
+ with pytest.raises(ImportError):
+ import_path(simple_module, mode="importlib", root=tmp_path)
+
+
+def test_resolve_package_path(tmp_path: Path) -> None:
+ pkg = tmp_path / "pkg1"
+ pkg.mkdir()
+ (pkg / "__init__.py").touch()
+ (pkg / "subdir").mkdir()
+ (pkg / "subdir/__init__.py").touch()
+ assert resolve_package_path(pkg) == pkg
+ assert resolve_package_path(pkg.joinpath("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.mkdir()
+ pkg.joinpath("subdir/__init__.py").touch()
+ assert resolve_package_path(subdir) == subdir
+ xyz = subdir.joinpath("xyz.py")
+ xyz.touch()
+ assert resolve_package_path(xyz) == subdir
+ assert not resolve_package_path(pkg)
+
+
+def test_access_denied_during_cleanup(tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
+ """Ensure that deleting a numbered dir does not fail because of OSErrors (#4262)."""
+ path = tmp_path / "temp-1"
+ path.mkdir()
+
+ def renamed_failed(*args):
+ raise OSError("access denied")
+
+ monkeypatch.setattr(Path, "rename", renamed_failed)
+
+ lock_path = get_lock_path(path)
+ maybe_delete_a_numbered_dir(path)
+ assert not lock_path.is_file()
+
+
+def test_long_path_during_cleanup(tmp_path: Path) -> None:
+ """Ensure that deleting long path works (particularly on Windows (#6775))."""
+ path = (tmp_path / ("a" * 250)).resolve()
+ if sys.platform == "win32":
+ # make sure that the full path is > 260 characters without any
+ # component being over 260 characters
+ assert len(str(path)) > 260
+ extended_path = "\\\\?\\" + str(path)
+ else:
+ extended_path = str(path)
+ os.mkdir(extended_path)
+ assert os.path.isdir(extended_path)
+ maybe_delete_a_numbered_dir(path)
+ assert not os.path.isdir(extended_path)
+
+
+def test_get_extended_length_path_str() -> None:
+ assert get_extended_length_path_str(r"c:\foo") == r"\\?\c:\foo"
+ assert get_extended_length_path_str(r"\\share\foo") == r"\\?\UNC\share\foo"
+ assert get_extended_length_path_str(r"\\?\UNC\share\foo") == r"\\?\UNC\share\foo"
+ assert get_extended_length_path_str(r"\\?\c:\foo") == r"\\?\c:\foo"
+
+
+def test_suppress_error_removing_lock(tmp_path: Path) -> None:
+ """ensure_deletable should be resilient if lock file cannot be removed (#5456, #7491)"""
+ path = tmp_path / "dir"
+ path.mkdir()
+ lock = get_lock_path(path)
+ lock.touch()
+ mtime = lock.stat().st_mtime
+
+ with unittest.mock.patch.object(Path, "unlink", side_effect=OSError) as m:
+ assert not ensure_deletable(
+ path, consider_lock_dead_if_created_before=mtime + 30
+ )
+ assert m.call_count == 1
+ assert lock.is_file()
+
+ with unittest.mock.patch.object(Path, "is_file", side_effect=OSError) as m:
+ assert not ensure_deletable(
+ path, consider_lock_dead_if_created_before=mtime + 30
+ )
+ assert m.call_count == 1
+ assert lock.is_file()
+
+ # check now that we can remove the lock file in normal circumstances
+ assert ensure_deletable(path, consider_lock_dead_if_created_before=mtime + 30)
+ assert not lock.is_file()
+
+
+def test_bestrelpath() -> None:
+ curdir = Path("/foo/bar/baz/path")
+ assert bestrelpath(curdir, curdir) == "."
+ assert bestrelpath(curdir, curdir / "hello" / "world") == "hello" + os.sep + "world"
+ assert bestrelpath(curdir, curdir.parent / "sister") == ".." + os.sep + "sister"
+ assert bestrelpath(curdir, curdir.parent) == ".."
+ assert bestrelpath(curdir, Path("hello")) == "hello"
+
+
+def test_commonpath() -> None:
+ path = Path("/foo/bar/baz/path")
+ subpath = path / "sampledir"
+ assert commonpath(path, subpath) == path
+ assert commonpath(subpath, path) == path
+ assert commonpath(Path(str(path) + "suffix"), path) == path.parent
+ assert commonpath(path, path.parent.parent) == path.parent.parent
+
+
+def test_visit_ignores_errors(tmp_path: Path) -> None:
+ symlink_or_skip("recursive", tmp_path / "recursive")
+ tmp_path.joinpath("foo").write_bytes(b"")
+ tmp_path.joinpath("bar").write_bytes(b"")
+
+ assert [
+ entry.name for entry in visit(str(tmp_path), recurse=lambda entry: False)
+ ] == ["bar", "foo"]
+
+
+@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only")
+def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
+ """
+ import_file() should not raise ImportPathMismatchError if the paths are exactly
+ equal on Windows. It seems directories mounted as UNC paths make os.path.samefile
+ return False, even when they are clearly equal.
+ """
+ module_path = tmp_path.joinpath("my_module.py")
+ module_path.write_text("def foo(): return 42")
+ monkeypatch.syspath_prepend(tmp_path)
+
+ with monkeypatch.context() as mp:
+ # Forcibly make os.path.samefile() return False here to ensure we are comparing
+ # 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)
+ 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:
+ """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)
+ fn.write_text(
+ dedent(
+ """
+ from dataclasses import dataclass
+
+ @dataclass
+ class Data:
+ value: str
+ """
+ )
+ )
+
+ module = import_path(fn, mode="importlib", root=tmp_path)
+ 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 that importlib mode works with pickle (#7859)."""
+ fn = tmp_path.joinpath("_src/tests/test_pickle.py")
+ fn.parent.mkdir(parents=True)
+ fn.write_text(
+ dedent(
+ """
+ import pickle
+
+ def _action():
+ return 42
+
+ def round_trip():
+ s = pickle.dumps(_action)
+ return pickle.loads(s)
+ """
+ )
+ )
+
+ module = import_path(fn, mode="importlib", root=tmp_path)
+ round_trip = getattr(module, "round_trip")
+ action = round_trip()
+ assert action() == 42
+
+ def test_importmode_importlib_with_pickle_separate_modules(
+ self, tmp_path: Path
+ ) -> None:
+ """
+ Ensure that importlib mode works can load pickles that look similar but are
+ defined in separate modules.
+ """
+ fn1 = tmp_path.joinpath("_src/m1/tests/test.py")
+ fn1.parent.mkdir(parents=True)
+ fn1.write_text(
+ dedent(
+ """
+ import attr
+ import pickle
+
+ @attr.s(auto_attribs=True)
+ class Data:
+ x: int = 42
+ """
+ )
+ )
+
+ fn2 = tmp_path.joinpath("_src/m2/tests/test.py")
+ fn2.parent.mkdir(parents=True)
+ fn2.write_text(
+ dedent(
+ """
+ import attr
+ import pickle
+
+ @attr.s(auto_attribs=True)
+ class Data:
+ x: str = ""
+ """
+ )
+ )
+
+ import pickle
+
+ def round_trip(obj):
+ s = pickle.dumps(obj)
+ return pickle.loads(s)
+
+ module = import_path(fn1, mode="importlib", root=tmp_path)
+ Data1 = getattr(module, "Data")
+
+ module = import_path(fn2, mode="importlib", root=tmp_path)
+ Data2 = getattr(module, "Data")
+
+ assert round_trip(Data1(20)) == Data1(20)
+ assert round_trip(Data2("hello")) == Data2("hello")
+ assert Data1.__module__ == "_src.m1.tests.test"
+ assert Data2.__module__ == "_src.m2.tests.test"
+
+ def test_module_name_from_path(self, tmp_path: Path) -> None:
+ result = module_name_from_path(tmp_path / "src/tests/test_foo.py", tmp_path)
+ assert result == "src.tests.test_foo"
+
+ # Path is not relative to root dir: use the full path to obtain the module name.
+ 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"]
+
+ mod = ModuleType("mod", doc="My Module")
+ modules = {"src": mod}
+ insert_missing_modules(modules, "src")
+ assert modules == {"src": mod}
+
+ modules = {}
+ insert_missing_modules(modules, "")
+ assert modules == {}
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_pluginmanager.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_pluginmanager.py
new file mode 100644
index 0000000000..9fe23d1779
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_pluginmanager.py
@@ -0,0 +1,427 @@
+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
+from _pytest.config.exceptions import UsageError
+from _pytest.main import Session
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pathlib import import_path
+from _pytest.pytester import Pytester
+
+
+@pytest.fixture
+def pytestpm() -> PytestPluginManager:
+ return PytestPluginManager()
+
+
+class TestPytestPluginInteractions:
+ def test_addhooks_conftestplugin(
+ self, pytester: Pytester, _config_for_test: Config
+ ) -> None:
+ pytester.makepyfile(
+ newhooks="""
+ def pytest_myhook(xyz):
+ "new hook"
+ """
+ )
+ conf = pytester.makeconftest(
+ """
+ import newhooks
+ def pytest_addhooks(pluginmanager):
+ pluginmanager.add_hookspecs(newhooks)
+ def pytest_myhook(xyz):
+ return xyz + 1
+ """
+ )
+ config = _config_for_test
+ pm = config.pluginmanager
+ pm.hook.pytest_addhooks.call_historic(
+ kwargs=dict(pluginmanager=config.pluginmanager)
+ )
+ config.pluginmanager._importconftest(
+ conf, importmode="prepend", rootpath=pytester.path
+ )
+ # print(config.pluginmanager.get_plugins())
+ res = config.hook.pytest_myhook(xyz=10)
+ assert res == [11]
+
+ def test_addhooks_nohooks(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import sys
+ def pytest_addhooks(pluginmanager):
+ pluginmanager.add_hookspecs(sys)
+ """
+ )
+ res = pytester.runpytest()
+ assert res.ret != 0
+ res.stderr.fnmatch_lines(["*did not find*sys*"])
+
+ def test_do_option_postinitialize(self, pytester: Pytester) -> None:
+ config = pytester.parseconfigure()
+ assert not hasattr(config.option, "test123")
+ p = pytester.makepyfile(
+ """
+ def pytest_addoption(parser):
+ parser.addoption('--test123', action="store_true",
+ default=True)
+ """
+ )
+ config.pluginmanager._importconftest(
+ p, importmode="prepend", rootpath=pytester.path
+ )
+ assert config.option.test123
+
+ def test_configure(self, pytester: Pytester) -> None:
+ config = pytester.parseconfig()
+ values = []
+
+ class A:
+ def pytest_configure(self):
+ values.append(self)
+
+ config.pluginmanager.register(A())
+ assert len(values) == 0
+ config._do_configure()
+ assert len(values) == 1
+ config.pluginmanager.register(A()) # leads to a configured() plugin
+ assert len(values) == 2
+ assert values[0] != values[1]
+
+ config._ensure_unconfigure()
+ config.pluginmanager.register(A())
+ assert len(values) == 2
+
+ def test_hook_tracing(self, _config_for_test: Config) -> None:
+ pytestpm = _config_for_test.pluginmanager # fully initialized with plugins
+ saveindent = []
+
+ class api1:
+ def pytest_plugin_registered(self):
+ saveindent.append(pytestpm.trace.root.indent)
+
+ class api2:
+ def pytest_plugin_registered(self):
+ saveindent.append(pytestpm.trace.root.indent)
+ raise ValueError()
+
+ values: List[str] = []
+ pytestpm.trace.root.setwriter(values.append)
+ undo = pytestpm.enable_tracing()
+ try:
+ indent = pytestpm.trace.root.indent
+ p = api1()
+ pytestpm.register(p)
+ assert pytestpm.trace.root.indent == indent
+ assert len(values) >= 2
+ assert "pytest_plugin_registered" in values[0]
+ assert "finish" in values[1]
+
+ values[:] = []
+ with pytest.raises(ValueError):
+ pytestpm.register(api2())
+ assert pytestpm.trace.root.indent == indent
+ assert saveindent[0] > indent
+ finally:
+ undo()
+
+ def test_hook_proxy(self, pytester: Pytester) -> None:
+ """Test the gethookproxy function(#2016)"""
+ config = pytester.parseconfig()
+ session = Session.from_config(config)
+ pytester.makepyfile(**{"tests/conftest.py": "", "tests/subdir/conftest.py": ""})
+
+ conftest1 = pytester.path.joinpath("tests/conftest.py")
+ conftest2 = pytester.path.joinpath("tests/subdir/conftest.py")
+
+ config.pluginmanager._importconftest(
+ conftest1, importmode="prepend", rootpath=pytester.path
+ )
+ ihook_a = session.gethookproxy(pytester.path / "tests")
+ assert ihook_a is not None
+ config.pluginmanager._importconftest(
+ conftest2, importmode="prepend", rootpath=pytester.path
+ )
+ ihook_b = session.gethookproxy(pytester.path / "tests")
+ assert ihook_a is not ihook_b
+
+ def test_hook_with_addoption(self, pytester: Pytester) -> None:
+ """Test that hooks can be used in a call to pytest_addoption"""
+ pytester.makepyfile(
+ newhooks="""
+ import pytest
+ @pytest.hookspec(firstresult=True)
+ def pytest_default_value():
+ pass
+ """
+ )
+ pytester.makepyfile(
+ myplugin="""
+ import newhooks
+ def pytest_addhooks(pluginmanager):
+ pluginmanager.add_hookspecs(newhooks)
+ def pytest_addoption(parser, pluginmanager):
+ default_value = pluginmanager.hook.pytest_default_value()
+ parser.addoption("--config", help="Config, defaults to %(default)s", default=default_value)
+ """
+ )
+ pytester.makeconftest(
+ """
+ pytest_plugins=("myplugin",)
+ def pytest_default_value():
+ return "default_value"
+ """
+ )
+ res = pytester.runpytest("--help")
+ res.stdout.fnmatch_lines(["*--config=CONFIG*default_value*"])
+
+
+def test_default_markers(pytester: Pytester) -> None:
+ result = pytester.runpytest("--markers")
+ result.stdout.fnmatch_lines(["*tryfirst*first*", "*trylast*last*"])
+
+
+def test_importplugin_error_message(
+ pytester: Pytester, pytestpm: PytestPluginManager
+) -> None:
+ """Don't hide import errors when importing plugins and provide
+ an easy to debug message.
+
+ See #375 and #1998.
+ """
+ pytester.syspathinsert(pytester.path)
+ pytester.makepyfile(
+ qwe="""\
+ def test_traceback():
+ raise ImportError('Not possible to import: ☺')
+ test_traceback()
+ """
+ )
+ with pytest.raises(ImportError) as excinfo:
+ pytestpm.import_plugin("qwe")
+
+ assert str(excinfo.value).endswith(
+ 'Error importing plugin "qwe": Not possible to import: ☺'
+ )
+ assert "in test_traceback" in str(excinfo.traceback[-1])
+
+
+class TestPytestPluginManager:
+ def test_register_imported_modules(self) -> None:
+ pm = PytestPluginManager()
+ mod = types.ModuleType("x.y.pytest_hello")
+ pm.register(mod)
+ assert pm.is_registered(mod)
+ values = pm.get_plugins()
+ assert mod in values
+ pytest.raises(ValueError, pm.register, mod)
+ pytest.raises(ValueError, lambda: pm.register(mod))
+ # assert not pm.is_registered(mod2)
+ assert pm.get_plugins() == values
+
+ def test_canonical_import(self, monkeypatch):
+ mod = types.ModuleType("pytest_xyz")
+ monkeypatch.setitem(sys.modules, "pytest_xyz", mod)
+ pm = PytestPluginManager()
+ pm.import_plugin("pytest_xyz")
+ assert pm.get_plugin("pytest_xyz") == mod
+ assert pm.is_registered(mod)
+
+ def test_consider_module(
+ self, pytester: Pytester, pytestpm: PytestPluginManager
+ ) -> None:
+ pytester.syspathinsert()
+ pytester.makepyfile(pytest_p1="#")
+ pytester.makepyfile(pytest_p2="#")
+ 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"
+
+ def test_consider_module_import_module(
+ self, pytester: Pytester, _config_for_test: Config
+ ) -> None:
+ pytestpm = _config_for_test.pluginmanager
+ mod = types.ModuleType("x")
+ mod.__dict__["pytest_plugins"] = "pytest_a"
+ aplugin = pytester.makepyfile(pytest_a="#")
+ reprec = pytester.make_hook_recorder(pytestpm)
+ pytester.syspathinsert(aplugin.parent)
+ pytestpm.consider_module(mod)
+ call = reprec.getcall(pytestpm.hook.pytest_plugin_registered.name)
+ assert call.plugin.__name__ == "pytest_a"
+
+ # check that it is not registered twice
+ pytestpm.consider_module(mod)
+ values = reprec.getcalls("pytest_plugin_registered")
+ assert len(values) == 1
+
+ def test_consider_env_fails_to_import(
+ self, monkeypatch: MonkeyPatch, pytestpm: PytestPluginManager
+ ) -> None:
+ monkeypatch.setenv("PYTEST_PLUGINS", "nonexisting", prepend=",")
+ with pytest.raises(ImportError):
+ pytestpm.consider_env()
+
+ @pytest.mark.filterwarnings("always")
+ def test_plugin_skip(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
+ p = pytester.makepyfile(
+ skipping1="""
+ import pytest
+ pytest.skip("hello", allow_module_level=True)
+ """
+ )
+ shutil.copy(p, p.with_name("skipping2.py"))
+ monkeypatch.setenv("PYTEST_PLUGINS", "skipping2")
+ result = pytester.runpytest("-p", "skipping1", syspathinsert=True)
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+ result.stdout.fnmatch_lines(
+ ["*skipped plugin*skipping1*hello*", "*skipped plugin*skipping2*hello*"]
+ )
+
+ def test_consider_env_plugin_instantiation(
+ self,
+ pytester: Pytester,
+ monkeypatch: MonkeyPatch,
+ pytestpm: PytestPluginManager,
+ ) -> None:
+ pytester.syspathinsert()
+ pytester.makepyfile(xy123="#")
+ monkeypatch.setitem(os.environ, "PYTEST_PLUGINS", "xy123")
+ l1 = len(pytestpm.get_plugins())
+ pytestpm.consider_env()
+ l2 = len(pytestpm.get_plugins())
+ assert l2 == l1 + 1
+ assert pytestpm.get_plugin("xy123")
+ pytestpm.consider_env()
+ l3 = len(pytestpm.get_plugins())
+ assert l2 == l3
+
+ def test_pluginmanager_ENV_startup(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ pytester.makepyfile(pytest_x500="#")
+ p = pytester.makepyfile(
+ """
+ import pytest
+ def test_hello(pytestconfig):
+ plugin = pytestconfig.pluginmanager.get_plugin('pytest_x500')
+ assert plugin is not None
+ """
+ )
+ monkeypatch.setenv("PYTEST_PLUGINS", "pytest_x500", prepend=",")
+ result = pytester.runpytest(p, syspathinsert=True)
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_import_plugin_importname(
+ self, pytester: Pytester, pytestpm: PytestPluginManager
+ ) -> None:
+ pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y")
+ pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwx.y")
+
+ pytester.syspathinsert()
+ pluginname = "pytest_hello"
+ pytester.makepyfile(**{pluginname: ""})
+ pytestpm.import_plugin("pytest_hello")
+ len1 = len(pytestpm.get_plugins())
+ pytestpm.import_plugin("pytest_hello")
+ len2 = len(pytestpm.get_plugins())
+ assert len1 == len2
+ plugin1 = pytestpm.get_plugin("pytest_hello")
+ assert plugin1.__name__.endswith("pytest_hello")
+ plugin2 = pytestpm.get_plugin("pytest_hello")
+ assert plugin2 is plugin1
+
+ def test_import_plugin_dotted_name(
+ self, pytester: Pytester, pytestpm: PytestPluginManager
+ ) -> None:
+ pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y")
+ pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwex.y")
+
+ pytester.syspathinsert()
+ pytester.mkpydir("pkg").joinpath("plug.py").write_text("x=3")
+ pluginname = "pkg.plug"
+ pytestpm.import_plugin(pluginname)
+ mod = pytestpm.get_plugin("pkg.plug")
+ assert mod.x == 3
+
+ def test_consider_conftest_deps(
+ self,
+ pytester: Pytester,
+ pytestpm: PytestPluginManager,
+ ) -> None:
+ mod = import_path(
+ pytester.makepyfile("pytest_plugins='xyz'"), root=pytester.path
+ )
+ with pytest.raises(ImportError):
+ pytestpm.consider_conftest(mod)
+
+
+class TestPytestPluginManagerBootstrapming:
+ def test_preparse_args(self, pytestpm: PytestPluginManager) -> None:
+ pytest.raises(
+ ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"])
+ )
+
+ # Handles -p without space (#3532).
+ with pytest.raises(ImportError) as excinfo:
+ pytestpm.consider_preparse(["-phello123"])
+ assert '"hello123"' in excinfo.value.args[0]
+ pytestpm.consider_preparse(["-pno:hello123"])
+
+ # Handles -p without following arg (when used without argparse).
+ pytestpm.consider_preparse(["-p"])
+
+ with pytest.raises(UsageError, match="^plugin main cannot be disabled$"):
+ pytestpm.consider_preparse(["-p", "no:main"])
+
+ def test_plugin_prevent_register(self, pytestpm: PytestPluginManager) -> None:
+ pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
+ l1 = pytestpm.get_plugins()
+ pytestpm.register(42, name="abc")
+ l2 = pytestpm.get_plugins()
+ assert len(l2) == len(l1)
+ assert 42 not in l2
+
+ def test_plugin_prevent_register_unregistered_alredy_registered(
+ self, pytestpm: PytestPluginManager
+ ) -> None:
+ pytestpm.register(42, name="abc")
+ l1 = pytestpm.get_plugins()
+ assert 42 in l1
+ pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
+ l2 = pytestpm.get_plugins()
+ assert 42 not in l2
+
+ def test_plugin_prevent_register_stepwise_on_cacheprovider_unregister(
+ self, pytestpm: PytestPluginManager
+ ) -> None:
+ """From PR #4304: The only way to unregister a module is documented at
+ the end of https://docs.pytest.org/en/stable/how-to/plugins.html.
+
+ When unregister cacheprovider, then unregister stepwise too.
+ """
+ pytestpm.register(42, name="cacheprovider")
+ pytestpm.register(43, name="stepwise")
+ l1 = pytestpm.get_plugins()
+ assert 42 in l1
+ assert 43 in l1
+ pytestpm.consider_preparse(["xyz", "-p", "no:cacheprovider"])
+ l2 = pytestpm.get_plugins()
+ assert 42 not in l2
+ assert 43 not in l2
+
+ def test_blocked_plugin_can_be_used(self, pytestpm: PytestPluginManager) -> None:
+ pytestpm.consider_preparse(["xyz", "-p", "no:abc", "-p", "abc"])
+
+ assert pytestpm.has_plugin("abc")
+ assert not pytestpm.is_blocked("abc")
+ assert not pytestpm.is_blocked("pytest_abc")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_pytester.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_pytester.py
new file mode 100644
index 0000000000..049f8b22d8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_pytester.py
@@ -0,0 +1,855 @@
+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
+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
+
+
+def test_make_hook_recorder(pytester: Pytester) -> None:
+ item = pytester.getitem("def test_func(): pass")
+ recorder = pytester.make_hook_recorder(item.config.pluginmanager)
+ assert not recorder.getfailures()
+
+ # (The silly condition is to fool mypy that the code below this is reachable)
+ if 1 + 1 == 2:
+ pytest.xfail("internal reportrecorder tests need refactoring")
+
+ class rep:
+ excinfo = None
+ passed = False
+ failed = True
+ skipped = False
+ when = "call"
+
+ recorder.hook.pytest_runtest_logreport(report=rep) # type: ignore[attr-defined]
+ failures = recorder.getfailures()
+ assert failures == [rep] # type: ignore[comparison-overlap]
+ failures = recorder.getfailures()
+ assert failures == [rep] # type: ignore[comparison-overlap]
+
+ class rep2:
+ excinfo = None
+ passed = False
+ failed = False
+ skipped = True
+ when = "call"
+
+ rep2.passed = False
+ rep2.skipped = True
+ recorder.hook.pytest_runtest_logreport(report=rep2) # type: ignore[attr-defined]
+
+ modcol = pytester.getmodulecol("")
+ rep3 = modcol.config.hook.pytest_make_collect_report(collector=modcol)
+ rep3.passed = False
+ rep3.failed = True
+ rep3.skipped = False
+ recorder.hook.pytest_collectreport(report=rep3) # type: ignore[attr-defined]
+
+ passed, skipped, failed = recorder.listoutcomes()
+ assert not passed and skipped and failed
+
+ numpassed, numskipped, numfailed = recorder.countoutcomes()
+ assert numpassed == 0
+ assert numskipped == 1
+ assert numfailed == 1
+ assert len(recorder.getfailedcollections()) == 1
+
+ recorder.unregister() # type: ignore[attr-defined]
+ recorder.clear()
+ recorder.hook.pytest_runtest_logreport(report=rep3) # type: ignore[attr-defined]
+ pytest.raises(ValueError, recorder.getfailures)
+
+
+def test_parseconfig(pytester: Pytester) -> None:
+ config1 = pytester.parseconfig()
+ config2 = pytester.parseconfig()
+ assert config2 is not config1
+
+
+def test_pytester_runs_with_plugin(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ pytest_plugins = "pytester"
+ def test_hello(pytester):
+ assert 1
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=1)
+
+
+def test_pytester_with_doctest(pytester: Pytester) -> None:
+ """Check that pytester can be used within doctests.
+
+ It used to use `request.function`, which is `None` with doctests."""
+ pytester.makepyfile(
+ **{
+ "sub/t-doctest.py": """
+ '''
+ >>> import os
+ >>> pytester = getfixture("pytester")
+ >>> str(pytester.makepyfile("content")).replace(os.sep, '/')
+ '.../basetemp/sub.t-doctest0/sub.py'
+ '''
+ """,
+ "sub/__init__.py": "",
+ }
+ )
+ result = pytester.runpytest(
+ "-p", "pytester", "--doctest-modules", "sub/t-doctest.py"
+ )
+ assert result.ret == 0
+
+
+def test_runresult_assertion_on_xfail(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ pytest_plugins = "pytester"
+
+ @pytest.mark.xfail
+ def test_potato():
+ assert False
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(xfailed=1)
+ assert result.ret == 0
+
+
+def test_runresult_assertion_on_xpassed(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ pytest_plugins = "pytester"
+
+ @pytest.mark.xfail
+ def test_potato():
+ assert True
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(xpassed=1)
+ assert result.ret == 0
+
+
+def test_xpassed_with_strict_is_considered_a_failure(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ pytest_plugins = "pytester"
+
+ @pytest.mark.xfail(strict=True)
+ def test_potato():
+ assert True
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(failed=1)
+ assert result.ret != 0
+
+
+def make_holder():
+ class apiclass:
+ def pytest_xyz(self, arg):
+ """X"""
+
+ def pytest_xyz_noarg(self):
+ """X"""
+
+ apimod = type(os)("api")
+
+ def pytest_xyz(arg):
+ """X"""
+
+ def pytest_xyz_noarg():
+ """X"""
+
+ apimod.pytest_xyz = pytest_xyz # type: ignore
+ apimod.pytest_xyz_noarg = pytest_xyz_noarg # type: ignore
+ return apiclass, apimod
+
+
+@pytest.mark.parametrize("holder", make_holder())
+def test_hookrecorder_basic(holder) -> None:
+ pm = PytestPluginManager()
+ pm.add_hookspecs(holder)
+ rec = HookRecorder(pm, _ispytest=True)
+ pm.hook.pytest_xyz(arg=123)
+ call = rec.popcall("pytest_xyz")
+ assert call.arg == 123
+ assert call._name == "pytest_xyz"
+ pytest.raises(pytest.fail.Exception, rec.popcall, "abc")
+ pm.hook.pytest_xyz_noarg()
+ call = rec.popcall("pytest_xyz_noarg")
+ assert call._name == "pytest_xyz_noarg"
+
+
+def test_makepyfile_unicode(pytester: Pytester) -> None:
+ pytester.makepyfile(chr(0xFFFD))
+
+
+def test_makepyfile_utf8(pytester: Pytester) -> None:
+ """Ensure makepyfile accepts utf-8 bytes as input (#2738)"""
+ utf8_contents = """
+ def setup_function(function):
+ mixed_encoding = 'São Paulo'
+ """.encode()
+ p = pytester.makepyfile(utf8_contents)
+ assert "mixed_encoding = 'São Paulo'".encode() in p.read_bytes()
+
+
+class TestInlineRunModulesCleanup:
+ def test_inline_run_test_module_not_cleaned_up(self, pytester: Pytester) -> None:
+ test_mod = pytester.makepyfile("def test_foo(): assert True")
+ 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")
+ result2 = pytester.inline_run(str(test_mod))
+ assert result2.ret == ExitCode.TESTS_FAILED
+
+ def spy_factory(self):
+ class SysModulesSnapshotSpy:
+ instances: List["SysModulesSnapshotSpy"] = [] # noqa: F821
+
+ def __init__(self, preserve=None) -> None:
+ SysModulesSnapshotSpy.instances.append(self)
+ self._spy_restore_count = 0
+ self._spy_preserve = preserve
+ self.__snapshot = SysModulesSnapshot(preserve=preserve)
+
+ def restore(self):
+ self._spy_restore_count += 1
+ return self.__snapshot.restore()
+
+ return SysModulesSnapshotSpy
+
+ def test_inline_run_taking_and_restoring_a_sys_modules_snapshot(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ spy_factory = self.spy_factory()
+ monkeypatch.setattr(pytester_mod, "SysModulesSnapshot", spy_factory)
+ pytester.syspathinsert()
+ original = dict(sys.modules)
+ pytester.makepyfile(import1="# you son of a silly person")
+ pytester.makepyfile(import2="# my hovercraft is full of eels")
+ test_mod = pytester.makepyfile(
+ """
+ import import1
+ def test_foo(): import import2"""
+ )
+ pytester.inline_run(str(test_mod))
+ assert len(spy_factory.instances) == 1
+ spy = spy_factory.instances[0]
+ assert spy._spy_restore_count == 1
+ assert sys.modules == original
+ assert all(sys.modules[x] is original[x] for x in sys.modules)
+
+ def test_inline_run_sys_modules_snapshot_restore_preserving_modules(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ spy_factory = self.spy_factory()
+ monkeypatch.setattr(pytester_mod, "SysModulesSnapshot", spy_factory)
+ test_mod = pytester.makepyfile("def test_foo(): pass")
+ pytester.inline_run(str(test_mod))
+ spy = spy_factory.instances[0]
+ assert not spy._spy_preserve("black_knight")
+ assert spy._spy_preserve("zope")
+ assert spy._spy_preserve("zope.interface")
+ assert spy._spy_preserve("zopelicious")
+
+ def test_external_test_module_imports_not_cleaned_up(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.syspathinsert()
+ pytester.makepyfile(imported="data = 'you son of a silly person'")
+ import imported
+
+ test_mod = pytester.makepyfile(
+ """
+ def test_foo():
+ import imported
+ imported.data = 42"""
+ )
+ pytester.inline_run(str(test_mod))
+ assert imported.data == 42
+
+
+def test_assert_outcomes_after_pytest_error(pytester: Pytester) -> None:
+ pytester.makepyfile("def test_foo(): assert True")
+
+ result = pytester.runpytest("--unexpected-argument")
+ with pytest.raises(ValueError, match="Pytest terminal summary report not found"):
+ 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"
+
+ def test_remove_added(self) -> None:
+ original = dict(sys.modules)
+ assert self.key not in sys.modules
+ snapshot = SysModulesSnapshot()
+ sys.modules[self.key] = ModuleType("something")
+ assert self.key in sys.modules
+ snapshot.restore()
+ assert sys.modules == original
+
+ def test_add_removed(self, monkeypatch: MonkeyPatch) -> None:
+ assert self.key not in sys.modules
+ monkeypatch.setitem(sys.modules, self.key, ModuleType("something"))
+ assert self.key in sys.modules
+ original = dict(sys.modules)
+ snapshot = SysModulesSnapshot()
+ del sys.modules[self.key]
+ assert self.key not in sys.modules
+ snapshot.restore()
+ assert sys.modules == original
+
+ def test_restore_reloaded(self, monkeypatch: MonkeyPatch) -> None:
+ assert self.key not in sys.modules
+ monkeypatch.setitem(sys.modules, self.key, ModuleType("something"))
+ assert self.key in sys.modules
+ original = dict(sys.modules)
+ snapshot = SysModulesSnapshot()
+ sys.modules[self.key] = ModuleType("something else")
+ snapshot.restore()
+ assert sys.modules == original
+
+ def test_preserve_modules(self, monkeypatch: MonkeyPatch) -> None:
+ key = [self.key + str(i) for i in range(3)]
+ assert not any(k in sys.modules for k in key)
+ for i, k in enumerate(key):
+ mod = ModuleType("something" + str(i))
+ monkeypatch.setitem(sys.modules, k, mod)
+ original = dict(sys.modules)
+
+ def preserve(name):
+ return name in (key[0], key[1], "some-other-key")
+
+ snapshot = SysModulesSnapshot(preserve=preserve)
+ sys.modules[key[0]] = original[key[0]] = ModuleType("something else0")
+ sys.modules[key[1]] = original[key[1]] = ModuleType("something else1")
+ sys.modules[key[2]] = ModuleType("something else2")
+ snapshot.restore()
+ assert sys.modules == original
+
+ def test_preserve_container(self, monkeypatch: MonkeyPatch) -> None:
+ original = dict(sys.modules)
+ assert self.key not in original
+ replacement = dict(sys.modules)
+ replacement[self.key] = ModuleType("life of brian")
+ snapshot = SysModulesSnapshot()
+ monkeypatch.setattr(sys, "modules", replacement)
+ snapshot.restore()
+ assert sys.modules is replacement
+ assert sys.modules == original
+
+
+@pytest.mark.parametrize("path_type", ("path", "meta_path"))
+class TestSysPathsSnapshot:
+ other_path = {"path": "meta_path", "meta_path": "path"}
+
+ @staticmethod
+ def path(n: int) -> str:
+ return "my-dirty-little-secret-" + str(n)
+
+ def test_restore(self, monkeypatch: MonkeyPatch, path_type) -> None:
+ other_path_type = self.other_path[path_type]
+ for i in range(10):
+ assert self.path(i) not in getattr(sys, path_type)
+ sys_path = [self.path(i) for i in range(6)]
+ monkeypatch.setattr(sys, path_type, sys_path)
+ original = list(sys_path)
+ original_other = list(getattr(sys, other_path_type))
+ snapshot = SysPathsSnapshot()
+ transformation = {"source": (0, 1, 2, 3, 4, 5), "target": (6, 2, 9, 7, 5, 8)}
+ assert sys_path == [self.path(x) for x in transformation["source"]]
+ sys_path[1] = self.path(6)
+ sys_path[3] = self.path(7)
+ sys_path.append(self.path(8))
+ del sys_path[4]
+ sys_path[3:3] = [self.path(9)]
+ del sys_path[0]
+ assert sys_path == [self.path(x) for x in transformation["target"]]
+ snapshot.restore()
+ assert getattr(sys, path_type) is sys_path
+ assert getattr(sys, path_type) == original
+ assert getattr(sys, other_path_type) == original_other
+
+ def test_preserve_container(self, monkeypatch: MonkeyPatch, path_type) -> None:
+ other_path_type = self.other_path[path_type]
+ original_data = list(getattr(sys, path_type))
+ original_other = getattr(sys, other_path_type)
+ original_other_data = list(original_other)
+ new: List[object] = []
+ snapshot = SysPathsSnapshot()
+ monkeypatch.setattr(sys, path_type, new)
+ snapshot.restore()
+ assert getattr(sys, path_type) is new
+ assert getattr(sys, path_type) == original_data
+ assert getattr(sys, other_path_type) is original_other
+ assert getattr(sys, other_path_type) == original_other_data
+
+
+def test_pytester_subprocess(pytester: Pytester) -> None:
+ testfile = pytester.makepyfile("def test_one(): pass")
+ assert pytester.runpytest_subprocess(testfile).ret == 0
+
+
+def test_pytester_subprocess_via_runpytest_arg(pytester: Pytester) -> None:
+ testfile = pytester.makepyfile(
+ """
+ def test_pytester_subprocess(pytester):
+ import os
+ testfile = pytester.makepyfile(
+ \"""
+ import os
+ def test_one():
+ assert {} != os.getpid()
+ \""".format(os.getpid())
+ )
+ assert pytester.runpytest(testfile).ret == 0
+ """
+ )
+ result = pytester.runpytest_inprocess(
+ "-p", "pytester", "--runpytest", "subprocess", testfile
+ )
+ assert result.ret == 0
+
+
+def test_unicode_args(pytester: Pytester) -> None:
+ result = pytester.runpytest("-k", "אבג")
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+
+def test_pytester_run_no_timeout(pytester: Pytester) -> None:
+ testfile = pytester.makepyfile("def test_no_timeout(): pass")
+ assert pytester.runpytest_subprocess(testfile).ret == ExitCode.OK
+
+
+def test_pytester_run_with_timeout(pytester: Pytester) -> None:
+ testfile = pytester.makepyfile("def test_no_timeout(): pass")
+
+ timeout = 120
+
+ start = time.time()
+ result = pytester.runpytest_subprocess(testfile, timeout=timeout)
+ end = time.time()
+ duration = end - start
+
+ assert result.ret == ExitCode.OK
+ assert duration < timeout
+
+
+def test_pytester_run_timeout_expires(pytester: Pytester) -> None:
+ testfile = pytester.makepyfile(
+ """
+ import time
+
+ def test_timeout():
+ time.sleep(10)"""
+ )
+ with pytest.raises(pytester.TimeoutExpired):
+ pytester.runpytest_subprocess(testfile, timeout=1)
+
+
+def test_linematcher_with_nonlist() -> None:
+ """Test LineMatcher with regard to passing in a set (accidentally)."""
+ from _pytest._code.source import Source
+
+ lm = LineMatcher([])
+ with pytest.raises(TypeError, match="invalid type for lines2: set"):
+ lm.fnmatch_lines(set()) # type: ignore[arg-type]
+ with pytest.raises(TypeError, match="invalid type for lines2: dict"):
+ lm.fnmatch_lines({}) # type: ignore[arg-type]
+ with pytest.raises(TypeError, match="invalid type for lines2: set"):
+ lm.re_match_lines(set()) # type: ignore[arg-type]
+ with pytest.raises(TypeError, match="invalid type for lines2: dict"):
+ lm.re_match_lines({}) # type: ignore[arg-type]
+ with pytest.raises(TypeError, match="invalid type for lines2: Source"):
+ lm.fnmatch_lines(Source()) # type: ignore[arg-type]
+ lm.fnmatch_lines([])
+ lm.fnmatch_lines(())
+ lm.fnmatch_lines("")
+ assert lm._getlines({}) == {} # type: ignore[arg-type,comparison-overlap]
+ assert lm._getlines(set()) == set() # type: ignore[arg-type,comparison-overlap]
+ assert lm._getlines(Source()) == []
+ assert lm._getlines(Source("pass\npass")) == ["pass", "pass"]
+
+
+def test_linematcher_match_failure() -> None:
+ lm = LineMatcher(["foo", "foo", "bar"])
+ with pytest.raises(pytest.fail.Exception) as e:
+ lm.fnmatch_lines(["foo", "f*", "baz"])
+ assert e.value.msg is not None
+ assert e.value.msg.splitlines() == [
+ "exact match: 'foo'",
+ "fnmatch: 'f*'",
+ " with: 'foo'",
+ "nomatch: 'baz'",
+ " and: 'bar'",
+ "remains unmatched: 'baz'",
+ ]
+
+ lm = LineMatcher(["foo", "foo", "bar"])
+ with pytest.raises(pytest.fail.Exception) as e:
+ lm.re_match_lines(["foo", "^f.*", "baz"])
+ assert e.value.msg is not None
+ assert e.value.msg.splitlines() == [
+ "exact match: 'foo'",
+ "re.match: '^f.*'",
+ " with: 'foo'",
+ " nomatch: 'baz'",
+ " and: 'bar'",
+ "remains unmatched: 'baz'",
+ ]
+
+
+def test_linematcher_consecutive() -> None:
+ lm = LineMatcher(["1", "", "2"])
+ with pytest.raises(pytest.fail.Exception) as excinfo:
+ lm.fnmatch_lines(["1", "2"], consecutive=True)
+ assert str(excinfo.value).splitlines() == [
+ "exact match: '1'",
+ "no consecutive match: '2'",
+ " with: ''",
+ ]
+
+ lm.re_match_lines(["1", r"\d?", "2"], consecutive=True)
+ with pytest.raises(pytest.fail.Exception) as excinfo:
+ lm.re_match_lines(["1", r"\d", "2"], consecutive=True)
+ assert str(excinfo.value).splitlines() == [
+ "exact match: '1'",
+ r"no consecutive match: '\\d'",
+ " with: ''",
+ ]
+
+
+@pytest.mark.parametrize("function", ["no_fnmatch_line", "no_re_match_line"])
+def test_linematcher_no_matching(function: str) -> None:
+ if function == "no_fnmatch_line":
+ good_pattern = "*.py OK*"
+ bad_pattern = "*X.py OK*"
+ else:
+ assert function == "no_re_match_line"
+ good_pattern = r".*py OK"
+ bad_pattern = r".*Xpy OK"
+
+ lm = LineMatcher(
+ [
+ "cachedir: .pytest_cache",
+ "collecting ... collected 1 item",
+ "",
+ "show_fixtures_per_test.py OK",
+ "=== elapsed 1s ===",
+ ]
+ )
+
+ # check the function twice to ensure we don't accumulate the internal buffer
+ for i in range(2):
+ with pytest.raises(pytest.fail.Exception) as e:
+ func = getattr(lm, function)
+ func(good_pattern)
+ obtained = str(e.value).splitlines()
+ if function == "no_fnmatch_line":
+ assert obtained == [
+ f"nomatch: '{good_pattern}'",
+ " and: 'cachedir: .pytest_cache'",
+ " and: 'collecting ... collected 1 item'",
+ " and: ''",
+ f"fnmatch: '{good_pattern}'",
+ " with: 'show_fixtures_per_test.py OK'",
+ ]
+ else:
+ assert obtained == [
+ f" nomatch: '{good_pattern}'",
+ " and: 'cachedir: .pytest_cache'",
+ " and: 'collecting ... collected 1 item'",
+ " and: ''",
+ f"re.match: '{good_pattern}'",
+ " with: 'show_fixtures_per_test.py OK'",
+ ]
+
+ func = getattr(lm, function)
+ func(bad_pattern) # bad pattern does not match any line: passes
+
+
+def test_linematcher_no_matching_after_match() -> None:
+ lm = LineMatcher(["1", "2", "3"])
+ lm.fnmatch_lines(["1", "3"])
+ with pytest.raises(pytest.fail.Exception) as e:
+ lm.no_fnmatch_line("*")
+ assert str(e.value).splitlines() == ["fnmatch: '*'", " with: '1'"]
+
+
+def test_linematcher_string_api() -> None:
+ lm = LineMatcher(["foo", "bar"])
+ assert str(lm) == "foo\nbar"
+
+
+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")
+ 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:
+ with pytest.raises(pytester.TimeoutExpired):
+ pytester.run(
+ sys.executable,
+ "-c",
+ "import sys, time; time.sleep(1); print(sys.stdin.read())",
+ stdin=subprocess.PIPE,
+ timeout=0.1,
+ )
+
+ with pytest.raises(pytester.TimeoutExpired):
+ result = pytester.run(
+ sys.executable,
+ "-c",
+ "import sys, time; time.sleep(1); print(sys.stdin.read())",
+ stdin=b"input\n2ndline",
+ timeout=0.1,
+ )
+
+ result = pytester.run(
+ sys.executable,
+ "-c",
+ "import sys; print(sys.stdin.read())",
+ stdin=b"input\n2ndline",
+ )
+ assert result.stdout.lines == ["input", "2ndline"]
+ assert result.stderr.str() == ""
+ assert result.ret == 0
+
+
+def test_popen_stdin_pipe(pytester: Pytester) -> None:
+ proc = pytester.popen(
+ [sys.executable, "-c", "import sys; print(sys.stdin.read())"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ stdin=subprocess.PIPE,
+ )
+ stdin = b"input\n2ndline"
+ stdout, stderr = proc.communicate(input=stdin)
+ assert stdout.decode("utf8").splitlines() == ["input", "2ndline"]
+ assert stderr == b""
+ assert proc.returncode == 0
+
+
+def test_popen_stdin_bytes(pytester: Pytester) -> None:
+ proc = pytester.popen(
+ [sys.executable, "-c", "import sys; print(sys.stdin.read())"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ stdin=b"input\n2ndline",
+ )
+ stdout, stderr = proc.communicate()
+ assert stdout.decode("utf8").splitlines() == ["input", "2ndline"]
+ assert stderr == b""
+ assert proc.returncode == 0
+
+
+def test_popen_default_stdin_stderr_and_stdin_None(pytester: Pytester) -> None:
+ # stdout, stderr default to pipes,
+ # stdin can be None to not close the pipe, avoiding
+ # "ValueError: flush of closed file" with `communicate()`.
+ #
+ # Wraps the test to make it not hang when run with "-s".
+ p1 = pytester.makepyfile(
+ '''
+ import sys
+
+ def test_inner(pytester):
+ p1 = pytester.makepyfile(
+ """
+ import sys
+ print(sys.stdin.read()) # empty
+ print('stdout')
+ sys.stderr.write('stderr')
+ """
+ )
+ proc = pytester.popen([sys.executable, str(p1)], stdin=None)
+ stdout, stderr = proc.communicate(b"ignored")
+ assert stdout.splitlines() == [b"", b"stdout"]
+ assert stderr.splitlines() == [b"stderr"]
+ assert proc.returncode == 0
+ '''
+ )
+ result = pytester.runpytest("-p", "pytester", str(p1))
+ assert result.ret == 0
+
+
+def test_spawn_uses_tmphome(pytester: Pytester) -> None:
+ tmphome = str(pytester.path)
+ assert os.environ.get("HOME") == tmphome
+
+ pytester._monkeypatch.setenv("CUSTOMENV", "42")
+
+ p1 = pytester.makepyfile(
+ """
+ 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()
+ assert child.wait() == 0, out.decode("utf8")
+
+
+def test_run_result_repr() -> None:
+ outlines = ["some", "normal", "output"]
+ errlines = ["some", "nasty", "errors", "happened"]
+
+ # 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"
+ " len(stderr.lines)=4 duration=0.50s>"
+ )
+
+ # unknown exit code: just the number
+ r = pytester_mod.RunResult(99, outlines, errlines, duration=0.5)
+ assert (
+ repr(r) == "<RunResult ret=99 len(stdout.lines)=3"
+ " len(stderr.lines)=4 duration=0.50s>"
+ )
+
+
+def test_pytester_outcomes_with_multiple_errors(pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def bad_fixture():
+ raise Exception("bad")
+
+ def test_error1(bad_fixture):
+ pass
+
+ def test_error2(bad_fixture):
+ pass
+ """
+ )
+ result = pytester.runpytest(str(p1))
+ result.assert_outcomes(errors=2)
+
+ assert result.parseoutcomes() == {"errors": 2}
+
+
+def test_parse_summary_line_always_plural() -> None:
+ """Parsing summaries always returns plural nouns (#6505)"""
+ lines = [
+ "some output 1",
+ "some output 2",
+ "======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====",
+ "done.",
+ ]
+ assert pytester_mod.RunResult.parse_summary_nouns(lines) == {
+ "errors": 1,
+ "failed": 1,
+ "passed": 1,
+ "warnings": 1,
+ }
+
+ lines = [
+ "some output 1",
+ "some output 2",
+ "======= 1 failed, 1 passed, 2 warnings, 2 errors in 0.13s ====",
+ "done.",
+ ]
+ assert pytester_mod.RunResult.parse_summary_nouns(lines) == {
+ "errors": 2,
+ "failed": 1,
+ "passed": 1,
+ "warnings": 2,
+ }
+
+
+def test_makefile_joins_absolute_path(pytester: Pytester) -> None:
+ absfile = pytester.path / "absfile"
+ p1 = pytester.makepyfile(**{str(absfile): ""})
+ assert str(p1) == str(pytester.path / "absfile.py")
+
+
+def test_pytester_makefile_dot_prefixes_extension_with_warning(
+ pytester: Pytester,
+) -> None:
+ with pytest.raises(
+ ValueError,
+ match="pytester.makefile expects a file extension, try .foo.bar instead of foo.bar",
+ ):
+ pytester.makefile("foo.bar", "")
+
+
+@pytest.mark.filterwarnings("default")
+def test_pytester_assert_outcomes_warnings(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import warnings
+
+ def test_with_warning():
+ warnings.warn(UserWarning("some custom warning"))
+ """
+ )
+ result = pytester.runpytest()
+ result.assert_outcomes(passed=1, warnings=1)
+ # If warnings is not passed, it is not checked at all.
+ result.assert_outcomes(passed=1)
+
+
+def test_pytester_outcomes_deselected(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_one():
+ pass
+
+ def test_two():
+ pass
+ """
+ )
+ result = pytester.runpytest("-k", "test_one")
+ result.assert_outcomes(passed=1, deselected=1)
+ # If deselected is not passed, it is not checked at all.
+ result.assert_outcomes(passed=1)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_python_path.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_python_path.py
new file mode 100644
index 0000000000..5ee0f55e36
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_python_path.py
@@ -0,0 +1,110 @@
+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
+
+
+@pytest.fixture()
+def file_structure(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_foo="""
+ from foo import foo
+
+ def test_foo():
+ assert foo() == 1
+ """
+ )
+
+ pytester.makepyfile(
+ test_bar="""
+ from bar import bar
+
+ def test_bar():
+ assert bar() == 2
+ """
+ )
+
+ foo_py = pytester.mkdir("sub") / "foo.py"
+ content = dedent(
+ """
+ def foo():
+ return 1
+ """
+ )
+ foo_py.write_text(content, encoding="utf-8")
+
+ bar_py = pytester.mkdir("sub2") / "bar.py"
+ content = dedent(
+ """
+ def bar():
+ return 2
+ """
+ )
+ bar_py.write_text(content, encoding="utf-8")
+
+
+def test_one_dir(pytester: Pytester, file_structure) -> None:
+ pytester.makefile(".ini", pytest="[pytest]\npythonpath=sub\n")
+ result = pytester.runpytest("test_foo.py")
+ assert result.ret == 0
+ result.assert_outcomes(passed=1)
+
+
+def test_two_dirs(pytester: Pytester, file_structure) -> None:
+ pytester.makefile(".ini", pytest="[pytest]\npythonpath=sub sub2\n")
+ result = pytester.runpytest("test_foo.py", "test_bar.py")
+ assert result.ret == 0
+ result.assert_outcomes(passed=2)
+
+
+def test_module_not_found(pytester: Pytester, file_structure) -> None:
+ """Without the pythonpath setting, the module should not be found."""
+ pytester.makefile(".ini", pytest="[pytest]\n")
+ result = pytester.runpytest("test_foo.py")
+ assert result.ret == pytest.ExitCode.INTERRUPTED
+ result.assert_outcomes(errors=1)
+ expected_error = "E ModuleNotFoundError: No module named 'foo'"
+ result.stdout.fnmatch_lines([expected_error])
+
+
+def test_no_ini(pytester: Pytester, file_structure) -> None:
+ """If no ini file, test should error."""
+ result = pytester.runpytest("test_foo.py")
+ assert result.ret == pytest.ExitCode.INTERRUPTED
+ result.assert_outcomes(errors=1)
+ expected_error = "E ModuleNotFoundError: No module named 'foo'"
+ result.stdout.fnmatch_lines([expected_error])
+
+
+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.
+ # 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.
+ pytester.makefile(".ini", pytest="[pytest]\npythonpath=I_SHALL_BE_REMOVED\n")
+ pytester.makepyfile(test_foo="""def test_foo(): pass""")
+
+ before: Optional[List[str]] = None
+ after: Optional[List[str]] = None
+
+ class Plugin:
+ @pytest.hookimpl(hookwrapper=True, tryfirst=True)
+ def pytest_unconfigure(self) -> Generator[None, None, None]:
+ nonlocal before, after
+ before = sys.path.copy()
+ yield
+ after = sys.path.copy()
+
+ result = pytester.runpytest_inprocess(plugins=[Plugin()])
+ assert result.ret == 0
+
+ assert before is not None
+ assert after is not None
+ assert any("I_SHALL_BE_REMOVED" in entry for entry in before)
+ assert not any("I_SHALL_BE_REMOVED" in entry for entry in after)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_recwarn.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_recwarn.py
new file mode 100644
index 0000000000..d3f218f166
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_recwarn.py
@@ -0,0 +1,410 @@
+import re
+import warnings
+from typing import Optional
+
+import pytest
+from _pytest.pytester import Pytester
+from _pytest.recwarn import WarningsRecorder
+
+
+def test_recwarn_stacklevel(recwarn: WarningsRecorder) -> None:
+ warnings.warn("hello")
+ warn = recwarn.pop()
+ assert warn.filename == __file__
+
+
+def test_recwarn_functional(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import warnings
+ def test_method(recwarn):
+ warnings.warn("hello")
+ warn = recwarn.pop()
+ assert isinstance(warn.message, UserWarning)
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+@pytest.mark.filterwarnings("")
+def test_recwarn_captures_deprecation_warning(recwarn: WarningsRecorder) -> None:
+ """
+ Check that recwarn can capture DeprecationWarning by default
+ without custom filterwarnings (see #8666).
+ """
+ warnings.warn(DeprecationWarning("some deprecation"))
+ assert len(recwarn) == 1
+ assert recwarn.pop(DeprecationWarning)
+
+
+class TestWarningsRecorderChecker:
+ def test_recording(self) -> None:
+ rec = WarningsRecorder(_ispytest=True)
+ with rec:
+ assert not rec.list
+ warnings.warn_explicit("hello", UserWarning, "xyz", 13)
+ assert len(rec.list) == 1
+ warnings.warn(DeprecationWarning("hello"))
+ assert len(rec.list) == 2
+ warn = rec.pop()
+ assert str(warn.message) == "hello"
+ values = rec.list
+ rec.clear()
+ assert len(rec.list) == 0
+ assert values is rec.list
+ pytest.raises(AssertionError, rec.pop)
+
+ def test_warn_stacklevel(self) -> None:
+ """#4243"""
+ rec = WarningsRecorder(_ispytest=True)
+ with rec:
+ warnings.warn("test", DeprecationWarning, 2)
+
+ def test_typechecking(self) -> None:
+ from _pytest.recwarn import WarningsChecker
+
+ with pytest.raises(TypeError):
+ WarningsChecker(5, _ispytest=True) # type: ignore[arg-type]
+ with pytest.raises(TypeError):
+ WarningsChecker(("hi", RuntimeWarning), _ispytest=True) # type: ignore[arg-type]
+ with pytest.raises(TypeError):
+ WarningsChecker([DeprecationWarning, RuntimeWarning], _ispytest=True) # type: ignore[arg-type]
+
+ def test_invalid_enter_exit(self) -> None:
+ # wrap this test in WarningsRecorder to ensure warning state gets reset
+ with WarningsRecorder(_ispytest=True):
+ with pytest.raises(RuntimeError):
+ rec = WarningsRecorder(_ispytest=True)
+ rec.__exit__(None, None, None) # can't exit before entering
+
+ with pytest.raises(RuntimeError):
+ rec = WarningsRecorder(_ispytest=True)
+ with rec:
+ with rec:
+ pass # can't enter twice
+
+
+class TestDeprecatedCall:
+ """test pytest.deprecated_call()"""
+
+ def dep(self, i: int, j: Optional[int] = None) -> int:
+ if i == 0:
+ warnings.warn("is deprecated", DeprecationWarning, stacklevel=1)
+ return 42
+
+ def dep_explicit(self, i: int) -> None:
+ if i == 0:
+ warnings.warn_explicit(
+ "dep_explicit", category=DeprecationWarning, filename="hello", lineno=3
+ )
+
+ def test_deprecated_call_raises(self) -> None:
+ with pytest.raises(pytest.fail.Exception, match="No warnings of type"):
+ pytest.deprecated_call(self.dep, 3, 5)
+
+ def test_deprecated_call(self) -> None:
+ pytest.deprecated_call(self.dep, 0, 5)
+
+ def test_deprecated_call_ret(self) -> None:
+ ret = pytest.deprecated_call(self.dep, 0)
+ assert ret == 42
+
+ def test_deprecated_call_preserves(self) -> None:
+ # Type ignored because `onceregistry` and `filters` are not
+ # documented API.
+ onceregistry = warnings.onceregistry.copy() # type: ignore
+ filters = warnings.filters[:] # type: ignore
+ 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 warn is warnings.warn
+ assert warn_explicit is warnings.warn_explicit
+
+ def test_deprecated_explicit_call_raises(self) -> None:
+ with pytest.raises(pytest.fail.Exception):
+ pytest.deprecated_call(self.dep_explicit, 3)
+
+ def test_deprecated_explicit_call(self) -> None:
+ pytest.deprecated_call(self.dep_explicit, 0)
+ pytest.deprecated_call(self.dep_explicit, 0)
+
+ @pytest.mark.parametrize("mode", ["context_manager", "call"])
+ def test_deprecated_call_no_warning(self, mode) -> None:
+ """Ensure deprecated_call() raises the expected failure when its block/function does
+ not raise a deprecation warning.
+ """
+
+ def f():
+ pass
+
+ msg = "No warnings of type (.*DeprecationWarning.*, .*PendingDeprecationWarning.*)"
+ with pytest.raises(pytest.fail.Exception, match=msg):
+ if mode == "call":
+ pytest.deprecated_call(f)
+ else:
+ with pytest.deprecated_call():
+ f()
+
+ @pytest.mark.parametrize(
+ "warning_type", [PendingDeprecationWarning, DeprecationWarning]
+ )
+ @pytest.mark.parametrize("mode", ["context_manager", "call"])
+ @pytest.mark.parametrize("call_f_first", [True, False])
+ @pytest.mark.filterwarnings("ignore")
+ def test_deprecated_call_modes(self, warning_type, mode, call_f_first) -> None:
+ """Ensure deprecated_call() captures a deprecation warning as expected inside its
+ block/function.
+ """
+
+ def f():
+ warnings.warn(warning_type("hi"))
+ return 10
+
+ # ensure deprecated_call() can capture the warning even if it has already been triggered
+ if call_f_first:
+ assert f() == 10
+ if mode == "call":
+ assert pytest.deprecated_call(f) == 10
+ else:
+ 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"))
+
+ 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)
+
+
+class TestWarns:
+ def test_check_callable(self) -> None:
+ source = "warnings.warn('w1', RuntimeWarning)"
+ with pytest.raises(TypeError, match=r".* must be callable"):
+ pytest.warns(RuntimeWarning, source) # type: ignore
+
+ 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))
+ pytest.warns(RuntimeWarning, lambda: warnings.warn("w3", RuntimeWarning))
+
+ def test_function(self) -> None:
+ pytest.warns(
+ SyntaxWarning, lambda msg: warnings.warn(msg, SyntaxWarning), "syntax"
+ )
+
+ def test_warning_tuple(self) -> None:
+ pytest.warns(
+ (RuntimeWarning, SyntaxWarning), lambda: warnings.warn("w1", RuntimeWarning)
+ )
+ pytest.warns(
+ (RuntimeWarning, SyntaxWarning), lambda: warnings.warn("w2", SyntaxWarning)
+ )
+ 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):
+ warnings.warn("runtime", RuntimeWarning)
+
+ with pytest.warns(UserWarning):
+ warnings.warn("user", UserWarning)
+
+ 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',?\)\]."
+ )
+
+ 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',?\)\]."
+ )
+
+ 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: \[\]."
+ )
+
+ 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)
+
+ 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]
+ )
+ )
+ )
+
+ def test_record(self) -> None:
+ with pytest.warns(UserWarning) as record:
+ warnings.warn("user", UserWarning)
+
+ assert len(record) == 1
+ assert str(record[0].message) == "user"
+
+ def test_record_only(self) -> None:
+ with pytest.warns() as record:
+ 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_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_by_subclass(self) -> None:
+ with pytest.warns(Warning) as record:
+ warnings.warn("user", UserWarning)
+ warnings.warn("runtime", RuntimeWarning)
+
+ assert len(record) == 2
+ assert str(record[0].message) == "user"
+ assert str(record[1].message) == "runtime"
+
+ class MyUserWarning(UserWarning):
+ pass
+
+ class MyRuntimeWarning(RuntimeWarning):
+ pass
+
+ with pytest.warns((UserWarning, RuntimeWarning)) as record:
+ warnings.warn("user", MyUserWarning)
+ warnings.warn("runtime", MyRuntimeWarning)
+
+ assert len(record) == 2
+ assert str(record[0].message) == "user"
+ assert str(record[1].message) == "runtime"
+
+ def test_double_test(self, pytester: Pytester) -> None:
+ """If a test is run again, the warning should still be raised"""
+ pytester.makepyfile(
+ """
+ import pytest
+ import warnings
+
+ @pytest.mark.parametrize('run', [1, 2])
+ def test(run):
+ with pytest.warns(RuntimeWarning):
+ warnings.warn("runtime", RuntimeWarning)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*2 passed in*"])
+
+ def test_match_regex(self) -> None:
+ 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.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)
+
+ 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)
+
+ @pytest.mark.filterwarnings("ignore")
+ def test_can_capture_previously_warned(self) -> None:
+ def f() -> int:
+ warnings.warn(UserWarning("ohai"))
+ return 10
+
+ assert f() == 10
+ assert pytest.warns(UserWarning, f) == 10
+ assert pytest.warns(UserWarning, f) == 10
+ assert pytest.warns(UserWarning, f) != "10" # type: ignore[comparison-overlap]
+
+ def test_warns_context_manager_with_kwargs(self) -> None:
+ with pytest.raises(TypeError) as excinfo:
+ with pytest.warns(UserWarning, foo="bar"): # type: ignore
+ pass
+ assert "Unexpected keyword arguments" in str(excinfo.value)
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_reports.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_reports.py
new file mode 100644
index 0000000000..31b6cf1afc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_reports.py
@@ -0,0 +1,488 @@
+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.reports import CollectReport
+from _pytest.reports import TestReport
+
+
+class TestReportSerialization:
+ def test_xdist_longrepr_to_str_issue_241(self, pytester: Pytester) -> None:
+ """Regarding issue pytest-xdist#241.
+
+ This test came originally from test_remote.py in xdist (ca03269).
+ """
+ pytester.makepyfile(
+ """
+ def test_a(): assert False
+ def test_b(): pass
+ """
+ )
+ reprec = pytester.inline_run()
+ reports = reprec.getreports("pytest_runtest_logreport")
+ assert len(reports) == 6
+ test_a_call = reports[1]
+ assert test_a_call.when == "call"
+ assert test_a_call.outcome == "failed"
+ assert test_a_call._to_json()["longrepr"]["reprtraceback"]["style"] == "long"
+ test_b_call = reports[4]
+ assert test_b_call.when == "call"
+ assert test_b_call.outcome == "passed"
+ assert test_b_call._to_json()["longrepr"] is None
+
+ def test_xdist_report_longrepr_reprcrash_130(self, pytester: Pytester) -> None:
+ """Regarding issue pytest-xdist#130
+
+ This test came originally from test_remote.py in xdist (ca03269).
+ """
+ reprec = pytester.inline_runsource(
+ """
+ def test_fail():
+ assert False, 'Expected Message'
+ """
+ )
+ reports = reprec.getreports("pytest_runtest_logreport")
+ assert len(reports) == 3
+ rep = reports[1]
+ added_section = ("Failure Metadata", "metadata metadata", "*")
+ assert isinstance(rep.longrepr, ExceptionRepr)
+ rep.longrepr.sections.append(added_section)
+ d = rep._to_json()
+ a = TestReport._from_json(d)
+ assert isinstance(a.longrepr, ExceptionRepr)
+ # Check assembled == rep
+ assert a.__dict__.keys() == rep.__dict__.keys()
+ for key in rep.__dict__.keys():
+ if key != "longrepr":
+ assert getattr(a, key) == getattr(rep, key)
+ assert rep.longrepr.reprcrash is not None
+ assert a.longrepr.reprcrash is not None
+ assert rep.longrepr.reprcrash.lineno == a.longrepr.reprcrash.lineno
+ assert rep.longrepr.reprcrash.message == a.longrepr.reprcrash.message
+ assert rep.longrepr.reprcrash.path == a.longrepr.reprcrash.path
+ assert rep.longrepr.reprtraceback.entrysep == a.longrepr.reprtraceback.entrysep
+ assert (
+ rep.longrepr.reprtraceback.extraline == a.longrepr.reprtraceback.extraline
+ )
+ assert rep.longrepr.reprtraceback.style == a.longrepr.reprtraceback.style
+ assert rep.longrepr.sections == a.longrepr.sections
+ # Missing section attribute PR171
+ assert added_section in a.longrepr.sections
+
+ def test_reprentries_serialization_170(self, pytester: Pytester) -> None:
+ """Regarding issue pytest-xdist#170
+
+ This test came originally from test_remote.py in xdist (ca03269).
+ """
+ from _pytest._code.code import ReprEntry
+
+ reprec = pytester.inline_runsource(
+ """
+ def test_repr_entry():
+ x = 0
+ assert x
+ """,
+ "--showlocals",
+ )
+ reports = reprec.getreports("pytest_runtest_logreport")
+ assert len(reports) == 3
+ rep = reports[1]
+ assert isinstance(rep.longrepr, ExceptionRepr)
+ d = rep._to_json()
+ a = TestReport._from_json(d)
+ assert isinstance(a.longrepr, ExceptionRepr)
+
+ rep_entries = rep.longrepr.reprtraceback.reprentries
+ a_entries = a.longrepr.reprtraceback.reprentries
+ for i in range(len(a_entries)):
+ rep_entry = rep_entries[i]
+ assert isinstance(rep_entry, ReprEntry)
+ assert rep_entry.reprfileloc is not None
+ assert rep_entry.reprfuncargs is not None
+ assert rep_entry.reprlocals is not None
+
+ a_entry = a_entries[i]
+ assert isinstance(a_entry, ReprEntry)
+ assert a_entry.reprfileloc is not None
+ assert a_entry.reprfuncargs is not None
+ assert a_entry.reprlocals is not None
+
+ assert rep_entry.lines == a_entry.lines
+ assert rep_entry.reprfileloc.lineno == a_entry.reprfileloc.lineno
+ assert rep_entry.reprfileloc.message == a_entry.reprfileloc.message
+ assert rep_entry.reprfileloc.path == a_entry.reprfileloc.path
+ assert rep_entry.reprfuncargs.args == a_entry.reprfuncargs.args
+ assert rep_entry.reprlocals.lines == a_entry.reprlocals.lines
+ assert rep_entry.style == a_entry.style
+
+ def test_reprentries_serialization_196(self, pytester: Pytester) -> None:
+ """Regarding issue pytest-xdist#196
+
+ This test came originally from test_remote.py in xdist (ca03269).
+ """
+ from _pytest._code.code import ReprEntryNative
+
+ reprec = pytester.inline_runsource(
+ """
+ def test_repr_entry_native():
+ x = 0
+ assert x
+ """,
+ "--tb=native",
+ )
+ reports = reprec.getreports("pytest_runtest_logreport")
+ assert len(reports) == 3
+ rep = reports[1]
+ assert isinstance(rep.longrepr, ExceptionRepr)
+ d = rep._to_json()
+ a = TestReport._from_json(d)
+ assert isinstance(a.longrepr, ExceptionRepr)
+
+ rep_entries = rep.longrepr.reprtraceback.reprentries
+ a_entries = a.longrepr.reprtraceback.reprentries
+ for i in range(len(a_entries)):
+ assert isinstance(rep_entries[i], ReprEntryNative)
+ assert rep_entries[i].lines == a_entries[i].lines
+
+ def test_itemreport_outcomes(self, pytester: Pytester) -> None:
+ # This test came originally from test_remote.py in xdist (ca03269).
+ reprec = pytester.inline_runsource(
+ """
+ import pytest
+ def test_pass(): pass
+ def test_fail(): 0/0
+ @pytest.mark.skipif("True")
+ def test_skip(): pass
+ def test_skip_imperative():
+ pytest.skip("hello")
+ @pytest.mark.xfail("True")
+ def test_xfail(): 0/0
+ def test_xfail_imperative():
+ pytest.xfail("hello")
+ """
+ )
+ reports = reprec.getreports("pytest_runtest_logreport")
+ assert len(reports) == 17 # with setup/teardown "passed" reports
+ for rep in reports:
+ d = rep._to_json()
+ newrep = TestReport._from_json(d)
+ assert newrep.passed == rep.passed
+ assert newrep.failed == rep.failed
+ assert newrep.skipped == rep.skipped
+ if newrep.skipped and not hasattr(newrep, "wasxfail"):
+ assert isinstance(newrep.longrepr, tuple)
+ assert len(newrep.longrepr) == 3
+ assert newrep.outcome == rep.outcome
+ assert newrep.when == rep.when
+ assert newrep.keywords == rep.keywords
+ if rep.failed:
+ assert newrep.longreprtext == rep.longreprtext
+
+ def test_collectreport_passed(self, pytester: Pytester) -> None:
+ """This test came originally from test_remote.py in xdist (ca03269)."""
+ reprec = pytester.inline_runsource("def test_func(): pass")
+ reports = reprec.getreports("pytest_collectreport")
+ for rep in reports:
+ d = rep._to_json()
+ newrep = CollectReport._from_json(d)
+ assert newrep.passed == rep.passed
+ assert newrep.failed == rep.failed
+ assert newrep.skipped == rep.skipped
+
+ def test_collectreport_fail(self, pytester: Pytester) -> None:
+ """This test came originally from test_remote.py in xdist (ca03269)."""
+ reprec = pytester.inline_runsource("qwe abc")
+ reports = reprec.getreports("pytest_collectreport")
+ assert reports
+ for rep in reports:
+ d = rep._to_json()
+ newrep = CollectReport._from_json(d)
+ assert newrep.passed == rep.passed
+ assert newrep.failed == rep.failed
+ assert newrep.skipped == rep.skipped
+ if rep.failed:
+ assert newrep.longrepr == str(rep.longrepr)
+
+ def test_extended_report_deserialization(self, pytester: Pytester) -> None:
+ """This test came originally from test_remote.py in xdist (ca03269)."""
+ reprec = pytester.inline_runsource("qwe abc")
+ reports = reprec.getreports("pytest_collectreport")
+ assert reports
+ for rep in reports:
+ rep.extra = True # type: ignore[attr-defined]
+ d = rep._to_json()
+ newrep = CollectReport._from_json(d)
+ assert newrep.extra
+ assert newrep.passed == rep.passed
+ assert newrep.failed == rep.failed
+ assert newrep.skipped == rep.skipped
+ if rep.failed:
+ assert newrep.longrepr == str(rep.longrepr)
+
+ def test_paths_support(self, pytester: Pytester) -> None:
+ """Report attributes which are path-like should become strings."""
+ pytester.makepyfile(
+ """
+ def test_a():
+ assert False
+ """
+ )
+
+ class MyPathLike:
+ def __init__(self, path: str) -> None:
+ self.path = path
+
+ def __fspath__(self) -> str:
+ return self.path
+
+ reprec = pytester.inline_run()
+ reports = reprec.getreports("pytest_runtest_logreport")
+ assert len(reports) == 3
+ test_a_call = reports[1]
+ test_a_call.path1 = MyPathLike(str(pytester.path)) # type: ignore[attr-defined]
+ test_a_call.path2 = pytester.path # type: ignore[attr-defined]
+ data = test_a_call._to_json()
+ assert data["path1"] == str(pytester.path)
+ assert data["path2"] == str(pytester.path)
+
+ def test_deserialization_failure(self, pytester: Pytester) -> None:
+ """Check handling of failure during deserialization of report types."""
+ pytester.makepyfile(
+ """
+ def test_a():
+ assert False
+ """
+ )
+ reprec = pytester.inline_run()
+ reports = reprec.getreports("pytest_runtest_logreport")
+ assert len(reports) == 3
+ test_a_call = reports[1]
+ data = test_a_call._to_json()
+ entry = data["longrepr"]["reprtraceback"]["reprentries"][0]
+ assert entry["type"] == "ReprEntry"
+
+ entry["type"] = "Unknown"
+ with pytest.raises(
+ RuntimeError, match="INTERNALERROR: Unknown entry type returned: Unknown"
+ ):
+ TestReport._from_json(data)
+
+ @pytest.mark.parametrize("report_class", [TestReport, CollectReport])
+ def test_chained_exceptions(
+ self, pytester: Pytester, tw_mock, report_class
+ ) -> None:
+ """Check serialization/deserialization of report objects containing chained exceptions (#5786)"""
+ pytester.makepyfile(
+ """
+ def foo():
+ raise ValueError('value error')
+ def test_a():
+ try:
+ foo()
+ except ValueError as e:
+ raise RuntimeError('runtime error') from e
+ if {error_during_import}:
+ 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")
+ # 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
+ reports = reprec.getreports("pytest_collectreport")
+ assert len(reports) == 2
+ report = reports[1]
+
+ def check_longrepr(longrepr: ExceptionChainRepr) -> None:
+ """Check the attributes of the given longrepr object according to the test file.
+
+ We can get away with testing both CollectReport and TestReport with this function because
+ the longrepr objects are very similar.
+ """
+ assert isinstance(longrepr, ExceptionChainRepr)
+ assert longrepr.sections == [("title", "contents", "=")]
+ assert len(longrepr.chain) == 2
+ entry1, entry2 = longrepr.chain
+ tb1, fileloc1, desc1 = entry1
+ tb2, fileloc2, desc2 = entry2
+
+ assert "ValueError('value error')" in str(tb1)
+ assert "RuntimeError('runtime error')" in str(tb2)
+
+ assert (
+ desc1
+ == "The above exception was the direct cause of the following exception:"
+ )
+ assert desc2 is None
+
+ assert report.failed
+ assert len(report.sections) == 0
+ assert isinstance(report.longrepr, ExceptionChainRepr)
+ report.longrepr.addsection("title", "contents", "=")
+ check_longrepr(report.longrepr)
+
+ data = report._to_json()
+ loaded_report = report_class._from_json(data)
+
+ assert loaded_report.failed
+ check_longrepr(loaded_report.longrepr)
+
+ # make sure we don't blow up on ``toterminal`` call; we don't test the actual output because it is very
+ # brittle and hard to maintain, but we can assume it is correct because ``toterminal`` is already tested
+ # elsewhere and we do check the contents of the longrepr object after loading it.
+ loaded_report.longrepr.toterminal(tw_mock)
+
+ def test_chained_exceptions_no_reprcrash(self, pytester: Pytester, tw_mock) -> None:
+ """Regression test for tracebacks without a reprcrash (#5971)
+
+ This happens notably on exceptions raised by multiprocess.pool: the exception transfer
+ from subprocess to main process creates an artificial exception, which ExceptionInfo
+ can't obtain the ReprFileLocation from.
+ """
+ pytester.makepyfile(
+ """
+ from concurrent.futures import ProcessPoolExecutor
+
+ def func():
+ raise ValueError('value error')
+
+ def test_a():
+ with ProcessPoolExecutor() as p:
+ p.submit(func).result()
+ """
+ )
+
+ pytester.syspathinsert()
+ reprec = pytester.inline_run()
+
+ reports = reprec.getreports("pytest_runtest_logreport")
+
+ def check_longrepr(longrepr: object) -> None:
+ assert isinstance(longrepr, ExceptionChainRepr)
+ assert len(longrepr.chain) == 2
+ entry1, entry2 = longrepr.chain
+ tb1, fileloc1, desc1 = entry1
+ tb2, fileloc2, desc2 = entry2
+
+ assert "RemoteTraceback" in str(tb1)
+ assert "ValueError: value error" in str(tb2)
+
+ assert fileloc1 is None
+ assert fileloc2 is not None
+ assert fileloc2.message == "ValueError: value error"
+
+ # 3 reports: setup/call/teardown: get the call report
+ assert len(reports) == 3
+ report = reports[1]
+
+ assert report.failed
+ check_longrepr(report.longrepr)
+
+ data = report._to_json()
+ loaded_report = TestReport._from_json(data)
+
+ assert loaded_report.failed
+ check_longrepr(loaded_report.longrepr)
+
+ # for same reasons as previous test, ensure we don't blow up here
+ assert loaded_report.longrepr is not None
+ assert isinstance(loaded_report.longrepr, ExceptionChainRepr)
+ loaded_report.longrepr.toterminal(tw_mock)
+
+ def test_report_prevent_ConftestImportFailure_hiding_exception(
+ self, pytester: Pytester
+ ) -> None:
+ sub_dir = pytester.path.joinpath("ns")
+ sub_dir.mkdir()
+ sub_dir.joinpath("conftest.py").write_text("import unknown")
+
+ result = pytester.runpytest_subprocess(".")
+ result.stdout.fnmatch_lines(["E *Error: No module named 'unknown'"])
+ result.stdout.no_fnmatch_line("ERROR - *ConftestImportFailure*")
+
+
+class TestHooks:
+ """Test that the hooks are working correctly for plugins"""
+
+ def test_test_report(self, pytester: Pytester, pytestconfig: Config) -> None:
+ pytester.makepyfile(
+ """
+ def test_a(): assert False
+ def test_b(): pass
+ """
+ )
+ reprec = pytester.inline_run()
+ reports = reprec.getreports("pytest_runtest_logreport")
+ assert len(reports) == 6
+ for rep in reports:
+ data = pytestconfig.hook.pytest_report_to_serializable(
+ config=pytestconfig, report=rep
+ )
+ assert data["$report_type"] == "TestReport"
+ new_rep = pytestconfig.hook.pytest_report_from_serializable(
+ config=pytestconfig, data=data
+ )
+ assert new_rep.nodeid == rep.nodeid
+ assert new_rep.when == rep.when
+ assert new_rep.outcome == rep.outcome
+
+ def test_collect_report(self, pytester: Pytester, pytestconfig: Config) -> None:
+ pytester.makepyfile(
+ """
+ def test_a(): assert False
+ def test_b(): pass
+ """
+ )
+ reprec = pytester.inline_run()
+ reports = reprec.getreports("pytest_collectreport")
+ assert len(reports) == 2
+ for rep in reports:
+ data = pytestconfig.hook.pytest_report_to_serializable(
+ config=pytestconfig, report=rep
+ )
+ assert data["$report_type"] == "CollectReport"
+ new_rep = pytestconfig.hook.pytest_report_from_serializable(
+ config=pytestconfig, data=data
+ )
+ assert new_rep.nodeid == rep.nodeid
+ assert new_rep.when == "collect"
+ assert new_rep.outcome == rep.outcome
+
+ @pytest.mark.parametrize(
+ "hook_name", ["pytest_runtest_logreport", "pytest_collectreport"]
+ )
+ def test_invalid_report_types(
+ self, pytester: Pytester, pytestconfig: Config, hook_name: str
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_a(): pass
+ """
+ )
+ reprec = pytester.inline_run()
+ reports = reprec.getreports(hook_name)
+ assert reports
+ rep = reports[0]
+ data = pytestconfig.hook.pytest_report_to_serializable(
+ config=pytestconfig, report=rep
+ )
+ data["$report_type"] = "Unknown"
+ with pytest.raises(AssertionError):
+ _ = pytestconfig.hook.pytest_report_from_serializable(
+ config=pytestconfig, data=data
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_runner.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_runner.py
new file mode 100644
index 0000000000..2e2c462d97
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_runner.py
@@ -0,0 +1,1061 @@
+import inspect
+import os
+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 pytest
+from _pytest import outcomes
+from _pytest import reports
+from _pytest import runner
+from _pytest._code import ExceptionInfo
+from _pytest._code.code import ExceptionChainRepr
+from _pytest.config import ExitCode
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.outcomes import OutcomeException
+from _pytest.pytester import Pytester
+
+
+class TestSetupState:
+ def test_setup(self, pytester: Pytester) -> None:
+ item = pytester.getitem("def test_func(): pass")
+ ss = item.session._setupstate
+ values = [1]
+ ss.setup(item)
+ ss.addfinalizer(values.pop, item)
+ assert values
+ ss.teardown_exact(None)
+ assert not values
+
+ def test_teardown_exact_stack_empty(self, pytester: Pytester) -> None:
+ item = pytester.getitem("def test_func(): pass")
+ ss = item.session._setupstate
+ ss.setup(item)
+ ss.teardown_exact(None)
+ ss.teardown_exact(None)
+ ss.teardown_exact(None)
+
+ def test_setup_fails_and_failure_is_cached(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ def setup_module(mod):
+ raise ValueError(42)
+ def test_func(): pass
+ """
+ )
+ ss = item.session._setupstate
+ with pytest.raises(ValueError):
+ ss.setup(item)
+ with pytest.raises(ValueError):
+ ss.setup(item)
+
+ def test_teardown_multiple_one_fails(self, pytester: Pytester) -> None:
+ r = []
+
+ def fin1():
+ r.append("fin1")
+
+ def fin2():
+ raise Exception("oops")
+
+ def fin3():
+ r.append("fin3")
+
+ item = pytester.getitem("def test_func(): pass")
+ ss = item.session._setupstate
+ ss.setup(item)
+ ss.addfinalizer(fin1, item)
+ ss.addfinalizer(fin2, item)
+ ss.addfinalizer(fin3, item)
+ with pytest.raises(Exception) as err:
+ ss.teardown_exact(None)
+ assert err.value.args == ("oops",)
+ 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")
+
+ def fin2():
+ raise Exception("oops2")
+
+ item = pytester.getitem("def test_func(): pass")
+ ss = item.session._setupstate
+ ss.setup(item)
+ ss.addfinalizer(fin1, item)
+ ss.addfinalizer(fin2, item)
+ with pytest.raises(Exception) as err:
+ ss.teardown_exact(None)
+ assert err.value.args == ("oops2",)
+
+ def test_teardown_multiple_scopes_one_fails(self, pytester: Pytester) -> None:
+ module_teardown = []
+
+ def fin_func():
+ raise Exception("oops1")
+
+ def fin_module():
+ module_teardown.append("fin_module")
+
+ item = pytester.getitem("def test_func(): pass")
+ mod = item.listchain()[-2]
+ ss = item.session._setupstate
+ ss.setup(item)
+ ss.addfinalizer(fin_module, mod)
+ ss.addfinalizer(fin_func, item)
+ with pytest.raises(Exception, match="oops1"):
+ ss.teardown_exact(None)
+ assert module_teardown == ["fin_module"]
+
+
+class BaseFunctionalTests:
+ def test_passfunction(self, pytester: Pytester) -> None:
+ reports = pytester.runitem(
+ """
+ def test_func():
+ pass
+ """
+ )
+ rep = reports[1]
+ assert rep.passed
+ assert not rep.failed
+ assert rep.outcome == "passed"
+ assert not rep.longrepr
+
+ def test_failfunction(self, pytester: Pytester) -> None:
+ reports = pytester.runitem(
+ """
+ def test_func():
+ assert 0
+ """
+ )
+ rep = reports[1]
+ assert not rep.passed
+ assert not rep.skipped
+ assert rep.failed
+ assert rep.when == "call"
+ assert rep.outcome == "failed"
+ # assert isinstance(rep.longrepr, ReprExceptionInfo)
+
+ def test_skipfunction(self, pytester: Pytester) -> None:
+ reports = pytester.runitem(
+ """
+ import pytest
+ def test_func():
+ pytest.skip("hello")
+ """
+ )
+ rep = reports[1]
+ assert not rep.failed
+ assert not rep.passed
+ assert rep.skipped
+ assert rep.outcome == "skipped"
+ # assert rep.skipped.when == "call"
+ # assert rep.skipped.when == "call"
+ # assert rep.skipped == "%sreason == "hello"
+ # assert rep.skipped.location.lineno == 3
+ # assert rep.skipped.location.path
+ # assert not rep.skipped.failurerepr
+
+ def test_skip_in_setup_function(self, pytester: Pytester) -> None:
+ reports = pytester.runitem(
+ """
+ import pytest
+ def setup_function(func):
+ pytest.skip("hello")
+ def test_func():
+ pass
+ """
+ )
+ print(reports)
+ rep = reports[0]
+ assert not rep.failed
+ assert not rep.passed
+ assert rep.skipped
+ # assert rep.skipped.reason == "hello"
+ # assert rep.skipped.location.lineno == 3
+ # assert rep.skipped.location.lineno == 3
+ assert len(reports) == 2
+ assert reports[1].passed # teardown
+
+ def test_failure_in_setup_function(self, pytester: Pytester) -> None:
+ reports = pytester.runitem(
+ """
+ import pytest
+ def setup_function(func):
+ raise ValueError(42)
+ def test_func():
+ pass
+ """
+ )
+ rep = reports[0]
+ assert not rep.skipped
+ assert not rep.passed
+ assert rep.failed
+ assert rep.when == "setup"
+ assert len(reports) == 2
+
+ def test_failure_in_teardown_function(self, pytester: Pytester) -> None:
+ reports = pytester.runitem(
+ """
+ import pytest
+ def teardown_function(func):
+ raise ValueError(42)
+ def test_func():
+ pass
+ """
+ )
+ print(reports)
+ assert len(reports) == 3
+ rep = reports[2]
+ assert not rep.skipped
+ assert not rep.passed
+ assert rep.failed
+ assert rep.when == "teardown"
+ # assert rep.longrepr.reprcrash.lineno == 3
+ # assert rep.longrepr.reprtraceback.reprentries
+
+ def test_custom_failure_repr(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ conftest="""
+ import pytest
+ class Function(pytest.Function):
+ def repr_failure(self, excinfo):
+ return "hello"
+ """
+ )
+ reports = pytester.runitem(
+ """
+ import pytest
+ def test_func():
+ assert 0
+ """
+ )
+ rep = reports[1]
+ assert not rep.skipped
+ assert not rep.passed
+ assert rep.failed
+ # assert rep.outcome.when == "call"
+ # assert rep.failed.where.lineno == 3
+ # assert rep.failed.where.path.basename == "test_func.py"
+ # assert rep.failed.failurerepr == "hello"
+
+ def test_teardown_final_returncode(self, pytester: Pytester) -> None:
+ rec = pytester.inline_runsource(
+ """
+ def test_func():
+ pass
+ def teardown_function(func):
+ raise ValueError(42)
+ """
+ )
+ assert rec.ret == 1
+
+ def test_logstart_logfinish_hooks(self, pytester: Pytester) -> None:
+ rec = pytester.inline_runsource(
+ """
+ import pytest
+ def test_func():
+ pass
+ """
+ )
+ reps = rec.getcalls("pytest_runtest_logstart pytest_runtest_logfinish")
+ assert [x._name for x in reps] == [
+ "pytest_runtest_logstart",
+ "pytest_runtest_logfinish",
+ ]
+ for rep in reps:
+ assert rep.nodeid == "test_logstart_logfinish_hooks.py::test_func"
+ assert rep.location == ("test_logstart_logfinish_hooks.py", 1, "test_func")
+
+ def test_exact_teardown_issue90(self, pytester: Pytester) -> None:
+ rec = pytester.inline_runsource(
+ """
+ import pytest
+
+ class TestClass(object):
+ def test_method(self):
+ pass
+ def teardown_class(cls):
+ raise Exception()
+
+ def test_func():
+ import sys
+ # on python2 exc_info is kept till a function exits
+ # so we would end up calling test functions while
+ # sys.exc_info would return the indexerror
+ # from guessing the lastitem
+ excinfo = sys.exc_info()
+ import traceback
+ assert excinfo[0] is None, \
+ traceback.format_exception(*excinfo)
+ def teardown_function(func):
+ raise ValueError(42)
+ """
+ )
+ reps = rec.getreports("pytest_runtest_logreport")
+ print(reps)
+ for i in range(2):
+ assert reps[i].nodeid.endswith("test_method")
+ assert reps[i].passed
+ assert reps[2].when == "teardown"
+ assert reps[2].failed
+ assert len(reps) == 6
+ for i in range(3, 5):
+ assert reps[i].nodeid.endswith("test_func")
+ assert reps[i].passed
+ assert reps[5].when == "teardown"
+ assert reps[5].nodeid.endswith("test_func")
+ assert reps[5].failed
+
+ def test_exact_teardown_issue1206(self, pytester: Pytester) -> None:
+ """Issue shadowing error with wrong number of arguments on teardown_method."""
+ rec = pytester.inline_runsource(
+ """
+ import pytest
+
+ class TestClass(object):
+ def teardown_method(self, x, y, z):
+ pass
+
+ def test_method(self):
+ assert True
+ """
+ )
+ reps = rec.getreports("pytest_runtest_logreport")
+ print(reps)
+ assert len(reps) == 3
+ #
+ assert reps[0].nodeid.endswith("test_method")
+ assert reps[0].passed
+ assert reps[0].when == "setup"
+ #
+ assert reps[1].nodeid.endswith("test_method")
+ assert reps[1].passed
+ assert reps[1].when == "call"
+ #
+ assert reps[2].nodeid.endswith("test_method")
+ assert reps[2].failed
+ assert reps[2].when == "teardown"
+ longrepr = reps[2].longrepr
+ assert isinstance(longrepr, ExceptionChainRepr)
+ assert longrepr.reprcrash
+ assert longrepr.reprcrash.message in (
+ "TypeError: teardown_method() missing 2 required positional arguments: 'y' and 'z'",
+ # Python >= 3.10
+ "TypeError: TestClass.teardown_method() missing 2 required positional arguments: 'y' and 'z'",
+ )
+
+ def test_failure_in_setup_function_ignores_custom_repr(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ conftest="""
+ import pytest
+ class Function(pytest.Function):
+ def repr_failure(self, excinfo):
+ assert 0
+ """
+ )
+ reports = pytester.runitem(
+ """
+ def setup_function(func):
+ raise ValueError(42)
+ def test_func():
+ pass
+ """
+ )
+ assert len(reports) == 2
+ rep = reports[0]
+ print(rep)
+ assert not rep.skipped
+ assert not rep.passed
+ assert rep.failed
+ # 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)
+
+ def test_systemexit_does_not_bail_out(self, pytester: Pytester) -> None:
+ try:
+ reports = pytester.runitem(
+ """
+ def test_func():
+ raise SystemExit(42)
+ """
+ )
+ except SystemExit:
+ assert False, "runner did not catch SystemExit"
+ rep = reports[1]
+ assert rep.failed
+ assert rep.when == "call"
+
+ def test_exit_propagates(self, pytester: Pytester) -> None:
+ try:
+ pytester.runitem(
+ """
+ import pytest
+ def test_func():
+ raise pytest.exit.Exception()
+ """
+ )
+ except pytest.exit.Exception:
+ pass
+ else:
+ assert False, "did not raise"
+
+
+class TestExecutionNonForked(BaseFunctionalTests):
+ def getrunner(self):
+ def f(item):
+ return runner.runtestprotocol(item, log=False)
+
+ return f
+
+ def test_keyboardinterrupt_propagates(self, pytester: Pytester) -> None:
+ try:
+ pytester.runitem(
+ """
+ def test_func():
+ raise KeyboardInterrupt("fake")
+ """
+ )
+ except KeyboardInterrupt:
+ pass
+ else:
+ assert False, "did not raise"
+
+
+class TestSessionReports:
+ def test_collect_result(self, pytester: Pytester) -> None:
+ col = pytester.getmodulecol(
+ """
+ def test_func1():
+ pass
+ class TestClass(object):
+ pass
+ """
+ )
+ rep = runner.collect_one_node(col)
+ assert not rep.failed
+ assert not rep.skipped
+ assert rep.passed
+ locinfo = rep.location
+ assert locinfo[0] == col.path.name
+ assert not locinfo[1]
+ assert locinfo[2] == col.path.name
+ res = rep.result
+ assert len(res) == 2
+ assert res[0].name == "test_func1"
+ assert res[1].name == "TestClass"
+
+
+reporttypes: List[Type[reports.BaseReport]] = [
+ reports.BaseReport,
+ reports.TestReport,
+ reports.CollectReport,
+]
+
+
+@pytest.mark.parametrize(
+ "reporttype", reporttypes, ids=[x.__name__ for x in reporttypes]
+)
+def test_report_extra_parameters(reporttype: Type[reports.BaseReport]) -> None:
+ args = list(inspect.signature(reporttype.__init__).parameters.keys())[1:]
+ basekw: Dict[str, List[object]] = dict.fromkeys(args, [])
+ report = reporttype(newthing=1, **basekw)
+ assert report.newthing == 1
+
+
+def test_callinfo() -> None:
+ ci = runner.CallInfo.from_call(lambda: 0, "collect")
+ assert ci.when == "collect"
+ assert ci.result == 0
+ assert "result" in repr(ci)
+ assert repr(ci) == "<CallInfo when='collect' result: 0>"
+ assert str(ci) == "<CallInfo when='collect' result: 0>"
+
+ ci2 = runner.CallInfo.from_call(lambda: 0 / 0, "collect")
+ assert ci2.when == "collect"
+ assert not hasattr(ci2, "result")
+ assert repr(ci2) == f"<CallInfo when='collect' excinfo={ci2.excinfo!r}>"
+ assert str(ci2) == repr(ci2)
+ assert ci2.excinfo
+
+ # Newlines are escaped.
+ def raise_assertion():
+ assert 0, "assert_msg"
+
+ ci3 = runner.CallInfo.from_call(raise_assertion, "call")
+ assert repr(ci3) == f"<CallInfo when='call' excinfo={ci3.excinfo!r}>"
+ assert "\n" not in repr(ci3)
+
+
+# design question: do we want general hooks in python files?
+# then something like the following functional tests makes sense
+
+
+@pytest.mark.xfail
+def test_runtest_in_module_ordering(pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ import pytest
+ def pytest_runtest_setup(item): # runs after class-level!
+ item.function.mylist.append("module")
+ class TestClass(object):
+ def pytest_runtest_setup(self, item):
+ assert not hasattr(item.function, 'mylist')
+ item.function.mylist = ['class']
+ @pytest.fixture
+ def mylist(self, request):
+ return request.function.mylist
+ @pytest.hookimpl(hookwrapper=True)
+ def pytest_runtest_call(self, item):
+ try:
+ (yield).get_result()
+ except ValueError:
+ pass
+ def test_hello1(self, mylist):
+ assert mylist == ['class', 'module'], mylist
+ raise ValueError()
+ def test_hello2(self, mylist):
+ assert mylist == ['class', 'module'], mylist
+ def pytest_runtest_teardown(item):
+ del item.function.mylist
+ """
+ )
+ result = pytester.runpytest(p1)
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+
+def test_outcomeexception_exceptionattributes() -> None:
+ outcome = outcomes.OutcomeException("test")
+ assert outcome.args[0] == outcome.msg
+
+
+def test_outcomeexception_passes_except_Exception() -> None:
+ with pytest.raises(outcomes.OutcomeException):
+ try:
+ raise outcomes.OutcomeException("test")
+ except Exception as e:
+ raise NotImplementedError from e
+
+
+def test_pytest_exit() -> None:
+ with pytest.raises(pytest.exit.Exception) as excinfo:
+ pytest.exit("hello")
+ assert excinfo.errisinstance(pytest.exit.Exception)
+
+
+def test_pytest_fail() -> None:
+ with pytest.raises(pytest.fail.Exception) as excinfo:
+ pytest.fail("hello")
+ s = excinfo.exconly(tryshort=True)
+ assert s.startswith("Failed")
+
+
+def test_pytest_exit_msg(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ def pytest_configure(config):
+ pytest.exit('oh noes')
+ """
+ )
+ result = pytester.runpytest()
+ result.stderr.fnmatch_lines(["Exit: oh noes"])
+
+
+def _strip_resource_warnings(lines):
+ # Assert no output on stderr, except for unreliable ResourceWarnings.
+ # (https://github.com/pytest-dev/pytest/issues/5088)
+ return [
+ x
+ for x in lines
+ if not x.startswith(("Exception ignored in:", "ResourceWarning"))
+ ]
+
+
+def test_pytest_exit_returncode(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """\
+ import pytest
+ def test_foo():
+ pytest.exit("some exit msg", 99)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*! *Exit: some exit msg !*"])
+
+ assert _strip_resource_warnings(result.stderr.lines) == []
+ assert result.ret == 99
+
+ # It prints to stderr also in case of exit during pytest_sessionstart.
+ pytester.makeconftest(
+ """\
+ import pytest
+
+ def pytest_sessionstart():
+ pytest.exit("during_sessionstart", 98)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*! *Exit: during_sessionstart !*"])
+ assert _strip_resource_warnings(result.stderr.lines) == [
+ "Exit: during_sessionstart"
+ ]
+ assert result.ret == 98
+
+
+def test_pytest_fail_notrace_runtest(pytester: Pytester) -> None:
+ """Test pytest.fail(..., pytrace=False) does not show tracebacks during test run."""
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_hello():
+ pytest.fail("hello", pytrace=False)
+ def teardown_function(function):
+ pytest.fail("world", pytrace=False)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["world", "hello"])
+ result.stdout.no_fnmatch_line("*def teardown_function*")
+
+
+def test_pytest_fail_notrace_collection(pytester: Pytester) -> None:
+ """Test pytest.fail(..., pytrace=False) does not show tracebacks during collection."""
+ pytester.makepyfile(
+ """
+ import pytest
+ def some_internal_function():
+ pytest.fail("hello", pytrace=False)
+ some_internal_function()
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["hello"])
+ result.stdout.no_fnmatch_line("*def some_internal_function()*")
+
+
+def test_pytest_fail_notrace_non_ascii(pytester: Pytester) -> None:
+ """Fix pytest.fail with pytrace=False with non-ascii characters (#1178).
+
+ This tests with native and unicode strings containing non-ascii chars.
+ """
+ pytester.makepyfile(
+ """\
+ import pytest
+
+ def test_hello():
+ pytest.fail('oh oh: ☺', pytrace=False)
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*test_hello*", "oh oh: ☺"])
+ result.stdout.no_fnmatch_line("*def test_hello*")
+
+
+def test_pytest_no_tests_collected_exit_status(pytester: Pytester) -> None:
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*collected 0 items*"])
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+ pytester.makepyfile(
+ test_foo="""
+ def test_foo():
+ assert 1
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*collected 1 item*"])
+ result.stdout.fnmatch_lines(["*1 passed*"])
+ assert result.ret == ExitCode.OK
+
+ result = pytester.runpytest("-k nonmatch")
+ result.stdout.fnmatch_lines(["*collected 1 item*"])
+ result.stdout.fnmatch_lines(["*1 deselected*"])
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+
+def test_exception_printing_skip() -> None:
+ assert pytest.skip.Exception == pytest.skip.Exception
+ try:
+ pytest.skip("hello")
+ except pytest.skip.Exception:
+ excinfo = ExceptionInfo.from_current()
+ s = excinfo.exconly(tryshort=True)
+ assert s.startswith("Skipped")
+
+
+def test_importorskip(monkeypatch) -> None:
+ importorskip = pytest.importorskip
+
+ def f():
+ importorskip("asdlkj")
+
+ try:
+ sysmod = importorskip("sys")
+ assert sysmod is sys
+ # path = pytest.importorskip("os.path")
+ # assert path == os.path
+ excinfo = pytest.raises(pytest.skip.Exception, f)
+ assert excinfo is not None
+ excrepr = excinfo.getrepr()
+ assert excrepr is not None
+ assert excrepr.reprcrash is not None
+ path = Path(excrepr.reprcrash.path)
+ # check that importorskip reports the actual call
+ # in this test the test_runner.py file
+ assert path.stem == "test_runner"
+ pytest.raises(SyntaxError, pytest.importorskip, "x y z")
+ pytest.raises(SyntaxError, pytest.importorskip, "x=y")
+ mod = types.ModuleType("hello123")
+ mod.__version__ = "1.3" # type: ignore
+ monkeypatch.setitem(sys.modules, "hello123", mod)
+ with pytest.raises(pytest.skip.Exception):
+ pytest.importorskip("hello123", minversion="1.3.1")
+ mod2 = pytest.importorskip("hello123", minversion="1.3")
+ assert mod2 == mod
+ except pytest.skip.Exception: # pragma: no cover
+ assert False, f"spurious skip: {ExceptionInfo.from_current()}"
+
+
+def test_importorskip_imports_last_module_part() -> None:
+ ospath = pytest.importorskip("os.path")
+ assert os.path == ospath
+
+
+def test_importorskip_dev_module(monkeypatch) -> None:
+ try:
+ mod = types.ModuleType("mockmodule")
+ mod.__version__ = "0.13.0.dev-43290" # type: ignore
+ monkeypatch.setitem(sys.modules, "mockmodule", mod)
+ mod2 = pytest.importorskip("mockmodule", minversion="0.12.0")
+ assert mod2 == mod
+ with pytest.raises(pytest.skip.Exception):
+ pytest.importorskip("mockmodule1", minversion="0.14.0")
+ except pytest.skip.Exception: # pragma: no cover
+ assert False, f"spurious skip: {ExceptionInfo.from_current()}"
+
+
+def test_importorskip_module_level(pytester: Pytester) -> None:
+ """`importorskip` must be able to skip entire modules when used at module level."""
+ pytester.makepyfile(
+ """
+ import pytest
+ foobarbaz = pytest.importorskip("foobarbaz")
+
+ def test_foo():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*collected 0 items / 1 skipped*"])
+
+
+def test_importorskip_custom_reason(pytester: Pytester) -> None:
+ """Make sure custom reasons are used."""
+ pytester.makepyfile(
+ """
+ import pytest
+ foobarbaz = pytest.importorskip("foobarbaz2", reason="just because")
+
+ def test_foo():
+ pass
+ """
+ )
+ result = pytester.runpytest("-ra")
+ result.stdout.fnmatch_lines(["*just because*"])
+ result.stdout.fnmatch_lines(["*collected 0 items / 1 skipped*"])
+
+
+def test_pytest_cmdline_main(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ def test_hello():
+ assert 1
+ if __name__ == '__main__':
+ pytest.cmdline.main([__file__])
+ """
+ )
+ import subprocess
+
+ popen = subprocess.Popen([sys.executable, str(p)], stdout=subprocess.PIPE)
+ popen.communicate()
+ ret = popen.wait()
+ assert ret == 0
+
+
+def test_unicode_in_longrepr(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """\
+ import pytest
+ @pytest.hookimpl(hookwrapper=True)
+ def pytest_runtest_makereport():
+ outcome = yield
+ rep = outcome.get_result()
+ if rep.when == "call":
+ rep.longrepr = 'ä'
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_out():
+ assert 0
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 1
+ assert "UnicodeEncodeError" not in result.stderr.str()
+
+
+def test_failure_in_setup(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def setup_module():
+ 0/0
+ def test_func():
+ pass
+ """
+ )
+ result = pytester.runpytest("--tb=line")
+ result.stdout.no_fnmatch_line("*def setup_module*")
+
+
+def test_makereport_getsource(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_foo():
+ if False: pass
+ else: assert False
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.no_fnmatch_line("*INTERNALERROR*")
+ result.stdout.fnmatch_lines(["*else: assert False*"])
+
+
+def test_makereport_getsource_dynamic_code(
+ pytester: Pytester, monkeypatch: MonkeyPatch
+) -> None:
+ """Test that exception in dynamically generated code doesn't break getting the source line."""
+ import inspect
+
+ original_findsource = inspect.findsource
+
+ def findsource(obj):
+ # Can be triggered by dynamically created functions
+ if obj.__name__ == "foo":
+ raise IndexError()
+ return original_findsource(obj)
+
+ monkeypatch.setattr(inspect, "findsource", findsource)
+
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def foo(missing):
+ pass
+
+ def test_fix(foo):
+ assert False
+ """
+ )
+ result = pytester.runpytest("-vv")
+ result.stdout.no_fnmatch_line("*INTERNALERROR*")
+ result.stdout.fnmatch_lines(["*test_fix*", "*fixture*'missing'*not found*"])
+
+
+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"
+ raise_error = True
+
+ def runtest(self):
+ if self.raise_error:
+ raise IndexError("TEST")
+
+ try:
+ runner.pytest_runtest_call(ItemMightRaise()) # type: ignore[arg-type]
+ except IndexError:
+ pass
+ # Check that exception info is stored on sys
+ assert sys.last_type is IndexError
+ assert isinstance(sys.last_value, IndexError)
+ assert sys.last_value.args[0] == "TEST"
+ assert sys.last_traceback
+
+ # The next run should clear the exception info stored by the previous run
+ ItemMightRaise.raise_error = False
+ runner.pytest_runtest_call(ItemMightRaise()) # type: ignore[arg-type]
+ assert not hasattr(sys, "last_type")
+ assert not hasattr(sys, "last_value")
+ assert not hasattr(sys, "last_traceback")
+
+
+def test_current_test_env_var(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
+ pytest_current_test_vars: List[Tuple[str, str]] = []
+ monkeypatch.setattr(
+ sys, "pytest_current_test_vars", pytest_current_test_vars, raising=False
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+ import sys
+ import os
+
+ @pytest.fixture
+ def fix():
+ sys.pytest_current_test_vars.append(('setup', os.environ['PYTEST_CURRENT_TEST']))
+ yield
+ sys.pytest_current_test_vars.append(('teardown', os.environ['PYTEST_CURRENT_TEST']))
+
+ def test(fix):
+ sys.pytest_current_test_vars.append(('call', os.environ['PYTEST_CURRENT_TEST']))
+ """
+ )
+ result = pytester.runpytest_inprocess()
+ assert result.ret == 0
+ test_id = "test_current_test_env_var.py::test"
+ assert pytest_current_test_vars == [
+ ("setup", test_id + " (setup)"),
+ ("call", test_id + " (call)"),
+ ("teardown", test_id + " (teardown)"),
+ ]
+ assert "PYTEST_CURRENT_TEST" not in os.environ
+
+
+class TestReportContents:
+ """Test user-level API of ``TestReport`` objects."""
+
+ def getrunner(self):
+ return lambda item: runner.runtestprotocol(item, log=False)
+
+ def test_longreprtext_pass(self, pytester: Pytester) -> None:
+ reports = pytester.runitem(
+ """
+ def test_func():
+ pass
+ """
+ )
+ rep = reports[1]
+ assert rep.longreprtext == ""
+
+ def test_longreprtext_skip(self, pytester: Pytester) -> None:
+ """TestReport.longreprtext can handle non-str ``longrepr`` attributes (#7559)"""
+ reports = pytester.runitem(
+ """
+ import pytest
+ def test_func():
+ pytest.skip()
+ """
+ )
+ _, call_rep, _ = reports
+ assert isinstance(call_rep.longrepr, tuple)
+ assert "Skipped" in call_rep.longreprtext
+
+ def test_longreprtext_collect_skip(self, pytester: Pytester) -> None:
+ """CollectReport.longreprtext can handle non-str ``longrepr`` attributes (#7559)"""
+ pytester.makepyfile(
+ """
+ import pytest
+ pytest.skip(allow_module_level=True)
+ """
+ )
+ rec = pytester.inline_run()
+ calls = rec.getcalls("pytest_collectreport")
+ _, call = calls
+ assert isinstance(call.report.longrepr, tuple)
+ assert "Skipped" in call.report.longreprtext
+
+ def test_longreprtext_failure(self, pytester: Pytester) -> None:
+ reports = pytester.runitem(
+ """
+ def test_func():
+ x = 1
+ assert x == 4
+ """
+ )
+ rep = reports[1]
+ assert "assert 1 == 4" in rep.longreprtext
+
+ def test_captured_text(self, pytester: Pytester) -> None:
+ reports = pytester.runitem(
+ """
+ import pytest
+ import sys
+
+ @pytest.fixture
+ def fix():
+ sys.stdout.write('setup: stdout\\n')
+ sys.stderr.write('setup: stderr\\n')
+ yield
+ sys.stdout.write('teardown: stdout\\n')
+ sys.stderr.write('teardown: stderr\\n')
+ assert 0
+
+ def test_func(fix):
+ sys.stdout.write('call: stdout\\n')
+ sys.stderr.write('call: stderr\\n')
+ assert 0
+ """
+ )
+ setup, call, teardown = reports
+ assert setup.capstdout == "setup: stdout\n"
+ assert call.capstdout == "setup: stdout\ncall: stdout\n"
+ assert teardown.capstdout == "setup: stdout\ncall: stdout\nteardown: stdout\n"
+
+ assert setup.capstderr == "setup: stderr\n"
+ assert call.capstderr == "setup: stderr\ncall: stderr\n"
+ assert teardown.capstderr == "setup: stderr\ncall: stderr\nteardown: stderr\n"
+
+ def test_no_captured_text(self, pytester: Pytester) -> None:
+ reports = pytester.runitem(
+ """
+ def test_func():
+ pass
+ """
+ )
+ rep = reports[1]
+ assert rep.capstdout == ""
+ assert rep.capstderr == ""
+
+ def test_longrepr_type(self, pytester: Pytester) -> None:
+ reports = pytester.runitem(
+ """
+ import pytest
+ def test_func():
+ pytest.fail(pytrace=False)
+ """
+ )
+ rep = reports[1]
+ assert isinstance(rep.longrepr, ExceptionChainRepr)
+
+
+def test_outcome_exception_bad_msg() -> None:
+ """Check that OutcomeExceptions validate their input to prevent confusing errors (#5578)"""
+
+ def func() -> None:
+ raise NotImplementedError()
+
+ expected = (
+ "OutcomeException expected string as 'msg' parameter, got 'function' instead.\n"
+ "Perhaps you meant to use a mark?"
+ )
+ with pytest.raises(TypeError) as excinfo:
+ OutcomeException(func) # type: ignore
+ assert str(excinfo.value) == expected
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_runner_xunit.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_runner_xunit.py
new file mode 100644
index 0000000000..e077ac41e2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_runner_xunit.py
@@ -0,0 +1,297 @@
+"""Test correct setup/teardowns at module, class, and instance level."""
+from typing import List
+
+import pytest
+from _pytest.pytester import Pytester
+
+
+def test_module_and_function_setup(pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ modlevel = []
+ def setup_module(module):
+ assert not modlevel
+ module.modlevel.append(42)
+
+ def teardown_module(module):
+ modlevel.pop()
+
+ def setup_function(function):
+ function.answer = 17
+
+ def teardown_function(function):
+ del function.answer
+
+ def test_modlevel():
+ assert modlevel[0] == 42
+ assert test_modlevel.answer == 17
+
+ class TestFromClass(object):
+ def test_module(self):
+ assert modlevel[0] == 42
+ assert not hasattr(test_modlevel, 'answer')
+ """
+ )
+ rep = reprec.matchreport("test_modlevel")
+ assert rep.passed
+ rep = reprec.matchreport("test_module")
+ assert rep.passed
+
+
+def test_module_setup_failure_no_teardown(pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ values = []
+ def setup_module(module):
+ values.append(1)
+ 0/0
+
+ def test_nothing():
+ pass
+
+ def teardown_module(module):
+ values.append(2)
+ """
+ )
+ reprec.assertoutcome(failed=1)
+ calls = reprec.getcalls("pytest_runtest_setup")
+ assert calls[0].item.module.values == [1]
+
+
+def test_setup_function_failure_no_teardown(pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ modlevel = []
+ def setup_function(function):
+ modlevel.append(1)
+ 0/0
+
+ def teardown_function(module):
+ modlevel.append(2)
+
+ def test_func():
+ pass
+ """
+ )
+ calls = reprec.getcalls("pytest_runtest_setup")
+ assert calls[0].item.module.modlevel == [1]
+
+
+def test_class_setup(pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ class TestSimpleClassSetup(object):
+ clslevel = []
+ def setup_class(cls):
+ cls.clslevel.append(23)
+
+ def teardown_class(cls):
+ cls.clslevel.pop()
+
+ def test_classlevel(self):
+ assert self.clslevel[0] == 23
+
+ class TestInheritedClassSetupStillWorks(TestSimpleClassSetup):
+ def test_classlevel_anothertime(self):
+ assert self.clslevel == [23]
+
+ def test_cleanup():
+ assert not TestSimpleClassSetup.clslevel
+ assert not TestInheritedClassSetupStillWorks.clslevel
+ """
+ )
+ reprec.assertoutcome(passed=1 + 2 + 1)
+
+
+def test_class_setup_failure_no_teardown(pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ class TestSimpleClassSetup(object):
+ clslevel = []
+ def setup_class(cls):
+ 0/0
+
+ def teardown_class(cls):
+ cls.clslevel.append(1)
+
+ def test_classlevel(self):
+ pass
+
+ def test_cleanup():
+ assert not TestSimpleClassSetup.clslevel
+ """
+ )
+ reprec.assertoutcome(failed=1, passed=1)
+
+
+def test_method_setup(pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ class TestSetupMethod(object):
+ def setup_method(self, meth):
+ self.methsetup = meth
+ def teardown_method(self, meth):
+ del self.methsetup
+
+ def test_some(self):
+ assert self.methsetup == self.test_some
+
+ def test_other(self):
+ assert self.methsetup == self.test_other
+ """
+ )
+ reprec.assertoutcome(passed=2)
+
+
+def test_method_setup_failure_no_teardown(pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ class TestMethodSetup(object):
+ clslevel = []
+ def setup_method(self, method):
+ self.clslevel.append(1)
+ 0/0
+
+ def teardown_method(self, method):
+ self.clslevel.append(2)
+
+ def test_method(self):
+ pass
+
+ def test_cleanup():
+ assert TestMethodSetup.clslevel == [1]
+ """
+ )
+ reprec.assertoutcome(failed=1, passed=1)
+
+
+def test_method_setup_uses_fresh_instances(pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ class TestSelfState1(object):
+ memory = []
+ def test_hello(self):
+ self.memory.append(self)
+
+ def test_afterhello(self):
+ assert self != self.memory[0]
+ """
+ )
+ reprec.assertoutcome(passed=2, failed=0)
+
+
+def test_setup_that_skips_calledagain(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ def setup_module(mod):
+ pytest.skip("x")
+ def test_function1():
+ pass
+ def test_function2():
+ pass
+ """
+ )
+ reprec = pytester.inline_run(p)
+ reprec.assertoutcome(skipped=2)
+
+
+def test_setup_fails_again_on_all_tests(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ def setup_module(mod):
+ raise ValueError(42)
+ def test_function1():
+ pass
+ def test_function2():
+ pass
+ """
+ )
+ reprec = pytester.inline_run(p)
+ reprec.assertoutcome(failed=2)
+
+
+def test_setup_funcarg_setup_when_outer_scope_fails(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ def setup_module(mod):
+ raise ValueError(42)
+ @pytest.fixture
+ def hello(request):
+ raise ValueError("xyz43")
+ def test_function1(hello):
+ pass
+ def test_function2(hello):
+ pass
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(
+ [
+ "*function1*",
+ "*ValueError*42*",
+ "*function2*",
+ "*ValueError*42*",
+ "*2 errors*",
+ ]
+ )
+ result.stdout.no_fnmatch_line("*xyz43*")
+
+
+@pytest.mark.parametrize("arg", ["", "arg"])
+def test_setup_teardown_function_level_with_optional_argument(
+ pytester: Pytester,
+ monkeypatch,
+ arg: str,
+) -> None:
+ """Parameter to setup/teardown xunit-style functions parameter is now optional (#1728)."""
+ import sys
+
+ trace_setups_teardowns: List[str] = []
+ monkeypatch.setattr(
+ sys, "trace_setups_teardowns", trace_setups_teardowns, raising=False
+ )
+ p = pytester.makepyfile(
+ """
+ import pytest
+ import sys
+
+ trace = sys.trace_setups_teardowns.append
+
+ def setup_module({arg}): trace('setup_module')
+ def teardown_module({arg}): trace('teardown_module')
+
+ def setup_function({arg}): trace('setup_function')
+ def teardown_function({arg}): trace('teardown_function')
+
+ def test_function_1(): pass
+ def test_function_2(): pass
+
+ class Test(object):
+ def setup_method(self, {arg}): trace('setup_method')
+ def teardown_method(self, {arg}): trace('teardown_method')
+
+ def test_method_1(self): pass
+ def test_method_2(self): pass
+ """.format(
+ arg=arg
+ )
+ )
+ result = pytester.inline_run(p)
+ result.assertoutcome(passed=4)
+
+ expected = [
+ "setup_module",
+ "setup_function",
+ "teardown_function",
+ "setup_function",
+ "teardown_function",
+ "setup_method",
+ "teardown_method",
+ "setup_method",
+ "teardown_method",
+ "teardown_module",
+ ]
+ assert trace_setups_teardowns == expected
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_scope.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_scope.py
new file mode 100644
index 0000000000..09ee1343a8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_scope.py
@@ -0,0 +1,39 @@
+import re
+
+import pytest
+from _pytest.scope import Scope
+
+
+def test_ordering() -> None:
+ assert Scope.Session > Scope.Package
+ assert Scope.Package > Scope.Module
+ assert Scope.Module > Scope.Class
+ assert Scope.Class > Scope.Function
+
+
+def test_next_lower() -> None:
+ assert Scope.Session.next_lower() is Scope.Package
+ assert Scope.Package.next_lower() is Scope.Module
+ assert Scope.Module.next_lower() is Scope.Class
+ assert Scope.Class.next_lower() is Scope.Function
+
+ with pytest.raises(ValueError, match="Function is the lower-most scope"):
+ Scope.Function.next_lower()
+
+
+def test_next_higher() -> None:
+ assert Scope.Function.next_higher() is Scope.Class
+ assert Scope.Class.next_higher() is Scope.Module
+ assert Scope.Module.next_higher() is Scope.Package
+ assert Scope.Package.next_higher() is Scope.Session
+
+ with pytest.raises(ValueError, match="Session is the upper-most scope"):
+ Scope.Session.next_higher()
+
+
+def test_from_user() -> None:
+ assert Scope.from_user("module", "for parametrize", "some::id") is Scope.Module
+
+ expected_msg = "for parametrize from some::id got an unexpected scope value 'foo'"
+ with pytest.raises(pytest.fail.Exception, match=re.escape(expected_msg)):
+ Scope.from_user("foo", "for parametrize", "some::id") # type:ignore[arg-type]
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_session.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_session.py
new file mode 100644
index 0000000000..3ca6d39038
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_session.py
@@ -0,0 +1,369 @@
+import pytest
+from _pytest.config import ExitCode
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pytester import Pytester
+
+
+class SessionTests:
+ def test_basic_testitem_events(self, pytester: Pytester) -> None:
+ tfile = pytester.makepyfile(
+ """
+ def test_one():
+ pass
+ def test_one_one():
+ assert 0
+ def test_other():
+ raise ValueError(23)
+ class TestClass(object):
+ def test_two(self, someargs):
+ pass
+ """
+ )
+ reprec = pytester.inline_run(tfile)
+ passed, skipped, failed = reprec.listoutcomes()
+ assert len(skipped) == 0
+ assert len(passed) == 1
+ assert len(failed) == 3
+
+ def end(x):
+ return x.nodeid.split("::")[-1]
+
+ assert end(failed[0]) == "test_one_one"
+ assert end(failed[1]) == "test_other"
+ itemstarted = reprec.getcalls("pytest_itemcollected")
+ assert len(itemstarted) == 4
+ # XXX check for failing funcarg setup
+ # colreports = reprec.getcalls("pytest_collectreport")
+ # assert len(colreports) == 4
+ # assert colreports[1].report.failed
+
+ def test_nested_import_error(self, pytester: Pytester) -> None:
+ tfile = pytester.makepyfile(
+ """
+ import import_fails
+ def test_this():
+ assert import_fails.a == 1
+ """,
+ import_fails="""
+ import does_not_work
+ a = 1
+ """,
+ )
+ reprec = pytester.inline_run(tfile)
+ values = reprec.getfailedcollections()
+ assert len(values) == 1
+ out = str(values[0].longrepr)
+ assert out.find("does_not_work") != -1
+
+ def test_raises_output(self, pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ import pytest
+ def test_raises_doesnt():
+ pytest.raises(ValueError, int, "3")
+ """
+ )
+ passed, skipped, failed = reprec.listoutcomes()
+ assert len(failed) == 1
+ out = failed[0].longrepr.reprcrash.message # type: ignore[union-attr]
+ assert "DID NOT RAISE" in out
+
+ def test_syntax_error_module(self, pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource("this is really not python")
+ values = reprec.getfailedcollections()
+ assert len(values) == 1
+ out = str(values[0].longrepr)
+ assert out.find("not python") != -1
+
+ def test_exit_first_problem(self, pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ def test_one(): assert 0
+ def test_two(): assert 0
+ """,
+ "--exitfirst",
+ )
+ passed, skipped, failed = reprec.countoutcomes()
+ assert failed == 1
+ assert passed == skipped == 0
+
+ def test_maxfail(self, pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ def test_one(): assert 0
+ def test_two(): assert 0
+ def test_three(): assert 0
+ """,
+ "--maxfail=2",
+ )
+ passed, skipped, failed = reprec.countoutcomes()
+ assert failed == 2
+ assert passed == skipped == 0
+
+ def test_broken_repr(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ class reprexc(BaseException):
+ def __str__(self):
+ return "Ha Ha fooled you, I'm a broken repr()."
+
+ class BrokenRepr1(object):
+ foo=0
+ def __repr__(self):
+ raise reprexc
+
+ class TestBrokenClass(object):
+ def test_explicit_bad_repr(self):
+ t = BrokenRepr1()
+ with pytest.raises(BaseException, match="broken repr"):
+ repr(t)
+
+ def test_implicit_bad_repr1(self):
+ t = BrokenRepr1()
+ assert t.foo == 1
+
+ """
+ )
+ reprec = pytester.inline_run(p)
+ passed, skipped, failed = reprec.listoutcomes()
+ assert (len(passed), len(skipped), len(failed)) == (1, 0, 1)
+ out = failed[0].longrepr.reprcrash.message # type: ignore[union-attr]
+ assert out.find("<[reprexc() raised in repr()] BrokenRepr1") != -1
+
+ def test_broken_repr_with_showlocals_verbose(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ class ObjWithErrorInRepr:
+ def __repr__(self):
+ raise NotImplementedError
+
+ def test_repr_error():
+ x = ObjWithErrorInRepr()
+ assert x == "value"
+ """
+ )
+ reprec = pytester.inline_run("--showlocals", "-vv", p)
+ passed, skipped, failed = reprec.listoutcomes()
+ assert (len(passed), len(skipped), len(failed)) == (0, 0, 1)
+ entries = failed[0].longrepr.reprtraceback.reprentries # type: ignore[union-attr]
+ assert len(entries) == 1
+ repr_locals = entries[0].reprlocals
+ assert repr_locals.lines
+ assert len(repr_locals.lines) == 1
+ assert repr_locals.lines[0].startswith(
+ "x = <[NotImplementedError() raised in repr()] ObjWithErrorInRepr"
+ )
+
+ def test_skip_file_by_conftest(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ conftest="""
+ import pytest
+ def pytest_collect_file():
+ pytest.skip("intentional")
+ """,
+ test_file="""
+ def test_one(): pass
+ """,
+ )
+ try:
+ reprec = pytester.inline_run(pytester.path)
+ 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
+
+
+class TestNewSession(SessionTests):
+ def test_order_of_execution(self, pytester: Pytester) -> None:
+ reprec = pytester.inline_runsource(
+ """
+ values = []
+ def test_1():
+ values.append(1)
+ def test_2():
+ values.append(2)
+ def test_3():
+ assert values == [1,2]
+ class Testmygroup(object):
+ reslist = values
+ def test_1(self):
+ self.reslist.append(1)
+ def test_2(self):
+ self.reslist.append(2)
+ def test_3(self):
+ self.reslist.append(3)
+ def test_4(self):
+ assert self.reslist == [1,2,1,2,3]
+ """
+ )
+ passed, skipped, failed = reprec.countoutcomes()
+ assert failed == skipped == 0
+ assert passed == 7
+
+ def test_collect_only_with_various_situations(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ test_one="""
+ def test_one():
+ raise ValueError()
+
+ class TestX(object):
+ def test_method_one(self):
+ pass
+
+ class TestY(TestX):
+ pass
+ """,
+ test_three="xxxdsadsadsadsa",
+ __init__="",
+ )
+ reprec = pytester.inline_run("--collect-only", p.parent)
+
+ itemstarted = reprec.getcalls("pytest_itemcollected")
+ assert len(itemstarted) == 3
+ assert not reprec.getreports("pytest_runtest_logreport")
+ started = reprec.getcalls("pytest_collectstart")
+ finished = reprec.getreports("pytest_collectreport")
+ assert len(started) == len(finished)
+ assert len(started) == 6
+ colfail = [x for x in finished if x.failed]
+ assert len(colfail) == 1
+
+ def test_minus_x_import_error(self, pytester: Pytester) -> None:
+ pytester.makepyfile(__init__="")
+ pytester.makepyfile(test_one="xxxx", test_two="yyyy")
+ reprec = pytester.inline_run("-x", pytester.path)
+ finished = reprec.getreports("pytest_collectreport")
+ colfail = [x for x in finished if x.failed]
+ assert len(colfail) == 1
+
+ def test_minus_x_overridden_by_maxfail(self, pytester: Pytester) -> None:
+ pytester.makepyfile(__init__="")
+ pytester.makepyfile(test_one="xxxx", test_two="yyyy", test_third="zzz")
+ reprec = pytester.inline_run("-x", "--maxfail=2", pytester.path)
+ finished = reprec.getreports("pytest_collectreport")
+ colfail = [x for x in finished if x.failed]
+ assert len(colfail) == 2
+
+
+def test_plugin_specify(pytester: Pytester) -> None:
+ with pytest.raises(ImportError):
+ pytester.parseconfig("-p", "nqweotexistent")
+ # pytest.raises(ImportError,
+ # "config.do_configure(config)"
+ # )
+
+
+def test_plugin_already_exists(pytester: Pytester) -> None:
+ config = pytester.parseconfig("-p", "terminal")
+ assert config.option.plugins == ["terminal"]
+ config._do_configure()
+ config._ensure_unconfigure()
+
+
+def test_exclude(pytester: Pytester) -> None:
+ hellodir = pytester.mkdir("hello")
+ hellodir.joinpath("test_hello.py").write_text("x y syntaxerror")
+ hello2dir = pytester.mkdir("hello2")
+ hello2dir.joinpath("test_hello2.py").write_text("x y syntaxerror")
+ pytester.makepyfile(test_ok="def test_pass(): pass")
+ result = pytester.runpytest("--ignore=hello", "--ignore=hello2")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+def test_exclude_glob(pytester: Pytester) -> None:
+ hellodir = pytester.mkdir("hello")
+ hellodir.joinpath("test_hello.py").write_text("x y syntaxerror")
+ hello2dir = pytester.mkdir("hello2")
+ hello2dir.joinpath("test_hello2.py").write_text("x y syntaxerror")
+ hello3dir = pytester.mkdir("hallo3")
+ hello3dir.joinpath("test_hello3.py").write_text("x y syntaxerror")
+ subdir = pytester.mkdir("sub")
+ subdir.joinpath("test_hello4.py").write_text("x y syntaxerror")
+ pytester.makepyfile(test_ok="def test_pass(): pass")
+ result = pytester.runpytest("--ignore-glob=*h[ea]llo*")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+def test_deselect(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_a="""
+ import pytest
+
+ def test_a1(): pass
+
+ @pytest.mark.parametrize('b', range(3))
+ def test_a2(b): pass
+
+ class TestClass:
+ def test_c1(self): pass
+
+ def test_c2(self): pass
+ """
+ )
+ result = pytester.runpytest(
+ "-v",
+ "--deselect=test_a.py::test_a2[1]",
+ "--deselect=test_a.py::test_a2[2]",
+ "--deselect=test_a.py::TestClass::test_c1",
+ )
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*3 passed, 3 deselected*"])
+ for line in result.stdout.lines:
+ assert not line.startswith(("test_a.py::test_a2[1]", "test_a.py::test_a2[2]"))
+
+
+def test_sessionfinish_with_start(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import os
+ values = []
+ def pytest_sessionstart():
+ values.append(os.getcwd())
+ os.chdir("..")
+
+ def pytest_sessionfinish():
+ assert values[0] == os.getcwd()
+
+ """
+ )
+ res = pytester.runpytest("--collect-only")
+ assert res.ret == ExitCode.NO_TESTS_COLLECTED
+
+
+@pytest.mark.parametrize("path", ["root", "{relative}/root", "{environment}/root"])
+def test_rootdir_option_arg(
+ pytester: Pytester, monkeypatch: MonkeyPatch, path: str
+) -> None:
+ monkeypatch.setenv("PY_ROOTDIR_PATH", str(pytester.path))
+ path = path.format(relative=str(pytester.path), environment="$PY_ROOTDIR_PATH")
+
+ rootdir = pytester.path / "root" / "tests"
+ rootdir.mkdir(parents=True)
+ pytester.makepyfile(
+ """
+ import os
+ def test_one():
+ assert 1
+ """
+ )
+
+ result = pytester.runpytest(f"--rootdir={path}")
+ result.stdout.fnmatch_lines(
+ [
+ f"*rootdir: {pytester.path}/root",
+ "root/test_rootdir_option_arg.py *",
+ "*1 passed*",
+ ]
+ )
+
+
+def test_rootdir_wrong_option_arg(pytester: Pytester) -> None:
+ result = pytester.runpytest("--rootdir=wrong_dir")
+ result.stderr.fnmatch_lines(
+ ["*Directory *wrong_dir* not found. Check your '--rootdir' option.*"]
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_setuponly.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_setuponly.py
new file mode 100644
index 0000000000..fe4bdc514e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_setuponly.py
@@ -0,0 +1,318 @@
+import sys
+
+import pytest
+from _pytest.config import ExitCode
+from _pytest.pytester import Pytester
+
+
+@pytest.fixture(params=["--setup-only", "--setup-plan", "--setup-show"], scope="module")
+def mode(request):
+ return request.param
+
+
+def test_show_only_active_fixtures(
+ pytester: Pytester, mode, dummy_yaml_custom_test
+) -> None:
+ pytester.makepyfile(
+ '''
+ import pytest
+ @pytest.fixture
+ def _arg0():
+ """hidden arg0 fixture"""
+ @pytest.fixture
+ def arg1():
+ """arg1 docstring"""
+ def test_arg1(arg1):
+ pass
+ '''
+ )
+
+ result = pytester.runpytest(mode)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ ["*SETUP F arg1*", "*test_arg1 (fixtures used: arg1)*", "*TEARDOWN F arg1*"]
+ )
+ result.stdout.no_fnmatch_line("*_arg0*")
+
+
+def test_show_different_scopes(pytester: Pytester, mode) -> None:
+ p = pytester.makepyfile(
+ '''
+ import pytest
+ @pytest.fixture
+ def arg_function():
+ """function scoped fixture"""
+ @pytest.fixture(scope='session')
+ def arg_session():
+ """session scoped fixture"""
+ def test_arg1(arg_session, arg_function):
+ pass
+ '''
+ )
+
+ result = pytester.runpytest(mode, p)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ [
+ "SETUP S arg_session*",
+ "*SETUP F arg_function*",
+ "*test_arg1 (fixtures used: arg_function, arg_session)*",
+ "*TEARDOWN F arg_function*",
+ "TEARDOWN S arg_session*",
+ ]
+ )
+
+
+def test_show_nested_fixtures(pytester: Pytester, mode) -> None:
+ pytester.makeconftest(
+ '''
+ import pytest
+ @pytest.fixture(scope='session')
+ def arg_same():
+ """session scoped fixture"""
+ '''
+ )
+ p = pytester.makepyfile(
+ '''
+ import pytest
+ @pytest.fixture(scope='function')
+ def arg_same(arg_same):
+ """function scoped fixture"""
+ def test_arg1(arg_same):
+ pass
+ '''
+ )
+
+ result = pytester.runpytest(mode, p)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ [
+ "SETUP S arg_same*",
+ "*SETUP F arg_same (fixtures used: arg_same)*",
+ "*test_arg1 (fixtures used: arg_same)*",
+ "*TEARDOWN F arg_same*",
+ "TEARDOWN S arg_same*",
+ ]
+ )
+
+
+def test_show_fixtures_with_autouse(pytester: Pytester, mode) -> None:
+ p = pytester.makepyfile(
+ '''
+ import pytest
+ @pytest.fixture
+ def arg_function():
+ """function scoped fixture"""
+ @pytest.fixture(scope='session', autouse=True)
+ def arg_session():
+ """session scoped fixture"""
+ def test_arg1(arg_function):
+ pass
+ '''
+ )
+
+ result = pytester.runpytest(mode, p)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ [
+ "SETUP S arg_session*",
+ "*SETUP F arg_function*",
+ "*test_arg1 (fixtures used: arg_function, arg_session)*",
+ ]
+ )
+
+
+def test_show_fixtures_with_parameters(pytester: Pytester, mode) -> None:
+ pytester.makeconftest(
+ '''
+ import pytest
+ @pytest.fixture(scope='session', params=['foo', 'bar'])
+ def arg_same():
+ """session scoped fixture"""
+ '''
+ )
+ p = pytester.makepyfile(
+ '''
+ import pytest
+ @pytest.fixture(scope='function')
+ def arg_other(arg_same):
+ """function scoped fixture"""
+ def test_arg1(arg_other):
+ pass
+ '''
+ )
+
+ result = pytester.runpytest(mode, p)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ [
+ "SETUP S arg_same?'foo'?",
+ "TEARDOWN S arg_same?'foo'?",
+ "SETUP S arg_same?'bar'?",
+ "TEARDOWN S arg_same?'bar'?",
+ ]
+ )
+
+
+def test_show_fixtures_with_parameter_ids(pytester: Pytester, mode) -> None:
+ pytester.makeconftest(
+ '''
+ import pytest
+ @pytest.fixture(
+ scope='session', params=['foo', 'bar'], ids=['spam', 'ham'])
+ def arg_same():
+ """session scoped fixture"""
+ '''
+ )
+ p = pytester.makepyfile(
+ '''
+ import pytest
+ @pytest.fixture(scope='function')
+ def arg_other(arg_same):
+ """function scoped fixture"""
+ def test_arg1(arg_other):
+ pass
+ '''
+ )
+
+ result = pytester.runpytest(mode, p)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ ["SETUP S arg_same?'spam'?", "SETUP S arg_same?'ham'?"]
+ )
+
+
+def test_show_fixtures_with_parameter_ids_function(pytester: Pytester, mode) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(params=['foo', 'bar'], ids=lambda p: p.upper())
+ def foobar():
+ pass
+ def test_foobar(foobar):
+ pass
+ """
+ )
+
+ result = pytester.runpytest(mode, p)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ ["*SETUP F foobar?'FOO'?", "*SETUP F foobar?'BAR'?"]
+ )
+
+
+def test_dynamic_fixture_request(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture()
+ def dynamically_requested_fixture():
+ pass
+ @pytest.fixture()
+ def dependent_fixture(request):
+ request.getfixturevalue('dynamically_requested_fixture')
+ def test_dyn(dependent_fixture):
+ pass
+ """
+ )
+
+ result = pytester.runpytest("--setup-only", p)
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ [
+ "*SETUP F dynamically_requested_fixture",
+ "*TEARDOWN F dynamically_requested_fixture",
+ ]
+ )
+
+
+def test_capturing(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest, sys
+ @pytest.fixture()
+ def one():
+ sys.stdout.write('this should be captured')
+ sys.stderr.write('this should also be captured')
+ @pytest.fixture()
+ def two(one):
+ assert 0
+ def test_capturing(two):
+ pass
+ """
+ )
+
+ result = pytester.runpytest("--setup-only", p)
+ result.stdout.fnmatch_lines(
+ ["this should be captured", "this should also be captured"]
+ )
+
+
+def test_show_fixtures_and_execute_test(pytester: Pytester) -> None:
+ """Verify that setups are shown and tests are executed."""
+ p = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture
+ def arg():
+ assert True
+ def test_arg(arg):
+ assert False
+ """
+ )
+
+ result = pytester.runpytest("--setup-show", p)
+ assert result.ret == 1
+
+ result.stdout.fnmatch_lines(
+ ["*SETUP F arg*", "*test_arg (fixtures used: arg)F*", "*TEARDOWN F arg*"]
+ )
+
+
+def test_setup_show_with_KeyboardInterrupt_in_test(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture
+ def arg():
+ pass
+ def test_arg(arg):
+ raise KeyboardInterrupt()
+ """
+ )
+ result = pytester.runpytest("--setup-show", p, no_reraise_ctrlc=True)
+ result.stdout.fnmatch_lines(
+ [
+ "*SETUP F arg*",
+ "*test_arg (fixtures used: arg)*",
+ "*TEARDOWN F arg*",
+ "*! KeyboardInterrupt !*",
+ "*= no tests ran in *",
+ ]
+ )
+ assert result.ret == ExitCode.INTERRUPTED
+
+
+def test_show_fixture_action_with_bytes(pytester: Pytester) -> None:
+ # Issue 7126, BytesWarning when using --setup-show with bytes parameter
+ test_file = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.parametrize('data', [b'Hello World'])
+ def test_data(data):
+ pass
+ """
+ )
+ result = pytester.run(
+ sys.executable, "-bb", "-m", "pytest", "--setup-show", str(test_file)
+ )
+ assert result.ret == 0
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_setupplan.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_setupplan.py
new file mode 100644
index 0000000000..d51a187395
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_setupplan.py
@@ -0,0 +1,120 @@
+from _pytest.pytester import Pytester
+
+
+def test_show_fixtures_and_test(
+ pytester: Pytester, dummy_yaml_custom_test: None
+) -> None:
+ """Verify that fixtures are not executed."""
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture
+ def arg():
+ assert False
+ def test_arg(arg):
+ assert False
+ """
+ )
+
+ result = pytester.runpytest("--setup-plan")
+ assert result.ret == 0
+
+ result.stdout.fnmatch_lines(
+ ["*SETUP F arg*", "*test_arg (fixtures used: arg)", "*TEARDOWN F arg*"]
+ )
+
+
+def test_show_multi_test_fixture_setup_and_teardown_correctly_simple(
+ pytester: Pytester,
+) -> None:
+ """Verify that when a fixture lives for longer than a single test, --setup-plan
+ correctly displays the SETUP/TEARDOWN indicators the right number of times.
+
+ As reported in https://github.com/pytest-dev/pytest/issues/2049
+ --setup-plan was showing SETUP/TEARDOWN on every test, even when the fixture
+ should persist through multiple tests.
+
+ (Note that this bug never affected actual test execution, which used the
+ correct fixture lifetimes. It was purely a display bug for --setup-plan, and
+ did not affect the related --setup-show or --setup-only.)
+ """
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope = 'class')
+ def fix():
+ return object()
+ class TestClass:
+ def test_one(self, fix):
+ assert False
+ def test_two(self, fix):
+ assert False
+ """
+ )
+
+ result = pytester.runpytest("--setup-plan")
+ assert result.ret == 0
+
+ setup_fragment = "SETUP C fix"
+ setup_count = 0
+
+ teardown_fragment = "TEARDOWN C fix"
+ teardown_count = 0
+
+ for line in result.stdout.lines:
+ if setup_fragment in line:
+ setup_count += 1
+ if teardown_fragment in line:
+ teardown_count += 1
+
+ # before the fix this tests, there would have been a setup/teardown
+ # message for each test, so the counts would each have been 2
+ assert setup_count == 1
+ assert teardown_count == 1
+
+
+def test_show_multi_test_fixture_setup_and_teardown_same_as_setup_show(
+ pytester: Pytester,
+) -> None:
+ """Verify that SETUP/TEARDOWN messages match what comes out of --setup-show."""
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope = 'session')
+ def sess():
+ return True
+ @pytest.fixture(scope = 'module')
+ def mod():
+ return True
+ @pytest.fixture(scope = 'class')
+ def cls():
+ return True
+ @pytest.fixture(scope = 'function')
+ def func():
+ return True
+ def test_outside(sess, mod, cls, func):
+ assert True
+ class TestCls:
+ def test_one(self, sess, mod, cls, func):
+ assert True
+ def test_two(self, sess, mod, cls, func):
+ assert True
+ """
+ )
+
+ plan_result = pytester.runpytest("--setup-plan")
+ show_result = pytester.runpytest("--setup-show")
+
+ # the number and text of these lines should be identical
+ plan_lines = [
+ line
+ for line in plan_result.stdout.lines
+ if "SETUP" in line or "TEARDOWN" in line
+ ]
+ show_lines = [
+ line
+ for line in show_result.stdout.lines
+ if "SETUP" in line or "TEARDOWN" in line
+ ]
+
+ assert plan_lines == show_lines
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_skipping.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_skipping.py
new file mode 100644
index 0000000000..3010943607
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_skipping.py
@@ -0,0 +1,1533 @@
+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
+
+
+class TestEvaluation:
+ def test_no_marker(self, pytester: Pytester) -> None:
+ item = pytester.getitem("def test_func(): pass")
+ skipped = evaluate_skip_marks(item)
+ assert not skipped
+
+ def test_marked_xfail_no_args(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ @pytest.mark.xfail
+ def test_func():
+ pass
+ """
+ )
+ xfailed = evaluate_xfail_marks(item)
+ assert xfailed
+ assert xfailed.reason == ""
+ assert xfailed.run
+
+ def test_marked_skipif_no_args(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ @pytest.mark.skipif
+ def test_func():
+ pass
+ """
+ )
+ skipped = evaluate_skip_marks(item)
+ assert skipped
+ assert skipped.reason == ""
+
+ def test_marked_one_arg(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ @pytest.mark.skipif("hasattr(os, 'sep')")
+ def test_func():
+ pass
+ """
+ )
+ skipped = evaluate_skip_marks(item)
+ assert skipped
+ assert skipped.reason == "condition: hasattr(os, 'sep')"
+
+ def test_marked_one_arg_with_reason(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ @pytest.mark.skipif("hasattr(os, 'sep')", attr=2, reason="hello world")
+ def test_func():
+ pass
+ """
+ )
+ skipped = evaluate_skip_marks(item)
+ assert skipped
+ assert skipped.reason == "hello world"
+
+ def test_marked_one_arg_twice(self, pytester: Pytester) -> None:
+ lines = [
+ """@pytest.mark.skipif("not hasattr(os, 'murks')")""",
+ """@pytest.mark.skipif(condition="hasattr(os, 'murks')")""",
+ ]
+ for i in range(0, 2):
+ item = pytester.getitem(
+ """
+ import pytest
+ %s
+ %s
+ def test_func():
+ pass
+ """
+ % (lines[i], lines[(i + 1) % 2])
+ )
+ skipped = evaluate_skip_marks(item)
+ assert skipped
+ assert skipped.reason == "condition: not hasattr(os, 'murks')"
+
+ def test_marked_one_arg_twice2(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ @pytest.mark.skipif("hasattr(os, 'murks')")
+ @pytest.mark.skipif("not hasattr(os, 'murks')")
+ def test_func():
+ pass
+ """
+ )
+ skipped = evaluate_skip_marks(item)
+ assert skipped
+ assert skipped.reason == "condition: not hasattr(os, 'murks')"
+
+ def test_marked_skipif_with_boolean_without_reason(
+ self, pytester: Pytester
+ ) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ @pytest.mark.skipif(False)
+ def test_func():
+ pass
+ """
+ )
+ with pytest.raises(pytest.fail.Exception) as excinfo:
+ evaluate_skip_marks(item)
+ assert excinfo.value.msg is not None
+ assert (
+ """Error evaluating 'skipif': you need to specify reason=STRING when using booleans as conditions."""
+ in excinfo.value.msg
+ )
+
+ def test_marked_skipif_with_invalid_boolean(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+
+ class InvalidBool:
+ def __bool__(self):
+ raise TypeError("INVALID")
+
+ @pytest.mark.skipif(InvalidBool(), reason="xxx")
+ def test_func():
+ pass
+ """
+ )
+ with pytest.raises(pytest.fail.Exception) as excinfo:
+ evaluate_skip_marks(item)
+ assert excinfo.value.msg is not None
+ assert "Error evaluating 'skipif' condition as a boolean" in excinfo.value.msg
+ assert "INVALID" in excinfo.value.msg
+
+ def test_skipif_class(self, pytester: Pytester) -> None:
+ (item,) = pytester.getitems(
+ """
+ import pytest
+ class TestClass(object):
+ pytestmark = pytest.mark.skipif("config._hackxyz")
+ def test_func(self):
+ pass
+ """
+ )
+ item.config._hackxyz = 3 # type: ignore[attr-defined]
+ skipped = evaluate_skip_marks(item)
+ assert skipped
+ assert skipped.reason == "condition: config._hackxyz"
+
+ def test_skipif_markeval_namespace(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ def pytest_markeval_namespace():
+ return {"color": "green"}
+ """
+ )
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.skipif("color == 'green'")
+ def test_1():
+ assert True
+
+ @pytest.mark.skipif("color == 'red'")
+ def test_2():
+ assert True
+ """
+ )
+ res = pytester.runpytest(p)
+ assert res.ret == 0
+ res.stdout.fnmatch_lines(["*1 skipped*"])
+ res.stdout.fnmatch_lines(["*1 passed*"])
+
+ def test_skipif_markeval_namespace_multiple(self, pytester: Pytester) -> None:
+ """Keys defined by ``pytest_markeval_namespace()`` in nested plugins override top-level ones."""
+ root = pytester.mkdir("root")
+ root.joinpath("__init__.py").touch()
+ root.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ def pytest_markeval_namespace():
+ return {"arg": "root"}
+ """
+ )
+ )
+ root.joinpath("test_root.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ @pytest.mark.skipif("arg == 'root'")
+ def test_root():
+ assert False
+ """
+ )
+ )
+ foo = root.joinpath("foo")
+ foo.mkdir()
+ foo.joinpath("__init__.py").touch()
+ foo.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ def pytest_markeval_namespace():
+ return {"arg": "foo"}
+ """
+ )
+ )
+ foo.joinpath("test_foo.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ @pytest.mark.skipif("arg == 'foo'")
+ def test_foo():
+ assert False
+ """
+ )
+ )
+ bar = root.joinpath("bar")
+ bar.mkdir()
+ bar.joinpath("__init__.py").touch()
+ bar.joinpath("conftest.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ def pytest_markeval_namespace():
+ return {"arg": "bar"}
+ """
+ )
+ )
+ bar.joinpath("test_bar.py").write_text(
+ textwrap.dedent(
+ """\
+ import pytest
+
+ @pytest.mark.skipif("arg == 'bar'")
+ def test_bar():
+ assert False
+ """
+ )
+ )
+
+ reprec = pytester.inline_run("-vs", "--capture=no")
+ reprec.assertoutcome(skipped=3)
+
+ def test_skipif_markeval_namespace_ValueError(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ def pytest_markeval_namespace():
+ return True
+ """
+ )
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.skipif("color == 'green'")
+ def test_1():
+ assert True
+ """
+ )
+ res = pytester.runpytest(p)
+ assert res.ret == 1
+ res.stdout.fnmatch_lines(
+ [
+ "*ValueError: pytest_markeval_namespace() needs to return a dict, got True*"
+ ]
+ )
+
+
+class TestXFail:
+ @pytest.mark.parametrize("strict", [True, False])
+ def test_xfail_simple(self, pytester: Pytester, strict: bool) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ @pytest.mark.xfail(strict=%s)
+ def test_func():
+ assert 0
+ """
+ % strict
+ )
+ reports = runtestprotocol(item, log=False)
+ assert len(reports) == 3
+ callreport = reports[1]
+ assert callreport.skipped
+ assert callreport.wasxfail == ""
+
+ def test_xfail_xpassed(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ @pytest.mark.xfail(reason="this is an xfail")
+ def test_func():
+ assert 1
+ """
+ )
+ reports = runtestprotocol(item, log=False)
+ assert len(reports) == 3
+ callreport = reports[1]
+ assert callreport.passed
+ assert callreport.wasxfail == "this is an xfail"
+
+ def test_xfail_using_platform(self, pytester: Pytester) -> None:
+ """Verify that platform can be used with xfail statements."""
+ item = pytester.getitem(
+ """
+ import pytest
+ @pytest.mark.xfail("platform.platform() == platform.platform()")
+ def test_func():
+ assert 0
+ """
+ )
+ reports = runtestprotocol(item, log=False)
+ assert len(reports) == 3
+ callreport = reports[1]
+ assert callreport.wasxfail
+
+ def test_xfail_xpassed_strict(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ @pytest.mark.xfail(strict=True, reason="nope")
+ def test_func():
+ assert 1
+ """
+ )
+ reports = runtestprotocol(item, log=False)
+ assert len(reports) == 3
+ callreport = reports[1]
+ assert callreport.failed
+ assert str(callreport.longrepr) == "[XPASS(strict)] nope"
+ assert not hasattr(callreport, "wasxfail")
+
+ def test_xfail_run_anyway(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.xfail
+ def test_func():
+ assert 0
+ def test_func2():
+ pytest.xfail("hello")
+ """
+ )
+ result = pytester.runpytest("--runxfail")
+ result.stdout.fnmatch_lines(
+ ["*def test_func():*", "*assert 0*", "*1 failed*1 pass*"]
+ )
+
+ @pytest.mark.parametrize(
+ "test_input,expected",
+ [
+ (
+ ["-rs"],
+ ["SKIPPED [1] test_sample.py:2: unconditional skip", "*1 skipped*"],
+ ),
+ (
+ ["-rs", "--runxfail"],
+ ["SKIPPED [1] test_sample.py:2: unconditional skip", "*1 skipped*"],
+ ),
+ ],
+ )
+ def test_xfail_run_with_skip_mark(
+ self, pytester: Pytester, test_input, expected
+ ) -> None:
+ pytester.makepyfile(
+ test_sample="""
+ import pytest
+ @pytest.mark.skip
+ def test_skip_location() -> None:
+ assert 0
+ """
+ )
+ result = pytester.runpytest(*test_input)
+ result.stdout.fnmatch_lines(expected)
+
+ def test_xfail_evalfalse_but_fails(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ @pytest.mark.xfail('False')
+ def test_func():
+ assert 0
+ """
+ )
+ reports = runtestprotocol(item, log=False)
+ callreport = reports[1]
+ assert callreport.failed
+ assert not hasattr(callreport, "wasxfail")
+ assert "xfail" in callreport.keywords
+
+ def test_xfail_not_report_default(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ test_one="""
+ import pytest
+ @pytest.mark.xfail
+ def test_this():
+ assert 0
+ """
+ )
+ pytester.runpytest(p, "-v")
+ # result.stdout.fnmatch_lines([
+ # "*HINT*use*-r*"
+ # ])
+
+ def test_xfail_not_run_xfail_reporting(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ test_one="""
+ import pytest
+ @pytest.mark.xfail(run=False, reason="noway")
+ def test_this():
+ assert 0
+ @pytest.mark.xfail("True", run=False)
+ def test_this_true():
+ assert 0
+ @pytest.mark.xfail("False", run=False, reason="huh")
+ def test_this_false():
+ assert 1
+ """
+ )
+ result = pytester.runpytest(p, "-rx")
+ result.stdout.fnmatch_lines(
+ [
+ "*test_one*test_this*",
+ "*NOTRUN*noway",
+ "*test_one*test_this_true*",
+ "*NOTRUN*condition:*True*",
+ "*1 passed*",
+ ]
+ )
+
+ def test_xfail_not_run_no_setup_run(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ test_one="""
+ import pytest
+ @pytest.mark.xfail(run=False, reason="hello")
+ def test_this():
+ assert 0
+ def setup_module(mod):
+ raise ValueError(42)
+ """
+ )
+ result = pytester.runpytest(p, "-rx")
+ result.stdout.fnmatch_lines(
+ ["*test_one*test_this*", "*NOTRUN*hello", "*1 xfailed*"]
+ )
+
+ def test_xfail_xpass(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ test_one="""
+ import pytest
+ @pytest.mark.xfail
+ def test_that():
+ assert 1
+ """
+ )
+ result = pytester.runpytest(p, "-rX")
+ result.stdout.fnmatch_lines(["*XPASS*test_that*", "*1 xpassed*"])
+ assert result.ret == 0
+
+ def test_xfail_imperative(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ def test_this():
+ pytest.xfail("hello")
+ """
+ )
+ 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 = pytester.runpytest(p, "--runxfail")
+ result.stdout.fnmatch_lines(["*1 pass*"])
+
+ def test_xfail_imperative_in_setup_function(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ def setup_function(function):
+ pytest.xfail("hello")
+
+ def test_this():
+ assert 0
+ """
+ )
+ 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 = pytester.runpytest(p, "--runxfail")
+ result.stdout.fnmatch_lines(
+ """
+ *def test_this*
+ *1 fail*
+ """
+ )
+
+ def xtest_dynamic_xfail_set_during_setup(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ def setup_function(function):
+ pytest.mark.xfail(function)
+ def test_this():
+ assert 0
+ def test_that():
+ assert 1
+ """
+ )
+ result = pytester.runpytest(p, "-rxX")
+ result.stdout.fnmatch_lines(["*XFAIL*test_this*", "*XPASS*test_that*"])
+
+ def test_dynamic_xfail_no_run(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture
+ def arg(request):
+ request.applymarker(pytest.mark.xfail(run=False))
+ def test_this(arg):
+ assert 0
+ """
+ )
+ result = pytester.runpytest(p, "-rxX")
+ result.stdout.fnmatch_lines(["*XFAIL*test_this*", "*NOTRUN*"])
+
+ def test_dynamic_xfail_set_during_funcarg_setup(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture
+ def arg(request):
+ request.applymarker(pytest.mark.xfail)
+ def test_this2(arg):
+ assert 0
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(["*1 xfailed*"])
+
+ def test_dynamic_xfail_set_during_runtest_failed(self, pytester: Pytester) -> None:
+ # Issue #7486.
+ p = pytester.makepyfile(
+ """
+ import pytest
+ def test_this(request):
+ request.node.add_marker(pytest.mark.xfail(reason="xfail"))
+ assert 0
+ """
+ )
+ result = pytester.runpytest(p)
+ result.assert_outcomes(xfailed=1)
+
+ def test_dynamic_xfail_set_during_runtest_passed_strict(
+ self, pytester: Pytester
+ ) -> None:
+ # Issue #7486.
+ p = pytester.makepyfile(
+ """
+ import pytest
+ def test_this(request):
+ request.node.add_marker(pytest.mark.xfail(reason="xfail", strict=True))
+ """
+ )
+ result = pytester.runpytest(p)
+ result.assert_outcomes(failed=1)
+
+ @pytest.mark.parametrize(
+ "expected, actual, matchline",
+ [
+ ("TypeError", "TypeError", "*1 xfailed*"),
+ ("(AttributeError, TypeError)", "TypeError", "*1 xfailed*"),
+ ("TypeError", "IndexError", "*1 failed*"),
+ ("(AttributeError, TypeError)", "IndexError", "*1 failed*"),
+ ],
+ )
+ def test_xfail_raises(
+ self, expected, actual, matchline, pytester: Pytester
+ ) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.xfail(raises=%s)
+ def test_raises():
+ raise %s()
+ """
+ % (expected, actual)
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines([matchline])
+
+ def test_strict_sanity(self, pytester: Pytester) -> None:
+ """Sanity check for xfail(strict=True): a failing test should behave
+ exactly like a normal xfail."""
+ p = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.xfail(reason='unsupported feature', strict=True)
+ def test_foo():
+ assert 0
+ """
+ )
+ result = pytester.runpytest(p, "-rxX")
+ result.stdout.fnmatch_lines(["*XFAIL*", "*unsupported feature*"])
+ assert result.ret == 0
+
+ @pytest.mark.parametrize("strict", [True, False])
+ def test_strict_xfail(self, pytester: Pytester, strict: bool) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.xfail(reason='unsupported feature', strict=%s)
+ def test_foo():
+ with open('foo_executed', 'w'): pass # make sure test executes
+ """
+ % strict
+ )
+ result = pytester.runpytest(p, "-rxX")
+ if strict:
+ result.stdout.fnmatch_lines(
+ ["*test_foo*", "*XPASS(strict)*unsupported feature*"]
+ )
+ else:
+ result.stdout.fnmatch_lines(
+ [
+ "*test_strict_xfail*",
+ "XPASS test_strict_xfail.py::test_foo unsupported feature",
+ ]
+ )
+ assert result.ret == (1 if strict else 0)
+ assert pytester.path.joinpath("foo_executed").exists()
+
+ @pytest.mark.parametrize("strict", [True, False])
+ def test_strict_xfail_condition(self, pytester: Pytester, strict: bool) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.xfail(False, reason='unsupported feature', strict=%s)
+ def test_foo():
+ pass
+ """
+ % strict
+ )
+ result = pytester.runpytest(p, "-rxX")
+ result.stdout.fnmatch_lines(["*1 passed*"])
+ assert result.ret == 0
+
+ @pytest.mark.parametrize("strict", [True, False])
+ def test_xfail_condition_keyword(self, pytester: Pytester, strict: bool) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.xfail(condition=False, reason='unsupported feature', strict=%s)
+ def test_foo():
+ pass
+ """
+ % strict
+ )
+ result = pytester.runpytest(p, "-rxX")
+ result.stdout.fnmatch_lines(["*1 passed*"])
+ assert result.ret == 0
+
+ @pytest.mark.parametrize("strict_val", ["true", "false"])
+ def test_strict_xfail_default_from_file(
+ self, pytester: Pytester, strict_val
+ ) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ xfail_strict = %s
+ """
+ % strict_val
+ )
+ p = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.xfail(reason='unsupported feature')
+ def test_foo():
+ pass
+ """
+ )
+ result = pytester.runpytest(p, "-rxX")
+ strict = strict_val == "true"
+ result.stdout.fnmatch_lines(["*1 failed*" if strict else "*1 xpassed*"])
+ assert result.ret == (1 if strict else 0)
+
+ def test_xfail_markeval_namespace(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ def pytest_markeval_namespace():
+ return {"color": "green"}
+ """
+ )
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.xfail("color == 'green'")
+ def test_1():
+ assert False
+
+ @pytest.mark.xfail("color == 'red'")
+ def test_2():
+ assert False
+ """
+ )
+ res = pytester.runpytest(p)
+ assert res.ret == 1
+ res.stdout.fnmatch_lines(["*1 failed*"])
+ res.stdout.fnmatch_lines(["*1 xfailed*"])
+
+
+class TestXFailwithSetupTeardown:
+ def test_failing_setup_issue9(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def setup_function(func):
+ assert 0
+
+ @pytest.mark.xfail
+ def test_func():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 xfail*"])
+
+ def test_failing_teardown_issue9(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def teardown_function(func):
+ assert 0
+
+ @pytest.mark.xfail
+ def test_func():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 xfail*"])
+
+
+class TestSkip:
+ def test_skip_class(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skip
+ class TestSomething(object):
+ def test_foo(self):
+ pass
+ def test_bar(self):
+ pass
+
+ def test_baz():
+ pass
+ """
+ )
+ rec = pytester.inline_run()
+ rec.assertoutcome(skipped=2, passed=1)
+
+ def test_skips_on_false_string(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skip('False')
+ def test_foo():
+ pass
+ """
+ )
+ rec = pytester.inline_run()
+ rec.assertoutcome(skipped=1)
+
+ def test_arg_as_reason(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skip('testing stuff')
+ def test_bar():
+ pass
+ """
+ )
+ result = pytester.runpytest("-rs")
+ result.stdout.fnmatch_lines(["*testing stuff*", "*1 skipped*"])
+
+ def test_skip_no_reason(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skip
+ def test_foo():
+ pass
+ """
+ )
+ result = pytester.runpytest("-rs")
+ result.stdout.fnmatch_lines(["*unconditional skip*", "*1 skipped*"])
+
+ def test_skip_with_reason(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skip(reason="for lolz")
+ def test_bar():
+ pass
+ """
+ )
+ result = pytester.runpytest("-rs")
+ result.stdout.fnmatch_lines(["*for lolz*", "*1 skipped*"])
+
+ def test_only_skips_marked_test(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skip
+ def test_foo():
+ pass
+ @pytest.mark.skip(reason="nothing in particular")
+ def test_bar():
+ pass
+ def test_baz():
+ assert True
+ """
+ )
+ result = pytester.runpytest("-rs")
+ result.stdout.fnmatch_lines(["*nothing in particular*", "*1 passed*2 skipped*"])
+
+ def test_strict_and_skip(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skip
+ def test_hello():
+ pass
+ """
+ )
+ result = pytester.runpytest("-rs", "--strict-markers")
+ result.stdout.fnmatch_lines(["*unconditional skip*", "*1 skipped*"])
+
+ def test_wrong_skip_usage(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skip(False, reason="I thought this was skipif")
+ def test_hello():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*TypeError: *__init__() got multiple values for argument 'reason'"
+ " - maybe you meant pytest.mark.skipif?"
+ ]
+ )
+
+
+class TestSkipif:
+ def test_skipif_conditional(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ @pytest.mark.skipif("hasattr(os, 'sep')")
+ def test_func():
+ pass
+ """
+ )
+ x = pytest.raises(pytest.skip.Exception, lambda: pytest_runtest_setup(item))
+ assert x.value.msg == "condition: hasattr(os, 'sep')"
+
+ @pytest.mark.parametrize(
+ "params", ["\"hasattr(sys, 'platform')\"", 'True, reason="invalid platform"']
+ )
+ def test_skipif_reporting(self, pytester: Pytester, params) -> None:
+ p = pytester.makepyfile(
+ test_foo="""
+ import pytest
+ @pytest.mark.skipif(%(params)s)
+ def test_that():
+ assert 0
+ """
+ % dict(params=params)
+ )
+ result = pytester.runpytest(p, "-s", "-rs")
+ result.stdout.fnmatch_lines(["*SKIP*1*test_foo.py*platform*", "*1 skipped*"])
+ assert result.ret == 0
+
+ def test_skipif_using_platform(self, pytester: Pytester) -> None:
+ item = pytester.getitem(
+ """
+ import pytest
+ @pytest.mark.skipif("platform.platform() == platform.platform()")
+ def test_func():
+ pass
+ """
+ )
+ pytest.raises(pytest.skip.Exception, lambda: pytest_runtest_setup(item))
+
+ @pytest.mark.parametrize(
+ "marker, msg1, msg2",
+ [("skipif", "SKIP", "skipped"), ("xfail", "XPASS", "xpassed")],
+ )
+ def test_skipif_reporting_multiple(
+ self, pytester: Pytester, marker, msg1, msg2
+ ) -> None:
+ pytester.makepyfile(
+ test_foo="""
+ 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(
+ [f"*{msg1}*test_foo.py*second_condition*", f"*1 {msg2}*"]
+ )
+ assert result.ret == 0
+
+
+def test_skip_not_report_default(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ test_one="""
+ import pytest
+ def test_this():
+ pytest.skip("hello")
+ """
+ )
+ result = pytester.runpytest(p, "-v")
+ result.stdout.fnmatch_lines(
+ [
+ # "*HINT*use*-r*",
+ "*1 skipped*"
+ ]
+ )
+
+
+def test_skipif_class(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ class TestClass(object):
+ pytestmark = pytest.mark.skipif("True")
+ def test_that(self):
+ assert 0
+ def test_though(self):
+ assert 0
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines(["*2 skipped*"])
+
+
+def test_skipped_reasons_functional(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_one="""
+ import pytest
+ from conftest import doskip
+
+ def setup_function(func):
+ doskip()
+
+ def test_func():
+ pass
+
+ class TestClass(object):
+ def test_method(self):
+ doskip()
+
+ @pytest.mark.skip("via_decorator")
+ def test_deco(self):
+ assert 0
+ """,
+ conftest="""
+ import pytest, sys
+ def doskip():
+ assert sys._getframe().f_lineno == 3
+ pytest.skip('test')
+ """,
+ )
+ result = pytester.runpytest("-rs")
+ result.stdout.fnmatch_lines_random(
+ [
+ "SKIPPED [[]2[]] conftest.py:4: test",
+ "SKIPPED [[]1[]] test_one.py:14: via_decorator",
+ ]
+ )
+ assert result.ret == 0
+
+
+def test_skipped_folding(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_one="""
+ import pytest
+ pytestmark = pytest.mark.skip("Folding")
+ def setup_function(func):
+ pass
+ def test_func():
+ pass
+ class TestClass(object):
+ def test_method(self):
+ pass
+ """
+ )
+ result = pytester.runpytest("-rs")
+ result.stdout.fnmatch_lines(["*SKIP*2*test_one.py: Folding"])
+ assert result.ret == 0
+
+
+def test_reportchars(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_1():
+ assert 0
+ @pytest.mark.xfail
+ def test_2():
+ assert 0
+ @pytest.mark.xfail
+ def test_3():
+ pass
+ def test_4():
+ pytest.skip("four")
+ """
+ )
+ result = pytester.runpytest("-rfxXs")
+ result.stdout.fnmatch_lines(
+ ["FAIL*test_1*", "XFAIL*test_2*", "XPASS*test_3*", "SKIP*four*"]
+ )
+
+
+def test_reportchars_error(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ conftest="""
+ def pytest_runtest_teardown():
+ assert 0
+ """,
+ test_simple="""
+ def test_foo():
+ pass
+ """,
+ )
+ result = pytester.runpytest("-rE")
+ result.stdout.fnmatch_lines(["ERROR*test_foo*"])
+
+
+def test_reportchars_all(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_1():
+ assert 0
+ @pytest.mark.xfail
+ def test_2():
+ assert 0
+ @pytest.mark.xfail
+ def test_3():
+ pass
+ def test_4():
+ pytest.skip("four")
+ @pytest.fixture
+ def fail():
+ assert 0
+ def test_5(fail):
+ pass
+ """
+ )
+ result = pytester.runpytest("-ra")
+ result.stdout.fnmatch_lines(
+ [
+ "SKIP*four*",
+ "XFAIL*test_2*",
+ "XPASS*test_3*",
+ "ERROR*test_5*",
+ "FAIL*test_1*",
+ ]
+ )
+
+
+def test_reportchars_all_error(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ conftest="""
+ def pytest_runtest_teardown():
+ assert 0
+ """,
+ test_simple="""
+ def test_foo():
+ pass
+ """,
+ )
+ result = pytester.runpytest("-ra")
+ result.stdout.fnmatch_lines(["ERROR*test_foo*"])
+
+
+def test_errors_in_xfail_skip_expressions(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skipif("asd")
+ def test_nameerror():
+ pass
+ @pytest.mark.xfail("syntax error")
+ def test_syntax():
+ pass
+
+ def test_func():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ 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:]
+
+ if sys.version_info[:2] >= (3, 10):
+ expected = [
+ "*ERROR*test_nameerror*",
+ "*asd*",
+ "",
+ "During handling of the above exception, another exception occurred:",
+ ]
+ else:
+ expected = [
+ "*ERROR*test_nameerror*",
+ ]
+
+ expected += [
+ "*evaluating*skipif*condition*",
+ "*asd*",
+ "*ERROR*test_syntax*",
+ "*evaluating*xfail*condition*",
+ " syntax error",
+ markline,
+ "SyntaxError: invalid syntax",
+ "*1 pass*2 errors*",
+ ]
+ result.stdout.fnmatch_lines(expected)
+
+
+def test_xfail_skipif_with_globals(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ x = 3
+ @pytest.mark.skipif("x == 3")
+ def test_skip1():
+ pass
+ @pytest.mark.xfail("x == 3")
+ def test_boolean():
+ assert 0
+ """
+ )
+ result = pytester.runpytest("-rsx")
+ result.stdout.fnmatch_lines(["*SKIP*x == 3*", "*XFAIL*test_boolean*", "*x == 3*"])
+
+
+def test_default_markers(pytester: Pytester) -> None:
+ result = pytester.runpytest("--markers")
+ result.stdout.fnmatch_lines(
+ [
+ "*skipif(condition, ..., [*], reason=...)*skip*",
+ "*xfail(condition, ..., [*], reason=..., run=True, raises=None, strict=xfail_strict)*expected failure*",
+ ]
+ )
+
+
+def test_xfail_test_setup_exception(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_runtest_setup():
+ 0 / 0
+ """
+ )
+ p = pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.xfail
+ def test_func():
+ assert 0
+ """
+ )
+ result = pytester.runpytest(p)
+ assert result.ret == 0
+ assert "xfailed" in result.stdout.str()
+ result.stdout.no_fnmatch_line("*xpassed*")
+
+
+def test_imperativeskip_on_xfail_test(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.xfail
+ def test_that_fails():
+ assert 0
+
+ @pytest.mark.skipif("True")
+ def test_hello():
+ pass
+ """
+ )
+ pytester.makeconftest(
+ """
+ import pytest
+ def pytest_runtest_setup(item):
+ pytest.skip("abc")
+ """
+ )
+ result = pytester.runpytest("-rsxX")
+ result.stdout.fnmatch_lines_random(
+ """
+ *SKIP*abc*
+ *SKIP*condition: True*
+ *2 skipped*
+ """
+ )
+
+
+class TestBooleanCondition:
+ def test_skipif(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skipif(True, reason="True123")
+ def test_func1():
+ pass
+ @pytest.mark.skipif(False, reason="True123")
+ def test_func2():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ """
+ *1 passed*1 skipped*
+ """
+ )
+
+ def test_skipif_noreason(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.skipif(True)
+ def test_func():
+ pass
+ """
+ )
+ result = pytester.runpytest("-rs")
+ result.stdout.fnmatch_lines(
+ """
+ *1 error*
+ """
+ )
+
+ def test_xfail(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.xfail(True, reason="True123")
+ def test_func():
+ assert 0
+ """
+ )
+ result = pytester.runpytest("-rxs")
+ result.stdout.fnmatch_lines(
+ """
+ *XFAIL*
+ *True123*
+ *1 xfail*
+ """
+ )
+
+
+def test_xfail_item(pytester: Pytester) -> None:
+ # Ensure pytest.xfail works with non-Python Item
+ pytester.makeconftest(
+ """
+ import pytest
+
+ class MyItem(pytest.Item):
+ nodeid = 'foo'
+ def runtest(self):
+ pytest.xfail("Expected Failure")
+
+ def pytest_collect_file(file_path, parent):
+ return MyItem.from_parent(name="foo", parent=parent)
+ """
+ )
+ result = pytester.inline_run()
+ passed, skipped, failed = result.listoutcomes()
+ assert not failed
+ xfailed = [r for r in skipped if hasattr(r, "wasxfail")]
+ assert xfailed
+
+
+def test_module_level_skip_error(pytester: Pytester) -> None:
+ """Verify that using pytest.skip at module level causes a collection error."""
+ pytester.makepyfile(
+ """
+ import pytest
+ pytest.skip("skip_module_level")
+
+ def test_func():
+ assert True
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ ["*Using pytest.skip outside of a test will skip the entire module*"]
+ )
+
+
+def test_module_level_skip_with_allow_module_level(pytester: Pytester) -> None:
+ """Verify that using pytest.skip(allow_module_level=True) is allowed."""
+ pytester.makepyfile(
+ """
+ import pytest
+ pytest.skip("skip_module_level", allow_module_level=True)
+
+ def test_func():
+ assert 0
+ """
+ )
+ result = pytester.runpytest("-rxs")
+ result.stdout.fnmatch_lines(["*SKIP*skip_module_level"])
+
+
+def test_invalid_skip_keyword_parameter(pytester: Pytester) -> None:
+ """Verify that using pytest.skip() with unknown parameter raises an error."""
+ pytester.makepyfile(
+ """
+ import pytest
+ pytest.skip("skip_module_level", unknown=1)
+
+ def test_func():
+ assert 0
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*TypeError:*['unknown']*"])
+
+
+def test_mark_xfail_item(pytester: Pytester) -> None:
+ # Ensure pytest.mark.xfail works with non-Python Item
+ pytester.makeconftest(
+ """
+ import pytest
+
+ class MyItem(pytest.Item):
+ nodeid = 'foo'
+ def setup(self):
+ marker = pytest.mark.xfail("1 == 2", reason="Expected failure - false")
+ self.add_marker(marker)
+ marker = pytest.mark.xfail(True, reason="Expected failure - true")
+ self.add_marker(marker)
+ def runtest(self):
+ assert False
+
+ def pytest_collect_file(file_path, parent):
+ return MyItem.from_parent(name="foo", parent=parent)
+ """
+ )
+ result = pytester.inline_run()
+ passed, skipped, failed = result.listoutcomes()
+ assert not failed
+ xfailed = [r for r in skipped if hasattr(r, "wasxfail")]
+ assert xfailed
+
+
+def test_summary_list_after_errors(pytester: Pytester) -> None:
+ """Ensure the list of errors/fails/xfails/skips appears after tracebacks in terminal reporting."""
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_fail():
+ assert 0
+ """
+ )
+ result = pytester.runpytest("-ra")
+ result.stdout.fnmatch_lines(
+ [
+ "=* FAILURES *=",
+ "*= short test summary info =*",
+ "FAILED test_summary_list_after_errors.py::test_fail - assert 0",
+ ]
+ )
+
+
+def test_importorskip() -> None:
+ with pytest.raises(
+ pytest.skip.Exception,
+ match="^could not import 'doesnotexist': No module named .*",
+ ):
+ pytest.importorskip("doesnotexist")
+
+
+def test_relpath_rootdir(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ **{
+ "tests/test_1.py": """
+ import pytest
+ @pytest.mark.skip()
+ def test_pass():
+ pass
+ """,
+ }
+ )
+ result = pytester.runpytest("-rs", "tests/test_1.py", "--rootdir=tests")
+ result.stdout.fnmatch_lines(
+ ["SKIPPED [[]1[]] tests/test_1.py:2: unconditional skip"]
+ )
+
+
+def test_skip_using_reason_works_ok(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ def test_skipping_reason():
+ pytest.skip(reason="skippedreason")
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.no_fnmatch_line("*PytestDeprecationWarning*")
+ result.assert_outcomes(skipped=1)
+
+
+def test_fail_using_reason_works_ok(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ def test_failing_reason():
+ pytest.fail(reason="failedreason")
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.no_fnmatch_line("*PytestDeprecationWarning*")
+ 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(
+ """
+ import pytest
+
+ def test_exit_reason_only():
+ pytest.exit(reason="foo")
+ """
+ )
+ result = pytester.runpytest(p)
+ result.stdout.fnmatch_lines("*_pytest.outcomes.Exit: foo*")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_stash.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_stash.py
new file mode 100644
index 0000000000..2c9df4832e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_stash.py
@@ -0,0 +1,67 @@
+import pytest
+from _pytest.stash import Stash
+from _pytest.stash import StashKey
+
+
+def test_stash() -> None:
+ stash = Stash()
+
+ assert len(stash) == 0
+ assert not stash
+
+ key1 = StashKey[str]()
+ key2 = StashKey[int]()
+
+ # Basic functionality - single key.
+ assert key1 not in stash
+ stash[key1] = "hello"
+ assert key1 in stash
+ assert stash[key1] == "hello"
+ assert stash.get(key1, None) == "hello"
+ stash[key1] = "world"
+ assert stash[key1] == "world"
+ # Has correct type (no mypy error).
+ stash[key1] + "string"
+ assert len(stash) == 1
+ assert stash
+
+ # No interaction with another key.
+ assert key2 not in stash
+ assert stash.get(key2, None) is None
+ with pytest.raises(KeyError):
+ stash[key2]
+ with pytest.raises(KeyError):
+ del stash[key2]
+ stash[key2] = 1
+ assert stash[key2] == 1
+ # Has correct type (no mypy error).
+ stash[key2] + 20
+ del stash[key1]
+ with pytest.raises(KeyError):
+ del stash[key1]
+ with pytest.raises(KeyError):
+ stash[key1]
+
+ # setdefault
+ stash[key1] = "existing"
+ assert stash.setdefault(key1, "default") == "existing"
+ assert stash[key1] == "existing"
+ key_setdefault = StashKey[bytes]()
+ assert stash.setdefault(key_setdefault, b"default") == b"default"
+ assert stash[key_setdefault] == b"default"
+ assert len(stash) == 3
+ assert stash
+
+ # Can't accidentally add attributes to stash object itself.
+ with pytest.raises(AttributeError):
+ stash.foo = "nope" # type: ignore[attr-defined]
+
+ # No interaction with another stash.
+ stash2 = Stash()
+ key3 = StashKey[int]()
+ assert key2 not in stash2
+ stash2[key2] = 100
+ stash2[key3] = 200
+ assert stash2[key2] + stash2[key3] == 300
+ assert stash[key2] == 1
+ assert key3 not in stash
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_stepwise.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_stepwise.py
new file mode 100644
index 0000000000..63d29d6241
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_stepwise.py
@@ -0,0 +1,280 @@
+import pytest
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pytester import Pytester
+
+
+@pytest.fixture
+def stepwise_pytester(pytester: Pytester) -> Pytester:
+ # Rather than having to modify our testfile between tests, we introduce
+ # a flag for whether or not the second test should fail.
+ pytester.makeconftest(
+ """
+def pytest_addoption(parser):
+ group = parser.getgroup('general')
+ group.addoption('--fail', action='store_true', dest='fail')
+ group.addoption('--fail-last', action='store_true', dest='fail_last')
+"""
+ )
+
+ # Create a simple test suite.
+ pytester.makepyfile(
+ test_a="""
+def test_success_before_fail():
+ assert 1
+
+def test_fail_on_flag(request):
+ assert not request.config.getvalue('fail')
+
+def test_success_after_fail():
+ assert 1
+
+def test_fail_last_on_flag(request):
+ assert not request.config.getvalue('fail_last')
+
+def test_success_after_last_fail():
+ assert 1
+"""
+ )
+
+ pytester.makepyfile(
+ test_b="""
+def test_success():
+ assert 1
+"""
+ )
+
+ # customize cache directory so we don't use the tox's cache directory, which makes tests in this module flaky
+ pytester.makeini(
+ """
+ [pytest]
+ cache_dir = .cache
+ """
+ )
+
+ return pytester
+
+
+@pytest.fixture
+def error_pytester(pytester: Pytester) -> Pytester:
+ pytester.makepyfile(
+ test_a="""
+def test_error(nonexisting_fixture):
+ assert 1
+
+def test_success_after_fail():
+ assert 1
+"""
+ )
+
+ return pytester
+
+
+@pytest.fixture
+def broken_pytester(pytester: Pytester) -> Pytester:
+ pytester.makepyfile(
+ working_testfile="def test_proper(): assert 1", broken_testfile="foobar"
+ )
+ return pytester
+
+
+def _strip_resource_warnings(lines):
+ # Strip unreliable ResourceWarnings, so no-output assertions on stderr can work.
+ # (https://github.com/pytest-dev/pytest/issues/5088)
+ return [
+ x
+ for x in lines
+ if not x.startswith(("Exception ignored in:", "ResourceWarning"))
+ ]
+
+
+def test_run_without_stepwise(stepwise_pytester: Pytester) -> None:
+ result = stepwise_pytester.runpytest("-v", "--strict-markers", "--fail")
+ result.stdout.fnmatch_lines(["*test_success_before_fail PASSED*"])
+ result.stdout.fnmatch_lines(["*test_fail_on_flag FAILED*"])
+ result.stdout.fnmatch_lines(["*test_success_after_fail PASSED*"])
+
+
+def test_stepwise_output_summary(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize("expected", [True, True, True, True, False])
+ def test_data(expected):
+ assert expected
+ """
+ )
+ result = pytester.runpytest("-v", "--stepwise")
+ result.stdout.fnmatch_lines(["stepwise: no previously failed tests, not skipping."])
+ result = pytester.runpytest("-v", "--stepwise")
+ result.stdout.fnmatch_lines(
+ ["stepwise: skipping 4 already passed items.", "*1 failed, 4 deselected*"]
+ )
+
+
+def test_fail_and_continue_with_stepwise(stepwise_pytester: Pytester) -> None:
+ # Run the tests with a failing second test.
+ result = stepwise_pytester.runpytest(
+ "-v", "--strict-markers", "--stepwise", "--fail"
+ )
+ assert _strip_resource_warnings(result.stderr.lines) == []
+
+ stdout = result.stdout.str()
+ # Make sure we stop after first failing test.
+ assert "test_success_before_fail PASSED" in stdout
+ assert "test_fail_on_flag FAILED" in stdout
+ assert "test_success_after_fail" not in stdout
+
+ # "Fix" the test that failed in the last run and run it again.
+ result = stepwise_pytester.runpytest("-v", "--strict-markers", "--stepwise")
+ assert _strip_resource_warnings(result.stderr.lines) == []
+
+ stdout = result.stdout.str()
+ # Make sure the latest failing test runs and then continues.
+ assert "test_success_before_fail" not in stdout
+ assert "test_fail_on_flag PASSED" in stdout
+ assert "test_success_after_fail PASSED" in stdout
+
+
+@pytest.mark.parametrize("stepwise_skip", ["--stepwise-skip", "--sw-skip"])
+def test_run_with_skip_option(stepwise_pytester: Pytester, stepwise_skip: str) -> None:
+ result = stepwise_pytester.runpytest(
+ "-v",
+ "--strict-markers",
+ "--stepwise",
+ stepwise_skip,
+ "--fail",
+ "--fail-last",
+ )
+ assert _strip_resource_warnings(result.stderr.lines) == []
+
+ stdout = result.stdout.str()
+ # Make sure first fail is ignore and second fail stops the test run.
+ assert "test_fail_on_flag FAILED" in stdout
+ assert "test_success_after_fail PASSED" in stdout
+ assert "test_fail_last_on_flag FAILED" in stdout
+ assert "test_success_after_last_fail" not in stdout
+
+
+def test_fail_on_errors(error_pytester: Pytester) -> None:
+ result = error_pytester.runpytest("-v", "--strict-markers", "--stepwise")
+
+ assert _strip_resource_warnings(result.stderr.lines) == []
+ stdout = result.stdout.str()
+
+ assert "test_error ERROR" in stdout
+ assert "test_success_after_fail" not in stdout
+
+
+def test_change_testfile(stepwise_pytester: Pytester) -> None:
+ result = stepwise_pytester.runpytest(
+ "-v", "--strict-markers", "--stepwise", "--fail", "test_a.py"
+ )
+ assert _strip_resource_warnings(result.stderr.lines) == []
+
+ stdout = result.stdout.str()
+ assert "test_fail_on_flag FAILED" in stdout
+
+ # Make sure the second test run starts from the beginning, since the
+ # test to continue from does not exist in testfile_b.
+ result = stepwise_pytester.runpytest(
+ "-v", "--strict-markers", "--stepwise", "test_b.py"
+ )
+ assert _strip_resource_warnings(result.stderr.lines) == []
+
+ stdout = result.stdout.str()
+ assert "test_success PASSED" in stdout
+
+
+@pytest.mark.parametrize("broken_first", [True, False])
+def test_stop_on_collection_errors(
+ broken_pytester: Pytester, broken_first: bool
+) -> None:
+ """Stop during collection errors. Broken test first or broken test last
+ actually surfaced a bug (#5444), so we test both situations."""
+ files = ["working_testfile.py", "broken_testfile.py"]
+ if broken_first:
+ files.reverse()
+ result = broken_pytester.runpytest("-v", "--strict-markers", "--stepwise", *files)
+ result.stdout.fnmatch_lines("*error during collection*")
+
+
+def test_xfail_handling(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
+ """Ensure normal xfail is ignored, and strict xfail interrupts the session in sw mode
+
+ (#5547)
+ """
+ monkeypatch.setattr("sys.dont_write_bytecode", True)
+
+ contents = """
+ import pytest
+ def test_a(): pass
+
+ @pytest.mark.xfail(strict={strict})
+ def test_b(): assert {assert_value}
+
+ def test_c(): pass
+ def test_d(): pass
+ """
+ pytester.makepyfile(contents.format(assert_value="0", strict="False"))
+ result = pytester.runpytest("--sw", "-v")
+ result.stdout.fnmatch_lines(
+ [
+ "*::test_a PASSED *",
+ "*::test_b XFAIL *",
+ "*::test_c PASSED *",
+ "*::test_d PASSED *",
+ "* 3 passed, 1 xfailed in *",
+ ]
+ )
+
+ pytester.makepyfile(contents.format(assert_value="1", strict="True"))
+ result = pytester.runpytest("--sw", "-v")
+ result.stdout.fnmatch_lines(
+ [
+ "*::test_a PASSED *",
+ "*::test_b FAILED *",
+ "* Interrupted*",
+ "* 1 failed, 1 passed in *",
+ ]
+ )
+
+ pytester.makepyfile(contents.format(assert_value="0", strict="True"))
+ result = pytester.runpytest("--sw", "-v")
+ result.stdout.fnmatch_lines(
+ [
+ "*::test_b XFAIL *",
+ "*::test_c PASSED *",
+ "*::test_d PASSED *",
+ "* 2 passed, 1 deselected, 1 xfailed in *",
+ ]
+ )
+
+
+def test_stepwise_skip_is_independent(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_one():
+ assert False
+
+ def test_two():
+ assert False
+
+ def test_three():
+ assert False
+
+ """
+ )
+ result = pytester.runpytest("--tb", "no", "--stepwise-skip")
+ result.assert_outcomes(failed=2)
+ result.stdout.fnmatch_lines(
+ [
+ "FAILED test_stepwise_skip_is_independent.py::test_one - assert False",
+ "FAILED test_stepwise_skip_is_independent.py::test_two - assert False",
+ "*Interrupted: Test failed, continuing from this test next run.*",
+ ]
+ )
+
+
+def test_sw_skip_help(pytester: Pytester) -> None:
+ result = pytester.runpytest("-h")
+ result.stdout.fnmatch_lines("*implicitly enables --stepwise.")
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_terminal.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_terminal.py
new file mode 100644
index 0000000000..23f597e332
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_terminal.py
@@ -0,0 +1,2486 @@
+"""Terminal reporting of the full testing process."""
+import collections
+import os
+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 Tuple
+
+import pluggy
+
+import _pytest.config
+import _pytest.terminal
+import pytest
+from _pytest._io.wcwidth import wcswidth
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pytester import Pytester
+from _pytest.reports import BaseReport
+from _pytest.reports import CollectReport
+from _pytest.reports import TestReport
+from _pytest.terminal import _folded_skips
+from _pytest.terminal import _format_trimmed
+from _pytest.terminal import _get_line_with_reprcrash_message
+from _pytest.terminal import _get_raw_skip_reason
+from _pytest.terminal import _plugin_nameversions
+from _pytest.terminal import getreportopt
+from _pytest.terminal import TerminalReporter
+
+DistInfo = collections.namedtuple("DistInfo", ["project_name", "version"])
+
+
+TRANS_FNMATCH = str.maketrans({"[": "[[]", "]": "[]]"})
+
+
+class Option:
+ def __init__(self, verbosity=0):
+ self.verbosity = verbosity
+
+ @property
+ def args(self):
+ values = []
+ values.append("--verbosity=%d" % self.verbosity)
+ return values
+
+
+@pytest.fixture(
+ params=[Option(verbosity=0), Option(verbosity=1), Option(verbosity=-1)],
+ ids=["default", "verbose", "quiet"],
+)
+def option(request):
+ return request.param
+
+
+@pytest.mark.parametrize(
+ "input,expected",
+ [
+ ([DistInfo(project_name="test", version=1)], ["test-1"]),
+ ([DistInfo(project_name="pytest-test", version=1)], ["test-1"]),
+ (
+ [
+ DistInfo(project_name="test", version=1),
+ DistInfo(project_name="test", version=1),
+ ],
+ ["test-1"],
+ ),
+ ],
+ ids=["normal", "prefix-strip", "deduplicate"],
+)
+def test_plugin_nameversion(input, expected):
+ pluginlist = [(None, x) for x in input]
+ result = _plugin_nameversions(pluginlist)
+ assert result == expected
+
+
+class TestTerminal:
+ def test_pass_skip_fail(self, pytester: Pytester, option) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_ok():
+ pass
+ def test_skip():
+ pytest.skip("xx")
+ def test_func():
+ assert 0
+ """
+ )
+ result = pytester.runpytest(*option.args)
+ if option.verbosity > 0:
+ result.stdout.fnmatch_lines(
+ [
+ "*test_pass_skip_fail.py::test_ok PASS*",
+ "*test_pass_skip_fail.py::test_skip SKIP*",
+ "*test_pass_skip_fail.py::test_func FAIL*",
+ ]
+ )
+ elif option.verbosity == 0:
+ result.stdout.fnmatch_lines(["*test_pass_skip_fail.py .sF*"])
+ else:
+ result.stdout.fnmatch_lines([".sF*"])
+ result.stdout.fnmatch_lines(
+ [" def test_func():", "> assert 0", "E assert 0"]
+ )
+
+ def test_internalerror(self, pytester: Pytester, linecomp) -> None:
+ modcol = pytester.getmodulecol("def test_one(): pass")
+ rep = TerminalReporter(modcol.config, file=linecomp.stringio)
+ with pytest.raises(ValueError) as excinfo:
+ raise ValueError("hello")
+ rep.pytest_internalerror(excinfo.getrepr())
+ linecomp.assert_contains_lines(["INTERNALERROR> *ValueError*hello*"])
+
+ def test_writeline(self, pytester: Pytester, linecomp) -> None:
+ modcol = pytester.getmodulecol("def test_one(): pass")
+ rep = TerminalReporter(modcol.config, file=linecomp.stringio)
+ rep.write_fspath_result(modcol.nodeid, ".")
+ rep.write_line("hello world")
+ lines = linecomp.stringio.getvalue().split("\n")
+ assert not lines[0]
+ assert lines[1].endswith(modcol.name + " .")
+ assert lines[2] == "hello world"
+
+ def test_show_runtest_logstart(self, pytester: Pytester, linecomp) -> None:
+ item = pytester.getitem("def test_func(): pass")
+ tr = TerminalReporter(item.config, file=linecomp.stringio)
+ item.config.pluginmanager.register(tr)
+ location = item.reportinfo()
+ tr.config.hook.pytest_runtest_logstart(
+ nodeid=item.nodeid, location=location, fspath=str(item.path)
+ )
+ linecomp.assert_contains_lines(["*test_show_runtest_logstart.py*"])
+
+ def test_runtest_location_shown_before_test_starts(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_1():
+ import time
+ time.sleep(20)
+ """
+ )
+ child = pytester.spawn_pytest("")
+ child.expect(".*test_runtest_location.*py")
+ child.sendeof()
+ child.kill(15)
+
+ def test_report_collect_after_half_a_second(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ """Test for "collecting" being updated after 0.5s"""
+
+ pytester.makepyfile(
+ **{
+ "test1.py": """
+ import _pytest.terminal
+
+ _pytest.terminal.REPORT_COLLECTING_RESOLUTION = 0
+
+ def test_1():
+ pass
+ """,
+ "test2.py": "def test_2(): pass",
+ }
+ )
+ # Explicitly test colored output.
+ monkeypatch.setenv("PY_COLORS", "1")
+
+ child = pytester.spawn_pytest("-v test1.py test2.py")
+ child.expect(r"collecting \.\.\.")
+ child.expect(r"collecting 1 item")
+ child.expect(r"collecting 2 items")
+ child.expect(r"collected 2 items")
+ rest = child.read().decode("utf8")
+ assert "= \x1b[32m\x1b[1m2 passed\x1b[0m\x1b[32m in" in rest
+
+ def test_itemreport_subclasses_show_subclassed_file(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ **{
+ "tests/test_p1": """
+ class BaseTests(object):
+ fail = False
+
+ def test_p1(self):
+ if self.fail: assert 0
+ """,
+ "tests/test_p2": """
+ from test_p1 import BaseTests
+
+ class TestMore(BaseTests): pass
+ """,
+ "tests/test_p3.py": """
+ from test_p1 import BaseTests
+
+ BaseTests.fail = True
+
+ class TestMore(BaseTests): pass
+ """,
+ }
+ )
+ result = pytester.runpytest("tests/test_p2.py", "--rootdir=tests")
+ result.stdout.fnmatch_lines(["tests/test_p2.py .*", "=* 1 passed in *"])
+
+ result = pytester.runpytest("-vv", "-rA", "tests/test_p2.py", "--rootdir=tests")
+ result.stdout.fnmatch_lines(
+ [
+ "tests/test_p2.py::TestMore::test_p1 <- test_p1.py PASSED *",
+ "*= short test summary info =*",
+ "PASSED tests/test_p2.py::TestMore::test_p1",
+ ]
+ )
+ result = pytester.runpytest("-vv", "-rA", "tests/test_p3.py", "--rootdir=tests")
+ result.stdout.fnmatch_lines(
+ [
+ "tests/test_p3.py::TestMore::test_p1 <- test_p1.py FAILED *",
+ "*_ TestMore.test_p1 _*",
+ " def test_p1(self):",
+ "> if self.fail: assert 0",
+ "E assert 0",
+ "",
+ "tests/test_p1.py:5: AssertionError",
+ "*= short test summary info =*",
+ "FAILED tests/test_p3.py::TestMore::test_p1 - assert 0",
+ "*= 1 failed in *",
+ ]
+ )
+
+ def test_itemreport_directclasses_not_shown_as_subclasses(
+ self, pytester: Pytester
+ ) -> None:
+ a = pytester.mkpydir("a123")
+ a.joinpath("test_hello123.py").write_text(
+ textwrap.dedent(
+ """\
+ class TestClass(object):
+ def test_method(self):
+ pass
+ """
+ )
+ )
+ result = pytester.runpytest("-vv")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*a123/test_hello123.py*PASS*"])
+ result.stdout.no_fnmatch_line("* <- *")
+
+ @pytest.mark.parametrize("fulltrace", ("", "--fulltrace"))
+ def test_keyboard_interrupt(self, pytester: Pytester, fulltrace) -> None:
+ pytester.makepyfile(
+ """
+ def test_foobar():
+ assert 0
+ def test_spamegg():
+ import py; pytest.skip('skip me please!')
+ def test_interrupt_me():
+ raise KeyboardInterrupt # simulating the user
+ """
+ )
+
+ result = pytester.runpytest(fulltrace, no_reraise_ctrlc=True)
+ result.stdout.fnmatch_lines(
+ [
+ " def test_foobar():",
+ "> assert 0",
+ "E assert 0",
+ "*_keyboard_interrupt.py:6: KeyboardInterrupt*",
+ ]
+ )
+ if fulltrace:
+ result.stdout.fnmatch_lines(
+ ["*raise KeyboardInterrupt # simulating the user*"]
+ )
+ else:
+ result.stdout.fnmatch_lines(
+ ["(to show a full traceback on KeyboardInterrupt use --full-trace)"]
+ )
+ result.stdout.fnmatch_lines(["*KeyboardInterrupt*"])
+
+ def test_keyboard_in_sessionstart(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_sessionstart():
+ raise KeyboardInterrupt
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_foobar():
+ pass
+ """
+ )
+
+ result = pytester.runpytest(no_reraise_ctrlc=True)
+ assert result.ret == 2
+ result.stdout.fnmatch_lines(["*KeyboardInterrupt*"])
+
+ def test_collect_single_item(self, pytester: Pytester) -> None:
+ """Use singular 'item' when reporting a single test item"""
+ pytester.makepyfile(
+ """
+ def test_foobar():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["collected 1 item"])
+
+ def test_rewrite(self, pytester: Pytester, monkeypatch) -> None:
+ config = pytester.parseconfig()
+ f = StringIO()
+ monkeypatch.setattr(f, "isatty", lambda *args: True)
+ tr = TerminalReporter(config, f)
+ tr._tw.fullwidth = 10
+ tr.write("hello")
+ tr.rewrite("hey", erase=True)
+ assert f.getvalue() == "hello" + "\r" + "hey" + (6 * " ")
+
+ def test_report_teststatus_explicit_markup(
+ self, monkeypatch: MonkeyPatch, pytester: Pytester, color_mapping
+ ) -> None:
+ """Test that TerminalReporter handles markup explicitly provided by
+ a pytest_report_teststatus hook."""
+ monkeypatch.setenv("PY_COLORS", "1")
+ pytester.makeconftest(
+ """
+ def pytest_report_teststatus(report):
+ return 'foo', 'F', ('FOO', {'red': True})
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_foobar():
+ pass
+ """
+ )
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(
+ color_mapping.format_for_fnmatch(["*{red}FOO{reset}*"])
+ )
+
+ def test_verbose_skip_reason(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.skip(reason="123")
+ def test_1():
+ pass
+
+ @pytest.mark.xfail(reason="456")
+ def test_2():
+ pass
+
+ @pytest.mark.xfail(reason="789")
+ def test_3():
+ assert False
+
+ @pytest.mark.xfail(reason="")
+ def test_4():
+ assert False
+
+ @pytest.mark.skip
+ def test_5():
+ pass
+
+ @pytest.mark.xfail
+ def test_6():
+ pass
+
+ def test_7():
+ pytest.skip()
+
+ def test_8():
+ pytest.skip("888 is great")
+
+ def test_9():
+ pytest.xfail()
+
+ def test_10():
+ pytest.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) *",
+ ]
+ )
+
+
+class TestCollectonly:
+ def test_collectonly_basic(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_func():
+ pass
+ """
+ )
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(
+ ["<Module test_collectonly_basic.py>", " <Function test_func>"]
+ )
+
+ def test_collectonly_skipped_module(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ pytest.skip("hello")
+ """
+ )
+ result = pytester.runpytest("--collect-only", "-rs")
+ result.stdout.fnmatch_lines(["*ERROR collecting*"])
+
+ def test_collectonly_displays_test_description(
+ self, pytester: Pytester, dummy_yaml_custom_test
+ ) -> None:
+ """Used dummy_yaml_custom_test for an Item without ``obj``."""
+ pytester.makepyfile(
+ """
+ def test_with_description():
+ ''' This test has a description.
+
+ more1.
+ more2.'''
+ """
+ )
+ 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.",
+ ],
+ consecutive=True,
+ )
+
+ def test_collectonly_failed_module(self, pytester: Pytester) -> None:
+ pytester.makepyfile("""raise ValueError(0)""")
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(["*raise ValueError*", "*1 error*"])
+
+ def test_collectonly_fatal(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_collectstart(collector):
+ assert 0, "urgs"
+ """
+ )
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines(["*INTERNAL*args*"])
+ assert result.ret == 3
+
+ def test_collectonly_simple(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ def test_func1():
+ pass
+ class TestClass(object):
+ def test_method(self):
+ pass
+ """
+ )
+ result = pytester.runpytest("--collect-only", p)
+ # assert stderr.startswith("inserting into sys.path")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(
+ [
+ "*<Module *.py>",
+ "* <Function test_func1>",
+ "* <Class TestClass>",
+ "* <Function test_method>",
+ ]
+ )
+
+ def test_collectonly_error(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile("import Errlkjqweqwe")
+ result = pytester.runpytest("--collect-only", p)
+ assert result.ret == 2
+ result.stdout.fnmatch_lines(
+ textwrap.dedent(
+ """\
+ *ERROR*
+ *ImportError*
+ *No module named *Errlk*
+ *1 error*
+ """
+ ).strip()
+ )
+
+ def test_collectonly_missing_path(self, pytester: Pytester) -> None:
+ """Issue 115: failure in parseargs will cause session not to
+ have the items attribute."""
+ result = pytester.runpytest("--collect-only", "uhm_missing_path")
+ assert result.ret == 4
+ result.stderr.fnmatch_lines(
+ ["*ERROR: file or directory not found: uhm_missing_path"]
+ )
+
+ def test_collectonly_quiet(self, pytester: Pytester) -> None:
+ pytester.makepyfile("def test_foo(): pass")
+ result = pytester.runpytest("--collect-only", "-q")
+ result.stdout.fnmatch_lines(["*test_foo*"])
+
+ def test_collectonly_more_quiet(self, pytester: Pytester) -> None:
+ pytester.makepyfile(test_fun="def test_foo(): pass")
+ result = pytester.runpytest("--collect-only", "-qq")
+ result.stdout.fnmatch_lines(["*test_fun.py: 1*"])
+
+ def test_collect_only_summary_status(self, pytester: Pytester) -> None:
+ """Custom status depending on test selection using -k or -m. #7701."""
+ pytester.makepyfile(
+ test_collect_foo="""
+ def test_foo(): pass
+ """,
+ test_collect_bar="""
+ def test_foobar(): pass
+ def test_bar(): pass
+ """,
+ )
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines("*== 3 tests collected in * ==*")
+
+ result = pytester.runpytest("--collect-only", "test_collect_foo.py")
+ result.stdout.fnmatch_lines("*== 1 test collected in * ==*")
+
+ result = pytester.runpytest("--collect-only", "-k", "foo")
+ result.stdout.fnmatch_lines("*== 2/3 tests collected (1 deselected) in * ==*")
+
+ result = pytester.runpytest("--collect-only", "-k", "test_bar")
+ result.stdout.fnmatch_lines("*== 1/3 tests collected (2 deselected) in * ==*")
+
+ result = pytester.runpytest("--collect-only", "-k", "invalid")
+ result.stdout.fnmatch_lines("*== no tests collected (3 deselected) in * ==*")
+
+ pytester.mkdir("no_tests_here")
+ result = pytester.runpytest("--collect-only", "no_tests_here")
+ result.stdout.fnmatch_lines("*== no tests collected in * ==*")
+
+ pytester.makepyfile(
+ test_contains_error="""
+ raise RuntimeError
+ """,
+ )
+ result = pytester.runpytest("--collect-only")
+ result.stdout.fnmatch_lines("*== 3 tests collected, 1 error in * ==*")
+ result = pytester.runpytest("--collect-only", "-k", "foo")
+ result.stdout.fnmatch_lines(
+ "*== 2/3 tests collected (1 deselected), 1 error in * ==*"
+ )
+
+
+class TestFixtureReporting:
+ def test_setup_fixture_error(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def setup_function(function):
+ print("setup func")
+ assert 0
+ def test_nada():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*ERROR at setup of test_nada*",
+ "*setup_function(function):*",
+ "*setup func*",
+ "*assert 0*",
+ "*1 error*",
+ ]
+ )
+ assert result.ret != 0
+
+ def test_teardown_fixture_error(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_nada():
+ pass
+ def teardown_function(function):
+ print("teardown func")
+ assert 0
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*ERROR at teardown*",
+ "*teardown_function(function):*",
+ "*assert 0*",
+ "*Captured stdout*",
+ "*teardown func*",
+ "*1 passed*1 error*",
+ ]
+ )
+
+ def test_teardown_fixture_error_and_test_failure(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_fail():
+ assert 0, "failingfunc"
+
+ def teardown_function(function):
+ print("teardown func")
+ assert False
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*ERROR at teardown of test_fail*",
+ "*teardown_function(function):*",
+ "*assert False*",
+ "*Captured stdout*",
+ "*teardown func*",
+ "*test_fail*",
+ "*def test_fail():",
+ "*failingfunc*",
+ "*1 failed*1 error*",
+ ]
+ )
+
+ def test_setup_teardown_output_and_test_failure(self, pytester: Pytester) -> None:
+ """Test for issue #442."""
+ pytester.makepyfile(
+ """
+ def setup_function(function):
+ print("setup func")
+
+ def test_fail():
+ assert 0, "failingfunc"
+
+ def teardown_function(function):
+ print("teardown func")
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*test_fail*",
+ "*def test_fail():",
+ "*failingfunc*",
+ "*Captured stdout setup*",
+ "*setup func*",
+ "*Captured stdout teardown*",
+ "*teardown func*",
+ "*1 failed*",
+ ]
+ )
+
+
+class TestTerminalFunctional:
+ def test_deselected(self, pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ def test_one():
+ pass
+ def test_two():
+ pass
+ def test_three():
+ pass
+ """
+ )
+ result = pytester.runpytest(
+ "-Wignore::pytest.PytestRemovedIn7Warning", "-k", "test_two:", 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:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.hookimpl(hookwrapper=True)
+ def pytest_collection_modifyitems(config, items):
+ yield
+ deselected = items.pop()
+ config.hook.pytest_deselected(items=[deselected])
+ """
+ )
+ testpath = pytester.makepyfile(
+ """
+ def test_one():
+ pass
+ def test_two():
+ pass
+ def test_three():
+ pass
+ """
+ )
+ result = pytester.runpytest(testpath)
+ result.stdout.fnmatch_lines(
+ [
+ "collected 3 items / 1 deselected / 2 selected",
+ "*= 2 passed, 1 deselected in*",
+ ]
+ )
+ assert result.ret == 0
+
+ def test_show_deselected_items_using_markexpr_before_test_execution(
+ self, pytester: Pytester
+ ) -> None:
+ pytester.makepyfile(
+ test_show_deselected="""
+ import pytest
+
+ @pytest.mark.foo
+ def test_foobar():
+ pass
+
+ @pytest.mark.bar
+ def test_bar():
+ pass
+
+ def test_pass():
+ pass
+ """
+ )
+ result = pytester.runpytest("-m", "not foo")
+ result.stdout.fnmatch_lines(
+ [
+ "collected 3 items / 1 deselected / 2 selected",
+ "*test_show_deselected.py ..*",
+ "*= 2 passed, 1 deselected in * =*",
+ ]
+ )
+ result.stdout.no_fnmatch_line("*= 1 deselected =*")
+ assert result.ret == 0
+
+ def test_no_skip_summary_if_failure(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def test_ok():
+ pass
+ def test_fail():
+ assert 0
+ def test_skip():
+ pytest.skip("dontshow")
+ """
+ )
+ result = pytester.runpytest()
+ assert result.stdout.str().find("skip test summary") == -1
+ assert result.ret == 1
+
+ def test_passes(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def test_passes():
+ pass
+ class TestClass(object):
+ def test_method(self):
+ pass
+ """
+ )
+ old = p1.parent
+ pytester.chdir()
+ try:
+ result = pytester.runpytest()
+ finally:
+ os.chdir(old)
+ result.stdout.fnmatch_lines(["test_passes.py ..*", "* 2 pass*"])
+ assert result.ret == 0
+
+ def test_header_trailer_info(
+ self, monkeypatch: MonkeyPatch, pytester: Pytester, request
+ ) -> None:
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
+ pytester.makepyfile(
+ """
+ def test_passes():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ verinfo = ".".join(map(str, sys.version_info[:3]))
+ result.stdout.fnmatch_lines(
+ [
+ "*===== test session starts ====*",
+ "platform %s -- Python %s*pytest-%s**pluggy-%s"
+ % (
+ sys.platform,
+ verinfo,
+ pytest.__version__,
+ pluggy.__version__,
+ ),
+ "*test_header_trailer_info.py .*",
+ "=* 1 passed*in *.[0-9][0-9]s *=",
+ ]
+ )
+ if request.config.pluginmanager.list_plugin_distinfo():
+ result.stdout.fnmatch_lines(["plugins: *"])
+
+ def test_no_header_trailer_info(
+ self, monkeypatch: MonkeyPatch, pytester: Pytester, request
+ ) -> None:
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
+ pytester.makepyfile(
+ """
+ def test_passes():
+ pass
+ """
+ )
+ 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__,
+ )
+ )
+ if request.config.pluginmanager.list_plugin_distinfo():
+ result.stdout.no_fnmatch_line("plugins: *")
+
+ def test_header(self, pytester: Pytester) -> None:
+ pytester.path.joinpath("tests").mkdir()
+ pytester.path.joinpath("gui").mkdir()
+
+ # no ini file
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["rootdir: *test_header0"])
+
+ # with configfile
+ pytester.makeini("""[pytest]""")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"])
+
+ # with testpaths option, and not passing anything in the command-line
+ pytester.makeini(
+ """
+ [pytest]
+ testpaths = tests gui
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ ["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"])
+
+ def test_header_absolute_testpath(
+ self, pytester: Pytester, monkeypatch: MonkeyPatch
+ ) -> None:
+ """Regresstion test for #7814."""
+ tests = pytester.path.joinpath("tests")
+ tests.mkdir()
+ pytester.makepyprojecttoml(
+ """
+ [tool.pytest.ini_options]
+ testpaths = ['{}']
+ """.format(
+ tests
+ )
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "rootdir: *absolute_testpath0, configfile: pyproject.toml, testpaths: {}".format(
+ tests
+ )
+ ]
+ )
+
+ def test_no_header(self, pytester: Pytester) -> None:
+ pytester.path.joinpath("tests").mkdir()
+ pytester.path.joinpath("gui").mkdir()
+
+ # with testpaths option, and not passing anything in the command-line
+ pytester.makeini(
+ """
+ [pytest]
+ testpaths = tests gui
+ """
+ )
+ result = pytester.runpytest("--no-header")
+ result.stdout.no_fnmatch_line(
+ "rootdir: *test_header0, inifile: tox.ini, testpaths: tests, gui"
+ )
+
+ # with testpaths option, passing directory in command-line: do not show testpaths then
+ result = pytester.runpytest("tests", "--no-header")
+ result.stdout.no_fnmatch_line("rootdir: *test_header0, inifile: tox.ini")
+
+ def test_no_summary(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def test_no_summary():
+ assert false
+ """
+ )
+ result = pytester.runpytest(p1, "--no-summary")
+ result.stdout.no_fnmatch_line("*= FAILURES =*")
+
+ def test_showlocals(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def test_showlocals():
+ x = 3
+ y = "x" * 5000
+ assert 0
+ """
+ )
+ result = pytester.runpytest(p1, "-l")
+ result.stdout.fnmatch_lines(
+ [
+ # "_ _ * Locals *",
+ "x* = 3",
+ "y* = 'xxxxxx*",
+ ]
+ )
+
+ def test_showlocals_short(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def test_showlocals_short():
+ x = 3
+ y = "xxxx"
+ assert 0
+ """
+ )
+ result = pytester.runpytest(p1, "-l", "--tb=short")
+ result.stdout.fnmatch_lines(
+ [
+ "test_showlocals_short.py:*",
+ " assert 0",
+ "E assert 0",
+ " x = 3",
+ " y = 'xxxx'",
+ ]
+ )
+
+ @pytest.fixture
+ def verbose_testfile(self, pytester: Pytester) -> Path:
+ return pytester.makepyfile(
+ """
+ import pytest
+ def test_fail():
+ raise ValueError()
+ def test_pass():
+ pass
+ class TestClass(object):
+ def test_skip(self):
+ pytest.skip("hello")
+ def test_gen():
+ def check(x):
+ assert x == 1
+ yield check, 0
+ """
+ )
+
+ def test_verbose_reporting(self, verbose_testfile, pytester: Pytester) -> None:
+ result = pytester.runpytest(
+ verbose_testfile, "-v", "-Walways::pytest.PytestWarning"
+ )
+ result.stdout.fnmatch_lines(
+ [
+ "*test_verbose_reporting.py::test_fail *FAIL*",
+ "*test_verbose_reporting.py::test_pass *PASS*",
+ "*test_verbose_reporting.py::TestClass::test_skip *SKIP*",
+ "*test_verbose_reporting.py::test_gen *XFAIL*",
+ ]
+ )
+ assert result.ret == 1
+
+ def test_verbose_reporting_xdist(
+ self,
+ verbose_testfile,
+ monkeypatch: MonkeyPatch,
+ pytester: Pytester,
+ pytestconfig,
+ ) -> None:
+ if not pytestconfig.pluginmanager.get_plugin("xdist"):
+ pytest.skip("xdist plugin not installed")
+
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
+ result = pytester.runpytest(
+ verbose_testfile, "-v", "-n 1", "-Walways::pytest.PytestWarning"
+ )
+ result.stdout.fnmatch_lines(
+ ["*FAIL*test_verbose_reporting_xdist.py::test_fail*"]
+ )
+ assert result.ret == 1
+
+ def test_quiet_reporting(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile("def test_pass(): pass")
+ result = pytester.runpytest(p1, "-q")
+ s = result.stdout.str()
+ assert "test session starts" not in s
+ assert p1.name not in s
+ assert "===" not in s
+ assert "passed" in s
+
+ def test_more_quiet_reporting(self, pytester: Pytester) -> None:
+ p1 = pytester.makepyfile("def test_pass(): pass")
+ result = pytester.runpytest(p1, "-qq")
+ s = result.stdout.str()
+ assert "test session starts" not in s
+ assert p1.name not in s
+ assert "===" not in s
+ assert "passed" not in s
+
+ @pytest.mark.parametrize(
+ "params", [(), ("--collect-only",)], ids=["no-params", "collect-only"]
+ )
+ def test_report_collectionfinish_hook(self, pytester: Pytester, params) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_report_collectionfinish(config, start_path, items):
+ return [f'hello from hook: {len(items)} items']
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize('i', range(3))
+ def test(i):
+ pass
+ """
+ )
+ result = pytester.runpytest(*params)
+ result.stdout.fnmatch_lines(["collected 3 items", "hello from hook: 3 items"])
+
+ def test_summary_f_alias(self, pytester: Pytester) -> None:
+ """Test that 'f' and 'F' report chars are aliases and don't show up twice in the summary (#6334)"""
+ pytester.makepyfile(
+ """
+ def test():
+ assert False
+ """
+ )
+ result = pytester.runpytest("-rfF")
+ expected = "FAILED test_summary_f_alias.py::test - assert False"
+ result.stdout.fnmatch_lines([expected])
+ assert result.stdout.lines.count(expected) == 1
+
+ def test_summary_s_alias(self, pytester: Pytester) -> None:
+ """Test that 's' and 'S' report chars are aliases and don't show up twice in the summary"""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.skip
+ def test():
+ pass
+ """
+ )
+ result = pytester.runpytest("-rsS")
+ expected = "SKIPPED [1] test_summary_s_alias.py:3: unconditional skip"
+ result.stdout.fnmatch_lines([expected])
+ assert result.stdout.lines.count(expected) == 1
+
+
+def test_fail_extra_reporting(pytester: Pytester, monkeypatch) -> None:
+ monkeypatch.setenv("COLUMNS", "80")
+ pytester.makepyfile("def test_this(): assert 0, 'this_failed' * 100")
+ result = pytester.runpytest("-rN")
+ result.stdout.no_fnmatch_line("*short test summary*")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*test summary*",
+ "FAILED test_fail_extra_reporting.py::test_this - AssertionError: this_failedt...",
+ ]
+ )
+
+
+def test_fail_reporting_on_pass(pytester: Pytester) -> None:
+ pytester.makepyfile("def test_this(): assert 1")
+ result = pytester.runpytest("-rf")
+ result.stdout.no_fnmatch_line("*short test summary*")
+
+
+def test_pass_extra_reporting(pytester: Pytester) -> None:
+ pytester.makepyfile("def test_this(): assert 1")
+ result = pytester.runpytest()
+ result.stdout.no_fnmatch_line("*short test summary*")
+ result = pytester.runpytest("-rp")
+ result.stdout.fnmatch_lines(["*test summary*", "PASS*test_pass_extra_reporting*"])
+
+
+def test_pass_reporting_on_fail(pytester: Pytester) -> None:
+ pytester.makepyfile("def test_this(): assert 0")
+ result = pytester.runpytest("-rp")
+ result.stdout.no_fnmatch_line("*short test summary*")
+
+
+def test_pass_output_reporting(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def setup_module():
+ print("setup_module")
+
+ def teardown_module():
+ print("teardown_module")
+
+ def test_pass_has_output():
+ print("Four score and seven years ago...")
+
+ def test_pass_no_output():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ s = result.stdout.str()
+ assert "test_pass_has_output" not in s
+ assert "Four score and seven years ago..." not in s
+ assert "test_pass_no_output" not in s
+ result = pytester.runpytest("-rPp")
+ result.stdout.fnmatch_lines(
+ [
+ "*= PASSES =*",
+ "*_ test_pass_has_output _*",
+ "*- Captured stdout setup -*",
+ "setup_module",
+ "*- Captured stdout call -*",
+ "Four score and seven years ago...",
+ "*- Captured stdout teardown -*",
+ "teardown_module",
+ "*= short test summary info =*",
+ "PASSED test_pass_output_reporting.py::test_pass_has_output",
+ "PASSED test_pass_output_reporting.py::test_pass_no_output",
+ "*= 2 passed in *",
+ ]
+ )
+
+
+def test_color_yes(pytester: Pytester, color_mapping) -> None:
+ p1 = pytester.makepyfile(
+ """
+ def fail():
+ assert 0
+
+ def test_this():
+ fail()
+ """
+ )
+ result = pytester.runpytest("--color=yes", str(p1))
+ result.stdout.fnmatch_lines(
+ color_mapping.format_for_fnmatch(
+ [
+ "{bold}=*= test session starts =*={reset}",
+ "collected 1 item",
+ "",
+ "test_color_yes.py {red}F{reset}{red} * [100%]{reset}",
+ "",
+ "=*= FAILURES =*=",
+ "{red}{bold}_*_ test_this _*_{reset}",
+ "",
+ " {kw}def{hl-reset} {function}test_this{hl-reset}():",
+ "> fail()",
+ "",
+ "{bold}{red}test_color_yes.py{reset}:5: ",
+ "_ _ * _ _*",
+ "",
+ " {kw}def{hl-reset} {function}fail{hl-reset}():",
+ "> {kw}assert{hl-reset} {number}0{hl-reset}",
+ "{bold}{red}E assert 0{reset}",
+ "",
+ "{bold}{red}test_color_yes.py{reset}:2: AssertionError",
+ "{red}=*= {red}{bold}1 failed{reset}{red} in *s{reset}{red} =*={reset}",
+ ]
+ )
+ )
+ result = pytester.runpytest("--color=yes", "--tb=short", str(p1))
+ result.stdout.fnmatch_lines(
+ color_mapping.format_for_fnmatch(
+ [
+ "{bold}=*= test session starts =*={reset}",
+ "collected 1 item",
+ "",
+ "test_color_yes.py {red}F{reset}{red} * [100%]{reset}",
+ "",
+ "=*= FAILURES =*=",
+ "{red}{bold}_*_ test_this _*_{reset}",
+ "{bold}{red}test_color_yes.py{reset}:5: in test_this",
+ " fail()",
+ "{bold}{red}test_color_yes.py{reset}:2: in fail",
+ " {kw}assert{hl-reset} {number}0{hl-reset}",
+ "{bold}{red}E assert 0{reset}",
+ "{red}=*= {red}{bold}1 failed{reset}{red} in *s{reset}{red} =*={reset}",
+ ]
+ )
+ )
+
+
+def test_color_no(pytester: Pytester) -> None:
+ pytester.makepyfile("def test_this(): assert 1")
+ result = pytester.runpytest("--color=no")
+ assert "test session starts" in result.stdout.str()
+ result.stdout.no_fnmatch_line("*\x1b[1m*")
+
+
+@pytest.mark.parametrize("verbose", [True, False])
+def test_color_yes_collection_on_non_atty(pytester: Pytester, verbose) -> None:
+ """#1397: Skip collect progress report when working on non-terminals."""
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize('i', range(10))
+ def test_this(i):
+ assert 1
+ """
+ )
+ args = ["--color=yes"]
+ if verbose:
+ args.append("-vv")
+ result = pytester.runpytest(*args)
+ assert "test session starts" in result.stdout.str()
+ assert "\x1b[1m" in result.stdout.str()
+ result.stdout.no_fnmatch_line("*collecting 10 items*")
+ if verbose:
+ assert "collecting ..." in result.stdout.str()
+ assert "collected 10 items" in result.stdout.str()
+
+
+def test_getreportopt() -> None:
+ from _pytest.terminal import _REPORTCHARS_DEFAULT
+
+ class FakeConfig:
+ class Option:
+ reportchars = _REPORTCHARS_DEFAULT
+ disable_warnings = False
+
+ option = Option()
+
+ config = cast(Config, FakeConfig())
+
+ assert _REPORTCHARS_DEFAULT == "fE"
+
+ # Default.
+ assert getreportopt(config) == "wfE"
+
+ config.option.reportchars = "sf"
+ assert getreportopt(config) == "wsf"
+
+ config.option.reportchars = "sfxw"
+ assert getreportopt(config) == "sfxw"
+
+ config.option.reportchars = "a"
+ assert getreportopt(config) == "wsxXEf"
+
+ config.option.reportchars = "N"
+ assert getreportopt(config) == "w"
+
+ config.option.reportchars = "NwfE"
+ assert getreportopt(config) == "wfE"
+
+ config.option.reportchars = "NfENx"
+ assert getreportopt(config) == "wx"
+
+ # Now with --disable-warnings.
+ config.option.disable_warnings = True
+ config.option.reportchars = "a"
+ assert getreportopt(config) == "sxXEf"
+
+ config.option.reportchars = "sfx"
+ assert getreportopt(config) == "sfx"
+
+ config.option.reportchars = "sfxw"
+ assert getreportopt(config) == "sfx"
+
+ config.option.reportchars = "a"
+ assert getreportopt(config) == "sxXEf"
+
+ config.option.reportchars = "A"
+ assert getreportopt(config) == "PpsxXEf"
+
+ config.option.reportchars = "AN"
+ assert getreportopt(config) == ""
+
+ config.option.reportchars = "NwfE"
+ assert getreportopt(config) == "fE"
+
+
+def test_terminalreporter_reportopt_addopts(pytester: Pytester) -> None:
+ pytester.makeini("[pytest]\naddopts=-rs")
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def tr(request):
+ tr = request.config.pluginmanager.getplugin("terminalreporter")
+ return tr
+ def test_opt(tr):
+ assert tr.hasopt('skipped')
+ assert not tr.hasopt('qwe')
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+
+def test_tbstyle_short(pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def arg(request):
+ return 42
+ def test_opt(arg):
+ x = 0
+ assert x
+ """
+ )
+ result = pytester.runpytest("--tb=short")
+ s = result.stdout.str()
+ assert "arg = 42" not in s
+ assert "x = 0" not in s
+ result.stdout.fnmatch_lines(["*%s:8*" % p.name, " assert x", "E assert*"])
+ result = pytester.runpytest()
+ s = result.stdout.str()
+ assert "x = 0" in s
+ assert "assert x" in s
+
+
+def test_traceconfig(pytester: Pytester) -> None:
+ result = pytester.runpytest("--traceconfig")
+ result.stdout.fnmatch_lines(["*active plugins*"])
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+
+class TestGenericReporting:
+ """Test class which can be subclassed with a different option provider to
+ run e.g. distributed tests."""
+
+ def test_collect_fail(self, pytester: Pytester, option) -> None:
+ pytester.makepyfile("import xyz\n")
+ result = pytester.runpytest(*option.args)
+ result.stdout.fnmatch_lines(
+ ["ImportError while importing*", "*No module named *xyz*", "*1 error*"]
+ )
+
+ def test_maxfailures(self, pytester: Pytester, option) -> None:
+ pytester.makepyfile(
+ """
+ def test_1():
+ assert 0
+ def test_2():
+ assert 0
+ def test_3():
+ assert 0
+ """
+ )
+ result = pytester.runpytest("--maxfail=2", *option.args)
+ result.stdout.fnmatch_lines(
+ [
+ "*def test_1():*",
+ "*def test_2():*",
+ "*! stopping after 2 failures !*",
+ "*2 failed*",
+ ]
+ )
+
+ def test_maxfailures_with_interrupted(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test(request):
+ request.session.shouldstop = "session_interrupted"
+ assert 0
+ """
+ )
+ result = pytester.runpytest("--maxfail=1", "-ra")
+ result.stdout.fnmatch_lines(
+ [
+ "*= short test summary info =*",
+ "FAILED *",
+ "*! stopping after 1 failures !*",
+ "*! session_interrupted !*",
+ "*= 1 failed in*",
+ ]
+ )
+
+ def test_tb_option(self, pytester: Pytester, option) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ def g():
+ raise IndexError
+ def test_func():
+ print(6*7)
+ g() # --calling--
+ """
+ )
+ for tbopt in ["long", "short", "no"]:
+ print("testing --tb=%s..." % tbopt)
+ result = pytester.runpytest("-rN", "--tb=%s" % tbopt)
+ s = result.stdout.str()
+ if tbopt == "long":
+ assert "print(6*7)" in s
+ else:
+ assert "print(6*7)" not in s
+ if tbopt != "no":
+ assert "--calling--" in s
+ assert "IndexError" in s
+ else:
+ assert "FAILURES" not in s
+ assert "--calling--" not in s
+ assert "IndexError" not in s
+
+ def test_tb_crashline(self, pytester: Pytester, option) -> None:
+ p = pytester.makepyfile(
+ """
+ import pytest
+ def g():
+ raise IndexError
+ def test_func1():
+ print(6*7)
+ g() # --calling--
+ def test_func2():
+ assert 0, "hello"
+ """
+ )
+ result = pytester.runpytest("--tb=line")
+ bn = p.name
+ result.stdout.fnmatch_lines(
+ ["*%s:3: IndexError*" % bn, "*%s:8: AssertionError: hello*" % bn]
+ )
+ s = result.stdout.str()
+ assert "def test_func2" not in s
+
+ def test_pytest_report_header(self, pytester: Pytester, option) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_sessionstart(session):
+ session.config._somevalue = 42
+ def pytest_report_header(config):
+ return "hello: %s" % config._somevalue
+ """
+ )
+ pytester.mkdir("a").joinpath("conftest.py").write_text(
+ """
+def pytest_report_header(config, start_path):
+ return ["line1", str(start_path)]
+"""
+ )
+ result = pytester.runpytest("a")
+ result.stdout.fnmatch_lines(["*hello: 42*", "line1", str(pytester.path)])
+
+ def test_show_capture(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import sys
+ import logging
+ def test_one():
+ sys.stdout.write('!This is stdout!')
+ sys.stderr.write('!This is stderr!')
+ logging.warning('!This is a warning log msg!')
+ assert False, 'Something failed'
+ """
+ )
+
+ result = pytester.runpytest("--tb=short")
+ result.stdout.fnmatch_lines(
+ [
+ "!This is stdout!",
+ "!This is stderr!",
+ "*WARNING*!This is a warning log msg!",
+ ]
+ )
+
+ result = pytester.runpytest("--show-capture=all", "--tb=short")
+ result.stdout.fnmatch_lines(
+ [
+ "!This is stdout!",
+ "!This is stderr!",
+ "*WARNING*!This is a warning log msg!",
+ ]
+ )
+
+ stdout = pytester.runpytest("--show-capture=stdout", "--tb=short").stdout.str()
+ assert "!This is stderr!" not in stdout
+ assert "!This is stdout!" in stdout
+ assert "!This is a warning log msg!" not in stdout
+
+ stdout = pytester.runpytest("--show-capture=stderr", "--tb=short").stdout.str()
+ assert "!This is stdout!" not in stdout
+ assert "!This is stderr!" in stdout
+ assert "!This is a warning log msg!" not in stdout
+
+ stdout = pytester.runpytest("--show-capture=log", "--tb=short").stdout.str()
+ assert "!This is stdout!" not in stdout
+ assert "!This is stderr!" not in stdout
+ assert "!This is a warning log msg!" in stdout
+
+ stdout = pytester.runpytest("--show-capture=no", "--tb=short").stdout.str()
+ assert "!This is stdout!" not in stdout
+ assert "!This is stderr!" not in stdout
+ assert "!This is a warning log msg!" not in stdout
+
+ def test_show_capture_with_teardown_logs(self, pytester: Pytester) -> None:
+ """Ensure that the capturing of teardown logs honor --show-capture setting"""
+ pytester.makepyfile(
+ """
+ import logging
+ import sys
+ import pytest
+
+ @pytest.fixture(scope="function", autouse="True")
+ def hook_each_test(request):
+ yield
+ sys.stdout.write("!stdout!")
+ sys.stderr.write("!stderr!")
+ logging.warning("!log!")
+
+ def test_func():
+ assert False
+ """
+ )
+
+ result = pytester.runpytest("--show-capture=stdout", "--tb=short").stdout.str()
+ assert "!stdout!" in result
+ assert "!stderr!" not in result
+ assert "!log!" not in result
+
+ result = pytester.runpytest("--show-capture=stderr", "--tb=short").stdout.str()
+ assert "!stdout!" not in result
+ assert "!stderr!" in result
+ assert "!log!" not in result
+
+ result = pytester.runpytest("--show-capture=log", "--tb=short").stdout.str()
+ assert "!stdout!" not in result
+ assert "!stderr!" not in result
+ assert "!log!" in result
+
+ result = pytester.runpytest("--show-capture=no", "--tb=short").stdout.str()
+ assert "!stdout!" not in result
+ assert "!stderr!" not in result
+ assert "!log!" not in result
+
+
+@pytest.mark.xfail("not hasattr(os, 'dup')")
+def test_fdopen_kept_alive_issue124(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import os, sys
+ k = []
+ def test_open_file_and_keep_alive(capfd):
+ stdout = os.fdopen(1, 'w', 1)
+ k.append(stdout)
+
+ def test_close_kept_alive_file():
+ stdout = k.pop()
+ stdout.close()
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+
+def test_tbstyle_native_setup_error(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture
+ def setup_error_fixture():
+ raise Exception("error in exception")
+
+ def test_error_fixture(setup_error_fixture):
+ pass
+ """
+ )
+ result = pytester.runpytest("--tb=native")
+ result.stdout.fnmatch_lines(
+ ['*File *test_tbstyle_native_setup_error.py", line *, in setup_error_fixture*']
+ )
+
+
+def test_terminal_summary(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_terminal_summary(terminalreporter, exitstatus):
+ w = terminalreporter
+ w.section("hello")
+ w.line("world")
+ w.line("exitstatus: {0}".format(exitstatus))
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ """
+ *==== hello ====*
+ world
+ exitstatus: 5
+ """
+ )
+
+
+@pytest.mark.filterwarnings("default::UserWarning")
+def test_terminal_summary_warnings_are_displayed(pytester: Pytester) -> None:
+ """Test that warnings emitted during pytest_terminal_summary are displayed.
+ (#1305).
+ """
+ pytester.makeconftest(
+ """
+ import warnings
+ def pytest_terminal_summary(terminalreporter):
+ warnings.warn(UserWarning('internal warning'))
+ """
+ )
+ pytester.makepyfile(
+ """
+ def test_failure():
+ import warnings
+ warnings.warn("warning_from_" + "test")
+ assert 0
+ """
+ )
+ result = pytester.runpytest("-ra")
+ result.stdout.fnmatch_lines(
+ [
+ "*= warnings summary =*",
+ "*warning_from_test*",
+ "*= short test summary info =*",
+ "*= warnings summary (final) =*",
+ "*conftest.py:3:*internal warning",
+ "*== 1 failed, 2 warnings in *",
+ ]
+ )
+ result.stdout.no_fnmatch_line("*None*")
+ stdout = result.stdout.str()
+ assert stdout.count("warning_from_test") == 1
+ assert stdout.count("=== warnings summary ") == 2
+
+
+@pytest.mark.filterwarnings("default::UserWarning")
+def test_terminal_summary_warnings_header_once(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_failure():
+ import warnings
+ warnings.warn("warning_from_" + "test")
+ assert 0
+ """
+ )
+ result = pytester.runpytest("-ra")
+ result.stdout.fnmatch_lines(
+ [
+ "*= warnings summary =*",
+ "*warning_from_test*",
+ "*= short test summary info =*",
+ "*== 1 failed, 1 warning in *",
+ ]
+ )
+ result.stdout.no_fnmatch_line("*None*")
+ stdout = result.stdout.str()
+ assert stdout.count("warning_from_test") == 1
+ assert stdout.count("=== warnings summary ") == 1
+
+
+@pytest.mark.filterwarnings("default")
+def test_terminal_no_summary_warnings_header_once(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ def test_failure():
+ import warnings
+ warnings.warn("warning_from_" + "test")
+ assert 0
+ """
+ )
+ result = pytester.runpytest("--no-summary")
+ result.stdout.no_fnmatch_line("*= warnings summary =*")
+ result.stdout.no_fnmatch_line("*= short test summary info =*")
+
+
+@pytest.fixture(scope="session")
+def tr() -> TerminalReporter:
+ config = _pytest.config._prepareconfig()
+ return TerminalReporter(config)
+
+
+@pytest.mark.parametrize(
+ "exp_color, exp_line, stats_arg",
+ [
+ # The method under test only cares about the length of each
+ # dict value, not the actual contents, so tuples of anything
+ # suffice
+ # Important statuses -- the highest priority of these always wins
+ ("red", [("1 failed", {"bold": True, "red": True})], {"failed": [1]}),
+ (
+ "red",
+ [
+ ("1 failed", {"bold": True, "red": True}),
+ ("1 passed", {"bold": False, "green": True}),
+ ],
+ {"failed": [1], "passed": [1]},
+ ),
+ ("red", [("1 error", {"bold": True, "red": True})], {"error": [1]}),
+ ("red", [("2 errors", {"bold": True, "red": True})], {"error": [1, 2]}),
+ (
+ "red",
+ [
+ ("1 passed", {"bold": False, "green": True}),
+ ("1 error", {"bold": True, "red": True}),
+ ],
+ {"error": [1], "passed": [1]},
+ ),
+ # (a status that's not known to the code)
+ ("yellow", [("1 weird", {"bold": True, "yellow": True})], {"weird": [1]}),
+ (
+ "yellow",
+ [
+ ("1 passed", {"bold": False, "green": True}),
+ ("1 weird", {"bold": True, "yellow": True}),
+ ],
+ {"weird": [1], "passed": [1]},
+ ),
+ ("yellow", [("1 warning", {"bold": True, "yellow": True})], {"warnings": [1]}),
+ (
+ "yellow",
+ [
+ ("1 passed", {"bold": False, "green": True}),
+ ("1 warning", {"bold": True, "yellow": True}),
+ ],
+ {"warnings": [1], "passed": [1]},
+ ),
+ (
+ "green",
+ [("5 passed", {"bold": True, "green": True})],
+ {"passed": [1, 2, 3, 4, 5]},
+ ),
+ # "Boring" statuses. These have no effect on the color of the summary
+ # line. Thus, if *every* test has a boring status, the summary line stays
+ # at its default color, i.e. yellow, to warn the user that the test run
+ # produced no useful information
+ ("yellow", [("1 skipped", {"bold": True, "yellow": True})], {"skipped": [1]}),
+ (
+ "green",
+ [
+ ("1 passed", {"bold": True, "green": True}),
+ ("1 skipped", {"bold": False, "yellow": True}),
+ ],
+ {"skipped": [1], "passed": [1]},
+ ),
+ (
+ "yellow",
+ [("1 deselected", {"bold": True, "yellow": True})],
+ {"deselected": [1]},
+ ),
+ (
+ "green",
+ [
+ ("1 passed", {"bold": True, "green": True}),
+ ("1 deselected", {"bold": False, "yellow": True}),
+ ],
+ {"deselected": [1], "passed": [1]},
+ ),
+ ("yellow", [("1 xfailed", {"bold": True, "yellow": True})], {"xfailed": [1]}),
+ (
+ "green",
+ [
+ ("1 passed", {"bold": True, "green": True}),
+ ("1 xfailed", {"bold": False, "yellow": True}),
+ ],
+ {"xfailed": [1], "passed": [1]},
+ ),
+ ("yellow", [("1 xpassed", {"bold": True, "yellow": True})], {"xpassed": [1]}),
+ (
+ "yellow",
+ [
+ ("1 passed", {"bold": False, "green": True}),
+ ("1 xpassed", {"bold": True, "yellow": True}),
+ ],
+ {"xpassed": [1], "passed": [1]},
+ ),
+ # Likewise if no tests were found at all
+ ("yellow", [("no tests ran", {"yellow": True})], {}),
+ # Test the empty-key special case
+ ("yellow", [("no tests ran", {"yellow": True})], {"": [1]}),
+ (
+ "green",
+ [("1 passed", {"bold": True, "green": True})],
+ {"": [1], "passed": [1]},
+ ),
+ # A couple more complex combinations
+ (
+ "red",
+ [
+ ("1 failed", {"bold": True, "red": True}),
+ ("2 passed", {"bold": False, "green": True}),
+ ("3 xfailed", {"bold": False, "yellow": True}),
+ ],
+ {"passed": [1, 2], "failed": [1], "xfailed": [1, 2, 3]},
+ ),
+ (
+ "green",
+ [
+ ("1 passed", {"bold": True, "green": True}),
+ ("2 skipped", {"bold": False, "yellow": True}),
+ ("3 deselected", {"bold": False, "yellow": True}),
+ ("2 xfailed", {"bold": False, "yellow": True}),
+ ],
+ {
+ "passed": [1],
+ "skipped": [1, 2],
+ "deselected": [1, 2, 3],
+ "xfailed": [1, 2],
+ },
+ ),
+ ],
+)
+def test_summary_stats(
+ tr: TerminalReporter,
+ exp_line: List[Tuple[str, Dict[str, bool]]],
+ exp_color: str,
+ stats_arg: Dict[str, List[object]],
+) -> None:
+ tr.stats = stats_arg
+
+ # Fake "_is_last_item" to be True.
+ class fake_session:
+ testscollected = 0
+
+ tr._session = fake_session # type: ignore[assignment]
+ assert tr._is_last_item
+
+ # Reset cache.
+ tr._main_color = None
+
+ print("Based on stats: %s" % stats_arg)
+ print(f'Expect summary: "{exp_line}"; with color "{exp_color}"')
+ (line, color) = tr.build_summary_stats_line()
+ print(f'Actually got: "{line}"; with color "{color}"')
+ assert line == exp_line
+ assert color == exp_color
+
+
+def test_skip_counting_towards_summary(tr):
+ class DummyReport(BaseReport):
+ count_towards_summary = True
+
+ r1 = DummyReport()
+ r2 = DummyReport()
+ tr.stats = {"failed": (r1, r2)}
+ tr._main_color = None
+ res = tr.build_summary_stats_line()
+ assert res == ([("2 failed", {"bold": True, "red": True})], "red")
+
+ r1.count_towards_summary = False
+ tr.stats = {"failed": (r1, r2)}
+ tr._main_color = None
+ res = tr.build_summary_stats_line()
+ assert res == ([("1 failed", {"bold": True, "red": True})], "red")
+
+
+class TestClassicOutputStyle:
+ """Ensure classic output style works as expected (#3883)"""
+
+ @pytest.fixture
+ def test_files(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ **{
+ "test_one.py": "def test_one(): pass",
+ "test_two.py": "def test_two(): assert 0",
+ "sub/test_three.py": """
+ def test_three_1(): pass
+ def test_three_2(): assert 0
+ def test_three_3(): pass
+ """,
+ }
+ )
+
+ def test_normal_verbosity(self, pytester: Pytester, test_files) -> None:
+ result = pytester.runpytest("-o", "console_output_style=classic")
+ result.stdout.fnmatch_lines(
+ [
+ "test_one.py .",
+ "test_two.py F",
+ f"sub{os.sep}test_three.py .F.",
+ "*2 failed, 3 passed in*",
+ ]
+ )
+
+ def test_verbose(self, pytester: Pytester, test_files) -> None:
+ 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",
+ "*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*"])
+
+
+class TestProgressOutputStyle:
+ @pytest.fixture
+ def many_tests_files(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_bar="""
+ import pytest
+ @pytest.mark.parametrize('i', range(10))
+ def test_bar(i): pass
+ """,
+ test_foo="""
+ import pytest
+ @pytest.mark.parametrize('i', range(5))
+ def test_foo(i): pass
+ """,
+ test_foobar="""
+ import pytest
+ @pytest.mark.parametrize('i', range(5))
+ def test_foobar(i): pass
+ """,
+ )
+
+ def test_zero_tests_collected(self, pytester: Pytester) -> None:
+ """Some plugins (testmon for example) might issue pytest_runtest_logreport without any tests being
+ actually collected (#2971)."""
+ pytester.makeconftest(
+ """
+ def pytest_collection_modifyitems(items, config):
+ from _pytest.runner import CollectReport
+ for node_id in ('nodeid1', 'nodeid2'):
+ rep = CollectReport(node_id, 'passed', None, None)
+ rep.when = 'passed'
+ rep.duration = 0.1
+ config.hook.pytest_runtest_logreport(report=rep)
+ """
+ )
+ output = pytester.runpytest()
+ output.stdout.no_fnmatch_line("*ZeroDivisionError*")
+ output.stdout.fnmatch_lines(["=* 2 passed in *="])
+
+ def test_normal(self, many_tests_files, pytester: Pytester) -> None:
+ output = pytester.runpytest()
+ 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%\]",
+ ]
+ )
+
+ def test_colored_progress(
+ self, pytester: Pytester, monkeypatch, color_mapping
+ ) -> None:
+ monkeypatch.setenv("PY_COLORS", "1")
+ pytester.makepyfile(
+ test_axfail="""
+ import pytest
+ @pytest.mark.xfail
+ def test_axfail(): assert 0
+ """,
+ test_bar="""
+ import pytest
+ @pytest.mark.parametrize('i', range(10))
+ def test_bar(i): pass
+ """,
+ test_foo="""
+ import pytest
+ import warnings
+ @pytest.mark.parametrize('i', range(5))
+ def test_foo(i):
+ warnings.warn(DeprecationWarning("collection"))
+ pass
+ """,
+ test_foobar="""
+ import pytest
+ @pytest.mark.parametrize('i', range(5))
+ def test_foobar(i): raise ValueError()
+ """,
+ )
+ result = pytester.runpytest()
+ result.stdout.re_match_lines(
+ color_mapping.format_for_rematch(
+ [
+ r"test_axfail.py {yellow}x{reset}{green} \s+ \[ 4%\]{reset}",
+ r"test_bar.py ({green}\.{reset}){{10}}{green} \s+ \[ 52%\]{reset}",
+ r"test_foo.py ({green}\.{reset}){{5}}{yellow} \s+ \[ 76%\]{reset}",
+ r"test_foobar.py ({red}F{reset}){{5}}{red} \s+ \[100%\]{reset}",
+ ]
+ )
+ )
+
+ # Only xfail should have yellow progress indicator.
+ result = pytester.runpytest("test_axfail.py")
+ result.stdout.re_match_lines(
+ color_mapping.format_for_rematch(
+ [
+ r"test_axfail.py {yellow}x{reset}{yellow} \s+ \[100%\]{reset}",
+ r"^{yellow}=+ ({yellow}{bold}|{bold}{yellow})1 xfailed{reset}{yellow} in ",
+ ]
+ )
+ )
+
+ def test_count(self, many_tests_files, pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ console_output_style = count
+ """
+ )
+ output = pytester.runpytest()
+ output.stdout.re_match_lines(
+ [
+ r"test_bar.py \.{10} \s+ \[10/20\]",
+ r"test_foo.py \.{5} \s+ \[15/20\]",
+ r"test_foobar.py \.{5} \s+ \[20/20\]",
+ ]
+ )
+
+ def test_verbose(self, many_tests_files, pytester: Pytester) -> None:
+ output = pytester.runpytest("-v")
+ output.stdout.re_match_lines(
+ [
+ r"test_bar.py::test_bar\[0\] PASSED \s+ \[ 5%\]",
+ r"test_foo.py::test_foo\[4\] PASSED \s+ \[ 75%\]",
+ r"test_foobar.py::test_foobar\[4\] PASSED \s+ \[100%\]",
+ ]
+ )
+
+ def test_verbose_count(self, many_tests_files, pytester: Pytester) -> None:
+ pytester.makeini(
+ """
+ [pytest]
+ console_output_style = count
+ """
+ )
+ output = pytester.runpytest("-v")
+ output.stdout.re_match_lines(
+ [
+ r"test_bar.py::test_bar\[0\] PASSED \s+ \[ 1/20\]",
+ r"test_foo.py::test_foo\[4\] PASSED \s+ \[15/20\]",
+ r"test_foobar.py::test_foobar\[4\] PASSED \s+ \[20/20\]",
+ ]
+ )
+
+ def test_xdist_normal(
+ self, many_tests_files, pytester: Pytester, monkeypatch
+ ) -> None:
+ pytest.importorskip("xdist")
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
+ output = pytester.runpytest("-n2")
+ output.stdout.re_match_lines([r"\.{20} \s+ \[100%\]"])
+
+ def test_xdist_normal_count(
+ self, many_tests_files, pytester: Pytester, monkeypatch
+ ) -> None:
+ pytest.importorskip("xdist")
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
+ pytester.makeini(
+ """
+ [pytest]
+ console_output_style = count
+ """
+ )
+ output = pytester.runpytest("-n2")
+ output.stdout.re_match_lines([r"\.{20} \s+ \[20/20\]"])
+
+ def test_xdist_verbose(
+ self, many_tests_files, pytester: Pytester, monkeypatch
+ ) -> None:
+ pytest.importorskip("xdist")
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
+ output = pytester.runpytest("-n2", "-v")
+ output.stdout.re_match_lines_random(
+ [
+ r"\[gw\d\] \[\s*\d+%\] PASSED test_bar.py::test_bar\[1\]",
+ r"\[gw\d\] \[\s*\d+%\] PASSED test_foo.py::test_foo\[1\]",
+ r"\[gw\d\] \[\s*\d+%\] PASSED test_foobar.py::test_foobar\[1\]",
+ ]
+ )
+ output.stdout.fnmatch_lines_random(
+ [
+ line.translate(TRANS_FNMATCH)
+ for line in [
+ "test_bar.py::test_bar[0] ",
+ "test_foo.py::test_foo[0] ",
+ "test_foobar.py::test_foobar[0] ",
+ "[gw?] [ 5%] PASSED test_*[?] ",
+ "[gw?] [ 10%] PASSED test_*[?] ",
+ "[gw?] [ 55%] PASSED test_*[?] ",
+ "[gw?] [ 60%] PASSED test_*[?] ",
+ "[gw?] [ 95%] PASSED test_*[?] ",
+ "[gw?] [100%] PASSED test_*[?] ",
+ ]
+ ]
+ )
+
+ def test_capture_no(self, many_tests_files, pytester: Pytester) -> None:
+ output = pytester.runpytest("-s")
+ output.stdout.re_match_lines(
+ [r"test_bar.py \.{10}", r"test_foo.py \.{5}", r"test_foobar.py \.{5}"]
+ )
+
+ output = pytester.runpytest("--capture=no")
+ output.stdout.no_fnmatch_line("*%]*")
+
+
+class TestProgressWithTeardown:
+ """Ensure we show the correct percentages for tests that fail during teardown (#3088)"""
+
+ @pytest.fixture
+ def contest_with_teardown_fixture(self, pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture
+ def fail_teardown():
+ yield
+ assert False
+ """
+ )
+
+ @pytest.fixture
+ def many_files(self, pytester: Pytester, contest_with_teardown_fixture) -> None:
+ pytester.makepyfile(
+ test_bar="""
+ import pytest
+ @pytest.mark.parametrize('i', range(5))
+ def test_bar(fail_teardown, i):
+ pass
+ """,
+ test_foo="""
+ import pytest
+ @pytest.mark.parametrize('i', range(15))
+ def test_foo(fail_teardown, i):
+ pass
+ """,
+ )
+
+ def test_teardown_simple(
+ self, pytester: Pytester, contest_with_teardown_fixture
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_foo(fail_teardown):
+ pass
+ """
+ )
+ output = pytester.runpytest()
+ output.stdout.re_match_lines([r"test_teardown_simple.py \.E\s+\[100%\]"])
+
+ def test_teardown_with_test_also_failing(
+ self, pytester: Pytester, contest_with_teardown_fixture
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_foo(fail_teardown):
+ assert 0
+ """
+ )
+ output = pytester.runpytest("-rfE")
+ output.stdout.re_match_lines(
+ [
+ r"test_teardown_with_test_also_failing.py FE\s+\[100%\]",
+ "FAILED test_teardown_with_test_also_failing.py::test_foo - assert 0",
+ "ERROR test_teardown_with_test_also_failing.py::test_foo - assert False",
+ ]
+ )
+
+ def test_teardown_many(self, pytester: Pytester, many_files) -> None:
+ output = pytester.runpytest()
+ output.stdout.re_match_lines(
+ [r"test_bar.py (\.E){5}\s+\[ 25%\]", r"test_foo.py (\.E){15}\s+\[100%\]"]
+ )
+
+ def test_teardown_many_verbose(
+ self, pytester: Pytester, many_files, color_mapping
+ ) -> None:
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(
+ color_mapping.format_for_fnmatch(
+ [
+ "test_bar.py::test_bar[0] PASSED * [ 5%]",
+ "test_bar.py::test_bar[0] ERROR * [ 5%]",
+ "test_bar.py::test_bar[4] PASSED * [ 25%]",
+ "test_foo.py::test_foo[14] PASSED * [100%]",
+ "test_foo.py::test_foo[14] ERROR * [100%]",
+ "=* 20 passed, 20 errors in *",
+ ]
+ )
+ )
+
+ def test_xdist_normal(self, many_files, pytester: Pytester, monkeypatch) -> None:
+ pytest.importorskip("xdist")
+ monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
+ output = pytester.runpytest("-n2")
+ output.stdout.re_match_lines([r"[\.E]{40} \s+ \[100%\]"])
+
+
+def test_skip_reasons_folding() -> None:
+ path = "xyz"
+ lineno = 3
+ message = "justso"
+ longrepr = (path, lineno, message)
+
+ class X:
+ pass
+
+ ev1 = cast(CollectReport, X())
+ ev1.when = "execute"
+ ev1.skipped = True # type: ignore[misc]
+ ev1.longrepr = longrepr
+
+ ev2 = cast(CollectReport, X())
+ ev2.when = "execute"
+ ev2.longrepr = longrepr
+ ev2.skipped = True # type: ignore[misc]
+
+ # ev3 might be a collection report
+ ev3 = cast(CollectReport, X())
+ ev3.when = "collect"
+ ev3.longrepr = longrepr
+ ev3.skipped = True # type: ignore[misc]
+
+ values = _folded_skips(Path.cwd(), [ev1, ev2, ev3])
+ assert len(values) == 1
+ num, fspath, lineno_, reason = values[0]
+ assert num == 3
+ assert fspath == path
+ assert lineno_ == lineno
+ assert reason == message
+
+
+def test_line_with_reprcrash(monkeypatch: MonkeyPatch) -> None:
+ mocked_verbose_word = "FAILED"
+
+ mocked_pos = "some::nodeid"
+
+ def mock_get_pos(*args):
+ return mocked_pos
+
+ monkeypatch.setattr(_pytest.terminal, "_get_pos", mock_get_pos)
+
+ class config:
+ pass
+
+ class rep:
+ def _get_verbose_word(self, *args):
+ return mocked_verbose_word
+
+ class longrepr:
+ class reprcrash:
+ pass
+
+ def check(msg, width, expected):
+ __tracebackhide__ = True
+ if msg:
+ rep.longrepr.reprcrash.message = msg # type: ignore
+ actual = _get_line_with_reprcrash_message(config, rep(), width) # type: ignore
+
+ assert actual == expected
+ if actual != f"{mocked_verbose_word} {mocked_pos}":
+ assert len(actual) <= width
+ assert wcswidth(actual) <= width
+
+ # AttributeError with message
+ check(None, 80, "FAILED some::nodeid")
+
+ check("msg", 80, "FAILED some::nodeid - msg")
+ check("msg", 3, "FAILED some::nodeid")
+
+ check("msg", 24, "FAILED some::nodeid")
+ check("msg", 25, "FAILED some::nodeid - msg")
+
+ check("some longer msg", 24, "FAILED some::nodeid")
+ check("some longer msg", 25, "FAILED some::nodeid - ...")
+ check("some longer msg", 26, "FAILED some::nodeid - s...")
+
+ check("some\nmessage", 25, "FAILED some::nodeid - ...")
+ check("some\nmessage", 26, "FAILED some::nodeid - some")
+ check("some\nmessage", 80, "FAILED some::nodeid - some")
+
+ # Test unicode safety.
+ check("🉐🉐🉐🉐🉐\n2nd line", 25, "FAILED some::nodeid - ...")
+ check("🉐🉐🉐🉐🉐\n2nd line", 26, "FAILED some::nodeid - ...")
+ check("🉐🉐🉐🉐🉐\n2nd line", 27, "FAILED some::nodeid - 🉐...")
+ check("🉐🉐🉐🉐🉐\n2nd line", 28, "FAILED some::nodeid - 🉐...")
+ check("🉐🉐🉐🉐🉐\n2nd line", 29, "FAILED some::nodeid - 🉐🉐...")
+
+ # NOTE: constructed, not sure if this is supported.
+ mocked_pos = "nodeid::🉐::withunicode"
+ check("🉐🉐🉐🉐🉐\n2nd line", 29, "FAILED nodeid::🉐::withunicode")
+ check("🉐🉐🉐🉐🉐\n2nd line", 40, "FAILED nodeid::🉐::withunicode - 🉐🉐...")
+ check("🉐🉐🉐🉐🉐\n2nd line", 41, "FAILED nodeid::🉐::withunicode - 🉐🉐...")
+ check("🉐🉐🉐🉐🉐\n2nd line", 42, "FAILED nodeid::🉐::withunicode - 🉐🉐🉐...")
+ check("🉐🉐🉐🉐🉐\n2nd line", 80, "FAILED nodeid::🉐::withunicode - 🉐🉐🉐🉐🉐")
+
+
+@pytest.mark.parametrize(
+ "seconds, expected",
+ [
+ (10.0, "10.00s"),
+ (10.34, "10.34s"),
+ (59.99, "59.99s"),
+ (60.55, "60.55s (0:01:00)"),
+ (123.55, "123.55s (0:02:03)"),
+ (60 * 60 + 0.5, "3600.50s (1:00:00)"),
+ ],
+)
+def test_format_session_duration(seconds, expected):
+ from _pytest.terminal import format_session_duration
+
+ assert format_session_duration(seconds) == expected
+
+
+def test_collecterror(pytester: Pytester) -> None:
+ p1 = pytester.makepyfile("raise SyntaxError()")
+ result = pytester.runpytest("-ra", str(p1))
+ result.stdout.fnmatch_lines(
+ [
+ "collected 0 items / 1 error",
+ "*= ERRORS =*",
+ "*_ ERROR collecting test_collecterror.py _*",
+ "E SyntaxError: *",
+ "*= short test summary info =*",
+ "ERROR test_collecterror.py",
+ "*! Interrupted: 1 error during collection !*",
+ "*= 1 error in *",
+ ]
+ )
+
+
+def test_no_summary_collecterror(pytester: Pytester) -> None:
+ p1 = pytester.makepyfile("raise SyntaxError()")
+ result = pytester.runpytest("-ra", "--no-summary", str(p1))
+ result.stdout.no_fnmatch_line("*= ERRORS =*")
+
+
+def test_via_exec(pytester: Pytester) -> None:
+ p1 = pytester.makepyfile("exec('def test_via_exec(): pass')")
+ result = pytester.runpytest(str(p1), "-vv")
+ result.stdout.fnmatch_lines(
+ ["test_via_exec.py::test_via_exec <- <string> PASSED*", "*= 1 passed in *"]
+ )
+
+
+class TestCodeHighlight:
+ def test_code_highlight_simple(self, pytester: Pytester, color_mapping) -> None:
+ pytester.makepyfile(
+ """
+ def test_foo():
+ assert 1 == 10
+ """
+ )
+ result = pytester.runpytest("--color=yes")
+ 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}",
+ "{bold}{red}E assert 1 == 10{reset}",
+ ]
+ )
+ )
+
+ def test_code_highlight_continuation(
+ self, pytester: Pytester, color_mapping
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_foo():
+ print('''
+ '''); assert 0
+ """
+ )
+ result = pytester.runpytest("--color=yes")
+
+ result.stdout.fnmatch_lines(
+ color_mapping.format_for_fnmatch(
+ [
+ " {kw}def{hl-reset} {function}test_foo{hl-reset}():",
+ " {print}print{hl-reset}({str}'''{hl-reset}{str}{hl-reset}",
+ "> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}",
+ "{bold}{red}E assert 0{reset}",
+ ]
+ )
+ )
+
+ def test_code_highlight_custom_theme(
+ self, pytester: Pytester, color_mapping, monkeypatch: MonkeyPatch
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_foo():
+ assert 1 == 10
+ """
+ )
+ monkeypatch.setenv("PYTEST_THEME", "solarized-dark")
+ monkeypatch.setenv("PYTEST_THEME_MODE", "dark")
+ result = pytester.runpytest("--color=yes")
+ 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}",
+ "{bold}{red}E assert 1 == 10{reset}",
+ ]
+ )
+ )
+
+ def test_code_highlight_invalid_theme(
+ self, pytester: Pytester, color_mapping, monkeypatch: MonkeyPatch
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_foo():
+ assert 1 == 10
+ """
+ )
+ monkeypatch.setenv("PYTEST_THEME", "invalid")
+ result = pytester.runpytest_subprocess("--color=yes")
+ result.stderr.fnmatch_lines(
+ "ERROR: PYTEST_THEME environment variable had an invalid value: 'invalid'. "
+ "Only valid pygment styles are allowed."
+ )
+
+ def test_code_highlight_invalid_theme_mode(
+ self, pytester: Pytester, color_mapping, monkeypatch: MonkeyPatch
+ ) -> None:
+ pytester.makepyfile(
+ """
+ def test_foo():
+ assert 1 == 10
+ """
+ )
+ monkeypatch.setenv("PYTEST_THEME_MODE", "invalid")
+ result = pytester.runpytest_subprocess("--color=yes")
+ result.stderr.fnmatch_lines(
+ "ERROR: PYTEST_THEME_MODE environment variable had an invalid value: 'invalid'. "
+ "The only allowed values are 'dark' and 'light'."
+ )
+
+
+def test_raw_skip_reason_skipped() -> None:
+ report = SimpleNamespace()
+ report.skipped = True
+ report.longrepr = ("xyz", 3, "Skipped: Just so")
+
+ reason = _get_raw_skip_reason(cast(TestReport, report))
+ assert reason == "Just so"
+
+
+def test_raw_skip_reason_xfail() -> None:
+ report = SimpleNamespace()
+ report.wasxfail = "reason: To everything there is a season"
+
+ reason = _get_raw_skip_reason(cast(TestReport, report))
+ assert reason == "To everything there is a season"
+
+
+def test_format_trimmed() -> None:
+ msg = "unconditional skip"
+
+ assert _format_trimmed(" ({}) ", msg, len(msg) + 4) == " (unconditional skip) "
+ assert _format_trimmed(" ({}) ", msg, len(msg) + 3) == " (unconditional ...) "
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_threadexception.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_threadexception.py
new file mode 100644
index 0000000000..5b7519f27d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_threadexception.py
@@ -0,0 +1,137 @@
+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)
+
+
+@pytest.mark.filterwarnings("default::pytest.PytestUnhandledThreadExceptionWarning")
+def test_unhandled_thread_exception(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_it="""
+ import threading
+
+ def test_it():
+ def oops():
+ raise ValueError("Oops")
+
+ t = threading.Thread(target=oops, name="MyThread")
+ t.start()
+ t.join()
+
+ def test_2(): pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+ assert result.parseoutcomes() == {"passed": 2, "warnings": 1}
+ result.stdout.fnmatch_lines(
+ [
+ "*= warnings summary =*",
+ "test_it.py::test_it",
+ " * PytestUnhandledThreadExceptionWarning: Exception in thread MyThread",
+ " ",
+ " Traceback (most recent call last):",
+ " ValueError: Oops",
+ " ",
+ " warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))",
+ ]
+ )
+
+
+@pytest.mark.filterwarnings("default::pytest.PytestUnhandledThreadExceptionWarning")
+def test_unhandled_thread_exception_in_setup(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_it="""
+ import threading
+ import pytest
+
+ @pytest.fixture
+ def threadexc():
+ def oops():
+ raise ValueError("Oops")
+ t = threading.Thread(target=oops, name="MyThread")
+ t.start()
+ t.join()
+
+ def test_it(threadexc): pass
+ def test_2(): pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+ assert result.parseoutcomes() == {"passed": 2, "warnings": 1}
+ result.stdout.fnmatch_lines(
+ [
+ "*= warnings summary =*",
+ "test_it.py::test_it",
+ " * PytestUnhandledThreadExceptionWarning: Exception in thread MyThread",
+ " ",
+ " Traceback (most recent call last):",
+ " ValueError: Oops",
+ " ",
+ " warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))",
+ ]
+ )
+
+
+@pytest.mark.filterwarnings("default::pytest.PytestUnhandledThreadExceptionWarning")
+def test_unhandled_thread_exception_in_teardown(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_it="""
+ import threading
+ import pytest
+
+ @pytest.fixture
+ def threadexc():
+ def oops():
+ raise ValueError("Oops")
+ yield
+ t = threading.Thread(target=oops, name="MyThread")
+ t.start()
+ t.join()
+
+ def test_it(threadexc): pass
+ def test_2(): pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+ assert result.parseoutcomes() == {"passed": 2, "warnings": 1}
+ result.stdout.fnmatch_lines(
+ [
+ "*= warnings summary =*",
+ "test_it.py::test_it",
+ " * PytestUnhandledThreadExceptionWarning: Exception in thread MyThread",
+ " ",
+ " Traceback (most recent call last):",
+ " ValueError: Oops",
+ " ",
+ " warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))",
+ ]
+ )
+
+
+@pytest.mark.filterwarnings("error::pytest.PytestUnhandledThreadExceptionWarning")
+def test_unhandled_thread_exception_warning_error(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_it="""
+ import threading
+ import pytest
+
+ def test_it():
+ def oops():
+ raise ValueError("Oops")
+ t = threading.Thread(target=oops, name="MyThread")
+ t.start()
+ t.join()
+
+ def test_2(): pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == pytest.ExitCode.TESTS_FAILED
+ assert result.parseoutcomes() == {"passed": 1, "failed": 1}
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_tmpdir.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_tmpdir.py
new file mode 100644
index 0000000000..4f7c538470
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_tmpdir.py
@@ -0,0 +1,480 @@
+import os
+import stat
+import sys
+import warnings
+from pathlib import Path
+from typing import Callable
+from typing import cast
+from typing import List
+
+import attr
+
+import pytest
+from _pytest import pathlib
+from _pytest.config import Config
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.pathlib import cleanup_numbered_dir
+from _pytest.pathlib import create_cleanup_lock
+from _pytest.pathlib import make_numbered_dir
+from _pytest.pathlib import maybe_delete_a_numbered_dir
+from _pytest.pathlib import on_rm_rf_error
+from _pytest.pathlib import register_cleanup_lock_removal
+from _pytest.pathlib import rm_rf
+from _pytest.pytester import Pytester
+from _pytest.tmpdir import get_user
+from _pytest.tmpdir import TempPathFactory
+
+
+def test_tmp_path_fixture(pytester: Pytester) -> None:
+ p = pytester.copy_example("tmpdir/tmp_path_fixture.py")
+ results = pytester.runpytest(p)
+ results.stdout.fnmatch_lines(["*1 passed*"])
+
+
+@attr.s
+class FakeConfig:
+ basetemp = attr.ib()
+
+ @property
+ def trace(self):
+ return self
+
+ def get(self, key):
+ return lambda *k: None
+
+ @property
+ def option(self):
+ return self
+
+
+class TestTmpPathHandler:
+ def test_mktemp(self, tmp_path):
+ config = cast(Config, FakeConfig(tmp_path))
+ t = TempPathFactory.from_config(config, _ispytest=True)
+ tmp = t.mktemp("world")
+ assert str(tmp.relative_to(t.getbasetemp())) == "world0"
+ tmp = t.mktemp("this")
+ assert str(tmp.relative_to(t.getbasetemp())).startswith("this")
+ tmp2 = t.mktemp("this")
+ assert str(tmp2.relative_to(t.getbasetemp())).startswith("this")
+ assert tmp2 != tmp
+
+ def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch):
+ """#4425"""
+ monkeypatch.chdir(tmp_path)
+ config = cast(Config, FakeConfig("hello"))
+ t = TempPathFactory.from_config(config, _ispytest=True)
+ assert t.getbasetemp().resolve() == (tmp_path / "hello").resolve()
+
+
+class TestConfigTmpPath:
+ def test_getbasetemp_custom_removes_old(self, pytester: Pytester) -> None:
+ mytemp = pytester.path.joinpath("xyz")
+ p = pytester.makepyfile(
+ """
+ def test_1(tmp_path):
+ pass
+ """
+ )
+ pytester.runpytest(p, "--basetemp=%s" % mytemp)
+ assert mytemp.exists()
+ mytemp.joinpath("hello").touch()
+
+ pytester.runpytest(p, "--basetemp=%s" % mytemp)
+ assert mytemp.exists()
+ assert not mytemp.joinpath("hello").exists()
+
+
+testdata = [
+ ("mypath", True),
+ ("/mypath1", False),
+ ("./mypath1", True),
+ ("../mypath3", False),
+ ("../../mypath4", False),
+ ("mypath5/..", False),
+ ("mypath6/../mypath6", True),
+ ("mypath7/../mypath7/..", False),
+]
+
+
+@pytest.mark.parametrize("basename, is_ok", testdata)
+def test_mktemp(pytester: Pytester, basename: str, is_ok: bool) -> None:
+ mytemp = pytester.mkdir("mytemp")
+ p = pytester.makepyfile(
+ """
+ def test_abs_path(tmp_path_factory):
+ tmp_path_factory.mktemp('{}', numbered=False)
+ """.format(
+ basename
+ )
+ )
+
+ result = pytester.runpytest(p, "--basetemp=%s" % mytemp)
+ if is_ok:
+ assert result.ret == 0
+ assert mytemp.joinpath(basename).exists()
+ else:
+ assert result.ret == 1
+ result.stdout.fnmatch_lines("*ValueError*")
+
+
+def test_tmp_path_always_is_realpath(pytester: Pytester, monkeypatch) -> None:
+ # the reason why tmp_path should be a realpath is that
+ # when you cd to it and do "os.getcwd()" you will anyway
+ # get the realpath. Using the symlinked path can thus
+ # easily result in path-inequality
+ # XXX if that proves to be a problem, consider using
+ # os.environ["PWD"]
+ realtemp = pytester.mkdir("myrealtemp")
+ linktemp = pytester.path.joinpath("symlinktemp")
+ attempt_symlink_to(linktemp, str(realtemp))
+ monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(linktemp))
+ pytester.makepyfile(
+ """
+ def test_1(tmp_path):
+ assert tmp_path.resolve() == tmp_path
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+def test_tmp_path_too_long_on_parametrization(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.mark.parametrize("arg", ["1"*1000])
+ def test_some(arg, tmp_path):
+ tmp_path.joinpath("hello").touch()
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+def test_tmp_path_factory(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import pytest
+ @pytest.fixture(scope='session')
+ def session_dir(tmp_path_factory):
+ return tmp_path_factory.mktemp('data', numbered=False)
+ def test_some(session_dir):
+ assert session_dir.is_dir()
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+def test_tmp_path_fallback_tox_env(pytester: Pytester, monkeypatch) -> None:
+ """Test that tmp_path works even if environment variables required by getpass
+ module are missing (#1010).
+ """
+ monkeypatch.delenv("USER", raising=False)
+ monkeypatch.delenv("USERNAME", raising=False)
+ pytester.makepyfile(
+ """
+ def test_some(tmp_path):
+ assert tmp_path.is_dir()
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+@pytest.fixture
+def break_getuser(monkeypatch):
+ monkeypatch.setattr("os.getuid", lambda: -1)
+ # taken from python 2.7/3.4
+ for envvar in ("LOGNAME", "USER", "LNAME", "USERNAME"):
+ monkeypatch.delenv(envvar, raising=False)
+
+
+@pytest.mark.usefixtures("break_getuser")
+@pytest.mark.skipif(sys.platform.startswith("win"), reason="no os.getuid on windows")
+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):
+ assert tmp_path.is_dir()
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+@pytest.mark.usefixtures("break_getuser")
+@pytest.mark.skipif(sys.platform.startswith("win"), reason="no os.getuid on windows")
+def test_get_user_uid_not_found():
+ """Test that get_user() function works even if the current process's
+ user id does not correspond to a valid user (e.g. running pytest in a
+ Docker container with 'docker run -u'.
+ """
+ assert get_user() is None
+
+
+@pytest.mark.skipif(not sys.platform.startswith("win"), reason="win only")
+def test_get_user(monkeypatch):
+ """Test that get_user() function works even if environment variables
+ required by getpass module are missing from the environment on Windows
+ (#1010).
+ """
+ monkeypatch.delenv("USER", raising=False)
+ monkeypatch.delenv("USERNAME", raising=False)
+ assert get_user() is None
+
+
+class TestNumberedDir:
+ PREFIX = "fun-"
+
+ def test_make(self, tmp_path):
+ for i in range(10):
+ d = make_numbered_dir(root=tmp_path, prefix=self.PREFIX)
+ assert d.name.startswith(self.PREFIX)
+ assert d.name.endswith(str(i))
+
+ symlink = tmp_path.joinpath(self.PREFIX + "current")
+ if symlink.exists():
+ # unix
+ assert symlink.is_symlink()
+ assert symlink.resolve() == d.resolve()
+
+ def test_cleanup_lock_create(self, tmp_path):
+ d = tmp_path.joinpath("test")
+ d.mkdir()
+ lockfile = create_cleanup_lock(d)
+ with pytest.raises(OSError, match="cannot create lockfile in .*"):
+ create_cleanup_lock(d)
+
+ lockfile.unlink()
+
+ def test_lock_register_cleanup_removal(self, tmp_path: Path) -> None:
+ lock = create_cleanup_lock(tmp_path)
+
+ registry: List[Callable[..., None]] = []
+ register_cleanup_lock_removal(lock, register=registry.append)
+
+ (cleanup_func,) = registry
+
+ assert lock.is_file()
+
+ cleanup_func(original_pid="intentionally_different")
+
+ assert lock.is_file()
+
+ cleanup_func()
+
+ assert not lock.exists()
+
+ cleanup_func()
+
+ assert not lock.exists()
+
+ def _do_cleanup(self, tmp_path: Path) -> None:
+ self.test_make(tmp_path)
+ cleanup_numbered_dir(
+ root=tmp_path,
+ prefix=self.PREFIX,
+ keep=2,
+ consider_lock_dead_if_created_before=0,
+ )
+
+ def test_cleanup_keep(self, tmp_path):
+ self._do_cleanup(tmp_path)
+ a, b = (x for x in tmp_path.iterdir() if not x.is_symlink())
+ print(a, b)
+
+ def test_cleanup_locked(self, tmp_path):
+ p = make_numbered_dir(root=tmp_path, prefix=self.PREFIX)
+
+ create_cleanup_lock(p)
+
+ assert not pathlib.ensure_deletable(
+ p, consider_lock_dead_if_created_before=p.stat().st_mtime - 1
+ )
+ assert pathlib.ensure_deletable(
+ p, consider_lock_dead_if_created_before=p.stat().st_mtime + 1
+ )
+
+ def test_cleanup_ignores_symlink(self, tmp_path):
+ the_symlink = tmp_path / (self.PREFIX + "current")
+ attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5"))
+ self._do_cleanup(tmp_path)
+
+ def test_removal_accepts_lock(self, tmp_path):
+ folder = make_numbered_dir(root=tmp_path, prefix=self.PREFIX)
+ create_cleanup_lock(folder)
+ maybe_delete_a_numbered_dir(folder)
+ assert folder.is_dir()
+
+
+class TestRmRf:
+ def test_rm_rf(self, tmp_path):
+ adir = tmp_path / "adir"
+ adir.mkdir()
+ rm_rf(adir)
+
+ assert not adir.exists()
+
+ adir.mkdir()
+ afile = adir / "afile"
+ afile.write_bytes(b"aa")
+
+ rm_rf(adir)
+ assert not adir.exists()
+
+ def test_rm_rf_with_read_only_file(self, tmp_path):
+ """Ensure rm_rf can remove directories with read-only files in them (#5524)"""
+ fn = tmp_path / "dir/foo.txt"
+ fn.parent.mkdir()
+
+ fn.touch()
+
+ self.chmod_r(fn)
+
+ rm_rf(fn.parent)
+
+ assert not fn.parent.is_dir()
+
+ def chmod_r(self, path):
+ mode = os.stat(str(path)).st_mode
+ os.chmod(str(path), mode & ~stat.S_IWRITE)
+
+ def test_rm_rf_with_read_only_directory(self, tmp_path):
+ """Ensure rm_rf can remove read-only directories (#5524)"""
+ adir = tmp_path / "dir"
+ adir.mkdir()
+
+ (adir / "foo.txt").touch()
+ self.chmod_r(adir)
+
+ rm_rf(adir)
+
+ assert not adir.is_dir()
+
+ def test_on_rm_rf_error(self, tmp_path: Path) -> None:
+ adir = tmp_path / "dir"
+ adir.mkdir()
+
+ fn = adir / "foo.txt"
+ fn.touch()
+ self.chmod_r(fn)
+
+ # unknown exception
+ with pytest.warns(pytest.PytestWarning):
+ exc_info1 = (None, 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)
+ 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: ",
+ ):
+ exc_info3 = (None, 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)
+ on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path)
+ assert not fn.is_file()
+
+
+def attempt_symlink_to(path, to_path):
+ """Try to make a symlink from "path" to "to_path", skipping in case this platform
+ does not support it or we don't have sufficient privileges (common on Windows)."""
+ try:
+ Path(path).symlink_to(Path(to_path))
+ except OSError:
+ pytest.skip("could not create symbolic link")
+
+
+def test_basetemp_with_read_only_files(pytester: Pytester) -> None:
+ """Integration test for #5524"""
+ pytester.makepyfile(
+ """
+ import os
+ import stat
+
+ def test(tmp_path):
+ fn = tmp_path / 'foo.txt'
+ fn.write_text('hello')
+ mode = os.stat(str(fn)).st_mode
+ os.chmod(str(fn), mode & ~stat.S_IREAD)
+ """
+ )
+ result = pytester.runpytest("--basetemp=tmp")
+ assert result.ret == 0
+ # running a second time and ensure we don't crash
+ result = pytester.runpytest("--basetemp=tmp")
+ assert result.ret == 0
+
+
+def test_tmp_path_factory_handles_invalid_dir_characters(
+ tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch
+) -> None:
+ monkeypatch.setattr("getpass.getuser", lambda: "os/<:*?;>agnostic")
+ # _basetemp / _given_basetemp are cached / set in parallel runs, patch them
+ monkeypatch.setattr(tmp_path_factory, "_basetemp", None)
+ monkeypatch.setattr(tmp_path_factory, "_given_basetemp", None)
+ p = tmp_path_factory.getbasetemp()
+ assert "pytest-of-unknown" in str(p)
+
+
+@pytest.mark.skipif(not hasattr(os, "getuid"), reason="checks unix permissions")
+def test_tmp_path_factory_create_directory_with_safe_permissions(
+ tmp_path: Path, monkeypatch: MonkeyPatch
+) -> None:
+ """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)
+ basetemp = tmp_factory.getbasetemp()
+
+ # No world-readable permissions.
+ assert (basetemp.stat().st_mode & 0o077) == 0
+ # Parent too (pytest-of-foo).
+ assert (basetemp.parent.stat().st_mode & 0o077) == 0
+
+
+@pytest.mark.skipif(not hasattr(os, "getuid"), reason="checks unix permissions")
+def test_tmp_path_factory_fixes_up_world_readable_permissions(
+ tmp_path: Path, monkeypatch: MonkeyPatch
+) -> None:
+ """Verify that if a /tmp/pytest-of-foo directory already exists with
+ world-readable permissions, it is fixed.
+
+ pytest used to mkdir with such permissions, that's why we fix it up.
+ """
+ # 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)
+ 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)
+ basetemp = tmp_factory.getbasetemp()
+
+ # After - fixed.
+ assert (basetemp.parent.stat().st_mode & 0o077) == 0
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_unittest.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_unittest.py
new file mode 100644
index 0000000000..1601086d5b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_unittest.py
@@ -0,0 +1,1500 @@
+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
+
+
+def test_simple_unittest(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+ class MyTestCase(unittest.TestCase):
+ def testpassing(self):
+ self.assertEqual('foo', 'foo')
+ def test_failing(self):
+ self.assertEqual('foo', 'bar')
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ assert reprec.matchreport("testpassing").passed
+ assert reprec.matchreport("test_failing").failed
+
+
+def test_runTest_method(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import unittest
+ class MyTestCaseWithRunTest(unittest.TestCase):
+ def runTest(self):
+ self.assertEqual('foo', 'foo')
+ class MyTestCaseWithoutRunTest(unittest.TestCase):
+ def runTest(self):
+ self.assertEqual('foo', 'foo')
+ def test_something(self):
+ pass
+ """
+ )
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(
+ """
+ *MyTestCaseWithRunTest::runTest*
+ *MyTestCaseWithoutRunTest::test_something*
+ *2 passed*
+ """
+ )
+
+
+def test_isclasscheck_issue53(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+ class _E(object):
+ def __getattr__(self, tag):
+ pass
+ E = _E()
+ """
+ )
+ result = pytester.runpytest(testpath)
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+
+def test_setup(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+ class MyTestCase(unittest.TestCase):
+ def setUp(self):
+ self.foo = 1
+ def setup_method(self, method):
+ self.foo2 = 1
+ def test_both(self):
+ self.assertEqual(1, self.foo)
+ assert self.foo2 == 1
+ def teardown_method(self, method):
+ assert 0, "42"
+
+ """
+ )
+ reprec = pytester.inline_run("-s", testpath)
+ assert reprec.matchreport("test_both", when="call").passed
+ rep = reprec.matchreport("test_both", when="teardown")
+ assert rep.failed and "42" in str(rep.longrepr)
+
+
+def test_setUpModule(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ values = []
+
+ def setUpModule():
+ values.append(1)
+
+ def tearDownModule():
+ del values[0]
+
+ def test_hello():
+ assert values == [1]
+
+ def test_world():
+ assert values == [1]
+ """
+ )
+ result = pytester.runpytest(testpath)
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+
+def test_setUpModule_failing_no_teardown(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ values = []
+
+ def setUpModule():
+ 0/0
+
+ def tearDownModule():
+ values.append(1)
+
+ def test_hello():
+ pass
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ reprec.assertoutcome(passed=0, failed=1)
+ call = reprec.getcalls("pytest_runtest_setup")[0]
+ assert not call.item.module.values
+
+
+def test_new_instances(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+ class MyTestCase(unittest.TestCase):
+ def test_func1(self):
+ self.x = 2
+ def test_func2(self):
+ assert not hasattr(self, 'x')
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ reprec.assertoutcome(passed=2)
+
+
+def test_function_item_obj_is_instance(pytester: Pytester) -> None:
+ """item.obj should be a bound method on unittest.TestCase function items (#5390)."""
+ pytester.makeconftest(
+ """
+ def pytest_runtest_makereport(item, call):
+ if call.when == 'call':
+ class_ = item.parent.obj
+ assert isinstance(item.obj.__self__, class_)
+ """
+ )
+ pytester.makepyfile(
+ """
+ import unittest
+
+ class Test(unittest.TestCase):
+ def test_foo(self):
+ pass
+ """
+ )
+ result = pytester.runpytest_inprocess()
+ result.stdout.fnmatch_lines(["* 1 passed in*"])
+
+
+def test_teardown(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+ class MyTestCase(unittest.TestCase):
+ values = []
+ def test_one(self):
+ pass
+ def tearDown(self):
+ self.values.append(None)
+ class Second(unittest.TestCase):
+ def test_check(self):
+ self.assertEqual(MyTestCase.values, [None])
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ passed, skipped, failed = reprec.countoutcomes()
+ assert failed == 0, failed
+ assert passed == 2
+ assert passed + skipped + failed == 2
+
+
+def test_teardown_issue1649(pytester: Pytester) -> None:
+ """
+ Are TestCase objects cleaned up? Often unittest TestCase objects set
+ attributes that are large and expensive during setUp.
+
+ The TestCase will not be cleaned up if the test fails, because it
+ would then exist in the stackframe.
+ """
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+ class TestCaseObjectsShouldBeCleanedUp(unittest.TestCase):
+ def setUp(self):
+ self.an_expensive_object = 1
+ def test_demo(self):
+ pass
+
+ """
+ )
+ pytester.inline_run("-s", testpath)
+ gc.collect()
+ for obj in gc.get_objects():
+ assert type(obj).__name__ != "TestCaseObjectsShouldBeCleanedUp"
+
+
+def test_unittest_skip_issue148(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+
+ @unittest.skip("hello")
+ class MyTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ xxx
+ def test_one(self):
+ pass
+ @classmethod
+ def tearDownClass(self):
+ xxx
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ reprec.assertoutcome(skipped=1)
+
+
+def test_method_and_teardown_failing_reporting(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import unittest
+ class TC(unittest.TestCase):
+ def tearDown(self):
+ assert 0, "down1"
+ def test_method(self):
+ assert False, "down2"
+ """
+ )
+ result = pytester.runpytest("-s")
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(
+ [
+ "*tearDown*",
+ "*assert 0*",
+ "*test_method*",
+ "*assert False*",
+ "*1 failed*1 error*",
+ ]
+ )
+
+
+def test_setup_failure_is_shown(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import unittest
+ import pytest
+ class TC(unittest.TestCase):
+ def setUp(self):
+ assert 0, "down1"
+ def test_method(self):
+ print("never42")
+ xyz
+ """
+ )
+ result = pytester.runpytest("-s")
+ assert result.ret == 1
+ result.stdout.fnmatch_lines(["*setUp*", "*assert 0*down1*", "*1 failed*"])
+ result.stdout.no_fnmatch_line("*never42*")
+
+
+def test_setup_setUpClass(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+ import pytest
+ class MyTestCase(unittest.TestCase):
+ x = 0
+ @classmethod
+ def setUpClass(cls):
+ cls.x += 1
+ def test_func1(self):
+ assert self.x == 1
+ def test_func2(self):
+ assert self.x == 1
+ @classmethod
+ def tearDownClass(cls):
+ cls.x -= 1
+ def test_teareddown():
+ assert MyTestCase.x == 0
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ reprec.assertoutcome(passed=3)
+
+
+def test_fixtures_setup_setUpClass_issue8394(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import unittest
+ class MyTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ pass
+ def test_func1(self):
+ pass
+ @classmethod
+ def tearDownClass(cls):
+ pass
+ """
+ )
+ result = pytester.runpytest("--fixtures")
+ assert result.ret == 0
+ result.stdout.no_fnmatch_line("*no docstring available*")
+
+ result = pytester.runpytest("--fixtures", "-v")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(["*no docstring available*"])
+
+
+def test_setup_class(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+ import pytest
+ class MyTestCase(unittest.TestCase):
+ x = 0
+ def setup_class(cls):
+ cls.x += 1
+ def test_func1(self):
+ assert self.x == 1
+ def test_func2(self):
+ assert self.x == 1
+ def teardown_class(cls):
+ cls.x -= 1
+ def test_teareddown():
+ assert MyTestCase.x == 0
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ reprec.assertoutcome(passed=3)
+
+
+@pytest.mark.parametrize("type", ["Error", "Failure"])
+def test_testcase_adderrorandfailure_defers(pytester: Pytester, type: str) -> None:
+ pytester.makepyfile(
+ """
+ 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)
+ except KeyboardInterrupt:
+ raise
+ except:
+ pytest.fail("add%s should not raise")
+ def test_hello(self):
+ pass
+ """
+ % (type, type)
+ )
+ result = pytester.runpytest()
+ result.stdout.no_fnmatch_line("*should not raise*")
+
+
+@pytest.mark.parametrize("type", ["Error", "Failure"])
+def test_testcase_custom_exception_info(pytester: Pytester, type: str) -> None:
+ pytester.makepyfile(
+ """
+ from typing import Generic, TypeVar
+ from unittest import TestCase
+ import pytest, _pytest._code
+
+ class MyTestCase(TestCase):
+ def run(self, result):
+ excinfo = pytest.raises(ZeroDivisionError, lambda: 0/0)
+ # We fake an incompatible exception info.
+ class FakeExceptionInfo(Generic[TypeVar("E")]):
+ def __init__(self, *args, **kwargs):
+ mp.undo()
+ raise TypeError()
+ @classmethod
+ def from_current(cls):
+ return cls()
+ @classmethod
+ def from_exc_info(cls, *args, **kwargs):
+ return cls()
+ mp = pytest.MonkeyPatch()
+ mp.setattr(_pytest._code, 'ExceptionInfo', FakeExceptionInfo)
+ try:
+ excinfo = excinfo._excinfo
+ result.add%(type)s(self, excinfo)
+ finally:
+ mp.undo()
+
+ def test_hello(self):
+ pass
+ """
+ % locals()
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "NOTE: Incompatible Exception Representation*",
+ "*ZeroDivisionError*",
+ "*1 failed*",
+ ]
+ )
+
+
+def test_testcase_totally_incompatible_exception_info(pytester: Pytester) -> None:
+ import _pytest.unittest
+
+ (item,) = pytester.getitems(
+ """
+ from unittest import TestCase
+ class MyTestCase(TestCase):
+ def test_hello(self):
+ pass
+ """
+ )
+ assert isinstance(item, _pytest.unittest.TestCaseFunction)
+ item.addError(None, 42) # type: ignore[arg-type]
+ excinfo = item._excinfo
+ assert excinfo is not None
+ assert "ERROR: Unknown Incompatible" in str(excinfo.pop(0).getrepr())
+
+
+def test_module_level_pytestmark(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+ import pytest
+ pytestmark = pytest.mark.xfail
+ class MyTestCase(unittest.TestCase):
+ def test_func1(self):
+ assert 0
+ """
+ )
+ reprec = pytester.inline_run(testpath, "-s")
+ reprec.assertoutcome(skipped=1)
+
+
+class TestTrialUnittest:
+ def setup_class(cls):
+ cls.ut = pytest.importorskip("twisted.trial.unittest")
+ # on windows trial uses a socket for a reactor and apparently doesn't close it properly
+ # https://twistedmatrix.com/trac/ticket/9227
+ cls.ignore_unclosed_socket_warning = ("-W", "always")
+
+ def test_trial_testcase_runtest_not_collected(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ from twisted.trial.unittest import TestCase
+
+ class TC(TestCase):
+ def test_hello(self):
+ pass
+ """
+ )
+ reprec = pytester.inline_run(*self.ignore_unclosed_socket_warning)
+ reprec.assertoutcome(passed=1)
+ pytester.makepyfile(
+ """
+ from twisted.trial.unittest import TestCase
+
+ class TC(TestCase):
+ def runTest(self):
+ pass
+ """
+ )
+ reprec = pytester.inline_run(*self.ignore_unclosed_socket_warning)
+ reprec.assertoutcome(passed=1)
+
+ def test_trial_exceptions_with_skips(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ from twisted.trial import unittest
+ import pytest
+ class TC(unittest.TestCase):
+ def test_hello(self):
+ pytest.skip("skip_in_method")
+ @pytest.mark.skipif("sys.version_info != 1")
+ def test_hello2(self):
+ pass
+ @pytest.mark.xfail(reason="iwanto")
+ def test_hello3(self):
+ assert 0
+ def test_hello4(self):
+ pytest.xfail("i2wanto")
+ def test_trial_skip(self):
+ pass
+ test_trial_skip.skip = "trialselfskip"
+
+ def test_trial_todo(self):
+ assert 0
+ test_trial_todo.todo = "mytodo"
+
+ def test_trial_todo_success(self):
+ pass
+ test_trial_todo_success.todo = "mytodo"
+
+ class TC2(unittest.TestCase):
+ def setup_class(cls):
+ pytest.skip("skip_in_setup_class")
+ def test_method(self):
+ pass
+ """
+ )
+ result = pytester.runpytest("-rxs", *self.ignore_unclosed_socket_warning)
+ result.stdout.fnmatch_lines_random(
+ [
+ "*XFAIL*test_trial_todo*",
+ "*trialselfskip*",
+ "*skip_in_setup_class*",
+ "*iwanto*",
+ "*i2wanto*",
+ "*sys.version_info*",
+ "*skip_in_method*",
+ "*1 failed*4 skipped*3 xfailed*",
+ ]
+ )
+ assert result.ret == 1
+
+ def test_trial_error(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ from twisted.trial.unittest import TestCase
+ from twisted.internet.defer import Deferred
+ from twisted.internet import reactor
+
+ class TC(TestCase):
+ def test_one(self):
+ crash
+
+ def test_two(self):
+ def f(_):
+ crash
+
+ d = Deferred()
+ d.addCallback(f)
+ reactor.callLater(0.3, d.callback, None)
+ return d
+
+ def test_three(self):
+ def f():
+ pass # will never get called
+ reactor.callLater(0.3, f)
+ # will crash at teardown
+
+ def test_four(self):
+ def f(_):
+ reactor.callLater(0.3, f)
+ crash
+
+ d = Deferred()
+ d.addCallback(f)
+ reactor.callLater(0.3, d.callback, None)
+ return d
+ # will crash both at test time and at teardown
+ """
+ )
+ result = pytester.runpytest(
+ "-vv", "-oconsole_output_style=classic", "-W", "ignore::DeprecationWarning"
+ )
+ result.stdout.fnmatch_lines(
+ [
+ "test_trial_error.py::TC::test_four FAILED",
+ "test_trial_error.py::TC::test_four ERROR",
+ "test_trial_error.py::TC::test_one FAILED",
+ "test_trial_error.py::TC::test_three FAILED",
+ "test_trial_error.py::TC::test_two FAILED",
+ "*ERRORS*",
+ "*_ ERROR at teardown of TC.test_four _*",
+ "*DelayedCalls*",
+ "*= FAILURES =*",
+ "*_ TC.test_four _*",
+ "*NameError*crash*",
+ "*_ TC.test_one _*",
+ "*NameError*crash*",
+ "*_ TC.test_three _*",
+ "*DelayedCalls*",
+ "*_ TC.test_two _*",
+ "*NameError*crash*",
+ "*= 4 failed, 1 error in *",
+ ]
+ )
+
+ def test_trial_pdb(self, pytester: Pytester) -> None:
+ p = pytester.makepyfile(
+ """
+ from twisted.trial import unittest
+ import pytest
+ class TC(unittest.TestCase):
+ def test_hello(self):
+ assert 0, "hellopdb"
+ """
+ )
+ child = pytester.spawn_pytest(str(p))
+ child.expect("hellopdb")
+ child.sendeof()
+
+ def test_trial_testcase_skip_property(self, pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ from twisted.trial import unittest
+ class MyTestCase(unittest.TestCase):
+ skip = 'dont run'
+ def test_func(self):
+ pass
+ """
+ )
+ reprec = pytester.inline_run(testpath, "-s")
+ reprec.assertoutcome(skipped=1)
+
+ def test_trial_testfunction_skip_property(self, pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ from twisted.trial import unittest
+ class MyTestCase(unittest.TestCase):
+ def test_func(self):
+ pass
+ test_func.skip = 'dont run'
+ """
+ )
+ reprec = pytester.inline_run(testpath, "-s")
+ reprec.assertoutcome(skipped=1)
+
+ def test_trial_testcase_todo_property(self, pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ from twisted.trial import unittest
+ class MyTestCase(unittest.TestCase):
+ todo = 'dont run'
+ def test_func(self):
+ assert 0
+ """
+ )
+ reprec = pytester.inline_run(testpath, "-s")
+ reprec.assertoutcome(skipped=1)
+
+ def test_trial_testfunction_todo_property(self, pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ from twisted.trial import unittest
+ class MyTestCase(unittest.TestCase):
+ def test_func(self):
+ assert 0
+ test_func.todo = 'dont run'
+ """
+ )
+ reprec = pytester.inline_run(
+ testpath, "-s", *self.ignore_unclosed_socket_warning
+ )
+ reprec.assertoutcome(skipped=1)
+
+
+def test_djangolike_testcase(pytester: Pytester) -> None:
+ # contributed from Morten Breekevold
+ pytester.makepyfile(
+ """
+ from unittest import TestCase, main
+
+ class DjangoLikeTestCase(TestCase):
+
+ def setUp(self):
+ print("setUp()")
+
+ def test_presetup_has_been_run(self):
+ print("test_thing()")
+ self.assertTrue(hasattr(self, 'was_presetup'))
+
+ def tearDown(self):
+ print("tearDown()")
+
+ def __call__(self, result=None):
+ try:
+ self._pre_setup()
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except Exception:
+ import sys
+ result.addError(self, sys.exc_info())
+ return
+ super(DjangoLikeTestCase, self).__call__(result)
+ try:
+ self._post_teardown()
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except Exception:
+ import sys
+ result.addError(self, sys.exc_info())
+ return
+
+ def _pre_setup(self):
+ print("_pre_setup()")
+ self.was_presetup = True
+
+ def _post_teardown(self):
+ print("_post_teardown()")
+ """
+ )
+ result = pytester.runpytest("-s")
+ assert result.ret == 0
+ result.stdout.fnmatch_lines(
+ [
+ "*_pre_setup()*",
+ "*setUp()*",
+ "*test_thing()*",
+ "*tearDown()*",
+ "*_post_teardown()*",
+ ]
+ )
+
+
+def test_unittest_not_shown_in_traceback(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import unittest
+ class t(unittest.TestCase):
+ def test_hello(self):
+ x = 3
+ self.assertEqual(x, 4)
+ """
+ )
+ res = pytester.runpytest()
+ res.stdout.no_fnmatch_line("*failUnlessEqual*")
+
+
+def test_unorderable_types(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import unittest
+ class TestJoinEmpty(unittest.TestCase):
+ pass
+
+ def make_test():
+ class Test(unittest.TestCase):
+ pass
+ Test.__name__ = "TestFoo"
+ return Test
+ TestFoo = make_test()
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.no_fnmatch_line("*TypeError*")
+ assert result.ret == ExitCode.NO_TESTS_COLLECTED
+
+
+def test_unittest_typerror_traceback(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import unittest
+ class TestJoinEmpty(unittest.TestCase):
+ def test_hello(self, arg1):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ assert "TypeError" in result.stdout.str()
+ assert result.ret == 1
+
+
+@pytest.mark.parametrize("runner", ["pytest", "unittest"])
+def test_unittest_expected_failure_for_failing_test_is_xfail(
+ pytester: Pytester, runner
+) -> None:
+ script = pytester.makepyfile(
+ """
+ import unittest
+ class MyTestCase(unittest.TestCase):
+ @unittest.expectedFailure
+ def test_failing_test_is_xfail(self):
+ assert False
+ if __name__ == '__main__':
+ unittest.main()
+ """
+ )
+ if runner == "pytest":
+ result = pytester.runpytest("-rxX")
+ result.stdout.fnmatch_lines(
+ ["*XFAIL*MyTestCase*test_failing_test_is_xfail*", "*1 xfailed*"]
+ )
+ else:
+ result = pytester.runpython(script)
+ result.stderr.fnmatch_lines(["*1 test in*", "*OK*(expected failures=1)*"])
+ assert result.ret == 0
+
+
+@pytest.mark.parametrize("runner", ["pytest", "unittest"])
+def test_unittest_expected_failure_for_passing_test_is_fail(
+ pytester: Pytester,
+ runner: str,
+) -> None:
+ script = pytester.makepyfile(
+ """
+ import unittest
+ class MyTestCase(unittest.TestCase):
+ @unittest.expectedFailure
+ def test_passing_test_is_fail(self):
+ assert True
+ if __name__ == '__main__':
+ unittest.main()
+ """
+ )
+
+ if runner == "pytest":
+ result = pytester.runpytest("-rxX")
+ result.stdout.fnmatch_lines(
+ [
+ "*MyTestCase*test_passing_test_is_fail*",
+ "Unexpected success",
+ "*1 failed*",
+ ]
+ )
+ else:
+ result = pytester.runpython(script)
+ result.stderr.fnmatch_lines(["*1 test in*", "*(unexpected successes=1)*"])
+
+ assert result.ret == 1
+
+
+@pytest.mark.parametrize("stmt", ["return", "yield"])
+def test_unittest_setup_interaction(pytester: Pytester, stmt: str) -> None:
+ pytester.makepyfile(
+ """
+ import unittest
+ import pytest
+ class MyTestCase(unittest.TestCase):
+ @pytest.fixture(scope="class", autouse=True)
+ def perclass(self, request):
+ request.cls.hello = "world"
+ {stmt}
+ @pytest.fixture(scope="function", autouse=True)
+ def perfunction(self, request):
+ request.instance.funcname = request.function.__name__
+ {stmt}
+
+ def test_method1(self):
+ assert self.funcname == "test_method1"
+ assert self.hello == "world"
+
+ def test_method2(self):
+ assert self.funcname == "test_method2"
+
+ def test_classattr(self):
+ assert self.__class__.hello == "world"
+ """.format(
+ stmt=stmt
+ )
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*3 passed*"])
+
+
+def test_non_unittest_no_setupclass_support(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ class TestFoo(object):
+ x = 0
+
+ @classmethod
+ def setUpClass(cls):
+ cls.x = 1
+
+ def test_method1(self):
+ assert self.x == 0
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.x = 1
+
+ def test_not_teareddown():
+ assert TestFoo.x == 0
+
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ reprec.assertoutcome(passed=2)
+
+
+def test_no_teardown_if_setupclass_failed(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+
+ class MyTestCase(unittest.TestCase):
+ x = 0
+
+ @classmethod
+ def setUpClass(cls):
+ cls.x = 1
+ assert False
+
+ def test_func1(self):
+ cls.x = 10
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.x = 100
+
+ def test_notTornDown():
+ assert MyTestCase.x == 1
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ reprec.assertoutcome(passed=1, failed=1)
+
+
+def test_cleanup_functions(pytester: Pytester) -> None:
+ """Ensure functions added with addCleanup are always called after each test ends (#6947)"""
+ pytester.makepyfile(
+ """
+ import unittest
+
+ cleanups = []
+
+ class Test(unittest.TestCase):
+
+ def test_func_1(self):
+ self.addCleanup(cleanups.append, "test_func_1")
+
+ def test_func_2(self):
+ self.addCleanup(cleanups.append, "test_func_2")
+ assert 0
+
+ def test_func_3_check_cleanups(self):
+ assert cleanups == ["test_func_1", "test_func_2"]
+ """
+ )
+ result = pytester.runpytest("-v")
+ result.stdout.fnmatch_lines(
+ [
+ "*::test_func_1 PASSED *",
+ "*::test_func_2 FAILED *",
+ "*::test_func_3_check_cleanups PASSED *",
+ ]
+ )
+
+
+def test_issue333_result_clearing(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ import pytest
+ @pytest.hookimpl(hookwrapper=True)
+ def pytest_runtest_call(item):
+ yield
+ assert 0
+ """
+ )
+ pytester.makepyfile(
+ """
+ import unittest
+ class TestIt(unittest.TestCase):
+ def test_func(self):
+ 0/0
+ """
+ )
+
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(failed=1)
+
+
+def test_unittest_raise_skip_issue748(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_foo="""
+ import unittest
+
+ class MyTestCase(unittest.TestCase):
+ def test_one(self):
+ raise unittest.SkipTest('skipping due to reasons')
+ """
+ )
+ result = pytester.runpytest("-v", "-rs")
+ result.stdout.fnmatch_lines(
+ """
+ *SKIP*[1]*test_foo.py*skipping due to reasons*
+ *1 skipped*
+ """
+ )
+
+
+def test_unittest_skip_issue1169(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_foo="""
+ import unittest
+
+ class MyTestCase(unittest.TestCase):
+ @unittest.skip("skipping due to reasons")
+ def test_skip(self):
+ self.fail()
+ """
+ )
+ result = pytester.runpytest("-v", "-rs")
+ result.stdout.fnmatch_lines(
+ """
+ *SKIP*[1]*skipping due to reasons*
+ *1 skipped*
+ """
+ )
+
+
+def test_class_method_containing_test_issue1558(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_foo="""
+ import unittest
+
+ class MyTestCase(unittest.TestCase):
+ def test_should_run(self):
+ pass
+ def test_should_not_run(self):
+ pass
+ test_should_not_run.__test__ = False
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(passed=1)
+
+
+@pytest.mark.parametrize("base", ["builtins.object", "unittest.TestCase"])
+def test_usefixtures_marker_on_unittest(base, pytester: Pytester) -> None:
+ """#3498"""
+ module = base.rsplit(".", 1)[0]
+ pytest.importorskip(module)
+ pytester.makepyfile(
+ conftest="""
+ import pytest
+
+ @pytest.fixture(scope='function')
+ def fixture1(request, monkeypatch):
+ monkeypatch.setattr(request.instance, 'fixture1', True )
+
+
+ @pytest.fixture(scope='function')
+ def fixture2(request, monkeypatch):
+ monkeypatch.setattr(request.instance, 'fixture2', True )
+
+ def node_and_marks(item):
+ print(item.nodeid)
+ for mark in item.iter_markers():
+ print(" ", mark)
+
+ @pytest.fixture(autouse=True)
+ def my_marks(request):
+ node_and_marks(request.node)
+
+ def pytest_collection_modifyitems(items):
+ for item in items:
+ node_and_marks(item)
+
+ """
+ )
+
+ pytester.makepyfile(
+ """
+ import pytest
+ import {module}
+
+ class Tests({base}):
+ fixture1 = False
+ fixture2 = False
+
+ @pytest.mark.usefixtures("fixture1")
+ def test_one(self):
+ assert self.fixture1
+ assert not self.fixture2
+
+ @pytest.mark.usefixtures("fixture1", "fixture2")
+ def test_two(self):
+ assert self.fixture1
+ assert self.fixture2
+
+
+ """.format(
+ module=module, base=base
+ )
+ )
+
+ result = pytester.runpytest("-s")
+ result.assert_outcomes(passed=2)
+
+
+def test_testcase_handles_init_exceptions(pytester: Pytester) -> None:
+ """
+ Regression test to make sure exceptions in the __init__ method are bubbled up correctly.
+ See https://github.com/pytest-dev/pytest/issues/3788
+ """
+ pytester.makepyfile(
+ """
+ from unittest import TestCase
+ import pytest
+ class MyTestCase(TestCase):
+ def __init__(self, *args, **kwargs):
+ raise Exception("should raise this exception")
+ def test_hello(self):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ assert "should raise this exception" in result.stdout.str()
+ result.stdout.no_fnmatch_line("*ERROR at teardown of MyTestCase.test_hello*")
+
+
+def test_error_message_with_parametrized_fixtures(pytester: Pytester) -> None:
+ pytester.copy_example("unittest/test_parametrized_fixture_error_message.py")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*test_two does not support fixtures*",
+ "*TestSomethingElse::test_two",
+ "*Function type: TestCaseFunction",
+ ]
+ )
+
+
+@pytest.mark.parametrize(
+ "test_name, expected_outcome",
+ [
+ ("test_setup_skip.py", "1 skipped"),
+ ("test_setup_skip_class.py", "1 skipped"),
+ ("test_setup_skip_module.py", "1 error"),
+ ],
+)
+def test_setup_inheritance_skipping(
+ pytester: Pytester, test_name, expected_outcome
+) -> None:
+ """Issue #4700"""
+ pytester.copy_example(f"unittest/{test_name}")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines([f"* {expected_outcome} in *"])
+
+
+def test_BdbQuit(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_foo="""
+ import unittest
+
+ class MyTestCase(unittest.TestCase):
+ def test_bdbquit(self):
+ import bdb
+ raise bdb.BdbQuit()
+
+ def test_should_not_run(self):
+ pass
+ """
+ )
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(failed=1, passed=1)
+
+
+def test_exit_outcome(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_foo="""
+ import pytest
+ import unittest
+
+ class MyTestCase(unittest.TestCase):
+ def test_exit_outcome(self):
+ pytest.exit("pytest_exit called")
+
+ def test_should_not_run(self):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*Exit: pytest_exit called*", "*= no tests ran in *"])
+
+
+def test_trace(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
+ calls = []
+
+ def check_call(*args, **kwargs):
+ calls.append((args, kwargs))
+ assert args == ("runcall",)
+
+ class _pdb:
+ def runcall(*args, **kwargs):
+ calls.append((args, kwargs))
+
+ return _pdb
+
+ monkeypatch.setattr("_pytest.debugging.pytestPDB._init_pdb", check_call)
+
+ p1 = pytester.makepyfile(
+ """
+ import unittest
+
+ class MyTestCase(unittest.TestCase):
+ def test(self):
+ self.assertEqual('foo', 'foo')
+ """
+ )
+ result = pytester.runpytest("--trace", str(p1))
+ assert len(calls) == 2
+ assert result.ret == 0
+
+
+def test_pdb_teardown_called(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
+ """Ensure tearDown() is always called when --pdb is given in the command-line.
+
+ We delay the normal tearDown() calls when --pdb is given, so this ensures we are calling
+ tearDown() eventually to avoid memory leaks when using --pdb.
+ """
+ teardowns: List[str] = []
+ monkeypatch.setattr(
+ pytest, "test_pdb_teardown_called_teardowns", teardowns, raising=False
+ )
+
+ pytester.makepyfile(
+ """
+ import unittest
+ import pytest
+
+ class MyTestCase(unittest.TestCase):
+
+ def tearDown(self):
+ pytest.test_pdb_teardown_called_teardowns.append(self.id())
+
+ def test_1(self):
+ pass
+ def test_2(self):
+ pass
+ """
+ )
+ result = pytester.runpytest_inprocess("--pdb")
+ result.stdout.fnmatch_lines("* 2 passed in *")
+ assert teardowns == [
+ "test_pdb_teardown_called.MyTestCase.test_1",
+ "test_pdb_teardown_called.MyTestCase.test_2",
+ ]
+
+
+@pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"])
+def test_pdb_teardown_skipped(
+ pytester: Pytester, monkeypatch: MonkeyPatch, mark: str
+) -> None:
+ """With --pdb, setUp and tearDown should not be called for skipped tests."""
+ tracked: List[str] = []
+ monkeypatch.setattr(pytest, "test_pdb_teardown_skipped", tracked, raising=False)
+
+ pytester.makepyfile(
+ """
+ import unittest
+ import pytest
+
+ class MyTestCase(unittest.TestCase):
+
+ def setUp(self):
+ pytest.test_pdb_teardown_skipped.append("setUp:" + self.id())
+
+ def tearDown(self):
+ pytest.test_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 == []
+
+
+def test_async_support(pytester: Pytester) -> None:
+ pytest.importorskip("unittest.async_case")
+
+ pytester.copy_example("unittest/test_unittest_asyncio.py")
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(failed=1, passed=2)
+
+
+def test_asynctest_support(pytester: Pytester) -> None:
+ """Check asynctest support (#7110)"""
+ pytest.importorskip("asynctest")
+
+ pytester.copy_example("unittest/test_unittest_asynctest.py")
+ reprec = pytester.inline_run()
+ reprec.assertoutcome(failed=1, passed=2)
+
+
+def test_plain_unittest_does_not_support_async(pytester: Pytester) -> None:
+ """Async functions in plain unittest.TestCase subclasses are not supported without plugins.
+
+ This test exists here to avoid introducing this support by accident, leading users
+ to expect that it works, rather than doing so intentionally as a feature.
+
+ See https://github.com/pytest-dev/pytest-asyncio/issues/180 for more context.
+ """
+ pytester.copy_example("unittest/test_unittest_plain_async.py")
+ result = pytester.runpytest_subprocess()
+ if hasattr(sys, "pypy_version_info"):
+ # in PyPy we can't reliable get the warning about the coroutine not being awaited,
+ # because it depends on the coroutine being garbage collected; given that
+ # we are running in a subprocess, that's difficult to enforce
+ expected_lines = ["*1 passed*"]
+ else:
+ expected_lines = [
+ "*RuntimeWarning: coroutine * was never awaited",
+ "*1 passed*",
+ ]
+ 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(
+ """
+ import unittest
+ class MyTestCase(unittest.TestCase):
+ values = []
+ @classmethod
+ def setUpClass(cls):
+ def cleanup():
+ cls.values.append(1)
+ cls.addClassCleanup(cleanup)
+ def test_one(self):
+ pass
+ def test_two(self):
+ pass
+ def test_cleanup_called_exactly_once():
+ assert MyTestCase.values == [1]
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ passed, skipped, failed = reprec.countoutcomes()
+ assert failed == 0
+ 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(
+ """
+ import unittest
+ class MyTestCase(unittest.TestCase):
+ values = []
+ @classmethod
+ def setUpClass(cls):
+ def cleanup():
+ cls.values.append(1)
+ cls.addClassCleanup(cleanup)
+ assert False
+ def test_one(self):
+ pass
+ def test_cleanup_called_exactly_once():
+ assert MyTestCase.values == [1]
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ passed, skipped, failed = reprec.countoutcomes()
+ assert failed == 1
+ 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(
+ """
+ import unittest
+ class MyTestCase(unittest.TestCase):
+ values = []
+ @classmethod
+ def setUpClass(cls):
+ def cleanup():
+ cls.values.append(1)
+ cls.addClassCleanup(cleanup)
+ @classmethod
+ def tearDownClass(cls):
+ assert False
+ def test_one(self):
+ pass
+ def test_two(self):
+ pass
+ def test_cleanup_called_exactly_once():
+ assert MyTestCase.values == [1]
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ passed, skipped, failed = reprec.countoutcomes()
+ assert passed == 3
+
+
+def test_do_cleanups_on_success(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+ class MyTestCase(unittest.TestCase):
+ values = []
+ def setUp(self):
+ def cleanup():
+ self.values.append(1)
+ self.addCleanup(cleanup)
+ def test_one(self):
+ pass
+ def test_two(self):
+ pass
+ def test_cleanup_called_the_right_number_of_times():
+ assert MyTestCase.values == [1, 1]
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ passed, skipped, failed = reprec.countoutcomes()
+ assert failed == 0
+ assert passed == 3
+
+
+def test_do_cleanups_on_setup_failure(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+ class MyTestCase(unittest.TestCase):
+ values = []
+ def setUp(self):
+ def cleanup():
+ self.values.append(1)
+ self.addCleanup(cleanup)
+ assert False
+ def test_one(self):
+ pass
+ def test_two(self):
+ pass
+ def test_cleanup_called_the_right_number_of_times():
+ assert MyTestCase.values == [1, 1]
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ passed, skipped, failed = reprec.countoutcomes()
+ assert failed == 2
+ assert passed == 1
+
+
+def test_do_cleanups_on_teardown_failure(pytester: Pytester) -> None:
+ testpath = pytester.makepyfile(
+ """
+ import unittest
+ class MyTestCase(unittest.TestCase):
+ values = []
+ def setUp(self):
+ def cleanup():
+ self.values.append(1)
+ self.addCleanup(cleanup)
+ def tearDown(self):
+ assert False
+ def test_one(self):
+ pass
+ def test_two(self):
+ pass
+ def test_cleanup_called_the_right_number_of_times():
+ assert MyTestCase.values == [1, 1]
+ """
+ )
+ reprec = pytester.inline_run(testpath)
+ passed, skipped, failed = reprec.countoutcomes()
+ assert failed == 2
+ assert passed == 1
+
+
+def test_traceback_pruning(pytester: Pytester) -> None:
+ """Regression test for #9610 - doesn't crash during traceback pruning."""
+ pytester.makepyfile(
+ """
+ import unittest
+
+ class MyTestCase(unittest.TestCase):
+ def __init__(self, test_method):
+ unittest.TestCase.__init__(self, test_method)
+
+ class TestIt(MyTestCase):
+ @classmethod
+ def tearDownClass(cls) -> None:
+ assert False
+
+ def test_it(self):
+ pass
+ """
+ )
+ reprec = pytester.inline_run()
+ passed, skipped, failed = reprec.countoutcomes()
+ assert passed == 1
+ assert failed == 1
+ assert reprec.ret == 1
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_unraisableexception.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_unraisableexception.py
new file mode 100644
index 0000000000..f625833dce
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_unraisableexception.py
@@ -0,0 +1,133 @@
+import sys
+
+import pytest
+from _pytest.pytester import Pytester
+
+
+if sys.version_info < (3, 8):
+ pytest.skip("unraisableexception plugin needs Python>=3.8", allow_module_level=True)
+
+
+@pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning")
+def test_unraisable(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_it="""
+ class BrokenDel:
+ def __del__(self):
+ raise ValueError("del is broken")
+
+ def test_it():
+ obj = BrokenDel()
+ del obj
+
+ def test_2(): pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+ assert result.parseoutcomes() == {"passed": 2, "warnings": 1}
+ result.stdout.fnmatch_lines(
+ [
+ "*= warnings summary =*",
+ "test_it.py::test_it",
+ " * PytestUnraisableExceptionWarning: Exception ignored in: <function BrokenDel.__del__ at *>",
+ " ",
+ " Traceback (most recent call last):",
+ " ValueError: del is broken",
+ " ",
+ " warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))",
+ ]
+ )
+
+
+@pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning")
+def test_unraisable_in_setup(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_it="""
+ import pytest
+
+ class BrokenDel:
+ def __del__(self):
+ raise ValueError("del is broken")
+
+ @pytest.fixture
+ def broken_del():
+ obj = BrokenDel()
+ del obj
+
+ def test_it(broken_del): pass
+ def test_2(): pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+ assert result.parseoutcomes() == {"passed": 2, "warnings": 1}
+ result.stdout.fnmatch_lines(
+ [
+ "*= warnings summary =*",
+ "test_it.py::test_it",
+ " * PytestUnraisableExceptionWarning: Exception ignored in: <function BrokenDel.__del__ at *>",
+ " ",
+ " Traceback (most recent call last):",
+ " ValueError: del is broken",
+ " ",
+ " warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))",
+ ]
+ )
+
+
+@pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning")
+def test_unraisable_in_teardown(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_it="""
+ import pytest
+
+ class BrokenDel:
+ def __del__(self):
+ raise ValueError("del is broken")
+
+ @pytest.fixture
+ def broken_del():
+ yield
+ obj = BrokenDel()
+ del obj
+
+ def test_it(broken_del): pass
+ def test_2(): pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == 0
+ assert result.parseoutcomes() == {"passed": 2, "warnings": 1}
+ result.stdout.fnmatch_lines(
+ [
+ "*= warnings summary =*",
+ "test_it.py::test_it",
+ " * PytestUnraisableExceptionWarning: Exception ignored in: <function BrokenDel.__del__ at *>",
+ " ",
+ " Traceback (most recent call last):",
+ " ValueError: del is broken",
+ " ",
+ " warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))",
+ ]
+ )
+
+
+@pytest.mark.filterwarnings("error::pytest.PytestUnraisableExceptionWarning")
+def test_unraisable_warning_error(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ test_it="""
+ class BrokenDel:
+ def __del__(self) -> None:
+ raise ValueError("del is broken")
+
+ def test_it() -> None:
+ obj = BrokenDel()
+ del obj
+
+ def test_2(): pass
+ """
+ )
+ result = pytester.runpytest()
+ assert result.ret == pytest.ExitCode.TESTS_FAILED
+ assert result.parseoutcomes() == {"passed": 1, "failed": 1}
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_warning_types.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_warning_types.py
new file mode 100644
index 0000000000..b49cc68f9c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_warning_types.py
@@ -0,0 +1,38 @@
+import inspect
+
+import pytest
+from _pytest import warning_types
+from _pytest.pytester import Pytester
+
+
+@pytest.mark.parametrize(
+ "warning_class",
+ [
+ w
+ for n, w in vars(warning_types).items()
+ if inspect.isclass(w) and issubclass(w, Warning)
+ ],
+)
+def test_warning_types(warning_class: UserWarning) -> None:
+ """Make sure all warnings declared in _pytest.warning_types are displayed as coming
+ from 'pytest' instead of the internal module (#5452).
+ """
+ assert warning_class.__module__ == "pytest"
+
+
+@pytest.mark.filterwarnings("error::pytest.PytestWarning")
+def test_pytest_warnings_repr_integration_test(pytester: Pytester) -> None:
+ """Small integration test to ensure our small hack of setting the __module__ attribute
+ of our warnings actually works (#5452).
+ """
+ pytester.makepyfile(
+ """
+ import pytest
+ import warnings
+
+ def test():
+ warnings.warn(pytest.PytestWarning("some warning"))
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["E pytest.PytestWarning: some warning"])
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/test_warnings.py b/testing/web-platform/tests/tools/third_party/pytest/testing/test_warnings.py
new file mode 100644
index 0000000000..5663c46cea
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/test_warnings.py
@@ -0,0 +1,775 @@
+import os
+import warnings
+from typing import List
+from typing import Optional
+from typing import Tuple
+
+import pytest
+from _pytest.fixtures import FixtureRequest
+from _pytest.pytester import Pytester
+
+WARNINGS_SUMMARY_HEADER = "warnings summary"
+
+
+@pytest.fixture
+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"
+ test_file = pytester.makepyfile(
+ """
+ import {module_name}
+ def test_func():
+ assert {module_name}.foo() == 1
+ """.format(
+ module_name=module_name
+ ),
+ **{
+ module_name: """
+ import warnings
+ def foo():
+ warnings.warn(UserWarning("user warning"))
+ warnings.warn(RuntimeWarning("runtime warning"))
+ return 1
+ """,
+ },
+ )
+ return str(test_file)
+
+
+@pytest.mark.filterwarnings("default::UserWarning", "default::RuntimeWarning")
+def test_normal_flow(pytester: Pytester, pyfile_with_warnings) -> None:
+ """Check that the warnings section is displayed."""
+ result = pytester.runpytest(pyfile_with_warnings)
+ result.stdout.fnmatch_lines(
+ [
+ "*== %s ==*" % WARNINGS_SUMMARY_HEADER,
+ "test_normal_flow.py::test_func",
+ "*normal_flow_module.py:3: UserWarning: user warning",
+ '* warnings.warn(UserWarning("user warning"))',
+ "*normal_flow_module.py:4: RuntimeWarning: runtime warning",
+ '* warnings.warn(RuntimeWarning("runtime warning"))',
+ "* 1 passed, 2 warnings*",
+ ]
+ )
+
+
+@pytest.mark.filterwarnings("always::UserWarning")
+def test_setup_teardown_warnings(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import warnings
+ import pytest
+
+ @pytest.fixture
+ def fix():
+ warnings.warn(UserWarning("warning during setup"))
+ yield
+ warnings.warn(UserWarning("warning during teardown"))
+
+ def test_func(fix):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*== %s ==*" % WARNINGS_SUMMARY_HEADER,
+ "*test_setup_teardown_warnings.py:6: UserWarning: warning during setup",
+ '*warnings.warn(UserWarning("warning during setup"))',
+ "*test_setup_teardown_warnings.py:8: UserWarning: warning during teardown",
+ '*warnings.warn(UserWarning("warning during teardown"))',
+ "* 1 passed, 2 warnings*",
+ ]
+ )
+
+
+@pytest.mark.parametrize("method", ["cmdline", "ini"])
+def test_as_errors(pytester: Pytester, pyfile_with_warnings, method) -> None:
+ args = ("-W", "error") if method == "cmdline" else ()
+ if method == "ini":
+ pytester.makeini(
+ """
+ [pytest]
+ filterwarnings=error
+ """
+ )
+ # Use a subprocess, since changing logging level affects other threads
+ # (xdist).
+ result = pytester.runpytest_subprocess(*args, pyfile_with_warnings)
+ result.stdout.fnmatch_lines(
+ [
+ "E UserWarning: user warning",
+ "as_errors_module.py:3: UserWarning",
+ "* 1 failed in *",
+ ]
+ )
+
+
+@pytest.mark.parametrize("method", ["cmdline", "ini"])
+def test_ignore(pytester: Pytester, pyfile_with_warnings, method) -> None:
+ args = ("-W", "ignore") if method == "cmdline" else ()
+ if method == "ini":
+ pytester.makeini(
+ """
+ [pytest]
+ filterwarnings= ignore
+ """
+ )
+
+ result = pytester.runpytest(*args, pyfile_with_warnings)
+ result.stdout.fnmatch_lines(["* 1 passed in *"])
+ assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
+
+
+@pytest.mark.filterwarnings("always::UserWarning")
+def test_unicode(pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """
+ import warnings
+ import pytest
+
+
+ @pytest.fixture
+ def fix():
+ warnings.warn("测试")
+ yield
+
+ def test_func(fix):
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*== %s ==*" % WARNINGS_SUMMARY_HEADER,
+ "*test_unicode.py:7: UserWarning: \u6d4b\u8bd5*",
+ "* 1 passed, 1 warning*",
+ ]
+ )
+
+
+def test_works_with_filterwarnings(pytester: Pytester) -> None:
+ """Ensure our warnings capture does not mess with pre-installed filters (#2430)."""
+ pytester.makepyfile(
+ """
+ import warnings
+
+ class MyWarning(Warning):
+ pass
+
+ warnings.filterwarnings("error", category=MyWarning)
+
+ class TestWarnings(object):
+ def test_my_warning(self):
+ try:
+ warnings.warn(MyWarning("warn!"))
+ assert False
+ except MyWarning:
+ assert True
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(["*== 1 passed in *"])
+
+
+@pytest.mark.parametrize("default_config", ["ini", "cmdline"])
+def test_filterwarnings_mark(pytester: Pytester, default_config) -> None:
+ """Test ``filterwarnings`` mark works and takes precedence over command
+ line and ini options."""
+ if default_config == "ini":
+ pytester.makeini(
+ """
+ [pytest]
+ filterwarnings = always::RuntimeWarning
+ """
+ )
+ pytester.makepyfile(
+ """
+ import warnings
+ import pytest
+
+ @pytest.mark.filterwarnings('ignore::RuntimeWarning')
+ def test_ignore_runtime_warning():
+ warnings.warn(RuntimeWarning())
+
+ @pytest.mark.filterwarnings('error')
+ def test_warning_error():
+ warnings.warn(RuntimeWarning())
+
+ def test_show_warning():
+ warnings.warn(RuntimeWarning())
+ """
+ )
+ result = pytester.runpytest(
+ "-W always::RuntimeWarning" if default_config == "cmdline" else ""
+ )
+ result.stdout.fnmatch_lines(["*= 1 failed, 2 passed, 1 warning in *"])
+
+
+def test_non_string_warning_argument(pytester: Pytester) -> None:
+ """Non-str argument passed to warning breaks pytest (#2956)"""
+ pytester.makepyfile(
+ """\
+ import warnings
+ import pytest
+
+ def test():
+ warnings.warn(UserWarning(1, 'foo'))
+ """
+ )
+ result = pytester.runpytest("-W", "always::UserWarning")
+ result.stdout.fnmatch_lines(["*= 1 passed, 1 warning in *"])
+
+
+def test_filterwarnings_mark_registration(pytester: Pytester) -> None:
+ """Ensure filterwarnings mark is registered"""
+ pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.filterwarnings('error')
+ def test_func():
+ pass
+ """
+ )
+ result = pytester.runpytest("--strict-markers")
+ assert result.ret == 0
+
+
+@pytest.mark.filterwarnings("always::UserWarning")
+def test_warning_captured_hook(pytester: Pytester) -> None:
+ pytester.makeconftest(
+ """
+ def pytest_configure(config):
+ config.issue_config_time_warning(UserWarning("config warning"), stacklevel=2)
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest, warnings
+
+ warnings.warn(UserWarning("collect warning"))
+
+ @pytest.fixture
+ def fix():
+ warnings.warn(UserWarning("setup warning"))
+ yield 1
+ warnings.warn(UserWarning("teardown warning"))
+
+ def test_func(fix):
+ warnings.warn(UserWarning("call warning"))
+ assert fix == 1
+ """
+ )
+
+ collected = []
+
+ class WarningCollector:
+ def pytest_warning_recorded(self, warning_message, when, nodeid, location):
+ collected.append((str(warning_message.message), when, nodeid, location))
+
+ result = pytester.runpytest(plugins=[WarningCollector()])
+ result.stdout.fnmatch_lines(["*1 passed*"])
+
+ 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"),
+ ]
+ for index in range(len(expected)):
+ collected_result = collected[index]
+ expected_result = expected[index]
+
+ assert collected_result[0] == expected_result[0], str(collected)
+ assert collected_result[1] == expected_result[1], str(collected)
+ assert collected_result[2] == expected_result[2], str(collected)
+
+ # NOTE: collected_result[3] is location, which differs based on the platform you are on
+ # thus, the best we can do here is assert the types of the parameters match what we expect
+ # and not try and preload it in the expected array
+ if collected_result[3] is not None:
+ assert type(collected_result[3][0]) is str, str(collected)
+ assert type(collected_result[3][1]) is int, str(collected)
+ assert type(collected_result[3][2]) is str, str(collected)
+ else:
+ assert collected_result[3] is None, str(collected)
+
+
+@pytest.mark.filterwarnings("always::UserWarning")
+def test_collection_warnings(pytester: Pytester) -> None:
+ """Check that we also capture warnings issued during test collection (#3251)."""
+ pytester.makepyfile(
+ """
+ import warnings
+
+ warnings.warn(UserWarning("collection warning"))
+
+ def test_foo():
+ pass
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*== %s ==*" % WARNINGS_SUMMARY_HEADER,
+ " *collection_warnings.py:3: UserWarning: collection warning",
+ ' warnings.warn(UserWarning("collection warning"))',
+ "* 1 passed, 1 warning*",
+ ]
+ )
+
+
+@pytest.mark.filterwarnings("always::UserWarning")
+def test_mark_regex_escape(pytester: Pytester) -> None:
+ """@pytest.mark.filterwarnings should not try to escape regex characters (#3936)"""
+ pytester.makepyfile(
+ r"""
+ import pytest, warnings
+
+ @pytest.mark.filterwarnings(r"ignore:some \(warning\)")
+ def test_foo():
+ warnings.warn(UserWarning("some (warning)"))
+ """
+ )
+ result = pytester.runpytest()
+ assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
+
+
+@pytest.mark.filterwarnings("default::pytest.PytestWarning")
+@pytest.mark.parametrize("ignore_pytest_warnings", ["no", "ini", "cmdline"])
+def test_hide_pytest_internal_warnings(
+ pytester: Pytester, ignore_pytest_warnings
+) -> None:
+ """Make sure we can ignore internal pytest warnings using a warnings filter."""
+ pytester.makepyfile(
+ """
+ import pytest
+ import warnings
+
+ warnings.warn(pytest.PytestWarning("some internal warning"))
+
+ def test_bar():
+ pass
+ """
+ )
+ if ignore_pytest_warnings == "ini":
+ pytester.makeini(
+ """
+ [pytest]
+ filterwarnings = ignore::pytest.PytestWarning
+ """
+ )
+ args = (
+ ["-W", "ignore::pytest.PytestWarning"]
+ if ignore_pytest_warnings == "cmdline"
+ else []
+ )
+ result = pytester.runpytest(*args)
+ if ignore_pytest_warnings != "no":
+ assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
+ else:
+ result.stdout.fnmatch_lines(
+ [
+ "*== %s ==*" % WARNINGS_SUMMARY_HEADER,
+ "*test_hide_pytest_internal_warnings.py:4: PytestWarning: some internal warning",
+ "* 1 passed, 1 warning *",
+ ]
+ )
+
+
+@pytest.mark.parametrize("ignore_on_cmdline", [True, False])
+def test_option_precedence_cmdline_over_ini(
+ pytester: Pytester, ignore_on_cmdline
+) -> None:
+ """Filters defined in the command-line should take precedence over filters in ini files (#3946)."""
+ pytester.makeini(
+ """
+ [pytest]
+ filterwarnings = error::UserWarning
+ """
+ )
+ pytester.makepyfile(
+ """
+ import warnings
+ def test():
+ warnings.warn(UserWarning('hello'))
+ """
+ )
+ args = ["-W", "ignore"] if ignore_on_cmdline else []
+ result = pytester.runpytest(*args)
+ if ignore_on_cmdline:
+ result.stdout.fnmatch_lines(["* 1 passed in*"])
+ else:
+ result.stdout.fnmatch_lines(["* 1 failed in*"])
+
+
+def test_option_precedence_mark(pytester: Pytester) -> None:
+ """Filters defined by marks should always take precedence (#3946)."""
+ pytester.makeini(
+ """
+ [pytest]
+ filterwarnings = ignore
+ """
+ )
+ pytester.makepyfile(
+ """
+ import pytest, warnings
+ @pytest.mark.filterwarnings('error')
+ def test():
+ warnings.warn(UserWarning('hello'))
+ """
+ )
+ result = pytester.runpytest("-W", "ignore")
+ result.stdout.fnmatch_lines(["* 1 failed in*"])
+
+
+class TestDeprecationWarningsByDefault:
+ """
+ Note: all pytest runs are executed in a subprocess so we don't inherit warning filters
+ from pytest's own test suite
+ """
+
+ def create_file(self, pytester: Pytester, mark="") -> None:
+ pytester.makepyfile(
+ """
+ import pytest, warnings
+
+ warnings.warn(DeprecationWarning("collection"))
+
+ {mark}
+ def test_foo():
+ warnings.warn(PendingDeprecationWarning("test run"))
+ """.format(
+ mark=mark
+ )
+ )
+
+ @pytest.mark.parametrize("customize_filters", [True, False])
+ def test_shown_by_default(self, pytester: Pytester, customize_filters) -> None:
+ """Show deprecation warnings by default, even if user has customized the warnings filters (#4013)."""
+ self.create_file(pytester)
+ if customize_filters:
+ pytester.makeini(
+ """
+ [pytest]
+ filterwarnings =
+ once::UserWarning
+ """
+ )
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(
+ [
+ "*== %s ==*" % WARNINGS_SUMMARY_HEADER,
+ "*test_shown_by_default.py:3: DeprecationWarning: collection",
+ "*test_shown_by_default.py:7: PendingDeprecationWarning: test run",
+ "* 1 passed, 2 warnings*",
+ ]
+ )
+
+ def test_hidden_by_ini(self, pytester: Pytester) -> None:
+ self.create_file(pytester)
+ pytester.makeini(
+ """
+ [pytest]
+ filterwarnings =
+ ignore::DeprecationWarning
+ ignore::PendingDeprecationWarning
+ """
+ )
+ result = pytester.runpytest_subprocess()
+ assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
+
+ def test_hidden_by_mark(self, pytester: Pytester) -> None:
+ """Should hide the deprecation warning from the function, but the warning during collection should
+ be displayed normally.
+ """
+ self.create_file(
+ pytester,
+ mark='@pytest.mark.filterwarnings("ignore::PendingDeprecationWarning")',
+ )
+ result = pytester.runpytest_subprocess()
+ result.stdout.fnmatch_lines(
+ [
+ "*== %s ==*" % WARNINGS_SUMMARY_HEADER,
+ "*test_hidden_by_mark.py:3: DeprecationWarning: collection",
+ "* 1 passed, 1 warning*",
+ ]
+ )
+
+ def test_hidden_by_cmdline(self, pytester: Pytester) -> None:
+ self.create_file(pytester)
+ result = pytester.runpytest_subprocess(
+ "-W",
+ "ignore::DeprecationWarning",
+ "-W",
+ "ignore::PendingDeprecationWarning",
+ )
+ assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
+
+ def test_hidden_by_system(self, pytester: Pytester, monkeypatch) -> None:
+ self.create_file(pytester)
+ monkeypatch.setenv("PYTHONWARNINGS", "once::UserWarning")
+ result = pytester.runpytest_subprocess()
+ assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
+
+
+@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.
+
+ This test should be enabled as part of each major release, and skipped again afterwards
+ to ensure our deprecations are turning into warnings as expected.
+ """
+ pytester.makepyfile(
+ """
+ import warnings, pytest
+ def test():
+ warnings.warn(pytest.PytestRemovedIn7Warning("some warning"))
+ """
+ )
+ if change_default == "ini":
+ pytester.makeini(
+ """
+ [pytest]
+ filterwarnings =
+ ignore::pytest.PytestRemovedIn7Warning
+ """
+ )
+
+ args = (
+ ("-Wignore::pytest.PytestRemovedIn7Warning",)
+ if change_default == "cmdline"
+ else ()
+ )
+ result = pytester.runpytest(*args)
+ if change_default is None:
+ result.stdout.fnmatch_lines(["* 1 failed in *"])
+ else:
+ assert change_default in ("ini", "cmdline")
+ result.stdout.fnmatch_lines(["* 1 passed in *"])
+
+
+class TestAssertionWarnings:
+ @staticmethod
+ def assert_result_warns(result, msg) -> None:
+ result.stdout.fnmatch_lines(["*PytestAssertRewriteWarning: %s*" % msg])
+
+ def test_tuple_warning(self, pytester: Pytester) -> None:
+ pytester.makepyfile(
+ """\
+ def test_foo():
+ assert (1,2)
+ """
+ )
+ result = pytester.runpytest()
+ self.assert_result_warns(
+ result, "assertion is always true, perhaps remove parentheses?"
+ )
+
+
+def test_warnings_checker_twice() -> None:
+ """Issue #4617"""
+ expectation = pytest.warns(UserWarning)
+ with expectation:
+ warnings.warn("Message A", UserWarning)
+ with expectation:
+ warnings.warn("Message B", UserWarning)
+
+
+@pytest.mark.filterwarnings("always::UserWarning")
+def test_group_warnings_by_message(pytester: Pytester) -> None:
+ pytester.copy_example("warnings/test_group_warnings_by_message.py")
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*== %s ==*" % WARNINGS_SUMMARY_HEADER,
+ "test_group_warnings_by_message.py::test_foo[[]0[]]",
+ "test_group_warnings_by_message.py::test_foo[[]1[]]",
+ "test_group_warnings_by_message.py::test_foo[[]2[]]",
+ "test_group_warnings_by_message.py::test_foo[[]3[]]",
+ "test_group_warnings_by_message.py::test_foo[[]4[]]",
+ "test_group_warnings_by_message.py::test_foo_1",
+ " */test_group_warnings_by_message.py:*: UserWarning: foo",
+ " warnings.warn(UserWarning(msg))",
+ "",
+ "test_group_warnings_by_message.py::test_bar[[]0[]]",
+ "test_group_warnings_by_message.py::test_bar[[]1[]]",
+ "test_group_warnings_by_message.py::test_bar[[]2[]]",
+ "test_group_warnings_by_message.py::test_bar[[]3[]]",
+ "test_group_warnings_by_message.py::test_bar[[]4[]]",
+ " */test_group_warnings_by_message.py:*: UserWarning: bar",
+ " warnings.warn(UserWarning(msg))",
+ "",
+ "-- Docs: *",
+ "*= 11 passed, 11 warnings *",
+ ],
+ consecutive=True,
+ )
+
+
+@pytest.mark.filterwarnings("always::UserWarning")
+def test_group_warnings_by_message_summary(pytester: Pytester) -> None:
+ pytester.copy_example("warnings/test_group_warnings_by_message_summary")
+ pytester.syspathinsert()
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ [
+ "*== %s ==*" % WARNINGS_SUMMARY_HEADER,
+ "test_1.py: 21 warnings",
+ "test_2.py: 1 warning",
+ " */test_1.py:7: UserWarning: foo",
+ " warnings.warn(UserWarning(msg))",
+ "",
+ "test_1.py: 20 warnings",
+ " */test_1.py:7: UserWarning: bar",
+ " warnings.warn(UserWarning(msg))",
+ "",
+ "-- Docs: *",
+ "*= 42 passed, 42 warnings *",
+ ],
+ consecutive=True,
+ )
+
+
+def test_pytest_configure_warning(pytester: Pytester, recwarn) -> None:
+ """Issue 5115."""
+ pytester.makeconftest(
+ """
+ def pytest_configure():
+ import warnings
+
+ warnings.warn("from pytest_configure")
+ """
+ )
+
+ result = pytester.runpytest()
+ assert result.ret == 5
+ assert "INTERNALERROR" not in result.stderr.str()
+ warning = recwarn.pop()
+ assert str(warning.message) == "from pytest_configure"
+
+
+class TestStackLevel:
+ @pytest.fixture
+ def capwarn(self, pytester: Pytester):
+ class CapturedWarnings:
+ captured: List[
+ Tuple[warnings.WarningMessage, Optional[Tuple[str, int, str]]]
+ ] = []
+
+ @classmethod
+ def pytest_warning_recorded(cls, warning_message, when, nodeid, location):
+ cls.captured.append((warning_message, location))
+
+ pytester.plugins = [CapturedWarnings()]
+
+ return CapturedWarnings
+
+ def test_issue4445_rewrite(self, pytester: Pytester, capwarn) -> None:
+ """#4445: Make sure the warning points to a reasonable location
+ See origin of _issue_warning_captured at: _pytest.assertion.rewrite.py:241
+ """
+ pytester.makepyfile(some_mod="")
+ conftest = pytester.makeconftest(
+ """
+ import some_mod
+ import pytest
+
+ pytest.register_assert_rewrite("some_mod")
+ """
+ )
+ pytester.parseconfig()
+
+ # with stacklevel=5 the warning originates from register_assert_rewrite
+ # function in the created conftest.py
+ assert len(capwarn.captured) == 1
+ warning, location = capwarn.captured.pop()
+ file, lineno, func = location
+
+ assert "Module already imported" in str(warning.message)
+ assert file == str(conftest)
+ assert func == "<module>" # the above conftest.py
+ assert lineno == 4
+
+ def test_issue4445_preparse(self, pytester: Pytester, capwarn) -> None:
+ """#4445: Make sure the warning points to a reasonable location
+ See origin of _issue_warning_captured at: _pytest.config.__init__.py:910
+ """
+ pytester.makeconftest(
+ """
+ import nothing
+ """
+ )
+ pytester.parseconfig("--help")
+
+ # with stacklevel=2 the warning should originate from config._preparse and is
+ # thrown by an erroneous conftest.py
+ assert len(capwarn.captured) == 1
+ warning, location = capwarn.captured.pop()
+ file, _, func = location
+
+ assert "could not load initial conftests" in str(warning.message)
+ assert f"config{os.sep}__init__.py" in file
+ assert func == "_preparse"
+
+ @pytest.mark.filterwarnings("default")
+ def test_conftest_warning_captured(self, pytester: Pytester) -> None:
+ """Warnings raised during importing of conftest.py files is captured (#2891)."""
+ pytester.makeconftest(
+ """
+ import warnings
+ warnings.warn(UserWarning("my custom warning"))
+ """
+ )
+ result = pytester.runpytest()
+ result.stdout.fnmatch_lines(
+ ["conftest.py:2", "*UserWarning: my custom warning*"]
+ )
+
+ def test_issue4445_import_plugin(self, pytester: Pytester, capwarn) -> None:
+ """#4445: Make sure the warning points to a reasonable location"""
+ pytester.makepyfile(
+ some_plugin="""
+ import pytest
+ pytest.skip("thing", allow_module_level=True)
+ """
+ )
+ pytester.syspathinsert()
+ pytester.parseconfig("-p", "some_plugin")
+
+ # with stacklevel=2 the warning should originate from
+ # config.PytestPluginManager.import_plugin is thrown by a skipped plugin
+
+ assert len(capwarn.captured) == 1
+ warning, location = capwarn.captured.pop()
+ file, _, func = location
+
+ assert "skipped plugin 'some_plugin': thing" in str(warning.message)
+ assert f"config{os.sep}__init__.py" in file
+ assert func == "_warn_about_skipped_plugins"
+
+ def test_issue4445_issue5928_mark_generator(self, pytester: Pytester) -> None:
+ """#4445 and #5928: Make sure the warning from an unknown mark points to
+ the test file where this mark is used.
+ """
+ testfile = pytester.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.unknown
+ def test_it():
+ pass
+ """
+ )
+ result = pytester.runpytest_subprocess()
+ # with stacklevel=2 the warning should originate from the above created test file
+ result.stdout.fnmatch_lines_random(
+ [
+ f"*{testfile}:3*",
+ "*Unknown pytest.mark.unknown*",
+ ]
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pytest/testing/typing_checks.py b/testing/web-platform/tests/tools/third_party/pytest/testing/typing_checks.py
new file mode 100644
index 0000000000..0a6b5ad284
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/testing/typing_checks.py
@@ -0,0 +1,24 @@
+"""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 pytest
+
+
+# Issue #7488.
+@pytest.mark.xfail(raises=RuntimeError)
+def check_mark_xfail_raises() -> None:
+ pass
+
+
+# Issue #7494.
+@pytest.fixture(params=[(0, 0), (1, 1)], ids=lambda x: str(x[0]))
+def check_fixture_ids_callable() -> None:
+ pass
+
+
+# Issue #7494.
+@pytest.mark.parametrize("func", [str, int], ids=lambda x: str(x.__name__))
+def check_parametrize_ids_callable(func) -> None:
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pytest/tox.ini b/testing/web-platform/tests/tools/third_party/pytest/tox.ini
new file mode 100644
index 0000000000..b2f90008ce
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pytest/tox.ini
@@ -0,0 +1,184 @@
+[tox]
+isolated_build = True
+minversion = 3.20.0
+distshare = {homedir}/.tox/distshare
+envlist =
+ linting
+ py36
+ py37
+ py38
+ py39
+ py310
+ py311
+ pypy3
+ py37-{pexpect,xdist,unittestextras,numpy,pluggymain}
+ doctesting
+ plugins
+ py37-freeze
+ docs
+ docs-checklinks
+
+
+
+[testenv]
+commands =
+ {env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:{env:_PYTEST_TOX_DEFAULT_POSARGS:}}
+ 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
+setenv =
+ _PYTEST_TOX_DEFAULT_POSARGS={env:_PYTEST_TOX_POSARGS_DOCTESTING:} {env:_PYTEST_TOX_POSARGS_LSOF:} {env:_PYTEST_TOX_POSARGS_XDIST:}
+
+ # Configuration to run with coverage similar to CI, e.g.
+ # "tox -e py37-coverage".
+ coverage: _PYTEST_TOX_COVERAGE_RUN=coverage run -m
+ coverage: _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess
+ coverage: COVERAGE_FILE={toxinidir}/.coverage
+ coverage: COVERAGE_PROCESS_START={toxinidir}/.coveragerc
+
+ doctesting: _PYTEST_TOX_POSARGS_DOCTESTING=doc/en
+
+ nobyte: PYTHONDONTWRITEBYTECODE=1
+
+ lsof: _PYTEST_TOX_POSARGS_LSOF=--lsof
+
+ xdist: _PYTEST_TOX_POSARGS_XDIST=-n auto
+extras = testing
+deps =
+ doctesting: PyYAML
+ numpy: numpy>=1.19.4
+ pexpect: pexpect>=4.8.0
+ pluggymain: pluggy @ git+https://github.com/pytest-dev/pluggy.git
+ unittestextras: twisted
+ unittestextras: asynctest
+ xdist: pytest-xdist>=2.1.0
+ xdist: -e .
+ {env:_PYTEST_TOX_EXTRA_DEP:}
+
+[testenv:linting]
+skip_install = True
+basepython = python3
+deps = pre-commit>=2.9.3
+commands = pre-commit run --all-files --show-diff-on-failure {posargs:}
+
+[testenv:docs]
+basepython = python3
+usedevelop = True
+deps =
+ -r{toxinidir}/doc/en/requirements.txt
+ # https://github.com/twisted/towncrier/issues/340
+ towncrier<21.3.0
+commands =
+ 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 --keep-going -b html doc/en doc/en/_build/html -t changelog_towncrier_draft {posargs:}
+
+[testenv:docs-checklinks]
+basepython = python3
+usedevelop = True
+changedir = doc/en
+deps = -r{toxinidir}/doc/en/requirements.txt
+commands =
+ sphinx-build -W -q --keep-going -b linkcheck . _build
+
+[testenv:regen]
+changedir = doc/en
+basepython = python3
+passenv = SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST
+deps =
+ dataclasses
+ PyYAML
+ regendoc>=0.8.1
+ sphinx
+whitelist_externals =
+ make
+commands =
+ make regen
+
+[testenv:plugins]
+# use latest versions of all plugins, including pre-releases
+pip_pre=true
+# use latest pip and new dependency resolver (#7783)
+download=true
+install_command=python -m pip --use-feature=2020-resolver install {opts} {packages}
+changedir = testing/plugins_integration
+deps = -rtesting/plugins_integration/requirements.txt
+setenv =
+ PYTHONPATH=.
+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
+ pytest --reruns 5 simple_integration.py
+ pytest pytest_anyio_integration.py
+ pytest pytest_asyncio_integration.py
+ pytest pytest_mock_integration.py
+ pytest pytest_trio_integration.py
+ pytest pytest_twisted_integration.py
+ pytest simple_integration.py --force-sugar --flakes
+
+[testenv:py37-freeze]
+changedir = testing/freeze
+deps =
+ pyinstaller
+commands =
+ {envpython} create_executable.py
+ {envpython} tox_run.py
+
+[testenv:release]
+description = do a release, required posarg of the version number
+basepython = python3
+usedevelop = True
+passenv = *
+deps =
+ colorama
+ github3.py
+ pre-commit>=2.9.3
+ wheel
+ # https://github.com/twisted/towncrier/issues/340
+ towncrier<21.3.0
+commands = python scripts/release.py {posargs}
+
+[testenv:prepare-release-pr]
+description = prepare a release PR from a manual trigger in GitHub actions
+usedevelop = {[testenv:release]usedevelop}
+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
+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
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore b/testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore
new file mode 100644
index 0000000000..70f2867054
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore
@@ -0,0 +1,4 @@
+*.pyc
+build/
+*.egg-info/
+dist/
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml b/testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml
new file mode 100644
index 0000000000..2065a644dd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml
@@ -0,0 +1,17 @@
+language: python
+python:
+ - 2.7
+ - 3.5
+ - 3.6
+ - 3.7
+ - 3.8
+ - nightly
+
+matrix:
+ allow_failures:
+ - python: 3.5, nightly
+install:
+ - pip install six yapf
+script:
+ - python test/run_all.py
+ - yapf --diff --recursive .
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING b/testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING
new file mode 100644
index 0000000000..f975be126f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING
@@ -0,0 +1,30 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution;
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+For instructions for contributing code, please read:
+https://github.com/google/pywebsocket/wiki/CodeReviewInstruction
+
+## Community Guidelines
+
+This project follows
+[Google's Open Source Community Guidelines](https://opensource.google/conduct/).
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/LICENSE b/testing/web-platform/tests/tools/third_party/pywebsocket3/LICENSE
new file mode 100644
index 0000000000..c91bea9025
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/LICENSE
@@ -0,0 +1,28 @@
+Copyright 2020, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in b/testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in
new file mode 100644
index 0000000000..19256882c5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in
@@ -0,0 +1,6 @@
+include COPYING
+include MANIFEST.in
+include README
+recursive-include example *.py
+recursive-include mod_pywebsocket *.py
+recursive-include test *.py
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/README.md b/testing/web-platform/tests/tools/third_party/pywebsocket3/README.md
new file mode 100644
index 0000000000..8684f2cc7e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/README.md
@@ -0,0 +1,36 @@
+
+# pywebsocket3 #
+
+The pywebsocket project aims to provide a [WebSocket](https://tools.ietf.org/html/rfc6455) standalone server.
+
+pywebsocket is intended for **testing** or **experimental** purposes.
+
+Run this to read the general document:
+```
+$ pydoc mod_pywebsocket
+```
+
+Please see [Wiki](https://github.com/GoogleChromeLabs/pywebsocket3/wiki) for more details.
+
+# INSTALL #
+
+To install this package to the system, run this:
+```
+$ python setup.py build
+$ sudo python setup.py install
+```
+
+To install this package as a normal user, run this instead:
+
+```
+$ python setup.py build
+$ python setup.py install --user
+```
+# LAUNCH #
+
+To use pywebsocket as standalone server, run this to read the document:
+```
+$ pydoc mod_pywebsocket.standalone
+```
+# Disclaimer #
+This is not an officially supported Google product
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py
new file mode 100644
index 0000000000..1b719ca897
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py
@@ -0,0 +1,43 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from mod_pywebsocket import handshake
+
+
+def web_socket_do_extra_handshake(request):
+ raise handshake.AbortedByUserException(
+ "Aborted in web_socket_do_extra_handshake")
+
+
+def web_socket_transfer_data(request):
+ pass
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py
new file mode 100644
index 0000000000..d4c240bf2c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py
@@ -0,0 +1,43 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from mod_pywebsocket import handshake
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ raise handshake.AbortedByUserException(
+ "Aborted in web_socket_transfer_data")
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/arraybuffer_benchmark.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/arraybuffer_benchmark.html
new file mode 100644
index 0000000000..869cd7e1ee
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/arraybuffer_benchmark.html
@@ -0,0 +1,134 @@
+<!--
+Copyright 2013, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+
+<html>
+<head>
+<title>ArrayBuffer benchmark</title>
+<script src="util.js"></script>
+<script>
+var PRINT_SIZE = true;
+
+// Initial size of arrays.
+var START_SIZE = 10 * 1024;
+// Stops benchmark when the size of an array exceeds this threshold.
+var STOP_THRESHOLD = 100000 * 1024;
+// If the size of each array is small, write/read the array multiple times
+// until the sum of sizes reaches this threshold.
+var MIN_TOTAL = 100000 * 1024;
+var MULTIPLIERS = [5, 2];
+
+// Repeat benchmark for several times to measure performance of optimized
+// (such as JIT) run.
+var REPEAT_FOR_WARMUP = 3;
+
+function writeBenchmark(size, minTotal) {
+ var totalSize = 0;
+ while (totalSize < minTotal) {
+ var arrayBuffer = new ArrayBuffer(size);
+
+ // Write 'a's.
+ fillArrayBuffer(arrayBuffer, 0x61);
+
+ totalSize += size;
+ }
+ return totalSize;
+}
+
+function readBenchmark(size, minTotal) {
+ var totalSize = 0;
+ while (totalSize < minTotal) {
+ var arrayBuffer = new ArrayBuffer(size);
+
+ if (!verifyArrayBuffer(arrayBuffer, 0x00)) {
+ queueLog('Verification failed');
+ return -1;
+ }
+
+ totalSize += size;
+ }
+ return totalSize;
+}
+
+function runBenchmark(benchmarkFunction,
+ size,
+ stopThreshold,
+ minTotal,
+ multipliers,
+ multiplierIndex) {
+ while (size <= stopThreshold) {
+ var maxSpeed = 0;
+
+ for (var i = 0; i < REPEAT_FOR_WARMUP; ++i) {
+ var startTimeInMs = getTimeStamp();
+
+ var totalSize = benchmarkFunction(size, minTotal);
+
+ maxSpeed = Math.max(maxSpeed,
+ calculateSpeedInKB(totalSize, startTimeInMs));
+ }
+ queueLog(formatResultInKiB(size, maxSpeed, PRINT_SIZE));
+
+ size *= multipliers[multiplierIndex];
+ multiplierIndex = (multiplierIndex + 1) % multipliers.length;
+ }
+}
+
+function runBenchmarks() {
+ queueLog('Message size in KiB, Speed in kB/s');
+
+ queueLog('Write benchmark');
+ runBenchmark(
+ writeBenchmark, START_SIZE, STOP_THRESHOLD, MIN_TOTAL, MULTIPLIERS, 0);
+ queueLog('Finished');
+
+ queueLog('Read benchmark');
+ runBenchmark(
+ readBenchmark, START_SIZE, STOP_THRESHOLD, MIN_TOTAL, MULTIPLIERS, 0);
+ addToLog('Finished');
+}
+
+function init() {
+ logBox = document.getElementById('log');
+
+ queueLog(window.navigator.userAgent.toLowerCase());
+
+ addToLog('Started...');
+
+ setTimeout(runBenchmarks, 0);
+}
+
+</script>
+</head>
+<body onload="init()">
+<textarea
+ id="log" rows="50" style="width: 100%" readonly></textarea>
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py
new file mode 100644
index 0000000000..2df50e77db
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py
@@ -0,0 +1,59 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""A simple load tester for WebSocket clients.
+
+A client program sends a message formatted as "<time> <count> <message>" to
+this handler. This handler starts sending total <count> WebSocket messages
+containing <message> every <time> seconds. <time> can be a floating point
+value. <count> must be an integer value.
+"""
+
+from __future__ import absolute_import
+import time
+from six.moves import range
+
+
+def web_socket_do_extra_handshake(request):
+ pass # Always accept.
+
+
+def web_socket_transfer_data(request):
+ line = request.ws_stream.receive_message()
+ parts = line.split(' ')
+ if len(parts) != 3:
+ raise ValueError('Bad parameter format')
+ wait = float(parts[0])
+ count = int(parts[1])
+ message = parts[2]
+ for i in range(count):
+ request.ws_stream.send_message(message)
+ time.sleep(wait)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html
new file mode 100644
index 0000000000..f1e5c97b3a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html
@@ -0,0 +1,175 @@
+<!--
+Copyright 2013, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+
+<html>
+<head>
+<title>WebSocket benchmark</title>
+<script src="util_main.js"></script>
+<script src="util.js"></script>
+<script src="benchmark.js"></script>
+<script>
+var addressBox = null;
+
+function getConfig() {
+ return {
+ prefixUrl: addressBox.value,
+ printSize: getBoolFromCheckBox('printsize'),
+ numSockets: getIntFromInput('numsockets'),
+ // Initial size of messages.
+ numIterations: getIntFromInput('numiterations'),
+ numWarmUpIterations: getIntFromInput('numwarmupiterations'),
+ startSize: getIntFromInput('startsize'),
+ // Stops benchmark when the size of message exceeds this threshold.
+ stopThreshold: getIntFromInput('stopthreshold'),
+ // If the size of each message is small, send/receive multiple messages
+ // until the sum of sizes reaches this threshold.
+ minTotal: getIntFromInput('mintotal'),
+ multipliers: getFloatArrayFromInput('multipliers'),
+ verifyData: getBoolFromCheckBox('verifydata'),
+ addToLog: addToLog,
+ addToSummary: addToSummary,
+ measureValue: measureValue,
+ notifyAbort: notifyAbort
+ };
+}
+
+function onSendBenchmark() {
+ var config = getConfig();
+ doAction(config, getBoolFromCheckBox('worker'), 'sendBenchmark');
+}
+
+function onReceiveBenchmark() {
+ var config = getConfig();
+ doAction(config, getBoolFromCheckBox('worker'), 'receiveBenchmark');
+}
+
+function onBatchBenchmark() {
+ var config = getConfig();
+ doAction(config, getBoolFromCheckBox('worker'), 'batchBenchmark');
+}
+
+function onStop() {
+ var config = getConfig();
+ doAction(config, getBoolFromCheckBox('worker'), 'stop');
+}
+
+function init() {
+ addressBox = document.getElementById('address');
+ logBox = document.getElementById('log');
+
+ summaryBox = document.getElementById('summary');
+
+ var scheme = window.location.protocol == 'https:' ? 'wss://' : 'ws://';
+ var defaultAddress = scheme + window.location.host + '/benchmark_helper';
+
+ addressBox.value = defaultAddress;
+
+ addToLog(window.navigator.userAgent.toLowerCase());
+ addToSummary(window.navigator.userAgent.toLowerCase());
+
+ if (!('WebSocket' in window)) {
+ addToLog('WebSocket is not available');
+ }
+
+ initWorker('');
+}
+</script>
+</head>
+<body onload="init()">
+
+<div id="benchmark_div">
+ url <input type="text" id="address" size="40">
+ <input type="button" value="send" onclick="onSendBenchmark()">
+ <input type="button" value="receive" onclick="onReceiveBenchmark()">
+ <input type="button" value="batch" onclick="onBatchBenchmark()">
+ <input type="button" value="stop" onclick="onStop()">
+
+ <br/>
+
+ <input type="checkbox" id="printsize" checked>
+ <label for="printsize">Print size and time per message</label>
+ <input type="checkbox" id="verifydata" checked>
+ <label for="verifydata">Verify data</label>
+ <input type="checkbox" id="worker">
+ <label for="worker">Run on worker</label>
+
+ <br/>
+
+ Parameters:
+
+ <br/>
+
+ <table>
+ <tr>
+ <td>Num sockets</td>
+ <td><input type="text" id="numsockets" value="1"></td>
+ </tr>
+ <tr>
+ <td>Number of iterations</td>
+ <td><input type="text" id="numiterations" value="1"></td>
+ </tr>
+ <tr>
+ <td>Number of warm-up iterations</td>
+ <td><input type="text" id="numwarmupiterations" value="0"></td>
+ </tr>
+ <tr>
+ <td>Start size</td>
+ <td><input type="text" id="startsize" value="10240"></td>
+ </tr>
+ <tr>
+ <td>Stop threshold</td>
+ <td><input type="text" id="stopthreshold" value="102400000"></td>
+ </tr>
+ <tr>
+ <td>Minimum total</td>
+ <td><input type="text" id="mintotal" value="102400000"></td>
+ </tr>
+ <tr>
+ <td>Multipliers</td>
+ <td><input type="text" id="multipliers" value="5, 2"></td>
+ </tr>
+ </table>
+</div>
+
+<div id="log_div">
+ <textarea
+ id="log" rows="20" style="width: 100%" readonly></textarea>
+</div>
+<div id="summary_div">
+ Summary
+ <textarea
+ id="summary" rows="20" style="width: 100%" readonly></textarea>
+</div>
+
+Note: Effect of RTT is not eliminated.
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.js
new file mode 100644
index 0000000000..2701472a4f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.js
@@ -0,0 +1,238 @@
+// Copyright 2014, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+if (typeof importScripts !== "undefined") {
+ // Running on a worker
+ importScripts('util.js', 'util_worker.js');
+}
+
+// Namespace for holding globals.
+var benchmark = {startTimeInMs: 0};
+
+var sockets = [];
+var numEstablishedSockets = 0;
+
+var timerID = null;
+
+function destroySocket(socket) {
+ socket.onopen = null;
+ socket.onmessage = null;
+ socket.onerror = null;
+ socket.onclose = null;
+ socket.close();
+}
+
+function destroyAllSockets() {
+ for (var i = 0; i < sockets.length; ++i) {
+ destroySocket(sockets[i]);
+ }
+ sockets = [];
+}
+
+function sendBenchmarkStep(size, config, isWarmUp) {
+ timerID = null;
+
+ var totalSize = 0;
+ var totalReplied = 0;
+
+ var onMessageHandler = function(event) {
+ if (!verifyAcknowledgement(config, event.data, size)) {
+ destroyAllSockets();
+ config.notifyAbort();
+ return;
+ }
+
+ totalReplied += size;
+
+ if (totalReplied < totalSize) {
+ return;
+ }
+
+ calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize,
+ isWarmUp);
+
+ runNextTask(config);
+ };
+
+ for (var i = 0; i < sockets.length; ++i) {
+ var socket = sockets[i];
+ socket.onmessage = onMessageHandler;
+ }
+
+ var dataArray = [];
+
+ while (totalSize < config.minTotal) {
+ var buffer = new ArrayBuffer(size);
+
+ fillArrayBuffer(buffer, 0x61);
+
+ dataArray.push(buffer);
+ totalSize += size;
+ }
+
+ benchmark.startTimeInMs = getTimeStamp();
+
+ totalSize = 0;
+
+ var socketIndex = 0;
+ var dataIndex = 0;
+ while (totalSize < config.minTotal) {
+ var command = ['send'];
+ command.push(config.verifyData ? '1' : '0');
+ sockets[socketIndex].send(command.join(' '));
+ sockets[socketIndex].send(dataArray[dataIndex]);
+ socketIndex = (socketIndex + 1) % sockets.length;
+
+ totalSize += size;
+ ++dataIndex;
+ }
+}
+
+function receiveBenchmarkStep(size, config, isWarmUp) {
+ timerID = null;
+
+ var totalSize = 0;
+ var totalReplied = 0;
+
+ var onMessageHandler = function(event) {
+ var bytesReceived = event.data.byteLength;
+ if (bytesReceived != size) {
+ config.addToLog('Expected ' + size + 'B but received ' +
+ bytesReceived + 'B');
+ destroyAllSockets();
+ config.notifyAbort();
+ return;
+ }
+
+ if (config.verifyData && !verifyArrayBuffer(event.data, 0x61)) {
+ config.addToLog('Response verification failed');
+ destroyAllSockets();
+ config.notifyAbort();
+ return;
+ }
+
+ totalReplied += bytesReceived;
+
+ if (totalReplied < totalSize) {
+ return;
+ }
+
+ calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize,
+ isWarmUp);
+
+ runNextTask(config);
+ };
+
+ for (var i = 0; i < sockets.length; ++i) {
+ var socket = sockets[i];
+ socket.binaryType = 'arraybuffer';
+ socket.onmessage = onMessageHandler;
+ }
+
+ benchmark.startTimeInMs = getTimeStamp();
+
+ var socketIndex = 0;
+ while (totalSize < config.minTotal) {
+ sockets[socketIndex].send('receive ' + size);
+ socketIndex = (socketIndex + 1) % sockets.length;
+
+ totalSize += size;
+ }
+}
+
+function createSocket(config) {
+ // TODO(tyoshino): Add TCP warm up.
+ var url = config.prefixUrl;
+
+ config.addToLog('Connect ' + url);
+
+ var socket = new WebSocket(url);
+ socket.onmessage = function(event) {
+ config.addToLog('Unexpected message received. Aborting.');
+ };
+ socket.onerror = function() {
+ config.addToLog('Error');
+ };
+ socket.onclose = function(event) {
+ config.addToLog('Closed');
+ config.notifyAbort();
+ };
+ return socket;
+}
+
+function startBenchmark(config) {
+ clearTimeout(timerID);
+ destroyAllSockets();
+
+ numEstablishedSockets = 0;
+
+ for (var i = 0; i < config.numSockets; ++i) {
+ var socket = createSocket(config);
+ socket.onopen = function() {
+ config.addToLog('Opened');
+
+ ++numEstablishedSockets;
+
+ if (numEstablishedSockets == sockets.length) {
+ runNextTask(config);
+ }
+ };
+ sockets.push(socket);
+ }
+}
+
+function getConfigString(config) {
+ return '(WebSocket' +
+ ', ' + (typeof importScripts !== "undefined" ? 'Worker' : 'Main') +
+ ', numSockets=' + config.numSockets +
+ ', numIterations=' + config.numIterations +
+ ', verifyData=' + config.verifyData +
+ ', minTotal=' + config.minTotal +
+ ', numWarmUpIterations=' + config.numWarmUpIterations +
+ ')';
+}
+
+function batchBenchmark(config) {
+ config.addToLog('Batch benchmark');
+ config.addToLog(buildLegendString(config));
+
+ tasks = [];
+ clearAverageData();
+ addTasks(config, sendBenchmarkStep);
+ addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config));
+ addTasks(config, receiveBenchmarkStep);
+ addResultReportingTask(config, 'Receive Benchmark ' +
+ getConfigString(config));
+ startBenchmark(config);
+}
+
+function cleanup() {
+ destroyAllSockets();
+}
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py
new file mode 100644
index 0000000000..fc17533335
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py
@@ -0,0 +1,84 @@
+# Copyright 2013, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Handler for benchmark.html."""
+from __future__ import absolute_import
+import six
+
+
+def web_socket_do_extra_handshake(request):
+ # Turn off compression.
+ request.ws_extension_processors = []
+
+
+def web_socket_transfer_data(request):
+ data = b''
+
+ while True:
+ command = request.ws_stream.receive_message()
+ if command is None:
+ return
+
+ if not isinstance(command, six.text_type):
+ raise ValueError('Invalid command data:' + command)
+ commands = command.split(' ')
+ if len(commands) == 0:
+ raise ValueError('Invalid command data: ' + command)
+
+ if commands[0] == 'receive':
+ if len(commands) != 2:
+ raise ValueError(
+ 'Illegal number of arguments for send command' + command)
+ size = int(commands[1])
+
+ # Reuse data if possible.
+ if len(data) != size:
+ data = b'a' * size
+ request.ws_stream.send_message(data, binary=True)
+ elif commands[0] == 'send':
+ if len(commands) != 2:
+ raise ValueError(
+ 'Illegal number of arguments for receive command' +
+ command)
+ verify_data = commands[1] == '1'
+
+ data = request.ws_stream.receive_message()
+ if data is None:
+ raise ValueError('Payload not received')
+ size = len(data)
+
+ if verify_data:
+ if data != b'a' * size:
+ raise ValueError('Payload verification failed')
+
+ request.ws_stream.send_message(str(size))
+ else:
+ raise ValueError('Invalid command: ' + commands[0])
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cgi-bin/hi.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cgi-bin/hi.py
new file mode 100755
index 0000000000..f136f2c442
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cgi-bin/hi.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+
+print('Content-Type: text/plain')
+print('')
+print('Hi from hi.py')
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py
new file mode 100644
index 0000000000..8f0005ffea
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py
@@ -0,0 +1,70 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+import struct
+
+from mod_pywebsocket import common
+from mod_pywebsocket import stream
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ while True:
+ line = request.ws_stream.receive_message()
+ if line is None:
+ return
+ code, reason = line.split(' ', 1)
+ if code is None or reason is None:
+ return
+ request.ws_stream.close_connection(int(code), reason)
+ # close_connection() initiates closing handshake. It validates code
+ # and reason. If you want to send a broken close frame for a test,
+ # following code will be useful.
+ # > data = struct.pack('!H', int(code)) + reason.encode('UTF-8')
+ # > request.connection.write(stream.create_close_frame(data))
+ # > # Suppress to re-respond client responding close frame.
+ # > raise Exception("customized server initiated closing handshake")
+
+
+def web_socket_passive_closing_handshake(request):
+ # Simply echo a close status code
+ code, reason = request.ws_close_code, request.ws_close_reason
+
+ # pywebsocket sets pseudo code for receiving an empty body close frame.
+ if code == common.STATUS_NO_STATUS_RECEIVED:
+ code = None
+ reason = ''
+ return code, reason
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/console.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/console.html
new file mode 100644
index 0000000000..ccd6d8f806
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/console.html
@@ -0,0 +1,317 @@
+<!--
+Copyright 2011, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+
+<!--
+A simple console for testing WebSocket server.
+
+Type an address into the top text input and click connect to establish
+WebSocket. Then, type some message into the bottom text input and click send
+to send the message. Received/sent messages and connection state will be shown
+on the middle textarea.
+-->
+
+<html>
+<head>
+<title>WebSocket console</title>
+<script>
+var socket = null;
+
+var showTimeStamp = false;
+
+var addressBox = null;
+var protocolsBox = null;
+var logBox = null;
+var messageBox = null;
+var fileBox = null;
+var codeBox = null;
+var reasonBox = null;
+
+function getTimeStamp() {
+ return new Date().getTime();
+}
+
+function addToLog(log) {
+ if (showTimeStamp) {
+ logBox.value += '[' + getTimeStamp() + '] ';
+ }
+ logBox.value += log + '\n'
+ // Large enough to keep showing the latest message.
+ logBox.scrollTop = 1000000;
+}
+
+function setbinarytype(binaryType) {
+ if (!socket) {
+ addToLog('Not connected');
+ return;
+ }
+
+ socket.binaryType = binaryType;
+ addToLog('Set binaryType to ' + binaryType);
+}
+
+function send() {
+ if (!socket) {
+ addToLog('Not connected');
+ return;
+ }
+
+ socket.send(messageBox.value);
+ addToLog('> ' + messageBox.value);
+ messageBox.value = '';
+}
+
+function sendfile() {
+ if (!socket) {
+ addToLog('Not connected');
+ return;
+ }
+
+ var files = fileBox.files;
+
+ if (files.length == 0) {
+ addToLog('File not selected');
+ return;
+ }
+
+ socket.send(files[0]);
+ addToLog('> Send ' + files[0].name);
+}
+
+function parseProtocols(protocolsText) {
+ var protocols = protocolsText.split(',');
+ for (var i = 0; i < protocols.length; ++i) {
+ protocols[i] = protocols[i].trim();
+ }
+
+ if (protocols.length == 0) {
+ // Don't pass.
+ protocols = null;
+ } else if (protocols.length == 1) {
+ if (protocols[0].length == 0) {
+ // Don't pass.
+ protocols = null;
+ } else {
+ // Pass as a string.
+ protocols = protocols[0];
+ }
+ }
+
+ return protocols;
+}
+
+function connect() {
+ var url = addressBox.value;
+ var protocols = parseProtocols(protocolsBox.value);
+
+ if ('WebSocket' in window) {
+ if (protocols) {
+ socket = new WebSocket(url, protocols);
+ } else {
+ socket = new WebSocket(url);
+ }
+ } else {
+ return;
+ }
+
+ socket.onopen = function () {
+ var extraInfo = [];
+ if (('protocol' in socket) && socket.protocol) {
+ extraInfo.push('protocol = ' + socket.protocol);
+ }
+ if (('extensions' in socket) && socket.extensions) {
+ extraInfo.push('extensions = ' + socket.extensions);
+ }
+
+ var logMessage = 'Opened';
+ if (extraInfo.length > 0) {
+ logMessage += ' (' + extraInfo.join(', ') + ')';
+ }
+ addToLog(logMessage);
+ };
+ socket.onmessage = function (event) {
+ if (('ArrayBuffer' in window) && (event.data instanceof ArrayBuffer)) {
+ addToLog('< Received an ArrayBuffer of ' + event.data.byteLength +
+ ' bytes')
+ } else if (('Blob' in window) && (event.data instanceof Blob)) {
+ addToLog('< Received a Blob of ' + event.data.size + ' bytes')
+ } else {
+ addToLog('< ' + event.data);
+ }
+ };
+ socket.onerror = function () {
+ addToLog('Error');
+ };
+ socket.onclose = function (event) {
+ var logMessage = 'Closed (';
+ if ((arguments.length == 1) && ('CloseEvent' in window) &&
+ (event instanceof CloseEvent)) {
+ logMessage += 'wasClean = ' + event.wasClean;
+ // code and reason are present only for
+ // draft-ietf-hybi-thewebsocketprotocol-06 and later
+ if ('code' in event) {
+ logMessage += ', code = ' + event.code;
+ }
+ if ('reason' in event) {
+ logMessage += ', reason = ' + event.reason;
+ }
+ } else {
+ logMessage += 'CloseEvent is not available';
+ }
+ addToLog(logMessage + ')');
+ };
+
+ if (protocols) {
+ addToLog('Connect ' + url + ' (protocols = ' + protocols + ')');
+ } else {
+ addToLog('Connect ' + url);
+ }
+}
+
+function closeSocket() {
+ if (!socket) {
+ addToLog('Not connected');
+ return;
+ }
+
+ if (codeBox.value || reasonBox.value) {
+ socket.close(codeBox.value, reasonBox.value);
+ } else {
+ socket.close();
+ }
+}
+
+function printState() {
+ if (!socket) {
+ addToLog('Not connected');
+ return;
+ }
+
+ addToLog(
+ 'url = ' + socket.url +
+ ', readyState = ' + socket.readyState +
+ ', bufferedAmount = ' + socket.bufferedAmount);
+}
+
+function init() {
+ var scheme = window.location.protocol == 'https:' ? 'wss://' : 'ws://';
+ var defaultAddress = scheme + window.location.host + '/echo';
+
+ addressBox = document.getElementById('address');
+ protocolsBox = document.getElementById('protocols');
+ logBox = document.getElementById('log');
+ messageBox = document.getElementById('message');
+ fileBox = document.getElementById('file');
+ codeBox = document.getElementById('code');
+ reasonBox = document.getElementById('reason');
+
+ addressBox.value = defaultAddress;
+
+ if (!('WebSocket' in window)) {
+ addToLog('WebSocket is not available');
+ }
+}
+</script>
+<style type="text/css">
+form {
+ margin: 0px;
+}
+
+#connect_div, #log_div, #send_div, #sendfile_div, #close_div, #printstate_div {
+ padding: 5px;
+ margin: 5px;
+ border-width: 0px 0px 0px 10px;
+ border-style: solid;
+ border-color: silver;
+}
+</style>
+</head>
+<body onload="init()">
+
+<div>
+
+<div id="connect_div">
+ <form action="#" onsubmit="connect(); return false;">
+ url <input type="text" id="address" size="40">
+ <input type="submit" value="connect">
+ <br/>
+ protocols <input type="text" id="protocols" size="20">
+ </form>
+</div>
+
+<div id="log_div">
+ <textarea id="log" rows="10" cols="40" readonly></textarea>
+ <br/>
+ <input type="checkbox"
+ name="showtimestamp"
+ value="showtimestamp"
+ onclick="showTimeStamp = this.checked">Show time stamp
+</div>
+
+<div id="send_div">
+ <form action="#" onsubmit="send(); return false;">
+ data <input type="text" id="message" size="40">
+ <input type="submit" value="send">
+ </form>
+</div>
+
+<div id="sendfile_div">
+ <form action="#" onsubmit="sendfile(); return false;">
+ <input type="file" id="file" size="40">
+ <input type="submit" value="send file">
+ </form>
+
+ Set binaryType
+ <input type="radio"
+ name="binarytype"
+ value="blob"
+ onclick="setbinarytype('blob')" checked>blob
+ <input type="radio"
+ name="binarytype"
+ value="arraybuffer"
+ onclick="setbinarytype('arraybuffer')">arraybuffer
+</div>
+
+<div id="close_div">
+ <form action="#" onsubmit="closeSocket(); return false;">
+ code <input type="text" id="code" size="10">
+ reason <input type="text" id="reason" size="20">
+ <input type="submit" value="close">
+ </form>
+</div>
+
+<div id="printstate_div">
+ <input type="button" value="print state" onclick="printState();">
+</div>
+
+</div>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py
new file mode 100644
index 0000000000..815209694e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py
@@ -0,0 +1,54 @@
+# Copyright 2020 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from six.moves import urllib
+
+
+def _add_set_cookie(request, value):
+ request.extra_headers.append(('Set-Cookie', value))
+
+
+def web_socket_do_extra_handshake(request):
+ components = urllib.parse.urlparse(request.uri)
+ command = components[4]
+
+ ONE_DAY_LIFE = 'Max-Age=86400'
+
+ if command == 'set':
+ _add_set_cookie(request, '; '.join(['foo=bar', ONE_DAY_LIFE]))
+ elif command == 'set_httponly':
+ _add_set_cookie(
+ request, '; '.join(['httpOnlyFoo=bar', ONE_DAY_LIFE, 'httpOnly']))
+ elif command == 'clear':
+ _add_set_cookie(request, 'foo=0; Max-Age=0')
+ _add_set_cookie(request, 'httpOnlyFoo=0; Max-Age=0')
+
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py
new file mode 100755
index 0000000000..2ed60b3b59
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py
@@ -0,0 +1,699 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Simple WebSocket client named echo_client just because of historical reason.
+
+mod_pywebsocket directory must be in PYTHONPATH.
+
+Example Usage:
+
+# server setup
+ % cd $pywebsocket
+ % PYTHONPATH=$cwd/src python ./mod_pywebsocket/standalone.py -p 8880 \
+ -d $cwd/src/example
+
+# run client
+ % PYTHONPATH=$cwd/src python ./src/example/echo_client.py -p 8880 \
+ -s localhost \
+ -o http://localhost -r /echo -m test
+"""
+
+from __future__ import absolute_import
+from __future__ import print_function
+import base64
+import codecs
+from hashlib import sha1
+import logging
+import argparse
+import os
+import random
+import re
+import six
+import socket
+import ssl
+import struct
+import sys
+
+from mod_pywebsocket import common
+from mod_pywebsocket.extensions import PerMessageDeflateExtensionProcessor
+from mod_pywebsocket.extensions import _PerMessageDeflateFramer
+from mod_pywebsocket.extensions import _parse_window_bits
+from mod_pywebsocket.stream import Stream
+from mod_pywebsocket.stream import StreamOptions
+from mod_pywebsocket import util
+
+_TIMEOUT_SEC = 10
+_UNDEFINED_PORT = -1
+
+_UPGRADE_HEADER = 'Upgrade: websocket\r\n'
+_CONNECTION_HEADER = 'Connection: Upgrade\r\n'
+
+# Special message that tells the echo server to start closing handshake
+_GOODBYE_MESSAGE = 'Goodbye'
+
+_PROTOCOL_VERSION_HYBI13 = 'hybi13'
+
+
+class ClientHandshakeError(Exception):
+ pass
+
+
+def _build_method_line(resource):
+ return 'GET %s HTTP/1.1\r\n' % resource
+
+
+def _origin_header(header, origin):
+ # 4.1 13. concatenation of the string "Origin:", a U+0020 SPACE character,
+ # and the /origin/ value, converted to ASCII lowercase, to /fields/.
+ return '%s: %s\r\n' % (header, origin.lower())
+
+
+def _format_host_header(host, port, secure):
+ # 4.1 9. Let /hostport/ be an empty string.
+ # 4.1 10. Append the /host/ value, converted to ASCII lowercase, to
+ # /hostport/
+ hostport = host.lower()
+ # 4.1 11. If /secure/ is false, and /port/ is not 80, or if /secure/
+ # is true, and /port/ is not 443, then append a U+003A COLON character
+ # (:) followed by the value of /port/, expressed as a base-ten integer,
+ # to /hostport/
+ if ((not secure and port != common.DEFAULT_WEB_SOCKET_PORT)
+ or (secure and port != common.DEFAULT_WEB_SOCKET_SECURE_PORT)):
+ hostport += ':' + str(port)
+ # 4.1 12. concatenation of the string "Host:", a U+0020 SPACE
+ # character, and /hostport/, to /fields/.
+ return '%s: %s\r\n' % (common.HOST_HEADER, hostport)
+
+
+def _receive_bytes(socket, length):
+ recv_bytes = []
+ remaining = length
+ while remaining > 0:
+ received_bytes = socket.recv(remaining)
+ if not received_bytes:
+ raise IOError(
+ 'Connection closed before receiving requested length '
+ '(requested %d bytes but received only %d bytes)' %
+ (length, length - remaining))
+ recv_bytes.append(received_bytes)
+ remaining -= len(received_bytes)
+ return b''.join(recv_bytes)
+
+
+def _get_mandatory_header(fields, name):
+ """Gets the value of the header specified by name from fields.
+
+ This function expects that there's only one header with the specified name
+ in fields. Otherwise, raises an ClientHandshakeError.
+ """
+
+ values = fields.get(name.lower())
+ if values is None or len(values) == 0:
+ raise ClientHandshakeError('%s header not found: %r' % (name, values))
+ if len(values) > 1:
+ raise ClientHandshakeError('Multiple %s headers found: %r' %
+ (name, values))
+ return values[0]
+
+
+def _validate_mandatory_header(fields,
+ name,
+ expected_value,
+ case_sensitive=False):
+ """Gets and validates the value of the header specified by name from
+ fields.
+
+ If expected_value is specified, compares expected value and actual value
+ and raises an ClientHandshakeError on failure. You can specify case
+ sensitiveness in this comparison by case_sensitive parameter. This function
+ expects that there's only one header with the specified name in fields.
+ Otherwise, raises an ClientHandshakeError.
+ """
+
+ value = _get_mandatory_header(fields, name)
+
+ if ((case_sensitive and value != expected_value) or
+ (not case_sensitive and value.lower() != expected_value.lower())):
+ raise ClientHandshakeError(
+ 'Illegal value for header %s: %r (expected) vs %r (actual)' %
+ (name, expected_value, value))
+
+
+class _TLSSocket(object):
+ """Wrapper for a TLS connection."""
+ def __init__(self, raw_socket):
+ self._logger = util.get_class_logger(self)
+
+ self._tls_socket = ssl.wrap_socket(raw_socket)
+
+ # Print cipher in use. Handshake is done on wrap_socket call.
+ self._logger.info("Cipher: %s", self._tls_socket.cipher())
+
+ def send(self, data):
+ return self._tls_socket.write(data)
+
+ def sendall(self, data):
+ return self._tls_socket.sendall(data)
+
+ def recv(self, size=-1):
+ return self._tls_socket.read(size)
+
+ def close(self):
+ return self._tls_socket.close()
+
+ def getpeername(self):
+ return self._tls_socket.getpeername()
+
+
+class ClientHandshakeBase(object):
+ """A base class for WebSocket opening handshake processors for each
+ protocol version.
+ """
+ def __init__(self):
+ self._logger = util.get_class_logger(self)
+
+ def _read_fields(self):
+ # 4.1 32. let /fields/ be a list of name-value pairs, initially empty.
+ fields = {}
+ while True: # "Field"
+ # 4.1 33. let /name/ and /value/ be empty byte arrays
+ name = b''
+ value = b''
+ # 4.1 34. read /name/
+ name = self._read_name()
+ if name is None:
+ break
+ # 4.1 35. read spaces
+ # TODO(tyoshino): Skip only one space as described in the spec.
+ ch = self._skip_spaces()
+ # 4.1 36. read /value/
+ value = self._read_value(ch)
+ # 4.1 37. read a byte from the server
+ ch = _receive_bytes(self._socket, 1)
+ if ch != b'\n': # 0x0A
+ raise ClientHandshakeError(
+ 'Expected LF but found %r while reading value %r for '
+ 'header %r' % (ch, value, name))
+ self._logger.debug('Received %r header', name)
+ # 4.1 38. append an entry to the /fields/ list that has the name
+ # given by the string obtained by interpreting the /name/ byte
+ # array as a UTF-8 stream and the value given by the string
+ # obtained by interpreting the /value/ byte array as a UTF-8 byte
+ # stream.
+ fields.setdefault(name.decode('UTF-8'),
+ []).append(value.decode('UTF-8'))
+ # 4.1 39. return to the "Field" step above
+ return fields
+
+ def _read_name(self):
+ # 4.1 33. let /name/ be empty byte arrays
+ name = b''
+ while True:
+ # 4.1 34. read a byte from the server
+ ch = _receive_bytes(self._socket, 1)
+ if ch == b'\r': # 0x0D
+ return None
+ elif ch == b'\n': # 0x0A
+ raise ClientHandshakeError(
+ 'Unexpected LF when reading header name %r' % name)
+ elif ch == b':': # 0x3A
+ return name.lower()
+ else:
+ name += ch
+
+ def _skip_spaces(self):
+ # 4.1 35. read a byte from the server
+ while True:
+ ch = _receive_bytes(self._socket, 1)
+ if ch == b' ': # 0x20
+ continue
+ return ch
+
+ def _read_value(self, ch):
+ # 4.1 33. let /value/ be empty byte arrays
+ value = b''
+ # 4.1 36. read a byte from server.
+ while True:
+ if ch == b'\r': # 0x0D
+ return value
+ elif ch == b'\n': # 0x0A
+ raise ClientHandshakeError(
+ 'Unexpected LF when reading header value %r' % value)
+ else:
+ value += ch
+ ch = _receive_bytes(self._socket, 1)
+
+
+def _get_permessage_deflate_framer(extension_response):
+ """Validate the response and return a framer object using the parameters in
+ the response. This method doesn't accept the server_.* parameters.
+ """
+
+ client_max_window_bits = None
+ client_no_context_takeover = None
+
+ client_max_window_bits_name = (
+ PerMessageDeflateExtensionProcessor._CLIENT_MAX_WINDOW_BITS_PARAM)
+ client_no_context_takeover_name = (
+ PerMessageDeflateExtensionProcessor._CLIENT_NO_CONTEXT_TAKEOVER_PARAM)
+
+ # We didn't send any server_.* parameter.
+ # Handle those parameters as invalid if found in the response.
+
+ for param_name, param_value in extension_response.get_parameters():
+ if param_name == client_max_window_bits_name:
+ if client_max_window_bits is not None:
+ raise ClientHandshakeError('Multiple %s found' %
+ client_max_window_bits_name)
+
+ parsed_value = _parse_window_bits(param_value)
+ if parsed_value is None:
+ raise ClientHandshakeError(
+ 'Bad %s: %r' % (client_max_window_bits_name, param_value))
+ client_max_window_bits = parsed_value
+ elif param_name == client_no_context_takeover_name:
+ if client_no_context_takeover is not None:
+ raise ClientHandshakeError('Multiple %s found' %
+ client_no_context_takeover_name)
+
+ if param_value is not None:
+ raise ClientHandshakeError(
+ 'Bad %s: Has value %r' %
+ (client_no_context_takeover_name, param_value))
+ client_no_context_takeover = True
+
+ if client_no_context_takeover is None:
+ client_no_context_takeover = False
+
+ return _PerMessageDeflateFramer(client_max_window_bits,
+ client_no_context_takeover)
+
+
+class ClientHandshakeProcessor(ClientHandshakeBase):
+ """WebSocket opening handshake processor
+ """
+ def __init__(self, socket, options):
+ super(ClientHandshakeProcessor, self).__init__()
+
+ self._socket = socket
+ self._options = options
+
+ self._logger = util.get_class_logger(self)
+
+ def handshake(self):
+ """Performs opening handshake on the specified socket.
+
+ Raises:
+ ClientHandshakeError: handshake failed.
+ """
+
+ request_line = _build_method_line(self._options.resource)
+ self._logger.debug('Client\'s opening handshake Request-Line: %r',
+ request_line)
+ self._socket.sendall(request_line.encode('UTF-8'))
+
+ fields = []
+ fields.append(
+ _format_host_header(self._options.server_host,
+ self._options.server_port,
+ self._options.use_tls))
+ fields.append(_UPGRADE_HEADER)
+ fields.append(_CONNECTION_HEADER)
+ if self._options.origin is not None:
+ fields.append(
+ _origin_header(common.ORIGIN_HEADER, self._options.origin))
+
+ original_key = os.urandom(16)
+ self._key = base64.b64encode(original_key)
+ self._logger.debug('%s: %r (%s)', common.SEC_WEBSOCKET_KEY_HEADER,
+ self._key, util.hexify(original_key))
+ fields.append(
+ '%s: %s\r\n' %
+ (common.SEC_WEBSOCKET_KEY_HEADER, self._key.decode('UTF-8')))
+
+ fields.append(
+ '%s: %d\r\n' %
+ (common.SEC_WEBSOCKET_VERSION_HEADER, common.VERSION_HYBI_LATEST))
+
+ extensions_to_request = []
+
+ if self._options.use_permessage_deflate:
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ # Accept the client_max_window_bits extension parameter by default.
+ extension.add_parameter(
+ PerMessageDeflateExtensionProcessor.
+ _CLIENT_MAX_WINDOW_BITS_PARAM, None)
+ extensions_to_request.append(extension)
+
+ if len(extensions_to_request) != 0:
+ fields.append('%s: %s\r\n' %
+ (common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
+ common.format_extensions(extensions_to_request)))
+
+ for field in fields:
+ self._socket.sendall(field.encode('UTF-8'))
+
+ self._socket.sendall(b'\r\n')
+
+ self._logger.debug('Sent client\'s opening handshake headers: %r',
+ fields)
+ self._logger.debug('Start reading Status-Line')
+
+ status_line = b''
+ while True:
+ ch = _receive_bytes(self._socket, 1)
+ status_line += ch
+ if ch == b'\n':
+ break
+
+ m = re.match(b'HTTP/\\d+\.\\d+ (\\d\\d\\d) .*\r\n', status_line)
+ if m is None:
+ raise ClientHandshakeError('Wrong status line format: %r' %
+ status_line)
+ status_code = m.group(1)
+ if status_code != b'101':
+ self._logger.debug(
+ 'Unexpected status code %s with following headers: %r',
+ status_code, self._read_fields())
+ raise ClientHandshakeError(
+ 'Expected HTTP status code 101 but found %r' % status_code)
+
+ self._logger.debug('Received valid Status-Line')
+ self._logger.debug('Start reading headers until we see an empty line')
+
+ fields = self._read_fields()
+
+ ch = _receive_bytes(self._socket, 1)
+ if ch != b'\n': # 0x0A
+ raise ClientHandshakeError(
+ 'Expected LF but found %r while reading value %r for header '
+ 'name %r' % (ch, value, name))
+
+ self._logger.debug('Received an empty line')
+ self._logger.debug('Server\'s opening handshake headers: %r', fields)
+
+ _validate_mandatory_header(fields, common.UPGRADE_HEADER,
+ common.WEBSOCKET_UPGRADE_TYPE, False)
+
+ _validate_mandatory_header(fields, common.CONNECTION_HEADER,
+ common.UPGRADE_CONNECTION_TYPE, False)
+
+ accept = _get_mandatory_header(fields,
+ common.SEC_WEBSOCKET_ACCEPT_HEADER)
+
+ # Validate
+ try:
+ binary_accept = base64.b64decode(accept)
+ except TypeError:
+ raise HandshakeError('Illegal value for header %s: %r' %
+ (common.SEC_WEBSOCKET_ACCEPT_HEADER, accept))
+
+ if len(binary_accept) != 20:
+ raise ClientHandshakeError(
+ 'Decoded value of %s is not 20-byte long' %
+ common.SEC_WEBSOCKET_ACCEPT_HEADER)
+
+ self._logger.debug('Response for challenge : %r (%s)', accept,
+ util.hexify(binary_accept))
+
+ binary_expected_accept = sha1(self._key +
+ common.WEBSOCKET_ACCEPT_UUID).digest()
+ expected_accept = base64.b64encode(binary_expected_accept)
+
+ self._logger.debug('Expected response for challenge: %r (%s)',
+ expected_accept,
+ util.hexify(binary_expected_accept))
+
+ if accept != expected_accept.decode('UTF-8'):
+ raise ClientHandshakeError(
+ 'Invalid %s header: %r (expected: %s)' %
+ (common.SEC_WEBSOCKET_ACCEPT_HEADER, accept, expected_accept))
+
+ permessage_deflate_accepted = False
+
+ extensions_header = fields.get(
+ common.SEC_WEBSOCKET_EXTENSIONS_HEADER.lower())
+ accepted_extensions = []
+ if extensions_header is not None and len(extensions_header) != 0:
+ accepted_extensions = common.parse_extensions(extensions_header[0])
+
+ for extension in accepted_extensions:
+ extension_name = extension.name()
+ if (extension_name == common.PERMESSAGE_DEFLATE_EXTENSION
+ and self._options.use_permessage_deflate):
+ permessage_deflate_accepted = True
+
+ framer = _get_permessage_deflate_framer(extension)
+ framer.set_compress_outgoing_enabled(True)
+ self._options.use_permessage_deflate = framer
+ continue
+
+ raise ClientHandshakeError('Unexpected extension %r' %
+ extension_name)
+
+ if (self._options.use_permessage_deflate
+ and not permessage_deflate_accepted):
+ raise ClientHandshakeError(
+ 'Requested %s, but the server rejected it' %
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+
+ # TODO(tyoshino): Handle Sec-WebSocket-Protocol
+ # TODO(tyoshino): Handle Cookie, etc.
+
+
+class ClientConnection(object):
+ """A wrapper for socket object to provide the mp_conn interface.
+ """
+ def __init__(self, socket):
+ self._socket = socket
+
+ def write(self, data):
+ self._socket.sendall(data)
+
+ def read(self, n):
+ return self._socket.recv(n)
+
+ def get_remote_addr(self):
+ return self._socket.getpeername()
+
+ remote_addr = property(get_remote_addr)
+
+
+class ClientRequest(object):
+ """A wrapper class just to make it able to pass a socket object to
+ functions that expect a mp_request object.
+ """
+ def __init__(self, socket):
+ self._logger = util.get_class_logger(self)
+
+ self._socket = socket
+ self.connection = ClientConnection(socket)
+ self.ws_version = common.VERSION_HYBI_LATEST
+
+
+class EchoClient(object):
+ """WebSocket echo client."""
+ def __init__(self, options):
+ self._options = options
+ self._socket = None
+
+ self._logger = util.get_class_logger(self)
+
+ def run(self):
+ """Run the client.
+
+ Shake hands and then repeat sending message and receiving its echo.
+ """
+
+ self._socket = socket.socket()
+ self._socket.settimeout(self._options.socket_timeout)
+ try:
+ self._socket.connect(
+ (self._options.server_host, self._options.server_port))
+ if self._options.use_tls:
+ self._socket = _TLSSocket(self._socket)
+
+ self._handshake = ClientHandshakeProcessor(self._socket,
+ self._options)
+
+ self._handshake.handshake()
+
+ self._logger.info('Connection established')
+
+ request = ClientRequest(self._socket)
+
+ stream_option = StreamOptions()
+ stream_option.mask_send = True
+ stream_option.unmask_receive = False
+
+ if self._options.use_permessage_deflate is not False:
+ framer = self._options.use_permessage_deflate
+ framer.setup_stream_options(stream_option)
+
+ self._stream = Stream(request, stream_option)
+
+ for line in self._options.message.split(','):
+ self._stream.send_message(line)
+ if self._options.verbose:
+ print('Send: %s' % line)
+ try:
+ received = self._stream.receive_message()
+
+ if self._options.verbose:
+ print('Recv: %s' % received)
+ except Exception as e:
+ if self._options.verbose:
+ print('Error: %s' % e)
+ raise
+
+ self._do_closing_handshake()
+ finally:
+ self._socket.close()
+
+ def _do_closing_handshake(self):
+ """Perform closing handshake using the specified closing frame."""
+
+ if self._options.message.split(',')[-1] == _GOODBYE_MESSAGE:
+ # requested server initiated closing handshake, so
+ # expecting closing handshake message from server.
+ self._logger.info('Wait for server-initiated closing handshake')
+ message = self._stream.receive_message()
+ if message is None:
+ print('Recv close')
+ print('Send ack')
+ self._logger.info('Received closing handshake and sent ack')
+ return
+ print('Send close')
+ self._stream.close_connection()
+ self._logger.info('Sent closing handshake')
+ print('Recv ack')
+ self._logger.info('Received ack')
+
+
+def main():
+ # Force Python 2 to use the locale encoding, even when the output is not a
+ # tty. This makes the behaviour the same as Python 3. The encoding won't
+ # necessarily support all unicode characters. This problem is particularly
+ # prevalent on Windows.
+ if six.PY2:
+ import locale
+ encoding = locale.getpreferredencoding()
+ sys.stdout = codecs.getwriter(encoding)(sys.stdout)
+
+ parser = argparse.ArgumentParser()
+ # We accept --command_line_flag style flags which is the same as Google
+ # gflags in addition to common --command-line-flag style flags.
+ parser.add_argument('-s',
+ '--server-host',
+ '--server_host',
+ dest='server_host',
+ type=six.text_type,
+ default='localhost',
+ help='server host')
+ parser.add_argument('-p',
+ '--server-port',
+ '--server_port',
+ dest='server_port',
+ type=int,
+ default=_UNDEFINED_PORT,
+ help='server port')
+ parser.add_argument('-o',
+ '--origin',
+ dest='origin',
+ type=six.text_type,
+ default=None,
+ help='origin')
+ parser.add_argument('-r',
+ '--resource',
+ dest='resource',
+ type=six.text_type,
+ default='/echo',
+ help='resource path')
+ parser.add_argument(
+ '-m',
+ '--message',
+ dest='message',
+ type=six.text_type,
+ default=u'Hello,<>',
+ help=('comma-separated messages to send. '
+ '%s will force close the connection from server.' %
+ _GOODBYE_MESSAGE))
+ parser.add_argument('-q',
+ '--quiet',
+ dest='verbose',
+ action='store_false',
+ default=True,
+ help='suppress messages')
+ parser.add_argument('-t',
+ '--tls',
+ dest='use_tls',
+ action='store_true',
+ default=False,
+ help='use TLS (wss://).')
+ parser.add_argument('-k',
+ '--socket-timeout',
+ '--socket_timeout',
+ dest='socket_timeout',
+ type=int,
+ default=_TIMEOUT_SEC,
+ help='Timeout(sec) for sockets')
+ parser.add_argument('--use-permessage-deflate',
+ '--use_permessage_deflate',
+ dest='use_permessage_deflate',
+ action='store_true',
+ default=False,
+ help='Use the permessage-deflate extension.')
+ parser.add_argument('--log-level',
+ '--log_level',
+ type=six.text_type,
+ dest='log_level',
+ default='warn',
+ choices=['debug', 'info', 'warn', 'error', 'critical'],
+ help='Log level.')
+
+ options = parser.parse_args()
+
+ logging.basicConfig(level=logging.getLevelName(options.log_level.upper()))
+
+ # Default port number depends on whether TLS is used.
+ if options.server_port == _UNDEFINED_PORT:
+ if options.use_tls:
+ options.server_port = common.DEFAULT_WEB_SOCKET_SECURE_PORT
+ else:
+ options.server_port = common.DEFAULT_WEB_SOCKET_PORT
+
+ EchoClient(options).run()
+
+
+if __name__ == '__main__':
+ main()
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_noext_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_noext_wsh.py
new file mode 100644
index 0000000000..eba5032218
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_noext_wsh.py
@@ -0,0 +1,62 @@
+# Copyright 2013, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import six
+
+_GOODBYE_MESSAGE = u'Goodbye'
+
+
+def web_socket_do_extra_handshake(request):
+ """Received Sec-WebSocket-Extensions header value is parsed into
+ request.ws_requested_extensions. pywebsocket creates extension
+ processors using it before do_extra_handshake call and never looks at it
+ after the call.
+
+ To reject requested extensions, clear the processor list.
+ """
+
+ request.ws_extension_processors = []
+
+
+def web_socket_transfer_data(request):
+ """Echo. Same as echo_wsh.py."""
+
+ while True:
+ line = request.ws_stream.receive_message()
+ if line is None:
+ return
+ if isinstance(line, six.text_type):
+ request.ws_stream.send_message(line, binary=False)
+ if line == _GOODBYE_MESSAGE:
+ return
+ else:
+ request.ws_stream.send_message(line, binary=True)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_wsh.py
new file mode 100644
index 0000000000..f7b3c6c531
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_wsh.py
@@ -0,0 +1,55 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import six
+
+_GOODBYE_MESSAGE = u'Goodbye'
+
+
+def web_socket_do_extra_handshake(request):
+ # This example handler accepts any request. See origin_check_wsh.py for how
+ # to reject access from untrusted scripts based on origin value.
+
+ pass # Always accept.
+
+
+def web_socket_transfer_data(request):
+ while True:
+ line = request.ws_stream.receive_message()
+ if line is None:
+ return
+ if isinstance(line, six.text_type):
+ request.ws_stream.send_message(line, binary=False)
+ if line == _GOODBYE_MESSAGE:
+ return
+ else:
+ request.ws_stream.send_message(line, binary=True)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/handler_map.txt b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/handler_map.txt
new file mode 100644
index 0000000000..21c4c09aa0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/handler_map.txt
@@ -0,0 +1,11 @@
+# websocket handler map file, used by standalone.py -m option.
+# A line starting with '#' is a comment line.
+# Each line consists of 'alias_resource_path' and 'existing_resource_path'
+# separated by spaces.
+# Aliasing is processed from the top to the bottom of the line, and
+# 'existing_resource_path' must exist before it is aliased.
+# For example,
+# / /echo
+# means that a request to '/' will be handled by handlers for '/echo'.
+/ /echo
+
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/hsts_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/hsts_wsh.py
new file mode 100644
index 0000000000..e861946921
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/hsts_wsh.py
@@ -0,0 +1,40 @@
+# Copyright 2013, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+def web_socket_do_extra_handshake(request):
+ request.extra_headers.append(
+ ('Strict-Transport-Security', 'max-age=86400'))
+
+
+def web_socket_transfer_data(request):
+ request.ws_stream.send_message('Hello', binary=False)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py
new file mode 100644
index 0000000000..04aa684283
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py
@@ -0,0 +1,42 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ raise msgutil.BadOperationException('Intentional')
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/origin_check_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/origin_check_wsh.py
new file mode 100644
index 0000000000..e05767ab93
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/origin_check_wsh.py
@@ -0,0 +1,44 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# This example is derived from test/testdata/handlers/origin_check_wsh.py.
+
+
+def web_socket_do_extra_handshake(request):
+ if request.ws_origin == 'http://example.com':
+ return
+ raise ValueError('Unacceptable origin: %r' % request.ws_origin)
+
+
+def web_socket_transfer_data(request):
+ request.connection.write('origin_check_wsh.py is called for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.html
new file mode 100644
index 0000000000..c18b2c08f6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.html
@@ -0,0 +1,37 @@
+<!--
+Copyright 2020, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+
+<!DOCTYPE html>
+<head>
+<script src="util.js"></script>
+<script src="performance_test_iframe.js"></script>
+<script src="benchmark.js"></script>
+</head>
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js
new file mode 100644
index 0000000000..270409aa6e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js
@@ -0,0 +1,86 @@
+// Copyright 2020, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+function perfTestAddToLog(text) {
+ parent.postMessage({'command': 'log', 'value': text}, '*');
+}
+
+function perfTestAddToSummary(text) {
+}
+
+function perfTestMeasureValue(value) {
+ parent.postMessage({'command': 'measureValue', 'value': value}, '*');
+}
+
+function perfTestNotifyAbort() {
+ parent.postMessage({'command': 'notifyAbort'}, '*');
+}
+
+function getConfigForPerformanceTest(dataType, async,
+ verifyData, numIterations,
+ numWarmUpIterations) {
+
+ return {
+ prefixUrl: 'ws://' + location.host + '/benchmark_helper',
+ printSize: true,
+ numSockets: 1,
+ // + 1 is for a warmup iteration by the Telemetry framework.
+ numIterations: numIterations + numWarmUpIterations + 1,
+ numWarmUpIterations: numWarmUpIterations,
+ minTotal: 10240000,
+ startSize: 10240000,
+ stopThreshold: 10240000,
+ multipliers: [2],
+ verifyData: verifyData,
+ dataType: dataType,
+ async: async,
+ addToLog: perfTestAddToLog,
+ addToSummary: perfTestAddToSummary,
+ measureValue: perfTestMeasureValue,
+ notifyAbort: perfTestNotifyAbort
+ };
+}
+
+var data;
+onmessage = function(message) {
+ var action;
+ if (message.data.command === 'start') {
+ data = message.data;
+ initWorker('http://' + location.host);
+ action = data.benchmarkName;
+ } else {
+ action = 'stop';
+ }
+
+ var config = getConfigForPerformanceTest(data.dataType, data.async,
+ data.verifyData,
+ data.numIterations,
+ data.numWarmUpIterations);
+ doAction(config, data.isWorker, action);
+};
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi
new file mode 100755
index 0000000000..703cb7401b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi
@@ -0,0 +1,26 @@
+#!/usr/bin/python
+
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the COPYING file or at
+# https://developers.google.com/open-source/licenses/bsd
+"""CGI script sample for testing effect of HTTP headers on the origin page.
+
+Note that CGI scripts don't work on the standalone pywebsocket running in TLS
+mode.
+"""
+
+print """Content-type: text/html
+Content-Security-Policy: connect-src self
+
+<html>
+<head>
+<title></title>
+</head>
+<body>
+<script>
+var socket = new WebSocket("ws://example.com");
+</script>
+</body>
+</html>"""
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util.js
new file mode 100644
index 0000000000..990160cb40
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util.js
@@ -0,0 +1,323 @@
+// Copyright 2013, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+// Utilities for example applications (for both main and worker thread).
+
+var results = {};
+
+function getTimeStamp() {
+ return Date.now();
+}
+
+function formatResultInKiB(size, timePerMessageInMs, stddevTimePerMessageInMs,
+ speed, printSize) {
+ if (printSize) {
+ return (size / 1024) +
+ '\t' + timePerMessageInMs.toFixed(3) +
+ (stddevTimePerMessageInMs == -1 ?
+ '' :
+ '\t' + stddevTimePerMessageInMs.toFixed(3)) +
+ '\t' + speed.toFixed(3);
+ } else {
+ return speed.toString();
+ }
+}
+
+function clearAverageData() {
+ results = {};
+}
+
+function reportAverageData(config) {
+ config.addToSummary(
+ 'Size[KiB]\tAverage time[ms]\tStddev time[ms]\tSpeed[KB/s]');
+ for (var size in results) {
+ var averageTimePerMessageInMs = results[size].sum_t / results[size].n;
+ var speed = calculateSpeedInKB(size, averageTimePerMessageInMs);
+ // Calculate sample standard deviation
+ var stddevTimePerMessageInMs = Math.sqrt(
+ (results[size].sum_t2 / results[size].n -
+ averageTimePerMessageInMs * averageTimePerMessageInMs) *
+ results[size].n /
+ (results[size].n - 1));
+ config.addToSummary(formatResultInKiB(
+ size, averageTimePerMessageInMs, stddevTimePerMessageInMs, speed,
+ true));
+ }
+}
+
+function calculateSpeedInKB(size, timeSpentInMs) {
+ return Math.round(size / timeSpentInMs * 1000) / 1000;
+}
+
+function calculateAndLogResult(config, size, startTimeInMs, totalSize,
+ isWarmUp) {
+ var timeSpentInMs = getTimeStamp() - startTimeInMs;
+ var speed = calculateSpeedInKB(totalSize, timeSpentInMs);
+ var timePerMessageInMs = timeSpentInMs / (totalSize / size);
+ if (!isWarmUp) {
+ config.measureValue(timePerMessageInMs);
+ if (!results[size]) {
+ results[size] = {n: 0, sum_t: 0, sum_t2: 0};
+ }
+ results[size].n ++;
+ results[size].sum_t += timePerMessageInMs;
+ results[size].sum_t2 += timePerMessageInMs * timePerMessageInMs;
+ }
+ config.addToLog(formatResultInKiB(size, timePerMessageInMs, -1, speed,
+ config.printSize));
+}
+
+function repeatString(str, count) {
+ var data = '';
+ var expChunk = str;
+ var remain = count;
+ while (true) {
+ if (remain % 2) {
+ data += expChunk;
+ remain = (remain - 1) / 2;
+ } else {
+ remain /= 2;
+ }
+
+ if (remain == 0)
+ break;
+
+ expChunk = expChunk + expChunk;
+ }
+ return data;
+}
+
+function fillArrayBuffer(buffer, c) {
+ var i;
+
+ var u32Content = c * 0x01010101;
+
+ var u32Blocks = Math.floor(buffer.byteLength / 4);
+ var u32View = new Uint32Array(buffer, 0, u32Blocks);
+ // length attribute is slow on Chrome. Don't use it for loop condition.
+ for (i = 0; i < u32Blocks; ++i) {
+ u32View[i] = u32Content;
+ }
+
+ // Fraction
+ var u8Blocks = buffer.byteLength - u32Blocks * 4;
+ var u8View = new Uint8Array(buffer, u32Blocks * 4, u8Blocks);
+ for (i = 0; i < u8Blocks; ++i) {
+ u8View[i] = c;
+ }
+}
+
+function verifyArrayBuffer(buffer, expectedChar) {
+ var i;
+
+ var expectedU32Value = expectedChar * 0x01010101;
+
+ var u32Blocks = Math.floor(buffer.byteLength / 4);
+ var u32View = new Uint32Array(buffer, 0, u32Blocks);
+ for (i = 0; i < u32Blocks; ++i) {
+ if (u32View[i] != expectedU32Value) {
+ return false;
+ }
+ }
+
+ var u8Blocks = buffer.byteLength - u32Blocks * 4;
+ var u8View = new Uint8Array(buffer, u32Blocks * 4, u8Blocks);
+ for (i = 0; i < u8Blocks; ++i) {
+ if (u8View[i] != expectedChar) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function verifyBlob(config, blob, expectedChar, doneCallback) {
+ var reader = new FileReader(blob);
+ reader.onerror = function() {
+ config.addToLog('FileReader Error: ' + reader.error.message);
+ doneCallback(blob.size, false);
+ }
+ reader.onloadend = function() {
+ var result = verifyArrayBuffer(reader.result, expectedChar);
+ doneCallback(blob.size, result);
+ }
+ reader.readAsArrayBuffer(blob);
+}
+
+function verifyAcknowledgement(config, message, size) {
+ if (typeof message != 'string') {
+ config.addToLog('Invalid ack type: ' + typeof message);
+ return false;
+ }
+ var parsedAck = parseInt(message);
+ if (isNaN(parsedAck)) {
+ config.addToLog('Invalid ack value: ' + message);
+ return false;
+ }
+ if (parsedAck != size) {
+ config.addToLog(
+ 'Expected ack for ' + size + 'B but received one for ' + parsedAck +
+ 'B');
+ return false;
+ }
+
+ return true;
+}
+
+function cloneConfig(obj) {
+ var newObj = {};
+ for (key in obj) {
+ newObj[key] = obj[key];
+ }
+ return newObj;
+}
+
+var tasks = [];
+
+function runNextTask(config) {
+ var task = tasks.shift();
+ if (task == undefined) {
+ config.addToLog('Finished');
+ cleanup();
+ return;
+ }
+ timerID = setTimeout(task, 0);
+}
+
+function buildLegendString(config) {
+ var legend = ''
+ if (config.printSize)
+ legend = 'Message size in KiB, Time/message in ms, ';
+ legend += 'Speed in kB/s';
+ return legend;
+}
+
+function addTasks(config, stepFunc) {
+ for (var i = 0;
+ i < config.numWarmUpIterations + config.numIterations; ++i) {
+ var multiplierIndex = 0;
+ for (var size = config.startSize;
+ size <= config.stopThreshold;
+ ++multiplierIndex) {
+ var task = stepFunc.bind(
+ null,
+ size,
+ config,
+ i < config.numWarmUpIterations);
+ tasks.push(task);
+ var multiplier = config.multipliers[
+ multiplierIndex % config.multipliers.length];
+ if (multiplier <= 1) {
+ config.addToLog('Invalid multiplier ' + multiplier);
+ config.notifyAbort();
+ throw new Error('Invalid multipler');
+ }
+ size = Math.ceil(size * multiplier);
+ }
+ }
+}
+
+function addResultReportingTask(config, title) {
+ tasks.push(function(){
+ timerID = null;
+ config.addToSummary(title);
+ reportAverageData(config);
+ clearAverageData();
+ runNextTask(config);
+ });
+}
+
+function sendBenchmark(config) {
+ config.addToLog('Send benchmark');
+ config.addToLog(buildLegendString(config));
+
+ tasks = [];
+ clearAverageData();
+ addTasks(config, sendBenchmarkStep);
+ addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config));
+ startBenchmark(config);
+}
+
+function receiveBenchmark(config) {
+ config.addToLog('Receive benchmark');
+ config.addToLog(buildLegendString(config));
+
+ tasks = [];
+ clearAverageData();
+ addTasks(config, receiveBenchmarkStep);
+ addResultReportingTask(config,
+ 'Receive Benchmark ' + getConfigString(config));
+ startBenchmark(config);
+}
+
+function stop(config) {
+ clearTimeout(timerID);
+ timerID = null;
+ tasks = [];
+ config.addToLog('Stopped');
+ cleanup();
+}
+
+var worker;
+
+function initWorker(origin) {
+ worker = new Worker(origin + '/benchmark.js');
+}
+
+function doAction(config, isWindowToWorker, action) {
+ if (isWindowToWorker) {
+ worker.onmessage = function(addToLog, addToSummary,
+ measureValue, notifyAbort, message) {
+ if (message.data.type === 'addToLog')
+ addToLog(message.data.data);
+ else if (message.data.type === 'addToSummary')
+ addToSummary(message.data.data);
+ else if (message.data.type === 'measureValue')
+ measureValue(message.data.data);
+ else if (message.data.type === 'notifyAbort')
+ notifyAbort();
+ }.bind(undefined, config.addToLog, config.addToSummary,
+ config.measureValue, config.notifyAbort);
+ config.addToLog = undefined;
+ config.addToSummary = undefined;
+ config.measureValue = undefined;
+ config.notifyAbort = undefined;
+ worker.postMessage({type: action, config: config});
+ } else {
+ if (action === 'sendBenchmark')
+ sendBenchmark(config);
+ else if (action === 'receiveBenchmark')
+ receiveBenchmark(config);
+ else if (action === 'batchBenchmark')
+ batchBenchmark(config);
+ else if (action === 'stop')
+ stop(config);
+ }
+}
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js
new file mode 100644
index 0000000000..78add48731
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js
@@ -0,0 +1,89 @@
+// Copyright 2014, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+// Utilities for example applications (for the main thread only).
+
+var logBox = null;
+var queuedLog = '';
+
+var summaryBox = null;
+
+function queueLog(log) {
+ queuedLog += log + '\n';
+}
+
+function addToLog(log) {
+ logBox.value += queuedLog;
+ queuedLog = '';
+ logBox.value += log + '\n';
+ logBox.scrollTop = 1000000;
+}
+
+function addToSummary(log) {
+ summaryBox.value += log + '\n';
+ summaryBox.scrollTop = 1000000;
+}
+
+// value: execution time in milliseconds.
+// config.measureValue is intended to be used in Performance Tests.
+// Do nothing here in non-PerformanceTest.
+function measureValue(value) {
+}
+
+// config.notifyAbort is called when the benchmark failed and aborted, and
+// intended to be used in Performance Tests.
+// Do nothing here in non-PerformanceTest.
+function notifyAbort() {
+}
+
+function getIntFromInput(id) {
+ return parseInt(document.getElementById(id).value);
+}
+
+function getStringFromRadioBox(name) {
+ var list = document.getElementById('benchmark_form')[name];
+ for (var i = 0; i < list.length; ++i)
+ if (list.item(i).checked)
+ return list.item(i).value;
+ return undefined;
+}
+function getBoolFromCheckBox(id) {
+ return document.getElementById(id).checked;
+}
+
+function getIntArrayFromInput(id) {
+ var strArray = document.getElementById(id).value.split(',');
+ return strArray.map(function(str) { return parseInt(str, 10); });
+}
+
+function getFloatArrayFromInput(id) {
+ var strArray = document.getElementById(id).value.split(',');
+ return strArray.map(parseFloat);
+}
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js
new file mode 100644
index 0000000000..dd90449a90
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js
@@ -0,0 +1,44 @@
+// Copyright 2014, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+// Utilities for example applications (for the worker threads only).
+
+onmessage = function (message) {
+ var config = message.data.config;
+ config.addToLog = function(text) {
+ postMessage({type: 'addToLog', data: text}); };
+ config.addToSummary = function(text) {
+ postMessage({type: 'addToSummary', data: text}); };
+ config.measureValue = function(value) {
+ postMessage({type: 'measureValue', data: value}); };
+ config.notifyAbort = function() { postMessage({type: 'notifyAbort'}); };
+
+ doAction(config, false, message.data.type);
+};
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/__init__.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/__init__.py
new file mode 100644
index 0000000000..28d5f5950f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/__init__.py
@@ -0,0 +1,172 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+""" A Standalone WebSocket Server for testing purposes
+
+mod_pywebsocket is an API that provides WebSocket functionalities with
+a standalone WebSocket server. It is intended for testing or
+experimental purposes.
+
+Installation
+============
+1. Follow standalone server documentation to start running the
+standalone server. It can be read by running the following command:
+
+ $ pydoc mod_pywebsocket.standalone
+
+2. Once the standalone server is launched verify it by accessing
+http://localhost[:port]/console.html. Include the port number when
+specified on launch. If everything is working correctly, you
+will see a simple echo console.
+
+
+Writing WebSocket handlers
+==========================
+
+When a WebSocket request comes in, the resource name
+specified in the handshake is considered as if it is a file path under
+<websock_handlers> and the handler defined in
+<websock_handlers>/<resource_name>_wsh.py is invoked.
+
+For example, if the resource name is /example/chat, the handler defined in
+<websock_handlers>/example/chat_wsh.py is invoked.
+
+A WebSocket handler is composed of the following three functions:
+
+ web_socket_do_extra_handshake(request)
+ web_socket_transfer_data(request)
+ web_socket_passive_closing_handshake(request)
+
+where:
+ request: mod_python request.
+
+web_socket_do_extra_handshake is called during the handshake after the
+headers are successfully parsed and WebSocket properties (ws_origin,
+and ws_resource) are added to request. A handler
+can reject the request by raising an exception.
+
+A request object has the following properties that you can use during the
+extra handshake (web_socket_do_extra_handshake):
+- ws_resource
+- ws_origin
+- ws_version
+- ws_extensions
+- ws_deflate
+- ws_protocol
+- ws_requested_protocols
+
+The last two are a bit tricky. See the next subsection.
+
+
+Subprotocol Negotiation
+-----------------------
+
+ws_protocol is always set to None when
+web_socket_do_extra_handshake is called. If ws_requested_protocols is not
+None, you must choose one subprotocol from this list and set it to
+ws_protocol.
+
+Data Transfer
+-------------
+
+web_socket_transfer_data is called after the handshake completed
+successfully. A handler can receive/send messages from/to the client
+using request. mod_pywebsocket.msgutil module provides utilities
+for data transfer.
+
+You can receive a message by the following statement.
+
+ message = request.ws_stream.receive_message()
+
+This call blocks until any complete text frame arrives, and the payload data
+of the incoming frame will be stored into message. When you're using IETF
+HyBi 00 or later protocol, receive_message() will return None on receiving
+client-initiated closing handshake. When any error occurs, receive_message()
+will raise some exception.
+
+You can send a message by the following statement.
+
+ request.ws_stream.send_message(message)
+
+
+Closing Connection
+------------------
+
+Executing the following statement or just return-ing from
+web_socket_transfer_data cause connection close.
+
+ request.ws_stream.close_connection()
+
+close_connection will wait
+for closing handshake acknowledgement coming from the client. When it
+couldn't receive a valid acknowledgement, raises an exception.
+
+web_socket_passive_closing_handshake is called after the server receives
+incoming closing frame from the client peer immediately. You can specify
+code and reason by return values. They are sent as a outgoing closing frame
+from the server. A request object has the following properties that you can
+use in web_socket_passive_closing_handshake.
+- ws_close_code
+- ws_close_reason
+
+
+Threading
+---------
+
+A WebSocket handler must be thread-safe. The standalone
+server uses threads by default.
+
+
+Configuring WebSocket Extension Processors
+------------------------------------------
+
+See extensions.py for supported WebSocket extensions. Note that they are
+unstable and their APIs are subject to change substantially.
+
+A request object has these extension processing related attributes.
+
+- ws_requested_extensions:
+
+ A list of common.ExtensionParameter instances representing extension
+ parameters received from the client in the client's opening handshake.
+ You shouldn't modify it manually.
+
+- ws_extensions:
+
+ A list of common.ExtensionParameter instances representing extension
+ parameters to send back to the client in the server's opening handshake.
+ You shouldn't touch it directly. Instead, call methods on extension
+ processors.
+
+- ws_extension_processors:
+
+ A list of loaded extension processors. Find the processor for the
+ extension you want to configure from it, and call its methods.
+"""
+
+# vi:sts=4 sw=4 et tw=72
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/_stream_exceptions.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/_stream_exceptions.py
new file mode 100644
index 0000000000..b47878bc4a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/_stream_exceptions.py
@@ -0,0 +1,82 @@
+# Copyright 2020, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Stream Exceptions.
+"""
+
+# Note: request.connection.write/read are used in this module, even though
+# mod_python document says that they should be used only in connection
+# handlers. Unfortunately, we have no other options. For example,
+# request.write/read are not suitable because they don't allow direct raw bytes
+# writing/reading.
+
+
+# Exceptions
+class ConnectionTerminatedException(Exception):
+ """This exception will be raised when a connection is terminated
+ unexpectedly.
+ """
+
+ pass
+
+
+class InvalidFrameException(ConnectionTerminatedException):
+ """This exception will be raised when we received an invalid frame we
+ cannot parse.
+ """
+
+ pass
+
+
+class BadOperationException(Exception):
+ """This exception will be raised when send_message() is called on
+ server-terminated connection or receive_message() is called on
+ client-terminated connection.
+ """
+
+ pass
+
+
+class UnsupportedFrameException(Exception):
+ """This exception will be raised when we receive a frame with flag, opcode
+ we cannot handle. Handlers can just catch and ignore this exception and
+ call receive_message() again to continue processing the next frame.
+ """
+
+ pass
+
+
+class InvalidUTF8Exception(Exception):
+ """This exception will be raised when we receive a text frame which
+ contains invalid UTF-8 strings.
+ """
+
+ pass
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/common.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/common.py
new file mode 100644
index 0000000000..9cb11f15cb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/common.py
@@ -0,0 +1,273 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""This file must not depend on any module specific to the WebSocket protocol.
+"""
+
+from __future__ import absolute_import
+from mod_pywebsocket import http_header_util
+
+# Additional log level definitions.
+LOGLEVEL_FINE = 9
+
+# Constants indicating WebSocket protocol version.
+VERSION_HYBI13 = 13
+VERSION_HYBI14 = 13
+VERSION_HYBI15 = 13
+VERSION_HYBI16 = 13
+VERSION_HYBI17 = 13
+
+# Constants indicating WebSocket protocol latest version.
+VERSION_HYBI_LATEST = VERSION_HYBI13
+
+# Port numbers
+DEFAULT_WEB_SOCKET_PORT = 80
+DEFAULT_WEB_SOCKET_SECURE_PORT = 443
+
+# Schemes
+WEB_SOCKET_SCHEME = 'ws'
+WEB_SOCKET_SECURE_SCHEME = 'wss'
+
+# Frame opcodes defined in the spec.
+OPCODE_CONTINUATION = 0x0
+OPCODE_TEXT = 0x1
+OPCODE_BINARY = 0x2
+OPCODE_CLOSE = 0x8
+OPCODE_PING = 0x9
+OPCODE_PONG = 0xa
+
+# UUID for the opening handshake and frame masking.
+WEBSOCKET_ACCEPT_UUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
+
+# Opening handshake header names and expected values.
+UPGRADE_HEADER = 'Upgrade'
+WEBSOCKET_UPGRADE_TYPE = 'websocket'
+CONNECTION_HEADER = 'Connection'
+UPGRADE_CONNECTION_TYPE = 'Upgrade'
+HOST_HEADER = 'Host'
+ORIGIN_HEADER = 'Origin'
+SEC_WEBSOCKET_KEY_HEADER = 'Sec-WebSocket-Key'
+SEC_WEBSOCKET_ACCEPT_HEADER = 'Sec-WebSocket-Accept'
+SEC_WEBSOCKET_VERSION_HEADER = 'Sec-WebSocket-Version'
+SEC_WEBSOCKET_PROTOCOL_HEADER = 'Sec-WebSocket-Protocol'
+SEC_WEBSOCKET_EXTENSIONS_HEADER = 'Sec-WebSocket-Extensions'
+
+# Extensions
+PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
+
+# Status codes
+# Code STATUS_NO_STATUS_RECEIVED, STATUS_ABNORMAL_CLOSURE, and
+# STATUS_TLS_HANDSHAKE are pseudo codes to indicate specific error cases.
+# Could not be used for codes in actual closing frames.
+# Application level errors must use codes in the range
+# STATUS_USER_REGISTERED_BASE to STATUS_USER_PRIVATE_MAX. The codes in the
+# range STATUS_USER_REGISTERED_BASE to STATUS_USER_REGISTERED_MAX are managed
+# by IANA. Usually application must define user protocol level errors in the
+# range STATUS_USER_PRIVATE_BASE to STATUS_USER_PRIVATE_MAX.
+STATUS_NORMAL_CLOSURE = 1000
+STATUS_GOING_AWAY = 1001
+STATUS_PROTOCOL_ERROR = 1002
+STATUS_UNSUPPORTED_DATA = 1003
+STATUS_NO_STATUS_RECEIVED = 1005
+STATUS_ABNORMAL_CLOSURE = 1006
+STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007
+STATUS_POLICY_VIOLATION = 1008
+STATUS_MESSAGE_TOO_BIG = 1009
+STATUS_MANDATORY_EXTENSION = 1010
+STATUS_INTERNAL_ENDPOINT_ERROR = 1011
+STATUS_TLS_HANDSHAKE = 1015
+STATUS_USER_REGISTERED_BASE = 3000
+STATUS_USER_REGISTERED_MAX = 3999
+STATUS_USER_PRIVATE_BASE = 4000
+STATUS_USER_PRIVATE_MAX = 4999
+# Following definitions are aliases to keep compatibility. Applications must
+# not use these obsoleted definitions anymore.
+STATUS_NORMAL = STATUS_NORMAL_CLOSURE
+STATUS_UNSUPPORTED = STATUS_UNSUPPORTED_DATA
+STATUS_CODE_NOT_AVAILABLE = STATUS_NO_STATUS_RECEIVED
+STATUS_ABNORMAL_CLOSE = STATUS_ABNORMAL_CLOSURE
+STATUS_INVALID_FRAME_PAYLOAD = STATUS_INVALID_FRAME_PAYLOAD_DATA
+STATUS_MANDATORY_EXT = STATUS_MANDATORY_EXTENSION
+
+# HTTP status codes
+HTTP_STATUS_BAD_REQUEST = 400
+HTTP_STATUS_FORBIDDEN = 403
+HTTP_STATUS_NOT_FOUND = 404
+
+
+def is_control_opcode(opcode):
+ return (opcode >> 3) == 1
+
+
+class ExtensionParameter(object):
+ """This is exchanged on extension negotiation in opening handshake."""
+ def __init__(self, name):
+ self._name = name
+ # TODO(tyoshino): Change the data structure to more efficient one such
+ # as dict when the spec changes to say like
+ # - Parameter names must be unique
+ # - The order of parameters is not significant
+ self._parameters = []
+
+ def name(self):
+ """Return the extension name."""
+ return self._name
+
+ def add_parameter(self, name, value):
+ """Add a parameter."""
+ self._parameters.append((name, value))
+
+ def get_parameters(self):
+ """Return the parameters."""
+ return self._parameters
+
+ def get_parameter_names(self):
+ """Return the names of the parameters."""
+ return [name for name, unused_value in self._parameters]
+
+ def has_parameter(self, name):
+ """Test if a parameter exists."""
+ for param_name, param_value in self._parameters:
+ if param_name == name:
+ return True
+ return False
+
+ def get_parameter_value(self, name):
+ """Get the value of a specific parameter."""
+ for param_name, param_value in self._parameters:
+ if param_name == name:
+ return param_value
+
+
+class ExtensionParsingException(Exception):
+ """Exception to handle errors in extension parsing."""
+ def __init__(self, name):
+ super(ExtensionParsingException, self).__init__(name)
+
+
+def _parse_extension_param(state, definition):
+ param_name = http_header_util.consume_token(state)
+
+ if param_name is None:
+ raise ExtensionParsingException('No valid parameter name found')
+
+ http_header_util.consume_lwses(state)
+
+ if not http_header_util.consume_string(state, '='):
+ definition.add_parameter(param_name, None)
+ return
+
+ http_header_util.consume_lwses(state)
+
+ # TODO(tyoshino): Add code to validate that parsed param_value is token
+ param_value = http_header_util.consume_token_or_quoted_string(state)
+ if param_value is None:
+ raise ExtensionParsingException(
+ 'No valid parameter value found on the right-hand side of '
+ 'parameter %r' % param_name)
+
+ definition.add_parameter(param_name, param_value)
+
+
+def _parse_extension(state):
+ extension_token = http_header_util.consume_token(state)
+ if extension_token is None:
+ return None
+
+ extension = ExtensionParameter(extension_token)
+
+ while True:
+ http_header_util.consume_lwses(state)
+
+ if not http_header_util.consume_string(state, ';'):
+ break
+
+ http_header_util.consume_lwses(state)
+
+ try:
+ _parse_extension_param(state, extension)
+ except ExtensionParsingException as e:
+ raise ExtensionParsingException(
+ 'Failed to parse parameter for %r (%r)' % (extension_token, e))
+
+ return extension
+
+
+def parse_extensions(data):
+ """Parse Sec-WebSocket-Extensions header value.
+
+ Returns a list of ExtensionParameter objects.
+ Leading LWSes must be trimmed.
+ """
+ state = http_header_util.ParsingState(data)
+
+ extension_list = []
+ while True:
+ extension = _parse_extension(state)
+ if extension is not None:
+ extension_list.append(extension)
+
+ http_header_util.consume_lwses(state)
+
+ if http_header_util.peek(state) is None:
+ break
+
+ if not http_header_util.consume_string(state, ','):
+ raise ExtensionParsingException(
+ 'Failed to parse Sec-WebSocket-Extensions header: '
+ 'Expected a comma but found %r' % http_header_util.peek(state))
+
+ http_header_util.consume_lwses(state)
+
+ if len(extension_list) == 0:
+ raise ExtensionParsingException('No valid extension entry found')
+
+ return extension_list
+
+
+def format_extension(extension):
+ """Format an ExtensionParameter object."""
+ formatted_params = [extension.name()]
+ for param_name, param_value in extension.get_parameters():
+ if param_value is None:
+ formatted_params.append(param_name)
+ else:
+ quoted_value = http_header_util.quote_if_necessary(param_value)
+ formatted_params.append('%s=%s' % (param_name, quoted_value))
+ return '; '.join(formatted_params)
+
+
+def format_extensions(extension_list):
+ """Format a list of ExtensionParameter objects."""
+ formatted_extension_list = []
+ for extension in extension_list:
+ formatted_extension_list.append(format_extension(extension))
+ return ', '.join(formatted_extension_list)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/dispatch.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/dispatch.py
new file mode 100644
index 0000000000..4ee943a5b8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/dispatch.py
@@ -0,0 +1,385 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Dispatch WebSocket request.
+"""
+
+from __future__ import absolute_import
+import logging
+import os
+import re
+import traceback
+
+from mod_pywebsocket import common
+from mod_pywebsocket import handshake
+from mod_pywebsocket import msgutil
+from mod_pywebsocket import stream
+from mod_pywebsocket import util
+
+_SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$')
+_SOURCE_SUFFIX = '_wsh.py'
+_DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake'
+_TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data'
+_PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = (
+ 'web_socket_passive_closing_handshake')
+
+
+class DispatchException(Exception):
+ """Exception in dispatching WebSocket request."""
+ def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND):
+ super(DispatchException, self).__init__(name)
+ self.status = status
+
+
+def _default_passive_closing_handshake_handler(request):
+ """Default web_socket_passive_closing_handshake handler."""
+
+ return common.STATUS_NORMAL_CLOSURE, ''
+
+
+def _normalize_path(path):
+ """Normalize path.
+
+ Args:
+ path: the path to normalize.
+
+ Path is converted to the absolute path.
+ The input path can use either '\\' or '/' as the separator.
+ The normalized path always uses '/' regardless of the platform.
+ """
+
+ path = path.replace('\\', os.path.sep)
+ path = os.path.realpath(path)
+ path = path.replace('\\', '/')
+ return path
+
+
+def _create_path_to_resource_converter(base_dir):
+ """Returns a function that converts the path of a WebSocket handler source
+ file to a resource string by removing the path to the base directory from
+ its head, removing _SOURCE_SUFFIX from its tail, and replacing path
+ separators in it with '/'.
+
+ Args:
+ base_dir: the path to the base directory.
+ """
+
+ base_dir = _normalize_path(base_dir)
+
+ base_len = len(base_dir)
+ suffix_len = len(_SOURCE_SUFFIX)
+
+ def converter(path):
+ if not path.endswith(_SOURCE_SUFFIX):
+ return None
+ # _normalize_path must not be used because resolving symlink breaks
+ # following path check.
+ path = path.replace('\\', '/')
+ if not path.startswith(base_dir):
+ return None
+ return path[base_len:-suffix_len]
+
+ return converter
+
+
+def _enumerate_handler_file_paths(directory):
+ """Returns a generator that enumerates WebSocket Handler source file names
+ in the given directory.
+ """
+
+ for root, unused_dirs, files in os.walk(directory):
+ for base in files:
+ path = os.path.join(root, base)
+ if _SOURCE_PATH_PATTERN.search(path):
+ yield path
+
+
+class _HandlerSuite(object):
+ """A handler suite holder class."""
+ def __init__(self, do_extra_handshake, transfer_data,
+ passive_closing_handshake):
+ self.do_extra_handshake = do_extra_handshake
+ self.transfer_data = transfer_data
+ self.passive_closing_handshake = passive_closing_handshake
+
+
+def _source_handler_file(handler_definition):
+ """Source a handler definition string.
+
+ Args:
+ handler_definition: a string containing Python statements that define
+ handler functions.
+ """
+
+ global_dic = {}
+ try:
+ # This statement is gramatically different in python 2 and 3.
+ # Hence, yapf will complain about this. To overcome this, we disable
+ # yapf for this line.
+ exec(handler_definition, global_dic) # yapf: disable
+ except Exception:
+ raise DispatchException('Error in sourcing handler:' +
+ traceback.format_exc())
+ passive_closing_handshake_handler = None
+ try:
+ passive_closing_handshake_handler = _extract_handler(
+ global_dic, _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME)
+ except Exception:
+ passive_closing_handshake_handler = (
+ _default_passive_closing_handshake_handler)
+ return _HandlerSuite(
+ _extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME),
+ _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME),
+ passive_closing_handshake_handler)
+
+
+def _extract_handler(dic, name):
+ """Extracts a callable with the specified name from the given dictionary
+ dic.
+ """
+
+ if name not in dic:
+ raise DispatchException('%s is not defined.' % name)
+ handler = dic[name]
+ if not callable(handler):
+ raise DispatchException('%s is not callable.' % name)
+ return handler
+
+
+class Dispatcher(object):
+ """Dispatches WebSocket requests.
+
+ This class maintains a map from resource name to handlers.
+ """
+ def __init__(self,
+ root_dir,
+ scan_dir=None,
+ allow_handlers_outside_root_dir=True):
+ """Construct an instance.
+
+ Args:
+ root_dir: The directory where handler definition files are
+ placed.
+ scan_dir: The directory where handler definition files are
+ searched. scan_dir must be a directory under root_dir,
+ including root_dir itself. If scan_dir is None,
+ root_dir is used as scan_dir. scan_dir can be useful
+ in saving scan time when root_dir contains many
+ subdirectories.
+ allow_handlers_outside_root_dir: Scans handler files even if their
+ canonical path is not under root_dir.
+ """
+
+ self._logger = util.get_class_logger(self)
+
+ self._handler_suite_map = {}
+ self._source_warnings = []
+ if scan_dir is None:
+ scan_dir = root_dir
+ if not os.path.realpath(scan_dir).startswith(
+ os.path.realpath(root_dir)):
+ raise DispatchException('scan_dir:%s must be a directory under '
+ 'root_dir:%s.' % (scan_dir, root_dir))
+ self._source_handler_files_in_dir(root_dir, scan_dir,
+ allow_handlers_outside_root_dir)
+
+ def add_resource_path_alias(self, alias_resource_path,
+ existing_resource_path):
+ """Add resource path alias.
+
+ Once added, request to alias_resource_path would be handled by
+ handler registered for existing_resource_path.
+
+ Args:
+ alias_resource_path: alias resource path
+ existing_resource_path: existing resource path
+ """
+ try:
+ handler_suite = self._handler_suite_map[existing_resource_path]
+ self._handler_suite_map[alias_resource_path] = handler_suite
+ except KeyError:
+ raise DispatchException('No handler for: %r' %
+ existing_resource_path)
+
+ def source_warnings(self):
+ """Return warnings in sourcing handlers."""
+
+ return self._source_warnings
+
+ def do_extra_handshake(self, request):
+ """Do extra checking in WebSocket handshake.
+
+ Select a handler based on request.uri and call its
+ web_socket_do_extra_handshake function.
+
+ Args:
+ request: mod_python request.
+
+ Raises:
+ DispatchException: when handler was not found
+ AbortedByUserException: when user handler abort connection
+ HandshakeException: when opening handshake failed
+ """
+
+ handler_suite = self.get_handler_suite(request.ws_resource)
+ if handler_suite is None:
+ raise DispatchException('No handler for: %r' % request.ws_resource)
+ do_extra_handshake_ = handler_suite.do_extra_handshake
+ try:
+ do_extra_handshake_(request)
+ except handshake.AbortedByUserException as e:
+ # Re-raise to tell the caller of this function to finish this
+ # connection without sending any error.
+ self._logger.debug('%s', traceback.format_exc())
+ raise
+ except Exception as e:
+ util.prepend_message_to_exception(
+ '%s raised exception for %s: ' %
+ (_DO_EXTRA_HANDSHAKE_HANDLER_NAME, request.ws_resource), e)
+ raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN)
+
+ def transfer_data(self, request):
+ """Let a handler transfer_data with a WebSocket client.
+
+ Select a handler based on request.ws_resource and call its
+ web_socket_transfer_data function.
+
+ Args:
+ request: mod_python request.
+
+ Raises:
+ DispatchException: when handler was not found
+ AbortedByUserException: when user handler abort connection
+ """
+
+ # TODO(tyoshino): Terminate underlying TCP connection if possible.
+ try:
+ handler_suite = self.get_handler_suite(request.ws_resource)
+ if handler_suite is None:
+ raise DispatchException('No handler for: %r' %
+ request.ws_resource)
+ transfer_data_ = handler_suite.transfer_data
+ transfer_data_(request)
+
+ if not request.server_terminated:
+ request.ws_stream.close_connection()
+ # Catch non-critical exceptions the handler didn't handle.
+ except handshake.AbortedByUserException as e:
+ self._logger.debug('%s', traceback.format_exc())
+ raise
+ except msgutil.BadOperationException as e:
+ self._logger.debug('%s', e)
+ request.ws_stream.close_connection(
+ common.STATUS_INTERNAL_ENDPOINT_ERROR)
+ except msgutil.InvalidFrameException as e:
+ # InvalidFrameException must be caught before
+ # ConnectionTerminatedException that catches InvalidFrameException.
+ self._logger.debug('%s', e)
+ request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR)
+ except msgutil.UnsupportedFrameException as e:
+ self._logger.debug('%s', e)
+ request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA)
+ except stream.InvalidUTF8Exception as e:
+ self._logger.debug('%s', e)
+ request.ws_stream.close_connection(
+ common.STATUS_INVALID_FRAME_PAYLOAD_DATA)
+ except msgutil.ConnectionTerminatedException as e:
+ self._logger.debug('%s', e)
+ except Exception as e:
+ # Any other exceptions are forwarded to the caller of this
+ # function.
+ util.prepend_message_to_exception(
+ '%s raised exception for %s: ' %
+ (_TRANSFER_DATA_HANDLER_NAME, request.ws_resource), e)
+ raise
+
+ def passive_closing_handshake(self, request):
+ """Prepare code and reason for responding client initiated closing
+ handshake.
+ """
+
+ handler_suite = self.get_handler_suite(request.ws_resource)
+ if handler_suite is None:
+ return _default_passive_closing_handshake_handler(request)
+ return handler_suite.passive_closing_handshake(request)
+
+ def get_handler_suite(self, resource):
+ """Retrieves two handlers (one for extra handshake processing, and one
+ for data transfer) for the given request as a HandlerSuite object.
+ """
+
+ fragment = None
+ if '#' in resource:
+ resource, fragment = resource.split('#', 1)
+ if '?' in resource:
+ resource = resource.split('?', 1)[0]
+ handler_suite = self._handler_suite_map.get(resource)
+ if handler_suite and fragment:
+ raise DispatchException(
+ 'Fragment identifiers MUST NOT be used on WebSocket URIs',
+ common.HTTP_STATUS_BAD_REQUEST)
+ return handler_suite
+
+ def _source_handler_files_in_dir(self, root_dir, scan_dir,
+ allow_handlers_outside_root_dir):
+ """Source all the handler source files in the scan_dir directory.
+
+ The resource path is determined relative to root_dir.
+ """
+
+ # We build a map from resource to handler code assuming that there's
+ # only one path from root_dir to scan_dir and it can be obtained by
+ # comparing realpath of them.
+
+ # Here we cannot use abspath. See
+ # https://bugs.webkit.org/show_bug.cgi?id=31603
+
+ convert = _create_path_to_resource_converter(root_dir)
+ scan_realpath = os.path.realpath(scan_dir)
+ root_realpath = os.path.realpath(root_dir)
+ for path in _enumerate_handler_file_paths(scan_realpath):
+ if (not allow_handlers_outside_root_dir and
+ (not os.path.realpath(path).startswith(root_realpath))):
+ self._logger.debug(
+ 'Canonical path of %s is not under root directory' % path)
+ continue
+ try:
+ with open(path) as handler_file:
+ handler_suite = _source_handler_file(handler_file.read())
+ except DispatchException as e:
+ self._source_warnings.append('%s: %s' % (path, e))
+ continue
+ resource = convert(path)
+ if resource is None:
+ self._logger.debug('Path to resource conversion on %s failed' %
+ path)
+ else:
+ self._handler_suite_map[convert(path)] = handler_suite
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/extensions.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/extensions.py
new file mode 100644
index 0000000000..314a949d45
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/extensions.py
@@ -0,0 +1,474 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from mod_pywebsocket import common
+from mod_pywebsocket import util
+from mod_pywebsocket.http_header_util import quote_if_necessary
+
+# The list of available server side extension processor classes.
+_available_processors = {}
+
+
+class ExtensionProcessorInterface(object):
+ def __init__(self, request):
+ self._logger = util.get_class_logger(self)
+
+ self._request = request
+ self._active = True
+
+ def request(self):
+ return self._request
+
+ def name(self):
+ return None
+
+ def check_consistency_with_other_processors(self, processors):
+ pass
+
+ def set_active(self, active):
+ self._active = active
+
+ def is_active(self):
+ return self._active
+
+ def _get_extension_response_internal(self):
+ return None
+
+ def get_extension_response(self):
+ if not self._active:
+ self._logger.debug('Extension %s is deactivated', self.name())
+ return None
+
+ response = self._get_extension_response_internal()
+ if response is None:
+ self._active = False
+ return response
+
+ def _setup_stream_options_internal(self, stream_options):
+ pass
+
+ def setup_stream_options(self, stream_options):
+ if self._active:
+ self._setup_stream_options_internal(stream_options)
+
+
+def _log_outgoing_compression_ratio(logger, original_bytes, filtered_bytes,
+ average_ratio):
+ # Print inf when ratio is not available.
+ ratio = float('inf')
+ if original_bytes != 0:
+ ratio = float(filtered_bytes) / original_bytes
+
+ logger.debug('Outgoing compression ratio: %f (average: %f)' %
+ (ratio, average_ratio))
+
+
+def _log_incoming_compression_ratio(logger, received_bytes, filtered_bytes,
+ average_ratio):
+ # Print inf when ratio is not available.
+ ratio = float('inf')
+ if filtered_bytes != 0:
+ ratio = float(received_bytes) / filtered_bytes
+
+ logger.debug('Incoming compression ratio: %f (average: %f)' %
+ (ratio, average_ratio))
+
+
+def _parse_window_bits(bits):
+ """Return parsed integer value iff the given string conforms to the
+ grammar of the window bits extension parameters.
+ """
+
+ if bits is None:
+ raise ValueError('Value is required')
+
+ # For non integer values such as "10.0", ValueError will be raised.
+ int_bits = int(bits)
+
+ # First condition is to drop leading zero case e.g. "08".
+ if bits != str(int_bits) or int_bits < 8 or int_bits > 15:
+ raise ValueError('Invalid value: %r' % bits)
+
+ return int_bits
+
+
+class _AverageRatioCalculator(object):
+ """Stores total bytes of original and result data, and calculates average
+ result / original ratio.
+ """
+ def __init__(self):
+ self._total_original_bytes = 0
+ self._total_result_bytes = 0
+
+ def add_original_bytes(self, value):
+ self._total_original_bytes += value
+
+ def add_result_bytes(self, value):
+ self._total_result_bytes += value
+
+ def get_average_ratio(self):
+ if self._total_original_bytes != 0:
+ return (float(self._total_result_bytes) /
+ self._total_original_bytes)
+ else:
+ return float('inf')
+
+
+class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
+ """permessage-deflate extension processor.
+
+ Specification:
+ http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-08
+ """
+
+ _SERVER_MAX_WINDOW_BITS_PARAM = 'server_max_window_bits'
+ _SERVER_NO_CONTEXT_TAKEOVER_PARAM = 'server_no_context_takeover'
+ _CLIENT_MAX_WINDOW_BITS_PARAM = 'client_max_window_bits'
+ _CLIENT_NO_CONTEXT_TAKEOVER_PARAM = 'client_no_context_takeover'
+
+ def __init__(self, request):
+ """Construct PerMessageDeflateExtensionProcessor."""
+
+ ExtensionProcessorInterface.__init__(self, request)
+ self._logger = util.get_class_logger(self)
+
+ self._preferred_client_max_window_bits = None
+ self._client_no_context_takeover = False
+
+ def name(self):
+ # This method returns "deflate" (not "permessage-deflate") for
+ # compatibility.
+ return 'deflate'
+
+ def _get_extension_response_internal(self):
+ for name in self._request.get_parameter_names():
+ if name not in [
+ self._SERVER_MAX_WINDOW_BITS_PARAM,
+ self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
+ self._CLIENT_MAX_WINDOW_BITS_PARAM
+ ]:
+ self._logger.debug('Unknown parameter: %r', name)
+ return None
+
+ server_max_window_bits = None
+ if self._request.has_parameter(self._SERVER_MAX_WINDOW_BITS_PARAM):
+ server_max_window_bits = self._request.get_parameter_value(
+ self._SERVER_MAX_WINDOW_BITS_PARAM)
+ try:
+ server_max_window_bits = _parse_window_bits(
+ server_max_window_bits)
+ except ValueError as e:
+ self._logger.debug('Bad %s parameter: %r',
+ self._SERVER_MAX_WINDOW_BITS_PARAM, e)
+ return None
+
+ server_no_context_takeover = self._request.has_parameter(
+ self._SERVER_NO_CONTEXT_TAKEOVER_PARAM)
+ if (server_no_context_takeover and self._request.get_parameter_value(
+ self._SERVER_NO_CONTEXT_TAKEOVER_PARAM) is not None):
+ self._logger.debug('%s parameter must not have a value: %r',
+ self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
+ server_no_context_takeover)
+ return None
+
+ # client_max_window_bits from a client indicates whether the client can
+ # accept client_max_window_bits from a server or not.
+ client_client_max_window_bits = self._request.has_parameter(
+ self._CLIENT_MAX_WINDOW_BITS_PARAM)
+ if (client_client_max_window_bits
+ and self._request.get_parameter_value(
+ self._CLIENT_MAX_WINDOW_BITS_PARAM) is not None):
+ self._logger.debug(
+ '%s parameter must not have a value in a '
+ 'client\'s opening handshake: %r',
+ self._CLIENT_MAX_WINDOW_BITS_PARAM,
+ client_client_max_window_bits)
+ return None
+
+ self._rfc1979_deflater = util._RFC1979Deflater(
+ server_max_window_bits, server_no_context_takeover)
+
+ # Note that we prepare for incoming messages compressed with window
+ # bits upto 15 regardless of the client_max_window_bits value to be
+ # sent to the client.
+ self._rfc1979_inflater = util._RFC1979Inflater()
+
+ self._framer = _PerMessageDeflateFramer(server_max_window_bits,
+ server_no_context_takeover)
+ self._framer.set_bfinal(False)
+ self._framer.set_compress_outgoing_enabled(True)
+
+ response = common.ExtensionParameter(self._request.name())
+
+ if server_max_window_bits is not None:
+ response.add_parameter(self._SERVER_MAX_WINDOW_BITS_PARAM,
+ str(server_max_window_bits))
+
+ if server_no_context_takeover:
+ response.add_parameter(self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
+ None)
+
+ if self._preferred_client_max_window_bits is not None:
+ if not client_client_max_window_bits:
+ self._logger.debug(
+ 'Processor is configured to use %s but '
+ 'the client cannot accept it',
+ self._CLIENT_MAX_WINDOW_BITS_PARAM)
+ return None
+ response.add_parameter(self._CLIENT_MAX_WINDOW_BITS_PARAM,
+ str(self._preferred_client_max_window_bits))
+
+ if self._client_no_context_takeover:
+ response.add_parameter(self._CLIENT_NO_CONTEXT_TAKEOVER_PARAM,
+ None)
+
+ self._logger.debug('Enable %s extension ('
+ 'request: server_max_window_bits=%s; '
+ 'server_no_context_takeover=%r, '
+ 'response: client_max_window_bits=%s; '
+ 'client_no_context_takeover=%r)' %
+ (self._request.name(), server_max_window_bits,
+ server_no_context_takeover,
+ self._preferred_client_max_window_bits,
+ self._client_no_context_takeover))
+
+ return response
+
+ def _setup_stream_options_internal(self, stream_options):
+ self._framer.setup_stream_options(stream_options)
+
+ def set_client_max_window_bits(self, value):
+ """If this option is specified, this class adds the
+ client_max_window_bits extension parameter to the handshake response,
+ but doesn't reduce the LZ77 sliding window size of its inflater.
+ I.e., you can use this for testing client implementation but cannot
+ reduce memory usage of this class.
+
+ If this method has been called with True and an offer without the
+ client_max_window_bits extension parameter is received,
+
+ - (When processing the permessage-deflate extension) this processor
+ declines the request.
+ - (When processing the permessage-compress extension) this processor
+ accepts the request.
+ """
+
+ self._preferred_client_max_window_bits = value
+
+ def set_client_no_context_takeover(self, value):
+ """If this option is specified, this class adds the
+ client_no_context_takeover extension parameter to the handshake
+ response, but doesn't reset inflater for each message. I.e., you can
+ use this for testing client implementation but cannot reduce memory
+ usage of this class.
+ """
+
+ self._client_no_context_takeover = value
+
+ def set_bfinal(self, value):
+ self._framer.set_bfinal(value)
+
+ def enable_outgoing_compression(self):
+ self._framer.set_compress_outgoing_enabled(True)
+
+ def disable_outgoing_compression(self):
+ self._framer.set_compress_outgoing_enabled(False)
+
+
+class _PerMessageDeflateFramer(object):
+ """A framer for extensions with per-message DEFLATE feature."""
+ def __init__(self, deflate_max_window_bits, deflate_no_context_takeover):
+ self._logger = util.get_class_logger(self)
+
+ self._rfc1979_deflater = util._RFC1979Deflater(
+ deflate_max_window_bits, deflate_no_context_takeover)
+
+ self._rfc1979_inflater = util._RFC1979Inflater()
+
+ self._bfinal = False
+
+ self._compress_outgoing_enabled = False
+
+ # True if a message is fragmented and compression is ongoing.
+ self._compress_ongoing = False
+
+ # Calculates
+ # (Total outgoing bytes supplied to this filter) /
+ # (Total bytes sent to the network after applying this filter)
+ self._outgoing_average_ratio_calculator = _AverageRatioCalculator()
+
+ # Calculates
+ # (Total bytes received from the network) /
+ # (Total incoming bytes obtained after applying this filter)
+ self._incoming_average_ratio_calculator = _AverageRatioCalculator()
+
+ def set_bfinal(self, value):
+ self._bfinal = value
+
+ def set_compress_outgoing_enabled(self, value):
+ self._compress_outgoing_enabled = value
+
+ def _process_incoming_message(self, message, decompress):
+ if not decompress:
+ return message
+
+ received_payload_size = len(message)
+ self._incoming_average_ratio_calculator.add_result_bytes(
+ received_payload_size)
+
+ message = self._rfc1979_inflater.filter(message)
+
+ filtered_payload_size = len(message)
+ self._incoming_average_ratio_calculator.add_original_bytes(
+ filtered_payload_size)
+
+ _log_incoming_compression_ratio(
+ self._logger, received_payload_size, filtered_payload_size,
+ self._incoming_average_ratio_calculator.get_average_ratio())
+
+ return message
+
+ def _process_outgoing_message(self, message, end, binary):
+ if not binary:
+ message = message.encode('utf-8')
+
+ if not self._compress_outgoing_enabled:
+ return message
+
+ original_payload_size = len(message)
+ self._outgoing_average_ratio_calculator.add_original_bytes(
+ original_payload_size)
+
+ message = self._rfc1979_deflater.filter(message,
+ end=end,
+ bfinal=self._bfinal)
+
+ filtered_payload_size = len(message)
+ self._outgoing_average_ratio_calculator.add_result_bytes(
+ filtered_payload_size)
+
+ _log_outgoing_compression_ratio(
+ self._logger, original_payload_size, filtered_payload_size,
+ self._outgoing_average_ratio_calculator.get_average_ratio())
+
+ if not self._compress_ongoing:
+ self._outgoing_frame_filter.set_compression_bit()
+ self._compress_ongoing = not end
+ return message
+
+ def _process_incoming_frame(self, frame):
+ if frame.rsv1 == 1 and not common.is_control_opcode(frame.opcode):
+ self._incoming_message_filter.decompress_next_message()
+ frame.rsv1 = 0
+
+ def _process_outgoing_frame(self, frame, compression_bit):
+ if (not compression_bit or common.is_control_opcode(frame.opcode)):
+ return
+
+ frame.rsv1 = 1
+
+ def setup_stream_options(self, stream_options):
+ """Creates filters and sets them to the StreamOptions."""
+ class _OutgoingMessageFilter(object):
+ def __init__(self, parent):
+ self._parent = parent
+
+ def filter(self, message, end=True, binary=False):
+ return self._parent._process_outgoing_message(
+ message, end, binary)
+
+ class _IncomingMessageFilter(object):
+ def __init__(self, parent):
+ self._parent = parent
+ self._decompress_next_message = False
+
+ def decompress_next_message(self):
+ self._decompress_next_message = True
+
+ def filter(self, message):
+ message = self._parent._process_incoming_message(
+ message, self._decompress_next_message)
+ self._decompress_next_message = False
+ return message
+
+ self._outgoing_message_filter = _OutgoingMessageFilter(self)
+ self._incoming_message_filter = _IncomingMessageFilter(self)
+ stream_options.outgoing_message_filters.append(
+ self._outgoing_message_filter)
+ stream_options.incoming_message_filters.append(
+ self._incoming_message_filter)
+
+ class _OutgoingFrameFilter(object):
+ def __init__(self, parent):
+ self._parent = parent
+ self._set_compression_bit = False
+
+ def set_compression_bit(self):
+ self._set_compression_bit = True
+
+ def filter(self, frame):
+ self._parent._process_outgoing_frame(frame,
+ self._set_compression_bit)
+ self._set_compression_bit = False
+
+ class _IncomingFrameFilter(object):
+ def __init__(self, parent):
+ self._parent = parent
+
+ def filter(self, frame):
+ self._parent._process_incoming_frame(frame)
+
+ self._outgoing_frame_filter = _OutgoingFrameFilter(self)
+ self._incoming_frame_filter = _IncomingFrameFilter(self)
+ stream_options.outgoing_frame_filters.append(
+ self._outgoing_frame_filter)
+ stream_options.incoming_frame_filters.append(
+ self._incoming_frame_filter)
+
+ stream_options.encode_text_message_to_utf8 = False
+
+
+_available_processors[common.PERMESSAGE_DEFLATE_EXTENSION] = (
+ PerMessageDeflateExtensionProcessor)
+
+
+def get_extension_processor(extension_request):
+ """Given an ExtensionParameter representing an extension offer received
+ from a client, configures and returns an instance of the corresponding
+ extension processor class.
+ """
+
+ processor_class = _available_processors.get(extension_request.name())
+ if processor_class is None:
+ return None
+ return processor_class(extension_request)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/fast_masking.i b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/fast_masking.i
new file mode 100644
index 0000000000..ddaad27f53
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/fast_masking.i
@@ -0,0 +1,98 @@
+// Copyright 2013, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+%module fast_masking
+
+%include "cstring.i"
+
+%{
+#include <cstring>
+
+#ifdef __SSE2__
+#include <emmintrin.h>
+#endif
+%}
+
+%apply (char *STRING, int LENGTH) {
+ (const char* payload, int payload_length),
+ (const char* masking_key, int masking_key_length) };
+%cstring_output_allocate_size(
+ char** result, int* result_length, delete [] *$1);
+
+%inline %{
+
+void mask(
+ const char* payload, int payload_length,
+ const char* masking_key, int masking_key_length,
+ int masking_key_index,
+ char** result, int* result_length) {
+ *result = new char[payload_length];
+ *result_length = payload_length;
+ memcpy(*result, payload, payload_length);
+
+ char* cursor = *result;
+ char* cursor_end = *result + *result_length;
+
+#ifdef __SSE2__
+ while ((cursor < cursor_end) &&
+ (reinterpret_cast<size_t>(cursor) & 0xf)) {
+ *cursor ^= masking_key[masking_key_index];
+ ++cursor;
+ masking_key_index = (masking_key_index + 1) % masking_key_length;
+ }
+ if (cursor == cursor_end) {
+ return;
+ }
+
+ const int kBlockSize = 16;
+ __m128i masking_key_block;
+ for (int i = 0; i < kBlockSize; ++i) {
+ *(reinterpret_cast<char*>(&masking_key_block) + i) =
+ masking_key[masking_key_index];
+ masking_key_index = (masking_key_index + 1) % masking_key_length;
+ }
+
+ while (cursor + kBlockSize <= cursor_end) {
+ __m128i payload_block =
+ _mm_load_si128(reinterpret_cast<__m128i*>(cursor));
+ _mm_stream_si128(reinterpret_cast<__m128i*>(cursor),
+ _mm_xor_si128(payload_block, masking_key_block));
+ cursor += kBlockSize;
+ }
+#endif
+
+ while (cursor < cursor_end) {
+ *cursor ^= masking_key[masking_key_index];
+ ++cursor;
+ masking_key_index = (masking_key_index + 1) % masking_key_length;
+ }
+}
+
+%}
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/__init__.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/__init__.py
new file mode 100644
index 0000000000..4bc1c67c57
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/__init__.py
@@ -0,0 +1,101 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""WebSocket opening handshake processor. This class try to apply available
+opening handshake processors for each protocol version until a connection is
+successfully established.
+"""
+
+from __future__ import absolute_import
+import logging
+
+from mod_pywebsocket import common
+from mod_pywebsocket.handshake import hybi
+# Export AbortedByUserException, HandshakeException, and VersionException
+# symbol from this module.
+from mod_pywebsocket.handshake.base import AbortedByUserException
+from mod_pywebsocket.handshake.base import HandshakeException
+from mod_pywebsocket.handshake.base import VersionException
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def do_handshake(request, dispatcher):
+ """Performs WebSocket handshake.
+
+ Args:
+ request: mod_python request.
+ dispatcher: Dispatcher (dispatch.Dispatcher).
+
+ Handshaker will add attributes such as ws_resource in performing
+ handshake.
+ """
+
+ _LOGGER.debug('Client\'s opening handshake resource: %r', request.uri)
+ # To print mimetools.Message as escaped one-line string, we converts
+ # headers_in to dict object. Without conversion, if we use %r, it just
+ # prints the type and address, and if we use %s, it prints the original
+ # header string as multiple lines.
+ #
+ # Both mimetools.Message and MpTable_Type of mod_python can be
+ # converted to dict.
+ #
+ # mimetools.Message.__str__ returns the original header string.
+ # dict(mimetools.Message object) returns the map from header names to
+ # header values. While MpTable_Type doesn't have such __str__ but just
+ # __repr__ which formats itself as well as dictionary object.
+ _LOGGER.debug('Client\'s opening handshake headers: %r',
+ dict(request.headers_in))
+
+ handshakers = []
+ handshakers.append(('RFC 6455', hybi.Handshaker(request, dispatcher)))
+
+ for name, handshaker in handshakers:
+ _LOGGER.debug('Trying protocol version %s', name)
+ try:
+ handshaker.do_handshake()
+ _LOGGER.info('Established (%s protocol)', name)
+ return
+ except HandshakeException as e:
+ _LOGGER.debug(
+ 'Failed to complete opening handshake as %s protocol: %r',
+ name, e)
+ if e.status:
+ raise e
+ except AbortedByUserException as e:
+ raise
+ except VersionException as e:
+ raise
+
+ # TODO(toyoshim): Add a test to cover the case all handshakers fail.
+ raise HandshakeException(
+ 'Failed to complete opening handshake for all available protocols',
+ status=common.HTTP_STATUS_BAD_REQUEST)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/base.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/base.py
new file mode 100644
index 0000000000..ffad0614d6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/base.py
@@ -0,0 +1,396 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Common functions and exceptions used by WebSocket opening handshake
+processors.
+"""
+
+from __future__ import absolute_import
+
+from mod_pywebsocket import common
+from mod_pywebsocket import http_header_util
+from mod_pywebsocket.extensions import get_extension_processor
+from mod_pywebsocket.stream import StreamOptions
+from mod_pywebsocket.stream import Stream
+from mod_pywebsocket import util
+
+from six.moves import map
+from six.moves import range
+
+# Defining aliases for values used frequently.
+_VERSION_LATEST = common.VERSION_HYBI_LATEST
+_VERSION_LATEST_STRING = str(_VERSION_LATEST)
+_SUPPORTED_VERSIONS = [
+ _VERSION_LATEST,
+]
+
+
+class AbortedByUserException(Exception):
+ """Exception for aborting a connection intentionally.
+
+ If this exception is raised in do_extra_handshake handler, the connection
+ will be abandoned. No other WebSocket or HTTP(S) handler will be invoked.
+
+ If this exception is raised in transfer_data_handler, the connection will
+ be closed without closing handshake. No other WebSocket or HTTP(S) handler
+ will be invoked.
+ """
+
+ pass
+
+
+class HandshakeException(Exception):
+ """This exception will be raised when an error occurred while processing
+ WebSocket initial handshake.
+ """
+ def __init__(self, name, status=None):
+ super(HandshakeException, self).__init__(name)
+ self.status = status
+
+
+class VersionException(Exception):
+ """This exception will be raised when a version of client request does not
+ match with version the server supports.
+ """
+ def __init__(self, name, supported_versions=''):
+ """Construct an instance.
+
+ Args:
+ supported_version: a str object to show supported hybi versions.
+ (e.g. '13')
+ """
+ super(VersionException, self).__init__(name)
+ self.supported_versions = supported_versions
+
+
+def get_default_port(is_secure):
+ if is_secure:
+ return common.DEFAULT_WEB_SOCKET_SECURE_PORT
+ else:
+ return common.DEFAULT_WEB_SOCKET_PORT
+
+
+def validate_subprotocol(subprotocol):
+ """Validate a value in the Sec-WebSocket-Protocol field.
+
+ See the Section 4.1., 4.2.2., and 4.3. of RFC 6455.
+ """
+
+ if not subprotocol:
+ raise HandshakeException('Invalid subprotocol name: empty')
+
+ # Parameter should be encoded HTTP token.
+ state = http_header_util.ParsingState(subprotocol)
+ token = http_header_util.consume_token(state)
+ rest = http_header_util.peek(state)
+ # If |rest| is not None, |subprotocol| is not one token or invalid. If
+ # |rest| is None, |token| must not be None because |subprotocol| is
+ # concatenation of |token| and |rest| and is not None.
+ if rest is not None:
+ raise HandshakeException('Invalid non-token string in subprotocol '
+ 'name: %r' % rest)
+
+
+def parse_host_header(request):
+ fields = request.headers_in[common.HOST_HEADER].split(':', 1)
+ if len(fields) == 1:
+ return fields[0], get_default_port(request.is_https())
+ try:
+ return fields[0], int(fields[1])
+ except ValueError as e:
+ raise HandshakeException('Invalid port number format: %r' % e)
+
+
+def get_mandatory_header(request, key):
+ value = request.headers_in.get(key)
+ if value is None:
+ raise HandshakeException('Header %s is not defined' % key)
+ return value
+
+
+def validate_mandatory_header(request, key, expected_value, fail_status=None):
+ value = get_mandatory_header(request, key)
+
+ if value.lower() != expected_value.lower():
+ raise HandshakeException(
+ 'Expected %r for header %s but found %r (case-insensitive)' %
+ (expected_value, key, value),
+ status=fail_status)
+
+
+def parse_token_list(data):
+ """Parses a header value which follows 1#token and returns parsed elements
+ as a list of strings.
+
+ Leading LWSes must be trimmed.
+ """
+
+ state = http_header_util.ParsingState(data)
+
+ token_list = []
+
+ while True:
+ token = http_header_util.consume_token(state)
+ if token is not None:
+ token_list.append(token)
+
+ http_header_util.consume_lwses(state)
+
+ if http_header_util.peek(state) is None:
+ break
+
+ if not http_header_util.consume_string(state, ','):
+ raise HandshakeException('Expected a comma but found %r' %
+ http_header_util.peek(state))
+
+ http_header_util.consume_lwses(state)
+
+ if len(token_list) == 0:
+ raise HandshakeException('No valid token found')
+
+ return token_list
+
+
+class HandshakerBase(object):
+ def __init__(self, request, dispatcher):
+ self._logger = util.get_class_logger(self)
+ self._request = request
+ self._dispatcher = dispatcher
+
+ """ subclasses must implement the five following methods """
+
+ def _protocol_rfc(self):
+ """ Return the name of the RFC that the handshake class is implementing.
+ """
+
+ raise AssertionError("subclasses should implement this method")
+
+ def _transform_header(self, header):
+ """
+ :param header: header name
+
+ transform the header name if needed. For example, HTTP/2 subclass will
+ return the name of the header in lower case.
+ """
+
+ raise AssertionError("subclasses should implement this method")
+
+ def _validate_request(self):
+ """ validate that all the mandatory fields are set """
+
+ raise AssertionError("subclasses should implement this method")
+
+ def _set_accept(self):
+ """ Computes accept value based on Sec-WebSocket-Accept if needed. """
+
+ raise AssertionError("subclasses should implement this method")
+
+ def _send_handshake(self):
+ """ Prepare and send the response after it has been parsed and processed.
+ """
+
+ raise AssertionError("subclasses should implement this method")
+
+ def do_handshake(self):
+ self._request.ws_close_code = None
+ self._request.ws_close_reason = None
+
+ # Parsing.
+ self._validate_request()
+ self._request.ws_resource = self._request.uri
+ self._request.ws_version = self._check_version()
+
+ try:
+ self._get_origin()
+ self._set_protocol()
+ self._parse_extensions()
+
+ self._set_accept()
+
+ self._logger.debug('Protocol version is ' + self._protocol_rfc())
+
+ # Setup extension processors.
+ self._request.ws_extension_processors = self._get_extension_processors_requested(
+ )
+
+ # List of extra headers. The extra handshake handler may add header
+ # data as name/value pairs to this list and pywebsocket appends
+ # them to the WebSocket handshake.
+ self._request.extra_headers = []
+
+ # Extra handshake handler may modify/remove processors.
+ self._dispatcher.do_extra_handshake(self._request)
+
+ stream_options = StreamOptions()
+ self._process_extensions(stream_options)
+
+ self._request.ws_stream = Stream(self._request, stream_options)
+
+ if self._request.ws_requested_protocols is not None:
+ if self._request.ws_protocol is None:
+ raise HandshakeException(
+ 'do_extra_handshake must choose one subprotocol from '
+ 'ws_requested_protocols and set it to ws_protocol')
+ validate_subprotocol(self._request.ws_protocol)
+
+ self._logger.debug('Subprotocol accepted: %r',
+ self._request.ws_protocol)
+ else:
+ if self._request.ws_protocol is not None:
+ raise HandshakeException(
+ 'ws_protocol must be None when the client didn\'t '
+ 'request any subprotocol')
+
+ self._send_handshake()
+ except HandshakeException as e:
+ if not e.status:
+ # Fallback to 400 bad request by default.
+ e.status = common.HTTP_STATUS_BAD_REQUEST
+ raise e
+
+ def _check_version(self):
+ sec_websocket_version_header = self._transform_header(
+ common.SEC_WEBSOCKET_VERSION_HEADER)
+ version = get_mandatory_header(self._request,
+ sec_websocket_version_header)
+ if version == _VERSION_LATEST_STRING:
+ return _VERSION_LATEST
+
+ if version.find(',') >= 0:
+ raise HandshakeException(
+ 'Multiple versions (%r) are not allowed for header %s' %
+ (version, sec_websocket_version_header),
+ status=common.HTTP_STATUS_BAD_REQUEST)
+ raise VersionException('Unsupported version %r for header %s' %
+ (version, sec_websocket_version_header),
+ supported_versions=', '.join(
+ map(str, _SUPPORTED_VERSIONS)))
+
+ def _get_origin(self):
+ origin_header = self._transform_header(common.ORIGIN_HEADER)
+ origin = self._request.headers_in.get(origin_header)
+ if origin is None:
+ self._logger.debug('Client request does not have origin header')
+ self._request.ws_origin = origin
+
+ def _set_protocol(self):
+ self._request.ws_protocol = None
+
+ sec_websocket_protocol_header = self._transform_header(
+ common.SEC_WEBSOCKET_PROTOCOL_HEADER)
+ protocol_header = self._request.headers_in.get(
+ sec_websocket_protocol_header)
+
+ if protocol_header is None:
+ self._request.ws_requested_protocols = None
+ return
+
+ self._request.ws_requested_protocols = parse_token_list(
+ protocol_header)
+ self._logger.debug('Subprotocols requested: %r',
+ self._request.ws_requested_protocols)
+
+ def _parse_extensions(self):
+ sec_websocket_extensions_header = self._transform_header(
+ common.SEC_WEBSOCKET_EXTENSIONS_HEADER)
+ extensions_header = self._request.headers_in.get(
+ sec_websocket_extensions_header)
+ if not extensions_header:
+ self._request.ws_requested_extensions = None
+ return
+
+ try:
+ self._request.ws_requested_extensions = common.parse_extensions(
+ extensions_header)
+ except common.ExtensionParsingException as e:
+ raise HandshakeException(
+ 'Failed to parse sec-websocket-extensions header: %r' % e)
+
+ self._logger.debug(
+ 'Extensions requested: %r',
+ list(
+ map(common.ExtensionParameter.name,
+ self._request.ws_requested_extensions)))
+
+ def _get_extension_processors_requested(self):
+ processors = []
+ if self._request.ws_requested_extensions is not None:
+ for extension_request in self._request.ws_requested_extensions:
+ processor = get_extension_processor(extension_request)
+ # Unknown extension requests are just ignored.
+ if processor is not None:
+ processors.append(processor)
+ return processors
+
+ def _process_extensions(self, stream_options):
+ processors = [
+ processor for processor in self._request.ws_extension_processors
+ if processor is not None
+ ]
+
+ # Ask each processor if there are extensions on the request which
+ # cannot co-exist. When processor decided other processors cannot
+ # co-exist with it, the processor marks them (or itself) as
+ # "inactive". The first extension processor has the right to
+ # make the final call.
+ for processor in reversed(processors):
+ if processor.is_active():
+ processor.check_consistency_with_other_processors(processors)
+ processors = [
+ processor for processor in processors if processor.is_active()
+ ]
+
+ accepted_extensions = []
+
+ for index, processor in enumerate(processors):
+ if not processor.is_active():
+ continue
+
+ extension_response = processor.get_extension_response()
+ if extension_response is None:
+ # Rejected.
+ continue
+
+ accepted_extensions.append(extension_response)
+
+ processor.setup_stream_options(stream_options)
+
+ # Inactivate all of the following compression extensions.
+ for j in range(index + 1, len(processors)):
+ processors[j].set_active(False)
+
+ if len(accepted_extensions) > 0:
+ self._request.ws_extensions = accepted_extensions
+ self._logger.debug(
+ 'Extensions accepted: %r',
+ list(map(common.ExtensionParameter.name, accepted_extensions)))
+ else:
+ self._request.ws_extensions = None
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/hybi.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/hybi.py
new file mode 100644
index 0000000000..cf931db5a5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/hybi.py
@@ -0,0 +1,223 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""This file provides the opening handshake processor for the WebSocket
+protocol (RFC 6455).
+
+Specification:
+http://tools.ietf.org/html/rfc6455
+"""
+
+from __future__ import absolute_import
+import base64
+import re
+from hashlib import sha1
+
+from mod_pywebsocket import common
+from mod_pywebsocket.handshake.base import get_mandatory_header
+from mod_pywebsocket.handshake.base import HandshakeException
+from mod_pywebsocket.handshake.base import parse_token_list
+from mod_pywebsocket.handshake.base import validate_mandatory_header
+from mod_pywebsocket.handshake.base import HandshakerBase
+from mod_pywebsocket import util
+
+# Used to validate the value in the Sec-WebSocket-Key header strictly. RFC 4648
+# disallows non-zero padding, so the character right before == must be any of
+# A, Q, g and w.
+_SEC_WEBSOCKET_KEY_REGEX = re.compile('^[+/0-9A-Za-z]{21}[AQgw]==$')
+
+
+def check_request_line(request):
+ # 5.1 1. The three character UTF-8 string "GET".
+ # 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte).
+ if request.method != u'GET':
+ raise HandshakeException('Method is not GET: %r' % request.method)
+
+ if request.protocol != u'HTTP/1.1':
+ raise HandshakeException('Version is not HTTP/1.1: %r' %
+ request.protocol)
+
+
+def compute_accept(key):
+ """Computes value for the Sec-WebSocket-Accept header from value of the
+ Sec-WebSocket-Key header.
+ """
+
+ accept_binary = sha1(key + common.WEBSOCKET_ACCEPT_UUID).digest()
+ accept = base64.b64encode(accept_binary)
+
+ return accept
+
+
+def compute_accept_from_unicode(unicode_key):
+ """A wrapper function for compute_accept which takes a unicode string as an
+ argument, and encodes it to byte string. It then passes it on to
+ compute_accept.
+ """
+
+ key = unicode_key.encode('UTF-8')
+ return compute_accept(key)
+
+
+def format_header(name, value):
+ return u'%s: %s\r\n' % (name, value)
+
+
+class Handshaker(HandshakerBase):
+ """Opening handshake processor for the WebSocket protocol (RFC 6455)."""
+ def __init__(self, request, dispatcher):
+ """Construct an instance.
+
+ Args:
+ request: mod_python request.
+ dispatcher: Dispatcher (dispatch.Dispatcher).
+
+ Handshaker will add attributes such as ws_resource during handshake.
+ """
+ super(Handshaker, self).__init__(request, dispatcher)
+
+ def _transform_header(self, header):
+ return header
+
+ def _protocol_rfc(self):
+ return 'RFC 6455'
+
+ def _validate_connection_header(self):
+ connection = get_mandatory_header(self._request,
+ common.CONNECTION_HEADER)
+
+ try:
+ connection_tokens = parse_token_list(connection)
+ except HandshakeException as e:
+ raise HandshakeException('Failed to parse %s: %s' %
+ (common.CONNECTION_HEADER, e))
+
+ connection_is_valid = False
+ for token in connection_tokens:
+ if token.lower() == common.UPGRADE_CONNECTION_TYPE.lower():
+ connection_is_valid = True
+ break
+ if not connection_is_valid:
+ raise HandshakeException(
+ '%s header doesn\'t contain "%s"' %
+ (common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
+
+ def _validate_request(self):
+ check_request_line(self._request)
+ validate_mandatory_header(self._request, common.UPGRADE_HEADER,
+ common.WEBSOCKET_UPGRADE_TYPE)
+ self._validate_connection_header()
+ unused_host = get_mandatory_header(self._request, common.HOST_HEADER)
+
+ def _set_accept(self):
+ # Key validation, response generation.
+ key = self._get_key()
+ accept = compute_accept(key)
+ self._logger.debug('%s: %r (%s)', common.SEC_WEBSOCKET_ACCEPT_HEADER,
+ accept, util.hexify(base64.b64decode(accept)))
+ self._request._accept = accept
+
+ def _validate_key(self, key):
+ if key.find(',') >= 0:
+ raise HandshakeException('Request has multiple %s header lines or '
+ 'contains illegal character \',\': %r' %
+ (common.SEC_WEBSOCKET_KEY_HEADER, key))
+
+ # Validate
+ key_is_valid = False
+ try:
+ # Validate key by quick regex match before parsing by base64
+ # module. Because base64 module skips invalid characters, we have
+ # to do this in advance to make this server strictly reject illegal
+ # keys.
+ if _SEC_WEBSOCKET_KEY_REGEX.match(key):
+ decoded_key = base64.b64decode(key)
+ if len(decoded_key) == 16:
+ key_is_valid = True
+ except TypeError as e:
+ pass
+
+ if not key_is_valid:
+ raise HandshakeException('Illegal value for header %s: %r' %
+ (common.SEC_WEBSOCKET_KEY_HEADER, key))
+
+ return decoded_key
+
+ def _get_key(self):
+ key = get_mandatory_header(self._request,
+ common.SEC_WEBSOCKET_KEY_HEADER)
+
+ decoded_key = self._validate_key(key)
+
+ self._logger.debug('%s: %r (%s)', common.SEC_WEBSOCKET_KEY_HEADER, key,
+ util.hexify(decoded_key))
+
+ return key.encode('UTF-8')
+
+ def _create_handshake_response(self, accept):
+ response = []
+
+ response.append(u'HTTP/1.1 101 Switching Protocols\r\n')
+
+ # WebSocket headers
+ response.append(
+ format_header(common.UPGRADE_HEADER,
+ common.WEBSOCKET_UPGRADE_TYPE))
+ response.append(
+ format_header(common.CONNECTION_HEADER,
+ common.UPGRADE_CONNECTION_TYPE))
+ response.append(
+ format_header(common.SEC_WEBSOCKET_ACCEPT_HEADER,
+ accept.decode('UTF-8')))
+ if self._request.ws_protocol is not None:
+ response.append(
+ format_header(common.SEC_WEBSOCKET_PROTOCOL_HEADER,
+ self._request.ws_protocol))
+ if (self._request.ws_extensions is not None
+ and len(self._request.ws_extensions) != 0):
+ response.append(
+ format_header(
+ common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
+ common.format_extensions(self._request.ws_extensions)))
+
+ # Headers not specific for WebSocket
+ for name, value in self._request.extra_headers:
+ response.append(format_header(name, value))
+
+ response.append(u'\r\n')
+
+ return u''.join(response)
+
+ def _send_handshake(self):
+ raw_response = self._create_handshake_response(self._request._accept)
+ self._request.connection.write(raw_response.encode('UTF-8'))
+ self._logger.debug('Sent server\'s opening handshake: %r',
+ raw_response)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/http_header_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/http_header_util.py
new file mode 100644
index 0000000000..21fde59af1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/http_header_util.py
@@ -0,0 +1,254 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Utilities for parsing and formatting headers that follow the grammar defined
+in HTTP RFC http://www.ietf.org/rfc/rfc2616.txt.
+"""
+
+from __future__ import absolute_import
+import six.moves.urllib.parse
+
+_SEPARATORS = '()<>@,;:\\"/[]?={} \t'
+
+
+def _is_char(c):
+ """Returns true iff c is in CHAR as specified in HTTP RFC."""
+
+ return ord(c) <= 127
+
+
+def _is_ctl(c):
+ """Returns true iff c is in CTL as specified in HTTP RFC."""
+
+ return ord(c) <= 31 or ord(c) == 127
+
+
+class ParsingState(object):
+ def __init__(self, data):
+ self.data = data
+ self.head = 0
+
+
+def peek(state, pos=0):
+ """Peeks the character at pos from the head of data."""
+
+ if state.head + pos >= len(state.data):
+ return None
+
+ return state.data[state.head + pos]
+
+
+def consume(state, amount=1):
+ """Consumes specified amount of bytes from the head and returns the
+ consumed bytes. If there's not enough bytes to consume, returns None.
+ """
+
+ if state.head + amount > len(state.data):
+ return None
+
+ result = state.data[state.head:state.head + amount]
+ state.head = state.head + amount
+ return result
+
+
+def consume_string(state, expected):
+ """Given a parsing state and a expected string, consumes the string from
+ the head. Returns True if consumed successfully. Otherwise, returns
+ False.
+ """
+
+ pos = 0
+
+ for c in expected:
+ if c != peek(state, pos):
+ return False
+ pos += 1
+
+ consume(state, pos)
+ return True
+
+
+def consume_lws(state):
+ """Consumes a LWS from the head. Returns True if any LWS is consumed.
+ Otherwise, returns False.
+
+ LWS = [CRLF] 1*( SP | HT )
+ """
+
+ original_head = state.head
+
+ consume_string(state, '\r\n')
+
+ pos = 0
+
+ while True:
+ c = peek(state, pos)
+ if c == ' ' or c == '\t':
+ pos += 1
+ else:
+ if pos == 0:
+ state.head = original_head
+ return False
+ else:
+ consume(state, pos)
+ return True
+
+
+def consume_lwses(state):
+ r"""Consumes \*LWS from the head."""
+
+ while consume_lws(state):
+ pass
+
+
+def consume_token(state):
+ """Consumes a token from the head. Returns the token or None if no token
+ was found.
+ """
+
+ pos = 0
+
+ while True:
+ c = peek(state, pos)
+ if c is None or c in _SEPARATORS or _is_ctl(c) or not _is_char(c):
+ if pos == 0:
+ return None
+
+ return consume(state, pos)
+ else:
+ pos += 1
+
+
+def consume_token_or_quoted_string(state):
+ """Consumes a token or a quoted-string, and returns the token or unquoted
+ string. If no token or quoted-string was found, returns None.
+ """
+
+ original_head = state.head
+
+ if not consume_string(state, '"'):
+ return consume_token(state)
+
+ result = []
+
+ expect_quoted_pair = False
+
+ while True:
+ if not expect_quoted_pair and consume_lws(state):
+ result.append(' ')
+ continue
+
+ c = consume(state)
+ if c is None:
+ # quoted-string is not enclosed with double quotation
+ state.head = original_head
+ return None
+ elif expect_quoted_pair:
+ expect_quoted_pair = False
+ if _is_char(c):
+ result.append(c)
+ else:
+ # Non CHAR character found in quoted-pair
+ state.head = original_head
+ return None
+ elif c == '\\':
+ expect_quoted_pair = True
+ elif c == '"':
+ return ''.join(result)
+ elif _is_ctl(c):
+ # Invalid character %r found in qdtext
+ state.head = original_head
+ return None
+ else:
+ result.append(c)
+
+
+def quote_if_necessary(s):
+ """Quotes arbitrary string into quoted-string."""
+
+ quote = False
+ if s == '':
+ return '""'
+
+ result = []
+ for c in s:
+ if c == '"' or c in _SEPARATORS or _is_ctl(c) or not _is_char(c):
+ quote = True
+
+ if c == '"' or _is_ctl(c):
+ result.append('\\' + c)
+ else:
+ result.append(c)
+
+ if quote:
+ return '"' + ''.join(result) + '"'
+ else:
+ return ''.join(result)
+
+
+def parse_uri(uri):
+ """Parse absolute URI then return host, port and resource."""
+
+ parsed = six.moves.urllib.parse.urlsplit(uri)
+ if parsed.scheme != 'wss' and parsed.scheme != 'ws':
+ # |uri| must be a relative URI.
+ # TODO(toyoshim): Should validate |uri|.
+ return None, None, uri
+
+ if parsed.hostname is None:
+ return None, None, None
+
+ port = None
+ try:
+ port = parsed.port
+ except ValueError:
+ # The port property cause ValueError on invalid null port descriptions
+ # like 'ws://host:INVALID_PORT/path', where the assigned port is not
+ # *DIGIT. For python 3.6 and later, ValueError also raises when
+ # assigning invalid port numbers such as 'ws://host:-1/path'. Earlier
+ # versions simply return None and ignore invalid port attributes.
+ return None, None, None
+
+ if port is None:
+ if parsed.scheme == 'ws':
+ port = 80
+ else:
+ port = 443
+
+ path = parsed.path
+ if not path:
+ path += '/'
+ if parsed.query:
+ path += '?' + parsed.query
+ if parsed.fragment:
+ path += '#' + parsed.fragment
+
+ return parsed.hostname, port, path
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/memorizingfile.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/memorizingfile.py
new file mode 100644
index 0000000000..d353967618
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/memorizingfile.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Memorizing file.
+
+A memorizing file wraps a file and memorizes lines read by readline.
+"""
+
+from __future__ import absolute_import
+import sys
+
+
+class MemorizingFile(object):
+ """MemorizingFile wraps a file and memorizes lines read by readline.
+
+ Note that data read by other methods are not memorized. This behavior
+ is good enough for memorizing lines SimpleHTTPServer reads before
+ the control reaches WebSocketRequestHandler.
+ """
+ def __init__(self, file_, max_memorized_lines=sys.maxsize):
+ """Construct an instance.
+
+ Args:
+ file_: the file object to wrap.
+ max_memorized_lines: the maximum number of lines to memorize.
+ Only the first max_memorized_lines are memorized.
+ Default: sys.maxint.
+ """
+ self._file = file_
+ self._memorized_lines = []
+ self._max_memorized_lines = max_memorized_lines
+ self._buffered = False
+ self._buffered_line = None
+
+ def __getattribute__(self, name):
+ """Return a file attribute.
+
+ Returns the value overridden by this class for some attributes,
+ and forwards the call to _file for the other attributes.
+ """
+ if name in ('_file', '_memorized_lines', '_max_memorized_lines',
+ '_buffered', '_buffered_line', 'readline',
+ 'get_memorized_lines'):
+ return object.__getattribute__(self, name)
+ return self._file.__getattribute__(name)
+
+ def readline(self, size=-1):
+ """Override file.readline and memorize the line read.
+
+ Note that even if size is specified and smaller than actual size,
+ the whole line will be read out from underlying file object by
+ subsequent readline calls.
+ """
+ if self._buffered:
+ line = self._buffered_line
+ self._buffered = False
+ else:
+ line = self._file.readline()
+ if line and len(self._memorized_lines) < self._max_memorized_lines:
+ self._memorized_lines.append(line)
+ if size >= 0 and size < len(line):
+ self._buffered = True
+ self._buffered_line = line[size:]
+ return line[:size]
+ return line
+
+ def get_memorized_lines(self):
+ """Get lines memorized so far."""
+ return self._memorized_lines
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/msgutil.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/msgutil.py
new file mode 100644
index 0000000000..f58ca78e14
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/msgutil.py
@@ -0,0 +1,214 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Message related utilities.
+
+Note: request.connection.write/read are used in this module, even though
+mod_python document says that they should be used only in connection
+handlers. Unfortunately, we have no other options. For example,
+request.write/read are not suitable because they don't allow direct raw
+bytes writing/reading.
+"""
+
+from __future__ import absolute_import
+import six.moves.queue
+import threading
+
+# Export Exception symbols from msgutil for backward compatibility
+from mod_pywebsocket._stream_exceptions import ConnectionTerminatedException
+from mod_pywebsocket._stream_exceptions import InvalidFrameException
+from mod_pywebsocket._stream_exceptions import BadOperationException
+from mod_pywebsocket._stream_exceptions import UnsupportedFrameException
+
+
+# An API for handler to send/receive WebSocket messages.
+def close_connection(request):
+ """Close connection.
+
+ Args:
+ request: mod_python request.
+ """
+ request.ws_stream.close_connection()
+
+
+def send_message(request, payload_data, end=True, binary=False):
+ """Send a message (or part of a message).
+
+ Args:
+ request: mod_python request.
+ payload_data: unicode text or str binary to send.
+ end: True to terminate a message.
+ False to send payload_data as part of a message that is to be
+ terminated by next or later send_message call with end=True.
+ binary: send payload_data as binary frame(s).
+ Raises:
+ BadOperationException: when server already terminated.
+ """
+ request.ws_stream.send_message(payload_data, end, binary)
+
+
+def receive_message(request):
+ """Receive a WebSocket frame and return its payload as a text in
+ unicode or a binary in str.
+
+ Args:
+ request: mod_python request.
+ Raises:
+ InvalidFrameException: when client send invalid frame.
+ UnsupportedFrameException: when client send unsupported frame e.g. some
+ of reserved bit is set but no extension can
+ recognize it.
+ InvalidUTF8Exception: when client send a text frame containing any
+ invalid UTF-8 string.
+ ConnectionTerminatedException: when the connection is closed
+ unexpectedly.
+ BadOperationException: when client already terminated.
+ """
+ return request.ws_stream.receive_message()
+
+
+def send_ping(request, body):
+ request.ws_stream.send_ping(body)
+
+
+class MessageReceiver(threading.Thread):
+ """This class receives messages from the client.
+
+ This class provides three ways to receive messages: blocking,
+ non-blocking, and via callback. Callback has the highest precedence.
+
+ Note: This class should not be used with the standalone server for wss
+ because pyOpenSSL used by the server raises a fatal error if the socket
+ is accessed from multiple threads.
+ """
+ def __init__(self, request, onmessage=None):
+ """Construct an instance.
+
+ Args:
+ request: mod_python request.
+ onmessage: a function to be called when a message is received.
+ May be None. If not None, the function is called on
+ another thread. In that case, MessageReceiver.receive
+ and MessageReceiver.receive_nowait are useless
+ because they will never return any messages.
+ """
+
+ threading.Thread.__init__(self)
+ self._request = request
+ self._queue = six.moves.queue.Queue()
+ self._onmessage = onmessage
+ self._stop_requested = False
+ self.setDaemon(True)
+ self.start()
+
+ def run(self):
+ try:
+ while not self._stop_requested:
+ message = receive_message(self._request)
+ if self._onmessage:
+ self._onmessage(message)
+ else:
+ self._queue.put(message)
+ finally:
+ close_connection(self._request)
+
+ def receive(self):
+ """ Receive a message from the channel, blocking.
+
+ Returns:
+ message as a unicode string.
+ """
+ return self._queue.get()
+
+ def receive_nowait(self):
+ """ Receive a message from the channel, non-blocking.
+
+ Returns:
+ message as a unicode string if available. None otherwise.
+ """
+ try:
+ message = self._queue.get_nowait()
+ except six.moves.queue.Empty:
+ message = None
+ return message
+
+ def stop(self):
+ """Request to stop this instance.
+
+ The instance will be stopped after receiving the next message.
+ This method may not be very useful, but there is no clean way
+ in Python to forcefully stop a running thread.
+ """
+ self._stop_requested = True
+
+
+class MessageSender(threading.Thread):
+ """This class sends messages to the client.
+
+ This class provides both synchronous and asynchronous ways to send
+ messages.
+
+ Note: This class should not be used with the standalone server for wss
+ because pyOpenSSL used by the server raises a fatal error if the socket
+ is accessed from multiple threads.
+ """
+ def __init__(self, request):
+ """Construct an instance.
+
+ Args:
+ request: mod_python request.
+ """
+ threading.Thread.__init__(self)
+ self._request = request
+ self._queue = six.moves.queue.Queue()
+ self.setDaemon(True)
+ self.start()
+
+ def run(self):
+ while True:
+ message, condition = self._queue.get()
+ condition.acquire()
+ send_message(self._request, message)
+ condition.notify()
+ condition.release()
+
+ def send(self, message):
+ """Send a message, blocking."""
+
+ condition = threading.Condition()
+ condition.acquire()
+ self._queue.put((message, condition))
+ condition.wait()
+
+ def send_nowait(self, message):
+ """Send a message, non-blocking."""
+
+ self._queue.put((message, threading.Condition()))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/request_handler.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/request_handler.py
new file mode 100644
index 0000000000..5e9c875dc7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/request_handler.py
@@ -0,0 +1,319 @@
+# Copyright 2020, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Request Handler and Request/Connection classes for standalone server.
+"""
+
+import os
+
+from six.moves import CGIHTTPServer
+from six.moves import http_client
+
+from mod_pywebsocket import common
+from mod_pywebsocket import dispatch
+from mod_pywebsocket import handshake
+from mod_pywebsocket import http_header_util
+from mod_pywebsocket import memorizingfile
+from mod_pywebsocket import util
+
+# 1024 is practically large enough to contain WebSocket handshake lines.
+_MAX_MEMORIZED_LINES = 1024
+
+
+class _StandaloneConnection(object):
+ """Mimic mod_python mp_conn."""
+ def __init__(self, request_handler):
+ """Construct an instance.
+
+ Args:
+ request_handler: A WebSocketRequestHandler instance.
+ """
+
+ self._request_handler = request_handler
+
+ def get_local_addr(self):
+ """Getter to mimic mp_conn.local_addr."""
+
+ return (self._request_handler.server.server_name,
+ self._request_handler.server.server_port)
+
+ local_addr = property(get_local_addr)
+
+ def get_remote_addr(self):
+ """Getter to mimic mp_conn.remote_addr.
+
+ Setting the property in __init__ won't work because the request
+ handler is not initialized yet there."""
+
+ return self._request_handler.client_address
+
+ remote_addr = property(get_remote_addr)
+
+ def write(self, data):
+ """Mimic mp_conn.write()."""
+
+ return self._request_handler.wfile.write(data)
+
+ def read(self, length):
+ """Mimic mp_conn.read()."""
+
+ return self._request_handler.rfile.read(length)
+
+ def get_memorized_lines(self):
+ """Get memorized lines."""
+
+ return self._request_handler.rfile.get_memorized_lines()
+
+
+class _StandaloneRequest(object):
+ """Mimic mod_python request."""
+ def __init__(self, request_handler, use_tls):
+ """Construct an instance.
+
+ Args:
+ request_handler: A WebSocketRequestHandler instance.
+ """
+
+ self._logger = util.get_class_logger(self)
+
+ self._request_handler = request_handler
+ self.connection = _StandaloneConnection(request_handler)
+ self._use_tls = use_tls
+ self.headers_in = request_handler.headers
+
+ def get_uri(self):
+ """Getter to mimic request.uri.
+
+ This method returns the raw data at the Request-URI part of the
+ Request-Line, while the uri method on the request object of mod_python
+ returns the path portion after parsing the raw data. This behavior is
+ kept for compatibility.
+ """
+
+ return self._request_handler.path
+
+ uri = property(get_uri)
+
+ def get_unparsed_uri(self):
+ """Getter to mimic request.unparsed_uri."""
+
+ return self._request_handler.path
+
+ unparsed_uri = property(get_unparsed_uri)
+
+ def get_method(self):
+ """Getter to mimic request.method."""
+
+ return self._request_handler.command
+
+ method = property(get_method)
+
+ def get_protocol(self):
+ """Getter to mimic request.protocol."""
+
+ return self._request_handler.request_version
+
+ protocol = property(get_protocol)
+
+ def is_https(self):
+ """Mimic request.is_https()."""
+
+ return self._use_tls
+
+
+class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
+ """CGIHTTPRequestHandler specialized for WebSocket."""
+
+ # Use httplib.HTTPMessage instead of mimetools.Message.
+ MessageClass = http_client.HTTPMessage
+
+ def setup(self):
+ """Override SocketServer.StreamRequestHandler.setup to wrap rfile
+ with MemorizingFile.
+
+ This method will be called by BaseRequestHandler's constructor
+ before calling BaseHTTPRequestHandler.handle.
+ BaseHTTPRequestHandler.handle will call
+ BaseHTTPRequestHandler.handle_one_request and it will call
+ WebSocketRequestHandler.parse_request.
+ """
+
+ # Call superclass's setup to prepare rfile, wfile, etc. See setup
+ # definition on the root class SocketServer.StreamRequestHandler to
+ # understand what this does.
+ CGIHTTPServer.CGIHTTPRequestHandler.setup(self)
+
+ self.rfile = memorizingfile.MemorizingFile(
+ self.rfile, max_memorized_lines=_MAX_MEMORIZED_LINES)
+
+ def __init__(self, request, client_address, server):
+ self._logger = util.get_class_logger(self)
+
+ self._options = server.websocket_server_options
+
+ # Overrides CGIHTTPServerRequestHandler.cgi_directories.
+ self.cgi_directories = self._options.cgi_directories
+ # Replace CGIHTTPRequestHandler.is_executable method.
+ if self._options.is_executable_method is not None:
+ self.is_executable = self._options.is_executable_method
+
+ # This actually calls BaseRequestHandler.__init__.
+ CGIHTTPServer.CGIHTTPRequestHandler.__init__(self, request,
+ client_address, server)
+
+ def parse_request(self):
+ """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
+
+ Return True to continue processing for HTTP(S), False otherwise.
+
+ See BaseHTTPRequestHandler.handle_one_request method which calls
+ this method to understand how the return value will be handled.
+ """
+
+ # We hook parse_request method, but also call the original
+ # CGIHTTPRequestHandler.parse_request since when we return False,
+ # CGIHTTPRequestHandler.handle_one_request continues processing and
+ # it needs variables set by CGIHTTPRequestHandler.parse_request.
+ #
+ # Variables set by this method will be also used by WebSocket request
+ # handling (self.path, self.command, self.requestline, etc. See also
+ # how _StandaloneRequest's members are implemented using these
+ # attributes).
+ if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self):
+ return False
+
+ if self._options.use_basic_auth:
+ auth = self.headers.get('Authorization')
+ if auth != self._options.basic_auth_credential:
+ self.send_response(401)
+ self.send_header('WWW-Authenticate',
+ 'Basic realm="Pywebsocket"')
+ self.end_headers()
+ self._logger.info('Request basic authentication')
+ return False
+
+ host, port, resource = http_header_util.parse_uri(self.path)
+ if resource is None:
+ self._logger.info('Invalid URI: %r', self.path)
+ self._logger.info('Fallback to CGIHTTPRequestHandler')
+ return True
+ server_options = self.server.websocket_server_options
+ if host is not None:
+ validation_host = server_options.validation_host
+ if validation_host is not None and host != validation_host:
+ self._logger.info('Invalid host: %r (expected: %r)', host,
+ validation_host)
+ self._logger.info('Fallback to CGIHTTPRequestHandler')
+ return True
+ if port is not None:
+ validation_port = server_options.validation_port
+ if validation_port is not None and port != validation_port:
+ self._logger.info('Invalid port: %r (expected: %r)', port,
+ validation_port)
+ self._logger.info('Fallback to CGIHTTPRequestHandler')
+ return True
+ self.path = resource
+
+ request = _StandaloneRequest(self, self._options.use_tls)
+
+ try:
+ # Fallback to default http handler for request paths for which
+ # we don't have request handlers.
+ if not self._options.dispatcher.get_handler_suite(self.path):
+ self._logger.info('No handler for resource: %r', self.path)
+ self._logger.info('Fallback to CGIHTTPRequestHandler')
+ return True
+ except dispatch.DispatchException as e:
+ self._logger.info('Dispatch failed for error: %s', e)
+ self.send_error(e.status)
+ return False
+
+ # If any Exceptions without except clause setup (including
+ # DispatchException) is raised below this point, it will be caught
+ # and logged by WebSocketServer.
+
+ try:
+ try:
+ handshake.do_handshake(request, self._options.dispatcher)
+ except handshake.VersionException as e:
+ self._logger.info('Handshake failed for version error: %s', e)
+ self.send_response(common.HTTP_STATUS_BAD_REQUEST)
+ self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER,
+ e.supported_versions)
+ self.end_headers()
+ return False
+ except handshake.HandshakeException as e:
+ # Handshake for ws(s) failed.
+ self._logger.info('Handshake failed for error: %s', e)
+ self.send_error(e.status)
+ return False
+
+ request._dispatcher = self._options.dispatcher
+ self._options.dispatcher.transfer_data(request)
+ except handshake.AbortedByUserException as e:
+ self._logger.info('Aborted: %s', e)
+ return False
+
+ def log_request(self, code='-', size='-'):
+ """Override BaseHTTPServer.log_request."""
+
+ self._logger.info('"%s" %s %s', self.requestline, str(code), str(size))
+
+ def log_error(self, *args):
+ """Override BaseHTTPServer.log_error."""
+
+ # Despite the name, this method is for warnings than for errors.
+ # For example, HTTP status code is logged by this method.
+ self._logger.warning('%s - %s', self.address_string(),
+ args[0] % args[1:])
+
+ def is_cgi(self):
+ """Test whether self.path corresponds to a CGI script.
+
+ Add extra check that self.path doesn't contains ..
+ Also check if the file is a executable file or not.
+ If the file is not executable, it is handled as static file or dir
+ rather than a CGI script.
+ """
+
+ if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self):
+ if '..' in self.path:
+ return False
+ # strip query parameter from request path
+ resource_name = self.path.split('?', 2)[0]
+ # convert resource_name into real path name in filesystem.
+ scriptfile = self.translate_path(resource_name)
+ if not os.path.isfile(scriptfile):
+ return False
+ if not self.is_executable(scriptfile):
+ return False
+ return True
+ return False
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/server_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/server_util.py
new file mode 100644
index 0000000000..8f9e273e97
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/server_util.py
@@ -0,0 +1,87 @@
+# Copyright 2020, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Server related utilities."""
+
+import logging
+import logging.handlers
+import threading
+import time
+
+from mod_pywebsocket import common
+from mod_pywebsocket import util
+
+
+def _get_logger_from_class(c):
+ return logging.getLogger('%s.%s' % (c.__module__, c.__name__))
+
+
+def configure_logging(options):
+ logging.addLevelName(common.LOGLEVEL_FINE, 'FINE')
+
+ logger = logging.getLogger()
+ logger.setLevel(logging.getLevelName(options.log_level.upper()))
+ if options.log_file:
+ handler = logging.handlers.RotatingFileHandler(options.log_file, 'a',
+ options.log_max,
+ options.log_count)
+ else:
+ handler = logging.StreamHandler()
+ formatter = logging.Formatter(
+ '[%(asctime)s] [%(levelname)s] %(name)s: %(message)s')
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+
+ deflate_log_level_name = logging.getLevelName(
+ options.deflate_log_level.upper())
+ _get_logger_from_class(util._Deflater).setLevel(deflate_log_level_name)
+ _get_logger_from_class(util._Inflater).setLevel(deflate_log_level_name)
+
+
+class ThreadMonitor(threading.Thread):
+ daemon = True
+
+ def __init__(self, interval_in_sec):
+ threading.Thread.__init__(self, name='ThreadMonitor')
+
+ self._logger = util.get_class_logger(self)
+
+ self._interval_in_sec = interval_in_sec
+
+ def run(self):
+ while True:
+ thread_name_list = []
+ for thread in threading.enumerate():
+ thread_name_list.append(thread.name)
+ self._logger.info("%d active threads: %s",
+ threading.active_count(),
+ ', '.join(thread_name_list))
+ time.sleep(self._interval_in_sec)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/standalone.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/standalone.py
new file mode 100755
index 0000000000..0a3bcdbacd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/standalone.py
@@ -0,0 +1,481 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Standalone WebSocket server.
+
+Use this file to launch pywebsocket as a standalone server.
+
+
+BASIC USAGE
+===========
+
+Go to the src directory and run
+
+ $ python mod_pywebsocket/standalone.py [-p <ws_port>]
+ [-w <websock_handlers>]
+ [-d <document_root>]
+
+<ws_port> is the port number to use for ws:// connection.
+
+<document_root> is the path to the root directory of HTML files.
+
+<websock_handlers> is the path to the root directory of WebSocket handlers.
+If not specified, <document_root> will be used. See __init__.py (or
+run $ pydoc mod_pywebsocket) for how to write WebSocket handlers.
+
+For more detail and other options, run
+
+ $ python mod_pywebsocket/standalone.py --help
+
+or see _build_option_parser method below.
+
+For trouble shooting, adding "--log_level debug" might help you.
+
+
+TRY DEMO
+========
+
+Go to the src directory and run standalone.py with -d option to set the
+document root to the directory containing example HTMLs and handlers like this:
+
+ $ cd src
+ $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example
+
+to launch pywebsocket with the sample handler and html on port 80. Open
+http://localhost/console.html, click the connect button, type something into
+the text box next to the send button and click the send button. If everything
+is working, you'll see the message you typed echoed by the server.
+
+
+USING TLS
+=========
+
+To run the standalone server with TLS support, run it with -t, -k, and -c
+options. When TLS is enabled, the standalone server accepts only TLS connection.
+
+Note that when ssl module is used and the key/cert location is incorrect,
+TLS connection silently fails while pyOpenSSL fails on startup.
+
+Example:
+
+ $ PYTHONPATH=. python mod_pywebsocket/standalone.py \
+ -d example \
+ -p 10443 \
+ -t \
+ -c ../test/cert/cert.pem \
+ -k ../test/cert/key.pem \
+
+Note that when passing a relative path to -c and -k option, it will be resolved
+using the document root directory as the base.
+
+
+USING CLIENT AUTHENTICATION
+===========================
+
+To run the standalone server with TLS client authentication support, run it with
+--tls-client-auth and --tls-client-ca options in addition to ones required for
+TLS support.
+
+Example:
+
+ $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example -p 10443 -t \
+ -c ../test/cert/cert.pem -k ../test/cert/key.pem \
+ --tls-client-auth \
+ --tls-client-ca=../test/cert/cacert.pem
+
+Note that when passing a relative path to --tls-client-ca option, it will be
+resolved using the document root directory as the base.
+
+
+CONFIGURATION FILE
+==================
+
+You can also write a configuration file and use it by specifying the path to
+the configuration file by --config option. Please write a configuration file
+following the documentation of the Python ConfigParser library. Name of each
+entry must be the long version argument name. E.g. to set log level to debug,
+add the following line:
+
+log_level=debug
+
+For options which doesn't take value, please add some fake value. E.g. for
+--tls option, add the following line:
+
+tls=True
+
+Note that tls will be enabled even if you write tls=False as the value part is
+fake.
+
+When both a command line argument and a configuration file entry are set for
+the same configuration item, the command line value will override one in the
+configuration file.
+
+
+THREADING
+=========
+
+This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
+used for each request.
+
+
+SECURITY WARNING
+================
+
+This uses CGIHTTPServer and CGIHTTPServer is not secure.
+It may execute arbitrary Python code or external programs. It should not be
+used outside a firewall.
+"""
+
+from __future__ import absolute_import
+from six.moves import configparser
+import base64
+import logging
+import argparse
+import os
+import six
+import sys
+import traceback
+
+from mod_pywebsocket import common
+from mod_pywebsocket import util
+from mod_pywebsocket import server_util
+from mod_pywebsocket.websocket_server import WebSocketServer
+
+_DEFAULT_LOG_MAX_BYTES = 1024 * 256
+_DEFAULT_LOG_BACKUP_COUNT = 5
+
+_DEFAULT_REQUEST_QUEUE_SIZE = 128
+
+
+def _build_option_parser():
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument(
+ '--config',
+ dest='config_file',
+ type=six.text_type,
+ default=None,
+ help=('Path to configuration file. See the file comment '
+ 'at the top of this file for the configuration '
+ 'file format'))
+ parser.add_argument('-H',
+ '--server-host',
+ '--server_host',
+ dest='server_host',
+ default='',
+ help='server hostname to listen to')
+ parser.add_argument('-V',
+ '--validation-host',
+ '--validation_host',
+ dest='validation_host',
+ default=None,
+ help='server hostname to validate in absolute path.')
+ parser.add_argument('-p',
+ '--port',
+ dest='port',
+ type=int,
+ default=common.DEFAULT_WEB_SOCKET_PORT,
+ help='port to listen to')
+ parser.add_argument('-P',
+ '--validation-port',
+ '--validation_port',
+ dest='validation_port',
+ type=int,
+ default=None,
+ help='server port to validate in absolute path.')
+ parser.add_argument(
+ '-w',
+ '--websock-handlers',
+ '--websock_handlers',
+ dest='websock_handlers',
+ default='.',
+ help=('The root directory of WebSocket handler files. '
+ 'If the path is relative, --document-root is used '
+ 'as the base.'))
+ parser.add_argument('-m',
+ '--websock-handlers-map-file',
+ '--websock_handlers_map_file',
+ dest='websock_handlers_map_file',
+ default=None,
+ help=('WebSocket handlers map file. '
+ 'Each line consists of alias_resource_path and '
+ 'existing_resource_path, separated by spaces.'))
+ parser.add_argument('-s',
+ '--scan-dir',
+ '--scan_dir',
+ dest='scan_dir',
+ default=None,
+ help=('Must be a directory under --websock-handlers. '
+ 'Only handlers under this directory are scanned '
+ 'and registered to the server. '
+ 'Useful for saving scan time when the handler '
+ 'root directory contains lots of files that are '
+ 'not handler file or are handler files but you '
+ 'don\'t want them to be registered. '))
+ parser.add_argument(
+ '--allow-handlers-outside-root-dir',
+ '--allow_handlers_outside_root_dir',
+ dest='allow_handlers_outside_root_dir',
+ action='store_true',
+ default=False,
+ help=('Scans WebSocket handlers even if their canonical '
+ 'path is not under --websock-handlers.'))
+ parser.add_argument('-d',
+ '--document-root',
+ '--document_root',
+ dest='document_root',
+ default='.',
+ help='Document root directory.')
+ parser.add_argument('-x',
+ '--cgi-paths',
+ '--cgi_paths',
+ dest='cgi_paths',
+ default=None,
+ help=('CGI paths relative to document_root.'
+ 'Comma-separated. (e.g -x /cgi,/htbin) '
+ 'Files under document_root/cgi_path are handled '
+ 'as CGI programs. Must be executable.'))
+ parser.add_argument('-t',
+ '--tls',
+ dest='use_tls',
+ action='store_true',
+ default=False,
+ help='use TLS (wss://)')
+ parser.add_argument('-k',
+ '--private-key',
+ '--private_key',
+ dest='private_key',
+ default='',
+ help='TLS private key file.')
+ parser.add_argument('-c',
+ '--certificate',
+ dest='certificate',
+ default='',
+ help='TLS certificate file.')
+ parser.add_argument('--tls-client-auth',
+ dest='tls_client_auth',
+ action='store_true',
+ default=False,
+ help='Requests TLS client auth on every connection.')
+ parser.add_argument('--tls-client-cert-optional',
+ dest='tls_client_cert_optional',
+ action='store_true',
+ default=False,
+ help=('Makes client certificate optional even though '
+ 'TLS client auth is enabled.'))
+ parser.add_argument('--tls-client-ca',
+ dest='tls_client_ca',
+ default='',
+ help=('Specifies a pem file which contains a set of '
+ 'concatenated CA certificates which are used to '
+ 'validate certificates passed from clients'))
+ parser.add_argument('--basic-auth',
+ dest='use_basic_auth',
+ action='store_true',
+ default=False,
+ help='Requires Basic authentication.')
+ parser.add_argument(
+ '--basic-auth-credential',
+ dest='basic_auth_credential',
+ default='test:test',
+ help='Specifies the credential of basic authentication '
+ 'by username:password pair (e.g. test:test).')
+ parser.add_argument('-l',
+ '--log-file',
+ '--log_file',
+ dest='log_file',
+ default='',
+ help='Log file.')
+ # Custom log level:
+ # - FINE: Prints status of each frame processing step
+ parser.add_argument('--log-level',
+ '--log_level',
+ type=six.text_type,
+ dest='log_level',
+ default='warn',
+ choices=[
+ 'fine', 'debug', 'info', 'warning', 'warn',
+ 'error', 'critical'
+ ],
+ help='Log level.')
+ parser.add_argument(
+ '--deflate-log-level',
+ '--deflate_log_level',
+ type=six.text_type,
+ dest='deflate_log_level',
+ default='warn',
+ choices=['debug', 'info', 'warning', 'warn', 'error', 'critical'],
+ help='Log level for _Deflater and _Inflater.')
+ parser.add_argument('--thread-monitor-interval-in-sec',
+ '--thread_monitor_interval_in_sec',
+ dest='thread_monitor_interval_in_sec',
+ type=int,
+ default=-1,
+ help=('If positive integer is specified, run a thread '
+ 'monitor to show the status of server threads '
+ 'periodically in the specified inteval in '
+ 'second. If non-positive integer is specified, '
+ 'disable the thread monitor.'))
+ parser.add_argument('--log-max',
+ '--log_max',
+ dest='log_max',
+ type=int,
+ default=_DEFAULT_LOG_MAX_BYTES,
+ help='Log maximum bytes')
+ parser.add_argument('--log-count',
+ '--log_count',
+ dest='log_count',
+ type=int,
+ default=_DEFAULT_LOG_BACKUP_COUNT,
+ help='Log backup count')
+ parser.add_argument('-q',
+ '--queue',
+ dest='request_queue_size',
+ type=int,
+ default=_DEFAULT_REQUEST_QUEUE_SIZE,
+ help='request queue size')
+
+ return parser
+
+
+def _parse_args_and_config(args):
+ parser = _build_option_parser()
+
+ # First, parse options without configuration file.
+ temporary_options, temporary_args = parser.parse_known_args(args=args)
+ if temporary_args:
+ logging.critical('Unrecognized positional arguments: %r',
+ temporary_args)
+ sys.exit(1)
+
+ if temporary_options.config_file:
+ try:
+ config_fp = open(temporary_options.config_file, 'r')
+ except IOError as e:
+ logging.critical('Failed to open configuration file %r: %r',
+ temporary_options.config_file, e)
+ sys.exit(1)
+
+ config_parser = configparser.SafeConfigParser()
+ config_parser.readfp(config_fp)
+ config_fp.close()
+
+ args_from_config = []
+ for name, value in config_parser.items('pywebsocket'):
+ args_from_config.append('--' + name)
+ args_from_config.append(value)
+ if args is None:
+ args = args_from_config
+ else:
+ args = args_from_config + args
+ return parser.parse_known_args(args=args)
+ else:
+ return temporary_options, temporary_args
+
+
+def _main(args=None):
+ """You can call this function from your own program, but please note that
+ this function has some side-effects that might affect your program. For
+ example, it changes the current directory.
+ """
+
+ options, args = _parse_args_and_config(args=args)
+
+ os.chdir(options.document_root)
+
+ server_util.configure_logging(options)
+
+ # TODO(tyoshino): Clean up initialization of CGI related values. Move some
+ # of code here to WebSocketRequestHandler class if it's better.
+ options.cgi_directories = []
+ options.is_executable_method = None
+ if options.cgi_paths:
+ options.cgi_directories = options.cgi_paths.split(',')
+ if sys.platform in ('cygwin', 'win32'):
+ cygwin_path = None
+ # For Win32 Python, it is expected that CYGWIN_PATH
+ # is set to a directory of cygwin binaries.
+ # For example, websocket_server.py in Chromium sets CYGWIN_PATH to
+ # full path of third_party/cygwin/bin.
+ if 'CYGWIN_PATH' in os.environ:
+ cygwin_path = os.environ['CYGWIN_PATH']
+
+ def __check_script(scriptpath):
+ return util.get_script_interp(scriptpath, cygwin_path)
+
+ options.is_executable_method = __check_script
+
+ if options.use_tls:
+ logging.debug('Using ssl module')
+
+ if not options.private_key or not options.certificate:
+ logging.critical(
+ 'To use TLS, specify private_key and certificate.')
+ sys.exit(1)
+
+ if (options.tls_client_cert_optional and not options.tls_client_auth):
+ logging.critical('Client authentication must be enabled to '
+ 'specify tls_client_cert_optional')
+ sys.exit(1)
+ else:
+ if options.tls_client_auth:
+ logging.critical('TLS must be enabled for client authentication.')
+ sys.exit(1)
+
+ if options.tls_client_cert_optional:
+ logging.critical('TLS must be enabled for client authentication.')
+ sys.exit(1)
+
+ if not options.scan_dir:
+ options.scan_dir = options.websock_handlers
+
+ if options.use_basic_auth:
+ options.basic_auth_credential = 'Basic ' + base64.b64encode(
+ options.basic_auth_credential.encode('UTF-8')).decode()
+
+ try:
+ if options.thread_monitor_interval_in_sec > 0:
+ # Run a thread monitor to show the status of server threads for
+ # debugging.
+ server_util.ThreadMonitor(
+ options.thread_monitor_interval_in_sec).start()
+
+ server = WebSocketServer(options)
+ server.serve_forever()
+ except Exception as e:
+ logging.critical('mod_pywebsocket: %s' % e)
+ logging.critical('mod_pywebsocket: %s' % traceback.format_exc())
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ _main(sys.argv[1:])
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/stream.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/stream.py
new file mode 100644
index 0000000000..82d1ea619c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/stream.py
@@ -0,0 +1,950 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""This file provides classes and helper functions for parsing/building frames
+of the WebSocket protocol (RFC 6455).
+
+Specification:
+http://tools.ietf.org/html/rfc6455
+"""
+
+from collections import deque
+import logging
+import os
+import struct
+import time
+import socket
+import six
+
+from mod_pywebsocket import common
+from mod_pywebsocket import util
+from mod_pywebsocket._stream_exceptions import BadOperationException
+from mod_pywebsocket._stream_exceptions import ConnectionTerminatedException
+from mod_pywebsocket._stream_exceptions import InvalidFrameException
+from mod_pywebsocket._stream_exceptions import InvalidUTF8Exception
+from mod_pywebsocket._stream_exceptions import UnsupportedFrameException
+
+_NOOP_MASKER = util.NoopMasker()
+
+
+class Frame(object):
+ def __init__(self,
+ fin=1,
+ rsv1=0,
+ rsv2=0,
+ rsv3=0,
+ opcode=None,
+ payload=b''):
+ self.fin = fin
+ self.rsv1 = rsv1
+ self.rsv2 = rsv2
+ self.rsv3 = rsv3
+ self.opcode = opcode
+ self.payload = payload
+
+
+# Helper functions made public to be used for writing unittests for WebSocket
+# clients.
+
+
+def create_length_header(length, mask):
+ """Creates a length header.
+
+ Args:
+ length: Frame length. Must be less than 2^63.
+ mask: Mask bit. Must be boolean.
+
+ Raises:
+ ValueError: when bad data is given.
+ """
+
+ if mask:
+ mask_bit = 1 << 7
+ else:
+ mask_bit = 0
+
+ if length < 0:
+ raise ValueError('length must be non negative integer')
+ elif length <= 125:
+ return util.pack_byte(mask_bit | length)
+ elif length < (1 << 16):
+ return util.pack_byte(mask_bit | 126) + struct.pack('!H', length)
+ elif length < (1 << 63):
+ return util.pack_byte(mask_bit | 127) + struct.pack('!Q', length)
+ else:
+ raise ValueError('Payload is too big for one frame')
+
+
+def create_header(opcode, payload_length, fin, rsv1, rsv2, rsv3, mask):
+ """Creates a frame header.
+
+ Raises:
+ Exception: when bad data is given.
+ """
+
+ if opcode < 0 or 0xf < opcode:
+ raise ValueError('Opcode out of range')
+
+ if payload_length < 0 or (1 << 63) <= payload_length:
+ raise ValueError('payload_length out of range')
+
+ if (fin | rsv1 | rsv2 | rsv3) & ~1:
+ raise ValueError('FIN bit and Reserved bit parameter must be 0 or 1')
+
+ header = b''
+
+ first_byte = ((fin << 7)
+ | (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4)
+ | opcode)
+ header += util.pack_byte(first_byte)
+ header += create_length_header(payload_length, mask)
+
+ return header
+
+
+def _build_frame(header, body, mask):
+ if not mask:
+ return header + body
+
+ masking_nonce = os.urandom(4)
+ masker = util.RepeatedXorMasker(masking_nonce)
+
+ return header + masking_nonce + masker.mask(body)
+
+
+def _filter_and_format_frame_object(frame, mask, frame_filters):
+ for frame_filter in frame_filters:
+ frame_filter.filter(frame)
+
+ header = create_header(frame.opcode, len(frame.payload), frame.fin,
+ frame.rsv1, frame.rsv2, frame.rsv3, mask)
+ return _build_frame(header, frame.payload, mask)
+
+
+def create_binary_frame(message,
+ opcode=common.OPCODE_BINARY,
+ fin=1,
+ mask=False,
+ frame_filters=[]):
+ """Creates a simple binary frame with no extension, reserved bit."""
+
+ frame = Frame(fin=fin, opcode=opcode, payload=message)
+ return _filter_and_format_frame_object(frame, mask, frame_filters)
+
+
+def create_text_frame(message,
+ opcode=common.OPCODE_TEXT,
+ fin=1,
+ mask=False,
+ frame_filters=[]):
+ """Creates a simple text frame with no extension, reserved bit."""
+
+ encoded_message = message.encode('utf-8')
+ return create_binary_frame(encoded_message, opcode, fin, mask,
+ frame_filters)
+
+
+def parse_frame(receive_bytes,
+ logger=None,
+ ws_version=common.VERSION_HYBI_LATEST,
+ unmask_receive=True):
+ """Parses a frame. Returns a tuple containing each header field and
+ payload.
+
+ Args:
+ receive_bytes: a function that reads frame data from a stream or
+ something similar. The function takes length of the bytes to be
+ read. The function must raise ConnectionTerminatedException if
+ there is not enough data to be read.
+ logger: a logging object.
+ ws_version: the version of WebSocket protocol.
+ unmask_receive: unmask received frames. When received unmasked
+ frame, raises InvalidFrameException.
+
+ Raises:
+ ConnectionTerminatedException: when receive_bytes raises it.
+ InvalidFrameException: when the frame contains invalid data.
+ """
+
+ if not logger:
+ logger = logging.getLogger()
+
+ logger.log(common.LOGLEVEL_FINE, 'Receive the first 2 octets of a frame')
+
+ first_byte = ord(receive_bytes(1))
+ fin = (first_byte >> 7) & 1
+ rsv1 = (first_byte >> 6) & 1
+ rsv2 = (first_byte >> 5) & 1
+ rsv3 = (first_byte >> 4) & 1
+ opcode = first_byte & 0xf
+
+ second_byte = ord(receive_bytes(1))
+ mask = (second_byte >> 7) & 1
+ payload_length = second_byte & 0x7f
+
+ logger.log(
+ common.LOGLEVEL_FINE, 'FIN=%s, RSV1=%s, RSV2=%s, RSV3=%s, opcode=%s, '
+ 'Mask=%s, Payload_length=%s', fin, rsv1, rsv2, rsv3, opcode, mask,
+ payload_length)
+
+ if (mask == 1) != unmask_receive:
+ raise InvalidFrameException(
+ 'Mask bit on the received frame did\'nt match masking '
+ 'configuration for received frames')
+
+ # The HyBi and later specs disallow putting a value in 0x0-0xFFFF
+ # into the 8-octet extended payload length field (or 0x0-0xFD in
+ # 2-octet field).
+ valid_length_encoding = True
+ length_encoding_bytes = 1
+ if payload_length == 127:
+ logger.log(common.LOGLEVEL_FINE,
+ 'Receive 8-octet extended payload length')
+
+ extended_payload_length = receive_bytes(8)
+ payload_length = struct.unpack('!Q', extended_payload_length)[0]
+ if payload_length > 0x7FFFFFFFFFFFFFFF:
+ raise InvalidFrameException('Extended payload length >= 2^63')
+ if ws_version >= 13 and payload_length < 0x10000:
+ valid_length_encoding = False
+ length_encoding_bytes = 8
+
+ logger.log(common.LOGLEVEL_FINE, 'Decoded_payload_length=%s',
+ payload_length)
+ elif payload_length == 126:
+ logger.log(common.LOGLEVEL_FINE,
+ 'Receive 2-octet extended payload length')
+
+ extended_payload_length = receive_bytes(2)
+ payload_length = struct.unpack('!H', extended_payload_length)[0]
+ if ws_version >= 13 and payload_length < 126:
+ valid_length_encoding = False
+ length_encoding_bytes = 2
+
+ logger.log(common.LOGLEVEL_FINE, 'Decoded_payload_length=%s',
+ payload_length)
+
+ if not valid_length_encoding:
+ logger.warning(
+ 'Payload length is not encoded using the minimal number of '
+ 'bytes (%d is encoded using %d bytes)', payload_length,
+ length_encoding_bytes)
+
+ if mask == 1:
+ logger.log(common.LOGLEVEL_FINE, 'Receive mask')
+
+ masking_nonce = receive_bytes(4)
+ masker = util.RepeatedXorMasker(masking_nonce)
+
+ logger.log(common.LOGLEVEL_FINE, 'Mask=%r', masking_nonce)
+ else:
+ masker = _NOOP_MASKER
+
+ logger.log(common.LOGLEVEL_FINE, 'Receive payload data')
+ if logger.isEnabledFor(common.LOGLEVEL_FINE):
+ receive_start = time.time()
+
+ raw_payload_bytes = receive_bytes(payload_length)
+
+ if logger.isEnabledFor(common.LOGLEVEL_FINE):
+ logger.log(
+ common.LOGLEVEL_FINE, 'Done receiving payload data at %s MB/s',
+ payload_length / (time.time() - receive_start) / 1000 / 1000)
+ logger.log(common.LOGLEVEL_FINE, 'Unmask payload data')
+
+ if logger.isEnabledFor(common.LOGLEVEL_FINE):
+ unmask_start = time.time()
+
+ unmasked_bytes = masker.mask(raw_payload_bytes)
+
+ if logger.isEnabledFor(common.LOGLEVEL_FINE):
+ logger.log(common.LOGLEVEL_FINE,
+ 'Done unmasking payload data at %s MB/s',
+ payload_length / (time.time() - unmask_start) / 1000 / 1000)
+
+ return opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3
+
+
+class FragmentedFrameBuilder(object):
+ """A stateful class to send a message as fragments."""
+ def __init__(self, mask, frame_filters=[], encode_utf8=True):
+ """Constructs an instance."""
+
+ self._mask = mask
+ self._frame_filters = frame_filters
+ # This is for skipping UTF-8 encoding when building text type frames
+ # from compressed data.
+ self._encode_utf8 = encode_utf8
+
+ self._started = False
+
+ # Hold opcode of the first frame in messages to verify types of other
+ # frames in the message are all the same.
+ self._opcode = common.OPCODE_TEXT
+
+ def build(self, payload_data, end, binary):
+ if binary:
+ frame_type = common.OPCODE_BINARY
+ else:
+ frame_type = common.OPCODE_TEXT
+ if self._started:
+ if self._opcode != frame_type:
+ raise ValueError('Message types are different in frames for '
+ 'the same message')
+ opcode = common.OPCODE_CONTINUATION
+ else:
+ opcode = frame_type
+ self._opcode = frame_type
+
+ if end:
+ self._started = False
+ fin = 1
+ else:
+ self._started = True
+ fin = 0
+
+ if binary or not self._encode_utf8:
+ return create_binary_frame(payload_data, opcode, fin, self._mask,
+ self._frame_filters)
+ else:
+ return create_text_frame(payload_data, opcode, fin, self._mask,
+ self._frame_filters)
+
+
+def _create_control_frame(opcode, body, mask, frame_filters):
+ frame = Frame(opcode=opcode, payload=body)
+
+ for frame_filter in frame_filters:
+ frame_filter.filter(frame)
+
+ if len(frame.payload) > 125:
+ raise BadOperationException(
+ 'Payload data size of control frames must be 125 bytes or less')
+
+ header = create_header(frame.opcode, len(frame.payload), frame.fin,
+ frame.rsv1, frame.rsv2, frame.rsv3, mask)
+ return _build_frame(header, frame.payload, mask)
+
+
+def create_ping_frame(body, mask=False, frame_filters=[]):
+ return _create_control_frame(common.OPCODE_PING, body, mask, frame_filters)
+
+
+def create_pong_frame(body, mask=False, frame_filters=[]):
+ return _create_control_frame(common.OPCODE_PONG, body, mask, frame_filters)
+
+
+def create_close_frame(body, mask=False, frame_filters=[]):
+ return _create_control_frame(common.OPCODE_CLOSE, body, mask,
+ frame_filters)
+
+
+def create_closing_handshake_body(code, reason):
+ body = b''
+ if code is not None:
+ if (code > common.STATUS_USER_PRIVATE_MAX
+ or code < common.STATUS_NORMAL_CLOSURE):
+ raise BadOperationException('Status code is out of range')
+ if (code == common.STATUS_NO_STATUS_RECEIVED
+ or code == common.STATUS_ABNORMAL_CLOSURE
+ or code == common.STATUS_TLS_HANDSHAKE):
+ raise BadOperationException('Status code is reserved pseudo '
+ 'code')
+ encoded_reason = reason.encode('utf-8')
+ body = struct.pack('!H', code) + encoded_reason
+ return body
+
+
+class StreamOptions(object):
+ """Holds option values to configure Stream objects."""
+ def __init__(self):
+ """Constructs StreamOptions."""
+
+ # Filters applied to frames.
+ self.outgoing_frame_filters = []
+ self.incoming_frame_filters = []
+
+ # Filters applied to messages. Control frames are not affected by them.
+ self.outgoing_message_filters = []
+ self.incoming_message_filters = []
+
+ self.encode_text_message_to_utf8 = True
+ self.mask_send = False
+ self.unmask_receive = True
+
+
+class Stream(object):
+ """A class for parsing/building frames of the WebSocket protocol
+ (RFC 6455).
+ """
+ def __init__(self, request, options):
+ """Constructs an instance.
+
+ Args:
+ request: mod_python request.
+ """
+
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+ self._request = request
+
+ self._request.client_terminated = False
+ self._request.server_terminated = False
+
+ # Holds body of received fragments.
+ self._received_fragments = []
+ # Holds the opcode of the first fragment.
+ self._original_opcode = None
+
+ self._writer = FragmentedFrameBuilder(
+ self._options.mask_send, self._options.outgoing_frame_filters,
+ self._options.encode_text_message_to_utf8)
+
+ self._ping_queue = deque()
+
+ def _read(self, length):
+ """Reads length bytes from connection. In case we catch any exception,
+ prepends remote address to the exception message and raise again.
+
+ Raises:
+ ConnectionTerminatedException: when read returns empty string.
+ """
+
+ try:
+ read_bytes = self._request.connection.read(length)
+ if not read_bytes:
+ raise ConnectionTerminatedException(
+ 'Receiving %d byte failed. Peer (%r) closed connection' %
+ (length, (self._request.connection.remote_addr, )))
+ return read_bytes
+ except IOError as e:
+ # Also catch an IOError because mod_python throws it.
+ raise ConnectionTerminatedException(
+ 'Receiving %d byte failed. IOError (%s) occurred' %
+ (length, e))
+
+ def _write(self, bytes_to_write):
+ """Writes given bytes to connection. In case we catch any exception,
+ prepends remote address to the exception message and raise again.
+ """
+
+ try:
+ self._request.connection.write(bytes_to_write)
+ except Exception as e:
+ util.prepend_message_to_exception(
+ 'Failed to send message to %r: ' %
+ (self._request.connection.remote_addr, ), e)
+ raise
+
+ def receive_bytes(self, length):
+ """Receives multiple bytes. Retries read when we couldn't receive the
+ specified amount. This method returns byte strings.
+
+ Raises:
+ ConnectionTerminatedException: when read returns empty string.
+ """
+
+ read_bytes = []
+ while length > 0:
+ new_read_bytes = self._read(length)
+ read_bytes.append(new_read_bytes)
+ length -= len(new_read_bytes)
+ return b''.join(read_bytes)
+
+ def _read_until(self, delim_char):
+ """Reads bytes until we encounter delim_char. The result will not
+ contain delim_char.
+
+ Raises:
+ ConnectionTerminatedException: when read returns empty string.
+ """
+
+ read_bytes = []
+ while True:
+ ch = self._read(1)
+ if ch == delim_char:
+ break
+ read_bytes.append(ch)
+ return b''.join(read_bytes)
+
+ def _receive_frame(self):
+ """Receives a frame and return data in the frame as a tuple containing
+ each header field and payload separately.
+
+ Raises:
+ ConnectionTerminatedException: when read returns empty
+ string.
+ InvalidFrameException: when the frame contains invalid data.
+ """
+ def _receive_bytes(length):
+ return self.receive_bytes(length)
+
+ return parse_frame(receive_bytes=_receive_bytes,
+ logger=self._logger,
+ ws_version=self._request.ws_version,
+ unmask_receive=self._options.unmask_receive)
+
+ def _receive_frame_as_frame_object(self):
+ opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame()
+
+ return Frame(fin=fin,
+ rsv1=rsv1,
+ rsv2=rsv2,
+ rsv3=rsv3,
+ opcode=opcode,
+ payload=unmasked_bytes)
+
+ def receive_filtered_frame(self):
+ """Receives a frame and applies frame filters and message filters.
+ The frame to be received must satisfy following conditions:
+ - The frame is not fragmented.
+ - The opcode of the frame is TEXT or BINARY.
+
+ DO NOT USE this method except for testing purpose.
+ """
+
+ frame = self._receive_frame_as_frame_object()
+ if not frame.fin:
+ raise InvalidFrameException(
+ 'Segmented frames must not be received via '
+ 'receive_filtered_frame()')
+ if (frame.opcode != common.OPCODE_TEXT
+ and frame.opcode != common.OPCODE_BINARY):
+ raise InvalidFrameException(
+ 'Control frames must not be received via '
+ 'receive_filtered_frame()')
+
+ for frame_filter in self._options.incoming_frame_filters:
+ frame_filter.filter(frame)
+ for message_filter in self._options.incoming_message_filters:
+ frame.payload = message_filter.filter(frame.payload)
+ return frame
+
+ def send_message(self, message, end=True, binary=False):
+ """Send message.
+
+ Args:
+ message: text in unicode or binary in str to send.
+ binary: send message as binary frame.
+
+ Raises:
+ BadOperationException: when called on a server-terminated
+ connection or called with inconsistent message type or
+ binary parameter.
+ """
+
+ if self._request.server_terminated:
+ raise BadOperationException(
+ 'Requested send_message after sending out a closing handshake')
+
+ if binary and isinstance(message, six.text_type):
+ raise BadOperationException(
+ 'Message for binary frame must not be instance of Unicode')
+
+ for message_filter in self._options.outgoing_message_filters:
+ message = message_filter.filter(message, end, binary)
+
+ try:
+ # Set this to any positive integer to limit maximum size of data in
+ # payload data of each frame.
+ MAX_PAYLOAD_DATA_SIZE = -1
+
+ if MAX_PAYLOAD_DATA_SIZE <= 0:
+ self._write(self._writer.build(message, end, binary))
+ return
+
+ bytes_written = 0
+ while True:
+ end_for_this_frame = end
+ bytes_to_write = len(message) - bytes_written
+ if (MAX_PAYLOAD_DATA_SIZE > 0
+ and bytes_to_write > MAX_PAYLOAD_DATA_SIZE):
+ end_for_this_frame = False
+ bytes_to_write = MAX_PAYLOAD_DATA_SIZE
+
+ frame = self._writer.build(
+ message[bytes_written:bytes_written + bytes_to_write],
+ end_for_this_frame, binary)
+ self._write(frame)
+
+ bytes_written += bytes_to_write
+
+ # This if must be placed here (the end of while block) so that
+ # at least one frame is sent.
+ if len(message) <= bytes_written:
+ break
+ except ValueError as e:
+ raise BadOperationException(e)
+
+ def _get_message_from_frame(self, frame):
+ """Gets a message from frame. If the message is composed of fragmented
+ frames and the frame is not the last fragmented frame, this method
+ returns None. The whole message will be returned when the last
+ fragmented frame is passed to this method.
+
+ Raises:
+ InvalidFrameException: when the frame doesn't match defragmentation
+ context, or the frame contains invalid data.
+ """
+
+ if frame.opcode == common.OPCODE_CONTINUATION:
+ if not self._received_fragments:
+ if frame.fin:
+ raise InvalidFrameException(
+ 'Received a termination frame but fragmentation '
+ 'not started')
+ else:
+ raise InvalidFrameException(
+ 'Received an intermediate frame but '
+ 'fragmentation not started')
+
+ if frame.fin:
+ # End of fragmentation frame
+ self._received_fragments.append(frame.payload)
+ message = b''.join(self._received_fragments)
+ self._received_fragments = []
+ return message
+ else:
+ # Intermediate frame
+ self._received_fragments.append(frame.payload)
+ return None
+ else:
+ if self._received_fragments:
+ if frame.fin:
+ raise InvalidFrameException(
+ 'Received an unfragmented frame without '
+ 'terminating existing fragmentation')
+ else:
+ raise InvalidFrameException(
+ 'New fragmentation started without terminating '
+ 'existing fragmentation')
+
+ if frame.fin:
+ # Unfragmented frame
+
+ self._original_opcode = frame.opcode
+ return frame.payload
+ else:
+ # Start of fragmentation frame
+
+ if common.is_control_opcode(frame.opcode):
+ raise InvalidFrameException(
+ 'Control frames must not be fragmented')
+
+ self._original_opcode = frame.opcode
+ self._received_fragments.append(frame.payload)
+ return None
+
+ def _process_close_message(self, message):
+ """Processes close message.
+
+ Args:
+ message: close message.
+
+ Raises:
+ InvalidFrameException: when the message is invalid.
+ """
+
+ self._request.client_terminated = True
+
+ # Status code is optional. We can have status reason only if we
+ # have status code. Status reason can be empty string. So,
+ # allowed cases are
+ # - no application data: no code no reason
+ # - 2 octet of application data: has code but no reason
+ # - 3 or more octet of application data: both code and reason
+ if len(message) == 0:
+ self._logger.debug('Received close frame (empty body)')
+ self._request.ws_close_code = common.STATUS_NO_STATUS_RECEIVED
+ elif len(message) == 1:
+ raise InvalidFrameException(
+ 'If a close frame has status code, the length of '
+ 'status code must be 2 octet')
+ elif len(message) >= 2:
+ self._request.ws_close_code = struct.unpack('!H', message[0:2])[0]
+ self._request.ws_close_reason = message[2:].decode(
+ 'utf-8', 'replace')
+ self._logger.debug('Received close frame (code=%d, reason=%r)',
+ self._request.ws_close_code,
+ self._request.ws_close_reason)
+
+ # As we've received a close frame, no more data is coming over the
+ # socket. We can now safely close the socket without worrying about
+ # RST sending.
+
+ if self._request.server_terminated:
+ self._logger.debug(
+ 'Received ack for server-initiated closing handshake')
+ return
+
+ self._logger.debug('Received client-initiated closing handshake')
+
+ code = common.STATUS_NORMAL_CLOSURE
+ reason = ''
+ if hasattr(self._request, '_dispatcher'):
+ dispatcher = self._request._dispatcher
+ code, reason = dispatcher.passive_closing_handshake(self._request)
+ if code is None and reason is not None and len(reason) > 0:
+ self._logger.warning(
+ 'Handler specified reason despite code being None')
+ reason = ''
+ if reason is None:
+ reason = ''
+ self._send_closing_handshake(code, reason)
+ self._logger.debug(
+ 'Acknowledged closing handshake initiated by the peer '
+ '(code=%r, reason=%r)', code, reason)
+
+ def _process_ping_message(self, message):
+ """Processes ping message.
+
+ Args:
+ message: ping message.
+ """
+
+ try:
+ handler = self._request.on_ping_handler
+ if handler:
+ handler(self._request, message)
+ return
+ except AttributeError:
+ pass
+ self._send_pong(message)
+
+ def _process_pong_message(self, message):
+ """Processes pong message.
+
+ Args:
+ message: pong message.
+ """
+
+ # TODO(tyoshino): Add ping timeout handling.
+
+ inflight_pings = deque()
+
+ while True:
+ try:
+ expected_body = self._ping_queue.popleft()
+ if expected_body == message:
+ # inflight_pings contains pings ignored by the
+ # other peer. Just forget them.
+ self._logger.debug(
+ 'Ping %r is acked (%d pings were ignored)',
+ expected_body, len(inflight_pings))
+ break
+ else:
+ inflight_pings.append(expected_body)
+ except IndexError:
+ # The received pong was unsolicited pong. Keep the
+ # ping queue as is.
+ self._ping_queue = inflight_pings
+ self._logger.debug('Received a unsolicited pong')
+ break
+
+ try:
+ handler = self._request.on_pong_handler
+ if handler:
+ handler(self._request, message)
+ except AttributeError:
+ pass
+
+ def receive_message(self):
+ """Receive a WebSocket frame and return its payload as a text in
+ unicode or a binary in str.
+
+ Returns:
+ payload data of the frame
+ - as unicode instance if received text frame
+ - as str instance if received binary frame
+ or None iff received closing handshake.
+ Raises:
+ BadOperationException: when called on a client-terminated
+ connection.
+ ConnectionTerminatedException: when read returns empty
+ string.
+ InvalidFrameException: when the frame contains invalid
+ data.
+ UnsupportedFrameException: when the received frame has
+ flags, opcode we cannot handle. You can ignore this
+ exception and continue receiving the next frame.
+ """
+
+ if self._request.client_terminated:
+ raise BadOperationException(
+ 'Requested receive_message after receiving a closing '
+ 'handshake')
+
+ while True:
+ # mp_conn.read will block if no bytes are available.
+
+ frame = self._receive_frame_as_frame_object()
+
+ # Check the constraint on the payload size for control frames
+ # before extension processes the frame.
+ # See also http://tools.ietf.org/html/rfc6455#section-5.5
+ if (common.is_control_opcode(frame.opcode)
+ and len(frame.payload) > 125):
+ raise InvalidFrameException(
+ 'Payload data size of control frames must be 125 bytes or '
+ 'less')
+
+ for frame_filter in self._options.incoming_frame_filters:
+ frame_filter.filter(frame)
+
+ if frame.rsv1 or frame.rsv2 or frame.rsv3:
+ raise UnsupportedFrameException(
+ 'Unsupported flag is set (rsv = %d%d%d)' %
+ (frame.rsv1, frame.rsv2, frame.rsv3))
+
+ message = self._get_message_from_frame(frame)
+ if message is None:
+ continue
+
+ for message_filter in self._options.incoming_message_filters:
+ message = message_filter.filter(message)
+
+ if self._original_opcode == common.OPCODE_TEXT:
+ # The WebSocket protocol section 4.4 specifies that invalid
+ # characters must be replaced with U+fffd REPLACEMENT
+ # CHARACTER.
+ try:
+ return message.decode('utf-8')
+ except UnicodeDecodeError as e:
+ raise InvalidUTF8Exception(e)
+ elif self._original_opcode == common.OPCODE_BINARY:
+ return message
+ elif self._original_opcode == common.OPCODE_CLOSE:
+ self._process_close_message(message)
+ return None
+ elif self._original_opcode == common.OPCODE_PING:
+ self._process_ping_message(message)
+ elif self._original_opcode == common.OPCODE_PONG:
+ self._process_pong_message(message)
+ else:
+ raise UnsupportedFrameException('Opcode %d is not supported' %
+ self._original_opcode)
+
+ def _send_closing_handshake(self, code, reason):
+ body = create_closing_handshake_body(code, reason)
+ frame = create_close_frame(
+ body,
+ mask=self._options.mask_send,
+ frame_filters=self._options.outgoing_frame_filters)
+
+ self._request.server_terminated = True
+
+ self._write(frame)
+
+ def close_connection(self,
+ code=common.STATUS_NORMAL_CLOSURE,
+ reason='',
+ wait_response=True):
+ """Closes a WebSocket connection. Note that this method blocks until
+ it receives acknowledgement to the closing handshake.
+
+ Args:
+ code: Status code for close frame. If code is None, a close
+ frame with empty body will be sent.
+ reason: string representing close reason.
+ wait_response: True when caller want to wait the response.
+ Raises:
+ BadOperationException: when reason is specified with code None
+ or reason is not an instance of both str and unicode.
+ """
+
+ if self._request.server_terminated:
+ self._logger.debug(
+ 'Requested close_connection but server is already terminated')
+ return
+
+ # When we receive a close frame, we call _process_close_message().
+ # _process_close_message() immediately acknowledges to the
+ # server-initiated closing handshake and sets server_terminated to
+ # True. So, here we can assume that we haven't received any close
+ # frame. We're initiating a closing handshake.
+
+ if code is None:
+ if reason is not None and len(reason) > 0:
+ raise BadOperationException(
+ 'close reason must not be specified if code is None')
+ reason = ''
+ else:
+ if not isinstance(reason, bytes) and not isinstance(
+ reason, six.text_type):
+ raise BadOperationException(
+ 'close reason must be an instance of bytes or unicode')
+
+ self._send_closing_handshake(code, reason)
+ self._logger.debug('Initiated closing handshake (code=%r, reason=%r)',
+ code, reason)
+
+ if (code == common.STATUS_GOING_AWAY
+ or code == common.STATUS_PROTOCOL_ERROR) or not wait_response:
+ # It doesn't make sense to wait for a close frame if the reason is
+ # protocol error or that the server is going away. For some of
+ # other reasons, it might not make sense to wait for a close frame,
+ # but it's not clear, yet.
+ return
+
+ # TODO(ukai): 2. wait until the /client terminated/ flag has been set,
+ # or until a server-defined timeout expires.
+ #
+ # For now, we expect receiving closing handshake right after sending
+ # out closing handshake.
+ message = self.receive_message()
+ if message is not None:
+ raise ConnectionTerminatedException(
+ 'Didn\'t receive valid ack for closing handshake')
+ # TODO: 3. close the WebSocket connection.
+ # note: mod_python Connection (mp_conn) doesn't have close method.
+
+ def send_ping(self, body, binary=False):
+ if not binary and isinstance(body, six.text_type):
+ body = body.encode('UTF-8')
+ frame = create_ping_frame(body, self._options.mask_send,
+ self._options.outgoing_frame_filters)
+ self._write(frame)
+
+ self._ping_queue.append(body)
+
+ def _send_pong(self, body):
+ frame = create_pong_frame(body, self._options.mask_send,
+ self._options.outgoing_frame_filters)
+ self._write(frame)
+
+ def get_last_received_opcode(self):
+ """Returns the opcode of the WebSocket message which the last received
+ frame belongs to. The return value is valid iff immediately after
+ receive_message call.
+ """
+
+ return self._original_opcode
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/util.py
new file mode 100644
index 0000000000..04006ecacd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/util.py
@@ -0,0 +1,386 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""WebSocket utilities."""
+
+from __future__ import absolute_import
+import array
+import errno
+import logging
+import os
+import re
+import six
+from six.moves import map
+from six.moves import range
+import socket
+import struct
+import zlib
+
+try:
+ from mod_pywebsocket import fast_masking
+except ImportError:
+ pass
+
+
+def prepend_message_to_exception(message, exc):
+ """Prepend message to the exception."""
+ exc.args = (message + str(exc), )
+ return
+
+
+def __translate_interp(interp, cygwin_path):
+ """Translate interp program path for Win32 python to run cygwin program
+ (e.g. perl). Note that it doesn't support path that contains space,
+ which is typically true for Unix, where #!-script is written.
+ For Win32 python, cygwin_path is a directory of cygwin binaries.
+
+ Args:
+ interp: interp command line
+ cygwin_path: directory name of cygwin binary, or None
+ Returns:
+ translated interp command line.
+ """
+ if not cygwin_path:
+ return interp
+ m = re.match('^[^ ]*/([^ ]+)( .*)?', interp)
+ if m:
+ cmd = os.path.join(cygwin_path, m.group(1))
+ return cmd + m.group(2)
+ return interp
+
+
+def get_script_interp(script_path, cygwin_path=None):
+ r"""Get #!-interpreter command line from the script.
+
+ It also fixes command path. When Cygwin Python is used, e.g. in WebKit,
+ it could run "/usr/bin/perl -wT hello.pl".
+ When Win32 Python is used, e.g. in Chromium, it couldn't. So, fix
+ "/usr/bin/perl" to "<cygwin_path>\perl.exe".
+
+ Args:
+ script_path: pathname of the script
+ cygwin_path: directory name of cygwin binary, or None
+ Returns:
+ #!-interpreter command line, or None if it is not #!-script.
+ """
+ fp = open(script_path)
+ line = fp.readline()
+ fp.close()
+ m = re.match('^#!(.*)', line)
+ if m:
+ return __translate_interp(m.group(1), cygwin_path)
+ return None
+
+
+def hexify(s):
+ return ' '.join(['%02x' % x for x in six.iterbytes(s)])
+
+
+def get_class_logger(o):
+ """Return the logging class information."""
+ return logging.getLogger('%s.%s' %
+ (o.__class__.__module__, o.__class__.__name__))
+
+
+def pack_byte(b):
+ """Pack an integer to network-ordered byte"""
+ return struct.pack('!B', b)
+
+
+class NoopMasker(object):
+ """A NoOp masking object.
+
+ This has the same interface as RepeatedXorMasker but just returns
+ the string passed in without making any change.
+ """
+ def __init__(self):
+ """NoOp."""
+ pass
+
+ def mask(self, s):
+ """NoOp."""
+ return s
+
+
+class RepeatedXorMasker(object):
+ """A masking object that applies XOR on the string.
+
+ Applies XOR on the byte string given to mask method with the masking bytes
+ given to the constructor repeatedly. This object remembers the position
+ in the masking bytes the last mask method call ended and resumes from
+ that point on the next mask method call.
+ """
+ def __init__(self, masking_key):
+ self._masking_key = masking_key
+ self._masking_key_index = 0
+
+ def _mask_using_swig(self, s):
+ """Perform the mask via SWIG."""
+ masked_data = fast_masking.mask(s, self._masking_key,
+ self._masking_key_index)
+ self._masking_key_index = ((self._masking_key_index + len(s)) %
+ len(self._masking_key))
+ return masked_data
+
+ def _mask_using_array(self, s):
+ """Perform the mask via python."""
+ if isinstance(s, six.text_type):
+ raise Exception(
+ 'Masking Operation should not process unicode strings')
+
+ result = bytearray(s)
+
+ # Use temporary local variables to eliminate the cost to access
+ # attributes
+ masking_key = [c for c in six.iterbytes(self._masking_key)]
+ masking_key_size = len(masking_key)
+ masking_key_index = self._masking_key_index
+
+ for i in range(len(result)):
+ result[i] ^= masking_key[masking_key_index]
+ masking_key_index = (masking_key_index + 1) % masking_key_size
+
+ self._masking_key_index = masking_key_index
+
+ return bytes(result)
+
+ if 'fast_masking' in globals():
+ mask = _mask_using_swig
+ else:
+ mask = _mask_using_array
+
+
+# By making wbits option negative, we can suppress CMF/FLG (2 octet) and
+# ADLER32 (4 octet) fields of zlib so that we can use zlib module just as
+# deflate library. DICTID won't be added as far as we don't set dictionary.
+# LZ77 window of 32K will be used for both compression and decompression.
+# For decompression, we can just use 32K to cover any windows size. For
+# compression, we use 32K so receivers must use 32K.
+#
+# Compression level is Z_DEFAULT_COMPRESSION. We don't have to match level
+# to decode.
+#
+# See zconf.h, deflate.cc, inflate.cc of zlib library, and zlibmodule.c of
+# Python. See also RFC1950 (ZLIB 3.3).
+
+
+class _Deflater(object):
+ def __init__(self, window_bits):
+ self._logger = get_class_logger(self)
+
+ # Using the smallest window bits of 9 for generating input frames.
+ # On WebSocket spec, the smallest window bit is 8. However, zlib does
+ # not accept window_bit = 8.
+ #
+ # Because of a zlib deflate quirk, back-references will not use the
+ # entire range of 1 << window_bits, but will instead use a restricted
+ # range of (1 << window_bits) - 262. With an increased window_bits = 9,
+ # back-references will be within a range of 250. These can still be
+ # decompressed with window_bits = 8 and the 256-byte window used there.
+ #
+ # Similar disscussions can be found in https://crbug.com/691074
+ window_bits = max(window_bits, 9)
+
+ self._compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
+ zlib.DEFLATED, -window_bits)
+
+ def compress(self, bytes):
+ compressed_bytes = self._compress.compress(bytes)
+ self._logger.debug('Compress input %r', bytes)
+ self._logger.debug('Compress result %r', compressed_bytes)
+ return compressed_bytes
+
+ def compress_and_flush(self, bytes):
+ compressed_bytes = self._compress.compress(bytes)
+ compressed_bytes += self._compress.flush(zlib.Z_SYNC_FLUSH)
+ self._logger.debug('Compress input %r', bytes)
+ self._logger.debug('Compress result %r', compressed_bytes)
+ return compressed_bytes
+
+ def compress_and_finish(self, bytes):
+ compressed_bytes = self._compress.compress(bytes)
+ compressed_bytes += self._compress.flush(zlib.Z_FINISH)
+ self._logger.debug('Compress input %r', bytes)
+ self._logger.debug('Compress result %r', compressed_bytes)
+ return compressed_bytes
+
+
+class _Inflater(object):
+ def __init__(self, window_bits):
+ self._logger = get_class_logger(self)
+ self._window_bits = window_bits
+
+ self._unconsumed = b''
+
+ self.reset()
+
+ def decompress(self, size):
+ if not (size == -1 or size > 0):
+ raise Exception('size must be -1 or positive')
+
+ data = b''
+
+ while True:
+ data += self._decompress.decompress(self._unconsumed,
+ max(0, size - len(data)))
+ self._unconsumed = self._decompress.unconsumed_tail
+ if self._decompress.unused_data:
+ # Encountered a last block (i.e. a block with BFINAL = 1) and
+ # found a new stream (unused_data). We cannot use the same
+ # zlib.Decompress object for the new stream. Create a new
+ # Decompress object to decompress the new one.
+ #
+ # It's fine to ignore unconsumed_tail if unused_data is not
+ # empty.
+ self._unconsumed = self._decompress.unused_data
+ self.reset()
+ if size >= 0 and len(data) == size:
+ # data is filled. Don't call decompress again.
+ break
+ else:
+ # Re-invoke Decompress.decompress to try to decompress all
+ # available bytes before invoking read which blocks until
+ # any new byte is available.
+ continue
+ else:
+ # Here, since unused_data is empty, even if unconsumed_tail is
+ # not empty, bytes of requested length are already in data. We
+ # don't have to "continue" here.
+ break
+
+ if data:
+ self._logger.debug('Decompressed %r', data)
+ return data
+
+ def append(self, data):
+ self._logger.debug('Appended %r', data)
+ self._unconsumed += data
+
+ def reset(self):
+ self._logger.debug('Reset')
+ self._decompress = zlib.decompressobj(-self._window_bits)
+
+
+# Compresses/decompresses given octets using the method introduced in RFC1979.
+
+
+class _RFC1979Deflater(object):
+ """A compressor class that applies DEFLATE to given byte sequence and
+ flushes using the algorithm described in the RFC1979 section 2.1.
+ """
+ def __init__(self, window_bits, no_context_takeover):
+ self._deflater = None
+ if window_bits is None:
+ window_bits = zlib.MAX_WBITS
+ self._window_bits = window_bits
+ self._no_context_takeover = no_context_takeover
+
+ def filter(self, bytes, end=True, bfinal=False):
+ if self._deflater is None:
+ self._deflater = _Deflater(self._window_bits)
+
+ if bfinal:
+ result = self._deflater.compress_and_finish(bytes)
+ # Add a padding block with BFINAL = 0 and BTYPE = 0.
+ result = result + pack_byte(0)
+ self._deflater = None
+ return result
+
+ result = self._deflater.compress_and_flush(bytes)
+ if end:
+ # Strip last 4 octets which is LEN and NLEN field of a
+ # non-compressed block added for Z_SYNC_FLUSH.
+ result = result[:-4]
+
+ if self._no_context_takeover and end:
+ self._deflater = None
+
+ return result
+
+
+class _RFC1979Inflater(object):
+ """A decompressor class a la RFC1979.
+
+ A decompressor class for byte sequence compressed and flushed following
+ the algorithm described in the RFC1979 section 2.1.
+ """
+ def __init__(self, window_bits=zlib.MAX_WBITS):
+ self._inflater = _Inflater(window_bits)
+
+ def filter(self, bytes):
+ # Restore stripped LEN and NLEN field of a non-compressed block added
+ # for Z_SYNC_FLUSH.
+ self._inflater.append(bytes + b'\x00\x00\xff\xff')
+ return self._inflater.decompress(-1)
+
+
+class DeflateSocket(object):
+ """A wrapper class for socket object to intercept send and recv to perform
+ deflate compression and decompression transparently.
+ """
+
+ # Size of the buffer passed to recv to receive compressed data.
+ _RECV_SIZE = 4096
+
+ def __init__(self, socket):
+ self._socket = socket
+
+ self._logger = get_class_logger(self)
+
+ self._deflater = _Deflater(zlib.MAX_WBITS)
+ self._inflater = _Inflater(zlib.MAX_WBITS)
+
+ def recv(self, size):
+ """Receives data from the socket specified on the construction up
+ to the specified size. Once any data is available, returns it even
+ if it's smaller than the specified size.
+ """
+
+ # TODO(tyoshino): Allow call with size=0. It should block until any
+ # decompressed data is available.
+ if size <= 0:
+ raise Exception('Non-positive size passed')
+ while True:
+ data = self._inflater.decompress(size)
+ if len(data) != 0:
+ return data
+
+ read_data = self._socket.recv(DeflateSocket._RECV_SIZE)
+ if not read_data:
+ return b''
+ self._inflater.append(read_data)
+
+ def sendall(self, bytes):
+ self.send(bytes)
+
+ def send(self, bytes):
+ self._socket.sendall(self._deflater.compress_and_flush(bytes))
+ return len(bytes)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/websocket_server.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/websocket_server.py
new file mode 100644
index 0000000000..fa24bb9651
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/websocket_server.py
@@ -0,0 +1,285 @@
+# Copyright 2020, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Standalone WebsocketServer
+
+This file deals with the main module of standalone server. Although it is fine
+to import this file directly to use WebSocketServer, it is strongly recommended
+to use standalone.py, since it is intended to act as a skeleton of this module.
+"""
+
+from __future__ import absolute_import
+from six.moves import BaseHTTPServer
+from six.moves import socketserver
+import logging
+import re
+import select
+import socket
+import ssl
+import threading
+import traceback
+
+from mod_pywebsocket import dispatch
+from mod_pywebsocket import util
+from mod_pywebsocket.request_handler import WebSocketRequestHandler
+
+
+def _alias_handlers(dispatcher, websock_handlers_map_file):
+ """Set aliases specified in websock_handler_map_file in dispatcher.
+
+ Args:
+ dispatcher: dispatch.Dispatcher instance
+ websock_handler_map_file: alias map file
+ """
+
+ with open(websock_handlers_map_file) as f:
+ for line in f:
+ if line[0] == '#' or line.isspace():
+ continue
+ m = re.match(r'(\S+)\s+(\S+)$', line)
+ if not m:
+ logging.warning('Wrong format in map file:' + line)
+ continue
+ try:
+ dispatcher.add_resource_path_alias(m.group(1), m.group(2))
+ except dispatch.DispatchException as e:
+ logging.error(str(e))
+
+
+class WebSocketServer(socketserver.ThreadingMixIn, BaseHTTPServer.HTTPServer):
+ """HTTPServer specialized for WebSocket."""
+
+ # Overrides SocketServer.ThreadingMixIn.daemon_threads
+ daemon_threads = True
+ # Overrides BaseHTTPServer.HTTPServer.allow_reuse_address
+ allow_reuse_address = True
+
+ def __init__(self, options):
+ """Override SocketServer.TCPServer.__init__ to set SSL enabled
+ socket object to self.socket before server_bind and server_activate,
+ if necessary.
+ """
+
+ # Share a Dispatcher among request handlers to save time for
+ # instantiation. Dispatcher can be shared because it is thread-safe.
+ options.dispatcher = dispatch.Dispatcher(
+ options.websock_handlers, options.scan_dir,
+ options.allow_handlers_outside_root_dir)
+ if options.websock_handlers_map_file:
+ _alias_handlers(options.dispatcher,
+ options.websock_handlers_map_file)
+ warnings = options.dispatcher.source_warnings()
+ if warnings:
+ for warning in warnings:
+ logging.warning('Warning in source loading: %s' % warning)
+
+ self._logger = util.get_class_logger(self)
+
+ self.request_queue_size = options.request_queue_size
+ self.__ws_is_shut_down = threading.Event()
+ self.__ws_serving = False
+
+ socketserver.BaseServer.__init__(self,
+ (options.server_host, options.port),
+ WebSocketRequestHandler)
+
+ # Expose the options object to allow handler objects access it. We name
+ # it with websocket_ prefix to avoid conflict.
+ self.websocket_server_options = options
+
+ self._create_sockets()
+ self.server_bind()
+ self.server_activate()
+
+ def _create_sockets(self):
+ self.server_name, self.server_port = self.server_address
+ self._sockets = []
+ if not self.server_name:
+ # On platforms that doesn't support IPv6, the first bind fails.
+ # On platforms that supports IPv6
+ # - If it binds both IPv4 and IPv6 on call with AF_INET6, the
+ # first bind succeeds and the second fails (we'll see 'Address
+ # already in use' error).
+ # - If it binds only IPv6 on call with AF_INET6, both call are
+ # expected to succeed to listen both protocol.
+ addrinfo_array = [(socket.AF_INET6, socket.SOCK_STREAM, '', '',
+ ''),
+ (socket.AF_INET, socket.SOCK_STREAM, '', '', '')]
+ else:
+ addrinfo_array = socket.getaddrinfo(self.server_name,
+ self.server_port,
+ socket.AF_UNSPEC,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP)
+ for addrinfo in addrinfo_array:
+ self._logger.info('Create socket on: %r', addrinfo)
+ family, socktype, proto, canonname, sockaddr = addrinfo
+ try:
+ socket_ = socket.socket(family, socktype)
+ except Exception as e:
+ self._logger.info('Skip by failure: %r', e)
+ continue
+ server_options = self.websocket_server_options
+ if server_options.use_tls:
+ if server_options.tls_client_auth:
+ if server_options.tls_client_cert_optional:
+ client_cert_ = ssl.CERT_OPTIONAL
+ else:
+ client_cert_ = ssl.CERT_REQUIRED
+ else:
+ client_cert_ = ssl.CERT_NONE
+ socket_ = ssl.wrap_socket(
+ socket_,
+ keyfile=server_options.private_key,
+ certfile=server_options.certificate,
+ ca_certs=server_options.tls_client_ca,
+ cert_reqs=client_cert_)
+ self._sockets.append((socket_, addrinfo))
+
+ def server_bind(self):
+ """Override SocketServer.TCPServer.server_bind to enable multiple
+ sockets bind.
+ """
+
+ failed_sockets = []
+
+ for socketinfo in self._sockets:
+ socket_, addrinfo = socketinfo
+ self._logger.info('Bind on: %r', addrinfo)
+ if self.allow_reuse_address:
+ socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ try:
+ socket_.bind(self.server_address)
+ except Exception as e:
+ self._logger.info('Skip by failure: %r', e)
+ socket_.close()
+ failed_sockets.append(socketinfo)
+ if self.server_address[1] == 0:
+ # The operating system assigns the actual port number for port
+ # number 0. This case, the second and later sockets should use
+ # the same port number. Also self.server_port is rewritten
+ # because it is exported, and will be used by external code.
+ self.server_address = (self.server_name,
+ socket_.getsockname()[1])
+ self.server_port = self.server_address[1]
+ self._logger.info('Port %r is assigned', self.server_port)
+
+ for socketinfo in failed_sockets:
+ self._sockets.remove(socketinfo)
+
+ def server_activate(self):
+ """Override SocketServer.TCPServer.server_activate to enable multiple
+ sockets listen.
+ """
+
+ failed_sockets = []
+
+ for socketinfo in self._sockets:
+ socket_, addrinfo = socketinfo
+ self._logger.info('Listen on: %r', addrinfo)
+ try:
+ socket_.listen(self.request_queue_size)
+ except Exception as e:
+ self._logger.info('Skip by failure: %r', e)
+ socket_.close()
+ failed_sockets.append(socketinfo)
+
+ for socketinfo in failed_sockets:
+ self._sockets.remove(socketinfo)
+
+ if len(self._sockets) == 0:
+ self._logger.critical(
+ 'No sockets activated. Use info log level to see the reason.')
+
+ def server_close(self):
+ """Override SocketServer.TCPServer.server_close to enable multiple
+ sockets close.
+ """
+
+ for socketinfo in self._sockets:
+ socket_, addrinfo = socketinfo
+ self._logger.info('Close on: %r', addrinfo)
+ socket_.close()
+
+ def fileno(self):
+ """Override SocketServer.TCPServer.fileno."""
+
+ self._logger.critical('Not supported: fileno')
+ return self._sockets[0][0].fileno()
+
+ def handle_error(self, request, client_address):
+ """Override SocketServer.handle_error."""
+
+ self._logger.error('Exception in processing request from: %r\n%s',
+ client_address, traceback.format_exc())
+ # Note: client_address is a tuple.
+
+ def get_request(self):
+ """Override TCPServer.get_request."""
+
+ accepted_socket, client_address = self.socket.accept()
+
+ server_options = self.websocket_server_options
+ if server_options.use_tls:
+ # Print cipher in use. Handshake is done on accept.
+ self._logger.debug('Cipher: %s', accepted_socket.cipher())
+ self._logger.debug('Client cert: %r',
+ accepted_socket.getpeercert())
+
+ return accepted_socket, client_address
+
+ def serve_forever(self, poll_interval=0.5):
+ """Override SocketServer.BaseServer.serve_forever."""
+
+ self.__ws_serving = True
+ self.__ws_is_shut_down.clear()
+ handle_request = self.handle_request
+ if hasattr(self, '_handle_request_noblock'):
+ handle_request = self._handle_request_noblock
+ else:
+ self._logger.warning('Fallback to blocking request handler')
+ try:
+ while self.__ws_serving:
+ r, w, e = select.select(
+ [socket_[0] for socket_ in self._sockets], [], [],
+ poll_interval)
+ for socket_ in r:
+ self.socket = socket_
+ handle_request()
+ self.socket = None
+ finally:
+ self.__ws_is_shut_down.set()
+
+ def shutdown(self):
+ """Override SocketServer.BaseServer.shutdown."""
+
+ self.__ws_serving = False
+ self.__ws_is_shut_down.wait()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py
new file mode 100755
index 0000000000..b65904c94f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Set up script for mod_pywebsocket.
+"""
+
+from __future__ import absolute_import
+from __future__ import print_function
+from setuptools import setup, Extension
+import sys
+
+_PACKAGE_NAME = 'mod_pywebsocket'
+
+# Build and use a C++ extension for faster masking. SWIG is required.
+_USE_FAST_MASKING = False
+
+# This is used since python_requires field is not recognized with
+# pip version 9.0.0 and earlier
+if sys.hexversion < 0x020700f0:
+ print('%s requires Python 2.7 or later.' % _PACKAGE_NAME, file=sys.stderr)
+ sys.exit(1)
+
+if _USE_FAST_MASKING:
+ setup(ext_modules=[
+ Extension('mod_pywebsocket/_fast_masking',
+ ['mod_pywebsocket/fast_masking.i'],
+ swig_opts=['-c++'])
+ ])
+
+setup(
+ author='Yuzo Fujishima',
+ author_email='yuzo@chromium.org',
+ description='Standalone WebSocket Server for testing purposes.',
+ long_description=('mod_pywebsocket is a standalone server for '
+ 'the WebSocket Protocol (RFC 6455). '
+ 'See mod_pywebsocket/__init__.py for more detail.'),
+ license='See LICENSE',
+ name=_PACKAGE_NAME,
+ packages=[_PACKAGE_NAME, _PACKAGE_NAME + '.handshake'],
+ python_requires='>=2.7',
+ install_requires=['six'],
+ url='https://github.com/GoogleChromeLabs/pywebsocket3',
+ version='3.0.1',
+)
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/__init__.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cacert.pem b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cacert.pem
new file mode 100644
index 0000000000..4dadae121b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cacert.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICvDCCAiWgAwIBAgIJAKqVghkGF1rSMA0GCSqGSIb3DQEBBQUAMEkxCzAJBgNV
+BAYTAkpQMQ4wDAYDVQQIEwVUb2t5bzEUMBIGA1UEChMLcHl3ZWJzb2NrZXQxFDAS
+BgNVBAMTC3B5d2Vic29ja2V0MB4XDTEyMDYwNjA3MjQzM1oXDTM5MTAyMzA3MjQz
+M1owSTELMAkGA1UEBhMCSlAxDjAMBgNVBAgTBVRva3lvMRQwEgYDVQQKEwtweXdl
+YnNvY2tldDEUMBIGA1UEAxMLcHl3ZWJzb2NrZXQwgZ8wDQYJKoZIhvcNAQEBBQAD
+gY0AMIGJAoGBAKoSEW2biQxVrMMKdn/8PJzDYiSXDPR9WQbLRRQ1Gm5jkCYiahXW
+u2CbTThfPPfi2NHA3I+HlT7gO9yR7RVUvN6ISUzGwXDEq4f4UNqtQOhQaqqK+CZ9
+LO/BhO/YYfNrbSPlYzHUKaT9ese7xO9VzVKLW+qUf2Mjh4/+SzxBDNP7AgMBAAGj
+gaswgagwHQYDVR0OBBYEFOsWdxCSuyhwaZeab6BoTho3++bzMHkGA1UdIwRyMHCA
+FOsWdxCSuyhwaZeab6BoTho3++bzoU2kSzBJMQswCQYDVQQGEwJKUDEOMAwGA1UE
+CBMFVG9reW8xFDASBgNVBAoTC3B5d2Vic29ja2V0MRQwEgYDVQQDEwtweXdlYnNv
+Y2tldIIJAKqVghkGF1rSMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEA
+gsMI1WEYqNw/jhUIdrTBcCxJ0X6hJvA9ziKANVm1Rs+4P3YDArkQ8bCr6xY+Kw7s
+Zp0yE7dM8GMdi+DU6hL3t3E5eMkTS1yZr9WCK4f2RLo+et98selZydpHemF3DJJ3
+gAj8Sx4LBaG8Cb/WnEMPv3MxG3fBE5favF6V4jU07hQ=
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cert.pem b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cert.pem
new file mode 100644
index 0000000000..25379a72b0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cert.pem
@@ -0,0 +1,61 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 1 (0x1)
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: C=JP, ST=Tokyo, O=pywebsocket, CN=pywebsocket
+ Validity
+ Not Before: Jun 6 07:25:08 2012 GMT
+ Not After : Oct 23 07:25:08 2039 GMT
+ Subject: C=JP, ST=Tokyo, O=pywebsocket, CN=pywebsocket
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public Key: (1024 bit)
+ Modulus (1024 bit):
+ 00:de:10:ce:3a:5a:04:a4:1c:29:93:5c:23:82:1a:
+ f2:06:01:e6:2b:a4:0f:dd:77:49:76:89:03:a2:21:
+ de:04:75:c6:e2:dd:fb:35:27:3a:a2:92:8e:12:62:
+ 2b:3e:1f:f4:78:df:b6:94:cb:27:d6:cb:d6:37:d7:
+ 5c:08:f0:09:3e:c9:ce:24:2d:00:c9:df:4a:e0:99:
+ e5:fb:23:a9:e2:d6:c9:3d:96:fa:01:88:de:5a:89:
+ b0:cf:03:67:6f:04:86:1d:ef:62:1c:55:a9:07:9a:
+ 2e:66:2a:73:5b:4c:62:03:f9:82:83:db:68:bf:b8:
+ 4b:0b:8b:93:11:b8:54:73:7b
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints:
+ CA:FALSE
+ Netscape Cert Type:
+ SSL Server
+ Netscape Comment:
+ OpenSSL Generated Certificate
+ X509v3 Subject Key Identifier:
+ 82:A1:73:8B:16:0C:7C:E4:D3:46:95:13:95:1A:32:C1:84:E9:06:00
+ X509v3 Authority Key Identifier:
+ keyid:EB:16:77:10:92:BB:28:70:69:97:9A:6F:A0:68:4E:1A:37:FB:E6:F3
+
+ Signature Algorithm: sha1WithRSAEncryption
+ 6b:b3:46:29:02:df:b0:c8:8e:c4:d7:7f:a0:1e:0d:1a:eb:2f:
+ df:d1:48:57:36:5f:95:8c:1b:f0:51:d6:52:e7:8d:84:3b:9f:
+ d8:ed:22:9c:aa:bd:ee:9b:90:1d:84:a3:4c:0b:cb:eb:64:73:
+ ba:f7:15:ce:da:5f:db:8b:15:07:a6:28:7f:b9:8c:11:9b:64:
+ d3:f1:be:52:4f:c3:d8:58:fe:de:56:63:63:3b:51:ed:a7:81:
+ f9:05:51:70:63:32:09:0e:94:7e:05:fe:a1:56:18:34:98:d5:
+ 99:1e:4e:27:38:89:90:6a:e5:ce:60:35:01:f5:de:34:60:b1:
+ cb:ae
+-----BEGIN CERTIFICATE-----
+MIICmDCCAgGgAwIBAgIBATANBgkqhkiG9w0BAQUFADBJMQswCQYDVQQGEwJKUDEO
+MAwGA1UECBMFVG9reW8xFDASBgNVBAoTC3B5d2Vic29ja2V0MRQwEgYDVQQDEwtw
+eXdlYnNvY2tldDAeFw0xMjA2MDYwNzI1MDhaFw0zOTEwMjMwNzI1MDhaMEkxCzAJ
+BgNVBAYTAkpQMQ4wDAYDVQQIEwVUb2t5bzEUMBIGA1UEChMLcHl3ZWJzb2NrZXQx
+FDASBgNVBAMTC3B5d2Vic29ja2V0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
+gQDeEM46WgSkHCmTXCOCGvIGAeYrpA/dd0l2iQOiId4Edcbi3fs1Jzqiko4SYis+
+H/R437aUyyfWy9Y311wI8Ak+yc4kLQDJ30rgmeX7I6ni1sk9lvoBiN5aibDPA2dv
+BIYd72IcVakHmi5mKnNbTGID+YKD22i/uEsLi5MRuFRzewIDAQABo4GPMIGMMAkG
+A1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQDAgZAMCwGCWCGSAGG+EIBDQQfFh1PcGVu
+U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUgqFzixYMfOTTRpUT
+lRoywYTpBgAwHwYDVR0jBBgwFoAU6xZ3EJK7KHBpl5pvoGhOGjf75vMwDQYJKoZI
+hvcNAQEFBQADgYEAa7NGKQLfsMiOxNd/oB4NGusv39FIVzZflYwb8FHWUueNhDuf
+2O0inKq97puQHYSjTAvL62RzuvcVztpf24sVB6Yof7mMEZtk0/G+Uk/D2Fj+3lZj
+YztR7aeB+QVRcGMyCQ6UfgX+oVYYNJjVmR5OJziJkGrlzmA1AfXeNGCxy64=
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/client_cert.p12 b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/client_cert.p12
new file mode 100644
index 0000000000..14e1399279
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/client_cert.p12
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/key.pem b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/key.pem
new file mode 100644
index 0000000000..fae858318f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/key.pem
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXgIBAAKBgQDeEM46WgSkHCmTXCOCGvIGAeYrpA/dd0l2iQOiId4Edcbi3fs1
+Jzqiko4SYis+H/R437aUyyfWy9Y311wI8Ak+yc4kLQDJ30rgmeX7I6ni1sk9lvoB
+iN5aibDPA2dvBIYd72IcVakHmi5mKnNbTGID+YKD22i/uEsLi5MRuFRzewIDAQAB
+AoGBAIuCuV1Vcnb7rm8CwtgZP5XgmY8vSjxTldafa6XvawEYUTP0S77v/1llg1Yv
+UIV+I+PQgG9oVoYOl22LoimHS/Z3e1fsot5tDYszGe8/Gkst4oaReSoxvBUa6WXp
+QSo7YFCajuHtE+W/gzF+UHbdzzXIDjQZ314LNF5t+4UnsEPBAkEA+girImqWoM2t
+3UR8f8oekERwsmEMf9DH5YpH4cvUnvI+kwesC/r2U8Sho++fyEMUNm7aIXGqNLga
+ogAM+4NX4QJBAONdSxSay22egTGNoIhLndljWkuOt/9FWj2klf/4QxD4blMJQ5Oq
+QdOGAh7nVQjpPLQ5D7CBVAKpGM2CD+QJBtsCQEP2kz35pxPylG3urcC2mfQxBkkW
+ZCViBNP58GwJ0bOauTOSBEwFXWuLqTw8aDwxL49UNmqc0N0fpe2fAehj3UECQQCm
+FH/DjU8Lw7ybddjNtm6XXPuYNagxz3cbkB4B3FchDleIUDwMoVF0MW9bI5/54mV1
+QDk1tUKortxvQZJaAD4BAkEAhGOHQqPd6bBBoFBvpaLzPJMxwLKrB+Wtkq/QlC72
+ClRiMn2g8SALiIL3BDgGXKcKE/Wy7jo/af/JCzQ/cPqt/A==
+-----END RSA PRIVATE KEY-----
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py
new file mode 100644
index 0000000000..a45e8f5cf2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py
@@ -0,0 +1,726 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""WebSocket client utility for testing.
+
+This module contains helper methods for performing handshake, frame
+sending/receiving as a WebSocket client.
+
+This is code for testing mod_pywebsocket. Keep this code independent from
+mod_pywebsocket. Don't import e.g. Stream class for generating frame for
+testing. Using util.hexify, etc. that are not related to protocol processing
+is allowed.
+
+Note:
+This code is far from robust, e.g., we cut corners in handshake.
+"""
+
+from __future__ import absolute_import
+import base64
+import errno
+import logging
+import os
+import random
+import re
+import socket
+import struct
+import time
+from hashlib import sha1
+from six import iterbytes
+from six import indexbytes
+
+from mod_pywebsocket import common
+from mod_pywebsocket import util
+from mod_pywebsocket.handshake import HandshakeException
+
+DEFAULT_PORT = 80
+DEFAULT_SECURE_PORT = 443
+
+# Opcodes introduced in IETF HyBi 01 for the new framing format
+OPCODE_CONTINUATION = 0x0
+OPCODE_CLOSE = 0x8
+OPCODE_PING = 0x9
+OPCODE_PONG = 0xa
+OPCODE_TEXT = 0x1
+OPCODE_BINARY = 0x2
+
+# Strings used for handshake
+_UPGRADE_HEADER = 'Upgrade: websocket\r\n'
+_CONNECTION_HEADER = 'Connection: Upgrade\r\n'
+
+WEBSOCKET_ACCEPT_UUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
+
+# Status codes
+STATUS_NORMAL_CLOSURE = 1000
+STATUS_GOING_AWAY = 1001
+STATUS_PROTOCOL_ERROR = 1002
+STATUS_UNSUPPORTED_DATA = 1003
+STATUS_NO_STATUS_RECEIVED = 1005
+STATUS_ABNORMAL_CLOSURE = 1006
+STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007
+STATUS_POLICY_VIOLATION = 1008
+STATUS_MESSAGE_TOO_BIG = 1009
+STATUS_MANDATORY_EXT = 1010
+STATUS_INTERNAL_ENDPOINT_ERROR = 1011
+STATUS_TLS_HANDSHAKE = 1015
+
+# Extension tokens
+_PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
+
+
+def _method_line(resource):
+ return 'GET %s HTTP/1.1\r\n' % resource
+
+
+def _sec_origin_header(origin):
+ return 'Sec-WebSocket-Origin: %s\r\n' % origin.lower()
+
+
+def _origin_header(origin):
+ # 4.1 13. concatenation of the string "Origin:", a U+0020 SPACE character,
+ # and the /origin/ value, converted to ASCII lowercase, to /fields/.
+ return 'Origin: %s\r\n' % origin.lower()
+
+
+def _format_host_header(host, port, secure):
+ # 4.1 9. Let /hostport/ be an empty string.
+ # 4.1 10. Append the /host/ value, converted to ASCII lowercase, to
+ # /hostport/
+ hostport = host.lower()
+ # 4.1 11. If /secure/ is false, and /port/ is not 80, or if /secure/
+ # is true, and /port/ is not 443, then append a U+003A COLON character
+ # (:) followed by the value of /port/, expressed as a base-ten integer,
+ # to /hostport/
+ if ((not secure and port != DEFAULT_PORT)
+ or (secure and port != DEFAULT_SECURE_PORT)):
+ hostport += ':' + str(port)
+ # 4.1 12. concatenation of the string "Host:", a U+0020 SPACE
+ # character, and /hostport/, to /fields/.
+ return 'Host: %s\r\n' % hostport
+
+
+# TODO(tyoshino): Define a base class and move these shared methods to that.
+
+
+def receive_bytes(socket, length):
+ received_bytes = []
+ remaining = length
+ while remaining > 0:
+ new_received_bytes = socket.recv(remaining)
+ if not new_received_bytes:
+ raise Exception(
+ 'Connection closed before receiving requested length '
+ '(requested %d bytes but received only %d bytes)' %
+ (length, length - remaining))
+ received_bytes.append(new_received_bytes)
+ remaining -= len(new_received_bytes)
+ return b''.join(received_bytes)
+
+
+# TODO(tyoshino): Now the WebSocketHandshake class diverts these methods. We
+# should move to HTTP parser as specified in RFC 6455.
+
+
+def _read_fields(socket):
+ # 4.1 32. let /fields/ be a list of name-value pairs, initially empty.
+ fields = {}
+ while True:
+ # 4.1 33. let /name/ and /value/ be empty byte arrays
+ name = b''
+ value = b''
+ # 4.1 34. read /name/
+ name = _read_name(socket)
+ if name is None:
+ break
+ # 4.1 35. read spaces
+ # TODO(tyoshino): Skip only one space as described in the spec.
+ ch = _skip_spaces(socket)
+ # 4.1 36. read /value/
+ value = _read_value(socket, ch)
+ # 4.1 37. read a byte from the server
+ ch = receive_bytes(socket, 1)
+ if ch != b'\n': # 0x0A
+ raise Exception(
+ 'Expected LF but found %r while reading value %r for header '
+ '%r' % (ch, name, value))
+ # 4.1 38. append an entry to the /fields/ list that has the name
+ # given by the string obtained by interpreting the /name/ byte
+ # array as a UTF-8 stream and the value given by the string
+ # obtained by interpreting the /value/ byte array as a UTF-8 byte
+ # stream.
+ fields.setdefault(name.decode('UTF-8'),
+ []).append(value.decode('UTF-8'))
+ # 4.1 39. return to the "Field" step above
+ return fields
+
+
+def _read_name(socket):
+ # 4.1 33. let /name/ be empty byte arrays
+ name = b''
+ while True:
+ # 4.1 34. read a byte from the server
+ ch = receive_bytes(socket, 1)
+ if ch == b'\r': # 0x0D
+ return None
+ elif ch == b'\n': # 0x0A
+ raise Exception('Unexpected LF when reading header name %r' % name)
+ elif ch == b':': # 0x3A
+ return name.lower()
+ else:
+ name += ch
+
+
+def _skip_spaces(socket):
+ # 4.1 35. read a byte from the server
+ while True:
+ ch = receive_bytes(socket, 1)
+ if ch == b' ': # 0x20
+ continue
+ return ch
+
+
+def _read_value(socket, ch):
+ # 4.1 33. let /value/ be empty byte arrays
+ value = b''
+ # 4.1 36. read a byte from server.
+ while True:
+ if ch == b'\r': # 0x0D
+ return value
+ elif ch == b'\n': # 0x0A
+ raise Exception('Unexpected LF when reading header value %r' %
+ value)
+ else:
+ value += ch
+ ch = receive_bytes(socket, 1)
+
+
+def read_frame_header(socket):
+
+ first_byte = ord(receive_bytes(socket, 1))
+ fin = (first_byte >> 7) & 1
+ rsv1 = (first_byte >> 6) & 1
+ rsv2 = (first_byte >> 5) & 1
+ rsv3 = (first_byte >> 4) & 1
+ opcode = first_byte & 0xf
+
+ second_byte = ord(receive_bytes(socket, 1))
+ mask = (second_byte >> 7) & 1
+ payload_length = second_byte & 0x7f
+
+ if mask != 0:
+ raise Exception('Mask bit must be 0 for frames coming from server')
+
+ if payload_length == 127:
+ extended_payload_length = receive_bytes(socket, 8)
+ payload_length = struct.unpack('!Q', extended_payload_length)[0]
+ if payload_length > 0x7FFFFFFFFFFFFFFF:
+ raise Exception('Extended payload length >= 2^63')
+ elif payload_length == 126:
+ extended_payload_length = receive_bytes(socket, 2)
+ payload_length = struct.unpack('!H', extended_payload_length)[0]
+
+ return fin, rsv1, rsv2, rsv3, opcode, payload_length
+
+
+class _TLSSocket(object):
+ """Wrapper for a TLS connection."""
+ def __init__(self, raw_socket):
+ self._ssl = socket.ssl(raw_socket)
+
+ def send(self, bytes):
+ return self._ssl.write(bytes)
+
+ def recv(self, size=-1):
+ return self._ssl.read(size)
+
+ def close(self):
+ # Nothing to do.
+ pass
+
+
+class HttpStatusException(Exception):
+ """This exception will be raised when unexpected http status code was
+ received as a result of handshake.
+ """
+ def __init__(self, name, status):
+ super(HttpStatusException, self).__init__(name)
+ self.status = status
+
+
+class WebSocketHandshake(object):
+ """Opening handshake processor for the WebSocket protocol (RFC 6455)."""
+ def __init__(self, options):
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+
+ def handshake(self, socket):
+ """Handshake WebSocket.
+
+ Raises:
+ Exception: handshake failed.
+ """
+
+ self._socket = socket
+
+ request_line = _method_line(self._options.resource)
+ self._logger.debug('Opening handshake Request-Line: %r', request_line)
+ self._socket.sendall(request_line.encode('UTF-8'))
+
+ fields = []
+ fields.append(_UPGRADE_HEADER)
+ fields.append(_CONNECTION_HEADER)
+
+ fields.append(
+ _format_host_header(self._options.server_host,
+ self._options.server_port,
+ self._options.use_tls))
+
+ if self._options.version == 8:
+ fields.append(_sec_origin_header(self._options.origin))
+ else:
+ fields.append(_origin_header(self._options.origin))
+
+ original_key = os.urandom(16)
+ key = base64.b64encode(original_key)
+ self._logger.debug('Sec-WebSocket-Key: %s (%s)', key,
+ util.hexify(original_key))
+ fields.append(u'Sec-WebSocket-Key: %s\r\n' % key.decode('UTF-8'))
+
+ fields.append(u'Sec-WebSocket-Version: %d\r\n' % self._options.version)
+
+ if self._options.use_basic_auth:
+ credential = 'Basic ' + base64.b64encode(
+ self._options.basic_auth_credential.encode('UTF-8')).decode()
+ fields.append(u'Authorization: %s\r\n' % credential)
+
+ # Setting up extensions.
+ if len(self._options.extensions) > 0:
+ fields.append(u'Sec-WebSocket-Extensions: %s\r\n' %
+ ', '.join(self._options.extensions))
+
+ self._logger.debug('Opening handshake request headers: %r', fields)
+
+ for field in fields:
+ self._socket.sendall(field.encode('UTF-8'))
+ self._socket.sendall(b'\r\n')
+
+ self._logger.info('Sent opening handshake request')
+
+ field = b''
+ while True:
+ ch = receive_bytes(self._socket, 1)
+ field += ch
+ if ch == b'\n':
+ break
+
+ self._logger.debug('Opening handshake Response-Line: %r', field)
+
+ # Will raise a UnicodeDecodeError when the decode fails
+ if len(field) < 7 or not field.endswith(b'\r\n'):
+ raise Exception('Wrong status line: %s' % field.decode('Latin-1'))
+ m = re.match(b'[^ ]* ([^ ]*) .*', field)
+ if m is None:
+ raise Exception('No HTTP status code found in status line: %s' %
+ field.decode('Latin-1'))
+ code = m.group(1)
+ if not re.match(b'[0-9][0-9][0-9]$', code):
+ raise Exception(
+ 'HTTP status code %s is not three digit in status line: %s' %
+ (code.decode('Latin-1'), field.decode('Latin-1')))
+ if code != b'101':
+ raise HttpStatusException(
+ 'Expected HTTP status code 101 but found %s in status line: '
+ '%r' % (code.decode('Latin-1'), field.decode('Latin-1')),
+ int(code))
+ fields = _read_fields(self._socket)
+ ch = receive_bytes(self._socket, 1)
+ if ch != b'\n': # 0x0A
+ raise Exception('Expected LF but found: %r' % ch)
+
+ self._logger.debug('Opening handshake response headers: %r', fields)
+
+ # Check /fields/
+ if len(fields['upgrade']) != 1:
+ raise Exception('Multiple Upgrade headers found: %s' %
+ fields['upgrade'])
+ if len(fields['connection']) != 1:
+ raise Exception('Multiple Connection headers found: %s' %
+ fields['connection'])
+ if fields['upgrade'][0] != 'websocket':
+ raise Exception('Unexpected Upgrade header value: %s' %
+ fields['upgrade'][0])
+ if fields['connection'][0].lower() != 'upgrade':
+ raise Exception('Unexpected Connection header value: %s' %
+ fields['connection'][0])
+
+ if len(fields['sec-websocket-accept']) != 1:
+ raise Exception('Multiple Sec-WebSocket-Accept headers found: %s' %
+ fields['sec-websocket-accept'])
+
+ accept = fields['sec-websocket-accept'][0]
+
+ # Validate
+ try:
+ decoded_accept = base64.b64decode(accept)
+ except TypeError as e:
+ raise HandshakeException(
+ 'Illegal value for header Sec-WebSocket-Accept: ' + accept)
+
+ if len(decoded_accept) != 20:
+ raise HandshakeException(
+ 'Decoded value of Sec-WebSocket-Accept is not 20-byte long')
+
+ self._logger.debug('Actual Sec-WebSocket-Accept: %r (%s)', accept,
+ util.hexify(decoded_accept))
+
+ original_expected_accept = sha1(key + WEBSOCKET_ACCEPT_UUID).digest()
+ expected_accept = base64.b64encode(original_expected_accept)
+
+ self._logger.debug('Expected Sec-WebSocket-Accept: %r (%s)',
+ expected_accept,
+ util.hexify(original_expected_accept))
+
+ if accept != expected_accept.decode('UTF-8'):
+ raise Exception(
+ 'Invalid Sec-WebSocket-Accept header: %r (expected) != %r '
+ '(actual)' % (accept, expected_accept))
+
+ server_extensions_header = fields.get('sec-websocket-extensions')
+ accepted_extensions = []
+ if server_extensions_header is not None:
+ accepted_extensions = common.parse_extensions(
+ ', '.join(server_extensions_header))
+
+ # Scan accepted extension list to check if there is any unrecognized
+ # extensions or extensions we didn't request in it. Then, for
+ # extensions we request, parse them and store parameters. They will be
+ # used later by each extension.
+ for extension in accepted_extensions:
+ if extension.name() == _PERMESSAGE_DEFLATE_EXTENSION:
+ checker = self._options.check_permessage_deflate
+ if checker:
+ checker(extension)
+ continue
+
+ raise Exception('Received unrecognized extension: %s' %
+ extension.name())
+
+
+class WebSocketStream(object):
+ """Frame processor for the WebSocket protocol (RFC 6455)."""
+ def __init__(self, socket, handshake):
+ self._handshake = handshake
+ self._socket = socket
+
+ # Filters applied to application data part of data frames.
+ self._outgoing_frame_filter = None
+ self._incoming_frame_filter = None
+
+ self._fragmented = False
+
+ def _mask_hybi(self, s):
+ # TODO(tyoshino): os.urandom does open/read/close for every call. If
+ # performance matters, change this to some library call that generates
+ # cryptographically secure pseudo random number sequence.
+ masking_nonce = os.urandom(4)
+ result = [masking_nonce]
+ count = 0
+ for c in iterbytes(s):
+ result.append(util.pack_byte(c ^ indexbytes(masking_nonce, count)))
+ count = (count + 1) % len(masking_nonce)
+ return b''.join(result)
+
+ def send_frame_of_arbitrary_bytes(self, header, body):
+ self._socket.sendall(header + self._mask_hybi(body))
+
+ def send_data(self,
+ payload,
+ frame_type,
+ end=True,
+ mask=True,
+ rsv1=0,
+ rsv2=0,
+ rsv3=0):
+ if self._outgoing_frame_filter is not None:
+ payload = self._outgoing_frame_filter.filter(payload)
+
+ if self._fragmented:
+ opcode = OPCODE_CONTINUATION
+ else:
+ opcode = frame_type
+
+ if end:
+ self._fragmented = False
+ fin = 1
+ else:
+ self._fragmented = True
+ fin = 0
+
+ if mask:
+ mask_bit = 1 << 7
+ else:
+ mask_bit = 0
+
+ header = util.pack_byte(fin << 7 | rsv1 << 6 | rsv2 << 5 | rsv3 << 4
+ | opcode)
+ payload_length = len(payload)
+ if payload_length <= 125:
+ header += util.pack_byte(mask_bit | payload_length)
+ elif payload_length < 1 << 16:
+ header += util.pack_byte(mask_bit | 126) + struct.pack(
+ '!H', payload_length)
+ elif payload_length < 1 << 63:
+ header += util.pack_byte(mask_bit | 127) + struct.pack(
+ '!Q', payload_length)
+ else:
+ raise Exception('Too long payload (%d byte)' % payload_length)
+ if mask:
+ payload = self._mask_hybi(payload)
+ self._socket.sendall(header + payload)
+
+ def send_binary(self, payload, end=True, mask=True):
+ self.send_data(payload, OPCODE_BINARY, end, mask)
+
+ def send_text(self, payload, end=True, mask=True):
+ self.send_data(payload.encode('utf-8'), OPCODE_TEXT, end, mask)
+
+ def _assert_receive_data(self, payload, opcode, fin, rsv1, rsv2, rsv3):
+ (actual_fin, actual_rsv1, actual_rsv2, actual_rsv3, actual_opcode,
+ payload_length) = read_frame_header(self._socket)
+
+ if actual_opcode != opcode:
+ raise Exception('Unexpected opcode: %d (expected) vs %d (actual)' %
+ (opcode, actual_opcode))
+
+ if actual_fin != fin:
+ raise Exception('Unexpected fin: %d (expected) vs %d (actual)' %
+ (fin, actual_fin))
+
+ if rsv1 is None:
+ rsv1 = 0
+
+ if rsv2 is None:
+ rsv2 = 0
+
+ if rsv3 is None:
+ rsv3 = 0
+
+ if actual_rsv1 != rsv1:
+ raise Exception('Unexpected rsv1: %r (expected) vs %r (actual)' %
+ (rsv1, actual_rsv1))
+
+ if actual_rsv2 != rsv2:
+ raise Exception('Unexpected rsv2: %r (expected) vs %r (actual)' %
+ (rsv2, actual_rsv2))
+
+ if actual_rsv3 != rsv3:
+ raise Exception('Unexpected rsv3: %r (expected) vs %r (actual)' %
+ (rsv3, actual_rsv3))
+
+ received = receive_bytes(self._socket, payload_length)
+
+ if self._incoming_frame_filter is not None:
+ received = self._incoming_frame_filter.filter(received)
+
+ if len(received) != len(payload):
+ raise Exception(
+ 'Unexpected payload length: %d (expected) vs %d (actual)' %
+ (len(payload), len(received)))
+
+ if payload != received:
+ raise Exception(
+ 'Unexpected payload: %r (expected) vs %r (actual)' %
+ (payload, received))
+
+ def assert_receive_binary(self,
+ payload,
+ opcode=OPCODE_BINARY,
+ fin=1,
+ rsv1=None,
+ rsv2=None,
+ rsv3=None):
+ self._assert_receive_data(payload, opcode, fin, rsv1, rsv2, rsv3)
+
+ def assert_receive_text(self,
+ payload,
+ opcode=OPCODE_TEXT,
+ fin=1,
+ rsv1=None,
+ rsv2=None,
+ rsv3=None):
+ self._assert_receive_data(payload.encode('utf-8'), opcode, fin, rsv1,
+ rsv2, rsv3)
+
+ def _build_close_frame(self, code, reason, mask):
+ frame = util.pack_byte(1 << 7 | OPCODE_CLOSE)
+
+ if code is not None:
+ body = struct.pack('!H', code) + reason.encode('utf-8')
+ else:
+ body = b''
+ if mask:
+ frame += util.pack_byte(1 << 7 | len(body)) + self._mask_hybi(body)
+ else:
+ frame += util.pack_byte(len(body)) + body
+ return frame
+
+ def send_close(self, code, reason):
+ self._socket.sendall(self._build_close_frame(code, reason, True))
+
+ def assert_receive_close(self, code, reason):
+ expected_frame = self._build_close_frame(code, reason, False)
+ actual_frame = receive_bytes(self._socket, len(expected_frame))
+ if actual_frame != expected_frame:
+ raise Exception(
+ 'Unexpected close frame: %r (expected) vs %r (actual)' %
+ (expected_frame, actual_frame))
+
+
+class ClientOptions(object):
+ """Holds option values to configure the Client object."""
+ def __init__(self):
+ self.version = 13
+ self.server_host = ''
+ self.origin = ''
+ self.resource = ''
+ self.server_port = -1
+ self.socket_timeout = 1000
+ self.use_tls = False
+ self.use_basic_auth = False
+ self.basic_auth_credential = 'test:test'
+ self.extensions = []
+
+
+def connect_socket_with_retry(host,
+ port,
+ timeout,
+ use_tls,
+ retry=10,
+ sleep_sec=0.1):
+ retry_count = 0
+ while retry_count < retry:
+ try:
+ s = socket.socket()
+ s.settimeout(timeout)
+ s.connect((host, port))
+ if use_tls:
+ return _TLSSocket(s)
+ return s
+ except socket.error as e:
+ if e.errno != errno.ECONNREFUSED:
+ raise
+ else:
+ retry_count = retry_count + 1
+ time.sleep(sleep_sec)
+
+ return None
+
+
+class Client(object):
+ """WebSocket client."""
+ def __init__(self, options, handshake, stream_class):
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+ self._socket = None
+
+ self._handshake = handshake
+ self._stream_class = stream_class
+
+ def connect(self):
+ self._socket = connect_socket_with_retry(self._options.server_host,
+ self._options.server_port,
+ self._options.socket_timeout,
+ self._options.use_tls)
+
+ self._handshake.handshake(self._socket)
+
+ self._stream = self._stream_class(self._socket, self._handshake)
+
+ self._logger.info('Connection established')
+
+ def send_frame_of_arbitrary_bytes(self, header, body):
+ self._stream.send_frame_of_arbitrary_bytes(header, body)
+
+ def send_message(self,
+ message,
+ end=True,
+ binary=False,
+ raw=False,
+ mask=True):
+ if binary:
+ self._stream.send_binary(message, end, mask)
+ elif raw:
+ self._stream.send_data(message, OPCODE_TEXT, end, mask)
+ else:
+ self._stream.send_text(message, end, mask)
+
+ def assert_receive(self, payload, binary=False):
+ if binary:
+ self._stream.assert_receive_binary(payload)
+ else:
+ self._stream.assert_receive_text(payload)
+
+ def send_close(self, code=STATUS_NORMAL_CLOSURE, reason=''):
+ self._stream.send_close(code, reason)
+
+ def assert_receive_close(self, code=STATUS_NORMAL_CLOSURE, reason=''):
+ self._stream.assert_receive_close(code, reason)
+
+ def close_socket(self):
+ self._socket.close()
+
+ def assert_connection_closed(self):
+ try:
+ read_data = receive_bytes(self._socket, 1)
+ except Exception as e:
+ if str(e).find(
+ 'Connection closed before receiving requested length '
+ ) == 0:
+ return
+ try:
+ error_number, message = e
+ for error_name in ['ECONNRESET', 'WSAECONNRESET']:
+ if (error_name in dir(errno)
+ and error_number == getattr(errno, error_name)):
+ return
+ except:
+ raise e
+ raise e
+
+ raise Exception('Connection is not closed (Read: %r)' % read_data)
+
+
+def create_client(options):
+ return Client(options, WebSocketHandshake(options), WebSocketStream)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py
new file mode 100644
index 0000000000..eeaef52ecf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py
@@ -0,0 +1,227 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Mocks for testing.
+"""
+
+from __future__ import absolute_import
+import six.moves.queue
+import threading
+import struct
+import six
+
+from mod_pywebsocket import common
+from mod_pywebsocket import util
+from mod_pywebsocket.stream import Stream
+from mod_pywebsocket.stream import StreamOptions
+from six.moves import range
+
+
+class _MockConnBase(object):
+ """Base class of mocks for mod_python.apache.mp_conn.
+
+ This enables tests to check what is written to a (mock) mp_conn.
+ """
+ def __init__(self):
+ self._write_data = []
+ self.remote_addr = b'fake_address'
+
+ def write(self, data):
+ """Override mod_python.apache.mp_conn.write.
+
+ data should be bytes when touching this method manually.
+ """
+
+ self._write_data.append(data)
+
+ def written_data(self):
+ """Get bytes written to this mock."""
+
+ return b''.join(self._write_data)
+
+
+class MockConn(_MockConnBase):
+ """Mock for mod_python.apache.mp_conn.
+
+ This enables tests to specify what should be read from a (mock) mp_conn as
+ well as to check what is written to it.
+ """
+ def __init__(self, read_data):
+ """Constructs an instance.
+
+ Args:
+ read_data: bytes that should be returned when read* methods are
+ called.
+ """
+
+ _MockConnBase.__init__(self)
+ self._read_data = read_data
+ self._read_pos = 0
+
+ def readline(self):
+ """Override mod_python.apache.mp_conn.readline."""
+
+ if self._read_pos >= len(self._read_data):
+ return b''
+ end_index = self._read_data.find(b'\n', self._read_pos) + 1
+ if not end_index:
+ end_index = len(self._read_data)
+ return self._read_up_to(end_index)
+
+ def read(self, length):
+ """Override mod_python.apache.mp_conn.read."""
+
+ if self._read_pos >= len(self._read_data):
+ return b''
+ end_index = min(len(self._read_data), self._read_pos + length)
+ return self._read_up_to(end_index)
+
+ def _read_up_to(self, end_index):
+ line = self._read_data[self._read_pos:end_index]
+ self._read_pos = end_index
+ return line
+
+
+class MockBlockingConn(_MockConnBase):
+ """Blocking mock for mod_python.apache.mp_conn.
+
+ This enables tests to specify what should be read from a (mock) mp_conn as
+ well as to check what is written to it.
+ Callers of read* methods will block if there is no bytes available.
+ """
+ def __init__(self):
+ _MockConnBase.__init__(self)
+ self._queue = six.moves.queue.Queue()
+
+ def readline(self):
+ """Override mod_python.apache.mp_conn.readline."""
+ line = bytearray()
+ while True:
+ c = self._queue.get()
+ line.append(c)
+ if c == ord(b'\n'):
+ return bytes(line)
+
+ def read(self, length):
+ """Override mod_python.apache.mp_conn.read."""
+
+ data = bytearray()
+ for unused in range(length):
+ data.append(self._queue.get())
+
+ return bytes(data)
+
+ def put_bytes(self, bytes):
+ """Put bytes to be read from this mock.
+
+ Args:
+ bytes: bytes to be read.
+ """
+
+ for byte in six.iterbytes(bytes):
+ self._queue.put(byte)
+
+
+class MockTable(dict):
+ """Mock table.
+
+ This mimics mod_python mp_table. Note that only the methods used by
+ tests are overridden.
+ """
+ def __init__(self, copy_from={}):
+ if isinstance(copy_from, dict):
+ copy_from = list(copy_from.items())
+ for key, value in copy_from:
+ self.__setitem__(key, value)
+
+ def __getitem__(self, key):
+ return super(MockTable, self).__getitem__(key.lower())
+
+ def __setitem__(self, key, value):
+ super(MockTable, self).__setitem__(key.lower(), value)
+
+ def get(self, key, def_value=None):
+ return super(MockTable, self).get(key.lower(), def_value)
+
+
+class MockRequest(object):
+ """Mock request.
+
+ This mimics mod_python request.
+ """
+ def __init__(self,
+ uri=None,
+ headers_in={},
+ connection=None,
+ method='GET',
+ protocol='HTTP/1.1',
+ is_https=False):
+ """Construct an instance.
+
+ Arguments:
+ uri: URI of the request.
+ headers_in: Request headers.
+ connection: Connection used for the request.
+ method: request method.
+ is_https: Whether this request is over SSL.
+
+ See the document of mod_python Request for details.
+ """
+ self.uri = uri
+ self.unparsed_uri = uri
+ self.connection = connection
+ self.method = method
+ self.protocol = protocol
+ self.headers_in = MockTable(headers_in)
+ # self.is_https_ needs to be accessible from tests. To avoid name
+ # conflict with self.is_https(), it is named as such.
+ self.is_https_ = is_https
+ self.ws_stream = Stream(self, StreamOptions())
+ self.ws_close_code = None
+ self.ws_close_reason = None
+ self.ws_version = common.VERSION_HYBI_LATEST
+ self.ws_deflate = False
+
+ def is_https(self):
+ """Return whether this request is over SSL."""
+ return self.is_https_
+
+
+class MockDispatcher(object):
+ """Mock for dispatch.Dispatcher."""
+ def __init__(self):
+ self.do_extra_handshake_called = False
+
+ def do_extra_handshake(self, conn_context):
+ self.do_extra_handshake_called = True
+
+ def transfer_data(self, conn_context):
+ pass
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py
new file mode 100755
index 0000000000..ea52223cea
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Run all tests in the same directory.
+
+This suite is expected to be run under pywebsocket's src directory, i.e. the
+directory containing mod_pywebsocket, test, etc.
+
+To change loggin level, please specify --log-level option.
+ python test/run_test.py --log-level debug
+
+To pass any option to unittest module, please specify options after '--'. For
+example, run this for making the test runner verbose.
+ python test/run_test.py --log-level debug -- -v
+"""
+
+from __future__ import absolute_import
+import logging
+import argparse
+import os
+import re
+import six
+import sys
+import unittest
+
+_TEST_MODULE_PATTERN = re.compile(r'^(test_.+)\.py$')
+
+
+def _list_test_modules(directory):
+ module_names = []
+ for filename in os.listdir(directory):
+ match = _TEST_MODULE_PATTERN.search(filename)
+ if match:
+ module_names.append(match.group(1))
+ return module_names
+
+
+def _suite():
+ loader = unittest.TestLoader()
+ return loader.loadTestsFromNames(
+ _list_test_modules(os.path.dirname(__file__)))
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '--log-level',
+ '--log_level',
+ type=six.text_type,
+ dest='log_level',
+ default='warning',
+ choices=['debug', 'info', 'warning', 'warn', 'error', 'critical'])
+ options, args = parser.parse_known_args()
+ logging.basicConfig(level=logging.getLevelName(options.log_level.upper()),
+ format='%(levelname)s %(asctime)s '
+ '%(filename)s:%(lineno)d] '
+ '%(message)s',
+ datefmt='%H:%M:%S')
+ unittest.main(defaultTest='_suite', argv=[sys.argv[0]] + args)
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py
new file mode 100644
index 0000000000..48d0e116a5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py
@@ -0,0 +1,41 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Configuration for testing.
+
+Test files should import this module before mod_pywebsocket.
+"""
+
+from __future__ import absolute_import
+import os
+import sys
+
+# Add the parent directory to sys.path to enable importing mod_pywebsocket.
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py
new file mode 100755
index 0000000000..132dd92d76
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py
@@ -0,0 +1,298 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Tests for dispatch module."""
+
+from __future__ import absolute_import
+import os
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import dispatch
+from mod_pywebsocket import handshake
+from test import mock
+from six.moves import zip
+
+_TEST_HANDLERS_DIR = os.path.join(os.path.dirname(__file__), 'testdata',
+ 'handlers')
+
+_TEST_HANDLERS_SUB_DIR = os.path.join(_TEST_HANDLERS_DIR, 'sub')
+
+
+class DispatcherTest(unittest.TestCase):
+ """A unittest for dispatch module."""
+ def test_normalize_path(self):
+ self.assertEqual(
+ os.path.abspath('/a/b').replace('\\', '/'),
+ dispatch._normalize_path('/a/b'))
+ self.assertEqual(
+ os.path.abspath('/a/b').replace('\\', '/'),
+ dispatch._normalize_path('\\a\\b'))
+ self.assertEqual(
+ os.path.abspath('/a/b').replace('\\', '/'),
+ dispatch._normalize_path('/a/c/../b'))
+ self.assertEqual(
+ os.path.abspath('abc').replace('\\', '/'),
+ dispatch._normalize_path('abc'))
+
+ def test_converter(self):
+ converter = dispatch._create_path_to_resource_converter('/a/b')
+ # Python built by MSC inserts a drive name like 'C:\' via realpath().
+ # Converter Generator expands provided path using realpath() and uses
+ # the path including a drive name to verify the prefix.
+ os_root = os.path.realpath('/')
+ self.assertEqual('/h', converter(os_root + 'a/b/h_wsh.py'))
+ self.assertEqual('/c/h', converter(os_root + 'a/b/c/h_wsh.py'))
+ self.assertEqual(None, converter(os_root + 'a/b/h.py'))
+ self.assertEqual(None, converter('a/b/h_wsh.py'))
+
+ converter = dispatch._create_path_to_resource_converter('a/b')
+ self.assertEqual('/h',
+ converter(dispatch._normalize_path('a/b/h_wsh.py')))
+
+ converter = dispatch._create_path_to_resource_converter('/a/b///')
+ self.assertEqual('/h', converter(os_root + 'a/b/h_wsh.py'))
+ self.assertEqual(
+ '/h', converter(dispatch._normalize_path('/a/b/../b/h_wsh.py')))
+
+ converter = dispatch._create_path_to_resource_converter(
+ '/a/../a/b/../b/')
+ self.assertEqual('/h', converter(os_root + 'a/b/h_wsh.py'))
+
+ converter = dispatch._create_path_to_resource_converter(r'\a\b')
+ self.assertEqual('/h', converter(os_root + r'a\b\h_wsh.py'))
+ self.assertEqual('/h', converter(os_root + r'a/b/h_wsh.py'))
+
+ def test_enumerate_handler_file_paths(self):
+ paths = list(
+ dispatch._enumerate_handler_file_paths(_TEST_HANDLERS_DIR))
+ paths.sort()
+ self.assertEqual(8, len(paths))
+ expected_paths = [
+ os.path.join(_TEST_HANDLERS_DIR, 'abort_by_user_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'blank_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'origin_check_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub',
+ 'exception_in_transfer_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub', 'non_callable_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub', 'plain_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub',
+ 'wrong_handshake_sig_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub',
+ 'wrong_transfer_sig_wsh.py'),
+ ]
+ for expected, actual in zip(expected_paths, paths):
+ self.assertEqual(expected, actual)
+
+ def test_source_handler_file(self):
+ self.assertRaises(dispatch.DispatchException,
+ dispatch._source_handler_file, '')
+ self.assertRaises(dispatch.DispatchException,
+ dispatch._source_handler_file, 'def')
+ self.assertRaises(dispatch.DispatchException,
+ dispatch._source_handler_file, '1/0')
+ self.assertTrue(
+ dispatch._source_handler_file(
+ 'def web_socket_do_extra_handshake(request):pass\n'
+ 'def web_socket_transfer_data(request):pass\n'))
+
+ def test_source_warnings(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ warnings = dispatcher.source_warnings()
+ warnings.sort()
+ expected_warnings = [
+ (os.path.realpath(os.path.join(_TEST_HANDLERS_DIR, 'blank_wsh.py'))
+ + ': web_socket_do_extra_handshake is not defined.'),
+ (os.path.realpath(
+ os.path.join(_TEST_HANDLERS_DIR, 'sub', 'non_callable_wsh.py'))
+ + ': web_socket_do_extra_handshake is not callable.'),
+ (os.path.realpath(
+ os.path.join(_TEST_HANDLERS_DIR, 'sub',
+ 'wrong_handshake_sig_wsh.py')) +
+ ': web_socket_do_extra_handshake is not defined.'),
+ (os.path.realpath(
+ os.path.join(_TEST_HANDLERS_DIR, 'sub',
+ 'wrong_transfer_sig_wsh.py')) +
+ ': web_socket_transfer_data is not defined.'),
+ ]
+ self.assertEqual(4, len(warnings))
+ for expected, actual in zip(expected_warnings, warnings):
+ self.assertEqual(expected, actual)
+
+ def test_do_extra_handshake(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ request = mock.MockRequest()
+ request.ws_resource = '/origin_check'
+ request.ws_origin = 'http://example.com'
+ dispatcher.do_extra_handshake(request) # Must not raise exception.
+
+ request.ws_origin = 'http://bad.example.com'
+ try:
+ dispatcher.do_extra_handshake(request)
+ self.fail('Could not catch HandshakeException with 403 status')
+ except handshake.HandshakeException as e:
+ self.assertEqual(403, e.status)
+ except Exception as e:
+ self.fail('Unexpected exception: %r' % e)
+
+ def test_abort_extra_handshake(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ request = mock.MockRequest()
+ request.ws_resource = '/abort_by_user'
+ self.assertRaises(handshake.AbortedByUserException,
+ dispatcher.do_extra_handshake, request)
+
+ def test_transfer_data(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+
+ request = mock.MockRequest(
+ connection=mock.MockConn(b'\x88\x02\x03\xe8'))
+ request.ws_resource = '/origin_check'
+ request.ws_protocol = 'p1'
+ dispatcher.transfer_data(request)
+ self.assertEqual(
+ b'origin_check_wsh.py is called for /origin_check, p1'
+ b'\x88\x02\x03\xe8', request.connection.written_data())
+
+ request = mock.MockRequest(
+ connection=mock.MockConn(b'\x88\x02\x03\xe8'))
+ request.ws_resource = '/sub/plain'
+ request.ws_protocol = None
+ dispatcher.transfer_data(request)
+ self.assertEqual(
+ b'sub/plain_wsh.py is called for /sub/plain, None'
+ b'\x88\x02\x03\xe8', request.connection.written_data())
+
+ request = mock.MockRequest(
+ connection=mock.MockConn(b'\x88\x02\x03\xe8'))
+ request.ws_resource = '/sub/plain?'
+ request.ws_protocol = None
+ dispatcher.transfer_data(request)
+ self.assertEqual(
+ b'sub/plain_wsh.py is called for /sub/plain?, None'
+ b'\x88\x02\x03\xe8', request.connection.written_data())
+
+ request = mock.MockRequest(
+ connection=mock.MockConn(b'\x88\x02\x03\xe8'))
+ request.ws_resource = '/sub/plain?q=v'
+ request.ws_protocol = None
+ dispatcher.transfer_data(request)
+ self.assertEqual(
+ b'sub/plain_wsh.py is called for /sub/plain?q=v, None'
+ b'\x88\x02\x03\xe8', request.connection.written_data())
+
+ def test_transfer_data_no_handler(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ for resource in [
+ '/blank', '/sub/non_callable', '/sub/no_wsh_at_the_end',
+ '/does/not/exist'
+ ]:
+ request = mock.MockRequest(connection=mock.MockConn(b''))
+ request.ws_resource = resource
+ request.ws_protocol = 'p2'
+ try:
+ dispatcher.transfer_data(request)
+ self.fail()
+ except dispatch.DispatchException as e:
+ self.assertTrue(str(e).find('No handler') != -1)
+ except Exception:
+ self.fail()
+
+ def test_transfer_data_handler_exception(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ request = mock.MockRequest(connection=mock.MockConn(b''))
+ request.ws_resource = '/sub/exception_in_transfer'
+ request.ws_protocol = 'p3'
+ try:
+ dispatcher.transfer_data(request)
+ self.fail()
+ except Exception as e:
+ self.assertTrue(
+ str(e).find('Intentional') != -1,
+ 'Unexpected exception: %s' % e)
+
+ def test_abort_transfer_data(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ request = mock.MockRequest()
+ request.ws_resource = '/abort_by_user'
+ self.assertRaises(handshake.AbortedByUserException,
+ dispatcher.transfer_data, request)
+
+ def test_scan_dir(self):
+ disp = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ self.assertEqual(4, len(disp._handler_suite_map))
+ self.assertTrue('/origin_check' in disp._handler_suite_map)
+ self.assertTrue(
+ '/sub/exception_in_transfer' in disp._handler_suite_map)
+ self.assertTrue('/sub/plain' in disp._handler_suite_map)
+
+ def test_scan_sub_dir(self):
+ disp = dispatch.Dispatcher(_TEST_HANDLERS_DIR, _TEST_HANDLERS_SUB_DIR)
+ self.assertEqual(2, len(disp._handler_suite_map))
+ self.assertFalse('/origin_check' in disp._handler_suite_map)
+ self.assertTrue(
+ '/sub/exception_in_transfer' in disp._handler_suite_map)
+ self.assertTrue('/sub/plain' in disp._handler_suite_map)
+
+ def test_scan_sub_dir_as_root(self):
+ disp = dispatch.Dispatcher(_TEST_HANDLERS_SUB_DIR,
+ _TEST_HANDLERS_SUB_DIR)
+ self.assertEqual(2, len(disp._handler_suite_map))
+ self.assertFalse('/origin_check' in disp._handler_suite_map)
+ self.assertFalse(
+ '/sub/exception_in_transfer' in disp._handler_suite_map)
+ self.assertFalse('/sub/plain' in disp._handler_suite_map)
+ self.assertTrue('/exception_in_transfer' in disp._handler_suite_map)
+ self.assertTrue('/plain' in disp._handler_suite_map)
+
+ def test_scan_dir_must_under_root(self):
+ dispatch.Dispatcher('a/b', 'a/b/c') # OK
+ dispatch.Dispatcher('a/b///', 'a/b') # OK
+ self.assertRaises(dispatch.DispatchException, dispatch.Dispatcher,
+ 'a/b/c', 'a/b')
+
+ def test_resource_path_alias(self):
+ disp = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ disp.add_resource_path_alias('/', '/origin_check')
+ self.assertEqual(5, len(disp._handler_suite_map))
+ self.assertTrue('/origin_check' in disp._handler_suite_map)
+ self.assertTrue(
+ '/sub/exception_in_transfer' in disp._handler_suite_map)
+ self.assertTrue('/sub/plain' in disp._handler_suite_map)
+ self.assertTrue('/' in disp._handler_suite_map)
+ self.assertRaises(dispatch.DispatchException,
+ disp.add_resource_path_alias, '/alias', '/not-exist')
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py
new file mode 100755
index 0000000000..2789e4a57e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py
@@ -0,0 +1,738 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""End-to-end tests for pywebsocket. Tests standalone.py.
+"""
+
+from __future__ import absolute_import
+from six.moves import urllib
+import locale
+import logging
+import os
+import signal
+import socket
+import subprocess
+import sys
+import time
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from test import client_for_testing
+
+# Special message that tells the echo server to start closing handshake
+_GOODBYE_MESSAGE = 'Goodbye'
+
+_SERVER_WARMUP_IN_SEC = 0.2
+
+
+# Test body functions
+def _echo_check_procedure(client):
+ client.connect()
+
+ client.send_message('test')
+ client.assert_receive('test')
+ client.send_message('helloworld')
+ client.assert_receive('helloworld')
+
+ client.send_close()
+ client.assert_receive_close()
+
+ client.assert_connection_closed()
+
+
+def _echo_check_procedure_with_binary(client):
+ client.connect()
+
+ client.send_message(b'binary', binary=True)
+ client.assert_receive(b'binary', binary=True)
+ client.send_message(b'\x00\x80\xfe\xff\x00\x80', binary=True)
+ client.assert_receive(b'\x00\x80\xfe\xff\x00\x80', binary=True)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ client.assert_connection_closed()
+
+
+def _echo_check_procedure_with_goodbye(client):
+ client.connect()
+
+ client.send_message('test')
+ client.assert_receive('test')
+
+ client.send_message(_GOODBYE_MESSAGE)
+ client.assert_receive(_GOODBYE_MESSAGE)
+
+ client.assert_receive_close()
+ client.send_close()
+
+ client.assert_connection_closed()
+
+
+def _echo_check_procedure_with_code_and_reason(client, code, reason):
+ client.connect()
+
+ client.send_close(code, reason)
+ client.assert_receive_close(code, reason)
+
+ client.assert_connection_closed()
+
+
+def _unmasked_frame_check_procedure(client):
+ client.connect()
+
+ client.send_message('test', mask=False)
+ client.assert_receive_close(client_for_testing.STATUS_PROTOCOL_ERROR, '')
+
+ client.assert_connection_closed()
+
+
+def _check_handshake_with_basic_auth(client):
+ client.connect()
+
+ client.send_message(_GOODBYE_MESSAGE)
+ client.assert_receive(_GOODBYE_MESSAGE)
+
+ client.assert_receive_close()
+ client.send_close()
+
+ client.assert_connection_closed()
+
+
+class EndToEndTestBase(unittest.TestCase):
+ """Base class for end-to-end tests that launch pywebsocket standalone
+ server as a separate process, connect to it using the client_for_testing
+ module, and check if the server behaves correctly by exchanging opening
+ handshake and frames over a TCP connection.
+ """
+ def setUp(self):
+ self.server_stderr = None
+ self.top_dir = os.path.join(os.path.dirname(__file__), '..')
+ os.putenv('PYTHONPATH', os.path.pathsep.join(sys.path))
+ self.standalone_command = os.path.join(self.top_dir, 'mod_pywebsocket',
+ 'standalone.py')
+ self.document_root = os.path.join(self.top_dir, 'example')
+ s = socket.socket()
+ s.bind(('localhost', 0))
+ (_, self.test_port) = s.getsockname()
+ s.close()
+
+ self._options = client_for_testing.ClientOptions()
+ self._options.server_host = 'localhost'
+ self._options.origin = 'http://localhost'
+ self._options.resource = '/echo'
+
+ self._options.server_port = self.test_port
+
+ # TODO(tyoshino): Use tearDown to kill the server.
+
+ def _run_python_command(self, commandline, stdout=None, stderr=None):
+ close_fds = True if sys.platform != 'win32' else None
+ return subprocess.Popen([sys.executable] + commandline,
+ close_fds=close_fds,
+ stdout=stdout,
+ stderr=stderr)
+
+ def _run_server(self, extra_args=[]):
+ args = [
+ self.standalone_command, '-H', 'localhost', '-V', 'localhost',
+ '-p',
+ str(self.test_port), '-P',
+ str(self.test_port), '-d', self.document_root
+ ]
+
+ # Inherit the level set to the root logger by test runner.
+ root_logger = logging.getLogger()
+ log_level = root_logger.getEffectiveLevel()
+ if log_level != logging.NOTSET:
+ args.append('--log-level')
+ args.append(logging.getLevelName(log_level).lower())
+
+ args += extra_args
+
+ return self._run_python_command(args, stderr=self.server_stderr)
+
+ def _close_server(self, server):
+ """
+
+ This method mimics Popen.__exit__ to gracefully kill the server process.
+ Its main purpose is to maintain comptaibility between python 2 and 3,
+ since Popen in python 2 does not have __exit__ attribute.
+
+ """
+ server.kill()
+
+ if server.stdout:
+ server.stdout.close()
+ if server.stderr:
+ server.stderr.close()
+ if server.stdin:
+ server.stdin.close()
+
+ server.wait()
+
+
+class EndToEndHyBiTest(EndToEndTestBase):
+ def setUp(self):
+ EndToEndTestBase.setUp(self)
+
+ def _run_test_with_options(self,
+ test_function,
+ options,
+ server_options=[]):
+ server = self._run_server(server_options)
+ try:
+ # TODO(tyoshino): add some logic to poll the server until it
+ # becomes ready
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client = client_for_testing.create_client(options)
+ try:
+ test_function(client)
+ finally:
+ client.close_socket()
+ finally:
+ self._close_server(server)
+
+ def _run_test(self, test_function):
+ self._run_test_with_options(test_function, self._options)
+
+ def _run_permessage_deflate_test(self, offer, response_checker,
+ test_function):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ self._options.extensions += offer
+ self._options.check_permessage_deflate = response_checker
+ client = client_for_testing.create_client(self._options)
+
+ try:
+ client.connect()
+
+ if test_function is not None:
+ test_function(client)
+
+ client.assert_connection_closed()
+ finally:
+ client.close_socket()
+ finally:
+ self._close_server(server)
+
+ def _run_close_with_code_and_reason_test(self,
+ test_function,
+ code,
+ reason,
+ server_options=[]):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client = client_for_testing.create_client(self._options)
+ try:
+ test_function(client, code, reason)
+ finally:
+ client.close_socket()
+ finally:
+ self._close_server(server)
+
+ def _run_http_fallback_test(self, options, status):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client = client_for_testing.create_client(options)
+ try:
+ client.connect()
+ self.fail('Could not catch HttpStatusException')
+ except client_for_testing.HttpStatusException as e:
+ self.assertEqual(status, e.status)
+ except Exception as e:
+ self.fail('Catch unexpected exception')
+ finally:
+ client.close_socket()
+ finally:
+ self._close_server(server)
+
+ def test_echo(self):
+ self._run_test(_echo_check_procedure)
+
+ def test_echo_binary(self):
+ self._run_test(_echo_check_procedure_with_binary)
+
+ def test_echo_server_close(self):
+ self._run_test(_echo_check_procedure_with_goodbye)
+
+ def test_unmasked_frame(self):
+ self._run_test(_unmasked_frame_check_procedure)
+
+ def test_echo_permessage_deflate(self):
+ def test_function(client):
+ # From the examples in the spec.
+ compressed_hello = b'\xf2\x48\xcd\xc9\xc9\x07\x00'
+ client._stream.send_data(compressed_hello,
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ compressed_hello,
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEqual('permessage-deflate', parameter.name())
+ self.assertEqual([], parameter.get_parameters())
+
+ self._run_permessage_deflate_test(['permessage-deflate'],
+ response_checker, test_function)
+
+ def test_echo_permessage_deflate_two_frames(self):
+ def test_function(client):
+ # From the examples in the spec.
+ client._stream.send_data(b'\xf2\x48\xcd',
+ client_for_testing.OPCODE_TEXT,
+ end=False,
+ rsv1=1)
+ client._stream.send_data(b'\xc9\xc9\x07\x00',
+ client_for_testing.OPCODE_TEXT)
+ client._stream.assert_receive_binary(
+ b'\xf2\x48\xcd\xc9\xc9\x07\x00',
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEqual('permessage-deflate', parameter.name())
+ self.assertEqual([], parameter.get_parameters())
+
+ self._run_permessage_deflate_test(['permessage-deflate'],
+ response_checker, test_function)
+
+ def test_echo_permessage_deflate_two_messages(self):
+ def test_function(client):
+ # From the examples in the spec.
+ client._stream.send_data(b'\xf2\x48\xcd\xc9\xc9\x07\x00',
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.send_data(b'\xf2\x00\x11\x00\x00',
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ b'\xf2\x48\xcd\xc9\xc9\x07\x00',
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ b'\xf2\x00\x11\x00\x00',
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEqual('permessage-deflate', parameter.name())
+ self.assertEqual([], parameter.get_parameters())
+
+ self._run_permessage_deflate_test(['permessage-deflate'],
+ response_checker, test_function)
+
+ def test_echo_permessage_deflate_two_msgs_server_no_context_takeover(self):
+ def test_function(client):
+ # From the examples in the spec.
+ client._stream.send_data(b'\xf2\x48\xcd\xc9\xc9\x07\x00',
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.send_data(b'\xf2\x00\x11\x00\x00',
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ b'\xf2\x48\xcd\xc9\xc9\x07\x00',
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ b'\xf2\x48\xcd\xc9\xc9\x07\x00',
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEqual('permessage-deflate', parameter.name())
+ self.assertEqual([('server_no_context_takeover', None)],
+ parameter.get_parameters())
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate; server_no_context_takeover'],
+ response_checker, test_function)
+
+ def test_echo_permessage_deflate_preference(self):
+ def test_function(client):
+ # From the examples in the spec.
+ compressed_hello = b'\xf2\x48\xcd\xc9\xc9\x07\x00'
+ client._stream.send_data(compressed_hello,
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ compressed_hello,
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEqual('permessage-deflate', parameter.name())
+ self.assertEqual([], parameter.get_parameters())
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate', 'deflate-frame'], response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_with_parameters(self):
+ def test_function(client):
+ # From the examples in the spec.
+ compressed_hello = b'\xf2\x48\xcd\xc9\xc9\x07\x00'
+ client._stream.send_data(compressed_hello,
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ compressed_hello,
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEqual('permessage-deflate', parameter.name())
+ self.assertEqual([('server_max_window_bits', '10'),
+ ('server_no_context_takeover', None)],
+ parameter.get_parameters())
+
+ self._run_permessage_deflate_test([
+ 'permessage-deflate; server_max_window_bits=10; '
+ 'server_no_context_takeover'
+ ], response_checker, test_function)
+
+ def test_echo_permessage_deflate_with_bad_server_max_window_bits(self):
+ def test_function(client):
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ raise Exception('Unexpected acceptance of permessage-deflate')
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate; server_max_window_bits=3000000'],
+ response_checker, test_function)
+
+ def test_echo_permessage_deflate_with_bad_server_max_window_bits(self):
+ def test_function(client):
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ raise Exception('Unexpected acceptance of permessage-deflate')
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate; server_max_window_bits=3000000'],
+ response_checker, test_function)
+
+ def test_echo_permessage_deflate_with_undefined_parameter(self):
+ def test_function(client):
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ raise Exception('Unexpected acceptance of permessage-deflate')
+
+ self._run_permessage_deflate_test(['permessage-deflate; foo=bar'],
+ response_checker, test_function)
+
+ def test_echo_close_with_code_and_reason(self):
+ self._options.resource = '/close'
+ self._run_close_with_code_and_reason_test(
+ _echo_check_procedure_with_code_and_reason, 3333, 'sunsunsunsun')
+
+ def test_echo_close_with_empty_body(self):
+ self._options.resource = '/close'
+ self._run_close_with_code_and_reason_test(
+ _echo_check_procedure_with_code_and_reason, None, '')
+
+ def test_close_on_protocol_error(self):
+ """Tests that the server sends a close frame with protocol error status
+ code when the client sends data with some protocol error.
+ """
+ def test_function(client):
+ client.connect()
+
+ # Intermediate frame without any preceding start of fragmentation
+ # frame.
+ client.send_frame_of_arbitrary_bytes(b'\x80\x80', '')
+ client.assert_receive_close(
+ client_for_testing.STATUS_PROTOCOL_ERROR)
+
+ self._run_test(test_function)
+
+ def test_close_on_unsupported_frame(self):
+ """Tests that the server sends a close frame with unsupported operation
+ status code when the client sends data asking some operation that is
+ not supported by the server.
+ """
+ def test_function(client):
+ client.connect()
+
+ # Text frame with RSV3 bit raised.
+ client.send_frame_of_arbitrary_bytes(b'\x91\x80', '')
+ client.assert_receive_close(
+ client_for_testing.STATUS_UNSUPPORTED_DATA)
+
+ self._run_test(test_function)
+
+ def test_close_on_invalid_frame(self):
+ """Tests that the server sends a close frame with invalid frame payload
+ data status code when the client sends an invalid frame like containing
+ invalid UTF-8 character.
+ """
+ def test_function(client):
+ client.connect()
+
+ # Text frame with invalid UTF-8 string.
+ client.send_message(b'\x80', raw=True)
+ client.assert_receive_close(
+ client_for_testing.STATUS_INVALID_FRAME_PAYLOAD_DATA)
+
+ self._run_test(test_function)
+
+ def test_close_on_internal_endpoint_error(self):
+ """Tests that the server sends a close frame with internal endpoint
+ error status code when the handler does bad operation.
+ """
+
+ self._options.resource = '/internal_error'
+
+ def test_function(client):
+ client.connect()
+ client.assert_receive_close(
+ client_for_testing.STATUS_INTERNAL_ENDPOINT_ERROR)
+
+ self._run_test(test_function)
+
+ def test_absolute_uri(self):
+ """Tests absolute uri request."""
+
+ options = self._options
+ options.resource = 'ws://localhost:%d/echo' % options.server_port
+ self._run_test_with_options(_echo_check_procedure, options)
+
+ def test_invalid_absolute_uri(self):
+ """Tests invalid absolute uri request."""
+
+ options = self._options
+ options.resource = 'ws://invalidlocalhost:%d/echo' % options.server_port
+ options.server_stderr = subprocess.PIPE
+
+ self._run_http_fallback_test(options, 404)
+
+ def test_origin_check(self):
+ """Tests http fallback on origin check fail."""
+
+ options = self._options
+ options.resource = '/origin_check'
+ # Server shows warning message for http 403 fallback. This warning
+ # message is confusing. Following pipe disposes warning messages.
+ self.server_stderr = subprocess.PIPE
+ self._run_http_fallback_test(options, 403)
+
+ def test_invalid_resource(self):
+ """Tests invalid resource path."""
+
+ options = self._options
+ options.resource = '/no_resource'
+
+ self.server_stderr = subprocess.PIPE
+ self._run_http_fallback_test(options, 404)
+
+ def test_fragmentized_resource(self):
+ """Tests resource name with fragment"""
+
+ options = self._options
+ options.resource = '/echo#fragment'
+
+ self.server_stderr = subprocess.PIPE
+ self._run_http_fallback_test(options, 400)
+
+ def test_version_check(self):
+ """Tests http fallback on version check fail."""
+
+ options = self._options
+ options.version = 99
+ self._run_http_fallback_test(options, 400)
+
+ def test_basic_auth_connection(self):
+ """Test successful basic auth"""
+
+ options = self._options
+ options.use_basic_auth = True
+
+ self.server_stderr = subprocess.PIPE
+ self._run_test_with_options(_check_handshake_with_basic_auth,
+ options,
+ server_options=['--basic-auth'])
+
+ def test_invalid_basic_auth_connection(self):
+ """Tests basic auth with invalid credentials"""
+
+ options = self._options
+ options.use_basic_auth = True
+ options.basic_auth_credential = 'invalid:test'
+
+ self.server_stderr = subprocess.PIPE
+
+ with self.assertRaises(client_for_testing.HttpStatusException) as e:
+ self._run_test_with_options(_check_handshake_with_basic_auth,
+ options,
+ server_options=['--basic-auth'])
+ self.assertEqual(101, e.exception.status)
+
+
+class EndToEndTestWithEchoClient(EndToEndTestBase):
+ def setUp(self):
+ EndToEndTestBase.setUp(self)
+
+ def _check_example_echo_client_result(self, expected, stdoutdata,
+ stderrdata):
+ actual = stdoutdata.decode(locale.getpreferredencoding())
+
+ # In Python 3 on Windows we get "\r\n" terminators back from
+ # the subprocess and we need to replace them with "\n" to get
+ # a match. This is a bit of a hack, but avoids platform- and
+ # version- specific code.
+ actual = actual.replace('\r\n', '\n')
+
+ if actual != expected:
+ raise Exception('Unexpected result on example echo client: '
+ '%r (expected) vs %r (actual)' %
+ (expected, actual))
+ if stderrdata is not None:
+ raise Exception('Unexpected error message on example echo '
+ 'client: %r' % stderrdata)
+
+ def test_example_echo_client(self):
+ """Tests that the echo_client.py example can talk with the server."""
+
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client_command = os.path.join(self.top_dir, 'example',
+ 'echo_client.py')
+
+ # Expected output for the default messages.
+ default_expectation = (u'Send: Hello\n'
+ u'Recv: Hello\n'
+ u'Send: <>\n'
+ u'Recv: <>\n'
+ u'Send close\n'
+ u'Recv ack\n')
+
+ args = [client_command, '-p', str(self._options.server_port)]
+ client = self._run_python_command(args, stdout=subprocess.PIPE)
+ stdoutdata, stderrdata = client.communicate()
+ self._check_example_echo_client_result(default_expectation,
+ stdoutdata, stderrdata)
+
+ # Process a big message for which extended payload length is used.
+ # To handle extended payload length, ws_version attribute will be
+ # accessed. This test checks that ws_version is correctly set.
+ big_message = 'a' * 1024
+ args = [
+ client_command, '-p',
+ str(self._options.server_port), '-m', big_message
+ ]
+ client = self._run_python_command(args, stdout=subprocess.PIPE)
+ stdoutdata, stderrdata = client.communicate()
+ expected = ('Send: %s\nRecv: %s\nSend close\nRecv ack\n' %
+ (big_message, big_message))
+ self._check_example_echo_client_result(expected, stdoutdata,
+ stderrdata)
+
+ # Test the permessage-deflate extension.
+ args = [
+ client_command, '-p',
+ str(self._options.server_port), '--use_permessage_deflate'
+ ]
+ client = self._run_python_command(args, stdout=subprocess.PIPE)
+ stdoutdata, stderrdata = client.communicate()
+ self._check_example_echo_client_result(default_expectation,
+ stdoutdata, stderrdata)
+ finally:
+ self._close_server(server)
+
+
+class EndToEndTestWithCgi(EndToEndTestBase):
+ def setUp(self):
+ EndToEndTestBase.setUp(self)
+
+ def test_cgi(self):
+ """Verifies that CGI scripts work."""
+
+ server = self._run_server(extra_args=['--cgi-paths', '/cgi-bin'])
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ url = 'http://localhost:%d/cgi-bin/hi.py' % self._options.server_port
+
+ # urlopen() in Python 2.7 doesn't support "with".
+ try:
+ f = urllib.request.urlopen(url)
+ except:
+ self._close_server(server)
+ raise
+
+ try:
+ self.assertEqual(f.getcode(), 200)
+ self.assertEqual(f.info().get('Content-Type'), 'text/plain')
+ body = f.read()
+ self.assertEqual(body.rstrip(b'\r\n'), b'Hi from hi.py')
+ finally:
+ f.close()
+ self._close_server(server)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py
new file mode 100755
index 0000000000..39a111888b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py
@@ -0,0 +1,192 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Tests for extensions module."""
+
+from __future__ import absolute_import
+import unittest
+import zlib
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import common
+from mod_pywebsocket import extensions
+
+
+class ExtensionsTest(unittest.TestCase):
+ """A unittest for non-class methods in extensions.py"""
+ def test_parse_window_bits(self):
+ self.assertRaises(ValueError, extensions._parse_window_bits, None)
+ self.assertRaises(ValueError, extensions._parse_window_bits, 'foobar')
+ self.assertRaises(ValueError, extensions._parse_window_bits, ' 8 ')
+ self.assertRaises(ValueError, extensions._parse_window_bits, 'a8a')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '00000')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '00008')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '0x8')
+
+ self.assertRaises(ValueError, extensions._parse_window_bits, '9.5')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '8.0')
+
+ self.assertTrue(extensions._parse_window_bits, '8')
+ self.assertTrue(extensions._parse_window_bits, '15')
+
+ self.assertRaises(ValueError, extensions._parse_window_bits, '-8')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '0')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '7')
+
+ self.assertRaises(ValueError, extensions._parse_window_bits, '16')
+ self.assertRaises(ValueError, extensions._parse_window_bits,
+ '10000000')
+
+
+class PerMessageDeflateExtensionProcessorParsingTest(unittest.TestCase):
+ """A unittest for checking that PerMessageDeflateExtensionProcessor parses
+ given extension parameter correctly.
+ """
+ def test_registry(self):
+ processor = extensions.get_extension_processor(
+ common.ExtensionParameter('permessage-deflate'))
+ self.assertIsInstance(processor,
+ extensions.PerMessageDeflateExtensionProcessor)
+
+ def test_minimal_offer(self):
+ processor = extensions.PerMessageDeflateExtensionProcessor(
+ common.ExtensionParameter('permessage-deflate'))
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+ self.assertEqual(zlib.MAX_WBITS,
+ processor._rfc1979_deflater._window_bits)
+ self.assertFalse(processor._rfc1979_deflater._no_context_takeover)
+
+ def test_offer_with_max_window_bits(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_max_window_bits', '10')
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual([('server_max_window_bits', '10')],
+ response.get_parameters())
+
+ self.assertEqual(10, processor._rfc1979_deflater._window_bits)
+
+ def test_offer_with_out_of_range_max_window_bits(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_max_window_bits', '0')
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_max_window_bits_without_value(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_max_window_bits', None)
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_no_context_takeover(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_no_context_takeover', None)
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual([('server_no_context_takeover', None)],
+ response.get_parameters())
+
+ self.assertTrue(processor._rfc1979_deflater._no_context_takeover)
+
+ def test_offer_with_no_context_takeover_with_value(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_no_context_takeover', 'foobar')
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_unknown_parameter(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('foo', 'bar')
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+
+class PerMessageDeflateExtensionProcessorBuildingTest(unittest.TestCase):
+ """A unittest for checking that PerMessageDeflateExtensionProcessor builds
+ a response based on specified options correctly.
+ """
+ def test_response_with_max_window_bits(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('client_max_window_bits', None)
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+ processor.set_client_max_window_bits(10)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual([('client_max_window_bits', '10')],
+ response.get_parameters())
+
+ def test_response_with_max_window_bits_without_client_permission(self):
+ processor = extensions.PerMessageDeflateExtensionProcessor(
+ common.ExtensionParameter('permessage-deflate'))
+ processor.set_client_max_window_bits(10)
+
+ response = processor.get_extension_response()
+ self.assertIsNone(response)
+
+ def test_response_with_true_for_no_context_takeover(self):
+ processor = extensions.PerMessageDeflateExtensionProcessor(
+ common.ExtensionParameter('permessage-deflate'))
+
+ processor.set_client_no_context_takeover(True)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual([('client_no_context_takeover', None)],
+ response.get_parameters())
+
+ def test_response_with_false_for_no_context_takeover(self):
+ processor = extensions.PerMessageDeflateExtensionProcessor(
+ common.ExtensionParameter('permessage-deflate'))
+
+ processor.set_client_no_context_takeover(False)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py
new file mode 100755
index 0000000000..7f4acf56ff
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Tests for handshake.base module."""
+
+from __future__ import absolute_import
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket.common import ExtensionParameter
+from mod_pywebsocket.common import ExtensionParsingException
+from mod_pywebsocket.common import format_extensions
+from mod_pywebsocket.common import parse_extensions
+from mod_pywebsocket.handshake.base import HandshakeException
+from mod_pywebsocket.handshake.base import validate_subprotocol
+
+
+class ValidateSubprotocolTest(unittest.TestCase):
+ """A unittest for validate_subprotocol method."""
+ def test_validate_subprotocol(self):
+ # Should succeed.
+ validate_subprotocol('sample')
+ validate_subprotocol('Sample')
+ validate_subprotocol('sample\x7eprotocol')
+
+ # Should fail.
+ self.assertRaises(HandshakeException, validate_subprotocol, '')
+ self.assertRaises(HandshakeException, validate_subprotocol,
+ 'sample\x09protocol')
+ self.assertRaises(HandshakeException, validate_subprotocol,
+ 'sample\x19protocol')
+ self.assertRaises(HandshakeException, validate_subprotocol,
+ 'sample\x20protocol')
+ self.assertRaises(HandshakeException, validate_subprotocol,
+ 'sample\x7fprotocol')
+ self.assertRaises(
+ HandshakeException,
+ validate_subprotocol,
+ # "Japan" in Japanese
+ u'\u65e5\u672c')
+
+
+_TEST_TOKEN_EXTENSION_DATA = [
+ ('foo', [('foo', [])]),
+ ('foo; bar', [('foo', [('bar', None)])]),
+ ('foo; bar=baz', [('foo', [('bar', 'baz')])]),
+ ('foo; bar=baz; car=cdr', [('foo', [('bar', 'baz'), ('car', 'cdr')])]),
+ ('foo; bar=baz, car; cdr', [('foo', [('bar', 'baz')]),
+ ('car', [('cdr', None)])]),
+ ('a, b, c, d', [('a', []), ('b', []), ('c', []), ('d', [])]),
+]
+
+_TEST_QUOTED_EXTENSION_DATA = [
+ ('foo; bar=""', [('foo', [('bar', '')])]),
+ ('foo; bar=" baz "', [('foo', [('bar', ' baz ')])]),
+ ('foo; bar=",baz;"', [('foo', [('bar', ',baz;')])]),
+ ('foo; bar="\\\r\\\nbaz"', [('foo', [('bar', '\r\nbaz')])]),
+ ('foo; bar="\\"baz"', [('foo', [('bar', '"baz')])]),
+ ('foo; bar="\xbbbaz"', [('foo', [('bar', '\xbbbaz')])]),
+]
+
+_TEST_REDUNDANT_TOKEN_EXTENSION_DATA = [
+ ('foo \t ', [('foo', [])]),
+ ('foo; \r\n bar', [('foo', [('bar', None)])]),
+ ('foo; bar=\r\n \r\n baz', [('foo', [('bar', 'baz')])]),
+ ('foo ;bar = baz ', [('foo', [('bar', 'baz')])]),
+ ('foo,bar,,baz', [('foo', []), ('bar', []), ('baz', [])]),
+]
+
+_TEST_REDUNDANT_QUOTED_EXTENSION_DATA = [
+ ('foo; bar="\r\n \r\n baz"', [('foo', [('bar', ' baz')])]),
+]
+
+
+class ExtensionsParserTest(unittest.TestCase):
+ def _verify_extension_list(self, expected_list, actual_list):
+ """Verifies that ExtensionParameter objects in actual_list have the
+ same members as extension definitions in expected_list. Extension
+ definition used in this test is a pair of an extension name and a
+ parameter dictionary.
+ """
+
+ self.assertEqual(len(expected_list), len(actual_list))
+ for expected, actual in zip(expected_list, actual_list):
+ (name, parameters) = expected
+ self.assertEqual(name, actual._name)
+ self.assertEqual(parameters, actual._parameters)
+
+ def test_parse(self):
+ for formatted_string, definition in _TEST_TOKEN_EXTENSION_DATA:
+ self._verify_extension_list(definition,
+ parse_extensions(formatted_string))
+
+ def test_parse_quoted_data(self):
+ for formatted_string, definition in _TEST_QUOTED_EXTENSION_DATA:
+ self._verify_extension_list(definition,
+ parse_extensions(formatted_string))
+
+ def test_parse_redundant_data(self):
+ for (formatted_string,
+ definition) in _TEST_REDUNDANT_TOKEN_EXTENSION_DATA:
+ self._verify_extension_list(definition,
+ parse_extensions(formatted_string))
+
+ def test_parse_redundant_quoted_data(self):
+ for (formatted_string,
+ definition) in _TEST_REDUNDANT_QUOTED_EXTENSION_DATA:
+ self._verify_extension_list(definition,
+ parse_extensions(formatted_string))
+
+ def test_parse_bad_data(self):
+ _TEST_BAD_EXTENSION_DATA = [
+ ('foo; ; '),
+ ('foo; a a'),
+ ('foo foo'),
+ (',,,'),
+ ('foo; bar='),
+ ('foo; bar="hoge'),
+ ('foo; bar="a\r"'),
+ ('foo; bar="\\\xff"'),
+ ('foo; bar=\ra'),
+ ]
+
+ for formatted_string in _TEST_BAD_EXTENSION_DATA:
+ self.assertRaises(ExtensionParsingException, parse_extensions,
+ formatted_string)
+
+
+class FormatExtensionsTest(unittest.TestCase):
+ def test_format_extensions(self):
+ for formatted_string, definitions in _TEST_TOKEN_EXTENSION_DATA:
+ extensions = []
+ for definition in definitions:
+ (name, parameters) = definition
+ extension = ExtensionParameter(name)
+ extension._parameters = parameters
+ extensions.append(extension)
+ self.assertEqual(formatted_string, format_extensions(extensions))
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py
new file mode 100755
index 0000000000..8c65822170
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py
@@ -0,0 +1,422 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Tests for handshake module."""
+
+from __future__ import absolute_import
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+from mod_pywebsocket import common
+from mod_pywebsocket.handshake.base import AbortedByUserException
+from mod_pywebsocket.handshake.base import HandshakeException
+from mod_pywebsocket.handshake.base import VersionException
+from mod_pywebsocket.handshake.hybi import Handshaker
+
+from test import mock
+
+
+class RequestDefinition(object):
+ """A class for holding data for constructing opening handshake strings for
+ testing the opening handshake processor.
+ """
+ def __init__(self, method, uri, headers):
+ self.method = method
+ self.uri = uri
+ self.headers = headers
+
+
+def _create_good_request_def():
+ return RequestDefinition(
+ 'GET', '/demo', {
+ 'Host': 'server.example.com',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': '13',
+ 'Origin': 'http://example.com'
+ })
+
+
+def _create_request(request_def):
+ conn = mock.MockConn(b'')
+ return mock.MockRequest(method=request_def.method,
+ uri=request_def.uri,
+ headers_in=request_def.headers,
+ connection=conn)
+
+
+def _create_handshaker(request):
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ return handshaker
+
+
+class SubprotocolChoosingDispatcher(object):
+ """A dispatcher for testing. This dispatcher sets the i-th subprotocol
+ of requested ones to ws_protocol where i is given on construction as index
+ argument. If index is negative, default_value will be set to ws_protocol.
+ """
+ def __init__(self, index, default_value=None):
+ self.index = index
+ self.default_value = default_value
+
+ def do_extra_handshake(self, conn_context):
+ if self.index >= 0:
+ conn_context.ws_protocol = conn_context.ws_requested_protocols[
+ self.index]
+ else:
+ conn_context.ws_protocol = self.default_value
+
+ def transfer_data(self, conn_context):
+ pass
+
+
+class HandshakeAbortedException(Exception):
+ pass
+
+
+class AbortingDispatcher(object):
+ """A dispatcher for testing. This dispatcher raises an exception in
+ do_extra_handshake to reject the request.
+ """
+ def do_extra_handshake(self, conn_context):
+ raise HandshakeAbortedException('An exception to reject the request')
+
+ def transfer_data(self, conn_context):
+ pass
+
+
+class AbortedByUserDispatcher(object):
+ """A dispatcher for testing. This dispatcher raises an
+ AbortedByUserException in do_extra_handshake to reject the request.
+ """
+ def do_extra_handshake(self, conn_context):
+ raise AbortedByUserException('An AbortedByUserException to reject the '
+ 'request')
+
+ def transfer_data(self, conn_context):
+ pass
+
+
+_EXPECTED_RESPONSE = (
+ b'HTTP/1.1 101 Switching Protocols\r\n'
+ b'Upgrade: websocket\r\n'
+ b'Connection: Upgrade\r\n'
+ b'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n')
+
+
+class HandshakerTest(unittest.TestCase):
+ """A unittest for draft-ietf-hybi-thewebsocketprotocol-06 and later
+ handshake processor.
+ """
+ def test_do_handshake(self):
+ request = _create_request(_create_good_request_def())
+ dispatcher = mock.MockDispatcher()
+ handshaker = Handshaker(request, dispatcher)
+ handshaker.do_handshake()
+
+ self.assertTrue(dispatcher.do_extra_handshake_called)
+
+ self.assertEqual(_EXPECTED_RESPONSE, request.connection.written_data())
+ self.assertEqual('/demo', request.ws_resource)
+ self.assertEqual('http://example.com', request.ws_origin)
+ self.assertEqual(None, request.ws_protocol)
+ self.assertEqual(None, request.ws_extensions)
+ self.assertEqual(common.VERSION_HYBI_LATEST, request.ws_version)
+
+ def test_do_handshake_with_extra_headers(self):
+ request_def = _create_good_request_def()
+ # Add headers not related to WebSocket opening handshake.
+ request_def.headers['FooKey'] = 'BarValue'
+ request_def.headers['EmptyKey'] = ''
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(_EXPECTED_RESPONSE, request.connection.written_data())
+
+ def test_do_handshake_with_capitalized_value(self):
+ request_def = _create_good_request_def()
+ request_def.headers['upgrade'] = 'WEBSOCKET'
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(_EXPECTED_RESPONSE, request.connection.written_data())
+
+ request_def = _create_good_request_def()
+ request_def.headers['Connection'] = 'UPGRADE'
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(_EXPECTED_RESPONSE, request.connection.written_data())
+
+ def test_do_handshake_with_multiple_connection_values(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Connection'] = 'Upgrade, keep-alive, , '
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(_EXPECTED_RESPONSE, request.connection.written_data())
+
+ def test_aborting_handshake(self):
+ handshaker = Handshaker(_create_request(_create_good_request_def()),
+ AbortingDispatcher())
+ # do_extra_handshake raises an exception. Check that it's not caught by
+ # do_handshake.
+ self.assertRaises(HandshakeAbortedException, handshaker.do_handshake)
+
+ def test_do_handshake_with_protocol(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Protocol'] = 'chat, superchat'
+
+ request = _create_request(request_def)
+ handshaker = Handshaker(request, SubprotocolChoosingDispatcher(0))
+ handshaker.do_handshake()
+
+ EXPECTED_RESPONSE = (
+ b'HTTP/1.1 101 Switching Protocols\r\n'
+ b'Upgrade: websocket\r\n'
+ b'Connection: Upgrade\r\n'
+ b'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n'
+ b'Sec-WebSocket-Protocol: chat\r\n\r\n')
+
+ self.assertEqual(EXPECTED_RESPONSE, request.connection.written_data())
+ self.assertEqual('chat', request.ws_protocol)
+
+ def test_do_handshake_protocol_not_in_request_but_in_response(self):
+ request_def = _create_good_request_def()
+ request = _create_request(request_def)
+ handshaker = Handshaker(request,
+ SubprotocolChoosingDispatcher(-1, 'foobar'))
+ # No request has been made but ws_protocol is set. HandshakeException
+ # must be raised.
+ self.assertRaises(HandshakeException, handshaker.do_handshake)
+
+ def test_do_handshake_with_protocol_no_protocol_selection(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Protocol'] = 'chat, superchat'
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ # ws_protocol is not set. HandshakeException must be raised.
+ self.assertRaises(HandshakeException, handshaker.do_handshake)
+
+ def test_do_handshake_with_extensions(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = (
+ 'permessage-deflate; server_no_context_takeover')
+
+ EXPECTED_RESPONSE = (
+ b'HTTP/1.1 101 Switching Protocols\r\n'
+ b'Upgrade: websocket\r\n'
+ b'Connection: Upgrade\r\n'
+ b'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n'
+ b'Sec-WebSocket-Extensions: '
+ b'permessage-deflate; server_no_context_takeover\r\n'
+ b'\r\n')
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(EXPECTED_RESPONSE, request.connection.written_data())
+ self.assertEqual(1, len(request.ws_extensions))
+ extension = request.ws_extensions[0]
+ self.assertEqual(common.PERMESSAGE_DEFLATE_EXTENSION, extension.name())
+ self.assertEqual(['server_no_context_takeover'],
+ extension.get_parameter_names())
+ self.assertEqual(
+ None, extension.get_parameter_value('server_no_context_takeover'))
+ self.assertEqual(1, len(request.ws_extension_processors))
+ self.assertEqual('deflate', request.ws_extension_processors[0].name())
+
+ def test_do_handshake_with_quoted_extensions(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = (
+ 'permessage-deflate, , '
+ 'unknown; e = "mc^2"; ma="\r\n \\\rf "; pv=nrt')
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(2, len(request.ws_requested_extensions))
+ first_extension = request.ws_requested_extensions[0]
+ self.assertEqual('permessage-deflate', first_extension.name())
+ second_extension = request.ws_requested_extensions[1]
+ self.assertEqual('unknown', second_extension.name())
+ self.assertEqual(['e', 'ma', 'pv'],
+ second_extension.get_parameter_names())
+ self.assertEqual('mc^2', second_extension.get_parameter_value('e'))
+ self.assertEqual(' \rf ', second_extension.get_parameter_value('ma'))
+ self.assertEqual('nrt', second_extension.get_parameter_value('pv'))
+
+ def test_do_handshake_with_optional_headers(self):
+ request_def = _create_good_request_def()
+ request_def.headers['EmptyValue'] = ''
+ request_def.headers['AKey'] = 'AValue'
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual('AValue', request.headers_in['AKey'])
+ self.assertEqual('', request.headers_in['EmptyValue'])
+
+ def test_abort_extra_handshake(self):
+ handshaker = Handshaker(_create_request(_create_good_request_def()),
+ AbortedByUserDispatcher())
+ # do_extra_handshake raises an AbortedByUserException. Check that it's
+ # not caught by do_handshake.
+ self.assertRaises(AbortedByUserException, handshaker.do_handshake)
+
+ def test_bad_requests(self):
+ bad_cases = [
+ ('HTTP request',
+ RequestDefinition(
+ 'GET', '/demo', {
+ 'Host':
+ 'www.google.com',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5;'
+ ' en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3'
+ ' GTB6 GTBA',
+ 'Accept':
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,'
+ '*/*;q=0.8',
+ 'Accept-Language':
+ 'en-us,en;q=0.5',
+ 'Accept-Encoding':
+ 'gzip,deflate',
+ 'Accept-Charset':
+ 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+ 'Keep-Alive':
+ '300',
+ 'Connection':
+ 'keep-alive'
+ }), None, True)
+ ]
+
+ request_def = _create_good_request_def()
+ request_def.method = 'POST'
+ bad_cases.append(('Wrong method', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Host']
+ bad_cases.append(('Missing Host', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Upgrade']
+ bad_cases.append(('Missing Upgrade', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Upgrade'] = 'nonwebsocket'
+ bad_cases.append(('Wrong Upgrade', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Connection']
+ bad_cases.append(('Missing Connection', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Connection'] = 'Downgrade'
+ bad_cases.append(('Wrong Connection', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Sec-WebSocket-Key']
+ bad_cases.append(('Missing Sec-WebSocket-Key', request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Key'] = (
+ 'dGhlIHNhbXBsZSBub25jZQ==garbage')
+ bad_cases.append(('Wrong Sec-WebSocket-Key (with garbage on the tail)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Key'] = 'YQ==' # BASE64 of 'a'
+ bad_cases.append(
+ ('Wrong Sec-WebSocket-Key (decoded value is not 16 octets long)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ # The last character right before == must be any of A, Q, w and g.
+ request_def.headers['Sec-WebSocket-Key'] = 'AQIDBAUGBwgJCgsMDQ4PEC=='
+ bad_cases.append(
+ ('Wrong Sec-WebSocket-Key (padding bits are not zero)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Key'] = (
+ 'dGhlIHNhbXBsZSBub25jZQ==,dGhlIHNhbXBsZSBub25jZQ==')
+ bad_cases.append(('Wrong Sec-WebSocket-Key (multiple values)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Sec-WebSocket-Version']
+ bad_cases.append(
+ ('Missing Sec-WebSocket-Version', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Version'] = '3'
+ bad_cases.append(
+ ('Wrong Sec-WebSocket-Version', request_def, None, False))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Version'] = '13, 13'
+ bad_cases.append(('Wrong Sec-WebSocket-Version (multiple values)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Protocol'] = 'illegal\x09protocol'
+ bad_cases.append(
+ ('Illegal Sec-WebSocket-Protocol', request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Protocol'] = ''
+ bad_cases.append(
+ ('Empty Sec-WebSocket-Protocol', request_def, 400, True))
+
+ for (case_name, request_def, expected_status,
+ expect_handshake_exception) in bad_cases:
+ request = _create_request(request_def)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ try:
+ handshaker.do_handshake()
+ self.fail('No exception thrown for \'%s\' case' % case_name)
+ except HandshakeException as e:
+ self.assertTrue(expect_handshake_exception)
+ self.assertEqual(expected_status, e.status)
+ except VersionException as e:
+ self.assertFalse(expect_handshake_exception)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py
new file mode 100755
index 0000000000..f8c8e7a981
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Tests for http_header_util module."""
+
+from __future__ import absolute_import
+import unittest
+import sys
+
+from mod_pywebsocket import http_header_util
+
+
+class UnitTest(unittest.TestCase):
+ """A unittest for http_header_util module."""
+ def test_parse_relative_uri(self):
+ host, port, resource = http_header_util.parse_uri('/ws/test')
+ self.assertEqual(None, host)
+ self.assertEqual(None, port)
+ self.assertEqual('/ws/test', resource)
+
+ def test_parse_absolute_uri(self):
+ host, port, resource = http_header_util.parse_uri(
+ 'ws://localhost:10080/ws/test')
+ self.assertEqual('localhost', host)
+ self.assertEqual(10080, port)
+ self.assertEqual('/ws/test', resource)
+
+ host, port, resource = http_header_util.parse_uri(
+ 'ws://example.com/ws/test')
+ self.assertEqual('example.com', host)
+ self.assertEqual(80, port)
+ self.assertEqual('/ws/test', resource)
+
+ host, port, resource = http_header_util.parse_uri('wss://example.com/')
+ self.assertEqual('example.com', host)
+ self.assertEqual(443, port)
+ self.assertEqual('/', resource)
+
+ host, port, resource = http_header_util.parse_uri(
+ 'ws://example.com:8080')
+ self.assertEqual('example.com', host)
+ self.assertEqual(8080, port)
+ self.assertEqual('/', resource)
+
+ def test_parse_invalid_uri(self):
+ host, port, resource = http_header_util.parse_uri('ws:///')
+ self.assertEqual(None, resource)
+
+ host, port, resource = http_header_util.parse_uri(
+ 'ws://localhost:INVALID_PORT')
+ self.assertEqual(None, resource)
+
+ host, port, resource = http_header_util.parse_uri(
+ 'ws://localhost:-1/ws')
+ if sys.hexversion >= 0x030600f0:
+ self.assertEqual(None, resource)
+ else:
+ self.assertEqual('localhost', host)
+ self.assertEqual(80, port)
+ self.assertEqual('/ws', resource)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py
new file mode 100755
index 0000000000..f7288c510b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Tests for memorizingfile module."""
+
+from __future__ import absolute_import
+import unittest
+import six
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import memorizingfile
+
+
+class UtilTest(unittest.TestCase):
+ """A unittest for memorizingfile module."""
+ def check(self, memorizing_file, num_read, expected_list):
+ for unused in range(num_read):
+ memorizing_file.readline()
+ actual_list = memorizing_file.get_memorized_lines()
+ self.assertEqual(len(expected_list), len(actual_list))
+ for expected, actual in zip(expected_list, actual_list):
+ self.assertEqual(expected, actual)
+
+ def check_with_size(self, memorizing_file, read_size, expected_list):
+ read_list = []
+ read_line = ''
+ while True:
+ line = memorizing_file.readline(read_size)
+ line_length = len(line)
+ self.assertTrue(line_length <= read_size)
+ if line_length == 0:
+ if read_line != '':
+ read_list.append(read_line)
+ break
+ read_line += line
+ if line[line_length - 1] == '\n':
+ read_list.append(read_line)
+ read_line = ''
+ actual_list = memorizing_file.get_memorized_lines()
+ self.assertEqual(len(expected_list), len(actual_list))
+ self.assertEqual(len(expected_list), len(read_list))
+ for expected, actual, read in zip(expected_list, actual_list,
+ read_list):
+ self.assertEqual(expected, actual)
+ self.assertEqual(expected, read)
+
+ def test_get_memorized_lines(self):
+ memorizing_file = memorizingfile.MemorizingFile(
+ six.StringIO('Hello\nWorld\nWelcome'))
+ self.check(memorizing_file, 3, ['Hello\n', 'World\n', 'Welcome'])
+
+ def test_get_memorized_lines_limit_memorized_lines(self):
+ memorizing_file = memorizingfile.MemorizingFile(
+ six.StringIO('Hello\nWorld\nWelcome'), 2)
+ self.check(memorizing_file, 3, ['Hello\n', 'World\n'])
+
+ def test_get_memorized_lines_empty_file(self):
+ memorizing_file = memorizingfile.MemorizingFile(six.StringIO(''))
+ self.check(memorizing_file, 10, [])
+
+ def test_get_memorized_lines_with_size(self):
+ for size in range(1, 10):
+ memorizing_file = memorizingfile.MemorizingFile(
+ six.StringIO('Hello\nWorld\nWelcome'))
+ self.check_with_size(memorizing_file, size,
+ ['Hello\n', 'World\n', 'Welcome'])
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py
new file mode 100755
index 0000000000..073873dde9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Tests for mock module."""
+
+from __future__ import absolute_import
+import six.moves.queue
+import threading
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from test import mock
+
+
+class MockConnTest(unittest.TestCase):
+ """A unittest for MockConn class."""
+ def setUp(self):
+ self._conn = mock.MockConn(b'ABC\r\nDEFG\r\n\r\nHIJK')
+
+ def test_readline(self):
+ self.assertEqual(b'ABC\r\n', self._conn.readline())
+ self.assertEqual(b'DEFG\r\n', self._conn.readline())
+ self.assertEqual(b'\r\n', self._conn.readline())
+ self.assertEqual(b'HIJK', self._conn.readline())
+ self.assertEqual(b'', self._conn.readline())
+
+ def test_read(self):
+ self.assertEqual(b'ABC\r\nD', self._conn.read(6))
+ self.assertEqual(b'EFG\r\n\r\nHI', self._conn.read(9))
+ self.assertEqual(b'JK', self._conn.read(10))
+ self.assertEqual(b'', self._conn.read(10))
+
+ def test_read_and_readline(self):
+ self.assertEqual(b'ABC\r\nD', self._conn.read(6))
+ self.assertEqual(b'EFG\r\n', self._conn.readline())
+ self.assertEqual(b'\r\nHIJK', self._conn.read(9))
+ self.assertEqual(b'', self._conn.readline())
+
+ def test_write(self):
+ self._conn.write(b'Hello\r\n')
+ self._conn.write(b'World\r\n')
+ self.assertEqual(b'Hello\r\nWorld\r\n', self._conn.written_data())
+
+
+class MockBlockingConnTest(unittest.TestCase):
+ """A unittest for MockBlockingConn class."""
+ def test_read(self):
+ """Tests that data put to MockBlockingConn by put_bytes method can be
+ read from it.
+ """
+ class LineReader(threading.Thread):
+ """A test class that launches a thread, calls readline on the
+ specified conn repeatedly and puts the read data to the specified
+ queue.
+ """
+ def __init__(self, conn, queue):
+ threading.Thread.__init__(self)
+ self._queue = queue
+ self._conn = conn
+ self.setDaemon(True)
+ self.start()
+
+ def run(self):
+ while True:
+ data = self._conn.readline()
+ self._queue.put(data)
+
+ conn = mock.MockBlockingConn()
+ queue = six.moves.queue.Queue()
+ reader = LineReader(conn, queue)
+ self.assertTrue(queue.empty())
+ conn.put_bytes(b'Foo bar\r\n')
+ read = queue.get()
+ self.assertEqual(b'Foo bar\r\n', read)
+
+
+class MockTableTest(unittest.TestCase):
+ """A unittest for MockTable class."""
+ def test_create_from_dict(self):
+ table = mock.MockTable({'Key': 'Value'})
+ self.assertEqual('Value', table.get('KEY'))
+ self.assertEqual('Value', table['key'])
+
+ def test_create_from_list(self):
+ table = mock.MockTable([('Key', 'Value')])
+ self.assertEqual('Value', table.get('KEY'))
+ self.assertEqual('Value', table['key'])
+
+ def test_create_from_tuple(self):
+ table = mock.MockTable((('Key', 'Value'), ))
+ self.assertEqual('Value', table.get('KEY'))
+ self.assertEqual('Value', table['key'])
+
+ def test_set_and_get(self):
+ table = mock.MockTable()
+ self.assertEqual(None, table.get('Key'))
+ table['Key'] = 'Value'
+ self.assertEqual('Value', table.get('Key'))
+ self.assertEqual('Value', table.get('key'))
+ self.assertEqual('Value', table.get('KEY'))
+ self.assertEqual('Value', table['Key'])
+ self.assertEqual('Value', table['key'])
+ self.assertEqual('Value', table['KEY'])
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py
new file mode 100755
index 0000000000..1122c281b7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py
@@ -0,0 +1,912 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Tests for msgutil module."""
+
+from __future__ import absolute_import
+from __future__ import print_function
+from __future__ import division
+import array
+import six.moves.queue
+import random
+import struct
+import unittest
+import zlib
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import common
+from mod_pywebsocket.extensions import PerMessageDeflateExtensionProcessor
+from mod_pywebsocket import msgutil
+from mod_pywebsocket.stream import InvalidUTF8Exception
+from mod_pywebsocket.stream import Stream
+from mod_pywebsocket.stream import StreamOptions
+from mod_pywebsocket import util
+from test import mock
+from six.moves import map
+from six.moves import range
+from six import iterbytes
+
+# We use one fixed nonce for testing instead of cryptographically secure PRNG.
+_MASKING_NONCE = b'ABCD'
+
+
+def _mask_hybi(frame):
+ if isinstance(frame, six.text_type):
+ Exception('masking does not accept Texts')
+
+ frame_key = list(iterbytes(_MASKING_NONCE))
+ frame_key_len = len(frame_key)
+ result = bytearray(frame)
+ count = 0
+
+ for i in range(len(result)):
+ result[i] ^= frame_key[count]
+ count = (count + 1) % frame_key_len
+
+ return _MASKING_NONCE + bytes(result)
+
+
+def _install_extension_processor(processor, request, stream_options):
+ response = processor.get_extension_response()
+ if response is not None:
+ processor.setup_stream_options(stream_options)
+ request.ws_extension_processors.append(processor)
+
+
+def _create_request_from_rawdata(read_data, permessage_deflate_request=None):
+ req = mock.MockRequest(connection=mock.MockConn(read_data))
+ req.ws_version = common.VERSION_HYBI_LATEST
+ req.ws_extension_processors = []
+
+ processor = None
+ if permessage_deflate_request is not None:
+ processor = PerMessageDeflateExtensionProcessor(
+ permessage_deflate_request)
+
+ stream_options = StreamOptions()
+ if processor is not None:
+ _install_extension_processor(processor, req, stream_options)
+ req.ws_stream = Stream(req, stream_options)
+
+ return req
+
+
+def _create_request(*frames):
+ """Creates MockRequest using data given as frames.
+
+ frames will be returned on calling request.connection.read() where request
+ is MockRequest returned by this function.
+ """
+
+ read_data = []
+ for (header, body) in frames:
+ read_data.append(header + _mask_hybi(body))
+
+ return _create_request_from_rawdata(b''.join(read_data))
+
+
+def _create_blocking_request():
+ """Creates MockRequest.
+
+ Data written to a MockRequest can be read out by calling
+ request.connection.written_data().
+ """
+
+ req = mock.MockRequest(connection=mock.MockBlockingConn())
+ req.ws_version = common.VERSION_HYBI_LATEST
+ stream_options = StreamOptions()
+ req.ws_stream = Stream(req, stream_options)
+ return req
+
+
+class BasicMessageTest(unittest.TestCase):
+ """Basic tests for Stream."""
+ def test_send_message(self):
+ request = _create_request()
+ msgutil.send_message(request, 'Hello')
+ self.assertEqual(b'\x81\x05Hello', request.connection.written_data())
+
+ payload = 'a' * 125
+ request = _create_request()
+ msgutil.send_message(request, payload)
+ self.assertEqual(b'\x81\x7d' + payload.encode('UTF-8'),
+ request.connection.written_data())
+
+ def test_send_medium_message(self):
+ payload = 'a' * 126
+ request = _create_request()
+ msgutil.send_message(request, payload)
+ self.assertEqual(b'\x81\x7e\x00\x7e' + payload.encode('UTF-8'),
+ request.connection.written_data())
+
+ payload = 'a' * ((1 << 16) - 1)
+ request = _create_request()
+ msgutil.send_message(request, payload)
+ self.assertEqual(b'\x81\x7e\xff\xff' + payload.encode('UTF-8'),
+ request.connection.written_data())
+
+ def test_send_large_message(self):
+ payload = 'a' * (1 << 16)
+ request = _create_request()
+ msgutil.send_message(request, payload)
+ self.assertEqual(
+ b'\x81\x7f\x00\x00\x00\x00\x00\x01\x00\x00' +
+ payload.encode('UTF-8'), request.connection.written_data())
+
+ def test_send_message_unicode(self):
+ request = _create_request()
+ msgutil.send_message(request, u'\u65e5')
+ # U+65e5 is encoded as e6,97,a5 in UTF-8
+ self.assertEqual(b'\x81\x03\xe6\x97\xa5',
+ request.connection.written_data())
+
+ def test_send_message_fragments(self):
+ request = _create_request()
+ msgutil.send_message(request, 'Hello', False)
+ msgutil.send_message(request, ' ', False)
+ msgutil.send_message(request, 'World', False)
+ msgutil.send_message(request, '!', True)
+ self.assertEqual(b'\x01\x05Hello\x00\x01 \x00\x05World\x80\x01!',
+ request.connection.written_data())
+
+ def test_send_fragments_immediate_zero_termination(self):
+ request = _create_request()
+ msgutil.send_message(request, 'Hello World!', False)
+ msgutil.send_message(request, '', True)
+ self.assertEqual(b'\x01\x0cHello World!\x80\x00',
+ request.connection.written_data())
+
+ def test_receive_message(self):
+ request = _create_request((b'\x81\x85', b'Hello'),
+ (b'\x81\x86', b'World!'))
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertEqual('World!', msgutil.receive_message(request))
+
+ payload = b'a' * 125
+ request = _create_request((b'\x81\xfd', payload))
+ self.assertEqual(payload.decode('UTF-8'),
+ msgutil.receive_message(request))
+
+ def test_receive_medium_message(self):
+ payload = b'a' * 126
+ request = _create_request((b'\x81\xfe\x00\x7e', payload))
+ self.assertEqual(payload.decode('UTF-8'),
+ msgutil.receive_message(request))
+
+ payload = b'a' * ((1 << 16) - 1)
+ request = _create_request((b'\x81\xfe\xff\xff', payload))
+ self.assertEqual(payload.decode('UTF-8'),
+ msgutil.receive_message(request))
+
+ def test_receive_large_message(self):
+ payload = b'a' * (1 << 16)
+ request = _create_request(
+ (b'\x81\xff\x00\x00\x00\x00\x00\x01\x00\x00', payload))
+ self.assertEqual(payload.decode('UTF-8'),
+ msgutil.receive_message(request))
+
+ def test_receive_length_not_encoded_using_minimal_number_of_bytes(self):
+ # Log warning on receiving bad payload length field that doesn't use
+ # minimal number of bytes but continue processing.
+
+ payload = b'a'
+ # 1 byte can be represented without extended payload length field.
+ request = _create_request(
+ (b'\x81\xff\x00\x00\x00\x00\x00\x00\x00\x01', payload))
+ self.assertEqual(payload.decode('UTF-8'),
+ msgutil.receive_message(request))
+
+ def test_receive_message_unicode(self):
+ request = _create_request((b'\x81\x83', b'\xe6\x9c\xac'))
+ # U+672c is encoded as e6,9c,ac in UTF-8
+ self.assertEqual(u'\u672c', msgutil.receive_message(request))
+
+ def test_receive_message_erroneous_unicode(self):
+ # \x80 and \x81 are invalid as UTF-8.
+ request = _create_request((b'\x81\x82', b'\x80\x81'))
+ # Invalid characters should raise InvalidUTF8Exception
+ self.assertRaises(InvalidUTF8Exception, msgutil.receive_message,
+ request)
+
+ def test_receive_fragments(self):
+ request = _create_request((b'\x01\x85', b'Hello'), (b'\x00\x81', b' '),
+ (b'\x00\x85', b'World'), (b'\x80\x81', b'!'))
+ self.assertEqual('Hello World!', msgutil.receive_message(request))
+
+ def test_receive_fragments_unicode(self):
+ # UTF-8 encodes U+6f22 into e6bca2 and U+5b57 into e5ad97.
+ request = _create_request((b'\x01\x82', b'\xe6\xbc'),
+ (b'\x00\x82', b'\xa2\xe5'),
+ (b'\x80\x82', b'\xad\x97'))
+ self.assertEqual(u'\u6f22\u5b57', msgutil.receive_message(request))
+
+ def test_receive_fragments_immediate_zero_termination(self):
+ request = _create_request((b'\x01\x8c', b'Hello World!'),
+ (b'\x80\x80', b''))
+ self.assertEqual('Hello World!', msgutil.receive_message(request))
+
+ def test_receive_fragments_duplicate_start(self):
+ request = _create_request((b'\x01\x85', b'Hello'),
+ (b'\x01\x85', b'World'))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message, request)
+
+ def test_receive_fragments_intermediate_but_not_started(self):
+ request = _create_request((b'\x00\x85', b'Hello'))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message, request)
+
+ def test_receive_fragments_end_but_not_started(self):
+ request = _create_request((b'\x80\x85', b'Hello'))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message, request)
+
+ def test_receive_message_discard(self):
+ request = _create_request(
+ (b'\x8f\x86', b'IGNORE'), (b'\x81\x85', b'Hello'),
+ (b'\x8f\x89', b'DISREGARD'), (b'\x81\x86', b'World!'))
+ self.assertRaises(msgutil.UnsupportedFrameException,
+ msgutil.receive_message, request)
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertRaises(msgutil.UnsupportedFrameException,
+ msgutil.receive_message, request)
+ self.assertEqual('World!', msgutil.receive_message(request))
+
+ def test_receive_close(self):
+ request = _create_request(
+ (b'\x88\x8a', struct.pack('!H', 1000) + b'Good bye'))
+ self.assertEqual(None, msgutil.receive_message(request))
+ self.assertEqual(1000, request.ws_close_code)
+ self.assertEqual('Good bye', request.ws_close_reason)
+
+ def test_send_longest_close(self):
+ reason = 'a' * 123
+ request = _create_request(
+ (b'\x88\xfd', struct.pack('!H', common.STATUS_NORMAL_CLOSURE) +
+ reason.encode('UTF-8')))
+ request.ws_stream.close_connection(common.STATUS_NORMAL_CLOSURE,
+ reason)
+ self.assertEqual(request.ws_close_code, common.STATUS_NORMAL_CLOSURE)
+ self.assertEqual(request.ws_close_reason, reason)
+
+ def test_send_close_too_long(self):
+ request = _create_request()
+ self.assertRaises(msgutil.BadOperationException,
+ Stream.close_connection, request.ws_stream,
+ common.STATUS_NORMAL_CLOSURE, 'a' * 124)
+
+ def test_send_close_inconsistent_code_and_reason(self):
+ request = _create_request()
+ # reason parameter must not be specified when code is None.
+ self.assertRaises(msgutil.BadOperationException,
+ Stream.close_connection, request.ws_stream, None,
+ 'a')
+
+ def test_send_ping(self):
+ request = _create_request()
+ msgutil.send_ping(request, 'Hello World!')
+ self.assertEqual(b'\x89\x0cHello World!',
+ request.connection.written_data())
+
+ def test_send_longest_ping(self):
+ request = _create_request()
+ msgutil.send_ping(request, 'a' * 125)
+ self.assertEqual(b'\x89\x7d' + b'a' * 125,
+ request.connection.written_data())
+
+ def test_send_ping_too_long(self):
+ request = _create_request()
+ self.assertRaises(msgutil.BadOperationException, msgutil.send_ping,
+ request, 'a' * 126)
+
+ def test_receive_ping(self):
+ """Tests receiving a ping control frame."""
+ def handler(request, message):
+ request.called = True
+
+ # Stream automatically respond to ping with pong without any action
+ # by application layer.
+ request = _create_request((b'\x89\x85', b'Hello'),
+ (b'\x81\x85', b'World'))
+ self.assertEqual('World', msgutil.receive_message(request))
+ self.assertEqual(b'\x8a\x05Hello', request.connection.written_data())
+
+ request = _create_request((b'\x89\x85', b'Hello'),
+ (b'\x81\x85', b'World'))
+ request.on_ping_handler = handler
+ self.assertEqual('World', msgutil.receive_message(request))
+ self.assertTrue(request.called)
+
+ def test_receive_longest_ping(self):
+ request = _create_request((b'\x89\xfd', b'a' * 125),
+ (b'\x81\x85', b'World'))
+ self.assertEqual('World', msgutil.receive_message(request))
+ self.assertEqual(b'\x8a\x7d' + b'a' * 125,
+ request.connection.written_data())
+
+ def test_receive_ping_too_long(self):
+ request = _create_request((b'\x89\xfe\x00\x7e', b'a' * 126))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message, request)
+
+ def test_receive_pong(self):
+ """Tests receiving a pong control frame."""
+ def handler(request, message):
+ request.called = True
+
+ request = _create_request((b'\x8a\x85', b'Hello'),
+ (b'\x81\x85', b'World'))
+ request.on_pong_handler = handler
+ msgutil.send_ping(request, 'Hello')
+ self.assertEqual(b'\x89\x05Hello', request.connection.written_data())
+ # Valid pong is received, but receive_message won't return for it.
+ self.assertEqual('World', msgutil.receive_message(request))
+ # Check that nothing was written after receive_message call.
+ self.assertEqual(b'\x89\x05Hello', request.connection.written_data())
+
+ self.assertTrue(request.called)
+
+ def test_receive_unsolicited_pong(self):
+ # Unsolicited pong is allowed from HyBi 07.
+ request = _create_request((b'\x8a\x85', b'Hello'),
+ (b'\x81\x85', b'World'))
+ msgutil.receive_message(request)
+
+ request = _create_request((b'\x8a\x85', b'Hello'),
+ (b'\x81\x85', b'World'))
+ msgutil.send_ping(request, 'Jumbo')
+ # Body mismatch.
+ msgutil.receive_message(request)
+
+ def test_ping_cannot_be_fragmented(self):
+ request = _create_request((b'\x09\x85', b'Hello'))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message, request)
+
+ def test_ping_with_too_long_payload(self):
+ request = _create_request((b'\x89\xfe\x01\x00', b'a' * 256))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message, request)
+
+
+class PerMessageDeflateTest(unittest.TestCase):
+ """Tests for permessage-deflate extension."""
+ def test_response_parameters(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ extension.add_parameter('server_no_context_takeover', None)
+ processor = PerMessageDeflateExtensionProcessor(extension)
+ response = processor.get_extension_response()
+ self.assertTrue(response.has_parameter('server_no_context_takeover'))
+ self.assertEqual(
+ None, response.get_parameter_value('server_no_context_takeover'))
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ extension.add_parameter('client_max_window_bits', None)
+ processor = PerMessageDeflateExtensionProcessor(extension)
+
+ processor.set_client_max_window_bits(8)
+ processor.set_client_no_context_takeover(True)
+ response = processor.get_extension_response()
+ self.assertEqual(
+ '8', response.get_parameter_value('client_max_window_bits'))
+ self.assertTrue(response.has_parameter('client_no_context_takeover'))
+ self.assertEqual(
+ None, response.get_parameter_value('client_no_context_takeover'))
+
+ def test_send_message(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ b'', permessage_deflate_request=extension)
+ msgutil.send_message(request, 'Hello')
+
+ compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+ compressed_hello = compress.compress(b'Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ expected = b'\xc1%c' % len(compressed_hello)
+ expected += compressed_hello
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_empty_message(self):
+ """Test that an empty message is compressed correctly."""
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ b'', permessage_deflate_request=extension)
+
+ msgutil.send_message(request, '')
+
+ # Payload in binary: 0b00000000
+ # From LSB,
+ # - 1 bit of BFINAL (0)
+ # - 2 bits of BTYPE (no compression)
+ # - 5 bits of padding
+ self.assertEqual(b'\xc1\x01\x00', request.connection.written_data())
+
+ def test_send_message_with_null_character(self):
+ """Test that a simple payload (one null) is framed correctly."""
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ b'', permessage_deflate_request=extension)
+
+ msgutil.send_message(request, '\x00')
+
+ # Payload in binary: 0b01100010 0b00000000 0b00000000
+ # From LSB,
+ # - 1 bit of BFINAL (0)
+ # - 2 bits of BTYPE (01 that means fixed Huffman)
+ # - 8 bits of the first code (00110000 that is the code for the literal
+ # alphabet 0x00)
+ # - 7 bits of the second code (0000000 that is the code for the
+ # end-of-block)
+ # - 1 bit of BFINAL (0)
+ # - 2 bits of BTYPE (no compression)
+ # - 2 bits of padding
+ self.assertEqual(b'\xc1\x03\x62\x00\x00',
+ request.connection.written_data())
+
+ def test_send_two_messages(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ b'', permessage_deflate_request=extension)
+ msgutil.send_message(request, 'Hello')
+ msgutil.send_message(request, 'World')
+
+ compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+
+ expected = b''
+
+ compressed_hello = compress.compress(b'Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ expected += b'\xc1%c' % len(compressed_hello)
+ expected += compressed_hello
+
+ compressed_world = compress.compress(b'World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ expected += b'\xc1%c' % len(compressed_world)
+ expected += compressed_world
+
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_fragmented(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ b'', permessage_deflate_request=extension)
+ msgutil.send_message(request, 'Hello', end=False)
+ msgutil.send_message(request, 'Goodbye', end=False)
+ msgutil.send_message(request, 'World')
+
+ compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+ compressed_hello = compress.compress(b'Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected = b'\x41%c' % len(compressed_hello)
+ expected += compressed_hello
+ compressed_goodbye = compress.compress(b'Goodbye')
+ compressed_goodbye += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected += b'\x00%c' % len(compressed_goodbye)
+ expected += compressed_goodbye
+ compressed_world = compress.compress(b'World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ expected += b'\x80%c' % len(compressed_world)
+ expected += compressed_world
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_fragmented_empty_first_frame(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ b'', permessage_deflate_request=extension)
+ msgutil.send_message(request, '', end=False)
+ msgutil.send_message(request, 'Hello')
+
+ compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+ compressed_hello = compress.compress(b'')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected = b'\x41%c' % len(compressed_hello)
+ expected += compressed_hello
+ compressed_empty = compress.compress(b'Hello')
+ compressed_empty += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_empty = compressed_empty[:-4]
+ expected += b'\x80%c' % len(compressed_empty)
+ expected += compressed_empty
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_fragmented_empty_last_frame(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ b'', permessage_deflate_request=extension)
+ msgutil.send_message(request, 'Hello', end=False)
+ msgutil.send_message(request, '')
+
+ compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+ compressed_hello = compress.compress(b'Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected = b'\x41%c' % len(compressed_hello)
+ expected += compressed_hello
+ compressed_empty = compress.compress(b'')
+ compressed_empty += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_empty = compressed_empty[:-4]
+ expected += b'\x80%c' % len(compressed_empty)
+ expected += compressed_empty
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_using_small_window(self):
+ common_part = 'abcdefghijklmnopqrstuvwxyz'
+ test_message = common_part + '-' * 30000 + common_part
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ extension.add_parameter('server_max_window_bits', '8')
+ request = _create_request_from_rawdata(
+ b'', permessage_deflate_request=extension)
+ msgutil.send_message(request, test_message)
+
+ expected_websocket_header_size = 2
+ expected_websocket_payload_size = 91
+
+ actual_frame = request.connection.written_data()
+ self.assertEqual(
+ expected_websocket_header_size + expected_websocket_payload_size,
+ len(actual_frame))
+ actual_header = actual_frame[0:expected_websocket_header_size]
+ actual_payload = actual_frame[expected_websocket_header_size:]
+
+ self.assertEqual(b'\xc1%c' % expected_websocket_payload_size,
+ actual_header)
+ decompress = zlib.decompressobj(-8)
+ decompressed_message = decompress.decompress(actual_payload +
+ b'\x00\x00\xff\xff')
+ decompressed_message += decompress.flush()
+ self.assertEqual(test_message, decompressed_message.decode('UTF-8'))
+ self.assertEqual(0, len(decompress.unused_data))
+ self.assertEqual(0, len(decompress.unconsumed_tail))
+
+ def test_send_message_no_context_takeover_parameter(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ extension.add_parameter('server_no_context_takeover', None)
+ request = _create_request_from_rawdata(
+ b'', permessage_deflate_request=extension)
+ for i in range(3):
+ msgutil.send_message(request, 'Hello', end=False)
+ msgutil.send_message(request, 'Hello', end=True)
+
+ compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+
+ first_hello = compress.compress(b'Hello')
+ first_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected = b'\x41%c' % len(first_hello)
+ expected += first_hello
+ second_hello = compress.compress(b'Hello')
+ second_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ second_hello = second_hello[:-4]
+ expected += b'\x80%c' % len(second_hello)
+ expected += second_hello
+
+ self.assertEqual(expected + expected + expected,
+ request.connection.written_data())
+
+ def test_send_message_fragmented_bfinal(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ b'', permessage_deflate_request=extension)
+ self.assertEqual(1, len(request.ws_extension_processors))
+ request.ws_extension_processors[0].set_bfinal(True)
+ msgutil.send_message(request, 'Hello', end=False)
+ msgutil.send_message(request, 'World', end=True)
+
+ expected = b''
+
+ compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+ compressed_hello = compress.compress(b'Hello')
+ compressed_hello += compress.flush(zlib.Z_FINISH)
+ compressed_hello = compressed_hello + struct.pack('!B', 0)
+ expected += b'\x41%c' % len(compressed_hello)
+ expected += compressed_hello
+
+ compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+ compressed_world = compress.compress(b'World')
+ compressed_world += compress.flush(zlib.Z_FINISH)
+ compressed_world = compressed_world + struct.pack('!B', 0)
+ expected += b'\x80%c' % len(compressed_world)
+ expected += compressed_world
+
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_receive_message_deflate(self):
+ compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+
+ compressed_hello = compress.compress(b'Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ data = b'\xc1%c' % (len(compressed_hello) | 0x80)
+ data += _mask_hybi(compressed_hello)
+
+ # Close frame
+ data += b'\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + b'Good bye')
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, permessage_deflate_request=extension)
+ self.assertEqual('Hello', msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+ def test_receive_message_random_section(self):
+ """Test that a compressed message fragmented into lots of chunks is
+ correctly received.
+ """
+
+ random.seed(a=0)
+ payload = b''.join(
+ [struct.pack('!B', random.randint(0, 255)) for i in range(1000)])
+
+ compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+ compressed_payload = compress.compress(payload)
+ compressed_payload += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_payload = compressed_payload[:-4]
+
+ # Fragment the compressed payload into lots of frames.
+ bytes_chunked = 0
+ data = b''
+ frame_count = 0
+
+ chunk_sizes = []
+
+ while bytes_chunked < len(compressed_payload):
+ # Make sure that
+ # - the length of chunks are equal or less than 125 so that we can
+ # use 1 octet length header format for all frames.
+ # - at least 10 chunks are created.
+ chunk_size = random.randint(
+ 1,
+ min(125,
+ len(compressed_payload) // 10,
+ len(compressed_payload) - bytes_chunked))
+ chunk_sizes.append(chunk_size)
+ chunk = compressed_payload[bytes_chunked:bytes_chunked +
+ chunk_size]
+ bytes_chunked += chunk_size
+
+ first_octet = 0x00
+ if len(data) == 0:
+ first_octet = first_octet | 0x42
+ if bytes_chunked == len(compressed_payload):
+ first_octet = first_octet | 0x80
+
+ data += b'%c%c' % (first_octet, chunk_size | 0x80)
+ data += _mask_hybi(chunk)
+
+ frame_count += 1
+
+ self.assertTrue(len(chunk_sizes) > 10)
+
+ # Close frame
+ data += b'\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + b'Good bye')
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, permessage_deflate_request=extension)
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+ def test_receive_two_messages(self):
+ compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+
+ data = b''
+
+ compressed_hello = compress.compress(b'HelloWebSocket')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ split_position = len(compressed_hello) // 2
+ data += b'\x41%c' % (split_position | 0x80)
+ data += _mask_hybi(compressed_hello[:split_position])
+
+ data += b'\x80%c' % ((len(compressed_hello) - split_position) | 0x80)
+ data += _mask_hybi(compressed_hello[split_position:])
+
+ compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+
+ compressed_world = compress.compress(b'World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ data += b'\xc1%c' % (len(compressed_world) | 0x80)
+ data += _mask_hybi(compressed_world)
+
+ # Close frame
+ data += b'\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + b'Good bye')
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, permessage_deflate_request=extension)
+ self.assertEqual('HelloWebSocket', msgutil.receive_message(request))
+ self.assertEqual('World', msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+ def test_receive_message_mixed_btype(self):
+ """Test that a message compressed using lots of DEFLATE blocks with
+ various flush mode is correctly received.
+ """
+
+ random.seed(a=0)
+ payload = b''.join(
+ [struct.pack('!B', random.randint(0, 255)) for i in range(1000)])
+
+ compress = None
+
+ # Fragment the compressed payload into lots of frames.
+ bytes_chunked = 0
+ compressed_payload = b''
+
+ chunk_sizes = []
+ methods = []
+ sync_used = False
+ finish_used = False
+
+ while bytes_chunked < len(payload):
+ # Make sure at least 10 chunks are created.
+ chunk_size = random.randint(1,
+ min(100,
+ len(payload) - bytes_chunked))
+ chunk_sizes.append(chunk_size)
+ chunk = payload[bytes_chunked:bytes_chunked + chunk_size]
+
+ bytes_chunked += chunk_size
+
+ if compress is None:
+ compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
+ zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ if bytes_chunked == len(payload):
+ compressed_payload += compress.compress(chunk)
+ compressed_payload += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_payload = compressed_payload[:-4]
+ else:
+ method = random.randint(0, 1)
+ methods.append(method)
+ if method == 0:
+ compressed_payload += compress.compress(chunk)
+ compressed_payload += compress.flush(zlib.Z_SYNC_FLUSH)
+ sync_used = True
+ else:
+ compressed_payload += compress.compress(chunk)
+ compressed_payload += compress.flush(zlib.Z_FINISH)
+ compress = None
+ finish_used = True
+
+ self.assertTrue(len(chunk_sizes) > 10)
+ self.assertTrue(sync_used)
+ self.assertTrue(finish_used)
+
+ self.assertTrue(125 < len(compressed_payload))
+ self.assertTrue(len(compressed_payload) < 65536)
+ data = b'\xc2\xfe' + struct.pack('!H', len(compressed_payload))
+ data += _mask_hybi(compressed_payload)
+
+ # Close frame
+ data += b'\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + b'Good bye')
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, permessage_deflate_request=extension)
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+
+class MessageReceiverTest(unittest.TestCase):
+ """Tests the Stream class using MessageReceiver."""
+ def test_queue(self):
+ request = _create_blocking_request()
+ receiver = msgutil.MessageReceiver(request)
+
+ self.assertEqual(None, receiver.receive_nowait())
+
+ request.connection.put_bytes(b'\x81\x86' + _mask_hybi(b'Hello!'))
+ self.assertEqual('Hello!', receiver.receive())
+
+ def test_onmessage(self):
+ onmessage_queue = six.moves.queue.Queue()
+
+ def onmessage_handler(message):
+ onmessage_queue.put(message)
+
+ request = _create_blocking_request()
+ receiver = msgutil.MessageReceiver(request, onmessage_handler)
+
+ request.connection.put_bytes(b'\x81\x86' + _mask_hybi(b'Hello!'))
+ self.assertEqual('Hello!', onmessage_queue.get())
+
+
+class MessageSenderTest(unittest.TestCase):
+ """Tests the Stream class using MessageSender."""
+ def test_send(self):
+ request = _create_blocking_request()
+ sender = msgutil.MessageSender(request)
+
+ sender.send('World')
+ self.assertEqual(b'\x81\x05World', request.connection.written_data())
+
+ def test_send_nowait(self):
+ # Use a queue to check the bytes written by MessageSender.
+ # request.connection.written_data() cannot be used here because
+ # MessageSender runs in a separate thread.
+ send_queue = six.moves.queue.Queue()
+
+ def write(bytes):
+ send_queue.put(bytes)
+
+ request = _create_blocking_request()
+ request.connection.write = write
+
+ sender = msgutil.MessageSender(request)
+
+ sender.send_nowait('Hello')
+ sender.send_nowait('World')
+ self.assertEqual(b'\x81\x05Hello', send_queue.get())
+ self.assertEqual(b'\x81\x05World', send_queue.get())
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py
new file mode 100755
index 0000000000..153899d205
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Tests for stream module."""
+
+from __future__ import absolute_import
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import common
+from mod_pywebsocket import stream
+
+
+class StreamTest(unittest.TestCase):
+ """A unittest for stream module."""
+ def test_create_header(self):
+ # more, rsv1, ..., rsv4 are all true
+ header = stream.create_header(common.OPCODE_TEXT, 1, 1, 1, 1, 1, 1)
+ self.assertEqual(b'\xf1\x81', header)
+
+ # Maximum payload size
+ header = stream.create_header(common.OPCODE_TEXT, (1 << 63) - 1, 0, 0,
+ 0, 0, 0)
+ self.assertEqual(b'\x01\x7f\x7f\xff\xff\xff\xff\xff\xff\xff', header)
+
+ # Invalid opcode 0x10
+ self.assertRaises(ValueError, stream.create_header, 0x10, 0, 0, 0, 0,
+ 0, 0)
+
+ # Invalid value 0xf passed to more parameter
+ self.assertRaises(ValueError, stream.create_header, common.OPCODE_TEXT,
+ 0, 0xf, 0, 0, 0, 0)
+
+ # Too long payload_length
+ self.assertRaises(ValueError, stream.create_header, common.OPCODE_TEXT,
+ 1 << 63, 0, 0, 0, 0, 0)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py
new file mode 100755
index 0000000000..bf4bd32bba
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py
@@ -0,0 +1,191 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Tests for util module."""
+
+from __future__ import absolute_import
+from __future__ import print_function
+import os
+import random
+import sys
+import unittest
+import struct
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import util
+from six.moves import range
+from six import PY3
+from six import int2byte
+
+_TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'testdata')
+
+
+class UtilTest(unittest.TestCase):
+ """A unittest for util module."""
+ def test_prepend_message_to_exception(self):
+ exc = Exception('World')
+ self.assertEqual('World', str(exc))
+ util.prepend_message_to_exception('Hello ', exc)
+ self.assertEqual('Hello World', str(exc))
+
+ def test_get_script_interp(self):
+ cygwin_path = 'c:\\cygwin\\bin'
+ cygwin_perl = os.path.join(cygwin_path, 'perl')
+ self.assertEqual(
+ None, util.get_script_interp(os.path.join(_TEST_DATA_DIR,
+ 'README')))
+ self.assertEqual(
+ None,
+ util.get_script_interp(os.path.join(_TEST_DATA_DIR, 'README'),
+ cygwin_path))
+ self.assertEqual(
+ '/usr/bin/perl -wT',
+ util.get_script_interp(os.path.join(_TEST_DATA_DIR, 'hello.pl')))
+ self.assertEqual(
+ cygwin_perl + ' -wT',
+ util.get_script_interp(os.path.join(_TEST_DATA_DIR, 'hello.pl'),
+ cygwin_path))
+
+ def test_hexify(self):
+ self.assertEqual('61 7a 41 5a 30 39 20 09 0d 0a 00 ff',
+ util.hexify(b'azAZ09 \t\r\n\x00\xff'))
+
+
+class RepeatedXorMaskerTest(unittest.TestCase):
+ """A unittest for RepeatedXorMasker class."""
+ def test_mask(self):
+ # Sample input e6,97,a5 is U+65e5 in UTF-8
+ masker = util.RepeatedXorMasker(b'\xff\xff\xff\xff')
+ result = masker.mask(b'\xe6\x97\xa5')
+ self.assertEqual(b'\x19\x68\x5a', result)
+
+ masker = util.RepeatedXorMasker(b'\x00\x00\x00\x00')
+ result = masker.mask(b'\xe6\x97\xa5')
+ self.assertEqual(b'\xe6\x97\xa5', result)
+
+ masker = util.RepeatedXorMasker(b'\xe6\x97\xa5\x20')
+ result = masker.mask(b'\xe6\x97\xa5')
+ self.assertEqual(b'\x00\x00\x00', result)
+
+ def test_mask_twice(self):
+ masker = util.RepeatedXorMasker(b'\x00\x7f\xff\x20')
+ # mask[0], mask[1], ... will be used.
+ result = masker.mask(b'\x00\x00\x00\x00\x00')
+ self.assertEqual(b'\x00\x7f\xff\x20\x00', result)
+ # mask[2], mask[0], ... will be used for the next call.
+ result = masker.mask(b'\x00\x00\x00\x00\x00')
+ self.assertEqual(b'\x7f\xff\x20\x00\x7f', result)
+
+ def test_mask_large_data(self):
+ masker = util.RepeatedXorMasker(b'mASk')
+ original = b''.join([util.pack_byte(i % 256) for i in range(1000)])
+ result = masker.mask(original)
+ expected = b''.join([
+ util.pack_byte((i % 256) ^ ord('mASk'[i % 4])) for i in range(1000)
+ ])
+ self.assertEqual(expected, result)
+
+ masker = util.RepeatedXorMasker(b'MaSk')
+ first_part = b'The WebSocket Protocol enables two-way communication.'
+ result = masker.mask(first_part)
+ self.assertEqual(
+ b'\x19\t6K\x1a\x0418"\x028\x0e9A\x03\x19"\x15<\x08"\rs\x0e#'
+ b'\x001\x07(\x12s\x1f:\x0e~\x1c,\x18s\x08"\x0c>\x1e#\x080\n9'
+ b'\x08<\x05c', result)
+ second_part = b'It has two parts: a handshake and the data transfer.'
+ result = masker.mask(second_part)
+ self.assertEqual(
+ b"('K%\x00 K9\x16<K=\x00!\x1f>[s\nm\t2\x05)\x12;\n&\x04s\n#"
+ b"\x05s\x1f%\x04s\x0f,\x152K9\x132\x05>\x076\x19c", result)
+
+
+def get_random_section(source, min_num_chunks):
+ chunks = []
+ bytes_chunked = 0
+
+ while bytes_chunked < len(source):
+ chunk_size = random.randint(
+ 1, min(len(source) / min_num_chunks,
+ len(source) - bytes_chunked))
+ chunk = source[bytes_chunked:bytes_chunked + chunk_size]
+ chunks.append(chunk)
+ bytes_chunked += chunk_size
+
+ return chunks
+
+
+class InflaterDeflaterTest(unittest.TestCase):
+ """A unittest for _Inflater and _Deflater class."""
+ def test_inflate_deflate_default(self):
+ input = b'hello' + b'-' * 30000 + b'hello'
+ inflater15 = util._Inflater(15)
+ deflater15 = util._Deflater(15)
+ inflater8 = util._Inflater(8)
+ deflater8 = util._Deflater(8)
+
+ compressed15 = deflater15.compress_and_finish(input)
+ compressed8 = deflater8.compress_and_finish(input)
+
+ inflater15.append(compressed15)
+ inflater8.append(compressed8)
+
+ self.assertNotEqual(compressed15, compressed8)
+ self.assertEqual(input, inflater15.decompress(-1))
+ self.assertEqual(input, inflater8.decompress(-1))
+
+ def test_random_section(self):
+ random.seed(a=0)
+ source = b''.join(
+ [int2byte(random.randint(0, 255)) for i in range(100 * 1024)])
+
+ chunked_input = get_random_section(source, 10)
+
+ deflater = util._Deflater(15)
+ compressed = []
+ for chunk in chunked_input:
+ compressed.append(deflater.compress(chunk))
+ compressed.append(deflater.compress_and_finish(b''))
+
+ chunked_expectation = get_random_section(source, 10)
+
+ inflater = util._Inflater(15)
+ inflater.append(b''.join(compressed))
+ for chunk in chunked_expectation:
+ decompressed = inflater.decompress(len(chunk))
+ self.assertEqual(chunk, decompressed)
+
+ self.assertEqual(b'', inflater.decompress(-1))
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README
new file mode 100644
index 0000000000..c001aa5595
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README
@@ -0,0 +1 @@
+Test data directory
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py
new file mode 100644
index 0000000000..63cb541bb7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py
@@ -0,0 +1,41 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from mod_pywebsocket import handshake
+
+
+def web_socket_do_extra_handshake(request):
+ raise handshake.AbortedByUserException("abort for test")
+
+
+def web_socket_transfer_data(request):
+ raise handshake.AbortedByUserException("abort for test")
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/blank_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/blank_wsh.py
new file mode 100644
index 0000000000..b398e96778
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/blank_wsh.py
@@ -0,0 +1,30 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# intentionally left blank
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/origin_check_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/origin_check_wsh.py
new file mode 100644
index 0000000000..bf6442e65b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/origin_check_wsh.py
@@ -0,0 +1,43 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+def web_socket_do_extra_handshake(request):
+ if request.ws_origin == 'http://example.com':
+ return
+ raise ValueError('Unacceptable origin: %r' % request.ws_origin)
+
+
+def web_socket_transfer_data(request):
+ message = 'origin_check_wsh.py is called for %s, %s' % (
+ request.ws_resource, request.ws_protocol)
+ request.connection.write(message.encode('UTF-8'))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/exception_in_transfer_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/exception_in_transfer_wsh.py
new file mode 100644
index 0000000000..54390994d7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/exception_in_transfer_wsh.py
@@ -0,0 +1,42 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Exception in web_socket_transfer_data().
+"""
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ raise Exception('Intentional Exception for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/no_wsh_at_the_end.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/no_wsh_at_the_end.py
new file mode 100644
index 0000000000..ebfddb7449
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/no_wsh_at_the_end.py
@@ -0,0 +1,43 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Correct signatures, wrong file name.
+"""
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ message = 'sub/no_wsh_at_the_end.py is called for %s, %s' % (
+ request.ws_resource, request.ws_protocol)
+ request.connection.write(message.encode('UTF-8'))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/non_callable_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/non_callable_wsh.py
new file mode 100644
index 0000000000..8afcfa97a9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/non_callable_wsh.py
@@ -0,0 +1,35 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Non-callable handlers.
+"""
+
+web_socket_do_extra_handshake = True
+web_socket_transfer_data = 1
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/plain_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/plain_wsh.py
new file mode 100644
index 0000000000..8a7db1e5ac
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/plain_wsh.py
@@ -0,0 +1,41 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ message = 'sub/plain_wsh.py is called for %s, %s' % (request.ws_resource,
+ request.ws_protocol)
+ request.connection.write(message.encode('UTF-8'))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py
new file mode 100644
index 0000000000..cebb0da1ab
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py
@@ -0,0 +1,43 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Wrong web_socket_do_extra_handshake signature.
+"""
+
+
+def no_web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ message = 'sub/wrong_handshake_sig_wsh.py is called for %s, %s' % (
+ request.ws_resource, request.ws_protocol)
+ request.connection.write(message.encode('UTF-8'))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py
new file mode 100644
index 0000000000..ad27d6bcfe
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py
@@ -0,0 +1,43 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Wrong web_socket_transfer_data() signature.
+"""
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def no_web_socket_transfer_data(request):
+ message = 'sub/wrong_transfer_sig_wsh.py is called for %s, %s' % (
+ request.ws_resource, request.ws_protocol)
+ request.connection.write(message.encode('UTF-8'))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/hello.pl b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/hello.pl
new file mode 100644
index 0000000000..882ef5a100
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/hello.pl
@@ -0,0 +1,32 @@
+#!/usr/bin/perl -wT
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+print "Hello\n";
diff --git a/testing/web-platform/tests/tools/third_party/six/CHANGES b/testing/web-platform/tests/tools/third_party/six/CHANGES
new file mode 100644
index 0000000000..ffa702601b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/CHANGES
@@ -0,0 +1,315 @@
+Changelog for six
+=================
+
+This file lists the changes in each six version.
+
+1.13.0
+------
+
+- Issue #298, pull request #299: Add `six.moves.dbm_ndbm`.
+
+- Issue #155: Add `six.moves.collections_abc`, which aliases the `collections`
+ module on Python 2-3.2 and the `collections.abc` on Python 3.3 and greater.
+
+- Pull request #304: Re-add distutils fallback in `setup.py`.
+
+- Pull request #305: On Python 3.7, `with_metaclass` supports classes using PEP
+ 560 features.
+
+1.12.0
+------
+
+- Issue #259, pull request #260: `six.add_metaclass` now preserves
+ `__qualname__` from the original class.
+
+- Pull request #204: Add `six.ensure_binary`, `six.ensure_text`, and
+ `six.ensure_str`.
+
+1.11.0
+------
+
+- Pull request #178: `with_metaclass` now properly proxies `__prepare__` to the
+ underlying metaclass.
+
+- Pull request #191: Allow `with_metaclass` to work with metaclasses implemented
+ in C.
+
+- Pull request #203: Add parse_http_list and parse_keqv_list to moved
+ urllib.request.
+
+- Pull request #172 and issue #171: Add unquote_to_bytes to moved urllib.parse.
+
+- Pull request #167: Add `six.moves.getoutput`.
+
+- Pull request #80: Add `six.moves.urllib_parse.splitvalue`.
+
+- Pull request #75: Add `six.moves.email_mime_image`.
+
+- Pull request #72: Avoid creating reference cycles through tracebacks in
+ `reraise`.
+
+1.10.0
+------
+
+- Issue #122: Improve the performance of `six.int2byte` on Python 3.
+
+- Pull request #55 and issue #99: Don't add the `winreg` module to `six.moves`
+ on non-Windows platforms.
+
+- Pull request #60 and issue #108: Add `six.moves.getcwd` and
+ `six.moves.getcwdu`.
+
+- Pull request #64: Add `create_unbound_method` to create unbound methods.
+
+1.9.0
+-----
+
+- Issue #106: Support the `flush` parameter to `six.print_`.
+
+- Pull request #48 and issue #15: Add the `python_2_unicode_compatible`
+ decorator.
+
+- Pull request #57 and issue #50: Add several compatibility methods for unittest
+ assertions that were renamed between Python 2 and 3.
+
+- Issue #105 and pull request #58: Ensure `six.wraps` respects the *updated* and
+ *assigned* arguments.
+
+- Issue #102: Add `raise_from` to abstract out Python 3's raise from syntax.
+
+- Issue #97: Optimize `six.iterbytes` on Python 2.
+
+- Issue #98: Fix `six.moves` race condition in multi-threaded code.
+
+- Pull request #51: Add `six.view(keys|values|itmes)`, which provide dictionary
+ views on Python 2.7+.
+
+- Issue #112: `six.moves.reload_module` now uses the importlib module on
+ Python 3.4+.
+
+1.8.0
+-----
+
+- Issue #90: Add `six.moves.shlex_quote`.
+
+- Issue #59: Add `six.moves.intern`.
+
+- Add `six.urllib.parse.uses_(fragment|netloc|params|query|relative)`.
+
+- Issue #88: Fix add_metaclass when the class has `__slots__` containing
+ `__weakref__` or `__dict__`.
+
+- Issue #89: Make six use absolute imports.
+
+- Issue #85: Always accept *updated* and *assigned* arguments for `wraps()`.
+
+- Issue #86: In `reraise()`, instantiate the exception if the second argument is
+ `None`.
+
+- Pull request #45: Add `six.moves.email_mime_nonmultipart`.
+
+- Issue #81: Add `six.urllib.request.splittag` mapping.
+
+- Issue #80: Add `six.urllib.request.splituser` mapping.
+
+1.7.3
+-----
+
+- Issue #77: Fix import six on Python 3.4 with a custom loader.
+
+- Issue #74: `six.moves.xmlrpc_server` should map to `SimpleXMLRPCServer` on Python
+ 2 as documented not `xmlrpclib`.
+
+1.7.2
+-----
+
+- Issue #72: Fix installing on Python 2.
+
+1.7.1
+-----
+
+- Issue #71: Make the six.moves meta path importer handle reloading of the six
+ module gracefully.
+
+1.7.0
+-----
+
+- Pull request #30: Implement six.moves with a PEP 302 meta path hook.
+
+- Pull request #32: Add six.wraps, which is like functools.wraps but always sets
+ the __wrapped__ attribute.
+
+- Pull request #35: Improve add_metaclass, so that it doesn't end up inserting
+ another class into the hierarchy.
+
+- Pull request #34: Add import mappings for dummy_thread.
+
+- Pull request #33: Add import mappings for UserDict and UserList.
+
+- Pull request #31: Select the implementations of dictionary iterator routines
+ at import time for a 20% speed boost.
+
+1.6.1
+-----
+
+- Raise an AttributeError for six.moves.X when X is a module not available in
+ the current interpreter.
+
+1.6.0
+-----
+
+- Raise an AttributeError for every attribute of unimportable modules.
+
+- Issue #56: Make the fake modules six.moves puts into sys.modules appear not to
+ have a __path__ unless they are loaded.
+
+- Pull request #28: Add support for SplitResult.
+
+- Issue #55: Add move mapping for xmlrpc.server.
+
+- Pull request #29: Add move for urllib.parse.splitquery.
+
+1.5.2
+-----
+
+- Issue #53: Make the fake modules six.moves puts into sys.modules appear not to
+ have a __name__ unless they are loaded.
+
+1.5.1
+-----
+
+- Issue #51: Hack around the Django autoreloader after recent six.moves changes.
+
+1.5.0
+-----
+
+- Removed support for Python 2.4. This is because py.test no longer supports
+ 2.4.
+
+- Fix various import problems including issues #19 and #41. six.moves modules
+ are now lazy wrappers over the underlying modules instead of the actual
+ modules themselves.
+
+- Issue #49: Add six.moves mapping for tkinter.ttk.
+
+- Pull request #24: Add __dir__ special method to six.moves modules.
+
+- Issue #47: Fix add_metaclass on classes with a string for the __slots__
+ variable.
+
+- Issue #44: Fix interpretation of backslashes on Python 2 in the u() function.
+
+- Pull request #21: Add import mapping for urllib's proxy_bypass function.
+
+- Issue #43: Add import mapping for the Python 2 xmlrpclib module.
+
+- Issue #39: Add import mapping for the Python 2 thread module.
+
+- Issue #40: Add import mapping for the Python 2 gdbm module.
+
+- Issue #35: On Python versions less than 2.7, print_ now encodes unicode
+ strings when outputing to standard streams. (Python 2.7 handles this
+ automatically.)
+
+1.4.1
+-----
+
+- Issue #32: urllib module wrappings don't work when six is not a toplevel file.
+
+1.4.0
+-----
+
+- Issue #31: Add six.moves mapping for UserString.
+
+- Pull request #12: Add six.add_metaclass, a decorator for adding a metaclass to
+ a class.
+
+- Add six.moves.zip_longest and six.moves.filterfalse, which correspond
+ respectively to itertools.izip_longest and itertools.ifilterfalse on Python 2
+ and itertools.zip_longest and itertools.filterfalse on Python 3.
+
+- Issue #25: Add the unichr function, which returns a string for a Unicode
+ codepoint.
+
+- Issue #26: Add byte2int function, which complements int2byte.
+
+- Add a PY2 constant with obvious semantics.
+
+- Add helpers for indexing and iterating over bytes: iterbytes and indexbytes.
+
+- Add create_bound_method() wrapper.
+
+- Issue #23: Allow multiple base classes to be passed to with_metaclass.
+
+- Issue #24: Add six.moves.range alias. This exactly the same as the current
+ xrange alias.
+
+- Pull request #5: Create six.moves.urllib, which contains abstractions for a
+ bunch of things which are in urllib in Python 3 and spread out across urllib,
+ urllib2, and urlparse in Python 2.
+
+1.3.0
+-----
+
+- Issue #21: Add methods to access the closure and globals of a function.
+
+- In six.iter(items/keys/values/lists), passed keyword arguments through to the
+ underlying method.
+
+- Add six.iterlists().
+
+- Issue #20: Fix tests if tkinter is not available.
+
+- Issue #17: Define callable to be builtin callable when it is available again
+ in Python 3.2+.
+
+- Issue #16: Rename Python 2 exec_'s arguments, so casually calling exec_ with
+ keyword arguments will raise.
+
+- Issue #14: Put the six.moves package in sys.modules based on the name six is
+ imported under.
+
+- Fix Jython detection.
+
+- Pull request #4: Add email_mime_multipart, email_mime_text, and
+ email_mime_base to six.moves.
+
+1.2.0
+-----
+
+- Issue #13: Make iterkeys/itervalues/iteritems return iterators on Python 3
+ instead of iterables.
+
+- Issue #11: Fix maxsize support on Jython.
+
+- Add six.next() as an alias for six.advance_iterator().
+
+- Use the builtin next() function for advance_iterator() where is available
+ (2.6+), not just Python 3.
+
+- Add the Iterator class for writing portable iterators.
+
+1.1.0
+-----
+
+- Add the int2byte function.
+
+- Add compatibility mappings for iterators over the keys, values, and items of a
+ dictionary.
+
+- Fix six.MAXSIZE on platforms where sizeof(long) != sizeof(Py_ssize_t).
+
+- Issue #3: Add six.moves mappings for filter, map, and zip.
+
+1.0.0
+-----
+
+- Issue #2: u() on Python 2.x now resolves unicode escapes.
+
+- Expose an API for adding mappings to six.moves.
+
+1.0 beta 1
+----------
+
+- Reworked six into one .py file. This breaks imports. Please tell me if you
+ are interested in an import compatibility layer.
diff --git a/testing/web-platform/tests/tools/third_party/six/LICENSE b/testing/web-platform/tests/tools/third_party/six/LICENSE
new file mode 100644
index 0000000000..4b05a54526
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/LICENSE
@@ -0,0 +1,18 @@
+Copyright (c) 2010-2019 Benjamin Peterson
+
+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/testing/web-platform/tests/tools/third_party/six/MANIFEST.in b/testing/web-platform/tests/tools/third_party/six/MANIFEST.in
new file mode 100644
index 0000000000..b924e068ee
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/MANIFEST.in
@@ -0,0 +1,6 @@
+include CHANGES
+include LICENSE
+include test_six.py
+
+recursive-include documentation *
+prune documentation/_build
diff --git a/testing/web-platform/tests/tools/third_party/six/README.rst b/testing/web-platform/tests/tools/third_party/six/README.rst
new file mode 100644
index 0000000000..a99e6f5585
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/README.rst
@@ -0,0 +1,32 @@
+.. image:: https://img.shields.io/pypi/v/six.svg
+ :target: https://pypi.org/project/six/
+ :alt: six on PyPI
+
+.. image:: https://travis-ci.org/benjaminp/six.svg?branch=master
+ :target: https://travis-ci.org/benjaminp/six
+ :alt: six on TravisCI
+
+.. image:: https://readthedocs.org/projects/six/badge/?version=latest
+ :target: https://six.readthedocs.io/
+ :alt: six's documentation on Read the Docs
+
+.. image:: https://img.shields.io/badge/license-MIT-green.svg
+ :target: https://github.com/benjaminp/six/blob/master/LICENSE
+ :alt: MIT License badge
+
+Six is a Python 2 and 3 compatibility library. It provides utility functions
+for smoothing over the differences between the Python versions with the goal of
+writing Python code that is compatible on both Python versions. See the
+documentation for more information on what is provided.
+
+Six supports every Python version since 2.6. It is contained in only one Python
+file, so it can be easily copied into your project. (The copyright and license
+notice must be retained.)
+
+Online documentation is at https://six.readthedocs.io/.
+
+Bugs can be reported to https://github.com/benjaminp/six. The code can also
+be found there.
+
+For questions about six or porting in general, email the python-porting mailing
+list: https://mail.python.org/mailman/listinfo/python-porting
diff --git a/testing/web-platform/tests/tools/third_party/six/documentation/Makefile b/testing/web-platform/tests/tools/third_party/six/documentation/Makefile
new file mode 100644
index 0000000000..eebafcd6d6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/documentation/Makefile
@@ -0,0 +1,130 @@
+# 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)/*
+
+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/six.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/six.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/six"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/six"
+ @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/testing/web-platform/tests/tools/third_party/six/documentation/conf.py b/testing/web-platform/tests/tools/third_party/six/documentation/conf.py
new file mode 100644
index 0000000000..b3d1328adc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/documentation/conf.py
@@ -0,0 +1,217 @@
+# -*- coding: utf-8 -*-
+#
+# six documentation build configuration file
+
+import os
+import sys
+
+# 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.append(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.intersphinx"]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# The suffix of source filenames.
+source_suffix = ".rst"
+
+# The encoding of source files.
+#source_encoding = "utf-8-sig"
+
+# The master toctree document.
+master_doc = "index"
+
+# General information about the project.
+project = u"six"
+copyright = u"2010-2019, Benjamin Peterson"
+
+sys.path.append(os.path.abspath(os.path.join(".", "..")))
+from six import __version__ as six_version
+sys.path.pop()
+
+# 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.
+version = six_version[:-2]
+# The full version, including alpha/beta/rc tags.
+release = six_version
+
+# 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 = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'sixdoc'
+
+
+# -- 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", "six.tex", u"six Documentation",
+ u"Benjamin Peterson", "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", "six", u"six Documentation",
+ [u"Benjamin Peterson"], 1)
+]
+
+# -- Intersphinx ---------------------------------------------------------------
+
+intersphinx_mapping = {"py2" : ("https://docs.python.org/2/", None),
+ "py3" : ("https://docs.python.org/3/", None)}
diff --git a/testing/web-platform/tests/tools/third_party/six/documentation/index.rst b/testing/web-platform/tests/tools/third_party/six/documentation/index.rst
new file mode 100644
index 0000000000..b7ec2754ec
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/documentation/index.rst
@@ -0,0 +1,875 @@
+Six: Python 2 and 3 Compatibility Library
+=========================================
+
+.. module:: six
+ :synopsis: Python 2 and 3 compatibility
+
+.. moduleauthor:: Benjamin Peterson <benjamin@python.org>
+.. sectionauthor:: Benjamin Peterson <benjamin@python.org>
+
+
+Six provides simple utilities for wrapping over differences between Python 2 and
+Python 3. It is intended to support codebases that work on both Python 2 and 3
+without modification. six consists of only one Python file, so it is painless
+to copy into a project.
+
+Six can be downloaded on `PyPI <https://pypi.org/project/six/>`_. Its bug
+tracker and code hosting is on `GitHub <https://github.com/benjaminp/six>`_.
+
+The name, "six", comes from the fact that 2*3 equals 6. Why not addition?
+Multiplication is more powerful, and, anyway, "five" has already been snatched
+away by the (admittedly now moribund) Zope Five project.
+
+
+Indices and tables
+------------------
+
+* :ref:`genindex`
+* :ref:`search`
+
+
+Package contents
+----------------
+
+.. data:: PY2
+
+ A boolean indicating if the code is running on Python 2.
+
+.. data:: PY3
+
+ A boolean indicating if the code is running on Python 3.
+
+
+Constants
+>>>>>>>>>
+
+Six provides constants that may differ between Python versions. Ones ending
+``_types`` are mostly useful as the second argument to ``isinstance`` or
+``issubclass``.
+
+
+.. data:: class_types
+
+ Possible class types. In Python 2, this encompasses old-style
+ :data:`py2:types.ClassType` and new-style ``type`` classes. In Python 3,
+ this is just ``type``.
+
+
+.. data:: integer_types
+
+ Possible integer types. In Python 2, this is :func:`py2:long` and
+ :func:`py2:int`, and in Python 3, just :func:`py3:int`.
+
+
+.. data:: string_types
+
+ Possible types for text data. This is :func:`py2:basestring` in Python 2 and
+ :func:`py3:str` in Python 3.
+
+
+.. data:: text_type
+
+ Type for representing (Unicode) textual data. This is :func:`py2:unicode` in
+ Python 2 and :func:`py3:str` in Python 3.
+
+
+.. data:: binary_type
+
+ Type for representing binary data. This is :func:`py2:str` in Python 2 and
+ :func:`py3:bytes` in Python 3. Python 2.6 and 2.7 include ``bytes`` as a
+ builtin alias of ``str``, so six’s version is only necessary for Python 2.5
+ compatibility.
+
+
+.. data:: MAXSIZE
+
+ The maximum size of a container like :func:`py3:list` or :func:`py3:dict`.
+ This is equivalent to :data:`py3:sys.maxsize` in Python 2.6 and later
+ (including 3.x). Note, this is temptingly similar to, but not the same as
+ :data:`py2:sys.maxint` in Python 2. There is no direct equivalent to
+ :data:`py2:sys.maxint` in Python 3 because its integer type has no limits
+ aside from memory.
+
+
+Here's example usage of the module::
+
+ import six
+
+ def dispatch_types(value):
+ if isinstance(value, six.integer_types):
+ handle_integer(value)
+ elif isinstance(value, six.class_types):
+ handle_class(value)
+ elif isinstance(value, six.string_types):
+ handle_string(value)
+
+
+Object model compatibility
+>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+Python 3 renamed the attributes of several interpreter data structures. The
+following accessors are available. Note that the recommended way to inspect
+functions and methods is the stdlib :mod:`py3:inspect` module.
+
+
+.. function:: get_unbound_function(meth)
+
+ Get the function out of unbound method *meth*. In Python 3, unbound methods
+ don't exist, so this function just returns *meth* unchanged. Example
+ usage::
+
+ from six import get_unbound_function
+
+ class X(object):
+ def method(self):
+ pass
+ method_function = get_unbound_function(X.method)
+
+
+.. function:: get_method_function(meth)
+
+ Get the function out of method object *meth*.
+
+
+.. function:: get_method_self(meth)
+
+ Get the ``self`` of bound method *meth*.
+
+
+.. function:: get_function_closure(func)
+
+ Get the closure (list of cells) associated with *func*. This is equivalent
+ to ``func.__closure__`` on Python 2.6+ and ``func.func_closure`` on Python
+ 2.5.
+
+
+.. function:: get_function_code(func)
+
+ Get the code object associated with *func*. This is equivalent to
+ ``func.__code__`` on Python 2.6+ and ``func.func_code`` on Python 2.5.
+
+
+.. function:: get_function_defaults(func)
+
+ Get the defaults tuple associated with *func*. This is equivalent to
+ ``func.__defaults__`` on Python 2.6+ and ``func.func_defaults`` on Python
+ 2.5.
+
+
+.. function:: get_function_globals(func)
+
+ Get the globals of *func*. This is equivalent to ``func.__globals__`` on
+ Python 2.6+ and ``func.func_globals`` on Python 2.5.
+
+
+.. function:: next(it)
+ advance_iterator(it)
+
+ Get the next item of iterator *it*. :exc:`py3:StopIteration` is raised if
+ the iterator is exhausted. This is a replacement for calling ``it.next()``
+ in Python 2 and ``next(it)`` in Python 3. Python 2.6 and above have a
+ builtin ``next`` function, so six's version is only necessary for Python 2.5
+ compatibility.
+
+
+.. function:: callable(obj)
+
+ Check if *obj* can be called. Note ``callable`` has returned in Python 3.2,
+ so using six's version is only necessary when supporting Python 3.0 or 3.1.
+
+
+.. function:: iterkeys(dictionary, **kwargs)
+
+ Returns an iterator over *dictionary*\'s keys. This replaces
+ ``dictionary.iterkeys()`` on Python 2 and ``dictionary.keys()`` on
+ Python 3. *kwargs* are passed through to the underlying method.
+
+
+.. function:: itervalues(dictionary, **kwargs)
+
+ Returns an iterator over *dictionary*\'s values. This replaces
+ ``dictionary.itervalues()`` on Python 2 and ``dictionary.values()`` on
+ Python 3. *kwargs* are passed through to the underlying method.
+
+
+.. function:: iteritems(dictionary, **kwargs)
+
+ Returns an iterator over *dictionary*\'s items. This replaces
+ ``dictionary.iteritems()`` on Python 2 and ``dictionary.items()`` on
+ Python 3. *kwargs* are passed through to the underlying method.
+
+
+.. function:: iterlists(dictionary, **kwargs)
+
+ Calls ``dictionary.iterlists()`` on Python 2 and ``dictionary.lists()`` on
+ Python 3. No builtin Python mapping type has such a method; this method is
+ intended for use with multi-valued dictionaries like `Werkzeug's
+ <http://werkzeug.pocoo.org/docs/datastructures/#werkzeug.datastructures.MultiDict>`_.
+ *kwargs* are passed through to the underlying method.
+
+
+.. function:: viewkeys(dictionary)
+
+ Return a view over *dictionary*\'s keys. This replaces
+ :meth:`py2:dict.viewkeys` on Python 2.7 and :meth:`py3:dict.keys` on
+ Python 3.
+
+
+.. function:: viewvalues(dictionary)
+
+ Return a view over *dictionary*\'s values. This replaces
+ :meth:`py2:dict.viewvalues` on Python 2.7 and :meth:`py3:dict.values` on
+ Python 3.
+
+
+.. function:: viewitems(dictionary)
+
+ Return a view over *dictionary*\'s items. This replaces
+ :meth:`py2:dict.viewitems` on Python 2.7 and :meth:`py3:dict.items` on
+ Python 3.
+
+
+.. function:: create_bound_method(func, obj)
+
+ Return a method object wrapping *func* and bound to *obj*. On both Python 2
+ and 3, this will return a :func:`py3:types.MethodType` object. The reason
+ this wrapper exists is that on Python 2, the ``MethodType`` constructor
+ requires the *obj*'s class to be passed.
+
+
+.. function:: create_unbound_method(func, cls)
+
+ Return an unbound method object wrapping *func*. In Python 2, this will
+ return a :func:`py2:types.MethodType` object. In Python 3, unbound methods
+ do not exist and this wrapper will simply return *func*.
+
+
+.. class:: Iterator
+
+ A class for making portable iterators. The intention is that it be subclassed
+ and subclasses provide a ``__next__`` method. In Python 2, :class:`Iterator`
+ has one method: ``next``. It simply delegates to ``__next__``. An alternate
+ way to do this would be to simply alias ``next`` to ``__next__``. However,
+ this interacts badly with subclasses that override
+ ``__next__``. :class:`Iterator` is empty on Python 3. (In fact, it is just
+ aliased to :class:`py3:object`.)
+
+
+.. decorator:: wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, updated=functools.WRAPPER_UPDATES)
+
+ This is exactly the :func:`py3:functools.wraps` decorator, but it sets the
+ ``__wrapped__`` attribute on what it decorates as :func:`py3:functools.wraps`
+ does on Python versions after 3.2.
+
+
+Syntax compatibility
+>>>>>>>>>>>>>>>>>>>>
+
+These functions smooth over operations which have different syntaxes between
+Python 2 and 3.
+
+
+.. function:: exec_(code, globals=None, locals=None)
+
+ Execute *code* in the scope of *globals* and *locals*. *code* can be a
+ string or a code object. If *globals* or *locals* are not given, they will
+ default to the scope of the caller. If just *globals* is given, it will also
+ be used as *locals*.
+
+ .. note::
+
+ Python 3's :func:`py3:exec` doesn't take keyword arguments, so calling
+ :func:`exec` with them should be avoided.
+
+
+.. function:: print_(*args, *, file=sys.stdout, end="\\n", sep=" ", flush=False)
+
+ Print *args* into *file*. Each argument will be separated with *sep* and
+ *end* will be written to the file after the last argument is printed. If
+ *flush* is true, ``file.flush()`` will be called after all data is written.
+
+ .. note::
+
+ In Python 2, this function imitates Python 3's :func:`py3:print` by not
+ having softspace support. If you don't know what that is, you're probably
+ ok. :)
+
+
+.. function:: raise_from(exc_value, exc_value_from)
+
+ Raise an exception from a context. On Python 3, this is equivalent to
+ ``raise exc_value from exc_value_from``. On Python 2, which does not support
+ exception chaining, it is equivalent to ``raise exc_value``.
+
+
+.. function:: reraise(exc_type, exc_value, exc_traceback=None)
+
+ Reraise an exception, possibly with a different traceback. In the simple
+ case, ``reraise(*sys.exc_info())`` with an active exception (in an except
+ block) reraises the current exception with the last traceback. A different
+ traceback can be specified with the *exc_traceback* parameter. Note that
+ since the exception reraising is done within the :func:`reraise` function,
+ Python will attach the call frame of :func:`reraise` to whatever traceback is
+ raised.
+
+
+.. function:: with_metaclass(metaclass, *bases)
+
+ Create a new class with base classes *bases* and metaclass *metaclass*. This
+ is designed to be used in class declarations like this: ::
+
+ from six import with_metaclass
+
+ class Meta(type):
+ pass
+
+ class Base(object):
+ pass
+
+ class MyClass(with_metaclass(Meta, Base)):
+ pass
+
+ Another way to set a metaclass on a class is with the :func:`add_metaclass`
+ decorator.
+
+
+.. decorator:: add_metaclass(metaclass)
+
+ Class decorator that replaces a normally-constructed class with a
+ metaclass-constructed one. Example usage: ::
+
+ @add_metaclass(Meta)
+ class MyClass(object):
+ pass
+
+ That code produces a class equivalent to ::
+
+ class MyClass(object, metaclass=Meta):
+ pass
+
+ on Python 3 or ::
+
+ class MyClass(object):
+ __metaclass__ = Meta
+
+ on Python 2.
+
+ Note that class decorators require Python 2.6. However, the effect of the
+ decorator can be emulated on Python 2.5 like so::
+
+ class MyClass(object):
+ pass
+ MyClass = add_metaclass(Meta)(MyClass)
+
+
+Binary and text data
+>>>>>>>>>>>>>>>>>>>>
+
+Python 3 enforces the distinction between byte strings and text strings far more
+rigorously than Python 2 does; binary data cannot be automatically coerced to
+or from text data. six provides several functions to assist in classifying
+string data in all Python versions.
+
+
+.. function:: b(data)
+
+ A "fake" bytes literal. *data* should always be a normal string literal. In
+ Python 2, :func:`b` returns an 8-bit string. In Python 3, *data* is encoded
+ with the latin-1 encoding to bytes.
+
+
+ .. note::
+
+ Since all Python versions 2.6 and after support the ``b`` prefix,
+ code without 2.5 support doesn't need :func:`b`.
+
+
+.. function:: u(text)
+
+ A "fake" unicode literal. *text* should always be a normal string literal.
+ In Python 2, :func:`u` returns unicode, and in Python 3, a string. Also, in
+ Python 2, the string is decoded with the ``unicode-escape`` codec, which
+ allows unicode escapes to be used in it.
+
+
+ .. note::
+
+ In Python 3.3, the ``u`` prefix has been reintroduced. Code that only
+ supports Python 3 versions of 3.3 and higher thus does not need
+ :func:`u`.
+
+ .. note::
+
+ On Python 2, :func:`u` doesn't know what the encoding of the literal
+ is. Each byte is converted directly to the unicode codepoint of the same
+ value. Because of this, it's only safe to use :func:`u` with strings of
+ ASCII data.
+
+
+.. function:: unichr(c)
+
+ Return the (Unicode) string representing the codepoint *c*. This is
+ equivalent to :func:`py2:unichr` on Python 2 and :func:`py3:chr` on Python 3.
+
+
+.. function:: int2byte(i)
+
+ Converts *i* to a byte. *i* must be in ``range(0, 256)``. This is
+ equivalent to :func:`py2:chr` in Python 2 and ``bytes((i,))`` in Python 3.
+
+
+.. function:: byte2int(bs)
+
+ Converts the first byte of *bs* to an integer. This is equivalent to
+ ``ord(bs[0])`` on Python 2 and ``bs[0]`` on Python 3.
+
+
+.. function:: indexbytes(buf, i)
+
+ Return the byte at index *i* of *buf* as an integer. This is equivalent to
+ indexing a bytes object in Python 3.
+
+
+.. function:: iterbytes(buf)
+
+ Return an iterator over bytes in *buf* as integers. This is equivalent to
+ a bytes object iterator in Python 3.
+
+
+.. function:: ensure_binary(s, encoding='utf-8', errors='strict')
+
+ Coerce *s* to :data:`binary_type`. *encoding*, *errors* are the same as
+ :meth:`py3:str.encode`
+
+
+.. function:: ensure_str(s, encoding='utf-8', errors='strict')
+
+ Coerce *s* to ``str``. *encoding*, *errors* are the same as
+ :meth:`py3:str.encode`
+
+
+.. function:: ensure_text(s, encoding='utf-8', errors='strict')
+
+ Coerce *s* to :data:`text_type`. *encoding*, *errors* are the same as
+ :meth:`py3:str.encode`
+
+
+.. data:: StringIO
+
+ This is a fake file object for textual data. It's an alias for
+ :class:`py2:StringIO.StringIO` in Python 2 and :class:`py3:io.StringIO` in
+ Python 3.
+
+
+.. data:: BytesIO
+
+ This is a fake file object for binary data. In Python 2, it's an alias for
+ :class:`py2:StringIO.StringIO`, but in Python 3, it's an alias for
+ :class:`py3:io.BytesIO`.
+
+
+.. decorator:: python_2_unicode_compatible
+
+ A class decorator that takes a class defining a ``__str__`` method. On
+ Python 3, the decorator does nothing. On Python 2, it aliases the
+ ``__str__`` method to ``__unicode__`` and creates a new ``__str__`` method
+ that returns the result of ``__unicode__()`` encoded with UTF-8.
+
+
+unittest assertions
+>>>>>>>>>>>>>>>>>>>
+
+Six contains compatibility shims for unittest assertions that have been renamed.
+The parameters are the same as their aliases, but you must pass the test method
+as the first argument. For example::
+
+ import six
+ import unittest
+
+ class TestAssertCountEqual(unittest.TestCase):
+ def test(self):
+ six.assertCountEqual(self, (1, 2), [2, 1])
+
+Note these functions are only available on Python 2.7 or later.
+
+.. function:: assertCountEqual()
+
+ Alias for :meth:`~py3:unittest.TestCase.assertCountEqual` on Python 3 and
+ :meth:`~py2:unittest.TestCase.assertItemsEqual` on Python 2.
+
+
+.. function:: assertRaisesRegex()
+
+ Alias for :meth:`~py3:unittest.TestCase.assertRaisesRegex` on Python 3 and
+ :meth:`~py2:unittest.TestCase.assertRaisesRegexp` on Python 2.
+
+
+.. function:: assertRegex()
+
+ Alias for :meth:`~py3:unittest.TestCase.assertRegex` on Python 3 and
+ :meth:`~py2:unittest.TestCase.assertRegexpMatches` on Python 2.
+
+
+Renamed modules and attributes compatibility
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+.. module:: six.moves
+ :synopsis: Renamed modules and attributes compatibility
+
+Python 3 reorganized the standard library and moved several functions to
+different modules. Six provides a consistent interface to them through the fake
+:mod:`six.moves` module. For example, to load the module for parsing HTML on
+Python 2 or 3, write::
+
+ from six.moves import html_parser
+
+Similarly, to get the function to reload modules, which was moved from the
+builtin module to the ``importlib`` module, use::
+
+ from six.moves import reload_module
+
+For the most part, :mod:`six.moves` aliases are the names of the modules in
+Python 3. When the new Python 3 name is a package, the components of the name
+are separated by underscores. For example, ``html.parser`` becomes
+``html_parser``. In some cases where several modules have been combined, the
+Python 2 name is retained. This is so the appropriate modules can be found when
+running on Python 2. For example, ``BaseHTTPServer`` which is in
+``http.server`` in Python 3 is aliased as ``BaseHTTPServer``.
+
+Some modules which had two implementations have been merged in Python 3. For
+example, ``cPickle`` no longer exists in Python 3; it was merged with
+``pickle``. In these cases, fetching the fast version will load the fast one on
+Python 2 and the merged module in Python 3.
+
+The :mod:`py2:urllib`, :mod:`py2:urllib2`, and :mod:`py2:urlparse` modules have
+been combined in the :mod:`py3:urllib` package in Python 3. The
+:mod:`six.moves.urllib` package is a version-independent location for this
+functionality; its structure mimics the structure of the Python 3
+:mod:`py3:urllib` package.
+
+.. note::
+
+ In order to make imports of the form::
+
+ from six.moves.cPickle import loads
+
+ work, six places special proxy objects in :data:`py3:sys.modules`. These
+ proxies lazily load the underlying module when an attribute is fetched. This
+ will fail if the underlying module is not available in the Python
+ interpreter. For example, ``sys.modules["six.moves.winreg"].LoadKey`` would
+ fail on any non-Windows platform. Unfortunately, some applications try to
+ load attributes on every module in :data:`py3:sys.modules`. six mitigates
+ this problem for some applications by pretending attributes on unimportable
+ modules do not exist. This hack does not work in every case, though. If you are
+ encountering problems with the lazy modules and don't use any from imports
+ directly from ``six.moves`` modules, you can workaround the issue by removing
+ the six proxy modules::
+
+ d = [name for name in sys.modules if name.startswith("six.moves.")]
+ for name in d:
+ del sys.modules[name]
+
+Supported renames:
+
++------------------------------+-------------------------------------+---------------------------------------+
+| Name | Python 2 name | Python 3 name |
++==============================+=====================================+=======================================+
+| ``builtins`` | :mod:`py2:__builtin__` | :mod:`py3:builtins` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``configparser`` | :mod:`py2:ConfigParser` | :mod:`py3:configparser` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``copyreg`` | :mod:`py2:copy_reg` | :mod:`py3:copyreg` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``cPickle`` | :mod:`py2:cPickle` | :mod:`py3:pickle` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``cStringIO`` | :func:`py2:cStringIO.StringIO` | :class:`py3:io.StringIO` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``collections_abc`` | :mod:`py2:collections` | :mod:`py3:collections.abc` (3.3+) |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``dbm_gnu`` | :mod:`py2:gdbm` | :mod:`py3:dbm.gnu` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``dbm_ndbm`` | :mod:`py2:dbm` | :mod:`py3:dbm.ndbm` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``_dummy_thread`` | :mod:`py2:dummy_thread` | :mod:`py3:_dummy_thread` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``email_mime_base`` | :mod:`py2:email.MIMEBase` | :mod:`py3:email.mime.base` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``email_mime_image`` | :mod:`py2:email.MIMEImage` | :mod:`py3:email.mime.image` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``email_mime_multipart`` | :mod:`py2:email.MIMEMultipart` | :mod:`py3:email.mime.multipart` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``email_mime_nonmultipart`` | :mod:`py2:email.MIMENonMultipart` | :mod:`py3:email.mime.nonmultipart` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``email_mime_text`` | :mod:`py2:email.MIMEText` | :mod:`py3:email.mime.text` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``filter`` | :func:`py2:itertools.ifilter` | :func:`py3:filter` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``filterfalse`` | :func:`py2:itertools.ifilterfalse` | :func:`py3:itertools.filterfalse` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``getcwd`` | :func:`py2:os.getcwdu` | :func:`py3:os.getcwd` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``getcwdb`` | :func:`py2:os.getcwd` | :func:`py3:os.getcwdb` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``getoutput`` | :func:`py2:commands.getoutput` | :func:`py3:subprocess.getoutput` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``http_cookiejar`` | :mod:`py2:cookielib` | :mod:`py3:http.cookiejar` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``http_cookies`` | :mod:`py2:Cookie` | :mod:`py3:http.cookies` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``html_entities`` | :mod:`py2:htmlentitydefs` | :mod:`py3:html.entities` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``html_parser`` | :mod:`py2:HTMLParser` | :mod:`py3:html.parser` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``http_client`` | :mod:`py2:httplib` | :mod:`py3:http.client` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``BaseHTTPServer`` | :mod:`py2:BaseHTTPServer` | :mod:`py3:http.server` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``CGIHTTPServer`` | :mod:`py2:CGIHTTPServer` | :mod:`py3:http.server` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``SimpleHTTPServer`` | :mod:`py2:SimpleHTTPServer` | :mod:`py3:http.server` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``input`` | :func:`py2:raw_input` | :func:`py3:input` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``intern`` | :func:`py2:intern` | :func:`py3:sys.intern` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``map`` | :func:`py2:itertools.imap` | :func:`py3:map` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``queue`` | :mod:`py2:Queue` | :mod:`py3:queue` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``range`` | :func:`py2:xrange` | :func:`py3:range` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``reduce`` | :func:`py2:reduce` | :func:`py3:functools.reduce` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``reload_module`` | :func:`py2:reload` | :func:`py3:imp.reload`, |
+| | | :func:`py3:importlib.reload` |
+| | | on Python 3.4+ |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``reprlib`` | :mod:`py2:repr` | :mod:`py3:reprlib` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``shlex_quote`` | :mod:`py2:pipes.quote` | :mod:`py3:shlex.quote` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``socketserver`` | :mod:`py2:SocketServer` | :mod:`py3:socketserver` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``_thread`` | :mod:`py2:thread` | :mod:`py3:_thread` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter`` | :mod:`py2:Tkinter` | :mod:`py3:tkinter` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_dialog`` | :mod:`py2:Dialog` | :mod:`py3:tkinter.dialog` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_filedialog`` | :mod:`py2:FileDialog` | :mod:`py3:tkinter.FileDialog` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_scrolledtext`` | :mod:`py2:ScrolledText` | :mod:`py3:tkinter.scrolledtext` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_simpledialog`` | :mod:`py2:SimpleDialog` | :mod:`py3:tkinter.simpledialog` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_ttk`` | :mod:`py2:ttk` | :mod:`py3:tkinter.ttk` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_tix`` | :mod:`py2:Tix` | :mod:`py3:tkinter.tix` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_constants`` | :mod:`py2:Tkconstants` | :mod:`py3:tkinter.constants` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_dnd`` | :mod:`py2:Tkdnd` | :mod:`py3:tkinter.dnd` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_colorchooser`` | :mod:`py2:tkColorChooser` | :mod:`py3:tkinter.colorchooser` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_commondialog`` | :mod:`py2:tkCommonDialog` | :mod:`py3:tkinter.commondialog` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_tkfiledialog`` | :mod:`py2:tkFileDialog` | :mod:`py3:tkinter.filedialog` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_font`` | :mod:`py2:tkFont` | :mod:`py3:tkinter.font` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_messagebox`` | :mod:`py2:tkMessageBox` | :mod:`py3:tkinter.messagebox` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``tkinter_tksimpledialog`` | :mod:`py2:tkSimpleDialog` | :mod:`py3:tkinter.simpledialog` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``urllib.parse`` | See :mod:`six.moves.urllib.parse` | :mod:`py3:urllib.parse` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``urllib.error`` | See :mod:`six.moves.urllib.error` | :mod:`py3:urllib.error` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``urllib.request`` | See :mod:`six.moves.urllib.request` | :mod:`py3:urllib.request` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``urllib.response`` | See :mod:`six.moves.urllib.response`| :mod:`py3:urllib.response` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``urllib.robotparser`` | :mod:`py2:robotparser` | :mod:`py3:urllib.robotparser` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``urllib_robotparser`` | :mod:`py2:robotparser` | :mod:`py3:urllib.robotparser` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``UserDict`` | :class:`py2:UserDict.UserDict` | :class:`py3:collections.UserDict` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``UserList`` | :class:`py2:UserList.UserList` | :class:`py3:collections.UserList` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``UserString`` | :class:`py2:UserString.UserString` | :class:`py3:collections.UserString` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``winreg`` | :mod:`py2:_winreg` | :mod:`py3:winreg` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``xmlrpc_client`` | :mod:`py2:xmlrpclib` | :mod:`py3:xmlrpc.client` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``xmlrpc_server`` | :mod:`py2:SimpleXMLRPCServer` | :mod:`py3:xmlrpc.server` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``xrange`` | :func:`py2:xrange` | :func:`py3:range` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``zip`` | :func:`py2:itertools.izip` | :func:`py3:zip` |
++------------------------------+-------------------------------------+---------------------------------------+
+| ``zip_longest`` | :func:`py2:itertools.izip_longest` | :func:`py3:itertools.zip_longest` |
++------------------------------+-------------------------------------+---------------------------------------+
+
+urllib parse
+<<<<<<<<<<<<
+
+.. module:: six.moves.urllib.parse
+ :synopsis: Stuff from :mod:`py2:urlparse` and :mod:`py2:urllib` in Python 2 and :mod:`py3:urllib.parse` in Python 3
+
+Contains functions from Python 3's :mod:`py3:urllib.parse` and Python 2's:
+
+:mod:`py2:urlparse`:
+
+* :func:`py2:urlparse.ParseResult`
+* :func:`py2:urlparse.SplitResult`
+* :func:`py2:urlparse.urlparse`
+* :func:`py2:urlparse.urlunparse`
+* :func:`py2:urlparse.parse_qs`
+* :func:`py2:urlparse.parse_qsl`
+* :func:`py2:urlparse.urljoin`
+* :func:`py2:urlparse.urldefrag`
+* :func:`py2:urlparse.urlsplit`
+* :func:`py2:urlparse.urlunsplit`
+* :func:`py2:urlparse.splitquery`
+* :func:`py2:urlparse.uses_fragment`
+* :func:`py2:urlparse.uses_netloc`
+* :func:`py2:urlparse.uses_params`
+* :func:`py2:urlparse.uses_query`
+* :func:`py2:urlparse.uses_relative`
+
+and :mod:`py2:urllib`:
+
+* :func:`py2:urllib.quote`
+* :func:`py2:urllib.quote_plus`
+* :func:`py2:urllib.splittag`
+* :func:`py2:urllib.splituser`
+* :func:`py2:urllib.splitvalue`
+* :func:`py2:urllib.unquote` (also exposed as :func:`py3:urllib.parse.unquote_to_bytes`)
+* :func:`py2:urllib.unquote_plus`
+* :func:`py2:urllib.urlencode`
+
+
+urllib error
+<<<<<<<<<<<<
+
+.. module:: six.moves.urllib.error
+ :synopsis: Stuff from :mod:`py2:urllib` and :mod:`py2:urllib2` in Python 2 and :mod:`py3:urllib.error` in Python 3
+
+Contains exceptions from Python 3's :mod:`py3:urllib.error` and Python 2's:
+
+:mod:`py2:urllib`:
+
+* :exc:`py2:urllib.ContentTooShortError`
+
+and :mod:`py2:urllib2`:
+
+* :exc:`py2:urllib2.URLError`
+* :exc:`py2:urllib2.HTTPError`
+
+
+urllib request
+<<<<<<<<<<<<<<
+
+.. module:: six.moves.urllib.request
+ :synopsis: Stuff from :mod:`py2:urllib` and :mod:`py2:urllib2` in Python 2 and :mod:`py3:urllib.request` in Python 3
+
+Contains items from Python 3's :mod:`py3:urllib.request` and Python 2's:
+
+:mod:`py2:urllib`:
+
+* :func:`py2:urllib.pathname2url`
+* :func:`py2:urllib.url2pathname`
+* :func:`py2:urllib.getproxies`
+* :func:`py2:urllib.urlretrieve`
+* :func:`py2:urllib.urlcleanup`
+* :class:`py2:urllib.URLopener`
+* :class:`py2:urllib.FancyURLopener`
+* :func:`py2:urllib.proxy_bypass`
+
+and :mod:`py2:urllib2`:
+
+* :func:`py2:urllib2.urlopen`
+* :func:`py2:urllib2.install_opener`
+* :func:`py2:urllib2.build_opener`
+* :func:`py2:urllib2.parse_http_list`
+* :func:`py2:urllib2.parse_keqv_list`
+* :class:`py2:urllib2.Request`
+* :class:`py2:urllib2.OpenerDirector`
+* :class:`py2:urllib2.HTTPDefaultErrorHandler`
+* :class:`py2:urllib2.HTTPRedirectHandler`
+* :class:`py2:urllib2.HTTPCookieProcessor`
+* :class:`py2:urllib2.ProxyHandler`
+* :class:`py2:urllib2.BaseHandler`
+* :class:`py2:urllib2.HTTPPasswordMgr`
+* :class:`py2:urllib2.HTTPPasswordMgrWithDefaultRealm`
+* :class:`py2:urllib2.AbstractBasicAuthHandler`
+* :class:`py2:urllib2.HTTPBasicAuthHandler`
+* :class:`py2:urllib2.ProxyBasicAuthHandler`
+* :class:`py2:urllib2.AbstractDigestAuthHandler`
+* :class:`py2:urllib2.HTTPDigestAuthHandler`
+* :class:`py2:urllib2.ProxyDigestAuthHandler`
+* :class:`py2:urllib2.HTTPHandler`
+* :class:`py2:urllib2.HTTPSHandler`
+* :class:`py2:urllib2.FileHandler`
+* :class:`py2:urllib2.FTPHandler`
+* :class:`py2:urllib2.CacheFTPHandler`
+* :class:`py2:urllib2.UnknownHandler`
+* :class:`py2:urllib2.HTTPErrorProcessor`
+
+
+urllib response
+<<<<<<<<<<<<<<<
+
+.. module:: six.moves.urllib.response
+ :synopsis: Stuff from :mod:`py2:urllib` in Python 2 and :mod:`py3:urllib.response` in Python 3
+
+Contains classes from Python 3's :mod:`py3:urllib.response` and Python 2's:
+
+:mod:`py2:urllib`:
+
+* :class:`py2:urllib.addbase`
+* :class:`py2:urllib.addclosehook`
+* :class:`py2:urllib.addinfo`
+* :class:`py2:urllib.addinfourl`
+
+
+Advanced - Customizing renames
+<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
+
+.. currentmodule:: six
+
+It is possible to add additional names to the :mod:`six.moves` namespace.
+
+
+.. function:: add_move(item)
+
+ Add *item* to the :mod:`six.moves` mapping. *item* should be a
+ :class:`MovedAttribute` or :class:`MovedModule` instance.
+
+
+.. function:: remove_move(name)
+
+ Remove the :mod:`six.moves` mapping called *name*. *name* should be a
+ string.
+
+
+Instances of the following classes can be passed to :func:`add_move`. Neither
+have any public members.
+
+
+.. class:: MovedModule(name, old_mod, new_mod)
+
+ Create a mapping for :mod:`six.moves` called *name* that references different
+ modules in Python 2 and 3. *old_mod* is the name of the Python 2 module.
+ *new_mod* is the name of the Python 3 module.
+
+
+.. class:: MovedAttribute(name, old_mod, new_mod, old_attr=None, new_attr=None)
+
+ Create a mapping for :mod:`six.moves` called *name* that references different
+ attributes in Python 2 and 3. *old_mod* is the name of the Python 2 module.
+ *new_mod* is the name of the Python 3 module. If *new_attr* is not given, it
+ defaults to *old_attr*. If neither is given, they both default to *name*.
diff --git a/testing/web-platform/tests/tools/third_party/six/setup.cfg b/testing/web-platform/tests/tools/third_party/six/setup.cfg
new file mode 100644
index 0000000000..317e016c8c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/setup.cfg
@@ -0,0 +1,20 @@
+[bdist_wheel]
+universal = 1
+
+[flake8]
+max-line-length = 100
+ignore = F821
+
+[metadata]
+license_file = LICENSE
+
+[tool:pytest]
+minversion=2.2.0
+pep8ignore =
+ documentation/*.py ALL
+ test_six.py ALL
+
+flakes-ignore =
+ documentation/*.py ALL
+ test_six.py ALL
+ six.py UndefinedName
diff --git a/testing/web-platform/tests/tools/third_party/six/setup.py b/testing/web-platform/tests/tools/third_party/six/setup.py
new file mode 100644
index 0000000000..97c685b5a5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/setup.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2010-2019 Benjamin Peterson
+#
+# 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.
+
+from __future__ import with_statement
+
+# Six is a dependency of setuptools, so using setuptools creates a
+# circular dependency when building a Python stack from source. We
+# therefore allow falling back to distutils to install six.
+try:
+ from setuptools import setup
+except ImportError:
+ from distutils.core import setup
+
+import six
+
+six_classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 3",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Topic :: Software Development :: Libraries",
+ "Topic :: Utilities",
+]
+
+with open("README.rst", "r") as fp:
+ six_long_description = fp.read()
+
+setup(name="six",
+ version=six.__version__,
+ author="Benjamin Peterson",
+ author_email="benjamin@python.org",
+ url="https://github.com/benjaminp/six",
+ tests_require=["pytest"],
+ py_modules=["six"],
+ description="Python 2 and 3 compatibility utilities",
+ long_description=six_long_description,
+ license="MIT",
+ classifiers=six_classifiers,
+ python_requires=">=2.6, !=3.0.*, !=3.1.*",
+ )
diff --git a/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/INSTALLER b/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/INSTALLER
new file mode 100644
index 0000000000..a1b589e38a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/LICENSE b/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/LICENSE
new file mode 100644
index 0000000000..de6633112c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/LICENSE
@@ -0,0 +1,18 @@
+Copyright (c) 2010-2020 Benjamin Peterson
+
+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/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/METADATA b/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/METADATA
new file mode 100644
index 0000000000..869bf25a88
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/METADATA
@@ -0,0 +1,49 @@
+Metadata-Version: 2.1
+Name: six
+Version: 1.15.0
+Summary: Python 2 and 3 compatibility utilities
+Home-page: https://github.com/benjaminp/six
+Author: Benjamin Peterson
+Author-email: benjamin@python.org
+License: MIT
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 3
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Topic :: Software Development :: Libraries
+Classifier: Topic :: Utilities
+Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*
+
+.. image:: https://img.shields.io/pypi/v/six.svg
+ :target: https://pypi.org/project/six/
+ :alt: six on PyPI
+
+.. image:: https://travis-ci.org/benjaminp/six.svg?branch=master
+ :target: https://travis-ci.org/benjaminp/six
+ :alt: six on TravisCI
+
+.. image:: https://readthedocs.org/projects/six/badge/?version=latest
+ :target: https://six.readthedocs.io/
+ :alt: six's documentation on Read the Docs
+
+.. image:: https://img.shields.io/badge/license-MIT-green.svg
+ :target: https://github.com/benjaminp/six/blob/master/LICENSE
+ :alt: MIT License badge
+
+Six is a Python 2 and 3 compatibility library. It provides utility functions
+for smoothing over the differences between the Python versions with the goal of
+writing Python code that is compatible on both Python versions. See the
+documentation for more information on what is provided.
+
+Six supports Python 2.7 and 3.3+. It is contained in only one Python
+file, so it can be easily copied into your project. (The copyright and license
+notice must be retained.)
+
+Online documentation is at https://six.readthedocs.io/.
+
+Bugs can be reported to https://github.com/benjaminp/six. The code can also
+be found there.
+
+
diff --git a/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/RECORD b/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/RECORD
new file mode 100644
index 0000000000..d9754c61c4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/RECORD
@@ -0,0 +1,8 @@
+six-1.15.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+six-1.15.0.dist-info/LICENSE,sha256=i7hQxWWqOJ_cFvOkaWWtI9gq3_YPI5P8J2K2MYXo5sk,1066
+six-1.15.0.dist-info/METADATA,sha256=W6rlyoeMZHXh6srP9NXNsm0rjAf_660re8WdH5TBT8E,1795
+six-1.15.0.dist-info/RECORD,,
+six-1.15.0.dist-info/WHEEL,sha256=kGT74LWyRUZrL4VgLh6_g12IeVl_9u9ZVhadrgXZUEY,110
+six-1.15.0.dist-info/top_level.txt,sha256=_iVH_iYEtEXnD8nYGQYpYFUvkUW9sEO1GYbkeKSAais,4
+six.py,sha256=U4Z_yv534W5CNyjY9i8V1OXY2SjAny8y2L5vDLhhThM,34159
+six.pyc,,
diff --git a/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/WHEEL b/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/WHEEL
new file mode 100644
index 0000000000..ef99c6cf32
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/WHEEL
@@ -0,0 +1,6 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.34.2)
+Root-Is-Purelib: true
+Tag: py2-none-any
+Tag: py3-none-any
+
diff --git a/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/top_level.txt b/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/top_level.txt
new file mode 100644
index 0000000000..ffe2fce498
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/six-1.15.0.dist-info/top_level.txt
@@ -0,0 +1 @@
+six
diff --git a/testing/web-platform/tests/tools/third_party/six/six.py b/testing/web-platform/tests/tools/third_party/six/six.py
new file mode 100644
index 0000000000..83f69783d1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/six.py
@@ -0,0 +1,982 @@
+# Copyright (c) 2010-2020 Benjamin Peterson
+#
+# 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.
+
+"""Utilities for writing code that runs on Python 2 and 3"""
+
+from __future__ import absolute_import
+
+import functools
+import itertools
+import operator
+import sys
+import types
+
+__author__ = "Benjamin Peterson <benjamin@python.org>"
+__version__ = "1.15.0"
+
+
+# Useful for very coarse version differentiation.
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+PY34 = sys.version_info[0:2] >= (3, 4)
+
+if PY3:
+ string_types = str,
+ integer_types = int,
+ class_types = type,
+ text_type = str
+ binary_type = bytes
+
+ MAXSIZE = sys.maxsize
+else:
+ string_types = basestring,
+ integer_types = (int, long)
+ class_types = (type, types.ClassType)
+ text_type = unicode
+ binary_type = str
+
+ if sys.platform.startswith("java"):
+ # Jython always uses 32 bits.
+ MAXSIZE = int((1 << 31) - 1)
+ else:
+ # It's possible to have sizeof(long) != sizeof(Py_ssize_t).
+ class X(object):
+
+ def __len__(self):
+ return 1 << 31
+ try:
+ len(X())
+ except OverflowError:
+ # 32-bit
+ MAXSIZE = int((1 << 31) - 1)
+ else:
+ # 64-bit
+ MAXSIZE = int((1 << 63) - 1)
+ del X
+
+
+def _add_doc(func, doc):
+ """Add documentation to a function."""
+ func.__doc__ = doc
+
+
+def _import_module(name):
+ """Import module, returning the module after the last dot."""
+ __import__(name)
+ return sys.modules[name]
+
+
+class _LazyDescr(object):
+
+ def __init__(self, name):
+ self.name = name
+
+ def __get__(self, obj, tp):
+ result = self._resolve()
+ setattr(obj, self.name, result) # Invokes __set__.
+ try:
+ # This is a bit ugly, but it avoids running this again by
+ # removing this descriptor.
+ delattr(obj.__class__, self.name)
+ except AttributeError:
+ pass
+ return result
+
+
+class MovedModule(_LazyDescr):
+
+ def __init__(self, name, old, new=None):
+ super(MovedModule, self).__init__(name)
+ if PY3:
+ if new is None:
+ new = name
+ self.mod = new
+ else:
+ self.mod = old
+
+ def _resolve(self):
+ return _import_module(self.mod)
+
+ def __getattr__(self, attr):
+ _module = self._resolve()
+ value = getattr(_module, attr)
+ setattr(self, attr, value)
+ return value
+
+
+class _LazyModule(types.ModuleType):
+
+ def __init__(self, name):
+ super(_LazyModule, self).__init__(name)
+ self.__doc__ = self.__class__.__doc__
+
+ def __dir__(self):
+ attrs = ["__doc__", "__name__"]
+ attrs += [attr.name for attr in self._moved_attributes]
+ return attrs
+
+ # Subclasses should override this
+ _moved_attributes = []
+
+
+class MovedAttribute(_LazyDescr):
+
+ def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
+ super(MovedAttribute, self).__init__(name)
+ if PY3:
+ if new_mod is None:
+ new_mod = name
+ self.mod = new_mod
+ if new_attr is None:
+ if old_attr is None:
+ new_attr = name
+ else:
+ new_attr = old_attr
+ self.attr = new_attr
+ else:
+ self.mod = old_mod
+ if old_attr is None:
+ old_attr = name
+ self.attr = old_attr
+
+ def _resolve(self):
+ module = _import_module(self.mod)
+ return getattr(module, self.attr)
+
+
+class _SixMetaPathImporter(object):
+
+ """
+ A meta path importer to import six.moves and its submodules.
+
+ This class implements a PEP302 finder and loader. It should be compatible
+ with Python 2.5 and all existing versions of Python3
+ """
+
+ def __init__(self, six_module_name):
+ self.name = six_module_name
+ self.known_modules = {}
+
+ def _add_module(self, mod, *fullnames):
+ for fullname in fullnames:
+ self.known_modules[self.name + "." + fullname] = mod
+
+ def _get_module(self, fullname):
+ return self.known_modules[self.name + "." + fullname]
+
+ def find_module(self, fullname, path=None):
+ if fullname in self.known_modules:
+ return self
+ return None
+
+ def __get_module(self, fullname):
+ try:
+ return self.known_modules[fullname]
+ except KeyError:
+ raise ImportError("This loader does not know module " + fullname)
+
+ def load_module(self, fullname):
+ try:
+ # in case of a reload
+ return sys.modules[fullname]
+ except KeyError:
+ pass
+ mod = self.__get_module(fullname)
+ if isinstance(mod, MovedModule):
+ mod = mod._resolve()
+ else:
+ mod.__loader__ = self
+ sys.modules[fullname] = mod
+ return mod
+
+ def is_package(self, fullname):
+ """
+ Return true, if the named module is a package.
+
+ We need this method to get correct spec objects with
+ Python 3.4 (see PEP451)
+ """
+ return hasattr(self.__get_module(fullname), "__path__")
+
+ def get_code(self, fullname):
+ """Return None
+
+ Required, if is_package is implemented"""
+ self.__get_module(fullname) # eventually raises ImportError
+ return None
+ get_source = get_code # same as get_code
+
+_importer = _SixMetaPathImporter(__name__)
+
+
+class _MovedItems(_LazyModule):
+
+ """Lazy loading of moved objects"""
+ __path__ = [] # mark as package
+
+
+_moved_attributes = [
+ MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
+ MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
+ MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
+ MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
+ MovedAttribute("intern", "__builtin__", "sys"),
+ MovedAttribute("map", "itertools", "builtins", "imap", "map"),
+ MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"),
+ MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"),
+ MovedAttribute("getoutput", "commands", "subprocess"),
+ MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
+ MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"),
+ MovedAttribute("reduce", "__builtin__", "functools"),
+ MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
+ MovedAttribute("StringIO", "StringIO", "io"),
+ MovedAttribute("UserDict", "UserDict", "collections"),
+ MovedAttribute("UserList", "UserList", "collections"),
+ MovedAttribute("UserString", "UserString", "collections"),
+ MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
+ MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
+ MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
+ MovedModule("builtins", "__builtin__"),
+ MovedModule("configparser", "ConfigParser"),
+ MovedModule("collections_abc", "collections", "collections.abc" if sys.version_info >= (3, 3) else "collections"),
+ MovedModule("copyreg", "copy_reg"),
+ MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
+ MovedModule("dbm_ndbm", "dbm", "dbm.ndbm"),
+ MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread" if sys.version_info < (3, 9) else "_thread"),
+ MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
+ MovedModule("http_cookies", "Cookie", "http.cookies"),
+ MovedModule("html_entities", "htmlentitydefs", "html.entities"),
+ MovedModule("html_parser", "HTMLParser", "html.parser"),
+ MovedModule("http_client", "httplib", "http.client"),
+ MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
+ MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"),
+ MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
+ MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
+ MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
+ MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
+ MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
+ MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
+ MovedModule("cPickle", "cPickle", "pickle"),
+ MovedModule("queue", "Queue"),
+ MovedModule("reprlib", "repr"),
+ MovedModule("socketserver", "SocketServer"),
+ MovedModule("_thread", "thread", "_thread"),
+ MovedModule("tkinter", "Tkinter"),
+ MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
+ MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
+ MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
+ MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
+ MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
+ MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
+ MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
+ MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
+ MovedModule("tkinter_colorchooser", "tkColorChooser",
+ "tkinter.colorchooser"),
+ MovedModule("tkinter_commondialog", "tkCommonDialog",
+ "tkinter.commondialog"),
+ MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
+ MovedModule("tkinter_font", "tkFont", "tkinter.font"),
+ MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
+ MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
+ "tkinter.simpledialog"),
+ MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
+ MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
+ MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
+ MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
+ MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
+ MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
+]
+# Add windows specific modules.
+if sys.platform == "win32":
+ _moved_attributes += [
+ MovedModule("winreg", "_winreg"),
+ ]
+
+for attr in _moved_attributes:
+ setattr(_MovedItems, attr.name, attr)
+ if isinstance(attr, MovedModule):
+ _importer._add_module(attr, "moves." + attr.name)
+del attr
+
+_MovedItems._moved_attributes = _moved_attributes
+
+moves = _MovedItems(__name__ + ".moves")
+_importer._add_module(moves, "moves")
+
+
+class Module_six_moves_urllib_parse(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_parse"""
+
+
+_urllib_parse_moved_attributes = [
+ MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
+ MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
+ MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
+ MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
+ MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
+ MovedAttribute("urljoin", "urlparse", "urllib.parse"),
+ MovedAttribute("urlparse", "urlparse", "urllib.parse"),
+ MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
+ MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
+ MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
+ MovedAttribute("quote", "urllib", "urllib.parse"),
+ MovedAttribute("quote_plus", "urllib", "urllib.parse"),
+ MovedAttribute("unquote", "urllib", "urllib.parse"),
+ MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
+ MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"),
+ MovedAttribute("urlencode", "urllib", "urllib.parse"),
+ MovedAttribute("splitquery", "urllib", "urllib.parse"),
+ MovedAttribute("splittag", "urllib", "urllib.parse"),
+ MovedAttribute("splituser", "urllib", "urllib.parse"),
+ MovedAttribute("splitvalue", "urllib", "urllib.parse"),
+ MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_params", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_query", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
+]
+for attr in _urllib_parse_moved_attributes:
+ setattr(Module_six_moves_urllib_parse, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
+ "moves.urllib_parse", "moves.urllib.parse")
+
+
+class Module_six_moves_urllib_error(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_error"""
+
+
+_urllib_error_moved_attributes = [
+ MovedAttribute("URLError", "urllib2", "urllib.error"),
+ MovedAttribute("HTTPError", "urllib2", "urllib.error"),
+ MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
+]
+for attr in _urllib_error_moved_attributes:
+ setattr(Module_six_moves_urllib_error, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
+ "moves.urllib_error", "moves.urllib.error")
+
+
+class Module_six_moves_urllib_request(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_request"""
+
+
+_urllib_request_moved_attributes = [
+ MovedAttribute("urlopen", "urllib2", "urllib.request"),
+ MovedAttribute("install_opener", "urllib2", "urllib.request"),
+ MovedAttribute("build_opener", "urllib2", "urllib.request"),
+ MovedAttribute("pathname2url", "urllib", "urllib.request"),
+ MovedAttribute("url2pathname", "urllib", "urllib.request"),
+ MovedAttribute("getproxies", "urllib", "urllib.request"),
+ MovedAttribute("Request", "urllib2", "urllib.request"),
+ MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
+ MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
+ MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
+ MovedAttribute("FileHandler", "urllib2", "urllib.request"),
+ MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
+ MovedAttribute("urlretrieve", "urllib", "urllib.request"),
+ MovedAttribute("urlcleanup", "urllib", "urllib.request"),
+ MovedAttribute("URLopener", "urllib", "urllib.request"),
+ MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
+ MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
+ MovedAttribute("parse_http_list", "urllib2", "urllib.request"),
+ MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"),
+]
+for attr in _urllib_request_moved_attributes:
+ setattr(Module_six_moves_urllib_request, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
+ "moves.urllib_request", "moves.urllib.request")
+
+
+class Module_six_moves_urllib_response(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_response"""
+
+
+_urllib_response_moved_attributes = [
+ MovedAttribute("addbase", "urllib", "urllib.response"),
+ MovedAttribute("addclosehook", "urllib", "urllib.response"),
+ MovedAttribute("addinfo", "urllib", "urllib.response"),
+ MovedAttribute("addinfourl", "urllib", "urllib.response"),
+]
+for attr in _urllib_response_moved_attributes:
+ setattr(Module_six_moves_urllib_response, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
+ "moves.urllib_response", "moves.urllib.response")
+
+
+class Module_six_moves_urllib_robotparser(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_robotparser"""
+
+
+_urllib_robotparser_moved_attributes = [
+ MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
+]
+for attr in _urllib_robotparser_moved_attributes:
+ setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
+ "moves.urllib_robotparser", "moves.urllib.robotparser")
+
+
+class Module_six_moves_urllib(types.ModuleType):
+
+ """Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
+ __path__ = [] # mark as package
+ parse = _importer._get_module("moves.urllib_parse")
+ error = _importer._get_module("moves.urllib_error")
+ request = _importer._get_module("moves.urllib_request")
+ response = _importer._get_module("moves.urllib_response")
+ robotparser = _importer._get_module("moves.urllib_robotparser")
+
+ def __dir__(self):
+ return ['parse', 'error', 'request', 'response', 'robotparser']
+
+_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
+ "moves.urllib")
+
+
+def add_move(move):
+ """Add an item to six.moves."""
+ setattr(_MovedItems, move.name, move)
+
+
+def remove_move(name):
+ """Remove item from six.moves."""
+ try:
+ delattr(_MovedItems, name)
+ except AttributeError:
+ try:
+ del moves.__dict__[name]
+ except KeyError:
+ raise AttributeError("no such move, %r" % (name,))
+
+
+if PY3:
+ _meth_func = "__func__"
+ _meth_self = "__self__"
+
+ _func_closure = "__closure__"
+ _func_code = "__code__"
+ _func_defaults = "__defaults__"
+ _func_globals = "__globals__"
+else:
+ _meth_func = "im_func"
+ _meth_self = "im_self"
+
+ _func_closure = "func_closure"
+ _func_code = "func_code"
+ _func_defaults = "func_defaults"
+ _func_globals = "func_globals"
+
+
+try:
+ advance_iterator = next
+except NameError:
+ def advance_iterator(it):
+ return it.next()
+next = advance_iterator
+
+
+try:
+ callable = callable
+except NameError:
+ def callable(obj):
+ return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
+
+
+if PY3:
+ def get_unbound_function(unbound):
+ return unbound
+
+ create_bound_method = types.MethodType
+
+ def create_unbound_method(func, cls):
+ return func
+
+ Iterator = object
+else:
+ def get_unbound_function(unbound):
+ return unbound.im_func
+
+ def create_bound_method(func, obj):
+ return types.MethodType(func, obj, obj.__class__)
+
+ def create_unbound_method(func, cls):
+ return types.MethodType(func, None, cls)
+
+ class Iterator(object):
+
+ def next(self):
+ return type(self).__next__(self)
+
+ callable = callable
+_add_doc(get_unbound_function,
+ """Get the function out of a possibly unbound function""")
+
+
+get_method_function = operator.attrgetter(_meth_func)
+get_method_self = operator.attrgetter(_meth_self)
+get_function_closure = operator.attrgetter(_func_closure)
+get_function_code = operator.attrgetter(_func_code)
+get_function_defaults = operator.attrgetter(_func_defaults)
+get_function_globals = operator.attrgetter(_func_globals)
+
+
+if PY3:
+ def iterkeys(d, **kw):
+ return iter(d.keys(**kw))
+
+ def itervalues(d, **kw):
+ return iter(d.values(**kw))
+
+ def iteritems(d, **kw):
+ return iter(d.items(**kw))
+
+ def iterlists(d, **kw):
+ return iter(d.lists(**kw))
+
+ viewkeys = operator.methodcaller("keys")
+
+ viewvalues = operator.methodcaller("values")
+
+ viewitems = operator.methodcaller("items")
+else:
+ def iterkeys(d, **kw):
+ return d.iterkeys(**kw)
+
+ def itervalues(d, **kw):
+ return d.itervalues(**kw)
+
+ def iteritems(d, **kw):
+ return d.iteritems(**kw)
+
+ def iterlists(d, **kw):
+ return d.iterlists(**kw)
+
+ viewkeys = operator.methodcaller("viewkeys")
+
+ viewvalues = operator.methodcaller("viewvalues")
+
+ viewitems = operator.methodcaller("viewitems")
+
+_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
+_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
+_add_doc(iteritems,
+ "Return an iterator over the (key, value) pairs of a dictionary.")
+_add_doc(iterlists,
+ "Return an iterator over the (key, [values]) pairs of a dictionary.")
+
+
+if PY3:
+ def b(s):
+ return s.encode("latin-1")
+
+ def u(s):
+ return s
+ unichr = chr
+ import struct
+ int2byte = struct.Struct(">B").pack
+ del struct
+ byte2int = operator.itemgetter(0)
+ indexbytes = operator.getitem
+ iterbytes = iter
+ import io
+ StringIO = io.StringIO
+ BytesIO = io.BytesIO
+ del io
+ _assertCountEqual = "assertCountEqual"
+ if sys.version_info[1] <= 1:
+ _assertRaisesRegex = "assertRaisesRegexp"
+ _assertRegex = "assertRegexpMatches"
+ _assertNotRegex = "assertNotRegexpMatches"
+ else:
+ _assertRaisesRegex = "assertRaisesRegex"
+ _assertRegex = "assertRegex"
+ _assertNotRegex = "assertNotRegex"
+else:
+ def b(s):
+ return s
+ # Workaround for standalone backslash
+
+ def u(s):
+ return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
+ unichr = unichr
+ int2byte = chr
+
+ def byte2int(bs):
+ return ord(bs[0])
+
+ def indexbytes(buf, i):
+ return ord(buf[i])
+ iterbytes = functools.partial(itertools.imap, ord)
+ import StringIO
+ StringIO = BytesIO = StringIO.StringIO
+ _assertCountEqual = "assertItemsEqual"
+ _assertRaisesRegex = "assertRaisesRegexp"
+ _assertRegex = "assertRegexpMatches"
+ _assertNotRegex = "assertNotRegexpMatches"
+_add_doc(b, """Byte literal""")
+_add_doc(u, """Text literal""")
+
+
+def assertCountEqual(self, *args, **kwargs):
+ return getattr(self, _assertCountEqual)(*args, **kwargs)
+
+
+def assertRaisesRegex(self, *args, **kwargs):
+ return getattr(self, _assertRaisesRegex)(*args, **kwargs)
+
+
+def assertRegex(self, *args, **kwargs):
+ return getattr(self, _assertRegex)(*args, **kwargs)
+
+
+def assertNotRegex(self, *args, **kwargs):
+ return getattr(self, _assertNotRegex)(*args, **kwargs)
+
+
+if PY3:
+ exec_ = getattr(moves.builtins, "exec")
+
+ def reraise(tp, value, tb=None):
+ try:
+ if value is None:
+ value = tp()
+ if value.__traceback__ is not tb:
+ raise value.with_traceback(tb)
+ raise value
+ finally:
+ value = None
+ tb = None
+
+else:
+ def exec_(_code_, _globs_=None, _locs_=None):
+ """Execute code in a namespace."""
+ if _globs_ is None:
+ frame = sys._getframe(1)
+ _globs_ = frame.f_globals
+ if _locs_ is None:
+ _locs_ = frame.f_locals
+ del frame
+ elif _locs_ is None:
+ _locs_ = _globs_
+ exec("""exec _code_ in _globs_, _locs_""")
+
+ exec_("""def reraise(tp, value, tb=None):
+ try:
+ raise tp, value, tb
+ finally:
+ tb = None
+""")
+
+
+if sys.version_info[:2] > (3,):
+ exec_("""def raise_from(value, from_value):
+ try:
+ raise value from from_value
+ finally:
+ value = None
+""")
+else:
+ def raise_from(value, from_value):
+ raise value
+
+
+print_ = getattr(moves.builtins, "print", None)
+if print_ is None:
+ def print_(*args, **kwargs):
+ """The new-style print function for Python 2.4 and 2.5."""
+ fp = kwargs.pop("file", sys.stdout)
+ if fp is None:
+ return
+
+ def write(data):
+ if not isinstance(data, basestring):
+ data = str(data)
+ # If the file has an encoding, encode unicode with it.
+ if (isinstance(fp, file) and
+ isinstance(data, unicode) and
+ fp.encoding is not None):
+ errors = getattr(fp, "errors", None)
+ if errors is None:
+ errors = "strict"
+ data = data.encode(fp.encoding, errors)
+ fp.write(data)
+ want_unicode = False
+ sep = kwargs.pop("sep", None)
+ if sep is not None:
+ if isinstance(sep, unicode):
+ want_unicode = True
+ elif not isinstance(sep, str):
+ raise TypeError("sep must be None or a string")
+ end = kwargs.pop("end", None)
+ if end is not None:
+ if isinstance(end, unicode):
+ want_unicode = True
+ elif not isinstance(end, str):
+ raise TypeError("end must be None or a string")
+ if kwargs:
+ raise TypeError("invalid keyword arguments to print()")
+ if not want_unicode:
+ for arg in args:
+ if isinstance(arg, unicode):
+ want_unicode = True
+ break
+ if want_unicode:
+ newline = unicode("\n")
+ space = unicode(" ")
+ else:
+ newline = "\n"
+ space = " "
+ if sep is None:
+ sep = space
+ if end is None:
+ end = newline
+ for i, arg in enumerate(args):
+ if i:
+ write(sep)
+ write(arg)
+ write(end)
+if sys.version_info[:2] < (3, 3):
+ _print = print_
+
+ def print_(*args, **kwargs):
+ fp = kwargs.get("file", sys.stdout)
+ flush = kwargs.pop("flush", False)
+ _print(*args, **kwargs)
+ if flush and fp is not None:
+ fp.flush()
+
+_add_doc(reraise, """Reraise an exception.""")
+
+if sys.version_info[0:2] < (3, 4):
+ # This does exactly the same what the :func:`py3:functools.update_wrapper`
+ # function does on Python versions after 3.2. It sets the ``__wrapped__``
+ # attribute on ``wrapper`` object and it doesn't raise an error if any of
+ # the attributes mentioned in ``assigned`` and ``updated`` are missing on
+ # ``wrapped`` object.
+ def _update_wrapper(wrapper, wrapped,
+ assigned=functools.WRAPPER_ASSIGNMENTS,
+ updated=functools.WRAPPER_UPDATES):
+ for attr in assigned:
+ try:
+ value = getattr(wrapped, attr)
+ except AttributeError:
+ continue
+ else:
+ setattr(wrapper, attr, value)
+ for attr in updated:
+ getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
+ wrapper.__wrapped__ = wrapped
+ return wrapper
+ _update_wrapper.__doc__ = functools.update_wrapper.__doc__
+
+ def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
+ updated=functools.WRAPPER_UPDATES):
+ return functools.partial(_update_wrapper, wrapped=wrapped,
+ assigned=assigned, updated=updated)
+ wraps.__doc__ = functools.wraps.__doc__
+
+else:
+ wraps = functools.wraps
+
+
+def with_metaclass(meta, *bases):
+ """Create a base class with a metaclass."""
+ # This requires a bit of explanation: the basic idea is to make a dummy
+ # metaclass for one level of class instantiation that replaces itself with
+ # the actual metaclass.
+ class metaclass(type):
+
+ def __new__(cls, name, this_bases, d):
+ if sys.version_info[:2] >= (3, 7):
+ # This version introduced PEP 560 that requires a bit
+ # of extra care (we mimic what is done by __build_class__).
+ resolved_bases = types.resolve_bases(bases)
+ if resolved_bases is not bases:
+ d['__orig_bases__'] = bases
+ else:
+ resolved_bases = bases
+ return meta(name, resolved_bases, d)
+
+ @classmethod
+ def __prepare__(cls, name, this_bases):
+ return meta.__prepare__(name, bases)
+ return type.__new__(metaclass, 'temporary_class', (), {})
+
+
+def add_metaclass(metaclass):
+ """Class decorator for creating a class with a metaclass."""
+ def wrapper(cls):
+ orig_vars = cls.__dict__.copy()
+ slots = orig_vars.get('__slots__')
+ if slots is not None:
+ if isinstance(slots, str):
+ slots = [slots]
+ for slots_var in slots:
+ orig_vars.pop(slots_var)
+ orig_vars.pop('__dict__', None)
+ orig_vars.pop('__weakref__', None)
+ if hasattr(cls, '__qualname__'):
+ orig_vars['__qualname__'] = cls.__qualname__
+ return metaclass(cls.__name__, cls.__bases__, orig_vars)
+ return wrapper
+
+
+def ensure_binary(s, encoding='utf-8', errors='strict'):
+ """Coerce **s** to six.binary_type.
+
+ For Python 2:
+ - `unicode` -> encoded to `str`
+ - `str` -> `str`
+
+ For Python 3:
+ - `str` -> encoded to `bytes`
+ - `bytes` -> `bytes`
+ """
+ if isinstance(s, binary_type):
+ return s
+ if isinstance(s, text_type):
+ return s.encode(encoding, errors)
+ raise TypeError("not expecting type '%s'" % type(s))
+
+
+def ensure_str(s, encoding='utf-8', errors='strict'):
+ """Coerce *s* to `str`.
+
+ For Python 2:
+ - `unicode` -> encoded to `str`
+ - `str` -> `str`
+
+ For Python 3:
+ - `str` -> `str`
+ - `bytes` -> decoded to `str`
+ """
+ # Optimization: Fast return for the common case.
+ if type(s) is str:
+ return s
+ if PY2 and isinstance(s, text_type):
+ return s.encode(encoding, errors)
+ elif PY3 and isinstance(s, binary_type):
+ return s.decode(encoding, errors)
+ elif not isinstance(s, (text_type, binary_type)):
+ raise TypeError("not expecting type '%s'" % type(s))
+ return s
+
+
+def ensure_text(s, encoding='utf-8', errors='strict'):
+ """Coerce *s* to six.text_type.
+
+ For Python 2:
+ - `unicode` -> `unicode`
+ - `str` -> `unicode`
+
+ For Python 3:
+ - `str` -> `str`
+ - `bytes` -> decoded to `str`
+ """
+ if isinstance(s, binary_type):
+ return s.decode(encoding, errors)
+ elif isinstance(s, text_type):
+ return s
+ else:
+ raise TypeError("not expecting type '%s'" % type(s))
+
+
+def python_2_unicode_compatible(klass):
+ """
+ A class decorator that defines __unicode__ and __str__ methods under Python 2.
+ Under Python 3 it does nothing.
+
+ To support Python 2 and 3 with a single code base, define a __str__ method
+ returning text and apply this decorator to the class.
+ """
+ if PY2:
+ if '__str__' not in klass.__dict__:
+ raise ValueError("@python_2_unicode_compatible cannot be applied "
+ "to %s because it doesn't define __str__()." %
+ klass.__name__)
+ klass.__unicode__ = klass.__str__
+ klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
+ return klass
+
+
+# Complete the moves implementation.
+# This code is at the end of this module to speed up module loading.
+# Turn this module into a package.
+__path__ = [] # required for PEP 302 and PEP 451
+__package__ = __name__ # see PEP 366 @ReservedAssignment
+if globals().get("__spec__") is not None:
+ __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
+# Remove other six meta path importers, since they cause problems. This can
+# happen if six is removed from sys.modules and then reloaded. (Setuptools does
+# this for some reason.)
+if sys.meta_path:
+ for i, importer in enumerate(sys.meta_path):
+ # Here's some real nastiness: Another "instance" of the six module might
+ # be floating around. Therefore, we can't use isinstance() to check for
+ # the six meta path importer, since the other six instance will have
+ # inserted an importer with different class.
+ if (type(importer).__name__ == "_SixMetaPathImporter" and
+ importer.name == __name__):
+ del sys.meta_path[i]
+ break
+ del i, importer
+# Finally, add the importer to the meta path import hook.
+sys.meta_path.append(_importer)
diff --git a/testing/web-platform/tests/tools/third_party/six/test_six.py b/testing/web-platform/tests/tools/third_party/six/test_six.py
new file mode 100644
index 0000000000..3eefce37c3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/six/test_six.py
@@ -0,0 +1,1052 @@
+# Copyright (c) 2010-2019 Benjamin Peterson
+#
+# 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.
+
+import operator
+import sys
+import types
+import unittest
+import abc
+
+import pytest
+
+import six
+
+
+def test_add_doc():
+ def f():
+ """Icky doc"""
+ pass
+ six._add_doc(f, """New doc""")
+ assert f.__doc__ == "New doc"
+
+
+def test_import_module():
+ from logging import handlers
+ m = six._import_module("logging.handlers")
+ assert m is handlers
+
+
+def test_integer_types():
+ assert isinstance(1, six.integer_types)
+ assert isinstance(-1, six.integer_types)
+ assert isinstance(six.MAXSIZE + 23, six.integer_types)
+ assert not isinstance(.1, six.integer_types)
+
+
+def test_string_types():
+ assert isinstance("hi", six.string_types)
+ assert isinstance(six.u("hi"), six.string_types)
+ assert issubclass(six.text_type, six.string_types)
+
+
+def test_class_types():
+ class X:
+ pass
+ class Y(object):
+ pass
+ assert isinstance(X, six.class_types)
+ assert isinstance(Y, six.class_types)
+ assert not isinstance(X(), six.class_types)
+
+
+def test_text_type():
+ assert type(six.u("hi")) is six.text_type
+
+
+def test_binary_type():
+ assert type(six.b("hi")) is six.binary_type
+
+
+def test_MAXSIZE():
+ try:
+ # This shouldn't raise an overflow error.
+ six.MAXSIZE.__index__()
+ except AttributeError:
+ # Before Python 2.6.
+ pass
+ pytest.raises(
+ (ValueError, OverflowError),
+ operator.mul, [None], six.MAXSIZE + 1)
+
+
+def test_lazy():
+ if six.PY3:
+ html_name = "html.parser"
+ else:
+ html_name = "HTMLParser"
+ assert html_name not in sys.modules
+ mod = six.moves.html_parser
+ assert sys.modules[html_name] is mod
+ assert "htmlparser" not in six._MovedItems.__dict__
+
+
+try:
+ import _tkinter
+except ImportError:
+ have_tkinter = False
+else:
+ have_tkinter = True
+
+have_gdbm = True
+try:
+ import gdbm
+except ImportError:
+ try:
+ import dbm.gnu
+ except ImportError:
+ have_gdbm = False
+
+@pytest.mark.parametrize("item_name",
+ [item.name for item in six._moved_attributes])
+def test_move_items(item_name):
+ """Ensure that everything loads correctly."""
+ try:
+ item = getattr(six.moves, item_name)
+ if isinstance(item, types.ModuleType):
+ __import__("six.moves." + item_name)
+ except AttributeError:
+ if item_name == "zip_longest" and sys.version_info < (2, 6):
+ pytest.skip("zip_longest only available on 2.6+")
+ except ImportError:
+ if item_name == "winreg" and not sys.platform.startswith("win"):
+ pytest.skip("Windows only module")
+ if item_name.startswith("tkinter"):
+ if not have_tkinter:
+ pytest.skip("requires tkinter")
+ if item_name == "tkinter_ttk" and sys.version_info[:2] <= (2, 6):
+ pytest.skip("ttk only available on 2.7+")
+ if item_name.startswith("dbm_gnu") and not have_gdbm:
+ pytest.skip("requires gdbm")
+ raise
+ if sys.version_info[:2] >= (2, 6):
+ assert item_name in dir(six.moves)
+
+
+@pytest.mark.parametrize("item_name",
+ [item.name for item in six._urllib_parse_moved_attributes])
+def test_move_items_urllib_parse(item_name):
+ """Ensure that everything loads correctly."""
+ if item_name == "ParseResult" and sys.version_info < (2, 5):
+ pytest.skip("ParseResult is only found on 2.5+")
+ if item_name in ("parse_qs", "parse_qsl") and sys.version_info < (2, 6):
+ pytest.skip("parse_qs[l] is new in 2.6")
+ if sys.version_info[:2] >= (2, 6):
+ assert item_name in dir(six.moves.urllib.parse)
+ getattr(six.moves.urllib.parse, item_name)
+
+
+@pytest.mark.parametrize("item_name",
+ [item.name for item in six._urllib_error_moved_attributes])
+def test_move_items_urllib_error(item_name):
+ """Ensure that everything loads correctly."""
+ if sys.version_info[:2] >= (2, 6):
+ assert item_name in dir(six.moves.urllib.error)
+ getattr(six.moves.urllib.error, item_name)
+
+
+@pytest.mark.parametrize("item_name",
+ [item.name for item in six._urllib_request_moved_attributes])
+def test_move_items_urllib_request(item_name):
+ """Ensure that everything loads correctly."""
+ if sys.version_info[:2] >= (2, 6):
+ assert item_name in dir(six.moves.urllib.request)
+ getattr(six.moves.urllib.request, item_name)
+
+
+@pytest.mark.parametrize("item_name",
+ [item.name for item in six._urllib_response_moved_attributes])
+def test_move_items_urllib_response(item_name):
+ """Ensure that everything loads correctly."""
+ if sys.version_info[:2] >= (2, 6):
+ assert item_name in dir(six.moves.urllib.response)
+ getattr(six.moves.urllib.response, item_name)
+
+
+@pytest.mark.parametrize("item_name",
+ [item.name for item in six._urllib_robotparser_moved_attributes])
+def test_move_items_urllib_robotparser(item_name):
+ """Ensure that everything loads correctly."""
+ if sys.version_info[:2] >= (2, 6):
+ assert item_name in dir(six.moves.urllib.robotparser)
+ getattr(six.moves.urllib.robotparser, item_name)
+
+
+def test_import_moves_error_1():
+ from six.moves.urllib.parse import urljoin
+ from six import moves
+ # In 1.4.1: AttributeError: 'Module_six_moves_urllib_parse' object has no attribute 'urljoin'
+ assert moves.urllib.parse.urljoin
+
+
+def test_import_moves_error_2():
+ from six import moves
+ assert moves.urllib.parse.urljoin
+ # In 1.4.1: ImportError: cannot import name urljoin
+ from six.moves.urllib.parse import urljoin
+
+
+def test_import_moves_error_3():
+ from six.moves.urllib.parse import urljoin
+ # In 1.4.1: ImportError: cannot import name urljoin
+ from six.moves.urllib_parse import urljoin
+
+
+def test_from_imports():
+ from six.moves.queue import Queue
+ assert isinstance(Queue, six.class_types)
+ from six.moves.configparser import ConfigParser
+ assert isinstance(ConfigParser, six.class_types)
+
+
+def test_filter():
+ from six.moves import filter
+ f = filter(lambda x: x % 2, range(10))
+ assert six.advance_iterator(f) == 1
+
+
+def test_filter_false():
+ from six.moves import filterfalse
+ f = filterfalse(lambda x: x % 3, range(10))
+ assert six.advance_iterator(f) == 0
+ assert six.advance_iterator(f) == 3
+ assert six.advance_iterator(f) == 6
+
+def test_map():
+ from six.moves import map
+ assert six.advance_iterator(map(lambda x: x + 1, range(2))) == 1
+
+
+def test_getoutput():
+ from six.moves import getoutput
+ output = getoutput('echo "foo"')
+ assert output == 'foo'
+
+
+def test_zip():
+ from six.moves import zip
+ assert six.advance_iterator(zip(range(2), range(2))) == (0, 0)
+
+
+@pytest.mark.skipif("sys.version_info < (2, 6)")
+def test_zip_longest():
+ from six.moves import zip_longest
+ it = zip_longest(range(2), range(1))
+
+ assert six.advance_iterator(it) == (0, 0)
+ assert six.advance_iterator(it) == (1, None)
+
+
+class TestCustomizedMoves:
+
+ def teardown_method(self, meth):
+ try:
+ del six._MovedItems.spam
+ except AttributeError:
+ pass
+ try:
+ del six.moves.__dict__["spam"]
+ except KeyError:
+ pass
+
+
+ def test_moved_attribute(self):
+ attr = six.MovedAttribute("spam", "foo", "bar")
+ if six.PY3:
+ assert attr.mod == "bar"
+ else:
+ assert attr.mod == "foo"
+ assert attr.attr == "spam"
+ attr = six.MovedAttribute("spam", "foo", "bar", "lemma")
+ assert attr.attr == "lemma"
+ attr = six.MovedAttribute("spam", "foo", "bar", "lemma", "theorm")
+ if six.PY3:
+ assert attr.attr == "theorm"
+ else:
+ assert attr.attr == "lemma"
+
+
+ def test_moved_module(self):
+ attr = six.MovedModule("spam", "foo")
+ if six.PY3:
+ assert attr.mod == "spam"
+ else:
+ assert attr.mod == "foo"
+ attr = six.MovedModule("spam", "foo", "bar")
+ if six.PY3:
+ assert attr.mod == "bar"
+ else:
+ assert attr.mod == "foo"
+
+
+ def test_custom_move_module(self):
+ attr = six.MovedModule("spam", "six", "six")
+ six.add_move(attr)
+ six.remove_move("spam")
+ assert not hasattr(six.moves, "spam")
+ attr = six.MovedModule("spam", "six", "six")
+ six.add_move(attr)
+ from six.moves import spam
+ assert spam is six
+ six.remove_move("spam")
+ assert not hasattr(six.moves, "spam")
+
+
+ def test_custom_move_attribute(self):
+ attr = six.MovedAttribute("spam", "six", "six", "u", "u")
+ six.add_move(attr)
+ six.remove_move("spam")
+ assert not hasattr(six.moves, "spam")
+ attr = six.MovedAttribute("spam", "six", "six", "u", "u")
+ six.add_move(attr)
+ from six.moves import spam
+ assert spam is six.u
+ six.remove_move("spam")
+ assert not hasattr(six.moves, "spam")
+
+
+ def test_empty_remove(self):
+ pytest.raises(AttributeError, six.remove_move, "eggs")
+
+
+def test_get_unbound_function():
+ class X(object):
+ def m(self):
+ pass
+ assert six.get_unbound_function(X.m) is X.__dict__["m"]
+
+
+def test_get_method_self():
+ class X(object):
+ def m(self):
+ pass
+ x = X()
+ assert six.get_method_self(x.m) is x
+ pytest.raises(AttributeError, six.get_method_self, 42)
+
+
+def test_get_method_function():
+ class X(object):
+ def m(self):
+ pass
+ x = X()
+ assert six.get_method_function(x.m) is X.__dict__["m"]
+ pytest.raises(AttributeError, six.get_method_function, hasattr)
+
+
+def test_get_function_closure():
+ def f():
+ x = 42
+ def g():
+ return x
+ return g
+ cell = six.get_function_closure(f())[0]
+ assert type(cell).__name__ == "cell"
+
+
+def test_get_function_code():
+ def f():
+ pass
+ assert isinstance(six.get_function_code(f), types.CodeType)
+ if not hasattr(sys, "pypy_version_info"):
+ pytest.raises(AttributeError, six.get_function_code, hasattr)
+
+
+def test_get_function_defaults():
+ def f(x, y=3, b=4):
+ pass
+ assert six.get_function_defaults(f) == (3, 4)
+
+
+def test_get_function_globals():
+ def f():
+ pass
+ assert six.get_function_globals(f) is globals()
+
+
+def test_dictionary_iterators(monkeypatch):
+ def stock_method_name(iterwhat):
+ """Given a method suffix like "lists" or "values", return the name
+ of the dict method that delivers those on the version of Python
+ we're running in."""
+ if six.PY3:
+ return iterwhat
+ return 'iter' + iterwhat
+
+ class MyDict(dict):
+ if not six.PY3:
+ def lists(self, **kw):
+ return [1, 2, 3]
+ def iterlists(self, **kw):
+ return iter([1, 2, 3])
+ f = MyDict.iterlists
+ del MyDict.iterlists
+ setattr(MyDict, stock_method_name('lists'), f)
+
+ d = MyDict(zip(range(10), reversed(range(10))))
+ for name in "keys", "values", "items", "lists":
+ meth = getattr(six, "iter" + name)
+ it = meth(d)
+ assert not isinstance(it, list)
+ assert list(it) == list(getattr(d, name)())
+ pytest.raises(StopIteration, six.advance_iterator, it)
+ record = []
+ def with_kw(*args, **kw):
+ record.append(kw["kw"])
+ return old(*args)
+ old = getattr(MyDict, stock_method_name(name))
+ monkeypatch.setattr(MyDict, stock_method_name(name), with_kw)
+ meth(d, kw=42)
+ assert record == [42]
+ monkeypatch.undo()
+
+
+@pytest.mark.skipif("sys.version_info[:2] < (2, 7)",
+ reason="view methods on dictionaries only available on 2.7+")
+def test_dictionary_views():
+ d = dict(zip(range(10), (range(11, 20))))
+ for name in "keys", "values", "items":
+ meth = getattr(six, "view" + name)
+ view = meth(d)
+ assert set(view) == set(getattr(d, name)())
+
+
+def test_advance_iterator():
+ assert six.next is six.advance_iterator
+ l = [1, 2]
+ it = iter(l)
+ assert six.next(it) == 1
+ assert six.next(it) == 2
+ pytest.raises(StopIteration, six.next, it)
+ pytest.raises(StopIteration, six.next, it)
+
+
+def test_iterator():
+ class myiter(six.Iterator):
+ def __next__(self):
+ return 13
+ assert six.advance_iterator(myiter()) == 13
+ class myitersub(myiter):
+ def __next__(self):
+ return 14
+ assert six.advance_iterator(myitersub()) == 14
+
+
+def test_callable():
+ class X:
+ def __call__(self):
+ pass
+ def method(self):
+ pass
+ assert six.callable(X)
+ assert six.callable(X())
+ assert six.callable(test_callable)
+ assert six.callable(hasattr)
+ assert six.callable(X.method)
+ assert six.callable(X().method)
+ assert not six.callable(4)
+ assert not six.callable("string")
+
+
+def test_create_bound_method():
+ class X(object):
+ pass
+ def f(self):
+ return self
+ x = X()
+ b = six.create_bound_method(f, x)
+ assert isinstance(b, types.MethodType)
+ assert b() is x
+
+
+def test_create_unbound_method():
+ class X(object):
+ pass
+
+ def f(self):
+ return self
+ u = six.create_unbound_method(f, X)
+ pytest.raises(TypeError, u)
+ if six.PY2:
+ assert isinstance(u, types.MethodType)
+ x = X()
+ assert f(x) is x
+
+
+if six.PY3:
+
+ def test_b():
+ data = six.b("\xff")
+ assert isinstance(data, bytes)
+ assert len(data) == 1
+ assert data == bytes([255])
+
+
+ def test_u():
+ s = six.u("hi \u0439 \U00000439 \\ \\\\ \n")
+ assert isinstance(s, str)
+ assert s == "hi \u0439 \U00000439 \\ \\\\ \n"
+
+else:
+
+ def test_b():
+ data = six.b("\xff")
+ assert isinstance(data, str)
+ assert len(data) == 1
+ assert data == "\xff"
+
+
+ def test_u():
+ s = six.u("hi \u0439 \U00000439 \\ \\\\ \n")
+ assert isinstance(s, unicode)
+ assert s == "hi \xd0\xb9 \xd0\xb9 \\ \\\\ \n".decode("utf8")
+
+
+def test_u_escapes():
+ s = six.u("\u1234")
+ assert len(s) == 1
+
+
+def test_unichr():
+ assert six.u("\u1234") == six.unichr(0x1234)
+ assert type(six.u("\u1234")) is type(six.unichr(0x1234))
+
+
+def test_int2byte():
+ assert six.int2byte(3) == six.b("\x03")
+ pytest.raises(Exception, six.int2byte, 256)
+
+
+def test_byte2int():
+ assert six.byte2int(six.b("\x03")) == 3
+ assert six.byte2int(six.b("\x03\x04")) == 3
+ pytest.raises(IndexError, six.byte2int, six.b(""))
+
+
+def test_bytesindex():
+ assert six.indexbytes(six.b("hello"), 3) == ord("l")
+
+
+def test_bytesiter():
+ it = six.iterbytes(six.b("hi"))
+ assert six.next(it) == ord("h")
+ assert six.next(it) == ord("i")
+ pytest.raises(StopIteration, six.next, it)
+
+
+def test_StringIO():
+ fp = six.StringIO()
+ fp.write(six.u("hello"))
+ assert fp.getvalue() == six.u("hello")
+
+
+def test_BytesIO():
+ fp = six.BytesIO()
+ fp.write(six.b("hello"))
+ assert fp.getvalue() == six.b("hello")
+
+
+def test_exec_():
+ def f():
+ l = []
+ six.exec_("l.append(1)")
+ assert l == [1]
+ f()
+ ns = {}
+ six.exec_("x = 42", ns)
+ assert ns["x"] == 42
+ glob = {}
+ loc = {}
+ six.exec_("global y; y = 42; x = 12", glob, loc)
+ assert glob["y"] == 42
+ assert "x" not in glob
+ assert loc["x"] == 12
+ assert "y" not in loc
+
+
+def test_reraise():
+ def get_next(tb):
+ if six.PY3:
+ return tb.tb_next.tb_next
+ else:
+ return tb.tb_next
+ e = Exception("blah")
+ try:
+ raise e
+ except Exception:
+ tp, val, tb = sys.exc_info()
+ try:
+ six.reraise(tp, val, tb)
+ except Exception:
+ tp2, value2, tb2 = sys.exc_info()
+ assert tp2 is Exception
+ assert value2 is e
+ assert tb is get_next(tb2)
+ try:
+ six.reraise(tp, val)
+ except Exception:
+ tp2, value2, tb2 = sys.exc_info()
+ assert tp2 is Exception
+ assert value2 is e
+ assert tb2 is not tb
+ try:
+ six.reraise(tp, val, tb2)
+ except Exception:
+ tp2, value2, tb3 = sys.exc_info()
+ assert tp2 is Exception
+ assert value2 is e
+ assert get_next(tb3) is tb2
+ try:
+ six.reraise(tp, None, tb)
+ except Exception:
+ tp2, value2, tb2 = sys.exc_info()
+ assert tp2 is Exception
+ assert value2 is not val
+ assert isinstance(value2, Exception)
+ assert tb is get_next(tb2)
+
+
+def test_raise_from():
+ try:
+ try:
+ raise Exception("blah")
+ except Exception:
+ ctx = sys.exc_info()[1]
+ f = Exception("foo")
+ six.raise_from(f, None)
+ except Exception:
+ tp, val, tb = sys.exc_info()
+ if sys.version_info[:2] > (3, 0):
+ # We should have done a raise f from None equivalent.
+ assert val.__cause__ is None
+ assert val.__context__ is ctx
+ if sys.version_info[:2] >= (3, 3):
+ # And that should suppress the context on the exception.
+ assert val.__suppress_context__
+ # For all versions the outer exception should have raised successfully.
+ assert str(val) == "foo"
+
+
+def test_print_():
+ save = sys.stdout
+ out = sys.stdout = six.moves.StringIO()
+ try:
+ six.print_("Hello,", "person!")
+ finally:
+ sys.stdout = save
+ assert out.getvalue() == "Hello, person!\n"
+ out = six.StringIO()
+ six.print_("Hello,", "person!", file=out)
+ assert out.getvalue() == "Hello, person!\n"
+ out = six.StringIO()
+ six.print_("Hello,", "person!", file=out, end="")
+ assert out.getvalue() == "Hello, person!"
+ out = six.StringIO()
+ six.print_("Hello,", "person!", file=out, sep="X")
+ assert out.getvalue() == "Hello,Xperson!\n"
+ out = six.StringIO()
+ six.print_(six.u("Hello,"), six.u("person!"), file=out)
+ result = out.getvalue()
+ assert isinstance(result, six.text_type)
+ assert result == six.u("Hello, person!\n")
+ six.print_("Hello", file=None) # This works.
+ out = six.StringIO()
+ six.print_(None, file=out)
+ assert out.getvalue() == "None\n"
+ class FlushableStringIO(six.StringIO):
+ def __init__(self):
+ six.StringIO.__init__(self)
+ self.flushed = False
+ def flush(self):
+ self.flushed = True
+ out = FlushableStringIO()
+ six.print_("Hello", file=out)
+ assert not out.flushed
+ six.print_("Hello", file=out, flush=True)
+ assert out.flushed
+
+
+@pytest.mark.skipif("sys.version_info[:2] >= (2, 6)")
+def test_print_encoding(monkeypatch):
+ # Fool the type checking in print_.
+ monkeypatch.setattr(six, "file", six.BytesIO, raising=False)
+ out = six.BytesIO()
+ out.encoding = "utf-8"
+ out.errors = None
+ six.print_(six.u("\u053c"), end="", file=out)
+ assert out.getvalue() == six.b("\xd4\xbc")
+ out = six.BytesIO()
+ out.encoding = "ascii"
+ out.errors = "strict"
+ pytest.raises(UnicodeEncodeError, six.print_, six.u("\u053c"), file=out)
+ out.errors = "backslashreplace"
+ six.print_(six.u("\u053c"), end="", file=out)
+ assert out.getvalue() == six.b("\\u053c")
+
+
+def test_print_exceptions():
+ pytest.raises(TypeError, six.print_, x=3)
+ pytest.raises(TypeError, six.print_, end=3)
+ pytest.raises(TypeError, six.print_, sep=42)
+
+
+def test_with_metaclass():
+ class Meta(type):
+ pass
+ class X(six.with_metaclass(Meta)):
+ pass
+ assert type(X) is Meta
+ assert issubclass(X, object)
+ class Base(object):
+ pass
+ class X(six.with_metaclass(Meta, Base)):
+ pass
+ assert type(X) is Meta
+ assert issubclass(X, Base)
+ class Base2(object):
+ pass
+ class X(six.with_metaclass(Meta, Base, Base2)):
+ pass
+ assert type(X) is Meta
+ assert issubclass(X, Base)
+ assert issubclass(X, Base2)
+ assert X.__mro__ == (X, Base, Base2, object)
+ class X(six.with_metaclass(Meta)):
+ pass
+ class MetaSub(Meta):
+ pass
+ class Y(six.with_metaclass(MetaSub, X)):
+ pass
+ assert type(Y) is MetaSub
+ assert Y.__mro__ == (Y, X, object)
+
+
+@pytest.mark.skipif("sys.version_info[:2] < (2, 7)")
+def test_with_metaclass_typing():
+ try:
+ import typing
+ except ImportError:
+ pytest.skip("typing module required")
+ class Meta(type):
+ pass
+ if sys.version_info[:2] < (3, 7):
+ # Generics with custom metaclasses were broken on older versions.
+ class Meta(Meta, typing.GenericMeta):
+ pass
+ T = typing.TypeVar('T')
+ class G(six.with_metaclass(Meta, typing.Generic[T])):
+ pass
+ class GA(six.with_metaclass(abc.ABCMeta, typing.Generic[T])):
+ pass
+ assert isinstance(G, Meta)
+ assert isinstance(GA, abc.ABCMeta)
+ assert G[int] is not G[G[int]]
+ assert GA[int] is not GA[GA[int]]
+ assert G.__bases__ == (typing.Generic,)
+ assert G.__orig_bases__ == (typing.Generic[T],)
+
+
+@pytest.mark.skipif("sys.version_info[:2] < (3, 7)")
+def test_with_metaclass_pep_560():
+ class Meta(type):
+ pass
+ class A:
+ pass
+ class B:
+ pass
+ class Fake:
+ def __mro_entries__(self, bases):
+ return (A, B)
+ fake = Fake()
+ class G(six.with_metaclass(Meta, fake)):
+ pass
+ class GA(six.with_metaclass(abc.ABCMeta, fake)):
+ pass
+ assert isinstance(G, Meta)
+ assert isinstance(GA, abc.ABCMeta)
+ assert G.__bases__ == (A, B)
+ assert G.__orig_bases__ == (fake,)
+
+
+@pytest.mark.skipif("sys.version_info[:2] < (3, 0)")
+def test_with_metaclass_prepare():
+ """Test that with_metaclass causes Meta.__prepare__ to be called with the correct arguments."""
+
+ class MyDict(dict):
+ pass
+
+ class Meta(type):
+
+ @classmethod
+ def __prepare__(cls, name, bases):
+ namespace = MyDict(super().__prepare__(name, bases), cls=cls, bases=bases)
+ namespace['namespace'] = namespace
+ return namespace
+
+ class Base(object):
+ pass
+
+ bases = (Base,)
+
+ class X(six.with_metaclass(Meta, *bases)):
+ pass
+
+ assert getattr(X, 'cls', type) is Meta
+ assert getattr(X, 'bases', ()) == bases
+ assert isinstance(getattr(X, 'namespace', {}), MyDict)
+
+
+def test_wraps():
+ def f(g):
+ @six.wraps(g)
+ def w():
+ return 42
+ return w
+ def k():
+ pass
+ original_k = k
+ k = f(f(k))
+ assert hasattr(k, '__wrapped__')
+ k = k.__wrapped__
+ assert hasattr(k, '__wrapped__')
+ k = k.__wrapped__
+ assert k is original_k
+ assert not hasattr(k, '__wrapped__')
+
+ def f(g, assign, update):
+ def w():
+ return 42
+ w.glue = {"foo" : "bar"}
+ return six.wraps(g, assign, update)(w)
+ k.glue = {"melon" : "egg"}
+ k.turnip = 43
+ k = f(k, ["turnip"], ["glue"])
+ assert k.__name__ == "w"
+ assert k.turnip == 43
+ assert k.glue == {"melon" : "egg", "foo" : "bar"}
+
+
+def test_add_metaclass():
+ class Meta(type):
+ pass
+ class X:
+ "success"
+ X = six.add_metaclass(Meta)(X)
+ assert type(X) is Meta
+ assert issubclass(X, object)
+ assert X.__module__ == __name__
+ assert X.__doc__ == "success"
+ class Base(object):
+ pass
+ class X(Base):
+ pass
+ X = six.add_metaclass(Meta)(X)
+ assert type(X) is Meta
+ assert issubclass(X, Base)
+ class Base2(object):
+ pass
+ class X(Base, Base2):
+ pass
+ X = six.add_metaclass(Meta)(X)
+ assert type(X) is Meta
+ assert issubclass(X, Base)
+ assert issubclass(X, Base2)
+
+ # Test a second-generation subclass of a type.
+ class Meta1(type):
+ m1 = "m1"
+ class Meta2(Meta1):
+ m2 = "m2"
+ class Base:
+ b = "b"
+ Base = six.add_metaclass(Meta1)(Base)
+ class X(Base):
+ x = "x"
+ X = six.add_metaclass(Meta2)(X)
+ assert type(X) is Meta2
+ assert issubclass(X, Base)
+ assert type(Base) is Meta1
+ assert "__dict__" not in vars(X)
+ instance = X()
+ instance.attr = "test"
+ assert vars(instance) == {"attr": "test"}
+ assert instance.b == Base.b
+ assert instance.x == X.x
+
+ # Test a class with slots.
+ class MySlots(object):
+ __slots__ = ["a", "b"]
+ MySlots = six.add_metaclass(Meta1)(MySlots)
+
+ assert MySlots.__slots__ == ["a", "b"]
+ instance = MySlots()
+ instance.a = "foo"
+ pytest.raises(AttributeError, setattr, instance, "c", "baz")
+
+ # Test a class with string for slots.
+ class MyStringSlots(object):
+ __slots__ = "ab"
+ MyStringSlots = six.add_metaclass(Meta1)(MyStringSlots)
+ assert MyStringSlots.__slots__ == "ab"
+ instance = MyStringSlots()
+ instance.ab = "foo"
+ pytest.raises(AttributeError, setattr, instance, "a", "baz")
+ pytest.raises(AttributeError, setattr, instance, "b", "baz")
+
+ class MySlotsWeakref(object):
+ __slots__ = "__weakref__",
+ MySlotsWeakref = six.add_metaclass(Meta)(MySlotsWeakref)
+ assert type(MySlotsWeakref) is Meta
+
+
+@pytest.mark.skipif("sys.version_info[:2] < (3, 3)")
+def test_add_metaclass_nested():
+ # Regression test for https://github.com/benjaminp/six/issues/259
+ class Meta(type):
+ pass
+
+ class A:
+ class B: pass
+
+ expected = 'test_add_metaclass_nested.<locals>.A.B'
+
+ assert A.B.__qualname__ == expected
+
+ class A:
+ @six.add_metaclass(Meta)
+ class B: pass
+
+ assert A.B.__qualname__ == expected
+
+
+@pytest.mark.skipif("sys.version_info[:2] < (2, 7) or sys.version_info[:2] in ((3, 0), (3, 1))")
+def test_assertCountEqual():
+ class TestAssertCountEqual(unittest.TestCase):
+ def test(self):
+ with self.assertRaises(AssertionError):
+ six.assertCountEqual(self, (1, 2), [3, 4, 5])
+
+ six.assertCountEqual(self, (1, 2), [2, 1])
+
+ TestAssertCountEqual('test').test()
+
+
+@pytest.mark.skipif("sys.version_info[:2] < (2, 7)")
+def test_assertRegex():
+ class TestAssertRegex(unittest.TestCase):
+ def test(self):
+ with self.assertRaises(AssertionError):
+ six.assertRegex(self, 'test', r'^a')
+
+ six.assertRegex(self, 'test', r'^t')
+
+ TestAssertRegex('test').test()
+
+
+@pytest.mark.skipif("sys.version_info[:2] < (2, 7)")
+def test_assertRaisesRegex():
+ class TestAssertRaisesRegex(unittest.TestCase):
+ def test(self):
+ with six.assertRaisesRegex(self, AssertionError, '^Foo'):
+ raise AssertionError('Foo')
+
+ with self.assertRaises(AssertionError):
+ with six.assertRaisesRegex(self, AssertionError, r'^Foo'):
+ raise AssertionError('Bar')
+
+ TestAssertRaisesRegex('test').test()
+
+
+def test_python_2_unicode_compatible():
+ @six.python_2_unicode_compatible
+ class MyTest(object):
+ def __str__(self):
+ return six.u('hello')
+
+ def __bytes__(self):
+ return six.b('hello')
+
+ my_test = MyTest()
+
+ if six.PY2:
+ assert str(my_test) == six.b("hello")
+ assert unicode(my_test) == six.u("hello")
+ elif six.PY3:
+ assert bytes(my_test) == six.b("hello")
+ assert str(my_test) == six.u("hello")
+
+ assert getattr(six.moves.builtins, 'bytes', str)(my_test) == six.b("hello")
+
+
+class EnsureTests:
+
+ # grinning face emoji
+ UNICODE_EMOJI = six.u("\U0001F600")
+ BINARY_EMOJI = b"\xf0\x9f\x98\x80"
+
+ def test_ensure_binary_raise_type_error(self):
+ with pytest.raises(TypeError):
+ six.ensure_str(8)
+
+ def test_errors_and_encoding(self):
+ six.ensure_binary(self.UNICODE_EMOJI, encoding='latin-1', errors='ignore')
+ with pytest.raises(UnicodeEncodeError):
+ six.ensure_binary(self.UNICODE_EMOJI, encoding='latin-1', errors='strict')
+
+ def test_ensure_binary_raise(self):
+ converted_unicode = six.ensure_binary(self.UNICODE_EMOJI, encoding='utf-8', errors='strict')
+ converted_binary = six.ensure_binary(self.BINARY_EMOJI, encoding="utf-8", errors='strict')
+ if six.PY2:
+ # PY2: unicode -> str
+ assert converted_unicode == self.BINARY_EMOJI and isinstance(converted_unicode, str)
+ # PY2: str -> str
+ assert converted_binary == self.BINARY_EMOJI and isinstance(converted_binary, str)
+ else:
+ # PY3: str -> bytes
+ assert converted_unicode == self.BINARY_EMOJI and isinstance(converted_unicode, bytes)
+ # PY3: bytes -> bytes
+ assert converted_binary == self.BINARY_EMOJI and isinstance(converted_binary, bytes)
+
+ def test_ensure_str(self):
+ converted_unicode = six.ensure_str(self.UNICODE_EMOJI, encoding='utf-8', errors='strict')
+ converted_binary = six.ensure_str(self.BINARY_EMOJI, encoding="utf-8", errors='strict')
+ if six.PY2:
+ # PY2: unicode -> str
+ assert converted_unicode == self.BINARY_EMOJI and isinstance(converted_unicode, str)
+ # PY2: str -> str
+ assert converted_binary == self.BINARY_EMOJI and isinstance(converted_binary, str)
+ else:
+ # PY3: str -> str
+ assert converted_unicode == self.UNICODE_EMOJI and isinstance(converted_unicode, str)
+ # PY3: bytes -> str
+ assert converted_binary == self.UNICODE_EMOJI and isinstance(converted_unicode, str)
+
+ def test_ensure_text(self):
+ converted_unicode = six.ensure_text(self.UNICODE_EMOJI, encoding='utf-8', errors='strict')
+ converted_binary = six.ensure_text(self.BINARY_EMOJI, encoding="utf-8", errors='strict')
+ if six.PY2:
+ # PY2: unicode -> unicode
+ assert converted_unicode == self.UNICODE_EMOJI and isinstance(converted_unicode, unicode)
+ # PY2: str -> unicode
+ assert converted_binary == self.UNICODE_EMOJI and isinstance(converted_unicode, unicode)
+ else:
+ # PY3: str -> str
+ assert converted_unicode == self.UNICODE_EMOJI and isinstance(converted_unicode, str)
+ # PY3: bytes -> str
+ assert converted_binary == self.UNICODE_EMOJI and isinstance(converted_unicode, str)
diff --git a/testing/web-platform/tests/tools/third_party/tooltool/tooltool.py b/testing/web-platform/tests/tools/third_party/tooltool/tooltool.py
new file mode 100755
index 0000000000..5abc98a09d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/tooltool/tooltool.py
@@ -0,0 +1,1704 @@
+#!/usr/bin/env python3
+
+# tooltool is a lookaside cache implemented in Python
+# Copyright (C) 2011 John H. Ford <john@johnford.info>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation version 2
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+# A manifest file specifies files in that directory that are stored
+# elsewhere. This file should only list files in the same directory
+# in which the manifest file resides and it should be called
+# 'manifest.tt'
+
+import base64
+import calendar
+import hashlib
+import hmac
+import json
+import logging
+import math
+import optparse
+import os
+import pprint
+import re
+import shutil
+import stat
+import sys
+import tarfile
+import tempfile
+import threading
+import time
+import zipfile
+from contextlib import closing, contextmanager
+from functools import wraps
+from io import BytesIO, open
+from random import random
+from subprocess import PIPE, Popen
+
+__version__ = "1.4.0"
+
+# Allowed request header characters:
+# !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9, \, "
+REQUEST_HEADER_ATTRIBUTE_CHARS = re.compile(
+ r"^[ a-zA-Z0-9_\!#\$%&'\(\)\*\+,\-\./\:;<\=>\?@\[\]\^`\{\|\}~]*$"
+)
+DEFAULT_MANIFEST_NAME = "manifest.tt"
+TOOLTOOL_PACKAGE_SUFFIX = ".TOOLTOOL-PACKAGE"
+HAWK_VER = 1
+PY3 = sys.version_info[0] == 3
+
+if PY3:
+ six_binary_type = bytes
+ unicode = (
+ str # Silence `pyflakes` from reporting `undefined name 'unicode'` in Python 3.
+ )
+ import urllib.request as urllib2
+ from http.client import HTTPConnection, HTTPSConnection
+ from urllib.error import HTTPError, URLError
+ from urllib.parse import urljoin, urlparse
+ from urllib.request import Request
+else:
+ six_binary_type = str
+ import urllib2
+ from httplib import HTTPConnection, HTTPSConnection
+ from urllib2 import HTTPError, Request, URLError
+ from urlparse import urljoin, urlparse
+
+
+log = logging.getLogger(__name__)
+
+
+# Vendored code from `redo` module
+def retrier(attempts=5, sleeptime=10, max_sleeptime=300, sleepscale=1.5, jitter=1):
+ """
+ This function originates from redo 2.0.3 https://github.com/mozilla-releng/redo
+ A generator function that sleeps between retries, handles exponential
+ backoff and jitter. The action you are retrying is meant to run after
+ retrier yields.
+ """
+ jitter = jitter or 0 # py35 barfs on the next line if jitter is None
+ if jitter > sleeptime:
+ # To prevent negative sleep times
+ raise Exception(
+ "jitter ({}) must be less than sleep time ({})".format(jitter, sleeptime)
+ )
+
+ sleeptime_real = sleeptime
+ for _ in range(attempts):
+ log.debug("attempt %i/%i", _ + 1, attempts)
+
+ yield sleeptime_real
+
+ if jitter:
+ sleeptime_real = sleeptime + random.uniform(-jitter, jitter)
+ # our jitter should scale along with the sleeptime
+ jitter = jitter * sleepscale
+ else:
+ sleeptime_real = sleeptime
+
+ sleeptime *= sleepscale
+
+ if sleeptime_real > max_sleeptime:
+ sleeptime_real = max_sleeptime
+
+ # Don't need to sleep the last time
+ if _ < attempts - 1:
+ log.debug(
+ "sleeping for %.2fs (attempt %i/%i)", sleeptime_real, _ + 1, attempts
+ )
+ time.sleep(sleeptime_real)
+
+
+def retry(
+ action,
+ attempts=5,
+ sleeptime=60,
+ max_sleeptime=5 * 60,
+ sleepscale=1.5,
+ jitter=1,
+ retry_exceptions=(Exception,),
+ cleanup=None,
+ args=(),
+ kwargs={},
+ log_args=True,
+):
+ """
+ This function originates from redo 2.0.3 https://github.com/mozilla-releng/redo
+ Calls an action function until it succeeds, or we give up.
+ """
+ assert callable(action)
+ assert not cleanup or callable(cleanup)
+
+ action_name = getattr(action, "__name__", action)
+ if log_args and (args or kwargs):
+ log_attempt_args = (
+ "retry: calling %s with args: %s," " kwargs: %s, attempt #%d",
+ action_name,
+ args,
+ kwargs,
+ )
+ else:
+ log_attempt_args = ("retry: calling %s, attempt #%d", action_name)
+
+ if max_sleeptime < sleeptime:
+ log.debug("max_sleeptime %d less than sleeptime %d", max_sleeptime, sleeptime)
+
+ n = 1
+ for _ in retrier(
+ attempts=attempts,
+ sleeptime=sleeptime,
+ max_sleeptime=max_sleeptime,
+ sleepscale=sleepscale,
+ jitter=jitter,
+ ):
+ try:
+ logfn = log.info if n != 1 else log.debug
+ logfn_args = log_attempt_args + (n,)
+ logfn(*logfn_args)
+ return action(*args, **kwargs)
+ except retry_exceptions:
+ log.debug("retry: Caught exception: ", exc_info=True)
+ if cleanup:
+ cleanup()
+ if n == attempts:
+ log.info("retry: Giving up on %s", action_name)
+ raise
+ continue
+ finally:
+ n += 1
+
+
+def retriable(*retry_args, **retry_kwargs):
+ """
+ This function originates from redo 2.0.3 https://github.com/mozilla-releng/redo
+ A decorator factory for retry(). Wrap your function in @retriable(...) to
+ give it retry powers!
+ """
+
+ def _retriable_factory(func):
+ @wraps(func)
+ def _retriable_wrapper(*args, **kwargs):
+ return retry(func, args=args, kwargs=kwargs, *retry_args, **retry_kwargs)
+
+ return _retriable_wrapper
+
+ return _retriable_factory
+
+
+# end of vendored code from redo module
+
+
+def request_has_data(req):
+ if PY3:
+ return req.data is not None
+ return req.has_data()
+
+
+def get_hexdigest(val):
+ return hashlib.sha512(val).hexdigest()
+
+
+class FileRecordJSONEncoderException(Exception):
+ pass
+
+
+class InvalidManifest(Exception):
+ pass
+
+
+class ExceptionWithFilename(Exception):
+ def __init__(self, filename):
+ Exception.__init__(self)
+ self.filename = filename
+
+
+class BadFilenameException(ExceptionWithFilename):
+ pass
+
+
+class DigestMismatchException(ExceptionWithFilename):
+ pass
+
+
+class MissingFileException(ExceptionWithFilename):
+ pass
+
+
+class InvalidCredentials(Exception):
+ pass
+
+
+class BadHeaderValue(Exception):
+ pass
+
+
+def parse_url(url):
+ url_parts = urlparse(url)
+ url_dict = {
+ "scheme": url_parts.scheme,
+ "hostname": url_parts.hostname,
+ "port": url_parts.port,
+ "path": url_parts.path,
+ "resource": url_parts.path,
+ "query": url_parts.query,
+ }
+ if len(url_dict["query"]) > 0:
+ url_dict["resource"] = "%s?%s" % (
+ url_dict["resource"], # pragma: no cover
+ url_dict["query"],
+ )
+
+ if url_parts.port is None:
+ if url_parts.scheme == "http":
+ url_dict["port"] = 80
+ elif url_parts.scheme == "https": # pragma: no cover
+ url_dict["port"] = 443
+ return url_dict
+
+
+def utc_now(offset_in_seconds=0.0):
+ return int(math.floor(calendar.timegm(time.gmtime()) + float(offset_in_seconds)))
+
+
+def random_string(length):
+ return base64.urlsafe_b64encode(os.urandom(length))[:length]
+
+
+def prepare_header_val(val):
+ if isinstance(val, six_binary_type):
+ val = val.decode("utf-8")
+
+ if not REQUEST_HEADER_ATTRIBUTE_CHARS.match(val):
+ raise BadHeaderValue( # pragma: no cover
+ "header value value={val} contained an illegal character".format(
+ val=repr(val)
+ )
+ )
+
+ return val
+
+
+def parse_content_type(content_type): # pragma: no cover
+ if content_type:
+ return content_type.split(";")[0].strip().lower()
+ else:
+ return ""
+
+
+def calculate_payload_hash(algorithm, payload, content_type): # pragma: no cover
+ parts = [
+ part if isinstance(part, six_binary_type) else part.encode("utf8")
+ for part in [
+ "hawk." + str(HAWK_VER) + ".payload\n",
+ parse_content_type(content_type) + "\n",
+ payload or "",
+ "\n",
+ ]
+ ]
+
+ p_hash = hashlib.new(algorithm)
+ for p in parts:
+ p_hash.update(p)
+
+ log.debug(
+ "calculating payload hash from:\n{parts}".format(parts=pprint.pformat(parts))
+ )
+
+ return base64.b64encode(p_hash.digest())
+
+
+def validate_taskcluster_credentials(credentials):
+ if not hasattr(credentials, "__getitem__"):
+ raise InvalidCredentials(
+ "credentials must be a dict-like object"
+ ) # pragma: no cover
+ try:
+ credentials["clientId"]
+ credentials["accessToken"]
+ except KeyError: # pragma: no cover
+ etype, val, tb = sys.exc_info()
+ raise InvalidCredentials("{etype}: {val}".format(etype=etype, val=val))
+
+
+def normalize_header_attr(val):
+ if isinstance(val, six_binary_type):
+ return val.decode("utf-8")
+ return val # pragma: no cover
+
+
+def normalize_string(
+ mac_type,
+ timestamp,
+ nonce,
+ method,
+ name,
+ host,
+ port,
+ content_hash,
+):
+ return "\n".join(
+ [
+ normalize_header_attr(header)
+ # The blank lines are important. They follow what the Node Hawk lib does.
+ for header in [
+ "hawk." + str(HAWK_VER) + "." + mac_type,
+ timestamp,
+ nonce,
+ method or "",
+ name or "",
+ host,
+ port,
+ content_hash or "",
+ "", # for ext which is empty in this case
+ "", # Add trailing new line.
+ ]
+ ]
+ )
+
+
+def calculate_mac(
+ mac_type,
+ access_token,
+ algorithm,
+ timestamp,
+ nonce,
+ method,
+ name,
+ host,
+ port,
+ content_hash,
+):
+ normalized = normalize_string(
+ mac_type, timestamp, nonce, method, name, host, port, content_hash
+ )
+ log.debug(u"normalized resource for mac calc: {norm}".format(norm=normalized))
+ digestmod = getattr(hashlib, algorithm)
+
+ if not isinstance(normalized, six_binary_type):
+ normalized = normalized.encode("utf8")
+
+ if not isinstance(access_token, six_binary_type):
+ access_token = access_token.encode("ascii")
+
+ result = hmac.new(access_token, normalized, digestmod)
+ return base64.b64encode(result.digest())
+
+
+def make_taskcluster_header(credentials, req):
+ validate_taskcluster_credentials(credentials)
+
+ url = req.get_full_url()
+ method = req.get_method()
+ algorithm = "sha256"
+ timestamp = str(utc_now())
+ nonce = random_string(6)
+ url_parts = parse_url(url)
+
+ content_hash = None
+ if request_has_data(req):
+ if PY3:
+ data = req.data
+ else:
+ data = req.get_data()
+ content_hash = calculate_payload_hash( # pragma: no cover
+ algorithm,
+ data,
+ # maybe we should detect this from req.headers but we anyway expect json
+ content_type="application/json",
+ )
+
+ mac = calculate_mac(
+ "header",
+ credentials["accessToken"],
+ algorithm,
+ timestamp,
+ nonce,
+ method,
+ url_parts["resource"],
+ url_parts["hostname"],
+ str(url_parts["port"]),
+ content_hash,
+ )
+
+ header = u'Hawk mac="{}"'.format(prepare_header_val(mac))
+
+ if content_hash: # pragma: no cover
+ header = u'{}, hash="{}"'.format(header, prepare_header_val(content_hash))
+
+ header = u'{header}, id="{id}", ts="{ts}", nonce="{nonce}"'.format(
+ header=header,
+ id=prepare_header_val(credentials["clientId"]),
+ ts=prepare_header_val(timestamp),
+ nonce=prepare_header_val(nonce),
+ )
+
+ log.debug("Hawk header for URL={} method={}: {}".format(url, method, header))
+
+ return header
+
+
+class FileRecord(object):
+ def __init__(
+ self,
+ filename,
+ size,
+ digest,
+ algorithm,
+ unpack=False,
+ version=None,
+ visibility=None,
+ ):
+ object.__init__(self)
+ if "/" in filename or "\\" in filename:
+ log.error(
+ "The filename provided contains path information and is, therefore, invalid."
+ )
+ raise BadFilenameException(filename=filename)
+ self.filename = filename
+ self.size = size
+ self.digest = digest
+ self.algorithm = algorithm
+ self.unpack = unpack
+ self.version = version
+ self.visibility = visibility
+
+ def __eq__(self, other):
+ if self is other:
+ return True
+ if (
+ self.filename == other.filename
+ and self.size == other.size
+ and self.digest == other.digest
+ and self.algorithm == other.algorithm
+ and self.version == other.version
+ and self.visibility == other.visibility
+ ):
+ return True
+ else:
+ return False
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __str__(self):
+ return repr(self)
+
+ def __repr__(self):
+ return (
+ "%s.%s(filename='%s', size=%s, digest='%s', algorithm='%s', visibility=%r)"
+ % (
+ __name__,
+ self.__class__.__name__,
+ self.filename,
+ self.size,
+ self.digest,
+ self.algorithm,
+ self.visibility,
+ )
+ )
+
+ def present(self):
+ # Doesn't check validity
+ return os.path.exists(self.filename)
+
+ def validate_size(self):
+ if self.present():
+ return self.size == os.path.getsize(self.filename)
+ else:
+ log.debug("trying to validate size on a missing file, %s", self.filename)
+ raise MissingFileException(filename=self.filename)
+
+ def validate_digest(self):
+ if self.present():
+ with open(self.filename, "rb") as f:
+ return self.digest == digest_file(f, self.algorithm)
+ else:
+ log.debug("trying to validate digest on a missing file, %s', self.filename")
+ raise MissingFileException(filename=self.filename)
+
+ def validate(self):
+ if self.size is None or self.validate_size():
+ if self.validate_digest():
+ return True
+ return False
+
+ def describe(self):
+ if self.present() and self.validate():
+ return "'%s' is present and valid" % self.filename
+ elif self.present():
+ return "'%s' is present and invalid" % self.filename
+ else:
+ return "'%s' is absent" % self.filename
+
+
+def create_file_record(filename, algorithm):
+ fo = open(filename, "rb")
+ stored_filename = os.path.split(filename)[1]
+ fr = FileRecord(
+ stored_filename,
+ os.path.getsize(filename),
+ digest_file(fo, algorithm),
+ algorithm,
+ )
+ fo.close()
+ return fr
+
+
+class FileRecordJSONEncoder(json.JSONEncoder):
+ def encode_file_record(self, obj):
+ if not issubclass(type(obj), FileRecord):
+ err = (
+ "FileRecordJSONEncoder is only for FileRecord and lists of FileRecords, "
+ "not %s" % obj.__class__.__name__
+ )
+ log.warn(err)
+ raise FileRecordJSONEncoderException(err)
+ else:
+ rv = {
+ "filename": obj.filename,
+ "size": obj.size,
+ "algorithm": obj.algorithm,
+ "digest": obj.digest,
+ }
+ if obj.unpack:
+ rv["unpack"] = True
+ if obj.version:
+ rv["version"] = obj.version
+ if obj.visibility is not None:
+ rv["visibility"] = obj.visibility
+ return rv
+
+ def default(self, f):
+ if issubclass(type(f), list):
+ record_list = []
+ for i in f:
+ record_list.append(self.encode_file_record(i))
+ return record_list
+ else:
+ return self.encode_file_record(f)
+
+
+class FileRecordJSONDecoder(json.JSONDecoder):
+
+ """I help the json module materialize a FileRecord from
+ a JSON file. I understand FileRecords and lists of
+ FileRecords. I ignore things that I don't expect for now"""
+
+ # TODO: make this more explicit in what it's looking for
+ # and error out on unexpected things
+
+ def process_file_records(self, obj):
+ if isinstance(obj, list):
+ record_list = []
+ for i in obj:
+ record = self.process_file_records(i)
+ if issubclass(type(record), FileRecord):
+ record_list.append(record)
+ return record_list
+ required_fields = [
+ "filename",
+ "size",
+ "algorithm",
+ "digest",
+ ]
+ if isinstance(obj, dict):
+ missing = False
+ for req in required_fields:
+ if req not in obj:
+ missing = True
+ break
+
+ if not missing:
+ unpack = obj.get("unpack", False)
+ version = obj.get("version", None)
+ visibility = obj.get("visibility", None)
+ rv = FileRecord(
+ obj["filename"],
+ obj["size"],
+ obj["digest"],
+ obj["algorithm"],
+ unpack,
+ version,
+ visibility,
+ )
+ log.debug("materialized %s" % rv)
+ return rv
+ return obj
+
+ def decode(self, s):
+ decoded = json.JSONDecoder.decode(self, s)
+ rv = self.process_file_records(decoded)
+ return rv
+
+
+class Manifest(object):
+
+ valid_formats = ("json",)
+
+ def __init__(self, file_records=None):
+ self.file_records = file_records or []
+
+ def __eq__(self, other):
+ if self is other:
+ return True
+ if len(self.file_records) != len(other.file_records):
+ log.debug("Manifests differ in number of files")
+ return False
+ # sort the file records by filename before comparing
+ mine = sorted((fr.filename, fr) for fr in self.file_records)
+ theirs = sorted((fr.filename, fr) for fr in other.file_records)
+ return mine == theirs
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __deepcopy__(self, memo):
+ # This is required for a deep copy
+ return Manifest(self.file_records[:])
+
+ def __copy__(self):
+ return Manifest(self.file_records)
+
+ def copy(self):
+ return Manifest(self.file_records[:])
+
+ def present(self):
+ return all(i.present() for i in self.file_records)
+
+ def validate_sizes(self):
+ return all(i.validate_size() for i in self.file_records)
+
+ def validate_digests(self):
+ return all(i.validate_digest() for i in self.file_records)
+
+ def validate(self):
+ return all(i.validate() for i in self.file_records)
+
+ def load(self, data_file, fmt="json"):
+ assert fmt in self.valid_formats
+ if fmt == "json":
+ try:
+ self.file_records.extend(
+ json.load(data_file, cls=FileRecordJSONDecoder)
+ )
+ except ValueError:
+ raise InvalidManifest("trying to read invalid manifest file")
+
+ def loads(self, data_string, fmt="json"):
+ assert fmt in self.valid_formats
+ if fmt == "json":
+ try:
+ self.file_records.extend(
+ json.loads(data_string, cls=FileRecordJSONDecoder)
+ )
+ except ValueError:
+ raise InvalidManifest("trying to read invalid manifest file")
+
+ def dump(self, output_file, fmt="json"):
+ assert fmt in self.valid_formats
+ if fmt == "json":
+ return json.dump(
+ self.file_records,
+ output_file,
+ indent=2,
+ separators=(",", ": "),
+ cls=FileRecordJSONEncoder,
+ )
+
+ def dumps(self, fmt="json"):
+ assert fmt in self.valid_formats
+ if fmt == "json":
+ return json.dumps(
+ self.file_records,
+ indent=2,
+ separators=(",", ": "),
+ cls=FileRecordJSONEncoder,
+ )
+
+
+def digest_file(f, a):
+ """I take a file like object 'f' and return a hex-string containing
+ of the result of the algorithm 'a' applied to 'f'."""
+ h = hashlib.new(a)
+ chunk_size = 1024 * 10
+ data = f.read(chunk_size)
+ while data:
+ h.update(data)
+ data = f.read(chunk_size)
+ name = repr(f.name) if hasattr(f, "name") else "a file"
+ log.debug("hashed %s with %s to be %s", name, a, h.hexdigest())
+ return h.hexdigest()
+
+
+def execute(cmd):
+ """Execute CMD, logging its stdout at the info level"""
+ process = Popen(cmd, shell=True, stdout=PIPE)
+ while True:
+ line = process.stdout.readline()
+ if not line:
+ break
+ log.info(line.replace("\n", " "))
+ return process.wait() == 0
+
+
+def open_manifest(manifest_file):
+ """I know how to take a filename and load it into a Manifest object"""
+ if os.path.exists(manifest_file):
+ manifest = Manifest()
+ with open(manifest_file, "r" if PY3 else "rb") as f:
+ manifest.load(f)
+ log.debug("loaded manifest from file '%s'" % manifest_file)
+ return manifest
+ else:
+ log.debug("tried to load absent file '%s' as manifest" % manifest_file)
+ raise InvalidManifest("manifest file '%s' does not exist" % manifest_file)
+
+
+def list_manifest(manifest_file):
+ """I know how print all the files in a location"""
+ try:
+ manifest = open_manifest(manifest_file)
+ except InvalidManifest as e:
+ log.error(
+ "failed to load manifest file at '%s': %s"
+ % (
+ manifest_file,
+ str(e),
+ )
+ )
+ return False
+ for f in manifest.file_records:
+ print(
+ "{}\t{}\t{}".format(
+ "P" if f.present() else "-",
+ "V" if f.present() and f.validate() else "-",
+ f.filename,
+ )
+ )
+ return True
+
+
+def validate_manifest(manifest_file):
+ """I validate that all files in a manifest are present and valid but
+ don't fetch or delete them if they aren't"""
+ try:
+ manifest = open_manifest(manifest_file)
+ except InvalidManifest as e:
+ log.error(
+ "failed to load manifest file at '%s': %s"
+ % (
+ manifest_file,
+ str(e),
+ )
+ )
+ return False
+ invalid_files = []
+ absent_files = []
+ for f in manifest.file_records:
+ if not f.present():
+ absent_files.append(f)
+ else:
+ if not f.validate():
+ invalid_files.append(f)
+ if len(invalid_files + absent_files) == 0:
+ return True
+ else:
+ return False
+
+
+def add_files(manifest_file, algorithm, filenames, version, visibility, unpack):
+ # returns True if all files successfully added, False if not
+ # and doesn't catch library Exceptions. If any files are already
+ # tracked in the manifest, return will be False because they weren't
+ # added
+ all_files_added = True
+ # Create a old_manifest object to add to
+ if os.path.exists(manifest_file):
+ old_manifest = open_manifest(manifest_file)
+ else:
+ old_manifest = Manifest()
+ log.debug("creating a new manifest file")
+ new_manifest = Manifest() # use a different manifest for the output
+ for filename in filenames:
+ log.debug("adding %s" % filename)
+ path, name = os.path.split(filename)
+ new_fr = create_file_record(filename, algorithm)
+ new_fr.version = version
+ new_fr.visibility = visibility
+ new_fr.unpack = unpack
+ log.debug("appending a new file record to manifest file")
+ add = True
+ for fr in old_manifest.file_records:
+ log.debug(
+ "manifest file has '%s'"
+ % "', ".join([x.filename for x in old_manifest.file_records])
+ )
+ if new_fr == fr:
+ log.info("file already in old_manifest")
+ add = False
+ elif filename == fr.filename:
+ log.error(
+ "manifest already contains a different file named %s" % filename
+ )
+ add = False
+ if add:
+ new_manifest.file_records.append(new_fr)
+ log.debug("added '%s' to manifest" % filename)
+ else:
+ all_files_added = False
+ # copy any files in the old manifest that aren't in the new one
+ new_filenames = set(fr.filename for fr in new_manifest.file_records)
+ for old_fr in old_manifest.file_records:
+ if old_fr.filename not in new_filenames:
+ new_manifest.file_records.append(old_fr)
+ if PY3:
+ with open(manifest_file, mode="w") as output:
+ new_manifest.dump(output, fmt="json")
+ else:
+ with open(manifest_file, mode="wb") as output:
+ new_manifest.dump(output, fmt="json")
+ return all_files_added
+
+
+def touch(f):
+ """Used to modify mtime in cached files;
+ mtime is used by the purge command"""
+ try:
+ os.utime(f, None)
+ except OSError:
+ log.warn("impossible to update utime of file %s" % f)
+
+
+@contextmanager
+@retriable(sleeptime=2)
+def request(url, auth_file=None):
+ req = Request(url)
+ _authorize(req, auth_file)
+ with closing(urllib2.urlopen(req)) as f:
+ log.debug("opened %s for reading" % url)
+ yield f
+
+
+def fetch_file(base_urls, file_record, grabchunk=1024 * 4, auth_file=None, region=None):
+ # A file which is requested to be fetched that exists locally will be
+ # overwritten by this function
+ fd, temp_path = tempfile.mkstemp(dir=os.getcwd())
+ os.close(fd)
+ fetched_path = None
+ for base_url in base_urls:
+ # Generate the URL for the file on the server side
+ url = urljoin(base_url, "%s/%s" % (file_record.algorithm, file_record.digest))
+ if region is not None:
+ url += "?region=" + region
+
+ log.info("Attempting to fetch from '%s'..." % base_url)
+
+ # Well, the file doesn't exist locally. Let's fetch it.
+ try:
+ with request(url, auth_file) as f, open(temp_path, mode="wb") as out:
+ k = True
+ size = 0
+ while k:
+ # TODO: print statistics as file transfers happen both for info and to stop
+ # buildbot timeouts
+ indata = f.read(grabchunk)
+ out.write(indata)
+ size += len(indata)
+ if len(indata) == 0:
+ k = False
+ log.info(
+ "File %s fetched from %s as %s"
+ % (file_record.filename, base_url, temp_path)
+ )
+ fetched_path = temp_path
+ break
+ except (URLError, HTTPError, ValueError):
+ log.info(
+ "...failed to fetch '%s' from %s" % (file_record.filename, base_url),
+ exc_info=True,
+ )
+ except IOError: # pragma: no cover
+ log.info(
+ "failed to write to temporary file for '%s'" % file_record.filename,
+ exc_info=True,
+ )
+
+ # cleanup temp file in case of issues
+ if fetched_path:
+ return os.path.split(fetched_path)[1]
+ else:
+ try:
+ os.remove(temp_path)
+ except OSError: # pragma: no cover
+ pass
+ return None
+
+
+def clean_path(dirname):
+ """Remove a subtree if is exists. Helper for unpack_file()."""
+ if os.path.exists(dirname):
+ log.info("rm tree: %s" % dirname)
+ shutil.rmtree(dirname)
+
+
+CHECKSUM_SUFFIX = ".checksum"
+
+
+def validate_tar_member(member, path):
+ def _is_within_directory(directory, target):
+ real_directory = os.path.realpath(directory)
+ real_target = os.path.realpath(target)
+ prefix = os.path.commonprefix([real_directory, real_target])
+ return prefix == real_directory
+
+ member_path = os.path.join(path, member.name)
+ if not _is_within_directory(path, member_path):
+ raise Exception("Attempted path traversal in tar file: " + member.name)
+ if member.issym():
+ link_path = os.path.join(os.path.dirname(member_path), member.linkname)
+ if not _is_within_directory(path, link_path):
+ raise Exception("Attempted link path traversal in tar file: " + member.name)
+ if member.mode & (stat.S_ISUID | stat.S_ISGID):
+ raise Exception("Attempted setuid or setgid in tar file: " + member.name)
+
+
+def safe_extract(tar, path=".", *, numeric_owner=False):
+ def _files(tar, path):
+ for member in tar:
+ validate_tar_member(member, path)
+ yield member
+
+ tar.extractall(path, members=_files(tar, path), numeric_owner=numeric_owner)
+
+
+def unpack_file(filename):
+ """Untar `filename`, assuming it is uncompressed or compressed with bzip2,
+ xz, gzip, zst, or unzip a zip file. The file is assumed to contain a single
+ directory with a name matching the base of the given filename.
+ Xz support is handled by shelling out to 'tar'."""
+ if os.path.isfile(filename) and tarfile.is_tarfile(filename):
+ tar_file, zip_ext = os.path.splitext(filename)
+ base_file, tar_ext = os.path.splitext(tar_file)
+ clean_path(base_file)
+ log.info('untarring "%s"' % filename)
+ with tarfile.open(filename) as tar:
+ safe_extract(tar)
+ elif os.path.isfile(filename) and filename.endswith(".tar.xz"):
+ base_file = filename.replace(".tar.xz", "")
+ clean_path(base_file)
+ log.info('untarring "%s"' % filename)
+ # Not using tar -Jxf because it fails on Windows for some reason.
+ process = Popen(["xz", "-d", "-c", filename], stdout=PIPE)
+ stdout, stderr = process.communicate()
+ if process.returncode != 0:
+ return False
+ fileobj = BytesIO()
+ fileobj.write(stdout)
+ fileobj.seek(0)
+ with tarfile.open(fileobj=fileobj, mode="r|") as tar:
+ safe_extract(tar)
+ elif os.path.isfile(filename) and filename.endswith(".tar.zst"):
+ import zstandard
+
+ base_file = filename.replace(".tar.zst", "")
+ clean_path(base_file)
+ log.info('untarring "%s"' % filename)
+ dctx = zstandard.ZstdDecompressor()
+ with dctx.stream_reader(open(filename, "rb")) as fileobj:
+ with tarfile.open(fileobj=fileobj, mode="r|") as tar:
+ safe_extract(tar)
+ elif os.path.isfile(filename) and zipfile.is_zipfile(filename):
+ base_file = filename.replace(".zip", "")
+ clean_path(base_file)
+ log.info('unzipping "%s"' % filename)
+ z = zipfile.ZipFile(filename)
+ z.extractall()
+ z.close()
+ else:
+ log.error("Unknown archive extension for filename '%s'" % filename)
+ return False
+ return True
+
+
+def fetch_files(
+ manifest_file,
+ base_urls,
+ filenames=[],
+ cache_folder=None,
+ auth_file=None,
+ region=None,
+):
+ # Lets load the manifest file
+ try:
+ manifest = open_manifest(manifest_file)
+ except InvalidManifest as e:
+ log.error(
+ "failed to load manifest file at '%s': %s"
+ % (
+ manifest_file,
+ str(e),
+ )
+ )
+ return False
+
+ # we want to track files already in current working directory AND valid
+ # we will not need to fetch these
+ present_files = []
+
+ # We want to track files that fail to be fetched as well as
+ # files that are fetched
+ failed_files = []
+ fetched_files = []
+
+ # Files that we want to unpack.
+ unpack_files = []
+
+ # Lets go through the manifest and fetch the files that we want
+ for f in manifest.file_records:
+ # case 1: files are already present
+ if f.present():
+ if f.validate():
+ present_files.append(f.filename)
+ if f.unpack:
+ unpack_files.append(f.filename)
+ else:
+ # we have an invalid file here, better to cleanup!
+ # this invalid file needs to be replaced with a good one
+ # from the local cash or fetched from a tooltool server
+ log.info(
+ "File %s is present locally but it is invalid, so I will remove it "
+ "and try to fetch it" % f.filename
+ )
+ os.remove(os.path.join(os.getcwd(), f.filename))
+
+ # check if file is already in cache
+ if cache_folder and f.filename not in present_files:
+ try:
+ shutil.copy(
+ os.path.join(cache_folder, f.digest),
+ os.path.join(os.getcwd(), f.filename),
+ )
+ log.info(
+ "File %s retrieved from local cache %s" % (f.filename, cache_folder)
+ )
+ touch(os.path.join(cache_folder, f.digest))
+
+ filerecord_for_validation = FileRecord(
+ f.filename, f.size, f.digest, f.algorithm
+ )
+ if filerecord_for_validation.validate():
+ present_files.append(f.filename)
+ if f.unpack:
+ unpack_files.append(f.filename)
+ else:
+ # the file copied from the cache is invalid, better to
+ # clean up the cache version itself as well
+ log.warn(
+ "File %s retrieved from cache is invalid! I am deleting it from the "
+ "cache as well" % f.filename
+ )
+ os.remove(os.path.join(os.getcwd(), f.filename))
+ os.remove(os.path.join(cache_folder, f.digest))
+ except IOError:
+ log.info(
+ "File %s not present in local cache folder %s"
+ % (f.filename, cache_folder)
+ )
+
+ # now I will try to fetch all files which are not already present and
+ # valid, appending a suffix to avoid race conditions
+ temp_file_name = None
+ # 'filenames' is the list of filenames to be managed, if this variable
+ # is a non empty list it can be used to filter if filename is in
+ # present_files, it means that I have it already because it was already
+ # either in the working dir or in the cache
+ if (
+ f.filename in filenames or len(filenames) == 0
+ ) and f.filename not in present_files:
+ log.debug("fetching %s" % f.filename)
+ temp_file_name = fetch_file(
+ base_urls, f, auth_file=auth_file, region=region
+ )
+ if temp_file_name:
+ fetched_files.append((f, temp_file_name))
+ else:
+ failed_files.append(f.filename)
+ else:
+ log.debug("skipping %s" % f.filename)
+
+ # lets ensure that fetched files match what the manifest specified
+ for localfile, temp_file_name in fetched_files:
+ # since I downloaded to a temp file, I need to perform all validations on the temp file
+ # this is why filerecord_for_validation is created
+
+ filerecord_for_validation = FileRecord(
+ temp_file_name, localfile.size, localfile.digest, localfile.algorithm
+ )
+
+ if filerecord_for_validation.validate():
+ # great!
+ # I can rename the temp file
+ log.info(
+ "File integrity verified, renaming %s to %s"
+ % (temp_file_name, localfile.filename)
+ )
+ os.rename(
+ os.path.join(os.getcwd(), temp_file_name),
+ os.path.join(os.getcwd(), localfile.filename),
+ )
+
+ if localfile.unpack:
+ unpack_files.append(localfile.filename)
+
+ # if I am using a cache and a new file has just been retrieved from a
+ # remote location, I need to update the cache as well
+ if cache_folder:
+ log.info("Updating local cache %s..." % cache_folder)
+ try:
+ if not os.path.exists(cache_folder):
+ log.info("Creating cache in %s..." % cache_folder)
+ os.makedirs(cache_folder, 0o0700)
+ shutil.copy(
+ os.path.join(os.getcwd(), localfile.filename),
+ os.path.join(cache_folder, localfile.digest),
+ )
+ log.info(
+ "Local cache %s updated with %s"
+ % (cache_folder, localfile.filename)
+ )
+ touch(os.path.join(cache_folder, localfile.digest))
+ except (OSError, IOError):
+ log.warning(
+ "Impossible to add file %s to cache folder %s"
+ % (localfile.filename, cache_folder),
+ exc_info=True,
+ )
+ else:
+ failed_files.append(localfile.filename)
+ log.error("'%s'" % filerecord_for_validation.describe())
+ os.remove(temp_file_name)
+
+ # Unpack files that need to be unpacked.
+ for filename in unpack_files:
+ if not unpack_file(filename):
+ failed_files.append(filename)
+
+ # If we failed to fetch or validate a file, we need to fail
+ if len(failed_files) > 0:
+ log.error("The following files failed: '%s'" % "', ".join(failed_files))
+ return False
+ return True
+
+
+def freespace(p):
+ "Returns the number of bytes free under directory `p`"
+ if sys.platform == "win32": # pragma: no cover
+ # os.statvfs doesn't work on Windows
+ import win32file
+
+ secsPerClus, bytesPerSec, nFreeClus, totClus = win32file.GetDiskFreeSpace(p)
+ return secsPerClus * bytesPerSec * nFreeClus
+ else:
+ r = os.statvfs(p)
+ return r.f_frsize * r.f_bavail
+
+
+def purge(folder, gigs):
+ """If gigs is non 0, it deletes files in `folder` until `gigs` GB are free,
+ starting from older files. If gigs is 0, a full purge will be performed.
+ No recursive deletion of files in subfolder is performed."""
+
+ full_purge = bool(gigs == 0)
+ gigs *= 1024 * 1024 * 1024
+
+ if not full_purge and freespace(folder) >= gigs:
+ log.info("No need to cleanup")
+ return
+
+ files = []
+ for f in os.listdir(folder):
+ p = os.path.join(folder, f)
+ # it deletes files in folder without going into subfolders,
+ # assuming the cache has a flat structure
+ if not os.path.isfile(p):
+ continue
+ mtime = os.path.getmtime(p)
+ files.append((mtime, p))
+
+ # iterate files sorted by mtime
+ for _, f in sorted(files):
+ log.info("removing %s to free up space" % f)
+ try:
+ os.remove(f)
+ except OSError:
+ log.info("Impossible to remove %s" % f, exc_info=True)
+ if not full_purge and freespace(folder) >= gigs:
+ break
+
+
+def _log_api_error(e):
+ if hasattr(e, "hdrs") and e.hdrs["content-type"] == "application/json":
+ json_resp = json.load(e.fp)
+ log.error(
+ "%s: %s" % (json_resp["error"]["name"], json_resp["error"]["description"])
+ )
+ else:
+ log.exception("Error making RelengAPI request:")
+
+
+def _authorize(req, auth_file):
+ is_taskcluster_auth = False
+
+ if not auth_file:
+ try:
+ taskcluster_env_keys = {
+ "clientId": "TASKCLUSTER_CLIENT_ID",
+ "accessToken": "TASKCLUSTER_ACCESS_TOKEN",
+ }
+ auth_content = {k: os.environ[v] for k, v in taskcluster_env_keys.items()}
+ is_taskcluster_auth = True
+ except KeyError:
+ return
+ else:
+ with open(auth_file) as f:
+ auth_content = f.read().strip()
+ try:
+ auth_content = json.loads(auth_content)
+ is_taskcluster_auth = True
+ except Exception:
+ pass
+
+ if is_taskcluster_auth:
+ taskcluster_header = make_taskcluster_header(auth_content, req)
+ log.debug("Using taskcluster credentials in %s" % auth_file)
+ req.add_unredirected_header("Authorization", taskcluster_header)
+ else:
+ log.debug("Using Bearer token in %s" % auth_file)
+ req.add_unredirected_header("Authorization", "Bearer %s" % auth_content)
+
+
+def _send_batch(base_url, auth_file, batch, region):
+ url = urljoin(base_url, "upload")
+ if region is not None:
+ url += "?region=" + region
+ data = json.dumps(batch)
+ if PY3:
+ data = data.encode("utf-8")
+ req = Request(url, data, {"Content-Type": "application/json"})
+ _authorize(req, auth_file)
+ try:
+ resp = urllib2.urlopen(req)
+ except (URLError, HTTPError) as e:
+ _log_api_error(e)
+ return None
+ return json.load(resp)["result"]
+
+
+def _s3_upload(filename, file):
+ # urllib2 does not support streaming, so we fall back to good old httplib
+ url = urlparse(file["put_url"])
+ cls = HTTPSConnection if url.scheme == "https" else HTTPConnection
+ host, port = url.netloc.split(":") if ":" in url.netloc else (url.netloc, 443)
+ port = int(port)
+ conn = cls(host, port)
+ try:
+ req_path = "%s?%s" % (url.path, url.query) if url.query else url.path
+ with open(filename, "rb") as f:
+ content = f.read()
+ content_length = len(content)
+ f.seek(0)
+ conn.request(
+ "PUT",
+ req_path,
+ f,
+ {
+ "Content-Type": "application/octet-stream",
+ "Content-Length": str(content_length),
+ },
+ )
+ resp = conn.getresponse()
+ resp_body = resp.read()
+ conn.close()
+ if resp.status != 200:
+ raise RuntimeError(
+ "Non-200 return from AWS: %s %s\n%s"
+ % (resp.status, resp.reason, resp_body)
+ )
+ except Exception:
+ file["upload_exception"] = sys.exc_info()
+ file["upload_ok"] = False
+ else:
+ file["upload_ok"] = True
+
+
+def _notify_upload_complete(base_url, auth_file, file):
+ req = Request(urljoin(base_url, "upload/complete/%(algorithm)s/%(digest)s" % file))
+ _authorize(req, auth_file)
+ try:
+ urllib2.urlopen(req)
+ except HTTPError as e:
+ if e.code != 409:
+ _log_api_error(e)
+ return
+ # 409 indicates that the upload URL hasn't expired yet and we
+ # should retry after a delay
+ to_wait = int(e.headers.get("X-Retry-After", 60))
+ log.warning("Waiting %d seconds for upload URLs to expire" % to_wait)
+ time.sleep(to_wait)
+ _notify_upload_complete(base_url, auth_file, file)
+ except Exception:
+ log.exception("While notifying server of upload completion:")
+
+
+def upload(manifest, message, base_urls, auth_file, region):
+ try:
+ manifest = open_manifest(manifest)
+ except InvalidManifest:
+ log.exception("failed to load manifest file at '%s'")
+ return False
+
+ # verify the manifest, since we'll need the files present to upload
+ if not manifest.validate():
+ log.error("manifest is invalid")
+ return False
+
+ if any(fr.visibility is None for fr in manifest.file_records):
+ log.error("All files in a manifest for upload must have a visibility set")
+
+ # convert the manifest to an upload batch
+ batch = {
+ "message": message,
+ "files": {},
+ }
+ for fr in manifest.file_records:
+ batch["files"][fr.filename] = {
+ "size": fr.size,
+ "digest": fr.digest,
+ "algorithm": fr.algorithm,
+ "visibility": fr.visibility,
+ }
+
+ # make the upload request
+ resp = _send_batch(base_urls[0], auth_file, batch, region)
+ if not resp:
+ return None
+ files = resp["files"]
+
+ # Upload the files, each in a thread. This allows us to start all of the
+ # uploads before any of the URLs expire.
+ threads = {}
+ for filename, file in files.items():
+ if "put_url" in file:
+ log.info("%s: starting upload" % (filename,))
+ thd = threading.Thread(target=_s3_upload, args=(filename, file))
+ thd.daemon = 1
+ thd.start()
+ threads[filename] = thd
+ else:
+ log.info("%s: already exists on server" % (filename,))
+
+ # re-join all of those threads as they exit
+ success = True
+ while threads:
+ for filename, thread in list(threads.items()):
+ if not thread.is_alive():
+ # _s3_upload has annotated file with result information
+ file = files[filename]
+ thread.join()
+ if file["upload_ok"]:
+ log.info("%s: uploaded" % filename)
+ else:
+ log.error(
+ "%s: failed" % filename, exc_info=file["upload_exception"]
+ )
+ success = False
+ del threads[filename]
+
+ # notify the server that the uploads are completed. If the notification
+ # fails, we don't consider that an error (the server will notice
+ # eventually)
+ for filename, file in files.items():
+ if "put_url" in file and file["upload_ok"]:
+ log.info("notifying server of upload completion for %s" % (filename,))
+ _notify_upload_complete(base_urls[0], auth_file, file)
+
+ return success
+
+
+def send_operation_on_file(data, base_urls, digest, auth_file):
+ url = base_urls[0]
+ url = urljoin(url, "file/sha512/" + digest)
+
+ data = json.dumps(data)
+
+ req = Request(url, data, {"Content-Type": "application/json"})
+ req.get_method = lambda: "PATCH"
+
+ _authorize(req, auth_file)
+
+ try:
+ urllib2.urlopen(req)
+ except (URLError, HTTPError) as e:
+ _log_api_error(e)
+ return False
+ return True
+
+
+def change_visibility(base_urls, digest, visibility, auth_file):
+ data = [
+ {
+ "op": "set_visibility",
+ "visibility": visibility,
+ }
+ ]
+ return send_operation_on_file(data, base_urls, digest, auth_file)
+
+
+def delete_instances(base_urls, digest, auth_file):
+ data = [
+ {
+ "op": "delete_instances",
+ }
+ ]
+ return send_operation_on_file(data, base_urls, digest, auth_file)
+
+
+def process_command(options, args):
+ """I know how to take a list of program arguments and
+ start doing the right thing with them"""
+ cmd = args[0]
+ cmd_args = args[1:]
+ log.debug("processing '%s' command with args '%s'" % (cmd, '", "'.join(cmd_args)))
+ log.debug("using options: %s" % options)
+
+ if cmd == "list":
+ return list_manifest(options["manifest"])
+ if cmd == "validate":
+ return validate_manifest(options["manifest"])
+ elif cmd == "add":
+ return add_files(
+ options["manifest"],
+ options["algorithm"],
+ cmd_args,
+ options["version"],
+ options["visibility"],
+ options["unpack"],
+ )
+ elif cmd == "purge":
+ if options["cache_folder"]:
+ purge(folder=options["cache_folder"], gigs=options["size"])
+ else:
+ log.critical("please specify the cache folder to be purged")
+ return False
+ elif cmd == "fetch":
+ return fetch_files(
+ options["manifest"],
+ options["base_url"],
+ cmd_args,
+ cache_folder=options["cache_folder"],
+ auth_file=options.get("auth_file"),
+ region=options.get("region"),
+ )
+ elif cmd == "upload":
+ if not options.get("message"):
+ log.critical("upload command requires a message")
+ return False
+ return upload(
+ options.get("manifest"),
+ options.get("message"),
+ options.get("base_url"),
+ options.get("auth_file"),
+ options.get("region"),
+ )
+ elif cmd == "change-visibility":
+ if not options.get("digest"):
+ log.critical("change-visibility command requires a digest option")
+ return False
+ if not options.get("visibility"):
+ log.critical("change-visibility command requires a visibility option")
+ return False
+ return change_visibility(
+ options.get("base_url"),
+ options.get("digest"),
+ options.get("visibility"),
+ options.get("auth_file"),
+ )
+ elif cmd == "delete":
+ if not options.get("digest"):
+ log.critical("change-visibility command requires a digest option")
+ return False
+ return delete_instances(
+ options.get("base_url"),
+ options.get("digest"),
+ options.get("auth_file"),
+ )
+ else:
+ log.critical('command "%s" is not implemented' % cmd)
+ return False
+
+
+def main(argv, _skip_logging=False):
+ # Set up option parsing
+ parser = optparse.OptionParser()
+ parser.add_option(
+ "-q",
+ "--quiet",
+ default=logging.INFO,
+ dest="loglevel",
+ action="store_const",
+ const=logging.ERROR,
+ )
+ parser.add_option(
+ "-v", "--verbose", dest="loglevel", action="store_const", const=logging.DEBUG
+ )
+ parser.add_option(
+ "-m",
+ "--manifest",
+ default=DEFAULT_MANIFEST_NAME,
+ dest="manifest",
+ action="store",
+ help="specify the manifest file to be operated on",
+ )
+ parser.add_option(
+ "-d",
+ "--algorithm",
+ default="sha512",
+ dest="algorithm",
+ action="store",
+ help="hashing algorithm to use (only sha512 is allowed)",
+ )
+ parser.add_option(
+ "--digest",
+ default=None,
+ dest="digest",
+ action="store",
+ help="digest hash to change visibility for",
+ )
+ parser.add_option(
+ "--visibility",
+ default=None,
+ dest="visibility",
+ choices=["internal", "public"],
+ help='Visibility level of this file; "internal" is for '
+ "files that cannot be distributed out of the company "
+ 'but not for secrets; "public" files are available to '
+ "anyone without restriction",
+ )
+ parser.add_option(
+ "--unpack",
+ default=False,
+ dest="unpack",
+ action="store_true",
+ help="Request unpacking this file after fetch."
+ " This is helpful with tarballs.",
+ )
+ parser.add_option(
+ "--version",
+ default=None,
+ dest="version",
+ action="store",
+ help="Version string for this file. This annotates the "
+ "manifest entry with a version string to help "
+ "identify the contents.",
+ )
+ parser.add_option(
+ "-o",
+ "--overwrite",
+ default=False,
+ dest="overwrite",
+ action="store_true",
+ help="UNUSED; present for backward compatibility",
+ )
+ parser.add_option(
+ "--url",
+ dest="base_url",
+ action="append",
+ help="RelengAPI URL ending with /tooltool/; default "
+ "is appropriate for Mozilla",
+ )
+ parser.add_option(
+ "-c", "--cache-folder", dest="cache_folder", help="Local cache folder"
+ )
+ parser.add_option(
+ "-s",
+ "--size",
+ help="free space required (in GB)",
+ dest="size",
+ type="float",
+ default=0.0,
+ )
+ parser.add_option(
+ "-r",
+ "--region",
+ help="Preferred AWS region for upload or fetch; " "example: --region=us-west-2",
+ )
+ parser.add_option(
+ "--message",
+ help='The "commit message" for an upload; format with a bug number '
+ "and brief comment",
+ dest="message",
+ )
+ parser.add_option(
+ "--authentication-file",
+ help="Use the RelengAPI token found in the given file to "
+ "authenticate to the RelengAPI server.",
+ dest="auth_file",
+ )
+
+ (options_obj, args) = parser.parse_args(argv[1:])
+
+ if not options_obj.base_url:
+ tooltool_host = os.environ.get("TOOLTOOL_HOST", "tooltool.mozilla-releng.net")
+ taskcluster_proxy_url = os.environ.get("TASKCLUSTER_PROXY_URL")
+ if taskcluster_proxy_url:
+ tooltool_url = "{}/{}".format(taskcluster_proxy_url, tooltool_host)
+ else:
+ tooltool_url = "https://{}".format(tooltool_host)
+
+ options_obj.base_url = [tooltool_url]
+
+ # ensure all URLs have a trailing slash
+ def add_slash(url):
+ return url if url.endswith("/") else (url + "/")
+
+ options_obj.base_url = [add_slash(u) for u in options_obj.base_url]
+
+ # expand ~ in --authentication-file
+ if options_obj.auth_file:
+ options_obj.auth_file = os.path.expanduser(options_obj.auth_file)
+
+ # Dictionaries are easier to work with
+ options = vars(options_obj)
+
+ log.setLevel(options["loglevel"])
+
+ # Set up logging, for now just to the console
+ if not _skip_logging: # pragma: no cover
+ ch = logging.StreamHandler()
+ cf = logging.Formatter("%(levelname)s - %(message)s")
+ ch.setFormatter(cf)
+ log.addHandler(ch)
+
+ if options["algorithm"] != "sha512":
+ parser.error("only --algorithm sha512 is supported")
+
+ if len(args) < 1:
+ parser.error("You must specify a command")
+
+ return 0 if process_command(options, args) else 1
+
+
+if __name__ == "__main__": # pragma: no cover
+ sys.exit(main(sys.argv))
diff --git a/testing/web-platform/tests/tools/third_party/typing_extensions/LICENSE b/testing/web-platform/tests/tools/third_party/typing_extensions/LICENSE
new file mode 100644
index 0000000000..583f9f6e61
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/typing_extensions/LICENSE
@@ -0,0 +1,254 @@
+A. HISTORY OF THE SOFTWARE
+==========================
+
+Python was created in the early 1990s by Guido van Rossum at Stichting
+Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
+as a successor of a language called ABC. Guido remains Python's
+principal author, although it includes many contributions from others.
+
+In 1995, Guido continued his work on Python at the Corporation for
+National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
+in Reston, Virginia where he released several versions of the
+software.
+
+In May 2000, Guido and the Python core development team moved to
+BeOpen.com to form the BeOpen PythonLabs team. In October of the same
+year, the PythonLabs team moved to Digital Creations (now Zope
+Corporation, see http://www.zope.com). In 2001, the Python Software
+Foundation (PSF, see http://www.python.org/psf/) was formed, a
+non-profit organization created specifically to own Python-related
+Intellectual Property. Zope Corporation is a sponsoring member of
+the PSF.
+
+All Python releases are Open Source (see http://www.opensource.org for
+the Open Source Definition). Historically, most, but not all, Python
+releases have also been GPL-compatible; the table below summarizes
+the various releases.
+
+ Release Derived Year Owner GPL-
+ from compatible? (1)
+
+ 0.9.0 thru 1.2 1991-1995 CWI yes
+ 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
+ 1.6 1.5.2 2000 CNRI no
+ 2.0 1.6 2000 BeOpen.com no
+ 1.6.1 1.6 2001 CNRI yes (2)
+ 2.1 2.0+1.6.1 2001 PSF no
+ 2.0.1 2.0+1.6.1 2001 PSF yes
+ 2.1.1 2.1+2.0.1 2001 PSF yes
+ 2.1.2 2.1.1 2002 PSF yes
+ 2.1.3 2.1.2 2002 PSF yes
+ 2.2 and above 2.1.1 2001-now PSF yes
+
+Footnotes:
+
+(1) GPL-compatible doesn't mean that we're distributing Python under
+ the GPL. All Python licenses, unlike the GPL, let you distribute
+ a modified version without making your changes open source. The
+ GPL-compatible licenses make it possible to combine Python with
+ other software that is released under the GPL; the others don't.
+
+(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
+ because its license has a choice of law clause. According to
+ CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
+ is "not incompatible" with the GPL.
+
+Thanks to the many outside volunteers who have worked under Guido's
+direction to make these releases possible.
+
+
+B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
+===============================================================
+
+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 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.
+
+
+BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
+-------------------------------------------
+
+BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
+
+1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
+office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
+Individual or Organization ("Licensee") accessing and otherwise using
+this software in source or binary form and its associated
+documentation ("the Software").
+
+2. Subject to the terms and conditions of this BeOpen Python License
+Agreement, BeOpen hereby grants Licensee a non-exclusive,
+royalty-free, world-wide license to reproduce, analyze, test, perform
+and/or display publicly, prepare derivative works, distribute, and
+otherwise use the Software alone or in any derivative version,
+provided, however, that the BeOpen Python License is retained in the
+Software, alone or in any derivative version prepared by Licensee.
+
+3. BeOpen is making the Software available to Licensee on an "AS IS"
+basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
+SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
+AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
+DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+5. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+6. This License Agreement shall be governed by and interpreted in all
+respects by the law of the State of California, excluding conflict of
+law provisions. Nothing in this License Agreement shall be deemed to
+create any relationship of agency, partnership, or joint venture
+between BeOpen and Licensee. This License Agreement does not grant
+permission to use BeOpen trademarks or trade names in a trademark
+sense to endorse or promote products or services of Licensee, or any
+third party. As an exception, the "BeOpen Python" logos available at
+http://www.pythonlabs.com/logos.html may be used according to the
+permissions granted on that web page.
+
+7. By copying, installing or otherwise using the software, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
+---------------------------------------
+
+1. This LICENSE AGREEMENT is between the Corporation for National
+Research Initiatives, having an office at 1895 Preston White Drive,
+Reston, VA 20191 ("CNRI"), and the Individual or Organization
+("Licensee") accessing and otherwise using Python 1.6.1 software in
+source or binary form and its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, CNRI
+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 1.6.1
+alone or in any derivative version, provided, however, that CNRI's
+License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
+1995-2001 Corporation for National Research Initiatives; All Rights
+Reserved" are retained in Python 1.6.1 alone or in any derivative
+version prepared by Licensee. Alternately, in lieu of CNRI's License
+Agreement, Licensee may substitute the following text (omitting the
+quotes): "Python 1.6.1 is made available subject to the terms and
+conditions in CNRI's License Agreement. This Agreement together with
+Python 1.6.1 may be located on the Internet using the following
+unique, persistent identifier (known as a handle): 1895.22/1013. This
+Agreement may also be obtained from a proxy server on the Internet
+using the following URL: http://hdl.handle.net/1895.22/1013".
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python 1.6.1 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 1.6.1.
+
+4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
+basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
+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. This License Agreement shall be governed by the federal
+intellectual property law of the United States, including without
+limitation the federal copyright law, and, to the extent such
+U.S. federal law does not apply, by the law of the Commonwealth of
+Virginia, excluding Virginia's conflict of law provisions.
+Notwithstanding the foregoing, with regard to derivative works based
+on Python 1.6.1 that incorporate non-separable material that was
+previously distributed under the GNU General Public License (GPL), the
+law of the Commonwealth of Virginia shall govern this License
+Agreement only as to issues arising under or with respect to
+Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
+License Agreement shall be deemed to create any relationship of
+agency, partnership, or joint venture between CNRI and Licensee. This
+License Agreement does not grant permission to use CNRI trademarks or
+trade name in a trademark sense to endorse or promote products or
+services of Licensee, or any third party.
+
+8. By clicking on the "ACCEPT" button where indicated, or by copying,
+installing or otherwise using Python 1.6.1, Licensee agrees to be
+bound by the terms and conditions of this License Agreement.
+
+ ACCEPT
+
+
+CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
+--------------------------------------------------
+
+Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
+The Netherlands. All rights reserved.
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appear in all copies and that
+both that copyright notice and this permission notice appear in
+supporting documentation, and that the name of Stichting Mathematisch
+Centrum or CWI not be used in advertising or publicity pertaining to
+distribution of the software without specific, written prior
+permission.
+
+STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
+FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/testing/web-platform/tests/tools/third_party/typing_extensions/PKG-INFO b/testing/web-platform/tests/tools/third_party/typing_extensions/PKG-INFO
new file mode 100644
index 0000000000..ae2892d3d7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/typing_extensions/PKG-INFO
@@ -0,0 +1,35 @@
+Metadata-Version: 2.1
+Name: typing_extensions
+Version: 4.1.1
+Summary: Backported and Experimental Type Hints for Python 3.6+
+Keywords: annotations,backport,checker,checking,function,hinting,hints,type,typechecking,typehinting,typehints,typing
+Author-email: "Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee" <levkivskyi@gmail.com>
+Requires-Python: >=3.6
+Description-Content-Type: text/x-rst
+Classifier: Development Status :: 3 - Alpha
+Classifier: Environment :: Console
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Python Software Foundation License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3 :: Only
+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 :: 3.10
+Classifier: Topic :: Software Development
+Project-URL: Home, https://github.com/python/typing/blob/master/typing_extensions/README.rst
+
+Typing Extensions -- Backported and Experimental Type Hints for Python
+
+The ``typing`` module was added to the standard library in Python 3.5, but
+many new features have been added to the module since then.
+This means users of older Python versions who are unable to upgrade will not be
+able to take advantage of new types added to the ``typing`` module, such as
+``typing.Protocol`` or ``typing.TypedDict``.
+
+The ``typing_extensions`` module contains backports of these changes.
+Experimental types that may eventually be added to the ``typing``
+module are also included in ``typing_extensions``.
+
diff --git a/testing/web-platform/tests/tools/third_party/typing_extensions/pyproject.toml b/testing/web-platform/tests/tools/third_party/typing_extensions/pyproject.toml
new file mode 100644
index 0000000000..354c206824
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/typing_extensions/pyproject.toml
@@ -0,0 +1,63 @@
+# Build system requirements.
+[build-system]
+requires = ["flit_core >=3.4,<4"]
+build-backend = "flit_core.buildapi"
+
+# Project metadata
+[project]
+name = "typing_extensions"
+version = "4.1.1"
+description = "Backported and Experimental Type Hints for Python 3.6+"
+readme.text = """\
+Typing Extensions -- Backported and Experimental Type Hints for Python
+
+The ``typing`` module was added to the standard library in Python 3.5, but
+many new features have been added to the module since then.
+This means users of older Python versions who are unable to upgrade will not be
+able to take advantage of new types added to the ``typing`` module, such as
+``typing.Protocol`` or ``typing.TypedDict``.
+
+The ``typing_extensions`` module contains backports of these changes.
+Experimental types that may eventually be added to the ``typing``
+module are also included in ``typing_extensions``.
+"""
+readme.content-type = "text/x-rst"
+requires-python = ">=3.6"
+urls.Home = "https://github.com/python/typing/blob/master/typing_extensions/README.rst"
+license.file = "LICENSE"
+keywords = [
+ "annotations",
+ "backport",
+ "checker",
+ "checking",
+ "function",
+ "hinting",
+ "hints",
+ "type",
+ "typechecking",
+ "typehinting",
+ "typehints",
+ "typing"
+]
+# Classifiers list: https://pypi.org/classifiers/
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Python Software Foundation License",
+ "Operating System :: OS Independent",
+ "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"
+]
+
+# Project metadata -- authors. Flit stores this as a list of dicts, so it can't
+# be inline above.
+[[project.authors]]
+name = "Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee"
+email = "levkivskyi@gmail.com"
diff --git a/testing/web-platform/tests/tools/third_party/typing_extensions/src/typing_extensions.py b/testing/web-platform/tests/tools/third_party/typing_extensions/src/typing_extensions.py
new file mode 100644
index 0000000000..194731cd3c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/typing_extensions/src/typing_extensions.py
@@ -0,0 +1,2908 @@
+import abc
+import collections
+import collections.abc
+import operator
+import sys
+import types as _types
+import typing
+
+# After PEP 560, internal typing API was substantially reworked.
+# This is especially important for Protocol class which uses internal APIs
+# quite extensively.
+PEP_560 = sys.version_info[:3] >= (3, 7, 0)
+
+if PEP_560:
+ GenericMeta = type
+else:
+ # 3.6
+ from typing import GenericMeta, _type_vars # noqa
+
+
+# Please keep __all__ alphabetized within each category.
+__all__ = [
+ # Super-special typing primitives.
+ 'ClassVar',
+ 'Concatenate',
+ 'Final',
+ 'LiteralString',
+ 'ParamSpec',
+ 'Self',
+ 'Type',
+ 'TypeVarTuple',
+ 'Unpack',
+
+ # ABCs (from collections.abc).
+ 'Awaitable',
+ 'AsyncIterator',
+ 'AsyncIterable',
+ 'Coroutine',
+ 'AsyncGenerator',
+ 'AsyncContextManager',
+ 'ChainMap',
+
+ # Concrete collection types.
+ 'ContextManager',
+ 'Counter',
+ 'Deque',
+ 'DefaultDict',
+ 'OrderedDict',
+ 'TypedDict',
+
+ # Structural checks, a.k.a. protocols.
+ 'SupportsIndex',
+
+ # One-off things.
+ 'Annotated',
+ 'assert_never',
+ 'dataclass_transform',
+ 'final',
+ 'IntVar',
+ 'is_typeddict',
+ 'Literal',
+ 'NewType',
+ 'overload',
+ 'Protocol',
+ 'reveal_type',
+ 'runtime',
+ 'runtime_checkable',
+ 'Text',
+ 'TypeAlias',
+ 'TypeGuard',
+ 'TYPE_CHECKING',
+ 'Never',
+ 'NoReturn',
+ 'Required',
+ 'NotRequired',
+]
+
+if PEP_560:
+ __all__.extend(["get_args", "get_origin", "get_type_hints"])
+
+# The functions below are modified copies of typing internal helpers.
+# They are needed by _ProtocolMeta and they provide support for PEP 646.
+
+
+def _no_slots_copy(dct):
+ dict_copy = dict(dct)
+ if '__slots__' in dict_copy:
+ for slot in dict_copy['__slots__']:
+ dict_copy.pop(slot, None)
+ return dict_copy
+
+
+_marker = object()
+
+
+def _check_generic(cls, parameters, elen=_marker):
+ """Check correct count for parameters of a generic cls (internal helper).
+ This gives a nice error message in case of count mismatch.
+ """
+ if not elen:
+ raise TypeError(f"{cls} is not a generic class")
+ if elen is _marker:
+ if not hasattr(cls, "__parameters__") or not cls.__parameters__:
+ raise TypeError(f"{cls} is not a generic class")
+ elen = len(cls.__parameters__)
+ alen = len(parameters)
+ if alen != elen:
+ if hasattr(cls, "__parameters__"):
+ parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
+ num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters)
+ if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples):
+ return
+ raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};"
+ f" actual {alen}, expected {elen}")
+
+
+if sys.version_info >= (3, 10):
+ def _should_collect_from_parameters(t):
+ return isinstance(
+ t, (typing._GenericAlias, _types.GenericAlias, _types.UnionType)
+ )
+elif sys.version_info >= (3, 9):
+ def _should_collect_from_parameters(t):
+ return isinstance(t, (typing._GenericAlias, _types.GenericAlias))
+else:
+ def _should_collect_from_parameters(t):
+ return isinstance(t, typing._GenericAlias) and not t._special
+
+
+def _collect_type_vars(types, typevar_types=None):
+ """Collect all type variable contained in types in order of
+ first appearance (lexicographic order). For example::
+
+ _collect_type_vars((T, List[S, T])) == (T, S)
+ """
+ if typevar_types is None:
+ typevar_types = typing.TypeVar
+ tvars = []
+ for t in types:
+ if (
+ isinstance(t, typevar_types) and
+ t not in tvars and
+ not _is_unpack(t)
+ ):
+ tvars.append(t)
+ if _should_collect_from_parameters(t):
+ tvars.extend([t for t in t.__parameters__ if t not in tvars])
+ return tuple(tvars)
+
+
+# 3.6.2+
+if hasattr(typing, 'NoReturn'):
+ NoReturn = typing.NoReturn
+# 3.6.0-3.6.1
+else:
+ class _NoReturn(typing._FinalTypingBase, _root=True):
+ """Special type indicating functions that never return.
+ Example::
+
+ from typing import NoReturn
+
+ def stop() -> NoReturn:
+ raise Exception('no way')
+
+ This type is invalid in other positions, e.g., ``List[NoReturn]``
+ will fail in static type checkers.
+ """
+ __slots__ = ()
+
+ def __instancecheck__(self, obj):
+ raise TypeError("NoReturn cannot be used with isinstance().")
+
+ def __subclasscheck__(self, cls):
+ raise TypeError("NoReturn cannot be used with issubclass().")
+
+ NoReturn = _NoReturn(_root=True)
+
+# Some unconstrained type variables. These are used by the container types.
+# (These are not for export.)
+T = typing.TypeVar('T') # Any type.
+KT = typing.TypeVar('KT') # Key type.
+VT = typing.TypeVar('VT') # Value type.
+T_co = typing.TypeVar('T_co', covariant=True) # Any type covariant containers.
+T_contra = typing.TypeVar('T_contra', contravariant=True) # Ditto contravariant.
+
+ClassVar = typing.ClassVar
+
+# On older versions of typing there is an internal class named "Final".
+# 3.8+
+if hasattr(typing, 'Final') and sys.version_info[:2] >= (3, 7):
+ Final = typing.Final
+# 3.7
+elif sys.version_info[:2] >= (3, 7):
+ class _FinalForm(typing._SpecialForm, _root=True):
+
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ def __getitem__(self, parameters):
+ item = typing._type_check(parameters,
+ f'{self._name} accepts only single type')
+ return typing._GenericAlias(self, (item,))
+
+ Final = _FinalForm('Final',
+ doc="""A special typing construct to indicate that a name
+ cannot be re-assigned or overridden in a subclass.
+ For example:
+
+ MAX_SIZE: Final = 9000
+ MAX_SIZE += 1 # Error reported by type checker
+
+ class Connection:
+ TIMEOUT: Final[int] = 10
+ class FastConnector(Connection):
+ TIMEOUT = 1 # Error reported by type checker
+
+ There is no runtime checking of these properties.""")
+# 3.6
+else:
+ class _Final(typing._FinalTypingBase, _root=True):
+ """A special typing construct to indicate that a name
+ cannot be re-assigned or overridden in a subclass.
+ For example:
+
+ MAX_SIZE: Final = 9000
+ MAX_SIZE += 1 # Error reported by type checker
+
+ class Connection:
+ TIMEOUT: Final[int] = 10
+ class FastConnector(Connection):
+ TIMEOUT = 1 # Error reported by type checker
+
+ There is no runtime checking of these properties.
+ """
+
+ __slots__ = ('__type__',)
+
+ def __init__(self, tp=None, **kwds):
+ self.__type__ = tp
+
+ def __getitem__(self, item):
+ cls = type(self)
+ if self.__type__ is None:
+ return cls(typing._type_check(item,
+ f'{cls.__name__[1:]} accepts only single type.'),
+ _root=True)
+ raise TypeError(f'{cls.__name__[1:]} cannot be further subscripted')
+
+ def _eval_type(self, globalns, localns):
+ new_tp = typing._eval_type(self.__type__, globalns, localns)
+ if new_tp == self.__type__:
+ return self
+ return type(self)(new_tp, _root=True)
+
+ def __repr__(self):
+ r = super().__repr__()
+ if self.__type__ is not None:
+ r += f'[{typing._type_repr(self.__type__)}]'
+ return r
+
+ def __hash__(self):
+ return hash((type(self).__name__, self.__type__))
+
+ def __eq__(self, other):
+ if not isinstance(other, _Final):
+ return NotImplemented
+ if self.__type__ is not None:
+ return self.__type__ == other.__type__
+ return self is other
+
+ Final = _Final(_root=True)
+
+
+if sys.version_info >= (3, 11):
+ final = typing.final
+else:
+ # @final exists in 3.8+, but we backport it for all versions
+ # before 3.11 to keep support for the __final__ attribute.
+ # See https://bugs.python.org/issue46342
+ def final(f):
+ """This decorator can be used to indicate to type checkers that
+ the decorated method cannot be overridden, and decorated class
+ cannot be subclassed. For example:
+
+ class Base:
+ @final
+ def done(self) -> None:
+ ...
+ class Sub(Base):
+ def done(self) -> None: # Error reported by type checker
+ ...
+ @final
+ class Leaf:
+ ...
+ class Other(Leaf): # Error reported by type checker
+ ...
+
+ There is no runtime checking of these properties. The decorator
+ sets the ``__final__`` attribute to ``True`` on the decorated object
+ to allow runtime introspection.
+ """
+ try:
+ f.__final__ = True
+ except (AttributeError, TypeError):
+ # Skip the attribute silently if it is not writable.
+ # AttributeError happens if the object has __slots__ or a
+ # read-only property, TypeError if it's a builtin class.
+ pass
+ return f
+
+
+def IntVar(name):
+ return typing.TypeVar(name)
+
+
+# 3.8+:
+if hasattr(typing, 'Literal'):
+ Literal = typing.Literal
+# 3.7:
+elif sys.version_info[:2] >= (3, 7):
+ class _LiteralForm(typing._SpecialForm, _root=True):
+
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ def __getitem__(self, parameters):
+ return typing._GenericAlias(self, parameters)
+
+ Literal = _LiteralForm('Literal',
+ doc="""A type that can be used to indicate to type checkers
+ that the corresponding value has a value literally equivalent
+ to the provided parameter. For example:
+
+ var: Literal[4] = 4
+
+ The type checker understands that 'var' is literally equal to
+ the value 4 and no other value.
+
+ Literal[...] cannot be subclassed. There is no runtime
+ checking verifying that the parameter is actually a value
+ instead of a type.""")
+# 3.6:
+else:
+ class _Literal(typing._FinalTypingBase, _root=True):
+ """A type that can be used to indicate to type checkers that the
+ corresponding value has a value literally equivalent to the
+ provided parameter. For example:
+
+ var: Literal[4] = 4
+
+ The type checker understands that 'var' is literally equal to the
+ value 4 and no other value.
+
+ Literal[...] cannot be subclassed. There is no runtime checking
+ verifying that the parameter is actually a value instead of a type.
+ """
+
+ __slots__ = ('__values__',)
+
+ def __init__(self, values=None, **kwds):
+ self.__values__ = values
+
+ def __getitem__(self, values):
+ cls = type(self)
+ if self.__values__ is None:
+ if not isinstance(values, tuple):
+ values = (values,)
+ return cls(values, _root=True)
+ raise TypeError(f'{cls.__name__[1:]} cannot be further subscripted')
+
+ def _eval_type(self, globalns, localns):
+ return self
+
+ def __repr__(self):
+ r = super().__repr__()
+ if self.__values__ is not None:
+ r += f'[{", ".join(map(typing._type_repr, self.__values__))}]'
+ return r
+
+ def __hash__(self):
+ return hash((type(self).__name__, self.__values__))
+
+ def __eq__(self, other):
+ if not isinstance(other, _Literal):
+ return NotImplemented
+ if self.__values__ is not None:
+ return self.__values__ == other.__values__
+ return self is other
+
+ Literal = _Literal(_root=True)
+
+
+_overload_dummy = typing._overload_dummy # noqa
+overload = typing.overload
+
+
+# This is not a real generic class. Don't use outside annotations.
+Type = typing.Type
+
+# Various ABCs mimicking those in collections.abc.
+# A few are simply re-exported for completeness.
+
+
+class _ExtensionsGenericMeta(GenericMeta):
+ def __subclasscheck__(self, subclass):
+ """This mimics a more modern GenericMeta.__subclasscheck__() logic
+ (that does not have problems with recursion) to work around interactions
+ between collections, typing, and typing_extensions on older
+ versions of Python, see https://github.com/python/typing/issues/501.
+ """
+ if self.__origin__ is not None:
+ if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools']:
+ raise TypeError("Parameterized generics cannot be used with class "
+ "or instance checks")
+ return False
+ if not self.__extra__:
+ return super().__subclasscheck__(subclass)
+ res = self.__extra__.__subclasshook__(subclass)
+ if res is not NotImplemented:
+ return res
+ if self.__extra__ in subclass.__mro__:
+ return True
+ for scls in self.__extra__.__subclasses__():
+ if isinstance(scls, GenericMeta):
+ continue
+ if issubclass(subclass, scls):
+ return True
+ return False
+
+
+Awaitable = typing.Awaitable
+Coroutine = typing.Coroutine
+AsyncIterable = typing.AsyncIterable
+AsyncIterator = typing.AsyncIterator
+
+# 3.6.1+
+if hasattr(typing, 'Deque'):
+ Deque = typing.Deque
+# 3.6.0
+else:
+ class Deque(collections.deque, typing.MutableSequence[T],
+ metaclass=_ExtensionsGenericMeta,
+ extra=collections.deque):
+ __slots__ = ()
+
+ def __new__(cls, *args, **kwds):
+ if cls._gorg is Deque:
+ return collections.deque(*args, **kwds)
+ return typing._generic_new(collections.deque, cls, *args, **kwds)
+
+ContextManager = typing.ContextManager
+# 3.6.2+
+if hasattr(typing, 'AsyncContextManager'):
+ AsyncContextManager = typing.AsyncContextManager
+# 3.6.0-3.6.1
+else:
+ from _collections_abc import _check_methods as _check_methods_in_mro # noqa
+
+ class AsyncContextManager(typing.Generic[T_co]):
+ __slots__ = ()
+
+ async def __aenter__(self):
+ return self
+
+ @abc.abstractmethod
+ async def __aexit__(self, exc_type, exc_value, traceback):
+ return None
+
+ @classmethod
+ def __subclasshook__(cls, C):
+ if cls is AsyncContextManager:
+ return _check_methods_in_mro(C, "__aenter__", "__aexit__")
+ return NotImplemented
+
+DefaultDict = typing.DefaultDict
+
+# 3.7.2+
+if hasattr(typing, 'OrderedDict'):
+ OrderedDict = typing.OrderedDict
+# 3.7.0-3.7.2
+elif (3, 7, 0) <= sys.version_info[:3] < (3, 7, 2):
+ OrderedDict = typing._alias(collections.OrderedDict, (KT, VT))
+# 3.6
+else:
+ class OrderedDict(collections.OrderedDict, typing.MutableMapping[KT, VT],
+ metaclass=_ExtensionsGenericMeta,
+ extra=collections.OrderedDict):
+
+ __slots__ = ()
+
+ def __new__(cls, *args, **kwds):
+ if cls._gorg is OrderedDict:
+ return collections.OrderedDict(*args, **kwds)
+ return typing._generic_new(collections.OrderedDict, cls, *args, **kwds)
+
+# 3.6.2+
+if hasattr(typing, 'Counter'):
+ Counter = typing.Counter
+# 3.6.0-3.6.1
+else:
+ class Counter(collections.Counter,
+ typing.Dict[T, int],
+ metaclass=_ExtensionsGenericMeta, extra=collections.Counter):
+
+ __slots__ = ()
+
+ def __new__(cls, *args, **kwds):
+ if cls._gorg is Counter:
+ return collections.Counter(*args, **kwds)
+ return typing._generic_new(collections.Counter, cls, *args, **kwds)
+
+# 3.6.1+
+if hasattr(typing, 'ChainMap'):
+ ChainMap = typing.ChainMap
+elif hasattr(collections, 'ChainMap'):
+ class ChainMap(collections.ChainMap, typing.MutableMapping[KT, VT],
+ metaclass=_ExtensionsGenericMeta,
+ extra=collections.ChainMap):
+
+ __slots__ = ()
+
+ def __new__(cls, *args, **kwds):
+ if cls._gorg is ChainMap:
+ return collections.ChainMap(*args, **kwds)
+ return typing._generic_new(collections.ChainMap, cls, *args, **kwds)
+
+# 3.6.1+
+if hasattr(typing, 'AsyncGenerator'):
+ AsyncGenerator = typing.AsyncGenerator
+# 3.6.0
+else:
+ class AsyncGenerator(AsyncIterator[T_co], typing.Generic[T_co, T_contra],
+ metaclass=_ExtensionsGenericMeta,
+ extra=collections.abc.AsyncGenerator):
+ __slots__ = ()
+
+NewType = typing.NewType
+Text = typing.Text
+TYPE_CHECKING = typing.TYPE_CHECKING
+
+
+def _gorg(cls):
+ """This function exists for compatibility with old typing versions."""
+ assert isinstance(cls, GenericMeta)
+ if hasattr(cls, '_gorg'):
+ return cls._gorg
+ while cls.__origin__ is not None:
+ cls = cls.__origin__
+ return cls
+
+
+_PROTO_WHITELIST = ['Callable', 'Awaitable',
+ 'Iterable', 'Iterator', 'AsyncIterable', 'AsyncIterator',
+ 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible',
+ 'ContextManager', 'AsyncContextManager']
+
+
+def _get_protocol_attrs(cls):
+ attrs = set()
+ for base in cls.__mro__[:-1]: # without object
+ if base.__name__ in ('Protocol', 'Generic'):
+ continue
+ annotations = getattr(base, '__annotations__', {})
+ for attr in list(base.__dict__.keys()) + list(annotations.keys()):
+ if (not attr.startswith('_abc_') and attr not in (
+ '__abstractmethods__', '__annotations__', '__weakref__',
+ '_is_protocol', '_is_runtime_protocol', '__dict__',
+ '__args__', '__slots__',
+ '__next_in_mro__', '__parameters__', '__origin__',
+ '__orig_bases__', '__extra__', '__tree_hash__',
+ '__doc__', '__subclasshook__', '__init__', '__new__',
+ '__module__', '_MutableMapping__marker', '_gorg')):
+ attrs.add(attr)
+ return attrs
+
+
+def _is_callable_members_only(cls):
+ return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls))
+
+
+# 3.8+
+if hasattr(typing, 'Protocol'):
+ Protocol = typing.Protocol
+# 3.7
+elif PEP_560:
+
+ def _no_init(self, *args, **kwargs):
+ if type(self)._is_protocol:
+ raise TypeError('Protocols cannot be instantiated')
+
+ class _ProtocolMeta(abc.ABCMeta):
+ # This metaclass is a bit unfortunate and exists only because of the lack
+ # of __instancehook__.
+ def __instancecheck__(cls, instance):
+ # We need this method for situations where attributes are
+ # assigned in __init__.
+ if ((not getattr(cls, '_is_protocol', False) or
+ _is_callable_members_only(cls)) and
+ issubclass(instance.__class__, cls)):
+ return True
+ if cls._is_protocol:
+ if all(hasattr(instance, attr) and
+ (not callable(getattr(cls, attr, None)) or
+ getattr(instance, attr) is not None)
+ for attr in _get_protocol_attrs(cls)):
+ return True
+ return super().__instancecheck__(instance)
+
+ class Protocol(metaclass=_ProtocolMeta):
+ # There is quite a lot of overlapping code with typing.Generic.
+ # Unfortunately it is hard to avoid this while these live in two different
+ # modules. The duplicated code will be removed when Protocol is moved to typing.
+ """Base class for protocol classes. Protocol classes are defined as::
+
+ class Proto(Protocol):
+ def meth(self) -> int:
+ ...
+
+ Such classes are primarily used with static type checkers that recognize
+ structural subtyping (static duck-typing), for example::
+
+ class C:
+ def meth(self) -> int:
+ return 0
+
+ def func(x: Proto) -> int:
+ return x.meth()
+
+ func(C()) # Passes static type check
+
+ See PEP 544 for details. Protocol classes decorated with
+ @typing_extensions.runtime act as simple-minded runtime protocol that checks
+ only the presence of given attributes, ignoring their type signatures.
+
+ Protocol classes can be generic, they are defined as::
+
+ class GenProto(Protocol[T]):
+ def meth(self) -> T:
+ ...
+ """
+ __slots__ = ()
+ _is_protocol = True
+
+ def __new__(cls, *args, **kwds):
+ if cls is Protocol:
+ raise TypeError("Type Protocol cannot be instantiated; "
+ "it can only be used as a base class")
+ return super().__new__(cls)
+
+ @typing._tp_cache
+ def __class_getitem__(cls, params):
+ if not isinstance(params, tuple):
+ params = (params,)
+ if not params and cls is not typing.Tuple:
+ raise TypeError(
+ f"Parameter list to {cls.__qualname__}[...] cannot be empty")
+ msg = "Parameters to generic types must be types."
+ params = tuple(typing._type_check(p, msg) for p in params) # noqa
+ if cls is Protocol:
+ # Generic can only be subscripted with unique type variables.
+ if not all(isinstance(p, typing.TypeVar) for p in params):
+ i = 0
+ while isinstance(params[i], typing.TypeVar):
+ i += 1
+ raise TypeError(
+ "Parameters to Protocol[...] must all be type variables."
+ f" Parameter {i + 1} is {params[i]}")
+ if len(set(params)) != len(params):
+ raise TypeError(
+ "Parameters to Protocol[...] must all be unique")
+ else:
+ # Subscripting a regular Generic subclass.
+ _check_generic(cls, params, len(cls.__parameters__))
+ return typing._GenericAlias(cls, params)
+
+ def __init_subclass__(cls, *args, **kwargs):
+ tvars = []
+ if '__orig_bases__' in cls.__dict__:
+ error = typing.Generic in cls.__orig_bases__
+ else:
+ error = typing.Generic in cls.__bases__
+ if error:
+ raise TypeError("Cannot inherit from plain Generic")
+ if '__orig_bases__' in cls.__dict__:
+ tvars = typing._collect_type_vars(cls.__orig_bases__)
+ # Look for Generic[T1, ..., Tn] or Protocol[T1, ..., Tn].
+ # If found, tvars must be a subset of it.
+ # If not found, tvars is it.
+ # Also check for and reject plain Generic,
+ # and reject multiple Generic[...] and/or Protocol[...].
+ gvars = None
+ for base in cls.__orig_bases__:
+ if (isinstance(base, typing._GenericAlias) and
+ base.__origin__ in (typing.Generic, Protocol)):
+ # for error messages
+ the_base = base.__origin__.__name__
+ if gvars is not None:
+ raise TypeError(
+ "Cannot inherit from Generic[...]"
+ " and/or Protocol[...] multiple types.")
+ gvars = base.__parameters__
+ if gvars is None:
+ gvars = tvars
+ else:
+ tvarset = set(tvars)
+ gvarset = set(gvars)
+ if not tvarset <= gvarset:
+ s_vars = ', '.join(str(t) for t in tvars if t not in gvarset)
+ s_args = ', '.join(str(g) for g in gvars)
+ raise TypeError(f"Some type variables ({s_vars}) are"
+ f" not listed in {the_base}[{s_args}]")
+ tvars = gvars
+ cls.__parameters__ = tuple(tvars)
+
+ # Determine if this is a protocol or a concrete subclass.
+ if not cls.__dict__.get('_is_protocol', None):
+ cls._is_protocol = any(b is Protocol for b in cls.__bases__)
+
+ # Set (or override) the protocol subclass hook.
+ def _proto_hook(other):
+ if not cls.__dict__.get('_is_protocol', None):
+ return NotImplemented
+ if not getattr(cls, '_is_runtime_protocol', False):
+ if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']:
+ return NotImplemented
+ raise TypeError("Instance and class checks can only be used with"
+ " @runtime protocols")
+ if not _is_callable_members_only(cls):
+ if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']:
+ return NotImplemented
+ raise TypeError("Protocols with non-method members"
+ " don't support issubclass()")
+ if not isinstance(other, type):
+ # Same error as for issubclass(1, int)
+ raise TypeError('issubclass() arg 1 must be a class')
+ for attr in _get_protocol_attrs(cls):
+ for base in other.__mro__:
+ if attr in base.__dict__:
+ if base.__dict__[attr] is None:
+ return NotImplemented
+ break
+ annotations = getattr(base, '__annotations__', {})
+ if (isinstance(annotations, typing.Mapping) and
+ attr in annotations and
+ isinstance(other, _ProtocolMeta) and
+ other._is_protocol):
+ break
+ else:
+ return NotImplemented
+ return True
+ if '__subclasshook__' not in cls.__dict__:
+ cls.__subclasshook__ = _proto_hook
+
+ # We have nothing more to do for non-protocols.
+ if not cls._is_protocol:
+ return
+
+ # Check consistency of bases.
+ for base in cls.__bases__:
+ if not (base in (object, typing.Generic) or
+ base.__module__ == 'collections.abc' and
+ base.__name__ in _PROTO_WHITELIST or
+ isinstance(base, _ProtocolMeta) and base._is_protocol):
+ raise TypeError('Protocols can only inherit from other'
+ f' protocols, got {repr(base)}')
+ cls.__init__ = _no_init
+# 3.6
+else:
+ from typing import _next_in_mro, _type_check # noqa
+
+ def _no_init(self, *args, **kwargs):
+ if type(self)._is_protocol:
+ raise TypeError('Protocols cannot be instantiated')
+
+ class _ProtocolMeta(GenericMeta):
+ """Internal metaclass for Protocol.
+
+ This exists so Protocol classes can be generic without deriving
+ from Generic.
+ """
+ def __new__(cls, name, bases, namespace,
+ tvars=None, args=None, origin=None, extra=None, orig_bases=None):
+ # This is just a version copied from GenericMeta.__new__ that
+ # includes "Protocol" special treatment. (Comments removed for brevity.)
+ assert extra is None # Protocols should not have extra
+ if tvars is not None:
+ assert origin is not None
+ assert all(isinstance(t, typing.TypeVar) for t in tvars), tvars
+ else:
+ tvars = _type_vars(bases)
+ gvars = None
+ for base in bases:
+ if base is typing.Generic:
+ raise TypeError("Cannot inherit from plain Generic")
+ if (isinstance(base, GenericMeta) and
+ base.__origin__ in (typing.Generic, Protocol)):
+ if gvars is not None:
+ raise TypeError(
+ "Cannot inherit from Generic[...] or"
+ " Protocol[...] multiple times.")
+ gvars = base.__parameters__
+ if gvars is None:
+ gvars = tvars
+ else:
+ tvarset = set(tvars)
+ gvarset = set(gvars)
+ if not tvarset <= gvarset:
+ s_vars = ", ".join(str(t) for t in tvars if t not in gvarset)
+ s_args = ", ".join(str(g) for g in gvars)
+ cls_name = "Generic" if any(b.__origin__ is typing.Generic
+ for b in bases) else "Protocol"
+ raise TypeError(f"Some type variables ({s_vars}) are"
+ f" not listed in {cls_name}[{s_args}]")
+ tvars = gvars
+
+ initial_bases = bases
+ if (extra is not None and type(extra) is abc.ABCMeta and
+ extra not in bases):
+ bases = (extra,) + bases
+ bases = tuple(_gorg(b) if isinstance(b, GenericMeta) else b
+ for b in bases)
+ if any(isinstance(b, GenericMeta) and b is not typing.Generic for b in bases):
+ bases = tuple(b for b in bases if b is not typing.Generic)
+ namespace.update({'__origin__': origin, '__extra__': extra})
+ self = super(GenericMeta, cls).__new__(cls, name, bases, namespace,
+ _root=True)
+ super(GenericMeta, self).__setattr__('_gorg',
+ self if not origin else
+ _gorg(origin))
+ self.__parameters__ = tvars
+ self.__args__ = tuple(... if a is typing._TypingEllipsis else
+ () if a is typing._TypingEmpty else
+ a for a in args) if args else None
+ self.__next_in_mro__ = _next_in_mro(self)
+ if orig_bases is None:
+ self.__orig_bases__ = initial_bases
+ elif origin is not None:
+ self._abc_registry = origin._abc_registry
+ self._abc_cache = origin._abc_cache
+ if hasattr(self, '_subs_tree'):
+ self.__tree_hash__ = (hash(self._subs_tree()) if origin else
+ super(GenericMeta, self).__hash__())
+ return self
+
+ def __init__(cls, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if not cls.__dict__.get('_is_protocol', None):
+ cls._is_protocol = any(b is Protocol or
+ isinstance(b, _ProtocolMeta) and
+ b.__origin__ is Protocol
+ for b in cls.__bases__)
+ if cls._is_protocol:
+ for base in cls.__mro__[1:]:
+ if not (base in (object, typing.Generic) or
+ base.__module__ == 'collections.abc' and
+ base.__name__ in _PROTO_WHITELIST or
+ isinstance(base, typing.TypingMeta) and base._is_protocol or
+ isinstance(base, GenericMeta) and
+ base.__origin__ is typing.Generic):
+ raise TypeError(f'Protocols can only inherit from other'
+ f' protocols, got {repr(base)}')
+
+ cls.__init__ = _no_init
+
+ def _proto_hook(other):
+ if not cls.__dict__.get('_is_protocol', None):
+ return NotImplemented
+ if not isinstance(other, type):
+ # Same error as for issubclass(1, int)
+ raise TypeError('issubclass() arg 1 must be a class')
+ for attr in _get_protocol_attrs(cls):
+ for base in other.__mro__:
+ if attr in base.__dict__:
+ if base.__dict__[attr] is None:
+ return NotImplemented
+ break
+ annotations = getattr(base, '__annotations__', {})
+ if (isinstance(annotations, typing.Mapping) and
+ attr in annotations and
+ isinstance(other, _ProtocolMeta) and
+ other._is_protocol):
+ break
+ else:
+ return NotImplemented
+ return True
+ if '__subclasshook__' not in cls.__dict__:
+ cls.__subclasshook__ = _proto_hook
+
+ def __instancecheck__(self, instance):
+ # We need this method for situations where attributes are
+ # assigned in __init__.
+ if ((not getattr(self, '_is_protocol', False) or
+ _is_callable_members_only(self)) and
+ issubclass(instance.__class__, self)):
+ return True
+ if self._is_protocol:
+ if all(hasattr(instance, attr) and
+ (not callable(getattr(self, attr, None)) or
+ getattr(instance, attr) is not None)
+ for attr in _get_protocol_attrs(self)):
+ return True
+ return super(GenericMeta, self).__instancecheck__(instance)
+
+ def __subclasscheck__(self, cls):
+ if self.__origin__ is not None:
+ if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools']:
+ raise TypeError("Parameterized generics cannot be used with class "
+ "or instance checks")
+ return False
+ if (self.__dict__.get('_is_protocol', None) and
+ not self.__dict__.get('_is_runtime_protocol', None)):
+ if sys._getframe(1).f_globals['__name__'] in ['abc',
+ 'functools',
+ 'typing']:
+ return False
+ raise TypeError("Instance and class checks can only be used with"
+ " @runtime protocols")
+ if (self.__dict__.get('_is_runtime_protocol', None) and
+ not _is_callable_members_only(self)):
+ if sys._getframe(1).f_globals['__name__'] in ['abc',
+ 'functools',
+ 'typing']:
+ return super(GenericMeta, self).__subclasscheck__(cls)
+ raise TypeError("Protocols with non-method members"
+ " don't support issubclass()")
+ return super(GenericMeta, self).__subclasscheck__(cls)
+
+ @typing._tp_cache
+ def __getitem__(self, params):
+ # We also need to copy this from GenericMeta.__getitem__ to get
+ # special treatment of "Protocol". (Comments removed for brevity.)
+ if not isinstance(params, tuple):
+ params = (params,)
+ if not params and _gorg(self) is not typing.Tuple:
+ raise TypeError(
+ f"Parameter list to {self.__qualname__}[...] cannot be empty")
+ msg = "Parameters to generic types must be types."
+ params = tuple(_type_check(p, msg) for p in params)
+ if self in (typing.Generic, Protocol):
+ if not all(isinstance(p, typing.TypeVar) for p in params):
+ raise TypeError(
+ f"Parameters to {repr(self)}[...] must all be type variables")
+ if len(set(params)) != len(params):
+ raise TypeError(
+ f"Parameters to {repr(self)}[...] must all be unique")
+ tvars = params
+ args = params
+ elif self in (typing.Tuple, typing.Callable):
+ tvars = _type_vars(params)
+ args = params
+ elif self.__origin__ in (typing.Generic, Protocol):
+ raise TypeError(f"Cannot subscript already-subscripted {repr(self)}")
+ else:
+ _check_generic(self, params, len(self.__parameters__))
+ tvars = _type_vars(params)
+ args = params
+
+ prepend = (self,) if self.__origin__ is None else ()
+ return self.__class__(self.__name__,
+ prepend + self.__bases__,
+ _no_slots_copy(self.__dict__),
+ tvars=tvars,
+ args=args,
+ origin=self,
+ extra=self.__extra__,
+ orig_bases=self.__orig_bases__)
+
+ class Protocol(metaclass=_ProtocolMeta):
+ """Base class for protocol classes. Protocol classes are defined as::
+
+ class Proto(Protocol):
+ def meth(self) -> int:
+ ...
+
+ Such classes are primarily used with static type checkers that recognize
+ structural subtyping (static duck-typing), for example::
+
+ class C:
+ def meth(self) -> int:
+ return 0
+
+ def func(x: Proto) -> int:
+ return x.meth()
+
+ func(C()) # Passes static type check
+
+ See PEP 544 for details. Protocol classes decorated with
+ @typing_extensions.runtime act as simple-minded runtime protocol that checks
+ only the presence of given attributes, ignoring their type signatures.
+
+ Protocol classes can be generic, they are defined as::
+
+ class GenProto(Protocol[T]):
+ def meth(self) -> T:
+ ...
+ """
+ __slots__ = ()
+ _is_protocol = True
+
+ def __new__(cls, *args, **kwds):
+ if _gorg(cls) is Protocol:
+ raise TypeError("Type Protocol cannot be instantiated; "
+ "it can be used only as a base class")
+ return typing._generic_new(cls.__next_in_mro__, cls, *args, **kwds)
+
+
+# 3.8+
+if hasattr(typing, 'runtime_checkable'):
+ runtime_checkable = typing.runtime_checkable
+# 3.6-3.7
+else:
+ def runtime_checkable(cls):
+ """Mark a protocol class as a runtime protocol, so that it
+ can be used with isinstance() and issubclass(). Raise TypeError
+ if applied to a non-protocol class.
+
+ This allows a simple-minded structural check very similar to the
+ one-offs in collections.abc such as Hashable.
+ """
+ if not isinstance(cls, _ProtocolMeta) or not cls._is_protocol:
+ raise TypeError('@runtime_checkable can be only applied to protocol classes,'
+ f' got {cls!r}')
+ cls._is_runtime_protocol = True
+ return cls
+
+
+# Exists for backwards compatibility.
+runtime = runtime_checkable
+
+
+# 3.8+
+if hasattr(typing, 'SupportsIndex'):
+ SupportsIndex = typing.SupportsIndex
+# 3.6-3.7
+else:
+ @runtime_checkable
+ class SupportsIndex(Protocol):
+ __slots__ = ()
+
+ @abc.abstractmethod
+ def __index__(self) -> int:
+ pass
+
+
+if hasattr(typing, "Required"):
+ # The standard library TypedDict in Python 3.8 does not store runtime information
+ # about which (if any) keys are optional. See https://bugs.python.org/issue38834
+ # The standard library TypedDict in Python 3.9.0/1 does not honour the "total"
+ # keyword with old-style TypedDict(). See https://bugs.python.org/issue42059
+ # The standard library TypedDict below Python 3.11 does not store runtime
+ # information about optional and required keys when using Required or NotRequired.
+ TypedDict = typing.TypedDict
+ _TypedDictMeta = typing._TypedDictMeta
+ is_typeddict = typing.is_typeddict
+else:
+ def _check_fails(cls, other):
+ try:
+ if sys._getframe(1).f_globals['__name__'] not in ['abc',
+ 'functools',
+ 'typing']:
+ # Typed dicts are only for static structural subtyping.
+ raise TypeError('TypedDict does not support instance and class checks')
+ except (AttributeError, ValueError):
+ pass
+ return False
+
+ def _dict_new(*args, **kwargs):
+ if not args:
+ raise TypeError('TypedDict.__new__(): not enough arguments')
+ _, args = args[0], args[1:] # allow the "cls" keyword be passed
+ return dict(*args, **kwargs)
+
+ _dict_new.__text_signature__ = '($cls, _typename, _fields=None, /, **kwargs)'
+
+ def _typeddict_new(*args, total=True, **kwargs):
+ if not args:
+ raise TypeError('TypedDict.__new__(): not enough arguments')
+ _, args = args[0], args[1:] # allow the "cls" keyword be passed
+ if args:
+ typename, args = args[0], args[1:] # allow the "_typename" keyword be passed
+ elif '_typename' in kwargs:
+ typename = kwargs.pop('_typename')
+ import warnings
+ warnings.warn("Passing '_typename' as keyword argument is deprecated",
+ DeprecationWarning, stacklevel=2)
+ else:
+ raise TypeError("TypedDict.__new__() missing 1 required positional "
+ "argument: '_typename'")
+ if args:
+ try:
+ fields, = args # allow the "_fields" keyword be passed
+ except ValueError:
+ raise TypeError('TypedDict.__new__() takes from 2 to 3 '
+ f'positional arguments but {len(args) + 2} '
+ 'were given')
+ elif '_fields' in kwargs and len(kwargs) == 1:
+ fields = kwargs.pop('_fields')
+ import warnings
+ warnings.warn("Passing '_fields' as keyword argument is deprecated",
+ DeprecationWarning, stacklevel=2)
+ else:
+ fields = None
+
+ if fields is None:
+ fields = kwargs
+ elif kwargs:
+ raise TypeError("TypedDict takes either a dict or keyword arguments,"
+ " but not both")
+
+ ns = {'__annotations__': dict(fields)}
+ try:
+ # Setting correct module is necessary to make typed dict classes pickleable.
+ ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__')
+ except (AttributeError, ValueError):
+ pass
+
+ return _TypedDictMeta(typename, (), ns, total=total)
+
+ _typeddict_new.__text_signature__ = ('($cls, _typename, _fields=None,'
+ ' /, *, total=True, **kwargs)')
+
+ class _TypedDictMeta(type):
+ def __init__(cls, name, bases, ns, total=True):
+ super().__init__(name, bases, ns)
+
+ def __new__(cls, name, bases, ns, total=True):
+ # Create new typed dict class object.
+ # This method is called directly when TypedDict is subclassed,
+ # or via _typeddict_new when TypedDict is instantiated. This way
+ # TypedDict supports all three syntaxes described in its docstring.
+ # Subclasses and instances of TypedDict return actual dictionaries
+ # via _dict_new.
+ ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new
+ tp_dict = super().__new__(cls, name, (dict,), ns)
+
+ annotations = {}
+ own_annotations = ns.get('__annotations__', {})
+ msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
+ own_annotations = {
+ n: typing._type_check(tp, msg) for n, tp in own_annotations.items()
+ }
+ required_keys = set()
+ optional_keys = set()
+
+ for base in bases:
+ annotations.update(base.__dict__.get('__annotations__', {}))
+ required_keys.update(base.__dict__.get('__required_keys__', ()))
+ optional_keys.update(base.__dict__.get('__optional_keys__', ()))
+
+ annotations.update(own_annotations)
+ if PEP_560:
+ for annotation_key, annotation_type in own_annotations.items():
+ annotation_origin = get_origin(annotation_type)
+ if annotation_origin is Annotated:
+ annotation_args = get_args(annotation_type)
+ if annotation_args:
+ annotation_type = annotation_args[0]
+ annotation_origin = get_origin(annotation_type)
+
+ if annotation_origin is Required:
+ required_keys.add(annotation_key)
+ elif annotation_origin is NotRequired:
+ optional_keys.add(annotation_key)
+ elif total:
+ required_keys.add(annotation_key)
+ else:
+ optional_keys.add(annotation_key)
+ else:
+ own_annotation_keys = set(own_annotations.keys())
+ if total:
+ required_keys.update(own_annotation_keys)
+ else:
+ optional_keys.update(own_annotation_keys)
+
+ tp_dict.__annotations__ = annotations
+ tp_dict.__required_keys__ = frozenset(required_keys)
+ tp_dict.__optional_keys__ = frozenset(optional_keys)
+ if not hasattr(tp_dict, '__total__'):
+ tp_dict.__total__ = total
+ return tp_dict
+
+ __instancecheck__ = __subclasscheck__ = _check_fails
+
+ TypedDict = _TypedDictMeta('TypedDict', (dict,), {})
+ TypedDict.__module__ = __name__
+ TypedDict.__doc__ = \
+ """A simple typed name space. At runtime it is equivalent to a plain dict.
+
+ TypedDict creates a dictionary type that expects all of its
+ instances to have a certain set of keys, with each key
+ associated with a value of a consistent type. This expectation
+ is not checked at runtime but is only enforced by type checkers.
+ Usage::
+
+ class Point2D(TypedDict):
+ x: int
+ y: int
+ label: str
+
+ a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK
+ b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check
+
+ assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first')
+
+ The type info can be accessed via the Point2D.__annotations__ dict, and
+ the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets.
+ TypedDict supports two additional equivalent forms::
+
+ Point2D = TypedDict('Point2D', x=int, y=int, label=str)
+ Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str})
+
+ The class syntax is only supported in Python 3.6+, while two other
+ syntax forms work for Python 2.7 and 3.2+
+ """
+
+ if hasattr(typing, "_TypedDictMeta"):
+ _TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta)
+ else:
+ _TYPEDDICT_TYPES = (_TypedDictMeta,)
+
+ def is_typeddict(tp):
+ """Check if an annotation is a TypedDict class
+
+ For example::
+ class Film(TypedDict):
+ title: str
+ year: int
+
+ is_typeddict(Film) # => True
+ is_typeddict(Union[list, str]) # => False
+ """
+ return isinstance(tp, tuple(_TYPEDDICT_TYPES))
+
+if hasattr(typing, "Required"):
+ get_type_hints = typing.get_type_hints
+elif PEP_560:
+ import functools
+ import types
+
+ # replaces _strip_annotations()
+ def _strip_extras(t):
+ """Strips Annotated, Required and NotRequired from a given type."""
+ if isinstance(t, _AnnotatedAlias):
+ return _strip_extras(t.__origin__)
+ if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired):
+ return _strip_extras(t.__args__[0])
+ if isinstance(t, typing._GenericAlias):
+ stripped_args = tuple(_strip_extras(a) for a in t.__args__)
+ if stripped_args == t.__args__:
+ return t
+ return t.copy_with(stripped_args)
+ if hasattr(types, "GenericAlias") and isinstance(t, types.GenericAlias):
+ stripped_args = tuple(_strip_extras(a) for a in t.__args__)
+ if stripped_args == t.__args__:
+ return t
+ return types.GenericAlias(t.__origin__, stripped_args)
+ if hasattr(types, "UnionType") and isinstance(t, types.UnionType):
+ stripped_args = tuple(_strip_extras(a) for a in t.__args__)
+ if stripped_args == t.__args__:
+ return t
+ return functools.reduce(operator.or_, stripped_args)
+
+ return t
+
+ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
+ """Return type hints for an object.
+
+ This is often the same as obj.__annotations__, but it handles
+ forward references encoded as string literals, adds Optional[t] if a
+ default value equal to None is set and recursively replaces all
+ 'Annotated[T, ...]', 'Required[T]' or 'NotRequired[T]' with 'T'
+ (unless 'include_extras=True').
+
+ The argument may be a module, class, method, or function. The annotations
+ are returned as a dictionary. For classes, annotations include also
+ inherited members.
+
+ TypeError is raised if the argument is not of a type that can contain
+ annotations, and an empty dictionary is returned if no annotations are
+ present.
+
+ BEWARE -- the behavior of globalns and localns is counterintuitive
+ (unless you are familiar with how eval() and exec() work). The
+ search order is locals first, then globals.
+
+ - If no dict arguments are passed, an attempt is made to use the
+ globals from obj (or the respective module's globals for classes),
+ and these are also used as the locals. If the object does not appear
+ to have globals, an empty dictionary is used.
+
+ - If one dict argument is passed, it is used for both globals and
+ locals.
+
+ - If two dict arguments are passed, they specify globals and
+ locals, respectively.
+ """
+ if hasattr(typing, "Annotated"):
+ hint = typing.get_type_hints(
+ obj, globalns=globalns, localns=localns, include_extras=True
+ )
+ else:
+ hint = typing.get_type_hints(obj, globalns=globalns, localns=localns)
+ if include_extras:
+ return hint
+ return {k: _strip_extras(t) for k, t in hint.items()}
+
+
+# Python 3.9+ has PEP 593 (Annotated)
+if hasattr(typing, 'Annotated'):
+ Annotated = typing.Annotated
+ # Not exported and not a public API, but needed for get_origin() and get_args()
+ # to work.
+ _AnnotatedAlias = typing._AnnotatedAlias
+# 3.7-3.8
+elif PEP_560:
+ class _AnnotatedAlias(typing._GenericAlias, _root=True):
+ """Runtime representation of an annotated type.
+
+ At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't'
+ with extra annotations. The alias behaves like a normal typing alias,
+ instantiating is the same as instantiating the underlying type, binding
+ it to types is also the same.
+ """
+ def __init__(self, origin, metadata):
+ if isinstance(origin, _AnnotatedAlias):
+ metadata = origin.__metadata__ + metadata
+ origin = origin.__origin__
+ super().__init__(origin, origin)
+ self.__metadata__ = metadata
+
+ def copy_with(self, params):
+ assert len(params) == 1
+ new_type = params[0]
+ return _AnnotatedAlias(new_type, self.__metadata__)
+
+ def __repr__(self):
+ return (f"typing_extensions.Annotated[{typing._type_repr(self.__origin__)}, "
+ f"{', '.join(repr(a) for a in self.__metadata__)}]")
+
+ def __reduce__(self):
+ return operator.getitem, (
+ Annotated, (self.__origin__,) + self.__metadata__
+ )
+
+ def __eq__(self, other):
+ if not isinstance(other, _AnnotatedAlias):
+ return NotImplemented
+ if self.__origin__ != other.__origin__:
+ return False
+ return self.__metadata__ == other.__metadata__
+
+ def __hash__(self):
+ return hash((self.__origin__, self.__metadata__))
+
+ class Annotated:
+ """Add context specific metadata to a type.
+
+ Example: Annotated[int, runtime_check.Unsigned] indicates to the
+ hypothetical runtime_check module that this type is an unsigned int.
+ Every other consumer of this type can ignore this metadata and treat
+ this type as int.
+
+ The first argument to Annotated must be a valid type (and will be in
+ the __origin__ field), the remaining arguments are kept as a tuple in
+ the __extra__ field.
+
+ Details:
+
+ - It's an error to call `Annotated` with less than two arguments.
+ - Nested Annotated are flattened::
+
+ Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3]
+
+ - Instantiating an annotated type is equivalent to instantiating the
+ underlying type::
+
+ Annotated[C, Ann1](5) == C(5)
+
+ - Annotated can be used as a generic type alias::
+
+ Optimized = Annotated[T, runtime.Optimize()]
+ Optimized[int] == Annotated[int, runtime.Optimize()]
+
+ OptimizedList = Annotated[List[T], runtime.Optimize()]
+ OptimizedList[int] == Annotated[List[int], runtime.Optimize()]
+ """
+
+ __slots__ = ()
+
+ def __new__(cls, *args, **kwargs):
+ raise TypeError("Type Annotated cannot be instantiated.")
+
+ @typing._tp_cache
+ def __class_getitem__(cls, params):
+ if not isinstance(params, tuple) or len(params) < 2:
+ raise TypeError("Annotated[...] should be used "
+ "with at least two arguments (a type and an "
+ "annotation).")
+ allowed_special_forms = (ClassVar, Final)
+ if get_origin(params[0]) in allowed_special_forms:
+ origin = params[0]
+ else:
+ msg = "Annotated[t, ...]: t must be a type."
+ origin = typing._type_check(params[0], msg)
+ metadata = tuple(params[1:])
+ return _AnnotatedAlias(origin, metadata)
+
+ def __init_subclass__(cls, *args, **kwargs):
+ raise TypeError(
+ f"Cannot subclass {cls.__module__}.Annotated"
+ )
+# 3.6
+else:
+
+ def _is_dunder(name):
+ """Returns True if name is a __dunder_variable_name__."""
+ return len(name) > 4 and name.startswith('__') and name.endswith('__')
+
+ # Prior to Python 3.7 types did not have `copy_with`. A lot of the equality
+ # checks, argument expansion etc. are done on the _subs_tre. As a result we
+ # can't provide a get_type_hints function that strips out annotations.
+
+ class AnnotatedMeta(typing.GenericMeta):
+ """Metaclass for Annotated"""
+
+ def __new__(cls, name, bases, namespace, **kwargs):
+ if any(b is not object for b in bases):
+ raise TypeError("Cannot subclass " + str(Annotated))
+ return super().__new__(cls, name, bases, namespace, **kwargs)
+
+ @property
+ def __metadata__(self):
+ return self._subs_tree()[2]
+
+ def _tree_repr(self, tree):
+ cls, origin, metadata = tree
+ if not isinstance(origin, tuple):
+ tp_repr = typing._type_repr(origin)
+ else:
+ tp_repr = origin[0]._tree_repr(origin)
+ metadata_reprs = ", ".join(repr(arg) for arg in metadata)
+ return f'{cls}[{tp_repr}, {metadata_reprs}]'
+
+ def _subs_tree(self, tvars=None, args=None): # noqa
+ if self is Annotated:
+ return Annotated
+ res = super()._subs_tree(tvars=tvars, args=args)
+ # Flatten nested Annotated
+ if isinstance(res[1], tuple) and res[1][0] is Annotated:
+ sub_tp = res[1][1]
+ sub_annot = res[1][2]
+ return (Annotated, sub_tp, sub_annot + res[2])
+ return res
+
+ def _get_cons(self):
+ """Return the class used to create instance of this type."""
+ if self.__origin__ is None:
+ raise TypeError("Cannot get the underlying type of a "
+ "non-specialized Annotated type.")
+ tree = self._subs_tree()
+ while isinstance(tree, tuple) and tree[0] is Annotated:
+ tree = tree[1]
+ if isinstance(tree, tuple):
+ return tree[0]
+ else:
+ return tree
+
+ @typing._tp_cache
+ def __getitem__(self, params):
+ if not isinstance(params, tuple):
+ params = (params,)
+ if self.__origin__ is not None: # specializing an instantiated type
+ return super().__getitem__(params)
+ elif not isinstance(params, tuple) or len(params) < 2:
+ raise TypeError("Annotated[...] should be instantiated "
+ "with at least two arguments (a type and an "
+ "annotation).")
+ else:
+ if (
+ isinstance(params[0], typing._TypingBase) and
+ type(params[0]).__name__ == "_ClassVar"
+ ):
+ tp = params[0]
+ else:
+ msg = "Annotated[t, ...]: t must be a type."
+ tp = typing._type_check(params[0], msg)
+ metadata = tuple(params[1:])
+ return self.__class__(
+ self.__name__,
+ self.__bases__,
+ _no_slots_copy(self.__dict__),
+ tvars=_type_vars((tp,)),
+ # Metadata is a tuple so it won't be touched by _replace_args et al.
+ args=(tp, metadata),
+ origin=self,
+ )
+
+ def __call__(self, *args, **kwargs):
+ cons = self._get_cons()
+ result = cons(*args, **kwargs)
+ try:
+ result.__orig_class__ = self
+ except AttributeError:
+ pass
+ return result
+
+ def __getattr__(self, attr):
+ # For simplicity we just don't relay all dunder names
+ if self.__origin__ is not None and not _is_dunder(attr):
+ return getattr(self._get_cons(), attr)
+ raise AttributeError(attr)
+
+ def __setattr__(self, attr, value):
+ if _is_dunder(attr) or attr.startswith('_abc_'):
+ super().__setattr__(attr, value)
+ elif self.__origin__ is None:
+ raise AttributeError(attr)
+ else:
+ setattr(self._get_cons(), attr, value)
+
+ def __instancecheck__(self, obj):
+ raise TypeError("Annotated cannot be used with isinstance().")
+
+ def __subclasscheck__(self, cls):
+ raise TypeError("Annotated cannot be used with issubclass().")
+
+ class Annotated(metaclass=AnnotatedMeta):
+ """Add context specific metadata to a type.
+
+ Example: Annotated[int, runtime_check.Unsigned] indicates to the
+ hypothetical runtime_check module that this type is an unsigned int.
+ Every other consumer of this type can ignore this metadata and treat
+ this type as int.
+
+ The first argument to Annotated must be a valid type, the remaining
+ arguments are kept as a tuple in the __metadata__ field.
+
+ Details:
+
+ - It's an error to call `Annotated` with less than two arguments.
+ - Nested Annotated are flattened::
+
+ Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3]
+
+ - Instantiating an annotated type is equivalent to instantiating the
+ underlying type::
+
+ Annotated[C, Ann1](5) == C(5)
+
+ - Annotated can be used as a generic type alias::
+
+ Optimized = Annotated[T, runtime.Optimize()]
+ Optimized[int] == Annotated[int, runtime.Optimize()]
+
+ OptimizedList = Annotated[List[T], runtime.Optimize()]
+ OptimizedList[int] == Annotated[List[int], runtime.Optimize()]
+ """
+
+# Python 3.8 has get_origin() and get_args() but those implementations aren't
+# Annotated-aware, so we can't use those. Python 3.9's versions don't support
+# ParamSpecArgs and ParamSpecKwargs, so only Python 3.10's versions will do.
+if sys.version_info[:2] >= (3, 10):
+ get_origin = typing.get_origin
+ get_args = typing.get_args
+# 3.7-3.9
+elif PEP_560:
+ try:
+ # 3.9+
+ from typing import _BaseGenericAlias
+ except ImportError:
+ _BaseGenericAlias = typing._GenericAlias
+ try:
+ # 3.9+
+ from typing import GenericAlias
+ except ImportError:
+ GenericAlias = typing._GenericAlias
+
+ def get_origin(tp):
+ """Get the unsubscripted version of a type.
+
+ This supports generic types, Callable, Tuple, Union, Literal, Final, ClassVar
+ and Annotated. Return None for unsupported types. Examples::
+
+ get_origin(Literal[42]) is Literal
+ get_origin(int) is None
+ get_origin(ClassVar[int]) is ClassVar
+ get_origin(Generic) is Generic
+ get_origin(Generic[T]) is Generic
+ get_origin(Union[T, int]) is Union
+ get_origin(List[Tuple[T, T]][int]) == list
+ get_origin(P.args) is P
+ """
+ if isinstance(tp, _AnnotatedAlias):
+ return Annotated
+ if isinstance(tp, (typing._GenericAlias, GenericAlias, _BaseGenericAlias,
+ ParamSpecArgs, ParamSpecKwargs)):
+ return tp.__origin__
+ if tp is typing.Generic:
+ return typing.Generic
+ return None
+
+ def get_args(tp):
+ """Get type arguments with all substitutions performed.
+
+ For unions, basic simplifications used by Union constructor are performed.
+ Examples::
+ get_args(Dict[str, int]) == (str, int)
+ get_args(int) == ()
+ get_args(Union[int, Union[T, int], str][int]) == (int, str)
+ get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])
+ get_args(Callable[[], T][int]) == ([], int)
+ """
+ if isinstance(tp, _AnnotatedAlias):
+ return (tp.__origin__,) + tp.__metadata__
+ if isinstance(tp, (typing._GenericAlias, GenericAlias)):
+ if getattr(tp, "_special", False):
+ return ()
+ res = tp.__args__
+ if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis:
+ res = (list(res[:-1]), res[-1])
+ return res
+ return ()
+
+
+# 3.10+
+if hasattr(typing, 'TypeAlias'):
+ TypeAlias = typing.TypeAlias
+# 3.9
+elif sys.version_info[:2] >= (3, 9):
+ class _TypeAliasForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ @_TypeAliasForm
+ def TypeAlias(self, parameters):
+ """Special marker indicating that an assignment should
+ be recognized as a proper type alias definition by type
+ checkers.
+
+ For example::
+
+ Predicate: TypeAlias = Callable[..., bool]
+
+ It's invalid when used anywhere except as in the example above.
+ """
+ raise TypeError(f"{self} is not subscriptable")
+# 3.7-3.8
+elif sys.version_info[:2] >= (3, 7):
+ class _TypeAliasForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ TypeAlias = _TypeAliasForm('TypeAlias',
+ doc="""Special marker indicating that an assignment should
+ be recognized as a proper type alias definition by type
+ checkers.
+
+ For example::
+
+ Predicate: TypeAlias = Callable[..., bool]
+
+ It's invalid when used anywhere except as in the example
+ above.""")
+# 3.6
+else:
+ class _TypeAliasMeta(typing.TypingMeta):
+ """Metaclass for TypeAlias"""
+
+ def __repr__(self):
+ return 'typing_extensions.TypeAlias'
+
+ class _TypeAliasBase(typing._FinalTypingBase, metaclass=_TypeAliasMeta, _root=True):
+ """Special marker indicating that an assignment should
+ be recognized as a proper type alias definition by type
+ checkers.
+
+ For example::
+
+ Predicate: TypeAlias = Callable[..., bool]
+
+ It's invalid when used anywhere except as in the example above.
+ """
+ __slots__ = ()
+
+ def __instancecheck__(self, obj):
+ raise TypeError("TypeAlias cannot be used with isinstance().")
+
+ def __subclasscheck__(self, cls):
+ raise TypeError("TypeAlias cannot be used with issubclass().")
+
+ def __repr__(self):
+ return 'typing_extensions.TypeAlias'
+
+ TypeAlias = _TypeAliasBase(_root=True)
+
+
+# Python 3.10+ has PEP 612
+if hasattr(typing, 'ParamSpecArgs'):
+ ParamSpecArgs = typing.ParamSpecArgs
+ ParamSpecKwargs = typing.ParamSpecKwargs
+# 3.6-3.9
+else:
+ class _Immutable:
+ """Mixin to indicate that object should not be copied."""
+ __slots__ = ()
+
+ def __copy__(self):
+ return self
+
+ def __deepcopy__(self, memo):
+ return self
+
+ class ParamSpecArgs(_Immutable):
+ """The args for a ParamSpec object.
+
+ Given a ParamSpec object P, P.args is an instance of ParamSpecArgs.
+
+ ParamSpecArgs objects have a reference back to their ParamSpec:
+
+ P.args.__origin__ is P
+
+ This type is meant for runtime introspection and has no special meaning to
+ static type checkers.
+ """
+ def __init__(self, origin):
+ self.__origin__ = origin
+
+ def __repr__(self):
+ return f"{self.__origin__.__name__}.args"
+
+ def __eq__(self, other):
+ if not isinstance(other, ParamSpecArgs):
+ return NotImplemented
+ return self.__origin__ == other.__origin__
+
+ class ParamSpecKwargs(_Immutable):
+ """The kwargs for a ParamSpec object.
+
+ Given a ParamSpec object P, P.kwargs is an instance of ParamSpecKwargs.
+
+ ParamSpecKwargs objects have a reference back to their ParamSpec:
+
+ P.kwargs.__origin__ is P
+
+ This type is meant for runtime introspection and has no special meaning to
+ static type checkers.
+ """
+ def __init__(self, origin):
+ self.__origin__ = origin
+
+ def __repr__(self):
+ return f"{self.__origin__.__name__}.kwargs"
+
+ def __eq__(self, other):
+ if not isinstance(other, ParamSpecKwargs):
+ return NotImplemented
+ return self.__origin__ == other.__origin__
+
+# 3.10+
+if hasattr(typing, 'ParamSpec'):
+ ParamSpec = typing.ParamSpec
+# 3.6-3.9
+else:
+
+ # Inherits from list as a workaround for Callable checks in Python < 3.9.2.
+ class ParamSpec(list):
+ """Parameter specification variable.
+
+ Usage::
+
+ P = ParamSpec('P')
+
+ Parameter specification variables exist primarily for the benefit of static
+ type checkers. They are used to forward the parameter types of one
+ callable to another callable, a pattern commonly found in higher order
+ functions and decorators. They are only valid when used in ``Concatenate``,
+ or s the first argument to ``Callable``. In Python 3.10 and higher,
+ they are also supported in user-defined Generics at runtime.
+ See class Generic for more information on generic types. An
+ example for annotating a decorator::
+
+ T = TypeVar('T')
+ P = ParamSpec('P')
+
+ def add_logging(f: Callable[P, T]) -> Callable[P, T]:
+ '''A type-safe decorator to add logging to a function.'''
+ def inner(*args: P.args, **kwargs: P.kwargs) -> T:
+ logging.info(f'{f.__name__} was called')
+ return f(*args, **kwargs)
+ return inner
+
+ @add_logging
+ def add_two(x: float, y: float) -> float:
+ '''Add two numbers together.'''
+ return x + y
+
+ Parameter specification variables defined with covariant=True or
+ contravariant=True can be used to declare covariant or contravariant
+ generic types. These keyword arguments are valid, but their actual semantics
+ are yet to be decided. See PEP 612 for details.
+
+ Parameter specification variables can be introspected. e.g.:
+
+ P.__name__ == 'T'
+ P.__bound__ == None
+ P.__covariant__ == False
+ P.__contravariant__ == False
+
+ Note that only parameter specification variables defined in global scope can
+ be pickled.
+ """
+
+ # Trick Generic __parameters__.
+ __class__ = typing.TypeVar
+
+ @property
+ def args(self):
+ return ParamSpecArgs(self)
+
+ @property
+ def kwargs(self):
+ return ParamSpecKwargs(self)
+
+ def __init__(self, name, *, bound=None, covariant=False, contravariant=False):
+ super().__init__([self])
+ self.__name__ = name
+ self.__covariant__ = bool(covariant)
+ self.__contravariant__ = bool(contravariant)
+ if bound:
+ self.__bound__ = typing._type_check(bound, 'Bound must be a type.')
+ else:
+ self.__bound__ = None
+
+ # for pickling:
+ try:
+ def_mod = sys._getframe(1).f_globals.get('__name__', '__main__')
+ except (AttributeError, ValueError):
+ def_mod = None
+ if def_mod != 'typing_extensions':
+ self.__module__ = def_mod
+
+ def __repr__(self):
+ if self.__covariant__:
+ prefix = '+'
+ elif self.__contravariant__:
+ prefix = '-'
+ else:
+ prefix = '~'
+ return prefix + self.__name__
+
+ def __hash__(self):
+ return object.__hash__(self)
+
+ def __eq__(self, other):
+ return self is other
+
+ def __reduce__(self):
+ return self.__name__
+
+ # Hack to get typing._type_check to pass.
+ def __call__(self, *args, **kwargs):
+ pass
+
+ if not PEP_560:
+ # Only needed in 3.6.
+ def _get_type_vars(self, tvars):
+ if self not in tvars:
+ tvars.append(self)
+
+
+# 3.6-3.9
+if not hasattr(typing, 'Concatenate'):
+ # Inherits from list as a workaround for Callable checks in Python < 3.9.2.
+ class _ConcatenateGenericAlias(list):
+
+ # Trick Generic into looking into this for __parameters__.
+ if PEP_560:
+ __class__ = typing._GenericAlias
+ else:
+ __class__ = typing._TypingBase
+
+ # Flag in 3.8.
+ _special = False
+ # Attribute in 3.6 and earlier.
+ _gorg = typing.Generic
+
+ def __init__(self, origin, args):
+ super().__init__(args)
+ self.__origin__ = origin
+ self.__args__ = args
+
+ def __repr__(self):
+ _type_repr = typing._type_repr
+ return (f'{_type_repr(self.__origin__)}'
+ f'[{", ".join(_type_repr(arg) for arg in self.__args__)}]')
+
+ def __hash__(self):
+ return hash((self.__origin__, self.__args__))
+
+ # Hack to get typing._type_check to pass in Generic.
+ def __call__(self, *args, **kwargs):
+ pass
+
+ @property
+ def __parameters__(self):
+ return tuple(
+ tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec))
+ )
+
+ if not PEP_560:
+ # Only required in 3.6.
+ def _get_type_vars(self, tvars):
+ if self.__origin__ and self.__parameters__:
+ typing._get_type_vars(self.__parameters__, tvars)
+
+
+# 3.6-3.9
+@typing._tp_cache
+def _concatenate_getitem(self, parameters):
+ if parameters == ():
+ raise TypeError("Cannot take a Concatenate of no types.")
+ if not isinstance(parameters, tuple):
+ parameters = (parameters,)
+ if not isinstance(parameters[-1], ParamSpec):
+ raise TypeError("The last parameter to Concatenate should be a "
+ "ParamSpec variable.")
+ msg = "Concatenate[arg, ...]: each arg must be a type."
+ parameters = tuple(typing._type_check(p, msg) for p in parameters)
+ return _ConcatenateGenericAlias(self, parameters)
+
+
+# 3.10+
+if hasattr(typing, 'Concatenate'):
+ Concatenate = typing.Concatenate
+ _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # noqa
+# 3.9
+elif sys.version_info[:2] >= (3, 9):
+ @_TypeAliasForm
+ def Concatenate(self, parameters):
+ """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a
+ higher order function which adds, removes or transforms parameters of a
+ callable.
+
+ For example::
+
+ Callable[Concatenate[int, P], int]
+
+ See PEP 612 for detailed information.
+ """
+ return _concatenate_getitem(self, parameters)
+# 3.7-8
+elif sys.version_info[:2] >= (3, 7):
+ class _ConcatenateForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ def __getitem__(self, parameters):
+ return _concatenate_getitem(self, parameters)
+
+ Concatenate = _ConcatenateForm(
+ 'Concatenate',
+ doc="""Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a
+ higher order function which adds, removes or transforms parameters of a
+ callable.
+
+ For example::
+
+ Callable[Concatenate[int, P], int]
+
+ See PEP 612 for detailed information.
+ """)
+# 3.6
+else:
+ class _ConcatenateAliasMeta(typing.TypingMeta):
+ """Metaclass for Concatenate."""
+
+ def __repr__(self):
+ return 'typing_extensions.Concatenate'
+
+ class _ConcatenateAliasBase(typing._FinalTypingBase,
+ metaclass=_ConcatenateAliasMeta,
+ _root=True):
+ """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a
+ higher order function which adds, removes or transforms parameters of a
+ callable.
+
+ For example::
+
+ Callable[Concatenate[int, P], int]
+
+ See PEP 612 for detailed information.
+ """
+ __slots__ = ()
+
+ def __instancecheck__(self, obj):
+ raise TypeError("Concatenate cannot be used with isinstance().")
+
+ def __subclasscheck__(self, cls):
+ raise TypeError("Concatenate cannot be used with issubclass().")
+
+ def __repr__(self):
+ return 'typing_extensions.Concatenate'
+
+ def __getitem__(self, parameters):
+ return _concatenate_getitem(self, parameters)
+
+ Concatenate = _ConcatenateAliasBase(_root=True)
+
+# 3.10+
+if hasattr(typing, 'TypeGuard'):
+ TypeGuard = typing.TypeGuard
+# 3.9
+elif sys.version_info[:2] >= (3, 9):
+ class _TypeGuardForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ @_TypeGuardForm
+ def TypeGuard(self, parameters):
+ """Special typing form used to annotate the return type of a user-defined
+ type guard function. ``TypeGuard`` only accepts a single type argument.
+ At runtime, functions marked this way should return a boolean.
+
+ ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
+ type checkers to determine a more precise type of an expression within a
+ program's code flow. Usually type narrowing is done by analyzing
+ conditional code flow and applying the narrowing to a block of code. The
+ conditional expression here is sometimes referred to as a "type guard".
+
+ Sometimes it would be convenient to use a user-defined boolean function
+ as a type guard. Such a function should use ``TypeGuard[...]`` as its
+ return type to alert static type checkers to this intention.
+
+ Using ``-> TypeGuard`` tells the static type checker that for a given
+ function:
+
+ 1. The return value is a boolean.
+ 2. If the return value is ``True``, the type of its argument
+ is the type inside ``TypeGuard``.
+
+ For example::
+
+ def is_str(val: Union[str, float]):
+ # "isinstance" type guard
+ if isinstance(val, str):
+ # Type of ``val`` is narrowed to ``str``
+ ...
+ else:
+ # Else, type of ``val`` is narrowed to ``float``.
+ ...
+
+ Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower
+ form of ``TypeA`` (it can even be a wider form) and this may lead to
+ type-unsafe results. The main reason is to allow for things like
+ narrowing ``List[object]`` to ``List[str]`` even though the latter is not
+ a subtype of the former, since ``List`` is invariant. The responsibility of
+ writing type-safe type guards is left to the user.
+
+ ``TypeGuard`` also works with type variables. For more information, see
+ PEP 647 (User-Defined Type Guards).
+ """
+ item = typing._type_check(parameters, f'{self} accepts only single type.')
+ return typing._GenericAlias(self, (item,))
+# 3.7-3.8
+elif sys.version_info[:2] >= (3, 7):
+ class _TypeGuardForm(typing._SpecialForm, _root=True):
+
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ def __getitem__(self, parameters):
+ item = typing._type_check(parameters,
+ f'{self._name} accepts only a single type')
+ return typing._GenericAlias(self, (item,))
+
+ TypeGuard = _TypeGuardForm(
+ 'TypeGuard',
+ doc="""Special typing form used to annotate the return type of a user-defined
+ type guard function. ``TypeGuard`` only accepts a single type argument.
+ At runtime, functions marked this way should return a boolean.
+
+ ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
+ type checkers to determine a more precise type of an expression within a
+ program's code flow. Usually type narrowing is done by analyzing
+ conditional code flow and applying the narrowing to a block of code. The
+ conditional expression here is sometimes referred to as a "type guard".
+
+ Sometimes it would be convenient to use a user-defined boolean function
+ as a type guard. Such a function should use ``TypeGuard[...]`` as its
+ return type to alert static type checkers to this intention.
+
+ Using ``-> TypeGuard`` tells the static type checker that for a given
+ function:
+
+ 1. The return value is a boolean.
+ 2. If the return value is ``True``, the type of its argument
+ is the type inside ``TypeGuard``.
+
+ For example::
+
+ def is_str(val: Union[str, float]):
+ # "isinstance" type guard
+ if isinstance(val, str):
+ # Type of ``val`` is narrowed to ``str``
+ ...
+ else:
+ # Else, type of ``val`` is narrowed to ``float``.
+ ...
+
+ Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower
+ form of ``TypeA`` (it can even be a wider form) and this may lead to
+ type-unsafe results. The main reason is to allow for things like
+ narrowing ``List[object]`` to ``List[str]`` even though the latter is not
+ a subtype of the former, since ``List`` is invariant. The responsibility of
+ writing type-safe type guards is left to the user.
+
+ ``TypeGuard`` also works with type variables. For more information, see
+ PEP 647 (User-Defined Type Guards).
+ """)
+# 3.6
+else:
+ class _TypeGuard(typing._FinalTypingBase, _root=True):
+ """Special typing form used to annotate the return type of a user-defined
+ type guard function. ``TypeGuard`` only accepts a single type argument.
+ At runtime, functions marked this way should return a boolean.
+
+ ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
+ type checkers to determine a more precise type of an expression within a
+ program's code flow. Usually type narrowing is done by analyzing
+ conditional code flow and applying the narrowing to a block of code. The
+ conditional expression here is sometimes referred to as a "type guard".
+
+ Sometimes it would be convenient to use a user-defined boolean function
+ as a type guard. Such a function should use ``TypeGuard[...]`` as its
+ return type to alert static type checkers to this intention.
+
+ Using ``-> TypeGuard`` tells the static type checker that for a given
+ function:
+
+ 1. The return value is a boolean.
+ 2. If the return value is ``True``, the type of its argument
+ is the type inside ``TypeGuard``.
+
+ For example::
+
+ def is_str(val: Union[str, float]):
+ # "isinstance" type guard
+ if isinstance(val, str):
+ # Type of ``val`` is narrowed to ``str``
+ ...
+ else:
+ # Else, type of ``val`` is narrowed to ``float``.
+ ...
+
+ Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower
+ form of ``TypeA`` (it can even be a wider form) and this may lead to
+ type-unsafe results. The main reason is to allow for things like
+ narrowing ``List[object]`` to ``List[str]`` even though the latter is not
+ a subtype of the former, since ``List`` is invariant. The responsibility of
+ writing type-safe type guards is left to the user.
+
+ ``TypeGuard`` also works with type variables. For more information, see
+ PEP 647 (User-Defined Type Guards).
+ """
+
+ __slots__ = ('__type__',)
+
+ def __init__(self, tp=None, **kwds):
+ self.__type__ = tp
+
+ def __getitem__(self, item):
+ cls = type(self)
+ if self.__type__ is None:
+ return cls(typing._type_check(item,
+ f'{cls.__name__[1:]} accepts only a single type.'),
+ _root=True)
+ raise TypeError(f'{cls.__name__[1:]} cannot be further subscripted')
+
+ def _eval_type(self, globalns, localns):
+ new_tp = typing._eval_type(self.__type__, globalns, localns)
+ if new_tp == self.__type__:
+ return self
+ return type(self)(new_tp, _root=True)
+
+ def __repr__(self):
+ r = super().__repr__()
+ if self.__type__ is not None:
+ r += f'[{typing._type_repr(self.__type__)}]'
+ return r
+
+ def __hash__(self):
+ return hash((type(self).__name__, self.__type__))
+
+ def __eq__(self, other):
+ if not isinstance(other, _TypeGuard):
+ return NotImplemented
+ if self.__type__ is not None:
+ return self.__type__ == other.__type__
+ return self is other
+
+ TypeGuard = _TypeGuard(_root=True)
+
+
+if sys.version_info[:2] >= (3, 7):
+ # Vendored from cpython typing._SpecialFrom
+ class _SpecialForm(typing._Final, _root=True):
+ __slots__ = ('_name', '__doc__', '_getitem')
+
+ def __init__(self, getitem):
+ self._getitem = getitem
+ self._name = getitem.__name__
+ self.__doc__ = getitem.__doc__
+
+ def __getattr__(self, item):
+ if item in {'__name__', '__qualname__'}:
+ return self._name
+
+ raise AttributeError(item)
+
+ def __mro_entries__(self, bases):
+ raise TypeError(f"Cannot subclass {self!r}")
+
+ def __repr__(self):
+ return f'typing_extensions.{self._name}'
+
+ def __reduce__(self):
+ return self._name
+
+ def __call__(self, *args, **kwds):
+ raise TypeError(f"Cannot instantiate {self!r}")
+
+ def __or__(self, other):
+ return typing.Union[self, other]
+
+ def __ror__(self, other):
+ return typing.Union[other, self]
+
+ def __instancecheck__(self, obj):
+ raise TypeError(f"{self} cannot be used with isinstance()")
+
+ def __subclasscheck__(self, cls):
+ raise TypeError(f"{self} cannot be used with issubclass()")
+
+ @typing._tp_cache
+ def __getitem__(self, parameters):
+ return self._getitem(self, parameters)
+
+
+if hasattr(typing, "LiteralString"):
+ LiteralString = typing.LiteralString
+elif sys.version_info[:2] >= (3, 7):
+ @_SpecialForm
+ def LiteralString(self, params):
+ """Represents an arbitrary literal string.
+
+ Example::
+
+ from typing_extensions import LiteralString
+
+ def query(sql: LiteralString) -> ...:
+ ...
+
+ query("SELECT * FROM table") # ok
+ query(f"SELECT * FROM {input()}") # not ok
+
+ See PEP 675 for details.
+
+ """
+ raise TypeError(f"{self} is not subscriptable")
+else:
+ class _LiteralString(typing._FinalTypingBase, _root=True):
+ """Represents an arbitrary literal string.
+
+ Example::
+
+ from typing_extensions import LiteralString
+
+ def query(sql: LiteralString) -> ...:
+ ...
+
+ query("SELECT * FROM table") # ok
+ query(f"SELECT * FROM {input()}") # not ok
+
+ See PEP 675 for details.
+
+ """
+
+ __slots__ = ()
+
+ def __instancecheck__(self, obj):
+ raise TypeError(f"{self} cannot be used with isinstance().")
+
+ def __subclasscheck__(self, cls):
+ raise TypeError(f"{self} cannot be used with issubclass().")
+
+ LiteralString = _LiteralString(_root=True)
+
+
+if hasattr(typing, "Self"):
+ Self = typing.Self
+elif sys.version_info[:2] >= (3, 7):
+ @_SpecialForm
+ def Self(self, params):
+ """Used to spell the type of "self" in classes.
+
+ Example::
+
+ from typing import Self
+
+ class ReturnsSelf:
+ def parse(self, data: bytes) -> Self:
+ ...
+ return self
+
+ """
+
+ raise TypeError(f"{self} is not subscriptable")
+else:
+ class _Self(typing._FinalTypingBase, _root=True):
+ """Used to spell the type of "self" in classes.
+
+ Example::
+
+ from typing import Self
+
+ class ReturnsSelf:
+ def parse(self, data: bytes) -> Self:
+ ...
+ return self
+
+ """
+
+ __slots__ = ()
+
+ def __instancecheck__(self, obj):
+ raise TypeError(f"{self} cannot be used with isinstance().")
+
+ def __subclasscheck__(self, cls):
+ raise TypeError(f"{self} cannot be used with issubclass().")
+
+ Self = _Self(_root=True)
+
+
+if hasattr(typing, "Never"):
+ Never = typing.Never
+elif sys.version_info[:2] >= (3, 7):
+ @_SpecialForm
+ def Never(self, params):
+ """The bottom type, a type that has no members.
+
+ This can be used to define a function that should never be
+ called, or a function that never returns::
+
+ from typing_extensions import Never
+
+ def never_call_me(arg: Never) -> None:
+ pass
+
+ def int_or_str(arg: int | str) -> None:
+ never_call_me(arg) # type checker error
+ match arg:
+ case int():
+ print("It's an int")
+ case str():
+ print("It's a str")
+ case _:
+ never_call_me(arg) # ok, arg is of type Never
+
+ """
+
+ raise TypeError(f"{self} is not subscriptable")
+else:
+ class _Never(typing._FinalTypingBase, _root=True):
+ """The bottom type, a type that has no members.
+
+ This can be used to define a function that should never be
+ called, or a function that never returns::
+
+ from typing_extensions import Never
+
+ def never_call_me(arg: Never) -> None:
+ pass
+
+ def int_or_str(arg: int | str) -> None:
+ never_call_me(arg) # type checker error
+ match arg:
+ case int():
+ print("It's an int")
+ case str():
+ print("It's a str")
+ case _:
+ never_call_me(arg) # ok, arg is of type Never
+
+ """
+
+ __slots__ = ()
+
+ def __instancecheck__(self, obj):
+ raise TypeError(f"{self} cannot be used with isinstance().")
+
+ def __subclasscheck__(self, cls):
+ raise TypeError(f"{self} cannot be used with issubclass().")
+
+ Never = _Never(_root=True)
+
+
+if hasattr(typing, 'Required'):
+ Required = typing.Required
+ NotRequired = typing.NotRequired
+elif sys.version_info[:2] >= (3, 9):
+ class _ExtensionsSpecialForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ @_ExtensionsSpecialForm
+ def Required(self, parameters):
+ """A special typing construct to mark a key of a total=False TypedDict
+ as required. For example:
+
+ class Movie(TypedDict, total=False):
+ title: Required[str]
+ year: int
+
+ m = Movie(
+ title='The Matrix', # typechecker error if key is omitted
+ year=1999,
+ )
+
+ There is no runtime checking that a required key is actually provided
+ when instantiating a related TypedDict.
+ """
+ item = typing._type_check(parameters, f'{self._name} accepts only single type')
+ return typing._GenericAlias(self, (item,))
+
+ @_ExtensionsSpecialForm
+ def NotRequired(self, parameters):
+ """A special typing construct to mark a key of a TypedDict as
+ potentially missing. For example:
+
+ class Movie(TypedDict):
+ title: str
+ year: NotRequired[int]
+
+ m = Movie(
+ title='The Matrix', # typechecker error if key is omitted
+ year=1999,
+ )
+ """
+ item = typing._type_check(parameters, f'{self._name} accepts only single type')
+ return typing._GenericAlias(self, (item,))
+
+elif sys.version_info[:2] >= (3, 7):
+ class _RequiredForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ def __getitem__(self, parameters):
+ item = typing._type_check(parameters,
+ '{} accepts only single type'.format(self._name))
+ return typing._GenericAlias(self, (item,))
+
+ Required = _RequiredForm(
+ 'Required',
+ doc="""A special typing construct to mark a key of a total=False TypedDict
+ as required. For example:
+
+ class Movie(TypedDict, total=False):
+ title: Required[str]
+ year: int
+
+ m = Movie(
+ title='The Matrix', # typechecker error if key is omitted
+ year=1999,
+ )
+
+ There is no runtime checking that a required key is actually provided
+ when instantiating a related TypedDict.
+ """)
+ NotRequired = _RequiredForm(
+ 'NotRequired',
+ doc="""A special typing construct to mark a key of a TypedDict as
+ potentially missing. For example:
+
+ class Movie(TypedDict):
+ title: str
+ year: NotRequired[int]
+
+ m = Movie(
+ title='The Matrix', # typechecker error if key is omitted
+ year=1999,
+ )
+ """)
+else:
+ # NOTE: Modeled after _Final's implementation when _FinalTypingBase available
+ class _MaybeRequired(typing._FinalTypingBase, _root=True):
+ __slots__ = ('__type__',)
+
+ def __init__(self, tp=None, **kwds):
+ self.__type__ = tp
+
+ def __getitem__(self, item):
+ cls = type(self)
+ if self.__type__ is None:
+ return cls(typing._type_check(item,
+ '{} accepts only single type.'.format(cls.__name__[1:])),
+ _root=True)
+ raise TypeError('{} cannot be further subscripted'
+ .format(cls.__name__[1:]))
+
+ def _eval_type(self, globalns, localns):
+ new_tp = typing._eval_type(self.__type__, globalns, localns)
+ if new_tp == self.__type__:
+ return self
+ return type(self)(new_tp, _root=True)
+
+ def __repr__(self):
+ r = super().__repr__()
+ if self.__type__ is not None:
+ r += '[{}]'.format(typing._type_repr(self.__type__))
+ return r
+
+ def __hash__(self):
+ return hash((type(self).__name__, self.__type__))
+
+ def __eq__(self, other):
+ if not isinstance(other, type(self)):
+ return NotImplemented
+ if self.__type__ is not None:
+ return self.__type__ == other.__type__
+ return self is other
+
+ class _Required(_MaybeRequired, _root=True):
+ """A special typing construct to mark a key of a total=False TypedDict
+ as required. For example:
+
+ class Movie(TypedDict, total=False):
+ title: Required[str]
+ year: int
+
+ m = Movie(
+ title='The Matrix', # typechecker error if key is omitted
+ year=1999,
+ )
+
+ There is no runtime checking that a required key is actually provided
+ when instantiating a related TypedDict.
+ """
+
+ class _NotRequired(_MaybeRequired, _root=True):
+ """A special typing construct to mark a key of a TypedDict as
+ potentially missing. For example:
+
+ class Movie(TypedDict):
+ title: str
+ year: NotRequired[int]
+
+ m = Movie(
+ title='The Matrix', # typechecker error if key is omitted
+ year=1999,
+ )
+ """
+
+ Required = _Required(_root=True)
+ NotRequired = _NotRequired(_root=True)
+
+
+if sys.version_info[:2] >= (3, 9):
+ class _UnpackSpecialForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ class _UnpackAlias(typing._GenericAlias, _root=True):
+ __class__ = typing.TypeVar
+
+ @_UnpackSpecialForm
+ def Unpack(self, parameters):
+ """A special typing construct to unpack a variadic type. For example:
+
+ Shape = TypeVarTuple('Shape')
+ Batch = NewType('Batch', int)
+
+ def add_batch_axis(
+ x: Array[Unpack[Shape]]
+ ) -> Array[Batch, Unpack[Shape]]: ...
+
+ """
+ item = typing._type_check(parameters, f'{self._name} accepts only single type')
+ return _UnpackAlias(self, (item,))
+
+ def _is_unpack(obj):
+ return isinstance(obj, _UnpackAlias)
+
+elif sys.version_info[:2] >= (3, 7):
+ class _UnpackAlias(typing._GenericAlias, _root=True):
+ __class__ = typing.TypeVar
+
+ class _UnpackForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ def __getitem__(self, parameters):
+ item = typing._type_check(parameters,
+ f'{self._name} accepts only single type')
+ return _UnpackAlias(self, (item,))
+
+ Unpack = _UnpackForm(
+ 'Unpack',
+ doc="""A special typing construct to unpack a variadic type. For example:
+
+ Shape = TypeVarTuple('Shape')
+ Batch = NewType('Batch', int)
+
+ def add_batch_axis(
+ x: Array[Unpack[Shape]]
+ ) -> Array[Batch, Unpack[Shape]]: ...
+
+ """)
+
+ def _is_unpack(obj):
+ return isinstance(obj, _UnpackAlias)
+
+else:
+ # NOTE: Modeled after _Final's implementation when _FinalTypingBase available
+ class _Unpack(typing._FinalTypingBase, _root=True):
+ """A special typing construct to unpack a variadic type. For example:
+
+ Shape = TypeVarTuple('Shape')
+ Batch = NewType('Batch', int)
+
+ def add_batch_axis(
+ x: Array[Unpack[Shape]]
+ ) -> Array[Batch, Unpack[Shape]]: ...
+
+ """
+ __slots__ = ('__type__',)
+ __class__ = typing.TypeVar
+
+ def __init__(self, tp=None, **kwds):
+ self.__type__ = tp
+
+ def __getitem__(self, item):
+ cls = type(self)
+ if self.__type__ is None:
+ return cls(typing._type_check(item,
+ 'Unpack accepts only single type.'),
+ _root=True)
+ raise TypeError('Unpack cannot be further subscripted')
+
+ def _eval_type(self, globalns, localns):
+ new_tp = typing._eval_type(self.__type__, globalns, localns)
+ if new_tp == self.__type__:
+ return self
+ return type(self)(new_tp, _root=True)
+
+ def __repr__(self):
+ r = super().__repr__()
+ if self.__type__ is not None:
+ r += '[{}]'.format(typing._type_repr(self.__type__))
+ return r
+
+ def __hash__(self):
+ return hash((type(self).__name__, self.__type__))
+
+ def __eq__(self, other):
+ if not isinstance(other, _Unpack):
+ return NotImplemented
+ if self.__type__ is not None:
+ return self.__type__ == other.__type__
+ return self is other
+
+ # For 3.6 only
+ def _get_type_vars(self, tvars):
+ self.__type__._get_type_vars(tvars)
+
+ Unpack = _Unpack(_root=True)
+
+ def _is_unpack(obj):
+ return isinstance(obj, _Unpack)
+
+
+class TypeVarTuple:
+ """Type variable tuple.
+
+ Usage::
+
+ Ts = TypeVarTuple('Ts')
+
+ In the same way that a normal type variable is a stand-in for a single
+ type such as ``int``, a type variable *tuple* is a stand-in for a *tuple* type such as
+ ``Tuple[int, str]``.
+
+ Type variable tuples can be used in ``Generic`` declarations.
+ Consider the following example::
+
+ class Array(Generic[*Ts]): ...
+
+ The ``Ts`` type variable tuple here behaves like ``tuple[T1, T2]``,
+ where ``T1`` and ``T2`` are type variables. To use these type variables
+ as type parameters of ``Array``, we must *unpack* the type variable tuple using
+ the star operator: ``*Ts``. The signature of ``Array`` then behaves
+ as if we had simply written ``class Array(Generic[T1, T2]): ...``.
+ In contrast to ``Generic[T1, T2]``, however, ``Generic[*Shape]`` allows
+ us to parameterise the class with an *arbitrary* number of type parameters.
+
+ Type variable tuples can be used anywhere a normal ``TypeVar`` can.
+ This includes class definitions, as shown above, as well as function
+ signatures and variable annotations::
+
+ class Array(Generic[*Ts]):
+
+ def __init__(self, shape: Tuple[*Ts]):
+ self._shape: Tuple[*Ts] = shape
+
+ def get_shape(self) -> Tuple[*Ts]:
+ return self._shape
+
+ shape = (Height(480), Width(640))
+ x: Array[Height, Width] = Array(shape)
+ y = abs(x) # Inferred type is Array[Height, Width]
+ z = x + x # ... is Array[Height, Width]
+ x.get_shape() # ... is tuple[Height, Width]
+
+ """
+
+ # Trick Generic __parameters__.
+ __class__ = typing.TypeVar
+
+ def __iter__(self):
+ yield self.__unpacked__
+
+ def __init__(self, name):
+ self.__name__ = name
+
+ # for pickling:
+ try:
+ def_mod = sys._getframe(1).f_globals.get('__name__', '__main__')
+ except (AttributeError, ValueError):
+ def_mod = None
+ if def_mod != 'typing_extensions':
+ self.__module__ = def_mod
+
+ self.__unpacked__ = Unpack[self]
+
+ def __repr__(self):
+ return self.__name__
+
+ def __hash__(self):
+ return object.__hash__(self)
+
+ def __eq__(self, other):
+ return self is other
+
+ def __reduce__(self):
+ return self.__name__
+
+ def __init_subclass__(self, *args, **kwds):
+ if '_root' not in kwds:
+ raise TypeError("Cannot subclass special typing classes")
+
+ if not PEP_560:
+ # Only needed in 3.6.
+ def _get_type_vars(self, tvars):
+ if self not in tvars:
+ tvars.append(self)
+
+
+if hasattr(typing, "reveal_type"):
+ reveal_type = typing.reveal_type
+else:
+ def reveal_type(__obj: T) -> T:
+ """Reveal the inferred type of a variable.
+
+ When a static type checker encounters a call to ``reveal_type()``,
+ it will emit the inferred type of the argument::
+
+ x: int = 1
+ reveal_type(x)
+
+ Running a static type checker (e.g., ``mypy``) on this example
+ will produce output similar to 'Revealed type is "builtins.int"'.
+
+ At runtime, the function prints the runtime type of the
+ argument and returns it unchanged.
+
+ """
+ print(f"Runtime type is {type(__obj).__name__!r}", file=sys.stderr)
+ return __obj
+
+
+if hasattr(typing, "assert_never"):
+ assert_never = typing.assert_never
+else:
+ def assert_never(__arg: Never) -> Never:
+ """Assert to the type checker that a line of code is unreachable.
+
+ Example::
+
+ def int_or_str(arg: int | str) -> None:
+ match arg:
+ case int():
+ print("It's an int")
+ case str():
+ print("It's a str")
+ case _:
+ assert_never(arg)
+
+ If a type checker finds that a call to assert_never() is
+ reachable, it will emit an error.
+
+ At runtime, this throws an exception when called.
+
+ """
+ raise AssertionError("Expected code to be unreachable")
+
+
+if hasattr(typing, 'dataclass_transform'):
+ dataclass_transform = typing.dataclass_transform
+else:
+ def dataclass_transform(
+ *,
+ eq_default: bool = True,
+ order_default: bool = False,
+ kw_only_default: bool = False,
+ field_descriptors: typing.Tuple[
+ typing.Union[typing.Type[typing.Any], typing.Callable[..., typing.Any]],
+ ...
+ ] = (),
+ ) -> typing.Callable[[T], T]:
+ """Decorator that marks a function, class, or metaclass as providing
+ dataclass-like behavior.
+
+ Example:
+
+ from typing_extensions import dataclass_transform
+
+ _T = TypeVar("_T")
+
+ # Used on a decorator function
+ @dataclass_transform()
+ def create_model(cls: type[_T]) -> type[_T]:
+ ...
+ return cls
+
+ @create_model
+ class CustomerModel:
+ id: int
+ name: str
+
+ # Used on a base class
+ @dataclass_transform()
+ class ModelBase: ...
+
+ class CustomerModel(ModelBase):
+ id: int
+ name: str
+
+ # Used on a metaclass
+ @dataclass_transform()
+ class ModelMeta(type): ...
+
+ class ModelBase(metaclass=ModelMeta): ...
+
+ class CustomerModel(ModelBase):
+ id: int
+ name: str
+
+ Each of the ``CustomerModel`` classes defined in this example will now
+ behave similarly to a dataclass created with the ``@dataclasses.dataclass``
+ decorator. For example, the type checker will synthesize an ``__init__``
+ method.
+
+ The arguments to this decorator can be used to customize this behavior:
+ - ``eq_default`` indicates whether the ``eq`` parameter is assumed to be
+ True or False if it is omitted by the caller.
+ - ``order_default`` indicates whether the ``order`` parameter is
+ assumed to be True or False if it is omitted by the caller.
+ - ``kw_only_default`` indicates whether the ``kw_only`` parameter is
+ assumed to be True or False if it is omitted by the caller.
+ - ``field_descriptors`` specifies a static list of supported classes
+ or functions, that describe fields, similar to ``dataclasses.field()``.
+
+ At runtime, this decorator records its arguments in the
+ ``__dataclass_transform__`` attribute on the decorated object.
+
+ See PEP 681 for details.
+
+ """
+ def decorator(cls_or_fn):
+ cls_or_fn.__dataclass_transform__ = {
+ "eq_default": eq_default,
+ "order_default": order_default,
+ "kw_only_default": kw_only_default,
+ "field_descriptors": field_descriptors,
+ }
+ return cls_or_fn
+ return decorator
+
+
+# We have to do some monkey patching to deal with the dual nature of
+# Unpack/TypeVarTuple:
+# - We want Unpack to be a kind of TypeVar so it gets accepted in
+# Generic[Unpack[Ts]]
+# - We want it to *not* be treated as a TypeVar for the purposes of
+# counting generic parameters, so that when we subscript a generic,
+# the runtime doesn't try to substitute the Unpack with the subscripted type.
+if not hasattr(typing, "TypeVarTuple"):
+ typing._collect_type_vars = _collect_type_vars
+ typing._check_generic = _check_generic
diff --git a/testing/web-platform/tests/tools/third_party/webencodings/PKG-INFO b/testing/web-platform/tests/tools/third_party/webencodings/PKG-INFO
new file mode 100644
index 0000000000..2a827bbad9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/webencodings/PKG-INFO
@@ -0,0 +1,50 @@
+Metadata-Version: 1.1
+Name: webencodings
+Version: 0.5.1
+Summary: Character encoding aliases for legacy web content
+Home-page: https://github.com/SimonSapin/python-webencodings
+Author: Geoffrey Sneddon
+Author-email: me@gsnedders.com
+License: BSD
+Description: python-webencodings
+ ===================
+
+ This is a Python implementation of the `WHATWG Encoding standard
+ <http://encoding.spec.whatwg.org/>`_.
+
+ * Latest documentation: http://packages.python.org/webencodings/
+ * Source code and issue tracker:
+ https://github.com/gsnedders/python-webencodings
+ * PyPI releases: http://pypi.python.org/pypi/webencodings
+ * License: BSD
+ * Python 2.6+ and 3.3+
+
+ In order to be compatible with legacy web content
+ when interpreting something like ``Content-Type: text/html; charset=latin1``,
+ tools need to use a particular set of aliases for encoding labels
+ as well as some overriding rules.
+ For example, ``US-ASCII`` and ``iso-8859-1`` on the web are actually
+ aliases for ``windows-1252``, and an UTF-8 or UTF-16 BOM takes precedence
+ over any other encoding declaration.
+ The Encoding standard defines all such details so that implementations do
+ not have to reverse-engineer each other.
+
+ This module has encoding labels and BOM detection,
+ but the actual implementation for encoders and decoders is Python’s.
+
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Internet :: WWW/HTTP
diff --git a/testing/web-platform/tests/tools/third_party/webencodings/README.rst b/testing/web-platform/tests/tools/third_party/webencodings/README.rst
new file mode 100644
index 0000000000..c7e0f0cc3e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/webencodings/README.rst
@@ -0,0 +1,25 @@
+python-webencodings
+===================
+
+This is a Python implementation of the `WHATWG Encoding standard
+<http://encoding.spec.whatwg.org/>`_.
+
+* Latest documentation: http://packages.python.org/webencodings/
+* Source code and issue tracker:
+ https://github.com/gsnedders/python-webencodings
+* PyPI releases: http://pypi.python.org/pypi/webencodings
+* License: BSD
+* Python 2.6+ and 3.3+
+
+In order to be compatible with legacy web content
+when interpreting something like ``Content-Type: text/html; charset=latin1``,
+tools need to use a particular set of aliases for encoding labels
+as well as some overriding rules.
+For example, ``US-ASCII`` and ``iso-8859-1`` on the web are actually
+aliases for ``windows-1252``, and an UTF-8 or UTF-16 BOM takes precedence
+over any other encoding declaration.
+The Encoding standard defines all such details so that implementations do
+not have to reverse-engineer each other.
+
+This module has encoding labels and BOM detection,
+but the actual implementation for encoders and decoders is Python’s.
diff --git a/testing/web-platform/tests/tools/third_party/webencodings/setup.cfg b/testing/web-platform/tests/tools/third_party/webencodings/setup.cfg
new file mode 100644
index 0000000000..460b0b4057
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/webencodings/setup.cfg
@@ -0,0 +1,14 @@
+[bdist_wheel]
+universal = 1
+
+[build_sphinx]
+source-dir = docs
+build-dir = docs/_build
+
+[upload_sphinx]
+upload-dir = docs/_build/html
+
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/testing/web-platform/tests/tools/third_party/webencodings/setup.py b/testing/web-platform/tests/tools/third_party/webencodings/setup.py
new file mode 100644
index 0000000000..cf341cfd47
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/webencodings/setup.py
@@ -0,0 +1,47 @@
+from setuptools import setup, find_packages
+import io
+from os import path
+import re
+
+
+VERSION = re.search("VERSION = '([^']+)'", io.open(
+ path.join(path.dirname(__file__), 'webencodings', '__init__.py'),
+ encoding='utf-8'
+).read().strip()).group(1)
+
+LONG_DESCRIPTION = io.open(
+ path.join(path.dirname(__file__), 'README.rst'),
+ encoding='utf-8'
+).read()
+
+
+setup(
+ name='webencodings',
+ version=VERSION,
+ url='https://github.com/SimonSapin/python-webencodings',
+ license='BSD',
+ author='Simon Sapin',
+ author_email='simon.sapin@exyr.org',
+ maintainer='Geoffrey Sneddon',
+ maintainer_email='me@gsnedders.com',
+ description='Character encoding aliases for legacy web content',
+ long_description=LONG_DESCRIPTION,
+ classifiers=[
+ 'Development Status :: 4 - Beta',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ 'Programming Language :: Python :: Implementation :: PyPy',
+ 'Topic :: Internet :: WWW/HTTP',
+ ],
+ packages=find_packages(),
+)
diff --git a/testing/web-platform/tests/tools/third_party/webencodings/webencodings/__init__.py b/testing/web-platform/tests/tools/third_party/webencodings/webencodings/__init__.py
new file mode 100644
index 0000000000..d21d697c88
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/webencodings/webencodings/__init__.py
@@ -0,0 +1,342 @@
+# coding: utf-8
+"""
+
+ webencodings
+ ~~~~~~~~~~~~
+
+ This is a Python implementation of the `WHATWG Encoding standard
+ <http://encoding.spec.whatwg.org/>`. See README for details.
+
+ :copyright: Copyright 2012 by Simon Sapin
+ :license: BSD, see LICENSE for details.
+
+"""
+
+from __future__ import unicode_literals
+
+import codecs
+
+from .labels import LABELS
+
+
+VERSION = '0.5.1'
+
+
+# Some names in Encoding are not valid Python aliases. Remap these.
+PYTHON_NAMES = {
+ 'iso-8859-8-i': 'iso-8859-8',
+ 'x-mac-cyrillic': 'mac-cyrillic',
+ 'macintosh': 'mac-roman',
+ 'windows-874': 'cp874'}
+
+CACHE = {}
+
+
+def ascii_lower(string):
+ r"""Transform (only) ASCII letters to lower case: A-Z is mapped to a-z.
+
+ :param string: An Unicode string.
+ :returns: A new Unicode string.
+
+ This is used for `ASCII case-insensitive
+ <http://encoding.spec.whatwg.org/#ascii-case-insensitive>`_
+ matching of encoding labels.
+ The same matching is also used, among other things,
+ for `CSS keywords <http://dev.w3.org/csswg/css-values/#keywords>`_.
+
+ This is different from the :meth:`~py:str.lower` method of Unicode strings
+ which also affect non-ASCII characters,
+ sometimes mapping them into the ASCII range:
+
+ >>> keyword = u'Bac\N{KELVIN SIGN}ground'
+ >>> assert keyword.lower() == u'background'
+ >>> assert ascii_lower(keyword) != keyword.lower()
+ >>> assert ascii_lower(keyword) == u'bac\N{KELVIN SIGN}ground'
+
+ """
+ # This turns out to be faster than unicode.translate()
+ return string.encode('utf8').lower().decode('utf8')
+
+
+def lookup(label):
+ """
+ Look for an encoding by its label.
+ This is the spec’s `get an encoding
+ <http://encoding.spec.whatwg.org/#concept-encoding-get>`_ algorithm.
+ Supported labels are listed there.
+
+ :param label: A string.
+ :returns:
+ An :class:`Encoding` object, or :obj:`None` for an unknown label.
+
+ """
+ # Only strip ASCII whitespace: U+0009, U+000A, U+000C, U+000D, and U+0020.
+ label = ascii_lower(label.strip('\t\n\f\r '))
+ name = LABELS.get(label)
+ if name is None:
+ return None
+ encoding = CACHE.get(name)
+ if encoding is None:
+ if name == 'x-user-defined':
+ from .x_user_defined import codec_info
+ else:
+ python_name = PYTHON_NAMES.get(name, name)
+ # Any python_name value that gets to here should be valid.
+ codec_info = codecs.lookup(python_name)
+ encoding = Encoding(name, codec_info)
+ CACHE[name] = encoding
+ return encoding
+
+
+def _get_encoding(encoding_or_label):
+ """
+ Accept either an encoding object or label.
+
+ :param encoding: An :class:`Encoding` object or a label string.
+ :returns: An :class:`Encoding` object.
+ :raises: :exc:`~exceptions.LookupError` for an unknown label.
+
+ """
+ if hasattr(encoding_or_label, 'codec_info'):
+ return encoding_or_label
+
+ encoding = lookup(encoding_or_label)
+ if encoding is None:
+ raise LookupError('Unknown encoding label: %r' % encoding_or_label)
+ return encoding
+
+
+class Encoding(object):
+ """Reresents a character encoding such as UTF-8,
+ that can be used for decoding or encoding.
+
+ .. attribute:: name
+
+ Canonical name of the encoding
+
+ .. attribute:: codec_info
+
+ The actual implementation of the encoding,
+ a stdlib :class:`~codecs.CodecInfo` object.
+ See :func:`codecs.register`.
+
+ """
+ def __init__(self, name, codec_info):
+ self.name = name
+ self.codec_info = codec_info
+
+ def __repr__(self):
+ return '<Encoding %s>' % self.name
+
+
+#: The UTF-8 encoding. Should be used for new content and formats.
+UTF8 = lookup('utf-8')
+
+_UTF16LE = lookup('utf-16le')
+_UTF16BE = lookup('utf-16be')
+
+
+def decode(input, fallback_encoding, errors='replace'):
+ """
+ Decode a single string.
+
+ :param input: A byte string
+ :param fallback_encoding:
+ An :class:`Encoding` object or a label string.
+ The encoding to use if :obj:`input` does note have a BOM.
+ :param errors: Type of error handling. See :func:`codecs.register`.
+ :raises: :exc:`~exceptions.LookupError` for an unknown encoding label.
+ :return:
+ A ``(output, encoding)`` tuple of an Unicode string
+ and an :obj:`Encoding`.
+
+ """
+ # Fail early if `encoding` is an invalid label.
+ fallback_encoding = _get_encoding(fallback_encoding)
+ bom_encoding, input = _detect_bom(input)
+ encoding = bom_encoding or fallback_encoding
+ return encoding.codec_info.decode(input, errors)[0], encoding
+
+
+def _detect_bom(input):
+ """Return (bom_encoding, input), with any BOM removed from the input."""
+ if input.startswith(b'\xFF\xFE'):
+ return _UTF16LE, input[2:]
+ if input.startswith(b'\xFE\xFF'):
+ return _UTF16BE, input[2:]
+ if input.startswith(b'\xEF\xBB\xBF'):
+ return UTF8, input[3:]
+ return None, input
+
+
+def encode(input, encoding=UTF8, errors='strict'):
+ """
+ Encode a single string.
+
+ :param input: An Unicode string.
+ :param encoding: An :class:`Encoding` object or a label string.
+ :param errors: Type of error handling. See :func:`codecs.register`.
+ :raises: :exc:`~exceptions.LookupError` for an unknown encoding label.
+ :return: A byte string.
+
+ """
+ return _get_encoding(encoding).codec_info.encode(input, errors)[0]
+
+
+def iter_decode(input, fallback_encoding, errors='replace'):
+ """
+ "Pull"-based decoder.
+
+ :param input:
+ An iterable of byte strings.
+
+ The input is first consumed just enough to determine the encoding
+ based on the precense of a BOM,
+ then consumed on demand when the return value is.
+ :param fallback_encoding:
+ An :class:`Encoding` object or a label string.
+ The encoding to use if :obj:`input` does note have a BOM.
+ :param errors: Type of error handling. See :func:`codecs.register`.
+ :raises: :exc:`~exceptions.LookupError` for an unknown encoding label.
+ :returns:
+ An ``(output, encoding)`` tuple.
+ :obj:`output` is an iterable of Unicode strings,
+ :obj:`encoding` is the :obj:`Encoding` that is being used.
+
+ """
+
+ decoder = IncrementalDecoder(fallback_encoding, errors)
+ generator = _iter_decode_generator(input, decoder)
+ encoding = next(generator)
+ return generator, encoding
+
+
+def _iter_decode_generator(input, decoder):
+ """Return a generator that first yields the :obj:`Encoding`,
+ then yields output chukns as Unicode strings.
+
+ """
+ decode = decoder.decode
+ input = iter(input)
+ for chunck in input:
+ output = decode(chunck)
+ if output:
+ assert decoder.encoding is not None
+ yield decoder.encoding
+ yield output
+ break
+ else:
+ # Input exhausted without determining the encoding
+ output = decode(b'', final=True)
+ assert decoder.encoding is not None
+ yield decoder.encoding
+ if output:
+ yield output
+ return
+
+ for chunck in input:
+ output = decode(chunck)
+ if output:
+ yield output
+ output = decode(b'', final=True)
+ if output:
+ yield output
+
+
+def iter_encode(input, encoding=UTF8, errors='strict'):
+ """
+ “Pull”-based encoder.
+
+ :param input: An iterable of Unicode strings.
+ :param encoding: An :class:`Encoding` object or a label string.
+ :param errors: Type of error handling. See :func:`codecs.register`.
+ :raises: :exc:`~exceptions.LookupError` for an unknown encoding label.
+ :returns: An iterable of byte strings.
+
+ """
+ # Fail early if `encoding` is an invalid label.
+ encode = IncrementalEncoder(encoding, errors).encode
+ return _iter_encode_generator(input, encode)
+
+
+def _iter_encode_generator(input, encode):
+ for chunck in input:
+ output = encode(chunck)
+ if output:
+ yield output
+ output = encode('', final=True)
+ if output:
+ yield output
+
+
+class IncrementalDecoder(object):
+ """
+ “Push”-based decoder.
+
+ :param fallback_encoding:
+ An :class:`Encoding` object or a label string.
+ The encoding to use if :obj:`input` does note have a BOM.
+ :param errors: Type of error handling. See :func:`codecs.register`.
+ :raises: :exc:`~exceptions.LookupError` for an unknown encoding label.
+
+ """
+ def __init__(self, fallback_encoding, errors='replace'):
+ # Fail early if `encoding` is an invalid label.
+ self._fallback_encoding = _get_encoding(fallback_encoding)
+ self._errors = errors
+ self._buffer = b''
+ self._decoder = None
+ #: The actual :class:`Encoding` that is being used,
+ #: or :obj:`None` if that is not determined yet.
+ #: (Ie. if there is not enough input yet to determine
+ #: if there is a BOM.)
+ self.encoding = None # Not known yet.
+
+ def decode(self, input, final=False):
+ """Decode one chunk of the input.
+
+ :param input: A byte string.
+ :param final:
+ Indicate that no more input is available.
+ Must be :obj:`True` if this is the last call.
+ :returns: An Unicode string.
+
+ """
+ decoder = self._decoder
+ if decoder is not None:
+ return decoder(input, final)
+
+ input = self._buffer + input
+ encoding, input = _detect_bom(input)
+ if encoding is None:
+ if len(input) < 3 and not final: # Not enough data yet.
+ self._buffer = input
+ return ''
+ else: # No BOM
+ encoding = self._fallback_encoding
+ decoder = encoding.codec_info.incrementaldecoder(self._errors).decode
+ self._decoder = decoder
+ self.encoding = encoding
+ return decoder(input, final)
+
+
+class IncrementalEncoder(object):
+ """
+ “Push”-based encoder.
+
+ :param encoding: An :class:`Encoding` object or a label string.
+ :param errors: Type of error handling. See :func:`codecs.register`.
+ :raises: :exc:`~exceptions.LookupError` for an unknown encoding label.
+
+ .. method:: encode(input, final=False)
+
+ :param input: An Unicode string.
+ :param final:
+ Indicate that no more input is available.
+ Must be :obj:`True` if this is the last call.
+ :returns: A byte string.
+
+ """
+ def __init__(self, encoding=UTF8, errors='strict'):
+ encoding = _get_encoding(encoding)
+ self.encode = encoding.codec_info.incrementalencoder(errors).encode
diff --git a/testing/web-platform/tests/tools/third_party/webencodings/webencodings/labels.py b/testing/web-platform/tests/tools/third_party/webencodings/webencodings/labels.py
new file mode 100644
index 0000000000..29cbf91ef7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/webencodings/webencodings/labels.py
@@ -0,0 +1,231 @@
+"""
+
+ webencodings.labels
+ ~~~~~~~~~~~~~~~~~~~
+
+ Map encoding labels to their name.
+
+ :copyright: Copyright 2012 by Simon Sapin
+ :license: BSD, see LICENSE for details.
+
+"""
+
+# XXX Do not edit!
+# This file is automatically generated by mklabels.py
+
+LABELS = {
+ 'unicode-1-1-utf-8': 'utf-8',
+ 'utf-8': 'utf-8',
+ 'utf8': 'utf-8',
+ '866': 'ibm866',
+ 'cp866': 'ibm866',
+ 'csibm866': 'ibm866',
+ 'ibm866': 'ibm866',
+ 'csisolatin2': 'iso-8859-2',
+ 'iso-8859-2': 'iso-8859-2',
+ 'iso-ir-101': 'iso-8859-2',
+ 'iso8859-2': 'iso-8859-2',
+ 'iso88592': 'iso-8859-2',
+ 'iso_8859-2': 'iso-8859-2',
+ 'iso_8859-2:1987': 'iso-8859-2',
+ 'l2': 'iso-8859-2',
+ 'latin2': 'iso-8859-2',
+ 'csisolatin3': 'iso-8859-3',
+ 'iso-8859-3': 'iso-8859-3',
+ 'iso-ir-109': 'iso-8859-3',
+ 'iso8859-3': 'iso-8859-3',
+ 'iso88593': 'iso-8859-3',
+ 'iso_8859-3': 'iso-8859-3',
+ 'iso_8859-3:1988': 'iso-8859-3',
+ 'l3': 'iso-8859-3',
+ 'latin3': 'iso-8859-3',
+ 'csisolatin4': 'iso-8859-4',
+ 'iso-8859-4': 'iso-8859-4',
+ 'iso-ir-110': 'iso-8859-4',
+ 'iso8859-4': 'iso-8859-4',
+ 'iso88594': 'iso-8859-4',
+ 'iso_8859-4': 'iso-8859-4',
+ 'iso_8859-4:1988': 'iso-8859-4',
+ 'l4': 'iso-8859-4',
+ 'latin4': 'iso-8859-4',
+ 'csisolatincyrillic': 'iso-8859-5',
+ 'cyrillic': 'iso-8859-5',
+ 'iso-8859-5': 'iso-8859-5',
+ 'iso-ir-144': 'iso-8859-5',
+ 'iso8859-5': 'iso-8859-5',
+ 'iso88595': 'iso-8859-5',
+ 'iso_8859-5': 'iso-8859-5',
+ 'iso_8859-5:1988': 'iso-8859-5',
+ 'arabic': 'iso-8859-6',
+ 'asmo-708': 'iso-8859-6',
+ 'csiso88596e': 'iso-8859-6',
+ 'csiso88596i': 'iso-8859-6',
+ 'csisolatinarabic': 'iso-8859-6',
+ 'ecma-114': 'iso-8859-6',
+ 'iso-8859-6': 'iso-8859-6',
+ 'iso-8859-6-e': 'iso-8859-6',
+ 'iso-8859-6-i': 'iso-8859-6',
+ 'iso-ir-127': 'iso-8859-6',
+ 'iso8859-6': 'iso-8859-6',
+ 'iso88596': 'iso-8859-6',
+ 'iso_8859-6': 'iso-8859-6',
+ 'iso_8859-6:1987': 'iso-8859-6',
+ 'csisolatingreek': 'iso-8859-7',
+ 'ecma-118': 'iso-8859-7',
+ 'elot_928': 'iso-8859-7',
+ 'greek': 'iso-8859-7',
+ 'greek8': 'iso-8859-7',
+ 'iso-8859-7': 'iso-8859-7',
+ 'iso-ir-126': 'iso-8859-7',
+ 'iso8859-7': 'iso-8859-7',
+ 'iso88597': 'iso-8859-7',
+ 'iso_8859-7': 'iso-8859-7',
+ 'iso_8859-7:1987': 'iso-8859-7',
+ 'sun_eu_greek': 'iso-8859-7',
+ 'csiso88598e': 'iso-8859-8',
+ 'csisolatinhebrew': 'iso-8859-8',
+ 'hebrew': 'iso-8859-8',
+ 'iso-8859-8': 'iso-8859-8',
+ 'iso-8859-8-e': 'iso-8859-8',
+ 'iso-ir-138': 'iso-8859-8',
+ 'iso8859-8': 'iso-8859-8',
+ 'iso88598': 'iso-8859-8',
+ 'iso_8859-8': 'iso-8859-8',
+ 'iso_8859-8:1988': 'iso-8859-8',
+ 'visual': 'iso-8859-8',
+ 'csiso88598i': 'iso-8859-8-i',
+ 'iso-8859-8-i': 'iso-8859-8-i',
+ 'logical': 'iso-8859-8-i',
+ 'csisolatin6': 'iso-8859-10',
+ 'iso-8859-10': 'iso-8859-10',
+ 'iso-ir-157': 'iso-8859-10',
+ 'iso8859-10': 'iso-8859-10',
+ 'iso885910': 'iso-8859-10',
+ 'l6': 'iso-8859-10',
+ 'latin6': 'iso-8859-10',
+ 'iso-8859-13': 'iso-8859-13',
+ 'iso8859-13': 'iso-8859-13',
+ 'iso885913': 'iso-8859-13',
+ 'iso-8859-14': 'iso-8859-14',
+ 'iso8859-14': 'iso-8859-14',
+ 'iso885914': 'iso-8859-14',
+ 'csisolatin9': 'iso-8859-15',
+ 'iso-8859-15': 'iso-8859-15',
+ 'iso8859-15': 'iso-8859-15',
+ 'iso885915': 'iso-8859-15',
+ 'iso_8859-15': 'iso-8859-15',
+ 'l9': 'iso-8859-15',
+ 'iso-8859-16': 'iso-8859-16',
+ 'cskoi8r': 'koi8-r',
+ 'koi': 'koi8-r',
+ 'koi8': 'koi8-r',
+ 'koi8-r': 'koi8-r',
+ 'koi8_r': 'koi8-r',
+ 'koi8-u': 'koi8-u',
+ 'csmacintosh': 'macintosh',
+ 'mac': 'macintosh',
+ 'macintosh': 'macintosh',
+ 'x-mac-roman': 'macintosh',
+ 'dos-874': 'windows-874',
+ 'iso-8859-11': 'windows-874',
+ 'iso8859-11': 'windows-874',
+ 'iso885911': 'windows-874',
+ 'tis-620': 'windows-874',
+ 'windows-874': 'windows-874',
+ 'cp1250': 'windows-1250',
+ 'windows-1250': 'windows-1250',
+ 'x-cp1250': 'windows-1250',
+ 'cp1251': 'windows-1251',
+ 'windows-1251': 'windows-1251',
+ 'x-cp1251': 'windows-1251',
+ 'ansi_x3.4-1968': 'windows-1252',
+ 'ascii': 'windows-1252',
+ 'cp1252': 'windows-1252',
+ 'cp819': 'windows-1252',
+ 'csisolatin1': 'windows-1252',
+ 'ibm819': 'windows-1252',
+ 'iso-8859-1': 'windows-1252',
+ 'iso-ir-100': 'windows-1252',
+ 'iso8859-1': 'windows-1252',
+ 'iso88591': 'windows-1252',
+ 'iso_8859-1': 'windows-1252',
+ 'iso_8859-1:1987': 'windows-1252',
+ 'l1': 'windows-1252',
+ 'latin1': 'windows-1252',
+ 'us-ascii': 'windows-1252',
+ 'windows-1252': 'windows-1252',
+ 'x-cp1252': 'windows-1252',
+ 'cp1253': 'windows-1253',
+ 'windows-1253': 'windows-1253',
+ 'x-cp1253': 'windows-1253',
+ 'cp1254': 'windows-1254',
+ 'csisolatin5': 'windows-1254',
+ 'iso-8859-9': 'windows-1254',
+ 'iso-ir-148': 'windows-1254',
+ 'iso8859-9': 'windows-1254',
+ 'iso88599': 'windows-1254',
+ 'iso_8859-9': 'windows-1254',
+ 'iso_8859-9:1989': 'windows-1254',
+ 'l5': 'windows-1254',
+ 'latin5': 'windows-1254',
+ 'windows-1254': 'windows-1254',
+ 'x-cp1254': 'windows-1254',
+ 'cp1255': 'windows-1255',
+ 'windows-1255': 'windows-1255',
+ 'x-cp1255': 'windows-1255',
+ 'cp1256': 'windows-1256',
+ 'windows-1256': 'windows-1256',
+ 'x-cp1256': 'windows-1256',
+ 'cp1257': 'windows-1257',
+ 'windows-1257': 'windows-1257',
+ 'x-cp1257': 'windows-1257',
+ 'cp1258': 'windows-1258',
+ 'windows-1258': 'windows-1258',
+ 'x-cp1258': 'windows-1258',
+ 'x-mac-cyrillic': 'x-mac-cyrillic',
+ 'x-mac-ukrainian': 'x-mac-cyrillic',
+ 'chinese': 'gbk',
+ 'csgb2312': 'gbk',
+ 'csiso58gb231280': 'gbk',
+ 'gb2312': 'gbk',
+ 'gb_2312': 'gbk',
+ 'gb_2312-80': 'gbk',
+ 'gbk': 'gbk',
+ 'iso-ir-58': 'gbk',
+ 'x-gbk': 'gbk',
+ 'gb18030': 'gb18030',
+ 'hz-gb-2312': 'hz-gb-2312',
+ 'big5': 'big5',
+ 'big5-hkscs': 'big5',
+ 'cn-big5': 'big5',
+ 'csbig5': 'big5',
+ 'x-x-big5': 'big5',
+ 'cseucpkdfmtjapanese': 'euc-jp',
+ 'euc-jp': 'euc-jp',
+ 'x-euc-jp': 'euc-jp',
+ 'csiso2022jp': 'iso-2022-jp',
+ 'iso-2022-jp': 'iso-2022-jp',
+ 'csshiftjis': 'shift_jis',
+ 'ms_kanji': 'shift_jis',
+ 'shift-jis': 'shift_jis',
+ 'shift_jis': 'shift_jis',
+ 'sjis': 'shift_jis',
+ 'windows-31j': 'shift_jis',
+ 'x-sjis': 'shift_jis',
+ 'cseuckr': 'euc-kr',
+ 'csksc56011987': 'euc-kr',
+ 'euc-kr': 'euc-kr',
+ 'iso-ir-149': 'euc-kr',
+ 'korean': 'euc-kr',
+ 'ks_c_5601-1987': 'euc-kr',
+ 'ks_c_5601-1989': 'euc-kr',
+ 'ksc5601': 'euc-kr',
+ 'ksc_5601': 'euc-kr',
+ 'windows-949': 'euc-kr',
+ 'csiso2022kr': 'iso-2022-kr',
+ 'iso-2022-kr': 'iso-2022-kr',
+ 'utf-16be': 'utf-16be',
+ 'utf-16': 'utf-16le',
+ 'utf-16le': 'utf-16le',
+ 'x-user-defined': 'x-user-defined',
+}
diff --git a/testing/web-platform/tests/tools/third_party/webencodings/webencodings/mklabels.py b/testing/web-platform/tests/tools/third_party/webencodings/webencodings/mklabels.py
new file mode 100644
index 0000000000..295dc928ba
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/webencodings/webencodings/mklabels.py
@@ -0,0 +1,59 @@
+"""
+
+ webencodings.mklabels
+ ~~~~~~~~~~~~~~~~~~~~~
+
+ Regenarate the webencodings.labels module.
+
+ :copyright: Copyright 2012 by Simon Sapin
+ :license: BSD, see LICENSE for details.
+
+"""
+
+import json
+try:
+ from urllib import urlopen
+except ImportError:
+ from urllib.request import urlopen
+
+
+def assert_lower(string):
+ assert string == string.lower()
+ return string
+
+
+def generate(url):
+ parts = ['''\
+"""
+
+ webencodings.labels
+ ~~~~~~~~~~~~~~~~~~~
+
+ Map encoding labels to their name.
+
+ :copyright: Copyright 2012 by Simon Sapin
+ :license: BSD, see LICENSE for details.
+
+"""
+
+# XXX Do not edit!
+# This file is automatically generated by mklabels.py
+
+LABELS = {
+''']
+ labels = [
+ (repr(assert_lower(label)).lstrip('u'),
+ repr(encoding['name']).lstrip('u'))
+ for category in json.loads(urlopen(url).read().decode('ascii'))
+ for encoding in category['encodings']
+ for label in encoding['labels']]
+ max_len = max(len(label) for label, name in labels)
+ parts.extend(
+ ' %s:%s %s,\n' % (label, ' ' * (max_len - len(label)), name)
+ for label, name in labels)
+ parts.append('}')
+ return ''.join(parts)
+
+
+if __name__ == '__main__':
+ print(generate('http://encoding.spec.whatwg.org/encodings.json'))
diff --git a/testing/web-platform/tests/tools/third_party/webencodings/webencodings/tests.py b/testing/web-platform/tests/tools/third_party/webencodings/webencodings/tests.py
new file mode 100644
index 0000000000..e12c10d033
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/webencodings/webencodings/tests.py
@@ -0,0 +1,153 @@
+# coding: utf-8
+"""
+
+ webencodings.tests
+ ~~~~~~~~~~~~~~~~~~
+
+ A basic test suite for Encoding.
+
+ :copyright: Copyright 2012 by Simon Sapin
+ :license: BSD, see LICENSE for details.
+
+"""
+
+from __future__ import unicode_literals
+
+from . import (lookup, LABELS, decode, encode, iter_decode, iter_encode,
+ IncrementalDecoder, IncrementalEncoder, UTF8)
+
+
+def assert_raises(exception, function, *args, **kwargs):
+ try:
+ function(*args, **kwargs)
+ except exception:
+ return
+ else: # pragma: no cover
+ raise AssertionError('Did not raise %s.' % exception)
+
+
+def test_labels():
+ assert lookup('utf-8').name == 'utf-8'
+ assert lookup('Utf-8').name == 'utf-8'
+ assert lookup('UTF-8').name == 'utf-8'
+ assert lookup('utf8').name == 'utf-8'
+ assert lookup('utf8').name == 'utf-8'
+ assert lookup('utf8 ').name == 'utf-8'
+ assert lookup(' \r\nutf8\t').name == 'utf-8'
+ assert lookup('u8') is None # Python label.
+ assert lookup('utf-8 ') is None # Non-ASCII white space.
+
+ assert lookup('US-ASCII').name == 'windows-1252'
+ assert lookup('iso-8859-1').name == 'windows-1252'
+ assert lookup('latin1').name == 'windows-1252'
+ assert lookup('LATIN1').name == 'windows-1252'
+ assert lookup('latin-1') is None
+ assert lookup('LATİN1') is None # ASCII-only case insensitivity.
+
+
+def test_all_labels():
+ for label in LABELS:
+ assert decode(b'', label) == ('', lookup(label))
+ assert encode('', label) == b''
+ for repeat in [0, 1, 12]:
+ output, _ = iter_decode([b''] * repeat, label)
+ assert list(output) == []
+ assert list(iter_encode([''] * repeat, label)) == []
+ decoder = IncrementalDecoder(label)
+ assert decoder.decode(b'') == ''
+ assert decoder.decode(b'', final=True) == ''
+ encoder = IncrementalEncoder(label)
+ assert encoder.encode('') == b''
+ assert encoder.encode('', final=True) == b''
+ # All encoding names are valid labels too:
+ for name in set(LABELS.values()):
+ assert lookup(name).name == name
+
+
+def test_invalid_label():
+ assert_raises(LookupError, decode, b'\xEF\xBB\xBF\xc3\xa9', 'invalid')
+ assert_raises(LookupError, encode, 'é', 'invalid')
+ assert_raises(LookupError, iter_decode, [], 'invalid')
+ assert_raises(LookupError, iter_encode, [], 'invalid')
+ assert_raises(LookupError, IncrementalDecoder, 'invalid')
+ assert_raises(LookupError, IncrementalEncoder, 'invalid')
+
+
+def test_decode():
+ assert decode(b'\x80', 'latin1') == ('€', lookup('latin1'))
+ assert decode(b'\x80', lookup('latin1')) == ('€', lookup('latin1'))
+ assert decode(b'\xc3\xa9', 'utf8') == ('é', lookup('utf8'))
+ assert decode(b'\xc3\xa9', UTF8) == ('é', lookup('utf8'))
+ assert decode(b'\xc3\xa9', 'ascii') == ('é', lookup('ascii'))
+ assert decode(b'\xEF\xBB\xBF\xc3\xa9', 'ascii') == ('é', lookup('utf8')) # UTF-8 with BOM
+
+ assert decode(b'\xFE\xFF\x00\xe9', 'ascii') == ('é', lookup('utf-16be')) # UTF-16-BE with BOM
+ assert decode(b'\xFF\xFE\xe9\x00', 'ascii') == ('é', lookup('utf-16le')) # UTF-16-LE with BOM
+ assert decode(b'\xFE\xFF\xe9\x00', 'ascii') == ('\ue900', lookup('utf-16be'))
+ assert decode(b'\xFF\xFE\x00\xe9', 'ascii') == ('\ue900', lookup('utf-16le'))
+
+ assert decode(b'\x00\xe9', 'UTF-16BE') == ('é', lookup('utf-16be'))
+ assert decode(b'\xe9\x00', 'UTF-16LE') == ('é', lookup('utf-16le'))
+ assert decode(b'\xe9\x00', 'UTF-16') == ('é', lookup('utf-16le'))
+
+ assert decode(b'\xe9\x00', 'UTF-16BE') == ('\ue900', lookup('utf-16be'))
+ assert decode(b'\x00\xe9', 'UTF-16LE') == ('\ue900', lookup('utf-16le'))
+ assert decode(b'\x00\xe9', 'UTF-16') == ('\ue900', lookup('utf-16le'))
+
+
+def test_encode():
+ assert encode('é', 'latin1') == b'\xe9'
+ assert encode('é', 'utf8') == b'\xc3\xa9'
+ assert encode('é', 'utf8') == b'\xc3\xa9'
+ assert encode('é', 'utf-16') == b'\xe9\x00'
+ assert encode('é', 'utf-16le') == b'\xe9\x00'
+ assert encode('é', 'utf-16be') == b'\x00\xe9'
+
+
+def test_iter_decode():
+ def iter_decode_to_string(input, fallback_encoding):
+ output, _encoding = iter_decode(input, fallback_encoding)
+ return ''.join(output)
+ assert iter_decode_to_string([], 'latin1') == ''
+ assert iter_decode_to_string([b''], 'latin1') == ''
+ assert iter_decode_to_string([b'\xe9'], 'latin1') == 'é'
+ assert iter_decode_to_string([b'hello'], 'latin1') == 'hello'
+ assert iter_decode_to_string([b'he', b'llo'], 'latin1') == 'hello'
+ assert iter_decode_to_string([b'hell', b'o'], 'latin1') == 'hello'
+ assert iter_decode_to_string([b'\xc3\xa9'], 'latin1') == 'é'
+ assert iter_decode_to_string([b'\xEF\xBB\xBF\xc3\xa9'], 'latin1') == 'é'
+ assert iter_decode_to_string([
+ b'\xEF\xBB\xBF', b'\xc3', b'\xa9'], 'latin1') == 'é'
+ assert iter_decode_to_string([
+ b'\xEF\xBB\xBF', b'a', b'\xc3'], 'latin1') == 'a\uFFFD'
+ assert iter_decode_to_string([
+ b'', b'\xEF', b'', b'', b'\xBB\xBF\xc3', b'\xa9'], 'latin1') == 'é'
+ assert iter_decode_to_string([b'\xEF\xBB\xBF'], 'latin1') == ''
+ assert iter_decode_to_string([b'\xEF\xBB'], 'latin1') == 'ï»'
+ assert iter_decode_to_string([b'\xFE\xFF\x00\xe9'], 'latin1') == 'é'
+ assert iter_decode_to_string([b'\xFF\xFE\xe9\x00'], 'latin1') == 'é'
+ assert iter_decode_to_string([
+ b'', b'\xFF', b'', b'', b'\xFE\xe9', b'\x00'], 'latin1') == 'é'
+ assert iter_decode_to_string([
+ b'', b'h\xe9', b'llo'], 'x-user-defined') == 'h\uF7E9llo'
+
+
+def test_iter_encode():
+ assert b''.join(iter_encode([], 'latin1')) == b''
+ assert b''.join(iter_encode([''], 'latin1')) == b''
+ assert b''.join(iter_encode(['é'], 'latin1')) == b'\xe9'
+ assert b''.join(iter_encode(['', 'é', '', ''], 'latin1')) == b'\xe9'
+ assert b''.join(iter_encode(['', 'é', '', ''], 'utf-16')) == b'\xe9\x00'
+ assert b''.join(iter_encode(['', 'é', '', ''], 'utf-16le')) == b'\xe9\x00'
+ assert b''.join(iter_encode(['', 'é', '', ''], 'utf-16be')) == b'\x00\xe9'
+ assert b''.join(iter_encode([
+ '', 'h\uF7E9', '', 'llo'], 'x-user-defined')) == b'h\xe9llo'
+
+
+def test_x_user_defined():
+ encoded = b'2,\x0c\x0b\x1aO\xd9#\xcb\x0f\xc9\xbbt\xcf\xa8\xca'
+ decoded = '2,\x0c\x0b\x1aO\uf7d9#\uf7cb\x0f\uf7c9\uf7bbt\uf7cf\uf7a8\uf7ca'
+ encoded = b'aa'
+ decoded = 'aa'
+ assert decode(encoded, 'x-user-defined') == (decoded, lookup('x-user-defined'))
+ assert encode(decoded, 'x-user-defined') == encoded
diff --git a/testing/web-platform/tests/tools/third_party/webencodings/webencodings/x_user_defined.py b/testing/web-platform/tests/tools/third_party/webencodings/webencodings/x_user_defined.py
new file mode 100644
index 0000000000..d16e326024
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/webencodings/webencodings/x_user_defined.py
@@ -0,0 +1,325 @@
+# coding: utf-8
+"""
+
+ webencodings.x_user_defined
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ An implementation of the x-user-defined encoding.
+
+ :copyright: Copyright 2012 by Simon Sapin
+ :license: BSD, see LICENSE for details.
+
+"""
+
+from __future__ import unicode_literals
+
+import codecs
+
+
+### Codec APIs
+
+class Codec(codecs.Codec):
+
+ def encode(self, input, errors='strict'):
+ return codecs.charmap_encode(input, errors, encoding_table)
+
+ def decode(self, input, errors='strict'):
+ return codecs.charmap_decode(input, errors, decoding_table)
+
+
+class IncrementalEncoder(codecs.IncrementalEncoder):
+ def encode(self, input, final=False):
+ return codecs.charmap_encode(input, self.errors, encoding_table)[0]
+
+
+class IncrementalDecoder(codecs.IncrementalDecoder):
+ def decode(self, input, final=False):
+ return codecs.charmap_decode(input, self.errors, decoding_table)[0]
+
+
+class StreamWriter(Codec, codecs.StreamWriter):
+ pass
+
+
+class StreamReader(Codec, codecs.StreamReader):
+ pass
+
+
+### encodings module API
+
+codec_info = codecs.CodecInfo(
+ name='x-user-defined',
+ encode=Codec().encode,
+ decode=Codec().decode,
+ incrementalencoder=IncrementalEncoder,
+ incrementaldecoder=IncrementalDecoder,
+ streamreader=StreamReader,
+ streamwriter=StreamWriter,
+)
+
+
+### Decoding Table
+
+# Python 3:
+# for c in range(256): print(' %r' % chr(c if c < 128 else c + 0xF700))
+decoding_table = (
+ '\x00'
+ '\x01'
+ '\x02'
+ '\x03'
+ '\x04'
+ '\x05'
+ '\x06'
+ '\x07'
+ '\x08'
+ '\t'
+ '\n'
+ '\x0b'
+ '\x0c'
+ '\r'
+ '\x0e'
+ '\x0f'
+ '\x10'
+ '\x11'
+ '\x12'
+ '\x13'
+ '\x14'
+ '\x15'
+ '\x16'
+ '\x17'
+ '\x18'
+ '\x19'
+ '\x1a'
+ '\x1b'
+ '\x1c'
+ '\x1d'
+ '\x1e'
+ '\x1f'
+ ' '
+ '!'
+ '"'
+ '#'
+ '$'
+ '%'
+ '&'
+ "'"
+ '('
+ ')'
+ '*'
+ '+'
+ ','
+ '-'
+ '.'
+ '/'
+ '0'
+ '1'
+ '2'
+ '3'
+ '4'
+ '5'
+ '6'
+ '7'
+ '8'
+ '9'
+ ':'
+ ';'
+ '<'
+ '='
+ '>'
+ '?'
+ '@'
+ 'A'
+ 'B'
+ 'C'
+ 'D'
+ 'E'
+ 'F'
+ 'G'
+ 'H'
+ 'I'
+ 'J'
+ 'K'
+ 'L'
+ 'M'
+ 'N'
+ 'O'
+ 'P'
+ 'Q'
+ 'R'
+ 'S'
+ 'T'
+ 'U'
+ 'V'
+ 'W'
+ 'X'
+ 'Y'
+ 'Z'
+ '['
+ '\\'
+ ']'
+ '^'
+ '_'
+ '`'
+ 'a'
+ 'b'
+ 'c'
+ 'd'
+ 'e'
+ 'f'
+ 'g'
+ 'h'
+ 'i'
+ 'j'
+ 'k'
+ 'l'
+ 'm'
+ 'n'
+ 'o'
+ 'p'
+ 'q'
+ 'r'
+ 's'
+ 't'
+ 'u'
+ 'v'
+ 'w'
+ 'x'
+ 'y'
+ 'z'
+ '{'
+ '|'
+ '}'
+ '~'
+ '\x7f'
+ '\uf780'
+ '\uf781'
+ '\uf782'
+ '\uf783'
+ '\uf784'
+ '\uf785'
+ '\uf786'
+ '\uf787'
+ '\uf788'
+ '\uf789'
+ '\uf78a'
+ '\uf78b'
+ '\uf78c'
+ '\uf78d'
+ '\uf78e'
+ '\uf78f'
+ '\uf790'
+ '\uf791'
+ '\uf792'
+ '\uf793'
+ '\uf794'
+ '\uf795'
+ '\uf796'
+ '\uf797'
+ '\uf798'
+ '\uf799'
+ '\uf79a'
+ '\uf79b'
+ '\uf79c'
+ '\uf79d'
+ '\uf79e'
+ '\uf79f'
+ '\uf7a0'
+ '\uf7a1'
+ '\uf7a2'
+ '\uf7a3'
+ '\uf7a4'
+ '\uf7a5'
+ '\uf7a6'
+ '\uf7a7'
+ '\uf7a8'
+ '\uf7a9'
+ '\uf7aa'
+ '\uf7ab'
+ '\uf7ac'
+ '\uf7ad'
+ '\uf7ae'
+ '\uf7af'
+ '\uf7b0'
+ '\uf7b1'
+ '\uf7b2'
+ '\uf7b3'
+ '\uf7b4'
+ '\uf7b5'
+ '\uf7b6'
+ '\uf7b7'
+ '\uf7b8'
+ '\uf7b9'
+ '\uf7ba'
+ '\uf7bb'
+ '\uf7bc'
+ '\uf7bd'
+ '\uf7be'
+ '\uf7bf'
+ '\uf7c0'
+ '\uf7c1'
+ '\uf7c2'
+ '\uf7c3'
+ '\uf7c4'
+ '\uf7c5'
+ '\uf7c6'
+ '\uf7c7'
+ '\uf7c8'
+ '\uf7c9'
+ '\uf7ca'
+ '\uf7cb'
+ '\uf7cc'
+ '\uf7cd'
+ '\uf7ce'
+ '\uf7cf'
+ '\uf7d0'
+ '\uf7d1'
+ '\uf7d2'
+ '\uf7d3'
+ '\uf7d4'
+ '\uf7d5'
+ '\uf7d6'
+ '\uf7d7'
+ '\uf7d8'
+ '\uf7d9'
+ '\uf7da'
+ '\uf7db'
+ '\uf7dc'
+ '\uf7dd'
+ '\uf7de'
+ '\uf7df'
+ '\uf7e0'
+ '\uf7e1'
+ '\uf7e2'
+ '\uf7e3'
+ '\uf7e4'
+ '\uf7e5'
+ '\uf7e6'
+ '\uf7e7'
+ '\uf7e8'
+ '\uf7e9'
+ '\uf7ea'
+ '\uf7eb'
+ '\uf7ec'
+ '\uf7ed'
+ '\uf7ee'
+ '\uf7ef'
+ '\uf7f0'
+ '\uf7f1'
+ '\uf7f2'
+ '\uf7f3'
+ '\uf7f4'
+ '\uf7f5'
+ '\uf7f6'
+ '\uf7f7'
+ '\uf7f8'
+ '\uf7f9'
+ '\uf7fa'
+ '\uf7fb'
+ '\uf7fc'
+ '\uf7fd'
+ '\uf7fe'
+ '\uf7ff'
+)
+
+### Encoding table
+encoding_table = codecs.charmap_build(decoding_table)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.appveyor.yml b/testing/web-platform/tests/tools/third_party/websockets/.appveyor.yml
new file mode 100644
index 0000000000..7954ee4be7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.appveyor.yml
@@ -0,0 +1,27 @@
+branches:
+ only:
+ - master
+
+skip_branch_with_pr: true
+
+environment:
+# websockets only works on Python >= 3.6.
+ CIBW_SKIP: cp27-* cp33-* cp34-* cp35-*
+ CIBW_TEST_COMMAND: python -W default -m unittest
+ WEBSOCKETS_TESTS_TIMEOUT_FACTOR: 100
+
+install:
+# Ensure python is Python 3.
+ - set PATH=C:\Python37;%PATH%
+ - cmd: python -m pip install --upgrade cibuildwheel
+# Create file '.cibuildwheel' so that extension build is not optional (c.f. setup.py).
+ - cmd: touch .cibuildwheel
+
+build_script:
+ - cmd: python -m cibuildwheel --output-dir wheelhouse
+# Upload to PyPI on tags
+ - ps: >-
+ if ($env:APPVEYOR_REPO_TAG -eq "true") {
+ Invoke-Expression "python -m pip install twine"
+ Invoke-Expression "python -m twine upload --skip-existing wheelhouse/*.whl"
+ }
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.circleci/config.yml b/testing/web-platform/tests/tools/third_party/websockets/.circleci/config.yml
new file mode 100644
index 0000000000..0877c161ad
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.circleci/config.yml
@@ -0,0 +1,55 @@
+version: 2
+
+jobs:
+ main:
+ docker:
+ - image: circleci/python:3.7
+ steps:
+ # Remove IPv6 entry for localhost in Circle CI containers because it doesn't work anyway.
+ - run: sudo cp /etc/hosts /tmp; sudo sed -i '/::1/d' /tmp/hosts; sudo cp /tmp/hosts /etc
+ - checkout
+ - run: sudo pip install tox codecov
+ - run: tox -e coverage,black,flake8,isort,mypy
+ - run: codecov
+ py36:
+ docker:
+ - image: circleci/python:3.6
+ steps:
+ # Remove IPv6 entry for localhost in Circle CI containers because it doesn't work anyway.
+ - run: sudo cp /etc/hosts /tmp; sudo sed -i '/::1/d' /tmp/hosts; sudo cp /tmp/hosts /etc
+ - checkout
+ - run: sudo pip install tox
+ - run: tox -e py36
+ py37:
+ docker:
+ - image: circleci/python:3.7
+ steps:
+ # Remove IPv6 entry for localhost in Circle CI containers because it doesn't work anyway.
+ - run: sudo cp /etc/hosts /tmp; sudo sed -i '/::1/d' /tmp/hosts; sudo cp /tmp/hosts /etc
+ - checkout
+ - run: sudo pip install tox
+ - run: tox -e py37
+ py38:
+ docker:
+ - image: circleci/python:3.8.0rc1
+ steps:
+ # Remove IPv6 entry for localhost in Circle CI containers because it doesn't work anyway.
+ - run: sudo cp /etc/hosts /tmp; sudo sed -i '/::1/d' /tmp/hosts; sudo cp /tmp/hosts /etc
+ - checkout
+ - run: sudo pip install tox
+ - run: tox -e py38
+
+workflows:
+ version: 2
+ build:
+ jobs:
+ - main
+ - py36:
+ requires:
+ - main
+ - py37:
+ requires:
+ - main
+ - py38:
+ requires:
+ - main
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.github/FUNDING.yml b/testing/web-platform/tests/tools/third_party/websockets/.github/FUNDING.yml
new file mode 100644
index 0000000000..7ae223b3d8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.github/FUNDING.yml
@@ -0,0 +1 @@
+tidelift: "pypi/websockets"
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.gitignore b/testing/web-platform/tests/tools/third_party/websockets/.gitignore
new file mode 100644
index 0000000000..ef0d16520c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.gitignore
@@ -0,0 +1,12 @@
+*.pyc
+*.so
+.coverage
+.mypy_cache
+.tox
+build/
+compliance/reports/
+dist/
+docs/_build/
+htmlcov/
+MANIFEST
+websockets.egg-info/
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.readthedocs.yml b/testing/web-platform/tests/tools/third_party/websockets/.readthedocs.yml
new file mode 100644
index 0000000000..109affab45
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.readthedocs.yml
@@ -0,0 +1,7 @@
+build:
+ image: latest
+
+python:
+ version: 3.7
+
+requirements_file: docs/requirements.txt
diff --git a/testing/web-platform/tests/tools/third_party/websockets/.travis.yml b/testing/web-platform/tests/tools/third_party/websockets/.travis.yml
new file mode 100644
index 0000000000..0306937597
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/.travis.yml
@@ -0,0 +1,36 @@
+env:
+ global:
+ # websockets only works on Python >= 3.6.
+ - CIBW_SKIP="cp27-* cp33-* cp34-* cp35-*"
+ - CIBW_TEST_COMMAND="python3 -W default -m unittest"
+ - WEBSOCKETS_TESTS_TIMEOUT_FACTOR=100
+
+matrix:
+ include:
+ - language: python
+ dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069)
+ sudo: required
+ python: "3.7"
+ services:
+ - docker
+ - os: osx
+ osx_image: xcode8.3
+
+install:
+# Python 3 is needed to run cibuildwheel for websockets.
+ - if [ "${TRAVIS_OS_NAME:-}" == "osx" ]; then
+ brew update;
+ brew upgrade python;
+ fi
+# Install cibuildwheel using pip3 to make sure Python 3 is used.
+ - pip3 install --upgrade cibuildwheel
+# Create file '.cibuildwheel' so that extension build is not optional (c.f. setup.py).
+ - touch .cibuildwheel
+
+script:
+ - cibuildwheel --output-dir wheelhouse
+# Upload to PyPI on tags
+ - if [ "${TRAVIS_TAG:-}" != "" ]; then
+ pip3 install twine;
+ python3 -m twine upload --skip-existing wheelhouse/*;
+ fi
diff --git a/testing/web-platform/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md b/testing/web-platform/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..80f80d51b1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md
@@ -0,0 +1,46 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at aymeric DOT augustin AT fractalideas DOT com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/testing/web-platform/tests/tools/third_party/websockets/LICENSE b/testing/web-platform/tests/tools/third_party/websockets/LICENSE
new file mode 100644
index 0000000000..b2962adba2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) 2013-2019 Aymeric Augustin and contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of websockets nor the names of its contributors may
+ be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/MANIFEST.in b/testing/web-platform/tests/tools/third_party/websockets/MANIFEST.in
new file mode 100644
index 0000000000..1c660b95b1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/MANIFEST.in
@@ -0,0 +1,2 @@
+include LICENSE
+include src/websockets/py.typed
diff --git a/testing/web-platform/tests/tools/third_party/websockets/Makefile b/testing/web-platform/tests/tools/third_party/websockets/Makefile
new file mode 100644
index 0000000000..d9e16fefe3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/Makefile
@@ -0,0 +1,29 @@
+.PHONY: default style test coverage build clean
+
+export PYTHONASYNCIODEBUG=1
+export PYTHONPATH=src
+
+default: coverage style
+
+style:
+ isort --recursive src tests
+ black src tests
+ flake8 src tests
+ mypy --strict src
+
+test:
+ python -W default -m unittest
+
+coverage:
+ python -m coverage erase
+ python -W default -m coverage run -m unittest
+ python -m coverage html
+ python -m coverage report --show-missing --fail-under=100
+
+build:
+ python setup.py build_ext --inplace
+
+clean:
+ find . -name '*.pyc' -o -name '*.so' -delete
+ find . -name __pycache__ -delete
+ rm -rf .coverage build compliance/reports dist docs/_build htmlcov MANIFEST src/websockets.egg-info
diff --git a/testing/web-platform/tests/tools/third_party/websockets/README.rst b/testing/web-platform/tests/tools/third_party/websockets/README.rst
new file mode 100644
index 0000000000..1e15ba1981
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/README.rst
@@ -0,0 +1,154 @@
+.. image:: logo/horizontal.svg
+ :width: 480px
+ :alt: websockets
+
+|rtd| |pypi-v| |pypi-pyversions| |pypi-l| |pypi-wheel| |circleci| |codecov|
+
+.. |rtd| image:: https://readthedocs.org/projects/websockets/badge/?version=latest
+ :target: https://websockets.readthedocs.io/
+
+.. |pypi-v| image:: https://img.shields.io/pypi/v/websockets.svg
+ :target: https://pypi.python.org/pypi/websockets
+
+.. |pypi-pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg
+ :target: https://pypi.python.org/pypi/websockets
+
+.. |pypi-l| image:: https://img.shields.io/pypi/l/websockets.svg
+ :target: https://pypi.python.org/pypi/websockets
+
+.. |pypi-wheel| image:: https://img.shields.io/pypi/wheel/websockets.svg
+ :target: https://pypi.python.org/pypi/websockets
+
+.. |circleci| image:: https://img.shields.io/circleci/project/github/aaugustin/websockets.svg
+ :target: https://circleci.com/gh/aaugustin/websockets
+
+.. |codecov| image:: https://codecov.io/gh/aaugustin/websockets/branch/master/graph/badge.svg
+ :target: https://codecov.io/gh/aaugustin/websockets
+
+What is ``websockets``?
+-----------------------
+
+``websockets`` is a library for building WebSocket servers_ and clients_ in
+Python with a focus on correctness and simplicity.
+
+.. _servers: https://github.com/aaugustin/websockets/blob/master/example/server.py
+.. _clients: https://github.com/aaugustin/websockets/blob/master/example/client.py
+
+Built on top of ``asyncio``, Python's standard asynchronous I/O framework, it
+provides an elegant coroutine-based API.
+
+`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_
+
+Here's how a client sends and receives messages:
+
+.. copy-pasted because GitHub doesn't support the include directive
+
+.. code:: python
+
+ #!/usr/bin/env python
+
+ import asyncio
+ import websockets
+
+ async def hello(uri):
+ async with websockets.connect(uri) as websocket:
+ await websocket.send("Hello world!")
+ await websocket.recv()
+
+ asyncio.get_event_loop().run_until_complete(
+ hello('ws://localhost:8765'))
+
+And here's an echo server:
+
+.. code:: python
+
+ #!/usr/bin/env python
+
+ import asyncio
+ import websockets
+
+ async def echo(websocket, path):
+ async for message in websocket:
+ await websocket.send(message)
+
+ asyncio.get_event_loop().run_until_complete(
+ websockets.serve(echo, 'localhost', 8765))
+ asyncio.get_event_loop().run_forever()
+
+Does that look good?
+
+`Get started with the tutorial! <https://websockets.readthedocs.io/en/stable/intro.html>`_
+
+.. raw:: html
+
+ <hr>
+ <img align="left" height="150" width="150" src="https://raw.githubusercontent.com/aaugustin/websockets/master/logo/tidelift.png">
+ <h3 align="center"><i>websockets for enterprise</i></h3>
+ <p align="center"><i>Available as part of the Tidelift Subscription</i></p>
+ <p align="center"><i>The maintainers of websockets and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. <a href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme">Learn more.</a></i></p>
+ <hr>
+ <p>(If you contribute to <code>websockets</code> and would like to become an official support provider, <a href="https://fractalideas.com/">let me know</a>.)</p>
+
+Why should I use ``websockets``?
+--------------------------------
+
+The development of ``websockets`` is shaped by four principles:
+
+1. **Simplicity**: all you need to understand is ``msg = await ws.recv()`` and
+ ``await ws.send(msg)``; ``websockets`` takes care of managing connections
+ so you can focus on your application.
+
+2. **Robustness**: ``websockets`` is built for production; for example it was
+ the only library to `handle backpressure correctly`_ before the issue
+ became widely known in the Python community.
+
+3. **Quality**: ``websockets`` is heavily tested. Continuous integration fails
+ under 100% branch coverage. Also it passes the industry-standard `Autobahn
+ Testsuite`_.
+
+4. **Performance**: memory use is configurable. An extension written in C
+ accelerates expensive operations. It's pre-compiled for Linux, macOS and
+ Windows and packaged in the wheel format for each system and Python version.
+
+Documentation is a first class concern in the project. Head over to `Read the
+Docs`_ and see for yourself.
+
+.. _Read the Docs: https://websockets.readthedocs.io/
+.. _handle backpressure correctly: https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/#websocket-servers
+.. _Autobahn Testsuite: https://github.com/aaugustin/websockets/blob/master/compliance/README.rst
+
+Why shouldn't I use ``websockets``?
+-----------------------------------
+
+* If you prefer callbacks over coroutines: ``websockets`` was created to
+ provide the best coroutine-based API to manage WebSocket connections in
+ Python. Pick another library for a callback-based API.
+* If you're looking for a mixed HTTP / WebSocket library: ``websockets`` aims
+ at being an excellent implementation of :rfc:`6455`: The WebSocket Protocol
+ and :rfc:`7692`: Compression Extensions for WebSocket. Its support for HTTP
+ is minimal — just enough for a HTTP health check.
+* If you want to use Python 2: ``websockets`` builds upon ``asyncio`` which
+ only works on Python 3. ``websockets`` requires Python ≥ 3.6.1.
+
+What else?
+----------
+
+Bug reports, patches and suggestions are welcome!
+
+To report a security vulnerability, please use the `Tidelift security
+contact`_. Tidelift will coordinate the fix and disclosure.
+
+.. _Tidelift security contact: https://tidelift.com/security
+
+For anything else, please open an issue_ or send a `pull request`_.
+
+.. _issue: https://github.com/aaugustin/websockets/issues/new
+.. _pull request: https://github.com/aaugustin/websockets/compare/
+
+Participants must uphold the `Contributor Covenant code of conduct`_.
+
+.. _Contributor Covenant code of conduct: https://github.com/aaugustin/websockets/blob/master/CODE_OF_CONDUCT.md
+
+``websockets`` is released under the `BSD license`_.
+
+.. _BSD license: https://github.com/aaugustin/websockets/blob/master/LICENSE
diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/README.rst b/testing/web-platform/tests/tools/third_party/websockets/compliance/README.rst
new file mode 100644
index 0000000000..8570f9176d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/README.rst
@@ -0,0 +1,50 @@
+Autobahn Testsuite
+==================
+
+General information and installation instructions are available at
+https://github.com/crossbario/autobahn-testsuite.
+
+To improve performance, you should compile the C extension first::
+
+ $ python setup.py build_ext --inplace
+
+Running the test suite
+----------------------
+
+All commands below must be run from the directory containing this file.
+
+To test the server::
+
+ $ PYTHONPATH=.. python test_server.py
+ $ wstest -m fuzzingclient
+
+To test the client::
+
+ $ wstest -m fuzzingserver
+ $ PYTHONPATH=.. python test_client.py
+
+Run the first command in a shell. Run the second command in another shell.
+It should take about ten minutes to complete — wstest is the bottleneck.
+Then kill the first one with Ctrl-C.
+
+The test client or server shouldn't display any exceptions. The results are
+stored in reports/clients/index.html.
+
+Note that the Autobahn software only supports Python 2, while ``websockets``
+only supports Python 3; you need two different environments.
+
+Conformance notes
+-----------------
+
+Some test cases are more strict than the RFC. Given the implementation of the
+library and the test echo client or server, ``websockets`` gets a "Non-Strict"
+in these cases.
+
+In 3.2, 3.3, 4.1.3, 4.1.4, 4.2.3, 4.2.4, and 5.15 ``websockets`` notices the
+protocol error and closes the connection before it has had a chance to echo
+the previous frame.
+
+In 6.4.3 and 6.4.4, even though it uses an incremental decoder, ``websockets``
+doesn't notice the invalid utf-8 fast enough to get a "Strict" pass. These
+tests are more strict than the RFC.
+
diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingclient.json b/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingclient.json
new file mode 100644
index 0000000000..202ff49a03
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingclient.json
@@ -0,0 +1,11 @@
+
+{
+ "options": {"failByDrop": false},
+ "outdir": "./reports/servers",
+
+ "servers": [{"agent": "websockets", "url": "ws://localhost:8642", "options": {"version": 18}}],
+
+ "cases": ["*"],
+ "exclude-cases": [],
+ "exclude-agent-cases": {}
+}
diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingserver.json b/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingserver.json
new file mode 100644
index 0000000000..1bdb42723e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/fuzzingserver.json
@@ -0,0 +1,12 @@
+
+{
+ "url": "ws://localhost:8642",
+
+ "options": {"failByDrop": false},
+ "outdir": "./reports/clients",
+ "webport": 8080,
+
+ "cases": ["*"],
+ "exclude-cases": [],
+ "exclude-agent-cases": {}
+}
diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/test_client.py b/testing/web-platform/tests/tools/third_party/websockets/compliance/test_client.py
new file mode 100644
index 0000000000..5fd0f4b4fb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/test_client.py
@@ -0,0 +1,49 @@
+import json
+import logging
+import urllib.parse
+
+import asyncio
+import websockets
+
+
+logging.basicConfig(level=logging.WARNING)
+
+# Uncomment this line to make only websockets more verbose.
+# logging.getLogger('websockets').setLevel(logging.DEBUG)
+
+
+SERVER = "ws://127.0.0.1:8642"
+AGENT = "websockets"
+
+
+async def get_case_count(server):
+ uri = f"{server}/getCaseCount"
+ async with websockets.connect(uri) as ws:
+ msg = ws.recv()
+ return json.loads(msg)
+
+
+async def run_case(server, case, agent):
+ uri = f"{server}/runCase?case={case}&agent={agent}"
+ async with websockets.connect(uri, max_size=2 ** 25, max_queue=1) as ws:
+ async for msg in ws:
+ await ws.send(msg)
+
+
+async def update_reports(server, agent):
+ uri = f"{server}/updateReports?agent={agent}"
+ async with websockets.connect(uri):
+ pass
+
+
+async def run_tests(server, agent):
+ cases = await get_case_count(server)
+ for case in range(1, cases + 1):
+ print(f"Running test case {case} out of {cases}", end="\r")
+ await run_case(server, case, agent)
+ print(f"Ran {cases} test cases ")
+ await update_reports(server, agent)
+
+
+main = run_tests(SERVER, urllib.parse.quote(AGENT))
+asyncio.get_event_loop().run_until_complete(main)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/compliance/test_server.py b/testing/web-platform/tests/tools/third_party/websockets/compliance/test_server.py
new file mode 100644
index 0000000000..8020f68d35
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/compliance/test_server.py
@@ -0,0 +1,27 @@
+import logging
+
+import asyncio
+import websockets
+
+
+logging.basicConfig(level=logging.WARNING)
+
+# Uncomment this line to make only websockets more verbose.
+# logging.getLogger('websockets').setLevel(logging.DEBUG)
+
+
+HOST, PORT = "127.0.0.1", 8642
+
+
+async def echo(ws, path):
+ async for msg in ws:
+ await ws.send(msg)
+
+
+start_server = websockets.serve(echo, HOST, PORT, max_size=2 ** 25, max_queue=1)
+
+try:
+ asyncio.get_event_loop().run_until_complete(start_server)
+ asyncio.get_event_loop().run_forever()
+except KeyboardInterrupt:
+ pass
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile b/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile
new file mode 100644
index 0000000000..bb25aa49d0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile
@@ -0,0 +1,160 @@
+# 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) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+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 " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @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)"
+ @echo " spelling to check for typos in documentation"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+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/websockets.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/websockets.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/websockets"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/websockets"
+ @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."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+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."
+
+spelling:
+ $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling
+ @echo
+ @echo "Check finished. Wrong words can be found in " \
+ "$(BUILDDIR)/spelling/output.txt."
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png b/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png
new file mode 100644
index 0000000000..317dc4d985
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/_static/websockets.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/_static/websockets.svg
new file mode 100644
index 0000000000..b07fb22387
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/_static/websockets.svg
@@ -0,0 +1,31 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="480" height="320" viewBox="0 0 480 320">
+ <linearGradient id="w" x1="0.2333" y1="0" x2="0.5889" y2="0.5333">
+ <stop offset="0%" stop-color="#ffe873" />
+ <stop offset="100%" stop-color="#ffd43b" />
+ </linearGradient>
+ <linearGradient id="s" x1="0.2333" y1="0" x2="0.5889" y2="0.5333">
+ <stop offset="0%" stop-color="#5a9fd4" />
+ <stop offset="100%" stop-color="#306998" />
+ </linearGradient>
+ <g>
+ <path fill="url(#w)" d="m 263.40708,146.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.04076,-35.919454 c -3.43568,-3.42217 -7.33248,-5.347474 -11.58962,-5.723468 -2.22981,-0.198219 -4.47388,0.03111 -6.64036,0.675545 -3.24213,0.944875 -6.13552,2.664848 -8.59366,5.116366 -3.83437,3.819499 -5.86349,8.414979 -5.87598,13.287801 -0.0607,4.95281 1.95153,9.60074 5.8082,13.44424 l 55.62289,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 160.50255,128.2618 c -5.21417,-5.19459 -11.7029,-6.98745 -18.22998,-5.04881 -3.2457,0.9431 -6.13553,2.66307 -8.59545,5.11459 -3.83437,3.82127 -5.86527,8.41676 -5.87597,13.28957 -0.0562,4.95281 1.95152,9.60252 5.80641,13.4478 l 58.10689,57.90577 c 8.31984,8.29143 19.34042,11.9376 32.74331,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" />
+ <path fill="url(#s)" d="m 308.76038,138.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 259.37292,69.225372 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384848 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.515442 5.26947,-18.272611 -1.51003,-25.028952 L 299.00456,29.727312 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.626122 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" />
+ <path fill="#ffffff" d="m 327.48093,85.181572 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" />
+ </g>
+ <g>
+ <g fill="#ffd43b">
+ <path d="m 25.719398,284.91839 c 0,2.59075 0.912299,4.79875 2.736898,6.62269 1.824599,1.82657 4.033255,2.73821 6.625313,2.73821 2.591402,0 4.800058,-0.91164 6.624002,-2.73821 1.825254,-1.82394 2.738209,-4.03194 2.738209,-6.62269 v -21.77984 c 0,-1.32126 0.475811,-2.45901 1.42809,-3.40998 0.952278,-0.95359 2.089375,-1.43006 3.411947,-1.43006 h 0.0793 c 1.348132,0 2.471467,0.47647 3.371969,1.43006 0.952278,0.95097 1.428745,2.08938 1.428745,3.40998 v 21.77984 c 0,2.59075 0.912299,4.79875 2.738209,6.62269 1.823944,1.82657 4.031289,2.73821 6.624002,2.73821 2.618274,0 4.839382,-0.91164 6.663981,-2.73821 1.825254,-1.82394 2.738209,-4.03194 2.738209,-6.62269 v -21.77984 c 0,-1.32126 0.475156,-2.45901 1.42809,-3.40998 0.897881,-0.95359 2.022526,-1.43006 3.371969,-1.43006 h 0.07865 c 1.323228,0 2.460325,0.47647 3.411948,1.43006 0.926062,0.95097 1.388766,2.08938 1.388766,3.40998 v 21.77984 c 0,5.26211 -1.865233,9.75807 -5.593077,13.48657 -3.729156,3.7285 -8.22577,5.59373 -13.487876,5.59373 -6.294998,0 -11.028207,-2.08807 -14.202904,-6.26747 -3.199602,4.1794 -7.94723,6.26747 -14.240916,6.26747 -5.262763,0 -9.759377,-1.86523 -13.487876,-5.59373 C 17.866544,294.67646 16,290.18115 16,284.91839 v -21.77984 c 0,-1.32126 0.476467,-2.45901 1.428745,-3.40998 0.951623,-0.95359 2.075612,-1.43006 3.371969,-1.43006 h 0.11928 c 1.295702,0 2.419036,0.47647 3.372625,1.43006 0.950967,0.95097 1.427434,2.08938 1.427434,3.40998 v 21.77984 z" />
+ <path d="m 132.94801,271.6291 c 0.31786,0.66063 0.47712,1.33371 0.47712,2.02252 0,0.55577 -0.10551,1.11089 -0.3172,1.66665 -0.45026,1.24262 -1.29636,2.14181 -2.53898,2.69692 -3.70293,1.66665 -8.56853,3.8622 -14.59875,6.58599 -7.48453,3.38442 -11.87497,5.38139 -13.17067,5.9909 2.00942,2.53832 5.14414,3.80715 9.40219,3.80715 2.82931,0 5.39515,-0.83234 7.69556,-2.499 2.24798,-1.63977 3.82222,-3.75537 4.72141,-6.34808 0.76746,-2.16868 2.30107,-3.25269 4.60148,-3.25269 1.63912,0 2.94859,0.68881 3.92708,2.06185 0.6082,0.84742 0.9123,1.7335 0.9123,2.65891 0,0.55577 -0.10552,1.12399 -0.31655,1.70532 -1.56048,4.52348 -4.29869,8.17334 -8.21135,10.95087 -3.96706,2.88108 -8.41059,4.32293 -13.32993,4.32293 -6.29434,0 -11.67639,-2.23356 -16.145474,-6.70395 -4.469743,-4.46975 -6.704615,-9.85114 -6.704615,-16.14679 0,-6.29434 2.234872,-11.67507 6.704615,-16.14678 4.468434,-4.46843 9.851134,-6.70396 16.145474,-6.70396 4.54773,0 8.70027,1.24392 12.45629,3.7285 3.72785,2.43607 6.49162,5.63437 8.29,9.60274 z m -20.74695,-3.5332 c -3.64985,0 -6.7577,1.28391 -9.32289,3.84909 -2.53897,2.5665 -3.808452,5.67435 -3.808452,9.32289 v 0.27789 l 22.175692,-9.95731 c -1.95633,-2.32597 -4.97177,-3.49256 -9.04435,-3.49256 z" />
+ <path d="m 146.11999,242.03442 c 1.2957,0 2.41904,0.46336 3.37197,1.38876 0.95228,0.95097 1.42874,2.08938 1.42874,3.4113 v 15.4311 c 2.98792,-2.64318 7.36525,-3.96707 13.13004,-3.96707 6.29434,0 11.67638,2.23488 16.14613,6.70396 4.46908,4.47106 6.70461,9.85245 6.70461,16.14679 0,6.29499 -2.23553,11.67638 -6.70461,16.14678 -4.46909,4.4704 -9.85113,6.70396 -16.14613,6.70396 -6.295,0 -11.66262,-2.22111 -16.10549,-6.66529 -4.4704,-4.41469 -6.71838,-9.77052 -6.7446,-16.06617 v -34.43341 c 0,-1.32257 0.47647,-2.46032 1.42875,-3.41129 0.95162,-0.92541 2.07561,-1.38877 3.37197,-1.38877 h 0.11862 z m 17.93009,26.06148 c -3.64919,0 -6.75704,1.28391 -9.32288,3.84909 -2.53767,2.5665 -3.80781,5.67435 -3.80781,9.32289 0,3.62364 1.27014,6.71772 3.80781,9.28291 2.56584,2.56519 5.67303,3.84778 9.32288,3.84778 3.62364,0 6.71773,-1.28259 9.28357,-3.84778 2.56387,-2.56519 3.84712,-5.65927 3.84712,-9.28291 0,-3.64788 -1.28325,-6.75639 -3.84712,-9.32289 -2.56584,-2.56518 -5.65927,-3.84909 -9.28357,-3.84909 z" />
+ </g>
+ <g fill="#306998">
+ <path d="m 205.94246,268.01922 c -1.16397,0 -2.14247,0.39586 -2.93548,1.18888 -0.79368,0.82054 -1.19019,1.79838 -1.19019,2.93548 0,1.58735 0.76681,2.77753 2.30172,3.56989 0.52825,0.29165 2.7369,0.95228 6.62466,1.98386 3.14717,0.89985 5.48691,2.07627 7.02051,3.53057 2.19621,2.09003 3.29267,5.06549 3.29267,8.92704 0,3.80714 -1.34879,7.0736 -4.04571,9.79739 -2.72444,2.69823 -5.9909,4.04636 -9.7987,4.04636 h -10.35381 c -1.29701,0 -2.41969,-0.47516 -3.37262,-1.42875 -0.95228,-0.89853 -1.42875,-2.02252 -1.42875,-3.37065 v -0.0806 c 0,-1.32126 0.47647,-2.45901 1.42875,-3.41129 0.95227,-0.95228 2.07561,-1.42874 3.37262,-1.42874 h 10.75032 c 1.16331,0 2.14246,-0.39586 2.93548,-1.18888 0.79368,-0.79367 1.19019,-1.77151 1.19019,-2.93548 0,-1.45561 -0.7537,-2.55339 -2.26044,-3.29201 -0.3965,-0.18678 -2.61892,-0.84742 -6.66529,-1.98386 -3.14782,-0.9254 -5.48887,-2.14377 -7.02247,-3.65051 -2.19555,-2.1418 -3.29202,-5.17035 -3.29202,-9.08432 0,-3.80846 1.34945,-7.06049 4.04702,-9.75807 2.72314,-2.72379 5.99024,-4.087 9.79805,-4.087 h 7.2997 c 1.32192,0 2.45967,0.47647 3.41195,1.43006 0.95162,0.95097 1.42809,2.08938 1.42809,3.40998 v 0.0793 c 0,1.34945 -0.47647,2.47409 -1.42809,3.37263 -0.95228,0.95097 -2.09003,1.42874 -3.41195,1.42874 z" />
+ <path d="m 249.06434,258.29851 c 6.29434,0 11.67573,2.23488 16.14612,6.70396 4.46909,4.47106 6.70396,9.85245 6.70396,16.14679 0,6.29499 -2.23487,11.67638 -6.70396,16.14678 -4.46974,4.46974 -9.85178,6.70396 -16.14612,6.70396 -6.29435,0 -11.67639,-2.23356 -16.14548,-6.70396 -4.46974,-4.46974 -6.70461,-9.85113 -6.70461,-16.14678 0,-6.29434 2.23487,-11.67508 6.70461,-16.14679 4.46909,-4.46908 9.85113,-6.70396 16.14548,-6.70396 z m 0,9.79739 c -3.64986,0 -6.7577,1.28391 -9.32289,3.84909 -2.53963,2.5665 -3.80911,5.67435 -3.80911,9.32289 0,3.62364 1.26948,6.71772 3.80911,9.28291 2.56519,2.56519 5.67238,3.84778 9.32289,3.84778 3.62298,0 6.71706,-1.28259 9.28291,-3.84778 2.56518,-2.56519 3.84778,-5.65927 3.84778,-9.28291 0,-3.64788 -1.2826,-6.75639 -3.84778,-9.32289 -2.56585,-2.56518 -5.65928,-3.84909 -9.28291,-3.84909 z" />
+ <path d="m 307.22146,259.37007 c 2.24864,0.71438 3.37263,2.24798 3.37263,4.60148 v 0.19989 c 0,1.6116 -0.64884,2.89419 -1.94454,3.84778 -0.89919,0.63376 -1.82525,0.95097 -2.77622,0.95097 -0.50334,0 -1.01913,-0.0793 -1.54737,-0.23791 -1.29636,-0.42272 -2.63204,-0.63638 -4.00638,-0.63638 -3.64986,0 -6.75836,1.28391 -9.32289,3.84909 -2.53963,2.5665 -3.80846,5.67435 -3.80846,9.32289 0,3.62364 1.26883,6.71772 3.80846,9.28291 2.56453,2.56519 5.67238,3.84778 9.32289,3.84778 1.375,0 2.71068,-0.21103 4.00638,-0.63507 0.50203,-0.1586 1.00471,-0.2379 1.50739,-0.2379 0.97718,0 1.91767,0.31851 2.81686,0.95358 1.2957,0.95097 1.94453,2.24798 1.94453,3.88776 0,2.32728 -1.12464,3.86089 -3.37262,4.60148 -2.22111,0.6875 -4.52152,1.03027 -6.90189,1.03027 -6.29434,0 -11.67638,-2.23356 -16.14678,-6.70396 -4.46843,-4.46974 -6.70396,-9.85113 -6.70396,-16.14678 0,-6.29435 2.23487,-11.67508 6.70396,-16.14679 4.46974,-4.46843 9.85178,-6.70396 16.14678,-6.70396 2.37906,0.001 4.68012,0.35981 6.90123,1.07287 z" />
+ <path d="m 322.25671,242.03442 c 1.29504,0 2.41903,0.46336 3.37262,1.38876 0.95163,0.95097 1.42809,2.08938 1.42809,3.4113 v 27.49154 h 1.50739 c 3.38508,0 6.33301,-1.12399 8.84708,-3.37263 2.45901,-2.24798 3.86023,-5.0242 4.20431,-8.33063 0.15861,-1.24261 0.68816,-2.26174 1.58735,-3.0541 0.89854,-0.84611 1.96944,-1.27015 3.21271,-1.27015 h 0.11863 c 1.40252,0 2.5796,0.53021 3.53122,1.58735 0.84676,0.92541 1.26949,1.99697 1.26949,3.21271 0,0.15861 -0.0138,0.33163 -0.0393,0.51579 -0.63507,6.63842 -3.17405,11.61019 -7.61692,14.91531 2.32663,1.43006 4.46909,3.84909 6.42739,7.26039 2.03563,3.51746 3.05476,7.31412 3.05476,11.38473 v 2.02515 c 0,1.34813 -0.47712,2.47147 -1.42809,3.37066 -0.95359,0.95359 -2.07692,1.42874 -3.37263,1.42874 h -0.11928 c -1.29635,0 -2.41969,-0.47515 -3.37196,-1.42874 -0.95228,-0.89854 -1.42809,-2.02253 -1.42809,-3.37066 v -2.02515 c -0.0275,-3.59414 -1.31012,-6.67708 -3.84844,-9.24358 -2.56584,-2.53832 -5.66058,-3.80715 -9.28291,-3.80715 h -3.25269 v 15.07523 c 0,1.34813 -0.47646,2.47146 -1.42809,3.37065 -0.95293,0.95359 -2.07758,1.42875 -3.37262,1.42875 h -0.12059 c -1.2957,0 -2.41838,-0.47516 -3.37132,-1.42875 -0.95162,-0.89853 -1.42809,-2.02252 -1.42809,-3.37065 v -52.36547 c 0,-1.32257 0.47647,-2.46032 1.42809,-3.41129 0.95228,-0.92541 2.07562,-1.38877 3.37132,-1.38877 h 0.12059 z" />
+ <path d="m 402.31164,271.6291 c 0.31721,0.66063 0.47581,1.33371 0.47581,2.02252 0,0.55577 -0.10617,1.11089 -0.31655,1.66665 -0.45025,1.24262 -1.29635,2.14181 -2.53897,2.69692 -3.70294,1.66665 -8.56919,3.8622 -14.59876,6.58599 -7.48452,3.38442 -11.87496,5.38139 -13.17067,5.9909 2.00877,2.53832 5.14349,3.80715 9.40219,3.80715 2.82866,0 5.3945,-0.83234 7.69622,-2.499 2.24732,-1.63977 3.82091,-3.75537 4.7201,-6.34808 0.76681,-2.16868 2.30172,-3.25269 4.60148,-3.25269 1.63978,0 2.94924,0.68881 3.92839,2.06185 0.60689,0.84742 0.91165,1.7335 0.91165,2.65891 0,0.55577 -0.10552,1.12399 -0.31721,1.70532 -1.56048,4.52348 -4.29738,8.17334 -8.21135,10.95087 -3.96706,2.88108 -8.40994,4.32293 -13.32928,4.32293 -6.29434,0 -11.67638,-2.23356 -16.14547,-6.70395 -4.46974,-4.46975 -6.70461,-9.85114 -6.70461,-16.14679 0,-6.29434 2.23487,-11.67507 6.70461,-16.14678 4.46843,-4.46843 9.85113,-6.70396 16.14547,-6.70396 4.54774,0 8.70093,1.24392 12.4563,3.7285 3.7285,2.43607 6.49161,5.63437 8.29065,9.60274 z m -20.7476,-3.5332 c -3.6492,0 -6.7577,1.28391 -9.32289,3.84909 -2.53897,2.5665 -3.80846,5.67435 -3.80846,9.32289 v 0.27789 l 22.1757,-9.95731 c -1.95699,-2.32597 -4.97177,-3.49256 -9.04435,-3.49256 z" />
+ <path d="m 415.48166,242.03442 c 1.2957,0 2.41969,0.46336 3.37262,1.38876 0.95162,0.95097 1.42809,2.08938 1.42809,3.4113 v 11.46403 h 5.95092 c 1.2957,0 2.41903,0.47647 3.37262,1.43006 0.95163,0.95097 1.42678,2.08938 1.42678,3.40998 v 0.0793 c 0,1.34945 -0.47515,2.47409 -1.42678,3.37263 -0.95293,0.95097 -2.07692,1.42874 -3.37262,1.42874 h -5.95092 v 23.52252 c 0,0.76811 0.26347,1.41695 0.79367,1.94453 0.5289,0.53021 1.19019,0.79368 1.98321,0.79368 h 3.17404 c 1.2957,0 2.41903,0.47646 3.37262,1.42874 0.95163,0.95228 1.42678,2.09003 1.42678,3.41129 v 0.0806 c 0,1.34813 -0.47515,2.47146 -1.42678,3.37065 C 428.65298,303.52484 427.52899,304 426.23329,304 h -3.17404 c -3.43817,0 -6.38675,-1.21574 -8.84642,-3.6492 -2.43411,-2.45901 -3.6492,-5.39515 -3.6492,-8.80775 v -44.70726 c 0,-1.32258 0.47581,-2.46033 1.42809,-3.4113 0.95228,-0.9254 2.07627,-1.38876 3.37197,-1.38876 h 0.11797 z" />
+ <path d="m 448.88545,268.01922 c -1.16397,0 -2.14246,0.39586 -2.93548,1.18888 -0.79368,0.82054 -1.19019,1.79838 -1.19019,2.93548 0,1.58735 0.76681,2.77753 2.30042,3.56989 0.5302,0.29165 2.7382,0.95228 6.62596,1.98386 3.14652,0.89985 5.48691,2.07627 7.02117,3.53057 2.19489,2.09003 3.29267,5.06549 3.29267,8.92704 0,3.80714 -1.34945,7.0736 -4.04637,9.79739 -2.72379,2.69823 -5.99089,4.04636 -9.79869,4.04636 h -10.35382 c -1.29635,0 -2.41969,-0.47516 -3.37262,-1.42875 -0.95228,-0.89853 -1.42744,-2.02252 -1.42744,-3.37065 v -0.0806 c 0,-1.32126 0.47516,-2.45901 1.42744,-3.41129 0.95228,-0.95228 2.07627,-1.42874 3.37262,-1.42874 h 10.75032 c 1.16332,0 2.14312,-0.39586 2.93549,-1.18888 0.79367,-0.79367 1.19018,-1.77151 1.19018,-2.93548 0,-1.45561 -0.7537,-2.55339 -2.26043,-3.29201 -0.39782,-0.18678 -2.61893,-0.84742 -6.66529,-1.98386 -3.14783,-0.9254 -5.48887,-2.14377 -7.02248,-3.65051 -2.19555,-2.1418 -3.29201,-5.17035 -3.29201,-9.08432 0,-3.80846 1.34944,-7.06049 4.04701,-9.75807 2.72314,-2.72379 5.99025,-4.087 9.7987,-4.087 h 7.29906 c 1.32322,0 2.45967,0.47647 3.41129,1.43006 0.95228,0.95097 1.42809,2.08938 1.42809,3.40998 v 0.0793 c 0,1.34945 -0.47581,2.47409 -1.42809,3.37263 -0.95162,0.95097 -2.08872,1.42874 -3.41129,1.42874 z" />
+ </g>
+ </g>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/api.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/api.rst
new file mode 100644
index 0000000000..d265a91c2c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/api.rst
@@ -0,0 +1,152 @@
+API
+===
+
+Design
+------
+
+``websockets`` provides complete client and server implementations, as shown
+in the :doc:`getting started guide <intro>`. These functions are built on top
+of low-level APIs reflecting the two phases of the WebSocket protocol:
+
+1. An opening handshake, in the form of an HTTP Upgrade request;
+
+2. Data transfer, as framed messages, ending with a closing handshake.
+
+The first phase is designed to integrate with existing HTTP software.
+``websockets`` provides a minimal implementation to build, parse and validate
+HTTP requests and responses.
+
+The second phase is the core of the WebSocket protocol. ``websockets``
+provides a complete implementation on top of ``asyncio`` with a simple API.
+
+For convenience, public APIs can be imported directly from the
+:mod:`websockets` package, unless noted otherwise. Anything that isn't listed
+in this document is a private API.
+
+High-level
+----------
+
+Server
+......
+
+.. automodule:: websockets.server
+
+ .. autofunction:: serve(ws_handler, host=None, port=None, *, create_protocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, loop=None, compression='deflate', origins=None, extensions=None, subprotocols=None, extra_headers=None, process_request=None, select_subprotocol=None, **kwds)
+ :async:
+
+ .. autofunction:: unix_serve(ws_handler, path, *, create_protocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, loop=None, compression='deflate', origins=None, extensions=None, subprotocols=None, extra_headers=None, process_request=None, select_subprotocol=None, **kwds)
+ :async:
+
+
+ .. autoclass:: WebSocketServer
+
+ .. automethod:: close
+ .. automethod:: wait_closed
+ .. autoattribute:: sockets
+
+ .. autoclass:: WebSocketServerProtocol(ws_handler, ws_server, *, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, loop=None, origins=None, extensions=None, subprotocols=None, extra_headers=None, process_request=None, select_subprotocol=None)
+
+ .. automethod:: handshake
+ .. automethod:: process_request
+ .. automethod:: select_subprotocol
+
+Client
+......
+
+.. automodule:: websockets.client
+
+ .. autofunction:: connect(uri, *, create_protocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, loop=None, compression='deflate', origin=None, extensions=None, subprotocols=None, extra_headers=None, **kwds)
+ :async:
+
+ .. autofunction:: unix_connect(path, uri="ws://localhost/", *, create_protocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, loop=None, compression='deflate', origin=None, extensions=None, subprotocols=None, extra_headers=None, **kwds)
+ :async:
+
+ .. autoclass:: WebSocketClientProtocol(*, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, loop=None, origin=None, extensions=None, subprotocols=None, extra_headers=None)
+
+ .. automethod:: handshake
+
+Shared
+......
+
+.. automodule:: websockets.protocol
+
+ .. autoclass:: WebSocketCommonProtocol(*, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, loop=None)
+
+ .. automethod:: close
+ .. automethod:: wait_closed
+
+ .. automethod:: recv
+ .. automethod:: send
+
+ .. automethod:: ping
+ .. automethod:: pong
+
+ .. autoattribute:: local_address
+ .. autoattribute:: remote_address
+
+ .. autoattribute:: open
+ .. autoattribute:: closed
+
+Types
+.....
+
+.. automodule:: websockets.typing
+
+ .. autodata:: Data
+
+
+Per-Message Deflate Extension
+.............................
+
+.. automodule:: websockets.extensions.permessage_deflate
+
+ .. autoclass:: ServerPerMessageDeflateFactory
+
+ .. autoclass:: ClientPerMessageDeflateFactory
+
+HTTP Basic Auth
+...............
+
+.. automodule:: websockets.auth
+
+ .. autofunction:: basic_auth_protocol_factory
+
+ .. autoclass:: BasicAuthWebSocketServerProtocol
+
+ .. automethod:: process_request
+
+Exceptions
+..........
+
+.. automodule:: websockets.exceptions
+ :members:
+
+Low-level
+---------
+
+Opening handshake
+.................
+
+.. automodule:: websockets.handshake
+ :members:
+
+Data transfer
+.............
+
+.. automodule:: websockets.framing
+ :members:
+
+URI parser
+..........
+
+.. automodule:: websockets.uri
+ :members:
+
+Utilities
+.........
+
+.. automodule:: websockets.headers
+ :members:
+
+.. automodule:: websockets.http
+ :members:
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/changelog.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/changelog.rst
new file mode 100644
index 0000000000..04f18a7657
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/changelog.rst
@@ -0,0 +1,563 @@
+Changelog
+---------
+
+.. currentmodule:: websockets
+
+8.2
+...
+
+*In development*
+
+8.1
+...
+
+* Added compatibility with Python 3.8.
+
+8.0.2
+.....
+
+* Restored the ability to pass a socket with the ``sock`` parameter of
+ :func:`~server.serve`.
+
+* Removed an incorrect assertion when a connection drops.
+
+8.0.1
+.....
+
+* Restored the ability to import ``WebSocketProtocolError`` from
+ ``websockets``.
+
+8.0
+...
+
+.. warning::
+
+ **Version 8.0 drops compatibility with Python 3.4 and 3.5.**
+
+.. note::
+
+ **Version 8.0 expects** ``process_request`` **to be a coroutine.**
+
+ Previously, it could be a function or a coroutine.
+
+ If you're passing a ``process_request`` argument to :func:`~server.serve`
+ or :class:`~server.WebSocketServerProtocol`, or if you're overriding
+ :meth:`~protocol.WebSocketServerProtocol.process_request` in a subclass,
+ define it with ``async def`` instead of ``def``.
+
+ For backwards compatibility, functions are still mostly supported, but
+ mixing functions and coroutines won't work in some inheritance scenarios.
+
+.. note::
+
+ **Version 8.0 changes the behavior of the** ``max_queue`` **parameter.**
+
+ If you were setting ``max_queue=0`` to make the queue of incoming messages
+ unbounded, change it to ``max_queue=None``.
+
+.. note::
+
+ **Version 8.0 deprecates the** ``host`` **,** ``port`` **, and** ``secure``
+ **attributes of** :class:`~protocol.WebSocketCommonProtocol`.
+
+ Use :attr:`~protocol.WebSocketCommonProtocol.local_address` in servers and
+ :attr:`~protocol.WebSocketCommonProtocol.remote_address` in clients
+ instead of ``host`` and ``port``.
+
+.. note::
+
+ **Version 8.0 renames the** ``WebSocketProtocolError`` **exception**
+ to :exc:`ProtocolError` **.**
+
+ A ``WebSocketProtocolError`` alias provides backwards compatibility.
+
+.. note::
+
+ **Version 8.0 adds the reason phrase to the return type of the low-level
+ API** :func:`~http.read_response` **.**
+
+Also:
+
+* :meth:`~protocol.WebSocketCommonProtocol.send`,
+ :meth:`~protocol.WebSocketCommonProtocol.ping`, and
+ :meth:`~protocol.WebSocketCommonProtocol.pong` support bytes-like types
+ :class:`bytearray` and :class:`memoryview` in addition to :class:`bytes`.
+
+* Added :exc:`~exceptions.ConnectionClosedOK` and
+ :exc:`~exceptions.ConnectionClosedError` subclasses of
+ :exc:`~exceptions.ConnectionClosed` to tell apart normal connection
+ termination from errors.
+
+* Added :func:`~auth.basic_auth_protocol_factory` to enforce HTTP Basic Auth
+ on the server side.
+
+* :func:`~client.connect` handles redirects from the server during the
+ handshake.
+
+* :func:`~client.connect` supports overriding ``host`` and ``port``.
+
+* Added :func:`~client.unix_connect` for connecting to Unix sockets.
+
+* Improved support for sending fragmented messages by accepting asynchronous
+ iterators in :meth:`~protocol.WebSocketCommonProtocol.send`.
+
+* Prevented spurious log messages about :exc:`~exceptions.ConnectionClosed`
+ exceptions in keepalive ping task. If you were using ``ping_timeout=None``
+ as a workaround, you can remove it.
+
+* Changed :meth:`WebSocketServer.close() <server.WebSocketServer.close>` to
+ perform a proper closing handshake instead of failing the connection.
+
+* Avoided a crash when a ``extra_headers`` callable returns ``None``.
+
+* Improved error messages when HTTP parsing fails.
+
+* Enabled readline in the interactive client.
+
+* Added type hints (:pep:`484`).
+
+* Added a FAQ to the documentation.
+
+* Added documentation for extensions.
+
+* Documented how to optimize memory usage.
+
+* Improved API documentation.
+
+7.0
+...
+
+.. warning::
+
+ **Version 7.0 renames the** ``timeout`` **argument of**
+ :func:`~server.serve()` **and** :func:`~client.connect` **to**
+ ``close_timeout`` **.**
+
+ This prevents confusion with ``ping_timeout``.
+
+ For backwards compatibility, ``timeout`` is still supported.
+
+.. warning::
+
+ **Version 7.0 changes how a server terminates connections when it's
+ closed with** :meth:`~server.WebSocketServer.close` **.**
+
+ Previously, connections handlers were canceled. Now, connections are
+ closed with close code 1001 (going away). From the perspective of the
+ connection handler, this is the same as if the remote endpoint was
+ disconnecting. This removes the need to prepare for
+ :exc:`~asyncio.CancelledError` in connection handlers.
+
+ You can restore the previous behavior by adding the following line at the
+ beginning of connection handlers::
+
+ def handler(websocket, path):
+ closed = asyncio.ensure_future(websocket.wait_closed())
+ closed.add_done_callback(lambda task: task.cancel())
+
+.. note::
+
+ **Version 7.0 changes how a** :meth:`~protocol.WebSocketCommonProtocol.ping`
+ **that hasn't received a pong yet behaves when the connection is closed.**
+
+ The ping — as in ``ping = await websocket.ping()`` — used to be canceled
+ when the connection is closed, so that ``await ping`` raised
+ :exc:`~asyncio.CancelledError`. Now ``await ping`` raises
+ :exc:`~exceptions.ConnectionClosed` like other public APIs.
+
+.. note::
+
+ **Version 7.0 raises a** :exc:`RuntimeError` **exception if two coroutines
+ call** :meth:`~protocol.WebSocketCommonProtocol.recv` **concurrently.**
+
+ Concurrent calls lead to non-deterministic behavior because there are no
+ guarantees about which coroutine will receive which message.
+
+Also:
+
+* ``websockets`` sends Ping frames at regular intervals and closes the
+ connection if it doesn't receive a matching Pong frame. See
+ :class:`~protocol.WebSocketCommonProtocol` for details.
+
+* Added ``process_request`` and ``select_subprotocol`` arguments to
+ :func:`~server.serve` and :class:`~server.WebSocketServerProtocol` to
+ customize :meth:`~server.WebSocketServerProtocol.process_request` and
+ :meth:`~server.WebSocketServerProtocol.select_subprotocol` without
+ subclassing :class:`~server.WebSocketServerProtocol`.
+
+* Added support for sending fragmented messages.
+
+* Added the :meth:`~protocol.WebSocketCommonProtocol.wait_closed` method to
+ protocols.
+
+* Added an interactive client: ``python -m websockets <uri>``.
+
+* Changed the ``origins`` argument to represent the lack of an origin with
+ ``None`` rather than ``''``.
+
+* Fixed a data loss bug in :meth:`~protocol.WebSocketCommonProtocol.recv`:
+ canceling it at the wrong time could result in messages being dropped.
+
+* Improved handling of multiple HTTP headers with the same name.
+
+* Improved error messages when a required HTTP header is missing.
+
+6.0
+...
+
+.. warning::
+
+ **Version 6.0 introduces the** :class:`~http.Headers` **class for managing
+ HTTP headers and changes several public APIs:**
+
+ * :meth:`~server.WebSocketServerProtocol.process_request` now receives a
+ :class:`~http.Headers` instead of a :class:`~http.client.HTTPMessage` in
+ the ``request_headers`` argument.
+
+ * The :attr:`~protocol.WebSocketCommonProtocol.request_headers` and
+ :attr:`~protocol.WebSocketCommonProtocol.response_headers` attributes of
+ :class:`~protocol.WebSocketCommonProtocol` are :class:`~http.Headers`
+ instead of :class:`~http.client.HTTPMessage`.
+
+ * The :attr:`~protocol.WebSocketCommonProtocol.raw_request_headers` and
+ :attr:`~protocol.WebSocketCommonProtocol.raw_response_headers`
+ attributes of :class:`~protocol.WebSocketCommonProtocol` are removed.
+ Use :meth:`~http.Headers.raw_items` instead.
+
+ * Functions defined in the :mod:`~handshake` module now receive
+ :class:`~http.Headers` in argument instead of ``get_header`` or
+ ``set_header`` functions. This affects libraries that rely on
+ low-level APIs.
+
+ * Functions defined in the :mod:`~http` module now return HTTP headers as
+ :class:`~http.Headers` instead of lists of ``(name, value)`` pairs.
+
+ Since :class:`~http.Headers` and :class:`~http.client.HTTPMessage` provide
+ similar APIs, this change won't affect most of the code dealing with HTTP
+ headers.
+
+
+Also:
+
+* Added compatibility with Python 3.7.
+
+5.0.1
+.....
+
+* Fixed a regression in the 5.0 release that broke some invocations of
+ :func:`~server.serve()` and :func:`~client.connect`.
+
+5.0
+...
+
+.. note::
+
+ **Version 5.0 fixes a security issue introduced in version 4.0.**
+
+ Version 4.0 was vulnerable to denial of service by memory exhaustion
+ because it didn't enforce ``max_size`` when decompressing compressed
+ messages (`CVE-2018-1000518`_).
+
+ .. _CVE-2018-1000518: https://nvd.nist.gov/vuln/detail/CVE-2018-1000518
+
+.. note::
+
+ **Version 5.0 adds a** ``user_info`` **field to the return value of**
+ :func:`~uri.parse_uri` **and** :class:`~uri.WebSocketURI` **.**
+
+ If you're unpacking :class:`~exceptions.WebSocketURI` into four variables,
+ adjust your code to account for that fifth field.
+
+Also:
+
+* :func:`~client.connect` performs HTTP Basic Auth when the URI contains
+ credentials.
+
+* Iterating on incoming messages no longer raises an exception when the
+ connection terminates with close code 1001 (going away).
+
+* A plain HTTP request now receives a 426 Upgrade Required response and
+ doesn't log a stack trace.
+
+* :func:`~server.unix_serve` can be used as an asynchronous context manager on
+ Python ≥ 3.5.1.
+
+* Added the :attr:`~protocol.WebSocketCommonProtocol.closed` property to
+ protocols.
+
+* If a :meth:`~protocol.WebSocketCommonProtocol.ping` doesn't receive a pong,
+ it's canceled when the connection is closed.
+
+* Reported the cause of :exc:`~exceptions.ConnectionClosed` exceptions.
+
+* Added new examples in the documentation.
+
+* Updated documentation with new features from Python 3.6.
+
+* Improved several other sections of the documentation.
+
+* Fixed missing close code, which caused :exc:`TypeError` on connection close.
+
+* Fixed a race condition in the closing handshake that raised
+ :exc:`~exceptions.InvalidState`.
+
+* Stopped logging stack traces when the TCP connection dies prematurely.
+
+* Prevented writing to a closing TCP connection during unclean shutdowns.
+
+* Made connection termination more robust to network congestion.
+
+* Prevented processing of incoming frames after failing the connection.
+
+4.0.1
+.....
+
+* Fixed issues with the packaging of the 4.0 release.
+
+4.0
+...
+
+.. warning::
+
+ **Version 4.0 enables compression with the permessage-deflate extension.**
+
+ In August 2017, Firefox and Chrome support it, but not Safari and IE.
+
+ Compression should improve performance but it increases RAM and CPU use.
+
+ If you want to disable compression, add ``compression=None`` when calling
+ :func:`~server.serve()` or :func:`~client.connect`.
+
+.. warning::
+
+ **Version 4.0 drops compatibility with Python 3.3.**
+
+.. note::
+
+ **Version 4.0 removes the** ``state_name`` **attribute of protocols.**
+
+ Use ``protocol.state.name`` instead of ``protocol.state_name``.
+
+Also:
+
+* :class:`~protocol.WebSocketCommonProtocol` instances can be used as
+ asynchronous iterators on Python ≥ 3.6. They yield incoming messages.
+
+* Added :func:`~server.unix_serve` for listening on Unix sockets.
+
+* Added the :attr:`~server.WebSocketServer.sockets` attribute to the return
+ value of :func:`~server.serve`.
+
+* Reorganized and extended documentation.
+
+* Aborted connections if they don't close within the configured ``timeout``.
+
+* Rewrote connection termination to increase robustness in edge cases.
+
+* Stopped leaking pending tasks when :meth:`~asyncio.Task.cancel` is called on
+ a connection while it's being closed.
+
+* Reduced verbosity of "Failing the WebSocket connection" logs.
+
+* Allowed ``extra_headers`` to override ``Server`` and ``User-Agent`` headers.
+
+3.4
+...
+
+* Renamed :func:`~server.serve()` and :func:`~client.connect`'s ``klass``
+ argument to ``create_protocol`` to reflect that it can also be a callable.
+ For backwards compatibility, ``klass`` is still supported.
+
+* :func:`~server.serve` can be used as an asynchronous context manager on
+ Python ≥ 3.5.1.
+
+* Added support for customizing handling of incoming connections with
+ :meth:`~server.WebSocketServerProtocol.process_request`.
+
+* Made read and write buffer sizes configurable.
+
+* Rewrote HTTP handling for simplicity and performance.
+
+* Added an optional C extension to speed up low-level operations.
+
+* An invalid response status code during :func:`~client.connect` now raises
+ :class:`~exceptions.InvalidStatusCode` with a ``code`` attribute.
+
+* Providing a ``sock`` argument to :func:`~client.connect` no longer
+ crashes.
+
+3.3
+...
+
+* Ensured compatibility with Python 3.6.
+
+* Reduced noise in logs caused by connection resets.
+
+* Avoided crashing on concurrent writes on slow connections.
+
+3.2
+...
+
+* Added ``timeout``, ``max_size``, and ``max_queue`` arguments to
+ :func:`~client.connect()` and :func:`~server.serve`.
+
+* Made server shutdown more robust.
+
+3.1
+...
+
+* Avoided a warning when closing a connection before the opening handshake.
+
+* Added flow control for incoming data.
+
+3.0
+...
+
+.. warning::
+
+ **Version 3.0 introduces a backwards-incompatible change in the**
+ :meth:`~protocol.WebSocketCommonProtocol.recv` **API.**
+
+ **If you're upgrading from 2.x or earlier, please read this carefully.**
+
+ :meth:`~protocol.WebSocketCommonProtocol.recv` used to return ``None``
+ when the connection was closed. This required checking the return value of
+ every call::
+
+ message = await websocket.recv()
+ if message is None:
+ return
+
+ Now it raises a :exc:`~exceptions.ConnectionClosed` exception instead.
+ This is more Pythonic. The previous code can be simplified to::
+
+ message = await websocket.recv()
+
+ When implementing a server, which is the more popular use case, there's no
+ strong reason to handle such exceptions. Let them bubble up, terminate the
+ handler coroutine, and the server will simply ignore them.
+
+ In order to avoid stranding projects built upon an earlier version, the
+ previous behavior can be restored by passing ``legacy_recv=True`` to
+ :func:`~server.serve`, :func:`~client.connect`,
+ :class:`~server.WebSocketServerProtocol`, or
+ :class:`~client.WebSocketClientProtocol`. ``legacy_recv`` isn't documented
+ in their signatures but isn't scheduled for deprecation either.
+
+Also:
+
+* :func:`~client.connect` can be used as an asynchronous context manager on
+ Python ≥ 3.5.1.
+
+* Updated documentation with ``await`` and ``async`` syntax from Python 3.5.
+
+* :meth:`~protocol.WebSocketCommonProtocol.ping` and
+ :meth:`~protocol.WebSocketCommonProtocol.pong` support data passed as
+ :class:`str` in addition to :class:`bytes`.
+
+* Worked around an asyncio bug affecting connection termination under load.
+
+* Made ``state_name`` attribute on protocols a public API.
+
+* Improved documentation.
+
+2.7
+...
+
+* Added compatibility with Python 3.5.
+
+* Refreshed documentation.
+
+2.6
+...
+
+* Added ``local_address`` and ``remote_address`` attributes on protocols.
+
+* Closed open connections with code 1001 when a server shuts down.
+
+* Avoided TCP fragmentation of small frames.
+
+2.5
+...
+
+* Improved documentation.
+
+* Provided access to handshake request and response HTTP headers.
+
+* Allowed customizing handshake request and response HTTP headers.
+
+* Supported running on a non-default event loop.
+
+* Returned a 403 status code instead of 400 when the request Origin isn't
+ allowed.
+
+* Canceling :meth:`~protocol.WebSocketCommonProtocol.recv` no longer drops
+ the next message.
+
+* Clarified that the closing handshake can be initiated by the client.
+
+* Set the close code and reason more consistently.
+
+* Strengthened connection termination by simplifying the implementation.
+
+* Improved tests, added tox configuration, and enforced 100% branch coverage.
+
+2.4
+...
+
+* Added support for subprotocols.
+
+* Supported non-default event loop.
+
+* Added ``loop`` argument to :func:`~client.connect` and
+ :func:`~server.serve`.
+
+2.3
+...
+
+* Improved compliance of close codes.
+
+2.2
+...
+
+* Added support for limiting message size.
+
+2.1
+...
+
+* Added ``host``, ``port`` and ``secure`` attributes on protocols.
+
+* Added support for providing and checking Origin_.
+
+.. _Origin: https://tools.ietf.org/html/rfc6455#section-10.2
+
+2.0
+...
+
+.. warning::
+
+ **Version 2.0 introduces a backwards-incompatible change in the**
+ :meth:`~protocol.WebSocketCommonProtocol.send`,
+ :meth:`~protocol.WebSocketCommonProtocol.ping`, and
+ :meth:`~protocol.WebSocketCommonProtocol.pong` **APIs.**
+
+ **If you're upgrading from 1.x or earlier, please read this carefully.**
+
+ These APIs used to be functions. Now they're coroutines.
+
+ Instead of::
+
+ websocket.send(message)
+
+ you must now write::
+
+ await websocket.send(message)
+
+Also:
+
+* Added flow control for outgoing data.
+
+1.0
+...
+
+* Initial public release.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/cheatsheet.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/cheatsheet.rst
new file mode 100644
index 0000000000..f897326a6b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/cheatsheet.rst
@@ -0,0 +1,109 @@
+Cheat sheet
+===========
+
+.. currentmodule:: websockets
+
+Server
+------
+
+* Write a coroutine that handles a single connection. It receives a WebSocket
+ protocol instance and the URI path in argument.
+
+ * Call :meth:`~protocol.WebSocketCommonProtocol.recv` and
+ :meth:`~protocol.WebSocketCommonProtocol.send` to receive and send
+ messages at any time.
+
+ * When :meth:`~protocol.WebSocketCommonProtocol.recv` or
+ :meth:`~protocol.WebSocketCommonProtocol.send` raises
+ :exc:`~exceptions.ConnectionClosed`, clean up and exit. If you started
+ other :class:`asyncio.Task`, terminate them before exiting.
+
+ * If you aren't awaiting :meth:`~protocol.WebSocketCommonProtocol.recv`,
+ consider awaiting :meth:`~protocol.WebSocketCommonProtocol.wait_closed`
+ to detect quickly when the connection is closed.
+
+ * You may :meth:`~protocol.WebSocketCommonProtocol.ping` or
+ :meth:`~protocol.WebSocketCommonProtocol.pong` if you wish but it isn't
+ needed in general.
+
+* Create a server with :func:`~server.serve` which is similar to asyncio's
+ :meth:`~asyncio.AbstractEventLoop.create_server`. You can also use it as an
+ asynchronous context manager.
+
+ * The server takes care of establishing connections, then lets the handler
+ execute the application logic, and finally closes the connection after the
+ handler exits normally or with an exception.
+
+ * For advanced customization, you may subclass
+ :class:`~server.WebSocketServerProtocol` and pass either this subclass or
+ a factory function as the ``create_protocol`` argument.
+
+Client
+------
+
+* Create a client with :func:`~client.connect` which is similar to asyncio's
+ :meth:`~asyncio.BaseEventLoop.create_connection`. You can also use it as an
+ asynchronous context manager.
+
+ * For advanced customization, you may subclass
+ :class:`~server.WebSocketClientProtocol` and pass either this subclass or
+ a factory function as the ``create_protocol`` argument.
+
+* Call :meth:`~protocol.WebSocketCommonProtocol.recv` and
+ :meth:`~protocol.WebSocketCommonProtocol.send` to receive and send messages
+ at any time.
+
+* You may :meth:`~protocol.WebSocketCommonProtocol.ping` or
+ :meth:`~protocol.WebSocketCommonProtocol.pong` if you wish but it isn't
+ needed in general.
+
+* If you aren't using :func:`~client.connect` as a context manager, call
+ :meth:`~protocol.WebSocketCommonProtocol.close` to terminate the connection.
+
+.. _debugging:
+
+Debugging
+---------
+
+If you don't understand what ``websockets`` is doing, enable logging::
+
+ import logging
+ logger = logging.getLogger('websockets')
+ logger.setLevel(logging.INFO)
+ logger.addHandler(logging.StreamHandler())
+
+The logs contain:
+
+* Exceptions in the connection handler at the ``ERROR`` level
+* Exceptions in the opening or closing handshake at the ``INFO`` level
+* All frames at the ``DEBUG`` level — this can be very verbose
+
+If you're new to ``asyncio``, you will certainly encounter issues that are
+related to asynchronous programming in general rather than to ``websockets``
+in particular. Fortunately Python's official documentation provides advice to
+`develop with asyncio`_. Check it out: it's invaluable!
+
+.. _develop with asyncio: https://docs.python.org/3/library/asyncio-dev.html
+
+Passing additional arguments to the connection handler
+------------------------------------------------------
+
+When writing a server, if you need to pass additional arguments to the
+connection handler, you can bind them with :func:`functools.partial`::
+
+ import asyncio
+ import functools
+ import websockets
+
+ async def handler(websocket, path, extra_argument):
+ ...
+
+ bound_handler = functools.partial(handler, extra_argument='spam')
+ start_server = websockets.serve(bound_handler, '127.0.0.1', 8765)
+
+ asyncio.get_event_loop().run_until_complete(start_server)
+ asyncio.get_event_loop().run_forever()
+
+Another way to achieve this result is to define the ``handler`` coroutine in
+a scope where the ``extra_argument`` variable exists instead of injecting it
+through an argument.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py b/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py
new file mode 100644
index 0000000000..064c657bf1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py
@@ -0,0 +1,272 @@
+# -*- coding: utf-8 -*-
+#
+# websockets documentation build configuration file, created by
+# sphinx-quickstart on Sun Mar 31 20:48:44 2013.
+#
+# 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, datetime
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.join(os.path.abspath('..'), 'src'))
+
+# -- 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.intersphinx',
+ 'sphinx.ext.viewcode',
+ 'sphinx_autodoc_typehints',
+ 'sphinxcontrib_trio',
+ ]
+
+# Spelling check needs an additional module that is not installed by default.
+# Add it only if spelling check is requested so docs can be generated without it.
+if 'spelling' in sys.argv:
+ extensions.append('sphinxcontrib.spelling')
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'websockets'
+copyright = f'2013-{datetime.date.today().year}, Aymeric Augustin and contributors'
+
+# 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.
+version = '8.1'
+# The full version, including alpha/beta/rc tags.
+release = '8.1'
+
+# 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 = 'alabaster'
+
+# 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 = {
+ 'logo': 'websockets.svg',
+ 'description': 'A library for building WebSocket servers and clients in Python with a focus on correctness and simplicity.',
+ 'github_button': True,
+ 'github_user': 'aaugustin',
+ 'github_repo': 'websockets',
+ 'tidelift_url': 'https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=docs',
+}
+
+# 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 = {
+ '**': [
+ 'about.html',
+ 'searchbox.html',
+ 'navigation.html',
+ 'relations.html',
+ 'donate.html',
+ ]
+}
+
+# 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 = 'websocketsdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'websockets.tex', 'websockets Documentation',
+ 'Aymeric Augustin', '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
+
+# 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', 'websockets', 'websockets Documentation',
+ ['Aymeric Augustin'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'websockets', 'websockets Documentation',
+ 'Aymeric Augustin', 'websockets', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'https://docs.python.org/3/': None}
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/contributing.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/contributing.rst
new file mode 100644
index 0000000000..40f1dbb54a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/contributing.rst
@@ -0,0 +1,61 @@
+Contributing
+============
+
+Thanks for taking the time to contribute to websockets!
+
+Code of Conduct
+---------------
+
+This project and everyone participating in it is governed by the `Code of
+Conduct`_. By participating, you are expected to uphold this code. Please
+report inappropriate behavior to aymeric DOT augustin AT fractalideas DOT com.
+
+.. _Code of Conduct: https://github.com/aaugustin/websockets/blob/master/CODE_OF_CONDUCT.md
+
+*(If I'm the person with the inappropriate behavior, please accept my
+apologies. I know I can mess up. I can't expect you to tell me, but if you
+choose to do so, I'll do my best to handle criticism constructively.
+-- Aymeric)*
+
+Contributions
+-------------
+
+Bug reports, patches and suggestions are welcome!
+
+Please open an issue_ or send a `pull request`_.
+
+Feedback about the documentation is especially valuable — the authors of
+``websockets`` feel more confident about writing code than writing docs :-)
+
+If you're wondering why things are done in a certain way, the :doc:`design
+document <design>` provides lots of details about the internals of websockets.
+
+.. _issue: https://github.com/aaugustin/websockets/issues/new
+.. _pull request: https://github.com/aaugustin/websockets/compare/
+
+Questions
+---------
+
+GitHub issues aren't a good medium for handling questions. There are better
+places to ask questions, for example Stack Overflow.
+
+If you want to ask a question anyway, please make sure that:
+
+- it's a question about ``websockets`` and not about :mod:`asyncio`;
+- it isn't answered by the documentation;
+- it wasn't asked already.
+
+A good question can be written as a suggestion to improve the documentation.
+
+Bitcoin users
+-------------
+
+websockets appears to be quite popular for interfacing with Bitcoin or other
+cryptocurrency trackers. I'm strongly opposed to Bitcoin's carbon footprint.
+
+Please stop heating the planet where my children are supposed to live, thanks.
+
+Since ``websockets`` is released under an open-source license, you can use it
+for any purpose you like. However, I won't spend any of my time to help.
+
+I will summarily close issues related to Bitcoin or cryptocurrency in any way.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/deployment.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/deployment.rst
new file mode 100644
index 0000000000..5b05afff14
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/deployment.rst
@@ -0,0 +1,162 @@
+Deployment
+==========
+
+.. currentmodule:: websockets
+
+Application server
+------------------
+
+The author of ``websockets`` isn't aware of best practices for deploying
+network services based on :mod:`asyncio`, let alone application servers.
+
+You can run a script similar to the :ref:`server example <server-example>`,
+inside a supervisor if you deem that useful.
+
+You can also add a wrapper to daemonize the process. Third-party libraries
+provide solutions for that.
+
+If you can share knowledge on this topic, please file an issue_. Thanks!
+
+.. _issue: https://github.com/aaugustin/websockets/issues/new
+
+Graceful shutdown
+-----------------
+
+You may want to close connections gracefully when shutting down the server,
+perhaps after executing some cleanup logic. There are two ways to achieve this
+with the object returned by :func:`~server.serve`:
+
+- using it as a asynchronous context manager, or
+- calling its ``close()`` method, then waiting for its ``wait_closed()``
+ method to complete.
+
+On Unix systems, shutdown is usually triggered by sending a signal.
+
+Here's a full example for handling SIGTERM on Unix:
+
+.. literalinclude:: ../example/shutdown.py
+ :emphasize-lines: 13,17-19
+
+This example is easily adapted to handle other signals. If you override the
+default handler for SIGINT, which raises :exc:`KeyboardInterrupt`, be aware
+that you won't be able to interrupt a program with Ctrl-C anymore when it's
+stuck in a loop.
+
+It's more difficult to achieve the same effect on Windows. Some third-party
+projects try to help with this problem.
+
+If your server doesn't run in the main thread, look at
+:func:`~asyncio.AbstractEventLoop.call_soon_threadsafe`.
+
+Memory usage
+------------
+
+.. _memory-usage:
+
+In most cases, memory usage of a WebSocket server is proportional to the
+number of open connections. When a server handles thousands of connections,
+memory usage can become a bottleneck.
+
+Memory usage of a single connection is the sum of:
+
+1. the baseline amount of memory ``websockets`` requires for each connection,
+2. the amount of data held in buffers before the application processes it,
+3. any additional memory allocated by the application itself.
+
+Baseline
+........
+
+Compression settings are the main factor affecting the baseline amount of
+memory used by each connection.
+
+By default ``websockets`` maximizes compression rate at the expense of memory
+usage. If memory usage is an issue, lowering compression settings can help:
+
+- Context Takeover is necessary to get good performance for almost all
+ applications. It should remain enabled.
+- Window Bits is a trade-off between memory usage and compression rate.
+ It defaults to 15 and can be lowered. The default value isn't optimal
+ for small, repetitive messages which are typical of WebSocket servers.
+- Memory Level is a trade-off between memory usage and compression speed.
+ It defaults to 8 and can be lowered. A lower memory level can actually
+ increase speed thanks to memory locality, even if the CPU does more work!
+
+See this :ref:`example <per-message-deflate-configuration-example>` for how to
+configure compression settings.
+
+Here's how various compression settings affect memory usage of a single
+connection on a 64-bit system, as well a benchmark_ of compressed size and
+compression time for a corpus of small JSON documents.
+
++-------------+-------------+--------------+--------------+------------------+------------------+
+| Compression | Window Bits | Memory Level | Memory usage | Size vs. default | Time vs. default |
++=============+=============+==============+==============+==================+==================+
+| *default* | 15 | 8 | 325 KiB | +0% | +0% +
++-------------+-------------+--------------+--------------+------------------+------------------+
+| | 14 | 7 | 181 KiB | +1.5% | -5.3% |
++-------------+-------------+--------------+--------------+------------------+------------------+
+| | 13 | 6 | 110 KiB | +2.8% | -7.5% |
++-------------+-------------+--------------+--------------+------------------+------------------+
+| | 12 | 5 | 73 KiB | +4.4% | -18.9% |
++-------------+-------------+--------------+--------------+------------------+------------------+
+| | 11 | 4 | 55 KiB | +8.5% | -18.8% |
++-------------+-------------+--------------+--------------+------------------+------------------+
+| *disabled* | N/A | N/A | 22 KiB | N/A | N/A |
++-------------+-------------+--------------+--------------+------------------+------------------+
+
+*Don't assume this example is representative! Compressed size and compression
+time depend heavily on the kind of messages exchanged by the application!*
+
+You can run the same benchmark for your application by creating a list of
+typical messages and passing it to the ``_benchmark`` function_.
+
+.. _benchmark: https://gist.github.com/aaugustin/fbea09ce8b5b30c4e56458eb081fe599
+.. _function: https://gist.github.com/aaugustin/fbea09ce8b5b30c4e56458eb081fe599#file-compression-py-L48-L144
+
+This `blog post by Ilya Grigorik`_ provides more details about how compression
+settings affect memory usage and how to optimize them.
+
+.. _blog post by Ilya Grigorik: https://www.igvita.com/2013/11/27/configuring-and-optimizing-websocket-compression/
+
+This `experiment by Peter Thorson`_ suggests Window Bits = 11, Memory Level =
+4 as a sweet spot for optimizing memory usage.
+
+.. _experiment by Peter Thorson: https://www.ietf.org/mail-archive/web/hybi/current/msg10222.html
+
+Buffers
+.......
+
+Under normal circumstances, buffers are almost always empty.
+
+Under high load, if a server receives more messages than it can process,
+bufferbloat can result in excessive memory use.
+
+By default ``websockets`` has generous limits. It is strongly recommended to
+adapt them to your application. When you call :func:`~server.serve`:
+
+- Set ``max_size`` (default: 1 MiB, UTF-8 encoded) to the maximum size of
+ messages your application generates.
+- Set ``max_queue`` (default: 32) to the maximum number of messages your
+ application expects to receive faster than it can process them. The queue
+ provides burst tolerance without slowing down the TCP connection.
+
+Furthermore, you can lower ``read_limit`` and ``write_limit`` (default:
+64 KiB) to reduce the size of buffers for incoming and outgoing data.
+
+The design document provides :ref:`more details about buffers<buffers>`.
+
+Port sharing
+------------
+
+The WebSocket protocol is an extension of HTTP/1.1. It can be tempting to
+serve both HTTP and WebSocket on the same port.
+
+The author of ``websockets`` doesn't think that's a good idea, due to the
+widely different operational characteristics of HTTP and WebSocket.
+
+``websockets`` provide minimal support for responding to HTTP requests with
+the :meth:`~server.WebSocketServerProtocol.process_request` hook. Typical
+use cases include health checks. Here's an example:
+
+.. literalinclude:: ../example/health_check_server.py
+ :emphasize-lines: 9-11,17-19
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/design.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/design.rst
new file mode 100644
index 0000000000..74279b87f6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/design.rst
@@ -0,0 +1,571 @@
+Design
+======
+
+.. currentmodule:: websockets
+
+This document describes the design of ``websockets``. It assumes familiarity
+with the specification of the WebSocket protocol in :rfc:`6455`.
+
+It's primarily intended at maintainers. It may also be useful for users who
+wish to understand what happens under the hood.
+
+.. warning::
+
+ Internals described in this document may change at any time.
+
+ Backwards compatibility is only guaranteed for `public APIs <api>`_.
+
+
+Lifecycle
+---------
+
+State
+.....
+
+WebSocket connections go through a trivial state machine:
+
+- ``CONNECTING``: initial state,
+- ``OPEN``: when the opening handshake is complete,
+- ``CLOSING``: when the closing handshake is started,
+- ``CLOSED``: when the TCP connection is closed.
+
+Transitions happen in the following places:
+
+- ``CONNECTING -> OPEN``: in
+ :meth:`~protocol.WebSocketCommonProtocol.connection_open` which runs when
+ the :ref:`opening handshake <opening-handshake>` completes and the WebSocket
+ connection is established — not to be confused with
+ :meth:`~asyncio.Protocol.connection_made` which runs when the TCP connection
+ is established;
+- ``OPEN -> CLOSING``: in
+ :meth:`~protocol.WebSocketCommonProtocol.write_frame` immediately before
+ sending a close frame; since receiving a close frame triggers sending a
+ close frame, this does the right thing regardless of which side started the
+ :ref:`closing handshake <closing-handshake>`; also in
+ :meth:`~protocol.WebSocketCommonProtocol.fail_connection` which duplicates
+ a few lines of code from ``write_close_frame()`` and ``write_frame()``;
+- ``* -> CLOSED``: in
+ :meth:`~protocol.WebSocketCommonProtocol.connection_lost` which is always
+ called exactly once when the TCP connection is closed.
+
+Coroutines
+..........
+
+The following diagram shows which coroutines are running at each stage of the
+connection lifecycle on the client side.
+
+.. image:: lifecycle.svg
+ :target: _images/lifecycle.svg
+
+The lifecycle is identical on the server side, except inversion of control
+makes the equivalent of :meth:`~client.connect` implicit.
+
+Coroutines shown in green are called by the application. Multiple coroutines
+may interact with the WebSocket connection concurrently.
+
+Coroutines shown in gray manage the connection. When the opening handshake
+succeeds, :meth:`~protocol.WebSocketCommonProtocol.connection_open` starts
+two tasks:
+
+- :attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` runs
+ :meth:`~protocol.WebSocketCommonProtocol.transfer_data` which handles
+ incoming data and lets :meth:`~protocol.WebSocketCommonProtocol.recv`
+ consume it. It may be canceled to terminate the connection. It never exits
+ with an exception other than :exc:`~asyncio.CancelledError`. See :ref:`data
+ transfer <data-transfer>` below.
+
+- :attr:`~protocol.WebSocketCommonProtocol.keepalive_ping_task` runs
+ :meth:`~protocol.WebSocketCommonProtocol.keepalive_ping` which sends Ping
+ frames at regular intervals and ensures that corresponding Pong frames are
+ received. It is canceled when the connection terminates. It never exits
+ with an exception other than :exc:`~asyncio.CancelledError`.
+
+- :attr:`~protocol.WebSocketCommonProtocol.close_connection_task` runs
+ :meth:`~protocol.WebSocketCommonProtocol.close_connection` which waits for
+ the data transfer to terminate, then takes care of closing the TCP
+ connection. It must not be canceled. It never exits with an exception. See
+ :ref:`connection termination <connection-termination>` below.
+
+Besides, :meth:`~protocol.WebSocketCommonProtocol.fail_connection` starts
+the same :attr:`~protocol.WebSocketCommonProtocol.close_connection_task` when
+the opening handshake fails, in order to close the TCP connection.
+
+Splitting the responsibilities between two tasks makes it easier to guarantee
+that ``websockets`` can terminate connections:
+
+- within a fixed timeout,
+- without leaking pending tasks,
+- without leaking open TCP connections,
+
+regardless of whether the connection terminates normally or abnormally.
+
+:attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` completes when no
+more data will be received on the connection. Under normal circumstances, it
+exits after exchanging close frames.
+
+:attr:`~protocol.WebSocketCommonProtocol.close_connection_task` completes when
+the TCP connection is closed.
+
+
+.. _opening-handshake:
+
+Opening handshake
+-----------------
+
+``websockets`` performs the opening handshake when establishing a WebSocket
+connection. On the client side, :meth:`~client.connect` executes it before
+returning the protocol to the caller. On the server side, it's executed before
+passing the protocol to the ``ws_handler`` coroutine handling the connection.
+
+While the opening handshake is asymmetrical — the client sends an HTTP Upgrade
+request and the server replies with an HTTP Switching Protocols response —
+``websockets`` aims at keeping the implementation of both sides consistent
+with one another.
+
+On the client side, :meth:`~client.WebSocketClientProtocol.handshake`:
+
+- builds a HTTP request based on the ``uri`` and parameters passed to
+ :meth:`~client.connect`;
+- writes the HTTP request to the network;
+- reads a HTTP response from the network;
+- checks the HTTP response, validates ``extensions`` and ``subprotocol``, and
+ configures the protocol accordingly;
+- moves to the ``OPEN`` state.
+
+On the server side, :meth:`~server.WebSocketServerProtocol.handshake`:
+
+- reads a HTTP request from the network;
+- calls :meth:`~server.WebSocketServerProtocol.process_request` which may
+ abort the WebSocket handshake and return a HTTP response instead; this
+ hook only makes sense on the server side;
+- checks the HTTP request, negotiates ``extensions`` and ``subprotocol``, and
+ configures the protocol accordingly;
+- builds a HTTP response based on the above and parameters passed to
+ :meth:`~server.serve`;
+- writes the HTTP response to the network;
+- moves to the ``OPEN`` state;
+- returns the ``path`` part of the ``uri``.
+
+The most significant asymmetry between the two sides of the opening handshake
+lies in the negotiation of extensions and, to a lesser extent, of the
+subprotocol. The server knows everything about both sides and decides what the
+parameters should be for the connection. The client merely applies them.
+
+If anything goes wrong during the opening handshake, ``websockets``
+:ref:`fails the connection <connection-failure>`.
+
+
+.. _data-transfer:
+
+Data transfer
+-------------
+
+Symmetry
+........
+
+Once the opening handshake has completed, the WebSocket protocol enters the
+data transfer phase. This part is almost symmetrical. There are only two
+differences between a server and a client:
+
+- `client-to-server masking`_: the client masks outgoing frames; the server
+ unmasks incoming frames;
+- `closing the TCP connection`_: the server closes the connection immediately;
+ the client waits for the server to do it.
+
+.. _client-to-server masking: https://tools.ietf.org/html/rfc6455#section-5.3
+.. _closing the TCP connection: https://tools.ietf.org/html/rfc6455#section-5.5.1
+
+These differences are so minor that all the logic for `data framing`_, for
+`sending and receiving data`_ and for `closing the connection`_ is implemented
+in the same class, :class:`~protocol.WebSocketCommonProtocol`.
+
+.. _data framing: https://tools.ietf.org/html/rfc6455#section-5
+.. _sending and receiving data: https://tools.ietf.org/html/rfc6455#section-6
+.. _closing the connection: https://tools.ietf.org/html/rfc6455#section-7
+
+The :attr:`~protocol.WebSocketCommonProtocol.is_client` attribute tells which
+side a protocol instance is managing. This attribute is defined on the
+:attr:`~server.WebSocketServerProtocol` and
+:attr:`~client.WebSocketClientProtocol` classes.
+
+Data flow
+.........
+
+The following diagram shows how data flows between an application built on top
+of ``websockets`` and a remote endpoint. It applies regardless of which side
+is the server or the client.
+
+.. image:: protocol.svg
+ :target: _images/protocol.svg
+
+Public methods are shown in green, private methods in yellow, and buffers in
+orange. Methods related to connection termination are omitted; connection
+termination is discussed in another section below.
+
+Receiving data
+..............
+
+The left side of the diagram shows how ``websockets`` receives data.
+
+Incoming data is written to a :class:`~asyncio.StreamReader` in order to
+implement flow control and provide backpressure on the TCP connection.
+
+:attr:`~protocol.WebSocketCommonProtocol.transfer_data_task`, which is started
+when the WebSocket connection is established, processes this data.
+
+When it receives data frames, it reassembles fragments and puts the resulting
+messages in the :attr:`~protocol.WebSocketCommonProtocol.messages` queue.
+
+When it encounters a control frame:
+
+- if it's a close frame, it starts the closing handshake;
+- if it's a ping frame, it answers with a pong frame;
+- if it's a pong frame, it acknowledges the corresponding ping (unless it's an
+ unsolicited pong).
+
+Running this process in a task guarantees that control frames are processed
+promptly. Without such a task, ``websockets`` would depend on the application
+to drive the connection by having exactly one coroutine awaiting
+:meth:`~protocol.WebSocketCommonProtocol.recv` at any time. While this
+happens naturally in many use cases, it cannot be relied upon.
+
+Then :meth:`~protocol.WebSocketCommonProtocol.recv` fetches the next message
+from the :attr:`~protocol.WebSocketCommonProtocol.messages` queue, with some
+complexity added for handling backpressure and termination correctly.
+
+Sending data
+............
+
+The right side of the diagram shows how ``websockets`` sends data.
+
+:meth:`~protocol.WebSocketCommonProtocol.send` writes one or several data
+frames containing the message. While sending a fragmented message, concurrent
+calls to :meth:`~protocol.WebSocketCommonProtocol.send` are put on hold until
+all fragments are sent. This makes concurrent calls safe.
+
+:meth:`~protocol.WebSocketCommonProtocol.ping` writes a ping frame and
+yields a :class:`~asyncio.Future` which will be completed when a matching pong
+frame is received.
+
+:meth:`~protocol.WebSocketCommonProtocol.pong` writes a pong frame.
+
+:meth:`~protocol.WebSocketCommonProtocol.close` writes a close frame and
+waits for the TCP connection to terminate.
+
+Outgoing data is written to a :class:`~asyncio.StreamWriter` in order to
+implement flow control and provide backpressure from the TCP connection.
+
+.. _closing-handshake:
+
+Closing handshake
+.................
+
+When the other side of the connection initiates the closing handshake,
+:meth:`~protocol.WebSocketCommonProtocol.read_message` receives a close
+frame while in the ``OPEN`` state. It moves to the ``CLOSING`` state, sends a
+close frame, and returns ``None``, causing
+:attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` to terminate.
+
+When this side of the connection initiates the closing handshake with
+:meth:`~protocol.WebSocketCommonProtocol.close`, it moves to the ``CLOSING``
+state and sends a close frame. When the other side sends a close frame,
+:meth:`~protocol.WebSocketCommonProtocol.read_message` receives it in the
+``CLOSING`` state and returns ``None``, also causing
+:attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` to terminate.
+
+If the other side doesn't send a close frame within the connection's close
+timeout, ``websockets`` :ref:`fails the connection <connection-failure>`.
+
+The closing handshake can take up to ``2 * close_timeout``: one
+``close_timeout`` to write a close frame and one ``close_timeout`` to receive
+a close frame.
+
+Then ``websockets`` terminates the TCP connection.
+
+
+.. _connection-termination:
+
+Connection termination
+----------------------
+
+:attr:`~protocol.WebSocketCommonProtocol.close_connection_task`, which is
+started when the WebSocket connection is established, is responsible for
+eventually closing the TCP connection.
+
+First :attr:`~protocol.WebSocketCommonProtocol.close_connection_task` waits
+for :attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` to terminate,
+which may happen as a result of:
+
+- a successful closing handshake: as explained above, this exits the infinite
+ loop in :attr:`~protocol.WebSocketCommonProtocol.transfer_data_task`;
+- a timeout while waiting for the closing handshake to complete: this cancels
+ :attr:`~protocol.WebSocketCommonProtocol.transfer_data_task`;
+- a protocol error, including connection errors: depending on the exception,
+ :attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` :ref:`fails the
+ connection <connection-failure>` with a suitable code and exits.
+
+:attr:`~protocol.WebSocketCommonProtocol.close_connection_task` is separate
+from :attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` to make it
+easier to implement the timeout on the closing handshake. Canceling
+:attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` creates no risk
+of canceling :attr:`~protocol.WebSocketCommonProtocol.close_connection_task`
+and failing to close the TCP connection, thus leaking resources.
+
+Then :attr:`~protocol.WebSocketCommonProtocol.close_connection_task` cancels
+:attr:`~protocol.WebSocketCommonProtocol.keepalive_ping`. This task has no
+protocol compliance responsibilities. Terminating it to avoid leaking it is
+the only concern.
+
+Terminating the TCP connection can take up to ``2 * close_timeout`` on the
+server side and ``3 * close_timeout`` on the client side. Clients start by
+waiting for the server to close the connection, hence the extra
+``close_timeout``. Then both sides go through the following steps until the
+TCP connection is lost: half-closing the connection (only for non-TLS
+connections), closing the connection, aborting the connection. At this point
+the connection drops regardless of what happens on the network.
+
+
+.. _connection-failure:
+
+Connection failure
+------------------
+
+If the opening handshake doesn't complete successfully, ``websockets`` fails
+the connection by closing the TCP connection.
+
+Once the opening handshake has completed, ``websockets`` fails the connection
+by canceling :attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` and
+sending a close frame if appropriate.
+
+:attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` exits, unblocking
+:attr:`~protocol.WebSocketCommonProtocol.close_connection_task`, which closes
+the TCP connection.
+
+
+.. _server-shutdown:
+
+Server shutdown
+---------------
+
+:class:`~websockets.server.WebSocketServer` closes asynchronously like
+:class:`asyncio.Server`. The shutdown happen in two steps:
+
+1. Stop listening and accepting new connections;
+2. Close established connections with close code 1001 (going away) or, if
+ the opening handshake is still in progress, with HTTP status code 503
+ (Service Unavailable).
+
+The first call to :class:`~websockets.server.WebSocketServer.close` starts a
+task that performs this sequence. Further calls are ignored. This is the
+easiest way to make :class:`~websockets.server.WebSocketServer.close` and
+:class:`~websockets.server.WebSocketServer.wait_closed` idempotent.
+
+
+.. _cancellation:
+
+Cancellation
+------------
+
+User code
+.........
+
+``websockets`` provides a WebSocket application server. It manages connections
+and passes them to user-provided connection handlers. This is an *inversion of
+control* scenario: library code calls user code.
+
+If a connection drops, the corresponding handler should terminate. If the
+server shuts down, all connection handlers must terminate. Canceling
+connection handlers would terminate them.
+
+However, using cancellation for this purpose would require all connection
+handlers to handle it properly. For example, if a connection handler starts
+some tasks, it should catch :exc:`~asyncio.CancelledError`, terminate or
+cancel these tasks, and then re-raise the exception.
+
+Cancellation is tricky in :mod:`asyncio` applications, especially when it
+interacts with finalization logic. In the example above, what if a handler
+gets interrupted with :exc:`~asyncio.CancelledError` while it's finalizing
+the tasks it started, after detecting that the connection dropped?
+
+``websockets`` considers that cancellation may only be triggered by the caller
+of a coroutine when it doesn't care about the results of that coroutine
+anymore. (Source: `Guido van Rossum <https://groups.google.com/forum/#!msg
+/python-tulip/LZQe38CR3bg/7qZ1p_q5yycJ>`_). Since connection handlers run
+arbitrary user code, ``websockets`` has no way of deciding whether that code
+is still doing something worth caring about.
+
+For these reasons, ``websockets`` never cancels connection handlers. Instead
+it expects them to detect when the connection is closed, execute finalization
+logic if needed, and exit.
+
+Conversely, cancellation isn't a concern for WebSocket clients because they
+don't involve inversion of control.
+
+Library
+.......
+
+Most :doc:`public APIs <api>` of ``websockets`` are coroutines. They may be
+canceled, for example if the user starts a task that calls these coroutines
+and cancels the task later. ``websockets`` must handle this situation.
+
+Cancellation during the opening handshake is handled like any other exception:
+the TCP connection is closed and the exception is re-raised. This can only
+happen on the client side. On the server side, the opening handshake is
+managed by ``websockets`` and nothing results in a cancellation.
+
+Once the WebSocket connection is established, internal tasks
+:attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` and
+:attr:`~protocol.WebSocketCommonProtocol.close_connection_task` mustn't get
+accidentally canceled if a coroutine that awaits them is canceled. In other
+words, they must be shielded from cancellation.
+
+:meth:`~protocol.WebSocketCommonProtocol.recv` waits for the next message in
+the queue or for :attr:`~protocol.WebSocketCommonProtocol.transfer_data_task`
+to terminate, whichever comes first. It relies on :func:`~asyncio.wait` for
+waiting on two futures in parallel. As a consequence, even though it's waiting
+on a :class:`~asyncio.Future` signaling the next message and on
+:attr:`~protocol.WebSocketCommonProtocol.transfer_data_task`, it doesn't
+propagate cancellation to them.
+
+:meth:`~protocol.WebSocketCommonProtocol.ensure_open` is called by
+:meth:`~protocol.WebSocketCommonProtocol.send`,
+:meth:`~protocol.WebSocketCommonProtocol.ping`, and
+:meth:`~protocol.WebSocketCommonProtocol.pong`. When the connection state is
+``CLOSING``, it waits for
+:attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` but shields it to
+prevent cancellation.
+
+:meth:`~protocol.WebSocketCommonProtocol.close` waits for the data transfer
+task to terminate with :func:`~asyncio.wait_for`. If it's canceled or if the
+timeout elapses, :attr:`~protocol.WebSocketCommonProtocol.transfer_data_task`
+is canceled, which is correct at this point.
+:meth:`~protocol.WebSocketCommonProtocol.close` then waits for
+:attr:`~protocol.WebSocketCommonProtocol.close_connection_task` but shields it
+to prevent cancellation.
+
+:meth:`~protocol.WebSocketCommonProtocol.close` and
+:func:`~protocol.WebSocketCommonProtocol.fail_connection` are the only
+places where :attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` may
+be canceled.
+
+:attr:`~protocol.WebSocketCommonProtocol.close_connnection_task` starts by
+waiting for :attr:`~protocol.WebSocketCommonProtocol.transfer_data_task`. It
+catches :exc:`~asyncio.CancelledError` to prevent a cancellation of
+:attr:`~protocol.WebSocketCommonProtocol.transfer_data_task` from propagating
+to :attr:`~protocol.WebSocketCommonProtocol.close_connnection_task`.
+
+.. _backpressure:
+
+Backpressure
+------------
+
+.. note::
+
+ This section discusses backpressure from the perspective of a server but
+ the concept applies to clients symmetrically.
+
+With a naive implementation, if a server receives inputs faster than it can
+process them, or if it generates outputs faster than it can send them, data
+accumulates in buffers, eventually causing the server to run out of memory and
+crash.
+
+The solution to this problem is backpressure. Any part of the server that
+receives inputs faster than it can process them and send the outputs
+must propagate that information back to the previous part in the chain.
+
+``websockets`` is designed to make it easy to get backpressure right.
+
+For incoming data, ``websockets`` builds upon :class:`~asyncio.StreamReader`
+which propagates backpressure to its own buffer and to the TCP stream. Frames
+are parsed from the input stream and added to a bounded queue. If the queue
+fills up, parsing halts until the application reads a frame.
+
+For outgoing data, ``websockets`` builds upon :class:`~asyncio.StreamWriter`
+which implements flow control. If the output buffers grow too large, it waits
+until they're drained. That's why all APIs that write frames are asynchronous.
+
+Of course, it's still possible for an application to create its own unbounded
+buffers and break the backpressure. Be careful with queues.
+
+
+.. _buffers:
+
+Buffers
+-------
+
+.. note::
+
+ This section discusses buffers from the perspective of a server but it
+ applies to clients as well.
+
+An asynchronous systems works best when its buffers are almost always empty.
+
+For example, if a client sends data too fast for a server, the queue of
+incoming messages will be constantly full. The server will always be 32
+messages (by default) behind the client. This consumes memory and increases
+latency for no good reason. The problem is called bufferbloat.
+
+If buffers are almost always full and that problem cannot be solved by adding
+capacity — typically because the system is bottlenecked by the output and
+constantly regulated by backpressure — reducing the size of buffers minimizes
+negative consequences.
+
+By default ``websockets`` has rather high limits. You can decrease them
+according to your application's characteristics.
+
+Bufferbloat can happen at every level in the stack where there is a buffer.
+For each connection, the receiving side contains these buffers:
+
+- OS buffers: tuning them is an advanced optimization.
+- :class:`~asyncio.StreamReader` bytes buffer: the default limit is 64 KiB.
+ You can set another limit by passing a ``read_limit`` keyword argument to
+ :func:`~client.connect()` or :func:`~server.serve`.
+- Incoming messages :class:`~collections.deque`: its size depends both on
+ the size and the number of messages it contains. By default the maximum
+ UTF-8 encoded size is 1 MiB and the maximum number is 32. In the worst case,
+ after UTF-8 decoding, a single message could take up to 4 MiB of memory and
+ the overall memory consumption could reach 128 MiB. You should adjust these
+ limits by setting the ``max_size`` and ``max_queue`` keyword arguments of
+ :func:`~client.connect()` or :func:`~server.serve` according to your
+ application's requirements.
+
+For each connection, the sending side contains these buffers:
+
+- :class:`~asyncio.StreamWriter` bytes buffer: the default size is 64 KiB.
+ You can set another limit by passing a ``write_limit`` keyword argument to
+ :func:`~client.connect()` or :func:`~server.serve`.
+- OS buffers: tuning them is an advanced optimization.
+
+Concurrency
+-----------
+
+Awaiting any combination of :meth:`~protocol.WebSocketCommonProtocol.recv`,
+:meth:`~protocol.WebSocketCommonProtocol.send`,
+:meth:`~protocol.WebSocketCommonProtocol.close`
+:meth:`~protocol.WebSocketCommonProtocol.ping`, or
+:meth:`~protocol.WebSocketCommonProtocol.pong` concurrently is safe, including
+multiple calls to the same method, with one exception and one limitation.
+
+* **Only one coroutine can receive messages at a time.** This constraint
+ avoids non-deterministic behavior (and simplifies the implementation). If a
+ coroutine is awaiting :meth:`~protocol.WebSocketCommonProtocol.recv`,
+ awaiting it again in another coroutine raises :exc:`RuntimeError`.
+
+* **Sending a fragmented message forces serialization.** Indeed, the WebSocket
+ protocol doesn't support multiplexing messages. If a coroutine is awaiting
+ :meth:`~protocol.WebSocketCommonProtocol.send` to send a fragmented message,
+ awaiting it again in another coroutine waits until the first call completes.
+ This will be transparent in many cases. It may be a concern if the
+ fragmented message is generated slowly by an asynchronous iterator.
+
+Receiving frames is independent from sending frames. This isolates
+:meth:`~protocol.WebSocketCommonProtocol.recv`, which receives frames, from
+the other methods, which send frames.
+
+While the connection is open, each frame is sent with a single write. Combined
+with the concurrency model of :mod:`asyncio`, this enforces serialization. The
+only other requirement is to prevent interleaving other data frames in the
+middle of a fragmented message.
+
+After the connection is closed, sending a frame raises
+:exc:`~websockets.exceptions.ConnectionClosed`, which is safe.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/extensions.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/extensions.rst
new file mode 100644
index 0000000000..4000340906
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/extensions.rst
@@ -0,0 +1,87 @@
+Extensions
+==========
+
+.. currentmodule:: websockets
+
+The WebSocket protocol supports extensions_.
+
+At the time of writing, there's only one `registered extension`_, WebSocket
+Per-Message Deflate, specified in :rfc:`7692`.
+
+.. _extensions: https://tools.ietf.org/html/rfc6455#section-9
+.. _registered extension: https://www.iana.org/assignments/websocket/websocket.xhtml#extension-name
+
+Per-Message Deflate
+-------------------
+
+:func:`~server.serve()` and :func:`~client.connect` enable the Per-Message
+Deflate extension by default. You can disable this with ``compression=None``.
+
+You can also configure the Per-Message Deflate extension explicitly if you
+want to customize its parameters.
+
+.. _per-message-deflate-configuration-example:
+
+Here's an example on the server side::
+
+ import websockets
+ from websockets.extensions import permessage_deflate
+
+ websockets.serve(
+ ...,
+ extensions=[
+ permessage_deflate.ServerPerMessageDeflateFactory(
+ server_max_window_bits=11,
+ client_max_window_bits=11,
+ compress_settings={'memLevel': 4},
+ ),
+ ],
+ )
+
+Here's an example on the client side::
+
+ import websockets
+ from websockets.extensions import permessage_deflate
+
+ websockets.connect(
+ ...,
+ extensions=[
+ permessage_deflate.ClientPerMessageDeflateFactory(
+ server_max_window_bits=11,
+ client_max_window_bits=11,
+ compress_settings={'memLevel': 4},
+ ),
+ ],
+ )
+
+Refer to the API documentation of
+:class:`~extensions.permessage_deflate.ServerPerMessageDeflateFactory` and
+:class:`~extensions.permessage_deflate.ClientPerMessageDeflateFactory` for
+details.
+
+Writing an extension
+--------------------
+
+During the opening handshake, WebSocket clients and servers negotiate which
+extensions will be used with which parameters. Then each frame is processed by
+extensions before it's sent and after it's received.
+
+As a consequence writing an extension requires implementing several classes:
+
+1. Extension Factory: it negotiates parameters and instantiates the extension.
+ Clients and servers require separate extension factories with distinct APIs.
+
+2. Extension: it decodes incoming frames and encodes outgoing frames. If the
+ extension is symmetrical, clients and servers can use the same class.
+
+``websockets`` provides abstract base classes for extension factories and
+extensions.
+
+.. autoclass:: websockets.extensions.base.ServerExtensionFactory
+ :members:
+
+.. autoclass:: websockets.extensions.base.ClientExtensionFactory
+ :members:
+
+.. autoclass:: websockets.extensions.base.Extension
+ :members:
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq.rst
new file mode 100644
index 0000000000..cea3f53583
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq.rst
@@ -0,0 +1,261 @@
+FAQ
+===
+
+.. currentmodule:: websockets
+
+.. note::
+
+ Many questions asked in :mod:`websockets`' issue tracker are actually
+ about :mod:`asyncio`. Python's documentation about `developing with
+ asyncio`_ is a good complement.
+
+ .. _developing with asyncio: https://docs.python.org/3/library/asyncio-dev.html
+
+Server side
+-----------
+
+Why does the server close the connection after processing one message?
+......................................................................
+
+Your connection handler exits after processing one message. Write a loop to
+process multiple messages.
+
+For example, if your handler looks like this::
+
+ async def handler(websocket, path):
+ print(websocket.recv())
+
+change it like this::
+
+ async def handler(websocket, path):
+ async for message in websocket:
+ print(message)
+
+*Don't feel bad if this happens to you — it's the most common question in
+websockets' issue tracker :-)*
+
+Why can only one client connect at a time?
+..........................................
+
+Your connection handler blocks the event loop. Look for blocking calls.
+Any call that may take some time must be asynchronous.
+
+For example, if you have::
+
+ async def handler(websocket, path):
+ time.sleep(1)
+
+change it to::
+
+ async def handler(websocket, path):
+ await asyncio.sleep(1)
+
+This is part of learning asyncio. It isn't specific to websockets.
+
+See also Python's documentation about `running blocking code`_.
+
+.. _running blocking code: https://docs.python.org/3/library/asyncio-dev.html#running-blocking-code
+
+How do I get access HTTP headers, for example cookies?
+......................................................
+
+To access HTTP headers during the WebSocket handshake, you can override
+:attr:`~server.WebSocketServerProtocol.process_request`::
+
+ async def process_request(self, path, request_headers):
+ cookies = request_header["Cookie"]
+
+See
+
+Once the connection is established, they're available in
+:attr:`~protocol.WebSocketServerProtocol.request_headers`::
+
+ async def handler(websocket, path):
+ cookies = websocket.request_headers["Cookie"]
+
+How do I get the IP address of the client connecting to my server?
+..................................................................
+
+It's available in :attr:`~protocol.WebSocketCommonProtocol.remote_address`::
+
+ async def handler(websocket, path):
+ remote_ip = websocket.remote_address[0]
+
+How do I set which IP addresses my server listens to?
+.....................................................
+
+Look at the ``host`` argument of :meth:`~asyncio.loop.create_server`.
+
+:func:`serve` accepts the same arguments as
+:meth:`~asyncio.loop.create_server`.
+
+How do I close a connection properly?
+.....................................
+
+websockets takes care of closing the connection when the handler exits.
+
+How do I run a HTTP server and WebSocket server on the same port?
+.................................................................
+
+This isn't supported.
+
+Providing a HTTP server is out of scope for websockets. It only aims at
+providing a WebSocket server.
+
+There's limited support for returning HTTP responses with the
+:attr:`~server.WebSocketServerProtocol.process_request` hook.
+If you need more, pick a HTTP server and run it separately.
+
+Client side
+-----------
+
+How do I close a connection properly?
+.....................................
+
+The easiest is to use :func:`connect` as a context manager::
+
+ async with connect(...) as websocket:
+ ...
+
+How do I reconnect automatically when the connection drops?
+...........................................................
+
+See `issue 414`_.
+
+.. _issue 414: https://github.com/aaugustin/websockets/issues/414
+
+How do I disable TLS/SSL certificate verification?
+..................................................
+
+Look at the ``ssl`` argument of :meth:`~asyncio.loop.create_connection`.
+
+:func:`connect` accepts the same arguments as
+:meth:`~asyncio.loop.create_connection`.
+
+Both sides
+----------
+
+How do I do two things in parallel? How do I integrate with another coroutine?
+..............................................................................
+
+You must start two tasks, which the event loop will run concurrently. You can
+achieve this with :func:`asyncio.gather` or :func:`asyncio.wait`.
+
+This is also part of learning asyncio and not specific to websockets.
+
+Keep track of the tasks and make sure they terminate or you cancel them when
+the connection terminates.
+
+How do I create channels or topics?
+...................................
+
+websockets doesn't have built-in publish / subscribe for these use cases.
+
+Depending on the scale of your service, a simple in-memory implementation may
+do the job or you may need an external publish / subscribe component.
+
+What does ``ConnectionClosedError: code = 1006`` mean?
+......................................................
+
+If you're seeing this traceback in the logs of a server:
+
+.. code-block:: pytb
+
+ Error in connection handler
+ Traceback (most recent call last):
+ ...
+ asyncio.streams.IncompleteReadError: 0 bytes read on a total of 2 expected bytes
+
+ The above exception was the direct cause of the following exception:
+
+ Traceback (most recent call last):
+ ...
+ websockets.exceptions.ConnectionClosedError: code = 1006 (connection closed abnormally [internal]), no reason
+
+or if a client crashes with this traceback:
+
+.. code-block:: pytb
+
+ Traceback (most recent call last):
+ ...
+ ConnectionResetError: [Errno 54] Connection reset by peer
+
+ The above exception was the direct cause of the following exception:
+
+ Traceback (most recent call last):
+ ...
+ websockets.exceptions.ConnectionClosedError: code = 1006 (connection closed abnormally [internal]), no reason
+
+it means that the TCP connection was lost. As a consequence, the WebSocket
+connection was closed without receiving a close frame, which is abnormal.
+
+You can catch and handle :exc:`~exceptions.ConnectionClosed` to prevent it
+from being logged.
+
+There are several reasons why long-lived connections may be lost:
+
+* End-user devices tend to lose network connectivity often and unpredictably
+ because they can move out of wireless network coverage, get unplugged from
+ a wired network, enter airplane mode, be put to sleep, etc.
+* HTTP load balancers or proxies that aren't configured for long-lived
+ connections may terminate connections after a short amount of time, usually
+ 30 seconds.
+
+If you're facing a reproducible issue, :ref:`enable debug logs <debugging>` to
+see when and how connections are closed.
+
+Are there ``onopen``, ``onmessage``, ``onerror``, and ``onclose`` callbacks?
+............................................................................
+
+No, there aren't.
+
+websockets provides high-level, coroutine-based APIs. Compared to callbacks,
+coroutines make it easier to manage control flow in concurrent code.
+
+If you prefer callback-based APIs, you should use another library.
+
+Can I use ``websockets`` synchronously, without ``async`` / ``await``?
+......................................................................
+
+You can convert every asynchronous call to a synchronous call by wrapping it
+in ``asyncio.get_event_loop().run_until_complete(...)``.
+
+If this turns out to be impractical, you should use another library.
+
+Miscellaneous
+-------------
+
+How do I set a timeout on ``recv()``?
+.....................................
+
+Use :func:`~asyncio.wait_for`::
+
+ await asyncio.wait_for(websocket.recv(), timeout=10)
+
+This technique works for most APIs, except for asynchronous context managers.
+See `issue 574`_.
+
+.. _issue 574: https://github.com/aaugustin/websockets/issues/574
+
+How do I keep idle connections open?
+....................................
+
+websockets sends pings at 20 seconds intervals to keep the connection open.
+
+In closes the connection if it doesn't get a pong within 20 seconds.
+
+You can adjust this behavior with ``ping_interval`` and ``ping_timeout``.
+
+How do I respond to pings?
+..........................
+
+websockets takes care of responding to pings with pongs.
+
+Is there a Python 2 version?
+............................
+
+No, there isn't.
+
+websockets builds upon asyncio which requires Python 3.
+
+
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst
new file mode 100644
index 0000000000..1b2f85f0a4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst
@@ -0,0 +1,99 @@
+websockets
+==========
+
+|pypi-v| |pypi-pyversions| |pypi-l| |pypi-wheel| |circleci| |codecov|
+
+.. |pypi-v| image:: https://img.shields.io/pypi/v/websockets.svg
+ :target: https://pypi.python.org/pypi/websockets
+
+.. |pypi-pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg
+ :target: https://pypi.python.org/pypi/websockets
+
+.. |pypi-l| image:: https://img.shields.io/pypi/l/websockets.svg
+ :target: https://pypi.python.org/pypi/websockets
+
+.. |pypi-wheel| image:: https://img.shields.io/pypi/wheel/websockets.svg
+ :target: https://pypi.python.org/pypi/websockets
+
+.. |circleci| image:: https://img.shields.io/circleci/project/github/aaugustin/websockets.svg
+ :target: https://circleci.com/gh/aaugustin/websockets
+
+.. |codecov| image:: https://codecov.io/gh/aaugustin/websockets/branch/master/graph/badge.svg
+ :target: https://codecov.io/gh/aaugustin/websockets
+
+``websockets`` is a library for building WebSocket servers_ and clients_ in
+Python with a focus on correctness and simplicity.
+
+.. _servers: https://github.com/aaugustin/websockets/blob/master/example/server.py
+.. _clients: https://github.com/aaugustin/websockets/blob/master/example/client.py
+
+Built on top of :mod:`asyncio`, Python's standard asynchronous I/O framework,
+it provides an elegant coroutine-based API.
+
+Here's how a client sends and receives messages:
+
+.. literalinclude:: ../example/hello.py
+
+And here's an echo server:
+
+.. literalinclude:: ../example/echo.py
+
+Do you like it? Let's dive in!
+
+Tutorials
+---------
+
+If you're new to ``websockets``, this is the place to start.
+
+.. toctree::
+ :maxdepth: 2
+
+ intro
+ faq
+
+How-to guides
+-------------
+
+These guides will help you build and deploy a ``websockets`` application.
+
+.. toctree::
+ :maxdepth: 2
+
+ cheatsheet
+ deployment
+ extensions
+
+Reference
+---------
+
+Find all the details you could ask for, and then some.
+
+.. toctree::
+ :maxdepth: 2
+
+ api
+
+Discussions
+-----------
+
+Get a deeper understanding of how ``websockets`` is built and why.
+
+.. toctree::
+ :maxdepth: 2
+
+ design
+ limitations
+ security
+
+Project
+-------
+
+This is about websockets-the-project rather than websockets-the-software.
+
+.. toctree::
+ :maxdepth: 2
+
+ changelog
+ contributing
+ license
+ For enterprise <tidelift>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro.rst
new file mode 100644
index 0000000000..8be700239f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro.rst
@@ -0,0 +1,209 @@
+Getting started
+===============
+
+.. currentmodule:: websockets
+
+Requirements
+------------
+
+``websockets`` requires Python ≥ 3.6.1.
+
+You should use the latest version of Python if possible. If you're using an
+older version, be aware that for each minor version (3.x), only the latest
+bugfix release (3.x.y) is officially supported.
+
+Installation
+------------
+
+Install ``websockets`` with::
+
+ pip install websockets
+
+Basic example
+-------------
+
+.. _server-example:
+
+Here's a WebSocket server example.
+
+It reads a name from the client, sends a greeting, and closes the connection.
+
+.. literalinclude:: ../example/server.py
+ :emphasize-lines: 8,17
+
+.. _client-example:
+
+On the server side, ``websockets`` executes the handler coroutine ``hello``
+once for each WebSocket connection. It closes the connection when the handler
+coroutine returns.
+
+Here's a corresponding WebSocket client example.
+
+.. literalinclude:: ../example/client.py
+ :emphasize-lines: 8,10
+
+Using :func:`connect` as an asynchronous context manager ensures the
+connection is closed before exiting the ``hello`` coroutine.
+
+.. _secure-server-example:
+
+Secure example
+--------------
+
+Secure WebSocket connections improve confidentiality and also reliability
+because they reduce the risk of interference by bad proxies.
+
+The WSS protocol is to WS what HTTPS is to HTTP: the connection is encrypted
+with Transport Layer Security (TLS) — which is often referred to as Secure
+Sockets Layer (SSL). WSS requires TLS certificates like HTTPS.
+
+Here's how to adapt the server example to provide secure connections. See the
+documentation of the :mod:`ssl` module for configuring the context securely.
+
+.. literalinclude:: ../example/secure_server.py
+ :emphasize-lines: 19,23-25
+
+Here's how to adapt the client.
+
+.. literalinclude:: ../example/secure_client.py
+ :emphasize-lines: 10,15-18
+
+This client needs a context because the server uses a self-signed certificate.
+
+A client connecting to a secure WebSocket server with a valid certificate
+(i.e. signed by a CA that your Python installation trusts) can simply pass
+``ssl=True`` to :func:`connect` instead of building a context.
+
+Browser-based example
+---------------------
+
+Here's an example of how to run a WebSocket server and connect from a browser.
+
+Run this script in a console:
+
+.. literalinclude:: ../example/show_time.py
+
+Then open this HTML file in a browser.
+
+.. literalinclude:: ../example/show_time.html
+ :language: html
+
+Synchronization example
+-----------------------
+
+A WebSocket server can receive events from clients, process them to update the
+application state, and synchronize the resulting state across clients.
+
+Here's an example where any client can increment or decrement a counter.
+Updates are propagated to all connected clients.
+
+The concurrency model of :mod:`asyncio` guarantees that updates are
+serialized.
+
+Run this script in a console:
+
+.. literalinclude:: ../example/counter.py
+
+Then open this HTML file in several browsers.
+
+.. literalinclude:: ../example/counter.html
+ :language: html
+
+Common patterns
+---------------
+
+You will usually want to process several messages during the lifetime of a
+connection. Therefore you must write a loop. Here are the basic patterns for
+building a WebSocket server.
+
+Consumer
+........
+
+For receiving messages and passing them to a ``consumer`` coroutine::
+
+ async def consumer_handler(websocket, path):
+ async for message in websocket:
+ await consumer(message)
+
+In this example, ``consumer`` represents your business logic for processing
+messages received on the WebSocket connection.
+
+Iteration terminates when the client disconnects.
+
+Producer
+........
+
+For getting messages from a ``producer`` coroutine and sending them::
+
+ async def producer_handler(websocket, path):
+ while True:
+ message = await producer()
+ await websocket.send(message)
+
+In this example, ``producer`` represents your business logic for generating
+messages to send on the WebSocket connection.
+
+:meth:`~protocol.WebSocketCommonProtocol.send` raises a
+:exc:`~exceptions.ConnectionClosed` exception when the client disconnects,
+which breaks out of the ``while True`` loop.
+
+Both
+....
+
+You can read and write messages on the same connection by combining the two
+patterns shown above and running the two tasks in parallel::
+
+ async def handler(websocket, path):
+ consumer_task = asyncio.ensure_future(
+ consumer_handler(websocket, path))
+ producer_task = asyncio.ensure_future(
+ producer_handler(websocket, path))
+ done, pending = await asyncio.wait(
+ [consumer_task, producer_task],
+ return_when=asyncio.FIRST_COMPLETED,
+ )
+ for task in pending:
+ task.cancel()
+
+Registration
+............
+
+As shown in the synchronization example above, if you need to maintain a list
+of currently connected clients, you must register them when they connect and
+unregister them when they disconnect.
+
+::
+
+ connected = set()
+
+ async def handler(websocket, path):
+ # Register.
+ connected.add(websocket)
+ try:
+ # Implement logic here.
+ await asyncio.wait([ws.send("Hello!") for ws in connected])
+ await asyncio.sleep(10)
+ finally:
+ # Unregister.
+ connected.remove(websocket)
+
+This simplistic example keeps track of connected clients in memory. This only
+works as long as you run a single process. In a practical application, the
+handler may subscribe to some channels on a message broker, for example.
+
+That's all!
+-----------
+
+The design of the ``websockets`` API was driven by simplicity.
+
+You don't have to worry about performing the opening or the closing handshake,
+answering pings, or any other behavior required by the specification.
+
+``websockets`` handles all this under the hood so you don't have to.
+
+One more thing...
+-----------------
+
+``websockets`` provides an interactive client::
+
+ $ python -m websockets wss://echo.websocket.org/
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/license.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/license.rst
new file mode 100644
index 0000000000..842d3b07fc
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/license.rst
@@ -0,0 +1,4 @@
+License
+-------
+
+.. literalinclude:: ../LICENSE
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/lifecycle.graffle b/testing/web-platform/tests/tools/third_party/websockets/docs/lifecycle.graffle
new file mode 100644
index 0000000000..a8ab7ff09f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/lifecycle.graffle
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/lifecycle.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/lifecycle.svg
new file mode 100644
index 0000000000..0a9818d293
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/lifecycle.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="-14.3464565 112.653543 624.6929 372.69291" width="624.6929pt" height="372.69291pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.6.2 <dc:date>2018-07-29 15:25:34 +0000</dc:date></metadata><defs><font-face font-family="Courier New" font-size="12" panose-1="2 7 6 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="100.097656" slope="0" x-height="443.35938" cap-height="591.79688" ascent="832.51953" descent="-300.29297" font-weight="bold"><font-face-src><font-face-name name="CourierNewPS-BoldMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.8666667 0 L 0 0 M 0 -2.2 L 5.8666667 0 L 0 2.2" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Verdana" font-size="12" panose-1="2 11 6 4 3 5 4 4 2 4" units-per-em="1000" underline-position="-87.890625" underline-thickness="58.59375" slope="0" x-height="545.41016" cap-height="727.0508" ascent="1005.3711" descent="-209.96094" font-weight="500"><font-face-src><font-face-name name="Verdana"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><text transform="translate(19.173228 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="1.5138254" y="10" textLength="72.01172">CONNECTING</tspan></text><text transform="translate(160.90551 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="23.117341" y="10" textLength="28.804688">OPEN</tspan></text><text transform="translate(359.3307 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="12.315583" y="10" textLength="50.408203">CLOSING</tspan></text><text transform="translate(501.063 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="15.916169" y="10" textLength="43.20703">CLOSED</tspan></text><line x1="198.4252" y1="170.07874" x2="198.4252" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="396.8504" y1="170.07874" x2="396.8504" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="538.58267" y1="170.07874" x2="538.58267" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="56.692913" y1="170.07874" x2="56.692913" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><path d="M 240.94488 240.94488 L 411.02362 240.94488 C 418.85128 240.94488 425.19685 247.29045 425.19685 255.11811 L 425.19685 255.11811 C 425.19685 262.94577 418.85128 269.29134 411.02362 269.29134 L 240.94488 269.29134 C 233.11722 269.29134 226.77165 262.94577 226.77165 255.11811 L 226.77165 255.11811 C 226.77165 247.29045 233.11722 240.94488 240.94488 240.94488 Z" fill="#dadada"/><path d="M 240.94488 240.94488 L 411.02362 240.94488 C 418.85128 240.94488 425.19685 247.29045 425.19685 255.11811 L 425.19685 255.11811 C 425.19685 262.94577 418.85128 269.29134 411.02362 269.29134 L 240.94488 269.29134 C 233.11722 269.29134 226.77165 262.94577 226.77165 255.11811 L 226.77165 255.11811 C 226.77165 247.29045 233.11722 240.94488 240.94488 240.94488 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(226.77165 248.11811)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="52.40498" y="10" textLength="93.615234">transfer_data</tspan></text><path d="M 240.94488 354.3307 L 552.7559 354.3307 C 560.58356 354.3307 566.92913 360.67628 566.92913 368.50393 L 566.92913 368.50393 C 566.92913 376.3316 560.58356 382.67716 552.7559 382.67716 L 240.94488 382.67716 C 233.11722 382.67716 226.77165 376.3316 226.77165 368.50393 L 226.77165 368.50393 C 226.77165 360.67628 233.11722 354.3307 240.94488 354.3307 Z" fill="#dadada"/><path d="M 240.94488 354.3307 L 552.7559 354.3307 C 560.58356 354.3307 566.92913 360.67628 566.92913 368.50393 L 566.92913 368.50393 C 566.92913 376.3316 560.58356 382.67716 552.7559 382.67716 L 240.94488 382.67716 C 233.11722 382.67716 226.77165 376.3316 226.77165 368.50393 L 226.77165 368.50393 C 226.77165 360.67628 233.11722 354.3307 240.94488 354.3307 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(231.77165 361.50393)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="107.469364" y="10" textLength="115.21875">close_connection</tspan></text><path d="M 99.2126 184.25197 L 155.90551 184.25197 C 163.73317 184.25197 170.07874 190.59754 170.07874 198.4252 L 170.07874 198.4252 C 170.07874 206.25285 163.73317 212.59842 155.90551 212.59842 L 99.2126 212.59842 C 91.38494 212.59842 85.03937 206.25285 85.03937 198.4252 L 85.03937 198.4252 C 85.03937 190.59754 91.38494 184.25197 99.2126 184.25197 Z" fill="#6f6"/><path d="M 99.2126 184.25197 L 155.90551 184.25197 C 163.73317 184.25197 170.07874 190.59754 170.07874 198.4252 L 170.07874 198.4252 C 170.07874 206.25285 163.73317 212.59842 155.90551 212.59842 L 99.2126 212.59842 C 91.38494 212.59842 85.03937 206.25285 85.03937 198.4252 L 85.03937 198.4252 C 85.03937 190.59754 91.38494 184.25197 99.2126 184.25197 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 191.4252)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="12.315583" y="10" textLength="50.408203">connect</tspan></text><path d="M 240.94488 184.25197 L 496.063 184.25197 C 503.89065 184.25197 510.23622 190.59754 510.23622 198.4252 L 510.23622 198.4252 C 510.23622 206.25285 503.89065 212.59842 496.063 212.59842 L 240.94488 212.59842 C 233.11722 212.59842 226.77165 206.25285 226.77165 198.4252 L 226.77165 198.4252 C 226.77165 190.59754 233.11722 184.25197 240.94488 184.25197 Z" fill="#6f6"/><path d="M 240.94488 184.25197 L 496.063 184.25197 C 503.89065 184.25197 510.23622 190.59754 510.23622 198.4252 L 510.23622 198.4252 C 510.23622 206.25285 503.89065 212.59842 496.063 212.59842 L 240.94488 212.59842 C 233.11722 212.59842 226.77165 206.25285 226.77165 198.4252 L 226.77165 198.4252 C 226.77165 190.59754 233.11722 184.25197 240.94488 184.25197 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(231.77165 191.4252)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="17.912947" y="10" textLength="100.816406">recv / send / </tspan><tspan font-family="Courier New" font-size="12" font-weight="500" x="118.72935" y="10" textLength="93.615234">ping / pong /</tspan><tspan font-family="Courier New" font-size="12" font-weight="bold" x="212.34459" y="10" textLength="50.408203"> close </tspan></text><path d="M 170.07874 198.4252 L 183.97874 198.4252 L 198.4252 198.4252 L 198.4252 283.46457 L 198.4252 368.50393 L 212.87165 368.50393 L 215.37165 368.50393" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(75.86614 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="27.760296" y="12" textLength="52.083984">opening </tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="19.164593" y="27" textLength="58.02539">handshak</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="77.072796" y="27" textLength="7.1484375">e</tspan></text><text transform="translate(416.02362 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="19.182171" y="12" textLength="65.021484">connection</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="16.861858" y="27" textLength="69.66211">termination</tspan></text><text transform="translate(217.59842 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="41.03058" y="12" textLength="40.6875">data tr</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="81.507143" y="12" textLength="37.541016">ansfer</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="18.211245" y="27" textLength="116.625">&amp; closing handshak</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="134.71906" y="27" textLength="7.1484375">e</tspan></text><path d="M 425.19685 255.11811 L 439.09685 255.11811 L 453.5433 255.11811 L 453.5433 342.9307" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 240.94488 297.6378 L 411.02362 297.6378 C 418.85128 297.6378 425.19685 303.98336 425.19685 311.81102 L 425.19685 311.81102 C 425.19685 319.63868 418.85128 325.98425 411.02362 325.98425 L 240.94488 325.98425 C 233.11722 325.98425 226.77165 319.63868 226.77165 311.81102 L 226.77165 311.81102 C 226.77165 303.98336 233.11722 297.6378 240.94488 297.6378 Z" fill="#dadada"/><path d="M 240.94488 297.6378 L 411.02362 297.6378 C 418.85128 297.6378 425.19685 303.98336 425.19685 311.81102 L 425.19685 311.81102 C 425.19685 319.63868 418.85128 325.98425 411.02362 325.98425 L 240.94488 325.98425 C 233.11722 325.98425 226.77165 319.63868 226.77165 311.81102 L 226.77165 311.81102 C 226.77165 303.98336 233.11722 297.6378 240.94488 297.6378 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(226.77165 304.81102)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="48.804395" y="10" textLength="100.816406">keepalive_ping</tspan></text><line x1="198.4252" y1="255.11811" x2="214.62165" y2="255.11811" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="198.4252" y1="311.81102" x2="215.37165" y2="311.81102" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/></g></g></svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/limitations.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/limitations.rst
new file mode 100644
index 0000000000..bd6d32b2f6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/limitations.rst
@@ -0,0 +1,10 @@
+Limitations
+-----------
+
+The client doesn't attempt to guarantee that there is no more than one
+connection to a given IP address in a CONNECTING state.
+
+The client doesn't support connecting through a proxy.
+
+There is no way to fragment outgoing messages. A message is always sent in a
+single frame.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/protocol.graffle b/testing/web-platform/tests/tools/third_party/websockets/docs/protocol.graffle
new file mode 100644
index 0000000000..df76f49607
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/protocol.graffle
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/protocol.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/protocol.svg
new file mode 100644
index 0000000000..51bfd982be
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/protocol.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 624.34646 822.34646" width="624.34646pt" height="822.34646pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.6.2 <dc:date>2019-07-07 08:38:24 +0000</dc:date></metadata><defs><font-face font-family="Verdana" font-size="12" panose-1="2 11 6 4 3 5 4 4 2 4" units-per-em="1000" underline-position="-87.890625" underline-thickness="58.59375" slope="0" x-height="545.41016" cap-height="727.0508" ascent="1005.3711" descent="-209.96094" font-weight="500"><font-face-src><font-face-name name="Verdana"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 6 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="100.097656" slope="0" x-height="443.35938" cap-height="591.79688" ascent="832.51953" descent="-300.29297" font-weight="bold"><font-face-src><font-face-name name="CourierNewPS-BoldMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="10" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.8666667 0 L 0 0 M 0 -2.2 L 5.8666667 0 L 0 2.2" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><radialGradient cx="0" cy="0" r="1" id="Gradient" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="white"/><stop offset="1" stop-color="#a5a5a5"/></radialGradient><radialGradient id="Obj_Gradient" xl:href="#Gradient" gradientTransform="translate(311.81102 708.6614) scale(145.75703)"/><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker_2" viewBox="-1 -6 14 12" markerWidth="14" markerHeight="12" color="black"><g><path d="M 12 0 L 0 0 M 0 -4.5 L 12 0 L 0 4.5" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker_3" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.9253333 0 L 0 0 M 0 -2.222 L 5.9253333 0 L 0 2.222" fill="none" stroke="currentColor" stroke-width="1"/></g></marker></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><rect fill="white" width="1314" height="1698"/><g><title>Layer 1</title><rect x="28.346457" y="765.35433" width="566.92913" height="28.346457" fill="#6cf"/><rect x="28.346457" y="765.35433" width="566.92913" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 772.02755)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="228.50753" y="12" textLength="99.91406">remote endpoint</tspan></text><rect x="28.346457" y="85.03937" width="566.92913" height="566.92913" fill="white"/><rect x="28.346457" y="85.03937" width="566.92913" height="566.92913" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 90.03937)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="243.79171" y="12" textLength="51.333984">websock</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="295.00851" y="12" textLength="18.128906">ets</tspan><tspan font-family="Courier New" font-size="12" font-weight="500" x="195.65109" y="25" textLength="165.62695">WebSocketCommonProtocol</tspan></text><rect x="28.346457" y="28.346457" width="566.92913" height="28.346457" fill="#6f6"/><rect x="28.346457" y="28.346457" width="566.92913" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 35.019685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="230.0046" y="12" textLength="96.91992">application logic</tspan></text><path d="M 102.047243 586.77165 L 238.11023 586.77165 C 247.49858 586.77165 255.11811 596.93102 255.11811 609.4488 C 255.11811 621.9666 247.49858 632.12598 238.11023 632.12598 L 102.047243 632.12598 C 92.658897 632.12598 85.03937 621.9666 85.03937 609.4488 C 85.03937 596.93102 92.658897 586.77165 102.047243 586.77165" fill="#fc6"/><path d="M 102.047243 586.77165 L 238.11023 586.77165 C 247.49858 586.77165 255.11811 596.93102 255.11811 609.4488 C 255.11811 621.9666 247.49858 632.12598 238.11023 632.12598 L 102.047243 632.12598 C 92.658897 632.12598 85.03937 621.9666 85.03937 609.4488 C 85.03937 596.93102 92.658897 586.77165 102.047243 586.77165 M 238.11023 586.77165 C 228.72189 586.77165 221.10236 596.93102 221.10236 609.4488 C 221.10236 621.9666 228.72189 632.12598 238.11023 632.12598" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(125.33071 596.9488)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="14.896484" y="10" textLength="43.20703">reader</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x=".49414062" y="22" textLength="72.01172">StreamReader</tspan></text><path d="M 385.5118 586.77165 L 521.5748 586.77165 C 530.96315 586.77165 538.58267 596.93102 538.58267 609.4488 C 538.58267 621.9666 530.96315 632.12598 521.5748 632.12598 L 385.5118 632.12598 C 376.12346 632.12598 368.50393 621.9666 368.50393 609.4488 C 368.50393 596.93102 376.12346 586.77165 385.5118 586.77165" fill="#fc6"/><path d="M 385.5118 586.77165 L 521.5748 586.77165 C 530.96315 586.77165 538.58267 596.93102 538.58267 609.4488 C 538.58267 621.9666 530.96315 632.12598 521.5748 632.12598 L 385.5118 632.12598 C 376.12346 632.12598 368.50393 621.9666 368.50393 609.4488 C 368.50393 596.93102 376.12346 586.77165 385.5118 586.77165 M 521.5748 586.77165 C 512.18645 586.77165 504.56693 596.93102 504.56693 609.4488 C 504.56693 621.9666 512.18645 632.12598 521.5748 632.12598" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(408.79527 596.9488)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="14.896484" y="10" textLength="43.20703">writer</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x=".49414062" y="22" textLength="72.01172">StreamWriter</tspan></text><path d="M 481.88976 419.52756 L 481.88976 374.17323 C 481.88976 371.04378 469.19055 368.50393 453.5433 368.50393 C 437.89606 368.50393 425.19685 371.04378 425.19685 374.17323 L 425.19685 419.52756 C 425.19685 422.657 437.89606 425.19685 453.5433 425.19685 C 469.19055 425.19685 481.88976 422.657 481.88976 419.52756" fill="#fecc66"/><path d="M 481.88976 419.52756 L 481.88976 374.17323 C 481.88976 371.04378 469.19055 368.50393 453.5433 368.50393 C 437.89606 368.50393 425.19685 371.04378 425.19685 374.17323 L 425.19685 419.52756 C 425.19685 422.657 437.89606 425.19685 453.5433 425.19685 C 469.19055 425.19685 481.88976 422.657 481.88976 419.52756 M 481.88976 374.17323 C 481.88976 377.30267 469.19055 379.84252 453.5433 379.84252 C 437.89606 379.84252 425.19685 377.30267 425.19685 374.17323" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(429.19685 387.18504)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="6.343527" y="10" textLength="36.00586">pings</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="12.3445034" y="22" textLength="24.003906">dict</tspan></text><path d="M 85.039413 283.46457 L 255.11806 283.46457 C 270.7734 283.46457 283.46457 296.15573 283.46457 311.81107 L 283.46457 481.88972 C 283.46457 497.54506 270.7734 510.23622 255.11806 510.23622 L 85.039413 510.23622 C 69.384074 510.23622 56.692913 497.54506 56.692913 481.88972 L 56.692913 311.81107 C 56.692913 296.15573 69.384074 283.46457 85.039413 283.46457 Z" fill="#dadada"/><path d="M 85.039413 283.46457 L 255.11806 283.46457 C 270.7734 283.46457 283.46457 296.15573 283.46457 311.81107 L 283.46457 481.88972 C 283.46457 497.54506 270.7734 510.23622 255.11806 510.23622 L 85.039413 510.23622 C 69.384074 510.23622 56.692913 497.54506 56.692913 481.88972 L 56.692913 311.81107 C 56.692913 296.15573 69.384074 283.46457 85.039413 283.46457 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(61.692913 288.46457)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="43.57528" y="10" textLength="129.62109">transfer_data_task</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="96.383873" y="22" textLength="24.003906">Task</tspan></text><path d="M 297.6378 765.35433 L 297.6378 609.4488 L 255.11811 609.4488 L 269.01811 609.4488 L 266.51811 609.4488" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 368.50393 609.4488 L 354.60393 609.4488 L 325.98425 609.4488 L 325.98425 753.95433" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 207.03401 712.3154 C 161.22047 708.6614 179.48976 677.90097 252.5726 683.1496 C 259.35307 672.91835 344.33858 674.579 343.783 683.1496 C 397.0715 672.1877 465.17102 694.04553 419.49354 705.00744 C 474.30425 710.32206 418.80189 738.9565 373.8189 734.17322 C 370.2189 742.14584 289.80283 744.9358 282.74457 734.17322 C 237.20882 745.66715 142.25953 727.9946 207.03401 712.3154 Z" fill="url(#Obj_Gradient)"/><path d="M 207.03401 712.3154 C 161.22047 708.6614 179.48976 677.90097 252.5726 683.1496 C 259.35307 672.91835 344.33858 674.579 343.783 683.1496 C 397.0715 672.1877 465.17102 694.04553 419.49354 705.00744 C 474.30425 710.32206 418.80189 738.9565 373.8189 734.17322 C 370.2189 742.14584 289.80283 744.9358 282.74457 734.17322 C 237.20882 745.66715 142.25953 727.9946 207.03401 712.3154 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(217.59842 701.1614)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="69.81416" y="12" textLength="48.796875">network</tspan></text><rect x="85.03937" y="453.5433" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="453.5433" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 460.71653)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="44.03351" y="10" textLength="72.01172">read_frame</tspan></text><rect x="85.03937" y="396.8504" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="396.8504" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 404.02362)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="26.03058" y="10" textLength="108.01758">read_data_frame</tspan></text><rect x="85.03937" y="340.15748" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="340.15748" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 347.3307)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="36.832338" y="10" textLength="86.41406">read_message</tspan></text><text transform="translate(178.07874 490.563)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="30.004883">bytes</tspan></text><text transform="translate(178.07874 433.87008)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="36.00586">frames</tspan></text><text transform="translate(178.07874 371.67716)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="24.003906">data</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="19" textLength="36.00586">frames</tspan></text><rect x="368.50393" y="510.23622" width="170.07874" height="28.346457" fill="#ff6"/><rect x="368.50393" y="510.23622" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 517.40945)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="40.432924" y="10" textLength="79.21289">write_frame</tspan></text><path d="M 85.03937 609.4488 L 71.13937 609.4488 L 56.692913 609.4488 L 56.692913 595.2756 L 56.692913 566.92913 L 113.385826 566.92913 L 170.07874 566.92913 L 170.07874 495.78976 L 170.07874 494.03976" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 453.5433 539.33267 L 453.5433 552.48267 L 453.5433 566.92913 L 510.23622 566.92913 L 569.76378 566.92913 L 569.76378 595.2756 L 569.76378 609.4488 L 552.48267 609.4488 L 549.98267 609.4488" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="453.5433" x2="170.07874" y2="437.34685" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="396.8504" x2="170.07874" y2="380.65393" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 102.047243 204.09449 L 238.11023 204.09449 C 247.49858 204.09449 255.11811 214.25386 255.11811 226.77165 C 255.11811 239.28945 247.49858 249.44882 238.11023 249.44882 L 102.047243 249.44882 C 92.658897 249.44882 85.03937 239.28945 85.03937 226.77165 C 85.03937 214.25386 92.658897 204.09449 102.047243 204.09449" fill="#fc6"/><path d="M 102.047243 204.09449 L 238.11023 204.09449 C 247.49858 204.09449 255.11811 214.25386 255.11811 226.77165 C 255.11811 239.28945 247.49858 249.44882 238.11023 249.44882 L 102.047243 249.44882 C 92.658897 249.44882 85.03937 239.28945 85.03937 226.77165 C 85.03937 214.25386 92.658897 204.09449 102.047243 204.09449 M 238.11023 204.09449 C 228.72189 204.09449 221.10236 214.25386 221.10236 226.77165 C 221.10236 239.28945 228.72189 249.44882 238.11023 249.44882" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(132.33071 214.27165)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x=".1953125" y="10" textLength="57.609375">messages</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="13.997559" y="22" textLength="30.004883">deque</tspan></text><path d="M 255.11811 354.3307 L 269.01811 354.3307 L 297.6378 354.3307 L 297.6378 328.8189 L 297.6378 226.77165 L 269.01811 226.77165 L 266.51811 226.77165" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><rect x="85.03937" y="141.73228" width="170.07874" height="28.346457" fill="#cf6"/><rect x="85.03937" y="141.73228" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="65.637026" y="10" textLength="28.804688">recv</tspan></text><path d="M 85.03937 226.77165 L 71.13937 226.77165 L 42.519685 226.77165 L 42.519685 209.76378 L 42.519685 155.90551 L 71.13937 155.90551 L 73.63937 155.90551" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="141.73228" x2="170.07874" y2="68.092913" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="453.5433" y1="56.692913" x2="453.5433" y2="130.33228" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="467.71653" y1="56.692913" x2="467.71653" y2="187.8752" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="481.88976" y1="56.692913" x2="481.88976" y2="244.56811" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="496.063" y1="56.692913" x2="496.063" y2="300.32302" marker-end="url(#StickArrow_Marker_3)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><rect x="368.50393" y="141.73228" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="141.73228" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="65.637026" y="10" textLength="28.804688">send</tspan></text><rect x="368.50393" y="198.4252" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="198.4252" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(373.50393 205.59842)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="65.637026" y="10" textLength="28.804688">ping</tspan></text><rect x="368.50393" y="255.11811" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="255.11811" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(373.50393 262.29134)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="65.637026" y="10" textLength="28.804688">pong</tspan></text><rect x="368.50393" y="311.81102" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="311.81102" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 318.98425)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="62.03644" y="10" textLength="36.00586">close</tspan></text><path d="M 538.58267 155.90551 L 552.48267 155.90551 L 566.92913 155.90551 L 566.92913 481.88976 L 453.5433 481.88976 L 453.5433 496.33622 L 453.5433 498.08622" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="538.58267" y1="212.59842" x2="566.92913" y2="212.59842" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="538.58267" y1="269.29134" x2="566.92913" y2="269.29134" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="538.58267" y1="325.98425" x2="566.92913" y2="325.98425" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 255.86811 411.02362 L 262.61811 411.02362 L 340.15748 411.02362 L 340.15748 481.88976 L 453.5433 481.88976" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(291.94527 399.02362)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="42.006836">control</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="21" textLength="36.00586">frames</tspan></text><line x1="340.15748" y1="411.02362" x2="414.64685" y2="411.02362" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><path d="M 368.50393 212.59842 L 361.75393 212.59842 L 340.15748 212.59842 L 340.15748 340.15748 L 340.15748 382.67716" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(461.5433 547.2559)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="30.004883">bytes</tspan></text><text transform="translate(461.5433 490.563)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="36.00586">frames</tspan></text><line x1="340.15748" y1="382.67716" x2="414.64685" y2="382.67716" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/></g></g></svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt
new file mode 100644
index 0000000000..0eaf94fbe8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt
@@ -0,0 +1,4 @@
+sphinx
+sphinx-autodoc-typehints
+sphinxcontrib-spelling
+sphinxcontrib-trio
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/security.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/security.rst
new file mode 100644
index 0000000000..e9acf0629c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/security.rst
@@ -0,0 +1,39 @@
+Security
+========
+
+Encryption
+----------
+
+For production use, a server should require encrypted connections.
+
+See this example of :ref:`encrypting connections with TLS
+<secure-server-example>`.
+
+Memory use
+----------
+
+.. warning::
+
+ An attacker who can open an arbitrary number of connections will be able
+ to perform a denial of service by memory exhaustion. If you're concerned
+ by denial of service attacks, you must reject suspicious connections
+ before they reach ``websockets``, typically in a reverse proxy.
+
+With the default settings, opening a connection uses 325 KiB of memory.
+
+Sending some highly compressed messages could use up to 128 MiB of memory
+with an amplification factor of 1000 between network traffic and memory use.
+
+Configuring a server to :ref:`optimize memory usage <memory-usage>` will
+improve security in addition to improving performance.
+
+Other limits
+------------
+
+``websockets`` implements additional limits on the amount of data it accepts
+in order to minimize exposure to security vulnerabilities.
+
+In the opening handshake, ``websockets`` limits the number of HTTP headers to
+256 and the size of an individual header to 4096 bytes. These limits are 10 to
+20 times larger than what's expected in standard use cases. They're hard-coded.
+If you need to change them, monkey-patch the constants in ``websockets.http``.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt b/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt
new file mode 100644
index 0000000000..1eacc491df
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt
@@ -0,0 +1,39 @@
+attr
+augustin
+Auth
+awaitable
+aymeric
+backpressure
+Backpressure
+Bitcoin
+bufferbloat
+Bufferbloat
+bugfix
+bytestring
+bytestrings
+changelog
+cryptocurrency
+daemonize
+fractalideas
+iterable
+keepalive
+KiB
+lifecycle
+Lifecycle
+MiB
+nginx
+permessage
+pong
+pongs
+Pythonic
+serializers
+subclassing
+subprotocol
+subprotocols
+TLS
+Unparse
+uple
+username
+websocket
+WebSocket
+websockets
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/tidelift.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/tidelift.rst
new file mode 100644
index 0000000000..43b457aafa
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/tidelift.rst
@@ -0,0 +1,112 @@
+websockets for enterprise
+=========================
+
+Available as part of the Tidelift Subscription
+----------------------------------------------
+
+.. image:: _static/tidelift.png
+ :height: 150px
+ :width: 150px
+ :align: left
+
+Tidelift is working with the maintainers of websockets and thousands of other
+open source projects to deliver commercial support and maintenance for the
+open source dependencies you use to build your applications. Save time, reduce
+risk, and improve code health, while paying the maintainers of the exact
+dependencies you use.
+
+.. raw:: html
+
+ <style type="text/css">
+ .tidelift-links {
+ display: flex;
+ justify-content: center;
+ }
+ @media only screen and (max-width: 600px) {
+ .tidelift-links {
+ flex-direction: column;
+ }
+ }
+ .tidelift-links a {
+ border: thin solid #f6914d;
+ border-radius: 0.25em;
+ font-family: Verdana, sans-serif;
+ font-size: 15px;
+ margin: 0.5em 2em;
+ padding: 0.5em 2em;
+ text-align: center;
+ text-decoration: none;
+ text-transform: uppercase;
+ }
+ .tidelift-links a.tidelift-links__learn-more {
+ background-color: white;
+ color: #f6914d;
+ }
+ .tidelift-links a.tidelift-links__request-a-demo {
+ background-color: #f6914d;
+ color: white;
+ }
+ </style>
+
+ <div class="tidelift-links">
+ <a class="tidelift-links__learn-more" href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Learn more</a>
+ <a class="tidelift-links__request-a-demo" href="https://tidelift.com/subscription/request-a-demo?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Request a demo</a>
+ </div>
+
+Enterprise-ready open source software—managed for you
+-----------------------------------------------------
+
+The Tidelift Subscription is a managed open source subscription for
+application dependencies covering millions of open source projects across
+JavaScript, Python, Java, PHP, Ruby, .NET, and more.
+
+Your subscription includes:
+
+* **Security updates**
+
+ * Tidelift’s security response team coordinates patches for new breaking
+ security vulnerabilities and alerts immediately through a private channel,
+ so your software supply chain is always secure.
+
+* **Licensing verification and indemnification**
+
+ * Tidelift verifies license information to enable easy policy enforcement
+ and adds intellectual property indemnification to cover creators and users
+ in case something goes wrong. You always have a 100% up-to-date bill of
+ materials for your dependencies to share with your legal team, customers,
+ or partners.
+
+* **Maintenance and code improvement**
+
+ * Tidelift ensures the software you rely on keeps working as long as you
+ need it to work. Your managed dependencies are actively maintained and we
+ recruit additional maintainers where required.
+
+* **Package selection and version guidance**
+
+ * We help you choose the best open source packages from the start—and then
+ guide you through updates to stay on the best releases as new issues
+ arise.
+
+* **Roadmap input**
+
+ * Take a seat at the table with the creators behind the software you use.
+ Tidelift’s participating maintainers earn more income as their software is
+ used by more subscribers, so they’re interested in knowing what you need.
+
+* **Tooling and cloud integration**
+
+ * Tidelift works with GitHub, GitLab, BitBucket, and more. We support every
+ cloud platform (and other deployment targets, too).
+
+The end result? All of the capabilities you expect from commercial-grade
+software, for the full breadth of open source you use. That means less time
+grappling with esoteric open source trivia, and more time building your own
+applications—and your business.
+
+.. raw:: html
+
+ <div class="tidelift-links">
+ <a class="tidelift-links__learn-more" href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Learn more</a>
+ <a class="tidelift-links__request-a-demo" href="https://tidelift.com/subscription/request-a-demo?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Request a demo</a>
+ </div>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/basic_auth_client.py b/testing/web-platform/tests/tools/third_party/websockets/example/basic_auth_client.py
new file mode 100755
index 0000000000..cc94dbe4b4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/basic_auth_client.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+
+# WS client example with HTTP Basic Authentication
+
+import asyncio
+import websockets
+
+async def hello():
+ uri = "ws://mary:p@ssw0rd@localhost:8765"
+ async with websockets.connect(uri) as websocket:
+ greeting = await websocket.recv()
+ print(greeting)
+
+asyncio.get_event_loop().run_until_complete(hello())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/basic_auth_server.py b/testing/web-platform/tests/tools/third_party/websockets/example/basic_auth_server.py
new file mode 100755
index 0000000000..6740d57989
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/basic_auth_server.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+# Server example with HTTP Basic Authentication over TLS
+
+import asyncio
+import websockets
+
+async def hello(websocket, path):
+ greeting = f"Hello {websocket.username}!"
+ await websocket.send(greeting)
+
+start_server = websockets.serve(
+ hello, "localhost", 8765,
+ create_protocol=websockets.basic_auth_protocol_factory(
+ realm="example", credentials=("mary", "p@ssw0rd")
+ ),
+)
+
+asyncio.get_event_loop().run_until_complete(start_server)
+asyncio.get_event_loop().run_forever()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/client.py b/testing/web-platform/tests/tools/third_party/websockets/example/client.py
new file mode 100755
index 0000000000..4f969c478a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/client.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+# WS client example
+
+import asyncio
+import websockets
+
+async def hello():
+ uri = "ws://localhost:8765"
+ async with websockets.connect(uri) as websocket:
+ name = input("What's your name? ")
+
+ await websocket.send(name)
+ print(f"> {name}")
+
+ greeting = await websocket.recv()
+ print(f"< {greeting}")
+
+asyncio.get_event_loop().run_until_complete(hello())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/counter.html b/testing/web-platform/tests/tools/third_party/websockets/example/counter.html
new file mode 100644
index 0000000000..6310c4a16d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/counter.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>WebSocket demo</title>
+ <style type="text/css">
+ body {
+ font-family: "Courier New", sans-serif;
+ text-align: center;
+ }
+ .buttons {
+ font-size: 4em;
+ display: flex;
+ justify-content: center;
+ }
+ .button, .value {
+ line-height: 1;
+ padding: 2rem;
+ margin: 2rem;
+ border: medium solid;
+ min-height: 1em;
+ min-width: 1em;
+ }
+ .button {
+ cursor: pointer;
+ user-select: none;
+ }
+ .minus {
+ color: red;
+ }
+ .plus {
+ color: green;
+ }
+ .value {
+ min-width: 2em;
+ }
+ .state {
+ font-size: 2em;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="buttons">
+ <div class="minus button">-</div>
+ <div class="value">?</div>
+ <div class="plus button">+</div>
+ </div>
+ <div class="state">
+ <span class="users">?</span> online
+ </div>
+ <script>
+ var minus = document.querySelector('.minus'),
+ plus = document.querySelector('.plus'),
+ value = document.querySelector('.value'),
+ users = document.querySelector('.users'),
+ websocket = new WebSocket("ws://127.0.0.1:6789/");
+ minus.onclick = function (event) {
+ websocket.send(JSON.stringify({action: 'minus'}));
+ }
+ plus.onclick = function (event) {
+ websocket.send(JSON.stringify({action: 'plus'}));
+ }
+ websocket.onmessage = function (event) {
+ data = JSON.parse(event.data);
+ switch (data.type) {
+ case 'state':
+ value.textContent = data.value;
+ break;
+ case 'users':
+ users.textContent = (
+ data.count.toString() + " user" +
+ (data.count == 1 ? "" : "s"));
+ break;
+ default:
+ console.error(
+ "unsupported event", data);
+ }
+ };
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/counter.py b/testing/web-platform/tests/tools/third_party/websockets/example/counter.py
new file mode 100755
index 0000000000..dbbbe59358
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/counter.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+
+# WS server example that synchronizes state across clients
+
+import asyncio
+import json
+import logging
+import websockets
+
+logging.basicConfig()
+
+STATE = {"value": 0}
+
+USERS = set()
+
+
+def state_event():
+ return json.dumps({"type": "state", **STATE})
+
+
+def users_event():
+ return json.dumps({"type": "users", "count": len(USERS)})
+
+
+async def notify_state():
+ if USERS: # asyncio.wait doesn't accept an empty list
+ message = state_event()
+ await asyncio.wait([user.send(message) for user in USERS])
+
+
+async def notify_users():
+ if USERS: # asyncio.wait doesn't accept an empty list
+ message = users_event()
+ await asyncio.wait([user.send(message) for user in USERS])
+
+
+async def register(websocket):
+ USERS.add(websocket)
+ await notify_users()
+
+
+async def unregister(websocket):
+ USERS.remove(websocket)
+ await notify_users()
+
+
+async def counter(websocket, path):
+ # register(websocket) sends user_event() to websocket
+ await register(websocket)
+ try:
+ await websocket.send(state_event())
+ async for message in websocket:
+ data = json.loads(message)
+ if data["action"] == "minus":
+ STATE["value"] -= 1
+ await notify_state()
+ elif data["action"] == "plus":
+ STATE["value"] += 1
+ await notify_state()
+ else:
+ logging.error("unsupported event: {}", data)
+ finally:
+ await unregister(websocket)
+
+
+start_server = websockets.serve(counter, "localhost", 6789)
+
+asyncio.get_event_loop().run_until_complete(start_server)
+asyncio.get_event_loop().run_forever()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/echo.py b/testing/web-platform/tests/tools/third_party/websockets/example/echo.py
new file mode 100755
index 0000000000..b7ca38d321
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/echo.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+
+import asyncio
+import websockets
+
+async def echo(websocket, path):
+ async for message in websocket:
+ await websocket.send(message)
+
+start_server = websockets.serve(echo, "localhost", 8765)
+
+asyncio.get_event_loop().run_until_complete(start_server)
+asyncio.get_event_loop().run_forever()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/health_check_server.py b/testing/web-platform/tests/tools/third_party/websockets/example/health_check_server.py
new file mode 100755
index 0000000000..417063fce7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/health_check_server.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+
+# WS echo server with HTTP endpoint at /health/
+
+import asyncio
+import http
+import websockets
+
+async def health_check(path, request_headers):
+ if path == "/health/":
+ return http.HTTPStatus.OK, [], b"OK\n"
+
+async def echo(websocket, path):
+ async for message in websocket:
+ await websocket.send(message)
+
+start_server = websockets.serve(
+ echo, "localhost", 8765, process_request=health_check
+)
+
+asyncio.get_event_loop().run_until_complete(start_server)
+asyncio.get_event_loop().run_forever()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/hello.py b/testing/web-platform/tests/tools/third_party/websockets/example/hello.py
new file mode 100755
index 0000000000..6c9c839d82
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/hello.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+
+import asyncio
+import websockets
+
+async def hello():
+ uri = "ws://localhost:8765"
+ async with websockets.connect(uri) as websocket:
+ await websocket.send("Hello world!")
+ await websocket.recv()
+
+asyncio.get_event_loop().run_until_complete(hello())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/localhost.pem b/testing/web-platform/tests/tools/third_party/websockets/example/localhost.pem
new file mode 100644
index 0000000000..f9a30ba8f6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/localhost.pem
@@ -0,0 +1,48 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDG8iDak4UBpurI
+TWjSfqJ0YVG/S56nhswehupCaIzu0xQ8wqPSs36h5t1jMexJPZfvwyvFjcV+hYpj
+LMM0wMJPx9oBQEe0bsmlC66e8aF0UpSQw1aVfYoxA9BejgEyrFNE7cRbQNYFEb/5
+3HfqZKdEQA2fgQSlZ0RTRmLrD+l72iO5o2xl5bttXpqYZB2XOkyO79j/xWdu9zFE
+sgZJ5ysWbqoRAGgnxjdYYr9DARd8bIE/hN3SW7mDt5v4LqCIhGn1VmrwtT3d5AuG
+QPz4YEbm0t6GOlmFjIMYH5Y7pALRVfoJKRj6DGNIR1JicL+wqLV66kcVnj8WKbla
+20i7fR7NAgMBAAECggEAG5yvgqbG5xvLqlFUIyMAWTbIqcxNEONcoUAIc38fUGZr
+gKNjKXNQOBha0dG0AdZSqCxmftzWdGEEfA9SaJf4YCpUz6ekTB60Tfv5GIZg6kwr
+4ou6ELWD4Jmu6fC7qdTRGdgGUMQG8F0uT/eRjS67KHXbbi/x/SMAEK7MO+PRfCbj
++JGzS9Ym9mUweINPotgjHdDGwwd039VWYS+9A+QuNK27p3zq4hrWRb4wshSC8fKy
+oLoe4OQt81aowpX9k6mAU6N8vOmP8/EcQHYC+yFIIDZB2EmDP07R1LUEH3KJnzo7
+plCK1/kYPhX0a05cEdTpXdKa74AlvSRkS11sGqfUAQKBgQDj1SRv0AUGsHSA0LWx
+a0NT1ZLEXCG0uqgdgh0sTqIeirQsPROw3ky4lH5MbjkfReArFkhHu3M6KoywEPxE
+wanSRh/t1qcNjNNZUvFoUzAKVpb33RLkJppOTVEWPt+wtyDlfz1ZAXzMV66tACrx
+H2a3v0ZWUz6J+x/dESH5TTNL4QKBgQDfirmknp408pwBE+bulngKy0QvU09En8H0
+uvqr8q4jCXqJ1tXon4wsHg2yF4Fa37SCpSmvONIDwJvVWkkYLyBHKOns/fWCkW3n
+hIcYx0q2jgcoOLU0uoaM9ArRXhIxoWqV/KGkQzN+3xXC1/MxZ5OhyxBxfPCPIYIN
+YN3M1t/QbQKBgDImhsC+D30rdlmsl3IYZFed2ZKznQ/FTqBANd+8517FtWdPgnga
+VtUCitKUKKrDnNafLwXrMzAIkbNn6b/QyWrp2Lln2JnY9+TfpxgJx7de3BhvZ2sl
+PC4kQsccy+yAQxOBcKWY+Dmay251bP5qpRepWPhDlq6UwqzMyqev4KzBAoGAWDMi
+IEO9ZGK9DufNXCHeZ1PgKVQTmJ34JxmHQkTUVFqvEKfFaq1Y3ydUfAouLa7KSCnm
+ko42vuhGFB41bOdbMvh/o9RoBAZheNGfhDVN002ioUoOpSlbYU4A3q7hOtfXeCpf
+lLI3JT3cFi6ic8HMTDAU4tJLEA5GhATOPr4hPNkCgYB8jTYGcLvoeFaLEveg0kS2
+cz6ZXGLJx5m1AOQy5g9FwGaW+10lr8TF2k3AldwoiwX0R6sHAf/945aGU83ms5v9
+PB9/x66AYtSRUos9MwB4y1ur4g6FiXZUBgTJUqzz2nehPCyGjYhh49WucjszqcjX
+chS1bKZOY+1knWq8xj5Qyg==
+-----END PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIDTTCCAjWgAwIBAgIJAOjte6l+03jvMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV
+BAYTAkZSMQ4wDAYDVQQHDAVQYXJpczEZMBcGA1UECgwQQXltZXJpYyBBdWd1c3Rp
+bjESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE4MDUwNTE2NTkyOVoYDzIwNjAwNTA0
+MTY1OTI5WjBMMQswCQYDVQQGEwJGUjEOMAwGA1UEBwwFUGFyaXMxGTAXBgNVBAoM
+EEF5bWVyaWMgQXVndXN0aW4xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAMbyINqThQGm6shNaNJ+onRhUb9LnqeGzB6G
+6kJojO7TFDzCo9KzfqHm3WMx7Ek9l+/DK8WNxX6FimMswzTAwk/H2gFAR7RuyaUL
+rp7xoXRSlJDDVpV9ijED0F6OATKsU0TtxFtA1gURv/ncd+pkp0RADZ+BBKVnRFNG
+YusP6XvaI7mjbGXlu21emphkHZc6TI7v2P/FZ273MUSyBknnKxZuqhEAaCfGN1hi
+v0MBF3xsgT+E3dJbuYO3m/guoIiEafVWavC1Pd3kC4ZA/PhgRubS3oY6WYWMgxgf
+ljukAtFV+gkpGPoMY0hHUmJwv7CotXrqRxWePxYpuVrbSLt9Hs0CAwEAAaMwMC4w
+LAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0G
+CSqGSIb3DQEBCwUAA4IBAQC9TsTxTEvqHPUS6sfvF77eG0D6HLOONVN91J+L7LiX
+v3bFeS1xbUS6/wIxZi5EnAt/te5vaHk/5Q1UvznQP4j2gNoM6lH/DRkSARvRitVc
+H0qN4Xp2Yk1R9VEx4ZgArcyMpI+GhE4vJRx1LE/hsuAzw7BAdsTt9zicscNg2fxO
+3ao/eBcdaC6n9aFYdE6CADMpB1lCX2oWNVdj6IavQLu7VMc+WJ3RKncwC9th+5OP
+ISPvkVZWf25rR2STmvvb0qEm3CZjk4Xd7N+gxbKKUvzEgPjrLSWzKKJAWHjCLugI
+/kQqhpjWVlTbtKzWz5bViqCjSbrIPpU2MgG9AUV9y3iV
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/secure_client.py b/testing/web-platform/tests/tools/third_party/websockets/example/secure_client.py
new file mode 100755
index 0000000000..54971b9847
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/secure_client.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+
+# WSS (WS over TLS) client example, with a self-signed certificate
+
+import asyncio
+import pathlib
+import ssl
+import websockets
+
+ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+localhost_pem = pathlib.Path(__file__).with_name("localhost.pem")
+ssl_context.load_verify_locations(localhost_pem)
+
+async def hello():
+ uri = "wss://localhost:8765"
+ async with websockets.connect(
+ uri, ssl=ssl_context
+ ) as websocket:
+ name = input("What's your name? ")
+
+ await websocket.send(name)
+ print(f"> {name}")
+
+ greeting = await websocket.recv()
+ print(f"< {greeting}")
+
+asyncio.get_event_loop().run_until_complete(hello())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/secure_server.py b/testing/web-platform/tests/tools/third_party/websockets/example/secure_server.py
new file mode 100755
index 0000000000..2a00bdb504
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/secure_server.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+
+# WSS (WS over TLS) server example, with a self-signed certificate
+
+import asyncio
+import pathlib
+import ssl
+import websockets
+
+async def hello(websocket, path):
+ name = await websocket.recv()
+ print(f"< {name}")
+
+ greeting = f"Hello {name}!"
+
+ await websocket.send(greeting)
+ print(f"> {greeting}")
+
+ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+localhost_pem = pathlib.Path(__file__).with_name("localhost.pem")
+ssl_context.load_cert_chain(localhost_pem)
+
+start_server = websockets.serve(
+ hello, "localhost", 8765, ssl=ssl_context
+)
+
+asyncio.get_event_loop().run_until_complete(start_server)
+asyncio.get_event_loop().run_forever()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/server.py b/testing/web-platform/tests/tools/third_party/websockets/example/server.py
new file mode 100755
index 0000000000..c8ab69971b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/server.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+# WS server example
+
+import asyncio
+import websockets
+
+async def hello(websocket, path):
+ name = await websocket.recv()
+ print(f"< {name}")
+
+ greeting = f"Hello {name}!"
+
+ await websocket.send(greeting)
+ print(f"> {greeting}")
+
+start_server = websockets.serve(hello, "localhost", 8765)
+
+asyncio.get_event_loop().run_until_complete(start_server)
+asyncio.get_event_loop().run_forever()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/show_time.html b/testing/web-platform/tests/tools/third_party/websockets/example/show_time.html
new file mode 100644
index 0000000000..721f44264e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/show_time.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>WebSocket demo</title>
+ </head>
+ <body>
+ <script>
+ var ws = new WebSocket("ws://127.0.0.1:5678/"),
+ messages = document.createElement('ul');
+ ws.onmessage = function (event) {
+ var messages = document.getElementsByTagName('ul')[0],
+ message = document.createElement('li'),
+ content = document.createTextNode(event.data);
+ message.appendChild(content);
+ messages.appendChild(message);
+ };
+ document.body.appendChild(messages);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/show_time.py b/testing/web-platform/tests/tools/third_party/websockets/example/show_time.py
new file mode 100755
index 0000000000..e5d6ac9aa3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/show_time.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+# WS server that sends messages at random intervals
+
+import asyncio
+import datetime
+import random
+import websockets
+
+async def time(websocket, path):
+ while True:
+ now = datetime.datetime.utcnow().isoformat() + "Z"
+ await websocket.send(now)
+ await asyncio.sleep(random.random() * 3)
+
+start_server = websockets.serve(time, "127.0.0.1", 5678)
+
+asyncio.get_event_loop().run_until_complete(start_server)
+asyncio.get_event_loop().run_forever()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/shutdown.py b/testing/web-platform/tests/tools/third_party/websockets/example/shutdown.py
new file mode 100755
index 0000000000..86846abe73
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/shutdown.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+
+import asyncio
+import signal
+import websockets
+
+async def echo(websocket, path):
+ async for message in websocket:
+ await websocket.send(message)
+
+async def echo_server(stop):
+ async with websockets.serve(echo, "localhost", 8765):
+ await stop
+
+loop = asyncio.get_event_loop()
+
+# The stop condition is set when receiving SIGTERM.
+stop = loop.create_future()
+loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
+
+# Run the server until the stop condition is met.
+loop.run_until_complete(echo_server(stop))
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/unix_client.py b/testing/web-platform/tests/tools/third_party/websockets/example/unix_client.py
new file mode 100755
index 0000000000..577135b3db
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/unix_client.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+# WS client example connecting to a Unix socket
+
+import asyncio
+import os.path
+import websockets
+
+async def hello():
+ socket_path = os.path.join(os.path.dirname(__file__), "socket")
+ async with websockets.unix_connect(socket_path) as websocket:
+ name = input("What's your name? ")
+ await websocket.send(name)
+ print(f"> {name}")
+
+ greeting = await websocket.recv()
+ print(f"< {greeting}")
+
+asyncio.get_event_loop().run_until_complete(hello())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/unix_server.py b/testing/web-platform/tests/tools/third_party/websockets/example/unix_server.py
new file mode 100755
index 0000000000..a6ec0168a2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/unix_server.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+
+# WS server example listening on a Unix socket
+
+import asyncio
+import os.path
+import websockets
+
+async def hello(websocket, path):
+ name = await websocket.recv()
+ print(f"< {name}")
+
+ greeting = f"Hello {name}!"
+
+ await websocket.send(greeting)
+ print(f"> {greeting}")
+
+socket_path = os.path.join(os.path.dirname(__file__), "socket")
+start_server = websockets.unix_serve(hello, socket_path)
+
+asyncio.get_event_loop().run_until_complete(start_server)
+asyncio.get_event_loop().run_forever()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/horizontal.svg b/testing/web-platform/tests/tools/third_party/websockets/logo/horizontal.svg
new file mode 100644
index 0000000000..ee872dc478
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/logo/horizontal.svg
@@ -0,0 +1,31 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="256" viewBox="0 0 1024 256">
+ <linearGradient id="w" x1="0" y1="0" x2="0.1667" y2="0.6667">
+ <stop offset="0%" stop-color="#ffe873" />
+ <stop offset="100%" stop-color="#ffd43b" />
+ </linearGradient>
+ <linearGradient id="s" x1="0" y1="0" x2="0.1667" y2="0.6667">
+ <stop offset="0%" stop-color="#5a9fd4" />
+ <stop offset="100%" stop-color="#306998" />
+ </linearGradient>
+<g>
+ <path fill="url(#w)" d="m 151.60708,154.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.040757,-35.919452 c -3.43568,-3.42217 -7.332485,-5.347474 -11.589626,-5.723468 -2.229803,-0.198219 -4.473877,0.03111 -6.640354,0.675545 -3.242133,0.944875 -6.135526,2.664848 -8.593662,5.116366 -3.834369,3.819499 -5.86349,8.414979 -5.875977,13.287799 -0.06065,4.95281 1.951523,9.60074 5.808192,13.44424 l 55.622894,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 48.702551,136.2618 c -5.214172,-5.19459 -11.702899,-6.98745 -18.22998,-5.04881 -3.245701,0.9431 -6.135527,2.66307 -8.595446,5.11459 -3.83437,3.82127 -5.865275,8.41676 -5.875978,13.28957 -0.05619,4.95281 1.951524,9.60252 5.806409,13.4478 l 58.10689,57.90577 c 8.319842,8.29143 19.340421,11.9376 32.743314,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" />
+ <path fill="url(#s)" d="m 196.96038,146.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 147.57292,77.225374 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384846 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.51544 5.26947,-18.272609 -1.51003,-25.02895 L 187.20456,37.727314 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.62612 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" />
+ <path fill="#ffffff" d="m 215.68093,93.181574 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" />
+ </g>
+ <g>
+ <g fill="#ffd43b">
+ <path d="m 271.62046,177.33313 c 0,4.1637 1.46619,7.71227 4.39858,10.64361 2.9324,2.93556 6.48202,4.40069 10.64783,4.40069 4.16475,0 7.71438,-1.46513 10.64572,-4.40069 2.93344,-2.93134 4.40069,-6.47991 4.40069,-10.64361 v -35.00332 c 0,-2.12345 0.7647,-3.95198 2.29514,-5.48032 1.53045,-1.53256 3.35793,-2.29831 5.48349,-2.29831 h 0.12745 c 2.16664,0 3.972,0.76575 5.41923,2.29831 1.53045,1.52834 2.2962,3.35793 2.2962,5.48032 v 35.00332 c 0,4.1637 1.4662,7.71227 4.40069,10.64361 2.93134,2.93556 6.47886,4.40069 10.64572,4.40069 4.20794,0 7.77758,-1.46513 10.70997,-4.40069 2.93345,-2.93134 4.40069,-6.47991 4.40069,-10.64361 v -35.00332 c 0,-2.12345 0.76365,-3.95198 2.29515,-5.48032 1.44302,-1.53256 3.25049,-2.29831 5.41924,-2.29831 h 0.1264 c 2.12661,0 3.95409,0.76575 5.48349,2.29831 1.48831,1.52834 2.23194,3.35793 2.23194,5.48032 v 35.00332 c 0,8.45696 -2.9977,15.68261 -8.98887,21.67484 -5.99329,5.99224 -13.21999,8.98993 -21.67695,8.98993 -10.11696,0 -17.7239,-3.35583 -22.82609,-10.07272 -5.14222,6.71689 -12.77234,10.07272 -22.88719,10.07272 -8.45801,0 -15.68471,-2.99769 -21.67695,-8.98993 C 258.9998,193.01574 256,185.79113 256,177.33313 v -35.00332 c 0,-2.12345 0.76575,-3.95198 2.29619,-5.48032 1.5294,-1.53256 3.33581,-2.29831 5.41924,-2.29831 h 0.1917 c 2.08238,0 3.88774,0.76575 5.42029,2.29831 1.52834,1.52834 2.29409,3.35793 2.29409,5.48032 v 35.00332 z" />
+ <path d="m 443.95216,155.97534 c 0.51085,1.06173 0.7668,2.14346 0.7668,3.25048 0,0.8932 -0.16957,1.78536 -0.50979,2.67854 -0.72363,1.99707 -2.08343,3.4422 -4.0805,4.33434 -5.95114,2.67854 -13.77085,6.20711 -23.46228,10.58463 -12.02871,5.43924 -19.08477,8.64866 -21.16715,9.62823 3.22943,4.07944 8.26737,6.11863 15.11067,6.11863 4.5471,0 8.67077,-1.33769 12.36786,-4.01625 3.61283,-2.63534 6.14286,-6.03541 7.58798,-10.20227 1.23342,-3.48538 3.69815,-5.22754 7.39524,-5.22754 2.6343,0 4.7388,1.10702 6.31138,3.31369 0.97746,1.36193 1.46619,2.78598 1.46619,4.27325 0,0.8932 -0.16958,1.80641 -0.50874,2.74069 -2.50791,7.26988 -6.90861,13.13573 -13.19681,17.59961 -6.37563,4.63031 -13.51702,6.94757 -21.4231,6.94757 -10.11591,0 -18.76563,-3.58965 -25.94809,-10.7742 -7.18351,-7.18353 -10.77527,-15.83219 -10.77527,-25.9502 0,-10.11591 3.59176,-18.76351 10.77527,-25.95019 7.18142,-7.1814 15.83218,-10.77422 25.94809,-10.77422 7.30885,0 13.98257,1.99916 20.01904,5.99223 5.99118,3.91512 10.43296,9.05524 13.32321,15.43298 z m -33.34331,-5.67836 c -5.86583,0 -10.86059,2.06343 -14.98322,6.18604 -4.08049,4.12473 -6.12073,9.11949 -6.12073,14.98322 v 0.44661 l 35.63951,-16.00282 c -3.1441,-3.73817 -7.99035,-5.61305 -14.53556,-5.61305 z" />
+ <path d="m 465.12141,108.41246 c 2.08238,0 3.88775,0.74469 5.41924,2.23194 1.53045,1.52834 2.29619,3.35793 2.29619,5.48244 v 24.79998 c 4.80202,-4.24796 11.83701,-6.37564 21.10185,-6.37564 10.11591,0 18.76561,3.59177 25.94914,10.77422 7.18245,7.18563 10.77527,15.83429 10.77527,25.9502 0,10.11695 -3.59282,18.76561 -10.77527,25.95018 C 512.70536,204.41035 504.05566,208 493.93869,208 c -10.11696,0 -18.74349,-3.56964 -25.88382,-10.71207 -7.18457,-7.09504 -10.7974,-15.70262 -10.83954,-25.82063 v -55.33941 c 0,-2.12556 0.76576,-3.95409 2.29621,-5.48243 1.52939,-1.48727 3.3358,-2.23196 5.41924,-2.23196 h 0.19063 z m 28.81622,41.88452 c -5.86477,0 -10.85953,2.06343 -14.9832,6.18604 -4.0784,4.12473 -6.11969,9.11949 -6.11969,14.98322 0,5.8237 2.04129,10.79633 6.11969,14.91896 4.12367,4.12263 9.11737,6.18393 14.9832,6.18393 5.82371,0 10.79635,-2.0613 14.92002,-6.18393 4.12051,-4.12263 6.18288,-9.09526 6.18288,-14.91896 0,-5.86267 -2.06237,-10.85849 -6.18288,-14.98322 -4.12367,-4.12261 -9.09525,-6.18604 -14.92002,-6.18604 z" />
+ </g>
+ <g fill="#306998">
+ <path d="m 561.26467,150.17375 c -1.87066,0 -3.44325,0.6362 -4.71773,1.9107 -1.27556,1.31872 -1.91281,2.89025 -1.91281,4.71773 0,2.5511 1.23237,4.46389 3.69919,5.73733 0.84898,0.46872 4.39859,1.53045 10.64678,3.18834 5.05795,1.44619 8.81825,3.33686 11.28296,5.67413 3.52963,3.35898 5.29179,8.14097 5.29179,14.34703 0,6.11862 -2.16769,11.36829 -6.50203,15.74581 -4.37857,4.33644 -9.62823,6.50308 -15.74791,6.50308 h -16.64005 c -2.08448,0 -3.88879,-0.76365 -5.42029,-2.29621 -1.53045,-1.44407 -2.2962,-3.25048 -2.2962,-5.41712 v -0.12953 c 0,-2.12345 0.76575,-3.95198 2.2962,-5.48243 1.53044,-1.53045 3.33581,-2.29619 5.42029,-2.29619 h 17.2773 c 1.8696,0 3.44324,-0.6362 4.71773,-1.9107 1.27556,-1.27554 1.91281,-2.84707 1.91281,-4.71774 0,-2.33937 -1.21131,-4.10366 -3.63285,-5.29073 -0.63723,-0.30018 -4.20898,-1.36192 -10.71208,-3.18834 -5.05899,-1.48725 -8.82139,-3.44535 -11.28611,-5.8669 -3.52856,-3.44217 -5.29075,-8.30949 -5.29075,-14.5998 0,-6.12073 2.16876,-11.34721 6.50414,-15.68261 4.37648,-4.37752 9.62718,-6.56839 15.74687,-6.56839 h 11.73166 c 2.12452,0 3.95304,0.76575 5.48349,2.29831 1.52939,1.52834 2.29515,3.35793 2.29515,5.48032 v 0.12745 c 0,2.16876 -0.76576,3.97622 -2.29515,5.4203 -1.53045,1.52834 -3.35897,2.29619 -5.48349,2.29619 z" />
+ <path d="m 630.5677,134.55118 c 10.1159,0 18.76456,3.59177 25.94912,10.77422 7.18246,7.18563 10.77422,15.83429 10.77422,25.9502 0,10.11695 -3.59176,18.76561 -10.77422,25.95018 C 649.33331,204.40929 640.6836,208 630.5677,208 c -10.11592,0 -18.76563,-3.58965 -25.9481,-10.77422 -7.18351,-7.18351 -10.77526,-15.83217 -10.77526,-25.95018 0,-10.11591 3.59175,-18.76352 10.77526,-25.9502 7.18247,-7.18245 15.83218,-10.77422 25.9481,-10.77422 z m 0,15.7458 c -5.86585,0 -10.86059,2.06343 -14.98322,6.18604 -4.08155,4.12473 -6.12178,9.11949 -6.12178,14.98322 0,5.8237 2.04023,10.79633 6.12178,14.91896 4.12263,4.12263 9.11632,6.18393 14.98322,6.18393 5.82264,0 10.79527,-2.0613 14.91896,-6.18393 4.12261,-4.12263 6.18393,-9.09526 6.18393,-14.91896 0,-5.86267 -2.06132,-10.85849 -6.18393,-14.98322 -4.12369,-4.12261 -9.09527,-6.18604 -14.91896,-6.18604 z" />
+ <path d="m 724.0345,136.27333 c 3.61388,1.14811 5.4203,3.61282 5.4203,7.39523 v 0.32125 c 0,2.59008 -1.04278,4.65138 -3.12516,6.18394 -1.44512,1.01854 -2.93343,1.52834 -4.46178,1.52834 -0.80894,0 -1.63789,-0.12745 -2.48684,-0.38235 -2.08344,-0.67938 -4.23007,-1.02276 -6.43883,-1.02276 -5.86585,0 -10.86165,2.06343 -14.98322,6.18604 -4.08154,4.12473 -6.12074,9.11949 -6.12074,14.98322 0,5.8237 2.0392,10.79633 6.12074,14.91896 4.12157,4.12263 9.11633,6.18393 14.98322,6.18393 2.20982,0 4.35645,-0.33915 6.43883,-1.02065 0.80683,-0.25489 1.61471,-0.38234 2.42259,-0.38234 1.57046,0 3.08197,0.5119 4.52709,1.53254 2.08238,1.52835 3.12514,3.61283 3.12514,6.24819 0,3.74027 -1.80746,6.205 -5.42028,7.39524 -3.56964,1.10491 -7.26673,1.65579 -11.09232,1.65579 -10.11591,0 -18.76562,-3.58965 -25.95019,-10.77423 -7.1814,-7.18351 -10.77422,-15.83217 -10.77422,-25.95018 0,-10.11592 3.59176,-18.76352 10.77422,-25.9502 7.18351,-7.1814 15.83322,-10.77422 25.95019,-10.77422 3.82348,0.002 7.52162,0.57827 11.09126,1.72426 z" />
+ <path d="m 748.19829,108.41246 c 2.08132,0 3.88773,0.74469 5.42029,2.23194 1.5294,1.52834 2.29514,3.35793 2.29514,5.48244 v 44.18284 h 2.42259 c 5.44031,0 10.17805,-1.80642 14.21852,-5.4203 3.95198,-3.61283 6.20394,-8.07461 6.75693,-13.38852 0.25491,-1.99705 1.10597,-3.63494 2.5511,-4.90837 1.44408,-1.35982 3.16517,-2.04131 5.16328,-2.04131 h 0.19066 c 2.25405,0 4.14578,0.85212 5.67517,2.55109 1.36087,1.48727 2.04026,3.20942 2.04026,5.16329 0,0.25491 -0.0222,0.53298 -0.0632,0.82895 -1.02064,10.66889 -5.10115,18.65923 -12.24147,23.97103 3.73922,2.29831 7.18246,6.18604 10.32973,11.66849 3.27155,5.65306 4.90944,11.75483 4.90944,18.29688 v 3.25471 c 0,2.16664 -0.7668,3.972 -2.29515,5.41713 -1.53255,1.53256 -3.33791,2.29619 -5.4203,2.29619 h -0.1917 c -2.08342,0 -3.88879,-0.76363 -5.41922,-2.29619 -1.53045,-1.44408 -2.29514,-3.25049 -2.29514,-5.41713 v -3.25471 c -0.0442,-5.77629 -2.10555,-10.73102 -6.185,-14.85575 -4.12367,-4.07944 -9.09736,-6.11863 -14.91896,-6.11863 h -5.22754 v 24.22804 c 0,2.16664 -0.76574,3.97199 -2.29514,5.41712 -1.5315,1.53256 -3.33897,2.29621 -5.42028,2.29621 h -0.19381 c -2.08237,0 -3.88668,-0.76365 -5.41819,-2.29621 -1.52939,-1.44407 -2.29515,-3.25048 -2.29515,-5.41712 v -84.15879 c 0,-2.12556 0.76576,-3.95408 2.29515,-5.48243 1.53045,-1.48727 3.33582,-2.23195 5.41819,-2.23195 h 0.19381 z" />
+ <path d="m 876.85801,155.97534 c 0.5098,1.06173 0.76469,2.14346 0.76469,3.25048 0,0.8932 -0.17063,1.78536 -0.50874,2.67854 -0.72362,1.99707 -2.08342,3.4422 -4.08049,4.33434 -5.95115,2.67854 -13.77191,6.20711 -23.46229,10.58463 -12.02869,5.43924 -19.08476,8.64866 -21.16715,9.62823 3.22838,4.07944 8.26632,6.11863 15.11066,6.11863 4.54606,0 8.66973,-1.33769 12.36893,-4.01625 3.61176,-2.63534 6.14075,-6.03541 7.58587,-10.20227 1.23238,-3.48538 3.6992,-5.22754 7.39524,-5.22754 2.63536,0 4.73985,1.10702 6.31348,3.31369 0.97536,1.36193 1.46515,2.78598 1.46515,4.27325 0,0.8932 -0.16958,1.80641 -0.5098,2.74069 -2.50791,7.26988 -6.9065,13.13573 -13.19681,17.59961 -6.37563,4.63031 -13.51598,6.94757 -21.42206,6.94757 -10.1159,0 -18.76561,-3.58965 -25.94808,-10.7742 -7.18351,-7.18353 -10.77526,-15.83219 -10.77526,-25.9502 0,-10.11591 3.59175,-18.76351 10.77526,-25.95019 7.18141,-7.1814 15.83218,-10.77422 25.94808,-10.77422 7.30887,0 13.98364,1.99916 20.01906,5.99223 5.99223,3.91512 10.43294,9.05524 13.32426,15.43298 z m -33.34436,-5.67836 c -5.86479,0 -10.86059,2.06343 -14.98322,6.18604 -4.08049,4.12473 -6.12074,9.11949 -6.12074,14.98322 v 0.44661 l 35.63952,-16.00282 c -3.14516,-3.73817 -7.99034,-5.61305 -14.53556,-5.61305 z" />
+ <path d="m 898.02411,108.41246 c 2.08238,0 3.88879,0.74469 5.42028,2.23194 1.52939,1.52834 2.29515,3.35793 2.29515,5.48244 v 18.42434 h 9.56398 c 2.08237,0 3.88772,0.76575 5.42028,2.29831 1.5294,1.52834 2.29304,3.35793 2.29304,5.48032 v 0.12745 c 0,2.16876 -0.76364,3.97621 -2.29304,5.4203 -1.5315,1.52834 -3.33791,2.29619 -5.42028,2.29619 h -9.56398 v 37.80405 c 0,1.23446 0.42343,2.27724 1.27554,3.12514 0.85002,0.85212 1.9128,1.27555 3.1873,1.27555 h 5.10114 c 2.08237,0 3.88772,0.76574 5.42028,2.29619 1.5294,1.53045 2.29304,3.35898 2.29304,5.48243 v 0.12954 c 0,2.16664 -0.76364,3.97199 -2.29304,5.41711 C 919.1923,207.23635 917.38589,208 915.30352,208 h -5.10114 c -5.52563,0 -10.26442,-1.95387 -14.21746,-5.86478 -3.91196,-3.95198 -5.86479,-8.67078 -5.86479,-14.15532 v -71.85095 c 0,-2.12558 0.7647,-3.9541 2.29515,-5.48245 1.53045,-1.48725 3.33686,-2.23193 5.41924,-2.23193 h 0.18959 z" />
+ <path d="m 951.70877,150.17375 c -1.87066,0 -3.44324,0.6362 -4.71773,1.9107 -1.27556,1.31872 -1.91281,2.89025 -1.91281,4.71773 0,2.5511 1.23238,4.46389 3.69711,5.73733 0.8521,0.46872 4.40067,1.53045 10.64886,3.18834 5.05691,1.44619 8.81825,3.33686 11.28402,5.67413 3.52751,3.35898 5.2918,8.14097 5.2918,14.34703 0,6.11862 -2.16876,11.36829 -6.5031,15.74581 -4.37752,4.33644 -9.62822,6.50308 -15.74789,6.50308 h -16.64007 c -2.08342,0 -3.88879,-0.76365 -5.42028,-2.29621 -1.53045,-1.44407 -2.2941,-3.25048 -2.2941,-5.41712 v -0.12953 c 0,-2.12345 0.76365,-3.95198 2.2941,-5.48243 1.53045,-1.53045 3.33686,-2.29619 5.42028,-2.29619 h 17.2773 c 1.86962,0 3.4443,-0.6362 4.71775,-1.9107 1.27554,-1.27554 1.91279,-2.84707 1.91279,-4.71774 0,-2.33937 -1.2113,-4.10366 -3.63283,-5.29073 -0.63936,-0.30018 -4.209,-1.36192 -10.71208,-3.18834 -5.05901,-1.48725 -8.8214,-3.44535 -11.28613,-5.8669 -3.52856,-3.44217 -5.29073,-8.30949 -5.29073,-14.5998 0,-6.12073 2.16875,-11.34721 6.50413,-15.68261 4.37647,-4.37752 9.62718,-6.56839 15.74791,-6.56839 h 11.73063 c 2.1266,0 3.95304,0.76575 5.48243,2.29831 1.53045,1.52834 2.29514,3.35793 2.29514,5.48032 v 0.12745 c 0,2.16876 -0.76469,3.97622 -2.29514,5.4203 -1.52939,1.52834 -3.35687,2.29619 -5.48243,2.29619 z" />
+ </g>
+ </g>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/icon.svg b/testing/web-platform/tests/tools/third_party/websockets/logo/icon.svg
new file mode 100644
index 0000000000..cb760940aa
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/logo/icon.svg
@@ -0,0 +1,15 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
+ <linearGradient id="w" x1="0" y1="0" x2="0.6667" y2="0.6667">
+ <stop offset="0%" stop-color="#ffe873" />
+ <stop offset="100%" stop-color="#ffd43b" />
+ </linearGradient>
+ <linearGradient id="s" x1="0" y1="0" x2="0.6667" y2="0.6667">
+ <stop offset="0%" stop-color="#5a9fd4" />
+ <stop offset="100%" stop-color="#306998" />
+ </linearGradient>
+ <g>
+ <path fill="url(#w)" d="m 151.60708,154.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.040757,-35.919452 c -3.43568,-3.42217 -7.332485,-5.347474 -11.589626,-5.723468 -2.229803,-0.198219 -4.473877,0.03111 -6.640354,0.675545 -3.242133,0.944875 -6.135526,2.664848 -8.593662,5.116366 -3.834369,3.819499 -5.86349,8.414979 -5.875977,13.287799 -0.06065,4.95281 1.951523,9.60074 5.808192,13.44424 l 55.622894,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 48.702551,136.2618 c -5.214172,-5.19459 -11.702899,-6.98745 -18.22998,-5.04881 -3.245701,0.9431 -6.135527,2.66307 -8.595446,5.11459 -3.83437,3.82127 -5.865275,8.41676 -5.875978,13.28957 -0.05619,4.95281 1.951524,9.60252 5.806409,13.4478 l 58.10689,57.90577 c 8.319842,8.29143 19.340421,11.9376 32.743314,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" />
+ <path fill="url(#s)" d="m 196.96038,146.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 147.57292,77.225374 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384846 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.51544 5.26947,-18.272609 -1.51003,-25.02895 L 187.20456,37.727314 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.62612 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" />
+ <path fill="#ffffff" d="m 215.68093,93.181574 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" />
+ </g>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/old.svg b/testing/web-platform/tests/tools/third_party/websockets/logo/old.svg
new file mode 100644
index 0000000000..a073139e33
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/logo/old.svg
@@ -0,0 +1,14 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="360" height="120" viewBox="0 0 21 7">
+ <linearGradient id="w" x1="0" y1="0" x2="1" y2="1">
+ <stop offset="0%" stop-color="#5a9fd4" />
+ <stop offset="100%" stop-color="#306998" />
+ </linearGradient>
+ <linearGradient id="s" x1="0" y1="0" x2="1" y2="1">
+ <stop offset="0%" stop-color="#ffe873" />
+ <stop offset="100%" stop-color="#ffd43b" />
+ </linearGradient>
+ <polyline fill="none" stroke="url(#w)" stroke-linecap="round" stroke-linejoin="round"
+ points="1,1 1,5 5,5 5,1 5,5 9,5 9,1"/>
+ <polyline fill="none" stroke="url(#s)" stroke-linecap="round" stroke-linejoin="round"
+ points="19,1 11,1 11,3 19,3 19,5 11,5"/>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/tidelift.png b/testing/web-platform/tests/tools/third_party/websockets/logo/tidelift.png
new file mode 100644
index 0000000000..317dc4d985
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/logo/tidelift.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/websockets/logo/vertical.svg b/testing/web-platform/tests/tools/third_party/websockets/logo/vertical.svg
new file mode 100644
index 0000000000..b07fb22387
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/logo/vertical.svg
@@ -0,0 +1,31 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="480" height="320" viewBox="0 0 480 320">
+ <linearGradient id="w" x1="0.2333" y1="0" x2="0.5889" y2="0.5333">
+ <stop offset="0%" stop-color="#ffe873" />
+ <stop offset="100%" stop-color="#ffd43b" />
+ </linearGradient>
+ <linearGradient id="s" x1="0.2333" y1="0" x2="0.5889" y2="0.5333">
+ <stop offset="0%" stop-color="#5a9fd4" />
+ <stop offset="100%" stop-color="#306998" />
+ </linearGradient>
+ <g>
+ <path fill="url(#w)" d="m 263.40708,146.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.04076,-35.919454 c -3.43568,-3.42217 -7.33248,-5.347474 -11.58962,-5.723468 -2.22981,-0.198219 -4.47388,0.03111 -6.64036,0.675545 -3.24213,0.944875 -6.13552,2.664848 -8.59366,5.116366 -3.83437,3.819499 -5.86349,8.414979 -5.87598,13.287801 -0.0607,4.95281 1.95153,9.60074 5.8082,13.44424 l 55.62289,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 160.50255,128.2618 c -5.21417,-5.19459 -11.7029,-6.98745 -18.22998,-5.04881 -3.2457,0.9431 -6.13553,2.66307 -8.59545,5.11459 -3.83437,3.82127 -5.86527,8.41676 -5.87597,13.28957 -0.0562,4.95281 1.95152,9.60252 5.80641,13.4478 l 58.10689,57.90577 c 8.31984,8.29143 19.34042,11.9376 32.74331,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" />
+ <path fill="url(#s)" d="m 308.76038,138.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 259.37292,69.225372 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384848 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.515442 5.26947,-18.272611 -1.51003,-25.028952 L 299.00456,29.727312 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.626122 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" />
+ <path fill="#ffffff" d="m 327.48093,85.181572 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" />
+ </g>
+ <g>
+ <g fill="#ffd43b">
+ <path d="m 25.719398,284.91839 c 0,2.59075 0.912299,4.79875 2.736898,6.62269 1.824599,1.82657 4.033255,2.73821 6.625313,2.73821 2.591402,0 4.800058,-0.91164 6.624002,-2.73821 1.825254,-1.82394 2.738209,-4.03194 2.738209,-6.62269 v -21.77984 c 0,-1.32126 0.475811,-2.45901 1.42809,-3.40998 0.952278,-0.95359 2.089375,-1.43006 3.411947,-1.43006 h 0.0793 c 1.348132,0 2.471467,0.47647 3.371969,1.43006 0.952278,0.95097 1.428745,2.08938 1.428745,3.40998 v 21.77984 c 0,2.59075 0.912299,4.79875 2.738209,6.62269 1.823944,1.82657 4.031289,2.73821 6.624002,2.73821 2.618274,0 4.839382,-0.91164 6.663981,-2.73821 1.825254,-1.82394 2.738209,-4.03194 2.738209,-6.62269 v -21.77984 c 0,-1.32126 0.475156,-2.45901 1.42809,-3.40998 0.897881,-0.95359 2.022526,-1.43006 3.371969,-1.43006 h 0.07865 c 1.323228,0 2.460325,0.47647 3.411948,1.43006 0.926062,0.95097 1.388766,2.08938 1.388766,3.40998 v 21.77984 c 0,5.26211 -1.865233,9.75807 -5.593077,13.48657 -3.729156,3.7285 -8.22577,5.59373 -13.487876,5.59373 -6.294998,0 -11.028207,-2.08807 -14.202904,-6.26747 -3.199602,4.1794 -7.94723,6.26747 -14.240916,6.26747 -5.262763,0 -9.759377,-1.86523 -13.487876,-5.59373 C 17.866544,294.67646 16,290.18115 16,284.91839 v -21.77984 c 0,-1.32126 0.476467,-2.45901 1.428745,-3.40998 0.951623,-0.95359 2.075612,-1.43006 3.371969,-1.43006 h 0.11928 c 1.295702,0 2.419036,0.47647 3.372625,1.43006 0.950967,0.95097 1.427434,2.08938 1.427434,3.40998 v 21.77984 z" />
+ <path d="m 132.94801,271.6291 c 0.31786,0.66063 0.47712,1.33371 0.47712,2.02252 0,0.55577 -0.10551,1.11089 -0.3172,1.66665 -0.45026,1.24262 -1.29636,2.14181 -2.53898,2.69692 -3.70293,1.66665 -8.56853,3.8622 -14.59875,6.58599 -7.48453,3.38442 -11.87497,5.38139 -13.17067,5.9909 2.00942,2.53832 5.14414,3.80715 9.40219,3.80715 2.82931,0 5.39515,-0.83234 7.69556,-2.499 2.24798,-1.63977 3.82222,-3.75537 4.72141,-6.34808 0.76746,-2.16868 2.30107,-3.25269 4.60148,-3.25269 1.63912,0 2.94859,0.68881 3.92708,2.06185 0.6082,0.84742 0.9123,1.7335 0.9123,2.65891 0,0.55577 -0.10552,1.12399 -0.31655,1.70532 -1.56048,4.52348 -4.29869,8.17334 -8.21135,10.95087 -3.96706,2.88108 -8.41059,4.32293 -13.32993,4.32293 -6.29434,0 -11.67639,-2.23356 -16.145474,-6.70395 -4.469743,-4.46975 -6.704615,-9.85114 -6.704615,-16.14679 0,-6.29434 2.234872,-11.67507 6.704615,-16.14678 4.468434,-4.46843 9.851134,-6.70396 16.145474,-6.70396 4.54773,0 8.70027,1.24392 12.45629,3.7285 3.72785,2.43607 6.49162,5.63437 8.29,9.60274 z m -20.74695,-3.5332 c -3.64985,0 -6.7577,1.28391 -9.32289,3.84909 -2.53897,2.5665 -3.808452,5.67435 -3.808452,9.32289 v 0.27789 l 22.175692,-9.95731 c -1.95633,-2.32597 -4.97177,-3.49256 -9.04435,-3.49256 z" />
+ <path d="m 146.11999,242.03442 c 1.2957,0 2.41904,0.46336 3.37197,1.38876 0.95228,0.95097 1.42874,2.08938 1.42874,3.4113 v 15.4311 c 2.98792,-2.64318 7.36525,-3.96707 13.13004,-3.96707 6.29434,0 11.67638,2.23488 16.14613,6.70396 4.46908,4.47106 6.70461,9.85245 6.70461,16.14679 0,6.29499 -2.23553,11.67638 -6.70461,16.14678 -4.46909,4.4704 -9.85113,6.70396 -16.14613,6.70396 -6.295,0 -11.66262,-2.22111 -16.10549,-6.66529 -4.4704,-4.41469 -6.71838,-9.77052 -6.7446,-16.06617 v -34.43341 c 0,-1.32257 0.47647,-2.46032 1.42875,-3.41129 0.95162,-0.92541 2.07561,-1.38877 3.37197,-1.38877 h 0.11862 z m 17.93009,26.06148 c -3.64919,0 -6.75704,1.28391 -9.32288,3.84909 -2.53767,2.5665 -3.80781,5.67435 -3.80781,9.32289 0,3.62364 1.27014,6.71772 3.80781,9.28291 2.56584,2.56519 5.67303,3.84778 9.32288,3.84778 3.62364,0 6.71773,-1.28259 9.28357,-3.84778 2.56387,-2.56519 3.84712,-5.65927 3.84712,-9.28291 0,-3.64788 -1.28325,-6.75639 -3.84712,-9.32289 -2.56584,-2.56518 -5.65927,-3.84909 -9.28357,-3.84909 z" />
+ </g>
+ <g fill="#306998">
+ <path d="m 205.94246,268.01922 c -1.16397,0 -2.14247,0.39586 -2.93548,1.18888 -0.79368,0.82054 -1.19019,1.79838 -1.19019,2.93548 0,1.58735 0.76681,2.77753 2.30172,3.56989 0.52825,0.29165 2.7369,0.95228 6.62466,1.98386 3.14717,0.89985 5.48691,2.07627 7.02051,3.53057 2.19621,2.09003 3.29267,5.06549 3.29267,8.92704 0,3.80714 -1.34879,7.0736 -4.04571,9.79739 -2.72444,2.69823 -5.9909,4.04636 -9.7987,4.04636 h -10.35381 c -1.29701,0 -2.41969,-0.47516 -3.37262,-1.42875 -0.95228,-0.89853 -1.42875,-2.02252 -1.42875,-3.37065 v -0.0806 c 0,-1.32126 0.47647,-2.45901 1.42875,-3.41129 0.95227,-0.95228 2.07561,-1.42874 3.37262,-1.42874 h 10.75032 c 1.16331,0 2.14246,-0.39586 2.93548,-1.18888 0.79368,-0.79367 1.19019,-1.77151 1.19019,-2.93548 0,-1.45561 -0.7537,-2.55339 -2.26044,-3.29201 -0.3965,-0.18678 -2.61892,-0.84742 -6.66529,-1.98386 -3.14782,-0.9254 -5.48887,-2.14377 -7.02247,-3.65051 -2.19555,-2.1418 -3.29202,-5.17035 -3.29202,-9.08432 0,-3.80846 1.34945,-7.06049 4.04702,-9.75807 2.72314,-2.72379 5.99024,-4.087 9.79805,-4.087 h 7.2997 c 1.32192,0 2.45967,0.47647 3.41195,1.43006 0.95162,0.95097 1.42809,2.08938 1.42809,3.40998 v 0.0793 c 0,1.34945 -0.47647,2.47409 -1.42809,3.37263 -0.95228,0.95097 -2.09003,1.42874 -3.41195,1.42874 z" />
+ <path d="m 249.06434,258.29851 c 6.29434,0 11.67573,2.23488 16.14612,6.70396 4.46909,4.47106 6.70396,9.85245 6.70396,16.14679 0,6.29499 -2.23487,11.67638 -6.70396,16.14678 -4.46974,4.46974 -9.85178,6.70396 -16.14612,6.70396 -6.29435,0 -11.67639,-2.23356 -16.14548,-6.70396 -4.46974,-4.46974 -6.70461,-9.85113 -6.70461,-16.14678 0,-6.29434 2.23487,-11.67508 6.70461,-16.14679 4.46909,-4.46908 9.85113,-6.70396 16.14548,-6.70396 z m 0,9.79739 c -3.64986,0 -6.7577,1.28391 -9.32289,3.84909 -2.53963,2.5665 -3.80911,5.67435 -3.80911,9.32289 0,3.62364 1.26948,6.71772 3.80911,9.28291 2.56519,2.56519 5.67238,3.84778 9.32289,3.84778 3.62298,0 6.71706,-1.28259 9.28291,-3.84778 2.56518,-2.56519 3.84778,-5.65927 3.84778,-9.28291 0,-3.64788 -1.2826,-6.75639 -3.84778,-9.32289 -2.56585,-2.56518 -5.65928,-3.84909 -9.28291,-3.84909 z" />
+ <path d="m 307.22146,259.37007 c 2.24864,0.71438 3.37263,2.24798 3.37263,4.60148 v 0.19989 c 0,1.6116 -0.64884,2.89419 -1.94454,3.84778 -0.89919,0.63376 -1.82525,0.95097 -2.77622,0.95097 -0.50334,0 -1.01913,-0.0793 -1.54737,-0.23791 -1.29636,-0.42272 -2.63204,-0.63638 -4.00638,-0.63638 -3.64986,0 -6.75836,1.28391 -9.32289,3.84909 -2.53963,2.5665 -3.80846,5.67435 -3.80846,9.32289 0,3.62364 1.26883,6.71772 3.80846,9.28291 2.56453,2.56519 5.67238,3.84778 9.32289,3.84778 1.375,0 2.71068,-0.21103 4.00638,-0.63507 0.50203,-0.1586 1.00471,-0.2379 1.50739,-0.2379 0.97718,0 1.91767,0.31851 2.81686,0.95358 1.2957,0.95097 1.94453,2.24798 1.94453,3.88776 0,2.32728 -1.12464,3.86089 -3.37262,4.60148 -2.22111,0.6875 -4.52152,1.03027 -6.90189,1.03027 -6.29434,0 -11.67638,-2.23356 -16.14678,-6.70396 -4.46843,-4.46974 -6.70396,-9.85113 -6.70396,-16.14678 0,-6.29435 2.23487,-11.67508 6.70396,-16.14679 4.46974,-4.46843 9.85178,-6.70396 16.14678,-6.70396 2.37906,0.001 4.68012,0.35981 6.90123,1.07287 z" />
+ <path d="m 322.25671,242.03442 c 1.29504,0 2.41903,0.46336 3.37262,1.38876 0.95163,0.95097 1.42809,2.08938 1.42809,3.4113 v 27.49154 h 1.50739 c 3.38508,0 6.33301,-1.12399 8.84708,-3.37263 2.45901,-2.24798 3.86023,-5.0242 4.20431,-8.33063 0.15861,-1.24261 0.68816,-2.26174 1.58735,-3.0541 0.89854,-0.84611 1.96944,-1.27015 3.21271,-1.27015 h 0.11863 c 1.40252,0 2.5796,0.53021 3.53122,1.58735 0.84676,0.92541 1.26949,1.99697 1.26949,3.21271 0,0.15861 -0.0138,0.33163 -0.0393,0.51579 -0.63507,6.63842 -3.17405,11.61019 -7.61692,14.91531 2.32663,1.43006 4.46909,3.84909 6.42739,7.26039 2.03563,3.51746 3.05476,7.31412 3.05476,11.38473 v 2.02515 c 0,1.34813 -0.47712,2.47147 -1.42809,3.37066 -0.95359,0.95359 -2.07692,1.42874 -3.37263,1.42874 h -0.11928 c -1.29635,0 -2.41969,-0.47515 -3.37196,-1.42874 -0.95228,-0.89854 -1.42809,-2.02253 -1.42809,-3.37066 v -2.02515 c -0.0275,-3.59414 -1.31012,-6.67708 -3.84844,-9.24358 -2.56584,-2.53832 -5.66058,-3.80715 -9.28291,-3.80715 h -3.25269 v 15.07523 c 0,1.34813 -0.47646,2.47146 -1.42809,3.37065 -0.95293,0.95359 -2.07758,1.42875 -3.37262,1.42875 h -0.12059 c -1.2957,0 -2.41838,-0.47516 -3.37132,-1.42875 -0.95162,-0.89853 -1.42809,-2.02252 -1.42809,-3.37065 v -52.36547 c 0,-1.32257 0.47647,-2.46032 1.42809,-3.41129 0.95228,-0.92541 2.07562,-1.38877 3.37132,-1.38877 h 0.12059 z" />
+ <path d="m 402.31164,271.6291 c 0.31721,0.66063 0.47581,1.33371 0.47581,2.02252 0,0.55577 -0.10617,1.11089 -0.31655,1.66665 -0.45025,1.24262 -1.29635,2.14181 -2.53897,2.69692 -3.70294,1.66665 -8.56919,3.8622 -14.59876,6.58599 -7.48452,3.38442 -11.87496,5.38139 -13.17067,5.9909 2.00877,2.53832 5.14349,3.80715 9.40219,3.80715 2.82866,0 5.3945,-0.83234 7.69622,-2.499 2.24732,-1.63977 3.82091,-3.75537 4.7201,-6.34808 0.76681,-2.16868 2.30172,-3.25269 4.60148,-3.25269 1.63978,0 2.94924,0.68881 3.92839,2.06185 0.60689,0.84742 0.91165,1.7335 0.91165,2.65891 0,0.55577 -0.10552,1.12399 -0.31721,1.70532 -1.56048,4.52348 -4.29738,8.17334 -8.21135,10.95087 -3.96706,2.88108 -8.40994,4.32293 -13.32928,4.32293 -6.29434,0 -11.67638,-2.23356 -16.14547,-6.70395 -4.46974,-4.46975 -6.70461,-9.85114 -6.70461,-16.14679 0,-6.29434 2.23487,-11.67507 6.70461,-16.14678 4.46843,-4.46843 9.85113,-6.70396 16.14547,-6.70396 4.54774,0 8.70093,1.24392 12.4563,3.7285 3.7285,2.43607 6.49161,5.63437 8.29065,9.60274 z m -20.7476,-3.5332 c -3.6492,0 -6.7577,1.28391 -9.32289,3.84909 -2.53897,2.5665 -3.80846,5.67435 -3.80846,9.32289 v 0.27789 l 22.1757,-9.95731 c -1.95699,-2.32597 -4.97177,-3.49256 -9.04435,-3.49256 z" />
+ <path d="m 415.48166,242.03442 c 1.2957,0 2.41969,0.46336 3.37262,1.38876 0.95162,0.95097 1.42809,2.08938 1.42809,3.4113 v 11.46403 h 5.95092 c 1.2957,0 2.41903,0.47647 3.37262,1.43006 0.95163,0.95097 1.42678,2.08938 1.42678,3.40998 v 0.0793 c 0,1.34945 -0.47515,2.47409 -1.42678,3.37263 -0.95293,0.95097 -2.07692,1.42874 -3.37262,1.42874 h -5.95092 v 23.52252 c 0,0.76811 0.26347,1.41695 0.79367,1.94453 0.5289,0.53021 1.19019,0.79368 1.98321,0.79368 h 3.17404 c 1.2957,0 2.41903,0.47646 3.37262,1.42874 0.95163,0.95228 1.42678,2.09003 1.42678,3.41129 v 0.0806 c 0,1.34813 -0.47515,2.47146 -1.42678,3.37065 C 428.65298,303.52484 427.52899,304 426.23329,304 h -3.17404 c -3.43817,0 -6.38675,-1.21574 -8.84642,-3.6492 -2.43411,-2.45901 -3.6492,-5.39515 -3.6492,-8.80775 v -44.70726 c 0,-1.32258 0.47581,-2.46033 1.42809,-3.4113 0.95228,-0.9254 2.07627,-1.38876 3.37197,-1.38876 h 0.11797 z" />
+ <path d="m 448.88545,268.01922 c -1.16397,0 -2.14246,0.39586 -2.93548,1.18888 -0.79368,0.82054 -1.19019,1.79838 -1.19019,2.93548 0,1.58735 0.76681,2.77753 2.30042,3.56989 0.5302,0.29165 2.7382,0.95228 6.62596,1.98386 3.14652,0.89985 5.48691,2.07627 7.02117,3.53057 2.19489,2.09003 3.29267,5.06549 3.29267,8.92704 0,3.80714 -1.34945,7.0736 -4.04637,9.79739 -2.72379,2.69823 -5.99089,4.04636 -9.79869,4.04636 h -10.35382 c -1.29635,0 -2.41969,-0.47516 -3.37262,-1.42875 -0.95228,-0.89853 -1.42744,-2.02252 -1.42744,-3.37065 v -0.0806 c 0,-1.32126 0.47516,-2.45901 1.42744,-3.41129 0.95228,-0.95228 2.07627,-1.42874 3.37262,-1.42874 h 10.75032 c 1.16332,0 2.14312,-0.39586 2.93549,-1.18888 0.79367,-0.79367 1.19018,-1.77151 1.19018,-2.93548 0,-1.45561 -0.7537,-2.55339 -2.26043,-3.29201 -0.39782,-0.18678 -2.61893,-0.84742 -6.66529,-1.98386 -3.14783,-0.9254 -5.48887,-2.14377 -7.02248,-3.65051 -2.19555,-2.1418 -3.29201,-5.17035 -3.29201,-9.08432 0,-3.80846 1.34944,-7.06049 4.04701,-9.75807 2.72314,-2.72379 5.99025,-4.087 9.7987,-4.087 h 7.29906 c 1.32322,0 2.45967,0.47647 3.41129,1.43006 0.95228,0.95097 1.42809,2.08938 1.42809,3.40998 v 0.0793 c 0,1.34945 -0.47581,2.47409 -1.42809,3.37263 -0.95162,0.95097 -2.08872,1.42874 -3.41129,1.42874 z" />
+ </g>
+ </g>
+</svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/performance/mem_client.py b/testing/web-platform/tests/tools/third_party/websockets/performance/mem_client.py
new file mode 100644
index 0000000000..890216edf8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/performance/mem_client.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+
+import asyncio
+import statistics
+import tracemalloc
+
+import websockets
+from websockets.extensions import permessage_deflate
+
+
+CLIENTS = 10
+INTERVAL = 1 / 10 # seconds
+
+MEM_SIZE = []
+
+
+async def mem_client(client):
+ # Space out connections to make them sequential.
+ await asyncio.sleep(client * INTERVAL)
+
+ tracemalloc.start()
+
+ async with websockets.connect(
+ "ws://localhost:8765",
+ extensions=[
+ permessage_deflate.ClientPerMessageDeflateFactory(
+ server_max_window_bits=10,
+ client_max_window_bits=10,
+ compress_settings={"memLevel": 3},
+ )
+ ],
+ ) as ws:
+ await ws.send("hello")
+ await ws.recv()
+
+ await ws.send(b"hello")
+ await ws.recv()
+
+ MEM_SIZE.append(tracemalloc.get_traced_memory()[0])
+ tracemalloc.stop()
+
+ # Hold connection open until the end of the test.
+ await asyncio.sleep(CLIENTS * INTERVAL)
+
+
+asyncio.get_event_loop().run_until_complete(
+ asyncio.gather(*[mem_client(client) for client in range(CLIENTS + 1)])
+)
+
+# First connection incurs non-representative setup costs.
+del MEM_SIZE[0]
+
+print(f"µ = {statistics.mean(MEM_SIZE) / 1024:.1f} KiB")
+print(f"σ = {statistics.stdev(MEM_SIZE) / 1024:.1f} KiB")
diff --git a/testing/web-platform/tests/tools/third_party/websockets/performance/mem_server.py b/testing/web-platform/tests/tools/third_party/websockets/performance/mem_server.py
new file mode 100644
index 0000000000..0a4a29f76c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/performance/mem_server.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+
+import asyncio
+import signal
+import statistics
+import tracemalloc
+
+import websockets
+from websockets.extensions import permessage_deflate
+
+
+CLIENTS = 10
+INTERVAL = 1 / 10 # seconds
+
+MEM_SIZE = []
+
+
+async def handler(ws, path):
+ msg = await ws.recv()
+ await ws.send(msg)
+
+ msg = await ws.recv()
+ await ws.send(msg)
+
+ MEM_SIZE.append(tracemalloc.get_traced_memory()[0])
+ tracemalloc.stop()
+
+ tracemalloc.start()
+
+ # Hold connection open until the end of the test.
+ await asyncio.sleep(CLIENTS * INTERVAL)
+
+
+async def mem_server(stop):
+ async with websockets.serve(
+ handler,
+ "localhost",
+ 8765,
+ extensions=[
+ permessage_deflate.ServerPerMessageDeflateFactory(
+ server_max_window_bits=10,
+ client_max_window_bits=10,
+ compress_settings={"memLevel": 3},
+ )
+ ],
+ ):
+ await stop
+
+
+loop = asyncio.get_event_loop()
+
+stop = loop.create_future()
+loop.add_signal_handler(signal.SIGINT, stop.set_result, None)
+
+tracemalloc.start()
+
+loop.run_until_complete(mem_server(stop))
+
+# First connection incurs non-representative setup costs.
+del MEM_SIZE[0]
+
+print(f"µ = {statistics.mean(MEM_SIZE) / 1024:.1f} KiB")
+print(f"σ = {statistics.stdev(MEM_SIZE) / 1024:.1f} KiB")
diff --git a/testing/web-platform/tests/tools/third_party/websockets/setup.cfg b/testing/web-platform/tests/tools/third_party/websockets/setup.cfg
new file mode 100644
index 0000000000..c306b2d4fb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/setup.cfg
@@ -0,0 +1,30 @@
+[bdist_wheel]
+python-tag = py36.py37
+
+[metadata]
+license_file = LICENSE
+
+[flake8]
+ignore = E731,F403,F405,W503
+max-line-length = 88
+
+[isort]
+combine_as_imports = True
+force_grid_wrap = 0
+include_trailing_comma = True
+known_standard_library = asyncio
+line_length = 88
+lines_after_imports = 2
+multi_line_output = 3
+
+[coverage:run]
+branch = True
+omit = */__main__.py
+source =
+ websockets
+ tests
+
+[coverage:paths]
+source =
+ src/websockets
+ .tox/*/lib/python*/site-packages/websockets
diff --git a/testing/web-platform/tests/tools/third_party/websockets/setup.py b/testing/web-platform/tests/tools/third_party/websockets/setup.py
new file mode 100644
index 0000000000..f358192477
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/setup.py
@@ -0,0 +1,66 @@
+import pathlib
+import re
+import sys
+
+import setuptools
+
+
+root_dir = pathlib.Path(__file__).parent
+
+description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
+
+long_description = (root_dir / 'README.rst').read_text(encoding='utf-8')
+
+# PyPI disables the "raw" directive.
+long_description = re.sub(
+ r"^\.\. raw:: html.*?^(?=\w)",
+ "",
+ long_description,
+ flags=re.DOTALL | re.MULTILINE,
+)
+
+exec((root_dir / 'src' / 'websockets' / 'version.py').read_text(encoding='utf-8'))
+
+if sys.version_info[:3] < (3, 6, 1):
+ raise Exception("websockets requires Python >= 3.6.1.")
+
+packages = ['websockets', 'websockets/extensions']
+
+ext_modules = [
+ setuptools.Extension(
+ 'websockets.speedups',
+ sources=['src/websockets/speedups.c'],
+ optional=not (root_dir / '.cibuildwheel').exists(),
+ )
+]
+
+setuptools.setup(
+ name='websockets',
+ version=version,
+ description=description,
+ long_description=long_description,
+ url='https://github.com/aaugustin/websockets',
+ author='Aymeric Augustin',
+ author_email='aymeric.augustin@m4x.org',
+ license='BSD',
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: 3.8',
+ ],
+ package_dir = {'': 'src'},
+ package_data = {'websockets': ['py.typed']},
+ packages=packages,
+ ext_modules=ext_modules,
+ include_package_data=True,
+ zip_safe=False,
+ python_requires='>=3.6.1',
+ test_loader='unittest:TestLoader',
+)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py
new file mode 100644
index 0000000000..ea1d829a33
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py
@@ -0,0 +1,55 @@
+# This relies on each of the submodules having an __all__ variable.
+
+from .auth import * # noqa
+from .client import * # noqa
+from .exceptions import * # noqa
+from .protocol import * # noqa
+from .server import * # noqa
+from .typing import * # noqa
+from .uri import * # noqa
+from .version import version as __version__ # noqa
+
+
+__all__ = [
+ "AbortHandshake",
+ "basic_auth_protocol_factory",
+ "BasicAuthWebSocketServerProtocol",
+ "connect",
+ "ConnectionClosed",
+ "ConnectionClosedError",
+ "ConnectionClosedOK",
+ "Data",
+ "DuplicateParameter",
+ "ExtensionHeader",
+ "ExtensionParameter",
+ "InvalidHandshake",
+ "InvalidHeader",
+ "InvalidHeaderFormat",
+ "InvalidHeaderValue",
+ "InvalidMessage",
+ "InvalidOrigin",
+ "InvalidParameterName",
+ "InvalidParameterValue",
+ "InvalidState",
+ "InvalidStatusCode",
+ "InvalidUpgrade",
+ "InvalidURI",
+ "NegotiationError",
+ "Origin",
+ "parse_uri",
+ "PayloadTooBig",
+ "ProtocolError",
+ "RedirectHandshake",
+ "SecurityError",
+ "serve",
+ "Subprotocol",
+ "unix_connect",
+ "unix_serve",
+ "WebSocketClientProtocol",
+ "WebSocketCommonProtocol",
+ "WebSocketException",
+ "WebSocketProtocolError",
+ "WebSocketServer",
+ "WebSocketServerProtocol",
+ "WebSocketURI",
+]
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py
new file mode 100644
index 0000000000..394f7ac799
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py
@@ -0,0 +1,206 @@
+import argparse
+import asyncio
+import os
+import signal
+import sys
+import threading
+from typing import Any, Set
+
+from .client import connect
+from .exceptions import ConnectionClosed, format_close
+
+
+if sys.platform == "win32":
+
+ def win_enable_vt100() -> None:
+ """
+ Enable VT-100 for console output on Windows.
+
+ See also https://bugs.python.org/issue29059.
+
+ """
+ import ctypes
+
+ STD_OUTPUT_HANDLE = ctypes.c_uint(-11)
+ INVALID_HANDLE_VALUE = ctypes.c_uint(-1)
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x004
+
+ handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
+ if handle == INVALID_HANDLE_VALUE:
+ raise RuntimeError("unable to obtain stdout handle")
+
+ cur_mode = ctypes.c_uint()
+ if ctypes.windll.kernel32.GetConsoleMode(handle, ctypes.byref(cur_mode)) == 0:
+ raise RuntimeError("unable to query current console mode")
+
+ # ctypes ints lack support for the required bit-OR operation.
+ # Temporarily convert to Py int, do the OR and convert back.
+ py_int_mode = int.from_bytes(cur_mode, sys.byteorder)
+ new_mode = ctypes.c_uint(py_int_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
+
+ if ctypes.windll.kernel32.SetConsoleMode(handle, new_mode) == 0:
+ raise RuntimeError("unable to set console mode")
+
+
+def exit_from_event_loop_thread(
+ loop: asyncio.AbstractEventLoop, stop: "asyncio.Future[None]"
+) -> None:
+ loop.stop()
+ if not stop.done():
+ # When exiting the thread that runs the event loop, raise
+ # KeyboardInterrupt in the main thread to exit the program.
+ try:
+ ctrl_c = signal.CTRL_C_EVENT # Windows
+ except AttributeError:
+ ctrl_c = signal.SIGINT # POSIX
+ os.kill(os.getpid(), ctrl_c)
+
+
+def print_during_input(string: str) -> None:
+ sys.stdout.write(
+ # Save cursor position
+ "\N{ESC}7"
+ # Add a new line
+ "\N{LINE FEED}"
+ # Move cursor up
+ "\N{ESC}[A"
+ # Insert blank line, scroll last line down
+ "\N{ESC}[L"
+ # Print string in the inserted blank line
+ f"{string}\N{LINE FEED}"
+ # Restore cursor position
+ "\N{ESC}8"
+ # Move cursor down
+ "\N{ESC}[B"
+ )
+ sys.stdout.flush()
+
+
+def print_over_input(string: str) -> None:
+ sys.stdout.write(
+ # Move cursor to beginning of line
+ "\N{CARRIAGE RETURN}"
+ # Delete current line
+ "\N{ESC}[K"
+ # Print string
+ f"{string}\N{LINE FEED}"
+ )
+ sys.stdout.flush()
+
+
+async def run_client(
+ uri: str,
+ loop: asyncio.AbstractEventLoop,
+ inputs: "asyncio.Queue[str]",
+ stop: "asyncio.Future[None]",
+) -> None:
+ try:
+ websocket = await connect(uri)
+ except Exception as exc:
+ print_over_input(f"Failed to connect to {uri}: {exc}.")
+ exit_from_event_loop_thread(loop, stop)
+ return
+ else:
+ print_during_input(f"Connected to {uri}.")
+
+ try:
+ while True:
+ incoming: asyncio.Future[Any] = asyncio.ensure_future(websocket.recv())
+ outgoing: asyncio.Future[Any] = asyncio.ensure_future(inputs.get())
+ done: Set[asyncio.Future[Any]]
+ pending: Set[asyncio.Future[Any]]
+ done, pending = await asyncio.wait(
+ [incoming, outgoing, stop], return_when=asyncio.FIRST_COMPLETED
+ )
+
+ # Cancel pending tasks to avoid leaking them.
+ if incoming in pending:
+ incoming.cancel()
+ if outgoing in pending:
+ outgoing.cancel()
+
+ if incoming in done:
+ try:
+ message = incoming.result()
+ except ConnectionClosed:
+ break
+ else:
+ if isinstance(message, str):
+ print_during_input("< " + message)
+ else:
+ print_during_input("< (binary) " + message.hex())
+
+ if outgoing in done:
+ message = outgoing.result()
+ await websocket.send(message)
+
+ if stop in done:
+ break
+
+ finally:
+ await websocket.close()
+ close_status = format_close(websocket.close_code, websocket.close_reason)
+
+ print_over_input(f"Connection closed: {close_status}.")
+
+ exit_from_event_loop_thread(loop, stop)
+
+
+def main() -> None:
+ # If we're on Windows, enable VT100 terminal support.
+ if sys.platform == "win32":
+ try:
+ win_enable_vt100()
+ except RuntimeError as exc:
+ sys.stderr.write(
+ f"Unable to set terminal to VT100 mode. This is only "
+ f"supported since Win10 anniversary update. Expect "
+ f"weird symbols on the terminal.\nError: {exc}\n"
+ )
+ sys.stderr.flush()
+
+ try:
+ import readline # noqa
+ except ImportError: # Windows has no `readline` normally
+ pass
+
+ # Parse command line arguments.
+ parser = argparse.ArgumentParser(
+ prog="python -m websockets",
+ description="Interactive WebSocket client.",
+ add_help=False,
+ )
+ parser.add_argument("uri", metavar="<uri>")
+ args = parser.parse_args()
+
+ # Create an event loop that will run in a background thread.
+ loop = asyncio.new_event_loop()
+
+ # Create a queue of user inputs. There's no need to limit its size.
+ inputs: asyncio.Queue[str] = asyncio.Queue(loop=loop)
+
+ # Create a stop condition when receiving SIGINT or SIGTERM.
+ stop: asyncio.Future[None] = loop.create_future()
+
+ # Schedule the task that will manage the connection.
+ asyncio.ensure_future(run_client(args.uri, loop, inputs, stop), loop=loop)
+
+ # Start the event loop in a background thread.
+ thread = threading.Thread(target=loop.run_forever)
+ thread.start()
+
+ # Read from stdin in the main thread in order to receive signals.
+ try:
+ while True:
+ # Since there's no size limit, put_nowait is identical to put.
+ message = input("> ")
+ loop.call_soon_threadsafe(inputs.put_nowait, message)
+ except (KeyboardInterrupt, EOFError): # ^C, ^D
+ loop.call_soon_threadsafe(stop.set_result, None)
+
+ # Wait for the event loop to terminate.
+ thread.join()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py
new file mode 100644
index 0000000000..ae204b8d9c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py
@@ -0,0 +1,160 @@
+"""
+:mod:`websockets.auth` provides HTTP Basic Authentication according to
+:rfc:`7235` and :rfc:`7617`.
+
+"""
+
+
+import functools
+import http
+from typing import Any, Awaitable, Callable, Iterable, Optional, Tuple, Type, Union
+
+from .exceptions import InvalidHeader
+from .headers import build_www_authenticate_basic, parse_authorization_basic
+from .http import Headers
+from .server import HTTPResponse, WebSocketServerProtocol
+
+
+__all__ = ["BasicAuthWebSocketServerProtocol", "basic_auth_protocol_factory"]
+
+Credentials = Tuple[str, str]
+
+
+def is_credentials(value: Any) -> bool:
+ try:
+ username, password = value
+ except (TypeError, ValueError):
+ return False
+ else:
+ return isinstance(username, str) and isinstance(password, str)
+
+
+class BasicAuthWebSocketServerProtocol(WebSocketServerProtocol):
+ """
+ WebSocket server protocol that enforces HTTP Basic Auth.
+
+ """
+
+ def __init__(
+ self,
+ *args: Any,
+ realm: str,
+ check_credentials: Callable[[str, str], Awaitable[bool]],
+ **kwargs: Any,
+ ) -> None:
+ self.realm = realm
+ self.check_credentials = check_credentials
+ super().__init__(*args, **kwargs)
+
+ async def process_request(
+ self, path: str, request_headers: Headers
+ ) -> Optional[HTTPResponse]:
+ """
+ Check HTTP Basic Auth and return a HTTP 401 or 403 response if needed.
+
+ If authentication succeeds, the username of the authenticated user is
+ stored in the ``username`` attribute.
+
+ """
+ try:
+ authorization = request_headers["Authorization"]
+ except KeyError:
+ return (
+ http.HTTPStatus.UNAUTHORIZED,
+ [("WWW-Authenticate", build_www_authenticate_basic(self.realm))],
+ b"Missing credentials\n",
+ )
+
+ try:
+ username, password = parse_authorization_basic(authorization)
+ except InvalidHeader:
+ return (
+ http.HTTPStatus.UNAUTHORIZED,
+ [("WWW-Authenticate", build_www_authenticate_basic(self.realm))],
+ b"Unsupported credentials\n",
+ )
+
+ if not await self.check_credentials(username, password):
+ return (
+ http.HTTPStatus.UNAUTHORIZED,
+ [("WWW-Authenticate", build_www_authenticate_basic(self.realm))],
+ b"Invalid credentials\n",
+ )
+
+ self.username = username
+
+ return await super().process_request(path, request_headers)
+
+
+def basic_auth_protocol_factory(
+ realm: str,
+ credentials: Optional[Union[Credentials, Iterable[Credentials]]] = None,
+ check_credentials: Optional[Callable[[str, str], Awaitable[bool]]] = None,
+ create_protocol: Type[
+ BasicAuthWebSocketServerProtocol
+ ] = BasicAuthWebSocketServerProtocol,
+) -> Callable[[Any], BasicAuthWebSocketServerProtocol]:
+ """
+ Protocol factory that enforces HTTP Basic Auth.
+
+ ``basic_auth_protocol_factory`` is designed to integrate with
+ :func:`~websockets.server.serve` like this::
+
+ websockets.serve(
+ ...,
+ create_protocol=websockets.basic_auth_protocol_factory(
+ realm="my dev server",
+ credentials=("hello", "iloveyou"),
+ )
+ )
+
+ ``realm`` indicates the scope of protection. It should contain only ASCII
+ characters because the encoding of non-ASCII characters is undefined.
+ Refer to section 2.2 of :rfc:`7235` for details.
+
+ ``credentials`` defines hard coded authorized credentials. It can be a
+ ``(username, password)`` pair or a list of such pairs.
+
+ ``check_credentials`` defines a coroutine that checks whether credentials
+ are authorized. This coroutine receives ``username`` and ``password``
+ arguments and returns a :class:`bool`.
+
+ One of ``credentials`` or ``check_credentials`` must be provided but not
+ both.
+
+ By default, ``basic_auth_protocol_factory`` creates a factory for building
+ :class:`BasicAuthWebSocketServerProtocol` instances. You can override this
+ with the ``create_protocol`` parameter.
+
+ :param realm: scope of protection
+ :param credentials: hard coded credentials
+ :param check_credentials: coroutine that verifies credentials
+ :raises TypeError: if the credentials argument has the wrong type
+
+ """
+ if (credentials is None) == (check_credentials is None):
+ raise TypeError("provide either credentials or check_credentials")
+
+ if credentials is not None:
+ if is_credentials(credentials):
+
+ async def check_credentials(username: str, password: str) -> bool:
+ return (username, password) == credentials
+
+ elif isinstance(credentials, Iterable):
+ credentials_list = list(credentials)
+ if all(is_credentials(item) for item in credentials_list):
+ credentials_dict = dict(credentials_list)
+
+ async def check_credentials(username: str, password: str) -> bool:
+ return credentials_dict.get(username) == password
+
+ else:
+ raise TypeError(f"invalid credentials argument: {credentials}")
+
+ else:
+ raise TypeError(f"invalid credentials argument: {credentials}")
+
+ return functools.partial(
+ create_protocol, realm=realm, check_credentials=check_credentials
+ )
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py
new file mode 100644
index 0000000000..eb58f9f484
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py
@@ -0,0 +1,584 @@
+"""
+:mod:`websockets.client` defines the WebSocket client APIs.
+
+"""
+
+import asyncio
+import collections.abc
+import functools
+import logging
+import warnings
+from types import TracebackType
+from typing import Any, Generator, List, Optional, Sequence, Tuple, Type, cast
+
+from .exceptions import (
+ InvalidHandshake,
+ InvalidHeader,
+ InvalidMessage,
+ InvalidStatusCode,
+ NegotiationError,
+ RedirectHandshake,
+ SecurityError,
+)
+from .extensions.base import ClientExtensionFactory, Extension
+from .extensions.permessage_deflate import ClientPerMessageDeflateFactory
+from .handshake import build_request, check_response
+from .headers import (
+ build_authorization_basic,
+ build_extension,
+ build_subprotocol,
+ parse_extension,
+ parse_subprotocol,
+)
+from .http import USER_AGENT, Headers, HeadersLike, read_response
+from .protocol import WebSocketCommonProtocol
+from .typing import ExtensionHeader, Origin, Subprotocol
+from .uri import WebSocketURI, parse_uri
+
+
+__all__ = ["connect", "unix_connect", "WebSocketClientProtocol"]
+
+logger = logging.getLogger(__name__)
+
+
+class WebSocketClientProtocol(WebSocketCommonProtocol):
+ """
+ :class:`~asyncio.Protocol` subclass implementing a WebSocket client.
+
+ This class inherits most of its methods from
+ :class:`~websockets.protocol.WebSocketCommonProtocol`.
+
+ """
+
+ is_client = True
+ side = "client"
+
+ def __init__(
+ self,
+ *,
+ origin: Optional[Origin] = None,
+ extensions: Optional[Sequence[ClientExtensionFactory]] = None,
+ subprotocols: Optional[Sequence[Subprotocol]] = None,
+ extra_headers: Optional[HeadersLike] = None,
+ **kwargs: Any,
+ ) -> None:
+ self.origin = origin
+ self.available_extensions = extensions
+ self.available_subprotocols = subprotocols
+ self.extra_headers = extra_headers
+ super().__init__(**kwargs)
+
+ def write_http_request(self, path: str, headers: Headers) -> None:
+ """
+ Write request line and headers to the HTTP request.
+
+ """
+ self.path = path
+ self.request_headers = headers
+
+ logger.debug("%s > GET %s HTTP/1.1", self.side, path)
+ logger.debug("%s > %r", self.side, headers)
+
+ # Since the path and headers only contain ASCII characters,
+ # we can keep this simple.
+ request = f"GET {path} HTTP/1.1\r\n"
+ request += str(headers)
+
+ self.transport.write(request.encode())
+
+ async def read_http_response(self) -> Tuple[int, Headers]:
+ """
+ Read status line and headers from the HTTP response.
+
+ If the response contains a body, it may be read from ``self.reader``
+ after this coroutine returns.
+
+ :raises ~websockets.exceptions.InvalidMessage: if the HTTP message is
+ malformed or isn't an HTTP/1.1 GET response
+
+ """
+ try:
+ status_code, reason, headers = await read_response(self.reader)
+ except Exception as exc:
+ raise InvalidMessage("did not receive a valid HTTP response") from exc
+
+ logger.debug("%s < HTTP/1.1 %d %s", self.side, status_code, reason)
+ logger.debug("%s < %r", self.side, headers)
+
+ self.response_headers = headers
+
+ return status_code, self.response_headers
+
+ @staticmethod
+ def process_extensions(
+ headers: Headers,
+ available_extensions: Optional[Sequence[ClientExtensionFactory]],
+ ) -> List[Extension]:
+ """
+ Handle the Sec-WebSocket-Extensions HTTP response header.
+
+ Check that each extension is supported, as well as its parameters.
+
+ Return the list of accepted extensions.
+
+ Raise :exc:`~websockets.exceptions.InvalidHandshake` to abort the
+ connection.
+
+ :rfc:`6455` leaves the rules up to the specification of each
+ :extension.
+
+ To provide this level of flexibility, for each extension accepted by
+ the server, we check for a match with each extension available in the
+ client configuration. If no match is found, an exception is raised.
+
+ If several variants of the same extension are accepted by the server,
+ it may be configured severel times, which won't make sense in general.
+ Extensions must implement their own requirements. For this purpose,
+ the list of previously accepted extensions is provided.
+
+ Other requirements, for example related to mandatory extensions or the
+ order of extensions, may be implemented by overriding this method.
+
+ """
+ accepted_extensions: List[Extension] = []
+
+ header_values = headers.get_all("Sec-WebSocket-Extensions")
+
+ if header_values:
+
+ if available_extensions is None:
+ raise InvalidHandshake("no extensions supported")
+
+ parsed_header_values: List[ExtensionHeader] = sum(
+ [parse_extension(header_value) for header_value in header_values], []
+ )
+
+ for name, response_params in parsed_header_values:
+
+ for extension_factory in available_extensions:
+
+ # Skip non-matching extensions based on their name.
+ if extension_factory.name != name:
+ continue
+
+ # Skip non-matching extensions based on their params.
+ try:
+ extension = extension_factory.process_response_params(
+ response_params, accepted_extensions
+ )
+ except NegotiationError:
+ continue
+
+ # Add matching extension to the final list.
+ accepted_extensions.append(extension)
+
+ # Break out of the loop once we have a match.
+ break
+
+ # If we didn't break from the loop, no extension in our list
+ # matched what the server sent. Fail the connection.
+ else:
+ raise NegotiationError(
+ f"Unsupported extension: "
+ f"name = {name}, params = {response_params}"
+ )
+
+ return accepted_extensions
+
+ @staticmethod
+ def process_subprotocol(
+ headers: Headers, available_subprotocols: Optional[Sequence[Subprotocol]]
+ ) -> Optional[Subprotocol]:
+ """
+ Handle the Sec-WebSocket-Protocol HTTP response header.
+
+ Check that it contains exactly one supported subprotocol.
+
+ Return the selected subprotocol.
+
+ """
+ subprotocol: Optional[Subprotocol] = None
+
+ header_values = headers.get_all("Sec-WebSocket-Protocol")
+
+ if header_values:
+
+ if available_subprotocols is None:
+ raise InvalidHandshake("no subprotocols supported")
+
+ parsed_header_values: Sequence[Subprotocol] = sum(
+ [parse_subprotocol(header_value) for header_value in header_values], []
+ )
+
+ if len(parsed_header_values) > 1:
+ subprotocols = ", ".join(parsed_header_values)
+ raise InvalidHandshake(f"multiple subprotocols: {subprotocols}")
+
+ subprotocol = parsed_header_values[0]
+
+ if subprotocol not in available_subprotocols:
+ raise NegotiationError(f"unsupported subprotocol: {subprotocol}")
+
+ return subprotocol
+
+ async def handshake(
+ self,
+ wsuri: WebSocketURI,
+ origin: Optional[Origin] = None,
+ available_extensions: Optional[Sequence[ClientExtensionFactory]] = None,
+ available_subprotocols: Optional[Sequence[Subprotocol]] = None,
+ extra_headers: Optional[HeadersLike] = None,
+ ) -> None:
+ """
+ Perform the client side of the opening handshake.
+
+ :param origin: sets the Origin HTTP header
+ :param available_extensions: list of supported extensions in the order
+ in which they should be used
+ :param available_subprotocols: list of supported subprotocols in order
+ of decreasing preference
+ :param extra_headers: sets additional HTTP request headers; it must be
+ a :class:`~websockets.http.Headers` instance, a
+ :class:`~collections.abc.Mapping`, or an iterable of ``(name,
+ value)`` pairs
+ :raises ~websockets.exceptions.InvalidHandshake: if the handshake
+ fails
+
+ """
+ request_headers = Headers()
+
+ if wsuri.port == (443 if wsuri.secure else 80): # pragma: no cover
+ request_headers["Host"] = wsuri.host
+ else:
+ request_headers["Host"] = f"{wsuri.host}:{wsuri.port}"
+
+ if wsuri.user_info:
+ request_headers["Authorization"] = build_authorization_basic(
+ *wsuri.user_info
+ )
+
+ if origin is not None:
+ request_headers["Origin"] = origin
+
+ key = build_request(request_headers)
+
+ if available_extensions is not None:
+ extensions_header = build_extension(
+ [
+ (extension_factory.name, extension_factory.get_request_params())
+ for extension_factory in available_extensions
+ ]
+ )
+ request_headers["Sec-WebSocket-Extensions"] = extensions_header
+
+ if available_subprotocols is not None:
+ protocol_header = build_subprotocol(available_subprotocols)
+ request_headers["Sec-WebSocket-Protocol"] = protocol_header
+
+ if extra_headers is not None:
+ if isinstance(extra_headers, Headers):
+ extra_headers = extra_headers.raw_items()
+ elif isinstance(extra_headers, collections.abc.Mapping):
+ extra_headers = extra_headers.items()
+ for name, value in extra_headers:
+ request_headers[name] = value
+
+ request_headers.setdefault("User-Agent", USER_AGENT)
+
+ self.write_http_request(wsuri.resource_name, request_headers)
+
+ status_code, response_headers = await self.read_http_response()
+ if status_code in (301, 302, 303, 307, 308):
+ if "Location" not in response_headers:
+ raise InvalidHeader("Location")
+ raise RedirectHandshake(response_headers["Location"])
+ elif status_code != 101:
+ raise InvalidStatusCode(status_code)
+
+ check_response(response_headers, key)
+
+ self.extensions = self.process_extensions(
+ response_headers, available_extensions
+ )
+
+ self.subprotocol = self.process_subprotocol(
+ response_headers, available_subprotocols
+ )
+
+ self.connection_open()
+
+
+class Connect:
+ """
+ Connect to the WebSocket server at the given ``uri``.
+
+ Awaiting :func:`connect` yields a :class:`WebSocketClientProtocol` which
+ can then be used to send and receive messages.
+
+ :func:`connect` can also be used as a asynchronous context manager. In
+ that case, the connection is closed when exiting the context.
+
+ :func:`connect` is a wrapper around the event loop's
+ :meth:`~asyncio.loop.create_connection` method. Unknown keyword arguments
+ are passed to :meth:`~asyncio.loop.create_connection`.
+
+ For example, you can set the ``ssl`` keyword argument to a
+ :class:`~ssl.SSLContext` to enforce some TLS settings. When connecting to
+ a ``wss://`` URI, if this argument isn't provided explicitly,
+ :func:`ssl.create_default_context` is called to create a context.
+
+ You can connect to a different host and port from those found in ``uri``
+ by setting ``host`` and ``port`` keyword arguments. This only changes the
+ destination of the TCP connection. The host name from ``uri`` is still
+ used in the TLS handshake for secure connections and in the ``Host`` HTTP
+ header.
+
+ The ``create_protocol`` parameter allows customizing the
+ :class:`~asyncio.Protocol` that manages the connection. It should be a
+ callable or class accepting the same arguments as
+ :class:`WebSocketClientProtocol` and returning an instance of
+ :class:`WebSocketClientProtocol` or a subclass. It defaults to
+ :class:`WebSocketClientProtocol`.
+
+ The behavior of ``ping_interval``, ``ping_timeout``, ``close_timeout``,
+ ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit`` is
+ described in :class:`~websockets.protocol.WebSocketCommonProtocol`.
+
+ :func:`connect` also accepts the following optional arguments:
+
+ * ``compression`` is a shortcut to configure compression extensions;
+ by default it enables the "permessage-deflate" extension; set it to
+ ``None`` to disable compression
+ * ``origin`` sets the Origin HTTP header
+ * ``extensions`` is a list of supported extensions in order of
+ decreasing preference
+ * ``subprotocols`` is a list of supported subprotocols in order of
+ decreasing preference
+ * ``extra_headers`` sets additional HTTP request headers; it can be a
+ :class:`~websockets.http.Headers` instance, a
+ :class:`~collections.abc.Mapping`, or an iterable of ``(name, value)``
+ pairs
+
+ :raises ~websockets.uri.InvalidURI: if ``uri`` is invalid
+ :raises ~websockets.handshake.InvalidHandshake: if the opening handshake
+ fails
+
+ """
+
+ MAX_REDIRECTS_ALLOWED = 10
+
+ def __init__(
+ self,
+ uri: str,
+ *,
+ path: Optional[str] = None,
+ create_protocol: Optional[Type[WebSocketClientProtocol]] = None,
+ ping_interval: float = 20,
+ ping_timeout: float = 20,
+ close_timeout: Optional[float] = None,
+ max_size: int = 2 ** 20,
+ max_queue: int = 2 ** 5,
+ read_limit: int = 2 ** 16,
+ write_limit: int = 2 ** 16,
+ loop: Optional[asyncio.AbstractEventLoop] = None,
+ legacy_recv: bool = False,
+ klass: Optional[Type[WebSocketClientProtocol]] = None,
+ timeout: Optional[float] = None,
+ compression: Optional[str] = "deflate",
+ origin: Optional[Origin] = None,
+ extensions: Optional[Sequence[ClientExtensionFactory]] = None,
+ subprotocols: Optional[Sequence[Subprotocol]] = None,
+ extra_headers: Optional[HeadersLike] = None,
+ **kwargs: Any,
+ ) -> None:
+ # Backwards compatibility: close_timeout used to be called timeout.
+ if timeout is None:
+ timeout = 10
+ else:
+ warnings.warn("rename timeout to close_timeout", DeprecationWarning)
+ # If both are specified, timeout is ignored.
+ if close_timeout is None:
+ close_timeout = timeout
+
+ # Backwards compatibility: create_protocol used to be called klass.
+ if klass is None:
+ klass = WebSocketClientProtocol
+ else:
+ warnings.warn("rename klass to create_protocol", DeprecationWarning)
+ # If both are specified, klass is ignored.
+ if create_protocol is None:
+ create_protocol = klass
+
+ if loop is None:
+ loop = asyncio.get_event_loop()
+
+ wsuri = parse_uri(uri)
+ if wsuri.secure:
+ kwargs.setdefault("ssl", True)
+ elif kwargs.get("ssl") is not None:
+ raise ValueError(
+ "connect() received a ssl argument for a ws:// URI, "
+ "use a wss:// URI to enable TLS"
+ )
+
+ if compression == "deflate":
+ if extensions is None:
+ extensions = []
+ if not any(
+ extension_factory.name == ClientPerMessageDeflateFactory.name
+ for extension_factory in extensions
+ ):
+ extensions = list(extensions) + [
+ ClientPerMessageDeflateFactory(client_max_window_bits=True)
+ ]
+ elif compression is not None:
+ raise ValueError(f"unsupported compression: {compression}")
+
+ factory = functools.partial(
+ create_protocol,
+ ping_interval=ping_interval,
+ ping_timeout=ping_timeout,
+ close_timeout=close_timeout,
+ max_size=max_size,
+ max_queue=max_queue,
+ read_limit=read_limit,
+ write_limit=write_limit,
+ loop=loop,
+ host=wsuri.host,
+ port=wsuri.port,
+ secure=wsuri.secure,
+ legacy_recv=legacy_recv,
+ origin=origin,
+ extensions=extensions,
+ subprotocols=subprotocols,
+ extra_headers=extra_headers,
+ )
+
+ if path is None:
+ host: Optional[str]
+ port: Optional[int]
+ if kwargs.get("sock") is None:
+ host, port = wsuri.host, wsuri.port
+ else:
+ # If sock is given, host and port shouldn't be specified.
+ host, port = None, None
+ # If host and port are given, override values from the URI.
+ host = kwargs.pop("host", host)
+ port = kwargs.pop("port", port)
+ create_connection = functools.partial(
+ loop.create_connection, factory, host, port, **kwargs
+ )
+ else:
+ create_connection = functools.partial(
+ loop.create_unix_connection, factory, path, **kwargs
+ )
+
+ # This is a coroutine function.
+ self._create_connection = create_connection
+ self._wsuri = wsuri
+
+ def handle_redirect(self, uri: str) -> None:
+ # Update the state of this instance to connect to a new URI.
+ old_wsuri = self._wsuri
+ new_wsuri = parse_uri(uri)
+
+ # Forbid TLS downgrade.
+ if old_wsuri.secure and not new_wsuri.secure:
+ raise SecurityError("redirect from WSS to WS")
+
+ same_origin = (
+ old_wsuri.host == new_wsuri.host and old_wsuri.port == new_wsuri.port
+ )
+
+ # Rewrite the host and port arguments for cross-origin redirects.
+ # This preserves connection overrides with the host and port
+ # arguments if the redirect points to the same host and port.
+ if not same_origin:
+ # Replace the host and port argument passed to the protocol factory.
+ factory = self._create_connection.args[0]
+ factory = functools.partial(
+ factory.func,
+ *factory.args,
+ **dict(factory.keywords, host=new_wsuri.host, port=new_wsuri.port),
+ )
+ # Replace the host and port argument passed to create_connection.
+ self._create_connection = functools.partial(
+ self._create_connection.func,
+ *(factory, new_wsuri.host, new_wsuri.port),
+ **self._create_connection.keywords,
+ )
+
+ # Set the new WebSocket URI. This suffices for same-origin redirects.
+ self._wsuri = new_wsuri
+
+ # async with connect(...)
+
+ async def __aenter__(self) -> WebSocketClientProtocol:
+ return await self
+
+ async def __aexit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_value: Optional[BaseException],
+ traceback: Optional[TracebackType],
+ ) -> None:
+ await self.ws_client.close()
+
+ # await connect(...)
+
+ def __await__(self) -> Generator[Any, None, WebSocketClientProtocol]:
+ # Create a suitable iterator by calling __await__ on a coroutine.
+ return self.__await_impl__().__await__()
+
+ async def __await_impl__(self) -> WebSocketClientProtocol:
+ for redirects in range(self.MAX_REDIRECTS_ALLOWED):
+ transport, protocol = await self._create_connection()
+ # https://github.com/python/typeshed/pull/2756
+ transport = cast(asyncio.Transport, transport)
+ protocol = cast(WebSocketClientProtocol, protocol)
+
+ try:
+ try:
+ await protocol.handshake(
+ self._wsuri,
+ origin=protocol.origin,
+ available_extensions=protocol.available_extensions,
+ available_subprotocols=protocol.available_subprotocols,
+ extra_headers=protocol.extra_headers,
+ )
+ except Exception:
+ protocol.fail_connection()
+ await protocol.wait_closed()
+ raise
+ else:
+ self.ws_client = protocol
+ return protocol
+ except RedirectHandshake as exc:
+ self.handle_redirect(exc.uri)
+ else:
+ raise SecurityError("too many redirects")
+
+ # yield from connect(...)
+
+ __iter__ = __await__
+
+
+connect = Connect
+
+
+def unix_connect(path: str, uri: str = "ws://localhost/", **kwargs: Any) -> Connect:
+ """
+ Similar to :func:`connect`, but for connecting to a Unix socket.
+
+ This function calls the event loop's
+ :meth:`~asyncio.loop.create_unix_connection` method.
+
+ It is only available on Unix.
+
+ It's mainly useful for debugging servers listening on Unix sockets.
+
+ :param path: file system path to the Unix socket
+ :param uri: WebSocket URI
+
+ """
+ return connect(uri=uri, path=path, **kwargs)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py
new file mode 100644
index 0000000000..9873a17170
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py
@@ -0,0 +1,366 @@
+"""
+:mod:`websockets.exceptions` defines the following exception hierarchy:
+
+* :exc:`WebSocketException`
+ * :exc:`ConnectionClosed`
+ * :exc:`ConnectionClosedError`
+ * :exc:`ConnectionClosedOK`
+ * :exc:`InvalidHandshake`
+ * :exc:`SecurityError`
+ * :exc:`InvalidMessage`
+ * :exc:`InvalidHeader`
+ * :exc:`InvalidHeaderFormat`
+ * :exc:`InvalidHeaderValue`
+ * :exc:`InvalidOrigin`
+ * :exc:`InvalidUpgrade`
+ * :exc:`InvalidStatusCode`
+ * :exc:`NegotiationError`
+ * :exc:`DuplicateParameter`
+ * :exc:`InvalidParameterName`
+ * :exc:`InvalidParameterValue`
+ * :exc:`AbortHandshake`
+ * :exc:`RedirectHandshake`
+ * :exc:`InvalidState`
+ * :exc:`InvalidURI`
+ * :exc:`PayloadTooBig`
+ * :exc:`ProtocolError`
+
+"""
+
+import http
+from typing import Optional
+
+from .http import Headers, HeadersLike
+
+
+__all__ = [
+ "WebSocketException",
+ "ConnectionClosed",
+ "ConnectionClosedError",
+ "ConnectionClosedOK",
+ "InvalidHandshake",
+ "SecurityError",
+ "InvalidMessage",
+ "InvalidHeader",
+ "InvalidHeaderFormat",
+ "InvalidHeaderValue",
+ "InvalidOrigin",
+ "InvalidUpgrade",
+ "InvalidStatusCode",
+ "NegotiationError",
+ "DuplicateParameter",
+ "InvalidParameterName",
+ "InvalidParameterValue",
+ "AbortHandshake",
+ "RedirectHandshake",
+ "InvalidState",
+ "InvalidURI",
+ "PayloadTooBig",
+ "ProtocolError",
+ "WebSocketProtocolError",
+]
+
+
+class WebSocketException(Exception):
+ """
+ Base class for all exceptions defined by :mod:`websockets`.
+
+ """
+
+
+CLOSE_CODES = {
+ 1000: "OK",
+ 1001: "going away",
+ 1002: "protocol error",
+ 1003: "unsupported type",
+ # 1004 is reserved
+ 1005: "no status code [internal]",
+ 1006: "connection closed abnormally [internal]",
+ 1007: "invalid data",
+ 1008: "policy violation",
+ 1009: "message too big",
+ 1010: "extension required",
+ 1011: "unexpected error",
+ 1015: "TLS failure [internal]",
+}
+
+
+def format_close(code: int, reason: str) -> str:
+ """
+ Display a human-readable version of the close code and reason.
+
+ """
+ if 3000 <= code < 4000:
+ explanation = "registered"
+ elif 4000 <= code < 5000:
+ explanation = "private use"
+ else:
+ explanation = CLOSE_CODES.get(code, "unknown")
+ result = f"code = {code} ({explanation}), "
+
+ if reason:
+ result += f"reason = {reason}"
+ else:
+ result += "no reason"
+
+ return result
+
+
+class ConnectionClosed(WebSocketException):
+ """
+ Raised when trying to interact with a closed connection.
+
+ Provides the connection close code and reason in its ``code`` and
+ ``reason`` attributes respectively.
+
+ """
+
+ def __init__(self, code: int, reason: str) -> None:
+ self.code = code
+ self.reason = reason
+ super().__init__(format_close(code, reason))
+
+
+class ConnectionClosedError(ConnectionClosed):
+ """
+ Like :exc:`ConnectionClosed`, when the connection terminated with an error.
+
+ This means the close code is different from 1000 (OK) and 1001 (going away).
+
+ """
+
+ def __init__(self, code: int, reason: str) -> None:
+ assert code != 1000 and code != 1001
+ super().__init__(code, reason)
+
+
+class ConnectionClosedOK(ConnectionClosed):
+ """
+ Like :exc:`ConnectionClosed`, when the connection terminated properly.
+
+ This means the close code is 1000 (OK) or 1001 (going away).
+
+ """
+
+ def __init__(self, code: int, reason: str) -> None:
+ assert code == 1000 or code == 1001
+ super().__init__(code, reason)
+
+
+class InvalidHandshake(WebSocketException):
+ """
+ Raised during the handshake when the WebSocket connection fails.
+
+ """
+
+
+class SecurityError(InvalidHandshake):
+ """
+ Raised when a handshake request or response breaks a security rule.
+
+ Security limits are hard coded.
+
+ """
+
+
+class InvalidMessage(InvalidHandshake):
+ """
+ Raised when a handshake request or response is malformed.
+
+ """
+
+
+class InvalidHeader(InvalidHandshake):
+ """
+ Raised when a HTTP header doesn't have a valid format or value.
+
+ """
+
+ def __init__(self, name: str, value: Optional[str] = None) -> None:
+ self.name = name
+ self.value = value
+ if value is None:
+ message = f"missing {name} header"
+ elif value == "":
+ message = f"empty {name} header"
+ else:
+ message = f"invalid {name} header: {value}"
+ super().__init__(message)
+
+
+class InvalidHeaderFormat(InvalidHeader):
+ """
+ Raised when a HTTP header cannot be parsed.
+
+ The format of the header doesn't match the grammar for that header.
+
+ """
+
+ def __init__(self, name: str, error: str, header: str, pos: int) -> None:
+ self.name = name
+ error = f"{error} at {pos} in {header}"
+ super().__init__(name, error)
+
+
+class InvalidHeaderValue(InvalidHeader):
+ """
+ Raised when a HTTP header has a wrong value.
+
+ The format of the header is correct but a value isn't acceptable.
+
+ """
+
+
+class InvalidOrigin(InvalidHeader):
+ """
+ Raised when the Origin header in a request isn't allowed.
+
+ """
+
+ def __init__(self, origin: Optional[str]) -> None:
+ super().__init__("Origin", origin)
+
+
+class InvalidUpgrade(InvalidHeader):
+ """
+ Raised when the Upgrade or Connection header isn't correct.
+
+ """
+
+
+class InvalidStatusCode(InvalidHandshake):
+ """
+ Raised when a handshake response status code is invalid.
+
+ The integer status code is available in the ``status_code`` attribute.
+
+ """
+
+ def __init__(self, status_code: int) -> None:
+ self.status_code = status_code
+ message = f"server rejected WebSocket connection: HTTP {status_code}"
+ super().__init__(message)
+
+
+class NegotiationError(InvalidHandshake):
+ """
+ Raised when negotiating an extension fails.
+
+ """
+
+
+class DuplicateParameter(NegotiationError):
+ """
+ Raised when a parameter name is repeated in an extension header.
+
+ """
+
+ def __init__(self, name: str) -> None:
+ self.name = name
+ message = f"duplicate parameter: {name}"
+ super().__init__(message)
+
+
+class InvalidParameterName(NegotiationError):
+ """
+ Raised when a parameter name in an extension header is invalid.
+
+ """
+
+ def __init__(self, name: str) -> None:
+ self.name = name
+ message = f"invalid parameter name: {name}"
+ super().__init__(message)
+
+
+class InvalidParameterValue(NegotiationError):
+ """
+ Raised when a parameter value in an extension header is invalid.
+
+ """
+
+ def __init__(self, name: str, value: Optional[str]) -> None:
+ self.name = name
+ self.value = value
+ if value is None:
+ message = f"missing value for parameter {name}"
+ elif value == "":
+ message = f"empty value for parameter {name}"
+ else:
+ message = f"invalid value for parameter {name}: {value}"
+ super().__init__(message)
+
+
+class AbortHandshake(InvalidHandshake):
+ """
+ Raised to abort the handshake on purpose and return a HTTP response.
+
+ This exception is an implementation detail.
+
+ The public API is :meth:`~server.WebSocketServerProtocol.process_request`.
+
+ """
+
+ def __init__(
+ self, status: http.HTTPStatus, headers: HeadersLike, body: bytes = b""
+ ) -> None:
+ self.status = status
+ self.headers = Headers(headers)
+ self.body = body
+ message = f"HTTP {status}, {len(self.headers)} headers, {len(body)} bytes"
+ super().__init__(message)
+
+
+class RedirectHandshake(InvalidHandshake):
+ """
+ Raised when a handshake gets redirected.
+
+ This exception is an implementation detail.
+
+ """
+
+ def __init__(self, uri: str) -> None:
+ self.uri = uri
+
+ def __str__(self) -> str:
+ return f"redirect to {self.uri}"
+
+
+class InvalidState(WebSocketException, AssertionError):
+ """
+ Raised when an operation is forbidden in the current state.
+
+ This exception is an implementation detail.
+
+ It should never be raised in normal circumstances.
+
+ """
+
+
+class InvalidURI(WebSocketException):
+ """
+ Raised when connecting to an URI that isn't a valid WebSocket URI.
+
+ """
+
+ def __init__(self, uri: str) -> None:
+ self.uri = uri
+ message = "{} isn't a valid URI".format(uri)
+ super().__init__(message)
+
+
+class PayloadTooBig(WebSocketException):
+ """
+ Raised when receiving a frame with a payload exceeding the maximum size.
+
+ """
+
+
+class ProtocolError(WebSocketException):
+ """
+ Raised when the other side breaks the protocol.
+
+ """
+
+
+WebSocketProtocolError = ProtocolError # for backwards compatibility
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py
new file mode 100644
index 0000000000..aa52a7adbf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py
@@ -0,0 +1,119 @@
+"""
+:mod:`websockets.extensions.base` defines abstract classes for implementing
+extensions.
+
+See `section 9 of RFC 6455`_.
+
+.. _section 9 of RFC 6455: http://tools.ietf.org/html/rfc6455#section-9
+
+"""
+
+from typing import List, Optional, Sequence, Tuple
+
+from ..framing import Frame
+from ..typing import ExtensionName, ExtensionParameter
+
+
+__all__ = ["Extension", "ClientExtensionFactory", "ServerExtensionFactory"]
+
+
+class Extension:
+ """
+ Abstract class for extensions.
+
+ """
+
+ @property
+ def name(self) -> ExtensionName:
+ """
+ Extension identifier.
+
+ """
+
+ def decode(self, frame: Frame, *, max_size: Optional[int] = None) -> Frame:
+ """
+ Decode an incoming frame.
+
+ :param frame: incoming frame
+ :param max_size: maximum payload size in bytes
+
+ """
+
+ def encode(self, frame: Frame) -> Frame:
+ """
+ Encode an outgoing frame.
+
+ :param frame: outgoing frame
+
+ """
+
+
+class ClientExtensionFactory:
+ """
+ Abstract class for client-side extension factories.
+
+ """
+
+ @property
+ def name(self) -> ExtensionName:
+ """
+ Extension identifier.
+
+ """
+
+ def get_request_params(self) -> List[ExtensionParameter]:
+ """
+ Build request parameters.
+
+ Return a list of ``(name, value)`` pairs.
+
+ """
+
+ def process_response_params(
+ self,
+ params: Sequence[ExtensionParameter],
+ accepted_extensions: Sequence[Extension],
+ ) -> Extension:
+ """
+ Process response parameters received from the server.
+
+ :param params: list of ``(name, value)`` pairs.
+ :param accepted_extensions: list of previously accepted extensions.
+ :raises ~websockets.exceptions.NegotiationError: if parameters aren't
+ acceptable
+
+ """
+
+
+class ServerExtensionFactory:
+ """
+ Abstract class for server-side extension factories.
+
+ """
+
+ @property
+ def name(self) -> ExtensionName:
+ """
+ Extension identifier.
+
+ """
+
+ def process_request_params(
+ self,
+ params: Sequence[ExtensionParameter],
+ accepted_extensions: Sequence[Extension],
+ ) -> Tuple[List[ExtensionParameter], Extension]:
+ """
+ Process request parameters received from the client.
+
+ To accept the offer, return a 2-uple containing:
+
+ - response parameters: a list of ``(name, value)`` pairs
+ - an extension: an instance of a subclass of :class:`Extension`
+
+ :param params: list of ``(name, value)`` pairs.
+ :param accepted_extensions: list of previously accepted extensions.
+ :raises ~websockets.exceptions.NegotiationError: to reject the offer,
+ if parameters aren't acceptable
+
+ """
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py
new file mode 100644
index 0000000000..e38d9edaba
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py
@@ -0,0 +1,588 @@
+"""
+:mod:`websockets.extensions.permessage_deflate` implements the Compression
+Extensions for WebSocket as specified in :rfc:`7692`.
+
+"""
+
+import zlib
+from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
+
+from ..exceptions import (
+ DuplicateParameter,
+ InvalidParameterName,
+ InvalidParameterValue,
+ NegotiationError,
+ PayloadTooBig,
+)
+from ..framing import CTRL_OPCODES, OP_CONT, Frame
+from ..typing import ExtensionName, ExtensionParameter
+from .base import ClientExtensionFactory, Extension, ServerExtensionFactory
+
+
+__all__ = [
+ "PerMessageDeflate",
+ "ClientPerMessageDeflateFactory",
+ "ServerPerMessageDeflateFactory",
+]
+
+_EMPTY_UNCOMPRESSED_BLOCK = b"\x00\x00\xff\xff"
+
+_MAX_WINDOW_BITS_VALUES = [str(bits) for bits in range(8, 16)]
+
+
+class PerMessageDeflate(Extension):
+ """
+ Per-Message Deflate extension.
+
+ """
+
+ name = ExtensionName("permessage-deflate")
+
+ def __init__(
+ self,
+ remote_no_context_takeover: bool,
+ local_no_context_takeover: bool,
+ remote_max_window_bits: int,
+ local_max_window_bits: int,
+ compress_settings: Optional[Dict[Any, Any]] = None,
+ ) -> None:
+ """
+ Configure the Per-Message Deflate extension.
+
+ """
+ if compress_settings is None:
+ compress_settings = {}
+
+ assert remote_no_context_takeover in [False, True]
+ assert local_no_context_takeover in [False, True]
+ assert 8 <= remote_max_window_bits <= 15
+ assert 8 <= local_max_window_bits <= 15
+ assert "wbits" not in compress_settings
+
+ self.remote_no_context_takeover = remote_no_context_takeover
+ self.local_no_context_takeover = local_no_context_takeover
+ self.remote_max_window_bits = remote_max_window_bits
+ self.local_max_window_bits = local_max_window_bits
+ self.compress_settings = compress_settings
+
+ if not self.remote_no_context_takeover:
+ self.decoder = zlib.decompressobj(wbits=-self.remote_max_window_bits)
+
+ if not self.local_no_context_takeover:
+ self.encoder = zlib.compressobj(
+ wbits=-self.local_max_window_bits, **self.compress_settings
+ )
+
+ # To handle continuation frames properly, we must keep track of
+ # whether that initial frame was encoded.
+ self.decode_cont_data = False
+ # There's no need for self.encode_cont_data because we always encode
+ # outgoing frames, so it would always be True.
+
+ def __repr__(self) -> str:
+ return (
+ f"PerMessageDeflate("
+ f"remote_no_context_takeover={self.remote_no_context_takeover}, "
+ f"local_no_context_takeover={self.local_no_context_takeover}, "
+ f"remote_max_window_bits={self.remote_max_window_bits}, "
+ f"local_max_window_bits={self.local_max_window_bits})"
+ )
+
+ def decode(self, frame: Frame, *, max_size: Optional[int] = None) -> Frame:
+ """
+ Decode an incoming frame.
+
+ """
+ # Skip control frames.
+ if frame.opcode in CTRL_OPCODES:
+ return frame
+
+ # Handle continuation data frames:
+ # - skip if the initial data frame wasn't encoded
+ # - reset "decode continuation data" flag if it's a final frame
+ if frame.opcode == OP_CONT:
+ if not self.decode_cont_data:
+ return frame
+ if frame.fin:
+ self.decode_cont_data = False
+
+ # Handle text and binary data frames:
+ # - skip if the frame isn't encoded
+ # - set "decode continuation data" flag if it's a non-final frame
+ else:
+ if not frame.rsv1:
+ return frame
+ if not frame.fin: # frame.rsv1 is True at this point
+ self.decode_cont_data = True
+
+ # Re-initialize per-message decoder.
+ if self.remote_no_context_takeover:
+ self.decoder = zlib.decompressobj(wbits=-self.remote_max_window_bits)
+
+ # Uncompress compressed frames. Protect against zip bombs by
+ # preventing zlib from decompressing more than max_length bytes
+ # (except when the limit is disabled with max_size = None).
+ data = frame.data
+ if frame.fin:
+ data += _EMPTY_UNCOMPRESSED_BLOCK
+ max_length = 0 if max_size is None else max_size
+ data = self.decoder.decompress(data, max_length)
+ if self.decoder.unconsumed_tail:
+ raise PayloadTooBig(
+ f"Uncompressed payload length exceeds size limit (? > {max_size} bytes)"
+ )
+
+ # Allow garbage collection of the decoder if it won't be reused.
+ if frame.fin and self.remote_no_context_takeover:
+ del self.decoder
+
+ return frame._replace(data=data, rsv1=False)
+
+ def encode(self, frame: Frame) -> Frame:
+ """
+ Encode an outgoing frame.
+
+ """
+ # Skip control frames.
+ if frame.opcode in CTRL_OPCODES:
+ return frame
+
+ # Since we always encode and never fragment messages, there's no logic
+ # similar to decode() here at this time.
+
+ if frame.opcode != OP_CONT:
+ # Re-initialize per-message decoder.
+ if self.local_no_context_takeover:
+ self.encoder = zlib.compressobj(
+ wbits=-self.local_max_window_bits, **self.compress_settings
+ )
+
+ # Compress data frames.
+ data = self.encoder.compress(frame.data) + self.encoder.flush(zlib.Z_SYNC_FLUSH)
+ if frame.fin and data.endswith(_EMPTY_UNCOMPRESSED_BLOCK):
+ data = data[:-4]
+
+ # Allow garbage collection of the encoder if it won't be reused.
+ if frame.fin and self.local_no_context_takeover:
+ del self.encoder
+
+ return frame._replace(data=data, rsv1=True)
+
+
+def _build_parameters(
+ server_no_context_takeover: bool,
+ client_no_context_takeover: bool,
+ server_max_window_bits: Optional[int],
+ client_max_window_bits: Optional[Union[int, bool]],
+) -> List[ExtensionParameter]:
+ """
+ Build a list of ``(name, value)`` pairs for some compression parameters.
+
+ """
+ params: List[ExtensionParameter] = []
+ if server_no_context_takeover:
+ params.append(("server_no_context_takeover", None))
+ if client_no_context_takeover:
+ params.append(("client_no_context_takeover", None))
+ if server_max_window_bits:
+ params.append(("server_max_window_bits", str(server_max_window_bits)))
+ if client_max_window_bits is True: # only in handshake requests
+ params.append(("client_max_window_bits", None))
+ elif client_max_window_bits:
+ params.append(("client_max_window_bits", str(client_max_window_bits)))
+ return params
+
+
+def _extract_parameters(
+ params: Sequence[ExtensionParameter], *, is_server: bool
+) -> Tuple[bool, bool, Optional[int], Optional[Union[int, bool]]]:
+ """
+ Extract compression parameters from a list of ``(name, value)`` pairs.
+
+ If ``is_server`` is ``True``, ``client_max_window_bits`` may be provided
+ without a value. This is only allow in handshake requests.
+
+ """
+ server_no_context_takeover: bool = False
+ client_no_context_takeover: bool = False
+ server_max_window_bits: Optional[int] = None
+ client_max_window_bits: Optional[Union[int, bool]] = None
+
+ for name, value in params:
+
+ if name == "server_no_context_takeover":
+ if server_no_context_takeover:
+ raise DuplicateParameter(name)
+ if value is None:
+ server_no_context_takeover = True
+ else:
+ raise InvalidParameterValue(name, value)
+
+ elif name == "client_no_context_takeover":
+ if client_no_context_takeover:
+ raise DuplicateParameter(name)
+ if value is None:
+ client_no_context_takeover = True
+ else:
+ raise InvalidParameterValue(name, value)
+
+ elif name == "server_max_window_bits":
+ if server_max_window_bits is not None:
+ raise DuplicateParameter(name)
+ if value in _MAX_WINDOW_BITS_VALUES:
+ server_max_window_bits = int(value)
+ else:
+ raise InvalidParameterValue(name, value)
+
+ elif name == "client_max_window_bits":
+ if client_max_window_bits is not None:
+ raise DuplicateParameter(name)
+ if is_server and value is None: # only in handshake requests
+ client_max_window_bits = True
+ elif value in _MAX_WINDOW_BITS_VALUES:
+ client_max_window_bits = int(value)
+ else:
+ raise InvalidParameterValue(name, value)
+
+ else:
+ raise InvalidParameterName(name)
+
+ return (
+ server_no_context_takeover,
+ client_no_context_takeover,
+ server_max_window_bits,
+ client_max_window_bits,
+ )
+
+
+class ClientPerMessageDeflateFactory(ClientExtensionFactory):
+ """
+ Client-side extension factory for the Per-Message Deflate extension.
+
+ Parameters behave as described in `section 7.1 of RFC 7692`_. Set them to
+ ``True`` to include them in the negotiation offer without a value or to an
+ integer value to include them with this value.
+
+ .. _section 7.1 of RFC 7692: https://tools.ietf.org/html/rfc7692#section-7.1
+
+ :param server_no_context_takeover: defaults to ``False``
+ :param client_no_context_takeover: defaults to ``False``
+ :param server_max_window_bits: optional, defaults to ``None``
+ :param client_max_window_bits: optional, defaults to ``None``
+ :param compress_settings: optional, keyword arguments for
+ :func:`zlib.compressobj`, excluding ``wbits``
+
+ """
+
+ name = ExtensionName("permessage-deflate")
+
+ def __init__(
+ self,
+ server_no_context_takeover: bool = False,
+ client_no_context_takeover: bool = False,
+ server_max_window_bits: Optional[int] = None,
+ client_max_window_bits: Optional[Union[int, bool]] = None,
+ compress_settings: Optional[Dict[str, Any]] = None,
+ ) -> None:
+ """
+ Configure the Per-Message Deflate extension factory.
+
+ """
+ if not (server_max_window_bits is None or 8 <= server_max_window_bits <= 15):
+ raise ValueError("server_max_window_bits must be between 8 and 15")
+ if not (
+ client_max_window_bits is None
+ or client_max_window_bits is True
+ or 8 <= client_max_window_bits <= 15
+ ):
+ raise ValueError("client_max_window_bits must be between 8 and 15")
+ if compress_settings is not None and "wbits" in compress_settings:
+ raise ValueError(
+ "compress_settings must not include wbits, "
+ "set client_max_window_bits instead"
+ )
+
+ self.server_no_context_takeover = server_no_context_takeover
+ self.client_no_context_takeover = client_no_context_takeover
+ self.server_max_window_bits = server_max_window_bits
+ self.client_max_window_bits = client_max_window_bits
+ self.compress_settings = compress_settings
+
+ def get_request_params(self) -> List[ExtensionParameter]:
+ """
+ Build request parameters.
+
+ """
+ return _build_parameters(
+ self.server_no_context_takeover,
+ self.client_no_context_takeover,
+ self.server_max_window_bits,
+ self.client_max_window_bits,
+ )
+
+ def process_response_params(
+ self,
+ params: Sequence[ExtensionParameter],
+ accepted_extensions: Sequence["Extension"],
+ ) -> PerMessageDeflate:
+ """
+ Process response parameters.
+
+ Return an extension instance.
+
+ """
+ if any(other.name == self.name for other in accepted_extensions):
+ raise NegotiationError(f"received duplicate {self.name}")
+
+ # Request parameters are available in instance variables.
+
+ # Load response parameters in local variables.
+ (
+ server_no_context_takeover,
+ client_no_context_takeover,
+ server_max_window_bits,
+ client_max_window_bits,
+ ) = _extract_parameters(params, is_server=False)
+
+ # After comparing the request and the response, the final
+ # configuration must be available in the local variables.
+
+ # server_no_context_takeover
+ #
+ # Req. Resp. Result
+ # ------ ------ --------------------------------------------------
+ # False False False
+ # False True True
+ # True False Error!
+ # True True True
+
+ if self.server_no_context_takeover:
+ if not server_no_context_takeover:
+ raise NegotiationError("expected server_no_context_takeover")
+
+ # client_no_context_takeover
+ #
+ # Req. Resp. Result
+ # ------ ------ --------------------------------------------------
+ # False False False
+ # False True True
+ # True False True - must change value
+ # True True True
+
+ if self.client_no_context_takeover:
+ if not client_no_context_takeover:
+ client_no_context_takeover = True
+
+ # server_max_window_bits
+
+ # Req. Resp. Result
+ # ------ ------ --------------------------------------------------
+ # None None None
+ # None 8≤M≤15 M
+ # 8≤N≤15 None Error!
+ # 8≤N≤15 8≤M≤N M
+ # 8≤N≤15 N<M≤15 Error!
+
+ if self.server_max_window_bits is None:
+ pass
+
+ else:
+ if server_max_window_bits is None:
+ raise NegotiationError("expected server_max_window_bits")
+ elif server_max_window_bits > self.server_max_window_bits:
+ raise NegotiationError("unsupported server_max_window_bits")
+
+ # client_max_window_bits
+
+ # Req. Resp. Result
+ # ------ ------ --------------------------------------------------
+ # None None None
+ # None 8≤M≤15 Error!
+ # True None None
+ # True 8≤M≤15 M
+ # 8≤N≤15 None N - must change value
+ # 8≤N≤15 8≤M≤N M
+ # 8≤N≤15 N<M≤15 Error!
+
+ if self.client_max_window_bits is None:
+ if client_max_window_bits is not None:
+ raise NegotiationError("unexpected client_max_window_bits")
+
+ elif self.client_max_window_bits is True:
+ pass
+
+ else:
+ if client_max_window_bits is None:
+ client_max_window_bits = self.client_max_window_bits
+ elif client_max_window_bits > self.client_max_window_bits:
+ raise NegotiationError("unsupported client_max_window_bits")
+
+ return PerMessageDeflate(
+ server_no_context_takeover, # remote_no_context_takeover
+ client_no_context_takeover, # local_no_context_takeover
+ server_max_window_bits or 15, # remote_max_window_bits
+ client_max_window_bits or 15, # local_max_window_bits
+ self.compress_settings,
+ )
+
+
+class ServerPerMessageDeflateFactory(ServerExtensionFactory):
+ """
+ Server-side extension factory for the Per-Message Deflate extension.
+
+ Parameters behave as described in `section 7.1 of RFC 7692`_. Set them to
+ ``True`` to include them in the negotiation offer without a value or to an
+ integer value to include them with this value.
+
+ .. _section 7.1 of RFC 7692: https://tools.ietf.org/html/rfc7692#section-7.1
+
+ :param server_no_context_takeover: defaults to ``False``
+ :param client_no_context_takeover: defaults to ``False``
+ :param server_max_window_bits: optional, defaults to ``None``
+ :param client_max_window_bits: optional, defaults to ``None``
+ :param compress_settings: optional, keyword arguments for
+ :func:`zlib.compressobj`, excluding ``wbits``
+
+ """
+
+ name = ExtensionName("permessage-deflate")
+
+ def __init__(
+ self,
+ server_no_context_takeover: bool = False,
+ client_no_context_takeover: bool = False,
+ server_max_window_bits: Optional[int] = None,
+ client_max_window_bits: Optional[int] = None,
+ compress_settings: Optional[Dict[str, Any]] = None,
+ ) -> None:
+ """
+ Configure the Per-Message Deflate extension factory.
+
+ """
+ if not (server_max_window_bits is None or 8 <= server_max_window_bits <= 15):
+ raise ValueError("server_max_window_bits must be between 8 and 15")
+ if not (client_max_window_bits is None or 8 <= client_max_window_bits <= 15):
+ raise ValueError("client_max_window_bits must be between 8 and 15")
+ if compress_settings is not None and "wbits" in compress_settings:
+ raise ValueError(
+ "compress_settings must not include wbits, "
+ "set server_max_window_bits instead"
+ )
+
+ self.server_no_context_takeover = server_no_context_takeover
+ self.client_no_context_takeover = client_no_context_takeover
+ self.server_max_window_bits = server_max_window_bits
+ self.client_max_window_bits = client_max_window_bits
+ self.compress_settings = compress_settings
+
+ def process_request_params(
+ self,
+ params: Sequence[ExtensionParameter],
+ accepted_extensions: Sequence["Extension"],
+ ) -> Tuple[List[ExtensionParameter], PerMessageDeflate]:
+ """
+ Process request parameters.
+
+ Return response params and an extension instance.
+
+ """
+ if any(other.name == self.name for other in accepted_extensions):
+ raise NegotiationError(f"skipped duplicate {self.name}")
+
+ # Load request parameters in local variables.
+ (
+ server_no_context_takeover,
+ client_no_context_takeover,
+ server_max_window_bits,
+ client_max_window_bits,
+ ) = _extract_parameters(params, is_server=True)
+
+ # Configuration parameters are available in instance variables.
+
+ # After comparing the request and the configuration, the response must
+ # be available in the local variables.
+
+ # server_no_context_takeover
+ #
+ # Config Req. Resp.
+ # ------ ------ --------------------------------------------------
+ # False False False
+ # False True True
+ # True False True - must change value to True
+ # True True True
+
+ if self.server_no_context_takeover:
+ if not server_no_context_takeover:
+ server_no_context_takeover = True
+
+ # client_no_context_takeover
+ #
+ # Config Req. Resp.
+ # ------ ------ --------------------------------------------------
+ # False False False
+ # False True True (or False)
+ # True False True - must change value to True
+ # True True True (or False)
+
+ if self.client_no_context_takeover:
+ if not client_no_context_takeover:
+ client_no_context_takeover = True
+
+ # server_max_window_bits
+
+ # Config Req. Resp.
+ # ------ ------ --------------------------------------------------
+ # None None None
+ # None 8≤M≤15 M
+ # 8≤N≤15 None N - must change value
+ # 8≤N≤15 8≤M≤N M
+ # 8≤N≤15 N<M≤15 N - must change value
+
+ if self.server_max_window_bits is None:
+ pass
+
+ else:
+ if server_max_window_bits is None:
+ server_max_window_bits = self.server_max_window_bits
+ elif server_max_window_bits > self.server_max_window_bits:
+ server_max_window_bits = self.server_max_window_bits
+
+ # client_max_window_bits
+
+ # Config Req. Resp.
+ # ------ ------ --------------------------------------------------
+ # None None None
+ # None True None - must change value
+ # None 8≤M≤15 M (or None)
+ # 8≤N≤15 None Error!
+ # 8≤N≤15 True N - must change value
+ # 8≤N≤15 8≤M≤N M (or None)
+ # 8≤N≤15 N<M≤15 N
+
+ if self.client_max_window_bits is None:
+ if client_max_window_bits is True:
+ client_max_window_bits = self.client_max_window_bits
+
+ else:
+ if client_max_window_bits is None:
+ raise NegotiationError("required client_max_window_bits")
+ elif client_max_window_bits is True:
+ client_max_window_bits = self.client_max_window_bits
+ elif self.client_max_window_bits < client_max_window_bits:
+ client_max_window_bits = self.client_max_window_bits
+
+ return (
+ _build_parameters(
+ server_no_context_takeover,
+ client_no_context_takeover,
+ server_max_window_bits,
+ client_max_window_bits,
+ ),
+ PerMessageDeflate(
+ client_no_context_takeover, # remote_no_context_takeover
+ server_no_context_takeover, # local_no_context_takeover
+ client_max_window_bits or 15, # remote_max_window_bits
+ server_max_window_bits or 15, # local_max_window_bits
+ self.compress_settings,
+ ),
+ )
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/framing.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/framing.py
new file mode 100644
index 0000000000..26e58cdbfb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/framing.py
@@ -0,0 +1,342 @@
+"""
+:mod:`websockets.framing` reads and writes WebSocket frames.
+
+It deals with a single frame at a time. Anything that depends on the sequence
+of frames is implemented in :mod:`websockets.protocol`.
+
+See `section 5 of RFC 6455`_.
+
+.. _section 5 of RFC 6455: http://tools.ietf.org/html/rfc6455#section-5
+
+"""
+
+import io
+import random
+import struct
+from typing import Any, Awaitable, Callable, NamedTuple, Optional, Sequence, Tuple
+
+from .exceptions import PayloadTooBig, ProtocolError
+from .typing import Data
+
+
+try:
+ from .speedups import apply_mask
+except ImportError: # pragma: no cover
+ from .utils import apply_mask
+
+
+__all__ = [
+ "DATA_OPCODES",
+ "CTRL_OPCODES",
+ "OP_CONT",
+ "OP_TEXT",
+ "OP_BINARY",
+ "OP_CLOSE",
+ "OP_PING",
+ "OP_PONG",
+ "Frame",
+ "prepare_data",
+ "encode_data",
+ "parse_close",
+ "serialize_close",
+]
+
+DATA_OPCODES = OP_CONT, OP_TEXT, OP_BINARY = 0x00, 0x01, 0x02
+CTRL_OPCODES = OP_CLOSE, OP_PING, OP_PONG = 0x08, 0x09, 0x0A
+
+# Close code that are allowed in a close frame.
+# Using a list optimizes `code in EXTERNAL_CLOSE_CODES`.
+EXTERNAL_CLOSE_CODES = [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011]
+
+
+# Consider converting to a dataclass when dropping support for Python < 3.7.
+
+
+class Frame(NamedTuple):
+ """
+ WebSocket frame.
+
+ :param bool fin: FIN bit
+ :param bool rsv1: RSV1 bit
+ :param bool rsv2: RSV2 bit
+ :param bool rsv3: RSV3 bit
+ :param int opcode: opcode
+ :param bytes data: payload data
+
+ Only these fields are needed. The MASK bit, payload length and masking-key
+ are handled on the fly by :meth:`read` and :meth:`write`.
+
+ """
+
+ fin: bool
+ opcode: int
+ data: bytes
+ rsv1: bool = False
+ rsv2: bool = False
+ rsv3: bool = False
+
+ @classmethod
+ async def read(
+ cls,
+ reader: Callable[[int], Awaitable[bytes]],
+ *,
+ mask: bool,
+ max_size: Optional[int] = None,
+ extensions: Optional[Sequence["websockets.extensions.base.Extension"]] = None,
+ ) -> "Frame":
+ """
+ Read a WebSocket frame.
+
+ :param reader: coroutine that reads exactly the requested number of
+ bytes, unless the end of file is reached
+ :param mask: whether the frame should be masked i.e. whether the read
+ happens on the server side
+ :param max_size: maximum payload size in bytes
+ :param extensions: list of classes with a ``decode()`` method that
+ transforms the frame and return a new frame; extensions are applied
+ in reverse order
+ :raises ~websockets.exceptions.PayloadTooBig: if the frame exceeds
+ ``max_size``
+ :raises ~websockets.exceptions.ProtocolError: if the frame
+ contains incorrect values
+
+ """
+ # Read the header.
+ data = await reader(2)
+ head1, head2 = struct.unpack("!BB", data)
+
+ # While not Pythonic, this is marginally faster than calling bool().
+ fin = True if head1 & 0b10000000 else False
+ rsv1 = True if head1 & 0b01000000 else False
+ rsv2 = True if head1 & 0b00100000 else False
+ rsv3 = True if head1 & 0b00010000 else False
+ opcode = head1 & 0b00001111
+
+ if (True if head2 & 0b10000000 else False) != mask:
+ raise ProtocolError("incorrect masking")
+
+ length = head2 & 0b01111111
+ if length == 126:
+ data = await reader(2)
+ (length,) = struct.unpack("!H", data)
+ elif length == 127:
+ data = await reader(8)
+ (length,) = struct.unpack("!Q", data)
+ if max_size is not None and length > max_size:
+ raise PayloadTooBig(
+ f"payload length exceeds size limit ({length} > {max_size} bytes)"
+ )
+ if mask:
+ mask_bits = await reader(4)
+
+ # Read the data.
+ data = await reader(length)
+ if mask:
+ data = apply_mask(data, mask_bits)
+
+ frame = cls(fin, opcode, data, rsv1, rsv2, rsv3)
+
+ if extensions is None:
+ extensions = []
+ for extension in reversed(extensions):
+ frame = extension.decode(frame, max_size=max_size)
+
+ frame.check()
+
+ return frame
+
+ def write(
+ frame,
+ write: Callable[[bytes], Any],
+ *,
+ mask: bool,
+ extensions: Optional[Sequence["websockets.extensions.base.Extension"]] = None,
+ ) -> None:
+ """
+ Write a WebSocket frame.
+
+ :param frame: frame to write
+ :param write: function that writes bytes
+ :param mask: whether the frame should be masked i.e. whether the write
+ happens on the client side
+ :param extensions: list of classes with an ``encode()`` method that
+ transform the frame and return a new frame; extensions are applied
+ in order
+ :raises ~websockets.exceptions.ProtocolError: if the frame
+ contains incorrect values
+
+ """
+ # The first parameter is called `frame` rather than `self`,
+ # but it's the instance of class to which this method is bound.
+
+ frame.check()
+
+ if extensions is None:
+ extensions = []
+ for extension in extensions:
+ frame = extension.encode(frame)
+
+ output = io.BytesIO()
+
+ # Prepare the header.
+ head1 = (
+ (0b10000000 if frame.fin else 0)
+ | (0b01000000 if frame.rsv1 else 0)
+ | (0b00100000 if frame.rsv2 else 0)
+ | (0b00010000 if frame.rsv3 else 0)
+ | frame.opcode
+ )
+
+ head2 = 0b10000000 if mask else 0
+
+ length = len(frame.data)
+ if length < 126:
+ output.write(struct.pack("!BB", head1, head2 | length))
+ elif length < 65536:
+ output.write(struct.pack("!BBH", head1, head2 | 126, length))
+ else:
+ output.write(struct.pack("!BBQ", head1, head2 | 127, length))
+
+ if mask:
+ mask_bits = struct.pack("!I", random.getrandbits(32))
+ output.write(mask_bits)
+
+ # Prepare the data.
+ if mask:
+ data = apply_mask(frame.data, mask_bits)
+ else:
+ data = frame.data
+ output.write(data)
+
+ # Send the frame.
+
+ # The frame is written in a single call to write in order to prevent
+ # TCP fragmentation. See #68 for details. This also makes it safe to
+ # send frames concurrently from multiple coroutines.
+ write(output.getvalue())
+
+ def check(frame) -> None:
+ """
+ Check that reserved bits and opcode have acceptable values.
+
+ :raises ~websockets.exceptions.ProtocolError: if a reserved
+ bit or the opcode is invalid
+
+ """
+ # The first parameter is called `frame` rather than `self`,
+ # but it's the instance of class to which this method is bound.
+
+ if frame.rsv1 or frame.rsv2 or frame.rsv3:
+ raise ProtocolError("reserved bits must be 0")
+
+ if frame.opcode in DATA_OPCODES:
+ return
+ elif frame.opcode in CTRL_OPCODES:
+ if len(frame.data) > 125:
+ raise ProtocolError("control frame too long")
+ if not frame.fin:
+ raise ProtocolError("fragmented control frame")
+ else:
+ raise ProtocolError(f"invalid opcode: {frame.opcode}")
+
+
+def prepare_data(data: Data) -> Tuple[int, bytes]:
+ """
+ Convert a string or byte-like object to an opcode and a bytes-like object.
+
+ This function is designed for data frames.
+
+ If ``data`` is a :class:`str`, return ``OP_TEXT`` and a :class:`bytes`
+ object encoding ``data`` in UTF-8.
+
+ If ``data`` is a bytes-like object, return ``OP_BINARY`` and a bytes-like
+ object.
+
+ :raises TypeError: if ``data`` doesn't have a supported type
+
+ """
+ if isinstance(data, str):
+ return OP_TEXT, data.encode("utf-8")
+ elif isinstance(data, (bytes, bytearray)):
+ return OP_BINARY, data
+ elif isinstance(data, memoryview):
+ if data.c_contiguous:
+ return OP_BINARY, data
+ else:
+ return OP_BINARY, data.tobytes()
+ else:
+ raise TypeError("data must be bytes-like or str")
+
+
+def encode_data(data: Data) -> bytes:
+ """
+ Convert a string or byte-like object to bytes.
+
+ This function is designed for ping and pong frames.
+
+ If ``data`` is a :class:`str`, return a :class:`bytes` object encoding
+ ``data`` in UTF-8.
+
+ If ``data`` is a bytes-like object, return a :class:`bytes` object.
+
+ :raises TypeError: if ``data`` doesn't have a supported type
+
+ """
+ if isinstance(data, str):
+ return data.encode("utf-8")
+ elif isinstance(data, (bytes, bytearray)):
+ return bytes(data)
+ elif isinstance(data, memoryview):
+ return data.tobytes()
+ else:
+ raise TypeError("data must be bytes-like or str")
+
+
+def parse_close(data: bytes) -> Tuple[int, str]:
+ """
+ Parse the payload from a close frame.
+
+ Return ``(code, reason)``.
+
+ :raises ~websockets.exceptions.ProtocolError: if data is ill-formed
+ :raises UnicodeDecodeError: if the reason isn't valid UTF-8
+
+ """
+ length = len(data)
+ if length >= 2:
+ (code,) = struct.unpack("!H", data[:2])
+ check_close(code)
+ reason = data[2:].decode("utf-8")
+ return code, reason
+ elif length == 0:
+ return 1005, ""
+ else:
+ assert length == 1
+ raise ProtocolError("close frame too short")
+
+
+def serialize_close(code: int, reason: str) -> bytes:
+ """
+ Serialize the payload for a close frame.
+
+ This is the reverse of :func:`parse_close`.
+
+ """
+ check_close(code)
+ return struct.pack("!H", code) + reason.encode("utf-8")
+
+
+def check_close(code: int) -> None:
+ """
+ Check that the close code has an acceptable value for a close frame.
+
+ :raises ~websockets.exceptions.ProtocolError: if the close code
+ is invalid
+
+ """
+ if not (code in EXTERNAL_CLOSE_CODES or 3000 <= code < 5000):
+ raise ProtocolError("invalid status code")
+
+
+# at the bottom to allow circular import, because Extension depends on Frame
+import websockets.extensions.base # isort:skip # noqa
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/handshake.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/handshake.py
new file mode 100644
index 0000000000..9bfe27754f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/handshake.py
@@ -0,0 +1,185 @@
+"""
+:mod:`websockets.handshake` provides helpers for the WebSocket handshake.
+
+See `section 4 of RFC 6455`_.
+
+.. _section 4 of RFC 6455: http://tools.ietf.org/html/rfc6455#section-4
+
+Some checks cannot be performed because they depend too much on the
+context; instead, they're documented below.
+
+To accept a connection, a server must:
+
+- Read the request, check that the method is GET, and check the headers with
+ :func:`check_request`,
+- Send a 101 response to the client with the headers created by
+ :func:`build_response` if the request is valid; otherwise, send an
+ appropriate HTTP error code.
+
+To open a connection, a client must:
+
+- Send a GET request to the server with the headers created by
+ :func:`build_request`,
+- Read the response, check that the status code is 101, and check the headers
+ with :func:`check_response`.
+
+"""
+
+import base64
+import binascii
+import hashlib
+import random
+from typing import List
+
+from .exceptions import InvalidHeader, InvalidHeaderValue, InvalidUpgrade
+from .headers import ConnectionOption, UpgradeProtocol, parse_connection, parse_upgrade
+from .http import Headers, MultipleValuesError
+
+
+__all__ = ["build_request", "check_request", "build_response", "check_response"]
+
+GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+
+
+def build_request(headers: Headers) -> str:
+ """
+ Build a handshake request to send to the server.
+
+ Update request headers passed in argument.
+
+ :param headers: request headers
+ :returns: ``key`` which must be passed to :func:`check_response`
+
+ """
+ raw_key = bytes(random.getrandbits(8) for _ in range(16))
+ key = base64.b64encode(raw_key).decode()
+ headers["Upgrade"] = "websocket"
+ headers["Connection"] = "Upgrade"
+ headers["Sec-WebSocket-Key"] = key
+ headers["Sec-WebSocket-Version"] = "13"
+ return key
+
+
+def check_request(headers: Headers) -> str:
+ """
+ Check a handshake request received from the client.
+
+ This function doesn't verify that the request is an HTTP/1.1 or higher GET
+ request and doesn't perform ``Host`` and ``Origin`` checks. These controls
+ are usually performed earlier in the HTTP request handling code. They're
+ the responsibility of the caller.
+
+ :param headers: request headers
+ :returns: ``key`` which must be passed to :func:`build_response`
+ :raises ~websockets.exceptions.InvalidHandshake: if the handshake request
+ is invalid; then the server must return 400 Bad Request error
+
+ """
+ connection: List[ConnectionOption] = sum(
+ [parse_connection(value) for value in headers.get_all("Connection")], []
+ )
+
+ if not any(value.lower() == "upgrade" for value in connection):
+ raise InvalidUpgrade("Connection", ", ".join(connection))
+
+ upgrade: List[UpgradeProtocol] = sum(
+ [parse_upgrade(value) for value in headers.get_all("Upgrade")], []
+ )
+
+ # For compatibility with non-strict implementations, ignore case when
+ # checking the Upgrade header. It's supposed to be 'WebSocket'.
+ if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"):
+ raise InvalidUpgrade("Upgrade", ", ".join(upgrade))
+
+ try:
+ s_w_key = headers["Sec-WebSocket-Key"]
+ except KeyError:
+ raise InvalidHeader("Sec-WebSocket-Key")
+ except MultipleValuesError:
+ raise InvalidHeader(
+ "Sec-WebSocket-Key", "more than one Sec-WebSocket-Key header found"
+ )
+
+ try:
+ raw_key = base64.b64decode(s_w_key.encode(), validate=True)
+ except binascii.Error:
+ raise InvalidHeaderValue("Sec-WebSocket-Key", s_w_key)
+ if len(raw_key) != 16:
+ raise InvalidHeaderValue("Sec-WebSocket-Key", s_w_key)
+
+ try:
+ s_w_version = headers["Sec-WebSocket-Version"]
+ except KeyError:
+ raise InvalidHeader("Sec-WebSocket-Version")
+ except MultipleValuesError:
+ raise InvalidHeader(
+ "Sec-WebSocket-Version", "more than one Sec-WebSocket-Version header found"
+ )
+
+ if s_w_version != "13":
+ raise InvalidHeaderValue("Sec-WebSocket-Version", s_w_version)
+
+ return s_w_key
+
+
+def build_response(headers: Headers, key: str) -> None:
+ """
+ Build a handshake response to send to the client.
+
+ Update response headers passed in argument.
+
+ :param headers: response headers
+ :param key: comes from :func:`check_request`
+
+ """
+ headers["Upgrade"] = "websocket"
+ headers["Connection"] = "Upgrade"
+ headers["Sec-WebSocket-Accept"] = accept(key)
+
+
+def check_response(headers: Headers, key: str) -> None:
+ """
+ Check a handshake response received from the server.
+
+ This function doesn't verify that the response is an HTTP/1.1 or higher
+ response with a 101 status code. These controls are the responsibility of
+ the caller.
+
+ :param headers: response headers
+ :param key: comes from :func:`build_request`
+ :raises ~websockets.exceptions.InvalidHandshake: if the handshake response
+ is invalid
+
+ """
+ connection: List[ConnectionOption] = sum(
+ [parse_connection(value) for value in headers.get_all("Connection")], []
+ )
+
+ if not any(value.lower() == "upgrade" for value in connection):
+ raise InvalidUpgrade("Connection", " ".join(connection))
+
+ upgrade: List[UpgradeProtocol] = sum(
+ [parse_upgrade(value) for value in headers.get_all("Upgrade")], []
+ )
+
+ # For compatibility with non-strict implementations, ignore case when
+ # checking the Upgrade header. It's supposed to be 'WebSocket'.
+ if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"):
+ raise InvalidUpgrade("Upgrade", ", ".join(upgrade))
+
+ try:
+ s_w_accept = headers["Sec-WebSocket-Accept"]
+ except KeyError:
+ raise InvalidHeader("Sec-WebSocket-Accept")
+ except MultipleValuesError:
+ raise InvalidHeader(
+ "Sec-WebSocket-Accept", "more than one Sec-WebSocket-Accept header found"
+ )
+
+ if s_w_accept != accept(key):
+ raise InvalidHeaderValue("Sec-WebSocket-Accept", s_w_accept)
+
+
+def accept(key: str) -> str:
+ sha1 = hashlib.sha1((key + GUID).encode()).digest()
+ return base64.b64encode(sha1).decode()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/headers.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/headers.py
new file mode 100644
index 0000000000..f33c94c046
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/headers.py
@@ -0,0 +1,515 @@
+"""
+:mod:`websockets.headers` provides parsers and serializers for HTTP headers
+used in WebSocket handshake messages.
+
+These APIs cannot be imported from :mod:`websockets`. They must be imported
+from :mod:`websockets.headers`.
+
+"""
+
+import base64
+import binascii
+import re
+from typing import Callable, List, NewType, Optional, Sequence, Tuple, TypeVar, cast
+
+from .exceptions import InvalidHeaderFormat, InvalidHeaderValue
+from .typing import ExtensionHeader, ExtensionName, ExtensionParameter, Subprotocol
+
+
+__all__ = [
+ "parse_connection",
+ "parse_upgrade",
+ "parse_extension",
+ "build_extension",
+ "parse_subprotocol",
+ "build_subprotocol",
+ "build_www_authenticate_basic",
+ "parse_authorization_basic",
+ "build_authorization_basic",
+]
+
+
+T = TypeVar("T")
+
+ConnectionOption = NewType("ConnectionOption", str)
+UpgradeProtocol = NewType("UpgradeProtocol", str)
+
+
+# To avoid a dependency on a parsing library, we implement manually the ABNF
+# described in https://tools.ietf.org/html/rfc6455#section-9.1 with the
+# definitions from https://tools.ietf.org/html/rfc7230#appendix-B.
+
+
+def peek_ahead(header: str, pos: int) -> Optional[str]:
+ """
+ Return the next character from ``header`` at the given position.
+
+ Return ``None`` at the end of ``header``.
+
+ We never need to peek more than one character ahead.
+
+ """
+ return None if pos == len(header) else header[pos]
+
+
+_OWS_re = re.compile(r"[\t ]*")
+
+
+def parse_OWS(header: str, pos: int) -> int:
+ """
+ Parse optional whitespace from ``header`` at the given position.
+
+ Return the new position.
+
+ The whitespace itself isn't returned because it isn't significant.
+
+ """
+ # There's always a match, possibly empty, whose content doesn't matter.
+ match = _OWS_re.match(header, pos)
+ assert match is not None
+ return match.end()
+
+
+_token_re = re.compile(r"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+")
+
+
+def parse_token(header: str, pos: int, header_name: str) -> Tuple[str, int]:
+ """
+ Parse a token from ``header`` at the given position.
+
+ Return the token value and the new position.
+
+ :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
+
+ """
+ match = _token_re.match(header, pos)
+ if match is None:
+ raise InvalidHeaderFormat(header_name, "expected token", header, pos)
+ return match.group(), match.end()
+
+
+_quoted_string_re = re.compile(
+ r'"(?:[\x09\x20-\x21\x23-\x5b\x5d-\x7e]|\\[\x09\x20-\x7e\x80-\xff])*"'
+)
+
+
+_unquote_re = re.compile(r"\\([\x09\x20-\x7e\x80-\xff])")
+
+
+def parse_quoted_string(header: str, pos: int, header_name: str) -> Tuple[str, int]:
+ """
+ Parse a quoted string from ``header`` at the given position.
+
+ Return the unquoted value and the new position.
+
+ :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
+
+ """
+ match = _quoted_string_re.match(header, pos)
+ if match is None:
+ raise InvalidHeaderFormat(header_name, "expected quoted string", header, pos)
+ return _unquote_re.sub(r"\1", match.group()[1:-1]), match.end()
+
+
+_quotable_re = re.compile(r"[\x09\x20-\x7e\x80-\xff]*")
+
+
+_quote_re = re.compile(r"([\x22\x5c])")
+
+
+def build_quoted_string(value: str) -> str:
+ """
+ Format ``value`` as a quoted string.
+
+ This is the reverse of :func:`parse_quoted_string`.
+
+ """
+ match = _quotable_re.fullmatch(value)
+ if match is None:
+ raise ValueError("invalid characters for quoted-string encoding")
+ return '"' + _quote_re.sub(r"\\\1", value) + '"'
+
+
+def parse_list(
+ parse_item: Callable[[str, int, str], Tuple[T, int]],
+ header: str,
+ pos: int,
+ header_name: str,
+) -> List[T]:
+ """
+ Parse a comma-separated list from ``header`` at the given position.
+
+ This is appropriate for parsing values with the following grammar:
+
+ 1#item
+
+ ``parse_item`` parses one item.
+
+ ``header`` is assumed not to start or end with whitespace.
+
+ (This function is designed for parsing an entire header value and
+ :func:`~websockets.http.read_headers` strips whitespace from values.)
+
+ Return a list of items.
+
+ :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
+
+ """
+ # Per https://tools.ietf.org/html/rfc7230#section-7, "a recipient MUST
+ # parse and ignore a reasonable number of empty list elements"; hence
+ # while loops that remove extra delimiters.
+
+ # Remove extra delimiters before the first item.
+ while peek_ahead(header, pos) == ",":
+ pos = parse_OWS(header, pos + 1)
+
+ items = []
+ while True:
+ # Loop invariant: a item starts at pos in header.
+ item, pos = parse_item(header, pos, header_name)
+ items.append(item)
+ pos = parse_OWS(header, pos)
+
+ # We may have reached the end of the header.
+ if pos == len(header):
+ break
+
+ # There must be a delimiter after each element except the last one.
+ if peek_ahead(header, pos) == ",":
+ pos = parse_OWS(header, pos + 1)
+ else:
+ raise InvalidHeaderFormat(header_name, "expected comma", header, pos)
+
+ # Remove extra delimiters before the next item.
+ while peek_ahead(header, pos) == ",":
+ pos = parse_OWS(header, pos + 1)
+
+ # We may have reached the end of the header.
+ if pos == len(header):
+ break
+
+ # Since we only advance in the header by one character with peek_ahead()
+ # or with the end position of a regex match, we can't overshoot the end.
+ assert pos == len(header)
+
+ return items
+
+
+def parse_connection_option(
+ header: str, pos: int, header_name: str
+) -> Tuple[ConnectionOption, int]:
+ """
+ Parse a Connection option from ``header`` at the given position.
+
+ Return the protocol value and the new position.
+
+ :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
+
+ """
+ item, pos = parse_token(header, pos, header_name)
+ return cast(ConnectionOption, item), pos
+
+
+def parse_connection(header: str) -> List[ConnectionOption]:
+ """
+ Parse a ``Connection`` header.
+
+ Return a list of HTTP connection options.
+
+ :param header: value of the ``Connection`` header
+ :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
+
+ """
+ return parse_list(parse_connection_option, header, 0, "Connection")
+
+
+_protocol_re = re.compile(
+ r"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+(?:/[-!#$%&\'*+.^_`|~0-9a-zA-Z]+)?"
+)
+
+
+def parse_upgrade_protocol(
+ header: str, pos: int, header_name: str
+) -> Tuple[UpgradeProtocol, int]:
+ """
+ Parse an Upgrade protocol from ``header`` at the given position.
+
+ Return the protocol value and the new position.
+
+ :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
+
+ """
+ match = _protocol_re.match(header, pos)
+ if match is None:
+ raise InvalidHeaderFormat(header_name, "expected protocol", header, pos)
+ return cast(UpgradeProtocol, match.group()), match.end()
+
+
+def parse_upgrade(header: str) -> List[UpgradeProtocol]:
+ """
+ Parse an ``Upgrade`` header.
+
+ Return a list of HTTP protocols.
+
+ :param header: value of the ``Upgrade`` header
+ :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
+
+ """
+ return parse_list(parse_upgrade_protocol, header, 0, "Upgrade")
+
+
+def parse_extension_item_param(
+ header: str, pos: int, header_name: str
+) -> Tuple[ExtensionParameter, int]:
+ """
+ Parse a single extension parameter from ``header`` at the given position.
+
+ Return a ``(name, value)`` pair and the new position.
+
+ :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
+
+ """
+ # Extract parameter name.
+ name, pos = parse_token(header, pos, header_name)
+ pos = parse_OWS(header, pos)
+ # Extract parameter value, if there is one.
+ value: Optional[str] = None
+ if peek_ahead(header, pos) == "=":
+ pos = parse_OWS(header, pos + 1)
+ if peek_ahead(header, pos) == '"':
+ pos_before = pos # for proper error reporting below
+ value, pos = parse_quoted_string(header, pos, header_name)
+ # https://tools.ietf.org/html/rfc6455#section-9.1 says: the value
+ # after quoted-string unescaping MUST conform to the 'token' ABNF.
+ if _token_re.fullmatch(value) is None:
+ raise InvalidHeaderFormat(
+ header_name, "invalid quoted header content", header, pos_before
+ )
+ else:
+ value, pos = parse_token(header, pos, header_name)
+ pos = parse_OWS(header, pos)
+
+ return (name, value), pos
+
+
+def parse_extension_item(
+ header: str, pos: int, header_name: str
+) -> Tuple[ExtensionHeader, int]:
+ """
+ Parse an extension definition from ``header`` at the given position.
+
+ Return an ``(extension name, parameters)`` pair, where ``parameters`` is a
+ list of ``(name, value)`` pairs, and the new position.
+
+ :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
+
+ """
+ # Extract extension name.
+ name, pos = parse_token(header, pos, header_name)
+ pos = parse_OWS(header, pos)
+ # Extract all parameters.
+ parameters = []
+ while peek_ahead(header, pos) == ";":
+ pos = parse_OWS(header, pos + 1)
+ parameter, pos = parse_extension_item_param(header, pos, header_name)
+ parameters.append(parameter)
+ return (cast(ExtensionName, name), parameters), pos
+
+
+def parse_extension(header: str) -> List[ExtensionHeader]:
+ """
+ Parse a ``Sec-WebSocket-Extensions`` header.
+
+ Return a list of WebSocket extensions and their parameters in this format::
+
+ [
+ (
+ 'extension name',
+ [
+ ('parameter name', 'parameter value'),
+ ....
+ ]
+ ),
+ ...
+ ]
+
+ Parameter values are ``None`` when no value is provided.
+
+ :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
+
+ """
+ return parse_list(parse_extension_item, header, 0, "Sec-WebSocket-Extensions")
+
+
+parse_extension_list = parse_extension # alias for backwards compatibility
+
+
+def build_extension_item(
+ name: ExtensionName, parameters: List[ExtensionParameter]
+) -> str:
+ """
+ Build an extension definition.
+
+ This is the reverse of :func:`parse_extension_item`.
+
+ """
+ return "; ".join(
+ [cast(str, name)]
+ + [
+ # Quoted strings aren't necessary because values are always tokens.
+ name if value is None else f"{name}={value}"
+ for name, value in parameters
+ ]
+ )
+
+
+def build_extension(extensions: Sequence[ExtensionHeader]) -> str:
+ """
+ Build a ``Sec-WebSocket-Extensions`` header.
+
+ This is the reverse of :func:`parse_extension`.
+
+ """
+ return ", ".join(
+ build_extension_item(name, parameters) for name, parameters in extensions
+ )
+
+
+build_extension_list = build_extension # alias for backwards compatibility
+
+
+def parse_subprotocol_item(
+ header: str, pos: int, header_name: str
+) -> Tuple[Subprotocol, int]:
+ """
+ Parse a subprotocol from ``header`` at the given position.
+
+ Return the subprotocol value and the new position.
+
+ :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
+
+ """
+ item, pos = parse_token(header, pos, header_name)
+ return cast(Subprotocol, item), pos
+
+
+def parse_subprotocol(header: str) -> List[Subprotocol]:
+ """
+ Parse a ``Sec-WebSocket-Protocol`` header.
+
+ Return a list of WebSocket subprotocols.
+
+ :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
+
+ """
+ return parse_list(parse_subprotocol_item, header, 0, "Sec-WebSocket-Protocol")
+
+
+parse_subprotocol_list = parse_subprotocol # alias for backwards compatibility
+
+
+def build_subprotocol(protocols: Sequence[Subprotocol]) -> str:
+ """
+ Build a ``Sec-WebSocket-Protocol`` header.
+
+ This is the reverse of :func:`parse_subprotocol`.
+
+ """
+ return ", ".join(protocols)
+
+
+build_subprotocol_list = build_subprotocol # alias for backwards compatibility
+
+
+def build_www_authenticate_basic(realm: str) -> str:
+ """
+ Build a ``WWW-Authenticate`` header for HTTP Basic Auth.
+
+ :param realm: authentication realm
+
+ """
+ # https://tools.ietf.org/html/rfc7617#section-2
+ realm = build_quoted_string(realm)
+ charset = build_quoted_string("UTF-8")
+ return f"Basic realm={realm}, charset={charset}"
+
+
+_token68_re = re.compile(r"[A-Za-z0-9-._~+/]+=*")
+
+
+def parse_token68(header: str, pos: int, header_name: str) -> Tuple[str, int]:
+ """
+ Parse a token68 from ``header`` at the given position.
+
+ Return the token value and the new position.
+
+ :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
+
+ """
+ match = _token68_re.match(header, pos)
+ if match is None:
+ raise InvalidHeaderFormat(header_name, "expected token68", header, pos)
+ return match.group(), match.end()
+
+
+def parse_end(header: str, pos: int, header_name: str) -> None:
+ """
+ Check that parsing reached the end of header.
+
+ """
+ if pos < len(header):
+ raise InvalidHeaderFormat(header_name, "trailing data", header, pos)
+
+
+def parse_authorization_basic(header: str) -> Tuple[str, str]:
+ """
+ Parse an ``Authorization`` header for HTTP Basic Auth.
+
+ Return a ``(username, password)`` tuple.
+
+ :param header: value of the ``Authorization`` header
+ :raises InvalidHeaderFormat: on invalid inputs
+ :raises InvalidHeaderValue: on unsupported inputs
+
+ """
+ # https://tools.ietf.org/html/rfc7235#section-2.1
+ # https://tools.ietf.org/html/rfc7617#section-2
+ scheme, pos = parse_token(header, 0, "Authorization")
+ if scheme.lower() != "basic":
+ raise InvalidHeaderValue("Authorization", f"unsupported scheme: {scheme}")
+ if peek_ahead(header, pos) != " ":
+ raise InvalidHeaderFormat(
+ "Authorization", "expected space after scheme", header, pos
+ )
+ pos += 1
+ basic_credentials, pos = parse_token68(header, pos, "Authorization")
+ parse_end(header, pos, "Authorization")
+
+ try:
+ user_pass = base64.b64decode(basic_credentials.encode()).decode()
+ except binascii.Error:
+ raise InvalidHeaderValue(
+ "Authorization", "expected base64-encoded credentials"
+ ) from None
+ try:
+ username, password = user_pass.split(":", 1)
+ except ValueError:
+ raise InvalidHeaderValue(
+ "Authorization", "expected username:password credentials"
+ ) from None
+
+ return username, password
+
+
+def build_authorization_basic(username: str, password: str) -> str:
+ """
+ Build an ``Authorization`` header for HTTP Basic Auth.
+
+ This is the reverse of :func:`parse_authorization_basic`.
+
+ """
+ # https://tools.ietf.org/html/rfc7617#section-2
+ assert ":" not in username
+ user_pass = f"{username}:{password}"
+ basic_credentials = base64.b64encode(user_pass.encode()).decode()
+ return "Basic " + basic_credentials
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py
new file mode 100644
index 0000000000..ba6d274bf1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py
@@ -0,0 +1,360 @@
+"""
+:mod:`websockets.http` module provides basic HTTP/1.1 support. It is merely
+:adequate for WebSocket handshake messages.
+
+These APIs cannot be imported from :mod:`websockets`. They must be imported
+from :mod:`websockets.http`.
+
+"""
+
+import asyncio
+import re
+import sys
+from typing import (
+ Any,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Mapping,
+ MutableMapping,
+ Tuple,
+ Union,
+)
+
+from .version import version as websockets_version
+
+
+__all__ = [
+ "read_request",
+ "read_response",
+ "Headers",
+ "MultipleValuesError",
+ "USER_AGENT",
+]
+
+MAX_HEADERS = 256
+MAX_LINE = 4096
+
+USER_AGENT = f"Python/{sys.version[:3]} websockets/{websockets_version}"
+
+
+def d(value: bytes) -> str:
+ """
+ Decode a bytestring for interpolating into an error message.
+
+ """
+ return value.decode(errors="backslashreplace")
+
+
+# See https://tools.ietf.org/html/rfc7230#appendix-B.
+
+# Regex for validating header names.
+
+_token_re = re.compile(rb"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+")
+
+# Regex for validating header values.
+
+# We don't attempt to support obsolete line folding.
+
+# Include HTAB (\x09), SP (\x20), VCHAR (\x21-\x7e), obs-text (\x80-\xff).
+
+# The ABNF is complicated because it attempts to express that optional
+# whitespace is ignored. We strip whitespace and don't revalidate that.
+
+# See also https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189
+
+_value_re = re.compile(rb"[\x09\x20-\x7e\x80-\xff]*")
+
+
+async def read_request(stream: asyncio.StreamReader) -> Tuple[str, "Headers"]:
+ """
+ Read an HTTP/1.1 GET request and return ``(path, headers)``.
+
+ ``path`` isn't URL-decoded or validated in any way.
+
+ ``path`` and ``headers`` are expected to contain only ASCII characters.
+ Other characters are represented with surrogate escapes.
+
+ :func:`read_request` doesn't attempt to read the request body because
+ WebSocket handshake requests don't have one. If the request contains a
+ body, it may be read from ``stream`` after this coroutine returns.
+
+ :param stream: input to read the request from
+ :raises EOFError: if the connection is closed without a full HTTP request
+ :raises SecurityError: if the request exceeds a security limit
+ :raises ValueError: if the request isn't well formatted
+
+ """
+ # https://tools.ietf.org/html/rfc7230#section-3.1.1
+
+ # Parsing is simple because fixed values are expected for method and
+ # version and because path isn't checked. Since WebSocket software tends
+ # to implement HTTP/1.1 strictly, there's little need for lenient parsing.
+
+ try:
+ request_line = await read_line(stream)
+ except EOFError as exc:
+ raise EOFError("connection closed while reading HTTP request line") from exc
+
+ try:
+ method, raw_path, version = request_line.split(b" ", 2)
+ except ValueError: # not enough values to unpack (expected 3, got 1-2)
+ raise ValueError(f"invalid HTTP request line: {d(request_line)}") from None
+
+ if method != b"GET":
+ raise ValueError(f"unsupported HTTP method: {d(method)}")
+ if version != b"HTTP/1.1":
+ raise ValueError(f"unsupported HTTP version: {d(version)}")
+ path = raw_path.decode("ascii", "surrogateescape")
+
+ headers = await read_headers(stream)
+
+ return path, headers
+
+
+async def read_response(stream: asyncio.StreamReader) -> Tuple[int, str, "Headers"]:
+ """
+ Read an HTTP/1.1 response and return ``(status_code, reason, headers)``.
+
+ ``reason`` and ``headers`` are expected to contain only ASCII characters.
+ Other characters are represented with surrogate escapes.
+
+ :func:`read_request` doesn't attempt to read the response body because
+ WebSocket handshake responses don't have one. If the response contains a
+ body, it may be read from ``stream`` after this coroutine returns.
+
+ :param stream: input to read the response from
+ :raises EOFError: if the connection is closed without a full HTTP response
+ :raises SecurityError: if the response exceeds a security limit
+ :raises ValueError: if the response isn't well formatted
+
+ """
+ # https://tools.ietf.org/html/rfc7230#section-3.1.2
+
+ # As in read_request, parsing is simple because a fixed value is expected
+ # for version, status_code is a 3-digit number, and reason can be ignored.
+
+ try:
+ status_line = await read_line(stream)
+ except EOFError as exc:
+ raise EOFError("connection closed while reading HTTP status line") from exc
+
+ try:
+ version, raw_status_code, raw_reason = status_line.split(b" ", 2)
+ except ValueError: # not enough values to unpack (expected 3, got 1-2)
+ raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None
+
+ if version != b"HTTP/1.1":
+ raise ValueError(f"unsupported HTTP version: {d(version)}")
+ try:
+ status_code = int(raw_status_code)
+ except ValueError: # invalid literal for int() with base 10
+ raise ValueError(f"invalid HTTP status code: {d(raw_status_code)}") from None
+ if not 100 <= status_code < 1000:
+ raise ValueError(f"unsupported HTTP status code: {d(raw_status_code)}")
+ if not _value_re.fullmatch(raw_reason):
+ raise ValueError(f"invalid HTTP reason phrase: {d(raw_reason)}")
+ reason = raw_reason.decode()
+
+ headers = await read_headers(stream)
+
+ return status_code, reason, headers
+
+
+async def read_headers(stream: asyncio.StreamReader) -> "Headers":
+ """
+ Read HTTP headers from ``stream``.
+
+ Non-ASCII characters are represented with surrogate escapes.
+
+ """
+ # https://tools.ietf.org/html/rfc7230#section-3.2
+
+ # We don't attempt to support obsolete line folding.
+
+ headers = Headers()
+ for _ in range(MAX_HEADERS + 1):
+ try:
+ line = await read_line(stream)
+ except EOFError as exc:
+ raise EOFError("connection closed while reading HTTP headers") from exc
+ if line == b"":
+ break
+
+ try:
+ raw_name, raw_value = line.split(b":", 1)
+ except ValueError: # not enough values to unpack (expected 2, got 1)
+ raise ValueError(f"invalid HTTP header line: {d(line)}") from None
+ if not _token_re.fullmatch(raw_name):
+ raise ValueError(f"invalid HTTP header name: {d(raw_name)}")
+ raw_value = raw_value.strip(b" \t")
+ if not _value_re.fullmatch(raw_value):
+ raise ValueError(f"invalid HTTP header value: {d(raw_value)}")
+
+ name = raw_name.decode("ascii") # guaranteed to be ASCII at this point
+ value = raw_value.decode("ascii", "surrogateescape")
+ headers[name] = value
+
+ else:
+ raise websockets.exceptions.SecurityError("too many HTTP headers")
+
+ return headers
+
+
+async def read_line(stream: asyncio.StreamReader) -> bytes:
+ """
+ Read a single line from ``stream``.
+
+ CRLF is stripped from the return value.
+
+ """
+ # Security: this is bounded by the StreamReader's limit (default = 32 KiB).
+ line = await stream.readline()
+ # Security: this guarantees header values are small (hard-coded = 4 KiB)
+ if len(line) > MAX_LINE:
+ raise websockets.exceptions.SecurityError("line too long")
+ # Not mandatory but safe - https://tools.ietf.org/html/rfc7230#section-3.5
+ if not line.endswith(b"\r\n"):
+ raise EOFError("line without CRLF")
+ return line[:-2]
+
+
+class MultipleValuesError(LookupError):
+ """
+ Exception raised when :class:`Headers` has more than one value for a key.
+
+ """
+
+ def __str__(self) -> str:
+ # Implement the same logic as KeyError_str in Objects/exceptions.c.
+ if len(self.args) == 1:
+ return repr(self.args[0])
+ return super().__str__()
+
+
+class Headers(MutableMapping[str, str]):
+ """
+ Efficient data structure for manipulating HTTP headers.
+
+ A :class:`list` of ``(name, values)`` is inefficient for lookups.
+
+ A :class:`dict` doesn't suffice because header names are case-insensitive
+ and multiple occurrences of headers with the same name are possible.
+
+ :class:`Headers` stores HTTP headers in a hybrid data structure to provide
+ efficient insertions and lookups while preserving the original data.
+
+ In order to account for multiple values with minimal hassle,
+ :class:`Headers` follows this logic:
+
+ - When getting a header with ``headers[name]``:
+ - if there's no value, :exc:`KeyError` is raised;
+ - if there's exactly one value, it's returned;
+ - if there's more than one value, :exc:`MultipleValuesError` is raised.
+
+ - When setting a header with ``headers[name] = value``, the value is
+ appended to the list of values for that header.
+
+ - When deleting a header with ``del headers[name]``, all values for that
+ header are removed (this is slow).
+
+ Other methods for manipulating headers are consistent with this logic.
+
+ As long as no header occurs multiple times, :class:`Headers` behaves like
+ :class:`dict`, except keys are lower-cased to provide case-insensitivity.
+
+ Two methods support support manipulating multiple values explicitly:
+
+ - :meth:`get_all` returns a list of all values for a header;
+ - :meth:`raw_items` returns an iterator of ``(name, values)`` pairs.
+
+ """
+
+ __slots__ = ["_dict", "_list"]
+
+ def __init__(self, *args: Any, **kwargs: str) -> None:
+ self._dict: Dict[str, List[str]] = {}
+ self._list: List[Tuple[str, str]] = []
+ # MutableMapping.update calls __setitem__ for each (name, value) pair.
+ self.update(*args, **kwargs)
+
+ def __str__(self) -> str:
+ return "".join(f"{key}: {value}\r\n" for key, value in self._list) + "\r\n"
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}({self._list!r})"
+
+ def copy(self) -> "Headers":
+ copy = self.__class__()
+ copy._dict = self._dict.copy()
+ copy._list = self._list.copy()
+ return copy
+
+ # Collection methods
+
+ def __contains__(self, key: object) -> bool:
+ return isinstance(key, str) and key.lower() in self._dict
+
+ def __iter__(self) -> Iterator[str]:
+ return iter(self._dict)
+
+ def __len__(self) -> int:
+ return len(self._dict)
+
+ # MutableMapping methods
+
+ def __getitem__(self, key: str) -> str:
+ value = self._dict[key.lower()]
+ if len(value) == 1:
+ return value[0]
+ else:
+ raise MultipleValuesError(key)
+
+ def __setitem__(self, key: str, value: str) -> None:
+ self._dict.setdefault(key.lower(), []).append(value)
+ self._list.append((key, value))
+
+ def __delitem__(self, key: str) -> None:
+ key_lower = key.lower()
+ self._dict.__delitem__(key_lower)
+ # This is inefficent. Fortunately deleting HTTP headers is uncommon.
+ self._list = [(k, v) for k, v in self._list if k.lower() != key_lower]
+
+ def __eq__(self, other: Any) -> bool:
+ if not isinstance(other, Headers):
+ return NotImplemented
+ return self._list == other._list
+
+ def clear(self) -> None:
+ """
+ Remove all headers.
+
+ """
+ self._dict = {}
+ self._list = []
+
+ # Methods for handling multiple values
+
+ def get_all(self, key: str) -> List[str]:
+ """
+ Return the (possibly empty) list of all values for a header.
+
+ :param key: header name
+
+ """
+ return self._dict.get(key.lower(), [])
+
+ def raw_items(self) -> Iterator[Tuple[str, str]]:
+ """
+ Return an iterator of all values as ``(name, value)`` pairs.
+
+ """
+ return iter(self._list)
+
+
+HeadersLike = Union[Headers, Mapping[str, str], Iterable[Tuple[str, str]]]
+
+
+# at the bottom to allow circular import, because AbortHandshake depends on HeadersLike
+import websockets.exceptions # isort:skip # noqa
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py
new file mode 100644
index 0000000000..ede636d0db
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py
@@ -0,0 +1,1429 @@
+"""
+:mod:`websockets.protocol` handles WebSocket control and data frames.
+
+See `sections 4 to 8 of RFC 6455`_.
+
+.. _sections 4 to 8 of RFC 6455: http://tools.ietf.org/html/rfc6455#section-4
+
+"""
+
+import asyncio
+import codecs
+import collections
+import enum
+import logging
+import random
+import struct
+import sys
+import warnings
+from typing import (
+ Any,
+ AsyncIterable,
+ AsyncIterator,
+ Awaitable,
+ Deque,
+ Dict,
+ Iterable,
+ List,
+ Optional,
+ Union,
+ cast,
+)
+
+from .exceptions import (
+ ConnectionClosed,
+ ConnectionClosedError,
+ ConnectionClosedOK,
+ InvalidState,
+ PayloadTooBig,
+ ProtocolError,
+)
+from .extensions.base import Extension
+from .framing import *
+from .handshake import *
+from .http import Headers
+from .typing import Data
+
+
+__all__ = ["WebSocketCommonProtocol"]
+
+logger = logging.getLogger(__name__)
+
+
+# A WebSocket connection goes through the following four states, in order:
+
+
+class State(enum.IntEnum):
+ CONNECTING, OPEN, CLOSING, CLOSED = range(4)
+
+
+# In order to ensure consistency, the code always checks the current value of
+# WebSocketCommonProtocol.state before assigning a new value and never yields
+# between the check and the assignment.
+
+
+class WebSocketCommonProtocol(asyncio.Protocol):
+ """
+ :class:`~asyncio.Protocol` subclass implementing the data transfer phase.
+
+ Once the WebSocket connection is established, during the data transfer
+ phase, the protocol is almost symmetrical between the server side and the
+ client side. :class:`WebSocketCommonProtocol` implements logic that's
+ shared between servers and clients..
+
+ Subclasses such as :class:`~websockets.server.WebSocketServerProtocol` and
+ :class:`~websockets.client.WebSocketClientProtocol` implement the opening
+ handshake, which is different between servers and clients.
+
+ :class:`WebSocketCommonProtocol` performs four functions:
+
+ * It runs a task that stores incoming data frames in a queue and makes
+ them available with the :meth:`recv` coroutine.
+ * It sends outgoing data frames with the :meth:`send` coroutine.
+ * It deals with control frames automatically.
+ * It performs the closing handshake.
+
+ :class:`WebSocketCommonProtocol` supports asynchronous iteration::
+
+ async for message in websocket:
+ await process(message)
+
+ The iterator yields incoming messages. It exits normally when the
+ connection is closed with the close code 1000 (OK) or 1001 (going away).
+ It raises a :exc:`~websockets.exceptions.ConnectionClosedError` exception
+ when the connection is closed with any other code.
+
+ Once the connection is open, a `Ping frame`_ is sent every
+ ``ping_interval`` seconds. This serves as a keepalive. It helps keeping
+ the connection open, especially in the presence of proxies with short
+ timeouts on inactive connections. Set ``ping_interval`` to ``None`` to
+ disable this behavior.
+
+ .. _Ping frame: https://tools.ietf.org/html/rfc6455#section-5.5.2
+
+ If the corresponding `Pong frame`_ isn't received within ``ping_timeout``
+ seconds, the connection is considered unusable and is closed with
+ code 1011. This ensures that the remote endpoint remains responsive. Set
+ ``ping_timeout`` to ``None`` to disable this behavior.
+
+ .. _Pong frame: https://tools.ietf.org/html/rfc6455#section-5.5.3
+
+ The ``close_timeout`` parameter defines a maximum wait time in seconds for
+ completing the closing handshake and terminating the TCP connection.
+ :meth:`close` completes in at most ``4 * close_timeout`` on the server
+ side and ``5 * close_timeout`` on the client side.
+
+ ``close_timeout`` needs to be a parameter of the protocol because
+ ``websockets`` usually calls :meth:`close` implicitly:
+
+ - on the server side, when the connection handler terminates,
+ - on the client side, when exiting the context manager for the connection.
+
+ To apply a timeout to any other API, wrap it in :func:`~asyncio.wait_for`.
+
+ The ``max_size`` parameter enforces the maximum size for incoming messages
+ in bytes. The default value is 1 MiB. ``None`` disables the limit. If a
+ message larger than the maximum size is received, :meth:`recv` will
+ raise :exc:`~websockets.exceptions.ConnectionClosedError` and the
+ connection will be closed with code 1009.
+
+ The ``max_queue`` parameter sets the maximum length of the queue that
+ holds incoming messages. The default value is ``32``. ``None`` disables
+ the limit. Messages are added to an in-memory queue when they're received;
+ then :meth:`recv` pops from that queue. In order to prevent excessive
+ memory consumption when messages are received faster than they can be
+ processed, the queue must be bounded. If the queue fills up, the protocol
+ stops processing incoming data until :meth:`recv` is called. In this
+ situation, various receive buffers (at least in ``asyncio`` and in the OS)
+ will fill up, then the TCP receive window will shrink, slowing down
+ transmission to avoid packet loss.
+
+ Since Python can use up to 4 bytes of memory to represent a single
+ character, each connection may use up to ``4 * max_size * max_queue``
+ bytes of memory to store incoming messages. By default, this is 128 MiB.
+ You may want to lower the limits, depending on your application's
+ requirements.
+
+ The ``read_limit`` argument sets the high-water limit of the buffer for
+ incoming bytes. The low-water limit is half the high-water limit. The
+ default value is 64 KiB, half of asyncio's default (based on the current
+ implementation of :class:`~asyncio.StreamReader`).
+
+ The ``write_limit`` argument sets the high-water limit of the buffer for
+ outgoing bytes. The low-water limit is a quarter of the high-water limit.
+ The default value is 64 KiB, equal to asyncio's default (based on the
+ current implementation of ``FlowControlMixin``).
+
+ As soon as the HTTP request and response in the opening handshake are
+ processed:
+
+ * the request path is available in the :attr:`path` attribute;
+ * the request and response HTTP headers are available in the
+ :attr:`request_headers` and :attr:`response_headers` attributes,
+ which are :class:`~websockets.http.Headers` instances.
+
+ If a subprotocol was negotiated, it's available in the :attr:`subprotocol`
+ attribute.
+
+ Once the connection is closed, the code is available in the
+ :attr:`close_code` attribute and the reason in :attr:`close_reason`.
+
+ All these attributes must be treated as read-only.
+
+ """
+
+ # There are only two differences between the client-side and server-side
+ # behavior: masking the payload and closing the underlying TCP connection.
+ # Set is_client = True/False and side = "client"/"server" to pick a side.
+ is_client: bool
+ side: str = "undefined"
+
+ def __init__(
+ self,
+ *,
+ ping_interval: Optional[float] = 20,
+ ping_timeout: Optional[float] = 20,
+ close_timeout: Optional[float] = None,
+ max_size: Optional[int] = 2 ** 20,
+ max_queue: Optional[int] = 2 ** 5,
+ read_limit: int = 2 ** 16,
+ write_limit: int = 2 ** 16,
+ loop: Optional[asyncio.AbstractEventLoop] = None,
+ # The following arguments are kept only for backwards compatibility.
+ host: Optional[str] = None,
+ port: Optional[int] = None,
+ secure: Optional[bool] = None,
+ legacy_recv: bool = False,
+ timeout: Optional[float] = None,
+ ) -> None:
+ # Backwards compatibility: close_timeout used to be called timeout.
+ if timeout is None:
+ timeout = 10
+ else:
+ warnings.warn("rename timeout to close_timeout", DeprecationWarning)
+ # If both are specified, timeout is ignored.
+ if close_timeout is None:
+ close_timeout = timeout
+
+ self.ping_interval = ping_interval
+ self.ping_timeout = ping_timeout
+ self.close_timeout = close_timeout
+ self.max_size = max_size
+ self.max_queue = max_queue
+ self.read_limit = read_limit
+ self.write_limit = write_limit
+
+ if loop is None:
+ loop = asyncio.get_event_loop()
+ self.loop = loop
+
+ self._host = host
+ self._port = port
+ self._secure = secure
+ self.legacy_recv = legacy_recv
+
+ # Configure read buffer limits. The high-water limit is defined by
+ # ``self.read_limit``. The ``limit`` argument controls the line length
+ # limit and half the buffer limit of :class:`~asyncio.StreamReader`.
+ # That's why it must be set to half of ``self.read_limit``.
+ self.reader = asyncio.StreamReader(limit=read_limit // 2, loop=loop)
+
+ # Copied from asyncio.FlowControlMixin
+ self._paused = False
+ self._drain_waiter: Optional[asyncio.Future[None]] = None
+
+ self._drain_lock = asyncio.Lock(
+ **({"loop": loop} if sys.version_info[:2] < (3, 8) else {})
+ )
+
+ # This class implements the data transfer and closing handshake, which
+ # are shared between the client-side and the server-side.
+ # Subclasses implement the opening handshake and, on success, execute
+ # :meth:`connection_open` to change the state to OPEN.
+ self.state = State.CONNECTING
+ logger.debug("%s - state = CONNECTING", self.side)
+
+ # HTTP protocol parameters.
+ self.path: str
+ self.request_headers: Headers
+ self.response_headers: Headers
+
+ # WebSocket protocol parameters.
+ self.extensions: List[Extension] = []
+ self.subprotocol: Optional[str] = None
+
+ # The close code and reason are set when receiving a close frame or
+ # losing the TCP connection.
+ self.close_code: int
+ self.close_reason: str
+
+ # Completed when the connection state becomes CLOSED. Translates the
+ # :meth:`connection_lost` callback to a :class:`~asyncio.Future`
+ # that can be awaited. (Other :class:`~asyncio.Protocol` callbacks are
+ # translated by ``self.stream_reader``).
+ self.connection_lost_waiter: asyncio.Future[None] = loop.create_future()
+
+ # Queue of received messages.
+ self.messages: Deque[Data] = collections.deque()
+ self._pop_message_waiter: Optional[asyncio.Future[None]] = None
+ self._put_message_waiter: Optional[asyncio.Future[None]] = None
+
+ # Protect sending fragmented messages.
+ self._fragmented_message_waiter: Optional[asyncio.Future[None]] = None
+
+ # Mapping of ping IDs to waiters, in chronological order.
+ self.pings: Dict[bytes, asyncio.Future[None]] = {}
+
+ # Task running the data transfer.
+ self.transfer_data_task: asyncio.Task[None]
+
+ # Exception that occurred during data transfer, if any.
+ self.transfer_data_exc: Optional[BaseException] = None
+
+ # Task sending keepalive pings.
+ self.keepalive_ping_task: asyncio.Task[None]
+
+ # Task closing the TCP connection.
+ self.close_connection_task: asyncio.Task[None]
+
+ # Copied from asyncio.FlowControlMixin
+ async def _drain_helper(self) -> None: # pragma: no cover
+ if self.connection_lost_waiter.done():
+ raise ConnectionResetError("Connection lost")
+ if not self._paused:
+ return
+ waiter = self._drain_waiter
+ assert waiter is None or waiter.cancelled()
+ waiter = self.loop.create_future()
+ self._drain_waiter = waiter
+ await waiter
+
+ # Copied from asyncio.StreamWriter
+ async def _drain(self) -> None: # pragma: no cover
+ if self.reader is not None:
+ exc = self.reader.exception()
+ if exc is not None:
+ raise exc
+ if self.transport is not None:
+ if self.transport.is_closing():
+ # Yield to the event loop so connection_lost() may be
+ # called. Without this, _drain_helper() would return
+ # immediately, and code that calls
+ # write(...); yield from drain()
+ # in a loop would never call connection_lost(), so it
+ # would not see an error when the socket is closed.
+ await asyncio.sleep(
+ 0, **({"loop": self.loop} if sys.version_info[:2] < (3, 8) else {})
+ )
+ await self._drain_helper()
+
+ def connection_open(self) -> None:
+ """
+ Callback when the WebSocket opening handshake completes.
+
+ Enter the OPEN state and start the data transfer phase.
+
+ """
+ # 4.1. The WebSocket Connection is Established.
+ assert self.state is State.CONNECTING
+ self.state = State.OPEN
+ logger.debug("%s - state = OPEN", self.side)
+ # Start the task that receives incoming WebSocket messages.
+ self.transfer_data_task = self.loop.create_task(self.transfer_data())
+ # Start the task that sends pings at regular intervals.
+ self.keepalive_ping_task = self.loop.create_task(self.keepalive_ping())
+ # Start the task that eventually closes the TCP connection.
+ self.close_connection_task = self.loop.create_task(self.close_connection())
+
+ @property
+ def host(self) -> Optional[str]:
+ alternative = "remote_address" if self.is_client else "local_address"
+ warnings.warn(f"use {alternative}[0] instead of host", DeprecationWarning)
+ return self._host
+
+ @property
+ def port(self) -> Optional[int]:
+ alternative = "remote_address" if self.is_client else "local_address"
+ warnings.warn(f"use {alternative}[1] instead of port", DeprecationWarning)
+ return self._port
+
+ @property
+ def secure(self) -> Optional[bool]:
+ warnings.warn(f"don't use secure", DeprecationWarning)
+ return self._secure
+
+ # Public API
+
+ @property
+ def local_address(self) -> Any:
+ """
+ Local address of the connection.
+
+ This is a ``(host, port)`` tuple or ``None`` if the connection hasn't
+ been established yet.
+
+ """
+ try:
+ transport = self.transport
+ except AttributeError:
+ return None
+ else:
+ return transport.get_extra_info("sockname")
+
+ @property
+ def remote_address(self) -> Any:
+ """
+ Remote address of the connection.
+
+ This is a ``(host, port)`` tuple or ``None`` if the connection hasn't
+ been established yet.
+
+ """
+ try:
+ transport = self.transport
+ except AttributeError:
+ return None
+ else:
+ return transport.get_extra_info("peername")
+
+ @property
+ def open(self) -> bool:
+ """
+ ``True`` when the connection is usable.
+
+ It may be used to detect disconnections. However, this approach is
+ discouraged per the EAFP_ principle.
+
+ When ``open`` is ``False``, using the connection raises a
+ :exc:`~websockets.exceptions.ConnectionClosed` exception.
+
+ .. _EAFP: https://docs.python.org/3/glossary.html#term-eafp
+
+ """
+ return self.state is State.OPEN and not self.transfer_data_task.done()
+
+ @property
+ def closed(self) -> bool:
+ """
+ ``True`` once the connection is closed.
+
+ Be aware that both :attr:`open` and :attr:`closed` are ``False`` during
+ the opening and closing sequences.
+
+ """
+ return self.state is State.CLOSED
+
+ async def wait_closed(self) -> None:
+ """
+ Wait until the connection is closed.
+
+ This is identical to :attr:`closed`, except it can be awaited.
+
+ This can make it easier to handle connection termination, regardless
+ of its cause, in tasks that interact with the WebSocket connection.
+
+ """
+ await asyncio.shield(self.connection_lost_waiter)
+
+ async def __aiter__(self) -> AsyncIterator[Data]:
+ """
+ Iterate on received messages.
+
+ Exit normally when the connection is closed with code 1000 or 1001.
+
+ Raise an exception in other cases.
+
+ """
+ try:
+ while True:
+ yield await self.recv()
+ except ConnectionClosedOK:
+ return
+
+ async def recv(self) -> Data:
+ """
+ Receive the next message.
+
+ Return a :class:`str` for a text frame and :class:`bytes` for a binary
+ frame.
+
+ When the end of the message stream is reached, :meth:`recv` raises
+ :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it
+ raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal
+ connection closure and
+ :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol
+ error or a network failure.
+
+ .. versionchanged:: 3.0
+
+ :meth:`recv` used to return ``None`` instead. Refer to the
+ changelog for details.
+
+ Canceling :meth:`recv` is safe. There's no risk of losing the next
+ message. The next invocation of :meth:`recv` will return it. This
+ makes it possible to enforce a timeout by wrapping :meth:`recv` in
+ :func:`~asyncio.wait_for`.
+
+ :raises ~websockets.exceptions.ConnectionClosed: when the
+ connection is closed
+ :raises RuntimeError: if two coroutines call :meth:`recv` concurrently
+
+ """
+ if self._pop_message_waiter is not None:
+ raise RuntimeError(
+ "cannot call recv while another coroutine "
+ "is already waiting for the next message"
+ )
+
+ # Don't await self.ensure_open() here:
+ # - messages could be available in the queue even if the connection
+ # is closed;
+ # - messages could be received before the closing frame even if the
+ # connection is closing.
+
+ # Wait until there's a message in the queue (if necessary) or the
+ # connection is closed.
+ while len(self.messages) <= 0:
+ pop_message_waiter: asyncio.Future[None] = self.loop.create_future()
+ self._pop_message_waiter = pop_message_waiter
+ try:
+ # If asyncio.wait() is canceled, it doesn't cancel
+ # pop_message_waiter and self.transfer_data_task.
+ await asyncio.wait(
+ [pop_message_waiter, self.transfer_data_task],
+ **({"loop": self.loop} if sys.version_info[:2] < (3, 8) else {}),
+ return_when=asyncio.FIRST_COMPLETED,
+ )
+ finally:
+ self._pop_message_waiter = None
+
+ # If asyncio.wait(...) exited because self.transfer_data_task
+ # completed before receiving a new message, raise a suitable
+ # exception (or return None if legacy_recv is enabled).
+ if not pop_message_waiter.done():
+ if self.legacy_recv:
+ return None # type: ignore
+ else:
+ # Wait until the connection is closed to raise
+ # ConnectionClosed with the correct code and reason.
+ await self.ensure_open()
+
+ # Pop a message from the queue.
+ message = self.messages.popleft()
+
+ # Notify transfer_data().
+ if self._put_message_waiter is not None:
+ self._put_message_waiter.set_result(None)
+ self._put_message_waiter = None
+
+ return message
+
+ async def send(
+ self, message: Union[Data, Iterable[Data], AsyncIterable[Data]]
+ ) -> None:
+ """
+ Send a message.
+
+ A string (:class:`str`) is sent as a `Text frame`_. A bytestring or
+ bytes-like object (:class:`bytes`, :class:`bytearray`, or
+ :class:`memoryview`) is sent as a `Binary frame`_.
+
+ .. _Text frame: https://tools.ietf.org/html/rfc6455#section-5.6
+ .. _Binary frame: https://tools.ietf.org/html/rfc6455#section-5.6
+
+ :meth:`send` also accepts an iterable or an asynchronous iterable of
+ strings, bytestrings, or bytes-like objects. In that case the message
+ is fragmented. Each item is treated as a message fragment and sent in
+ its own frame. All items must be of the same type, or else
+ :meth:`send` will raise a :exc:`TypeError` and the connection will be
+ closed.
+
+ Canceling :meth:`send` is discouraged. Instead, you should close the
+ connection with :meth:`close`. Indeed, there only two situations where
+ :meth:`send` yields control to the event loop:
+
+ 1. The write buffer is full. If you don't want to wait until enough
+ data is sent, your only alternative is to close the connection.
+ :meth:`close` will likely time out then abort the TCP connection.
+ 2. ``message`` is an asynchronous iterator. Stopping in the middle of
+ a fragmented message will cause a protocol error. Closing the
+ connection has the same effect.
+
+ :raises TypeError: for unsupported inputs
+
+ """
+ await self.ensure_open()
+
+ # While sending a fragmented message, prevent sending other messages
+ # until all fragments are sent.
+ while self._fragmented_message_waiter is not None:
+ await asyncio.shield(self._fragmented_message_waiter)
+
+ # Unfragmented message -- this case must be handled first because
+ # strings and bytes-like objects are iterable.
+
+ if isinstance(message, (str, bytes, bytearray, memoryview)):
+ opcode, data = prepare_data(message)
+ await self.write_frame(True, opcode, data)
+
+ # Fragmented message -- regular iterator.
+
+ elif isinstance(message, Iterable):
+
+ # Work around https://github.com/python/mypy/issues/6227
+ message = cast(Iterable[Data], message)
+
+ iter_message = iter(message)
+ try:
+ message_chunk = next(iter_message)
+ except StopIteration:
+ return
+ opcode, data = prepare_data(message_chunk)
+
+ self._fragmented_message_waiter = asyncio.Future()
+ try:
+ # First fragment.
+ await self.write_frame(False, opcode, data)
+
+ # Other fragments.
+ for message_chunk in iter_message:
+ confirm_opcode, data = prepare_data(message_chunk)
+ if confirm_opcode != opcode:
+ raise TypeError("data contains inconsistent types")
+ await self.write_frame(False, OP_CONT, data)
+
+ # Final fragment.
+ await self.write_frame(True, OP_CONT, b"")
+
+ except Exception:
+ # We're half-way through a fragmented message and we can't
+ # complete it. This makes the connection unusable.
+ self.fail_connection(1011)
+ raise
+
+ finally:
+ self._fragmented_message_waiter.set_result(None)
+ self._fragmented_message_waiter = None
+
+ # Fragmented message -- asynchronous iterator
+
+ elif isinstance(message, AsyncIterable):
+ # aiter_message = aiter(message) without aiter
+ # https://github.com/python/mypy/issues/5738
+ aiter_message = type(message).__aiter__(message) # type: ignore
+ try:
+ # message_chunk = anext(aiter_message) without anext
+ # https://github.com/python/mypy/issues/5738
+ message_chunk = await type(aiter_message).__anext__( # type: ignore
+ aiter_message
+ )
+ except StopAsyncIteration:
+ return
+ opcode, data = prepare_data(message_chunk)
+
+ self._fragmented_message_waiter = asyncio.Future()
+ try:
+ # First fragment.
+ await self.write_frame(False, opcode, data)
+
+ # Other fragments.
+ # https://github.com/python/mypy/issues/5738
+ async for message_chunk in aiter_message: # type: ignore
+ confirm_opcode, data = prepare_data(message_chunk)
+ if confirm_opcode != opcode:
+ raise TypeError("data contains inconsistent types")
+ await self.write_frame(False, OP_CONT, data)
+
+ # Final fragment.
+ await self.write_frame(True, OP_CONT, b"")
+
+ except Exception:
+ # We're half-way through a fragmented message and we can't
+ # complete it. This makes the connection unusable.
+ self.fail_connection(1011)
+ raise
+
+ finally:
+ self._fragmented_message_waiter.set_result(None)
+ self._fragmented_message_waiter = None
+
+ else:
+ raise TypeError("data must be bytes, str, or iterable")
+
+ async def close(self, code: int = 1000, reason: str = "") -> None:
+ """
+ Perform the closing handshake.
+
+ :meth:`close` waits for the other end to complete the handshake and
+ for the TCP connection to terminate. As a consequence, there's no need
+ to await :meth:`wait_closed`; :meth:`close` already does it.
+
+ :meth:`close` is idempotent: it doesn't do anything once the
+ connection is closed.
+
+ Wrapping :func:`close` in :func:`~asyncio.create_task` is safe, given
+ that errors during connection termination aren't particularly useful.
+
+ Canceling :meth:`close` is discouraged. If it takes too long, you can
+ set a shorter ``close_timeout``. If you don't want to wait, let the
+ Python process exit, then the OS will close the TCP connection.
+
+ :param code: WebSocket close code
+ :param reason: WebSocket close reason
+
+ """
+ try:
+ await asyncio.wait_for(
+ self.write_close_frame(serialize_close(code, reason)),
+ self.close_timeout,
+ **({"loop": self.loop} if sys.version_info[:2] < (3, 8) else {}),
+ )
+ except asyncio.TimeoutError:
+ # If the close frame cannot be sent because the send buffers
+ # are full, the closing handshake won't complete anyway.
+ # Fail the connection to shut down faster.
+ self.fail_connection()
+
+ # If no close frame is received within the timeout, wait_for() cancels
+ # the data transfer task and raises TimeoutError.
+
+ # If close() is called multiple times concurrently and one of these
+ # calls hits the timeout, the data transfer task will be cancelled.
+ # Other calls will receive a CancelledError here.
+
+ try:
+ # If close() is canceled during the wait, self.transfer_data_task
+ # is canceled before the timeout elapses.
+ await asyncio.wait_for(
+ self.transfer_data_task,
+ self.close_timeout,
+ **({"loop": self.loop} if sys.version_info[:2] < (3, 8) else {}),
+ )
+ except (asyncio.TimeoutError, asyncio.CancelledError):
+ pass
+
+ # Wait for the close connection task to close the TCP connection.
+ await asyncio.shield(self.close_connection_task)
+
+ async def ping(self, data: Optional[Data] = None) -> Awaitable[None]:
+ """
+ Send a ping.
+
+ Return a :class:`~asyncio.Future` which will be completed when the
+ corresponding pong is received and which you may ignore if you don't
+ want to wait.
+
+ A ping may serve as a keepalive or as a check that the remote endpoint
+ received all messages up to this point::
+
+ pong_waiter = await ws.ping()
+ await pong_waiter # only if you want to wait for the pong
+
+ By default, the ping contains four random bytes. This payload may be
+ overridden with the optional ``data`` argument which must be a string
+ (which will be encoded to UTF-8) or a bytes-like object.
+
+ Canceling :meth:`ping` is discouraged. If :meth:`ping` doesn't return
+ immediately, it means the write buffer is full. If you don't want to
+ wait, you should close the connection.
+
+ Canceling the :class:`~asyncio.Future` returned by :meth:`ping` has no
+ effect.
+
+ """
+ await self.ensure_open()
+
+ if data is not None:
+ data = encode_data(data)
+
+ # Protect against duplicates if a payload is explicitly set.
+ if data in self.pings:
+ raise ValueError("already waiting for a pong with the same data")
+
+ # Generate a unique random payload otherwise.
+ while data is None or data in self.pings:
+ data = struct.pack("!I", random.getrandbits(32))
+
+ self.pings[data] = self.loop.create_future()
+
+ await self.write_frame(True, OP_PING, data)
+
+ return asyncio.shield(self.pings[data])
+
+ async def pong(self, data: Data = b"") -> None:
+ """
+ Send a pong.
+
+ An unsolicited pong may serve as a unidirectional heartbeat.
+
+ The payload may be set with the optional ``data`` argument which must
+ be a string (which will be encoded to UTF-8) or a bytes-like object.
+
+ Canceling :meth:`pong` is discouraged for the same reason as
+ :meth:`ping`.
+
+ """
+ await self.ensure_open()
+
+ data = encode_data(data)
+
+ await self.write_frame(True, OP_PONG, data)
+
+ # Private methods - no guarantees.
+
+ def connection_closed_exc(self) -> ConnectionClosed:
+ exception: ConnectionClosed
+ if self.close_code == 1000 or self.close_code == 1001:
+ exception = ConnectionClosedOK(self.close_code, self.close_reason)
+ else:
+ exception = ConnectionClosedError(self.close_code, self.close_reason)
+ # Chain to the exception that terminated data transfer, if any.
+ exception.__cause__ = self.transfer_data_exc
+ return exception
+
+ async def ensure_open(self) -> None:
+ """
+ Check that the WebSocket connection is open.
+
+ Raise :exc:`~websockets.exceptions.ConnectionClosed` if it isn't.
+
+ """
+ # Handle cases from most common to least common for performance.
+ if self.state is State.OPEN:
+ # If self.transfer_data_task exited without a closing handshake,
+ # self.close_connection_task may be closing the connection, going
+ # straight from OPEN to CLOSED.
+ if self.transfer_data_task.done():
+ await asyncio.shield(self.close_connection_task)
+ raise self.connection_closed_exc()
+ else:
+ return
+
+ if self.state is State.CLOSED:
+ raise self.connection_closed_exc()
+
+ if self.state is State.CLOSING:
+ # If we started the closing handshake, wait for its completion to
+ # get the proper close code and reason. self.close_connection_task
+ # will complete within 4 or 5 * close_timeout after close(). The
+ # CLOSING state also occurs when failing the connection. In that
+ # case self.close_connection_task will complete even faster.
+ await asyncio.shield(self.close_connection_task)
+ raise self.connection_closed_exc()
+
+ # Control may only reach this point in buggy third-party subclasses.
+ assert self.state is State.CONNECTING
+ raise InvalidState("WebSocket connection isn't established yet")
+
+ async def transfer_data(self) -> None:
+ """
+ Read incoming messages and put them in a queue.
+
+ This coroutine runs in a task until the closing handshake is started.
+
+ """
+ try:
+ while True:
+ message = await self.read_message()
+
+ # Exit the loop when receiving a close frame.
+ if message is None:
+ break
+
+ # Wait until there's room in the queue (if necessary).
+ if self.max_queue is not None:
+ while len(self.messages) >= self.max_queue:
+ self._put_message_waiter = self.loop.create_future()
+ try:
+ await asyncio.shield(self._put_message_waiter)
+ finally:
+ self._put_message_waiter = None
+
+ # Put the message in the queue.
+ self.messages.append(message)
+
+ # Notify recv().
+ if self._pop_message_waiter is not None:
+ self._pop_message_waiter.set_result(None)
+ self._pop_message_waiter = None
+
+ except asyncio.CancelledError as exc:
+ self.transfer_data_exc = exc
+ # If fail_connection() cancels this task, avoid logging the error
+ # twice and failing the connection again.
+ raise
+
+ except ProtocolError as exc:
+ self.transfer_data_exc = exc
+ self.fail_connection(1002)
+
+ except (ConnectionError, EOFError) as exc:
+ # Reading data with self.reader.readexactly may raise:
+ # - most subclasses of ConnectionError if the TCP connection
+ # breaks, is reset, or is aborted;
+ # - IncompleteReadError, a subclass of EOFError, if fewer
+ # bytes are available than requested.
+ self.transfer_data_exc = exc
+ self.fail_connection(1006)
+
+ except UnicodeDecodeError as exc:
+ self.transfer_data_exc = exc
+ self.fail_connection(1007)
+
+ except PayloadTooBig as exc:
+ self.transfer_data_exc = exc
+ self.fail_connection(1009)
+
+ except Exception as exc:
+ # This shouldn't happen often because exceptions expected under
+ # regular circumstances are handled above. If it does, consider
+ # catching and handling more exceptions.
+ logger.error("Error in data transfer", exc_info=True)
+
+ self.transfer_data_exc = exc
+ self.fail_connection(1011)
+
+ async def read_message(self) -> Optional[Data]:
+ """
+ Read a single message from the connection.
+
+ Re-assemble data frames if the message is fragmented.
+
+ Return ``None`` when the closing handshake is started.
+
+ """
+ frame = await self.read_data_frame(max_size=self.max_size)
+
+ # A close frame was received.
+ if frame is None:
+ return None
+
+ if frame.opcode == OP_TEXT:
+ text = True
+ elif frame.opcode == OP_BINARY:
+ text = False
+ else: # frame.opcode == OP_CONT
+ raise ProtocolError("unexpected opcode")
+
+ # Shortcut for the common case - no fragmentation
+ if frame.fin:
+ return frame.data.decode("utf-8") if text else frame.data
+
+ # 5.4. Fragmentation
+ chunks: List[Data] = []
+ max_size = self.max_size
+ if text:
+ decoder_factory = codecs.getincrementaldecoder("utf-8")
+ decoder = decoder_factory(errors="strict")
+ if max_size is None:
+
+ def append(frame: Frame) -> None:
+ nonlocal chunks
+ chunks.append(decoder.decode(frame.data, frame.fin))
+
+ else:
+
+ def append(frame: Frame) -> None:
+ nonlocal chunks, max_size
+ chunks.append(decoder.decode(frame.data, frame.fin))
+ assert isinstance(max_size, int)
+ max_size -= len(frame.data)
+
+ else:
+ if max_size is None:
+
+ def append(frame: Frame) -> None:
+ nonlocal chunks
+ chunks.append(frame.data)
+
+ else:
+
+ def append(frame: Frame) -> None:
+ nonlocal chunks, max_size
+ chunks.append(frame.data)
+ assert isinstance(max_size, int)
+ max_size -= len(frame.data)
+
+ append(frame)
+
+ while not frame.fin:
+ frame = await self.read_data_frame(max_size=max_size)
+ if frame is None:
+ raise ProtocolError("incomplete fragmented message")
+ if frame.opcode != OP_CONT:
+ raise ProtocolError("unexpected opcode")
+ append(frame)
+
+ # mypy cannot figure out that chunks have the proper type.
+ return ("" if text else b"").join(chunks) # type: ignore
+
+ async def read_data_frame(self, max_size: Optional[int]) -> Optional[Frame]:
+ """
+ Read a single data frame from the connection.
+
+ Process control frames received before the next data frame.
+
+ Return ``None`` if a close frame is encountered before any data frame.
+
+ """
+ # 6.2. Receiving Data
+ while True:
+ frame = await self.read_frame(max_size)
+
+ # 5.5. Control Frames
+ if frame.opcode == OP_CLOSE:
+ # 7.1.5. The WebSocket Connection Close Code
+ # 7.1.6. The WebSocket Connection Close Reason
+ self.close_code, self.close_reason = parse_close(frame.data)
+ try:
+ # Echo the original data instead of re-serializing it with
+ # serialize_close() because that fails when the close frame
+ # is empty and parse_close() synthetizes a 1005 close code.
+ await self.write_close_frame(frame.data)
+ except ConnectionClosed:
+ # It doesn't really matter if the connection was closed
+ # before we could send back a close frame.
+ pass
+ return None
+
+ elif frame.opcode == OP_PING:
+ # Answer pings.
+ ping_hex = frame.data.hex() or "[empty]"
+ logger.debug(
+ "%s - received ping, sending pong: %s", self.side, ping_hex
+ )
+ await self.pong(frame.data)
+
+ elif frame.opcode == OP_PONG:
+ # Acknowledge pings on solicited pongs.
+ if frame.data in self.pings:
+ logger.debug(
+ "%s - received solicited pong: %s",
+ self.side,
+ frame.data.hex() or "[empty]",
+ )
+ # Acknowledge all pings up to the one matching this pong.
+ ping_id = None
+ ping_ids = []
+ for ping_id, ping in self.pings.items():
+ ping_ids.append(ping_id)
+ if not ping.done():
+ ping.set_result(None)
+ if ping_id == frame.data:
+ break
+ else: # pragma: no cover
+ assert False, "ping_id is in self.pings"
+ # Remove acknowledged pings from self.pings.
+ for ping_id in ping_ids:
+ del self.pings[ping_id]
+ ping_ids = ping_ids[:-1]
+ if ping_ids:
+ pings_hex = ", ".join(
+ ping_id.hex() or "[empty]" for ping_id in ping_ids
+ )
+ plural = "s" if len(ping_ids) > 1 else ""
+ logger.debug(
+ "%s - acknowledged previous ping%s: %s",
+ self.side,
+ plural,
+ pings_hex,
+ )
+ else:
+ logger.debug(
+ "%s - received unsolicited pong: %s",
+ self.side,
+ frame.data.hex() or "[empty]",
+ )
+
+ # 5.6. Data Frames
+ else:
+ return frame
+
+ async def read_frame(self, max_size: Optional[int]) -> Frame:
+ """
+ Read a single frame from the connection.
+
+ """
+ frame = await Frame.read(
+ self.reader.readexactly,
+ mask=not self.is_client,
+ max_size=max_size,
+ extensions=self.extensions,
+ )
+ logger.debug("%s < %r", self.side, frame)
+ return frame
+
+ async def write_frame(
+ self, fin: bool, opcode: int, data: bytes, *, _expected_state: int = State.OPEN
+ ) -> None:
+ # Defensive assertion for protocol compliance.
+ if self.state is not _expected_state: # pragma: no cover
+ raise InvalidState(
+ f"Cannot write to a WebSocket in the {self.state.name} state"
+ )
+
+ frame = Frame(fin, opcode, data)
+ logger.debug("%s > %r", self.side, frame)
+ frame.write(
+ self.transport.write, mask=self.is_client, extensions=self.extensions
+ )
+
+ try:
+ # drain() cannot be called concurrently by multiple coroutines:
+ # http://bugs.python.org/issue29930. Remove this lock when no
+ # version of Python where this bugs exists is supported anymore.
+ async with self._drain_lock:
+ # Handle flow control automatically.
+ await self._drain()
+ except ConnectionError:
+ # Terminate the connection if the socket died.
+ self.fail_connection()
+ # Wait until the connection is closed to raise ConnectionClosed
+ # with the correct code and reason.
+ await self.ensure_open()
+
+ async def write_close_frame(self, data: bytes = b"") -> None:
+ """
+ Write a close frame if and only if the connection state is OPEN.
+
+ This dedicated coroutine must be used for writing close frames to
+ ensure that at most one close frame is sent on a given connection.
+
+ """
+ # Test and set the connection state before sending the close frame to
+ # avoid sending two frames in case of concurrent calls.
+ if self.state is State.OPEN:
+ # 7.1.3. The WebSocket Closing Handshake is Started
+ self.state = State.CLOSING
+ logger.debug("%s - state = CLOSING", self.side)
+
+ # 7.1.2. Start the WebSocket Closing Handshake
+ await self.write_frame(True, OP_CLOSE, data, _expected_state=State.CLOSING)
+
+ async def keepalive_ping(self) -> None:
+ """
+ Send a Ping frame and wait for a Pong frame at regular intervals.
+
+ This coroutine exits when the connection terminates and one of the
+ following happens:
+
+ - :meth:`ping` raises :exc:`ConnectionClosed`, or
+ - :meth:`close_connection` cancels :attr:`keepalive_ping_task`.
+
+ """
+ if self.ping_interval is None:
+ return
+
+ try:
+ while True:
+ await asyncio.sleep(
+ self.ping_interval,
+ **({"loop": self.loop} if sys.version_info[:2] < (3, 8) else {}),
+ )
+
+ # ping() raises CancelledError if the connection is closed,
+ # when close_connection() cancels self.keepalive_ping_task.
+
+ # ping() raises ConnectionClosed if the connection is lost,
+ # when connection_lost() calls abort_pings().
+
+ ping_waiter = await self.ping()
+
+ if self.ping_timeout is not None:
+ try:
+ await asyncio.wait_for(
+ ping_waiter,
+ self.ping_timeout,
+ **({"loop": self.loop} if sys.version_info[:2] < (3, 8) else {}),
+ )
+ except asyncio.TimeoutError:
+ logger.debug("%s ! timed out waiting for pong", self.side)
+ self.fail_connection(1011)
+ break
+
+ except asyncio.CancelledError:
+ raise
+
+ except ConnectionClosed:
+ pass
+
+ except Exception:
+ logger.warning("Unexpected exception in keepalive ping task", exc_info=True)
+
+ async def close_connection(self) -> None:
+ """
+ 7.1.1. Close the WebSocket Connection
+
+ When the opening handshake succeeds, :meth:`connection_open` starts
+ this coroutine in a task. It waits for the data transfer phase to
+ complete then it closes the TCP connection cleanly.
+
+ When the opening handshake fails, :meth:`fail_connection` does the
+ same. There's no data transfer phase in that case.
+
+ """
+ try:
+ # Wait for the data transfer phase to complete.
+ if hasattr(self, "transfer_data_task"):
+ try:
+ await self.transfer_data_task
+ except asyncio.CancelledError:
+ pass
+
+ # Cancel the keepalive ping task.
+ if hasattr(self, "keepalive_ping_task"):
+ self.keepalive_ping_task.cancel()
+
+ # A client should wait for a TCP close from the server.
+ if self.is_client and hasattr(self, "transfer_data_task"):
+ if await self.wait_for_connection_lost():
+ return
+ logger.debug("%s ! timed out waiting for TCP close", self.side)
+
+ # Half-close the TCP connection if possible (when there's no TLS).
+ if self.transport.can_write_eof():
+ logger.debug("%s x half-closing TCP connection", self.side)
+ self.transport.write_eof()
+
+ if await self.wait_for_connection_lost():
+ return
+ logger.debug("%s ! timed out waiting for TCP close", self.side)
+
+ finally:
+ # The try/finally ensures that the transport never remains open,
+ # even if this coroutine is canceled (for example).
+
+ # If connection_lost() was called, the TCP connection is closed.
+ # However, if TLS is enabled, the transport still needs closing.
+ # Else asyncio complains: ResourceWarning: unclosed transport.
+ if self.connection_lost_waiter.done() and self.transport.is_closing():
+ return
+
+ # Close the TCP connection. Buffers are flushed asynchronously.
+ logger.debug("%s x closing TCP connection", self.side)
+ self.transport.close()
+
+ if await self.wait_for_connection_lost():
+ return
+ logger.debug("%s ! timed out waiting for TCP close", self.side)
+
+ # Abort the TCP connection. Buffers are discarded.
+ logger.debug("%s x aborting TCP connection", self.side)
+ self.transport.abort()
+
+ # connection_lost() is called quickly after aborting.
+ await self.wait_for_connection_lost()
+
+ async def wait_for_connection_lost(self) -> bool:
+ """
+ Wait until the TCP connection is closed or ``self.close_timeout`` elapses.
+
+ Return ``True`` if the connection is closed and ``False`` otherwise.
+
+ """
+ if not self.connection_lost_waiter.done():
+ try:
+ await asyncio.wait_for(
+ asyncio.shield(self.connection_lost_waiter),
+ self.close_timeout,
+ **({"loop": self.loop} if sys.version_info[:2] < (3, 8) else {}),
+ )
+ except asyncio.TimeoutError:
+ pass
+ # Re-check self.connection_lost_waiter.done() synchronously because
+ # connection_lost() could run between the moment the timeout occurs
+ # and the moment this coroutine resumes running.
+ return self.connection_lost_waiter.done()
+
+ def fail_connection(self, code: int = 1006, reason: str = "") -> None:
+ """
+ 7.1.7. Fail the WebSocket Connection
+
+ This requires:
+
+ 1. Stopping all processing of incoming data, which means cancelling
+ :attr:`transfer_data_task`. The close code will be 1006 unless a
+ close frame was received earlier.
+
+ 2. Sending a close frame with an appropriate code if the opening
+ handshake succeeded and the other side is likely to process it.
+
+ 3. Closing the connection. :meth:`close_connection` takes care of
+ this once :attr:`transfer_data_task` exits after being canceled.
+
+ (The specification describes these steps in the opposite order.)
+
+ """
+ logger.debug(
+ "%s ! failing %s WebSocket connection with code %d",
+ self.side,
+ self.state.name,
+ code,
+ )
+
+ # Cancel transfer_data_task if the opening handshake succeeded.
+ # cancel() is idempotent and ignored if the task is done already.
+ if hasattr(self, "transfer_data_task"):
+ self.transfer_data_task.cancel()
+
+ # Send a close frame when the state is OPEN (a close frame was already
+ # sent if it's CLOSING), except when failing the connection because of
+ # an error reading from or writing to the network.
+ # Don't send a close frame if the connection is broken.
+ if code != 1006 and self.state is State.OPEN:
+
+ frame_data = serialize_close(code, reason)
+
+ # Write the close frame without draining the write buffer.
+
+ # Keeping fail_connection() synchronous guarantees it can't
+ # get stuck and simplifies the implementation of the callers.
+ # Not drainig the write buffer is acceptable in this context.
+
+ # This duplicates a few lines of code from write_close_frame()
+ # and write_frame().
+
+ self.state = State.CLOSING
+ logger.debug("%s - state = CLOSING", self.side)
+
+ frame = Frame(True, OP_CLOSE, frame_data)
+ logger.debug("%s > %r", self.side, frame)
+ frame.write(
+ self.transport.write, mask=self.is_client, extensions=self.extensions
+ )
+
+ # Start close_connection_task if the opening handshake didn't succeed.
+ if not hasattr(self, "close_connection_task"):
+ self.close_connection_task = self.loop.create_task(self.close_connection())
+
+ def abort_pings(self) -> None:
+ """
+ Raise ConnectionClosed in pending keepalive pings.
+
+ They'll never receive a pong once the connection is closed.
+
+ """
+ assert self.state is State.CLOSED
+ exc = self.connection_closed_exc()
+
+ for ping in self.pings.values():
+ ping.set_exception(exc)
+ # If the exception is never retrieved, it will be logged when ping
+ # is garbage-collected. This is confusing for users.
+ # Given that ping is done (with an exception), canceling it does
+ # nothing, but it prevents logging the exception.
+ ping.cancel()
+
+ if self.pings:
+ pings_hex = ", ".join(ping_id.hex() or "[empty]" for ping_id in self.pings)
+ plural = "s" if len(self.pings) > 1 else ""
+ logger.debug(
+ "%s - aborted pending ping%s: %s", self.side, plural, pings_hex
+ )
+
+ # asyncio.Protocol methods
+
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
+ """
+ Configure write buffer limits.
+
+ The high-water limit is defined by ``self.write_limit``.
+
+ The low-water limit currently defaults to ``self.write_limit // 4`` in
+ :meth:`~asyncio.WriteTransport.set_write_buffer_limits`, which should
+ be all right for reasonable use cases of this library.
+
+ This is the earliest point where we can get hold of the transport,
+ which means it's the best point for configuring it.
+
+ """
+ logger.debug("%s - event = connection_made(%s)", self.side, transport)
+
+ transport = cast(asyncio.Transport, transport)
+ transport.set_write_buffer_limits(self.write_limit)
+ self.transport = transport
+
+ # Copied from asyncio.StreamReaderProtocol
+ self.reader.set_transport(transport)
+
+ def connection_lost(self, exc: Optional[Exception]) -> None:
+ """
+ 7.1.4. The WebSocket Connection is Closed.
+
+ """
+ logger.debug("%s - event = connection_lost(%s)", self.side, exc)
+ self.state = State.CLOSED
+ logger.debug("%s - state = CLOSED", self.side)
+ if not hasattr(self, "close_code"):
+ self.close_code = 1006
+ if not hasattr(self, "close_reason"):
+ self.close_reason = ""
+ logger.debug(
+ "%s x code = %d, reason = %s",
+ self.side,
+ self.close_code,
+ self.close_reason or "[no reason]",
+ )
+ self.abort_pings()
+ # If self.connection_lost_waiter isn't pending, that's a bug, because:
+ # - it's set only here in connection_lost() which is called only once;
+ # - it must never be canceled.
+ self.connection_lost_waiter.set_result(None)
+
+ if True: # pragma: no cover
+
+ # Copied from asyncio.StreamReaderProtocol
+ if self.reader is not None:
+ if exc is None:
+ self.reader.feed_eof()
+ else:
+ self.reader.set_exception(exc)
+
+ # Copied from asyncio.FlowControlMixin
+ # Wake up the writer if currently paused.
+ if not self._paused:
+ return
+ waiter = self._drain_waiter
+ if waiter is None:
+ return
+ self._drain_waiter = None
+ if waiter.done():
+ return
+ if exc is None:
+ waiter.set_result(None)
+ else:
+ waiter.set_exception(exc)
+
+ def pause_writing(self) -> None: # pragma: no cover
+ assert not self._paused
+ self._paused = True
+
+ def resume_writing(self) -> None: # pragma: no cover
+ assert self._paused
+ self._paused = False
+
+ waiter = self._drain_waiter
+ if waiter is not None:
+ self._drain_waiter = None
+ if not waiter.done():
+ waiter.set_result(None)
+
+ def data_received(self, data: bytes) -> None:
+ logger.debug("%s - event = data_received(<%d bytes>)", self.side, len(data))
+ self.reader.feed_data(data)
+
+ def eof_received(self) -> None:
+ """
+ Close the transport after receiving EOF.
+
+ The WebSocket protocol has its own closing handshake: endpoints close
+ the TCP or TLS connection after sending and receiving a close frame.
+
+ As a consequence, they never need to write after receiving EOF, so
+ there's no reason to keep the transport open by returning ``True``.
+
+ Besides, that doesn't work on TLS connections.
+
+ """
+ logger.debug("%s - event = eof_received()", self.side)
+ self.reader.feed_eof()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/py.typed b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/py.typed
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/py.typed
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py
new file mode 100644
index 0000000000..0592083ef7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py
@@ -0,0 +1,996 @@
+"""
+:mod:`websockets.server` defines the WebSocket server APIs.
+
+"""
+
+import asyncio
+import collections.abc
+import email.utils
+import functools
+import http
+import logging
+import socket
+import sys
+import warnings
+from types import TracebackType
+from typing import (
+ Any,
+ Awaitable,
+ Callable,
+ Generator,
+ List,
+ Optional,
+ Sequence,
+ Set,
+ Tuple,
+ Type,
+ Union,
+ cast,
+)
+
+from .exceptions import (
+ AbortHandshake,
+ InvalidHandshake,
+ InvalidHeader,
+ InvalidMessage,
+ InvalidOrigin,
+ InvalidUpgrade,
+ NegotiationError,
+)
+from .extensions.base import Extension, ServerExtensionFactory
+from .extensions.permessage_deflate import ServerPerMessageDeflateFactory
+from .handshake import build_response, check_request
+from .headers import build_extension, parse_extension, parse_subprotocol
+from .http import USER_AGENT, Headers, HeadersLike, MultipleValuesError, read_request
+from .protocol import WebSocketCommonProtocol
+from .typing import ExtensionHeader, Origin, Subprotocol
+
+
+__all__ = ["serve", "unix_serve", "WebSocketServerProtocol", "WebSocketServer"]
+
+logger = logging.getLogger(__name__)
+
+
+HeadersLikeOrCallable = Union[HeadersLike, Callable[[str, Headers], HeadersLike]]
+
+HTTPResponse = Tuple[http.HTTPStatus, HeadersLike, bytes]
+
+
+class WebSocketServerProtocol(WebSocketCommonProtocol):
+ """
+ :class:`~asyncio.Protocol` subclass implementing a WebSocket server.
+
+ This class inherits most of its methods from
+ :class:`~websockets.protocol.WebSocketCommonProtocol`.
+
+ For the sake of simplicity, it doesn't rely on a full HTTP implementation.
+ Its support for HTTP responses is very limited.
+
+ """
+
+ is_client = False
+ side = "server"
+
+ def __init__(
+ self,
+ ws_handler: Callable[["WebSocketServerProtocol", str], Awaitable[Any]],
+ ws_server: "WebSocketServer",
+ *,
+ origins: Optional[Sequence[Optional[Origin]]] = None,
+ extensions: Optional[Sequence[ServerExtensionFactory]] = None,
+ subprotocols: Optional[Sequence[Subprotocol]] = None,
+ extra_headers: Optional[HeadersLikeOrCallable] = None,
+ process_request: Optional[
+ Callable[[str, Headers], Awaitable[Optional[HTTPResponse]]]
+ ] = None,
+ select_subprotocol: Optional[
+ Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol]
+ ] = None,
+ **kwargs: Any,
+ ) -> None:
+ # For backwards compatibility with 6.0 or earlier.
+ if origins is not None and "" in origins:
+ warnings.warn("use None instead of '' in origins", DeprecationWarning)
+ origins = [None if origin == "" else origin for origin in origins]
+ self.ws_handler = ws_handler
+ self.ws_server = ws_server
+ self.origins = origins
+ self.available_extensions = extensions
+ self.available_subprotocols = subprotocols
+ self.extra_headers = extra_headers
+ self._process_request = process_request
+ self._select_subprotocol = select_subprotocol
+ super().__init__(**kwargs)
+
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
+ """
+ Register connection and initialize a task to handle it.
+
+ """
+ super().connection_made(transport)
+ # Register the connection with the server before creating the handler
+ # task. Registering at the beginning of the handler coroutine would
+ # create a race condition between the creation of the task, which
+ # schedules its execution, and the moment the handler starts running.
+ self.ws_server.register(self)
+ self.handler_task = self.loop.create_task(self.handler())
+
+ async def handler(self) -> None:
+ """
+ Handle the lifecycle of a WebSocket connection.
+
+ Since this method doesn't have a caller able to handle exceptions, it
+ attemps to log relevant ones and guarantees that the TCP connection is
+ closed before exiting.
+
+ """
+ try:
+
+ try:
+ path = await self.handshake(
+ origins=self.origins,
+ available_extensions=self.available_extensions,
+ available_subprotocols=self.available_subprotocols,
+ extra_headers=self.extra_headers,
+ )
+ except ConnectionError:
+ logger.debug("Connection error in opening handshake", exc_info=True)
+ raise
+ except Exception as exc:
+ if isinstance(exc, AbortHandshake):
+ status, headers, body = exc.status, exc.headers, exc.body
+ elif isinstance(exc, InvalidOrigin):
+ logger.debug("Invalid origin", exc_info=True)
+ status, headers, body = (
+ http.HTTPStatus.FORBIDDEN,
+ Headers(),
+ f"Failed to open a WebSocket connection: {exc}.\n".encode(),
+ )
+ elif isinstance(exc, InvalidUpgrade):
+ logger.debug("Invalid upgrade", exc_info=True)
+ status, headers, body = (
+ http.HTTPStatus.UPGRADE_REQUIRED,
+ Headers([("Upgrade", "websocket")]),
+ (
+ f"Failed to open a WebSocket connection: {exc}.\n"
+ f"\n"
+ f"You cannot access a WebSocket server directly "
+ f"with a browser. You need a WebSocket client.\n"
+ ).encode(),
+ )
+ elif isinstance(exc, InvalidHandshake):
+ logger.debug("Invalid handshake", exc_info=True)
+ status, headers, body = (
+ http.HTTPStatus.BAD_REQUEST,
+ Headers(),
+ f"Failed to open a WebSocket connection: {exc}.\n".encode(),
+ )
+ else:
+ logger.warning("Error in opening handshake", exc_info=True)
+ status, headers, body = (
+ http.HTTPStatus.INTERNAL_SERVER_ERROR,
+ Headers(),
+ (
+ b"Failed to open a WebSocket connection.\n"
+ b"See server log for more information.\n"
+ ),
+ )
+
+ headers.setdefault("Date", email.utils.formatdate(usegmt=True))
+ headers.setdefault("Server", USER_AGENT)
+ headers.setdefault("Content-Length", str(len(body)))
+ headers.setdefault("Content-Type", "text/plain")
+ headers.setdefault("Connection", "close")
+
+ self.write_http_response(status, headers, body)
+ self.fail_connection()
+ await self.wait_closed()
+ return
+
+ try:
+ await self.ws_handler(self, path)
+ except Exception:
+ logger.error("Error in connection handler", exc_info=True)
+ if not self.closed:
+ self.fail_connection(1011)
+ raise
+
+ try:
+ await self.close()
+ except ConnectionError:
+ logger.debug("Connection error in closing handshake", exc_info=True)
+ raise
+ except Exception:
+ logger.warning("Error in closing handshake", exc_info=True)
+ raise
+
+ except Exception:
+ # Last-ditch attempt to avoid leaking connections on errors.
+ try:
+ self.transport.close()
+ except Exception: # pragma: no cover
+ pass
+
+ finally:
+ # Unregister the connection with the server when the handler task
+ # terminates. Registration is tied to the lifecycle of the handler
+ # task because the server waits for tasks attached to registered
+ # connections before terminating.
+ self.ws_server.unregister(self)
+
+ async def read_http_request(self) -> Tuple[str, Headers]:
+ """
+ Read request line and headers from the HTTP request.
+
+ If the request contains a body, it may be read from ``self.reader``
+ after this coroutine returns.
+
+ :raises ~websockets.exceptions.InvalidMessage: if the HTTP message is
+ malformed or isn't an HTTP/1.1 GET request
+
+ """
+ try:
+ path, headers = await read_request(self.reader)
+ except Exception as exc:
+ raise InvalidMessage("did not receive a valid HTTP request") from exc
+
+ logger.debug("%s < GET %s HTTP/1.1", self.side, path)
+ logger.debug("%s < %r", self.side, headers)
+
+ self.path = path
+ self.request_headers = headers
+
+ return path, headers
+
+ def write_http_response(
+ self, status: http.HTTPStatus, headers: Headers, body: Optional[bytes] = None
+ ) -> None:
+ """
+ Write status line and headers to the HTTP response.
+
+ This coroutine is also able to write a response body.
+
+ """
+ self.response_headers = headers
+
+ logger.debug("%s > HTTP/1.1 %d %s", self.side, status.value, status.phrase)
+ logger.debug("%s > %r", self.side, headers)
+
+ # Since the status line and headers only contain ASCII characters,
+ # we can keep this simple.
+ response = f"HTTP/1.1 {status.value} {status.phrase}\r\n"
+ response += str(headers)
+
+ self.transport.write(response.encode())
+
+ if body is not None:
+ logger.debug("%s > body (%d bytes)", self.side, len(body))
+ self.transport.write(body)
+
+ async def process_request(
+ self, path: str, request_headers: Headers
+ ) -> Optional[HTTPResponse]:
+ """
+ Intercept the HTTP request and return an HTTP response if appropriate.
+
+ If ``process_request`` returns ``None``, the WebSocket handshake
+ continues. If it returns 3-uple containing a status code, response
+ headers and a response body, that HTTP response is sent and the
+ connection is closed. In that case:
+
+ * The HTTP status must be a :class:`~http.HTTPStatus`.
+ * HTTP headers must be a :class:`~websockets.http.Headers` instance, a
+ :class:`~collections.abc.Mapping`, or an iterable of ``(name,
+ value)`` pairs.
+ * The HTTP response body must be :class:`bytes`. It may be empty.
+
+ This coroutine may be overridden in a :class:`WebSocketServerProtocol`
+ subclass, for example:
+
+ * to return a HTTP 200 OK response on a given path; then a load
+ balancer can use this path for a health check;
+ * to authenticate the request and return a HTTP 401 Unauthorized or a
+ HTTP 403 Forbidden when authentication fails.
+
+ Instead of subclassing, it is possible to override this method by
+ passing a ``process_request`` argument to the :func:`serve` function
+ or the :class:`WebSocketServerProtocol` constructor. This is
+ equivalent, except ``process_request`` won't have access to the
+ protocol instance, so it can't store information for later use.
+
+ ``process_request`` is expected to complete quickly. If it may run for
+ a long time, then it should await :meth:`wait_closed` and exit if
+ :meth:`wait_closed` completes, or else it could prevent the server
+ from shutting down.
+
+ :param path: request path, including optional query string
+ :param request_headers: request headers
+
+ """
+ if self._process_request is not None:
+ response = self._process_request(path, request_headers)
+ if isinstance(response, Awaitable):
+ return await response
+ else:
+ # For backwards compatibility with 7.0.
+ warnings.warn(
+ "declare process_request as a coroutine", DeprecationWarning
+ )
+ return response # type: ignore
+ return None
+
+ @staticmethod
+ def process_origin(
+ headers: Headers, origins: Optional[Sequence[Optional[Origin]]] = None
+ ) -> Optional[Origin]:
+ """
+ Handle the Origin HTTP request header.
+
+ :param headers: request headers
+ :param origins: optional list of acceptable origins
+ :raises ~websockets.exceptions.InvalidOrigin: if the origin isn't
+ acceptable
+
+ """
+ # "The user agent MUST NOT include more than one Origin header field"
+ # per https://tools.ietf.org/html/rfc6454#section-7.3.
+ try:
+ origin = cast(Origin, headers.get("Origin"))
+ except MultipleValuesError:
+ raise InvalidHeader("Origin", "more than one Origin header found")
+ if origins is not None:
+ if origin not in origins:
+ raise InvalidOrigin(origin)
+ return origin
+
+ @staticmethod
+ def process_extensions(
+ headers: Headers,
+ available_extensions: Optional[Sequence[ServerExtensionFactory]],
+ ) -> Tuple[Optional[str], List[Extension]]:
+ """
+ Handle the Sec-WebSocket-Extensions HTTP request header.
+
+ Accept or reject each extension proposed in the client request.
+ Negotiate parameters for accepted extensions.
+
+ Return the Sec-WebSocket-Extensions HTTP response header and the list
+ of accepted extensions.
+
+ :rfc:`6455` leaves the rules up to the specification of each
+ :extension.
+
+ To provide this level of flexibility, for each extension proposed by
+ the client, we check for a match with each extension available in the
+ server configuration. If no match is found, the extension is ignored.
+
+ If several variants of the same extension are proposed by the client,
+ it may be accepted severel times, which won't make sense in general.
+ Extensions must implement their own requirements. For this purpose,
+ the list of previously accepted extensions is provided.
+
+ This process doesn't allow the server to reorder extensions. It can
+ only select a subset of the extensions proposed by the client.
+
+ Other requirements, for example related to mandatory extensions or the
+ order of extensions, may be implemented by overriding this method.
+
+ :param headers: request headers
+ :param extensions: optional list of supported extensions
+ :raises ~websockets.exceptions.InvalidHandshake: to abort the
+ handshake with an HTTP 400 error code
+
+ """
+ response_header_value: Optional[str] = None
+
+ extension_headers: List[ExtensionHeader] = []
+ accepted_extensions: List[Extension] = []
+
+ header_values = headers.get_all("Sec-WebSocket-Extensions")
+
+ if header_values and available_extensions:
+
+ parsed_header_values: List[ExtensionHeader] = sum(
+ [parse_extension(header_value) for header_value in header_values], []
+ )
+
+ for name, request_params in parsed_header_values:
+
+ for ext_factory in available_extensions:
+
+ # Skip non-matching extensions based on their name.
+ if ext_factory.name != name:
+ continue
+
+ # Skip non-matching extensions based on their params.
+ try:
+ response_params, extension = ext_factory.process_request_params(
+ request_params, accepted_extensions
+ )
+ except NegotiationError:
+ continue
+
+ # Add matching extension to the final list.
+ extension_headers.append((name, response_params))
+ accepted_extensions.append(extension)
+
+ # Break out of the loop once we have a match.
+ break
+
+ # If we didn't break from the loop, no extension in our list
+ # matched what the client sent. The extension is declined.
+
+ # Serialize extension header.
+ if extension_headers:
+ response_header_value = build_extension(extension_headers)
+
+ return response_header_value, accepted_extensions
+
+ # Not @staticmethod because it calls self.select_subprotocol()
+ def process_subprotocol(
+ self, headers: Headers, available_subprotocols: Optional[Sequence[Subprotocol]]
+ ) -> Optional[Subprotocol]:
+ """
+ Handle the Sec-WebSocket-Protocol HTTP request header.
+
+ Return Sec-WebSocket-Protocol HTTP response header, which is the same
+ as the selected subprotocol.
+
+ :param headers: request headers
+ :param available_subprotocols: optional list of supported subprotocols
+ :raises ~websockets.exceptions.InvalidHandshake: to abort the
+ handshake with an HTTP 400 error code
+
+ """
+ subprotocol: Optional[Subprotocol] = None
+
+ header_values = headers.get_all("Sec-WebSocket-Protocol")
+
+ if header_values and available_subprotocols:
+
+ parsed_header_values: List[Subprotocol] = sum(
+ [parse_subprotocol(header_value) for header_value in header_values], []
+ )
+
+ subprotocol = self.select_subprotocol(
+ parsed_header_values, available_subprotocols
+ )
+
+ return subprotocol
+
+ def select_subprotocol(
+ self,
+ client_subprotocols: Sequence[Subprotocol],
+ server_subprotocols: Sequence[Subprotocol],
+ ) -> Optional[Subprotocol]:
+ """
+ Pick a subprotocol among those offered by the client.
+
+ If several subprotocols are supported by the client and the server,
+ the default implementation selects the preferred subprotocols by
+ giving equal value to the priorities of the client and the server.
+
+ If no subprotocol is supported by the client and the server, it
+ proceeds without a subprotocol.
+
+ This is unlikely to be the most useful implementation in practice, as
+ many servers providing a subprotocol will require that the client uses
+ that subprotocol. Such rules can be implemented in a subclass.
+
+ Instead of subclassing, it is possible to override this method by
+ passing a ``select_subprotocol`` argument to the :func:`serve`
+ function or the :class:`WebSocketServerProtocol` constructor
+
+ :param client_subprotocols: list of subprotocols offered by the client
+ :param server_subprotocols: list of subprotocols available on the server
+
+ """
+ if self._select_subprotocol is not None:
+ return self._select_subprotocol(client_subprotocols, server_subprotocols)
+
+ subprotocols = set(client_subprotocols) & set(server_subprotocols)
+ if not subprotocols:
+ return None
+ priority = lambda p: (
+ client_subprotocols.index(p) + server_subprotocols.index(p)
+ )
+ return sorted(subprotocols, key=priority)[0]
+
+ async def handshake(
+ self,
+ origins: Optional[Sequence[Optional[Origin]]] = None,
+ available_extensions: Optional[Sequence[ServerExtensionFactory]] = None,
+ available_subprotocols: Optional[Sequence[Subprotocol]] = None,
+ extra_headers: Optional[HeadersLikeOrCallable] = None,
+ ) -> str:
+ """
+ Perform the server side of the opening handshake.
+
+ Return the path of the URI of the request.
+
+ :param origins: list of acceptable values of the Origin HTTP header;
+ include ``None`` if the lack of an origin is acceptable
+ :param available_extensions: list of supported extensions in the order
+ in which they should be used
+ :param available_subprotocols: list of supported subprotocols in order
+ of decreasing preference
+ :param extra_headers: sets additional HTTP response headers when the
+ handshake succeeds; it can be a :class:`~websockets.http.Headers`
+ instance, a :class:`~collections.abc.Mapping`, an iterable of
+ ``(name, value)`` pairs, or a callable taking the request path and
+ headers in arguments and returning one of the above.
+ :raises ~websockets.exceptions.InvalidHandshake: if the handshake
+ fails
+
+ """
+ path, request_headers = await self.read_http_request()
+
+ # Hook for customizing request handling, for example checking
+ # authentication or treating some paths as plain HTTP endpoints.
+ early_response_awaitable = self.process_request(path, request_headers)
+ if isinstance(early_response_awaitable, Awaitable):
+ early_response = await early_response_awaitable
+ else:
+ # For backwards compatibility with 7.0.
+ warnings.warn("declare process_request as a coroutine", DeprecationWarning)
+ early_response = early_response_awaitable # type: ignore
+
+ # Change the response to a 503 error if the server is shutting down.
+ if not self.ws_server.is_serving():
+ early_response = (
+ http.HTTPStatus.SERVICE_UNAVAILABLE,
+ [],
+ b"Server is shutting down.\n",
+ )
+
+ if early_response is not None:
+ raise AbortHandshake(*early_response)
+
+ key = check_request(request_headers)
+
+ self.origin = self.process_origin(request_headers, origins)
+
+ extensions_header, self.extensions = self.process_extensions(
+ request_headers, available_extensions
+ )
+
+ protocol_header = self.subprotocol = self.process_subprotocol(
+ request_headers, available_subprotocols
+ )
+
+ response_headers = Headers()
+
+ build_response(response_headers, key)
+
+ if extensions_header is not None:
+ response_headers["Sec-WebSocket-Extensions"] = extensions_header
+
+ if protocol_header is not None:
+ response_headers["Sec-WebSocket-Protocol"] = protocol_header
+
+ if callable(extra_headers):
+ extra_headers = extra_headers(path, self.request_headers)
+ if extra_headers is not None:
+ if isinstance(extra_headers, Headers):
+ extra_headers = extra_headers.raw_items()
+ elif isinstance(extra_headers, collections.abc.Mapping):
+ extra_headers = extra_headers.items()
+ for name, value in extra_headers:
+ response_headers[name] = value
+
+ response_headers.setdefault("Date", email.utils.formatdate(usegmt=True))
+ response_headers.setdefault("Server", USER_AGENT)
+
+ self.write_http_response(http.HTTPStatus.SWITCHING_PROTOCOLS, response_headers)
+
+ self.connection_open()
+
+ return path
+
+
+class WebSocketServer:
+ """
+ WebSocket server returned by :func:`~websockets.server.serve`.
+
+ This class provides the same interface as
+ :class:`~asyncio.AbstractServer`, namely the
+ :meth:`~asyncio.AbstractServer.close` and
+ :meth:`~asyncio.AbstractServer.wait_closed` methods.
+
+ It keeps track of WebSocket connections in order to close them properly
+ when shutting down.
+
+ Instances of this class store a reference to the :class:`~asyncio.Server`
+ object returned by :meth:`~asyncio.loop.create_server` rather than inherit
+ from :class:`~asyncio.Server` in part because
+ :meth:`~asyncio.loop.create_server` doesn't support passing a custom
+ :class:`~asyncio.Server` class.
+
+ """
+
+ def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
+ # Store a reference to loop to avoid relying on self.server._loop.
+ self.loop = loop
+
+ # Keep track of active connections.
+ self.websockets: Set[WebSocketServerProtocol] = set()
+
+ # Task responsible for closing the server and terminating connections.
+ self.close_task: Optional[asyncio.Task[None]] = None
+
+ # Completed when the server is closed and connections are terminated.
+ self.closed_waiter: asyncio.Future[None] = loop.create_future()
+
+ def wrap(self, server: asyncio.AbstractServer) -> None:
+ """
+ Attach to a given :class:`~asyncio.Server`.
+
+ Since :meth:`~asyncio.loop.create_server` doesn't support injecting a
+ custom ``Server`` class, the easiest solution that doesn't rely on
+ private :mod:`asyncio` APIs is to:
+
+ - instantiate a :class:`WebSocketServer`
+ - give the protocol factory a reference to that instance
+ - call :meth:`~asyncio.loop.create_server` with the factory
+ - attach the resulting :class:`~asyncio.Server` with this method
+
+ """
+ self.server = server
+
+ def register(self, protocol: WebSocketServerProtocol) -> None:
+ """
+ Register a connection with this server.
+
+ """
+ self.websockets.add(protocol)
+
+ def unregister(self, protocol: WebSocketServerProtocol) -> None:
+ """
+ Unregister a connection with this server.
+
+ """
+ self.websockets.remove(protocol)
+
+ def is_serving(self) -> bool:
+ """
+ Tell whether the server is accepting new connections or shutting down.
+
+ """
+ try:
+ # Python ≥ 3.7
+ return self.server.is_serving()
+ except AttributeError: # pragma: no cover
+ # Python < 3.7
+ return self.server.sockets is not None
+
+ def close(self) -> None:
+ """
+ Close the server.
+
+ This method:
+
+ * closes the underlying :class:`~asyncio.Server`;
+ * rejects new WebSocket connections with an HTTP 503 (service
+ unavailable) error; this happens when the server accepted the TCP
+ connection but didn't complete the WebSocket opening handshake prior
+ to closing;
+ * closes open WebSocket connections with close code 1001 (going away).
+
+ :meth:`close` is idempotent.
+
+ """
+ if self.close_task is None:
+ self.close_task = self.loop.create_task(self._close())
+
+ async def _close(self) -> None:
+ """
+ Implementation of :meth:`close`.
+
+ This calls :meth:`~asyncio.Server.close` on the underlying
+ :class:`~asyncio.Server` object to stop accepting new connections and
+ then closes open connections with close code 1001.
+
+ """
+ # Stop accepting new connections.
+ self.server.close()
+
+ # Wait until self.server.close() completes.
+ await self.server.wait_closed()
+
+ # Wait until all accepted connections reach connection_made() and call
+ # register(). See https://bugs.python.org/issue34852 for details.
+ await asyncio.sleep(
+ 0,
+ **({"loop": self.loop} if sys.version_info[:2] < (3, 8) else {}),
+ )
+
+ # Close OPEN connections with status code 1001. Since the server was
+ # closed, handshake() closes OPENING conections with a HTTP 503 error.
+ # Wait until all connections are closed.
+
+ # asyncio.wait doesn't accept an empty first argument
+ if self.websockets:
+ await asyncio.wait(
+ [websocket.close(1001) for websocket in self.websockets],
+ **({"loop": self.loop} if sys.version_info[:2] < (3, 8) else {}),
+ )
+
+ # Wait until all connection handlers are complete.
+
+ # asyncio.wait doesn't accept an empty first argument.
+ if self.websockets:
+ await asyncio.wait(
+ [websocket.handler_task for websocket in self.websockets],
+ **({"loop": self.loop} if sys.version_info[:2] < (3, 8) else {}),
+ )
+
+ # Tell wait_closed() to return.
+ self.closed_waiter.set_result(None)
+
+ async def wait_closed(self) -> None:
+ """
+ Wait until the server is closed.
+
+ When :meth:`wait_closed` returns, all TCP connections are closed and
+ all connection handlers have returned.
+
+ """
+ await asyncio.shield(self.closed_waiter)
+
+ @property
+ def sockets(self) -> Optional[List[socket.socket]]:
+ """
+ List of :class:`~socket.socket` objects the server is listening to.
+
+ ``None`` if the server is closed.
+
+ """
+ return self.server.sockets
+
+
+class Serve:
+ """
+
+ Create, start, and return a WebSocket server on ``host`` and ``port``.
+
+ Whenever a client connects, the server accepts the connection, creates a
+ :class:`WebSocketServerProtocol`, performs the opening handshake, and
+ delegates to the connection handler defined by ``ws_handler``. Once the
+ handler completes, either normally or with an exception, the server
+ performs the closing handshake and closes the connection.
+
+ Awaiting :func:`serve` yields a :class:`WebSocketServer`. This instance
+ provides :meth:`~websockets.server.WebSocketServer.close` and
+ :meth:`~websockets.server.WebSocketServer.wait_closed` methods for
+ terminating the server and cleaning up its resources.
+
+ When a server is closed with :meth:`~WebSocketServer.close`, it closes all
+ connections with close code 1001 (going away). Connections handlers, which
+ are running the ``ws_handler`` coroutine, will receive a
+ :exc:`~websockets.exceptions.ConnectionClosedOK` exception on their
+ current or next interaction with the WebSocket connection.
+
+ :func:`serve` can also be used as an asynchronous context manager. In
+ this case, the server is shut down when exiting the context.
+
+ :func:`serve` is a wrapper around the event loop's
+ :meth:`~asyncio.loop.create_server` method. It creates and starts a
+ :class:`~asyncio.Server` with :meth:`~asyncio.loop.create_server`. Then it
+ wraps the :class:`~asyncio.Server` in a :class:`WebSocketServer` and
+ returns the :class:`WebSocketServer`.
+
+ The ``ws_handler`` argument is the WebSocket handler. It must be a
+ coroutine accepting two arguments: a :class:`WebSocketServerProtocol` and
+ the request URI.
+
+ The ``host`` and ``port`` arguments, as well as unrecognized keyword
+ arguments, are passed along to :meth:`~asyncio.loop.create_server`.
+
+ For example, you can set the ``ssl`` keyword argument to a
+ :class:`~ssl.SSLContext` to enable TLS.
+
+ The ``create_protocol`` parameter allows customizing the
+ :class:`~asyncio.Protocol` that manages the connection. It should be a
+ callable or class accepting the same arguments as
+ :class:`WebSocketServerProtocol` and returning an instance of
+ :class:`WebSocketServerProtocol` or a subclass. It defaults to
+ :class:`WebSocketServerProtocol`.
+
+ The behavior of ``ping_interval``, ``ping_timeout``, ``close_timeout``,
+ ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit`` is
+ described in :class:`~websockets.protocol.WebSocketCommonProtocol`.
+
+ :func:`serve` also accepts the following optional arguments:
+
+ * ``compression`` is a shortcut to configure compression extensions;
+ by default it enables the "permessage-deflate" extension; set it to
+ ``None`` to disable compression
+ * ``origins`` defines acceptable Origin HTTP headers; include ``None`` if
+ the lack of an origin is acceptable
+ * ``extensions`` is a list of supported extensions in order of
+ decreasing preference
+ * ``subprotocols`` is a list of supported subprotocols in order of
+ decreasing preference
+ * ``extra_headers`` sets additional HTTP response headers when the
+ handshake succeeds; it can be a :class:`~websockets.http.Headers`
+ instance, a :class:`~collections.abc.Mapping`, an iterable of ``(name,
+ value)`` pairs, or a callable taking the request path and headers in
+ arguments and returning one of the above
+ * ``process_request`` allows intercepting the HTTP request; it must be a
+ coroutine taking the request path and headers in argument; see
+ :meth:`~WebSocketServerProtocol.process_request` for details
+ * ``select_subprotocol`` allows customizing the logic for selecting a
+ subprotocol; it must be a callable taking the subprotocols offered by
+ the client and available on the server in argument; see
+ :meth:`~WebSocketServerProtocol.select_subprotocol` for details
+
+ Since there's no useful way to propagate exceptions triggered in handlers,
+ they're sent to the ``'websockets.server'`` logger instead. Debugging is
+ much easier if you configure logging to print them::
+
+ import logging
+ logger = logging.getLogger('websockets.server')
+ logger.setLevel(logging.ERROR)
+ logger.addHandler(logging.StreamHandler())
+
+ """
+
+ def __init__(
+ self,
+ ws_handler: Callable[[WebSocketServerProtocol, str], Awaitable[Any]],
+ host: Optional[Union[str, Sequence[str]]] = None,
+ port: Optional[int] = None,
+ *,
+ path: Optional[str] = None,
+ create_protocol: Optional[Type[WebSocketServerProtocol]] = None,
+ ping_interval: float = 20,
+ ping_timeout: float = 20,
+ close_timeout: Optional[float] = None,
+ max_size: int = 2 ** 20,
+ max_queue: int = 2 ** 5,
+ read_limit: int = 2 ** 16,
+ write_limit: int = 2 ** 16,
+ loop: Optional[asyncio.AbstractEventLoop] = None,
+ legacy_recv: bool = False,
+ klass: Optional[Type[WebSocketServerProtocol]] = None,
+ timeout: Optional[float] = None,
+ compression: Optional[str] = "deflate",
+ origins: Optional[Sequence[Optional[Origin]]] = None,
+ extensions: Optional[Sequence[ServerExtensionFactory]] = None,
+ subprotocols: Optional[Sequence[Subprotocol]] = None,
+ extra_headers: Optional[HeadersLikeOrCallable] = None,
+ process_request: Optional[
+ Callable[[str, Headers], Awaitable[Optional[HTTPResponse]]]
+ ] = None,
+ select_subprotocol: Optional[
+ Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol]
+ ] = None,
+ **kwargs: Any,
+ ) -> None:
+ # Backwards compatibility: close_timeout used to be called timeout.
+ if timeout is None:
+ timeout = 10
+ else:
+ warnings.warn("rename timeout to close_timeout", DeprecationWarning)
+ # If both are specified, timeout is ignored.
+ if close_timeout is None:
+ close_timeout = timeout
+
+ # Backwards compatibility: create_protocol used to be called klass.
+ if klass is None:
+ klass = WebSocketServerProtocol
+ else:
+ warnings.warn("rename klass to create_protocol", DeprecationWarning)
+ # If both are specified, klass is ignored.
+ if create_protocol is None:
+ create_protocol = klass
+
+ if loop is None:
+ loop = asyncio.get_event_loop()
+
+ ws_server = WebSocketServer(loop)
+
+ secure = kwargs.get("ssl") is not None
+
+ if compression == "deflate":
+ if extensions is None:
+ extensions = []
+ if not any(
+ ext_factory.name == ServerPerMessageDeflateFactory.name
+ for ext_factory in extensions
+ ):
+ extensions = list(extensions) + [ServerPerMessageDeflateFactory()]
+ elif compression is not None:
+ raise ValueError(f"unsupported compression: {compression}")
+
+ factory = functools.partial(
+ create_protocol,
+ ws_handler,
+ ws_server,
+ host=host,
+ port=port,
+ secure=secure,
+ ping_interval=ping_interval,
+ ping_timeout=ping_timeout,
+ close_timeout=close_timeout,
+ max_size=max_size,
+ max_queue=max_queue,
+ read_limit=read_limit,
+ write_limit=write_limit,
+ loop=loop,
+ legacy_recv=legacy_recv,
+ origins=origins,
+ extensions=extensions,
+ subprotocols=subprotocols,
+ extra_headers=extra_headers,
+ process_request=process_request,
+ select_subprotocol=select_subprotocol,
+ )
+
+ if path is None:
+ create_server = functools.partial(
+ loop.create_server, factory, host, port, **kwargs
+ )
+ else:
+ # unix_serve(path) must not specify host and port parameters.
+ assert host is None and port is None
+ create_server = functools.partial(
+ loop.create_unix_server, factory, path, **kwargs
+ )
+
+ # This is a coroutine function.
+ self._create_server = create_server
+ self.ws_server = ws_server
+
+ # async with serve(...)
+
+ async def __aenter__(self) -> WebSocketServer:
+ return await self
+
+ async def __aexit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_value: Optional[BaseException],
+ traceback: Optional[TracebackType],
+ ) -> None:
+ self.ws_server.close()
+ await self.ws_server.wait_closed()
+
+ # await serve(...)
+
+ def __await__(self) -> Generator[Any, None, WebSocketServer]:
+ # Create a suitable iterator by calling __await__ on a coroutine.
+ return self.__await_impl__().__await__()
+
+ async def __await_impl__(self) -> WebSocketServer:
+ server = await self._create_server()
+ self.ws_server.wrap(server)
+ return self.ws_server
+
+ # yield from serve(...)
+
+ __iter__ = __await__
+
+
+serve = Serve
+
+
+def unix_serve(
+ ws_handler: Callable[[WebSocketServerProtocol, str], Awaitable[Any]],
+ path: str,
+ **kwargs: Any,
+) -> Serve:
+ """
+ Similar to :func:`serve`, but for listening on Unix sockets.
+
+ This function calls the event loop's
+ :meth:`~asyncio.loop.create_unix_server` method.
+
+ It is only available on Unix.
+
+ It's useful for deploying a server behind a reverse proxy such as nginx.
+
+ :param path: file system path to the Unix socket
+
+ """
+ return serve(ws_handler, path=path, **kwargs)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.c b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.c
new file mode 100644
index 0000000000..d1c2b37e60
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.c
@@ -0,0 +1,206 @@
+/* C implementation of performance sensitive functions. */
+
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+#include <stdint.h> /* uint32_t, uint64_t */
+
+#if __SSE2__
+#include <emmintrin.h>
+#endif
+
+static const Py_ssize_t MASK_LEN = 4;
+
+/* Similar to PyBytes_AsStringAndSize, but accepts more types */
+
+static int
+_PyBytesLike_AsStringAndSize(PyObject *obj, char **buffer, Py_ssize_t *length)
+{
+ // This supports bytes, bytearrays, and C-contiguous memoryview objects,
+ // which are the most useful data structures for handling byte streams.
+ // websockets.framing.prepare_data() returns only values of these types.
+ // Any object implementing the buffer protocol could be supported, however
+ // that would require allocation or copying memory, which is expensive.
+ if (PyBytes_Check(obj))
+ {
+ *buffer = PyBytes_AS_STRING(obj);
+ *length = PyBytes_GET_SIZE(obj);
+ }
+ else if (PyByteArray_Check(obj))
+ {
+ *buffer = PyByteArray_AS_STRING(obj);
+ *length = PyByteArray_GET_SIZE(obj);
+ }
+ else if (PyMemoryView_Check(obj))
+ {
+ Py_buffer *mv_buf;
+ mv_buf = PyMemoryView_GET_BUFFER(obj);
+ if (PyBuffer_IsContiguous(mv_buf, 'C'))
+ {
+ *buffer = mv_buf->buf;
+ *length = mv_buf->len;
+ }
+ else
+ {
+ PyErr_Format(
+ PyExc_TypeError,
+ "expected a contiguous memoryview");
+ return -1;
+ }
+ }
+ else
+ {
+ PyErr_Format(
+ PyExc_TypeError,
+ "expected a bytes-like object, %.200s found",
+ Py_TYPE(obj)->tp_name);
+ return -1;
+ }
+
+ return 0;
+}
+
+/* C implementation of websockets.utils.apply_mask */
+
+static PyObject *
+apply_mask(PyObject *self, PyObject *args, PyObject *kwds)
+{
+
+ // In order to support various bytes-like types, accept any Python object.
+
+ static char *kwlist[] = {"data", "mask", NULL};
+ PyObject *input_obj;
+ PyObject *mask_obj;
+
+ // A pointer to a char * + length will be extracted from the data and mask
+ // arguments, possibly via a Py_buffer.
+
+ char *input;
+ Py_ssize_t input_len;
+ char *mask;
+ Py_ssize_t mask_len;
+
+ // Initialize a PyBytesObject then get a pointer to the underlying char *
+ // in order to avoid an extra memory copy in PyBytes_FromStringAndSize.
+
+ PyObject *result;
+ char *output;
+
+ // Other variables.
+
+ Py_ssize_t i = 0;
+
+ // Parse inputs.
+
+ if (!PyArg_ParseTupleAndKeywords(
+ args, kwds, "OO", kwlist, &input_obj, &mask_obj))
+ {
+ return NULL;
+ }
+
+ if (_PyBytesLike_AsStringAndSize(input_obj, &input, &input_len) == -1)
+ {
+ return NULL;
+ }
+
+ if (_PyBytesLike_AsStringAndSize(mask_obj, &mask, &mask_len) == -1)
+ {
+ return NULL;
+ }
+
+ if (mask_len != MASK_LEN)
+ {
+ PyErr_SetString(PyExc_ValueError, "mask must contain 4 bytes");
+ return NULL;
+ }
+
+ // Create output.
+
+ result = PyBytes_FromStringAndSize(NULL, input_len);
+ if (result == NULL)
+ {
+ return NULL;
+ }
+
+ // Since we juste created result, we don't need error checks.
+ output = PyBytes_AS_STRING(result);
+
+ // Perform the masking operation.
+
+ // Apparently GCC cannot figure out the following optimizations by itself.
+
+ // We need a new scope for MSVC 2010 (non C99 friendly)
+ {
+#if __SSE2__
+
+ // With SSE2 support, XOR by blocks of 16 bytes = 128 bits.
+
+ // Since we cannot control the 16-bytes alignment of input and output
+ // buffers, we rely on loadu/storeu rather than load/store.
+
+ Py_ssize_t input_len_128 = input_len & ~15;
+ __m128i mask_128 = _mm_set1_epi32(*(uint32_t *)mask);
+
+ for (; i < input_len_128; i += 16)
+ {
+ __m128i in_128 = _mm_loadu_si128((__m128i *)(input + i));
+ __m128i out_128 = _mm_xor_si128(in_128, mask_128);
+ _mm_storeu_si128((__m128i *)(output + i), out_128);
+ }
+
+#else
+
+ // Without SSE2 support, XOR by blocks of 8 bytes = 64 bits.
+
+ // We assume the memory allocator aligns everything on 8 bytes boundaries.
+
+ Py_ssize_t input_len_64 = input_len & ~7;
+ uint32_t mask_32 = *(uint32_t *)mask;
+ uint64_t mask_64 = ((uint64_t)mask_32 << 32) | (uint64_t)mask_32;
+
+ for (; i < input_len_64; i += 8)
+ {
+ *(uint64_t *)(output + i) = *(uint64_t *)(input + i) ^ mask_64;
+ }
+
+#endif
+ }
+
+ // XOR the remainder of the input byte by byte.
+
+ for (; i < input_len; i++)
+ {
+ output[i] = input[i] ^ mask[i & (MASK_LEN - 1)];
+ }
+
+ return result;
+
+}
+
+static PyMethodDef speedups_methods[] = {
+ {
+ "apply_mask",
+ (PyCFunction)apply_mask,
+ METH_VARARGS | METH_KEYWORDS,
+ "Apply masking to websocket message.",
+ },
+ {NULL, NULL, 0, NULL}, /* Sentinel */
+};
+
+static struct PyModuleDef speedups_module = {
+ PyModuleDef_HEAD_INIT,
+ "websocket.speedups", /* m_name */
+ "C implementation of performance sensitive functions.",
+ /* m_doc */
+ -1, /* m_size */
+ speedups_methods, /* m_methods */
+ NULL,
+ NULL,
+ NULL,
+ NULL
+};
+
+PyMODINIT_FUNC
+PyInit_speedups(void)
+{
+ return PyModule_Create(&speedups_module);
+}
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi
new file mode 100644
index 0000000000..821438a064
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi
@@ -0,0 +1 @@
+def apply_mask(data: bytes, mask: bytes) -> bytes: ...
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py
new file mode 100644
index 0000000000..4a60f93f64
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py
@@ -0,0 +1,49 @@
+from typing import List, NewType, Optional, Tuple, Union
+
+
+__all__ = ["Data", "Origin", "ExtensionHeader", "ExtensionParameter", "Subprotocol"]
+
+Data = Union[str, bytes]
+
+Data__doc__ = """
+Types supported in a WebSocket message:
+
+- :class:`str` for text messages
+- :class:`bytes` for binary messages
+
+"""
+# Remove try / except when dropping support for Python < 3.7
+try:
+ Data.__doc__ = Data__doc__ # type: ignore
+except AttributeError: # pragma: no cover
+ pass
+
+
+Origin = NewType("Origin", str)
+Origin.__doc__ = """Value of a Origin header"""
+
+
+ExtensionName = NewType("ExtensionName", str)
+ExtensionName.__doc__ = """Name of a WebSocket extension"""
+
+
+ExtensionParameter = Tuple[str, Optional[str]]
+
+ExtensionParameter__doc__ = """Parameter of a WebSocket extension"""
+try:
+ ExtensionParameter.__doc__ = ExtensionParameter__doc__ # type: ignore
+except AttributeError: # pragma: no cover
+ pass
+
+
+ExtensionHeader = Tuple[ExtensionName, List[ExtensionParameter]]
+
+ExtensionHeader__doc__ = """Item parsed in a Sec-WebSocket-Extensions header"""
+try:
+ ExtensionHeader.__doc__ = ExtensionHeader__doc__ # type: ignore
+except AttributeError: # pragma: no cover
+ pass
+
+
+Subprotocol = NewType("Subprotocol", str)
+Subprotocol.__doc__ = """Items parsed in a Sec-WebSocket-Protocol header"""
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py
new file mode 100644
index 0000000000..6669e56686
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py
@@ -0,0 +1,81 @@
+"""
+:mod:`websockets.uri` parses WebSocket URIs.
+
+See `section 3 of RFC 6455`_.
+
+.. _section 3 of RFC 6455: http://tools.ietf.org/html/rfc6455#section-3
+
+"""
+
+import urllib.parse
+from typing import NamedTuple, Optional, Tuple
+
+from .exceptions import InvalidURI
+
+
+__all__ = ["parse_uri", "WebSocketURI"]
+
+
+# Consider converting to a dataclass when dropping support for Python < 3.7.
+
+
+class WebSocketURI(NamedTuple):
+ """
+ WebSocket URI.
+
+ :param bool secure: secure flag
+ :param str host: lower-case host
+ :param int port: port, always set even if it's the default
+ :param str resource_name: path and optional query
+ :param str user_info: ``(username, password)`` tuple when the URI contains
+ `User Information`_, else ``None``.
+
+ .. _User Information: https://tools.ietf.org/html/rfc3986#section-3.2.1
+ """
+
+ secure: bool
+ host: str
+ port: int
+ resource_name: str
+ user_info: Optional[Tuple[str, str]]
+
+
+# Work around https://bugs.python.org/issue19931
+
+WebSocketURI.secure.__doc__ = ""
+WebSocketURI.host.__doc__ = ""
+WebSocketURI.port.__doc__ = ""
+WebSocketURI.resource_name.__doc__ = ""
+WebSocketURI.user_info.__doc__ = ""
+
+
+def parse_uri(uri: str) -> WebSocketURI:
+ """
+ Parse and validate a WebSocket URI.
+
+ :raises ValueError: if ``uri`` isn't a valid WebSocket URI.
+
+ """
+ parsed = urllib.parse.urlparse(uri)
+ try:
+ assert parsed.scheme in ["ws", "wss"]
+ assert parsed.params == ""
+ assert parsed.fragment == ""
+ assert parsed.hostname is not None
+ except AssertionError as exc:
+ raise InvalidURI(uri) from exc
+
+ secure = parsed.scheme == "wss"
+ host = parsed.hostname
+ port = parsed.port or (443 if secure else 80)
+ resource_name = parsed.path or "/"
+ if parsed.query:
+ resource_name += "?" + parsed.query
+ user_info = None
+ if parsed.username is not None:
+ # urllib.parse.urlparse accepts URLs with a username but without a
+ # password. This doesn't make sense for HTTP Basic Auth credentials.
+ if parsed.password is None:
+ raise InvalidURI(uri)
+ user_info = (parsed.username, parsed.password)
+ return WebSocketURI(secure, host, port, resource_name, user_info)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/utils.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/utils.py
new file mode 100644
index 0000000000..40ac8559ff
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/utils.py
@@ -0,0 +1,18 @@
+import itertools
+
+
+__all__ = ["apply_mask"]
+
+
+def apply_mask(data: bytes, mask: bytes) -> bytes:
+ """
+ Apply masking to the data of a WebSocket message.
+
+ :param data: Data to mask
+ :param mask: 4-bytes mask
+
+ """
+ if len(mask) != 4:
+ raise ValueError("mask must contain 4 bytes")
+
+ return bytes(b ^ m for b, m in zip(data, itertools.cycle(mask)))
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py
new file mode 100644
index 0000000000..7377332e12
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py
@@ -0,0 +1 @@
+version = "8.1"
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/tests/__init__.py
new file mode 100644
index 0000000000..dd78609f5b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/__init__.py
@@ -0,0 +1,5 @@
+import logging
+
+
+# Avoid displaying stack traces at the ERROR logging level.
+logging.basicConfig(level=logging.CRITICAL)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_base.py b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_base.py
new file mode 100644
index 0000000000..ba8657b654
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_base.py
@@ -0,0 +1,4 @@
+from websockets.extensions.base import * # noqa
+
+
+# Abstract classes don't provide any behavior to test.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py
new file mode 100644
index 0000000000..0ec49c6c02
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py
@@ -0,0 +1,792 @@
+import unittest
+import zlib
+
+from websockets.exceptions import (
+ DuplicateParameter,
+ InvalidParameterName,
+ InvalidParameterValue,
+ NegotiationError,
+ PayloadTooBig,
+)
+from websockets.extensions.permessage_deflate import *
+from websockets.framing import (
+ OP_BINARY,
+ OP_CLOSE,
+ OP_CONT,
+ OP_PING,
+ OP_PONG,
+ OP_TEXT,
+ Frame,
+ serialize_close,
+)
+
+
+class ExtensionTestsMixin:
+ def assertExtensionEqual(self, extension1, extension2):
+ self.assertEqual(
+ extension1.remote_no_context_takeover, extension2.remote_no_context_takeover
+ )
+ self.assertEqual(
+ extension1.local_no_context_takeover, extension2.local_no_context_takeover
+ )
+ self.assertEqual(
+ extension1.remote_max_window_bits, extension2.remote_max_window_bits
+ )
+ self.assertEqual(
+ extension1.local_max_window_bits, extension2.local_max_window_bits
+ )
+
+
+class PerMessageDeflateTests(unittest.TestCase, ExtensionTestsMixin):
+ def setUp(self):
+ # Set up an instance of the permessage-deflate extension with the most
+ # common settings. Since the extension is symmetrical, this instance
+ # may be used for testing both encoding and decoding.
+ self.extension = PerMessageDeflate(False, False, 15, 15)
+
+ def test_name(self):
+ assert self.extension.name == "permessage-deflate"
+
+ def test_repr(self):
+ self.assertExtensionEqual(eval(repr(self.extension)), self.extension)
+
+ # Control frames aren't encoded or decoded.
+
+ def test_no_encode_decode_ping_frame(self):
+ frame = Frame(True, OP_PING, b"")
+
+ self.assertEqual(self.extension.encode(frame), frame)
+
+ self.assertEqual(self.extension.decode(frame), frame)
+
+ def test_no_encode_decode_pong_frame(self):
+ frame = Frame(True, OP_PONG, b"")
+
+ self.assertEqual(self.extension.encode(frame), frame)
+
+ self.assertEqual(self.extension.decode(frame), frame)
+
+ def test_no_encode_decode_close_frame(self):
+ frame = Frame(True, OP_CLOSE, serialize_close(1000, ""))
+
+ self.assertEqual(self.extension.encode(frame), frame)
+
+ self.assertEqual(self.extension.decode(frame), frame)
+
+ # Data frames are encoded and decoded.
+
+ def test_encode_decode_text_frame(self):
+ frame = Frame(True, OP_TEXT, "café".encode("utf-8"))
+
+ enc_frame = self.extension.encode(frame)
+
+ self.assertEqual(enc_frame, frame._replace(rsv1=True, data=b"JNL;\xbc\x12\x00"))
+
+ dec_frame = self.extension.decode(enc_frame)
+
+ self.assertEqual(dec_frame, frame)
+
+ def test_encode_decode_binary_frame(self):
+ frame = Frame(True, OP_BINARY, b"tea")
+
+ enc_frame = self.extension.encode(frame)
+
+ self.assertEqual(enc_frame, frame._replace(rsv1=True, data=b"*IM\x04\x00"))
+
+ dec_frame = self.extension.decode(enc_frame)
+
+ self.assertEqual(dec_frame, frame)
+
+ def test_encode_decode_fragmented_text_frame(self):
+ frame1 = Frame(False, OP_TEXT, "café".encode("utf-8"))
+ frame2 = Frame(False, OP_CONT, " & ".encode("utf-8"))
+ frame3 = Frame(True, OP_CONT, "croissants".encode("utf-8"))
+
+ enc_frame1 = self.extension.encode(frame1)
+ enc_frame2 = self.extension.encode(frame2)
+ enc_frame3 = self.extension.encode(frame3)
+
+ self.assertEqual(
+ enc_frame1,
+ frame1._replace(rsv1=True, data=b"JNL;\xbc\x12\x00\x00\x00\xff\xff"),
+ )
+ self.assertEqual(
+ enc_frame2, frame2._replace(rsv1=True, data=b"RPS\x00\x00\x00\x00\xff\xff")
+ )
+ self.assertEqual(
+ enc_frame3, frame3._replace(rsv1=True, data=b"J.\xca\xcf,.N\xcc+)\x06\x00")
+ )
+
+ dec_frame1 = self.extension.decode(enc_frame1)
+ dec_frame2 = self.extension.decode(enc_frame2)
+ dec_frame3 = self.extension.decode(enc_frame3)
+
+ self.assertEqual(dec_frame1, frame1)
+ self.assertEqual(dec_frame2, frame2)
+ self.assertEqual(dec_frame3, frame3)
+
+ def test_encode_decode_fragmented_binary_frame(self):
+ frame1 = Frame(False, OP_TEXT, b"tea ")
+ frame2 = Frame(True, OP_CONT, b"time")
+
+ enc_frame1 = self.extension.encode(frame1)
+ enc_frame2 = self.extension.encode(frame2)
+
+ self.assertEqual(
+ enc_frame1, frame1._replace(rsv1=True, data=b"*IMT\x00\x00\x00\x00\xff\xff")
+ )
+ self.assertEqual(
+ enc_frame2, frame2._replace(rsv1=True, data=b"*\xc9\xccM\x05\x00")
+ )
+
+ dec_frame1 = self.extension.decode(enc_frame1)
+ dec_frame2 = self.extension.decode(enc_frame2)
+
+ self.assertEqual(dec_frame1, frame1)
+ self.assertEqual(dec_frame2, frame2)
+
+ def test_no_decode_text_frame(self):
+ frame = Frame(True, OP_TEXT, "café".encode("utf-8"))
+
+ # Try decoding a frame that wasn't encoded.
+ self.assertEqual(self.extension.decode(frame), frame)
+
+ def test_no_decode_binary_frame(self):
+ frame = Frame(True, OP_TEXT, b"tea")
+
+ # Try decoding a frame that wasn't encoded.
+ self.assertEqual(self.extension.decode(frame), frame)
+
+ def test_no_decode_fragmented_text_frame(self):
+ frame1 = Frame(False, OP_TEXT, "café".encode("utf-8"))
+ frame2 = Frame(False, OP_CONT, " & ".encode("utf-8"))
+ frame3 = Frame(True, OP_CONT, "croissants".encode("utf-8"))
+
+ dec_frame1 = self.extension.decode(frame1)
+ dec_frame2 = self.extension.decode(frame2)
+ dec_frame3 = self.extension.decode(frame3)
+
+ self.assertEqual(dec_frame1, frame1)
+ self.assertEqual(dec_frame2, frame2)
+ self.assertEqual(dec_frame3, frame3)
+
+ def test_no_decode_fragmented_binary_frame(self):
+ frame1 = Frame(False, OP_TEXT, b"tea ")
+ frame2 = Frame(True, OP_CONT, b"time")
+
+ dec_frame1 = self.extension.decode(frame1)
+ dec_frame2 = self.extension.decode(frame2)
+
+ self.assertEqual(dec_frame1, frame1)
+ self.assertEqual(dec_frame2, frame2)
+
+ def test_context_takeover(self):
+ frame = Frame(True, OP_TEXT, "café".encode("utf-8"))
+
+ enc_frame1 = self.extension.encode(frame)
+ enc_frame2 = self.extension.encode(frame)
+
+ self.assertEqual(enc_frame1.data, b"JNL;\xbc\x12\x00")
+ self.assertEqual(enc_frame2.data, b"J\x06\x11\x00\x00")
+
+ def test_remote_no_context_takeover(self):
+ # No context takeover when decoding messages.
+ self.extension = PerMessageDeflate(True, False, 15, 15)
+
+ frame = Frame(True, OP_TEXT, "café".encode("utf-8"))
+
+ enc_frame1 = self.extension.encode(frame)
+ enc_frame2 = self.extension.encode(frame)
+
+ self.assertEqual(enc_frame1.data, b"JNL;\xbc\x12\x00")
+ self.assertEqual(enc_frame2.data, b"J\x06\x11\x00\x00")
+
+ dec_frame1 = self.extension.decode(enc_frame1)
+ self.assertEqual(dec_frame1, frame)
+
+ with self.assertRaises(zlib.error) as exc:
+ self.extension.decode(enc_frame2)
+ self.assertIn("invalid distance too far back", str(exc.exception))
+
+ def test_local_no_context_takeover(self):
+ # No context takeover when encoding and decoding messages.
+ self.extension = PerMessageDeflate(True, True, 15, 15)
+
+ frame = Frame(True, OP_TEXT, "café".encode("utf-8"))
+
+ enc_frame1 = self.extension.encode(frame)
+ enc_frame2 = self.extension.encode(frame)
+
+ self.assertEqual(enc_frame1.data, b"JNL;\xbc\x12\x00")
+ self.assertEqual(enc_frame2.data, b"JNL;\xbc\x12\x00")
+
+ dec_frame1 = self.extension.decode(enc_frame1)
+ dec_frame2 = self.extension.decode(enc_frame2)
+
+ self.assertEqual(dec_frame1, frame)
+ self.assertEqual(dec_frame2, frame)
+
+ # Compression settings can be customized.
+
+ def test_compress_settings(self):
+ # Configure an extension so that no compression actually occurs.
+ extension = PerMessageDeflate(False, False, 15, 15, {"level": 0})
+
+ frame = Frame(True, OP_TEXT, "café".encode("utf-8"))
+
+ enc_frame = extension.encode(frame)
+
+ self.assertEqual(
+ enc_frame,
+ frame._replace(
+ rsv1=True, data=b"\x00\x05\x00\xfa\xffcaf\xc3\xa9\x00" # not compressed
+ ),
+ )
+
+ # Frames aren't decoded beyond max_length.
+
+ def test_decompress_max_size(self):
+ frame = Frame(True, OP_TEXT, ("a" * 20).encode("utf-8"))
+
+ enc_frame = self.extension.encode(frame)
+
+ self.assertEqual(enc_frame.data, b"JL\xc4\x04\x00\x00")
+
+ with self.assertRaises(PayloadTooBig):
+ self.extension.decode(enc_frame, max_size=10)
+
+
+class ClientPerMessageDeflateFactoryTests(unittest.TestCase, ExtensionTestsMixin):
+ def test_name(self):
+ assert ClientPerMessageDeflateFactory.name == "permessage-deflate"
+
+ def test_init(self):
+ for config in [
+ (False, False, 8, None), # server_max_window_bits ≥ 8
+ (False, True, 15, None), # server_max_window_bits ≤ 15
+ (True, False, None, 8), # client_max_window_bits ≥ 8
+ (True, True, None, 15), # client_max_window_bits ≤ 15
+ (False, False, None, True), # client_max_window_bits
+ (False, False, None, None, {"memLevel": 4}),
+ ]:
+ with self.subTest(config=config):
+ # This does not raise an exception.
+ ClientPerMessageDeflateFactory(*config)
+
+ def test_init_error(self):
+ for config in [
+ (False, False, 7, 8), # server_max_window_bits < 8
+ (False, True, 8, 7), # client_max_window_bits < 8
+ (True, False, 16, 15), # server_max_window_bits > 15
+ (True, True, 15, 16), # client_max_window_bits > 15
+ (False, False, True, None), # server_max_window_bits
+ (False, False, None, None, {"wbits": 11}),
+ ]:
+ with self.subTest(config=config):
+ with self.assertRaises(ValueError):
+ ClientPerMessageDeflateFactory(*config)
+
+ def test_get_request_params(self):
+ for config, result in [
+ # Test without any parameter
+ ((False, False, None, None), []),
+ # Test server_no_context_takeover
+ ((True, False, None, None), [("server_no_context_takeover", None)]),
+ # Test client_no_context_takeover
+ ((False, True, None, None), [("client_no_context_takeover", None)]),
+ # Test server_max_window_bits
+ ((False, False, 10, None), [("server_max_window_bits", "10")]),
+ # Test client_max_window_bits
+ ((False, False, None, 10), [("client_max_window_bits", "10")]),
+ ((False, False, None, True), [("client_max_window_bits", None)]),
+ # Test all parameters together
+ (
+ (True, True, 12, 12),
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "12"),
+ ("client_max_window_bits", "12"),
+ ],
+ ),
+ ]:
+ with self.subTest(config=config):
+ factory = ClientPerMessageDeflateFactory(*config)
+ self.assertEqual(factory.get_request_params(), result)
+
+ def test_process_response_params(self):
+ for config, response_params, result in [
+ # Test without any parameter
+ ((False, False, None, None), [], (False, False, 15, 15)),
+ ((False, False, None, None), [("unknown", None)], InvalidParameterName),
+ # Test server_no_context_takeover
+ (
+ (False, False, None, None),
+ [("server_no_context_takeover", None)],
+ (True, False, 15, 15),
+ ),
+ ((True, False, None, None), [], NegotiationError),
+ (
+ (True, False, None, None),
+ [("server_no_context_takeover", None)],
+ (True, False, 15, 15),
+ ),
+ (
+ (True, False, None, None),
+ [("server_no_context_takeover", None)] * 2,
+ DuplicateParameter,
+ ),
+ (
+ (True, False, None, None),
+ [("server_no_context_takeover", "42")],
+ InvalidParameterValue,
+ ),
+ # Test client_no_context_takeover
+ (
+ (False, False, None, None),
+ [("client_no_context_takeover", None)],
+ (False, True, 15, 15),
+ ),
+ ((False, True, None, None), [], (False, True, 15, 15)),
+ (
+ (False, True, None, None),
+ [("client_no_context_takeover", None)],
+ (False, True, 15, 15),
+ ),
+ (
+ (False, True, None, None),
+ [("client_no_context_takeover", None)] * 2,
+ DuplicateParameter,
+ ),
+ (
+ (False, True, None, None),
+ [("client_no_context_takeover", "42")],
+ InvalidParameterValue,
+ ),
+ # Test server_max_window_bits
+ (
+ (False, False, None, None),
+ [("server_max_window_bits", "7")],
+ NegotiationError,
+ ),
+ (
+ (False, False, None, None),
+ [("server_max_window_bits", "10")],
+ (False, False, 10, 15),
+ ),
+ (
+ (False, False, None, None),
+ [("server_max_window_bits", "16")],
+ NegotiationError,
+ ),
+ ((False, False, 12, None), [], NegotiationError),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "10")],
+ (False, False, 10, 15),
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "12")],
+ (False, False, 12, 15),
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "13")],
+ NegotiationError,
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "12")] * 2,
+ DuplicateParameter,
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "42")],
+ InvalidParameterValue,
+ ),
+ # Test client_max_window_bits
+ (
+ (False, False, None, None),
+ [("client_max_window_bits", "10")],
+ NegotiationError,
+ ),
+ ((False, False, None, True), [], (False, False, 15, 15)),
+ (
+ (False, False, None, True),
+ [("client_max_window_bits", "7")],
+ NegotiationError,
+ ),
+ (
+ (False, False, None, True),
+ [("client_max_window_bits", "10")],
+ (False, False, 15, 10),
+ ),
+ (
+ (False, False, None, True),
+ [("client_max_window_bits", "16")],
+ NegotiationError,
+ ),
+ ((False, False, None, 12), [], (False, False, 15, 12)),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "10")],
+ (False, False, 15, 10),
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "12")],
+ (False, False, 15, 12),
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "13")],
+ NegotiationError,
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "12")] * 2,
+ DuplicateParameter,
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "42")],
+ InvalidParameterValue,
+ ),
+ # Test all parameters together
+ (
+ (True, True, 12, 12),
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "10"),
+ ("client_max_window_bits", "10"),
+ ],
+ (True, True, 10, 10),
+ ),
+ (
+ (False, False, None, True),
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "10"),
+ ("client_max_window_bits", "10"),
+ ],
+ (True, True, 10, 10),
+ ),
+ (
+ (True, True, 12, 12),
+ [
+ ("server_no_context_takeover", None),
+ ("server_max_window_bits", "12"),
+ ],
+ (True, True, 12, 12),
+ ),
+ ]:
+ with self.subTest(config=config, response_params=response_params):
+ factory = ClientPerMessageDeflateFactory(*config)
+ if isinstance(result, type) and issubclass(result, Exception):
+ with self.assertRaises(result):
+ factory.process_response_params(response_params, [])
+ else:
+ extension = factory.process_response_params(response_params, [])
+ expected = PerMessageDeflate(*result)
+ self.assertExtensionEqual(extension, expected)
+
+ def test_process_response_params_deduplication(self):
+ factory = ClientPerMessageDeflateFactory(False, False, None, None)
+ with self.assertRaises(NegotiationError):
+ factory.process_response_params(
+ [], [PerMessageDeflate(False, False, 15, 15)]
+ )
+
+
+class ServerPerMessageDeflateFactoryTests(unittest.TestCase, ExtensionTestsMixin):
+ def test_name(self):
+ assert ServerPerMessageDeflateFactory.name == "permessage-deflate"
+
+ def test_init(self):
+ for config in [
+ (False, False, 8, None), # server_max_window_bits ≥ 8
+ (False, True, 15, None), # server_max_window_bits ≤ 15
+ (True, False, None, 8), # client_max_window_bits ≥ 8
+ (True, True, None, 15), # client_max_window_bits ≤ 15
+ (False, False, None, None, {"memLevel": 4}),
+ ]:
+ with self.subTest(config=config):
+ # This does not raise an exception.
+ ServerPerMessageDeflateFactory(*config)
+
+ def test_init_error(self):
+ for config in [
+ (False, False, 7, 8), # server_max_window_bits < 8
+ (False, True, 8, 7), # client_max_window_bits < 8
+ (True, False, 16, 15), # server_max_window_bits > 15
+ (True, True, 15, 16), # client_max_window_bits > 15
+ (False, False, None, True), # client_max_window_bits
+ (False, False, True, None), # server_max_window_bits
+ (False, False, None, None, {"wbits": 11}),
+ ]:
+ with self.subTest(config=config):
+ with self.assertRaises(ValueError):
+ ServerPerMessageDeflateFactory(*config)
+
+ def test_process_request_params(self):
+ # Parameters in result appear swapped vs. config because the order is
+ # (remote, local) vs. (server, client).
+ for config, request_params, response_params, result in [
+ # Test without any parameter
+ ((False, False, None, None), [], [], (False, False, 15, 15)),
+ (
+ (False, False, None, None),
+ [("unknown", None)],
+ None,
+ InvalidParameterName,
+ ),
+ # Test server_no_context_takeover
+ (
+ (False, False, None, None),
+ [("server_no_context_takeover", None)],
+ [("server_no_context_takeover", None)],
+ (False, True, 15, 15),
+ ),
+ (
+ (True, False, None, None),
+ [],
+ [("server_no_context_takeover", None)],
+ (False, True, 15, 15),
+ ),
+ (
+ (True, False, None, None),
+ [("server_no_context_takeover", None)],
+ [("server_no_context_takeover", None)],
+ (False, True, 15, 15),
+ ),
+ (
+ (True, False, None, None),
+ [("server_no_context_takeover", None)] * 2,
+ None,
+ DuplicateParameter,
+ ),
+ (
+ (True, False, None, None),
+ [("server_no_context_takeover", "42")],
+ None,
+ InvalidParameterValue,
+ ),
+ # Test client_no_context_takeover
+ (
+ (False, False, None, None),
+ [("client_no_context_takeover", None)],
+ [("client_no_context_takeover", None)], # doesn't matter
+ (True, False, 15, 15),
+ ),
+ (
+ (False, True, None, None),
+ [],
+ [("client_no_context_takeover", None)],
+ (True, False, 15, 15),
+ ),
+ (
+ (False, True, None, None),
+ [("client_no_context_takeover", None)],
+ [("client_no_context_takeover", None)], # doesn't matter
+ (True, False, 15, 15),
+ ),
+ (
+ (False, True, None, None),
+ [("client_no_context_takeover", None)] * 2,
+ None,
+ DuplicateParameter,
+ ),
+ (
+ (False, True, None, None),
+ [("client_no_context_takeover", "42")],
+ None,
+ InvalidParameterValue,
+ ),
+ # Test server_max_window_bits
+ (
+ (False, False, None, None),
+ [("server_max_window_bits", "7")],
+ None,
+ NegotiationError,
+ ),
+ (
+ (False, False, None, None),
+ [("server_max_window_bits", "10")],
+ [("server_max_window_bits", "10")],
+ (False, False, 15, 10),
+ ),
+ (
+ (False, False, None, None),
+ [("server_max_window_bits", "16")],
+ None,
+ NegotiationError,
+ ),
+ (
+ (False, False, 12, None),
+ [],
+ [("server_max_window_bits", "12")],
+ (False, False, 15, 12),
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "10")],
+ [("server_max_window_bits", "10")],
+ (False, False, 15, 10),
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "12")],
+ [("server_max_window_bits", "12")],
+ (False, False, 15, 12),
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "13")],
+ [("server_max_window_bits", "12")],
+ (False, False, 15, 12),
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "12")] * 2,
+ None,
+ DuplicateParameter,
+ ),
+ (
+ (False, False, 12, None),
+ [("server_max_window_bits", "42")],
+ None,
+ InvalidParameterValue,
+ ),
+ # Test client_max_window_bits
+ (
+ (False, False, None, None),
+ [("client_max_window_bits", None)],
+ [],
+ (False, False, 15, 15),
+ ),
+ (
+ (False, False, None, None),
+ [("client_max_window_bits", "7")],
+ None,
+ InvalidParameterValue,
+ ),
+ (
+ (False, False, None, None),
+ [("client_max_window_bits", "10")],
+ [("client_max_window_bits", "10")], # doesn't matter
+ (False, False, 10, 15),
+ ),
+ (
+ (False, False, None, None),
+ [("client_max_window_bits", "16")],
+ None,
+ InvalidParameterValue,
+ ),
+ ((False, False, None, 12), [], None, NegotiationError),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", None)],
+ [("client_max_window_bits", "12")],
+ (False, False, 12, 15),
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "10")],
+ [("client_max_window_bits", "10")],
+ (False, False, 10, 15),
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "12")],
+ [("client_max_window_bits", "12")], # doesn't matter
+ (False, False, 12, 15),
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "13")],
+ [("client_max_window_bits", "12")], # doesn't matter
+ (False, False, 12, 15),
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "12")] * 2,
+ None,
+ DuplicateParameter,
+ ),
+ (
+ (False, False, None, 12),
+ [("client_max_window_bits", "42")],
+ None,
+ InvalidParameterValue,
+ ),
+ # # Test all parameters together
+ (
+ (True, True, 12, 12),
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "10"),
+ ("client_max_window_bits", "10"),
+ ],
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "10"),
+ ("client_max_window_bits", "10"),
+ ],
+ (True, True, 10, 10),
+ ),
+ (
+ (False, False, None, None),
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "10"),
+ ("client_max_window_bits", "10"),
+ ],
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "10"),
+ ("client_max_window_bits", "10"),
+ ],
+ (True, True, 10, 10),
+ ),
+ (
+ (True, True, 12, 12),
+ [("client_max_window_bits", None)],
+ [
+ ("server_no_context_takeover", None),
+ ("client_no_context_takeover", None),
+ ("server_max_window_bits", "12"),
+ ("client_max_window_bits", "12"),
+ ],
+ (True, True, 12, 12),
+ ),
+ ]:
+ with self.subTest(
+ config=config,
+ request_params=request_params,
+ response_params=response_params,
+ ):
+ factory = ServerPerMessageDeflateFactory(*config)
+ if isinstance(result, type) and issubclass(result, Exception):
+ with self.assertRaises(result):
+ factory.process_request_params(request_params, [])
+ else:
+ params, extension = factory.process_request_params(
+ request_params, []
+ )
+ self.assertEqual(params, response_params)
+ expected = PerMessageDeflate(*result)
+ self.assertExtensionEqual(extension, expected)
+
+ def test_process_response_params_deduplication(self):
+ factory = ServerPerMessageDeflateFactory(False, False, None, None)
+ with self.assertRaises(NegotiationError):
+ factory.process_request_params(
+ [], [PerMessageDeflate(False, False, 15, 15)]
+ )
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_auth.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_auth.py
new file mode 100644
index 0000000000..97a4485a0f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_auth.py
@@ -0,0 +1,139 @@
+import unittest
+import urllib.error
+
+from websockets.auth import *
+from websockets.auth import is_credentials
+from websockets.exceptions import InvalidStatusCode
+from websockets.headers import build_authorization_basic
+
+from .test_client_server import ClientServerTestsMixin, with_client, with_server
+from .utils import AsyncioTestCase
+
+
+class AuthTests(unittest.TestCase):
+ def test_is_credentials(self):
+ self.assertTrue(is_credentials(("username", "password")))
+
+ def test_is_not_credentials(self):
+ self.assertFalse(is_credentials(None))
+ self.assertFalse(is_credentials("username"))
+
+
+class AuthClientServerTests(ClientServerTestsMixin, AsyncioTestCase):
+
+ create_protocol = basic_auth_protocol_factory(
+ realm="auth-tests", credentials=("hello", "iloveyou")
+ )
+
+ @with_server(create_protocol=create_protocol)
+ @with_client(user_info=("hello", "iloveyou"))
+ def test_basic_auth(self):
+ req_headers = self.client.request_headers
+ resp_headers = self.client.response_headers
+ self.assertEqual(req_headers["Authorization"], "Basic aGVsbG86aWxvdmV5b3U=")
+ self.assertNotIn("WWW-Authenticate", resp_headers)
+
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.loop.run_until_complete(self.client.recv())
+
+ def test_basic_auth_server_no_credentials(self):
+ with self.assertRaises(TypeError) as raised:
+ basic_auth_protocol_factory(realm="auth-tests", credentials=None)
+ self.assertEqual(
+ str(raised.exception), "provide either credentials or check_credentials"
+ )
+
+ def test_basic_auth_server_bad_credentials(self):
+ with self.assertRaises(TypeError) as raised:
+ basic_auth_protocol_factory(realm="auth-tests", credentials=42)
+ self.assertEqual(str(raised.exception), "invalid credentials argument: 42")
+
+ create_protocol_multiple_credentials = basic_auth_protocol_factory(
+ realm="auth-tests",
+ credentials=[("hello", "iloveyou"), ("goodbye", "stillloveu")],
+ )
+
+ @with_server(create_protocol=create_protocol_multiple_credentials)
+ @with_client(user_info=("hello", "iloveyou"))
+ def test_basic_auth_server_multiple_credentials(self):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.loop.run_until_complete(self.client.recv())
+
+ def test_basic_auth_bad_multiple_credentials(self):
+ with self.assertRaises(TypeError) as raised:
+ basic_auth_protocol_factory(
+ realm="auth-tests", credentials=[("hello", "iloveyou"), 42]
+ )
+ self.assertEqual(
+ str(raised.exception),
+ "invalid credentials argument: [('hello', 'iloveyou'), 42]",
+ )
+
+ async def check_credentials(username, password):
+ return password == "iloveyou"
+
+ create_protocol_check_credentials = basic_auth_protocol_factory(
+ realm="auth-tests", check_credentials=check_credentials
+ )
+
+ @with_server(create_protocol=create_protocol_check_credentials)
+ @with_client(user_info=("hello", "iloveyou"))
+ def test_basic_auth_check_credentials(self):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.loop.run_until_complete(self.client.recv())
+
+ @with_server(create_protocol=create_protocol)
+ def test_basic_auth_missing_credentials(self):
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client()
+ self.assertEqual(raised.exception.status_code, 401)
+
+ @with_server(create_protocol=create_protocol)
+ def test_basic_auth_missing_credentials_details(self):
+ with self.assertRaises(urllib.error.HTTPError) as raised:
+ self.loop.run_until_complete(self.make_http_request())
+ self.assertEqual(raised.exception.code, 401)
+ self.assertEqual(
+ raised.exception.headers["WWW-Authenticate"],
+ 'Basic realm="auth-tests", charset="UTF-8"',
+ )
+ self.assertEqual(raised.exception.read().decode(), "Missing credentials\n")
+
+ @with_server(create_protocol=create_protocol)
+ def test_basic_auth_unsupported_credentials(self):
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client(extra_headers={"Authorization": "Digest ..."})
+ self.assertEqual(raised.exception.status_code, 401)
+
+ @with_server(create_protocol=create_protocol)
+ def test_basic_auth_unsupported_credentials_details(self):
+ with self.assertRaises(urllib.error.HTTPError) as raised:
+ self.loop.run_until_complete(
+ self.make_http_request(headers={"Authorization": "Digest ..."})
+ )
+ self.assertEqual(raised.exception.code, 401)
+ self.assertEqual(
+ raised.exception.headers["WWW-Authenticate"],
+ 'Basic realm="auth-tests", charset="UTF-8"',
+ )
+ self.assertEqual(raised.exception.read().decode(), "Unsupported credentials\n")
+
+ @with_server(create_protocol=create_protocol)
+ def test_basic_auth_invalid_credentials(self):
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client(user_info=("hello", "ihateyou"))
+ self.assertEqual(raised.exception.status_code, 401)
+
+ @with_server(create_protocol=create_protocol)
+ def test_basic_auth_invalid_credentials_details(self):
+ with self.assertRaises(urllib.error.HTTPError) as raised:
+ authorization = build_authorization_basic("hello", "ihateyou")
+ self.loop.run_until_complete(
+ self.make_http_request(headers={"Authorization": authorization})
+ )
+ self.assertEqual(raised.exception.code, 401)
+ self.assertEqual(
+ raised.exception.headers["WWW-Authenticate"],
+ 'Basic realm="auth-tests", charset="UTF-8"',
+ )
+ self.assertEqual(raised.exception.read().decode(), "Invalid credentials\n")
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_client_server.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_client_server.py
new file mode 100644
index 0000000000..35913666c5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_client_server.py
@@ -0,0 +1,1546 @@
+import asyncio
+import contextlib
+import functools
+import http
+import pathlib
+import random
+import socket
+import ssl
+import tempfile
+import unittest
+import unittest.mock
+import urllib.error
+import urllib.request
+import warnings
+
+from websockets.client import *
+from websockets.exceptions import (
+ ConnectionClosed,
+ InvalidHandshake,
+ InvalidHeader,
+ InvalidStatusCode,
+ NegotiationError,
+)
+from websockets.extensions.permessage_deflate import (
+ ClientPerMessageDeflateFactory,
+ PerMessageDeflate,
+ ServerPerMessageDeflateFactory,
+)
+from websockets.handshake import build_response
+from websockets.http import USER_AGENT, Headers, read_response
+from websockets.protocol import State
+from websockets.server import *
+from websockets.uri import parse_uri
+
+from .test_protocol import MS
+from .utils import AsyncioTestCase
+
+
+# Generate TLS certificate with:
+# $ openssl req -x509 -config test_localhost.cnf -days 15340 -newkey rsa:2048 \
+# -out test_localhost.crt -keyout test_localhost.key
+# $ cat test_localhost.key test_localhost.crt > test_localhost.pem
+# $ rm test_localhost.key test_localhost.crt
+
+testcert = bytes(pathlib.Path(__file__).with_name("test_localhost.pem"))
+
+
+async def handler(ws, path):
+ if path == "/deprecated_attributes":
+ await ws.recv() # delay that allows catching warnings
+ await ws.send(repr((ws.host, ws.port, ws.secure)))
+ elif path == "/close_timeout":
+ await ws.send(repr(ws.close_timeout))
+ elif path == "/path":
+ await ws.send(str(ws.path))
+ elif path == "/headers":
+ await ws.send(repr(ws.request_headers))
+ await ws.send(repr(ws.response_headers))
+ elif path == "/extensions":
+ await ws.send(repr(ws.extensions))
+ elif path == "/subprotocol":
+ await ws.send(repr(ws.subprotocol))
+ elif path == "/slow_stop":
+ await ws.wait_closed()
+ await asyncio.sleep(2 * MS)
+ else:
+ await ws.send((await ws.recv()))
+
+
+@contextlib.contextmanager
+def temp_test_server(test, **kwargs):
+ test.start_server(**kwargs)
+ try:
+ yield
+ finally:
+ test.stop_server()
+
+
+@contextlib.contextmanager
+def temp_test_redirecting_server(
+ test, status, include_location=True, force_insecure=False
+):
+ test.start_redirecting_server(status, include_location, force_insecure)
+ try:
+ yield
+ finally:
+ test.stop_redirecting_server()
+
+
+@contextlib.contextmanager
+def temp_test_client(test, *args, **kwargs):
+ test.start_client(*args, **kwargs)
+ try:
+ yield
+ finally:
+ test.stop_client()
+
+
+def with_manager(manager, *args, **kwargs):
+ """
+ Return a decorator that wraps a function with a context manager.
+
+ """
+
+ def decorate(func):
+ @functools.wraps(func)
+ def _decorate(self, *_args, **_kwargs):
+ with manager(self, *args, **kwargs):
+ return func(self, *_args, **_kwargs)
+
+ return _decorate
+
+ return decorate
+
+
+def with_server(**kwargs):
+ """
+ Return a decorator for TestCase methods that starts and stops a server.
+
+ """
+ return with_manager(temp_test_server, **kwargs)
+
+
+def with_client(*args, **kwargs):
+ """
+ Return a decorator for TestCase methods that starts and stops a client.
+
+ """
+ return with_manager(temp_test_client, *args, **kwargs)
+
+
+def get_server_uri(server, secure=False, resource_name="/", user_info=None):
+ """
+ Return a WebSocket URI for connecting to the given server.
+
+ """
+ proto = "wss" if secure else "ws"
+
+ user_info = ":".join(user_info) + "@" if user_info else ""
+
+ # Pick a random socket in order to test both IPv4 and IPv6 on systems
+ # where both are available. Randomizing tests is usually a bad idea. If
+ # needed, either use the first socket, or test separately IPv4 and IPv6.
+ server_socket = random.choice(server.sockets)
+
+ if server_socket.family == socket.AF_INET6: # pragma: no cover
+ host, port = server_socket.getsockname()[:2] # (no IPv6 on CI)
+ host = f"[{host}]"
+ elif server_socket.family == socket.AF_INET:
+ host, port = server_socket.getsockname()
+ else: # pragma: no cover
+ raise ValueError("expected an IPv6, IPv4, or Unix socket")
+
+ return f"{proto}://{user_info}{host}:{port}{resource_name}"
+
+
+class UnauthorizedServerProtocol(WebSocketServerProtocol):
+ async def process_request(self, path, request_headers):
+ # Test returning headers as a Headers instance (1/3)
+ return http.HTTPStatus.UNAUTHORIZED, Headers([("X-Access", "denied")]), b""
+
+
+class ForbiddenServerProtocol(WebSocketServerProtocol):
+ async def process_request(self, path, request_headers):
+ # Test returning headers as a dict (2/3)
+ return http.HTTPStatus.FORBIDDEN, {"X-Access": "denied"}, b""
+
+
+class HealthCheckServerProtocol(WebSocketServerProtocol):
+ async def process_request(self, path, request_headers):
+ # Test returning headers as a list of pairs (3/3)
+ if path == "/__health__/":
+ return http.HTTPStatus.OK, [("X-Access", "OK")], b"status = green\n"
+
+
+class SlowOpeningHandshakeProtocol(WebSocketServerProtocol):
+ async def process_request(self, path, request_headers):
+ await asyncio.sleep(10 * MS)
+
+
+class FooClientProtocol(WebSocketClientProtocol):
+ pass
+
+
+class BarClientProtocol(WebSocketClientProtocol):
+ pass
+
+
+class ClientNoOpExtensionFactory:
+ name = "x-no-op"
+
+ def get_request_params(self):
+ return []
+
+ def process_response_params(self, params, accepted_extensions):
+ if params:
+ raise NegotiationError()
+ return NoOpExtension()
+
+
+class ServerNoOpExtensionFactory:
+ name = "x-no-op"
+
+ def __init__(self, params=None):
+ self.params = params or []
+
+ def process_request_params(self, params, accepted_extensions):
+ return self.params, NoOpExtension()
+
+
+class NoOpExtension:
+ name = "x-no-op"
+
+ def __repr__(self):
+ return "NoOpExtension()"
+
+ def decode(self, frame, *, max_size=None):
+ return frame
+
+ def encode(self, frame):
+ return frame
+
+
+class ClientServerTestsMixin:
+
+ secure = False
+
+ def setUp(self):
+ super().setUp()
+ self.server = None
+ self.redirecting_server = None
+
+ @property
+ def server_context(self):
+ return None
+
+ def start_server(self, deprecation_warnings=None, **kwargs):
+ # Disable compression by default in tests.
+ kwargs.setdefault("compression", None)
+ # Disable pings by default in tests.
+ kwargs.setdefault("ping_interval", None)
+
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ start_server = serve(handler, "localhost", 0, **kwargs)
+ self.server = self.loop.run_until_complete(start_server)
+
+ expected_warnings = [] if deprecation_warnings is None else deprecation_warnings
+ self.assertDeprecationWarnings(recorded_warnings, expected_warnings)
+
+ def start_redirecting_server(
+ self, status, include_location=True, force_insecure=False
+ ):
+ async def process_request(path, headers):
+ server_uri = get_server_uri(self.server, self.secure, path)
+ if force_insecure:
+ server_uri = server_uri.replace("wss:", "ws:")
+ headers = {"Location": server_uri} if include_location else []
+ return status, headers, b""
+
+ start_server = serve(
+ handler,
+ "localhost",
+ 0,
+ compression=None,
+ ping_interval=None,
+ process_request=process_request,
+ ssl=self.server_context,
+ )
+ self.redirecting_server = self.loop.run_until_complete(start_server)
+
+ def start_client(
+ self, resource_name="/", user_info=None, deprecation_warnings=None, **kwargs
+ ):
+ # Disable compression by default in tests.
+ kwargs.setdefault("compression", None)
+ # Disable pings by default in tests.
+ kwargs.setdefault("ping_interval", None)
+ secure = kwargs.get("ssl") is not None
+ try:
+ server_uri = kwargs.pop("uri")
+ except KeyError:
+ server = self.redirecting_server if self.redirecting_server else self.server
+ server_uri = get_server_uri(server, secure, resource_name, user_info)
+
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ start_client = connect(server_uri, **kwargs)
+ self.client = self.loop.run_until_complete(start_client)
+
+ expected_warnings = [] if deprecation_warnings is None else deprecation_warnings
+ self.assertDeprecationWarnings(recorded_warnings, expected_warnings)
+
+ def stop_client(self):
+ try:
+ self.loop.run_until_complete(
+ asyncio.wait_for(self.client.close_connection_task, timeout=1)
+ )
+ except asyncio.TimeoutError: # pragma: no cover
+ self.fail("Client failed to stop")
+
+ def stop_server(self):
+ self.server.close()
+ try:
+ self.loop.run_until_complete(
+ asyncio.wait_for(self.server.wait_closed(), timeout=1)
+ )
+ except asyncio.TimeoutError: # pragma: no cover
+ self.fail("Server failed to stop")
+
+ def stop_redirecting_server(self):
+ self.redirecting_server.close()
+ try:
+ self.loop.run_until_complete(
+ asyncio.wait_for(self.redirecting_server.wait_closed(), timeout=1)
+ )
+ except asyncio.TimeoutError: # pragma: no cover
+ self.fail("Redirecting server failed to stop")
+ finally:
+ self.redirecting_server = None
+
+ @contextlib.contextmanager
+ def temp_server(self, **kwargs):
+ with temp_test_server(self, **kwargs):
+ yield
+
+ @contextlib.contextmanager
+ def temp_client(self, *args, **kwargs):
+ with temp_test_client(self, *args, **kwargs):
+ yield
+
+ def make_http_request(self, path="/", headers=None):
+ if headers is None:
+ headers = {}
+
+ # Set url to 'https?://<host>:<port><path>'.
+ url = get_server_uri(
+ self.server, resource_name=path, secure=self.secure
+ ).replace("ws", "http")
+
+ request = urllib.request.Request(url=url, headers=headers)
+
+ if self.secure:
+ open_health_check = functools.partial(
+ urllib.request.urlopen, request, context=self.client_context
+ )
+ else:
+ open_health_check = functools.partial(urllib.request.urlopen, request)
+
+ return self.loop.run_in_executor(None, open_health_check)
+
+
+class SecureClientServerTestsMixin(ClientServerTestsMixin):
+
+ secure = True
+
+ @property
+ def server_context(self):
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+ ssl_context.load_cert_chain(testcert)
+ return ssl_context
+
+ @property
+ def client_context(self):
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ ssl_context.load_verify_locations(testcert)
+ return ssl_context
+
+ def start_server(self, **kwargs):
+ kwargs.setdefault("ssl", self.server_context)
+ super().start_server(**kwargs)
+
+ def start_client(self, path="/", **kwargs):
+ kwargs.setdefault("ssl", self.client_context)
+ super().start_client(path, **kwargs)
+
+
+class CommonClientServerTests:
+ """
+ Mixin that defines most tests but doesn't inherit unittest.TestCase.
+
+ Tests are run by the ClientServerTests and SecureClientServerTests subclasses.
+
+ """
+
+ @with_server()
+ @with_client()
+ def test_basic(self):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ @with_server()
+ def test_redirect(self):
+ redirect_statuses = [
+ http.HTTPStatus.MOVED_PERMANENTLY,
+ http.HTTPStatus.FOUND,
+ http.HTTPStatus.SEE_OTHER,
+ http.HTTPStatus.TEMPORARY_REDIRECT,
+ http.HTTPStatus.PERMANENT_REDIRECT,
+ ]
+ for status in redirect_statuses:
+ with temp_test_redirecting_server(self, status):
+ with temp_test_client(self):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ def test_infinite_redirect(self):
+ with temp_test_redirecting_server(self, http.HTTPStatus.FOUND):
+ self.server = self.redirecting_server
+ with self.assertRaises(InvalidHandshake):
+ with temp_test_client(self):
+ self.fail("Did not raise") # pragma: no cover
+
+ @with_server()
+ def test_redirect_missing_location(self):
+ with temp_test_redirecting_server(
+ self, http.HTTPStatus.FOUND, include_location=False
+ ):
+ with self.assertRaises(InvalidHeader):
+ with temp_test_client(self):
+ self.fail("Did not raise") # pragma: no cover
+
+ def test_explicit_event_loop(self):
+ with self.temp_server(loop=self.loop):
+ with self.temp_client(loop=self.loop):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ @with_server()
+ def test_explicit_host_port(self):
+ uri = get_server_uri(self.server, self.secure)
+ wsuri = parse_uri(uri)
+
+ # Change host and port to invalid values.
+ changed_uri = uri.replace(wsuri.host, "example.com").replace(
+ str(wsuri.port), str(65535 - wsuri.port)
+ )
+
+ with self.temp_client(uri=changed_uri, host=wsuri.host, port=wsuri.port):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ @with_server()
+ def test_explicit_socket(self):
+ class TrackedSocket(socket.socket):
+ def __init__(self, *args, **kwargs):
+ self.used_for_read = False
+ self.used_for_write = False
+ super().__init__(*args, **kwargs)
+
+ def recv(self, *args, **kwargs):
+ self.used_for_read = True
+ return super().recv(*args, **kwargs)
+
+ def send(self, *args, **kwargs):
+ self.used_for_write = True
+ return super().send(*args, **kwargs)
+
+ server_socket = [
+ sock for sock in self.server.sockets if sock.family == socket.AF_INET
+ ][0]
+ client_socket = TrackedSocket(socket.AF_INET, socket.SOCK_STREAM)
+ client_socket.connect(server_socket.getsockname())
+
+ try:
+ self.assertFalse(client_socket.used_for_read)
+ self.assertFalse(client_socket.used_for_write)
+
+ with self.temp_client(
+ sock=client_socket,
+ # "You must set server_hostname when using ssl without a host"
+ server_hostname="localhost" if self.secure else None,
+ ):
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ self.assertTrue(client_socket.used_for_read)
+ self.assertTrue(client_socket.used_for_write)
+
+ finally:
+ client_socket.close()
+
+ @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets")
+ def test_unix_socket(self):
+ with tempfile.TemporaryDirectory() as temp_dir:
+ path = bytes(pathlib.Path(temp_dir) / "websockets")
+
+ # Like self.start_server() but with unix_serve().
+ unix_server = unix_serve(handler, path)
+ self.server = self.loop.run_until_complete(unix_server)
+ try:
+ # Like self.start_client() but with unix_connect()
+ unix_client = unix_connect(path)
+ self.client = self.loop.run_until_complete(unix_client)
+ try:
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+ finally:
+ self.stop_client()
+ finally:
+ self.stop_server()
+
+ async def process_request_OK(path, request_headers):
+ return http.HTTPStatus.OK, [], b"OK\n"
+
+ @with_server(process_request=process_request_OK)
+ def test_process_request_argument(self):
+ response = self.loop.run_until_complete(self.make_http_request("/"))
+
+ with contextlib.closing(response):
+ self.assertEqual(response.code, 200)
+
+ def legacy_process_request_OK(path, request_headers):
+ return http.HTTPStatus.OK, [], b"OK\n"
+
+ @with_server(process_request=legacy_process_request_OK)
+ def test_process_request_argument_backwards_compatibility(self):
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ response = self.loop.run_until_complete(self.make_http_request("/"))
+
+ with contextlib.closing(response):
+ self.assertEqual(response.code, 200)
+
+ self.assertDeprecationWarnings(
+ recorded_warnings, ["declare process_request as a coroutine"]
+ )
+
+ class ProcessRequestOKServerProtocol(WebSocketServerProtocol):
+ async def process_request(self, path, request_headers):
+ return http.HTTPStatus.OK, [], b"OK\n"
+
+ @with_server(create_protocol=ProcessRequestOKServerProtocol)
+ def test_process_request_override(self):
+ response = self.loop.run_until_complete(self.make_http_request("/"))
+
+ with contextlib.closing(response):
+ self.assertEqual(response.code, 200)
+
+ class LegacyProcessRequestOKServerProtocol(WebSocketServerProtocol):
+ def process_request(self, path, request_headers):
+ return http.HTTPStatus.OK, [], b"OK\n"
+
+ @with_server(create_protocol=LegacyProcessRequestOKServerProtocol)
+ def test_process_request_override_backwards_compatibility(self):
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ response = self.loop.run_until_complete(self.make_http_request("/"))
+
+ with contextlib.closing(response):
+ self.assertEqual(response.code, 200)
+
+ self.assertDeprecationWarnings(
+ recorded_warnings, ["declare process_request as a coroutine"]
+ )
+
+ def select_subprotocol_chat(client_subprotocols, server_subprotocols):
+ return "chat"
+
+ @with_server(
+ subprotocols=["superchat", "chat"], select_subprotocol=select_subprotocol_chat
+ )
+ @with_client("/subprotocol", subprotocols=["superchat", "chat"])
+ def test_select_subprotocol_argument(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr("chat"))
+ self.assertEqual(self.client.subprotocol, "chat")
+
+ class SelectSubprotocolChatServerProtocol(WebSocketServerProtocol):
+ def select_subprotocol(self, client_subprotocols, server_subprotocols):
+ return "chat"
+
+ @with_server(
+ subprotocols=["superchat", "chat"],
+ create_protocol=SelectSubprotocolChatServerProtocol,
+ )
+ @with_client("/subprotocol", subprotocols=["superchat", "chat"])
+ def test_select_subprotocol_override(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr("chat"))
+ self.assertEqual(self.client.subprotocol, "chat")
+
+ @with_server()
+ @with_client("/deprecated_attributes")
+ def test_protocol_deprecated_attributes(self):
+ # The test could be connecting with IPv6 or IPv4.
+ expected_client_attrs = [
+ server_socket.getsockname()[:2] + (self.secure,)
+ for server_socket in self.server.sockets
+ ]
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ client_attrs = (self.client.host, self.client.port, self.client.secure)
+ self.assertDeprecationWarnings(
+ recorded_warnings,
+ [
+ "use remote_address[0] instead of host",
+ "use remote_address[1] instead of port",
+ "don't use secure",
+ ],
+ )
+ self.assertIn(client_attrs, expected_client_attrs)
+
+ expected_server_attrs = ("localhost", 0, self.secure)
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ self.loop.run_until_complete(self.client.send(""))
+ server_attrs = self.loop.run_until_complete(self.client.recv())
+ self.assertDeprecationWarnings(
+ recorded_warnings,
+ [
+ "use local_address[0] instead of host",
+ "use local_address[1] instead of port",
+ "don't use secure",
+ ],
+ )
+ self.assertEqual(server_attrs, repr(expected_server_attrs))
+
+ @with_server()
+ @with_client("/path")
+ def test_protocol_path(self):
+ client_path = self.client.path
+ self.assertEqual(client_path, "/path")
+ server_path = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_path, "/path")
+
+ @with_server()
+ @with_client("/headers")
+ def test_protocol_headers(self):
+ client_req = self.client.request_headers
+ client_resp = self.client.response_headers
+ self.assertEqual(client_req["User-Agent"], USER_AGENT)
+ self.assertEqual(client_resp["Server"], USER_AGENT)
+ server_req = self.loop.run_until_complete(self.client.recv())
+ server_resp = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_req, repr(client_req))
+ self.assertEqual(server_resp, repr(client_resp))
+
+ @with_server()
+ @with_client("/headers", extra_headers=Headers({"X-Spam": "Eggs"}))
+ def test_protocol_custom_request_headers(self):
+ req_headers = self.loop.run_until_complete(self.client.recv())
+ self.loop.run_until_complete(self.client.recv())
+ self.assertIn("('X-Spam', 'Eggs')", req_headers)
+
+ @with_server()
+ @with_client("/headers", extra_headers={"X-Spam": "Eggs"})
+ def test_protocol_custom_request_headers_dict(self):
+ req_headers = self.loop.run_until_complete(self.client.recv())
+ self.loop.run_until_complete(self.client.recv())
+ self.assertIn("('X-Spam', 'Eggs')", req_headers)
+
+ @with_server()
+ @with_client("/headers", extra_headers=[("X-Spam", "Eggs")])
+ def test_protocol_custom_request_headers_list(self):
+ req_headers = self.loop.run_until_complete(self.client.recv())
+ self.loop.run_until_complete(self.client.recv())
+ self.assertIn("('X-Spam', 'Eggs')", req_headers)
+
+ @with_server()
+ @with_client("/headers", extra_headers=[("User-Agent", "Eggs")])
+ def test_protocol_custom_request_user_agent(self):
+ req_headers = self.loop.run_until_complete(self.client.recv())
+ self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(req_headers.count("User-Agent"), 1)
+ self.assertIn("('User-Agent', 'Eggs')", req_headers)
+
+ @with_server(extra_headers=lambda p, r: Headers({"X-Spam": "Eggs"}))
+ @with_client("/headers")
+ def test_protocol_custom_response_headers_callable(self):
+ self.loop.run_until_complete(self.client.recv())
+ resp_headers = self.loop.run_until_complete(self.client.recv())
+ self.assertIn("('X-Spam', 'Eggs')", resp_headers)
+
+ @with_server(extra_headers=lambda p, r: {"X-Spam": "Eggs"})
+ @with_client("/headers")
+ def test_protocol_custom_response_headers_callable_dict(self):
+ self.loop.run_until_complete(self.client.recv())
+ resp_headers = self.loop.run_until_complete(self.client.recv())
+ self.assertIn("('X-Spam', 'Eggs')", resp_headers)
+
+ @with_server(extra_headers=lambda p, r: [("X-Spam", "Eggs")])
+ @with_client("/headers")
+ def test_protocol_custom_response_headers_callable_list(self):
+ self.loop.run_until_complete(self.client.recv())
+ resp_headers = self.loop.run_until_complete(self.client.recv())
+ self.assertIn("('X-Spam', 'Eggs')", resp_headers)
+
+ @with_server(extra_headers=lambda p, r: None)
+ @with_client("/headers")
+ def test_protocol_custom_response_headers_callable_none(self):
+ self.loop.run_until_complete(self.client.recv()) # doesn't crash
+ self.loop.run_until_complete(self.client.recv()) # nothing to check
+
+ @with_server(extra_headers=Headers({"X-Spam": "Eggs"}))
+ @with_client("/headers")
+ def test_protocol_custom_response_headers(self):
+ self.loop.run_until_complete(self.client.recv())
+ resp_headers = self.loop.run_until_complete(self.client.recv())
+ self.assertIn("('X-Spam', 'Eggs')", resp_headers)
+
+ @with_server(extra_headers={"X-Spam": "Eggs"})
+ @with_client("/headers")
+ def test_protocol_custom_response_headers_dict(self):
+ self.loop.run_until_complete(self.client.recv())
+ resp_headers = self.loop.run_until_complete(self.client.recv())
+ self.assertIn("('X-Spam', 'Eggs')", resp_headers)
+
+ @with_server(extra_headers=[("X-Spam", "Eggs")])
+ @with_client("/headers")
+ def test_protocol_custom_response_headers_list(self):
+ self.loop.run_until_complete(self.client.recv())
+ resp_headers = self.loop.run_until_complete(self.client.recv())
+ self.assertIn("('X-Spam', 'Eggs')", resp_headers)
+
+ @with_server(extra_headers=[("Server", "Eggs")])
+ @with_client("/headers")
+ def test_protocol_custom_response_user_agent(self):
+ self.loop.run_until_complete(self.client.recv())
+ resp_headers = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(resp_headers.count("Server"), 1)
+ self.assertIn("('Server', 'Eggs')", resp_headers)
+
+ @with_server(create_protocol=HealthCheckServerProtocol)
+ def test_http_request_http_endpoint(self):
+ # Making a HTTP request to a HTTP endpoint succeeds.
+ response = self.loop.run_until_complete(self.make_http_request("/__health__/"))
+
+ with contextlib.closing(response):
+ self.assertEqual(response.code, 200)
+ self.assertEqual(response.read(), b"status = green\n")
+
+ @with_server(create_protocol=HealthCheckServerProtocol)
+ def test_http_request_ws_endpoint(self):
+ # Making a HTTP request to a WS endpoint fails.
+ with self.assertRaises(urllib.error.HTTPError) as raised:
+ self.loop.run_until_complete(self.make_http_request())
+
+ self.assertEqual(raised.exception.code, 426)
+ self.assertEqual(raised.exception.headers["Upgrade"], "websocket")
+
+ @with_server(create_protocol=HealthCheckServerProtocol)
+ def test_ws_connection_http_endpoint(self):
+ # Making a WS connection to a HTTP endpoint fails.
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client("/__health__/")
+
+ self.assertEqual(raised.exception.status_code, 200)
+
+ @with_server(create_protocol=HealthCheckServerProtocol)
+ def test_ws_connection_ws_endpoint(self):
+ # Making a WS connection to a WS endpoint succeeds.
+ self.start_client()
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ self.loop.run_until_complete(self.client.recv())
+ self.stop_client()
+
+ def assert_client_raises_code(self, status_code):
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client()
+ self.assertEqual(raised.exception.status_code, status_code)
+
+ @with_server(create_protocol=UnauthorizedServerProtocol)
+ def test_server_create_protocol(self):
+ self.assert_client_raises_code(401)
+
+ def create_unauthorized_server_protocol(*args, **kwargs):
+ return UnauthorizedServerProtocol(*args, **kwargs)
+
+ @with_server(create_protocol=create_unauthorized_server_protocol)
+ def test_server_create_protocol_function(self):
+ self.assert_client_raises_code(401)
+
+ @with_server(
+ klass=UnauthorizedServerProtocol,
+ deprecation_warnings=["rename klass to create_protocol"],
+ )
+ def test_server_klass_backwards_compatibility(self):
+ self.assert_client_raises_code(401)
+
+ @with_server(
+ create_protocol=ForbiddenServerProtocol,
+ klass=UnauthorizedServerProtocol,
+ deprecation_warnings=["rename klass to create_protocol"],
+ )
+ def test_server_create_protocol_over_klass(self):
+ self.assert_client_raises_code(403)
+
+ @with_server()
+ @with_client("/path", create_protocol=FooClientProtocol)
+ def test_client_create_protocol(self):
+ self.assertIsInstance(self.client, FooClientProtocol)
+
+ @with_server()
+ @with_client(
+ "/path",
+ create_protocol=(lambda *args, **kwargs: FooClientProtocol(*args, **kwargs)),
+ )
+ def test_client_create_protocol_function(self):
+ self.assertIsInstance(self.client, FooClientProtocol)
+
+ @with_server()
+ @with_client(
+ "/path",
+ klass=FooClientProtocol,
+ deprecation_warnings=["rename klass to create_protocol"],
+ )
+ def test_client_klass(self):
+ self.assertIsInstance(self.client, FooClientProtocol)
+
+ @with_server()
+ @with_client(
+ "/path",
+ create_protocol=BarClientProtocol,
+ klass=FooClientProtocol,
+ deprecation_warnings=["rename klass to create_protocol"],
+ )
+ def test_client_create_protocol_over_klass(self):
+ self.assertIsInstance(self.client, BarClientProtocol)
+
+ @with_server(close_timeout=7)
+ @with_client("/close_timeout")
+ def test_server_close_timeout(self):
+ close_timeout = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(eval(close_timeout), 7)
+
+ @with_server(timeout=6, deprecation_warnings=["rename timeout to close_timeout"])
+ @with_client("/close_timeout")
+ def test_server_timeout_backwards_compatibility(self):
+ close_timeout = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(eval(close_timeout), 6)
+
+ @with_server(
+ close_timeout=7,
+ timeout=6,
+ deprecation_warnings=["rename timeout to close_timeout"],
+ )
+ @with_client("/close_timeout")
+ def test_server_close_timeout_over_timeout(self):
+ close_timeout = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(eval(close_timeout), 7)
+
+ @with_server()
+ @with_client("/close_timeout", close_timeout=7)
+ def test_client_close_timeout(self):
+ self.assertEqual(self.client.close_timeout, 7)
+
+ @with_server()
+ @with_client(
+ "/close_timeout",
+ timeout=6,
+ deprecation_warnings=["rename timeout to close_timeout"],
+ )
+ def test_client_timeout_backwards_compatibility(self):
+ self.assertEqual(self.client.close_timeout, 6)
+
+ @with_server()
+ @with_client(
+ "/close_timeout",
+ close_timeout=7,
+ timeout=6,
+ deprecation_warnings=["rename timeout to close_timeout"],
+ )
+ def test_client_close_timeout_over_timeout(self):
+ self.assertEqual(self.client.close_timeout, 7)
+
+ @with_server()
+ @with_client("/extensions")
+ def test_no_extension(self):
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_extensions, repr([]))
+ self.assertEqual(repr(self.client.extensions), repr([]))
+
+ @with_server(extensions=[ServerNoOpExtensionFactory()])
+ @with_client("/extensions", extensions=[ClientNoOpExtensionFactory()])
+ def test_extension(self):
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_extensions, repr([NoOpExtension()]))
+ self.assertEqual(repr(self.client.extensions), repr([NoOpExtension()]))
+
+ @with_server()
+ @with_client("/extensions", extensions=[ClientNoOpExtensionFactory()])
+ def test_extension_not_accepted(self):
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_extensions, repr([]))
+ self.assertEqual(repr(self.client.extensions), repr([]))
+
+ @with_server(extensions=[ServerNoOpExtensionFactory()])
+ @with_client("/extensions")
+ def test_extension_not_requested(self):
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_extensions, repr([]))
+ self.assertEqual(repr(self.client.extensions), repr([]))
+
+ @with_server(extensions=[ServerNoOpExtensionFactory([("foo", None)])])
+ def test_extension_client_rejection(self):
+ with self.assertRaises(NegotiationError):
+ self.start_client("/extensions", extensions=[ClientNoOpExtensionFactory()])
+
+ @with_server(
+ extensions=[
+ # No match because the client doesn't send client_max_window_bits.
+ ServerPerMessageDeflateFactory(client_max_window_bits=10),
+ ServerPerMessageDeflateFactory(),
+ ]
+ )
+ @with_client("/extensions", extensions=[ClientPerMessageDeflateFactory()])
+ def test_extension_no_match_then_match(self):
+ # The order requested by the client has priority.
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(
+ server_extensions, repr([PerMessageDeflate(False, False, 15, 15)])
+ )
+ self.assertEqual(
+ repr(self.client.extensions),
+ repr([PerMessageDeflate(False, False, 15, 15)]),
+ )
+
+ @with_server(extensions=[ServerPerMessageDeflateFactory()])
+ @with_client("/extensions", extensions=[ClientNoOpExtensionFactory()])
+ def test_extension_mismatch(self):
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_extensions, repr([]))
+ self.assertEqual(repr(self.client.extensions), repr([]))
+
+ @with_server(
+ extensions=[ServerNoOpExtensionFactory(), ServerPerMessageDeflateFactory()]
+ )
+ @with_client(
+ "/extensions",
+ extensions=[ClientPerMessageDeflateFactory(), ClientNoOpExtensionFactory()],
+ )
+ def test_extension_order(self):
+ # The order requested by the client has priority.
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(
+ server_extensions,
+ repr([PerMessageDeflate(False, False, 15, 15), NoOpExtension()]),
+ )
+ self.assertEqual(
+ repr(self.client.extensions),
+ repr([PerMessageDeflate(False, False, 15, 15), NoOpExtension()]),
+ )
+
+ @with_server(extensions=[ServerNoOpExtensionFactory()])
+ @unittest.mock.patch.object(WebSocketServerProtocol, "process_extensions")
+ def test_extensions_error(self, _process_extensions):
+ _process_extensions.return_value = "x-no-op", [NoOpExtension()]
+
+ with self.assertRaises(NegotiationError):
+ self.start_client(
+ "/extensions", extensions=[ClientPerMessageDeflateFactory()]
+ )
+
+ @with_server(extensions=[ServerNoOpExtensionFactory()])
+ @unittest.mock.patch.object(WebSocketServerProtocol, "process_extensions")
+ def test_extensions_error_no_extensions(self, _process_extensions):
+ _process_extensions.return_value = "x-no-op", [NoOpExtension()]
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client("/extensions")
+
+ @with_server(compression="deflate")
+ @with_client("/extensions", compression="deflate")
+ def test_compression_deflate(self):
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(
+ server_extensions, repr([PerMessageDeflate(False, False, 15, 15)])
+ )
+ self.assertEqual(
+ repr(self.client.extensions),
+ repr([PerMessageDeflate(False, False, 15, 15)]),
+ )
+
+ @with_server(
+ extensions=[
+ ServerPerMessageDeflateFactory(
+ client_no_context_takeover=True, server_max_window_bits=10
+ )
+ ],
+ compression="deflate", # overridden by explicit config
+ )
+ @with_client(
+ "/extensions",
+ extensions=[
+ ClientPerMessageDeflateFactory(
+ server_no_context_takeover=True, client_max_window_bits=12
+ )
+ ],
+ compression="deflate", # overridden by explicit config
+ )
+ def test_compression_deflate_and_explicit_config(self):
+ server_extensions = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(
+ server_extensions, repr([PerMessageDeflate(True, True, 12, 10)])
+ )
+ self.assertEqual(
+ repr(self.client.extensions), repr([PerMessageDeflate(True, True, 10, 12)])
+ )
+
+ def test_compression_unsupported_server(self):
+ with self.assertRaises(ValueError):
+ self.start_server(compression="xz")
+
+ @with_server()
+ def test_compression_unsupported_client(self):
+ with self.assertRaises(ValueError):
+ self.start_client(compression="xz")
+
+ @with_server()
+ @with_client("/subprotocol")
+ def test_no_subprotocol(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr(None))
+ self.assertEqual(self.client.subprotocol, None)
+
+ @with_server(subprotocols=["superchat", "chat"])
+ @with_client("/subprotocol", subprotocols=["otherchat", "chat"])
+ def test_subprotocol(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr("chat"))
+ self.assertEqual(self.client.subprotocol, "chat")
+
+ @with_server(subprotocols=["superchat"])
+ @with_client("/subprotocol", subprotocols=["otherchat"])
+ def test_subprotocol_not_accepted(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr(None))
+ self.assertEqual(self.client.subprotocol, None)
+
+ @with_server()
+ @with_client("/subprotocol", subprotocols=["otherchat", "chat"])
+ def test_subprotocol_not_offered(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr(None))
+ self.assertEqual(self.client.subprotocol, None)
+
+ @with_server(subprotocols=["superchat", "chat"])
+ @with_client("/subprotocol")
+ def test_subprotocol_not_requested(self):
+ server_subprotocol = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(server_subprotocol, repr(None))
+ self.assertEqual(self.client.subprotocol, None)
+
+ @with_server(subprotocols=["superchat"])
+ @unittest.mock.patch.object(WebSocketServerProtocol, "process_subprotocol")
+ def test_subprotocol_error(self, _process_subprotocol):
+ _process_subprotocol.return_value = "superchat"
+
+ with self.assertRaises(NegotiationError):
+ self.start_client("/subprotocol", subprotocols=["otherchat"])
+ self.run_loop_once()
+
+ @with_server(subprotocols=["superchat"])
+ @unittest.mock.patch.object(WebSocketServerProtocol, "process_subprotocol")
+ def test_subprotocol_error_no_subprotocols(self, _process_subprotocol):
+ _process_subprotocol.return_value = "superchat"
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client("/subprotocol")
+ self.run_loop_once()
+
+ @with_server(subprotocols=["superchat", "chat"])
+ @unittest.mock.patch.object(WebSocketServerProtocol, "process_subprotocol")
+ def test_subprotocol_error_two_subprotocols(self, _process_subprotocol):
+ _process_subprotocol.return_value = "superchat, chat"
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client("/subprotocol", subprotocols=["superchat", "chat"])
+ self.run_loop_once()
+
+ @with_server()
+ @unittest.mock.patch("websockets.server.read_request")
+ def test_server_receives_malformed_request(self, _read_request):
+ _read_request.side_effect = ValueError("read_request failed")
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client()
+
+ @with_server()
+ @unittest.mock.patch("websockets.client.read_response")
+ def test_client_receives_malformed_response(self, _read_response):
+ _read_response.side_effect = ValueError("read_response failed")
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client()
+ self.run_loop_once()
+
+ @with_server()
+ @unittest.mock.patch("websockets.client.build_request")
+ def test_client_sends_invalid_handshake_request(self, _build_request):
+ def wrong_build_request(headers):
+ return "42"
+
+ _build_request.side_effect = wrong_build_request
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client()
+
+ @with_server()
+ @unittest.mock.patch("websockets.server.build_response")
+ def test_server_sends_invalid_handshake_response(self, _build_response):
+ def wrong_build_response(headers, key):
+ return build_response(headers, "42")
+
+ _build_response.side_effect = wrong_build_response
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client()
+
+ @with_server()
+ @unittest.mock.patch("websockets.client.read_response")
+ def test_server_does_not_switch_protocols(self, _read_response):
+ async def wrong_read_response(stream):
+ status_code, reason, headers = await read_response(stream)
+ return 400, "Bad Request", headers
+
+ _read_response.side_effect = wrong_read_response
+
+ with self.assertRaises(InvalidStatusCode):
+ self.start_client()
+ self.run_loop_once()
+
+ @with_server()
+ @unittest.mock.patch("websockets.server.WebSocketServerProtocol.process_request")
+ def test_server_error_in_handshake(self, _process_request):
+ _process_request.side_effect = Exception("process_request crashed")
+
+ with self.assertRaises(InvalidHandshake):
+ self.start_client()
+
+ @with_server()
+ @unittest.mock.patch("websockets.server.WebSocketServerProtocol.send")
+ def test_server_handler_crashes(self, send):
+ send.side_effect = ValueError("send failed")
+
+ with self.temp_client():
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.client.recv())
+
+ # Connection ends with an unexpected error.
+ self.assertEqual(self.client.close_code, 1011)
+
+ @with_server()
+ @unittest.mock.patch("websockets.server.WebSocketServerProtocol.close")
+ def test_server_close_crashes(self, close):
+ close.side_effect = ValueError("close failed")
+
+ with self.temp_client():
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ # Connection ends with an abnormal closure.
+ self.assertEqual(self.client.close_code, 1006)
+
+ @with_server()
+ @with_client()
+ @unittest.mock.patch.object(WebSocketClientProtocol, "handshake")
+ def test_client_closes_connection_before_handshake(self, handshake):
+ # We have mocked the handshake() method to prevent the client from
+ # performing the opening handshake. Force it to close the connection.
+ self.client.transport.close()
+ # The server should stop properly anyway. It used to hang because the
+ # task handling the connection was waiting for the opening handshake.
+
+ @with_server(create_protocol=SlowOpeningHandshakeProtocol)
+ def test_server_shuts_down_during_opening_handshake(self):
+ self.loop.call_later(5 * MS, self.server.close)
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client()
+ exception = raised.exception
+ self.assertEqual(
+ str(exception), "server rejected WebSocket connection: HTTP 503"
+ )
+ self.assertEqual(exception.status_code, 503)
+
+ @with_server()
+ def test_server_shuts_down_during_connection_handling(self):
+ with self.temp_client():
+ server_ws = next(iter(self.server.websockets))
+ self.server.close()
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.client.recv())
+
+ # Websocket connection closes properly with 1001 Going Away.
+ self.assertEqual(self.client.close_code, 1001)
+ self.assertEqual(server_ws.close_code, 1001)
+
+ @with_server()
+ def test_server_shuts_down_waits_until_handlers_terminate(self):
+ # This handler waits a bit after the connection is closed in order
+ # to test that wait_closed() really waits for handlers to complete.
+ self.start_client("/slow_stop")
+ server_ws = next(iter(self.server.websockets))
+
+ # Test that the handler task keeps running after close().
+ self.server.close()
+ self.loop.run_until_complete(asyncio.sleep(MS))
+ self.assertFalse(server_ws.handler_task.done())
+
+ # Test that the handler task terminates before wait_closed() returns.
+ self.loop.run_until_complete(self.server.wait_closed())
+ self.assertTrue(server_ws.handler_task.done())
+
+ @with_server(create_protocol=ForbiddenServerProtocol)
+ def test_invalid_status_error_during_client_connect(self):
+ with self.assertRaises(InvalidStatusCode) as raised:
+ self.start_client()
+ exception = raised.exception
+ self.assertEqual(
+ str(exception), "server rejected WebSocket connection: HTTP 403"
+ )
+ self.assertEqual(exception.status_code, 403)
+
+ @with_server()
+ @unittest.mock.patch(
+ "websockets.server.WebSocketServerProtocol.write_http_response"
+ )
+ @unittest.mock.patch("websockets.server.WebSocketServerProtocol.read_http_request")
+ def test_connection_error_during_opening_handshake(
+ self, _read_http_request, _write_http_response
+ ):
+ _read_http_request.side_effect = ConnectionError
+
+ # This exception is currently platform-dependent. It was observed to
+ # be ConnectionResetError on Linux in the non-TLS case, and
+ # InvalidMessage otherwise (including both Linux and macOS). This
+ # doesn't matter though since this test is primarily for testing a
+ # code path on the server side.
+ with self.assertRaises(Exception):
+ self.start_client()
+
+ # No response must not be written if the network connection is broken.
+ _write_http_response.assert_not_called()
+
+ @with_server()
+ @unittest.mock.patch("websockets.server.WebSocketServerProtocol.close")
+ def test_connection_error_during_closing_handshake(self, close):
+ close.side_effect = ConnectionError
+
+ with self.temp_client():
+ self.loop.run_until_complete(self.client.send("Hello!"))
+ reply = self.loop.run_until_complete(self.client.recv())
+ self.assertEqual(reply, "Hello!")
+
+ # Connection ends with an abnormal closure.
+ self.assertEqual(self.client.close_code, 1006)
+
+
+class ClientServerTests(
+ CommonClientServerTests, ClientServerTestsMixin, AsyncioTestCase
+):
+ pass
+
+
+class SecureClientServerTests(
+ CommonClientServerTests, SecureClientServerTestsMixin, AsyncioTestCase
+):
+
+ # TLS over Unix sockets doesn't make sense.
+ test_unix_socket = None
+
+ @with_server()
+ def test_ws_uri_is_rejected(self):
+ with self.assertRaises(ValueError):
+ connect(get_server_uri(self.server, secure=False), ssl=self.client_context)
+
+ @with_server()
+ def test_redirect_insecure(self):
+ with temp_test_redirecting_server(
+ self, http.HTTPStatus.FOUND, force_insecure=True
+ ):
+ with self.assertRaises(InvalidHandshake):
+ with temp_test_client(self):
+ self.fail("Did not raise") # pragma: no cover
+
+
+class ClientServerOriginTests(AsyncioTestCase):
+ def test_checking_origin_succeeds(self):
+ server = self.loop.run_until_complete(
+ serve(handler, "localhost", 0, origins=["http://localhost"])
+ )
+ client = self.loop.run_until_complete(
+ connect(get_server_uri(server), origin="http://localhost")
+ )
+
+ self.loop.run_until_complete(client.send("Hello!"))
+ self.assertEqual(self.loop.run_until_complete(client.recv()), "Hello!")
+
+ self.loop.run_until_complete(client.close())
+ server.close()
+ self.loop.run_until_complete(server.wait_closed())
+
+ def test_checking_origin_fails(self):
+ server = self.loop.run_until_complete(
+ serve(handler, "localhost", 0, origins=["http://localhost"])
+ )
+ with self.assertRaisesRegex(
+ InvalidHandshake, "server rejected WebSocket connection: HTTP 403"
+ ):
+ self.loop.run_until_complete(
+ connect(get_server_uri(server), origin="http://otherhost")
+ )
+
+ server.close()
+ self.loop.run_until_complete(server.wait_closed())
+
+ def test_checking_origins_fails_with_multiple_headers(self):
+ server = self.loop.run_until_complete(
+ serve(handler, "localhost", 0, origins=["http://localhost"])
+ )
+ with self.assertRaisesRegex(
+ InvalidHandshake, "server rejected WebSocket connection: HTTP 400"
+ ):
+ self.loop.run_until_complete(
+ connect(
+ get_server_uri(server),
+ origin="http://localhost",
+ extra_headers=[("Origin", "http://otherhost")],
+ )
+ )
+
+ server.close()
+ self.loop.run_until_complete(server.wait_closed())
+
+ def test_checking_lack_of_origin_succeeds(self):
+ server = self.loop.run_until_complete(
+ serve(handler, "localhost", 0, origins=[None])
+ )
+ client = self.loop.run_until_complete(connect(get_server_uri(server)))
+
+ self.loop.run_until_complete(client.send("Hello!"))
+ self.assertEqual(self.loop.run_until_complete(client.recv()), "Hello!")
+
+ self.loop.run_until_complete(client.close())
+ server.close()
+ self.loop.run_until_complete(server.wait_closed())
+
+ def test_checking_lack_of_origin_succeeds_backwards_compatibility(self):
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ server = self.loop.run_until_complete(
+ serve(handler, "localhost", 0, origins=[""])
+ )
+ client = self.loop.run_until_complete(connect(get_server_uri(server)))
+
+ self.assertDeprecationWarnings(
+ recorded_warnings, ["use None instead of '' in origins"]
+ )
+
+ self.loop.run_until_complete(client.send("Hello!"))
+ self.assertEqual(self.loop.run_until_complete(client.recv()), "Hello!")
+
+ self.loop.run_until_complete(client.close())
+ server.close()
+ self.loop.run_until_complete(server.wait_closed())
+
+
+class YieldFromTests(AsyncioTestCase):
+ def test_client(self):
+ start_server = serve(handler, "localhost", 0)
+ server = self.loop.run_until_complete(start_server)
+
+ # @asyncio.coroutine is deprecated on Python ≥ 3.8
+ with warnings.catch_warnings(record=True):
+
+ @asyncio.coroutine
+ def run_client():
+ # Yield from connect.
+ client = yield from connect(get_server_uri(server))
+ self.assertEqual(client.state, State.OPEN)
+ yield from client.close()
+ self.assertEqual(client.state, State.CLOSED)
+
+ self.loop.run_until_complete(run_client())
+
+ server.close()
+ self.loop.run_until_complete(server.wait_closed())
+
+ def test_server(self):
+ # @asyncio.coroutine is deprecated on Python ≥ 3.8
+ with warnings.catch_warnings(record=True):
+
+ @asyncio.coroutine
+ def run_server():
+ # Yield from serve.
+ server = yield from serve(handler, "localhost", 0)
+ self.assertTrue(server.sockets)
+ server.close()
+ yield from server.wait_closed()
+ self.assertFalse(server.sockets)
+
+ self.loop.run_until_complete(run_server())
+
+
+class AsyncAwaitTests(AsyncioTestCase):
+ def test_client(self):
+ start_server = serve(handler, "localhost", 0)
+ server = self.loop.run_until_complete(start_server)
+
+ async def run_client():
+ # Await connect.
+ client = await connect(get_server_uri(server))
+ self.assertEqual(client.state, State.OPEN)
+ await client.close()
+ self.assertEqual(client.state, State.CLOSED)
+
+ self.loop.run_until_complete(run_client())
+
+ server.close()
+ self.loop.run_until_complete(server.wait_closed())
+
+ def test_server(self):
+ async def run_server():
+ # Await serve.
+ server = await serve(handler, "localhost", 0)
+ self.assertTrue(server.sockets)
+ server.close()
+ await server.wait_closed()
+ self.assertFalse(server.sockets)
+
+ self.loop.run_until_complete(run_server())
+
+
+class ContextManagerTests(AsyncioTestCase):
+ def test_client(self):
+ start_server = serve(handler, "localhost", 0)
+ server = self.loop.run_until_complete(start_server)
+
+ async def run_client():
+ # Use connect as an asynchronous context manager.
+ async with connect(get_server_uri(server)) as client:
+ self.assertEqual(client.state, State.OPEN)
+
+ # Check that exiting the context manager closed the connection.
+ self.assertEqual(client.state, State.CLOSED)
+
+ self.loop.run_until_complete(run_client())
+
+ server.close()
+ self.loop.run_until_complete(server.wait_closed())
+
+ def test_server(self):
+ async def run_server():
+ # Use serve as an asynchronous context manager.
+ async with serve(handler, "localhost", 0) as server:
+ self.assertTrue(server.sockets)
+
+ # Check that exiting the context manager closed the server.
+ self.assertFalse(server.sockets)
+
+ self.loop.run_until_complete(run_server())
+
+ @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets")
+ def test_unix_server(self):
+ async def run_server(path):
+ async with unix_serve(handler, path) as server:
+ self.assertTrue(server.sockets)
+
+ # Check that exiting the context manager closed the server.
+ self.assertFalse(server.sockets)
+
+ with tempfile.TemporaryDirectory() as temp_dir:
+ path = bytes(pathlib.Path(temp_dir) / "websockets")
+ self.loop.run_until_complete(run_server(path))
+
+
+class AsyncIteratorTests(AsyncioTestCase):
+
+ # This is a protocol-level feature, but since it's a high-level API, it is
+ # much easier to exercise at the client or server level.
+
+ MESSAGES = ["3", "2", "1", "Fire!"]
+
+ def test_iterate_on_messages(self):
+ async def handler(ws, path):
+ for message in self.MESSAGES:
+ await ws.send(message)
+
+ start_server = serve(handler, "localhost", 0)
+ server = self.loop.run_until_complete(start_server)
+
+ messages = []
+
+ async def run_client():
+ nonlocal messages
+ async with connect(get_server_uri(server)) as ws:
+ async for message in ws:
+ messages.append(message)
+
+ self.loop.run_until_complete(run_client())
+
+ self.assertEqual(messages, self.MESSAGES)
+
+ server.close()
+ self.loop.run_until_complete(server.wait_closed())
+
+ def test_iterate_on_messages_going_away_exit_ok(self):
+ async def handler(ws, path):
+ for message in self.MESSAGES:
+ await ws.send(message)
+ await ws.close(1001)
+
+ start_server = serve(handler, "localhost", 0)
+ server = self.loop.run_until_complete(start_server)
+
+ messages = []
+
+ async def run_client():
+ nonlocal messages
+ async with connect(get_server_uri(server)) as ws:
+ async for message in ws:
+ messages.append(message)
+
+ self.loop.run_until_complete(run_client())
+
+ self.assertEqual(messages, self.MESSAGES)
+
+ server.close()
+ self.loop.run_until_complete(server.wait_closed())
+
+ def test_iterate_on_messages_internal_error_exit_not_ok(self):
+ async def handler(ws, path):
+ for message in self.MESSAGES:
+ await ws.send(message)
+ await ws.close(1011)
+
+ start_server = serve(handler, "localhost", 0)
+ server = self.loop.run_until_complete(start_server)
+
+ messages = []
+
+ async def run_client():
+ nonlocal messages
+ async with connect(get_server_uri(server)) as ws:
+ async for message in ws:
+ messages.append(message)
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(run_client())
+
+ self.assertEqual(messages, self.MESSAGES)
+
+ server.close()
+ self.loop.run_until_complete(server.wait_closed())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_exceptions.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_exceptions.py
new file mode 100644
index 0000000000..7ad5ad8335
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_exceptions.py
@@ -0,0 +1,145 @@
+import unittest
+
+from websockets.exceptions import *
+from websockets.http import Headers
+
+
+class ExceptionsTests(unittest.TestCase):
+ def test_str(self):
+ for exception, exception_str in [
+ # fmt: off
+ (
+ WebSocketException("something went wrong"),
+ "something went wrong",
+ ),
+ (
+ ConnectionClosed(1000, ""),
+ "code = 1000 (OK), no reason",
+ ),
+ (
+ ConnectionClosed(1006, None),
+ "code = 1006 (connection closed abnormally [internal]), no reason"
+ ),
+ (
+ ConnectionClosed(3000, None),
+ "code = 3000 (registered), no reason"
+ ),
+ (
+ ConnectionClosed(4000, None),
+ "code = 4000 (private use), no reason"
+ ),
+ (
+ ConnectionClosedError(1016, None),
+ "code = 1016 (unknown), no reason"
+ ),
+ (
+ ConnectionClosedOK(1001, "bye"),
+ "code = 1001 (going away), reason = bye",
+ ),
+ (
+ InvalidHandshake("invalid request"),
+ "invalid request",
+ ),
+ (
+ SecurityError("redirect from WSS to WS"),
+ "redirect from WSS to WS",
+ ),
+ (
+ InvalidMessage("malformed HTTP message"),
+ "malformed HTTP message",
+ ),
+ (
+ InvalidHeader("Name"),
+ "missing Name header",
+ ),
+ (
+ InvalidHeader("Name", None),
+ "missing Name header",
+ ),
+ (
+ InvalidHeader("Name", ""),
+ "empty Name header",
+ ),
+ (
+ InvalidHeader("Name", "Value"),
+ "invalid Name header: Value",
+ ),
+ (
+ InvalidHeaderFormat(
+ "Sec-WebSocket-Protocol", "expected token", "a=|", 3
+ ),
+ "invalid Sec-WebSocket-Protocol header: "
+ "expected token at 3 in a=|",
+ ),
+ (
+ InvalidHeaderValue("Sec-WebSocket-Version", "42"),
+ "invalid Sec-WebSocket-Version header: 42",
+ ),
+ (
+ InvalidOrigin("http://bad.origin"),
+ "invalid Origin header: http://bad.origin",
+ ),
+ (
+ InvalidUpgrade("Upgrade"),
+ "missing Upgrade header",
+ ),
+ (
+ InvalidUpgrade("Connection", "websocket"),
+ "invalid Connection header: websocket",
+ ),
+ (
+ InvalidStatusCode(403),
+ "server rejected WebSocket connection: HTTP 403",
+ ),
+ (
+ NegotiationError("unsupported subprotocol: spam"),
+ "unsupported subprotocol: spam",
+ ),
+ (
+ DuplicateParameter("a"),
+ "duplicate parameter: a",
+ ),
+ (
+ InvalidParameterName("|"),
+ "invalid parameter name: |",
+ ),
+ (
+ InvalidParameterValue("a", None),
+ "missing value for parameter a",
+ ),
+ (
+ InvalidParameterValue("a", ""),
+ "empty value for parameter a",
+ ),
+ (
+ InvalidParameterValue("a", "|"),
+ "invalid value for parameter a: |",
+ ),
+ (
+ AbortHandshake(200, Headers(), b"OK\n"),
+ "HTTP 200, 0 headers, 3 bytes",
+ ),
+ (
+ RedirectHandshake("wss://example.com"),
+ "redirect to wss://example.com",
+ ),
+ (
+ InvalidState("WebSocket connection isn't established yet"),
+ "WebSocket connection isn't established yet",
+ ),
+ (
+ InvalidURI("|"),
+ "| isn't a valid URI",
+ ),
+ (
+ PayloadTooBig("payload length exceeds limit: 2 > 1 bytes"),
+ "payload length exceeds limit: 2 > 1 bytes",
+ ),
+ (
+ ProtocolError("invalid opcode: 7"),
+ "invalid opcode: 7",
+ ),
+ # fmt: on
+ ]:
+ with self.subTest(exception=exception):
+ self.assertEqual(str(exception), exception_str)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_exports.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_exports.py
new file mode 100644
index 0000000000..7fcbc80e38
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_exports.py
@@ -0,0 +1,22 @@
+import unittest
+
+import websockets
+
+
+combined_exports = (
+ websockets.auth.__all__
+ + websockets.client.__all__
+ + websockets.exceptions.__all__
+ + websockets.protocol.__all__
+ + websockets.server.__all__
+ + websockets.typing.__all__
+ + websockets.uri.__all__
+)
+
+
+class TestExportsAllSubmodules(unittest.TestCase):
+ def test_top_level_module_reexports_all_submodule_exports(self):
+ self.assertEqual(set(combined_exports), set(websockets.__all__))
+
+ def test_submodule_exports_are_globally_unique(self):
+ self.assertEqual(len(set(combined_exports)), len(combined_exports))
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_framing.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_framing.py
new file mode 100644
index 0000000000..5def415d28
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_framing.py
@@ -0,0 +1,242 @@
+import asyncio
+import codecs
+import unittest
+import unittest.mock
+
+from websockets.exceptions import PayloadTooBig, ProtocolError
+from websockets.framing import *
+
+from .utils import AsyncioTestCase
+
+
+class FramingTests(AsyncioTestCase):
+ def decode(self, message, mask=False, max_size=None, extensions=None):
+ self.stream = asyncio.StreamReader(loop=self.loop)
+ self.stream.feed_data(message)
+ self.stream.feed_eof()
+ frame = self.loop.run_until_complete(
+ Frame.read(
+ self.stream.readexactly,
+ mask=mask,
+ max_size=max_size,
+ extensions=extensions,
+ )
+ )
+ # Make sure all the data was consumed.
+ self.assertTrue(self.stream.at_eof())
+ return frame
+
+ def encode(self, frame, mask=False, extensions=None):
+ write = unittest.mock.Mock()
+ frame.write(write, mask=mask, extensions=extensions)
+ # Ensure the entire frame is sent with a single call to write().
+ # Multiple calls cause TCP fragmentation and degrade performance.
+ self.assertEqual(write.call_count, 1)
+ # The frame data is the single positional argument of that call.
+ self.assertEqual(len(write.call_args[0]), 1)
+ self.assertEqual(len(write.call_args[1]), 0)
+ return write.call_args[0][0]
+
+ def round_trip(self, message, expected, mask=False, extensions=None):
+ decoded = self.decode(message, mask, extensions=extensions)
+ self.assertEqual(decoded, expected)
+ encoded = self.encode(decoded, mask, extensions=extensions)
+ if mask: # non-deterministic encoding
+ decoded = self.decode(encoded, mask, extensions=extensions)
+ self.assertEqual(decoded, expected)
+ else: # deterministic encoding
+ self.assertEqual(encoded, message)
+
+ def round_trip_close(self, data, code, reason):
+ parsed = parse_close(data)
+ self.assertEqual(parsed, (code, reason))
+ serialized = serialize_close(code, reason)
+ self.assertEqual(serialized, data)
+
+ def test_text(self):
+ self.round_trip(b"\x81\x04Spam", Frame(True, OP_TEXT, b"Spam"))
+
+ def test_text_masked(self):
+ self.round_trip(
+ b"\x81\x84\x5b\xfb\xe1\xa8\x08\x8b\x80\xc5",
+ Frame(True, OP_TEXT, b"Spam"),
+ mask=True,
+ )
+
+ def test_binary(self):
+ self.round_trip(b"\x82\x04Eggs", Frame(True, OP_BINARY, b"Eggs"))
+
+ def test_binary_masked(self):
+ self.round_trip(
+ b"\x82\x84\x53\xcd\xe2\x89\x16\xaa\x85\xfa",
+ Frame(True, OP_BINARY, b"Eggs"),
+ mask=True,
+ )
+
+ def test_non_ascii_text(self):
+ self.round_trip(
+ b"\x81\x05caf\xc3\xa9", Frame(True, OP_TEXT, "café".encode("utf-8"))
+ )
+
+ def test_non_ascii_text_masked(self):
+ self.round_trip(
+ b"\x81\x85\x64\xbe\xee\x7e\x07\xdf\x88\xbd\xcd",
+ Frame(True, OP_TEXT, "café".encode("utf-8")),
+ mask=True,
+ )
+
+ def test_close(self):
+ self.round_trip(b"\x88\x00", Frame(True, OP_CLOSE, b""))
+
+ def test_ping(self):
+ self.round_trip(b"\x89\x04ping", Frame(True, OP_PING, b"ping"))
+
+ def test_pong(self):
+ self.round_trip(b"\x8a\x04pong", Frame(True, OP_PONG, b"pong"))
+
+ def test_long(self):
+ self.round_trip(
+ b"\x82\x7e\x00\x7e" + 126 * b"a", Frame(True, OP_BINARY, 126 * b"a")
+ )
+
+ def test_very_long(self):
+ self.round_trip(
+ b"\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x00" + 65536 * b"a",
+ Frame(True, OP_BINARY, 65536 * b"a"),
+ )
+
+ def test_payload_too_big(self):
+ with self.assertRaises(PayloadTooBig):
+ self.decode(b"\x82\x7e\x04\x01" + 1025 * b"a", max_size=1024)
+
+ def test_bad_reserved_bits(self):
+ for encoded in [b"\xc0\x00", b"\xa0\x00", b"\x90\x00"]:
+ with self.subTest(encoded=encoded):
+ with self.assertRaises(ProtocolError):
+ self.decode(encoded)
+
+ def test_good_opcode(self):
+ for opcode in list(range(0x00, 0x03)) + list(range(0x08, 0x0B)):
+ encoded = bytes([0x80 | opcode, 0])
+ with self.subTest(encoded=encoded):
+ self.decode(encoded) # does not raise an exception
+
+ def test_bad_opcode(self):
+ for opcode in list(range(0x03, 0x08)) + list(range(0x0B, 0x10)):
+ encoded = bytes([0x80 | opcode, 0])
+ with self.subTest(encoded=encoded):
+ with self.assertRaises(ProtocolError):
+ self.decode(encoded)
+
+ def test_mask_flag(self):
+ # Mask flag correctly set.
+ self.decode(b"\x80\x80\x00\x00\x00\x00", mask=True)
+ # Mask flag incorrectly unset.
+ with self.assertRaises(ProtocolError):
+ self.decode(b"\x80\x80\x00\x00\x00\x00")
+ # Mask flag correctly unset.
+ self.decode(b"\x80\x00")
+ # Mask flag incorrectly set.
+ with self.assertRaises(ProtocolError):
+ self.decode(b"\x80\x00", mask=True)
+
+ def test_control_frame_max_length(self):
+ # At maximum allowed length.
+ self.decode(b"\x88\x7e\x00\x7d" + 125 * b"a")
+ # Above maximum allowed length.
+ with self.assertRaises(ProtocolError):
+ self.decode(b"\x88\x7e\x00\x7e" + 126 * b"a")
+
+ def test_prepare_data_str(self):
+ self.assertEqual(prepare_data("café"), (OP_TEXT, b"caf\xc3\xa9"))
+
+ def test_prepare_data_bytes(self):
+ self.assertEqual(prepare_data(b"tea"), (OP_BINARY, b"tea"))
+
+ def test_prepare_data_bytearray(self):
+ self.assertEqual(
+ prepare_data(bytearray(b"tea")), (OP_BINARY, bytearray(b"tea"))
+ )
+
+ def test_prepare_data_memoryview(self):
+ self.assertEqual(
+ prepare_data(memoryview(b"tea")), (OP_BINARY, memoryview(b"tea"))
+ )
+
+ def test_prepare_data_non_contiguous_memoryview(self):
+ self.assertEqual(prepare_data(memoryview(b"tteeaa")[::2]), (OP_BINARY, b"tea"))
+
+ def test_prepare_data_list(self):
+ with self.assertRaises(TypeError):
+ prepare_data([])
+
+ def test_prepare_data_none(self):
+ with self.assertRaises(TypeError):
+ prepare_data(None)
+
+ def test_encode_data_str(self):
+ self.assertEqual(encode_data("café"), b"caf\xc3\xa9")
+
+ def test_encode_data_bytes(self):
+ self.assertEqual(encode_data(b"tea"), b"tea")
+
+ def test_encode_data_bytearray(self):
+ self.assertEqual(encode_data(bytearray(b"tea")), b"tea")
+
+ def test_encode_data_memoryview(self):
+ self.assertEqual(encode_data(memoryview(b"tea")), b"tea")
+
+ def test_encode_data_non_contiguous_memoryview(self):
+ self.assertEqual(encode_data(memoryview(b"tteeaa")[::2]), b"tea")
+
+ def test_encode_data_list(self):
+ with self.assertRaises(TypeError):
+ encode_data([])
+
+ def test_encode_data_none(self):
+ with self.assertRaises(TypeError):
+ encode_data(None)
+
+ def test_fragmented_control_frame(self):
+ # Fin bit correctly set.
+ self.decode(b"\x88\x00")
+ # Fin bit incorrectly unset.
+ with self.assertRaises(ProtocolError):
+ self.decode(b"\x08\x00")
+
+ def test_parse_close_and_serialize_close(self):
+ self.round_trip_close(b"\x03\xe8", 1000, "")
+ self.round_trip_close(b"\x03\xe8OK", 1000, "OK")
+
+ def test_parse_close_empty(self):
+ self.assertEqual(parse_close(b""), (1005, ""))
+
+ def test_parse_close_errors(self):
+ with self.assertRaises(ProtocolError):
+ parse_close(b"\x03")
+ with self.assertRaises(ProtocolError):
+ parse_close(b"\x03\xe7")
+ with self.assertRaises(UnicodeDecodeError):
+ parse_close(b"\x03\xe8\xff\xff")
+
+ def test_serialize_close_errors(self):
+ with self.assertRaises(ProtocolError):
+ serialize_close(999, "")
+
+ def test_extensions(self):
+ class Rot13:
+ @staticmethod
+ def encode(frame):
+ assert frame.opcode == OP_TEXT
+ text = frame.data.decode()
+ data = codecs.encode(text, "rot13").encode()
+ return frame._replace(data=data)
+
+ # This extensions is symmetrical.
+ @staticmethod
+ def decode(frame, *, max_size=None):
+ return Rot13.encode(frame)
+
+ self.round_trip(
+ b"\x81\x05uryyb", Frame(True, OP_TEXT, b"hello"), extensions=[Rot13()]
+ )
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_handshake.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_handshake.py
new file mode 100644
index 0000000000..7d04777152
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_handshake.py
@@ -0,0 +1,190 @@
+import contextlib
+import unittest
+
+from websockets.exceptions import (
+ InvalidHandshake,
+ InvalidHeader,
+ InvalidHeaderValue,
+ InvalidUpgrade,
+)
+from websockets.handshake import *
+from websockets.handshake import accept # private API
+from websockets.http import Headers
+
+
+class HandshakeTests(unittest.TestCase):
+ def test_accept(self):
+ # Test vector from RFC 6455
+ key = "dGhlIHNhbXBsZSBub25jZQ=="
+ acc = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
+ self.assertEqual(accept(key), acc)
+
+ def test_round_trip(self):
+ request_headers = Headers()
+ request_key = build_request(request_headers)
+ response_key = check_request(request_headers)
+ self.assertEqual(request_key, response_key)
+ response_headers = Headers()
+ build_response(response_headers, response_key)
+ check_response(response_headers, request_key)
+
+ @contextlib.contextmanager
+ def assertValidRequestHeaders(self):
+ """
+ Provide request headers for modification.
+
+ Assert that the transformation kept them valid.
+
+ """
+ headers = Headers()
+ build_request(headers)
+ yield headers
+ check_request(headers)
+
+ @contextlib.contextmanager
+ def assertInvalidRequestHeaders(self, exc_type):
+ """
+ Provide request headers for modification.
+
+ Assert that the transformation made them invalid.
+
+ """
+ headers = Headers()
+ build_request(headers)
+ yield headers
+ assert issubclass(exc_type, InvalidHandshake)
+ with self.assertRaises(exc_type):
+ check_request(headers)
+
+ def test_request_invalid_connection(self):
+ with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers:
+ del headers["Connection"]
+ headers["Connection"] = "Downgrade"
+
+ def test_request_missing_connection(self):
+ with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers:
+ del headers["Connection"]
+
+ def test_request_additional_connection(self):
+ with self.assertValidRequestHeaders() as headers:
+ headers["Connection"] = "close"
+
+ def test_request_invalid_upgrade(self):
+ with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers:
+ del headers["Upgrade"]
+ headers["Upgrade"] = "socketweb"
+
+ def test_request_missing_upgrade(self):
+ with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers:
+ del headers["Upgrade"]
+
+ def test_request_additional_upgrade(self):
+ with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers:
+ headers["Upgrade"] = "socketweb"
+
+ def test_request_invalid_key_not_base64(self):
+ with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers:
+ del headers["Sec-WebSocket-Key"]
+ headers["Sec-WebSocket-Key"] = "!@#$%^&*()"
+
+ def test_request_invalid_key_not_well_padded(self):
+ with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers:
+ del headers["Sec-WebSocket-Key"]
+ headers["Sec-WebSocket-Key"] = "CSIRmL8dWYxeAdr/XpEHRw"
+
+ def test_request_invalid_key_not_16_bytes_long(self):
+ with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers:
+ del headers["Sec-WebSocket-Key"]
+ headers["Sec-WebSocket-Key"] = "ZLpprpvK4PE="
+
+ def test_request_missing_key(self):
+ with self.assertInvalidRequestHeaders(InvalidHeader) as headers:
+ del headers["Sec-WebSocket-Key"]
+
+ def test_request_additional_key(self):
+ with self.assertInvalidRequestHeaders(InvalidHeader) as headers:
+ # This duplicates the Sec-WebSocket-Key header.
+ headers["Sec-WebSocket-Key"] = headers["Sec-WebSocket-Key"]
+
+ def test_request_invalid_version(self):
+ with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers:
+ del headers["Sec-WebSocket-Version"]
+ headers["Sec-WebSocket-Version"] = "42"
+
+ def test_request_missing_version(self):
+ with self.assertInvalidRequestHeaders(InvalidHeader) as headers:
+ del headers["Sec-WebSocket-Version"]
+
+ def test_request_additional_version(self):
+ with self.assertInvalidRequestHeaders(InvalidHeader) as headers:
+ # This duplicates the Sec-WebSocket-Version header.
+ headers["Sec-WebSocket-Version"] = headers["Sec-WebSocket-Version"]
+
+ @contextlib.contextmanager
+ def assertValidResponseHeaders(self, key="CSIRmL8dWYxeAdr/XpEHRw=="):
+ """
+ Provide response headers for modification.
+
+ Assert that the transformation kept them valid.
+
+ """
+ headers = Headers()
+ build_response(headers, key)
+ yield headers
+ check_response(headers, key)
+
+ @contextlib.contextmanager
+ def assertInvalidResponseHeaders(self, exc_type, key="CSIRmL8dWYxeAdr/XpEHRw=="):
+ """
+ Provide response headers for modification.
+
+ Assert that the transformation made them invalid.
+
+ """
+ headers = Headers()
+ build_response(headers, key)
+ yield headers
+ assert issubclass(exc_type, InvalidHandshake)
+ with self.assertRaises(exc_type):
+ check_response(headers, key)
+
+ def test_response_invalid_connection(self):
+ with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers:
+ del headers["Connection"]
+ headers["Connection"] = "Downgrade"
+
+ def test_response_missing_connection(self):
+ with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers:
+ del headers["Connection"]
+
+ def test_response_additional_connection(self):
+ with self.assertValidResponseHeaders() as headers:
+ headers["Connection"] = "close"
+
+ def test_response_invalid_upgrade(self):
+ with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers:
+ del headers["Upgrade"]
+ headers["Upgrade"] = "socketweb"
+
+ def test_response_missing_upgrade(self):
+ with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers:
+ del headers["Upgrade"]
+
+ def test_response_additional_upgrade(self):
+ with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers:
+ headers["Upgrade"] = "socketweb"
+
+ def test_response_invalid_accept(self):
+ with self.assertInvalidResponseHeaders(InvalidHeaderValue) as headers:
+ del headers["Sec-WebSocket-Accept"]
+ other_key = "1Eq4UDEFQYg3YspNgqxv5g=="
+ headers["Sec-WebSocket-Accept"] = accept(other_key)
+
+ def test_response_missing_accept(self):
+ with self.assertInvalidResponseHeaders(InvalidHeader) as headers:
+ del headers["Sec-WebSocket-Accept"]
+
+ def test_response_additional_accept(self):
+ with self.assertInvalidResponseHeaders(InvalidHeader) as headers:
+ # This duplicates the Sec-WebSocket-Accept header.
+ headers["Sec-WebSocket-Accept"] = headers["Sec-WebSocket-Accept"]
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_headers.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_headers.py
new file mode 100644
index 0000000000..26d85fa5ea
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_headers.py
@@ -0,0 +1,185 @@
+import unittest
+
+from websockets.exceptions import InvalidHeaderFormat, InvalidHeaderValue
+from websockets.headers import *
+
+
+class HeadersTests(unittest.TestCase):
+ def test_parse_connection(self):
+ for header, parsed in [
+ # Realistic use cases
+ ("Upgrade", ["Upgrade"]), # Safari, Chrome
+ ("keep-alive, Upgrade", ["keep-alive", "Upgrade"]), # Firefox
+ # Pathological example
+ (",,\t, , ,Upgrade ,,", ["Upgrade"]),
+ ]:
+ with self.subTest(header=header):
+ self.assertEqual(parse_connection(header), parsed)
+
+ def test_parse_connection_invalid_header_format(self):
+ for header in ["???", "keep-alive; Upgrade"]:
+ with self.subTest(header=header):
+ with self.assertRaises(InvalidHeaderFormat):
+ parse_connection(header)
+
+ def test_parse_upgrade(self):
+ for header, parsed in [
+ # Realistic use case
+ ("websocket", ["websocket"]),
+ # Synthetic example
+ ("http/3.0, websocket", ["http/3.0", "websocket"]),
+ # Pathological example
+ (",, WebSocket, \t,,", ["WebSocket"]),
+ ]:
+ with self.subTest(header=header):
+ self.assertEqual(parse_upgrade(header), parsed)
+
+ def test_parse_upgrade_invalid_header_format(self):
+ for header in ["???", "websocket 2", "http/3.0; websocket"]:
+ with self.subTest(header=header):
+ with self.assertRaises(InvalidHeaderFormat):
+ parse_upgrade(header)
+
+ def test_parse_extension(self):
+ for header, parsed in [
+ # Synthetic examples
+ ("foo", [("foo", [])]),
+ ("foo, bar", [("foo", []), ("bar", [])]),
+ (
+ 'foo; name; token=token; quoted-string="quoted-string", '
+ "bar; quux; quuux",
+ [
+ (
+ "foo",
+ [
+ ("name", None),
+ ("token", "token"),
+ ("quoted-string", "quoted-string"),
+ ],
+ ),
+ ("bar", [("quux", None), ("quuux", None)]),
+ ],
+ ),
+ # Pathological example
+ (
+ ",\t, , ,foo ;bar = 42,, baz,,",
+ [("foo", [("bar", "42")]), ("baz", [])],
+ ),
+ # Realistic use cases for permessage-deflate
+ ("permessage-deflate", [("permessage-deflate", [])]),
+ (
+ "permessage-deflate; client_max_window_bits",
+ [("permessage-deflate", [("client_max_window_bits", None)])],
+ ),
+ (
+ "permessage-deflate; server_max_window_bits=10",
+ [("permessage-deflate", [("server_max_window_bits", "10")])],
+ ),
+ ]:
+ with self.subTest(header=header):
+ self.assertEqual(parse_extension(header), parsed)
+ # Also ensure that build_extension round-trips cleanly.
+ unparsed = build_extension(parsed)
+ self.assertEqual(parse_extension(unparsed), parsed)
+
+ def test_parse_extension_invalid_header_format(self):
+ for header in [
+ # Truncated examples
+ "",
+ ",\t,",
+ "foo;",
+ "foo; bar;",
+ "foo; bar=",
+ 'foo; bar="baz',
+ # Wrong delimiter
+ "foo, bar, baz=quux; quuux",
+ # Value in quoted string parameter that isn't a token
+ 'foo; bar=" "',
+ ]:
+ with self.subTest(header=header):
+ with self.assertRaises(InvalidHeaderFormat):
+ parse_extension(header)
+
+ def test_parse_subprotocol(self):
+ for header, parsed in [
+ # Synthetic examples
+ ("foo", ["foo"]),
+ ("foo, bar", ["foo", "bar"]),
+ # Pathological example
+ (",\t, , ,foo ,, bar,baz,,", ["foo", "bar", "baz"]),
+ ]:
+ with self.subTest(header=header):
+ self.assertEqual(parse_subprotocol(header), parsed)
+ # Also ensure that build_subprotocol round-trips cleanly.
+ unparsed = build_subprotocol(parsed)
+ self.assertEqual(parse_subprotocol(unparsed), parsed)
+
+ def test_parse_subprotocol_invalid_header(self):
+ for header in [
+ # Truncated examples
+ "",
+ ",\t,"
+ # Wrong delimiter
+ "foo; bar",
+ ]:
+ with self.subTest(header=header):
+ with self.assertRaises(InvalidHeaderFormat):
+ parse_subprotocol(header)
+
+ def test_build_www_authenticate_basic(self):
+ # Test vector from RFC 7617
+ self.assertEqual(
+ build_www_authenticate_basic("foo"), 'Basic realm="foo", charset="UTF-8"'
+ )
+
+ def test_build_www_authenticate_basic_invalid_realm(self):
+ # Realm contains a control character forbidden in quoted-string encoding
+ with self.assertRaises(ValueError):
+ build_www_authenticate_basic("\u0007")
+
+ def test_build_authorization_basic(self):
+ # Test vector from RFC 7617
+ self.assertEqual(
+ build_authorization_basic("Aladdin", "open sesame"),
+ "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
+ )
+
+ def test_build_authorization_basic_utf8(self):
+ # Test vector from RFC 7617
+ self.assertEqual(
+ build_authorization_basic("test", "123£"), "Basic dGVzdDoxMjPCow=="
+ )
+
+ def test_parse_authorization_basic(self):
+ for header, parsed in [
+ ("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", ("Aladdin", "open sesame")),
+ # Password contains non-ASCII character
+ ("Basic dGVzdDoxMjPCow==", ("test", "123£")),
+ # Password contains a colon
+ ("Basic YWxhZGRpbjpvcGVuOnNlc2FtZQ==", ("aladdin", "open:sesame")),
+ # Scheme name must be case insensitive
+ ("basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", ("Aladdin", "open sesame")),
+ ]:
+ with self.subTest(header=header):
+ self.assertEqual(parse_authorization_basic(header), parsed)
+
+ def test_parse_authorization_basic_invalid_header_format(self):
+ for header in [
+ "// Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
+ "Basic\tQWxhZGRpbjpvcGVuIHNlc2FtZQ==",
+ "Basic ****************************",
+ "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== //",
+ ]:
+ with self.subTest(header=header):
+ with self.assertRaises(InvalidHeaderFormat):
+ parse_authorization_basic(header)
+
+ def test_parse_authorization_basic_invalid_header_value(self):
+ for header in [
+ "Digest ...",
+ "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ",
+ "Basic QWxhZGNlc2FtZQ==",
+ ]:
+ with self.subTest(header=header):
+ with self.assertRaises(InvalidHeaderValue):
+ parse_authorization_basic(header)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_http.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_http.py
new file mode 100644
index 0000000000..41b522c3d5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_http.py
@@ -0,0 +1,249 @@
+import asyncio
+import unittest
+
+from websockets.exceptions import SecurityError
+from websockets.http import *
+from websockets.http import read_headers
+
+from .utils import AsyncioTestCase
+
+
+class HTTPAsyncTests(AsyncioTestCase):
+ def setUp(self):
+ super().setUp()
+ self.stream = asyncio.StreamReader(loop=self.loop)
+
+ async def test_read_request(self):
+ # Example from the protocol overview in RFC 6455
+ self.stream.feed_data(
+ b"GET /chat HTTP/1.1\r\n"
+ b"Host: server.example.com\r\n"
+ b"Upgrade: websocket\r\n"
+ b"Connection: Upgrade\r\n"
+ b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ b"Origin: http://example.com\r\n"
+ b"Sec-WebSocket-Protocol: chat, superchat\r\n"
+ b"Sec-WebSocket-Version: 13\r\n"
+ b"\r\n"
+ )
+ path, headers = await read_request(self.stream)
+ self.assertEqual(path, "/chat")
+ self.assertEqual(headers["Upgrade"], "websocket")
+
+ async def test_read_request_empty(self):
+ self.stream.feed_eof()
+ with self.assertRaisesRegex(
+ EOFError, "connection closed while reading HTTP request line"
+ ):
+ await read_request(self.stream)
+
+ async def test_read_request_invalid_request_line(self):
+ self.stream.feed_data(b"GET /\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "invalid HTTP request line: GET /"):
+ await read_request(self.stream)
+
+ async def test_read_request_unsupported_method(self):
+ self.stream.feed_data(b"OPTIONS * HTTP/1.1\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "unsupported HTTP method: OPTIONS"):
+ await read_request(self.stream)
+
+ async def test_read_request_unsupported_version(self):
+ self.stream.feed_data(b"GET /chat HTTP/1.0\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "unsupported HTTP version: HTTP/1.0"):
+ await read_request(self.stream)
+
+ async def test_read_request_invalid_header(self):
+ self.stream.feed_data(b"GET /chat HTTP/1.1\r\nOops\r\n")
+ with self.assertRaisesRegex(ValueError, "invalid HTTP header line: Oops"):
+ await read_request(self.stream)
+
+ async def test_read_response(self):
+ # Example from the protocol overview in RFC 6455
+ self.stream.feed_data(
+ b"HTTP/1.1 101 Switching Protocols\r\n"
+ b"Upgrade: websocket\r\n"
+ b"Connection: Upgrade\r\n"
+ b"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
+ b"Sec-WebSocket-Protocol: chat\r\n"
+ b"\r\n"
+ )
+ status_code, reason, headers = await read_response(self.stream)
+ self.assertEqual(status_code, 101)
+ self.assertEqual(reason, "Switching Protocols")
+ self.assertEqual(headers["Upgrade"], "websocket")
+
+ async def test_read_response_empty(self):
+ self.stream.feed_eof()
+ with self.assertRaisesRegex(
+ EOFError, "connection closed while reading HTTP status line"
+ ):
+ await read_response(self.stream)
+
+ async def test_read_request_invalid_status_line(self):
+ self.stream.feed_data(b"Hello!\r\n")
+ with self.assertRaisesRegex(ValueError, "invalid HTTP status line: Hello!"):
+ await read_response(self.stream)
+
+ async def test_read_response_unsupported_version(self):
+ self.stream.feed_data(b"HTTP/1.0 400 Bad Request\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "unsupported HTTP version: HTTP/1.0"):
+ await read_response(self.stream)
+
+ async def test_read_response_invalid_status(self):
+ self.stream.feed_data(b"HTTP/1.1 OMG WTF\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "invalid HTTP status code: OMG"):
+ await read_response(self.stream)
+
+ async def test_read_response_unsupported_status(self):
+ self.stream.feed_data(b"HTTP/1.1 007 My name is Bond\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "unsupported HTTP status code: 007"):
+ await read_response(self.stream)
+
+ async def test_read_response_invalid_reason(self):
+ self.stream.feed_data(b"HTTP/1.1 200 \x7f\r\n\r\n")
+ with self.assertRaisesRegex(ValueError, "invalid HTTP reason phrase: \\x7f"):
+ await read_response(self.stream)
+
+ async def test_read_response_invalid_header(self):
+ self.stream.feed_data(b"HTTP/1.1 500 Internal Server Error\r\nOops\r\n")
+ with self.assertRaisesRegex(ValueError, "invalid HTTP header line: Oops"):
+ await read_response(self.stream)
+
+ async def test_header_name(self):
+ self.stream.feed_data(b"foo bar: baz qux\r\n\r\n")
+ with self.assertRaises(ValueError):
+ await read_headers(self.stream)
+
+ async def test_header_value(self):
+ self.stream.feed_data(b"foo: \x00\x00\x0f\r\n\r\n")
+ with self.assertRaises(ValueError):
+ await read_headers(self.stream)
+
+ async def test_headers_limit(self):
+ self.stream.feed_data(b"foo: bar\r\n" * 257 + b"\r\n")
+ with self.assertRaises(SecurityError):
+ await read_headers(self.stream)
+
+ async def test_line_limit(self):
+ # Header line contains 5 + 4090 + 2 = 4097 bytes.
+ self.stream.feed_data(b"foo: " + b"a" * 4090 + b"\r\n\r\n")
+ with self.assertRaises(SecurityError):
+ await read_headers(self.stream)
+
+ async def test_line_ending(self):
+ self.stream.feed_data(b"foo: bar\n\n")
+ with self.assertRaises(EOFError):
+ await read_headers(self.stream)
+
+
+class HeadersTests(unittest.TestCase):
+ def setUp(self):
+ self.headers = Headers([("Connection", "Upgrade"), ("Server", USER_AGENT)])
+
+ def test_str(self):
+ self.assertEqual(
+ str(self.headers), f"Connection: Upgrade\r\nServer: {USER_AGENT}\r\n\r\n"
+ )
+
+ def test_repr(self):
+ self.assertEqual(
+ repr(self.headers),
+ f"Headers([('Connection', 'Upgrade'), " f"('Server', '{USER_AGENT}')])",
+ )
+
+ def test_multiple_values_error_str(self):
+ self.assertEqual(str(MultipleValuesError("Connection")), "'Connection'")
+ self.assertEqual(str(MultipleValuesError()), "")
+
+ def test_contains(self):
+ self.assertIn("Server", self.headers)
+
+ def test_contains_case_insensitive(self):
+ self.assertIn("server", self.headers)
+
+ def test_contains_not_found(self):
+ self.assertNotIn("Date", self.headers)
+
+ def test_contains_non_string_key(self):
+ self.assertNotIn(42, self.headers)
+
+ def test_iter(self):
+ self.assertEqual(set(iter(self.headers)), {"connection", "server"})
+
+ def test_len(self):
+ self.assertEqual(len(self.headers), 2)
+
+ def test_getitem(self):
+ self.assertEqual(self.headers["Server"], USER_AGENT)
+
+ def test_getitem_case_insensitive(self):
+ self.assertEqual(self.headers["server"], USER_AGENT)
+
+ def test_getitem_key_error(self):
+ with self.assertRaises(KeyError):
+ self.headers["Upgrade"]
+
+ def test_getitem_multiple_values_error(self):
+ self.headers["Server"] = "2"
+ with self.assertRaises(MultipleValuesError):
+ self.headers["Server"]
+
+ def test_setitem(self):
+ self.headers["Upgrade"] = "websocket"
+ self.assertEqual(self.headers["Upgrade"], "websocket")
+
+ def test_setitem_case_insensitive(self):
+ self.headers["upgrade"] = "websocket"
+ self.assertEqual(self.headers["Upgrade"], "websocket")
+
+ def test_setitem_multiple_values(self):
+ self.headers["Connection"] = "close"
+ with self.assertRaises(MultipleValuesError):
+ self.headers["Connection"]
+
+ def test_delitem(self):
+ del self.headers["Connection"]
+ with self.assertRaises(KeyError):
+ self.headers["Connection"]
+
+ def test_delitem_case_insensitive(self):
+ del self.headers["connection"]
+ with self.assertRaises(KeyError):
+ self.headers["Connection"]
+
+ def test_delitem_multiple_values(self):
+ self.headers["Connection"] = "close"
+ del self.headers["Connection"]
+ with self.assertRaises(KeyError):
+ self.headers["Connection"]
+
+ def test_eq(self):
+ other_headers = self.headers.copy()
+ self.assertEqual(self.headers, other_headers)
+
+ def test_eq_not_equal(self):
+ self.assertNotEqual(self.headers, [])
+
+ def test_clear(self):
+ self.headers.clear()
+ self.assertFalse(self.headers)
+ self.assertEqual(self.headers, Headers())
+
+ def test_get_all(self):
+ self.assertEqual(self.headers.get_all("Connection"), ["Upgrade"])
+
+ def test_get_all_case_insensitive(self):
+ self.assertEqual(self.headers.get_all("connection"), ["Upgrade"])
+
+ def test_get_all_no_values(self):
+ self.assertEqual(self.headers.get_all("Upgrade"), [])
+
+ def test_get_all_multiple_values(self):
+ self.headers["Connection"] = "close"
+ self.assertEqual(self.headers.get_all("Connection"), ["Upgrade", "close"])
+
+ def test_raw_items(self):
+ self.assertEqual(
+ list(self.headers.raw_items()),
+ [("Connection", "Upgrade"), ("Server", USER_AGENT)],
+ )
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.cnf b/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.cnf
new file mode 100644
index 0000000000..6dc331ac69
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.cnf
@@ -0,0 +1,26 @@
+[ req ]
+
+default_md = sha256
+encrypt_key = no
+
+prompt = no
+
+distinguished_name = dn
+x509_extensions = ext
+
+[ dn ]
+
+C = "FR"
+L = "Paris"
+O = "Aymeric Augustin"
+CN = "localhost"
+
+[ ext ]
+
+subjectAltName = @san
+
+[ san ]
+
+DNS.1 = localhost
+IP.2 = 127.0.0.1
+IP.3 = ::1
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.pem b/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.pem
new file mode 100644
index 0000000000..b8a9ea9ab3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_localhost.pem
@@ -0,0 +1,48 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCUgrQVkNbAWRlo
+zZUj14Ufz7YEp2MXmvmhdlfOGLwjy+xPO98aJRv5/nYF2eWM3llcmLe8FbBSK+QF
+To4su7ZVnc6qITOHqcSDUw06WarQUMs94bhHUvQp1u8+b2hNiMeGw6+QiBI6OJRO
+iGpLRbkN6Uj3AKwi8SYVoLyMiztuwbNyGf8fF3DDpHZtBitGtMSBCMsQsfB465pl
+2UoyBrWa2lsbLt3VvBZZvHqfEuPjpjjKN5USIXnaf0NizaR6ps3EyfftWy4i7zIQ
+N5uTExvaPDyPn9nH3q/dkT99mSMSU1AvTTpX8PN7DlqE6wZMbQsBPRGW7GElQ+Ox
+IKdKOLk5AgMBAAECggEAd3kqzQqnaTiEs4ZoC9yPUUc1pErQ8iWP27Ar9TZ67MVa
+B2ggFJV0C0sFwbFI9WnPNCn77gj4vzJmD0riH+SnS/tXThDFtscBu7BtvNp0C4Bj
+8RWMvXxjxuENuQnBPFbkRWtZ6wk8uK/Zx9AAyyt9M07Qjz1wPfAIdm/IH7zHBFMA
+gsqjnkLh1r0FvjNEbLiuGqYU/GVxaZYd+xy+JU52IxjHUUL9yD0BPWb+Szar6AM2
+gUpmTX6+BcCZwwZ//DzCoWYZ9JbP8akn6edBeZyuMPqYgLzZkPyQ+hRW46VPPw89
+yg4LR9nzgQiBHlac0laB4NrWa+d9QRRLitl1O3gVAQKBgQDDkptxXu7w9Lpc+HeE
+N/pJfpCzUuF7ZC4vatdoDzvfB5Ky6W88Poq+I7bB9m7StXdFAbDyUBxvisjTBMVA
+OtYqpAk/rhX8MjSAtjoFe2nH+eEiQriuZmtA5CdKEXS4hNbc/HhEPWhk7Zh8OV5v
+y7l4r6l4UHqaN9QyE0vlFdmcmQKBgQDCZZR/trJ2/g2OquaS+Zd2h/3NXw0NBq4z
+4OBEWqNa/R35jdK6WlWJH7+tKOacr+xtswLpPeZHGwMdk64/erbYWBuJWAjpH72J
+DM9+1H5fFHANWpWTNn94enQxwfzZRvdkxq4IWzGhesptYnHIzoAmaqC3lbn/e3u0
+Flng32hFoQKBgQCF3D4K3hib0lYQtnxPgmUMktWF+A+fflViXTWs4uhu4mcVkFNz
+n7clJ5q6reryzAQjtmGfqRedfRex340HRn46V2aBMK2Znd9zzcZu5CbmGnFvGs3/
+iNiWZNNDjike9sV+IkxLIODoW/vH4xhxWrbLFSjg0ezoy5ew4qZK2abF2QKBgQC5
+M5efeQpbjTyTUERtf/aKCZOGZmkDoPq0GCjxVjzNQdqd1z0NJ2TYR/QP36idXIlu
+FZ7PYZaS5aw5MGpQtfOe94n8dm++0et7t0WzunRO1yTNxCA+aSxWNquegAcJZa/q
+RdKlyWPmSRqzzZdDzWCPuQQ3AyF5wkYfUy/7qjwoIQKBgB2v96BV7+lICviIKzzb
+1o3A3VzAX5MGd98uLGjlK4qsBC+s7mk2eQztiNZgbA0W6fhQ5Dz3HcXJ5ppy8Okc
+jeAktrNRzz15hvi/XkWdO+VMqiHW4l+sWYukjhCyod1oO1KGHq0LYYvv076syxGw
+vRKLq7IJ4WIp1VtfaBlrIogq
+-----END PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIDTTCCAjWgAwIBAgIJAJ6VG2cQlsepMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV
+BAYTAkZSMQ4wDAYDVQQHDAVQYXJpczEZMBcGA1UECgwQQXltZXJpYyBBdWd1c3Rp
+bjESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE4MDUwNTE2NTc1NloYDzIwNjAwNTA0
+MTY1NzU2WjBMMQswCQYDVQQGEwJGUjEOMAwGA1UEBwwFUGFyaXMxGTAXBgNVBAoM
+EEF5bWVyaWMgQXVndXN0aW4xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAJSCtBWQ1sBZGWjNlSPXhR/PtgSnYxea+aF2
+V84YvCPL7E873xolG/n+dgXZ5YzeWVyYt7wVsFIr5AVOjiy7tlWdzqohM4epxINT
+DTpZqtBQyz3huEdS9CnW7z5vaE2Ix4bDr5CIEjo4lE6IaktFuQ3pSPcArCLxJhWg
+vIyLO27Bs3IZ/x8XcMOkdm0GK0a0xIEIyxCx8HjrmmXZSjIGtZraWxsu3dW8Flm8
+ep8S4+OmOMo3lRIhedp/Q2LNpHqmzcTJ9+1bLiLvMhA3m5MTG9o8PI+f2cfer92R
+P32ZIxJTUC9NOlfw83sOWoTrBkxtCwE9EZbsYSVD47Egp0o4uTkCAwEAAaMwMC4w
+LAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0G
+CSqGSIb3DQEBCwUAA4IBAQA0imKp/rflfbDCCx78NdsR5rt0jKem2t3YPGT6tbeU
++FQz62SEdeD2OHWxpvfPf+6h3iTXJbkakr2R4lP3z7GHUe61lt3So9VHAvgbtPTH
+aB1gOdThA83o0fzQtnIv67jCvE9gwPQInViZLEcm2iQEZLj6AuSvBKmluTR7vNRj
+8/f2R4LsDfCWGrzk2W+deGRvSow7irS88NQ8BW8S8otgMiBx4D2UlOmQwqr6X+/r
+jYIDuMb6GDKRXtBUGDokfE94hjj9u2mrNRwt8y4tqu8ZNa//yLEQ0Ow2kP3QJPLY
+941VZpwRi2v/+JvI7OBYlvbOTFwM8nAk79k+Dgviygd9
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_protocol.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_protocol.py
new file mode 100644
index 0000000000..d32c1f72e7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_protocol.py
@@ -0,0 +1,1475 @@
+import asyncio
+import contextlib
+import sys
+import unittest
+import unittest.mock
+import warnings
+
+from websockets.exceptions import ConnectionClosed, InvalidState
+from websockets.framing import *
+from websockets.protocol import State, WebSocketCommonProtocol
+
+from .utils import MS, AsyncioTestCase
+
+
+async def async_iterable(iterable):
+ for item in iterable:
+ yield item
+
+
+class TransportMock(unittest.mock.Mock):
+ """
+ Transport mock to control the protocol's inputs and outputs in tests.
+
+ It calls the protocol's connection_made and connection_lost methods like
+ actual transports.
+
+ It also calls the protocol's connection_open method to bypass the
+ WebSocket handshake.
+
+ To simulate incoming data, tests call the protocol's data_received and
+ eof_received methods directly.
+
+ They could also pause_writing and resume_writing to test flow control.
+
+ """
+
+ # This should happen in __init__ but overriding Mock.__init__ is hard.
+ def setup_mock(self, loop, protocol):
+ self.loop = loop
+ self.protocol = protocol
+ self._eof = False
+ self._closing = False
+ # Simulate a successful TCP handshake.
+ self.protocol.connection_made(self)
+ # Simulate a successful WebSocket handshake.
+ self.protocol.connection_open()
+
+ def can_write_eof(self):
+ return True
+
+ def write_eof(self):
+ # When the protocol half-closes the TCP connection, it expects the
+ # other end to close it. Simulate that.
+ if not self._eof:
+ self.loop.call_soon(self.close)
+ self._eof = True
+
+ def close(self):
+ # Simulate how actual transports drop the connection.
+ if not self._closing:
+ self.loop.call_soon(self.protocol.connection_lost, None)
+ self._closing = True
+
+ def abort(self):
+ # Change this to an `if` if tests call abort() multiple times.
+ assert self.protocol.state is not State.CLOSED
+ self.loop.call_soon(self.protocol.connection_lost, None)
+
+
+class CommonTests:
+ """
+ Mixin that defines most tests but doesn't inherit unittest.TestCase.
+
+ Tests are run by the ServerTests and ClientTests subclasses.
+
+ """
+
+ def setUp(self):
+ super().setUp()
+ # Disable pings to make it easier to test what frames are sent exactly.
+ self.protocol = WebSocketCommonProtocol(ping_interval=None)
+ self.transport = TransportMock()
+ self.transport.setup_mock(self.loop, self.protocol)
+
+ def tearDown(self):
+ self.transport.close()
+ self.loop.run_until_complete(self.protocol.close())
+ super().tearDown()
+
+ # Utilities for writing tests.
+
+ def make_drain_slow(self, delay=MS):
+ # Process connection_made in order to initialize self.protocol.transport.
+ self.run_loop_once()
+
+ original_drain = self.protocol._drain
+
+ async def delayed_drain():
+ await asyncio.sleep(
+ delay, loop=self.loop if sys.version_info[:2] < (3, 8) else None
+ )
+ await original_drain()
+
+ self.protocol._drain = delayed_drain
+
+ close_frame = Frame(True, OP_CLOSE, serialize_close(1000, "close"))
+ local_close = Frame(True, OP_CLOSE, serialize_close(1000, "local"))
+ remote_close = Frame(True, OP_CLOSE, serialize_close(1000, "remote"))
+
+ def receive_frame(self, frame):
+ """
+ Make the protocol receive a frame.
+
+ """
+ write = self.protocol.data_received
+ mask = not self.protocol.is_client
+ frame.write(write, mask=mask)
+
+ def receive_eof(self):
+ """
+ Make the protocol receive the end of the data stream.
+
+ Since ``WebSocketCommonProtocol.eof_received`` returns ``None``, an
+ actual transport would close itself after calling it. This function
+ emulates that behavior.
+
+ """
+ self.protocol.eof_received()
+ self.loop.call_soon(self.transport.close)
+
+ def receive_eof_if_client(self):
+ """
+ Like receive_eof, but only if this is the client side.
+
+ Since the server is supposed to initiate the termination of the TCP
+ connection, this method helps making tests work for both sides.
+
+ """
+ if self.protocol.is_client:
+ self.receive_eof()
+
+ def close_connection(self, code=1000, reason="close"):
+ """
+ Execute a closing handshake.
+
+ This puts the connection in the CLOSED state.
+
+ """
+ close_frame_data = serialize_close(code, reason)
+ # Prepare the response to the closing handshake from the remote side.
+ self.receive_frame(Frame(True, OP_CLOSE, close_frame_data))
+ self.receive_eof_if_client()
+ # Trigger the closing handshake from the local side and complete it.
+ self.loop.run_until_complete(self.protocol.close(code, reason))
+ # Empty the outgoing data stream so we can make assertions later on.
+ self.assertOneFrameSent(True, OP_CLOSE, close_frame_data)
+
+ assert self.protocol.state is State.CLOSED
+
+ def half_close_connection_local(self, code=1000, reason="close"):
+ """
+ Start a closing handshake but do not complete it.
+
+ The main difference with `close_connection` is that the connection is
+ left in the CLOSING state until the event loop runs again.
+
+ The current implementation returns a task that must be awaited or
+ canceled, else asyncio complains about destroying a pending task.
+
+ """
+ close_frame_data = serialize_close(code, reason)
+ # Trigger the closing handshake from the local endpoint.
+ close_task = self.loop.create_task(self.protocol.close(code, reason))
+ self.run_loop_once() # wait_for executes
+ self.run_loop_once() # write_frame executes
+ # Empty the outgoing data stream so we can make assertions later on.
+ self.assertOneFrameSent(True, OP_CLOSE, close_frame_data)
+
+ assert self.protocol.state is State.CLOSING
+
+ # Complete the closing sequence at 1ms intervals so the test can run
+ # at each point even it goes back to the event loop several times.
+ self.loop.call_later(
+ MS, self.receive_frame, Frame(True, OP_CLOSE, close_frame_data)
+ )
+ self.loop.call_later(2 * MS, self.receive_eof_if_client)
+
+ # This task must be awaited or canceled by the caller.
+ return close_task
+
+ def half_close_connection_remote(self, code=1000, reason="close"):
+ """
+ Receive a closing handshake but do not complete it.
+
+ The main difference with `close_connection` is that the connection is
+ left in the CLOSING state until the event loop runs again.
+
+ """
+ # On the server side, websockets completes the closing handshake and
+ # closes the TCP connection immediately. Yield to the event loop after
+ # sending the close frame to run the test while the connection is in
+ # the CLOSING state.
+ if not self.protocol.is_client:
+ self.make_drain_slow()
+
+ close_frame_data = serialize_close(code, reason)
+ # Trigger the closing handshake from the remote endpoint.
+ self.receive_frame(Frame(True, OP_CLOSE, close_frame_data))
+ self.run_loop_once() # read_frame executes
+ # Empty the outgoing data stream so we can make assertions later on.
+ self.assertOneFrameSent(True, OP_CLOSE, close_frame_data)
+
+ assert self.protocol.state is State.CLOSING
+
+ # Complete the closing sequence at 1ms intervals so the test can run
+ # at each point even it goes back to the event loop several times.
+ self.loop.call_later(2 * MS, self.receive_eof_if_client)
+
+ def process_invalid_frames(self):
+ """
+ Make the protocol fail quickly after simulating invalid data.
+
+ To achieve this, this function triggers the protocol's eof_received,
+ which interrupts pending reads waiting for more data.
+
+ """
+ self.run_loop_once()
+ self.receive_eof()
+ self.loop.run_until_complete(self.protocol.close_connection_task)
+
+ def sent_frames(self):
+ """
+ Read all frames sent to the transport.
+
+ """
+ stream = asyncio.StreamReader(loop=self.loop)
+
+ for (data,), kw in self.transport.write.call_args_list:
+ stream.feed_data(data)
+ self.transport.write.call_args_list = []
+ stream.feed_eof()
+
+ frames = []
+ while not stream.at_eof():
+ frames.append(
+ self.loop.run_until_complete(
+ Frame.read(stream.readexactly, mask=self.protocol.is_client)
+ )
+ )
+ return frames
+
+ def last_sent_frame(self):
+ """
+ Read the last frame sent to the transport.
+
+ This method assumes that at most one frame was sent. It raises an
+ AssertionError otherwise.
+
+ """
+ frames = self.sent_frames()
+ if frames:
+ assert len(frames) == 1
+ return frames[0]
+
+ def assertFramesSent(self, *frames):
+ self.assertEqual(self.sent_frames(), [Frame(*args) for args in frames])
+
+ def assertOneFrameSent(self, *args):
+ self.assertEqual(self.last_sent_frame(), Frame(*args))
+
+ def assertNoFrameSent(self):
+ self.assertIsNone(self.last_sent_frame())
+
+ def assertConnectionClosed(self, code, message):
+ # The following line guarantees that connection_lost was called.
+ self.assertEqual(self.protocol.state, State.CLOSED)
+ # A close frame was received.
+ self.assertEqual(self.protocol.close_code, code)
+ self.assertEqual(self.protocol.close_reason, message)
+
+ def assertConnectionFailed(self, code, message):
+ # The following line guarantees that connection_lost was called.
+ self.assertEqual(self.protocol.state, State.CLOSED)
+ # No close frame was received.
+ self.assertEqual(self.protocol.close_code, 1006)
+ self.assertEqual(self.protocol.close_reason, "")
+ # A close frame was sent -- unless the connection was already lost.
+ if code == 1006:
+ self.assertNoFrameSent()
+ else:
+ self.assertOneFrameSent(True, OP_CLOSE, serialize_close(code, message))
+
+ @contextlib.contextmanager
+ def assertCompletesWithin(self, min_time, max_time):
+ t0 = self.loop.time()
+ yield
+ t1 = self.loop.time()
+ dt = t1 - t0
+ self.assertGreaterEqual(dt, min_time, f"Too fast: {dt} < {min_time}")
+ self.assertLess(dt, max_time, f"Too slow: {dt} >= {max_time}")
+
+ # Test constructor.
+
+ def test_timeout_backwards_compatibility(self):
+ with warnings.catch_warnings(record=True) as recorded_warnings:
+ protocol = WebSocketCommonProtocol(timeout=5)
+
+ self.assertEqual(protocol.close_timeout, 5)
+
+ self.assertEqual(len(recorded_warnings), 1)
+ warning = recorded_warnings[0].message
+ self.assertEqual(str(warning), "rename timeout to close_timeout")
+ self.assertEqual(type(warning), DeprecationWarning)
+
+ # Test public attributes.
+
+ def test_local_address(self):
+ get_extra_info = unittest.mock.Mock(return_value=("host", 4312))
+ self.transport.get_extra_info = get_extra_info
+
+ self.assertEqual(self.protocol.local_address, ("host", 4312))
+ get_extra_info.assert_called_with("sockname")
+
+ def test_local_address_before_connection(self):
+ # Emulate the situation before connection_open() runs.
+ _transport = self.protocol.transport
+ del self.protocol.transport
+ try:
+ self.assertEqual(self.protocol.local_address, None)
+ finally:
+ self.protocol.transport = _transport
+
+ def test_remote_address(self):
+ get_extra_info = unittest.mock.Mock(return_value=("host", 4312))
+ self.transport.get_extra_info = get_extra_info
+
+ self.assertEqual(self.protocol.remote_address, ("host", 4312))
+ get_extra_info.assert_called_with("peername")
+
+ def test_remote_address_before_connection(self):
+ # Emulate the situation before connection_open() runs.
+ _transport = self.protocol.transport
+ del self.protocol.transport
+ try:
+ self.assertEqual(self.protocol.remote_address, None)
+ finally:
+ self.protocol.transport = _transport
+
+ def test_open(self):
+ self.assertTrue(self.protocol.open)
+ self.close_connection()
+ self.assertFalse(self.protocol.open)
+
+ def test_closed(self):
+ self.assertFalse(self.protocol.closed)
+ self.close_connection()
+ self.assertTrue(self.protocol.closed)
+
+ def test_wait_closed(self):
+ wait_closed = self.loop.create_task(self.protocol.wait_closed())
+ self.assertFalse(wait_closed.done())
+ self.close_connection()
+ self.assertTrue(wait_closed.done())
+
+ # Test the recv coroutine.
+
+ def test_recv_text(self):
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8")))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, "café")
+
+ def test_recv_binary(self):
+ self.receive_frame(Frame(True, OP_BINARY, b"tea"))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, b"tea")
+
+ def test_recv_on_closing_connection_local(self):
+ close_task = self.half_close_connection_local()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.recv())
+
+ self.loop.run_until_complete(close_task) # cleanup
+
+ def test_recv_on_closing_connection_remote(self):
+ self.half_close_connection_remote()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.recv())
+
+ def test_recv_on_closed_connection(self):
+ self.close_connection()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.recv())
+
+ def test_recv_protocol_error(self):
+ self.receive_frame(Frame(True, OP_CONT, "café".encode("utf-8")))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(1002, "")
+
+ def test_recv_unicode_error(self):
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("latin-1")))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(1007, "")
+
+ def test_recv_text_payload_too_big(self):
+ self.protocol.max_size = 1024
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8") * 205))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(1009, "")
+
+ def test_recv_binary_payload_too_big(self):
+ self.protocol.max_size = 1024
+ self.receive_frame(Frame(True, OP_BINARY, b"tea" * 342))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(1009, "")
+
+ def test_recv_text_no_max_size(self):
+ self.protocol.max_size = None # for test coverage
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8") * 205))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, "café" * 205)
+
+ def test_recv_binary_no_max_size(self):
+ self.protocol.max_size = None # for test coverage
+ self.receive_frame(Frame(True, OP_BINARY, b"tea" * 342))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, b"tea" * 342)
+
+ def test_recv_queue_empty(self):
+ recv = self.loop.create_task(self.protocol.recv())
+ with self.assertRaises(asyncio.TimeoutError):
+ self.loop.run_until_complete(
+ asyncio.wait_for(asyncio.shield(recv), timeout=MS)
+ )
+
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8")))
+ data = self.loop.run_until_complete(recv)
+ self.assertEqual(data, "café")
+
+ def test_recv_queue_full(self):
+ self.protocol.max_queue = 2
+ # Test internals because it's hard to verify buffers from the outside.
+ self.assertEqual(list(self.protocol.messages), [])
+
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8")))
+ self.run_loop_once()
+ self.assertEqual(list(self.protocol.messages), ["café"])
+
+ self.receive_frame(Frame(True, OP_BINARY, b"tea"))
+ self.run_loop_once()
+ self.assertEqual(list(self.protocol.messages), ["café", b"tea"])
+
+ self.receive_frame(Frame(True, OP_BINARY, b"milk"))
+ self.run_loop_once()
+ self.assertEqual(list(self.protocol.messages), ["café", b"tea"])
+
+ self.loop.run_until_complete(self.protocol.recv())
+ self.run_loop_once()
+ self.assertEqual(list(self.protocol.messages), [b"tea", b"milk"])
+
+ self.loop.run_until_complete(self.protocol.recv())
+ self.run_loop_once()
+ self.assertEqual(list(self.protocol.messages), [b"milk"])
+
+ self.loop.run_until_complete(self.protocol.recv())
+ self.run_loop_once()
+ self.assertEqual(list(self.protocol.messages), [])
+
+ def test_recv_queue_no_limit(self):
+ self.protocol.max_queue = None
+
+ for _ in range(100):
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8")))
+ self.run_loop_once()
+
+ # Incoming message queue can contain at least 100 messages.
+ self.assertEqual(list(self.protocol.messages), ["café"] * 100)
+
+ for _ in range(100):
+ self.loop.run_until_complete(self.protocol.recv())
+
+ self.assertEqual(list(self.protocol.messages), [])
+
+ def test_recv_other_error(self):
+ async def read_message():
+ raise Exception("BOOM")
+
+ self.protocol.read_message = read_message
+ self.process_invalid_frames()
+ self.assertConnectionFailed(1011, "")
+
+ def test_recv_canceled(self):
+ recv = self.loop.create_task(self.protocol.recv())
+ self.loop.call_soon(recv.cancel)
+
+ with self.assertRaises(asyncio.CancelledError):
+ self.loop.run_until_complete(recv)
+
+ # The next frame doesn't disappear in a vacuum (it used to).
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8")))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, "café")
+
+ def test_recv_canceled_race_condition(self):
+ recv = self.loop.create_task(
+ asyncio.wait_for(self.protocol.recv(), timeout=0.000_001)
+ )
+ self.loop.call_soon(
+ self.receive_frame, Frame(True, OP_TEXT, "café".encode("utf-8"))
+ )
+
+ with self.assertRaises(asyncio.TimeoutError):
+ self.loop.run_until_complete(recv)
+
+ # The previous frame doesn't disappear in a vacuum (it used to).
+ self.receive_frame(Frame(True, OP_TEXT, "tea".encode("utf-8")))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ # If we're getting "tea" there, it means "café" was swallowed (ha, ha).
+ self.assertEqual(data, "café")
+
+ def test_recv_when_transfer_data_cancelled(self):
+ # Clog incoming queue.
+ self.protocol.max_queue = 1
+ self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8")))
+ self.receive_frame(Frame(True, OP_BINARY, b"tea"))
+ self.run_loop_once()
+
+ # Flow control kicks in (check with an implementation detail).
+ self.assertFalse(self.protocol._put_message_waiter.done())
+
+ # Schedule recv().
+ recv = self.loop.create_task(self.protocol.recv())
+
+ # Cancel transfer_data_task (again, implementation detail).
+ self.protocol.fail_connection()
+ self.run_loop_once()
+ self.assertTrue(self.protocol.transfer_data_task.cancelled())
+
+ # recv() completes properly.
+ self.assertEqual(self.loop.run_until_complete(recv), "café")
+
+ def test_recv_prevents_concurrent_calls(self):
+ recv = self.loop.create_task(self.protocol.recv())
+
+ with self.assertRaisesRegex(
+ RuntimeError,
+ "cannot call recv while another coroutine "
+ "is already waiting for the next message",
+ ):
+ self.loop.run_until_complete(self.protocol.recv())
+
+ recv.cancel()
+
+ # Test the send coroutine.
+
+ def test_send_text(self):
+ self.loop.run_until_complete(self.protocol.send("café"))
+ self.assertOneFrameSent(True, OP_TEXT, "café".encode("utf-8"))
+
+ def test_send_binary(self):
+ self.loop.run_until_complete(self.protocol.send(b"tea"))
+ self.assertOneFrameSent(True, OP_BINARY, b"tea")
+
+ def test_send_binary_from_bytearray(self):
+ self.loop.run_until_complete(self.protocol.send(bytearray(b"tea")))
+ self.assertOneFrameSent(True, OP_BINARY, b"tea")
+
+ def test_send_binary_from_memoryview(self):
+ self.loop.run_until_complete(self.protocol.send(memoryview(b"tea")))
+ self.assertOneFrameSent(True, OP_BINARY, b"tea")
+
+ def test_send_binary_from_non_contiguous_memoryview(self):
+ self.loop.run_until_complete(self.protocol.send(memoryview(b"tteeaa")[::2]))
+ self.assertOneFrameSent(True, OP_BINARY, b"tea")
+
+ def test_send_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(self.protocol.send(42))
+ self.assertNoFrameSent()
+
+ def test_send_iterable_text(self):
+ self.loop.run_until_complete(self.protocol.send(["ca", "fé"]))
+ self.assertFramesSent(
+ (False, OP_TEXT, "ca".encode("utf-8")),
+ (False, OP_CONT, "fé".encode("utf-8")),
+ (True, OP_CONT, "".encode("utf-8")),
+ )
+
+ def test_send_iterable_binary(self):
+ self.loop.run_until_complete(self.protocol.send([b"te", b"a"]))
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_iterable_binary_from_bytearray(self):
+ self.loop.run_until_complete(
+ self.protocol.send([bytearray(b"te"), bytearray(b"a")])
+ )
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_iterable_binary_from_memoryview(self):
+ self.loop.run_until_complete(
+ self.protocol.send([memoryview(b"te"), memoryview(b"a")])
+ )
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_iterable_binary_from_non_contiguous_memoryview(self):
+ self.loop.run_until_complete(
+ self.protocol.send([memoryview(b"ttee")[::2], memoryview(b"aa")[::2]])
+ )
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_empty_iterable(self):
+ self.loop.run_until_complete(self.protocol.send([]))
+ self.assertNoFrameSent()
+
+ def test_send_iterable_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(self.protocol.send([42]))
+ self.assertNoFrameSent()
+
+ def test_send_iterable_mixed_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(self.protocol.send(["café", b"tea"]))
+ self.assertFramesSent(
+ (False, OP_TEXT, "café".encode("utf-8")),
+ (True, OP_CLOSE, serialize_close(1011, "")),
+ )
+
+ def test_send_iterable_prevents_concurrent_send(self):
+ self.make_drain_slow(2 * MS)
+
+ async def send_iterable():
+ await self.protocol.send(["ca", "fé"])
+
+ async def send_concurrent():
+ await asyncio.sleep(MS)
+ await self.protocol.send(b"tea")
+
+ self.loop.run_until_complete(asyncio.gather(send_iterable(), send_concurrent()))
+ self.assertFramesSent(
+ (False, OP_TEXT, "ca".encode("utf-8")),
+ (False, OP_CONT, "fé".encode("utf-8")),
+ (True, OP_CONT, "".encode("utf-8")),
+ (True, OP_BINARY, b"tea"),
+ )
+
+ def test_send_async_iterable_text(self):
+ self.loop.run_until_complete(self.protocol.send(async_iterable(["ca", "fé"])))
+ self.assertFramesSent(
+ (False, OP_TEXT, "ca".encode("utf-8")),
+ (False, OP_CONT, "fé".encode("utf-8")),
+ (True, OP_CONT, "".encode("utf-8")),
+ )
+
+ def test_send_async_iterable_binary(self):
+ self.loop.run_until_complete(self.protocol.send(async_iterable([b"te", b"a"])))
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_async_iterable_binary_from_bytearray(self):
+ self.loop.run_until_complete(
+ self.protocol.send(async_iterable([bytearray(b"te"), bytearray(b"a")]))
+ )
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_async_iterable_binary_from_memoryview(self):
+ self.loop.run_until_complete(
+ self.protocol.send(async_iterable([memoryview(b"te"), memoryview(b"a")]))
+ )
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_async_iterable_binary_from_non_contiguous_memoryview(self):
+ self.loop.run_until_complete(
+ self.protocol.send(
+ async_iterable([memoryview(b"ttee")[::2], memoryview(b"aa")[::2]])
+ )
+ )
+ self.assertFramesSent(
+ (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"")
+ )
+
+ def test_send_empty_async_iterable(self):
+ self.loop.run_until_complete(self.protocol.send(async_iterable([])))
+ self.assertNoFrameSent()
+
+ def test_send_async_iterable_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(self.protocol.send(async_iterable([42])))
+ self.assertNoFrameSent()
+
+ def test_send_async_iterable_mixed_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(
+ self.protocol.send(async_iterable(["café", b"tea"]))
+ )
+ self.assertFramesSent(
+ (False, OP_TEXT, "café".encode("utf-8")),
+ (True, OP_CLOSE, serialize_close(1011, "")),
+ )
+
+ def test_send_async_iterable_prevents_concurrent_send(self):
+ self.make_drain_slow(2 * MS)
+
+ async def send_async_iterable():
+ await self.protocol.send(async_iterable(["ca", "fé"]))
+
+ async def send_concurrent():
+ await asyncio.sleep(MS)
+ await self.protocol.send(b"tea")
+
+ self.loop.run_until_complete(
+ asyncio.gather(send_async_iterable(), send_concurrent())
+ )
+ self.assertFramesSent(
+ (False, OP_TEXT, "ca".encode("utf-8")),
+ (False, OP_CONT, "fé".encode("utf-8")),
+ (True, OP_CONT, "".encode("utf-8")),
+ (True, OP_BINARY, b"tea"),
+ )
+
+ def test_send_on_closing_connection_local(self):
+ close_task = self.half_close_connection_local()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.send("foobar"))
+
+ self.assertNoFrameSent()
+
+ self.loop.run_until_complete(close_task) # cleanup
+
+ def test_send_on_closing_connection_remote(self):
+ self.half_close_connection_remote()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.send("foobar"))
+
+ self.assertNoFrameSent()
+
+ def test_send_on_closed_connection(self):
+ self.close_connection()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.send("foobar"))
+
+ self.assertNoFrameSent()
+
+ # Test the ping coroutine.
+
+ def test_ping_default(self):
+ self.loop.run_until_complete(self.protocol.ping())
+ # With our testing tools, it's more convenient to extract the expected
+ # ping data from the library's internals than from the frame sent.
+ ping_data = next(iter(self.protocol.pings))
+ self.assertIsInstance(ping_data, bytes)
+ self.assertEqual(len(ping_data), 4)
+ self.assertOneFrameSent(True, OP_PING, ping_data)
+
+ def test_ping_text(self):
+ self.loop.run_until_complete(self.protocol.ping("café"))
+ self.assertOneFrameSent(True, OP_PING, "café".encode("utf-8"))
+
+ def test_ping_binary(self):
+ self.loop.run_until_complete(self.protocol.ping(b"tea"))
+ self.assertOneFrameSent(True, OP_PING, b"tea")
+
+ def test_ping_binary_from_bytearray(self):
+ self.loop.run_until_complete(self.protocol.ping(bytearray(b"tea")))
+ self.assertOneFrameSent(True, OP_PING, b"tea")
+
+ def test_ping_binary_from_memoryview(self):
+ self.loop.run_until_complete(self.protocol.ping(memoryview(b"tea")))
+ self.assertOneFrameSent(True, OP_PING, b"tea")
+
+ def test_ping_binary_from_non_contiguous_memoryview(self):
+ self.loop.run_until_complete(self.protocol.ping(memoryview(b"tteeaa")[::2]))
+ self.assertOneFrameSent(True, OP_PING, b"tea")
+
+ def test_ping_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(self.protocol.ping(42))
+ self.assertNoFrameSent()
+
+ def test_ping_on_closing_connection_local(self):
+ close_task = self.half_close_connection_local()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.ping())
+
+ self.assertNoFrameSent()
+
+ self.loop.run_until_complete(close_task) # cleanup
+
+ def test_ping_on_closing_connection_remote(self):
+ self.half_close_connection_remote()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.ping())
+
+ self.assertNoFrameSent()
+
+ def test_ping_on_closed_connection(self):
+ self.close_connection()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.ping())
+
+ self.assertNoFrameSent()
+
+ # Test the pong coroutine.
+
+ def test_pong_default(self):
+ self.loop.run_until_complete(self.protocol.pong())
+ self.assertOneFrameSent(True, OP_PONG, b"")
+
+ def test_pong_text(self):
+ self.loop.run_until_complete(self.protocol.pong("café"))
+ self.assertOneFrameSent(True, OP_PONG, "café".encode("utf-8"))
+
+ def test_pong_binary(self):
+ self.loop.run_until_complete(self.protocol.pong(b"tea"))
+ self.assertOneFrameSent(True, OP_PONG, b"tea")
+
+ def test_pong_binary_from_bytearray(self):
+ self.loop.run_until_complete(self.protocol.pong(bytearray(b"tea")))
+ self.assertOneFrameSent(True, OP_PONG, b"tea")
+
+ def test_pong_binary_from_memoryview(self):
+ self.loop.run_until_complete(self.protocol.pong(memoryview(b"tea")))
+ self.assertOneFrameSent(True, OP_PONG, b"tea")
+
+ def test_pong_binary_from_non_contiguous_memoryview(self):
+ self.loop.run_until_complete(self.protocol.pong(memoryview(b"tteeaa")[::2]))
+ self.assertOneFrameSent(True, OP_PONG, b"tea")
+
+ def test_pong_type_error(self):
+ with self.assertRaises(TypeError):
+ self.loop.run_until_complete(self.protocol.pong(42))
+ self.assertNoFrameSent()
+
+ def test_pong_on_closing_connection_local(self):
+ close_task = self.half_close_connection_local()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.pong())
+
+ self.assertNoFrameSent()
+
+ self.loop.run_until_complete(close_task) # cleanup
+
+ def test_pong_on_closing_connection_remote(self):
+ self.half_close_connection_remote()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.pong())
+
+ self.assertNoFrameSent()
+
+ def test_pong_on_closed_connection(self):
+ self.close_connection()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.pong())
+
+ self.assertNoFrameSent()
+
+ # Test the protocol's logic for acknowledging pings with pongs.
+
+ def test_answer_ping(self):
+ self.receive_frame(Frame(True, OP_PING, b"test"))
+ self.run_loop_once()
+ self.assertOneFrameSent(True, OP_PONG, b"test")
+
+ def test_ignore_pong(self):
+ self.receive_frame(Frame(True, OP_PONG, b"test"))
+ self.run_loop_once()
+ self.assertNoFrameSent()
+
+ def test_acknowledge_ping(self):
+ ping = self.loop.run_until_complete(self.protocol.ping())
+ self.assertFalse(ping.done())
+ ping_frame = self.last_sent_frame()
+ pong_frame = Frame(True, OP_PONG, ping_frame.data)
+ self.receive_frame(pong_frame)
+ self.run_loop_once()
+ self.run_loop_once()
+ self.assertTrue(ping.done())
+
+ def test_abort_ping(self):
+ ping = self.loop.run_until_complete(self.protocol.ping())
+ # Remove the frame from the buffer, else close_connection() complains.
+ self.last_sent_frame()
+ self.assertFalse(ping.done())
+ self.close_connection()
+ self.assertTrue(ping.done())
+ self.assertIsInstance(ping.exception(), ConnectionClosed)
+
+ def test_abort_ping_does_not_log_exception_if_not_retreived(self):
+ self.loop.run_until_complete(self.protocol.ping())
+ # Get the internal Future, which isn't directly returned by ping().
+ (ping,) = self.protocol.pings.values()
+ # Remove the frame from the buffer, else close_connection() complains.
+ self.last_sent_frame()
+ self.close_connection()
+ # Check a private attribute, for lack of a better solution.
+ self.assertFalse(ping._log_traceback)
+
+ def test_acknowledge_previous_pings(self):
+ pings = [
+ (self.loop.run_until_complete(self.protocol.ping()), self.last_sent_frame())
+ for i in range(3)
+ ]
+ # Unsolicited pong doesn't acknowledge pings
+ self.receive_frame(Frame(True, OP_PONG, b""))
+ self.run_loop_once()
+ self.run_loop_once()
+ self.assertFalse(pings[0][0].done())
+ self.assertFalse(pings[1][0].done())
+ self.assertFalse(pings[2][0].done())
+ # Pong acknowledges all previous pings
+ self.receive_frame(Frame(True, OP_PONG, pings[1][1].data))
+ self.run_loop_once()
+ self.run_loop_once()
+ self.assertTrue(pings[0][0].done())
+ self.assertTrue(pings[1][0].done())
+ self.assertFalse(pings[2][0].done())
+
+ def test_acknowledge_aborted_ping(self):
+ ping = self.loop.run_until_complete(self.protocol.ping())
+ ping_frame = self.last_sent_frame()
+ # Clog incoming queue. This lets connection_lost() abort pending pings
+ # with a ConnectionClosed exception before transfer_data_task
+ # terminates and close_connection cancels keepalive_ping_task.
+ self.protocol.max_queue = 1
+ self.receive_frame(Frame(True, OP_TEXT, b"1"))
+ self.receive_frame(Frame(True, OP_TEXT, b"2"))
+ # Add pong frame to the queue.
+ pong_frame = Frame(True, OP_PONG, ping_frame.data)
+ self.receive_frame(pong_frame)
+ # Connection drops.
+ self.receive_eof()
+ self.loop.run_until_complete(self.protocol.wait_closed())
+ # Ping receives a ConnectionClosed exception.
+ with self.assertRaises(ConnectionClosed):
+ ping.result()
+
+ # transfer_data doesn't crash, which would be logged.
+ with self.assertNoLogs():
+ # Unclog incoming queue.
+ self.loop.run_until_complete(self.protocol.recv())
+ self.loop.run_until_complete(self.protocol.recv())
+
+ def test_canceled_ping(self):
+ ping = self.loop.run_until_complete(self.protocol.ping())
+ ping_frame = self.last_sent_frame()
+ ping.cancel()
+ pong_frame = Frame(True, OP_PONG, ping_frame.data)
+ self.receive_frame(pong_frame)
+ self.run_loop_once()
+ self.run_loop_once()
+ self.assertTrue(ping.cancelled())
+
+ def test_duplicate_ping(self):
+ self.loop.run_until_complete(self.protocol.ping(b"foobar"))
+ self.assertOneFrameSent(True, OP_PING, b"foobar")
+ with self.assertRaises(ValueError):
+ self.loop.run_until_complete(self.protocol.ping(b"foobar"))
+ self.assertNoFrameSent()
+
+ # Test the protocol's logic for rebuilding fragmented messages.
+
+ def test_fragmented_text(self):
+ self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8")))
+ self.receive_frame(Frame(True, OP_CONT, "fé".encode("utf-8")))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, "café")
+
+ def test_fragmented_binary(self):
+ self.receive_frame(Frame(False, OP_BINARY, b"t"))
+ self.receive_frame(Frame(False, OP_CONT, b"e"))
+ self.receive_frame(Frame(True, OP_CONT, b"a"))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, b"tea")
+
+ def test_fragmented_text_payload_too_big(self):
+ self.protocol.max_size = 1024
+ self.receive_frame(Frame(False, OP_TEXT, "café".encode("utf-8") * 100))
+ self.receive_frame(Frame(True, OP_CONT, "café".encode("utf-8") * 105))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(1009, "")
+
+ def test_fragmented_binary_payload_too_big(self):
+ self.protocol.max_size = 1024
+ self.receive_frame(Frame(False, OP_BINARY, b"tea" * 171))
+ self.receive_frame(Frame(True, OP_CONT, b"tea" * 171))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(1009, "")
+
+ def test_fragmented_text_no_max_size(self):
+ self.protocol.max_size = None # for test coverage
+ self.receive_frame(Frame(False, OP_TEXT, "café".encode("utf-8") * 100))
+ self.receive_frame(Frame(True, OP_CONT, "café".encode("utf-8") * 105))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, "café" * 205)
+
+ def test_fragmented_binary_no_max_size(self):
+ self.protocol.max_size = None # for test coverage
+ self.receive_frame(Frame(False, OP_BINARY, b"tea" * 171))
+ self.receive_frame(Frame(True, OP_CONT, b"tea" * 171))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, b"tea" * 342)
+
+ def test_control_frame_within_fragmented_text(self):
+ self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8")))
+ self.receive_frame(Frame(True, OP_PING, b""))
+ self.receive_frame(Frame(True, OP_CONT, "fé".encode("utf-8")))
+ data = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(data, "café")
+ self.assertOneFrameSent(True, OP_PONG, b"")
+
+ def test_unterminated_fragmented_text(self):
+ self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8")))
+ # Missing the second part of the fragmented frame.
+ self.receive_frame(Frame(True, OP_BINARY, b"tea"))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(1002, "")
+
+ def test_close_handshake_in_fragmented_text(self):
+ self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8")))
+ self.receive_frame(Frame(True, OP_CLOSE, b""))
+ self.process_invalid_frames()
+ # The RFC may have overlooked this case: it says that control frames
+ # can be interjected in the middle of a fragmented message and that a
+ # close frame must be echoed. Even though there's an unterminated
+ # message, technically, the closing handshake was successful.
+ self.assertConnectionClosed(1005, "")
+
+ def test_connection_close_in_fragmented_text(self):
+ self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8")))
+ self.process_invalid_frames()
+ self.assertConnectionFailed(1006, "")
+
+ # Test miscellaneous code paths to ensure full coverage.
+
+ def test_connection_lost(self):
+ # Test calling connection_lost without going through close_connection.
+ self.protocol.connection_lost(None)
+
+ self.assertConnectionFailed(1006, "")
+
+ def test_ensure_open_before_opening_handshake(self):
+ # Simulate a bug by forcibly reverting the protocol state.
+ self.protocol.state = State.CONNECTING
+
+ with self.assertRaises(InvalidState):
+ self.loop.run_until_complete(self.protocol.ensure_open())
+
+ def test_ensure_open_during_unclean_close(self):
+ # Process connection_made in order to start transfer_data_task.
+ self.run_loop_once()
+
+ # Ensure the test terminates quickly.
+ self.loop.call_later(MS, self.receive_eof_if_client)
+
+ # Simulate the case when close() times out sending a close frame.
+ self.protocol.fail_connection()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.ensure_open())
+
+ def test_legacy_recv(self):
+ # By default legacy_recv in disabled.
+ self.assertEqual(self.protocol.legacy_recv, False)
+
+ self.close_connection()
+
+ # Enable legacy_recv.
+ self.protocol.legacy_recv = True
+
+ # Now recv() returns None instead of raising ConnectionClosed.
+ self.assertIsNone(self.loop.run_until_complete(self.protocol.recv()))
+
+ def test_connection_closed_attributes(self):
+ self.close_connection()
+
+ with self.assertRaises(ConnectionClosed) as context:
+ self.loop.run_until_complete(self.protocol.recv())
+
+ connection_closed_exc = context.exception
+ self.assertEqual(connection_closed_exc.code, 1000)
+ self.assertEqual(connection_closed_exc.reason, "close")
+
+ # Test the protocol logic for sending keepalive pings.
+
+ def restart_protocol_with_keepalive_ping(
+ self, ping_interval=3 * MS, ping_timeout=3 * MS
+ ):
+ initial_protocol = self.protocol
+ # copied from tearDown
+ self.transport.close()
+ self.loop.run_until_complete(self.protocol.close())
+ # copied from setUp, but enables keepalive pings
+ self.protocol = WebSocketCommonProtocol(
+ ping_interval=ping_interval, ping_timeout=ping_timeout
+ )
+ self.transport = TransportMock()
+ self.transport.setup_mock(self.loop, self.protocol)
+ self.protocol.is_client = initial_protocol.is_client
+ self.protocol.side = initial_protocol.side
+
+ def test_keepalive_ping(self):
+ self.restart_protocol_with_keepalive_ping()
+
+ # Ping is sent at 3ms and acknowledged at 4ms.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ (ping_1,) = tuple(self.protocol.pings)
+ self.assertOneFrameSent(True, OP_PING, ping_1)
+ self.receive_frame(Frame(True, OP_PONG, ping_1))
+
+ # Next ping is sent at 7ms.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ (ping_2,) = tuple(self.protocol.pings)
+ self.assertOneFrameSent(True, OP_PING, ping_2)
+
+ # The keepalive ping task goes on.
+ self.assertFalse(self.protocol.keepalive_ping_task.done())
+
+ def test_keepalive_ping_not_acknowledged_closes_connection(self):
+ self.restart_protocol_with_keepalive_ping()
+
+ # Ping is sent at 3ms and not acknowleged.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ (ping_1,) = tuple(self.protocol.pings)
+ self.assertOneFrameSent(True, OP_PING, ping_1)
+
+ # Connection is closed at 6ms.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ self.assertOneFrameSent(True, OP_CLOSE, serialize_close(1011, ""))
+
+ # The keepalive ping task is complete.
+ self.assertEqual(self.protocol.keepalive_ping_task.result(), None)
+
+ def test_keepalive_ping_stops_when_connection_closing(self):
+ self.restart_protocol_with_keepalive_ping()
+ close_task = self.half_close_connection_local()
+
+ # No ping sent at 3ms because the closing handshake is in progress.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ self.assertNoFrameSent()
+
+ # The keepalive ping task terminated.
+ self.assertTrue(self.protocol.keepalive_ping_task.cancelled())
+
+ self.loop.run_until_complete(close_task) # cleanup
+
+ def test_keepalive_ping_stops_when_connection_closed(self):
+ self.restart_protocol_with_keepalive_ping()
+ self.close_connection()
+
+ # The keepalive ping task terminated.
+ self.assertTrue(self.protocol.keepalive_ping_task.cancelled())
+
+ def test_keepalive_ping_does_not_crash_when_connection_lost(self):
+ self.restart_protocol_with_keepalive_ping()
+ # Clog incoming queue. This lets connection_lost() abort pending pings
+ # with a ConnectionClosed exception before transfer_data_task
+ # terminates and close_connection cancels keepalive_ping_task.
+ self.protocol.max_queue = 1
+ self.receive_frame(Frame(True, OP_TEXT, b"1"))
+ self.receive_frame(Frame(True, OP_TEXT, b"2"))
+ # Ping is sent at 3ms.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ (ping_waiter,) = tuple(self.protocol.pings.values())
+ # Connection drops.
+ self.receive_eof()
+ self.loop.run_until_complete(self.protocol.wait_closed())
+
+ # The ping waiter receives a ConnectionClosed exception.
+ with self.assertRaises(ConnectionClosed):
+ ping_waiter.result()
+ # The keepalive ping task terminated properly.
+ self.assertIsNone(self.protocol.keepalive_ping_task.result())
+
+ # Unclog incoming queue to terminate the test quickly.
+ self.loop.run_until_complete(self.protocol.recv())
+ self.loop.run_until_complete(self.protocol.recv())
+
+ def test_keepalive_ping_with_no_ping_interval(self):
+ self.restart_protocol_with_keepalive_ping(ping_interval=None)
+
+ # No ping is sent at 3ms.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ self.assertNoFrameSent()
+
+ def test_keepalive_ping_with_no_ping_timeout(self):
+ self.restart_protocol_with_keepalive_ping(ping_timeout=None)
+
+ # Ping is sent at 3ms and not acknowleged.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ (ping_1,) = tuple(self.protocol.pings)
+ self.assertOneFrameSent(True, OP_PING, ping_1)
+
+ # Next ping is sent at 7ms anyway.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+ ping_1_again, ping_2 = tuple(self.protocol.pings)
+ self.assertEqual(ping_1, ping_1_again)
+ self.assertOneFrameSent(True, OP_PING, ping_2)
+
+ # The keepalive ping task goes on.
+ self.assertFalse(self.protocol.keepalive_ping_task.done())
+
+ def test_keepalive_ping_unexpected_error(self):
+ self.restart_protocol_with_keepalive_ping()
+
+ async def ping():
+ raise Exception("BOOM")
+
+ self.protocol.ping = ping
+
+ # The keepalive ping task fails when sending a ping at 3ms.
+ self.loop.run_until_complete(asyncio.sleep(4 * MS))
+
+ # The keepalive ping task is complete.
+ # It logs and swallows the exception.
+ self.assertEqual(self.protocol.keepalive_ping_task.result(), None)
+
+ # Test the protocol logic for closing the connection.
+
+ def test_local_close(self):
+ # Emulate how the remote endpoint answers the closing handshake.
+ self.loop.call_later(MS, self.receive_frame, self.close_frame)
+ self.loop.call_later(MS, self.receive_eof_if_client)
+
+ # Run the closing handshake.
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+
+ self.assertConnectionClosed(1000, "close")
+ self.assertOneFrameSent(*self.close_frame)
+
+ # Closing the connection again is a no-op.
+ self.loop.run_until_complete(self.protocol.close(reason="oh noes!"))
+
+ self.assertConnectionClosed(1000, "close")
+ self.assertNoFrameSent()
+
+ def test_remote_close(self):
+ # Emulate how the remote endpoint initiates the closing handshake.
+ self.loop.call_later(MS, self.receive_frame, self.close_frame)
+ self.loop.call_later(MS, self.receive_eof_if_client)
+
+ # Wait for some data in order to process the handshake.
+ # After recv() raises ConnectionClosed, the connection is closed.
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(self.protocol.recv())
+
+ self.assertConnectionClosed(1000, "close")
+ self.assertOneFrameSent(*self.close_frame)
+
+ # Closing the connection again is a no-op.
+ self.loop.run_until_complete(self.protocol.close(reason="oh noes!"))
+
+ self.assertConnectionClosed(1000, "close")
+ self.assertNoFrameSent()
+
+ def test_remote_close_and_connection_lost(self):
+ self.make_drain_slow()
+ # Drop the connection right after receiving a close frame,
+ # which prevents echoing the close frame properly.
+ self.receive_frame(self.close_frame)
+ self.receive_eof()
+
+ with self.assertNoLogs():
+ self.loop.run_until_complete(self.protocol.close(reason="oh noes!"))
+
+ self.assertConnectionClosed(1000, "close")
+ self.assertOneFrameSent(*self.close_frame)
+
+ def test_simultaneous_close(self):
+ # Receive the incoming close frame right after self.protocol.close()
+ # starts executing. This reproduces the error described in:
+ # https://github.com/aaugustin/websockets/issues/339
+ self.loop.call_soon(self.receive_frame, self.remote_close)
+ self.loop.call_soon(self.receive_eof_if_client)
+
+ self.loop.run_until_complete(self.protocol.close(reason="local"))
+
+ self.assertConnectionClosed(1000, "remote")
+ # The current implementation sends a close frame in response to the
+ # close frame received from the remote end. It skips the close frame
+ # that should be sent as a result of calling close().
+ self.assertOneFrameSent(*self.remote_close)
+
+ def test_close_preserves_incoming_frames(self):
+ self.receive_frame(Frame(True, OP_TEXT, b"hello"))
+
+ self.loop.call_later(MS, self.receive_frame, self.close_frame)
+ self.loop.call_later(MS, self.receive_eof_if_client)
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+
+ self.assertConnectionClosed(1000, "close")
+ self.assertOneFrameSent(*self.close_frame)
+
+ next_message = self.loop.run_until_complete(self.protocol.recv())
+ self.assertEqual(next_message, "hello")
+
+ def test_close_protocol_error(self):
+ invalid_close_frame = Frame(True, OP_CLOSE, b"\x00")
+ self.receive_frame(invalid_close_frame)
+ self.receive_eof_if_client()
+ self.run_loop_once()
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+
+ self.assertConnectionFailed(1002, "")
+
+ def test_close_connection_lost(self):
+ self.receive_eof()
+ self.run_loop_once()
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+
+ self.assertConnectionFailed(1006, "")
+
+ def test_local_close_during_recv(self):
+ recv = self.loop.create_task(self.protocol.recv())
+
+ self.loop.call_later(MS, self.receive_frame, self.close_frame)
+ self.loop.call_later(MS, self.receive_eof_if_client)
+
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(recv)
+
+ self.assertConnectionClosed(1000, "close")
+
+ # There is no test_remote_close_during_recv because it would be identical
+ # to test_remote_close.
+
+ def test_remote_close_during_send(self):
+ self.make_drain_slow()
+ send = self.loop.create_task(self.protocol.send("hello"))
+
+ self.receive_frame(self.close_frame)
+ self.receive_eof()
+
+ with self.assertRaises(ConnectionClosed):
+ self.loop.run_until_complete(send)
+
+ self.assertConnectionClosed(1000, "close")
+
+ # There is no test_local_close_during_send because this cannot really
+ # happen, considering that writes are serialized.
+
+
+class ServerTests(CommonTests, AsyncioTestCase):
+ def setUp(self):
+ super().setUp()
+ self.protocol.is_client = False
+ self.protocol.side = "server"
+
+ def test_local_close_send_close_frame_timeout(self):
+ self.protocol.close_timeout = 10 * MS
+ self.make_drain_slow(50 * MS)
+ # If we can't send a close frame, time out in 10ms.
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(9 * MS, 19 * MS):
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ self.assertConnectionClosed(1006, "")
+
+ def test_local_close_receive_close_frame_timeout(self):
+ self.protocol.close_timeout = 10 * MS
+ # If the client doesn't send a close frame, time out in 10ms.
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(9 * MS, 19 * MS):
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ self.assertConnectionClosed(1006, "")
+
+ def test_local_close_connection_lost_timeout_after_write_eof(self):
+ self.protocol.close_timeout = 10 * MS
+ # If the client doesn't close its side of the TCP connection after we
+ # half-close our side with write_eof(), time out in 10ms.
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(9 * MS, 19 * MS):
+ # HACK: disable write_eof => other end drops connection emulation.
+ self.transport._eof = True
+ self.receive_frame(self.close_frame)
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ self.assertConnectionClosed(1000, "close")
+
+ def test_local_close_connection_lost_timeout_after_close(self):
+ self.protocol.close_timeout = 10 * MS
+ # If the client doesn't close its side of the TCP connection after we
+ # half-close our side with write_eof() and close it with close(), time
+ # out in 20ms.
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(19 * MS, 29 * MS):
+ # HACK: disable write_eof => other end drops connection emulation.
+ self.transport._eof = True
+ # HACK: disable close => other end drops connection emulation.
+ self.transport._closing = True
+ self.receive_frame(self.close_frame)
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ self.assertConnectionClosed(1000, "close")
+
+
+class ClientTests(CommonTests, AsyncioTestCase):
+ def setUp(self):
+ super().setUp()
+ self.protocol.is_client = True
+ self.protocol.side = "client"
+
+ def test_local_close_send_close_frame_timeout(self):
+ self.protocol.close_timeout = 10 * MS
+ self.make_drain_slow(50 * MS)
+ # If we can't send a close frame, time out in 20ms.
+ # - 10ms waiting for sending a close frame
+ # - 10ms waiting for receiving a half-close
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(19 * MS, 29 * MS):
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ self.assertConnectionClosed(1006, "")
+
+ def test_local_close_receive_close_frame_timeout(self):
+ self.protocol.close_timeout = 10 * MS
+ # If the server doesn't send a close frame, time out in 20ms:
+ # - 10ms waiting for receiving a close frame
+ # - 10ms waiting for receiving a half-close
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(19 * MS, 29 * MS):
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ self.assertConnectionClosed(1006, "")
+
+ def test_local_close_connection_lost_timeout_after_write_eof(self):
+ self.protocol.close_timeout = 10 * MS
+ # If the server doesn't half-close its side of the TCP connection
+ # after we send a close frame, time out in 20ms:
+ # - 10ms waiting for receiving a half-close
+ # - 10ms waiting for receiving a close after write_eof
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(19 * MS, 29 * MS):
+ # HACK: disable write_eof => other end drops connection emulation.
+ self.transport._eof = True
+ self.receive_frame(self.close_frame)
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ self.assertConnectionClosed(1000, "close")
+
+ def test_local_close_connection_lost_timeout_after_close(self):
+ self.protocol.close_timeout = 10 * MS
+ # If the client doesn't close its side of the TCP connection after we
+ # half-close our side with write_eof() and close it with close(), time
+ # out in 20ms.
+ # - 10ms waiting for receiving a half-close
+ # - 10ms waiting for receiving a close after write_eof
+ # - 10ms waiting for receiving a close after close
+ # Check the timing within -1/+9ms for robustness.
+ with self.assertCompletesWithin(29 * MS, 39 * MS):
+ # HACK: disable write_eof => other end drops connection emulation.
+ self.transport._eof = True
+ # HACK: disable close => other end drops connection emulation.
+ self.transport._closing = True
+ self.receive_frame(self.close_frame)
+ self.loop.run_until_complete(self.protocol.close(reason="close"))
+ self.assertConnectionClosed(1000, "close")
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_uri.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_uri.py
new file mode 100644
index 0000000000..e41860b8e4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_uri.py
@@ -0,0 +1,33 @@
+import unittest
+
+from websockets.exceptions import InvalidURI
+from websockets.uri import *
+
+
+VALID_URIS = [
+ ("ws://localhost/", (False, "localhost", 80, "/", None)),
+ ("wss://localhost/", (True, "localhost", 443, "/", None)),
+ ("ws://localhost/path?query", (False, "localhost", 80, "/path?query", None)),
+ ("WS://LOCALHOST/PATH?QUERY", (False, "localhost", 80, "/PATH?QUERY", None)),
+ ("ws://user:pass@localhost/", (False, "localhost", 80, "/", ("user", "pass"))),
+]
+
+INVALID_URIS = [
+ "http://localhost/",
+ "https://localhost/",
+ "ws://localhost/path#fragment",
+ "ws://user@localhost/",
+]
+
+
+class URITests(unittest.TestCase):
+ def test_success(self):
+ for uri, parsed in VALID_URIS:
+ with self.subTest(uri=uri):
+ self.assertEqual(parse_uri(uri), parsed)
+
+ def test_error(self):
+ for uri in INVALID_URIS:
+ with self.subTest(uri=uri):
+ with self.assertRaises(InvalidURI):
+ parse_uri(uri)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/test_utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/test_utils.py
new file mode 100644
index 0000000000..e5570f098b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/test_utils.py
@@ -0,0 +1,92 @@
+import itertools
+import unittest
+
+from websockets.utils import apply_mask as py_apply_mask
+
+
+class UtilsTests(unittest.TestCase):
+ @staticmethod
+ def apply_mask(*args, **kwargs):
+ return py_apply_mask(*args, **kwargs)
+
+ apply_mask_type_combos = list(itertools.product([bytes, bytearray], repeat=2))
+
+ apply_mask_test_values = [
+ (b"", b"1234", b""),
+ (b"aBcDe", b"\x00\x00\x00\x00", b"aBcDe"),
+ (b"abcdABCD", b"1234", b"PPPPpppp"),
+ (b"abcdABCD" * 10, b"1234", b"PPPPpppp" * 10),
+ ]
+
+ def test_apply_mask(self):
+ for data_type, mask_type in self.apply_mask_type_combos:
+ for data_in, mask, data_out in self.apply_mask_test_values:
+ data_in, mask = data_type(data_in), mask_type(mask)
+
+ with self.subTest(data_in=data_in, mask=mask):
+ result = self.apply_mask(data_in, mask)
+ self.assertEqual(result, data_out)
+
+ def test_apply_mask_memoryview(self):
+ for data_type, mask_type in self.apply_mask_type_combos:
+ for data_in, mask, data_out in self.apply_mask_test_values:
+ data_in, mask = data_type(data_in), mask_type(mask)
+ data_in, mask = memoryview(data_in), memoryview(mask)
+
+ with self.subTest(data_in=data_in, mask=mask):
+ result = self.apply_mask(data_in, mask)
+ self.assertEqual(result, data_out)
+
+ def test_apply_mask_non_contiguous_memoryview(self):
+ for data_type, mask_type in self.apply_mask_type_combos:
+ for data_in, mask, data_out in self.apply_mask_test_values:
+ data_in, mask = data_type(data_in), mask_type(mask)
+ data_in, mask = memoryview(data_in), memoryview(mask)
+ data_in, mask = data_in[::-1], mask[::-1]
+ data_out = data_out[::-1]
+
+ with self.subTest(data_in=data_in, mask=mask):
+ result = self.apply_mask(data_in, mask)
+ self.assertEqual(result, data_out)
+
+ def test_apply_mask_check_input_types(self):
+ for data_in, mask in [(None, None), (b"abcd", None), (None, b"abcd")]:
+ with self.subTest(data_in=data_in, mask=mask):
+ with self.assertRaises(TypeError):
+ self.apply_mask(data_in, mask)
+
+ def test_apply_mask_check_mask_length(self):
+ for data_in, mask in [
+ (b"", b""),
+ (b"abcd", b"123"),
+ (b"", b"aBcDe"),
+ (b"12345678", b"12345678"),
+ ]:
+ with self.subTest(data_in=data_in, mask=mask):
+ with self.assertRaises(ValueError):
+ self.apply_mask(data_in, mask)
+
+
+try:
+ from websockets.speedups import apply_mask as c_apply_mask
+except ImportError: # pragma: no cover
+ pass
+else:
+
+ class SpeedupsTests(UtilsTests):
+ @staticmethod
+ def apply_mask(*args, **kwargs):
+ return c_apply_mask(*args, **kwargs)
+
+ def test_apply_mask_non_contiguous_memoryview(self):
+ for data_type, mask_type in self.apply_mask_type_combos:
+ for data_in, mask, data_out in self.apply_mask_test_values:
+ data_in, mask = data_type(data_in), mask_type(mask)
+ data_in, mask = memoryview(data_in), memoryview(mask)
+ data_in, mask = data_in[::-1], mask[::-1]
+ data_out = data_out[::-1]
+
+ with self.subTest(data_in=data_in, mask=mask):
+ # The C extension only supports contiguous memoryviews.
+ with self.assertRaises(TypeError):
+ self.apply_mask(data_in, mask)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tests/utils.py b/testing/web-platform/tests/tools/third_party/websockets/tests/utils.py
new file mode 100644
index 0000000000..983a91edf0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tests/utils.py
@@ -0,0 +1,93 @@
+import asyncio
+import contextlib
+import functools
+import logging
+import os
+import time
+import unittest
+
+
+class AsyncioTestCase(unittest.TestCase):
+ """
+ Base class for tests that sets up an isolated event loop for each test.
+
+ """
+
+ def __init_subclass__(cls, **kwargs):
+ """
+ Convert test coroutines to test functions.
+
+ This supports asychronous tests transparently.
+
+ """
+ super().__init_subclass__(**kwargs)
+ for name in unittest.defaultTestLoader.getTestCaseNames(cls):
+ test = getattr(cls, name)
+ if asyncio.iscoroutinefunction(test):
+ setattr(cls, name, cls.convert_async_to_sync(test))
+
+ @staticmethod
+ def convert_async_to_sync(test):
+ """
+ Convert a test coroutine to a test function.
+
+ """
+
+ @functools.wraps(test)
+ def test_func(self, *args, **kwargs):
+ return self.loop.run_until_complete(test(self, *args, **kwargs))
+
+ return test_func
+
+ def setUp(self):
+ super().setUp()
+ self.loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self.loop)
+
+ def tearDown(self):
+ self.loop.close()
+ super().tearDown()
+
+ def run_loop_once(self):
+ # Process callbacks scheduled with call_soon by appending a callback
+ # to stop the event loop then running it until it hits that callback.
+ self.loop.call_soon(self.loop.stop)
+ self.loop.run_forever()
+
+ @contextlib.contextmanager
+ def assertNoLogs(self, logger="websockets", level=logging.ERROR):
+ """
+ No message is logged on the given logger with at least the given level.
+
+ """
+ with self.assertLogs(logger, level) as logs:
+ # We want to test that no log message is emitted
+ # but assertLogs expects at least one log message.
+ logging.getLogger(logger).log(level, "dummy")
+ yield
+
+ level_name = logging.getLevelName(level)
+ self.assertEqual(logs.output, [f"{level_name}:{logger}:dummy"])
+
+ def assertDeprecationWarnings(self, recorded_warnings, expected_warnings):
+ """
+ Check recorded deprecation warnings match a list of expected messages.
+
+ """
+ self.assertEqual(len(recorded_warnings), len(expected_warnings))
+ for recorded, expected in zip(recorded_warnings, expected_warnings):
+ actual = recorded.message
+ self.assertEqual(str(actual), expected)
+ self.assertEqual(type(actual), DeprecationWarning)
+
+
+# Unit for timeouts. May be increased on slow machines by setting the
+# WEBSOCKETS_TESTS_TIMEOUT_FACTOR environment variable.
+MS = 0.001 * int(os.environ.get("WEBSOCKETS_TESTS_TIMEOUT_FACTOR", 1))
+
+# asyncio's debug mode has a 10x performance penalty for this test suite.
+if os.environ.get("PYTHONASYNCIODEBUG"): # pragma: no cover
+ MS *= 10
+
+# Ensure that timeouts are larger than the clock's resolution (for Windows).
+MS = max(MS, 2.5 * time.get_clock_info("monotonic").resolution)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/tox.ini b/testing/web-platform/tests/tools/third_party/websockets/tox.ini
new file mode 100644
index 0000000000..825e34061f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/tox.ini
@@ -0,0 +1,28 @@
+[tox]
+envlist = py36,py37,py38,coverage,black,flake8,isort,mypy
+
+[testenv]
+commands = python -W default -m unittest {posargs}
+
+[testenv:coverage]
+commands =
+ python -m coverage erase
+ python -W default -m coverage run -m unittest {posargs}
+ python -m coverage report --show-missing --fail-under=100
+deps = coverage
+
+[testenv:black]
+commands = black --check src tests
+deps = black
+
+[testenv:flake8]
+commands = flake8 src tests
+deps = flake8
+
+[testenv:isort]
+commands = isort --check-only --recursive src tests
+deps = isort
+
+[testenv:mypy]
+commands = mypy --strict src
+deps = mypy
diff --git a/testing/web-platform/tests/tools/third_party/zipp/.flake8 b/testing/web-platform/tests/tools/third_party/zipp/.flake8
new file mode 100644
index 0000000000..790c109fdb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/.flake8
@@ -0,0 +1,9 @@
+[flake8]
+max-line-length = 88
+ignore =
+ # W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513
+ W503
+ # W504 has issues https://github.com/OCA/maintainer-quality-tools/issues/545
+ W504
+ # Black creates whitespace before colon
+ E203
diff --git a/testing/web-platform/tests/tools/third_party/zipp/.github/workflows/main.yml b/testing/web-platform/tests/tools/third_party/zipp/.github/workflows/main.yml
new file mode 100644
index 0000000000..8c5c232c36
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/.github/workflows/main.yml
@@ -0,0 +1,42 @@
+name: Automated Tests
+
+on: [push, pull_request]
+
+jobs:
+ test:
+ strategy:
+ matrix:
+ python: [3.6, 3.8, 3.9]
+ platform: [ubuntu-latest, macos-latest, windows-latest]
+ runs-on: ${{ matrix.platform }}
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python }}
+ - name: Install tox
+ run: |
+ python -m pip install tox
+ - name: Run tests
+ run: tox
+
+ release:
+ needs: test
+ if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.9
+ - name: Install tox
+ run: |
+ python -m pip install tox
+ - name: Release
+ run: tox -e release
+ env:
+ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/testing/web-platform/tests/tools/third_party/zipp/.pre-commit-config.yaml b/testing/web-platform/tests/tools/third_party/zipp/.pre-commit-config.yaml
new file mode 100644
index 0000000000..922d94247a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/.pre-commit-config.yaml
@@ -0,0 +1,5 @@
+repos:
+- repo: https://github.com/ambv/black
+ rev: 18.9b0
+ hooks:
+ - id: black
diff --git a/testing/web-platform/tests/tools/third_party/zipp/.readthedocs.yml b/testing/web-platform/tests/tools/third_party/zipp/.readthedocs.yml
new file mode 100644
index 0000000000..8ae4468428
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/.readthedocs.yml
@@ -0,0 +1,5 @@
+python:
+ version: 3
+ extra_requirements:
+ - docs
+ pip_install: true
diff --git a/testing/web-platform/tests/tools/third_party/zipp/.travis.yml b/testing/web-platform/tests/tools/third_party/zipp/.travis.yml
new file mode 100644
index 0000000000..b7d8f3ac9d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/.travis.yml
@@ -0,0 +1,28 @@
+dist: xenial
+language: python
+
+python:
+- 2.7
+- 3.6
+- &latest_py3 3.8
+
+jobs:
+ fast_finish: true
+ include:
+ - stage: deploy
+ if: tag IS present
+ python: *latest_py3
+ before_script: skip
+ script: tox -e release
+
+cache: pip
+
+install:
+- pip install tox tox-venv
+
+before_script:
+ # Disable IPv6. Ref travis-ci/travis-ci#8361
+ - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then
+ sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6';
+ fi
+script: tox
diff --git a/testing/web-platform/tests/tools/third_party/zipp/CHANGES.rst b/testing/web-platform/tests/tools/third_party/zipp/CHANGES.rst
new file mode 100644
index 0000000000..a464a6324b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/CHANGES.rst
@@ -0,0 +1,100 @@
+v1.2.0
+======
+
+#44: ``zipp.Path.open()`` now supports a compatible signature
+as ``pathlib.Path.open()``, accepting text (default) or binary
+modes and soliciting keyword parameters passed through to
+``io.TextIOWrapper`` (encoding, newline, etc). The stream is
+opened in text-mode by default now. ``open`` no
+longer accepts ``pwd`` as a positional argument and does not
+accept the ``force_zip64`` parameter at all. This change is
+a backward-incompatible change for that single function.
+
+v1.1.1
+======
+
+#43: Restored performance of implicit dir computation.
+
+v1.1.0
+======
+
+#32: For read-only zip files, complexity of ``.exists`` and
+``joinpath`` is now constant time instead of ``O(n)``, preventing
+quadratic time in common use-cases and rendering large
+zip files unusable for Path. Big thanks to Benjy Weinberger
+for the bug report and contributed fix (#33).
+
+v1.0.0
+======
+
+Re-release of 0.6 to correspond with release as found in
+Python 3.8.
+
+v0.6.0
+======
+
+#12: When adding implicit dirs, ensure that ancestral directories
+are added and that duplicates are excluded.
+
+The library now relies on
+`more_itertools <https://pypi.org/project/more_itertools>`_.
+
+v0.5.2
+======
+
+#7: Parent of a directory now actually returns the parent.
+
+v0.5.1
+======
+
+Declared package as backport.
+
+v0.5.0
+======
+
+Add ``.joinpath()`` method and ``.parent`` property.
+
+Now a backport release of the ``zipfile.Path`` class.
+
+v0.4.0
+======
+
+#4: Add support for zip files with implied directories.
+
+v0.3.3
+======
+
+#3: Fix issue where ``.name`` on a directory was empty.
+
+v0.3.2
+======
+
+#2: Fix TypeError on Python 2.7 when classic division is used.
+
+v0.3.1
+======
+
+#1: Fix TypeError on Python 3.5 when joining to a path-like object.
+
+v0.3.0
+======
+
+Add support for constructing a ``zipp.Path`` from any path-like
+object.
+
+``zipp.Path`` is now a new-style class on Python 2.7.
+
+v0.2.1
+======
+
+Fix issue with ``__str__``.
+
+v0.2.0
+======
+
+Drop reliance on future-fstrings.
+
+v0.1.0
+======
+
+Initial release with basic functionality.
diff --git a/testing/web-platform/tests/tools/third_party/zipp/LICENSE b/testing/web-platform/tests/tools/third_party/zipp/LICENSE
new file mode 100644
index 0000000000..5e795a61f3
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/LICENSE
@@ -0,0 +1,7 @@
+Copyright Jason R. Coombs
+
+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/testing/web-platform/tests/tools/third_party/zipp/PKG-INFO b/testing/web-platform/tests/tools/third_party/zipp/PKG-INFO
new file mode 100644
index 0000000000..33ef1cf01c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/PKG-INFO
@@ -0,0 +1,39 @@
+Metadata-Version: 2.1
+Name: zipp
+Version: 1.2.0
+Summary: Backport of pathlib-compatible object wrapper for zip files
+Home-page: https://github.com/jaraco/zipp
+Author: Jason R. Coombs
+Author-email: jaraco@jaraco.com
+License: UNKNOWN
+Description: .. image:: https://img.shields.io/pypi/v/zipp.svg
+ :target: https://pypi.org/project/zipp
+
+ .. image:: https://img.shields.io/pypi/pyversions/zipp.svg
+
+ .. image:: https://img.shields.io/travis/jaraco/zipp/master.svg
+ :target: https://travis-ci.org/jaraco/zipp
+
+ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/ambv/black
+ :alt: Code style: Black
+
+ .. image:: https://img.shields.io/appveyor/ci/jaraco/zipp/master.svg
+ :target: https://ci.appveyor.com/project/jaraco/zipp/branch/master
+
+ .. .. image:: https://readthedocs.org/projects/zipp/badge/?version=latest
+ .. :target: https://zipp.readthedocs.io/en/latest/?badge=latest
+
+
+ A pathlib-compatible Zipfile object wrapper. A backport of the
+ `Path object <https://docs.python.org/3.8/library/zipfile.html#path-objects>`_.
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Requires-Python: >=2.7
+Provides-Extra: testing
+Provides-Extra: docs
diff --git a/testing/web-platform/tests/tools/third_party/zipp/README.rst b/testing/web-platform/tests/tools/third_party/zipp/README.rst
new file mode 100644
index 0000000000..ce128a32ba
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/README.rst
@@ -0,0 +1,21 @@
+.. image:: https://img.shields.io/pypi/v/zipp.svg
+ :target: https://pypi.org/project/zipp
+
+.. image:: https://img.shields.io/pypi/pyversions/zipp.svg
+
+.. image:: https://img.shields.io/travis/jaraco/zipp/master.svg
+ :target: https://travis-ci.org/jaraco/zipp
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/ambv/black
+ :alt: Code style: Black
+
+.. image:: https://img.shields.io/appveyor/ci/jaraco/zipp/master.svg
+ :target: https://ci.appveyor.com/project/jaraco/zipp/branch/master
+
+.. .. image:: https://readthedocs.org/projects/zipp/badge/?version=latest
+.. :target: https://zipp.readthedocs.io/en/latest/?badge=latest
+
+
+A pathlib-compatible Zipfile object wrapper. A backport of the
+`Path object <https://docs.python.org/3.8/library/zipfile.html#path-objects>`_.
diff --git a/testing/web-platform/tests/tools/third_party/zipp/appveyor.yml b/testing/web-platform/tests/tools/third_party/zipp/appveyor.yml
new file mode 100644
index 0000000000..f35aa27d68
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/appveyor.yml
@@ -0,0 +1,24 @@
+environment:
+
+ APPVEYOR: true
+
+ matrix:
+ - PYTHON: "C:\\Python36-x64"
+ - PYTHON: "C:\\Python27-x64"
+
+install:
+ # symlink python from a directory with a space
+ - "mklink /d \"C:\\Program Files\\Python\" %PYTHON%"
+ - "SET PYTHON=\"C:\\Program Files\\Python\""
+ - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
+
+build: off
+
+cache:
+ - '%LOCALAPPDATA%\pip\Cache'
+
+test_script:
+ - "python -m pip install -U tox tox-venv virtualenv"
+ - "tox"
+
+version: '{build}'
diff --git a/testing/web-platform/tests/tools/third_party/zipp/conftest.py b/testing/web-platform/tests/tools/third_party/zipp/conftest.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/conftest.py
diff --git a/testing/web-platform/tests/tools/third_party/zipp/docs/conf.py b/testing/web-platform/tests/tools/third_party/zipp/docs/conf.py
new file mode 100644
index 0000000000..41b53557fb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/docs/conf.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker']
+
+master_doc = "index"
+
+link_files = {
+ '../CHANGES.rst': dict(
+ using=dict(GH='https://github.com'),
+ replace=[
+ dict(
+ pattern=r'(Issue #|\B#)(?P<issue>\d+)',
+ url='{package_url}/issues/{issue}',
+ ),
+ dict(
+ pattern=r'^(?m)((?P<scm_version>v?\d+(\.\d+){1,2}))\n[-=]+\n',
+ with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n',
+ ),
+ dict(
+ pattern=r'PEP[- ](?P<pep_number>\d+)',
+ url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/',
+ ),
+ ],
+ )
+}
diff --git a/testing/web-platform/tests/tools/third_party/zipp/docs/history.rst b/testing/web-platform/tests/tools/third_party/zipp/docs/history.rst
new file mode 100644
index 0000000000..8e217503ba
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/docs/history.rst
@@ -0,0 +1,8 @@
+:tocdepth: 2
+
+.. _changes:
+
+History
+*******
+
+.. include:: ../CHANGES (links).rst
diff --git a/testing/web-platform/tests/tools/third_party/zipp/docs/index.rst b/testing/web-platform/tests/tools/third_party/zipp/docs/index.rst
new file mode 100644
index 0000000000..ff49bf9dc7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/docs/index.rst
@@ -0,0 +1,22 @@
+Welcome to zipp documentation!
+========================================
+
+.. toctree::
+ :maxdepth: 1
+
+ history
+
+
+.. automodule:: zipp
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/testing/web-platform/tests/tools/third_party/zipp/mypy.ini b/testing/web-platform/tests/tools/third_party/zipp/mypy.ini
new file mode 100644
index 0000000000..976ba02946
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/mypy.ini
@@ -0,0 +1,2 @@
+[mypy]
+ignore_missing_imports = True
diff --git a/testing/web-platform/tests/tools/third_party/zipp/pyproject.toml b/testing/web-platform/tests/tools/third_party/zipp/pyproject.toml
new file mode 100644
index 0000000000..3afc8c33b7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/pyproject.toml
@@ -0,0 +1,6 @@
+[build-system]
+requires = ["setuptools>=34.4", "wheel", "setuptools_scm>=1.15"]
+build-backend = "setuptools.build_meta"
+
+[tool.black]
+skip-string-normalization = true
diff --git a/testing/web-platform/tests/tools/third_party/zipp/pytest.ini b/testing/web-platform/tests/tools/third_party/zipp/pytest.ini
new file mode 100644
index 0000000000..d7f0b11559
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/pytest.ini
@@ -0,0 +1,9 @@
+[pytest]
+norecursedirs=dist build .tox .eggs
+addopts=--doctest-modules
+doctest_optionflags=ALLOW_UNICODE ELLIPSIS
+# workaround for warning pytest-dev/pytest#6178
+junit_family=xunit2
+filterwarnings=
+ # https://github.com/pytest-dev/pytest/issues/6928
+ ignore:direct construction of .*Item has been deprecated:DeprecationWarning
diff --git a/testing/web-platform/tests/tools/third_party/zipp/setup.cfg b/testing/web-platform/tests/tools/third_party/zipp/setup.cfg
new file mode 100644
index 0000000000..ef4abd248a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/setup.cfg
@@ -0,0 +1,45 @@
+[bdist_wheel]
+universal = 1
+
+[metadata]
+license_file = LICENSE
+name = zipp
+author = Jason R. Coombs
+author_email = jaraco@jaraco.com
+description = Backport of pathlib-compatible object wrapper for zip files
+long_description = file:README.rst
+url = https://github.com/jaraco/zipp
+classifiers =
+ Development Status :: 5 - Production/Stable
+ Intended Audience :: Developers
+ License :: OSI Approved :: MIT License
+ Programming Language :: Python :: 2.7
+ Programming Language :: Python :: 3
+
+[options]
+py_modules = zipp
+packages = find:
+include_package_data = true
+python_requires = >=2.7
+install_requires =
+ contextlib2; python_version < "3.4"
+setup_requires = setuptools_scm >= 1.15.0
+
+[options.extras_require]
+testing =
+
+ pathlib2
+ unittest2
+ jaraco.itertools
+ func-timeout
+docs =
+ sphinx
+ jaraco.packaging >= 3.2
+ rst.linker >= 1.9
+
+[options.entry_points]
+
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/testing/web-platform/tests/tools/third_party/zipp/setup.py b/testing/web-platform/tests/tools/third_party/zipp/setup.py
new file mode 100644
index 0000000000..827e955fcd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/setup.py
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+
+import setuptools
+
+if __name__ == "__main__":
+ setuptools.setup(use_scm_version=True)
diff --git a/testing/web-platform/tests/tools/third_party/zipp/skeleton.md b/testing/web-platform/tests/tools/third_party/zipp/skeleton.md
new file mode 100644
index 0000000000..52b97f09b4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/skeleton.md
@@ -0,0 +1,137 @@
+# Overview
+
+This project is merged with [skeleton](https://github.com/jaraco/skeleton). What is skeleton? It's the scaffolding of a Python project jaraco [introduced in his blog](https://blog.jaraco.com/a-project-skeleton-for-python-projects/). It seeks to provide a means to re-use techniques and inherit advances when managing projects for distribution.
+
+## An SCM Managed Approach
+
+While maintaining dozens of projects in PyPI, jaraco derives best practices for project distribution and publishes them in the [skeleton repo](https://github.com/jaraco/skeleton), a git repo capturing the evolution and culmination of these best practices.
+
+It's intended to be used by a new or existing project to adopt these practices and honed and proven techniques. Adopters are encouraged to use the project directly and maintain a small deviation from the technique, make their own fork for more substantial changes unique to their environment or preferences, or simply adopt the skeleton once and abandon it thereafter.
+
+The primary advantage to using an SCM for maintaining these techniques is that those tools help facilitate the merge between the template and its adopting projects.
+
+Another advantage to using an SCM-managed approach is that tools like GitHub recognize that a change in the skeleton is the _same change_ across all projects that merge with that skeleton. Without the ancestry, with a traditional copy/paste approach, a [commit like this](https://github.com/jaraco/skeleton/commit/12eed1326e1bc26ce256e7b3f8cd8d3a5beab2d5) would produce notifications in the upstream project issue for each and every application, but because it's centralized, GitHub provides just the one notification when the change is added to the skeleton.
+
+# Usage
+
+## new projects
+
+To use skeleton for a new project, simply pull the skeleton into a new project:
+
+```
+$ git init my-new-project
+$ cd my-new-project
+$ git pull gh://jaraco/skeleton
+```
+
+Now customize the project to suit your individual project needs.
+
+## existing projects
+
+If you have an existing project, you can still incorporate the skeleton by merging it into the codebase.
+
+```
+$ git merge skeleton --allow-unrelated-histories
+```
+
+The `--allow-unrelated-histories` is necessary because the history from the skeleton was previously unrelated to the existing codebase. Resolve any merge conflicts and commit to the master, and now the project is based on the shared skeleton.
+
+## Updating
+
+Whenever a change is needed or desired for the general technique for packaging, it can be made in the skeleton project and then merged into each of the derived projects as needed, recommended before each release. As a result, features and best practices for packaging are centrally maintained and readily trickle into a whole suite of packages. This technique lowers the amount of tedious work necessary to create or maintain a project, and coupled with other techniques like continuous integration and deployment, lowers the cost of creating and maintaining refined Python projects to just a few, familiar git operations.
+
+Thereafter, the target project can make whatever customizations it deems relevant to the scaffolding. The project may even at some point decide that the divergence is too great to merit renewed merging with the original skeleton. This approach applies maximal guidance while creating minimal constraints.
+
+# Features
+
+The features/techniques employed by the skeleton include:
+
+- PEP 517/518 based build relying on setuptools as the build tool
+- setuptools declarative configuration using setup.cfg
+- tox for running tests
+- A README.rst as reStructuredText with some popular badges, but with readthedocs and appveyor badges commented out
+- A CHANGES.rst file intended for publishing release notes about the project
+- Use of [black](https://black.readthedocs.io/en/stable/) for code formatting (disabled on unsupported Python 3.5 and earlier)
+
+## Packaging Conventions
+
+A pyproject.toml is included to enable PEP 517 and PEP 518 compatibility and declares the requirements necessary to build the project on setuptools (a minimum version compatible with setup.cfg declarative config).
+
+The setup.cfg file implements the following features:
+
+- Assumes universal wheel for release
+- Advertises the project's LICENSE file (MIT by default)
+- Reads the README.rst file into the long description
+- Some common Trove classifiers
+- Includes all packages discovered in the repo
+- Data files in the package are also included (not just Python files)
+- Declares the required Python versions
+- Declares install requirements (empty by default)
+- Declares setup requirements for legacy environments
+- Supplies two 'extras':
+ - testing: requirements for running tests
+ - docs: requirements for building docs
+ - these extras split the declaration into "upstream" (requirements as declared by the skeleton) and "local" (those specific to the local project); these markers help avoid merge conflicts
+- Placeholder for defining entry points
+
+Additionally, the setup.py file declares `use_scm_version` which relies on [setuptools_scm](https://pypi.org/project/setuptools_scm) to do two things:
+
+- derive the project version from SCM tags
+- ensure that all files committed to the repo are automatically included in releases
+
+## Running Tests
+
+The skeleton assumes the developer has [tox](https://pypi.org/project/tox) installed. The developer is expected to run `tox` to run tests on the current Python version using [pytest](https://pypi.org/project/pytest).
+
+Other environments (invoked with `tox -e {name}`) supplied include:
+
+ - a `build-docs` environment to build the documentation
+ - a `release` environment to publish the package to PyPI
+
+A pytest.ini is included to define common options around running tests. In particular:
+
+- rely on default test discovery in the current directory
+- avoid recursing into common directories not containing tests
+- run doctests on modules and invoke flake8 tests
+- in doctests, allow unicode literals and regular literals to match, allowing for doctests to run on Python 2 and 3. Also enable ELLIPSES, a default that would be undone by supplying the prior option.
+- filters out known warnings caused by libraries/functionality included by the skeleton
+
+Relies a .flake8 file to correct some default behaviors:
+
+- disable mutually incompatible rules W503 and W504
+- support for black format
+
+## Continuous Integration
+
+The project is pre-configured to run tests in [Travis-CI](https://travis-ci.org) (.travis.yml). Any new project must be enabled either through their web site or with the `travis enable` command.
+
+Features include:
+- test against Python 2 and 3
+- run on Ubuntu Xenial
+- correct for broken IPv6
+
+Also provided is a minimal template for running under Appveyor (Windows).
+
+### Continuous Deployments
+
+In addition to running tests, an additional deploy stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with Travis as the TWINE_PASSWORD environment variable. After the Travis project is created, configure the token through the web UI or with a command like the following (bash syntax):
+
+```
+TWINE_PASSWORD={token} travis env copy TWINE_PASSWORD
+```
+
+## Building Documentation
+
+Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e build-docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`.
+
+In addition to building the sphinx docs scaffolded in `docs/`, the docs build a `history.html` file that first injects release dates and hyperlinks into the CHANGES.rst before incorporating it as history in the docs.
+
+## Cutting releases
+
+By default, tagged commits are released through the continuous integration deploy stage.
+
+Releases may also be cut manually by invoking the tox environment `release` with the PyPI token set as the TWINE_PASSWORD:
+
+```
+TWINE_PASSWORD={token} tox -e release
+```
diff --git a/testing/web-platform/tests/tools/third_party/zipp/test_zipp.py b/testing/web-platform/tests/tools/third_party/zipp/test_zipp.py
new file mode 100644
index 0000000000..810d10bd68
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/test_zipp.py
@@ -0,0 +1,245 @@
+# coding: utf-8
+
+from __future__ import division, unicode_literals
+
+import io
+import zipfile
+import contextlib
+import tempfile
+import shutil
+import string
+
+try:
+ import pathlib
+except ImportError:
+ import pathlib2 as pathlib
+
+if not hasattr(contextlib, 'ExitStack'):
+ import contextlib2
+ contextlib.ExitStack = contextlib2.ExitStack
+
+try:
+ import unittest
+
+ unittest.TestCase.subTest
+except AttributeError:
+ import unittest2 as unittest
+
+import jaraco.itertools
+import func_timeout
+
+import zipp
+
+__metaclass__ = type
+consume = tuple
+
+
+def add_dirs(zf):
+ """
+ Given a writable zip file zf, inject directory entries for
+ any directories implied by the presence of children.
+ """
+ for name in zipp.CompleteDirs._implied_dirs(zf.namelist()):
+ zf.writestr(name, b"")
+ return zf
+
+
+def build_alpharep_fixture():
+ """
+ Create a zip file with this structure:
+
+ .
+ ├── a.txt
+ ├── b
+ │ ├── c.txt
+ │ ├── d
+ │ │ └── e.txt
+ │ └── f.txt
+ └── g
+ └── h
+ └── i.txt
+
+ This fixture has the following key characteristics:
+
+ - a file at the root (a)
+ - a file two levels deep (b/d/e)
+ - multiple files in a directory (b/c, b/f)
+ - a directory containing only a directory (g/h)
+
+ "alpha" because it uses alphabet
+ "rep" because it's a representative example
+ """
+ data = io.BytesIO()
+ zf = zipfile.ZipFile(data, "w")
+ zf.writestr("a.txt", b"content of a")
+ zf.writestr("b/c.txt", b"content of c")
+ zf.writestr("b/d/e.txt", b"content of e")
+ zf.writestr("b/f.txt", b"content of f")
+ zf.writestr("g/h/i.txt", b"content of i")
+ zf.filename = "alpharep.zip"
+ return zf
+
+
+@contextlib.contextmanager
+def temp_dir():
+ tmpdir = tempfile.mkdtemp()
+ try:
+ yield pathlib.Path(tmpdir)
+ finally:
+ shutil.rmtree(tmpdir)
+
+
+class TestPath(unittest.TestCase):
+ def setUp(self):
+ self.fixtures = contextlib.ExitStack()
+ self.addCleanup(self.fixtures.close)
+
+ def zipfile_alpharep(self):
+ with self.subTest():
+ yield build_alpharep_fixture()
+ with self.subTest():
+ yield add_dirs(build_alpharep_fixture())
+
+ def zipfile_ondisk(self):
+ tmpdir = pathlib.Path(self.fixtures.enter_context(temp_dir()))
+ for alpharep in self.zipfile_alpharep():
+ buffer = alpharep.fp
+ alpharep.close()
+ path = tmpdir / alpharep.filename
+ with path.open("wb") as strm:
+ strm.write(buffer.getvalue())
+ yield path
+
+ def test_iterdir_and_types(self):
+ for alpharep in self.zipfile_alpharep():
+ root = zipp.Path(alpharep)
+ assert root.is_dir()
+ a, b, g = root.iterdir()
+ assert a.is_file()
+ assert b.is_dir()
+ assert g.is_dir()
+ c, f, d = b.iterdir()
+ assert c.is_file() and f.is_file()
+ e, = d.iterdir()
+ assert e.is_file()
+ h, = g.iterdir()
+ i, = h.iterdir()
+ assert i.is_file()
+
+ def test_open(self):
+ for alpharep in self.zipfile_alpharep():
+ root = zipp.Path(alpharep)
+ a, b, g = root.iterdir()
+ with a.open() as strm:
+ data = strm.read()
+ assert data == "content of a"
+
+ def test_read(self):
+ for alpharep in self.zipfile_alpharep():
+ root = zipp.Path(alpharep)
+ a, b, g = root.iterdir()
+ assert a.read_text() == "content of a"
+ assert a.read_bytes() == b"content of a"
+
+ def test_joinpath(self):
+ for alpharep in self.zipfile_alpharep():
+ root = zipp.Path(alpharep)
+ a = root.joinpath("a")
+ assert a.is_file()
+ e = root.joinpath("b").joinpath("d").joinpath("e.txt")
+ assert e.read_text() == "content of e"
+
+ def test_traverse_truediv(self):
+ for alpharep in self.zipfile_alpharep():
+ root = zipp.Path(alpharep)
+ a = root / "a"
+ assert a.is_file()
+ e = root / "b" / "d" / "e.txt"
+ assert e.read_text() == "content of e"
+
+ def test_traverse_simplediv(self):
+ """
+ Disable the __future__.division when testing traversal.
+ """
+ for alpharep in self.zipfile_alpharep():
+ code = compile(
+ source="zipp.Path(alpharep) / 'a'",
+ filename="(test)",
+ mode="eval",
+ dont_inherit=True,
+ )
+ eval(code)
+
+ def test_pathlike_construction(self):
+ """
+ zipp.Path should be constructable from a path-like object
+ """
+ for zipfile_ondisk in self.zipfile_ondisk():
+ pathlike = pathlib.Path(str(zipfile_ondisk))
+ zipp.Path(pathlike)
+
+ def test_traverse_pathlike(self):
+ for alpharep in self.zipfile_alpharep():
+ root = zipp.Path(alpharep)
+ root / pathlib.Path("a")
+
+ def test_parent(self):
+ for alpharep in self.zipfile_alpharep():
+ root = zipp.Path(alpharep)
+ assert (root / 'a').parent.at == ''
+ assert (root / 'a' / 'b').parent.at == 'a/'
+
+ def test_dir_parent(self):
+ for alpharep in self.zipfile_alpharep():
+ root = zipp.Path(alpharep)
+ assert (root / 'b').parent.at == ''
+ assert (root / 'b/').parent.at == ''
+
+ def test_missing_dir_parent(self):
+ for alpharep in self.zipfile_alpharep():
+ root = zipp.Path(alpharep)
+ assert (root / 'missing dir/').parent.at == ''
+
+ def test_mutability(self):
+ """
+ If the underlying zipfile is changed, the Path object should
+ reflect that change.
+ """
+ for alpharep in self.zipfile_alpharep():
+ root = zipp.Path(alpharep)
+ a, b, g = root.iterdir()
+ alpharep.writestr('foo.txt', b'foo')
+ alpharep.writestr('bar/baz.txt', b'baz')
+ assert any(
+ child.name == 'foo.txt'
+ for child in root.iterdir())
+ assert (root / 'foo.txt').read_text() == 'foo'
+ baz, = (root / 'bar').iterdir()
+ assert baz.read_text() == 'baz'
+
+ HUGE_ZIPFILE_NUM_ENTRIES = 2 ** 13
+
+ def huge_zipfile(self):
+ """Create a read-only zipfile with a huge number of entries entries."""
+ strm = io.BytesIO()
+ zf = zipfile.ZipFile(strm, "w")
+ for entry in map(str, range(self.HUGE_ZIPFILE_NUM_ENTRIES)):
+ zf.writestr(entry, entry)
+ zf.mode = 'r'
+ return zf
+
+ def test_joinpath_constant_time(self):
+ """
+ Ensure joinpath on items in zipfile is linear time.
+ """
+ root = zipp.Path(self.huge_zipfile())
+ entries = jaraco.itertools.Counter(root.iterdir())
+ for entry in entries:
+ entry.joinpath('suffix')
+ # Check the file iterated all items
+ assert entries.count == self.HUGE_ZIPFILE_NUM_ENTRIES
+
+ @func_timeout.func_set_timeout(3)
+ def test_implied_dirs_performance(self):
+ data = ['/'.join(string.ascii_lowercase + str(n)) for n in range(10000)]
+ zipp.CompleteDirs._implied_dirs(data)
diff --git a/testing/web-platform/tests/tools/third_party/zipp/tox.ini b/testing/web-platform/tests/tools/third_party/zipp/tox.ini
new file mode 100644
index 0000000000..cb542c136c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/tox.ini
@@ -0,0 +1,36 @@
+[tox]
+envlist = python
+minversion = 3.2
+# https://github.com/jaraco/skeleton/issues/6
+tox_pip_extensions_ext_venv_update = true
+
+[testenv]
+deps =
+ setuptools>=31.0.1
+commands =
+ python -m unittest discover
+usedevelop = True
+extras = testing
+
+[testenv:build-docs]
+extras =
+ docs
+ testing
+changedir = docs
+commands =
+ python -m sphinx . {toxinidir}/build/html
+
+[testenv:release]
+skip_install = True
+deps =
+ pep517>=0.5
+ twine>=1.13
+ path.py
+passenv =
+ TWINE_PASSWORD
+setenv =
+ TWINE_USERNAME = {env:TWINE_USERNAME:__token__}
+commands =
+ python -c "import path; path.Path('dist').rmtree_p()"
+ python -m pep517.build .
+ python -m twine upload dist/*
diff --git a/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/PKG-INFO b/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/PKG-INFO
new file mode 100644
index 0000000000..33ef1cf01c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/PKG-INFO
@@ -0,0 +1,39 @@
+Metadata-Version: 2.1
+Name: zipp
+Version: 1.2.0
+Summary: Backport of pathlib-compatible object wrapper for zip files
+Home-page: https://github.com/jaraco/zipp
+Author: Jason R. Coombs
+Author-email: jaraco@jaraco.com
+License: UNKNOWN
+Description: .. image:: https://img.shields.io/pypi/v/zipp.svg
+ :target: https://pypi.org/project/zipp
+
+ .. image:: https://img.shields.io/pypi/pyversions/zipp.svg
+
+ .. image:: https://img.shields.io/travis/jaraco/zipp/master.svg
+ :target: https://travis-ci.org/jaraco/zipp
+
+ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/ambv/black
+ :alt: Code style: Black
+
+ .. image:: https://img.shields.io/appveyor/ci/jaraco/zipp/master.svg
+ :target: https://ci.appveyor.com/project/jaraco/zipp/branch/master
+
+ .. .. image:: https://readthedocs.org/projects/zipp/badge/?version=latest
+ .. :target: https://zipp.readthedocs.io/en/latest/?badge=latest
+
+
+ A pathlib-compatible Zipfile object wrapper. A backport of the
+ `Path object <https://docs.python.org/3.8/library/zipfile.html#path-objects>`_.
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Requires-Python: >=2.7
+Provides-Extra: testing
+Provides-Extra: docs
diff --git a/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/SOURCES.txt b/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/SOURCES.txt
new file mode 100644
index 0000000000..845b342cef
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/SOURCES.txt
@@ -0,0 +1,24 @@
+.flake8
+.pre-commit-config.yaml
+.readthedocs.yml
+.travis.yml
+CHANGES.rst
+LICENSE
+README.rst
+appveyor.yml
+conftest.py
+pyproject.toml
+setup.cfg
+setup.py
+skeleton.md
+test_zipp.py
+tox.ini
+zipp.py
+docs/conf.py
+docs/history.rst
+docs/index.rst
+zipp.egg-info/PKG-INFO
+zipp.egg-info/SOURCES.txt
+zipp.egg-info/dependency_links.txt
+zipp.egg-info/requires.txt
+zipp.egg-info/top_level.txt \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/dependency_links.txt b/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/dependency_links.txt
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/requires.txt b/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/requires.txt
new file mode 100644
index 0000000000..90bab46ac4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/requires.txt
@@ -0,0 +1,14 @@
+
+[:python_version < "3.4"]
+contextlib2
+
+[docs]
+sphinx
+jaraco.packaging>=3.2
+rst.linker>=1.9
+
+[testing]
+pathlib2
+unittest2
+jaraco.itertools
+func-timeout
diff --git a/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/top_level.txt b/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/top_level.txt
new file mode 100644
index 0000000000..e82f676f82
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/zipp.egg-info/top_level.txt
@@ -0,0 +1 @@
+zipp
diff --git a/testing/web-platform/tests/tools/third_party/zipp/zipp.py b/testing/web-platform/tests/tools/third_party/zipp/zipp.py
new file mode 100644
index 0000000000..892205834a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/zipp/zipp.py
@@ -0,0 +1,286 @@
+# coding: utf-8
+
+from __future__ import division
+
+import io
+import sys
+import posixpath
+import zipfile
+import functools
+import itertools
+from collections import OrderedDict
+
+try:
+ from contextlib import suppress
+except ImportError:
+ from contextlib2 import suppress
+
+__metaclass__ = type
+
+
+def _parents(path):
+ """
+ Given a path with elements separated by
+ posixpath.sep, generate all parents of that path.
+
+ >>> list(_parents('b/d'))
+ ['b']
+ >>> list(_parents('/b/d/'))
+ ['/b']
+ >>> list(_parents('b/d/f/'))
+ ['b/d', 'b']
+ >>> list(_parents('b'))
+ []
+ >>> list(_parents(''))
+ []
+ """
+ return itertools.islice(_ancestry(path), 1, None)
+
+
+def _ancestry(path):
+ """
+ Given a path with elements separated by
+ posixpath.sep, generate all elements of that path
+
+ >>> list(_ancestry('b/d'))
+ ['b/d', 'b']
+ >>> list(_ancestry('/b/d/'))
+ ['/b/d', '/b']
+ >>> list(_ancestry('b/d/f/'))
+ ['b/d/f', 'b/d', 'b']
+ >>> list(_ancestry('b'))
+ ['b']
+ >>> list(_ancestry(''))
+ []
+ """
+ path = path.rstrip(posixpath.sep)
+ while path and path != posixpath.sep:
+ yield path
+ path, tail = posixpath.split(path)
+
+
+class CompleteDirs(zipfile.ZipFile):
+ """
+ A ZipFile subclass that ensures that implied directories
+ are always included in the namelist.
+ """
+
+ @staticmethod
+ def _implied_dirs(names):
+ parents = itertools.chain.from_iterable(map(_parents, names))
+ # Cast names to a set for O(1) lookups
+ existing = set(names)
+ # Deduplicate entries in original order
+ implied_dirs = OrderedDict.fromkeys(
+ p + posixpath.sep for p in parents
+ if p + posixpath.sep not in existing
+ )
+ return implied_dirs
+
+ def namelist(self):
+ names = super(CompleteDirs, self).namelist()
+ return names + list(self._implied_dirs(names))
+
+ def _name_set(self):
+ return set(self.namelist())
+
+ def resolve_dir(self, name):
+ """
+ If the name represents a directory, return that name
+ as a directory (with the trailing slash).
+ """
+ names = self._name_set()
+ dirname = name + '/'
+ dir_match = name not in names and dirname in names
+ return dirname if dir_match else name
+
+ @classmethod
+ def make(cls, source):
+ """
+ Given a source (filename or zipfile), return an
+ appropriate CompleteDirs subclass.
+ """
+ if isinstance(source, CompleteDirs):
+ return source
+
+ if not isinstance(source, zipfile.ZipFile):
+ return cls(_pathlib_compat(source))
+
+ # Only allow for FastPath when supplied zipfile is read-only
+ if 'r' not in source.mode:
+ cls = CompleteDirs
+
+ res = cls.__new__(cls)
+ vars(res).update(vars(source))
+ return res
+
+
+class FastLookup(CompleteDirs):
+ """
+ ZipFile subclass to ensure implicit
+ dirs exist and are resolved rapidly.
+ """
+ def namelist(self):
+ with suppress(AttributeError):
+ return self.__names
+ self.__names = super(FastLookup, self).namelist()
+ return self.__names
+
+ def _name_set(self):
+ with suppress(AttributeError):
+ return self.__lookup
+ self.__lookup = super(FastLookup, self)._name_set()
+ return self.__lookup
+
+
+def _pathlib_compat(path):
+ """
+ For path-like objects, convert to a filename for compatibility
+ on Python 3.6.1 and earlier.
+ """
+ try:
+ return path.__fspath__()
+ except AttributeError:
+ return str(path)
+
+
+class Path:
+ """
+ A pathlib-compatible interface for zip files.
+
+ Consider a zip file with this structure::
+
+ .
+ ├── a.txt
+ └── b
+ ├── c.txt
+ └── d
+ └── e.txt
+
+ >>> data = io.BytesIO()
+ >>> zf = zipfile.ZipFile(data, 'w')
+ >>> zf.writestr('a.txt', 'content of a')
+ >>> zf.writestr('b/c.txt', 'content of c')
+ >>> zf.writestr('b/d/e.txt', 'content of e')
+ >>> zf.filename = 'abcde.zip'
+
+ Path accepts the zipfile object itself or a filename
+
+ >>> root = Path(zf)
+
+ From there, several path operations are available.
+
+ Directory iteration (including the zip file itself):
+
+ >>> a, b = root.iterdir()
+ >>> a
+ Path('abcde.zip', 'a.txt')
+ >>> b
+ Path('abcde.zip', 'b/')
+
+ name property:
+
+ >>> b.name
+ 'b'
+
+ join with divide operator:
+
+ >>> c = b / 'c.txt'
+ >>> c
+ Path('abcde.zip', 'b/c.txt')
+ >>> c.name
+ 'c.txt'
+
+ Read text:
+
+ >>> c.read_text()
+ 'content of c'
+
+ existence:
+
+ >>> c.exists()
+ True
+ >>> (b / 'missing.txt').exists()
+ False
+
+ Coercion to string:
+
+ >>> str(c)
+ 'abcde.zip/b/c.txt'
+ """
+
+ __repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})"
+
+ def __init__(self, root, at=""):
+ self.root = FastLookup.make(root)
+ self.at = at
+
+ def open(self, mode='r', *args, **kwargs):
+ """
+ Open this entry as text or binary following the semantics
+ of ``pathlib.Path.open()`` by passing arguments through
+ to io.TextIOWrapper().
+ """
+ pwd = kwargs.pop('pwd', None)
+ zip_mode = mode[0]
+ stream = self.root.open(self.at, zip_mode, pwd=pwd)
+ if 'b' in mode:
+ if args or kwargs:
+ raise ValueError("encoding args invalid for binary operation")
+ return stream
+ return io.TextIOWrapper(stream, *args, **kwargs)
+
+ @property
+ def name(self):
+ return posixpath.basename(self.at.rstrip("/"))
+
+ def read_text(self, *args, **kwargs):
+ with self.open('r', *args, **kwargs) as strm:
+ return strm.read()
+
+ def read_bytes(self):
+ with self.open('rb') as strm:
+ return strm.read()
+
+ def _is_child(self, path):
+ return posixpath.dirname(path.at.rstrip("/")) == self.at.rstrip("/")
+
+ def _next(self, at):
+ return Path(self.root, at)
+
+ def is_dir(self):
+ return not self.at or self.at.endswith("/")
+
+ def is_file(self):
+ return not self.is_dir()
+
+ def exists(self):
+ return self.at in self.root._name_set()
+
+ def iterdir(self):
+ if not self.is_dir():
+ raise ValueError("Can't listdir a file")
+ subs = map(self._next, self.root.namelist())
+ return filter(self._is_child, subs)
+
+ def __str__(self):
+ return posixpath.join(self.root.filename, self.at)
+
+ def __repr__(self):
+ return self.__repr.format(self=self)
+
+ def joinpath(self, add):
+ next = posixpath.join(self.at, _pathlib_compat(add))
+ return self._next(self.root.resolve_dir(next))
+
+ __truediv__ = joinpath
+
+ @property
+ def parent(self):
+ parent_at = posixpath.dirname(self.at.rstrip('/'))
+ if parent_at:
+ parent_at += '/'
+ return self._next(parent_at)
+
+ if sys.version_info < (3,):
+ __div__ = __truediv__